summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-05-21 20:56:19 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-05-21 20:56:19 +0000
commit0b6210cd37b68b94252cb798598b12974a20e1c1 (patch)
treee371686554a877842d95aa94f100bee552ff2a8e
parentInitial commit. (diff)
downloadnode-undici-upstream.tar.xz
node-undici-upstream.zip
Adding upstream version 5.28.2+dfsg1+~cs23.11.12.3.upstream/5.28.2+dfsg1+_cs23.11.12.3upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
-rw-r--r--.dockerignore8
-rw-r--r--.editorconfig9
-rw-r--r--.github/ISSUE_TEMPLATE/bug-report.md34
-rw-r--r--.github/ISSUE_TEMPLATE/feature-request.md28
-rw-r--r--.github/PULL_REQUEST_TEMPLATE.md53
-rw-r--r--.github/dependabot.yml23
-rw-r--r--.github/workflows/bench.yml43
-rw-r--r--.github/workflows/codeql.yml78
-rw-r--r--.github/workflows/dependency-review.yml27
-rw-r--r--.github/workflows/fuzz.yml39
-rw-r--r--.github/workflows/lint.yml17
-rw-r--r--.github/workflows/nodejs.yml45
-rw-r--r--.github/workflows/publish-undici-types.yml26
-rw-r--r--.github/workflows/scorecard.yml56
-rw-r--r--.gitignore81
-rwxr-xr-x.husky/pre-commit4
-rw-r--r--.nojekyll0
-rw-r--r--.npmignore2
-rw-r--r--.taprc7
-rw-r--r--CNAME1
-rw-r--r--CODE_OF_CONDUCT.md6
-rw-r--r--CONTRIBUTING.md201
-rw-r--r--GOVERNANCE.md136
-rw-r--r--LICENSE21
-rw-r--r--MAINTAINERS.md33
-rw-r--r--README.md443
-rw-r--r--SECURITY.md2
-rw-r--r--benchmarks/benchmark-http2.js306
-rw-r--r--benchmarks/benchmark-https.js319
-rw-r--r--benchmarks/benchmark.js300
-rw-r--r--benchmarks/server-http2.js49
-rw-r--r--benchmarks/server-https.js41
-rw-r--r--benchmarks/server.js33
-rw-r--r--benchmarks/wait.js22
-rw-r--r--binary-search/.gitignore1
-rw-r--r--binary-search/.travis.yml6
-rw-r--r--binary-search/README.md46
-rw-r--r--binary-search/binary-search.d.ts22
-rw-r--r--binary-search/index.js45
-rw-r--r--binary-search/package.json28
-rw-r--r--binary-search/test.js46
-rw-r--r--build/Dockerfile18
-rw-r--r--build/wasm.js101
-rw-r--r--docs/api/Agent.md80
-rw-r--r--docs/api/BalancedPool.md99
-rw-r--r--docs/api/CacheStorage.md30
-rw-r--r--docs/api/Client.md273
-rw-r--r--docs/api/Connector.md115
-rw-r--r--docs/api/ContentType.md57
-rw-r--r--docs/api/Cookies.md101
-rw-r--r--docs/api/DiagnosticsChannel.md204
-rw-r--r--docs/api/DispatchInterceptor.md60
-rw-r--r--docs/api/Dispatcher.md887
-rw-r--r--docs/api/Errors.md47
-rw-r--r--docs/api/Fetch.md27
-rw-r--r--docs/api/MockAgent.md540
-rw-r--r--docs/api/MockClient.md77
-rw-r--r--docs/api/MockErrors.md12
-rw-r--r--docs/api/MockPool.md547
-rw-r--r--docs/api/Pool.md84
-rw-r--r--docs/api/PoolStats.md35
-rw-r--r--docs/api/ProxyAgent.md126
-rw-r--r--docs/api/RetryHandler.md108
-rw-r--r--docs/api/WebSocket.md43
-rw-r--r--docs/api/api-lifecycle.md62
-rw-r--r--docs/assets/lifecycle-diagram.pngbin0 -> 47090 bytes
-rw-r--r--docs/best-practices/client-certificate.md64
-rw-r--r--docs/best-practices/mocking-request.md136
-rw-r--r--docs/best-practices/proxy.md127
-rw-r--r--docs/best-practices/writing-tests.md20
-rw-r--r--docsify/sidebar.md28
-rw-r--r--examples/ca-fingerprint/index.js80
-rw-r--r--examples/fetch.js13
-rw-r--r--examples/proxy-agent.js25
-rw-r--r--examples/proxy/index.js49
-rw-r--r--examples/proxy/proxy.js256
-rw-r--r--examples/request.js18
-rw-r--r--fastify-busboy/.eslintrc.js27
-rw-r--r--fastify-busboy/.gitattributes2
-rw-r--r--fastify-busboy/.github/dependabot.yml13
-rw-r--r--fastify-busboy/.github/workflows/ci.yml22
-rw-r--r--fastify-busboy/.github/workflows/coverage.yml44
-rw-r--r--fastify-busboy/.github/workflows/linting.yml35
-rw-r--r--fastify-busboy/.gitignore152
-rw-r--r--fastify-busboy/.taprc4
-rw-r--r--fastify-busboy/CHANGELOG.md28
-rw-r--r--fastify-busboy/LICENSE19
-rw-r--r--fastify-busboy/README.md271
-rw-r--r--fastify-busboy/bench/busboy-form-bench-latin1.js32
-rw-r--r--fastify-busboy/bench/busboy-form-bench-utf8.js32
-rw-r--r--fastify-busboy/bench/createMultipartBufferForEncodingBench.js23
-rw-r--r--fastify-busboy/bench/dicer/dicer-bench-multipart-parser.js60
-rw-r--r--fastify-busboy/bench/dicer/formidable-bench-multipart-parser.js71
-rw-r--r--fastify-busboy/bench/dicer/multipartser-bench-multipart-parser.js57
-rw-r--r--fastify-busboy/bench/dicer/multiparty-bench-multipart-parser.js78
-rw-r--r--fastify-busboy/bench/dicer/parted-bench-multipart-parser.js65
-rw-r--r--fastify-busboy/bench/dicer/parted-multipart.js486
-rw-r--r--fastify-busboy/bench/fastify-busboy-form-bench-latin1.js31
-rw-r--r--fastify-busboy/bench/fastify-busboy-form-bench-utf8.js31
-rw-r--r--fastify-busboy/bench/parse-params.js21
-rw-r--r--fastify-busboy/benchmarks/_results/Busboy_comparison-busboy-Node_12.json10
-rw-r--r--fastify-busboy/benchmarks/_results/Busboy_comparison-busboy-Node_16.json10
-rw-r--r--fastify-busboy/benchmarks/_results/Busboy_comparison-fastify-busboy-Node_16.json10
-rw-r--r--fastify-busboy/benchmarks/busboy/contestants/busboy.js40
-rw-r--r--fastify-busboy/benchmarks/busboy/contestants/fastify-busboy.js41
-rw-r--r--fastify-busboy/benchmarks/busboy/data.js34
-rw-r--r--fastify-busboy/benchmarks/busboy/executioner.js50
-rw-r--r--fastify-busboy/benchmarks/busboy/regenerate.cmd17
-rw-r--r--fastify-busboy/benchmarks/busboy/validator.js15
-rw-r--r--fastify-busboy/benchmarks/common/commonBuilder.js46
-rw-r--r--fastify-busboy/benchmarks/common/contestantResolver.js26
-rw-r--r--fastify-busboy/benchmarks/common/executionUtils.js18
-rw-r--r--fastify-busboy/benchmarks/common/resultUtils.js17
-rw-r--r--fastify-busboy/benchmarks/common/resultsCombinator.js54
-rw-r--r--fastify-busboy/benchmarks/package.json21
-rw-r--r--fastify-busboy/deps/dicer/LICENSE19
-rw-r--r--fastify-busboy/deps/dicer/lib/Dicer.js207
-rw-r--r--fastify-busboy/deps/dicer/lib/HeaderParser.js100
-rw-r--r--fastify-busboy/deps/dicer/lib/PartStream.js13
-rw-r--r--fastify-busboy/deps/dicer/lib/dicer.d.ts164
-rw-r--r--fastify-busboy/deps/streamsearch/sbmh.js228
-rw-r--r--fastify-busboy/lib/main.d.ts196
-rw-r--r--fastify-busboy/lib/main.js85
-rw-r--r--fastify-busboy/lib/types/multipart.js306
-rw-r--r--fastify-busboy/lib/types/urlencoded.js190
-rw-r--r--fastify-busboy/lib/utils/Decoder.js54
-rw-r--r--fastify-busboy/lib/utils/basename.js14
-rw-r--r--fastify-busboy/lib/utils/decodeText.js114
-rw-r--r--fastify-busboy/lib/utils/getLimit.js16
-rw-r--r--fastify-busboy/lib/utils/parseParams.js196
-rw-r--r--fastify-busboy/package.json86
-rw-r--r--fastify-busboy/test/busboy-constructor.test.js75
-rw-r--r--fastify-busboy/test/decoder.test.js98
-rw-r--r--fastify-busboy/test/dicer-constructor.test.js22
-rw-r--r--fastify-busboy/test/dicer-endfinish.test.js96
-rw-r--r--fastify-busboy/test/dicer-export.test.js24
-rw-r--r--fastify-busboy/test/dicer-headerparser.test.js192
-rw-r--r--fastify-busboy/test/dicer-malformed-header.test.js29
-rw-r--r--fastify-busboy/test/dicer-multipart-extra-trailer.test.js82
-rw-r--r--fastify-busboy/test/dicer-multipart-nolisteners.test.js44
-rw-r--r--fastify-busboy/test/dicer-multipart.test.js223
-rw-r--r--fastify-busboy/test/fixtures/many-noend/original31
-rw-r--r--fastify-busboy/test/fixtures/many-noend/part11
-rw-r--r--fastify-busboy/test/fixtures/many-noend/part1.header1
-rw-r--r--fastify-busboy/test/fixtures/many-noend/part20
-rw-r--r--fastify-busboy/test/fixtures/many-noend/part2.header1
-rw-r--r--fastify-busboy/test/fixtures/many-noend/part30
-rw-r--r--fastify-busboy/test/fixtures/many-noend/part3.header1
-rw-r--r--fastify-busboy/test/fixtures/many-noend/part40
-rw-r--r--fastify-busboy/test/fixtures/many-noend/part4.header1
-rw-r--r--fastify-busboy/test/fixtures/many-noend/part53
-rw-r--r--fastify-busboy/test/fixtures/many-noend/part5.header1
-rw-r--r--fastify-busboy/test/fixtures/many-noend/part61
-rw-r--r--fastify-busboy/test/fixtures/many-noend/part6.header1
-rw-r--r--fastify-busboy/test/fixtures/many-noend/part7.header2
-rw-r--r--fastify-busboy/test/fixtures/many-wrongboundary/original32
-rw-r--r--fastify-busboy/test/fixtures/many-wrongboundary/preamble33
-rw-r--r--fastify-busboy/test/fixtures/many-wrongboundary/preamble.error1
-rw-r--r--fastify-busboy/test/fixtures/many/original32
-rw-r--r--fastify-busboy/test/fixtures/many/part11
-rw-r--r--fastify-busboy/test/fixtures/many/part1.header1
-rw-r--r--fastify-busboy/test/fixtures/many/part20
-rw-r--r--fastify-busboy/test/fixtures/many/part2.header1
-rw-r--r--fastify-busboy/test/fixtures/many/part30
-rw-r--r--fastify-busboy/test/fixtures/many/part3.header1
-rw-r--r--fastify-busboy/test/fixtures/many/part40
-rw-r--r--fastify-busboy/test/fixtures/many/part4.header1
-rw-r--r--fastify-busboy/test/fixtures/many/part53
-rw-r--r--fastify-busboy/test/fixtures/many/part5.header1
-rw-r--r--fastify-busboy/test/fixtures/many/part60
-rw-r--r--fastify-busboy/test/fixtures/many/part6.header2
-rw-r--r--fastify-busboy/test/fixtures/many/part71
-rw-r--r--fastify-busboy/test/fixtures/many/part7.header1
-rw-r--r--fastify-busboy/test/fixtures/nested-full/original24
-rw-r--r--fastify-busboy/test/fixtures/nested-full/part11
-rw-r--r--fastify-busboy/test/fixtures/nested-full/part1.header1
-rw-r--r--fastify-busboy/test/fixtures/nested-full/part212
-rw-r--r--fastify-busboy/test/fixtures/nested-full/part2.header2
-rw-r--r--fastify-busboy/test/fixtures/nested-full/preamble.header2
-rw-r--r--fastify-busboy/test/fixtures/nested/original21
-rw-r--r--fastify-busboy/test/fixtures/nested/part11
-rw-r--r--fastify-busboy/test/fixtures/nested/part1.header1
-rw-r--r--fastify-busboy/test/fixtures/nested/part212
-rw-r--r--fastify-busboy/test/fixtures/nested/part2.header2
-rw-r--r--fastify-busboy/test/get-limit.test.js34
-rw-r--r--fastify-busboy/test/multipart-stream-pause.test.js82
-rw-r--r--fastify-busboy/test/parse-params.test.js124
-rw-r--r--fastify-busboy/test/streamsearch.test.js396
-rw-r--r--fastify-busboy/test/types-multipart.test.js678
-rw-r--r--fastify-busboy/test/types-urlencoded.test.js210
-rw-r--r--fastify-busboy/test/types/dicer.test-d.ts81
-rw-r--r--fastify-busboy/test/types/main.test-d.ts241
-rw-r--r--fastify-busboy/tsconfig.json30
-rw-r--r--index-fetch.js15
-rw-r--r--index.d.ts3
-rw-r--r--index.html35
-rw-r--r--index.js167
-rw-r--r--lib/agent.js148
-rw-r--r--lib/api/abort-signal.js54
-rw-r--r--lib/api/api-connect.js104
-rw-r--r--lib/api/api-pipeline.js249
-rw-r--r--lib/api/api-request.js180
-rw-r--r--lib/api/api-stream.js220
-rw-r--r--lib/api/api-upgrade.js105
-rw-r--r--lib/api/index.js7
-rw-r--r--lib/api/readable.js322
-rw-r--r--lib/api/util.js46
-rw-r--r--lib/balanced-pool.js190
-rw-r--r--lib/cache/cache.js838
-rw-r--r--lib/cache/cachestorage.js144
-rw-r--r--lib/cache/symbols.js5
-rw-r--r--lib/cache/util.js49
-rw-r--r--lib/client.js2283
-rw-r--r--lib/compat/dispatcher-weakref.js48
-rw-r--r--lib/cookies/constants.js12
-rw-r--r--lib/cookies/index.js184
-rw-r--r--lib/cookies/parse.js317
-rw-r--r--lib/cookies/util.js291
-rw-r--r--lib/core/connect.js189
-rw-r--r--lib/core/errors.js230
-rw-r--r--lib/core/request.js499
-rw-r--r--lib/core/symbols.js63
-rw-r--r--lib/core/util.js511
-rw-r--r--lib/dispatcher-base.js192
-rw-r--r--lib/dispatcher.js19
-rw-r--r--lib/fetch/LICENSE21
-rw-r--r--lib/fetch/body.js605
-rw-r--r--lib/fetch/constants.js151
-rw-r--r--lib/fetch/dataURL.js627
-rw-r--r--lib/fetch/file.js344
-rw-r--r--lib/fetch/formdata.js265
-rw-r--r--lib/fetch/global.js40
-rw-r--r--lib/fetch/headers.js589
-rw-r--r--lib/fetch/index.js2145
-rw-r--r--lib/fetch/request.js946
-rw-r--r--lib/fetch/response.js571
-rw-r--r--lib/fetch/symbols.js10
-rw-r--r--lib/fetch/util.js1071
-rw-r--r--lib/fetch/webidl.js646
-rw-r--r--lib/fileapi/encoding.js290
-rw-r--r--lib/fileapi/filereader.js344
-rw-r--r--lib/fileapi/progressevent.js78
-rw-r--r--lib/fileapi/symbols.js10
-rw-r--r--lib/fileapi/util.js392
-rw-r--r--lib/global.js32
-rw-r--r--lib/handler/DecoratorHandler.js35
-rw-r--r--lib/handler/RedirectHandler.js216
-rw-r--r--lib/handler/RetryHandler.js336
-rw-r--r--lib/interceptor/redirectInterceptor.js21
-rw-r--r--lib/llhttp/constants.d.ts199
-rw-r--r--lib/llhttp/constants.js278
-rw-r--r--lib/llhttp/utils.d.ts4
-rw-r--r--lib/llhttp/utils.js15
-rw-r--r--lib/llhttp/wasm_build_env.txt32
-rw-r--r--lib/mock/mock-agent.js171
-rw-r--r--lib/mock/mock-client.js59
-rw-r--r--lib/mock/mock-errors.js17
-rw-r--r--lib/mock/mock-interceptor.js206
-rw-r--r--lib/mock/mock-pool.js59
-rw-r--r--lib/mock/mock-symbols.js23
-rw-r--r--lib/mock/mock-utils.js351
-rw-r--r--lib/mock/pending-interceptors-formatter.js40
-rw-r--r--lib/mock/pluralizer.js29
-rw-r--r--lib/node/fixed-queue.js117
-rw-r--r--lib/pool-base.js194
-rw-r--r--lib/pool-stats.js34
-rw-r--r--lib/pool.js94
-rw-r--r--lib/proxy-agent.js189
-rw-r--r--lib/timers.js97
-rw-r--r--lib/websocket/connection.js291
-rw-r--r--lib/websocket/constants.js51
-rw-r--r--lib/websocket/events.js303
-rw-r--r--lib/websocket/frame.js73
-rw-r--r--lib/websocket/receiver.js344
-rw-r--r--lib/websocket/symbols.js12
-rw-r--r--lib/websocket/util.js200
-rw-r--r--lib/websocket/websocket.js641
-rw-r--r--llhttp/.dockerignore6
-rw-r--r--llhttp/.eslintrc.js31
-rw-r--r--llhttp/.github/workflows/aiohttp.yml61
-rw-r--r--llhttp/.github/workflows/ci.yaml117
-rw-r--r--llhttp/.gitignore6
-rw-r--r--llhttp/.npmrc1
-rw-r--r--llhttp/CMakeLists.txt117
-rw-r--r--llhttp/CNAME1
-rw-r--r--llhttp/CODE_OF_CONDUCT.md4
-rw-r--r--llhttp/Dockerfile13
-rw-r--r--llhttp/LICENSE-MIT22
-rw-r--r--llhttp/Makefile93
-rw-r--r--llhttp/README.md501
-rw-r--r--llhttp/_config.yml1
-rw-r--r--llhttp/bench/index.ts71
-rw-r--r--llhttp/bin/build_wasm.ts95
-rwxr-xr-xllhttp/bin/generate.ts47
-rw-r--r--llhttp/docs/releasing.md65
-rw-r--r--llhttp/examples/wasm.ts248
-rw-r--r--llhttp/images/http-loose-none.pngbin0 -> 3571702 bytes
-rw-r--r--llhttp/images/http-strict-none.pngbin0 -> 4166480 bytes
-rw-r--r--llhttp/libllhttp.pc.in10
-rw-r--r--llhttp/package-lock.json2995
-rw-r--r--llhttp/package.json60
-rw-r--r--llhttp/src/common.gypi46
-rw-r--r--llhttp/src/llhttp.gyp22
-rw-r--r--llhttp/src/llhttp.ts7
-rw-r--r--llhttp/src/llhttp/c-headers.ts106
-rw-r--r--llhttp/src/llhttp/constants.ts540
-rw-r--r--llhttp/src/llhttp/http.ts1299
-rw-r--r--llhttp/src/llhttp/url.ts220
-rw-r--r--llhttp/src/llhttp/utils.ts27
-rw-r--r--llhttp/src/native/api.c510
-rw-r--r--llhttp/src/native/api.h355
-rw-r--r--llhttp/src/native/http.c170
-rw-r--r--llhttp/test/fixtures/extra.c457
-rw-r--r--llhttp/test/fixtures/index.ts116
-rw-r--r--llhttp/test/fuzzers/fuzz_parser.c45
-rw-r--r--llhttp/test/md-test.ts269
-rw-r--r--llhttp/test/request/connection.md732
-rw-r--r--llhttp/test/request/content-length.md482
-rw-r--r--llhttp/test/request/finish.md69
-rw-r--r--llhttp/test/request/invalid.md607
-rw-r--r--llhttp/test/request/lenient-headers.md145
-rw-r--r--llhttp/test/request/lenient-version.md23
-rw-r--r--llhttp/test/request/method.md450
-rw-r--r--llhttp/test/request/pausing.md381
-rw-r--r--llhttp/test/request/pipelining.md66
-rw-r--r--llhttp/test/request/sample.md629
-rw-r--r--llhttp/test/request/transfer-encoding.md1187
-rw-r--r--llhttp/test/request/uri.md243
-rw-r--r--llhttp/test/response/connection.md647
-rw-r--r--llhttp/test/response/content-length.md158
-rw-r--r--llhttp/test/response/finish.md23
-rw-r--r--llhttp/test/response/invalid.md285
-rw-r--r--llhttp/test/response/lenient-version.md20
-rw-r--r--llhttp/test/response/pausing.md330
-rw-r--r--llhttp/test/response/pipelining.md60
-rw-r--r--llhttp/test/response/sample.md653
-rw-r--r--llhttp/test/response/transfer-encoding.md410
-rw-r--r--llhttp/test/url.md261
-rw-r--r--llhttp/tsconfig.json15
-rw-r--r--llhttp/tslint.json14
-rw-r--r--llparse-builder/.gitignore3
-rw-r--r--llparse-builder/.travis.yml4
-rw-r--r--llparse-builder/README.md32
-rw-r--r--llparse-builder/package-lock.json1466
-rw-r--r--llparse-builder/package.json48
-rw-r--r--llparse-builder/src/builder.ts147
-rw-r--r--llparse-builder/src/code/and.ts7
-rw-r--r--llparse-builder/src/code/base.ts16
-rw-r--r--llparse-builder/src/code/creator.ts184
-rw-r--r--llparse-builder/src/code/field-value.ts9
-rw-r--r--llparse-builder/src/code/field.ts10
-rw-r--r--llparse-builder/src/code/index.ts15
-rw-r--r--llparse-builder/src/code/is-equal.ts7
-rw-r--r--llparse-builder/src/code/load.ts7
-rw-r--r--llparse-builder/src/code/match.ts7
-rw-r--r--llparse-builder/src/code/mul-add.ts28
-rw-r--r--llparse-builder/src/code/or.ts7
-rw-r--r--llparse-builder/src/code/span.ts5
-rw-r--r--llparse-builder/src/code/store.ts7
-rw-r--r--llparse-builder/src/code/test.ts7
-rw-r--r--llparse-builder/src/code/update.ts7
-rw-r--r--llparse-builder/src/code/value.ts7
-rw-r--r--llparse-builder/src/edge.ts54
-rw-r--r--llparse-builder/src/loop-checker/index.ts205
-rw-r--r--llparse-builder/src/loop-checker/lattice.ts115
-rw-r--r--llparse-builder/src/node/base.ts96
-rw-r--r--llparse-builder/src/node/consume.ts19
-rw-r--r--llparse-builder/src/node/error.ts24
-rw-r--r--llparse-builder/src/node/index.ts8
-rw-r--r--llparse-builder/src/node/invoke.ts39
-rw-r--r--llparse-builder/src/node/match.ts162
-rw-r--r--llparse-builder/src/node/pause.ts25
-rw-r--r--llparse-builder/src/node/span-end.ts19
-rw-r--r--llparse-builder/src/node/span-start.ts16
-rw-r--r--llparse-builder/src/property.ts12
-rw-r--r--llparse-builder/src/reachability.ts31
-rw-r--r--llparse-builder/src/span-allocator.ts182
-rw-r--r--llparse-builder/src/span.ts57
-rw-r--r--llparse-builder/src/transform/base.ts12
-rw-r--r--llparse-builder/src/transform/creator.ts28
-rw-r--r--llparse-builder/src/transform/index.ts3
-rw-r--r--llparse-builder/src/transform/to-lower-unsafe.ts7
-rw-r--r--llparse-builder/src/transform/to-lower.ts7
-rw-r--r--llparse-builder/src/utils.ts19
-rw-r--r--llparse-builder/test/builder-test.ts94
-rw-r--r--llparse-builder/test/loop-checker-test.ts118
-rw-r--r--llparse-builder/test/span-allocator-test.ts146
-rw-r--r--llparse-builder/tsconfig.json15
-rw-r--r--llparse-builder/tslint.json14
-rw-r--r--llparse-frontend/.gitignore2
-rw-r--r--llparse-frontend/.travis.yml6
-rw-r--r--llparse-frontend/README.md30
-rw-r--r--llparse-frontend/package-lock.json1516
-rw-r--r--llparse-frontend/package.json43
-rw-r--r--llparse-frontend/src/code/and.ts8
-rw-r--r--llparse-frontend/src/code/base.ts8
-rw-r--r--llparse-frontend/src/code/external.ts7
-rw-r--r--llparse-frontend/src/code/field-value.ts13
-rw-r--r--llparse-frontend/src/code/field.ts8
-rw-r--r--llparse-frontend/src/code/index.ts15
-rw-r--r--llparse-frontend/src/code/is-equal.ts9
-rw-r--r--llparse-frontend/src/code/load.ts7
-rw-r--r--llparse-frontend/src/code/match.ts7
-rw-r--r--llparse-frontend/src/code/mul-add.ts26
-rw-r--r--llparse-frontend/src/code/or.ts8
-rw-r--r--llparse-frontend/src/code/span.ts7
-rw-r--r--llparse-frontend/src/code/store.ts7
-rw-r--r--llparse-frontend/src/code/test.ts8
-rw-r--r--llparse-frontend/src/code/update.ts8
-rw-r--r--llparse-frontend/src/code/value.ts7
-rw-r--r--llparse-frontend/src/container/index.ts84
-rw-r--r--llparse-frontend/src/container/wrap.ts15
-rw-r--r--llparse-frontend/src/enumerator.ts23
-rw-r--r--llparse-frontend/src/frontend.ts513
-rw-r--r--llparse-frontend/src/implementation/code.ts16
-rw-r--r--llparse-frontend/src/implementation/full.ts9
-rw-r--r--llparse-frontend/src/implementation/index.ts4
-rw-r--r--llparse-frontend/src/implementation/node.ts15
-rw-r--r--llparse-frontend/src/implementation/transform.ts9
-rw-r--r--llparse-frontend/src/namespace/frontend.ts5
-rw-r--r--llparse-frontend/src/node/base.ts46
-rw-r--r--llparse-frontend/src/node/consume.ts8
-rw-r--r--llparse-frontend/src/node/empty.ts4
-rw-r--r--llparse-frontend/src/node/error.ts9
-rw-r--r--llparse-frontend/src/node/index.ts13
-rw-r--r--llparse-frontend/src/node/invoke.ts39
-rw-r--r--llparse-frontend/src/node/match.ts11
-rw-r--r--llparse-frontend/src/node/pause.ts4
-rw-r--r--llparse-frontend/src/node/sequence.ts44
-rw-r--r--llparse-frontend/src/node/single.ts46
-rw-r--r--llparse-frontend/src/node/slot.ts20
-rw-r--r--llparse-frontend/src/node/span-end.ts12
-rw-r--r--llparse-frontend/src/node/span-start.ts12
-rw-r--r--llparse-frontend/src/node/table-lookup.ts43
-rw-r--r--llparse-frontend/src/peephole.ts52
-rw-r--r--llparse-frontend/src/span-field.ts8
-rw-r--r--llparse-frontend/src/transform/base.ts4
-rw-r--r--llparse-frontend/src/transform/id.ts7
-rw-r--r--llparse-frontend/src/transform/index.ts4
-rw-r--r--llparse-frontend/src/transform/to-lower-unsafe.ts7
-rw-r--r--llparse-frontend/src/transform/to-lower.ts7
-rw-r--r--llparse-frontend/src/trie/empty.ts9
-rw-r--r--llparse-frontend/src/trie/index.ts136
-rw-r--r--llparse-frontend/src/trie/node.ts2
-rw-r--r--llparse-frontend/src/trie/sequence.ts9
-rw-r--r--llparse-frontend/src/trie/single.ts16
-rw-r--r--llparse-frontend/src/utils/identifier.ts32
-rw-r--r--llparse-frontend/src/utils/index.ts19
-rw-r--r--llparse-frontend/src/wrap.ts3
-rw-r--r--llparse-frontend/test/container-test.ts46
-rw-r--r--llparse-frontend/test/fixtures/a-implementation/code/and.ts8
-rw-r--r--llparse-frontend/test/fixtures/a-implementation/code/base.ts6
-rw-r--r--llparse-frontend/test/fixtures/a-implementation/code/index.ts15
-rw-r--r--llparse-frontend/test/fixtures/a-implementation/code/is-equal.ts8
-rw-r--r--llparse-frontend/test/fixtures/a-implementation/code/load.ts8
-rw-r--r--llparse-frontend/test/fixtures/a-implementation/code/match.ts8
-rw-r--r--llparse-frontend/test/fixtures/a-implementation/code/mul-add.ts8
-rw-r--r--llparse-frontend/test/fixtures/a-implementation/code/or.ts8
-rw-r--r--llparse-frontend/test/fixtures/a-implementation/code/span.ts8
-rw-r--r--llparse-frontend/test/fixtures/a-implementation/code/store.ts8
-rw-r--r--llparse-frontend/test/fixtures/a-implementation/code/test.ts8
-rw-r--r--llparse-frontend/test/fixtures/a-implementation/code/update.ts8
-rw-r--r--llparse-frontend/test/fixtures/a-implementation/code/value.ts8
-rw-r--r--llparse-frontend/test/fixtures/a-implementation/index.ts5
-rw-r--r--llparse-frontend/test/fixtures/a-implementation/node/base.ts38
-rw-r--r--llparse-frontend/test/fixtures/a-implementation/node/consume.ts8
-rw-r--r--llparse-frontend/test/fixtures/a-implementation/node/empty.ts8
-rw-r--r--llparse-frontend/test/fixtures/a-implementation/node/error.ts10
-rw-r--r--llparse-frontend/test/fixtures/a-implementation/node/index.ts15
-rw-r--r--llparse-frontend/test/fixtures/a-implementation/node/invoke.ts8
-rw-r--r--llparse-frontend/test/fixtures/a-implementation/node/pause.ts8
-rw-r--r--llparse-frontend/test/fixtures/a-implementation/node/sequence.ts8
-rw-r--r--llparse-frontend/test/fixtures/a-implementation/node/single.ts18
-rw-r--r--llparse-frontend/test/fixtures/a-implementation/node/span-end.ts8
-rw-r--r--llparse-frontend/test/fixtures/a-implementation/node/span-start.ts8
-rw-r--r--llparse-frontend/test/fixtures/a-implementation/node/table-lookup.ts8
-rw-r--r--llparse-frontend/test/fixtures/a-implementation/transform/base.ts6
-rw-r--r--llparse-frontend/test/fixtures/a-implementation/transform/id.ts8
-rw-r--r--llparse-frontend/test/fixtures/a-implementation/transform/index.ts5
-rw-r--r--llparse-frontend/test/fixtures/a-implementation/transform/to-lower-unsafe.ts8
-rw-r--r--llparse-frontend/test/fixtures/a-implementation/transform/to-lower.ts8
-rw-r--r--llparse-frontend/test/fixtures/implementation/code/and.ts8
-rw-r--r--llparse-frontend/test/fixtures/implementation/code/base.ts6
-rw-r--r--llparse-frontend/test/fixtures/implementation/code/index.ts15
-rw-r--r--llparse-frontend/test/fixtures/implementation/code/is-equal.ts8
-rw-r--r--llparse-frontend/test/fixtures/implementation/code/load.ts8
-rw-r--r--llparse-frontend/test/fixtures/implementation/code/match.ts8
-rw-r--r--llparse-frontend/test/fixtures/implementation/code/mul-add.ts8
-rw-r--r--llparse-frontend/test/fixtures/implementation/code/or.ts8
-rw-r--r--llparse-frontend/test/fixtures/implementation/code/span.ts8
-rw-r--r--llparse-frontend/test/fixtures/implementation/code/store.ts8
-rw-r--r--llparse-frontend/test/fixtures/implementation/code/test.ts8
-rw-r--r--llparse-frontend/test/fixtures/implementation/code/update.ts8
-rw-r--r--llparse-frontend/test/fixtures/implementation/code/value.ts8
-rw-r--r--llparse-frontend/test/fixtures/implementation/index.ts5
-rw-r--r--llparse-frontend/test/fixtures/implementation/node/base.ts39
-rw-r--r--llparse-frontend/test/fixtures/implementation/node/consume.ts8
-rw-r--r--llparse-frontend/test/fixtures/implementation/node/empty.ts8
-rw-r--r--llparse-frontend/test/fixtures/implementation/node/error.ts10
-rw-r--r--llparse-frontend/test/fixtures/implementation/node/index.ts15
-rw-r--r--llparse-frontend/test/fixtures/implementation/node/invoke.ts8
-rw-r--r--llparse-frontend/test/fixtures/implementation/node/pause.ts8
-rw-r--r--llparse-frontend/test/fixtures/implementation/node/sequence.ts15
-rw-r--r--llparse-frontend/test/fixtures/implementation/node/single.ts22
-rw-r--r--llparse-frontend/test/fixtures/implementation/node/span-end.ts8
-rw-r--r--llparse-frontend/test/fixtures/implementation/node/span-start.ts8
-rw-r--r--llparse-frontend/test/fixtures/implementation/node/table-lookup.ts8
-rw-r--r--llparse-frontend/test/fixtures/implementation/transform/base.ts6
-rw-r--r--llparse-frontend/test/fixtures/implementation/transform/id.ts8
-rw-r--r--llparse-frontend/test/fixtures/implementation/transform/index.ts5
-rw-r--r--llparse-frontend/test/fixtures/implementation/transform/to-lower-unsafe.ts8
-rw-r--r--llparse-frontend/test/fixtures/implementation/transform/to-lower.ts8
-rw-r--r--llparse-frontend/test/frontend-test.ts187
-rw-r--r--llparse-frontend/tsconfig.json15
-rw-r--r--llparse-frontend/tslint.json16
-rw-r--r--llparse/.gitignore4
-rw-r--r--llparse/.travis.yml6
-rw-r--r--llparse/CNAME1
-rw-r--r--llparse/CODE_OF_CONDUCT.md4
-rw-r--r--llparse/LICENSE-MIT22
-rw-r--r--llparse/README.md86
-rw-r--r--llparse/_config.yml1
-rw-r--r--llparse/examples/http/.gitignore6
-rw-r--r--llparse/examples/http/Makefile11
-rw-r--r--llparse/examples/http/index.ts51
-rw-r--r--llparse/examples/http/main.c48
-rw-r--r--llparse/package-lock.json1802
-rw-r--r--llparse/package.json49
-rw-r--r--llparse/src/api.ts47
-rw-r--r--llparse/src/compiler/header-builder.ts80
-rw-r--r--llparse/src/compiler/index.ts88
-rw-r--r--llparse/src/implementation/c/code/and.ts11
-rw-r--r--llparse/src/implementation/c/code/base.ts12
-rw-r--r--llparse/src/implementation/c/code/external.ts19
-rw-r--r--llparse/src/implementation/c/code/field.ts28
-rw-r--r--llparse/src/implementation/c/code/index.ts27
-rw-r--r--llparse/src/implementation/c/code/is-equal.ts10
-rw-r--r--llparse/src/implementation/c/code/load.ts10
-rw-r--r--llparse/src/implementation/c/code/mul-add.ts67
-rw-r--r--llparse/src/implementation/c/code/or.ts11
-rw-r--r--llparse/src/implementation/c/code/store.ts11
-rw-r--r--llparse/src/implementation/c/code/test.ts11
-rw-r--r--llparse/src/implementation/c/code/update.ts11
-rw-r--r--llparse/src/implementation/c/compilation.ts336
-rw-r--r--llparse/src/implementation/c/constants.ts45
-rw-r--r--llparse/src/implementation/c/helpers/match-sequence.ts75
-rw-r--r--llparse/src/implementation/c/index.ts199
-rw-r--r--llparse/src/implementation/c/node/base.ts77
-rw-r--r--llparse/src/implementation/c/node/consume.ts48
-rw-r--r--llparse/src/implementation/c/node/empty.ts16
-rw-r--r--llparse/src/implementation/c/node/error.ts33
-rw-r--r--llparse/src/implementation/c/node/index.ts27
-rw-r--r--llparse/src/implementation/c/node/invoke.ts44
-rw-r--r--llparse/src/implementation/c/node/pause.ts19
-rw-r--r--llparse/src/implementation/c/node/sequence.ts55
-rw-r--r--llparse/src/implementation/c/node/single.ts47
-rw-r--r--llparse/src/implementation/c/node/span-end.ts56
-rw-r--r--llparse/src/implementation/c/node/span-start.ts26
-rw-r--r--llparse/src/implementation/c/node/table-lookup.ts196
-rw-r--r--llparse/src/implementation/c/transform/base.ts10
-rw-r--r--llparse/src/implementation/c/transform/id.ts11
-rw-r--r--llparse/src/implementation/c/transform/index.ts11
-rw-r--r--llparse/src/implementation/c/transform/to-lower-unsafe.ts10
-rw-r--r--llparse/src/implementation/c/transform/to-lower.ts11
-rw-r--r--llparse/test/code-test.ts168
-rw-r--r--llparse/test/compiler-test.ts289
-rw-r--r--llparse/test/consume-test.ts69
-rw-r--r--llparse/test/fixtures/extra.c84
-rw-r--r--llparse/test/fixtures/index.ts52
-rw-r--r--llparse/test/resumption-test.ts55
-rw-r--r--llparse/test/span-test.ts107
-rw-r--r--llparse/test/transform-test.ts41
-rw-r--r--llparse/tsconfig.json15
-rw-r--r--llparse/tslint.json16
-rw-r--r--package.json167
-rw-r--r--scripts/generate-pem.js3
-rw-r--r--scripts/generate-undici-types-package-json.js28
-rw-r--r--scripts/verifyVersion.js15
-rw-r--r--test/abort-controller.js238
-rw-r--r--test/abort-event-emitter.js259
-rw-r--r--test/agent.js782
-rw-r--r--test/async_hooks.js206
-rw-r--r--test/autoselectfamily.js198
-rw-r--r--test/balanced-pool.js566
-rw-r--r--test/ca-fingerprint.js126
-rw-r--r--test/client-abort.js213
-rw-r--r--test/client-connect.js308
-rw-r--r--test/client-dispatch.js815
-rw-r--r--test/client-errors.js1285
-rw-r--r--test/client-head-reset-override.js62
-rw-r--r--test/client-idempotent-body.js43
-rw-r--r--test/client-keep-alive.js359
-rw-r--r--test/client-node-max-header-size.js23
-rw-r--r--test/client-pipeline.js1042
-rw-r--r--test/client-pipelining.js752
-rw-r--r--test/client-post.js73
-rw-r--r--test/client-reconnect.js54
-rw-r--r--test/client-request.js997
-rw-r--r--test/client-stream.js847
-rw-r--r--test/client-timeout.js197
-rw-r--r--test/client-unref.js47
-rw-r--r--test/client-upgrade.js452
-rw-r--r--test/client-write-max-listeners.js51
-rw-r--r--test/client.js2096
-rw-r--r--test/close-and-destroy.js344
-rw-r--r--test/connect-abort.js28
-rw-r--r--test/connect-errconnect.js32
-rw-r--r--test/connect-timeout.js68
-rw-r--r--test/content-length.js445
-rw-r--r--test/cookie/cookies.js616
-rw-r--r--test/cookie/global-headers.js70
-rw-r--r--test/diagnostics-channel/connect-error.js61
-rw-r--r--test/diagnostics-channel/error.js52
-rw-r--r--test/diagnostics-channel/get.js141
-rw-r--r--test/diagnostics-channel/post-stream.js149
-rw-r--r--test/diagnostics-channel/post.js147
-rw-r--r--test/dispatcher.js22
-rw-r--r--test/errors.js81
-rw-r--r--test/esm-wrapper.js19
-rw-r--r--test/fetch/407-statuscode-window-null.js20
-rw-r--r--test/fetch/abort.js82
-rw-r--r--test/fetch/abort2.js60
-rw-r--r--test/fetch/about-uri.js21
-rw-r--r--test/fetch/blob-uri.js100
-rw-r--r--test/fetch/bundle.js41
-rw-r--r--test/fetch/client-error-stack-trace.js21
-rw-r--r--test/fetch/client-fetch.js688
-rw-r--r--test/fetch/client-node-max-header-size.js29
-rw-r--r--test/fetch/content-length.js29
-rw-r--r--test/fetch/cookies.js69
-rw-r--r--test/fetch/data-uri.js214
-rw-r--r--test/fetch/encoding.js58
-rw-r--r--test/fetch/fetch-leak.js44
-rw-r--r--test/fetch/fetch-timeouts.js56
-rw-r--r--test/fetch/file.js190
-rw-r--r--test/fetch/formdata.js401
-rw-r--r--test/fetch/general.js30
-rw-r--r--test/fetch/headers.js743
-rw-r--r--test/fetch/http2.js415
-rw-r--r--test/fetch/integrity.js150
-rw-r--r--test/fetch/issue-1447.js46
-rw-r--r--test/fetch/issue-2009.js28
-rw-r--r--test/fetch/issue-2021.js32
-rw-r--r--test/fetch/issue-2171.js25
-rw-r--r--test/fetch/issue-2242.js8
-rw-r--r--test/fetch/issue-2318.js25
-rw-r--r--test/fetch/issue-node-46525.js28
-rw-r--r--test/fetch/iterators.js140
-rw-r--r--test/fetch/jsdom-abortcontroller-1910-1464495619.js26
-rw-r--r--test/fetch/redirect-cross-origin-header.js48
-rw-r--r--test/fetch/redirect.js50
-rw-r--r--test/fetch/relative-url.js110
-rw-r--r--test/fetch/request.js514
-rw-r--r--test/fetch/resource-timing.js72
-rw-r--r--test/fetch/response-json.js113
-rw-r--r--test/fetch/response.js257
-rw-r--r--test/fetch/user-agent.js32
-rw-r--r--test/fetch/util.js281
-rw-r--r--test/fixed-queue.js38
-rw-r--r--test/fixtures/ca.pem16
-rw-r--r--test/fixtures/cert.pem18
-rw-r--r--test/fixtures/client-ca-crt.pem17
-rw-r--r--test/fixtures/client-crt-2048.pem22
-rw-r--r--test/fixtures/client-crt.pem17
-rw-r--r--test/fixtures/client-key-2048.pem27
-rw-r--r--test/fixtures/client-key.pem27
-rw-r--r--test/fixtures/key.pem15
-rw-r--r--test/fuzzing/client/client-fuzz-body.js28
-rw-r--r--test/fuzzing/client/client-fuzz-headers.js27
-rw-r--r--test/fuzzing/client/client-fuzz-options.js38
-rw-r--r--test/fuzzing/client/index.js7
-rw-r--r--test/fuzzing/fuzz.js66
-rw-r--r--test/fuzzing/server/index.js6
-rw-r--r--test/fuzzing/server/server-fuzz-append-data.js7
-rw-r--r--test/fuzzing/server/server-fuzz-split-data.js17
-rw-r--r--test/gc.js98
-rw-r--r--test/get-head-body.js184
-rw-r--r--test/headers-as-array.js131
-rw-r--r--test/headers-crlf.js36
-rw-r--r--test/http-100.js141
-rw-r--r--test/http-req-destroy.js69
-rw-r--r--test/http2-alpn.js277
-rw-r--r--test/http2.js1191
-rw-r--r--test/https.js74
-rw-r--r--test/imports/undici-import.ts5
-rw-r--r--test/inflight-and-close.js31
-rw-r--r--test/invalid-headers.js108
-rw-r--r--test/issue-1670.js12
-rw-r--r--test/issue-1903.js78
-rw-r--r--test/issue-2065.js71
-rw-r--r--test/issue-2078.js30
-rw-r--r--test/issue-2349.js53
-rw-r--r--test/issue-803.js47
-rw-r--r--test/issue-810.js135
-rw-r--r--test/jest/instanceof-error.test.js44
-rw-r--r--test/jest/interceptor.test.js197
-rw-r--r--test/jest/issue-1757.test.js61
-rw-r--r--test/jest/mock-agent.test.js46
-rw-r--r--test/jest/mock-scope.test.js32
-rw-r--r--test/jest/test.js36
-rw-r--r--test/max-headers.js41
-rw-r--r--test/max-response-size.js105
-rw-r--r--test/mock-agent.js2637
-rw-r--r--test/mock-client.js446
-rw-r--r--test/mock-errors.js32
-rw-r--r--test/mock-interceptor-unused-assertions.js219
-rw-r--r--test/mock-interceptor.js258
-rw-r--r--test/mock-pool.js369
-rw-r--r--test/mock-scope.js73
-rw-r--r--test/mock-utils.js160
-rw-r--r--test/no-strict-content-length.js349
-rw-r--r--test/node-fetch/LICENSE22
-rw-r--r--test/node-fetch/headers.js282
-rw-r--r--test/node-fetch/main.js1661
-rw-r--r--test/node-fetch/mock.js112
-rw-r--r--test/node-fetch/request.js281
-rw-r--r--test/node-fetch/response.js251
-rw-r--r--test/node-fetch/utils/chai-timeout.js15
-rw-r--r--test/node-fetch/utils/dummy.txt1
-rw-r--r--test/node-fetch/utils/read-stream.js9
-rw-r--r--test/node-fetch/utils/server.js467
-rw-r--r--test/parser-issues.js114
-rw-r--r--test/pipeline-pipelining.js108
-rw-r--r--test/pool.js1101
-rw-r--r--test/promises.js280
-rw-r--r--test/proxy-agent.js720
-rw-r--r--test/proxy.js132
-rw-r--r--test/readable.test.js23
-rw-r--r--test/redirect-pipeline.js50
-rw-r--r--test/redirect-relative.js22
-rw-r--r--test/redirect-request.js420
-rw-r--r--test/redirect-stream.js423
-rw-r--r--test/redirect-upgrade.js34
-rw-r--r--test/request-crlf.js32
-rw-r--r--test/request-timeout.js820
-rw-r--r--test/request-timeout2.js48
-rw-r--r--test/request.js248
-rw-r--r--test/retry-handler.js622
-rw-r--r--test/socket-back-pressure.js54
-rw-r--r--test/socket-timeout.js100
-rw-r--r--test/stream-compat.js75
-rw-r--r--test/tls-client-cert.js70
-rw-r--r--test/tls-session-reuse.js185
-rw-r--r--test/tls.js188
-rw-r--r--test/trailers.js57
-rw-r--r--test/types/agent.test-d.ts110
-rw-r--r--test/types/api.test-d.ts28
-rw-r--r--test/types/balanced-pool.test-d.ts113
-rw-r--r--test/types/cache-storage.test-d.ts39
-rw-r--r--test/types/client.test-d.ts185
-rw-r--r--test/types/connector.test-d.ts38
-rw-r--r--test/types/diagnostics-channel.test-d.ts72
-rw-r--r--test/types/dispatcher.events.test-d.ts45
-rw-r--r--test/types/dispatcher.test-d.ts123
-rw-r--r--test/types/errors.test-d.ts115
-rw-r--r--test/types/fetch.test-d.ts173
-rw-r--r--test/types/formdata.test-d.ts27
-rw-r--r--test/types/global-dispatcher.test-d.ts12
-rw-r--r--test/types/header.test-d.ts16
-rw-r--r--test/types/index.test-d.ts23
-rw-r--r--test/types/interceptor.test-d.ts5
-rw-r--r--test/types/mock-agent.test-d.ts75
-rw-r--r--test/types/mock-client.test-d.ts43
-rw-r--r--test/types/mock-errors.test-d.ts19
-rw-r--r--test/types/mock-interceptor.test-d.ts80
-rw-r--r--test/types/mock-pool.test-d.ts42
-rw-r--r--test/types/pool.test-d.ts112
-rw-r--r--test/types/proxy-agent.test-d.ts43
-rw-r--r--test/types/readable.test-d.ts34
-rw-r--r--test/unix.js141
-rw-r--r--test/util.js123
-rw-r--r--test/utils/async-iterators.js25
-rw-r--r--test/utils/esm-wrapper.mjs102
-rw-r--r--test/utils/formdata.js49
-rw-r--r--test/utils/redirecting-servers.js265
-rw-r--r--test/utils/stream.js48
-rw-r--r--test/validations.js63
-rw-r--r--test/webidl/converters.js202
-rw-r--r--test/webidl/helpers.js75
-rw-r--r--test/webidl/util.js106
-rw-r--r--test/websocket/close.js130
-rw-r--r--test/websocket/constructor.js48
-rw-r--r--test/websocket/custom-headers.js30
-rw-r--r--test/websocket/diagnostics-channel.js71
-rw-r--r--test/websocket/events.js204
-rw-r--r--test/websocket/fragments.js40
-rw-r--r--test/websocket/frame.js24
-rw-r--r--test/websocket/opening-handshake.js215
-rw-r--r--test/websocket/ping-pong.js46
-rw-r--r--test/websocket/receive.js60
-rw-r--r--test/websocket/send.js216
-rw-r--r--test/websocket/websocketinit.js45
-rw-r--r--test/wpt/runner/runner.mjs356
-rw-r--r--test/wpt/runner/util.mjs172
-rw-r--r--test/wpt/runner/worker.mjs164
-rw-r--r--test/wpt/server/routes/network-partition-key.mjs111
-rw-r--r--test/wpt/server/routes/redirect.mjs104
-rw-r--r--test/wpt/server/server.mjs397
-rw-r--r--test/wpt/server/websocket.mjs46
-rw-r--r--test/wpt/start-FileAPI.mjs26
-rw-r--r--test/wpt/start-cacheStorage.mjs26
-rw-r--r--test/wpt/start-fetch.mjs31
-rw-r--r--test/wpt/start-mimesniff.mjs31
-rw-r--r--test/wpt/start-websockets.mjs47
-rw-r--r--test/wpt/start-xhr.mjs12
-rw-r--r--test/wpt/status/FileAPI.status.json75
-rw-r--r--test/wpt/status/fetch.status.json457
-rw-r--r--test/wpt/status/mimesniff.status.json7
-rw-r--r--test/wpt/status/service-workers/cache-storage.status.json24
-rw-r--r--test/wpt/status/websockets.status.json115
-rw-r--r--test/wpt/status/xhr/formdata.status.json1
-rw-r--r--test/wpt/tests/.azure-pipelines.yml595
-rw-r--r--test/wpt/tests/.gitattributes1
-rw-r--r--test/wpt/tests/.gitignore52
-rw-r--r--test/wpt/tests/.mailmap9
-rw-r--r--test/wpt/tests/.taskcluster.yml82
-rw-r--r--test/wpt/tests/CODEOWNERS6
-rw-r--r--test/wpt/tests/CODE_OF_CONDUCT.md138
-rw-r--r--test/wpt/tests/CONTRIBUTING.md11
-rw-r--r--test/wpt/tests/FileAPI/Blob-methods-from-detached-frame.html59
-rw-r--r--test/wpt/tests/FileAPI/BlobURL/cross-partition.tentative.https.html276
-rw-r--r--test/wpt/tests/FileAPI/BlobURL/support/file_test2.txt0
-rw-r--r--test/wpt/tests/FileAPI/BlobURL/test2-manual.html62
-rw-r--r--test/wpt/tests/FileAPI/FileReader/progress_event_bubbles_cancelable.html33
-rw-r--r--test/wpt/tests/FileAPI/FileReader/support/file_test1.txt0
-rw-r--r--test/wpt/tests/FileAPI/FileReader/test_errors-manual.html72
-rw-r--r--test/wpt/tests/FileAPI/FileReader/test_notreadableerrors-manual.html42
-rw-r--r--test/wpt/tests/FileAPI/FileReader/test_securityerrors-manual.html40
-rw-r--r--test/wpt/tests/FileAPI/FileReader/workers.html27
-rw-r--r--test/wpt/tests/FileAPI/FileReaderSync.worker.js56
-rw-r--r--test/wpt/tests/FileAPI/META.yml6
-rw-r--r--test/wpt/tests/FileAPI/blob/Blob-array-buffer.any.js45
-rw-r--r--test/wpt/tests/FileAPI/blob/Blob-constructor-dom.window.js53
-rw-r--r--test/wpt/tests/FileAPI/blob/Blob-constructor-endings.html104
-rw-r--r--test/wpt/tests/FileAPI/blob/Blob-constructor.any.js468
-rw-r--r--test/wpt/tests/FileAPI/blob/Blob-in-worker.worker.js9
-rw-r--r--test/wpt/tests/FileAPI/blob/Blob-slice-overflow.any.js32
-rw-r--r--test/wpt/tests/FileAPI/blob/Blob-slice.any.js231
-rw-r--r--test/wpt/tests/FileAPI/blob/Blob-stream-byob-crash.html11
-rw-r--r--test/wpt/tests/FileAPI/blob/Blob-stream-sync-xhr-crash.html13
-rw-r--r--test/wpt/tests/FileAPI/blob/Blob-stream.any.js83
-rw-r--r--test/wpt/tests/FileAPI/blob/Blob-text.any.js64
-rw-r--r--test/wpt/tests/FileAPI/file/File-constructor-endings.html104
-rw-r--r--test/wpt/tests/FileAPI/file/File-constructor.any.js155
-rw-r--r--test/wpt/tests/FileAPI/file/Worker-read-file-constructor.worker.js15
-rw-r--r--test/wpt/tests/FileAPI/file/resources/echo-content-escaped.py26
-rw-r--r--test/wpt/tests/FileAPI/file/send-file-form-controls.html113
-rw-r--r--test/wpt/tests/FileAPI/file/send-file-form-iso-2022-jp.html65
-rw-r--r--test/wpt/tests/FileAPI/file/send-file-form-punctuation.html226
-rw-r--r--test/wpt/tests/FileAPI/file/send-file-form-utf-8.html62
-rw-r--r--test/wpt/tests/FileAPI/file/send-file-form-windows-1252.html62
-rw-r--r--test/wpt/tests/FileAPI/file/send-file-form-x-user-defined.html63
-rw-r--r--test/wpt/tests/FileAPI/file/send-file-form.html25
-rw-r--r--test/wpt/tests/FileAPI/file/send-file-formdata-controls.any.js69
-rw-r--r--test/wpt/tests/FileAPI/file/send-file-formdata-punctuation.any.js144
-rw-r--r--test/wpt/tests/FileAPI/file/send-file-formdata-utf-8.any.js33
-rw-r--r--test/wpt/tests/FileAPI/file/send-file-formdata.any.js8
-rw-r--r--test/wpt/tests/FileAPI/fileReader.any.js59
-rw-r--r--test/wpt/tests/FileAPI/filelist-section/filelist.html57
-rw-r--r--test/wpt/tests/FileAPI/filelist-section/filelist_multiple_selected_files-manual.html64
-rw-r--r--test/wpt/tests/FileAPI/filelist-section/filelist_selected_file-manual.html64
-rw-r--r--test/wpt/tests/FileAPI/filelist-section/support/upload.txt1
-rw-r--r--test/wpt/tests/FileAPI/filelist-section/support/upload.zipbin0 -> 220 bytes
-rw-r--r--test/wpt/tests/FileAPI/historical.https.html65
-rw-r--r--test/wpt/tests/FileAPI/idlharness-manual.html45
-rw-r--r--test/wpt/tests/FileAPI/idlharness.any.js19
-rw-r--r--test/wpt/tests/FileAPI/idlharness.html37
-rw-r--r--test/wpt/tests/FileAPI/idlharness.worker.js17
-rw-r--r--test/wpt/tests/FileAPI/progress-manual.html49
-rw-r--r--test/wpt/tests/FileAPI/reading-data-section/Determining-Encoding.any.js81
-rw-r--r--test/wpt/tests/FileAPI/reading-data-section/FileReader-event-handler-attributes.any.js17
-rw-r--r--test/wpt/tests/FileAPI/reading-data-section/FileReader-multiple-reads.any.js81
-rw-r--r--test/wpt/tests/FileAPI/reading-data-section/filereader_abort.any.js38
-rw-r--r--test/wpt/tests/FileAPI/reading-data-section/filereader_error.any.js19
-rw-r--r--test/wpt/tests/FileAPI/reading-data-section/filereader_events.any.js19
-rw-r--r--test/wpt/tests/FileAPI/reading-data-section/filereader_file-manual.html69
-rw-r--r--test/wpt/tests/FileAPI/reading-data-section/filereader_file_img-manual.html47
-rw-r--r--test/wpt/tests/FileAPI/reading-data-section/filereader_readAsArrayBuffer.any.js23
-rw-r--r--test/wpt/tests/FileAPI/reading-data-section/filereader_readAsBinaryString.any.js23
-rw-r--r--test/wpt/tests/FileAPI/reading-data-section/filereader_readAsDataURL.any.js54
-rw-r--r--test/wpt/tests/FileAPI/reading-data-section/filereader_readAsText.any.js36
-rw-r--r--test/wpt/tests/FileAPI/reading-data-section/filereader_readystate.any.js19
-rw-r--r--test/wpt/tests/FileAPI/reading-data-section/filereader_result.any.js82
-rw-r--r--test/wpt/tests/FileAPI/reading-data-section/support/blue-100x100.pngbin0 -> 227 bytes
-rw-r--r--test/wpt/tests/FileAPI/support/Blob.js70
-rw-r--r--test/wpt/tests/FileAPI/support/document-domain-setter.sub.html7
-rw-r--r--test/wpt/tests/FileAPI/support/empty-document.html3
-rw-r--r--test/wpt/tests/FileAPI/support/historical-serviceworker.js5
-rw-r--r--test/wpt/tests/FileAPI/support/incumbent.sub.html22
-rw-r--r--test/wpt/tests/FileAPI/support/send-file-form-helper.js282
-rw-r--r--test/wpt/tests/FileAPI/support/send-file-formdata-helper.js99
-rw-r--r--test/wpt/tests/FileAPI/support/upload.txt1
-rw-r--r--test/wpt/tests/FileAPI/support/url-origin.html6
-rw-r--r--test/wpt/tests/FileAPI/unicode.html46
-rw-r--r--test/wpt/tests/FileAPI/url/cross-global-revoke.sub.html62
-rw-r--r--test/wpt/tests/FileAPI/url/multi-global-origin-serialization.sub.html26
-rw-r--r--test/wpt/tests/FileAPI/url/resources/create-helper.html7
-rw-r--r--test/wpt/tests/FileAPI/url/resources/create-helper.js4
-rw-r--r--test/wpt/tests/FileAPI/url/resources/fetch-tests.js71
-rw-r--r--test/wpt/tests/FileAPI/url/resources/revoke-helper.html7
-rw-r--r--test/wpt/tests/FileAPI/url/resources/revoke-helper.js9
-rw-r--r--test/wpt/tests/FileAPI/url/sandboxed-iframe.html32
-rw-r--r--test/wpt/tests/FileAPI/url/unicode-origin.sub.html23
-rw-r--r--test/wpt/tests/FileAPI/url/url-charset.window.js34
-rw-r--r--test/wpt/tests/FileAPI/url/url-format.any.js70
-rw-r--r--test/wpt/tests/FileAPI/url/url-in-tags-revoke.window.js115
-rw-r--r--test/wpt/tests/FileAPI/url/url-in-tags.window.js48
-rw-r--r--test/wpt/tests/FileAPI/url/url-lifetime.html56
-rw-r--r--test/wpt/tests/FileAPI/url/url-reload.window.js36
-rw-r--r--test/wpt/tests/FileAPI/url/url-with-fetch.any.js72
-rw-r--r--test/wpt/tests/FileAPI/url/url-with-xhr.any.js68
-rw-r--r--test/wpt/tests/FileAPI/url/url_createobjecturl_file-manual.html45
-rw-r--r--test/wpt/tests/FileAPI/url/url_createobjecturl_file_img-manual.html28
-rw-r--r--test/wpt/tests/FileAPI/url/url_xmlhttprequest_img-ref.html12
-rw-r--r--test/wpt/tests/FileAPI/url/url_xmlhttprequest_img.html27
-rw-r--r--test/wpt/tests/LICENSE.md11
-rw-r--r--test/wpt/tests/README.md124
-rw-r--r--test/wpt/tests/common/CustomCorsResponse.py30
-rw-r--r--test/wpt/tests/common/META.yml3
-rw-r--r--test/wpt/tests/common/PrefixedLocalStorage.js116
-rw-r--r--test/wpt/tests/common/PrefixedLocalStorage.js.headers1
-rw-r--r--test/wpt/tests/common/PrefixedPostMessage.js100
-rw-r--r--test/wpt/tests/common/PrefixedPostMessage.js.headers1
-rw-r--r--test/wpt/tests/common/README.md10
-rw-r--r--test/wpt/tests/common/__init__.py0
-rw-r--r--test/wpt/tests/common/arrays.js31
-rw-r--r--test/wpt/tests/common/blank-with-cors.html0
-rw-r--r--test/wpt/tests/common/blank-with-cors.html.headers1
-rw-r--r--test/wpt/tests/common/blank.html0
-rw-r--r--test/wpt/tests/common/custom-cors-response.js32
-rw-r--r--test/wpt/tests/common/dispatcher/README.md228
-rw-r--r--test/wpt/tests/common/dispatcher/dispatcher.js256
-rw-r--r--test/wpt/tests/common/dispatcher/dispatcher.py53
-rw-r--r--test/wpt/tests/common/dispatcher/executor-service-worker.js24
-rw-r--r--test/wpt/tests/common/dispatcher/executor-worker.js12
-rw-r--r--test/wpt/tests/common/dispatcher/executor.html15
-rw-r--r--test/wpt/tests/common/dispatcher/remote-executor.html12
-rw-r--r--test/wpt/tests/common/domain-setter.sub.html8
-rw-r--r--test/wpt/tests/common/dummy.xhtml2
-rw-r--r--test/wpt/tests/common/dummy.xml1
-rw-r--r--test/wpt/tests/common/echo.py6
-rw-r--r--test/wpt/tests/common/gc.js52
-rw-r--r--test/wpt/tests/common/get-host-info.sub.js63
-rw-r--r--test/wpt/tests/common/get-host-info.sub.js.headers1
-rw-r--r--test/wpt/tests/common/media.js61
-rw-r--r--test/wpt/tests/common/media.js.headers1
-rw-r--r--test/wpt/tests/common/object-association.js74
-rw-r--r--test/wpt/tests/common/object-association.js.headers1
-rw-r--r--test/wpt/tests/common/performance-timeline-utils.js56
-rw-r--r--test/wpt/tests/common/performance-timeline-utils.js.headers1
-rw-r--r--test/wpt/tests/common/proxy-all.sub.pac3
-rw-r--r--test/wpt/tests/common/redirect-opt-in.py20
-rw-r--r--test/wpt/tests/common/redirect.py19
-rw-r--r--test/wpt/tests/common/refresh.py11
-rw-r--r--test/wpt/tests/common/reftest-wait.js39
-rw-r--r--test/wpt/tests/common/reftest-wait.js.headers1
-rw-r--r--test/wpt/tests/common/rendering-utils.js19
-rw-r--r--test/wpt/tests/common/sab.js21
-rw-r--r--test/wpt/tests/common/security-features/README.md460
-rw-r--r--test/wpt/tests/common/security-features/__init__.py0
-rw-r--r--test/wpt/tests/common/security-features/resources/common.sub.js1311
-rw-r--r--test/wpt/tests/common/security-features/resources/common.sub.js.headers1
-rw-r--r--test/wpt/tests/common/security-features/scope/__init__.py0
-rw-r--r--test/wpt/tests/common/security-features/scope/document.py36
-rw-r--r--test/wpt/tests/common/security-features/scope/template/document.html.template30
-rw-r--r--test/wpt/tests/common/security-features/scope/template/worker.js.template29
-rw-r--r--test/wpt/tests/common/security-features/scope/util.py43
-rw-r--r--test/wpt/tests/common/security-features/scope/worker.py44
-rw-r--r--test/wpt/tests/common/security-features/subresource/__init__.py0
-rw-r--r--test/wpt/tests/common/security-features/subresource/audio.py18
-rw-r--r--test/wpt/tests/common/security-features/subresource/document.py12
-rw-r--r--test/wpt/tests/common/security-features/subresource/empty.py14
-rw-r--r--test/wpt/tests/common/security-features/subresource/font.py76
-rw-r--r--test/wpt/tests/common/security-features/subresource/image.py116
-rw-r--r--test/wpt/tests/common/security-features/subresource/referrer.py4
-rw-r--r--test/wpt/tests/common/security-features/subresource/script.py14
-rw-r--r--test/wpt/tests/common/security-features/subresource/shared-worker.py13
-rw-r--r--test/wpt/tests/common/security-features/subresource/static-import.py61
-rw-r--r--test/wpt/tests/common/security-features/subresource/stylesheet.py61
-rw-r--r--test/wpt/tests/common/security-features/subresource/subresource.py199
-rw-r--r--test/wpt/tests/common/security-features/subresource/svg.py37
-rw-r--r--test/wpt/tests/common/security-features/subresource/template/document.html.template16
-rw-r--r--test/wpt/tests/common/security-features/subresource/template/font.css.template9
-rw-r--r--test/wpt/tests/common/security-features/subresource/template/image.css.template3
-rw-r--r--test/wpt/tests/common/security-features/subresource/template/script.js.template3
-rw-r--r--test/wpt/tests/common/security-features/subresource/template/shared-worker.js.template5
-rw-r--r--test/wpt/tests/common/security-features/subresource/template/static-import.js.template1
-rw-r--r--test/wpt/tests/common/security-features/subresource/template/svg.css.template3
-rw-r--r--test/wpt/tests/common/security-features/subresource/template/svg.embedded.template5
-rw-r--r--test/wpt/tests/common/security-features/subresource/template/worker.js.template3
-rw-r--r--test/wpt/tests/common/security-features/subresource/video.py17
-rw-r--r--test/wpt/tests/common/security-features/subresource/worker.py13
-rw-r--r--test/wpt/tests/common/security-features/subresource/xhr.py16
-rw-r--r--test/wpt/tests/common/security-features/tools/format_spec_src_json.py24
-rw-r--r--test/wpt/tests/common/security-features/tools/generate.py462
-rw-r--r--test/wpt/tests/common/security-features/tools/spec.src.json533
-rw-r--r--test/wpt/tests/common/security-features/tools/spec_validator.py251
-rw-r--r--test/wpt/tests/common/security-features/tools/template/disclaimer.template1
-rw-r--r--test/wpt/tests/common/security-features/tools/template/spec_json.js.template1
-rw-r--r--test/wpt/tests/common/security-features/tools/template/test.debug.html.template26
-rw-r--r--test/wpt/tests/common/security-features/tools/template/test.release.html.template22
-rw-r--r--test/wpt/tests/common/security-features/tools/util.py228
-rw-r--r--test/wpt/tests/common/security-features/types.md62
-rw-r--r--test/wpt/tests/common/slow-redirect.py29
-rw-r--r--test/wpt/tests/common/slow.py6
-rw-r--r--test/wpt/tests/common/square.pngbin0 -> 18299 bytes
-rw-r--r--test/wpt/tests/common/stringifiers.js57
-rw-r--r--test/wpt/tests/common/stringifiers.js.headers1
-rw-r--r--test/wpt/tests/common/subset-tests-by-key.js83
-rw-r--r--test/wpt/tests/common/subset-tests.js60
-rw-r--r--test/wpt/tests/common/test-setting-immutable-prototype.js67
-rw-r--r--test/wpt/tests/common/test-setting-immutable-prototype.js.headers1
-rw-r--r--test/wpt/tests/common/text-plain.txt4
-rw-r--r--test/wpt/tests/common/third_party/reftest-analyzer.xhtml934
-rw-r--r--test/wpt/tests/common/utils.js98
-rw-r--r--test/wpt/tests/common/utils.js.headers1
-rw-r--r--test/wpt/tests/common/window-name-setter.html12
-rw-r--r--test/wpt/tests/common/worklet-reftest.js50
-rw-r--r--test/wpt/tests/common/worklet-reftest.js.headers1
-rw-r--r--test/wpt/tests/fetch/META.yml7
-rw-r--r--test/wpt/tests/fetch/README.md6
-rw-r--r--test/wpt/tests/fetch/api/abort/cache.https.any.js47
-rw-r--r--test/wpt/tests/fetch/api/abort/destroyed-context.html27
-rw-r--r--test/wpt/tests/fetch/api/abort/general.any.js572
-rw-r--r--test/wpt/tests/fetch/api/abort/keepalive.html85
-rw-r--r--test/wpt/tests/fetch/api/abort/request.any.js85
-rw-r--r--test/wpt/tests/fetch/api/abort/serviceworker-intercepted.https.html212
-rw-r--r--test/wpt/tests/fetch/api/basic/accept-header.any.js34
-rw-r--r--test/wpt/tests/fetch/api/basic/block-mime-as-script.html43
-rw-r--r--test/wpt/tests/fetch/api/basic/conditional-get.any.js38
-rw-r--r--test/wpt/tests/fetch/api/basic/error-after-response.any.js24
-rw-r--r--test/wpt/tests/fetch/api/basic/header-value-combining.any.js15
-rw-r--r--test/wpt/tests/fetch/api/basic/header-value-null-byte.any.js5
-rw-r--r--test/wpt/tests/fetch/api/basic/historical.any.js17
-rw-r--r--test/wpt/tests/fetch/api/basic/http-response-code.any.js14
-rw-r--r--test/wpt/tests/fetch/api/basic/integrity.sub.any.js87
-rw-r--r--test/wpt/tests/fetch/api/basic/keepalive.any.js43
-rw-r--r--test/wpt/tests/fetch/api/basic/mediasource.window.js5
-rw-r--r--test/wpt/tests/fetch/api/basic/mode-no-cors.sub.any.js29
-rw-r--r--test/wpt/tests/fetch/api/basic/mode-same-origin.any.js28
-rw-r--r--test/wpt/tests/fetch/api/basic/referrer.any.js29
-rw-r--r--test/wpt/tests/fetch/api/basic/request-forbidden-headers.any.js100
-rw-r--r--test/wpt/tests/fetch/api/basic/request-head.any.js6
-rw-r--r--test/wpt/tests/fetch/api/basic/request-headers-case.any.js13
-rw-r--r--test/wpt/tests/fetch/api/basic/request-headers-nonascii.any.js29
-rw-r--r--test/wpt/tests/fetch/api/basic/request-headers.any.js82
-rw-r--r--test/wpt/tests/fetch/api/basic/request-referrer-redirected-worker.html17
-rw-r--r--test/wpt/tests/fetch/api/basic/request-referrer.any.js24
-rw-r--r--test/wpt/tests/fetch/api/basic/request-upload.any.js135
-rw-r--r--test/wpt/tests/fetch/api/basic/request-upload.h2.any.js186
-rw-r--r--test/wpt/tests/fetch/api/basic/response-null-body.any.js38
-rw-r--r--test/wpt/tests/fetch/api/basic/response-url.sub.any.js16
-rw-r--r--test/wpt/tests/fetch/api/basic/scheme-about.any.js26
-rw-r--r--test/wpt/tests/fetch/api/basic/scheme-blob.sub.any.js125
-rw-r--r--test/wpt/tests/fetch/api/basic/scheme-data.any.js43
-rw-r--r--test/wpt/tests/fetch/api/basic/scheme-others.sub.any.js31
-rw-r--r--test/wpt/tests/fetch/api/basic/status.h2.any.js17
-rw-r--r--test/wpt/tests/fetch/api/basic/stream-response.any.js40
-rw-r--r--test/wpt/tests/fetch/api/basic/stream-safe-creation.any.js54
-rw-r--r--test/wpt/tests/fetch/api/basic/text-utf8.any.js74
-rw-r--r--test/wpt/tests/fetch/api/body/cloned-any.js50
-rw-r--r--test/wpt/tests/fetch/api/body/formdata.any.js14
-rw-r--r--test/wpt/tests/fetch/api/body/mime-type.any.js127
-rw-r--r--test/wpt/tests/fetch/api/cors/cors-basic.any.js43
-rw-r--r--test/wpt/tests/fetch/api/cors/cors-cookies-redirect.any.js49
-rw-r--r--test/wpt/tests/fetch/api/cors/cors-cookies.any.js56
-rw-r--r--test/wpt/tests/fetch/api/cors/cors-expose-star.sub.any.js41
-rw-r--r--test/wpt/tests/fetch/api/cors/cors-filtering.sub.any.js69
-rw-r--r--test/wpt/tests/fetch/api/cors/cors-keepalive.any.js118
-rw-r--r--test/wpt/tests/fetch/api/cors/cors-multiple-origins.sub.any.js22
-rw-r--r--test/wpt/tests/fetch/api/cors/cors-no-preflight.any.js41
-rw-r--r--test/wpt/tests/fetch/api/cors/cors-origin.any.js51
-rw-r--r--test/wpt/tests/fetch/api/cors/cors-preflight-cache.any.js46
-rw-r--r--test/wpt/tests/fetch/api/cors/cors-preflight-not-cors-safelisted.any.js19
-rw-r--r--test/wpt/tests/fetch/api/cors/cors-preflight-redirect.any.js37
-rw-r--r--test/wpt/tests/fetch/api/cors/cors-preflight-referrer.any.js51
-rw-r--r--test/wpt/tests/fetch/api/cors/cors-preflight-response-validation.any.js33
-rw-r--r--test/wpt/tests/fetch/api/cors/cors-preflight-star.any.js86
-rw-r--r--test/wpt/tests/fetch/api/cors/cors-preflight-status.any.js37
-rw-r--r--test/wpt/tests/fetch/api/cors/cors-preflight.any.js62
-rw-r--r--test/wpt/tests/fetch/api/cors/cors-redirect-credentials.any.js52
-rw-r--r--test/wpt/tests/fetch/api/cors/cors-redirect-preflight.any.js46
-rw-r--r--test/wpt/tests/fetch/api/cors/cors-redirect.any.js42
-rw-r--r--test/wpt/tests/fetch/api/cors/data-url-iframe.html58
-rw-r--r--test/wpt/tests/fetch/api/cors/data-url-shared-worker.html53
-rw-r--r--test/wpt/tests/fetch/api/cors/data-url-worker.html50
-rw-r--r--test/wpt/tests/fetch/api/cors/resources/corspreflight.js58
-rw-r--r--test/wpt/tests/fetch/api/cors/resources/not-cors-safelisted.json13
-rw-r--r--test/wpt/tests/fetch/api/cors/sandboxed-iframe.html14
-rw-r--r--test/wpt/tests/fetch/api/crashtests/body-window-destroy.html11
-rw-r--r--test/wpt/tests/fetch/api/crashtests/request.html8
-rw-r--r--test/wpt/tests/fetch/api/credentials/authentication-basic.any.js17
-rw-r--r--test/wpt/tests/fetch/api/credentials/authentication-redirection.any.js29
-rw-r--r--test/wpt/tests/fetch/api/credentials/cookies.any.js49
-rw-r--r--test/wpt/tests/fetch/api/headers/header-setcookie.any.js266
-rw-r--r--test/wpt/tests/fetch/api/headers/header-values-normalize.any.js72
-rw-r--r--test/wpt/tests/fetch/api/headers/header-values.any.js63
-rw-r--r--test/wpt/tests/fetch/api/headers/headers-basic.any.js275
-rw-r--r--test/wpt/tests/fetch/api/headers/headers-casing.any.js54
-rw-r--r--test/wpt/tests/fetch/api/headers/headers-combine.any.js66
-rw-r--r--test/wpt/tests/fetch/api/headers/headers-errors.any.js96
-rw-r--r--test/wpt/tests/fetch/api/headers/headers-no-cors.any.js59
-rw-r--r--test/wpt/tests/fetch/api/headers/headers-normalize.any.js56
-rw-r--r--test/wpt/tests/fetch/api/headers/headers-record.any.js357
-rw-r--r--test/wpt/tests/fetch/api/headers/headers-structure.any.js20
-rw-r--r--test/wpt/tests/fetch/api/idlharness.any.js21
-rw-r--r--test/wpt/tests/fetch/api/policies/csp-blocked-worker.html16
-rw-r--r--test/wpt/tests/fetch/api/policies/csp-blocked.html15
-rw-r--r--test/wpt/tests/fetch/api/policies/csp-blocked.html.headers1
-rw-r--r--test/wpt/tests/fetch/api/policies/csp-blocked.js13
-rw-r--r--test/wpt/tests/fetch/api/policies/csp-blocked.js.headers1
-rw-r--r--test/wpt/tests/fetch/api/policies/nested-policy.js1
-rw-r--r--test/wpt/tests/fetch/api/policies/nested-policy.js.headers1
-rw-r--r--test/wpt/tests/fetch/api/policies/referrer-no-referrer-service-worker.https.html18
-rw-r--r--test/wpt/tests/fetch/api/policies/referrer-no-referrer-worker.html17
-rw-r--r--test/wpt/tests/fetch/api/policies/referrer-no-referrer.html15
-rw-r--r--test/wpt/tests/fetch/api/policies/referrer-no-referrer.html.headers1
-rw-r--r--test/wpt/tests/fetch/api/policies/referrer-no-referrer.js19
-rw-r--r--test/wpt/tests/fetch/api/policies/referrer-no-referrer.js.headers1
-rw-r--r--test/wpt/tests/fetch/api/policies/referrer-origin-service-worker.https.html18
-rw-r--r--test/wpt/tests/fetch/api/policies/referrer-origin-when-cross-origin-service-worker.https.html17
-rw-r--r--test/wpt/tests/fetch/api/policies/referrer-origin-when-cross-origin-worker.html16
-rw-r--r--test/wpt/tests/fetch/api/policies/referrer-origin-when-cross-origin.html16
-rw-r--r--test/wpt/tests/fetch/api/policies/referrer-origin-when-cross-origin.html.headers1
-rw-r--r--test/wpt/tests/fetch/api/policies/referrer-origin-when-cross-origin.js21
-rw-r--r--test/wpt/tests/fetch/api/policies/referrer-origin-when-cross-origin.js.headers1
-rw-r--r--test/wpt/tests/fetch/api/policies/referrer-origin-worker.html17
-rw-r--r--test/wpt/tests/fetch/api/policies/referrer-origin.html16
-rw-r--r--test/wpt/tests/fetch/api/policies/referrer-origin.html.headers1
-rw-r--r--test/wpt/tests/fetch/api/policies/referrer-origin.js30
-rw-r--r--test/wpt/tests/fetch/api/policies/referrer-origin.js.headers1
-rw-r--r--test/wpt/tests/fetch/api/policies/referrer-unsafe-url-service-worker.https.html18
-rw-r--r--test/wpt/tests/fetch/api/policies/referrer-unsafe-url-worker.html17
-rw-r--r--test/wpt/tests/fetch/api/policies/referrer-unsafe-url.html16
-rw-r--r--test/wpt/tests/fetch/api/policies/referrer-unsafe-url.html.headers1
-rw-r--r--test/wpt/tests/fetch/api/policies/referrer-unsafe-url.js21
-rw-r--r--test/wpt/tests/fetch/api/policies/referrer-unsafe-url.js.headers1
-rw-r--r--test/wpt/tests/fetch/api/redirect/redirect-back-to-original-origin.any.js38
-rw-r--r--test/wpt/tests/fetch/api/redirect/redirect-count.any.js51
-rw-r--r--test/wpt/tests/fetch/api/redirect/redirect-empty-location.any.js21
-rw-r--r--test/wpt/tests/fetch/api/redirect/redirect-keepalive.any.js94
-rw-r--r--test/wpt/tests/fetch/api/redirect/redirect-location-escape.tentative.any.js46
-rw-r--r--test/wpt/tests/fetch/api/redirect/redirect-location.any.js73
-rw-r--r--test/wpt/tests/fetch/api/redirect/redirect-method.any.js112
-rw-r--r--test/wpt/tests/fetch/api/redirect/redirect-mode.any.js59
-rw-r--r--test/wpt/tests/fetch/api/redirect/redirect-origin.any.js68
-rw-r--r--test/wpt/tests/fetch/api/redirect/redirect-referrer-override.any.js104
-rw-r--r--test/wpt/tests/fetch/api/redirect/redirect-referrer.any.js66
-rw-r--r--test/wpt/tests/fetch/api/redirect/redirect-schemes.any.js19
-rw-r--r--test/wpt/tests/fetch/api/redirect/redirect-to-dataurl.any.js28
-rw-r--r--test/wpt/tests/fetch/api/redirect/redirect-upload.h2.any.js33
-rw-r--r--test/wpt/tests/fetch/api/request/destination/fetch-destination-frame.https.html51
-rw-r--r--test/wpt/tests/fetch/api/request/destination/fetch-destination-iframe.https.html51
-rw-r--r--test/wpt/tests/fetch/api/request/destination/fetch-destination-no-load-event.https.html124
-rw-r--r--test/wpt/tests/fetch/api/request/destination/fetch-destination-prefetch.https.html46
-rw-r--r--test/wpt/tests/fetch/api/request/destination/fetch-destination-worker.https.html60
-rw-r--r--test/wpt/tests/fetch/api/request/destination/fetch-destination.https.html435
-rw-r--r--test/wpt/tests/fetch/api/request/destination/resources/dummy0
-rw-r--r--test/wpt/tests/fetch/api/request/destination/resources/dummy.es0
-rw-r--r--test/wpt/tests/fetch/api/request/destination/resources/dummy.es.headers1
-rw-r--r--test/wpt/tests/fetch/api/request/destination/resources/dummy.html0
-rw-r--r--test/wpt/tests/fetch/api/request/destination/resources/dummy.pngbin0 -> 18299 bytes
-rw-r--r--test/wpt/tests/fetch/api/request/destination/resources/dummy.ttfbin0 -> 2528 bytes
-rw-r--r--test/wpt/tests/fetch/api/request/destination/resources/dummy_audio.mp3bin0 -> 20498 bytes
-rw-r--r--test/wpt/tests/fetch/api/request/destination/resources/dummy_audio.ogabin0 -> 18541 bytes
-rw-r--r--test/wpt/tests/fetch/api/request/destination/resources/dummy_video.mp4bin0 -> 67369 bytes
-rw-r--r--test/wpt/tests/fetch/api/request/destination/resources/dummy_video.ogvbin0 -> 94372 bytes
-rw-r--r--test/wpt/tests/fetch/api/request/destination/resources/dummy_video.webmbin0 -> 96902 bytes
-rw-r--r--test/wpt/tests/fetch/api/request/destination/resources/empty.https.html0
-rw-r--r--test/wpt/tests/fetch/api/request/destination/resources/fetch-destination-worker-frame.js20
-rw-r--r--test/wpt/tests/fetch/api/request/destination/resources/fetch-destination-worker-iframe.js20
-rw-r--r--test/wpt/tests/fetch/api/request/destination/resources/fetch-destination-worker-no-load-event.js20
-rw-r--r--test/wpt/tests/fetch/api/request/destination/resources/fetch-destination-worker.js12
-rw-r--r--test/wpt/tests/fetch/api/request/destination/resources/importer.js1
-rw-r--r--test/wpt/tests/fetch/api/request/forbidden-method.any.js13
-rw-r--r--test/wpt/tests/fetch/api/request/multi-globals/construct-in-detached-frame.window.js11
-rw-r--r--test/wpt/tests/fetch/api/request/multi-globals/current/current.html3
-rw-r--r--test/wpt/tests/fetch/api/request/multi-globals/incumbent/incumbent.html14
-rw-r--r--test/wpt/tests/fetch/api/request/multi-globals/url-parsing.html27
-rw-r--r--test/wpt/tests/fetch/api/request/request-bad-port.any.js92
-rw-r--r--test/wpt/tests/fetch/api/request/request-cache-default-conditional.any.js170
-rw-r--r--test/wpt/tests/fetch/api/request/request-cache-default.any.js39
-rw-r--r--test/wpt/tests/fetch/api/request/request-cache-force-cache.any.js67
-rw-r--r--test/wpt/tests/fetch/api/request/request-cache-no-cache.any.js25
-rw-r--r--test/wpt/tests/fetch/api/request/request-cache-no-store.any.js37
-rw-r--r--test/wpt/tests/fetch/api/request/request-cache-only-if-cached.any.js66
-rw-r--r--test/wpt/tests/fetch/api/request/request-cache-reload.any.js51
-rw-r--r--test/wpt/tests/fetch/api/request/request-cache.js223
-rw-r--r--test/wpt/tests/fetch/api/request/request-clone.sub.html63
-rw-r--r--test/wpt/tests/fetch/api/request/request-consume-empty.any.js101
-rw-r--r--test/wpt/tests/fetch/api/request/request-consume.any.js145
-rw-r--r--test/wpt/tests/fetch/api/request/request-disturbed.any.js109
-rw-r--r--test/wpt/tests/fetch/api/request/request-error.any.js56
-rw-r--r--test/wpt/tests/fetch/api/request/request-error.js57
-rw-r--r--test/wpt/tests/fetch/api/request/request-headers.any.js178
-rw-r--r--test/wpt/tests/fetch/api/request/request-init-001.sub.html112
-rw-r--r--test/wpt/tests/fetch/api/request/request-init-002.any.js60
-rw-r--r--test/wpt/tests/fetch/api/request/request-init-003.sub.html84
-rw-r--r--test/wpt/tests/fetch/api/request/request-init-contenttype.any.js141
-rw-r--r--test/wpt/tests/fetch/api/request/request-init-priority.any.js26
-rw-r--r--test/wpt/tests/fetch/api/request/request-init-stream.any.js147
-rw-r--r--test/wpt/tests/fetch/api/request/request-keepalive-quota.html97
-rw-r--r--test/wpt/tests/fetch/api/request/request-keepalive.any.js17
-rw-r--r--test/wpt/tests/fetch/api/request/request-reset-attributes.https.html96
-rw-r--r--test/wpt/tests/fetch/api/request/request-structure.any.js143
-rw-r--r--test/wpt/tests/fetch/api/request/resources/cache.py67
-rw-r--r--test/wpt/tests/fetch/api/request/resources/hello.txt1
-rw-r--r--test/wpt/tests/fetch/api/request/resources/request-reset-attributes-worker.js19
-rw-r--r--test/wpt/tests/fetch/api/request/url-encoding.html25
-rw-r--r--test/wpt/tests/fetch/api/resources/authentication.py14
-rw-r--r--test/wpt/tests/fetch/api/resources/bad-chunk-encoding.py13
-rw-r--r--test/wpt/tests/fetch/api/resources/basic.html5
-rw-r--r--test/wpt/tests/fetch/api/resources/cache.py18
-rw-r--r--test/wpt/tests/fetch/api/resources/clean-stash.py6
-rw-r--r--test/wpt/tests/fetch/api/resources/cors-top.txt1
-rw-r--r--test/wpt/tests/fetch/api/resources/cors-top.txt.headers1
-rw-r--r--test/wpt/tests/fetch/api/resources/data.json1
-rw-r--r--test/wpt/tests/fetch/api/resources/dump-authorization-header.py14
-rw-r--r--test/wpt/tests/fetch/api/resources/echo-content.h2.py7
-rw-r--r--test/wpt/tests/fetch/api/resources/echo-content.py12
-rw-r--r--test/wpt/tests/fetch/api/resources/empty.txt0
-rw-r--r--test/wpt/tests/fetch/api/resources/infinite-slow-response.py35
-rw-r--r--test/wpt/tests/fetch/api/resources/inspect-headers.py24
-rw-r--r--test/wpt/tests/fetch/api/resources/keepalive-helper.js99
-rw-r--r--test/wpt/tests/fetch/api/resources/keepalive-iframe.html21
-rw-r--r--test/wpt/tests/fetch/api/resources/keepalive-redirect-iframe.html23
-rw-r--r--test/wpt/tests/fetch/api/resources/keepalive-redirect-window.html42
-rw-r--r--test/wpt/tests/fetch/api/resources/method.py18
-rw-r--r--test/wpt/tests/fetch/api/resources/preflight.py78
-rw-r--r--test/wpt/tests/fetch/api/resources/redirect-empty-location.py3
-rw-r--r--test/wpt/tests/fetch/api/resources/redirect.h2.py14
-rw-r--r--test/wpt/tests/fetch/api/resources/redirect.py73
-rw-r--r--test/wpt/tests/fetch/api/resources/sandboxed-iframe.html34
-rw-r--r--test/wpt/tests/fetch/api/resources/script-with-header.py7
-rw-r--r--test/wpt/tests/fetch/api/resources/stash-put.py19
-rw-r--r--test/wpt/tests/fetch/api/resources/stash-take.py9
-rw-r--r--test/wpt/tests/fetch/api/resources/status.py11
-rw-r--r--test/wpt/tests/fetch/api/resources/sw-intercept-abort.js19
-rw-r--r--test/wpt/tests/fetch/api/resources/sw-intercept.js10
-rw-r--r--test/wpt/tests/fetch/api/resources/top.txt1
-rw-r--r--test/wpt/tests/fetch/api/resources/trickle.py15
-rw-r--r--test/wpt/tests/fetch/api/resources/utils.js105
-rw-r--r--test/wpt/tests/fetch/api/response/json.any.js14
-rw-r--r--test/wpt/tests/fetch/api/response/many-empty-chunks-crash.html14
-rw-r--r--test/wpt/tests/fetch/api/response/multi-globals/current/current.html3
-rw-r--r--test/wpt/tests/fetch/api/response/multi-globals/incumbent/incumbent.html16
-rw-r--r--test/wpt/tests/fetch/api/response/multi-globals/relevant/relevant.html2
-rw-r--r--test/wpt/tests/fetch/api/response/multi-globals/url-parsing.html27
-rw-r--r--test/wpt/tests/fetch/api/response/response-body-read-task-handling.html86
-rw-r--r--test/wpt/tests/fetch/api/response/response-cancel-stream.any.js64
-rw-r--r--test/wpt/tests/fetch/api/response/response-clone-iframe.window.js32
-rw-r--r--test/wpt/tests/fetch/api/response/response-clone.any.js140
-rw-r--r--test/wpt/tests/fetch/api/response/response-consume-empty.any.js99
-rw-r--r--test/wpt/tests/fetch/api/response/response-consume-stream.any.js61
-rw-r--r--test/wpt/tests/fetch/api/response/response-consume.html317
-rw-r--r--test/wpt/tests/fetch/api/response/response-error-from-stream.any.js59
-rw-r--r--test/wpt/tests/fetch/api/response/response-error.any.js27
-rw-r--r--test/wpt/tests/fetch/api/response/response-from-stream.any.js23
-rw-r--r--test/wpt/tests/fetch/api/response/response-init-001.any.js64
-rw-r--r--test/wpt/tests/fetch/api/response/response-init-002.any.js61
-rw-r--r--test/wpt/tests/fetch/api/response/response-init-contenttype.any.js125
-rw-r--r--test/wpt/tests/fetch/api/response/response-static-error.any.js34
-rw-r--r--test/wpt/tests/fetch/api/response/response-static-json.any.js96
-rw-r--r--test/wpt/tests/fetch/api/response/response-static-redirect.any.js40
-rw-r--r--test/wpt/tests/fetch/api/response/response-stream-bad-chunk.any.js24
-rw-r--r--test/wpt/tests/fetch/api/response/response-stream-disturbed-1.any.js44
-rw-r--r--test/wpt/tests/fetch/api/response/response-stream-disturbed-2.any.js35
-rw-r--r--test/wpt/tests/fetch/api/response/response-stream-disturbed-3.any.js36
-rw-r--r--test/wpt/tests/fetch/api/response/response-stream-disturbed-4.any.js35
-rw-r--r--test/wpt/tests/fetch/api/response/response-stream-disturbed-5.any.js19
-rw-r--r--test/wpt/tests/fetch/api/response/response-stream-disturbed-6.any.js76
-rw-r--r--test/wpt/tests/fetch/api/response/response-stream-disturbed-by-pipe.any.js17
-rw-r--r--test/wpt/tests/fetch/api/response/response-stream-disturbed-util.js17
-rw-r--r--test/wpt/tests/fetch/api/response/response-stream-with-broken-then.any.js117
-rw-r--r--test/wpt/tests/fetch/connection-pool/network-partition-key.html264
-rw-r--r--test/wpt/tests/fetch/connection-pool/resources/network-partition-about-blank-checker.html35
-rw-r--r--test/wpt/tests/fetch/connection-pool/resources/network-partition-checker.html30
-rw-r--r--test/wpt/tests/fetch/connection-pool/resources/network-partition-iframe-checker.html22
-rw-r--r--test/wpt/tests/fetch/connection-pool/resources/network-partition-key.js47
-rw-r--r--test/wpt/tests/fetch/connection-pool/resources/network-partition-key.py130
-rw-r--r--test/wpt/tests/fetch/connection-pool/resources/network-partition-worker-checker.html24
-rw-r--r--test/wpt/tests/fetch/connection-pool/resources/network-partition-worker.js15
-rw-r--r--test/wpt/tests/fetch/content-encoding/bad-gzip-body.any.js22
-rw-r--r--test/wpt/tests/fetch/content-encoding/gzip-body.any.js16
-rw-r--r--test/wpt/tests/fetch/content-encoding/resources/bad-gzip-body.py3
-rw-r--r--test/wpt/tests/fetch/content-encoding/resources/foo.octetstream.gzbin0 -> 64 bytes
-rw-r--r--test/wpt/tests/fetch/content-encoding/resources/foo.octetstream.gz.headers2
-rw-r--r--test/wpt/tests/fetch/content-encoding/resources/foo.text.gzbin0 -> 57 bytes
-rw-r--r--test/wpt/tests/fetch/content-encoding/resources/foo.text.gz.headers2
-rw-r--r--test/wpt/tests/fetch/content-length/api-and-duplicate-headers.any.js23
-rw-r--r--test/wpt/tests/fetch/content-length/content-length.html14
-rw-r--r--test/wpt/tests/fetch/content-length/content-length.html.headers1
-rw-r--r--test/wpt/tests/fetch/content-length/parsing.window.js18
-rw-r--r--test/wpt/tests/fetch/content-length/resources/content-length.py10
-rw-r--r--test/wpt/tests/fetch/content-length/resources/content-lengths.json142
-rw-r--r--test/wpt/tests/fetch/content-length/resources/identical-duplicates.asis9
-rw-r--r--test/wpt/tests/fetch/content-length/too-long.window.js4
-rw-r--r--test/wpt/tests/fetch/content-type/README.md20
-rw-r--r--test/wpt/tests/fetch/content-type/multipart-malformed.any.js22
-rw-r--r--test/wpt/tests/fetch/content-type/multipart.window.js33
-rw-r--r--test/wpt/tests/fetch/content-type/resources/content-type.py18
-rw-r--r--test/wpt/tests/fetch/content-type/resources/content-types.json122
-rw-r--r--test/wpt/tests/fetch/content-type/resources/script-content-types.json92
-rw-r--r--test/wpt/tests/fetch/content-type/response.window.js72
-rw-r--r--test/wpt/tests/fetch/content-type/script.window.js48
-rw-r--r--test/wpt/tests/fetch/corb/README.md67
-rw-r--r--test/wpt/tests/fetch/corb/img-html-correctly-labeled.sub-ref.html4
-rw-r--r--test/wpt/tests/fetch/corb/img-html-correctly-labeled.sub.html11
-rw-r--r--test/wpt/tests/fetch/corb/img-mime-types-coverage.tentative.sub.html85
-rw-r--r--test/wpt/tests/fetch/corb/img-png-mislabeled-as-html-nosniff.tentative.sub-ref.html4
-rw-r--r--test/wpt/tests/fetch/corb/img-png-mislabeled-as-html-nosniff.tentative.sub.html11
-rw-r--r--test/wpt/tests/fetch/corb/img-png-mislabeled-as-html.sub-ref.html4
-rw-r--r--test/wpt/tests/fetch/corb/img-png-mislabeled-as-html.sub.html10
-rw-r--r--test/wpt/tests/fetch/corb/img-svg-doctype-html-mimetype-empty.sub.html7
-rw-r--r--test/wpt/tests/fetch/corb/img-svg-doctype-html-mimetype-svg.sub.html11
-rw-r--r--test/wpt/tests/fetch/corb/img-svg-invalid.sub-ref.html5
-rw-r--r--test/wpt/tests/fetch/corb/img-svg-labeled-as-dash.sub.html6
-rw-r--r--test/wpt/tests/fetch/corb/img-svg-labeled-as-svg-xml.sub.html6
-rw-r--r--test/wpt/tests/fetch/corb/img-svg-xml-decl.sub.html6
-rw-r--r--test/wpt/tests/fetch/corb/img-svg.sub-ref.html5
-rw-r--r--test/wpt/tests/fetch/corb/preload-image-png-mislabeled-as-html-nosniff.tentative.sub.html24
-rw-r--r--test/wpt/tests/fetch/corb/resources/css-mislabeled-as-html-nosniff.css1
-rw-r--r--test/wpt/tests/fetch/corb/resources/css-mislabeled-as-html-nosniff.css.headers2
-rw-r--r--test/wpt/tests/fetch/corb/resources/css-mislabeled-as-html.css1
-rw-r--r--test/wpt/tests/fetch/corb/resources/css-mislabeled-as-html.css.headers1
-rw-r--r--test/wpt/tests/fetch/corb/resources/css-with-json-parser-breaker.css3
-rw-r--r--test/wpt/tests/fetch/corb/resources/empty-labeled-as-png.png0
-rw-r--r--test/wpt/tests/fetch/corb/resources/empty-labeled-as-png.png.headers1
-rw-r--r--test/wpt/tests/fetch/corb/resources/html-correctly-labeled.html10
-rw-r--r--test/wpt/tests/fetch/corb/resources/html-correctly-labeled.html.headers1
-rw-r--r--test/wpt/tests/fetch/corb/resources/html-js-polyglot.js9
-rw-r--r--test/wpt/tests/fetch/corb/resources/html-js-polyglot.js.headers1
-rw-r--r--test/wpt/tests/fetch/corb/resources/html-js-polyglot2.js10
-rw-r--r--test/wpt/tests/fetch/corb/resources/html-js-polyglot2.js.headers1
-rw-r--r--test/wpt/tests/fetch/corb/resources/js-mislabeled-as-html-nosniff.js1
-rw-r--r--test/wpt/tests/fetch/corb/resources/js-mislabeled-as-html-nosniff.js.headers2
-rw-r--r--test/wpt/tests/fetch/corb/resources/js-mislabeled-as-html.js1
-rw-r--r--test/wpt/tests/fetch/corb/resources/js-mislabeled-as-html.js.headers1
-rw-r--r--test/wpt/tests/fetch/corb/resources/png-correctly-labeled.pngbin0 -> 1010 bytes
-rw-r--r--test/wpt/tests/fetch/corb/resources/png-correctly-labeled.png.headers1
-rw-r--r--test/wpt/tests/fetch/corb/resources/png-mislabeled-as-html-nosniff.pngbin0 -> 1010 bytes
-rw-r--r--test/wpt/tests/fetch/corb/resources/png-mislabeled-as-html-nosniff.png.headers2
-rw-r--r--test/wpt/tests/fetch/corb/resources/png-mislabeled-as-html.pngbin0 -> 1010 bytes
-rw-r--r--test/wpt/tests/fetch/corb/resources/png-mislabeled-as-html.png.headers1
-rw-r--r--test/wpt/tests/fetch/corb/resources/response_block_probe.js1
-rw-r--r--test/wpt/tests/fetch/corb/resources/response_block_probe.js.headers1
-rw-r--r--test/wpt/tests/fetch/corb/resources/sniffable-resource.py11
-rw-r--r--test/wpt/tests/fetch/corb/resources/subframe-that-posts-html-containing-blob-url-to-parent.html16
-rw-r--r--test/wpt/tests/fetch/corb/resources/svg-doctype-html-mimetype-empty.svg4
-rw-r--r--test/wpt/tests/fetch/corb/resources/svg-doctype-html-mimetype-empty.svg.headers1
-rw-r--r--test/wpt/tests/fetch/corb/resources/svg-doctype-html-mimetype-svg.svg4
-rw-r--r--test/wpt/tests/fetch/corb/resources/svg-doctype-html-mimetype-svg.svg.headers1
-rw-r--r--test/wpt/tests/fetch/corb/resources/svg-labeled-as-dash.svg3
-rw-r--r--test/wpt/tests/fetch/corb/resources/svg-labeled-as-dash.svg.headers1
-rw-r--r--test/wpt/tests/fetch/corb/resources/svg-labeled-as-svg-xml.svg3
-rw-r--r--test/wpt/tests/fetch/corb/resources/svg-labeled-as-svg-xml.svg.headers1
-rw-r--r--test/wpt/tests/fetch/corb/resources/svg-xml-decl.svg4
-rw-r--r--test/wpt/tests/fetch/corb/resources/svg.svg3
-rw-r--r--test/wpt/tests/fetch/corb/resources/svg.svg.headers1
-rw-r--r--test/wpt/tests/fetch/corb/response_block.tentative.https.html50
-rw-r--r--test/wpt/tests/fetch/corb/script-html-correctly-labeled.tentative.sub.html32
-rw-r--r--test/wpt/tests/fetch/corb/script-html-js-polyglot.sub.html32
-rw-r--r--test/wpt/tests/fetch/corb/script-html-via-cross-origin-blob-url.sub.html38
-rw-r--r--test/wpt/tests/fetch/corb/script-js-mislabeled-as-html-nosniff.sub.html33
-rw-r--r--test/wpt/tests/fetch/corb/script-js-mislabeled-as-html.sub.html25
-rw-r--r--test/wpt/tests/fetch/corb/script-resource-with-json-parser-breaker.tentative.sub.html85
-rw-r--r--test/wpt/tests/fetch/corb/script-resource-with-nonsniffable-types.tentative.sub.html84
-rw-r--r--test/wpt/tests/fetch/corb/style-css-mislabeled-as-html-nosniff.sub.html42
-rw-r--r--test/wpt/tests/fetch/corb/style-css-mislabeled-as-html.sub.html36
-rw-r--r--test/wpt/tests/fetch/corb/style-css-with-json-parser-breaker.sub.html38
-rw-r--r--test/wpt/tests/fetch/corb/style-html-correctly-labeled.sub.html41
-rw-r--r--test/wpt/tests/fetch/cross-origin-resource-policy/fetch-in-iframe.html67
-rw-r--r--test/wpt/tests/fetch/cross-origin-resource-policy/fetch.any.js76
-rw-r--r--test/wpt/tests/fetch/cross-origin-resource-policy/fetch.https.any.js56
-rw-r--r--test/wpt/tests/fetch/cross-origin-resource-policy/iframe-loads.html46
-rw-r--r--test/wpt/tests/fetch/cross-origin-resource-policy/image-loads.html54
-rw-r--r--test/wpt/tests/fetch/cross-origin-resource-policy/resources/green.pngbin0 -> 87 bytes
-rw-r--r--test/wpt/tests/fetch/cross-origin-resource-policy/resources/hello.py6
-rw-r--r--test/wpt/tests/fetch/cross-origin-resource-policy/resources/iframe.py5
-rw-r--r--test/wpt/tests/fetch/cross-origin-resource-policy/resources/iframeFetch.html19
-rw-r--r--test/wpt/tests/fetch/cross-origin-resource-policy/resources/image.py22
-rw-r--r--test/wpt/tests/fetch/cross-origin-resource-policy/resources/redirect.py6
-rw-r--r--test/wpt/tests/fetch/cross-origin-resource-policy/resources/script.py6
-rw-r--r--test/wpt/tests/fetch/cross-origin-resource-policy/scheme-restriction.any.js7
-rw-r--r--test/wpt/tests/fetch/cross-origin-resource-policy/scheme-restriction.https.window.js13
-rw-r--r--test/wpt/tests/fetch/cross-origin-resource-policy/script-loads.html52
-rw-r--r--test/wpt/tests/fetch/cross-origin-resource-policy/syntax.any.js19
-rw-r--r--test/wpt/tests/fetch/data-urls/README.md11
-rw-r--r--test/wpt/tests/fetch/data-urls/base64.any.js18
-rw-r--r--test/wpt/tests/fetch/data-urls/navigate.window.js75
-rw-r--r--test/wpt/tests/fetch/data-urls/processing.any.js22
-rw-r--r--test/wpt/tests/fetch/data-urls/resources/base64.json82
-rw-r--r--test/wpt/tests/fetch/data-urls/resources/data-urls.json214
-rw-r--r--test/wpt/tests/fetch/fetch-later/META.yml3
-rw-r--r--test/wpt/tests/fetch/fetch-later/README.md3
-rw-r--r--test/wpt/tests/fetch/fetch-later/basic.tentative.https.window.js13
-rw-r--r--test/wpt/tests/fetch/fetch-later/non-secure.window.js8
-rw-r--r--test/wpt/tests/fetch/fetch-later/sendondiscard.tentative.https.window.js28
-rw-r--r--test/wpt/tests/fetch/h1-parsing/README.md5
-rw-r--r--test/wpt/tests/fetch/h1-parsing/lone-cr.window.js23
-rw-r--r--test/wpt/tests/fetch/h1-parsing/resources-with-0x00-in-header.window.js31
-rw-r--r--test/wpt/tests/fetch/h1-parsing/resources/README.md6
-rw-r--r--test/wpt/tests/fetch/h1-parsing/resources/blue-with-0x00-in-a-header.asisbin0 -> 546 bytes
-rw-r--r--test/wpt/tests/fetch/h1-parsing/resources/document-with-0x00-in-header.py4
-rw-r--r--test/wpt/tests/fetch/h1-parsing/resources/message.py3
-rw-r--r--test/wpt/tests/fetch/h1-parsing/resources/script-with-0x00-in-header.py4
-rw-r--r--test/wpt/tests/fetch/h1-parsing/resources/status-code.py6
-rw-r--r--test/wpt/tests/fetch/h1-parsing/status-code.window.js98
-rw-r--r--test/wpt/tests/fetch/http-cache/304-update.any.js146
-rw-r--r--test/wpt/tests/fetch/http-cache/README.md72
-rw-r--r--test/wpt/tests/fetch/http-cache/basic-auth-cache-test-ref.html6
-rw-r--r--test/wpt/tests/fetch/http-cache/basic-auth-cache-test.html27
-rw-r--r--test/wpt/tests/fetch/http-cache/cache-mode.any.js61
-rw-r--r--test/wpt/tests/fetch/http-cache/cc-request.any.js202
-rw-r--r--test/wpt/tests/fetch/http-cache/credentials.tentative.any.js62
-rw-r--r--test/wpt/tests/fetch/http-cache/freshness.any.js215
-rw-r--r--test/wpt/tests/fetch/http-cache/heuristic.any.js93
-rw-r--r--test/wpt/tests/fetch/http-cache/http-cache.js274
-rw-r--r--test/wpt/tests/fetch/http-cache/invalidate.any.js235
-rw-r--r--test/wpt/tests/fetch/http-cache/partial.any.js208
-rw-r--r--test/wpt/tests/fetch/http-cache/post-patch.any.js46
-rw-r--r--test/wpt/tests/fetch/http-cache/resources/http-cache.py124
-rw-r--r--test/wpt/tests/fetch/http-cache/resources/securedimage.py19
-rw-r--r--test/wpt/tests/fetch/http-cache/resources/split-cache-popup-with-iframe.html34
-rw-r--r--test/wpt/tests/fetch/http-cache/resources/split-cache-popup.html28
-rw-r--r--test/wpt/tests/fetch/http-cache/split-cache.html158
-rw-r--r--test/wpt/tests/fetch/http-cache/status.any.js60
-rw-r--r--test/wpt/tests/fetch/http-cache/vary.any.js313
-rw-r--r--test/wpt/tests/fetch/images/canvas-remote-read-remote-image-redirect.html28
-rw-r--r--test/wpt/tests/fetch/metadata/META.yml4
-rw-r--r--test/wpt/tests/fetch/metadata/README.md9
-rw-r--r--test/wpt/tests/fetch/metadata/audio-worklet.https.html20
-rw-r--r--test/wpt/tests/fetch/metadata/embed.https.sub.tentative.html63
-rw-r--r--test/wpt/tests/fetch/metadata/fetch-preflight.https.sub.any.js29
-rw-r--r--test/wpt/tests/fetch/metadata/fetch.https.sub.any.js58
-rw-r--r--test/wpt/tests/fetch/metadata/generated/appcache-manifest.https.sub.html341
-rw-r--r--test/wpt/tests/fetch/metadata/generated/audioworklet.https.sub.html271
-rw-r--r--test/wpt/tests/fetch/metadata/generated/css-font-face.https.sub.tentative.html230
-rw-r--r--test/wpt/tests/fetch/metadata/generated/css-font-face.sub.tentative.html196
-rw-r--r--test/wpt/tests/fetch/metadata/generated/css-images.https.sub.tentative.html1384
-rw-r--r--test/wpt/tests/fetch/metadata/generated/css-images.sub.tentative.html1099
-rw-r--r--test/wpt/tests/fetch/metadata/generated/element-a.https.sub.html482
-rw-r--r--test/wpt/tests/fetch/metadata/generated/element-a.sub.html342
-rw-r--r--test/wpt/tests/fetch/metadata/generated/element-area.https.sub.html482
-rw-r--r--test/wpt/tests/fetch/metadata/generated/element-area.sub.html342
-rw-r--r--test/wpt/tests/fetch/metadata/generated/element-audio.https.sub.html325
-rw-r--r--test/wpt/tests/fetch/metadata/generated/element-audio.sub.html229
-rw-r--r--test/wpt/tests/fetch/metadata/generated/element-embed.https.sub.html224
-rw-r--r--test/wpt/tests/fetch/metadata/generated/element-embed.sub.html190
-rw-r--r--test/wpt/tests/fetch/metadata/generated/element-frame.https.sub.html309
-rw-r--r--test/wpt/tests/fetch/metadata/generated/element-frame.sub.html250
-rw-r--r--test/wpt/tests/fetch/metadata/generated/element-iframe.https.sub.html309
-rw-r--r--test/wpt/tests/fetch/metadata/generated/element-iframe.sub.html250
-rw-r--r--test/wpt/tests/fetch/metadata/generated/element-img-environment-change.https.sub.html357
-rw-r--r--test/wpt/tests/fetch/metadata/generated/element-img-environment-change.sub.html270
-rw-r--r--test/wpt/tests/fetch/metadata/generated/element-img.https.sub.html645
-rw-r--r--test/wpt/tests/fetch/metadata/generated/element-img.sub.html456
-rw-r--r--test/wpt/tests/fetch/metadata/generated/element-input-image.https.sub.html229
-rw-r--r--test/wpt/tests/fetch/metadata/generated/element-input-image.sub.html184
-rw-r--r--test/wpt/tests/fetch/metadata/generated/element-link-icon.https.sub.html371
-rw-r--r--test/wpt/tests/fetch/metadata/generated/element-link-icon.sub.html279
-rw-r--r--test/wpt/tests/fetch/metadata/generated/element-link-prefetch.https.optional.sub.html559
-rw-r--r--test/wpt/tests/fetch/metadata/generated/element-link-prefetch.optional.sub.html275
-rw-r--r--test/wpt/tests/fetch/metadata/generated/element-meta-refresh.https.optional.sub.html276
-rw-r--r--test/wpt/tests/fetch/metadata/generated/element-meta-refresh.optional.sub.html225
-rw-r--r--test/wpt/tests/fetch/metadata/generated/element-picture.https.sub.html997
-rw-r--r--test/wpt/tests/fetch/metadata/generated/element-picture.sub.html721
-rw-r--r--test/wpt/tests/fetch/metadata/generated/element-script.https.sub.html593
-rw-r--r--test/wpt/tests/fetch/metadata/generated/element-script.sub.html488
-rw-r--r--test/wpt/tests/fetch/metadata/generated/element-video-poster.https.sub.html243
-rw-r--r--test/wpt/tests/fetch/metadata/generated/element-video-poster.sub.html198
-rw-r--r--test/wpt/tests/fetch/metadata/generated/element-video.https.sub.html325
-rw-r--r--test/wpt/tests/fetch/metadata/generated/element-video.sub.html229
-rw-r--r--test/wpt/tests/fetch/metadata/generated/fetch-via-serviceworker.https.sub.html683
-rw-r--r--test/wpt/tests/fetch/metadata/generated/fetch.https.sub.html302
-rw-r--r--test/wpt/tests/fetch/metadata/generated/fetch.sub.html220
-rw-r--r--test/wpt/tests/fetch/metadata/generated/form-submission.https.sub.html522
-rw-r--r--test/wpt/tests/fetch/metadata/generated/form-submission.sub.html400
-rw-r--r--test/wpt/tests/fetch/metadata/generated/header-link.https.sub.html529
-rw-r--r--test/wpt/tests/fetch/metadata/generated/header-link.https.sub.tentative.html51
-rw-r--r--test/wpt/tests/fetch/metadata/generated/header-link.sub.html460
-rw-r--r--test/wpt/tests/fetch/metadata/generated/header-refresh.https.optional.sub.html273
-rw-r--r--test/wpt/tests/fetch/metadata/generated/header-refresh.optional.sub.html222
-rw-r--r--test/wpt/tests/fetch/metadata/generated/script-module-import-dynamic.https.sub.html254
-rw-r--r--test/wpt/tests/fetch/metadata/generated/script-module-import-dynamic.sub.html214
-rw-r--r--test/wpt/tests/fetch/metadata/generated/script-module-import-static.https.sub.html288
-rw-r--r--test/wpt/tests/fetch/metadata/generated/script-module-import-static.sub.html246
-rw-r--r--test/wpt/tests/fetch/metadata/generated/serviceworker.https.sub.html170
-rw-r--r--test/wpt/tests/fetch/metadata/generated/svg-image.https.sub.html367
-rw-r--r--test/wpt/tests/fetch/metadata/generated/svg-image.sub.html265
-rw-r--r--test/wpt/tests/fetch/metadata/generated/window-history.https.sub.html237
-rw-r--r--test/wpt/tests/fetch/metadata/generated/window-history.sub.html360
-rw-r--r--test/wpt/tests/fetch/metadata/generated/window-location.https.sub.html1184
-rw-r--r--test/wpt/tests/fetch/metadata/generated/window-location.sub.html894
-rw-r--r--test/wpt/tests/fetch/metadata/generated/worker-dedicated-constructor.https.sub.html118
-rw-r--r--test/wpt/tests/fetch/metadata/generated/worker-dedicated-constructor.sub.html204
-rw-r--r--test/wpt/tests/fetch/metadata/generated/worker-dedicated-importscripts.https.sub.html268
-rw-r--r--test/wpt/tests/fetch/metadata/generated/worker-dedicated-importscripts.sub.html228
-rw-r--r--test/wpt/tests/fetch/metadata/navigation.https.sub.html23
-rw-r--r--test/wpt/tests/fetch/metadata/object.https.sub.html62
-rw-r--r--test/wpt/tests/fetch/metadata/paint-worklet.https.html19
-rw-r--r--test/wpt/tests/fetch/metadata/portal.https.sub.html50
-rw-r--r--test/wpt/tests/fetch/metadata/preload.https.sub.html50
-rw-r--r--test/wpt/tests/fetch/metadata/redirect/multiple-redirect-https-downgrade-upgrade.sub.html18
-rw-r--r--test/wpt/tests/fetch/metadata/redirect/redirect-http-upgrade.sub.html17
-rw-r--r--test/wpt/tests/fetch/metadata/redirect/redirect-https-downgrade.sub.html17
-rw-r--r--test/wpt/tests/fetch/metadata/report.https.sub.html33
-rw-r--r--test/wpt/tests/fetch/metadata/report.https.sub.html.sub.headers3
-rw-r--r--test/wpt/tests/fetch/metadata/resources/appcache-iframe.sub.html15
-rw-r--r--test/wpt/tests/fetch/metadata/resources/dedicatedWorker.js1
-rw-r--r--test/wpt/tests/fetch/metadata/resources/echo-as-json.py29
-rw-r--r--test/wpt/tests/fetch/metadata/resources/echo-as-script.py14
-rw-r--r--test/wpt/tests/fetch/metadata/resources/es-module.sub.js1
-rw-r--r--test/wpt/tests/fetch/metadata/resources/fetch-via-serviceworker--fallback--sw.js3
-rw-r--r--test/wpt/tests/fetch/metadata/resources/fetch-via-serviceworker--respondWith--sw.js3
-rw-r--r--test/wpt/tests/fetch/metadata/resources/fetch-via-serviceworker-frame.html3
-rw-r--r--test/wpt/tests/fetch/metadata/resources/header-link.py15
-rw-r--r--test/wpt/tests/fetch/metadata/resources/helper.js42
-rw-r--r--test/wpt/tests/fetch/metadata/resources/helper.sub.js67
-rw-r--r--test/wpt/tests/fetch/metadata/resources/message-opener.html17
-rw-r--r--test/wpt/tests/fetch/metadata/resources/post-to-owner.py36
-rw-r--r--test/wpt/tests/fetch/metadata/resources/record-header.py145
-rw-r--r--test/wpt/tests/fetch/metadata/resources/record-headers.py73
-rw-r--r--test/wpt/tests/fetch/metadata/resources/redirectTestHelper.sub.js167
-rw-r--r--test/wpt/tests/fetch/metadata/resources/serviceworker-accessors-frame.html3
-rw-r--r--test/wpt/tests/fetch/metadata/resources/serviceworker-accessors.sw.js14
-rw-r--r--test/wpt/tests/fetch/metadata/resources/sharedWorker.js9
-rw-r--r--test/wpt/tests/fetch/metadata/resources/unload-with-beacon.html12
-rw-r--r--test/wpt/tests/fetch/metadata/resources/xslt-test.sub.xml12
-rw-r--r--test/wpt/tests/fetch/metadata/serviceworker-accessors.https.sub.html51
-rw-r--r--test/wpt/tests/fetch/metadata/sharedworker.https.sub.html40
-rw-r--r--test/wpt/tests/fetch/metadata/style.https.sub.html86
-rw-r--r--test/wpt/tests/fetch/metadata/tools/README.md126
-rw-r--r--test/wpt/tests/fetch/metadata/tools/fetch-metadata.conf.yml806
-rw-r--r--test/wpt/tests/fetch/metadata/tools/generate.py195
-rw-r--r--test/wpt/tests/fetch/metadata/tools/templates/appcache-manifest.sub.https.html63
-rw-r--r--test/wpt/tests/fetch/metadata/tools/templates/audioworklet.https.sub.html53
-rw-r--r--test/wpt/tests/fetch/metadata/tools/templates/css-font-face.sub.html60
-rw-r--r--test/wpt/tests/fetch/metadata/tools/templates/css-images.sub.html137
-rw-r--r--test/wpt/tests/fetch/metadata/tools/templates/element-a.sub.html72
-rw-r--r--test/wpt/tests/fetch/metadata/tools/templates/element-area.sub.html72
-rw-r--r--test/wpt/tests/fetch/metadata/tools/templates/element-audio.sub.html51
-rw-r--r--test/wpt/tests/fetch/metadata/tools/templates/element-embed.sub.html54
-rw-r--r--test/wpt/tests/fetch/metadata/tools/templates/element-frame.sub.html62
-rw-r--r--test/wpt/tests/fetch/metadata/tools/templates/element-iframe.sub.html62
-rw-r--r--test/wpt/tests/fetch/metadata/tools/templates/element-img-environment-change.sub.html78
-rw-r--r--test/wpt/tests/fetch/metadata/tools/templates/element-img.sub.html52
-rw-r--r--test/wpt/tests/fetch/metadata/tools/templates/element-input-image.sub.html48
-rw-r--r--test/wpt/tests/fetch/metadata/tools/templates/element-link-icon.sub.html75
-rw-r--r--test/wpt/tests/fetch/metadata/tools/templates/element-link-prefetch.optional.sub.html71
-rw-r--r--test/wpt/tests/fetch/metadata/tools/templates/element-meta-refresh.optional.sub.html60
-rw-r--r--test/wpt/tests/fetch/metadata/tools/templates/element-picture.sub.html101
-rw-r--r--test/wpt/tests/fetch/metadata/tools/templates/element-script.sub.html54
-rw-r--r--test/wpt/tests/fetch/metadata/tools/templates/element-video-poster.sub.html62
-rw-r--r--test/wpt/tests/fetch/metadata/tools/templates/element-video.sub.html51
-rw-r--r--test/wpt/tests/fetch/metadata/tools/templates/fetch-via-serviceworker.https.sub.html88
-rw-r--r--test/wpt/tests/fetch/metadata/tools/templates/fetch.sub.html42
-rw-r--r--test/wpt/tests/fetch/metadata/tools/templates/form-submission.sub.html87
-rw-r--r--test/wpt/tests/fetch/metadata/tools/templates/header-link.sub.html56
-rw-r--r--test/wpt/tests/fetch/metadata/tools/templates/header-refresh.optional.sub.html59
-rw-r--r--test/wpt/tests/fetch/metadata/tools/templates/script-module-import-dynamic.sub.html35
-rw-r--r--test/wpt/tests/fetch/metadata/tools/templates/script-module-import-static.sub.html53
-rw-r--r--test/wpt/tests/fetch/metadata/tools/templates/serviceworker.https.sub.html72
-rw-r--r--test/wpt/tests/fetch/metadata/tools/templates/svg-image.sub.html75
-rw-r--r--test/wpt/tests/fetch/metadata/tools/templates/window-history.sub.html134
-rw-r--r--test/wpt/tests/fetch/metadata/tools/templates/window-location.sub.html128
-rw-r--r--test/wpt/tests/fetch/metadata/tools/templates/worker-dedicated-constructor.sub.html49
-rw-r--r--test/wpt/tests/fetch/metadata/tools/templates/worker-dedicated-importscripts.sub.html54
-rw-r--r--test/wpt/tests/fetch/metadata/track.https.sub.html119
-rw-r--r--test/wpt/tests/fetch/metadata/trailing-dot.https.sub.any.js30
-rw-r--r--test/wpt/tests/fetch/metadata/unload.https.sub.html64
-rw-r--r--test/wpt/tests/fetch/metadata/window-open.https.sub.html199
-rw-r--r--test/wpt/tests/fetch/metadata/worker.https.sub.html24
-rw-r--r--test/wpt/tests/fetch/metadata/xslt.https.sub.html25
-rw-r--r--test/wpt/tests/fetch/nosniff/image.html39
-rw-r--r--test/wpt/tests/fetch/nosniff/importscripts.html14
-rw-r--r--test/wpt/tests/fetch/nosniff/importscripts.js28
-rw-r--r--test/wpt/tests/fetch/nosniff/parsing-nosniff.window.js27
-rw-r--r--test/wpt/tests/fetch/nosniff/resources/css.py23
-rw-r--r--test/wpt/tests/fetch/nosniff/resources/image.py24
-rw-r--r--test/wpt/tests/fetch/nosniff/resources/js.py17
-rw-r--r--test/wpt/tests/fetch/nosniff/resources/nosniff.py11
-rw-r--r--test/wpt/tests/fetch/nosniff/resources/worker.py16
-rw-r--r--test/wpt/tests/fetch/nosniff/resources/x-content-type-options.json62
-rw-r--r--test/wpt/tests/fetch/nosniff/script.html43
-rw-r--r--test/wpt/tests/fetch/nosniff/stylesheet.html60
-rw-r--r--test/wpt/tests/fetch/nosniff/worker.html28
-rw-r--r--test/wpt/tests/fetch/orb/resources/data.json3
-rw-r--r--test/wpt/tests/fetch/orb/resources/data_non_ascii.json1
-rw-r--r--test/wpt/tests/fetch/orb/resources/empty.json1
-rw-r--r--test/wpt/tests/fetch/orb/resources/font.ttfbin0 -> 2528 bytes
-rw-r--r--test/wpt/tests/fetch/orb/resources/image.pngbin0 -> 1010 bytes
-rw-r--r--test/wpt/tests/fetch/orb/resources/js-unlabeled-utf16-without-bom.jsonbin0 -> 70 bytes
-rw-r--r--test/wpt/tests/fetch/orb/resources/js-unlabeled.js1
-rw-r--r--test/wpt/tests/fetch/orb/resources/png-mislabeled-as-html.pngbin0 -> 1010 bytes
-rw-r--r--test/wpt/tests/fetch/orb/resources/png-mislabeled-as-html.png.headers1
-rw-r--r--test/wpt/tests/fetch/orb/resources/png-unlabeled.pngbin0 -> 1010 bytes
-rw-r--r--test/wpt/tests/fetch/orb/resources/script-asm-js-invalid.js4
-rw-r--r--test/wpt/tests/fetch/orb/resources/script-asm-js-valid.js4
-rw-r--r--test/wpt/tests/fetch/orb/resources/script-iso-8559-1.js4
-rw-r--r--test/wpt/tests/fetch/orb/resources/script-utf16-bom.jsbin0 -> 92 bytes
-rw-r--r--test/wpt/tests/fetch/orb/resources/script-utf16-without-bom.jsbin0 -> 90 bytes
-rw-r--r--test/wpt/tests/fetch/orb/resources/script.js4
-rw-r--r--test/wpt/tests/fetch/orb/resources/sound.mp3bin0 -> 539 bytes
-rw-r--r--test/wpt/tests/fetch/orb/resources/text.txt1
-rw-r--r--test/wpt/tests/fetch/orb/resources/utils.js18
-rw-r--r--test/wpt/tests/fetch/orb/tentative/compressed-image-sniffing.sub.html20
-rw-r--r--test/wpt/tests/fetch/orb/tentative/content-range.sub.any.js31
-rw-r--r--test/wpt/tests/fetch/orb/tentative/img-mime-types-coverage.tentative.sub.html126
-rw-r--r--test/wpt/tests/fetch/orb/tentative/img-png-mislabeled-as-html.sub-ref.html5
-rw-r--r--test/wpt/tests/fetch/orb/tentative/img-png-mislabeled-as-html.sub.html7
-rw-r--r--test/wpt/tests/fetch/orb/tentative/img-png-unlabeled.sub-ref.html5
-rw-r--r--test/wpt/tests/fetch/orb/tentative/img-png-unlabeled.sub.html7
-rw-r--r--test/wpt/tests/fetch/orb/tentative/known-mime-type.sub.any.js86
-rw-r--r--test/wpt/tests/fetch/orb/tentative/nosniff.sub.any.js59
-rw-r--r--test/wpt/tests/fetch/orb/tentative/script-js-unlabeled-gziped.sub.html24
-rw-r--r--test/wpt/tests/fetch/orb/tentative/script-unlabeled.sub.html24
-rw-r--r--test/wpt/tests/fetch/orb/tentative/script-utf16-without-bom-hint-charset.sub.html22
-rw-r--r--test/wpt/tests/fetch/orb/tentative/status.sub.any.js33
-rw-r--r--test/wpt/tests/fetch/orb/tentative/status.sub.html17
-rw-r--r--test/wpt/tests/fetch/orb/tentative/unknown-mime-type.sub.any.js28
-rw-r--r--test/wpt/tests/fetch/origin/assorted.window.js211
-rw-r--r--test/wpt/tests/fetch/origin/resources/redirect-and-stash.py38
-rw-r--r--test/wpt/tests/fetch/origin/resources/referrer-policy.py7
-rw-r--r--test/wpt/tests/fetch/private-network-access/META.yml7
-rw-r--r--test/wpt/tests/fetch/private-network-access/README.md10
-rw-r--r--test/wpt/tests/fetch/private-network-access/fenced-frame-no-preflight-required.tentative.https.window.js91
-rw-r--r--test/wpt/tests/fetch/private-network-access/fenced-frame-subresource-fetch.tentative.https.window.js330
-rw-r--r--test/wpt/tests/fetch/private-network-access/fenced-frame.tentative.https.window.js150
-rw-r--r--test/wpt/tests/fetch/private-network-access/fetch-from-treat-as-public.tentative.https.window.js80
-rw-r--r--test/wpt/tests/fetch/private-network-access/fetch.tentative.https.window.js271
-rw-r--r--test/wpt/tests/fetch/private-network-access/fetch.tentative.window.js183
-rw-r--r--test/wpt/tests/fetch/private-network-access/iframe.tentative.https.window.js266
-rw-r--r--test/wpt/tests/fetch/private-network-access/iframe.tentative.window.js110
-rw-r--r--test/wpt/tests/fetch/private-network-access/mixed-content-fetch.tentative.https.window.js277
-rw-r--r--test/wpt/tests/fetch/private-network-access/nested-worker.tentative.https.window.js36
-rw-r--r--test/wpt/tests/fetch/private-network-access/nested-worker.tentative.window.js36
-rw-r--r--test/wpt/tests/fetch/private-network-access/preflight-cache.https.tentative.window.js88
-rw-r--r--test/wpt/tests/fetch/private-network-access/redirect.tentative.https.window.js640
-rw-r--r--test/wpt/tests/fetch/private-network-access/resources/executor.html9
-rw-r--r--test/wpt/tests/fetch/private-network-access/resources/fenced-frame-fetcher.https.html25
-rw-r--r--test/wpt/tests/fetch/private-network-access/resources/fenced-frame-fetcher.https.html.headers1
-rw-r--r--test/wpt/tests/fetch/private-network-access/resources/fenced-frame-local-network-access-target.https.html8
-rw-r--r--test/wpt/tests/fetch/private-network-access/resources/fenced-frame-local-network-access.https.html14
-rw-r--r--test/wpt/tests/fetch/private-network-access/resources/fenced-frame-local-network-access.https.html.headers1
-rw-r--r--test/wpt/tests/fetch/private-network-access/resources/fetcher.html21
-rw-r--r--test/wpt/tests/fetch/private-network-access/resources/fetcher.js20
-rw-r--r--test/wpt/tests/fetch/private-network-access/resources/iframed.html7
-rw-r--r--test/wpt/tests/fetch/private-network-access/resources/iframer.html9
-rw-r--r--test/wpt/tests/fetch/private-network-access/resources/preflight.py175
-rw-r--r--test/wpt/tests/fetch/private-network-access/resources/service-worker-bridge.html155
-rw-r--r--test/wpt/tests/fetch/private-network-access/resources/service-worker.js18
-rw-r--r--test/wpt/tests/fetch/private-network-access/resources/shared-fetcher.js23
-rw-r--r--test/wpt/tests/fetch/private-network-access/resources/shared-worker-blob-fetcher.html50
-rw-r--r--test/wpt/tests/fetch/private-network-access/resources/shared-worker-fetcher.html19
-rw-r--r--test/wpt/tests/fetch/private-network-access/resources/socket-opener.html15
-rw-r--r--test/wpt/tests/fetch/private-network-access/resources/support.sub.js759
-rw-r--r--test/wpt/tests/fetch/private-network-access/resources/worker-blob-fetcher.html45
-rw-r--r--test/wpt/tests/fetch/private-network-access/resources/worker-fetcher.html18
-rw-r--r--test/wpt/tests/fetch/private-network-access/resources/worker-fetcher.js11
-rw-r--r--test/wpt/tests/fetch/private-network-access/resources/xhr-sender.html33
-rw-r--r--test/wpt/tests/fetch/private-network-access/service-worker-background-fetch.tentative.https.window.js142
-rw-r--r--test/wpt/tests/fetch/private-network-access/service-worker-fetch.tentative.https.window.js235
-rw-r--r--test/wpt/tests/fetch/private-network-access/service-worker-update.tentative.https.window.js106
-rw-r--r--test/wpt/tests/fetch/private-network-access/service-worker.tentative.https.window.js84
-rw-r--r--test/wpt/tests/fetch/private-network-access/shared-worker-blob-fetch.tentative.https.window.js168
-rw-r--r--test/wpt/tests/fetch/private-network-access/shared-worker-blob-fetch.tentative.window.js173
-rw-r--r--test/wpt/tests/fetch/private-network-access/shared-worker-fetch.tentative.https.window.js167
-rw-r--r--test/wpt/tests/fetch/private-network-access/shared-worker-fetch.tentative.window.js154
-rw-r--r--test/wpt/tests/fetch/private-network-access/shared-worker.tentative.https.window.js34
-rw-r--r--test/wpt/tests/fetch/private-network-access/shared-worker.tentative.window.js34
-rw-r--r--test/wpt/tests/fetch/private-network-access/websocket.tentative.https.window.js40
-rw-r--r--test/wpt/tests/fetch/private-network-access/websocket.tentative.window.js40
-rw-r--r--test/wpt/tests/fetch/private-network-access/worker-blob-fetch.tentative.window.js155
-rw-r--r--test/wpt/tests/fetch/private-network-access/worker-fetch.tentative.https.window.js151
-rw-r--r--test/wpt/tests/fetch/private-network-access/worker-fetch.tentative.window.js154
-rw-r--r--test/wpt/tests/fetch/private-network-access/worker.tentative.https.window.js37
-rw-r--r--test/wpt/tests/fetch/private-network-access/worker.tentative.window.js37
-rw-r--r--test/wpt/tests/fetch/private-network-access/xhr-from-treat-as-public.tentative.https.window.js83
-rw-r--r--test/wpt/tests/fetch/private-network-access/xhr.https.tentative.window.js142
-rw-r--r--test/wpt/tests/fetch/private-network-access/xhr.tentative.window.js195
-rw-r--r--test/wpt/tests/fetch/range/blob.any.js233
-rw-r--r--test/wpt/tests/fetch/range/data.any.js29
-rw-r--r--test/wpt/tests/fetch/range/general.any.js140
-rw-r--r--test/wpt/tests/fetch/range/general.window.js29
-rw-r--r--test/wpt/tests/fetch/range/non-matching-range-response.html34
-rw-r--r--test/wpt/tests/fetch/range/resources/basic.html1
-rw-r--r--test/wpt/tests/fetch/range/resources/long-wav.py134
-rw-r--r--test/wpt/tests/fetch/range/resources/partial-script.py29
-rw-r--r--test/wpt/tests/fetch/range/resources/partial-text.py53
-rw-r--r--test/wpt/tests/fetch/range/resources/range-sw.js218
-rw-r--r--test/wpt/tests/fetch/range/resources/stash-take.py7
-rw-r--r--test/wpt/tests/fetch/range/resources/utils.js36
-rw-r--r--test/wpt/tests/fetch/range/resources/video-with-range.py43
-rw-r--r--test/wpt/tests/fetch/range/sw.https.window.js228
-rw-r--r--test/wpt/tests/fetch/redirect-navigate/302-found-post-handler.py15
-rw-r--r--test/wpt/tests/fetch/redirect-navigate/302-found-post.html20
-rw-r--r--test/wpt/tests/fetch/redirect-navigate/preserve-fragment.html202
-rw-r--r--test/wpt/tests/fetch/redirect-navigate/resources/destination.html28
-rw-r--r--test/wpt/tests/fetch/redirects/data.window.js25
-rw-r--r--test/wpt/tests/fetch/redirects/subresource-fragments.html39
-rw-r--r--test/wpt/tests/fetch/security/1xx-response.any.js28
-rw-r--r--test/wpt/tests/fetch/security/dangling-markup-mitigation-data-url.tentative.sub.html229
-rw-r--r--test/wpt/tests/fetch/security/dangling-markup-mitigation.tentative.html147
-rw-r--r--test/wpt/tests/fetch/security/embedded-credentials.tentative.sub.html89
-rw-r--r--test/wpt/tests/fetch/security/redirect-to-url-with-credentials.https.html68
-rw-r--r--test/wpt/tests/fetch/security/support/embedded-credential-window.sub.html19
-rw-r--r--test/wpt/tests/fetch/stale-while-revalidate/fetch-sw.https.html65
-rw-r--r--test/wpt/tests/fetch/stale-while-revalidate/fetch.any.js32
-rw-r--r--test/wpt/tests/fetch/stale-while-revalidate/resources/stale-css.py28
-rw-r--r--test/wpt/tests/fetch/stale-while-revalidate/resources/stale-image.py40
-rw-r--r--test/wpt/tests/fetch/stale-while-revalidate/resources/stale-script.py32
-rw-r--r--test/wpt/tests/fetch/stale-while-revalidate/revalidate-not-blocked-by-csp.html69
-rw-r--r--test/wpt/tests/fetch/stale-while-revalidate/stale-css.html51
-rw-r--r--test/wpt/tests/fetch/stale-while-revalidate/stale-image.html55
-rw-r--r--test/wpt/tests/fetch/stale-while-revalidate/stale-script.html59
-rw-r--r--test/wpt/tests/fetch/stale-while-revalidate/sw-intercept.js14
-rw-r--r--test/wpt/tests/interfaces/ANGLE_instanced_arrays.idl12
-rw-r--r--test/wpt/tests/interfaces/CSP.idl56
-rw-r--r--test/wpt/tests/interfaces/DOM-Parsing.idl26
-rw-r--r--test/wpt/tests/interfaces/EXT_blend_minmax.idl10
-rw-r--r--test/wpt/tests/interfaces/EXT_color_buffer_float.idl8
-rw-r--r--test/wpt/tests/interfaces/EXT_color_buffer_half_float.idl12
-rw-r--r--test/wpt/tests/interfaces/EXT_disjoint_timer_query.idl30
-rw-r--r--test/wpt/tests/interfaces/EXT_disjoint_timer_query_webgl2.idl14
-rw-r--r--test/wpt/tests/interfaces/EXT_float_blend.idl8
-rw-r--r--test/wpt/tests/interfaces/EXT_frag_depth.idl8
-rw-r--r--test/wpt/tests/interfaces/EXT_sRGB.idl12
-rw-r--r--test/wpt/tests/interfaces/EXT_shader_texture_lod.idl8
-rw-r--r--test/wpt/tests/interfaces/EXT_texture_compression_bptc.idl12
-rw-r--r--test/wpt/tests/interfaces/EXT_texture_compression_rgtc.idl12
-rw-r--r--test/wpt/tests/interfaces/EXT_texture_filter_anisotropic.idl10
-rw-r--r--test/wpt/tests/interfaces/EXT_texture_norm16.idl16
-rw-r--r--test/wpt/tests/interfaces/FedCM.idl67
-rw-r--r--test/wpt/tests/interfaces/FileAPI.idl100
-rw-r--r--test/wpt/tests/interfaces/IndexedDB.idl226
-rw-r--r--test/wpt/tests/interfaces/KHR_parallel_shader_compile.idl9
-rw-r--r--test/wpt/tests/interfaces/META.yml2
-rw-r--r--test/wpt/tests/interfaces/OES_draw_buffers_indexed.idl26
-rw-r--r--test/wpt/tests/interfaces/OES_element_index_uint.idl8
-rw-r--r--test/wpt/tests/interfaces/OES_fbo_render_mipmap.idl8
-rw-r--r--test/wpt/tests/interfaces/OES_standard_derivatives.idl9
-rw-r--r--test/wpt/tests/interfaces/OES_texture_float.idl7
-rw-r--r--test/wpt/tests/interfaces/OES_texture_float_linear.idl7
-rw-r--r--test/wpt/tests/interfaces/OES_texture_half_float.idl9
-rw-r--r--test/wpt/tests/interfaces/OES_texture_half_float_linear.idl7
-rw-r--r--test/wpt/tests/interfaces/OES_vertex_array_object.idl18
-rw-r--r--test/wpt/tests/interfaces/OVR_multiview2.idl14
-rw-r--r--test/wpt/tests/interfaces/README.md3
-rw-r--r--test/wpt/tests/interfaces/SVG.idl693
-rw-r--r--test/wpt/tests/interfaces/WEBGL_blend_equation_advanced_coherent.idl23
-rw-r--r--test/wpt/tests/interfaces/WEBGL_clip_cull_distance.idl20
-rw-r--r--test/wpt/tests/interfaces/WEBGL_color_buffer_float.idl11
-rw-r--r--test/wpt/tests/interfaces/WEBGL_compressed_texture_astc.idl41
-rw-r--r--test/wpt/tests/interfaces/WEBGL_compressed_texture_etc.idl19
-rw-r--r--test/wpt/tests/interfaces/WEBGL_compressed_texture_etc1.idl10
-rw-r--r--test/wpt/tests/interfaces/WEBGL_compressed_texture_pvrtc.idl13
-rw-r--r--test/wpt/tests/interfaces/WEBGL_compressed_texture_s3tc.idl13
-rw-r--r--test/wpt/tests/interfaces/WEBGL_compressed_texture_s3tc_srgb.idl13
-rw-r--r--test/wpt/tests/interfaces/WEBGL_debug_renderer_info.idl12
-rw-r--r--test/wpt/tests/interfaces/WEBGL_debug_shaders.idl11
-rw-r--r--test/wpt/tests/interfaces/WEBGL_depth_texture.idl9
-rw-r--r--test/wpt/tests/interfaces/WEBGL_draw_buffers.idl46
-rw-r--r--test/wpt/tests/interfaces/WEBGL_draw_instanced_base_vertex_base_instance.idl14
-rw-r--r--test/wpt/tests/interfaces/WEBGL_lose_context.idl10
-rw-r--r--test/wpt/tests/interfaces/WEBGL_multi_draw.idl32
-rw-r--r--test/wpt/tests/interfaces/WEBGL_multi_draw_instanced_base_vertex_base_instance.idl26
-rw-r--r--test/wpt/tests/interfaces/WEBGL_provoking_vertex.idl13
-rw-r--r--test/wpt/tests/interfaces/WebCryptoAPI.idl237
-rw-r--r--test/wpt/tests/interfaces/accelerometer.idl40
-rw-r--r--test/wpt/tests/interfaces/ambient-light.idl14
-rw-r--r--test/wpt/tests/interfaces/anchors.idl37
-rw-r--r--test/wpt/tests/interfaces/attribution-reporting-api.idl26
-rw-r--r--test/wpt/tests/interfaces/audio-output.idl17
-rw-r--r--test/wpt/tests/interfaces/autoplay-detection.idl19
-rw-r--r--test/wpt/tests/interfaces/background-fetch.idl89
-rw-r--r--test/wpt/tests/interfaces/background-sync.idl30
-rw-r--r--test/wpt/tests/interfaces/badging.idl15
-rw-r--r--test/wpt/tests/interfaces/battery-status.idl21
-rw-r--r--test/wpt/tests/interfaces/beacon.idl8
-rw-r--r--test/wpt/tests/interfaces/capture-handle-identity.idl27
-rw-r--r--test/wpt/tests/interfaces/captured-mouse-events.tentative.idl25
-rw-r--r--test/wpt/tests/interfaces/clipboard-apis.idl51
-rw-r--r--test/wpt/tests/interfaces/close-watcher.idl19
-rw-r--r--test/wpt/tests/interfaces/compat.idl13
-rw-r--r--test/wpt/tests/interfaces/compression.idl22
-rw-r--r--test/wpt/tests/interfaces/compute-pressure.idl37
-rw-r--r--test/wpt/tests/interfaces/console.idl34
-rw-r--r--test/wpt/tests/interfaces/contact-picker.idl44
-rw-r--r--test/wpt/tests/interfaces/content-index.idl46
-rw-r--r--test/wpt/tests/interfaces/cookie-store.idl110
-rw-r--r--test/wpt/tests/interfaces/credential-management.idl105
-rw-r--r--test/wpt/tests/interfaces/csp-embedded-enforcement.idl8
-rw-r--r--test/wpt/tests/interfaces/csp-next.idl21
-rw-r--r--test/wpt/tests/interfaces/css-anchor-position.idl11
-rw-r--r--test/wpt/tests/interfaces/css-animation-worklet.idl37
-rw-r--r--test/wpt/tests/interfaces/css-animations-2.idl9
-rw-r--r--test/wpt/tests/interfaces/css-animations.idl47
-rw-r--r--test/wpt/tests/interfaces/css-cascade-6.idl10
-rw-r--r--test/wpt/tests/interfaces/css-cascade.idl14
-rw-r--r--test/wpt/tests/interfaces/css-color-5.idl12
-rw-r--r--test/wpt/tests/interfaces/css-conditional.idl27
-rw-r--r--test/wpt/tests/interfaces/css-contain-3.idl10
-rw-r--r--test/wpt/tests/interfaces/css-contain.idl13
-rw-r--r--test/wpt/tests/interfaces/css-counter-styles.idl23
-rw-r--r--test/wpt/tests/interfaces/css-font-loading.idl134
-rw-r--r--test/wpt/tests/interfaces/css-fonts.idl36
-rw-r--r--test/wpt/tests/interfaces/css-highlight-api.idl27
-rw-r--r--test/wpt/tests/interfaces/css-images-4.idl8
-rw-r--r--test/wpt/tests/interfaces/css-layout-api.idl144
-rw-r--r--test/wpt/tests/interfaces/css-masking.idl20
-rw-r--r--test/wpt/tests/interfaces/css-nav.idl48
-rw-r--r--test/wpt/tests/interfaces/css-nesting.idl10
-rw-r--r--test/wpt/tests/interfaces/css-paint-api.idl39
-rw-r--r--test/wpt/tests/interfaces/css-parser-api.idl76
-rw-r--r--test/wpt/tests/interfaces/css-properties-values-api.idl23
-rw-r--r--test/wpt/tests/interfaces/css-pseudo.idl16
-rw-r--r--test/wpt/tests/interfaces/css-regions.idl29
-rw-r--r--test/wpt/tests/interfaces/css-shadow-parts.idl8
-rw-r--r--test/wpt/tests/interfaces/css-toggle.tentative.idl51
-rw-r--r--test/wpt/tests/interfaces/css-transitions-2.idl9
-rw-r--r--test/wpt/tests/interfaces/css-transitions.idl25
-rw-r--r--test/wpt/tests/interfaces/css-typed-om.idl423
-rw-r--r--test/wpt/tests/interfaces/css-view-transitions.idl18
-rw-r--r--test/wpt/tests/interfaces/cssom-view.idl200
-rw-r--r--test/wpt/tests/interfaces/cssom.idl169
-rw-r--r--test/wpt/tests/interfaces/custom-state-pseudo-class.idl14
-rw-r--r--test/wpt/tests/interfaces/datacue.idl12
-rw-r--r--test/wpt/tests/interfaces/deprecation-reporting.idl15
-rw-r--r--test/wpt/tests/interfaces/device-memory.idl14
-rw-r--r--test/wpt/tests/interfaces/device-posture.idl20
-rw-r--r--test/wpt/tests/interfaces/digital-goods.idl44
-rw-r--r--test/wpt/tests/interfaces/document-picture-in-picture.idl34
-rw-r--r--test/wpt/tests/interfaces/dom.idl646
-rw-r--r--test/wpt/tests/interfaces/edit-context.idl111
-rw-r--r--test/wpt/tests/interfaces/element-timing.idl22
-rw-r--r--test/wpt/tests/interfaces/encoding.idl59
-rw-r--r--test/wpt/tests/interfaces/encrypted-media.idl125
-rw-r--r--test/wpt/tests/interfaces/entries-api.idl71
-rw-r--r--test/wpt/tests/interfaces/event-timing.idl29
-rw-r--r--test/wpt/tests/interfaces/eyedropper-api.idl18
-rw-r--r--test/wpt/tests/interfaces/fenced-frame.idl57
-rw-r--r--test/wpt/tests/interfaces/fetch.idl117
-rw-r--r--test/wpt/tests/interfaces/fido.idl47
-rw-r--r--test/wpt/tests/interfaces/file-system-access.idl72
-rw-r--r--test/wpt/tests/interfaces/filter-effects.idl341
-rw-r--r--test/wpt/tests/interfaces/font-metrics-api.idl42
-rw-r--r--test/wpt/tests/interfaces/fs.idl97
-rw-r--r--test/wpt/tests/interfaces/fullscreen.idl35
-rw-r--r--test/wpt/tests/interfaces/gamepad-extensions.idl71
-rw-r--r--test/wpt/tests/interfaces/gamepad.idl49
-rw-r--r--test/wpt/tests/interfaces/generic-sensor.idl60
-rw-r--r--test/wpt/tests/interfaces/geolocation-sensor.idl47
-rw-r--r--test/wpt/tests/interfaces/geolocation.idl65
-rw-r--r--test/wpt/tests/interfaces/geometry.idl290
-rw-r--r--test/wpt/tests/interfaces/get-installed-related-apps.idl16
-rw-r--r--test/wpt/tests/interfaces/gpc-spec.idl10
-rw-r--r--test/wpt/tests/interfaces/gyroscope.idl24
-rw-r--r--test/wpt/tests/interfaces/hr-time.idl19
-rw-r--r--test/wpt/tests/interfaces/html-media-capture.idl8
-rw-r--r--test/wpt/tests/interfaces/html.idl2725
-rw-r--r--test/wpt/tests/interfaces/idle-detection.idl31
-rw-r--r--test/wpt/tests/interfaces/image-capture.idl160
-rw-r--r--test/wpt/tests/interfaces/image-resource.idl11
-rw-r--r--test/wpt/tests/interfaces/ink-enhancement.idl32
-rw-r--r--test/wpt/tests/interfaces/input-device-capabilities.idl24
-rw-r--r--test/wpt/tests/interfaces/input-events.idl14
-rw-r--r--test/wpt/tests/interfaces/intersection-observer.idl46
-rw-r--r--test/wpt/tests/interfaces/intervention-reporting.idl14
-rw-r--r--test/wpt/tests/interfaces/is-input-pending.idl16
-rw-r--r--test/wpt/tests/interfaces/js-self-profiling.idl44
-rw-r--r--test/wpt/tests/interfaces/keyboard-lock.idl14
-rw-r--r--test/wpt/tests/interfaces/keyboard-map.idl15
-rw-r--r--test/wpt/tests/interfaces/largest-contentful-paint.idl15
-rw-r--r--test/wpt/tests/interfaces/layout-instability.idl20
-rw-r--r--test/wpt/tests/interfaces/local-font-access.idl24
-rw-r--r--test/wpt/tests/interfaces/longtasks.idl19
-rw-r--r--test/wpt/tests/interfaces/magnetometer.idl46
-rw-r--r--test/wpt/tests/interfaces/manifest-incubations.idl24
-rw-r--r--test/wpt/tests/interfaces/mathml-core.idl9
-rw-r--r--test/wpt/tests/interfaces/media-capabilities.idl115
-rw-r--r--test/wpt/tests/interfaces/media-playback-quality.idl18
-rw-r--r--test/wpt/tests/interfaces/media-source.idl91
-rw-r--r--test/wpt/tests/interfaces/mediacapture-automation.idl36
-rw-r--r--test/wpt/tests/interfaces/mediacapture-fromelement.idl17
-rw-r--r--test/wpt/tests/interfaces/mediacapture-handle-actions.idl31
-rw-r--r--test/wpt/tests/interfaces/mediacapture-region.idl15
-rw-r--r--test/wpt/tests/interfaces/mediacapture-streams.idl248
-rw-r--r--test/wpt/tests/interfaces/mediacapture-transform.idl23
-rw-r--r--test/wpt/tests/interfaces/mediacapture-viewport.idl14
-rw-r--r--test/wpt/tests/interfaces/mediasession.idl84
-rw-r--r--test/wpt/tests/interfaces/mediastream-recording.idl62
-rw-r--r--test/wpt/tests/interfaces/model-element.idl9
-rw-r--r--test/wpt/tests/interfaces/mst-content-hint.idl18
-rw-r--r--test/wpt/tests/interfaces/navigation-timing.idl71
-rw-r--r--test/wpt/tests/interfaces/netinfo.idl43
-rw-r--r--test/wpt/tests/interfaces/notifications.idl101
-rw-r--r--test/wpt/tests/interfaces/orientation-event.idl78
-rw-r--r--test/wpt/tests/interfaces/orientation-sensor.idl35
-rw-r--r--test/wpt/tests/interfaces/page-lifecycle.idl19
-rw-r--r--test/wpt/tests/interfaces/paint-timing.idl7
-rw-r--r--test/wpt/tests/interfaces/parakeet.tentative.idl32
-rw-r--r--test/wpt/tests/interfaces/payment-handler.idl131
-rw-r--r--test/wpt/tests/interfaces/payment-request.idl112
-rw-r--r--test/wpt/tests/interfaces/performance-measure-memory.idl30
-rw-r--r--test/wpt/tests/interfaces/performance-timeline.idl49
-rw-r--r--test/wpt/tests/interfaces/periodic-background-sync.idl34
-rw-r--r--test/wpt/tests/interfaces/permissions-policy.idl29
-rw-r--r--test/wpt/tests/interfaces/permissions-request.idl8
-rw-r--r--test/wpt/tests/interfaces/permissions-revoke.idl8
-rw-r--r--test/wpt/tests/interfaces/permissions.idl41
-rw-r--r--test/wpt/tests/interfaces/picture-in-picture.idl41
-rw-r--r--test/wpt/tests/interfaces/pointerevents.idl64
-rw-r--r--test/wpt/tests/interfaces/pointerlock.idl28
-rw-r--r--test/wpt/tests/interfaces/portals.idl50
-rw-r--r--test/wpt/tests/interfaces/prefer-current-tab.idl8
-rw-r--r--test/wpt/tests/interfaces/prerendering-revamped.idl15
-rw-r--r--test/wpt/tests/interfaces/presentation-api.idl95
-rw-r--r--test/wpt/tests/interfaces/private-click-measurement.idl8
-rw-r--r--test/wpt/tests/interfaces/proximity.idl18
-rw-r--r--test/wpt/tests/interfaces/push-api.idl93
-rw-r--r--test/wpt/tests/interfaces/raw-camera-access.idl18
-rw-r--r--test/wpt/tests/interfaces/real-world-meshing.idl21
-rw-r--r--test/wpt/tests/interfaces/referrer-policy.idl16
-rw-r--r--test/wpt/tests/interfaces/remote-playback.idl32
-rw-r--r--test/wpt/tests/interfaces/reporting.idl39
-rw-r--r--test/wpt/tests/interfaces/requestStorageAccessFor.idl12
-rw-r--r--test/wpt/tests/interfaces/requestidlecallback.idl20
-rw-r--r--test/wpt/tests/interfaces/resize-observer.idl37
-rw-r--r--test/wpt/tests/interfaces/resource-timing.idl42
-rw-r--r--test/wpt/tests/interfaces/sanitizer-api.idl38
-rw-r--r--test/wpt/tests/interfaces/sanitizer-api.tentative.idl17
-rw-r--r--test/wpt/tests/interfaces/savedata.idl10
-rw-r--r--test/wpt/tests/interfaces/scheduling-apis.idl63
-rw-r--r--test/wpt/tests/interfaces/screen-capture.idl85
-rw-r--r--test/wpt/tests/interfaces/screen-orientation.idl35
-rw-r--r--test/wpt/tests/interfaces/screen-wake-lock.idl24
-rw-r--r--test/wpt/tests/interfaces/scroll-animations.idl46
-rw-r--r--test/wpt/tests/interfaces/scroll-to-text-fragment.idl12
-rw-r--r--test/wpt/tests/interfaces/secure-payment-confirmation.idl52
-rw-r--r--test/wpt/tests/interfaces/selection-api.idl46
-rw-r--r--test/wpt/tests/interfaces/serial.idl85
-rw-r--r--test/wpt/tests/interfaces/server-timing.idl17
-rw-r--r--test/wpt/tests/interfaces/service-workers.idl240
-rw-r--r--test/wpt/tests/interfaces/shape-detection-api.idl69
-rw-r--r--test/wpt/tests/interfaces/shared-storage.idl80
-rw-r--r--test/wpt/tests/interfaces/speech-api.idl202
-rw-r--r--test/wpt/tests/interfaces/storage-access.idl9
-rw-r--r--test/wpt/tests/interfaces/storage-buckets.idl53
-rw-r--r--test/wpt/tests/interfaces/storage-buckets.tentative.idl36
-rw-r--r--test/wpt/tests/interfaces/storage.idl25
-rw-r--r--test/wpt/tests/interfaces/streams.idl222
-rw-r--r--test/wpt/tests/interfaces/sub-apps.tentative.idl17
-rw-r--r--test/wpt/tests/interfaces/svg-animations.idl68
-rw-r--r--test/wpt/tests/interfaces/testutils.idl9
-rw-r--r--test/wpt/tests/interfaces/text-detection-api.idl18
-rw-r--r--test/wpt/tests/interfaces/touch-events.idl79
-rw-r--r--test/wpt/tests/interfaces/trust-token-api.idl26
-rw-r--r--test/wpt/tests/interfaces/trusted-types.idl71
-rw-r--r--test/wpt/tests/interfaces/turtledove.idl120
-rw-r--r--test/wpt/tests/interfaces/ua-client-hints.idl45
-rw-r--r--test/wpt/tests/interfaces/uievents.idl248
-rw-r--r--test/wpt/tests/interfaces/url.idl46
-rw-r--r--test/wpt/tests/interfaces/urlpattern.idl59
-rw-r--r--test/wpt/tests/interfaces/user-timing.idl34
-rw-r--r--test/wpt/tests/interfaces/vibration.idl10
-rw-r--r--test/wpt/tests/interfaces/video-rvfc.idl27
-rw-r--r--test/wpt/tests/interfaces/virtual-keyboard.idl21
-rw-r--r--test/wpt/tests/interfaces/virtual-keyboard.tentative.idl15
-rw-r--r--test/wpt/tests/interfaces/wai-aria.idl59
-rw-r--r--test/wpt/tests/interfaces/wasm-js-api.idl110
-rw-r--r--test/wpt/tests/interfaces/wasm-web-api.idl11
-rw-r--r--test/wpt/tests/interfaces/web-animations-2.idl112
-rw-r--r--test/wpt/tests/interfaces/web-animations.idl149
-rw-r--r--test/wpt/tests/interfaces/web-app-launch.idl19
-rw-r--r--test/wpt/tests/interfaces/web-bluetooth.idl252
-rw-r--r--test/wpt/tests/interfaces/web-locks.idl50
-rw-r--r--test/wpt/tests/interfaces/web-nfc.idl81
-rw-r--r--test/wpt/tests/interfaces/web-otp.idl21
-rw-r--r--test/wpt/tests/interfaces/web-share.idl16
-rw-r--r--test/wpt/tests/interfaces/webaudio.idl674
-rw-r--r--test/wpt/tests/interfaces/webauthn.idl350
-rw-r--r--test/wpt/tests/interfaces/webcodecs-aac-codec-registration.idl17
-rw-r--r--test/wpt/tests/interfaces/webcodecs-av1-codec-registration.idl20
-rw-r--r--test/wpt/tests/interfaces/webcodecs-avc-codec-registration.idl25
-rw-r--r--test/wpt/tests/interfaces/webcodecs-flac-codec-registration.idl13
-rw-r--r--test/wpt/tests/interfaces/webcodecs-hevc-codec-registration.idl17
-rw-r--r--test/wpt/tests/interfaces/webcodecs-opus-codec-registration.idl22
-rw-r--r--test/wpt/tests/interfaces/webcodecs-vp9-codec-registration.idl12
-rw-r--r--test/wpt/tests/interfaces/webcodecs.idl501
-rw-r--r--test/wpt/tests/interfaces/webcrypto-secure-curves.idl8
-rw-r--r--test/wpt/tests/interfaces/webdriver.idl9
-rw-r--r--test/wpt/tests/interfaces/webgl1.idl745
-rw-r--r--test/wpt/tests/interfaces/webgl2.idl582
-rw-r--r--test/wpt/tests/interfaces/webgpu.idl1293
-rw-r--r--test/wpt/tests/interfaces/webhid.idl127
-rw-r--r--test/wpt/tests/interfaces/webidl.idl48
-rw-r--r--test/wpt/tests/interfaces/webmidi.idl91
-rw-r--r--test/wpt/tests/interfaces/webnn.idl544
-rw-r--r--test/wpt/tests/interfaces/webrtc-encoded-transform.idl128
-rw-r--r--test/wpt/tests/interfaces/webrtc-ice.idl24
-rw-r--r--test/wpt/tests/interfaces/webrtc-identity.idl97
-rw-r--r--test/wpt/tests/interfaces/webrtc-priority.idl24
-rw-r--r--test/wpt/tests/interfaces/webrtc-stats.idl288
-rw-r--r--test/wpt/tests/interfaces/webrtc-svc.idl8
-rw-r--r--test/wpt/tests/interfaces/webrtc.idl627
-rw-r--r--test/wpt/tests/interfaces/websockets.idl48
-rw-r--r--test/wpt/tests/interfaces/webtransport.idl145
-rw-r--r--test/wpt/tests/interfaces/webusb.idl249
-rw-r--r--test/wpt/tests/interfaces/webvr.tentative.idl204
-rw-r--r--test/wpt/tests/interfaces/webvtt.idl40
-rw-r--r--test/wpt/tests/interfaces/webxr-ar-module.idl29
-rw-r--r--test/wpt/tests/interfaces/webxr-depth-sensing.idl57
-rw-r--r--test/wpt/tests/interfaces/webxr-dom-overlays.idl31
-rw-r--r--test/wpt/tests/interfaces/webxr-gamepads-module.idl8
-rw-r--r--test/wpt/tests/interfaces/webxr-hand-input.idl66
-rw-r--r--test/wpt/tests/interfaces/webxr-hit-test.idl69
-rw-r--r--test/wpt/tests/interfaces/webxr-lighting-estimation.idl39
-rw-r--r--test/wpt/tests/interfaces/webxr.idl295
-rw-r--r--test/wpt/tests/interfaces/webxrlayers.idl221
-rw-r--r--test/wpt/tests/interfaces/window-controls-overlay.idl28
-rw-r--r--test/wpt/tests/interfaces/window-management.idl42
-rw-r--r--test/wpt/tests/interfaces/xhr.idl99
-rw-r--r--test/wpt/tests/lint.ignore707
-rw-r--r--test/wpt/tests/mimesniff/META.yml3
-rw-r--r--test/wpt/tests/mimesniff/README.md4
-rw-r--r--test/wpt/tests/mimesniff/media/media-sniff.window.js32
-rw-r--r--test/wpt/tests/mimesniff/media/resources/flac.flacbin0 -> 8493 bytes
-rw-r--r--test/wpt/tests/mimesniff/media/resources/make-vectors.sh10
-rw-r--r--test/wpt/tests/mimesniff/media/resources/mp3-raw.mp3bin0 -> 417 bytes
-rw-r--r--test/wpt/tests/mimesniff/media/resources/mp3-with-id3.mp3bin0 -> 644 bytes
-rw-r--r--test/wpt/tests/mimesniff/media/resources/mp4.mp4bin0 -> 1231 bytes
-rw-r--r--test/wpt/tests/mimesniff/media/resources/ogg.oggbin0 -> 3594 bytes
-rw-r--r--test/wpt/tests/mimesniff/media/resources/wav.wavbin0 -> 486 bytes
-rw-r--r--test/wpt/tests/mimesniff/media/resources/webm.webmbin0 -> 877 bytes
-rw-r--r--test/wpt/tests/mimesniff/mime-types/README.md47
-rw-r--r--test/wpt/tests/mimesniff/mime-types/charset-parameter.window.js61
-rw-r--r--test/wpt/tests/mimesniff/mime-types/parsing.any.js57
-rw-r--r--test/wpt/tests/mimesniff/mime-types/resources/generated-mime-types.json3526
-rw-r--r--test/wpt/tests/mimesniff/mime-types/resources/generated-mime-types.py48
-rw-r--r--test/wpt/tests/mimesniff/mime-types/resources/mime-charset.py19
-rw-r--r--test/wpt/tests/mimesniff/mime-types/resources/mime-groups.json159
-rw-r--r--test/wpt/tests/mimesniff/mime-types/resources/mime-types.json397
-rw-r--r--test/wpt/tests/resources/.htaccess2
-rw-r--r--test/wpt/tests/resources/META.yml2
-rw-r--r--test/wpt/tests/resources/SVGAnimationTestCase-testharness.js102
-rw-r--r--test/wpt/tests/resources/accesskey.js34
-rw-r--r--test/wpt/tests/resources/blank.html16
-rw-r--r--test/wpt/tests/resources/channel.sub.js1097
-rw-r--r--test/wpt/tests/resources/check-layout-th.js252
-rw-r--r--test/wpt/tests/resources/check-layout.js245
-rw-r--r--test/wpt/tests/resources/chromium/README.md7
-rw-r--r--test/wpt/tests/resources/chromium/contacts_manager_mock.js90
-rw-r--r--test/wpt/tests/resources/chromium/content-index-helpers.js9
-rw-r--r--test/wpt/tests/resources/chromium/enable-hyperlink-auditing.js2
-rw-r--r--test/wpt/tests/resources/chromium/fake-hid.js297
-rw-r--r--test/wpt/tests/resources/chromium/fake-serial.js443
-rw-r--r--test/wpt/tests/resources/chromium/generic_sensor_mocks.js519
-rw-r--r--test/wpt/tests/resources/chromium/generic_sensor_mocks.js.headers1
-rw-r--r--test/wpt/tests/resources/chromium/mock-barcodedetection.js136
-rw-r--r--test/wpt/tests/resources/chromium/mock-barcodedetection.js.headers1
-rw-r--r--test/wpt/tests/resources/chromium/mock-battery-monitor.headers1
-rw-r--r--test/wpt/tests/resources/chromium/mock-battery-monitor.js61
-rw-r--r--test/wpt/tests/resources/chromium/mock-direct-sockets.js94
-rw-r--r--test/wpt/tests/resources/chromium/mock-facedetection.js130
-rw-r--r--test/wpt/tests/resources/chromium/mock-facedetection.js.headers1
-rw-r--r--test/wpt/tests/resources/chromium/mock-idle-detection.js80
-rw-r--r--test/wpt/tests/resources/chromium/mock-imagecapture.js309
-rw-r--r--test/wpt/tests/resources/chromium/mock-managed-config.js91
-rw-r--r--test/wpt/tests/resources/chromium/mock-pressure-service.js134
-rw-r--r--test/wpt/tests/resources/chromium/mock-pressure-service.js.headers1
-rw-r--r--test/wpt/tests/resources/chromium/mock-subapps.js89
-rw-r--r--test/wpt/tests/resources/chromium/mock-textdetection.js92
-rw-r--r--test/wpt/tests/resources/chromium/mock-textdetection.js.headers1
-rw-r--r--test/wpt/tests/resources/chromium/nfc-mock.js437
-rw-r--r--test/wpt/tests/resources/chromium/web-bluetooth-test.js629
-rw-r--r--test/wpt/tests/resources/chromium/web-bluetooth-test.js.headers1
-rw-r--r--test/wpt/tests/resources/chromium/webusb-child-test.js47
-rw-r--r--test/wpt/tests/resources/chromium/webusb-child-test.js.headers1
-rw-r--r--test/wpt/tests/resources/chromium/webusb-test.js583
-rw-r--r--test/wpt/tests/resources/chromium/webusb-test.js.headers1
-rw-r--r--test/wpt/tests/resources/chromium/webxr-test-math-helper.js298
-rw-r--r--test/wpt/tests/resources/chromium/webxr-test-math-helper.js.headers1
-rw-r--r--test/wpt/tests/resources/chromium/webxr-test.js2125
-rw-r--r--test/wpt/tests/resources/chromium/webxr-test.js.headers1
-rw-r--r--test/wpt/tests/resources/declarative-shadow-dom-polyfill.js25
-rw-r--r--test/wpt/tests/resources/idlharness-shadowrealm.js61
-rw-r--r--test/wpt/tests/resources/idlharness.js3554
-rw-r--r--test/wpt/tests/resources/idlharness.js.headers2
-rw-r--r--test/wpt/tests/resources/readme.md14
-rw-r--r--test/wpt/tests/resources/sriharness.js226
-rw-r--r--test/wpt/tests/resources/test-only-api.js31
-rw-r--r--test/wpt/tests/resources/test-only-api.js.headers2
-rw-r--r--test/wpt/tests/resources/test-only-api.m.js5
-rw-r--r--test/wpt/tests/resources/test-only-api.m.js.headers2
-rw-r--r--test/wpt/tests/resources/test/README.md83
-rw-r--r--test/wpt/tests/resources/test/conftest.py269
-rw-r--r--test/wpt/tests/resources/test/harness.html26
-rw-r--r--test/wpt/tests/resources/test/idl-helper.js24
-rw-r--r--test/wpt/tests/resources/test/nested-testharness.js80
-rw-r--r--test/wpt/tests/resources/test/requirements.txt1
-rw-r--r--test/wpt/tests/resources/test/tests/functional/abortsignal.html49
-rw-r--r--test/wpt/tests/resources/test/tests/functional/add_cleanup.html91
-rw-r--r--test/wpt/tests/resources/test/tests/functional/add_cleanup_async.html85
-rw-r--r--test/wpt/tests/resources/test/tests/functional/add_cleanup_async_bad_return.html50
-rw-r--r--test/wpt/tests/resources/test/tests/functional/add_cleanup_async_rejection.html94
-rw-r--r--test/wpt/tests/resources/test/tests/functional/add_cleanup_async_rejection_after_load.html52
-rw-r--r--test/wpt/tests/resources/test/tests/functional/add_cleanup_async_timeout.html57
-rw-r--r--test/wpt/tests/resources/test/tests/functional/add_cleanup_bad_return.html61
-rw-r--r--test/wpt/tests/resources/test/tests/functional/add_cleanup_count.html39
-rw-r--r--test/wpt/tests/resources/test/tests/functional/add_cleanup_err.html45
-rw-r--r--test/wpt/tests/resources/test/tests/functional/add_cleanup_err_multi.html52
-rw-r--r--test/wpt/tests/resources/test/tests/functional/add_cleanup_sync_queue.html55
-rw-r--r--test/wpt/tests/resources/test/tests/functional/api-tests-1.html991
-rw-r--r--test/wpt/tests/resources/test/tests/functional/api-tests-2.html62
-rw-r--r--test/wpt/tests/resources/test/tests/functional/api-tests-3.html34
-rw-r--r--test/wpt/tests/resources/test/tests/functional/assert-array-equals.html162
-rw-r--r--test/wpt/tests/resources/test/tests/functional/assert-throws-dom.html55
-rw-r--r--test/wpt/tests/resources/test/tests/functional/force_timeout.html60
-rw-r--r--test/wpt/tests/resources/test/tests/functional/generate-callback.html153
-rw-r--r--test/wpt/tests/resources/test/tests/functional/idlharness/IdlDictionary/test_partial_interface_of.html89
-rw-r--r--test/wpt/tests/resources/test/tests/functional/idlharness/IdlInterface/test_exposed_wildcard.html233
-rw-r--r--test/wpt/tests/resources/test/tests/functional/idlharness/IdlInterface/test_immutable_prototype.html298
-rw-r--r--test/wpt/tests/resources/test/tests/functional/idlharness/IdlInterface/test_interface_mixin.html131
-rw-r--r--test/wpt/tests/resources/test/tests/functional/idlharness/IdlInterface/test_partial_interface_of.html187
-rw-r--r--test/wpt/tests/resources/test/tests/functional/idlharness/IdlInterface/test_primary_interface_of.html116
-rw-r--r--test/wpt/tests/resources/test/tests/functional/idlharness/IdlInterface/test_to_json_operation.html177
-rw-r--r--test/wpt/tests/resources/test/tests/functional/idlharness/IdlNamespace/test_attribute.html100
-rw-r--r--test/wpt/tests/resources/test/tests/functional/idlharness/IdlNamespace/test_operation.html242
-rw-r--r--test/wpt/tests/resources/test/tests/functional/idlharness/IdlNamespace/test_partial_namespace.html125
-rw-r--r--test/wpt/tests/resources/test/tests/functional/iframe-callback.html116
-rw-r--r--test/wpt/tests/resources/test/tests/functional/iframe-consolidate-errors.html50
-rw-r--r--test/wpt/tests/resources/test/tests/functional/iframe-consolidate-tests.html85
-rw-r--r--test/wpt/tests/resources/test/tests/functional/iframe-msg.html84
-rw-r--r--test/wpt/tests/resources/test/tests/functional/log-insertion.html46
-rw-r--r--test/wpt/tests/resources/test/tests/functional/no-title.html146
-rw-r--r--test/wpt/tests/resources/test/tests/functional/order.html36
-rw-r--r--test/wpt/tests/resources/test/tests/functional/promise-async.html172
-rw-r--r--test/wpt/tests/resources/test/tests/functional/promise-with-sync.html79
-rw-r--r--test/wpt/tests/resources/test/tests/functional/promise.html219
-rw-r--r--test/wpt/tests/resources/test/tests/functional/queue.html130
-rw-r--r--test/wpt/tests/resources/test/tests/functional/setup-function-worker.js14
-rw-r--r--test/wpt/tests/resources/test/tests/functional/setup-worker-service.html86
-rw-r--r--test/wpt/tests/resources/test/tests/functional/single-page-test-fail.html28
-rw-r--r--test/wpt/tests/resources/test/tests/functional/single-page-test-no-assertions.html25
-rw-r--r--test/wpt/tests/resources/test/tests/functional/single-page-test-no-body.html26
-rw-r--r--test/wpt/tests/resources/test/tests/functional/single-page-test-pass.html28
-rw-r--r--test/wpt/tests/resources/test/tests/functional/step_wait.html57
-rw-r--r--test/wpt/tests/resources/test/tests/functional/step_wait_func.html49
-rw-r--r--test/wpt/tests/resources/test/tests/functional/task-scheduling-promise-test.html241
-rw-r--r--test/wpt/tests/resources/test/tests/functional/task-scheduling-test.html141
-rw-r--r--test/wpt/tests/resources/test/tests/functional/uncaught-exception-handle.html33
-rw-r--r--test/wpt/tests/resources/test/tests/functional/uncaught-exception-ignore.html35
-rw-r--r--test/wpt/tests/resources/test/tests/functional/worker-dedicated-uncaught-allow.html45
-rw-r--r--test/wpt/tests/resources/test/tests/functional/worker-dedicated-uncaught-single.html56
-rw-r--r--test/wpt/tests/resources/test/tests/functional/worker-dedicated.sub.html88
-rw-r--r--test/wpt/tests/resources/test/tests/functional/worker-error.js8
-rw-r--r--test/wpt/tests/resources/test/tests/functional/worker-service.html115
-rw-r--r--test/wpt/tests/resources/test/tests/functional/worker-shared.html73
-rw-r--r--test/wpt/tests/resources/test/tests/functional/worker-uncaught-allow.js19
-rw-r--r--test/wpt/tests/resources/test/tests/functional/worker-uncaught-single.js8
-rw-r--r--test/wpt/tests/resources/test/tests/functional/worker.js34
-rw-r--r--test/wpt/tests/resources/test/tests/unit/IdlArray/is_json_type.html192
-rw-r--r--test/wpt/tests/resources/test/tests/unit/IdlDictionary/get_reverse_inheritance_stack.html47
-rw-r--r--test/wpt/tests/resources/test/tests/unit/IdlDictionary/test_partial_dictionary.html39
-rw-r--r--test/wpt/tests/resources/test/tests/unit/IdlInterface/constructors.html26
-rw-r--r--test/wpt/tests/resources/test/tests/unit/IdlInterface/default_to_json_operation.html114
-rw-r--r--test/wpt/tests/resources/test/tests/unit/IdlInterface/do_member_unscopable_asserts.html56
-rw-r--r--test/wpt/tests/resources/test/tests/unit/IdlInterface/get_interface_object.html22
-rw-r--r--test/wpt/tests/resources/test/tests/unit/IdlInterface/get_interface_object_owner.html21
-rw-r--r--test/wpt/tests/resources/test/tests/unit/IdlInterface/get_legacy_namespace.html20
-rw-r--r--test/wpt/tests/resources/test/tests/unit/IdlInterface/get_qualified_name.html20
-rw-r--r--test/wpt/tests/resources/test/tests/unit/IdlInterface/get_reverse_inheritance_stack.html49
-rw-r--r--test/wpt/tests/resources/test/tests/unit/IdlInterface/has_default_to_json_regular_operation.html47
-rw-r--r--test/wpt/tests/resources/test/tests/unit/IdlInterface/has_to_json_regular_operation.html31
-rw-r--r--test/wpt/tests/resources/test/tests/unit/IdlInterface/should_have_interface_object.html30
-rw-r--r--test/wpt/tests/resources/test/tests/unit/IdlInterface/test_primary_interface_of_undefined.html29
-rw-r--r--test/wpt/tests/resources/test/tests/unit/IdlInterfaceMember/is_to_json_regular_operation.html42
-rw-r--r--test/wpt/tests/resources/test/tests/unit/IdlInterfaceMember/toString.html36
-rw-r--r--test/wpt/tests/resources/test/tests/unit/assert_implements.html43
-rw-r--r--test/wpt/tests/resources/test/tests/unit/assert_implements_optional.html43
-rw-r--r--test/wpt/tests/resources/test/tests/unit/assert_object_equals.html152
-rw-r--r--test/wpt/tests/resources/test/tests/unit/async-test-return-restrictions.html135
-rw-r--r--test/wpt/tests/resources/test/tests/unit/basic.html48
-rw-r--r--test/wpt/tests/resources/test/tests/unit/exceptional-cases-timeouts.html120
-rw-r--r--test/wpt/tests/resources/test/tests/unit/exceptional-cases.html392
-rw-r--r--test/wpt/tests/resources/test/tests/unit/format-value.html123
-rw-r--r--test/wpt/tests/resources/test/tests/unit/helpers.js21
-rw-r--r--test/wpt/tests/resources/test/tests/unit/late-test.html54
-rw-r--r--test/wpt/tests/resources/test/tests/unit/promise_setup-timeout.html28
-rw-r--r--test/wpt/tests/resources/test/tests/unit/promise_setup.html333
-rw-r--r--test/wpt/tests/resources/test/tests/unit/single_test.html94
-rw-r--r--test/wpt/tests/resources/test/tests/unit/test-return-restrictions.html156
-rw-r--r--test/wpt/tests/resources/test/tests/unit/throwing-assertions.html268
-rw-r--r--test/wpt/tests/resources/test/tests/unit/unpaired-surrogates.html143
-rw-r--r--test/wpt/tests/resources/test/tox.ini13
-rw-r--r--test/wpt/tests/resources/test/wptserver.py58
-rw-r--r--test/wpt/tests/resources/testdriver-actions.js599
-rw-r--r--test/wpt/tests/resources/testdriver-vendor.js0
-rw-r--r--test/wpt/tests/resources/testdriver-vendor.js.headers2
-rw-r--r--test/wpt/tests/resources/testdriver.js958
-rw-r--r--test/wpt/tests/resources/testdriver.js.headers2
-rw-r--r--test/wpt/tests/resources/testharness.js4933
-rw-r--r--test/wpt/tests/resources/testharness.js.headers2
-rw-r--r--test/wpt/tests/resources/testharnessreport.js57
-rw-r--r--test/wpt/tests/resources/testharnessreport.js.headers2
-rw-r--r--test/wpt/tests/resources/webidl2/build.sh12
-rw-r--r--test/wpt/tests/resources/webidl2/lib/README.md4
-rw-r--r--test/wpt/tests/resources/webidl2/lib/VERSION.md1
-rw-r--r--test/wpt/tests/resources/webidl2/lib/webidl2.js3824
-rw-r--r--test/wpt/tests/resources/webidl2/lib/webidl2.js.headers1
-rw-r--r--test/wpt/tests/service-workers/META.yml6
-rw-r--r--test/wpt/tests/service-workers/cache-storage/META.yml3
-rw-r--r--test/wpt/tests/service-workers/cache-storage/cache-abort.https.any.js81
-rw-r--r--test/wpt/tests/service-workers/cache-storage/cache-add.https.any.js368
-rw-r--r--test/wpt/tests/service-workers/cache-storage/cache-delete.https.any.js164
-rw-r--r--test/wpt/tests/service-workers/cache-storage/cache-keys-attributes-for-service-worker.https.html75
-rw-r--r--test/wpt/tests/service-workers/cache-storage/cache-keys.https.any.js212
-rw-r--r--test/wpt/tests/service-workers/cache-storage/cache-match.https.any.js437
-rw-r--r--test/wpt/tests/service-workers/cache-storage/cache-matchAll.https.any.js244
-rw-r--r--test/wpt/tests/service-workers/cache-storage/cache-put.https.any.js411
-rw-r--r--test/wpt/tests/service-workers/cache-storage/cache-storage-buckets.https.any.js64
-rw-r--r--test/wpt/tests/service-workers/cache-storage/cache-storage-keys.https.any.js35
-rw-r--r--test/wpt/tests/service-workers/cache-storage/cache-storage-match.https.any.js245
-rw-r--r--test/wpt/tests/service-workers/cache-storage/cache-storage.https.any.js239
-rw-r--r--test/wpt/tests/service-workers/cache-storage/common.https.window.js44
-rw-r--r--test/wpt/tests/service-workers/cache-storage/crashtests/cache-response-clone.https.html17
-rw-r--r--test/wpt/tests/service-workers/cache-storage/credentials.https.html46
-rw-r--r--test/wpt/tests/service-workers/cache-storage/cross-partition.https.tentative.html269
-rw-r--r--test/wpt/tests/service-workers/cache-storage/resources/blank.html2
-rw-r--r--test/wpt/tests/service-workers/cache-storage/resources/cache-keys-attributes-for-service-worker.js22
-rw-r--r--test/wpt/tests/service-workers/cache-storage/resources/common-worker.js15
-rw-r--r--test/wpt/tests/service-workers/cache-storage/resources/credentials-iframe.html38
-rw-r--r--test/wpt/tests/service-workers/cache-storage/resources/credentials-worker.js59
-rw-r--r--test/wpt/tests/service-workers/cache-storage/resources/fetch-status.py2
-rw-r--r--test/wpt/tests/service-workers/cache-storage/resources/iframe.html18
-rw-r--r--test/wpt/tests/service-workers/cache-storage/resources/simple.txt1
-rw-r--r--test/wpt/tests/service-workers/cache-storage/resources/test-helpers.js272
-rw-r--r--test/wpt/tests/service-workers/cache-storage/resources/vary.py25
-rw-r--r--test/wpt/tests/service-workers/cache-storage/sandboxed-iframes.https.html66
-rw-r--r--test/wpt/tests/service-workers/idlharness.https.any.js53
-rw-r--r--test/wpt/tests/service-workers/service-worker/Service-Worker-Allowed-header.https.html88
-rw-r--r--test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/close.https.html11
-rw-r--r--test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/extendable-message-event-constructor.https.html10
-rw-r--r--test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/extendable-message-event.https.html226
-rw-r--r--test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/fetch-on-the-right-interface.https.any.js14
-rw-r--r--test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/isSecureContext.https.html32
-rw-r--r--test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/isSecureContext.serviceworker.js5
-rw-r--r--test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/postmessage.https.html83
-rw-r--r--test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/registration-attribute.https.html107
-rw-r--r--test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/close-worker.js5
-rw-r--r--test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/error-worker.js12
-rw-r--r--test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-constructor-worker.js197
-rw-r--r--test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-loopback-worker.js36
-rw-r--r--test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-ping-worker.js23
-rw-r--r--test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-pong-worker.js18
-rw-r--r--test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-utils.js78
-rw-r--r--test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-worker.js5
-rw-r--r--test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/postmessage-loopback-worker.js15
-rw-r--r--test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/postmessage-ping-worker.js15
-rw-r--r--test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/postmessage-pong-worker.js4
-rw-r--r--test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/registration-attribute-newer-worker.js33
-rw-r--r--test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/registration-attribute-worker.js139
-rw-r--r--test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/unregister-controlling-worker.html0
-rw-r--r--test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/unregister-worker.js25
-rw-r--r--test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/update-worker.js22
-rw-r--r--test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/update-worker.py16
-rw-r--r--test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/service-worker-error-event.https.html31
-rw-r--r--test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/unregister.https.html139
-rw-r--r--test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/update.https.html48
-rw-r--r--test/wpt/tests/service-workers/service-worker/about-blank-replacement.https.html181
-rw-r--r--test/wpt/tests/service-workers/service-worker/activate-event-after-install-state-change.https.html33
-rw-r--r--test/wpt/tests/service-workers/service-worker/activation-after-registration.https.html28
-rw-r--r--test/wpt/tests/service-workers/service-worker/activation.https.html168
-rw-r--r--test/wpt/tests/service-workers/service-worker/active.https.html50
-rw-r--r--test/wpt/tests/service-workers/service-worker/claim-affect-other-registration.https.html136
-rw-r--r--test/wpt/tests/service-workers/service-worker/claim-fetch.https.html90
-rw-r--r--test/wpt/tests/service-workers/service-worker/claim-not-using-registration.https.html131
-rw-r--r--test/wpt/tests/service-workers/service-worker/claim-shared-worker-fetch.https.html71
-rw-r--r--test/wpt/tests/service-workers/service-worker/claim-using-registration.https.html103
-rw-r--r--test/wpt/tests/service-workers/service-worker/claim-with-redirect.https.html59
-rw-r--r--test/wpt/tests/service-workers/service-worker/claim-worker-fetch.https.html83
-rw-r--r--test/wpt/tests/service-workers/service-worker/client-id.https.html60
-rw-r--r--test/wpt/tests/service-workers/service-worker/client-navigate.https.html107
-rw-r--r--test/wpt/tests/service-workers/service-worker/client-url-of-blob-url-worker.https.html29
-rw-r--r--test/wpt/tests/service-workers/service-worker/clients-get-client-types.https.html108
-rw-r--r--test/wpt/tests/service-workers/service-worker/clients-get-cross-origin.https.html69
-rw-r--r--test/wpt/tests/service-workers/service-worker/clients-get-resultingClientId.https.html177
-rw-r--r--test/wpt/tests/service-workers/service-worker/clients-get.https.html154
-rw-r--r--test/wpt/tests/service-workers/service-worker/clients-matchall-blob-url-worker.https.html85
-rw-r--r--test/wpt/tests/service-workers/service-worker/clients-matchall-client-types.https.html92
-rw-r--r--test/wpt/tests/service-workers/service-worker/clients-matchall-exact-controller.https.html67
-rw-r--r--test/wpt/tests/service-workers/service-worker/clients-matchall-frozen.https.html64
-rw-r--r--test/wpt/tests/service-workers/service-worker/clients-matchall-include-uncontrolled.https.html117
-rw-r--r--test/wpt/tests/service-workers/service-worker/clients-matchall-on-evaluation.https.html24
-rw-r--r--test/wpt/tests/service-workers/service-worker/clients-matchall-order.https.html427
-rw-r--r--test/wpt/tests/service-workers/service-worker/clients-matchall.https.html50
-rw-r--r--test/wpt/tests/service-workers/service-worker/controlled-dedicatedworker-postMessage.https.html44
-rw-r--r--test/wpt/tests/service-workers/service-worker/controlled-iframe-postMessage.https.html67
-rw-r--r--test/wpt/tests/service-workers/service-worker/controller-on-disconnect.https.html40
-rw-r--r--test/wpt/tests/service-workers/service-worker/controller-on-load.https.html46
-rw-r--r--test/wpt/tests/service-workers/service-worker/controller-on-reload.https.html58
-rw-r--r--test/wpt/tests/service-workers/service-worker/controller-with-no-fetch-event-handler.https.html56
-rw-r--r--test/wpt/tests/service-workers/service-worker/credentials.https.html100
-rw-r--r--test/wpt/tests/service-workers/service-worker/data-iframe.html25
-rw-r--r--test/wpt/tests/service-workers/service-worker/data-transfer-files.https.html41
-rw-r--r--test/wpt/tests/service-workers/service-worker/dedicated-worker-service-worker-interception.https.html40
-rw-r--r--test/wpt/tests/service-workers/service-worker/detached-context.https.html124
-rw-r--r--test/wpt/tests/service-workers/service-worker/embed-and-object-are-not-intercepted.https.html104
-rw-r--r--test/wpt/tests/service-workers/service-worker/extendable-event-async-waituntil.https.html120
-rw-r--r--test/wpt/tests/service-workers/service-worker/extendable-event-waituntil.https.html140
-rw-r--r--test/wpt/tests/service-workers/service-worker/fetch-audio-tainting.https.html47
-rw-r--r--test/wpt/tests/service-workers/service-worker/fetch-canvas-tainting-double-write.https.html57
-rw-r--r--test/wpt/tests/service-workers/service-worker/fetch-canvas-tainting-image-cache.https.html16
-rw-r--r--test/wpt/tests/service-workers/service-worker/fetch-canvas-tainting-image.https.html16
-rw-r--r--test/wpt/tests/service-workers/service-worker/fetch-canvas-tainting-video-cache.https.html17
-rw-r--r--test/wpt/tests/service-workers/service-worker/fetch-canvas-tainting-video-with-range-request.https.html92
-rw-r--r--test/wpt/tests/service-workers/service-worker/fetch-canvas-tainting-video.https.html17
-rw-r--r--test/wpt/tests/service-workers/service-worker/fetch-cors-exposed-header-names.https.html30
-rw-r--r--test/wpt/tests/service-workers/service-worker/fetch-cors-xhr.https.html49
-rw-r--r--test/wpt/tests/service-workers/service-worker/fetch-csp.https.html138
-rw-r--r--test/wpt/tests/service-workers/service-worker/fetch-error.https.html29
-rw-r--r--test/wpt/tests/service-workers/service-worker/fetch-event-add-async.https.html11
-rw-r--r--test/wpt/tests/service-workers/service-worker/fetch-event-after-navigation-within-page.https.html71
-rw-r--r--test/wpt/tests/service-workers/service-worker/fetch-event-async-respond-with.https.html73
-rw-r--r--test/wpt/tests/service-workers/service-worker/fetch-event-handled.https.html86
-rw-r--r--test/wpt/tests/service-workers/service-worker/fetch-event-is-history-backward-navigation-manual.https.html8
-rw-r--r--test/wpt/tests/service-workers/service-worker/fetch-event-is-history-forward-navigation-manual.https.html8
-rw-r--r--test/wpt/tests/service-workers/service-worker/fetch-event-is-reload-iframe-navigation-manual.https.html31
-rw-r--r--test/wpt/tests/service-workers/service-worker/fetch-event-is-reload-navigation-manual.https.html8
-rw-r--r--test/wpt/tests/service-workers/service-worker/fetch-event-network-error.https.html44
-rw-r--r--test/wpt/tests/service-workers/service-worker/fetch-event-redirect.https.html1038
-rw-r--r--test/wpt/tests/service-workers/service-worker/fetch-event-referrer-policy.https.html274
-rw-r--r--test/wpt/tests/service-workers/service-worker/fetch-event-respond-with-argument.https.html44
-rw-r--r--test/wpt/tests/service-workers/service-worker/fetch-event-respond-with-body-loaded-in-chunk.https.html24
-rw-r--r--test/wpt/tests/service-workers/service-worker/fetch-event-respond-with-custom-response.https.html82
-rw-r--r--test/wpt/tests/service-workers/service-worker/fetch-event-respond-with-partial-stream.https.html62
-rw-r--r--test/wpt/tests/service-workers/service-worker/fetch-event-respond-with-readable-stream-chunk.https.html23
-rw-r--r--test/wpt/tests/service-workers/service-worker/fetch-event-respond-with-readable-stream.https.html88
-rw-r--r--test/wpt/tests/service-workers/service-worker/fetch-event-respond-with-response-body-with-invalid-chunk.https.html46
-rw-r--r--test/wpt/tests/service-workers/service-worker/fetch-event-respond-with-stops-propagation.https.html37
-rw-r--r--test/wpt/tests/service-workers/service-worker/fetch-event-throws-after-respond-with.https.html37
-rw-r--r--test/wpt/tests/service-workers/service-worker/fetch-event-within-sw-manual.https.html122
-rw-r--r--test/wpt/tests/service-workers/service-worker/fetch-event-within-sw.https.html53
-rw-r--r--test/wpt/tests/service-workers/service-worker/fetch-event.https.h2.html112
-rw-r--r--test/wpt/tests/service-workers/service-worker/fetch-event.https.html1000
-rw-r--r--test/wpt/tests/service-workers/service-worker/fetch-frame-resource.https.html236
-rw-r--r--test/wpt/tests/service-workers/service-worker/fetch-header-visibility.https.html54
-rw-r--r--test/wpt/tests/service-workers/service-worker/fetch-mixed-content-to-inscope.https.html21
-rw-r--r--test/wpt/tests/service-workers/service-worker/fetch-mixed-content-to-outscope.https.html21
-rw-r--r--test/wpt/tests/service-workers/service-worker/fetch-request-css-base-url.https.html87
-rw-r--r--test/wpt/tests/service-workers/service-worker/fetch-request-css-cross-origin.https.html81
-rw-r--r--test/wpt/tests/service-workers/service-worker/fetch-request-css-images.https.html214
-rw-r--r--test/wpt/tests/service-workers/service-worker/fetch-request-fallback.https.html282
-rw-r--r--test/wpt/tests/service-workers/service-worker/fetch-request-no-freshness-headers.https.html55
-rw-r--r--test/wpt/tests/service-workers/service-worker/fetch-request-redirect.https.html385
-rw-r--r--test/wpt/tests/service-workers/service-worker/fetch-request-resources.https.html302
-rw-r--r--test/wpt/tests/service-workers/service-worker/fetch-request-xhr-sync-error.https.window.js19
-rw-r--r--test/wpt/tests/service-workers/service-worker/fetch-request-xhr-sync-on-worker.https.html41
-rw-r--r--test/wpt/tests/service-workers/service-worker/fetch-request-xhr-sync.https.html53
-rw-r--r--test/wpt/tests/service-workers/service-worker/fetch-request-xhr.https.html75
-rw-r--r--test/wpt/tests/service-workers/service-worker/fetch-response-taint.https.html223
-rw-r--r--test/wpt/tests/service-workers/service-worker/fetch-response-xhr.https.html50
-rw-r--r--test/wpt/tests/service-workers/service-worker/fetch-waits-for-activate.https.html128
-rw-r--r--test/wpt/tests/service-workers/service-worker/getregistration.https.html108
-rw-r--r--test/wpt/tests/service-workers/service-worker/getregistrations.https.html134
-rw-r--r--test/wpt/tests/service-workers/service-worker/global-serviceworker.https.any.js53
-rw-r--r--test/wpt/tests/service-workers/service-worker/historical.https.any.js5
-rw-r--r--test/wpt/tests/service-workers/service-worker/http-to-https-redirect-and-register.https.html49
-rw-r--r--test/wpt/tests/service-workers/service-worker/immutable-prototype-serviceworker.https.html23
-rw-r--r--test/wpt/tests/service-workers/service-worker/import-scripts-cross-origin.https.html18
-rw-r--r--test/wpt/tests/service-workers/service-worker/import-scripts-data-url.https.html18
-rw-r--r--test/wpt/tests/service-workers/service-worker/import-scripts-mime-types.https.html30
-rw-r--r--test/wpt/tests/service-workers/service-worker/import-scripts-redirect.https.html55
-rw-r--r--test/wpt/tests/service-workers/service-worker/import-scripts-resource-map.https.html34
-rw-r--r--test/wpt/tests/service-workers/service-worker/import-scripts-updated-flag.https.html83
-rw-r--r--test/wpt/tests/service-workers/service-worker/indexeddb.https.html78
-rw-r--r--test/wpt/tests/service-workers/service-worker/install-event-type.https.html30
-rw-r--r--test/wpt/tests/service-workers/service-worker/installing.https.html48
-rw-r--r--test/wpt/tests/service-workers/service-worker/interface-requirements-sw.https.html16
-rw-r--r--test/wpt/tests/service-workers/service-worker/invalid-blobtype.https.html40
-rw-r--r--test/wpt/tests/service-workers/service-worker/invalid-header.https.html39
-rw-r--r--test/wpt/tests/service-workers/service-worker/iso-latin1-header.https.html40
-rw-r--r--test/wpt/tests/service-workers/service-worker/local-url-inherit-controller.https.html115
-rw-r--r--test/wpt/tests/service-workers/service-worker/mime-sniffing.https.html24
-rw-r--r--test/wpt/tests/service-workers/service-worker/multi-globals/current/current.https.html2
-rw-r--r--test/wpt/tests/service-workers/service-worker/multi-globals/current/test-sw.js1
-rw-r--r--test/wpt/tests/service-workers/service-worker/multi-globals/incumbent/incumbent.https.html20
-rw-r--r--test/wpt/tests/service-workers/service-worker/multi-globals/incumbent/test-sw.js1
-rw-r--r--test/wpt/tests/service-workers/service-worker/multi-globals/relevant/relevant.https.html2
-rw-r--r--test/wpt/tests/service-workers/service-worker/multi-globals/relevant/test-sw.js1
-rw-r--r--test/wpt/tests/service-workers/service-worker/multi-globals/test-sw.js1
-rw-r--r--test/wpt/tests/service-workers/service-worker/multi-globals/url-parsing.https.html73
-rw-r--r--test/wpt/tests/service-workers/service-worker/multipart-image.https.html68
-rw-r--r--test/wpt/tests/service-workers/service-worker/multiple-register.https.html117
-rw-r--r--test/wpt/tests/service-workers/service-worker/multiple-update.https.html94
-rw-r--r--test/wpt/tests/service-workers/service-worker/navigate-window.https.html151
-rw-r--r--test/wpt/tests/service-workers/service-worker/navigation-headers.https.html819
-rw-r--r--test/wpt/tests/service-workers/service-worker/navigation-preload/broken-chunked-encoding.https.html42
-rw-r--r--test/wpt/tests/service-workers/service-worker/navigation-preload/chunked-encoding.https.html25
-rw-r--r--test/wpt/tests/service-workers/service-worker/navigation-preload/empty-preload-response-body.https.html25
-rw-r--r--test/wpt/tests/service-workers/service-worker/navigation-preload/get-state.https.html217
-rw-r--r--test/wpt/tests/service-workers/service-worker/navigation-preload/navigationPreload.https.html20
-rw-r--r--test/wpt/tests/service-workers/service-worker/navigation-preload/redirect.https.html93
-rw-r--r--test/wpt/tests/service-workers/service-worker/navigation-preload/request-headers.https.html41
-rw-r--r--test/wpt/tests/service-workers/service-worker/navigation-preload/resource-timing.https.html92
-rw-r--r--test/wpt/tests/service-workers/service-worker/navigation-preload/resources/broken-chunked-encoding-scope.asis6
-rw-r--r--test/wpt/tests/service-workers/service-worker/navigation-preload/resources/broken-chunked-encoding-worker.js11
-rw-r--r--test/wpt/tests/service-workers/service-worker/navigation-preload/resources/chunked-encoding-scope.py19
-rw-r--r--test/wpt/tests/service-workers/service-worker/navigation-preload/resources/chunked-encoding-worker.js8
-rw-r--r--test/wpt/tests/service-workers/service-worker/navigation-preload/resources/cookie.py20
-rw-r--r--test/wpt/tests/service-workers/service-worker/navigation-preload/resources/empty-preload-response-body-scope.html0
-rw-r--r--test/wpt/tests/service-workers/service-worker/navigation-preload/resources/empty-preload-response-body-worker.js15
-rw-r--r--test/wpt/tests/service-workers/service-worker/navigation-preload/resources/get-state-worker.js21
-rw-r--r--test/wpt/tests/service-workers/service-worker/navigation-preload/resources/helpers.js5
-rw-r--r--test/wpt/tests/service-workers/service-worker/navigation-preload/resources/navigation-preload-worker.js3
-rw-r--r--test/wpt/tests/service-workers/service-worker/navigation-preload/resources/redirect-redirected.html3
-rw-r--r--test/wpt/tests/service-workers/service-worker/navigation-preload/resources/redirect-scope.py38
-rw-r--r--test/wpt/tests/service-workers/service-worker/navigation-preload/resources/redirect-worker.js35
-rw-r--r--test/wpt/tests/service-workers/service-worker/navigation-preload/resources/request-headers-scope.py14
-rw-r--r--test/wpt/tests/service-workers/service-worker/navigation-preload/resources/request-headers-worker.js10
-rw-r--r--test/wpt/tests/service-workers/service-worker/navigation-preload/resources/resource-timing-scope.py19
-rw-r--r--test/wpt/tests/service-workers/service-worker/navigation-preload/resources/resource-timing-worker.js37
-rw-r--r--test/wpt/tests/service-workers/service-worker/navigation-preload/resources/samesite-iframe.html10
-rw-r--r--test/wpt/tests/service-workers/service-worker/navigation-preload/resources/samesite-sw-helper.html34
-rw-r--r--test/wpt/tests/service-workers/service-worker/navigation-preload/resources/samesite-worker.js8
-rw-r--r--test/wpt/tests/service-workers/service-worker/navigation-preload/resources/wait-for-activate-worker.js40
-rw-r--r--test/wpt/tests/service-workers/service-worker/navigation-preload/samesite-cookies.https.html61
-rw-r--r--test/wpt/tests/service-workers/service-worker/navigation-preload/samesite-iframe.https.html67
-rw-r--r--test/wpt/tests/service-workers/service-worker/navigation-redirect-body.https.html53
-rw-r--r--test/wpt/tests/service-workers/service-worker/navigation-redirect-resolution.https.html58
-rw-r--r--test/wpt/tests/service-workers/service-worker/navigation-redirect-to-http.https.html25
-rw-r--r--test/wpt/tests/service-workers/service-worker/navigation-redirect.https.html846
-rw-r--r--test/wpt/tests/service-workers/service-worker/navigation-sets-cookie.https.html133
-rw-r--r--test/wpt/tests/service-workers/service-worker/navigation-timing-extended.https.html55
-rw-r--r--test/wpt/tests/service-workers/service-worker/navigation-timing.https.html77
-rw-r--r--test/wpt/tests/service-workers/service-worker/nested-blob-url-workers.https.html42
-rw-r--r--test/wpt/tests/service-workers/service-worker/next-hop-protocol.https.html49
-rw-r--r--test/wpt/tests/service-workers/service-worker/no-dynamic-import-in-module.any.js7
-rw-r--r--test/wpt/tests/service-workers/service-worker/no-dynamic-import.any.js3
-rw-r--r--test/wpt/tests/service-workers/service-worker/onactivate-script-error.https.html74
-rw-r--r--test/wpt/tests/service-workers/service-worker/oninstall-script-error.https.html72
-rw-r--r--test/wpt/tests/service-workers/service-worker/opaque-response-preloaded.https.html50
-rw-r--r--test/wpt/tests/service-workers/service-worker/opaque-script.https.html71
-rw-r--r--test/wpt/tests/service-workers/service-worker/partitioned-claim.tentative.https.html74
-rw-r--r--test/wpt/tests/service-workers/service-worker/partitioned-cookies.tentative.https.html119
-rw-r--r--test/wpt/tests/service-workers/service-worker/partitioned-getRegistrations.tentative.https.html99
-rw-r--r--test/wpt/tests/service-workers/service-worker/partitioned-matchAll.tentative.https.html65
-rw-r--r--test/wpt/tests/service-workers/service-worker/partitioned.tentative.https.html188
-rw-r--r--test/wpt/tests/service-workers/service-worker/performance-timeline.https.html49
-rw-r--r--test/wpt/tests/service-workers/service-worker/postMessage-client-worker.js23
-rw-r--r--test/wpt/tests/service-workers/service-worker/postmessage-blob-url.https.html33
-rw-r--r--test/wpt/tests/service-workers/service-worker/postmessage-from-waiting-serviceworker.https.html50
-rw-r--r--test/wpt/tests/service-workers/service-worker/postmessage-msgport-to-client.https.html43
-rw-r--r--test/wpt/tests/service-workers/service-worker/postmessage-to-client-message-queue.https.html212
-rw-r--r--test/wpt/tests/service-workers/service-worker/postmessage-to-client.https.html42
-rw-r--r--test/wpt/tests/service-workers/service-worker/postmessage.https.html202
-rw-r--r--test/wpt/tests/service-workers/service-worker/ready.https.window.js223
-rw-r--r--test/wpt/tests/service-workers/service-worker/redirected-response.https.html471
-rw-r--r--test/wpt/tests/service-workers/service-worker/referer.https.html40
-rw-r--r--test/wpt/tests/service-workers/service-worker/referrer-policy-header.https.html67
-rw-r--r--test/wpt/tests/service-workers/service-worker/referrer-toplevel-script-fetch.https.html64
-rw-r--r--test/wpt/tests/service-workers/service-worker/register-closed-window.https.html35
-rw-r--r--test/wpt/tests/service-workers/service-worker/register-default-scope.https.html69
-rw-r--r--test/wpt/tests/service-workers/service-worker/register-same-scope-different-script-url.https.html233
-rw-r--r--test/wpt/tests/service-workers/service-worker/register-wait-forever-in-install-worker.https.html57
-rw-r--r--test/wpt/tests/service-workers/service-worker/registration-basic.https.html39
-rw-r--r--test/wpt/tests/service-workers/service-worker/registration-end-to-end.https.html88
-rw-r--r--test/wpt/tests/service-workers/service-worker/registration-events.https.html42
-rw-r--r--test/wpt/tests/service-workers/service-worker/registration-iframe.https.html116
-rw-r--r--test/wpt/tests/service-workers/service-worker/registration-mime-types.https.html10
-rw-r--r--test/wpt/tests/service-workers/service-worker/registration-schedule-job.https.html107
-rw-r--r--test/wpt/tests/service-workers/service-worker/registration-scope-module-static-import.https.html41
-rw-r--r--test/wpt/tests/service-workers/service-worker/registration-scope.https.html9
-rw-r--r--test/wpt/tests/service-workers/service-worker/registration-script-module.https.html13
-rw-r--r--test/wpt/tests/service-workers/service-worker/registration-script-url.https.html9
-rw-r--r--test/wpt/tests/service-workers/service-worker/registration-script.https.html12
-rw-r--r--test/wpt/tests/service-workers/service-worker/registration-security-error.https.html9
-rw-r--r--test/wpt/tests/service-workers/service-worker/registration-service-worker-attributes.https.html72
-rw-r--r--test/wpt/tests/service-workers/service-worker/registration-updateviacache.https.html204
-rw-r--r--test/wpt/tests/service-workers/service-worker/rejections.https.html21
-rw-r--r--test/wpt/tests/service-workers/service-worker/request-end-to-end.https.html40
-rw-r--r--test/wpt/tests/service-workers/service-worker/resource-timing-bodySize.https.html55
-rw-r--r--test/wpt/tests/service-workers/service-worker/resource-timing-cross-origin.https.html46
-rw-r--r--test/wpt/tests/service-workers/service-worker/resource-timing-fetch-variants.https.html121
-rw-r--r--test/wpt/tests/service-workers/service-worker/resource-timing.sub.https.html150
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/404.py5
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/about-blank-replacement-blank-dynamic-nested-frame.html21
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/about-blank-replacement-blank-nested-frame.html21
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/about-blank-replacement-frame.py31
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/about-blank-replacement-ping-frame.py49
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/about-blank-replacement-popup-frame.py32
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/about-blank-replacement-srcdoc-nested-frame.html22
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/about-blank-replacement-uncontrolled-nested-frame.html22
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/about-blank-replacement-worker.js95
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/basic-module-2.js1
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/basic-module.js1
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/blank.html2
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/bytecheck-worker-imported-script.py20
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/bytecheck-worker.py38
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/claim-blob-url-worker-fetch-iframe.html21
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/claim-nested-worker-fetch-iframe.html16
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/claim-nested-worker-fetch-parent-worker.js12
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/claim-shared-worker-fetch-iframe.html13
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/claim-shared-worker-fetch-worker.js8
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/claim-with-redirect-iframe.html48
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/claim-worker-fetch-iframe.html13
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/claim-worker-fetch-worker.js5
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/claim-worker.js19
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/classic-worker.js1
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/client-id-worker.js27
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/client-navigate-frame.html12
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/client-navigate-worker.js92
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/client-navigated-frame.html3
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/client-url-of-blob-url-worker.html26
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/client-url-of-blob-url-worker.js10
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/clients-frame-freeze.html15
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/clients-get-client-types-frame-worker.js11
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/clients-get-client-types-frame.html17
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/clients-get-client-types-shared-worker.js10
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/clients-get-client-types-worker.js11
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/clients-get-cross-origin-frame.html50
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/clients-get-frame.html12
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/clients-get-other-origin.html64
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/clients-get-resultingClientId-worker.js60
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/clients-get-worker.js41
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/clients-matchall-blob-url-worker.html20
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/clients-matchall-client-types-dedicated-worker.js3
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/clients-matchall-client-types-iframe.html8
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/clients-matchall-client-types-shared-worker.js4
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/clients-matchall-on-evaluation-worker.js11
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/clients-matchall-worker.js40
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/controlled-frame-postMessage.html39
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/controlled-worker-late-postMessage.js6
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/controlled-worker-postMessage.js4
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/cors-approved.txt1
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/cors-approved.txt.headers3
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/cors-denied.txt2
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/create-blob-url-worker.js22
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/create-out-of-scope-worker.html19
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/echo-content.py16
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/echo-cookie-worker.py24
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/echo-message-to-source-worker.js3
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/embed-and-object-are-not-intercepted-worker.js14
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/embed-image-is-not-intercepted-iframe.html21
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/embed-is-not-intercepted-iframe.html17
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/embed-navigation-is-not-intercepted-iframe.html23
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/embedded-content-from-server.html6
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/embedded-content-from-service-worker.html7
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/empty-but-slow-worker.js8
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/empty-worker.js1
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/empty.h2.js0
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/empty.html6
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/empty.js0
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/enable-client-message-queue.html39
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/end-to-end-worker.js7
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/events-worker.js12
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/extendable-event-async-waituntil.js210
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/extendable-event-waituntil.js87
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/fail-on-fetch-worker.js5
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/fetch-access-control-login.html16
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/fetch-access-control.py114
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/fetch-canvas-tainting-double-write-worker.js7
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/fetch-canvas-tainting-iframe.html70
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/fetch-canvas-tainting-tests.js241
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/fetch-cors-exposed-header-names-worker.js3
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/fetch-cors-xhr-iframe.html170
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/fetch-csp-iframe.html16
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/fetch-csp-iframe.html.sub.headers1
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/fetch-error-worker.js22
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/fetch-event-add-async-worker.js6
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/fetch-event-after-navigation-within-page-iframe.html22
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/fetch-event-async-respond-with-worker.js66
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/fetch-event-handled-worker.js37
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/fetch-event-network-error-controllee-iframe.html60
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/fetch-event-network-error-worker.js49
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/fetch-event-network-fallback-worker.js3
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/fetch-event-respond-with-argument-iframe.html55
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/fetch-event-respond-with-argument-worker.js14
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/fetch-event-respond-with-body-loaded-in-chunk-worker.js7
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/fetch-event-respond-with-custom-response-worker.js45
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/fetch-event-respond-with-partial-stream-worker.js28
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/fetch-event-respond-with-readable-stream-chunk-worker.js40
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/fetch-event-respond-with-readable-stream-worker.js81
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/fetch-event-respond-with-response-body-with-invalid-chunk-iframe.html15
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/fetch-event-respond-with-response-body-with-invalid-chunk-worker.js12
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/fetch-event-respond-with-stops-propagation-worker.js15
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/fetch-event-test-worker.js224
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/fetch-event-within-sw-worker.js48
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/fetch-header-visibility-iframe.html66
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/fetch-mixed-content-iframe-inscope-to-inscope.html71
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/fetch-mixed-content-iframe-inscope-to-outscope.html80
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/fetch-mixed-content-iframe.html71
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/fetch-request-css-base-url-iframe.html20
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/fetch-request-css-base-url-style.css1
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/fetch-request-css-base-url-worker.js45
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-mime-check-cross.css1
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-mime-check-cross.html1
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-mime-check-iframe.html17
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-mime-check-same.css1
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-mime-check-same.html1
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-read-contents.html15
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-worker.js65
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/fetch-request-fallback-iframe.html32
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/fetch-request-fallback-worker.js13
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/fetch-request-html-imports-iframe.html13
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/fetch-request-html-imports-worker.js30
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/fetch-request-no-freshness-headers-iframe.html1
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/fetch-request-no-freshness-headers-script.py6
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/fetch-request-no-freshness-headers-worker.js18
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/fetch-request-redirect-iframe.html35
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/fetch-request-resources-iframe.https.html87
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/fetch-request-resources-worker.js26
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/fetch-request-xhr-iframe.https.html208
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/fetch-request-xhr-sync-error-worker.js19
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/fetch-request-xhr-sync-iframe.html13
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/fetch-request-xhr-sync-on-worker-worker.js41
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/fetch-request-xhr-sync-worker.js7
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/fetch-request-xhr-worker.js22
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/fetch-response-taint-iframe.html2
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/fetch-response-xhr-iframe.https.html53
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/fetch-response-xhr-worker.js12
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/fetch-response.html29
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/fetch-response.js35
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/fetch-rewrite-worker-referrer-policy.js4
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/fetch-rewrite-worker-referrer-policy.js.headers2
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/fetch-rewrite-worker.js166
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/fetch-rewrite-worker.js.headers2
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/fetch-variants-worker.js35
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/fetch-waits-for-activate-worker.js31
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/form-poster.html13
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/frame-for-getregistrations.html19
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/get-resultingClientId-worker.js107
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/http-to-https-redirect-and-register-iframe.html25
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/iframe-with-fetch-variants.html14
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/iframe-with-image.html2
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/immutable-prototype-serviceworker.js19
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/import-echo-cookie-worker-module.py6
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/import-echo-cookie-worker.js1
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/import-mime-type-worker.py10
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/import-relative.xsl5
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/import-scripts-404-after-update-plus-update-worker.js8
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/import-scripts-404-after-update.js6
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/import-scripts-404.js1
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/import-scripts-cross-origin-worker.sub.js1
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/import-scripts-data-url-worker.js1
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/import-scripts-diff-resource-map-worker.js10
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/import-scripts-echo.py6
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/import-scripts-get.py6
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/import-scripts-mime-types-worker.js49
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/import-scripts-redirect-import.js1
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/import-scripts-redirect-on-second-time-worker.js7
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/import-scripts-redirect-worker.js1
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/import-scripts-resource-map-worker.js15
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/import-scripts-updated-flag-worker.js31
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/import-scripts-version.py17
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/imported-classic-script.js1
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/imported-module-script.js1
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/indexeddb-worker.js57
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/install-event-type-worker.js9
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/install-worker.html22
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/interface-requirements-worker.sub.js59
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/invalid-blobtype-iframe.https.html28
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/invalid-blobtype-worker.js10
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/invalid-chunked-encoding-with-flush.py9
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/invalid-chunked-encoding.py2
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/invalid-header-iframe.https.html25
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/invalid-header-worker.js12
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/iso-latin1-header-iframe.html23
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/iso-latin1-header-worker.js12
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/load_worker.js29
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/loaded.html9
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/local-url-inherit-controller-frame.html130
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/local-url-inherit-controller-worker.js5
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/location-setter.html10
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/malformed-http-response.asis1
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/malformed-worker.py14
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/message-vs-microtask.html18
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/mime-sniffing-worker.js9
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/mime-type-worker.py4
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/mint-new-worker.py27
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/missing.asis4
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/module-worker.js1
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/multipart-image-iframe.html19
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/multipart-image-worker.js21
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/multipart-image.py23
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/navigate-window-worker.js21
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/navigation-headers-server.py19
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/navigation-redirect-body-worker.js11
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/navigation-redirect-body.py11
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/navigation-redirect-other-origin.html89
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/navigation-redirect-out-scope.py22
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/navigation-redirect-scope1.py22
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/navigation-redirect-scope2.py22
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/navigation-redirect-to-http-iframe.html42
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/navigation-redirect-to-http-worker.js22
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/navigation-timing-worker-extended.js22
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/navigation-timing-worker.js15
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/nested-blob-url-worker-created-from-worker.html16
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/nested-blob-url-workers.html38
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/nested-iframe-parent.html5
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/nested-parent.html18
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/nested-worker-created-from-blob-url-worker.html33
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/nested_load_worker.js23
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/no-dynamic-import.js18
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/notification_icon.py11
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/object-image-is-not-intercepted-iframe.html21
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/object-is-not-intercepted-iframe.html18
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/object-navigation-is-not-intercepted-iframe.html24
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/onactivate-throw-error-from-nested-event-worker.js13
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/onactivate-throw-error-then-cancel-worker.js3
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/onactivate-throw-error-then-prevent-default-worker.js7
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/onactivate-throw-error-with-empty-onerror-worker.js2
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/onactivate-throw-error-worker.js7
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/onactivate-waituntil-forever.js8
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/onfetch-waituntil-forever.js10
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/oninstall-throw-error-from-nested-event-worker.js12
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/oninstall-throw-error-then-cancel-worker.js3
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/oninstall-throw-error-then-prevent-default-worker.js7
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/oninstall-throw-error-with-empty-onerror-worker.js2
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/oninstall-throw-error-worker.js7
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/oninstall-waituntil-forever.js8
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/oninstall-waituntil-throw-error-worker.js5
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/onparse-infiniteloop-worker.js8
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/opaque-response-being-preloaded-xhr.html33
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/opaque-response-preloaded-worker.js12
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/opaque-response-preloaded-xhr.html35
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/opaque-script-frame.html21
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/opaque-script-large.js41
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/opaque-script-small.js3
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/opaque-script-sw.js37
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/other.html3
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/override_assert_object_equals.js58
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/partitioned-cookies-3p-credentialless-frame.html114
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/partitioned-cookies-3p-frame.html108
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/partitioned-cookies-3p-sw.js53
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/partitioned-cookies-3p-window.html35
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/partitioned-cookies-sw.js53
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/partitioned-service-worker-iframe-claim.html59
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/partitioned-service-worker-nested-iframe-child.html44
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/partitioned-service-worker-nested-iframe-parent.html30
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/partitioned-service-worker-third-party-iframe-getRegistrations.html40
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/partitioned-service-worker-third-party-iframe-matchAll.html27
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/partitioned-service-worker-third-party-iframe.html36
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/partitioned-service-worker-third-party-window.html41
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/partitioned-storage-sw.js81
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/partitioned-utils.js110
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/pass-through-worker.js3
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/pass.txt1
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/performance-timeline-worker.js62
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/postmessage-blob-url.js5
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/postmessage-dictionary-transferables-worker.js24
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/postmessage-echo-worker.js3
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/postmessage-fetched-text.js5
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/postmessage-msgport-to-client-worker.js19
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/postmessage-on-load-worker.js9
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/postmessage-to-client-worker.js10
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/postmessage-transferables-worker.js24
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/postmessage-worker.js19
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/range-request-to-different-origins-worker.js40
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/range-request-with-different-cors-modes-worker.js60
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/redirect-worker.js145
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/redirect.py27
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/referer-iframe.html39
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/referrer-policy-iframe.html32
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/register-closed-window-iframe.html19
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/register-iframe.html4
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/register-rewrite-worker.html32
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/registration-tests-mime-types.js96
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/registration-tests-scope.js120
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/registration-tests-script-url.js82
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/registration-tests-script.js121
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/registration-tests-security-error.js78
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/registration-worker.js1
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/reject-install-worker.js3
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/reply-to-message.html7
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/request-end-to-end-worker.js34
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/request-headers.py8
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/resource-timing-iframe.sub.html10
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/resource-timing-worker.js12
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/respond-then-throw-worker.js40
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/respond-with-body-accessed-response-iframe.html20
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/respond-with-body-accessed-response-worker.js93
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/respond-with-body-accessed-response.jsonp1
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/sample-worker-interceptor.js62
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/sample.html2
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/sample.js1
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/sample.txt1
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/sandboxed-iframe-fetch-event-iframe.html63
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/sandboxed-iframe-fetch-event-iframe.py18
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/sandboxed-iframe-fetch-event-worker.js20
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/sandboxed-iframe-navigator-serviceworker-iframe.html25
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/scope1/module-worker-importing-redirect-to-scope2.js1
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/scope1/module-worker-importing-scope2.js1
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/scope1/redirect.py6
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/scope2/import-scripts-echo.py6
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/scope2/imported-module-script.js4
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/scope2/simple.txt1
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/scope2/worker_interception_redirect_webworker.py6
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/secure-context-service-worker.js21
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/secure-context/sender.html1
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/secure-context/window.html15
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/service-worker-csp-worker.py183
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/service-worker-header.py20
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/service-worker-interception-dynamic-import-worker.js1
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/service-worker-interception-network-worker.js1
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/service-worker-interception-service-worker.js9
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/service-worker-interception-static-import-worker.js1
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/silence.ogabin0 -> 12983 bytes
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/simple-intercept-worker.js5
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/simple-intercept-worker.js.headers1
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/simple.html3
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/simple.txt1
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/skip-waiting-installed-worker.js33
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/skip-waiting-worker.js21
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/square.pngbin0 -> 18299 bytes
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/square.png.sub.headers2
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/stalling-service-worker.js54
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/subdir/blank.html2
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/subdir/import-scripts-echo.py6
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/subdir/simple.txt1
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/subdir/worker_interception_redirect_webworker.py6
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/success.py8
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/svg-target-reftest-001-frame.html3
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/svg-target-reftest-001.html5
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/svg-target-reftest-frame.html2
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/test-helpers.sub.js300
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/test-request-headers-worker.js10
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/test-request-headers-worker.py21
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/test-request-mode-worker.js10
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/test-request-mode-worker.py22
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/testharness-helpers.js136
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/trickle.py14
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/type-check-worker.js10
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/unregister-controller-page.html16
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/unregister-immediately-helpers.js19
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/unregister-rewrite-worker.html18
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/update-claim-worker.py24
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/update-during-installation-worker.js61
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/update-during-installation-worker.py11
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/update-fetch-worker.py18
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/update-max-aged-worker-imported-script.py14
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/update-max-aged-worker.py30
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/update-missing-import-scripts-imported-worker.py9
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/update-missing-import-scripts-main-worker.py15
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/update-nocookie-worker.py14
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/update-recovery-worker.py25
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/update-registration-with-type.py33
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/update-smaller-body-after-update-worker.js1
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/update-smaller-body-before-update-worker.js2
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/update-worker-from-file.py33
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/update-worker.py62
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/update/update-after-oneday.https.html8
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/update_shell.py32
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/vtt-frame.html6
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/wait-forever-in-install-worker.js12
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/websocket-worker.js35
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/websocket.js7
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/window-opener.html17
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/windowclient-navigate-worker.js75
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/worker-client-id-worker.js25
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/worker-fetching-cross-origin.js12
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/worker-interception-redirect-serviceworker.js53
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/worker-interception-redirect-webworker.js56
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/worker-load-interceptor.js16
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/worker-testharness.js49
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/worker_interception_redirect_webworker.py20
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/xhr-content-length-worker.js22
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/xhr-iframe.html23
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/xhr-response-url-worker.js32
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/xsl-base-url-iframe.xml5
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/xsl-base-url-worker.js12
-rw-r--r--test/wpt/tests/service-workers/service-worker/resources/xslt-pass.xsl11
-rw-r--r--test/wpt/tests/service-workers/service-worker/respond-with-body-accessed-response.https.html54
-rw-r--r--test/wpt/tests/service-workers/service-worker/same-site-cookies.https.html496
-rw-r--r--test/wpt/tests/service-workers/service-worker/sandboxed-iframe-fetch-event.https.html536
-rw-r--r--test/wpt/tests/service-workers/service-worker/sandboxed-iframe-navigator-serviceworker.https.html120
-rw-r--r--test/wpt/tests/service-workers/service-worker/secure-context.https.html57
-rw-r--r--test/wpt/tests/service-workers/service-worker/service-worker-csp-connect.https.html10
-rw-r--r--test/wpt/tests/service-workers/service-worker/service-worker-csp-default.https.html10
-rw-r--r--test/wpt/tests/service-workers/service-worker/service-worker-csp-script.https.html10
-rw-r--r--test/wpt/tests/service-workers/service-worker/service-worker-header.https.html23
-rw-r--r--test/wpt/tests/service-workers/service-worker/serviceworker-message-event-historical.https.html45
-rw-r--r--test/wpt/tests/service-workers/service-worker/serviceworkerobject-scripturl.https.html26
-rw-r--r--test/wpt/tests/service-workers/service-worker/skip-waiting-installed.https.html70
-rw-r--r--test/wpt/tests/service-workers/service-worker/skip-waiting-using-registration.https.html66
-rw-r--r--test/wpt/tests/service-workers/service-worker/skip-waiting-without-client.https.html12
-rw-r--r--test/wpt/tests/service-workers/service-worker/skip-waiting-without-using-registration.https.html44
-rw-r--r--test/wpt/tests/service-workers/service-worker/skip-waiting.https.html58
-rw-r--r--test/wpt/tests/service-workers/service-worker/state.https.html74
-rw-r--r--test/wpt/tests/service-workers/service-worker/svg-target-reftest.https.html28
-rw-r--r--test/wpt/tests/service-workers/service-worker/synced-state.https.html93
-rw-r--r--test/wpt/tests/service-workers/service-worker/tentative/static-router/README.md4
-rw-r--r--test/wpt/tests/service-workers/service-worker/tentative/static-router/resources/direct.txt1
-rw-r--r--test/wpt/tests/service-workers/service-worker/tentative/static-router/resources/simple-test-for-condition-main-resource.html3
-rw-r--r--test/wpt/tests/service-workers/service-worker/tentative/static-router/resources/simple.html3
-rw-r--r--test/wpt/tests/service-workers/service-worker/tentative/static-router/resources/static-router-sw.js35
-rw-r--r--test/wpt/tests/service-workers/service-worker/tentative/static-router/resources/test-helpers.sub.js303
-rw-r--r--test/wpt/tests/service-workers/service-worker/tentative/static-router/static-router-main-resource.https.html58
-rw-r--r--test/wpt/tests/service-workers/service-worker/tentative/static-router/static-router-subresource.https.html48
-rw-r--r--test/wpt/tests/service-workers/service-worker/uncontrolled-page.https.html39
-rw-r--r--test/wpt/tests/service-workers/service-worker/unregister-controller.https.html108
-rw-r--r--test/wpt/tests/service-workers/service-worker/unregister-immediately-before-installed.https.html57
-rw-r--r--test/wpt/tests/service-workers/service-worker/unregister-immediately-during-extendable-events.https.html50
-rw-r--r--test/wpt/tests/service-workers/service-worker/unregister-immediately.https.html134
-rw-r--r--test/wpt/tests/service-workers/service-worker/unregister-then-register-new-script.https.html136
-rw-r--r--test/wpt/tests/service-workers/service-worker/unregister-then-register.https.html107
-rw-r--r--test/wpt/tests/service-workers/service-worker/unregister.https.html40
-rw-r--r--test/wpt/tests/service-workers/service-worker/update-after-navigation-fetch-event.https.html91
-rw-r--r--test/wpt/tests/service-workers/service-worker/update-after-navigation-redirect.https.html74
-rw-r--r--test/wpt/tests/service-workers/service-worker/update-after-oneday.https.html51
-rw-r--r--test/wpt/tests/service-workers/service-worker/update-bytecheck-cors-import.https.html92
-rw-r--r--test/wpt/tests/service-workers/service-worker/update-bytecheck.https.html92
-rw-r--r--test/wpt/tests/service-workers/service-worker/update-import-scripts.https.html135
-rw-r--r--test/wpt/tests/service-workers/service-worker/update-missing-import-scripts.https.html33
-rw-r--r--test/wpt/tests/service-workers/service-worker/update-module-request-mode.https.html45
-rw-r--r--test/wpt/tests/service-workers/service-worker/update-no-cache-request-headers.https.html48
-rw-r--r--test/wpt/tests/service-workers/service-worker/update-not-allowed.https.html140
-rw-r--r--test/wpt/tests/service-workers/service-worker/update-on-navigation.https.html20
-rw-r--r--test/wpt/tests/service-workers/service-worker/update-recovery.https.html73
-rw-r--r--test/wpt/tests/service-workers/service-worker/update-registration-with-type.https.html208
-rw-r--r--test/wpt/tests/service-workers/service-worker/update-result.https.html23
-rw-r--r--test/wpt/tests/service-workers/service-worker/update.https.html164
-rw-r--r--test/wpt/tests/service-workers/service-worker/waiting.https.html47
-rw-r--r--test/wpt/tests/service-workers/service-worker/websocket-in-service-worker.https.html27
-rw-r--r--test/wpt/tests/service-workers/service-worker/websocket.https.html45
-rw-r--r--test/wpt/tests/service-workers/service-worker/webvtt-cross-origin.https.html175
-rw-r--r--test/wpt/tests/service-workers/service-worker/windowclient-navigate.https.html190
-rw-r--r--test/wpt/tests/service-workers/service-worker/worker-client-id.https.html58
-rw-r--r--test/wpt/tests/service-workers/service-worker/worker-in-sandboxed-iframe-by-csp-fetch-event.https.html132
-rw-r--r--test/wpt/tests/service-workers/service-worker/worker-interception-redirect.https.html212
-rw-r--r--test/wpt/tests/service-workers/service-worker/worker-interception.https.html244
-rw-r--r--test/wpt/tests/service-workers/service-worker/xhr-content-length.https.window.js55
-rw-r--r--test/wpt/tests/service-workers/service-worker/xhr-response-url.https.html103
-rw-r--r--test/wpt/tests/service-workers/service-worker/xsl-base-url.https.html32
-rw-r--r--test/wpt/tests/storage/META.yml4
-rw-r--r--test/wpt/tests/storage/README.md7
-rw-r--r--test/wpt/tests/storage/buckets/META.yml5
-rw-r--r--test/wpt/tests/storage/buckets/bucket-quota-indexeddb.tentative.https.any.js35
-rw-r--r--test/wpt/tests/storage/buckets/bucket-storage-policy.tentative.https.any.js21
-rw-r--r--test/wpt/tests/storage/buckets/resources/cached-resource.txt1
-rw-r--r--test/wpt/tests/storage/buckets/resources/util.js57
-rw-r--r--test/wpt/tests/storage/estimate-indexeddb.https.any.js61
-rw-r--r--test/wpt/tests/storage/estimate-parallel.https.any.js13
-rw-r--r--test/wpt/tests/storage/estimate-usage-details-caches.https.tentative.any.js20
-rw-r--r--test/wpt/tests/storage/estimate-usage-details-indexeddb.https.tentative.any.js59
-rw-r--r--test/wpt/tests/storage/estimate-usage-details-service-workers.https.tentative.window.js38
-rw-r--r--test/wpt/tests/storage/estimate-usage-details.https.tentative.any.js12
-rw-r--r--test/wpt/tests/storage/helpers.js46
-rw-r--r--test/wpt/tests/storage/idlharness.https.any.js18
-rw-r--r--test/wpt/tests/storage/opaque-origin.https.window.js80
-rw-r--r--test/wpt/tests/storage/partitioned-estimate-usage-details-caches.tentative.https.sub.html74
-rw-r--r--test/wpt/tests/storage/partitioned-estimate-usage-details-indexeddb.tentative.https.sub.html84
-rw-r--r--test/wpt/tests/storage/partitioned-estimate-usage-details-service-workers.tentative.https.sub.html88
-rw-r--r--test/wpt/tests/storage/permission-query.https.any.js10
-rw-r--r--test/wpt/tests/storage/persist-permission-manual.https.html27
-rw-r--r--test/wpt/tests/storage/persisted.https.any.js14
-rw-r--r--test/wpt/tests/storage/quotachange-in-detached-iframe.tentative.https.html21
-rw-r--r--test/wpt/tests/storage/resources/partitioned-estimate-usage-details-caches-helper-frame.html30
-rw-r--r--test/wpt/tests/storage/resources/partitioned-estimate-usage-details-indexeddb-helper-frame.html28
-rw-r--r--test/wpt/tests/storage/resources/partitioned-estimate-usage-details-service-workers-helper-frame.html30
-rw-r--r--test/wpt/tests/storage/resources/worker.js3
-rw-r--r--test/wpt/tests/storage/storagemanager-estimate.https.any.js60
-rw-r--r--test/wpt/tests/storage/storagemanager-persist-persisted-match.https.any.js9
-rw-r--r--test/wpt/tests/storage/storagemanager-persist.https.window.js10
-rw-r--r--test/wpt/tests/storage/storagemanager-persist.https.worker.js8
-rw-r--r--test/wpt/tests/storage/storagemanager-persisted.https.any.js10
-rw-r--r--test/wpt/tests/websockets/Close-1000-reason.any.js21
-rw-r--r--test/wpt/tests/websockets/Close-1000-verify-code.any.js21
-rw-r--r--test/wpt/tests/websockets/Close-1000.any.js21
-rw-r--r--test/wpt/tests/websockets/Close-1005-verify-code.any.js21
-rw-r--r--test/wpt/tests/websockets/Close-1005.any.js18
-rw-r--r--test/wpt/tests/websockets/Close-2999-reason.any.js17
-rw-r--r--test/wpt/tests/websockets/Close-3000-reason.any.js21
-rw-r--r--test/wpt/tests/websockets/Close-3000-verify-code.any.js20
-rw-r--r--test/wpt/tests/websockets/Close-4999-reason.any.js21
-rw-r--r--test/wpt/tests/websockets/Close-Reason-124Bytes.any.js20
-rw-r--r--test/wpt/tests/websockets/Close-delayed.any.js27
-rw-r--r--test/wpt/tests/websockets/Close-onlyReason.any.js17
-rw-r--r--test/wpt/tests/websockets/Close-readyState-Closed.any.js21
-rw-r--r--test/wpt/tests/websockets/Close-readyState-Closing.any.js20
-rw-r--r--test/wpt/tests/websockets/Close-reason-unpaired-surrogates.any.js22
-rw-r--r--test/wpt/tests/websockets/Close-server-initiated-close.any.js21
-rw-r--r--test/wpt/tests/websockets/Close-undefined.any.js19
-rw-r--r--test/wpt/tests/websockets/Create-asciiSep-protocol-string.any.js12
-rw-r--r--test/wpt/tests/websockets/Create-blocked-port.any.js97
-rw-r--r--test/wpt/tests/websockets/Create-extensions-empty.any.js20
-rw-r--r--test/wpt/tests/websockets/Create-http-urls.any.js19
-rw-r--r--test/wpt/tests/websockets/Create-invalid-urls.any.js14
-rw-r--r--test/wpt/tests/websockets/Create-non-absolute-url.any.js14
-rw-r--r--test/wpt/tests/websockets/Create-nonAscii-protocol-string.any.js12
-rw-r--r--test/wpt/tests/websockets/Create-on-worker-shutdown.any.js26
-rw-r--r--test/wpt/tests/websockets/Create-protocol-with-space.any.js11
-rw-r--r--test/wpt/tests/websockets/Create-protocols-repeated-case-insensitive.any.js11
-rw-r--r--test/wpt/tests/websockets/Create-protocols-repeated.any.js11
-rw-r--r--test/wpt/tests/websockets/Create-url-with-space.any.js12
-rw-r--r--test/wpt/tests/websockets/Create-url-with-windows-1252-encoding.html20
-rw-r--r--test/wpt/tests/websockets/Create-valid-url-array-protocols.any.js21
-rw-r--r--test/wpt/tests/websockets/Create-valid-url-binaryType-blob.any.js21
-rw-r--r--test/wpt/tests/websockets/Create-valid-url-protocol-empty.any.js10
-rw-r--r--test/wpt/tests/websockets/Create-valid-url-protocol-setCorrectly.any.js21
-rw-r--r--test/wpt/tests/websockets/Create-valid-url-protocol-string.any.js21
-rw-r--r--test/wpt/tests/websockets/Create-valid-url-protocol.any.js21
-rw-r--r--test/wpt/tests/websockets/Create-valid-url.any.js21
-rw-r--r--test/wpt/tests/websockets/META.yml6
-rw-r--r--test/wpt/tests/websockets/README.md1
-rw-r--r--test/wpt/tests/websockets/Send-0byte-data.any.js30
-rw-r--r--test/wpt/tests/websockets/Send-65K-data.any.js33
-rw-r--r--test/wpt/tests/websockets/Send-before-open.any.js11
-rw-r--r--test/wpt/tests/websockets/Send-binary-65K-arraybuffer.any.js33
-rw-r--r--test/wpt/tests/websockets/Send-binary-arraybuffer.any.js33
-rw-r--r--test/wpt/tests/websockets/Send-binary-arraybufferview-float32.any.js40
-rw-r--r--test/wpt/tests/websockets/Send-binary-arraybufferview-float64.any.js40
-rw-r--r--test/wpt/tests/websockets/Send-binary-arraybufferview-int16-offset.any.js40
-rw-r--r--test/wpt/tests/websockets/Send-binary-arraybufferview-int32.any.js40
-rw-r--r--test/wpt/tests/websockets/Send-binary-arraybufferview-int8.any.js40
-rw-r--r--test/wpt/tests/websockets/Send-binary-arraybufferview-uint16-offset-length.any.js40
-rw-r--r--test/wpt/tests/websockets/Send-binary-arraybufferview-uint32-offset.any.js40
-rw-r--r--test/wpt/tests/websockets/Send-binary-arraybufferview-uint8-offset-length.any.js40
-rw-r--r--test/wpt/tests/websockets/Send-binary-arraybufferview-uint8-offset.any.js40
-rw-r--r--test/wpt/tests/websockets/Send-binary-blob.any.js36
-rw-r--r--test/wpt/tests/websockets/Send-data.any.js30
-rw-r--r--test/wpt/tests/websockets/Send-data.worker.js26
-rw-r--r--test/wpt/tests/websockets/Send-null.any.js32
-rw-r--r--test/wpt/tests/websockets/Send-paired-surrogates.any.js30
-rw-r--r--test/wpt/tests/websockets/Send-unicode-data.any.js30
-rw-r--r--test/wpt/tests/websockets/Send-unpaired-surrogates.any.js30
-rw-r--r--test/wpt/tests/websockets/back-forward-cache-with-closed-websocket-connection-ccns.tentative.window.js31
-rw-r--r--test/wpt/tests/websockets/back-forward-cache-with-closed-websocket-connection.window.js20
-rw-r--r--test/wpt/tests/websockets/back-forward-cache-with-open-websocket-connection-ccns.tentative.window.js32
-rw-r--r--test/wpt/tests/websockets/back-forward-cache-with-open-websocket-connection.window.js21
-rw-r--r--test/wpt/tests/websockets/basic-auth.any.js17
-rw-r--r--test/wpt/tests/websockets/binary/001.html27
-rw-r--r--test/wpt/tests/websockets/binary/002.html28
-rw-r--r--test/wpt/tests/websockets/binary/004.html27
-rw-r--r--test/wpt/tests/websockets/binary/005.html26
-rw-r--r--test/wpt/tests/websockets/binaryType-wrong-value.any.js23
-rw-r--r--test/wpt/tests/websockets/bufferedAmount-unchanged-by-sync-xhr.any.js25
-rw-r--r--test/wpt/tests/websockets/close-invalid.any.js21
-rw-r--r--test/wpt/tests/websockets/closing-handshake/002.html23
-rw-r--r--test/wpt/tests/websockets/closing-handshake/003.html24
-rw-r--r--test/wpt/tests/websockets/closing-handshake/004.html25
-rw-r--r--test/wpt/tests/websockets/constants.sub.js94
-rw-r--r--test/wpt/tests/websockets/constructor.any.js10
-rw-r--r--test/wpt/tests/websockets/constructor/001.html14
-rw-r--r--test/wpt/tests/websockets/constructor/004.html36
-rw-r--r--test/wpt/tests/websockets/constructor/005.html14
-rw-r--r--test/wpt/tests/websockets/constructor/006.html29
-rw-r--r--test/wpt/tests/websockets/constructor/007.html17
-rw-r--r--test/wpt/tests/websockets/constructor/008.html15
-rw-r--r--test/wpt/tests/websockets/constructor/009.html24
-rw-r--r--test/wpt/tests/websockets/constructor/010.html22
-rw-r--r--test/wpt/tests/websockets/constructor/011.html28
-rw-r--r--test/wpt/tests/websockets/constructor/012.html20
-rw-r--r--test/wpt/tests/websockets/constructor/013.html42
-rw-r--r--test/wpt/tests/websockets/constructor/014.html39
-rw-r--r--test/wpt/tests/websockets/constructor/016.html20
-rw-r--r--test/wpt/tests/websockets/constructor/017.html19
-rw-r--r--test/wpt/tests/websockets/constructor/018.html20
-rw-r--r--test/wpt/tests/websockets/constructor/019.html21
-rw-r--r--test/wpt/tests/websockets/constructor/020.html21
-rw-r--r--test/wpt/tests/websockets/constructor/021.html12
-rw-r--r--test/wpt/tests/websockets/constructor/022.html23
-rw-r--r--test/wpt/tests/websockets/cookies/001.html28
-rw-r--r--test/wpt/tests/websockets/cookies/002.html26
-rw-r--r--test/wpt/tests/websockets/cookies/003.html34
-rw-r--r--test/wpt/tests/websockets/cookies/004.html31
-rw-r--r--test/wpt/tests/websockets/cookies/005.html35
-rw-r--r--test/wpt/tests/websockets/cookies/006.html35
-rw-r--r--test/wpt/tests/websockets/cookies/007.html36
-rw-r--r--test/wpt/tests/websockets/cookies/support/set-cookie.py7
-rw-r--r--test/wpt/tests/websockets/cookies/support/websocket-cookies-helper.sub.js57
-rw-r--r--test/wpt/tests/websockets/cookies/third-party-cookie-accepted.https.html25
-rw-r--r--test/wpt/tests/websockets/eventhandlers.any.js15
-rw-r--r--test/wpt/tests/websockets/extended-payload-length.html72
-rw-r--r--test/wpt/tests/websockets/handlers/basic_auth_wsh.py26
-rw-r--r--test/wpt/tests/websockets/handlers/delayed-passive-close_wsh.py27
-rw-r--r--test/wpt/tests/websockets/handlers/echo-cookie_wsh.py12
-rw-r--r--test/wpt/tests/websockets/handlers/echo-query_v13_wsh.py11
-rw-r--r--test/wpt/tests/websockets/handlers/echo-query_wsh.py9
-rw-r--r--test/wpt/tests/websockets/handlers/echo_close_data_wsh.py20
-rw-r--r--test/wpt/tests/websockets/handlers/echo_exit_wsh.py19
-rw-r--r--test/wpt/tests/websockets/handlers/echo_raw_wsh.py16
-rw-r--r--test/wpt/tests/websockets/handlers/echo_wsh.py36
-rw-r--r--test/wpt/tests/websockets/handlers/empty-message_wsh.py13
-rw-r--r--test/wpt/tests/websockets/handlers/handshake_no_extensions_wsh.py9
-rw-r--r--test/wpt/tests/websockets/handlers/handshake_no_protocol_wsh.py8
-rw-r--r--test/wpt/tests/websockets/handlers/handshake_protocol_wsh.py7
-rw-r--r--test/wpt/tests/websockets/handlers/handshake_sleep_2_wsh.py9
-rw-r--r--test/wpt/tests/websockets/handlers/invalid_wsh.py8
-rw-r--r--test/wpt/tests/websockets/handlers/msg_channel_wsh.py234
-rw-r--r--test/wpt/tests/websockets/handlers/origin_wsh.py11
-rw-r--r--test/wpt/tests/websockets/handlers/protocol_array_wsh.py14
-rw-r--r--test/wpt/tests/websockets/handlers/protocol_wsh.py12
-rw-r--r--test/wpt/tests/websockets/handlers/receive-backpressure_wsh.py14
-rw-r--r--test/wpt/tests/websockets/handlers/receive-many-with-backpressure_wsh.py23
-rw-r--r--test/wpt/tests/websockets/handlers/referrer_wsh.py12
-rw-r--r--test/wpt/tests/websockets/handlers/send-backpressure_wsh.py39
-rw-r--r--test/wpt/tests/websockets/handlers/set-cookie-secure_wsh.py11
-rw-r--r--test/wpt/tests/websockets/handlers/set-cookie_http_wsh.py11
-rw-r--r--test/wpt/tests/websockets/handlers/set-cookie_wsh.py11
-rw-r--r--test/wpt/tests/websockets/handlers/set-cookies-samesite_wsh.py25
-rw-r--r--test/wpt/tests/websockets/handlers/simple_handshake_wsh.py35
-rw-r--r--test/wpt/tests/websockets/handlers/sleep_10_v13_wsh.py24
-rw-r--r--test/wpt/tests/websockets/handlers/stash_responder_blocking_wsh.py45
-rw-r--r--test/wpt/tests/websockets/handlers/stash_responder_wsh.py45
-rw-r--r--test/wpt/tests/websockets/handlers/wrong_accept_key_wsh.py19
-rw-r--r--test/wpt/tests/websockets/idlharness.any.js17
-rw-r--r--test/wpt/tests/websockets/interfaces/CloseEvent/clean-close.html24
-rw-r--r--test/wpt/tests/websockets/interfaces/CloseEvent/constructor.html35
-rw-r--r--test/wpt/tests/websockets/interfaces/CloseEvent/historical.html12
-rw-r--r--test/wpt/tests/websockets/interfaces/WebSocket/bufferedAmount/bufferedAmount-arraybuffer.html27
-rw-r--r--test/wpt/tests/websockets/interfaces/WebSocket/bufferedAmount/bufferedAmount-blob.html28
-rw-r--r--test/wpt/tests/websockets/interfaces/WebSocket/bufferedAmount/bufferedAmount-defineProperty-getter.html18
-rw-r--r--test/wpt/tests/websockets/interfaces/WebSocket/bufferedAmount/bufferedAmount-defineProperty-setter.html20
-rw-r--r--test/wpt/tests/websockets/interfaces/WebSocket/bufferedAmount/bufferedAmount-deleting.html23
-rw-r--r--test/wpt/tests/websockets/interfaces/WebSocket/bufferedAmount/bufferedAmount-getting.html54
-rw-r--r--test/wpt/tests/websockets/interfaces/WebSocket/bufferedAmount/bufferedAmount-initial.html15
-rw-r--r--test/wpt/tests/websockets/interfaces/WebSocket/bufferedAmount/bufferedAmount-large.html29
-rw-r--r--test/wpt/tests/websockets/interfaces/WebSocket/bufferedAmount/bufferedAmount-readonly.html16
-rw-r--r--test/wpt/tests/websockets/interfaces/WebSocket/bufferedAmount/bufferedAmount-unicode.html25
-rw-r--r--test/wpt/tests/websockets/interfaces/WebSocket/close/close-basic.html26
-rw-r--r--test/wpt/tests/websockets/interfaces/WebSocket/close/close-connecting.html25
-rw-r--r--test/wpt/tests/websockets/interfaces/WebSocket/close/close-multiple.html29
-rw-r--r--test/wpt/tests/websockets/interfaces/WebSocket/close/close-nested.html28
-rw-r--r--test/wpt/tests/websockets/interfaces/WebSocket/close/close-replace.html15
-rw-r--r--test/wpt/tests/websockets/interfaces/WebSocket/close/close-return.html14
-rw-r--r--test/wpt/tests/websockets/interfaces/WebSocket/constants/001.html17
-rw-r--r--test/wpt/tests/websockets/interfaces/WebSocket/constants/002.html24
-rw-r--r--test/wpt/tests/websockets/interfaces/WebSocket/constants/003.html22
-rw-r--r--test/wpt/tests/websockets/interfaces/WebSocket/constants/004.html21
-rw-r--r--test/wpt/tests/websockets/interfaces/WebSocket/constants/005.html20
-rw-r--r--test/wpt/tests/websockets/interfaces/WebSocket/constants/006.html20
-rw-r--r--test/wpt/tests/websockets/interfaces/WebSocket/events/001.html18
-rw-r--r--test/wpt/tests/websockets/interfaces/WebSocket/events/002.html20
-rw-r--r--test/wpt/tests/websockets/interfaces/WebSocket/events/003.html21
-rw-r--r--test/wpt/tests/websockets/interfaces/WebSocket/events/004.html16
-rw-r--r--test/wpt/tests/websockets/interfaces/WebSocket/events/006.html17
-rw-r--r--test/wpt/tests/websockets/interfaces/WebSocket/events/007.html22
-rw-r--r--test/wpt/tests/websockets/interfaces/WebSocket/events/008.html24
-rw-r--r--test/wpt/tests/websockets/interfaces/WebSocket/events/009.html21
-rw-r--r--test/wpt/tests/websockets/interfaces/WebSocket/events/010.html21
-rw-r--r--test/wpt/tests/websockets/interfaces/WebSocket/events/011.html18
-rw-r--r--test/wpt/tests/websockets/interfaces/WebSocket/events/012.html18
-rw-r--r--test/wpt/tests/websockets/interfaces/WebSocket/events/013.html20
-rw-r--r--test/wpt/tests/websockets/interfaces/WebSocket/events/014.html21
-rw-r--r--test/wpt/tests/websockets/interfaces/WebSocket/events/015.html36
-rw-r--r--test/wpt/tests/websockets/interfaces/WebSocket/events/016.html39
-rw-r--r--test/wpt/tests/websockets/interfaces/WebSocket/events/017.html56
-rw-r--r--test/wpt/tests/websockets/interfaces/WebSocket/events/018.html52
-rw-r--r--test/wpt/tests/websockets/interfaces/WebSocket/events/019.html31
-rw-r--r--test/wpt/tests/websockets/interfaces/WebSocket/events/020.html17
-rw-r--r--test/wpt/tests/websockets/interfaces/WebSocket/extensions/001.html14
-rw-r--r--test/wpt/tests/websockets/interfaces/WebSocket/protocol/protocol-initial.html14
-rw-r--r--test/wpt/tests/websockets/interfaces/WebSocket/readyState/001.html13
-rw-r--r--test/wpt/tests/websockets/interfaces/WebSocket/readyState/002.html15
-rw-r--r--test/wpt/tests/websockets/interfaces/WebSocket/readyState/003.html18
-rw-r--r--test/wpt/tests/websockets/interfaces/WebSocket/readyState/004.html17
-rw-r--r--test/wpt/tests/websockets/interfaces/WebSocket/readyState/005.html19
-rw-r--r--test/wpt/tests/websockets/interfaces/WebSocket/readyState/006.html19
-rw-r--r--test/wpt/tests/websockets/interfaces/WebSocket/readyState/007.html19
-rw-r--r--test/wpt/tests/websockets/interfaces/WebSocket/readyState/008.html21
-rw-r--r--test/wpt/tests/websockets/interfaces/WebSocket/send/001.html15
-rw-r--r--test/wpt/tests/websockets/interfaces/WebSocket/send/002.html15
-rw-r--r--test/wpt/tests/websockets/interfaces/WebSocket/send/003.html15
-rw-r--r--test/wpt/tests/websockets/interfaces/WebSocket/send/004.html25
-rw-r--r--test/wpt/tests/websockets/interfaces/WebSocket/send/005.html19
-rw-r--r--test/wpt/tests/websockets/interfaces/WebSocket/send/006.html28
-rw-r--r--test/wpt/tests/websockets/interfaces/WebSocket/send/007.html27
-rw-r--r--test/wpt/tests/websockets/interfaces/WebSocket/send/008.html25
-rw-r--r--test/wpt/tests/websockets/interfaces/WebSocket/send/009.html27
-rw-r--r--test/wpt/tests/websockets/interfaces/WebSocket/send/010.html42
-rw-r--r--test/wpt/tests/websockets/interfaces/WebSocket/send/011.html28
-rw-r--r--test/wpt/tests/websockets/interfaces/WebSocket/send/012.html28
-rw-r--r--test/wpt/tests/websockets/interfaces/WebSocket/url/001.html13
-rw-r--r--test/wpt/tests/websockets/interfaces/WebSocket/url/002.html15
-rw-r--r--test/wpt/tests/websockets/interfaces/WebSocket/url/003.html17
-rw-r--r--test/wpt/tests/websockets/interfaces/WebSocket/url/004.html17
-rw-r--r--test/wpt/tests/websockets/interfaces/WebSocket/url/005.html17
-rw-r--r--test/wpt/tests/websockets/interfaces/WebSocket/url/006.html19
-rw-r--r--test/wpt/tests/websockets/interfaces/WebSocket/url/resolve.html14
-rw-r--r--test/wpt/tests/websockets/keeping-connection-open/001.html29
-rw-r--r--test/wpt/tests/websockets/mixed-content.https.any.js7
-rw-r--r--test/wpt/tests/websockets/multi-globals/message-received.html33
-rw-r--r--test/wpt/tests/websockets/multi-globals/support/incumbent.sub.html24
-rw-r--r--test/wpt/tests/websockets/multi-globals/support/relevant.html2
-rw-r--r--test/wpt/tests/websockets/multi-globals/url-parsing/current/current.html2
-rw-r--r--test/wpt/tests/websockets/multi-globals/url-parsing/incumbent/incumbent.html13
-rw-r--r--test/wpt/tests/websockets/multi-globals/url-parsing/url-parsing.html22
-rw-r--r--test/wpt/tests/websockets/opening-handshake/001.html20
-rw-r--r--test/wpt/tests/websockets/opening-handshake/002.html24
-rw-r--r--test/wpt/tests/websockets/opening-handshake/003-sets-origin.worker.js17
-rw-r--r--test/wpt/tests/websockets/opening-handshake/003.html27
-rw-r--r--test/wpt/tests/websockets/opening-handshake/005.html25
-rw-r--r--test/wpt/tests/websockets/referrer.any.js13
-rw-r--r--test/wpt/tests/websockets/remove-own-iframe-during-onerror.window.js23
-rw-r--r--test/wpt/tests/websockets/resources/websockets-test-helpers.sub.js25
-rw-r--r--test/wpt/tests/websockets/security/001.html16
-rw-r--r--test/wpt/tests/websockets/security/002.html20
-rw-r--r--test/wpt/tests/websockets/security/check.py2
-rw-r--r--test/wpt/tests/websockets/send-many-64K-messages-with-backpressure.any.js49
-rw-r--r--test/wpt/tests/websockets/stream/tentative/README.md9
-rw-r--r--test/wpt/tests/websockets/stream/tentative/abort.any.js50
-rw-r--r--test/wpt/tests/websockets/stream/tentative/backpressure-receive.any.js40
-rw-r--r--test/wpt/tests/websockets/stream/tentative/backpressure-send.any.js25
-rw-r--r--test/wpt/tests/websockets/stream/tentative/close.any.js187
-rw-r--r--test/wpt/tests/websockets/stream/tentative/constructor.any.js67
-rw-r--r--test/wpt/tests/websockets/stream/tentative/resources/url-constants.js8
-rw-r--r--test/wpt/tests/websockets/unload-a-document/001-1.html25
-rw-r--r--test/wpt/tests/websockets/unload-a-document/001-2.html4
-rw-r--r--test/wpt/tests/websockets/unload-a-document/001.html26
-rw-r--r--test/wpt/tests/websockets/unload-a-document/002-1.html32
-rw-r--r--test/wpt/tests/websockets/unload-a-document/002-2.html4
-rw-r--r--test/wpt/tests/websockets/unload-a-document/002.html27
-rw-r--r--test/wpt/tests/websockets/unload-a-document/003.html14
-rw-r--r--test/wpt/tests/websockets/unload-a-document/004.html16
-rw-r--r--test/wpt/tests/websockets/unload-a-document/005-1.html22
-rw-r--r--test/wpt/tests/websockets/unload-a-document/005.html21
-rw-r--r--test/wpt/tests/wpt10
-rw-r--r--test/wpt/tests/wpt.py7
-rw-r--r--test/wpt/tests/xhr/META.yml7
-rw-r--r--test/wpt/tests/xhr/README.md7
-rw-r--r--test/wpt/tests/xhr/XMLHttpRequest-withCredentials.any.js40
-rw-r--r--test/wpt/tests/xhr/abort-after-receive.any.js30
-rw-r--r--test/wpt/tests/xhr/abort-after-send.any.js29
-rw-r--r--test/wpt/tests/xhr/abort-after-stop.window.js22
-rw-r--r--test/wpt/tests/xhr/abort-after-timeout.any.js43
-rw-r--r--test/wpt/tests/xhr/abort-during-done.window.js78
-rw-r--r--test/wpt/tests/xhr/abort-during-headers-received.window.js41
-rw-r--r--test/wpt/tests/xhr/abort-during-loading.window.js41
-rw-r--r--test/wpt/tests/xhr/abort-during-open.any.js18
-rw-r--r--test/wpt/tests/xhr/abort-during-readystatechange.any.js19
-rw-r--r--test/wpt/tests/xhr/abort-during-unsent.any.js19
-rw-r--r--test/wpt/tests/xhr/abort-during-upload.any.js17
-rw-r--r--test/wpt/tests/xhr/abort-event-abort.any.js32
-rw-r--r--test/wpt/tests/xhr/abort-event-listeners.any.js13
-rw-r--r--test/wpt/tests/xhr/abort-event-loadend.any.js30
-rw-r--r--test/wpt/tests/xhr/abort-event-order.htm52
-rw-r--r--test/wpt/tests/xhr/abort-upload-event-abort.any.js31
-rw-r--r--test/wpt/tests/xhr/abort-upload-event-loadend.any.js31
-rw-r--r--test/wpt/tests/xhr/access-control-and-redirects-async-same-origin.any.js61
-rw-r--r--test/wpt/tests/xhr/access-control-and-redirects-async.any.js79
-rw-r--r--test/wpt/tests/xhr/access-control-and-redirects.any.js50
-rw-r--r--test/wpt/tests/xhr/access-control-basic-allow-access-control-origin-header-data-url.htm43
-rw-r--r--test/wpt/tests/xhr/access-control-basic-allow-access-control-origin-header.any.js13
-rw-r--r--test/wpt/tests/xhr/access-control-basic-allow-async.any.js19
-rw-r--r--test/wpt/tests/xhr/access-control-basic-allow-non-cors-safelisted-method-async.any.js17
-rw-r--r--test/wpt/tests/xhr/access-control-basic-allow-non-cors-safelisted-method.any.js14
-rw-r--r--test/wpt/tests/xhr/access-control-basic-allow-preflight-cache-invalidation-by-header.any.js38
-rw-r--r--test/wpt/tests/xhr/access-control-basic-allow-preflight-cache-invalidation-by-method.any.js37
-rw-r--r--test/wpt/tests/xhr/access-control-basic-allow-preflight-cache-timeout.any.js37
-rw-r--r--test/wpt/tests/xhr/access-control-basic-allow-preflight-cache.any.js35
-rw-r--r--test/wpt/tests/xhr/access-control-basic-allow-star.any.js12
-rw-r--r--test/wpt/tests/xhr/access-control-basic-allow.any.js12
-rw-r--r--test/wpt/tests/xhr/access-control-basic-cors-safelisted-request-headers.htm31
-rw-r--r--test/wpt/tests/xhr/access-control-basic-cors-safelisted-response-headers.htm32
-rw-r--r--test/wpt/tests/xhr/access-control-basic-denied.htm30
-rw-r--r--test/wpt/tests/xhr/access-control-basic-get-fail-non-simple.htm26
-rw-r--r--test/wpt/tests/xhr/access-control-basic-non-cors-safelisted-content-type.htm30
-rw-r--r--test/wpt/tests/xhr/access-control-basic-post-success-no-content-type.htm26
-rw-r--r--test/wpt/tests/xhr/access-control-basic-post-with-non-cors-safelisted-content-type.htm37
-rw-r--r--test/wpt/tests/xhr/access-control-basic-preflight-denied.htm31
-rw-r--r--test/wpt/tests/xhr/access-control-expose-headers-on-redirect.html33
-rw-r--r--test/wpt/tests/xhr/access-control-preflight-async-header-denied.htm39
-rw-r--r--test/wpt/tests/xhr/access-control-preflight-async-method-denied.htm38
-rw-r--r--test/wpt/tests/xhr/access-control-preflight-async-not-supported.htm37
-rw-r--r--test/wpt/tests/xhr/access-control-preflight-credential-async.htm29
-rw-r--r--test/wpt/tests/xhr/access-control-preflight-credential-sync.htm24
-rw-r--r--test/wpt/tests/xhr/access-control-preflight-headers-async.htm35
-rw-r--r--test/wpt/tests/xhr/access-control-preflight-headers-sync.htm29
-rw-r--r--test/wpt/tests/xhr/access-control-preflight-request-allow-headers-returns-star.any.js26
-rw-r--r--test/wpt/tests/xhr/access-control-preflight-request-header-lowercase.htm29
-rw-r--r--test/wpt/tests/xhr/access-control-preflight-request-header-returns-origin.any.js26
-rw-r--r--test/wpt/tests/xhr/access-control-preflight-request-header-sorted.htm28
-rw-r--r--test/wpt/tests/xhr/access-control-preflight-request-headers-origin.htm29
-rw-r--r--test/wpt/tests/xhr/access-control-preflight-request-invalid-status-301.htm28
-rw-r--r--test/wpt/tests/xhr/access-control-preflight-request-invalid-status-400.htm28
-rw-r--r--test/wpt/tests/xhr/access-control-preflight-request-invalid-status-501.htm28
-rw-r--r--test/wpt/tests/xhr/access-control-preflight-request-must-not-contain-cookie.htm57
-rw-r--r--test/wpt/tests/xhr/access-control-preflight-sync-header-denied.htm34
-rw-r--r--test/wpt/tests/xhr/access-control-preflight-sync-method-denied.htm33
-rw-r--r--test/wpt/tests/xhr/access-control-preflight-sync-not-supported.htm33
-rw-r--r--test/wpt/tests/xhr/access-control-recursive-failed-request.htm38
-rw-r--r--test/wpt/tests/xhr/access-control-response-with-body-sync.htm25
-rw-r--r--test/wpt/tests/xhr/access-control-response-with-body.htm29
-rw-r--r--test/wpt/tests/xhr/access-control-response-with-exposed-headers.htm38
-rw-r--r--test/wpt/tests/xhr/access-control-sandboxed-iframe-allow-origin-null.htm32
-rw-r--r--test/wpt/tests/xhr/access-control-sandboxed-iframe-allow.htm32
-rw-r--r--test/wpt/tests/xhr/access-control-sandboxed-iframe-denied-without-wildcard.htm43
-rw-r--r--test/wpt/tests/xhr/access-control-sandboxed-iframe-denied.htm41
-rw-r--r--test/wpt/tests/xhr/allow-lists-starting-with-comma.htm33
-rw-r--r--test/wpt/tests/xhr/anonymous-mode-unsupported.htm40
-rw-r--r--test/wpt/tests/xhr/blob-range.any.js246
-rw-r--r--test/wpt/tests/xhr/close-worker-with-xhr-in-progress.html26
-rw-r--r--test/wpt/tests/xhr/content-type-unmodified.any.js16
-rw-r--r--test/wpt/tests/xhr/cookies.http.html41
-rw-r--r--test/wpt/tests/xhr/cors-expose-star.sub.any.js52
-rw-r--r--test/wpt/tests/xhr/cors-upload.any.js59
-rw-r--r--test/wpt/tests/xhr/data-uri.htm41
-rw-r--r--test/wpt/tests/xhr/event-abort.any.js15
-rw-r--r--test/wpt/tests/xhr/event-error-order.sub.html35
-rw-r--r--test/wpt/tests/xhr/event-error.sub.any.js28
-rw-r--r--test/wpt/tests/xhr/event-load.any.js21
-rw-r--r--test/wpt/tests/xhr/event-loadend.any.js19
-rw-r--r--test/wpt/tests/xhr/event-loadstart-upload.any.js19
-rw-r--r--test/wpt/tests/xhr/event-loadstart.any.js17
-rw-r--r--test/wpt/tests/xhr/event-progress.any.js18
-rw-r--r--test/wpt/tests/xhr/event-readystate-sync-open.any.js23
-rw-r--r--test/wpt/tests/xhr/event-readystatechange-loaded.any.js23
-rw-r--r--test/wpt/tests/xhr/event-timeout-order.any.js21
-rw-r--r--test/wpt/tests/xhr/event-timeout.any.js18
-rw-r--r--test/wpt/tests/xhr/event-upload-progress-crossorigin.any.js26
-rw-r--r--test/wpt/tests/xhr/event-upload-progress.any.js30
-rw-r--r--test/wpt/tests/xhr/firing-events-http-content-length.html32
-rw-r--r--test/wpt/tests/xhr/firing-events-http-no-content-length.html35
-rw-r--r--test/wpt/tests/xhr/folder.txt1
-rw-r--r--test/wpt/tests/xhr/formdata.html90
-rw-r--r--test/wpt/tests/xhr/formdata/append-formelement.html52
-rw-r--r--test/wpt/tests/xhr/formdata/append.any.js37
-rw-r--r--test/wpt/tests/xhr/formdata/constructor-formelement.html150
-rw-r--r--test/wpt/tests/xhr/formdata/constructor-submitter.html100
-rw-r--r--test/wpt/tests/xhr/formdata/constructor.any.js6
-rw-r--r--test/wpt/tests/xhr/formdata/delete-formelement.html41
-rw-r--r--test/wpt/tests/xhr/formdata/delete.any.js26
-rw-r--r--test/wpt/tests/xhr/formdata/foreach.any.js56
-rw-r--r--test/wpt/tests/xhr/formdata/get-formelement.html34
-rw-r--r--test/wpt/tests/xhr/formdata/get.any.js28
-rw-r--r--test/wpt/tests/xhr/formdata/has-formelement.html25
-rw-r--r--test/wpt/tests/xhr/formdata/has.any.js19
-rw-r--r--test/wpt/tests/xhr/formdata/iteration.any.js65
-rw-r--r--test/wpt/tests/xhr/formdata/set-blob.any.js61
-rw-r--r--test/wpt/tests/xhr/formdata/set-formelement.html51
-rw-r--r--test/wpt/tests/xhr/formdata/set.any.js36
-rw-r--r--test/wpt/tests/xhr/getallresponseheaders-cookies.htm38
-rw-r--r--test/wpt/tests/xhr/getallresponseheaders-status.htm33
-rw-r--r--test/wpt/tests/xhr/getallresponseheaders.htm35
-rw-r--r--test/wpt/tests/xhr/getresponseheader-case-insensitive.htm34
-rw-r--r--test/wpt/tests/xhr/getresponseheader-chunked-trailer.htm32
-rw-r--r--test/wpt/tests/xhr/getresponseheader-cookies-and-more.htm36
-rw-r--r--test/wpt/tests/xhr/getresponseheader-error-state.htm36
-rw-r--r--test/wpt/tests/xhr/getresponseheader-server-date.htm29
-rw-r--r--test/wpt/tests/xhr/getresponseheader-special-characters.htm34
-rw-r--r--test/wpt/tests/xhr/getresponseheader-unsent-opened-state.htm32
-rw-r--r--test/wpt/tests/xhr/getresponseheader.any.js18
-rw-r--r--test/wpt/tests/xhr/header-user-agent-async.htm26
-rw-r--r--test/wpt/tests/xhr/header-user-agent-sync.htm20
-rw-r--r--test/wpt/tests/xhr/headers-normalize-response.htm43
-rw-r--r--test/wpt/tests/xhr/historical.html15
-rw-r--r--test/wpt/tests/xhr/idlharness.any.js28
-rw-r--r--test/wpt/tests/xhr/json.any.js23
-rw-r--r--test/wpt/tests/xhr/loadstart-and-state.html40
-rw-r--r--test/wpt/tests/xhr/open-after-abort.htm77
-rw-r--r--test/wpt/tests/xhr/open-after-setrequestheader.htm33
-rw-r--r--test/wpt/tests/xhr/open-after-stop.window.js43
-rw-r--r--test/wpt/tests/xhr/open-during-abort-event.htm56
-rw-r--r--test/wpt/tests/xhr/open-during-abort-processing.htm62
-rw-r--r--test/wpt/tests/xhr/open-during-abort.htm33
-rw-r--r--test/wpt/tests/xhr/open-method-bogus.htm28
-rw-r--r--test/wpt/tests/xhr/open-method-case-insensitive.htm29
-rw-r--r--test/wpt/tests/xhr/open-method-case-sensitive.htm31
-rw-r--r--test/wpt/tests/xhr/open-method-insecure.htm29
-rw-r--r--test/wpt/tests/xhr/open-method-responsetype-set-sync.htm32
-rw-r--r--test/wpt/tests/xhr/open-open-send.htm33
-rw-r--r--test/wpt/tests/xhr/open-open-sync-send.htm31
-rw-r--r--test/wpt/tests/xhr/open-parameters-toString.htm54
-rw-r--r--test/wpt/tests/xhr/open-referer.htm20
-rw-r--r--test/wpt/tests/xhr/open-send-during-abort.htm27
-rw-r--r--test/wpt/tests/xhr/open-send-open.htm33
-rw-r--r--test/wpt/tests/xhr/open-sync-open-send.htm41
-rw-r--r--test/wpt/tests/xhr/open-url-about-blank-window.htm23
-rw-r--r--test/wpt/tests/xhr/open-url-base-inserted-after-open.htm24
-rw-r--r--test/wpt/tests/xhr/open-url-base-inserted.htm24
-rw-r--r--test/wpt/tests/xhr/open-url-base.htm22
-rw-r--r--test/wpt/tests/xhr/open-url-encoding.htm26
-rw-r--r--test/wpt/tests/xhr/open-url-fragment.htm38
-rw-r--r--test/wpt/tests/xhr/open-url-javascript-window-2.htm19
-rw-r--r--test/wpt/tests/xhr/open-url-javascript-window.htm28
-rw-r--r--test/wpt/tests/xhr/open-url-multi-window-2.htm25
-rw-r--r--test/wpt/tests/xhr/open-url-multi-window-3.htm25
-rw-r--r--test/wpt/tests/xhr/open-url-multi-window-4.htm50
-rw-r--r--test/wpt/tests/xhr/open-url-multi-window-5.htm32
-rw-r--r--test/wpt/tests/xhr/open-url-multi-window-6.htm41
-rw-r--r--test/wpt/tests/xhr/open-url-multi-window.htm31
-rw-r--r--test/wpt/tests/xhr/open-url-redirected-sharedworker-origin.htm11
-rw-r--r--test/wpt/tests/xhr/open-url-redirected-worker-origin.htm11
-rw-r--r--test/wpt/tests/xhr/open-url-worker-origin.htm9
-rw-r--r--test/wpt/tests/xhr/open-url-worker-simple.htm25
-rw-r--r--test/wpt/tests/xhr/open-user-password-non-same-origin.htm25
-rw-r--r--test/wpt/tests/xhr/over-1-meg.any.js16
-rw-r--r--test/wpt/tests/xhr/overridemimetype-blob.html57
-rw-r--r--test/wpt/tests/xhr/overridemimetype-done-state.any.js20
-rw-r--r--test/wpt/tests/xhr/overridemimetype-edge-cases.window.js50
-rw-r--r--test/wpt/tests/xhr/overridemimetype-headers-received-state-force-shiftjis.htm34
-rw-r--r--test/wpt/tests/xhr/overridemimetype-invalid-mime-type.htm41
-rw-r--r--test/wpt/tests/xhr/overridemimetype-loading-state.htm32
-rw-r--r--test/wpt/tests/xhr/overridemimetype-open-state-force-utf-8.htm27
-rw-r--r--test/wpt/tests/xhr/overridemimetype-open-state-force-xml.htm34
-rw-r--r--test/wpt/tests/xhr/overridemimetype-unsent-state-force-shiftjis.any.js12
-rw-r--r--test/wpt/tests/xhr/preserve-ua-header-on-redirect.htm43
-rw-r--r--test/wpt/tests/xhr/progress-events-response-data-gzip.htm83
-rw-r--r--test/wpt/tests/xhr/progressevent-constructor.html47
-rw-r--r--test/wpt/tests/xhr/progressevent-interface.html49
-rw-r--r--test/wpt/tests/xhr/request-content-length.any.js31
-rw-r--r--test/wpt/tests/xhr/resources/accept-language.py3
-rw-r--r--test/wpt/tests/xhr/resources/accept.py2
-rw-r--r--test/wpt/tests/xhr/resources/access-control-allow-lists.py26
-rw-r--r--test/wpt/tests/xhr/resources/access-control-allow-with-body.py15
-rw-r--r--test/wpt/tests/xhr/resources/access-control-auth-basic.py17
-rw-r--r--test/wpt/tests/xhr/resources/access-control-basic-allow-no-credentials.py5
-rw-r--r--test/wpt/tests/xhr/resources/access-control-basic-allow-star.py5
-rw-r--r--test/wpt/tests/xhr/resources/access-control-basic-allow.py6
-rw-r--r--test/wpt/tests/xhr/resources/access-control-basic-cors-safelisted-request-headers.py16
-rw-r--r--test/wpt/tests/xhr/resources/access-control-basic-cors-safelisted-response-headers.py19
-rw-r--r--test/wpt/tests/xhr/resources/access-control-basic-denied.py5
-rw-r--r--test/wpt/tests/xhr/resources/access-control-basic-options-not-supported.py12
-rw-r--r--test/wpt/tests/xhr/resources/access-control-basic-preflight-cache-invalidation.py49
-rw-r--r--test/wpt/tests/xhr/resources/access-control-basic-preflight-cache-timeout.py50
-rw-r--r--test/wpt/tests/xhr/resources/access-control-basic-preflight-cache.py50
-rw-r--r--test/wpt/tests/xhr/resources/access-control-basic-put-allow.py22
-rw-r--r--test/wpt/tests/xhr/resources/access-control-cookie.py16
-rw-r--r--test/wpt/tests/xhr/resources/access-control-origin-header.py8
-rw-r--r--test/wpt/tests/xhr/resources/access-control-preflight-denied.py49
-rw-r--r--test/wpt/tests/xhr/resources/access-control-preflight-request-allow-headers-returns-star.py12
-rw-r--r--test/wpt/tests/xhr/resources/access-control-preflight-request-header-lowercase.py16
-rw-r--r--test/wpt/tests/xhr/resources/access-control-preflight-request-header-returns-origin.py12
-rw-r--r--test/wpt/tests/xhr/resources/access-control-preflight-request-header-sorted.py18
-rw-r--r--test/wpt/tests/xhr/resources/access-control-preflight-request-headers-origin.py12
-rw-r--r--test/wpt/tests/xhr/resources/access-control-preflight-request-invalid-status.py16
-rw-r--r--test/wpt/tests/xhr/resources/access-control-preflight-request-must-not-contain-cookie.py12
-rw-r--r--test/wpt/tests/xhr/resources/access-control-sandboxed-iframe.html24
-rw-r--r--test/wpt/tests/xhr/resources/auth1/auth.py12
-rw-r--r--test/wpt/tests/xhr/resources/auth10/auth.py12
-rw-r--r--test/wpt/tests/xhr/resources/auth11/auth.py12
-rw-r--r--test/wpt/tests/xhr/resources/auth2/auth.py12
-rw-r--r--test/wpt/tests/xhr/resources/auth2/corsenabled.py18
-rw-r--r--test/wpt/tests/xhr/resources/auth3/auth.py12
-rw-r--r--test/wpt/tests/xhr/resources/auth4/auth.py12
-rw-r--r--test/wpt/tests/xhr/resources/auth5/auth.py15
-rw-r--r--test/wpt/tests/xhr/resources/auth6/auth.py15
-rw-r--r--test/wpt/tests/xhr/resources/auth7/corsenabled.py20
-rw-r--r--test/wpt/tests/xhr/resources/auth8/corsenabled-no-authorize.py20
-rw-r--r--test/wpt/tests/xhr/resources/auth9/auth.py12
-rw-r--r--test/wpt/tests/xhr/resources/authentication.py24
-rw-r--r--test/wpt/tests/xhr/resources/bad-chunk-encoding.py17
-rw-r--r--test/wpt/tests/xhr/resources/base.xml1
-rw-r--r--test/wpt/tests/xhr/resources/chunked.py17
-rw-r--r--test/wpt/tests/xhr/resources/conditional.py29
-rw-r--r--test/wpt/tests/xhr/resources/content.py20
-rw-r--r--test/wpt/tests/xhr/resources/corsenabled.py25
-rw-r--r--test/wpt/tests/xhr/resources/delay.py7
-rw-r--r--test/wpt/tests/xhr/resources/echo-content-cors.py23
-rw-r--r--test/wpt/tests/xhr/resources/echo-content-type.py6
-rw-r--r--test/wpt/tests/xhr/resources/echo-headers.py7
-rw-r--r--test/wpt/tests/xhr/resources/echo-method.py16
-rw-r--r--test/wpt/tests/xhr/resources/empty-div-utf8-html.py5
-rw-r--r--test/wpt/tests/xhr/resources/folder.txt1
-rw-r--r--test/wpt/tests/xhr/resources/form.py2
-rw-r--r--test/wpt/tests/xhr/resources/get-set-cookie.py18
-rw-r--r--test/wpt/tests/xhr/resources/gzip.py24
-rw-r--r--test/wpt/tests/xhr/resources/header-content-length-twice.asis3
-rw-r--r--test/wpt/tests/xhr/resources/header-content-length.asis2
-rw-r--r--test/wpt/tests/xhr/resources/header-user-agent.py15
-rw-r--r--test/wpt/tests/xhr/resources/headers-basic.asis4
-rw-r--r--test/wpt/tests/xhr/resources/headers-double-empty.asis3
-rw-r--r--test/wpt/tests/xhr/resources/headers-some-are-empty.asis7
-rw-r--r--test/wpt/tests/xhr/resources/headers-www-authenticate.asis4
-rw-r--r--test/wpt/tests/xhr/resources/headers.asis6
-rw-r--r--test/wpt/tests/xhr/resources/headers.py12
-rw-r--r--test/wpt/tests/xhr/resources/image.gifbin0 -> 167145 bytes
-rw-r--r--test/wpt/tests/xhr/resources/img-utf8-html.py5
-rw-r--r--test/wpt/tests/xhr/resources/img.jpgbin0 -> 108761 bytes
-rw-r--r--test/wpt/tests/xhr/resources/infinite-redirects.py24
-rw-r--r--test/wpt/tests/xhr/resources/init.htm20
-rw-r--r--test/wpt/tests/xhr/resources/inspect-headers.py36
-rw-r--r--test/wpt/tests/xhr/resources/invalid-utf8-html.py5
-rw-r--r--test/wpt/tests/xhr/resources/last-modified.py9
-rw-r--r--test/wpt/tests/xhr/resources/no-custom-header-on-preflight.py27
-rw-r--r--test/wpt/tests/xhr/resources/nocors/folder.txt1
-rw-r--r--test/wpt/tests/xhr/resources/over-1-meg.txt1
-rw-r--r--test/wpt/tests/xhr/resources/parse-headers.py6
-rw-r--r--test/wpt/tests/xhr/resources/pass.txt1
-rw-r--r--test/wpt/tests/xhr/resources/redirect-cors.py20
-rw-r--r--test/wpt/tests/xhr/resources/redirect.py16
-rw-r--r--test/wpt/tests/xhr/resources/requri.py5
-rw-r--r--test/wpt/tests/xhr/resources/reset-token.py5
-rw-r--r--test/wpt/tests/xhr/resources/responseType-document-in-worker.js9
-rw-r--r--test/wpt/tests/xhr/resources/responseXML-unavailable-in-worker.js9
-rw-r--r--test/wpt/tests/xhr/resources/send-after-setting-document-domain-window-1.htm23
-rw-r--r--test/wpt/tests/xhr/resources/send-after-setting-document-domain-window-2.htm20
-rw-r--r--test/wpt/tests/xhr/resources/send-after-setting-document-domain-window-helper.js32
-rw-r--r--test/wpt/tests/xhr/resources/shift-jis-html.py6
-rw-r--r--test/wpt/tests/xhr/resources/status.py11
-rw-r--r--test/wpt/tests/xhr/resources/top.txt1
-rw-r--r--test/wpt/tests/xhr/resources/trickle.py15
-rw-r--r--test/wpt/tests/xhr/resources/upload.py17
-rw-r--r--test/wpt/tests/xhr/resources/utf16-bom.jsonbin0 -> 30 bytes
-rw-r--r--test/wpt/tests/xhr/resources/utf16.txtbin0 -> 18 bytes
-rw-r--r--test/wpt/tests/xhr/resources/well-formed.xml4
-rw-r--r--test/wpt/tests/xhr/resources/win-1252-html.py5
-rw-r--r--test/wpt/tests/xhr/resources/win-1252-xml.py5
-rw-r--r--test/wpt/tests/xhr/resources/workerxhr-origin-referrer.js63
-rw-r--r--test/wpt/tests/xhr/resources/workerxhr-simple.js9
-rw-r--r--test/wpt/tests/xhr/resources/xmlhttprequest-event-order.js83
-rw-r--r--test/wpt/tests/xhr/resources/xmlhttprequest-timeout-aborted.js15
-rw-r--r--test/wpt/tests/xhr/resources/xmlhttprequest-timeout-abortedonmain.js8
-rw-r--r--test/wpt/tests/xhr/resources/xmlhttprequest-timeout-overrides.js12
-rw-r--r--test/wpt/tests/xhr/resources/xmlhttprequest-timeout-overridesexpires.js12
-rw-r--r--test/wpt/tests/xhr/resources/xmlhttprequest-timeout-runner.js21
-rw-r--r--test/wpt/tests/xhr/resources/xmlhttprequest-timeout-simple.js6
-rw-r--r--test/wpt/tests/xhr/resources/xmlhttprequest-timeout-synconmain.js2
-rw-r--r--test/wpt/tests/xhr/resources/xmlhttprequest-timeout-synconworker.js11
-rw-r--r--test/wpt/tests/xhr/resources/xmlhttprequest-timeout-twice.js6
-rw-r--r--test/wpt/tests/xhr/resources/xmlhttprequest-timeout.js333
-rw-r--r--test/wpt/tests/xhr/resources/zlib.py19
-rw-r--r--test/wpt/tests/xhr/response-body-errors.any.js23
-rw-r--r--test/wpt/tests/xhr/response-data-arraybuffer.htm54
-rw-r--r--test/wpt/tests/xhr/response-data-blob.htm55
-rw-r--r--test/wpt/tests/xhr/response-data-deflate.htm42
-rw-r--r--test/wpt/tests/xhr/response-data-gzip.htm42
-rw-r--r--test/wpt/tests/xhr/response-data-progress.htm52
-rw-r--r--test/wpt/tests/xhr/response-invalid-responsetype.htm38
-rw-r--r--test/wpt/tests/xhr/response-json.htm61
-rw-r--r--test/wpt/tests/xhr/response-method.htm21
-rw-r--r--test/wpt/tests/xhr/responseText-status.html33
-rw-r--r--test/wpt/tests/xhr/responseType-document-in-worker.html13
-rw-r--r--test/wpt/tests/xhr/responseXML-unavailable-in-worker.html13
-rw-r--r--test/wpt/tests/xhr/responsedocument-decoding.htm39
-rw-r--r--test/wpt/tests/xhr/responsetext-decoding.htm93
-rw-r--r--test/wpt/tests/xhr/responsetype.any.js135
-rw-r--r--test/wpt/tests/xhr/responseurl.html37
-rw-r--r--test/wpt/tests/xhr/responsexml-basic.htm33
-rw-r--r--test/wpt/tests/xhr/responsexml-document-properties.htm123
-rw-r--r--test/wpt/tests/xhr/responsexml-get-twice.htm66
-rw-r--r--test/wpt/tests/xhr/responsexml-invalid-type.html21
-rw-r--r--test/wpt/tests/xhr/responsexml-media-type.htm41
-rw-r--r--test/wpt/tests/xhr/responsexml-non-document-types.htm45
-rw-r--r--test/wpt/tests/xhr/responsexml-non-well-formed.htm30
-rw-r--r--test/wpt/tests/xhr/security-consideration.sub.html36
-rw-r--r--test/wpt/tests/xhr/send-accept-language.htm27
-rw-r--r--test/wpt/tests/xhr/send-accept.htm24
-rw-r--r--test/wpt/tests/xhr/send-after-setting-document-domain.htm39
-rw-r--r--test/wpt/tests/xhr/send-authentication-basic-cors-not-enabled.htm29
-rw-r--r--test/wpt/tests/xhr/send-authentication-basic-cors.htm35
-rw-r--r--test/wpt/tests/xhr/send-authentication-basic-repeat-no-args.htm33
-rw-r--r--test/wpt/tests/xhr/send-authentication-basic-setrequestheader-and-arguments.htm36
-rw-r--r--test/wpt/tests/xhr/send-authentication-basic-setrequestheader-existing-session.htm53
-rw-r--r--test/wpt/tests/xhr/send-authentication-basic-setrequestheader.htm36
-rw-r--r--test/wpt/tests/xhr/send-authentication-basic.htm27
-rw-r--r--test/wpt/tests/xhr/send-authentication-competing-names-passwords.htm50
-rw-r--r--test/wpt/tests/xhr/send-authentication-cors-basic-setrequestheader.htm31
-rw-r--r--test/wpt/tests/xhr/send-authentication-cors-setrequestheader-no-cred.htm62
-rw-r--r--test/wpt/tests/xhr/send-authentication-existing-session-manual.htm33
-rw-r--r--test/wpt/tests/xhr/send-authentication-prompt-2-manual.htm25
-rw-r--r--test/wpt/tests/xhr/send-authentication-prompt-manual.htm25
-rw-r--r--test/wpt/tests/xhr/send-blob-with-no-mime-type.html61
-rw-r--r--test/wpt/tests/xhr/send-conditional-cors.htm42
-rw-r--r--test/wpt/tests/xhr/send-conditional.htm34
-rw-r--r--test/wpt/tests/xhr/send-content-type-charset.htm115
-rw-r--r--test/wpt/tests/xhr/send-content-type-string.htm26
-rw-r--r--test/wpt/tests/xhr/send-data-arraybuffer.any.js31
-rw-r--r--test/wpt/tests/xhr/send-data-arraybufferview.any.js18
-rw-r--r--test/wpt/tests/xhr/send-data-blob.htm62
-rw-r--r--test/wpt/tests/xhr/send-data-es-object.any.js58
-rw-r--r--test/wpt/tests/xhr/send-data-formdata.any.js21
-rw-r--r--test/wpt/tests/xhr/send-data-sharedarraybuffer.any.js27
-rw-r--r--test/wpt/tests/xhr/send-data-string-invalid-unicode.any.js46
-rw-r--r--test/wpt/tests/xhr/send-data-unexpected-tostring.htm56
-rw-r--r--test/wpt/tests/xhr/send-entity-body-basic.htm28
-rw-r--r--test/wpt/tests/xhr/send-entity-body-document-bogus.htm26
-rw-r--r--test/wpt/tests/xhr/send-entity-body-document.htm92
-rw-r--r--test/wpt/tests/xhr/send-entity-body-empty.htm26
-rw-r--r--test/wpt/tests/xhr/send-entity-body-get-head-async.htm39
-rw-r--r--test/wpt/tests/xhr/send-entity-body-get-head.htm36
-rw-r--r--test/wpt/tests/xhr/send-entity-body-none.htm40
-rw-r--r--test/wpt/tests/xhr/send-network-error-async-events.sub.htm47
-rw-r--r--test/wpt/tests/xhr/send-network-error-sync-events.sub.htm45
-rw-r--r--test/wpt/tests/xhr/send-no-response-event-loadend.htm48
-rw-r--r--test/wpt/tests/xhr/send-no-response-event-loadstart.htm48
-rw-r--r--test/wpt/tests/xhr/send-no-response-event-order.htm45
-rw-r--r--test/wpt/tests/xhr/send-non-same-origin.htm33
-rw-r--r--test/wpt/tests/xhr/send-receive-utf16.htm37
-rw-r--r--test/wpt/tests/xhr/send-redirect-bogus-sync.htm26
-rw-r--r--test/wpt/tests/xhr/send-redirect-bogus.htm36
-rw-r--r--test/wpt/tests/xhr/send-redirect-infinite-sync.htm24
-rw-r--r--test/wpt/tests/xhr/send-redirect-infinite.htm35
-rw-r--r--test/wpt/tests/xhr/send-redirect-no-location.htm40
-rw-r--r--test/wpt/tests/xhr/send-redirect-post-upload.htm140
-rw-r--r--test/wpt/tests/xhr/send-redirect-to-cors.htm92
-rw-r--r--test/wpt/tests/xhr/send-redirect-to-non-cors.htm37
-rw-r--r--test/wpt/tests/xhr/send-redirect.htm36
-rw-r--r--test/wpt/tests/xhr/send-response-event-order.htm40
-rw-r--r--test/wpt/tests/xhr/send-response-upload-event-loadend.htm40
-rw-r--r--test/wpt/tests/xhr/send-response-upload-event-loadstart.htm39
-rw-r--r--test/wpt/tests/xhr/send-response-upload-event-progress.htm39
-rw-r--r--test/wpt/tests/xhr/send-send.any.js7
-rw-r--r--test/wpt/tests/xhr/send-sync-blocks-async.htm53
-rw-r--r--test/wpt/tests/xhr/send-sync-no-response-event-load.htm38
-rw-r--r--test/wpt/tests/xhr/send-sync-no-response-event-loadend.htm38
-rw-r--r--test/wpt/tests/xhr/send-sync-no-response-event-order.htm51
-rw-r--r--test/wpt/tests/xhr/send-sync-response-event-order.htm35
-rw-r--r--test/wpt/tests/xhr/send-sync-timeout.htm29
-rw-r--r--test/wpt/tests/xhr/send-timeout-events.htm62
-rw-r--r--test/wpt/tests/xhr/send-usp.any.js46
-rw-r--r--test/wpt/tests/xhr/setrequestheader-after-send.htm27
-rw-r--r--test/wpt/tests/xhr/setrequestheader-allow-empty-value.htm26
-rw-r--r--test/wpt/tests/xhr/setrequestheader-allow-whitespace-in-value.htm27
-rw-r--r--test/wpt/tests/xhr/setrequestheader-before-open.htm18
-rw-r--r--test/wpt/tests/xhr/setrequestheader-bogus-name.htm59
-rw-r--r--test/wpt/tests/xhr/setrequestheader-bogus-value.htm36
-rw-r--r--test/wpt/tests/xhr/setrequestheader-case-insensitive.htm34
-rw-r--r--test/wpt/tests/xhr/setrequestheader-combining.window.js12
-rw-r--r--test/wpt/tests/xhr/setrequestheader-content-type.htm220
-rw-r--r--test/wpt/tests/xhr/setrequestheader-header-allowed.htm34
-rw-r--r--test/wpt/tests/xhr/setrequestheader-header-forbidden.htm95
-rw-r--r--test/wpt/tests/xhr/setrequestheader-open-setrequestheader.htm53
-rw-r--r--test/wpt/tests/xhr/status-async.htm62
-rw-r--r--test/wpt/tests/xhr/status-basic.htm51
-rw-r--r--test/wpt/tests/xhr/status-error.htm87
-rw-r--r--test/wpt/tests/xhr/status.h2.window.js21
-rw-r--r--test/wpt/tests/xhr/sync-no-progress.any.js13
-rw-r--r--test/wpt/tests/xhr/sync-no-timeout.any.js16
-rw-r--r--test/wpt/tests/xhr/sync-xhr-and-window-onload.html25
-rw-r--r--test/wpt/tests/xhr/sync-xhr-supported-by-feature-policy.html11
-rw-r--r--test/wpt/tests/xhr/template-element.html36
-rw-r--r--test/wpt/tests/xhr/thrown-error-in-events.html60
-rw-r--r--test/wpt/tests/xhr/timeout-cors-async.htm43
-rw-r--r--test/wpt/tests/xhr/timeout-multiple-fetches.html32
-rw-r--r--test/wpt/tests/xhr/timeout-sync.htm25
-rw-r--r--test/wpt/tests/xhr/xhr-authorization-redirect.any.js28
-rw-r--r--test/wpt/tests/xhr/xhr-timeout-longtask.any.js14
-rw-r--r--test/wpt/tests/xhr/xmlhttprequest-basic.htm45
-rw-r--r--test/wpt/tests/xhr/xmlhttprequest-eventtarget.htm48
-rw-r--r--test/wpt/tests/xhr/xmlhttprequest-network-error-sync.htm34
-rw-r--r--test/wpt/tests/xhr/xmlhttprequest-network-error.htm39
-rw-r--r--test/wpt/tests/xhr/xmlhttprequest-sync-block-defer-scripts-subframe.html17
-rw-r--r--test/wpt/tests/xhr/xmlhttprequest-sync-block-defer-scripts.html15
-rw-r--r--test/wpt/tests/xhr/xmlhttprequest-sync-block-scripts.html22
-rw-r--r--test/wpt/tests/xhr/xmlhttprequest-sync-default-feature-policy.sub.html32
-rw-r--r--test/wpt/tests/xhr/xmlhttprequest-sync-not-hang-scriptloader-subframe.html17
-rw-r--r--test/wpt/tests/xhr/xmlhttprequest-sync-not-hang-scriptloader.html16
-rw-r--r--test/wpt/tests/xhr/xmlhttprequest-timeout-aborted.html29
-rw-r--r--test/wpt/tests/xhr/xmlhttprequest-timeout-abortedonmain.html25
-rw-r--r--test/wpt/tests/xhr/xmlhttprequest-timeout-overrides.html26
-rw-r--r--test/wpt/tests/xhr/xmlhttprequest-timeout-overridesexpires.html26
-rw-r--r--test/wpt/tests/xhr/xmlhttprequest-timeout-reused.html49
-rw-r--r--test/wpt/tests/xhr/xmlhttprequest-timeout-simple.html27
-rw-r--r--test/wpt/tests/xhr/xmlhttprequest-timeout-synconmain.html23
-rw-r--r--test/wpt/tests/xhr/xmlhttprequest-timeout-twice.html28
-rw-r--r--test/wpt/tests/xhr/xmlhttprequest-timeout-worker-aborted.html31
-rw-r--r--test/wpt/tests/xhr/xmlhttprequest-timeout-worker-overrides.html27
-rw-r--r--test/wpt/tests/xhr/xmlhttprequest-timeout-worker-overridesexpires.html28
-rw-r--r--test/wpt/tests/xhr/xmlhttprequest-timeout-worker-simple.html29
-rw-r--r--test/wpt/tests/xhr/xmlhttprequest-timeout-worker-synconworker.html28
-rw-r--r--test/wpt/tests/xhr/xmlhttprequest-timeout-worker-twice.html29
-rw-r--r--test/wpt/tests/xhr/xmlhttprequest-unsent.htm36
-rw-r--r--types/README.md6
-rw-r--r--types/agent.d.ts31
-rw-r--r--types/api.d.ts43
-rw-r--r--types/balanced-pool.d.ts18
-rw-r--r--types/cache.d.ts36
-rw-r--r--types/client.d.ts97
-rw-r--r--types/connector.d.ts34
-rw-r--r--types/content-type.d.ts21
-rw-r--r--types/cookies.d.ts28
-rw-r--r--types/diagnostics-channel.d.ts67
-rw-r--r--types/dispatcher.d.ts241
-rw-r--r--types/errors.d.ts128
-rw-r--r--types/fetch.d.ts209
-rw-r--r--types/file.d.ts39
-rw-r--r--types/filereader.d.ts54
-rw-r--r--types/formdata.d.ts108
-rw-r--r--types/global-dispatcher.d.ts9
-rw-r--r--types/global-origin.d.ts7
-rw-r--r--types/handlers.d.ts9
-rw-r--r--types/header.d.ts4
-rw-r--r--types/index.d.ts65
-rw-r--r--types/interceptors.d.ts5
-rw-r--r--types/mock-agent.d.ts50
-rw-r--r--types/mock-client.d.ts25
-rw-r--r--types/mock-errors.d.ts12
-rw-r--r--types/mock-interceptor.d.ts93
-rw-r--r--types/mock-pool.d.ts25
-rw-r--r--types/patch.d.ts71
-rw-r--r--types/pool-stats.d.ts19
-rw-r--r--types/pool.d.ts28
-rw-r--r--types/proxy-agent.d.ts30
-rw-r--r--types/readable.d.ts61
-rw-r--r--types/retry-handler.d.ts116
-rw-r--r--types/webidl.d.ts220
-rw-r--r--types/websocket.d.ts131
3690 files changed, 304422 insertions, 0 deletions
diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..4ad0cf5
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,8 @@
+# Ignore everything but the stuff following the `*` with the `!`
+# See https://docs.docker.com/engine/reference/builder/#dockerignore-file
+
+*
+!package.json
+!lib
+!deps
+!build
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..c7a0d1f
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,9 @@
+# https://editorconfig.org/
+
+root = true
+
+[*]
+indent_size = 2
+indent_style = space
+insert_final_newline = true
+trim_trailing_whitespace = true
diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md
new file mode 100644
index 0000000..8ff7029
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug-report.md
@@ -0,0 +1,34 @@
+---
+name: Bug Report
+about: Report an issue
+title: ''
+labels: bug
+assignees: ''
+
+---
+
+## Bug Description
+
+<!-- A clear and concise description of what the bug is. -->
+
+## Reproducible By
+
+<!-- A step by step list on how the bug can be reproduced for examination. -->
+
+## Expected Behavior
+
+<!-- A clear and concise description of what you expected to happen. -->
+
+## Logs & Screenshots
+
+<!-- If applicable, add screenshots to help explain your problem, or
+alternatively add your console logs here. -->
+
+## Environment
+
+<!-- This is just your OS and environment information [e.g. Ubuntu 18.04 LTS,
+Node v14.14.0] -->
+
+### Additional context
+
+<!-- Add any other context about the problem here. -->
diff --git a/.github/ISSUE_TEMPLATE/feature-request.md b/.github/ISSUE_TEMPLATE/feature-request.md
new file mode 100644
index 0000000..0c3a4ff
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature-request.md
@@ -0,0 +1,28 @@
+---
+name: Feature Request
+about: Make a suggestion on a feature or improvement for the project
+title: ''
+labels: enhancement
+assignees: ''
+
+---
+
+## This would solve...
+
+<!-- A clear and concise description of the problem this feature request relates
+to, if applicable. -->
+
+## The implementation should look like...
+
+<!-- A clear and concise description of how you expect this to be resolved or
+implemented. -->
+
+## I have also considered...
+
+<!-- A clear and concise description of any alternative solutions or features
+you have considered. -->
+
+## Additional context
+
+<!-- Add any other context, screenshots or ideas about the feature request
+here. -->
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 0000000..2620ffb
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,53 @@
+<!--
+Before submitting a Pull Request, please read our contribution guidelines, which
+can be found at CONTRIBUTING.md in the repository root.
+
+For code changes:
+1. Include tests for any bug fixes or new features.
+2. Update documentation if relevant.
+3. Ensure that tests and linting pass.
+
+You will also need to ensure that your contribution complies with the
+Developer's Certificate of Origin, outlined in CONTRIBUTING.md
+-->
+
+## This relates to...
+
+<!-- List the issues this resolves or relates to here (if applicable) -->
+
+## Rationale
+
+<!-- Briefly explain the purpose of this pull request, if not already
+justifiable with the above section. If it is, you may omit this section. -->
+
+## Changes
+
+<!-- Write a summary or list of changes here -->
+
+### Features
+
+<!-- List the new features here (if applicable), or write N/A if not -->
+
+### Bug Fixes
+
+<!-- List the fixed bugs here (if applicable), or write N/A if not -->
+
+### Breaking Changes and Deprecations
+
+<!-- List the breaking changes (changes that modify the existing API) and
+deprecations (removed features) here -->
+
+## Status
+
+<!-- KEY: S = Skipped, x = complete -->
+
+
+- [ ] I have read and agreed to the [Developer's Certificate of Origin][cert]
+- [ ] Tested
+- [ ] Benchmarked (**optional**)
+- [ ] Documented
+- [ ] Review ready
+- [ ] In review
+- [ ] Merge ready
+
+[cert]: https://github.com/nodejs/undici/blob/main/CONTRIBUTING.md
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..18b9fbf
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,23 @@
+version: 2
+updates:
+ - package-ecosystem: "github-actions"
+ directory: "/"
+ schedule:
+ interval: "monthly"
+ open-pull-requests-limit: 10
+
+ - package-ecosystem: "npm"
+ directory: "/"
+ schedule:
+ interval: "weekly"
+ open-pull-requests-limit: 10
+
+ - package-ecosystem: docker
+ directory: /build
+ schedule:
+ interval: daily
+
+ - package-ecosystem: pip
+ directory: /test/wpt/tests/resources/test
+ schedule:
+ interval: daily
diff --git a/.github/workflows/bench.yml b/.github/workflows/bench.yml
new file mode 100644
index 0000000..281bdc6
--- /dev/null
+++ b/.github/workflows/bench.yml
@@ -0,0 +1,43 @@
+name: Benchmarks
+on:
+ - push
+ - pull_request
+
+permissions:
+ contents: read
+
+jobs:
+ benchmark_current:
+ name: benchmark current
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout Code
+ uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
+ with:
+ persist-credentials: false
+ ref: ${{ github.base_ref }}
+ - name: Setup Node
+ uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4.0.0
+ with:
+ node-version: lts/*
+ - name: Install Modules
+ run: npm i
+ - name: Run Benchmark
+ run: npm run bench
+
+ benchmark_branch:
+ name: benchmark branch
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout Code
+ uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
+ with:
+ persist-credentials: false
+ - name: Setup Node
+ uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4.0.0
+ with:
+ node-version: lts/*
+ - name: Install Modules
+ run: npm i
+ - name: Run Benchmark
+ run: npm run bench
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
new file mode 100644
index 0000000..3c44e66
--- /dev/null
+++ b/.github/workflows/codeql.yml
@@ -0,0 +1,78 @@
+# For most projects, this workflow file will not need changing; you simply need
+# to commit it to your repository.
+#
+# You may wish to alter this file to override the set of languages analyzed,
+# or to provide custom queries or build logic.
+#
+# ******** NOTE ********
+# We have attempted to detect the languages in your repository. Please check
+# the `language` matrix defined below to confirm you have the correct set of
+# supported CodeQL languages.
+#
+name: "CodeQL"
+
+on:
+ push:
+ branches: ["main"]
+ pull_request:
+ # The branches below must be a subset of the branches above
+ branches: ["main"]
+ schedule:
+ - cron: "0 0 * * 1"
+
+permissions:
+ contents: read
+
+jobs:
+ analyze:
+ name: Analyze
+ runs-on: ubuntu-latest
+ permissions:
+ actions: read
+ contents: read
+ security-events: write
+
+ strategy:
+ fail-fast: false
+ matrix:
+ language: ["javascript", "python", "typescript"]
+ # CodeQL supports [ $supported-codeql-languages ]
+ # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
+
+ steps:
+ - name: Harden Runner
+ uses: step-security/harden-runner@1b05615854632b887b69ae1be8cbefe72d3ae423 # v2.6.0
+ with:
+ egress-policy: audit
+
+ - name: Checkout repository
+ uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
+
+ # Initializes the CodeQL tools for scanning.
+ - name: Initialize CodeQL
+ uses: github/codeql-action/init@74483a38d39275f33fcff5f35b679b5ca4a26a99 # v2.3.3
+ with:
+ languages: ${{ matrix.language }}
+ # If you wish to specify custom queries, you can do so here or in a config file.
+ # By default, queries listed here will override any specified in a config file.
+ # Prefix the list here with "+" to use these queries and those in the config file.
+
+ # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
+ # If this step fails, then you should remove it and run the build manually (see below)
+ - name: Autobuild
+ uses: github/codeql-action/autobuild@74483a38d39275f33fcff5f35b679b5ca4a26a99 # v2.3.3
+
+ # â„¹ï¸ Command-line programs to run using the OS shell.
+ # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
+
+ # If the Autobuild fails above, remove it and uncomment the following three lines.
+ # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
+
+ # - run: |
+ # echo "Run, Build Application using script"
+ # ./location_of_script_within_repo/buildscript.sh
+
+ - name: Perform CodeQL Analysis
+ uses: github/codeql-action/analyze@74483a38d39275f33fcff5f35b679b5ca4a26a99 # v2.3.3
+ with:
+ category: "/language:${{matrix.language}}"
diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml
new file mode 100644
index 0000000..0e356c7
--- /dev/null
+++ b/.github/workflows/dependency-review.yml
@@ -0,0 +1,27 @@
+# Dependency Review Action
+#
+# This Action will scan dependency manifest files that change as part of a Pull Request,
+# surfacing known-vulnerable versions of the packages declared or updated in the PR.
+# Once installed, if the workflow run is marked as required,
+# PRs introducing known-vulnerable packages will be blocked from merging.
+#
+# Source repository: https://github.com/actions/dependency-review-action
+name: 'Dependency Review'
+on: [pull_request]
+
+permissions:
+ contents: read
+
+jobs:
+ dependency-review:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Harden Runner
+ uses: step-security/harden-runner@1b05615854632b887b69ae1be8cbefe72d3ae423 # v2.6.0
+ with:
+ egress-policy: audit
+
+ - name: 'Checkout Repository'
+ uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
+ - name: 'Dependency Review'
+ uses: actions/dependency-review-action@6c5ccdad469c9f8a2996bfecaec55a631a347034 # v3.1.0
diff --git a/.github/workflows/fuzz.yml b/.github/workflows/fuzz.yml
new file mode 100644
index 0000000..29d7490
--- /dev/null
+++ b/.github/workflows/fuzz.yml
@@ -0,0 +1,39 @@
+name: Fuzzing
+
+on: [push, pull_request]
+
+permissions:
+ contents: read
+
+jobs:
+ fuzzing:
+ name: Fuzz
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
+ with:
+ persist-credentials: false
+
+ - name: Setup Node
+ uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4.0.0
+ with:
+ node-version: lts/*
+
+ - name: Install
+ run: |
+ npm install
+
+ - name: Run fuzzing
+ timeout-minutes: 10
+ run: |
+ npm run fuzz
+
+ - uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
+ if: ${{ failure() }}
+ with:
+ name: undici-fuzz-results-${{ github.sha }}
+ path: |
+ corpus/
+ crash-*
+ fuzz-results-*.json
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
new file mode 100644
index 0000000..2eb2b6b
--- /dev/null
+++ b/.github/workflows/lint.yml
@@ -0,0 +1,17 @@
+name: Lint
+on: [push, pull_request]
+permissions:
+ contents: read
+
+jobs:
+ lint:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
+ with:
+ persist-credentials: false
+ - uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4.0.0
+ with:
+ node-version: lts/*
+ - run: npm install
+ - run: npm run lint
diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml
new file mode 100644
index 0000000..4c3a77e
--- /dev/null
+++ b/.github/workflows/nodejs.yml
@@ -0,0 +1,45 @@
+# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
+# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
+
+
+name: Node CI
+
+on:
+ push:
+ branches:
+ - current
+ - next
+ - 'v*'
+ pull_request:
+
+jobs:
+ build:
+ name: Test
+ uses: pkgjs/action/.github/workflows/node-test.yaml@v0.1.7
+ with:
+ runs-on: ubuntu-latest, windows-latest
+ test-command: npm run coverage:ci
+ timeout-minutes: 15
+ post-test-steps: |
+ - name: Coverage Report
+ uses: codecov/codecov-action@v3
+ include: |
+ - runs-on: ubuntu-latest
+ node-version: 16.8
+ exclude: |
+ - runs-on: windows-latest
+ node-version: 14
+ - runs-on: windows-latest
+ node-version: 16
+ automerge:
+ if: >
+ github.event_name == 'pull_request' && github.event.pull_request.user.login == 'dependabot[bot]'
+ needs: build
+ runs-on: ubuntu-latest
+ permissions:
+ pull-requests: write
+ contents: write
+ steps:
+ - uses: fastify/github-action-merge-dependabot@59fc8817458fac20df8884576cfe69dbb77c9a07 # v3.9.1
+ with:
+ github-token: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.github/workflows/publish-undici-types.yml b/.github/workflows/publish-undici-types.yml
new file mode 100644
index 0000000..3f8fea3
--- /dev/null
+++ b/.github/workflows/publish-undici-types.yml
@@ -0,0 +1,26 @@
+name: Publish undici-types
+
+on:
+ push:
+ tags:
+ - 'v*'
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+jobs:
+ publish:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
+ - uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4.0.0
+ with:
+ node-version: '16.x'
+ registry-url: 'https://registry.npmjs.org'
+ - run: npm install
+ - run: node scripts/generate-undici-types-package-json.js
+ - run: npm publish
+ working-directory: './types'
+ env:
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml
new file mode 100644
index 0000000..f52ad55
--- /dev/null
+++ b/.github/workflows/scorecard.yml
@@ -0,0 +1,56 @@
+# This workflow uses actions that are not certified by GitHub. They are provided
+# by a third-party and are governed by separate terms of service, privacy
+# policy, and support documentation.
+
+name: Scorecard supply-chain security
+on:
+ # For Branch-Protection check. Only the default branch is supported. See
+ # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection
+ branch_protection_rule:
+ # To guarantee Maintained check is occasionally updated. See
+ # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained
+ schedule:
+ - cron: '16 10 * * 2'
+ push:
+ branches: [ "main" ]
+
+# Declare default permissions as read only.
+permissions: read-all
+
+jobs:
+ analysis:
+ name: Scorecard analysis
+ runs-on: ubuntu-latest
+ permissions:
+ # Needed to upload the results to code-scanning dashboard.
+ security-events: write
+ # Needed to publish results and get a badge (see publish_results below).
+ id-token: write
+
+ steps:
+ - name: "Checkout code"
+ uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
+ with:
+ persist-credentials: false
+
+ - name: "Run analysis"
+ uses: ossf/scorecard-action@0864cf19026789058feabb7e87baa5f140aac736 # v2.3.1
+ with:
+ results_file: results.sarif
+ results_format: sarif
+ publish_results: true
+
+ # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
+ # format to the repository Actions tab.
+ - name: "Upload artifact"
+ uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
+ with:
+ name: SARIF file
+ path: results.sarif
+ retention-days: 5
+
+ # Upload the results to GitHub's code scanning dashboard.
+ - name: "Upload to code-scanning"
+ uses: github/codeql-action/upload-sarif@74483a38d39275f33fcff5f35b679b5ca4a26a99 # v2.22.5
+ with:
+ sarif_file: results.sarif
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..acd1b69
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,81 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# Runtime data
+pids
+*.pid
+*.seed
+*.pid.lock
+
+# Directory for instrumented libs generated by jscoverage/JSCover
+lib-cov
+
+# Coverage directory used by tools like istanbul
+coverage
+
+# nyc test coverage
+.nyc_output
+
+# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
+.grunt
+
+# Bower dependency directory (https://bower.io/)
+bower_components
+
+# node-waf configuration
+.lock-wscript
+
+# Compiled binary addons (https://nodejs.org/api/addons.html)
+build/Release
+
+# Dependency directories
+node_modules/
+jspm_packages/
+
+# TypeScript v1 declaration files
+typings/
+
+# Optional npm cache directory
+.npm
+
+# Optional eslint cache
+.eslintcache
+
+# Optional REPL history
+.node_repl_history
+
+# Output of 'npm pack'
+*.tgz
+
+# Yarn Integrity file
+.yarn-integrity
+
+# dotenv environment variables file
+.env
+
+# next.js build output
+.next
+
+# lock files
+package-lock.json
+yarn.lock
+
+# IDE files
+.idea
+.vscode
+
+*0x
+*clinic*
+
+# Fuzzing
+corpus/
+crash-*
+fuzz-results-*.json
+
+# Bundle output
+undici-fetch.js
+/test/imports/undici-import.js
diff --git a/.husky/pre-commit b/.husky/pre-commit
new file mode 100755
index 0000000..20d0d06
--- /dev/null
+++ b/.husky/pre-commit
@@ -0,0 +1,4 @@
+#!/bin/sh
+. "$(dirname "$0")/_/husky.sh"
+
+npm run lint
diff --git a/.nojekyll b/.nojekyll
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/.nojekyll
diff --git a/.npmignore b/.npmignore
new file mode 100644
index 0000000..344e7f6
--- /dev/null
+++ b/.npmignore
@@ -0,0 +1,2 @@
+lib/llhttp/llhttp_simd.wasm
+lib/llhttp/llhttp.wasm
diff --git a/.taprc b/.taprc
new file mode 100644
index 0000000..61f7051
--- /dev/null
+++ b/.taprc
@@ -0,0 +1,7 @@
+ts: false
+jsx: false
+flow: false
+coverage: false
+expose-gc: true
+timeout: 60
+check-coverage: false
diff --git a/CNAME b/CNAME
new file mode 100644
index 0000000..27d813e
--- /dev/null
+++ b/CNAME
@@ -0,0 +1 @@
+undici.nodejs.org \ No newline at end of file
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
new file mode 100644
index 0000000..cb674bc
--- /dev/null
+++ b/CODE_OF_CONDUCT.md
@@ -0,0 +1,6 @@
+# Code of Conduct
+
+Undici is committed to upholding the Node.js Code of Conduct.
+
+The Node.js Code of Conduct document can be found at
+https://github.com/nodejs/admin/blob/main/CODE_OF_CONDUCT.md
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..3a7f3ff
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,201 @@
+# Contributing to Undici
+
+* [Guides](#guides)
+ * [Update `llhttp`](#update-llhttp)
+ * [Lint](#lint)
+ * [Test](#test)
+ * [Coverage](#coverage)
+ * [Update `WPTs`](#update-wpts)
+* [Developer's Certificate of Origin 1.1](#developers-certificate-of-origin)
+ * [Moderation Policy](#moderation-policy)
+
+<a id="guides"></a>
+## Guides
+
+<a id="update-llhttp"></a>
+### Update `llhttp`
+
+The HTTP parser used by `undici` is a WebAssembly build of [`llhttp`](https://github.com/nodejs/llhttp).
+
+While the project itself provides a way to compile targeting WebAssembly, at the moment we embed the sources
+directly and compile the module in `undici`.
+
+The `deps/llhttp/include` folder contains the C header files, while the `deps/llhttp/src` folder contains
+the C source files needed to compile the module.
+
+The `lib/llhttp` folder contains the `.js` transpiled assets required to implement a parser.
+
+The following are the steps required to perform an update.
+
+#### Clone the [llhttp](https://github.com/nodejs/llhttp) project
+
+```bash
+git clone git@github.com:nodejs/llhttp.git
+
+cd llhttp
+```
+#### Checkout a `llhttp` release
+
+```bash
+git checkout <tag>
+```
+
+#### Install the `llhttp` dependencies
+
+```bash
+npm i
+```
+
+#### Run the wasm build script
+
+> This requires [docker](https://www.docker.com/) installed on your machine.
+
+```bash
+npm run build-wasm
+```
+
+#### Copy the sources to `undici`
+
+```bash
+cp build/wasm/*.js <your-path-to-undici>/lib/llhttp/
+
+cp build/wasm/*.js.map <your-path-to-undici>/lib/llhttp/
+
+cp build/wasm/*.d.ts <your-path-to-undici>/lib/llhttp/
+
+cp src/native/api.c src/native/http.c build/c/llhttp.c <your-path-to-undici>/deps/llhttp/src/
+
+cp src/native/api.h build/llhttp.h <your-path-to-undici>/deps/llhttp/include/
+```
+
+#### Build the WebAssembly module in `undici`
+
+> This requires [docker](https://www.docker.com/) installed on your machine.
+
+```bash
+cd <your-path-to-undici>
+
+npm run build:wasm
+```
+
+#### Commit the contents of lib/llhttp
+
+Create a commit which includes all of the updated files in lib/llhttp.
+
+<a id="update-wpts"></a>
+### Update `WPTs`
+
+`undici` runs a subset of the [`web-platform-tests`](https://github.com/web-platform-tests/wpt).
+
+Here are the steps to update them.
+
+<details>
+<summary>Skip the tutorial</summary>
+
+```bash
+git clone --depth 1 --single-branch --branch epochs/daily --filter=blob:none --sparse https://github.com/web-platform-tests/wpt.git test/wpt/tests
+cd test/wpt/tests
+
+git sparse-checkout add /resources
+git sparse-checkout add /interfaces
+git sparse-checkout add /common
+git sparse-checkout add /fetch
+git sparse-checkout add /FileAPI
+git sparse-checkout add /xhr
+git sparse-checkout add /websockets
+git sparse-checkout add /mimesniff
+git sparse-checkout add /storage
+git sparse-checkout add /service-workers
+```
+</details>
+
+#### Sparse-clone the [wpt](https://github.com/web-platform-tests/wpt) repo
+
+```bash
+git clone --depth 1 --single-branch --branch epochs/daily --filter=blob:none --sparse https://github.com/web-platform-tests/wpt.git test/wpt/tests
+
+cd test/wpt/tests
+
+```
+
+#### Checkout the tests
+
+Only run the commands for the folder(s) you want to update.
+
+```bash
+git sparse-checkout add /fetch
+git sparse-checkout add /FileAPI
+git sparse-checkout add /xhr
+git sparse-checkout add /websockets
+git sparse-checkout add /resources
+git sparse-checkout add /common
+
+# etc
+```
+
+#### Run the tests
+
+Run the tests to ensure that any new failures are marked as such.
+
+You can mark tests as failing in their corresponding [status](./test/wpt/status) file.
+
+```bash
+npm run test:wpt
+```
+
+<a id="lint"></a>
+
+### Lint
+
+```bash
+npm run lint
+```
+
+<a id="test"></a>
+### Test
+
+```bash
+npm run test
+```
+
+<a id="coverage"></a>
+### Coverage
+
+```bash
+npm run coverage
+```
+
+<a id="developers-certificate-of-origin"></a>
+## Developer's Certificate of Origin 1.1
+
+By making a contribution to this project, I certify that:
+
+* (a) The contribution was created in whole or in part by me and I
+ have the right to submit it under the open source license
+ indicated in the file; or
+
+* (b) The contribution is based upon previous work that, to the best
+ of my knowledge, is covered under an appropriate open source
+ license and I have the right under that license to submit that
+ work with modifications, whether created in whole or in part
+ by me, under the same open source license (unless I am
+ permitted to submit under a different license), as indicated
+ in the file; or
+
+* (c) The contribution was provided directly to me by some other
+ person who certified (a), (b) or (c) and I have not modified
+ it.
+
+* (d) I understand and agree that this project and the contribution
+ are public and that a record of the contribution (including all
+ personal information I submit with it, including my sign-off) is
+ maintained indefinitely and may be redistributed consistent with
+ this project or the open source license(s) involved.
+
+<a id="moderation-policy"></a>
+### Moderation Policy
+
+The [Node.js Moderation Policy] applies to this project.
+
+[Node.js Moderation Policy]:
+https://github.com/nodejs/admin/blob/main/Moderation-Policy.md
diff --git a/GOVERNANCE.md b/GOVERNANCE.md
new file mode 100644
index 0000000..3e88d4b
--- /dev/null
+++ b/GOVERNANCE.md
@@ -0,0 +1,136 @@
+### Undici Working Group
+
+The Node.js Undici project is governed by a Working Group (WG)
+that is responsible for high-level guidance of the project.
+
+The WG has final authority over this project including:
+
+* Technical direction
+* Project governance and process (including this policy)
+* Contribution policy
+* GitHub repository hosting
+* Conduct guidelines
+* Maintaining the list of additional Collaborators
+
+For the current list of WG members, see the project
+[README.md](./README.md#collaborators).
+
+### Collaborators
+
+The undici GitHub repository is
+maintained by the WG and additional Collaborators who are added by the
+WG on an ongoing basis.
+
+Individuals making significant and valuable contributions are made
+Collaborators and given commit-access to the project. These
+individuals are identified by the WG and their addition as
+Collaborators is discussed during the WG meeting.
+
+_Note:_ If you make a significant contribution and are not considered
+for commit-access log an issue or contact a WG member directly and it
+will be brought up in the next WG meeting.
+
+Modifications of the contents of the undici repository are
+made on
+a collaborative basis. Anybody with a GitHub account may propose a
+modification via pull request and it will be considered by the project
+Collaborators. All pull requests must be reviewed and accepted by a
+Collaborator with sufficient expertise who is able to take full
+responsibility for the change. In the case of pull requests proposed
+by an existing Collaborator, an additional Collaborator is required
+for sign-off. Consensus should be sought if additional Collaborators
+participate and there is disagreement around a particular
+modification. See _Consensus Seeking Process_ below for further detail
+on the consensus model used for governance.
+
+Collaborators may opt to elevate significant or controversial
+modifications, or modifications that have not found consensus to the
+WG for discussion by assigning the ***WG-agenda*** tag to a pull
+request or issue. The WG should serve as the final arbiter where
+required.
+
+For the current list of Collaborators, see the project
+[README.md](./README.md#collaborators). The list shall be in an
+alphabetical order.
+
+### WG Membership
+
+WG seats are not time-limited. There is no fixed size of the WG.
+However, the expected target is between 6 and 12, to ensure adequate
+coverage of important areas of expertise, balanced with the ability to
+make decisions efficiently.
+
+There is no specific set of requirements or qualifications for WG
+membership beyond these rules.
+
+The WG may add additional members to the WG by unanimous consensus.
+
+A WG member may be removed from the WG by voluntary resignation, or by
+unanimous consensus of all other WG members.
+
+Changes to WG membership should be posted in the agenda, and may be
+suggested as any other agenda item (see "WG Meetings" below).
+
+If an addition or removal is proposed during a meeting, and the full
+WG is not in attendance to participate, then the addition or removal
+is added to the agenda for the subsequent meeting. This is to ensure
+that all members are given the opportunity to participate in all
+membership decisions. If a WG member is unable to attend a meeting
+where a planned membership decision is being made, then their consent
+is assumed.
+
+No more than 1/3 of the WG members may be affiliated with the same
+employer. If removal or resignation of a WG member, or a change of
+employment by a WG member, creates a situation where more than 1/3 of
+the WG membership shares an employer, then the situation must be
+immediately remedied by the resignation or removal of one or more WG
+members affiliated with the over-represented employer(s).
+
+### WG Meetings
+
+The WG meets occasionally on Zoom. A designated moderator
+approved by the WG runs the meeting. Each meeting should be
+published to YouTube.
+
+Items are added to the WG agenda that are considered contentious or
+are modifications of governance, contribution policy, WG membership,
+or release process.
+
+The intention of the agenda is not to approve or review all patches;
+that should happen continuously on GitHub and be handled by the larger
+group of Collaborators.
+
+Any community member or contributor can ask that something be added to
+the next meeting's agenda by logging a GitHub Issue. Any Collaborator,
+WG member or the moderator can add the item to the agenda by adding
+the ***WG-agenda*** tag to the issue.
+
+Prior to each WG meeting the moderator will share the Agenda with
+members of the WG. WG members can add any items they like to the
+agenda at the beginning of each meeting. The moderator and the WG
+cannot veto or remove items.
+
+The WG may invite persons or representatives from certain projects to
+participate in a non-voting capacity.
+
+The moderator is responsible for summarizing the discussion of each
+agenda item and sends it as a pull request after the meeting.
+
+### Consensus Seeking Process
+
+The WG follows a
+[Consensus
+Seeking](http://en.wikipedia.org/wiki/Consensus-seeking_decision-making)
+decision-making model.
+
+When an agenda item has appeared to reach a consensus the moderator
+will ask "Does anyone object?" as a final call for dissent from the
+consensus.
+
+If an agenda item cannot reach a consensus a WG member can call for
+either a closing vote or a vote to table the issue to the next
+meeting. The call for a vote must be seconded by a majority of the WG
+or else the discussion will continue. Simple majority wins.
+
+Note that changes to WG membership require a majority consensus. See
+"WG Membership" above.
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..e7323bb
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) Matteo Collina and Undici contributors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/MAINTAINERS.md b/MAINTAINERS.md
new file mode 100644
index 0000000..b98d904
--- /dev/null
+++ b/MAINTAINERS.md
@@ -0,0 +1,33 @@
+# Maintainers
+
+This document details any and all processes relevant to project maintainers. Maintainers should feel empowered to contribute back to this document with any process changes they feel improve the overall experience for themselves and other maintainers.
+
+## Labels
+
+Maintainers are encouraged to use the extensive and detailed list of labels for easier repo management.
+
+* Generally, all issues should be labelled. The most general labels are `bug`, `enhancement`, and `Status: help-wanted`.
+* Issues specific to a certain aspect of the project should be labeled using one of the specificity labels listed below. For example, a bug in the `Client` class should have the `Client` and `bug` label assigned.
+ * Specificity labels:
+ * `Agent`
+ * `Client`
+ * `Docs`
+ * `Performance`
+ * `Pool`
+ * `Tests`
+ * `Types`
+* Any `question` or `usage help` issues should be converted into Q&A Discussions
+* `Status:` labels should be added to all open issues indicating their relative development status.
+ * Status labels:
+ * `Status: blocked`
+ * `Status: help-wanted`
+ * `Status: in-progress`
+ * `Status: wontfix`
+* Issues and/or pull requests with an agreed upon semver status can be assigned the appropriate `semver-` label.
+ * Semver labels:
+ * `semver-major`
+ * `semver-minor`
+ * `semver-patch`
+* Issues with a low-barrier of entry should be assigned the `good first issue` label.
+* Do not use the `invalid` label, instead use `bug` or `Status: wontfix`.
+* Duplicate issues should initially be assigned the `duplicate` label.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..3ba8989
--- /dev/null
+++ b/README.md
@@ -0,0 +1,443 @@
+# undici
+
+[![Node CI](https://github.com/nodejs/undici/actions/workflows/nodejs.yml/badge.svg)](https://github.com/nodejs/undici/actions/workflows/nodejs.yml) [![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat)](http://standardjs.com/) [![npm version](https://badge.fury.io/js/undici.svg)](https://badge.fury.io/js/undici) [![codecov](https://codecov.io/gh/nodejs/undici/branch/main/graph/badge.svg?token=yZL6LtXkOA)](https://codecov.io/gh/nodejs/undici)
+
+An HTTP/1.1 client, written from scratch for Node.js.
+
+> Undici means eleven in Italian. 1.1 -> 11 -> Eleven -> Undici.
+It is also a Stranger Things reference.
+
+Have a question about using Undici? Open a [Q&A Discussion](https://github.com/nodejs/undici/discussions/new) or join our official OpenJS [Slack](https://openjs-foundation.slack.com/archives/C01QF9Q31QD) channel.
+
+## Install
+
+```
+npm i undici
+```
+
+## Benchmarks
+
+The benchmark is a simple `hello world` [example](benchmarks/benchmark.js) using a
+number of unix sockets (connections) with a pipelining depth of 10 running on Node 20.6.0.
+
+### Connections 1
+
+
+| Tests | Samples | Result | Tolerance | Difference with slowest |
+|---------------------|---------|---------------|-----------|-------------------------|
+| http - no keepalive | 15 | 5.32 req/sec | ± 2.61 % | - |
+| http - keepalive | 10 | 5.35 req/sec | ± 2.47 % | + 0.44 % |
+| undici - fetch | 15 | 41.85 req/sec | ± 2.49 % | + 686.04 % |
+| undici - pipeline | 40 | 50.36 req/sec | ± 2.77 % | + 845.92 % |
+| undici - stream | 15 | 60.58 req/sec | ± 2.75 % | + 1037.72 % |
+| undici - request | 10 | 61.19 req/sec | ± 2.60 % | + 1049.24 % |
+| undici - dispatch | 20 | 64.84 req/sec | ± 2.81 % | + 1117.81 % |
+
+
+### Connections 50
+
+| Tests | Samples | Result | Tolerance | Difference with slowest |
+|---------------------|---------|------------------|-----------|-------------------------|
+| undici - fetch | 30 | 2107.19 req/sec | ± 2.69 % | - |
+| http - no keepalive | 10 | 2698.90 req/sec | ± 2.68 % | + 28.08 % |
+| http - keepalive | 10 | 4639.49 req/sec | ± 2.55 % | + 120.17 % |
+| undici - pipeline | 40 | 6123.33 req/sec | ± 2.97 % | + 190.59 % |
+| undici - stream | 50 | 9426.51 req/sec | ± 2.92 % | + 347.35 % |
+| undici - request | 10 | 10162.88 req/sec | ± 2.13 % | + 382.29 % |
+| undici - dispatch | 50 | 11191.11 req/sec | ± 2.98 % | + 431.09 % |
+
+
+## Quick Start
+
+```js
+import { request } from 'undici'
+
+const {
+ statusCode,
+ headers,
+ trailers,
+ body
+} = await request('http://localhost:3000/foo')
+
+console.log('response received', statusCode)
+console.log('headers', headers)
+
+for await (const data of body) {
+ console.log('data', data)
+}
+
+console.log('trailers', trailers)
+```
+
+## Body Mixins
+
+The `body` mixins are the most common way to format the request/response body. Mixins include:
+
+- [`.formData()`](https://fetch.spec.whatwg.org/#dom-body-formdata)
+- [`.json()`](https://fetch.spec.whatwg.org/#dom-body-json)
+- [`.text()`](https://fetch.spec.whatwg.org/#dom-body-text)
+
+Example usage:
+
+```js
+import { request } from 'undici'
+
+const {
+ statusCode,
+ headers,
+ trailers,
+ body
+} = await request('http://localhost:3000/foo')
+
+console.log('response received', statusCode)
+console.log('headers', headers)
+console.log('data', await body.json())
+console.log('trailers', trailers)
+```
+
+_Note: Once a mixin has been called then the body cannot be reused, thus calling additional mixins on `.body`, e.g. `.body.json(); .body.text()` will result in an error `TypeError: unusable` being thrown and returned through the `Promise` rejection._
+
+Should you need to access the `body` in plain-text after using a mixin, the best practice is to use the `.text()` mixin first and then manually parse the text to the desired format.
+
+For more information about their behavior, please reference the body mixin from the [Fetch Standard](https://fetch.spec.whatwg.org/#body-mixin).
+
+## Common API Methods
+
+This section documents our most commonly used API methods. Additional APIs are documented in their own files within the [docs](./docs/) folder and are accessible via the navigation list on the left side of the docs site.
+
+### `undici.request([url, options]): Promise`
+
+Arguments:
+
+* **url** `string | URL | UrlObject`
+* **options** [`RequestOptions`](./docs/api/Dispatcher.md#parameter-requestoptions)
+ * **dispatcher** `Dispatcher` - Default: [getGlobalDispatcher](#undicigetglobaldispatcher)
+ * **method** `String` - Default: `PUT` if `options.body`, otherwise `GET`
+ * **maxRedirections** `Integer` - Default: `0`
+
+Returns a promise with the result of the `Dispatcher.request` method.
+
+Calls `options.dispatcher.request(options)`.
+
+See [Dispatcher.request](./docs/api/Dispatcher.md#dispatcherrequestoptions-callback) for more details.
+
+### `undici.stream([url, options, ]factory): Promise`
+
+Arguments:
+
+* **url** `string | URL | UrlObject`
+* **options** [`StreamOptions`](./docs/api/Dispatcher.md#parameter-streamoptions)
+ * **dispatcher** `Dispatcher` - Default: [getGlobalDispatcher](#undicigetglobaldispatcher)
+ * **method** `String` - Default: `PUT` if `options.body`, otherwise `GET`
+ * **maxRedirections** `Integer` - Default: `0`
+* **factory** `Dispatcher.stream.factory`
+
+Returns a promise with the result of the `Dispatcher.stream` method.
+
+Calls `options.dispatcher.stream(options, factory)`.
+
+See [Dispatcher.stream](docs/api/Dispatcher.md#dispatcherstreamoptions-factory-callback) for more details.
+
+### `undici.pipeline([url, options, ]handler): Duplex`
+
+Arguments:
+
+* **url** `string | URL | UrlObject`
+* **options** [`PipelineOptions`](docs/api/Dispatcher.md#parameter-pipelineoptions)
+ * **dispatcher** `Dispatcher` - Default: [getGlobalDispatcher](#undicigetglobaldispatcher)
+ * **method** `String` - Default: `PUT` if `options.body`, otherwise `GET`
+ * **maxRedirections** `Integer` - Default: `0`
+* **handler** `Dispatcher.pipeline.handler`
+
+Returns: `stream.Duplex`
+
+Calls `options.dispatch.pipeline(options, handler)`.
+
+See [Dispatcher.pipeline](docs/api/Dispatcher.md#dispatcherpipelineoptions-handler) for more details.
+
+### `undici.connect([url, options]): Promise`
+
+Starts two-way communications with the requested resource using [HTTP CONNECT](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/CONNECT).
+
+Arguments:
+
+* **url** `string | URL | UrlObject`
+* **options** [`ConnectOptions`](docs/api/Dispatcher.md#parameter-connectoptions)
+ * **dispatcher** `Dispatcher` - Default: [getGlobalDispatcher](#undicigetglobaldispatcher)
+ * **maxRedirections** `Integer` - Default: `0`
+* **callback** `(err: Error | null, data: ConnectData | null) => void` (optional)
+
+Returns a promise with the result of the `Dispatcher.connect` method.
+
+Calls `options.dispatch.connect(options)`.
+
+See [Dispatcher.connect](docs/api/Dispatcher.md#dispatcherconnectoptions-callback) for more details.
+
+### `undici.fetch(input[, init]): Promise`
+
+Implements [fetch](https://fetch.spec.whatwg.org/#fetch-method).
+
+* https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch
+* https://fetch.spec.whatwg.org/#fetch-method
+
+Only supported on Node 16.8+.
+
+Basic usage example:
+
+```js
+import { fetch } from 'undici'
+
+
+const res = await fetch('https://example.com')
+const json = await res.json()
+console.log(json)
+```
+
+You can pass an optional dispatcher to `fetch` as:
+
+```js
+import { fetch, Agent } from 'undici'
+
+const res = await fetch('https://example.com', {
+ // Mocks are also supported
+ dispatcher: new Agent({
+ keepAliveTimeout: 10,
+ keepAliveMaxTimeout: 10
+ })
+})
+const json = await res.json()
+console.log(json)
+```
+
+#### `request.body`
+
+A body can be of the following types:
+
+- ArrayBuffer
+- ArrayBufferView
+- AsyncIterables
+- Blob
+- Iterables
+- String
+- URLSearchParams
+- FormData
+
+In this implementation of fetch, ```request.body``` now accepts ```Async Iterables```. It is not present in the [Fetch Standard.](https://fetch.spec.whatwg.org)
+
+```js
+import { fetch } from 'undici'
+
+const data = {
+ async *[Symbol.asyncIterator]() {
+ yield 'hello'
+ yield 'world'
+ },
+}
+
+await fetch('https://example.com', { body: data, method: 'POST', duplex: 'half' })
+```
+
+#### `request.duplex`
+
+- half
+
+In this implementation of fetch, `request.duplex` must be set if `request.body` is `ReadableStream` or `Async Iterables`. And fetch requests are currently always be full duplex. More detail refer to [Fetch Standard.](https://fetch.spec.whatwg.org/#dom-requestinit-duplex)
+
+#### `response.body`
+
+Nodejs has two kinds of streams: [web streams](https://nodejs.org/dist/latest-v16.x/docs/api/webstreams.html), which follow the API of the WHATWG web standard found in browsers, and an older Node-specific [streams API](https://nodejs.org/api/stream.html). `response.body` returns a readable web stream. If you would prefer to work with a Node stream you can convert a web stream using `.fromWeb()`.
+
+```js
+import { fetch } from 'undici'
+import { Readable } from 'node:stream'
+
+const response = await fetch('https://example.com')
+const readableWebStream = response.body
+const readableNodeStream = Readable.fromWeb(readableWebStream)
+```
+
+#### Specification Compliance
+
+This section documents parts of the [Fetch Standard](https://fetch.spec.whatwg.org) that Undici does
+not support or does not fully implement.
+
+##### Garbage Collection
+
+* https://fetch.spec.whatwg.org/#garbage-collection
+
+The [Fetch Standard](https://fetch.spec.whatwg.org) allows users to skip consuming the response body by relying on
+[garbage collection](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Memory_Management#garbage_collection) to release connection resources. Undici does not do the same. Therefore, it is important to always either consume or cancel the response body.
+
+Garbage collection in Node is less aggressive and deterministic
+(due to the lack of clear idle periods that browsers have through the rendering refresh rate)
+which means that leaving the release of connection resources to the garbage collector can lead
+to excessive connection usage, reduced performance (due to less connection re-use), and even
+stalls or deadlocks when running out of connections.
+
+```js
+// Do
+const headers = await fetch(url)
+ .then(async res => {
+ for await (const chunk of res.body) {
+ // force consumption of body
+ }
+ return res.headers
+ })
+
+// Do not
+const headers = await fetch(url)
+ .then(res => res.headers)
+```
+
+However, if you want to get only headers, it might be better to use `HEAD` request method. Usage of this method will obviate the need for consumption or cancelling of the response body. See [MDN - HTTP - HTTP request methods - HEAD](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/HEAD) for more details.
+
+```js
+const headers = await fetch(url, { method: 'HEAD' })
+ .then(res => res.headers)
+```
+
+##### Forbidden and Safelisted Header Names
+
+* https://fetch.spec.whatwg.org/#cors-safelisted-response-header-name
+* https://fetch.spec.whatwg.org/#forbidden-header-name
+* https://fetch.spec.whatwg.org/#forbidden-response-header-name
+* https://github.com/wintercg/fetch/issues/6
+
+The [Fetch Standard](https://fetch.spec.whatwg.org) requires implementations to exclude certain headers from requests and responses. In browser environments, some headers are forbidden so the user agent remains in full control over them. In Undici, these constraints are removed to give more control to the user.
+
+### `undici.upgrade([url, options]): Promise`
+
+Upgrade to a different protocol. See [MDN - HTTP - Protocol upgrade mechanism](https://developer.mozilla.org/en-US/docs/Web/HTTP/Protocol_upgrade_mechanism) for more details.
+
+Arguments:
+
+* **url** `string | URL | UrlObject`
+* **options** [`UpgradeOptions`](docs/api/Dispatcher.md#parameter-upgradeoptions)
+ * **dispatcher** `Dispatcher` - Default: [getGlobalDispatcher](#undicigetglobaldispatcher)
+ * **maxRedirections** `Integer` - Default: `0`
+* **callback** `(error: Error | null, data: UpgradeData) => void` (optional)
+
+Returns a promise with the result of the `Dispatcher.upgrade` method.
+
+Calls `options.dispatcher.upgrade(options)`.
+
+See [Dispatcher.upgrade](docs/api/Dispatcher.md#dispatcherupgradeoptions-callback) for more details.
+
+### `undici.setGlobalDispatcher(dispatcher)`
+
+* dispatcher `Dispatcher`
+
+Sets the global dispatcher used by Common API Methods.
+
+### `undici.getGlobalDispatcher()`
+
+Gets the global dispatcher used by Common API Methods.
+
+Returns: `Dispatcher`
+
+### `undici.setGlobalOrigin(origin)`
+
+* origin `string | URL | undefined`
+
+Sets the global origin used in `fetch`.
+
+If `undefined` is passed, the global origin will be reset. This will cause `Response.redirect`, `new Request()`, and `fetch` to throw an error when a relative path is passed.
+
+```js
+setGlobalOrigin('http://localhost:3000')
+
+const response = await fetch('/api/ping')
+
+console.log(response.url) // http://localhost:3000/api/ping
+```
+
+### `undici.getGlobalOrigin()`
+
+Gets the global origin used in `fetch`.
+
+Returns: `URL`
+
+### `UrlObject`
+
+* **port** `string | number` (optional)
+* **path** `string` (optional)
+* **pathname** `string` (optional)
+* **hostname** `string` (optional)
+* **origin** `string` (optional)
+* **protocol** `string` (optional)
+* **search** `string` (optional)
+
+## Specification Compliance
+
+This section documents parts of the HTTP/1.1 specification that Undici does
+not support or does not fully implement.
+
+### Expect
+
+Undici does not support the `Expect` request header field. The request
+body is always immediately sent and the `100 Continue` response will be
+ignored.
+
+Refs: https://tools.ietf.org/html/rfc7231#section-5.1.1
+
+### Pipelining
+
+Undici will only use pipelining if configured with a `pipelining` factor
+greater than `1`.
+
+Undici always assumes that connections are persistent and will immediately
+pipeline requests, without checking whether the connection is persistent.
+Hence, automatic fallback to HTTP/1.0 or HTTP/1.1 without pipelining is
+not supported.
+
+Undici will immediately pipeline when retrying requests after a failed
+connection. However, Undici will not retry the first remaining requests in
+the prior pipeline and instead error the corresponding callback/promise/stream.
+
+Undici will abort all running requests in the pipeline when any of them are
+aborted.
+
+* Refs: https://tools.ietf.org/html/rfc2616#section-8.1.2.2
+* Refs: https://tools.ietf.org/html/rfc7230#section-6.3.2
+
+### Manual Redirect
+
+Since it is not possible to manually follow an HTTP redirect on the server-side,
+Undici returns the actual response instead of an `opaqueredirect` filtered one
+when invoked with a `manual` redirect. This aligns `fetch()` with the other
+implementations in Deno and Cloudflare Workers.
+
+Refs: https://fetch.spec.whatwg.org/#atomic-http-redirect-handling
+
+## Workarounds
+
+### Network address family autoselection.
+
+If you experience problem when connecting to a remote server that is resolved by your DNS servers to a IPv6 (AAAA record)
+first, there are chances that your local router or ISP might have problem connecting to IPv6 networks. In that case
+undici will throw an error with code `UND_ERR_CONNECT_TIMEOUT`.
+
+If the target server resolves to both a IPv6 and IPv4 (A records) address and you are using a compatible Node version
+(18.3.0 and above), you can fix the problem by providing the `autoSelectFamily` option (support by both `undici.request`
+and `undici.Agent`) which will enable the family autoselection algorithm when establishing the connection.
+
+## Collaborators
+
+* [__Daniele Belardi__](https://github.com/dnlup), <https://www.npmjs.com/~dnlup>
+* [__Ethan Arrowood__](https://github.com/ethan-arrowood), <https://www.npmjs.com/~ethan_arrowood>
+* [__Matteo Collina__](https://github.com/mcollina), <https://www.npmjs.com/~matteo.collina>
+* [__Matthew Aitken__](https://github.com/KhafraDev), <https://www.npmjs.com/~khaf>
+* [__Robert Nagy__](https://github.com/ronag), <https://www.npmjs.com/~ronag>
+* [__Szymon Marczak__](https://github.com/szmarczak), <https://www.npmjs.com/~szmarczak>
+* [__Tomas Della Vedova__](https://github.com/delvedor), <https://www.npmjs.com/~delvedor>
+
+### Releasers
+
+* [__Ethan Arrowood__](https://github.com/ethan-arrowood), <https://www.npmjs.com/~ethan_arrowood>
+* [__Matteo Collina__](https://github.com/mcollina), <https://www.npmjs.com/~matteo.collina>
+* [__Robert Nagy__](https://github.com/ronag), <https://www.npmjs.com/~ronag>
+* [__Matthew Aitken__](https://github.com/KhafraDev), <https://www.npmjs.com/~khaf>
+
+## License
+
+MIT
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 0000000..dc5499a
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,2 @@
+If you believe you have found a security issue in the software in this
+repository, please consult https://github.com/nodejs/node/blob/HEAD/SECURITY.md.
diff --git a/benchmarks/benchmark-http2.js b/benchmarks/benchmark-http2.js
new file mode 100644
index 0000000..d8555de
--- /dev/null
+++ b/benchmarks/benchmark-http2.js
@@ -0,0 +1,306 @@
+'use strict'
+
+const { connect } = require('http2')
+const { createSecureContext } = require('tls')
+const os = require('os')
+const path = require('path')
+const { readFileSync } = require('fs')
+const { table } = require('table')
+const { Writable } = require('stream')
+const { WritableStream } = require('stream/web')
+const { isMainThread } = require('worker_threads')
+
+const { Pool, Client, fetch, Agent, setGlobalDispatcher } = require('..')
+
+const ca = readFileSync(path.join(__dirname, '..', 'test', 'fixtures', 'ca.pem'), 'utf8')
+const servername = 'agent1'
+
+const iterations = (parseInt(process.env.SAMPLES, 10) || 10) + 1
+const errorThreshold = parseInt(process.env.ERROR_THRESHOLD, 10) || 3
+const connections = parseInt(process.env.CONNECTIONS, 10) || 50
+const pipelining = parseInt(process.env.PIPELINING, 10) || 10
+const parallelRequests = parseInt(process.env.PARALLEL, 10) || 100
+const headersTimeout = parseInt(process.env.HEADERS_TIMEOUT, 10) || 0
+const bodyTimeout = parseInt(process.env.BODY_TIMEOUT, 10) || 0
+const dest = {}
+
+if (process.env.PORT) {
+ dest.port = process.env.PORT
+ dest.url = `https://localhost:${process.env.PORT}`
+} else {
+ dest.url = 'https://localhost'
+ dest.socketPath = path.join(os.tmpdir(), 'undici.sock')
+}
+
+const httpsBaseOptions = {
+ ca,
+ servername,
+ protocol: 'https:',
+ hostname: 'localhost',
+ method: 'GET',
+ path: '/',
+ query: {
+ frappucino: 'muffin',
+ goat: 'scone',
+ pond: 'moose',
+ foo: ['bar', 'baz', 'bal'],
+ bool: true,
+ numberKey: 256
+ },
+ ...dest
+}
+
+const http2ClientOptions = {
+ secureContext: createSecureContext({ ca }),
+ servername
+}
+
+const undiciOptions = {
+ path: '/',
+ method: 'GET',
+ headersTimeout,
+ bodyTimeout
+}
+
+const Class = connections > 1 ? Pool : Client
+const dispatcher = new Class(httpsBaseOptions.url, {
+ allowH2: true,
+ pipelining,
+ connections,
+ connect: {
+ rejectUnauthorized: false,
+ ca,
+ servername
+ },
+ ...dest
+})
+
+setGlobalDispatcher(new Agent({
+ allowH2: true,
+ pipelining,
+ connections,
+ connect: {
+ rejectUnauthorized: false,
+ ca,
+ servername
+ }
+}))
+
+class SimpleRequest {
+ constructor (resolve) {
+ this.dst = new Writable({
+ write (chunk, encoding, callback) {
+ callback()
+ }
+ }).on('finish', resolve)
+ }
+
+ onConnect (abort) { }
+
+ onHeaders (statusCode, headers, resume) {
+ this.dst.on('drain', resume)
+ }
+
+ onData (chunk) {
+ return this.dst.write(chunk)
+ }
+
+ onComplete () {
+ this.dst.end()
+ }
+
+ onError (err) {
+ throw err
+ }
+}
+
+function makeParallelRequests (cb) {
+ return Promise.all(Array.from(Array(parallelRequests)).map(() => new Promise(cb)))
+}
+
+function printResults (results) {
+ // Sort results by least performant first, then compare relative performances and also printing padding
+ let last
+
+ const rows = Object.entries(results)
+ // If any failed, put on the top of the list, otherwise order by mean, ascending
+ .sort((a, b) => (!a[1].success ? -1 : b[1].mean - a[1].mean))
+ .map(([name, result]) => {
+ if (!result.success) {
+ return [name, result.size, 'Errored', 'N/A', 'N/A']
+ }
+
+ // Calculate throughput and relative performance
+ const { size, mean, standardError } = result
+ const relative = last !== 0 ? (last / mean - 1) * 100 : 0
+
+ // Save the slowest for relative comparison
+ if (typeof last === 'undefined') {
+ last = mean
+ }
+
+ return [
+ name,
+ size,
+ `${((connections * 1e9) / mean).toFixed(2)} req/sec`,
+ `± ${((standardError / mean) * 100).toFixed(2)} %`,
+ relative > 0 ? `+ ${relative.toFixed(2)} %` : '-'
+ ]
+ })
+
+ console.log(results)
+
+ // Add the header row
+ rows.unshift(['Tests', 'Samples', 'Result', 'Tolerance', 'Difference with slowest'])
+
+ return table(rows, {
+ columns: {
+ 0: {
+ alignment: 'left'
+ },
+ 1: {
+ alignment: 'right'
+ },
+ 2: {
+ alignment: 'right'
+ },
+ 3: {
+ alignment: 'right'
+ },
+ 4: {
+ alignment: 'right'
+ }
+ },
+ drawHorizontalLine: (index, size) => index > 0 && index < size,
+ border: {
+ bodyLeft: '│',
+ bodyRight: '│',
+ bodyJoin: '│',
+ joinLeft: '|',
+ joinRight: '|',
+ joinJoin: '|'
+ }
+ })
+}
+
+const experiments = {
+ 'http2 - request' () {
+ return makeParallelRequests(resolve => {
+ connect(dest.url, http2ClientOptions, (session) => {
+ const headers = {
+ ':path': '/',
+ ':method': 'GET',
+ ':scheme': 'https',
+ ':authority': `localhost:${dest.port}`
+ }
+
+ const request = session.request(headers)
+
+ request.pipe(
+ new Writable({
+ write (chunk, encoding, callback) {
+ callback()
+ }
+ })
+ ).on('finish', resolve)
+ })
+ })
+ },
+ 'undici - pipeline' () {
+ return makeParallelRequests(resolve => {
+ dispatcher
+ .pipeline(undiciOptions, data => {
+ return data.body
+ })
+ .end()
+ .pipe(
+ new Writable({
+ write (chunk, encoding, callback) {
+ callback()
+ }
+ })
+ )
+ .on('finish', resolve)
+ })
+ },
+ 'undici - request' () {
+ return makeParallelRequests(resolve => {
+ try {
+ dispatcher.request(undiciOptions).then(({ body }) => {
+ body
+ .pipe(
+ new Writable({
+ write (chunk, encoding, callback) {
+ callback()
+ }
+ })
+ )
+ .on('error', (err) => {
+ console.log('undici - request - dispatcher.request - body - error', err)
+ })
+ .on('finish', () => {
+ resolve()
+ })
+ })
+ } catch (err) {
+ console.error('undici - request - dispatcher.request - requestCount', err)
+ }
+ })
+ },
+ 'undici - stream' () {
+ return makeParallelRequests(resolve => {
+ return dispatcher
+ .stream(undiciOptions, () => {
+ return new Writable({
+ write (chunk, encoding, callback) {
+ callback()
+ }
+ })
+ })
+ .then(resolve)
+ })
+ },
+ 'undici - dispatch' () {
+ return makeParallelRequests(resolve => {
+ dispatcher.dispatch(undiciOptions, new SimpleRequest(resolve))
+ })
+ }
+}
+
+if (process.env.PORT) {
+ // fetch does not support the socket
+ experiments['undici - fetch'] = () => {
+ return makeParallelRequests(resolve => {
+ fetch(dest.url, {}).then(res => {
+ res.body.pipeTo(new WritableStream({ write () { }, close () { resolve() } }))
+ }).catch(console.log)
+ })
+ }
+}
+
+async function main () {
+ const { cronometro } = await import('cronometro')
+
+ cronometro(
+ experiments,
+ {
+ iterations,
+ errorThreshold,
+ print: false
+ },
+ (err, results) => {
+ if (err) {
+ throw err
+ }
+
+ console.log(printResults(results))
+ dispatcher.destroy()
+ }
+ )
+}
+
+if (isMainThread) {
+ main()
+} else {
+ module.exports = main
+}
diff --git a/benchmarks/benchmark-https.js b/benchmarks/benchmark-https.js
new file mode 100644
index 0000000..a364f0a
--- /dev/null
+++ b/benchmarks/benchmark-https.js
@@ -0,0 +1,319 @@
+'use strict'
+
+const https = require('https')
+const os = require('os')
+const path = require('path')
+const { readFileSync } = require('fs')
+const { table } = require('table')
+const { Writable } = require('stream')
+const { WritableStream } = require('stream/web')
+const { isMainThread } = require('worker_threads')
+
+const { Pool, Client, fetch, Agent, setGlobalDispatcher } = require('..')
+
+const ca = readFileSync(path.join(__dirname, '..', 'test', 'fixtures', 'ca.pem'), 'utf8')
+const servername = 'agent1'
+
+const iterations = (parseInt(process.env.SAMPLES, 10) || 10) + 1
+const errorThreshold = parseInt(process.env.ERROR_TRESHOLD, 10) || 3
+const connections = parseInt(process.env.CONNECTIONS, 10) || 50
+const pipelining = parseInt(process.env.PIPELINING, 10) || 10
+const parallelRequests = parseInt(process.env.PARALLEL, 10) || 100
+const headersTimeout = parseInt(process.env.HEADERS_TIMEOUT, 10) || 0
+const bodyTimeout = parseInt(process.env.BODY_TIMEOUT, 10) || 0
+const dest = {}
+
+if (process.env.PORT) {
+ dest.port = process.env.PORT
+ dest.url = `https://localhost:${process.env.PORT}`
+} else {
+ dest.url = 'https://localhost'
+ dest.socketPath = path.join(os.tmpdir(), 'undici.sock')
+}
+
+const httpsBaseOptions = {
+ ca,
+ servername,
+ protocol: 'https:',
+ hostname: 'localhost',
+ method: 'GET',
+ path: '/',
+ query: {
+ frappucino: 'muffin',
+ goat: 'scone',
+ pond: 'moose',
+ foo: ['bar', 'baz', 'bal'],
+ bool: true,
+ numberKey: 256
+ },
+ ...dest
+}
+
+const httpsNoKeepAliveOptions = {
+ ...httpsBaseOptions,
+ agent: new https.Agent({
+ keepAlive: false,
+ maxSockets: connections,
+ // rejectUnauthorized: false,
+ ca,
+ servername
+ })
+}
+
+const httpsKeepAliveOptions = {
+ ...httpsBaseOptions,
+ agent: new https.Agent({
+ keepAlive: true,
+ maxSockets: connections,
+ // rejectUnauthorized: false,
+ ca,
+ servername
+ })
+}
+
+const undiciOptions = {
+ path: '/',
+ method: 'GET',
+ headersTimeout,
+ bodyTimeout
+}
+
+const Class = connections > 1 ? Pool : Client
+const dispatcher = new Class(httpsBaseOptions.url, {
+ pipelining,
+ connections,
+ connect: {
+ // rejectUnauthorized: false,
+ ca,
+ servername
+ },
+ ...dest
+})
+
+setGlobalDispatcher(new Agent({
+ pipelining,
+ connections,
+ connect: {
+ // rejectUnauthorized: false,
+ ca,
+ servername
+ }
+}))
+
+class SimpleRequest {
+ constructor (resolve) {
+ this.dst = new Writable({
+ write (chunk, encoding, callback) {
+ callback()
+ }
+ }).on('finish', resolve)
+ }
+
+ onConnect (abort) { }
+
+ onHeaders (statusCode, headers, resume) {
+ this.dst.on('drain', resume)
+ }
+
+ onData (chunk) {
+ return this.dst.write(chunk)
+ }
+
+ onComplete () {
+ this.dst.end()
+ }
+
+ onError (err) {
+ throw err
+ }
+}
+
+function makeParallelRequests (cb) {
+ return Promise.all(Array.from(Array(parallelRequests)).map(() => new Promise(cb)))
+}
+
+function printResults (results) {
+ // Sort results by least performant first, then compare relative performances and also printing padding
+ let last
+
+ const rows = Object.entries(results)
+ // If any failed, put on the top of the list, otherwise order by mean, ascending
+ .sort((a, b) => (!a[1].success ? -1 : b[1].mean - a[1].mean))
+ .map(([name, result]) => {
+ if (!result.success) {
+ return [name, result.size, 'Errored', 'N/A', 'N/A']
+ }
+
+ // Calculate throughput and relative performance
+ const { size, mean, standardError } = result
+ const relative = last !== 0 ? (last / mean - 1) * 100 : 0
+
+ // Save the slowest for relative comparison
+ if (typeof last === 'undefined') {
+ last = mean
+ }
+
+ return [
+ name,
+ size,
+ `${((connections * 1e9) / mean).toFixed(2)} req/sec`,
+ `± ${((standardError / mean) * 100).toFixed(2)} %`,
+ relative > 0 ? `+ ${relative.toFixed(2)} %` : '-'
+ ]
+ })
+
+ console.log(results)
+
+ // Add the header row
+ rows.unshift(['Tests', 'Samples', 'Result', 'Tolerance', 'Difference with slowest'])
+
+ return table(rows, {
+ columns: {
+ 0: {
+ alignment: 'left'
+ },
+ 1: {
+ alignment: 'right'
+ },
+ 2: {
+ alignment: 'right'
+ },
+ 3: {
+ alignment: 'right'
+ },
+ 4: {
+ alignment: 'right'
+ }
+ },
+ drawHorizontalLine: (index, size) => index > 0 && index < size,
+ border: {
+ bodyLeft: '│',
+ bodyRight: '│',
+ bodyJoin: '│',
+ joinLeft: '|',
+ joinRight: '|',
+ joinJoin: '|'
+ }
+ })
+}
+
+const experiments = {
+ 'https - no keepalive' () {
+ return makeParallelRequests(resolve => {
+ https.get(httpsNoKeepAliveOptions, res => {
+ res
+ .pipe(
+ new Writable({
+ write (chunk, encoding, callback) {
+ callback()
+ }
+ })
+ )
+ .on('finish', resolve)
+ })
+ })
+ },
+ 'https - keepalive' () {
+ return makeParallelRequests(resolve => {
+ https.get(httpsKeepAliveOptions, res => {
+ res
+ .pipe(
+ new Writable({
+ write (chunk, encoding, callback) {
+ callback()
+ }
+ })
+ )
+ .on('finish', resolve)
+ })
+ })
+ },
+ 'undici - pipeline' () {
+ return makeParallelRequests(resolve => {
+ dispatcher
+ .pipeline(undiciOptions, data => {
+ return data.body
+ })
+ .end()
+ .pipe(
+ new Writable({
+ write (chunk, encoding, callback) {
+ callback()
+ }
+ })
+ )
+ .on('finish', resolve)
+ })
+ },
+ 'undici - request' () {
+ return makeParallelRequests(resolve => {
+ dispatcher.request(undiciOptions).then(({ body }) => {
+ body
+ .pipe(
+ new Writable({
+ write (chunk, encoding, callback) {
+ callback()
+ }
+ })
+ )
+ .on('finish', resolve)
+ })
+ })
+ },
+ 'undici - stream' () {
+ return makeParallelRequests(resolve => {
+ return dispatcher
+ .stream(undiciOptions, () => {
+ return new Writable({
+ write (chunk, encoding, callback) {
+ callback()
+ }
+ })
+ })
+ .then(resolve)
+ })
+ },
+ 'undici - dispatch' () {
+ return makeParallelRequests(resolve => {
+ dispatcher.dispatch(undiciOptions, new SimpleRequest(resolve))
+ })
+ }
+}
+
+if (process.env.PORT) {
+ // fetch does not support the socket
+ experiments['undici - fetch'] = () => {
+ return makeParallelRequests(resolve => {
+ fetch(dest.url, {}).then(res => {
+ res.body.pipeTo(new WritableStream({ write () { }, close () { resolve() } }))
+ }).catch(console.log)
+ })
+ }
+}
+
+async function main () {
+ const { cronometro } = await import('cronometro')
+
+ cronometro(
+ experiments,
+ {
+ iterations,
+ errorThreshold,
+ print: false
+ },
+ (err, results) => {
+ if (err) {
+ throw err
+ }
+
+ console.log(printResults(results))
+ dispatcher.destroy()
+ }
+ )
+}
+
+if (isMainThread) {
+ main()
+} else {
+ module.exports = main
+}
diff --git a/benchmarks/benchmark.js b/benchmarks/benchmark.js
new file mode 100644
index 0000000..5bf3d2e
--- /dev/null
+++ b/benchmarks/benchmark.js
@@ -0,0 +1,300 @@
+'use strict'
+
+const http = require('http')
+const os = require('os')
+const path = require('path')
+const { table } = require('table')
+const { Writable } = require('stream')
+const { WritableStream } = require('stream/web')
+const { isMainThread } = require('worker_threads')
+
+const { Pool, Client, fetch, Agent, setGlobalDispatcher } = require('..')
+
+const iterations = (parseInt(process.env.SAMPLES, 10) || 10) + 1
+const errorThreshold = parseInt(process.env.ERROR_TRESHOLD, 10) || 3
+const connections = parseInt(process.env.CONNECTIONS, 10) || 50
+const pipelining = parseInt(process.env.PIPELINING, 10) || 10
+const parallelRequests = parseInt(process.env.PARALLEL, 10) || 100
+const headersTimeout = parseInt(process.env.HEADERS_TIMEOUT, 10) || 0
+const bodyTimeout = parseInt(process.env.BODY_TIMEOUT, 10) || 0
+const dest = {}
+
+if (process.env.PORT) {
+ dest.port = process.env.PORT
+ dest.url = `http://localhost:${process.env.PORT}`
+} else {
+ dest.url = 'http://localhost'
+ dest.socketPath = path.join(os.tmpdir(), 'undici.sock')
+}
+
+const httpBaseOptions = {
+ protocol: 'http:',
+ hostname: 'localhost',
+ method: 'GET',
+ path: '/',
+ query: {
+ frappucino: 'muffin',
+ goat: 'scone',
+ pond: 'moose',
+ foo: ['bar', 'baz', 'bal'],
+ bool: true,
+ numberKey: 256
+ },
+ ...dest
+}
+
+const httpNoKeepAliveOptions = {
+ ...httpBaseOptions,
+ agent: new http.Agent({
+ keepAlive: false,
+ maxSockets: connections
+ })
+}
+
+const httpKeepAliveOptions = {
+ ...httpBaseOptions,
+ agent: new http.Agent({
+ keepAlive: true,
+ maxSockets: connections
+ })
+}
+
+const undiciOptions = {
+ path: '/',
+ method: 'GET',
+ headersTimeout,
+ bodyTimeout
+}
+
+const Class = connections > 1 ? Pool : Client
+const dispatcher = new Class(httpBaseOptions.url, {
+ pipelining,
+ connections,
+ ...dest
+})
+
+setGlobalDispatcher(new Agent({
+ pipelining,
+ connections,
+ connect: {
+ rejectUnauthorized: false
+ }
+}))
+
+class SimpleRequest {
+ constructor (resolve) {
+ this.dst = new Writable({
+ write (chunk, encoding, callback) {
+ callback()
+ }
+ }).on('finish', resolve)
+ }
+
+ onConnect (abort) { }
+
+ onHeaders (statusCode, headers, resume) {
+ this.dst.on('drain', resume)
+ }
+
+ onData (chunk) {
+ return this.dst.write(chunk)
+ }
+
+ onComplete () {
+ this.dst.end()
+ }
+
+ onError (err) {
+ throw err
+ }
+}
+
+function makeParallelRequests (cb) {
+ return Promise.all(Array.from(Array(parallelRequests)).map(() => new Promise(cb)))
+}
+
+function printResults (results) {
+ // Sort results by least performant first, then compare relative performances and also printing padding
+ let last
+
+ const rows = Object.entries(results)
+ // If any failed, put on the top of the list, otherwise order by mean, ascending
+ .sort((a, b) => (!a[1].success ? -1 : b[1].mean - a[1].mean))
+ .map(([name, result]) => {
+ if (!result.success) {
+ return [name, result.size, 'Errored', 'N/A', 'N/A']
+ }
+
+ // Calculate throughput and relative performance
+ const { size, mean, standardError } = result
+ const relative = last !== 0 ? (last / mean - 1) * 100 : 0
+
+ // Save the slowest for relative comparison
+ if (typeof last === 'undefined') {
+ last = mean
+ }
+
+ return [
+ name,
+ size,
+ `${((connections * 1e9) / mean).toFixed(2)} req/sec`,
+ `± ${((standardError / mean) * 100).toFixed(2)} %`,
+ relative > 0 ? `+ ${relative.toFixed(2)} %` : '-'
+ ]
+ })
+
+ console.log(results)
+
+ // Add the header row
+ rows.unshift(['Tests', 'Samples', 'Result', 'Tolerance', 'Difference with slowest'])
+
+ return table(rows, {
+ columns: {
+ 0: {
+ alignment: 'left'
+ },
+ 1: {
+ alignment: 'right'
+ },
+ 2: {
+ alignment: 'right'
+ },
+ 3: {
+ alignment: 'right'
+ },
+ 4: {
+ alignment: 'right'
+ }
+ },
+ drawHorizontalLine: (index, size) => index > 0 && index < size,
+ border: {
+ bodyLeft: '│',
+ bodyRight: '│',
+ bodyJoin: '│',
+ joinLeft: '|',
+ joinRight: '|',
+ joinJoin: '|'
+ }
+ })
+}
+
+const experiments = {
+ 'http - no keepalive' () {
+ return makeParallelRequests(resolve => {
+ http.get(httpNoKeepAliveOptions, res => {
+ res
+ .pipe(
+ new Writable({
+ write (chunk, encoding, callback) {
+ callback()
+ }
+ })
+ )
+ .on('finish', resolve)
+ })
+ })
+ },
+ 'http - keepalive' () {
+ return makeParallelRequests(resolve => {
+ http.get(httpKeepAliveOptions, res => {
+ res
+ .pipe(
+ new Writable({
+ write (chunk, encoding, callback) {
+ callback()
+ }
+ })
+ )
+ .on('finish', resolve)
+ })
+ })
+ },
+ 'undici - pipeline' () {
+ return makeParallelRequests(resolve => {
+ dispatcher
+ .pipeline(undiciOptions, data => {
+ return data.body
+ })
+ .end()
+ .pipe(
+ new Writable({
+ write (chunk, encoding, callback) {
+ callback()
+ }
+ })
+ )
+ .on('finish', resolve)
+ })
+ },
+ 'undici - request' () {
+ return makeParallelRequests(resolve => {
+ dispatcher.request(undiciOptions).then(({ body }) => {
+ body
+ .pipe(
+ new Writable({
+ write (chunk, encoding, callback) {
+ callback()
+ }
+ })
+ )
+ .on('finish', resolve)
+ })
+ })
+ },
+ 'undici - stream' () {
+ return makeParallelRequests(resolve => {
+ return dispatcher
+ .stream(undiciOptions, () => {
+ return new Writable({
+ write (chunk, encoding, callback) {
+ callback()
+ }
+ })
+ })
+ .then(resolve)
+ })
+ },
+ 'undici - dispatch' () {
+ return makeParallelRequests(resolve => {
+ dispatcher.dispatch(undiciOptions, new SimpleRequest(resolve))
+ })
+ }
+}
+
+if (process.env.PORT) {
+ // fetch does not support the socket
+ experiments['undici - fetch'] = () => {
+ return makeParallelRequests(resolve => {
+ fetch(dest.url).then(res => {
+ res.body.pipeTo(new WritableStream({ write () { }, close () { resolve() } }))
+ }).catch(console.log)
+ })
+ }
+}
+
+async function main () {
+ const { cronometro } = await import('cronometro')
+
+ cronometro(
+ experiments,
+ {
+ iterations,
+ errorThreshold,
+ print: false
+ },
+ (err, results) => {
+ if (err) {
+ throw err
+ }
+
+ console.log(printResults(results))
+ dispatcher.destroy()
+ }
+ )
+}
+
+if (isMainThread) {
+ main()
+} else {
+ module.exports = main
+}
diff --git a/benchmarks/server-http2.js b/benchmarks/server-http2.js
new file mode 100644
index 0000000..0be99cd
--- /dev/null
+++ b/benchmarks/server-http2.js
@@ -0,0 +1,49 @@
+'use strict'
+
+const { unlinkSync, readFileSync } = require('fs')
+const { createSecureServer } = require('http2')
+const os = require('os')
+const path = require('path')
+const cluster = require('cluster')
+
+const key = readFileSync(path.join(__dirname, '..', 'test', 'fixtures', 'key.pem'), 'utf8')
+const cert = readFileSync(path.join(__dirname, '..', 'test', 'fixtures', 'cert.pem'), 'utf8')
+
+const socketPath = path.join(os.tmpdir(), 'undici.sock')
+
+const port = process.env.PORT || socketPath
+const timeout = parseInt(process.env.TIMEOUT, 10) || 1
+const workers = parseInt(process.env.WORKERS) || os.cpus().length
+
+const sessionTimeout = 600e3 // 10 minutes
+
+if (cluster.isPrimary) {
+ try {
+ unlinkSync(socketPath)
+ } catch (_) {
+ // Do nothing if the socket does not exist
+ }
+
+ for (let i = 0; i < workers; i++) {
+ cluster.fork()
+ }
+} else {
+ const buf = Buffer.alloc(64 * 1024, '_')
+ const server = createSecureServer(
+ {
+ key,
+ cert,
+ allowHTTP1: true,
+ sessionTimeout
+ },
+ (req, res) => {
+ setTimeout(() => {
+ res.end(buf)
+ }, timeout)
+ }
+ )
+
+ server.keepAliveTimeout = 600e3
+
+ server.listen(port)
+}
diff --git a/benchmarks/server-https.js b/benchmarks/server-https.js
new file mode 100644
index 0000000..f0275d9
--- /dev/null
+++ b/benchmarks/server-https.js
@@ -0,0 +1,41 @@
+'use strict'
+
+const { unlinkSync, readFileSync } = require('fs')
+const { createServer } = require('https')
+const os = require('os')
+const path = require('path')
+const cluster = require('cluster')
+
+const key = readFileSync(path.join(__dirname, '..', 'test', 'fixtures', 'key.pem'), 'utf8')
+const cert = readFileSync(path.join(__dirname, '..', 'test', 'fixtures', 'cert.pem'), 'utf8')
+
+const socketPath = path.join(os.tmpdir(), 'undici.sock')
+
+const port = process.env.PORT || socketPath
+const timeout = parseInt(process.env.TIMEOUT, 10) || 1
+const workers = parseInt(process.env.WORKERS) || os.cpus().length
+
+if (cluster.isPrimary) {
+ try {
+ unlinkSync(socketPath)
+ } catch (_) {
+ // Do nothing if the socket does not exist
+ }
+
+ for (let i = 0; i < workers; i++) {
+ cluster.fork()
+ }
+} else {
+ const buf = Buffer.alloc(64 * 1024, '_')
+ const server = createServer({
+ key,
+ cert,
+ keepAliveTimeout: 600e3
+ }, (req, res) => {
+ setTimeout(() => {
+ res.end(buf)
+ }, timeout)
+ })
+
+ server.listen(port)
+}
diff --git a/benchmarks/server.js b/benchmarks/server.js
new file mode 100644
index 0000000..e1a32e8
--- /dev/null
+++ b/benchmarks/server.js
@@ -0,0 +1,33 @@
+'use strict'
+
+const { unlinkSync } = require('fs')
+const { createServer } = require('http')
+const os = require('os')
+const path = require('path')
+const cluster = require('cluster')
+
+const socketPath = path.join(os.tmpdir(), 'undici.sock')
+
+const port = process.env.PORT || socketPath
+const timeout = parseInt(process.env.TIMEOUT, 10) || 1
+const workers = parseInt(process.env.WORKERS) || os.cpus().length
+
+if (cluster.isPrimary) {
+ try {
+ unlinkSync(socketPath)
+ } catch (_) {
+ // Do nothing if the socket does not exist
+ }
+
+ for (let i = 0; i < workers; i++) {
+ cluster.fork()
+ }
+} else {
+ const buf = Buffer.alloc(64 * 1024, '_')
+ const server = createServer((req, res) => {
+ setTimeout(function () {
+ res.end(buf)
+ }, timeout)
+ }).listen(port)
+ server.keepAliveTimeout = 600e3
+}
diff --git a/benchmarks/wait.js b/benchmarks/wait.js
new file mode 100644
index 0000000..771f9f2
--- /dev/null
+++ b/benchmarks/wait.js
@@ -0,0 +1,22 @@
+'use strict'
+
+const os = require('os')
+const path = require('path')
+const waitOn = require('wait-on')
+
+const socketPath = path.join(os.tmpdir(), 'undici.sock')
+
+let resources
+if (process.env.PORT) {
+ resources = [`http-get://localhost:${process.env.PORT}/`]
+} else {
+ resources = [`http-get://unix:${socketPath}:/`]
+}
+
+waitOn({
+ resources,
+ timeout: 5000
+}).catch((err) => {
+ console.error(err)
+ process.exit(1)
+})
diff --git a/binary-search/.gitignore b/binary-search/.gitignore
new file mode 100644
index 0000000..07e6e47
--- /dev/null
+++ b/binary-search/.gitignore
@@ -0,0 +1 @@
+/node_modules
diff --git a/binary-search/.travis.yml b/binary-search/.travis.yml
new file mode 100644
index 0000000..795ac70
--- /dev/null
+++ b/binary-search/.travis.yml
@@ -0,0 +1,6 @@
+language: node_js
+node_js:
+ - '6'
+cache:
+ directories:
+ - node_modules
diff --git a/binary-search/README.md b/binary-search/README.md
new file mode 100644
index 0000000..e02805a
--- /dev/null
+++ b/binary-search/README.md
@@ -0,0 +1,46 @@
+binary-search
+=============
+
+This is a really tiny, stupid, simple binary search library for Node.JS. We
+wrote it because existing solutions were bloated and incorrect.
+
+This version is a straight port of the Java version mentioned by Joshua Bloch
+in his article, [Nearly All Binary Searches and Merge Sorts are Broken](http://googleresearch.blogspot.com/2006/06/extra-extra-read-all-about-it-nearly.html).
+
+Thanks to [Conrad Irwin](https://github.com/ConradIrwin) and [Michael
+Marino](https://github.com/mgmarino) for, ironically, pointing out bugs.
+
+Example
+-------
+
+```js
+var bs = require("binary-search");
+
+bs([1, 2, 3, 4], 3, function(element, needle) { return element - needle; });
+// => 2
+
+bs([1, 2, 4, 5], 3, function(element, needle) { return element - needle; });
+// => -3
+```
+
+Be advised that passing in a comparator function is *required*. Since you're
+probably using one for your sort function anyway, this isn't a big deal.
+
+The comparator takes a 1st and 2nd argument of element and needle, respectively.
+
+The comparator also takes a 3rd and 4th argument, the current index and array,
+respectively. You shouldn't normally need the index or array to compare values,
+but it's there if you do.
+
+You may also, optionally, specify an input range as the final two parameters,
+in case you want to limit the search to a particular range of inputs. However,
+be advised that this is generally a bad idea (but sometimes bad ideas are
+necessary).
+
+License
+-------
+
+To the extent possible by law, The Dark Sky Company, LLC has [waived all
+copyright and related or neighboring rights][cc0] to this library.
+
+[cc0]: http://creativecommons.org/publicdomain/zero/1.0/
diff --git a/binary-search/binary-search.d.ts b/binary-search/binary-search.d.ts
new file mode 100644
index 0000000..0395d93
--- /dev/null
+++ b/binary-search/binary-search.d.ts
@@ -0,0 +1,22 @@
+//Typescript type definition for:
+//https://github.com/darkskyapp/binary-search
+declare module 'binary-search' {
+
+function binarySearch<A, B>(
+ haystack: ArrayLike<A>,
+ needle: B,
+ comparator: (a: A, b: B, index?: number, haystack?: A[]) => any,
+ // Notes about comparator return value:
+ // * when a<b the comparator's returned value should be:
+ // * negative number or a value such that `+value` is a negative number
+ // * examples: `-1` or the string `"-1"`
+ // * when a>b the comparator's returned value should be:
+ // * positive number or a value such that `+value` is a positive number
+ // * examples: `1` or the string `"1"`
+ // * when a===b
+ // * any value other than the return cases for a<b and a>b
+ // * examples: undefined, NaN, 'abc'
+ low?: number,
+ high?: number): number; //returns index of found result or number < 0 if not found
+export = binarySearch;
+}
diff --git a/binary-search/index.js b/binary-search/index.js
new file mode 100644
index 0000000..bc281ca
--- /dev/null
+++ b/binary-search/index.js
@@ -0,0 +1,45 @@
+module.exports = function(haystack, needle, comparator, low, high) {
+ var mid, cmp;
+
+ if(low === undefined)
+ low = 0;
+
+ else {
+ low = low|0;
+ if(low < 0 || low >= haystack.length)
+ throw new RangeError("invalid lower bound");
+ }
+
+ if(high === undefined)
+ high = haystack.length - 1;
+
+ else {
+ high = high|0;
+ if(high < low || high >= haystack.length)
+ throw new RangeError("invalid upper bound");
+ }
+
+ while(low <= high) {
+ // The naive `low + high >>> 1` could fail for array lengths > 2**31
+ // because `>>>` converts its operands to int32. `low + (high - low >>> 1)`
+ // works for array lengths <= 2**32-1 which is also Javascript's max array
+ // length.
+ mid = low + ((high - low) >>> 1);
+ cmp = +comparator(haystack[mid], needle, mid, haystack);
+
+ // Too low.
+ if(cmp < 0.0)
+ low = mid + 1;
+
+ // Too high.
+ else if(cmp > 0.0)
+ high = mid - 1;
+
+ // Key found.
+ else
+ return mid;
+ }
+
+ // Key not found.
+ return ~low;
+}
diff --git a/binary-search/package.json b/binary-search/package.json
new file mode 100644
index 0000000..9a91ed5
--- /dev/null
+++ b/binary-search/package.json
@@ -0,0 +1,28 @@
+{
+ "name": "binary-search",
+ "version": "1.3.6",
+ "description": "tiny binary search function with comparators",
+ "license": "CC0-1.0",
+ "typings": "./binary-search.d.ts",
+ "author": {
+ "name": "The Dark Sky Company, LLC",
+ "email": "support@darkskyapp.com"
+ },
+ "contributors": [
+ {
+ "name": "Darcy Parker",
+ "web": "https://github.com/darcyparker"
+ }
+ ],
+ "repository": {
+ "type": "git",
+ "url": "git://github.com/darkskyapp/binary-search.git"
+ },
+ "devDependencies": {
+ "chai": "^4.2.0",
+ "mocha": "^5.2.0"
+ },
+ "scripts": {
+ "test": "mocha"
+ }
+}
diff --git a/binary-search/test.js b/binary-search/test.js
new file mode 100644
index 0000000..95a497f
--- /dev/null
+++ b/binary-search/test.js
@@ -0,0 +1,46 @@
+var expect = require("chai").expect;
+
+describe("binarysearch", function() {
+ var bs = require("./"),
+ arr = [1, 2, 2, 2, 3, 5, 9],
+ cmp = function(a, b) { return a - b; };
+
+ it("should bail if not passed an array", function() {
+ expect(function() { bs(undefined, 3, cmp); }).to.throw(TypeError);
+ });
+
+ it("should bail if not passed a comparator", function() {
+ expect(function() { bs(arr, 3, undefined); }).to.throw(TypeError);
+ });
+
+ it("should return the index of an item in a sorted array", function() {
+ expect(bs(arr, 3, cmp)).to.equal(4);
+ });
+
+ it("should return the index of where the item would go plus one, negated, if the item is not found", function() {
+ expect(bs(arr, 4, cmp)).to.equal(-6);
+ });
+
+ it("should return any valid index if an item exists multiple times in the array", function() {
+ expect(bs(arr, 2, cmp)).to.equal(3);
+ });
+
+ it("should work even on empty arrays", function() {
+ expect(bs([], 42, cmp)).to.equal(-1);
+ });
+
+ it("should work even on arrays of doubles", function() {
+ expect(bs([0.0, 0.1, 0.2, 0.3, 0.4], 0.25, cmp)).to.equal(-4);
+ });
+
+ it("should pass the index and array parameters to the comparator", function() {
+ var indexes = [],
+ indexCmp = function(a, b, i, array) {
+ expect(array).to.equal(arr);
+ indexes.push(i);
+ return cmp(a, b);
+ };
+ bs(arr, 3, indexCmp);
+ expect(indexes).to.deep.equal([3, 5, 4])
+ });
+});
diff --git a/build/Dockerfile b/build/Dockerfile
new file mode 100644
index 0000000..5438b73
--- /dev/null
+++ b/build/Dockerfile
@@ -0,0 +1,18 @@
+FROM node:20-alpine@sha256:4559bc033338938e54d0a3c2f0d7c3ad7d1d13c28c4c405b85c6b3a26f4ce5f7
+
+ARG UID=1000
+ARG GID=1000
+
+RUN apk add -U clang lld wasi-sdk
+RUN mkdir /home/node/undici
+
+WORKDIR /home/node/undici
+
+COPY package.json .
+COPY build build
+COPY deps deps
+COPY lib lib
+
+RUN npm i
+
+USER node
diff --git a/build/wasm.js b/build/wasm.js
new file mode 100644
index 0000000..fd90ac2
--- /dev/null
+++ b/build/wasm.js
@@ -0,0 +1,101 @@
+'use strict'
+
+const { execSync } = require('child_process')
+const { writeFileSync, readFileSync } = require('fs')
+const { join, resolve } = require('path')
+
+const ROOT = resolve(__dirname, '../')
+const WASM_SRC = resolve(__dirname, '../deps/llhttp')
+const WASM_OUT = resolve(__dirname, '../lib/llhttp')
+const DOCKERFILE = resolve(__dirname, './Dockerfile')
+
+let platform = process.env.WASM_PLATFORM
+if (!platform && process.argv[2]) {
+ platform = execSync('docker info -f "{{.OSType}}/{{.Architecture}}"').toString().trim()
+}
+
+if (process.argv[2] === '--prebuild') {
+ const cmd = `docker build --platform=${platform.toString().trim()} -t llhttp_wasm_builder -f ${DOCKERFILE} ${ROOT}`
+
+ console.log(`> ${cmd}\n\n`)
+ execSync(cmd, { stdio: 'inherit' })
+
+ process.exit(0)
+}
+
+if (process.argv[2] === '--docker') {
+ let cmd = `docker run --rm -it --platform=${platform.toString().trim()}`
+ if (process.platform === 'linux') {
+ cmd += ` --user ${process.getuid()}:${process.getegid()}`
+ }
+
+ cmd += ` --mount type=bind,source=${ROOT}/lib/llhttp,target=/home/node/undici/lib/llhttp llhttp_wasm_builder node build/wasm.js`
+ console.log(`> ${cmd}\n\n`)
+ execSync(cmd, { stdio: 'inherit' })
+ process.exit(0)
+}
+
+// Gather information about the tools used for the build
+const buildInfo = execSync('apk info -v').toString()
+if (!buildInfo.includes('wasi-sdk')) {
+ console.log('Failed to generate build environment information')
+ process.exit(-1)
+}
+writeFileSync(join(WASM_OUT, 'wasm_build_env.txt'), buildInfo)
+
+// Build wasm binary
+execSync(`clang \
+ --sysroot=/usr/share/wasi-sysroot \
+ -target wasm32-unknown-wasi \
+ -Ofast \
+ -fno-exceptions \
+ -fvisibility=hidden \
+ -mexec-model=reactor \
+ -Wl,-error-limit=0 \
+ -Wl,-O3 \
+ -Wl,--lto-O3 \
+ -Wl,--strip-all \
+ -Wl,--allow-undefined \
+ -Wl,--export-dynamic \
+ -Wl,--export-table \
+ -Wl,--export=malloc \
+ -Wl,--export=free \
+ -Wl,--no-entry \
+ ${join(WASM_SRC, 'src')}/*.c \
+ -I${join(WASM_SRC, 'include')} \
+ -o ${join(WASM_OUT, 'llhttp.wasm')}`, { stdio: 'inherit' })
+
+const base64Wasm = readFileSync(join(WASM_OUT, 'llhttp.wasm')).toString('base64')
+writeFileSync(
+ join(WASM_OUT, 'llhttp-wasm.js'),
+ `module.exports = '${base64Wasm}'\n`
+)
+
+// Build wasm simd binary
+execSync(`clang \
+ --sysroot=/usr/share/wasi-sysroot \
+ -target wasm32-unknown-wasi \
+ -msimd128 \
+ -Ofast \
+ -fno-exceptions \
+ -fvisibility=hidden \
+ -mexec-model=reactor \
+ -Wl,-error-limit=0 \
+ -Wl,-O3 \
+ -Wl,--lto-O3 \
+ -Wl,--strip-all \
+ -Wl,--allow-undefined \
+ -Wl,--export-dynamic \
+ -Wl,--export-table \
+ -Wl,--export=malloc \
+ -Wl,--export=free \
+ -Wl,--no-entry \
+ ${join(WASM_SRC, 'src')}/*.c \
+ -I${join(WASM_SRC, 'include')} \
+ -o ${join(WASM_OUT, 'llhttp_simd.wasm')}`, { stdio: 'inherit' })
+
+const base64WasmSimd = readFileSync(join(WASM_OUT, 'llhttp_simd.wasm')).toString('base64')
+writeFileSync(
+ join(WASM_OUT, 'llhttp_simd-wasm.js'),
+ `module.exports = '${base64WasmSimd}'\n`
+)
diff --git a/docs/api/Agent.md b/docs/api/Agent.md
new file mode 100644
index 0000000..dd5d99b
--- /dev/null
+++ b/docs/api/Agent.md
@@ -0,0 +1,80 @@
+# Agent
+
+Extends: `undici.Dispatcher`
+
+Agent allow dispatching requests against multiple different origins.
+
+Requests are not guaranteed to be dispatched in order of invocation.
+
+## `new undici.Agent([options])`
+
+Arguments:
+
+* **options** `AgentOptions` (optional)
+
+Returns: `Agent`
+
+### Parameter: `AgentOptions`
+
+Extends: [`PoolOptions`](Pool.md#parameter-pooloptions)
+
+* **factory** `(origin: URL, opts: Object) => Dispatcher` - Default: `(origin, opts) => new Pool(origin, opts)`
+* **maxRedirections** `Integer` - Default: `0`. The number of HTTP redirection to follow unless otherwise specified in `DispatchOptions`.
+* **interceptors** `{ Agent: DispatchInterceptor[] }` - Default: `[RedirectInterceptor]` - A list of interceptors that are applied to the dispatch method. Additional logic can be applied (such as, but not limited to: 302 status code handling, authentication, cookies, compression and caching). Note that the behavior of interceptors is Experimental and might change at any given time.
+
+## Instance Properties
+
+### `Agent.closed`
+
+Implements [Client.closed](Client.md#clientclosed)
+
+### `Agent.destroyed`
+
+Implements [Client.destroyed](Client.md#clientdestroyed)
+
+## Instance Methods
+
+### `Agent.close([callback])`
+
+Implements [`Dispatcher.close([callback])`](Dispatcher.md#dispatcherclosecallback-promise).
+
+### `Agent.destroy([error, callback])`
+
+Implements [`Dispatcher.destroy([error, callback])`](Dispatcher.md#dispatcherdestroyerror-callback-promise).
+
+### `Agent.dispatch(options, handler: AgentDispatchOptions)`
+
+Implements [`Dispatcher.dispatch(options, handler)`](Dispatcher.md#dispatcherdispatchoptions-handler).
+
+#### Parameter: `AgentDispatchOptions`
+
+Extends: [`DispatchOptions`](Dispatcher.md#parameter-dispatchoptions)
+
+* **origin** `string | URL`
+* **maxRedirections** `Integer`.
+
+Implements [`Dispatcher.destroy([error, callback])`](Dispatcher.md#dispatcherdestroyerror-callback-promise).
+
+### `Agent.connect(options[, callback])`
+
+See [`Dispatcher.connect(options[, callback])`](Dispatcher.md#dispatcherconnectoptions-callback).
+
+### `Agent.dispatch(options, handler)`
+
+Implements [`Dispatcher.dispatch(options, handler)`](Dispatcher.md#dispatcherdispatchoptions-handler).
+
+### `Agent.pipeline(options, handler)`
+
+See [`Dispatcher.pipeline(options, handler)`](Dispatcher.md#dispatcherpipelineoptions-handler).
+
+### `Agent.request(options[, callback])`
+
+See [`Dispatcher.request(options [, callback])`](Dispatcher.md#dispatcherrequestoptions-callback).
+
+### `Agent.stream(options, factory[, callback])`
+
+See [`Dispatcher.stream(options, factory[, callback])`](Dispatcher.md#dispatcherstreamoptions-factory-callback).
+
+### `Agent.upgrade(options[, callback])`
+
+See [`Dispatcher.upgrade(options[, callback])`](Dispatcher.md#dispatcherupgradeoptions-callback).
diff --git a/docs/api/BalancedPool.md b/docs/api/BalancedPool.md
new file mode 100644
index 0000000..290c734
--- /dev/null
+++ b/docs/api/BalancedPool.md
@@ -0,0 +1,99 @@
+# Class: BalancedPool
+
+Extends: `undici.Dispatcher`
+
+A pool of [Pool](Pool.md) instances connected to multiple upstreams.
+
+Requests are not guaranteed to be dispatched in order of invocation.
+
+## `new BalancedPool(upstreams [, options])`
+
+Arguments:
+
+* **upstreams** `URL | string | string[]` - It should only include the **protocol, hostname, and port**.
+* **options** `BalancedPoolOptions` (optional)
+
+### Parameter: `BalancedPoolOptions`
+
+Extends: [`PoolOptions`](Pool.md#parameter-pooloptions)
+
+* **factory** `(origin: URL, opts: Object) => Dispatcher` - Default: `(origin, opts) => new Pool(origin, opts)`
+
+The `PoolOptions` are passed to each of the `Pool` instances being created.
+## Instance Properties
+
+### `BalancedPool.upstreams`
+
+Returns an array of upstreams that were previously added.
+
+### `BalancedPool.closed`
+
+Implements [Client.closed](Client.md#clientclosed)
+
+### `BalancedPool.destroyed`
+
+Implements [Client.destroyed](Client.md#clientdestroyed)
+
+### `Pool.stats`
+
+Returns [`PoolStats`](PoolStats.md) instance for this pool.
+
+## Instance Methods
+
+### `BalancedPool.addUpstream(upstream)`
+
+Add an upstream.
+
+Arguments:
+
+* **upstream** `string` - It should only include the **protocol, hostname, and port**.
+
+### `BalancedPool.removeUpstream(upstream)`
+
+Removes an upstream that was previously addded.
+
+### `BalancedPool.close([callback])`
+
+Implements [`Dispatcher.close([callback])`](Dispatcher.md#dispatcherclosecallback-promise).
+
+### `BalancedPool.destroy([error, callback])`
+
+Implements [`Dispatcher.destroy([error, callback])`](Dispatcher.md#dispatcherdestroyerror-callback-promise).
+
+### `BalancedPool.connect(options[, callback])`
+
+See [`Dispatcher.connect(options[, callback])`](Dispatcher.md#dispatcherconnectoptions-callback).
+
+### `BalancedPool.dispatch(options, handlers)`
+
+Implements [`Dispatcher.dispatch(options, handlers)`](Dispatcher.md#dispatcherdispatchoptions-handler).
+
+### `BalancedPool.pipeline(options, handler)`
+
+See [`Dispatcher.pipeline(options, handler)`](Dispatcher.md#dispatcherpipelineoptions-handler).
+
+### `BalancedPool.request(options[, callback])`
+
+See [`Dispatcher.request(options [, callback])`](Dispatcher.md#dispatcherrequestoptions-callback).
+
+### `BalancedPool.stream(options, factory[, callback])`
+
+See [`Dispatcher.stream(options, factory[, callback])`](Dispatcher.md#dispatcherstreamoptions-factory-callback).
+
+### `BalancedPool.upgrade(options[, callback])`
+
+See [`Dispatcher.upgrade(options[, callback])`](Dispatcher.md#dispatcherupgradeoptions-callback).
+
+## Instance Events
+
+### Event: `'connect'`
+
+See [Dispatcher Event: `'connect'`](Dispatcher.md#event-connect).
+
+### Event: `'disconnect'`
+
+See [Dispatcher Event: `'disconnect'`](Dispatcher.md#event-disconnect).
+
+### Event: `'drain'`
+
+See [Dispatcher Event: `'drain'`](Dispatcher.md#event-drain).
diff --git a/docs/api/CacheStorage.md b/docs/api/CacheStorage.md
new file mode 100644
index 0000000..08ee99f
--- /dev/null
+++ b/docs/api/CacheStorage.md
@@ -0,0 +1,30 @@
+# CacheStorage
+
+Undici exposes a W3C spec-compliant implementation of [CacheStorage](https://developer.mozilla.org/en-US/docs/Web/API/CacheStorage) and [Cache](https://developer.mozilla.org/en-US/docs/Web/API/Cache).
+
+## Opening a Cache
+
+Undici exports a top-level CacheStorage instance. You can open a new Cache, or duplicate a Cache with an existing name, by using `CacheStorage.prototype.open`. If you open a Cache with the same name as an already-existing Cache, its list of cached Responses will be shared between both instances.
+
+```mjs
+import { caches } from 'undici'
+
+const cache_1 = await caches.open('v1')
+const cache_2 = await caches.open('v1')
+
+// Although .open() creates a new instance,
+assert(cache_1 !== cache_2)
+// The same Response is matched in both.
+assert.deepStrictEqual(await cache_1.match('/req'), await cache_2.match('/req'))
+```
+
+## Deleting a Cache
+
+If a Cache is deleted, the cached Responses/Requests can still be used.
+
+```mjs
+const response = await cache_1.match('/req')
+await caches.delete('v1')
+
+await response.text() // the Response's body
+```
diff --git a/docs/api/Client.md b/docs/api/Client.md
new file mode 100644
index 0000000..b9e26f0
--- /dev/null
+++ b/docs/api/Client.md
@@ -0,0 +1,273 @@
+# Class: Client
+
+Extends: `undici.Dispatcher`
+
+A basic HTTP/1.1 client, mapped on top a single TCP/TLS connection. Pipelining is disabled by default.
+
+Requests are not guaranteed to be dispatched in order of invocation.
+
+## `new Client(url[, options])`
+
+Arguments:
+
+* **url** `URL | string` - Should only include the **protocol, hostname, and port**.
+* **options** `ClientOptions` (optional)
+
+Returns: `Client`
+
+### Parameter: `ClientOptions`
+
+> âš ï¸ Warning: The `H2` support is experimental.
+
+* **bodyTimeout** `number | null` (optional) - Default: `300e3` - The timeout after which a request will time out, in milliseconds. Monitors time between receiving body data. Use `0` to disable it entirely. Defaults to 300 seconds.
+* **headersTimeout** `number | null` (optional) - Default: `300e3` - The amount of time, in milliseconds, the parser will wait to receive the complete HTTP headers while not sending the request. Defaults to 300 seconds.
+* **keepAliveMaxTimeout** `number | null` (optional) - Default: `600e3` - The maximum allowed `keepAliveTimeout`, in milliseconds, when overridden by *keep-alive* hints from the server. Defaults to 10 minutes.
+* **keepAliveTimeout** `number | null` (optional) - Default: `4e3` - The timeout, in milliseconds, after which a socket without active requests will time out. Monitors time between activity on a connected socket. This value may be overridden by *keep-alive* hints from the server. See [MDN: HTTP - Headers - Keep-Alive directives](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Keep-Alive#directives) for more details. Defaults to 4 seconds.
+* **keepAliveTimeoutThreshold** `number | null` (optional) - Default: `1e3` - A number of milliseconds subtracted from server *keep-alive* hints when overriding `keepAliveTimeout` to account for timing inaccuracies caused by e.g. transport latency. Defaults to 1 second.
+* **maxHeaderSize** `number | null` (optional) - Default: `--max-http-header-size` or `16384` - The maximum length of request headers in bytes. Defaults to Node.js' --max-http-header-size or 16KiB.
+* **maxResponseSize** `number | null` (optional) - Default: `-1` - The maximum length of response body in bytes. Set to `-1` to disable.
+* **pipelining** `number | null` (optional) - Default: `1` - The amount of concurrent requests to be sent over the single TCP/TLS connection according to [RFC7230](https://tools.ietf.org/html/rfc7230#section-6.3.2). Carefully consider your workload and environment before enabling concurrent requests as pipelining may reduce performance if used incorrectly. Pipelining is sensitive to network stack settings as well as head of line blocking caused by e.g. long running requests. Set to `0` to disable keep-alive connections.
+* **connect** `ConnectOptions | Function | null` (optional) - Default: `null`.
+* **strictContentLength** `Boolean` (optional) - Default: `true` - Whether to treat request content length mismatches as errors. If true, an error is thrown when the request content-length header doesn't match the length of the request body.
+* **interceptors** `{ Client: DispatchInterceptor[] }` - Default: `[RedirectInterceptor]` - A list of interceptors that are applied to the dispatch method. Additional logic can be applied (such as, but not limited to: 302 status code handling, authentication, cookies, compression and caching). Note that the behavior of interceptors is Experimental and might change at any given time.
+* **autoSelectFamily**: `boolean` (optional) - Default: depends on local Node version, on Node 18.13.0 and above is `false`. Enables a family autodetection algorithm that loosely implements section 5 of [RFC 8305](https://tools.ietf.org/html/rfc8305#section-5). See [here](https://nodejs.org/api/net.html#socketconnectoptions-connectlistener) for more details. This option is ignored if not supported by the current Node version.
+* **autoSelectFamilyAttemptTimeout**: `number` - Default: depends on local Node version, on Node 18.13.0 and above is `250`. The amount of time in milliseconds to wait for a connection attempt to finish before trying the next address when using the `autoSelectFamily` option. See [here](https://nodejs.org/api/net.html#socketconnectoptions-connectlistener) for more details.
+* **allowH2**: `boolean` - Default: `false`. Enables support for H2 if the server has assigned bigger priority to it through ALPN negotiation.
+* **maxConcurrentStreams**: `number` - Default: `100`. Dictates the maximum number of concurrent streams for a single H2 session. It can be overridden by a SETTINGS remote frame.
+
+#### Parameter: `ConnectOptions`
+
+Every Tls option, see [here](https://nodejs.org/api/tls.html#tls_tls_connect_options_callback).
+Furthermore, the following options can be passed:
+
+* **socketPath** `string | null` (optional) - Default: `null` - An IPC endpoint, either Unix domain socket or Windows named pipe.
+* **maxCachedSessions** `number | null` (optional) - Default: `100` - Maximum number of TLS cached sessions. Use 0 to disable TLS session caching. Default: 100.
+* **timeout** `number | null` (optional) - In milliseconds, Default `10e3`.
+* **servername** `string | null` (optional)
+* **keepAlive** `boolean | null` (optional) - Default: `true` - TCP keep-alive enabled
+* **keepAliveInitialDelay** `number | null` (optional) - Default: `60000` - TCP keep-alive interval for the socket in milliseconds
+
+### Example - Basic Client instantiation
+
+This will instantiate the undici Client, but it will not connect to the origin until something is queued. Consider using `client.connect` to prematurely connect to the origin, or just call `client.request`.
+
+```js
+'use strict'
+import { Client } from 'undici'
+
+const client = new Client('http://localhost:3000')
+```
+
+### Example - Custom connector
+
+This will allow you to perform some additional check on the socket that will be used for the next request.
+
+```js
+'use strict'
+import { Client, buildConnector } from 'undici'
+
+const connector = buildConnector({ rejectUnauthorized: false })
+const client = new Client('https://localhost:3000', {
+ connect (opts, cb) {
+ connector(opts, (err, socket) => {
+ if (err) {
+ cb(err)
+ } else if (/* assertion */) {
+ socket.destroy()
+ cb(new Error('kaboom'))
+ } else {
+ cb(null, socket)
+ }
+ })
+ }
+})
+```
+
+## Instance Methods
+
+### `Client.close([callback])`
+
+Implements [`Dispatcher.close([callback])`](Dispatcher.md#dispatcherclosecallback-promise).
+
+### `Client.destroy([error, callback])`
+
+Implements [`Dispatcher.destroy([error, callback])`](Dispatcher.md#dispatcherdestroyerror-callback-promise).
+
+Waits until socket is closed before invoking the callback (or returning a promise if no callback is provided).
+
+### `Client.connect(options[, callback])`
+
+See [`Dispatcher.connect(options[, callback])`](Dispatcher.md#dispatcherconnectoptions-callback).
+
+### `Client.dispatch(options, handlers)`
+
+Implements [`Dispatcher.dispatch(options, handlers)`](Dispatcher.md#dispatcherdispatchoptions-handler).
+
+### `Client.pipeline(options, handler)`
+
+See [`Dispatcher.pipeline(options, handler)`](Dispatcher.md#dispatcherpipelineoptions-handler).
+
+### `Client.request(options[, callback])`
+
+See [`Dispatcher.request(options [, callback])`](Dispatcher.md#dispatcherrequestoptions-callback).
+
+### `Client.stream(options, factory[, callback])`
+
+See [`Dispatcher.stream(options, factory[, callback])`](Dispatcher.md#dispatcherstreamoptions-factory-callback).
+
+### `Client.upgrade(options[, callback])`
+
+See [`Dispatcher.upgrade(options[, callback])`](Dispatcher.md#dispatcherupgradeoptions-callback).
+
+## Instance Properties
+
+### `Client.closed`
+
+* `boolean`
+
+`true` after `client.close()` has been called.
+
+### `Client.destroyed`
+
+* `boolean`
+
+`true` after `client.destroyed()` has been called or `client.close()` has been called and the client shutdown has completed.
+
+### `Client.pipelining`
+
+* `number`
+
+Property to get and set the pipelining factor.
+
+## Instance Events
+
+### Event: `'connect'`
+
+See [Dispatcher Event: `'connect'`](Dispatcher.md#event-connect).
+
+Parameters:
+
+* **origin** `URL`
+* **targets** `Array<Dispatcher>`
+
+Emitted when a socket has been created and connected. The client will connect once `client.size > 0`.
+
+#### Example - Client connect event
+
+```js
+import { createServer } from 'http'
+import { Client } from 'undici'
+import { once } from 'events'
+
+const server = createServer((request, response) => {
+ response.end('Hello, World!')
+}).listen()
+
+await once(server, 'listening')
+
+const client = new Client(`http://localhost:${server.address().port}`)
+
+client.on('connect', (origin) => {
+ console.log(`Connected to ${origin}`) // should print before the request body statement
+})
+
+try {
+ const { body } = await client.request({
+ path: '/',
+ method: 'GET'
+ })
+ body.setEncoding('utf-8')
+ body.on('data', console.log)
+ client.close()
+ server.close()
+} catch (error) {
+ console.error(error)
+ client.close()
+ server.close()
+}
+```
+
+### Event: `'disconnect'`
+
+See [Dispatcher Event: `'disconnect'`](Dispatcher.md#event-disconnect).
+
+Parameters:
+
+* **origin** `URL`
+* **targets** `Array<Dispatcher>`
+* **error** `Error`
+
+Emitted when socket has disconnected. The error argument of the event is the error which caused the socket to disconnect. The client will reconnect if or once `client.size > 0`.
+
+#### Example - Client disconnect event
+
+```js
+import { createServer } from 'http'
+import { Client } from 'undici'
+import { once } from 'events'
+
+const server = createServer((request, response) => {
+ response.destroy()
+}).listen()
+
+await once(server, 'listening')
+
+const client = new Client(`http://localhost:${server.address().port}`)
+
+client.on('disconnect', (origin) => {
+ console.log(`Disconnected from ${origin}`)
+})
+
+try {
+ await client.request({
+ path: '/',
+ method: 'GET'
+ })
+} catch (error) {
+ console.error(error.message)
+ client.close()
+ server.close()
+}
+```
+
+### Event: `'drain'`
+
+Emitted when pipeline is no longer busy.
+
+See [Dispatcher Event: `'drain'`](Dispatcher.md#event-drain).
+
+#### Example - Client drain event
+
+```js
+import { createServer } from 'http'
+import { Client } from 'undici'
+import { once } from 'events'
+
+const server = createServer((request, response) => {
+ response.end('Hello, World!')
+}).listen()
+
+await once(server, 'listening')
+
+const client = new Client(`http://localhost:${server.address().port}`)
+
+client.on('drain', () => {
+ console.log('drain event')
+ client.close()
+ server.close()
+})
+
+const requests = [
+ client.request({ path: '/', method: 'GET' }),
+ client.request({ path: '/', method: 'GET' }),
+ client.request({ path: '/', method: 'GET' })
+]
+
+await Promise.all(requests)
+
+console.log('requests completed')
+```
+
+### Event: `'error'`
+
+Invoked for users errors such as throwing in the `onError` handler.
diff --git a/docs/api/Connector.md b/docs/api/Connector.md
new file mode 100644
index 0000000..56821bd
--- /dev/null
+++ b/docs/api/Connector.md
@@ -0,0 +1,115 @@
+# Connector
+
+Undici creates the underlying socket via the connector builder.
+Normally, this happens automatically and you don't need to care about this,
+but if you need to perform some additional check over the currently used socket,
+this is the right place.
+
+If you want to create a custom connector, you must import the `buildConnector` utility.
+
+#### Parameter: `buildConnector.BuildOptions`
+
+Every Tls option, see [here](https://nodejs.org/api/tls.html#tls_tls_connect_options_callback).
+Furthermore, the following options can be passed:
+
+* **socketPath** `string | null` (optional) - Default: `null` - An IPC endpoint, either Unix domain socket or Windows named pipe.
+* **maxCachedSessions** `number | null` (optional) - Default: `100` - Maximum number of TLS cached sessions. Use 0 to disable TLS session caching. Default: `100`.
+* **timeout** `number | null` (optional) - In milliseconds. Default `10e3`.
+* **servername** `string | null` (optional)
+
+Once you call `buildConnector`, it will return a connector function, which takes the following parameters.
+
+#### Parameter: `connector.Options`
+
+* **hostname** `string` (required)
+* **host** `string` (optional)
+* **protocol** `string` (required)
+* **port** `string` (required)
+* **servername** `string` (optional)
+* **localAddress** `string | null` (optional) Local address the socket should connect from.
+* **httpSocket** `Socket` (optional) Establish secure connection on a given socket rather than creating a new socket. It can only be sent on TLS update.
+
+### Basic example
+
+```js
+'use strict'
+
+import { Client, buildConnector } from 'undici'
+
+const connector = buildConnector({ rejectUnauthorized: false })
+const client = new Client('https://localhost:3000', {
+ connect (opts, cb) {
+ connector(opts, (err, socket) => {
+ if (err) {
+ cb(err)
+ } else if (/* assertion */) {
+ socket.destroy()
+ cb(new Error('kaboom'))
+ } else {
+ cb(null, socket)
+ }
+ })
+ }
+})
+```
+
+### Example: validate the CA fingerprint
+
+```js
+'use strict'
+
+import { Client, buildConnector } from 'undici'
+
+const caFingerprint = 'FO:OB:AR'
+const connector = buildConnector({ rejectUnauthorized: false })
+const client = new Client('https://localhost:3000', {
+ connect (opts, cb) {
+ connector(opts, (err, socket) => {
+ if (err) {
+ cb(err)
+ } else if (getIssuerCertificate(socket).fingerprint256 !== caFingerprint) {
+ socket.destroy()
+ cb(new Error('Fingerprint does not match or malformed certificate'))
+ } else {
+ cb(null, socket)
+ }
+ })
+ }
+})
+
+client.request({
+ path: '/',
+ method: 'GET'
+}, (err, data) => {
+ if (err) throw err
+
+ const bufs = []
+ data.body.on('data', (buf) => {
+ bufs.push(buf)
+ })
+ data.body.on('end', () => {
+ console.log(Buffer.concat(bufs).toString('utf8'))
+ client.close()
+ })
+})
+
+function getIssuerCertificate (socket) {
+ let certificate = socket.getPeerCertificate(true)
+ while (certificate && Object.keys(certificate).length > 0) {
+ // invalid certificate
+ if (certificate.issuerCertificate == null) {
+ return null
+ }
+
+ // We have reached the root certificate.
+ // In case of self-signed certificates, `issuerCertificate` may be a circular reference.
+ if (certificate.fingerprint256 === certificate.issuerCertificate.fingerprint256) {
+ break
+ }
+
+ // continue the loop
+ certificate = certificate.issuerCertificate
+ }
+ return certificate
+}
+```
diff --git a/docs/api/ContentType.md b/docs/api/ContentType.md
new file mode 100644
index 0000000..2bcc9f7
--- /dev/null
+++ b/docs/api/ContentType.md
@@ -0,0 +1,57 @@
+# MIME Type Parsing
+
+## `MIMEType` interface
+
+* **type** `string`
+* **subtype** `string`
+* **parameters** `Map<string, string>`
+* **essence** `string`
+
+## `parseMIMEType(input)`
+
+Implements [parse a MIME type](https://mimesniff.spec.whatwg.org/#parse-a-mime-type).
+
+Parses a MIME type, returning its type, subtype, and any associated parameters. If the parser can't parse an input it returns the string literal `'failure'`.
+
+```js
+import { parseMIMEType } from 'undici'
+
+parseMIMEType('text/html; charset=gbk')
+// {
+// type: 'text',
+// subtype: 'html',
+// parameters: Map(1) { 'charset' => 'gbk' },
+// essence: 'text/html'
+// }
+```
+
+Arguments:
+
+* **input** `string`
+
+Returns: `MIMEType|'failure'`
+
+## `serializeAMimeType(input)`
+
+Implements [serialize a MIME type](https://mimesniff.spec.whatwg.org/#serialize-a-mime-type).
+
+Serializes a MIMEType object.
+
+```js
+import { serializeAMimeType } from 'undici'
+
+serializeAMimeType({
+ type: 'text',
+ subtype: 'html',
+ parameters: new Map([['charset', 'gbk']]),
+ essence: 'text/html'
+})
+// text/html;charset=gbk
+
+```
+
+Arguments:
+
+* **mimeType** `MIMEType`
+
+Returns: `string`
diff --git a/docs/api/Cookies.md b/docs/api/Cookies.md
new file mode 100644
index 0000000..0cad379
--- /dev/null
+++ b/docs/api/Cookies.md
@@ -0,0 +1,101 @@
+# Cookie Handling
+
+## `Cookie` interface
+
+* **name** `string`
+* **value** `string`
+* **expires** `Date|number` (optional)
+* **maxAge** `number` (optional)
+* **domain** `string` (optional)
+* **path** `string` (optional)
+* **secure** `boolean` (optional)
+* **httpOnly** `boolean` (optional)
+* **sameSite** `'String'|'Lax'|'None'` (optional)
+* **unparsed** `string[]` (optional) Left over attributes that weren't parsed.
+
+## `deleteCookie(headers, name[, attributes])`
+
+Sets the expiry time of the cookie to the unix epoch, causing browsers to delete it when received.
+
+```js
+import { deleteCookie, Headers } from 'undici'
+
+const headers = new Headers()
+deleteCookie(headers, 'name')
+
+console.log(headers.get('set-cookie')) // name=; Expires=Thu, 01 Jan 1970 00:00:00 GMT
+```
+
+Arguments:
+
+* **headers** `Headers`
+* **name** `string`
+* **attributes** `{ path?: string, domain?: string }` (optional)
+
+Returns: `void`
+
+## `getCookies(headers)`
+
+Parses the `Cookie` header and returns a list of attributes and values.
+
+```js
+import { getCookies, Headers } from 'undici'
+
+const headers = new Headers({
+ cookie: 'get=cookies; and=attributes'
+})
+
+console.log(getCookies(headers)) // { get: 'cookies', and: 'attributes' }
+```
+
+Arguments:
+
+* **headers** `Headers`
+
+Returns: `Record<string, string>`
+
+## `getSetCookies(headers)`
+
+Parses all `Set-Cookie` headers.
+
+```js
+import { getSetCookies, Headers } from 'undici'
+
+const headers = new Headers({ 'set-cookie': 'undici=getSetCookies; Secure' })
+
+console.log(getSetCookies(headers))
+// [
+// {
+// name: 'undici',
+// value: 'getSetCookies',
+// secure: true
+// }
+// ]
+
+```
+
+Arguments:
+
+* **headers** `Headers`
+
+Returns: `Cookie[]`
+
+## `setCookie(headers, cookie)`
+
+Appends a cookie to the `Set-Cookie` header.
+
+```js
+import { setCookie, Headers } from 'undici'
+
+const headers = new Headers()
+setCookie(headers, { name: 'undici', value: 'setCookie' })
+
+console.log(headers.get('Set-Cookie')) // undici=setCookie
+```
+
+Arguments:
+
+* **headers** `Headers`
+* **cookie** `Cookie`
+
+Returns: `void`
diff --git a/docs/api/DiagnosticsChannel.md b/docs/api/DiagnosticsChannel.md
new file mode 100644
index 0000000..0aa0b9a
--- /dev/null
+++ b/docs/api/DiagnosticsChannel.md
@@ -0,0 +1,204 @@
+# Diagnostics Channel Support
+
+Stability: Experimental.
+
+Undici supports the [`diagnostics_channel`](https://nodejs.org/api/diagnostics_channel.html) (currently available only on Node.js v16+).
+It is the preferred way to instrument Undici and retrieve internal information.
+
+The channels available are the following.
+
+## `undici:request:create`
+
+This message is published when a new outgoing request is created.
+
+```js
+import diagnosticsChannel from 'diagnostics_channel'
+
+diagnosticsChannel.channel('undici:request:create').subscribe(({ request }) => {
+ console.log('origin', request.origin)
+ console.log('completed', request.completed)
+ console.log('method', request.method)
+ console.log('path', request.path)
+ console.log('headers') // raw text, e.g: 'bar: bar\r\n'
+ request.addHeader('hello', 'world')
+ console.log('headers', request.headers) // e.g. 'bar: bar\r\nhello: world\r\n'
+})
+```
+
+Note: a request is only loosely completed to a given socket.
+
+
+## `undici:request:bodySent`
+
+```js
+import diagnosticsChannel from 'diagnostics_channel'
+
+diagnosticsChannel.channel('undici:request:bodySent').subscribe(({ request }) => {
+ // request is the same object undici:request:create
+})
+```
+
+## `undici:request:headers`
+
+This message is published after the response headers have been received, i.e. the response has been completed.
+
+```js
+import diagnosticsChannel from 'diagnostics_channel'
+
+diagnosticsChannel.channel('undici:request:headers').subscribe(({ request, response }) => {
+ // request is the same object undici:request:create
+ console.log('statusCode', response.statusCode)
+ console.log(response.statusText)
+ // response.headers are buffers.
+ console.log(response.headers.map((x) => x.toString()))
+})
+```
+
+## `undici:request:trailers`
+
+This message is published after the response body and trailers have been received, i.e. the response has been completed.
+
+```js
+import diagnosticsChannel from 'diagnostics_channel'
+
+diagnosticsChannel.channel('undici:request:trailers').subscribe(({ request, trailers }) => {
+ // request is the same object undici:request:create
+ console.log('completed', request.completed)
+ // trailers are buffers.
+ console.log(trailers.map((x) => x.toString()))
+})
+```
+
+## `undici:request:error`
+
+This message is published if the request is going to error, but it has not errored yet.
+
+```js
+import diagnosticsChannel from 'diagnostics_channel'
+
+diagnosticsChannel.channel('undici:request:error').subscribe(({ request, error }) => {
+ // request is the same object undici:request:create
+})
+```
+
+## `undici:client:sendHeaders`
+
+This message is published right before the first byte of the request is written to the socket.
+
+*Note*: It will publish the exact headers that will be sent to the server in raw format.
+
+```js
+import diagnosticsChannel from 'diagnostics_channel'
+
+diagnosticsChannel.channel('undici:client:sendHeaders').subscribe(({ request, headers, socket }) => {
+ // request is the same object undici:request:create
+ console.log(`Full headers list ${headers.split('\r\n')}`);
+})
+```
+
+## `undici:client:beforeConnect`
+
+This message is published before creating a new connection for **any** request.
+You can not assume that this event is related to any specific request.
+
+```js
+import diagnosticsChannel from 'diagnostics_channel'
+
+diagnosticsChannel.channel('undici:client:beforeConnect').subscribe(({ connectParams, connector }) => {
+ // const { host, hostname, protocol, port, servername } = connectParams
+ // connector is a function that creates the socket
+})
+```
+
+## `undici:client:connected`
+
+This message is published after a connection is established.
+
+```js
+import diagnosticsChannel from 'diagnostics_channel'
+
+diagnosticsChannel.channel('undici:client:connected').subscribe(({ socket, connectParams, connector }) => {
+ // const { host, hostname, protocol, port, servername } = connectParams
+ // connector is a function that creates the socket
+})
+```
+
+## `undici:client:connectError`
+
+This message is published if it did not succeed to create new connection
+
+```js
+import diagnosticsChannel from 'diagnostics_channel'
+
+diagnosticsChannel.channel('undici:client:connectError').subscribe(({ error, socket, connectParams, connector }) => {
+ // const { host, hostname, protocol, port, servername } = connectParams
+ // connector is a function that creates the socket
+ console.log(`Connect failed with ${error.message}`)
+})
+```
+
+## `undici:websocket:open`
+
+This message is published after the client has successfully connected to a server.
+
+```js
+import diagnosticsChannel from 'diagnostics_channel'
+
+diagnosticsChannel.channel('undici:websocket:open').subscribe(({ address, protocol, extensions }) => {
+ console.log(address) // address, family, and port
+ console.log(protocol) // negotiated subprotocols
+ console.log(extensions) // negotiated extensions
+})
+```
+
+## `undici:websocket:close`
+
+This message is published after the connection has closed.
+
+```js
+import diagnosticsChannel from 'diagnostics_channel'
+
+diagnosticsChannel.channel('undici:websocket:close').subscribe(({ websocket, code, reason }) => {
+ console.log(websocket) // the WebSocket object
+ console.log(code) // the closing status code
+ console.log(reason) // the closing reason
+})
+```
+
+## `undici:websocket:socket_error`
+
+This message is published if the socket experiences an error.
+
+```js
+import diagnosticsChannel from 'diagnostics_channel'
+
+diagnosticsChannel.channel('undici:websocket:socket_error').subscribe((error) => {
+ console.log(error)
+})
+```
+
+## `undici:websocket:ping`
+
+This message is published after the client receives a ping frame, if the connection is not closing.
+
+```js
+import diagnosticsChannel from 'diagnostics_channel'
+
+diagnosticsChannel.channel('undici:websocket:ping').subscribe(({ payload }) => {
+ // a Buffer or undefined, containing the optional application data of the frame
+ console.log(payload)
+})
+```
+
+## `undici:websocket:pong`
+
+This message is published after the client receives a pong frame.
+
+```js
+import diagnosticsChannel from 'diagnostics_channel'
+
+diagnosticsChannel.channel('undici:websocket:pong').subscribe(({ payload }) => {
+ // a Buffer or undefined, containing the optional application data of the frame
+ console.log(payload)
+})
+```
diff --git a/docs/api/DispatchInterceptor.md b/docs/api/DispatchInterceptor.md
new file mode 100644
index 0000000..7dfc260
--- /dev/null
+++ b/docs/api/DispatchInterceptor.md
@@ -0,0 +1,60 @@
+# Interface: DispatchInterceptor
+
+Extends: `Function`
+
+A function that can be applied to the `Dispatcher.Dispatch` function before it is invoked with a dispatch request.
+
+This allows one to write logic to intercept both the outgoing request, and the incoming response.
+
+### Parameter: `Dispatcher.Dispatch`
+
+The base dispatch function you are decorating.
+
+### ReturnType: `Dispatcher.Dispatch`
+
+A dispatch function that has been altered to provide additional logic
+
+### Basic Example
+
+Here is an example of an interceptor being used to provide a JWT bearer token
+
+```js
+'use strict'
+
+const insertHeaderInterceptor = dispatch => {
+ return function InterceptedDispatch(opts, handler){
+ opts.headers.push('Authorization', 'Bearer [Some token]')
+ return dispatch(opts, handler)
+ }
+}
+
+const client = new Client('https://localhost:3000', {
+ interceptors: { Client: [insertHeaderInterceptor] }
+})
+
+```
+
+### Basic Example 2
+
+Here is a contrived example of an interceptor stripping the headers from a response.
+
+```js
+'use strict'
+
+const clearHeadersInterceptor = dispatch => {
+ const { DecoratorHandler } = require('undici')
+ class ResultInterceptor extends DecoratorHandler {
+ onHeaders (statusCode, headers, resume) {
+ return super.onHeaders(statusCode, [], resume)
+ }
+ }
+ return function InterceptedDispatch(opts, handler){
+ return dispatch(opts, new ResultInterceptor(handler))
+ }
+}
+
+const client = new Client('https://localhost:3000', {
+ interceptors: { Client: [clearHeadersInterceptor] }
+})
+
+```
diff --git a/docs/api/Dispatcher.md b/docs/api/Dispatcher.md
new file mode 100644
index 0000000..fd463bf
--- /dev/null
+++ b/docs/api/Dispatcher.md
@@ -0,0 +1,887 @@
+# Dispatcher
+
+Extends: `events.EventEmitter`
+
+Dispatcher is the core API used to dispatch requests.
+
+Requests are not guaranteed to be dispatched in order of invocation.
+
+## Instance Methods
+
+### `Dispatcher.close([callback]): Promise`
+
+Closes the dispatcher and gracefully waits for enqueued requests to complete before resolving.
+
+Arguments:
+
+* **callback** `(error: Error | null, data: null) => void` (optional)
+
+Returns: `void | Promise<null>` - Only returns a `Promise` if no `callback` argument was passed
+
+```js
+dispatcher.close() // -> Promise
+dispatcher.close(() => {}) // -> void
+```
+
+#### Example - Request resolves before Client closes
+
+```js
+import { createServer } from 'http'
+import { Client } from 'undici'
+import { once } from 'events'
+
+const server = createServer((request, response) => {
+ response.end('undici')
+}).listen()
+
+await once(server, 'listening')
+
+const client = new Client(`http://localhost:${server.address().port}`)
+
+try {
+ const { body } = await client.request({
+ path: '/',
+ method: 'GET'
+ })
+ body.setEncoding('utf8')
+ body.on('data', console.log)
+} catch (error) {}
+
+await client.close()
+
+console.log('Client closed')
+server.close()
+```
+
+### `Dispatcher.connect(options[, callback])`
+
+Starts two-way communications with the requested resource using [HTTP CONNECT](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/CONNECT).
+
+Arguments:
+
+* **options** `ConnectOptions`
+* **callback** `(err: Error | null, data: ConnectData | null) => void` (optional)
+
+Returns: `void | Promise<ConnectData>` - Only returns a `Promise` if no `callback` argument was passed
+
+#### Parameter: `ConnectOptions`
+
+* **path** `string`
+* **headers** `UndiciHeaders` (optional) - Default: `null`
+* **signal** `AbortSignal | events.EventEmitter | null` (optional) - Default: `null`
+* **opaque** `unknown` (optional) - This argument parameter is passed through to `ConnectData`
+
+#### Parameter: `ConnectData`
+
+* **statusCode** `number`
+* **headers** `Record<string, string | string[] | undefined>`
+* **socket** `stream.Duplex`
+* **opaque** `unknown`
+
+#### Example - Connect request with echo
+
+```js
+import { createServer } from 'http'
+import { Client } from 'undici'
+import { once } from 'events'
+
+const server = createServer((request, response) => {
+ throw Error('should never get here')
+}).listen()
+
+server.on('connect', (req, socket, head) => {
+ socket.write('HTTP/1.1 200 Connection established\r\n\r\n')
+
+ let data = head.toString()
+ socket.on('data', (buf) => {
+ data += buf.toString()
+ })
+
+ socket.on('end', () => {
+ socket.end(data)
+ })
+})
+
+await once(server, 'listening')
+
+const client = new Client(`http://localhost:${server.address().port}`)
+
+try {
+ const { socket } = await client.connect({
+ path: '/'
+ })
+ const wanted = 'Body'
+ let data = ''
+ socket.on('data', d => { data += d })
+ socket.on('end', () => {
+ console.log(`Data received: ${data.toString()} | Data wanted: ${wanted}`)
+ client.close()
+ server.close()
+ })
+ socket.write(wanted)
+ socket.end()
+} catch (error) { }
+```
+
+### `Dispatcher.destroy([error, callback]): Promise`
+
+Destroy the dispatcher abruptly with the given error. All the pending and running requests will be asynchronously aborted and error. Since this operation is asynchronously dispatched there might still be some progress on dispatched requests.
+
+Both arguments are optional; the method can be called in four different ways:
+
+Arguments:
+
+* **error** `Error | null` (optional)
+* **callback** `(error: Error | null, data: null) => void` (optional)
+
+Returns: `void | Promise<void>` - Only returns a `Promise` if no `callback` argument was passed
+
+```js
+dispatcher.destroy() // -> Promise
+dispatcher.destroy(new Error()) // -> Promise
+dispatcher.destroy(() => {}) // -> void
+dispatcher.destroy(new Error(), () => {}) // -> void
+```
+
+#### Example - Request is aborted when Client is destroyed
+
+```js
+import { createServer } from 'http'
+import { Client } from 'undici'
+import { once } from 'events'
+
+const server = createServer((request, response) => {
+ response.end()
+}).listen()
+
+await once(server, 'listening')
+
+const client = new Client(`http://localhost:${server.address().port}`)
+
+try {
+ const request = client.request({
+ path: '/',
+ method: 'GET'
+ })
+ client.destroy()
+ .then(() => {
+ console.log('Client destroyed')
+ server.close()
+ })
+ await request
+} catch (error) {
+ console.error(error)
+}
+```
+
+### `Dispatcher.dispatch(options, handler)`
+
+This is the low level API which all the preceding APIs are implemented on top of.
+This API is expected to evolve through semver-major versions and is less stable than the preceding higher level APIs.
+It is primarily intended for library developers who implement higher level APIs on top of this.
+
+Arguments:
+
+* **options** `DispatchOptions`
+* **handler** `DispatchHandler`
+
+Returns: `Boolean` - `false` if dispatcher is busy and further dispatch calls won't make any progress until the `'drain'` event has been emitted.
+
+#### Parameter: `DispatchOptions`
+
+* **origin** `string | URL`
+* **path** `string`
+* **method** `string`
+* **reset** `boolean` (optional) - Default: `false` - If `false`, the request will attempt to create a long-living connection by sending the `connection: keep-alive` header,otherwise will attempt to close it immediately after response by sending `connection: close` within the request and closing the socket afterwards.
+* **body** `string | Buffer | Uint8Array | stream.Readable | Iterable | AsyncIterable | null` (optional) - Default: `null`
+* **headers** `UndiciHeaders | string[]` (optional) - Default: `null`.
+* **query** `Record<string, any> | null` (optional) - Default: `null` - Query string params to be embedded in the request URL. Note that both keys and values of query are encoded using `encodeURIComponent`. If for some reason you need to send them unencoded, embed query params into path directly instead.
+* **idempotent** `boolean` (optional) - Default: `true` if `method` is `'HEAD'` or `'GET'` - Whether the requests can be safely retried or not. If `false` the request won't be sent until all preceding requests in the pipeline has completed.
+* **blocking** `boolean` (optional) - Default: `false` - Whether the response is expected to take a long time and would end up blocking the pipeline. When this is set to `true` further pipelining will be avoided on the same connection until headers have been received.
+* **upgrade** `string | null` (optional) - Default: `null` - Upgrade the request. Should be used to specify the kind of upgrade i.e. `'Websocket'`.
+* **bodyTimeout** `number | null` (optional) - The timeout after which a request will time out, in milliseconds. Monitors time between receiving body data. Use `0` to disable it entirely. Defaults to 300 seconds.
+* **headersTimeout** `number | null` (optional) - The amount of time, in milliseconds, the parser will wait to receive the complete HTTP headers while not sending the request. Defaults to 300 seconds.
+* **throwOnError** `boolean` (optional) - Default: `false` - Whether Undici should throw an error upon receiving a 4xx or 5xx response from the server.
+* **expectContinue** `boolean` (optional) - Default: `false` - For H2, it appends the expect: 100-continue header, and halts the request body until a 100-continue is received from the remote server
+
+#### Parameter: `DispatchHandler`
+
+* **onConnect** `(abort: () => void, context: object) => void` - Invoked before request is dispatched on socket. May be invoked multiple times when a request is retried when the request at the head of the pipeline fails.
+* **onError** `(error: Error) => void` - Invoked when an error has occurred. May not throw.
+* **onUpgrade** `(statusCode: number, headers: Buffer[], socket: Duplex) => void` (optional) - Invoked when request is upgraded. Required if `DispatchOptions.upgrade` is defined or `DispatchOptions.method === 'CONNECT'`.
+* **onHeaders** `(statusCode: number, headers: Buffer[], resume: () => void, statusText: string) => boolean` - Invoked when statusCode and headers have been received. May be invoked multiple times due to 1xx informational headers. Not required for `upgrade` requests.
+* **onData** `(chunk: Buffer) => boolean` - Invoked when response payload data is received. Not required for `upgrade` requests.
+* **onComplete** `(trailers: Buffer[]) => void` - Invoked when response payload and trailers have been received and the request has completed. Not required for `upgrade` requests.
+* **onBodySent** `(chunk: string | Buffer | Uint8Array) => void` - Invoked when a body chunk is sent to the server. Not required. For a stream or iterable body this will be invoked for every chunk. For other body types, it will be invoked once after the body is sent.
+
+#### Example 1 - Dispatch GET request
+
+```js
+import { createServer } from 'http'
+import { Client } from 'undici'
+import { once } from 'events'
+
+const server = createServer((request, response) => {
+ response.end('Hello, World!')
+}).listen()
+
+await once(server, 'listening')
+
+const client = new Client(`http://localhost:${server.address().port}`)
+
+const data = []
+
+client.dispatch({
+ path: '/',
+ method: 'GET',
+ headers: {
+ 'x-foo': 'bar'
+ }
+}, {
+ onConnect: () => {
+ console.log('Connected!')
+ },
+ onError: (error) => {
+ console.error(error)
+ },
+ onHeaders: (statusCode, headers) => {
+ console.log(`onHeaders | statusCode: ${statusCode} | headers: ${headers}`)
+ },
+ onData: (chunk) => {
+ console.log('onData: chunk received')
+ data.push(chunk)
+ },
+ onComplete: (trailers) => {
+ console.log(`onComplete | trailers: ${trailers}`)
+ const res = Buffer.concat(data).toString('utf8')
+ console.log(`Data: ${res}`)
+ client.close()
+ server.close()
+ }
+})
+```
+
+#### Example 2 - Dispatch Upgrade Request
+
+```js
+import { createServer } from 'http'
+import { Client } from 'undici'
+import { once } from 'events'
+
+const server = createServer((request, response) => {
+ response.end()
+}).listen()
+
+await once(server, 'listening')
+
+server.on('upgrade', (request, socket, head) => {
+ console.log('Node.js Server - upgrade event')
+ socket.write('HTTP/1.1 101 Web Socket Protocol Handshake\r\n')
+ socket.write('Upgrade: WebSocket\r\n')
+ socket.write('Connection: Upgrade\r\n')
+ socket.write('\r\n')
+ socket.end()
+})
+
+const client = new Client(`http://localhost:${server.address().port}`)
+
+client.dispatch({
+ path: '/',
+ method: 'GET',
+ upgrade: 'websocket'
+}, {
+ onConnect: () => {
+ console.log('Undici Client - onConnect')
+ },
+ onError: (error) => {
+ console.log('onError') // shouldn't print
+ },
+ onUpgrade: (statusCode, headers, socket) => {
+ console.log('Undici Client - onUpgrade')
+ console.log(`onUpgrade Headers: ${headers}`)
+ socket.on('data', buffer => {
+ console.log(buffer.toString('utf8'))
+ })
+ socket.on('end', () => {
+ client.close()
+ server.close()
+ })
+ socket.end()
+ }
+})
+```
+
+#### Example 3 - Dispatch POST request
+
+```js
+import { createServer } from 'http'
+import { Client } from 'undici'
+import { once } from 'events'
+
+const server = createServer((request, response) => {
+ request.on('data', (data) => {
+ console.log(`Request Data: ${data.toString('utf8')}`)
+ const body = JSON.parse(data)
+ body.message = 'World'
+ response.end(JSON.stringify(body))
+ })
+}).listen()
+
+await once(server, 'listening')
+
+const client = new Client(`http://localhost:${server.address().port}`)
+
+const data = []
+
+client.dispatch({
+ path: '/',
+ method: 'POST',
+ headers: {
+ 'content-type': 'application/json'
+ },
+ body: JSON.stringify({ message: 'Hello' })
+}, {
+ onConnect: () => {
+ console.log('Connected!')
+ },
+ onError: (error) => {
+ console.error(error)
+ },
+ onHeaders: (statusCode, headers) => {
+ console.log(`onHeaders | statusCode: ${statusCode} | headers: ${headers}`)
+ },
+ onData: (chunk) => {
+ console.log('onData: chunk received')
+ data.push(chunk)
+ },
+ onComplete: (trailers) => {
+ console.log(`onComplete | trailers: ${trailers}`)
+ const res = Buffer.concat(data).toString('utf8')
+ console.log(`Response Data: ${res}`)
+ client.close()
+ server.close()
+ }
+})
+```
+
+### `Dispatcher.pipeline(options, handler)`
+
+For easy use with [stream.pipeline](https://nodejs.org/api/stream.html#stream_stream_pipeline_source_transforms_destination_callback). The `handler` argument should return a `Readable` from which the result will be read. Usually it should just return the `body` argument unless some kind of transformation needs to be performed based on e.g. `headers` or `statusCode`. The `handler` should validate the response and save any required state. If there is an error, it should be thrown. The function returns a `Duplex` which writes to the request and reads from the response.
+
+Arguments:
+
+* **options** `PipelineOptions`
+* **handler** `(data: PipelineHandlerData) => stream.Readable`
+
+Returns: `stream.Duplex`
+
+#### Parameter: PipelineOptions
+
+Extends: [`RequestOptions`](#parameter-requestoptions)
+
+* **objectMode** `boolean` (optional) - Default: `false` - Set to `true` if the `handler` will return an object stream.
+
+#### Parameter: PipelineHandlerData
+
+* **statusCode** `number`
+* **headers** `Record<string, string | string[] | undefined>`
+* **opaque** `unknown`
+* **body** `stream.Readable`
+* **context** `object`
+* **onInfo** `({statusCode: number, headers: Record<string, string | string[]>}) => void | null` (optional) - Default: `null` - Callback collecting all the info headers (HTTP 100-199) received.
+
+#### Example 1 - Pipeline Echo
+
+```js
+import { Readable, Writable, PassThrough, pipeline } from 'stream'
+import { createServer } from 'http'
+import { Client } from 'undici'
+import { once } from 'events'
+
+const server = createServer((request, response) => {
+ request.pipe(response)
+}).listen()
+
+await once(server, 'listening')
+
+const client = new Client(`http://localhost:${server.address().port}`)
+
+let res = ''
+
+pipeline(
+ new Readable({
+ read () {
+ this.push(Buffer.from('undici'))
+ this.push(null)
+ }
+ }),
+ client.pipeline({
+ path: '/',
+ method: 'GET'
+ }, ({ statusCode, headers, body }) => {
+ console.log(`response received ${statusCode}`)
+ console.log('headers', headers)
+ return pipeline(body, new PassThrough(), () => {})
+ }),
+ new Writable({
+ write (chunk, _, callback) {
+ res += chunk.toString()
+ callback()
+ },
+ final (callback) {
+ console.log(`Response pipelined to writable: ${res}`)
+ callback()
+ }
+ }),
+ error => {
+ if (error) {
+ console.error(error)
+ }
+
+ client.close()
+ server.close()
+ }
+)
+```
+
+### `Dispatcher.request(options[, callback])`
+
+Performs a HTTP request.
+
+Non-idempotent requests will not be pipelined in order
+to avoid indirect failures.
+
+Idempotent requests will be automatically retried if
+they fail due to indirect failure from the request
+at the head of the pipeline. This does not apply to
+idempotent requests with a stream request body.
+
+All response bodies must always be fully consumed or destroyed.
+
+Arguments:
+
+* **options** `RequestOptions`
+* **callback** `(error: Error | null, data: ResponseData) => void` (optional)
+
+Returns: `void | Promise<ResponseData>` - Only returns a `Promise` if no `callback` argument was passed.
+
+#### Parameter: `RequestOptions`
+
+Extends: [`DispatchOptions`](#parameter-dispatchoptions)
+
+* **opaque** `unknown` (optional) - Default: `null` - Used for passing through context to `ResponseData`.
+* **signal** `AbortSignal | events.EventEmitter | null` (optional) - Default: `null`.
+* **onInfo** `({statusCode: number, headers: Record<string, string | string[]>}) => void | null` (optional) - Default: `null` - Callback collecting all the info headers (HTTP 100-199) received.
+
+The `RequestOptions.method` property should not be value `'CONNECT'`.
+
+#### Parameter: `ResponseData`
+
+* **statusCode** `number`
+* **headers** `Record<string, string | string[]>` - Note that all header keys are lower-cased, e. g. `content-type`.
+* **body** `stream.Readable` which also implements [the body mixin from the Fetch Standard](https://fetch.spec.whatwg.org/#body-mixin).
+* **trailers** `Record<string, string>` - This object starts out
+ as empty and will be mutated to contain trailers after `body` has emitted `'end'`.
+* **opaque** `unknown`
+* **context** `object`
+
+`body` contains the following additional [body mixin](https://fetch.spec.whatwg.org/#body-mixin) methods and properties:
+
+- `text()`
+- `json()`
+- `arrayBuffer()`
+- `body`
+- `bodyUsed`
+
+`body` can not be consumed twice. For example, calling `text()` after `json()` throws `TypeError`.
+
+`body` contains the following additional extensions:
+
+- `dump({ limit: Integer })`, dump the response by reading up to `limit` bytes without killing the socket (optional) - Default: 262144.
+
+Note that body will still be a `Readable` even if it is empty, but attempting to deserialize it with `json()` will result in an exception. Recommended way to ensure there is a body to deserialize is to check if status code is not 204, and `content-type` header starts with `application/json`.
+
+#### Example 1 - Basic GET Request
+
+```js
+import { createServer } from 'http'
+import { Client } from 'undici'
+import { once } from 'events'
+
+const server = createServer((request, response) => {
+ response.end('Hello, World!')
+}).listen()
+
+await once(server, 'listening')
+
+const client = new Client(`http://localhost:${server.address().port}`)
+
+try {
+ const { body, headers, statusCode, trailers } = await client.request({
+ path: '/',
+ method: 'GET'
+ })
+ console.log(`response received ${statusCode}`)
+ console.log('headers', headers)
+ body.setEncoding('utf8')
+ body.on('data', console.log)
+ body.on('end', () => {
+ console.log('trailers', trailers)
+ })
+
+ client.close()
+ server.close()
+} catch (error) {
+ console.error(error)
+}
+```
+
+#### Example 2 - Aborting a request
+
+> Node.js v15+ is required to run this example
+
+```js
+import { createServer } from 'http'
+import { Client } from 'undici'
+import { once } from 'events'
+
+const server = createServer((request, response) => {
+ response.end('Hello, World!')
+}).listen()
+
+await once(server, 'listening')
+
+const client = new Client(`http://localhost:${server.address().port}`)
+const abortController = new AbortController()
+
+try {
+ client.request({
+ path: '/',
+ method: 'GET',
+ signal: abortController.signal
+ })
+} catch (error) {
+ console.error(error) // should print an RequestAbortedError
+ client.close()
+ server.close()
+}
+
+abortController.abort()
+```
+
+Alternatively, any `EventEmitter` that emits an `'abort'` event may be used as an abort controller:
+
+```js
+import { createServer } from 'http'
+import { Client } from 'undici'
+import EventEmitter, { once } from 'events'
+
+const server = createServer((request, response) => {
+ response.end('Hello, World!')
+}).listen()
+
+await once(server, 'listening')
+
+const client = new Client(`http://localhost:${server.address().port}`)
+const ee = new EventEmitter()
+
+try {
+ client.request({
+ path: '/',
+ method: 'GET',
+ signal: ee
+ })
+} catch (error) {
+ console.error(error) // should print an RequestAbortedError
+ client.close()
+ server.close()
+}
+
+ee.emit('abort')
+```
+
+Destroying the request or response body will have the same effect.
+
+```js
+import { createServer } from 'http'
+import { Client } from 'undici'
+import { once } from 'events'
+
+const server = createServer((request, response) => {
+ response.end('Hello, World!')
+}).listen()
+
+await once(server, 'listening')
+
+const client = new Client(`http://localhost:${server.address().port}`)
+
+try {
+ const { body } = await client.request({
+ path: '/',
+ method: 'GET'
+ })
+ body.destroy()
+} catch (error) {
+ console.error(error) // should print an RequestAbortedError
+ client.close()
+ server.close()
+}
+```
+
+### `Dispatcher.stream(options, factory[, callback])`
+
+A faster version of `Dispatcher.request`. This method expects the second argument `factory` to return a [`stream.Writable`](https://nodejs.org/api/stream.html#stream_class_stream_writable) stream which the response will be written to. This improves performance by avoiding creating an intermediate [`stream.Readable`](https://nodejs.org/api/stream.html#stream_readable_streams) stream when the user expects to directly pipe the response body to a [`stream.Writable`](https://nodejs.org/api/stream.html#stream_class_stream_writable) stream.
+
+As demonstrated in [Example 1 - Basic GET stream request](#example-1---basic-get-stream-request), it is recommended to use the `option.opaque` property to avoid creating a closure for the `factory` method. This pattern works well with Node.js Web Frameworks such as [Fastify](https://fastify.io). See [Example 2 - Stream to Fastify Response](#example-2---stream-to-fastify-response) for more details.
+
+Arguments:
+
+* **options** `RequestOptions`
+* **factory** `(data: StreamFactoryData) => stream.Writable`
+* **callback** `(error: Error | null, data: StreamData) => void` (optional)
+
+Returns: `void | Promise<StreamData>` - Only returns a `Promise` if no `callback` argument was passed
+
+#### Parameter: `StreamFactoryData`
+
+* **statusCode** `number`
+* **headers** `Record<string, string | string[] | undefined>`
+* **opaque** `unknown`
+* **onInfo** `({statusCode: number, headers: Record<string, string | string[]>}) => void | null` (optional) - Default: `null` - Callback collecting all the info headers (HTTP 100-199) received.
+
+#### Parameter: `StreamData`
+
+* **opaque** `unknown`
+* **trailers** `Record<string, string>`
+* **context** `object`
+
+#### Example 1 - Basic GET stream request
+
+```js
+import { createServer } from 'http'
+import { Client } from 'undici'
+import { once } from 'events'
+import { Writable } from 'stream'
+
+const server = createServer((request, response) => {
+ response.end('Hello, World!')
+}).listen()
+
+await once(server, 'listening')
+
+const client = new Client(`http://localhost:${server.address().port}`)
+
+const bufs = []
+
+try {
+ await client.stream({
+ path: '/',
+ method: 'GET',
+ opaque: { bufs }
+ }, ({ statusCode, headers, opaque: { bufs } }) => {
+ console.log(`response received ${statusCode}`)
+ console.log('headers', headers)
+ return new Writable({
+ write (chunk, encoding, callback) {
+ bufs.push(chunk)
+ callback()
+ }
+ })
+ })
+
+ console.log(Buffer.concat(bufs).toString('utf-8'))
+
+ client.close()
+ server.close()
+} catch (error) {
+ console.error(error)
+}
+```
+
+#### Example 2 - Stream to Fastify Response
+
+In this example, a (fake) request is made to the fastify server using `fastify.inject()`. This request then executes the fastify route handler which makes a subsequent request to the raw Node.js http server using `undici.dispatcher.stream()`. The fastify response is passed to the `opaque` option so that undici can tap into the underlying writable stream using `response.raw`. This methodology demonstrates how one could use undici and fastify together to create fast-as-possible requests from one backend server to another.
+
+```js
+import { createServer } from 'http'
+import { Client } from 'undici'
+import { once } from 'events'
+import fastify from 'fastify'
+
+const nodeServer = createServer((request, response) => {
+ response.end('Hello, World! From Node.js HTTP Server')
+}).listen()
+
+await once(nodeServer, 'listening')
+
+console.log('Node Server listening')
+
+const nodeServerUndiciClient = new Client(`http://localhost:${nodeServer.address().port}`)
+
+const fastifyServer = fastify()
+
+fastifyServer.route({
+ url: '/',
+ method: 'GET',
+ handler: (request, response) => {
+ nodeServerUndiciClient.stream({
+ path: '/',
+ method: 'GET',
+ opaque: response
+ }, ({ opaque }) => opaque.raw)
+ }
+})
+
+await fastifyServer.listen()
+
+console.log('Fastify Server listening')
+
+const fastifyServerUndiciClient = new Client(`http://localhost:${fastifyServer.server.address().port}`)
+
+try {
+ const { statusCode, body } = await fastifyServerUndiciClient.request({
+ path: '/',
+ method: 'GET'
+ })
+
+ console.log(`response received ${statusCode}`)
+ body.setEncoding('utf8')
+ body.on('data', console.log)
+
+ nodeServerUndiciClient.close()
+ fastifyServerUndiciClient.close()
+ fastifyServer.close()
+ nodeServer.close()
+} catch (error) { }
+```
+
+### `Dispatcher.upgrade(options[, callback])`
+
+Upgrade to a different protocol. Visit [MDN - HTTP - Protocol upgrade mechanism](https://developer.mozilla.org/en-US/docs/Web/HTTP/Protocol_upgrade_mechanism) for more details.
+
+Arguments:
+
+* **options** `UpgradeOptions`
+
+* **callback** `(error: Error | null, data: UpgradeData) => void` (optional)
+
+Returns: `void | Promise<UpgradeData>` - Only returns a `Promise` if no `callback` argument was passed
+
+#### Parameter: `UpgradeOptions`
+
+* **path** `string`
+* **method** `string` (optional) - Default: `'GET'`
+* **headers** `UndiciHeaders` (optional) - Default: `null`
+* **protocol** `string` (optional) - Default: `'Websocket'` - A string of comma separated protocols, in descending preference order.
+* **signal** `AbortSignal | EventEmitter | null` (optional) - Default: `null`
+
+#### Parameter: `UpgradeData`
+
+* **headers** `http.IncomingHeaders`
+* **socket** `stream.Duplex`
+* **opaque** `unknown`
+
+#### Example 1 - Basic Upgrade Request
+
+```js
+import { createServer } from 'http'
+import { Client } from 'undici'
+import { once } from 'events'
+
+const server = createServer((request, response) => {
+ response.statusCode = 101
+ response.setHeader('connection', 'upgrade')
+ response.setHeader('upgrade', request.headers.upgrade)
+ response.end()
+}).listen()
+
+await once(server, 'listening')
+
+const client = new Client(`http://localhost:${server.address().port}`)
+
+try {
+ const { headers, socket } = await client.upgrade({
+ path: '/',
+ })
+ socket.on('end', () => {
+ console.log(`upgrade: ${headers.upgrade}`) // upgrade: Websocket
+ client.close()
+ server.close()
+ })
+ socket.end()
+} catch (error) {
+ console.error(error)
+ client.close()
+ server.close()
+}
+```
+
+## Instance Events
+
+### Event: `'connect'`
+
+Parameters:
+
+* **origin** `URL`
+* **targets** `Array<Dispatcher>`
+
+### Event: `'disconnect'`
+
+Parameters:
+
+* **origin** `URL`
+* **targets** `Array<Dispatcher>`
+* **error** `Error`
+
+### Event: `'connectionError'`
+
+Parameters:
+
+* **origin** `URL`
+* **targets** `Array<Dispatcher>`
+* **error** `Error`
+
+Emitted when dispatcher fails to connect to
+origin.
+
+### Event: `'drain'`
+
+Parameters:
+
+* **origin** `URL`
+
+Emitted when dispatcher is no longer busy.
+
+## Parameter: `UndiciHeaders`
+
+* `Record<string, string | string[] | undefined> | string[] | null`
+
+Header arguments such as `options.headers` in [`Client.dispatch`](Client.md#clientdispatchoptions-handlers) can be specified in two forms; either as an object specified by the `Record<string, string | string[] | undefined>` (`IncomingHttpHeaders`) type, or an array of strings. An array representation of a header list must have an even length or an `InvalidArgumentError` will be thrown.
+
+Keys are lowercase and values are not modified.
+
+Response headers will derive a `host` from the `url` of the [Client](Client.md#class-client) instance if no `host` header was previously specified.
+
+### Example 1 - Object
+
+```js
+{
+ 'content-length': '123',
+ 'content-type': 'text/plain',
+ connection: 'keep-alive',
+ host: 'mysite.com',
+ accept: '*/*'
+}
+```
+
+### Example 2 - Array
+
+```js
+[
+ 'content-length', '123',
+ 'content-type', 'text/plain',
+ 'connection', 'keep-alive',
+ 'host', 'mysite.com',
+ 'accept', '*/*'
+]
+```
diff --git a/docs/api/Errors.md b/docs/api/Errors.md
new file mode 100644
index 0000000..917e45d
--- /dev/null
+++ b/docs/api/Errors.md
@@ -0,0 +1,47 @@
+# Errors
+
+Undici exposes a variety of error objects that you can use to enhance your error handling.
+You can find all the error objects inside the `errors` key.
+
+```js
+import { errors } from 'undici'
+```
+
+| Error | Error Codes | Description |
+| ------------------------------------ | ------------------------------------- | ------------------------------------------------------------------------- |
+| `UndiciError` | `UND_ERR` | all errors below are extended from `UndiciError`. |
+| `ConnectTimeoutError` | `UND_ERR_CONNECT_TIMEOUT` | socket is destroyed due to connect timeout. |
+| `HeadersTimeoutError` | `UND_ERR_HEADERS_TIMEOUT` | socket is destroyed due to headers timeout. |
+| `HeadersOverflowError` | `UND_ERR_HEADERS_OVERFLOW` | socket is destroyed due to headers' max size being exceeded. |
+| `BodyTimeoutError` | `UND_ERR_BODY_TIMEOUT` | socket is destroyed due to body timeout. |
+| `ResponseStatusCodeError` | `UND_ERR_RESPONSE_STATUS_CODE` | an error is thrown when `throwOnError` is `true` for status codes >= 400. |
+| `InvalidArgumentError` | `UND_ERR_INVALID_ARG` | passed an invalid argument. |
+| `InvalidReturnValueError` | `UND_ERR_INVALID_RETURN_VALUE` | returned an invalid value. |
+| `RequestAbortedError` | `UND_ERR_ABORTED` | the request has been aborted by the user |
+| `ClientDestroyedError` | `UND_ERR_DESTROYED` | trying to use a destroyed client. |
+| `ClientClosedError` | `UND_ERR_CLOSED` | trying to use a closed client. |
+| `SocketError` | `UND_ERR_SOCKET` | there is an error with the socket. |
+| `NotSupportedError` | `UND_ERR_NOT_SUPPORTED` | encountered unsupported functionality. |
+| `RequestContentLengthMismatchError` | `UND_ERR_REQ_CONTENT_LENGTH_MISMATCH` | request body does not match content-length header |
+| `ResponseContentLengthMismatchError` | `UND_ERR_RES_CONTENT_LENGTH_MISMATCH` | response body does not match content-length header |
+| `InformationalError` | `UND_ERR_INFO` | expected error with reason |
+| `ResponseExceededMaxSizeError` | `UND_ERR_RES_EXCEEDED_MAX_SIZE` | response body exceed the max size allowed |
+
+### `SocketError`
+
+The `SocketError` has a `.socket` property which holds socket metadata:
+
+```ts
+interface SocketInfo {
+ localAddress?: string
+ localPort?: number
+ remoteAddress?: string
+ remotePort?: number
+ remoteFamily?: string
+ timeout?: number
+ bytesWritten?: number
+ bytesRead?: number
+}
+```
+
+Be aware that in some cases the `.socket` property can be `null`.
diff --git a/docs/api/Fetch.md b/docs/api/Fetch.md
new file mode 100644
index 0000000..b5a6242
--- /dev/null
+++ b/docs/api/Fetch.md
@@ -0,0 +1,27 @@
+# Fetch
+
+Undici exposes a fetch() method starts the process of fetching a resource from the network.
+
+Documentation and examples can be found on [MDN](https://developer.mozilla.org/en-US/docs/Web/API/fetch).
+
+## File
+
+This API is implemented as per the standard, you can find documentation on [MDN](https://developer.mozilla.org/en-US/docs/Web/API/File)
+
+In Node versions v18.13.0 and above and v19.2.0 and above, undici will default to using Node's [File](https://nodejs.org/api/buffer.html#class-file) class. In versions where it's not available, it will default to the undici one.
+
+## FormData
+
+This API is implemented as per the standard, you can find documentation on [MDN](https://developer.mozilla.org/en-US/docs/Web/API/FormData)
+
+## Response
+
+This API is implemented as per the standard, you can find documentation on [MDN](https://developer.mozilla.org/en-US/docs/Web/API/Response)
+
+## Request
+
+This API is implemented as per the standard, you can find documentation on [MDN](https://developer.mozilla.org/en-US/docs/Web/API/Request)
+
+## Header
+
+This API is implemented as per the standard, you can find documentation on [MDN](https://developer.mozilla.org/en-US/docs/Web/API/Headers)
diff --git a/docs/api/MockAgent.md b/docs/api/MockAgent.md
new file mode 100644
index 0000000..85ae690
--- /dev/null
+++ b/docs/api/MockAgent.md
@@ -0,0 +1,540 @@
+# Class: MockAgent
+
+Extends: `undici.Dispatcher`
+
+A mocked Agent class that implements the Agent API. It allows one to intercept HTTP requests made through undici and return mocked responses instead.
+
+## `new MockAgent([options])`
+
+Arguments:
+
+* **options** `MockAgentOptions` (optional) - It extends the `Agent` options.
+
+Returns: `MockAgent`
+
+### Parameter: `MockAgentOptions`
+
+Extends: [`AgentOptions`](Agent.md#parameter-agentoptions)
+
+* **agent** `Agent` (optional) - Default: `new Agent([options])` - a custom agent encapsulated by the MockAgent.
+
+### Example - Basic MockAgent instantiation
+
+This will instantiate the MockAgent. It will not do anything until registered as the agent to use with requests and mock interceptions are added.
+
+```js
+import { MockAgent } from 'undici'
+
+const mockAgent = new MockAgent()
+```
+
+### Example - Basic MockAgent instantiation with custom agent
+
+```js
+import { Agent, MockAgent } from 'undici'
+
+const agent = new Agent()
+
+const mockAgent = new MockAgent({ agent })
+```
+
+## Instance Methods
+
+### `MockAgent.get(origin)`
+
+This method creates and retrieves MockPool or MockClient instances which can then be used to intercept HTTP requests. If the number of connections on the mock agent is set to 1, a MockClient instance is returned. Otherwise a MockPool instance is returned.
+
+For subsequent `MockAgent.get` calls on the same origin, the same mock instance will be returned.
+
+Arguments:
+
+* **origin** `string | RegExp | (value) => boolean` - a matcher for the pool origin to be retrieved from the MockAgent.
+
+| Matcher type | Condition to pass |
+|:------------:| -------------------------- |
+| `string` | Exact match against string |
+| `RegExp` | Regex must pass |
+| `Function` | Function must return true |
+
+Returns: `MockClient | MockPool`.
+
+| `MockAgentOptions` | Mock instance returned |
+| -------------------- | ---------------------- |
+| `connections === 1` | `MockClient` |
+| `connections` > `1` | `MockPool` |
+
+#### Example - Basic Mocked Request
+
+```js
+import { MockAgent, setGlobalDispatcher, request } from 'undici'
+
+const mockAgent = new MockAgent()
+setGlobalDispatcher(mockAgent)
+
+const mockPool = mockAgent.get('http://localhost:3000')
+mockPool.intercept({ path: '/foo' }).reply(200, 'foo')
+
+const { statusCode, body } = await request('http://localhost:3000/foo')
+
+console.log('response received', statusCode) // response received 200
+
+for await (const data of body) {
+ console.log('data', data.toString('utf8')) // data foo
+}
+```
+
+#### Example - Basic Mocked Request with local mock agent dispatcher
+
+```js
+import { MockAgent, request } from 'undici'
+
+const mockAgent = new MockAgent()
+
+const mockPool = mockAgent.get('http://localhost:3000')
+mockPool.intercept({ path: '/foo' }).reply(200, 'foo')
+
+const {
+ statusCode,
+ body
+} = await request('http://localhost:3000/foo', { dispatcher: mockAgent })
+
+console.log('response received', statusCode) // response received 200
+
+for await (const data of body) {
+ console.log('data', data.toString('utf8')) // data foo
+}
+```
+
+#### Example - Basic Mocked Request with local mock pool dispatcher
+
+```js
+import { MockAgent, request } from 'undici'
+
+const mockAgent = new MockAgent()
+
+const mockPool = mockAgent.get('http://localhost:3000')
+mockPool.intercept({ path: '/foo' }).reply(200, 'foo')
+
+const {
+ statusCode,
+ body
+} = await request('http://localhost:3000/foo', { dispatcher: mockPool })
+
+console.log('response received', statusCode) // response received 200
+
+for await (const data of body) {
+ console.log('data', data.toString('utf8')) // data foo
+}
+```
+
+#### Example - Basic Mocked Request with local mock client dispatcher
+
+```js
+import { MockAgent, request } from 'undici'
+
+const mockAgent = new MockAgent({ connections: 1 })
+
+const mockClient = mockAgent.get('http://localhost:3000')
+mockClient.intercept({ path: '/foo' }).reply(200, 'foo')
+
+const {
+ statusCode,
+ body
+} = await request('http://localhost:3000/foo', { dispatcher: mockClient })
+
+console.log('response received', statusCode) // response received 200
+
+for await (const data of body) {
+ console.log('data', data.toString('utf8')) // data foo
+}
+```
+
+#### Example - Basic Mocked requests with multiple intercepts
+
+```js
+import { MockAgent, setGlobalDispatcher, request } from 'undici'
+
+const mockAgent = new MockAgent()
+setGlobalDispatcher(mockAgent)
+
+const mockPool = mockAgent.get('http://localhost:3000')
+mockPool.intercept({ path: '/foo' }).reply(200, 'foo')
+mockPool.intercept({ path: '/hello'}).reply(200, 'hello')
+
+const result1 = await request('http://localhost:3000/foo')
+
+console.log('response received', result1.statusCode) // response received 200
+
+for await (const data of result1.body) {
+ console.log('data', data.toString('utf8')) // data foo
+}
+
+const result2 = await request('http://localhost:3000/hello')
+
+console.log('response received', result2.statusCode) // response received 200
+
+for await (const data of result2.body) {
+ console.log('data', data.toString('utf8')) // data hello
+}
+```
+#### Example - Mock different requests within the same file
+```js
+const { MockAgent, setGlobalDispatcher } = require('undici');
+const agent = new MockAgent();
+agent.disableNetConnect();
+setGlobalDispatcher(agent);
+describe('Test', () => {
+ it('200', async () => {
+ const mockAgent = agent.get('http://test.com');
+ // your test
+ });
+ it('200', async () => {
+ const mockAgent = agent.get('http://testing.com');
+ // your test
+ });
+});
+```
+
+#### Example - Mocked request with query body, headers and trailers
+
+```js
+import { MockAgent, setGlobalDispatcher, request } from 'undici'
+
+const mockAgent = new MockAgent()
+setGlobalDispatcher(mockAgent)
+
+const mockPool = mockAgent.get('http://localhost:3000')
+
+mockPool.intercept({
+ path: '/foo?hello=there&see=ya',
+ method: 'POST',
+ body: 'form1=data1&form2=data2'
+}).reply(200, { foo: 'bar' }, {
+ headers: { 'content-type': 'application/json' },
+ trailers: { 'Content-MD5': 'test' }
+})
+
+const {
+ statusCode,
+ headers,
+ trailers,
+ body
+} = await request('http://localhost:3000/foo?hello=there&see=ya', {
+ method: 'POST',
+ body: 'form1=data1&form2=data2'
+})
+
+console.log('response received', statusCode) // response received 200
+console.log('headers', headers) // { 'content-type': 'application/json' }
+
+for await (const data of body) {
+ console.log('data', data.toString('utf8')) // '{"foo":"bar"}'
+}
+
+console.log('trailers', trailers) // { 'content-md5': 'test' }
+```
+
+#### Example - Mocked request with origin regex
+
+```js
+import { MockAgent, setGlobalDispatcher, request } from 'undici'
+
+const mockAgent = new MockAgent()
+setGlobalDispatcher(mockAgent)
+
+const mockPool = mockAgent.get(new RegExp('http://localhost:3000'))
+mockPool.intercept({ path: '/foo' }).reply(200, 'foo')
+
+const {
+ statusCode,
+ body
+} = await request('http://localhost:3000/foo')
+
+console.log('response received', statusCode) // response received 200
+
+for await (const data of body) {
+ console.log('data', data.toString('utf8')) // data foo
+}
+```
+
+#### Example - Mocked request with origin function
+
+```js
+import { MockAgent, setGlobalDispatcher, request } from 'undici'
+
+const mockAgent = new MockAgent()
+setGlobalDispatcher(mockAgent)
+
+const mockPool = mockAgent.get((origin) => origin === 'http://localhost:3000')
+mockPool.intercept({ path: '/foo' }).reply(200, 'foo')
+
+const {
+ statusCode,
+ body
+} = await request('http://localhost:3000/foo')
+
+console.log('response received', statusCode) // response received 200
+
+for await (const data of body) {
+ console.log('data', data.toString('utf8')) // data foo
+}
+```
+
+### `MockAgent.close()`
+
+Closes the mock agent and waits for registered mock pools and clients to also close before resolving.
+
+Returns: `Promise<void>`
+
+#### Example - clean up after tests are complete
+
+```js
+import { MockAgent, setGlobalDispatcher } from 'undici'
+
+const mockAgent = new MockAgent()
+setGlobalDispatcher(mockAgent)
+
+await mockAgent.close()
+```
+
+### `MockAgent.dispatch(options, handlers)`
+
+Implements [`Agent.dispatch(options, handlers)`](Agent.md#parameter-agentdispatchoptions).
+
+### `MockAgent.request(options[, callback])`
+
+See [`Dispatcher.request(options [, callback])`](Dispatcher.md#dispatcherrequestoptions-callback).
+
+#### Example - MockAgent request
+
+```js
+import { MockAgent } from 'undici'
+
+const mockAgent = new MockAgent()
+
+const mockPool = mockAgent.get('http://localhost:3000')
+mockPool.intercept({ path: '/foo' }).reply(200, 'foo')
+
+const {
+ statusCode,
+ body
+} = await mockAgent.request({
+ origin: 'http://localhost:3000',
+ path: '/foo',
+ method: 'GET'
+})
+
+console.log('response received', statusCode) // response received 200
+
+for await (const data of body) {
+ console.log('data', data.toString('utf8')) // data foo
+}
+```
+
+### `MockAgent.deactivate()`
+
+This method disables mocking in MockAgent.
+
+Returns: `void`
+
+#### Example - Deactivate Mocking
+
+```js
+import { MockAgent, setGlobalDispatcher } from 'undici'
+
+const mockAgent = new MockAgent()
+setGlobalDispatcher(mockAgent)
+
+mockAgent.deactivate()
+```
+
+### `MockAgent.activate()`
+
+This method enables mocking in a MockAgent instance. When instantiated, a MockAgent is automatically activated. Therefore, this method is only effective after `MockAgent.deactivate` has been called.
+
+Returns: `void`
+
+#### Example - Activate Mocking
+
+```js
+import { MockAgent, setGlobalDispatcher } from 'undici'
+
+const mockAgent = new MockAgent()
+setGlobalDispatcher(mockAgent)
+
+mockAgent.deactivate()
+// No mocking will occur
+
+// Later
+mockAgent.activate()
+```
+
+### `MockAgent.enableNetConnect([host])`
+
+When requests are not matched in a MockAgent intercept, a real HTTP request is attempted. We can control this further through the use of `enableNetConnect`. This is achieved by defining host matchers so only matching requests will be attempted.
+
+When using a string, it should only include the **hostname and optionally, the port**. In addition, calling this method multiple times with a string will allow all HTTP requests that match these values.
+
+Arguments:
+
+* **host** `string | RegExp | (value) => boolean` - (optional)
+
+Returns: `void`
+
+#### Example - Allow all non-matching urls to be dispatched in a real HTTP request
+
+```js
+import { MockAgent, setGlobalDispatcher, request } from 'undici'
+
+const mockAgent = new MockAgent()
+setGlobalDispatcher(mockAgent)
+
+mockAgent.enableNetConnect()
+
+await request('http://example.com')
+// A real request is made
+```
+
+#### Example - Allow requests matching a host string to make real requests
+
+```js
+import { MockAgent, setGlobalDispatcher, request } from 'undici'
+
+const mockAgent = new MockAgent()
+setGlobalDispatcher(mockAgent)
+
+mockAgent.enableNetConnect('example-1.com')
+mockAgent.enableNetConnect('example-2.com:8080')
+
+await request('http://example-1.com')
+// A real request is made
+
+await request('http://example-2.com:8080')
+// A real request is made
+
+await request('http://example-3.com')
+// Will throw
+```
+
+#### Example - Allow requests matching a host regex to make real requests
+
+```js
+import { MockAgent, setGlobalDispatcher, request } from 'undici'
+
+const mockAgent = new MockAgent()
+setGlobalDispatcher(mockAgent)
+
+mockAgent.enableNetConnect(new RegExp('example.com'))
+
+await request('http://example.com')
+// A real request is made
+```
+
+#### Example - Allow requests matching a host function to make real requests
+
+```js
+import { MockAgent, setGlobalDispatcher, request } from 'undici'
+
+const mockAgent = new MockAgent()
+setGlobalDispatcher(mockAgent)
+
+mockAgent.enableNetConnect((value) => value === 'example.com')
+
+await request('http://example.com')
+// A real request is made
+```
+
+### `MockAgent.disableNetConnect()`
+
+This method causes all requests to throw when requests are not matched in a MockAgent intercept.
+
+Returns: `void`
+
+#### Example - Disable all non-matching requests by throwing an error for each
+
+```js
+import { MockAgent, request } from 'undici'
+
+const mockAgent = new MockAgent()
+
+mockAgent.disableNetConnect()
+
+await request('http://example.com')
+// Will throw
+```
+
+### `MockAgent.pendingInterceptors()`
+
+This method returns any pending interceptors registered on a mock agent. A pending interceptor meets one of the following criteria:
+
+- Is registered with neither `.times(<number>)` nor `.persist()`, and has not been invoked;
+- Is persistent (i.e., registered with `.persist()`) and has not been invoked;
+- Is registered with `.times(<number>)` and has not been invoked `<number>` of times.
+
+Returns: `PendingInterceptor[]` (where `PendingInterceptor` is a `MockDispatch` with an additional `origin: string`)
+
+#### Example - List all pending inteceptors
+
+```js
+const agent = new MockAgent()
+agent.disableNetConnect()
+
+agent
+ .get('https://example.com')
+ .intercept({ method: 'GET', path: '/' })
+ .reply(200)
+
+const pendingInterceptors = agent.pendingInterceptors()
+// Returns [
+// {
+// timesInvoked: 0,
+// times: 1,
+// persist: false,
+// consumed: false,
+// pending: true,
+// path: '/',
+// method: 'GET',
+// body: undefined,
+// headers: undefined,
+// data: {
+// error: null,
+// statusCode: 200,
+// data: '',
+// headers: {},
+// trailers: {}
+// },
+// origin: 'https://example.com'
+// }
+// ]
+```
+
+### `MockAgent.assertNoPendingInterceptors([options])`
+
+This method throws if the mock agent has any pending interceptors. A pending interceptor meets one of the following criteria:
+
+- Is registered with neither `.times(<number>)` nor `.persist()`, and has not been invoked;
+- Is persistent (i.e., registered with `.persist()`) and has not been invoked;
+- Is registered with `.times(<number>)` and has not been invoked `<number>` of times.
+
+#### Example - Check that there are no pending interceptors
+
+```js
+const agent = new MockAgent()
+agent.disableNetConnect()
+
+agent
+ .get('https://example.com')
+ .intercept({ method: 'GET', path: '/' })
+ .reply(200)
+
+agent.assertNoPendingInterceptors()
+// Throws an UndiciError with the following message:
+//
+// 1 interceptor is pending:
+//
+// ┌─────────┬────────┬───────────────────────┬──────┬─────────────┬────────────┬─────────────┬───────────â”
+// │ (index) │ Method │ Origin │ Path │ Status code │ Persistent │ Invocations │ Remaining │
+// ├─────────┼────────┼───────────────────────┼──────┼─────────────┼────────────┼─────────────┼───────────┤
+// │ 0 │ 'GET' │ 'https://example.com' │ '/' │ 200 │ 'âŒ' │ 0 │ 1 │
+// └─────────┴────────┴───────────────────────┴──────┴─────────────┴────────────┴─────────────┴───────────┘
+```
diff --git a/docs/api/MockClient.md b/docs/api/MockClient.md
new file mode 100644
index 0000000..ac54691
--- /dev/null
+++ b/docs/api/MockClient.md
@@ -0,0 +1,77 @@
+# Class: MockClient
+
+Extends: `undici.Client`
+
+A mock client class that implements the same api as [MockPool](MockPool.md).
+
+## `new MockClient(origin, [options])`
+
+Arguments:
+
+* **origin** `string` - It should only include the **protocol, hostname, and port**.
+* **options** `MockClientOptions` - It extends the `Client` options.
+
+Returns: `MockClient`
+
+### Parameter: `MockClientOptions`
+
+Extends: `ClientOptions`
+
+* **agent** `Agent` - the agent to associate this MockClient with.
+
+### Example - Basic MockClient instantiation
+
+We can use MockAgent to instantiate a MockClient ready to be used to intercept specified requests. It will not do anything until registered as the agent to use and any mock request are registered.
+
+```js
+import { MockAgent } from 'undici'
+
+// Connections must be set to 1 to return a MockClient instance
+const mockAgent = new MockAgent({ connections: 1 })
+
+const mockClient = mockAgent.get('http://localhost:3000')
+```
+
+## Instance Methods
+
+### `MockClient.intercept(options)`
+
+Implements: [`MockPool.intercept(options)`](MockPool.md#mockpoolinterceptoptions)
+
+### `MockClient.close()`
+
+Implements: [`MockPool.close()`](MockPool.md#mockpoolclose)
+
+### `MockClient.dispatch(options, handlers)`
+
+Implements [`Dispatcher.dispatch(options, handlers)`](Dispatcher.md#dispatcherdispatchoptions-handler).
+
+### `MockClient.request(options[, callback])`
+
+See [`Dispatcher.request(options [, callback])`](Dispatcher.md#dispatcherrequestoptions-callback).
+
+#### Example - MockClient request
+
+```js
+import { MockAgent } from 'undici'
+
+const mockAgent = new MockAgent({ connections: 1 })
+
+const mockClient = mockAgent.get('http://localhost:3000')
+mockClient.intercept({ path: '/foo' }).reply(200, 'foo')
+
+const {
+ statusCode,
+ body
+} = await mockClient.request({
+ origin: 'http://localhost:3000',
+ path: '/foo',
+ method: 'GET'
+})
+
+console.log('response received', statusCode) // response received 200
+
+for await (const data of body) {
+ console.log('data', data.toString('utf8')) // data foo
+}
+```
diff --git a/docs/api/MockErrors.md b/docs/api/MockErrors.md
new file mode 100644
index 0000000..c1aa3db
--- /dev/null
+++ b/docs/api/MockErrors.md
@@ -0,0 +1,12 @@
+# MockErrors
+
+Undici exposes a variety of mock error objects that you can use to enhance your mock error handling.
+You can find all the mock error objects inside the `mockErrors` key.
+
+```js
+import { mockErrors } from 'undici'
+```
+
+| Mock Error | Mock Error Codes | Description |
+| --------------------- | ------------------------------- | ---------------------------------------------------------- |
+| `MockNotMatchedError` | `UND_MOCK_ERR_MOCK_NOT_MATCHED` | The request does not match any registered mock dispatches. |
diff --git a/docs/api/MockPool.md b/docs/api/MockPool.md
new file mode 100644
index 0000000..96a986f
--- /dev/null
+++ b/docs/api/MockPool.md
@@ -0,0 +1,547 @@
+# Class: MockPool
+
+Extends: `undici.Pool`
+
+A mock Pool class that implements the Pool API and is used by MockAgent to intercept real requests and return mocked responses.
+
+## `new MockPool(origin, [options])`
+
+Arguments:
+
+* **origin** `string` - It should only include the **protocol, hostname, and port**.
+* **options** `MockPoolOptions` - It extends the `Pool` options.
+
+Returns: `MockPool`
+
+### Parameter: `MockPoolOptions`
+
+Extends: `PoolOptions`
+
+* **agent** `Agent` - the agent to associate this MockPool with.
+
+### Example - Basic MockPool instantiation
+
+We can use MockAgent to instantiate a MockPool ready to be used to intercept specified requests. It will not do anything until registered as the agent to use and any mock request are registered.
+
+```js
+import { MockAgent } from 'undici'
+
+const mockAgent = new MockAgent()
+
+const mockPool = mockAgent.get('http://localhost:3000')
+```
+
+## Instance Methods
+
+### `MockPool.intercept(options)`
+
+This method defines the interception rules for matching against requests for a MockPool or MockPool. We can intercept multiple times on a single instance, but each intercept is only used once. For example if you expect to make 2 requests inside a test, you need to call `intercept()` twice. Assuming you use `disableNetConnect()` you will get `MockNotMatchedError` on the second request when you only call `intercept()` once.
+
+When defining interception rules, all the rules must pass for a request to be intercepted. If a request is not intercepted, a real request will be attempted.
+
+| Matcher type | Condition to pass |
+|:------------:| -------------------------- |
+| `string` | Exact match against string |
+| `RegExp` | Regex must pass |
+| `Function` | Function must return true |
+
+Arguments:
+
+* **options** `MockPoolInterceptOptions` - Interception options.
+
+Returns: `MockInterceptor` corresponding to the input options.
+
+### Parameter: `MockPoolInterceptOptions`
+
+* **path** `string | RegExp | (path: string) => boolean` - a matcher for the HTTP request path. When a `RegExp` or callback is used, it will match against the request path including all query parameters in alphabetical order. When a `string` is provided, the query parameters can be conveniently specified through the `MockPoolInterceptOptions.query` setting.
+* **method** `string | RegExp | (method: string) => boolean` - (optional) - a matcher for the HTTP request method. Defaults to `GET`.
+* **body** `string | RegExp | (body: string) => boolean` - (optional) - a matcher for the HTTP request body.
+* **headers** `Record<string, string | RegExp | (body: string) => boolean`> - (optional) - a matcher for the HTTP request headers. To be intercepted, a request must match all defined headers. Extra headers not defined here may (or may not) be included in the request and do not affect the interception in any way.
+* **query** `Record<string, any> | null` - (optional) - a matcher for the HTTP request query string params. Only applies when a `string` was provided for `MockPoolInterceptOptions.path`.
+
+### Return: `MockInterceptor`
+
+We can define the behaviour of an intercepted request with the following options.
+
+* **reply** `(statusCode: number, replyData: string | Buffer | object | MockInterceptor.MockResponseDataHandler, responseOptions?: MockResponseOptions) => MockScope` - define a reply for a matching request. You can define the replyData as a callback to read incoming request data. Default for `responseOptions` is `{}`.
+* **reply** `(callback: MockInterceptor.MockReplyOptionsCallback) => MockScope` - define a reply for a matching request, allowing dynamic mocking of all reply options rather than just the data.
+* **replyWithError** `(error: Error) => MockScope` - define an error for a matching request to throw.
+* **defaultReplyHeaders** `(headers: Record<string, string>) => MockInterceptor` - define default headers to be included in subsequent replies. These are in addition to headers on a specific reply.
+* **defaultReplyTrailers** `(trailers: Record<string, string>) => MockInterceptor` - define default trailers to be included in subsequent replies. These are in addition to trailers on a specific reply.
+* **replyContentLength** `() => MockInterceptor` - define automatically calculated `content-length` headers to be included in subsequent replies.
+
+The reply data of an intercepted request may either be a string, buffer, or JavaScript object. Objects are converted to JSON while strings and buffers are sent as-is.
+
+By default, `reply` and `replyWithError` define the behaviour for the first matching request only. Subsequent requests will not be affected (this can be changed using the returned `MockScope`).
+
+### Parameter: `MockResponseOptions`
+
+* **headers** `Record<string, string>` - headers to be included on the mocked reply.
+* **trailers** `Record<string, string>` - trailers to be included on the mocked reply.
+
+### Return: `MockScope`
+
+A `MockScope` is associated with a single `MockInterceptor`. With this, we can configure the default behaviour of a intercepted reply.
+
+* **delay** `(waitInMs: number) => MockScope` - delay the associated reply by a set amount in ms.
+* **persist** `() => MockScope` - any matching request will always reply with the defined response indefinitely.
+* **times** `(repeatTimes: number) => MockScope` - any matching request will reply with the defined response a fixed amount of times. This is overridden by **persist**.
+
+#### Example - Basic Mocked Request
+
+```js
+import { MockAgent, setGlobalDispatcher, request } from 'undici'
+
+const mockAgent = new MockAgent()
+setGlobalDispatcher(mockAgent)
+
+// MockPool
+const mockPool = mockAgent.get('http://localhost:3000')
+mockPool.intercept({ path: '/foo' }).reply(200, 'foo')
+
+const {
+ statusCode,
+ body
+} = await request('http://localhost:3000/foo')
+
+console.log('response received', statusCode) // response received 200
+
+for await (const data of body) {
+ console.log('data', data.toString('utf8')) // data foo
+}
+```
+
+#### Example - Mocked request using reply data callbacks
+
+```js
+import { MockAgent, setGlobalDispatcher, request } from 'undici'
+
+const mockAgent = new MockAgent()
+setGlobalDispatcher(mockAgent)
+
+const mockPool = mockAgent.get('http://localhost:3000')
+
+mockPool.intercept({
+ path: '/echo',
+ method: 'GET',
+ headers: {
+ 'User-Agent': 'undici',
+ Host: 'example.com'
+ }
+}).reply(200, ({ headers }) => ({ message: headers.get('message') }))
+
+const { statusCode, body, headers } = await request('http://localhost:3000', {
+ headers: {
+ message: 'hello world!'
+ }
+})
+
+console.log('response received', statusCode) // response received 200
+console.log('headers', headers) // { 'content-type': 'application/json' }
+
+for await (const data of body) {
+ console.log('data', data.toString('utf8')) // { "message":"hello world!" }
+}
+```
+
+#### Example - Mocked request using reply options callback
+
+```js
+import { MockAgent, setGlobalDispatcher, request } from 'undici'
+
+const mockAgent = new MockAgent()
+setGlobalDispatcher(mockAgent)
+
+const mockPool = mockAgent.get('http://localhost:3000')
+
+mockPool.intercept({
+ path: '/echo',
+ method: 'GET',
+ headers: {
+ 'User-Agent': 'undici',
+ Host: 'example.com'
+ }
+}).reply(({ headers }) => ({ statusCode: 200, data: { message: headers.get('message') }})))
+
+const { statusCode, body, headers } = await request('http://localhost:3000', {
+ headers: {
+ message: 'hello world!'
+ }
+})
+
+console.log('response received', statusCode) // response received 200
+console.log('headers', headers) // { 'content-type': 'application/json' }
+
+for await (const data of body) {
+ console.log('data', data.toString('utf8')) // { "message":"hello world!" }
+}
+```
+
+#### Example - Basic Mocked requests with multiple intercepts
+
+```js
+import { MockAgent, setGlobalDispatcher, request } from 'undici'
+
+const mockAgent = new MockAgent()
+setGlobalDispatcher(mockAgent)
+
+const mockPool = mockAgent.get('http://localhost:3000')
+
+mockPool.intercept({
+ path: '/foo',
+ method: 'GET'
+}).reply(200, 'foo')
+
+mockPool.intercept({
+ path: '/hello',
+ method: 'GET',
+}).reply(200, 'hello')
+
+const result1 = await request('http://localhost:3000/foo')
+
+console.log('response received', result1.statusCode) // response received 200
+
+for await (const data of result1.body) {
+ console.log('data', data.toString('utf8')) // data foo
+}
+
+const result2 = await request('http://localhost:3000/hello')
+
+console.log('response received', result2.statusCode) // response received 200
+
+for await (const data of result2.body) {
+ console.log('data', data.toString('utf8')) // data hello
+}
+```
+
+#### Example - Mocked request with query body, request headers and response headers and trailers
+
+```js
+import { MockAgent, setGlobalDispatcher, request } from 'undici'
+
+const mockAgent = new MockAgent()
+setGlobalDispatcher(mockAgent)
+
+const mockPool = mockAgent.get('http://localhost:3000')
+
+mockPool.intercept({
+ path: '/foo?hello=there&see=ya',
+ method: 'POST',
+ body: 'form1=data1&form2=data2',
+ headers: {
+ 'User-Agent': 'undici',
+ Host: 'example.com'
+ }
+}).reply(200, { foo: 'bar' }, {
+ headers: { 'content-type': 'application/json' },
+ trailers: { 'Content-MD5': 'test' }
+})
+
+const {
+ statusCode,
+ headers,
+ trailers,
+ body
+} = await request('http://localhost:3000/foo?hello=there&see=ya', {
+ method: 'POST',
+ body: 'form1=data1&form2=data2',
+ headers: {
+ foo: 'bar',
+ 'User-Agent': 'undici',
+ Host: 'example.com'
+ }
+ })
+
+console.log('response received', statusCode) // response received 200
+console.log('headers', headers) // { 'content-type': 'application/json' }
+
+for await (const data of body) {
+ console.log('data', data.toString('utf8')) // '{"foo":"bar"}'
+}
+
+console.log('trailers', trailers) // { 'content-md5': 'test' }
+```
+
+#### Example - Mocked request using different matchers
+
+```js
+import { MockAgent, setGlobalDispatcher, request } from 'undici'
+
+const mockAgent = new MockAgent()
+setGlobalDispatcher(mockAgent)
+
+const mockPool = mockAgent.get('http://localhost:3000')
+
+mockPool.intercept({
+ path: '/foo',
+ method: /^GET$/,
+ body: (value) => value === 'form=data',
+ headers: {
+ 'User-Agent': 'undici',
+ Host: /^example.com$/
+ }
+}).reply(200, 'foo')
+
+const {
+ statusCode,
+ body
+} = await request('http://localhost:3000/foo', {
+ method: 'GET',
+ body: 'form=data',
+ headers: {
+ foo: 'bar',
+ 'User-Agent': 'undici',
+ Host: 'example.com'
+ }
+})
+
+console.log('response received', statusCode) // response received 200
+
+for await (const data of body) {
+ console.log('data', data.toString('utf8')) // data foo
+}
+```
+
+#### Example - Mocked request with reply with a defined error
+
+```js
+import { MockAgent, setGlobalDispatcher, request } from 'undici'
+
+const mockAgent = new MockAgent()
+setGlobalDispatcher(mockAgent)
+
+const mockPool = mockAgent.get('http://localhost:3000')
+
+mockPool.intercept({
+ path: '/foo',
+ method: 'GET'
+}).replyWithError(new Error('kaboom'))
+
+try {
+ await request('http://localhost:3000/foo', {
+ method: 'GET'
+ })
+} catch (error) {
+ console.error(error) // Error: kaboom
+}
+```
+
+#### Example - Mocked request with defaultReplyHeaders
+
+```js
+import { MockAgent, setGlobalDispatcher, request } from 'undici'
+
+const mockAgent = new MockAgent()
+setGlobalDispatcher(mockAgent)
+
+const mockPool = mockAgent.get('http://localhost:3000')
+
+mockPool.intercept({
+ path: '/foo',
+ method: 'GET'
+}).defaultReplyHeaders({ foo: 'bar' })
+ .reply(200, 'foo')
+
+const { headers } = await request('http://localhost:3000/foo')
+
+console.log('headers', headers) // headers { foo: 'bar' }
+```
+
+#### Example - Mocked request with defaultReplyTrailers
+
+```js
+import { MockAgent, setGlobalDispatcher, request } from 'undici'
+
+const mockAgent = new MockAgent()
+setGlobalDispatcher(mockAgent)
+
+const mockPool = mockAgent.get('http://localhost:3000')
+
+mockPool.intercept({
+ path: '/foo',
+ method: 'GET'
+}).defaultReplyTrailers({ foo: 'bar' })
+ .reply(200, 'foo')
+
+const { trailers } = await request('http://localhost:3000/foo')
+
+console.log('trailers', trailers) // trailers { foo: 'bar' }
+```
+
+#### Example - Mocked request with automatic content-length calculation
+
+```js
+import { MockAgent, setGlobalDispatcher, request } from 'undici'
+
+const mockAgent = new MockAgent()
+setGlobalDispatcher(mockAgent)
+
+const mockPool = mockAgent.get('http://localhost:3000')
+
+mockPool.intercept({
+ path: '/foo',
+ method: 'GET'
+}).replyContentLength().reply(200, 'foo')
+
+const { headers } = await request('http://localhost:3000/foo')
+
+console.log('headers', headers) // headers { 'content-length': '3' }
+```
+
+#### Example - Mocked request with automatic content-length calculation on an object
+
+```js
+import { MockAgent, setGlobalDispatcher, request } from 'undici'
+
+const mockAgent = new MockAgent()
+setGlobalDispatcher(mockAgent)
+
+const mockPool = mockAgent.get('http://localhost:3000')
+
+mockPool.intercept({
+ path: '/foo',
+ method: 'GET'
+}).replyContentLength().reply(200, { foo: 'bar' })
+
+const { headers } = await request('http://localhost:3000/foo')
+
+console.log('headers', headers) // headers { 'content-length': '13' }
+```
+
+#### Example - Mocked request with persist enabled
+
+```js
+import { MockAgent, setGlobalDispatcher, request } from 'undici'
+
+const mockAgent = new MockAgent()
+setGlobalDispatcher(mockAgent)
+
+const mockPool = mockAgent.get('http://localhost:3000')
+
+mockPool.intercept({
+ path: '/foo',
+ method: 'GET'
+}).reply(200, 'foo').persist()
+
+const result1 = await request('http://localhost:3000/foo')
+// Will match and return mocked data
+
+const result2 = await request('http://localhost:3000/foo')
+// Will match and return mocked data
+
+// Etc
+```
+
+#### Example - Mocked request with times enabled
+
+```js
+import { MockAgent, setGlobalDispatcher, request } from 'undici'
+
+const mockAgent = new MockAgent()
+setGlobalDispatcher(mockAgent)
+
+const mockPool = mockAgent.get('http://localhost:3000')
+
+mockPool.intercept({
+ path: '/foo',
+ method: 'GET'
+}).reply(200, 'foo').times(2)
+
+const result1 = await request('http://localhost:3000/foo')
+// Will match and return mocked data
+
+const result2 = await request('http://localhost:3000/foo')
+// Will match and return mocked data
+
+const result3 = await request('http://localhost:3000/foo')
+// Will not match and make attempt a real request
+```
+
+#### Example - Mocked request with path callback
+
+```js
+import { MockAgent, setGlobalDispatcher, request } from 'undici'
+import querystring from 'querystring'
+
+const mockAgent = new MockAgent()
+setGlobalDispatcher(mockAgent)
+
+const mockPool = mockAgent.get('http://localhost:3000')
+
+const matchPath = requestPath => {
+ const [pathname, search] = requestPath.split('?')
+ const requestQuery = querystring.parse(search)
+
+ if (!pathname.startsWith('/foo')) {
+ return false
+ }
+
+ if (!Object.keys(requestQuery).includes('foo') || requestQuery.foo !== 'bar') {
+ return false
+ }
+
+ return true
+}
+
+mockPool.intercept({
+ path: matchPath,
+ method: 'GET'
+}).reply(200, 'foo')
+
+const result = await request('http://localhost:3000/foo?foo=bar')
+// Will match and return mocked data
+```
+
+### `MockPool.close()`
+
+Closes the mock pool and de-registers from associated MockAgent.
+
+Returns: `Promise<void>`
+
+#### Example - clean up after tests are complete
+
+```js
+import { MockAgent } from 'undici'
+
+const mockAgent = new MockAgent()
+const mockPool = mockAgent.get('http://localhost:3000')
+
+await mockPool.close()
+```
+
+### `MockPool.dispatch(options, handlers)`
+
+Implements [`Dispatcher.dispatch(options, handlers)`](Dispatcher.md#dispatcherdispatchoptions-handler).
+
+### `MockPool.request(options[, callback])`
+
+See [`Dispatcher.request(options [, callback])`](Dispatcher.md#dispatcherrequestoptions-callback).
+
+#### Example - MockPool request
+
+```js
+import { MockAgent } from 'undici'
+
+const mockAgent = new MockAgent()
+
+const mockPool = mockAgent.get('http://localhost:3000')
+mockPool.intercept({
+ path: '/foo',
+ method: 'GET',
+}).reply(200, 'foo')
+
+const {
+ statusCode,
+ body
+} = await mockPool.request({
+ origin: 'http://localhost:3000',
+ path: '/foo',
+ method: 'GET'
+})
+
+console.log('response received', statusCode) // response received 200
+
+for await (const data of body) {
+ console.log('data', data.toString('utf8')) // data foo
+}
+```
diff --git a/docs/api/Pool.md b/docs/api/Pool.md
new file mode 100644
index 0000000..8fcabac
--- /dev/null
+++ b/docs/api/Pool.md
@@ -0,0 +1,84 @@
+# Class: Pool
+
+Extends: `undici.Dispatcher`
+
+A pool of [Client](Client.md) instances connected to the same upstream target.
+
+Requests are not guaranteed to be dispatched in order of invocation.
+
+## `new Pool(url[, options])`
+
+Arguments:
+
+* **url** `URL | string` - It should only include the **protocol, hostname, and port**.
+* **options** `PoolOptions` (optional)
+
+### Parameter: `PoolOptions`
+
+Extends: [`ClientOptions`](Client.md#parameter-clientoptions)
+
+* **factory** `(origin: URL, opts: Object) => Dispatcher` - Default: `(origin, opts) => new Client(origin, opts)`
+* **connections** `number | null` (optional) - Default: `null` - The number of `Client` instances to create. When set to `null`, the `Pool` instance will create an unlimited amount of `Client` instances.
+* **interceptors** `{ Pool: DispatchInterceptor[] } }` - Default: `{ Pool: [] }` - A list of interceptors that are applied to the dispatch method. Additional logic can be applied (such as, but not limited to: 302 status code handling, authentication, cookies, compression and caching).
+
+## Instance Properties
+
+### `Pool.closed`
+
+Implements [Client.closed](Client.md#clientclosed)
+
+### `Pool.destroyed`
+
+Implements [Client.destroyed](Client.md#clientdestroyed)
+
+### `Pool.stats`
+
+Returns [`PoolStats`](PoolStats.md) instance for this pool.
+
+## Instance Methods
+
+### `Pool.close([callback])`
+
+Implements [`Dispatcher.close([callback])`](Dispatcher.md#dispatcherclosecallback-promise).
+
+### `Pool.destroy([error, callback])`
+
+Implements [`Dispatcher.destroy([error, callback])`](Dispatcher.md#dispatcherdestroyerror-callback-promise).
+
+### `Pool.connect(options[, callback])`
+
+See [`Dispatcher.connect(options[, callback])`](Dispatcher.md#dispatcherconnectoptions-callback).
+
+### `Pool.dispatch(options, handler)`
+
+Implements [`Dispatcher.dispatch(options, handler)`](Dispatcher.md#dispatcherdispatchoptions-handler).
+
+### `Pool.pipeline(options, handler)`
+
+See [`Dispatcher.pipeline(options, handler)`](Dispatcher.md#dispatcherpipelineoptions-handler).
+
+### `Pool.request(options[, callback])`
+
+See [`Dispatcher.request(options [, callback])`](Dispatcher.md#dispatcherrequestoptions-callback).
+
+### `Pool.stream(options, factory[, callback])`
+
+See [`Dispatcher.stream(options, factory[, callback])`](Dispatcher.md#dispatcherstreamoptions-factory-callback).
+
+### `Pool.upgrade(options[, callback])`
+
+See [`Dispatcher.upgrade(options[, callback])`](Dispatcher.md#dispatcherupgradeoptions-callback).
+
+## Instance Events
+
+### Event: `'connect'`
+
+See [Dispatcher Event: `'connect'`](Dispatcher.md#event-connect).
+
+### Event: `'disconnect'`
+
+See [Dispatcher Event: `'disconnect'`](Dispatcher.md#event-disconnect).
+
+### Event: `'drain'`
+
+See [Dispatcher Event: `'drain'`](Dispatcher.md#event-drain).
diff --git a/docs/api/PoolStats.md b/docs/api/PoolStats.md
new file mode 100644
index 0000000..16b6dc2
--- /dev/null
+++ b/docs/api/PoolStats.md
@@ -0,0 +1,35 @@
+# Class: PoolStats
+
+Aggregate stats for a [Pool](Pool.md) or [BalancedPool](BalancedPool.md).
+
+## `new PoolStats(pool)`
+
+Arguments:
+
+* **pool** `Pool` - Pool or BalancedPool from which to return stats.
+
+## Instance Properties
+
+### `PoolStats.connected`
+
+Number of open socket connections in this pool.
+
+### `PoolStats.free`
+
+Number of open socket connections in this pool that do not have an active request.
+
+### `PoolStats.pending`
+
+Number of pending requests across all clients in this pool.
+
+### `PoolStats.queued`
+
+Number of queued requests across all clients in this pool.
+
+### `PoolStats.running`
+
+Number of currently active requests across all clients in this pool.
+
+### `PoolStats.size`
+
+Number of active, pending, or queued requests across all clients in this pool.
diff --git a/docs/api/ProxyAgent.md b/docs/api/ProxyAgent.md
new file mode 100644
index 0000000..cebfe68
--- /dev/null
+++ b/docs/api/ProxyAgent.md
@@ -0,0 +1,126 @@
+# Class: ProxyAgent
+
+Extends: `undici.Dispatcher`
+
+A Proxy Agent class that implements the Agent API. It allows the connection through proxy in a simple way.
+
+## `new ProxyAgent([options])`
+
+Arguments:
+
+* **options** `ProxyAgentOptions` (required) - It extends the `Agent` options.
+
+Returns: `ProxyAgent`
+
+### Parameter: `ProxyAgentOptions`
+
+Extends: [`AgentOptions`](Agent.md#parameter-agentoptions)
+
+* **uri** `string` (required) - It can be passed either by a string or a object containing `uri` as string.
+* **token** `string` (optional) - It can be passed by a string of token for authentication.
+* **auth** `string` (**deprecated**) - Use token.
+* **clientFactory** `(origin: URL, opts: Object) => Dispatcher` (optional) - Default: `(origin, opts) => new Pool(origin, opts)`
+* **requestTls** `BuildOptions` (optional) - Options object passed when creating the underlying socket via the connector builder for the request. See [TLS](https://nodejs.org/api/tls.html#tlsconnectoptions-callback).
+* **proxyTls** `BuildOptions` (optional) - Options object passed when creating the underlying socket via the connector builder for the proxy server. See [TLS](https://nodejs.org/api/tls.html#tlsconnectoptions-callback).
+
+Examples:
+
+```js
+import { ProxyAgent } from 'undici'
+
+const proxyAgent = new ProxyAgent('my.proxy.server')
+// or
+const proxyAgent = new ProxyAgent({ uri: 'my.proxy.server' })
+```
+
+#### Example - Basic ProxyAgent instantiation
+
+This will instantiate the ProxyAgent. It will not do anything until registered as the agent to use with requests.
+
+```js
+import { ProxyAgent } from 'undici'
+
+const proxyAgent = new ProxyAgent('my.proxy.server')
+```
+
+#### Example - Basic Proxy Request with global agent dispatcher
+
+```js
+import { setGlobalDispatcher, request, ProxyAgent } from 'undici'
+
+const proxyAgent = new ProxyAgent('my.proxy.server')
+setGlobalDispatcher(proxyAgent)
+
+const { statusCode, body } = await request('http://localhost:3000/foo')
+
+console.log('response received', statusCode) // response received 200
+
+for await (const data of body) {
+ console.log('data', data.toString('utf8')) // data foo
+}
+```
+
+#### Example - Basic Proxy Request with local agent dispatcher
+
+```js
+import { ProxyAgent, request } from 'undici'
+
+const proxyAgent = new ProxyAgent('my.proxy.server')
+
+const {
+ statusCode,
+ body
+} = await request('http://localhost:3000/foo', { dispatcher: proxyAgent })
+
+console.log('response received', statusCode) // response received 200
+
+for await (const data of body) {
+ console.log('data', data.toString('utf8')) // data foo
+}
+```
+
+#### Example - Basic Proxy Request with authentication
+
+```js
+import { setGlobalDispatcher, request, ProxyAgent } from 'undici';
+
+const proxyAgent = new ProxyAgent({
+ uri: 'my.proxy.server',
+ // token: 'Bearer xxxx'
+ token: `Basic ${Buffer.from('username:password').toString('base64')}`
+});
+setGlobalDispatcher(proxyAgent);
+
+const { statusCode, body } = await request('http://localhost:3000/foo');
+
+console.log('response received', statusCode); // response received 200
+
+for await (const data of body) {
+ console.log('data', data.toString('utf8')); // data foo
+}
+```
+
+### `ProxyAgent.close()`
+
+Closes the proxy agent and waits for registered pools and clients to also close before resolving.
+
+Returns: `Promise<void>`
+
+#### Example - clean up after tests are complete
+
+```js
+import { ProxyAgent, setGlobalDispatcher } from 'undici'
+
+const proxyAgent = new ProxyAgent('my.proxy.server')
+setGlobalDispatcher(proxyAgent)
+
+await proxyAgent.close()
+```
+
+### `ProxyAgent.dispatch(options, handlers)`
+
+Implements [`Agent.dispatch(options, handlers)`](Agent.md#parameter-agentdispatchoptions).
+
+### `ProxyAgent.request(options[, callback])`
+
+See [`Dispatcher.request(options [, callback])`](Dispatcher.md#dispatcherrequestoptions-callback).
diff --git a/docs/api/RetryHandler.md b/docs/api/RetryHandler.md
new file mode 100644
index 0000000..2323ce4
--- /dev/null
+++ b/docs/api/RetryHandler.md
@@ -0,0 +1,108 @@
+# Class: RetryHandler
+
+Extends: `undici.DispatcherHandlers`
+
+A handler class that implements the retry logic for a request.
+
+## `new RetryHandler(dispatchOptions, retryHandlers, [retryOptions])`
+
+Arguments:
+
+- **options** `Dispatch.DispatchOptions & RetryOptions` (required) - It is an intersection of `Dispatcher.DispatchOptions` and `RetryOptions`.
+- **retryHandlers** `RetryHandlers` (required) - Object containing the `dispatch` to be used on every retry, and `handler` for handling the `dispatch` lifecycle.
+
+Returns: `retryHandler`
+
+### Parameter: `Dispatch.DispatchOptions & RetryOptions`
+
+Extends: [`Dispatch.DispatchOptions`](Dispatcher.md#parameter-dispatchoptions).
+
+#### `RetryOptions`
+
+- **retry** `(err: Error, context: RetryContext, callback: (err?: Error | null) => void) => void` (optional) - Function to be called after every retry. It should pass error if no more retries should be performed.
+- **maxRetries** `number` (optional) - Maximum number of retries. Default: `5`
+- **maxTimeout** `number` (optional) - Maximum number of milliseconds to wait before retrying. Default: `30000` (30 seconds)
+- **minTimeout** `number` (optional) - Minimum number of milliseconds to wait before retrying. Default: `500` (half a second)
+- **timeoutFactor** `number` (optional) - Factor to multiply the timeout by for each retry attempt. Default: `2`
+- **retryAfter** `boolean` (optional) - It enables automatic retry after the `Retry-After` header is received. Default: `true`
+-
+- **methods** `string[]` (optional) - Array of HTTP methods to retry. Default: `['GET', 'PUT', 'HEAD', 'OPTIONS', 'DELETE']`
+- **statusCodes** `number[]` (optional) - Array of HTTP status codes to retry. Default: `[429, 500, 502, 503, 504]`
+- **errorCodes** `string[]` (optional) - Array of Error codes to retry. Default: `['ECONNRESET', 'ECONNREFUSED', 'ENOTFOUND', 'ENETDOWN','ENETUNREACH', 'EHOSTDOWN',
+
+**`RetryContext`**
+
+- `state`: `RetryState` - Current retry state. It can be mutated.
+- `opts`: `Dispatch.DispatchOptions & RetryOptions` - Options passed to the retry handler.
+
+### Parameter `RetryHandlers`
+
+- **dispatch** `(options: Dispatch.DispatchOptions, handlers: Dispatch.DispatchHandlers) => Promise<Dispatch.DispatchResponse>` (required) - Dispatch function to be called after every retry.
+- **handler** Extends [`Dispatch.DispatchHandlers`](Dispatcher.md#dispatcherdispatchoptions-handler) (required) - Handler function to be called after the request is successful or the retries are exhausted.
+
+Examples:
+
+```js
+const client = new Client(`http://localhost:${server.address().port}`);
+const chunks = [];
+const handler = new RetryHandler(
+ {
+ ...dispatchOptions,
+ retryOptions: {
+ // custom retry function
+ retry: function (err, state, callback) {
+ counter++;
+
+ if (err.code && err.code === "UND_ERR_DESTROYED") {
+ callback(err);
+ return;
+ }
+
+ if (err.statusCode === 206) {
+ callback(err);
+ return;
+ }
+
+ setTimeout(() => callback(null), 1000);
+ },
+ },
+ },
+ {
+ dispatch: (...args) => {
+ return client.dispatch(...args);
+ },
+ handler: {
+ onConnect() {},
+ onBodySent() {},
+ onHeaders(status, _rawHeaders, resume, _statusMessage) {
+ // do something with headers
+ },
+ onData(chunk) {
+ chunks.push(chunk);
+ return true;
+ },
+ onComplete() {},
+ onError() {
+ // handle error properly
+ },
+ },
+ }
+);
+```
+
+#### Example - Basic RetryHandler with defaults
+
+```js
+const client = new Client(`http://localhost:${server.address().port}`);
+const handler = new RetryHandler(dispatchOptions, {
+ dispatch: client.dispatch.bind(client),
+ handler: {
+ onConnect() {},
+ onBodySent() {},
+ onHeaders(status, _rawHeaders, resume, _statusMessage) {},
+ onData(chunk) {},
+ onComplete() {},
+ onError(err) {},
+ },
+});
+```
diff --git a/docs/api/WebSocket.md b/docs/api/WebSocket.md
new file mode 100644
index 0000000..9d374f4
--- /dev/null
+++ b/docs/api/WebSocket.md
@@ -0,0 +1,43 @@
+# Class: WebSocket
+
+> âš ï¸ Warning: the WebSocket API is experimental.
+
+Extends: [`EventTarget`](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget)
+
+The WebSocket object provides a way to manage a WebSocket connection to a server, allowing bidirectional communication. The API follows the [WebSocket spec](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) and [RFC 6455](https://datatracker.ietf.org/doc/html/rfc6455).
+
+## `new WebSocket(url[, protocol])`
+
+Arguments:
+
+* **url** `URL | string` - The url's protocol *must* be `ws` or `wss`.
+* **protocol** `string | string[] | WebSocketInit` (optional) - Subprotocol(s) to request the server use, or a [`Dispatcher`](./Dispatcher.md).
+
+### Example:
+
+This example will not work in browsers or other platforms that don't allow passing an object.
+
+```mjs
+import { WebSocket, ProxyAgent } from 'undici'
+
+const proxyAgent = new ProxyAgent('my.proxy.server')
+
+const ws = new WebSocket('wss://echo.websocket.events', {
+ dispatcher: proxyAgent,
+ protocols: ['echo', 'chat']
+})
+```
+
+If you do not need a custom Dispatcher, it's recommended to use the following pattern:
+
+```mjs
+import { WebSocket } from 'undici'
+
+const ws = new WebSocket('wss://echo.websocket.events', ['echo', 'chat'])
+```
+
+## Read More
+
+- [MDN - WebSocket](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket)
+- [The WebSocket Specification](https://www.rfc-editor.org/rfc/rfc6455)
+- [The WHATWG WebSocket Specification](https://websockets.spec.whatwg.org/)
diff --git a/docs/api/api-lifecycle.md b/docs/api/api-lifecycle.md
new file mode 100644
index 0000000..d158126
--- /dev/null
+++ b/docs/api/api-lifecycle.md
@@ -0,0 +1,62 @@
+# Client Lifecycle
+
+An Undici [Client](Client.md) can be best described as a state machine. The following list is a summary of the various state transitions the `Client` will go through in its lifecycle. This document also contains detailed breakdowns of each state.
+
+> This diagram is not a perfect representation of the undici Client. Since the Client class is not actually implemented as a state-machine, actual execution may deviate slightly from what is described below. Consider this as a general resource for understanding the inner workings of the Undici client rather than some kind of formal specification.
+
+## State Transition Overview
+
+* A `Client` begins in the **idle** state with no socket connection and no requests in queue.
+ * The *connect* event transitions the `Client` to the **pending** state where requests can be queued prior to processing.
+ * The *close* and *destroy* events transition the `Client` to the **destroyed** state. Since there are no requests in the queue, the *close* event immediately transitions to the **destroyed** state.
+* The **pending** state indicates the underlying socket connection has been successfully established and requests are queueing.
+ * The *process* event transitions the `Client` to the **processing** state where requests are processed.
+ * If requests are queued, the *close* event transitions to the **processing** state; otherwise, it transitions to the **destroyed** state.
+ * The *destroy* event transitions to the **destroyed** state.
+* The **processing** state initializes to the **processing.running** state.
+ * If the current request requires draining, the *needDrain* event transitions the `Client` into the **processing.busy** state which will return to the **processing.running** state with the *drainComplete* event.
+ * After all queued requests are completed, the *keepalive* event transitions the `Client` back to the **pending** state. If no requests are queued during the timeout, the **close** event transitions the `Client` to the **destroyed** state.
+ * If the *close* event is fired while the `Client` still has queued requests, the `Client` transitions to the **process.closing** state where it will complete all existing requests before firing the *done* event.
+ * The *done* event gracefully transitions the `Client` to the **destroyed** state.
+ * At any point in time, the *destroy* event will transition the `Client` from the **processing** state to the **destroyed** state, destroying any queued requests.
+* The **destroyed** state is a final state and the `Client` is no longer functional.
+
+![A state diagram representing an Undici Client instance](../assets/lifecycle-diagram.png)
+
+> The diagram was generated using Mermaid.js Live Editor. Modify the state diagram [here](https://mermaid-js.github.io/mermaid-live-editor/#/edit/eyJjb2RlIjoic3RhdGVEaWFncmFtLXYyXG4gICAgWypdIC0tPiBpZGxlXG4gICAgaWRsZSAtLT4gcGVuZGluZyA6IGNvbm5lY3RcbiAgICBpZGxlIC0tPiBkZXN0cm95ZWQgOiBkZXN0cm95L2Nsb3NlXG4gICAgXG4gICAgcGVuZGluZyAtLT4gaWRsZSA6IHRpbWVvdXRcbiAgICBwZW5kaW5nIC0tPiBkZXN0cm95ZWQgOiBkZXN0cm95XG5cbiAgICBzdGF0ZSBjbG9zZV9mb3JrIDw8Zm9yaz4-XG4gICAgcGVuZGluZyAtLT4gY2xvc2VfZm9yayA6IGNsb3NlXG4gICAgY2xvc2VfZm9yayAtLT4gcHJvY2Vzc2luZ1xuICAgIGNsb3NlX2ZvcmsgLS0-IGRlc3Ryb3llZFxuXG4gICAgcGVuZGluZyAtLT4gcHJvY2Vzc2luZyA6IHByb2Nlc3NcblxuICAgIHByb2Nlc3NpbmcgLS0-IHBlbmRpbmcgOiBrZWVwYWxpdmVcbiAgICBwcm9jZXNzaW5nIC0tPiBkZXN0cm95ZWQgOiBkb25lXG4gICAgcHJvY2Vzc2luZyAtLT4gZGVzdHJveWVkIDogZGVzdHJveVxuXG4gICAgc3RhdGUgcHJvY2Vzc2luZyB7XG4gICAgICAgIHJ1bm5pbmcgLS0-IGJ1c3kgOiBuZWVkRHJhaW5cbiAgICAgICAgYnVzeSAtLT4gcnVubmluZyA6IGRyYWluQ29tcGxldGVcbiAgICAgICAgcnVubmluZyAtLT4gWypdIDoga2VlcGFsaXZlXG4gICAgICAgIHJ1bm5pbmcgLS0-IGNsb3NpbmcgOiBjbG9zZVxuICAgICAgICBjbG9zaW5nIC0tPiBbKl0gOiBkb25lXG4gICAgICAgIFsqXSAtLT4gcnVubmluZ1xuICAgIH1cbiAgICAiLCJtZXJtYWlkIjp7InRoZW1lIjoiYmFzZSJ9LCJ1cGRhdGVFZGl0b3IiOmZhbHNlfQ)
+
+## State details
+
+### idle
+
+The **idle** state is the initial state of a `Client` instance. While an `origin` is required for instantiating a `Client` instance, the underlying socket connection will not be established until a request is queued using [`Client.dispatch()`](Client.md#clientdispatchoptions-handlers). By calling `Client.dispatch()` directly or using one of the multiple implementations ([`Client.connect()`](Client.md#clientconnectoptions-callback), [`Client.pipeline()`](Client.md#clientpipelineoptions-handler), [`Client.request()`](Client.md#clientrequestoptions-callback), [`Client.stream()`](Client.md#clientstreamoptions-factory-callback), and [`Client.upgrade()`](Client.md#clientupgradeoptions-callback)), the `Client` instance will transition from **idle** to [**pending**](#pending) and then most likely directly to [**processing**](#processing).
+
+Calling [`Client.close()`](Client.md#clientclosecallback) or [`Client.destroy()`](Client.md#clientdestroyerror-callback) transitions directly to the [**destroyed**](#destroyed) state since the `Client` instance will have no queued requests in this state.
+
+### pending
+
+The **pending** state signifies a non-processing `Client`. Upon entering this state, the `Client` establishes a socket connection and emits the [`'connect'`](Client.md#event-connect) event signalling a connection was successfully established with the `origin` provided during `Client` instantiation. The internal queue is initially empty, and requests can start queueing.
+
+Calling [`Client.close()`](Client.md#clientclosecallback) with queued requests, transitions the `Client` to the [**processing**](#processing) state. Without queued requests, it transitions to the [**destroyed**](#destroyed) state.
+
+Calling [`Client.destroy()`](Client.md#clientdestroyerror-callback) transitions directly to the [**destroyed**](#destroyed) state regardless of existing requests.
+
+### processing
+
+The **processing** state is a state machine within itself. It initializes to the [**processing.running**](#running) state. The [`Client.dispatch()`](Client.md#clientdispatchoptions-handlers), [`Client.close()`](Client.md#clientclosecallback), and [`Client.destroy()`](Client.md#clientdestroyerror-callback) can be called at any time while the `Client` is in this state. `Client.dispatch()` will add more requests to the queue while existing requests continue to be processed. `Client.close()` will transition to the [**processing.closing**](#closing) state. And `Client.destroy()` will transition to [**destroyed**](#destroyed).
+
+#### running
+
+In the **processing.running** sub-state, queued requests are being processed in a FIFO order. If a request body requires draining, the *needDrain* event transitions to the [**processing.busy**](#busy) sub-state. The *close* event transitions the Client to the [**process.closing**](#closing) sub-state. If all queued requests are processed and neither [`Client.close()`](Client.md#clientclosecallback) nor [`Client.destroy()`](Client.md#clientdestroyerror-callback) are called, then the [**processing**](#processing) machine will trigger a *keepalive* event transitioning the `Client` back to the [**pending**](#pending) state. During this time, the `Client` is waiting for the socket connection to timeout, and once it does, it triggers the *timeout* event and transitions to the [**idle**](#idle) state.
+
+#### busy
+
+This sub-state is only entered when a request body is an instance of [Stream](https://nodejs.org/api/stream.html) and requires draining. The `Client` cannot process additional requests while in this state and must wait until the currently processing request body is completely drained before transitioning back to [**processing.running**](#running).
+
+#### closing
+
+This sub-state is only entered when a `Client` instance has queued requests and the [`Client.close()`](Client.md#clientclosecallback) method is called. In this state, the `Client` instance continues to process requests as usual, with the one exception that no additional requests can be queued. Once all of the queued requests are processed, the `Client` will trigger the *done* event gracefully entering the [**destroyed**](#destroyed) state without an error.
+
+### destroyed
+
+The **destroyed** state is a final state for the `Client` instance. Once in this state, a `Client` is nonfunctional. Calling any other `Client` methods will result in an `ClientDestroyedError`.
diff --git a/docs/assets/lifecycle-diagram.png b/docs/assets/lifecycle-diagram.png
new file mode 100644
index 0000000..4ef17b5
--- /dev/null
+++ b/docs/assets/lifecycle-diagram.png
Binary files differ
diff --git a/docs/best-practices/client-certificate.md b/docs/best-practices/client-certificate.md
new file mode 100644
index 0000000..4fc84ec
--- /dev/null
+++ b/docs/best-practices/client-certificate.md
@@ -0,0 +1,64 @@
+# Client certificate
+
+Client certificate authentication can be configured with the `Client`, the required options are passed along through the `connect` option.
+
+The client certificates must be signed by a trusted CA. The Node.js default is to trust the well-known CAs curated by Mozilla.
+
+Setting the server option `requestCert: true` tells the server to request the client certificate.
+
+The server option `rejectUnauthorized: false` allows us to handle any invalid certificate errors in client code. The `authorized` property on the socket of the incoming request will show if the client certificate was valid. The `authorizationError` property will give the reason if the certificate was not valid.
+
+### Client Certificate Authentication
+
+```js
+const { readFileSync } = require('fs')
+const { join } = require('path')
+const { createServer } = require('https')
+const { Client } = require('undici')
+
+const serverOptions = {
+ ca: [
+ readFileSync(join(__dirname, 'client-ca-crt.pem'), 'utf8')
+ ],
+ key: readFileSync(join(__dirname, 'server-key.pem'), 'utf8'),
+ cert: readFileSync(join(__dirname, 'server-crt.pem'), 'utf8'),
+ requestCert: true,
+ rejectUnauthorized: false
+}
+
+const server = createServer(serverOptions, (req, res) => {
+ // true if client cert is valid
+ if(req.client.authorized === true) {
+ console.log('valid')
+ } else {
+ console.error(req.client.authorizationError)
+ }
+ res.end()
+})
+
+server.listen(0, function () {
+ const tls = {
+ ca: [
+ readFileSync(join(__dirname, 'server-ca-crt.pem'), 'utf8')
+ ],
+ key: readFileSync(join(__dirname, 'client-key.pem'), 'utf8'),
+ cert: readFileSync(join(__dirname, 'client-crt.pem'), 'utf8'),
+ rejectUnauthorized: false,
+ servername: 'agent1'
+ }
+ const client = new Client(`https://localhost:${server.address().port}`, {
+ connect: tls
+ })
+
+ client.request({
+ path: '/',
+ method: 'GET'
+ }, (err, { body }) => {
+ body.on('data', (buf) => {})
+ body.on('end', () => {
+ client.close()
+ server.close()
+ })
+ })
+})
+```
diff --git a/docs/best-practices/mocking-request.md b/docs/best-practices/mocking-request.md
new file mode 100644
index 0000000..6954392
--- /dev/null
+++ b/docs/best-practices/mocking-request.md
@@ -0,0 +1,136 @@
+# Mocking Request
+
+Undici has its own mocking [utility](../api/MockAgent.md). It allow us to intercept undici HTTP requests and return mocked values instead. It can be useful for testing purposes.
+
+Example:
+
+```js
+// bank.mjs
+import { request } from 'undici'
+
+export async function bankTransfer(recipient, amount) {
+ const { body } = await request('http://localhost:3000/bank-transfer',
+ {
+ method: 'POST',
+ headers: {
+ 'X-TOKEN-SECRET': 'SuperSecretToken',
+ },
+ body: JSON.stringify({
+ recipient,
+ amount
+ })
+ }
+ )
+ return await body.json()
+}
+```
+
+And this is what the test file looks like:
+
+```js
+// index.test.mjs
+import { strict as assert } from 'assert'
+import { MockAgent, setGlobalDispatcher, } from 'undici'
+import { bankTransfer } from './bank.mjs'
+
+const mockAgent = new MockAgent();
+
+setGlobalDispatcher(mockAgent);
+
+// Provide the base url to the request
+const mockPool = mockAgent.get('http://localhost:3000');
+
+// intercept the request
+mockPool.intercept({
+ path: '/bank-transfer',
+ method: 'POST',
+ headers: {
+ 'X-TOKEN-SECRET': 'SuperSecretToken',
+ },
+ body: JSON.stringify({
+ recipient: '1234567890',
+ amount: '100'
+ })
+}).reply(200, {
+ message: 'transaction processed'
+})
+
+const success = await bankTransfer('1234567890', '100')
+
+assert.deepEqual(success, { message: 'transaction processed' })
+
+// if you dont want to check whether the body or the headers contain the same value
+// just remove it from interceptor
+mockPool.intercept({
+ path: '/bank-transfer',
+ method: 'POST',
+}).reply(400, {
+ message: 'bank account not found'
+})
+
+const badRequest = await bankTransfer('1234567890', '100')
+
+assert.deepEqual(badRequest, { message: 'bank account not found' })
+```
+
+Explore other MockAgent functionality [here](../api/MockAgent.md)
+
+## Debug Mock Value
+
+When the interceptor and the request options are not the same, undici will automatically make a real HTTP request. To prevent real requests from being made, use `mockAgent.disableNetConnect()`:
+
+```js
+const mockAgent = new MockAgent();
+
+setGlobalDispatcher(mockAgent);
+mockAgent.disableNetConnect()
+
+// Provide the base url to the request
+const mockPool = mockAgent.get('http://localhost:3000');
+
+mockPool.intercept({
+ path: '/bank-transfer',
+ method: 'POST',
+}).reply(200, {
+ message: 'transaction processed'
+})
+
+const badRequest = await bankTransfer('1234567890', '100')
+// Will throw an error
+// MockNotMatchedError: Mock dispatch not matched for path '/bank-transfer':
+// subsequent request to origin http://localhost:3000 was not allowed (net.connect disabled)
+```
+
+## Reply with data based on request
+
+If the mocked response needs to be dynamically derived from the request parameters, you can provide a function instead of an object to `reply`:
+
+```js
+mockPool.intercept({
+ path: '/bank-transfer',
+ method: 'POST',
+ headers: {
+ 'X-TOKEN-SECRET': 'SuperSecretToken',
+ },
+ body: JSON.stringify({
+ recipient: '1234567890',
+ amount: '100'
+ })
+}).reply(200, (opts) => {
+ // do something with opts
+
+ return { message: 'transaction processed' }
+})
+```
+
+in this case opts will be
+
+```
+{
+ method: 'POST',
+ headers: { 'X-TOKEN-SECRET': 'SuperSecretToken' },
+ body: '{"recipient":"1234567890","amount":"100"}',
+ origin: 'http://localhost:3000',
+ path: '/bank-transfer'
+}
+```
diff --git a/docs/best-practices/proxy.md b/docs/best-practices/proxy.md
new file mode 100644
index 0000000..bf10295
--- /dev/null
+++ b/docs/best-practices/proxy.md
@@ -0,0 +1,127 @@
+# Connecting through a proxy
+
+Connecting through a proxy is possible by:
+
+- Using [AgentProxy](../api/ProxyAgent.md).
+- Configuring `Client` or `Pool` constructor.
+
+The proxy url should be passed to the `Client` or `Pool` constructor, while the upstream server url
+should be added to every request call in the `path`.
+For instance, if you need to send a request to the `/hello` route of your upstream server,
+the `path` should be `path: 'http://upstream.server:port/hello?foo=bar'`.
+
+If you proxy requires basic authentication, you can send it via the `proxy-authorization` header.
+
+### Connect without authentication
+
+```js
+import { Client } from 'undici'
+import { createServer } from 'http'
+import proxy from 'proxy'
+
+const server = await buildServer()
+const proxyServer = await buildProxy()
+
+const serverUrl = `http://localhost:${server.address().port}`
+const proxyUrl = `http://localhost:${proxyServer.address().port}`
+
+server.on('request', (req, res) => {
+ console.log(req.url) // '/hello?foo=bar'
+ res.setHeader('content-type', 'application/json')
+ res.end(JSON.stringify({ hello: 'world' }))
+})
+
+const client = new Client(proxyUrl)
+
+const response = await client.request({
+ method: 'GET',
+ path: serverUrl + '/hello?foo=bar'
+})
+
+response.body.setEncoding('utf8')
+let data = ''
+for await (const chunk of response.body) {
+ data += chunk
+}
+console.log(response.statusCode) // 200
+console.log(JSON.parse(data)) // { hello: 'world' }
+
+server.close()
+proxyServer.close()
+client.close()
+
+function buildServer () {
+ return new Promise((resolve, reject) => {
+ const server = createServer()
+ server.listen(0, () => resolve(server))
+ })
+}
+
+function buildProxy () {
+ return new Promise((resolve, reject) => {
+ const server = proxy(createServer())
+ server.listen(0, () => resolve(server))
+ })
+}
+```
+
+### Connect with authentication
+
+```js
+import { Client } from 'undici'
+import { createServer } from 'http'
+import proxy from 'proxy'
+
+const server = await buildServer()
+const proxyServer = await buildProxy()
+
+const serverUrl = `http://localhost:${server.address().port}`
+const proxyUrl = `http://localhost:${proxyServer.address().port}`
+
+proxyServer.authenticate = function (req, fn) {
+ fn(null, req.headers['proxy-authorization'] === `Basic ${Buffer.from('user:pass').toString('base64')}`)
+}
+
+server.on('request', (req, res) => {
+ console.log(req.url) // '/hello?foo=bar'
+ res.setHeader('content-type', 'application/json')
+ res.end(JSON.stringify({ hello: 'world' }))
+})
+
+const client = new Client(proxyUrl)
+
+const response = await client.request({
+ method: 'GET',
+ path: serverUrl + '/hello?foo=bar',
+ headers: {
+ 'proxy-authorization': `Basic ${Buffer.from('user:pass').toString('base64')}`
+ }
+})
+
+response.body.setEncoding('utf8')
+let data = ''
+for await (const chunk of response.body) {
+ data += chunk
+}
+console.log(response.statusCode) // 200
+console.log(JSON.parse(data)) // { hello: 'world' }
+
+server.close()
+proxyServer.close()
+client.close()
+
+function buildServer () {
+ return new Promise((resolve, reject) => {
+ const server = createServer()
+ server.listen(0, () => resolve(server))
+ })
+}
+
+function buildProxy () {
+ return new Promise((resolve, reject) => {
+ const server = proxy(createServer())
+ server.listen(0, () => resolve(server))
+ })
+}
+```
+
diff --git a/docs/best-practices/writing-tests.md b/docs/best-practices/writing-tests.md
new file mode 100644
index 0000000..57549de
--- /dev/null
+++ b/docs/best-practices/writing-tests.md
@@ -0,0 +1,20 @@
+# Writing tests
+
+Undici is tuned for a production use case and its default will keep
+a socket open for a few seconds after an HTTP request is completed to
+remove the overhead of opening up a new socket. These settings that makes
+Undici shine in production are not a good fit for using Undici in automated
+tests, as it will result in longer execution times.
+
+The following are good defaults that will keep the socket open for only 10ms:
+
+```js
+import { request, setGlobalDispatcher, Agent } from 'undici'
+
+const agent = new Agent({
+ keepAliveTimeout: 10, // milliseconds
+ keepAliveMaxTimeout: 10 // milliseconds
+})
+
+setGlobalDispatcher(agent)
+```
diff --git a/docsify/sidebar.md b/docsify/sidebar.md
new file mode 100644
index 0000000..b7c7d6a
--- /dev/null
+++ b/docsify/sidebar.md
@@ -0,0 +1,28 @@
+<!-- Sidebar for Docsify -->
+
+* [**Home**](/ "Node.js Undici")
+* API
+ * [Dispatcher](/docs/api/Dispatcher.md "Undici API - Dispatcher")
+ * [Client](/docs/api/Client.md "Undici API - Client")
+ * [Pool](/docs/api/Pool.md "Undici API - Pool")
+ * [BalancedPool](/docs/api/BalancedPool.md "Undici API - BalancedPool")
+ * [Agent](/docs/api/Agent.md "Undici API - Agent")
+ * [ProxyAgent](/docs/api/ProxyAgent.md "Undici API - ProxyAgent")
+ * [Connector](/docs/api/Connector.md "Custom connector")
+ * [Errors](/docs/api/Errors.md "Undici API - Errors")
+ * [Fetch](/docs/api/Fetch.md "Undici API - Fetch")
+ * [Cookies](/docs/api/Cookies.md "Undici API - Cookies")
+ * [MockClient](/docs/api/MockClient.md "Undici API - MockClient")
+ * [MockPool](/docs/api/MockPool.md "Undici API - MockPool")
+ * [MockAgent](/docs/api/MockAgent.md "Undici API - MockAgent")
+ * [MockErrors](/docs/api/MockErrors.md "Undici API - MockErrors")
+ * [API Lifecycle](/docs/api/api-lifecycle.md "Undici API - Lifecycle")
+ * [Diagnostics Channel Support](/docs/api/DiagnosticsChannel.md "Diagnostics Channel Support")
+ * [WebSocket](/docs/api/WebSocket.md "Undici API - WebSocket")
+ * [MIME Type Parsing](/docs/api/ContentType.md "Undici API - MIME Type Parsing")
+ * [CacheStorage](/docs/api/CacheStorage.md "Undici API - CacheStorage")
+* Best Practices
+ * [Proxy](/docs/best-practices/proxy.md "Connecting through a proxy")
+ * [Client Certificate](/docs/best-practices/client-certificate.md "Connect using a client certificate")
+ * [Writing Tests](/docs/best-practices/writing-tests.md "Using Undici inside tests")
+ * [Mocking Request](/docs/best-practices/mocking-request.md "Using Undici inside tests")
diff --git a/examples/ca-fingerprint/index.js b/examples/ca-fingerprint/index.js
new file mode 100644
index 0000000..792c08c
--- /dev/null
+++ b/examples/ca-fingerprint/index.js
@@ -0,0 +1,80 @@
+'use strict'
+
+const crypto = require('crypto')
+const https = require('https')
+const { Client, buildConnector } = require('../..')
+const pem = require('https-pem')
+
+const caFingerprint = getFingerprint(pem.cert.toString()
+ .split('\n')
+ .slice(1, -1)
+ .map(line => line.trim())
+ .join('')
+)
+
+const server = https.createServer(pem, (req, res) => {
+ res.setHeader('Content-Type', 'text/plain')
+ res.end('hello')
+})
+
+server.listen(0, function () {
+ const connector = buildConnector({ rejectUnauthorized: false })
+ const client = new Client(`https://localhost:${server.address().port}`, {
+ connect (opts, cb) {
+ connector(opts, (err, socket) => {
+ if (err) {
+ cb(err)
+ } else if (getIssuerCertificate(socket).fingerprint256 !== caFingerprint) {
+ socket.destroy()
+ cb(new Error('Fingerprint does not match or malformed certificate'))
+ } else {
+ cb(null, socket)
+ }
+ })
+ }
+ })
+
+ client.request({
+ path: '/',
+ method: 'GET'
+ }, (err, data) => {
+ if (err) throw err
+
+ const bufs = []
+ data.body.on('data', (buf) => {
+ bufs.push(buf)
+ })
+ data.body.on('end', () => {
+ console.log(Buffer.concat(bufs).toString('utf8'))
+ client.close()
+ server.close()
+ })
+ })
+})
+
+function getIssuerCertificate (socket) {
+ let certificate = socket.getPeerCertificate(true)
+ while (certificate && Object.keys(certificate).length > 0) {
+ // invalid certificate
+ if (certificate.issuerCertificate == null) {
+ return null
+ }
+
+ // We have reached the root certificate.
+ // In case of self-signed certificates, `issuerCertificate` may be a circular reference.
+ if (certificate.fingerprint256 === certificate.issuerCertificate.fingerprint256) {
+ break
+ }
+
+ // continue the loop
+ certificate = certificate.issuerCertificate
+ }
+ return certificate
+}
+
+function getFingerprint (content, inputEncoding = 'base64', outputEncoding = 'hex') {
+ const shasum = crypto.createHash('sha256')
+ shasum.update(content, inputEncoding)
+ const res = shasum.digest(outputEncoding)
+ return res.toUpperCase().match(/.{1,2}/g).join(':')
+}
diff --git a/examples/fetch.js b/examples/fetch.js
new file mode 100644
index 0000000..7ece2b8
--- /dev/null
+++ b/examples/fetch.js
@@ -0,0 +1,13 @@
+'use strict'
+
+const { fetch } = require('../')
+
+async function main () {
+ const res = await fetch('http://localhost:3001/')
+
+ const data = await res.text()
+ console.log('response received', res.status)
+ console.log('headers', res.headers)
+ console.log('data', data)
+}
+main()
diff --git a/examples/proxy-agent.js b/examples/proxy-agent.js
new file mode 100644
index 0000000..7caf836
--- /dev/null
+++ b/examples/proxy-agent.js
@@ -0,0 +1,25 @@
+'use strict'
+
+const { request, setGlobalDispatcher, ProxyAgent } = require('../')
+
+setGlobalDispatcher(new ProxyAgent('http://localhost:8000/'))
+
+async function main () {
+ const {
+ statusCode,
+ headers,
+ trailers,
+ body
+ // send the request via the http://localhost:8000/ HTTP proxy
+ } = await request('http://localhost:3000/undici')
+
+ console.log('response received', statusCode)
+ console.log('headers', headers)
+
+ for await (const data of body) {
+ console.log('data', data)
+ }
+
+ console.log('trailers', trailers)
+}
+main()
diff --git a/examples/proxy/index.js b/examples/proxy/index.js
new file mode 100644
index 0000000..5f35049
--- /dev/null
+++ b/examples/proxy/index.js
@@ -0,0 +1,49 @@
+const { Pool, Client } = require('../../')
+const http = require('http')
+const proxy = require('./proxy')
+
+const pool = new Pool('http://localhost:4001', {
+ connections: 256,
+ pipelining: 1
+})
+
+async function run () {
+ await Promise.all([
+ new Promise(resolve => {
+ // Proxy
+ http.createServer((req, res) => {
+ proxy({ req, res, proxyName: 'example' }, pool).catch(err => {
+ if (res.headersSent) {
+ res.destroy(err)
+ } else {
+ for (const name of res.getHeaderNames()) {
+ res.removeHeader(name)
+ }
+ res.statusCode = err.statusCode || 500
+ res.end()
+ }
+ })
+ }).listen(4000, resolve)
+ }),
+ new Promise(resolve => {
+ // Upstream
+ http.createServer((req, res) => {
+ res.end('hello world')
+ }).listen(4001, resolve)
+ })
+ ])
+
+ const client = new Client('http://localhost:4000')
+ const { body } = await client.request({
+ method: 'GET',
+ path: '/'
+ })
+
+ for await (const chunk of body) {
+ console.log(String(chunk))
+ }
+}
+
+run()
+
+// TODO: Add websocket example.
diff --git a/examples/proxy/proxy.js b/examples/proxy/proxy.js
new file mode 100644
index 0000000..bb9fcc4
--- /dev/null
+++ b/examples/proxy/proxy.js
@@ -0,0 +1,256 @@
+const net = require('net')
+const { pipeline } = require('stream')
+const createError = require('http-errors')
+
+module.exports = async function proxy (ctx, client) {
+ const { req, socket, proxyName } = ctx
+
+ const headers = getHeaders({
+ headers: req.rawHeaders,
+ httpVersion: req.httpVersion,
+ socket: req.socket,
+ proxyName
+ })
+
+ if (socket) {
+ const handler = new WSHandler(ctx)
+ client.dispatch({
+ method: req.method,
+ path: req.url,
+ headers,
+ upgrade: 'Websocket'
+ }, handler)
+ return handler.promise
+ } else {
+ const handler = new HTTPHandler(ctx)
+ client.dispatch({
+ method: req.method,
+ path: req.url,
+ headers,
+ body: req
+ }, handler)
+ return handler.promise
+ }
+}
+
+class HTTPHandler {
+ constructor (ctx) {
+ const { req, res, proxyName } = ctx
+
+ this.proxyName = proxyName
+ this.req = req
+ this.res = res
+ this.resume = null
+ this.abort = null
+ this.promise = new Promise((resolve, reject) => {
+ this.callback = err => err ? reject(err) : resolve()
+ })
+ }
+
+ onConnect (abort) {
+ if (this.req.aborted) {
+ abort()
+ } else {
+ this.abort = abort
+ this.res.on('close', abort)
+ }
+ }
+
+ onHeaders (statusCode, headers, resume) {
+ if (statusCode < 200) {
+ return
+ }
+
+ this.resume = resume
+ this.res.on('drain', resume)
+ this.res.writeHead(statusCode, getHeaders({
+ headers,
+ proxyName: this.proxyName,
+ httpVersion: this.httpVersion
+ }))
+ }
+
+ onData (chunk) {
+ return this.res.write(chunk)
+ }
+
+ onComplete () {
+ this.res.off('close', this.abort)
+ this.res.off('drain', this.resume)
+
+ this.res.end()
+ this.callback()
+ }
+
+ onError (err) {
+ this.res.off('close', this.abort)
+ this.res.off('drain', this.resume)
+
+ this.callback(err)
+ }
+}
+
+class WSHandler {
+ constructor (ctx) {
+ const { req, socket, proxyName, head } = ctx
+
+ setupSocket(socket)
+
+ this.proxyName = proxyName
+ this.httpVersion = req.httpVersion
+ this.socket = socket
+ this.head = head
+ this.abort = null
+ this.promise = new Promise((resolve, reject) => {
+ this.callback = err => err ? reject(err) : resolve()
+ })
+ }
+
+ onConnect (abort) {
+ if (this.socket.destroyed) {
+ abort()
+ } else {
+ this.abort = abort
+ this.socket.on('close', abort)
+ }
+ }
+
+ onUpgrade (statusCode, headers, socket) {
+ this.socket.off('close', this.abort)
+
+ // TODO: Check statusCode?
+
+ if (this.head && this.head.length) {
+ socket.unshift(this.head)
+ }
+
+ setupSocket(socket)
+
+ headers = getHeaders({
+ headers,
+ proxyName: this.proxyName,
+ httpVersion: this.httpVersion
+ })
+
+ let head = ''
+ for (let n = 0; n < headers.length; n += 2) {
+ head += `\r\n${headers[n]}: ${headers[n + 1]}`
+ }
+
+ this.socket.write(`HTTP/1.1 101 Switching Protocols\r\nconnection: upgrade\r\nupgrade: websocket${head}\r\n\r\n`)
+
+ pipeline(socket, this.socket, socket, this.callback)
+ }
+
+ onError (err) {
+ this.socket.off('close', this.abort)
+
+ this.callback(err)
+ }
+}
+
+// This expression matches hop-by-hop headers.
+// These headers are meaningful only for a single transport-level connection,
+// and must not be retransmitted by proxies or cached.
+const HOP_EXPR = /^(te|host|upgrade|trailers|connection|keep-alive|http2-settings|transfer-encoding|proxy-connection|proxy-authenticate|proxy-authorization)$/i
+
+// Removes hop-by-hop and pseudo headers.
+// Updates via and forwarded headers.
+// Only hop-by-hop headers may be set using the Connection general header.
+function getHeaders ({
+ headers,
+ proxyName,
+ httpVersion,
+ socket
+}) {
+ let via = ''
+ let forwarded = ''
+ let host = ''
+ let authority = ''
+ let connection = ''
+
+ for (let n = 0; n < headers.length; n += 2) {
+ const key = headers[n]
+ const val = headers[n + 1]
+
+ if (!via && key.length === 3 && key.toLowerCase() === 'via') {
+ via = val
+ } else if (!host && key.length === 4 && key.toLowerCase() === 'host') {
+ host = val
+ } else if (!forwarded && key.length === 9 && key.toLowerCase() === 'forwarded') {
+ forwarded = val
+ } else if (!connection && key.length === 10 && key.toLowerCase() === 'connection') {
+ connection = val
+ } else if (!authority && key.length === 10 && key === ':authority') {
+ authority = val
+ }
+ }
+
+ let remove
+ if (connection && !HOP_EXPR.test(connection)) {
+ remove = connection.split(/,\s*/)
+ }
+
+ const result = []
+ for (let n = 0; n < headers.length; n += 2) {
+ const key = headers[n]
+ const val = headers[n + 1]
+
+ if (
+ key.charAt(0) !== ':' &&
+ !HOP_EXPR.test(key) &&
+ (!remove || !remove.includes(key))
+ ) {
+ result.push(key, val)
+ }
+ }
+
+ if (socket) {
+ result.push('forwarded', (forwarded ? forwarded + ', ' : '') + [
+ `by=${printIp(socket.localAddress, socket.localPort)}`,
+ `for=${printIp(socket.remoteAddress, socket.remotePort)}`,
+ `proto=${socket.encrypted ? 'https' : 'http'}`,
+ `host=${printIp(authority || host || '')}`
+ ].join(';'))
+ } else if (forwarded) {
+ // The forwarded header should not be included in response.
+ throw new createError.BadGateway()
+ }
+
+ if (proxyName) {
+ if (via) {
+ if (via.split(',').some(name => name.endsWith(proxyName))) {
+ throw new createError.LoopDetected()
+ }
+ via += ', '
+ }
+ via += `${httpVersion} ${proxyName}`
+ }
+
+ if (via) {
+ result.push('via', via)
+ }
+
+ return result
+}
+
+function setupSocket (socket) {
+ socket.setTimeout(0)
+ socket.setNoDelay(true)
+ socket.setKeepAlive(true, 0)
+}
+
+function printIp (address, port) {
+ const isIPv6 = net.isIPv6(address)
+ let str = `${address}`
+ if (isIPv6) {
+ str = `[${str}]`
+ }
+ if (port) {
+ str = `${str}:${port}`
+ }
+ if (isIPv6 || port) {
+ str = `"${str}"`
+ }
+ return str
+}
diff --git a/examples/request.js b/examples/request.js
new file mode 100644
index 0000000..1b03254
--- /dev/null
+++ b/examples/request.js
@@ -0,0 +1,18 @@
+'use strict'
+
+const { request } = require('../')
+
+async function main () {
+ const {
+ statusCode,
+ headers,
+ body
+ } = await request('http://localhost:3001/')
+
+ const data = await body.text()
+ console.log('response received', statusCode)
+ console.log('headers', headers)
+ console.log('data', data)
+}
+
+main()
diff --git a/fastify-busboy/.eslintrc.js b/fastify-busboy/.eslintrc.js
new file mode 100644
index 0000000..4b904cd
--- /dev/null
+++ b/fastify-busboy/.eslintrc.js
@@ -0,0 +1,27 @@
+module.exports = {
+ ignorePatterns: [
+ 'bench',
+ 'deps/encoding'
+ ],
+ extends: [
+ 'standard',
+ 'eslint:recommended',
+ 'plugin:n/recommended'
+ ],
+ rules: {
+ 'no-unused-vars': [1, { vars: 'all', args: 'none' }],
+ 'n/no-missing-require': 1,
+ 'no-constant-condition': 'off',
+ 'no-var': 'off',
+ 'no-redeclare': 1,
+ 'no-fallthrough': 1,
+ 'no-control-regex': 1,
+ 'no-empty': 'off',
+ 'prefer-const': 'off'
+ },
+ env: {
+ node: true,
+ mocha: true,
+ es6: true
+ }
+}
diff --git a/fastify-busboy/.gitattributes b/fastify-busboy/.gitattributes
new file mode 100644
index 0000000..49b4f89
--- /dev/null
+++ b/fastify-busboy/.gitattributes
@@ -0,0 +1,2 @@
+* text=false
+*.header -crlf
diff --git a/fastify-busboy/.github/dependabot.yml b/fastify-busboy/.github/dependabot.yml
new file mode 100644
index 0000000..dfa7fa6
--- /dev/null
+++ b/fastify-busboy/.github/dependabot.yml
@@ -0,0 +1,13 @@
+version: 2
+updates:
+ - package-ecosystem: "github-actions"
+ directory: "/"
+ schedule:
+ interval: "monthly"
+ open-pull-requests-limit: 10
+
+ - package-ecosystem: "npm"
+ directory: "/"
+ schedule:
+ interval: "weekly"
+ open-pull-requests-limit: 10
diff --git a/fastify-busboy/.github/workflows/ci.yml b/fastify-busboy/.github/workflows/ci.yml
new file mode 100644
index 0000000..babd56d
--- /dev/null
+++ b/fastify-busboy/.github/workflows/ci.yml
@@ -0,0 +1,22 @@
+name: CI
+
+on:
+ push:
+ branches:
+ - main
+ - master
+ - next
+ - 'v*'
+ paths-ignore:
+ - 'docs/**'
+ - '*.md'
+ pull_request:
+ paths-ignore:
+ - 'docs/**'
+ - '*.md'
+
+jobs:
+ test:
+ uses: fastify/workflows/.github/workflows/plugins-ci.yml@v3
+ with:
+ license-check: true
diff --git a/fastify-busboy/.github/workflows/coverage.yml b/fastify-busboy/.github/workflows/coverage.yml
new file mode 100644
index 0000000..3d7b943
--- /dev/null
+++ b/fastify-busboy/.github/workflows/coverage.yml
@@ -0,0 +1,44 @@
+---
+
+name: coverage
+
+on:
+ push:
+ branches:
+ - master
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ name: coverage
+
+ strategy:
+ matrix:
+ node-version: [16.x]
+
+ steps:
+ - name: Checkout Repository
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 1
+
+ - name: Setup Node ${{ matrix.node-version }}
+ uses: actions/setup-node@v4
+ with:
+ always-auth: false
+ node-version: ${{ matrix.node-version }}
+
+ - name: Run npm install
+ run: npm install
+
+ - name: Run Tests
+ run: npm run test:coverage
+
+ - name: Generate LCOV
+ run: npm run coveralls
+
+ - name: Update Coveralls
+ uses: coverallsapp/github-action@master
+ with:
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ if: success()
diff --git a/fastify-busboy/.github/workflows/linting.yml b/fastify-busboy/.github/workflows/linting.yml
new file mode 100644
index 0000000..407f53e
--- /dev/null
+++ b/fastify-busboy/.github/workflows/linting.yml
@@ -0,0 +1,35 @@
+---
+
+name: Linting and Types
+
+on:
+ pull_request:
+ branches:
+ - master
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ name: Linting and Types
+
+ strategy:
+ matrix:
+ node-version: [16.x]
+
+ steps:
+ - name: Checkout Repository
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 1
+
+ - name: Setup Node ${{ matrix.node-version }}
+ uses: actions/setup-node@v4
+ with:
+ always-auth: false
+ node-version: ${{ matrix.node-version }}
+
+ - name: Run npm install
+ run: npm install
+
+ - name: Run lint:everything
+ run: npm run lint:everything
diff --git a/fastify-busboy/.gitignore b/fastify-busboy/.gitignore
new file mode 100644
index 0000000..6e49526
--- /dev/null
+++ b/fastify-busboy/.gitignore
@@ -0,0 +1,152 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+lerna-debug.log*
+.pnpm-debug.log*
+
+# Diagnostic reports (https://nodejs.org/api/report.html)
+report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
+
+# Runtime data
+pids
+*.pid
+*.seed
+*.pid.lock
+
+# Directory for instrumented libs generated by jscoverage/JSCover
+lib-cov
+
+# Coverage directory used by tools like istanbul
+coverage
+*.lcov
+
+# nyc test coverage
+.nyc_output
+
+# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
+.grunt
+
+# Bower dependency directory (https://bower.io/)
+bower_components
+
+# node-waf configuration
+.lock-wscript
+
+# Compiled binary addons (https://nodejs.org/api/addons.html)
+build/Release
+
+# Dependency directories
+node_modules/
+jspm_packages/
+
+# Snowpack dependency directory (https://snowpack.dev/)
+web_modules/
+
+# TypeScript cache
+*.tsbuildinfo
+
+# Optional npm cache directory
+.npm
+
+# Optional eslint cache
+.eslintcache
+
+# Optional stylelint cache
+.stylelintcache
+
+# Microbundle cache
+.rpt2_cache/
+.rts2_cache_cjs/
+.rts2_cache_es/
+.rts2_cache_umd/
+
+# Optional REPL history
+.node_repl_history
+
+# Output of 'npm pack'
+*.tgz
+
+# Yarn Integrity file
+.yarn-integrity
+
+# dotenv environment variable files
+.env
+.env.development.local
+.env.test.local
+.env.production.local
+.env.local
+
+# parcel-bundler cache (https://parceljs.org/)
+.cache
+.parcel-cache
+
+# Next.js build output
+.next
+out
+
+# Nuxt.js build / generate output
+.nuxt
+dist
+
+# Gatsby files
+.cache/
+# Comment in the public line in if your project uses Gatsby and not Next.js
+# https://nextjs.org/blog/next-9-1#public-directory-support
+# public
+
+# vuepress build output
+.vuepress/dist
+
+# vuepress v2.x temp and cache directory
+.temp
+.cache
+
+# Docusaurus cache and generated files
+.docusaurus
+
+# Serverless directories
+.serverless/
+
+# FuseBox cache
+.fusebox/
+
+# DynamoDB Local files
+.dynamodb/
+
+# TernJS port file
+.tern-port
+
+# Stores VSCode versions used for testing VSCode extensions
+.vscode-test
+
+# yarn v2
+.yarn/cache
+.yarn/unplugged
+.yarn/build-state.yml
+.yarn/install-state.gz
+.pnp.*
+
+# Vim swap files
+*.swp
+
+# macOS files
+.DS_Store
+
+# Clinic
+.clinic
+
+# lock files
+bun.lockb
+package-lock.json
+pnpm-lock.yaml
+yarn.lock
+
+# editor files
+.vscode
+.idea
+
+/benchmarks/node_modules/
+/benchmarks/package-lock.json
diff --git a/fastify-busboy/.taprc b/fastify-busboy/.taprc
new file mode 100644
index 0000000..30a802b
--- /dev/null
+++ b/fastify-busboy/.taprc
@@ -0,0 +1,4 @@
+files:
+ - test/**/*.test.js
+
+coverage: false \ No newline at end of file
diff --git a/fastify-busboy/CHANGELOG.md b/fastify-busboy/CHANGELOG.md
new file mode 100644
index 0000000..6d10297
--- /dev/null
+++ b/fastify-busboy/CHANGELOG.md
@@ -0,0 +1,28 @@
+# Changelog
+
+Major changes since the last busboy release (0.3.1):
+
+# 1.1.0 - 09 June, 2022
+
+* Fix potential ReDOS-Attack-Vector in Headerparser (#72)
+* Improve array parse performances (#69)
+* Export Dicer library (#90)
+
+# 1.0.0 - 04 December, 2021
+
+* Prevent malformed headers from crashing the web server (#34)
+* Prevent empty parts from hanging the process (#55)
+* Use non-deprecated Buffer creation (#8, #10)
+* Include TypeScript types in the package itself (#13)
+* Make `busboy` importable both as ESM and as CJS module (#61)
+* Improve performance (#21, #32, #36)
+* Set `autoDestroy` to `false` by default in order to avoid regressions when upgrading from Node.js 12 to Node.js 14 (#9)
+* Add option `isPartAFile`, to make the file-detection configurable (#53)
+* Add property `bytesRead` on FileStreams (#51)
+* Add and expose headerSize limit (#64)
+* Throw an error on non-number limit (#7)
+* Use the native TextDecoder and the package `text-decoding` for fallback if Node.js does not support the requested encoding (#50)
+* Integrate `dicer` dependency into `busboy` itself (#14)
+* Convert tests to Mocha (#11, #12, #22, #23)
+* Implement better benchmarks (#40, #54)
+* Use JavaScript Standard style (#44, #45)
diff --git a/fastify-busboy/LICENSE b/fastify-busboy/LICENSE
new file mode 100644
index 0000000..290762e
--- /dev/null
+++ b/fastify-busboy/LICENSE
@@ -0,0 +1,19 @@
+Copyright Brian White. All rights reserved.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to
+deal in the Software without restriction, including without limitation the
+rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+sell copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+IN THE SOFTWARE. \ No newline at end of file
diff --git a/fastify-busboy/README.md b/fastify-busboy/README.md
new file mode 100644
index 0000000..c74e618
--- /dev/null
+++ b/fastify-busboy/README.md
@@ -0,0 +1,271 @@
+# busboy
+
+<div align="center">
+
+[![Build Status](https://github.com/fastify/busboy/workflows/ci/badge.svg)](https://github.com/fastify/busboy/actions)
+[![Coverage Status](https://coveralls.io/repos/fastify/busboy/badge.svg?branch=master)](https://coveralls.io/r/fastify/busboy?branch=master)
+[![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat)](https://standardjs.com/)
+[![Security Responsible Disclosure](https://img.shields.io/badge/Security-Responsible%20Disclosure-yellow.svg)](https://github.com/nodejs/security-wg/blob/HEAD/processes/responsible_disclosure_template.md)
+
+</div>
+
+<div align="center">
+
+[![NPM version](https://img.shields.io/npm/v/@fastify/busboy.svg?style=flat)](https://www.npmjs.com/package/@fastify/busboy)
+[![NPM downloads](https://img.shields.io/npm/dm/@fastify/busboy.svg?style=flat)](https://www.npmjs.com/package/@fastify/busboy)
+
+</div>
+
+Description
+===========
+
+A Node.js module for parsing incoming HTML form data.
+
+This is an officially supported fork by [fastify](https://github.com/fastify/) organization of the amazing library [originally created](https://github.com/mscdex/busboy) by Brian White,
+aimed at addressing long-standing issues with it.
+
+Benchmark (Mean time for 500 Kb payload, 2000 cycles, 1000 cycle warmup):
+
+| Library | Version | Mean time in nanoseconds (less is better) |
+|-----------------------|---------|-------------------------------------------|
+| busboy | 0.3.1 | `340114` |
+| @fastify/busboy | 1.0.0 | `270984` |
+
+[Changelog](https://github.com/fastify/busboy/blob/master/CHANGELOG.md) since busboy 0.31.
+
+Requirements
+============
+
+* [Node.js](http://nodejs.org/) 10+
+
+
+Install
+=======
+
+ npm i @fastify/busboy
+
+
+Examples
+========
+
+* Parsing (multipart) with default options:
+
+```javascript
+const http = require('node:http');
+const { inspect } = require('node:util');
+const Busboy = require('busboy');
+
+http.createServer((req, res) => {
+ if (req.method === 'POST') {
+ const busboy = new Busboy({ headers: req.headers });
+ busboy.on('file', (fieldname, file, filename, encoding, mimetype) => {
+ console.log(`File [${fieldname}]: filename: ${filename}, encoding: ${encoding}, mimetype: ${mimetype}`);
+ file.on('data', data => {
+ console.log(`File [${fieldname}] got ${data.length} bytes`);
+ });
+ file.on('end', () => {
+ console.log(`File [${fieldname}] Finished`);
+ });
+ });
+ busboy.on('field', (fieldname, val, fieldnameTruncated, valTruncated, encoding, mimetype) => {
+ console.log(`Field [${fieldname}]: value: ${inspect(val)}`);
+ });
+ busboy.on('finish', () => {
+ console.log('Done parsing form!');
+ res.writeHead(303, { Connection: 'close', Location: '/' });
+ res.end();
+ });
+ req.pipe(busboy);
+ } else if (req.method === 'GET') {
+ res.writeHead(200, { Connection: 'close' });
+ res.end(`<html><head></head><body>
+ <form method="POST" enctype="multipart/form-data">
+ <input type="text" name="textfield"><br>
+ <input type="file" name="filefield"><br>
+ <input type="submit">
+ </form>
+ </body></html>`);
+ }
+}).listen(8000, () => {
+ console.log('Listening for requests');
+});
+
+// Example output, using http://nodejs.org/images/ryan-speaker.jpg as the file:
+//
+// Listening for requests
+// File [filefield]: filename: ryan-speaker.jpg, encoding: binary
+// File [filefield] got 11971 bytes
+// Field [textfield]: value: 'testing! :-)'
+// File [filefield] Finished
+// Done parsing form!
+```
+
+* Save all incoming files to disk:
+
+```javascript
+const http = require('node:http');
+const path = require('node:path');
+const os = require('node:os');
+const fs = require('node:fs');
+
+const Busboy = require('busboy');
+
+http.createServer(function(req, res) {
+ if (req.method === 'POST') {
+ const busboy = new Busboy({ headers: req.headers });
+ busboy.on('file', function(fieldname, file, filename, encoding, mimetype) {
+ var saveTo = path.join(os.tmpdir(), path.basename(fieldname));
+ file.pipe(fs.createWriteStream(saveTo));
+ });
+ busboy.on('finish', function() {
+ res.writeHead(200, { 'Connection': 'close' });
+ res.end("That's all folks!");
+ });
+ return req.pipe(busboy);
+ }
+ res.writeHead(404);
+ res.end();
+}).listen(8000, function() {
+ console.log('Listening for requests');
+});
+```
+
+* Parsing (urlencoded) with default options:
+
+```javascript
+const http = require('node:http');
+const { inspect } = require('node:util');
+
+const Busboy = require('busboy');
+
+http.createServer(function(req, res) {
+ if (req.method === 'POST') {
+ const busboy = new Busboy({ headers: req.headers });
+ busboy.on('file', function(fieldname, file, filename, encoding, mimetype) {
+ console.log('File [' + fieldname + ']: filename: ' + filename);
+ file.on('data', function(data) {
+ console.log('File [' + fieldname + '] got ' + data.length + ' bytes');
+ });
+ file.on('end', function() {
+ console.log('File [' + fieldname + '] Finished');
+ });
+ });
+ busboy.on('field', function(fieldname, val, fieldnameTruncated, valTruncated) {
+ console.log('Field [' + fieldname + ']: value: ' + inspect(val));
+ });
+ busboy.on('finish', function() {
+ console.log('Done parsing form!');
+ res.writeHead(303, { Connection: 'close', Location: '/' });
+ res.end();
+ });
+ req.pipe(busboy);
+ } else if (req.method === 'GET') {
+ res.writeHead(200, { Connection: 'close' });
+ res.end('<html><head></head><body>\
+ <form method="POST">\
+ <input type="text" name="textfield"><br />\
+ <select name="selectfield">\
+ <option value="1">1</option>\
+ <option value="10">10</option>\
+ <option value="100">100</option>\
+ <option value="9001">9001</option>\
+ </select><br />\
+ <input type="checkbox" name="checkfield">Node.js rules!<br />\
+ <input type="submit">\
+ </form>\
+ </body></html>');
+ }
+}).listen(8000, function() {
+ console.log('Listening for requests');
+});
+
+// Example output:
+//
+// Listening for requests
+// Field [textfield]: value: 'testing! :-)'
+// Field [selectfield]: value: '9001'
+// Field [checkfield]: value: 'on'
+// Done parsing form!
+```
+
+
+API
+===
+
+_Busboy_ is a _Writable_ stream
+
+Busboy (special) events
+-----------------------
+
+* **file**(< _string_ >fieldname, < _ReadableStream_ >stream, < _string_ >filename, < _string_ >transferEncoding, < _string_ >mimeType) - Emitted for each new file form field found. `transferEncoding` contains the 'Content-Transfer-Encoding' value for the file stream. `mimeType` contains the 'Content-Type' value for the file stream.
+ * Note: if you listen for this event, you should always handle the `stream` no matter if you care about the file contents or not (e.g. you can simply just do `stream.resume();` if you want to discard the contents), otherwise the 'finish' event will never fire on the Busboy instance. However, if you don't care about **any** incoming files, you can simply not listen for the 'file' event at all and any/all files will be automatically and safely discarded (these discarded files do still count towards `files` and `parts` limits).
+ * If a configured file size limit was reached, `stream` will both have a boolean property `truncated` (best checked at the end of the stream) and emit a 'limit' event to notify you when this happens.
+ * The property `bytesRead` informs about the number of bytes that have been read so far.
+
+* **field**(< _string_ >fieldname, < _string_ >value, < _boolean_ >fieldnameTruncated, < _boolean_ >valueTruncated, < _string_ >transferEncoding, < _string_ >mimeType) - Emitted for each new non-file field found.
+
+* **partsLimit**() - Emitted when specified `parts` limit has been reached. No more 'file' or 'field' events will be emitted.
+
+* **filesLimit**() - Emitted when specified `files` limit has been reached. No more 'file' events will be emitted.
+
+* **fieldsLimit**() - Emitted when specified `fields` limit has been reached. No more 'field' events will be emitted.
+
+
+Busboy methods
+--------------
+
+* **(constructor)**(< _object_ >config) - Creates and returns a new Busboy instance.
+
+ * The constructor takes the following valid `config` settings:
+
+ * **headers** - _object_ - These are the HTTP headers of the incoming request, which are used by individual parsers.
+
+ * **autoDestroy** - _boolean_ - Whether this stream should automatically call .destroy() on itself after ending. (Default: false).
+
+ * **highWaterMark** - _integer_ - highWaterMark to use for this Busboy instance (Default: WritableStream default).
+
+ * **fileHwm** - _integer_ - highWaterMark to use for file streams (Default: ReadableStream default).
+
+ * **defCharset** - _string_ - Default character set to use when one isn't defined (Default: 'utf8').
+
+ * **preservePath** - _boolean_ - If paths in the multipart 'filename' field shall be preserved. (Default: false).
+
+ * **isPartAFile** - __function__ - Use this function to override the default file detection functionality. It has following parameters:
+
+ * fieldName - __string__ The name of the field.
+
+ * contentType - __string__ The content-type of the part, e.g. `text/plain`, `image/jpeg`, `application/octet-stream`
+
+ * fileName - __string__ The name of a file supplied by the part.
+
+ (Default: `(fieldName, contentType, fileName) => (contentType === 'application/octet-stream' || fileName !== undefined)`)
+
+ * **limits** - _object_ - Various limits on incoming data. Valid properties are:
+
+ * **fieldNameSize** - _integer_ - Max field name size (in bytes) (Default: 100 bytes).
+
+ * **fieldSize** - _integer_ - Max field value size (in bytes) (Default: 1 MiB, which is 1024 x 1024 bytes).
+
+ * **fields** - _integer_ - Max number of non-file fields (Default: Infinity).
+
+ * **fileSize** - _integer_ - For multipart forms, the max file size (in bytes) (Default: Infinity).
+
+ * **files** - _integer_ - For multipart forms, the max number of file fields (Default: Infinity).
+
+ * **parts** - _integer_ - For multipart forms, the max number of parts (fields + files) (Default: Infinity).
+
+ * **headerPairs** - _integer_ - For multipart forms, the max number of header key=>value pairs to parse **Default:** 2000
+
+ * **headerSize** - _integer_ - For multipart forms, the max size of a multipart header **Default:** 81920.
+
+ * The constructor can throw errors:
+
+ * **Busboy expected an options-Object.** - Busboy expected an Object as first parameters.
+
+ * **Busboy expected an options-Object with headers-attribute.** - The first parameter is lacking of a headers-attribute.
+
+ * **Limit $limit is not a valid number** - Busboy expected the desired limit to be of type number. Busboy throws this Error to prevent a potential security issue by falling silently back to the Busboy-defaults. Potential source for this Error can be the direct use of environment variables without transforming them to the type number.
+
+ * **Unsupported Content-Type.** - The `Content-Type` isn't one Busboy can parse.
+
+ * **Missing Content-Type-header.** - The provided headers don't include `Content-Type` at all.
diff --git a/fastify-busboy/bench/busboy-form-bench-latin1.js b/fastify-busboy/bench/busboy-form-bench-latin1.js
new file mode 100644
index 0000000..33634ad
--- /dev/null
+++ b/fastify-busboy/bench/busboy-form-bench-latin1.js
@@ -0,0 +1,32 @@
+'use strict'
+
+const Busboy = require('busboy');
+const { createMultipartBufferForEncodingBench } = require("./createMultipartBufferForEncodingBench");
+
+ for (var i = 0, il = 10000; i < il; i++) { // eslint-disable-line no-var
+ const boundary = '-----------------------------168072824752491622650073',
+ busboy = new Busboy({
+ headers: {
+ 'content-type': 'multipart/form-data; boundary=' + boundary
+ }
+ }),
+ buffer = createMultipartBufferForEncodingBench(boundary, 100, 'iso-8859-1'),
+ mb = buffer.length / 1048576;
+
+ let processedData = 0;
+ busboy.on('file', (field, file, filename, encoding, mimetype) => {
+ file.resume()
+ })
+
+ busboy.on('error', function (err) {
+ })
+ busboy.on('finish', function () {
+ })
+
+ const start = +new Date();
+ const result = busboy.write(buffer, () => { });
+ busboy.end();
+ const duration = +new Date - start;
+ const mbPerSec = (mb / (duration / 1000)).toFixed(2);
+ console.log(mbPerSec + ' mb/sec');
+ } \ No newline at end of file
diff --git a/fastify-busboy/bench/busboy-form-bench-utf8.js b/fastify-busboy/bench/busboy-form-bench-utf8.js
new file mode 100644
index 0000000..1e6e0b7
--- /dev/null
+++ b/fastify-busboy/bench/busboy-form-bench-utf8.js
@@ -0,0 +1,32 @@
+'use strict'
+
+const Busboy = require('busboy');
+const { createMultipartBufferForEncodingBench } = require("./createMultipartBufferForEncodingBench");
+
+ for (var i = 0, il = 10000; i < il; i++) { // eslint-disable-line no-var
+ const boundary = '-----------------------------168072824752491622650073',
+ busboy = new Busboy({
+ headers: {
+ 'content-type': 'multipart/form-data; boundary=' + boundary
+ }
+ }),
+ buffer = createMultipartBufferForEncodingBench(boundary, 100, 'utf-8'),
+ mb = buffer.length / 1048576;
+
+ let processedData = 0;
+ busboy.on('file', (field, file, filename, encoding, mimetype) => {
+ file.resume()
+ })
+
+ busboy.on('error', function (err) {
+ })
+ busboy.on('finish', function () {
+ })
+
+ const start = +new Date();
+ const result = busboy.write(buffer, () => { });
+ busboy.end();
+ const duration = +new Date - start;
+ const mbPerSec = (mb / (duration / 1000)).toFixed(2);
+ console.log(mbPerSec + ' mb/sec');
+ } \ No newline at end of file
diff --git a/fastify-busboy/bench/createMultipartBufferForEncodingBench.js b/fastify-busboy/bench/createMultipartBufferForEncodingBench.js
new file mode 100644
index 0000000..9d20f8f
--- /dev/null
+++ b/fastify-busboy/bench/createMultipartBufferForEncodingBench.js
@@ -0,0 +1,23 @@
+'use strict'
+
+function createMultipartBufferForEncodingBench(boundary, amount, charset) {
+ const filename = charset === 'utf-8' ? 'utf-8\'\'%c2%a3%20and%20%e2%82%ac%20rates' : `${charset}\'en\'%A3%20rates`;
+ const head = '--' + boundary + '\r\n'
+ + 'content-disposition: form-data; name="field1"\r\n'
+ + 'content-type: text/plain;charset=' + charset + '; filename*=' + filename + '\r\n'
+ + '\r\n', tail = '\r\n--' + boundary + '--\r\n', buffer = Buffer.concat([Buffer.from(head), Buffer.from(`
+Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus pretium leo ex, vitae dignissim felis viverra non. Praesent id quam ac elit tincidunt porttitor sed eget magna. Vivamus nibh ipsum, malesuada in eros sit amet, rutrum mattis leo. Ut nunc justo, ornare a finibus in, consectetur euismod sapien. Praesent facilisis, odio consectetur facilisis varius, tellus justo tristique sapien, non porttitor eros massa quis nibh. Nam blandit orci ac efficitur cursus. Nunc non mollis sapien, sit amet pretium odio. Nam vestibulum lectus ac orci egestas aliquet. Duis nec nibh quis augue consequat vulputate a a dui.
+
+Aenean nec laoreet dolor, commodo aliquam leo. Quisque at placerat sem. In scelerisque cursus dolor, ac aliquam metus malesuada in. Vestibulum lacinia dolor purus, at convallis ipsum iaculis id. Integer bibendum sem neque, at bibendum enim lobortis eu. Cras pretium arcu eget congue cursus. Curabitur blandit ultricies mollis. Sed lacinia quis felis ut fringilla.
+
+Nulla vitae lobortis metus. Morbi gravida risus tortor, in pulvinar massa lobortis vitae. Etiam vitae massa libero. Sed id tincidunt elit. Quisque congue felis vel aliquam varius. Sed a massa vitae lectus vehicula lacinia vitae ac justo. In commodo sodales nisi finibus vulputate. Suspendisse viverra, est eget fringilla gravida, nulla justo vulputate lorem, at eleifend nisi urna a eros. Sed sit amet ipsum vehicula, venenatis urna ac, interdum felis.
+
+Cras semper mi magna, nec iaculis neque rhoncus at. In sit amet odio sed libero fringilla commodo. Sed hendrerit pulvinar turpis sed porta. Pellentesque consequat scelerisque sapien nec iaculis. Aenean sed nunc a purus laoreet efficitur id eu orci. Mauris tincidunt auctor congue. Aliquam nisi ligula, facilisis a molestie sed, luctus vitae mauris. Mauris at facilisis elit. Maecenas sodales pretium nisi in sodales. Cras nec blandit enim. Praesent in lacus et nibh varius suscipit in sit amet nibh.
+
+Nam hendrerit justo eu lectus molestie, sit amet fringilla ipsum semper. Maecenas sit amet nunc elementum, interdum nunc eu, euismod ipsum. Vestibulum ut mauris sapien. Praesent nec felis ex. Fusce vel leo lobortis, mattis sem a, ullamcorper dolor. Aliquam erat volutpat. Fusce feugiat odio ut feugiat volutpat. Vestibulum magna ante, tempor in volutpat ut, gravida vitae justo. Praesent vitae eleifend eros. Integer feugiat molestie dolor, et pretium enim accumsan sit amet. Sed quis suscipit dui. Integer gravida dolor elit, sit amet fringilla odio commodo at. Quisque ut eleifend risus. Nunc mollis velit quis lectus laoreet pellentesque.\r\n\r\n`)]);
+
+ const buffers = new Array(amount).fill(buffer);
+ buffers.push(Buffer.from(tail));
+ return Buffer.concat(buffers);
+}
+exports.createMultipartBufferForEncodingBench = createMultipartBufferForEncodingBench;
diff --git a/fastify-busboy/bench/dicer/dicer-bench-multipart-parser.js b/fastify-busboy/bench/dicer/dicer-bench-multipart-parser.js
new file mode 100644
index 0000000..d24f599
--- /dev/null
+++ b/fastify-busboy/bench/dicer/dicer-bench-multipart-parser.js
@@ -0,0 +1,60 @@
+'use strict'
+
+const Dicer = require('../../deps/dicer/lib/Dicer')
+
+function createMultipartBuffer(boundary, size) {
+ const head =
+ '--' + boundary + '\r\n'
+ + 'content-disposition: form-data; name="field1"\r\n'
+ + '\r\n'
+ , tail = '\r\n--' + boundary + '--\r\n'
+ , buffer = Buffer.allocUnsafe(size);
+
+ buffer.write(head, 0, 'ascii');
+ buffer.write(tail, buffer.length - tail.length, 'ascii');
+ return buffer;
+}
+
+for (var i = 0, il = 10; i < il; i++) { // eslint-disable-line no-var
+ const boundary = '-----------------------------168072824752491622650073',
+ d = new Dicer({ boundary: boundary }),
+ mb = 100,
+ buffer = createMultipartBuffer(boundary, mb * 1024 * 1024),
+ callbacks =
+ {
+ partBegin: -1,
+ partEnd: -1,
+ headerField: -1,
+ headerValue: -1,
+ partData: -1,
+ end: -1,
+ };
+
+
+ d.on('part', function (p) {
+ callbacks.partBegin++;
+ p.on('header', function (header) {
+ /*for (var h in header)
+ console.log('Part header: k: ' + inspect(h) + ', v: ' + inspect(header[h]));*/
+ });
+ p.on('data', function (data) {
+ callbacks.partData++;
+ //console.log('Part data: ' + inspect(data.toString()));
+ });
+ p.on('end', function () {
+ //console.log('End of part\n');
+ callbacks.partEnd++;
+ });
+ });
+ d.on('end', function () {
+ //console.log('End of parts');
+ callbacks.end++;
+ });
+
+ const start = +new Date();
+ d.write(buffer);
+ const duration = +new Date - start;
+ const mbPerSec = (mb / (duration / 1000)).toFixed(2);
+
+ console.log(mbPerSec + ' mb/sec');
+}
diff --git a/fastify-busboy/bench/dicer/formidable-bench-multipart-parser.js b/fastify-busboy/bench/dicer/formidable-bench-multipart-parser.js
new file mode 100644
index 0000000..0470771
--- /dev/null
+++ b/fastify-busboy/bench/dicer/formidable-bench-multipart-parser.js
@@ -0,0 +1,71 @@
+'use strict'
+
+require('../node_modules/formidable/test/common');
+var multipartParser = require('../node_modules/formidable/lib/multipart_parser'),
+ MultipartParser = multipartParser.MultipartParser,
+ parser = new MultipartParser(),
+ boundary = '-----------------------------168072824752491622650073',
+ mb = 100,
+ buffer = createMultipartBuffer(boundary, mb * 1024 * 1024),
+ callbacks =
+ { partBegin: -1,
+ partEnd: -1,
+ headerField: -1,
+ headerValue: -1,
+ partData: -1,
+ end: -1,
+ };
+
+
+parser.initWithBoundary(boundary);
+parser.onHeaderField = function() {
+ callbacks.headerField++;
+};
+
+parser.onHeaderValue = function() {
+ callbacks.headerValue++;
+};
+
+parser.onPartBegin = function() {
+ callbacks.partBegin++;
+};
+
+parser.onPartData = function() {
+ callbacks.partData++;
+};
+
+parser.onPartEnd = function() {
+ callbacks.partEnd++;
+};
+
+parser.onEnd = function() {
+ callbacks.end++;
+};
+
+var start = +new Date(),
+ nparsed = parser.write(buffer),
+ duration = +new Date - start,
+ mbPerSec = (mb / (duration / 1000)).toFixed(2);
+
+console.log(mbPerSec+' mb/sec');
+
+//assert.equal(nparsed, buffer.length);
+
+function createMultipartBuffer(boundary, size) {
+ var head =
+ '--'+boundary+'\r\n'
+ + 'content-disposition: form-data; name="field1"\r\n'
+ + '\r\n'
+ , tail = '\r\n--'+boundary+'--\r\n'
+ , buffer = Buffer.allocUnsafe(size);
+
+ buffer.write(head, 'ascii', 0);
+ buffer.write(tail, 'ascii', buffer.length - tail.length);
+ return buffer;
+}
+
+process.on('exit', function() {
+ /*for (var k in callbacks) {
+ assert.equal(0, callbacks[k], k+' count off by '+callbacks[k]);
+ }*/
+});
diff --git a/fastify-busboy/bench/dicer/multipartser-bench-multipart-parser.js b/fastify-busboy/bench/dicer/multipartser-bench-multipart-parser.js
new file mode 100644
index 0000000..40ca00b
--- /dev/null
+++ b/fastify-busboy/bench/dicer/multipartser-bench-multipart-parser.js
@@ -0,0 +1,57 @@
+'use strict'
+
+var multipartser = require('multipartser'),
+ boundary = '-----------------------------168072824752491622650073',
+ parser = multipartser(),
+ mb = 100,
+ buffer = createMultipartBuffer(boundary, mb * 1024 * 1024),
+ callbacks =
+ { partBegin: -1,
+ partEnd: -1,
+ headerField: -1,
+ headerValue: -1,
+ partData: -1,
+ end: -1,
+ };
+
+parser.boundary( boundary );
+
+parser.on( 'part', function ( part ) {
+});
+
+parser.on( 'end', function () {
+ //console.log( 'completed parsing' );
+});
+
+parser.on( 'error', function ( error ) {
+ console.error( error );
+});
+
+var start = +new Date(),
+ nparsed = parser.data(buffer),
+ nend = parser.end(),
+ duration = +new Date - start,
+ mbPerSec = (mb / (duration / 1000)).toFixed(2);
+
+console.log(mbPerSec+' mb/sec');
+
+//assert.equal(nparsed, buffer.length);
+
+function createMultipartBuffer(boundary, size) {
+ var head =
+ '--'+boundary+'\r\n'
+ + 'content-disposition: form-data; name="field1"\r\n'
+ + '\r\n'
+ , tail = '\r\n--'+boundary+'--\r\n'
+ , buffer = Buffer.allocUnsafe(size);
+
+ buffer.write(head, 'ascii', 0);
+ buffer.write(tail, 'ascii', buffer.length - tail.length);
+ return buffer;
+}
+
+process.on('exit', function() {
+ /*for (var k in callbacks) {
+ assert.equal(0, callbacks[k], k+' count off by '+callbacks[k]);
+ }*/
+});
diff --git a/fastify-busboy/bench/dicer/multiparty-bench-multipart-parser.js b/fastify-busboy/bench/dicer/multiparty-bench-multipart-parser.js
new file mode 100644
index 0000000..ab79ec0
--- /dev/null
+++ b/fastify-busboy/bench/dicer/multiparty-bench-multipart-parser.js
@@ -0,0 +1,78 @@
+'use strict'
+
+var assert = require('node:assert'),
+ Form = require('multiparty').Form,
+ boundary = '-----------------------------168072824752491622650073',
+ mb = 100,
+ buffer = createMultipartBuffer(boundary, mb * 1024 * 1024),
+ callbacks =
+ { partBegin: -1,
+ partEnd: -1,
+ headerField: -1,
+ headerValue: -1,
+ partData: -1,
+ end: -1,
+ };
+
+var form = new Form({ boundary: boundary });
+
+hijack('onParseHeaderField', function() {
+ callbacks.headerField++;
+});
+
+hijack('onParseHeaderValue', function() {
+ callbacks.headerValue++;
+});
+
+hijack('onParsePartBegin', function() {
+ callbacks.partBegin++;
+});
+
+hijack('onParsePartData', function() {
+ callbacks.partData++;
+});
+
+hijack('onParsePartEnd', function() {
+ callbacks.partEnd++;
+});
+
+form.on('finish', function() {
+ callbacks.end++;
+});
+
+var start = new Date();
+form.write(buffer, function(err) {
+ var duration = new Date() - start;
+ assert.ifError(err);
+ var mbPerSec = (mb / (duration / 1000)).toFixed(2);
+ console.log(mbPerSec+' mb/sec');
+});
+
+//assert.equal(nparsed, buffer.length);
+
+function createMultipartBuffer(boundary, size) {
+ var head =
+ '--'+boundary+'\r\n'
+ + 'content-disposition: form-data; name="field1"\r\n'
+ + '\r\n'
+ , tail = '\r\n--'+boundary+'--\r\n'
+ , buffer = Buffer.allocUnsafe(size);
+
+ buffer.write(head, 'ascii', 0);
+ buffer.write(tail, 'ascii', buffer.length - tail.length);
+ return buffer;
+}
+
+process.on('exit', function() {
+ /*for (var k in callbacks) {
+ assert.equal(0, callbacks[k], k+' count off by '+callbacks[k]);
+ }*/
+});
+
+function hijack(name, fn) {
+ var oldFn = form[name];
+ form[name] = function() {
+ fn();
+ return oldFn.apply(this, arguments);
+ };
+}
diff --git a/fastify-busboy/bench/dicer/parted-bench-multipart-parser.js b/fastify-busboy/bench/dicer/parted-bench-multipart-parser.js
new file mode 100644
index 0000000..e0a4670
--- /dev/null
+++ b/fastify-busboy/bench/dicer/parted-bench-multipart-parser.js
@@ -0,0 +1,65 @@
+'use strict'
+
+// A special, edited version of the multipart parser from parted is needed here
+// because otherwise it attempts to do some things above and beyond just parsing
+// -- like saving to disk and whatnot
+
+var assert = require('node:assert');
+var Parser = require('./parted-multipart'),
+ boundary = '-----------------------------168072824752491622650073',
+ parser = new Parser('boundary=' + boundary),
+ mb = 100,
+ buffer = createMultipartBuffer(boundary, mb * 1024 * 1024),
+ callbacks =
+ { partBegin: -1,
+ partEnd: -1,
+ headerField: -1,
+ headerValue: -1,
+ partData: -1,
+ end: -1,
+ };
+
+
+parser.on('header', function() {
+ //callbacks.headerField++;
+});
+
+parser.on('data', function() {
+ //callbacks.partBegin++;
+});
+
+parser.on('part', function() {
+
+});
+
+parser.on('end', function() {
+ //callbacks.end++;
+});
+
+var start = +new Date(),
+ nparsed = parser.write(buffer),
+ duration = +new Date - start,
+ mbPerSec = (mb / (duration / 1000)).toFixed(2);
+
+console.log(mbPerSec+' mb/sec');
+
+//assert.equal(nparsed, buffer.length);
+
+function createMultipartBuffer(boundary, size) {
+ var head =
+ '--'+boundary+'\r\n'
+ + 'content-disposition: form-data; name="field1"\r\n'
+ + '\r\n'
+ , tail = '\r\n--'+boundary+'--\r\n'
+ , buffer = Buffer.allocUnsafe(size);
+
+ buffer.write(head, 'ascii', 0);
+ buffer.write(tail, 'ascii', buffer.length - tail.length);
+ return buffer;
+}
+
+process.on('exit', function() {
+ /*for (var k in callbacks) {
+ assert.equal(0, callbacks[k], k+' count off by '+callbacks[k]);
+ }*/
+});
diff --git a/fastify-busboy/bench/dicer/parted-multipart.js b/fastify-busboy/bench/dicer/parted-multipart.js
new file mode 100644
index 0000000..f214ff4
--- /dev/null
+++ b/fastify-busboy/bench/dicer/parted-multipart.js
@@ -0,0 +1,486 @@
+'use strict'
+
+/**
+ * Parted (https://github.com/chjj/parted)
+ * A streaming multipart state parser.
+ * Copyright (c) 2011, Christopher Jeffrey. (MIT Licensed)
+ */
+
+var fs = require('node:fs')
+ , path = require('node:path')
+ , EventEmitter = require('node:events').EventEmitter
+ , StringDecoder = require('node:string_decoder').StringDecoder
+ , set = require('qs').set
+ , each = Array.prototype.forEach;
+
+/**
+ * Character Constants
+ */
+
+var DASH = '-'.charCodeAt(0)
+ , CR = '\r'.charCodeAt(0)
+ , LF = '\n'.charCodeAt(0)
+ , COLON = ':'.charCodeAt(0)
+ , SPACE = ' '.charCodeAt(0);
+
+/**
+ * Parser
+ */
+
+var Parser = function(type, options) {
+ if (!(this instanceof Parser)) {
+ return new Parser(type, options);
+ }
+
+ EventEmitter.call(this);
+
+ this.writable = true;
+ this.readable = true;
+
+ this.options = options || {};
+
+ var key = grab(type, 'boundary');
+ if (!key) {
+ return this._error('No boundary key found.');
+ }
+
+ this.key = Buffer.allocUnsafe('\r\n--' + key);
+
+ this._key = {};
+ each.call(this.key, function(ch) {
+ this._key[ch] = true;
+ }, this);
+
+ this.state = 'start';
+ this.pending = 0;
+ this.written = 0;
+ this.writtenDisk = 0;
+ this.buff = Buffer.allocUnsafe(200);
+
+ this.preamble = true;
+ this.epilogue = false;
+
+ this._reset();
+};
+
+Parser.prototype.__proto__ = EventEmitter.prototype;
+
+/**
+ * Parsing
+ */
+
+Parser.prototype.write = function(data) {
+ if (!this.writable
+ || this.epilogue) return;
+
+ try {
+ this._parse(data);
+ } catch (e) {
+ this._error(e);
+ }
+
+ return true;
+};
+
+Parser.prototype.end = function(data) {
+ if (!this.writable) return;
+
+ if (data) this.write(data);
+
+ if (!this.epilogue) {
+ return this._error('Message underflow.');
+ }
+
+ return true;
+};
+
+Parser.prototype._parse = function(data) {
+ var i = 0
+ , len = data.length
+ , buff = this.buff
+ , key = this.key
+ , ch
+ , val
+ , j;
+
+ for (; i < len; i++) {
+ if (this.pos >= 200) {
+ return this._error('Potential buffer overflow.');
+ }
+
+ ch = data[i];
+
+ switch (this.state) {
+ case 'start':
+ switch (ch) {
+ case DASH:
+ this.pos = 3;
+ this.state = 'key';
+ break;
+ default:
+ break;
+ }
+ break;
+ case 'key':
+ if (this.pos === key.length) {
+ this.state = 'key_end';
+ i--;
+ } else if (ch !== key[this.pos]) {
+ if (this.preamble) {
+ this.state = 'start';
+ i--;
+ } else {
+ this.state = 'body';
+ val = this.pos - i;
+ if (val > 0) {
+ this._write(key.slice(0, val));
+ }
+ i--;
+ }
+ } else {
+ this.pos++;
+ }
+ break;
+ case 'key_end':
+ switch (ch) {
+ case CR:
+ this.state = 'key_line_end';
+ break;
+ case DASH:
+ this.state = 'key_dash_end';
+ break;
+ default:
+ return this._error('Expected CR or DASH.');
+ }
+ break;
+ case 'key_line_end':
+ switch (ch) {
+ case LF:
+ if (this.preamble) {
+ this.preamble = false;
+ } else {
+ this._finish();
+ }
+ this.state = 'header_name';
+ this.pos = 0;
+ break;
+ default:
+ return this._error('Expected CR.');
+ }
+ break;
+ case 'key_dash_end':
+ switch (ch) {
+ case DASH:
+ this.epilogue = true;
+ this._finish();
+ return;
+ default:
+ return this._error('Expected DASH.');
+ }
+ case 'header_name':
+ switch (ch) {
+ case COLON:
+ this.header = buff.toString('ascii', 0, this.pos);
+ this.pos = 0;
+ this.state = 'header_val';
+ break;
+ default:
+ buff[this.pos++] = ch | 32;
+ break;
+ }
+ break;
+ case 'header_val':
+ switch (ch) {
+ case CR:
+ this.state = 'header_val_end';
+ break;
+ case SPACE:
+ if (this.pos === 0) {
+ break;
+ }
+ // FALL-THROUGH
+ default:
+ buff[this.pos++] = ch;
+ break;
+ }
+ break;
+ case 'header_val_end':
+ switch (ch) {
+ case LF:
+ val = buff.toString('ascii', 0, this.pos);
+ this._header(this.header, val);
+ this.pos = 0;
+ this.state = 'header_end';
+ break;
+ default:
+ return this._error('Expected LF.');
+ }
+ break;
+ case 'header_end':
+ switch (ch) {
+ case CR:
+ this.state = 'head_end';
+ break;
+ default:
+ this.state = 'header_name';
+ i--;
+ break;
+ }
+ break;
+ case 'head_end':
+ switch (ch) {
+ case LF:
+ this.state = 'body';
+ i++;
+ if (i >= len) return;
+ data = data.slice(i);
+ i = -1;
+ len = data.length;
+ break;
+ default:
+ return this._error('Expected LF.');
+ }
+ break;
+ case 'body':
+ switch (ch) {
+ case CR:
+ if (i > 0) {
+ this._write(data.slice(0, i));
+ }
+ this.pos = 1;
+ this.state = 'key';
+ data = data.slice(i);
+ i = 0;
+ len = data.length;
+ break;
+ default:
+ // boyer-moore-like algorithm
+ // at felixge's suggestion
+ while ((j = i + key.length - 1) < len) {
+ if (this._key[data[j]]) break;
+ i = j;
+ }
+ break;
+ }
+ break;
+ }
+ }
+
+ if (this.state === 'body') {
+ this._write(data);
+ }
+};
+
+Parser.prototype._header = function(name, val) {
+ /*if (name === 'content-disposition') {
+ this.field = grab(val, 'name');
+ this.file = grab(val, 'filename');
+
+ if (this.file) {
+ this.data = stream(this.file, this.options.path);
+ } else {
+ this.decode = new StringDecoder('utf8');
+ this.data = '';
+ }
+ }*/
+
+ return this.emit('header', name, val);
+};
+
+Parser.prototype._write = function(data) {
+ /*if (this.data == null) {
+ return this._error('No disposition.');
+ }
+
+ if (this.file) {
+ this.data.write(data);
+ this.writtenDisk += data.length;
+ } else {
+ this.data += this.decode.write(data);
+ this.written += data.length;
+ }*/
+
+ this.emit('data', data);
+};
+
+Parser.prototype._reset = function() {
+ this.pos = 0;
+ this.decode = null;
+ this.field = null;
+ this.data = null;
+ this.file = null;
+ this.header = null;
+};
+
+Parser.prototype._error = function(err) {
+ this.destroy();
+ this.emit('error', typeof err === 'string'
+ ? new Error(err)
+ : err);
+};
+
+Parser.prototype.destroy = function(err) {
+ this.writable = false;
+ this.readable = false;
+ this._reset();
+};
+
+Parser.prototype._finish = function() {
+ var self = this
+ , field = this.field
+ , data = this.data
+ , file = this.file
+ , part;
+
+ this.pending++;
+
+ this._reset();
+
+ if (data && data.path) {
+ part = data.path;
+ data.end(next);
+ } else {
+ part = data;
+ next();
+ }
+
+ function next() {
+ if (!self.readable) return;
+
+ self.pending--;
+
+ self.emit('part', field, part);
+
+ if (data && data.path) {
+ self.emit('file', field, part, file);
+ }
+
+ if (self.epilogue && !self.pending) {
+ self.emit('end');
+ self.destroy();
+ }
+ }
+};
+
+/**
+ * Uploads
+ */
+
+Parser.root = process.platform === 'win32'
+ ? 'C:/Temp'
+ : '/tmp';
+
+/**
+ * Middleware
+ */
+
+Parser.middleware = function(options) {
+ options = options || {};
+ return function(req, res, next) {
+ if (options.ensureBody) {
+ req.body = {};
+ }
+
+ if (req.method === 'GET'
+ || req.method === 'HEAD'
+ || req._multipart) return next();
+
+ req._multipart = true;
+
+ var type = req.headers['content-type'];
+
+ if (type) type = type.split(';', 1)[0].trim().toLowerCase();
+
+ if (type === 'multipart/form-data') {
+ Parser.handle(req, res, next, options);
+ } else {
+ next();
+ }
+ };
+};
+
+/**
+ * Handler
+ */
+
+Parser.handle = function(req, res, next, options) {
+ var parser = new Parser(req.headers['content-type'], options)
+ , diskLimit = options.diskLimit
+ , limit = options.limit
+ , parts = {}
+ , files = {};
+
+ parser.on('error', function(err) {
+ req.destroy();
+ next(err);
+ });
+
+ parser.on('part', function(field, part) {
+ set(parts, field, part);
+ });
+
+ parser.on('file', function(field, path, name) {
+ set(files, field, {
+ path: path,
+ name: name,
+ toString: function() {
+ return path;
+ }
+ });
+ });
+
+ parser.on('data', function() {
+ if (this.writtenDisk > diskLimit || this.written > limit) {
+ this.emit('error', new Error('Overflow.'));
+ this.destroy();
+ }
+ });
+
+ parser.on('end', next);
+
+ req.body = parts;
+ req.files = files;
+ req.pipe(parser);
+};
+
+/**
+ * Helpers
+ */
+
+var isWindows = process.platform === 'win32';
+
+var stream = function(name, dir) {
+ var ext = path.extname(name) || ''
+ , name = path.basename(name, ext) || ''
+ , dir = dir || Parser.root
+ , tag;
+
+ tag = Math.random().toString(36).substring(2);
+
+ name = name.substring(0, 200) + '.' + tag;
+ name = path.join(dir, name) + ext.substring(0, 6);
+ name = name.replace(/\0/g, '');
+
+ if (isWindows) {
+ name = name.replace(/[:*<>|"?]/g, '');
+ }
+
+ return fs.createWriteStream(name);
+};
+
+var grab = function(str, name) {
+ if (!str) return;
+
+ var rx = new RegExp('\\b' + name + '\\s*=\\s*("[^"]+"|\'[^\']+\'|[^;,]+)', 'i')
+ , cap = rx.exec(str);
+
+ if (cap) {
+ return cap[1].trim().replace(/^['"]|['"]$/g, '');
+ }
+};
+
+/**
+ * Expose
+ */
+
+module.exports = Parser;
diff --git a/fastify-busboy/bench/fastify-busboy-form-bench-latin1.js b/fastify-busboy/bench/fastify-busboy-form-bench-latin1.js
new file mode 100644
index 0000000..7ca5f44
--- /dev/null
+++ b/fastify-busboy/bench/fastify-busboy-form-bench-latin1.js
@@ -0,0 +1,31 @@
+'use strict'
+
+const Busboy = require('../lib/main');
+const { createMultipartBufferForEncodingBench } = require("./createMultipartBufferForEncodingBench");
+
+ for (var i = 0, il = 10000; i < il; i++) { // eslint-disable-line no-var
+ const boundary = '-----------------------------168072824752491622650073',
+ busboy = new Busboy({
+ headers: {
+ 'content-type': 'multipart/form-data; boundary=' + boundary
+ }
+ }),
+ buffer = createMultipartBufferForEncodingBench(boundary, 100, 'iso-8859-1'),
+ mb = buffer.length / 1048576;
+
+ busboy.on('file', (field, file, filename, encoding, mimetype) => {
+ file.resume()
+ })
+
+ busboy.on('error', function (err) {
+ })
+ busboy.on('finish', function () {
+ })
+
+ const start = +new Date();
+ busboy.write(buffer, () => { });
+ busboy.end();
+ const duration = +new Date - start;
+ const mbPerSec = (mb / (duration / 1000)).toFixed(2);
+ console.log(mbPerSec + ' mb/sec');
+ }
diff --git a/fastify-busboy/bench/fastify-busboy-form-bench-utf8.js b/fastify-busboy/bench/fastify-busboy-form-bench-utf8.js
new file mode 100644
index 0000000..6c35071
--- /dev/null
+++ b/fastify-busboy/bench/fastify-busboy-form-bench-utf8.js
@@ -0,0 +1,31 @@
+'use strict'
+
+const Busboy = require('../lib/main');
+const { createMultipartBufferForEncodingBench } = require("./createMultipartBufferForEncodingBench");
+
+ for (var i = 0, il = 10000; i < il; i++) { // eslint-disable-line no-var
+ const boundary = '-----------------------------168072824752491622650073',
+ busboy = new Busboy({
+ headers: {
+ 'content-type': 'multipart/form-data; boundary=' + boundary
+ }
+ }),
+ buffer = createMultipartBufferForEncodingBench(boundary, 100, 'utf-8'),
+ mb = buffer.length / 1048576;
+
+ busboy.on('file', (field, file, filename, encoding, mimetype) => {
+ file.resume()
+ })
+
+ busboy.on('error', function (err) {
+ })
+ busboy.on('finish', function () {
+ })
+
+ const start = +new Date();
+ busboy.write(buffer, () => { });
+ busboy.end();
+ const duration = +new Date - start;
+ const mbPerSec = (mb / (duration / 1000)).toFixed(2);
+ console.log(mbPerSec + ' mb/sec');
+ }
diff --git a/fastify-busboy/bench/parse-params.js b/fastify-busboy/bench/parse-params.js
new file mode 100644
index 0000000..439a372
--- /dev/null
+++ b/fastify-busboy/bench/parse-params.js
@@ -0,0 +1,21 @@
+'use strict'
+
+const parseParams = require('../lib/utils/parseParams')
+const { Bench } = require('tinybench');
+const bench = new Bench();
+
+const simple = 'video/ogg'
+const complex = "'text/plain; filename*=utf-8''%c2%a3%20and%20%e2%82%ac%20rates'"
+
+bench
+ .add(simple, function () { parseParams(simple) })
+ .add(complex, function () { parseParams(complex) })
+ .run()
+ .then((tasks) => {
+ const errors = tasks.map(t => t.result?.error).filter((t) => t)
+ if (errors.length) {
+ errors.map((e) => console.error(e))
+ } else {
+ console.table(bench.table())
+ }
+ })
diff --git a/fastify-busboy/benchmarks/_results/Busboy_comparison-busboy-Node_12.json b/fastify-busboy/benchmarks/_results/Busboy_comparison-busboy-Node_12.json
new file mode 100644
index 0000000..69468dd
--- /dev/null
+++ b/fastify-busboy/benchmarks/_results/Busboy_comparison-busboy-Node_12.json
@@ -0,0 +1,10 @@
+{
+ "runtimeVersion": "12.22.7, V8 7.8.279.23-node.56",
+ "benchmarkName": "Busboy comparison",
+ "benchmarkEntryName": "busboy",
+ "benchmarkCycles": 10,
+ "benchmarkCycleSamples": 50,
+ "warmupCycles": 10,
+ "meanTimeNs": 1945927.3472222222,
+ "meanTimeMs": 1.9459273472222223
+} \ No newline at end of file
diff --git a/fastify-busboy/benchmarks/_results/Busboy_comparison-busboy-Node_16.json b/fastify-busboy/benchmarks/_results/Busboy_comparison-busboy-Node_16.json
new file mode 100644
index 0000000..b4c492a
--- /dev/null
+++ b/fastify-busboy/benchmarks/_results/Busboy_comparison-busboy-Node_16.json
@@ -0,0 +1,10 @@
+{
+ "runtimeVersion": "16.13.0, V8 9.4.146.19-node.13",
+ "benchmarkName": "Busboy comparison",
+ "benchmarkEntryName": "busboy",
+ "benchmarkCycles": 2000,
+ "benchmarkCycleSamples": 50,
+ "warmupCycles": 1000,
+ "meanTimeNs": 340114.0411908194,
+ "meanTimeMs": 0.3401140411908194
+} \ No newline at end of file
diff --git a/fastify-busboy/benchmarks/_results/Busboy_comparison-fastify-busboy-Node_16.json b/fastify-busboy/benchmarks/_results/Busboy_comparison-fastify-busboy-Node_16.json
new file mode 100644
index 0000000..30f5d1e
--- /dev/null
+++ b/fastify-busboy/benchmarks/_results/Busboy_comparison-fastify-busboy-Node_16.json
@@ -0,0 +1,10 @@
+{
+ "runtimeVersion": "16.13.0, V8 9.4.146.19-node.13",
+ "benchmarkName": "Busboy comparison",
+ "benchmarkEntryName": "fastify-busboy",
+ "benchmarkCycles": 2000,
+ "benchmarkCycleSamples": 50,
+ "warmupCycles": 1000,
+ "meanTimeNs": 270984.48082281026,
+ "meanTimeMs": 0.27098448082281024
+} \ No newline at end of file
diff --git a/fastify-busboy/benchmarks/busboy/contestants/busboy.js b/fastify-busboy/benchmarks/busboy/contestants/busboy.js
new file mode 100644
index 0000000..6cb3414
--- /dev/null
+++ b/fastify-busboy/benchmarks/busboy/contestants/busboy.js
@@ -0,0 +1,40 @@
+'use strict'
+
+const Busboy = require('busboy')
+const { buffer, boundary } = require('../data')
+
+function process () {
+ const busboy = Busboy({
+ headers: {
+ 'content-type': 'multipart/form-data; boundary=' + boundary
+ }
+ })
+ let processedData = ''
+
+ return new Promise((resolve, reject) => {
+ busboy.on('file', (field, file, filename, encoding, mimetype) => {
+ // console.log('read file')
+ file.on('data', (data) => {
+ processedData += data.toString()
+ // console.log(`File [${filename}] got ${data.length} bytes`);
+ })
+ file.on('end', (fieldname) => {
+ // console.log(`File [${fieldname}] Finished`);
+ })
+ })
+
+ busboy.on('error', function (err) {
+ reject(err)
+ })
+ busboy.on('finish', function () {
+ resolve(processedData)
+ })
+ busboy.write(buffer, () => { })
+
+ busboy.end()
+ })
+}
+
+module.exports = {
+ process
+}
diff --git a/fastify-busboy/benchmarks/busboy/contestants/fastify-busboy.js b/fastify-busboy/benchmarks/busboy/contestants/fastify-busboy.js
new file mode 100644
index 0000000..6750f77
--- /dev/null
+++ b/fastify-busboy/benchmarks/busboy/contestants/fastify-busboy.js
@@ -0,0 +1,41 @@
+'use strict'
+
+const Busboy = require('../../../lib/main')
+const { buffer, boundary } = require('../data')
+
+function process () {
+ const busboy = new Busboy({
+ headers: {
+ 'content-type': 'multipart/form-data; boundary=' + boundary
+ }
+ })
+
+ let processedData = ''
+
+ return new Promise((resolve, reject) => {
+ busboy.on('file', (field, file, filename, encoding, mimetype) => {
+ // console.log('read file')
+ file.on('data', (data) => {
+ processedData += data.toString()
+ // console.log(`File [${filename}] got ${data.length} bytes`);
+ })
+ file.on('end', (fieldname) => {
+ // console.log(`File [${fieldname}] Finished`);
+ })
+ })
+
+ busboy.on('error', function (err) {
+ reject(err)
+ })
+ busboy.on('finish', function () {
+ resolve(processedData)
+ })
+ busboy.write(buffer, () => { })
+
+ busboy.end()
+ })
+}
+
+module.exports = {
+ process
+}
diff --git a/fastify-busboy/benchmarks/busboy/data.js b/fastify-busboy/benchmarks/busboy/data.js
new file mode 100644
index 0000000..4fdefae
--- /dev/null
+++ b/fastify-busboy/benchmarks/busboy/data.js
@@ -0,0 +1,34 @@
+'use strict'
+
+const boundary = '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k'
+const randomContent = Buffer.from(makeString(1024 * 500), 'utf8')
+const buffer = createMultipartBuffer(boundary)
+
+function makeString (length) {
+ let result = ''
+ const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
+ const charactersLength = characters.length
+ for (var i = 0; i < length; i++) { // eslint-disable-line no-var
+ result += characters.charAt(Math.floor(Math.random() *
+ charactersLength))
+ }
+ return result
+}
+
+function createMultipartBuffer (boundary) {
+ const payload = [
+ '--' + boundary,
+ 'Content-Disposition: form-data; name="upload_file_0"; filename="1k_a.dat"',
+ 'Content-Type: application/octet-stream',
+ '',
+ randomContent,
+ '--' + boundary + '--'
+ ].join('\r\n')
+ return Buffer.from(payload, 'ascii')
+}
+
+module.exports = {
+ boundary,
+ buffer,
+ randomContent
+}
diff --git a/fastify-busboy/benchmarks/busboy/executioner.js b/fastify-busboy/benchmarks/busboy/executioner.js
new file mode 100644
index 0000000..524912c
--- /dev/null
+++ b/fastify-busboy/benchmarks/busboy/executioner.js
@@ -0,0 +1,50 @@
+'use strict'
+
+const { process: processBusboy } = require('./contestants/busboy')
+const { process: processFastify } = require('./contestants/fastify-busboy')
+const { getCommonBuilder } = require('../common/commonBuilder')
+const { validateAccuracy } = require('./validator')
+const { resolveContestant } = require('../common/contestantResolver')
+const { outputResults } = require('../common/resultUtils')
+
+const contestants = {
+ busboy: measureBusboy,
+ fastify: measureFastify
+}
+
+async function measureBusboy () {
+ const benchmark = getCommonBuilder()
+ .benchmarkName('Busboy comparison')
+ .benchmarkEntryName('busboy')
+ .asyncFunctionUnderTest(processBusboy)
+ .build()
+ const benchmarkResults = await benchmark.executeAsync()
+ outputResults(benchmark, benchmarkResults)
+}
+
+async function measureFastify () {
+ const benchmark = getCommonBuilder()
+ .benchmarkName('Busboy comparison')
+ .benchmarkEntryName('fastify-busboy')
+ .asyncFunctionUnderTest(processFastify)
+ .build()
+ const benchmarkResults = await benchmark.executeAsync()
+ outputResults(benchmark, benchmarkResults)
+}
+
+function execute () {
+ return validateAccuracy(processBusboy())
+ .then(() => {
+ return validateAccuracy(processFastify())
+ })
+ .then(() => {
+ const contestant = resolveContestant(contestants)
+ return contestant()
+ }).then(() => {
+ console.log('all done')
+ }).catch((err) => {
+ console.error(`Something went wrong: ${err.message}`)
+ })
+}
+
+execute()
diff --git a/fastify-busboy/benchmarks/busboy/regenerate.cmd b/fastify-busboy/benchmarks/busboy/regenerate.cmd
new file mode 100644
index 0000000..87c0768
--- /dev/null
+++ b/fastify-busboy/benchmarks/busboy/regenerate.cmd
@@ -0,0 +1,17 @@
+rem Make sure to run this in Admin account
+rem
+call npm run install-node
+timeout /t 2
+call nvm use 17.2.0
+timeout /t 2
+call npm run benchmark-all
+call nvm use 16.13.1
+timeout /t 2
+call npm run benchmark-all
+call nvm use 14.18.2
+timeout /t 2
+call npm run benchmark-all
+call nvm use 12.22.7
+timeout /t 2
+call npm run benchmark-all
+call npm run combine-results
diff --git a/fastify-busboy/benchmarks/busboy/validator.js b/fastify-busboy/benchmarks/busboy/validator.js
new file mode 100644
index 0000000..a86cc33
--- /dev/null
+++ b/fastify-busboy/benchmarks/busboy/validator.js
@@ -0,0 +1,15 @@
+'use strict'
+
+const { validateEqual } = require('validation-utils')
+const { randomContent } = require('./data')
+
+const EXPECTED_RESULT = randomContent.toString()
+
+async function validateAccuracy (actualResultPromise) {
+ const result = await actualResultPromise
+ validateEqual(result, EXPECTED_RESULT)
+}
+
+module.exports = {
+ validateAccuracy
+}
diff --git a/fastify-busboy/benchmarks/common/commonBuilder.js b/fastify-busboy/benchmarks/common/commonBuilder.js
new file mode 100644
index 0000000..b5707aa
--- /dev/null
+++ b/fastify-busboy/benchmarks/common/commonBuilder.js
@@ -0,0 +1,46 @@
+'use strict'
+
+const { validateNotNil } = require('validation-utils')
+const { BenchmarkBuilder } = require('photofinish')
+const getopts = require('getopts')
+
+const options = getopts(process.argv.slice(1), {
+ alias: {
+ preset: 'p'
+ },
+ default: {}
+})
+
+const PRESET = {
+ LOW: (builder) => {
+ return builder
+ .warmupCycles(1000)
+ .benchmarkCycles(1000)
+ },
+
+ MEDIUM: (builder) => {
+ return builder
+ .warmupCycles(1000)
+ .benchmarkCycles(2000)
+ },
+
+ HIGH: (builder) => {
+ return builder
+ .warmupCycles(1000)
+ .benchmarkCycles(10000)
+ }
+}
+
+function getCommonBuilder () {
+ const presetId = options.preset || 'MEDIUM'
+ const preset = validateNotNil(PRESET[presetId.toUpperCase()], `Unknown preset: ${presetId}`)
+
+ const builder = new BenchmarkBuilder()
+ preset(builder)
+ return builder
+ .benchmarkCycleSamples(50)
+}
+
+module.exports = {
+ getCommonBuilder
+}
diff --git a/fastify-busboy/benchmarks/common/contestantResolver.js b/fastify-busboy/benchmarks/common/contestantResolver.js
new file mode 100644
index 0000000..7cfc90e
--- /dev/null
+++ b/fastify-busboy/benchmarks/common/contestantResolver.js
@@ -0,0 +1,26 @@
+'use strict'
+
+const getopts = require('getopts')
+
+const options = getopts(process.argv.slice(1), {
+ alias: {
+ contestant: 'c'
+ },
+ default: {}
+})
+
+function resolveContestant (contestants) {
+ const contestantId = options.contestant
+ const contestant = Number.isFinite(contestantId)
+ ? Object.values(contestants)[contestantId]
+ : contestants[contestantId]
+
+ if (!contestant) {
+ throw new Error(`Unknown contestant ${contestantId}`)
+ }
+ return contestant
+}
+
+module.exports = {
+ resolveContestant
+}
diff --git a/fastify-busboy/benchmarks/common/executionUtils.js b/fastify-busboy/benchmarks/common/executionUtils.js
new file mode 100644
index 0000000..8c52ec8
--- /dev/null
+++ b/fastify-busboy/benchmarks/common/executionUtils.js
@@ -0,0 +1,18 @@
+'use strict'
+
+const { getCommonBuilder } = require('./commonBuilder')
+const { outputResults } = require('./resultUtils')
+
+function getMeasureFn (constestandId, fn) {
+ return () => {
+ const benchmark = getCommonBuilder()
+ .benchmarkEntryName(constestandId)
+ .functionUnderTest(fn).build()
+ const benchmarkResults = benchmark.execute()
+ outputResults(benchmark, benchmarkResults)
+ }
+}
+
+module.exports = {
+ getMeasureFn
+}
diff --git a/fastify-busboy/benchmarks/common/resultUtils.js b/fastify-busboy/benchmarks/common/resultUtils.js
new file mode 100644
index 0000000..ec7bce7
--- /dev/null
+++ b/fastify-busboy/benchmarks/common/resultUtils.js
@@ -0,0 +1,17 @@
+'use strict'
+
+const { exportResults } = require('photofinish')
+
+function outputResults (benchmark, benchmarkResults) {
+ console.log(
+ `Mean time for ${
+ benchmark.benchmarkEntryName
+ } is ${benchmarkResults.meanTime.getTimeInNanoSeconds()} nanoseconds`
+ )
+
+ exportResults(benchmarkResults, { exportPath: '_results' })
+}
+
+module.exports = {
+ outputResults
+}
diff --git a/fastify-busboy/benchmarks/common/resultsCombinator.js b/fastify-busboy/benchmarks/common/resultsCombinator.js
new file mode 100644
index 0000000..253211b
--- /dev/null
+++ b/fastify-busboy/benchmarks/common/resultsCombinator.js
@@ -0,0 +1,54 @@
+'use strict'
+
+const fs = require('node:fs')
+const path = require('node:path')
+const getopts = require('getopts')
+const systemInformation = require('systeminformation')
+const { loadResults } = require('photofinish')
+
+const options = getopts(process.argv.slice(1), {
+ alias: {
+ resultsDir: 'r',
+ precision: 'p'
+ },
+ default: {}
+})
+
+const { generateTable } = require('photofinish')
+
+async function getSpecs () {
+ const cpuInfo = await systemInformation.cpu()
+
+ return {
+ cpu: {
+ brand: cpuInfo.brand,
+ speed: `${cpuInfo.speed} GHz`
+ }
+ }
+}
+
+async function saveTable () {
+ const baseResultsDir = options.resultsDir
+ const benchmarkResults = await loadResults(baseResultsDir)
+
+ const table = generateTable(benchmarkResults, {
+ precision: options.precision,
+ sortBy: [
+ { field: 'meanTimeNs', order: 'asc' }
+ ]
+ })
+
+ const specs = await getSpecs()
+
+ console.log(specs)
+ console.log(table)
+
+ const targetFilePath = path.resolve(baseResultsDir, 'results.md')
+ fs.writeFileSync(
+ targetFilePath,
+ `${table}` +
+ `\n\n**Specs**: ${specs.cpu.brand} (${specs.cpu.speed})`
+ )
+}
+
+saveTable()
diff --git a/fastify-busboy/benchmarks/package.json b/fastify-busboy/benchmarks/package.json
new file mode 100644
index 0000000..2574b8b
--- /dev/null
+++ b/fastify-busboy/benchmarks/package.json
@@ -0,0 +1,21 @@
+{
+ "name": "busboy-benchmarks",
+ "version": "1.0.0",
+ "license": "MIT",
+ "dependencies": {
+ "getopts": "^2.3.0",
+ "photofinish": "^1.8.0",
+ "systeminformation": "^5.9.15",
+ "tslib": "^2.3.1",
+ "validation-utils": "^7.0.0"
+ },
+ "scripts": {
+ "install-node": "nvm install 17.2.0 && nvm install 16.13.1 && nvm install 14.18.2 && nvm install 12.22.7",
+ "benchmark-busboy": "node busboy/executioner.js -c 0",
+ "benchmark-fastify": "node busboy/executioner.js -c 1",
+ "benchmark-all": "npm run benchmark-busboy -- -p high && npm run benchmark-fastify -- -p high",
+ "benchmark-all-medium": "npm run benchmark-busboy -- -p medium && npm run benchmark-fastify -- -p medium",
+ "benchmark-all-low": "npm run benchmark-busboy -- -p low && npm run benchmark-fastify -- -p low",
+ "combine-results": "node common/resultsCombinator.js -r _results -p 6"
+ }
+}
diff --git a/fastify-busboy/deps/dicer/LICENSE b/fastify-busboy/deps/dicer/LICENSE
new file mode 100644
index 0000000..290762e
--- /dev/null
+++ b/fastify-busboy/deps/dicer/LICENSE
@@ -0,0 +1,19 @@
+Copyright Brian White. All rights reserved.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to
+deal in the Software without restriction, including without limitation the
+rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+sell copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+IN THE SOFTWARE. \ No newline at end of file
diff --git a/fastify-busboy/deps/dicer/lib/Dicer.js b/fastify-busboy/deps/dicer/lib/Dicer.js
new file mode 100644
index 0000000..79da160
--- /dev/null
+++ b/fastify-busboy/deps/dicer/lib/Dicer.js
@@ -0,0 +1,207 @@
+'use strict'
+
+const WritableStream = require('node:stream').Writable
+const inherits = require('node:util').inherits
+
+const StreamSearch = require('../../streamsearch/sbmh')
+
+const PartStream = require('./PartStream')
+const HeaderParser = require('./HeaderParser')
+
+const DASH = 45
+const B_ONEDASH = Buffer.from('-')
+const B_CRLF = Buffer.from('\r\n')
+const EMPTY_FN = function () {}
+
+function Dicer (cfg) {
+ if (!(this instanceof Dicer)) { return new Dicer(cfg) }
+ WritableStream.call(this, cfg)
+
+ if (!cfg || (!cfg.headerFirst && typeof cfg.boundary !== 'string')) { throw new TypeError('Boundary required') }
+
+ if (typeof cfg.boundary === 'string') { this.setBoundary(cfg.boundary) } else { this._bparser = undefined }
+
+ this._headerFirst = cfg.headerFirst
+
+ this._dashes = 0
+ this._parts = 0
+ this._finished = false
+ this._realFinish = false
+ this._isPreamble = true
+ this._justMatched = false
+ this._firstWrite = true
+ this._inHeader = true
+ this._part = undefined
+ this._cb = undefined
+ this._ignoreData = false
+ this._partOpts = { highWaterMark: cfg.partHwm }
+ this._pause = false
+
+ const self = this
+ this._hparser = new HeaderParser(cfg)
+ this._hparser.on('header', function (header) {
+ self._inHeader = false
+ self._part.emit('header', header)
+ })
+}
+inherits(Dicer, WritableStream)
+
+Dicer.prototype.emit = function (ev) {
+ if (ev === 'finish' && !this._realFinish) {
+ if (!this._finished) {
+ const self = this
+ process.nextTick(function () {
+ self.emit('error', new Error('Unexpected end of multipart data'))
+ if (self._part && !self._ignoreData) {
+ const type = (self._isPreamble ? 'Preamble' : 'Part')
+ self._part.emit('error', new Error(type + ' terminated early due to unexpected end of multipart data'))
+ self._part.push(null)
+ process.nextTick(function () {
+ self._realFinish = true
+ self.emit('finish')
+ self._realFinish = false
+ })
+ return
+ }
+ self._realFinish = true
+ self.emit('finish')
+ self._realFinish = false
+ })
+ }
+ } else { WritableStream.prototype.emit.apply(this, arguments) }
+}
+
+Dicer.prototype._write = function (data, encoding, cb) {
+ // ignore unexpected data (e.g. extra trailer data after finished)
+ if (!this._hparser && !this._bparser) { return cb() }
+
+ if (this._headerFirst && this._isPreamble) {
+ if (!this._part) {
+ this._part = new PartStream(this._partOpts)
+ if (this._events.preamble) { this.emit('preamble', this._part) } else { this._ignore() }
+ }
+ const r = this._hparser.push(data)
+ if (!this._inHeader && r !== undefined && r < data.length) { data = data.slice(r) } else { return cb() }
+ }
+
+ // allows for "easier" testing
+ if (this._firstWrite) {
+ this._bparser.push(B_CRLF)
+ this._firstWrite = false
+ }
+
+ this._bparser.push(data)
+
+ if (this._pause) { this._cb = cb } else { cb() }
+}
+
+Dicer.prototype.reset = function () {
+ this._part = undefined
+ this._bparser = undefined
+ this._hparser = undefined
+}
+
+Dicer.prototype.setBoundary = function (boundary) {
+ const self = this
+ this._bparser = new StreamSearch('\r\n--' + boundary)
+ this._bparser.on('info', function (isMatch, data, start, end) {
+ self._oninfo(isMatch, data, start, end)
+ })
+}
+
+Dicer.prototype._ignore = function () {
+ if (this._part && !this._ignoreData) {
+ this._ignoreData = true
+ this._part.on('error', EMPTY_FN)
+ // we must perform some kind of read on the stream even though we are
+ // ignoring the data, otherwise node's Readable stream will not emit 'end'
+ // after pushing null to the stream
+ this._part.resume()
+ }
+}
+
+Dicer.prototype._oninfo = function (isMatch, data, start, end) {
+ let buf; const self = this; let i = 0; let r; let shouldWriteMore = true
+
+ if (!this._part && this._justMatched && data) {
+ while (this._dashes < 2 && (start + i) < end) {
+ if (data[start + i] === DASH) {
+ ++i
+ ++this._dashes
+ } else {
+ if (this._dashes) { buf = B_ONEDASH }
+ this._dashes = 0
+ break
+ }
+ }
+ if (this._dashes === 2) {
+ if ((start + i) < end && this._events.trailer) { this.emit('trailer', data.slice(start + i, end)) }
+ this.reset()
+ this._finished = true
+ // no more parts will be added
+ if (self._parts === 0) {
+ self._realFinish = true
+ self.emit('finish')
+ self._realFinish = false
+ }
+ }
+ if (this._dashes) { return }
+ }
+ if (this._justMatched) { this._justMatched = false }
+ if (!this._part) {
+ this._part = new PartStream(this._partOpts)
+ this._part._read = function (n) {
+ self._unpause()
+ }
+ if (this._isPreamble && this._events.preamble) { this.emit('preamble', this._part) } else if (this._isPreamble !== true && this._events.part) { this.emit('part', this._part) } else { this._ignore() }
+ if (!this._isPreamble) { this._inHeader = true }
+ }
+ if (data && start < end && !this._ignoreData) {
+ if (this._isPreamble || !this._inHeader) {
+ if (buf) { shouldWriteMore = this._part.push(buf) }
+ shouldWriteMore = this._part.push(data.slice(start, end))
+ if (!shouldWriteMore) { this._pause = true }
+ } else if (!this._isPreamble && this._inHeader) {
+ if (buf) { this._hparser.push(buf) }
+ r = this._hparser.push(data.slice(start, end))
+ if (!this._inHeader && r !== undefined && r < end) { this._oninfo(false, data, start + r, end) }
+ }
+ }
+ if (isMatch) {
+ this._hparser.reset()
+ if (this._isPreamble) { this._isPreamble = false } else {
+ if (start !== end) {
+ ++this._parts
+ this._part.on('end', function () {
+ if (--self._parts === 0) {
+ if (self._finished) {
+ self._realFinish = true
+ self.emit('finish')
+ self._realFinish = false
+ } else {
+ self._unpause()
+ }
+ }
+ })
+ }
+ }
+ this._part.push(null)
+ this._part = undefined
+ this._ignoreData = false
+ this._justMatched = true
+ this._dashes = 0
+ }
+}
+
+Dicer.prototype._unpause = function () {
+ if (!this._pause) { return }
+
+ this._pause = false
+ if (this._cb) {
+ const cb = this._cb
+ this._cb = undefined
+ cb()
+ }
+}
+
+module.exports = Dicer
diff --git a/fastify-busboy/deps/dicer/lib/HeaderParser.js b/fastify-busboy/deps/dicer/lib/HeaderParser.js
new file mode 100644
index 0000000..65f667b
--- /dev/null
+++ b/fastify-busboy/deps/dicer/lib/HeaderParser.js
@@ -0,0 +1,100 @@
+'use strict'
+
+const EventEmitter = require('node:events').EventEmitter
+const inherits = require('node:util').inherits
+const getLimit = require('../../../lib/utils/getLimit')
+
+const StreamSearch = require('../../streamsearch/sbmh')
+
+const B_DCRLF = Buffer.from('\r\n\r\n')
+const RE_CRLF = /\r\n/g
+const RE_HDR = /^([^:]+):[ \t]?([\x00-\xFF]+)?$/ // eslint-disable-line no-control-regex
+
+function HeaderParser (cfg) {
+ EventEmitter.call(this)
+
+ cfg = cfg || {}
+ const self = this
+ this.nread = 0
+ this.maxed = false
+ this.npairs = 0
+ this.maxHeaderPairs = getLimit(cfg, 'maxHeaderPairs', 2000)
+ this.maxHeaderSize = getLimit(cfg, 'maxHeaderSize', 80 * 1024)
+ this.buffer = ''
+ this.header = {}
+ this.finished = false
+ this.ss = new StreamSearch(B_DCRLF)
+ this.ss.on('info', function (isMatch, data, start, end) {
+ if (data && !self.maxed) {
+ if (self.nread + end - start >= self.maxHeaderSize) {
+ end = self.maxHeaderSize - self.nread + start
+ self.nread = self.maxHeaderSize
+ self.maxed = true
+ } else { self.nread += (end - start) }
+
+ self.buffer += data.toString('binary', start, end)
+ }
+ if (isMatch) { self._finish() }
+ })
+}
+inherits(HeaderParser, EventEmitter)
+
+HeaderParser.prototype.push = function (data) {
+ const r = this.ss.push(data)
+ if (this.finished) { return r }
+}
+
+HeaderParser.prototype.reset = function () {
+ this.finished = false
+ this.buffer = ''
+ this.header = {}
+ this.ss.reset()
+}
+
+HeaderParser.prototype._finish = function () {
+ if (this.buffer) { this._parseHeader() }
+ this.ss.matches = this.ss.maxMatches
+ const header = this.header
+ this.header = {}
+ this.buffer = ''
+ this.finished = true
+ this.nread = this.npairs = 0
+ this.maxed = false
+ this.emit('header', header)
+}
+
+HeaderParser.prototype._parseHeader = function () {
+ if (this.npairs === this.maxHeaderPairs) { return }
+
+ const lines = this.buffer.split(RE_CRLF)
+ const len = lines.length
+ let m, h
+
+ for (var i = 0; i < len; ++i) { // eslint-disable-line no-var
+ if (lines[i].length === 0) { continue }
+ if (lines[i][0] === '\t' || lines[i][0] === ' ') {
+ // folded header content
+ // RFC2822 says to just remove the CRLF and not the whitespace following
+ // it, so we follow the RFC and include the leading whitespace ...
+ if (h) {
+ this.header[h][this.header[h].length - 1] += lines[i]
+ continue
+ }
+ }
+
+ const posColon = lines[i].indexOf(':')
+ if (
+ posColon === -1 ||
+ posColon === 0
+ ) {
+ return
+ }
+ m = RE_HDR.exec(lines[i])
+ h = m[1].toLowerCase()
+ this.header[h] = this.header[h] || []
+ this.header[h].push((m[2] || ''))
+ if (++this.npairs === this.maxHeaderPairs) { break }
+ }
+}
+
+module.exports = HeaderParser
diff --git a/fastify-busboy/deps/dicer/lib/PartStream.js b/fastify-busboy/deps/dicer/lib/PartStream.js
new file mode 100644
index 0000000..c91da1c
--- /dev/null
+++ b/fastify-busboy/deps/dicer/lib/PartStream.js
@@ -0,0 +1,13 @@
+'use strict'
+
+const inherits = require('node:util').inherits
+const ReadableStream = require('node:stream').Readable
+
+function PartStream (opts) {
+ ReadableStream.call(this, opts)
+}
+inherits(PartStream, ReadableStream)
+
+PartStream.prototype._read = function (n) {}
+
+module.exports = PartStream
diff --git a/fastify-busboy/deps/dicer/lib/dicer.d.ts b/fastify-busboy/deps/dicer/lib/dicer.d.ts
new file mode 100644
index 0000000..3c5b896
--- /dev/null
+++ b/fastify-busboy/deps/dicer/lib/dicer.d.ts
@@ -0,0 +1,164 @@
+// Type definitions for dicer 0.2
+// Project: https://github.com/mscdex/dicer
+// Definitions by: BendingBender <https://github.com/BendingBender>
+// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
+// TypeScript Version: 2.2
+/// <reference types="node" />
+
+import stream = require("stream");
+
+// tslint:disable:unified-signatures
+
+/**
+ * A very fast streaming multipart parser for node.js.
+ * Dicer is a WritableStream
+ *
+ * Dicer (special) events:
+ * - on('finish', ()) - Emitted when all parts have been parsed and the Dicer instance has been ended.
+ * - on('part', (stream: PartStream)) - Emitted when a new part has been found.
+ * - on('preamble', (stream: PartStream)) - Emitted for preamble if you should happen to need it (can usually be ignored).
+ * - on('trailer', (data: Buffer)) - Emitted when trailing data was found after the terminating boundary (as with the preamble, this can usually be ignored too).
+ */
+export class Dicer extends stream.Writable {
+ /**
+ * Creates and returns a new Dicer instance with the following valid config settings:
+ *
+ * @param config The configuration to use
+ */
+ constructor(config: Dicer.Config);
+ /**
+ * Sets the boundary to use for parsing and performs some initialization needed for parsing.
+ * You should only need to use this if you set headerFirst to true in the constructor and are parsing the boundary from the preamble header.
+ *
+ * @param boundary The boundary to use
+ */
+ setBoundary(boundary: string): void;
+ addListener(event: "finish", listener: () => void): this;
+ addListener(event: "part", listener: (stream: Dicer.PartStream) => void): this;
+ addListener(event: "preamble", listener: (stream: Dicer.PartStream) => void): this;
+ addListener(event: "trailer", listener: (data: Buffer) => void): this;
+ addListener(event: "close", listener: () => void): this;
+ addListener(event: "drain", listener: () => void): this;
+ addListener(event: "error", listener: (err: Error) => void): this;
+ addListener(event: "pipe", listener: (src: stream.Readable) => void): this;
+ addListener(event: "unpipe", listener: (src: stream.Readable) => void): this;
+ addListener(event: string, listener: (...args: any[]) => void): this;
+ on(event: "finish", listener: () => void): this;
+ on(event: "part", listener: (stream: Dicer.PartStream) => void): this;
+ on(event: "preamble", listener: (stream: Dicer.PartStream) => void): this;
+ on(event: "trailer", listener: (data: Buffer) => void): this;
+ on(event: "close", listener: () => void): this;
+ on(event: "drain", listener: () => void): this;
+ on(event: "error", listener: (err: Error) => void): this;
+ on(event: "pipe", listener: (src: stream.Readable) => void): this;
+ on(event: "unpipe", listener: (src: stream.Readable) => void): this;
+ on(event: string, listener: (...args: any[]) => void): this;
+ once(event: "finish", listener: () => void): this;
+ once(event: "part", listener: (stream: Dicer.PartStream) => void): this;
+ once(event: "preamble", listener: (stream: Dicer.PartStream) => void): this;
+ once(event: "trailer", listener: (data: Buffer) => void): this;
+ once(event: "close", listener: () => void): this;
+ once(event: "drain", listener: () => void): this;
+ once(event: "error", listener: (err: Error) => void): this;
+ once(event: "pipe", listener: (src: stream.Readable) => void): this;
+ once(event: "unpipe", listener: (src: stream.Readable) => void): this;
+ once(event: string, listener: (...args: any[]) => void): this;
+ prependListener(event: "finish", listener: () => void): this;
+ prependListener(event: "part", listener: (stream: Dicer.PartStream) => void): this;
+ prependListener(event: "preamble", listener: (stream: Dicer.PartStream) => void): this;
+ prependListener(event: "trailer", listener: (data: Buffer) => void): this;
+ prependListener(event: "close", listener: () => void): this;
+ prependListener(event: "drain", listener: () => void): this;
+ prependListener(event: "error", listener: (err: Error) => void): this;
+ prependListener(event: "pipe", listener: (src: stream.Readable) => void): this;
+ prependListener(event: "unpipe", listener: (src: stream.Readable) => void): this;
+ prependListener(event: string, listener: (...args: any[]) => void): this;
+ prependOnceListener(event: "finish", listener: () => void): this;
+ prependOnceListener(event: "part", listener: (stream: Dicer.PartStream) => void): this;
+ prependOnceListener(event: "preamble", listener: (stream: Dicer.PartStream) => void): this;
+ prependOnceListener(event: "trailer", listener: (data: Buffer) => void): this;
+ prependOnceListener(event: "close", listener: () => void): this;
+ prependOnceListener(event: "drain", listener: () => void): this;
+ prependOnceListener(event: "error", listener: (err: Error) => void): this;
+ prependOnceListener(event: "pipe", listener: (src: stream.Readable) => void): this;
+ prependOnceListener(event: "unpipe", listener: (src: stream.Readable) => void): this;
+ prependOnceListener(event: string, listener: (...args: any[]) => void): this;
+ removeListener(event: "finish", listener: () => void): this;
+ removeListener(event: "part", listener: (stream: Dicer.PartStream) => void): this;
+ removeListener(event: "preamble", listener: (stream: Dicer.PartStream) => void): this;
+ removeListener(event: "trailer", listener: (data: Buffer) => void): this;
+ removeListener(event: "close", listener: () => void): this;
+ removeListener(event: "drain", listener: () => void): this;
+ removeListener(event: "error", listener: (err: Error) => void): this;
+ removeListener(event: "pipe", listener: (src: stream.Readable) => void): this;
+ removeListener(event: "unpipe", listener: (src: stream.Readable) => void): this;
+ removeListener(event: string, listener: (...args: any[]) => void): this;
+}
+
+declare namespace Dicer {
+ interface Config {
+ /**
+ * This is the boundary used to detect the beginning of a new part.
+ */
+ boundary?: string | undefined;
+ /**
+ * If true, preamble header parsing will be performed first.
+ */
+ headerFirst?: boolean | undefined;
+ /**
+ * The maximum number of header key=>value pairs to parse Default: 2000 (same as node's http).
+ */
+ maxHeaderPairs?: number | undefined;
+ }
+
+ /**
+ * PartStream is a _ReadableStream_
+ *
+ * PartStream (special) events:
+ * - on('header', (header: object)) - An object containing the header for this particular part. Each property value is an array of one or more string values.
+ */
+ interface PartStream extends stream.Readable {
+ addListener(event: "header", listener: (header: object) => void): this;
+ addListener(event: "close", listener: () => void): this;
+ addListener(event: "data", listener: (chunk: Buffer | string) => void): this;
+ addListener(event: "end", listener: () => void): this;
+ addListener(event: "readable", listener: () => void): this;
+ addListener(event: "error", listener: (err: Error) => void): this;
+ addListener(event: string, listener: (...args: any[]) => void): this;
+ on(event: "header", listener: (header: object) => void): this;
+ on(event: "close", listener: () => void): this;
+ on(event: "data", listener: (chunk: Buffer | string) => void): this;
+ on(event: "end", listener: () => void): this;
+ on(event: "readable", listener: () => void): this;
+ on(event: "error", listener: (err: Error) => void): this;
+ on(event: string, listener: (...args: any[]) => void): this;
+ once(event: "header", listener: (header: object) => void): this;
+ once(event: "close", listener: () => void): this;
+ once(event: "data", listener: (chunk: Buffer | string) => void): this;
+ once(event: "end", listener: () => void): this;
+ once(event: "readable", listener: () => void): this;
+ once(event: "error", listener: (err: Error) => void): this;
+ once(event: string, listener: (...args: any[]) => void): this;
+ prependListener(event: "header", listener: (header: object) => void): this;
+ prependListener(event: "close", listener: () => void): this;
+ prependListener(event: "data", listener: (chunk: Buffer | string) => void): this;
+ prependListener(event: "end", listener: () => void): this;
+ prependListener(event: "readable", listener: () => void): this;
+ prependListener(event: "error", listener: (err: Error) => void): this;
+ prependListener(event: string, listener: (...args: any[]) => void): this;
+ prependOnceListener(event: "header", listener: (header: object) => void): this;
+ prependOnceListener(event: "close", listener: () => void): this;
+ prependOnceListener(event: "data", listener: (chunk: Buffer | string) => void): this;
+ prependOnceListener(event: "end", listener: () => void): this;
+ prependOnceListener(event: "readable", listener: () => void): this;
+ prependOnceListener(event: "error", listener: (err: Error) => void): this;
+ prependOnceListener(event: string, listener: (...args: any[]) => void): this;
+ removeListener(event: "header", listener: (header: object) => void): this;
+ removeListener(event: "close", listener: () => void): this;
+ removeListener(event: "data", listener: (chunk: Buffer | string) => void): this;
+ removeListener(event: "end", listener: () => void): this;
+ removeListener(event: "readable", listener: () => void): this;
+ removeListener(event: "error", listener: (err: Error) => void): this;
+ removeListener(event: string, listener: (...args: any[]) => void): this;
+ }
+} \ No newline at end of file
diff --git a/fastify-busboy/deps/streamsearch/sbmh.js b/fastify-busboy/deps/streamsearch/sbmh.js
new file mode 100644
index 0000000..b90c0e8
--- /dev/null
+++ b/fastify-busboy/deps/streamsearch/sbmh.js
@@ -0,0 +1,228 @@
+'use strict'
+
+/**
+ * Copyright Brian White. All rights reserved.
+ *
+ * @see https://github.com/mscdex/streamsearch
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ *
+ * Based heavily on the Streaming Boyer-Moore-Horspool C++ implementation
+ * by Hongli Lai at: https://github.com/FooBarWidget/boyer-moore-horspool
+ */
+const EventEmitter = require('node:events').EventEmitter
+const inherits = require('node:util').inherits
+
+function SBMH (needle) {
+ if (typeof needle === 'string') {
+ needle = Buffer.from(needle)
+ }
+
+ if (!Buffer.isBuffer(needle)) {
+ throw new TypeError('The needle has to be a String or a Buffer.')
+ }
+
+ const needleLength = needle.length
+
+ if (needleLength === 0) {
+ throw new Error('The needle cannot be an empty String/Buffer.')
+ }
+
+ if (needleLength > 256) {
+ throw new Error('The needle cannot have a length bigger than 256.')
+ }
+
+ this.maxMatches = Infinity
+ this.matches = 0
+
+ this._occ = new Array(256)
+ .fill(needleLength) // Initialize occurrence table.
+ this._lookbehind_size = 0
+ this._needle = needle
+ this._bufpos = 0
+
+ this._lookbehind = Buffer.alloc(needleLength)
+
+ // Populate occurrence table with analysis of the needle,
+ // ignoring last letter.
+ for (var i = 0; i < needleLength - 1; ++i) { // eslint-disable-line no-var
+ this._occ[needle[i]] = needleLength - 1 - i
+ }
+}
+inherits(SBMH, EventEmitter)
+
+SBMH.prototype.reset = function () {
+ this._lookbehind_size = 0
+ this.matches = 0
+ this._bufpos = 0
+}
+
+SBMH.prototype.push = function (chunk, pos) {
+ if (!Buffer.isBuffer(chunk)) {
+ chunk = Buffer.from(chunk, 'binary')
+ }
+ const chlen = chunk.length
+ this._bufpos = pos || 0
+ let r
+ while (r !== chlen && this.matches < this.maxMatches) { r = this._sbmh_feed(chunk) }
+ return r
+}
+
+SBMH.prototype._sbmh_feed = function (data) {
+ const len = data.length
+ const needle = this._needle
+ const needleLength = needle.length
+ const lastNeedleChar = needle[needleLength - 1]
+
+ // Positive: points to a position in `data`
+ // pos == 3 points to data[3]
+ // Negative: points to a position in the lookbehind buffer
+ // pos == -2 points to lookbehind[lookbehind_size - 2]
+ let pos = -this._lookbehind_size
+ let ch
+
+ if (pos < 0) {
+ // Lookbehind buffer is not empty. Perform Boyer-Moore-Horspool
+ // search with character lookup code that considers both the
+ // lookbehind buffer and the current round's haystack data.
+ //
+ // Loop until
+ // there is a match.
+ // or until
+ // we've moved past the position that requires the
+ // lookbehind buffer. In this case we switch to the
+ // optimized loop.
+ // or until
+ // the character to look at lies outside the haystack.
+ while (pos < 0 && pos <= len - needleLength) {
+ ch = this._sbmh_lookup_char(data, pos + needleLength - 1)
+
+ if (
+ ch === lastNeedleChar &&
+ this._sbmh_memcmp(data, pos, needleLength - 1)
+ ) {
+ this._lookbehind_size = 0
+ ++this.matches
+ this.emit('info', true)
+
+ return (this._bufpos = pos + needleLength)
+ }
+ pos += this._occ[ch]
+ }
+
+ // No match.
+
+ if (pos < 0) {
+ // There's too few data for Boyer-Moore-Horspool to run,
+ // so let's use a different algorithm to skip as much as
+ // we can.
+ // Forward pos until
+ // the trailing part of lookbehind + data
+ // looks like the beginning of the needle
+ // or until
+ // pos == 0
+ while (pos < 0 && !this._sbmh_memcmp(data, pos, len - pos)) { ++pos }
+ }
+
+ if (pos >= 0) {
+ // Discard lookbehind buffer.
+ this.emit('info', false, this._lookbehind, 0, this._lookbehind_size)
+ this._lookbehind_size = 0
+ } else {
+ // Cut off part of the lookbehind buffer that has
+ // been processed and append the entire haystack
+ // into it.
+ const bytesToCutOff = this._lookbehind_size + pos
+ if (bytesToCutOff > 0) {
+ // The cut off data is guaranteed not to contain the needle.
+ this.emit('info', false, this._lookbehind, 0, bytesToCutOff)
+ }
+
+ this._lookbehind.copy(this._lookbehind, 0, bytesToCutOff,
+ this._lookbehind_size - bytesToCutOff)
+ this._lookbehind_size -= bytesToCutOff
+
+ data.copy(this._lookbehind, this._lookbehind_size)
+ this._lookbehind_size += len
+
+ this._bufpos = len
+ return len
+ }
+ }
+
+ pos += (pos >= 0) * this._bufpos
+
+ // Lookbehind buffer is now empty. We only need to check if the
+ // needle is in the haystack.
+ if (data.indexOf(needle, pos) !== -1) {
+ pos = data.indexOf(needle, pos)
+ ++this.matches
+ if (pos > 0) { this.emit('info', true, data, this._bufpos, pos) } else { this.emit('info', true) }
+
+ return (this._bufpos = pos + needleLength)
+ } else {
+ pos = len - needleLength
+ }
+
+ // There was no match. If there's trailing haystack data that we cannot
+ // match yet using the Boyer-Moore-Horspool algorithm (because the trailing
+ // data is less than the needle size) then match using a modified
+ // algorithm that starts matching from the beginning instead of the end.
+ // Whatever trailing data is left after running this algorithm is added to
+ // the lookbehind buffer.
+ while (
+ pos < len &&
+ (
+ data[pos] !== needle[0] ||
+ (
+ (Buffer.compare(
+ data.subarray(pos, pos + len - pos),
+ needle.subarray(0, len - pos)
+ ) !== 0)
+ )
+ )
+ ) {
+ ++pos
+ }
+ if (pos < len) {
+ data.copy(this._lookbehind, 0, pos, pos + (len - pos))
+ this._lookbehind_size = len - pos
+ }
+
+ // Everything until pos is guaranteed not to contain needle data.
+ if (pos > 0) { this.emit('info', false, data, this._bufpos, pos < len ? pos : len) }
+
+ this._bufpos = len
+ return len
+}
+
+SBMH.prototype._sbmh_lookup_char = function (data, pos) {
+ return (pos < 0)
+ ? this._lookbehind[this._lookbehind_size + pos]
+ : data[pos]
+}
+
+SBMH.prototype._sbmh_memcmp = function (data, pos, len) {
+ for (var i = 0; i < len; ++i) { // eslint-disable-line no-var
+ if (this._sbmh_lookup_char(data, pos + i) !== this._needle[i]) { return false }
+ }
+ return true
+}
+
+module.exports = SBMH
diff --git a/fastify-busboy/lib/main.d.ts b/fastify-busboy/lib/main.d.ts
new file mode 100644
index 0000000..91b6448
--- /dev/null
+++ b/fastify-busboy/lib/main.d.ts
@@ -0,0 +1,196 @@
+// Definitions by: Jacob Baskin <https://github.com/jacobbaskin>
+// BendingBender <https://github.com/BendingBender>
+// Igor Savin <https://github.com/kibertoad>
+
+/// <reference types="node" />
+
+import * as http from 'http';
+import { Readable, Writable } from 'stream';
+export { Dicer } from "../deps/dicer/lib/dicer";
+
+export const Busboy: BusboyConstructor;
+export default Busboy;
+
+export interface BusboyConfig {
+ /**
+ * These are the HTTP headers of the incoming request, which are used by individual parsers.
+ */
+ headers: BusboyHeaders;
+ /**
+ * `highWaterMark` to use for this Busboy instance.
+ * @default WritableStream default.
+ */
+ highWaterMark?: number | undefined;
+ /**
+ * highWaterMark to use for file streams.
+ * @default ReadableStream default.
+ */
+ fileHwm?: number | undefined;
+ /**
+ * Default character set to use when one isn't defined.
+ * @default 'utf8'
+ */
+ defCharset?: string | undefined;
+ /**
+ * Detect if a Part is a file.
+ *
+ * By default a file is detected if contentType
+ * is application/octet-stream or fileName is not
+ * undefined.
+ *
+ * Modify this to handle e.g. Blobs.
+ */
+ isPartAFile?: (fieldName: string | undefined, contentType: string | undefined, fileName: string | undefined) => boolean;
+ /**
+ * If paths in the multipart 'filename' field shall be preserved.
+ * @default false
+ */
+ preservePath?: boolean | undefined;
+ /**
+ * Various limits on incoming data.
+ */
+ limits?:
+ | {
+ /**
+ * Max field name size (in bytes)
+ * @default 100 bytes
+ */
+ fieldNameSize?: number | undefined;
+ /**
+ * Max field value size (in bytes)
+ * @default 1MB
+ */
+ fieldSize?: number | undefined;
+ /**
+ * Max number of non-file fields
+ * @default Infinity
+ */
+ fields?: number | undefined;
+ /**
+ * For multipart forms, the max file size (in bytes)
+ * @default Infinity
+ */
+ fileSize?: number | undefined;
+ /**
+ * For multipart forms, the max number of file fields
+ * @default Infinity
+ */
+ files?: number | undefined;
+ /**
+ * For multipart forms, the max number of parts (fields + files)
+ * @default Infinity
+ */
+ parts?: number | undefined;
+ /**
+ * For multipart forms, the max number of header key=>value pairs to parse
+ * @default 2000
+ */
+ headerPairs?: number | undefined;
+
+ /**
+ * For multipart forms, the max size of a header part
+ * @default 81920
+ */
+ headerSize?: number | undefined;
+ }
+ | undefined;
+}
+
+export type BusboyHeaders = { 'content-type': string } & http.IncomingHttpHeaders;
+
+export interface BusboyFileStream extends
+ Readable {
+
+ truncated: boolean;
+
+ /**
+ * The number of bytes that have been read so far.
+ */
+ bytesRead: number;
+}
+
+export interface Busboy extends Writable {
+ addListener<Event extends keyof BusboyEvents>(event: Event, listener: BusboyEvents[Event]): this;
+
+ addListener(event: string | symbol, listener: (...args: any[]) => void): this;
+
+ on<Event extends keyof BusboyEvents>(event: Event, listener: BusboyEvents[Event]): this;
+
+ on(event: string | symbol, listener: (...args: any[]) => void): this;
+
+ once<Event extends keyof BusboyEvents>(event: Event, listener: BusboyEvents[Event]): this;
+
+ once(event: string | symbol, listener: (...args: any[]) => void): this;
+
+ removeListener<Event extends keyof BusboyEvents>(event: Event, listener: BusboyEvents[Event]): this;
+
+ removeListener(event: string | symbol, listener: (...args: any[]) => void): this;
+
+ off<Event extends keyof BusboyEvents>(event: Event, listener: BusboyEvents[Event]): this;
+
+ off(event: string | symbol, listener: (...args: any[]) => void): this;
+
+ prependListener<Event extends keyof BusboyEvents>(event: Event, listener: BusboyEvents[Event]): this;
+
+ prependListener(event: string | symbol, listener: (...args: any[]) => void): this;
+
+ prependOnceListener<Event extends keyof BusboyEvents>(event: Event, listener: BusboyEvents[Event]): this;
+
+ prependOnceListener(event: string | symbol, listener: (...args: any[]) => void): this;
+}
+
+export interface BusboyEvents {
+ /**
+ * Emitted for each new file form field found.
+ *
+ * * Note: if you listen for this event, you should always handle the `stream` no matter if you care about the
+ * file contents or not (e.g. you can simply just do `stream.resume();` if you want to discard the contents),
+ * otherwise the 'finish' event will never fire on the Busboy instance. However, if you don't care about **any**
+ * incoming files, you can simply not listen for the 'file' event at all and any/all files will be automatically
+ * and safely discarded (these discarded files do still count towards `files` and `parts` limits).
+ * * If a configured file size limit was reached, `stream` will both have a boolean property `truncated`
+ * (best checked at the end of the stream) and emit a 'limit' event to notify you when this happens.
+ *
+ * @param listener.transferEncoding Contains the 'Content-Transfer-Encoding' value for the file stream.
+ * @param listener.mimeType Contains the 'Content-Type' value for the file stream.
+ */
+ file: (
+ fieldname: string,
+ stream: BusboyFileStream,
+ filename: string,
+ transferEncoding: string,
+ mimeType: string,
+ ) => void;
+ /**
+ * Emitted for each new non-file field found.
+ */
+ field: (
+ fieldname: string,
+ value: string,
+ fieldnameTruncated: boolean,
+ valueTruncated: boolean,
+ transferEncoding: string,
+ mimeType: string,
+ ) => void;
+ finish: () => void;
+ /**
+ * Emitted when specified `parts` limit has been reached. No more 'file' or 'field' events will be emitted.
+ */
+ partsLimit: () => void;
+ /**
+ * Emitted when specified `files` limit has been reached. No more 'file' events will be emitted.
+ */
+ filesLimit: () => void;
+ /**
+ * Emitted when specified `fields` limit has been reached. No more 'field' events will be emitted.
+ */
+ fieldsLimit: () => void;
+ error: (error: unknown) => void;
+}
+
+export interface BusboyConstructor {
+ (options: BusboyConfig): Busboy;
+
+ new(options: BusboyConfig): Busboy;
+}
+
diff --git a/fastify-busboy/lib/main.js b/fastify-busboy/lib/main.js
new file mode 100644
index 0000000..8794beb
--- /dev/null
+++ b/fastify-busboy/lib/main.js
@@ -0,0 +1,85 @@
+'use strict'
+
+const WritableStream = require('node:stream').Writable
+const { inherits } = require('node:util')
+const Dicer = require('../deps/dicer/lib/Dicer')
+
+const MultipartParser = require('./types/multipart')
+const UrlencodedParser = require('./types/urlencoded')
+const parseParams = require('./utils/parseParams')
+
+function Busboy (opts) {
+ if (!(this instanceof Busboy)) { return new Busboy(opts) }
+
+ if (typeof opts !== 'object') {
+ throw new TypeError('Busboy expected an options-Object.')
+ }
+ if (typeof opts.headers !== 'object') {
+ throw new TypeError('Busboy expected an options-Object with headers-attribute.')
+ }
+ if (typeof opts.headers['content-type'] !== 'string') {
+ throw new TypeError('Missing Content-Type-header.')
+ }
+
+ const {
+ headers,
+ ...streamOptions
+ } = opts
+
+ this.opts = {
+ autoDestroy: false,
+ ...streamOptions
+ }
+ WritableStream.call(this, this.opts)
+
+ this._done = false
+ this._parser = this.getParserByHeaders(headers)
+ this._finished = false
+}
+inherits(Busboy, WritableStream)
+
+Busboy.prototype.emit = function (ev) {
+ if (ev === 'finish') {
+ if (!this._done) {
+ this._parser?.end()
+ return
+ } else if (this._finished) {
+ return
+ }
+ this._finished = true
+ }
+ WritableStream.prototype.emit.apply(this, arguments)
+}
+
+Busboy.prototype.getParserByHeaders = function (headers) {
+ const parsed = parseParams(headers['content-type'])
+
+ const cfg = {
+ defCharset: this.opts.defCharset,
+ fileHwm: this.opts.fileHwm,
+ headers,
+ highWaterMark: this.opts.highWaterMark,
+ isPartAFile: this.opts.isPartAFile,
+ limits: this.opts.limits,
+ parsedConType: parsed,
+ preservePath: this.opts.preservePath
+ }
+
+ if (MultipartParser.detect.test(parsed[0])) {
+ return new MultipartParser(this, cfg)
+ }
+ if (UrlencodedParser.detect.test(parsed[0])) {
+ return new UrlencodedParser(this, cfg)
+ }
+ throw new Error('Unsupported Content-Type.')
+}
+
+Busboy.prototype._write = function (chunk, encoding, cb) {
+ this._parser.write(chunk, cb)
+}
+
+module.exports = Busboy
+module.exports.default = Busboy
+module.exports.Busboy = Busboy
+
+module.exports.Dicer = Dicer
diff --git a/fastify-busboy/lib/types/multipart.js b/fastify-busboy/lib/types/multipart.js
new file mode 100644
index 0000000..ad242db
--- /dev/null
+++ b/fastify-busboy/lib/types/multipart.js
@@ -0,0 +1,306 @@
+'use strict'
+
+// TODO:
+// * support 1 nested multipart level
+// (see second multipart example here:
+// http://www.w3.org/TR/html401/interact/forms.html#didx-multipartform-data)
+// * support limits.fieldNameSize
+// -- this will require modifications to utils.parseParams
+
+const { Readable } = require('node:stream')
+const { inherits } = require('node:util')
+
+const Dicer = require('../../deps/dicer/lib/Dicer')
+
+const parseParams = require('../utils/parseParams')
+const decodeText = require('../utils/decodeText')
+const basename = require('../utils/basename')
+const getLimit = require('../utils/getLimit')
+
+const RE_BOUNDARY = /^boundary$/i
+const RE_FIELD = /^form-data$/i
+const RE_CHARSET = /^charset$/i
+const RE_FILENAME = /^filename$/i
+const RE_NAME = /^name$/i
+
+Multipart.detect = /^multipart\/form-data/i
+function Multipart (boy, cfg) {
+ let i
+ let len
+ const self = this
+ let boundary
+ const limits = cfg.limits
+ const isPartAFile = cfg.isPartAFile || ((fieldName, contentType, fileName) => (contentType === 'application/octet-stream' || fileName !== undefined))
+ const parsedConType = cfg.parsedConType || []
+ const defCharset = cfg.defCharset || 'utf8'
+ const preservePath = cfg.preservePath
+ const fileOpts = { highWaterMark: cfg.fileHwm }
+
+ for (i = 0, len = parsedConType.length; i < len; ++i) {
+ if (Array.isArray(parsedConType[i]) &&
+ RE_BOUNDARY.test(parsedConType[i][0])) {
+ boundary = parsedConType[i][1]
+ break
+ }
+ }
+
+ function checkFinished () {
+ if (nends === 0 && finished && !boy._done) {
+ finished = false
+ self.end()
+ }
+ }
+
+ if (typeof boundary !== 'string') { throw new Error('Multipart: Boundary not found') }
+
+ const fieldSizeLimit = getLimit(limits, 'fieldSize', 1 * 1024 * 1024)
+ const fileSizeLimit = getLimit(limits, 'fileSize', Infinity)
+ const filesLimit = getLimit(limits, 'files', Infinity)
+ const fieldsLimit = getLimit(limits, 'fields', Infinity)
+ const partsLimit = getLimit(limits, 'parts', Infinity)
+ const headerPairsLimit = getLimit(limits, 'headerPairs', 2000)
+ const headerSizeLimit = getLimit(limits, 'headerSize', 80 * 1024)
+
+ let nfiles = 0
+ let nfields = 0
+ let nends = 0
+ let curFile
+ let curField
+ let finished = false
+
+ this._needDrain = false
+ this._pause = false
+ this._cb = undefined
+ this._nparts = 0
+ this._boy = boy
+
+ const parserCfg = {
+ boundary,
+ maxHeaderPairs: headerPairsLimit,
+ maxHeaderSize: headerSizeLimit,
+ partHwm: fileOpts.highWaterMark,
+ highWaterMark: cfg.highWaterMark
+ }
+
+ this.parser = new Dicer(parserCfg)
+ this.parser.on('drain', function () {
+ self._needDrain = false
+ if (self._cb && !self._pause) {
+ const cb = self._cb
+ self._cb = undefined
+ cb()
+ }
+ }).on('part', function onPart (part) {
+ if (++self._nparts > partsLimit) {
+ self.parser.removeListener('part', onPart)
+ self.parser.on('part', skipPart)
+ boy.hitPartsLimit = true
+ boy.emit('partsLimit')
+ return skipPart(part)
+ }
+
+ // hack because streams2 _always_ doesn't emit 'end' until nextTick, so let
+ // us emit 'end' early since we know the part has ended if we are already
+ // seeing the next part
+ if (curField) {
+ const field = curField
+ field.emit('end')
+ field.removeAllListeners('end')
+ }
+
+ part.on('header', function (header) {
+ let contype
+ let fieldname
+ let parsed
+ let charset
+ let encoding
+ let filename
+ let nsize = 0
+
+ if (header['content-type']) {
+ parsed = parseParams(header['content-type'][0])
+ if (parsed[0]) {
+ contype = parsed[0].toLowerCase()
+ for (i = 0, len = parsed.length; i < len; ++i) {
+ if (RE_CHARSET.test(parsed[i][0])) {
+ charset = parsed[i][1].toLowerCase()
+ break
+ }
+ }
+ }
+ }
+
+ if (contype === undefined) { contype = 'text/plain' }
+ if (charset === undefined) { charset = defCharset }
+
+ if (header['content-disposition']) {
+ parsed = parseParams(header['content-disposition'][0])
+ if (!RE_FIELD.test(parsed[0])) { return skipPart(part) }
+ for (i = 0, len = parsed.length; i < len; ++i) {
+ if (RE_NAME.test(parsed[i][0])) {
+ fieldname = parsed[i][1]
+ } else if (RE_FILENAME.test(parsed[i][0])) {
+ filename = parsed[i][1]
+ if (!preservePath) { filename = basename(filename) }
+ }
+ }
+ } else { return skipPart(part) }
+
+ if (header['content-transfer-encoding']) { encoding = header['content-transfer-encoding'][0].toLowerCase() } else { encoding = '7bit' }
+
+ let onData,
+ onEnd
+
+ if (isPartAFile(fieldname, contype, filename)) {
+ // file/binary field
+ if (nfiles === filesLimit) {
+ if (!boy.hitFilesLimit) {
+ boy.hitFilesLimit = true
+ boy.emit('filesLimit')
+ }
+ return skipPart(part)
+ }
+
+ ++nfiles
+
+ if (!boy._events.file) {
+ self.parser._ignore()
+ return
+ }
+
+ ++nends
+ const file = new FileStream(fileOpts)
+ curFile = file
+ file.on('end', function () {
+ --nends
+ self._pause = false
+ checkFinished()
+ if (self._cb && !self._needDrain) {
+ const cb = self._cb
+ self._cb = undefined
+ cb()
+ }
+ })
+ file._read = function (n) {
+ if (!self._pause) { return }
+ self._pause = false
+ if (self._cb && !self._needDrain) {
+ const cb = self._cb
+ self._cb = undefined
+ cb()
+ }
+ }
+ boy.emit('file', fieldname, file, filename, encoding, contype)
+
+ onData = function (data) {
+ if ((nsize += data.length) > fileSizeLimit) {
+ const extralen = fileSizeLimit - nsize + data.length
+ if (extralen > 0) { file.push(data.slice(0, extralen)) }
+ file.truncated = true
+ file.bytesRead = fileSizeLimit
+ part.removeAllListeners('data')
+ file.emit('limit')
+ return
+ } else if (!file.push(data)) { self._pause = true }
+
+ file.bytesRead = nsize
+ }
+
+ onEnd = function () {
+ curFile = undefined
+ file.push(null)
+ }
+ } else {
+ // non-file field
+ if (nfields === fieldsLimit) {
+ if (!boy.hitFieldsLimit) {
+ boy.hitFieldsLimit = true
+ boy.emit('fieldsLimit')
+ }
+ return skipPart(part)
+ }
+
+ ++nfields
+ ++nends
+ let buffer = ''
+ let truncated = false
+ curField = part
+
+ onData = function (data) {
+ if ((nsize += data.length) > fieldSizeLimit) {
+ const extralen = (fieldSizeLimit - (nsize - data.length))
+ buffer += data.toString('binary', 0, extralen)
+ truncated = true
+ part.removeAllListeners('data')
+ } else { buffer += data.toString('binary') }
+ }
+
+ onEnd = function () {
+ curField = undefined
+ if (buffer.length) { buffer = decodeText(buffer, 'binary', charset) }
+ boy.emit('field', fieldname, buffer, false, truncated, encoding, contype)
+ --nends
+ checkFinished()
+ }
+ }
+
+ /* As of node@2efe4ab761666 (v0.10.29+/v0.11.14+), busboy had become
+ broken. Streams2/streams3 is a huge black box of confusion, but
+ somehow overriding the sync state seems to fix things again (and still
+ seems to work for previous node versions).
+ */
+ part._readableState.sync = false
+
+ part.on('data', onData)
+ part.on('end', onEnd)
+ }).on('error', function (err) {
+ if (curFile) { curFile.emit('error', err) }
+ })
+ }).on('error', function (err) {
+ boy.emit('error', err)
+ }).on('finish', function () {
+ finished = true
+ checkFinished()
+ })
+}
+
+Multipart.prototype.write = function (chunk, cb) {
+ const r = this.parser.write(chunk)
+ if (r && !this._pause) {
+ cb()
+ } else {
+ this._needDrain = !r
+ this._cb = cb
+ }
+}
+
+Multipart.prototype.end = function () {
+ const self = this
+
+ if (self.parser.writable) {
+ self.parser.end()
+ } else if (!self._boy._done) {
+ process.nextTick(function () {
+ self._boy._done = true
+ self._boy.emit('finish')
+ })
+ }
+}
+
+function skipPart (part) {
+ part.resume()
+}
+
+function FileStream (opts) {
+ Readable.call(this, opts)
+
+ this.bytesRead = 0
+
+ this.truncated = false
+}
+
+inherits(FileStream, Readable)
+
+FileStream.prototype._read = function (n) {}
+
+module.exports = Multipart
diff --git a/fastify-busboy/lib/types/urlencoded.js b/fastify-busboy/lib/types/urlencoded.js
new file mode 100644
index 0000000..6f5f784
--- /dev/null
+++ b/fastify-busboy/lib/types/urlencoded.js
@@ -0,0 +1,190 @@
+'use strict'
+
+const Decoder = require('../utils/Decoder')
+const decodeText = require('../utils/decodeText')
+const getLimit = require('../utils/getLimit')
+
+const RE_CHARSET = /^charset$/i
+
+UrlEncoded.detect = /^application\/x-www-form-urlencoded/i
+function UrlEncoded (boy, cfg) {
+ const limits = cfg.limits
+ const parsedConType = cfg.parsedConType
+ this.boy = boy
+
+ this.fieldSizeLimit = getLimit(limits, 'fieldSize', 1 * 1024 * 1024)
+ this.fieldNameSizeLimit = getLimit(limits, 'fieldNameSize', 100)
+ this.fieldsLimit = getLimit(limits, 'fields', Infinity)
+
+ let charset
+ for (var i = 0, len = parsedConType.length; i < len; ++i) { // eslint-disable-line no-var
+ if (Array.isArray(parsedConType[i]) &&
+ RE_CHARSET.test(parsedConType[i][0])) {
+ charset = parsedConType[i][1].toLowerCase()
+ break
+ }
+ }
+
+ if (charset === undefined) { charset = cfg.defCharset || 'utf8' }
+
+ this.decoder = new Decoder()
+ this.charset = charset
+ this._fields = 0
+ this._state = 'key'
+ this._checkingBytes = true
+ this._bytesKey = 0
+ this._bytesVal = 0
+ this._key = ''
+ this._val = ''
+ this._keyTrunc = false
+ this._valTrunc = false
+ this._hitLimit = false
+}
+
+UrlEncoded.prototype.write = function (data, cb) {
+ if (this._fields === this.fieldsLimit) {
+ if (!this.boy.hitFieldsLimit) {
+ this.boy.hitFieldsLimit = true
+ this.boy.emit('fieldsLimit')
+ }
+ return cb()
+ }
+
+ let idxeq; let idxamp; let i; let p = 0; const len = data.length
+
+ while (p < len) {
+ if (this._state === 'key') {
+ idxeq = idxamp = undefined
+ for (i = p; i < len; ++i) {
+ if (!this._checkingBytes) { ++p }
+ if (data[i] === 0x3D/* = */) {
+ idxeq = i
+ break
+ } else if (data[i] === 0x26/* & */) {
+ idxamp = i
+ break
+ }
+ if (this._checkingBytes && this._bytesKey === this.fieldNameSizeLimit) {
+ this._hitLimit = true
+ break
+ } else if (this._checkingBytes) { ++this._bytesKey }
+ }
+
+ if (idxeq !== undefined) {
+ // key with assignment
+ if (idxeq > p) { this._key += this.decoder.write(data.toString('binary', p, idxeq)) }
+ this._state = 'val'
+
+ this._hitLimit = false
+ this._checkingBytes = true
+ this._val = ''
+ this._bytesVal = 0
+ this._valTrunc = false
+ this.decoder.reset()
+
+ p = idxeq + 1
+ } else if (idxamp !== undefined) {
+ // key with no assignment
+ ++this._fields
+ let key; const keyTrunc = this._keyTrunc
+ if (idxamp > p) { key = (this._key += this.decoder.write(data.toString('binary', p, idxamp))) } else { key = this._key }
+
+ this._hitLimit = false
+ this._checkingBytes = true
+ this._key = ''
+ this._bytesKey = 0
+ this._keyTrunc = false
+ this.decoder.reset()
+
+ if (key.length) {
+ this.boy.emit('field', decodeText(key, 'binary', this.charset),
+ '',
+ keyTrunc,
+ false)
+ }
+
+ p = idxamp + 1
+ if (this._fields === this.fieldsLimit) { return cb() }
+ } else if (this._hitLimit) {
+ // we may not have hit the actual limit if there are encoded bytes...
+ if (i > p) { this._key += this.decoder.write(data.toString('binary', p, i)) }
+ p = i
+ if ((this._bytesKey = this._key.length) === this.fieldNameSizeLimit) {
+ // yep, we actually did hit the limit
+ this._checkingBytes = false
+ this._keyTrunc = true
+ }
+ } else {
+ if (p < len) { this._key += this.decoder.write(data.toString('binary', p)) }
+ p = len
+ }
+ } else {
+ idxamp = undefined
+ for (i = p; i < len; ++i) {
+ if (!this._checkingBytes) { ++p }
+ if (data[i] === 0x26/* & */) {
+ idxamp = i
+ break
+ }
+ if (this._checkingBytes && this._bytesVal === this.fieldSizeLimit) {
+ this._hitLimit = true
+ break
+ } else if (this._checkingBytes) { ++this._bytesVal }
+ }
+
+ if (idxamp !== undefined) {
+ ++this._fields
+ if (idxamp > p) { this._val += this.decoder.write(data.toString('binary', p, idxamp)) }
+ this.boy.emit('field', decodeText(this._key, 'binary', this.charset),
+ decodeText(this._val, 'binary', this.charset),
+ this._keyTrunc,
+ this._valTrunc)
+ this._state = 'key'
+
+ this._hitLimit = false
+ this._checkingBytes = true
+ this._key = ''
+ this._bytesKey = 0
+ this._keyTrunc = false
+ this.decoder.reset()
+
+ p = idxamp + 1
+ if (this._fields === this.fieldsLimit) { return cb() }
+ } else if (this._hitLimit) {
+ // we may not have hit the actual limit if there are encoded bytes...
+ if (i > p) { this._val += this.decoder.write(data.toString('binary', p, i)) }
+ p = i
+ if ((this._val === '' && this.fieldSizeLimit === 0) ||
+ (this._bytesVal = this._val.length) === this.fieldSizeLimit) {
+ // yep, we actually did hit the limit
+ this._checkingBytes = false
+ this._valTrunc = true
+ }
+ } else {
+ if (p < len) { this._val += this.decoder.write(data.toString('binary', p)) }
+ p = len
+ }
+ }
+ }
+ cb()
+}
+
+UrlEncoded.prototype.end = function () {
+ if (this.boy._done) { return }
+
+ if (this._state === 'key' && this._key.length > 0) {
+ this.boy.emit('field', decodeText(this._key, 'binary', this.charset),
+ '',
+ this._keyTrunc,
+ false)
+ } else if (this._state === 'val') {
+ this.boy.emit('field', decodeText(this._key, 'binary', this.charset),
+ decodeText(this._val, 'binary', this.charset),
+ this._keyTrunc,
+ this._valTrunc)
+ }
+ this.boy._done = true
+ this.boy.emit('finish')
+}
+
+module.exports = UrlEncoded
diff --git a/fastify-busboy/lib/utils/Decoder.js b/fastify-busboy/lib/utils/Decoder.js
new file mode 100644
index 0000000..7917678
--- /dev/null
+++ b/fastify-busboy/lib/utils/Decoder.js
@@ -0,0 +1,54 @@
+'use strict'
+
+const RE_PLUS = /\+/g
+
+const HEX = [
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0,
+ 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
+]
+
+function Decoder () {
+ this.buffer = undefined
+}
+Decoder.prototype.write = function (str) {
+ // Replace '+' with ' ' before decoding
+ str = str.replace(RE_PLUS, ' ')
+ let res = ''
+ let i = 0; let p = 0; const len = str.length
+ for (; i < len; ++i) {
+ if (this.buffer !== undefined) {
+ if (!HEX[str.charCodeAt(i)]) {
+ res += '%' + this.buffer
+ this.buffer = undefined
+ --i // retry character
+ } else {
+ this.buffer += str[i]
+ ++p
+ if (this.buffer.length === 2) {
+ res += String.fromCharCode(parseInt(this.buffer, 16))
+ this.buffer = undefined
+ }
+ }
+ } else if (str[i] === '%') {
+ if (i > p) {
+ res += str.substring(p, i)
+ p = i
+ }
+ this.buffer = ''
+ ++p
+ }
+ }
+ if (p < len && this.buffer === undefined) { res += str.substring(p) }
+ return res
+}
+Decoder.prototype.reset = function () {
+ this.buffer = undefined
+}
+
+module.exports = Decoder
diff --git a/fastify-busboy/lib/utils/basename.js b/fastify-busboy/lib/utils/basename.js
new file mode 100644
index 0000000..db58819
--- /dev/null
+++ b/fastify-busboy/lib/utils/basename.js
@@ -0,0 +1,14 @@
+'use strict'
+
+module.exports = function basename (path) {
+ if (typeof path !== 'string') { return '' }
+ for (var i = path.length - 1; i >= 0; --i) { // eslint-disable-line no-var
+ switch (path.charCodeAt(i)) {
+ case 0x2F: // '/'
+ case 0x5C: // '\'
+ path = path.slice(i + 1)
+ return (path === '..' || path === '.' ? '' : path)
+ }
+ }
+ return (path === '..' || path === '.' ? '' : path)
+}
diff --git a/fastify-busboy/lib/utils/decodeText.js b/fastify-busboy/lib/utils/decodeText.js
new file mode 100644
index 0000000..be35d6b
--- /dev/null
+++ b/fastify-busboy/lib/utils/decodeText.js
@@ -0,0 +1,114 @@
+'use strict'
+
+// Node has always utf-8
+const utf8Decoder = new TextDecoder('utf-8')
+const textDecoders = new Map([
+ ['utf-8', utf8Decoder],
+ ['utf8', utf8Decoder]
+])
+
+function getDecoder (charset) {
+ let lc
+ while (true) {
+ switch (charset) {
+ case 'utf-8':
+ case 'utf8':
+ return decoders.utf8
+ case 'latin1':
+ case 'ascii': // TODO: Make these a separate, strict decoder?
+ case 'us-ascii':
+ case 'iso-8859-1':
+ case 'iso8859-1':
+ case 'iso88591':
+ case 'iso_8859-1':
+ case 'windows-1252':
+ case 'iso_8859-1:1987':
+ case 'cp1252':
+ case 'x-cp1252':
+ return decoders.latin1
+ case 'utf16le':
+ case 'utf-16le':
+ case 'ucs2':
+ case 'ucs-2':
+ return decoders.utf16le
+ case 'base64':
+ return decoders.base64
+ default:
+ if (lc === undefined) {
+ lc = true
+ charset = charset.toLowerCase()
+ continue
+ }
+ return decoders.other.bind(charset)
+ }
+ }
+}
+
+const decoders = {
+ utf8: (data, sourceEncoding) => {
+ if (data.length === 0) {
+ return ''
+ }
+ if (typeof data === 'string') {
+ data = Buffer.from(data, sourceEncoding)
+ }
+ return data.utf8Slice(0, data.length)
+ },
+
+ latin1: (data, sourceEncoding) => {
+ if (data.length === 0) {
+ return ''
+ }
+ if (typeof data === 'string') {
+ return data
+ }
+ return data.latin1Slice(0, data.length)
+ },
+
+ utf16le: (data, sourceEncoding) => {
+ if (data.length === 0) {
+ return ''
+ }
+ if (typeof data === 'string') {
+ data = Buffer.from(data, sourceEncoding)
+ }
+ return data.ucs2Slice(0, data.length)
+ },
+
+ base64: (data, sourceEncoding) => {
+ if (data.length === 0) {
+ return ''
+ }
+ if (typeof data === 'string') {
+ data = Buffer.from(data, sourceEncoding)
+ }
+ return data.base64Slice(0, data.length)
+ },
+
+ other: (data, sourceEncoding) => {
+ if (data.length === 0) {
+ return ''
+ }
+ if (typeof data === 'string') {
+ data = Buffer.from(data, sourceEncoding)
+ }
+
+ if (textDecoders.has(this.toString())) {
+ try {
+ return textDecoders.get(this).decode(data)
+ } catch (e) { }
+ }
+ return typeof data === 'string'
+ ? data
+ : data.toString()
+ }
+}
+
+function decodeText (text, sourceEncoding, destEncoding) {
+ if (text) {
+ return getDecoder(destEncoding)(text, sourceEncoding)
+ }
+ return text
+}
+
+module.exports = decodeText
diff --git a/fastify-busboy/lib/utils/getLimit.js b/fastify-busboy/lib/utils/getLimit.js
new file mode 100644
index 0000000..cb64fd6
--- /dev/null
+++ b/fastify-busboy/lib/utils/getLimit.js
@@ -0,0 +1,16 @@
+'use strict'
+
+module.exports = function getLimit (limits, name, defaultLimit) {
+ if (
+ !limits ||
+ limits[name] === undefined ||
+ limits[name] === null
+ ) { return defaultLimit }
+
+ if (
+ typeof limits[name] !== 'number' ||
+ isNaN(limits[name])
+ ) { throw new TypeError('Limit ' + name + ' is not a valid number') }
+
+ return limits[name]
+}
diff --git a/fastify-busboy/lib/utils/parseParams.js b/fastify-busboy/lib/utils/parseParams.js
new file mode 100644
index 0000000..1698e62
--- /dev/null
+++ b/fastify-busboy/lib/utils/parseParams.js
@@ -0,0 +1,196 @@
+/* eslint-disable object-property-newline */
+'use strict'
+
+const decodeText = require('./decodeText')
+
+const RE_ENCODED = /%[a-fA-F0-9][a-fA-F0-9]/g
+
+const EncodedLookup = {
+ '%00': '\x00', '%01': '\x01', '%02': '\x02', '%03': '\x03', '%04': '\x04',
+ '%05': '\x05', '%06': '\x06', '%07': '\x07', '%08': '\x08', '%09': '\x09',
+ '%0a': '\x0a', '%0A': '\x0a', '%0b': '\x0b', '%0B': '\x0b', '%0c': '\x0c',
+ '%0C': '\x0c', '%0d': '\x0d', '%0D': '\x0d', '%0e': '\x0e', '%0E': '\x0e',
+ '%0f': '\x0f', '%0F': '\x0f', '%10': '\x10', '%11': '\x11', '%12': '\x12',
+ '%13': '\x13', '%14': '\x14', '%15': '\x15', '%16': '\x16', '%17': '\x17',
+ '%18': '\x18', '%19': '\x19', '%1a': '\x1a', '%1A': '\x1a', '%1b': '\x1b',
+ '%1B': '\x1b', '%1c': '\x1c', '%1C': '\x1c', '%1d': '\x1d', '%1D': '\x1d',
+ '%1e': '\x1e', '%1E': '\x1e', '%1f': '\x1f', '%1F': '\x1f', '%20': '\x20',
+ '%21': '\x21', '%22': '\x22', '%23': '\x23', '%24': '\x24', '%25': '\x25',
+ '%26': '\x26', '%27': '\x27', '%28': '\x28', '%29': '\x29', '%2a': '\x2a',
+ '%2A': '\x2a', '%2b': '\x2b', '%2B': '\x2b', '%2c': '\x2c', '%2C': '\x2c',
+ '%2d': '\x2d', '%2D': '\x2d', '%2e': '\x2e', '%2E': '\x2e', '%2f': '\x2f',
+ '%2F': '\x2f', '%30': '\x30', '%31': '\x31', '%32': '\x32', '%33': '\x33',
+ '%34': '\x34', '%35': '\x35', '%36': '\x36', '%37': '\x37', '%38': '\x38',
+ '%39': '\x39', '%3a': '\x3a', '%3A': '\x3a', '%3b': '\x3b', '%3B': '\x3b',
+ '%3c': '\x3c', '%3C': '\x3c', '%3d': '\x3d', '%3D': '\x3d', '%3e': '\x3e',
+ '%3E': '\x3e', '%3f': '\x3f', '%3F': '\x3f', '%40': '\x40', '%41': '\x41',
+ '%42': '\x42', '%43': '\x43', '%44': '\x44', '%45': '\x45', '%46': '\x46',
+ '%47': '\x47', '%48': '\x48', '%49': '\x49', '%4a': '\x4a', '%4A': '\x4a',
+ '%4b': '\x4b', '%4B': '\x4b', '%4c': '\x4c', '%4C': '\x4c', '%4d': '\x4d',
+ '%4D': '\x4d', '%4e': '\x4e', '%4E': '\x4e', '%4f': '\x4f', '%4F': '\x4f',
+ '%50': '\x50', '%51': '\x51', '%52': '\x52', '%53': '\x53', '%54': '\x54',
+ '%55': '\x55', '%56': '\x56', '%57': '\x57', '%58': '\x58', '%59': '\x59',
+ '%5a': '\x5a', '%5A': '\x5a', '%5b': '\x5b', '%5B': '\x5b', '%5c': '\x5c',
+ '%5C': '\x5c', '%5d': '\x5d', '%5D': '\x5d', '%5e': '\x5e', '%5E': '\x5e',
+ '%5f': '\x5f', '%5F': '\x5f', '%60': '\x60', '%61': '\x61', '%62': '\x62',
+ '%63': '\x63', '%64': '\x64', '%65': '\x65', '%66': '\x66', '%67': '\x67',
+ '%68': '\x68', '%69': '\x69', '%6a': '\x6a', '%6A': '\x6a', '%6b': '\x6b',
+ '%6B': '\x6b', '%6c': '\x6c', '%6C': '\x6c', '%6d': '\x6d', '%6D': '\x6d',
+ '%6e': '\x6e', '%6E': '\x6e', '%6f': '\x6f', '%6F': '\x6f', '%70': '\x70',
+ '%71': '\x71', '%72': '\x72', '%73': '\x73', '%74': '\x74', '%75': '\x75',
+ '%76': '\x76', '%77': '\x77', '%78': '\x78', '%79': '\x79', '%7a': '\x7a',
+ '%7A': '\x7a', '%7b': '\x7b', '%7B': '\x7b', '%7c': '\x7c', '%7C': '\x7c',
+ '%7d': '\x7d', '%7D': '\x7d', '%7e': '\x7e', '%7E': '\x7e', '%7f': '\x7f',
+ '%7F': '\x7f', '%80': '\x80', '%81': '\x81', '%82': '\x82', '%83': '\x83',
+ '%84': '\x84', '%85': '\x85', '%86': '\x86', '%87': '\x87', '%88': '\x88',
+ '%89': '\x89', '%8a': '\x8a', '%8A': '\x8a', '%8b': '\x8b', '%8B': '\x8b',
+ '%8c': '\x8c', '%8C': '\x8c', '%8d': '\x8d', '%8D': '\x8d', '%8e': '\x8e',
+ '%8E': '\x8e', '%8f': '\x8f', '%8F': '\x8f', '%90': '\x90', '%91': '\x91',
+ '%92': '\x92', '%93': '\x93', '%94': '\x94', '%95': '\x95', '%96': '\x96',
+ '%97': '\x97', '%98': '\x98', '%99': '\x99', '%9a': '\x9a', '%9A': '\x9a',
+ '%9b': '\x9b', '%9B': '\x9b', '%9c': '\x9c', '%9C': '\x9c', '%9d': '\x9d',
+ '%9D': '\x9d', '%9e': '\x9e', '%9E': '\x9e', '%9f': '\x9f', '%9F': '\x9f',
+ '%a0': '\xa0', '%A0': '\xa0', '%a1': '\xa1', '%A1': '\xa1', '%a2': '\xa2',
+ '%A2': '\xa2', '%a3': '\xa3', '%A3': '\xa3', '%a4': '\xa4', '%A4': '\xa4',
+ '%a5': '\xa5', '%A5': '\xa5', '%a6': '\xa6', '%A6': '\xa6', '%a7': '\xa7',
+ '%A7': '\xa7', '%a8': '\xa8', '%A8': '\xa8', '%a9': '\xa9', '%A9': '\xa9',
+ '%aa': '\xaa', '%Aa': '\xaa', '%aA': '\xaa', '%AA': '\xaa', '%ab': '\xab',
+ '%Ab': '\xab', '%aB': '\xab', '%AB': '\xab', '%ac': '\xac', '%Ac': '\xac',
+ '%aC': '\xac', '%AC': '\xac', '%ad': '\xad', '%Ad': '\xad', '%aD': '\xad',
+ '%AD': '\xad', '%ae': '\xae', '%Ae': '\xae', '%aE': '\xae', '%AE': '\xae',
+ '%af': '\xaf', '%Af': '\xaf', '%aF': '\xaf', '%AF': '\xaf', '%b0': '\xb0',
+ '%B0': '\xb0', '%b1': '\xb1', '%B1': '\xb1', '%b2': '\xb2', '%B2': '\xb2',
+ '%b3': '\xb3', '%B3': '\xb3', '%b4': '\xb4', '%B4': '\xb4', '%b5': '\xb5',
+ '%B5': '\xb5', '%b6': '\xb6', '%B6': '\xb6', '%b7': '\xb7', '%B7': '\xb7',
+ '%b8': '\xb8', '%B8': '\xb8', '%b9': '\xb9', '%B9': '\xb9', '%ba': '\xba',
+ '%Ba': '\xba', '%bA': '\xba', '%BA': '\xba', '%bb': '\xbb', '%Bb': '\xbb',
+ '%bB': '\xbb', '%BB': '\xbb', '%bc': '\xbc', '%Bc': '\xbc', '%bC': '\xbc',
+ '%BC': '\xbc', '%bd': '\xbd', '%Bd': '\xbd', '%bD': '\xbd', '%BD': '\xbd',
+ '%be': '\xbe', '%Be': '\xbe', '%bE': '\xbe', '%BE': '\xbe', '%bf': '\xbf',
+ '%Bf': '\xbf', '%bF': '\xbf', '%BF': '\xbf', '%c0': '\xc0', '%C0': '\xc0',
+ '%c1': '\xc1', '%C1': '\xc1', '%c2': '\xc2', '%C2': '\xc2', '%c3': '\xc3',
+ '%C3': '\xc3', '%c4': '\xc4', '%C4': '\xc4', '%c5': '\xc5', '%C5': '\xc5',
+ '%c6': '\xc6', '%C6': '\xc6', '%c7': '\xc7', '%C7': '\xc7', '%c8': '\xc8',
+ '%C8': '\xc8', '%c9': '\xc9', '%C9': '\xc9', '%ca': '\xca', '%Ca': '\xca',
+ '%cA': '\xca', '%CA': '\xca', '%cb': '\xcb', '%Cb': '\xcb', '%cB': '\xcb',
+ '%CB': '\xcb', '%cc': '\xcc', '%Cc': '\xcc', '%cC': '\xcc', '%CC': '\xcc',
+ '%cd': '\xcd', '%Cd': '\xcd', '%cD': '\xcd', '%CD': '\xcd', '%ce': '\xce',
+ '%Ce': '\xce', '%cE': '\xce', '%CE': '\xce', '%cf': '\xcf', '%Cf': '\xcf',
+ '%cF': '\xcf', '%CF': '\xcf', '%d0': '\xd0', '%D0': '\xd0', '%d1': '\xd1',
+ '%D1': '\xd1', '%d2': '\xd2', '%D2': '\xd2', '%d3': '\xd3', '%D3': '\xd3',
+ '%d4': '\xd4', '%D4': '\xd4', '%d5': '\xd5', '%D5': '\xd5', '%d6': '\xd6',
+ '%D6': '\xd6', '%d7': '\xd7', '%D7': '\xd7', '%d8': '\xd8', '%D8': '\xd8',
+ '%d9': '\xd9', '%D9': '\xd9', '%da': '\xda', '%Da': '\xda', '%dA': '\xda',
+ '%DA': '\xda', '%db': '\xdb', '%Db': '\xdb', '%dB': '\xdb', '%DB': '\xdb',
+ '%dc': '\xdc', '%Dc': '\xdc', '%dC': '\xdc', '%DC': '\xdc', '%dd': '\xdd',
+ '%Dd': '\xdd', '%dD': '\xdd', '%DD': '\xdd', '%de': '\xde', '%De': '\xde',
+ '%dE': '\xde', '%DE': '\xde', '%df': '\xdf', '%Df': '\xdf', '%dF': '\xdf',
+ '%DF': '\xdf', '%e0': '\xe0', '%E0': '\xe0', '%e1': '\xe1', '%E1': '\xe1',
+ '%e2': '\xe2', '%E2': '\xe2', '%e3': '\xe3', '%E3': '\xe3', '%e4': '\xe4',
+ '%E4': '\xe4', '%e5': '\xe5', '%E5': '\xe5', '%e6': '\xe6', '%E6': '\xe6',
+ '%e7': '\xe7', '%E7': '\xe7', '%e8': '\xe8', '%E8': '\xe8', '%e9': '\xe9',
+ '%E9': '\xe9', '%ea': '\xea', '%Ea': '\xea', '%eA': '\xea', '%EA': '\xea',
+ '%eb': '\xeb', '%Eb': '\xeb', '%eB': '\xeb', '%EB': '\xeb', '%ec': '\xec',
+ '%Ec': '\xec', '%eC': '\xec', '%EC': '\xec', '%ed': '\xed', '%Ed': '\xed',
+ '%eD': '\xed', '%ED': '\xed', '%ee': '\xee', '%Ee': '\xee', '%eE': '\xee',
+ '%EE': '\xee', '%ef': '\xef', '%Ef': '\xef', '%eF': '\xef', '%EF': '\xef',
+ '%f0': '\xf0', '%F0': '\xf0', '%f1': '\xf1', '%F1': '\xf1', '%f2': '\xf2',
+ '%F2': '\xf2', '%f3': '\xf3', '%F3': '\xf3', '%f4': '\xf4', '%F4': '\xf4',
+ '%f5': '\xf5', '%F5': '\xf5', '%f6': '\xf6', '%F6': '\xf6', '%f7': '\xf7',
+ '%F7': '\xf7', '%f8': '\xf8', '%F8': '\xf8', '%f9': '\xf9', '%F9': '\xf9',
+ '%fa': '\xfa', '%Fa': '\xfa', '%fA': '\xfa', '%FA': '\xfa', '%fb': '\xfb',
+ '%Fb': '\xfb', '%fB': '\xfb', '%FB': '\xfb', '%fc': '\xfc', '%Fc': '\xfc',
+ '%fC': '\xfc', '%FC': '\xfc', '%fd': '\xfd', '%Fd': '\xfd', '%fD': '\xfd',
+ '%FD': '\xfd', '%fe': '\xfe', '%Fe': '\xfe', '%fE': '\xfe', '%FE': '\xfe',
+ '%ff': '\xff', '%Ff': '\xff', '%fF': '\xff', '%FF': '\xff'
+}
+
+function encodedReplacer (match) {
+ return EncodedLookup[match]
+}
+
+const STATE_KEY = 0
+const STATE_VALUE = 1
+const STATE_CHARSET = 2
+const STATE_LANG = 3
+
+function parseParams (str) {
+ const res = []
+ let state = STATE_KEY
+ let charset = ''
+ let inquote = false
+ let escaping = false
+ let p = 0
+ let tmp = ''
+ const len = str.length
+
+ for (var i = 0; i < len; ++i) { // eslint-disable-line no-var
+ const char = str[i]
+ if (char === '\\' && inquote) {
+ if (escaping) { escaping = false } else {
+ escaping = true
+ continue
+ }
+ } else if (char === '"') {
+ if (!escaping) {
+ if (inquote) {
+ inquote = false
+ state = STATE_KEY
+ } else { inquote = true }
+ continue
+ } else { escaping = false }
+ } else {
+ if (escaping && inquote) { tmp += '\\' }
+ escaping = false
+ if ((state === STATE_CHARSET || state === STATE_LANG) && char === "'") {
+ if (state === STATE_CHARSET) {
+ state = STATE_LANG
+ charset = tmp.substring(1)
+ } else { state = STATE_VALUE }
+ tmp = ''
+ continue
+ } else if (state === STATE_KEY &&
+ (char === '*' || char === '=') &&
+ res.length) {
+ state = char === '*'
+ ? STATE_CHARSET
+ : STATE_VALUE
+ res[p] = [tmp, undefined]
+ tmp = ''
+ continue
+ } else if (!inquote && char === ';') {
+ state = STATE_KEY
+ if (charset) {
+ if (tmp.length) {
+ tmp = decodeText(tmp.replace(RE_ENCODED, encodedReplacer),
+ 'binary',
+ charset)
+ }
+ charset = ''
+ } else if (tmp.length) {
+ tmp = decodeText(tmp, 'binary', 'utf8')
+ }
+ if (res[p] === undefined) { res[p] = tmp } else { res[p][1] = tmp }
+ tmp = ''
+ ++p
+ continue
+ } else if (!inquote && (char === ' ' || char === '\t')) { continue }
+ }
+ tmp += char
+ }
+ if (charset && tmp.length) {
+ tmp = decodeText(tmp.replace(RE_ENCODED, encodedReplacer),
+ 'binary',
+ charset)
+ } else if (tmp) {
+ tmp = decodeText(tmp, 'binary', 'utf8')
+ }
+
+ if (res[p] === undefined) {
+ if (tmp) { res[p] = tmp }
+ } else { res[p][1] = tmp }
+
+ return res
+}
+
+module.exports = parseParams
diff --git a/fastify-busboy/package.json b/fastify-busboy/package.json
new file mode 100644
index 0000000..4be895c
--- /dev/null
+++ b/fastify-busboy/package.json
@@ -0,0 +1,86 @@
+{
+ "name": "@fastify/busboy",
+ "version": "2.1.0",
+ "private": false,
+ "author": "Brian White <mscdex@mscdex.net>",
+ "contributors": [
+ {
+ "name": "Igor Savin",
+ "email": "kibertoad@gmail.com",
+ "url": "https://github.com/kibertoad"
+ },
+ {
+ "name": "Aras Abbasi",
+ "email": "aras.abbasi@gmail.com",
+ "url": "https://github.com/uzlopak"
+ }
+ ],
+ "description": "A streaming parser for HTML form data for node.js",
+ "main": "lib/main",
+ "type": "commonjs",
+ "types": "lib/main.d.ts",
+ "scripts": {
+ "bench:busboy": "cd benchmarks && npm install && npm run benchmark-fastify",
+ "bench:dicer": "node bench/dicer/dicer-bench-multipart-parser.js",
+ "coveralls": "nyc report --reporter=lcov",
+ "lint": "npm run lint:standard",
+ "lint:everything": "npm run lint && npm run test:types",
+ "lint:fix": "standard --fix",
+ "lint:standard": "standard --verbose | snazzy",
+ "test:mocha": "tap",
+ "test:types": "tsd",
+ "test:coverage": "nyc npm run test",
+ "test": "npm run test:mocha"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "devDependencies": {
+ "@types/node": "^20.1.0",
+ "busboy": "^1.0.0",
+ "photofinish": "^1.8.0",
+ "snazzy": "^9.0.0",
+ "standard": "^17.0.0",
+ "tap": "^16.3.8",
+ "tinybench": "^2.5.1",
+ "tsd": "^0.29.0",
+ "typescript": "^5.0.2"
+ },
+ "keywords": [
+ "uploads",
+ "forms",
+ "multipart",
+ "form-data"
+ ],
+ "license": "MIT",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/fastify/busboy.git"
+ },
+ "tsd": {
+ "directory": "test/types",
+ "compilerOptions": {
+ "esModuleInterop": false,
+ "module": "commonjs",
+ "target": "ES2017"
+ }
+ },
+ "standard": {
+ "globals": [
+ "describe",
+ "it"
+ ],
+ "ignore": [
+ "bench"
+ ]
+ },
+ "files": [
+ "README.md",
+ "LICENSE",
+ "lib/*",
+ "deps/encoding/*",
+ "deps/dicer/lib",
+ "deps/streamsearch/",
+ "deps/dicer/LICENSE"
+ ]
+}
diff --git a/fastify-busboy/test/busboy-constructor.test.js b/fastify-busboy/test/busboy-constructor.test.js
new file mode 100644
index 0000000..8607789
--- /dev/null
+++ b/fastify-busboy/test/busboy-constructor.test.js
@@ -0,0 +1,75 @@
+'use strict'
+
+const Busboy = require('../lib/main')
+const { test } = require('tap')
+
+test('busboy-constructor - should throw an Error if no options are provided', t => {
+ t.plan(1)
+
+ t.throws(() => new Busboy(), new Error('Busboy expected an options-Object.'))
+})
+
+test('busboy-constructor - should throw an Error if options does not contain headers', t => {
+ t.plan(1)
+
+ t.throws(() => new Busboy({}), new Error('Busboy expected an options-Object with headers-attribute.'))
+})
+
+test('busboy-constructor - if busboy is called without new-operator, still creates a busboy instance', t => {
+ t.plan(1)
+
+ const busboyInstance = Busboy({ headers: { 'content-type': 'application/x-www-form-urlencoded' } })
+ t.type(busboyInstance, Busboy)
+})
+
+test('busboy-constructor - should throw an Error if content-type is not set', t => {
+ t.plan(1)
+
+ t.throws(() => new Busboy({ headers: {} }), new Error('Missing Content-Type-header.'))
+})
+
+test('busboy-constructor - should throw an Error if content-type is unsupported', t => {
+ t.plan(1)
+
+ t.throws(() => new Busboy({ headers: { 'content-type': 'unsupported' } }), new Error('Unsupported Content-Type.'))
+})
+
+test('busboy-constructor - should not throw an Error if content-type is urlencoded', t => {
+ t.plan(1)
+
+ t.doesNotThrow(() => new Busboy({ headers: { 'content-type': 'application/x-www-form-urlencoded' } }))
+})
+
+test('busboy-constructor - if busboy is called without stream options autoDestroy is set to false', t => {
+ t.plan(1)
+
+ const busboyInstance = Busboy({ headers: { 'content-type': 'application/x-www-form-urlencoded' } })
+ t.equal(busboyInstance._writableState.autoDestroy, false)
+})
+
+test('busboy-constructor - if busboy is called with invalid value for stream option highWaterMark we should throw', t => {
+ t.plan(1)
+
+ t.throws(() => Busboy({ highWaterMark: 'not_allowed_value_for_highWaterMark', headers: { 'content-type': 'application/x-www-form-urlencoded' } }), new Error('not_allowed_value_for_highWaterMark'))
+})
+
+test('busboy-constructor - if busboy is called with stream options and autoDestroy:true, autoDestroy should be set to true', t => {
+ t.plan(1)
+
+ const busboyInstance = Busboy({ autoDestroy: true, headers: { 'content-type': 'application/x-www-form-urlencoded' } })
+ t.equal(busboyInstance._writableState.autoDestroy, true)
+})
+
+test('busboy-constructor - busboy should be initialized with private attribute _done set as false', t => {
+ t.plan(1)
+
+ const busboyInstance = Busboy({ headers: { 'content-type': 'application/x-www-form-urlencoded' } })
+ t.equal(busboyInstance._done, false)
+})
+
+test('busboy-constructor - busboy should be initialized with private attribute _finished set as false', t => {
+ t.plan(1)
+
+ const busboyInstance = Busboy({ headers: { 'content-type': 'application/x-www-form-urlencoded' } })
+ t.equal(busboyInstance._finished, false)
+})
diff --git a/fastify-busboy/test/decoder.test.js b/fastify-busboy/test/decoder.test.js
new file mode 100644
index 0000000..fa4ce69
--- /dev/null
+++ b/fastify-busboy/test/decoder.test.js
@@ -0,0 +1,98 @@
+'use strict'
+
+const { test } = require('tap')
+const Decoder = require('../lib/utils/Decoder')
+
+test('Decoder', t => {
+ const tests =
+ [
+ {
+ source: ['Hello world'],
+ expected: 'Hello world',
+ what: 'No encoded bytes'
+ },
+ {
+ source: ['Hello%20world'],
+ expected: 'Hello world',
+ what: 'One full encoded byte'
+ },
+ {
+ source: ['Hello%20world%21'],
+ expected: 'Hello world!',
+ what: 'Two full encoded bytes'
+ },
+ {
+ source: ['Hello%', '20world'],
+ expected: 'Hello world',
+ what: 'One full encoded byte split #1'
+ },
+ {
+ source: ['Hello%2', '0world'],
+ expected: 'Hello world',
+ what: 'One full encoded byte split #2'
+ },
+ {
+ source: ['Hello%20', 'world'],
+ expected: 'Hello world',
+ what: 'One full encoded byte (concat)'
+ },
+ {
+ source: ['Hello%2Qworld'],
+ expected: 'Hello%2Qworld',
+ what: 'Malformed encoded byte #1'
+ },
+ {
+ source: ['Hello%world'],
+ expected: 'Hello%world',
+ what: 'Malformed encoded byte #2'
+ },
+ {
+ source: ['Hello+world'],
+ expected: 'Hello world',
+ what: 'Plus to space'
+ },
+ {
+ source: ['Hello+world%21'],
+ expected: 'Hello world!',
+ what: 'Plus and encoded byte'
+ },
+ {
+ source: ['5%2B5%3D10'],
+ expected: '5+5=10',
+ what: 'Encoded plus'
+ },
+ {
+ source: ['5+%2B+5+%3D+10'],
+ expected: '5 + 5 = 10',
+ what: 'Spaces and encoded plus'
+ }
+ ]
+ t.plan(tests.length + 1)
+
+ tests.forEach((v) => {
+ t.test(v.what, t => {
+ t.plan(1)
+
+ const dec = new Decoder()
+ let result = ''
+ v.source.forEach(function (s) {
+ result += dec.write(s)
+ })
+ const msg = 'Decoded string mismatch.\n' +
+ 'Saw: ' + result + '\n' +
+ 'Expected: ' + v.expected
+ t.strictSame(result, v.expected, msg)
+ })
+ })
+
+ t.test('reset sets internal buffer to undefined', t => {
+ t.plan(2)
+
+ const dec = new Decoder()
+ dec.write('Hello+world%2')
+
+ t.notSame(dec.buffer, undefined)
+ dec.reset()
+ t.equal(dec.buffer, undefined)
+ })
+})
diff --git a/fastify-busboy/test/dicer-constructor.test.js b/fastify-busboy/test/dicer-constructor.test.js
new file mode 100644
index 0000000..e0e6a6c
--- /dev/null
+++ b/fastify-busboy/test/dicer-constructor.test.js
@@ -0,0 +1,22 @@
+'use strict'
+
+const { test } = require('tap')
+const Dicer = require('../deps/dicer/lib/Dicer')
+
+test('dicer-constructor', t => {
+ t.plan(2)
+
+ t.test('should throw an Error when no options parameter is supplied to Dicer', t => {
+ t.plan(1)
+
+ t.throws(() => new Dicer(), new Error('Boundary required'))
+ })
+
+ t.test('without new operator a new dicer instance will be initialized', t => {
+ t.plan(1)
+
+ t.type(Dicer({
+ boundary: '----boundary'
+ }), Dicer)
+ })
+})
diff --git a/fastify-busboy/test/dicer-endfinish.test.js b/fastify-busboy/test/dicer-endfinish.test.js
new file mode 100644
index 0000000..4718076
--- /dev/null
+++ b/fastify-busboy/test/dicer-endfinish.test.js
@@ -0,0 +1,96 @@
+'use strict'
+
+const Dicer = require('../deps/dicer/lib/Dicer')
+const { test } = require('tap')
+
+test('dicer-endfinish', t => {
+ t.plan(1)
+
+ t.test('should properly handle finish', t => {
+ t.plan(4)
+
+ const CRLF = '\r\n'
+ const boundary = 'boundary'
+
+ const writeSep = '--' + boundary
+
+ const writePart = [
+ writeSep,
+ 'Content-Type: text/plain',
+ 'Content-Length: 0'
+ ].join(CRLF) +
+ CRLF + CRLF +
+ 'some data' + CRLF
+
+ const writeEnd = '--' + CRLF
+
+ let firedEnd = false
+ let firedFinish = false
+
+ const dicer = new Dicer({ boundary })
+ dicer.on('part', partListener)
+ dicer.on('finish', finishListener)
+ dicer.write(writePart + writeSep)
+
+ function partListener (partReadStream) {
+ partReadStream.on('data', function () { })
+ partReadStream.on('end', partEndListener)
+ }
+ function partEndListener () {
+ firedEnd = true
+ setImmediate(afterEnd)
+ }
+ function afterEnd () {
+ dicer.end(writeEnd)
+ setImmediate(afterWrite)
+ }
+ function finishListener () {
+ t.ok(firedEnd, 'end before finishing')
+ firedFinish = true
+ test2()
+ }
+ function afterWrite () {
+ t.ok(firedFinish, 'Failed to finish')
+ }
+
+ let isPausePush = true
+
+ let firedPauseCallback = false
+ let firedPauseFinish = false
+
+ let dicer2 = null
+
+ function test2 () {
+ dicer2 = new Dicer({ boundary })
+ dicer2.on('part', pausePartListener)
+ dicer2.on('finish', pauseFinish)
+ dicer2.write(writePart + writeSep, 'utf8', pausePartCallback)
+ setImmediate(pauseAfterWrite)
+ }
+ function pausePartListener (partReadStream) {
+ partReadStream.on('data', function () { })
+ partReadStream.on('end', function () { })
+ const realPush = partReadStream.push
+ partReadStream.push = function fakePush () {
+ realPush.apply(partReadStream, arguments)
+ if (!isPausePush) { return true }
+ isPausePush = false
+ return false
+ }
+ }
+ function pauseAfterWrite () {
+ dicer2.end(writeEnd)
+ setImmediate(pauseAfterEnd)
+ }
+ function pauseAfterEnd () {
+ t.ok(firedPauseCallback, 'Called callback after pause')
+ t.ok(firedPauseFinish, 'Finish after pause')
+ }
+ function pauseFinish () {
+ firedPauseFinish = true
+ }
+ function pausePartCallback () {
+ firedPauseCallback = true
+ }
+ })
+})
diff --git a/fastify-busboy/test/dicer-export.test.js b/fastify-busboy/test/dicer-export.test.js
new file mode 100644
index 0000000..05df4e6
--- /dev/null
+++ b/fastify-busboy/test/dicer-export.test.js
@@ -0,0 +1,24 @@
+'use strict'
+
+const { test } = require('tap')
+const { Dicer } = require('../lib/main')
+
+test('dicer-export', t => {
+ t.plan(2)
+
+ t.test('without new operator a new dicer instance will be initialized', t => {
+ t.plan(1)
+
+ t.type(Dicer({
+ boundary: '----boundary'
+ }), Dicer)
+ })
+
+ t.test('with new operator a new dicer instance will be initialized', t => {
+ t.plan(1)
+
+ t.type(new Dicer({
+ boundary: '----boundary'
+ }), Dicer)
+ })
+})
diff --git a/fastify-busboy/test/dicer-headerparser.test.js b/fastify-busboy/test/dicer-headerparser.test.js
new file mode 100644
index 0000000..73da283
--- /dev/null
+++ b/fastify-busboy/test/dicer-headerparser.test.js
@@ -0,0 +1,192 @@
+'use strict'
+
+const { test } = require('tap')
+const HeaderParser = require('../deps/dicer/lib/HeaderParser')
+
+test('dicer-headerparser', t => {
+ const DCRLF = '\r\n\r\n'
+ const MAXED_BUFFER = Buffer.allocUnsafe(128 * 1024)
+ MAXED_BUFFER.fill(0x41) // 'A'
+
+ const tests = [
+ {
+ source: DCRLF,
+ expected: {},
+ what: 'No header'
+ },
+ {
+ source: ['Content-Type:\t text/plain',
+ 'Content-Length:0'
+ ].join('\r\n') + DCRLF,
+ expected: { 'content-type': [' text/plain'], 'content-length': ['0'] },
+ what: 'Value spacing'
+ },
+ {
+ source: ['Content-Type:\t text/plain',
+ 'Content-Length:0'
+ ].join('\r\n') + DCRLF,
+ cfg: {
+ maxHeaderPairs: 0
+ },
+ expected: {},
+ what: 'should enforce maxHeaderPairs of 0'
+ },
+ {
+ source: ['Content-Type:\t text/plain',
+ 'Content-Length:0'
+ ].join('\r\n') + DCRLF,
+ cfg: {
+ maxHeaderPairs: 1
+ },
+ expected: { 'content-type': [' text/plain'] },
+ what: 'should enforce maxHeaderPairs of 1'
+ },
+ {
+ source: ['Content-Type:\r\n text/plain',
+ 'Foo:\r\n bar\r\n baz'
+ ].join('\r\n') + DCRLF,
+ expected: {},
+ cfg: {
+ maxHeaderSize: 0
+ },
+ what: 'should enforce maxHeaderSize of 0'
+ },
+ {
+ source: ['Content-Type:\r\n text/plain',
+ 'Foo:\r\n bar\r\n baz'
+ ].join('\r\n') + DCRLF,
+ expected: { 'content-type': [' text/plai'] },
+ cfg: {
+ maxHeaderSize: 25
+ },
+ what: 'should enforce maxHeaderSize of 25'
+ },
+ {
+ source: ['Content-Type:\r\n text/plain',
+ 'Foo:\r\n bar\r\n baz'
+ ].join('\r\n') + DCRLF,
+ expected: { 'content-type': [' text/plain'] },
+ cfg: {
+ maxHeaderSize: 31
+ },
+ what: 'should enforce maxHeaderSize of 31 and ignore the second header'
+ },
+ {
+ source: ['Content-Type:\r\n text/plain',
+ 'Foo:\r\n bar\r\n baz'
+ ].join('\r\n') + DCRLF,
+ expected: { 'content-type': [' text/plain'], foo: [''] },
+ cfg: {
+ maxHeaderSize: 32
+ },
+ what: 'should enforce maxHeaderSize of 32 and only add key of second header'
+ },
+ {
+ source: ['Content-Type:\r\n text/plain',
+ 'Foo:\r\n bar\r\n baz'
+ ].join('\r\n') + DCRLF,
+ expected: { 'content-type': [' text/plain'], foo: ['\r'] },
+ cfg: {
+ maxHeaderSize: 33
+ },
+ what: 'should enforce maxHeaderSize of 32 and get only first character of second pair'
+ },
+ {
+ source: ['Content-Type:\r\n text/plain',
+ ' : '
+ ].join('\r\n') + DCRLF,
+ expected: { 'content-type': [' text/plain : '] },
+ what: 'should not break if invalid header pair (colon exists but empty key and value) is provided'
+ },
+ {
+ source: ['Content-Type:\r\n text/plain',
+ 'FoobazFoobazFoobazFoobazFoobazFoobazFoobazFoobazFoobazFoobazFoobazFoobazFoobazFoobazFoobazFoobazFoobazFoobazFoobazFoobaz'
+ ].join('\r\n') + DCRLF,
+ expected: { 'content-type': [' text/plain'] },
+ what: 'should not break if invalid header pair (no distinctive colon) is provided'
+ },
+ {
+ source: ['Content-Type:\r\n text/plain',
+ ':FoobazFoobazFoobazFoobazFoobazFoobazFoobazFoobazFoobazFoobazFoobazFoobazFoobazFoobazFoobazFoobazFoobazFoobazFoobazFoobaz'
+ ].join('\r\n') + DCRLF,
+ expected: { 'content-type': [' text/plain'] },
+ what: 'should not break if invalid header pair (no key) is provided'
+ },
+ {
+ source: ['Content-Type:\t text/plain',
+ 'Content-Length:0'
+ ].join('\r\n') + DCRLF,
+ cfg: {
+ maxHeaderPairs: 2
+ },
+ expected: { 'content-type': [' text/plain'], 'content-length': ['0'] },
+ what: 'should enforce maxHeaderPairs of 2'
+ },
+ {
+ source: ['Content-Type:\r\n text/plain',
+ 'Foo:\r\n bar\r\n baz'
+ ].join('\r\n') + DCRLF,
+ expected: { 'content-type': [' text/plain'], foo: [' bar baz'] },
+ what: 'Folded values'
+ },
+ {
+ source: [
+ 'Foo: bar',
+ 'Foo: baz'
+ ].join('\r\n') + DCRLF,
+ expected: { foo: ['bar', 'baz'] },
+ what: 'Folded values'
+ },
+ {
+ source: ['Content-Type:',
+ 'Foo: '
+ ].join('\r\n') + DCRLF,
+ expected: { 'content-type': [''], foo: [''] },
+ what: 'Empty values'
+ },
+ {
+ source: MAXED_BUFFER.toString('ascii') + DCRLF,
+ expected: {},
+ what: 'Max header size (single chunk)'
+ },
+ {
+ source: ['ABCDEFGHIJ', MAXED_BUFFER.toString('ascii'), DCRLF],
+ expected: {},
+ what: 'Max header size (multiple chunks #1)'
+ },
+ {
+ source: [MAXED_BUFFER.toString('ascii'), MAXED_BUFFER.toString('ascii'), DCRLF],
+ expected: {},
+ what: 'Max header size (multiple chunk #2)'
+ }
+ ]
+
+ t.plan(tests.length)
+
+ tests.forEach(function (v) {
+ t.test(v.what, t => {
+ t.plan(4)
+
+ const cfg = {
+ ...v.cfg
+ }
+
+ const parser = Object.keys(cfg).length ? new HeaderParser(cfg) : new HeaderParser()
+ let fired = false
+
+ parser.on('header', function (header) {
+ t.ok(!fired, `${v.what}: Header event fired more than once`)
+ fired = true
+ t.strictSame(header,
+ v.expected,
+ `${v.what}: Parsed result mismatch`)
+ })
+ if (!Array.isArray(v.source)) { v.source = [v.source] }
+ v.source.forEach(function (s) {
+ parser.push(s)
+ })
+ t.ok(fired, `${v.what}: Did not receive header from parser`)
+ t.pass()
+ })
+ })
+})
diff --git a/fastify-busboy/test/dicer-malformed-header.test.js b/fastify-busboy/test/dicer-malformed-header.test.js
new file mode 100644
index 0000000..c25ccdd
--- /dev/null
+++ b/fastify-busboy/test/dicer-malformed-header.test.js
@@ -0,0 +1,29 @@
+'use strict'
+
+const { test } = require('tap')
+const Dicer = require('../deps/dicer/lib/Dicer')
+
+test('dicer-malformed-header', t => {
+ t.plan(1)
+
+ t.test('should gracefully handle headers with leading whitespace', t => {
+ t.plan(3)
+ const d = new Dicer({ boundary: '----WebKitFormBoundaryoo6vortfDzBsDiro' })
+
+ d.on('part', function (p) {
+ p.on('header', function (header) {
+ t.hasProp(header, ' content-disposition')
+ t.strictSame(header[' content-disposition'], ['form-data; name="bildbeschreibung"'])
+ })
+ p.on('data', function (data) {
+ })
+ p.on('end', function () {
+ })
+ })
+ d.on('finish', function () {
+ t.pass()
+ })
+
+ d.write(Buffer.from('------WebKitFormBoundaryoo6vortfDzBsDiro\r\n Content-Disposition: form-data; name="bildbeschreibung"\r\n\r\n\r\n------WebKitFormBoundaryoo6vortfDzBsDiro--'))
+ })
+})
diff --git a/fastify-busboy/test/dicer-multipart-extra-trailer.test.js b/fastify-busboy/test/dicer-multipart-extra-trailer.test.js
new file mode 100644
index 0000000..335605a
--- /dev/null
+++ b/fastify-busboy/test/dicer-multipart-extra-trailer.test.js
@@ -0,0 +1,82 @@
+'use strict'
+
+const { test } = require('tap')
+const Dicer = require('../deps/dicer/lib/Dicer')
+const fs = require('fs')
+const path = require('path')
+
+const FIXTURES_ROOT = path.join(__dirname, 'fixtures/')
+
+test('dicer-multipart-extra-trailer', t => {
+ t.plan(1)
+
+ t.test('Extra trailer data pushed after finished', t => {
+ t.plan(5)
+ const fixtureBase = FIXTURES_ROOT + 'many'
+ let n = 0
+ const buffer = Buffer.allocUnsafe(16)
+ const state = { parts: [] }
+
+ const fd = fs.openSync(fixtureBase + '/original', 'r')
+
+ const dicer = new Dicer({ boundary: '----WebKitFormBoundaryWLHCs9qmcJJoyjKR' })
+ let error
+ let finishes = 0
+ let trailerEmitted = false
+
+ dicer.on('part', function (p) {
+ const part = {
+ body: undefined,
+ bodylen: 0,
+ error: undefined,
+ header: undefined
+ }
+
+ p.on('header', function (h) {
+ part.header = h
+ }).on('data', function (data) {
+ // make a copy because we are using readSync which re-uses a buffer ...
+ const copy = Buffer.allocUnsafe(data.length)
+ data.copy(copy)
+ data = copy
+ if (!part.body) { part.body = [data] } else { part.body.push(data) }
+ part.bodylen += data.length
+ }).on('error', function (err) {
+ part.error = err
+ t.fail()
+ }).on('end', function () {
+ if (part.body) { part.body = Buffer.concat(part.body, part.bodylen) }
+ state.parts.push(part)
+ })
+ }).on('error', function (err) {
+ error = err
+ }).on('trailer', function (data) {
+ trailerEmitted = true
+ t.equal(data.toString(), 'Extra', 'trailer should contain the extra data')
+ }).on('finish', function () {
+ t.ok(finishes++ === 0, makeMsg('Extra trailer data pushed after finished', 'finish emitted multiple times'))
+ t.ok(trailerEmitted, makeMsg('Extra trailer data pushed after finished', 'should have emitted trailer'))
+
+ t.ok(error === undefined, makeMsg('Extra trailer data pushed after finished', 'Unexpected error'))
+
+ t.pass()
+ })
+
+ while (true) {
+ n = fs.readSync(fd, buffer, 0, buffer.length, null)
+ if (n === 0) {
+ setTimeout(function () {
+ dicer.write('\r\n\r\n\r\n')
+ dicer.end()
+ }, 50)
+ break
+ }
+ dicer.write(n === buffer.length ? buffer : buffer.slice(0, n))
+ }
+ fs.closeSync(fd)
+ })
+})
+
+function makeMsg (what, msg) {
+ return what + ': ' + msg
+}
diff --git a/fastify-busboy/test/dicer-multipart-nolisteners.test.js b/fastify-busboy/test/dicer-multipart-nolisteners.test.js
new file mode 100644
index 0000000..1e311ba
--- /dev/null
+++ b/fastify-busboy/test/dicer-multipart-nolisteners.test.js
@@ -0,0 +1,44 @@
+'use strict'
+
+const Dicer = require('../deps/dicer/lib/Dicer')
+const { test } = require('tap')
+const fs = require('fs')
+const path = require('path')
+
+const FIXTURES_ROOT = path.join(__dirname, 'fixtures/')
+
+test('dicer-multipart-nolisteners', t => {
+ t.plan(1)
+
+ t.test('No preamble or part listeners', t => {
+ t.plan(3)
+ const fixtureBase = path.resolve(FIXTURES_ROOT, 'many')
+ let n = 0
+ const buffer = Buffer.allocUnsafe(16)
+
+ const fd = fs.openSync(fixtureBase + '/original', 'r')
+
+ const dicer = new Dicer({ boundary: '----WebKitFormBoundaryWLHCs9qmcJJoyjKR' })
+ let error
+ let finishes = 0
+
+ dicer.on('error', function (err) {
+ error = err
+ }).on('finish', function () {
+ t.ok(finishes++ === 0, 'finish emitted multiple times')
+
+ t.ok(error === undefined, `Unexpected error: ${error}`)
+ t.pass()
+ })
+
+ while (true) {
+ n = fs.readSync(fd, buffer, 0, buffer.length, null)
+ if (n === 0) {
+ dicer.end()
+ break
+ }
+ dicer.write(n === buffer.length ? buffer : buffer.slice(0, n))
+ }
+ fs.closeSync(fd)
+ })
+})
diff --git a/fastify-busboy/test/dicer-multipart.test.js b/fastify-busboy/test/dicer-multipart.test.js
new file mode 100644
index 0000000..c35c4d0
--- /dev/null
+++ b/fastify-busboy/test/dicer-multipart.test.js
@@ -0,0 +1,223 @@
+'use strict'
+
+const Dicer = require('../deps/dicer/lib/Dicer')
+const assert = require('node:assert')
+const fs = require('node:fs')
+const path = require('node:path')
+const inspect = require('node:util').inspect
+const { test } = require('tap')
+
+const FIXTURES_ROOT = path.join(__dirname, 'fixtures/')
+
+test('dicer-multipart', t => {
+ const tests =
+ [
+ {
+ source: 'nested',
+ opts: { boundary: 'AaB03x' },
+ chsize: 32,
+ nparts: 2,
+ what: 'One nested multipart'
+ },
+ {
+ source: 'many',
+ opts: { boundary: '----WebKitFormBoundaryWLHCs9qmcJJoyjKR' },
+ chsize: 16,
+ nparts: 7,
+ what: 'Many parts'
+ },
+ {
+ source: 'many-wrongboundary',
+ opts: { boundary: 'LOLOLOL' },
+ chsize: 8,
+ nparts: 0,
+ dicerError: true,
+ what: 'Many parts, wrong boundary'
+ },
+ {
+ source: 'many-noend',
+ opts: { boundary: '----WebKitFormBoundaryWLHCs9qmcJJoyjKR' },
+ chsize: 16,
+ nparts: 7,
+ npartErrors: 1,
+ dicerError: true,
+ what: 'Many parts, end boundary missing, 1 file open'
+ },
+ {
+ source: 'nested-full',
+ opts: { boundary: 'AaB03x', headerFirst: true },
+ chsize: 32,
+ nparts: 2,
+ what: 'One nested multipart with preceding header'
+ },
+ {
+ source: 'nested-full',
+ opts: { headerFirst: true },
+ chsize: 32,
+ nparts: 2,
+ setBoundary: 'AaB03x',
+ what: 'One nested multipart with preceding header, using setBoundary'
+ }
+ ]
+
+ t.plan(tests.length)
+
+ tests.forEach(function (v) {
+ t.test(v.what, t => {
+ t.plan(1)
+ const fixtureBase = FIXTURES_ROOT + v.source
+ const state = { parts: [], preamble: undefined }
+
+ const dicer = new Dicer(v.opts)
+ let error
+ let partErrors = 0
+ let finishes = 0
+
+ dicer.on('preamble', function (p) {
+ const preamble = {
+ body: undefined,
+ bodylen: 0,
+ error: undefined,
+ header: undefined
+ }
+
+ p.on('header', function (h) {
+ preamble.header = h
+ if (v.setBoundary) { dicer.setBoundary(v.setBoundary) }
+ }).on('data', function (data) {
+ // make a copy because we are using readSync which re-uses a buffer ...
+ const copy = Buffer.allocUnsafe(data.length)
+ data.copy(copy)
+ data = copy
+ if (!preamble.body) { preamble.body = [data] } else { preamble.body.push(data) }
+ preamble.bodylen += data.length
+ }).on('error', function (err) {
+ preamble.error = err
+ }).on('end', function () {
+ if (preamble.body) { preamble.body = Buffer.concat(preamble.body, preamble.bodylen) }
+ if (preamble.body || preamble.header) { state.preamble = preamble }
+ })
+ })
+ dicer.on('part', function (p) {
+ const part = {
+ body: undefined,
+ bodylen: 0,
+ error: undefined,
+ header: undefined
+ }
+
+ p.on('header', function (h) {
+ part.header = h
+ }).on('data', function (data) {
+ if (!part.body) { part.body = [data] } else { part.body.push(data) }
+ part.bodylen += data.length
+ }).on('error', function (err) {
+ part.error = err
+ ++partErrors
+ }).on('end', function () {
+ if (part.body) { part.body = Buffer.concat(part.body, part.bodylen) }
+ state.parts.push(part)
+ })
+ }).on('error', function (err) {
+ error = err
+ }).on('finish', function () {
+ assert(finishes++ === 0, makeMsg(v.what, 'finish emitted multiple times'))
+
+ if (v.dicerError) { assert(error !== undefined, makeMsg(v.what, 'Expected error')) } else { assert(error === undefined, makeMsg(v.what, 'Unexpected error: ' + error)) }
+
+ let preamble
+ if (fs.existsSync(fixtureBase + '/preamble')) {
+ const prebody = fs.readFileSync(fixtureBase + '/preamble')
+ if (prebody.length) {
+ preamble = {
+ body: prebody,
+ bodylen: prebody.length,
+ error: undefined,
+ header: undefined
+ }
+ }
+ }
+ if (fs.existsSync(fixtureBase + '/preamble.header')) {
+ const prehead = JSON.parse(fs.readFileSync(fixtureBase +
+ '/preamble.header', 'binary'))
+ if (!preamble) {
+ preamble = {
+ body: undefined,
+ bodylen: 0,
+ error: undefined,
+ header: prehead
+ }
+ } else { preamble.header = prehead }
+ }
+ if (fs.existsSync(fixtureBase + '/preamble.error')) {
+ const err = new Error(fs.readFileSync(fixtureBase +
+ '/preamble.error', 'binary'))
+ if (!preamble) {
+ preamble = {
+ body: undefined,
+ bodylen: 0,
+ error: err,
+ header: undefined
+ }
+ } else { preamble.error = err }
+ }
+
+ assert.deepEqual(state.preamble,
+ preamble,
+ makeMsg(v.what,
+ 'Preamble mismatch:\nActual:' +
+ inspect(state.preamble) +
+ '\nExpected: ' +
+ inspect(preamble)))
+
+ assert.equal(state.parts.length,
+ v.nparts,
+ makeMsg(v.what,
+ 'Part count mismatch:\nActual: ' +
+ state.parts.length +
+ '\nExpected: ' +
+ v.nparts))
+
+ if (!v.npartErrors) { v.npartErrors = 0 }
+ assert.equal(partErrors,
+ v.npartErrors,
+ makeMsg(v.what,
+ 'Part errors mismatch:\nActual: ' +
+ partErrors +
+ '\nExpected: ' +
+ v.npartErrors))
+
+ for (let i = 0, header, body; i < v.nparts; ++i) {
+ if (fs.existsSync(fixtureBase + '/part' + (i + 1))) {
+ body = fs.readFileSync(fixtureBase + '/part' + (i + 1))
+ if (body.length === 0) { body = undefined }
+ } else { body = undefined }
+ assert.deepEqual(state.parts[i].body,
+ body,
+ makeMsg(v.what,
+ 'Part #' + (i + 1) + ' body mismatch'))
+ if (fs.existsSync(fixtureBase + '/part' + (i + 1) + '.header')) {
+ header = fs.readFileSync(fixtureBase +
+ '/part' + (i + 1) + '.header', 'binary')
+ header = JSON.parse(header)
+ } else { header = undefined }
+ assert.deepEqual(state.parts[i].header,
+ header,
+ makeMsg(v.what,
+ 'Part #' + (i + 1) +
+ ' parsed header mismatch:\nActual: ' +
+ inspect(state.parts[i].header) +
+ '\nExpected: ' +
+ inspect(header)))
+ }
+ t.pass()
+ })
+
+ fs.createReadStream(fixtureBase + '/original').pipe(dicer)
+ })
+ })
+})
+
+function makeMsg (what, msg) {
+ return what + ': ' + msg
+}
diff --git a/fastify-busboy/test/fixtures/many-noend/original b/fastify-busboy/test/fixtures/many-noend/original
new file mode 100644
index 0000000..ad9f0cc
--- /dev/null
+++ b/fastify-busboy/test/fixtures/many-noend/original
@@ -0,0 +1,31 @@
+------WebKitFormBoundaryWLHCs9qmcJJoyjKR
+Content-Disposition: form-data; name="_method"
+
+put
+------WebKitFormBoundaryWLHCs9qmcJJoyjKR
+Content-Disposition: form-data; name="profile[blog]"
+
+
+------WebKitFormBoundaryWLHCs9qmcJJoyjKR
+Content-Disposition: form-data; name="profile[public_email]"
+
+
+------WebKitFormBoundaryWLHCs9qmcJJoyjKR
+Content-Disposition: form-data; name="profile[interests]"
+
+
+------WebKitFormBoundaryWLHCs9qmcJJoyjKR
+Content-Disposition: form-data; name="profile[bio]"
+
+hello
+
+"quote"
+------WebKitFormBoundaryWLHCs9qmcJJoyjKR
+Content-Disposition: form-data; name="commit"
+
+Save
+------WebKitFormBoundaryWLHCs9qmcJJoyjKR
+Content-Disposition: form-data; name="media"; filename=""
+Content-Type: application/octet-stream
+
+
diff --git a/fastify-busboy/test/fixtures/many-noend/part1 b/fastify-busboy/test/fixtures/many-noend/part1
new file mode 100644
index 0000000..a232311
--- /dev/null
+++ b/fastify-busboy/test/fixtures/many-noend/part1
@@ -0,0 +1 @@
+put \ No newline at end of file
diff --git a/fastify-busboy/test/fixtures/many-noend/part1.header b/fastify-busboy/test/fixtures/many-noend/part1.header
new file mode 100644
index 0000000..5e6bbe5
--- /dev/null
+++ b/fastify-busboy/test/fixtures/many-noend/part1.header
@@ -0,0 +1 @@
+{"content-disposition": ["form-data; name=\"_method\""]} \ No newline at end of file
diff --git a/fastify-busboy/test/fixtures/many-noend/part2 b/fastify-busboy/test/fixtures/many-noend/part2
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/fastify-busboy/test/fixtures/many-noend/part2
diff --git a/fastify-busboy/test/fixtures/many-noend/part2.header b/fastify-busboy/test/fixtures/many-noend/part2.header
new file mode 100644
index 0000000..5b53966
--- /dev/null
+++ b/fastify-busboy/test/fixtures/many-noend/part2.header
@@ -0,0 +1 @@
+{"content-disposition": ["form-data; name=\"profile[blog]\""]} \ No newline at end of file
diff --git a/fastify-busboy/test/fixtures/many-noend/part3 b/fastify-busboy/test/fixtures/many-noend/part3
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/fastify-busboy/test/fixtures/many-noend/part3
diff --git a/fastify-busboy/test/fixtures/many-noend/part3.header b/fastify-busboy/test/fixtures/many-noend/part3.header
new file mode 100644
index 0000000..579e16e
--- /dev/null
+++ b/fastify-busboy/test/fixtures/many-noend/part3.header
@@ -0,0 +1 @@
+{"content-disposition": ["form-data; name=\"profile[public_email]\""]} \ No newline at end of file
diff --git a/fastify-busboy/test/fixtures/many-noend/part4 b/fastify-busboy/test/fixtures/many-noend/part4
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/fastify-busboy/test/fixtures/many-noend/part4
diff --git a/fastify-busboy/test/fixtures/many-noend/part4.header b/fastify-busboy/test/fixtures/many-noend/part4.header
new file mode 100644
index 0000000..b41be09
--- /dev/null
+++ b/fastify-busboy/test/fixtures/many-noend/part4.header
@@ -0,0 +1 @@
+{"content-disposition": ["form-data; name=\"profile[interests]\""]} \ No newline at end of file
diff --git a/fastify-busboy/test/fixtures/many-noend/part5 b/fastify-busboy/test/fixtures/many-noend/part5
new file mode 100644
index 0000000..f2bb979
--- /dev/null
+++ b/fastify-busboy/test/fixtures/many-noend/part5
@@ -0,0 +1,3 @@
+hello
+
+"quote" \ No newline at end of file
diff --git a/fastify-busboy/test/fixtures/many-noend/part5.header b/fastify-busboy/test/fixtures/many-noend/part5.header
new file mode 100644
index 0000000..92e417f
--- /dev/null
+++ b/fastify-busboy/test/fixtures/many-noend/part5.header
@@ -0,0 +1 @@
+{"content-disposition": ["form-data; name=\"profile[bio]\""]} \ No newline at end of file
diff --git a/fastify-busboy/test/fixtures/many-noend/part6 b/fastify-busboy/test/fixtures/many-noend/part6
new file mode 100644
index 0000000..f0f5479
--- /dev/null
+++ b/fastify-busboy/test/fixtures/many-noend/part6
@@ -0,0 +1 @@
+Save \ No newline at end of file
diff --git a/fastify-busboy/test/fixtures/many-noend/part6.header b/fastify-busboy/test/fixtures/many-noend/part6.header
new file mode 100644
index 0000000..65a68a9
--- /dev/null
+++ b/fastify-busboy/test/fixtures/many-noend/part6.header
@@ -0,0 +1 @@
+{"content-disposition": ["form-data; name=\"commit\""]} \ No newline at end of file
diff --git a/fastify-busboy/test/fixtures/many-noend/part7.header b/fastify-busboy/test/fixtures/many-noend/part7.header
new file mode 100644
index 0000000..25171e8
--- /dev/null
+++ b/fastify-busboy/test/fixtures/many-noend/part7.header
@@ -0,0 +1,2 @@
+{"content-disposition": ["form-data; name=\"media\"; filename=\"\""],
+ "content-type": ["application/octet-stream"]} \ No newline at end of file
diff --git a/fastify-busboy/test/fixtures/many-wrongboundary/original b/fastify-busboy/test/fixtures/many-wrongboundary/original
new file mode 100644
index 0000000..859770c
--- /dev/null
+++ b/fastify-busboy/test/fixtures/many-wrongboundary/original
@@ -0,0 +1,32 @@
+------WebKitFormBoundaryWLHCs9qmcJJoyjKR
+Content-Disposition: form-data; name="_method"
+
+put
+------WebKitFormBoundaryWLHCs9qmcJJoyjKR
+Content-Disposition: form-data; name="profile[blog]"
+
+
+------WebKitFormBoundaryWLHCs9qmcJJoyjKR
+Content-Disposition: form-data; name="profile[public_email]"
+
+
+------WebKitFormBoundaryWLHCs9qmcJJoyjKR
+Content-Disposition: form-data; name="profile[interests]"
+
+
+------WebKitFormBoundaryWLHCs9qmcJJoyjKR
+Content-Disposition: form-data; name="profile[bio]"
+
+hello
+
+"quote"
+------WebKitFormBoundaryWLHCs9qmcJJoyjKR
+Content-Disposition: form-data; name="media"; filename=""
+Content-Type: application/octet-stream
+
+
+------WebKitFormBoundaryWLHCs9qmcJJoyjKR
+Content-Disposition: form-data; name="commit"
+
+Save
+------WebKitFormBoundaryWLHCs9qmcJJoyjKR-- \ No newline at end of file
diff --git a/fastify-busboy/test/fixtures/many-wrongboundary/preamble b/fastify-busboy/test/fixtures/many-wrongboundary/preamble
new file mode 100644
index 0000000..6e4bcc6
--- /dev/null
+++ b/fastify-busboy/test/fixtures/many-wrongboundary/preamble
@@ -0,0 +1,33 @@
+
+------WebKitFormBoundaryWLHCs9qmcJJoyjKR
+Content-Disposition: form-data; name="_method"
+
+put
+------WebKitFormBoundaryWLHCs9qmcJJoyjKR
+Content-Disposition: form-data; name="profile[blog]"
+
+
+------WebKitFormBoundaryWLHCs9qmcJJoyjKR
+Content-Disposition: form-data; name="profile[public_email]"
+
+
+------WebKitFormBoundaryWLHCs9qmcJJoyjKR
+Content-Disposition: form-data; name="profile[interests]"
+
+
+------WebKitFormBoundaryWLHCs9qmcJJoyjKR
+Content-Disposition: form-data; name="profile[bio]"
+
+hello
+
+"quote"
+------WebKitFormBoundaryWLHCs9qmcJJoyjKR
+Content-Disposition: form-data; name="media"; filename=""
+Content-Type: application/octet-stream
+
+
+------WebKitFormBoundaryWLHCs9qmcJJoyjKR
+Content-Disposition: form-data; name="commit"
+
+Save
+------WebKitFormBoundaryWLHCs9qmcJJoyjKR-- \ No newline at end of file
diff --git a/fastify-busboy/test/fixtures/many-wrongboundary/preamble.error b/fastify-busboy/test/fixtures/many-wrongboundary/preamble.error
new file mode 100644
index 0000000..15f4c89
--- /dev/null
+++ b/fastify-busboy/test/fixtures/many-wrongboundary/preamble.error
@@ -0,0 +1 @@
+Preamble terminated early due to unexpected end of multipart data \ No newline at end of file
diff --git a/fastify-busboy/test/fixtures/many/original b/fastify-busboy/test/fixtures/many/original
new file mode 100644
index 0000000..779c5cb
--- /dev/null
+++ b/fastify-busboy/test/fixtures/many/original
@@ -0,0 +1,32 @@
+------WebKitFormBoundaryWLHCs9qmcJJoyjKR
+Content-Disposition: form-data; name="_method"
+
+put
+------WebKitFormBoundaryWLHCs9qmcJJoyjKR
+Content-Disposition: form-data; name="profile[blog]"
+
+
+------WebKitFormBoundaryWLHCs9qmcJJoyjKR
+Content-Disposition: form-data; name="profile[public_email]"
+
+
+------WebKitFormBoundaryWLHCs9qmcJJoyjKR
+Content-Disposition: form-data; name="profile[interests]"
+
+
+------WebKitFormBoundaryWLHCs9qmcJJoyjKR
+Content-Disposition: form-data; name="profile[bio]"
+
+hello
+
+"quote"
+------WebKitFormBoundaryWLHCs9qmcJJoyjKR
+Content-Disposition: form-data; name="media"; filename=""
+Content-Type: application/octet-stream
+
+
+------WebKitFormBoundaryWLHCs9qmcJJoyjKR
+Content-Disposition: form-data; name="commit"
+
+Save
+------WebKitFormBoundaryWLHCs9qmcJJoyjKR--Extra \ No newline at end of file
diff --git a/fastify-busboy/test/fixtures/many/part1 b/fastify-busboy/test/fixtures/many/part1
new file mode 100644
index 0000000..a232311
--- /dev/null
+++ b/fastify-busboy/test/fixtures/many/part1
@@ -0,0 +1 @@
+put \ No newline at end of file
diff --git a/fastify-busboy/test/fixtures/many/part1.header b/fastify-busboy/test/fixtures/many/part1.header
new file mode 100644
index 0000000..5e6bbe5
--- /dev/null
+++ b/fastify-busboy/test/fixtures/many/part1.header
@@ -0,0 +1 @@
+{"content-disposition": ["form-data; name=\"_method\""]} \ No newline at end of file
diff --git a/fastify-busboy/test/fixtures/many/part2 b/fastify-busboy/test/fixtures/many/part2
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/fastify-busboy/test/fixtures/many/part2
diff --git a/fastify-busboy/test/fixtures/many/part2.header b/fastify-busboy/test/fixtures/many/part2.header
new file mode 100644
index 0000000..5b53966
--- /dev/null
+++ b/fastify-busboy/test/fixtures/many/part2.header
@@ -0,0 +1 @@
+{"content-disposition": ["form-data; name=\"profile[blog]\""]} \ No newline at end of file
diff --git a/fastify-busboy/test/fixtures/many/part3 b/fastify-busboy/test/fixtures/many/part3
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/fastify-busboy/test/fixtures/many/part3
diff --git a/fastify-busboy/test/fixtures/many/part3.header b/fastify-busboy/test/fixtures/many/part3.header
new file mode 100644
index 0000000..579e16e
--- /dev/null
+++ b/fastify-busboy/test/fixtures/many/part3.header
@@ -0,0 +1 @@
+{"content-disposition": ["form-data; name=\"profile[public_email]\""]} \ No newline at end of file
diff --git a/fastify-busboy/test/fixtures/many/part4 b/fastify-busboy/test/fixtures/many/part4
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/fastify-busboy/test/fixtures/many/part4
diff --git a/fastify-busboy/test/fixtures/many/part4.header b/fastify-busboy/test/fixtures/many/part4.header
new file mode 100644
index 0000000..b41be09
--- /dev/null
+++ b/fastify-busboy/test/fixtures/many/part4.header
@@ -0,0 +1 @@
+{"content-disposition": ["form-data; name=\"profile[interests]\""]} \ No newline at end of file
diff --git a/fastify-busboy/test/fixtures/many/part5 b/fastify-busboy/test/fixtures/many/part5
new file mode 100644
index 0000000..f2bb979
--- /dev/null
+++ b/fastify-busboy/test/fixtures/many/part5
@@ -0,0 +1,3 @@
+hello
+
+"quote" \ No newline at end of file
diff --git a/fastify-busboy/test/fixtures/many/part5.header b/fastify-busboy/test/fixtures/many/part5.header
new file mode 100644
index 0000000..92e417f
--- /dev/null
+++ b/fastify-busboy/test/fixtures/many/part5.header
@@ -0,0 +1 @@
+{"content-disposition": ["form-data; name=\"profile[bio]\""]} \ No newline at end of file
diff --git a/fastify-busboy/test/fixtures/many/part6 b/fastify-busboy/test/fixtures/many/part6
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/fastify-busboy/test/fixtures/many/part6
diff --git a/fastify-busboy/test/fixtures/many/part6.header b/fastify-busboy/test/fixtures/many/part6.header
new file mode 100644
index 0000000..25171e8
--- /dev/null
+++ b/fastify-busboy/test/fixtures/many/part6.header
@@ -0,0 +1,2 @@
+{"content-disposition": ["form-data; name=\"media\"; filename=\"\""],
+ "content-type": ["application/octet-stream"]} \ No newline at end of file
diff --git a/fastify-busboy/test/fixtures/many/part7 b/fastify-busboy/test/fixtures/many/part7
new file mode 100644
index 0000000..f0f5479
--- /dev/null
+++ b/fastify-busboy/test/fixtures/many/part7
@@ -0,0 +1 @@
+Save \ No newline at end of file
diff --git a/fastify-busboy/test/fixtures/many/part7.header b/fastify-busboy/test/fixtures/many/part7.header
new file mode 100644
index 0000000..65a68a9
--- /dev/null
+++ b/fastify-busboy/test/fixtures/many/part7.header
@@ -0,0 +1 @@
+{"content-disposition": ["form-data; name=\"commit\""]} \ No newline at end of file
diff --git a/fastify-busboy/test/fixtures/nested-full/original b/fastify-busboy/test/fixtures/nested-full/original
new file mode 100644
index 0000000..3044550
--- /dev/null
+++ b/fastify-busboy/test/fixtures/nested-full/original
@@ -0,0 +1,24 @@
+User-Agent: foo bar baz
+Content-Type: multipart/form-data; boundary=AaB03x
+
+--AaB03x
+Content-Disposition: form-data; name="foo"
+
+bar
+--AaB03x
+Content-Disposition: form-data; name="files"
+Content-Type: multipart/mixed, boundary=BbC04y
+
+--BbC04y
+Content-Disposition: attachment; filename="file.txt"
+Content-Type: text/plain
+
+contents
+--BbC04y
+Content-Disposition: attachment; filename="flowers.jpg"
+Content-Type: image/jpeg
+Content-Transfer-Encoding: binary
+
+contents
+--BbC04y--
+--AaB03x-- \ No newline at end of file
diff --git a/fastify-busboy/test/fixtures/nested-full/part1 b/fastify-busboy/test/fixtures/nested-full/part1
new file mode 100644
index 0000000..ba0e162
--- /dev/null
+++ b/fastify-busboy/test/fixtures/nested-full/part1
@@ -0,0 +1 @@
+bar \ No newline at end of file
diff --git a/fastify-busboy/test/fixtures/nested-full/part1.header b/fastify-busboy/test/fixtures/nested-full/part1.header
new file mode 100644
index 0000000..03bd093
--- /dev/null
+++ b/fastify-busboy/test/fixtures/nested-full/part1.header
@@ -0,0 +1 @@
+{"content-disposition": ["form-data; name=\"foo\""]}
diff --git a/fastify-busboy/test/fixtures/nested-full/part2 b/fastify-busboy/test/fixtures/nested-full/part2
new file mode 100644
index 0000000..2d4deb5
--- /dev/null
+++ b/fastify-busboy/test/fixtures/nested-full/part2
@@ -0,0 +1,12 @@
+--BbC04y
+Content-Disposition: attachment; filename="file.txt"
+Content-Type: text/plain
+
+contents
+--BbC04y
+Content-Disposition: attachment; filename="flowers.jpg"
+Content-Type: image/jpeg
+Content-Transfer-Encoding: binary
+
+contents
+--BbC04y-- \ No newline at end of file
diff --git a/fastify-busboy/test/fixtures/nested-full/part2.header b/fastify-busboy/test/fixtures/nested-full/part2.header
new file mode 100644
index 0000000..bbe4513
--- /dev/null
+++ b/fastify-busboy/test/fixtures/nested-full/part2.header
@@ -0,0 +1,2 @@
+{"content-disposition": ["form-data; name=\"files\""],
+ "content-type": ["multipart/mixed, boundary=BbC04y"]} \ No newline at end of file
diff --git a/fastify-busboy/test/fixtures/nested-full/preamble.header b/fastify-busboy/test/fixtures/nested-full/preamble.header
new file mode 100644
index 0000000..2815341
--- /dev/null
+++ b/fastify-busboy/test/fixtures/nested-full/preamble.header
@@ -0,0 +1,2 @@
+{"user-agent": ["foo bar baz"],
+ "content-type": ["multipart/form-data; boundary=AaB03x"]} \ No newline at end of file
diff --git a/fastify-busboy/test/fixtures/nested/original b/fastify-busboy/test/fixtures/nested/original
new file mode 100644
index 0000000..380f451
--- /dev/null
+++ b/fastify-busboy/test/fixtures/nested/original
@@ -0,0 +1,21 @@
+--AaB03x
+Content-Disposition: form-data; name="foo"
+
+bar
+--AaB03x
+Content-Disposition: form-data; name="files"
+Content-Type: multipart/mixed, boundary=BbC04y
+
+--BbC04y
+Content-Disposition: attachment; filename="file.txt"
+Content-Type: text/plain
+
+contents
+--BbC04y
+Content-Disposition: attachment; filename="flowers.jpg"
+Content-Type: image/jpeg
+Content-Transfer-Encoding: binary
+
+contents
+--BbC04y--
+--AaB03x-- \ No newline at end of file
diff --git a/fastify-busboy/test/fixtures/nested/part1 b/fastify-busboy/test/fixtures/nested/part1
new file mode 100644
index 0000000..ba0e162
--- /dev/null
+++ b/fastify-busboy/test/fixtures/nested/part1
@@ -0,0 +1 @@
+bar \ No newline at end of file
diff --git a/fastify-busboy/test/fixtures/nested/part1.header b/fastify-busboy/test/fixtures/nested/part1.header
new file mode 100644
index 0000000..03bd093
--- /dev/null
+++ b/fastify-busboy/test/fixtures/nested/part1.header
@@ -0,0 +1 @@
+{"content-disposition": ["form-data; name=\"foo\""]}
diff --git a/fastify-busboy/test/fixtures/nested/part2 b/fastify-busboy/test/fixtures/nested/part2
new file mode 100644
index 0000000..2d4deb5
--- /dev/null
+++ b/fastify-busboy/test/fixtures/nested/part2
@@ -0,0 +1,12 @@
+--BbC04y
+Content-Disposition: attachment; filename="file.txt"
+Content-Type: text/plain
+
+contents
+--BbC04y
+Content-Disposition: attachment; filename="flowers.jpg"
+Content-Type: image/jpeg
+Content-Transfer-Encoding: binary
+
+contents
+--BbC04y-- \ No newline at end of file
diff --git a/fastify-busboy/test/fixtures/nested/part2.header b/fastify-busboy/test/fixtures/nested/part2.header
new file mode 100644
index 0000000..bbe4513
--- /dev/null
+++ b/fastify-busboy/test/fixtures/nested/part2.header
@@ -0,0 +1,2 @@
+{"content-disposition": ["form-data; name=\"files\""],
+ "content-type": ["multipart/mixed, boundary=BbC04y"]} \ No newline at end of file
diff --git a/fastify-busboy/test/get-limit.test.js b/fastify-busboy/test/get-limit.test.js
new file mode 100644
index 0000000..76a2997
--- /dev/null
+++ b/fastify-busboy/test/get-limit.test.js
@@ -0,0 +1,34 @@
+'use strict'
+
+const getLimit = require('../lib/utils/getLimit')
+const { test } = require('tap')
+
+test('Get limit', t => {
+ t.plan(2)
+
+ t.test('Correctly resolves limits', t => {
+ t.plan(8)
+ t.strictSame(getLimit(undefined, 'fieldSize', 1), 1)
+ t.strictSame(getLimit(undefined, 'fileSize', Infinity), Infinity)
+
+ t.strictSame(getLimit({}, 'fieldSize', 1), 1)
+ t.strictSame(getLimit({}, 'fileSize', Infinity), Infinity)
+ t.strictSame(getLimit({ fieldSize: null }, 'fieldSize', 1), 1)
+ t.strictSame(getLimit({ fileSize: null }, 'fileSize', Infinity), Infinity)
+
+ t.strictSame(getLimit({ fieldSize: 0 }, 'fieldSize', 1), 0)
+ t.strictSame(getLimit({ fileSize: 2 }, 'fileSize', 1), 2)
+ })
+
+ t.test('Throws an error on incorrect limits', t => {
+ t.plan(2)
+
+ t.throws(function () {
+ getLimit({ fieldSize: '1' }, 'fieldSize', 1)
+ }, new Error('Limit fieldSize is not a valid number'))
+
+ t.throws(function () {
+ getLimit({ fieldSize: NaN }, 'fieldSize', 1)
+ }, new Error('Limit fieldSize is not a valid number'))
+ })
+})
diff --git a/fastify-busboy/test/multipart-stream-pause.test.js b/fastify-busboy/test/multipart-stream-pause.test.js
new file mode 100644
index 0000000..856cf71
--- /dev/null
+++ b/fastify-busboy/test/multipart-stream-pause.test.js
@@ -0,0 +1,82 @@
+'use strict'
+
+const { inspect } = require('util')
+const { test } = require('tap')
+
+const Busboy = require('..')
+
+const BOUNDARY = 'u2KxIV5yF1y+xUspOQCCZopaVgeV6Jxihv35XQJmuTx8X3sh'
+
+function formDataSection (key, value) {
+ return Buffer.from('\r\n--' + BOUNDARY +
+ '\r\nContent-Disposition: form-data; name="' +
+ key + '"\r\n\r\n' + value)
+}
+function formDataFile (key, filename, contentType) {
+ return Buffer.concat([
+ Buffer.from('\r\n--' + BOUNDARY + '\r\n'),
+ Buffer.from('Content-Disposition: form-data; name="' +
+ key + '"; filename="' + filename + '"\r\n'),
+ Buffer.from('Content-Type: ' + contentType + '\r\n\r\n'),
+ Buffer.allocUnsafe(100000)
+ ])
+}
+
+test('multipart-stream-pause - processes stream correctly', t => {
+ t.plan(6)
+ const reqChunks = [
+ Buffer.concat([
+ formDataFile('file', 'file.bin', 'application/octet-stream'),
+ formDataSection('foo', 'foo value')
+ ]),
+ formDataSection('bar', 'bar value'),
+ Buffer.from('\r\n--' + BOUNDARY + '--\r\n')
+ ]
+ const busboy = new Busboy({
+ headers: {
+ 'content-type': 'multipart/form-data; boundary=' + BOUNDARY
+ }
+ })
+ let finishes = 0
+ const results = []
+ const expected = [
+ ['file', 'file', 'file.bin', '7bit', 'application/octet-stream'],
+ ['field', 'foo', 'foo value', false, false, '7bit', 'text/plain'],
+ ['field', 'bar', 'bar value', false, false, '7bit', 'text/plain']
+ ]
+
+ busboy.on('field', function (key, val, keyTrunc, valTrunc, encoding, contype) {
+ results.push(['field', key, val, keyTrunc, valTrunc, encoding, contype])
+ })
+ busboy.on('file', function (fieldname, stream, filename, encoding, mimeType) {
+ results.push(['file', fieldname, filename, encoding, mimeType])
+ // Simulate a pipe where the destination is pausing (perhaps due to waiting
+ // for file system write to finish)
+ setTimeout(function () {
+ stream.resume()
+ }, 10)
+ })
+ busboy.on('finish', function () {
+ t.ok(finishes++ === 0, 'finish emitted multiple times')
+ t.strictSame(results.length,
+ expected.length,
+ 'Parsed result count mismatch. Saw ' +
+ results.length +
+ '. Expected: ' + expected.length)
+
+ results.forEach(function (result, i) {
+ t.strictSame(result,
+ expected[i],
+ 'Result mismatch:\nParsed: ' + inspect(result) +
+ '\nExpected: ' + inspect(expected[i]))
+ })
+ t.pass()
+ }).on('error', function (err) {
+ t.error(err)
+ })
+
+ reqChunks.forEach(function (buf) {
+ busboy.write(buf)
+ })
+ busboy.end()
+})
diff --git a/fastify-busboy/test/parse-params.test.js b/fastify-busboy/test/parse-params.test.js
new file mode 100644
index 0000000..eea4768
--- /dev/null
+++ b/fastify-busboy/test/parse-params.test.js
@@ -0,0 +1,124 @@
+'use strict'
+
+const { inspect } = require('node:util')
+const { test } = require('tap')
+const parseParams = require('../lib/utils/parseParams')
+
+test('parse-params', t => {
+ const tests = [
+ {
+ source: 'video/ogg',
+ expected: ['video/ogg'],
+ what: 'No parameters'
+ },
+ {
+ source: 'video/ogg;',
+ expected: ['video/ogg'],
+ what: 'No parameters (with separator)'
+ },
+ {
+ source: 'video/ogg; ',
+ expected: ['video/ogg'],
+ what: 'No parameters (with separator followed by whitespace)'
+ },
+ {
+ source: ';video/ogg',
+ expected: ['', 'video/ogg'],
+ what: 'Empty parameter'
+ },
+ {
+ source: 'video/*',
+ expected: ['video/*'],
+ what: 'Subtype with asterisk'
+ },
+ {
+ source: 'text/plain; encoding=utf8',
+ expected: ['text/plain', ['encoding', 'utf8']],
+ what: 'Unquoted'
+ },
+ {
+ source: 'text/plain; encoding=',
+ expected: ['text/plain', ['encoding', '']],
+ what: 'Unquoted empty string'
+ },
+ {
+ source: 'text/plain; encoding="utf8"',
+ expected: ['text/plain', ['encoding', 'utf8']],
+ what: 'Quoted'
+ },
+ {
+ source: 'text/plain; greeting="hello \\"world\\""',
+ expected: ['text/plain', ['greeting', 'hello "world"']],
+ what: 'Quotes within quoted'
+ },
+ {
+ source: 'text/plain; encoding=""',
+ expected: ['text/plain', ['encoding', '']],
+ what: 'Quoted empty string'
+ },
+ {
+ source: 'text/plain; encoding="utf8";\t foo=bar;test',
+ expected: ['text/plain', ['encoding', 'utf8'], ['foo', 'bar'], 'test'],
+ what: 'Multiple params with various spacing'
+ },
+ {
+ source: "text/plain; filename*=iso-8859-1'en'%A3%20rates",
+ expected: ['text/plain', ['filename', '£ rates']],
+ what: 'Extended parameter (RFC 5987) with language'
+ },
+ {
+ source: "text/plain; filename*=utf-8''%c2%a3%20and%20%e2%82%ac%20rates",
+ expected: ['text/plain', ['filename', '£ and € rates']],
+ what: 'Extended parameter (RFC 5987) without language'
+ },
+ {
+ source: "text/plain; filename*=utf-8''%E6%B5%8B%E8%AF%95%E6%96%87%E6%A1%A3",
+ expected: ['text/plain', ['filename', '测试文档']],
+ what: 'Extended parameter (RFC 5987) without language #2'
+ },
+ {
+ source: "text/plain; filename*=iso-8859-1'en'%A3%20rates; altfilename*=utf-8''%c2%a3%20and%20%e2%82%ac%20rates",
+ expected: ['text/plain', ['filename', '£ rates'], ['altfilename', '£ and € rates']],
+ what: 'Multiple extended parameters (RFC 5987) with mixed charsets'
+ },
+ {
+ source: "text/plain; filename*=iso-8859-1'en'%A3%20rates; altfilename=\"foobarbaz\"",
+ expected: ['text/plain', ['filename', '£ rates'], ['altfilename', 'foobarbaz']],
+ what: 'Mixed regular and extended parameters (RFC 5987)'
+ },
+ {
+ source: "text/plain; filename=\"foobarbaz\"; altfilename*=iso-8859-1'en'%A3%20rates",
+ expected: ['text/plain', ['filename', 'foobarbaz'], ['altfilename', '£ rates']],
+ what: 'Mixed regular and extended parameters (RFC 5987) #2'
+ },
+ {
+ source: 'text/plain; filename="C:\\folder\\test.png"',
+ expected: ['text/plain', ['filename', 'C:\\folder\\test.png']],
+ what: 'Unescaped backslashes should be considered backslashes'
+ },
+ {
+ source: 'text/plain; filename="John \\"Magic\\" Smith.png"',
+ expected: ['text/plain', ['filename', 'John "Magic" Smith.png']],
+ what: 'Escaped double-quotes should be considered double-quotes'
+ },
+ {
+ source: 'multipart/form-data; charset=utf-8; boundary=0xKhTmLbOuNdArY',
+ expected: ['multipart/form-data', ['charset', 'utf-8'], ['boundary', '0xKhTmLbOuNdArY']],
+ what: 'Multiple non-quoted parameters'
+ }
+ ]
+
+ t.plan(tests.length)
+
+ tests.forEach((v) => {
+ t.test(v.what, t => {
+ t.plan(1)
+
+ const result = parseParams(v.source)
+ t.strictSame(
+ result,
+ v.expected,
+ `parsed parameters match.\nSaw: ${inspect(result)}\nExpected: ${inspect(v.expected)}`)
+ })
+ })
+})
diff --git a/fastify-busboy/test/streamsearch.test.js b/fastify-busboy/test/streamsearch.test.js
new file mode 100644
index 0000000..968c7de
--- /dev/null
+++ b/fastify-busboy/test/streamsearch.test.js
@@ -0,0 +1,396 @@
+'use strict'
+
+const { test } = require('tap')
+const Streamsearch = require('../deps/streamsearch/sbmh')
+
+test('streamsearch', t => {
+ t.plan(17)
+
+ t.test('should throw an error if the needle is not a String or Buffer', t => {
+ t.plan(1)
+
+ t.throws(() => new Streamsearch(2), new Error('The needle has to be a String or a Buffer.'))
+ })
+ t.test('should throw an error if the needle is an empty String', t => {
+ t.plan(1)
+
+ t.throws(() => new Streamsearch(''), new Error('The needle cannot be an empty String/Buffer.'))
+ })
+ t.test('should throw an error if the needle is an empty Buffer', t => {
+ t.plan(1)
+
+ t.throws(() => new Streamsearch(Buffer.from('')), new Error('The needle cannot be an empty String/Buffer.'))
+ })
+ t.test('should throw an error if the needle is bigger than 256 characters', t => {
+ t.plan(1)
+
+ t.throws(() => new Streamsearch(Buffer.from(Array(257).fill('a').join(''))), new Error('The needle cannot have a length bigger than 256.'))
+ })
+
+ t.test('should process a Buffer without a needle', t => {
+ t.plan(5)
+ const expected = [
+ [false, Buffer.from('bar hello'), 0, 9]
+ ]
+ const needle = '\r\n'
+ const s = new Streamsearch(needle)
+ const chunks = [
+ Buffer.from('bar hello')
+ ]
+ let i = 0
+ s.on('info', (isMatched, data, start, end) => {
+ t.strictSame(isMatched, expected[i][0])
+ t.strictSame(data, expected[i][1])
+ t.strictSame(start, expected[i][2])
+ t.strictSame(end, expected[i][3])
+ i++
+ if (i >= 1) {
+ t.pass()
+ }
+ })
+
+ s.push(chunks[0])
+ })
+
+ t.test('should cast a string without a needle', t => {
+ t.plan(5)
+
+ const expected = [
+ [false, Buffer.from('bar hello'), 0, 9]
+ ]
+ const needle = '\r\n'
+ const s = new Streamsearch(needle)
+ const chunks = [
+ 'bar hello'
+ ]
+ let i = 0
+ s.on('info', (isMatched, data, start, end) => {
+ t.strictSame(isMatched, expected[i][0])
+ t.strictSame(data, expected[i][1])
+ t.strictSame(start, expected[i][2])
+ t.strictSame(end, expected[i][3])
+ i++
+ if (i >= 1) {
+ t.pass()
+ }
+ })
+
+ s.push(chunks[0])
+ })
+
+ t.test('should process a chunk with a needle at the beginning', t => {
+ t.plan(9)
+
+ const expected = [
+ [true, undefined, undefined, undefined],
+ [false, Buffer.from('\r\nbar hello'), 2, 11]
+ ]
+ const needle = '\r\n'
+ const s = new Streamsearch(needle)
+ const chunks = [
+ Buffer.from('\r\nbar hello')
+ ]
+ let i = 0
+ s.on('info', (isMatched, data, start, end) => {
+ t.strictSame(isMatched, expected[i][0])
+ t.strictSame(data, expected[i][1])
+ t.strictSame(start, expected[i][2])
+ t.strictSame(end, expected[i][3])
+ i++
+ if (i >= 2) {
+ t.pass()
+ }
+ })
+
+ s.push(chunks[0])
+ })
+
+ t.test('should process a chunk with a needle in the middle', t => {
+ t.plan(9)
+ const expected = [
+ [true, Buffer.from('bar\r\n hello'), 0, 3],
+ [false, Buffer.from('bar\r\n hello'), 5, 11]
+ ]
+ const needle = '\r\n'
+ const s = new Streamsearch(needle)
+ const chunks = [
+ Buffer.from('bar\r\n hello')
+ ]
+ let i = 0
+ s.on('info', (isMatched, data, start, end) => {
+ t.strictSame(isMatched, expected[i][0])
+ t.strictSame(data, expected[i][1])
+ t.strictSame(start, expected[i][2])
+ t.strictSame(end, expected[i][3])
+ i++
+ if (i >= 2) {
+ t.pass()
+ }
+ })
+
+ s.push(chunks[0])
+ })
+
+ t.test('should process a chunk with a needle at the end', t => {
+ t.plan(5)
+ const expected = [
+ [true, Buffer.from('bar hello\r\n'), 0, 9]
+ ]
+ const needle = '\r\n'
+ const s = new Streamsearch(needle)
+ const chunks = [
+ Buffer.from('bar hello\r\n')
+ ]
+ let i = 0
+ s.on('info', (isMatched, data, start, end) => {
+ t.strictSame(isMatched, expected[i][0])
+ t.strictSame(data, expected[i][1])
+ t.strictSame(start, expected[i][2])
+ t.strictSame(end, expected[i][3])
+ i++
+ if (i >= 1) {
+ t.pass()
+ }
+ })
+
+ s.push(chunks[0])
+ })
+
+ t.test('should process a chunk with multiple needle at the end', t => {
+ t.plan(9)
+ const expected = [
+ [true, Buffer.from('bar hello\r\n\r\n'), 0, 9],
+ [true, Buffer.from('bar hello\r\n\r\n'), 11, 11]
+ ]
+ const needle = '\r\n'
+ const s = new Streamsearch(needle)
+ const chunks = [
+ Buffer.from('bar hello\r\n\r\n')
+ ]
+ let i = 0
+ s.on('info', (isMatched, data, start, end) => {
+ t.strictSame(isMatched, expected[i][0])
+ t.strictSame(data, expected[i][1])
+ t.strictSame(start, expected[i][2])
+ t.strictSame(end, expected[i][3])
+ i++
+ if (i >= 2) {
+ t.pass()
+ }
+ })
+
+ s.push(chunks[0])
+ })
+
+ t.test('should process two chunks without a needle', t => {
+ t.plan(9)
+ const expected = [
+ [false, Buffer.from('bar'), 0, 3],
+ [false, Buffer.from('hello'), 0, 5]
+ ]
+ const needle = '\r\n'
+ const s = new Streamsearch(needle)
+ const chunks = [
+ Buffer.from('bar'),
+ Buffer.from('hello')
+ ]
+ let i = 0
+ s.on('info', (isMatched, data, start, end) => {
+ t.strictSame(isMatched, expected[i][0])
+ t.strictSame(data, expected[i][1])
+ t.strictSame(start, expected[i][2])
+ t.strictSame(end, expected[i][3])
+ i++
+ if (i >= 2) {
+ t.pass()
+ }
+ })
+
+ s.push(chunks[0])
+ s.push(chunks[1])
+ })
+
+ t.test('should process two chunks with an overflowing needle', t => {
+ t.plan(13)
+ const expected = [
+ [false, Buffer.from('bar\r'), 0, 3],
+ [true, undefined, undefined, undefined],
+ [false, Buffer.from('\nhello'), 1, 6]
+ ]
+ const needle = '\r\n'
+ const s = new Streamsearch(needle)
+ const chunks = [
+ Buffer.from('bar\r'),
+ Buffer.from('\nhello')
+ ]
+ let i = 0
+ s.on('info', (isMatched, data, start, end) => {
+ t.strictSame(isMatched, expected[i][0])
+ t.strictSame(data, expected[i][1])
+ t.strictSame(start, expected[i][2])
+ t.strictSame(end, expected[i][3])
+ i++
+ if (i >= 3) {
+ t.pass()
+ }
+ })
+
+ s.push(chunks[0])
+ s.push(chunks[1])
+ })
+
+ t.test('should process two chunks with a potentially overflowing needle', t => {
+ t.plan(13)
+
+ const expected = [
+ [false, Buffer.from('bar\r'), 0, 3],
+ [false, Buffer.from('\r\0\0'), 0, 1],
+ [false, Buffer.from('\n\r\nhello'), 0, 8]
+ ]
+ const needle = '\r\n\n'
+ const s = new Streamsearch(needle)
+ const chunks = [
+ Buffer.from('bar\r'),
+ Buffer.from('\n\r\nhello')
+ ]
+ let i = 0
+ s.on('info', (isMatched, data, start, end) => {
+ t.strictSame(isMatched, expected[i][0])
+ t.strictSame(data, expected[i][1])
+ t.strictSame(start, expected[i][2])
+ t.strictSame(end, expected[i][3])
+ i++
+ if (i >= 3) {
+ t.pass()
+ }
+ })
+
+ s.push(chunks[0])
+ s.push(chunks[1])
+ })
+
+ t.test('should process three chunks with a overflowing needle', t => {
+ t.plan(13)
+
+ const expected = [
+ [false, Buffer.from('bar\r'), 0, 3],
+ [true, undefined, undefined, undefined],
+ [false, Buffer.from('\nhello'), 1, 6]
+ ]
+ const needle = '\r\n\n'
+ const s = new Streamsearch(needle)
+ const chunks = [
+ Buffer.from('bar\r'),
+ Buffer.from('\n'),
+ Buffer.from('\nhello')
+ ]
+ let i = 0
+ s.on('info', (isMatched, data, start, end) => {
+ t.strictSame(isMatched, expected[i][0])
+ t.strictSame(data, expected[i][1])
+ t.strictSame(start, expected[i][2])
+ t.strictSame(end, expected[i][3])
+ i++
+ if (i >= 3) {
+ t.pass()
+ }
+ })
+
+ s.push(chunks[0])
+ s.push(chunks[1])
+ s.push(chunks[2])
+ })
+
+ t.test('should process four chunks with a overflowing needle', t => {
+ t.plan(13)
+
+ const expected = [
+ [false, Buffer.from('bar\r'), 0, 3],
+ [true, undefined, undefined, undefined],
+ [false, Buffer.from('hello'), 0, 5]
+ ]
+ const needle = '\r\n\n'
+ const s = new Streamsearch(needle)
+ const chunks = [
+ Buffer.from('bar\r'),
+ Buffer.from('\n'),
+ Buffer.from('\n'),
+ Buffer.from('hello')
+ ]
+ let i = 0
+ s.on('info', (isMatched, data, start, end) => {
+ t.strictSame(isMatched, expected[i][0])
+ t.strictSame(data, expected[i][1])
+ t.strictSame(start, expected[i][2])
+ t.strictSame(end, expected[i][3])
+ i++
+ if (i >= 3) {
+ t.pass()
+ }
+ })
+
+ s.push(chunks[0])
+ s.push(chunks[1])
+ s.push(chunks[2])
+ s.push(chunks[3])
+ })
+
+ t.test('should process four chunks with a potentially overflowing needle', t => {
+ t.plan(17)
+
+ const expected = [
+ [false, Buffer.from('bar\r'), 0, 3],
+ [false, Buffer.from('\r\n\0'), 0, 2],
+ [false, Buffer.from('\r\n\0'), 0, 1],
+ [false, Buffer.from('hello'), 0, 5]
+ ]
+ const needle = '\r\n\n'
+ const s = new Streamsearch(needle)
+ const chunks = [
+ Buffer.from('bar\r'),
+ Buffer.from('\n'),
+ Buffer.from('\r'),
+ Buffer.from('hello')
+ ]
+ let i = 0
+ s.on('info', (isMatched, data, start, end) => {
+ t.strictSame(isMatched, expected[i][0])
+ t.strictSame(data, expected[i][1])
+ t.strictSame(start, expected[i][2])
+ t.strictSame(end, expected[i][3])
+ i++
+ if (i >= 4) {
+ t.pass()
+ }
+ })
+
+ s.push(chunks[0])
+ s.push(chunks[1])
+ s.push(chunks[2])
+ s.push(chunks[3])
+ })
+
+ t.test('should reset the internal values if .reset() is called', t => {
+ t.plan(9)
+
+ const s = new Streamsearch('test')
+
+ t.strictSame(s._lookbehind_size, 0)
+ t.strictSame(s.matches, 0)
+ t.strictSame(s._bufpos, 0)
+
+ s._lookbehind_size = 1
+ s._bufpos = 1
+ s.matches = 1
+
+ t.strictSame(s._lookbehind_size, 1)
+ t.strictSame(s.matches, 1)
+ t.strictSame(s._bufpos, 1)
+
+ s.reset()
+
+ t.strictSame(s._lookbehind_size, 0)
+ t.strictSame(s.matches, 0)
+ t.strictSame(s._bufpos, 0)
+ })
+})
diff --git a/fastify-busboy/test/types-multipart.test.js b/fastify-busboy/test/types-multipart.test.js
new file mode 100644
index 0000000..dc7ae88
--- /dev/null
+++ b/fastify-busboy/test/types-multipart.test.js
@@ -0,0 +1,678 @@
+'use strict'
+
+const Busboy = require('..')
+
+const { test } = require('tap')
+const { inspect } = require('util')
+
+const EMPTY_FN = function () {
+}
+
+const tests = [
+ {
+ source: [
+ ['-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k',
+ 'Content-Disposition: form-data; name="file_name_0"',
+ '',
+ 'super alpha file',
+ '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k',
+ 'Content-Disposition: form-data; name="file_name_1"',
+ '',
+ 'super beta file',
+ '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k',
+ 'Content-Disposition: form-data; name="upload_file_0"; filename="1k_a.dat"',
+ 'Content-Type: application/octet-stream',
+ '',
+ 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
+ '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k',
+ 'Content-Disposition: form-data; name="upload_file_1"; filename="1k_b.dat"',
+ 'Content-Type: application/octet-stream',
+ '',
+ 'BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB',
+ '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k--'
+ ].join('\r\n')
+ ],
+ boundary: '---------------------------paZqsnEHRufoShdX6fh0lUhXBP4k',
+ expected: [
+ ['field', 'file_name_0', 'super alpha file', false, false, '7bit', 'text/plain'],
+ ['field', 'file_name_1', 'super beta file', false, false, '7bit', 'text/plain'],
+ ['file', 'upload_file_0', 1023, 0, '1k_a.dat', '7bit', 'application/octet-stream'],
+ ['file', 'upload_file_1', 1023, 0, '1k_b.dat', '7bit', 'application/octet-stream']
+ ],
+ what: 'Fields and files',
+ plan: 11
+ },
+ {
+ source: [
+ ['------WebKitFormBoundaryTB2MiQ36fnSJlrhY',
+ 'Content-Disposition: form-data; name="cont"',
+ '',
+ 'some random content',
+ '------WebKitFormBoundaryTB2MiQ36fnSJlrhY',
+ 'Content-Disposition: form-data; name="pass"',
+ '',
+ 'some random pass',
+ '------WebKitFormBoundaryTB2MiQ36fnSJlrhY',
+ 'Content-Disposition: form-data; name="bit"',
+ '',
+ '2',
+ '------WebKitFormBoundaryTB2MiQ36fnSJlrhY--'
+ ].join('\r\n')
+ ],
+ boundary: '----WebKitFormBoundaryTB2MiQ36fnSJlrhY',
+ expected: [
+ ['field', 'cont', 'some random content', false, false, '7bit', 'text/plain'],
+ ['field', 'pass', 'some random pass', false, false, '7bit', 'text/plain'],
+ ['field', 'bit', '2', false, false, '7bit', 'text/plain']
+ ],
+ what: 'Fields only',
+ plan: 6
+ },
+ {
+ source: [
+ ''
+ ],
+ boundary: '----WebKitFormBoundaryTB2MiQ36fnSJlrhY',
+ expected: [],
+ shouldError: 'Unexpected end of multipart data',
+ what: 'No fields and no files',
+ plan: 3
+ },
+ {
+ source: [
+ ['-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k',
+ 'Content-Disposition: form-data; name="file_name_0"',
+ '',
+ 'super alpha file',
+ '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k',
+ 'Content-Disposition: form-data; name="upload_file_0"; filename="1k_a.dat"',
+ 'Content-Type: application/octet-stream',
+ '',
+ 'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
+ '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k--'
+ ].join('\r\n')
+ ],
+ boundary: '---------------------------paZqsnEHRufoShdX6fh0lUhXBP4k',
+ limits: {
+ fileSize: 13,
+ fieldSize: 5
+ },
+ expected: [
+ ['field', 'file_name_0', 'super', false, true, '7bit', 'text/plain'],
+ ['file', 'upload_file_0', 13, 2, '1k_a.dat', '7bit', 'application/octet-stream']
+ ],
+ what: 'Fields and files (limits)',
+ plan: 7
+ },
+ {
+ source: [
+ ['-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k',
+ 'Content-Disposition: form-data; name="upload_file_0"; filename="1k_a.dat"',
+ 'Content-Type: application/octet-stream',
+ '',
+ 'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
+ '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k--'
+ ].join('\r\n')
+ ],
+ boundary: '---------------------------paZqsnEHRufoShdX6fh0lUhXBP4k',
+ limits: {
+ fields: 0
+ },
+ events: ['file'],
+ expected: [
+ ['file', 'upload_file_0', 26, 0, '1k_a.dat', '7bit', 'application/octet-stream']
+ ],
+ what: 'should not emit fieldsLimit if no field was sent',
+ plan: 6
+ },
+ {
+ source: [
+ ['-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k',
+ 'Content-Disposition: form-data; name="file_name_0"',
+ '',
+ 'super alpha file',
+ '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k',
+ 'Content-Disposition: form-data; name="upload_file_0"; filename="1k_a.dat"',
+ 'Content-Type: application/octet-stream',
+ '',
+ 'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
+ '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k--'
+ ].join('\r\n')
+ ],
+ boundary: '---------------------------paZqsnEHRufoShdX6fh0lUhXBP4k',
+ limits: {
+ fields: 0
+ },
+ events: ['file', 'fieldsLimit'],
+ expected: [
+ ['file', 'upload_file_0', 26, 0, '1k_a.dat', '7bit', 'application/octet-stream']
+ ],
+ what: 'should respect fields limit of 0',
+ plan: 6
+ },
+ {
+ source: [
+ ['-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k',
+ 'Content-Disposition: form-data; name="file_name_0"',
+ '',
+ 'super alpha file',
+ '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k',
+ 'Content-Disposition: form-data; name="file_name_1"',
+ '',
+ 'super beta file',
+ '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k',
+ 'Content-Disposition: form-data; name="upload_file_0"; filename="1k_a.dat"',
+ 'Content-Type: application/octet-stream',
+ '',
+ 'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
+ '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k--'
+ ].join('\r\n')
+ ],
+ boundary: '---------------------------paZqsnEHRufoShdX6fh0lUhXBP4k',
+ limits: {
+ fields: 1
+ },
+ events: ['field', 'file', 'fieldsLimit'],
+ expected: [
+ ['field', 'file_name_0', 'super alpha file', false, false, '7bit', 'text/plain'],
+ ['file', 'upload_file_0', 26, 0, '1k_a.dat', '7bit', 'application/octet-stream']
+ ],
+ what: 'should respect fields limit of 7',
+ plan: 7
+ },
+ {
+ source: [
+ ['-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k',
+ 'Content-Disposition: form-data; name="file_name_0"',
+ '',
+ 'super alpha file',
+ '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k--'
+ ].join('\r\n')
+ ],
+ boundary: '---------------------------paZqsnEHRufoShdX6fh0lUhXBP4k',
+ limits: {
+ files: 0
+ },
+ events: ['field'],
+ expected: [
+ ['field', 'file_name_0', 'super alpha file', false, false, '7bit', 'text/plain']
+ ],
+ what: 'should not emit filesLimit if no file was sent',
+ plan: 4
+ },
+ {
+ source: [
+ ['-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k',
+ 'Content-Disposition: form-data; name="file_name_0"',
+ '',
+ 'super alpha file',
+ '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k',
+ 'Content-Disposition: form-data; name="upload_file_0"; filename="1k_a.dat"',
+ 'Content-Type: application/octet-stream',
+ '',
+ 'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
+ '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k--'
+ ].join('\r\n')
+ ],
+ boundary: '---------------------------paZqsnEHRufoShdX6fh0lUhXBP4k',
+ limits: {
+ files: 0
+ },
+ events: ['field', 'filesLimit'],
+ expected: [
+ ['field', 'file_name_0', 'super alpha file', false, false, '7bit', 'text/plain']
+ ],
+ what: 'should respect fields limit of 0',
+ plan: 4
+ },
+ {
+ source: [
+ ['-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k',
+ 'Content-Disposition: form-data; name="file_name_0"',
+ '',
+ 'super alpha file',
+ '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k',
+ 'Content-Disposition: form-data; name="upload_file_0"; filename="1k_a.dat"',
+ 'Content-Type: application/octet-stream',
+ '',
+ 'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
+ '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k',
+ 'Content-Disposition: form-data; name="upload_file_b"; filename="1k_b.dat"',
+ 'Content-Type: application/octet-stream',
+ '',
+ 'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
+ '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k--'
+ ].join('\r\n')
+ ],
+ boundary: '---------------------------paZqsnEHRufoShdX6fh0lUhXBP4k',
+ limits: {
+ files: 1
+ },
+ events: ['field', 'file', 'filesLimit'],
+ expected: [
+ ['field', 'file_name_0', 'super alpha file', false, false, '7bit', 'text/plain'],
+ ['file', 'upload_file_0', 26, 0, '1k_a.dat', '7bit', 'application/octet-stream']
+ ],
+ what: 'should respect fields limit of 1',
+ plan: 7
+ },
+ {
+ source: [
+ ['-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k',
+ 'Content-Disposition: form-data; name="file_name_0"',
+ '',
+ 'super alpha file',
+ '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k',
+ 'Content-Disposition: form-data; name="file_name_1"',
+ '',
+ 'super beta file',
+ '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k',
+ 'Content-Disposition: form-data; name="upload_file_0"; filename="1k_a.dat"',
+ 'Content-Type: application/octet-stream',
+ '',
+ 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
+ '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k',
+ 'Content-Disposition: form-data; name="upload_file_1"; filename="1k_b.dat"',
+ 'Content-Type: application/octet-stream',
+ '',
+ 'BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB',
+ '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k--'
+ ].join('\r\n')
+ ],
+ boundary: '---------------------------paZqsnEHRufoShdX6fh0lUhXBP4k',
+ expected: [
+ ['field', 'file_name_0', 'super alpha file', false, false, '7bit', 'text/plain'],
+ ['field', 'file_name_1', 'super beta file', false, false, '7bit', 'text/plain']
+ ],
+ events: ['field'],
+ what: 'Fields and (ignored) files',
+ plan: 5
+ },
+ {
+ source: [
+ ['-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k',
+ 'Content-Disposition: form-data; name="upload_file_0"; filename="/tmp/1k_a.dat"',
+ 'Content-Type: application/octet-stream',
+ '',
+ 'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
+ '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k',
+ 'Content-Disposition: form-data; name="upload_file_1"; filename="C:\\files\\1k_b.dat"',
+ 'Content-Type: application/octet-stream',
+ '',
+ 'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
+ '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k',
+ 'Content-Disposition: form-data; name="upload_file_2"; filename="relative/1k_c.dat"',
+ 'Content-Type: application/octet-stream',
+ '',
+ 'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
+ '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k--'
+ ].join('\r\n')
+ ],
+ boundary: '---------------------------paZqsnEHRufoShdX6fh0lUhXBP4k',
+ expected: [
+ ['file', 'upload_file_0', 26, 0, '1k_a.dat', '7bit', 'application/octet-stream'],
+ ['file', 'upload_file_1', 26, 0, '1k_b.dat', '7bit', 'application/octet-stream'],
+ ['file', 'upload_file_2', 26, 0, '1k_c.dat', '7bit', 'application/octet-stream']
+ ],
+ what: 'Files with filenames containing paths',
+ plan: 12
+ },
+ {
+ source: [
+ ['-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k',
+ 'Content-Disposition: form-data; name="upload_file_0"; filename="/absolute/1k_a.dat"',
+ 'Content-Type: application/octet-stream',
+ '',
+ 'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
+ '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k',
+ 'Content-Disposition: form-data; name="upload_file_1"; filename="C:\\absolute\\1k_b.dat"',
+ 'Content-Type: application/octet-stream',
+ '',
+ 'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
+ '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k',
+ 'Content-Disposition: form-data; name="upload_file_2"; filename="relative/1k_c.dat"',
+ 'Content-Type: application/octet-stream',
+ '',
+ 'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
+ '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k--'
+ ].join('\r\n')
+ ],
+ boundary: '---------------------------paZqsnEHRufoShdX6fh0lUhXBP4k',
+ preservePath: true,
+ expected: [
+ ['file', 'upload_file_0', 26, 0, '/absolute/1k_a.dat', '7bit', 'application/octet-stream'],
+ ['file', 'upload_file_1', 26, 0, 'C:\\absolute\\1k_b.dat', '7bit', 'application/octet-stream'],
+ ['file', 'upload_file_2', 26, 0, 'relative/1k_c.dat', '7bit', 'application/octet-stream']
+ ],
+ what: 'Paths to be preserved through the preservePath option',
+ plan: 12
+ },
+ {
+ source: [
+ ['------WebKitFormBoundaryTB2MiQ36fnSJlrhY',
+ 'Content-Disposition: form-data; name="cont"',
+ 'Content-Type: ',
+ '',
+ 'some random content',
+ '------WebKitFormBoundaryTB2MiQ36fnSJlrhY',
+ 'Content-Disposition: ',
+ '',
+ 'some random pass',
+ '------WebKitFormBoundaryTB2MiQ36fnSJlrhY--'
+ ].join('\r\n')
+ ],
+ boundary: '----WebKitFormBoundaryTB2MiQ36fnSJlrhY',
+ expected: [
+ ['field', 'cont', 'some random content', false, false, '7bit', 'text/plain']
+ ],
+ what: 'Empty content-type and empty content-disposition',
+ plan: 4
+ },
+ {
+ config: {
+ isPartAFile: (fieldName) => (fieldName !== 'upload_file_0')
+ },
+ source: [
+ ['-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k',
+ 'Content-Disposition: form-data; name="upload_file_0"; filename="blob"',
+ 'Content-Type: application/json',
+ '',
+ 'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
+ '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k--'
+ ].join('\r\n')
+ ],
+ boundary: '---------------------------paZqsnEHRufoShdX6fh0lUhXBP4k',
+ expected: [
+ ['field', 'upload_file_0', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', false, false, '7bit', 'application/json']
+ ],
+ what: 'Blob uploads should be handled as fields if isPartAFile is provided.',
+ plan: 4
+ },
+ {
+ config: {
+ isPartAFile: (fieldName) => (fieldName !== 'upload_file_0')
+ },
+ source: [
+ ['-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k',
+ 'Content-Disposition: form-data; name="upload_file_0"; filename="blob"',
+ 'Content-Type: application/json',
+ '',
+ 'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
+ '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k',
+ 'Content-Disposition: form-data; name="file"; filename*=utf-8\'\'n%C3%A4me.txt',
+ 'Content-Type: application/octet-stream',
+ '',
+ 'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
+ '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k--'
+ ].join('\r\n')
+ ],
+ boundary: '---------------------------paZqsnEHRufoShdX6fh0lUhXBP4k',
+ expected: [
+ ['field', 'upload_file_0', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', false, false, '7bit', 'application/json'],
+ ['file', 'file', 26, 0, 'näme.txt', '7bit', 'application/octet-stream']
+ ],
+ what: 'Blob uploads should be handled as fields if isPartAFile is provided. Other parts should be files.',
+ plan: 7
+ },
+ {
+ config: {
+ isPartAFile: (fieldName) => (fieldName === 'upload_file_0')
+ },
+ source: [
+ ['-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k',
+ 'Content-Disposition: form-data; name="upload_file_0"; filename="blob"',
+ 'Content-Type: application/json',
+ '',
+ 'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
+ '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k',
+ 'Content-Disposition: form-data; name="file"; filename*=utf-8\'\'n%C3%A4me.txt',
+ 'Content-Type: application/octet-stream',
+ '',
+ 'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
+ '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k--'
+ ].join('\r\n')
+ ],
+ boundary: '---------------------------paZqsnEHRufoShdX6fh0lUhXBP4k',
+ expected: [
+ ['file', 'upload_file_0', 26, 0, 'blob', '7bit', 'application/json'],
+ ['field', 'file', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', false, false, '7bit', 'application/octet-stream']
+ ],
+ what: 'Blob uploads sould be handled as files if corresponding isPartAFile is provided. Other parts should be fields.',
+ plan: 7
+ },
+ {
+ source: [
+ ['-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k',
+ 'Content-Disposition: form-data; name="file"; filename*=utf-8\'\'n%C3%A4me.txt',
+ 'Content-Type: application/octet-stream',
+ '',
+ 'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
+ '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k--'
+ ].join('\r\n')
+ ],
+ boundary: '---------------------------paZqsnEHRufoShdX6fh0lUhXBP4k',
+ expected: [
+ ['file', 'file', 26, 0, 'näme.txt', '7bit', 'application/octet-stream']
+ ],
+ what: 'Unicode filenames',
+ plan: 6
+ },
+ {
+ source: [
+ ['--asdasdasdasd\r\n',
+ 'Content-Type: text/plain\r\n',
+ 'Content-Disposition: form-data; name="foo"\r\n',
+ '\r\n',
+ 'asd\r\n',
+ '--asdasdasdasd--'
+ ].join(':)')
+ ],
+ boundary: 'asdasdasdasd',
+ expected: [],
+ shouldError: 'Unexpected end of multipart data',
+ what: 'Stopped mid-header',
+ plan: 3
+ },
+ {
+ source: [
+ ['------WebKitFormBoundaryTB2MiQ36fnSJlrhY',
+ 'Content-Disposition: form-data; name="cont"',
+ 'Content-Type: application/json',
+ '',
+ '{}',
+ '------WebKitFormBoundaryTB2MiQ36fnSJlrhY--'
+ ].join('\r\n')
+ ],
+ boundary: '----WebKitFormBoundaryTB2MiQ36fnSJlrhY',
+ expected: [
+ ['field', 'cont', '{}', false, false, '7bit', 'application/json']
+ ],
+ what: 'content-type for fields',
+ plan: 4
+ },
+ {
+ source: [
+ '------WebKitFormBoundaryTB2MiQ36fnSJlrhY--\r\n'
+ ],
+ boundary: '----WebKitFormBoundaryTB2MiQ36fnSJlrhY',
+ expected: [],
+ what: 'empty form',
+ plan: 3
+ },
+ {
+ source: [
+ ['-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k',
+ 'Content-Disposition: form-data; name="field1"',
+ 'content-type: text/plain; charset=utf-8',
+ '',
+ 'Aufklärung ist der Ausgang des Menschen aus seiner selbstverschuldeten Unmündigkeit.',
+ '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k',
+ 'Content-Disposition: form-data; name="field2"',
+ 'content-type: text/plain; charset=iso-8859-1',
+ '',
+ 'sapere aude!',
+ '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k--'
+ ].join('\r\n')
+ ],
+ boundary: '---------------------------paZqsnEHRufoShdX6fh0lUhXBP4k',
+ expected: [
+ ['field', 'field1', 'Aufklärung ist der Ausgang des Menschen aus seiner selbstverschuldeten Unmündigkeit.', false, false, '7bit', 'text/plain'],
+ ['field', 'field2', 'sapere aude!', false, false, '7bit', 'text/plain']
+ ],
+ what: 'Fields and files',
+ plan: 5
+ },
+ {
+ source: [[
+ '------WebKitFormBoundaryzca7IDMnT6QwqBp7',
+ 'Content-Disposition: form-data; name="regsubmit"',
+ '',
+ 'yes',
+ '------WebKitFormBoundaryzca7IDMnT6QwqBp7',
+ '------WebKitFormBoundaryzca7IDMnT6QwqBp7',
+ 'Content-Disposition: form-data; name="referer"',
+ '',
+ 'http://domainExample/./',
+ '------WebKitFormBoundaryzca7IDMnT6QwqBp7',
+ 'Content-Disposition: form-data; name="activationauth"',
+ '',
+ '',
+ '------WebKitFormBoundaryzca7IDMnT6QwqBp7',
+ 'Content-Disposition: form-data; name="seccodemodid"',
+ '',
+ 'member::register',
+ '------WebKitFormBoundaryzca7IDMnT6QwqBp7--'].join('\r\n')
+ ],
+ boundary: '----WebKitFormBoundaryzca7IDMnT6QwqBp7',
+ expected: [
+ ['field', 'regsubmit', 'yes', false, false, '7bit', 'text/plain'],
+ ['field', 'referer', 'http://domainExample/./', false, false, '7bit', 'text/plain'],
+ ['field', 'activationauth', '', false, false, '7bit', 'text/plain'],
+ ['field', 'seccodemodid', 'member::register', false, false, '7bit', 'text/plain']
+ ],
+ what: 'one empty part should get ignored',
+ plan: 7
+ },
+ {
+ source: [
+ ' ------WebKitFormBoundaryTB2MiQ36fnSJlrhY--\r\n'
+ ],
+ boundary: '----WebKitFormBoundaryTB2MiQ36fnSJlrhY',
+ expected: [],
+ shouldError: 'Unexpected end of multipart data',
+ what: 'empty form with preceding whitespace',
+ plan: 3
+ },
+ {
+ source: [
+ '------WebKitFormBoundaryTB2MiQ36fnSJlrhY--\r\n'
+ ],
+ boundary: '----WebKitFormBoundaryTB2MiQ36fnSJlrhYY',
+ expected: [],
+ shouldError: 'Unexpected end of multipart data',
+ what: 'empty form with wrong boundary (extra Y)',
+ plan: 3
+ },
+ {
+ source: [[
+ '------WebKitFormBoundaryzca7IDMnT6QwqBp7',
+ 'Content-Disposition: form-data; name="regsubmit"',
+ '',
+ 'yes',
+ '------WebKitFormBoundaryzca7IDMnT6QwqBp7',
+ '------WebKitFormBoundaryzca7IDMnT6QwqBp7',
+ '------WebKitFormBoundaryzca7IDMnT6QwqBp7',
+ '------WebKitFormBoundaryzca7IDMnT6QwqBp7',
+ 'Content-Disposition: form-data; name="referer"',
+ '',
+ 'http://domainExample/./',
+ '------WebKitFormBoundaryzca7IDMnT6QwqBp7',
+ 'Content-Disposition: form-data; name="activationauth"',
+ '',
+ '',
+ '------WebKitFormBoundaryzca7IDMnT6QwqBp7',
+ 'Content-Disposition: form-data; name="seccodemodid"',
+ '',
+ 'member::register',
+ '------WebKitFormBoundaryzca7IDMnT6QwqBp7--'].join('\r\n')
+ ],
+ boundary: '----WebKitFormBoundaryzca7IDMnT6QwqBp7',
+ expected: [
+ ['field', 'regsubmit', 'yes', false, false, '7bit', 'text/plain'],
+ ['field', 'referer', 'http://domainExample/./', false, false, '7bit', 'text/plain'],
+ ['field', 'activationauth', '', false, false, '7bit', 'text/plain'],
+ ['field', 'seccodemodid', 'member::register', false, false, '7bit', 'text/plain']
+ ],
+ what: 'multiple empty parts should get ignored',
+ plan: 7
+ }
+]
+
+tests.forEach((v) => {
+ test(v.what, t => {
+ t.plan(v.plan)
+ const busboy = new Busboy({
+ ...v.config,
+ limits: v.limits,
+ preservePath: v.preservePath,
+ headers: {
+ 'content-type': 'multipart/form-data; boundary=' + v.boundary
+ }
+ })
+ let finishes = 0
+ const results = []
+
+ if (v.events === undefined || v.events.indexOf('field') > -1) {
+ busboy.on('field', function (key, val, keyTrunc, valTrunc, encoding, contype) {
+ results.push(['field', key, val, keyTrunc, valTrunc, encoding, contype])
+ })
+ }
+ if (v.events === undefined || v.events.indexOf('file') > -1) {
+ busboy.on('file', function (fieldname, stream, filename, encoding, mimeType) {
+ let nb = 0
+ const info = ['file',
+ fieldname,
+ nb,
+ 0,
+ filename,
+ encoding,
+ mimeType]
+ results.push(info)
+ stream.on('data', function (d) {
+ nb += d.length
+ }).on('limit', function () {
+ ++info[3]
+ }).on('end', function () {
+ info[2] = nb
+ t.ok(typeof (stream.bytesRead) === 'number', 'file.bytesRead is missing')
+ t.ok(stream.bytesRead === nb, 'file.bytesRead is not equal to filesize')
+ if (stream.truncated) { ++info[3] }
+ })
+ })
+ }
+ busboy.on('finish', function () {
+ t.ok(finishes++ === 0, 'finish emitted multiple times')
+ t.equal(results.length,
+ v.expected.length,
+ 'Parsed result count mismatch. Saw ' +
+ results.length +
+ '. Expected: ' + v.expected.length)
+
+ results.forEach(function (result, i) {
+ t.strictSame(result,
+ v.expected[i],
+ 'Result mismatch:\nParsed: ' + inspect(result) +
+ '\nExpected: ' + inspect(v.expected[i])
+ )
+ })
+ t.pass()
+ }).on('error', function (err) {
+ if (!v.shouldError || v.shouldError !== err.message) { t.error(err) }
+ })
+
+ v.source.forEach(function (s) {
+ busboy.write(Buffer.from(s, 'utf8'), EMPTY_FN)
+ })
+ busboy.end()
+ })
+})
diff --git a/fastify-busboy/test/types-urlencoded.test.js b/fastify-busboy/test/types-urlencoded.test.js
new file mode 100644
index 0000000..73cc286
--- /dev/null
+++ b/fastify-busboy/test/types-urlencoded.test.js
@@ -0,0 +1,210 @@
+'use strict'
+
+const { inspect } = require('util')
+const Busboy = require('..')
+const { test } = require('tap')
+
+const EMPTY_FN = function () {
+}
+
+const tests = [
+ {
+ source: ['foo'],
+ expected: [['foo', '', false, false]],
+ what: 'Unassigned value',
+ plan: 4
+ },
+ {
+ source: ['foo=bar'],
+ expected: [['foo', 'bar', false, false]],
+ what: 'Assigned value',
+ plan: 4
+ },
+ {
+ source: ['foo&bar=baz'],
+ expected: [['foo', '', false, false],
+ ['bar', 'baz', false, false]],
+ what: 'Unassigned and assigned value',
+ plan: 5
+ },
+ {
+ source: ['foo=bar&baz'],
+ expected: [['foo', 'bar', false, false],
+ ['baz', '', false, false]],
+ what: 'Assigned and unassigned value',
+ plan: 5
+ },
+ {
+ source: ['foo=bar&baz=bla'],
+ expected: [['foo', 'bar', false, false],
+ ['baz', 'bla', false, false]],
+ what: 'Two assigned values',
+ plan: 5
+ },
+ {
+ source: ['foo&bar'],
+ expected: [['foo', '', false, false],
+ ['bar', '', false, false]],
+ what: 'Two unassigned values',
+ plan: 5
+ },
+ {
+ source: ['foo&bar&'],
+ expected: [['foo', '', false, false],
+ ['bar', '', false, false]],
+ what: 'Two unassigned values and ampersand',
+ plan: 5
+ },
+ {
+ source: ['foo=bar+baz%2Bquux'],
+ expected: [['foo', 'bar baz+quux', false, false]],
+ what: 'Assigned value with (plus) space',
+ plan: 4
+ },
+ {
+ source: ['foo=bar%20baz%21'],
+ expected: [['foo', 'bar baz!', false, false]],
+ what: 'Assigned value with encoded bytes',
+ plan: 4
+ },
+ {
+ source: ['foo%20bar=baz%20bla%21'],
+ expected: [['foo bar', 'baz bla!', false, false]],
+ what: 'Assigned value with encoded bytes #2',
+ plan: 4
+ },
+ {
+ source: ['foo=bar%20baz%21&num=1000'],
+ expected: [['foo', 'bar baz!', false, false],
+ ['num', '1000', false, false]],
+ what: 'Two assigned values, one with encoded bytes',
+ plan: 5
+ },
+ {
+ source: ['foo=bar&baz=bla'],
+ expected: [],
+ what: 'Limits: zero fields',
+ limits: { fields: 0 },
+ plan: 3
+ },
+ {
+ source: ['foo=bar&baz=bla'],
+ expected: [['foo', 'bar', false, false]],
+ what: 'Limits: one field',
+ limits: { fields: 1 },
+ plan: 4
+ },
+ {
+ source: ['foo=bar&baz=bla'],
+ expected: [['foo', 'bar', false, false],
+ ['baz', 'bla', false, false]],
+ what: 'Limits: field part lengths match limits',
+ limits: { fieldNameSize: 3, fieldSize: 3 },
+ plan: 5
+ },
+ {
+ source: ['foo=bar&baz=bla'],
+ expected: [['fo', 'bar', true, false],
+ ['ba', 'bla', true, false]],
+ what: 'Limits: truncated field name',
+ limits: { fieldNameSize: 2 },
+ plan: 5
+ },
+ {
+ source: ['foo=bar&baz=bla'],
+ expected: [['foo', 'ba', false, true],
+ ['baz', 'bl', false, true]],
+ what: 'Limits: truncated field value',
+ limits: { fieldSize: 2 },
+ plan: 5
+ },
+ {
+ source: ['foo=bar&baz=bla'],
+ expected: [['fo', 'ba', true, true],
+ ['ba', 'bl', true, true]],
+ what: 'Limits: truncated field name and value',
+ limits: { fieldNameSize: 2, fieldSize: 2 },
+ plan: 5
+ },
+ {
+ source: ['foo=bar&baz=bla'],
+ expected: [['fo', '', true, true],
+ ['ba', '', true, true]],
+ what: 'Limits: truncated field name and zero value limit',
+ limits: { fieldNameSize: 2, fieldSize: 0 },
+ plan: 5
+ },
+ {
+ source: ['foo=bar&baz=bla'],
+ expected: [['', '', true, true],
+ ['', '', true, true]],
+ what: 'Limits: truncated zero field name and zero value limit',
+ limits: { fieldNameSize: 0, fieldSize: 0 },
+ plan: 5
+ },
+ {
+ source: ['&'],
+ expected: [],
+ what: 'Ampersand',
+ plan: 3
+ },
+ {
+ source: ['&&&&&'],
+ expected: [],
+ what: 'Many ampersands',
+ plan: 3
+ },
+ {
+ source: ['='],
+ expected: [['', '', false, false]],
+ what: 'Assigned value, empty name and value',
+ plan: 4
+ },
+ {
+ source: [''],
+ expected: [],
+ what: 'Nothing',
+ plan: 3
+ }
+]
+
+tests.forEach((v) => {
+ test(v.what, t => {
+ t.plan(v.plan || 20)
+ const busboy = new Busboy({
+ limits: v.limits,
+ headers: {
+ 'content-type': 'application/x-www-form-urlencoded; charset=utf-8'
+ }
+ })
+ let finishes = 0
+ const results = []
+
+ busboy.on('field', function (key, val, keyTrunc, valTrunc) {
+ results.push([key, val, keyTrunc, valTrunc])
+ })
+ busboy.on('file', function () {
+ throw new Error('Unexpected file')
+ })
+ busboy.on('finish', function () {
+ t.ok(finishes++ === 0, 'finish emitted multiple times')
+ t.equal(results.length, v.expected.length)
+
+ let i = 0
+ results.forEach(function (result) {
+ t.strictSame(result,
+ v.expected[i],
+ 'Result mismatch:\nParsed: ' + inspect(result) +
+ '\nExpected: ' + inspect(v.expected[i])
+ )
+ ++i
+ })
+ t.pass()
+ })
+
+ v.source.forEach(function (s) {
+ busboy.write(Buffer.from(s, 'utf8'), EMPTY_FN)
+ })
+ busboy.end()
+ })
+})
diff --git a/fastify-busboy/test/types/dicer.test-d.ts b/fastify-busboy/test/types/dicer.test-d.ts
new file mode 100644
index 0000000..466c1e1
--- /dev/null
+++ b/fastify-busboy/test/types/dicer.test-d.ts
@@ -0,0 +1,81 @@
+import { Dicer } from "../../lib/main";
+import * as fs from "fs";
+import * as stream from "stream";
+
+function testDicerSyntax() {
+ const opts: Dicer.Config = {
+ boundary: "testing",
+ };
+ const dicer = new Dicer(opts);
+ const opts2: Dicer.Config = {
+ headerFirst: true,
+ maxHeaderPairs: 1,
+ };
+ const opts3: Dicer.Config = {
+ boundary: "more-testing",
+ headerFirst: false,
+ maxHeaderPairs: 8,
+ };
+ dicer.setBoundary("new-testing-boundary");
+ dicer.on("part", handleDicerPartStream);
+ dicer.on("finish", () => {
+ console.log("dicer parsing finished");
+ });
+ dicer.on("preamble", part => {
+ console.log("dicer preamble to new part");
+ });
+ dicer.on("trailer", data => {
+ console.log(`dicer trailing data found: ${data.length} bytes`);
+ });
+ dicer.on("close", () => {
+ console.log("dicer close");
+ });
+ dicer.on("drain", () => {
+ console.log("dicer drain");
+ });
+ dicer.on("error", err => {
+ console.error(`dicer error: ${err.message || JSON.stringify(err)}`);
+ });
+ dicer.on("finish", () => {
+ console.log("dicer finish");
+ });
+ dicer.on("pipe", (src: stream.Readable) => {
+ console.log("dicer pipe");
+ });
+ dicer.on("unpipe", (src: stream.Readable) => {
+ console.log("dicer unpipe");
+ });
+ const inputFileStream = fs.createReadStream("in-test-file.txt");
+ inputFileStream.pipe(dicer);
+}
+/**
+ * Handle a part found by a Dicer parser
+ *
+ * @param part Part found
+ */
+function handleDicerPartStream(part: Dicer.PartStream) {
+ console.log("dicer part found");
+ const outputFileStream = fs.createWriteStream("out-test-file.txt");
+ part.on("readable", () => {
+ console.log("part readable");
+ });
+ part.on("header", header => {
+ console.log(`part header found:\n${JSON.stringify(header)}`);
+ });
+ part.on("data", () => {
+ console.log("part data");
+ });
+ part.on("finish", () => {
+ console.log("part finished");
+ });
+ part.on("error", err => {
+ console.error(`part error: ${err.message || JSON.stringify(err)}`);
+ });
+ part.on("end", () => {
+ console.log("part ended");
+ });
+ part.on("close", () => {
+ console.log("part closed");
+ });
+ part.pipe(outputFileStream);
+} \ No newline at end of file
diff --git a/fastify-busboy/test/types/main.test-d.ts b/fastify-busboy/test/types/main.test-d.ts
new file mode 100644
index 0000000..fb58b3f
--- /dev/null
+++ b/fastify-busboy/test/types/main.test-d.ts
@@ -0,0 +1,241 @@
+import BusboyDefault, { BusboyConstructor, BusboyConfig, BusboyHeaders, Busboy, BusboyEvents, BusboyFileStream } from '../..';
+import {expectError, expectType} from "tsd";
+import BusboyESM from "../..";
+
+// test type exports
+type Constructor = BusboyConstructor;
+type Config = BusboyConfig;
+type Headers = BusboyHeaders;
+type Events = BusboyEvents;
+type BB = Busboy;
+
+expectType<Busboy>(new BusboyESM({ headers: { 'content-type': 'foo' } }));
+expectType<Busboy>(new Busboy({ headers: { 'content-type': 'foo' } }));
+
+expectError(new BusboyDefault({}));
+const busboy = BusboyDefault({ headers: { 'content-type': 'foo' } }); // $ExpectType Busboy
+new BusboyDefault({ headers: { 'content-type': 'foo' } }); // $ExpectType Busboy
+new BusboyDefault({ headers: { 'content-type': 'foo' }, highWaterMark: 1000 }); // $ExpectType Busboy
+new BusboyDefault({ headers: { 'content-type': 'foo' }, fileHwm: 1000 }); // $ExpectType Busboy
+new BusboyDefault({ headers: { 'content-type': 'foo' }, defCharset: 'utf8' }); // $ExpectType Busboy
+new BusboyDefault({ headers: { 'content-type': 'foo' }, preservePath: true }); // $ExpectType Busboy
+new BusboyDefault({ headers: { 'content-type': 'foo' }, limits: { fieldNameSize: 200 } }); // $ExpectType Busboy
+new BusboyDefault({ headers: { 'content-type': 'foo' }, limits: { fieldSize: 200 } }); // $ExpectType Busboy
+new BusboyDefault({ headers: { 'content-type': 'foo' }, limits: { fields: 200 } }); // $ExpectType Busboy
+new BusboyDefault({ headers: { 'content-type': 'foo' }, limits: { fileSize: 200 } }); // $ExpectType Busboy
+new BusboyDefault({ headers: { 'content-type': 'foo' }, limits: { files: 200 } }); // $ExpectType Busboy
+new BusboyDefault({ headers: { 'content-type': 'foo' }, limits: { parts: 200 } }); // $ExpectType Busboy
+new BusboyDefault({ headers: { 'content-type': 'foo' }, limits: { headerPairs: 200 } }); // $ExpectType Busboy
+new BusboyDefault({ headers: { 'content-type': 'foo' }, limits: { headerSize: 200 } }); // $ExpectType Busboy
+new BusboyDefault({ headers: { 'content-type': 'foo' }, isPartAFile: (fieldName, contentType, fileName) => fieldName === 'my-special-field' || fileName !== 'not-so-special.txt' }); // $ExpectType Busboy
+new BusboyDefault({ headers: { 'content-type': 'foo' }, isPartAFile: (fieldName, contentType, fileName) => fileName !== undefined }); // $ExpectType Busboy
+
+busboy.addListener('file', (fieldname, file, filename, encoding, mimetype) => {
+ expectType<string> (fieldname)
+ expectType<BusboyFileStream>(file);
+ expectType<string>(filename);
+ expectType<string>(encoding);
+ expectType<string>(mimetype);
+});
+busboy.addListener('field', (fieldname, val, fieldnameTruncated, valTruncated, encoding, mimetype) => {
+ expectType<string> (fieldname);
+ expectType<string> (val);
+ expectType<boolean> (fieldnameTruncated);
+ expectType<boolean> (valTruncated);
+ expectType<string> (encoding);
+ expectType<string> (mimetype);
+});
+busboy.addListener('partsLimit', () => {});
+busboy.addListener('filesLimit', () => {});
+busboy.addListener('fieldsLimit', () => {});
+busboy.addListener('error', e => {
+ expectType<unknown> (e);
+});
+busboy.addListener('finish', () => {});
+// test fallback
+busboy.on('foo', foo => {
+ expectType<any> (foo);
+});
+busboy.on(Symbol('foo'), foo => {
+ expectType<any>(foo);
+});
+
+busboy.on('file', (fieldname, file, filename, encoding, mimetype) => {
+ expectType<string> (fieldname);
+ expectType<BusboyFileStream> (file);
+ expectType<string> (filename);
+ expectType<string> (encoding);
+ expectType<string> (mimetype);
+});
+busboy.on('field', (fieldname, val, fieldnameTruncated, valTruncated, encoding, mimetype) => {
+ expectType<string> (fieldname);
+ expectType<string> (val);
+ expectType<boolean> (fieldnameTruncated);
+ expectType<boolean> (valTruncated);
+ expectType<string> (encoding);
+ expectType<string> (mimetype);
+});
+busboy.on('partsLimit', () => {});
+busboy.on('filesLimit', () => {});
+busboy.on('fieldsLimit', () => {});
+busboy.on('error', e => {
+ expectType<unknown> (e);
+});
+busboy.on('finish', () => {});
+// test fallback
+busboy.on('foo', foo => {
+ expectType<any> (foo);
+});
+busboy.on(Symbol('foo'), foo => {
+ expectType<any> (foo);
+});
+
+busboy.once('file', (fieldname, file, filename, encoding, mimetype) => {
+ expectType<string> (fieldname);
+ expectType<BusboyFileStream> (file);
+ expectType<string> (filename);
+ expectType<string> (encoding);
+ expectType<string> (mimetype);
+});
+busboy.once('field', (fieldname, val, fieldnameTruncated, valTruncated, encoding, mimetype) => {
+ expectType<string> (fieldname);
+ expectType<string> (val);
+ expectType<boolean> (fieldnameTruncated);
+ expectType<boolean> (valTruncated);
+ expectType<string> (encoding);
+ expectType<string> (mimetype);
+});
+busboy.once('partsLimit', () => {});
+busboy.once('filesLimit', () => {});
+busboy.once('fieldsLimit', () => {});
+busboy.once('error', e => {
+ expectType<unknown> (e);
+});
+busboy.once('finish', () => {});
+// test fallback
+busboy.once('foo', foo => {
+ expectType<any> (foo);
+});
+busboy.once(Symbol('foo'), foo => {
+ expectType<any> (foo);
+});
+
+busboy.removeListener('file', (fieldname, file, filename, encoding, mimetype) => {
+ expectType<string> (fieldname);
+ expectType<BusboyFileStream> (file);
+ expectType<string> (filename);
+ expectType<string> (encoding);
+ expectType<string> (mimetype);
+});
+busboy.removeListener('field', (fieldname, val, fieldnameTruncated, valTruncated, encoding, mimetype) => {
+ expectType<string> (fieldname);
+ expectType<string> (val);
+ expectType<boolean> (fieldnameTruncated);
+ expectType<boolean> (valTruncated);
+ expectType<string> (encoding);
+ expectType<string> (mimetype);
+});
+busboy.removeListener('partsLimit', () => {});
+busboy.removeListener('filesLimit', () => {});
+busboy.removeListener('fieldsLimit', () => {});
+busboy.removeListener('error', e => {
+ expectType<unknown> (e);
+});
+busboy.removeListener('finish', () => {});
+// test fallback
+busboy.removeListener('foo', foo => {
+ expectType<any> (foo);
+});
+busboy.removeListener(Symbol('foo'), foo => {
+ expectType<any> (foo);
+});
+
+busboy.off('file', (fieldname, file, filename, encoding, mimetype) => {
+ expectType<string> (fieldname);
+ expectType<BusboyFileStream> (file);
+ expectType<string> (filename);
+ expectType<string> (encoding);
+ expectType<string> (mimetype);
+});
+busboy.off('field', (fieldname, val, fieldnameTruncated, valTruncated, encoding, mimetype) => {
+ expectType<string> (fieldname);
+ expectType<string> (val);
+ expectType<boolean> (fieldnameTruncated);
+ expectType<boolean> (valTruncated);
+ expectType<string> (encoding);
+ expectType<string> (mimetype);
+});
+busboy.off('partsLimit', () => {});
+busboy.off('filesLimit', () => {});
+busboy.off('fieldsLimit', () => {});
+busboy.off('error', e => {
+ expectType<unknown> (e);
+});
+busboy.off('finish', () => {});
+// test fallback
+busboy.off('foo', foo => {
+ expectType<any> (foo);
+});
+busboy.off(Symbol('foo'), foo => {
+ expectType<any> (foo);
+});
+
+busboy.prependListener('file', (fieldname, file, filename, encoding, mimetype) => {
+ expectType<string> (fieldname);
+ expectType<BusboyFileStream> (file);
+ expectType<string> (filename);
+ expectType<string> (encoding);
+ expectType<string> (mimetype);
+});
+busboy.prependListener('field', (fieldname, val, fieldnameTruncated, valTruncated, encoding, mimetype) => {
+ expectType<string> (fieldname);
+ expectType<string> (val);
+ expectType<boolean> (fieldnameTruncated);
+ expectType<boolean> (valTruncated);
+ expectType<string> (encoding);
+ expectType<string> (mimetype);
+});
+busboy.prependListener('partsLimit', () => {});
+busboy.prependListener('filesLimit', () => {});
+busboy.prependListener('fieldsLimit', () => {});
+busboy.prependListener('error', e => {
+ expectType<unknown> (e);
+});
+busboy.prependListener('finish', () => {});
+// test fallback
+busboy.prependListener('foo', foo => {
+ expectType<any> (foo);
+});
+busboy.prependListener(Symbol('foo'), foo => {
+ expectType<any> (foo);
+});
+
+busboy.prependOnceListener('file', (fieldname, file, filename, encoding, mimetype) => {
+ expectType<string> (fieldname);
+ expectType<BusboyFileStream> (file);
+ expectType<string> (filename);
+ expectType<string> (encoding);
+ expectType<string> (mimetype);
+});
+busboy.prependOnceListener('field', (fieldname, val, fieldnameTruncated, valTruncated, encoding, mimetype) => {
+ expectType<string> (fieldname);
+ expectType<string> (val);
+ expectType<boolean> (fieldnameTruncated);
+ expectType<boolean> (valTruncated);
+ expectType<string> (encoding);
+ expectType<string> (mimetype);
+});
+busboy.prependOnceListener('partsLimit', () => {});
+busboy.prependOnceListener('filesLimit', () => {});
+busboy.prependOnceListener('fieldsLimit', () => {});
+busboy.prependOnceListener('error', e => {
+ expectType<unknown> (e);
+});
+busboy.prependOnceListener('finish', () => {});
+// test fallback
+busboy.prependOnceListener('foo', foo => {
+ expectType<any> (foo);
+});
+busboy.prependOnceListener(Symbol('foo'), foo => {
+ expectType<any> (foo);
+});
diff --git a/fastify-busboy/tsconfig.json b/fastify-busboy/tsconfig.json
new file mode 100644
index 0000000..eec9314
--- /dev/null
+++ b/fastify-busboy/tsconfig.json
@@ -0,0 +1,30 @@
+{
+ "compilerOptions": {
+ "outDir": "dist",
+ "module": "commonjs",
+ "target": "es2015",
+ "sourceMap": false,
+ "declaration": true,
+ "declarationMap": false,
+ "types": ["node"],
+ "strict": true,
+ "moduleResolution": "node",
+ "noUnusedLocals": false,
+ "noUnusedParameters": false,
+ "noFallthroughCasesInSwitch": true,
+ "noImplicitReturns": true,
+ "noImplicitAny": true,
+ "noImplicitThis": true,
+ "strictNullChecks": true,
+ "importHelpers": true,
+ "baseUrl": ".",
+ "allowSyntheticDefaultImports": true,
+ "esModuleInterop": true,
+ "forceConsistentCasingInFileNames": true
+ },
+ "exclude": [
+ "node_modules",
+ "test",
+ "dist"
+ ]
+}
diff --git a/index-fetch.js b/index-fetch.js
new file mode 100644
index 0000000..ba31a65
--- /dev/null
+++ b/index-fetch.js
@@ -0,0 +1,15 @@
+'use strict'
+
+const fetchImpl = require('./lib/fetch').fetch
+
+module.exports.fetch = function fetch (resource, init = undefined) {
+ return fetchImpl(resource, init).catch((err) => {
+ Error.captureStackTrace(err, this)
+ throw err
+ })
+}
+module.exports.FormData = require('./lib/fetch/formdata').FormData
+module.exports.Headers = require('./lib/fetch/headers').Headers
+module.exports.Response = require('./lib/fetch/response').Response
+module.exports.Request = require('./lib/fetch/request').Request
+module.exports.WebSocket = require('./lib/websocket/websocket').WebSocket
diff --git a/index.d.ts b/index.d.ts
new file mode 100644
index 0000000..83a786d
--- /dev/null
+++ b/index.d.ts
@@ -0,0 +1,3 @@
+export * from './types/index'
+import Undici from './types/index'
+export default Undici
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..672a592
--- /dev/null
+++ b/index.html
@@ -0,0 +1,35 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <title>Node.js Undici</title>
+ <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
+ <meta name="description" content="A HTTP/1.1 client, written from scratch for Node.js.">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0">
+ <link rel="stylesheet" href="//cdn.jsdelivr.net/npm/docsify@4/lib/themes/vue.css">
+ <link rel="icon" type="image/png" href="https://nodejs.org/static/images/favicons/favicon-32x32.png"/>
+ <link rel="icon" type="image/png" href="https://nodejs.org/static/images/favicons/favicon-16x16.png" />
+ <style>
+ </style>
+</head>
+<body>
+ <div id="app"></div>
+ <script>
+ window.$docsify = {
+ name: 'Node.js Undici',
+ repo: 'https://github.com/nodejs/undici',
+ loadSidebar: 'docsify/sidebar.md',
+ auto2top: true,
+ subMaxLevel: 3,
+ maxLevel: 3,
+ themeColor: '#2B91F0',
+ noCompileLinks: [
+ 'benchmarks/.*'
+ ],
+ relativePath: true
+ }
+ </script>
+ <!-- Docsify v4 -->
+ <script src="//cdn.jsdelivr.net/npm/docsify@4"></script>
+</body>
+</html>
diff --git a/index.js b/index.js
new file mode 100644
index 0000000..26302cc
--- /dev/null
+++ b/index.js
@@ -0,0 +1,167 @@
+'use strict'
+
+const Client = require('./lib/client')
+const Dispatcher = require('./lib/dispatcher')
+const errors = require('./lib/core/errors')
+const Pool = require('./lib/pool')
+const BalancedPool = require('./lib/balanced-pool')
+const Agent = require('./lib/agent')
+const util = require('./lib/core/util')
+const { InvalidArgumentError } = errors
+const api = require('./lib/api')
+const buildConnector = require('./lib/core/connect')
+const MockClient = require('./lib/mock/mock-client')
+const MockAgent = require('./lib/mock/mock-agent')
+const MockPool = require('./lib/mock/mock-pool')
+const mockErrors = require('./lib/mock/mock-errors')
+const ProxyAgent = require('./lib/proxy-agent')
+const RetryHandler = require('./lib/handler/RetryHandler')
+const { getGlobalDispatcher, setGlobalDispatcher } = require('./lib/global')
+const DecoratorHandler = require('./lib/handler/DecoratorHandler')
+const RedirectHandler = require('./lib/handler/RedirectHandler')
+const createRedirectInterceptor = require('./lib/interceptor/redirectInterceptor')
+
+let hasCrypto
+try {
+ require('crypto')
+ hasCrypto = true
+} catch {
+ hasCrypto = false
+}
+
+Object.assign(Dispatcher.prototype, api)
+
+module.exports.Dispatcher = Dispatcher
+module.exports.Client = Client
+module.exports.Pool = Pool
+module.exports.BalancedPool = BalancedPool
+module.exports.Agent = Agent
+module.exports.ProxyAgent = ProxyAgent
+module.exports.RetryHandler = RetryHandler
+
+module.exports.DecoratorHandler = DecoratorHandler
+module.exports.RedirectHandler = RedirectHandler
+module.exports.createRedirectInterceptor = createRedirectInterceptor
+
+module.exports.buildConnector = buildConnector
+module.exports.errors = errors
+
+function makeDispatcher (fn) {
+ return (url, opts, handler) => {
+ if (typeof opts === 'function') {
+ handler = opts
+ opts = null
+ }
+
+ if (!url || (typeof url !== 'string' && typeof url !== 'object' && !(url instanceof URL))) {
+ throw new InvalidArgumentError('invalid url')
+ }
+
+ if (opts != null && typeof opts !== 'object') {
+ throw new InvalidArgumentError('invalid opts')
+ }
+
+ if (opts && opts.path != null) {
+ if (typeof opts.path !== 'string') {
+ throw new InvalidArgumentError('invalid opts.path')
+ }
+
+ let path = opts.path
+ if (!opts.path.startsWith('/')) {
+ path = `/${path}`
+ }
+
+ url = new URL(util.parseOrigin(url).origin + path)
+ } else {
+ if (!opts) {
+ opts = typeof url === 'object' ? url : {}
+ }
+
+ url = util.parseURL(url)
+ }
+
+ const { agent, dispatcher = getGlobalDispatcher() } = opts
+
+ if (agent) {
+ throw new InvalidArgumentError('unsupported opts.agent. Did you mean opts.client?')
+ }
+
+ return fn.call(dispatcher, {
+ ...opts,
+ origin: url.origin,
+ path: url.search ? `${url.pathname}${url.search}` : url.pathname,
+ method: opts.method || (opts.body ? 'PUT' : 'GET')
+ }, handler)
+ }
+}
+
+module.exports.setGlobalDispatcher = setGlobalDispatcher
+module.exports.getGlobalDispatcher = getGlobalDispatcher
+
+if (util.nodeMajor > 16 || (util.nodeMajor === 16 && util.nodeMinor >= 8)) {
+ let fetchImpl = null
+ module.exports.fetch = async function fetch (resource) {
+ if (!fetchImpl) {
+ fetchImpl = require('./lib/fetch').fetch
+ }
+
+ try {
+ return await fetchImpl(...arguments)
+ } catch (err) {
+ if (typeof err === 'object') {
+ Error.captureStackTrace(err, this)
+ }
+
+ throw err
+ }
+ }
+ module.exports.Headers = require('./lib/fetch/headers').Headers
+ module.exports.Response = require('./lib/fetch/response').Response
+ module.exports.Request = require('./lib/fetch/request').Request
+ module.exports.FormData = require('./lib/fetch/formdata').FormData
+ module.exports.File = require('./lib/fetch/file').File
+ module.exports.FileReader = require('./lib/fileapi/filereader').FileReader
+
+ const { setGlobalOrigin, getGlobalOrigin } = require('./lib/fetch/global')
+
+ module.exports.setGlobalOrigin = setGlobalOrigin
+ module.exports.getGlobalOrigin = getGlobalOrigin
+
+ const { CacheStorage } = require('./lib/cache/cachestorage')
+ const { kConstruct } = require('./lib/cache/symbols')
+
+ // Cache & CacheStorage are tightly coupled with fetch. Even if it may run
+ // in an older version of Node, it doesn't have any use without fetch.
+ module.exports.caches = new CacheStorage(kConstruct)
+}
+
+if (util.nodeMajor >= 16) {
+ const { deleteCookie, getCookies, getSetCookies, setCookie } = require('./lib/cookies')
+
+ module.exports.deleteCookie = deleteCookie
+ module.exports.getCookies = getCookies
+ module.exports.getSetCookies = getSetCookies
+ module.exports.setCookie = setCookie
+
+ const { parseMIMEType, serializeAMimeType } = require('./lib/fetch/dataURL')
+
+ module.exports.parseMIMEType = parseMIMEType
+ module.exports.serializeAMimeType = serializeAMimeType
+}
+
+if (util.nodeMajor >= 18 && hasCrypto) {
+ const { WebSocket } = require('./lib/websocket/websocket')
+
+ module.exports.WebSocket = WebSocket
+}
+
+module.exports.request = makeDispatcher(api.request)
+module.exports.stream = makeDispatcher(api.stream)
+module.exports.pipeline = makeDispatcher(api.pipeline)
+module.exports.connect = makeDispatcher(api.connect)
+module.exports.upgrade = makeDispatcher(api.upgrade)
+
+module.exports.MockClient = MockClient
+module.exports.MockPool = MockPool
+module.exports.MockAgent = MockAgent
+module.exports.mockErrors = mockErrors
diff --git a/lib/agent.js b/lib/agent.js
new file mode 100644
index 0000000..0b18f2a
--- /dev/null
+++ b/lib/agent.js
@@ -0,0 +1,148 @@
+'use strict'
+
+const { InvalidArgumentError } = require('./core/errors')
+const { kClients, kRunning, kClose, kDestroy, kDispatch, kInterceptors } = require('./core/symbols')
+const DispatcherBase = require('./dispatcher-base')
+const Pool = require('./pool')
+const Client = require('./client')
+const util = require('./core/util')
+const createRedirectInterceptor = require('./interceptor/redirectInterceptor')
+const { WeakRef, FinalizationRegistry } = require('./compat/dispatcher-weakref')()
+
+const kOnConnect = Symbol('onConnect')
+const kOnDisconnect = Symbol('onDisconnect')
+const kOnConnectionError = Symbol('onConnectionError')
+const kMaxRedirections = Symbol('maxRedirections')
+const kOnDrain = Symbol('onDrain')
+const kFactory = Symbol('factory')
+const kFinalizer = Symbol('finalizer')
+const kOptions = Symbol('options')
+
+function defaultFactory (origin, opts) {
+ return opts && opts.connections === 1
+ ? new Client(origin, opts)
+ : new Pool(origin, opts)
+}
+
+class Agent extends DispatcherBase {
+ constructor ({ factory = defaultFactory, maxRedirections = 0, connect, ...options } = {}) {
+ super()
+
+ if (typeof factory !== 'function') {
+ throw new InvalidArgumentError('factory must be a function.')
+ }
+
+ if (connect != null && typeof connect !== 'function' && typeof connect !== 'object') {
+ throw new InvalidArgumentError('connect must be a function or an object')
+ }
+
+ if (!Number.isInteger(maxRedirections) || maxRedirections < 0) {
+ throw new InvalidArgumentError('maxRedirections must be a positive number')
+ }
+
+ if (connect && typeof connect !== 'function') {
+ connect = { ...connect }
+ }
+
+ this[kInterceptors] = options.interceptors && options.interceptors.Agent && Array.isArray(options.interceptors.Agent)
+ ? options.interceptors.Agent
+ : [createRedirectInterceptor({ maxRedirections })]
+
+ this[kOptions] = { ...util.deepClone(options), connect }
+ this[kOptions].interceptors = options.interceptors
+ ? { ...options.interceptors }
+ : undefined
+ this[kMaxRedirections] = maxRedirections
+ this[kFactory] = factory
+ this[kClients] = new Map()
+ this[kFinalizer] = new FinalizationRegistry(/* istanbul ignore next: gc is undeterministic */ key => {
+ const ref = this[kClients].get(key)
+ if (ref !== undefined && ref.deref() === undefined) {
+ this[kClients].delete(key)
+ }
+ })
+
+ const agent = this
+
+ this[kOnDrain] = (origin, targets) => {
+ agent.emit('drain', origin, [agent, ...targets])
+ }
+
+ this[kOnConnect] = (origin, targets) => {
+ agent.emit('connect', origin, [agent, ...targets])
+ }
+
+ this[kOnDisconnect] = (origin, targets, err) => {
+ agent.emit('disconnect', origin, [agent, ...targets], err)
+ }
+
+ this[kOnConnectionError] = (origin, targets, err) => {
+ agent.emit('connectionError', origin, [agent, ...targets], err)
+ }
+ }
+
+ get [kRunning] () {
+ let ret = 0
+ for (const ref of this[kClients].values()) {
+ const client = ref.deref()
+ /* istanbul ignore next: gc is undeterministic */
+ if (client) {
+ ret += client[kRunning]
+ }
+ }
+ return ret
+ }
+
+ [kDispatch] (opts, handler) {
+ let key
+ if (opts.origin && (typeof opts.origin === 'string' || opts.origin instanceof URL)) {
+ key = String(opts.origin)
+ } else {
+ throw new InvalidArgumentError('opts.origin must be a non-empty string or URL.')
+ }
+
+ const ref = this[kClients].get(key)
+
+ let dispatcher = ref ? ref.deref() : null
+ if (!dispatcher) {
+ dispatcher = this[kFactory](opts.origin, this[kOptions])
+ .on('drain', this[kOnDrain])
+ .on('connect', this[kOnConnect])
+ .on('disconnect', this[kOnDisconnect])
+ .on('connectionError', this[kOnConnectionError])
+
+ this[kClients].set(key, new WeakRef(dispatcher))
+ this[kFinalizer].register(dispatcher, key)
+ }
+
+ return dispatcher.dispatch(opts, handler)
+ }
+
+ async [kClose] () {
+ const closePromises = []
+ for (const ref of this[kClients].values()) {
+ const client = ref.deref()
+ /* istanbul ignore else: gc is undeterministic */
+ if (client) {
+ closePromises.push(client.close())
+ }
+ }
+
+ await Promise.all(closePromises)
+ }
+
+ async [kDestroy] (err) {
+ const destroyPromises = []
+ for (const ref of this[kClients].values()) {
+ const client = ref.deref()
+ /* istanbul ignore else: gc is undeterministic */
+ if (client) {
+ destroyPromises.push(client.destroy(err))
+ }
+ }
+
+ await Promise.all(destroyPromises)
+ }
+}
+
+module.exports = Agent
diff --git a/lib/api/abort-signal.js b/lib/api/abort-signal.js
new file mode 100644
index 0000000..2985c1e
--- /dev/null
+++ b/lib/api/abort-signal.js
@@ -0,0 +1,54 @@
+const { addAbortListener } = require('../core/util')
+const { RequestAbortedError } = require('../core/errors')
+
+const kListener = Symbol('kListener')
+const kSignal = Symbol('kSignal')
+
+function abort (self) {
+ if (self.abort) {
+ self.abort()
+ } else {
+ self.onError(new RequestAbortedError())
+ }
+}
+
+function addSignal (self, signal) {
+ self[kSignal] = null
+ self[kListener] = null
+
+ if (!signal) {
+ return
+ }
+
+ if (signal.aborted) {
+ abort(self)
+ return
+ }
+
+ self[kSignal] = signal
+ self[kListener] = () => {
+ abort(self)
+ }
+
+ addAbortListener(self[kSignal], self[kListener])
+}
+
+function removeSignal (self) {
+ if (!self[kSignal]) {
+ return
+ }
+
+ if ('removeEventListener' in self[kSignal]) {
+ self[kSignal].removeEventListener('abort', self[kListener])
+ } else {
+ self[kSignal].removeListener('abort', self[kListener])
+ }
+
+ self[kSignal] = null
+ self[kListener] = null
+}
+
+module.exports = {
+ addSignal,
+ removeSignal
+}
diff --git a/lib/api/api-connect.js b/lib/api/api-connect.js
new file mode 100644
index 0000000..fd2b6ad
--- /dev/null
+++ b/lib/api/api-connect.js
@@ -0,0 +1,104 @@
+'use strict'
+
+const { AsyncResource } = require('async_hooks')
+const { InvalidArgumentError, RequestAbortedError, SocketError } = require('../core/errors')
+const util = require('../core/util')
+const { addSignal, removeSignal } = require('./abort-signal')
+
+class ConnectHandler extends AsyncResource {
+ constructor (opts, callback) {
+ if (!opts || typeof opts !== 'object') {
+ throw new InvalidArgumentError('invalid opts')
+ }
+
+ if (typeof callback !== 'function') {
+ throw new InvalidArgumentError('invalid callback')
+ }
+
+ const { signal, opaque, responseHeaders } = opts
+
+ if (signal && typeof signal.on !== 'function' && typeof signal.addEventListener !== 'function') {
+ throw new InvalidArgumentError('signal must be an EventEmitter or EventTarget')
+ }
+
+ super('UNDICI_CONNECT')
+
+ this.opaque = opaque || null
+ this.responseHeaders = responseHeaders || null
+ this.callback = callback
+ this.abort = null
+
+ addSignal(this, signal)
+ }
+
+ onConnect (abort, context) {
+ if (!this.callback) {
+ throw new RequestAbortedError()
+ }
+
+ this.abort = abort
+ this.context = context
+ }
+
+ onHeaders () {
+ throw new SocketError('bad connect', null)
+ }
+
+ onUpgrade (statusCode, rawHeaders, socket) {
+ const { callback, opaque, context } = this
+
+ removeSignal(this)
+
+ this.callback = null
+
+ let headers = rawHeaders
+ // Indicates is an HTTP2Session
+ if (headers != null) {
+ headers = this.responseHeaders === 'raw' ? util.parseRawHeaders(rawHeaders) : util.parseHeaders(rawHeaders)
+ }
+
+ this.runInAsyncScope(callback, null, null, {
+ statusCode,
+ headers,
+ socket,
+ opaque,
+ context
+ })
+ }
+
+ onError (err) {
+ const { callback, opaque } = this
+
+ removeSignal(this)
+
+ if (callback) {
+ this.callback = null
+ queueMicrotask(() => {
+ this.runInAsyncScope(callback, null, err, { opaque })
+ })
+ }
+ }
+}
+
+function connect (opts, callback) {
+ if (callback === undefined) {
+ return new Promise((resolve, reject) => {
+ connect.call(this, opts, (err, data) => {
+ return err ? reject(err) : resolve(data)
+ })
+ })
+ }
+
+ try {
+ const connectHandler = new ConnectHandler(opts, callback)
+ this.dispatch({ ...opts, method: 'CONNECT' }, connectHandler)
+ } catch (err) {
+ if (typeof callback !== 'function') {
+ throw err
+ }
+ const opaque = opts && opts.opaque
+ queueMicrotask(() => callback(err, { opaque }))
+ }
+}
+
+module.exports = connect
diff --git a/lib/api/api-pipeline.js b/lib/api/api-pipeline.js
new file mode 100644
index 0000000..af4a180
--- /dev/null
+++ b/lib/api/api-pipeline.js
@@ -0,0 +1,249 @@
+'use strict'
+
+const {
+ Readable,
+ Duplex,
+ PassThrough
+} = require('stream')
+const {
+ InvalidArgumentError,
+ InvalidReturnValueError,
+ RequestAbortedError
+} = require('../core/errors')
+const util = require('../core/util')
+const { AsyncResource } = require('async_hooks')
+const { addSignal, removeSignal } = require('./abort-signal')
+const assert = require('assert')
+
+const kResume = Symbol('resume')
+
+class PipelineRequest extends Readable {
+ constructor () {
+ super({ autoDestroy: true })
+
+ this[kResume] = null
+ }
+
+ _read () {
+ const { [kResume]: resume } = this
+
+ if (resume) {
+ this[kResume] = null
+ resume()
+ }
+ }
+
+ _destroy (err, callback) {
+ this._read()
+
+ callback(err)
+ }
+}
+
+class PipelineResponse extends Readable {
+ constructor (resume) {
+ super({ autoDestroy: true })
+ this[kResume] = resume
+ }
+
+ _read () {
+ this[kResume]()
+ }
+
+ _destroy (err, callback) {
+ if (!err && !this._readableState.endEmitted) {
+ err = new RequestAbortedError()
+ }
+
+ callback(err)
+ }
+}
+
+class PipelineHandler extends AsyncResource {
+ constructor (opts, handler) {
+ if (!opts || typeof opts !== 'object') {
+ throw new InvalidArgumentError('invalid opts')
+ }
+
+ if (typeof handler !== 'function') {
+ throw new InvalidArgumentError('invalid handler')
+ }
+
+ const { signal, method, opaque, onInfo, responseHeaders } = opts
+
+ if (signal && typeof signal.on !== 'function' && typeof signal.addEventListener !== 'function') {
+ throw new InvalidArgumentError('signal must be an EventEmitter or EventTarget')
+ }
+
+ if (method === 'CONNECT') {
+ throw new InvalidArgumentError('invalid method')
+ }
+
+ if (onInfo && typeof onInfo !== 'function') {
+ throw new InvalidArgumentError('invalid onInfo callback')
+ }
+
+ super('UNDICI_PIPELINE')
+
+ this.opaque = opaque || null
+ this.responseHeaders = responseHeaders || null
+ this.handler = handler
+ this.abort = null
+ this.context = null
+ this.onInfo = onInfo || null
+
+ this.req = new PipelineRequest().on('error', util.nop)
+
+ this.ret = new Duplex({
+ readableObjectMode: opts.objectMode,
+ autoDestroy: true,
+ read: () => {
+ const { body } = this
+
+ if (body && body.resume) {
+ body.resume()
+ }
+ },
+ write: (chunk, encoding, callback) => {
+ const { req } = this
+
+ if (req.push(chunk, encoding) || req._readableState.destroyed) {
+ callback()
+ } else {
+ req[kResume] = callback
+ }
+ },
+ destroy: (err, callback) => {
+ const { body, req, res, ret, abort } = this
+
+ if (!err && !ret._readableState.endEmitted) {
+ err = new RequestAbortedError()
+ }
+
+ if (abort && err) {
+ abort()
+ }
+
+ util.destroy(body, err)
+ util.destroy(req, err)
+ util.destroy(res, err)
+
+ removeSignal(this)
+
+ callback(err)
+ }
+ }).on('prefinish', () => {
+ const { req } = this
+
+ // Node < 15 does not call _final in same tick.
+ req.push(null)
+ })
+
+ this.res = null
+
+ addSignal(this, signal)
+ }
+
+ onConnect (abort, context) {
+ const { ret, res } = this
+
+ assert(!res, 'pipeline cannot be retried')
+
+ if (ret.destroyed) {
+ throw new RequestAbortedError()
+ }
+
+ this.abort = abort
+ this.context = context
+ }
+
+ onHeaders (statusCode, rawHeaders, resume) {
+ const { opaque, handler, context } = this
+
+ if (statusCode < 200) {
+ if (this.onInfo) {
+ const headers = this.responseHeaders === 'raw' ? util.parseRawHeaders(rawHeaders) : util.parseHeaders(rawHeaders)
+ this.onInfo({ statusCode, headers })
+ }
+ return
+ }
+
+ this.res = new PipelineResponse(resume)
+
+ let body
+ try {
+ this.handler = null
+ const headers = this.responseHeaders === 'raw' ? util.parseRawHeaders(rawHeaders) : util.parseHeaders(rawHeaders)
+ body = this.runInAsyncScope(handler, null, {
+ statusCode,
+ headers,
+ opaque,
+ body: this.res,
+ context
+ })
+ } catch (err) {
+ this.res.on('error', util.nop)
+ throw err
+ }
+
+ if (!body || typeof body.on !== 'function') {
+ throw new InvalidReturnValueError('expected Readable')
+ }
+
+ body
+ .on('data', (chunk) => {
+ const { ret, body } = this
+
+ if (!ret.push(chunk) && body.pause) {
+ body.pause()
+ }
+ })
+ .on('error', (err) => {
+ const { ret } = this
+
+ util.destroy(ret, err)
+ })
+ .on('end', () => {
+ const { ret } = this
+
+ ret.push(null)
+ })
+ .on('close', () => {
+ const { ret } = this
+
+ if (!ret._readableState.ended) {
+ util.destroy(ret, new RequestAbortedError())
+ }
+ })
+
+ this.body = body
+ }
+
+ onData (chunk) {
+ const { res } = this
+ return res.push(chunk)
+ }
+
+ onComplete (trailers) {
+ const { res } = this
+ res.push(null)
+ }
+
+ onError (err) {
+ const { ret } = this
+ this.handler = null
+ util.destroy(ret, err)
+ }
+}
+
+function pipeline (opts, handler) {
+ try {
+ const pipelineHandler = new PipelineHandler(opts, handler)
+ this.dispatch({ ...opts, body: pipelineHandler.req }, pipelineHandler)
+ return pipelineHandler.ret
+ } catch (err) {
+ return new PassThrough().destroy(err)
+ }
+}
+
+module.exports = pipeline
diff --git a/lib/api/api-request.js b/lib/api/api-request.js
new file mode 100644
index 0000000..d4281ce
--- /dev/null
+++ b/lib/api/api-request.js
@@ -0,0 +1,180 @@
+'use strict'
+
+const Readable = require('./readable')
+const {
+ InvalidArgumentError,
+ RequestAbortedError
+} = require('../core/errors')
+const util = require('../core/util')
+const { getResolveErrorBodyCallback } = require('./util')
+const { AsyncResource } = require('async_hooks')
+const { addSignal, removeSignal } = require('./abort-signal')
+
+class RequestHandler extends AsyncResource {
+ constructor (opts, callback) {
+ if (!opts || typeof opts !== 'object') {
+ throw new InvalidArgumentError('invalid opts')
+ }
+
+ const { signal, method, opaque, body, onInfo, responseHeaders, throwOnError, highWaterMark } = opts
+
+ try {
+ if (typeof callback !== 'function') {
+ throw new InvalidArgumentError('invalid callback')
+ }
+
+ if (highWaterMark && (typeof highWaterMark !== 'number' || highWaterMark < 0)) {
+ throw new InvalidArgumentError('invalid highWaterMark')
+ }
+
+ if (signal && typeof signal.on !== 'function' && typeof signal.addEventListener !== 'function') {
+ throw new InvalidArgumentError('signal must be an EventEmitter or EventTarget')
+ }
+
+ if (method === 'CONNECT') {
+ throw new InvalidArgumentError('invalid method')
+ }
+
+ if (onInfo && typeof onInfo !== 'function') {
+ throw new InvalidArgumentError('invalid onInfo callback')
+ }
+
+ super('UNDICI_REQUEST')
+ } catch (err) {
+ if (util.isStream(body)) {
+ util.destroy(body.on('error', util.nop), err)
+ }
+ throw err
+ }
+
+ this.responseHeaders = responseHeaders || null
+ this.opaque = opaque || null
+ this.callback = callback
+ this.res = null
+ this.abort = null
+ this.body = body
+ this.trailers = {}
+ this.context = null
+ this.onInfo = onInfo || null
+ this.throwOnError = throwOnError
+ this.highWaterMark = highWaterMark
+
+ if (util.isStream(body)) {
+ body.on('error', (err) => {
+ this.onError(err)
+ })
+ }
+
+ addSignal(this, signal)
+ }
+
+ onConnect (abort, context) {
+ if (!this.callback) {
+ throw new RequestAbortedError()
+ }
+
+ this.abort = abort
+ this.context = context
+ }
+
+ onHeaders (statusCode, rawHeaders, resume, statusMessage) {
+ const { callback, opaque, abort, context, responseHeaders, highWaterMark } = this
+
+ const headers = responseHeaders === 'raw' ? util.parseRawHeaders(rawHeaders) : util.parseHeaders(rawHeaders)
+
+ if (statusCode < 200) {
+ if (this.onInfo) {
+ this.onInfo({ statusCode, headers })
+ }
+ return
+ }
+
+ const parsedHeaders = responseHeaders === 'raw' ? util.parseHeaders(rawHeaders) : headers
+ const contentType = parsedHeaders['content-type']
+ const body = new Readable({ resume, abort, contentType, highWaterMark })
+
+ this.callback = null
+ this.res = body
+ if (callback !== null) {
+ if (this.throwOnError && statusCode >= 400) {
+ this.runInAsyncScope(getResolveErrorBodyCallback, null,
+ { callback, body, contentType, statusCode, statusMessage, headers }
+ )
+ } else {
+ this.runInAsyncScope(callback, null, null, {
+ statusCode,
+ headers,
+ trailers: this.trailers,
+ opaque,
+ body,
+ context
+ })
+ }
+ }
+ }
+
+ onData (chunk) {
+ const { res } = this
+ return res.push(chunk)
+ }
+
+ onComplete (trailers) {
+ const { res } = this
+
+ removeSignal(this)
+
+ util.parseHeaders(trailers, this.trailers)
+
+ res.push(null)
+ }
+
+ onError (err) {
+ const { res, callback, body, opaque } = this
+
+ removeSignal(this)
+
+ if (callback) {
+ // TODO: Does this need queueMicrotask?
+ this.callback = null
+ queueMicrotask(() => {
+ this.runInAsyncScope(callback, null, err, { opaque })
+ })
+ }
+
+ if (res) {
+ this.res = null
+ // Ensure all queued handlers are invoked before destroying res.
+ queueMicrotask(() => {
+ util.destroy(res, err)
+ })
+ }
+
+ if (body) {
+ this.body = null
+ util.destroy(body, err)
+ }
+ }
+}
+
+function request (opts, callback) {
+ if (callback === undefined) {
+ return new Promise((resolve, reject) => {
+ request.call(this, opts, (err, data) => {
+ return err ? reject(err) : resolve(data)
+ })
+ })
+ }
+
+ try {
+ this.dispatch(opts, new RequestHandler(opts, callback))
+ } catch (err) {
+ if (typeof callback !== 'function') {
+ throw err
+ }
+ const opaque = opts && opts.opaque
+ queueMicrotask(() => callback(err, { opaque }))
+ }
+}
+
+module.exports = request
+module.exports.RequestHandler = RequestHandler
diff --git a/lib/api/api-stream.js b/lib/api/api-stream.js
new file mode 100644
index 0000000..c571a6f
--- /dev/null
+++ b/lib/api/api-stream.js
@@ -0,0 +1,220 @@
+'use strict'
+
+const { finished, PassThrough } = require('stream')
+const {
+ InvalidArgumentError,
+ InvalidReturnValueError,
+ RequestAbortedError
+} = require('../core/errors')
+const util = require('../core/util')
+const { getResolveErrorBodyCallback } = require('./util')
+const { AsyncResource } = require('async_hooks')
+const { addSignal, removeSignal } = require('./abort-signal')
+
+class StreamHandler extends AsyncResource {
+ constructor (opts, factory, callback) {
+ if (!opts || typeof opts !== 'object') {
+ throw new InvalidArgumentError('invalid opts')
+ }
+
+ const { signal, method, opaque, body, onInfo, responseHeaders, throwOnError } = opts
+
+ try {
+ if (typeof callback !== 'function') {
+ throw new InvalidArgumentError('invalid callback')
+ }
+
+ if (typeof factory !== 'function') {
+ throw new InvalidArgumentError('invalid factory')
+ }
+
+ if (signal && typeof signal.on !== 'function' && typeof signal.addEventListener !== 'function') {
+ throw new InvalidArgumentError('signal must be an EventEmitter or EventTarget')
+ }
+
+ if (method === 'CONNECT') {
+ throw new InvalidArgumentError('invalid method')
+ }
+
+ if (onInfo && typeof onInfo !== 'function') {
+ throw new InvalidArgumentError('invalid onInfo callback')
+ }
+
+ super('UNDICI_STREAM')
+ } catch (err) {
+ if (util.isStream(body)) {
+ util.destroy(body.on('error', util.nop), err)
+ }
+ throw err
+ }
+
+ this.responseHeaders = responseHeaders || null
+ this.opaque = opaque || null
+ this.factory = factory
+ this.callback = callback
+ this.res = null
+ this.abort = null
+ this.context = null
+ this.trailers = null
+ this.body = body
+ this.onInfo = onInfo || null
+ this.throwOnError = throwOnError || false
+
+ if (util.isStream(body)) {
+ body.on('error', (err) => {
+ this.onError(err)
+ })
+ }
+
+ addSignal(this, signal)
+ }
+
+ onConnect (abort, context) {
+ if (!this.callback) {
+ throw new RequestAbortedError()
+ }
+
+ this.abort = abort
+ this.context = context
+ }
+
+ onHeaders (statusCode, rawHeaders, resume, statusMessage) {
+ const { factory, opaque, context, callback, responseHeaders } = this
+
+ const headers = responseHeaders === 'raw' ? util.parseRawHeaders(rawHeaders) : util.parseHeaders(rawHeaders)
+
+ if (statusCode < 200) {
+ if (this.onInfo) {
+ this.onInfo({ statusCode, headers })
+ }
+ return
+ }
+
+ this.factory = null
+
+ let res
+
+ if (this.throwOnError && statusCode >= 400) {
+ const parsedHeaders = responseHeaders === 'raw' ? util.parseHeaders(rawHeaders) : headers
+ const contentType = parsedHeaders['content-type']
+ res = new PassThrough()
+
+ this.callback = null
+ this.runInAsyncScope(getResolveErrorBodyCallback, null,
+ { callback, body: res, contentType, statusCode, statusMessage, headers }
+ )
+ } else {
+ if (factory === null) {
+ return
+ }
+
+ res = this.runInAsyncScope(factory, null, {
+ statusCode,
+ headers,
+ opaque,
+ context
+ })
+
+ if (
+ !res ||
+ typeof res.write !== 'function' ||
+ typeof res.end !== 'function' ||
+ typeof res.on !== 'function'
+ ) {
+ throw new InvalidReturnValueError('expected Writable')
+ }
+
+ // TODO: Avoid finished. It registers an unnecessary amount of listeners.
+ finished(res, { readable: false }, (err) => {
+ const { callback, res, opaque, trailers, abort } = this
+
+ this.res = null
+ if (err || !res.readable) {
+ util.destroy(res, err)
+ }
+
+ this.callback = null
+ this.runInAsyncScope(callback, null, err || null, { opaque, trailers })
+
+ if (err) {
+ abort()
+ }
+ })
+ }
+
+ res.on('drain', resume)
+
+ this.res = res
+
+ const needDrain = res.writableNeedDrain !== undefined
+ ? res.writableNeedDrain
+ : res._writableState && res._writableState.needDrain
+
+ return needDrain !== true
+ }
+
+ onData (chunk) {
+ const { res } = this
+
+ return res ? res.write(chunk) : true
+ }
+
+ onComplete (trailers) {
+ const { res } = this
+
+ removeSignal(this)
+
+ if (!res) {
+ return
+ }
+
+ this.trailers = util.parseHeaders(trailers)
+
+ res.end()
+ }
+
+ onError (err) {
+ const { res, callback, opaque, body } = this
+
+ removeSignal(this)
+
+ this.factory = null
+
+ if (res) {
+ this.res = null
+ util.destroy(res, err)
+ } else if (callback) {
+ this.callback = null
+ queueMicrotask(() => {
+ this.runInAsyncScope(callback, null, err, { opaque })
+ })
+ }
+
+ if (body) {
+ this.body = null
+ util.destroy(body, err)
+ }
+ }
+}
+
+function stream (opts, factory, callback) {
+ if (callback === undefined) {
+ return new Promise((resolve, reject) => {
+ stream.call(this, opts, factory, (err, data) => {
+ return err ? reject(err) : resolve(data)
+ })
+ })
+ }
+
+ try {
+ this.dispatch(opts, new StreamHandler(opts, factory, callback))
+ } catch (err) {
+ if (typeof callback !== 'function') {
+ throw err
+ }
+ const opaque = opts && opts.opaque
+ queueMicrotask(() => callback(err, { opaque }))
+ }
+}
+
+module.exports = stream
diff --git a/lib/api/api-upgrade.js b/lib/api/api-upgrade.js
new file mode 100644
index 0000000..ef783e8
--- /dev/null
+++ b/lib/api/api-upgrade.js
@@ -0,0 +1,105 @@
+'use strict'
+
+const { InvalidArgumentError, RequestAbortedError, SocketError } = require('../core/errors')
+const { AsyncResource } = require('async_hooks')
+const util = require('../core/util')
+const { addSignal, removeSignal } = require('./abort-signal')
+const assert = require('assert')
+
+class UpgradeHandler extends AsyncResource {
+ constructor (opts, callback) {
+ if (!opts || typeof opts !== 'object') {
+ throw new InvalidArgumentError('invalid opts')
+ }
+
+ if (typeof callback !== 'function') {
+ throw new InvalidArgumentError('invalid callback')
+ }
+
+ const { signal, opaque, responseHeaders } = opts
+
+ if (signal && typeof signal.on !== 'function' && typeof signal.addEventListener !== 'function') {
+ throw new InvalidArgumentError('signal must be an EventEmitter or EventTarget')
+ }
+
+ super('UNDICI_UPGRADE')
+
+ this.responseHeaders = responseHeaders || null
+ this.opaque = opaque || null
+ this.callback = callback
+ this.abort = null
+ this.context = null
+
+ addSignal(this, signal)
+ }
+
+ onConnect (abort, context) {
+ if (!this.callback) {
+ throw new RequestAbortedError()
+ }
+
+ this.abort = abort
+ this.context = null
+ }
+
+ onHeaders () {
+ throw new SocketError('bad upgrade', null)
+ }
+
+ onUpgrade (statusCode, rawHeaders, socket) {
+ const { callback, opaque, context } = this
+
+ assert.strictEqual(statusCode, 101)
+
+ removeSignal(this)
+
+ this.callback = null
+ const headers = this.responseHeaders === 'raw' ? util.parseRawHeaders(rawHeaders) : util.parseHeaders(rawHeaders)
+ this.runInAsyncScope(callback, null, null, {
+ headers,
+ socket,
+ opaque,
+ context
+ })
+ }
+
+ onError (err) {
+ const { callback, opaque } = this
+
+ removeSignal(this)
+
+ if (callback) {
+ this.callback = null
+ queueMicrotask(() => {
+ this.runInAsyncScope(callback, null, err, { opaque })
+ })
+ }
+ }
+}
+
+function upgrade (opts, callback) {
+ if (callback === undefined) {
+ return new Promise((resolve, reject) => {
+ upgrade.call(this, opts, (err, data) => {
+ return err ? reject(err) : resolve(data)
+ })
+ })
+ }
+
+ try {
+ const upgradeHandler = new UpgradeHandler(opts, callback)
+ this.dispatch({
+ ...opts,
+ method: opts.method || 'GET',
+ upgrade: opts.protocol || 'Websocket'
+ }, upgradeHandler)
+ } catch (err) {
+ if (typeof callback !== 'function') {
+ throw err
+ }
+ const opaque = opts && opts.opaque
+ queueMicrotask(() => callback(err, { opaque }))
+ }
+}
+
+module.exports = upgrade
diff --git a/lib/api/index.js b/lib/api/index.js
new file mode 100644
index 0000000..8983a5e
--- /dev/null
+++ b/lib/api/index.js
@@ -0,0 +1,7 @@
+'use strict'
+
+module.exports.request = require('./api-request')
+module.exports.stream = require('./api-stream')
+module.exports.pipeline = require('./api-pipeline')
+module.exports.upgrade = require('./api-upgrade')
+module.exports.connect = require('./api-connect')
diff --git a/lib/api/readable.js b/lib/api/readable.js
new file mode 100644
index 0000000..5269dfa
--- /dev/null
+++ b/lib/api/readable.js
@@ -0,0 +1,322 @@
+// Ported from https://github.com/nodejs/undici/pull/907
+
+'use strict'
+
+const assert = require('assert')
+const { Readable } = require('stream')
+const { RequestAbortedError, NotSupportedError, InvalidArgumentError } = require('../core/errors')
+const util = require('../core/util')
+const { ReadableStreamFrom, toUSVString } = require('../core/util')
+
+let Blob
+
+const kConsume = Symbol('kConsume')
+const kReading = Symbol('kReading')
+const kBody = Symbol('kBody')
+const kAbort = Symbol('abort')
+const kContentType = Symbol('kContentType')
+
+const noop = () => {}
+
+module.exports = class BodyReadable extends Readable {
+ constructor ({
+ resume,
+ abort,
+ contentType = '',
+ highWaterMark = 64 * 1024 // Same as nodejs fs streams.
+ }) {
+ super({
+ autoDestroy: true,
+ read: resume,
+ highWaterMark
+ })
+
+ this._readableState.dataEmitted = false
+
+ this[kAbort] = abort
+ this[kConsume] = null
+ this[kBody] = null
+ this[kContentType] = contentType
+
+ // Is stream being consumed through Readable API?
+ // This is an optimization so that we avoid checking
+ // for 'data' and 'readable' listeners in the hot path
+ // inside push().
+ this[kReading] = false
+ }
+
+ destroy (err) {
+ if (this.destroyed) {
+ // Node < 16
+ return this
+ }
+
+ if (!err && !this._readableState.endEmitted) {
+ err = new RequestAbortedError()
+ }
+
+ if (err) {
+ this[kAbort]()
+ }
+
+ return super.destroy(err)
+ }
+
+ emit (ev, ...args) {
+ if (ev === 'data') {
+ // Node < 16.7
+ this._readableState.dataEmitted = true
+ } else if (ev === 'error') {
+ // Node < 16
+ this._readableState.errorEmitted = true
+ }
+ return super.emit(ev, ...args)
+ }
+
+ on (ev, ...args) {
+ if (ev === 'data' || ev === 'readable') {
+ this[kReading] = true
+ }
+ return super.on(ev, ...args)
+ }
+
+ addListener (ev, ...args) {
+ return this.on(ev, ...args)
+ }
+
+ off (ev, ...args) {
+ const ret = super.off(ev, ...args)
+ if (ev === 'data' || ev === 'readable') {
+ this[kReading] = (
+ this.listenerCount('data') > 0 ||
+ this.listenerCount('readable') > 0
+ )
+ }
+ return ret
+ }
+
+ removeListener (ev, ...args) {
+ return this.off(ev, ...args)
+ }
+
+ push (chunk) {
+ if (this[kConsume] && chunk !== null && this.readableLength === 0) {
+ consumePush(this[kConsume], chunk)
+ return this[kReading] ? super.push(chunk) : true
+ }
+ return super.push(chunk)
+ }
+
+ // https://fetch.spec.whatwg.org/#dom-body-text
+ async text () {
+ return consume(this, 'text')
+ }
+
+ // https://fetch.spec.whatwg.org/#dom-body-json
+ async json () {
+ return consume(this, 'json')
+ }
+
+ // https://fetch.spec.whatwg.org/#dom-body-blob
+ async blob () {
+ return consume(this, 'blob')
+ }
+
+ // https://fetch.spec.whatwg.org/#dom-body-arraybuffer
+ async arrayBuffer () {
+ return consume(this, 'arrayBuffer')
+ }
+
+ // https://fetch.spec.whatwg.org/#dom-body-formdata
+ async formData () {
+ // TODO: Implement.
+ throw new NotSupportedError()
+ }
+
+ // https://fetch.spec.whatwg.org/#dom-body-bodyused
+ get bodyUsed () {
+ return util.isDisturbed(this)
+ }
+
+ // https://fetch.spec.whatwg.org/#dom-body-body
+ get body () {
+ if (!this[kBody]) {
+ this[kBody] = ReadableStreamFrom(this)
+ if (this[kConsume]) {
+ // TODO: Is this the best way to force a lock?
+ this[kBody].getReader() // Ensure stream is locked.
+ assert(this[kBody].locked)
+ }
+ }
+ return this[kBody]
+ }
+
+ dump (opts) {
+ let limit = opts && Number.isFinite(opts.limit) ? opts.limit : 262144
+ const signal = opts && opts.signal
+
+ if (signal) {
+ try {
+ if (typeof signal !== 'object' || !('aborted' in signal)) {
+ throw new InvalidArgumentError('signal must be an AbortSignal')
+ }
+ util.throwIfAborted(signal)
+ } catch (err) {
+ return Promise.reject(err)
+ }
+ }
+
+ if (this.closed) {
+ return Promise.resolve(null)
+ }
+
+ return new Promise((resolve, reject) => {
+ const signalListenerCleanup = signal
+ ? util.addAbortListener(signal, () => {
+ this.destroy()
+ })
+ : noop
+
+ this
+ .on('close', function () {
+ signalListenerCleanup()
+ if (signal && signal.aborted) {
+ reject(signal.reason || Object.assign(new Error('The operation was aborted'), { name: 'AbortError' }))
+ } else {
+ resolve(null)
+ }
+ })
+ .on('error', noop)
+ .on('data', function (chunk) {
+ limit -= chunk.length
+ if (limit <= 0) {
+ this.destroy()
+ }
+ })
+ .resume()
+ })
+ }
+}
+
+// https://streams.spec.whatwg.org/#readablestream-locked
+function isLocked (self) {
+ // Consume is an implicit lock.
+ return (self[kBody] && self[kBody].locked === true) || self[kConsume]
+}
+
+// https://fetch.spec.whatwg.org/#body-unusable
+function isUnusable (self) {
+ return util.isDisturbed(self) || isLocked(self)
+}
+
+async function consume (stream, type) {
+ if (isUnusable(stream)) {
+ throw new TypeError('unusable')
+ }
+
+ assert(!stream[kConsume])
+
+ return new Promise((resolve, reject) => {
+ stream[kConsume] = {
+ type,
+ stream,
+ resolve,
+ reject,
+ length: 0,
+ body: []
+ }
+
+ stream
+ .on('error', function (err) {
+ consumeFinish(this[kConsume], err)
+ })
+ .on('close', function () {
+ if (this[kConsume].body !== null) {
+ consumeFinish(this[kConsume], new RequestAbortedError())
+ }
+ })
+
+ process.nextTick(consumeStart, stream[kConsume])
+ })
+}
+
+function consumeStart (consume) {
+ if (consume.body === null) {
+ return
+ }
+
+ const { _readableState: state } = consume.stream
+
+ for (const chunk of state.buffer) {
+ consumePush(consume, chunk)
+ }
+
+ if (state.endEmitted) {
+ consumeEnd(this[kConsume])
+ } else {
+ consume.stream.on('end', function () {
+ consumeEnd(this[kConsume])
+ })
+ }
+
+ consume.stream.resume()
+
+ while (consume.stream.read() != null) {
+ // Loop
+ }
+}
+
+function consumeEnd (consume) {
+ const { type, body, resolve, stream, length } = consume
+
+ try {
+ if (type === 'text') {
+ resolve(toUSVString(Buffer.concat(body)))
+ } else if (type === 'json') {
+ resolve(JSON.parse(Buffer.concat(body)))
+ } else if (type === 'arrayBuffer') {
+ const dst = new Uint8Array(length)
+
+ let pos = 0
+ for (const buf of body) {
+ dst.set(buf, pos)
+ pos += buf.byteLength
+ }
+
+ resolve(dst.buffer)
+ } else if (type === 'blob') {
+ if (!Blob) {
+ Blob = require('buffer').Blob
+ }
+ resolve(new Blob(body, { type: stream[kContentType] }))
+ }
+
+ consumeFinish(consume)
+ } catch (err) {
+ stream.destroy(err)
+ }
+}
+
+function consumePush (consume, chunk) {
+ consume.length += chunk.length
+ consume.body.push(chunk)
+}
+
+function consumeFinish (consume, err) {
+ if (consume.body === null) {
+ return
+ }
+
+ if (err) {
+ consume.reject(err)
+ } else {
+ consume.resolve()
+ }
+
+ consume.type = null
+ consume.stream = null
+ consume.resolve = null
+ consume.reject = null
+ consume.length = 0
+ consume.body = null
+}
diff --git a/lib/api/util.js b/lib/api/util.js
new file mode 100644
index 0000000..bffd702
--- /dev/null
+++ b/lib/api/util.js
@@ -0,0 +1,46 @@
+const assert = require('assert')
+const {
+ ResponseStatusCodeError
+} = require('../core/errors')
+const { toUSVString } = require('../core/util')
+
+async function getResolveErrorBodyCallback ({ callback, body, contentType, statusCode, statusMessage, headers }) {
+ assert(body)
+
+ let chunks = []
+ let limit = 0
+
+ for await (const chunk of body) {
+ chunks.push(chunk)
+ limit += chunk.length
+ if (limit > 128 * 1024) {
+ chunks = null
+ break
+ }
+ }
+
+ if (statusCode === 204 || !contentType || !chunks) {
+ process.nextTick(callback, new ResponseStatusCodeError(`Response status code ${statusCode}${statusMessage ? `: ${statusMessage}` : ''}`, statusCode, headers))
+ return
+ }
+
+ try {
+ if (contentType.startsWith('application/json')) {
+ const payload = JSON.parse(toUSVString(Buffer.concat(chunks)))
+ process.nextTick(callback, new ResponseStatusCodeError(`Response status code ${statusCode}${statusMessage ? `: ${statusMessage}` : ''}`, statusCode, headers, payload))
+ return
+ }
+
+ if (contentType.startsWith('text/')) {
+ const payload = toUSVString(Buffer.concat(chunks))
+ process.nextTick(callback, new ResponseStatusCodeError(`Response status code ${statusCode}${statusMessage ? `: ${statusMessage}` : ''}`, statusCode, headers, payload))
+ return
+ }
+ } catch (err) {
+ // Process in a fallback if error
+ }
+
+ process.nextTick(callback, new ResponseStatusCodeError(`Response status code ${statusCode}${statusMessage ? `: ${statusMessage}` : ''}`, statusCode, headers))
+}
+
+module.exports = { getResolveErrorBodyCallback }
diff --git a/lib/balanced-pool.js b/lib/balanced-pool.js
new file mode 100644
index 0000000..10bc6a4
--- /dev/null
+++ b/lib/balanced-pool.js
@@ -0,0 +1,190 @@
+'use strict'
+
+const {
+ BalancedPoolMissingUpstreamError,
+ InvalidArgumentError
+} = require('./core/errors')
+const {
+ PoolBase,
+ kClients,
+ kNeedDrain,
+ kAddClient,
+ kRemoveClient,
+ kGetDispatcher
+} = require('./pool-base')
+const Pool = require('./pool')
+const { kUrl, kInterceptors } = require('./core/symbols')
+const { parseOrigin } = require('./core/util')
+const kFactory = Symbol('factory')
+
+const kOptions = Symbol('options')
+const kGreatestCommonDivisor = Symbol('kGreatestCommonDivisor')
+const kCurrentWeight = Symbol('kCurrentWeight')
+const kIndex = Symbol('kIndex')
+const kWeight = Symbol('kWeight')
+const kMaxWeightPerServer = Symbol('kMaxWeightPerServer')
+const kErrorPenalty = Symbol('kErrorPenalty')
+
+function getGreatestCommonDivisor (a, b) {
+ if (b === 0) return a
+ return getGreatestCommonDivisor(b, a % b)
+}
+
+function defaultFactory (origin, opts) {
+ return new Pool(origin, opts)
+}
+
+class BalancedPool extends PoolBase {
+ constructor (upstreams = [], { factory = defaultFactory, ...opts } = {}) {
+ super()
+
+ this[kOptions] = opts
+ this[kIndex] = -1
+ this[kCurrentWeight] = 0
+
+ this[kMaxWeightPerServer] = this[kOptions].maxWeightPerServer || 100
+ this[kErrorPenalty] = this[kOptions].errorPenalty || 15
+
+ if (!Array.isArray(upstreams)) {
+ upstreams = [upstreams]
+ }
+
+ if (typeof factory !== 'function') {
+ throw new InvalidArgumentError('factory must be a function.')
+ }
+
+ this[kInterceptors] = opts.interceptors && opts.interceptors.BalancedPool && Array.isArray(opts.interceptors.BalancedPool)
+ ? opts.interceptors.BalancedPool
+ : []
+ this[kFactory] = factory
+
+ for (const upstream of upstreams) {
+ this.addUpstream(upstream)
+ }
+ this._updateBalancedPoolStats()
+ }
+
+ addUpstream (upstream) {
+ const upstreamOrigin = parseOrigin(upstream).origin
+
+ if (this[kClients].find((pool) => (
+ pool[kUrl].origin === upstreamOrigin &&
+ pool.closed !== true &&
+ pool.destroyed !== true
+ ))) {
+ return this
+ }
+ const pool = this[kFactory](upstreamOrigin, Object.assign({}, this[kOptions]))
+
+ this[kAddClient](pool)
+ pool.on('connect', () => {
+ pool[kWeight] = Math.min(this[kMaxWeightPerServer], pool[kWeight] + this[kErrorPenalty])
+ })
+
+ pool.on('connectionError', () => {
+ pool[kWeight] = Math.max(1, pool[kWeight] - this[kErrorPenalty])
+ this._updateBalancedPoolStats()
+ })
+
+ pool.on('disconnect', (...args) => {
+ const err = args[2]
+ if (err && err.code === 'UND_ERR_SOCKET') {
+ // decrease the weight of the pool.
+ pool[kWeight] = Math.max(1, pool[kWeight] - this[kErrorPenalty])
+ this._updateBalancedPoolStats()
+ }
+ })
+
+ for (const client of this[kClients]) {
+ client[kWeight] = this[kMaxWeightPerServer]
+ }
+
+ this._updateBalancedPoolStats()
+
+ return this
+ }
+
+ _updateBalancedPoolStats () {
+ this[kGreatestCommonDivisor] = this[kClients].map(p => p[kWeight]).reduce(getGreatestCommonDivisor, 0)
+ }
+
+ removeUpstream (upstream) {
+ const upstreamOrigin = parseOrigin(upstream).origin
+
+ const pool = this[kClients].find((pool) => (
+ pool[kUrl].origin === upstreamOrigin &&
+ pool.closed !== true &&
+ pool.destroyed !== true
+ ))
+
+ if (pool) {
+ this[kRemoveClient](pool)
+ }
+
+ return this
+ }
+
+ get upstreams () {
+ return this[kClients]
+ .filter(dispatcher => dispatcher.closed !== true && dispatcher.destroyed !== true)
+ .map((p) => p[kUrl].origin)
+ }
+
+ [kGetDispatcher] () {
+ // We validate that pools is greater than 0,
+ // otherwise we would have to wait until an upstream
+ // is added, which might never happen.
+ if (this[kClients].length === 0) {
+ throw new BalancedPoolMissingUpstreamError()
+ }
+
+ const dispatcher = this[kClients].find(dispatcher => (
+ !dispatcher[kNeedDrain] &&
+ dispatcher.closed !== true &&
+ dispatcher.destroyed !== true
+ ))
+
+ if (!dispatcher) {
+ return
+ }
+
+ const allClientsBusy = this[kClients].map(pool => pool[kNeedDrain]).reduce((a, b) => a && b, true)
+
+ if (allClientsBusy) {
+ return
+ }
+
+ let counter = 0
+
+ let maxWeightIndex = this[kClients].findIndex(pool => !pool[kNeedDrain])
+
+ while (counter++ < this[kClients].length) {
+ this[kIndex] = (this[kIndex] + 1) % this[kClients].length
+ const pool = this[kClients][this[kIndex]]
+
+ // find pool index with the largest weight
+ if (pool[kWeight] > this[kClients][maxWeightIndex][kWeight] && !pool[kNeedDrain]) {
+ maxWeightIndex = this[kIndex]
+ }
+
+ // decrease the current weight every `this[kClients].length`.
+ if (this[kIndex] === 0) {
+ // Set the current weight to the next lower weight.
+ this[kCurrentWeight] = this[kCurrentWeight] - this[kGreatestCommonDivisor]
+
+ if (this[kCurrentWeight] <= 0) {
+ this[kCurrentWeight] = this[kMaxWeightPerServer]
+ }
+ }
+ if (pool[kWeight] >= this[kCurrentWeight] && (!pool[kNeedDrain])) {
+ return pool
+ }
+ }
+
+ this[kCurrentWeight] = this[kClients][maxWeightIndex][kWeight]
+ this[kIndex] = maxWeightIndex
+ return this[kClients][maxWeightIndex]
+ }
+}
+
+module.exports = BalancedPool
diff --git a/lib/cache/cache.js b/lib/cache/cache.js
new file mode 100644
index 0000000..9b31108
--- /dev/null
+++ b/lib/cache/cache.js
@@ -0,0 +1,838 @@
+'use strict'
+
+const { kConstruct } = require('./symbols')
+const { urlEquals, fieldValues: getFieldValues } = require('./util')
+const { kEnumerableProperty, isDisturbed } = require('../core/util')
+const { kHeadersList } = require('../core/symbols')
+const { webidl } = require('../fetch/webidl')
+const { Response, cloneResponse } = require('../fetch/response')
+const { Request } = require('../fetch/request')
+const { kState, kHeaders, kGuard, kRealm } = require('../fetch/symbols')
+const { fetching } = require('../fetch/index')
+const { urlIsHttpHttpsScheme, createDeferredPromise, readAllBytes } = require('../fetch/util')
+const assert = require('assert')
+const { getGlobalDispatcher } = require('../global')
+
+/**
+ * @see https://w3c.github.io/ServiceWorker/#dfn-cache-batch-operation
+ * @typedef {Object} CacheBatchOperation
+ * @property {'delete' | 'put'} type
+ * @property {any} request
+ * @property {any} response
+ * @property {import('../../types/cache').CacheQueryOptions} options
+ */
+
+/**
+ * @see https://w3c.github.io/ServiceWorker/#dfn-request-response-list
+ * @typedef {[any, any][]} requestResponseList
+ */
+
+class Cache {
+ /**
+ * @see https://w3c.github.io/ServiceWorker/#dfn-relevant-request-response-list
+ * @type {requestResponseList}
+ */
+ #relevantRequestResponseList
+
+ constructor () {
+ if (arguments[0] !== kConstruct) {
+ webidl.illegalConstructor()
+ }
+
+ this.#relevantRequestResponseList = arguments[1]
+ }
+
+ async match (request, options = {}) {
+ webidl.brandCheck(this, Cache)
+ webidl.argumentLengthCheck(arguments, 1, { header: 'Cache.match' })
+
+ request = webidl.converters.RequestInfo(request)
+ options = webidl.converters.CacheQueryOptions(options)
+
+ const p = await this.matchAll(request, options)
+
+ if (p.length === 0) {
+ return
+ }
+
+ return p[0]
+ }
+
+ async matchAll (request = undefined, options = {}) {
+ webidl.brandCheck(this, Cache)
+
+ if (request !== undefined) request = webidl.converters.RequestInfo(request)
+ options = webidl.converters.CacheQueryOptions(options)
+
+ // 1.
+ let r = null
+
+ // 2.
+ if (request !== undefined) {
+ if (request instanceof Request) {
+ // 2.1.1
+ r = request[kState]
+
+ // 2.1.2
+ if (r.method !== 'GET' && !options.ignoreMethod) {
+ return []
+ }
+ } else if (typeof request === 'string') {
+ // 2.2.1
+ r = new Request(request)[kState]
+ }
+ }
+
+ // 5.
+ // 5.1
+ const responses = []
+
+ // 5.2
+ if (request === undefined) {
+ // 5.2.1
+ for (const requestResponse of this.#relevantRequestResponseList) {
+ responses.push(requestResponse[1])
+ }
+ } else { // 5.3
+ // 5.3.1
+ const requestResponses = this.#queryCache(r, options)
+
+ // 5.3.2
+ for (const requestResponse of requestResponses) {
+ responses.push(requestResponse[1])
+ }
+ }
+
+ // 5.4
+ // We don't implement CORs so we don't need to loop over the responses, yay!
+
+ // 5.5.1
+ const responseList = []
+
+ // 5.5.2
+ for (const response of responses) {
+ // 5.5.2.1
+ const responseObject = new Response(response.body?.source ?? null)
+ const body = responseObject[kState].body
+ responseObject[kState] = response
+ responseObject[kState].body = body
+ responseObject[kHeaders][kHeadersList] = response.headersList
+ responseObject[kHeaders][kGuard] = 'immutable'
+
+ responseList.push(responseObject)
+ }
+
+ // 6.
+ return Object.freeze(responseList)
+ }
+
+ async add (request) {
+ webidl.brandCheck(this, Cache)
+ webidl.argumentLengthCheck(arguments, 1, { header: 'Cache.add' })
+
+ request = webidl.converters.RequestInfo(request)
+
+ // 1.
+ const requests = [request]
+
+ // 2.
+ const responseArrayPromise = this.addAll(requests)
+
+ // 3.
+ return await responseArrayPromise
+ }
+
+ async addAll (requests) {
+ webidl.brandCheck(this, Cache)
+ webidl.argumentLengthCheck(arguments, 1, { header: 'Cache.addAll' })
+
+ requests = webidl.converters['sequence<RequestInfo>'](requests)
+
+ // 1.
+ const responsePromises = []
+
+ // 2.
+ const requestList = []
+
+ // 3.
+ for (const request of requests) {
+ if (typeof request === 'string') {
+ continue
+ }
+
+ // 3.1
+ const r = request[kState]
+
+ // 3.2
+ if (!urlIsHttpHttpsScheme(r.url) || r.method !== 'GET') {
+ throw webidl.errors.exception({
+ header: 'Cache.addAll',
+ message: 'Expected http/s scheme when method is not GET.'
+ })
+ }
+ }
+
+ // 4.
+ /** @type {ReturnType<typeof fetching>[]} */
+ const fetchControllers = []
+
+ // 5.
+ for (const request of requests) {
+ // 5.1
+ const r = new Request(request)[kState]
+
+ // 5.2
+ if (!urlIsHttpHttpsScheme(r.url)) {
+ throw webidl.errors.exception({
+ header: 'Cache.addAll',
+ message: 'Expected http/s scheme.'
+ })
+ }
+
+ // 5.4
+ r.initiator = 'fetch'
+ r.destination = 'subresource'
+
+ // 5.5
+ requestList.push(r)
+
+ // 5.6
+ const responsePromise = createDeferredPromise()
+
+ // 5.7
+ fetchControllers.push(fetching({
+ request: r,
+ dispatcher: getGlobalDispatcher(),
+ processResponse (response) {
+ // 1.
+ if (response.type === 'error' || response.status === 206 || response.status < 200 || response.status > 299) {
+ responsePromise.reject(webidl.errors.exception({
+ header: 'Cache.addAll',
+ message: 'Received an invalid status code or the request failed.'
+ }))
+ } else if (response.headersList.contains('vary')) { // 2.
+ // 2.1
+ const fieldValues = getFieldValues(response.headersList.get('vary'))
+
+ // 2.2
+ for (const fieldValue of fieldValues) {
+ // 2.2.1
+ if (fieldValue === '*') {
+ responsePromise.reject(webidl.errors.exception({
+ header: 'Cache.addAll',
+ message: 'invalid vary field value'
+ }))
+
+ for (const controller of fetchControllers) {
+ controller.abort()
+ }
+
+ return
+ }
+ }
+ }
+ },
+ processResponseEndOfBody (response) {
+ // 1.
+ if (response.aborted) {
+ responsePromise.reject(new DOMException('aborted', 'AbortError'))
+ return
+ }
+
+ // 2.
+ responsePromise.resolve(response)
+ }
+ }))
+
+ // 5.8
+ responsePromises.push(responsePromise.promise)
+ }
+
+ // 6.
+ const p = Promise.all(responsePromises)
+
+ // 7.
+ const responses = await p
+
+ // 7.1
+ const operations = []
+
+ // 7.2
+ let index = 0
+
+ // 7.3
+ for (const response of responses) {
+ // 7.3.1
+ /** @type {CacheBatchOperation} */
+ const operation = {
+ type: 'put', // 7.3.2
+ request: requestList[index], // 7.3.3
+ response // 7.3.4
+ }
+
+ operations.push(operation) // 7.3.5
+
+ index++ // 7.3.6
+ }
+
+ // 7.5
+ const cacheJobPromise = createDeferredPromise()
+
+ // 7.6.1
+ let errorData = null
+
+ // 7.6.2
+ try {
+ this.#batchCacheOperations(operations)
+ } catch (e) {
+ errorData = e
+ }
+
+ // 7.6.3
+ queueMicrotask(() => {
+ // 7.6.3.1
+ if (errorData === null) {
+ cacheJobPromise.resolve(undefined)
+ } else {
+ // 7.6.3.2
+ cacheJobPromise.reject(errorData)
+ }
+ })
+
+ // 7.7
+ return cacheJobPromise.promise
+ }
+
+ async put (request, response) {
+ webidl.brandCheck(this, Cache)
+ webidl.argumentLengthCheck(arguments, 2, { header: 'Cache.put' })
+
+ request = webidl.converters.RequestInfo(request)
+ response = webidl.converters.Response(response)
+
+ // 1.
+ let innerRequest = null
+
+ // 2.
+ if (request instanceof Request) {
+ innerRequest = request[kState]
+ } else { // 3.
+ innerRequest = new Request(request)[kState]
+ }
+
+ // 4.
+ if (!urlIsHttpHttpsScheme(innerRequest.url) || innerRequest.method !== 'GET') {
+ throw webidl.errors.exception({
+ header: 'Cache.put',
+ message: 'Expected an http/s scheme when method is not GET'
+ })
+ }
+
+ // 5.
+ const innerResponse = response[kState]
+
+ // 6.
+ if (innerResponse.status === 206) {
+ throw webidl.errors.exception({
+ header: 'Cache.put',
+ message: 'Got 206 status'
+ })
+ }
+
+ // 7.
+ if (innerResponse.headersList.contains('vary')) {
+ // 7.1.
+ const fieldValues = getFieldValues(innerResponse.headersList.get('vary'))
+
+ // 7.2.
+ for (const fieldValue of fieldValues) {
+ // 7.2.1
+ if (fieldValue === '*') {
+ throw webidl.errors.exception({
+ header: 'Cache.put',
+ message: 'Got * vary field value'
+ })
+ }
+ }
+ }
+
+ // 8.
+ if (innerResponse.body && (isDisturbed(innerResponse.body.stream) || innerResponse.body.stream.locked)) {
+ throw webidl.errors.exception({
+ header: 'Cache.put',
+ message: 'Response body is locked or disturbed'
+ })
+ }
+
+ // 9.
+ const clonedResponse = cloneResponse(innerResponse)
+
+ // 10.
+ const bodyReadPromise = createDeferredPromise()
+
+ // 11.
+ if (innerResponse.body != null) {
+ // 11.1
+ const stream = innerResponse.body.stream
+
+ // 11.2
+ const reader = stream.getReader()
+
+ // 11.3
+ readAllBytes(reader).then(bodyReadPromise.resolve, bodyReadPromise.reject)
+ } else {
+ bodyReadPromise.resolve(undefined)
+ }
+
+ // 12.
+ /** @type {CacheBatchOperation[]} */
+ const operations = []
+
+ // 13.
+ /** @type {CacheBatchOperation} */
+ const operation = {
+ type: 'put', // 14.
+ request: innerRequest, // 15.
+ response: clonedResponse // 16.
+ }
+
+ // 17.
+ operations.push(operation)
+
+ // 19.
+ const bytes = await bodyReadPromise.promise
+
+ if (clonedResponse.body != null) {
+ clonedResponse.body.source = bytes
+ }
+
+ // 19.1
+ const cacheJobPromise = createDeferredPromise()
+
+ // 19.2.1
+ let errorData = null
+
+ // 19.2.2
+ try {
+ this.#batchCacheOperations(operations)
+ } catch (e) {
+ errorData = e
+ }
+
+ // 19.2.3
+ queueMicrotask(() => {
+ // 19.2.3.1
+ if (errorData === null) {
+ cacheJobPromise.resolve()
+ } else { // 19.2.3.2
+ cacheJobPromise.reject(errorData)
+ }
+ })
+
+ return cacheJobPromise.promise
+ }
+
+ async delete (request, options = {}) {
+ webidl.brandCheck(this, Cache)
+ webidl.argumentLengthCheck(arguments, 1, { header: 'Cache.delete' })
+
+ request = webidl.converters.RequestInfo(request)
+ options = webidl.converters.CacheQueryOptions(options)
+
+ /**
+ * @type {Request}
+ */
+ let r = null
+
+ if (request instanceof Request) {
+ r = request[kState]
+
+ if (r.method !== 'GET' && !options.ignoreMethod) {
+ return false
+ }
+ } else {
+ assert(typeof request === 'string')
+
+ r = new Request(request)[kState]
+ }
+
+ /** @type {CacheBatchOperation[]} */
+ const operations = []
+
+ /** @type {CacheBatchOperation} */
+ const operation = {
+ type: 'delete',
+ request: r,
+ options
+ }
+
+ operations.push(operation)
+
+ const cacheJobPromise = createDeferredPromise()
+
+ let errorData = null
+ let requestResponses
+
+ try {
+ requestResponses = this.#batchCacheOperations(operations)
+ } catch (e) {
+ errorData = e
+ }
+
+ queueMicrotask(() => {
+ if (errorData === null) {
+ cacheJobPromise.resolve(!!requestResponses?.length)
+ } else {
+ cacheJobPromise.reject(errorData)
+ }
+ })
+
+ return cacheJobPromise.promise
+ }
+
+ /**
+ * @see https://w3c.github.io/ServiceWorker/#dom-cache-keys
+ * @param {any} request
+ * @param {import('../../types/cache').CacheQueryOptions} options
+ * @returns {readonly Request[]}
+ */
+ async keys (request = undefined, options = {}) {
+ webidl.brandCheck(this, Cache)
+
+ if (request !== undefined) request = webidl.converters.RequestInfo(request)
+ options = webidl.converters.CacheQueryOptions(options)
+
+ // 1.
+ let r = null
+
+ // 2.
+ if (request !== undefined) {
+ // 2.1
+ if (request instanceof Request) {
+ // 2.1.1
+ r = request[kState]
+
+ // 2.1.2
+ if (r.method !== 'GET' && !options.ignoreMethod) {
+ return []
+ }
+ } else if (typeof request === 'string') { // 2.2
+ r = new Request(request)[kState]
+ }
+ }
+
+ // 4.
+ const promise = createDeferredPromise()
+
+ // 5.
+ // 5.1
+ const requests = []
+
+ // 5.2
+ if (request === undefined) {
+ // 5.2.1
+ for (const requestResponse of this.#relevantRequestResponseList) {
+ // 5.2.1.1
+ requests.push(requestResponse[0])
+ }
+ } else { // 5.3
+ // 5.3.1
+ const requestResponses = this.#queryCache(r, options)
+
+ // 5.3.2
+ for (const requestResponse of requestResponses) {
+ // 5.3.2.1
+ requests.push(requestResponse[0])
+ }
+ }
+
+ // 5.4
+ queueMicrotask(() => {
+ // 5.4.1
+ const requestList = []
+
+ // 5.4.2
+ for (const request of requests) {
+ const requestObject = new Request('https://a')
+ requestObject[kState] = request
+ requestObject[kHeaders][kHeadersList] = request.headersList
+ requestObject[kHeaders][kGuard] = 'immutable'
+ requestObject[kRealm] = request.client
+
+ // 5.4.2.1
+ requestList.push(requestObject)
+ }
+
+ // 5.4.3
+ promise.resolve(Object.freeze(requestList))
+ })
+
+ return promise.promise
+ }
+
+ /**
+ * @see https://w3c.github.io/ServiceWorker/#batch-cache-operations-algorithm
+ * @param {CacheBatchOperation[]} operations
+ * @returns {requestResponseList}
+ */
+ #batchCacheOperations (operations) {
+ // 1.
+ const cache = this.#relevantRequestResponseList
+
+ // 2.
+ const backupCache = [...cache]
+
+ // 3.
+ const addedItems = []
+
+ // 4.1
+ const resultList = []
+
+ try {
+ // 4.2
+ for (const operation of operations) {
+ // 4.2.1
+ if (operation.type !== 'delete' && operation.type !== 'put') {
+ throw webidl.errors.exception({
+ header: 'Cache.#batchCacheOperations',
+ message: 'operation type does not match "delete" or "put"'
+ })
+ }
+
+ // 4.2.2
+ if (operation.type === 'delete' && operation.response != null) {
+ throw webidl.errors.exception({
+ header: 'Cache.#batchCacheOperations',
+ message: 'delete operation should not have an associated response'
+ })
+ }
+
+ // 4.2.3
+ if (this.#queryCache(operation.request, operation.options, addedItems).length) {
+ throw new DOMException('???', 'InvalidStateError')
+ }
+
+ // 4.2.4
+ let requestResponses
+
+ // 4.2.5
+ if (operation.type === 'delete') {
+ // 4.2.5.1
+ requestResponses = this.#queryCache(operation.request, operation.options)
+
+ // TODO: the spec is wrong, this is needed to pass WPTs
+ if (requestResponses.length === 0) {
+ return []
+ }
+
+ // 4.2.5.2
+ for (const requestResponse of requestResponses) {
+ const idx = cache.indexOf(requestResponse)
+ assert(idx !== -1)
+
+ // 4.2.5.2.1
+ cache.splice(idx, 1)
+ }
+ } else if (operation.type === 'put') { // 4.2.6
+ // 4.2.6.1
+ if (operation.response == null) {
+ throw webidl.errors.exception({
+ header: 'Cache.#batchCacheOperations',
+ message: 'put operation should have an associated response'
+ })
+ }
+
+ // 4.2.6.2
+ const r = operation.request
+
+ // 4.2.6.3
+ if (!urlIsHttpHttpsScheme(r.url)) {
+ throw webidl.errors.exception({
+ header: 'Cache.#batchCacheOperations',
+ message: 'expected http or https scheme'
+ })
+ }
+
+ // 4.2.6.4
+ if (r.method !== 'GET') {
+ throw webidl.errors.exception({
+ header: 'Cache.#batchCacheOperations',
+ message: 'not get method'
+ })
+ }
+
+ // 4.2.6.5
+ if (operation.options != null) {
+ throw webidl.errors.exception({
+ header: 'Cache.#batchCacheOperations',
+ message: 'options must not be defined'
+ })
+ }
+
+ // 4.2.6.6
+ requestResponses = this.#queryCache(operation.request)
+
+ // 4.2.6.7
+ for (const requestResponse of requestResponses) {
+ const idx = cache.indexOf(requestResponse)
+ assert(idx !== -1)
+
+ // 4.2.6.7.1
+ cache.splice(idx, 1)
+ }
+
+ // 4.2.6.8
+ cache.push([operation.request, operation.response])
+
+ // 4.2.6.10
+ addedItems.push([operation.request, operation.response])
+ }
+
+ // 4.2.7
+ resultList.push([operation.request, operation.response])
+ }
+
+ // 4.3
+ return resultList
+ } catch (e) { // 5.
+ // 5.1
+ this.#relevantRequestResponseList.length = 0
+
+ // 5.2
+ this.#relevantRequestResponseList = backupCache
+
+ // 5.3
+ throw e
+ }
+ }
+
+ /**
+ * @see https://w3c.github.io/ServiceWorker/#query-cache
+ * @param {any} requestQuery
+ * @param {import('../../types/cache').CacheQueryOptions} options
+ * @param {requestResponseList} targetStorage
+ * @returns {requestResponseList}
+ */
+ #queryCache (requestQuery, options, targetStorage) {
+ /** @type {requestResponseList} */
+ const resultList = []
+
+ const storage = targetStorage ?? this.#relevantRequestResponseList
+
+ for (const requestResponse of storage) {
+ const [cachedRequest, cachedResponse] = requestResponse
+ if (this.#requestMatchesCachedItem(requestQuery, cachedRequest, cachedResponse, options)) {
+ resultList.push(requestResponse)
+ }
+ }
+
+ return resultList
+ }
+
+ /**
+ * @see https://w3c.github.io/ServiceWorker/#request-matches-cached-item-algorithm
+ * @param {any} requestQuery
+ * @param {any} request
+ * @param {any | null} response
+ * @param {import('../../types/cache').CacheQueryOptions | undefined} options
+ * @returns {boolean}
+ */
+ #requestMatchesCachedItem (requestQuery, request, response = null, options) {
+ // if (options?.ignoreMethod === false && request.method === 'GET') {
+ // return false
+ // }
+
+ const queryURL = new URL(requestQuery.url)
+
+ const cachedURL = new URL(request.url)
+
+ if (options?.ignoreSearch) {
+ cachedURL.search = ''
+
+ queryURL.search = ''
+ }
+
+ if (!urlEquals(queryURL, cachedURL, true)) {
+ return false
+ }
+
+ if (
+ response == null ||
+ options?.ignoreVary ||
+ !response.headersList.contains('vary')
+ ) {
+ return true
+ }
+
+ const fieldValues = getFieldValues(response.headersList.get('vary'))
+
+ for (const fieldValue of fieldValues) {
+ if (fieldValue === '*') {
+ return false
+ }
+
+ const requestValue = request.headersList.get(fieldValue)
+ const queryValue = requestQuery.headersList.get(fieldValue)
+
+ // If one has the header and the other doesn't, or one has
+ // a different value than the other, return false
+ if (requestValue !== queryValue) {
+ return false
+ }
+ }
+
+ return true
+ }
+}
+
+Object.defineProperties(Cache.prototype, {
+ [Symbol.toStringTag]: {
+ value: 'Cache',
+ configurable: true
+ },
+ match: kEnumerableProperty,
+ matchAll: kEnumerableProperty,
+ add: kEnumerableProperty,
+ addAll: kEnumerableProperty,
+ put: kEnumerableProperty,
+ delete: kEnumerableProperty,
+ keys: kEnumerableProperty
+})
+
+const cacheQueryOptionConverters = [
+ {
+ key: 'ignoreSearch',
+ converter: webidl.converters.boolean,
+ defaultValue: false
+ },
+ {
+ key: 'ignoreMethod',
+ converter: webidl.converters.boolean,
+ defaultValue: false
+ },
+ {
+ key: 'ignoreVary',
+ converter: webidl.converters.boolean,
+ defaultValue: false
+ }
+]
+
+webidl.converters.CacheQueryOptions = webidl.dictionaryConverter(cacheQueryOptionConverters)
+
+webidl.converters.MultiCacheQueryOptions = webidl.dictionaryConverter([
+ ...cacheQueryOptionConverters,
+ {
+ key: 'cacheName',
+ converter: webidl.converters.DOMString
+ }
+])
+
+webidl.converters.Response = webidl.interfaceConverter(Response)
+
+webidl.converters['sequence<RequestInfo>'] = webidl.sequenceConverter(
+ webidl.converters.RequestInfo
+)
+
+module.exports = {
+ Cache
+}
diff --git a/lib/cache/cachestorage.js b/lib/cache/cachestorage.js
new file mode 100644
index 0000000..7e7f0cf
--- /dev/null
+++ b/lib/cache/cachestorage.js
@@ -0,0 +1,144 @@
+'use strict'
+
+const { kConstruct } = require('./symbols')
+const { Cache } = require('./cache')
+const { webidl } = require('../fetch/webidl')
+const { kEnumerableProperty } = require('../core/util')
+
+class CacheStorage {
+ /**
+ * @see https://w3c.github.io/ServiceWorker/#dfn-relevant-name-to-cache-map
+ * @type {Map<string, import('./cache').requestResponseList}
+ */
+ #caches = new Map()
+
+ constructor () {
+ if (arguments[0] !== kConstruct) {
+ webidl.illegalConstructor()
+ }
+ }
+
+ async match (request, options = {}) {
+ webidl.brandCheck(this, CacheStorage)
+ webidl.argumentLengthCheck(arguments, 1, { header: 'CacheStorage.match' })
+
+ request = webidl.converters.RequestInfo(request)
+ options = webidl.converters.MultiCacheQueryOptions(options)
+
+ // 1.
+ if (options.cacheName != null) {
+ // 1.1.1.1
+ if (this.#caches.has(options.cacheName)) {
+ // 1.1.1.1.1
+ const cacheList = this.#caches.get(options.cacheName)
+ const cache = new Cache(kConstruct, cacheList)
+
+ return await cache.match(request, options)
+ }
+ } else { // 2.
+ // 2.2
+ for (const cacheList of this.#caches.values()) {
+ const cache = new Cache(kConstruct, cacheList)
+
+ // 2.2.1.2
+ const response = await cache.match(request, options)
+
+ if (response !== undefined) {
+ return response
+ }
+ }
+ }
+ }
+
+ /**
+ * @see https://w3c.github.io/ServiceWorker/#cache-storage-has
+ * @param {string} cacheName
+ * @returns {Promise<boolean>}
+ */
+ async has (cacheName) {
+ webidl.brandCheck(this, CacheStorage)
+ webidl.argumentLengthCheck(arguments, 1, { header: 'CacheStorage.has' })
+
+ cacheName = webidl.converters.DOMString(cacheName)
+
+ // 2.1.1
+ // 2.2
+ return this.#caches.has(cacheName)
+ }
+
+ /**
+ * @see https://w3c.github.io/ServiceWorker/#dom-cachestorage-open
+ * @param {string} cacheName
+ * @returns {Promise<Cache>}
+ */
+ async open (cacheName) {
+ webidl.brandCheck(this, CacheStorage)
+ webidl.argumentLengthCheck(arguments, 1, { header: 'CacheStorage.open' })
+
+ cacheName = webidl.converters.DOMString(cacheName)
+
+ // 2.1
+ if (this.#caches.has(cacheName)) {
+ // await caches.open('v1') !== await caches.open('v1')
+
+ // 2.1.1
+ const cache = this.#caches.get(cacheName)
+
+ // 2.1.1.1
+ return new Cache(kConstruct, cache)
+ }
+
+ // 2.2
+ const cache = []
+
+ // 2.3
+ this.#caches.set(cacheName, cache)
+
+ // 2.4
+ return new Cache(kConstruct, cache)
+ }
+
+ /**
+ * @see https://w3c.github.io/ServiceWorker/#cache-storage-delete
+ * @param {string} cacheName
+ * @returns {Promise<boolean>}
+ */
+ async delete (cacheName) {
+ webidl.brandCheck(this, CacheStorage)
+ webidl.argumentLengthCheck(arguments, 1, { header: 'CacheStorage.delete' })
+
+ cacheName = webidl.converters.DOMString(cacheName)
+
+ return this.#caches.delete(cacheName)
+ }
+
+ /**
+ * @see https://w3c.github.io/ServiceWorker/#cache-storage-keys
+ * @returns {string[]}
+ */
+ async keys () {
+ webidl.brandCheck(this, CacheStorage)
+
+ // 2.1
+ const keys = this.#caches.keys()
+
+ // 2.2
+ return [...keys]
+ }
+}
+
+Object.defineProperties(CacheStorage.prototype, {
+ [Symbol.toStringTag]: {
+ value: 'CacheStorage',
+ configurable: true
+ },
+ match: kEnumerableProperty,
+ has: kEnumerableProperty,
+ open: kEnumerableProperty,
+ delete: kEnumerableProperty,
+ keys: kEnumerableProperty
+})
+
+module.exports = {
+ CacheStorage
+}
diff --git a/lib/cache/symbols.js b/lib/cache/symbols.js
new file mode 100644
index 0000000..40448d6
--- /dev/null
+++ b/lib/cache/symbols.js
@@ -0,0 +1,5 @@
+'use strict'
+
+module.exports = {
+ kConstruct: require('../core/symbols').kConstruct
+}
diff --git a/lib/cache/util.js b/lib/cache/util.js
new file mode 100644
index 0000000..44d52b7
--- /dev/null
+++ b/lib/cache/util.js
@@ -0,0 +1,49 @@
+'use strict'
+
+const assert = require('assert')
+const { URLSerializer } = require('../fetch/dataURL')
+const { isValidHeaderName } = require('../fetch/util')
+
+/**
+ * @see https://url.spec.whatwg.org/#concept-url-equals
+ * @param {URL} A
+ * @param {URL} B
+ * @param {boolean | undefined} excludeFragment
+ * @returns {boolean}
+ */
+function urlEquals (A, B, excludeFragment = false) {
+ const serializedA = URLSerializer(A, excludeFragment)
+
+ const serializedB = URLSerializer(B, excludeFragment)
+
+ return serializedA === serializedB
+}
+
+/**
+ * @see https://github.com/chromium/chromium/blob/694d20d134cb553d8d89e5500b9148012b1ba299/content/browser/cache_storage/cache_storage_cache.cc#L260-L262
+ * @param {string} header
+ */
+function fieldValues (header) {
+ assert(header !== null)
+
+ const values = []
+
+ for (let value of header.split(',')) {
+ value = value.trim()
+
+ if (!value.length) {
+ continue
+ } else if (!isValidHeaderName(value)) {
+ continue
+ }
+
+ values.push(value)
+ }
+
+ return values
+}
+
+module.exports = {
+ urlEquals,
+ fieldValues
+}
diff --git a/lib/client.js b/lib/client.js
new file mode 100644
index 0000000..22cb390
--- /dev/null
+++ b/lib/client.js
@@ -0,0 +1,2283 @@
+// @ts-check
+
+'use strict'
+
+/* global WebAssembly */
+
+const assert = require('assert')
+const net = require('net')
+const http = require('http')
+const { pipeline } = require('stream')
+const util = require('./core/util')
+const timers = require('./timers')
+const Request = require('./core/request')
+const DispatcherBase = require('./dispatcher-base')
+const {
+ RequestContentLengthMismatchError,
+ ResponseContentLengthMismatchError,
+ InvalidArgumentError,
+ RequestAbortedError,
+ HeadersTimeoutError,
+ HeadersOverflowError,
+ SocketError,
+ InformationalError,
+ BodyTimeoutError,
+ HTTPParserError,
+ ResponseExceededMaxSizeError,
+ ClientDestroyedError
+} = require('./core/errors')
+const buildConnector = require('./core/connect')
+const {
+ kUrl,
+ kReset,
+ kServerName,
+ kClient,
+ kBusy,
+ kParser,
+ kConnect,
+ kBlocking,
+ kResuming,
+ kRunning,
+ kPending,
+ kSize,
+ kWriting,
+ kQueue,
+ kConnected,
+ kConnecting,
+ kNeedDrain,
+ kNoRef,
+ kKeepAliveDefaultTimeout,
+ kHostHeader,
+ kPendingIdx,
+ kRunningIdx,
+ kError,
+ kPipelining,
+ kSocket,
+ kKeepAliveTimeoutValue,
+ kMaxHeadersSize,
+ kKeepAliveMaxTimeout,
+ kKeepAliveTimeoutThreshold,
+ kHeadersTimeout,
+ kBodyTimeout,
+ kStrictContentLength,
+ kConnector,
+ kMaxRedirections,
+ kMaxRequests,
+ kCounter,
+ kClose,
+ kDestroy,
+ kDispatch,
+ kInterceptors,
+ kLocalAddress,
+ kMaxResponseSize,
+ kHTTPConnVersion,
+ // HTTP2
+ kHost,
+ kHTTP2Session,
+ kHTTP2SessionState,
+ kHTTP2BuildRequest,
+ kHTTP2CopyHeaders,
+ kHTTP1BuildRequest
+} = require('./core/symbols')
+
+/** @type {import('http2')} */
+let http2
+try {
+ http2 = require('http2')
+} catch {
+ // @ts-ignore
+ http2 = { constants: {} }
+}
+
+const {
+ constants: {
+ HTTP2_HEADER_AUTHORITY,
+ HTTP2_HEADER_METHOD,
+ HTTP2_HEADER_PATH,
+ HTTP2_HEADER_SCHEME,
+ HTTP2_HEADER_CONTENT_LENGTH,
+ HTTP2_HEADER_EXPECT,
+ HTTP2_HEADER_STATUS
+ }
+} = http2
+
+// Experimental
+let h2ExperimentalWarned = false
+
+const FastBuffer = Buffer[Symbol.species]
+
+const kClosedResolve = Symbol('kClosedResolve')
+
+const channels = {}
+
+try {
+ const diagnosticsChannel = require('diagnostics_channel')
+ channels.sendHeaders = diagnosticsChannel.channel('undici:client:sendHeaders')
+ channels.beforeConnect = diagnosticsChannel.channel('undici:client:beforeConnect')
+ channels.connectError = diagnosticsChannel.channel('undici:client:connectError')
+ channels.connected = diagnosticsChannel.channel('undici:client:connected')
+} catch {
+ channels.sendHeaders = { hasSubscribers: false }
+ channels.beforeConnect = { hasSubscribers: false }
+ channels.connectError = { hasSubscribers: false }
+ channels.connected = { hasSubscribers: false }
+}
+
+/**
+ * @type {import('../types/client').default}
+ */
+class Client extends DispatcherBase {
+ /**
+ *
+ * @param {string|URL} url
+ * @param {import('../types/client').Client.Options} options
+ */
+ constructor (url, {
+ interceptors,
+ maxHeaderSize,
+ headersTimeout,
+ socketTimeout,
+ requestTimeout,
+ connectTimeout,
+ bodyTimeout,
+ idleTimeout,
+ keepAlive,
+ keepAliveTimeout,
+ maxKeepAliveTimeout,
+ keepAliveMaxTimeout,
+ keepAliveTimeoutThreshold,
+ socketPath,
+ pipelining,
+ tls,
+ strictContentLength,
+ maxCachedSessions,
+ maxRedirections,
+ connect,
+ maxRequestsPerClient,
+ localAddress,
+ maxResponseSize,
+ autoSelectFamily,
+ autoSelectFamilyAttemptTimeout,
+ // h2
+ allowH2,
+ maxConcurrentStreams
+ } = {}) {
+ super()
+
+ if (keepAlive !== undefined) {
+ throw new InvalidArgumentError('unsupported keepAlive, use pipelining=0 instead')
+ }
+
+ if (socketTimeout !== undefined) {
+ throw new InvalidArgumentError('unsupported socketTimeout, use headersTimeout & bodyTimeout instead')
+ }
+
+ if (requestTimeout !== undefined) {
+ throw new InvalidArgumentError('unsupported requestTimeout, use headersTimeout & bodyTimeout instead')
+ }
+
+ if (idleTimeout !== undefined) {
+ throw new InvalidArgumentError('unsupported idleTimeout, use keepAliveTimeout instead')
+ }
+
+ if (maxKeepAliveTimeout !== undefined) {
+ throw new InvalidArgumentError('unsupported maxKeepAliveTimeout, use keepAliveMaxTimeout instead')
+ }
+
+ if (maxHeaderSize != null && !Number.isFinite(maxHeaderSize)) {
+ throw new InvalidArgumentError('invalid maxHeaderSize')
+ }
+
+ if (socketPath != null && typeof socketPath !== 'string') {
+ throw new InvalidArgumentError('invalid socketPath')
+ }
+
+ if (connectTimeout != null && (!Number.isFinite(connectTimeout) || connectTimeout < 0)) {
+ throw new InvalidArgumentError('invalid connectTimeout')
+ }
+
+ if (keepAliveTimeout != null && (!Number.isFinite(keepAliveTimeout) || keepAliveTimeout <= 0)) {
+ throw new InvalidArgumentError('invalid keepAliveTimeout')
+ }
+
+ if (keepAliveMaxTimeout != null && (!Number.isFinite(keepAliveMaxTimeout) || keepAliveMaxTimeout <= 0)) {
+ throw new InvalidArgumentError('invalid keepAliveMaxTimeout')
+ }
+
+ if (keepAliveTimeoutThreshold != null && !Number.isFinite(keepAliveTimeoutThreshold)) {
+ throw new InvalidArgumentError('invalid keepAliveTimeoutThreshold')
+ }
+
+ if (headersTimeout != null && (!Number.isInteger(headersTimeout) || headersTimeout < 0)) {
+ throw new InvalidArgumentError('headersTimeout must be a positive integer or zero')
+ }
+
+ if (bodyTimeout != null && (!Number.isInteger(bodyTimeout) || bodyTimeout < 0)) {
+ throw new InvalidArgumentError('bodyTimeout must be a positive integer or zero')
+ }
+
+ if (connect != null && typeof connect !== 'function' && typeof connect !== 'object') {
+ throw new InvalidArgumentError('connect must be a function or an object')
+ }
+
+ if (maxRedirections != null && (!Number.isInteger(maxRedirections) || maxRedirections < 0)) {
+ throw new InvalidArgumentError('maxRedirections must be a positive number')
+ }
+
+ if (maxRequestsPerClient != null && (!Number.isInteger(maxRequestsPerClient) || maxRequestsPerClient < 0)) {
+ throw new InvalidArgumentError('maxRequestsPerClient must be a positive number')
+ }
+
+ if (localAddress != null && (typeof localAddress !== 'string' || net.isIP(localAddress) === 0)) {
+ throw new InvalidArgumentError('localAddress must be valid string IP address')
+ }
+
+ if (maxResponseSize != null && (!Number.isInteger(maxResponseSize) || maxResponseSize < -1)) {
+ throw new InvalidArgumentError('maxResponseSize must be a positive number')
+ }
+
+ if (
+ autoSelectFamilyAttemptTimeout != null &&
+ (!Number.isInteger(autoSelectFamilyAttemptTimeout) || autoSelectFamilyAttemptTimeout < -1)
+ ) {
+ throw new InvalidArgumentError('autoSelectFamilyAttemptTimeout must be a positive number')
+ }
+
+ // h2
+ if (allowH2 != null && typeof allowH2 !== 'boolean') {
+ throw new InvalidArgumentError('allowH2 must be a valid boolean value')
+ }
+
+ if (maxConcurrentStreams != null && (typeof maxConcurrentStreams !== 'number' || maxConcurrentStreams < 1)) {
+ throw new InvalidArgumentError('maxConcurrentStreams must be a possitive integer, greater than 0')
+ }
+
+ if (typeof connect !== 'function') {
+ connect = buildConnector({
+ ...tls,
+ maxCachedSessions,
+ allowH2,
+ socketPath,
+ timeout: connectTimeout,
+ ...(util.nodeHasAutoSelectFamily && autoSelectFamily ? { autoSelectFamily, autoSelectFamilyAttemptTimeout } : undefined),
+ ...connect
+ })
+ }
+
+ this[kInterceptors] = interceptors && interceptors.Client && Array.isArray(interceptors.Client)
+ ? interceptors.Client
+ : [createRedirectInterceptor({ maxRedirections })]
+ this[kUrl] = util.parseOrigin(url)
+ this[kConnector] = connect
+ this[kSocket] = null
+ this[kPipelining] = pipelining != null ? pipelining : 1
+ this[kMaxHeadersSize] = maxHeaderSize || http.maxHeaderSize
+ this[kKeepAliveDefaultTimeout] = keepAliveTimeout == null ? 4e3 : keepAliveTimeout
+ this[kKeepAliveMaxTimeout] = keepAliveMaxTimeout == null ? 600e3 : keepAliveMaxTimeout
+ this[kKeepAliveTimeoutThreshold] = keepAliveTimeoutThreshold == null ? 1e3 : keepAliveTimeoutThreshold
+ this[kKeepAliveTimeoutValue] = this[kKeepAliveDefaultTimeout]
+ this[kServerName] = null
+ this[kLocalAddress] = localAddress != null ? localAddress : null
+ this[kResuming] = 0 // 0, idle, 1, scheduled, 2 resuming
+ this[kNeedDrain] = 0 // 0, idle, 1, scheduled, 2 resuming
+ this[kHostHeader] = `host: ${this[kUrl].hostname}${this[kUrl].port ? `:${this[kUrl].port}` : ''}\r\n`
+ this[kBodyTimeout] = bodyTimeout != null ? bodyTimeout : 300e3
+ this[kHeadersTimeout] = headersTimeout != null ? headersTimeout : 300e3
+ this[kStrictContentLength] = strictContentLength == null ? true : strictContentLength
+ this[kMaxRedirections] = maxRedirections
+ this[kMaxRequests] = maxRequestsPerClient
+ this[kClosedResolve] = null
+ this[kMaxResponseSize] = maxResponseSize > -1 ? maxResponseSize : -1
+ this[kHTTPConnVersion] = 'h1'
+
+ // HTTP/2
+ this[kHTTP2Session] = null
+ this[kHTTP2SessionState] = !allowH2
+ ? null
+ : {
+ // streams: null, // Fixed queue of streams - For future support of `push`
+ openStreams: 0, // Keep track of them to decide wether or not unref the session
+ maxConcurrentStreams: maxConcurrentStreams != null ? maxConcurrentStreams : 100 // Max peerConcurrentStreams for a Node h2 server
+ }
+ this[kHost] = `${this[kUrl].hostname}${this[kUrl].port ? `:${this[kUrl].port}` : ''}`
+
+ // kQueue is built up of 3 sections separated by
+ // the kRunningIdx and kPendingIdx indices.
+ // | complete | running | pending |
+ // ^ kRunningIdx ^ kPendingIdx ^ kQueue.length
+ // kRunningIdx points to the first running element.
+ // kPendingIdx points to the first pending element.
+ // This implements a fast queue with an amortized
+ // time of O(1).
+
+ this[kQueue] = []
+ this[kRunningIdx] = 0
+ this[kPendingIdx] = 0
+ }
+
+ get pipelining () {
+ return this[kPipelining]
+ }
+
+ set pipelining (value) {
+ this[kPipelining] = value
+ resume(this, true)
+ }
+
+ get [kPending] () {
+ return this[kQueue].length - this[kPendingIdx]
+ }
+
+ get [kRunning] () {
+ return this[kPendingIdx] - this[kRunningIdx]
+ }
+
+ get [kSize] () {
+ return this[kQueue].length - this[kRunningIdx]
+ }
+
+ get [kConnected] () {
+ return !!this[kSocket] && !this[kConnecting] && !this[kSocket].destroyed
+ }
+
+ get [kBusy] () {
+ const socket = this[kSocket]
+ return (
+ (socket && (socket[kReset] || socket[kWriting] || socket[kBlocking])) ||
+ (this[kSize] >= (this[kPipelining] || 1)) ||
+ this[kPending] > 0
+ )
+ }
+
+ /* istanbul ignore: only used for test */
+ [kConnect] (cb) {
+ connect(this)
+ this.once('connect', cb)
+ }
+
+ [kDispatch] (opts, handler) {
+ const origin = opts.origin || this[kUrl].origin
+
+ const request = this[kHTTPConnVersion] === 'h2'
+ ? Request[kHTTP2BuildRequest](origin, opts, handler)
+ : Request[kHTTP1BuildRequest](origin, opts, handler)
+
+ this[kQueue].push(request)
+ if (this[kResuming]) {
+ // Do nothing.
+ } else if (util.bodyLength(request.body) == null && util.isIterable(request.body)) {
+ // Wait a tick in case stream/iterator is ended in the same tick.
+ this[kResuming] = 1
+ process.nextTick(resume, this)
+ } else {
+ resume(this, true)
+ }
+
+ if (this[kResuming] && this[kNeedDrain] !== 2 && this[kBusy]) {
+ this[kNeedDrain] = 2
+ }
+
+ return this[kNeedDrain] < 2
+ }
+
+ async [kClose] () {
+ // TODO: for H2 we need to gracefully flush the remaining enqueued
+ // request and close each stream.
+ return new Promise((resolve) => {
+ if (!this[kSize]) {
+ resolve(null)
+ } else {
+ this[kClosedResolve] = resolve
+ }
+ })
+ }
+
+ async [kDestroy] (err) {
+ return new Promise((resolve) => {
+ const requests = this[kQueue].splice(this[kPendingIdx])
+ for (let i = 0; i < requests.length; i++) {
+ const request = requests[i]
+ errorRequest(this, request, err)
+ }
+
+ const callback = () => {
+ if (this[kClosedResolve]) {
+ // TODO (fix): Should we error here with ClientDestroyedError?
+ this[kClosedResolve]()
+ this[kClosedResolve] = null
+ }
+ resolve()
+ }
+
+ if (this[kHTTP2Session] != null) {
+ util.destroy(this[kHTTP2Session], err)
+ this[kHTTP2Session] = null
+ this[kHTTP2SessionState] = null
+ }
+
+ if (!this[kSocket]) {
+ queueMicrotask(callback)
+ } else {
+ util.destroy(this[kSocket].on('close', callback), err)
+ }
+
+ resume(this)
+ })
+ }
+}
+
+function onHttp2SessionError (err) {
+ assert(err.code !== 'ERR_TLS_CERT_ALTNAME_INVALID')
+
+ this[kSocket][kError] = err
+
+ onError(this[kClient], err)
+}
+
+function onHttp2FrameError (type, code, id) {
+ const err = new InformationalError(`HTTP/2: "frameError" received - type ${type}, code ${code}`)
+
+ if (id === 0) {
+ this[kSocket][kError] = err
+ onError(this[kClient], err)
+ }
+}
+
+function onHttp2SessionEnd () {
+ util.destroy(this, new SocketError('other side closed'))
+ util.destroy(this[kSocket], new SocketError('other side closed'))
+}
+
+function onHTTP2GoAway (code) {
+ const client = this[kClient]
+ const err = new InformationalError(`HTTP/2: "GOAWAY" frame received with code ${code}`)
+ client[kSocket] = null
+ client[kHTTP2Session] = null
+
+ if (client.destroyed) {
+ assert(this[kPending] === 0)
+
+ // Fail entire queue.
+ const requests = client[kQueue].splice(client[kRunningIdx])
+ for (let i = 0; i < requests.length; i++) {
+ const request = requests[i]
+ errorRequest(this, request, err)
+ }
+ } else if (client[kRunning] > 0) {
+ // Fail head of pipeline.
+ const request = client[kQueue][client[kRunningIdx]]
+ client[kQueue][client[kRunningIdx]++] = null
+
+ errorRequest(client, request, err)
+ }
+
+ client[kPendingIdx] = client[kRunningIdx]
+
+ assert(client[kRunning] === 0)
+
+ client.emit('disconnect',
+ client[kUrl],
+ [client],
+ err
+ )
+
+ resume(client)
+}
+
+const constants = require('./llhttp/constants')
+const createRedirectInterceptor = require('./interceptor/redirectInterceptor')
+const EMPTY_BUF = Buffer.alloc(0)
+
+async function lazyllhttp () {
+ const llhttpWasmData = process.env.JEST_WORKER_ID ? require('./llhttp/llhttp-wasm.js') : undefined
+
+ let mod
+ try {
+ mod = await WebAssembly.compile(Buffer.from(require('./llhttp/llhttp_simd-wasm.js'), 'base64'))
+ } catch (e) {
+ /* istanbul ignore next */
+
+ // We could check if the error was caused by the simd option not
+ // being enabled, but the occurring of this other error
+ // * https://github.com/emscripten-core/emscripten/issues/11495
+ // got me to remove that check to avoid breaking Node 12.
+ mod = await WebAssembly.compile(Buffer.from(llhttpWasmData || require('./llhttp/llhttp-wasm.js'), 'base64'))
+ }
+
+ return await WebAssembly.instantiate(mod, {
+ env: {
+ /* eslint-disable camelcase */
+
+ wasm_on_url: (p, at, len) => {
+ /* istanbul ignore next */
+ return 0
+ },
+ wasm_on_status: (p, at, len) => {
+ assert.strictEqual(currentParser.ptr, p)
+ const start = at - currentBufferPtr + currentBufferRef.byteOffset
+ return currentParser.onStatus(new FastBuffer(currentBufferRef.buffer, start, len)) || 0
+ },
+ wasm_on_message_begin: (p) => {
+ assert.strictEqual(currentParser.ptr, p)
+ return currentParser.onMessageBegin() || 0
+ },
+ wasm_on_header_field: (p, at, len) => {
+ assert.strictEqual(currentParser.ptr, p)
+ const start = at - currentBufferPtr + currentBufferRef.byteOffset
+ return currentParser.onHeaderField(new FastBuffer(currentBufferRef.buffer, start, len)) || 0
+ },
+ wasm_on_header_value: (p, at, len) => {
+ assert.strictEqual(currentParser.ptr, p)
+ const start = at - currentBufferPtr + currentBufferRef.byteOffset
+ return currentParser.onHeaderValue(new FastBuffer(currentBufferRef.buffer, start, len)) || 0
+ },
+ wasm_on_headers_complete: (p, statusCode, upgrade, shouldKeepAlive) => {
+ assert.strictEqual(currentParser.ptr, p)
+ return currentParser.onHeadersComplete(statusCode, Boolean(upgrade), Boolean(shouldKeepAlive)) || 0
+ },
+ wasm_on_body: (p, at, len) => {
+ assert.strictEqual(currentParser.ptr, p)
+ const start = at - currentBufferPtr + currentBufferRef.byteOffset
+ return currentParser.onBody(new FastBuffer(currentBufferRef.buffer, start, len)) || 0
+ },
+ wasm_on_message_complete: (p) => {
+ assert.strictEqual(currentParser.ptr, p)
+ return currentParser.onMessageComplete() || 0
+ }
+
+ /* eslint-enable camelcase */
+ }
+ })
+}
+
+let llhttpInstance = null
+let llhttpPromise = lazyllhttp()
+llhttpPromise.catch()
+
+let currentParser = null
+let currentBufferRef = null
+let currentBufferSize = 0
+let currentBufferPtr = null
+
+const TIMEOUT_HEADERS = 1
+const TIMEOUT_BODY = 2
+const TIMEOUT_IDLE = 3
+
+class Parser {
+ constructor (client, socket, { exports }) {
+ assert(Number.isFinite(client[kMaxHeadersSize]) && client[kMaxHeadersSize] > 0)
+
+ this.llhttp = exports
+ this.ptr = this.llhttp.llhttp_alloc(constants.TYPE.RESPONSE)
+ this.client = client
+ this.socket = socket
+ this.timeout = null
+ this.timeoutValue = null
+ this.timeoutType = null
+ this.statusCode = null
+ this.statusText = ''
+ this.upgrade = false
+ this.headers = []
+ this.headersSize = 0
+ this.headersMaxSize = client[kMaxHeadersSize]
+ this.shouldKeepAlive = false
+ this.paused = false
+ this.resume = this.resume.bind(this)
+
+ this.bytesRead = 0
+
+ this.keepAlive = ''
+ this.contentLength = ''
+ this.connection = ''
+ this.maxResponseSize = client[kMaxResponseSize]
+ }
+
+ setTimeout (value, type) {
+ this.timeoutType = type
+ if (value !== this.timeoutValue) {
+ timers.clearTimeout(this.timeout)
+ if (value) {
+ this.timeout = timers.setTimeout(onParserTimeout, value, this)
+ // istanbul ignore else: only for jest
+ if (this.timeout.unref) {
+ this.timeout.unref()
+ }
+ } else {
+ this.timeout = null
+ }
+ this.timeoutValue = value
+ } else if (this.timeout) {
+ // istanbul ignore else: only for jest
+ if (this.timeout.refresh) {
+ this.timeout.refresh()
+ }
+ }
+ }
+
+ resume () {
+ if (this.socket.destroyed || !this.paused) {
+ return
+ }
+
+ assert(this.ptr != null)
+ assert(currentParser == null)
+
+ this.llhttp.llhttp_resume(this.ptr)
+
+ assert(this.timeoutType === TIMEOUT_BODY)
+ if (this.timeout) {
+ // istanbul ignore else: only for jest
+ if (this.timeout.refresh) {
+ this.timeout.refresh()
+ }
+ }
+
+ this.paused = false
+ this.execute(this.socket.read() || EMPTY_BUF) // Flush parser.
+ this.readMore()
+ }
+
+ readMore () {
+ while (!this.paused && this.ptr) {
+ const chunk = this.socket.read()
+ if (chunk === null) {
+ break
+ }
+ this.execute(chunk)
+ }
+ }
+
+ execute (data) {
+ assert(this.ptr != null)
+ assert(currentParser == null)
+ assert(!this.paused)
+
+ const { socket, llhttp } = this
+
+ if (data.length > currentBufferSize) {
+ if (currentBufferPtr) {
+ llhttp.free(currentBufferPtr)
+ }
+ currentBufferSize = Math.ceil(data.length / 4096) * 4096
+ currentBufferPtr = llhttp.malloc(currentBufferSize)
+ }
+
+ new Uint8Array(llhttp.memory.buffer, currentBufferPtr, currentBufferSize).set(data)
+
+ // Call `execute` on the wasm parser.
+ // We pass the `llhttp_parser` pointer address, the pointer address of buffer view data,
+ // and finally the length of bytes to parse.
+ // The return value is an error code or `constants.ERROR.OK`.
+ try {
+ let ret
+
+ try {
+ currentBufferRef = data
+ currentParser = this
+ ret = llhttp.llhttp_execute(this.ptr, currentBufferPtr, data.length)
+ /* eslint-disable-next-line no-useless-catch */
+ } catch (err) {
+ /* istanbul ignore next: difficult to make a test case for */
+ throw err
+ } finally {
+ currentParser = null
+ currentBufferRef = null
+ }
+
+ const offset = llhttp.llhttp_get_error_pos(this.ptr) - currentBufferPtr
+
+ if (ret === constants.ERROR.PAUSED_UPGRADE) {
+ this.onUpgrade(data.slice(offset))
+ } else if (ret === constants.ERROR.PAUSED) {
+ this.paused = true
+ socket.unshift(data.slice(offset))
+ } else if (ret !== constants.ERROR.OK) {
+ const ptr = llhttp.llhttp_get_error_reason(this.ptr)
+ let message = ''
+ /* istanbul ignore else: difficult to make a test case for */
+ if (ptr) {
+ const len = new Uint8Array(llhttp.memory.buffer, ptr).indexOf(0)
+ message =
+ 'Response does not match the HTTP/1.1 protocol (' +
+ Buffer.from(llhttp.memory.buffer, ptr, len).toString() +
+ ')'
+ }
+ throw new HTTPParserError(message, constants.ERROR[ret], data.slice(offset))
+ }
+ } catch (err) {
+ util.destroy(socket, err)
+ }
+ }
+
+ destroy () {
+ assert(this.ptr != null)
+ assert(currentParser == null)
+
+ this.llhttp.llhttp_free(this.ptr)
+ this.ptr = null
+
+ timers.clearTimeout(this.timeout)
+ this.timeout = null
+ this.timeoutValue = null
+ this.timeoutType = null
+
+ this.paused = false
+ }
+
+ onStatus (buf) {
+ this.statusText = buf.toString()
+ }
+
+ onMessageBegin () {
+ const { socket, client } = this
+
+ /* istanbul ignore next: difficult to make a test case for */
+ if (socket.destroyed) {
+ return -1
+ }
+
+ const request = client[kQueue][client[kRunningIdx]]
+ if (!request) {
+ return -1
+ }
+ }
+
+ onHeaderField (buf) {
+ const len = this.headers.length
+
+ if ((len & 1) === 0) {
+ this.headers.push(buf)
+ } else {
+ this.headers[len - 1] = Buffer.concat([this.headers[len - 1], buf])
+ }
+
+ this.trackHeader(buf.length)
+ }
+
+ onHeaderValue (buf) {
+ let len = this.headers.length
+
+ if ((len & 1) === 1) {
+ this.headers.push(buf)
+ len += 1
+ } else {
+ this.headers[len - 1] = Buffer.concat([this.headers[len - 1], buf])
+ }
+
+ const key = this.headers[len - 2]
+ if (key.length === 10 && key.toString().toLowerCase() === 'keep-alive') {
+ this.keepAlive += buf.toString()
+ } else if (key.length === 10 && key.toString().toLowerCase() === 'connection') {
+ this.connection += buf.toString()
+ } else if (key.length === 14 && key.toString().toLowerCase() === 'content-length') {
+ this.contentLength += buf.toString()
+ }
+
+ this.trackHeader(buf.length)
+ }
+
+ trackHeader (len) {
+ this.headersSize += len
+ if (this.headersSize >= this.headersMaxSize) {
+ util.destroy(this.socket, new HeadersOverflowError())
+ }
+ }
+
+ onUpgrade (head) {
+ const { upgrade, client, socket, headers, statusCode } = this
+
+ assert(upgrade)
+
+ const request = client[kQueue][client[kRunningIdx]]
+ assert(request)
+
+ assert(!socket.destroyed)
+ assert(socket === client[kSocket])
+ assert(!this.paused)
+ assert(request.upgrade || request.method === 'CONNECT')
+
+ this.statusCode = null
+ this.statusText = ''
+ this.shouldKeepAlive = null
+
+ assert(this.headers.length % 2 === 0)
+ this.headers = []
+ this.headersSize = 0
+
+ socket.unshift(head)
+
+ socket[kParser].destroy()
+ socket[kParser] = null
+
+ socket[kClient] = null
+ socket[kError] = null
+ socket
+ .removeListener('error', onSocketError)
+ .removeListener('readable', onSocketReadable)
+ .removeListener('end', onSocketEnd)
+ .removeListener('close', onSocketClose)
+
+ client[kSocket] = null
+ client[kQueue][client[kRunningIdx]++] = null
+ client.emit('disconnect', client[kUrl], [client], new InformationalError('upgrade'))
+
+ try {
+ request.onUpgrade(statusCode, headers, socket)
+ } catch (err) {
+ util.destroy(socket, err)
+ }
+
+ resume(client)
+ }
+
+ onHeadersComplete (statusCode, upgrade, shouldKeepAlive) {
+ const { client, socket, headers, statusText } = this
+
+ /* istanbul ignore next: difficult to make a test case for */
+ if (socket.destroyed) {
+ return -1
+ }
+
+ const request = client[kQueue][client[kRunningIdx]]
+
+ /* istanbul ignore next: difficult to make a test case for */
+ if (!request) {
+ return -1
+ }
+
+ assert(!this.upgrade)
+ assert(this.statusCode < 200)
+
+ if (statusCode === 100) {
+ util.destroy(socket, new SocketError('bad response', util.getSocketInfo(socket)))
+ return -1
+ }
+
+ /* this can only happen if server is misbehaving */
+ if (upgrade && !request.upgrade) {
+ util.destroy(socket, new SocketError('bad upgrade', util.getSocketInfo(socket)))
+ return -1
+ }
+
+ assert.strictEqual(this.timeoutType, TIMEOUT_HEADERS)
+
+ this.statusCode = statusCode
+ this.shouldKeepAlive = (
+ shouldKeepAlive ||
+ // Override llhttp value which does not allow keepAlive for HEAD.
+ (request.method === 'HEAD' && !socket[kReset] && this.connection.toLowerCase() === 'keep-alive')
+ )
+
+ if (this.statusCode >= 200) {
+ const bodyTimeout = request.bodyTimeout != null
+ ? request.bodyTimeout
+ : client[kBodyTimeout]
+ this.setTimeout(bodyTimeout, TIMEOUT_BODY)
+ } else if (this.timeout) {
+ // istanbul ignore else: only for jest
+ if (this.timeout.refresh) {
+ this.timeout.refresh()
+ }
+ }
+
+ if (request.method === 'CONNECT') {
+ assert(client[kRunning] === 1)
+ this.upgrade = true
+ return 2
+ }
+
+ if (upgrade) {
+ assert(client[kRunning] === 1)
+ this.upgrade = true
+ return 2
+ }
+
+ assert(this.headers.length % 2 === 0)
+ this.headers = []
+ this.headersSize = 0
+
+ if (this.shouldKeepAlive && client[kPipelining]) {
+ const keepAliveTimeout = this.keepAlive ? util.parseKeepAliveTimeout(this.keepAlive) : null
+
+ if (keepAliveTimeout != null) {
+ const timeout = Math.min(
+ keepAliveTimeout - client[kKeepAliveTimeoutThreshold],
+ client[kKeepAliveMaxTimeout]
+ )
+ if (timeout <= 0) {
+ socket[kReset] = true
+ } else {
+ client[kKeepAliveTimeoutValue] = timeout
+ }
+ } else {
+ client[kKeepAliveTimeoutValue] = client[kKeepAliveDefaultTimeout]
+ }
+ } else {
+ // Stop more requests from being dispatched.
+ socket[kReset] = true
+ }
+
+ const pause = request.onHeaders(statusCode, headers, this.resume, statusText) === false
+
+ if (request.aborted) {
+ return -1
+ }
+
+ if (request.method === 'HEAD') {
+ return 1
+ }
+
+ if (statusCode < 200) {
+ return 1
+ }
+
+ if (socket[kBlocking]) {
+ socket[kBlocking] = false
+ resume(client)
+ }
+
+ return pause ? constants.ERROR.PAUSED : 0
+ }
+
+ onBody (buf) {
+ const { client, socket, statusCode, maxResponseSize } = this
+
+ if (socket.destroyed) {
+ return -1
+ }
+
+ const request = client[kQueue][client[kRunningIdx]]
+ assert(request)
+
+ assert.strictEqual(this.timeoutType, TIMEOUT_BODY)
+ if (this.timeout) {
+ // istanbul ignore else: only for jest
+ if (this.timeout.refresh) {
+ this.timeout.refresh()
+ }
+ }
+
+ assert(statusCode >= 200)
+
+ if (maxResponseSize > -1 && this.bytesRead + buf.length > maxResponseSize) {
+ util.destroy(socket, new ResponseExceededMaxSizeError())
+ return -1
+ }
+
+ this.bytesRead += buf.length
+
+ if (request.onData(buf) === false) {
+ return constants.ERROR.PAUSED
+ }
+ }
+
+ onMessageComplete () {
+ const { client, socket, statusCode, upgrade, headers, contentLength, bytesRead, shouldKeepAlive } = this
+
+ if (socket.destroyed && (!statusCode || shouldKeepAlive)) {
+ return -1
+ }
+
+ if (upgrade) {
+ return
+ }
+
+ const request = client[kQueue][client[kRunningIdx]]
+ assert(request)
+
+ assert(statusCode >= 100)
+
+ this.statusCode = null
+ this.statusText = ''
+ this.bytesRead = 0
+ this.contentLength = ''
+ this.keepAlive = ''
+ this.connection = ''
+
+ assert(this.headers.length % 2 === 0)
+ this.headers = []
+ this.headersSize = 0
+
+ if (statusCode < 200) {
+ return
+ }
+
+ /* istanbul ignore next: should be handled by llhttp? */
+ if (request.method !== 'HEAD' && contentLength && bytesRead !== parseInt(contentLength, 10)) {
+ util.destroy(socket, new ResponseContentLengthMismatchError())
+ return -1
+ }
+
+ request.onComplete(headers)
+
+ client[kQueue][client[kRunningIdx]++] = null
+
+ if (socket[kWriting]) {
+ assert.strictEqual(client[kRunning], 0)
+ // Response completed before request.
+ util.destroy(socket, new InformationalError('reset'))
+ return constants.ERROR.PAUSED
+ } else if (!shouldKeepAlive) {
+ util.destroy(socket, new InformationalError('reset'))
+ return constants.ERROR.PAUSED
+ } else if (socket[kReset] && client[kRunning] === 0) {
+ // Destroy socket once all requests have completed.
+ // The request at the tail of the pipeline is the one
+ // that requested reset and no further requests should
+ // have been queued since then.
+ util.destroy(socket, new InformationalError('reset'))
+ return constants.ERROR.PAUSED
+ } else if (client[kPipelining] === 1) {
+ // We must wait a full event loop cycle to reuse this socket to make sure
+ // that non-spec compliant servers are not closing the connection even if they
+ // said they won't.
+ setImmediate(resume, client)
+ } else {
+ resume(client)
+ }
+ }
+}
+
+function onParserTimeout (parser) {
+ const { socket, timeoutType, client } = parser
+
+ /* istanbul ignore else */
+ if (timeoutType === TIMEOUT_HEADERS) {
+ if (!socket[kWriting] || socket.writableNeedDrain || client[kRunning] > 1) {
+ assert(!parser.paused, 'cannot be paused while waiting for headers')
+ util.destroy(socket, new HeadersTimeoutError())
+ }
+ } else if (timeoutType === TIMEOUT_BODY) {
+ if (!parser.paused) {
+ util.destroy(socket, new BodyTimeoutError())
+ }
+ } else if (timeoutType === TIMEOUT_IDLE) {
+ assert(client[kRunning] === 0 && client[kKeepAliveTimeoutValue])
+ util.destroy(socket, new InformationalError('socket idle timeout'))
+ }
+}
+
+function onSocketReadable () {
+ const { [kParser]: parser } = this
+ if (parser) {
+ parser.readMore()
+ }
+}
+
+function onSocketError (err) {
+ const { [kClient]: client, [kParser]: parser } = this
+
+ assert(err.code !== 'ERR_TLS_CERT_ALTNAME_INVALID')
+
+ if (client[kHTTPConnVersion] !== 'h2') {
+ // On Mac OS, we get an ECONNRESET even if there is a full body to be forwarded
+ // to the user.
+ if (err.code === 'ECONNRESET' && parser.statusCode && !parser.shouldKeepAlive) {
+ // We treat all incoming data so for as a valid response.
+ parser.onMessageComplete()
+ return
+ }
+ }
+
+ this[kError] = err
+
+ onError(this[kClient], err)
+}
+
+function onError (client, err) {
+ if (
+ client[kRunning] === 0 &&
+ err.code !== 'UND_ERR_INFO' &&
+ err.code !== 'UND_ERR_SOCKET'
+ ) {
+ // Error is not caused by running request and not a recoverable
+ // socket error.
+
+ assert(client[kPendingIdx] === client[kRunningIdx])
+
+ const requests = client[kQueue].splice(client[kRunningIdx])
+ for (let i = 0; i < requests.length; i++) {
+ const request = requests[i]
+ errorRequest(client, request, err)
+ }
+ assert(client[kSize] === 0)
+ }
+}
+
+function onSocketEnd () {
+ const { [kParser]: parser, [kClient]: client } = this
+
+ if (client[kHTTPConnVersion] !== 'h2') {
+ if (parser.statusCode && !parser.shouldKeepAlive) {
+ // We treat all incoming data so far as a valid response.
+ parser.onMessageComplete()
+ return
+ }
+ }
+
+ util.destroy(this, new SocketError('other side closed', util.getSocketInfo(this)))
+}
+
+function onSocketClose () {
+ const { [kClient]: client, [kParser]: parser } = this
+
+ if (client[kHTTPConnVersion] === 'h1' && parser) {
+ if (!this[kError] && parser.statusCode && !parser.shouldKeepAlive) {
+ // We treat all incoming data so far as a valid response.
+ parser.onMessageComplete()
+ }
+
+ this[kParser].destroy()
+ this[kParser] = null
+ }
+
+ const err = this[kError] || new SocketError('closed', util.getSocketInfo(this))
+
+ client[kSocket] = null
+
+ if (client.destroyed) {
+ assert(client[kPending] === 0)
+
+ // Fail entire queue.
+ const requests = client[kQueue].splice(client[kRunningIdx])
+ for (let i = 0; i < requests.length; i++) {
+ const request = requests[i]
+ errorRequest(client, request, err)
+ }
+ } else if (client[kRunning] > 0 && err.code !== 'UND_ERR_INFO') {
+ // Fail head of pipeline.
+ const request = client[kQueue][client[kRunningIdx]]
+ client[kQueue][client[kRunningIdx]++] = null
+
+ errorRequest(client, request, err)
+ }
+
+ client[kPendingIdx] = client[kRunningIdx]
+
+ assert(client[kRunning] === 0)
+
+ client.emit('disconnect', client[kUrl], [client], err)
+
+ resume(client)
+}
+
+async function connect (client) {
+ assert(!client[kConnecting])
+ assert(!client[kSocket])
+
+ let { host, hostname, protocol, port } = client[kUrl]
+
+ // Resolve ipv6
+ if (hostname[0] === '[') {
+ const idx = hostname.indexOf(']')
+
+ assert(idx !== -1)
+ const ip = hostname.substring(1, idx)
+
+ assert(net.isIP(ip))
+ hostname = ip
+ }
+
+ client[kConnecting] = true
+
+ if (channels.beforeConnect.hasSubscribers) {
+ channels.beforeConnect.publish({
+ connectParams: {
+ host,
+ hostname,
+ protocol,
+ port,
+ servername: client[kServerName],
+ localAddress: client[kLocalAddress]
+ },
+ connector: client[kConnector]
+ })
+ }
+
+ try {
+ const socket = await new Promise((resolve, reject) => {
+ client[kConnector]({
+ host,
+ hostname,
+ protocol,
+ port,
+ servername: client[kServerName],
+ localAddress: client[kLocalAddress]
+ }, (err, socket) => {
+ if (err) {
+ reject(err)
+ } else {
+ resolve(socket)
+ }
+ })
+ })
+
+ if (client.destroyed) {
+ util.destroy(socket.on('error', () => {}), new ClientDestroyedError())
+ return
+ }
+
+ client[kConnecting] = false
+
+ assert(socket)
+
+ const isH2 = socket.alpnProtocol === 'h2'
+ if (isH2) {
+ if (!h2ExperimentalWarned) {
+ h2ExperimentalWarned = true
+ process.emitWarning('H2 support is experimental, expect them to change at any time.', {
+ code: 'UNDICI-H2'
+ })
+ }
+
+ const session = http2.connect(client[kUrl], {
+ createConnection: () => socket,
+ peerMaxConcurrentStreams: client[kHTTP2SessionState].maxConcurrentStreams
+ })
+
+ client[kHTTPConnVersion] = 'h2'
+ session[kClient] = client
+ session[kSocket] = socket
+ session.on('error', onHttp2SessionError)
+ session.on('frameError', onHttp2FrameError)
+ session.on('end', onHttp2SessionEnd)
+ session.on('goaway', onHTTP2GoAway)
+ session.on('close', onSocketClose)
+ session.unref()
+
+ client[kHTTP2Session] = session
+ socket[kHTTP2Session] = session
+ } else {
+ if (!llhttpInstance) {
+ llhttpInstance = await llhttpPromise
+ llhttpPromise = null
+ }
+
+ socket[kNoRef] = false
+ socket[kWriting] = false
+ socket[kReset] = false
+ socket[kBlocking] = false
+ socket[kParser] = new Parser(client, socket, llhttpInstance)
+ }
+
+ socket[kCounter] = 0
+ socket[kMaxRequests] = client[kMaxRequests]
+ socket[kClient] = client
+ socket[kError] = null
+
+ socket
+ .on('error', onSocketError)
+ .on('readable', onSocketReadable)
+ .on('end', onSocketEnd)
+ .on('close', onSocketClose)
+
+ client[kSocket] = socket
+
+ if (channels.connected.hasSubscribers) {
+ channels.connected.publish({
+ connectParams: {
+ host,
+ hostname,
+ protocol,
+ port,
+ servername: client[kServerName],
+ localAddress: client[kLocalAddress]
+ },
+ connector: client[kConnector],
+ socket
+ })
+ }
+ client.emit('connect', client[kUrl], [client])
+ } catch (err) {
+ if (client.destroyed) {
+ return
+ }
+
+ client[kConnecting] = false
+
+ if (channels.connectError.hasSubscribers) {
+ channels.connectError.publish({
+ connectParams: {
+ host,
+ hostname,
+ protocol,
+ port,
+ servername: client[kServerName],
+ localAddress: client[kLocalAddress]
+ },
+ connector: client[kConnector],
+ error: err
+ })
+ }
+
+ if (err.code === 'ERR_TLS_CERT_ALTNAME_INVALID') {
+ assert(client[kRunning] === 0)
+ while (client[kPending] > 0 && client[kQueue][client[kPendingIdx]].servername === client[kServerName]) {
+ const request = client[kQueue][client[kPendingIdx]++]
+ errorRequest(client, request, err)
+ }
+ } else {
+ onError(client, err)
+ }
+
+ client.emit('connectionError', client[kUrl], [client], err)
+ }
+
+ resume(client)
+}
+
+function emitDrain (client) {
+ client[kNeedDrain] = 0
+ client.emit('drain', client[kUrl], [client])
+}
+
+function resume (client, sync) {
+ if (client[kResuming] === 2) {
+ return
+ }
+
+ client[kResuming] = 2
+
+ _resume(client, sync)
+ client[kResuming] = 0
+
+ if (client[kRunningIdx] > 256) {
+ client[kQueue].splice(0, client[kRunningIdx])
+ client[kPendingIdx] -= client[kRunningIdx]
+ client[kRunningIdx] = 0
+ }
+}
+
+function _resume (client, sync) {
+ while (true) {
+ if (client.destroyed) {
+ assert(client[kPending] === 0)
+ return
+ }
+
+ if (client[kClosedResolve] && !client[kSize]) {
+ client[kClosedResolve]()
+ client[kClosedResolve] = null
+ return
+ }
+
+ const socket = client[kSocket]
+
+ if (socket && !socket.destroyed && socket.alpnProtocol !== 'h2') {
+ if (client[kSize] === 0) {
+ if (!socket[kNoRef] && socket.unref) {
+ socket.unref()
+ socket[kNoRef] = true
+ }
+ } else if (socket[kNoRef] && socket.ref) {
+ socket.ref()
+ socket[kNoRef] = false
+ }
+
+ if (client[kSize] === 0) {
+ if (socket[kParser].timeoutType !== TIMEOUT_IDLE) {
+ socket[kParser].setTimeout(client[kKeepAliveTimeoutValue], TIMEOUT_IDLE)
+ }
+ } else if (client[kRunning] > 0 && socket[kParser].statusCode < 200) {
+ if (socket[kParser].timeoutType !== TIMEOUT_HEADERS) {
+ const request = client[kQueue][client[kRunningIdx]]
+ const headersTimeout = request.headersTimeout != null
+ ? request.headersTimeout
+ : client[kHeadersTimeout]
+ socket[kParser].setTimeout(headersTimeout, TIMEOUT_HEADERS)
+ }
+ }
+ }
+
+ if (client[kBusy]) {
+ client[kNeedDrain] = 2
+ } else if (client[kNeedDrain] === 2) {
+ if (sync) {
+ client[kNeedDrain] = 1
+ process.nextTick(emitDrain, client)
+ } else {
+ emitDrain(client)
+ }
+ continue
+ }
+
+ if (client[kPending] === 0) {
+ return
+ }
+
+ if (client[kRunning] >= (client[kPipelining] || 1)) {
+ return
+ }
+
+ const request = client[kQueue][client[kPendingIdx]]
+
+ if (client[kUrl].protocol === 'https:' && client[kServerName] !== request.servername) {
+ if (client[kRunning] > 0) {
+ return
+ }
+
+ client[kServerName] = request.servername
+
+ if (socket && socket.servername !== request.servername) {
+ util.destroy(socket, new InformationalError('servername changed'))
+ return
+ }
+ }
+
+ if (client[kConnecting]) {
+ return
+ }
+
+ if (!socket && !client[kHTTP2Session]) {
+ connect(client)
+ return
+ }
+
+ if (socket.destroyed || socket[kWriting] || socket[kReset] || socket[kBlocking]) {
+ return
+ }
+
+ if (client[kRunning] > 0 && !request.idempotent) {
+ // Non-idempotent request cannot be retried.
+ // Ensure that no other requests are inflight and
+ // could cause failure.
+ return
+ }
+
+ if (client[kRunning] > 0 && (request.upgrade || request.method === 'CONNECT')) {
+ // Don't dispatch an upgrade until all preceding requests have completed.
+ // A misbehaving server might upgrade the connection before all pipelined
+ // request has completed.
+ return
+ }
+
+ if (client[kRunning] > 0 && util.bodyLength(request.body) !== 0 &&
+ (util.isStream(request.body) || util.isAsyncIterable(request.body))) {
+ // Request with stream or iterator body can error while other requests
+ // are inflight and indirectly error those as well.
+ // Ensure this doesn't happen by waiting for inflight
+ // to complete before dispatching.
+
+ // Request with stream or iterator body cannot be retried.
+ // Ensure that no other requests are inflight and
+ // could cause failure.
+ return
+ }
+
+ if (!request.aborted && write(client, request)) {
+ client[kPendingIdx]++
+ } else {
+ client[kQueue].splice(client[kPendingIdx], 1)
+ }
+ }
+}
+
+// https://www.rfc-editor.org/rfc/rfc7230#section-3.3.2
+function shouldSendContentLength (method) {
+ return method !== 'GET' && method !== 'HEAD' && method !== 'OPTIONS' && method !== 'TRACE' && method !== 'CONNECT'
+}
+
+function write (client, request) {
+ if (client[kHTTPConnVersion] === 'h2') {
+ writeH2(client, client[kHTTP2Session], request)
+ return
+ }
+
+ const { body, method, path, host, upgrade, headers, blocking, reset } = request
+
+ // https://tools.ietf.org/html/rfc7231#section-4.3.1
+ // https://tools.ietf.org/html/rfc7231#section-4.3.2
+ // https://tools.ietf.org/html/rfc7231#section-4.3.5
+
+ // Sending a payload body on a request that does not
+ // expect it can cause undefined behavior on some
+ // servers and corrupt connection state. Do not
+ // re-use the connection for further requests.
+
+ const expectsPayload = (
+ method === 'PUT' ||
+ method === 'POST' ||
+ method === 'PATCH'
+ )
+
+ if (body && typeof body.read === 'function') {
+ // Try to read EOF in order to get length.
+ body.read(0)
+ }
+
+ const bodyLength = util.bodyLength(body)
+
+ let contentLength = bodyLength
+
+ if (contentLength === null) {
+ contentLength = request.contentLength
+ }
+
+ if (contentLength === 0 && !expectsPayload) {
+ // https://tools.ietf.org/html/rfc7230#section-3.3.2
+ // A user agent SHOULD NOT send a Content-Length header field when
+ // the request message does not contain a payload body and the method
+ // semantics do not anticipate such a body.
+
+ contentLength = null
+ }
+
+ // https://github.com/nodejs/undici/issues/2046
+ // A user agent may send a Content-Length header with 0 value, this should be allowed.
+ if (shouldSendContentLength(method) && contentLength > 0 && request.contentLength !== null && request.contentLength !== contentLength) {
+ if (client[kStrictContentLength]) {
+ errorRequest(client, request, new RequestContentLengthMismatchError())
+ return false
+ }
+
+ process.emitWarning(new RequestContentLengthMismatchError())
+ }
+
+ const socket = client[kSocket]
+
+ try {
+ request.onConnect((err) => {
+ if (request.aborted || request.completed) {
+ return
+ }
+
+ errorRequest(client, request, err || new RequestAbortedError())
+
+ util.destroy(socket, new InformationalError('aborted'))
+ })
+ } catch (err) {
+ errorRequest(client, request, err)
+ }
+
+ if (request.aborted) {
+ return false
+ }
+
+ if (method === 'HEAD') {
+ // https://github.com/mcollina/undici/issues/258
+ // Close after a HEAD request to interop with misbehaving servers
+ // that may send a body in the response.
+
+ socket[kReset] = true
+ }
+
+ if (upgrade || method === 'CONNECT') {
+ // On CONNECT or upgrade, block pipeline from dispatching further
+ // requests on this connection.
+
+ socket[kReset] = true
+ }
+
+ if (reset != null) {
+ socket[kReset] = reset
+ }
+
+ if (client[kMaxRequests] && socket[kCounter]++ >= client[kMaxRequests]) {
+ socket[kReset] = true
+ }
+
+ if (blocking) {
+ socket[kBlocking] = true
+ }
+
+ let header = `${method} ${path} HTTP/1.1\r\n`
+
+ if (typeof host === 'string') {
+ header += `host: ${host}\r\n`
+ } else {
+ header += client[kHostHeader]
+ }
+
+ if (upgrade) {
+ header += `connection: upgrade\r\nupgrade: ${upgrade}\r\n`
+ } else if (client[kPipelining] && !socket[kReset]) {
+ header += 'connection: keep-alive\r\n'
+ } else {
+ header += 'connection: close\r\n'
+ }
+
+ if (headers) {
+ header += headers
+ }
+
+ if (channels.sendHeaders.hasSubscribers) {
+ channels.sendHeaders.publish({ request, headers: header, socket })
+ }
+
+ /* istanbul ignore else: assertion */
+ if (!body || bodyLength === 0) {
+ if (contentLength === 0) {
+ socket.write(`${header}content-length: 0\r\n\r\n`, 'latin1')
+ } else {
+ assert(contentLength === null, 'no body must not have content length')
+ socket.write(`${header}\r\n`, 'latin1')
+ }
+ request.onRequestSent()
+ } else if (util.isBuffer(body)) {
+ assert(contentLength === body.byteLength, 'buffer body must have content length')
+
+ socket.cork()
+ socket.write(`${header}content-length: ${contentLength}\r\n\r\n`, 'latin1')
+ socket.write(body)
+ socket.uncork()
+ request.onBodySent(body)
+ request.onRequestSent()
+ if (!expectsPayload) {
+ socket[kReset] = true
+ }
+ } else if (util.isBlobLike(body)) {
+ if (typeof body.stream === 'function') {
+ writeIterable({ body: body.stream(), client, request, socket, contentLength, header, expectsPayload })
+ } else {
+ writeBlob({ body, client, request, socket, contentLength, header, expectsPayload })
+ }
+ } else if (util.isStream(body)) {
+ writeStream({ body, client, request, socket, contentLength, header, expectsPayload })
+ } else if (util.isIterable(body)) {
+ writeIterable({ body, client, request, socket, contentLength, header, expectsPayload })
+ } else {
+ assert(false)
+ }
+
+ return true
+}
+
+function writeH2 (client, session, request) {
+ const { body, method, path, host, upgrade, expectContinue, signal, headers: reqHeaders } = request
+
+ let headers
+ if (typeof reqHeaders === 'string') headers = Request[kHTTP2CopyHeaders](reqHeaders.trim())
+ else headers = reqHeaders
+
+ if (upgrade) {
+ errorRequest(client, request, new Error('Upgrade not supported for H2'))
+ return false
+ }
+
+ try {
+ // TODO(HTTP/2): Should we call onConnect immediately or on stream ready event?
+ request.onConnect((err) => {
+ if (request.aborted || request.completed) {
+ return
+ }
+
+ errorRequest(client, request, err || new RequestAbortedError())
+ })
+ } catch (err) {
+ errorRequest(client, request, err)
+ }
+
+ if (request.aborted) {
+ return false
+ }
+
+ /** @type {import('node:http2').ClientHttp2Stream} */
+ let stream
+ const h2State = client[kHTTP2SessionState]
+
+ headers[HTTP2_HEADER_AUTHORITY] = host || client[kHost]
+ headers[HTTP2_HEADER_METHOD] = method
+
+ if (method === 'CONNECT') {
+ session.ref()
+ // we are already connected, streams are pending, first request
+ // will create a new stream. We trigger a request to create the stream and wait until
+ // `ready` event is triggered
+ // We disabled endStream to allow the user to write to the stream
+ stream = session.request(headers, { endStream: false, signal })
+
+ if (stream.id && !stream.pending) {
+ request.onUpgrade(null, null, stream)
+ ++h2State.openStreams
+ } else {
+ stream.once('ready', () => {
+ request.onUpgrade(null, null, stream)
+ ++h2State.openStreams
+ })
+ }
+
+ stream.once('close', () => {
+ h2State.openStreams -= 1
+ // TODO(HTTP/2): unref only if current streams count is 0
+ if (h2State.openStreams === 0) session.unref()
+ })
+
+ return true
+ }
+
+ // https://tools.ietf.org/html/rfc7540#section-8.3
+ // :path and :scheme headers must be omited when sending CONNECT
+
+ headers[HTTP2_HEADER_PATH] = path
+ headers[HTTP2_HEADER_SCHEME] = 'https'
+
+ // https://tools.ietf.org/html/rfc7231#section-4.3.1
+ // https://tools.ietf.org/html/rfc7231#section-4.3.2
+ // https://tools.ietf.org/html/rfc7231#section-4.3.5
+
+ // Sending a payload body on a request that does not
+ // expect it can cause undefined behavior on some
+ // servers and corrupt connection state. Do not
+ // re-use the connection for further requests.
+
+ const expectsPayload = (
+ method === 'PUT' ||
+ method === 'POST' ||
+ method === 'PATCH'
+ )
+
+ if (body && typeof body.read === 'function') {
+ // Try to read EOF in order to get length.
+ body.read(0)
+ }
+
+ let contentLength = util.bodyLength(body)
+
+ if (contentLength == null) {
+ contentLength = request.contentLength
+ }
+
+ if (contentLength === 0 || !expectsPayload) {
+ // https://tools.ietf.org/html/rfc7230#section-3.3.2
+ // A user agent SHOULD NOT send a Content-Length header field when
+ // the request message does not contain a payload body and the method
+ // semantics do not anticipate such a body.
+
+ contentLength = null
+ }
+
+ // https://github.com/nodejs/undici/issues/2046
+ // A user agent may send a Content-Length header with 0 value, this should be allowed.
+ if (shouldSendContentLength(method) && contentLength > 0 && request.contentLength != null && request.contentLength !== contentLength) {
+ if (client[kStrictContentLength]) {
+ errorRequest(client, request, new RequestContentLengthMismatchError())
+ return false
+ }
+
+ process.emitWarning(new RequestContentLengthMismatchError())
+ }
+
+ if (contentLength != null) {
+ assert(body, 'no body must not have content length')
+ headers[HTTP2_HEADER_CONTENT_LENGTH] = `${contentLength}`
+ }
+
+ session.ref()
+
+ const shouldEndStream = method === 'GET' || method === 'HEAD'
+ if (expectContinue) {
+ headers[HTTP2_HEADER_EXPECT] = '100-continue'
+ stream = session.request(headers, { endStream: shouldEndStream, signal })
+
+ stream.once('continue', writeBodyH2)
+ } else {
+ stream = session.request(headers, {
+ endStream: shouldEndStream,
+ signal
+ })
+ writeBodyH2()
+ }
+
+ // Increment counter as we have new several streams open
+ ++h2State.openStreams
+
+ stream.once('response', headers => {
+ const { [HTTP2_HEADER_STATUS]: statusCode, ...realHeaders } = headers
+
+ if (request.onHeaders(Number(statusCode), realHeaders, stream.resume.bind(stream), '') === false) {
+ stream.pause()
+ }
+ })
+
+ stream.once('end', () => {
+ request.onComplete([])
+ })
+
+ stream.on('data', (chunk) => {
+ if (request.onData(chunk) === false) {
+ stream.pause()
+ }
+ })
+
+ stream.once('close', () => {
+ h2State.openStreams -= 1
+ // TODO(HTTP/2): unref only if current streams count is 0
+ if (h2State.openStreams === 0) {
+ session.unref()
+ }
+ })
+
+ stream.once('error', function (err) {
+ if (client[kHTTP2Session] && !client[kHTTP2Session].destroyed && !this.closed && !this.destroyed) {
+ h2State.streams -= 1
+ util.destroy(stream, err)
+ }
+ })
+
+ stream.once('frameError', (type, code) => {
+ const err = new InformationalError(`HTTP/2: "frameError" received - type ${type}, code ${code}`)
+ errorRequest(client, request, err)
+
+ if (client[kHTTP2Session] && !client[kHTTP2Session].destroyed && !this.closed && !this.destroyed) {
+ h2State.streams -= 1
+ util.destroy(stream, err)
+ }
+ })
+
+ // stream.on('aborted', () => {
+ // // TODO(HTTP/2): Support aborted
+ // })
+
+ // stream.on('timeout', () => {
+ // // TODO(HTTP/2): Support timeout
+ // })
+
+ // stream.on('push', headers => {
+ // // TODO(HTTP/2): Suppor push
+ // })
+
+ // stream.on('trailers', headers => {
+ // // TODO(HTTP/2): Support trailers
+ // })
+
+ return true
+
+ function writeBodyH2 () {
+ /* istanbul ignore else: assertion */
+ if (!body) {
+ request.onRequestSent()
+ } else if (util.isBuffer(body)) {
+ assert(contentLength === body.byteLength, 'buffer body must have content length')
+ stream.cork()
+ stream.write(body)
+ stream.uncork()
+ stream.end()
+ request.onBodySent(body)
+ request.onRequestSent()
+ } else if (util.isBlobLike(body)) {
+ if (typeof body.stream === 'function') {
+ writeIterable({
+ client,
+ request,
+ contentLength,
+ h2stream: stream,
+ expectsPayload,
+ body: body.stream(),
+ socket: client[kSocket],
+ header: ''
+ })
+ } else {
+ writeBlob({
+ body,
+ client,
+ request,
+ contentLength,
+ expectsPayload,
+ h2stream: stream,
+ header: '',
+ socket: client[kSocket]
+ })
+ }
+ } else if (util.isStream(body)) {
+ writeStream({
+ body,
+ client,
+ request,
+ contentLength,
+ expectsPayload,
+ socket: client[kSocket],
+ h2stream: stream,
+ header: ''
+ })
+ } else if (util.isIterable(body)) {
+ writeIterable({
+ body,
+ client,
+ request,
+ contentLength,
+ expectsPayload,
+ header: '',
+ h2stream: stream,
+ socket: client[kSocket]
+ })
+ } else {
+ assert(false)
+ }
+ }
+}
+
+function writeStream ({ h2stream, body, client, request, socket, contentLength, header, expectsPayload }) {
+ assert(contentLength !== 0 || client[kRunning] === 0, 'stream body cannot be pipelined')
+
+ if (client[kHTTPConnVersion] === 'h2') {
+ // For HTTP/2, is enough to pipe the stream
+ const pipe = pipeline(
+ body,
+ h2stream,
+ (err) => {
+ if (err) {
+ util.destroy(body, err)
+ util.destroy(h2stream, err)
+ } else {
+ request.onRequestSent()
+ }
+ }
+ )
+
+ pipe.on('data', onPipeData)
+ pipe.once('end', () => {
+ pipe.removeListener('data', onPipeData)
+ util.destroy(pipe)
+ })
+
+ function onPipeData (chunk) {
+ request.onBodySent(chunk)
+ }
+
+ return
+ }
+
+ let finished = false
+
+ const writer = new AsyncWriter({ socket, request, contentLength, client, expectsPayload, header })
+
+ const onData = function (chunk) {
+ if (finished) {
+ return
+ }
+
+ try {
+ if (!writer.write(chunk) && this.pause) {
+ this.pause()
+ }
+ } catch (err) {
+ util.destroy(this, err)
+ }
+ }
+ const onDrain = function () {
+ if (finished) {
+ return
+ }
+
+ if (body.resume) {
+ body.resume()
+ }
+ }
+ const onAbort = function () {
+ if (finished) {
+ return
+ }
+ const err = new RequestAbortedError()
+ queueMicrotask(() => onFinished(err))
+ }
+ const onFinished = function (err) {
+ if (finished) {
+ return
+ }
+
+ finished = true
+
+ assert(socket.destroyed || (socket[kWriting] && client[kRunning] <= 1))
+
+ socket
+ .off('drain', onDrain)
+ .off('error', onFinished)
+
+ body
+ .removeListener('data', onData)
+ .removeListener('end', onFinished)
+ .removeListener('error', onFinished)
+ .removeListener('close', onAbort)
+
+ if (!err) {
+ try {
+ writer.end()
+ } catch (er) {
+ err = er
+ }
+ }
+
+ writer.destroy(err)
+
+ if (err && (err.code !== 'UND_ERR_INFO' || err.message !== 'reset')) {
+ util.destroy(body, err)
+ } else {
+ util.destroy(body)
+ }
+ }
+
+ body
+ .on('data', onData)
+ .on('end', onFinished)
+ .on('error', onFinished)
+ .on('close', onAbort)
+
+ if (body.resume) {
+ body.resume()
+ }
+
+ socket
+ .on('drain', onDrain)
+ .on('error', onFinished)
+}
+
+async function writeBlob ({ h2stream, body, client, request, socket, contentLength, header, expectsPayload }) {
+ assert(contentLength === body.size, 'blob body must have content length')
+
+ const isH2 = client[kHTTPConnVersion] === 'h2'
+ try {
+ if (contentLength != null && contentLength !== body.size) {
+ throw new RequestContentLengthMismatchError()
+ }
+
+ const buffer = Buffer.from(await body.arrayBuffer())
+
+ if (isH2) {
+ h2stream.cork()
+ h2stream.write(buffer)
+ h2stream.uncork()
+ } else {
+ socket.cork()
+ socket.write(`${header}content-length: ${contentLength}\r\n\r\n`, 'latin1')
+ socket.write(buffer)
+ socket.uncork()
+ }
+
+ request.onBodySent(buffer)
+ request.onRequestSent()
+
+ if (!expectsPayload) {
+ socket[kReset] = true
+ }
+
+ resume(client)
+ } catch (err) {
+ util.destroy(isH2 ? h2stream : socket, err)
+ }
+}
+
+async function writeIterable ({ h2stream, body, client, request, socket, contentLength, header, expectsPayload }) {
+ assert(contentLength !== 0 || client[kRunning] === 0, 'iterator body cannot be pipelined')
+
+ let callback = null
+ function onDrain () {
+ if (callback) {
+ const cb = callback
+ callback = null
+ cb()
+ }
+ }
+
+ const waitForDrain = () => new Promise((resolve, reject) => {
+ assert(callback === null)
+
+ if (socket[kError]) {
+ reject(socket[kError])
+ } else {
+ callback = resolve
+ }
+ })
+
+ if (client[kHTTPConnVersion] === 'h2') {
+ h2stream
+ .on('close', onDrain)
+ .on('drain', onDrain)
+
+ try {
+ // It's up to the user to somehow abort the async iterable.
+ for await (const chunk of body) {
+ if (socket[kError]) {
+ throw socket[kError]
+ }
+
+ const res = h2stream.write(chunk)
+ request.onBodySent(chunk)
+ if (!res) {
+ await waitForDrain()
+ }
+ }
+ } catch (err) {
+ h2stream.destroy(err)
+ } finally {
+ request.onRequestSent()
+ h2stream.end()
+ h2stream
+ .off('close', onDrain)
+ .off('drain', onDrain)
+ }
+
+ return
+ }
+
+ socket
+ .on('close', onDrain)
+ .on('drain', onDrain)
+
+ const writer = new AsyncWriter({ socket, request, contentLength, client, expectsPayload, header })
+ try {
+ // It's up to the user to somehow abort the async iterable.
+ for await (const chunk of body) {
+ if (socket[kError]) {
+ throw socket[kError]
+ }
+
+ if (!writer.write(chunk)) {
+ await waitForDrain()
+ }
+ }
+
+ writer.end()
+ } catch (err) {
+ writer.destroy(err)
+ } finally {
+ socket
+ .off('close', onDrain)
+ .off('drain', onDrain)
+ }
+}
+
+class AsyncWriter {
+ constructor ({ socket, request, contentLength, client, expectsPayload, header }) {
+ this.socket = socket
+ this.request = request
+ this.contentLength = contentLength
+ this.client = client
+ this.bytesWritten = 0
+ this.expectsPayload = expectsPayload
+ this.header = header
+
+ socket[kWriting] = true
+ }
+
+ write (chunk) {
+ const { socket, request, contentLength, client, bytesWritten, expectsPayload, header } = this
+
+ if (socket[kError]) {
+ throw socket[kError]
+ }
+
+ if (socket.destroyed) {
+ return false
+ }
+
+ const len = Buffer.byteLength(chunk)
+ if (!len) {
+ return true
+ }
+
+ // We should defer writing chunks.
+ if (contentLength !== null && bytesWritten + len > contentLength) {
+ if (client[kStrictContentLength]) {
+ throw new RequestContentLengthMismatchError()
+ }
+
+ process.emitWarning(new RequestContentLengthMismatchError())
+ }
+
+ socket.cork()
+
+ if (bytesWritten === 0) {
+ if (!expectsPayload) {
+ socket[kReset] = true
+ }
+
+ if (contentLength === null) {
+ socket.write(`${header}transfer-encoding: chunked\r\n`, 'latin1')
+ } else {
+ socket.write(`${header}content-length: ${contentLength}\r\n\r\n`, 'latin1')
+ }
+ }
+
+ if (contentLength === null) {
+ socket.write(`\r\n${len.toString(16)}\r\n`, 'latin1')
+ }
+
+ this.bytesWritten += len
+
+ const ret = socket.write(chunk)
+
+ socket.uncork()
+
+ request.onBodySent(chunk)
+
+ if (!ret) {
+ if (socket[kParser].timeout && socket[kParser].timeoutType === TIMEOUT_HEADERS) {
+ // istanbul ignore else: only for jest
+ if (socket[kParser].timeout.refresh) {
+ socket[kParser].timeout.refresh()
+ }
+ }
+ }
+
+ return ret
+ }
+
+ end () {
+ const { socket, contentLength, client, bytesWritten, expectsPayload, header, request } = this
+ request.onRequestSent()
+
+ socket[kWriting] = false
+
+ if (socket[kError]) {
+ throw socket[kError]
+ }
+
+ if (socket.destroyed) {
+ return
+ }
+
+ if (bytesWritten === 0) {
+ if (expectsPayload) {
+ // https://tools.ietf.org/html/rfc7230#section-3.3.2
+ // A user agent SHOULD send a Content-Length in a request message when
+ // no Transfer-Encoding is sent and the request method defines a meaning
+ // for an enclosed payload body.
+
+ socket.write(`${header}content-length: 0\r\n\r\n`, 'latin1')
+ } else {
+ socket.write(`${header}\r\n`, 'latin1')
+ }
+ } else if (contentLength === null) {
+ socket.write('\r\n0\r\n\r\n', 'latin1')
+ }
+
+ if (contentLength !== null && bytesWritten !== contentLength) {
+ if (client[kStrictContentLength]) {
+ throw new RequestContentLengthMismatchError()
+ } else {
+ process.emitWarning(new RequestContentLengthMismatchError())
+ }
+ }
+
+ if (socket[kParser].timeout && socket[kParser].timeoutType === TIMEOUT_HEADERS) {
+ // istanbul ignore else: only for jest
+ if (socket[kParser].timeout.refresh) {
+ socket[kParser].timeout.refresh()
+ }
+ }
+
+ resume(client)
+ }
+
+ destroy (err) {
+ const { socket, client } = this
+
+ socket[kWriting] = false
+
+ if (err) {
+ assert(client[kRunning] <= 1, 'pipeline should only contain this request')
+ util.destroy(socket, err)
+ }
+ }
+}
+
+function errorRequest (client, request, err) {
+ try {
+ request.onError(err)
+ assert(request.aborted)
+ } catch (err) {
+ client.emit('error', err)
+ }
+}
+
+module.exports = Client
diff --git a/lib/compat/dispatcher-weakref.js b/lib/compat/dispatcher-weakref.js
new file mode 100644
index 0000000..8cb99e2
--- /dev/null
+++ b/lib/compat/dispatcher-weakref.js
@@ -0,0 +1,48 @@
+'use strict'
+
+/* istanbul ignore file: only for Node 12 */
+
+const { kConnected, kSize } = require('../core/symbols')
+
+class CompatWeakRef {
+ constructor (value) {
+ this.value = value
+ }
+
+ deref () {
+ return this.value[kConnected] === 0 && this.value[kSize] === 0
+ ? undefined
+ : this.value
+ }
+}
+
+class CompatFinalizer {
+ constructor (finalizer) {
+ this.finalizer = finalizer
+ }
+
+ register (dispatcher, key) {
+ if (dispatcher.on) {
+ dispatcher.on('disconnect', () => {
+ if (dispatcher[kConnected] === 0 && dispatcher[kSize] === 0) {
+ this.finalizer(key)
+ }
+ })
+ }
+ }
+}
+
+module.exports = function () {
+ // FIXME: remove workaround when the Node bug is fixed
+ // https://github.com/nodejs/node/issues/49344#issuecomment-1741776308
+ if (process.env.NODE_V8_COVERAGE) {
+ return {
+ WeakRef: CompatWeakRef,
+ FinalizationRegistry: CompatFinalizer
+ }
+ }
+ return {
+ WeakRef: global.WeakRef || CompatWeakRef,
+ FinalizationRegistry: global.FinalizationRegistry || CompatFinalizer
+ }
+}
diff --git a/lib/cookies/constants.js b/lib/cookies/constants.js
new file mode 100644
index 0000000..85f1fec
--- /dev/null
+++ b/lib/cookies/constants.js
@@ -0,0 +1,12 @@
+'use strict'
+
+// https://wicg.github.io/cookie-store/#cookie-maximum-attribute-value-size
+const maxAttributeValueSize = 1024
+
+// https://wicg.github.io/cookie-store/#cookie-maximum-name-value-pair-size
+const maxNameValuePairSize = 4096
+
+module.exports = {
+ maxAttributeValueSize,
+ maxNameValuePairSize
+}
diff --git a/lib/cookies/index.js b/lib/cookies/index.js
new file mode 100644
index 0000000..c9c1f28
--- /dev/null
+++ b/lib/cookies/index.js
@@ -0,0 +1,184 @@
+'use strict'
+
+const { parseSetCookie } = require('./parse')
+const { stringify, getHeadersList } = require('./util')
+const { webidl } = require('../fetch/webidl')
+const { Headers } = require('../fetch/headers')
+
+/**
+ * @typedef {Object} Cookie
+ * @property {string} name
+ * @property {string} value
+ * @property {Date|number|undefined} expires
+ * @property {number|undefined} maxAge
+ * @property {string|undefined} domain
+ * @property {string|undefined} path
+ * @property {boolean|undefined} secure
+ * @property {boolean|undefined} httpOnly
+ * @property {'Strict'|'Lax'|'None'} sameSite
+ * @property {string[]} unparsed
+ */
+
+/**
+ * @param {Headers} headers
+ * @returns {Record<string, string>}
+ */
+function getCookies (headers) {
+ webidl.argumentLengthCheck(arguments, 1, { header: 'getCookies' })
+
+ webidl.brandCheck(headers, Headers, { strict: false })
+
+ const cookie = headers.get('cookie')
+ const out = {}
+
+ if (!cookie) {
+ return out
+ }
+
+ for (const piece of cookie.split(';')) {
+ const [name, ...value] = piece.split('=')
+
+ out[name.trim()] = value.join('=')
+ }
+
+ return out
+}
+
+/**
+ * @param {Headers} headers
+ * @param {string} name
+ * @param {{ path?: string, domain?: string }|undefined} attributes
+ * @returns {void}
+ */
+function deleteCookie (headers, name, attributes) {
+ webidl.argumentLengthCheck(arguments, 2, { header: 'deleteCookie' })
+
+ webidl.brandCheck(headers, Headers, { strict: false })
+
+ name = webidl.converters.DOMString(name)
+ attributes = webidl.converters.DeleteCookieAttributes(attributes)
+
+ // Matches behavior of
+ // https://github.com/denoland/deno_std/blob/63827b16330b82489a04614027c33b7904e08be5/http/cookie.ts#L278
+ setCookie(headers, {
+ name,
+ value: '',
+ expires: new Date(0),
+ ...attributes
+ })
+}
+
+/**
+ * @param {Headers} headers
+ * @returns {Cookie[]}
+ */
+function getSetCookies (headers) {
+ webidl.argumentLengthCheck(arguments, 1, { header: 'getSetCookies' })
+
+ webidl.brandCheck(headers, Headers, { strict: false })
+
+ const cookies = getHeadersList(headers).cookies
+
+ if (!cookies) {
+ return []
+ }
+
+ // In older versions of undici, cookies is a list of name:value.
+ return cookies.map((pair) => parseSetCookie(Array.isArray(pair) ? pair[1] : pair))
+}
+
+/**
+ * @param {Headers} headers
+ * @param {Cookie} cookie
+ * @returns {void}
+ */
+function setCookie (headers, cookie) {
+ webidl.argumentLengthCheck(arguments, 2, { header: 'setCookie' })
+
+ webidl.brandCheck(headers, Headers, { strict: false })
+
+ cookie = webidl.converters.Cookie(cookie)
+
+ const str = stringify(cookie)
+
+ if (str) {
+ headers.append('Set-Cookie', stringify(cookie))
+ }
+}
+
+webidl.converters.DeleteCookieAttributes = webidl.dictionaryConverter([
+ {
+ converter: webidl.nullableConverter(webidl.converters.DOMString),
+ key: 'path',
+ defaultValue: null
+ },
+ {
+ converter: webidl.nullableConverter(webidl.converters.DOMString),
+ key: 'domain',
+ defaultValue: null
+ }
+])
+
+webidl.converters.Cookie = webidl.dictionaryConverter([
+ {
+ converter: webidl.converters.DOMString,
+ key: 'name'
+ },
+ {
+ converter: webidl.converters.DOMString,
+ key: 'value'
+ },
+ {
+ converter: webidl.nullableConverter((value) => {
+ if (typeof value === 'number') {
+ return webidl.converters['unsigned long long'](value)
+ }
+
+ return new Date(value)
+ }),
+ key: 'expires',
+ defaultValue: null
+ },
+ {
+ converter: webidl.nullableConverter(webidl.converters['long long']),
+ key: 'maxAge',
+ defaultValue: null
+ },
+ {
+ converter: webidl.nullableConverter(webidl.converters.DOMString),
+ key: 'domain',
+ defaultValue: null
+ },
+ {
+ converter: webidl.nullableConverter(webidl.converters.DOMString),
+ key: 'path',
+ defaultValue: null
+ },
+ {
+ converter: webidl.nullableConverter(webidl.converters.boolean),
+ key: 'secure',
+ defaultValue: null
+ },
+ {
+ converter: webidl.nullableConverter(webidl.converters.boolean),
+ key: 'httpOnly',
+ defaultValue: null
+ },
+ {
+ converter: webidl.converters.USVString,
+ key: 'sameSite',
+ allowedValues: ['Strict', 'Lax', 'None']
+ },
+ {
+ converter: webidl.sequenceConverter(webidl.converters.DOMString),
+ key: 'unparsed',
+ defaultValue: []
+ }
+])
+
+module.exports = {
+ getCookies,
+ deleteCookie,
+ getSetCookies,
+ setCookie
+}
diff --git a/lib/cookies/parse.js b/lib/cookies/parse.js
new file mode 100644
index 0000000..aae2750
--- /dev/null
+++ b/lib/cookies/parse.js
@@ -0,0 +1,317 @@
+'use strict'
+
+const { maxNameValuePairSize, maxAttributeValueSize } = require('./constants')
+const { isCTLExcludingHtab } = require('./util')
+const { collectASequenceOfCodePointsFast } = require('../fetch/dataURL')
+const assert = require('assert')
+
+/**
+ * @description Parses the field-value attributes of a set-cookie header string.
+ * @see https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4
+ * @param {string} header
+ * @returns if the header is invalid, null will be returned
+ */
+function parseSetCookie (header) {
+ // 1. If the set-cookie-string contains a %x00-08 / %x0A-1F / %x7F
+ // character (CTL characters excluding HTAB): Abort these steps and
+ // ignore the set-cookie-string entirely.
+ if (isCTLExcludingHtab(header)) {
+ return null
+ }
+
+ let nameValuePair = ''
+ let unparsedAttributes = ''
+ let name = ''
+ let value = ''
+
+ // 2. If the set-cookie-string contains a %x3B (";") character:
+ if (header.includes(';')) {
+ // 1. The name-value-pair string consists of the characters up to,
+ // but not including, the first %x3B (";"), and the unparsed-
+ // attributes consist of the remainder of the set-cookie-string
+ // (including the %x3B (";") in question).
+ const position = { position: 0 }
+
+ nameValuePair = collectASequenceOfCodePointsFast(';', header, position)
+ unparsedAttributes = header.slice(position.position)
+ } else {
+ // Otherwise:
+
+ // 1. The name-value-pair string consists of all the characters
+ // contained in the set-cookie-string, and the unparsed-
+ // attributes is the empty string.
+ nameValuePair = header
+ }
+
+ // 3. If the name-value-pair string lacks a %x3D ("=") character, then
+ // the name string is empty, and the value string is the value of
+ // name-value-pair.
+ if (!nameValuePair.includes('=')) {
+ value = nameValuePair
+ } else {
+ // Otherwise, the name string consists of the characters up to, but
+ // not including, the first %x3D ("=") character, and the (possibly
+ // empty) value string consists of the characters after the first
+ // %x3D ("=") character.
+ const position = { position: 0 }
+ name = collectASequenceOfCodePointsFast(
+ '=',
+ nameValuePair,
+ position
+ )
+ value = nameValuePair.slice(position.position + 1)
+ }
+
+ // 4. Remove any leading or trailing WSP characters from the name
+ // string and the value string.
+ name = name.trim()
+ value = value.trim()
+
+ // 5. If the sum of the lengths of the name string and the value string
+ // is more than 4096 octets, abort these steps and ignore the set-
+ // cookie-string entirely.
+ if (name.length + value.length > maxNameValuePairSize) {
+ return null
+ }
+
+ // 6. The cookie-name is the name string, and the cookie-value is the
+ // value string.
+ return {
+ name, value, ...parseUnparsedAttributes(unparsedAttributes)
+ }
+}
+
+/**
+ * Parses the remaining attributes of a set-cookie header
+ * @see https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4
+ * @param {string} unparsedAttributes
+ * @param {[Object.<string, unknown>]={}} cookieAttributeList
+ */
+function parseUnparsedAttributes (unparsedAttributes, cookieAttributeList = {}) {
+ // 1. If the unparsed-attributes string is empty, skip the rest of
+ // these steps.
+ if (unparsedAttributes.length === 0) {
+ return cookieAttributeList
+ }
+
+ // 2. Discard the first character of the unparsed-attributes (which
+ // will be a %x3B (";") character).
+ assert(unparsedAttributes[0] === ';')
+ unparsedAttributes = unparsedAttributes.slice(1)
+
+ let cookieAv = ''
+
+ // 3. If the remaining unparsed-attributes contains a %x3B (";")
+ // character:
+ if (unparsedAttributes.includes(';')) {
+ // 1. Consume the characters of the unparsed-attributes up to, but
+ // not including, the first %x3B (";") character.
+ cookieAv = collectASequenceOfCodePointsFast(
+ ';',
+ unparsedAttributes,
+ { position: 0 }
+ )
+ unparsedAttributes = unparsedAttributes.slice(cookieAv.length)
+ } else {
+ // Otherwise:
+
+ // 1. Consume the remainder of the unparsed-attributes.
+ cookieAv = unparsedAttributes
+ unparsedAttributes = ''
+ }
+
+ // Let the cookie-av string be the characters consumed in this step.
+
+ let attributeName = ''
+ let attributeValue = ''
+
+ // 4. If the cookie-av string contains a %x3D ("=") character:
+ if (cookieAv.includes('=')) {
+ // 1. The (possibly empty) attribute-name string consists of the
+ // characters up to, but not including, the first %x3D ("=")
+ // character, and the (possibly empty) attribute-value string
+ // consists of the characters after the first %x3D ("=")
+ // character.
+ const position = { position: 0 }
+
+ attributeName = collectASequenceOfCodePointsFast(
+ '=',
+ cookieAv,
+ position
+ )
+ attributeValue = cookieAv.slice(position.position + 1)
+ } else {
+ // Otherwise:
+
+ // 1. The attribute-name string consists of the entire cookie-av
+ // string, and the attribute-value string is empty.
+ attributeName = cookieAv
+ }
+
+ // 5. Remove any leading or trailing WSP characters from the attribute-
+ // name string and the attribute-value string.
+ attributeName = attributeName.trim()
+ attributeValue = attributeValue.trim()
+
+ // 6. If the attribute-value is longer than 1024 octets, ignore the
+ // cookie-av string and return to Step 1 of this algorithm.
+ if (attributeValue.length > maxAttributeValueSize) {
+ return parseUnparsedAttributes(unparsedAttributes, cookieAttributeList)
+ }
+
+ // 7. Process the attribute-name and attribute-value according to the
+ // requirements in the following subsections. (Notice that
+ // attributes with unrecognized attribute-names are ignored.)
+ const attributeNameLowercase = attributeName.toLowerCase()
+
+ // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4.1
+ // If the attribute-name case-insensitively matches the string
+ // "Expires", the user agent MUST process the cookie-av as follows.
+ if (attributeNameLowercase === 'expires') {
+ // 1. Let the expiry-time be the result of parsing the attribute-value
+ // as cookie-date (see Section 5.1.1).
+ const expiryTime = new Date(attributeValue)
+
+ // 2. If the attribute-value failed to parse as a cookie date, ignore
+ // the cookie-av.
+
+ cookieAttributeList.expires = expiryTime
+ } else if (attributeNameLowercase === 'max-age') {
+ // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4.2
+ // If the attribute-name case-insensitively matches the string "Max-
+ // Age", the user agent MUST process the cookie-av as follows.
+
+ // 1. If the first character of the attribute-value is not a DIGIT or a
+ // "-" character, ignore the cookie-av.
+ const charCode = attributeValue.charCodeAt(0)
+
+ if ((charCode < 48 || charCode > 57) && attributeValue[0] !== '-') {
+ return parseUnparsedAttributes(unparsedAttributes, cookieAttributeList)
+ }
+
+ // 2. If the remainder of attribute-value contains a non-DIGIT
+ // character, ignore the cookie-av.
+ if (!/^\d+$/.test(attributeValue)) {
+ return parseUnparsedAttributes(unparsedAttributes, cookieAttributeList)
+ }
+
+ // 3. Let delta-seconds be the attribute-value converted to an integer.
+ const deltaSeconds = Number(attributeValue)
+
+ // 4. Let cookie-age-limit be the maximum age of the cookie (which
+ // SHOULD be 400 days or less, see Section 4.1.2.2).
+
+ // 5. Set delta-seconds to the smaller of its present value and cookie-
+ // age-limit.
+ // deltaSeconds = Math.min(deltaSeconds * 1000, maxExpiresMs)
+
+ // 6. If delta-seconds is less than or equal to zero (0), let expiry-
+ // time be the earliest representable date and time. Otherwise, let
+ // the expiry-time be the current date and time plus delta-seconds
+ // seconds.
+ // const expiryTime = deltaSeconds <= 0 ? Date.now() : Date.now() + deltaSeconds
+
+ // 7. Append an attribute to the cookie-attribute-list with an
+ // attribute-name of Max-Age and an attribute-value of expiry-time.
+ cookieAttributeList.maxAge = deltaSeconds
+ } else if (attributeNameLowercase === 'domain') {
+ // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4.3
+ // If the attribute-name case-insensitively matches the string "Domain",
+ // the user agent MUST process the cookie-av as follows.
+
+ // 1. Let cookie-domain be the attribute-value.
+ let cookieDomain = attributeValue
+
+ // 2. If cookie-domain starts with %x2E ("."), let cookie-domain be
+ // cookie-domain without its leading %x2E (".").
+ if (cookieDomain[0] === '.') {
+ cookieDomain = cookieDomain.slice(1)
+ }
+
+ // 3. Convert the cookie-domain to lower case.
+ cookieDomain = cookieDomain.toLowerCase()
+
+ // 4. Append an attribute to the cookie-attribute-list with an
+ // attribute-name of Domain and an attribute-value of cookie-domain.
+ cookieAttributeList.domain = cookieDomain
+ } else if (attributeNameLowercase === 'path') {
+ // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4.4
+ // If the attribute-name case-insensitively matches the string "Path",
+ // the user agent MUST process the cookie-av as follows.
+
+ // 1. If the attribute-value is empty or if the first character of the
+ // attribute-value is not %x2F ("/"):
+ let cookiePath = ''
+ if (attributeValue.length === 0 || attributeValue[0] !== '/') {
+ // 1. Let cookie-path be the default-path.
+ cookiePath = '/'
+ } else {
+ // Otherwise:
+
+ // 1. Let cookie-path be the attribute-value.
+ cookiePath = attributeValue
+ }
+
+ // 2. Append an attribute to the cookie-attribute-list with an
+ // attribute-name of Path and an attribute-value of cookie-path.
+ cookieAttributeList.path = cookiePath
+ } else if (attributeNameLowercase === 'secure') {
+ // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4.5
+ // If the attribute-name case-insensitively matches the string "Secure",
+ // the user agent MUST append an attribute to the cookie-attribute-list
+ // with an attribute-name of Secure and an empty attribute-value.
+
+ cookieAttributeList.secure = true
+ } else if (attributeNameLowercase === 'httponly') {
+ // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4.6
+ // If the attribute-name case-insensitively matches the string
+ // "HttpOnly", the user agent MUST append an attribute to the cookie-
+ // attribute-list with an attribute-name of HttpOnly and an empty
+ // attribute-value.
+
+ cookieAttributeList.httpOnly = true
+ } else if (attributeNameLowercase === 'samesite') {
+ // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4.7
+ // If the attribute-name case-insensitively matches the string
+ // "SameSite", the user agent MUST process the cookie-av as follows:
+
+ // 1. Let enforcement be "Default".
+ let enforcement = 'Default'
+
+ const attributeValueLowercase = attributeValue.toLowerCase()
+ // 2. If cookie-av's attribute-value is a case-insensitive match for
+ // "None", set enforcement to "None".
+ if (attributeValueLowercase.includes('none')) {
+ enforcement = 'None'
+ }
+
+ // 3. If cookie-av's attribute-value is a case-insensitive match for
+ // "Strict", set enforcement to "Strict".
+ if (attributeValueLowercase.includes('strict')) {
+ enforcement = 'Strict'
+ }
+
+ // 4. If cookie-av's attribute-value is a case-insensitive match for
+ // "Lax", set enforcement to "Lax".
+ if (attributeValueLowercase.includes('lax')) {
+ enforcement = 'Lax'
+ }
+
+ // 5. Append an attribute to the cookie-attribute-list with an
+ // attribute-name of "SameSite" and an attribute-value of
+ // enforcement.
+ cookieAttributeList.sameSite = enforcement
+ } else {
+ cookieAttributeList.unparsed ??= []
+
+ cookieAttributeList.unparsed.push(`${attributeName}=${attributeValue}`)
+ }
+
+ // 8. Return to Step 1 of this algorithm.
+ return parseUnparsedAttributes(unparsedAttributes, cookieAttributeList)
+}
+
+module.exports = {
+ parseSetCookie,
+ parseUnparsedAttributes
+}
diff --git a/lib/cookies/util.js b/lib/cookies/util.js
new file mode 100644
index 0000000..2290329
--- /dev/null
+++ b/lib/cookies/util.js
@@ -0,0 +1,291 @@
+'use strict'
+
+const assert = require('assert')
+const { kHeadersList } = require('../core/symbols')
+
+function isCTLExcludingHtab (value) {
+ if (value.length === 0) {
+ return false
+ }
+
+ for (const char of value) {
+ const code = char.charCodeAt(0)
+
+ if (
+ (code >= 0x00 || code <= 0x08) ||
+ (code >= 0x0A || code <= 0x1F) ||
+ code === 0x7F
+ ) {
+ return false
+ }
+ }
+}
+
+/**
+ CHAR = <any US-ASCII character (octets 0 - 127)>
+ token = 1*<any CHAR except CTLs or separators>
+ separators = "(" | ")" | "<" | ">" | "@"
+ | "," | ";" | ":" | "\" | <">
+ | "/" | "[" | "]" | "?" | "="
+ | "{" | "}" | SP | HT
+ * @param {string} name
+ */
+function validateCookieName (name) {
+ for (const char of name) {
+ const code = char.charCodeAt(0)
+
+ if (
+ (code <= 0x20 || code > 0x7F) ||
+ char === '(' ||
+ char === ')' ||
+ char === '>' ||
+ char === '<' ||
+ char === '@' ||
+ char === ',' ||
+ char === ';' ||
+ char === ':' ||
+ char === '\\' ||
+ char === '"' ||
+ char === '/' ||
+ char === '[' ||
+ char === ']' ||
+ char === '?' ||
+ char === '=' ||
+ char === '{' ||
+ char === '}'
+ ) {
+ throw new Error('Invalid cookie name')
+ }
+ }
+}
+
+/**
+ cookie-value = *cookie-octet / ( DQUOTE *cookie-octet DQUOTE )
+ cookie-octet = %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E
+ ; US-ASCII characters excluding CTLs,
+ ; whitespace DQUOTE, comma, semicolon,
+ ; and backslash
+ * @param {string} value
+ */
+function validateCookieValue (value) {
+ for (const char of value) {
+ const code = char.charCodeAt(0)
+
+ if (
+ code < 0x21 || // exclude CTLs (0-31)
+ code === 0x22 ||
+ code === 0x2C ||
+ code === 0x3B ||
+ code === 0x5C ||
+ code > 0x7E // non-ascii
+ ) {
+ throw new Error('Invalid header value')
+ }
+ }
+}
+
+/**
+ * path-value = <any CHAR except CTLs or ";">
+ * @param {string} path
+ */
+function validateCookiePath (path) {
+ for (const char of path) {
+ const code = char.charCodeAt(0)
+
+ if (code < 0x21 || char === ';') {
+ throw new Error('Invalid cookie path')
+ }
+ }
+}
+
+/**
+ * I have no idea why these values aren't allowed to be honest,
+ * but Deno tests these. - Khafra
+ * @param {string} domain
+ */
+function validateCookieDomain (domain) {
+ if (
+ domain.startsWith('-') ||
+ domain.endsWith('.') ||
+ domain.endsWith('-')
+ ) {
+ throw new Error('Invalid cookie domain')
+ }
+}
+
+/**
+ * @see https://www.rfc-editor.org/rfc/rfc7231#section-7.1.1.1
+ * @param {number|Date} date
+ IMF-fixdate = day-name "," SP date1 SP time-of-day SP GMT
+ ; fixed length/zone/capitalization subset of the format
+ ; see Section 3.3 of [RFC5322]
+
+ day-name = %x4D.6F.6E ; "Mon", case-sensitive
+ / %x54.75.65 ; "Tue", case-sensitive
+ / %x57.65.64 ; "Wed", case-sensitive
+ / %x54.68.75 ; "Thu", case-sensitive
+ / %x46.72.69 ; "Fri", case-sensitive
+ / %x53.61.74 ; "Sat", case-sensitive
+ / %x53.75.6E ; "Sun", case-sensitive
+ date1 = day SP month SP year
+ ; e.g., 02 Jun 1982
+
+ day = 2DIGIT
+ month = %x4A.61.6E ; "Jan", case-sensitive
+ / %x46.65.62 ; "Feb", case-sensitive
+ / %x4D.61.72 ; "Mar", case-sensitive
+ / %x41.70.72 ; "Apr", case-sensitive
+ / %x4D.61.79 ; "May", case-sensitive
+ / %x4A.75.6E ; "Jun", case-sensitive
+ / %x4A.75.6C ; "Jul", case-sensitive
+ / %x41.75.67 ; "Aug", case-sensitive
+ / %x53.65.70 ; "Sep", case-sensitive
+ / %x4F.63.74 ; "Oct", case-sensitive
+ / %x4E.6F.76 ; "Nov", case-sensitive
+ / %x44.65.63 ; "Dec", case-sensitive
+ year = 4DIGIT
+
+ GMT = %x47.4D.54 ; "GMT", case-sensitive
+
+ time-of-day = hour ":" minute ":" second
+ ; 00:00:00 - 23:59:60 (leap second)
+
+ hour = 2DIGIT
+ minute = 2DIGIT
+ second = 2DIGIT
+ */
+function toIMFDate (date) {
+ if (typeof date === 'number') {
+ date = new Date(date)
+ }
+
+ const days = [
+ 'Sun', 'Mon', 'Tue', 'Wed',
+ 'Thu', 'Fri', 'Sat'
+ ]
+
+ const months = [
+ 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
+ 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'
+ ]
+
+ const dayName = days[date.getUTCDay()]
+ const day = date.getUTCDate().toString().padStart(2, '0')
+ const month = months[date.getUTCMonth()]
+ const year = date.getUTCFullYear()
+ const hour = date.getUTCHours().toString().padStart(2, '0')
+ const minute = date.getUTCMinutes().toString().padStart(2, '0')
+ const second = date.getUTCSeconds().toString().padStart(2, '0')
+
+ return `${dayName}, ${day} ${month} ${year} ${hour}:${minute}:${second} GMT`
+}
+
+/**
+ max-age-av = "Max-Age=" non-zero-digit *DIGIT
+ ; In practice, both expires-av and max-age-av
+ ; are limited to dates representable by the
+ ; user agent.
+ * @param {number} maxAge
+ */
+function validateCookieMaxAge (maxAge) {
+ if (maxAge < 0) {
+ throw new Error('Invalid cookie max-age')
+ }
+}
+
+/**
+ * @see https://www.rfc-editor.org/rfc/rfc6265#section-4.1.1
+ * @param {import('./index').Cookie} cookie
+ */
+function stringify (cookie) {
+ if (cookie.name.length === 0) {
+ return null
+ }
+
+ validateCookieName(cookie.name)
+ validateCookieValue(cookie.value)
+
+ const out = [`${cookie.name}=${cookie.value}`]
+
+ // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-cookie-prefixes-00#section-3.1
+ // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-cookie-prefixes-00#section-3.2
+ if (cookie.name.startsWith('__Secure-')) {
+ cookie.secure = true
+ }
+
+ if (cookie.name.startsWith('__Host-')) {
+ cookie.secure = true
+ cookie.domain = null
+ cookie.path = '/'
+ }
+
+ if (cookie.secure) {
+ out.push('Secure')
+ }
+
+ if (cookie.httpOnly) {
+ out.push('HttpOnly')
+ }
+
+ if (typeof cookie.maxAge === 'number') {
+ validateCookieMaxAge(cookie.maxAge)
+ out.push(`Max-Age=${cookie.maxAge}`)
+ }
+
+ if (cookie.domain) {
+ validateCookieDomain(cookie.domain)
+ out.push(`Domain=${cookie.domain}`)
+ }
+
+ if (cookie.path) {
+ validateCookiePath(cookie.path)
+ out.push(`Path=${cookie.path}`)
+ }
+
+ if (cookie.expires && cookie.expires.toString() !== 'Invalid Date') {
+ out.push(`Expires=${toIMFDate(cookie.expires)}`)
+ }
+
+ if (cookie.sameSite) {
+ out.push(`SameSite=${cookie.sameSite}`)
+ }
+
+ for (const part of cookie.unparsed) {
+ if (!part.includes('=')) {
+ throw new Error('Invalid unparsed')
+ }
+
+ const [key, ...value] = part.split('=')
+
+ out.push(`${key.trim()}=${value.join('=')}`)
+ }
+
+ return out.join('; ')
+}
+
+let kHeadersListNode
+
+function getHeadersList (headers) {
+ if (headers[kHeadersList]) {
+ return headers[kHeadersList]
+ }
+
+ if (!kHeadersListNode) {
+ kHeadersListNode = Object.getOwnPropertySymbols(headers).find(
+ (symbol) => symbol.description === 'headers list'
+ )
+
+ assert(kHeadersListNode, 'Headers cannot be parsed')
+ }
+
+ const headersList = headers[kHeadersListNode]
+ assert(headersList)
+
+ return headersList
+}
+
+module.exports = {
+ isCTLExcludingHtab,
+ stringify,
+ getHeadersList
+}
diff --git a/lib/core/connect.js b/lib/core/connect.js
new file mode 100644
index 0000000..3309117
--- /dev/null
+++ b/lib/core/connect.js
@@ -0,0 +1,189 @@
+'use strict'
+
+const net = require('net')
+const assert = require('assert')
+const util = require('./util')
+const { InvalidArgumentError, ConnectTimeoutError } = require('./errors')
+
+let tls // include tls conditionally since it is not always available
+
+// TODO: session re-use does not wait for the first
+// connection to resolve the session and might therefore
+// resolve the same servername multiple times even when
+// re-use is enabled.
+
+let SessionCache
+// FIXME: remove workaround when the Node bug is fixed
+// https://github.com/nodejs/node/issues/49344#issuecomment-1741776308
+if (global.FinalizationRegistry && !process.env.NODE_V8_COVERAGE) {
+ SessionCache = class WeakSessionCache {
+ constructor (maxCachedSessions) {
+ this._maxCachedSessions = maxCachedSessions
+ this._sessionCache = new Map()
+ this._sessionRegistry = new global.FinalizationRegistry((key) => {
+ if (this._sessionCache.size < this._maxCachedSessions) {
+ return
+ }
+
+ const ref = this._sessionCache.get(key)
+ if (ref !== undefined && ref.deref() === undefined) {
+ this._sessionCache.delete(key)
+ }
+ })
+ }
+
+ get (sessionKey) {
+ const ref = this._sessionCache.get(sessionKey)
+ return ref ? ref.deref() : null
+ }
+
+ set (sessionKey, session) {
+ if (this._maxCachedSessions === 0) {
+ return
+ }
+
+ this._sessionCache.set(sessionKey, new WeakRef(session))
+ this._sessionRegistry.register(session, sessionKey)
+ }
+ }
+} else {
+ SessionCache = class SimpleSessionCache {
+ constructor (maxCachedSessions) {
+ this._maxCachedSessions = maxCachedSessions
+ this._sessionCache = new Map()
+ }
+
+ get (sessionKey) {
+ return this._sessionCache.get(sessionKey)
+ }
+
+ set (sessionKey, session) {
+ if (this._maxCachedSessions === 0) {
+ return
+ }
+
+ if (this._sessionCache.size >= this._maxCachedSessions) {
+ // remove the oldest session
+ const { value: oldestKey } = this._sessionCache.keys().next()
+ this._sessionCache.delete(oldestKey)
+ }
+
+ this._sessionCache.set(sessionKey, session)
+ }
+ }
+}
+
+function buildConnector ({ allowH2, maxCachedSessions, socketPath, timeout, ...opts }) {
+ if (maxCachedSessions != null && (!Number.isInteger(maxCachedSessions) || maxCachedSessions < 0)) {
+ throw new InvalidArgumentError('maxCachedSessions must be a positive integer or zero')
+ }
+
+ const options = { path: socketPath, ...opts }
+ const sessionCache = new SessionCache(maxCachedSessions == null ? 100 : maxCachedSessions)
+ timeout = timeout == null ? 10e3 : timeout
+ allowH2 = allowH2 != null ? allowH2 : false
+ return function connect ({ hostname, host, protocol, port, servername, localAddress, httpSocket }, callback) {
+ let socket
+ if (protocol === 'https:') {
+ if (!tls) {
+ tls = require('tls')
+ }
+ servername = servername || options.servername || util.getServerName(host) || null
+
+ const sessionKey = servername || hostname
+ const session = sessionCache.get(sessionKey) || null
+
+ assert(sessionKey)
+
+ socket = tls.connect({
+ highWaterMark: 16384, // TLS in node can't have bigger HWM anyway...
+ ...options,
+ servername,
+ session,
+ localAddress,
+ // TODO(HTTP/2): Add support for h2c
+ ALPNProtocols: allowH2 ? ['http/1.1', 'h2'] : ['http/1.1'],
+ socket: httpSocket, // upgrade socket connection
+ port: port || 443,
+ host: hostname
+ })
+
+ socket
+ .on('session', function (session) {
+ // TODO (fix): Can a session become invalid once established? Don't think so?
+ sessionCache.set(sessionKey, session)
+ })
+ } else {
+ assert(!httpSocket, 'httpSocket can only be sent on TLS update')
+ socket = net.connect({
+ highWaterMark: 64 * 1024, // Same as nodejs fs streams.
+ ...options,
+ localAddress,
+ port: port || 80,
+ host: hostname
+ })
+ }
+
+ // Set TCP keep alive options on the socket here instead of in connect() for the case of assigning the socket
+ if (options.keepAlive == null || options.keepAlive) {
+ const keepAliveInitialDelay = options.keepAliveInitialDelay === undefined ? 60e3 : options.keepAliveInitialDelay
+ socket.setKeepAlive(true, keepAliveInitialDelay)
+ }
+
+ const cancelTimeout = setupTimeout(() => onConnectTimeout(socket), timeout)
+
+ socket
+ .setNoDelay(true)
+ .once(protocol === 'https:' ? 'secureConnect' : 'connect', function () {
+ cancelTimeout()
+
+ if (callback) {
+ const cb = callback
+ callback = null
+ cb(null, this)
+ }
+ })
+ .on('error', function (err) {
+ cancelTimeout()
+
+ if (callback) {
+ const cb = callback
+ callback = null
+ cb(err)
+ }
+ })
+
+ return socket
+ }
+}
+
+function setupTimeout (onConnectTimeout, timeout) {
+ if (!timeout) {
+ return () => {}
+ }
+
+ let s1 = null
+ let s2 = null
+ const timeoutId = setTimeout(() => {
+ // setImmediate is added to make sure that we priotorise socket error events over timeouts
+ s1 = setImmediate(() => {
+ if (process.platform === 'win32') {
+ // Windows needs an extra setImmediate probably due to implementation differences in the socket logic
+ s2 = setImmediate(() => onConnectTimeout())
+ } else {
+ onConnectTimeout()
+ }
+ })
+ }, timeout)
+ return () => {
+ clearTimeout(timeoutId)
+ clearImmediate(s1)
+ clearImmediate(s2)
+ }
+}
+
+function onConnectTimeout (socket) {
+ util.destroy(socket, new ConnectTimeoutError())
+}
+
+module.exports = buildConnector
diff --git a/lib/core/errors.js b/lib/core/errors.js
new file mode 100644
index 0000000..7af704b
--- /dev/null
+++ b/lib/core/errors.js
@@ -0,0 +1,230 @@
+'use strict'
+
+class UndiciError extends Error {
+ constructor (message) {
+ super(message)
+ this.name = 'UndiciError'
+ this.code = 'UND_ERR'
+ }
+}
+
+class ConnectTimeoutError extends UndiciError {
+ constructor (message) {
+ super(message)
+ Error.captureStackTrace(this, ConnectTimeoutError)
+ this.name = 'ConnectTimeoutError'
+ this.message = message || 'Connect Timeout Error'
+ this.code = 'UND_ERR_CONNECT_TIMEOUT'
+ }
+}
+
+class HeadersTimeoutError extends UndiciError {
+ constructor (message) {
+ super(message)
+ Error.captureStackTrace(this, HeadersTimeoutError)
+ this.name = 'HeadersTimeoutError'
+ this.message = message || 'Headers Timeout Error'
+ this.code = 'UND_ERR_HEADERS_TIMEOUT'
+ }
+}
+
+class HeadersOverflowError extends UndiciError {
+ constructor (message) {
+ super(message)
+ Error.captureStackTrace(this, HeadersOverflowError)
+ this.name = 'HeadersOverflowError'
+ this.message = message || 'Headers Overflow Error'
+ this.code = 'UND_ERR_HEADERS_OVERFLOW'
+ }
+}
+
+class BodyTimeoutError extends UndiciError {
+ constructor (message) {
+ super(message)
+ Error.captureStackTrace(this, BodyTimeoutError)
+ this.name = 'BodyTimeoutError'
+ this.message = message || 'Body Timeout Error'
+ this.code = 'UND_ERR_BODY_TIMEOUT'
+ }
+}
+
+class ResponseStatusCodeError extends UndiciError {
+ constructor (message, statusCode, headers, body) {
+ super(message)
+ Error.captureStackTrace(this, ResponseStatusCodeError)
+ this.name = 'ResponseStatusCodeError'
+ this.message = message || 'Response Status Code Error'
+ this.code = 'UND_ERR_RESPONSE_STATUS_CODE'
+ this.body = body
+ this.status = statusCode
+ this.statusCode = statusCode
+ this.headers = headers
+ }
+}
+
+class InvalidArgumentError extends UndiciError {
+ constructor (message) {
+ super(message)
+ Error.captureStackTrace(this, InvalidArgumentError)
+ this.name = 'InvalidArgumentError'
+ this.message = message || 'Invalid Argument Error'
+ this.code = 'UND_ERR_INVALID_ARG'
+ }
+}
+
+class InvalidReturnValueError extends UndiciError {
+ constructor (message) {
+ super(message)
+ Error.captureStackTrace(this, InvalidReturnValueError)
+ this.name = 'InvalidReturnValueError'
+ this.message = message || 'Invalid Return Value Error'
+ this.code = 'UND_ERR_INVALID_RETURN_VALUE'
+ }
+}
+
+class RequestAbortedError extends UndiciError {
+ constructor (message) {
+ super(message)
+ Error.captureStackTrace(this, RequestAbortedError)
+ this.name = 'AbortError'
+ this.message = message || 'Request aborted'
+ this.code = 'UND_ERR_ABORTED'
+ }
+}
+
+class InformationalError extends UndiciError {
+ constructor (message) {
+ super(message)
+ Error.captureStackTrace(this, InformationalError)
+ this.name = 'InformationalError'
+ this.message = message || 'Request information'
+ this.code = 'UND_ERR_INFO'
+ }
+}
+
+class RequestContentLengthMismatchError extends UndiciError {
+ constructor (message) {
+ super(message)
+ Error.captureStackTrace(this, RequestContentLengthMismatchError)
+ this.name = 'RequestContentLengthMismatchError'
+ this.message = message || 'Request body length does not match content-length header'
+ this.code = 'UND_ERR_REQ_CONTENT_LENGTH_MISMATCH'
+ }
+}
+
+class ResponseContentLengthMismatchError extends UndiciError {
+ constructor (message) {
+ super(message)
+ Error.captureStackTrace(this, ResponseContentLengthMismatchError)
+ this.name = 'ResponseContentLengthMismatchError'
+ this.message = message || 'Response body length does not match content-length header'
+ this.code = 'UND_ERR_RES_CONTENT_LENGTH_MISMATCH'
+ }
+}
+
+class ClientDestroyedError extends UndiciError {
+ constructor (message) {
+ super(message)
+ Error.captureStackTrace(this, ClientDestroyedError)
+ this.name = 'ClientDestroyedError'
+ this.message = message || 'The client is destroyed'
+ this.code = 'UND_ERR_DESTROYED'
+ }
+}
+
+class ClientClosedError extends UndiciError {
+ constructor (message) {
+ super(message)
+ Error.captureStackTrace(this, ClientClosedError)
+ this.name = 'ClientClosedError'
+ this.message = message || 'The client is closed'
+ this.code = 'UND_ERR_CLOSED'
+ }
+}
+
+class SocketError extends UndiciError {
+ constructor (message, socket) {
+ super(message)
+ Error.captureStackTrace(this, SocketError)
+ this.name = 'SocketError'
+ this.message = message || 'Socket error'
+ this.code = 'UND_ERR_SOCKET'
+ this.socket = socket
+ }
+}
+
+class NotSupportedError extends UndiciError {
+ constructor (message) {
+ super(message)
+ Error.captureStackTrace(this, NotSupportedError)
+ this.name = 'NotSupportedError'
+ this.message = message || 'Not supported error'
+ this.code = 'UND_ERR_NOT_SUPPORTED'
+ }
+}
+
+class BalancedPoolMissingUpstreamError extends UndiciError {
+ constructor (message) {
+ super(message)
+ Error.captureStackTrace(this, NotSupportedError)
+ this.name = 'MissingUpstreamError'
+ this.message = message || 'No upstream has been added to the BalancedPool'
+ this.code = 'UND_ERR_BPL_MISSING_UPSTREAM'
+ }
+}
+
+class HTTPParserError extends Error {
+ constructor (message, code, data) {
+ super(message)
+ Error.captureStackTrace(this, HTTPParserError)
+ this.name = 'HTTPParserError'
+ this.code = code ? `HPE_${code}` : undefined
+ this.data = data ? data.toString() : undefined
+ }
+}
+
+class ResponseExceededMaxSizeError extends UndiciError {
+ constructor (message) {
+ super(message)
+ Error.captureStackTrace(this, ResponseExceededMaxSizeError)
+ this.name = 'ResponseExceededMaxSizeError'
+ this.message = message || 'Response content exceeded max size'
+ this.code = 'UND_ERR_RES_EXCEEDED_MAX_SIZE'
+ }
+}
+
+class RequestRetryError extends UndiciError {
+ constructor (message, code, { headers, data }) {
+ super(message)
+ Error.captureStackTrace(this, RequestRetryError)
+ this.name = 'RequestRetryError'
+ this.message = message || 'Request retry error'
+ this.code = 'UND_ERR_REQ_RETRY'
+ this.statusCode = code
+ this.data = data
+ this.headers = headers
+ }
+}
+
+module.exports = {
+ HTTPParserError,
+ UndiciError,
+ HeadersTimeoutError,
+ HeadersOverflowError,
+ BodyTimeoutError,
+ RequestContentLengthMismatchError,
+ ConnectTimeoutError,
+ ResponseStatusCodeError,
+ InvalidArgumentError,
+ InvalidReturnValueError,
+ RequestAbortedError,
+ ClientDestroyedError,
+ ClientClosedError,
+ InformationalError,
+ SocketError,
+ NotSupportedError,
+ ResponseContentLengthMismatchError,
+ BalancedPoolMissingUpstreamError,
+ ResponseExceededMaxSizeError,
+ RequestRetryError
+}
diff --git a/lib/core/request.js b/lib/core/request.js
new file mode 100644
index 0000000..3697e6a
--- /dev/null
+++ b/lib/core/request.js
@@ -0,0 +1,499 @@
+'use strict'
+
+const {
+ InvalidArgumentError,
+ NotSupportedError
+} = require('./errors')
+const assert = require('assert')
+const { kHTTP2BuildRequest, kHTTP2CopyHeaders, kHTTP1BuildRequest } = require('./symbols')
+const util = require('./util')
+
+// tokenRegExp and headerCharRegex have been lifted from
+// https://github.com/nodejs/node/blob/main/lib/_http_common.js
+
+/**
+ * Verifies that the given val is a valid HTTP token
+ * per the rules defined in RFC 7230
+ * See https://tools.ietf.org/html/rfc7230#section-3.2.6
+ */
+const tokenRegExp = /^[\^_`a-zA-Z\-0-9!#$%&'*+.|~]+$/
+
+/**
+ * Matches if val contains an invalid field-vchar
+ * field-value = *( field-content / obs-fold )
+ * field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ]
+ * field-vchar = VCHAR / obs-text
+ */
+const headerCharRegex = /[^\t\x20-\x7e\x80-\xff]/
+
+// Verifies that a given path is valid does not contain control chars \x00 to \x20
+const invalidPathRegex = /[^\u0021-\u00ff]/
+
+const kHandler = Symbol('handler')
+
+const channels = {}
+
+let extractBody
+
+try {
+ const diagnosticsChannel = require('diagnostics_channel')
+ channels.create = diagnosticsChannel.channel('undici:request:create')
+ channels.bodySent = diagnosticsChannel.channel('undici:request:bodySent')
+ channels.headers = diagnosticsChannel.channel('undici:request:headers')
+ channels.trailers = diagnosticsChannel.channel('undici:request:trailers')
+ channels.error = diagnosticsChannel.channel('undici:request:error')
+} catch {
+ channels.create = { hasSubscribers: false }
+ channels.bodySent = { hasSubscribers: false }
+ channels.headers = { hasSubscribers: false }
+ channels.trailers = { hasSubscribers: false }
+ channels.error = { hasSubscribers: false }
+}
+
+class Request {
+ constructor (origin, {
+ path,
+ method,
+ body,
+ headers,
+ query,
+ idempotent,
+ blocking,
+ upgrade,
+ headersTimeout,
+ bodyTimeout,
+ reset,
+ throwOnError,
+ expectContinue
+ }, handler) {
+ if (typeof path !== 'string') {
+ throw new InvalidArgumentError('path must be a string')
+ } else if (
+ path[0] !== '/' &&
+ !(path.startsWith('http://') || path.startsWith('https://')) &&
+ method !== 'CONNECT'
+ ) {
+ throw new InvalidArgumentError('path must be an absolute URL or start with a slash')
+ } else if (invalidPathRegex.exec(path) !== null) {
+ throw new InvalidArgumentError('invalid request path')
+ }
+
+ if (typeof method !== 'string') {
+ throw new InvalidArgumentError('method must be a string')
+ } else if (tokenRegExp.exec(method) === null) {
+ throw new InvalidArgumentError('invalid request method')
+ }
+
+ if (upgrade && typeof upgrade !== 'string') {
+ throw new InvalidArgumentError('upgrade must be a string')
+ }
+
+ if (headersTimeout != null && (!Number.isFinite(headersTimeout) || headersTimeout < 0)) {
+ throw new InvalidArgumentError('invalid headersTimeout')
+ }
+
+ if (bodyTimeout != null && (!Number.isFinite(bodyTimeout) || bodyTimeout < 0)) {
+ throw new InvalidArgumentError('invalid bodyTimeout')
+ }
+
+ if (reset != null && typeof reset !== 'boolean') {
+ throw new InvalidArgumentError('invalid reset')
+ }
+
+ if (expectContinue != null && typeof expectContinue !== 'boolean') {
+ throw new InvalidArgumentError('invalid expectContinue')
+ }
+
+ this.headersTimeout = headersTimeout
+
+ this.bodyTimeout = bodyTimeout
+
+ this.throwOnError = throwOnError === true
+
+ this.method = method
+
+ this.abort = null
+
+ if (body == null) {
+ this.body = null
+ } else if (util.isStream(body)) {
+ this.body = body
+
+ const rState = this.body._readableState
+ if (!rState || !rState.autoDestroy) {
+ this.endHandler = function autoDestroy () {
+ util.destroy(this)
+ }
+ this.body.on('end', this.endHandler)
+ }
+
+ this.errorHandler = err => {
+ if (this.abort) {
+ this.abort(err)
+ } else {
+ this.error = err
+ }
+ }
+ this.body.on('error', this.errorHandler)
+ } else if (util.isBuffer(body)) {
+ this.body = body.byteLength ? body : null
+ } else if (ArrayBuffer.isView(body)) {
+ this.body = body.buffer.byteLength ? Buffer.from(body.buffer, body.byteOffset, body.byteLength) : null
+ } else if (body instanceof ArrayBuffer) {
+ this.body = body.byteLength ? Buffer.from(body) : null
+ } else if (typeof body === 'string') {
+ this.body = body.length ? Buffer.from(body) : null
+ } else if (util.isFormDataLike(body) || util.isIterable(body) || util.isBlobLike(body)) {
+ this.body = body
+ } else {
+ throw new InvalidArgumentError('body must be a string, a Buffer, a Readable stream, an iterable, or an async iterable')
+ }
+
+ this.completed = false
+
+ this.aborted = false
+
+ this.upgrade = upgrade || null
+
+ this.path = query ? util.buildURL(path, query) : path
+
+ this.origin = origin
+
+ this.idempotent = idempotent == null
+ ? method === 'HEAD' || method === 'GET'
+ : idempotent
+
+ this.blocking = blocking == null ? false : blocking
+
+ this.reset = reset == null ? null : reset
+
+ this.host = null
+
+ this.contentLength = null
+
+ this.contentType = null
+
+ this.headers = ''
+
+ // Only for H2
+ this.expectContinue = expectContinue != null ? expectContinue : false
+
+ if (Array.isArray(headers)) {
+ if (headers.length % 2 !== 0) {
+ throw new InvalidArgumentError('headers array must be even')
+ }
+ for (let i = 0; i < headers.length; i += 2) {
+ processHeader(this, headers[i], headers[i + 1])
+ }
+ } else if (headers && typeof headers === 'object') {
+ const keys = Object.keys(headers)
+ for (let i = 0; i < keys.length; i++) {
+ const key = keys[i]
+ processHeader(this, key, headers[key])
+ }
+ } else if (headers != null) {
+ throw new InvalidArgumentError('headers must be an object or an array')
+ }
+
+ if (util.isFormDataLike(this.body)) {
+ if (util.nodeMajor < 16 || (util.nodeMajor === 16 && util.nodeMinor < 8)) {
+ throw new InvalidArgumentError('Form-Data bodies are only supported in node v16.8 and newer.')
+ }
+
+ if (!extractBody) {
+ extractBody = require('../fetch/body.js').extractBody
+ }
+
+ const [bodyStream, contentType] = extractBody(body)
+ if (this.contentType == null) {
+ this.contentType = contentType
+ this.headers += `content-type: ${contentType}\r\n`
+ }
+ this.body = bodyStream.stream
+ this.contentLength = bodyStream.length
+ } else if (util.isBlobLike(body) && this.contentType == null && body.type) {
+ this.contentType = body.type
+ this.headers += `content-type: ${body.type}\r\n`
+ }
+
+ util.validateHandler(handler, method, upgrade)
+
+ this.servername = util.getServerName(this.host)
+
+ this[kHandler] = handler
+
+ if (channels.create.hasSubscribers) {
+ channels.create.publish({ request: this })
+ }
+ }
+
+ onBodySent (chunk) {
+ if (this[kHandler].onBodySent) {
+ try {
+ return this[kHandler].onBodySent(chunk)
+ } catch (err) {
+ this.abort(err)
+ }
+ }
+ }
+
+ onRequestSent () {
+ if (channels.bodySent.hasSubscribers) {
+ channels.bodySent.publish({ request: this })
+ }
+
+ if (this[kHandler].onRequestSent) {
+ try {
+ return this[kHandler].onRequestSent()
+ } catch (err) {
+ this.abort(err)
+ }
+ }
+ }
+
+ onConnect (abort) {
+ assert(!this.aborted)
+ assert(!this.completed)
+
+ if (this.error) {
+ abort(this.error)
+ } else {
+ this.abort = abort
+ return this[kHandler].onConnect(abort)
+ }
+ }
+
+ onHeaders (statusCode, headers, resume, statusText) {
+ assert(!this.aborted)
+ assert(!this.completed)
+
+ if (channels.headers.hasSubscribers) {
+ channels.headers.publish({ request: this, response: { statusCode, headers, statusText } })
+ }
+
+ try {
+ return this[kHandler].onHeaders(statusCode, headers, resume, statusText)
+ } catch (err) {
+ this.abort(err)
+ }
+ }
+
+ onData (chunk) {
+ assert(!this.aborted)
+ assert(!this.completed)
+
+ try {
+ return this[kHandler].onData(chunk)
+ } catch (err) {
+ this.abort(err)
+ return false
+ }
+ }
+
+ onUpgrade (statusCode, headers, socket) {
+ assert(!this.aborted)
+ assert(!this.completed)
+
+ return this[kHandler].onUpgrade(statusCode, headers, socket)
+ }
+
+ onComplete (trailers) {
+ this.onFinally()
+
+ assert(!this.aborted)
+
+ this.completed = true
+ if (channels.trailers.hasSubscribers) {
+ channels.trailers.publish({ request: this, trailers })
+ }
+
+ try {
+ return this[kHandler].onComplete(trailers)
+ } catch (err) {
+ // TODO (fix): This might be a bad idea?
+ this.onError(err)
+ }
+ }
+
+ onError (error) {
+ this.onFinally()
+
+ if (channels.error.hasSubscribers) {
+ channels.error.publish({ request: this, error })
+ }
+
+ if (this.aborted) {
+ return
+ }
+ this.aborted = true
+
+ return this[kHandler].onError(error)
+ }
+
+ onFinally () {
+ if (this.errorHandler) {
+ this.body.off('error', this.errorHandler)
+ this.errorHandler = null
+ }
+
+ if (this.endHandler) {
+ this.body.off('end', this.endHandler)
+ this.endHandler = null
+ }
+ }
+
+ // TODO: adjust to support H2
+ addHeader (key, value) {
+ processHeader(this, key, value)
+ return this
+ }
+
+ static [kHTTP1BuildRequest] (origin, opts, handler) {
+ // TODO: Migrate header parsing here, to make Requests
+ // HTTP agnostic
+ return new Request(origin, opts, handler)
+ }
+
+ static [kHTTP2BuildRequest] (origin, opts, handler) {
+ const headers = opts.headers
+ opts = { ...opts, headers: null }
+
+ const request = new Request(origin, opts, handler)
+
+ request.headers = {}
+
+ if (Array.isArray(headers)) {
+ if (headers.length % 2 !== 0) {
+ throw new InvalidArgumentError('headers array must be even')
+ }
+ for (let i = 0; i < headers.length; i += 2) {
+ processHeader(request, headers[i], headers[i + 1], true)
+ }
+ } else if (headers && typeof headers === 'object') {
+ const keys = Object.keys(headers)
+ for (let i = 0; i < keys.length; i++) {
+ const key = keys[i]
+ processHeader(request, key, headers[key], true)
+ }
+ } else if (headers != null) {
+ throw new InvalidArgumentError('headers must be an object or an array')
+ }
+
+ return request
+ }
+
+ static [kHTTP2CopyHeaders] (raw) {
+ const rawHeaders = raw.split('\r\n')
+ const headers = {}
+
+ for (const header of rawHeaders) {
+ const [key, value] = header.split(': ')
+
+ if (value == null || value.length === 0) continue
+
+ if (headers[key]) headers[key] += `,${value}`
+ else headers[key] = value
+ }
+
+ return headers
+ }
+}
+
+function processHeaderValue (key, val, skipAppend) {
+ if (val && typeof val === 'object') {
+ throw new InvalidArgumentError(`invalid ${key} header`)
+ }
+
+ val = val != null ? `${val}` : ''
+
+ if (headerCharRegex.exec(val) !== null) {
+ throw new InvalidArgumentError(`invalid ${key} header`)
+ }
+
+ return skipAppend ? val : `${key}: ${val}\r\n`
+}
+
+function processHeader (request, key, val, skipAppend = false) {
+ if (val && (typeof val === 'object' && !Array.isArray(val))) {
+ throw new InvalidArgumentError(`invalid ${key} header`)
+ } else if (val === undefined) {
+ return
+ }
+
+ if (
+ request.host === null &&
+ key.length === 4 &&
+ key.toLowerCase() === 'host'
+ ) {
+ if (headerCharRegex.exec(val) !== null) {
+ throw new InvalidArgumentError(`invalid ${key} header`)
+ }
+ // Consumed by Client
+ request.host = val
+ } else if (
+ request.contentLength === null &&
+ key.length === 14 &&
+ key.toLowerCase() === 'content-length'
+ ) {
+ request.contentLength = parseInt(val, 10)
+ if (!Number.isFinite(request.contentLength)) {
+ throw new InvalidArgumentError('invalid content-length header')
+ }
+ } else if (
+ request.contentType === null &&
+ key.length === 12 &&
+ key.toLowerCase() === 'content-type'
+ ) {
+ request.contentType = val
+ if (skipAppend) request.headers[key] = processHeaderValue(key, val, skipAppend)
+ else request.headers += processHeaderValue(key, val)
+ } else if (
+ key.length === 17 &&
+ key.toLowerCase() === 'transfer-encoding'
+ ) {
+ throw new InvalidArgumentError('invalid transfer-encoding header')
+ } else if (
+ key.length === 10 &&
+ key.toLowerCase() === 'connection'
+ ) {
+ const value = typeof val === 'string' ? val.toLowerCase() : null
+ if (value !== 'close' && value !== 'keep-alive') {
+ throw new InvalidArgumentError('invalid connection header')
+ } else if (value === 'close') {
+ request.reset = true
+ }
+ } else if (
+ key.length === 10 &&
+ key.toLowerCase() === 'keep-alive'
+ ) {
+ throw new InvalidArgumentError('invalid keep-alive header')
+ } else if (
+ key.length === 7 &&
+ key.toLowerCase() === 'upgrade'
+ ) {
+ throw new InvalidArgumentError('invalid upgrade header')
+ } else if (
+ key.length === 6 &&
+ key.toLowerCase() === 'expect'
+ ) {
+ throw new NotSupportedError('expect header not supported')
+ } else if (tokenRegExp.exec(key) === null) {
+ throw new InvalidArgumentError('invalid header key')
+ } else {
+ if (Array.isArray(val)) {
+ for (let i = 0; i < val.length; i++) {
+ if (skipAppend) {
+ if (request.headers[key]) request.headers[key] += `,${processHeaderValue(key, val[i], skipAppend)}`
+ else request.headers[key] = processHeaderValue(key, val[i], skipAppend)
+ } else {
+ request.headers += processHeaderValue(key, val[i])
+ }
+ }
+ } else {
+ if (skipAppend) request.headers[key] = processHeaderValue(key, val, skipAppend)
+ else request.headers += processHeaderValue(key, val)
+ }
+ }
+}
+
+module.exports = Request
diff --git a/lib/core/symbols.js b/lib/core/symbols.js
new file mode 100644
index 0000000..68d8566
--- /dev/null
+++ b/lib/core/symbols.js
@@ -0,0 +1,63 @@
+module.exports = {
+ kClose: Symbol('close'),
+ kDestroy: Symbol('destroy'),
+ kDispatch: Symbol('dispatch'),
+ kUrl: Symbol('url'),
+ kWriting: Symbol('writing'),
+ kResuming: Symbol('resuming'),
+ kQueue: Symbol('queue'),
+ kConnect: Symbol('connect'),
+ kConnecting: Symbol('connecting'),
+ kHeadersList: Symbol('headers list'),
+ kKeepAliveDefaultTimeout: Symbol('default keep alive timeout'),
+ kKeepAliveMaxTimeout: Symbol('max keep alive timeout'),
+ kKeepAliveTimeoutThreshold: Symbol('keep alive timeout threshold'),
+ kKeepAliveTimeoutValue: Symbol('keep alive timeout'),
+ kKeepAlive: Symbol('keep alive'),
+ kHeadersTimeout: Symbol('headers timeout'),
+ kBodyTimeout: Symbol('body timeout'),
+ kServerName: Symbol('server name'),
+ kLocalAddress: Symbol('local address'),
+ kHost: Symbol('host'),
+ kNoRef: Symbol('no ref'),
+ kBodyUsed: Symbol('used'),
+ kRunning: Symbol('running'),
+ kBlocking: Symbol('blocking'),
+ kPending: Symbol('pending'),
+ kSize: Symbol('size'),
+ kBusy: Symbol('busy'),
+ kQueued: Symbol('queued'),
+ kFree: Symbol('free'),
+ kConnected: Symbol('connected'),
+ kClosed: Symbol('closed'),
+ kNeedDrain: Symbol('need drain'),
+ kReset: Symbol('reset'),
+ kDestroyed: Symbol.for('nodejs.stream.destroyed'),
+ kMaxHeadersSize: Symbol('max headers size'),
+ kRunningIdx: Symbol('running index'),
+ kPendingIdx: Symbol('pending index'),
+ kError: Symbol('error'),
+ kClients: Symbol('clients'),
+ kClient: Symbol('client'),
+ kParser: Symbol('parser'),
+ kOnDestroyed: Symbol('destroy callbacks'),
+ kPipelining: Symbol('pipelining'),
+ kSocket: Symbol('socket'),
+ kHostHeader: Symbol('host header'),
+ kConnector: Symbol('connector'),
+ kStrictContentLength: Symbol('strict content length'),
+ kMaxRedirections: Symbol('maxRedirections'),
+ kMaxRequests: Symbol('maxRequestsPerClient'),
+ kProxy: Symbol('proxy agent options'),
+ kCounter: Symbol('socket request counter'),
+ kInterceptors: Symbol('dispatch interceptors'),
+ kMaxResponseSize: Symbol('max response size'),
+ kHTTP2Session: Symbol('http2Session'),
+ kHTTP2SessionState: Symbol('http2Session state'),
+ kHTTP2BuildRequest: Symbol('http2 build request'),
+ kHTTP1BuildRequest: Symbol('http1 build request'),
+ kHTTP2CopyHeaders: Symbol('http2 copy headers'),
+ kHTTPConnVersion: Symbol('http connection version'),
+ kRetryHandlerDefaultRetry: Symbol('retry agent default retry'),
+ kConstruct: Symbol('constructable')
+}
diff --git a/lib/core/util.js b/lib/core/util.js
new file mode 100644
index 0000000..8d5450b
--- /dev/null
+++ b/lib/core/util.js
@@ -0,0 +1,511 @@
+'use strict'
+
+const assert = require('assert')
+const { kDestroyed, kBodyUsed } = require('./symbols')
+const { IncomingMessage } = require('http')
+const stream = require('stream')
+const net = require('net')
+const { InvalidArgumentError } = require('./errors')
+const { Blob } = require('buffer')
+const nodeUtil = require('util')
+const { stringify } = require('querystring')
+
+const [nodeMajor, nodeMinor] = process.versions.node.split('.').map(v => Number(v))
+
+function nop () {}
+
+function isStream (obj) {
+ return obj && typeof obj === 'object' && typeof obj.pipe === 'function' && typeof obj.on === 'function'
+}
+
+// based on https://github.com/node-fetch/fetch-blob/blob/8ab587d34080de94140b54f07168451e7d0b655e/index.js#L229-L241 (MIT License)
+function isBlobLike (object) {
+ return (Blob && object instanceof Blob) || (
+ object &&
+ typeof object === 'object' &&
+ (typeof object.stream === 'function' ||
+ typeof object.arrayBuffer === 'function') &&
+ /^(Blob|File)$/.test(object[Symbol.toStringTag])
+ )
+}
+
+function buildURL (url, queryParams) {
+ if (url.includes('?') || url.includes('#')) {
+ throw new Error('Query params cannot be passed when url already contains "?" or "#".')
+ }
+
+ const stringified = stringify(queryParams)
+
+ if (stringified) {
+ url += '?' + stringified
+ }
+
+ return url
+}
+
+function parseURL (url) {
+ if (typeof url === 'string') {
+ url = new URL(url)
+
+ if (!/^https?:/.test(url.origin || url.protocol)) {
+ throw new InvalidArgumentError('Invalid URL protocol: the URL must start with `http:` or `https:`.')
+ }
+
+ return url
+ }
+
+ if (!url || typeof url !== 'object') {
+ throw new InvalidArgumentError('Invalid URL: The URL argument must be a non-null object.')
+ }
+
+ if (!/^https?:/.test(url.origin || url.protocol)) {
+ throw new InvalidArgumentError('Invalid URL protocol: the URL must start with `http:` or `https:`.')
+ }
+
+ if (!(url instanceof URL)) {
+ if (url.port != null && url.port !== '' && !Number.isFinite(parseInt(url.port))) {
+ throw new InvalidArgumentError('Invalid URL: port must be a valid integer or a string representation of an integer.')
+ }
+
+ if (url.path != null && typeof url.path !== 'string') {
+ throw new InvalidArgumentError('Invalid URL path: the path must be a string or null/undefined.')
+ }
+
+ if (url.pathname != null && typeof url.pathname !== 'string') {
+ throw new InvalidArgumentError('Invalid URL pathname: the pathname must be a string or null/undefined.')
+ }
+
+ if (url.hostname != null && typeof url.hostname !== 'string') {
+ throw new InvalidArgumentError('Invalid URL hostname: the hostname must be a string or null/undefined.')
+ }
+
+ if (url.origin != null && typeof url.origin !== 'string') {
+ throw new InvalidArgumentError('Invalid URL origin: the origin must be a string or null/undefined.')
+ }
+
+ const port = url.port != null
+ ? url.port
+ : (url.protocol === 'https:' ? 443 : 80)
+ let origin = url.origin != null
+ ? url.origin
+ : `${url.protocol}//${url.hostname}:${port}`
+ let path = url.path != null
+ ? url.path
+ : `${url.pathname || ''}${url.search || ''}`
+
+ if (origin.endsWith('/')) {
+ origin = origin.substring(0, origin.length - 1)
+ }
+
+ if (path && !path.startsWith('/')) {
+ path = `/${path}`
+ }
+ // new URL(path, origin) is unsafe when `path` contains an absolute URL
+ // From https://developer.mozilla.org/en-US/docs/Web/API/URL/URL:
+ // If first parameter is a relative URL, second param is required, and will be used as the base URL.
+ // If first parameter is an absolute URL, a given second param will be ignored.
+ url = new URL(origin + path)
+ }
+
+ return url
+}
+
+function parseOrigin (url) {
+ url = parseURL(url)
+
+ if (url.pathname !== '/' || url.search || url.hash) {
+ throw new InvalidArgumentError('invalid url')
+ }
+
+ return url
+}
+
+function getHostname (host) {
+ if (host[0] === '[') {
+ const idx = host.indexOf(']')
+
+ assert(idx !== -1)
+ return host.substring(1, idx)
+ }
+
+ const idx = host.indexOf(':')
+ if (idx === -1) return host
+
+ return host.substring(0, idx)
+}
+
+// IP addresses are not valid server names per RFC6066
+// > Currently, the only server names supported are DNS hostnames
+function getServerName (host) {
+ if (!host) {
+ return null
+ }
+
+ assert.strictEqual(typeof host, 'string')
+
+ const servername = getHostname(host)
+ if (net.isIP(servername)) {
+ return ''
+ }
+
+ return servername
+}
+
+function deepClone (obj) {
+ return JSON.parse(JSON.stringify(obj))
+}
+
+function isAsyncIterable (obj) {
+ return !!(obj != null && typeof obj[Symbol.asyncIterator] === 'function')
+}
+
+function isIterable (obj) {
+ return !!(obj != null && (typeof obj[Symbol.iterator] === 'function' || typeof obj[Symbol.asyncIterator] === 'function'))
+}
+
+function bodyLength (body) {
+ if (body == null) {
+ return 0
+ } else if (isStream(body)) {
+ const state = body._readableState
+ return state && state.objectMode === false && state.ended === true && Number.isFinite(state.length)
+ ? state.length
+ : null
+ } else if (isBlobLike(body)) {
+ return body.size != null ? body.size : null
+ } else if (isBuffer(body)) {
+ return body.byteLength
+ }
+
+ return null
+}
+
+function isDestroyed (stream) {
+ return !stream || !!(stream.destroyed || stream[kDestroyed])
+}
+
+function isReadableAborted (stream) {
+ const state = stream && stream._readableState
+ return isDestroyed(stream) && state && !state.endEmitted
+}
+
+function destroy (stream, err) {
+ if (stream == null || !isStream(stream) || isDestroyed(stream)) {
+ return
+ }
+
+ if (typeof stream.destroy === 'function') {
+ if (Object.getPrototypeOf(stream).constructor === IncomingMessage) {
+ // See: https://github.com/nodejs/node/pull/38505/files
+ stream.socket = null
+ }
+
+ stream.destroy(err)
+ } else if (err) {
+ process.nextTick((stream, err) => {
+ stream.emit('error', err)
+ }, stream, err)
+ }
+
+ if (stream.destroyed !== true) {
+ stream[kDestroyed] = true
+ }
+}
+
+const KEEPALIVE_TIMEOUT_EXPR = /timeout=(\d+)/
+function parseKeepAliveTimeout (val) {
+ const m = val.toString().match(KEEPALIVE_TIMEOUT_EXPR)
+ return m ? parseInt(m[1], 10) * 1000 : null
+}
+
+function parseHeaders (headers, obj = {}) {
+ // For H2 support
+ if (!Array.isArray(headers)) return headers
+
+ for (let i = 0; i < headers.length; i += 2) {
+ const key = headers[i].toString().toLowerCase()
+ let val = obj[key]
+
+ if (!val) {
+ if (Array.isArray(headers[i + 1])) {
+ obj[key] = headers[i + 1].map(x => x.toString('utf8'))
+ } else {
+ obj[key] = headers[i + 1].toString('utf8')
+ }
+ } else {
+ if (!Array.isArray(val)) {
+ val = [val]
+ obj[key] = val
+ }
+ val.push(headers[i + 1].toString('utf8'))
+ }
+ }
+
+ // See https://github.com/nodejs/node/pull/46528
+ if ('content-length' in obj && 'content-disposition' in obj) {
+ obj['content-disposition'] = Buffer.from(obj['content-disposition']).toString('latin1')
+ }
+
+ return obj
+}
+
+function parseRawHeaders (headers) {
+ const ret = []
+ let hasContentLength = false
+ let contentDispositionIdx = -1
+
+ for (let n = 0; n < headers.length; n += 2) {
+ const key = headers[n + 0].toString()
+ const val = headers[n + 1].toString('utf8')
+
+ if (key.length === 14 && (key === 'content-length' || key.toLowerCase() === 'content-length')) {
+ ret.push(key, val)
+ hasContentLength = true
+ } else if (key.length === 19 && (key === 'content-disposition' || key.toLowerCase() === 'content-disposition')) {
+ contentDispositionIdx = ret.push(key, val) - 1
+ } else {
+ ret.push(key, val)
+ }
+ }
+
+ // See https://github.com/nodejs/node/pull/46528
+ if (hasContentLength && contentDispositionIdx !== -1) {
+ ret[contentDispositionIdx] = Buffer.from(ret[contentDispositionIdx]).toString('latin1')
+ }
+
+ return ret
+}
+
+function isBuffer (buffer) {
+ // See, https://github.com/mcollina/undici/pull/319
+ return buffer instanceof Uint8Array || Buffer.isBuffer(buffer)
+}
+
+function validateHandler (handler, method, upgrade) {
+ if (!handler || typeof handler !== 'object') {
+ throw new InvalidArgumentError('handler must be an object')
+ }
+
+ if (typeof handler.onConnect !== 'function') {
+ throw new InvalidArgumentError('invalid onConnect method')
+ }
+
+ if (typeof handler.onError !== 'function') {
+ throw new InvalidArgumentError('invalid onError method')
+ }
+
+ if (typeof handler.onBodySent !== 'function' && handler.onBodySent !== undefined) {
+ throw new InvalidArgumentError('invalid onBodySent method')
+ }
+
+ if (upgrade || method === 'CONNECT') {
+ if (typeof handler.onUpgrade !== 'function') {
+ throw new InvalidArgumentError('invalid onUpgrade method')
+ }
+ } else {
+ if (typeof handler.onHeaders !== 'function') {
+ throw new InvalidArgumentError('invalid onHeaders method')
+ }
+
+ if (typeof handler.onData !== 'function') {
+ throw new InvalidArgumentError('invalid onData method')
+ }
+
+ if (typeof handler.onComplete !== 'function') {
+ throw new InvalidArgumentError('invalid onComplete method')
+ }
+ }
+}
+
+// A body is disturbed if it has been read from and it cannot
+// be re-used without losing state or data.
+function isDisturbed (body) {
+ return !!(body && (
+ stream.isDisturbed
+ ? stream.isDisturbed(body) || body[kBodyUsed] // TODO (fix): Why is body[kBodyUsed] needed?
+ : body[kBodyUsed] ||
+ body.readableDidRead ||
+ (body._readableState && body._readableState.dataEmitted) ||
+ isReadableAborted(body)
+ ))
+}
+
+function isErrored (body) {
+ return !!(body && (
+ stream.isErrored
+ ? stream.isErrored(body)
+ : /state: 'errored'/.test(nodeUtil.inspect(body)
+ )))
+}
+
+function isReadable (body) {
+ return !!(body && (
+ stream.isReadable
+ ? stream.isReadable(body)
+ : /state: 'readable'/.test(nodeUtil.inspect(body)
+ )))
+}
+
+function getSocketInfo (socket) {
+ return {
+ localAddress: socket.localAddress,
+ localPort: socket.localPort,
+ remoteAddress: socket.remoteAddress,
+ remotePort: socket.remotePort,
+ remoteFamily: socket.remoteFamily,
+ timeout: socket.timeout,
+ bytesWritten: socket.bytesWritten,
+ bytesRead: socket.bytesRead
+ }
+}
+
+async function * convertIterableToBuffer (iterable) {
+ for await (const chunk of iterable) {
+ yield Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)
+ }
+}
+
+let ReadableStream
+function ReadableStreamFrom (iterable) {
+ if (!ReadableStream) {
+ ReadableStream = require('stream/web').ReadableStream
+ }
+
+ if (ReadableStream.from) {
+ return ReadableStream.from(convertIterableToBuffer(iterable))
+ }
+
+ let iterator
+ return new ReadableStream(
+ {
+ async start () {
+ iterator = iterable[Symbol.asyncIterator]()
+ },
+ async pull (controller) {
+ const { done, value } = await iterator.next()
+ if (done) {
+ queueMicrotask(() => {
+ controller.close()
+ })
+ } else {
+ const buf = Buffer.isBuffer(value) ? value : Buffer.from(value)
+ controller.enqueue(new Uint8Array(buf))
+ }
+ return controller.desiredSize > 0
+ },
+ async cancel (reason) {
+ await iterator.return()
+ }
+ },
+ 0
+ )
+}
+
+// The chunk should be a FormData instance and contains
+// all the required methods.
+function isFormDataLike (object) {
+ return (
+ object &&
+ typeof object === 'object' &&
+ typeof object.append === 'function' &&
+ typeof object.delete === 'function' &&
+ typeof object.get === 'function' &&
+ typeof object.getAll === 'function' &&
+ typeof object.has === 'function' &&
+ typeof object.set === 'function' &&
+ object[Symbol.toStringTag] === 'FormData'
+ )
+}
+
+function throwIfAborted (signal) {
+ if (!signal) { return }
+ if (typeof signal.throwIfAborted === 'function') {
+ signal.throwIfAborted()
+ } else {
+ if (signal.aborted) {
+ // DOMException not available < v17.0.0
+ const err = new Error('The operation was aborted')
+ err.name = 'AbortError'
+ throw err
+ }
+ }
+}
+
+function addAbortListener (signal, listener) {
+ if ('addEventListener' in signal) {
+ signal.addEventListener('abort', listener, { once: true })
+ return () => signal.removeEventListener('abort', listener)
+ }
+ signal.addListener('abort', listener)
+ return () => signal.removeListener('abort', listener)
+}
+
+const hasToWellFormed = !!String.prototype.toWellFormed
+
+/**
+ * @param {string} val
+ */
+function toUSVString (val) {
+ if (hasToWellFormed) {
+ return `${val}`.toWellFormed()
+ } else if (nodeUtil.toUSVString) {
+ return nodeUtil.toUSVString(val)
+ }
+
+ return `${val}`
+}
+
+// Parsed accordingly to RFC 9110
+// https://www.rfc-editor.org/rfc/rfc9110#field.content-range
+function parseRangeHeader (range) {
+ if (range == null || range === '') return { start: 0, end: null, size: null }
+
+ const m = range ? range.match(/^bytes (\d+)-(\d+)\/(\d+)?$/) : null
+ return m
+ ? {
+ start: parseInt(m[1]),
+ end: m[2] ? parseInt(m[2]) : null,
+ size: m[3] ? parseInt(m[3]) : null
+ }
+ : null
+}
+
+const kEnumerableProperty = Object.create(null)
+kEnumerableProperty.enumerable = true
+
+module.exports = {
+ kEnumerableProperty,
+ nop,
+ isDisturbed,
+ isErrored,
+ isReadable,
+ toUSVString,
+ isReadableAborted,
+ isBlobLike,
+ parseOrigin,
+ parseURL,
+ getServerName,
+ isStream,
+ isIterable,
+ isAsyncIterable,
+ isDestroyed,
+ parseRawHeaders,
+ parseHeaders,
+ parseKeepAliveTimeout,
+ destroy,
+ bodyLength,
+ deepClone,
+ ReadableStreamFrom,
+ isBuffer,
+ validateHandler,
+ getSocketInfo,
+ isFormDataLike,
+ buildURL,
+ throwIfAborted,
+ addAbortListener,
+ parseRangeHeader,
+ nodeMajor,
+ nodeMinor,
+ nodeHasAutoSelectFamily: nodeMajor > 18 || (nodeMajor === 18 && nodeMinor >= 13),
+ safeHTTPMethods: ['GET', 'HEAD', 'OPTIONS', 'TRACE']
+}
diff --git a/lib/dispatcher-base.js b/lib/dispatcher-base.js
new file mode 100644
index 0000000..5c0220b
--- /dev/null
+++ b/lib/dispatcher-base.js
@@ -0,0 +1,192 @@
+'use strict'
+
+const Dispatcher = require('./dispatcher')
+const {
+ ClientDestroyedError,
+ ClientClosedError,
+ InvalidArgumentError
+} = require('./core/errors')
+const { kDestroy, kClose, kDispatch, kInterceptors } = require('./core/symbols')
+
+const kDestroyed = Symbol('destroyed')
+const kClosed = Symbol('closed')
+const kOnDestroyed = Symbol('onDestroyed')
+const kOnClosed = Symbol('onClosed')
+const kInterceptedDispatch = Symbol('Intercepted Dispatch')
+
+class DispatcherBase extends Dispatcher {
+ constructor () {
+ super()
+
+ this[kDestroyed] = false
+ this[kOnDestroyed] = null
+ this[kClosed] = false
+ this[kOnClosed] = []
+ }
+
+ get destroyed () {
+ return this[kDestroyed]
+ }
+
+ get closed () {
+ return this[kClosed]
+ }
+
+ get interceptors () {
+ return this[kInterceptors]
+ }
+
+ set interceptors (newInterceptors) {
+ if (newInterceptors) {
+ for (let i = newInterceptors.length - 1; i >= 0; i--) {
+ const interceptor = this[kInterceptors][i]
+ if (typeof interceptor !== 'function') {
+ throw new InvalidArgumentError('interceptor must be an function')
+ }
+ }
+ }
+
+ this[kInterceptors] = newInterceptors
+ }
+
+ close (callback) {
+ if (callback === undefined) {
+ return new Promise((resolve, reject) => {
+ this.close((err, data) => {
+ return err ? reject(err) : resolve(data)
+ })
+ })
+ }
+
+ if (typeof callback !== 'function') {
+ throw new InvalidArgumentError('invalid callback')
+ }
+
+ if (this[kDestroyed]) {
+ queueMicrotask(() => callback(new ClientDestroyedError(), null))
+ return
+ }
+
+ if (this[kClosed]) {
+ if (this[kOnClosed]) {
+ this[kOnClosed].push(callback)
+ } else {
+ queueMicrotask(() => callback(null, null))
+ }
+ return
+ }
+
+ this[kClosed] = true
+ this[kOnClosed].push(callback)
+
+ const onClosed = () => {
+ const callbacks = this[kOnClosed]
+ this[kOnClosed] = null
+ for (let i = 0; i < callbacks.length; i++) {
+ callbacks[i](null, null)
+ }
+ }
+
+ // Should not error.
+ this[kClose]()
+ .then(() => this.destroy())
+ .then(() => {
+ queueMicrotask(onClosed)
+ })
+ }
+
+ destroy (err, callback) {
+ if (typeof err === 'function') {
+ callback = err
+ err = null
+ }
+
+ if (callback === undefined) {
+ return new Promise((resolve, reject) => {
+ this.destroy(err, (err, data) => {
+ return err ? /* istanbul ignore next: should never error */ reject(err) : resolve(data)
+ })
+ })
+ }
+
+ if (typeof callback !== 'function') {
+ throw new InvalidArgumentError('invalid callback')
+ }
+
+ if (this[kDestroyed]) {
+ if (this[kOnDestroyed]) {
+ this[kOnDestroyed].push(callback)
+ } else {
+ queueMicrotask(() => callback(null, null))
+ }
+ return
+ }
+
+ if (!err) {
+ err = new ClientDestroyedError()
+ }
+
+ this[kDestroyed] = true
+ this[kOnDestroyed] = this[kOnDestroyed] || []
+ this[kOnDestroyed].push(callback)
+
+ const onDestroyed = () => {
+ const callbacks = this[kOnDestroyed]
+ this[kOnDestroyed] = null
+ for (let i = 0; i < callbacks.length; i++) {
+ callbacks[i](null, null)
+ }
+ }
+
+ // Should not error.
+ this[kDestroy](err).then(() => {
+ queueMicrotask(onDestroyed)
+ })
+ }
+
+ [kInterceptedDispatch] (opts, handler) {
+ if (!this[kInterceptors] || this[kInterceptors].length === 0) {
+ this[kInterceptedDispatch] = this[kDispatch]
+ return this[kDispatch](opts, handler)
+ }
+
+ let dispatch = this[kDispatch].bind(this)
+ for (let i = this[kInterceptors].length - 1; i >= 0; i--) {
+ dispatch = this[kInterceptors][i](dispatch)
+ }
+ this[kInterceptedDispatch] = dispatch
+ return dispatch(opts, handler)
+ }
+
+ dispatch (opts, handler) {
+ if (!handler || typeof handler !== 'object') {
+ throw new InvalidArgumentError('handler must be an object')
+ }
+
+ try {
+ if (!opts || typeof opts !== 'object') {
+ throw new InvalidArgumentError('opts must be an object.')
+ }
+
+ if (this[kDestroyed] || this[kOnDestroyed]) {
+ throw new ClientDestroyedError()
+ }
+
+ if (this[kClosed]) {
+ throw new ClientClosedError()
+ }
+
+ return this[kInterceptedDispatch](opts, handler)
+ } catch (err) {
+ if (typeof handler.onError !== 'function') {
+ throw new InvalidArgumentError('invalid onError method')
+ }
+
+ handler.onError(err)
+
+ return false
+ }
+ }
+}
+
+module.exports = DispatcherBase
diff --git a/lib/dispatcher.js b/lib/dispatcher.js
new file mode 100644
index 0000000..9b809d8
--- /dev/null
+++ b/lib/dispatcher.js
@@ -0,0 +1,19 @@
+'use strict'
+
+const EventEmitter = require('events')
+
+class Dispatcher extends EventEmitter {
+ dispatch () {
+ throw new Error('not implemented')
+ }
+
+ close () {
+ throw new Error('not implemented')
+ }
+
+ destroy () {
+ throw new Error('not implemented')
+ }
+}
+
+module.exports = Dispatcher
diff --git a/lib/fetch/LICENSE b/lib/fetch/LICENSE
new file mode 100644
index 0000000..2943500
--- /dev/null
+++ b/lib/fetch/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2020 Ethan Arrowood
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/lib/fetch/body.js b/lib/fetch/body.js
new file mode 100644
index 0000000..fd8481b
--- /dev/null
+++ b/lib/fetch/body.js
@@ -0,0 +1,605 @@
+'use strict'
+
+const Busboy = require('@fastify/busboy')
+const util = require('../core/util')
+const {
+ ReadableStreamFrom,
+ isBlobLike,
+ isReadableStreamLike,
+ readableStreamClose,
+ createDeferredPromise,
+ fullyReadBody
+} = require('./util')
+const { FormData } = require('./formdata')
+const { kState } = require('./symbols')
+const { webidl } = require('./webidl')
+const { DOMException, structuredClone } = require('./constants')
+const { Blob, File: NativeFile } = require('buffer')
+const { kBodyUsed } = require('../core/symbols')
+const assert = require('assert')
+const { isErrored } = require('../core/util')
+const { isUint8Array, isArrayBuffer } = require('util/types')
+const { File: UndiciFile } = require('./file')
+const { parseMIMEType, serializeAMimeType } = require('./dataURL')
+
+let ReadableStream = globalThis.ReadableStream
+
+/** @type {globalThis['File']} */
+const File = NativeFile ?? UndiciFile
+const textEncoder = new TextEncoder()
+const textDecoder = new TextDecoder()
+
+// https://fetch.spec.whatwg.org/#concept-bodyinit-extract
+function extractBody (object, keepalive = false) {
+ if (!ReadableStream) {
+ ReadableStream = require('stream/web').ReadableStream
+ }
+
+ // 1. Let stream be null.
+ let stream = null
+
+ // 2. If object is a ReadableStream object, then set stream to object.
+ if (object instanceof ReadableStream) {
+ stream = object
+ } else if (isBlobLike(object)) {
+ // 3. Otherwise, if object is a Blob object, set stream to the
+ // result of running object’s get stream.
+ stream = object.stream()
+ } else {
+ // 4. Otherwise, set stream to a new ReadableStream object, and set
+ // up stream.
+ stream = new ReadableStream({
+ async pull (controller) {
+ controller.enqueue(
+ typeof source === 'string' ? textEncoder.encode(source) : source
+ )
+ queueMicrotask(() => readableStreamClose(controller))
+ },
+ start () {},
+ type: undefined
+ })
+ }
+
+ // 5. Assert: stream is a ReadableStream object.
+ assert(isReadableStreamLike(stream))
+
+ // 6. Let action be null.
+ let action = null
+
+ // 7. Let source be null.
+ let source = null
+
+ // 8. Let length be null.
+ let length = null
+
+ // 9. Let type be null.
+ let type = null
+
+ // 10. Switch on object:
+ if (typeof object === 'string') {
+ // Set source to the UTF-8 encoding of object.
+ // Note: setting source to a Uint8Array here breaks some mocking assumptions.
+ source = object
+
+ // Set type to `text/plain;charset=UTF-8`.
+ type = 'text/plain;charset=UTF-8'
+ } else if (object instanceof URLSearchParams) {
+ // URLSearchParams
+
+ // spec says to run application/x-www-form-urlencoded on body.list
+ // this is implemented in Node.js as apart of an URLSearchParams instance toString method
+ // See: https://github.com/nodejs/node/blob/e46c680bf2b211bbd52cf959ca17ee98c7f657f5/lib/internal/url.js#L490
+ // and https://github.com/nodejs/node/blob/e46c680bf2b211bbd52cf959ca17ee98c7f657f5/lib/internal/url.js#L1100
+
+ // Set source to the result of running the application/x-www-form-urlencoded serializer with object’s list.
+ source = object.toString()
+
+ // Set type to `application/x-www-form-urlencoded;charset=UTF-8`.
+ type = 'application/x-www-form-urlencoded;charset=UTF-8'
+ } else if (isArrayBuffer(object)) {
+ // BufferSource/ArrayBuffer
+
+ // Set source to a copy of the bytes held by object.
+ source = new Uint8Array(object.slice())
+ } else if (ArrayBuffer.isView(object)) {
+ // BufferSource/ArrayBufferView
+
+ // Set source to a copy of the bytes held by object.
+ source = new Uint8Array(object.buffer.slice(object.byteOffset, object.byteOffset + object.byteLength))
+ } else if (util.isFormDataLike(object)) {
+ const boundary = `----formdata-undici-0${`${Math.floor(Math.random() * 1e11)}`.padStart(11, '0')}`
+ const prefix = `--${boundary}\r\nContent-Disposition: form-data`
+
+ /*! formdata-polyfill. MIT License. Jimmy Wärting <https://jimmy.warting.se/opensource> */
+ const escape = (str) =>
+ str.replace(/\n/g, '%0A').replace(/\r/g, '%0D').replace(/"/g, '%22')
+ const normalizeLinefeeds = (value) => value.replace(/\r?\n|\r/g, '\r\n')
+
+ // Set action to this step: run the multipart/form-data
+ // encoding algorithm, with object’s entry list and UTF-8.
+ // - This ensures that the body is immutable and can't be changed afterwords
+ // - That the content-length is calculated in advance.
+ // - And that all parts are pre-encoded and ready to be sent.
+
+ const blobParts = []
+ const rn = new Uint8Array([13, 10]) // '\r\n'
+ length = 0
+ let hasUnknownSizeValue = false
+
+ for (const [name, value] of object) {
+ if (typeof value === 'string') {
+ const chunk = textEncoder.encode(prefix +
+ `; name="${escape(normalizeLinefeeds(name))}"` +
+ `\r\n\r\n${normalizeLinefeeds(value)}\r\n`)
+ blobParts.push(chunk)
+ length += chunk.byteLength
+ } else {
+ const chunk = textEncoder.encode(`${prefix}; name="${escape(normalizeLinefeeds(name))}"` +
+ (value.name ? `; filename="${escape(value.name)}"` : '') + '\r\n' +
+ `Content-Type: ${
+ value.type || 'application/octet-stream'
+ }\r\n\r\n`)
+ blobParts.push(chunk, value, rn)
+ if (typeof value.size === 'number') {
+ length += chunk.byteLength + value.size + rn.byteLength
+ } else {
+ hasUnknownSizeValue = true
+ }
+ }
+ }
+
+ const chunk = textEncoder.encode(`--${boundary}--`)
+ blobParts.push(chunk)
+ length += chunk.byteLength
+ if (hasUnknownSizeValue) {
+ length = null
+ }
+
+ // Set source to object.
+ source = object
+
+ action = async function * () {
+ for (const part of blobParts) {
+ if (part.stream) {
+ yield * part.stream()
+ } else {
+ yield part
+ }
+ }
+ }
+
+ // Set type to `multipart/form-data; boundary=`,
+ // followed by the multipart/form-data boundary string generated
+ // by the multipart/form-data encoding algorithm.
+ type = 'multipart/form-data; boundary=' + boundary
+ } else if (isBlobLike(object)) {
+ // Blob
+
+ // Set source to object.
+ source = object
+
+ // Set length to object’s size.
+ length = object.size
+
+ // If object’s type attribute is not the empty byte sequence, set
+ // type to its value.
+ if (object.type) {
+ type = object.type
+ }
+ } else if (typeof object[Symbol.asyncIterator] === 'function') {
+ // If keepalive is true, then throw a TypeError.
+ if (keepalive) {
+ throw new TypeError('keepalive')
+ }
+
+ // If object is disturbed or locked, then throw a TypeError.
+ if (util.isDisturbed(object) || object.locked) {
+ throw new TypeError(
+ 'Response body object should not be disturbed or locked'
+ )
+ }
+
+ stream =
+ object instanceof ReadableStream ? object : ReadableStreamFrom(object)
+ }
+
+ // 11. If source is a byte sequence, then set action to a
+ // step that returns source and length to source’s length.
+ if (typeof source === 'string' || util.isBuffer(source)) {
+ length = Buffer.byteLength(source)
+ }
+
+ // 12. If action is non-null, then run these steps in in parallel:
+ if (action != null) {
+ // Run action.
+ let iterator
+ stream = new ReadableStream({
+ async start () {
+ iterator = action(object)[Symbol.asyncIterator]()
+ },
+ async pull (controller) {
+ const { value, done } = await iterator.next()
+ if (done) {
+ // When running action is done, close stream.
+ queueMicrotask(() => {
+ controller.close()
+ })
+ } 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))
+ }
+ }
+ return controller.desiredSize > 0
+ },
+ async cancel (reason) {
+ await iterator.return()
+ },
+ type: undefined
+ })
+ }
+
+ // 13. Let body be a body whose stream is stream, source is source,
+ // and length is length.
+ const body = { stream, source, length }
+
+ // 14. Return (body, type).
+ return [body, type]
+}
+
+// https://fetch.spec.whatwg.org/#bodyinit-safely-extract
+function safelyExtractBody (object, keepalive = false) {
+ if (!ReadableStream) {
+ // istanbul ignore next
+ ReadableStream = require('stream/web').ReadableStream
+ }
+
+ // To safely extract a body and a `Content-Type` value from
+ // a byte sequence or BodyInit object object, run these steps:
+
+ // 1. If object is a ReadableStream object, then:
+ if (object instanceof ReadableStream) {
+ // Assert: object is neither disturbed nor locked.
+ // istanbul ignore next
+ assert(!util.isDisturbed(object), 'The body has already been consumed.')
+ // istanbul ignore next
+ assert(!object.locked, 'The stream is locked.')
+ }
+
+ // 2. Return the results of extracting object.
+ return extractBody(object, keepalive)
+}
+
+function cloneBody (body) {
+ // To clone a body body, run these steps:
+
+ // https://fetch.spec.whatwg.org/#concept-body-clone
+
+ // 1. Let « out1, out2 » be the result of teeing body’s stream.
+ const [out1, out2] = body.stream.tee()
+ const out2Clone = structuredClone(out2, { transfer: [out2] })
+ // This, for whatever reasons, unrefs out2Clone which allows
+ // the process to exit by itself.
+ const [, finalClone] = out2Clone.tee()
+
+ // 2. Set body’s stream to out1.
+ body.stream = out1
+
+ // 3. Return a body whose stream is out2 and other members are copied from body.
+ return {
+ stream: finalClone,
+ length: body.length,
+ source: body.source
+ }
+}
+
+async function * consumeBody (body) {
+ if (body) {
+ if (isUint8Array(body)) {
+ yield body
+ } else {
+ const stream = body.stream
+
+ if (util.isDisturbed(stream)) {
+ throw new TypeError('The body has already been consumed.')
+ }
+
+ if (stream.locked) {
+ throw new TypeError('The stream is locked.')
+ }
+
+ // Compat.
+ stream[kBodyUsed] = true
+
+ yield * stream
+ }
+ }
+}
+
+function throwIfAborted (state) {
+ if (state.aborted) {
+ throw new DOMException('The operation was aborted.', 'AbortError')
+ }
+}
+
+function bodyMixinMethods (instance) {
+ const methods = {
+ blob () {
+ // The blob() method steps are to return the result of
+ // running consume body with this and the following step
+ // given a byte sequence bytes: return a Blob whose
+ // contents are bytes and whose type attribute is this’s
+ // MIME type.
+ return specConsumeBody(this, (bytes) => {
+ let mimeType = bodyMimeType(this)
+
+ if (mimeType === 'failure') {
+ mimeType = ''
+ } else if (mimeType) {
+ mimeType = serializeAMimeType(mimeType)
+ }
+
+ // Return a Blob whose contents are bytes and type attribute
+ // is mimeType.
+ return new Blob([bytes], { type: mimeType })
+ }, instance)
+ },
+
+ arrayBuffer () {
+ // The arrayBuffer() method steps are to return the result
+ // of running consume body with this and the following step
+ // given a byte sequence bytes: return a new ArrayBuffer
+ // whose contents are bytes.
+ return specConsumeBody(this, (bytes) => {
+ return new Uint8Array(bytes).buffer
+ }, instance)
+ },
+
+ text () {
+ // The text() method steps are to return the result of running
+ // consume body with this and UTF-8 decode.
+ return specConsumeBody(this, utf8DecodeBytes, instance)
+ },
+
+ json () {
+ // The json() method steps are to return the result of running
+ // consume body with this and parse JSON from bytes.
+ return specConsumeBody(this, parseJSONFromBytes, instance)
+ },
+
+ async formData () {
+ webidl.brandCheck(this, instance)
+
+ throwIfAborted(this[kState])
+
+ const contentType = this.headers.get('Content-Type')
+
+ // If mimeType’s essence is "multipart/form-data", then:
+ if (/multipart\/form-data/.test(contentType)) {
+ const headers = {}
+ for (const [key, value] of this.headers) headers[key.toLowerCase()] = value
+
+ const responseFormData = new FormData()
+
+ let busboy
+
+ try {
+ busboy = new Busboy({
+ headers,
+ preservePath: true
+ })
+ } catch (err) {
+ throw new DOMException(`${err}`, 'AbortError')
+ }
+
+ busboy.on('field', (name, value) => {
+ responseFormData.append(name, value)
+ })
+ busboy.on('file', (name, value, filename, encoding, mimeType) => {
+ const chunks = []
+
+ if (encoding === 'base64' || encoding.toLowerCase() === 'base64') {
+ let base64chunk = ''
+
+ value.on('data', (chunk) => {
+ base64chunk += chunk.toString().replace(/[\r\n]/gm, '')
+
+ const end = base64chunk.length - base64chunk.length % 4
+ chunks.push(Buffer.from(base64chunk.slice(0, end), 'base64'))
+
+ base64chunk = base64chunk.slice(end)
+ })
+ value.on('end', () => {
+ chunks.push(Buffer.from(base64chunk, 'base64'))
+ responseFormData.append(name, new File(chunks, filename, { type: mimeType }))
+ })
+ } else {
+ value.on('data', (chunk) => {
+ chunks.push(chunk)
+ })
+ value.on('end', () => {
+ responseFormData.append(name, new File(chunks, filename, { type: mimeType }))
+ })
+ }
+ })
+
+ const busboyResolve = new Promise((resolve, reject) => {
+ busboy.on('finish', resolve)
+ busboy.on('error', (err) => reject(new TypeError(err)))
+ })
+
+ if (this.body !== null) for await (const chunk of consumeBody(this[kState].body)) busboy.write(chunk)
+ busboy.end()
+ await busboyResolve
+
+ return responseFormData
+ } else if (/application\/x-www-form-urlencoded/.test(contentType)) {
+ // Otherwise, if mimeType’s essence is "application/x-www-form-urlencoded", then:
+
+ // 1. Let entries be the result of parsing bytes.
+ let entries
+ try {
+ let text = ''
+ // application/x-www-form-urlencoded parser will keep the BOM.
+ // https://url.spec.whatwg.org/#concept-urlencoded-parser
+ // Note that streaming decoder is stateful and cannot be reused
+ const streamingDecoder = new TextDecoder('utf-8', { ignoreBOM: true })
+
+ for await (const chunk of consumeBody(this[kState].body)) {
+ if (!isUint8Array(chunk)) {
+ throw new TypeError('Expected Uint8Array chunk')
+ }
+ text += streamingDecoder.decode(chunk, { stream: true })
+ }
+ text += streamingDecoder.decode()
+ entries = new URLSearchParams(text)
+ } catch (err) {
+ // istanbul ignore next: Unclear when new URLSearchParams can fail on a string.
+ // 2. If entries is failure, then throw a TypeError.
+ throw Object.assign(new TypeError(), { cause: err })
+ }
+
+ // 3. Return a new FormData object whose entries are entries.
+ const formData = new FormData()
+ for (const [name, value] of entries) {
+ formData.append(name, value)
+ }
+ return formData
+ } else {
+ // Wait a tick before checking if the request has been aborted.
+ // Otherwise, a TypeError can be thrown when an AbortError should.
+ await Promise.resolve()
+
+ throwIfAborted(this[kState])
+
+ // Otherwise, throw a TypeError.
+ throw webidl.errors.exception({
+ header: `${instance.name}.formData`,
+ message: 'Could not parse content as FormData.'
+ })
+ }
+ }
+ }
+
+ return methods
+}
+
+function mixinBody (prototype) {
+ Object.assign(prototype.prototype, bodyMixinMethods(prototype))
+}
+
+/**
+ * @see https://fetch.spec.whatwg.org/#concept-body-consume-body
+ * @param {Response|Request} object
+ * @param {(value: unknown) => unknown} convertBytesToJSValue
+ * @param {Response|Request} instance
+ */
+async function specConsumeBody (object, convertBytesToJSValue, instance) {
+ webidl.brandCheck(object, instance)
+
+ throwIfAborted(object[kState])
+
+ // 1. If object is unusable, then return a promise rejected
+ // with a TypeError.
+ if (bodyUnusable(object[kState].body)) {
+ throw new TypeError('Body is unusable')
+ }
+
+ // 2. Let promise be a new promise.
+ const promise = createDeferredPromise()
+
+ // 3. Let errorSteps given error be to reject promise with error.
+ const errorSteps = (error) => promise.reject(error)
+
+ // 4. Let successSteps given a byte sequence data be to resolve
+ // promise with the result of running convertBytesToJSValue
+ // with data. If that threw an exception, then run errorSteps
+ // with that exception.
+ const successSteps = (data) => {
+ try {
+ promise.resolve(convertBytesToJSValue(data))
+ } catch (e) {
+ errorSteps(e)
+ }
+ }
+
+ // 5. If object’s body is null, then run successSteps with an
+ // empty byte sequence.
+ if (object[kState].body == null) {
+ successSteps(new Uint8Array())
+ return promise.promise
+ }
+
+ // 6. Otherwise, fully read object’s body given successSteps,
+ // errorSteps, and object’s relevant global object.
+ await fullyReadBody(object[kState].body, successSteps, errorSteps)
+
+ // 7. Return promise.
+ return promise.promise
+}
+
+// https://fetch.spec.whatwg.org/#body-unusable
+function bodyUnusable (body) {
+ // An object including the Body interface mixin is
+ // said to be unusable if its body is non-null and
+ // its body’s stream is disturbed or locked.
+ return body != null && (body.stream.locked || util.isDisturbed(body.stream))
+}
+
+/**
+ * @see https://encoding.spec.whatwg.org/#utf-8-decode
+ * @param {Buffer} buffer
+ */
+function utf8DecodeBytes (buffer) {
+ if (buffer.length === 0) {
+ return ''
+ }
+
+ // 1. Let buffer be the result of peeking three bytes from
+ // ioQueue, converted to a byte sequence.
+
+ // 2. If buffer is 0xEF 0xBB 0xBF, then read three
+ // bytes from ioQueue. (Do nothing with those bytes.)
+ if (buffer[0] === 0xEF && buffer[1] === 0xBB && buffer[2] === 0xBF) {
+ buffer = buffer.subarray(3)
+ }
+
+ // 3. Process a queue with an instance of UTF-8’s
+ // decoder, ioQueue, output, and "replacement".
+ const output = textDecoder.decode(buffer)
+
+ // 4. Return output.
+ return output
+}
+
+/**
+ * @see https://infra.spec.whatwg.org/#parse-json-bytes-to-a-javascript-value
+ * @param {Uint8Array} bytes
+ */
+function parseJSONFromBytes (bytes) {
+ return JSON.parse(utf8DecodeBytes(bytes))
+}
+
+/**
+ * @see https://fetch.spec.whatwg.org/#concept-body-mime-type
+ * @param {import('./response').Response|import('./request').Request} object
+ */
+function bodyMimeType (object) {
+ const { headersList } = object[kState]
+ const contentType = headersList.get('content-type')
+
+ if (contentType === null) {
+ return 'failure'
+ }
+
+ return parseMIMEType(contentType)
+}
+
+module.exports = {
+ extractBody,
+ safelyExtractBody,
+ cloneBody,
+ mixinBody
+}
diff --git a/lib/fetch/constants.js b/lib/fetch/constants.js
new file mode 100644
index 0000000..218fcbe
--- /dev/null
+++ b/lib/fetch/constants.js
@@ -0,0 +1,151 @@
+'use strict'
+
+const { MessageChannel, receiveMessageOnPort } = require('worker_threads')
+
+const corsSafeListedMethods = ['GET', 'HEAD', 'POST']
+const corsSafeListedMethodsSet = new Set(corsSafeListedMethods)
+
+const nullBodyStatus = [101, 204, 205, 304]
+
+const redirectStatus = [301, 302, 303, 307, 308]
+const redirectStatusSet = new Set(redirectStatus)
+
+// https://fetch.spec.whatwg.org/#block-bad-port
+const badPorts = [
+ '1', '7', '9', '11', '13', '15', '17', '19', '20', '21', '22', '23', '25', '37', '42', '43', '53', '69', '77', '79',
+ '87', '95', '101', '102', '103', '104', '109', '110', '111', '113', '115', '117', '119', '123', '135', '137',
+ '139', '143', '161', '179', '389', '427', '465', '512', '513', '514', '515', '526', '530', '531', '532',
+ '540', '548', '554', '556', '563', '587', '601', '636', '989', '990', '993', '995', '1719', '1720', '1723',
+ '2049', '3659', '4045', '5060', '5061', '6000', '6566', '6665', '6666', '6667', '6668', '6669', '6697',
+ '10080'
+]
+
+const badPortsSet = new Set(badPorts)
+
+// https://w3c.github.io/webappsec-referrer-policy/#referrer-policies
+const referrerPolicy = [
+ '',
+ 'no-referrer',
+ 'no-referrer-when-downgrade',
+ 'same-origin',
+ 'origin',
+ 'strict-origin',
+ 'origin-when-cross-origin',
+ 'strict-origin-when-cross-origin',
+ 'unsafe-url'
+]
+const referrerPolicySet = new Set(referrerPolicy)
+
+const requestRedirect = ['follow', 'manual', 'error']
+
+const safeMethods = ['GET', 'HEAD', 'OPTIONS', 'TRACE']
+const safeMethodsSet = new Set(safeMethods)
+
+const requestMode = ['navigate', 'same-origin', 'no-cors', 'cors']
+
+const requestCredentials = ['omit', 'same-origin', 'include']
+
+const requestCache = [
+ 'default',
+ 'no-store',
+ 'reload',
+ 'no-cache',
+ 'force-cache',
+ 'only-if-cached'
+]
+
+// https://fetch.spec.whatwg.org/#request-body-header-name
+const requestBodyHeader = [
+ 'content-encoding',
+ 'content-language',
+ 'content-location',
+ 'content-type',
+ // See https://github.com/nodejs/undici/issues/2021
+ // 'Content-Length' is a forbidden header name, which is typically
+ // removed in the Headers implementation. However, undici doesn't
+ // filter out headers, so we add it here.
+ 'content-length'
+]
+
+// https://fetch.spec.whatwg.org/#enumdef-requestduplex
+const requestDuplex = [
+ 'half'
+]
+
+// http://fetch.spec.whatwg.org/#forbidden-method
+const forbiddenMethods = ['CONNECT', 'TRACE', 'TRACK']
+const forbiddenMethodsSet = new Set(forbiddenMethods)
+
+const subresource = [
+ 'audio',
+ 'audioworklet',
+ 'font',
+ 'image',
+ 'manifest',
+ 'paintworklet',
+ 'script',
+ 'style',
+ 'track',
+ 'video',
+ 'xslt',
+ ''
+]
+const subresourceSet = new Set(subresource)
+
+/** @type {globalThis['DOMException']} */
+const DOMException = globalThis.DOMException ?? (() => {
+ // DOMException was only made a global in Node v17.0.0,
+ // but fetch supports >= v16.8.
+ try {
+ atob('~')
+ } catch (err) {
+ return Object.getPrototypeOf(err).constructor
+ }
+})()
+
+let channel
+
+/** @type {globalThis['structuredClone']} */
+const structuredClone =
+ globalThis.structuredClone ??
+ // https://github.com/nodejs/node/blob/b27ae24dcc4251bad726d9d84baf678d1f707fed/lib/internal/structured_clone.js
+ // structuredClone was added in v17.0.0, but fetch supports v16.8
+ function structuredClone (value, options = undefined) {
+ if (arguments.length === 0) {
+ throw new TypeError('missing argument')
+ }
+
+ if (!channel) {
+ channel = new MessageChannel()
+ }
+ channel.port1.unref()
+ channel.port2.unref()
+ channel.port1.postMessage(value, options?.transfer)
+ return receiveMessageOnPort(channel.port2).message
+ }
+
+module.exports = {
+ DOMException,
+ structuredClone,
+ subresource,
+ forbiddenMethods,
+ requestBodyHeader,
+ referrerPolicy,
+ requestRedirect,
+ requestMode,
+ requestCredentials,
+ requestCache,
+ redirectStatus,
+ corsSafeListedMethods,
+ nullBodyStatus,
+ safeMethods,
+ badPorts,
+ requestDuplex,
+ subresourceSet,
+ badPortsSet,
+ redirectStatusSet,
+ corsSafeListedMethodsSet,
+ safeMethodsSet,
+ forbiddenMethodsSet,
+ referrerPolicySet
+}
diff --git a/lib/fetch/dataURL.js b/lib/fetch/dataURL.js
new file mode 100644
index 0000000..7b6a606
--- /dev/null
+++ b/lib/fetch/dataURL.js
@@ -0,0 +1,627 @@
+const assert = require('assert')
+const { atob } = require('buffer')
+const { isomorphicDecode } = require('./util')
+
+const encoder = new TextEncoder()
+
+/**
+ * @see https://mimesniff.spec.whatwg.org/#http-token-code-point
+ */
+const HTTP_TOKEN_CODEPOINTS = /^[!#$%&'*+-.^_|~A-Za-z0-9]+$/
+const HTTP_WHITESPACE_REGEX = /(\u000A|\u000D|\u0009|\u0020)/ // eslint-disable-line
+/**
+ * @see https://mimesniff.spec.whatwg.org/#http-quoted-string-token-code-point
+ */
+const HTTP_QUOTED_STRING_TOKENS = /[\u0009|\u0020-\u007E|\u0080-\u00FF]/ // eslint-disable-line
+
+// https://fetch.spec.whatwg.org/#data-url-processor
+/** @param {URL} dataURL */
+function dataURLProcessor (dataURL) {
+ // 1. Assert: dataURL’s scheme is "data".
+ assert(dataURL.protocol === 'data:')
+
+ // 2. Let input be the result of running the URL
+ // serializer on dataURL with exclude fragment
+ // set to true.
+ let input = URLSerializer(dataURL, true)
+
+ // 3. Remove the leading "data:" string from input.
+ input = input.slice(5)
+
+ // 4. Let position point at the start of input.
+ const position = { position: 0 }
+
+ // 5. Let mimeType be the result of collecting a
+ // sequence of code points that are not equal
+ // to U+002C (,), given position.
+ let mimeType = collectASequenceOfCodePointsFast(
+ ',',
+ input,
+ position
+ )
+
+ // 6. Strip leading and trailing ASCII whitespace
+ // from mimeType.
+ // Undici implementation note: we need to store the
+ // length because if the mimetype has spaces removed,
+ // the wrong amount will be sliced from the input in
+ // step #9
+ const mimeTypeLength = mimeType.length
+ mimeType = removeASCIIWhitespace(mimeType, true, true)
+
+ // 7. If position is past the end of input, then
+ // return failure
+ if (position.position >= input.length) {
+ return 'failure'
+ }
+
+ // 8. Advance position by 1.
+ position.position++
+
+ // 9. Let encodedBody be the remainder of input.
+ const encodedBody = input.slice(mimeTypeLength + 1)
+
+ // 10. Let body be the percent-decoding of encodedBody.
+ let body = stringPercentDecode(encodedBody)
+
+ // 11. If mimeType ends with U+003B (;), followed by
+ // zero or more U+0020 SPACE, followed by an ASCII
+ // case-insensitive match for "base64", then:
+ if (/;(\u0020){0,}base64$/i.test(mimeType)) {
+ // 1. Let stringBody be the isomorphic decode of body.
+ const stringBody = isomorphicDecode(body)
+
+ // 2. Set body to the forgiving-base64 decode of
+ // stringBody.
+ body = forgivingBase64(stringBody)
+
+ // 3. If body is failure, then return failure.
+ if (body === 'failure') {
+ return 'failure'
+ }
+
+ // 4. Remove the last 6 code points from mimeType.
+ mimeType = mimeType.slice(0, -6)
+
+ // 5. Remove trailing U+0020 SPACE code points from mimeType,
+ // if any.
+ mimeType = mimeType.replace(/(\u0020)+$/, '')
+
+ // 6. Remove the last U+003B (;) code point from mimeType.
+ mimeType = mimeType.slice(0, -1)
+ }
+
+ // 12. If mimeType starts with U+003B (;), then prepend
+ // "text/plain" to mimeType.
+ if (mimeType.startsWith(';')) {
+ mimeType = 'text/plain' + mimeType
+ }
+
+ // 13. Let mimeTypeRecord be the result of parsing
+ // mimeType.
+ let mimeTypeRecord = parseMIMEType(mimeType)
+
+ // 14. If mimeTypeRecord is failure, then set
+ // mimeTypeRecord to text/plain;charset=US-ASCII.
+ if (mimeTypeRecord === 'failure') {
+ mimeTypeRecord = parseMIMEType('text/plain;charset=US-ASCII')
+ }
+
+ // 15. Return a new data: URL struct whose MIME
+ // type is mimeTypeRecord and body is body.
+ // https://fetch.spec.whatwg.org/#data-url-struct
+ return { mimeType: mimeTypeRecord, body }
+}
+
+// https://url.spec.whatwg.org/#concept-url-serializer
+/**
+ * @param {URL} url
+ * @param {boolean} excludeFragment
+ */
+function URLSerializer (url, excludeFragment = false) {
+ if (!excludeFragment) {
+ return url.href
+ }
+
+ const href = url.href
+ const hashLength = url.hash.length
+
+ return hashLength === 0 ? href : href.substring(0, href.length - hashLength)
+}
+
+// https://infra.spec.whatwg.org/#collect-a-sequence-of-code-points
+/**
+ * @param {(char: string) => boolean} condition
+ * @param {string} input
+ * @param {{ position: number }} position
+ */
+function collectASequenceOfCodePoints (condition, input, position) {
+ // 1. Let result be the empty string.
+ let result = ''
+
+ // 2. While position doesn’t point past the end of input and the
+ // code point at position within input meets the condition condition:
+ while (position.position < input.length && condition(input[position.position])) {
+ // 1. Append that code point to the end of result.
+ result += input[position.position]
+
+ // 2. Advance position by 1.
+ position.position++
+ }
+
+ // 3. Return result.
+ return result
+}
+
+/**
+ * A faster collectASequenceOfCodePoints that only works when comparing a single character.
+ * @param {string} char
+ * @param {string} input
+ * @param {{ position: number }} position
+ */
+function collectASequenceOfCodePointsFast (char, input, position) {
+ const idx = input.indexOf(char, position.position)
+ const start = position.position
+
+ if (idx === -1) {
+ position.position = input.length
+ return input.slice(start)
+ }
+
+ position.position = idx
+ return input.slice(start, position.position)
+}
+
+// https://url.spec.whatwg.org/#string-percent-decode
+/** @param {string} input */
+function stringPercentDecode (input) {
+ // 1. Let bytes be the UTF-8 encoding of input.
+ const bytes = encoder.encode(input)
+
+ // 2. Return the percent-decoding of bytes.
+ return percentDecode(bytes)
+}
+
+// https://url.spec.whatwg.org/#percent-decode
+/** @param {Uint8Array} input */
+function percentDecode (input) {
+ // 1. Let output be an empty byte sequence.
+ /** @type {number[]} */
+ const output = []
+
+ // 2. For each byte byte in input:
+ for (let i = 0; i < input.length; i++) {
+ const byte = input[i]
+
+ // 1. If byte is not 0x25 (%), then append byte to output.
+ if (byte !== 0x25) {
+ output.push(byte)
+
+ // 2. Otherwise, if byte is 0x25 (%) and the next two bytes
+ // after byte in input are not in the ranges
+ // 0x30 (0) to 0x39 (9), 0x41 (A) to 0x46 (F),
+ // and 0x61 (a) to 0x66 (f), all inclusive, append byte
+ // to output.
+ } else if (
+ byte === 0x25 &&
+ !/^[0-9A-Fa-f]{2}$/i.test(String.fromCharCode(input[i + 1], input[i + 2]))
+ ) {
+ output.push(0x25)
+
+ // 3. Otherwise:
+ } else {
+ // 1. Let bytePoint be the two bytes after byte in input,
+ // decoded, and then interpreted as hexadecimal number.
+ const nextTwoBytes = String.fromCharCode(input[i + 1], input[i + 2])
+ const bytePoint = Number.parseInt(nextTwoBytes, 16)
+
+ // 2. Append a byte whose value is bytePoint to output.
+ output.push(bytePoint)
+
+ // 3. Skip the next two bytes in input.
+ i += 2
+ }
+ }
+
+ // 3. Return output.
+ return Uint8Array.from(output)
+}
+
+// https://mimesniff.spec.whatwg.org/#parse-a-mime-type
+/** @param {string} input */
+function parseMIMEType (input) {
+ // 1. Remove any leading and trailing HTTP whitespace
+ // from input.
+ input = removeHTTPWhitespace(input, true, true)
+
+ // 2. Let position be a position variable for input,
+ // initially pointing at the start of input.
+ const position = { position: 0 }
+
+ // 3. Let type be the result of collecting a sequence
+ // of code points that are not U+002F (/) from
+ // input, given position.
+ const type = collectASequenceOfCodePointsFast(
+ '/',
+ input,
+ position
+ )
+
+ // 4. If type is the empty string or does not solely
+ // contain HTTP token code points, then return failure.
+ // https://mimesniff.spec.whatwg.org/#http-token-code-point
+ if (type.length === 0 || !HTTP_TOKEN_CODEPOINTS.test(type)) {
+ return 'failure'
+ }
+
+ // 5. If position is past the end of input, then return
+ // failure
+ if (position.position > input.length) {
+ return 'failure'
+ }
+
+ // 6. Advance position by 1. (This skips past U+002F (/).)
+ position.position++
+
+ // 7. Let subtype be the result of collecting a sequence of
+ // code points that are not U+003B (;) from input, given
+ // position.
+ let subtype = collectASequenceOfCodePointsFast(
+ ';',
+ input,
+ position
+ )
+
+ // 8. Remove any trailing HTTP whitespace from subtype.
+ subtype = removeHTTPWhitespace(subtype, false, true)
+
+ // 9. If subtype is the empty string or does not solely
+ // contain HTTP token code points, then return failure.
+ if (subtype.length === 0 || !HTTP_TOKEN_CODEPOINTS.test(subtype)) {
+ return 'failure'
+ }
+
+ const typeLowercase = type.toLowerCase()
+ const subtypeLowercase = subtype.toLowerCase()
+
+ // 10. Let mimeType be a new MIME type record whose type
+ // is type, in ASCII lowercase, and subtype is subtype,
+ // in ASCII lowercase.
+ // https://mimesniff.spec.whatwg.org/#mime-type
+ const mimeType = {
+ type: typeLowercase,
+ subtype: subtypeLowercase,
+ /** @type {Map<string, string>} */
+ parameters: new Map(),
+ // https://mimesniff.spec.whatwg.org/#mime-type-essence
+ essence: `${typeLowercase}/${subtypeLowercase}`
+ }
+
+ // 11. While position is not past the end of input:
+ while (position.position < input.length) {
+ // 1. Advance position by 1. (This skips past U+003B (;).)
+ position.position++
+
+ // 2. Collect a sequence of code points that are HTTP
+ // whitespace from input given position.
+ collectASequenceOfCodePoints(
+ // https://fetch.spec.whatwg.org/#http-whitespace
+ char => HTTP_WHITESPACE_REGEX.test(char),
+ input,
+ position
+ )
+
+ // 3. Let parameterName be the result of collecting a
+ // sequence of code points that are not U+003B (;)
+ // or U+003D (=) from input, given position.
+ let parameterName = collectASequenceOfCodePoints(
+ (char) => char !== ';' && char !== '=',
+ input,
+ position
+ )
+
+ // 4. Set parameterName to parameterName, in ASCII
+ // lowercase.
+ parameterName = parameterName.toLowerCase()
+
+ // 5. If position is not past the end of input, then:
+ if (position.position < input.length) {
+ // 1. If the code point at position within input is
+ // U+003B (;), then continue.
+ if (input[position.position] === ';') {
+ continue
+ }
+
+ // 2. Advance position by 1. (This skips past U+003D (=).)
+ position.position++
+ }
+
+ // 6. If position is past the end of input, then break.
+ if (position.position > input.length) {
+ break
+ }
+
+ // 7. Let parameterValue be null.
+ let parameterValue = null
+
+ // 8. If the code point at position within input is
+ // U+0022 ("), then:
+ if (input[position.position] === '"') {
+ // 1. Set parameterValue to the result of collecting
+ // an HTTP quoted string from input, given position
+ // and the extract-value flag.
+ parameterValue = collectAnHTTPQuotedString(input, position, true)
+
+ // 2. Collect a sequence of code points that are not
+ // U+003B (;) from input, given position.
+ collectASequenceOfCodePointsFast(
+ ';',
+ input,
+ position
+ )
+
+ // 9. Otherwise:
+ } else {
+ // 1. Set parameterValue to the result of collecting
+ // a sequence of code points that are not U+003B (;)
+ // from input, given position.
+ parameterValue = collectASequenceOfCodePointsFast(
+ ';',
+ input,
+ position
+ )
+
+ // 2. Remove any trailing HTTP whitespace from parameterValue.
+ parameterValue = removeHTTPWhitespace(parameterValue, false, true)
+
+ // 3. If parameterValue is the empty string, then continue.
+ if (parameterValue.length === 0) {
+ continue
+ }
+ }
+
+ // 10. If all of the following are true
+ // - parameterName is not the empty string
+ // - parameterName solely contains HTTP token code points
+ // - parameterValue solely contains HTTP quoted-string token code points
+ // - mimeType’s parameters[parameterName] does not exist
+ // then set mimeType’s parameters[parameterName] to parameterValue.
+ if (
+ parameterName.length !== 0 &&
+ HTTP_TOKEN_CODEPOINTS.test(parameterName) &&
+ (parameterValue.length === 0 || HTTP_QUOTED_STRING_TOKENS.test(parameterValue)) &&
+ !mimeType.parameters.has(parameterName)
+ ) {
+ mimeType.parameters.set(parameterName, parameterValue)
+ }
+ }
+
+ // 12. Return mimeType.
+ return mimeType
+}
+
+// https://infra.spec.whatwg.org/#forgiving-base64-decode
+/** @param {string} data */
+function forgivingBase64 (data) {
+ // 1. Remove all ASCII whitespace from data.
+ data = data.replace(/[\u0009\u000A\u000C\u000D\u0020]/g, '') // eslint-disable-line
+
+ // 2. If data’s code point length divides by 4 leaving
+ // no remainder, then:
+ if (data.length % 4 === 0) {
+ // 1. If data ends with one or two U+003D (=) code points,
+ // then remove them from data.
+ data = data.replace(/=?=$/, '')
+ }
+
+ // 3. If data’s code point length divides by 4 leaving
+ // a remainder of 1, then return failure.
+ if (data.length % 4 === 1) {
+ return 'failure'
+ }
+
+ // 4. If data contains a code point that is not one of
+ // U+002B (+)
+ // U+002F (/)
+ // ASCII alphanumeric
+ // then return failure.
+ if (/[^+/0-9A-Za-z]/.test(data)) {
+ return 'failure'
+ }
+
+ const binary = atob(data)
+ const bytes = new Uint8Array(binary.length)
+
+ for (let byte = 0; byte < binary.length; byte++) {
+ bytes[byte] = binary.charCodeAt(byte)
+ }
+
+ return bytes
+}
+
+// https://fetch.spec.whatwg.org/#collect-an-http-quoted-string
+// tests: https://fetch.spec.whatwg.org/#example-http-quoted-string
+/**
+ * @param {string} input
+ * @param {{ position: number }} position
+ * @param {boolean?} extractValue
+ */
+function collectAnHTTPQuotedString (input, position, extractValue) {
+ // 1. Let positionStart be position.
+ const positionStart = position.position
+
+ // 2. Let value be the empty string.
+ let value = ''
+
+ // 3. Assert: the code point at position within input
+ // is U+0022 (").
+ assert(input[position.position] === '"')
+
+ // 4. Advance position by 1.
+ position.position++
+
+ // 5. While true:
+ while (true) {
+ // 1. Append the result of collecting a sequence of code points
+ // that are not U+0022 (") or U+005C (\) from input, given
+ // position, to value.
+ value += collectASequenceOfCodePoints(
+ (char) => char !== '"' && char !== '\\',
+ input,
+ position
+ )
+
+ // 2. If position is past the end of input, then break.
+ if (position.position >= input.length) {
+ break
+ }
+
+ // 3. Let quoteOrBackslash be the code point at position within
+ // input.
+ const quoteOrBackslash = input[position.position]
+
+ // 4. Advance position by 1.
+ position.position++
+
+ // 5. If quoteOrBackslash is U+005C (\), then:
+ if (quoteOrBackslash === '\\') {
+ // 1. If position is past the end of input, then append
+ // U+005C (\) to value and break.
+ if (position.position >= input.length) {
+ value += '\\'
+ break
+ }
+
+ // 2. Append the code point at position within input to value.
+ value += input[position.position]
+
+ // 3. Advance position by 1.
+ position.position++
+
+ // 6. Otherwise:
+ } else {
+ // 1. Assert: quoteOrBackslash is U+0022 (").
+ assert(quoteOrBackslash === '"')
+
+ // 2. Break.
+ break
+ }
+ }
+
+ // 6. If the extract-value flag is set, then return value.
+ if (extractValue) {
+ return value
+ }
+
+ // 7. Return the code points from positionStart to position,
+ // inclusive, within input.
+ return input.slice(positionStart, position.position)
+}
+
+/**
+ * @see https://mimesniff.spec.whatwg.org/#serialize-a-mime-type
+ */
+function serializeAMimeType (mimeType) {
+ assert(mimeType !== 'failure')
+ const { parameters, essence } = mimeType
+
+ // 1. Let serialization be the concatenation of mimeType’s
+ // type, U+002F (/), and mimeType’s subtype.
+ let serialization = essence
+
+ // 2. For each name → value of mimeType’s parameters:
+ for (let [name, value] of parameters.entries()) {
+ // 1. Append U+003B (;) to serialization.
+ serialization += ';'
+
+ // 2. Append name to serialization.
+ serialization += name
+
+ // 3. Append U+003D (=) to serialization.
+ serialization += '='
+
+ // 4. If value does not solely contain HTTP token code
+ // points or value is the empty string, then:
+ if (!HTTP_TOKEN_CODEPOINTS.test(value)) {
+ // 1. Precede each occurence of U+0022 (") or
+ // U+005C (\) in value with U+005C (\).
+ value = value.replace(/(\\|")/g, '\\$1')
+
+ // 2. Prepend U+0022 (") to value.
+ value = '"' + value
+
+ // 3. Append U+0022 (") to value.
+ value += '"'
+ }
+
+ // 5. Append value to serialization.
+ serialization += value
+ }
+
+ // 3. Return serialization.
+ return serialization
+}
+
+/**
+ * @see https://fetch.spec.whatwg.org/#http-whitespace
+ * @param {string} char
+ */
+function isHTTPWhiteSpace (char) {
+ return char === '\r' || char === '\n' || char === '\t' || char === ' '
+}
+
+/**
+ * @see https://fetch.spec.whatwg.org/#http-whitespace
+ * @param {string} str
+ */
+function removeHTTPWhitespace (str, leading = true, trailing = true) {
+ let lead = 0
+ let trail = str.length - 1
+
+ if (leading) {
+ for (; lead < str.length && isHTTPWhiteSpace(str[lead]); lead++);
+ }
+
+ if (trailing) {
+ for (; trail > 0 && isHTTPWhiteSpace(str[trail]); trail--);
+ }
+
+ return str.slice(lead, trail + 1)
+}
+
+/**
+ * @see https://infra.spec.whatwg.org/#ascii-whitespace
+ * @param {string} char
+ */
+function isASCIIWhitespace (char) {
+ return char === '\r' || char === '\n' || char === '\t' || char === '\f' || char === ' '
+}
+
+/**
+ * @see https://infra.spec.whatwg.org/#strip-leading-and-trailing-ascii-whitespace
+ */
+function removeASCIIWhitespace (str, leading = true, trailing = true) {
+ let lead = 0
+ let trail = str.length - 1
+
+ if (leading) {
+ for (; lead < str.length && isASCIIWhitespace(str[lead]); lead++);
+ }
+
+ if (trailing) {
+ for (; trail > 0 && isASCIIWhitespace(str[trail]); trail--);
+ }
+
+ return str.slice(lead, trail + 1)
+}
+
+module.exports = {
+ dataURLProcessor,
+ URLSerializer,
+ collectASequenceOfCodePoints,
+ collectASequenceOfCodePointsFast,
+ stringPercentDecode,
+ parseMIMEType,
+ collectAnHTTPQuotedString,
+ serializeAMimeType
+}
diff --git a/lib/fetch/file.js b/lib/fetch/file.js
new file mode 100644
index 0000000..3133d25
--- /dev/null
+++ b/lib/fetch/file.js
@@ -0,0 +1,344 @@
+'use strict'
+
+const { Blob, File: NativeFile } = require('buffer')
+const { types } = require('util')
+const { kState } = require('./symbols')
+const { isBlobLike } = require('./util')
+const { webidl } = require('./webidl')
+const { parseMIMEType, serializeAMimeType } = require('./dataURL')
+const { kEnumerableProperty } = require('../core/util')
+const encoder = new TextEncoder()
+
+class File extends Blob {
+ constructor (fileBits, fileName, options = {}) {
+ // The File constructor is invoked with two or three parameters, depending
+ // on whether the optional dictionary parameter is used. When the File()
+ // constructor is invoked, user agents must run the following steps:
+ webidl.argumentLengthCheck(arguments, 2, { header: 'File constructor' })
+
+ fileBits = webidl.converters['sequence<BlobPart>'](fileBits)
+ fileName = webidl.converters.USVString(fileName)
+ options = webidl.converters.FilePropertyBag(options)
+
+ // 1. Let bytes be the result of processing blob parts given fileBits and
+ // options.
+ // Note: Blob handles this for us
+
+ // 2. Let n be the fileName argument to the constructor.
+ const n = fileName
+
+ // 3. Process FilePropertyBag dictionary argument by running the following
+ // substeps:
+
+ // 1. If the type member is provided and is not the empty string, let t
+ // be set to the type dictionary member. If t contains any characters
+ // outside the range U+0020 to U+007E, then set t to the empty string
+ // and return from these substeps.
+ // 2. Convert every character in t to ASCII lowercase.
+ let t = options.type
+ let d
+
+ // eslint-disable-next-line no-labels
+ substep: {
+ if (t) {
+ t = parseMIMEType(t)
+
+ if (t === 'failure') {
+ t = ''
+ // eslint-disable-next-line no-labels
+ break substep
+ }
+
+ t = serializeAMimeType(t).toLowerCase()
+ }
+
+ // 3. If the lastModified member is provided, let d be set to the
+ // lastModified dictionary member. If it is not provided, set d to the
+ // current date and time represented as the number of milliseconds since
+ // the Unix Epoch (which is the equivalent of Date.now() [ECMA-262]).
+ d = options.lastModified
+ }
+
+ // 4. Return a new File object F such that:
+ // F refers to the bytes byte sequence.
+ // F.size is set to the number of total bytes in bytes.
+ // F.name is set to n.
+ // F.type is set to t.
+ // F.lastModified is set to d.
+
+ super(processBlobParts(fileBits, options), { type: t })
+ this[kState] = {
+ name: n,
+ lastModified: d,
+ type: t
+ }
+ }
+
+ get name () {
+ webidl.brandCheck(this, File)
+
+ return this[kState].name
+ }
+
+ get lastModified () {
+ webidl.brandCheck(this, File)
+
+ return this[kState].lastModified
+ }
+
+ get type () {
+ webidl.brandCheck(this, File)
+
+ return this[kState].type
+ }
+}
+
+class FileLike {
+ constructor (blobLike, fileName, options = {}) {
+ // TODO: argument idl type check
+
+ // The File constructor is invoked with two or three parameters, depending
+ // on whether the optional dictionary parameter is used. When the File()
+ // constructor is invoked, user agents must run the following steps:
+
+ // 1. Let bytes be the result of processing blob parts given fileBits and
+ // options.
+
+ // 2. Let n be the fileName argument to the constructor.
+ const n = fileName
+
+ // 3. Process FilePropertyBag dictionary argument by running the following
+ // substeps:
+
+ // 1. If the type member is provided and is not the empty string, let t
+ // be set to the type dictionary member. If t contains any characters
+ // outside the range U+0020 to U+007E, then set t to the empty string
+ // and return from these substeps.
+ // TODO
+ const t = options.type
+
+ // 2. Convert every character in t to ASCII lowercase.
+ // TODO
+
+ // 3. If the lastModified member is provided, let d be set to the
+ // lastModified dictionary member. If it is not provided, set d to the
+ // current date and time represented as the number of milliseconds since
+ // the Unix Epoch (which is the equivalent of Date.now() [ECMA-262]).
+ const d = options.lastModified ?? Date.now()
+
+ // 4. Return a new File object F such that:
+ // F refers to the bytes byte sequence.
+ // F.size is set to the number of total bytes in bytes.
+ // F.name is set to n.
+ // F.type is set to t.
+ // F.lastModified is set to d.
+
+ this[kState] = {
+ blobLike,
+ name: n,
+ type: t,
+ lastModified: d
+ }
+ }
+
+ stream (...args) {
+ webidl.brandCheck(this, FileLike)
+
+ return this[kState].blobLike.stream(...args)
+ }
+
+ arrayBuffer (...args) {
+ webidl.brandCheck(this, FileLike)
+
+ return this[kState].blobLike.arrayBuffer(...args)
+ }
+
+ slice (...args) {
+ webidl.brandCheck(this, FileLike)
+
+ return this[kState].blobLike.slice(...args)
+ }
+
+ text (...args) {
+ webidl.brandCheck(this, FileLike)
+
+ return this[kState].blobLike.text(...args)
+ }
+
+ get size () {
+ webidl.brandCheck(this, FileLike)
+
+ return this[kState].blobLike.size
+ }
+
+ get type () {
+ webidl.brandCheck(this, FileLike)
+
+ return this[kState].blobLike.type
+ }
+
+ get name () {
+ webidl.brandCheck(this, FileLike)
+
+ return this[kState].name
+ }
+
+ get lastModified () {
+ webidl.brandCheck(this, FileLike)
+
+ return this[kState].lastModified
+ }
+
+ get [Symbol.toStringTag] () {
+ return 'File'
+ }
+}
+
+Object.defineProperties(File.prototype, {
+ [Symbol.toStringTag]: {
+ value: 'File',
+ configurable: true
+ },
+ name: kEnumerableProperty,
+ lastModified: kEnumerableProperty
+})
+
+webidl.converters.Blob = webidl.interfaceConverter(Blob)
+
+webidl.converters.BlobPart = function (V, opts) {
+ if (webidl.util.Type(V) === 'Object') {
+ if (isBlobLike(V)) {
+ return webidl.converters.Blob(V, { strict: false })
+ }
+
+ if (
+ ArrayBuffer.isView(V) ||
+ types.isAnyArrayBuffer(V)
+ ) {
+ return webidl.converters.BufferSource(V, opts)
+ }
+ }
+
+ return webidl.converters.USVString(V, opts)
+}
+
+webidl.converters['sequence<BlobPart>'] = webidl.sequenceConverter(
+ webidl.converters.BlobPart
+)
+
+// https://www.w3.org/TR/FileAPI/#dfn-FilePropertyBag
+webidl.converters.FilePropertyBag = webidl.dictionaryConverter([
+ {
+ key: 'lastModified',
+ converter: webidl.converters['long long'],
+ get defaultValue () {
+ return Date.now()
+ }
+ },
+ {
+ key: 'type',
+ converter: webidl.converters.DOMString,
+ defaultValue: ''
+ },
+ {
+ key: 'endings',
+ converter: (value) => {
+ value = webidl.converters.DOMString(value)
+ value = value.toLowerCase()
+
+ if (value !== 'native') {
+ value = 'transparent'
+ }
+
+ return value
+ },
+ defaultValue: 'transparent'
+ }
+])
+
+/**
+ * @see https://www.w3.org/TR/FileAPI/#process-blob-parts
+ * @param {(NodeJS.TypedArray|Blob|string)[]} parts
+ * @param {{ type: string, endings: string }} options
+ */
+function processBlobParts (parts, options) {
+ // 1. Let bytes be an empty sequence of bytes.
+ /** @type {NodeJS.TypedArray[]} */
+ const bytes = []
+
+ // 2. For each element in parts:
+ for (const element of parts) {
+ // 1. If element is a USVString, run the following substeps:
+ if (typeof element === 'string') {
+ // 1. Let s be element.
+ let s = element
+
+ // 2. If the endings member of options is "native", set s
+ // to the result of converting line endings to native
+ // of element.
+ if (options.endings === 'native') {
+ s = convertLineEndingsNative(s)
+ }
+
+ // 3. Append the result of UTF-8 encoding s to bytes.
+ bytes.push(encoder.encode(s))
+ } else if (
+ types.isAnyArrayBuffer(element) ||
+ types.isTypedArray(element)
+ ) {
+ // 2. If element is a BufferSource, get a copy of the
+ // bytes held by the buffer source, and append those
+ // bytes to bytes.
+ if (!element.buffer) { // ArrayBuffer
+ bytes.push(new Uint8Array(element))
+ } else {
+ bytes.push(
+ new Uint8Array(element.buffer, element.byteOffset, element.byteLength)
+ )
+ }
+ } else if (isBlobLike(element)) {
+ // 3. If element is a Blob, append the bytes it represents
+ // to bytes.
+ bytes.push(element)
+ }
+ }
+
+ // 3. Return bytes.
+ return bytes
+}
+
+/**
+ * @see https://www.w3.org/TR/FileAPI/#convert-line-endings-to-native
+ * @param {string} s
+ */
+function convertLineEndingsNative (s) {
+ // 1. Let native line ending be be the code point U+000A LF.
+ let nativeLineEnding = '\n'
+
+ // 2. If the underlying platform’s conventions are to
+ // represent newlines as a carriage return and line feed
+ // sequence, set native line ending to the code point
+ // U+000D CR followed by the code point U+000A LF.
+ if (process.platform === 'win32') {
+ nativeLineEnding = '\r\n'
+ }
+
+ return s.replace(/\r?\n/g, nativeLineEnding)
+}
+
+// If this function is moved to ./util.js, some tools (such as
+// rollup) will warn about circular dependencies. See:
+// https://github.com/nodejs/undici/issues/1629
+function isFileLike (object) {
+ return (
+ (NativeFile && object instanceof NativeFile) ||
+ object instanceof File || (
+ object &&
+ (typeof object.stream === 'function' ||
+ typeof object.arrayBuffer === 'function') &&
+ object[Symbol.toStringTag] === 'File'
+ )
+ )
+}
+
+module.exports = { File, FileLike, isFileLike }
diff --git a/lib/fetch/formdata.js b/lib/fetch/formdata.js
new file mode 100644
index 0000000..5975e26
--- /dev/null
+++ b/lib/fetch/formdata.js
@@ -0,0 +1,265 @@
+'use strict'
+
+const { isBlobLike, toUSVString, makeIterator } = require('./util')
+const { kState } = require('./symbols')
+const { File: UndiciFile, FileLike, isFileLike } = require('./file')
+const { webidl } = require('./webidl')
+const { Blob, File: NativeFile } = require('buffer')
+
+/** @type {globalThis['File']} */
+const File = NativeFile ?? UndiciFile
+
+// https://xhr.spec.whatwg.org/#formdata
+class FormData {
+ constructor (form) {
+ if (form !== undefined) {
+ throw webidl.errors.conversionFailed({
+ prefix: 'FormData constructor',
+ argument: 'Argument 1',
+ types: ['undefined']
+ })
+ }
+
+ this[kState] = []
+ }
+
+ append (name, value, filename = undefined) {
+ webidl.brandCheck(this, FormData)
+
+ webidl.argumentLengthCheck(arguments, 2, { header: 'FormData.append' })
+
+ if (arguments.length === 3 && !isBlobLike(value)) {
+ throw new TypeError(
+ "Failed to execute 'append' on 'FormData': parameter 2 is not of type 'Blob'"
+ )
+ }
+
+ // 1. Let value be value if given; otherwise blobValue.
+
+ name = webidl.converters.USVString(name)
+ value = isBlobLike(value)
+ ? webidl.converters.Blob(value, { strict: false })
+ : webidl.converters.USVString(value)
+ filename = arguments.length === 3
+ ? webidl.converters.USVString(filename)
+ : undefined
+
+ // 2. Let entry be the result of creating an entry with
+ // name, value, and filename if given.
+ const entry = makeEntry(name, value, filename)
+
+ // 3. Append entry to this’s entry list.
+ this[kState].push(entry)
+ }
+
+ delete (name) {
+ webidl.brandCheck(this, FormData)
+
+ webidl.argumentLengthCheck(arguments, 1, { header: 'FormData.delete' })
+
+ name = webidl.converters.USVString(name)
+
+ // The delete(name) method steps are to remove all entries whose name
+ // is name from this’s entry list.
+ this[kState] = this[kState].filter(entry => entry.name !== name)
+ }
+
+ get (name) {
+ webidl.brandCheck(this, FormData)
+
+ webidl.argumentLengthCheck(arguments, 1, { header: 'FormData.get' })
+
+ name = webidl.converters.USVString(name)
+
+ // 1. If there is no entry whose name is name in this’s entry list,
+ // then return null.
+ const idx = this[kState].findIndex((entry) => entry.name === name)
+ if (idx === -1) {
+ return null
+ }
+
+ // 2. Return the value of the first entry whose name is name from
+ // this’s entry list.
+ return this[kState][idx].value
+ }
+
+ getAll (name) {
+ webidl.brandCheck(this, FormData)
+
+ webidl.argumentLengthCheck(arguments, 1, { header: 'FormData.getAll' })
+
+ name = webidl.converters.USVString(name)
+
+ // 1. If there is no entry whose name is name in this’s entry list,
+ // then return the empty list.
+ // 2. Return the values of all entries whose name is name, in order,
+ // from this’s entry list.
+ return this[kState]
+ .filter((entry) => entry.name === name)
+ .map((entry) => entry.value)
+ }
+
+ has (name) {
+ webidl.brandCheck(this, FormData)
+
+ webidl.argumentLengthCheck(arguments, 1, { header: 'FormData.has' })
+
+ name = webidl.converters.USVString(name)
+
+ // The has(name) method steps are to return true if there is an entry
+ // whose name is name in this’s entry list; otherwise false.
+ return this[kState].findIndex((entry) => entry.name === name) !== -1
+ }
+
+ set (name, value, filename = undefined) {
+ webidl.brandCheck(this, FormData)
+
+ webidl.argumentLengthCheck(arguments, 2, { header: 'FormData.set' })
+
+ if (arguments.length === 3 && !isBlobLike(value)) {
+ throw new TypeError(
+ "Failed to execute 'set' on 'FormData': parameter 2 is not of type 'Blob'"
+ )
+ }
+
+ // The set(name, value) and set(name, blobValue, filename) method steps
+ // are:
+
+ // 1. Let value be value if given; otherwise blobValue.
+
+ name = webidl.converters.USVString(name)
+ value = isBlobLike(value)
+ ? webidl.converters.Blob(value, { strict: false })
+ : webidl.converters.USVString(value)
+ filename = arguments.length === 3
+ ? toUSVString(filename)
+ : undefined
+
+ // 2. Let entry be the result of creating an entry with name, value, and
+ // filename if given.
+ const entry = makeEntry(name, value, filename)
+
+ // 3. If there are entries in this’s entry list whose name is name, then
+ // replace the first such entry with entry and remove the others.
+ const idx = this[kState].findIndex((entry) => entry.name === name)
+ if (idx !== -1) {
+ this[kState] = [
+ ...this[kState].slice(0, idx),
+ entry,
+ ...this[kState].slice(idx + 1).filter((entry) => entry.name !== name)
+ ]
+ } else {
+ // 4. Otherwise, append entry to this’s entry list.
+ this[kState].push(entry)
+ }
+ }
+
+ entries () {
+ webidl.brandCheck(this, FormData)
+
+ return makeIterator(
+ () => this[kState].map(pair => [pair.name, pair.value]),
+ 'FormData',
+ 'key+value'
+ )
+ }
+
+ keys () {
+ webidl.brandCheck(this, FormData)
+
+ return makeIterator(
+ () => this[kState].map(pair => [pair.name, pair.value]),
+ 'FormData',
+ 'key'
+ )
+ }
+
+ values () {
+ webidl.brandCheck(this, FormData)
+
+ return makeIterator(
+ () => this[kState].map(pair => [pair.name, pair.value]),
+ 'FormData',
+ 'value'
+ )
+ }
+
+ /**
+ * @param {(value: string, key: string, self: FormData) => void} callbackFn
+ * @param {unknown} thisArg
+ */
+ forEach (callbackFn, thisArg = globalThis) {
+ webidl.brandCheck(this, FormData)
+
+ webidl.argumentLengthCheck(arguments, 1, { header: 'FormData.forEach' })
+
+ if (typeof callbackFn !== 'function') {
+ throw new TypeError(
+ "Failed to execute 'forEach' on 'FormData': parameter 1 is not of type 'Function'."
+ )
+ }
+
+ for (const [key, value] of this) {
+ callbackFn.apply(thisArg, [value, key, this])
+ }
+ }
+}
+
+FormData.prototype[Symbol.iterator] = FormData.prototype.entries
+
+Object.defineProperties(FormData.prototype, {
+ [Symbol.toStringTag]: {
+ value: 'FormData',
+ configurable: true
+ }
+})
+
+/**
+ * @see https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#create-an-entry
+ * @param {string} name
+ * @param {string|Blob} value
+ * @param {?string} filename
+ * @returns
+ */
+function makeEntry (name, value, filename) {
+ // 1. Set name to the result of converting name into a scalar value string.
+ // "To convert a string into a scalar value string, replace any surrogates
+ // with U+FFFD."
+ // see: https://nodejs.org/dist/latest-v18.x/docs/api/buffer.html#buftostringencoding-start-end
+ name = Buffer.from(name).toString('utf8')
+
+ // 2. If value is a string, then set value to the result of converting
+ // value into a scalar value string.
+ if (typeof value === 'string') {
+ value = Buffer.from(value).toString('utf8')
+ } else {
+ // 3. Otherwise:
+
+ // 1. If value is not a File object, then set value to a new File object,
+ // representing the same bytes, whose name attribute value is "blob"
+ if (!isFileLike(value)) {
+ value = value instanceof Blob
+ ? new File([value], 'blob', { type: value.type })
+ : new FileLike(value, 'blob', { type: value.type })
+ }
+
+ // 2. If filename is given, then set value to a new File object,
+ // representing the same bytes, whose name attribute is filename.
+ if (filename !== undefined) {
+ /** @type {FilePropertyBag} */
+ const options = {
+ type: value.type,
+ lastModified: value.lastModified
+ }
+
+ value = (NativeFile && value instanceof NativeFile) || value instanceof UndiciFile
+ ? new File([value], filename, options)
+ : new FileLike(value, filename, options)
+ }
+ }
+
+ // 4. Return an entry whose name is name and whose value is value.
+ return { name, value }
+}
+
+module.exports = { FormData }
diff --git a/lib/fetch/global.js b/lib/fetch/global.js
new file mode 100644
index 0000000..1df6f12
--- /dev/null
+++ b/lib/fetch/global.js
@@ -0,0 +1,40 @@
+'use strict'
+
+// In case of breaking changes, increase the version
+// number to avoid conflicts.
+const globalOrigin = Symbol.for('undici.globalOrigin.1')
+
+function getGlobalOrigin () {
+ return globalThis[globalOrigin]
+}
+
+function setGlobalOrigin (newOrigin) {
+ if (newOrigin === undefined) {
+ Object.defineProperty(globalThis, globalOrigin, {
+ value: undefined,
+ writable: true,
+ enumerable: false,
+ configurable: false
+ })
+
+ return
+ }
+
+ const parsedURL = new URL(newOrigin)
+
+ if (parsedURL.protocol !== 'http:' && parsedURL.protocol !== 'https:') {
+ throw new TypeError(`Only http & https urls are allowed, received ${parsedURL.protocol}`)
+ }
+
+ Object.defineProperty(globalThis, globalOrigin, {
+ value: parsedURL,
+ writable: true,
+ enumerable: false,
+ configurable: false
+ })
+}
+
+module.exports = {
+ getGlobalOrigin,
+ setGlobalOrigin
+}
diff --git a/lib/fetch/headers.js b/lib/fetch/headers.js
new file mode 100644
index 0000000..2f1c0be
--- /dev/null
+++ b/lib/fetch/headers.js
@@ -0,0 +1,589 @@
+// https://github.com/Ethan-Arrowood/undici-fetch
+
+'use strict'
+
+const { kHeadersList, kConstruct } = require('../core/symbols')
+const { kGuard } = require('./symbols')
+const { kEnumerableProperty } = require('../core/util')
+const {
+ makeIterator,
+ isValidHeaderName,
+ isValidHeaderValue
+} = require('./util')
+const { webidl } = require('./webidl')
+const assert = require('assert')
+
+const kHeadersMap = Symbol('headers map')
+const kHeadersSortedMap = Symbol('headers map sorted')
+
+/**
+ * @param {number} code
+ */
+function isHTTPWhiteSpaceCharCode (code) {
+ return code === 0x00a || code === 0x00d || code === 0x009 || code === 0x020
+}
+
+/**
+ * @see https://fetch.spec.whatwg.org/#concept-header-value-normalize
+ * @param {string} potentialValue
+ */
+function headerValueNormalize (potentialValue) {
+ // To normalize a byte sequence potentialValue, remove
+ // any leading and trailing HTTP whitespace bytes from
+ // potentialValue.
+ let i = 0; let j = potentialValue.length
+
+ while (j > i && isHTTPWhiteSpaceCharCode(potentialValue.charCodeAt(j - 1))) --j
+ while (j > i && isHTTPWhiteSpaceCharCode(potentialValue.charCodeAt(i))) ++i
+
+ return i === 0 && j === potentialValue.length ? potentialValue : potentialValue.substring(i, j)
+}
+
+function fill (headers, object) {
+ // To fill a Headers object headers with a given object object, run these steps:
+
+ // 1. If object is a sequence, then for each header in object:
+ // Note: webidl conversion to array has already been done.
+ if (Array.isArray(object)) {
+ for (let i = 0; i < object.length; ++i) {
+ const header = object[i]
+ // 1. If header does not contain exactly two items, then throw a TypeError.
+ if (header.length !== 2) {
+ throw webidl.errors.exception({
+ header: 'Headers constructor',
+ message: `expected name/value pair to be length 2, found ${header.length}.`
+ })
+ }
+
+ // 2. Append (header’s first item, header’s second item) to headers.
+ appendHeader(headers, header[0], header[1])
+ }
+ } else if (typeof object === 'object' && object !== null) {
+ // Note: null should throw
+
+ // 2. Otherwise, object is a record, then for each key → value in object,
+ // append (key, value) to headers
+ const keys = Object.keys(object)
+ for (let i = 0; i < keys.length; ++i) {
+ appendHeader(headers, keys[i], object[keys[i]])
+ }
+ } else {
+ throw webidl.errors.conversionFailed({
+ prefix: 'Headers constructor',
+ argument: 'Argument 1',
+ types: ['sequence<sequence<ByteString>>', 'record<ByteString, ByteString>']
+ })
+ }
+}
+
+/**
+ * @see https://fetch.spec.whatwg.org/#concept-headers-append
+ */
+function appendHeader (headers, name, value) {
+ // 1. Normalize value.
+ value = headerValueNormalize(value)
+
+ // 2. If name is not a header name or value is not a
+ // header value, then throw a TypeError.
+ if (!isValidHeaderName(name)) {
+ throw webidl.errors.invalidArgument({
+ prefix: 'Headers.append',
+ value: name,
+ type: 'header name'
+ })
+ } else if (!isValidHeaderValue(value)) {
+ throw webidl.errors.invalidArgument({
+ prefix: 'Headers.append',
+ value,
+ type: 'header value'
+ })
+ }
+
+ // 3. If headers’s guard is "immutable", then throw a TypeError.
+ // 4. Otherwise, if headers’s guard is "request" and name is a
+ // forbidden header name, return.
+ // Note: undici does not implement forbidden header names
+ if (headers[kGuard] === 'immutable') {
+ throw new TypeError('immutable')
+ } else if (headers[kGuard] === 'request-no-cors') {
+ // 5. Otherwise, if headers’s guard is "request-no-cors":
+ // TODO
+ }
+
+ // 6. Otherwise, if headers’s guard is "response" and name is a
+ // forbidden response-header name, return.
+
+ // 7. Append (name, value) to headers’s header list.
+ return headers[kHeadersList].append(name, value)
+
+ // 8. If headers’s guard is "request-no-cors", then remove
+ // privileged no-CORS request headers from headers
+}
+
+class HeadersList {
+ /** @type {[string, string][]|null} */
+ cookies = null
+
+ constructor (init) {
+ if (init instanceof HeadersList) {
+ this[kHeadersMap] = new Map(init[kHeadersMap])
+ this[kHeadersSortedMap] = init[kHeadersSortedMap]
+ this.cookies = init.cookies === null ? null : [...init.cookies]
+ } else {
+ this[kHeadersMap] = new Map(init)
+ this[kHeadersSortedMap] = null
+ }
+ }
+
+ // https://fetch.spec.whatwg.org/#header-list-contains
+ contains (name) {
+ // A header list list contains a header name name if list
+ // contains a header whose name is a byte-case-insensitive
+ // match for name.
+ name = name.toLowerCase()
+
+ return this[kHeadersMap].has(name)
+ }
+
+ clear () {
+ this[kHeadersMap].clear()
+ this[kHeadersSortedMap] = null
+ this.cookies = null
+ }
+
+ // https://fetch.spec.whatwg.org/#concept-header-list-append
+ append (name, value) {
+ this[kHeadersSortedMap] = null
+
+ // 1. If list contains name, then set name to the first such
+ // header’s name.
+ const lowercaseName = name.toLowerCase()
+ const exists = this[kHeadersMap].get(lowercaseName)
+
+ // 2. Append (name, value) to list.
+ if (exists) {
+ const delimiter = lowercaseName === 'cookie' ? '; ' : ', '
+ this[kHeadersMap].set(lowercaseName, {
+ name: exists.name,
+ value: `${exists.value}${delimiter}${value}`
+ })
+ } else {
+ this[kHeadersMap].set(lowercaseName, { name, value })
+ }
+
+ if (lowercaseName === 'set-cookie') {
+ this.cookies ??= []
+ this.cookies.push(value)
+ }
+ }
+
+ // https://fetch.spec.whatwg.org/#concept-header-list-set
+ set (name, value) {
+ this[kHeadersSortedMap] = null
+ const lowercaseName = name.toLowerCase()
+
+ if (lowercaseName === 'set-cookie') {
+ this.cookies = [value]
+ }
+
+ // 1. If list contains name, then set the value of
+ // the first such header to value and remove the
+ // others.
+ // 2. Otherwise, append header (name, value) to list.
+ this[kHeadersMap].set(lowercaseName, { name, value })
+ }
+
+ // https://fetch.spec.whatwg.org/#concept-header-list-delete
+ delete (name) {
+ this[kHeadersSortedMap] = null
+
+ name = name.toLowerCase()
+
+ if (name === 'set-cookie') {
+ this.cookies = null
+ }
+
+ this[kHeadersMap].delete(name)
+ }
+
+ // https://fetch.spec.whatwg.org/#concept-header-list-get
+ get (name) {
+ const value = this[kHeadersMap].get(name.toLowerCase())
+
+ // 1. If list does not contain name, then return null.
+ // 2. Return the values of all headers in list whose name
+ // is a byte-case-insensitive match for name,
+ // separated from each other by 0x2C 0x20, in order.
+ return value === undefined ? null : value.value
+ }
+
+ * [Symbol.iterator] () {
+ // use the lowercased name
+ for (const [name, { value }] of this[kHeadersMap]) {
+ yield [name, value]
+ }
+ }
+
+ get entries () {
+ const headers = {}
+
+ if (this[kHeadersMap].size) {
+ for (const { name, value } of this[kHeadersMap].values()) {
+ headers[name] = value
+ }
+ }
+
+ return headers
+ }
+}
+
+// https://fetch.spec.whatwg.org/#headers-class
+class Headers {
+ constructor (init = undefined) {
+ if (init === kConstruct) {
+ return
+ }
+ this[kHeadersList] = new HeadersList()
+
+ // The new Headers(init) constructor steps are:
+
+ // 1. Set this’s guard to "none".
+ this[kGuard] = 'none'
+
+ // 2. If init is given, then fill this with init.
+ if (init !== undefined) {
+ init = webidl.converters.HeadersInit(init)
+ fill(this, init)
+ }
+ }
+
+ // https://fetch.spec.whatwg.org/#dom-headers-append
+ append (name, value) {
+ webidl.brandCheck(this, Headers)
+
+ webidl.argumentLengthCheck(arguments, 2, { header: 'Headers.append' })
+
+ name = webidl.converters.ByteString(name)
+ value = webidl.converters.ByteString(value)
+
+ return appendHeader(this, name, value)
+ }
+
+ // https://fetch.spec.whatwg.org/#dom-headers-delete
+ delete (name) {
+ webidl.brandCheck(this, Headers)
+
+ webidl.argumentLengthCheck(arguments, 1, { header: 'Headers.delete' })
+
+ name = webidl.converters.ByteString(name)
+
+ // 1. If name is not a header name, then throw a TypeError.
+ if (!isValidHeaderName(name)) {
+ throw webidl.errors.invalidArgument({
+ prefix: 'Headers.delete',
+ value: name,
+ type: 'header name'
+ })
+ }
+
+ // 2. If this’s guard is "immutable", then throw a TypeError.
+ // 3. Otherwise, if this’s guard is "request" and name is a
+ // forbidden header name, return.
+ // 4. Otherwise, if this’s guard is "request-no-cors", name
+ // is not a no-CORS-safelisted request-header name, and
+ // name is not a privileged no-CORS request-header name,
+ // return.
+ // 5. Otherwise, if this’s guard is "response" and name is
+ // a forbidden response-header name, return.
+ // Note: undici does not implement forbidden header names
+ if (this[kGuard] === 'immutable') {
+ throw new TypeError('immutable')
+ } else if (this[kGuard] === 'request-no-cors') {
+ // TODO
+ }
+
+ // 6. If this’s header list does not contain name, then
+ // return.
+ if (!this[kHeadersList].contains(name)) {
+ return
+ }
+
+ // 7. Delete name from this’s header list.
+ // 8. If this’s guard is "request-no-cors", then remove
+ // privileged no-CORS request headers from this.
+ this[kHeadersList].delete(name)
+ }
+
+ // https://fetch.spec.whatwg.org/#dom-headers-get
+ get (name) {
+ webidl.brandCheck(this, Headers)
+
+ webidl.argumentLengthCheck(arguments, 1, { header: 'Headers.get' })
+
+ name = webidl.converters.ByteString(name)
+
+ // 1. If name is not a header name, then throw a TypeError.
+ if (!isValidHeaderName(name)) {
+ throw webidl.errors.invalidArgument({
+ prefix: 'Headers.get',
+ value: name,
+ type: 'header name'
+ })
+ }
+
+ // 2. Return the result of getting name from this’s header
+ // list.
+ return this[kHeadersList].get(name)
+ }
+
+ // https://fetch.spec.whatwg.org/#dom-headers-has
+ has (name) {
+ webidl.brandCheck(this, Headers)
+
+ webidl.argumentLengthCheck(arguments, 1, { header: 'Headers.has' })
+
+ name = webidl.converters.ByteString(name)
+
+ // 1. If name is not a header name, then throw a TypeError.
+ if (!isValidHeaderName(name)) {
+ throw webidl.errors.invalidArgument({
+ prefix: 'Headers.has',
+ value: name,
+ type: 'header name'
+ })
+ }
+
+ // 2. Return true if this’s header list contains name;
+ // otherwise false.
+ return this[kHeadersList].contains(name)
+ }
+
+ // https://fetch.spec.whatwg.org/#dom-headers-set
+ set (name, value) {
+ webidl.brandCheck(this, Headers)
+
+ webidl.argumentLengthCheck(arguments, 2, { header: 'Headers.set' })
+
+ name = webidl.converters.ByteString(name)
+ value = webidl.converters.ByteString(value)
+
+ // 1. Normalize value.
+ value = headerValueNormalize(value)
+
+ // 2. If name is not a header name or value is not a
+ // header value, then throw a TypeError.
+ if (!isValidHeaderName(name)) {
+ throw webidl.errors.invalidArgument({
+ prefix: 'Headers.set',
+ value: name,
+ type: 'header name'
+ })
+ } else if (!isValidHeaderValue(value)) {
+ throw webidl.errors.invalidArgument({
+ prefix: 'Headers.set',
+ value,
+ type: 'header value'
+ })
+ }
+
+ // 3. If this’s guard is "immutable", then throw a TypeError.
+ // 4. Otherwise, if this’s guard is "request" and name is a
+ // forbidden header name, return.
+ // 5. Otherwise, if this’s guard is "request-no-cors" and
+ // name/value is not a no-CORS-safelisted request-header,
+ // return.
+ // 6. Otherwise, if this’s guard is "response" and name is a
+ // forbidden response-header name, return.
+ // Note: undici does not implement forbidden header names
+ if (this[kGuard] === 'immutable') {
+ throw new TypeError('immutable')
+ } else if (this[kGuard] === 'request-no-cors') {
+ // TODO
+ }
+
+ // 7. Set (name, value) in this’s header list.
+ // 8. If this’s guard is "request-no-cors", then remove
+ // privileged no-CORS request headers from this
+ this[kHeadersList].set(name, value)
+ }
+
+ // https://fetch.spec.whatwg.org/#dom-headers-getsetcookie
+ getSetCookie () {
+ webidl.brandCheck(this, Headers)
+
+ // 1. If this’s header list does not contain `Set-Cookie`, then return « ».
+ // 2. Return the values of all headers in this’s header list whose name is
+ // a byte-case-insensitive match for `Set-Cookie`, in order.
+
+ const list = this[kHeadersList].cookies
+
+ if (list) {
+ return [...list]
+ }
+
+ return []
+ }
+
+ // https://fetch.spec.whatwg.org/#concept-header-list-sort-and-combine
+ get [kHeadersSortedMap] () {
+ if (this[kHeadersList][kHeadersSortedMap]) {
+ return this[kHeadersList][kHeadersSortedMap]
+ }
+
+ // 1. Let headers be an empty list of headers with the key being the name
+ // and value the value.
+ const headers = []
+
+ // 2. Let names be the result of convert header names to a sorted-lowercase
+ // set with all the names of the headers in list.
+ const names = [...this[kHeadersList]].sort((a, b) => a[0] < b[0] ? -1 : 1)
+ const cookies = this[kHeadersList].cookies
+
+ // 3. For each name of names:
+ for (let i = 0; i < names.length; ++i) {
+ const [name, value] = names[i]
+ // 1. If name is `set-cookie`, then:
+ if (name === 'set-cookie') {
+ // 1. Let values be a list of all values of headers in list whose name
+ // is a byte-case-insensitive match for name, in order.
+
+ // 2. For each value of values:
+ // 1. Append (name, value) to headers.
+ for (let j = 0; j < cookies.length; ++j) {
+ headers.push([name, cookies[j]])
+ }
+ } else {
+ // 2. Otherwise:
+
+ // 1. Let value be the result of getting name from list.
+
+ // 2. Assert: value is non-null.
+ assert(value !== null)
+
+ // 3. Append (name, value) to headers.
+ headers.push([name, value])
+ }
+ }
+
+ this[kHeadersList][kHeadersSortedMap] = headers
+
+ // 4. Return headers.
+ return headers
+ }
+
+ keys () {
+ webidl.brandCheck(this, Headers)
+
+ if (this[kGuard] === 'immutable') {
+ const value = this[kHeadersSortedMap]
+ return makeIterator(() => value, 'Headers',
+ 'key')
+ }
+
+ return makeIterator(
+ () => [...this[kHeadersSortedMap].values()],
+ 'Headers',
+ 'key'
+ )
+ }
+
+ values () {
+ webidl.brandCheck(this, Headers)
+
+ if (this[kGuard] === 'immutable') {
+ const value = this[kHeadersSortedMap]
+ return makeIterator(() => value, 'Headers',
+ 'value')
+ }
+
+ return makeIterator(
+ () => [...this[kHeadersSortedMap].values()],
+ 'Headers',
+ 'value'
+ )
+ }
+
+ entries () {
+ webidl.brandCheck(this, Headers)
+
+ if (this[kGuard] === 'immutable') {
+ const value = this[kHeadersSortedMap]
+ return makeIterator(() => value, 'Headers',
+ 'key+value')
+ }
+
+ return makeIterator(
+ () => [...this[kHeadersSortedMap].values()],
+ 'Headers',
+ 'key+value'
+ )
+ }
+
+ /**
+ * @param {(value: string, key: string, self: Headers) => void} callbackFn
+ * @param {unknown} thisArg
+ */
+ forEach (callbackFn, thisArg = globalThis) {
+ webidl.brandCheck(this, Headers)
+
+ webidl.argumentLengthCheck(arguments, 1, { header: 'Headers.forEach' })
+
+ if (typeof callbackFn !== 'function') {
+ throw new TypeError(
+ "Failed to execute 'forEach' on 'Headers': parameter 1 is not of type 'Function'."
+ )
+ }
+
+ for (const [key, value] of this) {
+ callbackFn.apply(thisArg, [value, key, this])
+ }
+ }
+
+ [Symbol.for('nodejs.util.inspect.custom')] () {
+ webidl.brandCheck(this, Headers)
+
+ return this[kHeadersList]
+ }
+}
+
+Headers.prototype[Symbol.iterator] = Headers.prototype.entries
+
+Object.defineProperties(Headers.prototype, {
+ append: kEnumerableProperty,
+ delete: kEnumerableProperty,
+ get: kEnumerableProperty,
+ has: kEnumerableProperty,
+ set: kEnumerableProperty,
+ getSetCookie: kEnumerableProperty,
+ keys: kEnumerableProperty,
+ values: kEnumerableProperty,
+ entries: kEnumerableProperty,
+ forEach: kEnumerableProperty,
+ [Symbol.iterator]: { enumerable: false },
+ [Symbol.toStringTag]: {
+ value: 'Headers',
+ configurable: true
+ }
+})
+
+webidl.converters.HeadersInit = function (V) {
+ if (webidl.util.Type(V) === 'Object') {
+ if (V[Symbol.iterator]) {
+ return webidl.converters['sequence<sequence<ByteString>>'](V)
+ }
+
+ return webidl.converters['record<ByteString, ByteString>'](V)
+ }
+
+ throw webidl.errors.conversionFailed({
+ prefix: 'Headers constructor',
+ argument: 'Argument 1',
+ types: ['sequence<sequence<ByteString>>', 'record<ByteString, ByteString>']
+ })
+}
+
+module.exports = {
+ fill,
+ Headers,
+ HeadersList
+}
diff --git a/lib/fetch/index.js b/lib/fetch/index.js
new file mode 100644
index 0000000..17c3d87
--- /dev/null
+++ b/lib/fetch/index.js
@@ -0,0 +1,2145 @@
+// https://github.com/Ethan-Arrowood/undici-fetch
+
+'use strict'
+
+const {
+ Response,
+ makeNetworkError,
+ makeAppropriateNetworkError,
+ filterResponse,
+ makeResponse
+} = require('./response')
+const { Headers } = require('./headers')
+const { Request, makeRequest } = require('./request')
+const zlib = require('zlib')
+const {
+ bytesMatch,
+ makePolicyContainer,
+ clonePolicyContainer,
+ requestBadPort,
+ TAOCheck,
+ appendRequestOriginHeader,
+ responseLocationURL,
+ requestCurrentURL,
+ setRequestReferrerPolicyOnRedirect,
+ tryUpgradeRequestToAPotentiallyTrustworthyURL,
+ createOpaqueTimingInfo,
+ appendFetchMetadata,
+ corsCheck,
+ crossOriginResourcePolicyCheck,
+ determineRequestsReferrer,
+ coarsenedSharedCurrentTime,
+ createDeferredPromise,
+ isBlobLike,
+ sameOrigin,
+ isCancelled,
+ isAborted,
+ isErrorLike,
+ fullyReadBody,
+ readableStreamClose,
+ isomorphicEncode,
+ urlIsLocal,
+ urlIsHttpHttpsScheme,
+ urlHasHttpsScheme
+} = require('./util')
+const { kState, kHeaders, kGuard, kRealm } = require('./symbols')
+const assert = require('assert')
+const { safelyExtractBody } = require('./body')
+const {
+ redirectStatusSet,
+ nullBodyStatus,
+ safeMethodsSet,
+ requestBodyHeader,
+ subresourceSet,
+ DOMException
+} = require('./constants')
+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 { TransformStream } = require('stream/web')
+const { getGlobalDispatcher } = require('../global')
+const { webidl } = require('./webidl')
+const { STATUS_CODES } = require('http')
+const GET_OR_HEAD = ['GET', 'HEAD']
+
+/** @type {import('buffer').resolveObjectURL} */
+let resolveObjectURL
+let ReadableStream = globalThis.ReadableStream
+
+class Fetch extends EE {
+ constructor (dispatcher) {
+ super()
+
+ this.dispatcher = dispatcher
+ this.connection = null
+ this.dump = false
+ this.state = 'ongoing'
+ // 2 terminated listeners get added per request,
+ // but only 1 gets removed. If there are 20 redirects,
+ // 21 listeners will be added.
+ // See https://github.com/nodejs/undici/issues/1711
+ // TODO (fix): Find and fix root cause for leaked listener.
+ this.setMaxListeners(21)
+ }
+
+ terminate (reason) {
+ if (this.state !== 'ongoing') {
+ return
+ }
+
+ this.state = 'terminated'
+ this.connection?.destroy(reason)
+ this.emit('terminated', reason)
+ }
+
+ // https://fetch.spec.whatwg.org/#fetch-controller-abort
+ abort (error) {
+ if (this.state !== 'ongoing') {
+ return
+ }
+
+ // 1. Set controller’s state to "aborted".
+ this.state = 'aborted'
+
+ // 2. Let fallbackError be an "AbortError" DOMException.
+ // 3. Set error to fallbackError if it is not given.
+ if (!error) {
+ error = new DOMException('The operation was aborted.', 'AbortError')
+ }
+
+ // 4. Let serializedError be StructuredSerialize(error).
+ // If that threw an exception, catch it, and let
+ // serializedError be StructuredSerialize(fallbackError).
+
+ // 5. Set controller’s serialized abort reason to serializedError.
+ this.serializedAbortReason = error
+
+ this.connection?.destroy(error)
+ this.emit('terminated', error)
+ }
+}
+
+// https://fetch.spec.whatwg.org/#fetch-method
+function fetch (input, init = {}) {
+ webidl.argumentLengthCheck(arguments, 1, { header: 'globalThis.fetch' })
+
+ // 1. Let p be a new promise.
+ const p = createDeferredPromise()
+
+ // 2. Let requestObject be the result of invoking the initial value of
+ // Request as constructor with input and init as arguments. If this throws
+ // an exception, reject p with it and return p.
+ let requestObject
+
+ try {
+ requestObject = new Request(input, init)
+ } catch (e) {
+ p.reject(e)
+ return p.promise
+ }
+
+ // 3. Let request be requestObject’s request.
+ const request = requestObject[kState]
+
+ // 4. If requestObject’s signal’s aborted flag is set, then:
+ if (requestObject.signal.aborted) {
+ // 1. Abort the fetch() call with p, request, null, and
+ // requestObject’s signal’s abort reason.
+ abortFetch(p, request, null, requestObject.signal.reason)
+
+ // 2. Return p.
+ return p.promise
+ }
+
+ // 5. Let globalObject be request’s client’s global object.
+ const globalObject = request.client.globalObject
+
+ // 6. If globalObject is a ServiceWorkerGlobalScope object, then set
+ // request’s service-workers mode to "none".
+ if (globalObject?.constructor?.name === 'ServiceWorkerGlobalScope') {
+ request.serviceWorkers = 'none'
+ }
+
+ // 7. Let responseObject be null.
+ let responseObject = null
+
+ // 8. Let relevantRealm be this’s relevant Realm.
+ const relevantRealm = null
+
+ // 9. Let locallyAborted be false.
+ let locallyAborted = false
+
+ // 10. Let controller be null.
+ let controller = null
+
+ // 11. Add the following abort steps to requestObject’s signal:
+ addAbortListener(
+ requestObject.signal,
+ () => {
+ // 1. Set locallyAborted to true.
+ locallyAborted = true
+
+ // 2. Assert: controller is non-null.
+ assert(controller != null)
+
+ // 3. Abort controller with requestObject’s signal’s abort reason.
+ controller.abort(requestObject.signal.reason)
+
+ // 4. Abort the fetch() call with p, request, responseObject,
+ // and requestObject’s signal’s abort reason.
+ abortFetch(p, request, responseObject, requestObject.signal.reason)
+ }
+ )
+
+ // 12. Let handleFetchDone given response response be to finalize and
+ // report timing with response, globalObject, and "fetch".
+ const handleFetchDone = (response) =>
+ finalizeAndReportTiming(response, 'fetch')
+
+ // 13. Set controller to the result of calling fetch given request,
+ // with processResponseEndOfBody set to handleFetchDone, and processResponse
+ // given response being these substeps:
+
+ const processResponse = (response) => {
+ // 1. If locallyAborted is true, terminate these substeps.
+ if (locallyAborted) {
+ return Promise.resolve()
+ }
+
+ // 2. If response’s aborted flag is set, then:
+ if (response.aborted) {
+ // 1. Let deserializedError be the result of deserialize a serialized
+ // abort reason given controller’s serialized abort reason and
+ // relevantRealm.
+
+ // 2. Abort the fetch() call with p, request, responseObject, and
+ // deserializedError.
+
+ abortFetch(p, request, responseObject, controller.serializedAbortReason)
+ return Promise.resolve()
+ }
+
+ // 3. If response is a network error, then reject p with a TypeError
+ // and terminate these substeps.
+ if (response.type === 'error') {
+ p.reject(
+ Object.assign(new TypeError('fetch failed'), { cause: response.error })
+ )
+ return Promise.resolve()
+ }
+
+ // 4. Set responseObject to the result of creating a Response object,
+ // given response, "immutable", and relevantRealm.
+ responseObject = new Response()
+ responseObject[kState] = response
+ responseObject[kRealm] = relevantRealm
+ responseObject[kHeaders][kHeadersList] = response.headersList
+ responseObject[kHeaders][kGuard] = 'immutable'
+ responseObject[kHeaders][kRealm] = relevantRealm
+
+ // 5. Resolve p with responseObject.
+ p.resolve(responseObject)
+ }
+
+ controller = fetching({
+ request,
+ processResponseEndOfBody: handleFetchDone,
+ processResponse,
+ dispatcher: init.dispatcher ?? getGlobalDispatcher() // undici
+ })
+
+ // 14. Return p.
+ return p.promise
+}
+
+// https://fetch.spec.whatwg.org/#finalize-and-report-timing
+function finalizeAndReportTiming (response, initiatorType = 'other') {
+ // 1. If response is an aborted network error, then return.
+ if (response.type === 'error' && response.aborted) {
+ return
+ }
+
+ // 2. If response’s URL list is null or empty, then return.
+ if (!response.urlList?.length) {
+ return
+ }
+
+ // 3. Let originalURL be response’s URL list[0].
+ const originalURL = response.urlList[0]
+
+ // 4. Let timingInfo be response’s timing info.
+ let timingInfo = response.timingInfo
+
+ // 5. Let cacheState be response’s cache state.
+ let cacheState = response.cacheState
+
+ // 6. If originalURL’s scheme is not an HTTP(S) scheme, then return.
+ if (!urlIsHttpHttpsScheme(originalURL)) {
+ return
+ }
+
+ // 7. If timingInfo is null, then return.
+ if (timingInfo === null) {
+ return
+ }
+
+ // 8. If response’s timing allow passed flag is not set, then:
+ if (!response.timingAllowPassed) {
+ // 1. Set timingInfo to a the result of creating an opaque timing info for timingInfo.
+ timingInfo = createOpaqueTimingInfo({
+ startTime: timingInfo.startTime
+ })
+
+ // 2. Set cacheState to the empty string.
+ cacheState = ''
+ }
+
+ // 9. Set timingInfo’s end time to the coarsened shared current time
+ // given global’s relevant settings object’s cross-origin isolated
+ // capability.
+ // TODO: given global’s relevant settings object’s cross-origin isolated
+ // capability?
+ timingInfo.endTime = coarsenedSharedCurrentTime()
+
+ // 10. Set response’s timing info to timingInfo.
+ response.timingInfo = timingInfo
+
+ // 11. Mark resource timing for timingInfo, originalURL, initiatorType,
+ // global, and cacheState.
+ markResourceTiming(
+ timingInfo,
+ originalURL,
+ initiatorType,
+ globalThis,
+ cacheState
+ )
+}
+
+// https://w3c.github.io/resource-timing/#dfn-mark-resource-timing
+function markResourceTiming (timingInfo, originalURL, initiatorType, globalThis, cacheState) {
+ if (nodeMajor > 18 || (nodeMajor === 18 && nodeMinor >= 2)) {
+ performance.markResourceTiming(timingInfo, originalURL.href, initiatorType, globalThis, cacheState)
+ }
+}
+
+// https://fetch.spec.whatwg.org/#abort-fetch
+function abortFetch (p, request, responseObject, error) {
+ // Note: AbortSignal.reason was added in node v17.2.0
+ // which would give us an undefined error to reject with.
+ // Remove this once node v16 is no longer supported.
+ if (!error) {
+ error = new DOMException('The operation was aborted.', 'AbortError')
+ }
+
+ // 1. Reject promise with error.
+ p.reject(error)
+
+ // 2. If request’s body is not null and is readable, then cancel request’s
+ // body with error.
+ if (request.body != null && isReadable(request.body?.stream)) {
+ request.body.stream.cancel(error).catch((err) => {
+ if (err.code === 'ERR_INVALID_STATE') {
+ // Node bug?
+ return
+ }
+ throw err
+ })
+ }
+
+ // 3. If responseObject is null, then return.
+ if (responseObject == null) {
+ return
+ }
+
+ // 4. Let response be responseObject’s response.
+ const response = responseObject[kState]
+
+ // 5. If response’s body is not null and is readable, then error response’s
+ // body with error.
+ if (response.body != null && isReadable(response.body?.stream)) {
+ response.body.stream.cancel(error).catch((err) => {
+ if (err.code === 'ERR_INVALID_STATE') {
+ // Node bug?
+ return
+ }
+ throw err
+ })
+ }
+}
+
+// https://fetch.spec.whatwg.org/#fetching
+function fetching ({
+ request,
+ processRequestBodyChunkLength,
+ processRequestEndOfBody,
+ processResponse,
+ processResponseEndOfBody,
+ processResponseConsumeBody,
+ useParallelQueue = false,
+ dispatcher // undici
+}) {
+ // 1. Let taskDestination be null.
+ let taskDestination = null
+
+ // 2. Let crossOriginIsolatedCapability be false.
+ let crossOriginIsolatedCapability = false
+
+ // 3. If request’s client is non-null, then:
+ if (request.client != null) {
+ // 1. Set taskDestination to request’s client’s global object.
+ taskDestination = request.client.globalObject
+
+ // 2. Set crossOriginIsolatedCapability to request’s client’s cross-origin
+ // isolated capability.
+ crossOriginIsolatedCapability =
+ request.client.crossOriginIsolatedCapability
+ }
+
+ // 4. If useParallelQueue is true, then set taskDestination to the result of
+ // starting a new parallel queue.
+ // TODO
+
+ // 5. Let timingInfo be a new fetch timing info whose start time and
+ // post-redirect start time are the coarsened shared current time given
+ // crossOriginIsolatedCapability.
+ const currenTime = coarsenedSharedCurrentTime(crossOriginIsolatedCapability)
+ const timingInfo = createOpaqueTimingInfo({
+ startTime: currenTime
+ })
+
+ // 6. Let fetchParams be a new fetch params whose
+ // request is request,
+ // timing info is timingInfo,
+ // process request body chunk length is processRequestBodyChunkLength,
+ // process request end-of-body is processRequestEndOfBody,
+ // process response is processResponse,
+ // process response consume body is processResponseConsumeBody,
+ // process response end-of-body is processResponseEndOfBody,
+ // task destination is taskDestination,
+ // and cross-origin isolated capability is crossOriginIsolatedCapability.
+ const fetchParams = {
+ controller: new Fetch(dispatcher),
+ request,
+ timingInfo,
+ processRequestBodyChunkLength,
+ processRequestEndOfBody,
+ processResponse,
+ processResponseConsumeBody,
+ processResponseEndOfBody,
+ taskDestination,
+ crossOriginIsolatedCapability
+ }
+
+ // 7. If request’s body is a byte sequence, then set request’s body to
+ // request’s body as a body.
+ // NOTE: Since fetching is only called from fetch, body should already be
+ // extracted.
+ assert(!request.body || request.body.stream)
+
+ // 8. If request’s window is "client", then set request’s window to request’s
+ // client, if request’s client’s global object is a Window object; otherwise
+ // "no-window".
+ if (request.window === 'client') {
+ // TODO: What if request.client is null?
+ request.window =
+ request.client?.globalObject?.constructor?.name === 'Window'
+ ? request.client
+ : 'no-window'
+ }
+
+ // 9. If request’s origin is "client", then set request’s origin to request’s
+ // client’s origin.
+ if (request.origin === 'client') {
+ // TODO: What if request.client is null?
+ request.origin = request.client?.origin
+ }
+
+ // 10. If all of the following conditions are true:
+ // TODO
+
+ // 11. If request’s policy container is "client", then:
+ if (request.policyContainer === 'client') {
+ // 1. If request’s client is non-null, then set request’s policy
+ // container to a clone of request’s client’s policy container. [HTML]
+ if (request.client != null) {
+ request.policyContainer = clonePolicyContainer(
+ request.client.policyContainer
+ )
+ } else {
+ // 2. Otherwise, set request’s policy container to a new policy
+ // container.
+ request.policyContainer = makePolicyContainer()
+ }
+ }
+
+ // 12. If request’s header list does not contain `Accept`, then:
+ if (!request.headersList.contains('accept')) {
+ // 1. Let value be `*/*`.
+ const value = '*/*'
+
+ // 2. A user agent should set value to the first matching statement, if
+ // any, switching on request’s destination:
+ // "document"
+ // "frame"
+ // "iframe"
+ // `text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8`
+ // "image"
+ // `image/png,image/svg+xml,image/*;q=0.8,*/*;q=0.5`
+ // "style"
+ // `text/css,*/*;q=0.1`
+ // TODO
+
+ // 3. Append `Accept`/value to request’s header list.
+ request.headersList.append('accept', value)
+ }
+
+ // 13. If request’s header list does not contain `Accept-Language`, then
+ // user agents should append `Accept-Language`/an appropriate value to
+ // request’s header list.
+ if (!request.headersList.contains('accept-language')) {
+ request.headersList.append('accept-language', '*')
+ }
+
+ // 14. If request’s priority is null, then use request’s initiator and
+ // destination appropriately in setting request’s priority to a
+ // user-agent-defined object.
+ if (request.priority === null) {
+ // TODO
+ }
+
+ // 15. If request is a subresource request, then:
+ if (subresourceSet.has(request.destination)) {
+ // TODO
+ }
+
+ // 16. Run main fetch given fetchParams.
+ mainFetch(fetchParams)
+ .catch(err => {
+ fetchParams.controller.terminate(err)
+ })
+
+ // 17. Return fetchParam's controller
+ return fetchParams.controller
+}
+
+// https://fetch.spec.whatwg.org/#concept-main-fetch
+async function mainFetch (fetchParams, recursive = false) {
+ // 1. Let request be fetchParams’s request.
+ const request = fetchParams.request
+
+ // 2. Let response be null.
+ let response = null
+
+ // 3. If request’s local-URLs-only flag is set and request’s current URL is
+ // not local, then set response to a network error.
+ if (request.localURLsOnly && !urlIsLocal(requestCurrentURL(request))) {
+ response = makeNetworkError('local URLs only')
+ }
+
+ // 4. Run report Content Security Policy violations for request.
+ // TODO
+
+ // 5. Upgrade request to a potentially trustworthy URL, if appropriate.
+ tryUpgradeRequestToAPotentiallyTrustworthyURL(request)
+
+ // 6. If should request be blocked due to a bad port, should fetching request
+ // be blocked as mixed content, or should request be blocked by Content
+ // Security Policy returns blocked, then set response to a network error.
+ if (requestBadPort(request) === 'blocked') {
+ response = makeNetworkError('bad port')
+ }
+ // TODO: should fetching request be blocked as mixed content?
+ // TODO: should request be blocked by Content Security Policy?
+
+ // 7. If request’s referrer policy is the empty string, then set request’s
+ // referrer policy to request’s policy container’s referrer policy.
+ if (request.referrerPolicy === '') {
+ request.referrerPolicy = request.policyContainer.referrerPolicy
+ }
+
+ // 8. If request’s referrer is not "no-referrer", then set request’s
+ // referrer to the result of invoking determine request’s referrer.
+ if (request.referrer !== 'no-referrer') {
+ request.referrer = determineRequestsReferrer(request)
+ }
+
+ // 9. Set request’s current URL’s scheme to "https" if all of the following
+ // conditions are true:
+ // - request’s current URL’s scheme is "http"
+ // - request’s current URL’s host is a domain
+ // - Matching request’s current URL’s host per Known HSTS Host Domain Name
+ // Matching results in either a superdomain match with an asserted
+ // includeSubDomains directive or a congruent match (with or without an
+ // asserted includeSubDomains directive). [HSTS]
+ // TODO
+
+ // 10. If recursive is false, then run the remaining steps in parallel.
+ // TODO
+
+ // 11. If response is null, then set response to the result of running
+ // the steps corresponding to the first matching statement:
+ if (response === null) {
+ response = await (async () => {
+ const currentURL = requestCurrentURL(request)
+
+ if (
+ // - request’s current URL’s origin is same origin with request’s origin,
+ // and request’s response tainting is "basic"
+ (sameOrigin(currentURL, request.url) && request.responseTainting === 'basic') ||
+ // request’s current URL’s scheme is "data"
+ (currentURL.protocol === 'data:') ||
+ // - request’s mode is "navigate" or "websocket"
+ (request.mode === 'navigate' || request.mode === 'websocket')
+ ) {
+ // 1. Set request’s response tainting to "basic".
+ request.responseTainting = 'basic'
+
+ // 2. Return the result of running scheme fetch given fetchParams.
+ return await schemeFetch(fetchParams)
+ }
+
+ // request’s mode is "same-origin"
+ if (request.mode === 'same-origin') {
+ // 1. Return a network error.
+ return makeNetworkError('request mode cannot be "same-origin"')
+ }
+
+ // request’s mode is "no-cors"
+ if (request.mode === 'no-cors') {
+ // 1. If request’s redirect mode is not "follow", then return a network
+ // error.
+ if (request.redirect !== 'follow') {
+ return makeNetworkError(
+ 'redirect mode cannot be "follow" for "no-cors" request'
+ )
+ }
+
+ // 2. Set request’s response tainting to "opaque".
+ request.responseTainting = 'opaque'
+
+ // 3. Return the result of running scheme fetch given fetchParams.
+ return await schemeFetch(fetchParams)
+ }
+
+ // request’s current URL’s scheme is not an HTTP(S) scheme
+ if (!urlIsHttpHttpsScheme(requestCurrentURL(request))) {
+ // Return a network error.
+ return makeNetworkError('URL scheme must be a HTTP(S) scheme')
+ }
+
+ // - request’s use-CORS-preflight flag is set
+ // - request’s unsafe-request flag is set and either request’s method is
+ // not a CORS-safelisted method or CORS-unsafe request-header names with
+ // request’s header list is not empty
+ // 1. Set request’s response tainting to "cors".
+ // 2. Let corsWithPreflightResponse be the result of running HTTP fetch
+ // given fetchParams and true.
+ // 3. If corsWithPreflightResponse is a network error, then clear cache
+ // entries using request.
+ // 4. Return corsWithPreflightResponse.
+ // TODO
+
+ // Otherwise
+ // 1. Set request’s response tainting to "cors".
+ request.responseTainting = 'cors'
+
+ // 2. Return the result of running HTTP fetch given fetchParams.
+ return await httpFetch(fetchParams)
+ })()
+ }
+
+ // 12. If recursive is true, then return response.
+ if (recursive) {
+ return response
+ }
+
+ // 13. If response is not a network error and response is not a filtered
+ // response, then:
+ if (response.status !== 0 && !response.internalResponse) {
+ // If request’s response tainting is "cors", then:
+ if (request.responseTainting === 'cors') {
+ // 1. Let headerNames be the result of extracting header list values
+ // given `Access-Control-Expose-Headers` and response’s header list.
+ // TODO
+ // 2. If request’s credentials mode is not "include" and headerNames
+ // contains `*`, then set response’s CORS-exposed header-name list to
+ // all unique header names in response’s header list.
+ // TODO
+ // 3. Otherwise, if headerNames is not null or failure, then set
+ // response’s CORS-exposed header-name list to headerNames.
+ // TODO
+ }
+
+ // Set response to the following filtered response with response as its
+ // internal response, depending on request’s response tainting:
+ if (request.responseTainting === 'basic') {
+ response = filterResponse(response, 'basic')
+ } else if (request.responseTainting === 'cors') {
+ response = filterResponse(response, 'cors')
+ } else if (request.responseTainting === 'opaque') {
+ response = filterResponse(response, 'opaque')
+ } else {
+ assert(false)
+ }
+ }
+
+ // 14. Let internalResponse be response, if response is a network error,
+ // and response’s internal response otherwise.
+ let internalResponse =
+ response.status === 0 ? response : response.internalResponse
+
+ // 15. If internalResponse’s URL list is empty, then set it to a clone of
+ // request’s URL list.
+ if (internalResponse.urlList.length === 0) {
+ internalResponse.urlList.push(...request.urlList)
+ }
+
+ // 16. If request’s timing allow failed flag is unset, then set
+ // internalResponse’s timing allow passed flag.
+ if (!request.timingAllowFailed) {
+ response.timingAllowPassed = true
+ }
+
+ // 17. If response is not a network error and any of the following returns
+ // blocked
+ // - should internalResponse to request be blocked as mixed content
+ // - should internalResponse to request be blocked by Content Security Policy
+ // - should internalResponse to request be blocked due to its MIME type
+ // - should internalResponse to request be blocked due to nosniff
+ // TODO
+
+ // 18. If response’s type is "opaque", internalResponse’s status is 206,
+ // internalResponse’s range-requested flag is set, and request’s header
+ // list does not contain `Range`, then set response and internalResponse
+ // to a network error.
+ if (
+ response.type === 'opaque' &&
+ internalResponse.status === 206 &&
+ internalResponse.rangeRequested &&
+ !request.headers.contains('range')
+ ) {
+ response = internalResponse = makeNetworkError()
+ }
+
+ // 19. If response is not a network error and either request’s method is
+ // `HEAD` or `CONNECT`, or internalResponse’s status is a null body status,
+ // set internalResponse’s body to null and disregard any enqueuing toward
+ // it (if any).
+ if (
+ response.status !== 0 &&
+ (request.method === 'HEAD' ||
+ request.method === 'CONNECT' ||
+ nullBodyStatus.includes(internalResponse.status))
+ ) {
+ internalResponse.body = null
+ fetchParams.controller.dump = true
+ }
+
+ // 20. If request’s integrity metadata is not the empty string, then:
+ if (request.integrity) {
+ // 1. Let processBodyError be this step: run fetch finale given fetchParams
+ // and a network error.
+ const processBodyError = (reason) =>
+ fetchFinale(fetchParams, makeNetworkError(reason))
+
+ // 2. If request’s response tainting is "opaque", or response’s body is null,
+ // then run processBodyError and abort these steps.
+ if (request.responseTainting === 'opaque' || response.body == null) {
+ processBodyError(response.error)
+ return
+ }
+
+ // 3. Let processBody given bytes be these steps:
+ const processBody = (bytes) => {
+ // 1. If bytes do not match request’s integrity metadata,
+ // then run processBodyError and abort these steps. [SRI]
+ if (!bytesMatch(bytes, request.integrity)) {
+ processBodyError('integrity mismatch')
+ return
+ }
+
+ // 2. Set response’s body to bytes as a body.
+ response.body = safelyExtractBody(bytes)[0]
+
+ // 3. Run fetch finale given fetchParams and response.
+ fetchFinale(fetchParams, response)
+ }
+
+ // 4. Fully read response’s body given processBody and processBodyError.
+ await fullyReadBody(response.body, processBody, processBodyError)
+ } else {
+ // 21. Otherwise, run fetch finale given fetchParams and response.
+ fetchFinale(fetchParams, response)
+ }
+}
+
+// https://fetch.spec.whatwg.org/#concept-scheme-fetch
+// given a fetch params fetchParams
+function schemeFetch (fetchParams) {
+ // Note: since the connection is destroyed on redirect, which sets fetchParams to a
+ // cancelled state, we do not want this condition to trigger *unless* there have been
+ // no redirects. See https://github.com/nodejs/undici/issues/1776
+ // 1. If fetchParams is canceled, then return the appropriate network error for fetchParams.
+ if (isCancelled(fetchParams) && fetchParams.request.redirectCount === 0) {
+ return Promise.resolve(makeAppropriateNetworkError(fetchParams))
+ }
+
+ // 2. Let request be fetchParams’s request.
+ const { request } = fetchParams
+
+ const { protocol: scheme } = requestCurrentURL(request)
+
+ // 3. Switch on request’s current URL’s scheme and run the associated steps:
+ switch (scheme) {
+ case 'about:': {
+ // If request’s current URL’s path is the string "blank", then return a new response
+ // whose status message is `OK`, header list is « (`Content-Type`, `text/html;charset=utf-8`) »,
+ // and body is the empty byte sequence as a body.
+
+ // Otherwise, return a network error.
+ return Promise.resolve(makeNetworkError('about scheme is not supported'))
+ }
+ case 'blob:': {
+ if (!resolveObjectURL) {
+ resolveObjectURL = require('buffer').resolveObjectURL
+ }
+
+ // 1. Let blobURLEntry be request’s current URL’s blob URL entry.
+ const blobURLEntry = requestCurrentURL(request)
+
+ // https://github.com/web-platform-tests/wpt/blob/7b0ebaccc62b566a1965396e5be7bb2bc06f841f/FileAPI/url/resources/fetch-tests.js#L52-L56
+ // Buffer.resolveObjectURL does not ignore URL queries.
+ if (blobURLEntry.search.length !== 0) {
+ return Promise.resolve(makeNetworkError('NetworkError when attempting to fetch resource.'))
+ }
+
+ const blobURLEntryObject = 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)) {
+ return Promise.resolve(makeNetworkError('invalid method'))
+ }
+
+ // 3. Let bodyWithType be the result of safely extracting blobURLEntry’s object.
+ const bodyWithType = safelyExtractBody(blobURLEntryObject)
+
+ // 4. Let body be bodyWithType’s body.
+ const body = bodyWithType[0]
+
+ // 5. Let length be body’s length, serialized and isomorphic encoded.
+ const length = isomorphicEncode(`${body.length}`)
+
+ // 6. Let type be bodyWithType’s type if it is non-null; otherwise the empty byte sequence.
+ const type = bodyWithType[1] ?? ''
+
+ // 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 }]
+ ]
+ })
+
+ response.body = body
+
+ return Promise.resolve(response)
+ }
+ case 'data:': {
+ // 1. Let dataURLStruct be the result of running the
+ // data: URL processor on request’s current URL.
+ const currentURL = requestCurrentURL(request)
+ const dataURLStruct = dataURLProcessor(currentURL)
+
+ // 2. If dataURLStruct is failure, then return a
+ // network error.
+ if (dataURLStruct === 'failure') {
+ return Promise.resolve(makeNetworkError('failed to fetch the data URL'))
+ }
+
+ // 3. Let mimeType be dataURLStruct’s MIME type, serialized.
+ const mimeType = serializeAMimeType(dataURLStruct.mimeType)
+
+ // 4. Return a response whose status message is `OK`,
+ // header list is « (`Content-Type`, mimeType) »,
+ // and body is dataURLStruct’s body as a body.
+ return Promise.resolve(makeResponse({
+ statusText: 'OK',
+ headersList: [
+ ['content-type', { name: 'Content-Type', value: mimeType }]
+ ],
+ body: safelyExtractBody(dataURLStruct.body)[0]
+ }))
+ }
+ case 'file:': {
+ // For now, unfortunate as it is, file URLs are left as an exercise for the reader.
+ // When in doubt, return a network error.
+ return Promise.resolve(makeNetworkError('not implemented... yet...'))
+ }
+ case 'http:':
+ case 'https:': {
+ // Return the result of running HTTP fetch given fetchParams.
+
+ return httpFetch(fetchParams)
+ .catch((err) => makeNetworkError(err))
+ }
+ default: {
+ return Promise.resolve(makeNetworkError('unknown scheme'))
+ }
+ }
+}
+
+// https://fetch.spec.whatwg.org/#finalize-response
+function finalizeResponse (fetchParams, response) {
+ // 1. Set fetchParams’s request’s done flag.
+ fetchParams.request.done = true
+
+ // 2, If fetchParams’s process response done is not null, then queue a fetch
+ // task to run fetchParams’s process response done given response, with
+ // fetchParams’s task destination.
+ if (fetchParams.processResponseDone != null) {
+ queueMicrotask(() => fetchParams.processResponseDone(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
+ })
+ }
+
+ // 2. 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))
+ }
+ }
+
+ // 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.
+ if (fetchParams.processResponse != null) {
+ queueMicrotask(() => fetchParams.processResponse(response))
+ }
+
+ // 4. If response’s body is null, then run processResponseEndOfBody.
+ if (response.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.
+ const transformStream = new TransformStream({
+ start () {},
+ transform: identityTransformAlgorithm,
+ 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) }
+ }
+
+ // 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)
+
+ // 2. Let processBodyError be this step: run fetchParams’s process
+ // response consume body given response and failure.
+ const processBodyError = (failure) => fetchParams.processResponseConsumeBody(response, failure)
+
+ // 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()
+ }
+}
+
+// https://fetch.spec.whatwg.org/#http-fetch
+async function httpFetch (fetchParams) {
+ // 1. Let request be fetchParams’s request.
+ const request = fetchParams.request
+
+ // 2. Let response be null.
+ let response = null
+
+ // 3. Let actualResponse be null.
+ let actualResponse = null
+
+ // 4. Let timingInfo be fetchParams’s timing info.
+ const timingInfo = fetchParams.timingInfo
+
+ // 5. If request’s service-workers mode is "all", then:
+ if (request.serviceWorkers === 'all') {
+ // TODO
+ }
+
+ // 6. If response is null, then:
+ if (response === null) {
+ // 1. If makeCORSPreflight is true and one of these conditions is true:
+ // TODO
+
+ // 2. If request’s redirect mode is "follow", then set request’s
+ // service-workers mode to "none".
+ if (request.redirect === 'follow') {
+ request.serviceWorkers = 'none'
+ }
+
+ // 3. Set response and actualResponse to the result of running
+ // HTTP-network-or-cache fetch given fetchParams.
+ actualResponse = response = await httpNetworkOrCacheFetch(fetchParams)
+
+ // 4. If request’s response tainting is "cors" and a CORS check
+ // for request and response returns failure, then return a network error.
+ if (
+ request.responseTainting === 'cors' &&
+ corsCheck(request, response) === 'failure'
+ ) {
+ return makeNetworkError('cors failure')
+ }
+
+ // 5. If the TAO check for request and response returns failure, then set
+ // request’s timing allow failed flag.
+ if (TAOCheck(request, response) === 'failure') {
+ request.timingAllowFailed = true
+ }
+ }
+
+ // 7. If either request’s response tainting or response’s type
+ // is "opaque", and the cross-origin resource policy check with
+ // request’s origin, request’s client, request’s destination,
+ // and actualResponse returns blocked, then return a network error.
+ if (
+ (request.responseTainting === 'opaque' || response.type === 'opaque') &&
+ crossOriginResourcePolicyCheck(
+ request.origin,
+ request.client,
+ request.destination,
+ actualResponse
+ ) === 'blocked'
+ ) {
+ return makeNetworkError('blocked')
+ }
+
+ // 8. If actualResponse’s status is a redirect status, then:
+ if (redirectStatusSet.has(actualResponse.status)) {
+ // 1. If actualResponse’s status is not 303, request’s body is not null,
+ // and the connection uses HTTP/2, then user agents may, and are even
+ // encouraged to, transmit an RST_STREAM frame.
+ // See, https://github.com/whatwg/fetch/issues/1288
+ if (request.redirect !== 'manual') {
+ fetchParams.controller.connection.destroy()
+ }
+
+ // 2. Switch on request’s redirect mode:
+ if (request.redirect === 'error') {
+ // Set response to a network error.
+ response = makeNetworkError('unexpected redirect')
+ } else if (request.redirect === 'manual') {
+ // Set response to an opaque-redirect filtered response whose internal
+ // response is actualResponse.
+ // NOTE(spec): On the web this would return an `opaqueredirect` response,
+ // but that doesn't make sense server side.
+ // See https://github.com/nodejs/undici/issues/1193.
+ response = actualResponse
+ } else if (request.redirect === 'follow') {
+ // Set response to the result of running HTTP-redirect fetch given
+ // fetchParams and response.
+ response = await httpRedirectFetch(fetchParams, response)
+ } else {
+ assert(false)
+ }
+ }
+
+ // 9. Set response’s timing info to timingInfo.
+ response.timingInfo = timingInfo
+
+ // 10. Return response.
+ return response
+}
+
+// https://fetch.spec.whatwg.org/#http-redirect-fetch
+function httpRedirectFetch (fetchParams, response) {
+ // 1. Let request be fetchParams’s request.
+ const request = fetchParams.request
+
+ // 2. Let actualResponse be response, if response is not a filtered response,
+ // and response’s internal response otherwise.
+ const actualResponse = response.internalResponse
+ ? response.internalResponse
+ : response
+
+ // 3. Let locationURL be actualResponse’s location URL given request’s current
+ // URL’s fragment.
+ let locationURL
+
+ try {
+ locationURL = responseLocationURL(
+ actualResponse,
+ requestCurrentURL(request).hash
+ )
+
+ // 4. If locationURL is null, then return response.
+ if (locationURL == null) {
+ return response
+ }
+ } catch (err) {
+ // 5. If locationURL is failure, then return a network error.
+ return Promise.resolve(makeNetworkError(err))
+ }
+
+ // 6. If locationURL’s scheme is not an HTTP(S) scheme, then return a network
+ // error.
+ if (!urlIsHttpHttpsScheme(locationURL)) {
+ return Promise.resolve(makeNetworkError('URL scheme must be a HTTP(S) scheme'))
+ }
+
+ // 7. If request’s redirect count is 20, then return a network error.
+ if (request.redirectCount === 20) {
+ return Promise.resolve(makeNetworkError('redirect count exceeded'))
+ }
+
+ // 8. Increase request’s redirect count by 1.
+ request.redirectCount += 1
+
+ // 9. If request’s mode is "cors", locationURL includes credentials, and
+ // request’s origin is not same origin with locationURL’s origin, then return
+ // a network error.
+ if (
+ request.mode === 'cors' &&
+ (locationURL.username || locationURL.password) &&
+ !sameOrigin(request, locationURL)
+ ) {
+ return Promise.resolve(makeNetworkError('cross origin not allowed for request mode "cors"'))
+ }
+
+ // 10. If request’s response tainting is "cors" and locationURL includes
+ // credentials, then return a network error.
+ if (
+ request.responseTainting === 'cors' &&
+ (locationURL.username || locationURL.password)
+ ) {
+ return Promise.resolve(makeNetworkError(
+ 'URL cannot contain credentials for request mode "cors"'
+ ))
+ }
+
+ // 11. If actualResponse’s status is not 303, request’s body is non-null,
+ // and request’s body’s source is null, then return a network error.
+ if (
+ actualResponse.status !== 303 &&
+ request.body != null &&
+ request.body.source == null
+ ) {
+ return Promise.resolve(makeNetworkError())
+ }
+
+ // 12. If one of the following is true
+ // - actualResponse’s status is 301 or 302 and request’s method is `POST`
+ // - actualResponse’s status is 303 and request’s method is not `GET` or `HEAD`
+ if (
+ ([301, 302].includes(actualResponse.status) && request.method === 'POST') ||
+ (actualResponse.status === 303 &&
+ !GET_OR_HEAD.includes(request.method))
+ ) {
+ // then:
+ // 1. Set request’s method to `GET` and request’s body to null.
+ request.method = 'GET'
+ request.body = null
+
+ // 2. For each headerName of request-body-header name, delete headerName from
+ // request’s header list.
+ for (const headerName of requestBodyHeader) {
+ request.headersList.delete(headerName)
+ }
+ }
+
+ // 13. If request’s current URL’s origin is not same origin with locationURL’s
+ // origin, then for each headerName of CORS non-wildcard request-header name,
+ // delete headerName from request’s header list.
+ if (!sameOrigin(requestCurrentURL(request), locationURL)) {
+ // https://fetch.spec.whatwg.org/#cors-non-wildcard-request-header-name
+ request.headersList.delete('authorization')
+
+ // "Cookie" and "Host" are forbidden request-headers, which undici doesn't implement.
+ request.headersList.delete('cookie')
+ request.headersList.delete('host')
+ }
+
+ // 14. If request’s body is non-null, then set request’s body to the first return
+ // value of safely extracting request’s body’s source.
+ if (request.body != null) {
+ assert(request.body.source != null)
+ request.body = safelyExtractBody(request.body.source)[0]
+ }
+
+ // 15. Let timingInfo be fetchParams’s timing info.
+ const timingInfo = fetchParams.timingInfo
+
+ // 16. Set timingInfo’s redirect end time and post-redirect start time to the
+ // coarsened shared current time given fetchParams’s cross-origin isolated
+ // capability.
+ timingInfo.redirectEndTime = timingInfo.postRedirectStartTime =
+ coarsenedSharedCurrentTime(fetchParams.crossOriginIsolatedCapability)
+
+ // 17. If timingInfo’s redirect start time is 0, then set timingInfo’s
+ // redirect start time to timingInfo’s start time.
+ if (timingInfo.redirectStartTime === 0) {
+ timingInfo.redirectStartTime = timingInfo.startTime
+ }
+
+ // 18. Append locationURL to request’s URL list.
+ request.urlList.push(locationURL)
+
+ // 19. Invoke set request’s referrer policy on redirect on request and
+ // actualResponse.
+ setRequestReferrerPolicyOnRedirect(request, actualResponse)
+
+ // 20. Return the result of running main fetch given fetchParams and true.
+ return mainFetch(fetchParams, true)
+}
+
+// https://fetch.spec.whatwg.org/#http-network-or-cache-fetch
+async function httpNetworkOrCacheFetch (
+ fetchParams,
+ isAuthenticationFetch = false,
+ isNewConnectionFetch = false
+) {
+ // 1. Let request be fetchParams’s request.
+ const request = fetchParams.request
+
+ // 2. Let httpFetchParams be null.
+ let httpFetchParams = null
+
+ // 3. Let httpRequest be null.
+ let httpRequest = null
+
+ // 4. Let response be null.
+ let response = null
+
+ // 5. Let storedResponse be null.
+ // TODO: cache
+
+ // 6. Let httpCache be null.
+ const httpCache = null
+
+ // 7. Let the revalidatingFlag be unset.
+ const revalidatingFlag = false
+
+ // 8. Run these steps, but abort when the ongoing fetch is terminated:
+
+ // 1. If request’s window is "no-window" and request’s redirect mode is
+ // "error", then set httpFetchParams to fetchParams and httpRequest to
+ // request.
+ if (request.window === 'no-window' && request.redirect === 'error') {
+ httpFetchParams = fetchParams
+ httpRequest = request
+ } else {
+ // Otherwise:
+
+ // 1. Set httpRequest to a clone of request.
+ httpRequest = makeRequest(request)
+
+ // 2. Set httpFetchParams to a copy of fetchParams.
+ httpFetchParams = { ...fetchParams }
+
+ // 3. Set httpFetchParams’s request to httpRequest.
+ httpFetchParams.request = httpRequest
+ }
+
+ // 3. Let includeCredentials be true if one of
+ const includeCredentials =
+ request.credentials === 'include' ||
+ (request.credentials === 'same-origin' &&
+ request.responseTainting === 'basic')
+
+ // 4. Let contentLength be httpRequest’s body’s length, if httpRequest’s
+ // body is non-null; otherwise null.
+ const contentLength = httpRequest.body ? httpRequest.body.length : null
+
+ // 5. Let contentLengthHeaderValue be null.
+ let contentLengthHeaderValue = null
+
+ // 6. If httpRequest’s body is null and httpRequest’s method is `POST` or
+ // `PUT`, then set contentLengthHeaderValue to `0`.
+ if (
+ httpRequest.body == null &&
+ ['POST', 'PUT'].includes(httpRequest.method)
+ ) {
+ contentLengthHeaderValue = '0'
+ }
+
+ // 7. If contentLength is non-null, then set contentLengthHeaderValue to
+ // contentLength, serialized and isomorphic encoded.
+ if (contentLength != null) {
+ contentLengthHeaderValue = isomorphicEncode(`${contentLength}`)
+ }
+
+ // 8. If contentLengthHeaderValue is non-null, then append
+ // `Content-Length`/contentLengthHeaderValue to httpRequest’s header
+ // list.
+ if (contentLengthHeaderValue != null) {
+ httpRequest.headersList.append('content-length', contentLengthHeaderValue)
+ }
+
+ // 9. If contentLengthHeaderValue is non-null, then append (`Content-Length`,
+ // contentLengthHeaderValue) to httpRequest’s header list.
+
+ // 10. If contentLength is non-null and httpRequest’s keepalive is true,
+ // then:
+ if (contentLength != null && httpRequest.keepalive) {
+ // NOTE: keepalive is a noop outside of browser context.
+ }
+
+ // 11. If httpRequest’s referrer is a URL, then append
+ // `Referer`/httpRequest’s referrer, serialized and isomorphic encoded,
+ // to httpRequest’s header list.
+ if (httpRequest.referrer instanceof URL) {
+ httpRequest.headersList.append('referer', isomorphicEncode(httpRequest.referrer.href))
+ }
+
+ // 12. Append a request `Origin` header for httpRequest.
+ appendRequestOriginHeader(httpRequest)
+
+ // 13. Append the Fetch metadata headers for httpRequest. [FETCH-METADATA]
+ appendFetchMetadata(httpRequest)
+
+ // 14. If httpRequest’s header list does not contain `User-Agent`, then
+ // user agents should append `User-Agent`/default `User-Agent` value to
+ // httpRequest’s header list.
+ if (!httpRequest.headersList.contains('user-agent')) {
+ httpRequest.headersList.append('user-agent', typeof esbuildDetection === 'undefined' ? 'undici' : 'node')
+ }
+
+ // 15. If httpRequest’s cache mode is "default" and httpRequest’s header
+ // list contains `If-Modified-Since`, `If-None-Match`,
+ // `If-Unmodified-Since`, `If-Match`, or `If-Range`, then set
+ // httpRequest’s cache mode to "no-store".
+ if (
+ httpRequest.cache === 'default' &&
+ (httpRequest.headersList.contains('if-modified-since') ||
+ httpRequest.headersList.contains('if-none-match') ||
+ httpRequest.headersList.contains('if-unmodified-since') ||
+ httpRequest.headersList.contains('if-match') ||
+ httpRequest.headersList.contains('if-range'))
+ ) {
+ httpRequest.cache = 'no-store'
+ }
+
+ // 16. If httpRequest’s cache mode is "no-cache", httpRequest’s prevent
+ // no-cache cache-control header modification flag is unset, and
+ // httpRequest’s header list does not contain `Cache-Control`, then append
+ // `Cache-Control`/`max-age=0` to httpRequest’s header list.
+ if (
+ httpRequest.cache === 'no-cache' &&
+ !httpRequest.preventNoCacheCacheControlHeaderModification &&
+ !httpRequest.headersList.contains('cache-control')
+ ) {
+ httpRequest.headersList.append('cache-control', 'max-age=0')
+ }
+
+ // 17. If httpRequest’s cache mode is "no-store" or "reload", then:
+ if (httpRequest.cache === 'no-store' || httpRequest.cache === 'reload') {
+ // 1. If httpRequest’s header list does not contain `Pragma`, then append
+ // `Pragma`/`no-cache` to httpRequest’s header list.
+ if (!httpRequest.headersList.contains('pragma')) {
+ httpRequest.headersList.append('pragma', 'no-cache')
+ }
+
+ // 2. If httpRequest’s header list does not contain `Cache-Control`,
+ // then append `Cache-Control`/`no-cache` to httpRequest’s header list.
+ if (!httpRequest.headersList.contains('cache-control')) {
+ httpRequest.headersList.append('cache-control', 'no-cache')
+ }
+ }
+
+ // 18. If httpRequest’s header list contains `Range`, then append
+ // `Accept-Encoding`/`identity` to httpRequest’s header list.
+ if (httpRequest.headersList.contains('range')) {
+ httpRequest.headersList.append('accept-encoding', 'identity')
+ }
+
+ // 19. Modify httpRequest’s header list per HTTP. Do not append a given
+ // header if httpRequest’s header list contains that header’s name.
+ // TODO: https://github.com/whatwg/fetch/issues/1285#issuecomment-896560129
+ if (!httpRequest.headersList.contains('accept-encoding')) {
+ if (urlHasHttpsScheme(requestCurrentURL(httpRequest))) {
+ httpRequest.headersList.append('accept-encoding', 'br, gzip, deflate')
+ } else {
+ httpRequest.headersList.append('accept-encoding', 'gzip, deflate')
+ }
+ }
+
+ httpRequest.headersList.delete('host')
+
+ // 20. If includeCredentials is true, then:
+ if (includeCredentials) {
+ // 1. If the user agent is not configured to block cookies for httpRequest
+ // (see section 7 of [COOKIES]), then:
+ // TODO: credentials
+ // 2. If httpRequest’s header list does not contain `Authorization`, then:
+ // TODO: credentials
+ }
+
+ // 21. If there’s a proxy-authentication entry, use it as appropriate.
+ // TODO: proxy-authentication
+
+ // 22. Set httpCache to the result of determining the HTTP cache
+ // partition, given httpRequest.
+ // TODO: cache
+
+ // 23. If httpCache is null, then set httpRequest’s cache mode to
+ // "no-store".
+ if (httpCache == null) {
+ httpRequest.cache = 'no-store'
+ }
+
+ // 24. If httpRequest’s cache mode is neither "no-store" nor "reload",
+ // then:
+ if (httpRequest.mode !== 'no-store' && httpRequest.mode !== 'reload') {
+ // TODO: cache
+ }
+
+ // 9. If aborted, then return the appropriate network error for fetchParams.
+ // TODO
+
+ // 10. If response is null, then:
+ if (response == null) {
+ // 1. If httpRequest’s cache mode is "only-if-cached", then return a
+ // network error.
+ if (httpRequest.mode === 'only-if-cached') {
+ return makeNetworkError('only if cached')
+ }
+
+ // 2. Let forwardResponse be the result of running HTTP-network fetch
+ // given httpFetchParams, includeCredentials, and isNewConnectionFetch.
+ const forwardResponse = await httpNetworkFetch(
+ httpFetchParams,
+ includeCredentials,
+ isNewConnectionFetch
+ )
+
+ // 3. If httpRequest’s method is unsafe and forwardResponse’s status is
+ // in the range 200 to 399, inclusive, invalidate appropriate stored
+ // responses in httpCache, as per the "Invalidation" chapter of HTTP
+ // Caching, and set storedResponse to null. [HTTP-CACHING]
+ if (
+ !safeMethodsSet.has(httpRequest.method) &&
+ forwardResponse.status >= 200 &&
+ forwardResponse.status <= 399
+ ) {
+ // TODO: cache
+ }
+
+ // 4. If the revalidatingFlag is set and forwardResponse’s status is 304,
+ // then:
+ if (revalidatingFlag && forwardResponse.status === 304) {
+ // TODO: cache
+ }
+
+ // 5. If response is null, then:
+ if (response == null) {
+ // 1. Set response to forwardResponse.
+ response = forwardResponse
+
+ // 2. Store httpRequest and forwardResponse in httpCache, as per the
+ // "Storing Responses in Caches" chapter of HTTP Caching. [HTTP-CACHING]
+ // TODO: cache
+ }
+ }
+
+ // 11. Set response’s URL list to a clone of httpRequest’s URL list.
+ response.urlList = [...httpRequest.urlList]
+
+ // 12. If httpRequest’s header list contains `Range`, then set response’s
+ // range-requested flag.
+ if (httpRequest.headersList.contains('range')) {
+ response.rangeRequested = true
+ }
+
+ // 13. Set response’s request-includes-credentials to includeCredentials.
+ response.requestIncludesCredentials = includeCredentials
+
+ // 14. If response’s status is 401, httpRequest’s response tainting is not
+ // "cors", includeCredentials is true, and request’s window is an environment
+ // settings object, then:
+ // TODO
+
+ // 15. If response’s status is 407, then:
+ if (response.status === 407) {
+ // 1. If request’s window is "no-window", then return a network error.
+ if (request.window === 'no-window') {
+ return makeNetworkError()
+ }
+
+ // 2. ???
+
+ // 3. If fetchParams is canceled, then return the appropriate network error for fetchParams.
+ if (isCancelled(fetchParams)) {
+ return makeAppropriateNetworkError(fetchParams)
+ }
+
+ // 4. Prompt the end user as appropriate in request’s window and store
+ // the result as a proxy-authentication entry. [HTTP-AUTH]
+ // TODO: Invoke some kind of callback?
+
+ // 5. Set response to the result of running HTTP-network-or-cache fetch given
+ // fetchParams.
+ // TODO
+ return makeNetworkError('proxy authentication required')
+ }
+
+ // 16. If all of the following are true
+ if (
+ // response’s status is 421
+ response.status === 421 &&
+ // isNewConnectionFetch is false
+ !isNewConnectionFetch &&
+ // request’s body is null, or request’s body is non-null and request’s body’s source is non-null
+ (request.body == null || request.body.source != null)
+ ) {
+ // then:
+
+ // 1. If fetchParams is canceled, then return the appropriate network error for fetchParams.
+ if (isCancelled(fetchParams)) {
+ return makeAppropriateNetworkError(fetchParams)
+ }
+
+ // 2. Set response to the result of running HTTP-network-or-cache
+ // fetch given fetchParams, isAuthenticationFetch, and true.
+
+ // TODO (spec): The spec doesn't specify this but we need to cancel
+ // the active response before we can start a new one.
+ // https://github.com/whatwg/fetch/issues/1293
+ fetchParams.controller.connection.destroy()
+
+ response = await httpNetworkOrCacheFetch(
+ fetchParams,
+ isAuthenticationFetch,
+ true
+ )
+ }
+
+ // 17. If isAuthenticationFetch is true, then create an authentication entry
+ if (isAuthenticationFetch) {
+ // TODO
+ }
+
+ // 18. Return response.
+ return response
+}
+
+// https://fetch.spec.whatwg.org/#http-network-fetch
+async function httpNetworkFetch (
+ fetchParams,
+ includeCredentials = false,
+ forceNewConnection = false
+) {
+ assert(!fetchParams.controller.connection || fetchParams.controller.connection.destroyed)
+
+ fetchParams.controller.connection = {
+ abort: null,
+ destroyed: false,
+ destroy (err) {
+ if (!this.destroyed) {
+ this.destroyed = true
+ this.abort?.(err ?? new DOMException('The operation was aborted.', 'AbortError'))
+ }
+ }
+ }
+
+ // 1. Let request be fetchParams’s request.
+ const request = fetchParams.request
+
+ // 2. Let response be null.
+ let response = null
+
+ // 3. Let timingInfo be fetchParams’s timing info.
+ const timingInfo = fetchParams.timingInfo
+
+ // 4. Let httpCache be the result of determining the HTTP cache partition,
+ // given request.
+ // TODO: cache
+ const httpCache = null
+
+ // 5. If httpCache is null, then set request’s cache mode to "no-store".
+ if (httpCache == null) {
+ request.cache = 'no-store'
+ }
+
+ // 6. Let networkPartitionKey be the result of determining the network
+ // partition key given request.
+ // TODO
+
+ // 7. Let newConnection be "yes" if forceNewConnection is true; otherwise
+ // "no".
+ const newConnection = forceNewConnection ? 'yes' : 'no' // eslint-disable-line no-unused-vars
+
+ // 8. Switch on request’s mode:
+ if (request.mode === 'websocket') {
+ // Let connection be the result of obtaining a WebSocket connection,
+ // given request’s current URL.
+ // TODO
+ } else {
+ // Let connection be the result of obtaining a connection, given
+ // networkPartitionKey, request’s current URL’s origin,
+ // includeCredentials, and forceNewConnection.
+ // TODO
+ }
+
+ // 9. Run these steps, but abort when the ongoing fetch is terminated:
+
+ // 1. If connection is failure, then return a network error.
+
+ // 2. Set timingInfo’s final connection timing info to the result of
+ // calling clamp and coarsen connection timing info with connection’s
+ // timing info, timingInfo’s post-redirect start time, and fetchParams’s
+ // cross-origin isolated capability.
+
+ // 3. If connection is not an HTTP/2 connection, request’s body is non-null,
+ // and request’s body’s source is null, then append (`Transfer-Encoding`,
+ // `chunked`) to request’s header list.
+
+ // 4. Set timingInfo’s final network-request start time to the coarsened
+ // shared current time given fetchParams’s cross-origin isolated
+ // capability.
+
+ // 5. Set response to the result of making an HTTP request over connection
+ // using request with the following caveats:
+
+ // - Follow the relevant requirements from HTTP. [HTTP] [HTTP-SEMANTICS]
+ // [HTTP-COND] [HTTP-CACHING] [HTTP-AUTH]
+
+ // - If request’s body is non-null, and request’s body’s source is null,
+ // then the user agent may have a buffer of up to 64 kibibytes and store
+ // a part of request’s body in that buffer. If the user agent reads from
+ // request’s body beyond that buffer’s size and the user agent needs to
+ // resend request, then instead return a network error.
+
+ // - Set timingInfo’s final network-response start time to the coarsened
+ // shared current time given fetchParams’s cross-origin isolated capability,
+ // immediately after the user agent’s HTTP parser receives the first byte
+ // of the response (e.g., frame header bytes for HTTP/2 or response status
+ // line for HTTP/1.x).
+
+ // - Wait until all the headers are transmitted.
+
+ // - Any responses whose status is in the range 100 to 199, inclusive,
+ // and is not 101, are to be ignored, except for the purposes of setting
+ // timingInfo’s final network-response start time above.
+
+ // - If request’s header list contains `Transfer-Encoding`/`chunked` and
+ // response is transferred via HTTP/1.0 or older, then return a network
+ // error.
+
+ // - If the HTTP request results in a TLS client certificate dialog, then:
+
+ // 1. If request’s window is an environment settings object, make the
+ // dialog available in request’s window.
+
+ // 2. Otherwise, return a network error.
+
+ // To transmit request’s body body, run these steps:
+ let requestBody = null
+ // 1. If body is null and fetchParams’s process request end-of-body is
+ // non-null, then queue a fetch task given fetchParams’s process request
+ // end-of-body and fetchParams’s task destination.
+ if (request.body == null && fetchParams.processRequestEndOfBody) {
+ queueMicrotask(() => fetchParams.processRequestEndOfBody())
+ } else if (request.body != null) {
+ // 2. Otherwise, if body is non-null:
+
+ // 1. Let processBodyChunk given bytes be these steps:
+ const processBodyChunk = async function * (bytes) {
+ // 1. If the ongoing fetch is terminated, then abort these steps.
+ if (isCancelled(fetchParams)) {
+ return
+ }
+
+ // 2. Run this step in parallel: transmit bytes.
+ yield bytes
+
+ // 3. If fetchParams’s process request body is non-null, then run
+ // fetchParams’s process request body given bytes’s length.
+ fetchParams.processRequestBodyChunkLength?.(bytes.byteLength)
+ }
+
+ // 2. Let processEndOfBody be these steps:
+ const processEndOfBody = () => {
+ // 1. If fetchParams is canceled, then abort these steps.
+ if (isCancelled(fetchParams)) {
+ return
+ }
+
+ // 2. If fetchParams’s process request end-of-body is non-null,
+ // then run fetchParams’s process request end-of-body.
+ if (fetchParams.processRequestEndOfBody) {
+ fetchParams.processRequestEndOfBody()
+ }
+ }
+
+ // 3. Let processBodyError given e be these steps:
+ const processBodyError = (e) => {
+ // 1. If fetchParams is canceled, then abort these steps.
+ if (isCancelled(fetchParams)) {
+ return
+ }
+
+ // 2. If e is an "AbortError" DOMException, then abort fetchParams’s controller.
+ if (e.name === 'AbortError') {
+ fetchParams.controller.abort()
+ } else {
+ fetchParams.controller.terminate(e)
+ }
+ }
+
+ // 4. Incrementally read request’s body given processBodyChunk, processEndOfBody,
+ // processBodyError, and fetchParams’s task destination.
+ requestBody = (async function * () {
+ try {
+ for await (const bytes of request.body.stream) {
+ yield * processBodyChunk(bytes)
+ }
+ processEndOfBody()
+ } catch (err) {
+ processBodyError(err)
+ }
+ })()
+ }
+
+ try {
+ // socket is only provided for websockets
+ const { body, status, statusText, headersList, socket } = await dispatch({ body: requestBody })
+
+ if (socket) {
+ response = makeResponse({ status, statusText, headersList, socket })
+ } else {
+ const iterator = body[Symbol.asyncIterator]()
+ fetchParams.controller.next = () => iterator.next()
+
+ response = makeResponse({ status, statusText, headersList })
+ }
+ } catch (err) {
+ // 10. If aborted, then:
+ if (err.name === 'AbortError') {
+ // 1. If connection uses HTTP/2, then transmit an RST_STREAM frame.
+ fetchParams.controller.connection.destroy()
+
+ // 2. Return the appropriate network error for fetchParams.
+ return makeAppropriateNetworkError(fetchParams, err)
+ }
+
+ return makeNetworkError(err)
+ }
+
+ // 11. Let pullAlgorithm be an action that resumes the ongoing fetch
+ // if it is suspended.
+ const pullAlgorithm = () => {
+ fetchParams.controller.resume()
+ }
+
+ // 12. Let cancelAlgorithm be an algorithm that aborts fetchParams’s
+ // controller with reason, given reason.
+ const cancelAlgorithm = (reason) => {
+ fetchParams.controller.abort(reason)
+ }
+
+ // 13. Let highWaterMark be a non-negative, non-NaN number, chosen by
+ // the user agent.
+ // TODO
+
+ // 14. Let sizeAlgorithm be an algorithm that accepts a chunk object
+ // and returns a non-negative, non-NaN, non-infinite number, chosen by the user agent.
+ // 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.
+ if (!ReadableStream) {
+ ReadableStream = require('stream/web').ReadableStream
+ }
+
+ const stream = new ReadableStream(
+ {
+ async start (controller) {
+ fetchParams.controller.controller = controller
+ },
+ async pull (controller) {
+ await pullAlgorithm(controller)
+ },
+ async cancel (reason) {
+ await cancelAlgorithm(reason)
+ }
+ },
+ {
+ highWaterMark: 0,
+ size () {
+ return 1
+ }
+ }
+ )
+
+ // 17. Run these steps, but abort when the ongoing fetch is terminated:
+
+ // 1. Set response’s body to a new body whose stream is stream.
+ response.body = { stream }
+
+ // 2. If response is not a network error and request’s cache mode is
+ // not "no-store", then update response in httpCache for request.
+ // TODO
+
+ // 3. If includeCredentials is true and the user agent is not configured
+ // to block cookies for request (see section 7 of [COOKIES]), then run the
+ // "set-cookie-string" parsing algorithm (see section 5.2 of [COOKIES]) on
+ // the value of each header whose name is a byte-case-insensitive match for
+ // `Set-Cookie` in response’s header list, if any, and request’s current URL.
+ // TODO
+
+ // 18. If aborted, then:
+ // TODO
+
+ // 19. Run these steps in parallel:
+
+ // 1. Run these steps, but abort when fetchParams is canceled:
+ fetchParams.controller.on('terminated', onAborted)
+ fetchParams.controller.resume = async () => {
+ // 1. While true
+ while (true) {
+ // 1-3. See onData...
+
+ // 4. Set bytes to the result of handling content codings given
+ // codings and bytes.
+ let bytes
+ let isFailure
+ try {
+ const { done, value } = await fetchParams.controller.next()
+
+ if (isAborted(fetchParams)) {
+ break
+ }
+
+ bytes = done ? undefined : value
+ } catch (err) {
+ if (fetchParams.controller.ended && !timingInfo.encodedBodySize) {
+ // zlib doesn't like empty streams.
+ bytes = undefined
+ } else {
+ bytes = err
+
+ // err may be propagated from the result of calling readablestream.cancel,
+ // which might not be an error. https://github.com/nodejs/undici/issues/2009
+ isFailure = true
+ }
+ }
+
+ if (bytes === undefined) {
+ // 2. Otherwise, if the bytes transmission for response’s message
+ // body is done normally and stream is readable, then close
+ // stream, finalize response for fetchParams and response, and
+ // abort these in-parallel steps.
+ readableStreamClose(fetchParams.controller.controller)
+
+ finalizeResponse(fetchParams, response)
+
+ return
+ }
+
+ // 5. Increase timingInfo’s decoded body size by bytes’s length.
+ timingInfo.decodedBodySize += bytes?.byteLength ?? 0
+
+ // 6. If bytes is failure, then terminate fetchParams’s controller.
+ if (isFailure) {
+ fetchParams.controller.terminate(bytes)
+ return
+ }
+
+ // 7. Enqueue a Uint8Array wrapping an ArrayBuffer containing bytes
+ // into stream.
+ fetchParams.controller.controller.enqueue(new Uint8Array(bytes))
+
+ // 8. If stream is errored, then terminate the ongoing fetch.
+ if (isErrored(stream)) {
+ fetchParams.controller.terminate()
+ return
+ }
+
+ // 9. If stream doesn’t need more data ask the user agent to suspend
+ // the ongoing fetch.
+ if (!fetchParams.controller.controller.desiredSize) {
+ return
+ }
+ }
+ }
+
+ // 2. If aborted, then:
+ function onAborted (reason) {
+ // 2. If fetchParams is aborted, then:
+ if (isAborted(fetchParams)) {
+ // 1. Set response’s aborted flag.
+ response.aborted = true
+
+ // 2. If stream is readable, then error stream with the result of
+ // deserialize a serialized abort reason given fetchParams’s
+ // controller’s serialized abort reason and an
+ // implementation-defined realm.
+ if (isReadable(stream)) {
+ fetchParams.controller.controller.error(
+ fetchParams.controller.serializedAbortReason
+ )
+ }
+ } else {
+ // 3. Otherwise, if stream is readable, error stream with a TypeError.
+ if (isReadable(stream)) {
+ fetchParams.controller.controller.error(new TypeError('terminated', {
+ cause: isErrorLike(reason) ? reason : undefined
+ }))
+ }
+ }
+
+ // 4. If connection uses HTTP/2, then transmit an RST_STREAM frame.
+ // 5. Otherwise, the user agent should close connection unless it would be bad for performance to do so.
+ fetchParams.controller.connection.destroy()
+ }
+
+ // 20. Return response.
+ return response
+
+ async function dispatch ({ body }) {
+ const url = requestCurrentURL(request)
+ /** @type {import('../..').Agent} */
+ const agent = fetchParams.controller.dispatcher
+
+ return new Promise((resolve, reject) => agent.dispatch(
+ {
+ path: url.pathname + url.search,
+ origin: url.origin,
+ method: request.method,
+ body: fetchParams.controller.dispatcher.isMockActive ? request.body && (request.body.source || request.body.stream) : body,
+ headers: request.headersList.entries,
+ maxRedirections: 0,
+ upgrade: request.mode === 'websocket' ? 'websocket' : undefined
+ },
+ {
+ body: null,
+ abort: null,
+
+ onConnect (abort) {
+ // TODO (fix): Do we need connection here?
+ const { connection } = fetchParams.controller
+
+ if (connection.destroyed) {
+ abort(new DOMException('The operation was aborted.', 'AbortError'))
+ } else {
+ fetchParams.controller.on('terminated', abort)
+ this.abort = connection.abort = abort
+ }
+ },
+
+ onHeaders (status, headersList, resume, statusText) {
+ if (status < 200) {
+ return
+ }
+
+ let codings = []
+ let location = ''
+
+ const headers = new Headers()
+
+ // For H2, the headers are a plain JS object
+ // We distinguish between them and iterate accordingly
+ if (Array.isArray(headersList)) {
+ for (let n = 0; n < headersList.length; n += 2) {
+ const key = headersList[n + 0].toString('latin1')
+ const val = headersList[n + 1].toString('latin1')
+ if (key.toLowerCase() === 'content-encoding') {
+ // https://www.rfc-editor.org/rfc/rfc7231#section-3.1.2.1
+ // "All content-coding values are case-insensitive..."
+ codings = val.toLowerCase().split(',').map((x) => x.trim())
+ } else if (key.toLowerCase() === 'location') {
+ location = val
+ }
+
+ headers[kHeadersList].append(key, val)
+ }
+ } else {
+ const keys = Object.keys(headersList)
+ for (const key of keys) {
+ const val = headersList[key]
+ if (key.toLowerCase() === 'content-encoding') {
+ // https://www.rfc-editor.org/rfc/rfc7231#section-3.1.2.1
+ // "All content-coding values are case-insensitive..."
+ codings = val.toLowerCase().split(',').map((x) => x.trim()).reverse()
+ } else if (key.toLowerCase() === 'location') {
+ location = val
+ }
+
+ headers[kHeadersList].append(key, val)
+ }
+ }
+
+ this.body = new Readable({ read: resume })
+
+ const decoders = []
+
+ const willFollow = request.redirect === 'follow' &&
+ location &&
+ redirectStatusSet.has(status)
+
+ // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Encoding
+ if (request.method !== 'HEAD' && request.method !== 'CONNECT' && !nullBodyStatus.includes(status) && !willFollow) {
+ for (const coding of codings) {
+ // https://www.rfc-editor.org/rfc/rfc9112.html#section-7.2
+ if (coding === 'x-gzip' || coding === 'gzip') {
+ decoders.push(zlib.createGunzip({
+ // Be less strict when decoding compressed responses, since sometimes
+ // servers send slightly invalid responses that are still accepted
+ // by common browsers.
+ // Always using Z_SYNC_FLUSH is what cURL does.
+ flush: zlib.constants.Z_SYNC_FLUSH,
+ finishFlush: zlib.constants.Z_SYNC_FLUSH
+ }))
+ } else if (coding === 'deflate') {
+ decoders.push(zlib.createInflate())
+ } else if (coding === 'br') {
+ decoders.push(zlib.createBrotliDecompress())
+ } else {
+ decoders.length = 0
+ break
+ }
+ }
+ }
+
+ resolve({
+ status,
+ statusText,
+ headersList: headers[kHeadersList],
+ body: decoders.length
+ ? pipeline(this.body, ...decoders, () => { })
+ : this.body.on('error', () => {})
+ })
+
+ return true
+ },
+
+ onData (chunk) {
+ if (fetchParams.controller.dump) {
+ return
+ }
+
+ // 1. If one or more bytes have been transmitted from response’s
+ // message body, then:
+
+ // 1. Let bytes be the transmitted bytes.
+ const bytes = chunk
+
+ // 2. Let codings be the result of extracting header list values
+ // given `Content-Encoding` and response’s header list.
+ // See pullAlgorithm.
+
+ // 3. Increase timingInfo’s encoded body size by bytes’s length.
+ timingInfo.encodedBodySize += bytes.byteLength
+
+ // 4. See pullAlgorithm...
+
+ return this.body.push(bytes)
+ },
+
+ onComplete () {
+ if (this.abort) {
+ fetchParams.controller.off('terminated', this.abort)
+ }
+
+ fetchParams.controller.ended = true
+
+ this.body.push(null)
+ },
+
+ onError (error) {
+ if (this.abort) {
+ fetchParams.controller.off('terminated', this.abort)
+ }
+
+ this.body?.destroy(error)
+
+ fetchParams.controller.terminate(error)
+
+ reject(error)
+ },
+
+ onUpgrade (status, headersList, socket) {
+ if (status !== 101) {
+ return
+ }
+
+ const headers = new Headers()
+
+ for (let n = 0; n < headersList.length; n += 2) {
+ const key = headersList[n + 0].toString('latin1')
+ const val = headersList[n + 1].toString('latin1')
+
+ headers[kHeadersList].append(key, val)
+ }
+
+ resolve({
+ status,
+ statusText: STATUS_CODES[status],
+ headersList: headers[kHeadersList],
+ socket
+ })
+
+ return true
+ }
+ }
+ ))
+ }
+}
+
+module.exports = {
+ fetch,
+ Fetch,
+ fetching,
+ finalizeAndReportTiming
+}
diff --git a/lib/fetch/request.js b/lib/fetch/request.js
new file mode 100644
index 0000000..6fe4dff
--- /dev/null
+++ b/lib/fetch/request.js
@@ -0,0 +1,946 @@
+/* globals AbortController */
+
+'use strict'
+
+const { extractBody, mixinBody, cloneBody } = require('./body')
+const { Headers, fill: fillHeaders, HeadersList } = require('./headers')
+const { FinalizationRegistry } = require('../compat/dispatcher-weakref')()
+const util = require('../core/util')
+const {
+ isValidHTTPToken,
+ sameOrigin,
+ normalizeMethod,
+ makePolicyContainer,
+ normalizeMethodRecord
+} = require('./util')
+const {
+ forbiddenMethodsSet,
+ corsSafeListedMethodsSet,
+ referrerPolicy,
+ requestRedirect,
+ requestMode,
+ requestCredentials,
+ requestCache,
+ requestDuplex
+} = require('./constants')
+const { kEnumerableProperty } = util
+const { kHeaders, kSignal, kState, kGuard, kRealm } = require('./symbols')
+const { webidl } = require('./webidl')
+const { getGlobalOrigin } = require('./global')
+const { URLSerializer } = require('./dataURL')
+const { kHeadersList, kConstruct } = require('../core/symbols')
+const assert = require('assert')
+const { getMaxListeners, setMaxListeners, getEventListeners, defaultMaxListeners } = require('events')
+
+let TransformStream = globalThis.TransformStream
+
+const kAbortController = Symbol('abortController')
+
+const requestFinalizer = new FinalizationRegistry(({ signal, abort }) => {
+ signal.removeEventListener('abort', abort)
+})
+
+// https://fetch.spec.whatwg.org/#request-class
+class Request {
+ // https://fetch.spec.whatwg.org/#dom-request
+ constructor (input, init = {}) {
+ if (input === kConstruct) {
+ return
+ }
+
+ webidl.argumentLengthCheck(arguments, 1, { header: 'Request constructor' })
+
+ input = webidl.converters.RequestInfo(input)
+ init = webidl.converters.RequestInit(init)
+
+ // https://html.spec.whatwg.org/multipage/webappapis.html#environment-settings-object
+ this[kRealm] = {
+ settingsObject: {
+ baseUrl: getGlobalOrigin(),
+ get origin () {
+ return this.baseUrl?.origin
+ },
+ policyContainer: makePolicyContainer()
+ }
+ }
+
+ // 1. Let request be null.
+ let request = null
+
+ // 2. Let fallbackMode be null.
+ let fallbackMode = null
+
+ // 3. Let baseURL be this’s relevant settings object’s API base URL.
+ const baseUrl = this[kRealm].settingsObject.baseUrl
+
+ // 4. Let signal be null.
+ let signal = null
+
+ // 5. If input is a string, then:
+ if (typeof input === 'string') {
+ // 1. Let parsedURL be the result of parsing input with baseURL.
+ // 2. If parsedURL is failure, then throw a TypeError.
+ let parsedURL
+ try {
+ parsedURL = new URL(input, baseUrl)
+ } catch (err) {
+ throw new TypeError('Failed to parse URL from ' + input, { cause: err })
+ }
+
+ // 3. If parsedURL includes credentials, then throw a TypeError.
+ if (parsedURL.username || parsedURL.password) {
+ throw new TypeError(
+ 'Request cannot be constructed from a URL that includes credentials: ' +
+ input
+ )
+ }
+
+ // 4. Set request to a new request whose URL is parsedURL.
+ request = makeRequest({ urlList: [parsedURL] })
+
+ // 5. Set fallbackMode to "cors".
+ fallbackMode = 'cors'
+ } else {
+ // 6. Otherwise:
+
+ // 7. Assert: input is a Request object.
+ assert(input instanceof Request)
+
+ // 8. Set request to input’s request.
+ request = input[kState]
+
+ // 9. Set signal to input’s signal.
+ signal = input[kSignal]
+ }
+
+ // 7. Let origin be this’s relevant settings object’s origin.
+ const origin = this[kRealm].settingsObject.origin
+
+ // 8. Let window be "client".
+ let window = 'client'
+
+ // 9. If request’s window is an environment settings object and its origin
+ // is same origin with origin, then set window to request’s window.
+ if (
+ request.window?.constructor?.name === 'EnvironmentSettingsObject' &&
+ sameOrigin(request.window, origin)
+ ) {
+ window = request.window
+ }
+
+ // 10. If init["window"] exists and is non-null, then throw a TypeError.
+ if (init.window != null) {
+ throw new TypeError(`'window' option '${window}' must be null`)
+ }
+
+ // 11. If init["window"] exists, then set window to "no-window".
+ if ('window' in init) {
+ window = 'no-window'
+ }
+
+ // 12. Set request to a new request with the following properties:
+ request = makeRequest({
+ // URL request’s URL.
+ // undici implementation note: this is set as the first item in request's urlList in makeRequest
+ // method request’s method.
+ method: request.method,
+ // header list A copy of request’s header list.
+ // undici implementation note: headersList is cloned in makeRequest
+ headersList: request.headersList,
+ // unsafe-request flag Set.
+ unsafeRequest: request.unsafeRequest,
+ // client This’s relevant settings object.
+ client: this[kRealm].settingsObject,
+ // window window.
+ window,
+ // priority request’s priority.
+ priority: request.priority,
+ // origin request’s origin. The propagation of the origin is only significant for navigation requests
+ // being handled by a service worker. In this scenario a request can have an origin that is different
+ // from the current client.
+ origin: request.origin,
+ // referrer request’s referrer.
+ referrer: request.referrer,
+ // referrer policy request’s referrer policy.
+ referrerPolicy: request.referrerPolicy,
+ // mode request’s mode.
+ mode: request.mode,
+ // credentials mode request’s credentials mode.
+ credentials: request.credentials,
+ // cache mode request’s cache mode.
+ cache: request.cache,
+ // redirect mode request’s redirect mode.
+ redirect: request.redirect,
+ // integrity metadata request’s integrity metadata.
+ integrity: request.integrity,
+ // keepalive request’s keepalive.
+ keepalive: request.keepalive,
+ // reload-navigation flag request’s reload-navigation flag.
+ reloadNavigation: request.reloadNavigation,
+ // history-navigation flag request’s history-navigation flag.
+ historyNavigation: request.historyNavigation,
+ // URL list A clone of request’s URL list.
+ urlList: [...request.urlList]
+ })
+
+ const initHasKey = Object.keys(init).length !== 0
+
+ // 13. If init is not empty, then:
+ if (initHasKey) {
+ // 1. If request’s mode is "navigate", then set it to "same-origin".
+ if (request.mode === 'navigate') {
+ request.mode = 'same-origin'
+ }
+
+ // 2. Unset request’s reload-navigation flag.
+ request.reloadNavigation = false
+
+ // 3. Unset request’s history-navigation flag.
+ request.historyNavigation = false
+
+ // 4. Set request’s origin to "client".
+ request.origin = 'client'
+
+ // 5. Set request’s referrer to "client"
+ request.referrer = 'client'
+
+ // 6. Set request’s referrer policy to the empty string.
+ request.referrerPolicy = ''
+
+ // 7. Set request’s URL to request’s current URL.
+ request.url = request.urlList[request.urlList.length - 1]
+
+ // 8. Set request’s URL list to « request’s URL ».
+ request.urlList = [request.url]
+ }
+
+ // 14. If init["referrer"] exists, then:
+ if (init.referrer !== undefined) {
+ // 1. Let referrer be init["referrer"].
+ const referrer = init.referrer
+
+ // 2. If referrer is the empty string, then set request’s referrer to "no-referrer".
+ if (referrer === '') {
+ request.referrer = 'no-referrer'
+ } else {
+ // 1. Let parsedReferrer be the result of parsing referrer with
+ // baseURL.
+ // 2. If parsedReferrer is failure, then throw a TypeError.
+ let parsedReferrer
+ try {
+ parsedReferrer = new URL(referrer, baseUrl)
+ } catch (err) {
+ throw new TypeError(`Referrer "${referrer}" is not a valid URL.`, { cause: err })
+ }
+
+ // 3. If one of the following is true
+ // - parsedReferrer’s scheme is "about" and path is the string "client"
+ // - parsedReferrer’s origin is not same origin with origin
+ // then set request’s referrer to "client".
+ if (
+ (parsedReferrer.protocol === 'about:' && parsedReferrer.hostname === 'client') ||
+ (origin && !sameOrigin(parsedReferrer, this[kRealm].settingsObject.baseUrl))
+ ) {
+ request.referrer = 'client'
+ } else {
+ // 4. Otherwise, set request’s referrer to parsedReferrer.
+ request.referrer = parsedReferrer
+ }
+ }
+ }
+
+ // 15. If init["referrerPolicy"] exists, then set request’s referrer policy
+ // to it.
+ if (init.referrerPolicy !== undefined) {
+ request.referrerPolicy = init.referrerPolicy
+ }
+
+ // 16. Let mode be init["mode"] if it exists, and fallbackMode otherwise.
+ let mode
+ if (init.mode !== undefined) {
+ mode = init.mode
+ } else {
+ mode = fallbackMode
+ }
+
+ // 17. If mode is "navigate", then throw a TypeError.
+ if (mode === 'navigate') {
+ throw webidl.errors.exception({
+ header: 'Request constructor',
+ message: 'invalid request mode navigate.'
+ })
+ }
+
+ // 18. If mode is non-null, set request’s mode to mode.
+ if (mode != null) {
+ request.mode = mode
+ }
+
+ // 19. If init["credentials"] exists, then set request’s credentials mode
+ // to it.
+ if (init.credentials !== undefined) {
+ request.credentials = init.credentials
+ }
+
+ // 18. If init["cache"] exists, then set request’s cache mode to it.
+ if (init.cache !== undefined) {
+ request.cache = init.cache
+ }
+
+ // 21. If request’s cache mode is "only-if-cached" and request’s mode is
+ // not "same-origin", then throw a TypeError.
+ if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') {
+ throw new TypeError(
+ "'only-if-cached' can be set only with 'same-origin' mode"
+ )
+ }
+
+ // 22. If init["redirect"] exists, then set request’s redirect mode to it.
+ if (init.redirect !== undefined) {
+ request.redirect = init.redirect
+ }
+
+ // 23. If init["integrity"] exists, then set request’s integrity metadata to it.
+ if (init.integrity != null) {
+ request.integrity = String(init.integrity)
+ }
+
+ // 24. If init["keepalive"] exists, then set request’s keepalive to it.
+ if (init.keepalive !== undefined) {
+ request.keepalive = Boolean(init.keepalive)
+ }
+
+ // 25. If init["method"] exists, then:
+ if (init.method !== undefined) {
+ // 1. Let method be init["method"].
+ let method = init.method
+
+ // 2. If method is not a method or method is a forbidden method, then
+ // throw a TypeError.
+ if (!isValidHTTPToken(method)) {
+ throw new TypeError(`'${method}' is not a valid HTTP method.`)
+ }
+
+ if (forbiddenMethodsSet.has(method.toUpperCase())) {
+ throw new TypeError(`'${method}' HTTP method is unsupported.`)
+ }
+
+ // 3. Normalize method.
+ method = normalizeMethodRecord[method] ?? normalizeMethod(method)
+
+ // 4. Set request’s method to method.
+ request.method = method
+ }
+
+ // 26. If init["signal"] exists, then set signal to it.
+ if (init.signal !== undefined) {
+ signal = init.signal
+ }
+
+ // 27. Set this’s request to request.
+ this[kState] = request
+
+ // 28. Set this’s signal to a new AbortSignal object with this’s relevant
+ // Realm.
+ // TODO: could this be simplified with AbortSignal.any
+ // (https://dom.spec.whatwg.org/#dom-abortsignal-any)
+ const ac = new AbortController()
+ this[kSignal] = ac.signal
+ this[kSignal][kRealm] = this[kRealm]
+
+ // 29. If signal is not null, then make this’s signal follow signal.
+ if (signal != null) {
+ if (
+ !signal ||
+ typeof signal.aborted !== 'boolean' ||
+ typeof signal.addEventListener !== 'function'
+ ) {
+ throw new TypeError(
+ "Failed to construct 'Request': member signal is not of type AbortSignal."
+ )
+ }
+
+ if (signal.aborted) {
+ ac.abort(signal.reason)
+ } else {
+ // Keep a strong ref to ac while request object
+ // is alive. This is needed to prevent AbortController
+ // from being prematurely garbage collected.
+ // See, https://github.com/nodejs/undici/issues/1926.
+ this[kAbortController] = ac
+
+ const acRef = new WeakRef(ac)
+ const abort = function () {
+ const ac = acRef.deref()
+ if (ac !== undefined) {
+ ac.abort(this.reason)
+ }
+ }
+
+ // Third-party AbortControllers may not work with these.
+ // See, https://github.com/nodejs/undici/pull/1910#issuecomment-1464495619.
+ try {
+ // If the max amount of listeners is equal to the default, increase it
+ // This is only available in node >= v19.9.0
+ if (typeof getMaxListeners === 'function' && getMaxListeners(signal) === defaultMaxListeners) {
+ setMaxListeners(100, signal)
+ } else if (getEventListeners(signal, 'abort').length >= defaultMaxListeners) {
+ setMaxListeners(100, signal)
+ }
+ } catch {}
+
+ util.addAbortListener(signal, abort)
+ requestFinalizer.register(ac, { signal, abort })
+ }
+ }
+
+ // 30. Set this’s headers to a new Headers object with this’s relevant
+ // Realm, whose header list is request’s header list and guard is
+ // "request".
+ this[kHeaders] = new Headers(kConstruct)
+ this[kHeaders][kHeadersList] = request.headersList
+ this[kHeaders][kGuard] = 'request'
+ this[kHeaders][kRealm] = this[kRealm]
+
+ // 31. If this’s request’s mode is "no-cors", then:
+ if (mode === 'no-cors') {
+ // 1. If this’s request’s method is not a CORS-safelisted method,
+ // then throw a TypeError.
+ if (!corsSafeListedMethodsSet.has(request.method)) {
+ throw new TypeError(
+ `'${request.method} is unsupported in no-cors mode.`
+ )
+ }
+
+ // 2. Set this’s headers’s guard to "request-no-cors".
+ this[kHeaders][kGuard] = 'request-no-cors'
+ }
+
+ // 32. If init is not empty, then:
+ if (initHasKey) {
+ /** @type {HeadersList} */
+ const headersList = this[kHeaders][kHeadersList]
+ // 1. Let headers be a copy of this’s headers and its associated header
+ // list.
+ // 2. If init["headers"] exists, then set headers to init["headers"].
+ const headers = init.headers !== undefined ? init.headers : new HeadersList(headersList)
+
+ // 3. Empty this’s headers’s header list.
+ headersList.clear()
+
+ // 4. If headers is a Headers object, then for each header in its header
+ // list, append header’s name/header’s value to this’s headers.
+ if (headers instanceof HeadersList) {
+ for (const [key, val] of headers) {
+ headersList.append(key, val)
+ }
+ // Note: Copy the `set-cookie` meta-data.
+ headersList.cookies = headers.cookies
+ } else {
+ // 5. Otherwise, fill this’s headers with headers.
+ fillHeaders(this[kHeaders], headers)
+ }
+ }
+
+ // 33. Let inputBody be input’s request’s body if input is a Request
+ // object; otherwise null.
+ const inputBody = input instanceof Request ? input[kState].body : null
+
+ // 34. If either init["body"] exists and is non-null or inputBody is
+ // non-null, and request’s method is `GET` or `HEAD`, then throw a
+ // TypeError.
+ if (
+ (init.body != null || inputBody != null) &&
+ (request.method === 'GET' || request.method === 'HEAD')
+ ) {
+ throw new TypeError('Request with GET/HEAD method cannot have body.')
+ }
+
+ // 35. Let initBody be null.
+ let initBody = null
+
+ // 36. If init["body"] exists and is non-null, then:
+ if (init.body != null) {
+ // 1. Let Content-Type be null.
+ // 2. Set initBody and Content-Type to the result of extracting
+ // init["body"], with keepalive set to request’s keepalive.
+ const [extractedBody, contentType] = extractBody(
+ init.body,
+ request.keepalive
+ )
+ initBody = extractedBody
+
+ // 3, If Content-Type is non-null and this’s headers’s header list does
+ // not contain `Content-Type`, then append `Content-Type`/Content-Type to
+ // this’s headers.
+ if (contentType && !this[kHeaders][kHeadersList].contains('content-type')) {
+ this[kHeaders].append('content-type', contentType)
+ }
+ }
+
+ // 37. Let inputOrInitBody be initBody if it is non-null; otherwise
+ // inputBody.
+ const inputOrInitBody = initBody ?? inputBody
+
+ // 38. If inputOrInitBody is non-null and inputOrInitBody’s source is
+ // null, then:
+ if (inputOrInitBody != null && inputOrInitBody.source == null) {
+ // 1. If initBody is non-null and init["duplex"] does not exist,
+ // then throw a TypeError.
+ if (initBody != null && init.duplex == null) {
+ throw new TypeError('RequestInit: duplex option is required when sending a body.')
+ }
+
+ // 2. If this’s request’s mode is neither "same-origin" nor "cors",
+ // then throw a TypeError.
+ if (request.mode !== 'same-origin' && request.mode !== 'cors') {
+ throw new TypeError(
+ 'If request is made from ReadableStream, mode should be "same-origin" or "cors"'
+ )
+ }
+
+ // 3. Set this’s request’s use-CORS-preflight flag.
+ request.useCORSPreflightFlag = true
+ }
+
+ // 39. Let finalBody be inputOrInitBody.
+ let finalBody = inputOrInitBody
+
+ // 40. If initBody is null and inputBody is non-null, then:
+ if (initBody == null && inputBody != null) {
+ // 1. If input is unusable, then throw a TypeError.
+ if (util.isDisturbed(inputBody.stream) || inputBody.stream.locked) {
+ throw new TypeError(
+ 'Cannot construct a Request with a Request object that has already been used.'
+ )
+ }
+
+ // 2. Set finalBody to the result of creating a proxy for inputBody.
+ if (!TransformStream) {
+ TransformStream = require('stream/web').TransformStream
+ }
+
+ // https://streams.spec.whatwg.org/#readablestream-create-a-proxy
+ const identityTransform = new TransformStream()
+ inputBody.stream.pipeThrough(identityTransform)
+ finalBody = {
+ source: inputBody.source,
+ length: inputBody.length,
+ stream: identityTransform.readable
+ }
+ }
+
+ // 41. Set this’s request’s body to finalBody.
+ this[kState].body = finalBody
+ }
+
+ // Returns request’s HTTP method, which is "GET" by default.
+ get method () {
+ webidl.brandCheck(this, Request)
+
+ // The method getter steps are to return this’s request’s method.
+ return this[kState].method
+ }
+
+ // Returns the URL of request as a string.
+ get url () {
+ webidl.brandCheck(this, Request)
+
+ // The url getter steps are to return this’s request’s URL, serialized.
+ return URLSerializer(this[kState].url)
+ }
+
+ // Returns a Headers object consisting of the headers associated with request.
+ // Note that headers added in the network layer by the user agent will not
+ // be accounted for in this object, e.g., the "Host" header.
+ get headers () {
+ webidl.brandCheck(this, Request)
+
+ // The headers getter steps are to return this’s headers.
+ return this[kHeaders]
+ }
+
+ // Returns the kind of resource requested by request, e.g., "document"
+ // or "script".
+ get destination () {
+ webidl.brandCheck(this, Request)
+
+ // The destination getter are to return this’s request’s destination.
+ return this[kState].destination
+ }
+
+ // Returns the referrer of request. Its value can be a same-origin URL if
+ // explicitly set in init, the empty string to indicate no referrer, and
+ // "about:client" when defaulting to the global’s default. This is used
+ // during fetching to determine the value of the `Referer` header of the
+ // request being made.
+ get referrer () {
+ webidl.brandCheck(this, Request)
+
+ // 1. If this’s request’s referrer is "no-referrer", then return the
+ // empty string.
+ if (this[kState].referrer === 'no-referrer') {
+ return ''
+ }
+
+ // 2. If this’s request’s referrer is "client", then return
+ // "about:client".
+ if (this[kState].referrer === 'client') {
+ return 'about:client'
+ }
+
+ // Return this’s request’s referrer, serialized.
+ return this[kState].referrer.toString()
+ }
+
+ // Returns the referrer policy associated with request.
+ // This is used during fetching to compute the value of the request’s
+ // referrer.
+ get referrerPolicy () {
+ webidl.brandCheck(this, Request)
+
+ // The referrerPolicy getter steps are to return this’s request’s referrer policy.
+ return this[kState].referrerPolicy
+ }
+
+ // Returns the mode associated with request, which is a string indicating
+ // whether the request will use CORS, or will be restricted to same-origin
+ // URLs.
+ get mode () {
+ webidl.brandCheck(this, Request)
+
+ // The mode getter steps are to return this’s request’s mode.
+ return this[kState].mode
+ }
+
+ // Returns the credentials mode associated with request,
+ // which is a string indicating whether credentials will be sent with the
+ // request always, never, or only when sent to a same-origin URL.
+ get credentials () {
+ // The credentials getter steps are to return this’s request’s credentials mode.
+ return this[kState].credentials
+ }
+
+ // Returns the cache mode associated with request,
+ // which is a string indicating how the request will
+ // interact with the browser’s cache when fetching.
+ get cache () {
+ webidl.brandCheck(this, Request)
+
+ // The cache getter steps are to return this’s request’s cache mode.
+ return this[kState].cache
+ }
+
+ // Returns the redirect mode associated with request,
+ // which is a string indicating how redirects for the
+ // request will be handled during fetching. A request
+ // will follow redirects by default.
+ get redirect () {
+ webidl.brandCheck(this, Request)
+
+ // The redirect getter steps are to return this’s request’s redirect mode.
+ return this[kState].redirect
+ }
+
+ // Returns request’s subresource integrity metadata, which is a
+ // cryptographic hash of the resource being fetched. Its value
+ // consists of multiple hashes separated by whitespace. [SRI]
+ get integrity () {
+ webidl.brandCheck(this, Request)
+
+ // The integrity getter steps are to return this’s request’s integrity
+ // metadata.
+ return this[kState].integrity
+ }
+
+ // Returns a boolean indicating whether or not request can outlive the
+ // global in which it was created.
+ get keepalive () {
+ webidl.brandCheck(this, Request)
+
+ // The keepalive getter steps are to return this’s request’s keepalive.
+ return this[kState].keepalive
+ }
+
+ // Returns a boolean indicating whether or not request is for a reload
+ // navigation.
+ get isReloadNavigation () {
+ webidl.brandCheck(this, Request)
+
+ // The isReloadNavigation getter steps are to return true if this’s
+ // request’s reload-navigation flag is set; otherwise false.
+ return this[kState].reloadNavigation
+ }
+
+ // Returns a boolean indicating whether or not request is for a history
+ // navigation (a.k.a. back-foward navigation).
+ get isHistoryNavigation () {
+ webidl.brandCheck(this, Request)
+
+ // The isHistoryNavigation getter steps are to return true if this’s request’s
+ // history-navigation flag is set; otherwise false.
+ return this[kState].historyNavigation
+ }
+
+ // Returns the signal associated with request, which is an AbortSignal
+ // object indicating whether or not request has been aborted, and its
+ // abort event handler.
+ get signal () {
+ webidl.brandCheck(this, Request)
+
+ // The signal getter steps are to return this’s signal.
+ return this[kSignal]
+ }
+
+ get body () {
+ webidl.brandCheck(this, Request)
+
+ return this[kState].body ? this[kState].body.stream : null
+ }
+
+ get bodyUsed () {
+ webidl.brandCheck(this, Request)
+
+ return !!this[kState].body && util.isDisturbed(this[kState].body.stream)
+ }
+
+ get duplex () {
+ webidl.brandCheck(this, Request)
+
+ return 'half'
+ }
+
+ // Returns a clone of request.
+ clone () {
+ webidl.brandCheck(this, Request)
+
+ // 1. If this is unusable, then throw a TypeError.
+ if (this.bodyUsed || this.body?.locked) {
+ throw new TypeError('unusable')
+ }
+
+ // 2. Let clonedRequest be the result of cloning this’s request.
+ const clonedRequest = cloneRequest(this[kState])
+
+ // 3. Let clonedRequestObject be the result of creating a Request object,
+ // given clonedRequest, this’s headers’s guard, and this’s relevant Realm.
+ const clonedRequestObject = new Request(kConstruct)
+ clonedRequestObject[kState] = clonedRequest
+ clonedRequestObject[kRealm] = this[kRealm]
+ clonedRequestObject[kHeaders] = new Headers(kConstruct)
+ clonedRequestObject[kHeaders][kHeadersList] = clonedRequest.headersList
+ clonedRequestObject[kHeaders][kGuard] = this[kHeaders][kGuard]
+ clonedRequestObject[kHeaders][kRealm] = this[kHeaders][kRealm]
+
+ // 4. Make clonedRequestObject’s signal follow this’s signal.
+ const ac = new AbortController()
+ if (this.signal.aborted) {
+ ac.abort(this.signal.reason)
+ } else {
+ util.addAbortListener(
+ this.signal,
+ () => {
+ ac.abort(this.signal.reason)
+ }
+ )
+ }
+ clonedRequestObject[kSignal] = ac.signal
+
+ // 4. Return clonedRequestObject.
+ return clonedRequestObject
+ }
+}
+
+mixinBody(Request)
+
+function makeRequest (init) {
+ // https://fetch.spec.whatwg.org/#requests
+ const request = {
+ method: 'GET',
+ localURLsOnly: false,
+ unsafeRequest: false,
+ body: null,
+ client: null,
+ reservedClient: null,
+ replacesClientId: '',
+ window: 'client',
+ keepalive: false,
+ serviceWorkers: 'all',
+ initiator: '',
+ destination: '',
+ priority: null,
+ origin: 'client',
+ policyContainer: 'client',
+ referrer: 'client',
+ referrerPolicy: '',
+ mode: 'no-cors',
+ useCORSPreflightFlag: false,
+ credentials: 'same-origin',
+ useCredentials: false,
+ cache: 'default',
+ redirect: 'follow',
+ integrity: '',
+ cryptoGraphicsNonceMetadata: '',
+ parserMetadata: '',
+ reloadNavigation: false,
+ historyNavigation: false,
+ userActivation: false,
+ taintedOrigin: false,
+ redirectCount: 0,
+ responseTainting: 'basic',
+ preventNoCacheCacheControlHeaderModification: false,
+ done: false,
+ timingAllowFailed: false,
+ ...init,
+ headersList: init.headersList
+ ? new HeadersList(init.headersList)
+ : new HeadersList()
+ }
+ request.url = request.urlList[0]
+ return request
+}
+
+// https://fetch.spec.whatwg.org/#concept-request-clone
+function cloneRequest (request) {
+ // To clone a request request, run these steps:
+
+ // 1. Let newRequest be a copy of request, except for its body.
+ const newRequest = makeRequest({ ...request, body: null })
+
+ // 2. If request’s body is non-null, set newRequest’s body to the
+ // result of cloning request’s body.
+ if (request.body != null) {
+ newRequest.body = cloneBody(request.body)
+ }
+
+ // 3. Return newRequest.
+ return newRequest
+}
+
+Object.defineProperties(Request.prototype, {
+ method: kEnumerableProperty,
+ url: kEnumerableProperty,
+ headers: kEnumerableProperty,
+ redirect: kEnumerableProperty,
+ clone: kEnumerableProperty,
+ signal: kEnumerableProperty,
+ duplex: kEnumerableProperty,
+ destination: kEnumerableProperty,
+ body: kEnumerableProperty,
+ bodyUsed: kEnumerableProperty,
+ isHistoryNavigation: kEnumerableProperty,
+ isReloadNavigation: kEnumerableProperty,
+ keepalive: kEnumerableProperty,
+ integrity: kEnumerableProperty,
+ cache: kEnumerableProperty,
+ credentials: kEnumerableProperty,
+ attribute: kEnumerableProperty,
+ referrerPolicy: kEnumerableProperty,
+ referrer: kEnumerableProperty,
+ mode: kEnumerableProperty,
+ [Symbol.toStringTag]: {
+ value: 'Request',
+ configurable: true
+ }
+})
+
+webidl.converters.Request = webidl.interfaceConverter(
+ Request
+)
+
+// https://fetch.spec.whatwg.org/#requestinfo
+webidl.converters.RequestInfo = function (V) {
+ if (typeof V === 'string') {
+ return webidl.converters.USVString(V)
+ }
+
+ if (V instanceof Request) {
+ return webidl.converters.Request(V)
+ }
+
+ return webidl.converters.USVString(V)
+}
+
+webidl.converters.AbortSignal = webidl.interfaceConverter(
+ AbortSignal
+)
+
+// https://fetch.spec.whatwg.org/#requestinit
+webidl.converters.RequestInit = webidl.dictionaryConverter([
+ {
+ key: 'method',
+ converter: webidl.converters.ByteString
+ },
+ {
+ key: 'headers',
+ converter: webidl.converters.HeadersInit
+ },
+ {
+ key: 'body',
+ converter: webidl.nullableConverter(
+ webidl.converters.BodyInit
+ )
+ },
+ {
+ key: 'referrer',
+ converter: webidl.converters.USVString
+ },
+ {
+ key: 'referrerPolicy',
+ converter: webidl.converters.DOMString,
+ // https://w3c.github.io/webappsec-referrer-policy/#referrer-policy
+ allowedValues: referrerPolicy
+ },
+ {
+ key: 'mode',
+ converter: webidl.converters.DOMString,
+ // https://fetch.spec.whatwg.org/#concept-request-mode
+ allowedValues: requestMode
+ },
+ {
+ key: 'credentials',
+ converter: webidl.converters.DOMString,
+ // https://fetch.spec.whatwg.org/#requestcredentials
+ allowedValues: requestCredentials
+ },
+ {
+ key: 'cache',
+ converter: webidl.converters.DOMString,
+ // https://fetch.spec.whatwg.org/#requestcache
+ allowedValues: requestCache
+ },
+ {
+ key: 'redirect',
+ converter: webidl.converters.DOMString,
+ // https://fetch.spec.whatwg.org/#requestredirect
+ allowedValues: requestRedirect
+ },
+ {
+ key: 'integrity',
+ converter: webidl.converters.DOMString
+ },
+ {
+ key: 'keepalive',
+ converter: webidl.converters.boolean
+ },
+ {
+ key: 'signal',
+ converter: webidl.nullableConverter(
+ (signal) => webidl.converters.AbortSignal(
+ signal,
+ { strict: false }
+ )
+ )
+ },
+ {
+ key: 'window',
+ converter: webidl.converters.any
+ },
+ {
+ key: 'duplex',
+ converter: webidl.converters.DOMString,
+ allowedValues: requestDuplex
+ }
+])
+
+module.exports = { Request, makeRequest }
diff --git a/lib/fetch/response.js b/lib/fetch/response.js
new file mode 100644
index 0000000..7338612
--- /dev/null
+++ b/lib/fetch/response.js
@@ -0,0 +1,571 @@
+'use strict'
+
+const { Headers, HeadersList, fill } = require('./headers')
+const { extractBody, cloneBody, mixinBody } = require('./body')
+const util = require('../core/util')
+const { kEnumerableProperty } = util
+const {
+ isValidReasonPhrase,
+ isCancelled,
+ isAborted,
+ isBlobLike,
+ serializeJavascriptValueToJSONString,
+ isErrorLike,
+ isomorphicEncode
+} = require('./util')
+const {
+ redirectStatusSet,
+ nullBodyStatus,
+ DOMException
+} = require('./constants')
+const { kState, kHeaders, kGuard, kRealm } = require('./symbols')
+const { webidl } = require('./webidl')
+const { FormData } = require('./formdata')
+const { getGlobalOrigin } = require('./global')
+const { URLSerializer } = require('./dataURL')
+const { kHeadersList, kConstruct } = require('../core/symbols')
+const assert = require('assert')
+const { types } = require('util')
+
+const ReadableStream = globalThis.ReadableStream || require('stream/web').ReadableStream
+const textEncoder = new TextEncoder('utf-8')
+
+// https://fetch.spec.whatwg.org/#response-class
+class Response {
+ // Creates network error Response.
+ static error () {
+ // TODO
+ const relevantRealm = { settingsObject: {} }
+
+ // The static error() method steps are to return the result of creating a
+ // Response object, given a new network error, "immutable", and this’s
+ // relevant Realm.
+ const responseObject = new Response()
+ responseObject[kState] = makeNetworkError()
+ responseObject[kRealm] = relevantRealm
+ responseObject[kHeaders][kHeadersList] = responseObject[kState].headersList
+ responseObject[kHeaders][kGuard] = 'immutable'
+ responseObject[kHeaders][kRealm] = relevantRealm
+ return responseObject
+ }
+
+ // https://fetch.spec.whatwg.org/#dom-response-json
+ static json (data, init = {}) {
+ webidl.argumentLengthCheck(arguments, 1, { header: 'Response.json' })
+
+ if (init !== null) {
+ init = webidl.converters.ResponseInit(init)
+ }
+
+ // 1. Let bytes the result of running serialize a JavaScript value to JSON bytes on data.
+ const bytes = textEncoder.encode(
+ serializeJavascriptValueToJSONString(data)
+ )
+
+ // 2. Let body be the result of extracting bytes.
+ const body = extractBody(bytes)
+
+ // 3. Let responseObject be the result of creating a Response object, given a new response,
+ // "response", and this’s relevant Realm.
+ const relevantRealm = { settingsObject: {} }
+ const responseObject = new Response()
+ responseObject[kRealm] = relevantRealm
+ responseObject[kHeaders][kGuard] = 'response'
+ responseObject[kHeaders][kRealm] = relevantRealm
+
+ // 4. Perform initialize a response given responseObject, init, and (body, "application/json").
+ initializeResponse(responseObject, init, { body: body[0], type: 'application/json' })
+
+ // 5. Return responseObject.
+ return responseObject
+ }
+
+ // Creates a redirect Response that redirects to url with status status.
+ static redirect (url, status = 302) {
+ const relevantRealm = { settingsObject: {} }
+
+ webidl.argumentLengthCheck(arguments, 1, { header: 'Response.redirect' })
+
+ url = webidl.converters.USVString(url)
+ status = webidl.converters['unsigned short'](status)
+
+ // 1. Let parsedURL be the result of parsing url with current settings
+ // object’s API base URL.
+ // 2. If parsedURL is failure, then throw a TypeError.
+ // TODO: base-URL?
+ let parsedURL
+ try {
+ parsedURL = new URL(url, getGlobalOrigin())
+ } catch (err) {
+ throw Object.assign(new TypeError('Failed to parse URL from ' + url), {
+ cause: err
+ })
+ }
+
+ // 3. If status is not a redirect status, then throw a RangeError.
+ if (!redirectStatusSet.has(status)) {
+ throw new RangeError('Invalid status code ' + status)
+ }
+
+ // 4. Let responseObject be the result of creating a Response object,
+ // given a new response, "immutable", and this’s relevant Realm.
+ const responseObject = new Response()
+ responseObject[kRealm] = relevantRealm
+ responseObject[kHeaders][kGuard] = 'immutable'
+ responseObject[kHeaders][kRealm] = relevantRealm
+
+ // 5. Set responseObject’s response’s status to status.
+ responseObject[kState].status = status
+
+ // 6. Let value be parsedURL, serialized and isomorphic encoded.
+ const value = isomorphicEncode(URLSerializer(parsedURL))
+
+ // 7. Append `Location`/value to responseObject’s response’s header list.
+ responseObject[kState].headersList.append('location', value)
+
+ // 8. Return responseObject.
+ return responseObject
+ }
+
+ // https://fetch.spec.whatwg.org/#dom-response
+ constructor (body = null, init = {}) {
+ if (body !== null) {
+ body = webidl.converters.BodyInit(body)
+ }
+
+ init = webidl.converters.ResponseInit(init)
+
+ // TODO
+ this[kRealm] = { settingsObject: {} }
+
+ // 1. Set this’s response to a new response.
+ this[kState] = makeResponse({})
+
+ // 2. Set this’s headers to a new Headers object with this’s relevant
+ // Realm, whose header list is this’s response’s header list and guard
+ // is "response".
+ this[kHeaders] = new Headers(kConstruct)
+ this[kHeaders][kGuard] = 'response'
+ this[kHeaders][kHeadersList] = this[kState].headersList
+ this[kHeaders][kRealm] = this[kRealm]
+
+ // 3. Let bodyWithType be null.
+ let bodyWithType = null
+
+ // 4. If body is non-null, then set bodyWithType to the result of extracting body.
+ if (body != null) {
+ const [extractedBody, type] = extractBody(body)
+ bodyWithType = { body: extractedBody, type }
+ }
+
+ // 5. Perform initialize a response given this, init, and bodyWithType.
+ initializeResponse(this, init, bodyWithType)
+ }
+
+ // Returns response’s type, e.g., "cors".
+ get type () {
+ webidl.brandCheck(this, Response)
+
+ // The type getter steps are to return this’s response’s type.
+ return this[kState].type
+ }
+
+ // Returns response’s URL, if it has one; otherwise the empty string.
+ get url () {
+ webidl.brandCheck(this, Response)
+
+ const urlList = this[kState].urlList
+
+ // The url getter steps are to return the empty string if this’s
+ // response’s URL is null; otherwise this’s response’s URL,
+ // serialized with exclude fragment set to true.
+ const url = urlList[urlList.length - 1] ?? null
+
+ if (url === null) {
+ return ''
+ }
+
+ return URLSerializer(url, true)
+ }
+
+ // Returns whether response was obtained through a redirect.
+ get redirected () {
+ webidl.brandCheck(this, Response)
+
+ // The redirected getter steps are to return true if this’s response’s URL
+ // list has more than one item; otherwise false.
+ return this[kState].urlList.length > 1
+ }
+
+ // Returns response’s status.
+ get status () {
+ webidl.brandCheck(this, Response)
+
+ // The status getter steps are to return this’s response’s status.
+ return this[kState].status
+ }
+
+ // Returns whether response’s status is an ok status.
+ get ok () {
+ webidl.brandCheck(this, Response)
+
+ // The ok getter steps are to return true if this’s response’s status is an
+ // ok status; otherwise false.
+ return this[kState].status >= 200 && this[kState].status <= 299
+ }
+
+ // Returns response’s status message.
+ get statusText () {
+ webidl.brandCheck(this, Response)
+
+ // The statusText getter steps are to return this’s response’s status
+ // message.
+ return this[kState].statusText
+ }
+
+ // Returns response’s headers as Headers.
+ get headers () {
+ webidl.brandCheck(this, Response)
+
+ // The headers getter steps are to return this’s headers.
+ return this[kHeaders]
+ }
+
+ get body () {
+ webidl.brandCheck(this, Response)
+
+ return this[kState].body ? this[kState].body.stream : null
+ }
+
+ get bodyUsed () {
+ webidl.brandCheck(this, Response)
+
+ return !!this[kState].body && util.isDisturbed(this[kState].body.stream)
+ }
+
+ // Returns a clone of response.
+ clone () {
+ webidl.brandCheck(this, Response)
+
+ // 1. If this is unusable, then throw a TypeError.
+ if (this.bodyUsed || (this.body && this.body.locked)) {
+ throw webidl.errors.exception({
+ header: 'Response.clone',
+ message: 'Body has already been consumed.'
+ })
+ }
+
+ // 2. Let clonedResponse be the result of cloning this’s response.
+ const clonedResponse = cloneResponse(this[kState])
+
+ // 3. Return the result of creating a Response object, given
+ // clonedResponse, this’s headers’s guard, and this’s relevant Realm.
+ const clonedResponseObject = new Response()
+ clonedResponseObject[kState] = clonedResponse
+ clonedResponseObject[kRealm] = this[kRealm]
+ clonedResponseObject[kHeaders][kHeadersList] = clonedResponse.headersList
+ clonedResponseObject[kHeaders][kGuard] = this[kHeaders][kGuard]
+ clonedResponseObject[kHeaders][kRealm] = this[kHeaders][kRealm]
+
+ return clonedResponseObject
+ }
+}
+
+mixinBody(Response)
+
+Object.defineProperties(Response.prototype, {
+ type: kEnumerableProperty,
+ url: kEnumerableProperty,
+ status: kEnumerableProperty,
+ ok: kEnumerableProperty,
+ redirected: kEnumerableProperty,
+ statusText: kEnumerableProperty,
+ headers: kEnumerableProperty,
+ clone: kEnumerableProperty,
+ body: kEnumerableProperty,
+ bodyUsed: kEnumerableProperty,
+ [Symbol.toStringTag]: {
+ value: 'Response',
+ configurable: true
+ }
+})
+
+Object.defineProperties(Response, {
+ json: kEnumerableProperty,
+ redirect: kEnumerableProperty,
+ error: kEnumerableProperty
+})
+
+// https://fetch.spec.whatwg.org/#concept-response-clone
+function cloneResponse (response) {
+ // To clone a response response, run these steps:
+
+ // 1. If response is a filtered response, then return a new identical
+ // filtered response whose internal response is a clone of response’s
+ // internal response.
+ if (response.internalResponse) {
+ return filterResponse(
+ cloneResponse(response.internalResponse),
+ response.type
+ )
+ }
+
+ // 2. Let newResponse be a copy of response, except for its body.
+ const newResponse = makeResponse({ ...response, body: null })
+
+ // 3. If response’s body is non-null, then set newResponse’s body to the
+ // result of cloning response’s body.
+ if (response.body != null) {
+ newResponse.body = cloneBody(response.body)
+ }
+
+ // 4. Return newResponse.
+ return newResponse
+}
+
+function makeResponse (init) {
+ return {
+ aborted: false,
+ rangeRequested: false,
+ timingAllowPassed: false,
+ requestIncludesCredentials: false,
+ type: 'default',
+ status: 200,
+ timingInfo: null,
+ cacheState: '',
+ statusText: '',
+ ...init,
+ headersList: init.headersList
+ ? new HeadersList(init.headersList)
+ : new HeadersList(),
+ urlList: init.urlList ? [...init.urlList] : []
+ }
+}
+
+function makeNetworkError (reason) {
+ const isError = isErrorLike(reason)
+ return makeResponse({
+ type: 'error',
+ status: 0,
+ error: isError
+ ? reason
+ : new Error(reason ? String(reason) : reason),
+ aborted: reason && reason.name === 'AbortError'
+ })
+}
+
+function makeFilteredResponse (response, state) {
+ state = {
+ internalResponse: response,
+ ...state
+ }
+
+ return new Proxy(response, {
+ get (target, p) {
+ return p in state ? state[p] : target[p]
+ },
+ set (target, p, value) {
+ assert(!(p in state))
+ target[p] = value
+ return true
+ }
+ })
+}
+
+// https://fetch.spec.whatwg.org/#concept-filtered-response
+function filterResponse (response, type) {
+ // Set response to the following filtered response with response as its
+ // internal response, depending on request’s response tainting:
+ if (type === 'basic') {
+ // A basic filtered response is a filtered response whose type is "basic"
+ // and header list excludes any headers in internal response’s header list
+ // whose name is a forbidden response-header name.
+
+ // Note: undici does not implement forbidden response-header names
+ return makeFilteredResponse(response, {
+ type: 'basic',
+ headersList: response.headersList
+ })
+ } else if (type === 'cors') {
+ // A CORS filtered response is a filtered response whose type is "cors"
+ // and header list excludes any headers in internal response’s header
+ // list whose name is not a CORS-safelisted response-header name, given
+ // internal response’s CORS-exposed header-name list.
+
+ // Note: undici does not implement CORS-safelisted response-header names
+ return makeFilteredResponse(response, {
+ type: 'cors',
+ headersList: response.headersList
+ })
+ } else if (type === 'opaque') {
+ // An opaque filtered response is a filtered response whose type is
+ // "opaque", URL list is the empty list, status is 0, status message
+ // is the empty byte sequence, header list is empty, and body is null.
+
+ return makeFilteredResponse(response, {
+ type: 'opaque',
+ urlList: Object.freeze([]),
+ status: 0,
+ statusText: '',
+ body: null
+ })
+ } else if (type === 'opaqueredirect') {
+ // An opaque-redirect filtered response is a filtered response whose type
+ // is "opaqueredirect", status is 0, status message is the empty byte
+ // sequence, header list is empty, and body is null.
+
+ return makeFilteredResponse(response, {
+ type: 'opaqueredirect',
+ status: 0,
+ statusText: '',
+ headersList: [],
+ body: null
+ })
+ } else {
+ assert(false)
+ }
+}
+
+// https://fetch.spec.whatwg.org/#appropriate-network-error
+function makeAppropriateNetworkError (fetchParams, err = null) {
+ // 1. Assert: fetchParams is canceled.
+ assert(isCancelled(fetchParams))
+
+ // 2. Return an aborted network error if fetchParams is aborted;
+ // otherwise return a network error.
+ return isAborted(fetchParams)
+ ? makeNetworkError(Object.assign(new DOMException('The operation was aborted.', 'AbortError'), { cause: err }))
+ : makeNetworkError(Object.assign(new DOMException('Request was cancelled.'), { cause: err }))
+}
+
+// https://whatpr.org/fetch/1392.html#initialize-a-response
+function initializeResponse (response, init, body) {
+ // 1. If init["status"] is not in the range 200 to 599, inclusive, then
+ // throw a RangeError.
+ if (init.status !== null && (init.status < 200 || init.status > 599)) {
+ throw new RangeError('init["status"] must be in the range of 200 to 599, inclusive.')
+ }
+
+ // 2. If init["statusText"] does not match the reason-phrase token production,
+ // then throw a TypeError.
+ if ('statusText' in init && init.statusText != null) {
+ // See, https://datatracker.ietf.org/doc/html/rfc7230#section-3.1.2:
+ // reason-phrase = *( HTAB / SP / VCHAR / obs-text )
+ if (!isValidReasonPhrase(String(init.statusText))) {
+ throw new TypeError('Invalid statusText')
+ }
+ }
+
+ // 3. Set response’s response’s status to init["status"].
+ if ('status' in init && init.status != null) {
+ response[kState].status = init.status
+ }
+
+ // 4. Set response’s response’s status message to init["statusText"].
+ if ('statusText' in init && init.statusText != null) {
+ response[kState].statusText = init.statusText
+ }
+
+ // 5. If init["headers"] exists, then fill response’s headers with init["headers"].
+ if ('headers' in init && init.headers != null) {
+ fill(response[kHeaders], init.headers)
+ }
+
+ // 6. If body was given, then:
+ if (body) {
+ // 1. If response's status is a null body status, then throw a TypeError.
+ if (nullBodyStatus.includes(response.status)) {
+ throw webidl.errors.exception({
+ header: 'Response constructor',
+ message: 'Invalid response status code ' + response.status
+ })
+ }
+
+ // 2. Set response's body to body's body.
+ response[kState].body = body.body
+
+ // 3. If body's type is non-null and response's header list does not contain
+ // `Content-Type`, then append (`Content-Type`, body's type) to response's header list.
+ if (body.type != null && !response[kState].headersList.contains('Content-Type')) {
+ response[kState].headersList.append('content-type', body.type)
+ }
+ }
+}
+
+webidl.converters.ReadableStream = webidl.interfaceConverter(
+ ReadableStream
+)
+
+webidl.converters.FormData = webidl.interfaceConverter(
+ FormData
+)
+
+webidl.converters.URLSearchParams = webidl.interfaceConverter(
+ URLSearchParams
+)
+
+// https://fetch.spec.whatwg.org/#typedefdef-xmlhttprequestbodyinit
+webidl.converters.XMLHttpRequestBodyInit = function (V) {
+ if (typeof V === 'string') {
+ return webidl.converters.USVString(V)
+ }
+
+ if (isBlobLike(V)) {
+ return webidl.converters.Blob(V, { strict: false })
+ }
+
+ if (types.isArrayBuffer(V) || types.isTypedArray(V) || types.isDataView(V)) {
+ return webidl.converters.BufferSource(V)
+ }
+
+ if (util.isFormDataLike(V)) {
+ return webidl.converters.FormData(V, { strict: false })
+ }
+
+ if (V instanceof URLSearchParams) {
+ return webidl.converters.URLSearchParams(V)
+ }
+
+ return webidl.converters.DOMString(V)
+}
+
+// https://fetch.spec.whatwg.org/#bodyinit
+webidl.converters.BodyInit = function (V) {
+ if (V instanceof ReadableStream) {
+ return webidl.converters.ReadableStream(V)
+ }
+
+ // Note: the spec doesn't include async iterables,
+ // this is an undici extension.
+ if (V?.[Symbol.asyncIterator]) {
+ return V
+ }
+
+ return webidl.converters.XMLHttpRequestBodyInit(V)
+}
+
+webidl.converters.ResponseInit = webidl.dictionaryConverter([
+ {
+ key: 'status',
+ converter: webidl.converters['unsigned short'],
+ defaultValue: 200
+ },
+ {
+ key: 'statusText',
+ converter: webidl.converters.ByteString,
+ defaultValue: ''
+ },
+ {
+ key: 'headers',
+ converter: webidl.converters.HeadersInit
+ }
+])
+
+module.exports = {
+ makeNetworkError,
+ makeResponse,
+ makeAppropriateNetworkError,
+ filterResponse,
+ Response,
+ cloneResponse
+}
diff --git a/lib/fetch/symbols.js b/lib/fetch/symbols.js
new file mode 100644
index 0000000..0b947d5
--- /dev/null
+++ b/lib/fetch/symbols.js
@@ -0,0 +1,10 @@
+'use strict'
+
+module.exports = {
+ kUrl: Symbol('url'),
+ kHeaders: Symbol('headers'),
+ kSignal: Symbol('signal'),
+ kState: Symbol('state'),
+ kGuard: Symbol('guard'),
+ kRealm: Symbol('realm')
+}
diff --git a/lib/fetch/util.js b/lib/fetch/util.js
new file mode 100644
index 0000000..b12142c
--- /dev/null
+++ b/lib/fetch/util.js
@@ -0,0 +1,1071 @@
+'use strict'
+
+const { redirectStatusSet, referrerPolicySet: referrerPolicyTokens, badPortsSet } = require('./constants')
+const { getGlobalOrigin } = require('./global')
+const { performance } = require('perf_hooks')
+const { isBlobLike, toUSVString, ReadableStreamFrom } = require('../core/util')
+const assert = require('assert')
+const { isUint8Array } = require('util/types')
+
+// https://nodejs.org/api/crypto.html#determining-if-crypto-support-is-unavailable
+/** @type {import('crypto')|undefined} */
+let crypto
+
+try {
+ crypto = require('crypto')
+} catch {
+
+}
+
+function responseURL (response) {
+ // https://fetch.spec.whatwg.org/#responses
+ // A response has an associated URL. It is a pointer to the last URL
+ // in response’s URL list and null if response’s URL list is empty.
+ const urlList = response.urlList
+ const length = urlList.length
+ return length === 0 ? null : urlList[length - 1].toString()
+}
+
+// https://fetch.spec.whatwg.org/#concept-response-location-url
+function responseLocationURL (response, requestFragment) {
+ // 1. If response’s status is not a redirect status, then return null.
+ if (!redirectStatusSet.has(response.status)) {
+ return null
+ }
+
+ // 2. Let location be the result of extracting header list values given
+ // `Location` and response’s header list.
+ let location = response.headersList.get('location')
+
+ // 3. If location is a header value, then set location to the result of
+ // parsing location with response’s URL.
+ if (location !== null && isValidHeaderValue(location)) {
+ location = new URL(location, responseURL(response))
+ }
+
+ // 4. If location is a URL whose fragment is null, then set location’s
+ // fragment to requestFragment.
+ if (location && !location.hash) {
+ location.hash = requestFragment
+ }
+
+ // 5. Return location.
+ return location
+}
+
+/** @returns {URL} */
+function requestCurrentURL (request) {
+ return request.urlList[request.urlList.length - 1]
+}
+
+function requestBadPort (request) {
+ // 1. Let url be request’s current URL.
+ const url = requestCurrentURL(request)
+
+ // 2. If url’s scheme is an HTTP(S) scheme and url’s port is a bad port,
+ // then return blocked.
+ if (urlIsHttpHttpsScheme(url) && badPortsSet.has(url.port)) {
+ return 'blocked'
+ }
+
+ // 3. Return allowed.
+ return 'allowed'
+}
+
+function isErrorLike (object) {
+ return object instanceof Error || (
+ object?.constructor?.name === 'Error' ||
+ object?.constructor?.name === 'DOMException'
+ )
+}
+
+// Check whether |statusText| is a ByteString and
+// matches the Reason-Phrase token production.
+// RFC 2616: https://tools.ietf.org/html/rfc2616
+// RFC 7230: https://tools.ietf.org/html/rfc7230
+// "reason-phrase = *( HTAB / SP / VCHAR / obs-text )"
+// https://github.com/chromium/chromium/blob/94.0.4604.1/third_party/blink/renderer/core/fetch/response.cc#L116
+function isValidReasonPhrase (statusText) {
+ for (let i = 0; i < statusText.length; ++i) {
+ const c = statusText.charCodeAt(i)
+ if (
+ !(
+ (
+ c === 0x09 || // HTAB
+ (c >= 0x20 && c <= 0x7e) || // SP / VCHAR
+ (c >= 0x80 && c <= 0xff)
+ ) // obs-text
+ )
+ ) {
+ return false
+ }
+ }
+ return true
+}
+
+/**
+ * @see https://tools.ietf.org/html/rfc7230#section-3.2.6
+ * @param {number} c
+ */
+function isTokenCharCode (c) {
+ switch (c) {
+ case 0x22:
+ case 0x28:
+ case 0x29:
+ case 0x2c:
+ case 0x2f:
+ case 0x3a:
+ case 0x3b:
+ case 0x3c:
+ case 0x3d:
+ case 0x3e:
+ case 0x3f:
+ case 0x40:
+ case 0x5b:
+ case 0x5c:
+ case 0x5d:
+ case 0x7b:
+ case 0x7d:
+ // DQUOTE and "(),/:;<=>?@[\]{}"
+ return false
+ default:
+ // VCHAR %x21-7E
+ return c >= 0x21 && c <= 0x7e
+ }
+}
+
+/**
+ * @param {string} characters
+ */
+function isValidHTTPToken (characters) {
+ if (characters.length === 0) {
+ return false
+ }
+ for (let i = 0; i < characters.length; ++i) {
+ if (!isTokenCharCode(characters.charCodeAt(i))) {
+ return false
+ }
+ }
+ return true
+}
+
+/**
+ * @see https://fetch.spec.whatwg.org/#header-name
+ * @param {string} potentialValue
+ */
+function isValidHeaderName (potentialValue) {
+ return isValidHTTPToken(potentialValue)
+}
+
+/**
+ * @see https://fetch.spec.whatwg.org/#header-value
+ * @param {string} potentialValue
+ */
+function isValidHeaderValue (potentialValue) {
+ // - Has no leading or trailing HTTP tab or space bytes.
+ // - Contains no 0x00 (NUL) or HTTP newline bytes.
+ if (
+ potentialValue.startsWith('\t') ||
+ potentialValue.startsWith(' ') ||
+ potentialValue.endsWith('\t') ||
+ potentialValue.endsWith(' ')
+ ) {
+ return false
+ }
+
+ if (
+ potentialValue.includes('\0') ||
+ potentialValue.includes('\r') ||
+ potentialValue.includes('\n')
+ ) {
+ return false
+ }
+
+ return true
+}
+
+// https://w3c.github.io/webappsec-referrer-policy/#set-requests-referrer-policy-on-redirect
+function setRequestReferrerPolicyOnRedirect (request, actualResponse) {
+ // Given a request request and a response actualResponse, this algorithm
+ // updates request’s referrer policy according to the Referrer-Policy
+ // header (if any) in actualResponse.
+
+ // 1. Let policy be the result of executing § 8.1 Parse a referrer policy
+ // from a Referrer-Policy header on actualResponse.
+
+ // 8.1 Parse a referrer policy from a Referrer-Policy header
+ // 1. Let policy-tokens be the result of extracting header list values given `Referrer-Policy` and response’s header list.
+ const { headersList } = actualResponse
+ // 2. Let policy be the empty string.
+ // 3. For each token in policy-tokens, if token is a referrer policy and token is not the empty string, then set policy to token.
+ // 4. Return policy.
+ const policyHeader = (headersList.get('referrer-policy') ?? '').split(',')
+
+ // Note: As the referrer-policy can contain multiple policies
+ // separated by comma, we need to loop through all of them
+ // and pick the first valid one.
+ // Ref: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy#specify_a_fallback_policy
+ let policy = ''
+ if (policyHeader.length > 0) {
+ // The right-most policy takes precedence.
+ // The left-most policy is the fallback.
+ for (let i = policyHeader.length; i !== 0; i--) {
+ const token = policyHeader[i - 1].trim()
+ if (referrerPolicyTokens.has(token)) {
+ policy = token
+ break
+ }
+ }
+ }
+
+ // 2. If policy is not the empty string, then set request’s referrer policy to policy.
+ if (policy !== '') {
+ request.referrerPolicy = policy
+ }
+}
+
+// https://fetch.spec.whatwg.org/#cross-origin-resource-policy-check
+function crossOriginResourcePolicyCheck () {
+ // TODO
+ return 'allowed'
+}
+
+// https://fetch.spec.whatwg.org/#concept-cors-check
+function corsCheck () {
+ // TODO
+ return 'success'
+}
+
+// https://fetch.spec.whatwg.org/#concept-tao-check
+function TAOCheck () {
+ // TODO
+ return 'success'
+}
+
+function appendFetchMetadata (httpRequest) {
+ // https://w3c.github.io/webappsec-fetch-metadata/#sec-fetch-dest-header
+ // TODO
+
+ // https://w3c.github.io/webappsec-fetch-metadata/#sec-fetch-mode-header
+
+ // 1. Assert: r’s url is a potentially trustworthy URL.
+ // TODO
+
+ // 2. Let header be a Structured Header whose value is a token.
+ let header = null
+
+ // 3. Set header’s value to r’s mode.
+ header = httpRequest.mode
+
+ // 4. Set a structured field value `Sec-Fetch-Mode`/header in r’s header list.
+ httpRequest.headersList.set('sec-fetch-mode', header)
+
+ // https://w3c.github.io/webappsec-fetch-metadata/#sec-fetch-site-header
+ // TODO
+
+ // https://w3c.github.io/webappsec-fetch-metadata/#sec-fetch-user-header
+ // TODO
+}
+
+// https://fetch.spec.whatwg.org/#append-a-request-origin-header
+function appendRequestOriginHeader (request) {
+ // 1. Let serializedOrigin be the result of byte-serializing a request origin with request.
+ let serializedOrigin = request.origin
+
+ // 2. If request’s response tainting is "cors" or request’s mode is "websocket", then append (`Origin`, serializedOrigin) to request’s header list.
+ if (request.responseTainting === 'cors' || request.mode === 'websocket') {
+ if (serializedOrigin) {
+ request.headersList.append('origin', serializedOrigin)
+ }
+
+ // 3. Otherwise, if request’s method is neither `GET` nor `HEAD`, then:
+ } else if (request.method !== 'GET' && request.method !== 'HEAD') {
+ // 1. Switch on request’s referrer policy:
+ switch (request.referrerPolicy) {
+ case 'no-referrer':
+ // Set serializedOrigin to `null`.
+ serializedOrigin = null
+ break
+ case 'no-referrer-when-downgrade':
+ case 'strict-origin':
+ case 'strict-origin-when-cross-origin':
+ // If request’s origin is a tuple origin, its scheme is "https", and request’s current URL’s scheme is not "https", then set serializedOrigin to `null`.
+ if (request.origin && urlHasHttpsScheme(request.origin) && !urlHasHttpsScheme(requestCurrentURL(request))) {
+ serializedOrigin = null
+ }
+ break
+ case 'same-origin':
+ // If request’s origin is not same origin with request’s current URL’s origin, then set serializedOrigin to `null`.
+ if (!sameOrigin(request, requestCurrentURL(request))) {
+ serializedOrigin = null
+ }
+ break
+ default:
+ // Do nothing.
+ }
+
+ if (serializedOrigin) {
+ // 2. Append (`Origin`, serializedOrigin) to request’s header list.
+ request.headersList.append('origin', serializedOrigin)
+ }
+ }
+}
+
+function coarsenedSharedCurrentTime (crossOriginIsolatedCapability) {
+ // TODO
+ return performance.now()
+}
+
+// https://fetch.spec.whatwg.org/#create-an-opaque-timing-info
+function createOpaqueTimingInfo (timingInfo) {
+ return {
+ startTime: timingInfo.startTime ?? 0,
+ redirectStartTime: 0,
+ redirectEndTime: 0,
+ postRedirectStartTime: timingInfo.startTime ?? 0,
+ finalServiceWorkerStartTime: 0,
+ finalNetworkResponseStartTime: 0,
+ finalNetworkRequestStartTime: 0,
+ endTime: 0,
+ encodedBodySize: 0,
+ decodedBodySize: 0,
+ finalConnectionTimingInfo: null
+ }
+}
+
+// https://html.spec.whatwg.org/multipage/origin.html#policy-container
+function makePolicyContainer () {
+ // Note: the fetch spec doesn't make use of embedder policy or CSP list
+ return {
+ referrerPolicy: 'strict-origin-when-cross-origin'
+ }
+}
+
+// https://html.spec.whatwg.org/multipage/origin.html#clone-a-policy-container
+function clonePolicyContainer (policyContainer) {
+ return {
+ referrerPolicy: policyContainer.referrerPolicy
+ }
+}
+
+// https://w3c.github.io/webappsec-referrer-policy/#determine-requests-referrer
+function determineRequestsReferrer (request) {
+ // 1. Let policy be request's referrer policy.
+ const policy = request.referrerPolicy
+
+ // Note: policy cannot (shouldn't) be null or an empty string.
+ assert(policy)
+
+ // 2. Let environment be request’s client.
+
+ let referrerSource = null
+
+ // 3. Switch on request’s referrer:
+ if (request.referrer === 'client') {
+ // Note: node isn't a browser and doesn't implement document/iframes,
+ // so we bypass this step and replace it with our own.
+
+ const globalOrigin = getGlobalOrigin()
+
+ if (!globalOrigin || globalOrigin.origin === 'null') {
+ return 'no-referrer'
+ }
+
+ // note: we need to clone it as it's mutated
+ referrerSource = new URL(globalOrigin)
+ } else if (request.referrer instanceof URL) {
+ // Let referrerSource be request’s referrer.
+ referrerSource = request.referrer
+ }
+
+ // 4. Let request’s referrerURL be the result of stripping referrerSource for
+ // use as a referrer.
+ let referrerURL = stripURLForReferrer(referrerSource)
+
+ // 5. Let referrerOrigin be the result of stripping referrerSource for use as
+ // a referrer, with the origin-only flag set to true.
+ const referrerOrigin = stripURLForReferrer(referrerSource, true)
+
+ // 6. If the result of serializing referrerURL is a string whose length is
+ // greater than 4096, set referrerURL to referrerOrigin.
+ if (referrerURL.toString().length > 4096) {
+ referrerURL = referrerOrigin
+ }
+
+ const areSameOrigin = sameOrigin(request, referrerURL)
+ const isNonPotentiallyTrustWorthy = isURLPotentiallyTrustworthy(referrerURL) &&
+ !isURLPotentiallyTrustworthy(request.url)
+
+ // 8. Execute the switch statements corresponding to the value of policy:
+ switch (policy) {
+ case 'origin': return referrerOrigin != null ? referrerOrigin : stripURLForReferrer(referrerSource, true)
+ case 'unsafe-url': return referrerURL
+ case 'same-origin':
+ return areSameOrigin ? referrerOrigin : 'no-referrer'
+ case 'origin-when-cross-origin':
+ return areSameOrigin ? referrerURL : referrerOrigin
+ case 'strict-origin-when-cross-origin': {
+ const currentURL = requestCurrentURL(request)
+
+ // 1. If the origin of referrerURL and the origin of request’s current
+ // URL are the same, then return referrerURL.
+ if (sameOrigin(referrerURL, currentURL)) {
+ return referrerURL
+ }
+
+ // 2. If referrerURL is a potentially trustworthy URL and request’s
+ // current URL is not a potentially trustworthy URL, then return no
+ // referrer.
+ if (isURLPotentiallyTrustworthy(referrerURL) && !isURLPotentiallyTrustworthy(currentURL)) {
+ return 'no-referrer'
+ }
+
+ // 3. Return referrerOrigin.
+ return referrerOrigin
+ }
+ case 'strict-origin': // eslint-disable-line
+ /**
+ * 1. If referrerURL is a potentially trustworthy URL and
+ * request’s current URL is not a potentially trustworthy URL,
+ * then return no referrer.
+ * 2. Return referrerOrigin
+ */
+ case 'no-referrer-when-downgrade': // eslint-disable-line
+ /**
+ * 1. If referrerURL is a potentially trustworthy URL and
+ * request’s current URL is not a potentially trustworthy URL,
+ * then return no referrer.
+ * 2. Return referrerOrigin
+ */
+
+ default: // eslint-disable-line
+ return isNonPotentiallyTrustWorthy ? 'no-referrer' : referrerOrigin
+ }
+}
+
+/**
+ * @see https://w3c.github.io/webappsec-referrer-policy/#strip-url
+ * @param {URL} url
+ * @param {boolean|undefined} originOnly
+ */
+function stripURLForReferrer (url, originOnly) {
+ // 1. Assert: url is a URL.
+ assert(url instanceof URL)
+
+ // 2. If url’s scheme is a local scheme, then return no referrer.
+ if (url.protocol === 'file:' || url.protocol === 'about:' || url.protocol === 'blank:') {
+ return 'no-referrer'
+ }
+
+ // 3. Set url’s username to the empty string.
+ url.username = ''
+
+ // 4. Set url’s password to the empty string.
+ url.password = ''
+
+ // 5. Set url’s fragment to null.
+ url.hash = ''
+
+ // 6. If the origin-only flag is true, then:
+ if (originOnly) {
+ // 1. Set url’s path to « the empty string ».
+ url.pathname = ''
+
+ // 2. Set url’s query to null.
+ url.search = ''
+ }
+
+ // 7. Return url.
+ return url
+}
+
+function isURLPotentiallyTrustworthy (url) {
+ if (!(url instanceof URL)) {
+ return false
+ }
+
+ // If child of about, return true
+ if (url.href === 'about:blank' || url.href === 'about:srcdoc') {
+ return true
+ }
+
+ // If scheme is data, return true
+ if (url.protocol === 'data:') return true
+
+ // If file, return true
+ if (url.protocol === 'file:') return true
+
+ return isOriginPotentiallyTrustworthy(url.origin)
+
+ function isOriginPotentiallyTrustworthy (origin) {
+ // If origin is explicitly null, return false
+ if (origin == null || origin === 'null') return false
+
+ const originAsURL = new URL(origin)
+
+ // If secure, return true
+ if (originAsURL.protocol === 'https:' || originAsURL.protocol === 'wss:') {
+ return true
+ }
+
+ // If localhost or variants, return true
+ if (/^127(?:\.[0-9]+){0,2}\.[0-9]+$|^\[(?:0*:)*?:?0*1\]$/.test(originAsURL.hostname) ||
+ (originAsURL.hostname === 'localhost' || originAsURL.hostname.includes('localhost.')) ||
+ (originAsURL.hostname.endsWith('.localhost'))) {
+ return true
+ }
+
+ // If any other, return false
+ return false
+ }
+}
+
+/**
+ * @see https://w3c.github.io/webappsec-subresource-integrity/#does-response-match-metadatalist
+ * @param {Uint8Array} bytes
+ * @param {string} metadataList
+ */
+function bytesMatch (bytes, metadataList) {
+ // If node is not built with OpenSSL support, we cannot check
+ // a request's integrity, so allow it by default (the spec will
+ // allow requests if an invalid hash is given, as precedence).
+ /* istanbul ignore if: only if node is built with --without-ssl */
+ if (crypto === undefined) {
+ return true
+ }
+
+ // 1. Let parsedMetadata be the result of parsing metadataList.
+ const parsedMetadata = parseMetadata(metadataList)
+
+ // 2. If parsedMetadata is no metadata, return true.
+ if (parsedMetadata === 'no metadata') {
+ return true
+ }
+
+ // 3. If parsedMetadata is the empty set, return true.
+ if (parsedMetadata.length === 0) {
+ return true
+ }
+
+ // 4. Let metadata be the result of getting the strongest
+ // metadata from parsedMetadata.
+ const list = parsedMetadata.sort((c, d) => d.algo.localeCompare(c.algo))
+ // get the strongest algorithm
+ const strongest = list[0].algo
+ // get all entries that use the strongest algorithm; ignore weaker
+ const metadata = list.filter((item) => item.algo === strongest)
+
+ // 5. For each item in metadata:
+ for (const item of metadata) {
+ // 1. Let algorithm be the alg component of item.
+ const algorithm = item.algo
+
+ // 2. Let expectedValue be the val component of item.
+ let expectedValue = item.hash
+
+ // See https://github.com/web-platform-tests/wpt/commit/e4c5cc7a5e48093220528dfdd1c4012dc3837a0e
+ // "be liberal with padding". This is annoying, and it's not even in the spec.
+
+ if (expectedValue.endsWith('==')) {
+ expectedValue = expectedValue.slice(0, -2)
+ }
+
+ // 3. Let actualValue be the result of applying algorithm to bytes.
+ let actualValue = crypto.createHash(algorithm).update(bytes).digest('base64')
+
+ if (actualValue.endsWith('==')) {
+ actualValue = actualValue.slice(0, -2)
+ }
+
+ // 4. If actualValue is a case-sensitive match for expectedValue,
+ // return true.
+ if (actualValue === expectedValue) {
+ return true
+ }
+
+ let actualBase64URL = crypto.createHash(algorithm).update(bytes).digest('base64url')
+
+ if (actualBase64URL.endsWith('==')) {
+ actualBase64URL = actualBase64URL.slice(0, -2)
+ }
+
+ if (actualBase64URL === expectedValue) {
+ return true
+ }
+ }
+
+ // 6. Return false.
+ return false
+}
+
+// https://w3c.github.io/webappsec-subresource-integrity/#grammardef-hash-with-options
+// https://www.w3.org/TR/CSP2/#source-list-syntax
+// https://www.rfc-editor.org/rfc/rfc5234#appendix-B.1
+const parseHashWithOptions = /((?<algo>sha256|sha384|sha512)-(?<hash>[A-z0-9+/]{1}.*={0,2}))( +[\x21-\x7e]?)?/i
+
+/**
+ * @see https://w3c.github.io/webappsec-subresource-integrity/#parse-metadata
+ * @param {string} metadata
+ */
+function parseMetadata (metadata) {
+ // 1. Let result be the empty set.
+ /** @type {{ algo: string, hash: string }[]} */
+ const result = []
+
+ // 2. Let empty be equal to true.
+ let empty = true
+
+ const supportedHashes = crypto.getHashes()
+
+ // 3. For each token returned by splitting metadata on spaces:
+ for (const token of metadata.split(' ')) {
+ // 1. Set empty to false.
+ empty = false
+
+ // 2. Parse token as a hash-with-options.
+ const parsedToken = parseHashWithOptions.exec(token)
+
+ // 3. If token does not parse, continue to the next token.
+ if (parsedToken === null || parsedToken.groups === undefined) {
+ // Note: Chromium blocks the request at this point, but Firefox
+ // gives a warning that an invalid integrity was given. The
+ // correct behavior is to ignore these, and subsequently not
+ // check the integrity of the resource.
+ continue
+ }
+
+ // 4. Let algorithm be the hash-algo component of token.
+ const algorithm = parsedToken.groups.algo
+
+ // 5. If algorithm is a hash function recognized by the user
+ // agent, add the parsed token to result.
+ if (supportedHashes.includes(algorithm.toLowerCase())) {
+ result.push(parsedToken.groups)
+ }
+ }
+
+ // 4. Return no metadata if empty is true, otherwise return result.
+ if (empty === true) {
+ return 'no metadata'
+ }
+
+ return result
+}
+
+// https://w3c.github.io/webappsec-upgrade-insecure-requests/#upgrade-request
+function tryUpgradeRequestToAPotentiallyTrustworthyURL (request) {
+ // TODO
+}
+
+/**
+ * @link {https://html.spec.whatwg.org/multipage/origin.html#same-origin}
+ * @param {URL} A
+ * @param {URL} B
+ */
+function sameOrigin (A, B) {
+ // 1. If A and B are the same opaque origin, then return true.
+ if (A.origin === B.origin && A.origin === 'null') {
+ return true
+ }
+
+ // 2. If A and B are both tuple origins and their schemes,
+ // hosts, and port are identical, then return true.
+ if (A.protocol === B.protocol && A.hostname === B.hostname && A.port === B.port) {
+ return true
+ }
+
+ // 3. Return false.
+ return false
+}
+
+function createDeferredPromise () {
+ let res
+ let rej
+ const promise = new Promise((resolve, reject) => {
+ res = resolve
+ rej = reject
+ })
+
+ return { promise, resolve: res, reject: rej }
+}
+
+function isAborted (fetchParams) {
+ return fetchParams.controller.state === 'aborted'
+}
+
+function isCancelled (fetchParams) {
+ return fetchParams.controller.state === 'aborted' ||
+ fetchParams.controller.state === 'terminated'
+}
+
+const normalizeMethodRecord = {
+ delete: 'DELETE',
+ DELETE: 'DELETE',
+ get: 'GET',
+ GET: 'GET',
+ head: 'HEAD',
+ HEAD: 'HEAD',
+ options: 'OPTIONS',
+ OPTIONS: 'OPTIONS',
+ post: 'POST',
+ POST: 'POST',
+ put: 'PUT',
+ PUT: 'PUT'
+}
+
+// Note: object prototypes should not be able to be referenced. e.g. `Object#hasOwnProperty`.
+Object.setPrototypeOf(normalizeMethodRecord, null)
+
+/**
+ * @see https://fetch.spec.whatwg.org/#concept-method-normalize
+ * @param {string} method
+ */
+function normalizeMethod (method) {
+ return normalizeMethodRecord[method.toLowerCase()] ?? method
+}
+
+// https://infra.spec.whatwg.org/#serialize-a-javascript-value-to-a-json-string
+function serializeJavascriptValueToJSONString (value) {
+ // 1. Let result be ? Call(%JSON.stringify%, undefined, « value »).
+ const result = JSON.stringify(value)
+
+ // 2. If result is undefined, then throw a TypeError.
+ if (result === undefined) {
+ throw new TypeError('Value is not JSON serializable')
+ }
+
+ // 3. Assert: result is a string.
+ assert(typeof result === 'string')
+
+ // 4. Return result.
+ return result
+}
+
+// https://tc39.es/ecma262/#sec-%25iteratorprototype%25-object
+const esIteratorPrototype = Object.getPrototypeOf(Object.getPrototypeOf([][Symbol.iterator]()))
+
+/**
+ * @see https://webidl.spec.whatwg.org/#dfn-iterator-prototype-object
+ * @param {() => unknown[]} iterator
+ * @param {string} name name of the instance
+ * @param {'key'|'value'|'key+value'} kind
+ */
+function makeIterator (iterator, name, kind) {
+ const object = {
+ index: 0,
+ kind,
+ target: iterator
+ }
+
+ const i = {
+ next () {
+ // 1. Let interface be the interface for which the iterator prototype object exists.
+
+ // 2. Let thisValue be the this value.
+
+ // 3. Let object be ? ToObject(thisValue).
+
+ // 4. If object is a platform object, then perform a security
+ // check, passing:
+
+ // 5. If object is not a default iterator object for interface,
+ // then throw a TypeError.
+ if (Object.getPrototypeOf(this) !== i) {
+ throw new TypeError(
+ `'next' called on an object that does not implement interface ${name} Iterator.`
+ )
+ }
+
+ // 6. Let index be object’s index.
+ // 7. Let kind be object’s kind.
+ // 8. Let values be object’s target's value pairs to iterate over.
+ const { index, kind, target } = object
+ const values = target()
+
+ // 9. Let len be the length of values.
+ const len = values.length
+
+ // 10. If index is greater than or equal to len, then return
+ // CreateIterResultObject(undefined, true).
+ if (index >= len) {
+ return { value: undefined, done: true }
+ }
+
+ // 11. Let pair be the entry in values at index index.
+ const pair = values[index]
+
+ // 12. Set object’s index to index + 1.
+ object.index = index + 1
+
+ // 13. Return the iterator result for pair and kind.
+ return iteratorResult(pair, kind)
+ },
+ // The class string of an iterator prototype object for a given interface is the
+ // result of concatenating the identifier of the interface and the string " Iterator".
+ [Symbol.toStringTag]: `${name} Iterator`
+ }
+
+ // The [[Prototype]] internal slot of an iterator prototype object must be %IteratorPrototype%.
+ Object.setPrototypeOf(i, esIteratorPrototype)
+ // esIteratorPrototype needs to be the prototype of i
+ // which is the prototype of an empty object. Yes, it's confusing.
+ return Object.setPrototypeOf({}, i)
+}
+
+// https://webidl.spec.whatwg.org/#iterator-result
+function iteratorResult (pair, kind) {
+ let result
+
+ // 1. Let result be a value determined by the value of kind:
+ switch (kind) {
+ case 'key': {
+ // 1. Let idlKey be pair’s key.
+ // 2. Let key be the result of converting idlKey to an
+ // ECMAScript value.
+ // 3. result is key.
+ result = pair[0]
+ break
+ }
+ case 'value': {
+ // 1. Let idlValue be pair’s value.
+ // 2. Let value be the result of converting idlValue to
+ // an ECMAScript value.
+ // 3. result is value.
+ result = pair[1]
+ break
+ }
+ case 'key+value': {
+ // 1. Let idlKey be pair’s key.
+ // 2. Let idlValue be pair’s value.
+ // 3. Let key be the result of converting idlKey to an
+ // ECMAScript value.
+ // 4. Let value be the result of converting idlValue to
+ // an ECMAScript value.
+ // 5. Let array be ! ArrayCreate(2).
+ // 6. Call ! CreateDataProperty(array, "0", key).
+ // 7. Call ! CreateDataProperty(array, "1", value).
+ // 8. result is array.
+ result = pair
+ break
+ }
+ }
+
+ // 2. Return CreateIterResultObject(result, false).
+ return { value: result, done: false }
+}
+
+/**
+ * @see https://fetch.spec.whatwg.org/#body-fully-read
+ */
+async function fullyReadBody (body, processBody, processBodyError) {
+ // 1. If taskDestination is null, then set taskDestination to
+ // the result of starting a new parallel queue.
+
+ // 2. Let successSteps given a byte sequence bytes be to queue a
+ // fetch task to run processBody given bytes, with taskDestination.
+ const successSteps = processBody
+
+ // 3. Let errorSteps be to queue a fetch task to run processBodyError,
+ // with taskDestination.
+ const errorSteps = processBodyError
+
+ // 4. Let reader be the result of getting a reader for body’s stream.
+ // If that threw an exception, then run errorSteps with that
+ // exception and return.
+ let reader
+
+ try {
+ reader = body.stream.getReader()
+ } catch (e) {
+ errorSteps(e)
+ return
+ }
+
+ // 5. Read all bytes from reader, given successSteps and errorSteps.
+ try {
+ const result = await readAllBytes(reader)
+ successSteps(result)
+ } catch (e) {
+ errorSteps(e)
+ }
+}
+
+/** @type {ReadableStream} */
+let ReadableStream = globalThis.ReadableStream
+
+function isReadableStreamLike (stream) {
+ if (!ReadableStream) {
+ ReadableStream = require('stream/web').ReadableStream
+ }
+
+ return stream instanceof ReadableStream || (
+ stream[Symbol.toStringTag] === 'ReadableStream' &&
+ typeof stream.tee === 'function'
+ )
+}
+
+const MAXIMUM_ARGUMENT_LENGTH = 65535
+
+/**
+ * @see https://infra.spec.whatwg.org/#isomorphic-decode
+ * @param {number[]|Uint8Array} input
+ */
+function isomorphicDecode (input) {
+ // 1. To isomorphic decode a byte sequence input, return a string whose code point
+ // length is equal to input’s length and whose code points have the same values
+ // as the values of input’s bytes, in the same order.
+
+ if (input.length < MAXIMUM_ARGUMENT_LENGTH) {
+ return String.fromCharCode(...input)
+ }
+
+ return input.reduce((previous, current) => previous + String.fromCharCode(current), '')
+}
+
+/**
+ * @param {ReadableStreamController<Uint8Array>} controller
+ */
+function readableStreamClose (controller) {
+ try {
+ controller.close()
+ } catch (err) {
+ // TODO: add comment explaining why this error occurs.
+ if (!err.message.includes('Controller is already closed')) {
+ throw err
+ }
+ }
+}
+
+/**
+ * @see https://infra.spec.whatwg.org/#isomorphic-encode
+ * @param {string} input
+ */
+function isomorphicEncode (input) {
+ // 1. Assert: input contains no code points greater than U+00FF.
+ for (let i = 0; i < input.length; i++) {
+ assert(input.charCodeAt(i) <= 0xFF)
+ }
+
+ // 2. Return a byte sequence whose length is equal to input’s code
+ // point length and whose bytes have the same values as the
+ // values of input’s code points, in the same order
+ return input
+}
+
+/**
+ * @see https://streams.spec.whatwg.org/#readablestreamdefaultreader-read-all-bytes
+ * @see https://streams.spec.whatwg.org/#read-loop
+ * @param {ReadableStreamDefaultReader} reader
+ */
+async function readAllBytes (reader) {
+ const bytes = []
+ let byteLength = 0
+
+ while (true) {
+ const { done, value: chunk } = await reader.read()
+
+ if (done) {
+ // 1. Call successSteps with bytes.
+ return Buffer.concat(bytes, byteLength)
+ }
+
+ // 1. If chunk is not a Uint8Array object, call failureSteps
+ // with a TypeError and abort these steps.
+ if (!isUint8Array(chunk)) {
+ throw new TypeError('Received non-Uint8Array chunk')
+ }
+
+ // 2. Append the bytes represented by chunk to bytes.
+ bytes.push(chunk)
+ byteLength += chunk.length
+
+ // 3. Read-loop given reader, bytes, successSteps, and failureSteps.
+ }
+}
+
+/**
+ * @see https://fetch.spec.whatwg.org/#is-local
+ * @param {URL} url
+ */
+function urlIsLocal (url) {
+ assert('protocol' in url) // ensure it's a url object
+
+ const protocol = url.protocol
+
+ return protocol === 'about:' || protocol === 'blob:' || protocol === 'data:'
+}
+
+/**
+ * @param {string|URL} url
+ */
+function urlHasHttpsScheme (url) {
+ if (typeof url === 'string') {
+ return url.startsWith('https:')
+ }
+
+ return url.protocol === 'https:'
+}
+
+/**
+ * @see https://fetch.spec.whatwg.org/#http-scheme
+ * @param {URL} url
+ */
+function urlIsHttpHttpsScheme (url) {
+ assert('protocol' in url) // ensure it's a url object
+
+ const protocol = url.protocol
+
+ return protocol === 'http:' || protocol === 'https:'
+}
+
+/**
+ * Fetch supports node >= 16.8.0, but Object.hasOwn was added in v16.9.0.
+ */
+const hasOwn = Object.hasOwn || ((dict, key) => Object.prototype.hasOwnProperty.call(dict, key))
+
+module.exports = {
+ isAborted,
+ isCancelled,
+ createDeferredPromise,
+ ReadableStreamFrom,
+ toUSVString,
+ tryUpgradeRequestToAPotentiallyTrustworthyURL,
+ coarsenedSharedCurrentTime,
+ determineRequestsReferrer,
+ makePolicyContainer,
+ clonePolicyContainer,
+ appendFetchMetadata,
+ appendRequestOriginHeader,
+ TAOCheck,
+ corsCheck,
+ crossOriginResourcePolicyCheck,
+ createOpaqueTimingInfo,
+ setRequestReferrerPolicyOnRedirect,
+ isValidHTTPToken,
+ requestBadPort,
+ requestCurrentURL,
+ responseURL,
+ responseLocationURL,
+ isBlobLike,
+ isURLPotentiallyTrustworthy,
+ isValidReasonPhrase,
+ sameOrigin,
+ normalizeMethod,
+ serializeJavascriptValueToJSONString,
+ makeIterator,
+ isValidHeaderName,
+ isValidHeaderValue,
+ hasOwn,
+ isErrorLike,
+ fullyReadBody,
+ bytesMatch,
+ isReadableStreamLike,
+ readableStreamClose,
+ isomorphicEncode,
+ isomorphicDecode,
+ urlIsLocal,
+ urlHasHttpsScheme,
+ urlIsHttpHttpsScheme,
+ readAllBytes,
+ normalizeMethodRecord
+}
diff --git a/lib/fetch/webidl.js b/lib/fetch/webidl.js
new file mode 100644
index 0000000..6fcf2ab
--- /dev/null
+++ b/lib/fetch/webidl.js
@@ -0,0 +1,646 @@
+'use strict'
+
+const { types } = require('util')
+const { hasOwn, toUSVString } = require('./util')
+
+/** @type {import('../../types/webidl').Webidl} */
+const webidl = {}
+webidl.converters = {}
+webidl.util = {}
+webidl.errors = {}
+
+webidl.errors.exception = function (message) {
+ return new TypeError(`${message.header}: ${message.message}`)
+}
+
+webidl.errors.conversionFailed = function (context) {
+ const plural = context.types.length === 1 ? '' : ' one of'
+ const message =
+ `${context.argument} could not be converted to` +
+ `${plural}: ${context.types.join(', ')}.`
+
+ return webidl.errors.exception({
+ header: context.prefix,
+ message
+ })
+}
+
+webidl.errors.invalidArgument = function (context) {
+ return webidl.errors.exception({
+ header: context.prefix,
+ message: `"${context.value}" is an invalid ${context.type}.`
+ })
+}
+
+// https://webidl.spec.whatwg.org/#implements
+webidl.brandCheck = function (V, I, opts = undefined) {
+ if (opts?.strict !== false && !(V instanceof I)) {
+ throw new TypeError('Illegal invocation')
+ } else {
+ return V?.[Symbol.toStringTag] === I.prototype[Symbol.toStringTag]
+ }
+}
+
+webidl.argumentLengthCheck = function ({ length }, min, ctx) {
+ if (length < min) {
+ throw webidl.errors.exception({
+ message: `${min} argument${min !== 1 ? 's' : ''} required, ` +
+ `but${length ? ' only' : ''} ${length} found.`,
+ ...ctx
+ })
+ }
+}
+
+webidl.illegalConstructor = function () {
+ throw webidl.errors.exception({
+ header: 'TypeError',
+ message: 'Illegal constructor'
+ })
+}
+
+// https://tc39.es/ecma262/#sec-ecmascript-data-types-and-values
+webidl.util.Type = function (V) {
+ switch (typeof V) {
+ case 'undefined': return 'Undefined'
+ case 'boolean': return 'Boolean'
+ case 'string': return 'String'
+ case 'symbol': return 'Symbol'
+ case 'number': return 'Number'
+ case 'bigint': return 'BigInt'
+ case 'function':
+ case 'object': {
+ if (V === null) {
+ return 'Null'
+ }
+
+ return 'Object'
+ }
+ }
+}
+
+// https://webidl.spec.whatwg.org/#abstract-opdef-converttoint
+webidl.util.ConvertToInt = function (V, bitLength, signedness, opts = {}) {
+ let upperBound
+ let lowerBound
+
+ // 1. If bitLength is 64, then:
+ if (bitLength === 64) {
+ // 1. Let upperBound be 2^53 − 1.
+ upperBound = Math.pow(2, 53) - 1
+
+ // 2. If signedness is "unsigned", then let lowerBound be 0.
+ if (signedness === 'unsigned') {
+ lowerBound = 0
+ } else {
+ // 3. Otherwise let lowerBound be −2^53 + 1.
+ lowerBound = Math.pow(-2, 53) + 1
+ }
+ } else if (signedness === 'unsigned') {
+ // 2. Otherwise, if signedness is "unsigned", then:
+
+ // 1. Let lowerBound be 0.
+ lowerBound = 0
+
+ // 2. Let upperBound be 2^bitLength − 1.
+ upperBound = Math.pow(2, bitLength) - 1
+ } else {
+ // 3. Otherwise:
+
+ // 1. Let lowerBound be -2^bitLength − 1.
+ lowerBound = Math.pow(-2, bitLength) - 1
+
+ // 2. Let upperBound be 2^bitLength − 1 − 1.
+ upperBound = Math.pow(2, bitLength - 1) - 1
+ }
+
+ // 4. Let x be ? ToNumber(V).
+ let x = Number(V)
+
+ // 5. If x is −0, then set x to +0.
+ if (x === 0) {
+ x = 0
+ }
+
+ // 6. If the conversion is to an IDL type associated
+ // with the [EnforceRange] extended attribute, then:
+ if (opts.enforceRange === true) {
+ // 1. If x is NaN, +∞, or −∞, then throw a TypeError.
+ if (
+ Number.isNaN(x) ||
+ x === Number.POSITIVE_INFINITY ||
+ x === Number.NEGATIVE_INFINITY
+ ) {
+ throw webidl.errors.exception({
+ header: 'Integer conversion',
+ message: `Could not convert ${V} to an integer.`
+ })
+ }
+
+ // 2. Set x to IntegerPart(x).
+ x = webidl.util.IntegerPart(x)
+
+ // 3. If x < lowerBound or x > upperBound, then
+ // throw a TypeError.
+ if (x < lowerBound || x > upperBound) {
+ throw webidl.errors.exception({
+ header: 'Integer conversion',
+ message: `Value must be between ${lowerBound}-${upperBound}, got ${x}.`
+ })
+ }
+
+ // 4. Return x.
+ return x
+ }
+
+ // 7. If x is not NaN and the conversion is to an IDL
+ // type associated with the [Clamp] extended
+ // attribute, then:
+ if (!Number.isNaN(x) && opts.clamp === true) {
+ // 1. Set x to min(max(x, lowerBound), upperBound).
+ x = Math.min(Math.max(x, lowerBound), upperBound)
+
+ // 2. Round x to the nearest integer, choosing the
+ // even integer if it lies halfway between two,
+ // and choosing +0 rather than −0.
+ if (Math.floor(x) % 2 === 0) {
+ x = Math.floor(x)
+ } else {
+ x = Math.ceil(x)
+ }
+
+ // 3. Return x.
+ return x
+ }
+
+ // 8. If x is NaN, +0, +∞, or −∞, then return +0.
+ if (
+ Number.isNaN(x) ||
+ (x === 0 && Object.is(0, x)) ||
+ x === Number.POSITIVE_INFINITY ||
+ x === Number.NEGATIVE_INFINITY
+ ) {
+ return 0
+ }
+
+ // 9. Set x to IntegerPart(x).
+ x = webidl.util.IntegerPart(x)
+
+ // 10. Set x to x modulo 2^bitLength.
+ x = x % Math.pow(2, bitLength)
+
+ // 11. If signedness is "signed" and x ≥ 2^bitLength − 1,
+ // then return x − 2^bitLength.
+ if (signedness === 'signed' && x >= Math.pow(2, bitLength) - 1) {
+ return x - Math.pow(2, bitLength)
+ }
+
+ // 12. Otherwise, return x.
+ return x
+}
+
+// https://webidl.spec.whatwg.org/#abstract-opdef-integerpart
+webidl.util.IntegerPart = function (n) {
+ // 1. Let r be floor(abs(n)).
+ const r = Math.floor(Math.abs(n))
+
+ // 2. If n < 0, then return -1 × r.
+ if (n < 0) {
+ return -1 * r
+ }
+
+ // 3. Otherwise, return r.
+ return r
+}
+
+// https://webidl.spec.whatwg.org/#es-sequence
+webidl.sequenceConverter = function (converter) {
+ return (V) => {
+ // 1. If Type(V) is not Object, throw a TypeError.
+ if (webidl.util.Type(V) !== 'Object') {
+ throw webidl.errors.exception({
+ header: 'Sequence',
+ message: `Value of type ${webidl.util.Type(V)} is not an Object.`
+ })
+ }
+
+ // 2. Let method be ? GetMethod(V, @@iterator).
+ /** @type {Generator} */
+ const method = V?.[Symbol.iterator]?.()
+ const seq = []
+
+ // 3. If method is undefined, throw a TypeError.
+ if (
+ method === undefined ||
+ typeof method.next !== 'function'
+ ) {
+ throw webidl.errors.exception({
+ header: 'Sequence',
+ message: 'Object is not an iterator.'
+ })
+ }
+
+ // https://webidl.spec.whatwg.org/#create-sequence-from-iterable
+ while (true) {
+ const { done, value } = method.next()
+
+ if (done) {
+ break
+ }
+
+ seq.push(converter(value))
+ }
+
+ return seq
+ }
+}
+
+// https://webidl.spec.whatwg.org/#es-to-record
+webidl.recordConverter = function (keyConverter, valueConverter) {
+ return (O) => {
+ // 1. If Type(O) is not Object, throw a TypeError.
+ if (webidl.util.Type(O) !== 'Object') {
+ throw webidl.errors.exception({
+ header: 'Record',
+ message: `Value of type ${webidl.util.Type(O)} is not an Object.`
+ })
+ }
+
+ // 2. Let result be a new empty instance of record<K, V>.
+ const result = {}
+
+ if (!types.isProxy(O)) {
+ // Object.keys only returns enumerable properties
+ const keys = Object.keys(O)
+
+ for (const key of keys) {
+ // 1. Let typedKey be key converted to an IDL value of type K.
+ const typedKey = keyConverter(key)
+
+ // 2. Let value be ? Get(O, key).
+ // 3. Let typedValue be value converted to an IDL value of type V.
+ const typedValue = valueConverter(O[key])
+
+ // 4. Set result[typedKey] to typedValue.
+ result[typedKey] = typedValue
+ }
+
+ // 5. Return result.
+ return result
+ }
+
+ // 3. Let keys be ? O.[[OwnPropertyKeys]]().
+ const keys = Reflect.ownKeys(O)
+
+ // 4. For each key of keys.
+ for (const key of keys) {
+ // 1. Let desc be ? O.[[GetOwnProperty]](key).
+ const desc = Reflect.getOwnPropertyDescriptor(O, key)
+
+ // 2. If desc is not undefined and desc.[[Enumerable]] is true:
+ if (desc?.enumerable) {
+ // 1. Let typedKey be key converted to an IDL value of type K.
+ const typedKey = keyConverter(key)
+
+ // 2. Let value be ? Get(O, key).
+ // 3. Let typedValue be value converted to an IDL value of type V.
+ const typedValue = valueConverter(O[key])
+
+ // 4. Set result[typedKey] to typedValue.
+ result[typedKey] = typedValue
+ }
+ }
+
+ // 5. Return result.
+ return result
+ }
+}
+
+webidl.interfaceConverter = function (i) {
+ return (V, opts = {}) => {
+ if (opts.strict !== false && !(V instanceof i)) {
+ throw webidl.errors.exception({
+ header: i.name,
+ message: `Expected ${V} to be an instance of ${i.name}.`
+ })
+ }
+
+ return V
+ }
+}
+
+webidl.dictionaryConverter = function (converters) {
+ return (dictionary) => {
+ const type = webidl.util.Type(dictionary)
+ const dict = {}
+
+ if (type === 'Null' || type === 'Undefined') {
+ return dict
+ } else if (type !== 'Object') {
+ throw webidl.errors.exception({
+ header: 'Dictionary',
+ message: `Expected ${dictionary} to be one of: Null, Undefined, Object.`
+ })
+ }
+
+ for (const options of converters) {
+ const { key, defaultValue, required, converter } = options
+
+ if (required === true) {
+ if (!hasOwn(dictionary, key)) {
+ throw webidl.errors.exception({
+ header: 'Dictionary',
+ message: `Missing required key "${key}".`
+ })
+ }
+ }
+
+ let value = dictionary[key]
+ const hasDefault = hasOwn(options, 'defaultValue')
+
+ // Only use defaultValue if value is undefined and
+ // a defaultValue options was provided.
+ if (hasDefault && value !== null) {
+ value = value ?? defaultValue
+ }
+
+ // A key can be optional and have no default value.
+ // When this happens, do not perform a conversion,
+ // and do not assign the key a value.
+ if (required || hasDefault || value !== undefined) {
+ value = converter(value)
+
+ if (
+ options.allowedValues &&
+ !options.allowedValues.includes(value)
+ ) {
+ throw webidl.errors.exception({
+ header: 'Dictionary',
+ message: `${value} is not an accepted type. Expected one of ${options.allowedValues.join(', ')}.`
+ })
+ }
+
+ dict[key] = value
+ }
+ }
+
+ return dict
+ }
+}
+
+webidl.nullableConverter = function (converter) {
+ return (V) => {
+ if (V === null) {
+ return V
+ }
+
+ return converter(V)
+ }
+}
+
+// https://webidl.spec.whatwg.org/#es-DOMString
+webidl.converters.DOMString = function (V, opts = {}) {
+ // 1. If V is null and the conversion is to an IDL type
+ // associated with the [LegacyNullToEmptyString]
+ // extended attribute, then return the DOMString value
+ // that represents the empty string.
+ if (V === null && opts.legacyNullToEmptyString) {
+ return ''
+ }
+
+ // 2. Let x be ? ToString(V).
+ if (typeof V === 'symbol') {
+ throw new TypeError('Could not convert argument of type symbol to string.')
+ }
+
+ // 3. Return the IDL DOMString value that represents the
+ // same sequence of code units as the one the
+ // ECMAScript String value x represents.
+ return String(V)
+}
+
+// https://webidl.spec.whatwg.org/#es-ByteString
+webidl.converters.ByteString = function (V) {
+ // 1. Let x be ? ToString(V).
+ // Note: DOMString converter perform ? ToString(V)
+ const x = webidl.converters.DOMString(V)
+
+ // 2. If the value of any element of x is greater than
+ // 255, then throw a TypeError.
+ for (let index = 0; index < x.length; index++) {
+ if (x.charCodeAt(index) > 255) {
+ throw new TypeError(
+ 'Cannot convert argument to a ByteString because the character at ' +
+ `index ${index} has a value of ${x.charCodeAt(index)} which is greater than 255.`
+ )
+ }
+ }
+
+ // 3. Return an IDL ByteString value whose length is the
+ // length of x, and where the value of each element is
+ // the value of the corresponding element of x.
+ return x
+}
+
+// https://webidl.spec.whatwg.org/#es-USVString
+webidl.converters.USVString = toUSVString
+
+// https://webidl.spec.whatwg.org/#es-boolean
+webidl.converters.boolean = function (V) {
+ // 1. Let x be the result of computing ToBoolean(V).
+ const x = Boolean(V)
+
+ // 2. Return the IDL boolean value that is the one that represents
+ // the same truth value as the ECMAScript Boolean value x.
+ return x
+}
+
+// https://webidl.spec.whatwg.org/#es-any
+webidl.converters.any = function (V) {
+ return V
+}
+
+// https://webidl.spec.whatwg.org/#es-long-long
+webidl.converters['long long'] = function (V) {
+ // 1. Let x be ? ConvertToInt(V, 64, "signed").
+ const x = webidl.util.ConvertToInt(V, 64, 'signed')
+
+ // 2. Return the IDL long long value that represents
+ // the same numeric value as x.
+ return x
+}
+
+// https://webidl.spec.whatwg.org/#es-unsigned-long-long
+webidl.converters['unsigned long long'] = function (V) {
+ // 1. Let x be ? ConvertToInt(V, 64, "unsigned").
+ const x = webidl.util.ConvertToInt(V, 64, 'unsigned')
+
+ // 2. Return the IDL unsigned long long value that
+ // represents the same numeric value as x.
+ return x
+}
+
+// https://webidl.spec.whatwg.org/#es-unsigned-long
+webidl.converters['unsigned long'] = function (V) {
+ // 1. Let x be ? ConvertToInt(V, 32, "unsigned").
+ const x = webidl.util.ConvertToInt(V, 32, 'unsigned')
+
+ // 2. Return the IDL unsigned long value that
+ // represents the same numeric value as x.
+ return x
+}
+
+// https://webidl.spec.whatwg.org/#es-unsigned-short
+webidl.converters['unsigned short'] = function (V, opts) {
+ // 1. Let x be ? ConvertToInt(V, 16, "unsigned").
+ const x = webidl.util.ConvertToInt(V, 16, 'unsigned', opts)
+
+ // 2. Return the IDL unsigned short value that represents
+ // the same numeric value as x.
+ return x
+}
+
+// https://webidl.spec.whatwg.org/#idl-ArrayBuffer
+webidl.converters.ArrayBuffer = function (V, opts = {}) {
+ // 1. If Type(V) is not Object, or V does not have an
+ // [[ArrayBufferData]] internal slot, then throw a
+ // TypeError.
+ // see: https://tc39.es/ecma262/#sec-properties-of-the-arraybuffer-instances
+ // see: https://tc39.es/ecma262/#sec-properties-of-the-sharedarraybuffer-instances
+ if (
+ webidl.util.Type(V) !== 'Object' ||
+ !types.isAnyArrayBuffer(V)
+ ) {
+ throw webidl.errors.conversionFailed({
+ prefix: `${V}`,
+ argument: `${V}`,
+ types: ['ArrayBuffer']
+ })
+ }
+
+ // 2. If the conversion is not to an IDL type associated
+ // with the [AllowShared] extended attribute, and
+ // IsSharedArrayBuffer(V) is true, then throw a
+ // TypeError.
+ if (opts.allowShared === false && types.isSharedArrayBuffer(V)) {
+ throw webidl.errors.exception({
+ header: 'ArrayBuffer',
+ message: 'SharedArrayBuffer is not allowed.'
+ })
+ }
+
+ // 3. If the conversion is not to an IDL type associated
+ // with the [AllowResizable] extended attribute, and
+ // IsResizableArrayBuffer(V) is true, then throw a
+ // TypeError.
+ // Note: resizable ArrayBuffers are currently a proposal.
+
+ // 4. Return the IDL ArrayBuffer value that is a
+ // reference to the same object as V.
+ return V
+}
+
+webidl.converters.TypedArray = function (V, T, opts = {}) {
+ // 1. Let T be the IDL type V is being converted to.
+
+ // 2. If Type(V) is not Object, or V does not have a
+ // [[TypedArrayName]] internal slot with a value
+ // equal to T’s name, then throw a TypeError.
+ if (
+ webidl.util.Type(V) !== 'Object' ||
+ !types.isTypedArray(V) ||
+ V.constructor.name !== T.name
+ ) {
+ throw webidl.errors.conversionFailed({
+ prefix: `${T.name}`,
+ argument: `${V}`,
+ types: [T.name]
+ })
+ }
+
+ // 3. If the conversion is not to an IDL type associated
+ // with the [AllowShared] extended attribute, and
+ // IsSharedArrayBuffer(V.[[ViewedArrayBuffer]]) is
+ // true, then throw a TypeError.
+ if (opts.allowShared === false && types.isSharedArrayBuffer(V.buffer)) {
+ throw webidl.errors.exception({
+ header: 'ArrayBuffer',
+ message: 'SharedArrayBuffer is not allowed.'
+ })
+ }
+
+ // 4. If the conversion is not to an IDL type associated
+ // with the [AllowResizable] extended attribute, and
+ // IsResizableArrayBuffer(V.[[ViewedArrayBuffer]]) is
+ // true, then throw a TypeError.
+ // Note: resizable array buffers are currently a proposal
+
+ // 5. Return the IDL value of type T that is a reference
+ // to the same object as V.
+ return V
+}
+
+webidl.converters.DataView = function (V, opts = {}) {
+ // 1. If Type(V) is not Object, or V does not have a
+ // [[DataView]] internal slot, then throw a TypeError.
+ if (webidl.util.Type(V) !== 'Object' || !types.isDataView(V)) {
+ throw webidl.errors.exception({
+ header: 'DataView',
+ message: 'Object is not a DataView.'
+ })
+ }
+
+ // 2. If the conversion is not to an IDL type associated
+ // with the [AllowShared] extended attribute, and
+ // IsSharedArrayBuffer(V.[[ViewedArrayBuffer]]) is true,
+ // then throw a TypeError.
+ if (opts.allowShared === false && types.isSharedArrayBuffer(V.buffer)) {
+ throw webidl.errors.exception({
+ header: 'ArrayBuffer',
+ message: 'SharedArrayBuffer is not allowed.'
+ })
+ }
+
+ // 3. If the conversion is not to an IDL type associated
+ // with the [AllowResizable] extended attribute, and
+ // IsResizableArrayBuffer(V.[[ViewedArrayBuffer]]) is
+ // true, then throw a TypeError.
+ // Note: resizable ArrayBuffers are currently a proposal
+
+ // 4. Return the IDL DataView value that is a reference
+ // to the same object as V.
+ return V
+}
+
+// https://webidl.spec.whatwg.org/#BufferSource
+webidl.converters.BufferSource = function (V, opts = {}) {
+ if (types.isAnyArrayBuffer(V)) {
+ return webidl.converters.ArrayBuffer(V, opts)
+ }
+
+ if (types.isTypedArray(V)) {
+ return webidl.converters.TypedArray(V, V.constructor)
+ }
+
+ if (types.isDataView(V)) {
+ return webidl.converters.DataView(V, opts)
+ }
+
+ throw new TypeError(`Could not convert ${V} to a BufferSource.`)
+}
+
+webidl.converters['sequence<ByteString>'] = webidl.sequenceConverter(
+ webidl.converters.ByteString
+)
+
+webidl.converters['sequence<sequence<ByteString>>'] = webidl.sequenceConverter(
+ webidl.converters['sequence<ByteString>']
+)
+
+webidl.converters['record<ByteString, ByteString>'] = webidl.recordConverter(
+ webidl.converters.ByteString,
+ webidl.converters.ByteString
+)
+
+module.exports = {
+ webidl
+}
diff --git a/lib/fileapi/encoding.js b/lib/fileapi/encoding.js
new file mode 100644
index 0000000..1d1d2b6
--- /dev/null
+++ b/lib/fileapi/encoding.js
@@ -0,0 +1,290 @@
+'use strict'
+
+/**
+ * @see https://encoding.spec.whatwg.org/#concept-encoding-get
+ * @param {string|undefined} label
+ */
+function getEncoding (label) {
+ if (!label) {
+ return 'failure'
+ }
+
+ // 1. Remove any leading and trailing ASCII whitespace from label.
+ // 2. If label is an ASCII case-insensitive match for any of the
+ // labels listed in the table below, then return the
+ // corresponding encoding; otherwise return failure.
+ switch (label.trim().toLowerCase()) {
+ case 'unicode-1-1-utf-8':
+ case 'unicode11utf8':
+ case 'unicode20utf8':
+ case 'utf-8':
+ case 'utf8':
+ case 'x-unicode20utf8':
+ return 'UTF-8'
+ case '866':
+ case 'cp866':
+ case 'csibm866':
+ case 'ibm866':
+ return 'IBM866'
+ case 'csisolatin2':
+ case 'iso-8859-2':
+ case 'iso-ir-101':
+ case 'iso8859-2':
+ case 'iso88592':
+ case 'iso_8859-2':
+ case 'iso_8859-2:1987':
+ case 'l2':
+ case 'latin2':
+ return 'ISO-8859-2'
+ case 'csisolatin3':
+ case 'iso-8859-3':
+ case 'iso-ir-109':
+ case 'iso8859-3':
+ case 'iso88593':
+ case 'iso_8859-3':
+ case 'iso_8859-3:1988':
+ case 'l3':
+ case 'latin3':
+ return 'ISO-8859-3'
+ case 'csisolatin4':
+ case 'iso-8859-4':
+ case 'iso-ir-110':
+ case 'iso8859-4':
+ case 'iso88594':
+ case 'iso_8859-4':
+ case 'iso_8859-4:1988':
+ case 'l4':
+ case 'latin4':
+ return 'ISO-8859-4'
+ case 'csisolatincyrillic':
+ case 'cyrillic':
+ case 'iso-8859-5':
+ case 'iso-ir-144':
+ case 'iso8859-5':
+ case 'iso88595':
+ case 'iso_8859-5':
+ case 'iso_8859-5:1988':
+ return 'ISO-8859-5'
+ case 'arabic':
+ case 'asmo-708':
+ case 'csiso88596e':
+ case 'csiso88596i':
+ case 'csisolatinarabic':
+ case 'ecma-114':
+ case 'iso-8859-6':
+ case 'iso-8859-6-e':
+ case 'iso-8859-6-i':
+ case 'iso-ir-127':
+ case 'iso8859-6':
+ case 'iso88596':
+ case 'iso_8859-6':
+ case 'iso_8859-6:1987':
+ return 'ISO-8859-6'
+ case 'csisolatingreek':
+ case 'ecma-118':
+ case 'elot_928':
+ case 'greek':
+ case 'greek8':
+ case 'iso-8859-7':
+ case 'iso-ir-126':
+ case 'iso8859-7':
+ case 'iso88597':
+ case 'iso_8859-7':
+ case 'iso_8859-7:1987':
+ case 'sun_eu_greek':
+ return 'ISO-8859-7'
+ case 'csiso88598e':
+ case 'csisolatinhebrew':
+ case 'hebrew':
+ case 'iso-8859-8':
+ case 'iso-8859-8-e':
+ case 'iso-ir-138':
+ case 'iso8859-8':
+ case 'iso88598':
+ case 'iso_8859-8':
+ case 'iso_8859-8:1988':
+ case 'visual':
+ return 'ISO-8859-8'
+ case 'csiso88598i':
+ case 'iso-8859-8-i':
+ case 'logical':
+ return 'ISO-8859-8-I'
+ case 'csisolatin6':
+ case 'iso-8859-10':
+ case 'iso-ir-157':
+ case 'iso8859-10':
+ case 'iso885910':
+ case 'l6':
+ case 'latin6':
+ return 'ISO-8859-10'
+ case 'iso-8859-13':
+ case 'iso8859-13':
+ case 'iso885913':
+ return 'ISO-8859-13'
+ case 'iso-8859-14':
+ case 'iso8859-14':
+ case 'iso885914':
+ return 'ISO-8859-14'
+ case 'csisolatin9':
+ case 'iso-8859-15':
+ case 'iso8859-15':
+ case 'iso885915':
+ case 'iso_8859-15':
+ case 'l9':
+ return 'ISO-8859-15'
+ case 'iso-8859-16':
+ return 'ISO-8859-16'
+ case 'cskoi8r':
+ case 'koi':
+ case 'koi8':
+ case 'koi8-r':
+ case 'koi8_r':
+ return 'KOI8-R'
+ case 'koi8-ru':
+ case 'koi8-u':
+ return 'KOI8-U'
+ case 'csmacintosh':
+ case 'mac':
+ case 'macintosh':
+ case 'x-mac-roman':
+ return 'macintosh'
+ case 'iso-8859-11':
+ case 'iso8859-11':
+ case 'iso885911':
+ case 'tis-620':
+ case 'windows-874':
+ return 'windows-874'
+ case 'cp1250':
+ case 'windows-1250':
+ case 'x-cp1250':
+ return 'windows-1250'
+ case 'cp1251':
+ case 'windows-1251':
+ case 'x-cp1251':
+ return 'windows-1251'
+ case 'ansi_x3.4-1968':
+ case 'ascii':
+ case 'cp1252':
+ case 'cp819':
+ case 'csisolatin1':
+ case 'ibm819':
+ case 'iso-8859-1':
+ case 'iso-ir-100':
+ case 'iso8859-1':
+ case 'iso88591':
+ case 'iso_8859-1':
+ case 'iso_8859-1:1987':
+ case 'l1':
+ case 'latin1':
+ case 'us-ascii':
+ case 'windows-1252':
+ case 'x-cp1252':
+ return 'windows-1252'
+ case 'cp1253':
+ case 'windows-1253':
+ case 'x-cp1253':
+ return 'windows-1253'
+ case 'cp1254':
+ case 'csisolatin5':
+ case 'iso-8859-9':
+ case 'iso-ir-148':
+ case 'iso8859-9':
+ case 'iso88599':
+ case 'iso_8859-9':
+ case 'iso_8859-9:1989':
+ case 'l5':
+ case 'latin5':
+ case 'windows-1254':
+ case 'x-cp1254':
+ return 'windows-1254'
+ case 'cp1255':
+ case 'windows-1255':
+ case 'x-cp1255':
+ return 'windows-1255'
+ case 'cp1256':
+ case 'windows-1256':
+ case 'x-cp1256':
+ return 'windows-1256'
+ case 'cp1257':
+ case 'windows-1257':
+ case 'x-cp1257':
+ return 'windows-1257'
+ case 'cp1258':
+ case 'windows-1258':
+ case 'x-cp1258':
+ return 'windows-1258'
+ case 'x-mac-cyrillic':
+ case 'x-mac-ukrainian':
+ return 'x-mac-cyrillic'
+ case 'chinese':
+ case 'csgb2312':
+ case 'csiso58gb231280':
+ case 'gb2312':
+ case 'gb_2312':
+ case 'gb_2312-80':
+ case 'gbk':
+ case 'iso-ir-58':
+ case 'x-gbk':
+ return 'GBK'
+ case 'gb18030':
+ return 'gb18030'
+ case 'big5':
+ case 'big5-hkscs':
+ case 'cn-big5':
+ case 'csbig5':
+ case 'x-x-big5':
+ return 'Big5'
+ case 'cseucpkdfmtjapanese':
+ case 'euc-jp':
+ case 'x-euc-jp':
+ return 'EUC-JP'
+ case 'csiso2022jp':
+ case 'iso-2022-jp':
+ return 'ISO-2022-JP'
+ case 'csshiftjis':
+ case 'ms932':
+ case 'ms_kanji':
+ case 'shift-jis':
+ case 'shift_jis':
+ case 'sjis':
+ case 'windows-31j':
+ case 'x-sjis':
+ return 'Shift_JIS'
+ case 'cseuckr':
+ case 'csksc56011987':
+ case 'euc-kr':
+ case 'iso-ir-149':
+ case 'korean':
+ case 'ks_c_5601-1987':
+ case 'ks_c_5601-1989':
+ case 'ksc5601':
+ case 'ksc_5601':
+ case 'windows-949':
+ return 'EUC-KR'
+ case 'csiso2022kr':
+ case 'hz-gb-2312':
+ case 'iso-2022-cn':
+ case 'iso-2022-cn-ext':
+ case 'iso-2022-kr':
+ case 'replacement':
+ return 'replacement'
+ case 'unicodefffe':
+ case 'utf-16be':
+ return 'UTF-16BE'
+ case 'csunicode':
+ case 'iso-10646-ucs-2':
+ case 'ucs-2':
+ case 'unicode':
+ case 'unicodefeff':
+ case 'utf-16':
+ case 'utf-16le':
+ return 'UTF-16LE'
+ case 'x-user-defined':
+ return 'x-user-defined'
+ default: return 'failure'
+ }
+}
+
+module.exports = {
+ getEncoding
+}
diff --git a/lib/fileapi/filereader.js b/lib/fileapi/filereader.js
new file mode 100644
index 0000000..cd36a22
--- /dev/null
+++ b/lib/fileapi/filereader.js
@@ -0,0 +1,344 @@
+'use strict'
+
+const {
+ staticPropertyDescriptors,
+ readOperation,
+ fireAProgressEvent
+} = require('./util')
+const {
+ kState,
+ kError,
+ kResult,
+ kEvents,
+ kAborted
+} = require('./symbols')
+const { webidl } = require('../fetch/webidl')
+const { kEnumerableProperty } = require('../core/util')
+
+class FileReader extends EventTarget {
+ constructor () {
+ super()
+
+ this[kState] = 'empty'
+ this[kResult] = null
+ this[kError] = null
+ this[kEvents] = {
+ loadend: null,
+ error: null,
+ abort: null,
+ load: null,
+ progress: null,
+ loadstart: null
+ }
+ }
+
+ /**
+ * @see https://w3c.github.io/FileAPI/#dfn-readAsArrayBuffer
+ * @param {import('buffer').Blob} blob
+ */
+ readAsArrayBuffer (blob) {
+ webidl.brandCheck(this, FileReader)
+
+ webidl.argumentLengthCheck(arguments, 1, { header: 'FileReader.readAsArrayBuffer' })
+
+ blob = webidl.converters.Blob(blob, { strict: false })
+
+ // The readAsArrayBuffer(blob) method, when invoked,
+ // must initiate a read operation for blob with ArrayBuffer.
+ readOperation(this, blob, 'ArrayBuffer')
+ }
+
+ /**
+ * @see https://w3c.github.io/FileAPI/#readAsBinaryString
+ * @param {import('buffer').Blob} blob
+ */
+ readAsBinaryString (blob) {
+ webidl.brandCheck(this, FileReader)
+
+ webidl.argumentLengthCheck(arguments, 1, { header: 'FileReader.readAsBinaryString' })
+
+ blob = webidl.converters.Blob(blob, { strict: false })
+
+ // The readAsBinaryString(blob) method, when invoked,
+ // must initiate a read operation for blob with BinaryString.
+ readOperation(this, blob, 'BinaryString')
+ }
+
+ /**
+ * @see https://w3c.github.io/FileAPI/#readAsDataText
+ * @param {import('buffer').Blob} blob
+ * @param {string?} encoding
+ */
+ readAsText (blob, encoding = undefined) {
+ webidl.brandCheck(this, FileReader)
+
+ webidl.argumentLengthCheck(arguments, 1, { header: 'FileReader.readAsText' })
+
+ blob = webidl.converters.Blob(blob, { strict: false })
+
+ if (encoding !== undefined) {
+ encoding = webidl.converters.DOMString(encoding)
+ }
+
+ // The readAsText(blob, encoding) method, when invoked,
+ // must initiate a read operation for blob with Text and encoding.
+ readOperation(this, blob, 'Text', encoding)
+ }
+
+ /**
+ * @see https://w3c.github.io/FileAPI/#dfn-readAsDataURL
+ * @param {import('buffer').Blob} blob
+ */
+ readAsDataURL (blob) {
+ webidl.brandCheck(this, FileReader)
+
+ webidl.argumentLengthCheck(arguments, 1, { header: 'FileReader.readAsDataURL' })
+
+ blob = webidl.converters.Blob(blob, { strict: false })
+
+ // The readAsDataURL(blob) method, when invoked, must
+ // initiate a read operation for blob with DataURL.
+ readOperation(this, blob, 'DataURL')
+ }
+
+ /**
+ * @see https://w3c.github.io/FileAPI/#dfn-abort
+ */
+ abort () {
+ // 1. If this's state is "empty" or if this's state is
+ // "done" set this's result to null and terminate
+ // this algorithm.
+ if (this[kState] === 'empty' || this[kState] === 'done') {
+ this[kResult] = null
+ return
+ }
+
+ // 2. If this's state is "loading" set this's state to
+ // "done" and set this's result to null.
+ if (this[kState] === 'loading') {
+ this[kState] = 'done'
+ this[kResult] = null
+ }
+
+ // 3. If there are any tasks from this on the file reading
+ // task source in an affiliated task queue, then remove
+ // those tasks from that task queue.
+ this[kAborted] = true
+
+ // 4. Terminate the algorithm for the read method being processed.
+ // TODO
+
+ // 5. Fire a progress event called abort at this.
+ fireAProgressEvent('abort', this)
+
+ // 6. If this's state is not "loading", fire a progress
+ // event called loadend at this.
+ if (this[kState] !== 'loading') {
+ fireAProgressEvent('loadend', this)
+ }
+ }
+
+ /**
+ * @see https://w3c.github.io/FileAPI/#dom-filereader-readystate
+ */
+ get readyState () {
+ webidl.brandCheck(this, FileReader)
+
+ switch (this[kState]) {
+ case 'empty': return this.EMPTY
+ case 'loading': return this.LOADING
+ case 'done': return this.DONE
+ }
+ }
+
+ /**
+ * @see https://w3c.github.io/FileAPI/#dom-filereader-result
+ */
+ get result () {
+ webidl.brandCheck(this, FileReader)
+
+ // The result attribute’s getter, when invoked, must return
+ // this's result.
+ return this[kResult]
+ }
+
+ /**
+ * @see https://w3c.github.io/FileAPI/#dom-filereader-error
+ */
+ get error () {
+ webidl.brandCheck(this, FileReader)
+
+ // The error attribute’s getter, when invoked, must return
+ // this's error.
+ return this[kError]
+ }
+
+ get onloadend () {
+ webidl.brandCheck(this, FileReader)
+
+ return this[kEvents].loadend
+ }
+
+ set onloadend (fn) {
+ webidl.brandCheck(this, FileReader)
+
+ if (this[kEvents].loadend) {
+ this.removeEventListener('loadend', this[kEvents].loadend)
+ }
+
+ if (typeof fn === 'function') {
+ this[kEvents].loadend = fn
+ this.addEventListener('loadend', fn)
+ } else {
+ this[kEvents].loadend = null
+ }
+ }
+
+ get onerror () {
+ webidl.brandCheck(this, FileReader)
+
+ return this[kEvents].error
+ }
+
+ set onerror (fn) {
+ webidl.brandCheck(this, FileReader)
+
+ if (this[kEvents].error) {
+ this.removeEventListener('error', this[kEvents].error)
+ }
+
+ if (typeof fn === 'function') {
+ this[kEvents].error = fn
+ this.addEventListener('error', fn)
+ } else {
+ this[kEvents].error = null
+ }
+ }
+
+ get onloadstart () {
+ webidl.brandCheck(this, FileReader)
+
+ return this[kEvents].loadstart
+ }
+
+ set onloadstart (fn) {
+ webidl.brandCheck(this, FileReader)
+
+ if (this[kEvents].loadstart) {
+ this.removeEventListener('loadstart', this[kEvents].loadstart)
+ }
+
+ if (typeof fn === 'function') {
+ this[kEvents].loadstart = fn
+ this.addEventListener('loadstart', fn)
+ } else {
+ this[kEvents].loadstart = null
+ }
+ }
+
+ get onprogress () {
+ webidl.brandCheck(this, FileReader)
+
+ return this[kEvents].progress
+ }
+
+ set onprogress (fn) {
+ webidl.brandCheck(this, FileReader)
+
+ if (this[kEvents].progress) {
+ this.removeEventListener('progress', this[kEvents].progress)
+ }
+
+ if (typeof fn === 'function') {
+ this[kEvents].progress = fn
+ this.addEventListener('progress', fn)
+ } else {
+ this[kEvents].progress = null
+ }
+ }
+
+ get onload () {
+ webidl.brandCheck(this, FileReader)
+
+ return this[kEvents].load
+ }
+
+ set onload (fn) {
+ webidl.brandCheck(this, FileReader)
+
+ if (this[kEvents].load) {
+ this.removeEventListener('load', this[kEvents].load)
+ }
+
+ if (typeof fn === 'function') {
+ this[kEvents].load = fn
+ this.addEventListener('load', fn)
+ } else {
+ this[kEvents].load = null
+ }
+ }
+
+ get onabort () {
+ webidl.brandCheck(this, FileReader)
+
+ return this[kEvents].abort
+ }
+
+ set onabort (fn) {
+ webidl.brandCheck(this, FileReader)
+
+ if (this[kEvents].abort) {
+ this.removeEventListener('abort', this[kEvents].abort)
+ }
+
+ if (typeof fn === 'function') {
+ this[kEvents].abort = fn
+ this.addEventListener('abort', fn)
+ } else {
+ this[kEvents].abort = null
+ }
+ }
+}
+
+// https://w3c.github.io/FileAPI/#dom-filereader-empty
+FileReader.EMPTY = FileReader.prototype.EMPTY = 0
+// https://w3c.github.io/FileAPI/#dom-filereader-loading
+FileReader.LOADING = FileReader.prototype.LOADING = 1
+// https://w3c.github.io/FileAPI/#dom-filereader-done
+FileReader.DONE = FileReader.prototype.DONE = 2
+
+Object.defineProperties(FileReader.prototype, {
+ EMPTY: staticPropertyDescriptors,
+ LOADING: staticPropertyDescriptors,
+ DONE: staticPropertyDescriptors,
+ readAsArrayBuffer: kEnumerableProperty,
+ readAsBinaryString: kEnumerableProperty,
+ readAsText: kEnumerableProperty,
+ readAsDataURL: kEnumerableProperty,
+ abort: kEnumerableProperty,
+ readyState: kEnumerableProperty,
+ result: kEnumerableProperty,
+ error: kEnumerableProperty,
+ onloadstart: kEnumerableProperty,
+ onprogress: kEnumerableProperty,
+ onload: kEnumerableProperty,
+ onabort: kEnumerableProperty,
+ onerror: kEnumerableProperty,
+ onloadend: kEnumerableProperty,
+ [Symbol.toStringTag]: {
+ value: 'FileReader',
+ writable: false,
+ enumerable: false,
+ configurable: true
+ }
+})
+
+Object.defineProperties(FileReader, {
+ EMPTY: staticPropertyDescriptors,
+ LOADING: staticPropertyDescriptors,
+ DONE: staticPropertyDescriptors
+})
+
+module.exports = {
+ FileReader
+}
diff --git a/lib/fileapi/progressevent.js b/lib/fileapi/progressevent.js
new file mode 100644
index 0000000..778cf22
--- /dev/null
+++ b/lib/fileapi/progressevent.js
@@ -0,0 +1,78 @@
+'use strict'
+
+const { webidl } = require('../fetch/webidl')
+
+const kState = Symbol('ProgressEvent state')
+
+/**
+ * @see https://xhr.spec.whatwg.org/#progressevent
+ */
+class ProgressEvent extends Event {
+ constructor (type, eventInitDict = {}) {
+ type = webidl.converters.DOMString(type)
+ eventInitDict = webidl.converters.ProgressEventInit(eventInitDict ?? {})
+
+ super(type, eventInitDict)
+
+ this[kState] = {
+ lengthComputable: eventInitDict.lengthComputable,
+ loaded: eventInitDict.loaded,
+ total: eventInitDict.total
+ }
+ }
+
+ get lengthComputable () {
+ webidl.brandCheck(this, ProgressEvent)
+
+ return this[kState].lengthComputable
+ }
+
+ get loaded () {
+ webidl.brandCheck(this, ProgressEvent)
+
+ return this[kState].loaded
+ }
+
+ get total () {
+ webidl.brandCheck(this, ProgressEvent)
+
+ return this[kState].total
+ }
+}
+
+webidl.converters.ProgressEventInit = webidl.dictionaryConverter([
+ {
+ key: 'lengthComputable',
+ converter: webidl.converters.boolean,
+ defaultValue: false
+ },
+ {
+ key: 'loaded',
+ converter: webidl.converters['unsigned long long'],
+ defaultValue: 0
+ },
+ {
+ key: 'total',
+ converter: webidl.converters['unsigned long long'],
+ defaultValue: 0
+ },
+ {
+ key: 'bubbles',
+ converter: webidl.converters.boolean,
+ defaultValue: false
+ },
+ {
+ key: 'cancelable',
+ converter: webidl.converters.boolean,
+ defaultValue: false
+ },
+ {
+ key: 'composed',
+ converter: webidl.converters.boolean,
+ defaultValue: false
+ }
+])
+
+module.exports = {
+ ProgressEvent
+}
diff --git a/lib/fileapi/symbols.js b/lib/fileapi/symbols.js
new file mode 100644
index 0000000..dd11746
--- /dev/null
+++ b/lib/fileapi/symbols.js
@@ -0,0 +1,10 @@
+'use strict'
+
+module.exports = {
+ kState: Symbol('FileReader state'),
+ kResult: Symbol('FileReader result'),
+ kError: Symbol('FileReader error'),
+ kLastProgressEventFired: Symbol('FileReader last progress event fired timestamp'),
+ kEvents: Symbol('FileReader events'),
+ kAborted: Symbol('FileReader aborted')
+}
diff --git a/lib/fileapi/util.js b/lib/fileapi/util.js
new file mode 100644
index 0000000..1d10899
--- /dev/null
+++ b/lib/fileapi/util.js
@@ -0,0 +1,392 @@
+'use strict'
+
+const {
+ kState,
+ kError,
+ kResult,
+ kAborted,
+ kLastProgressEventFired
+} = require('./symbols')
+const { ProgressEvent } = require('./progressevent')
+const { getEncoding } = require('./encoding')
+const { DOMException } = require('../fetch/constants')
+const { serializeAMimeType, parseMIMEType } = require('../fetch/dataURL')
+const { types } = require('util')
+const { StringDecoder } = require('string_decoder')
+const { btoa } = require('buffer')
+
+/** @type {PropertyDescriptor} */
+const staticPropertyDescriptors = {
+ enumerable: true,
+ writable: false,
+ configurable: false
+}
+
+/**
+ * @see https://w3c.github.io/FileAPI/#readOperation
+ * @param {import('./filereader').FileReader} fr
+ * @param {import('buffer').Blob} blob
+ * @param {string} type
+ * @param {string?} encodingName
+ */
+function readOperation (fr, blob, type, encodingName) {
+ // 1. If fr’s state is "loading", throw an InvalidStateError
+ // DOMException.
+ if (fr[kState] === 'loading') {
+ throw new DOMException('Invalid state', 'InvalidStateError')
+ }
+
+ // 2. Set fr’s state to "loading".
+ fr[kState] = 'loading'
+
+ // 3. Set fr’s result to null.
+ fr[kResult] = null
+
+ // 4. Set fr’s error to null.
+ fr[kError] = null
+
+ // 5. Let stream be the result of calling get stream on blob.
+ /** @type {import('stream/web').ReadableStream} */
+ const stream = blob.stream()
+
+ // 6. Let reader be the result of getting a reader from stream.
+ const reader = stream.getReader()
+
+ // 7. Let bytes be an empty byte sequence.
+ /** @type {Uint8Array[]} */
+ const bytes = []
+
+ // 8. Let chunkPromise be the result of reading a chunk from
+ // stream with reader.
+ let chunkPromise = reader.read()
+
+ // 9. Let isFirstChunk be true.
+ let isFirstChunk = true
+
+ // 10. In parallel, while true:
+ // Note: "In parallel" just means non-blocking
+ // Note 2: readOperation itself cannot be async as double
+ // reading the body would then reject the promise, instead
+ // of throwing an error.
+ ;(async () => {
+ while (!fr[kAborted]) {
+ // 1. Wait for chunkPromise to be fulfilled or rejected.
+ try {
+ const { done, value } = await chunkPromise
+
+ // 2. If chunkPromise is fulfilled, and isFirstChunk is
+ // true, queue a task to fire a progress event called
+ // loadstart at fr.
+ if (isFirstChunk && !fr[kAborted]) {
+ queueMicrotask(() => {
+ fireAProgressEvent('loadstart', fr)
+ })
+ }
+
+ // 3. Set isFirstChunk to false.
+ isFirstChunk = false
+
+ // 4. If chunkPromise is fulfilled with an object whose
+ // done property is false and whose value property is
+ // a Uint8Array object, run these steps:
+ if (!done && types.isUint8Array(value)) {
+ // 1. Let bs be the byte sequence represented by the
+ // Uint8Array object.
+
+ // 2. Append bs to bytes.
+ bytes.push(value)
+
+ // 3. If roughly 50ms have passed since these steps
+ // were last invoked, queue a task to fire a
+ // progress event called progress at fr.
+ if (
+ (
+ fr[kLastProgressEventFired] === undefined ||
+ Date.now() - fr[kLastProgressEventFired] >= 50
+ ) &&
+ !fr[kAborted]
+ ) {
+ fr[kLastProgressEventFired] = Date.now()
+ queueMicrotask(() => {
+ fireAProgressEvent('progress', fr)
+ })
+ }
+
+ // 4. Set chunkPromise to the result of reading a
+ // chunk from stream with reader.
+ chunkPromise = reader.read()
+ } else if (done) {
+ // 5. Otherwise, if chunkPromise is fulfilled with an
+ // object whose done property is true, queue a task
+ // to run the following steps and abort this algorithm:
+ queueMicrotask(() => {
+ // 1. Set fr’s state to "done".
+ fr[kState] = 'done'
+
+ // 2. Let result be the result of package data given
+ // bytes, type, blob’s type, and encodingName.
+ try {
+ const result = packageData(bytes, type, blob.type, encodingName)
+
+ // 4. Else:
+
+ if (fr[kAborted]) {
+ return
+ }
+
+ // 1. Set fr’s result to result.
+ fr[kResult] = result
+
+ // 2. Fire a progress event called load at the fr.
+ fireAProgressEvent('load', fr)
+ } catch (error) {
+ // 3. If package data threw an exception error:
+
+ // 1. Set fr’s error to error.
+ fr[kError] = error
+
+ // 2. Fire a progress event called error at fr.
+ fireAProgressEvent('error', fr)
+ }
+
+ // 5. If fr’s state is not "loading", fire a progress
+ // event called loadend at the fr.
+ if (fr[kState] !== 'loading') {
+ fireAProgressEvent('loadend', fr)
+ }
+ })
+
+ break
+ }
+ } catch (error) {
+ if (fr[kAborted]) {
+ return
+ }
+
+ // 6. Otherwise, if chunkPromise is rejected with an
+ // error error, queue a task to run the following
+ // steps and abort this algorithm:
+ queueMicrotask(() => {
+ // 1. Set fr’s state to "done".
+ fr[kState] = 'done'
+
+ // 2. Set fr’s error to error.
+ fr[kError] = error
+
+ // 3. Fire a progress event called error at fr.
+ fireAProgressEvent('error', fr)
+
+ // 4. If fr’s state is not "loading", fire a progress
+ // event called loadend at fr.
+ if (fr[kState] !== 'loading') {
+ fireAProgressEvent('loadend', fr)
+ }
+ })
+
+ break
+ }
+ }
+ })()
+}
+
+/**
+ * @see https://w3c.github.io/FileAPI/#fire-a-progress-event
+ * @see https://dom.spec.whatwg.org/#concept-event-fire
+ * @param {string} e The name of the event
+ * @param {import('./filereader').FileReader} reader
+ */
+function fireAProgressEvent (e, reader) {
+ // The progress event e does not bubble. e.bubbles must be false
+ // The progress event e is NOT cancelable. e.cancelable must be false
+ const event = new ProgressEvent(e, {
+ bubbles: false,
+ cancelable: false
+ })
+
+ reader.dispatchEvent(event)
+}
+
+/**
+ * @see https://w3c.github.io/FileAPI/#blob-package-data
+ * @param {Uint8Array[]} bytes
+ * @param {string} type
+ * @param {string?} mimeType
+ * @param {string?} encodingName
+ */
+function packageData (bytes, type, mimeType, encodingName) {
+ // 1. A Blob has an associated package data algorithm, given
+ // bytes, a type, a optional mimeType, and a optional
+ // encodingName, which switches on type and runs the
+ // associated steps:
+
+ switch (type) {
+ case 'DataURL': {
+ // 1. Return bytes as a DataURL [RFC2397] subject to
+ // the considerations below:
+ // * Use mimeType as part of the Data URL if it is
+ // available in keeping with the Data URL
+ // specification [RFC2397].
+ // * If mimeType is not available return a Data URL
+ // without a media-type. [RFC2397].
+
+ // https://datatracker.ietf.org/doc/html/rfc2397#section-3
+ // dataurl := "data:" [ mediatype ] [ ";base64" ] "," data
+ // mediatype := [ type "/" subtype ] *( ";" parameter )
+ // data := *urlchar
+ // parameter := attribute "=" value
+ let dataURL = 'data:'
+
+ const parsed = parseMIMEType(mimeType || 'application/octet-stream')
+
+ if (parsed !== 'failure') {
+ dataURL += serializeAMimeType(parsed)
+ }
+
+ dataURL += ';base64,'
+
+ const decoder = new StringDecoder('latin1')
+
+ for (const chunk of bytes) {
+ dataURL += btoa(decoder.write(chunk))
+ }
+
+ dataURL += btoa(decoder.end())
+
+ return dataURL
+ }
+ case 'Text': {
+ // 1. Let encoding be failure
+ let encoding = 'failure'
+
+ // 2. If the encodingName is present, set encoding to the
+ // result of getting an encoding from encodingName.
+ if (encodingName) {
+ encoding = getEncoding(encodingName)
+ }
+
+ // 3. If encoding is failure, and mimeType is present:
+ if (encoding === 'failure' && mimeType) {
+ // 1. Let type be the result of parse a MIME type
+ // given mimeType.
+ const type = parseMIMEType(mimeType)
+
+ // 2. If type is not failure, set encoding to the result
+ // of getting an encoding from type’s parameters["charset"].
+ if (type !== 'failure') {
+ encoding = getEncoding(type.parameters.get('charset'))
+ }
+ }
+
+ // 4. If encoding is failure, then set encoding to UTF-8.
+ if (encoding === 'failure') {
+ encoding = 'UTF-8'
+ }
+
+ // 5. Decode bytes using fallback encoding encoding, and
+ // return the result.
+ return decode(bytes, encoding)
+ }
+ case 'ArrayBuffer': {
+ // Return a new ArrayBuffer whose contents are bytes.
+ const sequence = combineByteSequences(bytes)
+
+ return sequence.buffer
+ }
+ case 'BinaryString': {
+ // Return bytes as a binary string, in which every byte
+ // is represented by a code unit of equal value [0..255].
+ let binaryString = ''
+
+ const decoder = new StringDecoder('latin1')
+
+ for (const chunk of bytes) {
+ binaryString += decoder.write(chunk)
+ }
+
+ binaryString += decoder.end()
+
+ return binaryString
+ }
+ }
+}
+
+/**
+ * @see https://encoding.spec.whatwg.org/#decode
+ * @param {Uint8Array[]} ioQueue
+ * @param {string} encoding
+ */
+function decode (ioQueue, encoding) {
+ const bytes = combineByteSequences(ioQueue)
+
+ // 1. Let BOMEncoding be the result of BOM sniffing ioQueue.
+ const BOMEncoding = BOMSniffing(bytes)
+
+ let slice = 0
+
+ // 2. If BOMEncoding is non-null:
+ if (BOMEncoding !== null) {
+ // 1. Set encoding to BOMEncoding.
+ encoding = BOMEncoding
+
+ // 2. Read three bytes from ioQueue, if BOMEncoding is
+ // UTF-8; otherwise read two bytes.
+ // (Do nothing with those bytes.)
+ slice = BOMEncoding === 'UTF-8' ? 3 : 2
+ }
+
+ // 3. Process a queue with an instance of encoding’s
+ // decoder, ioQueue, output, and "replacement".
+
+ // 4. Return output.
+
+ const sliced = bytes.slice(slice)
+ return new TextDecoder(encoding).decode(sliced)
+}
+
+/**
+ * @see https://encoding.spec.whatwg.org/#bom-sniff
+ * @param {Uint8Array} ioQueue
+ */
+function BOMSniffing (ioQueue) {
+ // 1. Let BOM be the result of peeking 3 bytes from ioQueue,
+ // converted to a byte sequence.
+ const [a, b, c] = ioQueue
+
+ // 2. For each of the rows in the table below, starting with
+ // the first one and going down, if BOM starts with the
+ // bytes given in the first column, then return the
+ // encoding given in the cell in the second column of that
+ // row. Otherwise, return null.
+ if (a === 0xEF && b === 0xBB && c === 0xBF) {
+ return 'UTF-8'
+ } else if (a === 0xFE && b === 0xFF) {
+ return 'UTF-16BE'
+ } else if (a === 0xFF && b === 0xFE) {
+ return 'UTF-16LE'
+ }
+
+ return null
+}
+
+/**
+ * @param {Uint8Array[]} sequences
+ */
+function combineByteSequences (sequences) {
+ const size = sequences.reduce((a, b) => {
+ return a + b.byteLength
+ }, 0)
+
+ let offset = 0
+
+ return sequences.reduce((a, b) => {
+ a.set(b, offset)
+ offset += b.byteLength
+ return a
+ }, new Uint8Array(size))
+}
+
+module.exports = {
+ staticPropertyDescriptors,
+ readOperation,
+ fireAProgressEvent
+}
diff --git a/lib/global.js b/lib/global.js
new file mode 100644
index 0000000..18bfd73
--- /dev/null
+++ b/lib/global.js
@@ -0,0 +1,32 @@
+'use strict'
+
+// We include a version number for the Dispatcher API. In case of breaking changes,
+// this version number must be increased to avoid conflicts.
+const globalDispatcher = Symbol.for('undici.globalDispatcher.1')
+const { InvalidArgumentError } = require('./core/errors')
+const Agent = require('./agent')
+
+if (getGlobalDispatcher() === undefined) {
+ setGlobalDispatcher(new Agent())
+}
+
+function setGlobalDispatcher (agent) {
+ if (!agent || typeof agent.dispatch !== 'function') {
+ throw new InvalidArgumentError('Argument agent must implement Agent')
+ }
+ Object.defineProperty(globalThis, globalDispatcher, {
+ value: agent,
+ writable: true,
+ enumerable: false,
+ configurable: false
+ })
+}
+
+function getGlobalDispatcher () {
+ return globalThis[globalDispatcher]
+}
+
+module.exports = {
+ setGlobalDispatcher,
+ getGlobalDispatcher
+}
diff --git a/lib/handler/DecoratorHandler.js b/lib/handler/DecoratorHandler.js
new file mode 100644
index 0000000..9d70a76
--- /dev/null
+++ b/lib/handler/DecoratorHandler.js
@@ -0,0 +1,35 @@
+'use strict'
+
+module.exports = class DecoratorHandler {
+ constructor (handler) {
+ this.handler = handler
+ }
+
+ onConnect (...args) {
+ return this.handler.onConnect(...args)
+ }
+
+ onError (...args) {
+ return this.handler.onError(...args)
+ }
+
+ onUpgrade (...args) {
+ return this.handler.onUpgrade(...args)
+ }
+
+ onHeaders (...args) {
+ return this.handler.onHeaders(...args)
+ }
+
+ onData (...args) {
+ return this.handler.onData(...args)
+ }
+
+ onComplete (...args) {
+ return this.handler.onComplete(...args)
+ }
+
+ onBodySent (...args) {
+ return this.handler.onBodySent(...args)
+ }
+}
diff --git a/lib/handler/RedirectHandler.js b/lib/handler/RedirectHandler.js
new file mode 100644
index 0000000..baca27e
--- /dev/null
+++ b/lib/handler/RedirectHandler.js
@@ -0,0 +1,216 @@
+'use strict'
+
+const util = require('../core/util')
+const { kBodyUsed } = require('../core/symbols')
+const assert = require('assert')
+const { InvalidArgumentError } = require('../core/errors')
+const EE = require('events')
+
+const redirectableStatusCodes = [300, 301, 302, 303, 307, 308]
+
+const kBody = Symbol('body')
+
+class BodyAsyncIterable {
+ constructor (body) {
+ this[kBody] = body
+ this[kBodyUsed] = false
+ }
+
+ async * [Symbol.asyncIterator] () {
+ assert(!this[kBodyUsed], 'disturbed')
+ this[kBodyUsed] = true
+ yield * this[kBody]
+ }
+}
+
+class RedirectHandler {
+ constructor (dispatch, maxRedirections, opts, handler) {
+ if (maxRedirections != null && (!Number.isInteger(maxRedirections) || maxRedirections < 0)) {
+ throw new InvalidArgumentError('maxRedirections must be a positive number')
+ }
+
+ util.validateHandler(handler, opts.method, opts.upgrade)
+
+ this.dispatch = dispatch
+ this.location = null
+ this.abort = null
+ this.opts = { ...opts, maxRedirections: 0 } // opts must be a copy
+ this.maxRedirections = maxRedirections
+ this.handler = handler
+ this.history = []
+
+ if (util.isStream(this.opts.body)) {
+ // TODO (fix): Provide some way for the user to cache the file to e.g. /tmp
+ // so that it can be dispatched again?
+ // TODO (fix): Do we need 100-expect support to provide a way to do this properly?
+ if (util.bodyLength(this.opts.body) === 0) {
+ this.opts.body
+ .on('data', function () {
+ assert(false)
+ })
+ }
+
+ if (typeof this.opts.body.readableDidRead !== 'boolean') {
+ this.opts.body[kBodyUsed] = false
+ EE.prototype.on.call(this.opts.body, 'data', function () {
+ this[kBodyUsed] = true
+ })
+ }
+ } else if (this.opts.body && typeof this.opts.body.pipeTo === 'function') {
+ // TODO (fix): We can't access ReadableStream internal state
+ // to determine whether or not it has been disturbed. This is just
+ // a workaround.
+ this.opts.body = new BodyAsyncIterable(this.opts.body)
+ } else if (
+ this.opts.body &&
+ typeof this.opts.body !== 'string' &&
+ !ArrayBuffer.isView(this.opts.body) &&
+ util.isIterable(this.opts.body)
+ ) {
+ // TODO: Should we allow re-using iterable if !this.opts.idempotent
+ // or through some other flag?
+ this.opts.body = new BodyAsyncIterable(this.opts.body)
+ }
+ }
+
+ onConnect (abort) {
+ this.abort = abort
+ this.handler.onConnect(abort, { history: this.history })
+ }
+
+ onUpgrade (statusCode, headers, socket) {
+ this.handler.onUpgrade(statusCode, headers, socket)
+ }
+
+ onError (error) {
+ this.handler.onError(error)
+ }
+
+ onHeaders (statusCode, headers, resume, statusText) {
+ this.location = this.history.length >= this.maxRedirections || util.isDisturbed(this.opts.body)
+ ? null
+ : parseLocation(statusCode, headers)
+
+ if (this.opts.origin) {
+ this.history.push(new URL(this.opts.path, this.opts.origin))
+ }
+
+ if (!this.location) {
+ return this.handler.onHeaders(statusCode, headers, resume, statusText)
+ }
+
+ const { origin, pathname, search } = util.parseURL(new URL(this.location, this.opts.origin && new URL(this.opts.path, this.opts.origin)))
+ const path = search ? `${pathname}${search}` : pathname
+
+ // Remove headers referring to the original URL.
+ // By default it is Host only, unless it's a 303 (see below), which removes also all Content-* headers.
+ // https://tools.ietf.org/html/rfc7231#section-6.4
+ this.opts.headers = cleanRequestHeaders(this.opts.headers, statusCode === 303, this.opts.origin !== origin)
+ this.opts.path = path
+ this.opts.origin = origin
+ this.opts.maxRedirections = 0
+ this.opts.query = null
+
+ // https://tools.ietf.org/html/rfc7231#section-6.4.4
+ // In case of HTTP 303, always replace method to be either HEAD or GET
+ if (statusCode === 303 && this.opts.method !== 'HEAD') {
+ this.opts.method = 'GET'
+ this.opts.body = null
+ }
+ }
+
+ onData (chunk) {
+ if (this.location) {
+ /*
+ https://tools.ietf.org/html/rfc7231#section-6.4
+
+ TLDR: undici always ignores 3xx response bodies.
+
+ Redirection is used to serve the requested resource from another URL, so it is assumes that
+ no body is generated (and thus can be ignored). Even though generating a body is not prohibited.
+
+ For status 301, 302, 303, 307 and 308 (the latter from RFC 7238), the specs mention that the body usually
+ (which means it's optional and not mandated) contain just an hyperlink to the value of
+ the Location response header, so the body can be ignored safely.
+
+ For status 300, which is "Multiple Choices", the spec mentions both generating a Location
+ response header AND a response body with the other possible location to follow.
+ Since the spec explicitily chooses not to specify a format for such body and leave it to
+ servers and browsers implementors, we ignore the body as there is no specified way to eventually parse it.
+ */
+ } else {
+ return this.handler.onData(chunk)
+ }
+ }
+
+ onComplete (trailers) {
+ if (this.location) {
+ /*
+ https://tools.ietf.org/html/rfc7231#section-6.4
+
+ TLDR: undici always ignores 3xx response trailers as they are not expected in case of redirections
+ and neither are useful if present.
+
+ See comment on onData method above for more detailed informations.
+ */
+
+ this.location = null
+ this.abort = null
+
+ this.dispatch(this.opts, this)
+ } else {
+ this.handler.onComplete(trailers)
+ }
+ }
+
+ onBodySent (chunk) {
+ if (this.handler.onBodySent) {
+ this.handler.onBodySent(chunk)
+ }
+ }
+}
+
+function parseLocation (statusCode, headers) {
+ if (redirectableStatusCodes.indexOf(statusCode) === -1) {
+ return null
+ }
+
+ for (let i = 0; i < headers.length; i += 2) {
+ if (headers[i].toString().toLowerCase() === 'location') {
+ return headers[i + 1]
+ }
+ }
+}
+
+// https://tools.ietf.org/html/rfc7231#section-6.4.4
+function shouldRemoveHeader (header, removeContent, unknownOrigin) {
+ return (
+ (header.length === 4 && header.toString().toLowerCase() === 'host') ||
+ (removeContent && header.toString().toLowerCase().indexOf('content-') === 0) ||
+ (unknownOrigin && header.length === 13 && header.toString().toLowerCase() === 'authorization') ||
+ (unknownOrigin && header.length === 6 && header.toString().toLowerCase() === 'cookie')
+ )
+}
+
+// https://tools.ietf.org/html/rfc7231#section-6.4
+function cleanRequestHeaders (headers, removeContent, unknownOrigin) {
+ const ret = []
+ if (Array.isArray(headers)) {
+ for (let i = 0; i < headers.length; i += 2) {
+ if (!shouldRemoveHeader(headers[i], removeContent, unknownOrigin)) {
+ ret.push(headers[i], headers[i + 1])
+ }
+ }
+ } else if (headers && typeof headers === 'object') {
+ for (const key of Object.keys(headers)) {
+ if (!shouldRemoveHeader(key, removeContent, unknownOrigin)) {
+ ret.push(key, headers[key])
+ }
+ }
+ } else {
+ assert(headers == null, 'headers must be an object or an array')
+ }
+ return ret
+}
+
+module.exports = RedirectHandler
diff --git a/lib/handler/RetryHandler.js b/lib/handler/RetryHandler.js
new file mode 100644
index 0000000..3710447
--- /dev/null
+++ b/lib/handler/RetryHandler.js
@@ -0,0 +1,336 @@
+const assert = require('assert')
+
+const { kRetryHandlerDefaultRetry } = require('../core/symbols')
+const { RequestRetryError } = require('../core/errors')
+const { isDisturbed, parseHeaders, parseRangeHeader } = require('../core/util')
+
+function calculateRetryAfterHeader (retryAfter) {
+ const current = Date.now()
+ const diff = new Date(retryAfter).getTime() - current
+
+ return diff
+}
+
+class RetryHandler {
+ constructor (opts, handlers) {
+ const { retryOptions, ...dispatchOpts } = opts
+ const {
+ // Retry scoped
+ retry: retryFn,
+ maxRetries,
+ maxTimeout,
+ minTimeout,
+ timeoutFactor,
+ // Response scoped
+ methods,
+ errorCodes,
+ retryAfter,
+ statusCodes
+ } = retryOptions ?? {}
+
+ this.dispatch = handlers.dispatch
+ this.handler = handlers.handler
+ this.opts = dispatchOpts
+ this.abort = null
+ this.aborted = false
+ this.retryOpts = {
+ retry: retryFn ?? RetryHandler[kRetryHandlerDefaultRetry],
+ retryAfter: retryAfter ?? true,
+ maxTimeout: maxTimeout ?? 30 * 1000, // 30s,
+ timeout: minTimeout ?? 500, // .5s
+ timeoutFactor: timeoutFactor ?? 2,
+ maxRetries: maxRetries ?? 5,
+ // What errors we should retry
+ methods: methods ?? ['GET', 'HEAD', 'OPTIONS', 'PUT', 'DELETE', 'TRACE'],
+ // Indicates which errors to retry
+ statusCodes: statusCodes ?? [500, 502, 503, 504, 429],
+ // List of errors to retry
+ errorCodes: errorCodes ?? [
+ 'ECONNRESET',
+ 'ECONNREFUSED',
+ 'ENOTFOUND',
+ 'ENETDOWN',
+ 'ENETUNREACH',
+ 'EHOSTDOWN',
+ 'EHOSTUNREACH',
+ 'EPIPE'
+ ]
+ }
+
+ this.retryCount = 0
+ this.start = 0
+ this.end = null
+ this.etag = null
+ this.resume = null
+
+ // Handle possible onConnect duplication
+ this.handler.onConnect(reason => {
+ this.aborted = true
+ if (this.abort) {
+ this.abort(reason)
+ } else {
+ this.reason = reason
+ }
+ })
+ }
+
+ onRequestSent () {
+ if (this.handler.onRequestSent) {
+ this.handler.onRequestSent()
+ }
+ }
+
+ onUpgrade (statusCode, headers, socket) {
+ if (this.handler.onUpgrade) {
+ this.handler.onUpgrade(statusCode, headers, socket)
+ }
+ }
+
+ onConnect (abort) {
+ if (this.aborted) {
+ abort(this.reason)
+ } else {
+ this.abort = abort
+ }
+ }
+
+ onBodySent (chunk) {
+ if (this.handler.onBodySent) return this.handler.onBodySent(chunk)
+ }
+
+ static [kRetryHandlerDefaultRetry] (err, { state, opts }, cb) {
+ const { statusCode, code, headers } = err
+ const { method, retryOptions } = opts
+ const {
+ maxRetries,
+ timeout,
+ maxTimeout,
+ timeoutFactor,
+ statusCodes,
+ errorCodes,
+ methods
+ } = retryOptions
+ let { counter, currentTimeout } = state
+
+ currentTimeout =
+ currentTimeout != null && currentTimeout > 0 ? currentTimeout : timeout
+
+ // Any code that is not a Undici's originated and allowed to retry
+ if (
+ code &&
+ code !== 'UND_ERR_REQ_RETRY' &&
+ code !== 'UND_ERR_SOCKET' &&
+ !errorCodes.includes(code)
+ ) {
+ cb(err)
+ return
+ }
+
+ // If a set of method are provided and the current method is not in the list
+ if (Array.isArray(methods) && !methods.includes(method)) {
+ cb(err)
+ return
+ }
+
+ // If a set of status code are provided and the current status code is not in the list
+ if (
+ statusCode != null &&
+ Array.isArray(statusCodes) &&
+ !statusCodes.includes(statusCode)
+ ) {
+ cb(err)
+ return
+ }
+
+ // If we reached the max number of retries
+ if (counter > maxRetries) {
+ cb(err)
+ return
+ }
+
+ let retryAfterHeader = headers != null && headers['retry-after']
+ if (retryAfterHeader) {
+ retryAfterHeader = Number(retryAfterHeader)
+ retryAfterHeader = isNaN(retryAfterHeader)
+ ? calculateRetryAfterHeader(retryAfterHeader)
+ : retryAfterHeader * 1e3 // Retry-After is in seconds
+ }
+
+ const retryTimeout =
+ retryAfterHeader > 0
+ ? Math.min(retryAfterHeader, maxTimeout)
+ : Math.min(currentTimeout * timeoutFactor ** counter, maxTimeout)
+
+ state.currentTimeout = retryTimeout
+
+ setTimeout(() => cb(null), retryTimeout)
+ }
+
+ onHeaders (statusCode, rawHeaders, resume, statusMessage) {
+ const headers = parseHeaders(rawHeaders)
+
+ this.retryCount += 1
+
+ if (statusCode >= 300) {
+ this.abort(
+ new RequestRetryError('Request failed', statusCode, {
+ headers,
+ count: this.retryCount
+ })
+ )
+ return false
+ }
+
+ // Checkpoint for resume from where we left it
+ if (this.resume != null) {
+ this.resume = null
+
+ if (statusCode !== 206) {
+ return true
+ }
+
+ const contentRange = parseRangeHeader(headers['content-range'])
+ // If no content range
+ if (!contentRange) {
+ this.abort(
+ new RequestRetryError('Content-Range mismatch', statusCode, {
+ headers,
+ count: this.retryCount
+ })
+ )
+ return false
+ }
+
+ // Let's start with a weak etag check
+ if (this.etag != null && this.etag !== headers.etag) {
+ this.abort(
+ new RequestRetryError('ETag mismatch', statusCode, {
+ headers,
+ count: this.retryCount
+ })
+ )
+ return false
+ }
+
+ const { start, size, end = size } = contentRange
+
+ assert(this.start === start, 'content-range mismatch')
+ assert(this.end == null || this.end === end, 'content-range mismatch')
+
+ this.resume = resume
+ return true
+ }
+
+ if (this.end == null) {
+ if (statusCode === 206) {
+ // First time we receive 206
+ const range = parseRangeHeader(headers['content-range'])
+
+ if (range == null) {
+ return this.handler.onHeaders(
+ statusCode,
+ rawHeaders,
+ resume,
+ statusMessage
+ )
+ }
+
+ const { start, size, end = size } = range
+
+ assert(
+ start != null && Number.isFinite(start) && this.start !== start,
+ 'content-range mismatch'
+ )
+ assert(Number.isFinite(start))
+ assert(
+ end != null && Number.isFinite(end) && this.end !== end,
+ 'invalid content-length'
+ )
+
+ this.start = start
+ this.end = end
+ }
+
+ // We make our best to checkpoint the body for further range headers
+ if (this.end == null) {
+ const contentLength = headers['content-length']
+ this.end = contentLength != null ? Number(contentLength) : null
+ }
+
+ assert(Number.isFinite(this.start))
+ assert(
+ this.end == null || Number.isFinite(this.end),
+ 'invalid content-length'
+ )
+
+ this.resume = resume
+ this.etag = headers.etag != null ? headers.etag : null
+
+ return this.handler.onHeaders(
+ statusCode,
+ rawHeaders,
+ resume,
+ statusMessage
+ )
+ }
+
+ const err = new RequestRetryError('Request failed', statusCode, {
+ headers,
+ count: this.retryCount
+ })
+
+ this.abort(err)
+
+ return false
+ }
+
+ onData (chunk) {
+ this.start += chunk.length
+
+ return this.handler.onData(chunk)
+ }
+
+ onComplete (rawTrailers) {
+ this.retryCount = 0
+ return this.handler.onComplete(rawTrailers)
+ }
+
+ onError (err) {
+ if (this.aborted || isDisturbed(this.opts.body)) {
+ return this.handler.onError(err)
+ }
+
+ this.retryOpts.retry(
+ err,
+ {
+ state: { counter: this.retryCount++, currentTimeout: this.retryAfter },
+ opts: { retryOptions: this.retryOpts, ...this.opts }
+ },
+ onRetry.bind(this)
+ )
+
+ function onRetry (err) {
+ if (err != null || this.aborted || isDisturbed(this.opts.body)) {
+ return this.handler.onError(err)
+ }
+
+ if (this.start !== 0) {
+ this.opts = {
+ ...this.opts,
+ headers: {
+ ...this.opts.headers,
+ range: `bytes=${this.start}-${this.end ?? ''}`
+ }
+ }
+ }
+
+ try {
+ this.dispatch(this.opts, this)
+ } catch (err) {
+ this.handler.onError(err)
+ }
+ }
+ }
+}
+
+module.exports = RetryHandler
diff --git a/lib/interceptor/redirectInterceptor.js b/lib/interceptor/redirectInterceptor.js
new file mode 100644
index 0000000..7cc035e
--- /dev/null
+++ b/lib/interceptor/redirectInterceptor.js
@@ -0,0 +1,21 @@
+'use strict'
+
+const RedirectHandler = require('../handler/RedirectHandler')
+
+function createRedirectInterceptor ({ maxRedirections: defaultMaxRedirections }) {
+ return (dispatch) => {
+ return function Intercept (opts, handler) {
+ const { maxRedirections = defaultMaxRedirections } = opts
+
+ if (!maxRedirections) {
+ return dispatch(opts, handler)
+ }
+
+ const redirectHandler = new RedirectHandler(dispatch, maxRedirections, opts, handler)
+ opts = { ...opts, maxRedirections: 0 } // Stop sub dispatcher from also redirecting.
+ return dispatch(opts, redirectHandler)
+ }
+ }
+}
+
+module.exports = createRedirectInterceptor
diff --git a/lib/llhttp/constants.d.ts b/lib/llhttp/constants.d.ts
new file mode 100644
index 0000000..b75ab1b
--- /dev/null
+++ b/lib/llhttp/constants.d.ts
@@ -0,0 +1,199 @@
+import { IEnumMap } from './utils';
+export declare type HTTPMode = 'loose' | 'strict';
+export declare enum ERROR {
+ OK = 0,
+ INTERNAL = 1,
+ STRICT = 2,
+ LF_EXPECTED = 3,
+ UNEXPECTED_CONTENT_LENGTH = 4,
+ CLOSED_CONNECTION = 5,
+ INVALID_METHOD = 6,
+ INVALID_URL = 7,
+ INVALID_CONSTANT = 8,
+ INVALID_VERSION = 9,
+ INVALID_HEADER_TOKEN = 10,
+ INVALID_CONTENT_LENGTH = 11,
+ INVALID_CHUNK_SIZE = 12,
+ INVALID_STATUS = 13,
+ INVALID_EOF_STATE = 14,
+ INVALID_TRANSFER_ENCODING = 15,
+ CB_MESSAGE_BEGIN = 16,
+ CB_HEADERS_COMPLETE = 17,
+ CB_MESSAGE_COMPLETE = 18,
+ CB_CHUNK_HEADER = 19,
+ CB_CHUNK_COMPLETE = 20,
+ PAUSED = 21,
+ PAUSED_UPGRADE = 22,
+ PAUSED_H2_UPGRADE = 23,
+ USER = 24
+}
+export declare enum TYPE {
+ BOTH = 0,
+ REQUEST = 1,
+ RESPONSE = 2
+}
+export declare enum FLAGS {
+ CONNECTION_KEEP_ALIVE = 1,
+ CONNECTION_CLOSE = 2,
+ CONNECTION_UPGRADE = 4,
+ CHUNKED = 8,
+ UPGRADE = 16,
+ CONTENT_LENGTH = 32,
+ SKIPBODY = 64,
+ TRAILING = 128,
+ TRANSFER_ENCODING = 512
+}
+export declare enum LENIENT_FLAGS {
+ HEADERS = 1,
+ CHUNKED_LENGTH = 2,
+ KEEP_ALIVE = 4
+}
+export declare enum METHODS {
+ DELETE = 0,
+ GET = 1,
+ HEAD = 2,
+ POST = 3,
+ PUT = 4,
+ CONNECT = 5,
+ OPTIONS = 6,
+ TRACE = 7,
+ COPY = 8,
+ LOCK = 9,
+ MKCOL = 10,
+ MOVE = 11,
+ PROPFIND = 12,
+ PROPPATCH = 13,
+ SEARCH = 14,
+ UNLOCK = 15,
+ BIND = 16,
+ REBIND = 17,
+ UNBIND = 18,
+ ACL = 19,
+ REPORT = 20,
+ MKACTIVITY = 21,
+ CHECKOUT = 22,
+ MERGE = 23,
+ 'M-SEARCH' = 24,
+ NOTIFY = 25,
+ SUBSCRIBE = 26,
+ UNSUBSCRIBE = 27,
+ PATCH = 28,
+ PURGE = 29,
+ MKCALENDAR = 30,
+ LINK = 31,
+ UNLINK = 32,
+ SOURCE = 33,
+ PRI = 34,
+ DESCRIBE = 35,
+ ANNOUNCE = 36,
+ SETUP = 37,
+ PLAY = 38,
+ PAUSE = 39,
+ TEARDOWN = 40,
+ GET_PARAMETER = 41,
+ SET_PARAMETER = 42,
+ REDIRECT = 43,
+ RECORD = 44,
+ FLUSH = 45
+}
+export declare const METHODS_HTTP: METHODS[];
+export declare const METHODS_ICE: METHODS[];
+export declare const METHODS_RTSP: METHODS[];
+export declare const METHOD_MAP: IEnumMap;
+export declare const H_METHOD_MAP: IEnumMap;
+export declare enum FINISH {
+ SAFE = 0,
+ SAFE_WITH_CB = 1,
+ UNSAFE = 2
+}
+export declare type CharList = Array<string | number>;
+export declare const ALPHA: CharList;
+export declare const NUM_MAP: {
+ 0: number;
+ 1: number;
+ 2: number;
+ 3: number;
+ 4: number;
+ 5: number;
+ 6: number;
+ 7: number;
+ 8: number;
+ 9: number;
+};
+export declare const HEX_MAP: {
+ 0: number;
+ 1: number;
+ 2: number;
+ 3: number;
+ 4: number;
+ 5: number;
+ 6: number;
+ 7: number;
+ 8: number;
+ 9: number;
+ A: number;
+ B: number;
+ C: number;
+ D: number;
+ E: number;
+ F: number;
+ a: number;
+ b: number;
+ c: number;
+ d: number;
+ e: number;
+ f: number;
+};
+export declare const NUM: CharList;
+export declare const ALPHANUM: CharList;
+export declare const MARK: CharList;
+export declare const USERINFO_CHARS: CharList;
+export declare const STRICT_URL_CHAR: CharList;
+export declare const URL_CHAR: CharList;
+export declare const HEX: CharList;
+export declare const STRICT_TOKEN: CharList;
+export declare const TOKEN: CharList;
+export declare const HEADER_CHARS: CharList;
+export declare const CONNECTION_TOKEN_CHARS: CharList;
+export declare const MAJOR: {
+ 0: number;
+ 1: number;
+ 2: number;
+ 3: number;
+ 4: number;
+ 5: number;
+ 6: number;
+ 7: number;
+ 8: number;
+ 9: number;
+};
+export declare const MINOR: {
+ 0: number;
+ 1: number;
+ 2: number;
+ 3: number;
+ 4: number;
+ 5: number;
+ 6: number;
+ 7: number;
+ 8: number;
+ 9: number;
+};
+export declare enum HEADER_STATE {
+ GENERAL = 0,
+ CONNECTION = 1,
+ CONTENT_LENGTH = 2,
+ TRANSFER_ENCODING = 3,
+ UPGRADE = 4,
+ CONNECTION_KEEP_ALIVE = 5,
+ CONNECTION_CLOSE = 6,
+ CONNECTION_UPGRADE = 7,
+ TRANSFER_ENCODING_CHUNKED = 8
+}
+export declare const SPECIAL_HEADERS: {
+ connection: HEADER_STATE;
+ 'content-length': HEADER_STATE;
+ 'proxy-connection': HEADER_STATE;
+ 'transfer-encoding': HEADER_STATE;
+ upgrade: HEADER_STATE;
+};
diff --git a/lib/llhttp/constants.js b/lib/llhttp/constants.js
new file mode 100644
index 0000000..fb0b5a2
--- /dev/null
+++ b/lib/llhttp/constants.js
@@ -0,0 +1,278 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.SPECIAL_HEADERS = exports.HEADER_STATE = exports.MINOR = exports.MAJOR = exports.CONNECTION_TOKEN_CHARS = exports.HEADER_CHARS = exports.TOKEN = exports.STRICT_TOKEN = exports.HEX = exports.URL_CHAR = exports.STRICT_URL_CHAR = exports.USERINFO_CHARS = exports.MARK = exports.ALPHANUM = exports.NUM = exports.HEX_MAP = exports.NUM_MAP = exports.ALPHA = exports.FINISH = exports.H_METHOD_MAP = exports.METHOD_MAP = exports.METHODS_RTSP = exports.METHODS_ICE = exports.METHODS_HTTP = exports.METHODS = exports.LENIENT_FLAGS = exports.FLAGS = exports.TYPE = exports.ERROR = void 0;
+const utils_1 = require("./utils");
+// C headers
+var ERROR;
+(function (ERROR) {
+ ERROR[ERROR["OK"] = 0] = "OK";
+ ERROR[ERROR["INTERNAL"] = 1] = "INTERNAL";
+ ERROR[ERROR["STRICT"] = 2] = "STRICT";
+ ERROR[ERROR["LF_EXPECTED"] = 3] = "LF_EXPECTED";
+ ERROR[ERROR["UNEXPECTED_CONTENT_LENGTH"] = 4] = "UNEXPECTED_CONTENT_LENGTH";
+ ERROR[ERROR["CLOSED_CONNECTION"] = 5] = "CLOSED_CONNECTION";
+ ERROR[ERROR["INVALID_METHOD"] = 6] = "INVALID_METHOD";
+ ERROR[ERROR["INVALID_URL"] = 7] = "INVALID_URL";
+ ERROR[ERROR["INVALID_CONSTANT"] = 8] = "INVALID_CONSTANT";
+ ERROR[ERROR["INVALID_VERSION"] = 9] = "INVALID_VERSION";
+ ERROR[ERROR["INVALID_HEADER_TOKEN"] = 10] = "INVALID_HEADER_TOKEN";
+ ERROR[ERROR["INVALID_CONTENT_LENGTH"] = 11] = "INVALID_CONTENT_LENGTH";
+ ERROR[ERROR["INVALID_CHUNK_SIZE"] = 12] = "INVALID_CHUNK_SIZE";
+ ERROR[ERROR["INVALID_STATUS"] = 13] = "INVALID_STATUS";
+ ERROR[ERROR["INVALID_EOF_STATE"] = 14] = "INVALID_EOF_STATE";
+ ERROR[ERROR["INVALID_TRANSFER_ENCODING"] = 15] = "INVALID_TRANSFER_ENCODING";
+ ERROR[ERROR["CB_MESSAGE_BEGIN"] = 16] = "CB_MESSAGE_BEGIN";
+ ERROR[ERROR["CB_HEADERS_COMPLETE"] = 17] = "CB_HEADERS_COMPLETE";
+ ERROR[ERROR["CB_MESSAGE_COMPLETE"] = 18] = "CB_MESSAGE_COMPLETE";
+ ERROR[ERROR["CB_CHUNK_HEADER"] = 19] = "CB_CHUNK_HEADER";
+ ERROR[ERROR["CB_CHUNK_COMPLETE"] = 20] = "CB_CHUNK_COMPLETE";
+ ERROR[ERROR["PAUSED"] = 21] = "PAUSED";
+ ERROR[ERROR["PAUSED_UPGRADE"] = 22] = "PAUSED_UPGRADE";
+ ERROR[ERROR["PAUSED_H2_UPGRADE"] = 23] = "PAUSED_H2_UPGRADE";
+ ERROR[ERROR["USER"] = 24] = "USER";
+})(ERROR = exports.ERROR || (exports.ERROR = {}));
+var TYPE;
+(function (TYPE) {
+ TYPE[TYPE["BOTH"] = 0] = "BOTH";
+ TYPE[TYPE["REQUEST"] = 1] = "REQUEST";
+ TYPE[TYPE["RESPONSE"] = 2] = "RESPONSE";
+})(TYPE = exports.TYPE || (exports.TYPE = {}));
+var FLAGS;
+(function (FLAGS) {
+ FLAGS[FLAGS["CONNECTION_KEEP_ALIVE"] = 1] = "CONNECTION_KEEP_ALIVE";
+ FLAGS[FLAGS["CONNECTION_CLOSE"] = 2] = "CONNECTION_CLOSE";
+ FLAGS[FLAGS["CONNECTION_UPGRADE"] = 4] = "CONNECTION_UPGRADE";
+ FLAGS[FLAGS["CHUNKED"] = 8] = "CHUNKED";
+ FLAGS[FLAGS["UPGRADE"] = 16] = "UPGRADE";
+ FLAGS[FLAGS["CONTENT_LENGTH"] = 32] = "CONTENT_LENGTH";
+ FLAGS[FLAGS["SKIPBODY"] = 64] = "SKIPBODY";
+ FLAGS[FLAGS["TRAILING"] = 128] = "TRAILING";
+ // 1 << 8 is unused
+ FLAGS[FLAGS["TRANSFER_ENCODING"] = 512] = "TRANSFER_ENCODING";
+})(FLAGS = exports.FLAGS || (exports.FLAGS = {}));
+var LENIENT_FLAGS;
+(function (LENIENT_FLAGS) {
+ LENIENT_FLAGS[LENIENT_FLAGS["HEADERS"] = 1] = "HEADERS";
+ LENIENT_FLAGS[LENIENT_FLAGS["CHUNKED_LENGTH"] = 2] = "CHUNKED_LENGTH";
+ LENIENT_FLAGS[LENIENT_FLAGS["KEEP_ALIVE"] = 4] = "KEEP_ALIVE";
+})(LENIENT_FLAGS = exports.LENIENT_FLAGS || (exports.LENIENT_FLAGS = {}));
+var METHODS;
+(function (METHODS) {
+ METHODS[METHODS["DELETE"] = 0] = "DELETE";
+ METHODS[METHODS["GET"] = 1] = "GET";
+ METHODS[METHODS["HEAD"] = 2] = "HEAD";
+ METHODS[METHODS["POST"] = 3] = "POST";
+ METHODS[METHODS["PUT"] = 4] = "PUT";
+ /* pathological */
+ METHODS[METHODS["CONNECT"] = 5] = "CONNECT";
+ METHODS[METHODS["OPTIONS"] = 6] = "OPTIONS";
+ METHODS[METHODS["TRACE"] = 7] = "TRACE";
+ /* WebDAV */
+ METHODS[METHODS["COPY"] = 8] = "COPY";
+ METHODS[METHODS["LOCK"] = 9] = "LOCK";
+ METHODS[METHODS["MKCOL"] = 10] = "MKCOL";
+ METHODS[METHODS["MOVE"] = 11] = "MOVE";
+ METHODS[METHODS["PROPFIND"] = 12] = "PROPFIND";
+ METHODS[METHODS["PROPPATCH"] = 13] = "PROPPATCH";
+ METHODS[METHODS["SEARCH"] = 14] = "SEARCH";
+ METHODS[METHODS["UNLOCK"] = 15] = "UNLOCK";
+ METHODS[METHODS["BIND"] = 16] = "BIND";
+ METHODS[METHODS["REBIND"] = 17] = "REBIND";
+ METHODS[METHODS["UNBIND"] = 18] = "UNBIND";
+ METHODS[METHODS["ACL"] = 19] = "ACL";
+ /* subversion */
+ METHODS[METHODS["REPORT"] = 20] = "REPORT";
+ METHODS[METHODS["MKACTIVITY"] = 21] = "MKACTIVITY";
+ METHODS[METHODS["CHECKOUT"] = 22] = "CHECKOUT";
+ METHODS[METHODS["MERGE"] = 23] = "MERGE";
+ /* upnp */
+ METHODS[METHODS["M-SEARCH"] = 24] = "M-SEARCH";
+ METHODS[METHODS["NOTIFY"] = 25] = "NOTIFY";
+ METHODS[METHODS["SUBSCRIBE"] = 26] = "SUBSCRIBE";
+ METHODS[METHODS["UNSUBSCRIBE"] = 27] = "UNSUBSCRIBE";
+ /* RFC-5789 */
+ METHODS[METHODS["PATCH"] = 28] = "PATCH";
+ METHODS[METHODS["PURGE"] = 29] = "PURGE";
+ /* CalDAV */
+ METHODS[METHODS["MKCALENDAR"] = 30] = "MKCALENDAR";
+ /* RFC-2068, section 19.6.1.2 */
+ METHODS[METHODS["LINK"] = 31] = "LINK";
+ METHODS[METHODS["UNLINK"] = 32] = "UNLINK";
+ /* icecast */
+ METHODS[METHODS["SOURCE"] = 33] = "SOURCE";
+ /* RFC-7540, section 11.6 */
+ METHODS[METHODS["PRI"] = 34] = "PRI";
+ /* RFC-2326 RTSP */
+ METHODS[METHODS["DESCRIBE"] = 35] = "DESCRIBE";
+ METHODS[METHODS["ANNOUNCE"] = 36] = "ANNOUNCE";
+ METHODS[METHODS["SETUP"] = 37] = "SETUP";
+ METHODS[METHODS["PLAY"] = 38] = "PLAY";
+ METHODS[METHODS["PAUSE"] = 39] = "PAUSE";
+ METHODS[METHODS["TEARDOWN"] = 40] = "TEARDOWN";
+ METHODS[METHODS["GET_PARAMETER"] = 41] = "GET_PARAMETER";
+ METHODS[METHODS["SET_PARAMETER"] = 42] = "SET_PARAMETER";
+ METHODS[METHODS["REDIRECT"] = 43] = "REDIRECT";
+ METHODS[METHODS["RECORD"] = 44] = "RECORD";
+ /* RAOP */
+ METHODS[METHODS["FLUSH"] = 45] = "FLUSH";
+})(METHODS = exports.METHODS || (exports.METHODS = {}));
+exports.METHODS_HTTP = [
+ METHODS.DELETE,
+ METHODS.GET,
+ METHODS.HEAD,
+ METHODS.POST,
+ METHODS.PUT,
+ METHODS.CONNECT,
+ METHODS.OPTIONS,
+ METHODS.TRACE,
+ METHODS.COPY,
+ METHODS.LOCK,
+ METHODS.MKCOL,
+ METHODS.MOVE,
+ METHODS.PROPFIND,
+ METHODS.PROPPATCH,
+ METHODS.SEARCH,
+ METHODS.UNLOCK,
+ METHODS.BIND,
+ METHODS.REBIND,
+ METHODS.UNBIND,
+ METHODS.ACL,
+ METHODS.REPORT,
+ METHODS.MKACTIVITY,
+ METHODS.CHECKOUT,
+ METHODS.MERGE,
+ METHODS['M-SEARCH'],
+ METHODS.NOTIFY,
+ METHODS.SUBSCRIBE,
+ METHODS.UNSUBSCRIBE,
+ METHODS.PATCH,
+ METHODS.PURGE,
+ METHODS.MKCALENDAR,
+ METHODS.LINK,
+ METHODS.UNLINK,
+ METHODS.PRI,
+ // TODO(indutny): should we allow it with HTTP?
+ METHODS.SOURCE,
+];
+exports.METHODS_ICE = [
+ METHODS.SOURCE,
+];
+exports.METHODS_RTSP = [
+ METHODS.OPTIONS,
+ METHODS.DESCRIBE,
+ METHODS.ANNOUNCE,
+ METHODS.SETUP,
+ METHODS.PLAY,
+ METHODS.PAUSE,
+ METHODS.TEARDOWN,
+ METHODS.GET_PARAMETER,
+ METHODS.SET_PARAMETER,
+ METHODS.REDIRECT,
+ METHODS.RECORD,
+ METHODS.FLUSH,
+ // For AirPlay
+ METHODS.GET,
+ METHODS.POST,
+];
+exports.METHOD_MAP = utils_1.enumToMap(METHODS);
+exports.H_METHOD_MAP = {};
+Object.keys(exports.METHOD_MAP).forEach((key) => {
+ if (/^H/.test(key)) {
+ exports.H_METHOD_MAP[key] = exports.METHOD_MAP[key];
+ }
+});
+var FINISH;
+(function (FINISH) {
+ FINISH[FINISH["SAFE"] = 0] = "SAFE";
+ FINISH[FINISH["SAFE_WITH_CB"] = 1] = "SAFE_WITH_CB";
+ FINISH[FINISH["UNSAFE"] = 2] = "UNSAFE";
+})(FINISH = exports.FINISH || (exports.FINISH = {}));
+exports.ALPHA = [];
+for (let i = 'A'.charCodeAt(0); i <= 'Z'.charCodeAt(0); i++) {
+ // Upper case
+ exports.ALPHA.push(String.fromCharCode(i));
+ // Lower case
+ exports.ALPHA.push(String.fromCharCode(i + 0x20));
+}
+exports.NUM_MAP = {
+ 0: 0, 1: 1, 2: 2, 3: 3, 4: 4,
+ 5: 5, 6: 6, 7: 7, 8: 8, 9: 9,
+};
+exports.HEX_MAP = {
+ 0: 0, 1: 1, 2: 2, 3: 3, 4: 4,
+ 5: 5, 6: 6, 7: 7, 8: 8, 9: 9,
+ A: 0XA, B: 0XB, C: 0XC, D: 0XD, E: 0XE, F: 0XF,
+ a: 0xa, b: 0xb, c: 0xc, d: 0xd, e: 0xe, f: 0xf,
+};
+exports.NUM = [
+ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
+];
+exports.ALPHANUM = exports.ALPHA.concat(exports.NUM);
+exports.MARK = ['-', '_', '.', '!', '~', '*', '\'', '(', ')'];
+exports.USERINFO_CHARS = exports.ALPHANUM
+ .concat(exports.MARK)
+ .concat(['%', ';', ':', '&', '=', '+', '$', ',']);
+// TODO(indutny): use RFC
+exports.STRICT_URL_CHAR = [
+ '!', '"', '$', '%', '&', '\'',
+ '(', ')', '*', '+', ',', '-', '.', '/',
+ ':', ';', '<', '=', '>',
+ '@', '[', '\\', ']', '^', '_',
+ '`',
+ '{', '|', '}', '~',
+].concat(exports.ALPHANUM);
+exports.URL_CHAR = exports.STRICT_URL_CHAR
+ .concat(['\t', '\f']);
+// All characters with 0x80 bit set to 1
+for (let i = 0x80; i <= 0xff; i++) {
+ exports.URL_CHAR.push(i);
+}
+exports.HEX = exports.NUM.concat(['a', 'b', 'c', 'd', 'e', 'f', 'A', 'B', 'C', 'D', 'E', 'F']);
+/* Tokens as defined by rfc 2616. Also lowercases them.
+ * token = 1*<any CHAR except CTLs or separators>
+ * separators = "(" | ")" | "<" | ">" | "@"
+ * | "," | ";" | ":" | "\" | <">
+ * | "/" | "[" | "]" | "?" | "="
+ * | "{" | "}" | SP | HT
+ */
+exports.STRICT_TOKEN = [
+ '!', '#', '$', '%', '&', '\'',
+ '*', '+', '-', '.',
+ '^', '_', '`',
+ '|', '~',
+].concat(exports.ALPHANUM);
+exports.TOKEN = exports.STRICT_TOKEN.concat([' ']);
+/*
+ * Verify that a char is a valid visible (printable) US-ASCII
+ * character or %x80-FF
+ */
+exports.HEADER_CHARS = ['\t'];
+for (let i = 32; i <= 255; i++) {
+ if (i !== 127) {
+ exports.HEADER_CHARS.push(i);
+ }
+}
+// ',' = \x44
+exports.CONNECTION_TOKEN_CHARS = exports.HEADER_CHARS.filter((c) => c !== 44);
+exports.MAJOR = exports.NUM_MAP;
+exports.MINOR = exports.MAJOR;
+var HEADER_STATE;
+(function (HEADER_STATE) {
+ HEADER_STATE[HEADER_STATE["GENERAL"] = 0] = "GENERAL";
+ HEADER_STATE[HEADER_STATE["CONNECTION"] = 1] = "CONNECTION";
+ HEADER_STATE[HEADER_STATE["CONTENT_LENGTH"] = 2] = "CONTENT_LENGTH";
+ HEADER_STATE[HEADER_STATE["TRANSFER_ENCODING"] = 3] = "TRANSFER_ENCODING";
+ HEADER_STATE[HEADER_STATE["UPGRADE"] = 4] = "UPGRADE";
+ HEADER_STATE[HEADER_STATE["CONNECTION_KEEP_ALIVE"] = 5] = "CONNECTION_KEEP_ALIVE";
+ HEADER_STATE[HEADER_STATE["CONNECTION_CLOSE"] = 6] = "CONNECTION_CLOSE";
+ HEADER_STATE[HEADER_STATE["CONNECTION_UPGRADE"] = 7] = "CONNECTION_UPGRADE";
+ HEADER_STATE[HEADER_STATE["TRANSFER_ENCODING_CHUNKED"] = 8] = "TRANSFER_ENCODING_CHUNKED";
+})(HEADER_STATE = exports.HEADER_STATE || (exports.HEADER_STATE = {}));
+exports.SPECIAL_HEADERS = {
+ 'connection': HEADER_STATE.CONNECTION,
+ 'content-length': HEADER_STATE.CONTENT_LENGTH,
+ 'proxy-connection': HEADER_STATE.CONNECTION,
+ 'transfer-encoding': HEADER_STATE.TRANSFER_ENCODING,
+ 'upgrade': HEADER_STATE.UPGRADE,
+};
+//# sourceMappingURL=constants.js.map \ No newline at end of file
diff --git a/lib/llhttp/utils.d.ts b/lib/llhttp/utils.d.ts
new file mode 100644
index 0000000..15497f3
--- /dev/null
+++ b/lib/llhttp/utils.d.ts
@@ -0,0 +1,4 @@
+export interface IEnumMap {
+ [key: string]: number;
+}
+export declare function enumToMap(obj: any): IEnumMap;
diff --git a/lib/llhttp/utils.js b/lib/llhttp/utils.js
new file mode 100644
index 0000000..8a32e56
--- /dev/null
+++ b/lib/llhttp/utils.js
@@ -0,0 +1,15 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.enumToMap = void 0;
+function enumToMap(obj) {
+ const res = {};
+ Object.keys(obj).forEach((key) => {
+ const value = obj[key];
+ if (typeof value === 'number') {
+ res[key] = value;
+ }
+ });
+ return res;
+}
+exports.enumToMap = enumToMap;
+//# sourceMappingURL=utils.js.map \ No newline at end of file
diff --git a/lib/llhttp/wasm_build_env.txt b/lib/llhttp/wasm_build_env.txt
new file mode 100644
index 0000000..5f478b5
--- /dev/null
+++ b/lib/llhttp/wasm_build_env.txt
@@ -0,0 +1,32 @@
+alpine-baselayout-data-3.4.0-r0
+musl-1.2.3-r4
+busybox-1.35.0-r29
+busybox-binsh-1.35.0-r29
+alpine-baselayout-3.4.0-r0
+alpine-keys-2.4-r1
+ca-certificates-bundle-20220614-r4
+libcrypto3-3.0.8-r3
+libssl3-3.0.8-r3
+ssl_client-1.35.0-r29
+zlib-1.2.13-r0
+apk-tools-2.12.10-r1
+scanelf-1.3.5-r1
+musl-utils-1.2.3-r4
+libc-utils-0.7.2-r3
+libgcc-12.2.1_git20220924-r4
+libstdc++-12.2.1_git20220924-r4
+libffi-3.4.4-r0
+xz-libs-5.2.9-r0
+libxml2-2.10.4-r0
+zstd-libs-1.5.5-r0
+llvm15-libs-15.0.7-r0
+clang15-libs-15.0.7-r0
+libstdc++-dev-12.2.1_git20220924-r4
+clang15-15.0.7-r0
+lld-libs-15.0.7-r0
+lld-15.0.7-r0
+wasi-libc-0.20220525-r1
+wasi-libcxx-15.0.7-r0
+wasi-libcxxabi-15.0.7-r0
+wasi-compiler-rt-15.0.7-r0
+wasi-sdk-16-r0
diff --git a/lib/mock/mock-agent.js b/lib/mock/mock-agent.js
new file mode 100644
index 0000000..828e8af
--- /dev/null
+++ b/lib/mock/mock-agent.js
@@ -0,0 +1,171 @@
+'use strict'
+
+const { kClients } = require('../core/symbols')
+const Agent = require('../agent')
+const {
+ kAgent,
+ kMockAgentSet,
+ kMockAgentGet,
+ kDispatches,
+ kIsMockActive,
+ kNetConnect,
+ kGetNetConnect,
+ kOptions,
+ kFactory
+} = require('./mock-symbols')
+const MockClient = require('./mock-client')
+const MockPool = require('./mock-pool')
+const { matchValue, buildMockOptions } = require('./mock-utils')
+const { InvalidArgumentError, UndiciError } = require('../core/errors')
+const Dispatcher = require('../dispatcher')
+const Pluralizer = require('./pluralizer')
+const PendingInterceptorsFormatter = require('./pending-interceptors-formatter')
+
+class FakeWeakRef {
+ constructor (value) {
+ this.value = value
+ }
+
+ deref () {
+ return this.value
+ }
+}
+
+class MockAgent extends Dispatcher {
+ constructor (opts) {
+ super(opts)
+
+ this[kNetConnect] = true
+ this[kIsMockActive] = true
+
+ // Instantiate Agent and encapsulate
+ if ((opts && opts.agent && typeof opts.agent.dispatch !== 'function')) {
+ throw new InvalidArgumentError('Argument opts.agent must implement Agent')
+ }
+ const agent = opts && opts.agent ? opts.agent : new Agent(opts)
+ this[kAgent] = agent
+
+ this[kClients] = agent[kClients]
+ this[kOptions] = buildMockOptions(opts)
+ }
+
+ get (origin) {
+ let dispatcher = this[kMockAgentGet](origin)
+
+ if (!dispatcher) {
+ dispatcher = this[kFactory](origin)
+ this[kMockAgentSet](origin, dispatcher)
+ }
+ return dispatcher
+ }
+
+ dispatch (opts, handler) {
+ // Call MockAgent.get to perform additional setup before dispatching as normal
+ this.get(opts.origin)
+ return this[kAgent].dispatch(opts, handler)
+ }
+
+ async close () {
+ await this[kAgent].close()
+ this[kClients].clear()
+ }
+
+ deactivate () {
+ this[kIsMockActive] = false
+ }
+
+ activate () {
+ this[kIsMockActive] = true
+ }
+
+ enableNetConnect (matcher) {
+ if (typeof matcher === 'string' || typeof matcher === 'function' || matcher instanceof RegExp) {
+ if (Array.isArray(this[kNetConnect])) {
+ this[kNetConnect].push(matcher)
+ } else {
+ this[kNetConnect] = [matcher]
+ }
+ } else if (typeof matcher === 'undefined') {
+ this[kNetConnect] = true
+ } else {
+ throw new InvalidArgumentError('Unsupported matcher. Must be one of String|Function|RegExp.')
+ }
+ }
+
+ disableNetConnect () {
+ this[kNetConnect] = false
+ }
+
+ // This is required to bypass issues caused by using global symbols - see:
+ // https://github.com/nodejs/undici/issues/1447
+ get isMockActive () {
+ return this[kIsMockActive]
+ }
+
+ [kMockAgentSet] (origin, dispatcher) {
+ this[kClients].set(origin, new FakeWeakRef(dispatcher))
+ }
+
+ [kFactory] (origin) {
+ const mockOptions = Object.assign({ agent: this }, this[kOptions])
+ return this[kOptions] && this[kOptions].connections === 1
+ ? new MockClient(origin, mockOptions)
+ : new MockPool(origin, mockOptions)
+ }
+
+ [kMockAgentGet] (origin) {
+ // First check if we can immediately find it
+ const ref = this[kClients].get(origin)
+ if (ref) {
+ return ref.deref()
+ }
+
+ // If the origin is not a string create a dummy parent pool and return to user
+ if (typeof origin !== 'string') {
+ const dispatcher = this[kFactory]('http://localhost:9999')
+ this[kMockAgentSet](origin, dispatcher)
+ return dispatcher
+ }
+
+ // If we match, create a pool and assign the same dispatches
+ for (const [keyMatcher, nonExplicitRef] of Array.from(this[kClients])) {
+ const nonExplicitDispatcher = nonExplicitRef.deref()
+ if (nonExplicitDispatcher && typeof keyMatcher !== 'string' && matchValue(keyMatcher, origin)) {
+ const dispatcher = this[kFactory](origin)
+ this[kMockAgentSet](origin, dispatcher)
+ dispatcher[kDispatches] = nonExplicitDispatcher[kDispatches]
+ return dispatcher
+ }
+ }
+ }
+
+ [kGetNetConnect] () {
+ return this[kNetConnect]
+ }
+
+ pendingInterceptors () {
+ const mockAgentClients = this[kClients]
+
+ return Array.from(mockAgentClients.entries())
+ .flatMap(([origin, scope]) => scope.deref()[kDispatches].map(dispatch => ({ ...dispatch, origin })))
+ .filter(({ pending }) => pending)
+ }
+
+ assertNoPendingInterceptors ({ pendingInterceptorsFormatter = new PendingInterceptorsFormatter() } = {}) {
+ const pending = this.pendingInterceptors()
+
+ if (pending.length === 0) {
+ return
+ }
+
+ const pluralizer = new Pluralizer('interceptor', 'interceptors').pluralize(pending.length)
+
+ throw new UndiciError(`
+${pluralizer.count} ${pluralizer.noun} ${pluralizer.is} pending:
+
+${pendingInterceptorsFormatter.format(pending)}
+`.trim())
+ }
+}
+
+module.exports = MockAgent
diff --git a/lib/mock/mock-client.js b/lib/mock/mock-client.js
new file mode 100644
index 0000000..5f31215
--- /dev/null
+++ b/lib/mock/mock-client.js
@@ -0,0 +1,59 @@
+'use strict'
+
+const { promisify } = require('util')
+const Client = require('../client')
+const { buildMockDispatch } = require('./mock-utils')
+const {
+ kDispatches,
+ kMockAgent,
+ kClose,
+ kOriginalClose,
+ kOrigin,
+ kOriginalDispatch,
+ kConnected
+} = require('./mock-symbols')
+const { MockInterceptor } = require('./mock-interceptor')
+const Symbols = require('../core/symbols')
+const { InvalidArgumentError } = require('../core/errors')
+
+/**
+ * MockClient provides an API that extends the Client to influence the mockDispatches.
+ */
+class MockClient extends Client {
+ constructor (origin, opts) {
+ super(origin, opts)
+
+ if (!opts || !opts.agent || typeof opts.agent.dispatch !== 'function') {
+ throw new InvalidArgumentError('Argument opts.agent must implement Agent')
+ }
+
+ this[kMockAgent] = opts.agent
+ this[kOrigin] = origin
+ this[kDispatches] = []
+ this[kConnected] = 1
+ this[kOriginalDispatch] = this.dispatch
+ this[kOriginalClose] = this.close.bind(this)
+
+ this.dispatch = buildMockDispatch.call(this)
+ this.close = this[kClose]
+ }
+
+ get [Symbols.kConnected] () {
+ return this[kConnected]
+ }
+
+ /**
+ * Sets up the base interceptor for mocking replies from undici.
+ */
+ intercept (opts) {
+ return new MockInterceptor(opts, this[kDispatches])
+ }
+
+ async [kClose] () {
+ await promisify(this[kOriginalClose])()
+ this[kConnected] = 0
+ this[kMockAgent][Symbols.kClients].delete(this[kOrigin])
+ }
+}
+
+module.exports = MockClient
diff --git a/lib/mock/mock-errors.js b/lib/mock/mock-errors.js
new file mode 100644
index 0000000..5442c0e
--- /dev/null
+++ b/lib/mock/mock-errors.js
@@ -0,0 +1,17 @@
+'use strict'
+
+const { UndiciError } = require('../core/errors')
+
+class MockNotMatchedError extends UndiciError {
+ constructor (message) {
+ super(message)
+ Error.captureStackTrace(this, MockNotMatchedError)
+ this.name = 'MockNotMatchedError'
+ this.message = message || 'The request does not match any registered mock dispatches'
+ this.code = 'UND_MOCK_ERR_MOCK_NOT_MATCHED'
+ }
+}
+
+module.exports = {
+ MockNotMatchedError
+}
diff --git a/lib/mock/mock-interceptor.js b/lib/mock/mock-interceptor.js
new file mode 100644
index 0000000..781e477
--- /dev/null
+++ b/lib/mock/mock-interceptor.js
@@ -0,0 +1,206 @@
+'use strict'
+
+const { getResponseData, buildKey, addMockDispatch } = require('./mock-utils')
+const {
+ kDispatches,
+ kDispatchKey,
+ kDefaultHeaders,
+ kDefaultTrailers,
+ kContentLength,
+ kMockDispatch
+} = require('./mock-symbols')
+const { InvalidArgumentError } = require('../core/errors')
+const { buildURL } = require('../core/util')
+
+/**
+ * Defines the scope API for an interceptor reply
+ */
+class MockScope {
+ constructor (mockDispatch) {
+ this[kMockDispatch] = mockDispatch
+ }
+
+ /**
+ * Delay a reply by a set amount in ms.
+ */
+ delay (waitInMs) {
+ if (typeof waitInMs !== 'number' || !Number.isInteger(waitInMs) || waitInMs <= 0) {
+ throw new InvalidArgumentError('waitInMs must be a valid integer > 0')
+ }
+
+ this[kMockDispatch].delay = waitInMs
+ return this
+ }
+
+ /**
+ * For a defined reply, never mark as consumed.
+ */
+ persist () {
+ this[kMockDispatch].persist = true
+ return this
+ }
+
+ /**
+ * Allow one to define a reply for a set amount of matching requests.
+ */
+ times (repeatTimes) {
+ if (typeof repeatTimes !== 'number' || !Number.isInteger(repeatTimes) || repeatTimes <= 0) {
+ throw new InvalidArgumentError('repeatTimes must be a valid integer > 0')
+ }
+
+ this[kMockDispatch].times = repeatTimes
+ return this
+ }
+}
+
+/**
+ * Defines an interceptor for a Mock
+ */
+class MockInterceptor {
+ constructor (opts, mockDispatches) {
+ if (typeof opts !== 'object') {
+ throw new InvalidArgumentError('opts must be an object')
+ }
+ if (typeof opts.path === 'undefined') {
+ throw new InvalidArgumentError('opts.path must be defined')
+ }
+ if (typeof opts.method === 'undefined') {
+ opts.method = 'GET'
+ }
+ // See https://github.com/nodejs/undici/issues/1245
+ // As per RFC 3986, clients are not supposed to send URI
+ // fragments to servers when they retrieve a document,
+ if (typeof opts.path === 'string') {
+ if (opts.query) {
+ opts.path = buildURL(opts.path, opts.query)
+ } else {
+ // Matches https://github.com/nodejs/undici/blob/main/lib/fetch/index.js#L1811
+ const parsedURL = new URL(opts.path, 'data://')
+ opts.path = parsedURL.pathname + parsedURL.search
+ }
+ }
+ if (typeof opts.method === 'string') {
+ opts.method = opts.method.toUpperCase()
+ }
+
+ this[kDispatchKey] = buildKey(opts)
+ this[kDispatches] = mockDispatches
+ this[kDefaultHeaders] = {}
+ this[kDefaultTrailers] = {}
+ this[kContentLength] = false
+ }
+
+ createMockScopeDispatchData (statusCode, data, responseOptions = {}) {
+ const responseData = getResponseData(data)
+ const contentLength = this[kContentLength] ? { 'content-length': responseData.length } : {}
+ const headers = { ...this[kDefaultHeaders], ...contentLength, ...responseOptions.headers }
+ const trailers = { ...this[kDefaultTrailers], ...responseOptions.trailers }
+
+ return { statusCode, data, headers, trailers }
+ }
+
+ validateReplyParameters (statusCode, data, responseOptions) {
+ if (typeof statusCode === 'undefined') {
+ throw new InvalidArgumentError('statusCode must be defined')
+ }
+ if (typeof data === 'undefined') {
+ throw new InvalidArgumentError('data must be defined')
+ }
+ if (typeof responseOptions !== 'object') {
+ throw new InvalidArgumentError('responseOptions must be an object')
+ }
+ }
+
+ /**
+ * Mock an undici request with a defined reply.
+ */
+ reply (replyData) {
+ // Values of reply aren't available right now as they
+ // can only be available when the reply callback is invoked.
+ if (typeof replyData === 'function') {
+ // We'll first wrap the provided callback in another function,
+ // this function will properly resolve the data from the callback
+ // when invoked.
+ const wrappedDefaultsCallback = (opts) => {
+ // Our reply options callback contains the parameter for statusCode, data and options.
+ const resolvedData = replyData(opts)
+
+ // Check if it is in the right format
+ if (typeof resolvedData !== 'object') {
+ throw new InvalidArgumentError('reply options callback must return an object')
+ }
+
+ const { statusCode, data = '', responseOptions = {} } = resolvedData
+ this.validateReplyParameters(statusCode, data, responseOptions)
+ // Since the values can be obtained immediately we return them
+ // from this higher order function that will be resolved later.
+ return {
+ ...this.createMockScopeDispatchData(statusCode, data, responseOptions)
+ }
+ }
+
+ // Add usual dispatch data, but this time set the data parameter to function that will eventually provide data.
+ const newMockDispatch = addMockDispatch(this[kDispatches], this[kDispatchKey], wrappedDefaultsCallback)
+ return new MockScope(newMockDispatch)
+ }
+
+ // We can have either one or three parameters, if we get here,
+ // we should have 1-3 parameters. So we spread the arguments of
+ // this function to obtain the parameters, since replyData will always
+ // just be the statusCode.
+ const [statusCode, data = '', responseOptions = {}] = [...arguments]
+ this.validateReplyParameters(statusCode, data, responseOptions)
+
+ // Send in-already provided data like usual
+ const dispatchData = this.createMockScopeDispatchData(statusCode, data, responseOptions)
+ const newMockDispatch = addMockDispatch(this[kDispatches], this[kDispatchKey], dispatchData)
+ return new MockScope(newMockDispatch)
+ }
+
+ /**
+ * Mock an undici request with a defined error.
+ */
+ replyWithError (error) {
+ if (typeof error === 'undefined') {
+ throw new InvalidArgumentError('error must be defined')
+ }
+
+ const newMockDispatch = addMockDispatch(this[kDispatches], this[kDispatchKey], { error })
+ return new MockScope(newMockDispatch)
+ }
+
+ /**
+ * Set default reply headers on the interceptor for subsequent replies
+ */
+ defaultReplyHeaders (headers) {
+ if (typeof headers === 'undefined') {
+ throw new InvalidArgumentError('headers must be defined')
+ }
+
+ this[kDefaultHeaders] = headers
+ return this
+ }
+
+ /**
+ * Set default reply trailers on the interceptor for subsequent replies
+ */
+ defaultReplyTrailers (trailers) {
+ if (typeof trailers === 'undefined') {
+ throw new InvalidArgumentError('trailers must be defined')
+ }
+
+ this[kDefaultTrailers] = trailers
+ return this
+ }
+
+ /**
+ * Set reply content length header for replies on the interceptor
+ */
+ replyContentLength () {
+ this[kContentLength] = true
+ return this
+ }
+}
+
+module.exports.MockInterceptor = MockInterceptor
+module.exports.MockScope = MockScope
diff --git a/lib/mock/mock-pool.js b/lib/mock/mock-pool.js
new file mode 100644
index 0000000..0a3a7cd
--- /dev/null
+++ b/lib/mock/mock-pool.js
@@ -0,0 +1,59 @@
+'use strict'
+
+const { promisify } = require('util')
+const Pool = require('../pool')
+const { buildMockDispatch } = require('./mock-utils')
+const {
+ kDispatches,
+ kMockAgent,
+ kClose,
+ kOriginalClose,
+ kOrigin,
+ kOriginalDispatch,
+ kConnected
+} = require('./mock-symbols')
+const { MockInterceptor } = require('./mock-interceptor')
+const Symbols = require('../core/symbols')
+const { InvalidArgumentError } = require('../core/errors')
+
+/**
+ * MockPool provides an API that extends the Pool to influence the mockDispatches.
+ */
+class MockPool extends Pool {
+ constructor (origin, opts) {
+ super(origin, opts)
+
+ if (!opts || !opts.agent || typeof opts.agent.dispatch !== 'function') {
+ throw new InvalidArgumentError('Argument opts.agent must implement Agent')
+ }
+
+ this[kMockAgent] = opts.agent
+ this[kOrigin] = origin
+ this[kDispatches] = []
+ this[kConnected] = 1
+ this[kOriginalDispatch] = this.dispatch
+ this[kOriginalClose] = this.close.bind(this)
+
+ this.dispatch = buildMockDispatch.call(this)
+ this.close = this[kClose]
+ }
+
+ get [Symbols.kConnected] () {
+ return this[kConnected]
+ }
+
+ /**
+ * Sets up the base interceptor for mocking replies from undici.
+ */
+ intercept (opts) {
+ return new MockInterceptor(opts, this[kDispatches])
+ }
+
+ async [kClose] () {
+ await promisify(this[kOriginalClose])()
+ this[kConnected] = 0
+ this[kMockAgent][Symbols.kClients].delete(this[kOrigin])
+ }
+}
+
+module.exports = MockPool
diff --git a/lib/mock/mock-symbols.js b/lib/mock/mock-symbols.js
new file mode 100644
index 0000000..8c4cbb6
--- /dev/null
+++ b/lib/mock/mock-symbols.js
@@ -0,0 +1,23 @@
+'use strict'
+
+module.exports = {
+ kAgent: Symbol('agent'),
+ kOptions: Symbol('options'),
+ kFactory: Symbol('factory'),
+ kDispatches: Symbol('dispatches'),
+ kDispatchKey: Symbol('dispatch key'),
+ kDefaultHeaders: Symbol('default headers'),
+ kDefaultTrailers: Symbol('default trailers'),
+ kContentLength: Symbol('content length'),
+ kMockAgent: Symbol('mock agent'),
+ kMockAgentSet: Symbol('mock agent set'),
+ kMockAgentGet: Symbol('mock agent get'),
+ kMockDispatch: Symbol('mock dispatch'),
+ kClose: Symbol('close'),
+ kOriginalClose: Symbol('original agent close'),
+ kOrigin: Symbol('origin'),
+ kIsMockActive: Symbol('is mock active'),
+ kNetConnect: Symbol('net connect'),
+ kGetNetConnect: Symbol('get net connect'),
+ kConnected: Symbol('connected')
+}
diff --git a/lib/mock/mock-utils.js b/lib/mock/mock-utils.js
new file mode 100644
index 0000000..42ea185
--- /dev/null
+++ b/lib/mock/mock-utils.js
@@ -0,0 +1,351 @@
+'use strict'
+
+const { MockNotMatchedError } = require('./mock-errors')
+const {
+ kDispatches,
+ kMockAgent,
+ kOriginalDispatch,
+ kOrigin,
+ kGetNetConnect
+} = require('./mock-symbols')
+const { buildURL, nop } = require('../core/util')
+const { STATUS_CODES } = require('http')
+const {
+ types: {
+ isPromise
+ }
+} = require('util')
+
+function matchValue (match, value) {
+ if (typeof match === 'string') {
+ return match === value
+ }
+ if (match instanceof RegExp) {
+ return match.test(value)
+ }
+ if (typeof match === 'function') {
+ return match(value) === true
+ }
+ return false
+}
+
+function lowerCaseEntries (headers) {
+ return Object.fromEntries(
+ Object.entries(headers).map(([headerName, headerValue]) => {
+ return [headerName.toLocaleLowerCase(), headerValue]
+ })
+ )
+}
+
+/**
+ * @param {import('../../index').Headers|string[]|Record<string, string>} headers
+ * @param {string} key
+ */
+function getHeaderByName (headers, key) {
+ if (Array.isArray(headers)) {
+ for (let i = 0; i < headers.length; i += 2) {
+ if (headers[i].toLocaleLowerCase() === key.toLocaleLowerCase()) {
+ return headers[i + 1]
+ }
+ }
+
+ return undefined
+ } else if (typeof headers.get === 'function') {
+ return headers.get(key)
+ } else {
+ return lowerCaseEntries(headers)[key.toLocaleLowerCase()]
+ }
+}
+
+/** @param {string[]} headers */
+function buildHeadersFromArray (headers) { // fetch HeadersList
+ const clone = headers.slice()
+ const entries = []
+ for (let index = 0; index < clone.length; index += 2) {
+ entries.push([clone[index], clone[index + 1]])
+ }
+ return Object.fromEntries(entries)
+}
+
+function matchHeaders (mockDispatch, headers) {
+ if (typeof mockDispatch.headers === 'function') {
+ if (Array.isArray(headers)) { // fetch HeadersList
+ headers = buildHeadersFromArray(headers)
+ }
+ return mockDispatch.headers(headers ? lowerCaseEntries(headers) : {})
+ }
+ if (typeof mockDispatch.headers === 'undefined') {
+ return true
+ }
+ if (typeof headers !== 'object' || typeof mockDispatch.headers !== 'object') {
+ return false
+ }
+
+ for (const [matchHeaderName, matchHeaderValue] of Object.entries(mockDispatch.headers)) {
+ const headerValue = getHeaderByName(headers, matchHeaderName)
+
+ if (!matchValue(matchHeaderValue, headerValue)) {
+ return false
+ }
+ }
+ return true
+}
+
+function safeUrl (path) {
+ if (typeof path !== 'string') {
+ return path
+ }
+
+ const pathSegments = path.split('?')
+
+ if (pathSegments.length !== 2) {
+ return path
+ }
+
+ const qp = new URLSearchParams(pathSegments.pop())
+ qp.sort()
+ return [...pathSegments, qp.toString()].join('?')
+}
+
+function matchKey (mockDispatch, { path, method, body, headers }) {
+ const pathMatch = matchValue(mockDispatch.path, path)
+ const methodMatch = matchValue(mockDispatch.method, method)
+ const bodyMatch = typeof mockDispatch.body !== 'undefined' ? matchValue(mockDispatch.body, body) : true
+ const headersMatch = matchHeaders(mockDispatch, headers)
+ return pathMatch && methodMatch && bodyMatch && headersMatch
+}
+
+function getResponseData (data) {
+ if (Buffer.isBuffer(data)) {
+ return data
+ } else if (typeof data === 'object') {
+ return JSON.stringify(data)
+ } else {
+ return data.toString()
+ }
+}
+
+function getMockDispatch (mockDispatches, key) {
+ const basePath = key.query ? buildURL(key.path, key.query) : key.path
+ const resolvedPath = typeof basePath === 'string' ? safeUrl(basePath) : basePath
+
+ // Match path
+ let matchedMockDispatches = mockDispatches.filter(({ consumed }) => !consumed).filter(({ path }) => matchValue(safeUrl(path), resolvedPath))
+ if (matchedMockDispatches.length === 0) {
+ throw new MockNotMatchedError(`Mock dispatch not matched for path '${resolvedPath}'`)
+ }
+
+ // Match method
+ matchedMockDispatches = matchedMockDispatches.filter(({ method }) => matchValue(method, key.method))
+ if (matchedMockDispatches.length === 0) {
+ throw new MockNotMatchedError(`Mock dispatch not matched for method '${key.method}'`)
+ }
+
+ // Match body
+ matchedMockDispatches = matchedMockDispatches.filter(({ body }) => typeof body !== 'undefined' ? matchValue(body, key.body) : true)
+ if (matchedMockDispatches.length === 0) {
+ throw new MockNotMatchedError(`Mock dispatch not matched for body '${key.body}'`)
+ }
+
+ // Match headers
+ matchedMockDispatches = matchedMockDispatches.filter((mockDispatch) => matchHeaders(mockDispatch, key.headers))
+ if (matchedMockDispatches.length === 0) {
+ throw new MockNotMatchedError(`Mock dispatch not matched for headers '${typeof key.headers === 'object' ? JSON.stringify(key.headers) : key.headers}'`)
+ }
+
+ return matchedMockDispatches[0]
+}
+
+function addMockDispatch (mockDispatches, key, data) {
+ const baseData = { timesInvoked: 0, times: 1, persist: false, consumed: false }
+ const replyData = typeof data === 'function' ? { callback: data } : { ...data }
+ const newMockDispatch = { ...baseData, ...key, pending: true, data: { error: null, ...replyData } }
+ mockDispatches.push(newMockDispatch)
+ return newMockDispatch
+}
+
+function deleteMockDispatch (mockDispatches, key) {
+ const index = mockDispatches.findIndex(dispatch => {
+ if (!dispatch.consumed) {
+ return false
+ }
+ return matchKey(dispatch, key)
+ })
+ if (index !== -1) {
+ mockDispatches.splice(index, 1)
+ }
+}
+
+function buildKey (opts) {
+ const { path, method, body, headers, query } = opts
+ return {
+ path,
+ method,
+ body,
+ headers,
+ query
+ }
+}
+
+function generateKeyValues (data) {
+ return Object.entries(data).reduce((keyValuePairs, [key, value]) => [
+ ...keyValuePairs,
+ Buffer.from(`${key}`),
+ Array.isArray(value) ? value.map(x => Buffer.from(`${x}`)) : Buffer.from(`${value}`)
+ ], [])
+}
+
+/**
+ * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Status
+ * @param {number} statusCode
+ */
+function getStatusText (statusCode) {
+ return STATUS_CODES[statusCode] || 'unknown'
+}
+
+async function getResponse (body) {
+ const buffers = []
+ for await (const data of body) {
+ buffers.push(data)
+ }
+ return Buffer.concat(buffers).toString('utf8')
+}
+
+/**
+ * Mock dispatch function used to simulate undici dispatches
+ */
+function mockDispatch (opts, handler) {
+ // Get mock dispatch from built key
+ const key = buildKey(opts)
+ const mockDispatch = getMockDispatch(this[kDispatches], key)
+
+ mockDispatch.timesInvoked++
+
+ // Here's where we resolve a callback if a callback is present for the dispatch data.
+ if (mockDispatch.data.callback) {
+ mockDispatch.data = { ...mockDispatch.data, ...mockDispatch.data.callback(opts) }
+ }
+
+ // Parse mockDispatch data
+ const { data: { statusCode, data, headers, trailers, error }, delay, persist } = mockDispatch
+ const { timesInvoked, times } = mockDispatch
+
+ // If it's used up and not persistent, mark as consumed
+ mockDispatch.consumed = !persist && timesInvoked >= times
+ mockDispatch.pending = timesInvoked < times
+
+ // If specified, trigger dispatch error
+ if (error !== null) {
+ deleteMockDispatch(this[kDispatches], key)
+ handler.onError(error)
+ return true
+ }
+
+ // Handle the request with a delay if necessary
+ if (typeof delay === 'number' && delay > 0) {
+ setTimeout(() => {
+ handleReply(this[kDispatches])
+ }, delay)
+ } else {
+ handleReply(this[kDispatches])
+ }
+
+ function handleReply (mockDispatches, _data = data) {
+ // fetch's HeadersList is a 1D string array
+ const optsHeaders = Array.isArray(opts.headers)
+ ? buildHeadersFromArray(opts.headers)
+ : opts.headers
+ const body = typeof _data === 'function'
+ ? _data({ ...opts, headers: optsHeaders })
+ : _data
+
+ // util.types.isPromise is likely needed for jest.
+ if (isPromise(body)) {
+ // If handleReply is asynchronous, throwing an error
+ // in the callback will reject the promise, rather than
+ // synchronously throw the error, which breaks some tests.
+ // Rather, we wait for the callback to resolve if it is a
+ // promise, and then re-run handleReply with the new body.
+ body.then((newData) => handleReply(mockDispatches, newData))
+ return
+ }
+
+ const responseData = getResponseData(body)
+ const responseHeaders = generateKeyValues(headers)
+ const responseTrailers = generateKeyValues(trailers)
+
+ handler.abort = nop
+ handler.onHeaders(statusCode, responseHeaders, resume, getStatusText(statusCode))
+ handler.onData(Buffer.from(responseData))
+ handler.onComplete(responseTrailers)
+ deleteMockDispatch(mockDispatches, key)
+ }
+
+ function resume () {}
+
+ return true
+}
+
+function buildMockDispatch () {
+ const agent = this[kMockAgent]
+ const origin = this[kOrigin]
+ const originalDispatch = this[kOriginalDispatch]
+
+ return function dispatch (opts, handler) {
+ if (agent.isMockActive) {
+ try {
+ mockDispatch.call(this, opts, handler)
+ } catch (error) {
+ if (error instanceof MockNotMatchedError) {
+ const netConnect = agent[kGetNetConnect]()
+ if (netConnect === false) {
+ throw new MockNotMatchedError(`${error.message}: subsequent request to origin ${origin} was not allowed (net.connect disabled)`)
+ }
+ if (checkNetConnect(netConnect, origin)) {
+ originalDispatch.call(this, opts, handler)
+ } else {
+ throw new MockNotMatchedError(`${error.message}: subsequent request to origin ${origin} was not allowed (net.connect is not enabled for this origin)`)
+ }
+ } else {
+ throw error
+ }
+ }
+ } else {
+ originalDispatch.call(this, opts, handler)
+ }
+ }
+}
+
+function checkNetConnect (netConnect, origin) {
+ const url = new URL(origin)
+ if (netConnect === true) {
+ return true
+ } else if (Array.isArray(netConnect) && netConnect.some((matcher) => matchValue(matcher, url.host))) {
+ return true
+ }
+ return false
+}
+
+function buildMockOptions (opts) {
+ if (opts) {
+ const { agent, ...mockOptions } = opts
+ return mockOptions
+ }
+}
+
+module.exports = {
+ getResponseData,
+ getMockDispatch,
+ addMockDispatch,
+ deleteMockDispatch,
+ buildKey,
+ generateKeyValues,
+ matchValue,
+ getResponse,
+ getStatusText,
+ mockDispatch,
+ buildMockDispatch,
+ checkNetConnect,
+ buildMockOptions,
+ getHeaderByName
+}
diff --git a/lib/mock/pending-interceptors-formatter.js b/lib/mock/pending-interceptors-formatter.js
new file mode 100644
index 0000000..1bc7539
--- /dev/null
+++ b/lib/mock/pending-interceptors-formatter.js
@@ -0,0 +1,40 @@
+'use strict'
+
+const { Transform } = require('stream')
+const { Console } = require('console')
+
+/**
+ * Gets the output of `console.table(…)` as a string.
+ */
+module.exports = class PendingInterceptorsFormatter {
+ constructor ({ disableColors } = {}) {
+ this.transform = new Transform({
+ transform (chunk, _enc, cb) {
+ cb(null, chunk)
+ }
+ })
+
+ this.logger = new Console({
+ stdout: this.transform,
+ inspectOptions: {
+ colors: !disableColors && !process.env.CI
+ }
+ })
+ }
+
+ format (pendingInterceptors) {
+ const withPrettyHeaders = pendingInterceptors.map(
+ ({ method, path, data: { statusCode }, persist, times, timesInvoked, origin }) => ({
+ Method: method,
+ Origin: origin,
+ Path: path,
+ 'Status code': statusCode,
+ Persistent: persist ? '✅' : 'âŒ',
+ Invocations: timesInvoked,
+ Remaining: persist ? Infinity : times - timesInvoked
+ }))
+
+ this.logger.table(withPrettyHeaders)
+ return this.transform.read().toString()
+ }
+}
diff --git a/lib/mock/pluralizer.js b/lib/mock/pluralizer.js
new file mode 100644
index 0000000..47f150b
--- /dev/null
+++ b/lib/mock/pluralizer.js
@@ -0,0 +1,29 @@
+'use strict'
+
+const singulars = {
+ pronoun: 'it',
+ is: 'is',
+ was: 'was',
+ this: 'this'
+}
+
+const plurals = {
+ pronoun: 'they',
+ is: 'are',
+ was: 'were',
+ this: 'these'
+}
+
+module.exports = class Pluralizer {
+ constructor (singular, plural) {
+ this.singular = singular
+ this.plural = plural
+ }
+
+ pluralize (count) {
+ const one = count === 1
+ const keys = one ? singulars : plurals
+ const noun = one ? this.singular : this.plural
+ return { ...keys, count, noun }
+ }
+}
diff --git a/lib/node/fixed-queue.js b/lib/node/fixed-queue.js
new file mode 100644
index 0000000..3572681
--- /dev/null
+++ b/lib/node/fixed-queue.js
@@ -0,0 +1,117 @@
+/* eslint-disable */
+
+'use strict'
+
+// Extracted from node/lib/internal/fixed_queue.js
+
+// Currently optimal queue size, tested on V8 6.0 - 6.6. Must be power of two.
+const kSize = 2048;
+const kMask = kSize - 1;
+
+// The FixedQueue is implemented as a singly-linked list of fixed-size
+// circular buffers. It looks something like this:
+//
+// head tail
+// | |
+// v v
+// +-----------+ <-----\ +-----------+ <------\ +-----------+
+// | [null] | \----- | next | \------- | next |
+// +-----------+ +-----------+ +-----------+
+// | item | <-- bottom | item | <-- bottom | [empty] |
+// | item | | item | | [empty] |
+// | item | | item | | [empty] |
+// | item | | item | | [empty] |
+// | item | | item | bottom --> | item |
+// | item | | item | | item |
+// | ... | | ... | | ... |
+// | item | | item | | item |
+// | item | | item | | item |
+// | [empty] | <-- top | item | | item |
+// | [empty] | | item | | item |
+// | [empty] | | [empty] | <-- top top --> | [empty] |
+// +-----------+ +-----------+ +-----------+
+//
+// Or, if there is only one circular buffer, it looks something
+// like either of these:
+//
+// head tail head tail
+// | | | |
+// v v v v
+// +-----------+ +-----------+
+// | [null] | | [null] |
+// +-----------+ +-----------+
+// | [empty] | | item |
+// | [empty] | | item |
+// | item | <-- bottom top --> | [empty] |
+// | item | | [empty] |
+// | [empty] | <-- top bottom --> | item |
+// | [empty] | | item |
+// +-----------+ +-----------+
+//
+// Adding a value means moving `top` forward by one, removing means
+// moving `bottom` forward by one. After reaching the end, the queue
+// wraps around.
+//
+// When `top === bottom` the current queue is empty and when
+// `top + 1 === bottom` it's full. This wastes a single space of storage
+// but allows much quicker checks.
+
+class FixedCircularBuffer {
+ constructor() {
+ this.bottom = 0;
+ this.top = 0;
+ this.list = new Array(kSize);
+ this.next = null;
+ }
+
+ isEmpty() {
+ return this.top === this.bottom;
+ }
+
+ isFull() {
+ return ((this.top + 1) & kMask) === this.bottom;
+ }
+
+ push(data) {
+ this.list[this.top] = data;
+ this.top = (this.top + 1) & kMask;
+ }
+
+ shift() {
+ const nextItem = this.list[this.bottom];
+ if (nextItem === undefined)
+ return null;
+ this.list[this.bottom] = undefined;
+ this.bottom = (this.bottom + 1) & kMask;
+ return nextItem;
+ }
+}
+
+module.exports = class FixedQueue {
+ constructor() {
+ this.head = this.tail = new FixedCircularBuffer();
+ }
+
+ isEmpty() {
+ return this.head.isEmpty();
+ }
+
+ push(data) {
+ if (this.head.isFull()) {
+ // Head is full: Creates a new queue, sets the old queue's `.next` to it,
+ // and sets it as the new main queue.
+ this.head = this.head.next = new FixedCircularBuffer();
+ }
+ this.head.push(data);
+ }
+
+ shift() {
+ const tail = this.tail;
+ const next = tail.shift();
+ if (tail.isEmpty() && tail.next !== null) {
+ // If there is another queue, it forms the new tail.
+ this.tail = tail.next;
+ }
+ return next;
+ }
+};
diff --git a/lib/pool-base.js b/lib/pool-base.js
new file mode 100644
index 0000000..2a909ee
--- /dev/null
+++ b/lib/pool-base.js
@@ -0,0 +1,194 @@
+'use strict'
+
+const DispatcherBase = require('./dispatcher-base')
+const FixedQueue = require('./node/fixed-queue')
+const { kConnected, kSize, kRunning, kPending, kQueued, kBusy, kFree, kUrl, kClose, kDestroy, kDispatch } = require('./core/symbols')
+const PoolStats = require('./pool-stats')
+
+const kClients = Symbol('clients')
+const kNeedDrain = Symbol('needDrain')
+const kQueue = Symbol('queue')
+const kClosedResolve = Symbol('closed resolve')
+const kOnDrain = Symbol('onDrain')
+const kOnConnect = Symbol('onConnect')
+const kOnDisconnect = Symbol('onDisconnect')
+const kOnConnectionError = Symbol('onConnectionError')
+const kGetDispatcher = Symbol('get dispatcher')
+const kAddClient = Symbol('add client')
+const kRemoveClient = Symbol('remove client')
+const kStats = Symbol('stats')
+
+class PoolBase extends DispatcherBase {
+ constructor () {
+ super()
+
+ this[kQueue] = new FixedQueue()
+ this[kClients] = []
+ this[kQueued] = 0
+
+ const pool = this
+
+ this[kOnDrain] = function onDrain (origin, targets) {
+ const queue = pool[kQueue]
+
+ let needDrain = false
+
+ while (!needDrain) {
+ const item = queue.shift()
+ if (!item) {
+ break
+ }
+ pool[kQueued]--
+ needDrain = !this.dispatch(item.opts, item.handler)
+ }
+
+ this[kNeedDrain] = needDrain
+
+ if (!this[kNeedDrain] && pool[kNeedDrain]) {
+ pool[kNeedDrain] = false
+ pool.emit('drain', origin, [pool, ...targets])
+ }
+
+ if (pool[kClosedResolve] && queue.isEmpty()) {
+ Promise
+ .all(pool[kClients].map(c => c.close()))
+ .then(pool[kClosedResolve])
+ }
+ }
+
+ this[kOnConnect] = (origin, targets) => {
+ pool.emit('connect', origin, [pool, ...targets])
+ }
+
+ this[kOnDisconnect] = (origin, targets, err) => {
+ pool.emit('disconnect', origin, [pool, ...targets], err)
+ }
+
+ this[kOnConnectionError] = (origin, targets, err) => {
+ pool.emit('connectionError', origin, [pool, ...targets], err)
+ }
+
+ this[kStats] = new PoolStats(this)
+ }
+
+ get [kBusy] () {
+ return this[kNeedDrain]
+ }
+
+ get [kConnected] () {
+ return this[kClients].filter(client => client[kConnected]).length
+ }
+
+ get [kFree] () {
+ return this[kClients].filter(client => client[kConnected] && !client[kNeedDrain]).length
+ }
+
+ get [kPending] () {
+ let ret = this[kQueued]
+ for (const { [kPending]: pending } of this[kClients]) {
+ ret += pending
+ }
+ return ret
+ }
+
+ get [kRunning] () {
+ let ret = 0
+ for (const { [kRunning]: running } of this[kClients]) {
+ ret += running
+ }
+ return ret
+ }
+
+ get [kSize] () {
+ let ret = this[kQueued]
+ for (const { [kSize]: size } of this[kClients]) {
+ ret += size
+ }
+ return ret
+ }
+
+ get stats () {
+ return this[kStats]
+ }
+
+ async [kClose] () {
+ if (this[kQueue].isEmpty()) {
+ return Promise.all(this[kClients].map(c => c.close()))
+ } else {
+ return new Promise((resolve) => {
+ this[kClosedResolve] = resolve
+ })
+ }
+ }
+
+ async [kDestroy] (err) {
+ while (true) {
+ const item = this[kQueue].shift()
+ if (!item) {
+ break
+ }
+ item.handler.onError(err)
+ }
+
+ return Promise.all(this[kClients].map(c => c.destroy(err)))
+ }
+
+ [kDispatch] (opts, handler) {
+ const dispatcher = this[kGetDispatcher]()
+
+ if (!dispatcher) {
+ this[kNeedDrain] = true
+ this[kQueue].push({ opts, handler })
+ this[kQueued]++
+ } else if (!dispatcher.dispatch(opts, handler)) {
+ dispatcher[kNeedDrain] = true
+ this[kNeedDrain] = !this[kGetDispatcher]()
+ }
+
+ return !this[kNeedDrain]
+ }
+
+ [kAddClient] (client) {
+ client
+ .on('drain', this[kOnDrain])
+ .on('connect', this[kOnConnect])
+ .on('disconnect', this[kOnDisconnect])
+ .on('connectionError', this[kOnConnectionError])
+
+ this[kClients].push(client)
+
+ if (this[kNeedDrain]) {
+ process.nextTick(() => {
+ if (this[kNeedDrain]) {
+ this[kOnDrain](client[kUrl], [this, client])
+ }
+ })
+ }
+
+ return this
+ }
+
+ [kRemoveClient] (client) {
+ client.close(() => {
+ const idx = this[kClients].indexOf(client)
+ if (idx !== -1) {
+ this[kClients].splice(idx, 1)
+ }
+ })
+
+ this[kNeedDrain] = this[kClients].some(dispatcher => (
+ !dispatcher[kNeedDrain] &&
+ dispatcher.closed !== true &&
+ dispatcher.destroyed !== true
+ ))
+ }
+}
+
+module.exports = {
+ PoolBase,
+ kClients,
+ kNeedDrain,
+ kAddClient,
+ kRemoveClient,
+ kGetDispatcher
+}
diff --git a/lib/pool-stats.js b/lib/pool-stats.js
new file mode 100644
index 0000000..b4af8ae
--- /dev/null
+++ b/lib/pool-stats.js
@@ -0,0 +1,34 @@
+const { kFree, kConnected, kPending, kQueued, kRunning, kSize } = require('./core/symbols')
+const kPool = Symbol('pool')
+
+class PoolStats {
+ constructor (pool) {
+ this[kPool] = pool
+ }
+
+ get connected () {
+ return this[kPool][kConnected]
+ }
+
+ get free () {
+ return this[kPool][kFree]
+ }
+
+ get pending () {
+ return this[kPool][kPending]
+ }
+
+ get queued () {
+ return this[kPool][kQueued]
+ }
+
+ get running () {
+ return this[kPool][kRunning]
+ }
+
+ get size () {
+ return this[kPool][kSize]
+ }
+}
+
+module.exports = PoolStats
diff --git a/lib/pool.js b/lib/pool.js
new file mode 100644
index 0000000..e3cd339
--- /dev/null
+++ b/lib/pool.js
@@ -0,0 +1,94 @@
+'use strict'
+
+const {
+ PoolBase,
+ kClients,
+ kNeedDrain,
+ kAddClient,
+ kGetDispatcher
+} = require('./pool-base')
+const Client = require('./client')
+const {
+ InvalidArgumentError
+} = require('./core/errors')
+const util = require('./core/util')
+const { kUrl, kInterceptors } = require('./core/symbols')
+const buildConnector = require('./core/connect')
+
+const kOptions = Symbol('options')
+const kConnections = Symbol('connections')
+const kFactory = Symbol('factory')
+
+function defaultFactory (origin, opts) {
+ return new Client(origin, opts)
+}
+
+class Pool extends PoolBase {
+ constructor (origin, {
+ connections,
+ factory = defaultFactory,
+ connect,
+ connectTimeout,
+ tls,
+ maxCachedSessions,
+ socketPath,
+ autoSelectFamily,
+ autoSelectFamilyAttemptTimeout,
+ allowH2,
+ ...options
+ } = {}) {
+ super()
+
+ if (connections != null && (!Number.isFinite(connections) || connections < 0)) {
+ throw new InvalidArgumentError('invalid connections')
+ }
+
+ if (typeof factory !== 'function') {
+ throw new InvalidArgumentError('factory must be a function.')
+ }
+
+ if (connect != null && typeof connect !== 'function' && typeof connect !== 'object') {
+ throw new InvalidArgumentError('connect must be a function or an object')
+ }
+
+ if (typeof connect !== 'function') {
+ connect = buildConnector({
+ ...tls,
+ maxCachedSessions,
+ allowH2,
+ socketPath,
+ timeout: connectTimeout,
+ ...(util.nodeHasAutoSelectFamily && autoSelectFamily ? { autoSelectFamily, autoSelectFamilyAttemptTimeout } : undefined),
+ ...connect
+ })
+ }
+
+ this[kInterceptors] = options.interceptors && options.interceptors.Pool && Array.isArray(options.interceptors.Pool)
+ ? options.interceptors.Pool
+ : []
+ this[kConnections] = connections || null
+ this[kUrl] = util.parseOrigin(origin)
+ this[kOptions] = { ...util.deepClone(options), connect, allowH2 }
+ this[kOptions].interceptors = options.interceptors
+ ? { ...options.interceptors }
+ : undefined
+ this[kFactory] = factory
+ }
+
+ [kGetDispatcher] () {
+ let dispatcher = this[kClients].find(dispatcher => !dispatcher[kNeedDrain])
+
+ if (dispatcher) {
+ return dispatcher
+ }
+
+ if (!this[kConnections] || this[kClients].length < this[kConnections]) {
+ dispatcher = this[kFactory](this[kUrl], this[kOptions])
+ this[kAddClient](dispatcher)
+ }
+
+ return dispatcher
+ }
+}
+
+module.exports = Pool
diff --git a/lib/proxy-agent.js b/lib/proxy-agent.js
new file mode 100644
index 0000000..e3c0f6f
--- /dev/null
+++ b/lib/proxy-agent.js
@@ -0,0 +1,189 @@
+'use strict'
+
+const { kProxy, kClose, kDestroy, kInterceptors } = require('./core/symbols')
+const { URL } = require('url')
+const Agent = require('./agent')
+const Pool = require('./pool')
+const DispatcherBase = require('./dispatcher-base')
+const { InvalidArgumentError, RequestAbortedError } = require('./core/errors')
+const buildConnector = require('./core/connect')
+
+const kAgent = Symbol('proxy agent')
+const kClient = Symbol('proxy client')
+const kProxyHeaders = Symbol('proxy headers')
+const kRequestTls = Symbol('request tls settings')
+const kProxyTls = Symbol('proxy tls settings')
+const kConnectEndpoint = Symbol('connect endpoint function')
+
+function defaultProtocolPort (protocol) {
+ return protocol === 'https:' ? 443 : 80
+}
+
+function buildProxyOptions (opts) {
+ if (typeof opts === 'string') {
+ opts = { uri: opts }
+ }
+
+ if (!opts || !opts.uri) {
+ throw new InvalidArgumentError('Proxy opts.uri is mandatory')
+ }
+
+ return {
+ uri: opts.uri,
+ protocol: opts.protocol || 'https'
+ }
+}
+
+function defaultFactory (origin, opts) {
+ return new Pool(origin, opts)
+}
+
+class ProxyAgent extends DispatcherBase {
+ constructor (opts) {
+ super(opts)
+ this[kProxy] = buildProxyOptions(opts)
+ this[kAgent] = new Agent(opts)
+ this[kInterceptors] = opts.interceptors && opts.interceptors.ProxyAgent && Array.isArray(opts.interceptors.ProxyAgent)
+ ? opts.interceptors.ProxyAgent
+ : []
+
+ if (typeof opts === 'string') {
+ opts = { uri: opts }
+ }
+
+ if (!opts || !opts.uri) {
+ throw new InvalidArgumentError('Proxy opts.uri is mandatory')
+ }
+
+ const { clientFactory = defaultFactory } = opts
+
+ if (typeof clientFactory !== 'function') {
+ throw new InvalidArgumentError('Proxy opts.clientFactory must be a function.')
+ }
+
+ this[kRequestTls] = opts.requestTls
+ this[kProxyTls] = opts.proxyTls
+ this[kProxyHeaders] = opts.headers || {}
+
+ const resolvedUrl = new URL(opts.uri)
+ const { origin, port, host, username, password } = resolvedUrl
+
+ if (opts.auth && opts.token) {
+ throw new InvalidArgumentError('opts.auth cannot be used in combination with opts.token')
+ } else if (opts.auth) {
+ /* @deprecated in favour of opts.token */
+ this[kProxyHeaders]['proxy-authorization'] = `Basic ${opts.auth}`
+ } else if (opts.token) {
+ this[kProxyHeaders]['proxy-authorization'] = opts.token
+ } else if (username && password) {
+ this[kProxyHeaders]['proxy-authorization'] = `Basic ${Buffer.from(`${decodeURIComponent(username)}:${decodeURIComponent(password)}`).toString('base64')}`
+ }
+
+ const connect = buildConnector({ ...opts.proxyTls })
+ this[kConnectEndpoint] = buildConnector({ ...opts.requestTls })
+ this[kClient] = clientFactory(resolvedUrl, { connect })
+ this[kAgent] = new Agent({
+ ...opts,
+ connect: async (opts, callback) => {
+ let requestedHost = opts.host
+ if (!opts.port) {
+ requestedHost += `:${defaultProtocolPort(opts.protocol)}`
+ }
+ try {
+ const { socket, statusCode } = await this[kClient].connect({
+ origin,
+ port,
+ path: requestedHost,
+ signal: opts.signal,
+ headers: {
+ ...this[kProxyHeaders],
+ host
+ }
+ })
+ if (statusCode !== 200) {
+ socket.on('error', () => {}).destroy()
+ callback(new RequestAbortedError(`Proxy response (${statusCode}) !== 200 when HTTP Tunneling`))
+ }
+ if (opts.protocol !== 'https:') {
+ callback(null, socket)
+ return
+ }
+ let servername
+ if (this[kRequestTls]) {
+ servername = this[kRequestTls].servername
+ } else {
+ servername = opts.servername
+ }
+ this[kConnectEndpoint]({ ...opts, servername, httpSocket: socket }, callback)
+ } catch (err) {
+ callback(err)
+ }
+ }
+ })
+ }
+
+ dispatch (opts, handler) {
+ const { host } = new URL(opts.origin)
+ const headers = buildHeaders(opts.headers)
+ throwIfProxyAuthIsSent(headers)
+ return this[kAgent].dispatch(
+ {
+ ...opts,
+ headers: {
+ ...headers,
+ host
+ }
+ },
+ handler
+ )
+ }
+
+ async [kClose] () {
+ await this[kAgent].close()
+ await this[kClient].close()
+ }
+
+ async [kDestroy] () {
+ await this[kAgent].destroy()
+ await this[kClient].destroy()
+ }
+}
+
+/**
+ * @param {string[] | Record<string, string>} headers
+ * @returns {Record<string, string>}
+ */
+function buildHeaders (headers) {
+ // When using undici.fetch, the headers list is stored
+ // as an array.
+ if (Array.isArray(headers)) {
+ /** @type {Record<string, string>} */
+ const headersPair = {}
+
+ for (let i = 0; i < headers.length; i += 2) {
+ headersPair[headers[i]] = headers[i + 1]
+ }
+
+ return headersPair
+ }
+
+ return headers
+}
+
+/**
+ * @param {Record<string, string>} headers
+ *
+ * Previous versions of ProxyAgent suggests the Proxy-Authorization in request headers
+ * Nevertheless, it was changed and to avoid a security vulnerability by end users
+ * this check was created.
+ * It should be removed in the next major version for performance reasons
+ */
+function throwIfProxyAuthIsSent (headers) {
+ const existProxyAuth = headers && Object.keys(headers)
+ .find((key) => key.toLowerCase() === 'proxy-authorization')
+ if (existProxyAuth) {
+ throw new InvalidArgumentError('Proxy-Authorization should be sent in ProxyAgent constructor')
+ }
+}
+
+module.exports = ProxyAgent
diff --git a/lib/timers.js b/lib/timers.js
new file mode 100644
index 0000000..5782217
--- /dev/null
+++ b/lib/timers.js
@@ -0,0 +1,97 @@
+'use strict'
+
+let fastNow = Date.now()
+let fastNowTimeout
+
+const fastTimers = []
+
+function onTimeout () {
+ fastNow = Date.now()
+
+ let len = fastTimers.length
+ let idx = 0
+ while (idx < len) {
+ const timer = fastTimers[idx]
+
+ if (timer.state === 0) {
+ timer.state = fastNow + timer.delay
+ } else if (timer.state > 0 && fastNow >= timer.state) {
+ timer.state = -1
+ timer.callback(timer.opaque)
+ }
+
+ if (timer.state === -1) {
+ timer.state = -2
+ if (idx !== len - 1) {
+ fastTimers[idx] = fastTimers.pop()
+ } else {
+ fastTimers.pop()
+ }
+ len -= 1
+ } else {
+ idx += 1
+ }
+ }
+
+ if (fastTimers.length > 0) {
+ refreshTimeout()
+ }
+}
+
+function refreshTimeout () {
+ if (fastNowTimeout && fastNowTimeout.refresh) {
+ fastNowTimeout.refresh()
+ } else {
+ clearTimeout(fastNowTimeout)
+ fastNowTimeout = setTimeout(onTimeout, 1e3)
+ if (fastNowTimeout.unref) {
+ fastNowTimeout.unref()
+ }
+ }
+}
+
+class Timeout {
+ constructor (callback, delay, opaque) {
+ this.callback = callback
+ this.delay = delay
+ this.opaque = opaque
+
+ // -2 not in timer list
+ // -1 in timer list but inactive
+ // 0 in timer list waiting for time
+ // > 0 in timer list waiting for time to expire
+ this.state = -2
+
+ this.refresh()
+ }
+
+ refresh () {
+ if (this.state === -2) {
+ fastTimers.push(this)
+ if (!fastNowTimeout || fastTimers.length === 1) {
+ refreshTimeout()
+ }
+ }
+
+ this.state = 0
+ }
+
+ clear () {
+ this.state = -1
+ }
+}
+
+module.exports = {
+ setTimeout (callback, delay, opaque) {
+ return delay < 1e3
+ ? setTimeout(callback, delay, opaque)
+ : new Timeout(callback, delay, opaque)
+ },
+ clearTimeout (timeout) {
+ if (timeout instanceof Timeout) {
+ timeout.clear()
+ } else {
+ clearTimeout(timeout)
+ }
+ }
+}
diff --git a/lib/websocket/connection.js b/lib/websocket/connection.js
new file mode 100644
index 0000000..e0fa697
--- /dev/null
+++ b/lib/websocket/connection.js
@@ -0,0 +1,291 @@
+'use strict'
+
+const diagnosticsChannel = require('diagnostics_channel')
+const { uid, states } = require('./constants')
+const {
+ kReadyState,
+ kSentClose,
+ kByteParser,
+ kReceivedClose
+} = require('./symbols')
+const { fireEvent, failWebsocketConnection } = require('./util')
+const { CloseEvent } = require('./events')
+const { makeRequest } = require('../fetch/request')
+const { fetching } = require('../fetch/index')
+const { Headers } = require('../fetch/headers')
+const { getGlobalDispatcher } = require('../global')
+const { kHeadersList } = require('../core/symbols')
+
+const channels = {}
+channels.open = diagnosticsChannel.channel('undici:websocket:open')
+channels.close = diagnosticsChannel.channel('undici:websocket:close')
+channels.socketError = diagnosticsChannel.channel('undici:websocket:socket_error')
+
+/** @type {import('crypto')} */
+let crypto
+try {
+ crypto = require('crypto')
+} catch {
+
+}
+
+/**
+ * @see https://websockets.spec.whatwg.org/#concept-websocket-establish
+ * @param {URL} url
+ * @param {string|string[]} protocols
+ * @param {import('./websocket').WebSocket} ws
+ * @param {(response: any) => void} onEstablish
+ * @param {Partial<import('../../types/websocket').WebSocketInit>} options
+ */
+function establishWebSocketConnection (url, protocols, ws, onEstablish, options) {
+ // 1. Let requestURL be a copy of url, with its scheme set to "http", if url’s
+ // scheme is "ws", and to "https" otherwise.
+ const requestURL = url
+
+ requestURL.protocol = url.protocol === 'ws:' ? 'http:' : 'https:'
+
+ // 2. Let request be a new request, whose URL is requestURL, client is client,
+ // service-workers mode is "none", referrer is "no-referrer", mode is
+ // "websocket", credentials mode is "include", cache mode is "no-store" ,
+ // and redirect mode is "error".
+ const request = makeRequest({
+ urlList: [requestURL],
+ serviceWorkers: 'none',
+ referrer: 'no-referrer',
+ mode: 'websocket',
+ credentials: 'include',
+ cache: 'no-store',
+ redirect: 'error'
+ })
+
+ // Note: undici extension, allow setting custom headers.
+ if (options.headers) {
+ const headersList = new Headers(options.headers)[kHeadersList]
+
+ request.headersList = headersList
+ }
+
+ // 3. Append (`Upgrade`, `websocket`) to request’s header list.
+ // 4. Append (`Connection`, `Upgrade`) to request’s header list.
+ // Note: both of these are handled by undici currently.
+ // https://github.com/nodejs/undici/blob/68c269c4144c446f3f1220951338daef4a6b5ec4/lib/client.js#L1397
+
+ // 5. Let keyValue be a nonce consisting of a randomly selected
+ // 16-byte value that has been forgiving-base64-encoded and
+ // isomorphic encoded.
+ const keyValue = crypto.randomBytes(16).toString('base64')
+
+ // 6. Append (`Sec-WebSocket-Key`, keyValue) to request’s
+ // header list.
+ request.headersList.append('sec-websocket-key', keyValue)
+
+ // 7. Append (`Sec-WebSocket-Version`, `13`) to request’s
+ // header list.
+ request.headersList.append('sec-websocket-version', '13')
+
+ // 8. For each protocol in protocols, combine
+ // (`Sec-WebSocket-Protocol`, protocol) in request’s header
+ // list.
+ for (const protocol of protocols) {
+ request.headersList.append('sec-websocket-protocol', protocol)
+ }
+
+ // 9. Let permessageDeflate be a user-agent defined
+ // "permessage-deflate" extension header value.
+ // https://github.com/mozilla/gecko-dev/blob/ce78234f5e653a5d3916813ff990f053510227bc/netwerk/protocol/websocket/WebSocketChannel.cpp#L2673
+ // TODO: enable once permessage-deflate is supported
+ const permessageDeflate = '' // 'permessage-deflate; 15'
+
+ // 10. Append (`Sec-WebSocket-Extensions`, permessageDeflate) to
+ // request’s header list.
+ // request.headersList.append('sec-websocket-extensions', permessageDeflate)
+
+ // 11. Fetch request with useParallelQueue set to true, and
+ // processResponse given response being these steps:
+ const controller = fetching({
+ request,
+ useParallelQueue: true,
+ dispatcher: options.dispatcher ?? getGlobalDispatcher(),
+ processResponse (response) {
+ // 1. If response is a network error or its status is not 101,
+ // fail the WebSocket connection.
+ if (response.type === 'error' || response.status !== 101) {
+ failWebsocketConnection(ws, 'Received network error or non-101 status code.')
+ return
+ }
+
+ // 2. If protocols is not the empty list and extracting header
+ // list values given `Sec-WebSocket-Protocol` and response’s
+ // header list results in null, failure, or the empty byte
+ // sequence, then fail the WebSocket connection.
+ if (protocols.length !== 0 && !response.headersList.get('Sec-WebSocket-Protocol')) {
+ failWebsocketConnection(ws, 'Server did not respond with sent protocols.')
+ return
+ }
+
+ // 3. Follow the requirements stated step 2 to step 6, inclusive,
+ // of the last set of steps in section 4.1 of The WebSocket
+ // Protocol to validate response. This either results in fail
+ // the WebSocket connection or the WebSocket connection is
+ // established.
+
+ // 2. If the response lacks an |Upgrade| header field or the |Upgrade|
+ // header field contains a value that is not an ASCII case-
+ // insensitive match for the value "websocket", the client MUST
+ // _Fail the WebSocket Connection_.
+ if (response.headersList.get('Upgrade')?.toLowerCase() !== 'websocket') {
+ failWebsocketConnection(ws, 'Server did not set Upgrade header to "websocket".')
+ return
+ }
+
+ // 3. If the response lacks a |Connection| header field or the
+ // |Connection| header field doesn't contain a token that is an
+ // ASCII case-insensitive match for the value "Upgrade", the client
+ // MUST _Fail the WebSocket Connection_.
+ if (response.headersList.get('Connection')?.toLowerCase() !== 'upgrade') {
+ failWebsocketConnection(ws, 'Server did not set Connection header to "upgrade".')
+ return
+ }
+
+ // 4. If the response lacks a |Sec-WebSocket-Accept| header field or
+ // the |Sec-WebSocket-Accept| contains a value other than the
+ // base64-encoded SHA-1 of the concatenation of the |Sec-WebSocket-
+ // Key| (as a string, not base64-decoded) with the string "258EAFA5-
+ // E914-47DA-95CA-C5AB0DC85B11" but ignoring any leading and
+ // trailing whitespace, the client MUST _Fail the WebSocket
+ // Connection_.
+ const secWSAccept = response.headersList.get('Sec-WebSocket-Accept')
+ const digest = crypto.createHash('sha1').update(keyValue + uid).digest('base64')
+ if (secWSAccept !== digest) {
+ failWebsocketConnection(ws, 'Incorrect hash received in Sec-WebSocket-Accept header.')
+ return
+ }
+
+ // 5. If the response includes a |Sec-WebSocket-Extensions| header
+ // field and this header field indicates the use of an extension
+ // that was not present in the client's handshake (the server has
+ // indicated an extension not requested by the client), the client
+ // MUST _Fail the WebSocket Connection_. (The parsing of this
+ // header field to determine which extensions are requested is
+ // discussed in Section 9.1.)
+ const secExtension = response.headersList.get('Sec-WebSocket-Extensions')
+
+ if (secExtension !== null && secExtension !== permessageDeflate) {
+ failWebsocketConnection(ws, 'Received different permessage-deflate than the one set.')
+ return
+ }
+
+ // 6. If the response includes a |Sec-WebSocket-Protocol| header field
+ // and this header field indicates the use of a subprotocol that was
+ // not present in the client's handshake (the server has indicated a
+ // subprotocol not requested by the client), the client MUST _Fail
+ // the WebSocket Connection_.
+ const secProtocol = response.headersList.get('Sec-WebSocket-Protocol')
+
+ if (secProtocol !== null && secProtocol !== request.headersList.get('Sec-WebSocket-Protocol')) {
+ failWebsocketConnection(ws, 'Protocol was not set in the opening handshake.')
+ return
+ }
+
+ response.socket.on('data', onSocketData)
+ response.socket.on('close', onSocketClose)
+ response.socket.on('error', onSocketError)
+
+ if (channels.open.hasSubscribers) {
+ channels.open.publish({
+ address: response.socket.address(),
+ protocol: secProtocol,
+ extensions: secExtension
+ })
+ }
+
+ onEstablish(response)
+ }
+ })
+
+ return controller
+}
+
+/**
+ * @param {Buffer} chunk
+ */
+function onSocketData (chunk) {
+ if (!this.ws[kByteParser].write(chunk)) {
+ this.pause()
+ }
+}
+
+/**
+ * @see https://websockets.spec.whatwg.org/#feedback-from-the-protocol
+ * @see https://datatracker.ietf.org/doc/html/rfc6455#section-7.1.4
+ */
+function onSocketClose () {
+ const { ws } = this
+
+ // If the TCP connection was closed after the
+ // WebSocket closing handshake was completed, the WebSocket connection
+ // is said to have been closed _cleanly_.
+ const wasClean = ws[kSentClose] && ws[kReceivedClose]
+
+ let code = 1005
+ let reason = ''
+
+ const result = ws[kByteParser].closingInfo
+
+ if (result) {
+ code = result.code ?? 1005
+ reason = result.reason
+ } else if (!ws[kSentClose]) {
+ // If _The WebSocket
+ // Connection is Closed_ and no Close control frame was received by the
+ // endpoint (such as could occur if the underlying transport connection
+ // is lost), _The WebSocket Connection Close Code_ is considered to be
+ // 1006.
+ code = 1006
+ }
+
+ // 1. Change the ready state to CLOSED (3).
+ ws[kReadyState] = states.CLOSED
+
+ // 2. If the user agent was required to fail the WebSocket
+ // connection, or if the WebSocket connection was closed
+ // after being flagged as full, fire an event named error
+ // at the WebSocket object.
+ // TODO
+
+ // 3. Fire an event named close at the WebSocket object,
+ // using CloseEvent, with the wasClean attribute
+ // initialized to true if the connection closed cleanly
+ // and false otherwise, the code attribute initialized to
+ // the WebSocket connection close code, and the reason
+ // attribute initialized to the result of applying UTF-8
+ // decode without BOM to the WebSocket connection close
+ // reason.
+ fireEvent('close', ws, CloseEvent, {
+ wasClean, code, reason
+ })
+
+ if (channels.close.hasSubscribers) {
+ channels.close.publish({
+ websocket: ws,
+ code,
+ reason
+ })
+ }
+}
+
+function onSocketError (error) {
+ const { ws } = this
+
+ ws[kReadyState] = states.CLOSING
+
+ if (channels.socketError.hasSubscribers) {
+ channels.socketError.publish(error)
+ }
+
+ this.destroy()
+}
+
+module.exports = {
+ establishWebSocketConnection
+}
diff --git a/lib/websocket/constants.js b/lib/websocket/constants.js
new file mode 100644
index 0000000..406b8e3
--- /dev/null
+++ b/lib/websocket/constants.js
@@ -0,0 +1,51 @@
+'use strict'
+
+// This is a Globally Unique Identifier unique used
+// to validate that the endpoint accepts websocket
+// connections.
+// See https://www.rfc-editor.org/rfc/rfc6455.html#section-1.3
+const uid = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
+
+/** @type {PropertyDescriptor} */
+const staticPropertyDescriptors = {
+ enumerable: true,
+ writable: false,
+ configurable: false
+}
+
+const states = {
+ CONNECTING: 0,
+ OPEN: 1,
+ CLOSING: 2,
+ CLOSED: 3
+}
+
+const opcodes = {
+ CONTINUATION: 0x0,
+ TEXT: 0x1,
+ BINARY: 0x2,
+ CLOSE: 0x8,
+ PING: 0x9,
+ PONG: 0xA
+}
+
+const maxUnsigned16Bit = 2 ** 16 - 1 // 65535
+
+const parserStates = {
+ INFO: 0,
+ PAYLOADLENGTH_16: 2,
+ PAYLOADLENGTH_64: 3,
+ READ_DATA: 4
+}
+
+const emptyBuffer = Buffer.allocUnsafe(0)
+
+module.exports = {
+ uid,
+ staticPropertyDescriptors,
+ states,
+ opcodes,
+ maxUnsigned16Bit,
+ parserStates,
+ emptyBuffer
+}
diff --git a/lib/websocket/events.js b/lib/websocket/events.js
new file mode 100644
index 0000000..621a226
--- /dev/null
+++ b/lib/websocket/events.js
@@ -0,0 +1,303 @@
+'use strict'
+
+const { webidl } = require('../fetch/webidl')
+const { kEnumerableProperty } = require('../core/util')
+const { MessagePort } = require('worker_threads')
+
+/**
+ * @see https://html.spec.whatwg.org/multipage/comms.html#messageevent
+ */
+class MessageEvent extends Event {
+ #eventInit
+
+ constructor (type, eventInitDict = {}) {
+ webidl.argumentLengthCheck(arguments, 1, { header: 'MessageEvent constructor' })
+
+ type = webidl.converters.DOMString(type)
+ eventInitDict = webidl.converters.MessageEventInit(eventInitDict)
+
+ super(type, eventInitDict)
+
+ this.#eventInit = eventInitDict
+ }
+
+ get data () {
+ webidl.brandCheck(this, MessageEvent)
+
+ return this.#eventInit.data
+ }
+
+ get origin () {
+ webidl.brandCheck(this, MessageEvent)
+
+ return this.#eventInit.origin
+ }
+
+ get lastEventId () {
+ webidl.brandCheck(this, MessageEvent)
+
+ return this.#eventInit.lastEventId
+ }
+
+ get source () {
+ webidl.brandCheck(this, MessageEvent)
+
+ return this.#eventInit.source
+ }
+
+ get ports () {
+ webidl.brandCheck(this, MessageEvent)
+
+ if (!Object.isFrozen(this.#eventInit.ports)) {
+ Object.freeze(this.#eventInit.ports)
+ }
+
+ return this.#eventInit.ports
+ }
+
+ initMessageEvent (
+ type,
+ bubbles = false,
+ cancelable = false,
+ data = null,
+ origin = '',
+ lastEventId = '',
+ source = null,
+ ports = []
+ ) {
+ webidl.brandCheck(this, MessageEvent)
+
+ webidl.argumentLengthCheck(arguments, 1, { header: 'MessageEvent.initMessageEvent' })
+
+ return new MessageEvent(type, {
+ bubbles, cancelable, data, origin, lastEventId, source, ports
+ })
+ }
+}
+
+/**
+ * @see https://websockets.spec.whatwg.org/#the-closeevent-interface
+ */
+class CloseEvent extends Event {
+ #eventInit
+
+ constructor (type, eventInitDict = {}) {
+ webidl.argumentLengthCheck(arguments, 1, { header: 'CloseEvent constructor' })
+
+ type = webidl.converters.DOMString(type)
+ eventInitDict = webidl.converters.CloseEventInit(eventInitDict)
+
+ super(type, eventInitDict)
+
+ this.#eventInit = eventInitDict
+ }
+
+ get wasClean () {
+ webidl.brandCheck(this, CloseEvent)
+
+ return this.#eventInit.wasClean
+ }
+
+ get code () {
+ webidl.brandCheck(this, CloseEvent)
+
+ return this.#eventInit.code
+ }
+
+ get reason () {
+ webidl.brandCheck(this, CloseEvent)
+
+ return this.#eventInit.reason
+ }
+}
+
+// https://html.spec.whatwg.org/multipage/webappapis.html#the-errorevent-interface
+class ErrorEvent extends Event {
+ #eventInit
+
+ constructor (type, eventInitDict) {
+ webidl.argumentLengthCheck(arguments, 1, { header: 'ErrorEvent constructor' })
+
+ super(type, eventInitDict)
+
+ type = webidl.converters.DOMString(type)
+ eventInitDict = webidl.converters.ErrorEventInit(eventInitDict ?? {})
+
+ this.#eventInit = eventInitDict
+ }
+
+ get message () {
+ webidl.brandCheck(this, ErrorEvent)
+
+ return this.#eventInit.message
+ }
+
+ get filename () {
+ webidl.brandCheck(this, ErrorEvent)
+
+ return this.#eventInit.filename
+ }
+
+ get lineno () {
+ webidl.brandCheck(this, ErrorEvent)
+
+ return this.#eventInit.lineno
+ }
+
+ get colno () {
+ webidl.brandCheck(this, ErrorEvent)
+
+ return this.#eventInit.colno
+ }
+
+ get error () {
+ webidl.brandCheck(this, ErrorEvent)
+
+ return this.#eventInit.error
+ }
+}
+
+Object.defineProperties(MessageEvent.prototype, {
+ [Symbol.toStringTag]: {
+ value: 'MessageEvent',
+ configurable: true
+ },
+ data: kEnumerableProperty,
+ origin: kEnumerableProperty,
+ lastEventId: kEnumerableProperty,
+ source: kEnumerableProperty,
+ ports: kEnumerableProperty,
+ initMessageEvent: kEnumerableProperty
+})
+
+Object.defineProperties(CloseEvent.prototype, {
+ [Symbol.toStringTag]: {
+ value: 'CloseEvent',
+ configurable: true
+ },
+ reason: kEnumerableProperty,
+ code: kEnumerableProperty,
+ wasClean: kEnumerableProperty
+})
+
+Object.defineProperties(ErrorEvent.prototype, {
+ [Symbol.toStringTag]: {
+ value: 'ErrorEvent',
+ configurable: true
+ },
+ message: kEnumerableProperty,
+ filename: kEnumerableProperty,
+ lineno: kEnumerableProperty,
+ colno: kEnumerableProperty,
+ error: kEnumerableProperty
+})
+
+webidl.converters.MessagePort = webidl.interfaceConverter(MessagePort)
+
+webidl.converters['sequence<MessagePort>'] = webidl.sequenceConverter(
+ webidl.converters.MessagePort
+)
+
+const eventInit = [
+ {
+ key: 'bubbles',
+ converter: webidl.converters.boolean,
+ defaultValue: false
+ },
+ {
+ key: 'cancelable',
+ converter: webidl.converters.boolean,
+ defaultValue: false
+ },
+ {
+ key: 'composed',
+ converter: webidl.converters.boolean,
+ defaultValue: false
+ }
+]
+
+webidl.converters.MessageEventInit = webidl.dictionaryConverter([
+ ...eventInit,
+ {
+ key: 'data',
+ converter: webidl.converters.any,
+ defaultValue: null
+ },
+ {
+ key: 'origin',
+ converter: webidl.converters.USVString,
+ defaultValue: ''
+ },
+ {
+ key: 'lastEventId',
+ converter: webidl.converters.DOMString,
+ defaultValue: ''
+ },
+ {
+ key: 'source',
+ // Node doesn't implement WindowProxy or ServiceWorker, so the only
+ // valid value for source is a MessagePort.
+ converter: webidl.nullableConverter(webidl.converters.MessagePort),
+ defaultValue: null
+ },
+ {
+ key: 'ports',
+ converter: webidl.converters['sequence<MessagePort>'],
+ get defaultValue () {
+ return []
+ }
+ }
+])
+
+webidl.converters.CloseEventInit = webidl.dictionaryConverter([
+ ...eventInit,
+ {
+ key: 'wasClean',
+ converter: webidl.converters.boolean,
+ defaultValue: false
+ },
+ {
+ key: 'code',
+ converter: webidl.converters['unsigned short'],
+ defaultValue: 0
+ },
+ {
+ key: 'reason',
+ converter: webidl.converters.USVString,
+ defaultValue: ''
+ }
+])
+
+webidl.converters.ErrorEventInit = webidl.dictionaryConverter([
+ ...eventInit,
+ {
+ key: 'message',
+ converter: webidl.converters.DOMString,
+ defaultValue: ''
+ },
+ {
+ key: 'filename',
+ converter: webidl.converters.USVString,
+ defaultValue: ''
+ },
+ {
+ key: 'lineno',
+ converter: webidl.converters['unsigned long'],
+ defaultValue: 0
+ },
+ {
+ key: 'colno',
+ converter: webidl.converters['unsigned long'],
+ defaultValue: 0
+ },
+ {
+ key: 'error',
+ converter: webidl.converters.any
+ }
+])
+
+module.exports = {
+ MessageEvent,
+ CloseEvent,
+ ErrorEvent
+}
diff --git a/lib/websocket/frame.js b/lib/websocket/frame.js
new file mode 100644
index 0000000..d867ad1
--- /dev/null
+++ b/lib/websocket/frame.js
@@ -0,0 +1,73 @@
+'use strict'
+
+const { maxUnsigned16Bit } = require('./constants')
+
+/** @type {import('crypto')} */
+let crypto
+try {
+ crypto = require('crypto')
+} catch {
+
+}
+
+class WebsocketFrameSend {
+ /**
+ * @param {Buffer|undefined} data
+ */
+ constructor (data) {
+ this.frameData = data
+ this.maskKey = crypto.randomBytes(4)
+ }
+
+ createFrame (opcode) {
+ const bodyLength = this.frameData?.byteLength ?? 0
+
+ /** @type {number} */
+ let payloadLength = bodyLength // 0-125
+ let offset = 6
+
+ if (bodyLength > maxUnsigned16Bit) {
+ offset += 8 // payload length is next 8 bytes
+ payloadLength = 127
+ } else if (bodyLength > 125) {
+ offset += 2 // payload length is next 2 bytes
+ payloadLength = 126
+ }
+
+ const buffer = Buffer.allocUnsafe(bodyLength + offset)
+
+ // Clear first 2 bytes, everything else is overwritten
+ buffer[0] = buffer[1] = 0
+ buffer[0] |= 0x80 // FIN
+ buffer[0] = (buffer[0] & 0xF0) + opcode // opcode
+
+ /*! ws. MIT License. Einar Otto Stangvik <einaros@gmail.com> */
+ buffer[offset - 4] = this.maskKey[0]
+ buffer[offset - 3] = this.maskKey[1]
+ buffer[offset - 2] = this.maskKey[2]
+ buffer[offset - 1] = this.maskKey[3]
+
+ buffer[1] = payloadLength
+
+ if (payloadLength === 126) {
+ buffer.writeUInt16BE(bodyLength, 2)
+ } else if (payloadLength === 127) {
+ // Clear extended payload length
+ buffer[2] = buffer[3] = 0
+ buffer.writeUIntBE(bodyLength, 4, 6)
+ }
+
+ buffer[1] |= 0x80 // MASK
+
+ // mask body
+ for (let i = 0; i < bodyLength; i++) {
+ buffer[offset + i] = this.frameData[i] ^ this.maskKey[i % 4]
+ }
+
+ return buffer
+ }
+}
+
+module.exports = {
+ WebsocketFrameSend
+}
diff --git a/lib/websocket/receiver.js b/lib/websocket/receiver.js
new file mode 100644
index 0000000..bdd2031
--- /dev/null
+++ b/lib/websocket/receiver.js
@@ -0,0 +1,344 @@
+'use strict'
+
+const { Writable } = require('stream')
+const diagnosticsChannel = require('diagnostics_channel')
+const { parserStates, opcodes, states, emptyBuffer } = require('./constants')
+const { kReadyState, kSentClose, kResponse, kReceivedClose } = require('./symbols')
+const { isValidStatusCode, failWebsocketConnection, websocketMessageReceived } = require('./util')
+const { WebsocketFrameSend } = require('./frame')
+
+// This code was influenced by ws released under the MIT license.
+// Copyright (c) 2011 Einar Otto Stangvik <einaros@gmail.com>
+// Copyright (c) 2013 Arnout Kazemier and contributors
+// Copyright (c) 2016 Luigi Pinca and contributors
+
+const channels = {}
+channels.ping = diagnosticsChannel.channel('undici:websocket:ping')
+channels.pong = diagnosticsChannel.channel('undici:websocket:pong')
+
+class ByteParser extends Writable {
+ #buffers = []
+ #byteOffset = 0
+
+ #state = parserStates.INFO
+
+ #info = {}
+ #fragments = []
+
+ constructor (ws) {
+ super()
+
+ this.ws = ws
+ }
+
+ /**
+ * @param {Buffer} chunk
+ * @param {() => void} callback
+ */
+ _write (chunk, _, callback) {
+ this.#buffers.push(chunk)
+ this.#byteOffset += chunk.length
+
+ this.run(callback)
+ }
+
+ /**
+ * Runs whenever a new chunk is received.
+ * Callback is called whenever there are no more chunks buffering,
+ * or not enough bytes are buffered to parse.
+ */
+ run (callback) {
+ while (true) {
+ if (this.#state === parserStates.INFO) {
+ // If there aren't enough bytes to parse the payload length, etc.
+ if (this.#byteOffset < 2) {
+ return callback()
+ }
+
+ const buffer = this.consume(2)
+
+ this.#info.fin = (buffer[0] & 0x80) !== 0
+ this.#info.opcode = buffer[0] & 0x0F
+
+ // If we receive a fragmented message, we use the type of the first
+ // frame to parse the full message as binary/text, when it's terminated
+ this.#info.originalOpcode ??= this.#info.opcode
+
+ this.#info.fragmented = !this.#info.fin && this.#info.opcode !== opcodes.CONTINUATION
+
+ if (this.#info.fragmented && this.#info.opcode !== opcodes.BINARY && this.#info.opcode !== opcodes.TEXT) {
+ // Only text and binary frames can be fragmented
+ failWebsocketConnection(this.ws, 'Invalid frame type was fragmented.')
+ return
+ }
+
+ const payloadLength = buffer[1] & 0x7F
+
+ if (payloadLength <= 125) {
+ this.#info.payloadLength = payloadLength
+ this.#state = parserStates.READ_DATA
+ } else if (payloadLength === 126) {
+ this.#state = parserStates.PAYLOADLENGTH_16
+ } else if (payloadLength === 127) {
+ this.#state = parserStates.PAYLOADLENGTH_64
+ }
+
+ if (this.#info.fragmented && payloadLength > 125) {
+ // A fragmented frame can't be fragmented itself
+ failWebsocketConnection(this.ws, 'Fragmented frame exceeded 125 bytes.')
+ return
+ } else if (
+ (this.#info.opcode === opcodes.PING ||
+ this.#info.opcode === opcodes.PONG ||
+ this.#info.opcode === opcodes.CLOSE) &&
+ payloadLength > 125
+ ) {
+ // Control frames can have a payload length of 125 bytes MAX
+ failWebsocketConnection(this.ws, 'Payload length for control frame exceeded 125 bytes.')
+ return
+ } else if (this.#info.opcode === opcodes.CLOSE) {
+ if (payloadLength === 1) {
+ failWebsocketConnection(this.ws, 'Received close frame with a 1-byte body.')
+ return
+ }
+
+ const body = this.consume(payloadLength)
+
+ this.#info.closeInfo = this.parseCloseBody(false, body)
+
+ if (!this.ws[kSentClose]) {
+ // If an endpoint receives a Close frame and did not previously send a
+ // Close frame, the endpoint MUST send a Close frame in response. (When
+ // sending a Close frame in response, the endpoint typically echos the
+ // status code it received.)
+ const body = Buffer.allocUnsafe(2)
+ body.writeUInt16BE(this.#info.closeInfo.code, 0)
+ const closeFrame = new WebsocketFrameSend(body)
+
+ this.ws[kResponse].socket.write(
+ closeFrame.createFrame(opcodes.CLOSE),
+ (err) => {
+ if (!err) {
+ this.ws[kSentClose] = true
+ }
+ }
+ )
+ }
+
+ // Upon either sending or receiving a Close control frame, it is said
+ // that _The WebSocket Closing Handshake is Started_ and that the
+ // WebSocket connection is in the CLOSING state.
+ this.ws[kReadyState] = states.CLOSING
+ this.ws[kReceivedClose] = true
+
+ this.end()
+
+ return
+ } else if (this.#info.opcode === opcodes.PING) {
+ // Upon receipt of a Ping frame, an endpoint MUST send a Pong frame in
+ // response, unless it already received a Close frame.
+ // A Pong frame sent in response to a Ping frame must have identical
+ // "Application data"
+
+ const body = this.consume(payloadLength)
+
+ if (!this.ws[kReceivedClose]) {
+ const frame = new WebsocketFrameSend(body)
+
+ this.ws[kResponse].socket.write(frame.createFrame(opcodes.PONG))
+
+ if (channels.ping.hasSubscribers) {
+ channels.ping.publish({
+ payload: body
+ })
+ }
+ }
+
+ this.#state = parserStates.INFO
+
+ if (this.#byteOffset > 0) {
+ continue
+ } else {
+ callback()
+ return
+ }
+ } else if (this.#info.opcode === opcodes.PONG) {
+ // A Pong frame MAY be sent unsolicited. This serves as a
+ // unidirectional heartbeat. A response to an unsolicited Pong frame is
+ // not expected.
+
+ const body = this.consume(payloadLength)
+
+ if (channels.pong.hasSubscribers) {
+ channels.pong.publish({
+ payload: body
+ })
+ }
+
+ if (this.#byteOffset > 0) {
+ continue
+ } else {
+ callback()
+ return
+ }
+ }
+ } else if (this.#state === parserStates.PAYLOADLENGTH_16) {
+ if (this.#byteOffset < 2) {
+ return callback()
+ }
+
+ const buffer = this.consume(2)
+
+ this.#info.payloadLength = buffer.readUInt16BE(0)
+ this.#state = parserStates.READ_DATA
+ } else if (this.#state === parserStates.PAYLOADLENGTH_64) {
+ if (this.#byteOffset < 8) {
+ return callback()
+ }
+
+ const buffer = this.consume(8)
+ const upper = buffer.readUInt32BE(0)
+
+ // 2^31 is the maxinimum bytes an arraybuffer can contain
+ // on 32-bit systems. Although, on 64-bit systems, this is
+ // 2^53-1 bytes.
+ // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Invalid_array_length
+ // https://source.chromium.org/chromium/chromium/src/+/main:v8/src/common/globals.h;drc=1946212ac0100668f14eb9e2843bdd846e510a1e;bpv=1;bpt=1;l=1275
+ // https://source.chromium.org/chromium/chromium/src/+/main:v8/src/objects/js-array-buffer.h;l=34;drc=1946212ac0100668f14eb9e2843bdd846e510a1e
+ if (upper > 2 ** 31 - 1) {
+ failWebsocketConnection(this.ws, 'Received payload length > 2^31 bytes.')
+ return
+ }
+
+ const lower = buffer.readUInt32BE(4)
+
+ this.#info.payloadLength = (upper << 8) + lower
+ this.#state = parserStates.READ_DATA
+ } else if (this.#state === parserStates.READ_DATA) {
+ if (this.#byteOffset < this.#info.payloadLength) {
+ // If there is still more data in this chunk that needs to be read
+ return callback()
+ } else if (this.#byteOffset >= this.#info.payloadLength) {
+ // If the server sent multiple frames in a single chunk
+
+ const body = this.consume(this.#info.payloadLength)
+
+ this.#fragments.push(body)
+
+ // If the frame is unfragmented, or a fragmented frame was terminated,
+ // a message was received
+ if (!this.#info.fragmented || (this.#info.fin && this.#info.opcode === opcodes.CONTINUATION)) {
+ const fullMessage = Buffer.concat(this.#fragments)
+
+ websocketMessageReceived(this.ws, this.#info.originalOpcode, fullMessage)
+
+ this.#info = {}
+ this.#fragments.length = 0
+ }
+
+ this.#state = parserStates.INFO
+ }
+ }
+
+ if (this.#byteOffset > 0) {
+ continue
+ } else {
+ callback()
+ break
+ }
+ }
+ }
+
+ /**
+ * Take n bytes from the buffered Buffers
+ * @param {number} n
+ * @returns {Buffer|null}
+ */
+ consume (n) {
+ if (n > this.#byteOffset) {
+ return null
+ } else if (n === 0) {
+ return emptyBuffer
+ }
+
+ if (this.#buffers[0].length === n) {
+ this.#byteOffset -= this.#buffers[0].length
+ return this.#buffers.shift()
+ }
+
+ const buffer = Buffer.allocUnsafe(n)
+ let offset = 0
+
+ while (offset !== n) {
+ const next = this.#buffers[0]
+ const { length } = next
+
+ if (length + offset === n) {
+ buffer.set(this.#buffers.shift(), offset)
+ break
+ } else if (length + offset > n) {
+ buffer.set(next.subarray(0, n - offset), offset)
+ this.#buffers[0] = next.subarray(n - offset)
+ break
+ } else {
+ buffer.set(this.#buffers.shift(), offset)
+ offset += next.length
+ }
+ }
+
+ this.#byteOffset -= n
+
+ return buffer
+ }
+
+ parseCloseBody (onlyCode, data) {
+ // https://datatracker.ietf.org/doc/html/rfc6455#section-7.1.5
+ /** @type {number|undefined} */
+ let code
+
+ if (data.length >= 2) {
+ // _The WebSocket Connection Close Code_ is
+ // defined as the status code (Section 7.4) contained in the first Close
+ // control frame received by the application
+ code = data.readUInt16BE(0)
+ }
+
+ if (onlyCode) {
+ if (!isValidStatusCode(code)) {
+ return null
+ }
+
+ return { code }
+ }
+
+ // https://datatracker.ietf.org/doc/html/rfc6455#section-7.1.6
+ /** @type {Buffer} */
+ let reason = data.subarray(2)
+
+ // Remove BOM
+ if (reason[0] === 0xEF && reason[1] === 0xBB && reason[2] === 0xBF) {
+ reason = reason.subarray(3)
+ }
+
+ if (code !== undefined && !isValidStatusCode(code)) {
+ return null
+ }
+
+ try {
+ // TODO: optimize this
+ reason = new TextDecoder('utf-8', { fatal: true }).decode(reason)
+ } catch {
+ return null
+ }
+
+ return { code, reason }
+ }
+
+ get closingInfo () {
+ return this.#info.closeInfo
+ }
+}
+
+module.exports = {
+ ByteParser
+}
diff --git a/lib/websocket/symbols.js b/lib/websocket/symbols.js
new file mode 100644
index 0000000..11d03e3
--- /dev/null
+++ b/lib/websocket/symbols.js
@@ -0,0 +1,12 @@
+'use strict'
+
+module.exports = {
+ kWebSocketURL: Symbol('url'),
+ kReadyState: Symbol('ready state'),
+ kController: Symbol('controller'),
+ kResponse: Symbol('response'),
+ kBinaryType: Symbol('binary type'),
+ kSentClose: Symbol('sent close'),
+ kReceivedClose: Symbol('received close'),
+ kByteParser: Symbol('byte parser')
+}
diff --git a/lib/websocket/util.js b/lib/websocket/util.js
new file mode 100644
index 0000000..6c59b2c
--- /dev/null
+++ b/lib/websocket/util.js
@@ -0,0 +1,200 @@
+'use strict'
+
+const { kReadyState, kController, kResponse, kBinaryType, kWebSocketURL } = require('./symbols')
+const { states, opcodes } = require('./constants')
+const { MessageEvent, ErrorEvent } = require('./events')
+
+/* globals Blob */
+
+/**
+ * @param {import('./websocket').WebSocket} ws
+ */
+function isEstablished (ws) {
+ // If the server's response is validated as provided for above, it is
+ // said that _The WebSocket Connection is Established_ and that the
+ // WebSocket Connection is in the OPEN state.
+ return ws[kReadyState] === states.OPEN
+}
+
+/**
+ * @param {import('./websocket').WebSocket} ws
+ */
+function isClosing (ws) {
+ // Upon either sending or receiving a Close control frame, it is said
+ // that _The WebSocket Closing Handshake is Started_ and that the
+ // WebSocket connection is in the CLOSING state.
+ return ws[kReadyState] === states.CLOSING
+}
+
+/**
+ * @param {import('./websocket').WebSocket} ws
+ */
+function isClosed (ws) {
+ return ws[kReadyState] === states.CLOSED
+}
+
+/**
+ * @see https://dom.spec.whatwg.org/#concept-event-fire
+ * @param {string} e
+ * @param {EventTarget} target
+ * @param {EventInit | undefined} eventInitDict
+ */
+function fireEvent (e, target, eventConstructor = Event, eventInitDict) {
+ // 1. If eventConstructor is not given, then let eventConstructor be Event.
+
+ // 2. Let event be the result of creating an event given eventConstructor,
+ // in the relevant realm of target.
+ // 3. Initialize event’s type attribute to e.
+ const event = new eventConstructor(e, eventInitDict) // eslint-disable-line new-cap
+
+ // 4. Initialize any other IDL attributes of event as described in the
+ // invocation of this algorithm.
+
+ // 5. Return the result of dispatching event at target, with legacy target
+ // override flag set if set.
+ target.dispatchEvent(event)
+}
+
+/**
+ * @see https://websockets.spec.whatwg.org/#feedback-from-the-protocol
+ * @param {import('./websocket').WebSocket} ws
+ * @param {number} type Opcode
+ * @param {Buffer} data application data
+ */
+function websocketMessageReceived (ws, type, data) {
+ // 1. If ready state is not OPEN (1), then return.
+ if (ws[kReadyState] !== states.OPEN) {
+ return
+ }
+
+ // 2. Let dataForEvent be determined by switching on type and binary type:
+ let dataForEvent
+
+ if (type === opcodes.TEXT) {
+ // -> type indicates that the data is Text
+ // a new DOMString containing data
+ try {
+ dataForEvent = new TextDecoder('utf-8', { fatal: true }).decode(data)
+ } catch {
+ failWebsocketConnection(ws, 'Received invalid UTF-8 in text frame.')
+ return
+ }
+ } else if (type === opcodes.BINARY) {
+ if (ws[kBinaryType] === 'blob') {
+ // -> type indicates that the data is Binary and binary type is "blob"
+ // a new Blob object, created in the relevant Realm of the WebSocket
+ // object, that represents data as its raw data
+ dataForEvent = new Blob([data])
+ } else {
+ // -> type indicates that the data is Binary and binary type is "arraybuffer"
+ // a new ArrayBuffer object, created in the relevant Realm of the
+ // WebSocket object, whose contents are data
+ dataForEvent = new Uint8Array(data).buffer
+ }
+ }
+
+ // 3. Fire an event named message at the WebSocket object, using MessageEvent,
+ // with the origin attribute initialized to the serialization of the WebSocket
+ // object’s url's origin, and the data attribute initialized to dataForEvent.
+ fireEvent('message', ws, MessageEvent, {
+ origin: ws[kWebSocketURL].origin,
+ data: dataForEvent
+ })
+}
+
+/**
+ * @see https://datatracker.ietf.org/doc/html/rfc6455
+ * @see https://datatracker.ietf.org/doc/html/rfc2616
+ * @see https://bugs.chromium.org/p/chromium/issues/detail?id=398407
+ * @param {string} protocol
+ */
+function isValidSubprotocol (protocol) {
+ // If present, this value indicates one
+ // or more comma-separated subprotocol the client wishes to speak,
+ // ordered by preference. The elements that comprise this value
+ // MUST be non-empty strings with characters in the range U+0021 to
+ // U+007E not including separator characters as defined in
+ // [RFC2616] and MUST all be unique strings.
+ if (protocol.length === 0) {
+ return false
+ }
+
+ for (const char of protocol) {
+ const code = char.charCodeAt(0)
+
+ if (
+ code < 0x21 ||
+ code > 0x7E ||
+ char === '(' ||
+ char === ')' ||
+ char === '<' ||
+ char === '>' ||
+ char === '@' ||
+ char === ',' ||
+ char === ';' ||
+ char === ':' ||
+ char === '\\' ||
+ char === '"' ||
+ char === '/' ||
+ char === '[' ||
+ char === ']' ||
+ char === '?' ||
+ char === '=' ||
+ char === '{' ||
+ char === '}' ||
+ code === 32 || // SP
+ code === 9 // HT
+ ) {
+ return false
+ }
+ }
+
+ return true
+}
+
+/**
+ * @see https://datatracker.ietf.org/doc/html/rfc6455#section-7-4
+ * @param {number} code
+ */
+function isValidStatusCode (code) {
+ if (code >= 1000 && code < 1015) {
+ return (
+ code !== 1004 && // reserved
+ code !== 1005 && // "MUST NOT be set as a status code"
+ code !== 1006 // "MUST NOT be set as a status code"
+ )
+ }
+
+ return code >= 3000 && code <= 4999
+}
+
+/**
+ * @param {import('./websocket').WebSocket} ws
+ * @param {string|undefined} reason
+ */
+function failWebsocketConnection (ws, reason) {
+ const { [kController]: controller, [kResponse]: response } = ws
+
+ controller.abort()
+
+ if (response?.socket && !response.socket.destroyed) {
+ response.socket.destroy()
+ }
+
+ if (reason) {
+ fireEvent('error', ws, ErrorEvent, {
+ error: new Error(reason)
+ })
+ }
+}
+
+module.exports = {
+ isEstablished,
+ isClosing,
+ isClosed,
+ fireEvent,
+ isValidSubprotocol,
+ isValidStatusCode,
+ failWebsocketConnection,
+ websocketMessageReceived
+}
diff --git a/lib/websocket/websocket.js b/lib/websocket/websocket.js
new file mode 100644
index 0000000..e4aa58f
--- /dev/null
+++ b/lib/websocket/websocket.js
@@ -0,0 +1,641 @@
+'use strict'
+
+const { webidl } = require('../fetch/webidl')
+const { DOMException } = require('../fetch/constants')
+const { URLSerializer } = require('../fetch/dataURL')
+const { getGlobalOrigin } = require('../fetch/global')
+const { staticPropertyDescriptors, states, opcodes, emptyBuffer } = require('./constants')
+const {
+ kWebSocketURL,
+ kReadyState,
+ kController,
+ kBinaryType,
+ kResponse,
+ kSentClose,
+ kByteParser
+} = require('./symbols')
+const { isEstablished, isClosing, isValidSubprotocol, failWebsocketConnection, fireEvent } = require('./util')
+const { establishWebSocketConnection } = require('./connection')
+const { WebsocketFrameSend } = require('./frame')
+const { ByteParser } = require('./receiver')
+const { kEnumerableProperty, isBlobLike } = require('../core/util')
+const { getGlobalDispatcher } = require('../global')
+const { types } = require('util')
+
+let experimentalWarned = false
+
+// https://websockets.spec.whatwg.org/#interface-definition
+class WebSocket extends EventTarget {
+ #events = {
+ open: null,
+ error: null,
+ close: null,
+ message: null
+ }
+
+ #bufferedAmount = 0
+ #protocol = ''
+ #extensions = ''
+
+ /**
+ * @param {string} url
+ * @param {string|string[]} protocols
+ */
+ constructor (url, protocols = []) {
+ super()
+
+ webidl.argumentLengthCheck(arguments, 1, { header: 'WebSocket constructor' })
+
+ if (!experimentalWarned) {
+ experimentalWarned = true
+ process.emitWarning('WebSockets are experimental, expect them to change at any time.', {
+ code: 'UNDICI-WS'
+ })
+ }
+
+ const options = webidl.converters['DOMString or sequence<DOMString> or WebSocketInit'](protocols)
+
+ url = webidl.converters.USVString(url)
+ protocols = options.protocols
+
+ // 1. Let baseURL be this's relevant settings object's API base URL.
+ const baseURL = getGlobalOrigin()
+
+ // 1. Let urlRecord be the result of applying the URL parser to url with baseURL.
+ let urlRecord
+
+ try {
+ urlRecord = new URL(url, baseURL)
+ } catch (e) {
+ // 3. If urlRecord is failure, then throw a "SyntaxError" DOMException.
+ throw new DOMException(e, 'SyntaxError')
+ }
+
+ // 4. If urlRecord’s scheme is "http", then set urlRecord’s scheme to "ws".
+ if (urlRecord.protocol === 'http:') {
+ urlRecord.protocol = 'ws:'
+ } else if (urlRecord.protocol === 'https:') {
+ // 5. Otherwise, if urlRecord’s scheme is "https", set urlRecord’s scheme to "wss".
+ urlRecord.protocol = 'wss:'
+ }
+
+ // 6. If urlRecord’s scheme is not "ws" or "wss", then throw a "SyntaxError" DOMException.
+ if (urlRecord.protocol !== 'ws:' && urlRecord.protocol !== 'wss:') {
+ throw new DOMException(
+ `Expected a ws: or wss: protocol, got ${urlRecord.protocol}`,
+ 'SyntaxError'
+ )
+ }
+
+ // 7. If urlRecord’s fragment is non-null, then throw a "SyntaxError"
+ // DOMException.
+ if (urlRecord.hash || urlRecord.href.endsWith('#')) {
+ throw new DOMException('Got fragment', 'SyntaxError')
+ }
+
+ // 8. If protocols is a string, set protocols to a sequence consisting
+ // of just that string.
+ if (typeof protocols === 'string') {
+ protocols = [protocols]
+ }
+
+ // 9. If any of the values in protocols occur more than once or otherwise
+ // fail to match the requirements for elements that comprise the value
+ // of `Sec-WebSocket-Protocol` fields as defined by The WebSocket
+ // protocol, then throw a "SyntaxError" DOMException.
+ if (protocols.length !== new Set(protocols.map(p => p.toLowerCase())).size) {
+ throw new DOMException('Invalid Sec-WebSocket-Protocol value', 'SyntaxError')
+ }
+
+ if (protocols.length > 0 && !protocols.every(p => isValidSubprotocol(p))) {
+ throw new DOMException('Invalid Sec-WebSocket-Protocol value', 'SyntaxError')
+ }
+
+ // 10. Set this's url to urlRecord.
+ this[kWebSocketURL] = new URL(urlRecord.href)
+
+ // 11. Let client be this's relevant settings object.
+
+ // 12. Run this step in parallel:
+
+ // 1. Establish a WebSocket connection given urlRecord, protocols,
+ // and client.
+ this[kController] = establishWebSocketConnection(
+ urlRecord,
+ protocols,
+ this,
+ (response) => this.#onConnectionEstablished(response),
+ options
+ )
+
+ // Each WebSocket object has an associated ready state, which is a
+ // number representing the state of the connection. Initially it must
+ // be CONNECTING (0).
+ this[kReadyState] = WebSocket.CONNECTING
+
+ // The extensions attribute must initially return the empty string.
+
+ // The protocol attribute must initially return the empty string.
+
+ // Each WebSocket object has an associated binary type, which is a
+ // BinaryType. Initially it must be "blob".
+ this[kBinaryType] = 'blob'
+ }
+
+ /**
+ * @see https://websockets.spec.whatwg.org/#dom-websocket-close
+ * @param {number|undefined} code
+ * @param {string|undefined} reason
+ */
+ close (code = undefined, reason = undefined) {
+ webidl.brandCheck(this, WebSocket)
+
+ if (code !== undefined) {
+ code = webidl.converters['unsigned short'](code, { clamp: true })
+ }
+
+ if (reason !== undefined) {
+ reason = webidl.converters.USVString(reason)
+ }
+
+ // 1. If code is present, but is neither an integer equal to 1000 nor an
+ // integer in the range 3000 to 4999, inclusive, throw an
+ // "InvalidAccessError" DOMException.
+ if (code !== undefined) {
+ if (code !== 1000 && (code < 3000 || code > 4999)) {
+ throw new DOMException('invalid code', 'InvalidAccessError')
+ }
+ }
+
+ let reasonByteLength = 0
+
+ // 2. If reason is present, then run these substeps:
+ if (reason !== undefined) {
+ // 1. Let reasonBytes be the result of encoding reason.
+ // 2. If reasonBytes is longer than 123 bytes, then throw a
+ // "SyntaxError" DOMException.
+ reasonByteLength = Buffer.byteLength(reason)
+
+ if (reasonByteLength > 123) {
+ throw new DOMException(
+ `Reason must be less than 123 bytes; received ${reasonByteLength}`,
+ 'SyntaxError'
+ )
+ }
+ }
+
+ // 3. Run the first matching steps from the following list:
+ if (this[kReadyState] === WebSocket.CLOSING || this[kReadyState] === WebSocket.CLOSED) {
+ // If this's ready state is CLOSING (2) or CLOSED (3)
+ // Do nothing.
+ } else if (!isEstablished(this)) {
+ // If the WebSocket connection is not yet established
+ // Fail the WebSocket connection and set this's ready state
+ // to CLOSING (2).
+ failWebsocketConnection(this, 'Connection was closed before it was established.')
+ this[kReadyState] = WebSocket.CLOSING
+ } else if (!isClosing(this)) {
+ // If the WebSocket closing handshake has not yet been started
+ // Start the WebSocket closing handshake and set this's ready
+ // state to CLOSING (2).
+ // - If neither code nor reason is present, the WebSocket Close
+ // message must not have a body.
+ // - If code is present, then the status code to use in the
+ // WebSocket Close message must be the integer given by code.
+ // - If reason is also present, then reasonBytes must be
+ // provided in the Close message after the status code.
+
+ const frame = new WebsocketFrameSend()
+
+ // If neither code nor reason is present, the WebSocket Close
+ // message must not have a body.
+
+ // If code is present, then the status code to use in the
+ // WebSocket Close message must be the integer given by code.
+ if (code !== undefined && reason === undefined) {
+ frame.frameData = Buffer.allocUnsafe(2)
+ frame.frameData.writeUInt16BE(code, 0)
+ } else if (code !== undefined && reason !== undefined) {
+ // If reason is also present, then reasonBytes must be
+ // provided in the Close message after the status code.
+ frame.frameData = Buffer.allocUnsafe(2 + reasonByteLength)
+ frame.frameData.writeUInt16BE(code, 0)
+ // the body MAY contain UTF-8-encoded data with value /reason/
+ frame.frameData.write(reason, 2, 'utf-8')
+ } else {
+ frame.frameData = emptyBuffer
+ }
+
+ /** @type {import('stream').Duplex} */
+ const socket = this[kResponse].socket
+
+ socket.write(frame.createFrame(opcodes.CLOSE), (err) => {
+ if (!err) {
+ this[kSentClose] = true
+ }
+ })
+
+ // Upon either sending or receiving a Close control frame, it is said
+ // that _The WebSocket Closing Handshake is Started_ and that the
+ // WebSocket connection is in the CLOSING state.
+ this[kReadyState] = states.CLOSING
+ } else {
+ // Otherwise
+ // Set this's ready state to CLOSING (2).
+ this[kReadyState] = WebSocket.CLOSING
+ }
+ }
+
+ /**
+ * @see https://websockets.spec.whatwg.org/#dom-websocket-send
+ * @param {NodeJS.TypedArray|ArrayBuffer|Blob|string} data
+ */
+ send (data) {
+ webidl.brandCheck(this, WebSocket)
+
+ webidl.argumentLengthCheck(arguments, 1, { header: 'WebSocket.send' })
+
+ data = webidl.converters.WebSocketSendData(data)
+
+ // 1. If this's ready state is CONNECTING, then throw an
+ // "InvalidStateError" DOMException.
+ if (this[kReadyState] === WebSocket.CONNECTING) {
+ throw new DOMException('Sent before connected.', 'InvalidStateError')
+ }
+
+ // 2. Run the appropriate set of steps from the following list:
+ // https://datatracker.ietf.org/doc/html/rfc6455#section-6.1
+ // https://datatracker.ietf.org/doc/html/rfc6455#section-5.2
+
+ if (!isEstablished(this) || isClosing(this)) {
+ return
+ }
+
+ /** @type {import('stream').Duplex} */
+ const socket = this[kResponse].socket
+
+ // If data is a string
+ if (typeof data === 'string') {
+ // If the WebSocket connection is established and the WebSocket
+ // closing handshake has not yet started, then the user agent
+ // must send a WebSocket Message comprised of the data argument
+ // using a text frame opcode; if the data cannot be sent, e.g.
+ // because it would need to be buffered but the buffer is full,
+ // the user agent must flag the WebSocket as full and then close
+ // the WebSocket connection. Any invocation of this method with a
+ // string argument that does not throw an exception must increase
+ // the bufferedAmount attribute by the number of bytes needed to
+ // express the argument as UTF-8.
+
+ const value = Buffer.from(data)
+ const frame = new WebsocketFrameSend(value)
+ const buffer = frame.createFrame(opcodes.TEXT)
+
+ this.#bufferedAmount += value.byteLength
+ socket.write(buffer, () => {
+ this.#bufferedAmount -= value.byteLength
+ })
+ } else if (types.isArrayBuffer(data)) {
+ // If the WebSocket connection is established, and the WebSocket
+ // closing handshake has not yet started, then the user agent must
+ // send a WebSocket Message comprised of data using a binary frame
+ // opcode; if the data cannot be sent, e.g. because it would need
+ // to be buffered but the buffer is full, the user agent must flag
+ // the WebSocket as full and then close the WebSocket connection.
+ // The data to be sent is the data stored in the buffer described
+ // by the ArrayBuffer object. Any invocation of this method with an
+ // ArrayBuffer argument that does not throw an exception must
+ // increase the bufferedAmount attribute by the length of the
+ // ArrayBuffer in bytes.
+
+ const value = Buffer.from(data)
+ const frame = new WebsocketFrameSend(value)
+ const buffer = frame.createFrame(opcodes.BINARY)
+
+ this.#bufferedAmount += value.byteLength
+ socket.write(buffer, () => {
+ this.#bufferedAmount -= value.byteLength
+ })
+ } else if (ArrayBuffer.isView(data)) {
+ // If the WebSocket connection is established, and the WebSocket
+ // closing handshake has not yet started, then the user agent must
+ // send a WebSocket Message comprised of data using a binary frame
+ // opcode; if the data cannot be sent, e.g. because it would need to
+ // be buffered but the buffer is full, the user agent must flag the
+ // WebSocket as full and then close the WebSocket connection. The
+ // data to be sent is the data stored in the section of the buffer
+ // described by the ArrayBuffer object that data references. Any
+ // invocation of this method with this kind of argument that does
+ // not throw an exception must increase the bufferedAmount attribute
+ // by the length of data’s buffer in bytes.
+
+ const ab = Buffer.from(data, data.byteOffset, data.byteLength)
+
+ const frame = new WebsocketFrameSend(ab)
+ const buffer = frame.createFrame(opcodes.BINARY)
+
+ this.#bufferedAmount += ab.byteLength
+ socket.write(buffer, () => {
+ this.#bufferedAmount -= ab.byteLength
+ })
+ } else if (isBlobLike(data)) {
+ // If the WebSocket connection is established, and the WebSocket
+ // closing handshake has not yet started, then the user agent must
+ // send a WebSocket Message comprised of data using a binary frame
+ // opcode; if the data cannot be sent, e.g. because it would need to
+ // be buffered but the buffer is full, the user agent must flag the
+ // WebSocket as full and then close the WebSocket connection. The data
+ // to be sent is the raw data represented by the Blob object. Any
+ // invocation of this method with a Blob argument that does not throw
+ // an exception must increase the bufferedAmount attribute by the size
+ // of the Blob object’s raw data, in bytes.
+
+ const frame = new WebsocketFrameSend()
+
+ data.arrayBuffer().then((ab) => {
+ const value = Buffer.from(ab)
+ frame.frameData = value
+ const buffer = frame.createFrame(opcodes.BINARY)
+
+ this.#bufferedAmount += value.byteLength
+ socket.write(buffer, () => {
+ this.#bufferedAmount -= value.byteLength
+ })
+ })
+ }
+ }
+
+ get readyState () {
+ webidl.brandCheck(this, WebSocket)
+
+ // The readyState getter steps are to return this's ready state.
+ return this[kReadyState]
+ }
+
+ get bufferedAmount () {
+ webidl.brandCheck(this, WebSocket)
+
+ return this.#bufferedAmount
+ }
+
+ get url () {
+ webidl.brandCheck(this, WebSocket)
+
+ // The url getter steps are to return this's url, serialized.
+ return URLSerializer(this[kWebSocketURL])
+ }
+
+ get extensions () {
+ webidl.brandCheck(this, WebSocket)
+
+ return this.#extensions
+ }
+
+ get protocol () {
+ webidl.brandCheck(this, WebSocket)
+
+ return this.#protocol
+ }
+
+ get onopen () {
+ webidl.brandCheck(this, WebSocket)
+
+ return this.#events.open
+ }
+
+ set onopen (fn) {
+ webidl.brandCheck(this, WebSocket)
+
+ if (this.#events.open) {
+ this.removeEventListener('open', this.#events.open)
+ }
+
+ if (typeof fn === 'function') {
+ this.#events.open = fn
+ this.addEventListener('open', fn)
+ } else {
+ this.#events.open = null
+ }
+ }
+
+ get onerror () {
+ webidl.brandCheck(this, WebSocket)
+
+ return this.#events.error
+ }
+
+ set onerror (fn) {
+ webidl.brandCheck(this, WebSocket)
+
+ if (this.#events.error) {
+ this.removeEventListener('error', this.#events.error)
+ }
+
+ if (typeof fn === 'function') {
+ this.#events.error = fn
+ this.addEventListener('error', fn)
+ } else {
+ this.#events.error = null
+ }
+ }
+
+ get onclose () {
+ webidl.brandCheck(this, WebSocket)
+
+ return this.#events.close
+ }
+
+ set onclose (fn) {
+ webidl.brandCheck(this, WebSocket)
+
+ if (this.#events.close) {
+ this.removeEventListener('close', this.#events.close)
+ }
+
+ if (typeof fn === 'function') {
+ this.#events.close = fn
+ this.addEventListener('close', fn)
+ } else {
+ this.#events.close = null
+ }
+ }
+
+ get onmessage () {
+ webidl.brandCheck(this, WebSocket)
+
+ return this.#events.message
+ }
+
+ set onmessage (fn) {
+ webidl.brandCheck(this, WebSocket)
+
+ if (this.#events.message) {
+ this.removeEventListener('message', this.#events.message)
+ }
+
+ if (typeof fn === 'function') {
+ this.#events.message = fn
+ this.addEventListener('message', fn)
+ } else {
+ this.#events.message = null
+ }
+ }
+
+ get binaryType () {
+ webidl.brandCheck(this, WebSocket)
+
+ return this[kBinaryType]
+ }
+
+ set binaryType (type) {
+ webidl.brandCheck(this, WebSocket)
+
+ if (type !== 'blob' && type !== 'arraybuffer') {
+ this[kBinaryType] = 'blob'
+ } else {
+ this[kBinaryType] = type
+ }
+ }
+
+ /**
+ * @see https://websockets.spec.whatwg.org/#feedback-from-the-protocol
+ */
+ #onConnectionEstablished (response) {
+ // processResponse is called when the "response’s header list has been received and initialized."
+ // once this happens, the connection is open
+ this[kResponse] = response
+
+ const parser = new ByteParser(this)
+ parser.on('drain', function onParserDrain () {
+ this.ws[kResponse].socket.resume()
+ })
+
+ response.socket.ws = this
+ this[kByteParser] = parser
+
+ // 1. Change the ready state to OPEN (1).
+ this[kReadyState] = states.OPEN
+
+ // 2. Change the extensions attribute’s value to the extensions in use, if
+ // it is not the null value.
+ // https://datatracker.ietf.org/doc/html/rfc6455#section-9.1
+ const extensions = response.headersList.get('sec-websocket-extensions')
+
+ if (extensions !== null) {
+ this.#extensions = extensions
+ }
+
+ // 3. Change the protocol attribute’s value to the subprotocol in use, if
+ // it is not the null value.
+ // https://datatracker.ietf.org/doc/html/rfc6455#section-1.9
+ const protocol = response.headersList.get('sec-websocket-protocol')
+
+ if (protocol !== null) {
+ this.#protocol = protocol
+ }
+
+ // 4. Fire an event named open at the WebSocket object.
+ fireEvent('open', this)
+ }
+}
+
+// https://websockets.spec.whatwg.org/#dom-websocket-connecting
+WebSocket.CONNECTING = WebSocket.prototype.CONNECTING = states.CONNECTING
+// https://websockets.spec.whatwg.org/#dom-websocket-open
+WebSocket.OPEN = WebSocket.prototype.OPEN = states.OPEN
+// https://websockets.spec.whatwg.org/#dom-websocket-closing
+WebSocket.CLOSING = WebSocket.prototype.CLOSING = states.CLOSING
+// https://websockets.spec.whatwg.org/#dom-websocket-closed
+WebSocket.CLOSED = WebSocket.prototype.CLOSED = states.CLOSED
+
+Object.defineProperties(WebSocket.prototype, {
+ CONNECTING: staticPropertyDescriptors,
+ OPEN: staticPropertyDescriptors,
+ CLOSING: staticPropertyDescriptors,
+ CLOSED: staticPropertyDescriptors,
+ url: kEnumerableProperty,
+ readyState: kEnumerableProperty,
+ bufferedAmount: kEnumerableProperty,
+ onopen: kEnumerableProperty,
+ onerror: kEnumerableProperty,
+ onclose: kEnumerableProperty,
+ close: kEnumerableProperty,
+ onmessage: kEnumerableProperty,
+ binaryType: kEnumerableProperty,
+ send: kEnumerableProperty,
+ extensions: kEnumerableProperty,
+ protocol: kEnumerableProperty,
+ [Symbol.toStringTag]: {
+ value: 'WebSocket',
+ writable: false,
+ enumerable: false,
+ configurable: true
+ }
+})
+
+Object.defineProperties(WebSocket, {
+ CONNECTING: staticPropertyDescriptors,
+ OPEN: staticPropertyDescriptors,
+ CLOSING: staticPropertyDescriptors,
+ CLOSED: staticPropertyDescriptors
+})
+
+webidl.converters['sequence<DOMString>'] = webidl.sequenceConverter(
+ webidl.converters.DOMString
+)
+
+webidl.converters['DOMString or sequence<DOMString>'] = function (V) {
+ if (webidl.util.Type(V) === 'Object' && Symbol.iterator in V) {
+ return webidl.converters['sequence<DOMString>'](V)
+ }
+
+ return webidl.converters.DOMString(V)
+}
+
+// This implements the propsal made in https://github.com/whatwg/websockets/issues/42
+webidl.converters.WebSocketInit = webidl.dictionaryConverter([
+ {
+ key: 'protocols',
+ converter: webidl.converters['DOMString or sequence<DOMString>'],
+ get defaultValue () {
+ return []
+ }
+ },
+ {
+ key: 'dispatcher',
+ converter: (V) => V,
+ get defaultValue () {
+ return getGlobalDispatcher()
+ }
+ },
+ {
+ key: 'headers',
+ converter: webidl.nullableConverter(webidl.converters.HeadersInit)
+ }
+])
+
+webidl.converters['DOMString or sequence<DOMString> or WebSocketInit'] = function (V) {
+ if (webidl.util.Type(V) === 'Object' && !(Symbol.iterator in V)) {
+ return webidl.converters.WebSocketInit(V)
+ }
+
+ return { protocols: webidl.converters['DOMString or sequence<DOMString>'](V) }
+}
+
+webidl.converters.WebSocketSendData = function (V) {
+ if (webidl.util.Type(V) === 'Object') {
+ if (isBlobLike(V)) {
+ return webidl.converters.Blob(V, { strict: false })
+ }
+
+ if (ArrayBuffer.isView(V) || types.isAnyArrayBuffer(V)) {
+ return webidl.converters.BufferSource(V)
+ }
+ }
+
+ return webidl.converters.USVString(V)
+}
+
+module.exports = {
+ WebSocket
+}
diff --git a/llhttp/.dockerignore b/llhttp/.dockerignore
new file mode 100644
index 0000000..11b226d
--- /dev/null
+++ b/llhttp/.dockerignore
@@ -0,0 +1,6 @@
+*
+!package.json
+!package-lock.json
+!tsconfig.json
+!bin
+!src
diff --git a/llhttp/.eslintrc.js b/llhttp/.eslintrc.js
new file mode 100644
index 0000000..595cf53
--- /dev/null
+++ b/llhttp/.eslintrc.js
@@ -0,0 +1,31 @@
+module.exports = {
+ 'env': {
+ 'browser': false,
+ 'commonjs': true,
+ 'es6': true,
+ 'node': true
+ },
+ 'extends': 'eslint:recommended',
+ 'rules': {
+ 'max-len': [ 2, {
+ 'code': 80,
+ 'ignoreComments': true
+ } ],
+ 'indent': [
+ 'error',
+ 2
+ ],
+ 'linebreak-style': [
+ 'error',
+ 'unix'
+ ],
+ 'quotes': [
+ 'error',
+ 'single'
+ ],
+ 'semi': [
+ 'error',
+ 'always'
+ ]
+ }
+};
diff --git a/llhttp/.github/workflows/aiohttp.yml b/llhttp/.github/workflows/aiohttp.yml
new file mode 100644
index 0000000..8ae8eb3
--- /dev/null
+++ b/llhttp/.github/workflows/aiohttp.yml
@@ -0,0 +1,61 @@
+name: Aiohttp
+# If you don't understand the reason for a test failure, ping @Dreamsorcerer or open an issue in aio-libs/aiohttp.
+
+on:
+ push:
+ branches:
+ - 'main'
+ pull_request:
+ branches:
+ - 'main'
+
+jobs:
+ test:
+ permissions:
+ contents: read # to fetch code (actions/checkout)
+
+ name: Aiohttp regression tests
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout aiohttp
+ uses: actions/checkout@v4
+ with:
+ repository: aio-libs/aiohttp
+ - name: Checkout llhttp
+ uses: actions/checkout@v4
+ with:
+ path: vendor/llhttp
+ - name: Restore node_modules cache
+ uses: actions/cache@v3
+ with:
+ path: vendor/llhttp/.npm
+ key: ubuntu-latest-node-${{ hashFiles('vendor/llhttp/**/package-lock.json') }}
+ restore-keys: ubuntu-latest-node-
+ - name: Install llhttp dependencies
+ run: npm install --ignore-scripts
+ working-directory: vendor/llhttp
+ - name: Build llhttp
+ run: make
+ working-directory: vendor/llhttp
+ - name: Setup Python
+ uses: actions/setup-python@v4
+ with:
+ python-version: 3.x
+ cache: 'pip'
+ cache-dependency-path: 'requirements/*.txt'
+ - name: Provision the dev env
+ run: >-
+ PATH="${HOME}/.local/bin:${PATH}"
+ make .develop
+ - name: Run tests
+ env:
+ COLOR: yes
+ run: >-
+ PATH="${HOME}/.local/bin:${PATH}"
+ pytest tests/test_http_parser.py tests/test_web_functional.py
+ - name: Run dev_mode tests
+ env:
+ COLOR: yes
+ run: >-
+ PATH="${HOME}/.local/bin:${PATH}"
+ python -X dev -m pytest -m dev_mode tests/test_http_parser.py tests/test_web_functional.py
diff --git a/llhttp/.github/workflows/ci.yaml b/llhttp/.github/workflows/ci.yaml
new file mode 100644
index 0000000..d1b3a65
--- /dev/null
+++ b/llhttp/.github/workflows/ci.yaml
@@ -0,0 +1,117 @@
+name: CI
+
+on: [push, pull_request]
+
+env:
+ CI: true
+
+jobs:
+ build:
+ name: Build libllhttp.a
+ runs-on: ${{ matrix.os }}
+ strategy:
+ matrix:
+ os:
+ - macos-latest
+ - ubuntu-latest
+ - windows-latest
+ steps:
+ - name: Install clang for Windows
+ if: runner.os == 'Windows'
+ run: |
+ iwr -useb get.scoop.sh -outfile 'install.ps1'
+ .\install.ps1 -RunAsAdmin
+ scoop install llvm --global
+
+ # Scoop modifies the PATH so we make the modified PATH global.
+ echo $env:PATH >> $env:GITHUB_PATH
+
+ - name: Fetch code
+ uses: actions/checkout@v3
+ with:
+ fetch-depth: 1
+
+ # Skip macOS & Windows, cache there is slower
+ - name: Restore node_modules cache for Linux
+ uses: actions/cache@v3
+ if: runner.os == 'Linux'
+ with:
+ path: ~/.npm
+ key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
+ restore-keys: |
+ ${{ runner.os }}-node-
+
+ - name: Install dependencies
+ run: npm install --ignore-scripts
+
+ - name: Build libllhttp.a
+ shell: bash
+ run: |
+ make build/libllhttp.a
+
+ test:
+ name: Run tests
+ runs-on: ${{ matrix.os }}
+ strategy:
+ matrix:
+ os:
+ - macos-latest
+ - ubuntu-latest
+ - windows-latest
+ steps:
+ - name: Install clang for Windows
+ if: runner.os == 'Windows'
+ run: |
+ iwr -useb get.scoop.sh -outfile 'install.ps1'
+ .\install.ps1 -RunAsAdmin
+ scoop install llvm --global
+
+ # Scoop modifies the PATH so we make the modified PATH global.
+ echo $env:PATH >> $env:GITHUB_PATH
+
+ - name: Fetch code
+ uses: actions/checkout@v3
+ with:
+ fetch-depth: 1
+
+ # Skip macOS & Windows, cache there is slower
+ - name: Restore node_modules cache for Linux
+ uses: actions/cache@v3
+ if: runner.os == 'Linux'
+ with:
+ path: ~/.npm
+ key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
+ restore-keys: |
+ ${{ runner.os }}-node-
+
+ - name: Install dependencies
+ run: npm install --ignore-scripts
+
+ # Custom script, because progress looks not good in CI
+ - name: Run tests
+ env:
+ CFLAGS: -O0
+ run: npx mocha --timeout 30000 -r ts-node/register/type-check test/*-test.ts
+
+ lint:
+ name: Run TSLint
+ runs-on: ubuntu-latest
+ steps:
+ - name: Fetch code
+ uses: actions/checkout@v3
+ with:
+ fetch-depth: 1
+
+ - name: Restore node_modules cache
+ uses: actions/cache@v3
+ with:
+ path: ~/.npm
+ key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
+ restore-keys: |
+ ${{ runner.os }}-node-
+
+ - name: Install dependencies
+ run: npm install --ignore-scripts
+
+ - name: Run lint command
+ run: npm run lint
diff --git a/llhttp/.gitignore b/llhttp/.gitignore
new file mode 100644
index 0000000..c2e9902
--- /dev/null
+++ b/llhttp/.gitignore
@@ -0,0 +1,6 @@
+node_modules/
+npm-debug.log
+test/tmp/
+lib/
+build/
+release/
diff --git a/llhttp/.npmrc b/llhttp/.npmrc
new file mode 100644
index 0000000..cafe685
--- /dev/null
+++ b/llhttp/.npmrc
@@ -0,0 +1 @@
+package-lock=true
diff --git a/llhttp/CMakeLists.txt b/llhttp/CMakeLists.txt
new file mode 100644
index 0000000..97fa408
--- /dev/null
+++ b/llhttp/CMakeLists.txt
@@ -0,0 +1,117 @@
+cmake_minimum_required(VERSION 3.5.1)
+cmake_policy(SET CMP0069 NEW)
+
+project(llhttp VERSION _RELEASE_)
+include(GNUInstallDirs)
+
+set(CMAKE_C_STANDARD 99)
+
+# By default build in relwithdebinfo type, supports both lowercase and uppercase
+if(NOT CMAKE_CONFIGURATION_TYPES)
+ set(allowableBuildTypes DEBUG RELEASE RELWITHDEBINFO MINSIZEREL)
+ set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS "${allowableBuildTypes}")
+ if(NOT CMAKE_BUILD_TYPE)
+ set(CMAKE_BUILD_TYPE RELWITHDEBINFO CACHE STRING "" FORCE)
+ else()
+ string(TOUPPER ${CMAKE_BUILD_TYPE} CMAKE_BUILD_TYPE)
+ if(NOT CMAKE_BUILD_TYPE IN_LIST allowableBuildTypes)
+ message(FATAL_ERROR "Invalid build type: ${CMAKE_BUILD_TYPE}")
+ endif()
+ endif()
+endif()
+
+#
+# Options
+#
+# Generic option
+option(BUILD_SHARED_LIBS "Build shared libraries (.dll/.so)" ON)
+option(BUILD_STATIC_LIBS "Build static libraries (.lib/.a)" OFF)
+
+# Source code
+set(LLHTTP_SOURCES
+ ${CMAKE_CURRENT_SOURCE_DIR}/src/llhttp.c
+ ${CMAKE_CURRENT_SOURCE_DIR}/src/http.c
+ ${CMAKE_CURRENT_SOURCE_DIR}/src/api.c
+)
+
+set(LLHTTP_HEADERS
+ ${CMAKE_CURRENT_SOURCE_DIR}/include/llhttp.h
+)
+
+configure_file(
+ ${CMAKE_CURRENT_SOURCE_DIR}/libllhttp.pc.in
+ ${CMAKE_CURRENT_SOURCE_DIR}/libllhttp.pc
+ @ONLY
+)
+
+function(config_library target)
+ target_sources(${target} PRIVATE ${LLHTTP_SOURCES} ${LLHTTP_HEADERS})
+
+ target_include_directories(${target} PUBLIC
+ $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
+ $<INSTALL_INTERFACE:include>
+ )
+
+ set_target_properties(${target} PROPERTIES
+ OUTPUT_NAME llhttp
+ VERSION ${PROJECT_VERSION}
+ SOVERSION ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}
+ PUBLIC_HEADER ${LLHTTP_HEADERS}
+ )
+
+ install(TARGETS ${target}
+ EXPORT llhttp
+ LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
+ ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
+ PUBLIC_HEADER DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
+ )
+
+ install(FILES
+ ${CMAKE_CURRENT_SOURCE_DIR}/libllhttp.pc
+ DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig
+ )
+
+ # This is required to work with FetchContent
+ install(EXPORT llhttp
+ FILE llhttp-config.cmake
+ NAMESPACE llhttp::
+ DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/llhttp
+ )
+endfunction(config_library target)
+
+if(BUILD_SHARED_LIBS)
+ add_library(llhttp_shared SHARED
+ ${llhttp_src}
+ )
+ add_library(llhttp::llhttp ALIAS llhttp_shared)
+ config_library(llhttp_shared)
+endif()
+
+if(BUILD_STATIC_LIBS)
+ add_library(llhttp_static STATIC
+ ${llhttp_src}
+ )
+ if(BUILD_SHARED_LIBS)
+ add_library(llhttp::llhttp ALIAS llhttp_shared)
+ else()
+ add_library(llhttp::llhttp ALIAS llhttp_static)
+ endif()
+ config_library(llhttp_static)
+endif()
+
+# On windows with Visual Studio, add a debug postfix so that release
+# and debug libraries can coexist.
+if(MSVC)
+ set(CMAKE_DEBUG_POSTFIX "d")
+endif()
+
+# Print project configure summary
+message(STATUS "")
+message(STATUS "")
+message(STATUS "Project configure summary:")
+message(STATUS "")
+message(STATUS " CMake build type .................: ${CMAKE_BUILD_TYPE}")
+message(STATUS " Install prefix ...................: ${CMAKE_INSTALL_PREFIX}")
+message(STATUS " Build shared library .............: ${BUILD_SHARED_LIBS}")
+message(STATUS " Build static library .............: ${BUILD_STATIC_LIBS}")
+message(STATUS "")
diff --git a/llhttp/CNAME b/llhttp/CNAME
new file mode 100644
index 0000000..4c4e078
--- /dev/null
+++ b/llhttp/CNAME
@@ -0,0 +1 @@
+llhttp.org \ No newline at end of file
diff --git a/llhttp/CODE_OF_CONDUCT.md b/llhttp/CODE_OF_CONDUCT.md
new file mode 100644
index 0000000..8470ae4
--- /dev/null
+++ b/llhttp/CODE_OF_CONDUCT.md
@@ -0,0 +1,4 @@
+# Code of Conduct
+
+* [Node.js Code of Conduct](https://github.com/nodejs/admin/blob/main/CODE_OF_CONDUCT.md)
+* [Node.js Moderation Policy](https://github.com/nodejs/admin/blob/main/Moderation-Policy.md)
diff --git a/llhttp/Dockerfile b/llhttp/Dockerfile
new file mode 100644
index 0000000..2b5bfae
--- /dev/null
+++ b/llhttp/Dockerfile
@@ -0,0 +1,13 @@
+FROM node:18-alpine
+ARG UID=1000
+ARG GID=1000
+
+RUN apk add -U clang lld wasi-sdk && mkdir /home/node/llhttp
+
+WORKDIR /home/node/llhttp
+
+COPY . .
+
+RUN npm ci
+
+USER node
diff --git a/llhttp/LICENSE-MIT b/llhttp/LICENSE-MIT
new file mode 100644
index 0000000..6c1512d
--- /dev/null
+++ b/llhttp/LICENSE-MIT
@@ -0,0 +1,22 @@
+This software is licensed under the MIT License.
+
+Copyright Fedor Indutny, 2018.
+
+Permission is hereby granted, free of charge, to any person obtaining a
+copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to permit
+persons to whom the Software is furnished to do so, subject to the
+following conditions:
+
+The above copyright notice and this permission notice shall be included
+in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
+NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
+USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/llhttp/Makefile b/llhttp/Makefile
new file mode 100644
index 0000000..d9c6d35
--- /dev/null
+++ b/llhttp/Makefile
@@ -0,0 +1,93 @@
+CLANG ?= clang
+CFLAGS ?=
+OS ?=
+
+CFLAGS += -Os -g3 -Wall -Wextra -Wno-unused-parameter
+ifneq ($(OS),Windows_NT)
+ # NOTE: clang on windows does not support fPIC
+ CFLAGS += -fPIC
+endif
+
+INCLUDES += -Ibuild/
+
+INSTALL ?= install
+PREFIX ?= /usr/local
+LIBDIR = $(PREFIX)/lib
+INCLUDEDIR = $(PREFIX)/include
+
+all: build/libllhttp.a build/libllhttp.so
+
+clean:
+ rm -rf release/
+ rm -rf build/
+
+build/libllhttp.so: build/c/llhttp.o build/native/api.o \
+ build/native/http.o
+ $(CLANG) -shared $^ -o $@
+
+build/libllhttp.a: build/c/llhttp.o build/native/api.o \
+ build/native/http.o
+ $(AR) rcs $@ build/c/llhttp.o build/native/api.o build/native/http.o
+
+build/c/llhttp.o: build/c/llhttp.c
+ $(CLANG) $(CFLAGS) $(INCLUDES) -c $< -o $@
+
+build/native/%.o: src/native/%.c build/llhttp.h src/native/api.h \
+ build/native
+ $(CLANG) $(CFLAGS) $(INCLUDES) -c $< -o $@
+
+build/llhttp.h: generate
+build/c/llhttp.c: generate
+
+build/native:
+ mkdir -p build/native
+
+release: clean generate
+ @echo "${RELEASE}" | grep -q -E ".+" || { echo "Please make sure the RELEASE argument is set."; exit 1; }
+ rm -rf release
+ mkdir -p release/src
+ mkdir -p release/include
+ cp -rf build/llhttp.h release/include/
+ cp -rf build/c/llhttp.c release/src/
+ cp -rf src/native/*.c release/src/
+ cp -rf src/llhttp.gyp release/
+ cp -rf src/common.gypi release/
+ sed s/_RELEASE_/$(RELEASE)/ CMakeLists.txt > release/CMakeLists.txt
+ cp -rf libllhttp.pc.in release/
+ cp -rf README.md release/
+ cp -rf LICENSE-MIT release/
+
+github-release:
+ @echo "${RELEASE_V}" | grep -q -E "^v" || { echo "Please make sure version starts with \"v\"."; exit 1; }
+ gh release create -d --generate-notes ${RELEASE_V}
+ @sleep 5
+ gh release view ${RELEASE_V} -t "{{.body}}" --json body > RELEASE_NOTES
+ gh release delete ${RELEASE_V} -y
+ gh release create -F RELEASE_NOTES -d --title ${RELEASE_V} --target release release/${RELEASE_V}
+ @sleep 5
+ rm -rf RELEASE_NOTES
+ open $$(gh release view release/${RELEASE_V} --json url -t "{{.url}}")
+
+postversion: release
+ git fetch origin
+ git push
+ git checkout release --
+ cp -rf release/* ./
+ rm -rf release
+ git add include src *.gyp *.gypi CMakeLists.txt README.md LICENSE-MIT libllhttp.pc.in
+ git commit -a -m "release: $(RELEASE)"
+ git tag "release/v$(RELEASE)"
+ git push && git push --tags
+ git checkout main
+
+generate:
+ npx ts-node bin/generate.ts
+
+install: build/libllhttp.a build/libllhttp.so
+ $(INSTALL) -d $(DESTDIR)$(INCLUDEDIR)
+ $(INSTALL) -d $(DESTDIR)$(LIBDIR)
+ $(INSTALL) -C build/llhttp.h $(DESTDIR)$(INCLUDEDIR)/llhttp.h
+ $(INSTALL) -C build/libllhttp.a $(DESTDIR)$(LIBDIR)/libllhttp.a
+ $(INSTALL) build/libllhttp.so $(DESTDIR)$(LIBDIR)/libllhttp.so
+
+.PHONY: all generate clean release postversion github-release
diff --git a/llhttp/README.md b/llhttp/README.md
new file mode 100644
index 0000000..4960dbb
--- /dev/null
+++ b/llhttp/README.md
@@ -0,0 +1,501 @@
+# llhttp
+[![CI](https://github.com/nodejs/llhttp/workflows/CI/badge.svg)](https://github.com/nodejs/llhttp/actions?query=workflow%3ACI)
+
+Port of [http_parser][0] to [llparse][1].
+
+## Why?
+
+Let's face it, [http_parser][0] is practically unmaintainable. Even
+introduction of a single new method results in a significant code churn.
+
+This project aims to:
+
+* Make it maintainable
+* Verifiable
+* Improving benchmarks where possible
+
+More details in [Fedor Indutny's talk at JSConf EU 2019](https://youtu.be/x3k_5Mi66sY)
+
+## How?
+
+Over time, different approaches for improving [http_parser][0]'s code base
+were tried. However, all of them failed due to resulting significant performance
+degradation.
+
+This project is a port of [http_parser][0] to TypeScript. [llparse][1] is used
+to generate the output C source file, which could be compiled and
+linked with the embedder's program (like [Node.js][7]).
+
+## Performance
+
+So far llhttp outperforms http_parser:
+
+| | input size | bandwidth | reqs/sec | time |
+|:----------------|-----------:|-------------:|-----------:|--------:|
+| **llhttp** | 8192.00 mb | 1777.24 mb/s | 3583799.39 req/sec | 4.61 s |
+| **http_parser** | 8192.00 mb | 694.66 mb/s | 1406180.33 req/sec | 11.79 s |
+
+llhttp is faster by approximately **156%**.
+
+## Maintenance
+
+llhttp project has about 1400 lines of TypeScript code describing the parser
+itself and around 450 lines of C code and headers providing the helper methods.
+The whole [http_parser][0] is implemented in approximately 2500 lines of C, and
+436 lines of headers.
+
+All optimizations and multi-character matching in llhttp are generated
+automatically, and thus doesn't add any extra maintenance cost. On the contrary,
+most of http_parser's code is hand-optimized and unrolled. Instead describing
+"how" it should parse the HTTP requests/responses, a maintainer should
+implement the new features in [http_parser][0] cautiously, considering
+possible performance degradation and manually optimizing the new code.
+
+## Verification
+
+The state machine graph is encoded explicitly in llhttp. The [llparse][1]
+automatically checks the graph for absence of loops and correct reporting of the
+input ranges (spans) like header names and values. In the future, additional
+checks could be performed to get even stricter verification of the llhttp.
+
+## Usage
+
+```C
+#include "stdio.h"
+#include "llhttp.h"
+#include "string.h"
+
+int handle_on_message_complete(llhttp_t* parser) {
+ fprintf(stdout, "Message completed!\n");
+ return 0;
+}
+
+int main() {
+ llhttp_t parser;
+ llhttp_settings_t settings;
+
+ /*Initialize user callbacks and settings */
+ llhttp_settings_init(&settings);
+
+ /*Set user callback */
+ settings.on_message_complete = handle_on_message_complete;
+
+ /*Initialize the parser in HTTP_BOTH mode, meaning that it will select between
+ *HTTP_REQUEST and HTTP_RESPONSE parsing automatically while reading the first
+ *input.
+ */
+ llhttp_init(&parser, HTTP_BOTH, &settings);
+
+ /*Parse request! */
+ const char* request = "GET / HTTP/1.1\r\n\r\n";
+ int request_len = strlen(request);
+
+ enum llhttp_errno err = llhttp_execute(&parser, request, request_len);
+ if (err == HPE_OK) {
+ fprintf(stdout, "Successfully parsed!\n");
+ } else {
+ fprintf(stderr, "Parse error: %s %s\n", llhttp_errno_name(err), parser.reason);
+ }
+}
+```
+For more information on API usage, please refer to [src/native/api.h](https://github.com/nodejs/llhttp/blob/main/src/native/api.h).
+
+## API
+
+### llhttp_settings_t
+
+The settings object contains a list of callbacks that the parser will invoke.
+
+The following callbacks can return `0` (proceed normally), `-1` (error) or `HPE_PAUSED` (pause the parser):
+
+* `on_message_begin`: Invoked when a new request/response starts.
+* `on_message_complete`: Invoked when a request/response has been completedly parsed.
+* `on_url_complete`: Invoked after the URL has been parsed.
+* `on_method_complete`: Invoked after the HTTP method has been parsed.
+* `on_version_complete`: Invoked after the HTTP version has been parsed.
+* `on_status_complete`: Invoked after the status code has been parsed.
+* `on_header_field_complete`: Invoked after a header name has been parsed.
+* `on_header_value_complete`: Invoked after a header value has been parsed.
+* `on_chunk_header`: Invoked after a new chunk is started. The current chunk length is stored in `parser->content_length`.
+* `on_chunk_extension_name_complete`: Invoked after a chunk extension name is started.
+* `on_chunk_extension_value_complete`: Invoked after a chunk extension value is started.
+* `on_chunk_complete`: Invoked after a new chunk is received.
+* `on_reset`: Invoked after `on_message_complete` and before `on_message_begin` when a new message
+ is received on the same parser. This is not invoked for the first message of the parser.
+
+The following callbacks can return `0` (proceed normally), `-1` (error) or `HPE_USER` (error from the callback):
+
+* `on_url`: Invoked when another character of the URL is received.
+* `on_status`: Invoked when another character of the status is received.
+* `on_method`: Invoked when another character of the method is received.
+ When parser is created with `HTTP_BOTH` and the input is a response, this also invoked for the sequence `HTTP/`
+ of the first message.
+* `on_version`: Invoked when another character of the version is received.
+* `on_header_field`: Invoked when another character of a header name is received.
+* `on_header_value`: Invoked when another character of a header value is received.
+* `on_chunk_extension_name`: Invoked when another character of a chunk extension name is received.
+* `on_chunk_extension_value`: Invoked when another character of a extension value is received.
+
+The callback `on_headers_complete`, invoked when headers are completed, can return:
+
+* `0`: Proceed normally.
+* `1`: Assume that request/response has no body, and proceed to parsing the next message.
+* `2`: Assume absence of body (as above) and make `llhttp_execute()` return `HPE_PAUSED_UPGRADE`.
+* `-1`: Error
+* `HPE_PAUSED`: Pause the parser.
+
+### `void llhttp_init(llhttp_t* parser, llhttp_type_t type, const llhttp_settings_t* settings)`
+
+Initialize the parser with specific type and user settings.
+
+### `uint8_t llhttp_get_type(llhttp_t* parser)`
+
+Returns the type of the parser.
+
+### `uint8_t llhttp_get_http_major(llhttp_t* parser)`
+
+Returns the major version of the HTTP protocol of the current request/response.
+
+### `uint8_t llhttp_get_http_minor(llhttp_t* parser)`
+
+Returns the minor version of the HTTP protocol of the current request/response.
+
+### `uint8_t llhttp_get_method(llhttp_t* parser)`
+
+Returns the method of the current request.
+
+### `int llhttp_get_status_code(llhttp_t* parser)`
+
+Returns the method of the current response.
+
+### `uint8_t llhttp_get_upgrade(llhttp_t* parser)`
+
+Returns `1` if request includes the `Connection: upgrade` header.
+
+### `void llhttp_reset(llhttp_t* parser)`
+
+Reset an already initialized parser back to the start state, preserving the
+existing parser type, callback settings, user data, and lenient flags.
+
+### `void llhttp_settings_init(llhttp_settings_t* settings)`
+
+Initialize the settings object.
+
+### `llhttp_errno_t llhttp_execute(llhttp_t* parser, const char* data, size_t len)`
+
+Parse full or partial request/response, invoking user callbacks along the way.
+
+If any of `llhttp_data_cb` returns errno not equal to `HPE_OK` - the parsing interrupts,
+and such errno is returned from `llhttp_execute()`. If `HPE_PAUSED` was used as a errno,
+the execution can be resumed with `llhttp_resume()` call.
+
+In a special case of CONNECT/Upgrade request/response `HPE_PAUSED_UPGRADE` is returned
+after fully parsing the request/response. If the user wishes to continue parsing,
+they need to invoke `llhttp_resume_after_upgrade()`.
+
+**if this function ever returns a non-pause type error, it will continue to return
+the same error upon each successive call up until `llhttp_init()` is called.**
+
+### `llhttp_errno_t llhttp_finish(llhttp_t* parser)`
+
+This method should be called when the other side has no further bytes to
+send (e.g. shutdown of readable side of the TCP connection.)
+
+Requests without `Content-Length` and other messages might require treating
+all incoming bytes as the part of the body, up to the last byte of the
+connection.
+
+This method will invoke `on_message_complete()` callback if the
+request was terminated safely. Otherwise a error code would be returned.
+
+
+### `int llhttp_message_needs_eof(const llhttp_t* parser)`
+
+Returns `1` if the incoming message is parsed until the last byte, and has to be completed by calling `llhttp_finish()` on EOF.
+
+### `int llhttp_should_keep_alive(const llhttp_t* parser)`
+
+Returns `1` if there might be any other messages following the last that was
+successfully parsed.
+
+### `void llhttp_pause(llhttp_t* parser)`
+
+Make further calls of `llhttp_execute()` return `HPE_PAUSED` and set
+appropriate error reason.
+
+**Do not call this from user callbacks! User callbacks must return
+`HPE_PAUSED` if pausing is required.**
+
+### `void llhttp_resume(llhttp_t* parser)`
+
+Might be called to resume the execution after the pause in user's callback.
+
+See `llhttp_execute()` above for details.
+
+**Call this only if `llhttp_execute()` returns `HPE_PAUSED`.**
+
+### `void llhttp_resume_after_upgrade(llhttp_t* parser)`
+
+Might be called to resume the execution after the pause in user's callback.
+See `llhttp_execute()` above for details.
+
+**Call this only if `llhttp_execute()` returns `HPE_PAUSED_UPGRADE`**
+
+### `llhttp_errno_t llhttp_get_errno(const llhttp_t* parser)`
+
+Returns the latest error.
+
+### `const char* llhttp_get_error_reason(const llhttp_t* parser)`
+
+Returns the verbal explanation of the latest returned error.
+
+**User callback should set error reason when returning the error. See
+`llhttp_set_error_reason()` for details.**
+
+### `void llhttp_set_error_reason(llhttp_t* parser, const char* reason)`
+
+Assign verbal description to the returned error. Must be called in user
+callbacks right before returning the errno.
+
+**`HPE_USER` error code might be useful in user callbacks.**
+
+### `const char* llhttp_get_error_pos(const llhttp_t* parser)`
+
+Returns the pointer to the last parsed byte before the returned error. The
+pointer is relative to the `data` argument of `llhttp_execute()`.
+
+**This method might be useful for counting the number of parsed bytes.**
+
+### `const char* llhttp_errno_name(llhttp_errno_t err)`
+
+Returns textual name of error code.
+
+### `const char* llhttp_method_name(llhttp_method_t method)`
+
+Returns textual name of HTTP method.
+
+### `const char* llhttp_status_name(llhttp_status_t status)`
+
+Returns textual name of HTTP status.
+
+### `void llhttp_set_lenient_headers(llhttp_t* parser, int enabled)`
+
+Enables/disables lenient header value parsing (disabled by default).
+Lenient parsing disables header value token checks, extending llhttp's
+protocol support to highly non-compliant clients/server.
+
+No `HPE_INVALID_HEADER_TOKEN` will be raised for incorrect header values when
+lenient parsing is "on".
+
+**Enabling this flag can pose a security issue since you will be exposed to request smuggling attacks. USE WITH CAUTION!**
+
+### `void llhttp_set_lenient_chunked_length(llhttp_t* parser, int enabled)`
+
+Enables/disables lenient handling of conflicting `Transfer-Encoding` and
+`Content-Length` headers (disabled by default).
+
+Normally `llhttp` would error when `Transfer-Encoding` is present in
+conjunction with `Content-Length`.
+
+This error is important to prevent HTTP request smuggling, but may be less desirable
+for small number of cases involving legacy servers.
+
+**Enabling this flag can pose a security issue since you will be exposed to request smuggling attacks. USE WITH CAUTION!**
+
+### `void llhttp_set_lenient_keep_alive(llhttp_t* parser, int enabled)`
+
+Enables/disables lenient handling of `Connection: close` and HTTP/1.0
+requests responses.
+
+Normally `llhttp` would error the HTTP request/response
+after the request/response with `Connection: close` and `Content-Length`.
+
+This is important to prevent cache poisoning attacks,
+but might interact badly with outdated and insecure clients.
+
+With this flag the extra request/response will be parsed normally.
+
+**Enabling this flag can pose a security issue since you will be exposed to poisoning attacks. USE WITH CAUTION!**
+
+### `void llhttp_set_lenient_transfer_encoding(llhttp_t* parser, int enabled)`
+
+Enables/disables lenient handling of `Transfer-Encoding` header.
+
+Normally `llhttp` would error when a `Transfer-Encoding` has `chunked` value
+and another value after it (either in a single header or in multiple
+headers whose value are internally joined using `, `).
+
+This is mandated by the spec to reliably determine request body size and thus
+avoid request smuggling.
+
+With this flag the extra value will be parsed normally.
+
+**Enabling this flag can pose a security issue since you will be exposed to request smuggling attacks. USE WITH CAUTION!**
+
+### `void llhttp_set_lenient_version(llhttp_t* parser, int enabled)`
+
+Enables/disables lenient handling of HTTP version.
+
+Normally `llhttp` would error when the HTTP version in the request or status line
+is not `0.9`, `1.0`, `1.1` or `2.0`.
+With this flag the extra value will be parsed normally.
+
+**Enabling this flag can pose a security issue since you will allow unsupported HTTP versions. USE WITH CAUTION!**
+
+### `void llhttp_set_lenient_data_after_close(llhttp_t* parser, int enabled)`
+
+Enables/disables lenient handling of additional data received after a message ends
+and keep-alive is disabled.
+
+Normally `llhttp` would error when additional unexpected data is received if the message
+contains the `Connection` header with `close` value.
+With this flag the extra data will discarded without throwing an error.
+
+**Enabling this flag can pose a security issue since you will be exposed to poisoning attacks. USE WITH CAUTION!**
+
+### `void llhttp_set_lenient_optional_lf_after_cr(llhttp_t* parser, int enabled)`
+
+Enables/disables lenient handling of incomplete CRLF sequences.
+
+Normally `llhttp` would error when a CR is not followed by LF when terminating the
+request line, the status line, the headers or a chunk header.
+With this flag only a CR is required to terminate such sections.
+
+**Enabling this flag can pose a security issue since you will be exposed to request smuggling attacks. USE WITH CAUTION!**
+
+### `void llhttp_set_lenient_optional_cr_before_lf(llhttp_t* parser, int enabled)`
+
+Enables/disables lenient handling of line separators.
+
+Normally `llhttp` would error when a LF is not preceded by CR when terminating the
+request line, the status line, the headers, a chunk header or a chunk data.
+With this flag only a LF is required to terminate such sections.
+
+**Enabling this flag can pose a security issue since you will be exposed to request smuggling attacks. USE WITH CAUTION!**
+
+### `void llhttp_set_lenient_optional_crlf_after_chunk(llhttp_t* parser, int enabled)`
+
+Enables/disables lenient handling of chunks not separated via CRLF.
+
+Normally `llhttp` would error when after a chunk data a CRLF is missing before
+starting a new chunk.
+With this flag the new chunk can start immediately after the previous one.
+
+**Enabling this flag can pose a security issue since you will be exposed to request smuggling attacks. USE WITH CAUTION!**
+
+### `void llhttp_set_lenient_spaces_after_chunk_size(llhttp_t* parser, int enabled)`
+
+Enables/disables lenient handling of spaces after chunk size.
+
+Normally `llhttp` would error when after a chunk size is followed by one or more spaces are present instead of a CRLF or `;`.
+With this flag this check is disabled.
+
+**Enabling this flag can pose a security issue since you will be exposed to request smuggling attacks. USE WITH CAUTION!**
+
+## Build Instructions
+
+Make sure you have [Node.js](https://nodejs.org/), npm and npx installed. Then under project directory run:
+
+```sh
+npm install
+make
+```
+
+---
+
+### Bindings to other languages
+
+* Lua: [MunifTanjim/llhttp.lua][11]
+* Python: [pallas/pyllhttp][8]
+* Ruby: [metabahn/llhttp][9]
+* Rust: [JackLiar/rust-llhttp][10]
+
+### Using with CMake
+
+If you want to use this library in a CMake project as a shared library, you can use the snippet below.
+
+```
+FetchContent_Declare(llhttp
+ URL "https://github.com/nodejs/llhttp/archive/refs/tags/release/v8.1.0.tar.gz")
+
+FetchContent_MakeAvailable(llhttp)
+
+# Link with the llhttp_shared target
+target_link_libraries(${EXAMPLE_PROJECT_NAME} ${PROJECT_LIBRARIES} llhttp_shared ${PROJECT_NAME})
+```
+
+If you want to use this library in a CMake project as a static library, you can set some cache variables first.
+
+```
+FetchContent_Declare(llhttp
+ URL "https://github.com/nodejs/llhttp/archive/refs/tags/release/v8.1.0.tar.gz")
+
+set(BUILD_SHARED_LIBS OFF CACHE INTERNAL "")
+set(BUILD_STATIC_LIBS ON CACHE INTERNAL "")
+FetchContent_MakeAvailable(llhttp)
+
+# Link with the llhttp_static target
+target_link_libraries(${EXAMPLE_PROJECT_NAME} ${PROJECT_LIBRARIES} llhttp_static ${PROJECT_NAME})
+```
+
+_Note that using the git repo directly (e.g., via a git repo url and tag) will not work with FetchContent_Declare because [CMakeLists.txt](./CMakeLists.txt) requires string replacements (e.g., `_RELEASE_`) before it will build._
+
+## Building on Windows
+
+### Installation
+
+* `choco install git`
+* `choco install node`
+* `choco install llvm` (or install the `C++ Clang tools for Windows` optional package from the Visual Studio 2019 installer)
+* `choco install make` (or if you have MinGW, it comes bundled)
+
+1. Ensure that `Clang` and `make` are in your system path.
+2. Using Git Bash, clone the repo to your preferred location.
+3. Cd into the cloned directory and run `npm install`
+5. Run `make`
+6. Your `repo/build` directory should now have `libllhttp.a` and `libllhttp.so` static and dynamic libraries.
+7. When building your executable, you can link to these libraries. Make sure to set the build folder as an include path when building so you can reference the declarations in `repo/build/llhttp.h`.
+
+### A simple example on linking with the library:
+
+Assuming you have an executable `main.cpp` in your current working directory, you would run: `clang++ -Os -g3 -Wall -Wextra -Wno-unused-parameter -I/path/to/llhttp/build main.cpp /path/to/llhttp/build/libllhttp.a -o main.exe`.
+
+If you are getting `unresolved external symbol` linker errors you are likely attempting to build `llhttp.c` without linking it with object files from `api.c` and `http.c`.
+
+#### LICENSE
+
+This software is licensed under the MIT License.
+
+Copyright Fedor Indutny, 2018.
+
+Permission is hereby granted, free of charge, to any person obtaining a
+copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to permit
+persons to whom the Software is furnished to do so, subject to the
+following conditions:
+
+The above copyright notice and this permission notice shall be included
+in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
+NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
+USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+[0]: https://github.com/nodejs/http-parser
+[1]: https://github.com/nodejs/llparse
+[2]: https://en.wikipedia.org/wiki/Register_allocation#Spilling
+[3]: https://en.wikipedia.org/wiki/Tail_call
+[4]: https://llvm.org/docs/LangRef.html
+[5]: https://llvm.org/docs/LangRef.html#call-instruction
+[6]: https://clang.llvm.org/
+[7]: https://github.com/nodejs/node
+[8]: https://github.com/pallas/pyllhttp
+[9]: https://github.com/metabahn/llhttp
+[10]: https://github.com/JackLiar/rust-llhttp
+[11]: https://github.com/MunifTanjim/llhttp.lua
diff --git a/llhttp/_config.yml b/llhttp/_config.yml
new file mode 100644
index 0000000..1885487
--- /dev/null
+++ b/llhttp/_config.yml
@@ -0,0 +1 @@
+theme: jekyll-theme-midnight \ No newline at end of file
diff --git a/llhttp/bench/index.ts b/llhttp/bench/index.ts
new file mode 100644
index 0000000..b3ff2e1
--- /dev/null
+++ b/llhttp/bench/index.ts
@@ -0,0 +1,71 @@
+import * as assert from "assert";
+import { spawnSync } from "child_process";
+import { existsSync } from "fs";
+import { resolve } from "path";
+
+function request(tpl: TemplateStringsArray): string {
+ return tpl.raw[0].replace(/^\s+/gm, '').replace(/\n/gm, '').replace(/\\r/gm, '\r').replace(/\\n/gm, '\n')
+}
+
+const urlExecutable = resolve(__dirname, "../test/tmp/url-url-c");
+const httpExecutable = resolve(__dirname, "../test/tmp/http-request-c");
+
+const httpRequests: Record<string, string> = {
+ "seanmonstar/httparse": request`
+ GET /wp-content/uploads/2010/03/hello-kitty-darth-vader-pink.jpg HTTP/1.1\r\n
+ Host: www.kittyhell.com\r\n
+ User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.6; ja-JP-mac; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3 Pathtraq/0.9\r\n
+ Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n
+ Accept-Language: ja,en-us;q=0.7,en;q=0.3\r\n
+ Accept-Encoding: gzip,deflate\r\n
+ Accept-Charset: Shift_JIS,utf-8;q=0.7,*;q=0.7\r\n
+ Keep-Alive: 115\r\n
+ Connection: keep-alive\r\n
+ Cookie: wp_ozh_wsa_visits=2; wp_ozh_wsa_visit_lasttime=xxxxxxxxxx; __utma=xxxxxxxxx.xxxxxxxxxx.xxxxxxxxxx.xxxxxxxxxx.xxxxxxxxxx.x; __utmz=xxxxxxxxx.xxxxxxxxxx.x.x.utmccn=(referral)|utmcsr=reader.livedoor.com|utmcct=/reader/|utmcmd=referral\r\n\r\n
+ `,
+ "nodejs/http-parser": request`
+ POST /joyent/http-parser HTTP/1.1\r\n
+ Host: github.com\r\n
+ DNT: 1\r\n
+ Accept-Encoding: gzip, deflate, sdch\r\n
+ Accept-Language: ru-RU,ru;q=0.8,en-US;q=0.6,en;q=0.4\r\n
+ User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1)
+ AppleWebKit/537.36 (KHTML, like Gecko)
+ Chrome/39.0.2171.65 Safari/537.36\r\n
+ Accept: text/html,application/xhtml+xml,application/xml;q=0.9,
+ image/webp,*/*;q=0.8\r\n
+ Referer: https://github.com/joyent/http-parser\r\n
+ Connection: keep-alive\r\n
+ Transfer-Encoding: chunked\r\n
+ Cache-Control: max-age=0\r\n\r\nb\r\nhello world\r\n0\r\n\r\n
+ `
+}
+const urlRequest = "http://example.com/path/to/file?query=value#fragment";
+
+if (!existsSync(urlExecutable) || !existsSync(urlExecutable)) {
+ console.error(
+ "\x1b[31m\x1b[1mPlease run npm test in order to create required executables."
+ );
+ process.exit(1);
+}
+
+if (process.argv[2] === "loop") {
+ const reqName = process.argv[3];
+ const request = httpRequests[reqName]!;
+
+ assert(request, `Unknown request name: "${reqName}"`);
+ spawnSync(httpExecutable, ["loop", request], { stdio: "inherit" });
+ process.exit(0);
+}
+
+if (!process.argv[2] || process.argv[2] === "url") {
+ console.log("url (C)");
+ spawnSync(urlExecutable, ["bench", urlRequest], { stdio: "inherit" });
+}
+
+if (!process.argv[2] || process.argv[2] === "http") {
+ for (const [name, request] of Object.entries(httpRequests)) {
+ console.log('http: "%s" (C)', name);
+ spawnSync(httpExecutable, ["bench", request], { stdio: "inherit" });
+ }
+}
diff --git a/llhttp/bin/build_wasm.ts b/llhttp/bin/build_wasm.ts
new file mode 100644
index 0000000..a885703
--- /dev/null
+++ b/llhttp/bin/build_wasm.ts
@@ -0,0 +1,95 @@
+import { execSync } from 'child_process';
+import { copyFileSync, mkdirSync } from 'fs';
+import { join, resolve } from 'path';
+
+let platform = process.env.WASM_PLATFORM ?? '';
+const WASM_OUT = resolve(__dirname, '../build/wasm');
+const WASM_SRC = resolve(__dirname, '../');
+
+if (!platform && process.argv[2]) {
+ platform = execSync('docker info -f "{{.OSType}}/{{.Architecture}}"').toString().trim();
+}
+
+if (process.argv[2] === '--prebuild') {
+ const cmd = `docker build --platform=${platform.toString().trim()} -t llhttp_wasm_builder .`;
+
+ /* tslint:disable-next-line no-console */
+ console.log(`> ${cmd}\n\n`);
+ execSync(cmd, { stdio: 'inherit' });
+
+ process.exit(0);
+}
+
+if (process.argv[2] === '--setup') {
+ try {
+ mkdirSync(join(WASM_SRC, 'build'));
+ process.exit(0);
+ } catch (error) {
+ if (error.code !== 'EEXIST') {
+ throw error;
+ }
+ process.exit(0);
+ }
+}
+
+if (process.argv[2] === '--docker') {
+ let cmd = `docker run --rm -it --platform=${platform.toString().trim()}`;
+ // Try to avoid root permission problems on compiled assets
+ // when running on linux.
+ // It will work flawessly if uid === gid === 1000
+ // there will be some warnings otherwise.
+ if (process.platform === 'linux') {
+ cmd += ` --user ${process.getuid()}:${process.getegid()}`;
+ }
+ cmd += ` --mount type=bind,source=${WASM_SRC}/build,target=/home/node/llhttp/build llhttp_wasm_builder npm run wasm`;
+
+ /* tslint:disable-next-line no-console */
+ console.log(`> ${cmd}\n\n`);
+ execSync(cmd, { cwd: WASM_SRC, stdio: 'inherit' });
+ process.exit(0);
+}
+
+try {
+ mkdirSync(WASM_OUT);
+} catch (error) {
+ if (error.code !== 'EEXIST') {
+ throw error;
+ }
+}
+
+// Build ts
+execSync('npm run build', { cwd: WASM_SRC, stdio: 'inherit' });
+
+// Build wasm binary
+execSync(
+ `clang \
+ --sysroot=/usr/share/wasi-sysroot \
+ -target wasm32-unknown-wasi \
+ -Ofast \
+ -fno-exceptions \
+ -fvisibility=hidden \
+ -mexec-model=reactor \
+ -Wl,-error-limit=0 \
+ -Wl,-O3 \
+ -Wl,--lto-O3 \
+ -Wl,--strip-all \
+ -Wl,--allow-undefined \
+ -Wl,--export-dynamic \
+ -Wl,--export-table \
+ -Wl,--export=malloc \
+ -Wl,--export=free \
+ -Wl,--no-entry \
+ ${join(WASM_SRC, 'build', 'c')}/*.c \
+ ${join(WASM_SRC, 'src', 'native')}/*.c \
+ -I${join(WASM_SRC, 'build')} \
+ -o ${join(WASM_OUT, 'llhttp.wasm')}`,
+ { stdio: 'inherit' },
+);
+
+// Copy constants for `.js` and `.ts` users.
+copyFileSync(join(WASM_SRC, 'lib', 'llhttp', 'constants.js'), join(WASM_OUT, 'constants.js'));
+copyFileSync(join(WASM_SRC, 'lib', 'llhttp', 'constants.js.map'), join(WASM_OUT, 'constants.js.map'));
+copyFileSync(join(WASM_SRC, 'lib', 'llhttp', 'constants.d.ts'), join(WASM_OUT, 'constants.d.ts'));
+copyFileSync(join(WASM_SRC, 'lib', 'llhttp', 'utils.js'), join(WASM_OUT, 'utils.js'));
+copyFileSync(join(WASM_SRC, 'lib', 'llhttp', 'utils.js.map'), join(WASM_OUT, 'utils.js.map'));
+copyFileSync(join(WASM_SRC, 'lib', 'llhttp', 'utils.d.ts'), join(WASM_OUT, 'utils.d.ts'));
diff --git a/llhttp/bin/generate.ts b/llhttp/bin/generate.ts
new file mode 100755
index 0000000..edb7f49
--- /dev/null
+++ b/llhttp/bin/generate.ts
@@ -0,0 +1,47 @@
+#!/usr/bin/env -S npx ts-node
+
+import { mkdirSync, readFileSync, writeFileSync } from 'fs';
+import { LLParse } from 'llparse';
+import { dirname, resolve } from 'path';
+import { parse } from 'semver';
+import { CHeaders, HTTP } from '../src/llhttp';
+
+const C_FILE = resolve(__dirname, '../build/c/llhttp.c');
+const HEADER_FILE = resolve(__dirname, '../build/llhttp.h');
+
+const pkg = JSON.parse(
+ readFileSync(resolve(__dirname, '..', 'package.json')).toString(),
+);
+const version = parse(pkg.version)!;
+const llparse = new LLParse('llhttp__internal');
+
+const cHeaders = new CHeaders();
+const nativeHeaders = readFileSync(resolve(__dirname, '../src/native/api.h'));
+const generated = llparse.build(new HTTP(llparse).build().entry, {
+ c: {
+ header: 'llhttp',
+ },
+ debug: process.env.LLPARSE_DEBUG ? 'llhttp__debug' : undefined,
+ headerGuard: 'INCLUDE_LLHTTP_ITSELF_H_',
+});
+
+const headers = `
+#ifndef INCLUDE_LLHTTP_H_
+#define INCLUDE_LLHTTP_H_
+
+#define LLHTTP_VERSION_MAJOR ${version.major}
+#define LLHTTP_VERSION_MINOR ${version.minor}
+#define LLHTTP_VERSION_PATCH ${version.patch}
+
+${generated.header}
+
+${cHeaders.build()}
+
+${nativeHeaders}
+
+#endif /* INCLUDE_LLHTTP_H_ */
+`;
+
+mkdirSync(dirname(C_FILE), { recursive: true });
+writeFileSync(HEADER_FILE, headers);
+writeFileSync(C_FILE, generated.c);
diff --git a/llhttp/docs/releasing.md b/llhttp/docs/releasing.md
new file mode 100644
index 0000000..f83e0f7
--- /dev/null
+++ b/llhttp/docs/releasing.md
@@ -0,0 +1,65 @@
+# How to release a new version of llhttp
+
+## What does releasing involves?
+
+These are the required steps to release a new version of llhttp:
+
+1. Increase the version number.
+2. Build it locally.
+3. Create a new build and push it to GitHub.
+4. Create a new release on GitHub release.
+
+> Do not try to execute the commands in the Makefile manually. This is really error-prone!
+
+## Which commands to run?
+
+First of all, make sure you have [GitHub CLI](https://cli.github.com) installed and configured. While this is not strictly necessary, it will make your life way easier.
+
+As a preliminary check, run the build command and execute the test suite locally:
+
+```
+npm run build
+npm test
+```
+
+If all goes good, you are ready to go!
+
+To release a new version of llhttp, first increase the version using `npm` and make sure it also execute the `postversion` script. Unless you have some very specific setup, this should happen automatically, which means the following command will suffice:
+
+```
+npm version [major|minor|patch]
+```
+
+The command will increase the version and then will create a new release branch on GitHub.
+
+> Even thought there is a package on NPM, it is not updated anymore. NEVER RUN `npm publish`!
+
+It's now time to create the release on GitHub. If you DON'T have GitHub CLI available, skip to the next section, otherwise run the following command:
+
+```
+npm run github-release
+```
+
+This command will create a draft release on GitHub and then show it in your browser so you can review and publish it.
+
+Congratulation, you are all set!
+
+## Create a GitHub release without GitHub CLI
+
+> From now on, `$VERSION` will be the new version you are trying to create, including the leading letter, for instance `v6.0.9`.
+
+If you don't want to or can't use GitHub CLI, you can still create the release on GitHub following this procedure.
+
+1. Go on GitHub and start creating a new release which targets tag `$VERSION`. Generate the notes using the `Generate release notes` button.
+
+2. At the bottom of the generated notes, make sure the previous and current version in the notes are correct.
+
+ The last line should be something like this: `**Full Changelog**: https://github.com/nodejs/llhttp/compare/v6.0.8...v6.0.9`
+
+ In this case it says we are creating release `v6.0.9` and we are showing the changes between `v6.0.8` and `v6.0.9`.
+
+3. Change the target of the release to point to tag `release/$VERSION`.
+
+4. Review and then publish the release.
+
+Congratulation, you are all set! \ No newline at end of file
diff --git a/llhttp/examples/wasm.ts b/llhttp/examples/wasm.ts
new file mode 100644
index 0000000..995fed8
--- /dev/null
+++ b/llhttp/examples/wasm.ts
@@ -0,0 +1,248 @@
+/**
+ * A minimal Parser that mimicks a small fraction of the Node.js parser
+ * API.
+ * To run:
+ * - `npm run build-wasm`
+ * - `npx ts-node examples/wasm.ts`
+ */
+import { readFileSync } from 'fs';
+import { resolve } from 'path';
+import * as constants from '../build/wasm/constants';
+
+const bin = readFileSync(resolve(__dirname, '../build/wasm/llhttp.wasm'));
+const mod = new WebAssembly.Module(bin);
+
+const REQUEST = constants.TYPE.REQUEST;
+const RESPONSE = constants.TYPE.RESPONSE;
+const kOnMessageBegin = 0;
+const kOnHeaders = 1;
+const kOnHeadersComplete = 2;
+const kOnBody = 3;
+const kOnMessageComplete = 4;
+const kOnExecute = 5;
+
+const kPtr = Symbol('kPtr');
+const kUrl = Symbol('kUrl');
+const kStatusMessage = Symbol('kStatusMessage');
+const kHeadersFields = Symbol('kHeadersFields');
+const kHeadersValues = Symbol('kHeadersValues');
+const kBody = Symbol('kBody');
+const kReset = Symbol('kReset');
+const kCheckErr = Symbol('kCheckErr');
+
+const cstr = (ptr: number, len: number): string =>
+ Buffer.from(memory.buffer, ptr, len).toString();
+
+const wasm_on_message_begin = (p: number) => {
+ const i = instMap.get(p);
+ i[kReset]();
+ return i[kOnMessageBegin]();
+};
+
+const wasm_on_url = (p: number, at: number, length: number) => {
+ instMap.get(p)[kUrl] = cstr(at, length);
+ return 0;
+};
+
+const wasm_on_status = (p: number, at: number, length: number) => {
+ instMap.get(p)[kStatusMessage] = cstr(at, length);
+ return 0;
+};
+
+const wasm_on_header_field = (p: number, at: number, length: number) => {
+ const i= instMap.get(p)
+ i[kHeadersFields].push(cstr(at, length));
+ return 0;
+};
+
+const wasm_on_header_value = (p: number, at: number, length: number) => {
+ const i = instMap.get(p);
+ i[kHeadersValues].push(cstr(at, length));
+ return 0;
+};
+
+const wasm_on_headers_complete = (p: number) => {
+ const i = instMap.get(p);
+ const type = get_type(p);
+ const versionMajor = get_version_major(p);
+ const versionMinor = get_version_minor(p);
+ const rawHeaders = [];
+ let method;
+ let url;
+ let statusCode;
+ let statusMessage;
+ const upgrade = get_upgrade(p);
+ const shouldKeepAlive = should_keep_alive(p);
+
+ for (let c = 0; c < i[kHeadersFields].length; c++) {
+ rawHeaders.push(i[kHeadersFields][c], i[kHeadersValues][c])
+ }
+
+ if (type === HTTPParser.REQUEST) {
+ method = constants.METHODS[get_method(p)];
+ url = i[kUrl];
+ } else if (type === HTTPParser.RESPONSE) {
+ statusCode = get_status_code(p);
+ statusMessage = i[kStatusMessage];
+ }
+ return i[kOnHeadersComplete](versionMajor, versionMinor, rawHeaders, method,
+url, statusCode, statusMessage, upgrade, shouldKeepAlive);
+};
+
+const wasm_on_body = (p: number, at: number, length: number) => {
+ const i = instMap.get(p);
+ const body = Buffer.from(memory.buffer, at, length);
+ return i[kOnBody](body);
+};
+
+const wasm_on_message_complete = (p: number) => {
+ return instMap.get(p)[kOnMessageComplete]();
+};
+
+const instMap = new Map();
+
+const inst = new WebAssembly.Instance(mod, {
+ env: {
+ wasm_on_message_begin,
+ wasm_on_url,
+ wasm_on_status,
+ wasm_on_header_field,
+ wasm_on_header_value,
+ wasm_on_headers_complete,
+ wasm_on_body,
+ wasm_on_message_complete,
+ },
+});
+
+const memory = inst.exports.memory as any;
+const alloc = inst.exports.llhttp_alloc as CallableFunction;
+const malloc = inst.exports.malloc as CallableFunction;
+const execute = inst.exports.llhttp_execute as CallableFunction;
+const get_type = inst.exports.llhttp_get_type as CallableFunction;
+const get_upgrade = inst.exports.llhttp_get_upgrade as CallableFunction;
+const should_keep_alive = inst.exports.llhttp_should_keep_alive as CallableFunction;
+const get_method = inst.exports.llhttp_get_method as CallableFunction;
+const get_status_code = inst.exports.llhttp_get_status_code as CallableFunction;
+const get_version_minor = inst.exports.llhttp_get_http_minor as CallableFunction;
+const get_version_major = inst.exports.llhttp_get_http_major as CallableFunction;
+const get_error_reason = inst.exports.llhttp_get_error_reason as CallableFunction;
+const free = inst.exports.free as CallableFunction;
+const initialize = inst.exports._initialize as CallableFunction;
+
+initialize(); // wasi reactor
+
+class HTTPParser {
+ static REQUEST = REQUEST;
+ static RESPONSE = RESPONSE;
+ static kOnMessageBegin = kOnMessageBegin;
+ static kOnHeaders = kOnHeaders;
+ static kOnHeadersComplete = kOnHeadersComplete;
+ static kOnBody = kOnBody;
+ static kOnMessageComplete = kOnMessageComplete;
+ static kOnExecute = kOnExecute;
+
+ [kPtr]: number;
+ [kUrl]: string;
+ [kStatusMessage]: null|string;
+ [kHeadersFields]: []|[string];
+ [kHeadersValues]: []|[string];
+ [kBody]: null|Buffer;
+
+ constructor(type: constants.TYPE) {
+ this[kPtr] = alloc(constants.TYPE[type]);
+ instMap.set(this[kPtr], this);
+
+ this[kUrl] = '';
+ this[kStatusMessage] = null;
+ this[kHeadersFields] = [];
+ this[kHeadersValues] = [];
+ this[kBody] = null;
+ }
+
+ [kReset]() {
+ this[kUrl] = '';
+ this[kStatusMessage] = null;
+ this[kHeadersFields] = [];
+ this[kHeadersValues] = [];
+ this[kBody] = null;
+ }
+
+ [kOnMessageBegin]() {
+ return 0;
+ }
+
+ [kOnHeaders](rawHeaders: [string]) {}
+
+ [kOnHeadersComplete](versionMajor: number, versionMinor: number, rawHeaders: [string], method: string,
+ url: string, statusCode: number, statusMessage: string, upgrade: boolean, shouldKeepAlive: boolean) {
+ return 0;
+ }
+
+ [kOnBody](body: Buffer) {
+ this[kBody] = body;
+ return 0;
+ }
+
+ [kOnMessageComplete]() {
+ return 0;
+ }
+
+ destroy() {
+ instMap.delete(this[kPtr]);
+ free(this[kPtr]);
+ }
+
+ execute(data: Buffer) {
+ const ptr = malloc(data.byteLength);
+ const u8 = new Uint8Array(memory.buffer);
+ u8.set(data, ptr);
+ const ret = execute(this[kPtr], ptr, data.length);
+ free(ptr);
+ this[kCheckErr](ret);
+ return ret;
+ }
+
+ [kCheckErr](n: number) {
+ if (n === constants.ERROR.OK) {
+ return;
+ }
+ const ptr = get_error_reason(this[kPtr]);
+ const u8 = new Uint8Array(memory.buffer);
+ const len = u8.indexOf(0, ptr) - ptr;
+ throw new Error(cstr(ptr, len));
+ }
+}
+
+
+{
+ const p = new HTTPParser(HTTPParser.REQUEST);
+
+ p.execute(Buffer.from([
+ 'POST /owo HTTP/1.1',
+ 'X: Y',
+ 'Content-Length: 9',
+ '',
+ 'uh, meow?',
+ '',
+ ].join('\r\n')));
+
+ console.log(p);
+
+ p.destroy();
+}
+
+{
+ const p = new HTTPParser(HTTPParser.RESPONSE);
+
+ p.execute(Buffer.from([
+ 'HTTP/1.1 200 OK',
+ 'X: Y',
+ 'Content-Length: 9',
+ '',
+ 'uh, meow?'
+ ].join('\r\n')));
+
+ console.log(p);
+
+ p.destroy();
+}
diff --git a/llhttp/images/http-loose-none.png b/llhttp/images/http-loose-none.png
new file mode 100644
index 0000000..3187765
--- /dev/null
+++ b/llhttp/images/http-loose-none.png
Binary files differ
diff --git a/llhttp/images/http-strict-none.png b/llhttp/images/http-strict-none.png
new file mode 100644
index 0000000..8f2aacf
--- /dev/null
+++ b/llhttp/images/http-strict-none.png
Binary files differ
diff --git a/llhttp/libllhttp.pc.in b/llhttp/libllhttp.pc.in
new file mode 100644
index 0000000..67d280a
--- /dev/null
+++ b/llhttp/libllhttp.pc.in
@@ -0,0 +1,10 @@
+prefix=@CMAKE_INSTALL_PREFIX@
+exec_prefix=@CMAKE_INSTALL_PREFIX@
+libdir=@CMAKE_INSTALL_PREFIX@/@CMAKE_INSTALL_LIBDIR@
+includedir=@CMAKE_INSTALL_PREFIX@/@CMAKE_INSTALL_INCLUDEDIR@
+
+Name: libllhttp
+Description: Node.js llhttp Library
+Version: @PROJECT_VERSION@
+Libs: -L${libdir} -lllhttp
+Cflags: -I${includedir} \ No newline at end of file
diff --git a/llhttp/package-lock.json b/llhttp/package-lock.json
new file mode 100644
index 0000000..a49ed36
--- /dev/null
+++ b/llhttp/package-lock.json
@@ -0,0 +1,2995 @@
+{
+ "name": "llhttp",
+ "version": "9.1.3",
+ "lockfileVersion": 2,
+ "requires": true,
+ "packages": {
+ "": {
+ "version": "9.1.3",
+ "license": "MIT",
+ "dependencies": {
+ "@types/semver": "^5.5.0",
+ "llparse": "^7.1.1",
+ "semver": "^5.7.1"
+ },
+ "devDependencies": {
+ "@types/mocha": "^5.2.7",
+ "@types/node": "^10.17.52",
+ "javascript-stringify": "^2.0.1",
+ "llparse-dot": "^1.0.1",
+ "llparse-test-fixture": "^5.0.1",
+ "mdgator": "^1.1.2",
+ "mocha": "^10.2.0",
+ "ts-node": "^7.0.1",
+ "tslint": "^5.20.1",
+ "typescript": "^3.9.9"
+ }
+ },
+ "node_modules/@babel/code-frame": {
+ "version": "7.12.13",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.13.tgz",
+ "integrity": "sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==",
+ "dev": true,
+ "dependencies": {
+ "@babel/highlight": "^7.12.13"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.14.0",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.0.tgz",
+ "integrity": "sha512-V3ts7zMSu5lfiwWDVWzRDGIN+lnCEUdaXgtVHJgLb1rGaA6jMrtB9EmE7L18foXJIE8Un/A/h6NJfGQp/e1J4A==",
+ "dev": true
+ },
+ "node_modules/@babel/highlight": {
+ "version": "7.14.0",
+ "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.0.tgz",
+ "integrity": "sha512-YSCOwxvTYEIMSGaBQb5kDDsCopDdiUGsqpatp3fOlI4+2HQSkTmEVWnVuySdAC5EWCqSWWTv0ib63RjR7dTBdg==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.14.0",
+ "chalk": "^2.0.0",
+ "js-tokens": "^4.0.0"
+ }
+ },
+ "node_modules/@types/debug": {
+ "version": "4.1.5",
+ "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.5.tgz",
+ "integrity": "sha512-Q1y515GcOdTHgagaVFhHnIFQ38ygs/kmxdNpvpou+raI9UO3YZcHDngBSYKQklcKlvA7iuQlmIKbzvmxcOE9CQ=="
+ },
+ "node_modules/@types/markdown-it": {
+ "version": "0.0.4",
+ "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-0.0.4.tgz",
+ "integrity": "sha512-FWR7QB7EqBRq1s9BMk0ccOSOuRLfVEWYpHQYpFPaXtCoqN6dJx2ttdsdQbUxLLnAlKpYeVjveGGhQ3583TTa7g==",
+ "dev": true
+ },
+ "node_modules/@types/mocha": {
+ "version": "5.2.7",
+ "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-5.2.7.tgz",
+ "integrity": "sha512-NYrtPht0wGzhwe9+/idPaBB+TqkY9AhTvOLMkThm0IoEfLaiVQZwBwyJ5puCkO3AUCWrmcoePjp2mbFocKy4SQ==",
+ "dev": true
+ },
+ "node_modules/@types/node": {
+ "version": "10.17.59",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.59.tgz",
+ "integrity": "sha512-7Uc8IRrL8yZz5ti45RaFxpbU8TxlzdC3HvxV+hOWo1EyLsuKv/w7y0n+TwZzwL3vdx3oZ2k3ubxPq131hNtXyg==",
+ "dev": true
+ },
+ "node_modules/@types/semver": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/@types/semver/-/semver-5.5.0.tgz",
+ "integrity": "sha512-41qEJgBH/TWgo5NFSvBCJ1qkoi3Q6ONSF2avrHq1LVEZfYpdHmj0y9SuTK+u9ZhG1sYQKBL1AWXKyLWP4RaUoQ=="
+ },
+ "node_modules/ansi-colors": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz",
+ "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+ "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+ "dev": true,
+ "dependencies": {
+ "color-convert": "^1.9.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/anymatch": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
+ "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
+ "dev": true,
+ "dependencies": {
+ "normalize-path": "^3.0.0",
+ "picomatch": "^2.0.4"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/argparse": {
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
+ "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
+ "dev": true,
+ "dependencies": {
+ "sprintf-js": "~1.0.2"
+ }
+ },
+ "node_modules/arrify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz",
+ "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true
+ },
+ "node_modules/binary-extensions": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
+ "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/binary-search": {
+ "version": "1.3.6",
+ "resolved": "https://registry.npmjs.org/binary-search/-/binary-search-1.3.6.tgz",
+ "integrity": "sha512-nbE1WxOTTrUWIfsfZ4aHGYu5DOuNkbxGokjV6Z2kxfJK3uaAb8zNK1muzOeipoLHZjInT4Br88BHpzevc681xA=="
+ },
+ "node_modules/brace-expansion": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "dev": true,
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/braces": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
+ "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
+ "dev": true,
+ "dependencies": {
+ "fill-range": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/browser-stdout": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz",
+ "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==",
+ "dev": true
+ },
+ "node_modules/buffer-from": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz",
+ "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==",
+ "dev": true
+ },
+ "node_modules/builtin-modules": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz",
+ "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/camelcase": {
+ "version": "5.3.1",
+ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
+ "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/chalk": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
+ "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^3.2.1",
+ "escape-string-regexp": "^1.0.5",
+ "supports-color": "^5.3.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/chalk/node_modules/supports-color": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/chokidar": {
+ "version": "3.5.3",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
+ "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://paulmillr.com/funding/"
+ }
+ ],
+ "dependencies": {
+ "anymatch": "~3.1.2",
+ "braces": "~3.0.2",
+ "glob-parent": "~5.1.2",
+ "is-binary-path": "~2.1.0",
+ "is-glob": "~4.0.1",
+ "normalize-path": "~3.0.0",
+ "readdirp": "~3.6.0"
+ },
+ "engines": {
+ "node": ">= 8.10.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/cliui": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
+ "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
+ "dev": true,
+ "dependencies": {
+ "string-width": "^4.2.0",
+ "strip-ansi": "^6.0.0",
+ "wrap-ansi": "^6.2.0"
+ }
+ },
+ "node_modules/color-convert": {
+ "version": "1.9.3",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
+ "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
+ "dev": true,
+ "dependencies": {
+ "color-name": "1.1.3"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
+ "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
+ "dev": true
+ },
+ "node_modules/commander": {
+ "version": "2.20.3",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
+ "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
+ "dev": true
+ },
+ "node_modules/concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
+ "dev": true
+ },
+ "node_modules/debug": {
+ "version": "4.3.4",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
+ "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
+ "dependencies": {
+ "ms": "2.1.2"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/decamelize": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
+ "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/diff": {
+ "version": "3.5.0",
+ "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz",
+ "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.3.1"
+ }
+ },
+ "node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true
+ },
+ "node_modules/entities": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz",
+ "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==",
+ "dev": true
+ },
+ "node_modules/escalade": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
+ "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/escape-string-regexp": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+ "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
+ "node_modules/esm": {
+ "version": "3.2.25",
+ "resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz",
+ "integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/esprima": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
+ "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
+ "dev": true,
+ "bin": {
+ "esparse": "bin/esparse.js",
+ "esvalidate": "bin/esvalidate.js"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/fill-range": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
+ "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
+ "dev": true,
+ "dependencies": {
+ "to-regex-range": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/find-up": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+ "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+ "dev": true,
+ "dependencies": {
+ "locate-path": "^6.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/flat": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz",
+ "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==",
+ "dev": true,
+ "bin": {
+ "flat": "cli.js"
+ }
+ },
+ "node_modules/fs.realpath": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+ "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
+ "dev": true
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
+ "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
+ "dev": true,
+ "hasInstallScript": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
+ "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
+ "dev": true
+ },
+ "node_modules/get-caller-file": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
+ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
+ "dev": true,
+ "engines": {
+ "node": "6.* || 8.* || >= 10.*"
+ }
+ },
+ "node_modules/glob": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz",
+ "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==",
+ "dev": true,
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.0.4",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ },
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "dev": true,
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/has": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
+ "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
+ "dev": true,
+ "dependencies": {
+ "function-bind": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4.0"
+ }
+ },
+ "node_modules/has-flag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+ "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/he": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
+ "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
+ "dev": true,
+ "bin": {
+ "he": "bin/he"
+ }
+ },
+ "node_modules/inflight": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+ "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
+ "dev": true,
+ "dependencies": {
+ "once": "^1.3.0",
+ "wrappy": "1"
+ }
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "dev": true
+ },
+ "node_modules/is-binary-path": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+ "dev": true,
+ "dependencies": {
+ "binary-extensions": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-core-module": {
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.4.0.tgz",
+ "integrity": "sha512-6A2fkfq1rfeQZjxrZJGerpLCTHRNEBiSgnu0+obeJpEPZRUooHgsizvzv0ZjJwOz3iWIHdJtVWJ/tmPr3D21/A==",
+ "dev": true,
+ "dependencies": {
+ "has": "^1.0.3"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dev": true,
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/is-plain-obj": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz",
+ "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-unicode-supported": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz",
+ "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/javascript-stringify": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/javascript-stringify/-/javascript-stringify-2.1.0.tgz",
+ "integrity": "sha512-JVAfqNPTvNq3sB/VHQJAFxN/sPgKnsKrCwyRt15zwNCdrMMJDdcEOdubuy+DuJYYdm0ox1J4uzEuYKkN+9yhVg==",
+ "dev": true
+ },
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "dev": true
+ },
+ "node_modules/js-yaml": {
+ "version": "3.13.1",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz",
+ "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==",
+ "dev": true,
+ "dependencies": {
+ "argparse": "^1.0.7",
+ "esprima": "^4.0.0"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/linkify-it": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-2.2.0.tgz",
+ "integrity": "sha512-GnAl/knGn+i1U/wjBz3akz2stz+HrHLsxMwHQGofCDfPvlf+gDKN58UtfmUquTY4/MXeE2x7k19KQmeoZi94Iw==",
+ "dev": true,
+ "dependencies": {
+ "uc.micro": "^1.0.1"
+ }
+ },
+ "node_modules/llparse": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/llparse/-/llparse-7.1.1.tgz",
+ "integrity": "sha512-lBxN5O6sKq6KSOaRFIGczoVpO/U/37mHhjJioQbPuiXdfZmwzP1zC3txV9xx778TRNFENzeCM0Uoo+mE1rfJOA==",
+ "dependencies": {
+ "debug": "^4.2.0",
+ "llparse-frontend": "^3.0.0"
+ }
+ },
+ "node_modules/llparse-builder": {
+ "version": "1.5.2",
+ "resolved": "https://registry.npmjs.org/llparse-builder/-/llparse-builder-1.5.2.tgz",
+ "integrity": "sha512-i862UNC3YUEdlfK/NUCJxlKjtWjgAI9AJXDRgjcfRHfwFt4Sf8eFPTRsc91/2R9MBZ0kyFdfhi8SVhMsZf1gNQ==",
+ "dependencies": {
+ "@types/debug": "4.1.5 ",
+ "binary-search": "^1.3.6",
+ "debug": "^4.2.0"
+ }
+ },
+ "node_modules/llparse-dot": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/llparse-dot/-/llparse-dot-1.0.1.tgz",
+ "integrity": "sha512-3e271C2LuDWBzhxaCUDzjpufamoEBuTYQz83QyMixI/i99BntCEk6ngHWOhhDb0XdtNNh6qAfRmXyjgNP+Nxpw==",
+ "dev": true,
+ "dependencies": {
+ "llparse-builder": "^1.0.0"
+ }
+ },
+ "node_modules/llparse-frontend": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/llparse-frontend/-/llparse-frontend-3.0.0.tgz",
+ "integrity": "sha512-G/o0Po2C+G5OtP8MJeQDjDf5qwDxcO7K6x4r6jqGsJwxk7yblbJnRqpmye7G/lZ8dD0Hv5neY4/KB5BhDmEc9Q==",
+ "dependencies": {
+ "debug": "^3.2.6",
+ "llparse-builder": "^1.5.2"
+ }
+ },
+ "node_modules/llparse-frontend/node_modules/debug": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+ "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+ "dependencies": {
+ "ms": "^2.1.1"
+ }
+ },
+ "node_modules/llparse-test-fixture": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/llparse-test-fixture/-/llparse-test-fixture-5.0.2.tgz",
+ "integrity": "sha512-61KI5J/b5uyRktD0y1EezleEW6UfaxhHkn1adLKNVemRZzklE+SpLakr251qo04kb9jN/ytk8lllgK+yFOj4cQ==",
+ "dev": true,
+ "dependencies": {
+ "esm": "^3.2.25",
+ "llparse": "^7.0.0",
+ "yargs": "^15.4.1"
+ }
+ },
+ "node_modules/locate-path": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+ "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+ "dev": true,
+ "dependencies": {
+ "p-locate": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/log-symbols": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz",
+ "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==",
+ "dev": true,
+ "dependencies": {
+ "chalk": "^4.1.0",
+ "is-unicode-supported": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/log-symbols/node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/log-symbols/node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/log-symbols/node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/log-symbols/node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
+ "node_modules/log-symbols/node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/log-symbols/node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/make-error": {
+ "version": "1.3.6",
+ "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
+ "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
+ "dev": true
+ },
+ "node_modules/markdown-it": {
+ "version": "8.4.2",
+ "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-8.4.2.tgz",
+ "integrity": "sha512-GcRz3AWTqSUphY3vsUqQSFMbgR38a4Lh3GWlHRh/7MRwz8mcu9n2IO7HOh+bXHrR9kOPDl5RNCaEsrneb+xhHQ==",
+ "dev": true,
+ "dependencies": {
+ "argparse": "^1.0.7",
+ "entities": "~1.1.1",
+ "linkify-it": "^2.0.0",
+ "mdurl": "^1.0.1",
+ "uc.micro": "^1.0.5"
+ },
+ "bin": {
+ "markdown-it": "bin/markdown-it.js"
+ }
+ },
+ "node_modules/mdgator": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/mdgator/-/mdgator-1.1.2.tgz",
+ "integrity": "sha512-S2GvsLIznUQ2McXfpe6BCD+IqhnRuHcBO7krqnvnsHgDpjjO1mLhr0vZtVa5ca4WZET037g3G+94DznpicKkOA==",
+ "dev": true,
+ "dependencies": {
+ "@types/markdown-it": "0.0.4",
+ "markdown-it": "^8.4.1"
+ }
+ },
+ "node_modules/mdurl": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz",
+ "integrity": "sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4=",
+ "dev": true
+ },
+ "node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/minimist": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
+ "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==",
+ "dev": true
+ },
+ "node_modules/mkdirp": {
+ "version": "0.5.5",
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
+ "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
+ "dev": true,
+ "dependencies": {
+ "minimist": "^1.2.5"
+ },
+ "bin": {
+ "mkdirp": "bin/cmd.js"
+ }
+ },
+ "node_modules/mocha": {
+ "version": "10.2.0",
+ "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.2.0.tgz",
+ "integrity": "sha512-IDY7fl/BecMwFHzoqF2sg/SHHANeBoMMXFlS9r0OXKDssYE1M5O43wUY/9BVPeIvfH2zmEbBfseqN9gBQZzXkg==",
+ "dev": true,
+ "dependencies": {
+ "ansi-colors": "4.1.1",
+ "browser-stdout": "1.3.1",
+ "chokidar": "3.5.3",
+ "debug": "4.3.4",
+ "diff": "5.0.0",
+ "escape-string-regexp": "4.0.0",
+ "find-up": "5.0.0",
+ "glob": "7.2.0",
+ "he": "1.2.0",
+ "js-yaml": "4.1.0",
+ "log-symbols": "4.1.0",
+ "minimatch": "5.0.1",
+ "ms": "2.1.3",
+ "nanoid": "3.3.3",
+ "serialize-javascript": "6.0.0",
+ "strip-json-comments": "3.1.1",
+ "supports-color": "8.1.1",
+ "workerpool": "6.2.1",
+ "yargs": "16.2.0",
+ "yargs-parser": "20.2.4",
+ "yargs-unparser": "2.0.0"
+ },
+ "bin": {
+ "_mocha": "bin/_mocha",
+ "mocha": "bin/mocha.js"
+ },
+ "engines": {
+ "node": ">= 14.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/mochajs"
+ }
+ },
+ "node_modules/mocha/node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/mocha/node_modules/argparse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "dev": true
+ },
+ "node_modules/mocha/node_modules/brace-expansion": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
+ "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
+ "dev": true,
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/mocha/node_modules/cliui": {
+ "version": "7.0.4",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz",
+ "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==",
+ "dev": true,
+ "dependencies": {
+ "string-width": "^4.2.0",
+ "strip-ansi": "^6.0.0",
+ "wrap-ansi": "^7.0.0"
+ }
+ },
+ "node_modules/mocha/node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/mocha/node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
+ "node_modules/mocha/node_modules/diff": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz",
+ "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.3.1"
+ }
+ },
+ "node_modules/mocha/node_modules/escape-string-regexp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/mocha/node_modules/js-yaml": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
+ "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+ "dev": true,
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/mocha/node_modules/minimatch": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz",
+ "integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==",
+ "dev": true,
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/mocha/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true
+ },
+ "node_modules/mocha/node_modules/wrap-ansi": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/mocha/node_modules/y18n": {
+ "version": "5.0.8",
+ "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
+ "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/mocha/node_modules/yargs": {
+ "version": "16.2.0",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz",
+ "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==",
+ "dev": true,
+ "dependencies": {
+ "cliui": "^7.0.2",
+ "escalade": "^3.1.1",
+ "get-caller-file": "^2.0.5",
+ "require-directory": "^2.1.1",
+ "string-width": "^4.2.0",
+ "y18n": "^5.0.5",
+ "yargs-parser": "^20.2.2"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz",
+ "integrity": "sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==",
+ "dev": true,
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/normalize-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
+ "dev": true,
+ "dependencies": {
+ "wrappy": "1"
+ }
+ },
+ "node_modules/p-limit": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
+ "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
+ "dev": true,
+ "dependencies": {
+ "p-try": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-locate": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+ "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+ "dev": true,
+ "dependencies": {
+ "p-limit": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-locate/node_modules/p-limit": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+ "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+ "dev": true,
+ "dependencies": {
+ "yocto-queue": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-try": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
+ "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-is-absolute": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+ "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/path-parse": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
+ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
+ "dev": true
+ },
+ "node_modules/picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "dev": true,
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/randombytes": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
+ "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
+ "dev": true,
+ "dependencies": {
+ "safe-buffer": "^5.1.0"
+ }
+ },
+ "node_modules/readdirp": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+ "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+ "dev": true,
+ "dependencies": {
+ "picomatch": "^2.2.1"
+ },
+ "engines": {
+ "node": ">=8.10.0"
+ }
+ },
+ "node_modules/require-directory": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
+ "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/require-main-filename": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
+ "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
+ "dev": true
+ },
+ "node_modules/resolve": {
+ "version": "1.20.0",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz",
+ "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==",
+ "dev": true,
+ "dependencies": {
+ "is-core-module": "^2.2.0",
+ "path-parse": "^1.0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ]
+ },
+ "node_modules/semver": {
+ "version": "5.7.2",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
+ "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
+ "bin": {
+ "semver": "bin/semver"
+ }
+ },
+ "node_modules/serialize-javascript": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz",
+ "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==",
+ "dev": true,
+ "dependencies": {
+ "randombytes": "^2.1.0"
+ }
+ },
+ "node_modules/set-blocking": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
+ "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=",
+ "dev": true
+ },
+ "node_modules/source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/source-map-support": {
+ "version": "0.5.19",
+ "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz",
+ "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==",
+ "dev": true,
+ "dependencies": {
+ "buffer-from": "^1.0.0",
+ "source-map": "^0.6.0"
+ }
+ },
+ "node_modules/sprintf-js": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
+ "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=",
+ "dev": true
+ },
+ "node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-json-comments": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/supports-color": {
+ "version": "8.1.1",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
+ "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/supports-color?sponsor=1"
+ }
+ },
+ "node_modules/supports-color/node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "dev": true,
+ "dependencies": {
+ "is-number": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/ts-node": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-7.0.1.tgz",
+ "integrity": "sha512-BVwVbPJRspzNh2yfslyT1PSbl5uIk03EZlb493RKHN4qej/D06n1cEhjlOJG69oFsE7OT8XjpTUcYf6pKTLMhw==",
+ "dev": true,
+ "dependencies": {
+ "arrify": "^1.0.0",
+ "buffer-from": "^1.1.0",
+ "diff": "^3.1.0",
+ "make-error": "^1.1.1",
+ "minimist": "^1.2.0",
+ "mkdirp": "^0.5.1",
+ "source-map-support": "^0.5.6",
+ "yn": "^2.0.0"
+ },
+ "bin": {
+ "ts-node": "dist/bin.js"
+ },
+ "engines": {
+ "node": ">=4.2.0"
+ }
+ },
+ "node_modules/tslib": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
+ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
+ "dev": true
+ },
+ "node_modules/tslint": {
+ "version": "5.20.1",
+ "resolved": "https://registry.npmjs.org/tslint/-/tslint-5.20.1.tgz",
+ "integrity": "sha512-EcMxhzCFt8k+/UP5r8waCf/lzmeSyVlqxqMEDQE7rWYiQky8KpIBz1JAoYXfROHrPZ1XXd43q8yQnULOLiBRQg==",
+ "dev": true,
+ "dependencies": {
+ "@babel/code-frame": "^7.0.0",
+ "builtin-modules": "^1.1.1",
+ "chalk": "^2.3.0",
+ "commander": "^2.12.1",
+ "diff": "^4.0.1",
+ "glob": "^7.1.1",
+ "js-yaml": "^3.13.1",
+ "minimatch": "^3.0.4",
+ "mkdirp": "^0.5.1",
+ "resolve": "^1.3.2",
+ "semver": "^5.3.0",
+ "tslib": "^1.8.0",
+ "tsutils": "^2.29.0"
+ },
+ "bin": {
+ "tslint": "bin/tslint"
+ },
+ "engines": {
+ "node": ">=4.8.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=2.3.0-dev || >=2.4.0-dev || >=2.5.0-dev || >=2.6.0-dev || >=2.7.0-dev || >=2.8.0-dev || >=2.9.0-dev || >=3.0.0-dev || >= 3.1.0-dev || >= 3.2.0-dev"
+ }
+ },
+ "node_modules/tslint/node_modules/diff": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
+ "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.3.1"
+ }
+ },
+ "node_modules/tsutils": {
+ "version": "2.29.0",
+ "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.29.0.tgz",
+ "integrity": "sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA==",
+ "dev": true,
+ "dependencies": {
+ "tslib": "^1.8.1"
+ },
+ "peerDependencies": {
+ "typescript": ">=2.1.0 || >=2.1.0-dev || >=2.2.0-dev || >=2.3.0-dev || >=2.4.0-dev || >=2.5.0-dev || >=2.6.0-dev || >=2.7.0-dev || >=2.8.0-dev || >=2.9.0-dev || >= 3.0.0-dev || >= 3.1.0-dev"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "3.9.9",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.9.tgz",
+ "integrity": "sha512-kdMjTiekY+z/ubJCATUPlRDl39vXYiMV9iyeMuEuXZh2we6zz80uovNN2WlAxmmdE/Z/YQe+EbOEXB5RHEED3w==",
+ "dev": true,
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=4.2.0"
+ }
+ },
+ "node_modules/uc.micro": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz",
+ "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==",
+ "dev": true
+ },
+ "node_modules/which-module": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz",
+ "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=",
+ "dev": true
+ },
+ "node_modules/workerpool": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz",
+ "integrity": "sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==",
+ "dev": true
+ },
+ "node_modules/wrap-ansi": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
+ "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/wrap-ansi/node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/wrap-ansi/node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/wrap-ansi/node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
+ "node_modules/wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
+ "dev": true
+ },
+ "node_modules/y18n": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
+ "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==",
+ "dev": true
+ },
+ "node_modules/yargs": {
+ "version": "15.4.1",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
+ "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
+ "dev": true,
+ "dependencies": {
+ "cliui": "^6.0.0",
+ "decamelize": "^1.2.0",
+ "find-up": "^4.1.0",
+ "get-caller-file": "^2.0.1",
+ "require-directory": "^2.1.1",
+ "require-main-filename": "^2.0.0",
+ "set-blocking": "^2.0.0",
+ "string-width": "^4.2.0",
+ "which-module": "^2.0.0",
+ "y18n": "^4.0.0",
+ "yargs-parser": "^18.1.2"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/yargs-parser": {
+ "version": "20.2.4",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz",
+ "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/yargs-unparser": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz",
+ "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==",
+ "dev": true,
+ "dependencies": {
+ "camelcase": "^6.0.0",
+ "decamelize": "^4.0.0",
+ "flat": "^5.0.2",
+ "is-plain-obj": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/yargs-unparser/node_modules/camelcase": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz",
+ "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/yargs-unparser/node_modules/decamelize": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz",
+ "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/yargs/node_modules/find-up": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
+ "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
+ "dev": true,
+ "dependencies": {
+ "locate-path": "^5.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/yargs/node_modules/locate-path": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
+ "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
+ "dev": true,
+ "dependencies": {
+ "p-locate": "^4.1.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/yargs/node_modules/p-locate": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
+ "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
+ "dev": true,
+ "dependencies": {
+ "p-limit": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/yargs/node_modules/yargs-parser": {
+ "version": "18.1.3",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
+ "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
+ "dev": true,
+ "dependencies": {
+ "camelcase": "^5.0.0",
+ "decamelize": "^1.2.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/yn": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/yn/-/yn-2.0.0.tgz",
+ "integrity": "sha1-5a2ryKz0CPY4X8dklWhMiOavaJo=",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/yocto-queue": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ }
+ },
+ "dependencies": {
+ "@babel/code-frame": {
+ "version": "7.12.13",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.13.tgz",
+ "integrity": "sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==",
+ "dev": true,
+ "requires": {
+ "@babel/highlight": "^7.12.13"
+ }
+ },
+ "@babel/helper-validator-identifier": {
+ "version": "7.14.0",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.0.tgz",
+ "integrity": "sha512-V3ts7zMSu5lfiwWDVWzRDGIN+lnCEUdaXgtVHJgLb1rGaA6jMrtB9EmE7L18foXJIE8Un/A/h6NJfGQp/e1J4A==",
+ "dev": true
+ },
+ "@babel/highlight": {
+ "version": "7.14.0",
+ "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.0.tgz",
+ "integrity": "sha512-YSCOwxvTYEIMSGaBQb5kDDsCopDdiUGsqpatp3fOlI4+2HQSkTmEVWnVuySdAC5EWCqSWWTv0ib63RjR7dTBdg==",
+ "dev": true,
+ "requires": {
+ "@babel/helper-validator-identifier": "^7.14.0",
+ "chalk": "^2.0.0",
+ "js-tokens": "^4.0.0"
+ }
+ },
+ "@types/debug": {
+ "version": "4.1.5",
+ "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.5.tgz",
+ "integrity": "sha512-Q1y515GcOdTHgagaVFhHnIFQ38ygs/kmxdNpvpou+raI9UO3YZcHDngBSYKQklcKlvA7iuQlmIKbzvmxcOE9CQ=="
+ },
+ "@types/markdown-it": {
+ "version": "0.0.4",
+ "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-0.0.4.tgz",
+ "integrity": "sha512-FWR7QB7EqBRq1s9BMk0ccOSOuRLfVEWYpHQYpFPaXtCoqN6dJx2ttdsdQbUxLLnAlKpYeVjveGGhQ3583TTa7g==",
+ "dev": true
+ },
+ "@types/mocha": {
+ "version": "5.2.7",
+ "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-5.2.7.tgz",
+ "integrity": "sha512-NYrtPht0wGzhwe9+/idPaBB+TqkY9AhTvOLMkThm0IoEfLaiVQZwBwyJ5puCkO3AUCWrmcoePjp2mbFocKy4SQ==",
+ "dev": true
+ },
+ "@types/node": {
+ "version": "10.17.59",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.59.tgz",
+ "integrity": "sha512-7Uc8IRrL8yZz5ti45RaFxpbU8TxlzdC3HvxV+hOWo1EyLsuKv/w7y0n+TwZzwL3vdx3oZ2k3ubxPq131hNtXyg==",
+ "dev": true
+ },
+ "@types/semver": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/@types/semver/-/semver-5.5.0.tgz",
+ "integrity": "sha512-41qEJgBH/TWgo5NFSvBCJ1qkoi3Q6ONSF2avrHq1LVEZfYpdHmj0y9SuTK+u9ZhG1sYQKBL1AWXKyLWP4RaUoQ=="
+ },
+ "ansi-colors": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz",
+ "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==",
+ "dev": true
+ },
+ "ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true
+ },
+ "ansi-styles": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+ "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+ "dev": true,
+ "requires": {
+ "color-convert": "^1.9.0"
+ }
+ },
+ "anymatch": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
+ "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
+ "dev": true,
+ "requires": {
+ "normalize-path": "^3.0.0",
+ "picomatch": "^2.0.4"
+ }
+ },
+ "argparse": {
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
+ "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
+ "dev": true,
+ "requires": {
+ "sprintf-js": "~1.0.2"
+ }
+ },
+ "arrify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz",
+ "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=",
+ "dev": true
+ },
+ "balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true
+ },
+ "binary-extensions": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
+ "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
+ "dev": true
+ },
+ "binary-search": {
+ "version": "1.3.6",
+ "resolved": "https://registry.npmjs.org/binary-search/-/binary-search-1.3.6.tgz",
+ "integrity": "sha512-nbE1WxOTTrUWIfsfZ4aHGYu5DOuNkbxGokjV6Z2kxfJK3uaAb8zNK1muzOeipoLHZjInT4Br88BHpzevc681xA=="
+ },
+ "brace-expansion": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "dev": true,
+ "requires": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "braces": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
+ "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
+ "dev": true,
+ "requires": {
+ "fill-range": "^7.0.1"
+ }
+ },
+ "browser-stdout": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz",
+ "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==",
+ "dev": true
+ },
+ "buffer-from": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz",
+ "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==",
+ "dev": true
+ },
+ "builtin-modules": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz",
+ "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=",
+ "dev": true
+ },
+ "camelcase": {
+ "version": "5.3.1",
+ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
+ "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
+ "dev": true
+ },
+ "chalk": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
+ "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
+ "dev": true,
+ "requires": {
+ "ansi-styles": "^3.2.1",
+ "escape-string-regexp": "^1.0.5",
+ "supports-color": "^5.3.0"
+ },
+ "dependencies": {
+ "supports-color": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+ "dev": true,
+ "requires": {
+ "has-flag": "^3.0.0"
+ }
+ }
+ }
+ },
+ "chokidar": {
+ "version": "3.5.3",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
+ "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==",
+ "dev": true,
+ "requires": {
+ "anymatch": "~3.1.2",
+ "braces": "~3.0.2",
+ "fsevents": "~2.3.2",
+ "glob-parent": "~5.1.2",
+ "is-binary-path": "~2.1.0",
+ "is-glob": "~4.0.1",
+ "normalize-path": "~3.0.0",
+ "readdirp": "~3.6.0"
+ }
+ },
+ "cliui": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
+ "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
+ "dev": true,
+ "requires": {
+ "string-width": "^4.2.0",
+ "strip-ansi": "^6.0.0",
+ "wrap-ansi": "^6.2.0"
+ }
+ },
+ "color-convert": {
+ "version": "1.9.3",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
+ "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
+ "dev": true,
+ "requires": {
+ "color-name": "1.1.3"
+ }
+ },
+ "color-name": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
+ "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
+ "dev": true
+ },
+ "commander": {
+ "version": "2.20.3",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
+ "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
+ "dev": true
+ },
+ "concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
+ "dev": true
+ },
+ "debug": {
+ "version": "4.3.4",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
+ "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
+ "requires": {
+ "ms": "2.1.2"
+ }
+ },
+ "decamelize": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
+ "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=",
+ "dev": true
+ },
+ "diff": {
+ "version": "3.5.0",
+ "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz",
+ "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==",
+ "dev": true
+ },
+ "emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true
+ },
+ "entities": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz",
+ "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==",
+ "dev": true
+ },
+ "escalade": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
+ "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==",
+ "dev": true
+ },
+ "escape-string-regexp": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+ "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=",
+ "dev": true
+ },
+ "esm": {
+ "version": "3.2.25",
+ "resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz",
+ "integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==",
+ "dev": true
+ },
+ "esprima": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
+ "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
+ "dev": true
+ },
+ "fill-range": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
+ "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
+ "dev": true,
+ "requires": {
+ "to-regex-range": "^5.0.1"
+ }
+ },
+ "find-up": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+ "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+ "dev": true,
+ "requires": {
+ "locate-path": "^6.0.0",
+ "path-exists": "^4.0.0"
+ }
+ },
+ "flat": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz",
+ "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==",
+ "dev": true
+ },
+ "fs.realpath": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+ "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
+ "dev": true
+ },
+ "fsevents": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
+ "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
+ "dev": true,
+ "optional": true
+ },
+ "function-bind": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
+ "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
+ "dev": true
+ },
+ "get-caller-file": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
+ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
+ "dev": true
+ },
+ "glob": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz",
+ "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==",
+ "dev": true,
+ "requires": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.0.4",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ }
+ },
+ "glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "dev": true,
+ "requires": {
+ "is-glob": "^4.0.1"
+ }
+ },
+ "has": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
+ "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
+ "dev": true,
+ "requires": {
+ "function-bind": "^1.1.1"
+ }
+ },
+ "has-flag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+ "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
+ "dev": true
+ },
+ "he": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
+ "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
+ "dev": true
+ },
+ "inflight": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+ "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
+ "dev": true,
+ "requires": {
+ "once": "^1.3.0",
+ "wrappy": "1"
+ }
+ },
+ "inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "dev": true
+ },
+ "is-binary-path": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+ "dev": true,
+ "requires": {
+ "binary-extensions": "^2.0.0"
+ }
+ },
+ "is-core-module": {
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.4.0.tgz",
+ "integrity": "sha512-6A2fkfq1rfeQZjxrZJGerpLCTHRNEBiSgnu0+obeJpEPZRUooHgsizvzv0ZjJwOz3iWIHdJtVWJ/tmPr3D21/A==",
+ "dev": true,
+ "requires": {
+ "has": "^1.0.3"
+ }
+ },
+ "is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "dev": true
+ },
+ "is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "dev": true
+ },
+ "is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dev": true,
+ "requires": {
+ "is-extglob": "^2.1.1"
+ }
+ },
+ "is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "dev": true
+ },
+ "is-plain-obj": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz",
+ "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==",
+ "dev": true
+ },
+ "is-unicode-supported": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz",
+ "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==",
+ "dev": true
+ },
+ "javascript-stringify": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/javascript-stringify/-/javascript-stringify-2.1.0.tgz",
+ "integrity": "sha512-JVAfqNPTvNq3sB/VHQJAFxN/sPgKnsKrCwyRt15zwNCdrMMJDdcEOdubuy+DuJYYdm0ox1J4uzEuYKkN+9yhVg==",
+ "dev": true
+ },
+ "js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "dev": true
+ },
+ "js-yaml": {
+ "version": "3.13.1",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz",
+ "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==",
+ "dev": true,
+ "requires": {
+ "argparse": "^1.0.7",
+ "esprima": "^4.0.0"
+ }
+ },
+ "linkify-it": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-2.2.0.tgz",
+ "integrity": "sha512-GnAl/knGn+i1U/wjBz3akz2stz+HrHLsxMwHQGofCDfPvlf+gDKN58UtfmUquTY4/MXeE2x7k19KQmeoZi94Iw==",
+ "dev": true,
+ "requires": {
+ "uc.micro": "^1.0.1"
+ }
+ },
+ "llparse": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/llparse/-/llparse-7.1.1.tgz",
+ "integrity": "sha512-lBxN5O6sKq6KSOaRFIGczoVpO/U/37mHhjJioQbPuiXdfZmwzP1zC3txV9xx778TRNFENzeCM0Uoo+mE1rfJOA==",
+ "requires": {
+ "debug": "^4.2.0",
+ "llparse-frontend": "^3.0.0"
+ }
+ },
+ "llparse-builder": {
+ "version": "1.5.2",
+ "resolved": "https://registry.npmjs.org/llparse-builder/-/llparse-builder-1.5.2.tgz",
+ "integrity": "sha512-i862UNC3YUEdlfK/NUCJxlKjtWjgAI9AJXDRgjcfRHfwFt4Sf8eFPTRsc91/2R9MBZ0kyFdfhi8SVhMsZf1gNQ==",
+ "requires": {
+ "@types/debug": "4.1.5 ",
+ "binary-search": "^1.3.6",
+ "debug": "^4.2.0"
+ }
+ },
+ "llparse-dot": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/llparse-dot/-/llparse-dot-1.0.1.tgz",
+ "integrity": "sha512-3e271C2LuDWBzhxaCUDzjpufamoEBuTYQz83QyMixI/i99BntCEk6ngHWOhhDb0XdtNNh6qAfRmXyjgNP+Nxpw==",
+ "dev": true,
+ "requires": {
+ "llparse-builder": "^1.0.0"
+ }
+ },
+ "llparse-frontend": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/llparse-frontend/-/llparse-frontend-3.0.0.tgz",
+ "integrity": "sha512-G/o0Po2C+G5OtP8MJeQDjDf5qwDxcO7K6x4r6jqGsJwxk7yblbJnRqpmye7G/lZ8dD0Hv5neY4/KB5BhDmEc9Q==",
+ "requires": {
+ "debug": "^3.2.6",
+ "llparse-builder": "^1.5.2"
+ },
+ "dependencies": {
+ "debug": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+ "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+ "requires": {
+ "ms": "^2.1.1"
+ }
+ }
+ }
+ },
+ "llparse-test-fixture": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/llparse-test-fixture/-/llparse-test-fixture-5.0.2.tgz",
+ "integrity": "sha512-61KI5J/b5uyRktD0y1EezleEW6UfaxhHkn1adLKNVemRZzklE+SpLakr251qo04kb9jN/ytk8lllgK+yFOj4cQ==",
+ "dev": true,
+ "requires": {
+ "esm": "^3.2.25",
+ "llparse": "^7.0.0",
+ "yargs": "^15.4.1"
+ }
+ },
+ "locate-path": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+ "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+ "dev": true,
+ "requires": {
+ "p-locate": "^5.0.0"
+ }
+ },
+ "log-symbols": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz",
+ "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==",
+ "dev": true,
+ "requires": {
+ "chalk": "^4.1.0",
+ "is-unicode-supported": "^0.1.0"
+ },
+ "dependencies": {
+ "ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "requires": {
+ "color-convert": "^2.0.1"
+ }
+ },
+ "chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "requires": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ }
+ },
+ "color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "requires": {
+ "color-name": "~1.1.4"
+ }
+ },
+ "color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
+ "has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true
+ },
+ "supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "requires": {
+ "has-flag": "^4.0.0"
+ }
+ }
+ }
+ },
+ "make-error": {
+ "version": "1.3.6",
+ "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
+ "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
+ "dev": true
+ },
+ "markdown-it": {
+ "version": "8.4.2",
+ "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-8.4.2.tgz",
+ "integrity": "sha512-GcRz3AWTqSUphY3vsUqQSFMbgR38a4Lh3GWlHRh/7MRwz8mcu9n2IO7HOh+bXHrR9kOPDl5RNCaEsrneb+xhHQ==",
+ "dev": true,
+ "requires": {
+ "argparse": "^1.0.7",
+ "entities": "~1.1.1",
+ "linkify-it": "^2.0.0",
+ "mdurl": "^1.0.1",
+ "uc.micro": "^1.0.5"
+ }
+ },
+ "mdgator": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/mdgator/-/mdgator-1.1.2.tgz",
+ "integrity": "sha512-S2GvsLIznUQ2McXfpe6BCD+IqhnRuHcBO7krqnvnsHgDpjjO1mLhr0vZtVa5ca4WZET037g3G+94DznpicKkOA==",
+ "dev": true,
+ "requires": {
+ "@types/markdown-it": "0.0.4",
+ "markdown-it": "^8.4.1"
+ }
+ },
+ "mdurl": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz",
+ "integrity": "sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4=",
+ "dev": true
+ },
+ "minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "requires": {
+ "brace-expansion": "^1.1.7"
+ }
+ },
+ "minimist": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
+ "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==",
+ "dev": true
+ },
+ "mkdirp": {
+ "version": "0.5.5",
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
+ "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
+ "dev": true,
+ "requires": {
+ "minimist": "^1.2.5"
+ }
+ },
+ "mocha": {
+ "version": "10.2.0",
+ "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.2.0.tgz",
+ "integrity": "sha512-IDY7fl/BecMwFHzoqF2sg/SHHANeBoMMXFlS9r0OXKDssYE1M5O43wUY/9BVPeIvfH2zmEbBfseqN9gBQZzXkg==",
+ "dev": true,
+ "requires": {
+ "ansi-colors": "4.1.1",
+ "browser-stdout": "1.3.1",
+ "chokidar": "3.5.3",
+ "debug": "4.3.4",
+ "diff": "5.0.0",
+ "escape-string-regexp": "4.0.0",
+ "find-up": "5.0.0",
+ "glob": "7.2.0",
+ "he": "1.2.0",
+ "js-yaml": "4.1.0",
+ "log-symbols": "4.1.0",
+ "minimatch": "5.0.1",
+ "ms": "2.1.3",
+ "nanoid": "3.3.3",
+ "serialize-javascript": "6.0.0",
+ "strip-json-comments": "3.1.1",
+ "supports-color": "8.1.1",
+ "workerpool": "6.2.1",
+ "yargs": "16.2.0",
+ "yargs-parser": "20.2.4",
+ "yargs-unparser": "2.0.0"
+ },
+ "dependencies": {
+ "ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "requires": {
+ "color-convert": "^2.0.1"
+ }
+ },
+ "argparse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "dev": true
+ },
+ "brace-expansion": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
+ "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
+ "dev": true,
+ "requires": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "cliui": {
+ "version": "7.0.4",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz",
+ "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==",
+ "dev": true,
+ "requires": {
+ "string-width": "^4.2.0",
+ "strip-ansi": "^6.0.0",
+ "wrap-ansi": "^7.0.0"
+ }
+ },
+ "color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "requires": {
+ "color-name": "~1.1.4"
+ }
+ },
+ "color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
+ "diff": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz",
+ "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==",
+ "dev": true
+ },
+ "escape-string-regexp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "dev": true
+ },
+ "js-yaml": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
+ "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+ "dev": true,
+ "requires": {
+ "argparse": "^2.0.1"
+ }
+ },
+ "minimatch": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz",
+ "integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==",
+ "dev": true,
+ "requires": {
+ "brace-expansion": "^2.0.1"
+ }
+ },
+ "ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true
+ },
+ "wrap-ansi": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "dev": true,
+ "requires": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ }
+ },
+ "y18n": {
+ "version": "5.0.8",
+ "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
+ "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
+ "dev": true
+ },
+ "yargs": {
+ "version": "16.2.0",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz",
+ "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==",
+ "dev": true,
+ "requires": {
+ "cliui": "^7.0.2",
+ "escalade": "^3.1.1",
+ "get-caller-file": "^2.0.5",
+ "require-directory": "^2.1.1",
+ "string-width": "^4.2.0",
+ "y18n": "^5.0.5",
+ "yargs-parser": "^20.2.2"
+ }
+ }
+ }
+ },
+ "ms": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
+ },
+ "nanoid": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz",
+ "integrity": "sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==",
+ "dev": true
+ },
+ "normalize-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+ "dev": true
+ },
+ "once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
+ "dev": true,
+ "requires": {
+ "wrappy": "1"
+ }
+ },
+ "p-limit": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
+ "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
+ "dev": true,
+ "requires": {
+ "p-try": "^2.0.0"
+ }
+ },
+ "p-locate": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+ "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+ "dev": true,
+ "requires": {
+ "p-limit": "^3.0.2"
+ },
+ "dependencies": {
+ "p-limit": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+ "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+ "dev": true,
+ "requires": {
+ "yocto-queue": "^0.1.0"
+ }
+ }
+ }
+ },
+ "p-try": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
+ "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
+ "dev": true
+ },
+ "path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "dev": true
+ },
+ "path-is-absolute": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+ "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
+ "dev": true
+ },
+ "path-parse": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
+ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
+ "dev": true
+ },
+ "picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "dev": true
+ },
+ "randombytes": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
+ "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
+ "dev": true,
+ "requires": {
+ "safe-buffer": "^5.1.0"
+ }
+ },
+ "readdirp": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+ "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+ "dev": true,
+ "requires": {
+ "picomatch": "^2.2.1"
+ }
+ },
+ "require-directory": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
+ "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=",
+ "dev": true
+ },
+ "require-main-filename": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
+ "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
+ "dev": true
+ },
+ "resolve": {
+ "version": "1.20.0",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz",
+ "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==",
+ "dev": true,
+ "requires": {
+ "is-core-module": "^2.2.0",
+ "path-parse": "^1.0.6"
+ }
+ },
+ "safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "dev": true
+ },
+ "semver": {
+ "version": "5.7.2",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
+ "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="
+ },
+ "serialize-javascript": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz",
+ "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==",
+ "dev": true,
+ "requires": {
+ "randombytes": "^2.1.0"
+ }
+ },
+ "set-blocking": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
+ "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=",
+ "dev": true
+ },
+ "source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "dev": true
+ },
+ "source-map-support": {
+ "version": "0.5.19",
+ "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz",
+ "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==",
+ "dev": true,
+ "requires": {
+ "buffer-from": "^1.0.0",
+ "source-map": "^0.6.0"
+ }
+ },
+ "sprintf-js": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
+ "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=",
+ "dev": true
+ },
+ "string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "requires": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ }
+ },
+ "strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "requires": {
+ "ansi-regex": "^5.0.1"
+ }
+ },
+ "strip-json-comments": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
+ "dev": true
+ },
+ "supports-color": {
+ "version": "8.1.1",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
+ "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
+ "dev": true,
+ "requires": {
+ "has-flag": "^4.0.0"
+ },
+ "dependencies": {
+ "has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true
+ }
+ }
+ },
+ "to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "dev": true,
+ "requires": {
+ "is-number": "^7.0.0"
+ }
+ },
+ "ts-node": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-7.0.1.tgz",
+ "integrity": "sha512-BVwVbPJRspzNh2yfslyT1PSbl5uIk03EZlb493RKHN4qej/D06n1cEhjlOJG69oFsE7OT8XjpTUcYf6pKTLMhw==",
+ "dev": true,
+ "requires": {
+ "arrify": "^1.0.0",
+ "buffer-from": "^1.1.0",
+ "diff": "^3.1.0",
+ "make-error": "^1.1.1",
+ "minimist": "^1.2.0",
+ "mkdirp": "^0.5.1",
+ "source-map-support": "^0.5.6",
+ "yn": "^2.0.0"
+ }
+ },
+ "tslib": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
+ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
+ "dev": true
+ },
+ "tslint": {
+ "version": "5.20.1",
+ "resolved": "https://registry.npmjs.org/tslint/-/tslint-5.20.1.tgz",
+ "integrity": "sha512-EcMxhzCFt8k+/UP5r8waCf/lzmeSyVlqxqMEDQE7rWYiQky8KpIBz1JAoYXfROHrPZ1XXd43q8yQnULOLiBRQg==",
+ "dev": true,
+ "requires": {
+ "@babel/code-frame": "^7.0.0",
+ "builtin-modules": "^1.1.1",
+ "chalk": "^2.3.0",
+ "commander": "^2.12.1",
+ "diff": "^4.0.1",
+ "glob": "^7.1.1",
+ "js-yaml": "^3.13.1",
+ "minimatch": "^3.0.4",
+ "mkdirp": "^0.5.1",
+ "resolve": "^1.3.2",
+ "semver": "^5.3.0",
+ "tslib": "^1.8.0",
+ "tsutils": "^2.29.0"
+ },
+ "dependencies": {
+ "diff": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
+ "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
+ "dev": true
+ }
+ }
+ },
+ "tsutils": {
+ "version": "2.29.0",
+ "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.29.0.tgz",
+ "integrity": "sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA==",
+ "dev": true,
+ "requires": {
+ "tslib": "^1.8.1"
+ }
+ },
+ "typescript": {
+ "version": "3.9.9",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.9.tgz",
+ "integrity": "sha512-kdMjTiekY+z/ubJCATUPlRDl39vXYiMV9iyeMuEuXZh2we6zz80uovNN2WlAxmmdE/Z/YQe+EbOEXB5RHEED3w==",
+ "dev": true
+ },
+ "uc.micro": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz",
+ "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==",
+ "dev": true
+ },
+ "which-module": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz",
+ "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=",
+ "dev": true
+ },
+ "workerpool": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz",
+ "integrity": "sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==",
+ "dev": true
+ },
+ "wrap-ansi": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
+ "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
+ "dev": true,
+ "requires": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "dependencies": {
+ "ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "requires": {
+ "color-convert": "^2.0.1"
+ }
+ },
+ "color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "requires": {
+ "color-name": "~1.1.4"
+ }
+ },
+ "color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ }
+ }
+ },
+ "wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
+ "dev": true
+ },
+ "y18n": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
+ "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==",
+ "dev": true
+ },
+ "yargs": {
+ "version": "15.4.1",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
+ "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
+ "dev": true,
+ "requires": {
+ "cliui": "^6.0.0",
+ "decamelize": "^1.2.0",
+ "find-up": "^4.1.0",
+ "get-caller-file": "^2.0.1",
+ "require-directory": "^2.1.1",
+ "require-main-filename": "^2.0.0",
+ "set-blocking": "^2.0.0",
+ "string-width": "^4.2.0",
+ "which-module": "^2.0.0",
+ "y18n": "^4.0.0",
+ "yargs-parser": "^18.1.2"
+ },
+ "dependencies": {
+ "find-up": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
+ "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
+ "dev": true,
+ "requires": {
+ "locate-path": "^5.0.0",
+ "path-exists": "^4.0.0"
+ }
+ },
+ "locate-path": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
+ "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
+ "dev": true,
+ "requires": {
+ "p-locate": "^4.1.0"
+ }
+ },
+ "p-locate": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
+ "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
+ "dev": true,
+ "requires": {
+ "p-limit": "^2.2.0"
+ }
+ },
+ "yargs-parser": {
+ "version": "18.1.3",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
+ "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
+ "dev": true,
+ "requires": {
+ "camelcase": "^5.0.0",
+ "decamelize": "^1.2.0"
+ }
+ }
+ }
+ },
+ "yargs-parser": {
+ "version": "20.2.4",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz",
+ "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==",
+ "dev": true
+ },
+ "yargs-unparser": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz",
+ "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==",
+ "dev": true,
+ "requires": {
+ "camelcase": "^6.0.0",
+ "decamelize": "^4.0.0",
+ "flat": "^5.0.2",
+ "is-plain-obj": "^2.1.0"
+ },
+ "dependencies": {
+ "camelcase": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz",
+ "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==",
+ "dev": true
+ },
+ "decamelize": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz",
+ "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==",
+ "dev": true
+ }
+ }
+ },
+ "yn": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/yn/-/yn-2.0.0.tgz",
+ "integrity": "sha1-5a2ryKz0CPY4X8dklWhMiOavaJo=",
+ "dev": true
+ },
+ "yocto-queue": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+ "dev": true
+ }
+ }
+}
diff --git a/llhttp/package.json b/llhttp/package.json
new file mode 100644
index 0000000..be715b8
--- /dev/null
+++ b/llhttp/package.json
@@ -0,0 +1,60 @@
+{
+ "name": "llhttp",
+ "version": "9.1.3",
+ "description": "HTTP parser in LLVM IR",
+ "main": "lib/llhttp.js",
+ "types": "lib/llhttp.d.ts",
+ "files": [
+ "lib",
+ "src"
+ ],
+ "scripts": {
+ "bench": "ts-node bench/",
+ "build": "ts-node bin/generate.ts",
+ "build-ts": "tsc",
+ "prebuild-wasm": "npm run wasm -- --prebuild && npm run wasm -- --setup",
+ "build-wasm": "npm run wasm -- --docker",
+ "wasm": "ts-node bin/build_wasm.ts",
+ "clean": "rm -rf lib && rm -rf test/tmp",
+ "prepare": "npm run clean && npm run build-ts",
+ "lint": "tslint -c tslint.json bin/*.ts src/*.ts src/**/*.ts test/*.ts test/**/*.ts",
+ "lint-fix": "tslint --fix -c tslint.json bin/*.ts src/*.ts src/**/*.ts test/*.ts test/**/*.ts",
+ "mocha": "mocha --timeout=10000 -r ts-node/register/type-check --reporter progress test/*-test.ts",
+ "test": "npm run mocha && npm run lint",
+ "postversion": "RELEASE=`node -e \"process.stdout.write(require('./package').version)\"` make -B postversion",
+ "github-release": "RELEASE_V=`node -e \"process.stdout.write('v' + require('./package').version)\"` make github-release"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+ssh://git@github.com/nodejs/llhttp.git"
+ },
+ "keywords": [
+ "http",
+ "llvm",
+ "ir",
+ "llparse"
+ ],
+ "author": "Fedor Indutny <fedor@indutny.com> (http://darksi.de/)",
+ "license": "MIT",
+ "bugs": {
+ "url": "https://github.com/nodejs/llhttp/issues"
+ },
+ "homepage": "https://github.com/nodejs/llhttp#readme",
+ "devDependencies": {
+ "@types/mocha": "^5.2.7",
+ "@types/node": "^10.17.52",
+ "javascript-stringify": "^2.0.1",
+ "llparse-dot": "^1.0.1",
+ "llparse-test-fixture": "^5.0.1",
+ "mdgator": "^1.1.2",
+ "mocha": "^10.2.0",
+ "ts-node": "^7.0.1",
+ "tslint": "^5.20.1",
+ "typescript": "^3.9.9"
+ },
+ "dependencies": {
+ "@types/semver": "^5.5.0",
+ "llparse": "^7.1.1",
+ "semver": "^5.7.1"
+ }
+}
diff --git a/llhttp/src/common.gypi b/llhttp/src/common.gypi
new file mode 100644
index 0000000..ef7549f
--- /dev/null
+++ b/llhttp/src/common.gypi
@@ -0,0 +1,46 @@
+{
+ 'target_defaults': {
+ 'default_configuration': 'Debug',
+ 'configurations': {
+ # TODO: hoist these out and put them somewhere common, because
+ # RuntimeLibrary MUST MATCH across the entire project
+ 'Debug': {
+ 'defines': [ 'DEBUG', '_DEBUG' ],
+ 'cflags': [ '-Wall', '-Wextra', '-O0', '-g', '-ftrapv' ],
+ 'msvs_settings': {
+ 'VCCLCompilerTool': {
+ 'RuntimeLibrary': 1, # static debug
+ },
+ },
+ },
+ 'Release': {
+ 'defines': [ 'NDEBUG' ],
+ 'cflags': [ '-Wall', '-Wextra', '-O3' ],
+ 'msvs_settings': {
+ 'VCCLCompilerTool': {
+ 'RuntimeLibrary': 0, # static release
+ },
+ },
+ }
+ },
+ 'msvs_settings': {
+ 'VCCLCompilerTool': {
+ # Compile as C++. llhttp.c is actually C99, but C++ is
+ # close enough in this case.
+ 'CompileAs': 2,
+ },
+ 'VCLibrarianTool': {
+ },
+ 'VCLinkerTool': {
+ 'GenerateDebugInformation': 'true',
+ },
+ },
+ 'conditions': [
+ ['OS == "win"', {
+ 'defines': [
+ 'WIN32'
+ ],
+ }]
+ ],
+ },
+}
diff --git a/llhttp/src/llhttp.gyp b/llhttp/src/llhttp.gyp
new file mode 100644
index 0000000..c7b8800
--- /dev/null
+++ b/llhttp/src/llhttp.gyp
@@ -0,0 +1,22 @@
+{
+ 'variables': {
+ 'llhttp_sources': [
+ 'src/llhttp.c',
+ 'src/api.c',
+ 'src/http.c',
+ ]
+ },
+ 'targets': [
+ {
+ 'target_name': 'llhttp',
+ 'type': 'static_library',
+ 'include_dirs': [ '.', 'include' ],
+ 'direct_dependent_settings': {
+ 'include_dirs': [ 'include' ],
+ },
+ 'sources': [
+ '<@(llhttp_sources)',
+ ],
+ },
+ ]
+}
diff --git a/llhttp/src/llhttp.ts b/llhttp/src/llhttp.ts
new file mode 100644
index 0000000..ba36b01
--- /dev/null
+++ b/llhttp/src/llhttp.ts
@@ -0,0 +1,7 @@
+import * as constants from './llhttp/constants';
+
+export { constants };
+
+export { HTTP } from './llhttp/http';
+export { URL } from './llhttp/url';
+export { CHeaders } from './llhttp/c-headers';
diff --git a/llhttp/src/llhttp/c-headers.ts b/llhttp/src/llhttp/c-headers.ts
new file mode 100644
index 0000000..fad66de
--- /dev/null
+++ b/llhttp/src/llhttp/c-headers.ts
@@ -0,0 +1,106 @@
+import * as constants from './constants';
+import { enumToMap, IEnumMap } from './utils';
+
+type Encoding = 'none' | 'hex';
+
+export class CHeaders {
+ public build(): string {
+ let res = '';
+
+ res += '#ifndef LLLLHTTP_C_HEADERS_\n';
+ res += '#define LLLLHTTP_C_HEADERS_\n';
+
+ res += '#ifdef __cplusplus\n';
+ res += 'extern "C" {\n';
+ res += '#endif\n';
+
+ res += '\n';
+
+ const errorMap = enumToMap(constants.ERROR);
+ const methodMap = enumToMap(constants.METHODS);
+ const httpMethodMap = enumToMap(constants.METHODS, constants.METHODS_HTTP, [
+ constants.METHODS.PRI,
+ ]);
+ const rtspMethodMap = enumToMap(constants.METHODS, constants.METHODS_RTSP);
+ const statusMap = enumToMap(constants.STATUSES, constants.STATUSES_HTTP);
+
+ res += this.buildEnum('llhttp_errno', 'HPE', errorMap);
+ res += '\n';
+ res += this.buildEnum('llhttp_flags', 'F', enumToMap(constants.FLAGS),
+ 'hex');
+ res += '\n';
+ res += this.buildEnum('llhttp_lenient_flags', 'LENIENT',
+ enumToMap(constants.LENIENT_FLAGS), 'hex');
+ res += '\n';
+ res += this.buildEnum('llhttp_type', 'HTTP',
+ enumToMap(constants.TYPE));
+ res += '\n';
+ res += this.buildEnum('llhttp_finish', 'HTTP_FINISH',
+ enumToMap(constants.FINISH));
+ res += '\n';
+ res += this.buildEnum('llhttp_method', 'HTTP', methodMap);
+ res += '\n';
+ res += this.buildEnum('llhttp_status', 'HTTP_STATUS', statusMap);
+
+ res += '\n';
+
+ res += this.buildMap('HTTP_ERRNO', errorMap);
+ res += '\n';
+ res += this.buildMap('HTTP_METHOD', httpMethodMap);
+ res += '\n';
+ res += this.buildMap('RTSP_METHOD', rtspMethodMap);
+ res += '\n';
+ res += this.buildMap('HTTP_ALL_METHOD', methodMap);
+ res += '\n';
+ res += this.buildMap('HTTP_STATUS', statusMap);
+
+ res += '\n';
+
+ res += '#ifdef __cplusplus\n';
+ res += '} /* extern "C" */\n';
+ res += '#endif\n';
+ res += '#endif /* LLLLHTTP_C_HEADERS_ */\n';
+
+ return res;
+ }
+
+ private buildEnum(name: string, prefix: string, map: IEnumMap,
+ encoding: Encoding = 'none'): string {
+ let res = '';
+
+ res += `enum ${name} {\n`;
+ const keys = Object.keys(map);
+ const keysLength = keys.length;
+ for (let i = 0; i < keysLength; i++) {
+ const key = keys[i];
+ const isLast = i === keysLength - 1;
+
+ let value: number | string = map[key];
+
+ if (encoding === 'hex') {
+ value = `0x${value.toString(16)}`;
+ }
+
+ res += ` ${prefix}_${key.replace(/-/g, '')} = ${value}`;
+ if (!isLast) {
+ res += ',\n';
+ }
+ }
+ res += '\n};\n';
+ res += `typedef enum ${name} ${name}_t;\n`;
+
+ return res;
+ }
+
+ private buildMap(name: string, map: IEnumMap): string {
+ let res = '';
+
+ res += `#define ${name}_MAP(XX) \\\n`;
+ for (const [key, value] of Object.entries(map)) {
+ res += ` XX(${value!}, ${key.replace(/-/g, '')}, ${key}) \\\n`;
+ }
+ res += '\n';
+
+ return res;
+ }
+}
diff --git a/llhttp/src/llhttp/constants.ts b/llhttp/src/llhttp/constants.ts
new file mode 100644
index 0000000..00fc523
--- /dev/null
+++ b/llhttp/src/llhttp/constants.ts
@@ -0,0 +1,540 @@
+import { enumToMap, IEnumMap } from './utils';
+
+// C headers
+
+export enum ERROR {
+ OK = 0,
+ INTERNAL = 1,
+ STRICT = 2,
+ CR_EXPECTED = 25,
+ LF_EXPECTED = 3,
+ UNEXPECTED_CONTENT_LENGTH = 4,
+ UNEXPECTED_SPACE = 30,
+ CLOSED_CONNECTION = 5,
+ INVALID_METHOD = 6,
+ INVALID_URL = 7,
+ INVALID_CONSTANT = 8,
+ INVALID_VERSION = 9,
+ INVALID_HEADER_TOKEN = 10,
+ INVALID_CONTENT_LENGTH = 11,
+ INVALID_CHUNK_SIZE = 12,
+ INVALID_STATUS = 13,
+ INVALID_EOF_STATE = 14,
+ INVALID_TRANSFER_ENCODING = 15,
+
+ CB_MESSAGE_BEGIN = 16,
+ CB_HEADERS_COMPLETE = 17,
+ CB_MESSAGE_COMPLETE = 18,
+ CB_CHUNK_HEADER = 19,
+ CB_CHUNK_COMPLETE = 20,
+
+ PAUSED = 21,
+ PAUSED_UPGRADE = 22,
+ PAUSED_H2_UPGRADE = 23,
+
+ USER = 24,
+
+ CB_URL_COMPLETE = 26,
+ CB_STATUS_COMPLETE = 27,
+ CB_METHOD_COMPLETE = 32,
+ CB_VERSION_COMPLETE = 33,
+ CB_HEADER_FIELD_COMPLETE = 28,
+ CB_HEADER_VALUE_COMPLETE = 29,
+ CB_CHUNK_EXTENSION_NAME_COMPLETE = 34,
+ CB_CHUNK_EXTENSION_VALUE_COMPLETE = 35,
+ CB_RESET = 31,
+}
+
+export enum TYPE {
+ BOTH = 0, // default
+ REQUEST = 1,
+ RESPONSE = 2,
+}
+
+export enum FLAGS {
+ CONNECTION_KEEP_ALIVE = 1 << 0,
+ CONNECTION_CLOSE = 1 << 1,
+ CONNECTION_UPGRADE = 1 << 2,
+ CHUNKED = 1 << 3,
+ UPGRADE = 1 << 4,
+ CONTENT_LENGTH = 1 << 5,
+ SKIPBODY = 1 << 6,
+ TRAILING = 1 << 7,
+ // 1 << 8 is unused
+ TRANSFER_ENCODING = 1 << 9,
+}
+
+export enum LENIENT_FLAGS {
+ HEADERS = 1 << 0,
+ CHUNKED_LENGTH = 1 << 1,
+ KEEP_ALIVE = 1 << 2,
+ TRANSFER_ENCODING = 1 << 3,
+ VERSION = 1 << 4,
+ DATA_AFTER_CLOSE = 1 << 5,
+ OPTIONAL_LF_AFTER_CR = 1 << 6,
+ OPTIONAL_CRLF_AFTER_CHUNK = 1 << 7,
+ OPTIONAL_CR_BEFORE_LF = 1 << 8,
+ SPACES_AFTER_CHUNK_SIZE = 1 << 9,
+}
+
+export enum METHODS {
+ DELETE = 0,
+ GET = 1,
+ HEAD = 2,
+ POST = 3,
+ PUT = 4,
+ /* pathological */
+ CONNECT = 5,
+ OPTIONS = 6,
+ TRACE = 7,
+ /* WebDAV */
+ COPY = 8,
+ LOCK = 9,
+ MKCOL = 10,
+ MOVE = 11,
+ PROPFIND = 12,
+ PROPPATCH = 13,
+ SEARCH = 14,
+ UNLOCK = 15,
+ BIND = 16,
+ REBIND = 17,
+ UNBIND = 18,
+ ACL = 19,
+ /* subversion */
+ REPORT = 20,
+ MKACTIVITY = 21,
+ CHECKOUT = 22,
+ MERGE = 23,
+ /* upnp */
+ 'M-SEARCH' = 24,
+ NOTIFY = 25,
+ SUBSCRIBE = 26,
+ UNSUBSCRIBE = 27,
+ /* RFC-5789 */
+ PATCH = 28,
+ PURGE = 29,
+ /* CalDAV */
+ MKCALENDAR = 30,
+ /* RFC-2068, section 19.6.1.2 */
+ LINK = 31,
+ UNLINK = 32,
+ /* icecast */
+ SOURCE = 33,
+ /* RFC-7540, section 11.6 */
+ PRI = 34,
+ /* RFC-2326 RTSP */
+ DESCRIBE = 35,
+ ANNOUNCE = 36,
+ SETUP = 37,
+ PLAY = 38,
+ PAUSE = 39,
+ TEARDOWN = 40,
+ GET_PARAMETER = 41,
+ SET_PARAMETER = 42,
+ REDIRECT = 43,
+ RECORD = 44,
+ /* RAOP */
+ FLUSH = 45,
+}
+
+export const METHODS_HTTP = [
+ METHODS.DELETE,
+ METHODS.GET,
+ METHODS.HEAD,
+ METHODS.POST,
+ METHODS.PUT,
+ METHODS.CONNECT,
+ METHODS.OPTIONS,
+ METHODS.TRACE,
+ METHODS.COPY,
+ METHODS.LOCK,
+ METHODS.MKCOL,
+ METHODS.MOVE,
+ METHODS.PROPFIND,
+ METHODS.PROPPATCH,
+ METHODS.SEARCH,
+ METHODS.UNLOCK,
+ METHODS.BIND,
+ METHODS.REBIND,
+ METHODS.UNBIND,
+ METHODS.ACL,
+ METHODS.REPORT,
+ METHODS.MKACTIVITY,
+ METHODS.CHECKOUT,
+ METHODS.MERGE,
+ METHODS['M-SEARCH'],
+ METHODS.NOTIFY,
+ METHODS.SUBSCRIBE,
+ METHODS.UNSUBSCRIBE,
+ METHODS.PATCH,
+ METHODS.PURGE,
+ METHODS.MKCALENDAR,
+ METHODS.LINK,
+ METHODS.UNLINK,
+ METHODS.PRI,
+
+ // TODO(indutny): should we allow it with HTTP?
+ METHODS.SOURCE,
+];
+
+export const METHODS_ICE = [
+ METHODS.SOURCE,
+];
+
+export const METHODS_RTSP = [
+ METHODS.OPTIONS,
+ METHODS.DESCRIBE,
+ METHODS.ANNOUNCE,
+ METHODS.SETUP,
+ METHODS.PLAY,
+ METHODS.PAUSE,
+ METHODS.TEARDOWN,
+ METHODS.GET_PARAMETER,
+ METHODS.SET_PARAMETER,
+ METHODS.REDIRECT,
+ METHODS.RECORD,
+ METHODS.FLUSH,
+
+ // For AirPlay
+ METHODS.GET,
+ METHODS.POST,
+];
+
+export const METHOD_MAP = enumToMap(METHODS);
+export const H_METHOD_MAP: IEnumMap = {};
+
+for (const key of Object.keys(METHOD_MAP)) {
+ if (/^H/.test(key)) {
+ H_METHOD_MAP[key] = METHOD_MAP[key];
+ }
+}
+
+export enum STATUSES {
+ CONTINUE = 100,
+ SWITCHING_PROTOCOLS = 101,
+ PROCESSING = 102,
+ EARLY_HINTS = 103,
+ RESPONSE_IS_STALE = 110, // Unofficial
+ REVALIDATION_FAILED = 111, // Unofficial
+ DISCONNECTED_OPERATION = 112, // Unofficial
+ HEURISTIC_EXPIRATION = 113, // Unofficial
+ MISCELLANEOUS_WARNING = 199, // Unofficial
+ OK = 200,
+ CREATED = 201,
+ ACCEPTED = 202,
+ NON_AUTHORITATIVE_INFORMATION = 203,
+ NO_CONTENT = 204,
+ RESET_CONTENT = 205,
+ PARTIAL_CONTENT = 206,
+ MULTI_STATUS = 207,
+ ALREADY_REPORTED = 208,
+ TRANSFORMATION_APPLIED = 214, // Unofficial
+ IM_USED = 226,
+ MISCELLANEOUS_PERSISTENT_WARNING = 299, // Unofficial
+ MULTIPLE_CHOICES = 300,
+ MOVED_PERMANENTLY = 301,
+ FOUND = 302,
+ SEE_OTHER = 303,
+ NOT_MODIFIED = 304,
+ USE_PROXY = 305,
+ SWITCH_PROXY = 306, // No longer used
+ TEMPORARY_REDIRECT = 307,
+ PERMANENT_REDIRECT = 308,
+ BAD_REQUEST = 400,
+ UNAUTHORIZED = 401,
+ PAYMENT_REQUIRED = 402,
+ FORBIDDEN = 403,
+ NOT_FOUND = 404,
+ METHOD_NOT_ALLOWED = 405,
+ NOT_ACCEPTABLE = 406,
+ PROXY_AUTHENTICATION_REQUIRED = 407,
+ REQUEST_TIMEOUT = 408,
+ CONFLICT = 409,
+ GONE = 410,
+ LENGTH_REQUIRED = 411,
+ PRECONDITION_FAILED = 412,
+ PAYLOAD_TOO_LARGE = 413,
+ URI_TOO_LONG = 414,
+ UNSUPPORTED_MEDIA_TYPE = 415,
+ RANGE_NOT_SATISFIABLE = 416,
+ EXPECTATION_FAILED = 417,
+ IM_A_TEAPOT = 418,
+ PAGE_EXPIRED = 419, // Unofficial
+ ENHANCE_YOUR_CALM = 420, // Unofficial
+ MISDIRECTED_REQUEST = 421,
+ UNPROCESSABLE_ENTITY = 422,
+ LOCKED = 423,
+ FAILED_DEPENDENCY = 424,
+ TOO_EARLY = 425,
+ UPGRADE_REQUIRED = 426,
+ PRECONDITION_REQUIRED = 428,
+ TOO_MANY_REQUESTS = 429,
+ REQUEST_HEADER_FIELDS_TOO_LARGE_UNOFFICIAL = 430, // Unofficial
+ REQUEST_HEADER_FIELDS_TOO_LARGE = 431,
+ LOGIN_TIMEOUT = 440, // Unofficial
+ NO_RESPONSE = 444, // Unofficial
+ RETRY_WITH = 449, // Unofficial
+ BLOCKED_BY_PARENTAL_CONTROL = 450, // Unofficial
+ UNAVAILABLE_FOR_LEGAL_REASONS = 451,
+ CLIENT_CLOSED_LOAD_BALANCED_REQUEST = 460, // Unofficial
+ INVALID_X_FORWARDED_FOR = 463, // Unofficial
+ REQUEST_HEADER_TOO_LARGE = 494, // Unofficial
+ SSL_CERTIFICATE_ERROR = 495, // Unofficial
+ SSL_CERTIFICATE_REQUIRED = 496, // Unofficial
+ HTTP_REQUEST_SENT_TO_HTTPS_PORT = 497, // Unofficial
+ INVALID_TOKEN = 498, // Unofficial
+ CLIENT_CLOSED_REQUEST = 499, // Unofficial
+ INTERNAL_SERVER_ERROR = 500,
+ NOT_IMPLEMENTED = 501,
+ BAD_GATEWAY = 502,
+ SERVICE_UNAVAILABLE = 503,
+ GATEWAY_TIMEOUT = 504,
+ HTTP_VERSION_NOT_SUPPORTED = 505,
+ VARIANT_ALSO_NEGOTIATES = 506,
+ INSUFFICIENT_STORAGE = 507,
+ LOOP_DETECTED = 508,
+ BANDWIDTH_LIMIT_EXCEEDED = 509,
+ NOT_EXTENDED = 510,
+ NETWORK_AUTHENTICATION_REQUIRED = 511,
+ WEB_SERVER_UNKNOWN_ERROR = 520, // Unofficial
+ WEB_SERVER_IS_DOWN = 521, // Unofficial
+ CONNECTION_TIMEOUT = 522, // Unofficial
+ ORIGIN_IS_UNREACHABLE = 523, // Unofficial
+ TIMEOUT_OCCURED = 524, // Unofficial
+ SSL_HANDSHAKE_FAILED = 525, // Unofficial
+ INVALID_SSL_CERTIFICATE = 526, // Unofficial
+ RAILGUN_ERROR = 527, // Unofficial
+ SITE_IS_OVERLOADED = 529, // Unofficial
+ SITE_IS_FROZEN = 530, // Unofficial
+ IDENTITY_PROVIDER_AUTHENTICATION_ERROR = 561, // Unofficial
+ NETWORK_READ_TIMEOUT = 598, // Unofficial
+ NETWORK_CONNECT_TIMEOUT = 599, // Unofficial
+}
+
+export const STATUSES_HTTP = [
+ STATUSES.CONTINUE,
+ STATUSES.SWITCHING_PROTOCOLS,
+ STATUSES.PROCESSING,
+ STATUSES.EARLY_HINTS,
+ STATUSES.RESPONSE_IS_STALE,
+ STATUSES.REVALIDATION_FAILED,
+ STATUSES.DISCONNECTED_OPERATION,
+ STATUSES.HEURISTIC_EXPIRATION,
+ STATUSES.MISCELLANEOUS_WARNING,
+ STATUSES.OK,
+ STATUSES.CREATED,
+ STATUSES.ACCEPTED,
+ STATUSES.NON_AUTHORITATIVE_INFORMATION,
+ STATUSES.NO_CONTENT,
+ STATUSES.RESET_CONTENT,
+ STATUSES.PARTIAL_CONTENT,
+ STATUSES.MULTI_STATUS,
+ STATUSES.ALREADY_REPORTED,
+ STATUSES.TRANSFORMATION_APPLIED,
+ STATUSES.IM_USED,
+ STATUSES.MISCELLANEOUS_PERSISTENT_WARNING,
+ STATUSES.MULTIPLE_CHOICES,
+ STATUSES.MOVED_PERMANENTLY,
+ STATUSES.FOUND,
+ STATUSES.SEE_OTHER,
+ STATUSES.NOT_MODIFIED,
+ STATUSES.USE_PROXY,
+ STATUSES.SWITCH_PROXY,
+ STATUSES.TEMPORARY_REDIRECT,
+ STATUSES.PERMANENT_REDIRECT,
+ STATUSES.BAD_REQUEST,
+ STATUSES.UNAUTHORIZED,
+ STATUSES.PAYMENT_REQUIRED,
+ STATUSES.FORBIDDEN,
+ STATUSES.NOT_FOUND,
+ STATUSES.METHOD_NOT_ALLOWED,
+ STATUSES.NOT_ACCEPTABLE,
+ STATUSES.PROXY_AUTHENTICATION_REQUIRED,
+ STATUSES.REQUEST_TIMEOUT,
+ STATUSES.CONFLICT,
+ STATUSES.GONE,
+ STATUSES.LENGTH_REQUIRED,
+ STATUSES.PRECONDITION_FAILED,
+ STATUSES.PAYLOAD_TOO_LARGE,
+ STATUSES.URI_TOO_LONG,
+ STATUSES.UNSUPPORTED_MEDIA_TYPE,
+ STATUSES.RANGE_NOT_SATISFIABLE,
+ STATUSES.EXPECTATION_FAILED,
+ STATUSES.IM_A_TEAPOT,
+ STATUSES.PAGE_EXPIRED,
+ STATUSES.ENHANCE_YOUR_CALM,
+ STATUSES.MISDIRECTED_REQUEST,
+ STATUSES.UNPROCESSABLE_ENTITY,
+ STATUSES.LOCKED,
+ STATUSES.FAILED_DEPENDENCY,
+ STATUSES.TOO_EARLY,
+ STATUSES.UPGRADE_REQUIRED,
+ STATUSES.PRECONDITION_REQUIRED,
+ STATUSES.TOO_MANY_REQUESTS,
+ STATUSES.REQUEST_HEADER_FIELDS_TOO_LARGE_UNOFFICIAL,
+ STATUSES.REQUEST_HEADER_FIELDS_TOO_LARGE,
+ STATUSES.LOGIN_TIMEOUT,
+ STATUSES.NO_RESPONSE,
+ STATUSES.RETRY_WITH,
+ STATUSES.BLOCKED_BY_PARENTAL_CONTROL,
+ STATUSES.UNAVAILABLE_FOR_LEGAL_REASONS,
+ STATUSES.CLIENT_CLOSED_LOAD_BALANCED_REQUEST,
+ STATUSES.INVALID_X_FORWARDED_FOR,
+ STATUSES.REQUEST_HEADER_TOO_LARGE,
+ STATUSES.SSL_CERTIFICATE_ERROR,
+ STATUSES.SSL_CERTIFICATE_REQUIRED,
+ STATUSES.HTTP_REQUEST_SENT_TO_HTTPS_PORT,
+ STATUSES.INVALID_TOKEN,
+ STATUSES.CLIENT_CLOSED_REQUEST,
+ STATUSES.INTERNAL_SERVER_ERROR,
+ STATUSES.NOT_IMPLEMENTED,
+ STATUSES.BAD_GATEWAY,
+ STATUSES.SERVICE_UNAVAILABLE,
+ STATUSES.GATEWAY_TIMEOUT,
+ STATUSES.HTTP_VERSION_NOT_SUPPORTED,
+ STATUSES.VARIANT_ALSO_NEGOTIATES,
+ STATUSES.INSUFFICIENT_STORAGE,
+ STATUSES.LOOP_DETECTED,
+ STATUSES.BANDWIDTH_LIMIT_EXCEEDED,
+ STATUSES.NOT_EXTENDED,
+ STATUSES.NETWORK_AUTHENTICATION_REQUIRED,
+ STATUSES.WEB_SERVER_UNKNOWN_ERROR,
+ STATUSES.WEB_SERVER_IS_DOWN,
+ STATUSES.CONNECTION_TIMEOUT,
+ STATUSES.ORIGIN_IS_UNREACHABLE,
+ STATUSES.TIMEOUT_OCCURED,
+ STATUSES.SSL_HANDSHAKE_FAILED,
+ STATUSES.INVALID_SSL_CERTIFICATE,
+ STATUSES.RAILGUN_ERROR,
+ STATUSES.SITE_IS_OVERLOADED,
+ STATUSES.SITE_IS_FROZEN,
+ STATUSES.IDENTITY_PROVIDER_AUTHENTICATION_ERROR,
+ STATUSES.NETWORK_READ_TIMEOUT,
+ STATUSES.NETWORK_CONNECT_TIMEOUT,
+];
+
+export enum FINISH {
+ SAFE = 0,
+ SAFE_WITH_CB = 1,
+ UNSAFE = 2,
+}
+
+// Internal
+
+export type CharList = Array<string | number>;
+
+export const ALPHA: CharList = [];
+
+for (let i = 'A'.charCodeAt(0); i <= 'Z'.charCodeAt(0); i++) {
+ // Upper case
+ ALPHA.push(String.fromCharCode(i));
+
+ // Lower case
+ ALPHA.push(String.fromCharCode(i + 0x20));
+}
+
+export const NUM_MAP = {
+ 0: 0, 1: 1, 2: 2, 3: 3, 4: 4,
+ 5: 5, 6: 6, 7: 7, 8: 8, 9: 9,
+};
+
+export const HEX_MAP = {
+ 0: 0, 1: 1, 2: 2, 3: 3, 4: 4,
+ 5: 5, 6: 6, 7: 7, 8: 8, 9: 9,
+ A: 0XA, B: 0XB, C: 0XC, D: 0XD, E: 0XE, F: 0XF,
+ a: 0xa, b: 0xb, c: 0xc, d: 0xd, e: 0xe, f: 0xf,
+};
+
+export const NUM: CharList = [
+ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
+];
+
+export const ALPHANUM: CharList = ALPHA.concat(NUM);
+export const MARK: CharList = [ '-', '_', '.', '!', '~', '*', '\'', '(', ')' ];
+export const USERINFO_CHARS: CharList = ALPHANUM
+ .concat(MARK)
+ .concat([ '%', ';', ':', '&', '=', '+', '$', ',' ]);
+
+// TODO(indutny): use RFC
+export const URL_CHAR: CharList = ([
+ '!', '"', '$', '%', '&', '\'',
+ '(', ')', '*', '+', ',', '-', '.', '/',
+ ':', ';', '<', '=', '>',
+ '@', '[', '\\', ']', '^', '_',
+ '`',
+ '{', '|', '}', '~',
+] as CharList).concat(ALPHANUM);
+
+export const HEX: CharList = NUM.concat(
+ [ 'a', 'b', 'c', 'd', 'e', 'f', 'A', 'B', 'C', 'D', 'E', 'F' ]);
+
+/* Tokens as defined by rfc 2616. Also lowercases them.
+ * token = 1*<any CHAR except CTLs or separators>
+ * separators = "(" | ")" | "<" | ">" | "@"
+ * | "," | ";" | ":" | "\" | <">
+ * | "/" | "[" | "]" | "?" | "="
+ * | "{" | "}" | SP | HT
+ */
+export const TOKEN: CharList = ([
+ '!', '#', '$', '%', '&', '\'',
+ '*', '+', '-', '.',
+ '^', '_', '`',
+ '|', '~',
+] as CharList).concat(ALPHANUM);
+
+/*
+ * Verify that a char is a valid visible (printable) US-ASCII
+ * character or %x80-FF
+ */
+export const HEADER_CHARS: CharList = [ '\t' ];
+for (let i = 32; i <= 255; i++) {
+ if (i !== 127) {
+ HEADER_CHARS.push(i);
+ }
+}
+
+// ',' = \x44
+export const CONNECTION_TOKEN_CHARS: CharList =
+ HEADER_CHARS.filter((c: string | number) => c !== 44);
+
+export const QUOTED_STRING: CharList = [ '\t', ' ' ];
+for (let i = 0x21; i <= 0xff; i++) {
+ if (i !== 0x22 && i !== 0x5c) { // All characters in ASCII except \ and "
+ QUOTED_STRING.push(i);
+ }
+}
+
+export const HTAB_SP_VCHAR_OBS_TEXT: CharList = [ '\t', ' ' ];
+
+// VCHAR: https://tools.ietf.org/html/rfc5234#appendix-B.1
+for (let i = 0x21; i <= 0x7E; i++) {
+ HTAB_SP_VCHAR_OBS_TEXT.push(i);
+}
+// OBS_TEXT: https://datatracker.ietf.org/doc/html/rfc9110#name-collected-abnf
+for (let i = 0x80; i <= 0xff; i++) {
+ HTAB_SP_VCHAR_OBS_TEXT.push(i);
+}
+
+export const MAJOR = NUM_MAP;
+export const MINOR = MAJOR;
+
+export enum HEADER_STATE {
+ GENERAL = 0,
+ CONNECTION = 1,
+ CONTENT_LENGTH = 2,
+ TRANSFER_ENCODING = 3,
+ UPGRADE = 4,
+
+ CONNECTION_KEEP_ALIVE = 5,
+ CONNECTION_CLOSE = 6,
+ CONNECTION_UPGRADE = 7,
+ TRANSFER_ENCODING_CHUNKED = 8,
+}
+
+export const SPECIAL_HEADERS = {
+ 'connection': HEADER_STATE.CONNECTION,
+ 'content-length': HEADER_STATE.CONTENT_LENGTH,
+ 'proxy-connection': HEADER_STATE.CONNECTION,
+ 'transfer-encoding': HEADER_STATE.TRANSFER_ENCODING,
+ 'upgrade': HEADER_STATE.UPGRADE,
+};
diff --git a/llhttp/src/llhttp/http.ts b/llhttp/src/llhttp/http.ts
new file mode 100644
index 0000000..6a201ff
--- /dev/null
+++ b/llhttp/src/llhttp/http.ts
@@ -0,0 +1,1299 @@
+import * as assert from 'assert';
+import { LLParse, source } from 'llparse';
+
+import Match = source.node.Match;
+import Node = source.node.Node;
+
+import {
+ CharList,
+ CONNECTION_TOKEN_CHARS, ERROR, FINISH, FLAGS, H_METHOD_MAP, HEADER_CHARS,
+ HEADER_STATE, HEX_MAP, HTAB_SP_VCHAR_OBS_TEXT,
+ LENIENT_FLAGS,
+ MAJOR, METHOD_MAP, METHODS, METHODS_HTTP, METHODS_ICE, METHODS_RTSP,
+ MINOR, NUM_MAP, QUOTED_STRING, SPECIAL_HEADERS,
+ TOKEN, TYPE,
+} from './constants';
+import { URL } from './url';
+
+type MaybeNode = string | Match | Node;
+
+const NODES: ReadonlyArray<string> = [
+ 'start',
+ 'after_start',
+ 'start_req',
+ 'after_start_req',
+ 'start_res',
+ 'start_req_or_res',
+
+ 'req_or_res_method',
+
+ 'res_http_major',
+ 'res_http_dot',
+ 'res_http_minor',
+ 'res_http_end',
+ 'res_after_version',
+ 'res_status_code_digit_1',
+ 'res_status_code_digit_2',
+ 'res_status_code_digit_3',
+ 'res_status_code_otherwise',
+ 'res_status_start',
+ 'res_status',
+ 'res_line_almost_done',
+
+ 'req_first_space_before_url',
+ 'req_spaces_before_url',
+ 'req_http_start',
+ 'req_http_version',
+ 'req_http_major',
+ 'req_http_dot',
+ 'req_http_minor',
+ 'req_http_end',
+ 'req_http_complete',
+ 'req_http_complete_crlf',
+
+ 'req_pri_upgrade',
+
+ 'headers_start',
+ 'header_field_start',
+ 'header_field',
+ 'header_field_colon',
+ 'header_field_colon_discard_ws',
+ 'header_field_general',
+ 'header_field_general_otherwise',
+ 'header_value_discard_ws',
+ 'header_value_discard_ws_almost_done',
+ 'header_value_discard_lws',
+ 'header_value_start',
+ 'header_value',
+ 'header_value_otherwise',
+ 'header_value_lenient',
+ 'header_value_lenient_failed',
+ 'header_value_lws',
+ 'header_value_te_chunked',
+ 'header_value_te_chunked_last',
+ 'header_value_te_token',
+ 'header_value_te_token_ows',
+ 'header_value_content_length_once',
+ 'header_value_content_length',
+ 'header_value_content_length_ws',
+ 'header_value_connection',
+ 'header_value_connection_ws',
+ 'header_value_connection_token',
+ 'header_value_almost_done',
+
+ 'headers_almost_done',
+ 'headers_done',
+
+ 'chunk_size_start',
+ 'chunk_size_digit',
+ 'chunk_size',
+ 'chunk_size_otherwise',
+ 'chunk_size_almost_done',
+ 'chunk_size_almost_done_lf',
+ 'chunk_extensions',
+ 'chunk_extension_name',
+ 'chunk_extension_value',
+ 'chunk_extension_quoted_value',
+ 'chunk_extension_quoted_value_quoted_pair',
+ 'chunk_extension_quoted_value_done',
+ 'chunk_data',
+ 'chunk_data_almost_done',
+ 'chunk_complete',
+ 'body_identity',
+ 'body_identity_eof',
+
+ 'message_done',
+
+ 'eof',
+ 'cleanup',
+ 'closed',
+ 'restart',
+];
+
+interface ISpanMap {
+ readonly status: source.Span;
+ readonly method: source.Span;
+ readonly version: source.Span;
+ readonly headerField: source.Span;
+ readonly headerValue: source.Span;
+ readonly chunkExtensionName: source.Span;
+ readonly chunkExtensionValue: source.Span;
+ readonly body: source.Span;
+}
+
+interface ICallbackMap {
+ readonly onMessageBegin: source.code.Code;
+ readonly onUrlComplete: source.code.Code;
+ readonly onMethodComplete: source.code.Code;
+ readonly onVersionComplete: source.code.Code;
+ readonly onStatusComplete: source.code.Code;
+ readonly beforeHeadersComplete: source.code.Code;
+ readonly onHeaderFieldComplete: source.code.Code;
+ readonly onHeaderValueComplete: source.code.Code;
+ readonly onHeadersComplete: source.code.Code;
+ readonly afterHeadersComplete: source.code.Code;
+ readonly onChunkHeader: source.code.Code;
+ readonly onChunkExtensionName: source.code.Code;
+ readonly onChunkExtensionValue: source.code.Code;
+ readonly onChunkComplete: source.code.Code;
+ readonly onMessageComplete: source.code.Code;
+ readonly afterMessageComplete: source.code.Code;
+ readonly onReset: source.code.Code;
+}
+
+interface IMulTargets {
+ readonly overflow: string | Node;
+ readonly success: string | Node;
+}
+
+interface IMulOptions {
+ readonly base: number;
+ readonly max?: number;
+ readonly signed: boolean;
+}
+
+interface IIsEqualTargets {
+ readonly equal: string | Node;
+ readonly notEqual: string | Node;
+}
+
+export interface IHTTPResult {
+ readonly entry: Node;
+}
+
+export class HTTP {
+ private readonly url: URL;
+ private readonly TOKEN: CharList;
+ private readonly span: ISpanMap;
+ private readonly callback: ICallbackMap;
+ private readonly nodes: Map<string, Match> = new Map();
+
+ constructor(private readonly llparse: LLParse) {
+ const p = llparse;
+
+ this.url = new URL(p);
+ this.TOKEN = TOKEN;
+
+ this.span = {
+ body: p.span(p.code.span('llhttp__on_body')),
+ chunkExtensionName: p.span(p.code.span('llhttp__on_chunk_extension_name')),
+ chunkExtensionValue: p.span(p.code.span('llhttp__on_chunk_extension_value')),
+ headerField: p.span(p.code.span('llhttp__on_header_field')),
+ headerValue: p.span(p.code.span('llhttp__on_header_value')),
+ method: p.span(p.code.span('llhttp__on_method')),
+ status: p.span(p.code.span('llhttp__on_status')),
+ version: p.span(p.code.span('llhttp__on_version')),
+ };
+
+ /* tslint:disable:object-literal-sort-keys */
+ this.callback = {
+ // User callbacks
+ onUrlComplete: p.code.match('llhttp__on_url_complete'),
+ onStatusComplete: p.code.match('llhttp__on_status_complete'),
+ onMethodComplete: p.code.match('llhttp__on_method_complete'),
+ onVersionComplete: p.code.match('llhttp__on_version_complete'),
+ onHeaderFieldComplete: p.code.match('llhttp__on_header_field_complete'),
+ onHeaderValueComplete: p.code.match('llhttp__on_header_value_complete'),
+ onHeadersComplete: p.code.match('llhttp__on_headers_complete'),
+ onMessageBegin: p.code.match('llhttp__on_message_begin'),
+ onMessageComplete: p.code.match('llhttp__on_message_complete'),
+ onChunkHeader: p.code.match('llhttp__on_chunk_header'),
+ onChunkExtensionName: p.code.match('llhttp__on_chunk_extension_name_complete'),
+ onChunkExtensionValue: p.code.match('llhttp__on_chunk_extension_value_complete'),
+ onChunkComplete: p.code.match('llhttp__on_chunk_complete'),
+ onReset: p.code.match('llhttp__on_reset'),
+
+ // Internal callbacks `src/http.c`
+ beforeHeadersComplete:
+ p.code.match('llhttp__before_headers_complete'),
+ afterHeadersComplete: p.code.match('llhttp__after_headers_complete'),
+ afterMessageComplete: p.code.match('llhttp__after_message_complete'),
+ };
+ /* tslint:enable:object-literal-sort-keys */
+
+ for (const name of NODES) {
+ this.nodes.set(name, p.node(name) as Match);
+ }
+ }
+
+ public build(): IHTTPResult {
+ const p = this.llparse;
+
+ p.property('i64', 'content_length');
+ p.property('i8', 'type');
+ p.property('i8', 'method');
+ p.property('i8', 'http_major');
+ p.property('i8', 'http_minor');
+ p.property('i8', 'header_state');
+ p.property('i16', 'lenient_flags');
+ p.property('i8', 'upgrade');
+ p.property('i8', 'finish');
+ p.property('i16', 'flags');
+ p.property('i16', 'status_code');
+ p.property('i8', 'initial_message_completed');
+
+ // Verify defaults
+ assert.strictEqual(FINISH.SAFE, 0);
+ assert.strictEqual(TYPE.BOTH, 0);
+
+ // Shared settings (to be used in C wrapper)
+ p.property('ptr', 'settings');
+
+ this.buildLine();
+ this.buildHeaders();
+
+ return {
+ entry: this.node('start'),
+ };
+ }
+
+ private buildLine(): void {
+ const p = this.llparse;
+ const span = this.span;
+ const n = (name: string): Match => this.node<Match>(name);
+
+ const url = this.url.build();
+
+ const switchType = this.load('type', {
+ [TYPE.REQUEST]: n('start_req'),
+ [TYPE.RESPONSE]: n('start_res'),
+ }, n('start_req_or_res'));
+
+ n('start')
+ .match([ '\r', '\n' ], n('start'))
+ .otherwise(
+ this.load('initial_message_completed', {
+ 1: this.invokePausable('on_reset', ERROR.CB_RESET, n('after_start')),
+ }, n('after_start')),
+ );
+
+ n('after_start').otherwise(
+ this.update(
+ 'finish',
+ FINISH.UNSAFE,
+ this.invokePausable('on_message_begin', ERROR.CB_MESSAGE_BEGIN, switchType),
+ ),
+ );
+
+ n('start_req_or_res')
+ .peek('H', this.span.method.start(n('req_or_res_method')))
+ .otherwise(this.update('type', TYPE.REQUEST, 'start_req'));
+
+ n('req_or_res_method')
+ .select(H_METHOD_MAP, this.store('method',
+ this.update('type', TYPE.REQUEST, this.span.method.end(
+ this.invokePausable('on_method_complete', ERROR.CB_METHOD_COMPLETE, n('req_first_space_before_url')),
+ )),
+ ))
+ .match('HTTP/', this.span.method.end(this.update('type', TYPE.RESPONSE,
+ this.span.version.start(n('res_http_major')))))
+ .otherwise(p.error(ERROR.INVALID_CONSTANT, 'Invalid word encountered'));
+
+ const checkVersion = (destination: string): Node => {
+ const node = n(destination);
+ const errorNode = this.span.version.end(p.error(ERROR.INVALID_VERSION, 'Invalid HTTP version'));
+
+ return this.testLenientFlags(LENIENT_FLAGS.VERSION,
+ {
+ 1: node,
+ },
+ this.load('http_major', {
+ 0: this.load('http_minor', {
+ 9: node,
+ }, errorNode),
+ 1: this.load('http_minor', {
+ 0: node,
+ 1: node,
+ }, errorNode),
+ 2: this.load('http_minor', {
+ 0: node,
+ }, errorNode),
+ }, errorNode),
+ );
+ };
+
+ const checkIfAllowLFWithoutCR = (success: Node, failure: Node) => {
+ return this.testLenientFlags(LENIENT_FLAGS.OPTIONAL_CR_BEFORE_LF, { 1: success }, failure);
+ };
+
+ // Response
+ n('start_res')
+ .match('HTTP/', span.version.start(n('res_http_major')))
+ .otherwise(p.error(ERROR.INVALID_CONSTANT, 'Expected HTTP/'));
+
+ n('res_http_major')
+ .select(MAJOR, this.store('http_major', 'res_http_dot'))
+ .otherwise(this.span.version.end(p.error(ERROR.INVALID_VERSION, 'Invalid major version')));
+
+ n('res_http_dot')
+ .match('.', n('res_http_minor'))
+ .otherwise(this.span.version.end(p.error(ERROR.INVALID_VERSION, 'Expected dot')));
+
+ n('res_http_minor')
+ .select(MINOR, this.store('http_minor', checkVersion('res_http_end')))
+ .otherwise(this.span.version.end(p.error(ERROR.INVALID_VERSION, 'Invalid minor version')));
+
+ n('res_http_end')
+ .otherwise(this.span.version.end(
+ this.invokePausable('on_version_complete', ERROR.CB_VERSION_COMPLETE, 'res_after_version'),
+ ));
+
+ n('res_after_version')
+ .match(' ', this.update('status_code', 0, 'res_status_code_digit_1'))
+ .otherwise(p.error(ERROR.INVALID_VERSION,
+ 'Expected space after version'));
+
+ n('res_status_code_digit_1')
+ .select(NUM_MAP, this.mulAdd('status_code', {
+ overflow: p.error(ERROR.INVALID_STATUS, 'Invalid status code'),
+ success: 'res_status_code_digit_2',
+ }))
+ .otherwise(p.error(ERROR.INVALID_STATUS, 'Invalid status code'));
+
+ n('res_status_code_digit_2')
+ .select(NUM_MAP, this.mulAdd('status_code', {
+ overflow: p.error(ERROR.INVALID_STATUS, 'Invalid status code'),
+ success: 'res_status_code_digit_3',
+ }))
+ .otherwise(p.error(ERROR.INVALID_STATUS, 'Invalid status code'));
+
+ n('res_status_code_digit_3')
+ .select(NUM_MAP, this.mulAdd('status_code', {
+ overflow: p.error(ERROR.INVALID_STATUS, 'Invalid status code'),
+ success: 'res_status_code_otherwise',
+ }))
+ .otherwise(p.error(ERROR.INVALID_STATUS, 'Invalid status code'));
+
+ const onStatusComplete = this.invokePausable(
+ 'on_status_complete', ERROR.CB_STATUS_COMPLETE, n('headers_start'),
+ );
+
+ n('res_status_code_otherwise')
+ .match(' ', n('res_status_start'))
+ .match('\r', n('res_line_almost_done'))
+ .match(
+ '\n',
+ checkIfAllowLFWithoutCR(
+ onStatusComplete,
+ p.error(ERROR.INVALID_STATUS, 'Invalid response status'),
+ ),
+ )
+ .otherwise(p.error(ERROR.INVALID_STATUS, 'Invalid response status'));
+
+ n('res_status_start')
+ .otherwise(span.status.start(n('res_status')));
+
+ n('res_status')
+ .peek('\r', span.status.end().skipTo(n('res_line_almost_done')))
+ .peek(
+ '\n',
+ span.status.end().skipTo(
+ checkIfAllowLFWithoutCR(
+ onStatusComplete,
+ p.error(ERROR.CR_EXPECTED, 'Missing expected CR after response line'),
+ ),
+ ),
+ )
+ .skipTo(n('res_status'));
+
+ n('res_line_almost_done')
+ .match(['\r', '\n'], onStatusComplete)
+ .otherwise(this.testLenientFlags(LENIENT_FLAGS.OPTIONAL_LF_AFTER_CR, {
+ 1: onStatusComplete,
+ }, p.error(ERROR.STRICT, 'Expected LF after CR')));
+
+ // Request
+ n('start_req').otherwise(this.span.method.start(n('after_start_req')));
+
+ n('after_start_req')
+ .select(METHOD_MAP, this.store('method', this.span.method.end(
+ this.invokePausable('on_method_complete', ERROR.CB_METHOD_COMPLETE, n('req_first_space_before_url'),
+ ))))
+ .otherwise(p.error(ERROR.INVALID_METHOD, 'Invalid method encountered'));
+
+ n('req_first_space_before_url')
+ .match(' ', n('req_spaces_before_url'))
+ .otherwise(p.error(ERROR.INVALID_METHOD, 'Expected space after method'));
+
+ n('req_spaces_before_url')
+ .match(' ', n('req_spaces_before_url'))
+ .otherwise(this.isEqual('method', METHODS.CONNECT, {
+ equal: url.entry.connect,
+ notEqual: url.entry.normal,
+ }));
+
+ const onUrlCompleteHTTP = this.invokePausable(
+ 'on_url_complete', ERROR.CB_URL_COMPLETE, n('req_http_start'),
+ );
+
+ url.exit.toHTTP
+ .otherwise(onUrlCompleteHTTP);
+
+ const onUrlCompleteHTTP09 = this.invokePausable(
+ 'on_url_complete', ERROR.CB_URL_COMPLETE, n('headers_start'),
+ );
+
+ url.exit.toHTTP09
+ .otherwise(
+ this.update('http_major', 0,
+ this.update('http_minor', 9, onUrlCompleteHTTP09)),
+ );
+
+ const checkMethod = (methods: METHODS[], error: string): Node => {
+ const success = n('req_http_version');
+ const failure = p.error(ERROR.INVALID_CONSTANT, error);
+
+ const map: { [key: number]: Node } = {};
+ for (const method of methods) {
+ map[method] = success;
+ }
+
+ return this.load('method', map, failure);
+ };
+
+ n('req_http_start')
+ .match('HTTP/', checkMethod(METHODS_HTTP,
+ 'Invalid method for HTTP/x.x request'))
+ .match('RTSP/', checkMethod(METHODS_RTSP,
+ 'Invalid method for RTSP/x.x request'))
+ .match('ICE/', checkMethod(METHODS_ICE,
+ 'Expected SOURCE method for ICE/x.x request'))
+ .match(' ', n('req_http_start'))
+ .otherwise(p.error(ERROR.INVALID_CONSTANT, 'Expected HTTP/'));
+
+ n('req_http_version').otherwise(span.version.start(n('req_http_major')));
+
+ n('req_http_major')
+ .select(MAJOR, this.store('http_major', 'req_http_dot'))
+ .otherwise(this.span.version.end(p.error(ERROR.INVALID_VERSION, 'Invalid major version')));
+
+ n('req_http_dot')
+ .match('.', n('req_http_minor'))
+ .otherwise(this.span.version.end(p.error(ERROR.INVALID_VERSION, 'Expected dot')));
+
+ n('req_http_minor')
+ .select(MINOR, this.store('http_minor', checkVersion('req_http_end')))
+ .otherwise(this.span.version.end(p.error(ERROR.INVALID_VERSION, 'Invalid minor version')));
+
+ n('req_http_end').otherwise(
+ span.version.end(
+ this.invokePausable(
+ 'on_version_complete',
+ ERROR.CB_VERSION_COMPLETE,
+ this.load('method', {
+ [METHODS.PRI]: n('req_pri_upgrade'),
+ }, n('req_http_complete')),
+ ),
+ ),
+ );
+
+ n('req_http_complete')
+ .match('\r', n('req_http_complete_crlf'))
+ .match(
+ '\n',
+ checkIfAllowLFWithoutCR(
+ n('req_http_complete_crlf'),
+ p.error(ERROR.INVALID_VERSION, 'Expected CRLF after version'),
+ ),
+ )
+ .otherwise(p.error(ERROR.INVALID_VERSION, 'Expected CRLF after version'));
+
+ n('req_http_complete_crlf')
+ .match('\n', n('headers_start'))
+ .otherwise(this.testLenientFlags(LENIENT_FLAGS.OPTIONAL_LF_AFTER_CR, {
+ 1: n('headers_start'),
+ }, p.error(ERROR.STRICT, 'Expected CRLF after version')));
+
+ n('req_pri_upgrade')
+ .match('\r\n\r\nSM\r\n\r\n',
+ p.error(ERROR.PAUSED_H2_UPGRADE, 'Pause on PRI/Upgrade'))
+ .otherwise(
+ p.error(ERROR.INVALID_VERSION, 'Expected HTTP/2 Connection Preface'));
+ }
+
+ private buildHeaders(): void {
+ this.buildHeaderField();
+ this.buildHeaderValue();
+ }
+
+ private buildHeaderField(): void {
+ const p = this.llparse;
+ const span = this.span;
+ const n = (name: string): Match => this.node<Match>(name);
+
+ const onInvalidHeaderFieldChar =
+ p.error(ERROR.INVALID_HEADER_TOKEN, 'Invalid header field char');
+
+ n('headers_start')
+ .match(' ',
+ this.testLenientFlags(LENIENT_FLAGS.HEADERS, {
+ 1: n('header_field_start'),
+ }, p.error(ERROR.UNEXPECTED_SPACE, 'Unexpected space after start line')),
+ )
+ .otherwise(n('header_field_start'));
+
+ n('header_field_start')
+ .match('\r', n('headers_almost_done'))
+ .match('\n',
+ this.testLenientFlags(LENIENT_FLAGS.OPTIONAL_CR_BEFORE_LF, {
+ 1: this.testFlags(FLAGS.TRAILING, {
+ 1: this.invokePausable('on_chunk_complete',
+ ERROR.CB_CHUNK_COMPLETE, 'message_done'),
+ }).otherwise(this.headersCompleted()),
+ }, onInvalidHeaderFieldChar),
+ )
+ .peek(':', p.error(ERROR.INVALID_HEADER_TOKEN, 'Invalid header token'))
+ .otherwise(span.headerField.start(n('header_field')));
+
+ n('header_field')
+ .transform(p.transform.toLower())
+ // Match headers that need special treatment
+ .select(SPECIAL_HEADERS, this.store('header_state', 'header_field_colon'))
+ .otherwise(this.resetHeaderState('header_field_general'));
+
+ /* https://www.rfc-editor.org/rfc/rfc7230.html#section-3.3.3, paragraph 3.
+ *
+ * If a message is received with both a Transfer-Encoding and a
+ * Content-Length header field, the Transfer-Encoding overrides the
+ * Content-Length. Such a message might indicate an attempt to
+ * perform request smuggling (Section 9.5) or response splitting
+ * (Section 9.4) and **ought to be handled as an error**. A sender MUST
+ * remove the received Content-Length field prior to forwarding such
+ * a message downstream.
+ *
+ * Since llhttp 9, we go for the stricter approach and treat this as an error.
+ */
+ const checkInvalidTransferEncoding = (otherwise: Node) => {
+ return this.testFlags(FLAGS.CONTENT_LENGTH, {
+ 1: this.testLenientFlags(LENIENT_FLAGS.CHUNKED_LENGTH, {
+ 0: p.error(ERROR.INVALID_TRANSFER_ENCODING, "Transfer-Encoding can't be present with Content-Length"),
+ }).otherwise(otherwise),
+ }).otherwise(otherwise);
+ };
+
+ const checkInvalidContentLength = (otherwise: Node) => {
+ return this.testFlags(FLAGS.TRANSFER_ENCODING, {
+ 1: this.testLenientFlags(LENIENT_FLAGS.CHUNKED_LENGTH, {
+ 0: p.error(ERROR.INVALID_CONTENT_LENGTH, "Content-Length can't be present with Transfer-Encoding"),
+ }).otherwise(otherwise),
+ }).otherwise(otherwise);
+ };
+
+ const onHeaderFieldComplete = this.invokePausable(
+ 'on_header_field_complete', ERROR.CB_HEADER_FIELD_COMPLETE,
+ this.load('header_state', {
+ [HEADER_STATE.TRANSFER_ENCODING]: checkInvalidTransferEncoding(n('header_value_discard_ws')),
+ [HEADER_STATE.CONTENT_LENGTH]: checkInvalidContentLength(n('header_value_discard_ws')),
+ }, 'header_value_discard_ws'),
+ );
+
+ const checkLenientFlagsOnColon =
+ this.testLenientFlags(LENIENT_FLAGS.HEADERS, {
+ 1: n('header_field_colon_discard_ws'),
+ }, span.headerField.end().skipTo(onInvalidHeaderFieldChar));
+
+ n('header_field_colon')
+ // https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.4
+ // Whitespace character is not allowed between the header field-name
+ // and colon. If the next token matches whitespace then throw an error.
+ //
+ // Add a check for the lenient flag. If the lenient flag is set, the
+ // whitespace token is allowed to support legacy code not following
+ // http specs.
+ .peek(' ', checkLenientFlagsOnColon)
+ .peek(':', span.headerField.end().skipTo(onHeaderFieldComplete))
+ // Fallback to general header, there're additional characters:
+ // `Connection-Duration` instead of `Connection` and so on.
+ .otherwise(this.resetHeaderState('header_field_general'));
+
+ n('header_field_colon_discard_ws')
+ .match(' ', n('header_field_colon_discard_ws'))
+ .otherwise(n('header_field_colon'));
+
+ n('header_field_general')
+ .match(this.TOKEN, n('header_field_general'))
+ .otherwise(n('header_field_general_otherwise'));
+
+ // Just a performance optimization, split the node so that the fast case
+ // remains in `header_field_general`
+ n('header_field_general_otherwise')
+ .peek(':', span.headerField.end().skipTo(onHeaderFieldComplete))
+ .otherwise(p.error(ERROR.INVALID_HEADER_TOKEN, 'Invalid header token'));
+ }
+
+ private buildHeaderValue(): void {
+ const p = this.llparse;
+ const span = this.span;
+ const callback = this.callback;
+ const n = (name: string): Match => this.node<Match>(name);
+
+ const fallback = this.resetHeaderState('header_value');
+
+ n('header_value_discard_ws')
+ .match([ ' ', '\t' ], n('header_value_discard_ws'))
+ .match('\r', n('header_value_discard_ws_almost_done'))
+ .match('\n', this.testLenientFlags(LENIENT_FLAGS.OPTIONAL_CR_BEFORE_LF, {
+ 1: n('header_value_discard_lws'),
+ }, p.error(ERROR.INVALID_HEADER_TOKEN, 'Invalid header value char')))
+ .otherwise(span.headerValue.start(n('header_value_start')));
+
+ n('header_value_discard_ws_almost_done')
+ .match('\n', n('header_value_discard_lws'))
+ .otherwise(
+ this.testLenientFlags(LENIENT_FLAGS.HEADERS, {
+ 1: n('header_value_discard_lws'),
+ }, p.error(ERROR.STRICT, 'Expected LF after CR')),
+ );
+
+ const onHeaderValueComplete = this.invokePausable(
+ 'on_header_value_complete', ERROR.CB_HEADER_VALUE_COMPLETE, n('header_field_start'),
+ );
+
+ const emptyContentLengthError = p.error(
+ ERROR.INVALID_CONTENT_LENGTH, 'Empty Content-Length');
+ const checkContentLengthEmptiness = this.load('header_state', {
+ [HEADER_STATE.CONTENT_LENGTH]: emptyContentLengthError,
+ }, this.setHeaderFlags(
+ this.emptySpan(span.headerValue, onHeaderValueComplete)));
+
+ n('header_value_discard_lws')
+ .match([ ' ', '\t' ], this.testLenientFlags(LENIENT_FLAGS.HEADERS, {
+ 1: n('header_value_discard_ws'),
+ }, p.error(ERROR.INVALID_HEADER_TOKEN, 'Invalid header value char')))
+ .otherwise(checkContentLengthEmptiness);
+
+ // Multiple `Transfer-Encoding` headers should be treated as one, but with
+ // values separate by a comma.
+ //
+ // See: https://tools.ietf.org/html/rfc7230#section-3.2.2
+ const toTransferEncoding = this.unsetFlag(
+ FLAGS.CHUNKED,
+ 'header_value_te_chunked');
+
+ // Once chunked has been selected, no other encoding is possible in requests
+ // https://datatracker.ietf.org/doc/html/rfc7230#section-3.3.1
+ const forbidAfterChunkedInRequest = (otherwise: Node) => {
+ return this.load('type', {
+ [TYPE.REQUEST]: this.testLenientFlags(LENIENT_FLAGS.TRANSFER_ENCODING, {
+ 0: span.headerValue.end().skipTo(
+ p.error(ERROR.INVALID_TRANSFER_ENCODING, 'Invalid `Transfer-Encoding` header value'),
+ ),
+ }).otherwise(otherwise),
+ }, otherwise);
+ };
+
+ n('header_value_start')
+ .otherwise(this.load('header_state', {
+ [HEADER_STATE.UPGRADE]: this.setFlag(FLAGS.UPGRADE, fallback),
+ [HEADER_STATE.TRANSFER_ENCODING]: this.testFlags(
+ FLAGS.CHUNKED,
+ {
+ 1: forbidAfterChunkedInRequest(this.setFlag(FLAGS.TRANSFER_ENCODING, toTransferEncoding)),
+ },
+ this.setFlag(FLAGS.TRANSFER_ENCODING, toTransferEncoding)),
+ [HEADER_STATE.CONTENT_LENGTH]: n('header_value_content_length_once'),
+ [HEADER_STATE.CONNECTION]: n('header_value_connection'),
+ }, 'header_value'));
+
+ //
+ // Transfer-Encoding
+ //
+
+ n('header_value_te_chunked')
+ .transform(p.transform.toLowerUnsafe())
+ .match(
+ 'chunked',
+ n('header_value_te_chunked_last'),
+ )
+ .otherwise(n('header_value_te_token'));
+
+ n('header_value_te_chunked_last')
+ .match(' ', n('header_value_te_chunked_last'))
+ .peek([ '\r', '\n' ], this.update('header_state',
+ HEADER_STATE.TRANSFER_ENCODING_CHUNKED,
+ 'header_value_otherwise'))
+ .peek(',', forbidAfterChunkedInRequest(n('header_value_te_chunked')))
+ .otherwise(n('header_value_te_token'));
+
+ n('header_value_te_token')
+ .match(',', n('header_value_te_token_ows'))
+ .match(CONNECTION_TOKEN_CHARS, n('header_value_te_token'))
+ .otherwise(fallback);
+
+ n('header_value_te_token_ows')
+ .match([ ' ', '\t' ], n('header_value_te_token_ows'))
+ .otherwise(n('header_value_te_chunked'));
+
+ //
+ // Content-Length
+ //
+
+ const invalidContentLength = (reason: string): Node => {
+ // End span for easier testing
+ // TODO(indutny): minimize code size
+ return span.headerValue.end()
+ .otherwise(p.error(ERROR.INVALID_CONTENT_LENGTH, reason));
+ };
+
+ n('header_value_content_length_once')
+ .otherwise(this.testFlags(FLAGS.CONTENT_LENGTH, {
+ 0: n('header_value_content_length'),
+ }, p.error(ERROR.UNEXPECTED_CONTENT_LENGTH, 'Duplicate Content-Length')));
+
+ n('header_value_content_length')
+ .select(NUM_MAP, this.mulAdd('content_length', {
+ overflow: invalidContentLength('Content-Length overflow'),
+ success: 'header_value_content_length',
+ }))
+ .otherwise(n('header_value_content_length_ws'));
+
+ n('header_value_content_length_ws')
+ .match(' ', n('header_value_content_length_ws'))
+ .peek([ '\r', '\n' ],
+ this.setFlag(FLAGS.CONTENT_LENGTH, 'header_value_otherwise'))
+ .otherwise(invalidContentLength('Invalid character in Content-Length'));
+
+ //
+ // Connection
+ //
+
+ n('header_value_connection')
+ .transform(p.transform.toLower())
+ // TODO(indutny): extra node for token back-edge?
+ // Skip lws
+ .match([ ' ', '\t' ], n('header_value_connection'))
+ .match(
+ 'close',
+ this.update('header_state', HEADER_STATE.CONNECTION_CLOSE,
+ 'header_value_connection_ws'),
+ )
+ .match(
+ 'upgrade',
+ this.update('header_state', HEADER_STATE.CONNECTION_UPGRADE,
+ 'header_value_connection_ws'),
+ )
+ .match(
+ 'keep-alive',
+ this.update('header_state', HEADER_STATE.CONNECTION_KEEP_ALIVE,
+ 'header_value_connection_ws'),
+ )
+ .otherwise(n('header_value_connection_token'));
+
+ n('header_value_connection_ws')
+ .match(',', this.setHeaderFlags('header_value_connection'))
+ .match(' ', n('header_value_connection_ws'))
+ .peek([ '\r', '\n' ], n('header_value_otherwise'))
+ .otherwise(this.resetHeaderState('header_value_connection_token'));
+
+ n('header_value_connection_token')
+ .match(',', n('header_value_connection'))
+ .match(CONNECTION_TOKEN_CHARS,
+ n('header_value_connection_token'))
+ .otherwise(n('header_value_otherwise'));
+
+ // Split for performance reasons
+ n('header_value')
+ .match(HEADER_CHARS, n('header_value'))
+ .otherwise(n('header_value_otherwise'));
+
+ const checkIfAllowLFWithoutCR = (success: Node, failure: Node) => {
+ return this.testLenientFlags(LENIENT_FLAGS.OPTIONAL_CR_BEFORE_LF, { 1: success }, failure);
+ };
+
+ const checkLenient = this.testLenientFlags(LENIENT_FLAGS.HEADERS, {
+ 1: n('header_value_lenient'),
+ }, span.headerValue.end(p.error(ERROR.INVALID_HEADER_TOKEN, 'Invalid header value char')));
+
+ n('header_value_otherwise')
+ .peek('\r', span.headerValue.end().skipTo(n('header_value_almost_done')))
+ .peek(
+ '\n',
+ span.headerValue.end(
+ checkIfAllowLFWithoutCR(
+ n('header_value_almost_done'),
+ p.error(ERROR.CR_EXPECTED, 'Missing expected CR after header value'),
+ ),
+ ),
+ )
+ .otherwise(checkLenient);
+
+ n('header_value_lenient')
+ .peek('\r', span.headerValue.end().skipTo(n('header_value_almost_done')))
+ .peek('\n', span.headerValue.end(n('header_value_almost_done')))
+ .skipTo(n('header_value_lenient'));
+
+ n('header_value_almost_done')
+ .match('\n', n('header_value_lws'))
+ .otherwise(p.error(ERROR.LF_EXPECTED,
+ 'Missing expected LF after header value'));
+
+ n('header_value_lws')
+ .peek([ ' ', '\t' ],
+ this.load('header_state', {
+ [HEADER_STATE.TRANSFER_ENCODING_CHUNKED]:
+ this.resetHeaderState(span.headerValue.start(n('header_value_start'))),
+ }, span.headerValue.start(n('header_value_start'))))
+ .otherwise(this.setHeaderFlags(onHeaderValueComplete));
+
+ const checkTrailing = this.testFlags(FLAGS.TRAILING, {
+ 1: this.invokePausable('on_chunk_complete',
+ ERROR.CB_CHUNK_COMPLETE, 'message_done'),
+ }).otherwise(this.headersCompleted());
+
+ n('headers_almost_done')
+ .match('\n', checkTrailing)
+ .otherwise(
+ this.testLenientFlags(LENIENT_FLAGS.OPTIONAL_LF_AFTER_CR, {
+ 1: checkTrailing,
+ }, p.error(ERROR.STRICT, 'Expected LF after headers')));
+
+ const upgradePause = p.pause(ERROR.PAUSED_UPGRADE,
+ 'Pause on CONNECT/Upgrade');
+
+ const afterHeadersComplete = p.invoke(callback.afterHeadersComplete, {
+ 1: this.invokePausable('on_message_complete',
+ ERROR.CB_MESSAGE_COMPLETE, upgradePause),
+ 2: n('chunk_size_start'),
+ 3: n('body_identity'),
+ 4: n('body_identity_eof'),
+
+ // non-chunked `Transfer-Encoding` for request, see `src/native/http.c`
+ 5: p.error(ERROR.INVALID_TRANSFER_ENCODING,
+ 'Request has invalid `Transfer-Encoding`'),
+ });
+
+ n('headers_done')
+ .otherwise(afterHeadersComplete);
+
+ upgradePause
+ .otherwise(n('cleanup'));
+
+ afterHeadersComplete
+ .otherwise(this.invokePausable('on_message_complete',
+ ERROR.CB_MESSAGE_COMPLETE, 'cleanup'));
+
+ n('body_identity')
+ .otherwise(span.body.start()
+ .otherwise(p.consume('content_length').otherwise(
+ span.body.end(n('message_done')))));
+
+ n('body_identity_eof')
+ .otherwise(
+ this.update('finish', FINISH.SAFE_WITH_CB, span.body.start(n('eof'))));
+
+ // Just read everything until EOF
+ n('eof')
+ .skipTo(n('eof'));
+
+ n('chunk_size_start')
+ .otherwise(this.update('content_length', 0, 'chunk_size_digit'));
+
+ const addContentLength = this.mulAdd('content_length', {
+ overflow: p.error(ERROR.INVALID_CHUNK_SIZE, 'Chunk size overflow'),
+ success: 'chunk_size',
+ }, { signed: false, base: 0x10 });
+
+ n('chunk_size_digit')
+ .select(HEX_MAP, addContentLength)
+ .otherwise(p.error(ERROR.INVALID_CHUNK_SIZE,
+ 'Invalid character in chunk size'));
+
+ n('chunk_size')
+ .select(HEX_MAP, addContentLength)
+ .otherwise(n('chunk_size_otherwise'));
+
+ n('chunk_size_otherwise')
+ .match(
+ [ ' ', '\t' ],
+ this.testLenientFlags(
+ LENIENT_FLAGS.SPACES_AFTER_CHUNK_SIZE,
+ {
+ 1: n('chunk_size_otherwise'),
+ },
+ p.error(ERROR.INVALID_CHUNK_SIZE, 'Invalid character in chunk size'),
+ ),
+ )
+ .match('\r', n('chunk_size_almost_done'))
+ .match(
+ '\n',
+ checkIfAllowLFWithoutCR(
+ n('chunk_size_almost_done'),
+ p.error(ERROR.CR_EXPECTED, 'Missing expected CR after chunk size'),
+ ),
+ )
+ .match(';', n('chunk_extensions'))
+ .otherwise(p.error(ERROR.INVALID_CHUNK_SIZE,
+ 'Invalid character in chunk size'));
+
+ const onChunkExtensionNameCompleted = (destination: Node) => {
+ return this.invokePausable(
+ 'on_chunk_extension_name', ERROR.CB_CHUNK_EXTENSION_NAME_COMPLETE, destination);
+ };
+
+ const onChunkExtensionValueCompleted = (destination: Node) => {
+ return this.invokePausable(
+ 'on_chunk_extension_value', ERROR.CB_CHUNK_EXTENSION_VALUE_COMPLETE, destination);
+ };
+
+ n('chunk_extensions')
+ .match(' ', p.error(ERROR.STRICT, 'Invalid character in chunk extensions'))
+ .match('\r', p.error(ERROR.STRICT, 'Invalid character in chunk extensions'))
+ .otherwise(this.span.chunkExtensionName.start(n('chunk_extension_name')));
+
+ n('chunk_extension_name')
+ .match(TOKEN, n('chunk_extension_name'))
+ .peek('=', this.span.chunkExtensionName.end().skipTo(
+ this.span.chunkExtensionValue.start(
+ onChunkExtensionNameCompleted(n('chunk_extension_value')),
+ ),
+ ))
+ .peek(';', this.span.chunkExtensionName.end().skipTo(
+ onChunkExtensionNameCompleted(n('chunk_extensions')),
+ ))
+ .peek('\r', this.span.chunkExtensionName.end().skipTo(
+ onChunkExtensionNameCompleted(n('chunk_size_almost_done')),
+ ))
+ .peek('\n', this.span.chunkExtensionName.end(
+ onChunkExtensionNameCompleted(
+ checkIfAllowLFWithoutCR(
+ n('chunk_size_almost_done'),
+ p.error(ERROR.CR_EXPECTED, 'Missing expected CR after chunk extension name'),
+ ),
+ ),
+ ))
+ .otherwise(this.span.chunkExtensionName.end().skipTo(
+ p.error(ERROR.STRICT, 'Invalid character in chunk extensions name'),
+ ));
+
+ n('chunk_extension_value')
+ .match('"', n('chunk_extension_quoted_value'))
+ .match(TOKEN, n('chunk_extension_value'))
+ .peek(';', this.span.chunkExtensionValue.end().skipTo(
+ onChunkExtensionValueCompleted(n('chunk_extensions')),
+ ))
+ .peek('\r', this.span.chunkExtensionValue.end().skipTo(
+ onChunkExtensionValueCompleted(n('chunk_size_almost_done')),
+ ))
+ .peek('\n', this.span.chunkExtensionValue.end(
+ onChunkExtensionValueCompleted(
+ checkIfAllowLFWithoutCR(
+ n('chunk_size_almost_done'),
+ p.error(ERROR.CR_EXPECTED, 'Missing expected CR after chunk extension value'),
+ ),
+ ),
+ ))
+ .otherwise(this.span.chunkExtensionValue.end().skipTo(
+ p.error(ERROR.STRICT, 'Invalid character in chunk extensions value'),
+ ));
+
+ n('chunk_extension_quoted_value')
+ .match(QUOTED_STRING, n('chunk_extension_quoted_value'))
+ .match('"', this.span.chunkExtensionValue.end(
+ onChunkExtensionValueCompleted(n('chunk_extension_quoted_value_done')),
+ ))
+ .match('\\', n('chunk_extension_quoted_value_quoted_pair'))
+ .otherwise(this.span.chunkExtensionValue.end().skipTo(
+ p.error(ERROR.STRICT, 'Invalid character in chunk extensions quoted value'),
+ ));
+
+ n('chunk_extension_quoted_value_quoted_pair')
+ .match(HTAB_SP_VCHAR_OBS_TEXT, n('chunk_extension_quoted_value'))
+ .otherwise(this.span.chunkExtensionValue.end().skipTo(
+ p.error(ERROR.STRICT, 'Invalid quoted-pair in chunk extensions quoted value'),
+ ));
+
+ n('chunk_extension_quoted_value_done')
+ .match(';', n('chunk_extensions'))
+ .match('\r', n('chunk_size_almost_done'))
+ .peek(
+ '\n',
+ checkIfAllowLFWithoutCR(
+ n('chunk_size_almost_done'),
+ p.error(ERROR.CR_EXPECTED, 'Missing expected CR after chunk extension value'),
+ ),
+ )
+ .otherwise(p.error(ERROR.STRICT,
+ 'Invalid character in chunk extensions quote value'));
+
+ n('chunk_size_almost_done')
+ .match('\n', n('chunk_size_almost_done_lf'))
+ .otherwise(
+ this.testLenientFlags(LENIENT_FLAGS.OPTIONAL_LF_AFTER_CR, {
+ 1: n('chunk_size_almost_done_lf'),
+ }).otherwise(p.error(ERROR.STRICT, 'Expected LF after chunk size')),
+ );
+
+ const toChunk = this.isEqual('content_length', 0, {
+ equal: this.setFlag(FLAGS.TRAILING, 'header_field_start'),
+ notEqual: 'chunk_data',
+ });
+
+ n('chunk_size_almost_done_lf')
+ .otherwise(this.invokePausable('on_chunk_header',
+ ERROR.CB_CHUNK_HEADER, toChunk));
+
+ n('chunk_data')
+ .otherwise(span.body.start()
+ .otherwise(p.consume('content_length').otherwise(
+ span.body.end(n('chunk_data_almost_done')))));
+
+ n('chunk_data_almost_done')
+ .match('\r\n', n('chunk_complete'))
+ .match(
+ '\n',
+ checkIfAllowLFWithoutCR(
+ n('chunk_complete'),
+ p.error(ERROR.CR_EXPECTED, 'Missing expected CR after chunk data'),
+ ),
+ )
+ .otherwise(
+ this.testLenientFlags(LENIENT_FLAGS.OPTIONAL_CRLF_AFTER_CHUNK, {
+ 1: n('chunk_complete'),
+ }).otherwise(p.error(ERROR.STRICT, 'Expected LF after chunk data')),
+ );
+
+ n('chunk_complete')
+ .otherwise(this.invokePausable('on_chunk_complete',
+ ERROR.CB_CHUNK_COMPLETE, 'chunk_size_start'));
+
+ const upgradeAfterDone = this.isEqual('upgrade', 1, {
+ // Exit, the rest of the message is in a different protocol.
+ equal: upgradePause,
+
+ // Restart
+ notEqual: 'cleanup',
+ });
+
+ n('message_done')
+ .otherwise(this.invokePausable('on_message_complete',
+ ERROR.CB_MESSAGE_COMPLETE, upgradeAfterDone));
+
+ const lenientClose = this.testLenientFlags(LENIENT_FLAGS.KEEP_ALIVE, {
+ 1: n('restart'),
+ }, n('closed'));
+
+ // Check if we'd like to keep-alive
+ n('cleanup')
+ .otherwise(p.invoke(callback.afterMessageComplete, {
+ 1: this.update('content_length', 0, n('restart')),
+ }, this.update('finish', FINISH.SAFE, lenientClose)));
+
+ const lenientDiscardAfterClose = this.testLenientFlags(LENIENT_FLAGS.DATA_AFTER_CLOSE, {
+ 1: n('closed'),
+ }, p.error(ERROR.CLOSED_CONNECTION, 'Data after `Connection: close`'));
+
+ n('closed')
+ .match([ '\r', '\n' ], n('closed'))
+ .skipTo(lenientDiscardAfterClose);
+
+ n('restart')
+ .otherwise(
+ this.update('initial_message_completed', 1, this.update('finish', FINISH.SAFE, n('start')),
+ ));
+ }
+
+ private headersCompleted(): Node {
+ const p = this.llparse;
+ const callback = this.callback;
+ const n = (name: string): Match => this.node<Match>(name);
+
+ // Set `upgrade` if needed
+ const beforeHeadersComplete = p.invoke(callback.beforeHeadersComplete);
+
+ /* Here we call the headers_complete callback. This is somewhat
+ * different than other callbacks because if the user returns 1, we
+ * will interpret that as saying that this message has no body. This
+ * is needed for the annoying case of receiving a response to a HEAD
+ * request.
+ *
+ * We'd like to use CALLBACK_NOTIFY_NOADVANCE() here but we cannot, so
+ * we have to simulate it by handling a change in errno below.
+ */
+ const onHeadersComplete = p.invoke(callback.onHeadersComplete, {
+ 0: n('headers_done'),
+ 1: this.setFlag(FLAGS.SKIPBODY, 'headers_done'),
+ 2: this.update('upgrade', 1,
+ this.setFlag(FLAGS.SKIPBODY, 'headers_done')),
+ [ERROR.PAUSED]: this.pause('Paused by on_headers_complete',
+ 'headers_done'),
+ }, p.error(ERROR.CB_HEADERS_COMPLETE, 'User callback error'));
+
+ beforeHeadersComplete.otherwise(onHeadersComplete);
+
+ return beforeHeadersComplete;
+ }
+
+ private node<T extends Node>(name: string | T): T {
+ if (name instanceof Node) {
+ return name;
+ }
+
+ assert(this.nodes.has(name), `Unknown node with name "${name}"`);
+ return this.nodes.get(name)! as any;
+ }
+
+ private load(field: string, map: { [key: number]: Node },
+ next?: string | Node): Node {
+ const p = this.llparse;
+
+ const res = p.invoke(p.code.load(field), map);
+ if (next !== undefined) {
+ res.otherwise(this.node(next));
+ }
+ return res;
+ }
+
+ private store(field: string, next?: string | Node): Node {
+ const p = this.llparse;
+
+ const res = p.invoke(p.code.store(field));
+ if (next !== undefined) {
+ res.otherwise(this.node(next));
+ }
+ return res;
+ }
+
+ private update(field: string, value: number, next?: string | Node): Node {
+ const p = this.llparse;
+
+ const res = p.invoke(p.code.update(field, value));
+ if (next !== undefined) {
+ res.otherwise(this.node(next));
+ }
+ return res;
+ }
+
+ private resetHeaderState(next: string | Node): Node {
+ return this.update('header_state', HEADER_STATE.GENERAL, next);
+ }
+
+ private emptySpan(span: source.Span, next: string | Node): Node {
+ return span.start(span.end(this.node(next)));
+ }
+
+ private unsetFlag(flag: FLAGS, next: string | Node): Node {
+ const p = this.llparse;
+ return p.invoke(p.code.and('flags', ~flag), this.node(next));
+ }
+
+ private setFlag(flag: FLAGS, next: string | Node): Node {
+ const p = this.llparse;
+ return p.invoke(p.code.or('flags', flag), this.node(next));
+ }
+
+ private testFlags(flag: FLAGS, map: { [key: number]: Node },
+ next?: string | Node): Node {
+ const p = this.llparse;
+ const res = p.invoke(p.code.test('flags', flag), map);
+ if (next !== undefined) {
+ res.otherwise(this.node(next));
+ }
+ return res;
+ }
+
+ private testLenientFlags(flag: LENIENT_FLAGS, map: { [key: number]: Node },
+ next?: string | Node): Node {
+ const p = this.llparse;
+ const res = p.invoke(p.code.test('lenient_flags', flag), map);
+ if (next !== undefined) {
+ res.otherwise(this.node(next));
+ }
+ return res;
+ }
+
+ private setHeaderFlags(next: string | Node): Node {
+ const HS = HEADER_STATE;
+ const F = FLAGS;
+
+ const toConnection =
+ this.update('header_state', HEADER_STATE.CONNECTION, next);
+
+ return this.load('header_state', {
+ [HS.CONNECTION_KEEP_ALIVE]:
+ this.setFlag(F.CONNECTION_KEEP_ALIVE, toConnection),
+ [HS.CONNECTION_CLOSE]: this.setFlag(F.CONNECTION_CLOSE, toConnection),
+ [HS.CONNECTION_UPGRADE]: this.setFlag(F.CONNECTION_UPGRADE, toConnection),
+ [HS.TRANSFER_ENCODING_CHUNKED]: this.setFlag(F.CHUNKED, next),
+ }, this.node(next));
+ }
+
+ private mulAdd(field: string, targets: IMulTargets,
+ options: IMulOptions = { base: 10, signed: false }): Node {
+ const p = this.llparse;
+
+ return p.invoke(p.code.mulAdd(field, options), {
+ 1: this.node(targets.overflow),
+ }, this.node(targets.success));
+ }
+
+ private isEqual(field: string, value: number, map: IIsEqualTargets) {
+ const p = this.llparse;
+ return p.invoke(p.code.isEqual(field, value), {
+ 0: this.node(map.notEqual),
+ }, this.node(map.equal));
+ }
+
+ private pause(msg: string, next?: string | Node) {
+ const p = this.llparse;
+ const res = p.pause(ERROR.PAUSED, msg);
+ if (next !== undefined) {
+ res.otherwise(this.node(next));
+ }
+ return res;
+ }
+
+ private invokePausable(name: string, errorCode: ERROR, next: string | Node)
+ : Node {
+ let cb;
+
+ switch (name) {
+ case 'on_message_begin':
+ cb = this.callback.onMessageBegin;
+ break;
+ case 'on_url_complete':
+ cb = this.callback.onUrlComplete;
+ break;
+ case 'on_status_complete':
+ cb = this.callback.onStatusComplete;
+ break;
+ case 'on_method_complete':
+ cb = this.callback.onMethodComplete;
+ break;
+ case 'on_version_complete':
+ cb = this.callback.onVersionComplete;
+ break;
+ case 'on_header_field_complete':
+ cb = this.callback.onHeaderFieldComplete;
+ break;
+ case 'on_header_value_complete':
+ cb = this.callback.onHeaderValueComplete;
+ break;
+ case 'on_message_complete':
+ cb = this.callback.onMessageComplete;
+ break;
+ case 'on_chunk_header':
+ cb = this.callback.onChunkHeader;
+ break;
+ case 'on_chunk_extension_name':
+ cb = this.callback.onChunkExtensionName;
+ break;
+ case 'on_chunk_extension_value':
+ cb = this.callback.onChunkExtensionValue;
+ break;
+ case 'on_chunk_complete':
+ cb = this.callback.onChunkComplete;
+ break;
+ case 'on_reset':
+ cb = this.callback.onReset;
+ break;
+ default:
+ throw new Error('Unknown callback: ' + name);
+ }
+
+ const p = this.llparse;
+ return p.invoke(cb, {
+ 0: this.node(next),
+ [ERROR.PAUSED]: this.pause(`${name} pause`, next),
+ }, p.error(errorCode, `\`${name}\` callback error`));
+ }
+}
diff --git a/llhttp/src/llhttp/url.ts b/llhttp/src/llhttp/url.ts
new file mode 100644
index 0000000..c5fced9
--- /dev/null
+++ b/llhttp/src/llhttp/url.ts
@@ -0,0 +1,220 @@
+import { LLParse, source } from 'llparse';
+
+import Match = source.node.Match;
+import Node = source.node.Node;
+
+import {
+ ALPHA,
+ CharList,
+ ERROR,
+ URL_CHAR,
+ USERINFO_CHARS,
+} from './constants';
+
+type SpanName = 'schema' | 'host' | 'path' | 'query' | 'fragment' | 'url';
+
+export interface IURLResult {
+ readonly entry: {
+ readonly normal: Node;
+ readonly connect: Node;
+ };
+ readonly exit: {
+ readonly toHTTP: Node;
+ readonly toHTTP09: Node;
+ };
+}
+
+type SpanTable = Map<SpanName, source.Span>;
+
+export class URL {
+ private readonly spanTable: SpanTable = new Map();
+ private readonly errorInvalid: Node;
+ private readonly URL_CHAR: CharList;
+
+ constructor(private readonly llparse: LLParse, separateSpans: boolean = false) {
+ const p = this.llparse;
+
+ this.errorInvalid = p.error(ERROR.INVALID_URL, 'Invalid characters in url');
+
+ this.URL_CHAR = URL_CHAR;
+
+ const table = this.spanTable;
+ if (separateSpans) {
+ table.set('schema', p.span(p.code.span('llhttp__on_url_schema')));
+ table.set('host', p.span(p.code.span('llhttp__on_url_host')));
+ table.set('path', p.span(p.code.span('llhttp__on_url_path')));
+ table.set('query', p.span(p.code.span('llhttp__on_url_query')));
+ table.set('fragment',
+ p.span(p.code.span('llhttp__on_url_fragment')));
+ } else {
+ table.set('url', p.span(p.code.span('llhttp__on_url')));
+ }
+ }
+
+ public build(): IURLResult {
+ const p = this.llparse;
+
+ const entry = {
+ connect: this.node('entry_connect'),
+ normal: this.node('entry_normal'),
+ };
+
+ const start = this.node('start');
+ const path = this.node('path');
+ const queryOrFragment = this.node('query_or_fragment');
+ const schema = this.node('schema');
+ const schemaDelim = this.node('schema_delim');
+ const server = this.node('server');
+ const queryStart = this.node('query_start');
+ const query = this.node('query');
+ const fragment = this.node('fragment');
+ const serverWithAt = this.node('server_with_at');
+
+ entry.normal
+ .otherwise(this.spanStart('url', start));
+
+ entry.connect
+ .otherwise(this.spanStart('url', this.spanStart('host', server)));
+
+ start
+ .peek([ '/', '*' ], this.spanStart('path').skipTo(path))
+ .peek(ALPHA, this.spanStart('schema', schema))
+ .otherwise(p.error(ERROR.INVALID_URL, 'Unexpected start char in url'));
+
+ schema
+ .match(ALPHA, schema)
+ .peek(':', this.spanEnd('schema').skipTo(schemaDelim))
+ .otherwise(p.error(ERROR.INVALID_URL, 'Unexpected char in url schema'));
+
+ schemaDelim
+ .match('//', this.spanStart('host', server))
+ .otherwise(p.error(ERROR.INVALID_URL, 'Unexpected char in url schema'));
+
+ for (const node of [server, serverWithAt]) {
+ node
+ .peek('/', this.spanEnd('host', this.spanStart('path').skipTo(path)))
+ .match('?', this.spanEnd('host', this.spanStart('query', query)))
+ .match(USERINFO_CHARS, server)
+ .match([ '[', ']' ], server)
+ .otherwise(p.error(ERROR.INVALID_URL, 'Unexpected char in url server'));
+
+ if (node !== serverWithAt) {
+ node.match('@', serverWithAt);
+ }
+ }
+
+ serverWithAt
+ .match('@', p.error(ERROR.INVALID_URL, 'Double @ in url'));
+
+ path
+ .match(this.URL_CHAR, path)
+ .otherwise(this.spanEnd('path', queryOrFragment));
+
+ // Performance optimization, split `path` so that the fast case remains
+ // there
+ queryOrFragment
+ .match('?', this.spanStart('query', query))
+ .match('#', this.spanStart('fragment', fragment))
+ .otherwise(p.error(ERROR.INVALID_URL, 'Invalid char in url path'));
+
+ query
+ .match(this.URL_CHAR, query)
+ // Allow extra '?' in query string
+ .match('?', query)
+ .peek('#', this.spanEnd('query')
+ .skipTo(this.spanStart('fragment', fragment)))
+ .otherwise(p.error(ERROR.INVALID_URL, 'Invalid char in url query'));
+
+ fragment
+ .match(this.URL_CHAR, fragment)
+ .match([ '?', '#' ], fragment)
+ .otherwise(
+ p.error(ERROR.INVALID_URL, 'Invalid char in url fragment start'));
+
+ for (const node of [ start, schema, schemaDelim ]) {
+ /* No whitespace allowed here */
+ node.match([ ' ', '\r', '\n' ], this.errorInvalid);
+ }
+
+ // Adaptors
+ const toHTTP = this.node('to_http');
+ const toHTTP09 = this.node('to_http_09');
+
+ const skipToHTTP = this.node('skip_to_http')
+ .skipTo(toHTTP);
+
+ const skipToHTTP09 = this.node('skip_to_http09')
+ .skipTo(toHTTP09);
+
+ const skipCRLF = this.node('skip_lf_to_http09')
+ .match('\r\n', toHTTP09)
+ .otherwise(p.error(ERROR.INVALID_URL, 'Expected CRLF'));
+
+ for (const node of [server, serverWithAt, queryOrFragment, queryStart, query, fragment]) {
+ let spanName: SpanName | undefined;
+
+ if (node === server || node === serverWithAt) {
+ spanName = 'host';
+ } else if (node === queryStart || node === query) {
+ spanName = 'query';
+ } else if (node === fragment) {
+ spanName = 'fragment';
+ }
+
+ const endTo = (target: Node): Node => {
+ let res: Node = this.spanEnd('url', target);
+ if (spanName !== undefined) {
+ res = this.spanEnd(spanName, res);
+ }
+ return res;
+ };
+
+ node.peek(' ', endTo(skipToHTTP));
+
+ node.peek('\r', endTo(skipCRLF));
+ node.peek('\n', endTo(skipToHTTP09));
+ }
+
+ return {
+ entry,
+ exit: {
+ toHTTP,
+ toHTTP09,
+ },
+ };
+ }
+
+ private spanStart(name: SpanName, otherwise?: Node): Node {
+ let res: Node;
+ if (this.spanTable.has(name)) {
+ res = this.spanTable.get(name)!.start();
+ } else {
+ res = this.llparse.node('span_start_stub_' + name);
+ }
+ if (otherwise !== undefined) {
+ res.otherwise(otherwise);
+ }
+ return res;
+ }
+
+ private spanEnd(name: SpanName, otherwise?: Node): Node {
+ let res: Node;
+ if (this.spanTable.has(name)) {
+ res = this.spanTable.get(name)!.end();
+ } else {
+ res = this.llparse.node('span_end_stub_' + name);
+ }
+ if (otherwise !== undefined) {
+ res.otherwise(otherwise);
+ }
+ return res;
+ }
+
+ private node(name: string): Match {
+ const res = this.llparse.node('url_' + name);
+
+ res.match([ '\t', '\f' ], this.errorInvalid);
+
+ return res;
+ }
+}
diff --git a/llhttp/src/llhttp/utils.ts b/llhttp/src/llhttp/utils.ts
new file mode 100644
index 0000000..7c01d66
--- /dev/null
+++ b/llhttp/src/llhttp/utils.ts
@@ -0,0 +1,27 @@
+export interface IEnumMap {
+ [key: string]: number;
+}
+
+export function enumToMap(
+ obj: any,
+ filter?: ReadonlyArray<number>,
+ exceptions?: ReadonlyArray<number>,
+): IEnumMap {
+ const res: IEnumMap = {};
+
+ for (const key of Object.keys(obj)) {
+ const value = obj[key];
+ if (typeof value !== 'number') {
+ continue;
+ }
+ if (filter && !filter.includes(value)) {
+ continue;
+ }
+ if (exceptions && exceptions.includes(value)) {
+ continue;
+ }
+ res[key] = value;
+ }
+
+ return res;
+}
diff --git a/llhttp/src/native/api.c b/llhttp/src/native/api.c
new file mode 100644
index 0000000..8c2ce3d
--- /dev/null
+++ b/llhttp/src/native/api.c
@@ -0,0 +1,510 @@
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+
+#include "llhttp.h"
+
+#define CALLBACK_MAYBE(PARSER, NAME) \
+ do { \
+ const llhttp_settings_t* settings; \
+ settings = (const llhttp_settings_t*) (PARSER)->settings; \
+ if (settings == NULL || settings->NAME == NULL) { \
+ err = 0; \
+ break; \
+ } \
+ err = settings->NAME((PARSER)); \
+ } while (0)
+
+#define SPAN_CALLBACK_MAYBE(PARSER, NAME, START, LEN) \
+ do { \
+ const llhttp_settings_t* settings; \
+ settings = (const llhttp_settings_t*) (PARSER)->settings; \
+ if (settings == NULL || settings->NAME == NULL) { \
+ err = 0; \
+ break; \
+ } \
+ err = settings->NAME((PARSER), (START), (LEN)); \
+ if (err == -1) { \
+ err = HPE_USER; \
+ llhttp_set_error_reason((PARSER), "Span callback error in " #NAME); \
+ } \
+ } while (0)
+
+void llhttp_init(llhttp_t* parser, llhttp_type_t type,
+ const llhttp_settings_t* settings) {
+ llhttp__internal_init(parser);
+
+ parser->type = type;
+ parser->settings = (void*) settings;
+}
+
+
+#if defined(__wasm__)
+
+extern int wasm_on_message_begin(llhttp_t * p);
+extern int wasm_on_url(llhttp_t* p, const char* at, size_t length);
+extern int wasm_on_status(llhttp_t* p, const char* at, size_t length);
+extern int wasm_on_header_field(llhttp_t* p, const char* at, size_t length);
+extern int wasm_on_header_value(llhttp_t* p, const char* at, size_t length);
+extern int wasm_on_headers_complete(llhttp_t * p, int status_code,
+ uint8_t upgrade, int should_keep_alive);
+extern int wasm_on_body(llhttp_t* p, const char* at, size_t length);
+extern int wasm_on_message_complete(llhttp_t * p);
+
+static int wasm_on_headers_complete_wrap(llhttp_t* p) {
+ return wasm_on_headers_complete(p, p->status_code, p->upgrade,
+ llhttp_should_keep_alive(p));
+}
+
+const llhttp_settings_t wasm_settings = {
+ wasm_on_message_begin,
+ wasm_on_url,
+ wasm_on_status,
+ NULL,
+ NULL,
+ wasm_on_header_field,
+ wasm_on_header_value,
+ NULL,
+ NULL,
+ wasm_on_headers_complete_wrap,
+ wasm_on_body,
+ wasm_on_message_complete,
+ NULL,
+ NULL,
+ NULL,
+ NULL,
+ NULL,
+ NULL,
+ NULL,
+ NULL,
+ NULL,
+ NULL,
+ NULL,
+};
+
+
+llhttp_t* llhttp_alloc(llhttp_type_t type) {
+ llhttp_t* parser = malloc(sizeof(llhttp_t));
+ llhttp_init(parser, type, &wasm_settings);
+ return parser;
+}
+
+void llhttp_free(llhttp_t* parser) {
+ free(parser);
+}
+
+#endif // defined(__wasm__)
+
+/* Some getters required to get stuff from the parser */
+
+uint8_t llhttp_get_type(llhttp_t* parser) {
+ return parser->type;
+}
+
+uint8_t llhttp_get_http_major(llhttp_t* parser) {
+ return parser->http_major;
+}
+
+uint8_t llhttp_get_http_minor(llhttp_t* parser) {
+ return parser->http_minor;
+}
+
+uint8_t llhttp_get_method(llhttp_t* parser) {
+ return parser->method;
+}
+
+int llhttp_get_status_code(llhttp_t* parser) {
+ return parser->status_code;
+}
+
+uint8_t llhttp_get_upgrade(llhttp_t* parser) {
+ return parser->upgrade;
+}
+
+
+void llhttp_reset(llhttp_t* parser) {
+ llhttp_type_t type = parser->type;
+ const llhttp_settings_t* settings = parser->settings;
+ void* data = parser->data;
+ uint16_t lenient_flags = parser->lenient_flags;
+
+ llhttp__internal_init(parser);
+
+ parser->type = type;
+ parser->settings = (void*) settings;
+ parser->data = data;
+ parser->lenient_flags = lenient_flags;
+}
+
+
+llhttp_errno_t llhttp_execute(llhttp_t* parser, const char* data, size_t len) {
+ return llhttp__internal_execute(parser, data, data + len);
+}
+
+
+void llhttp_settings_init(llhttp_settings_t* settings) {
+ memset(settings, 0, sizeof(*settings));
+}
+
+
+llhttp_errno_t llhttp_finish(llhttp_t* parser) {
+ int err;
+
+ /* We're in an error state. Don't bother doing anything. */
+ if (parser->error != 0) {
+ return 0;
+ }
+
+ switch (parser->finish) {
+ case HTTP_FINISH_SAFE_WITH_CB:
+ CALLBACK_MAYBE(parser, on_message_complete);
+ if (err != HPE_OK) return err;
+
+ /* FALLTHROUGH */
+ case HTTP_FINISH_SAFE:
+ return HPE_OK;
+ case HTTP_FINISH_UNSAFE:
+ parser->reason = "Invalid EOF state";
+ return HPE_INVALID_EOF_STATE;
+ default:
+ abort();
+ }
+}
+
+
+void llhttp_pause(llhttp_t* parser) {
+ if (parser->error != HPE_OK) {
+ return;
+ }
+
+ parser->error = HPE_PAUSED;
+ parser->reason = "Paused";
+}
+
+
+void llhttp_resume(llhttp_t* parser) {
+ if (parser->error != HPE_PAUSED) {
+ return;
+ }
+
+ parser->error = 0;
+}
+
+
+void llhttp_resume_after_upgrade(llhttp_t* parser) {
+ if (parser->error != HPE_PAUSED_UPGRADE) {
+ return;
+ }
+
+ parser->error = 0;
+}
+
+
+llhttp_errno_t llhttp_get_errno(const llhttp_t* parser) {
+ return parser->error;
+}
+
+
+const char* llhttp_get_error_reason(const llhttp_t* parser) {
+ return parser->reason;
+}
+
+
+void llhttp_set_error_reason(llhttp_t* parser, const char* reason) {
+ parser->reason = reason;
+}
+
+
+const char* llhttp_get_error_pos(const llhttp_t* parser) {
+ return parser->error_pos;
+}
+
+
+const char* llhttp_errno_name(llhttp_errno_t err) {
+#define HTTP_ERRNO_GEN(CODE, NAME, _) case HPE_##NAME: return "HPE_" #NAME;
+ switch (err) {
+ HTTP_ERRNO_MAP(HTTP_ERRNO_GEN)
+ default: abort();
+ }
+#undef HTTP_ERRNO_GEN
+}
+
+
+const char* llhttp_method_name(llhttp_method_t method) {
+#define HTTP_METHOD_GEN(NUM, NAME, STRING) case HTTP_##NAME: return #STRING;
+ switch (method) {
+ HTTP_ALL_METHOD_MAP(HTTP_METHOD_GEN)
+ default: abort();
+ }
+#undef HTTP_METHOD_GEN
+}
+
+const char* llhttp_status_name(llhttp_status_t status) {
+#define HTTP_STATUS_GEN(NUM, NAME, STRING) case HTTP_STATUS_##NAME: return #STRING;
+ switch (status) {
+ HTTP_STATUS_MAP(HTTP_STATUS_GEN)
+ default: abort();
+ }
+#undef HTTP_STATUS_GEN
+}
+
+
+void llhttp_set_lenient_headers(llhttp_t* parser, int enabled) {
+ if (enabled) {
+ parser->lenient_flags |= LENIENT_HEADERS;
+ } else {
+ parser->lenient_flags &= ~LENIENT_HEADERS;
+ }
+}
+
+
+void llhttp_set_lenient_chunked_length(llhttp_t* parser, int enabled) {
+ if (enabled) {
+ parser->lenient_flags |= LENIENT_CHUNKED_LENGTH;
+ } else {
+ parser->lenient_flags &= ~LENIENT_CHUNKED_LENGTH;
+ }
+}
+
+
+void llhttp_set_lenient_keep_alive(llhttp_t* parser, int enabled) {
+ if (enabled) {
+ parser->lenient_flags |= LENIENT_KEEP_ALIVE;
+ } else {
+ parser->lenient_flags &= ~LENIENT_KEEP_ALIVE;
+ }
+}
+
+void llhttp_set_lenient_transfer_encoding(llhttp_t* parser, int enabled) {
+ if (enabled) {
+ parser->lenient_flags |= LENIENT_TRANSFER_ENCODING;
+ } else {
+ parser->lenient_flags &= ~LENIENT_TRANSFER_ENCODING;
+ }
+}
+
+void llhttp_set_lenient_version(llhttp_t* parser, int enabled) {
+ if (enabled) {
+ parser->lenient_flags |= LENIENT_VERSION;
+ } else {
+ parser->lenient_flags &= ~LENIENT_VERSION;
+ }
+}
+
+void llhttp_set_lenient_data_after_close(llhttp_t* parser, int enabled) {
+ if (enabled) {
+ parser->lenient_flags |= LENIENT_DATA_AFTER_CLOSE;
+ } else {
+ parser->lenient_flags &= ~LENIENT_DATA_AFTER_CLOSE;
+ }
+}
+
+void llhttp_set_lenient_optional_lf_after_cr(llhttp_t* parser, int enabled) {
+ if (enabled) {
+ parser->lenient_flags |= LENIENT_OPTIONAL_LF_AFTER_CR;
+ } else {
+ parser->lenient_flags &= ~LENIENT_OPTIONAL_LF_AFTER_CR;
+ }
+}
+
+void llhttp_set_lenient_optional_crlf_after_chunk(llhttp_t* parser, int enabled) {
+ if (enabled) {
+ parser->lenient_flags |= LENIENT_OPTIONAL_CRLF_AFTER_CHUNK;
+ } else {
+ parser->lenient_flags &= ~LENIENT_OPTIONAL_CRLF_AFTER_CHUNK;
+ }
+}
+
+void llhttp_set_lenient_optional_cr_before_lf(llhttp_t* parser, int enabled) {
+ if (enabled) {
+ parser->lenient_flags |= LENIENT_OPTIONAL_CR_BEFORE_LF;
+ } else {
+ parser->lenient_flags &= ~LENIENT_OPTIONAL_CR_BEFORE_LF;
+ }
+}
+
+void llhttp_set_lenient_spaces_after_chunk_size(llhttp_t* parser, int enabled) {
+ if (enabled) {
+ parser->lenient_flags |= LENIENT_SPACES_AFTER_CHUNK_SIZE;
+ } else {
+ parser->lenient_flags &= ~LENIENT_SPACES_AFTER_CHUNK_SIZE;
+ }
+}
+
+/* Callbacks */
+
+
+int llhttp__on_message_begin(llhttp_t* s, const char* p, const char* endp) {
+ int err;
+ CALLBACK_MAYBE(s, on_message_begin);
+ return err;
+}
+
+
+int llhttp__on_url(llhttp_t* s, const char* p, const char* endp) {
+ int err;
+ SPAN_CALLBACK_MAYBE(s, on_url, p, endp - p);
+ return err;
+}
+
+
+int llhttp__on_url_complete(llhttp_t* s, const char* p, const char* endp) {
+ int err;
+ CALLBACK_MAYBE(s, on_url_complete);
+ return err;
+}
+
+
+int llhttp__on_status(llhttp_t* s, const char* p, const char* endp) {
+ int err;
+ SPAN_CALLBACK_MAYBE(s, on_status, p, endp - p);
+ return err;
+}
+
+
+int llhttp__on_status_complete(llhttp_t* s, const char* p, const char* endp) {
+ int err;
+ CALLBACK_MAYBE(s, on_status_complete);
+ return err;
+}
+
+
+int llhttp__on_method(llhttp_t* s, const char* p, const char* endp) {
+ int err;
+ SPAN_CALLBACK_MAYBE(s, on_method, p, endp - p);
+ return err;
+}
+
+
+int llhttp__on_method_complete(llhttp_t* s, const char* p, const char* endp) {
+ int err;
+ CALLBACK_MAYBE(s, on_method_complete);
+ return err;
+}
+
+
+int llhttp__on_version(llhttp_t* s, const char* p, const char* endp) {
+ int err;
+ SPAN_CALLBACK_MAYBE(s, on_version, p, endp - p);
+ return err;
+}
+
+
+int llhttp__on_version_complete(llhttp_t* s, const char* p, const char* endp) {
+ int err;
+ CALLBACK_MAYBE(s, on_version_complete);
+ return err;
+}
+
+
+int llhttp__on_header_field(llhttp_t* s, const char* p, const char* endp) {
+ int err;
+ SPAN_CALLBACK_MAYBE(s, on_header_field, p, endp - p);
+ return err;
+}
+
+
+int llhttp__on_header_field_complete(llhttp_t* s, const char* p, const char* endp) {
+ int err;
+ CALLBACK_MAYBE(s, on_header_field_complete);
+ return err;
+}
+
+
+int llhttp__on_header_value(llhttp_t* s, const char* p, const char* endp) {
+ int err;
+ SPAN_CALLBACK_MAYBE(s, on_header_value, p, endp - p);
+ return err;
+}
+
+
+int llhttp__on_header_value_complete(llhttp_t* s, const char* p, const char* endp) {
+ int err;
+ CALLBACK_MAYBE(s, on_header_value_complete);
+ return err;
+}
+
+
+int llhttp__on_headers_complete(llhttp_t* s, const char* p, const char* endp) {
+ int err;
+ CALLBACK_MAYBE(s, on_headers_complete);
+ return err;
+}
+
+
+int llhttp__on_message_complete(llhttp_t* s, const char* p, const char* endp) {
+ int err;
+ CALLBACK_MAYBE(s, on_message_complete);
+ return err;
+}
+
+
+int llhttp__on_body(llhttp_t* s, const char* p, const char* endp) {
+ int err;
+ SPAN_CALLBACK_MAYBE(s, on_body, p, endp - p);
+ return err;
+}
+
+
+int llhttp__on_chunk_header(llhttp_t* s, const char* p, const char* endp) {
+ int err;
+ CALLBACK_MAYBE(s, on_chunk_header);
+ return err;
+}
+
+
+int llhttp__on_chunk_extension_name(llhttp_t* s, const char* p, const char* endp) {
+ int err;
+ SPAN_CALLBACK_MAYBE(s, on_chunk_extension_name, p, endp - p);
+ return err;
+}
+
+
+int llhttp__on_chunk_extension_name_complete(llhttp_t* s, const char* p, const char* endp) {
+ int err;
+ CALLBACK_MAYBE(s, on_chunk_extension_name_complete);
+ return err;
+}
+
+
+int llhttp__on_chunk_extension_value(llhttp_t* s, const char* p, const char* endp) {
+ int err;
+ SPAN_CALLBACK_MAYBE(s, on_chunk_extension_value, p, endp - p);
+ return err;
+}
+
+
+int llhttp__on_chunk_extension_value_complete(llhttp_t* s, const char* p, const char* endp) {
+ int err;
+ CALLBACK_MAYBE(s, on_chunk_extension_value_complete);
+ return err;
+}
+
+
+int llhttp__on_chunk_complete(llhttp_t* s, const char* p, const char* endp) {
+ int err;
+ CALLBACK_MAYBE(s, on_chunk_complete);
+ return err;
+}
+
+
+int llhttp__on_reset(llhttp_t* s, const char* p, const char* endp) {
+ int err;
+ CALLBACK_MAYBE(s, on_reset);
+ return err;
+}
+
+
+/* Private */
+
+
+void llhttp__debug(llhttp_t* s, const char* p, const char* endp,
+ const char* msg) {
+ if (p == endp) {
+ fprintf(stderr, "p=%p type=%d flags=%02x next=null debug=%s\n", s, s->type,
+ s->flags, msg);
+ } else {
+ fprintf(stderr, "p=%p type=%d flags=%02x next=%02x debug=%s\n", s,
+ s->type, s->flags, *p, msg);
+ }
+}
diff --git a/llhttp/src/native/api.h b/llhttp/src/native/api.h
new file mode 100644
index 0000000..321879c
--- /dev/null
+++ b/llhttp/src/native/api.h
@@ -0,0 +1,355 @@
+#ifndef INCLUDE_LLHTTP_API_H_
+#define INCLUDE_LLHTTP_API_H_
+#ifdef __cplusplus
+extern "C" {
+#endif
+#include <stddef.h>
+
+#if defined(__wasm__)
+#define LLHTTP_EXPORT __attribute__((visibility("default")))
+#else
+#define LLHTTP_EXPORT
+#endif
+
+typedef llhttp__internal_t llhttp_t;
+typedef struct llhttp_settings_s llhttp_settings_t;
+
+typedef int (*llhttp_data_cb)(llhttp_t*, const char *at, size_t length);
+typedef int (*llhttp_cb)(llhttp_t*);
+
+struct llhttp_settings_s {
+ /* Possible return values 0, -1, `HPE_PAUSED` */
+ llhttp_cb on_message_begin;
+
+ /* Possible return values 0, -1, HPE_USER */
+ llhttp_data_cb on_url;
+ llhttp_data_cb on_status;
+ llhttp_data_cb on_method;
+ llhttp_data_cb on_version;
+ llhttp_data_cb on_header_field;
+ llhttp_data_cb on_header_value;
+ llhttp_data_cb on_chunk_extension_name;
+ llhttp_data_cb on_chunk_extension_value;
+
+ /* Possible return values:
+ * 0 - Proceed normally
+ * 1 - Assume that request/response has no body, and proceed to parsing the
+ * next message
+ * 2 - Assume absence of body (as above) and make `llhttp_execute()` return
+ * `HPE_PAUSED_UPGRADE`
+ * -1 - Error
+ * `HPE_PAUSED`
+ */
+ llhttp_cb on_headers_complete;
+
+ /* Possible return values 0, -1, HPE_USER */
+ llhttp_data_cb on_body;
+
+ /* Possible return values 0, -1, `HPE_PAUSED` */
+ llhttp_cb on_message_complete;
+ llhttp_cb on_url_complete;
+ llhttp_cb on_status_complete;
+ llhttp_cb on_method_complete;
+ llhttp_cb on_version_complete;
+ llhttp_cb on_header_field_complete;
+ llhttp_cb on_header_value_complete;
+ llhttp_cb on_chunk_extension_name_complete;
+ llhttp_cb on_chunk_extension_value_complete;
+
+ /* When on_chunk_header is called, the current chunk length is stored
+ * in parser->content_length.
+ * Possible return values 0, -1, `HPE_PAUSED`
+ */
+ llhttp_cb on_chunk_header;
+ llhttp_cb on_chunk_complete;
+ llhttp_cb on_reset;
+};
+
+/* Initialize the parser with specific type and user settings.
+ *
+ * NOTE: lifetime of `settings` has to be at least the same as the lifetime of
+ * the `parser` here. In practice, `settings` has to be either a static
+ * variable or be allocated with `malloc`, `new`, etc.
+ */
+LLHTTP_EXPORT
+void llhttp_init(llhttp_t* parser, llhttp_type_t type,
+ const llhttp_settings_t* settings);
+
+LLHTTP_EXPORT
+llhttp_t* llhttp_alloc(llhttp_type_t type);
+
+LLHTTP_EXPORT
+void llhttp_free(llhttp_t* parser);
+
+LLHTTP_EXPORT
+uint8_t llhttp_get_type(llhttp_t* parser);
+
+LLHTTP_EXPORT
+uint8_t llhttp_get_http_major(llhttp_t* parser);
+
+LLHTTP_EXPORT
+uint8_t llhttp_get_http_minor(llhttp_t* parser);
+
+LLHTTP_EXPORT
+uint8_t llhttp_get_method(llhttp_t* parser);
+
+LLHTTP_EXPORT
+int llhttp_get_status_code(llhttp_t* parser);
+
+LLHTTP_EXPORT
+uint8_t llhttp_get_upgrade(llhttp_t* parser);
+
+/* Reset an already initialized parser back to the start state, preserving the
+ * existing parser type, callback settings, user data, and lenient flags.
+ */
+LLHTTP_EXPORT
+void llhttp_reset(llhttp_t* parser);
+
+/* Initialize the settings object */
+LLHTTP_EXPORT
+void llhttp_settings_init(llhttp_settings_t* settings);
+
+/* Parse full or partial request/response, invoking user callbacks along the
+ * way.
+ *
+ * If any of `llhttp_data_cb` returns errno not equal to `HPE_OK` - the parsing
+ * interrupts, and such errno is returned from `llhttp_execute()`. If
+ * `HPE_PAUSED` was used as a errno, the execution can be resumed with
+ * `llhttp_resume()` call.
+ *
+ * In a special case of CONNECT/Upgrade request/response `HPE_PAUSED_UPGRADE`
+ * is returned after fully parsing the request/response. If the user wishes to
+ * continue parsing, they need to invoke `llhttp_resume_after_upgrade()`.
+ *
+ * NOTE: if this function ever returns a non-pause type error, it will continue
+ * to return the same error upon each successive call up until `llhttp_init()`
+ * is called.
+ */
+LLHTTP_EXPORT
+llhttp_errno_t llhttp_execute(llhttp_t* parser, const char* data, size_t len);
+
+/* This method should be called when the other side has no further bytes to
+ * send (e.g. shutdown of readable side of the TCP connection.)
+ *
+ * Requests without `Content-Length` and other messages might require treating
+ * all incoming bytes as the part of the body, up to the last byte of the
+ * connection. This method will invoke `on_message_complete()` callback if the
+ * request was terminated safely. Otherwise a error code would be returned.
+ */
+LLHTTP_EXPORT
+llhttp_errno_t llhttp_finish(llhttp_t* parser);
+
+/* Returns `1` if the incoming message is parsed until the last byte, and has
+ * to be completed by calling `llhttp_finish()` on EOF
+ */
+LLHTTP_EXPORT
+int llhttp_message_needs_eof(const llhttp_t* parser);
+
+/* Returns `1` if there might be any other messages following the last that was
+ * successfully parsed.
+ */
+LLHTTP_EXPORT
+int llhttp_should_keep_alive(const llhttp_t* parser);
+
+/* Make further calls of `llhttp_execute()` return `HPE_PAUSED` and set
+ * appropriate error reason.
+ *
+ * Important: do not call this from user callbacks! User callbacks must return
+ * `HPE_PAUSED` if pausing is required.
+ */
+LLHTTP_EXPORT
+void llhttp_pause(llhttp_t* parser);
+
+/* Might be called to resume the execution after the pause in user's callback.
+ * See `llhttp_execute()` above for details.
+ *
+ * Call this only if `llhttp_execute()` returns `HPE_PAUSED`.
+ */
+LLHTTP_EXPORT
+void llhttp_resume(llhttp_t* parser);
+
+/* Might be called to resume the execution after the pause in user's callback.
+ * See `llhttp_execute()` above for details.
+ *
+ * Call this only if `llhttp_execute()` returns `HPE_PAUSED_UPGRADE`
+ */
+LLHTTP_EXPORT
+void llhttp_resume_after_upgrade(llhttp_t* parser);
+
+/* Returns the latest return error */
+LLHTTP_EXPORT
+llhttp_errno_t llhttp_get_errno(const llhttp_t* parser);
+
+/* Returns the verbal explanation of the latest returned error.
+ *
+ * Note: User callback should set error reason when returning the error. See
+ * `llhttp_set_error_reason()` for details.
+ */
+LLHTTP_EXPORT
+const char* llhttp_get_error_reason(const llhttp_t* parser);
+
+/* Assign verbal description to the returned error. Must be called in user
+ * callbacks right before returning the errno.
+ *
+ * Note: `HPE_USER` error code might be useful in user callbacks.
+ */
+LLHTTP_EXPORT
+void llhttp_set_error_reason(llhttp_t* parser, const char* reason);
+
+/* Returns the pointer to the last parsed byte before the returned error. The
+ * pointer is relative to the `data` argument of `llhttp_execute()`.
+ *
+ * Note: this method might be useful for counting the number of parsed bytes.
+ */
+LLHTTP_EXPORT
+const char* llhttp_get_error_pos(const llhttp_t* parser);
+
+/* Returns textual name of error code */
+LLHTTP_EXPORT
+const char* llhttp_errno_name(llhttp_errno_t err);
+
+/* Returns textual name of HTTP method */
+LLHTTP_EXPORT
+const char* llhttp_method_name(llhttp_method_t method);
+
+/* Returns textual name of HTTP status */
+LLHTTP_EXPORT
+const char* llhttp_status_name(llhttp_status_t status);
+
+/* Enables/disables lenient header value parsing (disabled by default).
+ *
+ * Lenient parsing disables header value token checks, extending llhttp's
+ * protocol support to highly non-compliant clients/server. No
+ * `HPE_INVALID_HEADER_TOKEN` will be raised for incorrect header values when
+ * lenient parsing is "on".
+ *
+ * **Enabling this flag can pose a security issue since you will be exposed to
+ * request smuggling attacks. USE WITH CAUTION!**
+ */
+LLHTTP_EXPORT
+void llhttp_set_lenient_headers(llhttp_t* parser, int enabled);
+
+
+/* Enables/disables lenient handling of conflicting `Transfer-Encoding` and
+ * `Content-Length` headers (disabled by default).
+ *
+ * Normally `llhttp` would error when `Transfer-Encoding` is present in
+ * conjunction with `Content-Length`. This error is important to prevent HTTP
+ * request smuggling, but may be less desirable for small number of cases
+ * involving legacy servers.
+ *
+ * **Enabling this flag can pose a security issue since you will be exposed to
+ * request smuggling attacks. USE WITH CAUTION!**
+ */
+LLHTTP_EXPORT
+void llhttp_set_lenient_chunked_length(llhttp_t* parser, int enabled);
+
+
+/* Enables/disables lenient handling of `Connection: close` and HTTP/1.0
+ * requests responses.
+ *
+ * Normally `llhttp` would error on (in strict mode) or discard (in loose mode)
+ * the HTTP request/response after the request/response with `Connection: close`
+ * and `Content-Length`. This is important to prevent cache poisoning attacks,
+ * but might interact badly with outdated and insecure clients. With this flag
+ * the extra request/response will be parsed normally.
+ *
+ * **Enabling this flag can pose a security issue since you will be exposed to
+ * poisoning attacks. USE WITH CAUTION!**
+ */
+LLHTTP_EXPORT
+void llhttp_set_lenient_keep_alive(llhttp_t* parser, int enabled);
+
+/* Enables/disables lenient handling of `Transfer-Encoding` header.
+ *
+ * Normally `llhttp` would error when a `Transfer-Encoding` has `chunked` value
+ * and another value after it (either in a single header or in multiple
+ * headers whose value are internally joined using `, `).
+ * This is mandated by the spec to reliably determine request body size and thus
+ * avoid request smuggling.
+ * With this flag the extra value will be parsed normally.
+ *
+ * **Enabling this flag can pose a security issue since you will be exposed to
+ * request smuggling attacks. USE WITH CAUTION!**
+ */
+LLHTTP_EXPORT
+void llhttp_set_lenient_transfer_encoding(llhttp_t* parser, int enabled);
+
+/* Enables/disables lenient handling of HTTP version.
+ *
+ * Normally `llhttp` would error when the HTTP version in the request or status line
+ * is not `0.9`, `1.0`, `1.1` or `2.0`.
+ * With this flag the invalid value will be parsed normally.
+ *
+ * **Enabling this flag can pose a security issue since you will allow unsupported
+ * HTTP versions. USE WITH CAUTION!**
+ */
+LLHTTP_EXPORT
+void llhttp_set_lenient_version(llhttp_t* parser, int enabled);
+
+/* Enables/disables lenient handling of additional data received after a message ends
+ * and keep-alive is disabled.
+ *
+ * Normally `llhttp` would error when additional unexpected data is received if the message
+ * contains the `Connection` header with `close` value.
+ * With this flag the extra data will discarded without throwing an error.
+ *
+ * **Enabling this flag can pose a security issue since you will be exposed to
+ * poisoning attacks. USE WITH CAUTION!**
+ */
+LLHTTP_EXPORT
+void llhttp_set_lenient_data_after_close(llhttp_t* parser, int enabled);
+
+/* Enables/disables lenient handling of incomplete CRLF sequences.
+ *
+ * Normally `llhttp` would error when a CR is not followed by LF when terminating the
+ * request line, the status line, the headers or a chunk header.
+ * With this flag only a CR is required to terminate such sections.
+ *
+ * **Enabling this flag can pose a security issue since you will be exposed to
+ * request smuggling attacks. USE WITH CAUTION!**
+ */
+LLHTTP_EXPORT
+void llhttp_set_lenient_optional_lf_after_cr(llhttp_t* parser, int enabled);
+
+/*
+ * Enables/disables lenient handling of line separators.
+ *
+ * Normally `llhttp` would error when a LF is not preceded by CR when terminating the
+ * request line, the status line, the headers, a chunk header or a chunk data.
+ * With this flag only a LF is required to terminate such sections.
+ *
+ * **Enabling this flag can pose a security issue since you will be exposed to
+ * request smuggling attacks. USE WITH CAUTION!**
+ */
+LLHTTP_EXPORT
+void llhttp_set_lenient_optional_cr_before_lf(llhttp_t* parser, int enabled);
+
+/* Enables/disables lenient handling of chunks not separated via CRLF.
+ *
+ * Normally `llhttp` would error when after a chunk data a CRLF is missing before
+ * starting a new chunk.
+ * With this flag the new chunk can start immediately after the previous one.
+ *
+ * **Enabling this flag can pose a security issue since you will be exposed to
+ * request smuggling attacks. USE WITH CAUTION!**
+ */
+LLHTTP_EXPORT
+void llhttp_set_lenient_optional_crlf_after_chunk(llhttp_t* parser, int enabled);
+
+/* Enables/disables lenient handling of spaces after chunk size.
+ *
+ * Normally `llhttp` would error when after a chunk size is followed by one or more
+ * spaces are present instead of a CRLF or `;`.
+ * With this flag this check is disabled.
+ *
+ * **Enabling this flag can pose a security issue since you will be exposed to
+ * request smuggling attacks. USE WITH CAUTION!**
+ */
+LLHTTP_EXPORT
+void llhttp_set_lenient_spaces_after_chunk_size(llhttp_t* parser, int enabled);
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+#endif /* INCLUDE_LLHTTP_API_H_ */
diff --git a/llhttp/src/native/http.c b/llhttp/src/native/http.c
new file mode 100644
index 0000000..1ab91a5
--- /dev/null
+++ b/llhttp/src/native/http.c
@@ -0,0 +1,170 @@
+#include <stdio.h>
+#ifndef LLHTTP__TEST
+# include "llhttp.h"
+#else
+# define llhttp_t llparse_t
+#endif /* */
+
+int llhttp_message_needs_eof(const llhttp_t* parser);
+int llhttp_should_keep_alive(const llhttp_t* parser);
+
+int llhttp__before_headers_complete(llhttp_t* parser, const char* p,
+ const char* endp) {
+ /* Set this here so that on_headers_complete() callbacks can see it */
+ if ((parser->flags & F_UPGRADE) &&
+ (parser->flags & F_CONNECTION_UPGRADE)) {
+ /* For responses, "Upgrade: foo" and "Connection: upgrade" are
+ * mandatory only when it is a 101 Switching Protocols response,
+ * otherwise it is purely informational, to announce support.
+ */
+ parser->upgrade =
+ (parser->type == HTTP_REQUEST || parser->status_code == 101);
+ } else {
+ parser->upgrade = (parser->method == HTTP_CONNECT);
+ }
+ return 0;
+}
+
+
+/* Return values:
+ * 0 - No body, `restart`, message_complete
+ * 1 - CONNECT request, `restart`, message_complete, and pause
+ * 2 - chunk_size_start
+ * 3 - body_identity
+ * 4 - body_identity_eof
+ * 5 - invalid transfer-encoding for request
+ */
+int llhttp__after_headers_complete(llhttp_t* parser, const char* p,
+ const char* endp) {
+ int hasBody;
+
+ hasBody = parser->flags & F_CHUNKED || parser->content_length > 0;
+ if (
+ (parser->upgrade && (parser->method == HTTP_CONNECT ||
+ (parser->flags & F_SKIPBODY) || !hasBody)) ||
+ /* See RFC 2616 section 4.4 - 1xx e.g. Continue */
+ (parser->type == HTTP_RESPONSE && parser->status_code == 101)
+ ) {
+ /* Exit, the rest of the message is in a different protocol. */
+ return 1;
+ }
+
+ if (parser->type == HTTP_RESPONSE && parser->status_code == 100) {
+ /* No body, restart as the message is complete */
+ return 0;
+ }
+
+ /* See RFC 2616 section 4.4 */
+ if (
+ parser->flags & F_SKIPBODY || /* response to a HEAD request */
+ (
+ parser->type == HTTP_RESPONSE && (
+ parser->status_code == 102 || /* Processing */
+ parser->status_code == 103 || /* Early Hints */
+ parser->status_code == 204 || /* No Content */
+ parser->status_code == 304 /* Not Modified */
+ )
+ )
+ ) {
+ return 0;
+ } else if (parser->flags & F_CHUNKED) {
+ /* chunked encoding - ignore Content-Length header, prepare for a chunk */
+ return 2;
+ } else if (parser->flags & F_TRANSFER_ENCODING) {
+ if (parser->type == HTTP_REQUEST &&
+ (parser->lenient_flags & LENIENT_CHUNKED_LENGTH) == 0 &&
+ (parser->lenient_flags & LENIENT_TRANSFER_ENCODING) == 0) {
+ /* RFC 7230 3.3.3 */
+
+ /* If a Transfer-Encoding header field
+ * is present in a request and the chunked transfer coding is not
+ * the final encoding, the message body length cannot be determined
+ * reliably; the server MUST respond with the 400 (Bad Request)
+ * status code and then close the connection.
+ */
+ return 5;
+ } else {
+ /* RFC 7230 3.3.3 */
+
+ /* If a Transfer-Encoding header field is present in a response and
+ * the chunked transfer coding is not the final encoding, the
+ * message body length is determined by reading the connection until
+ * it is closed by the server.
+ */
+ return 4;
+ }
+ } else {
+ if (!(parser->flags & F_CONTENT_LENGTH)) {
+ if (!llhttp_message_needs_eof(parser)) {
+ /* Assume content-length 0 - read the next */
+ return 0;
+ } else {
+ /* Read body until EOF */
+ return 4;
+ }
+ } else if (parser->content_length == 0) {
+ /* Content-Length header given but zero: Content-Length: 0\r\n */
+ return 0;
+ } else {
+ /* Content-Length header given and non-zero */
+ return 3;
+ }
+ }
+}
+
+
+int llhttp__after_message_complete(llhttp_t* parser, const char* p,
+ const char* endp) {
+ int should_keep_alive;
+
+ should_keep_alive = llhttp_should_keep_alive(parser);
+ parser->finish = HTTP_FINISH_SAFE;
+ parser->flags = 0;
+
+ /* NOTE: this is ignored in loose parsing mode */
+ return should_keep_alive;
+}
+
+
+int llhttp_message_needs_eof(const llhttp_t* parser) {
+ if (parser->type == HTTP_REQUEST) {
+ return 0;
+ }
+
+ /* See RFC 2616 section 4.4 */
+ if (parser->status_code / 100 == 1 || /* 1xx e.g. Continue */
+ parser->status_code == 204 || /* No Content */
+ parser->status_code == 304 || /* Not Modified */
+ (parser->flags & F_SKIPBODY)) { /* response to a HEAD request */
+ return 0;
+ }
+
+ /* RFC 7230 3.3.3, see `llhttp__after_headers_complete` */
+ if ((parser->flags & F_TRANSFER_ENCODING) &&
+ (parser->flags & F_CHUNKED) == 0) {
+ return 1;
+ }
+
+ if (parser->flags & (F_CHUNKED | F_CONTENT_LENGTH)) {
+ return 0;
+ }
+
+ return 1;
+}
+
+
+int llhttp_should_keep_alive(const llhttp_t* parser) {
+ if (parser->http_major > 0 && parser->http_minor > 0) {
+ /* HTTP/1.1 */
+ if (parser->flags & F_CONNECTION_CLOSE) {
+ return 0;
+ }
+ } else {
+ /* HTTP/1.0 or earlier */
+ if (!(parser->flags & F_CONNECTION_KEEP_ALIVE)) {
+ return 0;
+ }
+ }
+
+ return !llhttp_message_needs_eof(parser);
+}
diff --git a/llhttp/test/fixtures/extra.c b/llhttp/test/fixtures/extra.c
new file mode 100644
index 0000000..dadf8dc
--- /dev/null
+++ b/llhttp/test/fixtures/extra.c
@@ -0,0 +1,457 @@
+#include <stdlib.h>
+
+#include "fixture.h"
+
+int llhttp__on_url(llparse_t* s, const char* p, const char* endp) {
+ if (llparse__in_bench)
+ return 0;
+
+ return llparse__print_span("url", p, endp);
+}
+
+
+int llhttp__on_url_complete(llparse_t* s, const char* p, const char* endp) {
+ if (llparse__in_bench)
+ return 0;
+
+ llparse__print(p, endp, "url complete");
+
+ #ifdef LLHTTP__TEST_PAUSE_ON_URL_COMPLETE
+ return LLPARSE__ERROR_PAUSE;
+ #else
+ return 0;
+ #endif
+}
+
+
+int llhttp__on_url_schema(llparse_t* s, const char* p, const char* endp) {
+ if (llparse__in_bench)
+ return 0;
+
+ return llparse__print_span("url.schema", p, endp);
+}
+
+
+int llhttp__on_url_host(llparse_t* s, const char* p, const char* endp) {
+ if (llparse__in_bench)
+ return 0;
+
+ return llparse__print_span("url.host", p, endp);
+}
+
+
+int llhttp__on_url_path(llparse_t* s, const char* p, const char* endp) {
+ if (llparse__in_bench)
+ return 0;
+
+ return llparse__print_span("url.path", p, endp);
+}
+
+
+int llhttp__on_url_query(llparse_t* s, const char* p, const char* endp) {
+ if (llparse__in_bench)
+ return 0;
+
+ return llparse__print_span("url.query", p, endp);
+}
+
+
+int llhttp__on_url_fragment(llparse_t* s, const char* p, const char* endp) {
+ if (llparse__in_bench)
+ return 0;
+
+ return llparse__print_span("url.fragment", p, endp);
+}
+
+
+#ifdef LLHTTP__TEST_HTTP
+
+void llhttp__test_init_request(llparse_t* s) {
+ s->type = HTTP_REQUEST;
+}
+
+
+void llhttp__test_init_response(llparse_t* s) {
+ s->type = HTTP_RESPONSE;
+}
+
+
+void llhttp__test_init_request_lenient_all(llparse_t* s) {
+ llhttp__test_init_request(s);
+ s->lenient_flags |=
+ LENIENT_HEADERS | LENIENT_CHUNKED_LENGTH | LENIENT_KEEP_ALIVE |
+ LENIENT_TRANSFER_ENCODING | LENIENT_VERSION | LENIENT_DATA_AFTER_CLOSE |
+ LENIENT_OPTIONAL_LF_AFTER_CR | LENIENT_OPTIONAL_CR_BEFORE_LF |
+ LENIENT_OPTIONAL_CRLF_AFTER_CHUNK;
+}
+
+
+void llhttp__test_init_response_lenient_all(llparse_t* s) {
+ llhttp__test_init_response(s);
+ s->lenient_flags |=
+ LENIENT_HEADERS | LENIENT_CHUNKED_LENGTH | LENIENT_KEEP_ALIVE |
+ LENIENT_TRANSFER_ENCODING | LENIENT_VERSION | LENIENT_DATA_AFTER_CLOSE |
+ LENIENT_OPTIONAL_LF_AFTER_CR | LENIENT_OPTIONAL_CR_BEFORE_LF |
+ LENIENT_OPTIONAL_CRLF_AFTER_CHUNK;
+}
+
+
+void llhttp__test_init_request_lenient_headers(llparse_t* s) {
+ llhttp__test_init_request(s);
+ s->lenient_flags |= LENIENT_HEADERS;
+}
+
+
+void llhttp__test_init_request_lenient_chunked_length(llparse_t* s) {
+ llhttp__test_init_request(s);
+ s->lenient_flags |= LENIENT_CHUNKED_LENGTH;
+}
+
+
+void llhttp__test_init_request_lenient_keep_alive(llparse_t* s) {
+ llhttp__test_init_request(s);
+ s->lenient_flags |= LENIENT_KEEP_ALIVE;
+}
+
+void llhttp__test_init_request_lenient_transfer_encoding(llparse_t* s) {
+ llhttp__test_init_request(s);
+ s->lenient_flags |= LENIENT_TRANSFER_ENCODING;
+}
+
+
+void llhttp__test_init_request_lenient_version(llparse_t* s) {
+ llhttp__test_init_request(s);
+ s->lenient_flags |= LENIENT_VERSION;
+}
+
+
+void llhttp__test_init_response_lenient_keep_alive(llparse_t* s) {
+ llhttp__test_init_response(s);
+ s->lenient_flags |= LENIENT_KEEP_ALIVE;
+}
+
+void llhttp__test_init_response_lenient_version(llparse_t* s) {
+ llhttp__test_init_response(s);
+ s->lenient_flags |= LENIENT_VERSION;
+}
+
+
+void llhttp__test_init_response_lenient_headers(llparse_t* s) {
+ llhttp__test_init_response(s);
+ s->lenient_flags |= LENIENT_HEADERS;
+}
+
+void llhttp__test_init_request_lenient_data_after_close(llparse_t* s) {
+ llhttp__test_init_request(s);
+ s->lenient_flags |= LENIENT_DATA_AFTER_CLOSE;
+}
+
+void llhttp__test_init_response_lenient_data_after_close(llparse_t* s) {
+ llhttp__test_init_response(s);
+ s->lenient_flags |= LENIENT_DATA_AFTER_CLOSE;
+}
+
+void llhttp__test_init_request_lenient_optional_lf_after_cr(llparse_t* s) {
+ llhttp__test_init_request(s);
+ s->lenient_flags |= LENIENT_OPTIONAL_LF_AFTER_CR;
+}
+
+void llhttp__test_init_response_lenient_optional_lf_after_cr(llparse_t* s) {
+ llhttp__test_init_response(s);
+ s->lenient_flags |= LENIENT_OPTIONAL_LF_AFTER_CR;
+}
+
+void llhttp__test_init_request_lenient_optional_cr_before_lf(llparse_t* s) {
+ llhttp__test_init_request(s);
+ s->lenient_flags |= LENIENT_OPTIONAL_CR_BEFORE_LF;
+}
+
+void llhttp__test_init_response_lenient_optional_cr_before_lf(llparse_t* s) {
+ llhttp__test_init_response(s);
+ s->lenient_flags |= LENIENT_OPTIONAL_CR_BEFORE_LF;
+}
+
+void llhttp__test_init_request_lenient_optional_crlf_after_chunk(llparse_t* s) {
+ llhttp__test_init_request(s);
+ s->lenient_flags |= LENIENT_OPTIONAL_CRLF_AFTER_CHUNK;
+}
+
+void llhttp__test_init_response_lenient_optional_crlf_after_chunk(llparse_t* s) {
+ llhttp__test_init_response(s);
+ s->lenient_flags |= LENIENT_OPTIONAL_CRLF_AFTER_CHUNK;
+}
+
+void llhttp__test_init_request_lenient_spaces_after_chunk_size(llparse_t* s) {
+ llhttp__test_init_request(s);
+ s->lenient_flags |= LENIENT_SPACES_AFTER_CHUNK_SIZE;
+}
+
+void llhttp__test_init_response_lenient_spaces_after_chunk_size(llparse_t* s) {
+ llhttp__test_init_response(s);
+ s->lenient_flags |= LENIENT_SPACES_AFTER_CHUNK_SIZE;
+}
+
+
+void llhttp__test_finish(llparse_t* s) {
+ llparse__print(NULL, NULL, "finish=%d", s->finish);
+}
+
+
+int llhttp__on_message_begin(llparse_t* s, const char* p, const char* endp) {
+ if (llparse__in_bench)
+ return 0;
+
+ llparse__print(p, endp, "message begin");
+
+ #ifdef LLHTTP__TEST_PAUSE_ON_MESSAGE_BEGIN
+ return LLPARSE__ERROR_PAUSE;
+ #else
+ return 0;
+ #endif
+}
+
+
+int llhttp__on_message_complete(llparse_t* s, const char* p, const char* endp) {
+ if (llparse__in_bench)
+ return 0;
+
+ llparse__print(p, endp, "message complete");
+
+ #ifdef LLHTTP__TEST_PAUSE_ON_MESSAGE_COMPLETE
+ return LLPARSE__ERROR_PAUSE;
+ #else
+ return 0;
+ #endif
+}
+
+
+int llhttp__on_status(llparse_t* s, const char* p, const char* endp) {
+ if (llparse__in_bench)
+ return 0;
+
+ return llparse__print_span("status", p, endp);
+}
+
+
+int llhttp__on_status_complete(llparse_t* s, const char* p, const char* endp) {
+ if (llparse__in_bench)
+ return 0;
+
+ llparse__print(p, endp, "status complete");
+
+ #ifdef LLHTTP__TEST_PAUSE_ON_STATUS_COMPLETE
+ return LLPARSE__ERROR_PAUSE;
+ #else
+ return 0;
+ #endif
+}
+
+
+int llhttp__on_method(llparse_t* s, const char* p, const char* endp) {
+ if (llparse__in_bench || s->type != HTTP_REQUEST)
+ return 0;
+
+ return llparse__print_span("method", p, endp);
+}
+
+
+int llhttp__on_method_complete(llparse_t* s, const char* p, const char* endp) {
+ if (llparse__in_bench)
+ return 0;
+
+ llparse__print(p, endp, "method complete");
+
+ #ifdef LLHTTP__TEST_PAUSE_ON_METHOD_COMPLETE
+ return LLPARSE__ERROR_PAUSE;
+ #else
+ return 0;
+ #endif
+}
+
+
+int llhttp__on_version(llparse_t* s, const char* p, const char* endp) {
+ if (llparse__in_bench)
+ return 0;
+
+ return llparse__print_span("version", p, endp);
+}
+
+
+int llhttp__on_version_complete(llparse_t* s, const char* p, const char* endp) {
+ if (llparse__in_bench)
+ return 0;
+
+ llparse__print(p, endp, "version complete");
+
+ #ifdef LLHTTP__TEST_PAUSE_ON_VERSION_COMPLETE
+ return LLPARSE__ERROR_PAUSE;
+ #else
+ return 0;
+ #endif
+}
+
+int llhttp__on_header_field(llparse_t* s, const char* p, const char* endp) {
+ if (llparse__in_bench)
+ return 0;
+
+ return llparse__print_span("header_field", p, endp);
+}
+
+
+int llhttp__on_header_field_complete(llparse_t* s, const char* p, const char* endp) {
+ if (llparse__in_bench)
+ return 0;
+
+ llparse__print(p, endp, "header_field complete");
+
+ #ifdef LLHTTP__TEST_PAUSE_ON_HEADER_FIELD_COMPLETE
+ return LLPARSE__ERROR_PAUSE;
+ #else
+ return 0;
+ #endif
+}
+
+
+int llhttp__on_header_value(llparse_t* s, const char* p, const char* endp) {
+ if (llparse__in_bench)
+ return 0;
+
+ return llparse__print_span("header_value", p, endp);
+}
+
+
+int llhttp__on_header_value_complete(llparse_t* s, const char* p, const char* endp) {
+ if (llparse__in_bench)
+ return 0;
+
+ llparse__print(p, endp, "header_value complete");
+
+ #ifdef LLHTTP__TEST_PAUSE_ON_HEADER_VALUE_COMPLETE
+ return LLPARSE__ERROR_PAUSE;
+ #else
+ return 0;
+ #endif
+}
+
+
+int llhttp__on_headers_complete(llparse_t* s, const char* p, const char* endp) {
+ if (llparse__in_bench)
+ return 0;
+
+ if (s->type == HTTP_REQUEST) {
+ llparse__print(p, endp,
+ "headers complete method=%d v=%d/%d flags=%x content_length=%llu",
+ s->method, s->http_major, s->http_minor, s->flags, s->content_length);
+ } else if (s->type == HTTP_RESPONSE) {
+ llparse__print(p, endp,
+ "headers complete status=%d v=%d/%d flags=%x content_length=%llu",
+ s->status_code, s->http_major, s->http_minor, s->flags,
+ s->content_length);
+ } else {
+ llparse__print(p, endp, "invalid headers complete");
+ }
+
+ #ifdef LLHTTP__TEST_PAUSE_ON_HEADERS_COMPLETE
+ return LLPARSE__ERROR_PAUSE;
+ #elif defined(LLHTTP__TEST_SKIP_BODY)
+ llparse__print(p, endp, "skip body");
+ return 1;
+ #else
+ return 0;
+ #endif
+}
+
+
+int llhttp__on_body(llparse_t* s, const char* p, const char* endp) {
+ if (llparse__in_bench)
+ return 0;
+
+ return llparse__print_span("body", p, endp);
+}
+
+
+int llhttp__on_chunk_header(llparse_t* s, const char* p, const char* endp) {
+ if (llparse__in_bench)
+ return 0;
+
+ llparse__print(p, endp, "chunk header len=%d", (int) s->content_length);
+
+ #ifdef LLHTTP__TEST_PAUSE_ON_CHUNK_HEADER
+ return LLPARSE__ERROR_PAUSE;
+ #else
+ return 0;
+ #endif
+}
+
+
+int llhttp__on_chunk_extension_name(llparse_t* s, const char* p, const char* endp) {
+ if (llparse__in_bench)
+ return 0;
+
+ return llparse__print_span("chunk_extension_name", p, endp);
+}
+
+
+int llhttp__on_chunk_extension_name_complete(llparse_t* s, const char* p, const char* endp) {
+ if (llparse__in_bench)
+ return 0;
+
+ llparse__print(p, endp, "chunk_extension_name complete");
+
+ #ifdef LLHTTP__TEST_PAUSE_ON_CHUNK_EXTENSION_NAME
+ return LLPARSE__ERROR_PAUSE;
+ #else
+ return 0;
+ #endif
+}
+
+
+int llhttp__on_chunk_extension_value(llparse_t* s, const char* p, const char* endp) {
+ if (llparse__in_bench)
+ return 0;
+
+ return llparse__print_span("chunk_extension_value", p, endp);
+}
+
+
+int llhttp__on_chunk_extension_value_complete(llparse_t* s, const char* p, const char* endp) {
+ if (llparse__in_bench)
+ return 0;
+
+ llparse__print(p, endp, "chunk_extension_value complete");
+
+ #ifdef LLHTTP__TEST_PAUSE_ON_CHUNK_EXTENSION_VALUE
+ return LLPARSE__ERROR_PAUSE;
+ #else
+ return 0;
+ #endif
+}
+
+
+int llhttp__on_chunk_complete(llparse_t* s, const char* p, const char* endp) {
+ if (llparse__in_bench)
+ return 0;
+
+ llparse__print(p, endp, "chunk complete");
+
+ #ifdef LLHTTP__TEST_PAUSE_ON_CHUNK_COMPLETE
+ return LLPARSE__ERROR_PAUSE;
+ #else
+ return 0;
+ #endif
+}
+
+int llhttp__on_reset(llparse_t* s, const char* p, const char* endp) {
+ if (llparse__in_bench)
+ return 0;
+
+ llparse__print(p, endp, "reset");
+
+ #ifdef LLHTTP__TEST_PAUSE_ON_RESET
+ return LLPARSE__ERROR_PAUSE;
+ #else
+ return 0;
+ #endif
+}
+
+#endif /* LLHTTP__TEST_HTTP */
diff --git a/llhttp/test/fixtures/index.ts b/llhttp/test/fixtures/index.ts
new file mode 100644
index 0000000..1571f9d
--- /dev/null
+++ b/llhttp/test/fixtures/index.ts
@@ -0,0 +1,116 @@
+import * as fs from 'fs';
+import { ICompilerResult, LLParse } from 'llparse';
+import { Dot } from 'llparse-dot';
+import {
+ Fixture, FixtureResult, IFixtureBuildOptions,
+} from 'llparse-test-fixture';
+import * as path from 'path';
+
+import * as llhttp from '../../src/llhttp';
+
+export { FixtureResult };
+
+export type TestType = 'request' | 'response' | 'request-finish' | 'response-finish' |
+ 'request-lenient-all' | 'response-lenient-all' |
+ 'request-lenient-headers' | 'response-lenient-headers' |
+ 'request-lenient-chunked-length' | 'request-lenient-transfer-encoding' |
+ 'request-lenient-keep-alive' | 'response-lenient-keep-alive' |
+ 'request-lenient-version' | 'response-lenient-version' |
+ 'request-lenient-data-after-close' | 'response-lenient-data-after-close' |
+ 'request-lenient-optional-lf-after-cr' | 'response-lenient-optional-lf-after-cr' |
+ 'request-lenient-optional-cr-before-lf' | 'response-lenient-optional-cr-before-lf' |
+ 'request-lenient-optional-crlf-after-chunk' | 'response-lenient-optional-crlf-after-chunk' |
+ 'request-lenient-spaces-after-chunk-size' | 'response-lenient-spaces-after-chunk-size' |
+ 'none' | 'url';
+
+export const allowedTypes: TestType[] = [
+ 'request',
+ 'response',
+ 'request-finish',
+ 'response-finish',
+ 'request-lenient-all',
+ 'response-lenient-all',
+ 'request-lenient-headers',
+ 'response-lenient-headers',
+ 'request-lenient-keep-alive',
+ 'response-lenient-keep-alive',
+ 'request-lenient-chunked-length',
+ 'request-lenient-transfer-encoding',
+ 'request-lenient-version',
+ 'response-lenient-version',
+ 'request-lenient-data-after-close',
+ 'response-lenient-data-after-close',
+ 'request-lenient-optional-lf-after-cr',
+ 'response-lenient-optional-lf-after-cr',
+ 'request-lenient-optional-cr-before-lf',
+ 'response-lenient-optional-cr-before-lf',
+ 'request-lenient-optional-crlf-after-chunk',
+ 'response-lenient-optional-crlf-after-chunk',
+ 'request-lenient-spaces-after-chunk-size',
+ 'response-lenient-spaces-after-chunk-size',
+];
+
+const BUILD_DIR = path.join(__dirname, '..', 'tmp');
+const CHEADERS_FILE = path.join(BUILD_DIR, 'cheaders.h');
+
+const cheaders = new llhttp.CHeaders().build();
+try {
+ fs.mkdirSync(BUILD_DIR);
+} catch (e) {
+ // no-op
+}
+fs.writeFileSync(CHEADERS_FILE, cheaders);
+
+const fixtures = new Fixture({
+ buildDir: path.join(__dirname, '..', 'tmp'),
+ extra: [
+ '-msse4.2',
+ '-DLLHTTP__TEST',
+ '-DLLPARSE__ERROR_PAUSE=' + llhttp.constants.ERROR.PAUSED,
+ '-include', CHEADERS_FILE,
+ path.join(__dirname, 'extra.c'),
+ ],
+ maxParallel: process.env.LLPARSE_DEBUG ? 1 : undefined,
+});
+
+const cache: Map<any, ICompilerResult> = new Map();
+
+export async function build(
+ llparse: LLParse, node: any, outFile: string,
+ options: IFixtureBuildOptions = {},
+ ty: TestType = 'none'): Promise<FixtureResult> {
+ const dot = new Dot();
+ fs.writeFileSync(path.join(BUILD_DIR, outFile + '.dot'),
+ dot.build(node));
+
+ let artifacts: ICompilerResult;
+ if (cache.has(node)) {
+ artifacts = cache.get(node)!;
+ } else {
+ artifacts = llparse.build(node, {
+ c: { header: outFile },
+ debug: process.env.LLPARSE_DEBUG ? 'llparse__debug' : undefined,
+ });
+ cache.set(node, artifacts);
+ }
+
+ const extra = options.extra === undefined ? [] : options.extra.slice();
+
+ if (allowedTypes.includes(ty)) {
+ extra.push(
+ `-DLLPARSE__TEST_INIT=llhttp__test_init_${ty.replace(/-/g, '_')}`);
+ }
+
+ if (ty === 'request-finish' || ty === 'response-finish') {
+ if (ty === 'request-finish') {
+ extra.push('-DLLPARSE__TEST_INIT=llhttp__test_init_request');
+ } else {
+ extra.push('-DLLPARSE__TEST_INIT=llhttp__test_init_response');
+ }
+ extra.push('-DLLPARSE__TEST_FINISH=llhttp__test_finish');
+ }
+
+ return await fixtures.build(artifacts, outFile, Object.assign(options, {
+ extra,
+ }));
+}
diff --git a/llhttp/test/fuzzers/fuzz_parser.c b/llhttp/test/fuzzers/fuzz_parser.c
new file mode 100644
index 0000000..60d00ae
--- /dev/null
+++ b/llhttp/test/fuzzers/fuzz_parser.c
@@ -0,0 +1,45 @@
+#include "llhttp.h"
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+int handle_on_message_complete(llhttp_t *arg) { return 0; }
+
+int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
+ llhttp_t parser;
+ llhttp_settings_t settings;
+ llhttp_type_t http_type;
+
+ /* We need four bytes to determine variable parameters */
+ if (size < 4) {
+ return 0;
+ }
+
+ int headers = (data[0] & 0x01) == 1;
+ int chunked_length = (data[1] & 0x01) == 1;
+ int keep_alive = (data[2] & 0x01) == 1;
+ if (data[0] % 3 == 0) {
+ http_type = HTTP_BOTH;
+ } else if (data[0] % 3 == 1) {
+ http_type = HTTP_REQUEST;
+ } else {
+ http_type = HTTP_RESPONSE;
+ }
+ data += 4;
+ size -= 4;
+
+ /* Initialize user callbacks and settings */
+ llhttp_settings_init(&settings);
+
+ /* Set user callback */
+ settings.on_message_complete = handle_on_message_complete;
+
+ llhttp_init(&parser, http_type, &settings);
+ llhttp_set_lenient_headers(&parser, headers);
+ llhttp_set_lenient_chunked_length(&parser, chunked_length);
+ llhttp_set_lenient_keep_alive(&parser, keep_alive);
+
+ llhttp_execute(&parser, data, size);
+
+ return 0;
+}
diff --git a/llhttp/test/md-test.ts b/llhttp/test/md-test.ts
new file mode 100644
index 0000000..0c24e18
--- /dev/null
+++ b/llhttp/test/md-test.ts
@@ -0,0 +1,269 @@
+import * as assert from 'assert';
+import * as fs from 'fs';
+import { LLParse } from 'llparse';
+import { Group, MDGator, Metadata, Test } from 'mdgator';
+import * as path from 'path';
+import * as vm from 'vm';
+
+import * as llhttp from '../src/llhttp';
+import {IHTTPResult} from '../src/llhttp/http';
+import {IURLResult} from '../src/llhttp/url';
+import { allowedTypes, build, FixtureResult, TestType } from './fixtures';
+
+//
+// Cache nodes/llparse instances ahead of time
+// (different types of tests will re-use them)
+//
+
+interface INodeCacheEntry {
+ llparse: LLParse;
+ entry: IHTTPResult['entry'];
+}
+
+interface IUrlCacheEntry {
+ llparse: LLParse;
+ entry: IURLResult['entry']['normal'];
+}
+
+const modeCache = new Map<string, FixtureResult>();
+
+function buildNode() {
+ const p = new LLParse();
+ const instance = new llhttp.HTTP(p);
+
+ return { llparse: p, entry: instance.build().entry };
+}
+
+function buildURL() {
+ const p = new LLParse();
+ const instance = new llhttp.URL(p, true);
+
+ const node = instance.build();
+
+ // Loop
+ node.exit.toHTTP.otherwise(node.entry.normal);
+ node.exit.toHTTP09.otherwise(node.entry.normal);
+
+ return { llparse: p, entry: node.entry.normal };
+}
+
+//
+// Build binaries using cached nodes/llparse
+//
+
+async function buildMode(ty: TestType, meta: any)
+ : Promise<FixtureResult> {
+
+ const cacheKey = `${ty}:${JSON.stringify(meta || {})}`;
+ let entry = modeCache.get(cacheKey);
+
+ if (entry) {
+ return entry;
+ }
+
+ let node;
+ let prefix: string;
+ let extra: string[];
+ if (ty === 'url') {
+ node = buildURL();
+ prefix = 'url';
+ extra = [];
+ } else {
+ node = buildNode();
+ prefix = 'http';
+ extra = [
+ '-DLLHTTP__TEST_HTTP',
+ path.join(__dirname, '..', 'src', 'native', 'http.c'),
+ ];
+ }
+
+ if (meta.pause) {
+ extra.push(`-DLLHTTP__TEST_PAUSE_${meta.pause.toUpperCase()}=1`);
+ }
+
+ if (meta.skipBody) {
+ extra.push('-DLLHTTP__TEST_SKIP_BODY=1');
+ }
+
+ entry = await build(node.llparse, node.entry, `${prefix}-${ty}`, {
+ extra,
+ }, ty);
+
+ modeCache.set(cacheKey, entry);
+ return entry;
+}
+
+interface IFixtureMap {
+ [key: string]: { [key: string]: Promise<FixtureResult> };
+}
+
+//
+// Run test suite
+//
+
+function run(name: string): void {
+ const md = new MDGator();
+
+ const raw = fs.readFileSync(path.join(__dirname, name + '.md')).toString();
+ const groups = md.parse(raw);
+
+ function runSingleTest(ty: TestType, meta: any,
+ input: string,
+ expected: ReadonlyArray<string | RegExp>): void {
+ it(`should pass for type="${ty}"`, async () => {
+ const binary = await buildMode(ty, meta);
+ await binary.check(input, expected, {
+ noScan: meta.noScan === true,
+ });
+ });
+ }
+
+ function runTest(test: Test) {
+ describe(test.name + ` at ${name}.md:${test.line + 1}`, () => {
+ let types: TestType[] = [];
+
+ const isURL = test.values.has('url');
+ const inputKey = isURL ? 'url' : 'http';
+
+ assert(test.values.has(inputKey),
+ `Missing "${inputKey}" code in md file`);
+ assert.strictEqual(test.values.get(inputKey)!.length, 1,
+ `Expected just one "${inputKey}" input`);
+
+ let meta: Metadata;
+ if (test.meta.has(inputKey)) {
+ meta = test.meta.get(inputKey)![0]!;
+ } else {
+ assert(isURL, 'Missing required http metadata');
+ meta = {};
+ }
+
+ if (isURL) {
+ types = [ 'url' ];
+ } else {
+ assert(meta.hasOwnProperty('type'), 'Missing required `type` metadata');
+
+ if (meta.type) {
+ if (!allowedTypes.includes(meta.type)) {
+ throw new Error(`Invalid value of \`type\` metadata: "${meta.type}"`);
+ }
+
+ types.push(meta.type);
+ }
+ }
+
+ assert(test.values.has('log'), 'Missing `log` code in md file');
+
+ assert.strictEqual(test.values.get('log')!.length, 1,
+ 'Expected just one output');
+
+ let input: string = test.values.get(inputKey)![0];
+ let expected: string = test.values.get('log')![0];
+
+ // Remove trailing newline
+ input = input.replace(/\n$/, '');
+
+ // Remove escaped newlines
+ input = input.replace(/\\(\r\n|\r|\n)/g, '');
+
+ // Normalize all newlines
+ input = input.replace(/\r\n|\r|\n/g, '\r\n');
+
+ // Replace escaped CRLF, tabs, form-feed
+ input = input.replace(/\\r/g, '\r');
+ input = input.replace(/\\n/g, '\n');
+ input = input.replace(/\\t/g, '\t');
+ input = input.replace(/\\f/g, '\f');
+ input = input.replace(/\\x([0-9a-fA-F]+)/g, (all, hex) => {
+ return String.fromCharCode(parseInt(hex, 16));
+ });
+
+ // Useful in token tests
+ input = input.replace(/\\([0-7]{1,3})/g, (_, digits) => {
+ return String.fromCharCode(parseInt(digits, 8));
+ });
+
+ // Evaluate inline JavaScript
+ input = input.replace(/\$\{(.+?)\}/g, (_, code) => {
+ return vm.runInNewContext(code) + '';
+ });
+
+ // Escape first symbol `\r` or `\n`, `|`, `&` for Windows
+ if (process.platform === 'win32') {
+ const firstByte = Buffer.from(input)[0];
+ if (firstByte === 0x0a || firstByte === 0x0d) {
+ input = '\\' + input;
+ }
+
+ input = input.replace(/\|/g, '^|');
+ input = input.replace(/&/g, '^&');
+ }
+
+ // Replace escaped tabs/form-feed in expected too
+ expected = expected.replace(/\\t/g, '\t');
+ expected = expected.replace(/\\f/g, '\f');
+
+ // Split
+ const expectedLines = expected.split(/\n/g).slice(0, -1);
+
+ const fullExpected = expectedLines.map((line) => {
+ if (line.startsWith('/')) {
+ return new RegExp(line.trim().slice(1, -1));
+ } else {
+ return line;
+ }
+ });
+
+ for (const ty of types) {
+ if (meta.skip === true || (process.env.ONLY === 'true' && !meta.only)) {
+ continue;
+ }
+
+ runSingleTest(ty, meta, input, fullExpected);
+ }
+ });
+ }
+
+ function runGroup(group: Group) {
+ describe(group.name + ` at ${name}.md:${group.line + 1}`, function() {
+ this.timeout(60000);
+
+ for (const child of group.children) {
+ runGroup(child);
+ }
+
+ for (const test of group.tests) {
+ runTest(test);
+ }
+ });
+ }
+
+ for (const group of groups) {
+ runGroup(group);
+ }
+}
+
+run('request/sample');
+run('request/lenient-headers');
+run('request/lenient-version');
+run('request/method');
+run('request/uri');
+run('request/connection');
+run('request/content-length');
+run('request/transfer-encoding');
+run('request/invalid');
+run('request/finish');
+run('request/pausing');
+run('request/pipelining');
+
+run('response/sample');
+run('response/connection');
+run('response/content-length');
+run('response/transfer-encoding');
+run('response/invalid');
+run('response/finish');
+run('response/lenient-version');
+run('response/pausing');
+run('response/pipelining');
+
+run('url');
diff --git a/llhttp/test/request/connection.md b/llhttp/test/request/connection.md
new file mode 100644
index 0000000..a03242e
--- /dev/null
+++ b/llhttp/test/request/connection.md
@@ -0,0 +1,732 @@
+Connection header
+=================
+
+## `keep-alive`
+
+### Setting flag
+
+<!-- meta={"type": "request"} -->
+```http
+PUT /url HTTP/1.1
+Connection: keep-alive
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="PUT"
+off=3 method complete
+off=4 len=4 span[url]="/url"
+off=9 url complete
+off=14 len=3 span[version]="1.1"
+off=17 version complete
+off=19 len=10 span[header_field]="Connection"
+off=30 header_field complete
+off=31 len=10 span[header_value]="keep-alive"
+off=43 header_value complete
+off=45 headers complete method=4 v=1/1 flags=1 content_length=0
+off=45 message complete
+```
+
+### Restarting when keep-alive is explicitly
+
+<!-- meta={"type": "request"} -->
+```http
+PUT /url HTTP/1.1
+Connection: keep-alive
+
+PUT /url HTTP/1.1
+Connection: keep-alive
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="PUT"
+off=3 method complete
+off=4 len=4 span[url]="/url"
+off=9 url complete
+off=14 len=3 span[version]="1.1"
+off=17 version complete
+off=19 len=10 span[header_field]="Connection"
+off=30 header_field complete
+off=31 len=10 span[header_value]="keep-alive"
+off=43 header_value complete
+off=45 headers complete method=4 v=1/1 flags=1 content_length=0
+off=45 message complete
+off=45 reset
+off=45 message begin
+off=45 len=3 span[method]="PUT"
+off=48 method complete
+off=49 len=4 span[url]="/url"
+off=54 url complete
+off=59 len=3 span[version]="1.1"
+off=62 version complete
+off=64 len=10 span[header_field]="Connection"
+off=75 header_field complete
+off=76 len=10 span[header_value]="keep-alive"
+off=88 header_value complete
+off=90 headers complete method=4 v=1/1 flags=1 content_length=0
+off=90 message complete
+```
+
+### No restart when keep-alive is off (1.0)
+
+<!-- meta={"type": "request" } -->
+```http
+PUT /url HTTP/1.0
+
+PUT /url HTTP/1.1
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="PUT"
+off=3 method complete
+off=4 len=4 span[url]="/url"
+off=9 url complete
+off=14 len=3 span[version]="1.0"
+off=17 version complete
+off=21 headers complete method=4 v=1/0 flags=0 content_length=0
+off=21 message complete
+off=22 error code=5 reason="Data after `Connection: close`"
+```
+
+### Resetting flags when keep-alive is off (1.0, lenient)
+
+Even though we allow restarts in loose mode, the flags should be still set to
+`0` upon restart.
+
+<!-- meta={"type": "request-lenient-keep-alive"} -->
+```http
+PUT /url HTTP/1.0
+Content-Length: 0
+
+PUT /url HTTP/1.1
+Transfer-Encoding: chunked
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="PUT"
+off=3 method complete
+off=4 len=4 span[url]="/url"
+off=9 url complete
+off=14 len=3 span[version]="1.0"
+off=17 version complete
+off=19 len=14 span[header_field]="Content-Length"
+off=34 header_field complete
+off=35 len=1 span[header_value]="0"
+off=38 header_value complete
+off=40 headers complete method=4 v=1/0 flags=20 content_length=0
+off=40 message complete
+off=40 reset
+off=40 message begin
+off=40 len=3 span[method]="PUT"
+off=43 method complete
+off=44 len=4 span[url]="/url"
+off=49 url complete
+off=54 len=3 span[version]="1.1"
+off=57 version complete
+off=59 len=17 span[header_field]="Transfer-Encoding"
+off=77 header_field complete
+off=78 len=7 span[header_value]="chunked"
+off=87 header_value complete
+off=89 headers complete method=4 v=1/1 flags=208 content_length=0
+```
+
+### CRLF between requests, implicit `keep-alive`
+
+<!-- meta={"type": "request"} -->
+```http
+POST / HTTP/1.1
+Host: www.example.com
+Content-Type: application/x-www-form-urlencoded
+Content-Length: 4
+
+q=42
+
+GET / HTTP/1.1
+```
+_Note the trailing CRLF above_
+
+```log
+off=0 message begin
+off=0 len=4 span[method]="POST"
+off=4 method complete
+off=5 len=1 span[url]="/"
+off=7 url complete
+off=12 len=3 span[version]="1.1"
+off=15 version complete
+off=17 len=4 span[header_field]="Host"
+off=22 header_field complete
+off=23 len=15 span[header_value]="www.example.com"
+off=40 header_value complete
+off=40 len=12 span[header_field]="Content-Type"
+off=53 header_field complete
+off=54 len=33 span[header_value]="application/x-www-form-urlencoded"
+off=89 header_value complete
+off=89 len=14 span[header_field]="Content-Length"
+off=104 header_field complete
+off=105 len=1 span[header_value]="4"
+off=108 header_value complete
+off=110 headers complete method=3 v=1/1 flags=20 content_length=4
+off=110 len=4 span[body]="q=42"
+off=114 message complete
+off=118 reset
+off=118 message begin
+off=118 len=3 span[method]="GET"
+off=121 method complete
+off=122 len=1 span[url]="/"
+off=124 url complete
+off=129 len=3 span[version]="1.1"
+off=132 version complete
+```
+
+### Not treating `\r` as `-`
+
+<!-- meta={"type": "request"} -->
+```http
+PUT /url HTTP/1.1
+Connection: keep\ralive
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="PUT"
+off=3 method complete
+off=4 len=4 span[url]="/url"
+off=9 url complete
+off=14 len=3 span[version]="1.1"
+off=17 version complete
+off=19 len=10 span[header_field]="Connection"
+off=30 header_field complete
+off=31 len=4 span[header_value]="keep"
+off=36 error code=3 reason="Missing expected LF after header value"
+```
+
+## `close`
+
+### Setting flag on `close`
+
+<!-- meta={"type": "request"} -->
+```http
+PUT /url HTTP/1.1
+Connection: close
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="PUT"
+off=3 method complete
+off=4 len=4 span[url]="/url"
+off=9 url complete
+off=14 len=3 span[version]="1.1"
+off=17 version complete
+off=19 len=10 span[header_field]="Connection"
+off=30 header_field complete
+off=31 len=5 span[header_value]="close"
+off=38 header_value complete
+off=40 headers complete method=4 v=1/1 flags=2 content_length=0
+off=40 message complete
+```
+
+### CRLF between requests, explicit `close`
+
+`close` means closed connection
+
+<!-- meta={"type": "request" } -->
+```http
+POST / HTTP/1.1
+Host: www.example.com
+Content-Type: application/x-www-form-urlencoded
+Content-Length: 4
+Connection: close
+
+q=42
+
+GET / HTTP/1.1
+```
+_Note the trailing CRLF above_
+
+```log
+off=0 message begin
+off=0 len=4 span[method]="POST"
+off=4 method complete
+off=5 len=1 span[url]="/"
+off=7 url complete
+off=12 len=3 span[version]="1.1"
+off=15 version complete
+off=17 len=4 span[header_field]="Host"
+off=22 header_field complete
+off=23 len=15 span[header_value]="www.example.com"
+off=40 header_value complete
+off=40 len=12 span[header_field]="Content-Type"
+off=53 header_field complete
+off=54 len=33 span[header_value]="application/x-www-form-urlencoded"
+off=89 header_value complete
+off=89 len=14 span[header_field]="Content-Length"
+off=104 header_field complete
+off=105 len=1 span[header_value]="4"
+off=108 header_value complete
+off=108 len=10 span[header_field]="Connection"
+off=119 header_field complete
+off=120 len=5 span[header_value]="close"
+off=127 header_value complete
+off=129 headers complete method=3 v=1/1 flags=22 content_length=4
+off=129 len=4 span[body]="q=42"
+off=133 message complete
+off=138 error code=5 reason="Data after `Connection: close`"
+```
+
+### CRLF between requests, explicit `close` (lenient)
+
+Loose mode is more lenient, and allows further requests.
+
+<!-- meta={"type": "request-lenient-keep-alive"} -->
+```http
+POST / HTTP/1.1
+Host: www.example.com
+Content-Type: application/x-www-form-urlencoded
+Content-Length: 4
+Connection: close
+
+q=42
+
+GET / HTTP/1.1
+```
+_Note the trailing CRLF above_
+
+```log
+off=0 message begin
+off=0 len=4 span[method]="POST"
+off=4 method complete
+off=5 len=1 span[url]="/"
+off=7 url complete
+off=12 len=3 span[version]="1.1"
+off=15 version complete
+off=17 len=4 span[header_field]="Host"
+off=22 header_field complete
+off=23 len=15 span[header_value]="www.example.com"
+off=40 header_value complete
+off=40 len=12 span[header_field]="Content-Type"
+off=53 header_field complete
+off=54 len=33 span[header_value]="application/x-www-form-urlencoded"
+off=89 header_value complete
+off=89 len=14 span[header_field]="Content-Length"
+off=104 header_field complete
+off=105 len=1 span[header_value]="4"
+off=108 header_value complete
+off=108 len=10 span[header_field]="Connection"
+off=119 header_field complete
+off=120 len=5 span[header_value]="close"
+off=127 header_value complete
+off=129 headers complete method=3 v=1/1 flags=22 content_length=4
+off=129 len=4 span[body]="q=42"
+off=133 message complete
+off=137 reset
+off=137 message begin
+off=137 len=3 span[method]="GET"
+off=140 method complete
+off=141 len=1 span[url]="/"
+off=143 url complete
+off=148 len=3 span[version]="1.1"
+off=151 version complete
+```
+
+## Parsing multiple tokens
+
+### Sample
+
+<!-- meta={"type": "request"} -->
+```http
+PUT /url HTTP/1.1
+Connection: close, token, upgrade, token, keep-alive
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="PUT"
+off=3 method complete
+off=4 len=4 span[url]="/url"
+off=9 url complete
+off=14 len=3 span[version]="1.1"
+off=17 version complete
+off=19 len=10 span[header_field]="Connection"
+off=30 header_field complete
+off=31 len=40 span[header_value]="close, token, upgrade, token, keep-alive"
+off=73 header_value complete
+off=75 headers complete method=4 v=1/1 flags=7 content_length=0
+off=75 message complete
+```
+
+### Multiple tokens with folding
+
+<!-- meta={"type": "request"} -->
+```http
+GET /demo HTTP/1.1
+Host: example.com
+Connection: Something,
+ Upgrade, ,Keep-Alive
+Sec-WebSocket-Key2: 12998 5 Y3 1 .P00
+Sec-WebSocket-Protocol: sample
+Upgrade: WebSocket
+Sec-WebSocket-Key1: 4 @1 46546xW%0l 1 5
+Origin: http://example.com
+
+Hot diggity dogg
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="GET"
+off=3 method complete
+off=4 len=5 span[url]="/demo"
+off=10 url complete
+off=15 len=3 span[version]="1.1"
+off=18 version complete
+off=20 len=4 span[header_field]="Host"
+off=25 header_field complete
+off=26 len=11 span[header_value]="example.com"
+off=39 header_value complete
+off=39 len=10 span[header_field]="Connection"
+off=50 header_field complete
+off=51 len=10 span[header_value]="Something,"
+off=63 len=21 span[header_value]=" Upgrade, ,Keep-Alive"
+off=86 header_value complete
+off=86 len=18 span[header_field]="Sec-WebSocket-Key2"
+off=105 header_field complete
+off=106 len=18 span[header_value]="12998 5 Y3 1 .P00"
+off=126 header_value complete
+off=126 len=22 span[header_field]="Sec-WebSocket-Protocol"
+off=149 header_field complete
+off=150 len=6 span[header_value]="sample"
+off=158 header_value complete
+off=158 len=7 span[header_field]="Upgrade"
+off=166 header_field complete
+off=167 len=9 span[header_value]="WebSocket"
+off=178 header_value complete
+off=178 len=18 span[header_field]="Sec-WebSocket-Key1"
+off=197 header_field complete
+off=198 len=20 span[header_value]="4 @1 46546xW%0l 1 5"
+off=220 header_value complete
+off=220 len=6 span[header_field]="Origin"
+off=227 header_field complete
+off=228 len=18 span[header_value]="http://example.com"
+off=248 header_value complete
+off=250 headers complete method=1 v=1/1 flags=15 content_length=0
+off=250 message complete
+off=250 error code=22 reason="Pause on CONNECT/Upgrade"
+```
+
+### Multiple tokens with folding and LWS
+
+<!-- meta={"type": "request"} -->
+```http
+GET /demo HTTP/1.1
+Connection: keep-alive, upgrade
+Upgrade: WebSocket
+
+Hot diggity dogg
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="GET"
+off=3 method complete
+off=4 len=5 span[url]="/demo"
+off=10 url complete
+off=15 len=3 span[version]="1.1"
+off=18 version complete
+off=20 len=10 span[header_field]="Connection"
+off=31 header_field complete
+off=32 len=19 span[header_value]="keep-alive, upgrade"
+off=53 header_value complete
+off=53 len=7 span[header_field]="Upgrade"
+off=61 header_field complete
+off=62 len=9 span[header_value]="WebSocket"
+off=73 header_value complete
+off=75 headers complete method=1 v=1/1 flags=15 content_length=0
+off=75 message complete
+off=75 error code=22 reason="Pause on CONNECT/Upgrade"
+```
+
+### Multiple tokens with folding, LWS, and CRLF
+
+<!-- meta={"type": "request"} -->
+```http
+GET /demo HTTP/1.1
+Connection: keep-alive, \r\n upgrade
+Upgrade: WebSocket
+
+Hot diggity dogg
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="GET"
+off=3 method complete
+off=4 len=5 span[url]="/demo"
+off=10 url complete
+off=15 len=3 span[version]="1.1"
+off=18 version complete
+off=20 len=10 span[header_field]="Connection"
+off=31 header_field complete
+off=32 len=12 span[header_value]="keep-alive, "
+off=46 len=8 span[header_value]=" upgrade"
+off=56 header_value complete
+off=56 len=7 span[header_field]="Upgrade"
+off=64 header_field complete
+off=65 len=9 span[header_value]="WebSocket"
+off=76 header_value complete
+off=78 headers complete method=1 v=1/1 flags=15 content_length=0
+off=78 message complete
+off=78 error code=22 reason="Pause on CONNECT/Upgrade"
+```
+
+### Invalid whitespace token with `Connection` header field
+
+<!-- meta={"type": "request"} -->
+```http
+PUT /url HTTP/1.1
+Connection : upgrade
+Content-Length: 4
+Upgrade: ws
+
+abcdefgh
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="PUT"
+off=3 method complete
+off=4 len=4 span[url]="/url"
+off=9 url complete
+off=14 len=3 span[version]="1.1"
+off=17 version complete
+off=19 len=10 span[header_field]="Connection"
+off=30 error code=10 reason="Invalid header field char"
+```
+
+### Invalid whitespace token with `Connection` header field (lenient)
+
+<!-- meta={"type": "request-lenient-headers"} -->
+```http
+PUT /url HTTP/1.1
+Connection : upgrade
+Content-Length: 4
+Upgrade: ws
+
+abcdefgh
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="PUT"
+off=3 method complete
+off=4 len=4 span[url]="/url"
+off=9 url complete
+off=14 len=3 span[version]="1.1"
+off=17 version complete
+off=19 len=11 span[header_field]="Connection "
+off=31 header_field complete
+off=32 len=7 span[header_value]="upgrade"
+off=41 header_value complete
+off=41 len=14 span[header_field]="Content-Length"
+off=56 header_field complete
+off=57 len=1 span[header_value]="4"
+off=60 header_value complete
+off=60 len=7 span[header_field]="Upgrade"
+off=68 header_field complete
+off=69 len=2 span[header_value]="ws"
+off=73 header_value complete
+off=75 headers complete method=4 v=1/1 flags=34 content_length=4
+off=75 len=4 span[body]="abcd"
+off=79 message complete
+off=79 error code=22 reason="Pause on CONNECT/Upgrade"
+```
+
+## `upgrade`
+
+### Setting a flag and pausing
+
+<!-- meta={"type": "request"} -->
+```http
+PUT /url HTTP/1.1
+Connection: upgrade
+Upgrade: ws
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="PUT"
+off=3 method complete
+off=4 len=4 span[url]="/url"
+off=9 url complete
+off=14 len=3 span[version]="1.1"
+off=17 version complete
+off=19 len=10 span[header_field]="Connection"
+off=30 header_field complete
+off=31 len=7 span[header_value]="upgrade"
+off=40 header_value complete
+off=40 len=7 span[header_field]="Upgrade"
+off=48 header_field complete
+off=49 len=2 span[header_value]="ws"
+off=53 header_value complete
+off=55 headers complete method=4 v=1/1 flags=14 content_length=0
+off=55 message complete
+off=55 error code=22 reason="Pause on CONNECT/Upgrade"
+```
+
+### Emitting part of body and pausing
+
+<!-- meta={"type": "request"} -->
+```http
+PUT /url HTTP/1.1
+Connection: upgrade
+Content-Length: 4
+Upgrade: ws
+
+abcdefgh
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="PUT"
+off=3 method complete
+off=4 len=4 span[url]="/url"
+off=9 url complete
+off=14 len=3 span[version]="1.1"
+off=17 version complete
+off=19 len=10 span[header_field]="Connection"
+off=30 header_field complete
+off=31 len=7 span[header_value]="upgrade"
+off=40 header_value complete
+off=40 len=14 span[header_field]="Content-Length"
+off=55 header_field complete
+off=56 len=1 span[header_value]="4"
+off=59 header_value complete
+off=59 len=7 span[header_field]="Upgrade"
+off=67 header_field complete
+off=68 len=2 span[header_value]="ws"
+off=72 header_value complete
+off=74 headers complete method=4 v=1/1 flags=34 content_length=4
+off=74 len=4 span[body]="abcd"
+off=78 message complete
+off=78 error code=22 reason="Pause on CONNECT/Upgrade"
+```
+
+### Upgrade GET request
+
+<!-- meta={"type": "request"} -->
+```http
+GET /demo HTTP/1.1
+Host: example.com
+Connection: Upgrade
+Sec-WebSocket-Key2: 12998 5 Y3 1 .P00
+Sec-WebSocket-Protocol: sample
+Upgrade: WebSocket
+Sec-WebSocket-Key1: 4 @1 46546xW%0l 1 5
+Origin: http://example.com
+
+Hot diggity dogg
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="GET"
+off=3 method complete
+off=4 len=5 span[url]="/demo"
+off=10 url complete
+off=15 len=3 span[version]="1.1"
+off=18 version complete
+off=20 len=4 span[header_field]="Host"
+off=25 header_field complete
+off=26 len=11 span[header_value]="example.com"
+off=39 header_value complete
+off=39 len=10 span[header_field]="Connection"
+off=50 header_field complete
+off=51 len=7 span[header_value]="Upgrade"
+off=60 header_value complete
+off=60 len=18 span[header_field]="Sec-WebSocket-Key2"
+off=79 header_field complete
+off=80 len=18 span[header_value]="12998 5 Y3 1 .P00"
+off=100 header_value complete
+off=100 len=22 span[header_field]="Sec-WebSocket-Protocol"
+off=123 header_field complete
+off=124 len=6 span[header_value]="sample"
+off=132 header_value complete
+off=132 len=7 span[header_field]="Upgrade"
+off=140 header_field complete
+off=141 len=9 span[header_value]="WebSocket"
+off=152 header_value complete
+off=152 len=18 span[header_field]="Sec-WebSocket-Key1"
+off=171 header_field complete
+off=172 len=20 span[header_value]="4 @1 46546xW%0l 1 5"
+off=194 header_value complete
+off=194 len=6 span[header_field]="Origin"
+off=201 header_field complete
+off=202 len=18 span[header_value]="http://example.com"
+off=222 header_value complete
+off=224 headers complete method=1 v=1/1 flags=14 content_length=0
+off=224 message complete
+off=224 error code=22 reason="Pause on CONNECT/Upgrade"
+```
+
+### Upgrade POST request
+
+<!-- meta={"type": "request"} -->
+```http
+POST /demo HTTP/1.1
+Host: example.com
+Connection: Upgrade
+Upgrade: HTTP/2.0
+Content-Length: 15
+
+sweet post body\
+Hot diggity dogg
+```
+
+```log
+off=0 message begin
+off=0 len=4 span[method]="POST"
+off=4 method complete
+off=5 len=5 span[url]="/demo"
+off=11 url complete
+off=16 len=3 span[version]="1.1"
+off=19 version complete
+off=21 len=4 span[header_field]="Host"
+off=26 header_field complete
+off=27 len=11 span[header_value]="example.com"
+off=40 header_value complete
+off=40 len=10 span[header_field]="Connection"
+off=51 header_field complete
+off=52 len=7 span[header_value]="Upgrade"
+off=61 header_value complete
+off=61 len=7 span[header_field]="Upgrade"
+off=69 header_field complete
+off=70 len=8 span[header_value]="HTTP/2.0"
+off=80 header_value complete
+off=80 len=14 span[header_field]="Content-Length"
+off=95 header_field complete
+off=96 len=2 span[header_value]="15"
+off=100 header_value complete
+off=102 headers complete method=3 v=1/1 flags=34 content_length=15
+off=102 len=15 span[body]="sweet post body"
+off=117 message complete
+off=117 error code=22 reason="Pause on CONNECT/Upgrade"
+```
diff --git a/llhttp/test/request/content-length.md b/llhttp/test/request/content-length.md
new file mode 100644
index 0000000..524d183
--- /dev/null
+++ b/llhttp/test/request/content-length.md
@@ -0,0 +1,482 @@
+Content-Length header
+=====================
+
+## `Content-Length` with zeroes
+
+<!-- meta={"type": "request"} -->
+```http
+PUT /url HTTP/1.1
+Content-Length: 003
+
+abc
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="PUT"
+off=3 method complete
+off=4 len=4 span[url]="/url"
+off=9 url complete
+off=14 len=3 span[version]="1.1"
+off=17 version complete
+off=19 len=14 span[header_field]="Content-Length"
+off=34 header_field complete
+off=35 len=3 span[header_value]="003"
+off=40 header_value complete
+off=42 headers complete method=4 v=1/1 flags=20 content_length=3
+off=42 len=3 span[body]="abc"
+off=45 message complete
+```
+
+## `Content-Length` with follow-up headers
+
+The way the parser works is that special headers (like `Content-Length`) first
+set `header_state` to appropriate value, and then apply custom parsing using
+that value. For `Content-Length`, in particular, the `header_state` is used for
+setting the flag too.
+
+Make sure that `header_state` is reset to `0`, so that the flag won't be
+attempted to set twice (and error).
+
+<!-- meta={"type": "request"} -->
+```http
+PUT /url HTTP/1.1
+Content-Length: 003
+Ohai: world
+
+abc
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="PUT"
+off=3 method complete
+off=4 len=4 span[url]="/url"
+off=9 url complete
+off=14 len=3 span[version]="1.1"
+off=17 version complete
+off=19 len=14 span[header_field]="Content-Length"
+off=34 header_field complete
+off=35 len=3 span[header_value]="003"
+off=40 header_value complete
+off=40 len=4 span[header_field]="Ohai"
+off=45 header_field complete
+off=46 len=5 span[header_value]="world"
+off=53 header_value complete
+off=55 headers complete method=4 v=1/1 flags=20 content_length=3
+off=55 len=3 span[body]="abc"
+off=58 message complete
+```
+
+## Error on `Content-Length` overflow
+
+<!-- meta={"type": "request"} -->
+```http
+PUT /url HTTP/1.1
+Content-Length: 1000000000000000000000
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="PUT"
+off=3 method complete
+off=4 len=4 span[url]="/url"
+off=9 url complete
+off=14 len=3 span[version]="1.1"
+off=17 version complete
+off=19 len=14 span[header_field]="Content-Length"
+off=34 header_field complete
+off=35 len=21 span[header_value]="100000000000000000000"
+off=56 error code=11 reason="Content-Length overflow"
+```
+
+## Error on duplicate `Content-Length`
+
+<!-- meta={"type": "request"} -->
+```http
+PUT /url HTTP/1.1
+Content-Length: 1
+Content-Length: 2
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="PUT"
+off=3 method complete
+off=4 len=4 span[url]="/url"
+off=9 url complete
+off=14 len=3 span[version]="1.1"
+off=17 version complete
+off=19 len=14 span[header_field]="Content-Length"
+off=34 header_field complete
+off=35 len=1 span[header_value]="1"
+off=38 header_value complete
+off=38 len=14 span[header_field]="Content-Length"
+off=53 header_field complete
+off=54 error code=4 reason="Duplicate Content-Length"
+```
+
+## Error on simultaneous `Content-Length` and `Transfer-Encoding: identity`
+
+<!-- meta={"type": "request"} -->
+```http
+PUT /url HTTP/1.1
+Content-Length: 1
+Transfer-Encoding: identity
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="PUT"
+off=3 method complete
+off=4 len=4 span[url]="/url"
+off=9 url complete
+off=14 len=3 span[version]="1.1"
+off=17 version complete
+off=19 len=14 span[header_field]="Content-Length"
+off=34 header_field complete
+off=35 len=1 span[header_value]="1"
+off=38 header_value complete
+off=38 len=17 span[header_field]="Transfer-Encoding"
+off=56 header_field complete
+off=56 error code=15 reason="Transfer-Encoding can't be present with Content-Length"
+```
+
+## Invalid whitespace token with `Content-Length` header field
+
+<!-- meta={"type": "request"} -->
+```http
+PUT /url HTTP/1.1
+Connection: upgrade
+Content-Length : 4
+Upgrade: ws
+
+abcdefgh
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="PUT"
+off=3 method complete
+off=4 len=4 span[url]="/url"
+off=9 url complete
+off=14 len=3 span[version]="1.1"
+off=17 version complete
+off=19 len=10 span[header_field]="Connection"
+off=30 header_field complete
+off=31 len=7 span[header_value]="upgrade"
+off=40 header_value complete
+off=40 len=14 span[header_field]="Content-Length"
+off=55 error code=10 reason="Invalid header field char"
+```
+
+## Invalid whitespace token with `Content-Length` header field (lenient)
+
+<!-- meta={"type": "request-lenient-headers"} -->
+```http
+PUT /url HTTP/1.1
+Connection: upgrade
+Content-Length : 4
+Upgrade: ws
+
+abcdefgh
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="PUT"
+off=3 method complete
+off=4 len=4 span[url]="/url"
+off=9 url complete
+off=14 len=3 span[version]="1.1"
+off=17 version complete
+off=19 len=10 span[header_field]="Connection"
+off=30 header_field complete
+off=31 len=7 span[header_value]="upgrade"
+off=40 header_value complete
+off=40 len=15 span[header_field]="Content-Length "
+off=56 header_field complete
+off=57 len=1 span[header_value]="4"
+off=60 header_value complete
+off=60 len=7 span[header_field]="Upgrade"
+off=68 header_field complete
+off=69 len=2 span[header_value]="ws"
+off=73 header_value complete
+off=75 headers complete method=4 v=1/1 flags=34 content_length=4
+off=75 len=4 span[body]="abcd"
+off=79 message complete
+off=79 error code=22 reason="Pause on CONNECT/Upgrade"
+```
+
+## No error on simultaneous `Content-Length` and `Transfer-Encoding: identity` (lenient)
+
+<!-- meta={"type": "request-lenient-chunked-length"} -->
+```http
+PUT /url HTTP/1.1
+Content-Length: 1
+Transfer-Encoding: identity
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="PUT"
+off=3 method complete
+off=4 len=4 span[url]="/url"
+off=9 url complete
+off=14 len=3 span[version]="1.1"
+off=17 version complete
+off=19 len=14 span[header_field]="Content-Length"
+off=34 header_field complete
+off=35 len=1 span[header_value]="1"
+off=38 header_value complete
+off=38 len=17 span[header_field]="Transfer-Encoding"
+off=56 header_field complete
+off=57 len=8 span[header_value]="identity"
+off=67 header_value complete
+off=69 headers complete method=4 v=1/1 flags=220 content_length=1
+```
+
+## Funky `Content-Length` with body
+
+<!-- meta={"type": "request"} -->
+```http
+GET /get_funky_content_length_body_hello HTTP/1.0
+conTENT-Length: 5
+
+HELLO
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="GET"
+off=3 method complete
+off=4 len=36 span[url]="/get_funky_content_length_body_hello"
+off=41 url complete
+off=46 len=3 span[version]="1.0"
+off=49 version complete
+off=51 len=14 span[header_field]="conTENT-Length"
+off=66 header_field complete
+off=67 len=1 span[header_value]="5"
+off=70 header_value complete
+off=72 headers complete method=1 v=1/0 flags=20 content_length=5
+off=72 len=5 span[body]="HELLO"
+off=77 message complete
+```
+
+## Spaces in `Content-Length` (surrounding)
+
+<!-- meta={"type": "request"} -->
+```http
+POST / HTTP/1.1
+Content-Length: 42
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=4 span[method]="POST"
+off=4 method complete
+off=5 len=1 span[url]="/"
+off=7 url complete
+off=12 len=3 span[version]="1.1"
+off=15 version complete
+off=17 len=14 span[header_field]="Content-Length"
+off=32 header_field complete
+off=34 len=3 span[header_value]="42 "
+off=39 header_value complete
+off=41 headers complete method=3 v=1/1 flags=20 content_length=42
+```
+
+### Spaces in `Content-Length` #2
+
+<!-- meta={"type": "request"} -->
+```http
+POST / HTTP/1.1
+Content-Length: 4 2
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=4 span[method]="POST"
+off=4 method complete
+off=5 len=1 span[url]="/"
+off=7 url complete
+off=12 len=3 span[version]="1.1"
+off=15 version complete
+off=17 len=14 span[header_field]="Content-Length"
+off=32 header_field complete
+off=33 len=2 span[header_value]="4 "
+off=35 error code=11 reason="Invalid character in Content-Length"
+```
+
+### Spaces in `Content-Length` #3
+
+<!-- meta={"type": "request"} -->
+```http
+POST / HTTP/1.1
+Content-Length: 13 37
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=4 span[method]="POST"
+off=4 method complete
+off=5 len=1 span[url]="/"
+off=7 url complete
+off=12 len=3 span[version]="1.1"
+off=15 version complete
+off=17 len=14 span[header_field]="Content-Length"
+off=32 header_field complete
+off=33 len=3 span[header_value]="13 "
+off=36 error code=11 reason="Invalid character in Content-Length"
+```
+
+### Empty `Content-Length`
+
+<!-- meta={"type": "request"} -->
+```http
+POST / HTTP/1.1
+Content-Length:
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=4 span[method]="POST"
+off=4 method complete
+off=5 len=1 span[url]="/"
+off=7 url complete
+off=12 len=3 span[version]="1.1"
+off=15 version complete
+off=17 len=14 span[header_field]="Content-Length"
+off=32 header_field complete
+off=34 error code=11 reason="Empty Content-Length"
+```
+
+## `Content-Length` with CR instead of dash
+
+<!-- meta={"type": "request", "noScan": true} -->
+```http
+PUT /url HTTP/1.1
+Content\rLength: 003
+
+abc
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="PUT"
+off=3 method complete
+off=4 len=4 span[url]="/url"
+off=9 url complete
+off=14 len=3 span[version]="1.1"
+off=17 version complete
+off=26 error code=10 reason="Invalid header token"
+```
+
+## Content-Length reset when no body is received
+
+<!-- meta={"type": "request", "skipBody": true} -->
+```http
+PUT /url HTTP/1.1
+Content-Length: 123
+
+POST /url HTTP/1.1
+Content-Length: 456
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="PUT"
+off=3 method complete
+off=4 len=4 span[url]="/url"
+off=9 url complete
+off=14 len=3 span[version]="1.1"
+off=17 version complete
+off=19 len=14 span[header_field]="Content-Length"
+off=34 header_field complete
+off=35 len=3 span[header_value]="123"
+off=40 header_value complete
+off=42 headers complete method=4 v=1/1 flags=20 content_length=123
+off=42 skip body
+off=42 message complete
+off=42 reset
+off=42 message begin
+off=42 len=4 span[method]="POST"
+off=46 method complete
+off=47 len=4 span[url]="/url"
+off=52 url complete
+off=57 len=3 span[version]="1.1"
+off=60 version complete
+off=62 len=14 span[header_field]="Content-Length"
+off=77 header_field complete
+off=78 len=3 span[header_value]="456"
+off=83 header_value complete
+off=85 headers complete method=3 v=1/1 flags=20 content_length=456
+off=85 skip body
+off=85 message complete
+```
+
+## Missing CRLF-CRLF before body
+
+<!-- meta={"type": "request" } -->
+```http
+PUT /url HTTP/1.1
+Content-Length: 3
+\rabc
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="PUT"
+off=3 method complete
+off=4 len=4 span[url]="/url"
+off=9 url complete
+off=14 len=3 span[version]="1.1"
+off=17 version complete
+off=19 len=14 span[header_field]="Content-Length"
+off=34 header_field complete
+off=35 len=1 span[header_value]="3"
+off=38 header_value complete
+off=39 error code=2 reason="Expected LF after headers"
+```
+
+## Missing CRLF-CRLF before body (lenient)
+
+<!-- meta={"type": "request-lenient-optional-lf-after-cr" } -->
+```http
+PUT /url HTTP/1.1
+Content-Length: 3
+\rabc
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="PUT"
+off=3 method complete
+off=4 len=4 span[url]="/url"
+off=9 url complete
+off=14 len=3 span[version]="1.1"
+off=17 version complete
+off=19 len=14 span[header_field]="Content-Length"
+off=34 header_field complete
+off=35 len=1 span[header_value]="3"
+off=38 header_value complete
+off=39 headers complete method=4 v=1/1 flags=20 content_length=3
+off=39 len=3 span[body]="abc"
+off=42 message complete
+``` \ No newline at end of file
diff --git a/llhttp/test/request/finish.md b/llhttp/test/request/finish.md
new file mode 100644
index 0000000..710daa5
--- /dev/null
+++ b/llhttp/test/request/finish.md
@@ -0,0 +1,69 @@
+Finish
+======
+
+Those tests check the return codes and the behavior of `llhttp_finish()` C API.
+
+## It should be safe to finish after GET request
+
+<!-- meta={"type": "request-finish"} -->
+```http
+GET / HTTP/1.1
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="GET"
+off=3 method complete
+off=4 len=1 span[url]="/"
+off=6 url complete
+off=11 len=3 span[version]="1.1"
+off=14 version complete
+off=18 headers complete method=1 v=1/1 flags=0 content_length=0
+off=18 message complete
+off=NULL finish=0
+```
+
+## It should be unsafe to finish after incomplete PUT request
+
+<!-- meta={"type": "request-finish"} -->
+```http
+PUT / HTTP/1.1
+Content-Length: 100
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="PUT"
+off=3 method complete
+off=4 len=1 span[url]="/"
+off=6 url complete
+off=11 len=3 span[version]="1.1"
+off=14 version complete
+off=16 len=14 span[header_field]="Content-Length"
+off=31 header_field complete
+off=32 len=3 span[header_value]="100"
+off=NULL finish=2
+```
+
+## It should be unsafe to finish inside of the header
+
+<!-- meta={"type": "request-finish"} -->
+```http
+PUT / HTTP/1.1
+Content-Leng
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="PUT"
+off=3 method complete
+off=4 len=1 span[url]="/"
+off=6 url complete
+off=11 len=3 span[version]="1.1"
+off=14 version complete
+off=16 len=12 span[header_field]="Content-Leng"
+off=NULL finish=2
+```
diff --git a/llhttp/test/request/invalid.md b/llhttp/test/request/invalid.md
new file mode 100644
index 0000000..9fb8383
--- /dev/null
+++ b/llhttp/test/request/invalid.md
@@ -0,0 +1,607 @@
+Invalid requests
+================
+
+### ICE protocol and GET method
+
+<!-- meta={"type": "request"} -->
+```http
+GET /music/sweet/music ICE/1.0
+Host: example.com
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="GET"
+off=3 method complete
+off=4 len=18 span[url]="/music/sweet/music"
+off=23 url complete
+off=27 error code=8 reason="Expected SOURCE method for ICE/x.x request"
+```
+
+### ICE protocol, but not really
+
+<!-- meta={"type": "request"} -->
+```http
+GET /music/sweet/music IHTTP/1.0
+Host: example.com
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="GET"
+off=3 method complete
+off=4 len=18 span[url]="/music/sweet/music"
+off=23 url complete
+off=24 error code=8 reason="Expected HTTP/"
+```
+
+### RTSP protocol and PUT method
+
+<!-- meta={"type": "request"} -->
+```http
+PUT /music/sweet/music RTSP/1.0
+Host: example.com
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="PUT"
+off=3 method complete
+off=4 len=18 span[url]="/music/sweet/music"
+off=23 url complete
+off=28 error code=8 reason="Invalid method for RTSP/x.x request"
+```
+
+### HTTP protocol and ANNOUNCE method
+
+<!-- meta={"type": "request"} -->
+```http
+ANNOUNCE /music/sweet/music HTTP/1.0
+Host: example.com
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=8 span[method]="ANNOUNCE"
+off=8 method complete
+off=9 len=18 span[url]="/music/sweet/music"
+off=28 url complete
+off=33 error code=8 reason="Invalid method for HTTP/x.x request"
+```
+
+### Headers separated by CR
+
+<!-- meta={"type": "request"} -->
+```http
+GET / HTTP/1.1
+Foo: 1\rBar: 2
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="GET"
+off=3 method complete
+off=4 len=1 span[url]="/"
+off=6 url complete
+off=11 len=3 span[version]="1.1"
+off=14 version complete
+off=16 len=3 span[header_field]="Foo"
+off=20 header_field complete
+off=21 len=1 span[header_value]="1"
+off=23 error code=3 reason="Missing expected LF after header value"
+```
+
+### Headers separated by LF
+
+<!-- meta={"type": "request"} -->
+```http
+POST / HTTP/1.1
+Host: localhost:5000
+x:x\nTransfer-Encoding: chunked
+
+1
+A
+0
+
+```
+
+```log
+off=0 message begin
+off=0 len=4 span[method]="POST"
+off=4 method complete
+off=5 len=1 span[url]="/"
+off=7 url complete
+off=12 len=3 span[version]="1.1"
+off=15 version complete
+off=17 len=4 span[header_field]="Host"
+off=22 header_field complete
+off=23 len=14 span[header_value]="localhost:5000"
+off=39 header_value complete
+off=39 len=1 span[header_field]="x"
+off=41 header_field complete
+off=41 len=1 span[header_value]="x"
+off=42 error code=25 reason="Missing expected CR after header value"
+```
+
+### Headers separated by dummy characters
+
+<!-- meta={"type": "request"} -->
+```http
+GET / HTTP/1.1
+Connection: close
+Host: a
+\rZGET /evil: HTTP/1.1
+Host: a
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="GET"
+off=3 method complete
+off=4 len=1 span[url]="/"
+off=6 url complete
+off=11 len=3 span[version]="1.1"
+off=14 version complete
+off=16 len=10 span[header_field]="Connection"
+off=27 header_field complete
+off=28 len=5 span[header_value]="close"
+off=35 header_value complete
+off=35 len=4 span[header_field]="Host"
+off=40 header_field complete
+off=41 len=1 span[header_value]="a"
+off=44 header_value complete
+off=45 error code=2 reason="Expected LF after headers"
+```
+
+
+### Headers separated by dummy characters (lenient)
+
+<!-- meta={"type": "request-lenient-optional-lf-after-cr"} -->
+```http
+GET / HTTP/1.1
+Connection: close
+Host: a
+\rZGET /evil: HTTP/1.1
+Host: a
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="GET"
+off=3 method complete
+off=4 len=1 span[url]="/"
+off=6 url complete
+off=11 len=3 span[version]="1.1"
+off=14 version complete
+off=16 len=10 span[header_field]="Connection"
+off=27 header_field complete
+off=28 len=5 span[header_value]="close"
+off=35 header_value complete
+off=35 len=4 span[header_field]="Host"
+off=40 header_field complete
+off=41 len=1 span[header_value]="a"
+off=44 header_value complete
+off=45 headers complete method=1 v=1/1 flags=2 content_length=0
+off=45 message complete
+off=46 error code=5 reason="Data after `Connection: close`"
+```
+
+### Empty headers separated by CR
+
+<!-- meta={"type": "request" } -->
+```http
+POST / HTTP/1.1
+Connection: Close
+Host: localhost:5000
+x:\rTransfer-Encoding: chunked
+
+1
+A
+0
+
+```
+
+```log
+off=0 message begin
+off=0 len=4 span[method]="POST"
+off=4 method complete
+off=5 len=1 span[url]="/"
+off=7 url complete
+off=12 len=3 span[version]="1.1"
+off=15 version complete
+off=17 len=10 span[header_field]="Connection"
+off=28 header_field complete
+off=29 len=5 span[header_value]="Close"
+off=36 header_value complete
+off=36 len=4 span[header_field]="Host"
+off=41 header_field complete
+off=42 len=14 span[header_value]="localhost:5000"
+off=58 header_value complete
+off=58 len=1 span[header_field]="x"
+off=60 header_field complete
+off=61 error code=2 reason="Expected LF after CR"
+```
+
+### Empty headers separated by LF
+
+<!-- meta={"type": "request"} -->
+```http
+POST / HTTP/1.1
+Host: localhost:5000
+x:\nTransfer-Encoding: chunked
+
+1
+A
+0
+
+```
+
+```log
+off=0 message begin
+off=0 len=4 span[method]="POST"
+off=4 method complete
+off=5 len=1 span[url]="/"
+off=7 url complete
+off=12 len=3 span[version]="1.1"
+off=15 version complete
+off=17 len=4 span[header_field]="Host"
+off=22 header_field complete
+off=23 len=14 span[header_value]="localhost:5000"
+off=39 header_value complete
+off=39 len=1 span[header_field]="x"
+off=41 header_field complete
+off=42 error code=10 reason="Invalid header value char"
+```
+
+### Invalid header token #1
+
+<!-- meta={"type": "request", "noScan": true} -->
+```http
+GET / HTTP/1.1
+Fo@: Failure
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="GET"
+off=3 method complete
+off=4 len=1 span[url]="/"
+off=6 url complete
+off=11 len=3 span[version]="1.1"
+off=14 version complete
+off=18 error code=10 reason="Invalid header token"
+```
+
+### Invalid header token #2
+
+<!-- meta={"type": "request", "noScan": true} -->
+```http
+GET / HTTP/1.1
+Foo\01\test: Bar
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="GET"
+off=3 method complete
+off=4 len=1 span[url]="/"
+off=6 url complete
+off=11 len=3 span[version]="1.1"
+off=14 version complete
+off=19 error code=10 reason="Invalid header token"
+```
+
+### Invalid header token #3
+
+<!-- meta={"type": "request", "noScan": true} -->
+```http
+GET / HTTP/1.1
+: Bar
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="GET"
+off=3 method complete
+off=4 len=1 span[url]="/"
+off=6 url complete
+off=11 len=3 span[version]="1.1"
+off=14 version complete
+off=16 error code=10 reason="Invalid header token"
+```
+
+### Invalid method
+
+<!-- meta={"type": "request"} -->
+```http
+MKCOLA / HTTP/1.1
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=5 span[method]="MKCOL"
+off=5 method complete
+off=5 error code=6 reason="Expected space after method"
+```
+
+### Illegal header field name line folding
+
+<!-- meta={"type": "request", "noScan": true} -->
+```http
+GET / HTTP/1.1
+name
+ : value
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="GET"
+off=3 method complete
+off=4 len=1 span[url]="/"
+off=6 url complete
+off=11 len=3 span[version]="1.1"
+off=14 version complete
+off=20 error code=10 reason="Invalid header token"
+```
+
+### Corrupted Connection header
+
+<!-- meta={"type": "request", "noScan": true} -->
+```http
+GET / HTTP/1.1
+Host: www.example.com
+Connection\r\033\065\325eep-Alive
+Accept-Encoding: gzip
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="GET"
+off=3 method complete
+off=4 len=1 span[url]="/"
+off=6 url complete
+off=11 len=3 span[version]="1.1"
+off=14 version complete
+off=16 len=4 span[header_field]="Host"
+off=21 header_field complete
+off=22 len=15 span[header_value]="www.example.com"
+off=39 header_value complete
+off=49 error code=10 reason="Invalid header token"
+```
+
+### Corrupted header name
+
+<!-- meta={"type": "request", "noScan": true} -->
+```http
+GET / HTTP/1.1
+Host: www.example.com
+X-Some-Header\r\033\065\325eep-Alive
+Accept-Encoding: gzip
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="GET"
+off=3 method complete
+off=4 len=1 span[url]="/"
+off=6 url complete
+off=11 len=3 span[version]="1.1"
+off=14 version complete
+off=16 len=4 span[header_field]="Host"
+off=21 header_field complete
+off=22 len=15 span[header_value]="www.example.com"
+off=39 header_value complete
+off=52 error code=10 reason="Invalid header token"
+```
+
+### Missing CR between headers
+
+<!-- meta={"type": "request", "noScan": true} -->
+
+```http
+GET / HTTP/1.1
+Host: localhost
+Dummy: x\nContent-Length: 23
+
+GET / HTTP/1.1
+Dummy: GET /admin HTTP/1.1
+Host: localhost
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="GET"
+off=3 method complete
+off=4 len=1 span[url]="/"
+off=6 url complete
+off=11 len=3 span[version]="1.1"
+off=14 version complete
+off=16 len=4 span[header_field]="Host"
+off=21 header_field complete
+off=22 len=9 span[header_value]="localhost"
+off=33 header_value complete
+off=33 len=5 span[header_field]="Dummy"
+off=39 header_field complete
+off=40 len=1 span[header_value]="x"
+off=41 error code=25 reason="Missing expected CR after header value"
+```
+
+### Invalid HTTP version
+
+<!-- meta={"type": "request", "noScan": true} -->
+```http
+GET / HTTP/5.6
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="GET"
+off=3 method complete
+off=4 len=1 span[url]="/"
+off=6 url complete
+off=11 len=3 span[version]="5.6"
+off=14 error code=9 reason="Invalid HTTP version"
+```
+
+## Invalid space after start line
+
+<!-- meta={"type": "request"} -->
+```http
+GET / HTTP/1.1
+ Host: foo
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="GET"
+off=3 method complete
+off=4 len=1 span[url]="/"
+off=6 url complete
+off=11 len=3 span[version]="1.1"
+off=14 version complete
+off=17 error code=30 reason="Unexpected space after start line"
+```
+
+
+### Only LFs present
+
+<!-- meta={"type": "request"} -->
+```http
+POST / HTTP/1.1\n\
+Transfer-Encoding: chunked\n\
+Trailer: Baz
+Foo: abc\n\
+Bar: def\n\
+\n\
+1\n\
+A\n\
+1;abc\n\
+B\n\
+1;def=ghi\n\
+C\n\
+1;jkl="mno"\n\
+D\n\
+0\n\
+\n\
+Baz: ghi\n\
+\n\
+```
+
+```log
+off=0 message begin
+off=0 len=4 span[method]="POST"
+off=4 method complete
+off=5 len=1 span[url]="/"
+off=7 url complete
+off=12 len=3 span[version]="1.1"
+off=15 version complete
+off=16 error code=9 reason="Expected CRLF after version"
+```
+
+### Only LFs present (lenient)
+
+<!-- meta={"type": "request-lenient-all"} -->
+```http
+POST / HTTP/1.1\n\
+Transfer-Encoding: chunked\n\
+Trailer: Baz
+Foo: abc\n\
+Bar: def\n\
+\n\
+1\n\
+A\n\
+1;abc\n\
+B\n\
+1;def=ghi\n\
+C\n\
+1;jkl="mno"\n\
+D\n\
+0\n\
+\n\
+Baz: ghi\n\
+\n
+```
+
+```log
+off=0 message begin
+off=0 len=4 span[method]="POST"
+off=4 method complete
+off=5 len=1 span[url]="/"
+off=7 url complete
+off=12 len=3 span[version]="1.1"
+off=15 version complete
+off=16 len=17 span[header_field]="Transfer-Encoding"
+off=34 header_field complete
+off=35 len=7 span[header_value]="chunked"
+off=43 header_value complete
+off=43 len=7 span[header_field]="Trailer"
+off=51 header_field complete
+off=52 len=3 span[header_value]="Baz"
+off=57 header_value complete
+off=57 len=3 span[header_field]="Foo"
+off=61 header_field complete
+off=62 len=3 span[header_value]="abc"
+off=66 header_value complete
+off=66 len=3 span[header_field]="Bar"
+off=70 header_field complete
+off=71 len=3 span[header_value]="def"
+off=75 header_value complete
+off=76 headers complete method=3 v=1/1 flags=208 content_length=0
+off=78 chunk header len=1
+off=78 len=1 span[body]="A"
+off=80 chunk complete
+off=82 len=3 span[chunk_extension_name]="abc"
+off=85 chunk_extension_name complete
+off=86 chunk header len=1
+off=86 len=1 span[body]="B"
+off=88 chunk complete
+off=90 len=3 span[chunk_extension_name]="def"
+off=94 chunk_extension_name complete
+off=94 len=3 span[chunk_extension_value]="ghi"
+off=97 chunk_extension_value complete
+off=98 chunk header len=1
+off=98 len=1 span[body]="C"
+off=100 chunk complete
+off=102 len=3 span[chunk_extension_name]="jkl"
+off=106 chunk_extension_name complete
+off=106 len=5 span[chunk_extension_value]=""mno""
+off=111 chunk_extension_value complete
+off=112 chunk header len=1
+off=112 len=1 span[body]="D"
+off=114 chunk complete
+off=117 chunk header len=0
+off=117 len=3 span[header_field]="Baz"
+off=121 header_field complete
+off=122 len=3 span[header_value]="ghi"
+off=126 header_value complete
+off=127 chunk complete
+off=127 message complete
+``` \ No newline at end of file
diff --git a/llhttp/test/request/lenient-headers.md b/llhttp/test/request/lenient-headers.md
new file mode 100644
index 0000000..05e105f
--- /dev/null
+++ b/llhttp/test/request/lenient-headers.md
@@ -0,0 +1,145 @@
+Lenient header value parsing
+============================
+
+Parsing with header value token checks off.
+
+## Header value (lenient)
+
+<!-- meta={"type": "request-lenient-headers"} -->
+```http
+GET /url HTTP/1.1
+Header1: \f
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="GET"
+off=3 method complete
+off=4 len=4 span[url]="/url"
+off=9 url complete
+off=14 len=3 span[version]="1.1"
+off=17 version complete
+off=19 len=7 span[header_field]="Header1"
+off=27 header_field complete
+off=28 len=1 span[header_value]="\f"
+off=31 header_value complete
+off=33 headers complete method=1 v=1/1 flags=0 content_length=0
+off=33 message complete
+```
+
+## Second request header value (lenient)
+
+<!-- meta={"type": "request-lenient-headers"} -->
+```http
+GET /url HTTP/1.1
+Header1: Okay
+
+
+GET /url HTTP/1.1
+Header1: \f
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="GET"
+off=3 method complete
+off=4 len=4 span[url]="/url"
+off=9 url complete
+off=14 len=3 span[version]="1.1"
+off=17 version complete
+off=19 len=7 span[header_field]="Header1"
+off=27 header_field complete
+off=28 len=4 span[header_value]="Okay"
+off=34 header_value complete
+off=36 headers complete method=1 v=1/1 flags=0 content_length=0
+off=36 message complete
+off=38 reset
+off=38 message begin
+off=38 len=3 span[method]="GET"
+off=41 method complete
+off=42 len=4 span[url]="/url"
+off=47 url complete
+off=52 len=3 span[version]="1.1"
+off=55 version complete
+off=57 len=7 span[header_field]="Header1"
+off=65 header_field complete
+off=66 len=1 span[header_value]="\f"
+off=69 header_value complete
+off=71 headers complete method=1 v=1/1 flags=0 content_length=0
+off=71 message complete
+```
+
+## Header value
+
+<!-- meta={"type": "request"} -->
+```http
+GET /url HTTP/1.1
+Header1: \f
+
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="GET"
+off=3 method complete
+off=4 len=4 span[url]="/url"
+off=9 url complete
+off=14 len=3 span[version]="1.1"
+off=17 version complete
+off=19 len=7 span[header_field]="Header1"
+off=27 header_field complete
+off=28 len=0 span[header_value]=""
+off=28 error code=10 reason="Invalid header value char"
+```
+
+### Empty headers separated by CR (lenient)
+
+<!-- meta={"type": "request-lenient-headers"} -->
+```http
+POST / HTTP/1.1
+Connection: Close
+Host: localhost:5000
+x:\rTransfer-Encoding: chunked
+
+1
+A
+0
+
+```
+
+```log
+off=0 message begin
+off=0 len=4 span[method]="POST"
+off=4 method complete
+off=5 len=1 span[url]="/"
+off=7 url complete
+off=12 len=3 span[version]="1.1"
+off=15 version complete
+off=17 len=10 span[header_field]="Connection"
+off=28 header_field complete
+off=29 len=5 span[header_value]="Close"
+off=36 header_value complete
+off=36 len=4 span[header_field]="Host"
+off=41 header_field complete
+off=42 len=14 span[header_value]="localhost:5000"
+off=58 header_value complete
+off=58 len=1 span[header_field]="x"
+off=60 header_field complete
+off=61 len=0 span[header_value]=""
+off=61 header_value complete
+off=61 len=17 span[header_field]="Transfer-Encoding"
+off=79 header_field complete
+off=80 len=7 span[header_value]="chunked"
+off=89 header_value complete
+off=91 headers complete method=3 v=1/1 flags=20a content_length=0
+off=94 chunk header len=1
+off=94 len=1 span[body]="A"
+off=97 chunk complete
+off=100 chunk header len=0
+``` \ No newline at end of file
diff --git a/llhttp/test/request/lenient-version.md b/llhttp/test/request/lenient-version.md
new file mode 100644
index 0000000..4185556
--- /dev/null
+++ b/llhttp/test/request/lenient-version.md
@@ -0,0 +1,23 @@
+Lenient HTTP version parsing
+============================
+
+### Invalid HTTP version (lenient)
+
+<!-- meta={"type": "request-lenient-version"} -->
+```http
+GET / HTTP/5.6
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="GET"
+off=3 method complete
+off=4 len=1 span[url]="/"
+off=6 url complete
+off=11 len=3 span[version]="5.6"
+off=14 version complete
+off=18 headers complete method=1 v=5/6 flags=0 content_length=0
+off=18 message complete
+``` \ No newline at end of file
diff --git a/llhttp/test/request/method.md b/llhttp/test/request/method.md
new file mode 100644
index 0000000..dce262e
--- /dev/null
+++ b/llhttp/test/request/method.md
@@ -0,0 +1,450 @@
+Methods
+=======
+
+### REPORT request
+
+<!-- meta={"type": "request"} -->
+```http
+REPORT /test HTTP/1.1
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=6 span[method]="REPORT"
+off=6 method complete
+off=7 len=5 span[url]="/test"
+off=13 url complete
+off=18 len=3 span[version]="1.1"
+off=21 version complete
+off=25 headers complete method=20 v=1/1 flags=0 content_length=0
+off=25 message complete
+```
+
+### CONNECT request
+
+<!-- meta={"type": "request"} -->
+```http
+CONNECT 0-home0.netscape.com:443 HTTP/1.0
+User-agent: Mozilla/1.1N
+Proxy-authorization: basic aGVsbG86d29ybGQ=
+
+some data
+and yet even more data
+```
+
+```log
+off=0 message begin
+off=0 len=7 span[method]="CONNECT"
+off=7 method complete
+off=8 len=24 span[url]="0-home0.netscape.com:443"
+off=33 url complete
+off=38 len=3 span[version]="1.0"
+off=41 version complete
+off=43 len=10 span[header_field]="User-agent"
+off=54 header_field complete
+off=55 len=12 span[header_value]="Mozilla/1.1N"
+off=69 header_value complete
+off=69 len=19 span[header_field]="Proxy-authorization"
+off=89 header_field complete
+off=90 len=22 span[header_value]="basic aGVsbG86d29ybGQ="
+off=114 header_value complete
+off=116 headers complete method=5 v=1/0 flags=0 content_length=0
+off=116 message complete
+off=116 error code=22 reason="Pause on CONNECT/Upgrade"
+```
+
+### CONNECT request with CAPS
+
+<!-- meta={"type": "request"} -->
+```http
+CONNECT HOME0.NETSCAPE.COM:443 HTTP/1.0
+User-agent: Mozilla/1.1N
+Proxy-authorization: basic aGVsbG86d29ybGQ=
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=7 span[method]="CONNECT"
+off=7 method complete
+off=8 len=22 span[url]="HOME0.NETSCAPE.COM:443"
+off=31 url complete
+off=36 len=3 span[version]="1.0"
+off=39 version complete
+off=41 len=10 span[header_field]="User-agent"
+off=52 header_field complete
+off=53 len=12 span[header_value]="Mozilla/1.1N"
+off=67 header_value complete
+off=67 len=19 span[header_field]="Proxy-authorization"
+off=87 header_field complete
+off=88 len=22 span[header_value]="basic aGVsbG86d29ybGQ="
+off=112 header_value complete
+off=114 headers complete method=5 v=1/0 flags=0 content_length=0
+off=114 message complete
+off=114 error code=22 reason="Pause on CONNECT/Upgrade"
+```
+
+### CONNECT with body
+
+<!-- meta={"type": "request"} -->
+```http
+CONNECT foo.bar.com:443 HTTP/1.0
+User-agent: Mozilla/1.1N
+Proxy-authorization: basic aGVsbG86d29ybGQ=
+Content-Length: 10
+
+blarfcicle"
+```
+
+```log
+off=0 message begin
+off=0 len=7 span[method]="CONNECT"
+off=7 method complete
+off=8 len=15 span[url]="foo.bar.com:443"
+off=24 url complete
+off=29 len=3 span[version]="1.0"
+off=32 version complete
+off=34 len=10 span[header_field]="User-agent"
+off=45 header_field complete
+off=46 len=12 span[header_value]="Mozilla/1.1N"
+off=60 header_value complete
+off=60 len=19 span[header_field]="Proxy-authorization"
+off=80 header_field complete
+off=81 len=22 span[header_value]="basic aGVsbG86d29ybGQ="
+off=105 header_value complete
+off=105 len=14 span[header_field]="Content-Length"
+off=120 header_field complete
+off=121 len=2 span[header_value]="10"
+off=125 header_value complete
+off=127 headers complete method=5 v=1/0 flags=20 content_length=10
+off=127 message complete
+off=127 error code=22 reason="Pause on CONNECT/Upgrade"
+```
+
+### M-SEARCH request
+
+<!-- meta={"type": "request"} -->
+```http
+M-SEARCH * HTTP/1.1
+HOST: 239.255.255.250:1900
+MAN: "ssdp:discover"
+ST: "ssdp:all"
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=8 span[method]="M-SEARCH"
+off=8 method complete
+off=9 len=1 span[url]="*"
+off=11 url complete
+off=16 len=3 span[version]="1.1"
+off=19 version complete
+off=21 len=4 span[header_field]="HOST"
+off=26 header_field complete
+off=27 len=20 span[header_value]="239.255.255.250:1900"
+off=49 header_value complete
+off=49 len=3 span[header_field]="MAN"
+off=53 header_field complete
+off=54 len=15 span[header_value]=""ssdp:discover""
+off=71 header_value complete
+off=71 len=2 span[header_field]="ST"
+off=74 header_field complete
+off=75 len=10 span[header_value]=""ssdp:all""
+off=87 header_value complete
+off=89 headers complete method=24 v=1/1 flags=0 content_length=0
+off=89 message complete
+```
+
+### PATCH request
+
+<!-- meta={"type": "request"} -->
+```http
+PATCH /file.txt HTTP/1.1
+Host: www.example.com
+Content-Type: application/example
+If-Match: "e0023aa4e"
+Content-Length: 10
+
+cccccccccc
+```
+
+```log
+off=0 message begin
+off=0 len=5 span[method]="PATCH"
+off=5 method complete
+off=6 len=9 span[url]="/file.txt"
+off=16 url complete
+off=21 len=3 span[version]="1.1"
+off=24 version complete
+off=26 len=4 span[header_field]="Host"
+off=31 header_field complete
+off=32 len=15 span[header_value]="www.example.com"
+off=49 header_value complete
+off=49 len=12 span[header_field]="Content-Type"
+off=62 header_field complete
+off=63 len=19 span[header_value]="application/example"
+off=84 header_value complete
+off=84 len=8 span[header_field]="If-Match"
+off=93 header_field complete
+off=94 len=11 span[header_value]=""e0023aa4e""
+off=107 header_value complete
+off=107 len=14 span[header_field]="Content-Length"
+off=122 header_field complete
+off=123 len=2 span[header_value]="10"
+off=127 header_value complete
+off=129 headers complete method=28 v=1/1 flags=20 content_length=10
+off=129 len=10 span[body]="cccccccccc"
+off=139 message complete
+```
+
+### PURGE request
+
+<!-- meta={"type": "request"} -->
+```http
+PURGE /file.txt HTTP/1.1
+Host: www.example.com
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=5 span[method]="PURGE"
+off=5 method complete
+off=6 len=9 span[url]="/file.txt"
+off=16 url complete
+off=21 len=3 span[version]="1.1"
+off=24 version complete
+off=26 len=4 span[header_field]="Host"
+off=31 header_field complete
+off=32 len=15 span[header_value]="www.example.com"
+off=49 header_value complete
+off=51 headers complete method=29 v=1/1 flags=0 content_length=0
+off=51 message complete
+```
+
+### SEARCH request
+
+<!-- meta={"type": "request"} -->
+```http
+SEARCH / HTTP/1.1
+Host: www.example.com
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=6 span[method]="SEARCH"
+off=6 method complete
+off=7 len=1 span[url]="/"
+off=9 url complete
+off=14 len=3 span[version]="1.1"
+off=17 version complete
+off=19 len=4 span[header_field]="Host"
+off=24 header_field complete
+off=25 len=15 span[header_value]="www.example.com"
+off=42 header_value complete
+off=44 headers complete method=14 v=1/1 flags=0 content_length=0
+off=44 message complete
+```
+
+### LINK request
+
+<!-- meta={"type": "request"} -->
+```http
+LINK /images/my_dog.jpg HTTP/1.1
+Host: example.com
+Link: <http://example.com/profiles/joe>; rel="tag"
+Link: <http://example.com/profiles/sally>; rel="tag"
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=4 span[method]="LINK"
+off=4 method complete
+off=5 len=18 span[url]="/images/my_dog.jpg"
+off=24 url complete
+off=29 len=3 span[version]="1.1"
+off=32 version complete
+off=34 len=4 span[header_field]="Host"
+off=39 header_field complete
+off=40 len=11 span[header_value]="example.com"
+off=53 header_value complete
+off=53 len=4 span[header_field]="Link"
+off=58 header_field complete
+off=59 len=44 span[header_value]="<http://example.com/profiles/joe>; rel="tag""
+off=105 header_value complete
+off=105 len=4 span[header_field]="Link"
+off=110 header_field complete
+off=111 len=46 span[header_value]="<http://example.com/profiles/sally>; rel="tag""
+off=159 header_value complete
+off=161 headers complete method=31 v=1/1 flags=0 content_length=0
+off=161 message complete
+```
+
+### LINK request
+
+<!-- meta={"type": "request"} -->
+```http
+UNLINK /images/my_dog.jpg HTTP/1.1
+Host: example.com
+Link: <http://example.com/profiles/sally>; rel="tag"
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=6 span[method]="UNLINK"
+off=6 method complete
+off=7 len=18 span[url]="/images/my_dog.jpg"
+off=26 url complete
+off=31 len=3 span[version]="1.1"
+off=34 version complete
+off=36 len=4 span[header_field]="Host"
+off=41 header_field complete
+off=42 len=11 span[header_value]="example.com"
+off=55 header_value complete
+off=55 len=4 span[header_field]="Link"
+off=60 header_field complete
+off=61 len=46 span[header_value]="<http://example.com/profiles/sally>; rel="tag""
+off=109 header_value complete
+off=111 headers complete method=32 v=1/1 flags=0 content_length=0
+off=111 message complete
+```
+
+### SOURCE request
+
+<!-- meta={"type": "request"} -->
+```http
+SOURCE /music/sweet/music HTTP/1.1
+Host: example.com
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=6 span[method]="SOURCE"
+off=6 method complete
+off=7 len=18 span[url]="/music/sweet/music"
+off=26 url complete
+off=31 len=3 span[version]="1.1"
+off=34 version complete
+off=36 len=4 span[header_field]="Host"
+off=41 header_field complete
+off=42 len=11 span[header_value]="example.com"
+off=55 header_value complete
+off=57 headers complete method=33 v=1/1 flags=0 content_length=0
+off=57 message complete
+```
+
+### SOURCE request with ICE
+
+<!-- meta={"type": "request"} -->
+```http
+SOURCE /music/sweet/music ICE/1.0
+Host: example.com
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=6 span[method]="SOURCE"
+off=6 method complete
+off=7 len=18 span[url]="/music/sweet/music"
+off=26 url complete
+off=30 len=3 span[version]="1.0"
+off=33 version complete
+off=35 len=4 span[header_field]="Host"
+off=40 header_field complete
+off=41 len=11 span[header_value]="example.com"
+off=54 header_value complete
+off=56 headers complete method=33 v=1/0 flags=0 content_length=0
+off=56 message complete
+```
+
+### OPTIONS request with RTSP
+
+NOTE: `OPTIONS` is a valid HTTP metho too.
+
+<!-- meta={"type": "request"} -->
+```http
+OPTIONS /music/sweet/music RTSP/1.0
+Host: example.com
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=7 span[method]="OPTIONS"
+off=7 method complete
+off=8 len=18 span[url]="/music/sweet/music"
+off=27 url complete
+off=32 len=3 span[version]="1.0"
+off=35 version complete
+off=37 len=4 span[header_field]="Host"
+off=42 header_field complete
+off=43 len=11 span[header_value]="example.com"
+off=56 header_value complete
+off=58 headers complete method=6 v=1/0 flags=0 content_length=0
+off=58 message complete
+```
+
+### ANNOUNCE request with RTSP
+
+<!-- meta={"type": "request"} -->
+```http
+ANNOUNCE /music/sweet/music RTSP/1.0
+Host: example.com
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=8 span[method]="ANNOUNCE"
+off=8 method complete
+off=9 len=18 span[url]="/music/sweet/music"
+off=28 url complete
+off=33 len=3 span[version]="1.0"
+off=36 version complete
+off=38 len=4 span[header_field]="Host"
+off=43 header_field complete
+off=44 len=11 span[header_value]="example.com"
+off=57 header_value complete
+off=59 headers complete method=36 v=1/0 flags=0 content_length=0
+off=59 message complete
+```
+
+### PRI request HTTP2
+
+<!-- meta={"type": "request"} -->
+```http
+PRI * HTTP/1.1
+
+SM
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="PRI"
+off=3 method complete
+off=4 len=1 span[url]="*"
+off=6 url complete
+off=11 len=3 span[version]="1.1"
+off=14 version complete
+off=24 error code=23 reason="Pause on PRI/Upgrade"
+```
diff --git a/llhttp/test/request/pausing.md b/llhttp/test/request/pausing.md
new file mode 100644
index 0000000..8e501e3
--- /dev/null
+++ b/llhttp/test/request/pausing.md
@@ -0,0 +1,381 @@
+Pausing
+=======
+
+### on_message_begin
+
+<!-- meta={"type": "request", "pause": "on_message_begin"} -->
+```http
+POST / HTTP/1.1
+Content-Length: 3
+
+abc
+```
+
+```log
+off=0 message begin
+off=0 pause
+off=0 len=4 span[method]="POST"
+off=4 method complete
+off=5 len=1 span[url]="/"
+off=7 url complete
+off=12 len=3 span[version]="1.1"
+off=15 version complete
+off=17 len=14 span[header_field]="Content-Length"
+off=32 header_field complete
+off=33 len=1 span[header_value]="3"
+off=36 header_value complete
+off=38 headers complete method=3 v=1/1 flags=20 content_length=3
+off=38 len=3 span[body]="abc"
+off=41 message complete
+```
+
+### on_message_complete
+
+<!-- meta={"type": "request", "pause": "on_message_complete"} -->
+```http
+POST / HTTP/1.1
+Content-Length: 3
+
+abc
+```
+
+```log
+off=0 message begin
+off=0 len=4 span[method]="POST"
+off=4 method complete
+off=5 len=1 span[url]="/"
+off=7 url complete
+off=12 len=3 span[version]="1.1"
+off=15 version complete
+off=17 len=14 span[header_field]="Content-Length"
+off=32 header_field complete
+off=33 len=1 span[header_value]="3"
+off=36 header_value complete
+off=38 headers complete method=3 v=1/1 flags=20 content_length=3
+off=38 len=3 span[body]="abc"
+off=41 message complete
+off=41 pause
+```
+
+### on_method_complete
+
+<!-- meta={"type": "request", "pause": "on_method_complete"} -->
+```http
+POST / HTTP/1.1
+Content-Length: 3
+
+abc
+```
+
+```log
+off=0 message begin
+off=0 len=4 span[method]="POST"
+off=4 method complete
+off=4 pause
+off=5 len=1 span[url]="/"
+off=7 url complete
+off=12 len=3 span[version]="1.1"
+off=15 version complete
+off=17 len=14 span[header_field]="Content-Length"
+off=32 header_field complete
+off=33 len=1 span[header_value]="3"
+off=36 header_value complete
+off=38 headers complete method=3 v=1/1 flags=20 content_length=3
+off=38 len=3 span[body]="abc"
+off=41 message complete
+```
+
+### on_url_complete
+
+<!-- meta={"type": "request", "pause": "on_url_complete"} -->
+```http
+POST / HTTP/1.1
+Content-Length: 3
+
+abc
+```
+
+```log
+off=0 message begin
+off=0 len=4 span[method]="POST"
+off=4 method complete
+off=5 len=1 span[url]="/"
+off=7 url complete
+off=7 pause
+off=12 len=3 span[version]="1.1"
+off=15 version complete
+off=17 len=14 span[header_field]="Content-Length"
+off=32 header_field complete
+off=33 len=1 span[header_value]="3"
+off=36 header_value complete
+off=38 headers complete method=3 v=1/1 flags=20 content_length=3
+off=38 len=3 span[body]="abc"
+off=41 message complete
+```
+
+### on_version_complete
+
+<!-- meta={"type": "request", "pause": "on_version_complete"} -->
+```http
+POST / HTTP/1.1
+Content-Length: 3
+
+abc
+```
+
+```log
+off=0 message begin
+off=0 len=4 span[method]="POST"
+off=4 method complete
+off=5 len=1 span[url]="/"
+off=7 url complete
+off=12 len=3 span[version]="1.1"
+off=15 version complete
+off=15 pause
+off=17 len=14 span[header_field]="Content-Length"
+off=32 header_field complete
+off=33 len=1 span[header_value]="3"
+off=36 header_value complete
+off=38 headers complete method=3 v=1/1 flags=20 content_length=3
+off=38 len=3 span[body]="abc"
+off=41 message complete
+```
+
+### on_header_field_complete
+
+<!-- meta={"type": "request", "pause": "on_header_field_complete"} -->
+```http
+POST / HTTP/1.1
+Content-Length: 3
+
+abc
+```
+
+```log
+off=0 message begin
+off=0 len=4 span[method]="POST"
+off=4 method complete
+off=5 len=1 span[url]="/"
+off=7 url complete
+off=12 len=3 span[version]="1.1"
+off=15 version complete
+off=17 len=14 span[header_field]="Content-Length"
+off=32 header_field complete
+off=32 pause
+off=33 len=1 span[header_value]="3"
+off=36 header_value complete
+off=38 headers complete method=3 v=1/1 flags=20 content_length=3
+off=38 len=3 span[body]="abc"
+off=41 message complete
+```
+
+### on_header_value_complete
+
+<!-- meta={"type": "request", "pause": "on_header_value_complete"} -->
+```http
+POST / HTTP/1.1
+Content-Length: 3
+
+abc
+```
+
+```log
+off=0 message begin
+off=0 len=4 span[method]="POST"
+off=4 method complete
+off=5 len=1 span[url]="/"
+off=7 url complete
+off=12 len=3 span[version]="1.1"
+off=15 version complete
+off=17 len=14 span[header_field]="Content-Length"
+off=32 header_field complete
+off=33 len=1 span[header_value]="3"
+off=36 header_value complete
+off=36 pause
+off=38 headers complete method=3 v=1/1 flags=20 content_length=3
+off=38 len=3 span[body]="abc"
+off=41 message complete
+```
+
+### on_headers_complete
+
+<!-- meta={"type": "request", "pause": "on_headers_complete"} -->
+```http
+POST / HTTP/1.1
+Content-Length: 3
+
+abc
+```
+
+```log
+off=0 message begin
+off=0 len=4 span[method]="POST"
+off=4 method complete
+off=5 len=1 span[url]="/"
+off=7 url complete
+off=12 len=3 span[version]="1.1"
+off=15 version complete
+off=17 len=14 span[header_field]="Content-Length"
+off=32 header_field complete
+off=33 len=1 span[header_value]="3"
+off=36 header_value complete
+off=38 headers complete method=3 v=1/1 flags=20 content_length=3
+off=38 pause
+off=38 len=3 span[body]="abc"
+off=41 message complete
+```
+
+### on_chunk_header
+
+<!-- meta={"type": "request", "pause": "on_chunk_header"} -->
+```http
+PUT / HTTP/1.1
+Transfer-Encoding: chunked
+
+a
+0123456789
+0
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="PUT"
+off=3 method complete
+off=4 len=1 span[url]="/"
+off=6 url complete
+off=11 len=3 span[version]="1.1"
+off=14 version complete
+off=16 len=17 span[header_field]="Transfer-Encoding"
+off=34 header_field complete
+off=35 len=7 span[header_value]="chunked"
+off=44 header_value complete
+off=46 headers complete method=4 v=1/1 flags=208 content_length=0
+off=49 chunk header len=10
+off=49 pause
+off=49 len=10 span[body]="0123456789"
+off=61 chunk complete
+off=64 chunk header len=0
+off=64 pause
+off=66 chunk complete
+off=66 message complete
+```
+
+### on_chunk_extension_name
+
+<!-- meta={"type": "request", "pause": "on_chunk_extension_name"} -->
+```http
+PUT / HTTP/1.1
+Transfer-Encoding: chunked
+
+a;foo=bar
+0123456789
+0
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="PUT"
+off=3 method complete
+off=4 len=1 span[url]="/"
+off=6 url complete
+off=11 len=3 span[version]="1.1"
+off=14 version complete
+off=16 len=17 span[header_field]="Transfer-Encoding"
+off=34 header_field complete
+off=35 len=7 span[header_value]="chunked"
+off=44 header_value complete
+off=46 headers complete method=4 v=1/1 flags=208 content_length=0
+off=48 len=3 span[chunk_extension_name]="foo"
+off=52 chunk_extension_name complete
+off=52 pause
+off=52 len=3 span[chunk_extension_value]="bar"
+off=56 chunk_extension_value complete
+off=57 chunk header len=10
+off=57 len=10 span[body]="0123456789"
+off=69 chunk complete
+off=72 chunk header len=0
+off=74 chunk complete
+off=74 message complete
+```
+
+### on_chunk_extension_value
+
+<!-- meta={"type": "request", "pause": "on_chunk_extension_value"} -->
+```http
+PUT / HTTP/1.1
+Transfer-Encoding: chunked
+
+a;foo=bar
+0123456789
+0
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="PUT"
+off=3 method complete
+off=4 len=1 span[url]="/"
+off=6 url complete
+off=11 len=3 span[version]="1.1"
+off=14 version complete
+off=16 len=17 span[header_field]="Transfer-Encoding"
+off=34 header_field complete
+off=35 len=7 span[header_value]="chunked"
+off=44 header_value complete
+off=46 headers complete method=4 v=1/1 flags=208 content_length=0
+off=48 len=3 span[chunk_extension_name]="foo"
+off=52 chunk_extension_name complete
+off=52 len=3 span[chunk_extension_value]="bar"
+off=56 chunk_extension_value complete
+off=56 pause
+off=57 chunk header len=10
+off=57 len=10 span[body]="0123456789"
+off=69 chunk complete
+off=72 chunk header len=0
+off=74 chunk complete
+off=74 message complete
+```
+
+
+### on_chunk_complete
+
+<!-- meta={"type": "request", "pause": "on_chunk_complete"} -->
+```http
+PUT / HTTP/1.1
+Transfer-Encoding: chunked
+
+a
+0123456789
+0
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="PUT"
+off=3 method complete
+off=4 len=1 span[url]="/"
+off=6 url complete
+off=11 len=3 span[version]="1.1"
+off=14 version complete
+off=16 len=17 span[header_field]="Transfer-Encoding"
+off=34 header_field complete
+off=35 len=7 span[header_value]="chunked"
+off=44 header_value complete
+off=46 headers complete method=4 v=1/1 flags=208 content_length=0
+off=49 chunk header len=10
+off=49 len=10 span[body]="0123456789"
+off=61 chunk complete
+off=61 pause
+off=64 chunk header len=0
+off=66 chunk complete
+off=66 pause
+off=66 message complete
+```
diff --git a/llhttp/test/request/pipelining.md b/llhttp/test/request/pipelining.md
new file mode 100644
index 0000000..bdfe6ab
--- /dev/null
+++ b/llhttp/test/request/pipelining.md
@@ -0,0 +1,66 @@
+Pipelining
+==========
+
+## Should parse multiple events
+
+<!-- meta={"type": "request"} -->
+```http
+POST /aaa HTTP/1.1
+Content-Length: 3
+
+AAA
+PUT /bbb HTTP/1.1
+Content-Length: 4
+
+BBBB
+PATCH /ccc HTTP/1.1
+Content-Length: 5
+
+CCCC
+```
+
+```log
+off=0 message begin
+off=0 len=4 span[method]="POST"
+off=4 method complete
+off=5 len=4 span[url]="/aaa"
+off=10 url complete
+off=15 len=3 span[version]="1.1"
+off=18 version complete
+off=20 len=14 span[header_field]="Content-Length"
+off=35 header_field complete
+off=36 len=1 span[header_value]="3"
+off=39 header_value complete
+off=41 headers complete method=3 v=1/1 flags=20 content_length=3
+off=41 len=3 span[body]="AAA"
+off=44 message complete
+off=46 reset
+off=46 message begin
+off=46 len=3 span[method]="PUT"
+off=49 method complete
+off=50 len=4 span[url]="/bbb"
+off=55 url complete
+off=60 len=3 span[version]="1.1"
+off=63 version complete
+off=65 len=14 span[header_field]="Content-Length"
+off=80 header_field complete
+off=81 len=1 span[header_value]="4"
+off=84 header_value complete
+off=86 headers complete method=4 v=1/1 flags=20 content_length=4
+off=86 len=4 span[body]="BBBB"
+off=90 message complete
+off=92 reset
+off=92 message begin
+off=92 len=5 span[method]="PATCH"
+off=97 method complete
+off=98 len=4 span[url]="/ccc"
+off=103 url complete
+off=108 len=3 span[version]="1.1"
+off=111 version complete
+off=113 len=14 span[header_field]="Content-Length"
+off=128 header_field complete
+off=129 len=1 span[header_value]="5"
+off=132 header_value complete
+off=134 headers complete method=28 v=1/1 flags=20 content_length=5
+off=134 len=4 span[body]="CCCC"
+``` \ No newline at end of file
diff --git a/llhttp/test/request/sample.md b/llhttp/test/request/sample.md
new file mode 100644
index 0000000..f0a5d44
--- /dev/null
+++ b/llhttp/test/request/sample.md
@@ -0,0 +1,629 @@
+Sample requests
+===============
+
+Lots of sample requests, most ported from [http_parser][0] test suite.
+
+## Simple request
+
+<!-- meta={"type": "request"} -->
+```http
+OPTIONS /url HTTP/1.1
+Header1: Value1
+Header2:\t Value2
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=7 span[method]="OPTIONS"
+off=7 method complete
+off=8 len=4 span[url]="/url"
+off=13 url complete
+off=18 len=3 span[version]="1.1"
+off=21 version complete
+off=23 len=7 span[header_field]="Header1"
+off=31 header_field complete
+off=32 len=6 span[header_value]="Value1"
+off=40 header_value complete
+off=40 len=7 span[header_field]="Header2"
+off=48 header_field complete
+off=50 len=6 span[header_value]="Value2"
+off=58 header_value complete
+off=60 headers complete method=6 v=1/1 flags=0 content_length=0
+off=60 message complete
+```
+
+## Request with method starting with `H`
+
+There's a optimization in `start_req_or_res` that passes execution to
+`start_req` when the first character is not `H` (because response must start
+with `HTTP/`). However, there're still methods like `HEAD` that should get
+to `start_req`. Verify that it still works after optimization.
+
+<!-- meta={"type": "request", "noScan": true } -->
+```http
+HEAD /url HTTP/1.1
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=4 span[method]="HEAD"
+off=4 method complete
+off=5 len=4 span[url]="/url"
+off=10 url complete
+off=15 len=3 span[version]="1.1"
+off=18 version complete
+off=22 headers complete method=2 v=1/1 flags=0 content_length=0
+off=22 message complete
+```
+
+## curl GET
+
+<!-- meta={"type": "request"} -->
+```http
+GET /test HTTP/1.1
+User-Agent: curl/7.18.0 (i486-pc-linux-gnu) libcurl/7.18.0 OpenSSL/0.9.8g zlib/1.2.3.3 libidn/1.1
+Host: 0.0.0.0=5000
+Accept: */*
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="GET"
+off=3 method complete
+off=4 len=5 span[url]="/test"
+off=10 url complete
+off=15 len=3 span[version]="1.1"
+off=18 version complete
+off=20 len=10 span[header_field]="User-Agent"
+off=31 header_field complete
+off=32 len=85 span[header_value]="curl/7.18.0 (i486-pc-linux-gnu) libcurl/7.18.0 OpenSSL/0.9.8g zlib/1.2.3.3 libidn/1.1"
+off=119 header_value complete
+off=119 len=4 span[header_field]="Host"
+off=124 header_field complete
+off=125 len=12 span[header_value]="0.0.0.0=5000"
+off=139 header_value complete
+off=139 len=6 span[header_field]="Accept"
+off=146 header_field complete
+off=147 len=3 span[header_value]="*/*"
+off=152 header_value complete
+off=154 headers complete method=1 v=1/1 flags=0 content_length=0
+off=154 message complete
+```
+
+## Firefox GET
+
+<!-- meta={"type": "request"} -->
+```http
+GET /favicon.ico HTTP/1.1
+Host: 0.0.0.0=5000
+User-Agent: Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9) Gecko/2008061015 Firefox/3.0
+Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
+Accept-Language: en-us,en;q=0.5
+Accept-Encoding: gzip,deflate
+Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
+Keep-Alive: 300
+Connection: keep-alive
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="GET"
+off=3 method complete
+off=4 len=12 span[url]="/favicon.ico"
+off=17 url complete
+off=22 len=3 span[version]="1.1"
+off=25 version complete
+off=27 len=4 span[header_field]="Host"
+off=32 header_field complete
+off=33 len=12 span[header_value]="0.0.0.0=5000"
+off=47 header_value complete
+off=47 len=10 span[header_field]="User-Agent"
+off=58 header_field complete
+off=59 len=76 span[header_value]="Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9) Gecko/2008061015 Firefox/3.0"
+off=137 header_value complete
+off=137 len=6 span[header_field]="Accept"
+off=144 header_field complete
+off=145 len=63 span[header_value]="text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
+off=210 header_value complete
+off=210 len=15 span[header_field]="Accept-Language"
+off=226 header_field complete
+off=227 len=14 span[header_value]="en-us,en;q=0.5"
+off=243 header_value complete
+off=243 len=15 span[header_field]="Accept-Encoding"
+off=259 header_field complete
+off=260 len=12 span[header_value]="gzip,deflate"
+off=274 header_value complete
+off=274 len=14 span[header_field]="Accept-Charset"
+off=289 header_field complete
+off=290 len=30 span[header_value]="ISO-8859-1,utf-8;q=0.7,*;q=0.7"
+off=322 header_value complete
+off=322 len=10 span[header_field]="Keep-Alive"
+off=333 header_field complete
+off=334 len=3 span[header_value]="300"
+off=339 header_value complete
+off=339 len=10 span[header_field]="Connection"
+off=350 header_field complete
+off=351 len=10 span[header_value]="keep-alive"
+off=363 header_value complete
+off=365 headers complete method=1 v=1/1 flags=1 content_length=0
+off=365 message complete
+```
+
+## DUMBPACK
+
+<!-- meta={"type": "request"} -->
+```http
+GET /dumbpack HTTP/1.1
+aaaaaaaaaaaaa:++++++++++
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="GET"
+off=3 method complete
+off=4 len=9 span[url]="/dumbpack"
+off=14 url complete
+off=19 len=3 span[version]="1.1"
+off=22 version complete
+off=24 len=13 span[header_field]="aaaaaaaaaaaaa"
+off=38 header_field complete
+off=38 len=10 span[header_value]="++++++++++"
+off=50 header_value complete
+off=52 headers complete method=1 v=1/1 flags=0 content_length=0
+off=52 message complete
+```
+
+## No headers and no body
+
+<!-- meta={"type": "request"} -->
+```http
+GET /get_no_headers_no_body/world HTTP/1.1
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="GET"
+off=3 method complete
+off=4 len=29 span[url]="/get_no_headers_no_body/world"
+off=34 url complete
+off=39 len=3 span[version]="1.1"
+off=42 version complete
+off=46 headers complete method=1 v=1/1 flags=0 content_length=0
+off=46 message complete
+```
+
+## One header and no body
+
+<!-- meta={"type": "request"} -->
+```http
+GET /get_one_header_no_body HTTP/1.1
+Accept: */*
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="GET"
+off=3 method complete
+off=4 len=23 span[url]="/get_one_header_no_body"
+off=28 url complete
+off=33 len=3 span[version]="1.1"
+off=36 version complete
+off=38 len=6 span[header_field]="Accept"
+off=45 header_field complete
+off=46 len=3 span[header_value]="*/*"
+off=51 header_value complete
+off=53 headers complete method=1 v=1/1 flags=0 content_length=0
+off=53 message complete
+```
+
+## Apache bench GET
+
+The server receiving this request SHOULD NOT wait for EOF to know that
+`Content-Length == 0`.
+
+<!-- meta={"type": "request"} -->
+```http
+GET /test HTTP/1.0
+Host: 0.0.0.0:5000
+User-Agent: ApacheBench/2.3
+Accept: */*
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="GET"
+off=3 method complete
+off=4 len=5 span[url]="/test"
+off=10 url complete
+off=15 len=3 span[version]="1.0"
+off=18 version complete
+off=20 len=4 span[header_field]="Host"
+off=25 header_field complete
+off=26 len=12 span[header_value]="0.0.0.0:5000"
+off=40 header_value complete
+off=40 len=10 span[header_field]="User-Agent"
+off=51 header_field complete
+off=52 len=15 span[header_value]="ApacheBench/2.3"
+off=69 header_value complete
+off=69 len=6 span[header_field]="Accept"
+off=76 header_field complete
+off=77 len=3 span[header_value]="*/*"
+off=82 header_value complete
+off=84 headers complete method=1 v=1/0 flags=0 content_length=0
+off=84 message complete
+```
+
+## Prefix newline
+
+Some clients, especially after a POST in a keep-alive connection,
+will send an extra CRLF before the next request.
+
+<!-- meta={"type": "request"} -->
+```http
+\r\nGET /test HTTP/1.1
+
+
+```
+
+```log
+off=2 message begin
+off=2 len=3 span[method]="GET"
+off=5 method complete
+off=6 len=5 span[url]="/test"
+off=12 url complete
+off=17 len=3 span[version]="1.1"
+off=20 version complete
+off=24 headers complete method=1 v=1/1 flags=0 content_length=0
+off=24 message complete
+```
+
+## No HTTP version
+
+<!-- meta={"type": "request"} -->
+```http
+GET /
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="GET"
+off=3 method complete
+off=4 len=1 span[url]="/"
+off=7 url complete
+off=9 headers complete method=1 v=0/9 flags=0 content_length=0
+off=9 message complete
+```
+
+## Line folding in header value with CRLF
+
+<!-- meta={"type": "request-lenient-headers"} -->
+```http
+GET / HTTP/1.1
+Line1: abc
+\tdef
+ ghi
+\t\tjkl
+ mno
+\t \tqrs
+Line2: \t line2\t
+Line3:
+ line3
+Line4:
+
+Connection:
+ close
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="GET"
+off=3 method complete
+off=4 len=1 span[url]="/"
+off=6 url complete
+off=11 len=3 span[version]="1.1"
+off=14 version complete
+off=16 len=5 span[header_field]="Line1"
+off=22 header_field complete
+off=25 len=3 span[header_value]="abc"
+off=30 len=4 span[header_value]="\tdef"
+off=36 len=4 span[header_value]=" ghi"
+off=42 len=5 span[header_value]="\t\tjkl"
+off=49 len=6 span[header_value]=" mno "
+off=57 len=6 span[header_value]="\t \tqrs"
+off=65 header_value complete
+off=65 len=5 span[header_field]="Line2"
+off=71 header_field complete
+off=74 len=6 span[header_value]="line2\t"
+off=82 header_value complete
+off=82 len=5 span[header_field]="Line3"
+off=88 header_field complete
+off=91 len=5 span[header_value]="line3"
+off=98 header_value complete
+off=98 len=5 span[header_field]="Line4"
+off=104 header_field complete
+off=110 len=0 span[header_value]=""
+off=110 header_value complete
+off=110 len=10 span[header_field]="Connection"
+off=121 header_field complete
+off=124 len=5 span[header_value]="close"
+off=131 header_value complete
+off=133 headers complete method=1 v=1/1 flags=2 content_length=0
+off=133 message complete
+```
+
+## Line folding in header value with LF
+
+<!-- meta={"type": "request"} -->
+
+```http
+GET / HTTP/1.1
+Line1: abc\n\
+\tdef\n\
+ ghi\n\
+\t\tjkl\n\
+ mno \n\
+\t \tqrs\n\
+Line2: \t line2\t\n\
+Line3:\n\
+ line3\n\
+Line4: \n\
+ \n\
+Connection:\n\
+ close\n\
+\n
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="GET"
+off=3 method complete
+off=4 len=1 span[url]="/"
+off=6 url complete
+off=11 len=3 span[version]="1.1"
+off=14 version complete
+off=16 len=5 span[header_field]="Line1"
+off=22 header_field complete
+off=25 len=3 span[header_value]="abc"
+off=28 error code=25 reason="Missing expected CR after header value"
+```
+
+## No LF after CR
+
+<!-- meta={"type":"request"} -->
+
+```http
+GET / HTTP/1.1\rLine: 1
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="GET"
+off=3 method complete
+off=4 len=1 span[url]="/"
+off=6 url complete
+off=11 len=3 span[version]="1.1"
+off=14 version complete
+off=15 error code=2 reason="Expected CRLF after version"
+```
+
+## No LF after CR (lenient)
+
+<!-- meta={"type":"request-lenient-optional-lf-after-cr"} -->
+
+```http
+GET / HTTP/1.1\rLine: 1
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="GET"
+off=3 method complete
+off=4 len=1 span[url]="/"
+off=6 url complete
+off=11 len=3 span[version]="1.1"
+off=14 version complete
+off=15 len=4 span[header_field]="Line"
+off=20 header_field complete
+off=21 len=1 span[header_value]="1"
+```
+
+## Request starting with CRLF
+
+<!-- meta={"type": "request"} -->
+```http
+\r\nGET /url HTTP/1.1
+Header1: Value1
+
+
+```
+
+```log
+off=2 message begin
+off=2 len=3 span[method]="GET"
+off=5 method complete
+off=6 len=4 span[url]="/url"
+off=11 url complete
+off=16 len=3 span[version]="1.1"
+off=19 version complete
+off=21 len=7 span[header_field]="Header1"
+off=29 header_field complete
+off=30 len=6 span[header_value]="Value1"
+off=38 header_value complete
+off=40 headers complete method=1 v=1/1 flags=0 content_length=0
+off=40 message complete
+```
+
+## Extended Characters
+
+See nodejs/test/parallel/test-http-headers-obstext.js
+
+<!-- meta={"type": "request", "noScan": true} -->
+```http
+GET / HTTP/1.1
+Test: Düsseldorf
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="GET"
+off=3 method complete
+off=4 len=1 span[url]="/"
+off=6 url complete
+off=11 len=3 span[version]="1.1"
+off=14 version complete
+off=16 len=4 span[header_field]="Test"
+off=21 header_field complete
+off=22 len=11 span[header_value]="Düsseldorf"
+off=35 header_value complete
+off=37 headers complete method=1 v=1/1 flags=0 content_length=0
+off=37 message complete
+```
+
+## 255 ASCII in header value
+
+Note: `Buffer.from([ 0xff ]).toString('latin1') === 'ÿ'`.
+
+<!-- meta={"type": "request", "noScan": true} -->
+```http
+OPTIONS /url HTTP/1.1
+Header1: Value1
+Header2: \xffValue2
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=7 span[method]="OPTIONS"
+off=7 method complete
+off=8 len=4 span[url]="/url"
+off=13 url complete
+off=18 len=3 span[version]="1.1"
+off=21 version complete
+off=23 len=7 span[header_field]="Header1"
+off=31 header_field complete
+off=32 len=6 span[header_value]="Value1"
+off=40 header_value complete
+off=40 len=7 span[header_field]="Header2"
+off=48 header_field complete
+off=49 len=8 span[header_value]="ÿValue2"
+off=59 header_value complete
+off=61 headers complete method=6 v=1/1 flags=0 content_length=0
+off=61 message complete
+```
+
+## X-SSL-Nonsense
+
+See nodejs/test/parallel/test-http-headers-obstext.js
+
+<!-- meta={"type": "request"} -->
+```http
+GET / HTTP/1.1
+X-SSL-Nonsense: -----BEGIN CERTIFICATE-----
+\tMIIFbTCCBFWgAwIBAgICH4cwDQYJKoZIhvcNAQEFBQAwcDELMAkGA1UEBhMCVUsx
+\tETAPBgNVBAoTCGVTY2llbmNlMRIwEAYDVQQLEwlBdXRob3JpdHkxCzAJBgNVBAMT
+\tAkNBMS0wKwYJKoZIhvcNAQkBFh5jYS1vcGVyYXRvckBncmlkLXN1cHBvcnQuYWMu
+\tdWswHhcNMDYwNzI3MTQxMzI4WhcNMDcwNzI3MTQxMzI4WjBbMQswCQYDVQQGEwJV
+\tSzERMA8GA1UEChMIZVNjaWVuY2UxEzARBgNVBAsTCk1hbmNoZXN0ZXIxCzAJBgNV
+\tBAcTmrsogriqMWLAk1DMRcwFQYDVQQDEw5taWNoYWVsIHBhcmQYJKoZIhvcNAQEB
+\tBQADggEPADCCAQoCggEBANPEQBgl1IaKdSS1TbhF3hEXSl72G9J+WC/1R64fAcEF
+\tW51rEyFYiIeZGx/BVzwXbeBoNUK41OK65sxGuflMo5gLflbwJtHBRIEKAfVVp3YR
+\tgW7cMA/s/XKgL1GEC7rQw8lIZT8RApukCGqOVHSi/F1SiFlPDxuDfmdiNzL31+sL
+\t0iwHDdNkGjy5pyBSB8Y79dsSJtCW/iaLB0/n8Sj7HgvvZJ7x0fr+RQjYOUUfrePP
+\tu2MSpFyf+9BbC/aXgaZuiCvSR+8Snv3xApQY+fULK/xY8h8Ua51iXoQ5jrgu2SqR
+\twgA7BUi3G8LFzMBl8FRCDYGUDy7M6QaHXx1ZWIPWNKsCAwEAAaOCAiQwggIgMAwG
+\tA1UdEwEB/wQCMAAwEQYJYIZIAYb4QgHTTPAQDAgWgMA4GA1UdDwEB/wQEAwID6DAs
+\tBglghkgBhvhCAQ0EHxYdVUsgZS1TY2llbmNlIFVzZXIgQ2VydGlmaWNhdGUwHQYD
+\tVR0OBBYEFDTt/sf9PeMaZDHkUIldrDYMNTBZMIGaBgNVHSMEgZIwgY+AFAI4qxGj
+\tloCLDdMVKwiljjDastqooXSkcjBwMQswCQYDVQQGEwJVSzERMA8GA1UEChMIZVNj
+\taWVuY2UxEjAQBgNVBAsTCUF1dGhvcml0eTELMAkGA1UEAxMCQ0ExLTArBgkqhkiG
+\t9w0BCQEWHmNhLW9wZXJhdG9yQGdyaWQtc3VwcG9ydC5hYy51a4IBADApBgNVHRIE
+\tIjAggR5jYS1vcGVyYXRvckBncmlkLXN1cHBvcnQuYWMudWswGQYDVR0gBBIwEDAO
+\tBgwrBgEEAdkvAQEBAQYwPQYJYIZIAYb4QgEEBDAWLmh0dHA6Ly9jYS5ncmlkLXN1
+\tcHBvcnQuYWMudmT4sopwqlBWsvcHViL2NybC9jYWNybC5jcmwwPQYJYIZIAYb4QgEDBDAWLmh0
+\tdHA6Ly9jYS5ncmlkLXN1cHBvcnQuYWMudWsvcHViL2NybC9jYWNybC5jcmwwPwYD
+\tVR0fBDgwNjA0oDKgMIYuaHR0cDovL2NhLmdyaWQt5hYy51ay9wdWIv
+\tY3JsL2NhY3JsLmNybDANBgkqhkiG9w0BAQUFAAOCAQEAS/U4iiooBENGW/Hwmmd3
+\tXCy6Zrt08YjKCzGNjorT98g8uGsqYjSxv/hmi0qlnlHs+k/3Iobc3LjS5AMYr5L8
+\tUO7OSkgFFlLHQyC9JzPfmLCAugvzEbyv4Olnsr8hbxF1MbKZoQxUZtMVu29wjfXk
+\thTeApBv7eaKCWpSp7MCbvgzm74izKhu3vlDk9w6qVrxePfGgpKPqfHiOoGhFnbTK
+\twTC6o2xq5y0qZ03JonF7OJspEd3I5zKY3E+ov7/ZhW6DqT8UFvsAdjvQbXyhV8Eu
+\tYhixw1aKEPzNjNowuIseVogKOLXxWI5vAi5HgXdS0/ES5gDGsABo4fqovUKlgop3
+\tRA==
+\t-----END CERTIFICATE-----
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="GET"
+off=3 method complete
+off=4 len=1 span[url]="/"
+off=6 url complete
+off=11 len=3 span[version]="1.1"
+off=14 version complete
+off=16 len=14 span[header_field]="X-SSL-Nonsense"
+off=31 header_field complete
+off=34 len=27 span[header_value]="-----BEGIN CERTIFICATE-----"
+off=63 len=65 span[header_value]="\tMIIFbTCCBFWgAwIBAgICH4cwDQYJKoZIhvcNAQEFBQAwcDELMAkGA1UEBhMCVUsx"
+off=130 len=65 span[header_value]="\tETAPBgNVBAoTCGVTY2llbmNlMRIwEAYDVQQLEwlBdXRob3JpdHkxCzAJBgNVBAMT"
+off=197 len=65 span[header_value]="\tAkNBMS0wKwYJKoZIhvcNAQkBFh5jYS1vcGVyYXRvckBncmlkLXN1cHBvcnQuYWMu"
+off=264 len=65 span[header_value]="\tdWswHhcNMDYwNzI3MTQxMzI4WhcNMDcwNzI3MTQxMzI4WjBbMQswCQYDVQQGEwJV"
+off=331 len=65 span[header_value]="\tSzERMA8GA1UEChMIZVNjaWVuY2UxEzARBgNVBAsTCk1hbmNoZXN0ZXIxCzAJBgNV"
+off=398 len=65 span[header_value]="\tBAcTmrsogriqMWLAk1DMRcwFQYDVQQDEw5taWNoYWVsIHBhcmQYJKoZIhvcNAQEB"
+off=465 len=65 span[header_value]="\tBQADggEPADCCAQoCggEBANPEQBgl1IaKdSS1TbhF3hEXSl72G9J+WC/1R64fAcEF"
+off=532 len=65 span[header_value]="\tW51rEyFYiIeZGx/BVzwXbeBoNUK41OK65sxGuflMo5gLflbwJtHBRIEKAfVVp3YR"
+off=599 len=65 span[header_value]="\tgW7cMA/s/XKgL1GEC7rQw8lIZT8RApukCGqOVHSi/F1SiFlPDxuDfmdiNzL31+sL"
+off=666 len=65 span[header_value]="\t0iwHDdNkGjy5pyBSB8Y79dsSJtCW/iaLB0/n8Sj7HgvvZJ7x0fr+RQjYOUUfrePP"
+off=733 len=65 span[header_value]="\tu2MSpFyf+9BbC/aXgaZuiCvSR+8Snv3xApQY+fULK/xY8h8Ua51iXoQ5jrgu2SqR"
+off=800 len=65 span[header_value]="\twgA7BUi3G8LFzMBl8FRCDYGUDy7M6QaHXx1ZWIPWNKsCAwEAAaOCAiQwggIgMAwG"
+off=867 len=66 span[header_value]="\tA1UdEwEB/wQCMAAwEQYJYIZIAYb4QgHTTPAQDAgWgMA4GA1UdDwEB/wQEAwID6DAs"
+off=935 len=65 span[header_value]="\tBglghkgBhvhCAQ0EHxYdVUsgZS1TY2llbmNlIFVzZXIgQ2VydGlmaWNhdGUwHQYD"
+off=1002 len=65 span[header_value]="\tVR0OBBYEFDTt/sf9PeMaZDHkUIldrDYMNTBZMIGaBgNVHSMEgZIwgY+AFAI4qxGj"
+off=1069 len=65 span[header_value]="\tloCLDdMVKwiljjDastqooXSkcjBwMQswCQYDVQQGEwJVSzERMA8GA1UEChMIZVNj"
+off=1136 len=65 span[header_value]="\taWVuY2UxEjAQBgNVBAsTCUF1dGhvcml0eTELMAkGA1UEAxMCQ0ExLTArBgkqhkiG"
+off=1203 len=65 span[header_value]="\t9w0BCQEWHmNhLW9wZXJhdG9yQGdyaWQtc3VwcG9ydC5hYy51a4IBADApBgNVHRIE"
+off=1270 len=65 span[header_value]="\tIjAggR5jYS1vcGVyYXRvckBncmlkLXN1cHBvcnQuYWMudWswGQYDVR0gBBIwEDAO"
+off=1337 len=65 span[header_value]="\tBgwrBgEEAdkvAQEBAQYwPQYJYIZIAYb4QgEEBDAWLmh0dHA6Ly9jYS5ncmlkLXN1"
+off=1404 len=75 span[header_value]="\tcHBvcnQuYWMudmT4sopwqlBWsvcHViL2NybC9jYWNybC5jcmwwPQYJYIZIAYb4QgEDBDAWLmh0"
+off=1481 len=65 span[header_value]="\tdHA6Ly9jYS5ncmlkLXN1cHBvcnQuYWMudWsvcHViL2NybC9jYWNybC5jcmwwPwYD"
+off=1548 len=55 span[header_value]="\tVR0fBDgwNjA0oDKgMIYuaHR0cDovL2NhLmdyaWQt5hYy51ay9wdWIv"
+off=1605 len=65 span[header_value]="\tY3JsL2NhY3JsLmNybDANBgkqhkiG9w0BAQUFAAOCAQEAS/U4iiooBENGW/Hwmmd3"
+off=1672 len=65 span[header_value]="\tXCy6Zrt08YjKCzGNjorT98g8uGsqYjSxv/hmi0qlnlHs+k/3Iobc3LjS5AMYr5L8"
+off=1739 len=65 span[header_value]="\tUO7OSkgFFlLHQyC9JzPfmLCAugvzEbyv4Olnsr8hbxF1MbKZoQxUZtMVu29wjfXk"
+off=1806 len=65 span[header_value]="\thTeApBv7eaKCWpSp7MCbvgzm74izKhu3vlDk9w6qVrxePfGgpKPqfHiOoGhFnbTK"
+off=1873 len=65 span[header_value]="\twTC6o2xq5y0qZ03JonF7OJspEd3I5zKY3E+ov7/ZhW6DqT8UFvsAdjvQbXyhV8Eu"
+off=1940 len=65 span[header_value]="\tYhixw1aKEPzNjNowuIseVogKOLXxWI5vAi5HgXdS0/ES5gDGsABo4fqovUKlgop3"
+off=2007 len=5 span[header_value]="\tRA=="
+off=2014 len=26 span[header_value]="\t-----END CERTIFICATE-----"
+off=2042 header_value complete
+off=2044 headers complete method=1 v=1/1 flags=0 content_length=0
+off=2044 message complete
+```
+
+[0]: https://github.com/nodejs/http-parser
diff --git a/llhttp/test/request/transfer-encoding.md b/llhttp/test/request/transfer-encoding.md
new file mode 100644
index 0000000..0f839bc
--- /dev/null
+++ b/llhttp/test/request/transfer-encoding.md
@@ -0,0 +1,1187 @@
+Transfer-Encoding header
+========================
+
+## `chunked`
+
+### Parsing and setting flag
+
+<!-- meta={"type": "request"} -->
+```http
+PUT /url HTTP/1.1
+Transfer-Encoding: chunked
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="PUT"
+off=3 method complete
+off=4 len=4 span[url]="/url"
+off=9 url complete
+off=14 len=3 span[version]="1.1"
+off=17 version complete
+off=19 len=17 span[header_field]="Transfer-Encoding"
+off=37 header_field complete
+off=38 len=7 span[header_value]="chunked"
+off=47 header_value complete
+off=49 headers complete method=4 v=1/1 flags=208 content_length=0
+```
+
+### Parse chunks with lowercase size
+
+<!-- meta={"type": "request"} -->
+```http
+PUT /url HTTP/1.1
+Transfer-Encoding: chunked
+
+a
+0123456789
+0
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="PUT"
+off=3 method complete
+off=4 len=4 span[url]="/url"
+off=9 url complete
+off=14 len=3 span[version]="1.1"
+off=17 version complete
+off=19 len=17 span[header_field]="Transfer-Encoding"
+off=37 header_field complete
+off=38 len=7 span[header_value]="chunked"
+off=47 header_value complete
+off=49 headers complete method=4 v=1/1 flags=208 content_length=0
+off=52 chunk header len=10
+off=52 len=10 span[body]="0123456789"
+off=64 chunk complete
+off=67 chunk header len=0
+off=69 chunk complete
+off=69 message complete
+```
+
+### Parse chunks with uppercase size
+
+<!-- meta={"type": "request"} -->
+```http
+PUT /url HTTP/1.1
+Transfer-Encoding: chunked
+
+A
+0123456789
+0
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="PUT"
+off=3 method complete
+off=4 len=4 span[url]="/url"
+off=9 url complete
+off=14 len=3 span[version]="1.1"
+off=17 version complete
+off=19 len=17 span[header_field]="Transfer-Encoding"
+off=37 header_field complete
+off=38 len=7 span[header_value]="chunked"
+off=47 header_value complete
+off=49 headers complete method=4 v=1/1 flags=208 content_length=0
+off=52 chunk header len=10
+off=52 len=10 span[body]="0123456789"
+off=64 chunk complete
+off=67 chunk header len=0
+off=69 chunk complete
+off=69 message complete
+```
+
+### POST with `Transfer-Encoding: chunked`
+
+<!-- meta={"type": "request"} -->
+```http
+POST /post_chunked_all_your_base HTTP/1.1
+Transfer-Encoding: chunked
+
+1e
+all your base are belong to us
+0
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=4 span[method]="POST"
+off=4 method complete
+off=5 len=27 span[url]="/post_chunked_all_your_base"
+off=33 url complete
+off=38 len=3 span[version]="1.1"
+off=41 version complete
+off=43 len=17 span[header_field]="Transfer-Encoding"
+off=61 header_field complete
+off=62 len=7 span[header_value]="chunked"
+off=71 header_value complete
+off=73 headers complete method=3 v=1/1 flags=208 content_length=0
+off=77 chunk header len=30
+off=77 len=30 span[body]="all your base are belong to us"
+off=109 chunk complete
+off=112 chunk header len=0
+off=114 chunk complete
+off=114 message complete
+```
+
+### Two chunks and triple zero prefixed end chunk
+
+<!-- meta={"type": "request"} -->
+```http
+POST /two_chunks_mult_zero_end HTTP/1.1
+Transfer-Encoding: chunked
+
+5
+hello
+6
+ world
+000
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=4 span[method]="POST"
+off=4 method complete
+off=5 len=25 span[url]="/two_chunks_mult_zero_end"
+off=31 url complete
+off=36 len=3 span[version]="1.1"
+off=39 version complete
+off=41 len=17 span[header_field]="Transfer-Encoding"
+off=59 header_field complete
+off=60 len=7 span[header_value]="chunked"
+off=69 header_value complete
+off=71 headers complete method=3 v=1/1 flags=208 content_length=0
+off=74 chunk header len=5
+off=74 len=5 span[body]="hello"
+off=81 chunk complete
+off=84 chunk header len=6
+off=84 len=6 span[body]=" world"
+off=92 chunk complete
+off=97 chunk header len=0
+off=99 chunk complete
+off=99 message complete
+```
+
+### Trailing headers
+
+<!-- meta={"type": "request"} -->
+```http
+POST /chunked_w_trailing_headers HTTP/1.1
+Transfer-Encoding: chunked
+
+5
+hello
+6
+ world
+0
+Vary: *
+Content-Type: text/plain
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=4 span[method]="POST"
+off=4 method complete
+off=5 len=27 span[url]="/chunked_w_trailing_headers"
+off=33 url complete
+off=38 len=3 span[version]="1.1"
+off=41 version complete
+off=43 len=17 span[header_field]="Transfer-Encoding"
+off=61 header_field complete
+off=62 len=7 span[header_value]="chunked"
+off=71 header_value complete
+off=73 headers complete method=3 v=1/1 flags=208 content_length=0
+off=76 chunk header len=5
+off=76 len=5 span[body]="hello"
+off=83 chunk complete
+off=86 chunk header len=6
+off=86 len=6 span[body]=" world"
+off=94 chunk complete
+off=97 chunk header len=0
+off=97 len=4 span[header_field]="Vary"
+off=102 header_field complete
+off=103 len=1 span[header_value]="*"
+off=106 header_value complete
+off=106 len=12 span[header_field]="Content-Type"
+off=119 header_field complete
+off=120 len=10 span[header_value]="text/plain"
+off=132 header_value complete
+off=134 chunk complete
+off=134 message complete
+```
+
+### Chunk extensions
+
+<!-- meta={"type": "request"} -->
+```http
+POST /chunked_w_unicorns_after_length HTTP/1.1
+Transfer-Encoding: chunked
+
+5;ilovew3;somuchlove=aretheseparametersfor;another=withvalue
+hello
+6;blahblah;blah
+ world
+0
+
+```
+
+```log
+off=0 message begin
+off=0 len=4 span[method]="POST"
+off=4 method complete
+off=5 len=32 span[url]="/chunked_w_unicorns_after_length"
+off=38 url complete
+off=43 len=3 span[version]="1.1"
+off=46 version complete
+off=48 len=17 span[header_field]="Transfer-Encoding"
+off=66 header_field complete
+off=67 len=7 span[header_value]="chunked"
+off=76 header_value complete
+off=78 headers complete method=3 v=1/1 flags=208 content_length=0
+off=80 len=7 span[chunk_extension_name]="ilovew3"
+off=88 chunk_extension_name complete
+off=88 len=10 span[chunk_extension_name]="somuchlove"
+off=99 chunk_extension_name complete
+off=99 len=21 span[chunk_extension_value]="aretheseparametersfor"
+off=121 chunk_extension_value complete
+off=121 len=7 span[chunk_extension_name]="another"
+off=129 chunk_extension_name complete
+off=129 len=9 span[chunk_extension_value]="withvalue"
+off=139 chunk_extension_value complete
+off=140 chunk header len=5
+off=140 len=5 span[body]="hello"
+off=147 chunk complete
+off=149 len=8 span[chunk_extension_name]="blahblah"
+off=158 chunk_extension_name complete
+off=158 len=4 span[chunk_extension_name]="blah"
+off=163 chunk_extension_name complete
+off=164 chunk header len=6
+off=164 len=6 span[body]=" world"
+off=172 chunk complete
+off=175 chunk header len=0
+```
+
+### No semicolon before chunk extensions
+
+<!-- meta={"type": "request"} -->
+```http
+POST /chunked_w_unicorns_after_length HTTP/1.1
+Host: localhost
+Transfer-encoding: chunked
+
+2 erfrferferf
+aa
+0 rrrr
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=4 span[method]="POST"
+off=4 method complete
+off=5 len=32 span[url]="/chunked_w_unicorns_after_length"
+off=38 url complete
+off=43 len=3 span[version]="1.1"
+off=46 version complete
+off=48 len=4 span[header_field]="Host"
+off=53 header_field complete
+off=54 len=9 span[header_value]="localhost"
+off=65 header_value complete
+off=65 len=17 span[header_field]="Transfer-encoding"
+off=83 header_field complete
+off=84 len=7 span[header_value]="chunked"
+off=93 header_value complete
+off=95 headers complete method=3 v=1/1 flags=208 content_length=0
+off=97 error code=12 reason="Invalid character in chunk size"
+```
+
+### No extension after semicolon
+
+<!-- meta={"type": "request"} -->
+```http
+POST /chunked_w_unicorns_after_length HTTP/1.1
+Host: localhost
+Transfer-encoding: chunked
+
+2;
+aa
+0
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=4 span[method]="POST"
+off=4 method complete
+off=5 len=32 span[url]="/chunked_w_unicorns_after_length"
+off=38 url complete
+off=43 len=3 span[version]="1.1"
+off=46 version complete
+off=48 len=4 span[header_field]="Host"
+off=53 header_field complete
+off=54 len=9 span[header_value]="localhost"
+off=65 header_value complete
+off=65 len=17 span[header_field]="Transfer-encoding"
+off=83 header_field complete
+off=84 len=7 span[header_value]="chunked"
+off=93 header_value complete
+off=95 headers complete method=3 v=1/1 flags=208 content_length=0
+off=98 error code=2 reason="Invalid character in chunk extensions"
+```
+
+
+### Chunk extensions quoting
+
+<!-- meta={"type": "request"} -->
+```http
+POST /chunked_w_unicorns_after_length HTTP/1.1
+Transfer-Encoding: chunked
+
+5;ilovew3="I \"love\"; \\extensions\\";somuchlove="aretheseparametersfor";blah;foo=bar
+hello
+6;blahblah;blah
+ world
+0
+
+```
+
+```log
+off=0 message begin
+off=0 len=4 span[method]="POST"
+off=4 method complete
+off=5 len=32 span[url]="/chunked_w_unicorns_after_length"
+off=38 url complete
+off=43 len=3 span[version]="1.1"
+off=46 version complete
+off=48 len=17 span[header_field]="Transfer-Encoding"
+off=66 header_field complete
+off=67 len=7 span[header_value]="chunked"
+off=76 header_value complete
+off=78 headers complete method=3 v=1/1 flags=208 content_length=0
+off=80 len=7 span[chunk_extension_name]="ilovew3"
+off=88 chunk_extension_name complete
+off=88 len=28 span[chunk_extension_value]=""I \"love\"; \\extensions\\""
+off=116 chunk_extension_value complete
+off=117 len=10 span[chunk_extension_name]="somuchlove"
+off=128 chunk_extension_name complete
+off=128 len=23 span[chunk_extension_value]=""aretheseparametersfor""
+off=151 chunk_extension_value complete
+off=152 len=4 span[chunk_extension_name]="blah"
+off=157 chunk_extension_name complete
+off=157 len=3 span[chunk_extension_name]="foo"
+off=161 chunk_extension_name complete
+off=161 len=3 span[chunk_extension_value]="bar"
+off=165 chunk_extension_value complete
+off=166 chunk header len=5
+off=166 len=5 span[body]="hello"
+off=173 chunk complete
+off=175 len=8 span[chunk_extension_name]="blahblah"
+off=184 chunk_extension_name complete
+off=184 len=4 span[chunk_extension_name]="blah"
+off=189 chunk_extension_name complete
+off=190 chunk header len=6
+off=190 len=6 span[body]=" world"
+off=198 chunk complete
+off=201 chunk header len=0
+```
+
+
+### Unbalanced chunk extensions quoting
+
+<!-- meta={"type": "request"} -->
+```http
+POST /chunked_w_unicorns_after_length HTTP/1.1
+Transfer-Encoding: chunked
+
+5;ilovew3="abc";somuchlove="def; ghi
+hello
+6;blahblah;blah
+ world
+0
+
+```
+
+```log
+off=0 message begin
+off=0 len=4 span[method]="POST"
+off=4 method complete
+off=5 len=32 span[url]="/chunked_w_unicorns_after_length"
+off=38 url complete
+off=43 len=3 span[version]="1.1"
+off=46 version complete
+off=48 len=17 span[header_field]="Transfer-Encoding"
+off=66 header_field complete
+off=67 len=7 span[header_value]="chunked"
+off=76 header_value complete
+off=78 headers complete method=3 v=1/1 flags=208 content_length=0
+off=80 len=7 span[chunk_extension_name]="ilovew3"
+off=88 chunk_extension_name complete
+off=88 len=5 span[chunk_extension_value]=""abc""
+off=93 chunk_extension_value complete
+off=94 len=10 span[chunk_extension_name]="somuchlove"
+off=105 chunk_extension_name complete
+off=105 len=9 span[chunk_extension_value]=""def; ghi"
+off=115 error code=2 reason="Invalid character in chunk extensions quoted value"
+```
+
+## Ignoring `pigeons`
+
+Requests cannot have invalid `Transfer-Encoding`. It is impossible to determine
+their body size. Not erroring would make HTTP smuggling attacks possible.
+
+<!-- meta={"type": "request", "noScan": true} -->
+```http
+PUT /url HTTP/1.1
+Transfer-Encoding: pigeons
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="PUT"
+off=3 method complete
+off=4 len=4 span[url]="/url"
+off=9 url complete
+off=14 len=3 span[version]="1.1"
+off=17 version complete
+off=19 len=17 span[header_field]="Transfer-Encoding"
+off=37 header_field complete
+off=38 len=7 span[header_value]="pigeons"
+off=47 header_value complete
+off=49 headers complete method=4 v=1/1 flags=200 content_length=0
+off=49 error code=15 reason="Request has invalid `Transfer-Encoding`"
+```
+
+## POST with `Transfer-Encoding` and `Content-Length`
+
+<!-- meta={"type": "request"} -->
+```http
+POST /post_identity_body_world?q=search#hey HTTP/1.1
+Accept: */*
+Transfer-Encoding: identity
+Content-Length: 5
+
+World
+```
+
+```log
+off=0 message begin
+off=0 len=4 span[method]="POST"
+off=4 method complete
+off=5 len=38 span[url]="/post_identity_body_world?q=search#hey"
+off=44 url complete
+off=49 len=3 span[version]="1.1"
+off=52 version complete
+off=54 len=6 span[header_field]="Accept"
+off=61 header_field complete
+off=62 len=3 span[header_value]="*/*"
+off=67 header_value complete
+off=67 len=17 span[header_field]="Transfer-Encoding"
+off=85 header_field complete
+off=86 len=8 span[header_value]="identity"
+off=96 header_value complete
+off=96 len=14 span[header_field]="Content-Length"
+off=111 header_field complete
+off=111 error code=11 reason="Content-Length can't be present with Transfer-Encoding"
+```
+
+## POST with `Transfer-Encoding` and `Content-Length` (lenient)
+
+TODO(indutny): should we allow it even in lenient mode? (Consider disabling
+this).
+
+NOTE: `Content-Length` is ignored when `Transfer-Encoding` is present. Messages
+(in lenient mode) are read until EOF.
+
+<!-- meta={"type": "request-lenient-chunked-length"} -->
+```http
+POST /post_identity_body_world?q=search#hey HTTP/1.1
+Accept: */*
+Transfer-Encoding: identity
+Content-Length: 1
+
+World
+```
+
+```log
+off=0 message begin
+off=0 len=4 span[method]="POST"
+off=4 method complete
+off=5 len=38 span[url]="/post_identity_body_world?q=search#hey"
+off=44 url complete
+off=49 len=3 span[version]="1.1"
+off=52 version complete
+off=54 len=6 span[header_field]="Accept"
+off=61 header_field complete
+off=62 len=3 span[header_value]="*/*"
+off=67 header_value complete
+off=67 len=17 span[header_field]="Transfer-Encoding"
+off=85 header_field complete
+off=86 len=8 span[header_value]="identity"
+off=96 header_value complete
+off=96 len=14 span[header_field]="Content-Length"
+off=111 header_field complete
+off=112 len=1 span[header_value]="1"
+off=115 header_value complete
+off=117 headers complete method=3 v=1/1 flags=220 content_length=1
+off=117 len=5 span[body]="World"
+```
+
+## POST with empty `Transfer-Encoding` and `Content-Length` (lenient)
+
+<!-- meta={"type": "request"} -->
+```http
+POST / HTTP/1.1
+Host: foo
+Content-Length: 10
+Transfer-Encoding:
+Transfer-Encoding:
+Transfer-Encoding:
+
+2
+AA
+0
+```
+
+```log
+off=0 message begin
+off=0 len=4 span[method]="POST"
+off=4 method complete
+off=5 len=1 span[url]="/"
+off=7 url complete
+off=12 len=3 span[version]="1.1"
+off=15 version complete
+off=17 len=4 span[header_field]="Host"
+off=22 header_field complete
+off=23 len=3 span[header_value]="foo"
+off=28 header_value complete
+off=28 len=14 span[header_field]="Content-Length"
+off=43 header_field complete
+off=44 len=2 span[header_value]="10"
+off=48 header_value complete
+off=48 len=17 span[header_field]="Transfer-Encoding"
+off=66 header_field complete
+off=66 error code=15 reason="Transfer-Encoding can't be present with Content-Length"
+```
+
+## POST with `chunked` before other transfer coding names
+
+<!-- meta={"type": "request", "noScan": true} -->
+```http
+POST /post_identity_body_world?q=search#hey HTTP/1.1
+Accept: */*
+Transfer-Encoding: chunked, deflate
+
+World
+```
+
+```log
+off=0 message begin
+off=0 len=4 span[method]="POST"
+off=4 method complete
+off=5 len=38 span[url]="/post_identity_body_world?q=search#hey"
+off=44 url complete
+off=49 len=3 span[version]="1.1"
+off=52 version complete
+off=54 len=6 span[header_field]="Accept"
+off=61 header_field complete
+off=62 len=3 span[header_value]="*/*"
+off=67 header_value complete
+off=67 len=17 span[header_field]="Transfer-Encoding"
+off=85 header_field complete
+off=86 len=7 span[header_value]="chunked"
+off=94 error code=15 reason="Invalid `Transfer-Encoding` header value"
+```
+
+## POST with `chunked` and duplicate transfer-encoding
+
+<!-- meta={"type": "request", "noScan": true} -->
+```http
+POST /post_identity_body_world?q=search#hey HTTP/1.1
+Accept: */*
+Transfer-Encoding: chunked
+Transfer-Encoding: deflate
+
+World
+```
+
+```log
+off=0 message begin
+off=0 len=4 span[method]="POST"
+off=4 method complete
+off=5 len=38 span[url]="/post_identity_body_world?q=search#hey"
+off=44 url complete
+off=49 len=3 span[version]="1.1"
+off=52 version complete
+off=54 len=6 span[header_field]="Accept"
+off=61 header_field complete
+off=62 len=3 span[header_value]="*/*"
+off=67 header_value complete
+off=67 len=17 span[header_field]="Transfer-Encoding"
+off=85 header_field complete
+off=86 len=7 span[header_value]="chunked"
+off=95 header_value complete
+off=95 len=17 span[header_field]="Transfer-Encoding"
+off=113 header_field complete
+off=114 len=0 span[header_value]=""
+off=115 error code=15 reason="Invalid `Transfer-Encoding` header value"
+```
+
+## POST with `chunked` before other transfer-coding (lenient)
+
+<!-- meta={"type": "request-lenient-transfer-encoding"} -->
+```http
+POST /post_identity_body_world?q=search#hey HTTP/1.1
+Accept: */*
+Transfer-Encoding: chunked, deflate
+
+World
+```
+
+```log
+off=0 message begin
+off=0 len=4 span[method]="POST"
+off=4 method complete
+off=5 len=38 span[url]="/post_identity_body_world?q=search#hey"
+off=44 url complete
+off=49 len=3 span[version]="1.1"
+off=52 version complete
+off=54 len=6 span[header_field]="Accept"
+off=61 header_field complete
+off=62 len=3 span[header_value]="*/*"
+off=67 header_value complete
+off=67 len=17 span[header_field]="Transfer-Encoding"
+off=85 header_field complete
+off=86 len=16 span[header_value]="chunked, deflate"
+off=104 header_value complete
+off=106 headers complete method=3 v=1/1 flags=200 content_length=0
+off=106 len=5 span[body]="World"
+```
+
+## POST with `chunked` and duplicate transfer-encoding (lenient)
+
+<!-- meta={"type": "request-lenient-transfer-encoding"} -->
+```http
+POST /post_identity_body_world?q=search#hey HTTP/1.1
+Accept: */*
+Transfer-Encoding: chunked
+Transfer-Encoding: deflate
+
+World
+```
+
+```log
+off=0 message begin
+off=0 len=4 span[method]="POST"
+off=4 method complete
+off=5 len=38 span[url]="/post_identity_body_world?q=search#hey"
+off=44 url complete
+off=49 len=3 span[version]="1.1"
+off=52 version complete
+off=54 len=6 span[header_field]="Accept"
+off=61 header_field complete
+off=62 len=3 span[header_value]="*/*"
+off=67 header_value complete
+off=67 len=17 span[header_field]="Transfer-Encoding"
+off=85 header_field complete
+off=86 len=7 span[header_value]="chunked"
+off=95 header_value complete
+off=95 len=17 span[header_field]="Transfer-Encoding"
+off=113 header_field complete
+off=114 len=7 span[header_value]="deflate"
+off=123 header_value complete
+off=125 headers complete method=3 v=1/1 flags=200 content_length=0
+off=125 len=5 span[body]="World"
+```
+
+## POST with `chunked` as last transfer-encoding
+
+<!-- meta={"type": "request"} -->
+```http
+POST /post_identity_body_world?q=search#hey HTTP/1.1
+Accept: */*
+Transfer-Encoding: deflate, chunked
+
+5
+World
+0
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=4 span[method]="POST"
+off=4 method complete
+off=5 len=38 span[url]="/post_identity_body_world?q=search#hey"
+off=44 url complete
+off=49 len=3 span[version]="1.1"
+off=52 version complete
+off=54 len=6 span[header_field]="Accept"
+off=61 header_field complete
+off=62 len=3 span[header_value]="*/*"
+off=67 header_value complete
+off=67 len=17 span[header_field]="Transfer-Encoding"
+off=85 header_field complete
+off=86 len=16 span[header_value]="deflate, chunked"
+off=104 header_value complete
+off=106 headers complete method=3 v=1/1 flags=208 content_length=0
+off=109 chunk header len=5
+off=109 len=5 span[body]="World"
+off=116 chunk complete
+off=119 chunk header len=0
+off=121 chunk complete
+off=121 message complete
+```
+
+## POST with `chunked` as last transfer-encoding (multiple headers)
+
+<!-- meta={"type": "request"} -->
+```http
+POST /post_identity_body_world?q=search#hey HTTP/1.1
+Accept: */*
+Transfer-Encoding: deflate
+Transfer-Encoding: chunked
+
+5
+World
+0
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=4 span[method]="POST"
+off=4 method complete
+off=5 len=38 span[url]="/post_identity_body_world?q=search#hey"
+off=44 url complete
+off=49 len=3 span[version]="1.1"
+off=52 version complete
+off=54 len=6 span[header_field]="Accept"
+off=61 header_field complete
+off=62 len=3 span[header_value]="*/*"
+off=67 header_value complete
+off=67 len=17 span[header_field]="Transfer-Encoding"
+off=85 header_field complete
+off=86 len=7 span[header_value]="deflate"
+off=95 header_value complete
+off=95 len=17 span[header_field]="Transfer-Encoding"
+off=113 header_field complete
+off=114 len=7 span[header_value]="chunked"
+off=123 header_value complete
+off=125 headers complete method=3 v=1/1 flags=208 content_length=0
+off=128 chunk header len=5
+off=128 len=5 span[body]="World"
+off=135 chunk complete
+off=138 chunk header len=0
+off=140 chunk complete
+off=140 message complete
+```
+
+## POST with `chunkedchunked` as transfer-encoding
+
+<!-- meta={"type": "request"} -->
+```http
+POST /post_identity_body_world?q=search#hey HTTP/1.1
+Accept: */*
+Transfer-Encoding: chunkedchunked
+
+5
+World
+0
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=4 span[method]="POST"
+off=4 method complete
+off=5 len=38 span[url]="/post_identity_body_world?q=search#hey"
+off=44 url complete
+off=49 len=3 span[version]="1.1"
+off=52 version complete
+off=54 len=6 span[header_field]="Accept"
+off=61 header_field complete
+off=62 len=3 span[header_value]="*/*"
+off=67 header_value complete
+off=67 len=17 span[header_field]="Transfer-Encoding"
+off=85 header_field complete
+off=86 len=14 span[header_value]="chunkedchunked"
+off=102 header_value complete
+off=104 headers complete method=3 v=1/1 flags=200 content_length=0
+off=104 error code=15 reason="Request has invalid `Transfer-Encoding`"
+```
+
+## Missing last-chunk
+
+<!-- meta={"type": "request"} -->
+```http
+PUT /url HTTP/1.1
+Transfer-Encoding: chunked
+
+3
+foo
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="PUT"
+off=3 method complete
+off=4 len=4 span[url]="/url"
+off=9 url complete
+off=14 len=3 span[version]="1.1"
+off=17 version complete
+off=19 len=17 span[header_field]="Transfer-Encoding"
+off=37 header_field complete
+off=38 len=7 span[header_value]="chunked"
+off=47 header_value complete
+off=49 headers complete method=4 v=1/1 flags=208 content_length=0
+off=52 chunk header len=3
+off=52 len=3 span[body]="foo"
+off=57 chunk complete
+off=57 error code=12 reason="Invalid character in chunk size"
+```
+
+## Validate chunk parameters
+
+<!-- meta={"type": "request" } -->
+```http
+PUT /url HTTP/1.1
+Transfer-Encoding: chunked
+
+3 \n \r\n\
+foo
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="PUT"
+off=3 method complete
+off=4 len=4 span[url]="/url"
+off=9 url complete
+off=14 len=3 span[version]="1.1"
+off=17 version complete
+off=19 len=17 span[header_field]="Transfer-Encoding"
+off=37 header_field complete
+off=38 len=7 span[header_value]="chunked"
+off=47 header_value complete
+off=49 headers complete method=4 v=1/1 flags=208 content_length=0
+off=51 error code=12 reason="Invalid character in chunk size"
+```
+
+## Invalid OBS fold after chunked value
+
+<!-- meta={"type": "request" } -->
+```http
+PUT /url HTTP/1.1
+Transfer-Encoding: chunked
+ abc
+
+5
+World
+0
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="PUT"
+off=3 method complete
+off=4 len=4 span[url]="/url"
+off=9 url complete
+off=14 len=3 span[version]="1.1"
+off=17 version complete
+off=19 len=17 span[header_field]="Transfer-Encoding"
+off=37 header_field complete
+off=38 len=7 span[header_value]="chunked"
+off=47 len=5 span[header_value]=" abc"
+off=54 header_value complete
+off=56 headers complete method=4 v=1/1 flags=200 content_length=0
+off=56 error code=15 reason="Request has invalid `Transfer-Encoding`"
+```
+
+### Chunk header not terminated by CRLF
+
+<!-- meta={"type": "request" } -->
+
+```http
+GET / HTTP/1.1
+Host: a
+Connection: close
+Transfer-Encoding: chunked
+
+5\r\r;ABCD
+34
+E
+0
+
+GET / HTTP/1.1
+Host: a
+Content-Length: 5
+
+0
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="GET"
+off=3 method complete
+off=4 len=1 span[url]="/"
+off=6 url complete
+off=11 len=3 span[version]="1.1"
+off=14 version complete
+off=16 len=4 span[header_field]="Host"
+off=21 header_field complete
+off=22 len=1 span[header_value]="a"
+off=25 header_value complete
+off=25 len=10 span[header_field]="Connection"
+off=36 header_field complete
+off=37 len=6 span[header_value]="close "
+off=45 header_value complete
+off=45 len=17 span[header_field]="Transfer-Encoding"
+off=63 header_field complete
+off=64 len=8 span[header_value]="chunked "
+off=74 header_value complete
+off=76 headers complete method=1 v=1/1 flags=20a content_length=0
+off=78 error code=2 reason="Expected LF after chunk size"
+```
+
+### Chunk header not terminated by CRLF (lenient)
+
+<!-- meta={"type": "request-lenient-optional-lf-after-cr" } -->
+
+```http
+GET / HTTP/1.1
+Host: a
+Connection: close
+Transfer-Encoding: chunked
+
+6\r\r;ABCD
+33
+E
+0
+
+GET / HTTP/1.1
+Host: a
+Content-Length: 5
+0
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="GET"
+off=3 method complete
+off=4 len=1 span[url]="/"
+off=6 url complete
+off=11 len=3 span[version]="1.1"
+off=14 version complete
+off=16 len=4 span[header_field]="Host"
+off=21 header_field complete
+off=22 len=1 span[header_value]="a"
+off=25 header_value complete
+off=25 len=10 span[header_field]="Connection"
+off=36 header_field complete
+off=37 len=6 span[header_value]="close "
+off=45 header_value complete
+off=45 len=17 span[header_field]="Transfer-Encoding"
+off=63 header_field complete
+off=64 len=8 span[header_value]="chunked "
+off=74 header_value complete
+off=76 headers complete method=1 v=1/1 flags=20a content_length=0
+off=78 chunk header len=6
+off=78 len=1 span[body]=cr
+off=79 len=5 span[body]=";ABCD"
+off=86 chunk complete
+off=90 chunk header len=51
+off=90 len=1 span[body]="E"
+off=91 len=1 span[body]=cr
+off=92 len=1 span[body]=lf
+off=93 len=1 span[body]="0"
+off=94 len=1 span[body]=cr
+off=95 len=1 span[body]=lf
+off=96 len=1 span[body]=cr
+off=97 len=1 span[body]=lf
+off=98 len=15 span[body]="GET / HTTP/1.1 "
+off=113 len=1 span[body]=cr
+off=114 len=1 span[body]=lf
+off=115 len=7 span[body]="Host: a"
+off=122 len=1 span[body]=cr
+off=123 len=1 span[body]=lf
+off=124 len=17 span[body]="Content-Length: 5"
+off=143 chunk complete
+off=146 chunk header len=0
+off=148 chunk complete
+off=148 message complete
+```
+
+### Chunk data not terminated by CRLF
+
+<!-- meta={"type": "request" } -->
+
+```http
+GET / HTTP/1.1
+Host: a
+Connection: close
+Transfer-Encoding: chunked
+
+5
+ABCDE0
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="GET"
+off=3 method complete
+off=4 len=1 span[url]="/"
+off=6 url complete
+off=11 len=3 span[version]="1.1"
+off=14 version complete
+off=16 len=4 span[header_field]="Host"
+off=21 header_field complete
+off=22 len=1 span[header_value]="a"
+off=25 header_value complete
+off=25 len=10 span[header_field]="Connection"
+off=36 header_field complete
+off=37 len=6 span[header_value]="close "
+off=45 header_value complete
+off=45 len=17 span[header_field]="Transfer-Encoding"
+off=63 header_field complete
+off=64 len=8 span[header_value]="chunked "
+off=74 header_value complete
+off=76 headers complete method=1 v=1/1 flags=20a content_length=0
+off=79 chunk header len=5
+off=79 len=5 span[body]="ABCDE"
+off=84 error code=2 reason="Expected LF after chunk data"
+```
+
+### Chunk data not terminated by CRLF (lenient)
+
+<!-- meta={"type": "request-lenient-optional-crlf-after-chunk" } -->
+
+```http
+GET / HTTP/1.1
+Host: a
+Connection: close
+Transfer-Encoding: chunked
+
+5
+ABCDE0
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="GET"
+off=3 method complete
+off=4 len=1 span[url]="/"
+off=6 url complete
+off=11 len=3 span[version]="1.1"
+off=14 version complete
+off=16 len=4 span[header_field]="Host"
+off=21 header_field complete
+off=22 len=1 span[header_value]="a"
+off=25 header_value complete
+off=25 len=10 span[header_field]="Connection"
+off=36 header_field complete
+off=37 len=6 span[header_value]="close "
+off=45 header_value complete
+off=45 len=17 span[header_field]="Transfer-Encoding"
+off=63 header_field complete
+off=64 len=8 span[header_value]="chunked "
+off=74 header_value complete
+off=76 headers complete method=1 v=1/1 flags=20a content_length=0
+off=79 chunk header len=5
+off=79 len=5 span[body]="ABCDE"
+off=84 chunk complete
+off=87 chunk header len=0
+```
+
+## Space after chunk header
+
+<!-- meta={"type": "request"} -->
+```http
+PUT /url HTTP/1.1
+Transfer-Encoding: chunked
+
+a \r\n0123456789
+0
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="PUT"
+off=3 method complete
+off=4 len=4 span[url]="/url"
+off=9 url complete
+off=14 len=3 span[version]="1.1"
+off=17 version complete
+off=19 len=17 span[header_field]="Transfer-Encoding"
+off=37 header_field complete
+off=38 len=7 span[header_value]="chunked"
+off=47 header_value complete
+off=49 headers complete method=4 v=1/1 flags=208 content_length=0
+off=51 error code=12 reason="Invalid character in chunk size"
+```
+
+## Space after chunk header (lenient)
+
+<!-- meta={"type": "request-lenient-spaces-after-chunk-size"} -->
+```http
+PUT /url HTTP/1.1
+Transfer-Encoding: chunked
+
+a \r\n0123456789
+0
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="PUT"
+off=3 method complete
+off=4 len=4 span[url]="/url"
+off=9 url complete
+off=14 len=3 span[version]="1.1"
+off=17 version complete
+off=19 len=17 span[header_field]="Transfer-Encoding"
+off=37 header_field complete
+off=38 len=7 span[header_value]="chunked"
+off=47 header_value complete
+off=49 headers complete method=4 v=1/1 flags=208 content_length=0
+off=53 chunk header len=10
+off=53 len=10 span[body]="0123456789"
+off=65 chunk complete
+off=68 chunk header len=0
+off=70 chunk complete
+off=70 message complete
+```
diff --git a/llhttp/test/request/uri.md b/llhttp/test/request/uri.md
new file mode 100644
index 0000000..f7f12b0
--- /dev/null
+++ b/llhttp/test/request/uri.md
@@ -0,0 +1,243 @@
+URI
+===
+
+## Quotes in URI
+
+<!-- meta={"type": "request"} -->
+```http
+GET /with_"lovely"_quotes?foo=\"bar\" HTTP/1.1
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="GET"
+off=3 method complete
+off=4 len=33 span[url]="/with_"lovely"_quotes?foo=\"bar\""
+off=38 url complete
+off=43 len=3 span[version]="1.1"
+off=46 version complete
+off=50 headers complete method=1 v=1/1 flags=0 content_length=0
+off=50 message complete
+```
+
+## Query URL with question mark
+
+Some clients include `?` characters in query strings.
+
+<!-- meta={"type": "request"} -->
+```http
+GET /test.cgi?foo=bar?baz HTTP/1.1
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="GET"
+off=3 method complete
+off=4 len=21 span[url]="/test.cgi?foo=bar?baz"
+off=26 url complete
+off=31 len=3 span[version]="1.1"
+off=34 version complete
+off=38 headers complete method=1 v=1/1 flags=0 content_length=0
+off=38 message complete
+```
+
+## Host terminated by a query string
+
+<!-- meta={"type": "request"} -->
+```http
+GET http://hypnotoad.org?hail=all HTTP/1.1\r\n
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="GET"
+off=3 method complete
+off=4 len=29 span[url]="http://hypnotoad.org?hail=all"
+off=34 url complete
+off=39 len=3 span[version]="1.1"
+off=42 version complete
+off=46 headers complete method=1 v=1/1 flags=0 content_length=0
+off=46 message complete
+```
+
+## `host:port` terminated by a query string
+
+<!-- meta={"type": "request"} -->
+```http
+GET http://hypnotoad.org:1234?hail=all HTTP/1.1
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="GET"
+off=3 method complete
+off=4 len=34 span[url]="http://hypnotoad.org:1234?hail=all"
+off=39 url complete
+off=44 len=3 span[version]="1.1"
+off=47 version complete
+off=51 headers complete method=1 v=1/1 flags=0 content_length=0
+off=51 message complete
+```
+
+## Query URL with vertical bar character
+
+It should be allowed to have vertical bar symbol in URI: `|`.
+
+See: https://github.com/nodejs/node/issues/27584
+
+<!-- meta={"type": "request"} -->
+```http
+GET /test.cgi?query=| HTTP/1.1
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="GET"
+off=3 method complete
+off=4 len=17 span[url]="/test.cgi?query=|"
+off=22 url complete
+off=27 len=3 span[version]="1.1"
+off=30 version complete
+off=34 headers complete method=1 v=1/1 flags=0 content_length=0
+off=34 message complete
+```
+
+## `host:port` terminated by a space
+
+<!-- meta={"type": "request"} -->
+```http
+GET http://hypnotoad.org:1234 HTTP/1.1
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="GET"
+off=3 method complete
+off=4 len=25 span[url]="http://hypnotoad.org:1234"
+off=30 url complete
+off=35 len=3 span[version]="1.1"
+off=38 version complete
+off=42 headers complete method=1 v=1/1 flags=0 content_length=0
+off=42 message complete
+```
+
+## Disallow UTF-8 in URI path in strict mode
+
+<!-- meta={"type": "request", "noScan": true} -->
+```http
+GET /δ¶/δt/pope?q=1#narf HTTP/1.1
+Host: github.com
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="GET"
+off=3 method complete
+off=5 error code=7 reason="Invalid char in url path"
+```
+
+## Fragment in URI
+
+<!-- meta={"type": "request"} -->
+```http
+GET /forums/1/topics/2375?page=1#posts-17408 HTTP/1.1
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="GET"
+off=3 method complete
+off=4 len=40 span[url]="/forums/1/topics/2375?page=1#posts-17408"
+off=45 url complete
+off=50 len=3 span[version]="1.1"
+off=53 version complete
+off=57 headers complete method=1 v=1/1 flags=0 content_length=0
+off=57 message complete
+```
+
+## Underscore in hostname
+
+<!-- meta={"type": "request"} -->
+```http
+CONNECT home_0.netscape.com:443 HTTP/1.0
+User-agent: Mozilla/1.1N
+Proxy-authorization: basic aGVsbG86d29ybGQ=
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=7 span[method]="CONNECT"
+off=7 method complete
+off=8 len=23 span[url]="home_0.netscape.com:443"
+off=32 url complete
+off=37 len=3 span[version]="1.0"
+off=40 version complete
+off=42 len=10 span[header_field]="User-agent"
+off=53 header_field complete
+off=54 len=12 span[header_value]="Mozilla/1.1N"
+off=68 header_value complete
+off=68 len=19 span[header_field]="Proxy-authorization"
+off=88 header_field complete
+off=89 len=22 span[header_value]="basic aGVsbG86d29ybGQ="
+off=113 header_value complete
+off=115 headers complete method=5 v=1/0 flags=0 content_length=0
+off=115 message complete
+off=115 error code=22 reason="Pause on CONNECT/Upgrade"
+```
+
+## `host:port` and basic auth
+
+<!-- meta={"type": "request"} -->
+```http
+GET http://a%12:b!&*$@hypnotoad.org:1234/toto HTTP/1.1
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="GET"
+off=3 method complete
+off=4 len=41 span[url]="http://a%12:b!&*$@hypnotoad.org:1234/toto"
+off=46 url complete
+off=51 len=3 span[version]="1.1"
+off=54 version complete
+off=58 headers complete method=1 v=1/1 flags=0 content_length=0
+off=58 message complete
+```
+
+## Space in URI
+
+<!-- meta={"type": "request", "noScan": true} -->
+```http
+GET /foo bar/ HTTP/1.1
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="GET"
+off=3 method complete
+off=4 len=4 span[url]="/foo"
+off=9 url complete
+off=9 error code=8 reason="Expected HTTP/"
+```
diff --git a/llhttp/test/response/connection.md b/llhttp/test/response/connection.md
new file mode 100644
index 0000000..11f9eb6
--- /dev/null
+++ b/llhttp/test/response/connection.md
@@ -0,0 +1,647 @@
+Connection header
+=================
+
+## Proxy-Connection
+
+<!-- meta={"type": "response"} -->
+```http
+HTTP/1.1 200 OK
+Content-Type: text/html; charset=UTF-8
+Content-Length: 11
+Proxy-Connection: close
+Date: Thu, 31 Dec 2009 20:55:48 +0000
+
+hello world
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=2 span[status]="OK"
+off=17 status complete
+off=17 len=12 span[header_field]="Content-Type"
+off=30 header_field complete
+off=31 len=24 span[header_value]="text/html; charset=UTF-8"
+off=57 header_value complete
+off=57 len=14 span[header_field]="Content-Length"
+off=72 header_field complete
+off=73 len=2 span[header_value]="11"
+off=77 header_value complete
+off=77 len=16 span[header_field]="Proxy-Connection"
+off=94 header_field complete
+off=95 len=5 span[header_value]="close"
+off=102 header_value complete
+off=102 len=4 span[header_field]="Date"
+off=107 header_field complete
+off=108 len=31 span[header_value]="Thu, 31 Dec 2009 20:55:48 +0000"
+off=141 header_value complete
+off=143 headers complete status=200 v=1/1 flags=22 content_length=11
+off=143 len=11 span[body]="hello world"
+off=154 message complete
+```
+
+## HTTP/1.0 with keep-alive and EOF-terminated 200 status
+
+There is no `Content-Length` in this response, so even though the
+`keep-alive` is on - it should read until EOF.
+
+<!-- meta={"type": "response"} -->
+```http
+HTTP/1.0 200 OK
+Connection: keep-alive
+
+HTTP/1.0 200 OK
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.0"
+off=8 version complete
+off=13 len=2 span[status]="OK"
+off=17 status complete
+off=17 len=10 span[header_field]="Connection"
+off=28 header_field complete
+off=29 len=10 span[header_value]="keep-alive"
+off=41 header_value complete
+off=43 headers complete status=200 v=1/0 flags=1 content_length=0
+off=43 len=15 span[body]="HTTP/1.0 200 OK"
+```
+
+## HTTP/1.0 with keep-alive and 204 status
+
+Responses with `204` status cannot have a body.
+
+<!-- meta={"type": "response"} -->
+```http
+HTTP/1.0 204 No content
+Connection: keep-alive
+
+HTTP/1.0 200 OK
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.0"
+off=8 version complete
+off=13 len=10 span[status]="No content"
+off=25 status complete
+off=25 len=10 span[header_field]="Connection"
+off=36 header_field complete
+off=37 len=10 span[header_value]="keep-alive"
+off=49 header_value complete
+off=51 headers complete status=204 v=1/0 flags=1 content_length=0
+off=51 message complete
+off=51 reset
+off=51 message begin
+off=56 len=3 span[version]="1.0"
+off=59 version complete
+off=64 len=2 span[status]="OK"
+```
+
+## HTTP/1.1 with EOF-terminated 200 status
+
+There is no `Content-Length` in this response, so even though the
+`keep-alive` is on (implicitly in HTTP 1.1) - it should read until EOF.
+
+<!-- meta={"type": "response"} -->
+```http
+HTTP/1.1 200 OK
+
+HTTP/1.1 200 OK
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=2 span[status]="OK"
+off=17 status complete
+off=19 headers complete status=200 v=1/1 flags=0 content_length=0
+off=19 len=15 span[body]="HTTP/1.1 200 OK"
+```
+
+## HTTP/1.1 with 204 status
+
+Responses with `204` status cannot have a body.
+
+<!-- meta={"type": "response"} -->
+```http
+HTTP/1.1 204 No content
+
+HTTP/1.1 200 OK
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=10 span[status]="No content"
+off=25 status complete
+off=27 headers complete status=204 v=1/1 flags=0 content_length=0
+off=27 message complete
+off=27 reset
+off=27 message begin
+off=32 len=3 span[version]="1.1"
+off=35 version complete
+off=40 len=2 span[status]="OK"
+```
+
+## HTTP/1.1 with keep-alive disabled and 204 status
+
+<!-- meta={"type": "response" } -->
+```http
+HTTP/1.1 204 No content
+Connection: close
+
+HTTP/1.1 200 OK
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=10 span[status]="No content"
+off=25 status complete
+off=25 len=10 span[header_field]="Connection"
+off=36 header_field complete
+off=37 len=5 span[header_value]="close"
+off=44 header_value complete
+off=46 headers complete status=204 v=1/1 flags=2 content_length=0
+off=46 message complete
+off=47 error code=5 reason="Data after `Connection: close`"
+```
+
+## HTTP/1.1 with keep-alive disabled, content-length (lenient)
+
+Parser should discard extra request in lenient mode.
+
+<!-- meta={"type": "response-lenient-data-after-close" } -->
+```http
+HTTP/1.1 200 No content
+Content-Length: 5
+Connection: close
+
+2ad731e3-4dcd-4f70-b871-0ad284b29ffc
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=10 span[status]="No content"
+off=25 status complete
+off=25 len=14 span[header_field]="Content-Length"
+off=40 header_field complete
+off=41 len=1 span[header_value]="5"
+off=44 header_value complete
+off=44 len=10 span[header_field]="Connection"
+off=55 header_field complete
+off=56 len=5 span[header_value]="close"
+off=63 header_value complete
+off=65 headers complete status=200 v=1/1 flags=22 content_length=5
+off=65 len=5 span[body]="2ad73"
+off=70 message complete
+```
+
+## HTTP/1.1 with keep-alive disabled, content-length
+
+Parser should discard extra request in strict mode.
+
+<!-- meta={"type": "response" } -->
+```http
+HTTP/1.1 200 No content
+Content-Length: 5
+Connection: close
+
+2ad731e3-4dcd-4f70-b871-0ad284b29ffc
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=10 span[status]="No content"
+off=25 status complete
+off=25 len=14 span[header_field]="Content-Length"
+off=40 header_field complete
+off=41 len=1 span[header_value]="5"
+off=44 header_value complete
+off=44 len=10 span[header_field]="Connection"
+off=55 header_field complete
+off=56 len=5 span[header_value]="close"
+off=63 header_value complete
+off=65 headers complete status=200 v=1/1 flags=22 content_length=5
+off=65 len=5 span[body]="2ad73"
+off=70 message complete
+off=71 error code=5 reason="Data after `Connection: close`"
+```
+
+## HTTP/1.1 with keep-alive disabled and 204 status (lenient)
+
+<!-- meta={"type": "response-lenient-keep-alive"} -->
+```http
+HTTP/1.1 204 No content
+Connection: close
+
+HTTP/1.1 200 OK
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=10 span[status]="No content"
+off=25 status complete
+off=25 len=10 span[header_field]="Connection"
+off=36 header_field complete
+off=37 len=5 span[header_value]="close"
+off=44 header_value complete
+off=46 headers complete status=204 v=1/1 flags=2 content_length=0
+off=46 message complete
+off=46 reset
+off=46 message begin
+off=51 len=3 span[version]="1.1"
+off=54 version complete
+off=59 len=2 span[status]="OK"
+```
+
+## HTTP 101 response with Upgrade and Content-Length header
+
+<!-- meta={"type": "response"} -->
+```http
+HTTP/1.1 101 Switching Protocols
+Connection: upgrade
+Upgrade: h2c
+Content-Length: 4
+
+body\
+proto
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=19 span[status]="Switching Protocols"
+off=34 status complete
+off=34 len=10 span[header_field]="Connection"
+off=45 header_field complete
+off=46 len=7 span[header_value]="upgrade"
+off=55 header_value complete
+off=55 len=7 span[header_field]="Upgrade"
+off=63 header_field complete
+off=64 len=3 span[header_value]="h2c"
+off=69 header_value complete
+off=69 len=14 span[header_field]="Content-Length"
+off=84 header_field complete
+off=85 len=1 span[header_value]="4"
+off=88 header_value complete
+off=90 headers complete status=101 v=1/1 flags=34 content_length=4
+off=90 message complete
+off=90 error code=22 reason="Pause on CONNECT/Upgrade"
+```
+
+## HTTP 101 response with Upgrade and Transfer-Encoding header
+
+<!-- meta={"type": "response"} -->
+```http
+HTTP/1.1 101 Switching Protocols
+Connection: upgrade
+Upgrade: h2c
+Transfer-Encoding: chunked
+
+2
+bo
+2
+dy
+0
+
+proto
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=19 span[status]="Switching Protocols"
+off=34 status complete
+off=34 len=10 span[header_field]="Connection"
+off=45 header_field complete
+off=46 len=7 span[header_value]="upgrade"
+off=55 header_value complete
+off=55 len=7 span[header_field]="Upgrade"
+off=63 header_field complete
+off=64 len=3 span[header_value]="h2c"
+off=69 header_value complete
+off=69 len=17 span[header_field]="Transfer-Encoding"
+off=87 header_field complete
+off=88 len=7 span[header_value]="chunked"
+off=97 header_value complete
+off=99 headers complete status=101 v=1/1 flags=21c content_length=0
+off=99 message complete
+off=99 error code=22 reason="Pause on CONNECT/Upgrade"
+```
+
+## HTTP 200 response with Upgrade header
+
+<!-- meta={"type": "response"} -->
+```http
+HTTP/1.1 200 OK
+Connection: upgrade
+Upgrade: h2c
+
+body
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=2 span[status]="OK"
+off=17 status complete
+off=17 len=10 span[header_field]="Connection"
+off=28 header_field complete
+off=29 len=7 span[header_value]="upgrade"
+off=38 header_value complete
+off=38 len=7 span[header_field]="Upgrade"
+off=46 header_field complete
+off=47 len=3 span[header_value]="h2c"
+off=52 header_value complete
+off=54 headers complete status=200 v=1/1 flags=14 content_length=0
+off=54 len=4 span[body]="body"
+```
+
+## HTTP 200 response with Upgrade header and Content-Length
+
+<!-- meta={"type": "response"} -->
+```http
+HTTP/1.1 200 OK
+Connection: upgrade
+Upgrade: h2c
+Content-Length: 4
+
+body
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=2 span[status]="OK"
+off=17 status complete
+off=17 len=10 span[header_field]="Connection"
+off=28 header_field complete
+off=29 len=7 span[header_value]="upgrade"
+off=38 header_value complete
+off=38 len=7 span[header_field]="Upgrade"
+off=46 header_field complete
+off=47 len=3 span[header_value]="h2c"
+off=52 header_value complete
+off=52 len=14 span[header_field]="Content-Length"
+off=67 header_field complete
+off=68 len=1 span[header_value]="4"
+off=71 header_value complete
+off=73 headers complete status=200 v=1/1 flags=34 content_length=4
+off=73 len=4 span[body]="body"
+off=77 message complete
+```
+
+## HTTP 200 response with Upgrade header and Transfer-Encoding
+
+<!-- meta={"type": "response"} -->
+```http
+HTTP/1.1 200 OK
+Connection: upgrade
+Upgrade: h2c
+Transfer-Encoding: chunked
+
+2
+bo
+2
+dy
+0
+
+
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=2 span[status]="OK"
+off=17 status complete
+off=17 len=10 span[header_field]="Connection"
+off=28 header_field complete
+off=29 len=7 span[header_value]="upgrade"
+off=38 header_value complete
+off=38 len=7 span[header_field]="Upgrade"
+off=46 header_field complete
+off=47 len=3 span[header_value]="h2c"
+off=52 header_value complete
+off=52 len=17 span[header_field]="Transfer-Encoding"
+off=70 header_field complete
+off=71 len=7 span[header_value]="chunked"
+off=80 header_value complete
+off=82 headers complete status=200 v=1/1 flags=21c content_length=0
+off=85 chunk header len=2
+off=85 len=2 span[body]="bo"
+off=89 chunk complete
+off=92 chunk header len=2
+off=92 len=2 span[body]="dy"
+off=96 chunk complete
+off=99 chunk header len=0
+off=101 chunk complete
+off=101 message complete
+```
+
+## HTTP 304 with Content-Length
+
+<!-- meta={"type": "response"} -->
+```http
+HTTP/1.1 304 Not Modified
+Content-Length: 10
+
+
+HTTP/1.1 200 OK
+Content-Length: 5
+
+hello
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=12 span[status]="Not Modified"
+off=27 status complete
+off=27 len=14 span[header_field]="Content-Length"
+off=42 header_field complete
+off=43 len=2 span[header_value]="10"
+off=47 header_value complete
+off=49 headers complete status=304 v=1/1 flags=20 content_length=10
+off=49 message complete
+off=51 reset
+off=51 message begin
+off=56 len=3 span[version]="1.1"
+off=59 version complete
+off=64 len=2 span[status]="OK"
+off=68 status complete
+off=68 len=14 span[header_field]="Content-Length"
+off=83 header_field complete
+off=84 len=1 span[header_value]="5"
+off=87 header_value complete
+off=89 headers complete status=200 v=1/1 flags=20 content_length=5
+off=89 len=5 span[body]="hello"
+off=94 message complete
+```
+
+## HTTP 304 with Transfer-Encoding
+
+<!-- meta={"type": "response"} -->
+```http
+HTTP/1.1 304 Not Modified
+Transfer-Encoding: chunked
+
+HTTP/1.1 200 OK
+Transfer-Encoding: chunked
+
+5
+hello
+0
+
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=12 span[status]="Not Modified"
+off=27 status complete
+off=27 len=17 span[header_field]="Transfer-Encoding"
+off=45 header_field complete
+off=46 len=7 span[header_value]="chunked"
+off=55 header_value complete
+off=57 headers complete status=304 v=1/1 flags=208 content_length=0
+off=57 message complete
+off=57 reset
+off=57 message begin
+off=62 len=3 span[version]="1.1"
+off=65 version complete
+off=70 len=2 span[status]="OK"
+off=74 status complete
+off=74 len=17 span[header_field]="Transfer-Encoding"
+off=92 header_field complete
+off=93 len=7 span[header_value]="chunked"
+off=102 header_value complete
+off=104 headers complete status=200 v=1/1 flags=208 content_length=0
+off=107 chunk header len=5
+off=107 len=5 span[body]="hello"
+off=114 chunk complete
+off=117 chunk header len=0
+```
+
+## HTTP 100 first, then 400
+
+<!-- meta={"type": "response"} -->
+```http
+HTTP/1.1 100 Continue
+
+
+HTTP/1.1 404 Not Found
+Content-Type: text/plain; charset=utf-8
+Content-Length: 14
+Date: Fri, 15 Sep 2023 19:47:23 GMT
+Server: Python/3.10 aiohttp/4.0.0a2.dev0
+
+404: Not Found
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=8 span[status]="Continue"
+off=23 status complete
+off=25 headers complete status=100 v=1/1 flags=0 content_length=0
+off=25 message complete
+off=27 reset
+off=27 message begin
+off=32 len=3 span[version]="1.1"
+off=35 version complete
+off=40 len=9 span[status]="Not Found"
+off=51 status complete
+off=51 len=12 span[header_field]="Content-Type"
+off=64 header_field complete
+off=65 len=25 span[header_value]="text/plain; charset=utf-8"
+off=92 header_value complete
+off=92 len=14 span[header_field]="Content-Length"
+off=107 header_field complete
+off=108 len=2 span[header_value]="14"
+off=112 header_value complete
+off=112 len=4 span[header_field]="Date"
+off=117 header_field complete
+off=118 len=29 span[header_value]="Fri, 15 Sep 2023 19:47:23 GMT"
+off=149 header_value complete
+off=149 len=6 span[header_field]="Server"
+off=156 header_field complete
+off=157 len=32 span[header_value]="Python/3.10 aiohttp/4.0.0a2.dev0"
+off=191 header_value complete
+off=193 headers complete status=404 v=1/1 flags=20 content_length=14
+off=193 len=14 span[body]="404: Not Found"
+off=207 message complete
+```
+
+## HTTP 103 first, then 200
+
+<!-- meta={"type": "response"} -->
+```http
+HTTP/1.1 103 Early Hints
+Link: </styles.css>; rel=preload; as=style
+
+HTTP/1.1 200 OK
+Date: Wed, 13 Sep 2023 11:09:41 GMT
+Connection: keep-alive
+Keep-Alive: timeout=5
+Content-Length: 17
+
+response content
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=11 span[status]="Early Hints"
+off=26 status complete
+off=26 len=4 span[header_field]="Link"
+off=31 header_field complete
+off=32 len=36 span[header_value]="</styles.css>; rel=preload; as=style"
+off=70 header_value complete
+off=72 headers complete status=103 v=1/1 flags=0 content_length=0
+off=72 message complete
+off=72 reset
+off=72 message begin
+off=77 len=3 span[version]="1.1"
+off=80 version complete
+off=85 len=2 span[status]="OK"
+off=89 status complete
+off=89 len=4 span[header_field]="Date"
+off=94 header_field complete
+off=95 len=29 span[header_value]="Wed, 13 Sep 2023 11:09:41 GMT"
+off=126 header_value complete
+off=126 len=10 span[header_field]="Connection"
+off=137 header_field complete
+off=138 len=10 span[header_value]="keep-alive"
+off=150 header_value complete
+off=150 len=10 span[header_field]="Keep-Alive"
+off=161 header_field complete
+off=162 len=9 span[header_value]="timeout=5"
+off=173 header_value complete
+off=173 len=14 span[header_field]="Content-Length"
+off=188 header_field complete
+off=189 len=2 span[header_value]="17"
+off=193 header_value complete
+off=195 headers complete status=200 v=1/1 flags=21 content_length=17
+off=195 len=16 span[body]="response content"
+``` \ No newline at end of file
diff --git a/llhttp/test/response/content-length.md b/llhttp/test/response/content-length.md
new file mode 100644
index 0000000..6c33924
--- /dev/null
+++ b/llhttp/test/response/content-length.md
@@ -0,0 +1,158 @@
+Content-Length header
+=====================
+
+## Response without `Content-Length`, but with body
+
+The client should wait for the server's EOF. That is, when
+`Content-Length` is not specified, and `Connection: close`, the end of body is
+specified by the EOF.
+
+_(Compare with APACHEBENCH_GET)_
+
+<!-- meta={"type": "response"} -->
+```http
+HTTP/1.1 200 OK
+Date: Tue, 04 Aug 2009 07:59:32 GMT
+Server: Apache
+X-Powered-By: Servlet/2.5 JSP/2.1
+Content-Type: text/xml; charset=utf-8
+Connection: close
+
+<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\
+<SOAP-ENV:Envelope xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\">\n\
+ <SOAP-ENV:Body>\n\
+ <SOAP-ENV:Fault>\n\
+ <faultcode>SOAP-ENV:Client</faultcode>\n\
+ <faultstring>Client Error</faultstring>\n\
+ </SOAP-ENV:Fault>\n\
+ </SOAP-ENV:Body>\n\
+</SOAP-ENV:Envelope>
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=2 span[status]="OK"
+off=17 status complete
+off=17 len=4 span[header_field]="Date"
+off=22 header_field complete
+off=23 len=29 span[header_value]="Tue, 04 Aug 2009 07:59:32 GMT"
+off=54 header_value complete
+off=54 len=6 span[header_field]="Server"
+off=61 header_field complete
+off=62 len=6 span[header_value]="Apache"
+off=70 header_value complete
+off=70 len=12 span[header_field]="X-Powered-By"
+off=83 header_field complete
+off=84 len=19 span[header_value]="Servlet/2.5 JSP/2.1"
+off=105 header_value complete
+off=105 len=12 span[header_field]="Content-Type"
+off=118 header_field complete
+off=119 len=23 span[header_value]="text/xml; charset=utf-8"
+off=144 header_value complete
+off=144 len=10 span[header_field]="Connection"
+off=155 header_field complete
+off=156 len=5 span[header_value]="close"
+off=163 header_value complete
+off=165 headers complete status=200 v=1/1 flags=2 content_length=0
+off=165 len=42 span[body]="<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
+off=207 len=1 span[body]=lf
+off=208 len=80 span[body]="<SOAP-ENV:Envelope xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\">"
+off=288 len=1 span[body]=lf
+off=289 len=17 span[body]=" <SOAP-ENV:Body>"
+off=306 len=1 span[body]=lf
+off=307 len=20 span[body]=" <SOAP-ENV:Fault>"
+off=327 len=1 span[body]=lf
+off=328 len=45 span[body]=" <faultcode>SOAP-ENV:Client</faultcode>"
+off=373 len=1 span[body]=lf
+off=374 len=46 span[body]=" <faultstring>Client Error</faultstring>"
+off=420 len=1 span[body]=lf
+off=421 len=21 span[body]=" </SOAP-ENV:Fault>"
+off=442 len=1 span[body]=lf
+off=443 len=18 span[body]=" </SOAP-ENV:Body>"
+off=461 len=1 span[body]=lf
+off=462 len=20 span[body]="</SOAP-ENV:Envelope>"
+```
+
+## Content-Length-X
+
+The header that starts with `Content-Length*` should not be treated as
+`Content-Length`.
+
+<!-- meta={"type": "response"} -->
+```http
+HTTP/1.1 200 OK
+Content-Length-X: 0
+Transfer-Encoding: chunked
+
+2
+OK
+0
+
+
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=2 span[status]="OK"
+off=17 status complete
+off=17 len=16 span[header_field]="Content-Length-X"
+off=34 header_field complete
+off=35 len=1 span[header_value]="0"
+off=38 header_value complete
+off=38 len=17 span[header_field]="Transfer-Encoding"
+off=56 header_field complete
+off=57 len=7 span[header_value]="chunked"
+off=66 header_value complete
+off=68 headers complete status=200 v=1/1 flags=208 content_length=0
+off=71 chunk header len=2
+off=71 len=2 span[body]="OK"
+off=75 chunk complete
+off=78 chunk header len=0
+off=80 chunk complete
+off=80 message complete
+```
+
+## Content-Length reset when no body is received
+
+<!-- meta={"type": "response", "skipBody": true} -->
+```http
+HTTP/1.1 200 OK
+Content-Length: 123
+
+HTTP/1.1 200 OK
+Content-Length: 456
+
+
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=2 span[status]="OK"
+off=17 status complete
+off=17 len=14 span[header_field]="Content-Length"
+off=32 header_field complete
+off=33 len=3 span[header_value]="123"
+off=38 header_value complete
+off=40 headers complete status=200 v=1/1 flags=20 content_length=123
+off=40 skip body
+off=40 message complete
+off=40 reset
+off=40 message begin
+off=45 len=3 span[version]="1.1"
+off=48 version complete
+off=53 len=2 span[status]="OK"
+off=57 status complete
+off=57 len=14 span[header_field]="Content-Length"
+off=72 header_field complete
+off=73 len=3 span[header_value]="456"
+off=78 header_value complete
+off=80 headers complete status=200 v=1/1 flags=20 content_length=456
+off=80 skip body
+off=80 message complete
+```
diff --git a/llhttp/test/response/finish.md b/llhttp/test/response/finish.md
new file mode 100644
index 0000000..2938b83
--- /dev/null
+++ b/llhttp/test/response/finish.md
@@ -0,0 +1,23 @@
+Finish
+======
+
+Those tests check the return codes and the behavior of `llhttp_finish()` C API.
+
+## It should be safe to finish with cb after empty response
+
+<!-- meta={"type": "response-finish"} -->
+```http
+HTTP/1.1 200 OK
+
+
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=2 span[status]="OK"
+off=17 status complete
+off=19 headers complete status=200 v=1/1 flags=0 content_length=0
+off=NULL finish=1
+```
diff --git a/llhttp/test/response/invalid.md b/llhttp/test/response/invalid.md
new file mode 100644
index 0000000..034fc4d
--- /dev/null
+++ b/llhttp/test/response/invalid.md
@@ -0,0 +1,285 @@
+Invalid responses
+=================
+
+### Incomplete HTTP protocol
+
+<!-- meta={"type": "response"} -->
+```http
+HTP/1.1 200 OK
+
+
+```
+
+```log
+off=0 message begin
+off=2 error code=8 reason="Expected HTTP/"
+```
+
+### Extra digit in HTTP major version
+
+<!-- meta={"type": "response"} -->
+```http
+HTTP/01.1 200 OK
+
+
+```
+
+```log
+off=0 message begin
+off=5 len=1 span[version]="0"
+off=6 error code=9 reason="Expected dot"
+```
+
+### Extra digit in HTTP major version #2
+
+<!-- meta={"type": "response"} -->
+```http
+HTTP/11.1 200 OK
+
+
+```
+
+```log
+off=0 message begin
+off=5 len=1 span[version]="1"
+off=6 error code=9 reason="Expected dot"
+```
+
+### Extra digit in HTTP minor version
+
+<!-- meta={"type": "response"} -->
+```http
+HTTP/1.01 200 OK
+
+
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.0"
+off=8 version complete
+off=8 error code=9 reason="Expected space after version"
+```
+-->
+
+### Tab after HTTP version
+
+<!-- meta={"type": "response"} -->
+```http
+HTTP/1.1\t200 OK
+
+
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=8 error code=9 reason="Expected space after version"
+```
+
+### CR before response and tab after HTTP version
+
+<!-- meta={"type": "response"} -->
+```http
+\rHTTP/1.1\t200 OK
+
+
+```
+
+```log
+off=1 message begin
+off=6 len=3 span[version]="1.1"
+off=9 version complete
+off=9 error code=9 reason="Expected space after version"
+```
+
+### Headers separated by CR
+
+<!-- meta={"type": "response"} -->
+```http
+HTTP/1.1 200 OK
+Foo: 1\rBar: 2
+
+
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=2 span[status]="OK"
+off=17 status complete
+off=17 len=3 span[header_field]="Foo"
+off=21 header_field complete
+off=22 len=1 span[header_value]="1"
+off=24 error code=3 reason="Missing expected LF after header value"
+```
+
+### Invalid HTTP version
+
+<!-- meta={"type": "response"} -->
+```http
+HTTP/5.6 200 OK
+
+
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="5.6"
+off=8 error code=9 reason="Invalid HTTP version"
+```
+
+## Invalid space after start line
+
+<!-- meta={"type": "response"} -->
+```http
+HTTP/1.1 200 OK
+ Host: foo
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=2 span[status]="OK"
+off=17 status complete
+off=18 error code=30 reason="Unexpected space after start line"
+```
+
+### Extra space between HTTP version and status code
+
+<!-- meta={"type": "response"} -->
+```http
+HTTP/1.1 200 OK
+
+
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=9 error code=13 reason="Invalid status code"
+```
+
+### Extra space between status code and reason
+
+<!-- meta={"type": "response"} -->
+```http
+HTTP/1.1 200 OK
+
+
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=3 span[status]=" OK"
+off=18 status complete
+off=20 headers complete status=200 v=1/1 flags=0 content_length=0
+```
+
+### One-digit status code
+
+<!-- meta={"type": "response"} -->
+```http
+HTTP/1.1 2 OK
+
+
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=10 error code=13 reason="Invalid status code"
+```
+
+### Only LFs present and no body
+
+<!-- meta={"type": "response"} -->
+```http
+HTTP/1.1 200 OK\nContent-Length: 0\n\n
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=2 span[status]="OK"
+off=16 error code=25 reason="Missing expected CR after response line"
+```
+
+### Only LFs present and no body (lenient)
+
+<!-- meta={"type": "response-lenient-all"} -->
+```http
+HTTP/1.1 200 OK\nContent-Length: 0\n\n
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=2 span[status]="OK"
+off=16 status complete
+off=16 len=14 span[header_field]="Content-Length"
+off=31 header_field complete
+off=32 len=1 span[header_value]="0"
+off=34 header_value complete
+off=35 headers complete status=200 v=1/1 flags=20 content_length=0
+off=35 message complete
+```
+
+### Only LFs present
+
+<!-- meta={"type": "response"} -->
+```http
+HTTP/1.1 200 OK\n\
+Foo: abc\n\
+Bar: def\n\
+\n\
+BODY\n\
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=2 span[status]="OK"
+off=16 error code=25 reason="Missing expected CR after response line"
+```
+
+### Only LFs present (lenient)
+
+<!-- meta={"type": "response-lenient-all"} -->
+```http
+HTTP/1.1 200 OK\n\
+Foo: abc\n\
+Bar: def\n\
+\n\
+BODY\n\
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=2 span[status]="OK"
+off=16 status complete
+off=16 len=3 span[header_field]="Foo"
+off=20 header_field complete
+off=21 len=3 span[header_value]="abc"
+off=25 header_value complete
+off=25 len=3 span[header_field]="Bar"
+off=29 header_field complete
+off=30 len=3 span[header_value]="def"
+off=34 header_value complete
+off=35 headers complete status=200 v=1/1 flags=0 content_length=0
+off=35 len=4 span[body]="BODY"
+off=39 len=1 span[body]=lf
+off=40 len=1 span[body]="\"
+``` \ No newline at end of file
diff --git a/llhttp/test/response/lenient-version.md b/llhttp/test/response/lenient-version.md
new file mode 100644
index 0000000..86c6ede
--- /dev/null
+++ b/llhttp/test/response/lenient-version.md
@@ -0,0 +1,20 @@
+Lenient HTTP version parsing
+============================
+
+### Invalid HTTP version (lenient)
+
+<!-- meta={"type": "response-lenient-version"} -->
+```http
+HTTP/5.6 200 OK
+
+
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="5.6"
+off=8 version complete
+off=13 len=2 span[status]="OK"
+off=17 status complete
+off=19 headers complete status=200 v=5/6 flags=0 content_length=0
+```
diff --git a/llhttp/test/response/pausing.md b/llhttp/test/response/pausing.md
new file mode 100644
index 0000000..d2e870b
--- /dev/null
+++ b/llhttp/test/response/pausing.md
@@ -0,0 +1,330 @@
+Pausing
+=======
+
+### on_message_begin
+
+<!-- meta={"type": "response", "pause": "on_message_begin"} -->
+```http
+HTTP/1.1 200 OK
+Content-Length: 3
+
+abc
+```
+
+```log
+off=0 message begin
+off=0 pause
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=2 span[status]="OK"
+off=17 status complete
+off=17 len=14 span[header_field]="Content-Length"
+off=32 header_field complete
+off=33 len=1 span[header_value]="3"
+off=36 header_value complete
+off=38 headers complete status=200 v=1/1 flags=20 content_length=3
+off=38 len=3 span[body]="abc"
+off=41 message complete
+```
+
+### on_message_complete
+
+<!-- meta={"type": "response", "pause": "on_message_complete"} -->
+```http
+HTTP/1.1 200 OK
+Content-Length: 3
+
+abc
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=2 span[status]="OK"
+off=17 status complete
+off=17 len=14 span[header_field]="Content-Length"
+off=32 header_field complete
+off=33 len=1 span[header_value]="3"
+off=36 header_value complete
+off=38 headers complete status=200 v=1/1 flags=20 content_length=3
+off=38 len=3 span[body]="abc"
+off=41 message complete
+off=41 pause
+```
+
+### on_version_complete
+
+<!-- meta={"type": "response", "pause": "on_version_complete"} -->
+```http
+HTTP/1.1 200 OK
+Content-Length: 3
+
+abc
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=8 pause
+off=13 len=2 span[status]="OK"
+off=17 status complete
+off=17 len=14 span[header_field]="Content-Length"
+off=32 header_field complete
+off=33 len=1 span[header_value]="3"
+off=36 header_value complete
+off=38 headers complete status=200 v=1/1 flags=20 content_length=3
+off=38 len=3 span[body]="abc"
+off=41 message complete
+```
+
+### on_status_complete
+
+<!-- meta={"type": "response", "pause": "on_status_complete"} -->
+```http
+HTTP/1.1 200 OK
+Content-Length: 3
+
+abc
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=2 span[status]="OK"
+off=17 status complete
+off=17 pause
+off=17 len=14 span[header_field]="Content-Length"
+off=32 header_field complete
+off=33 len=1 span[header_value]="3"
+off=36 header_value complete
+off=38 headers complete status=200 v=1/1 flags=20 content_length=3
+off=38 len=3 span[body]="abc"
+off=41 message complete
+```
+
+### on_header_field_complete
+
+<!-- meta={"type": "response", "pause": "on_header_field_complete"} -->
+```http
+HTTP/1.1 200 OK
+Content-Length: 3
+
+abc
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=2 span[status]="OK"
+off=17 status complete
+off=17 len=14 span[header_field]="Content-Length"
+off=32 header_field complete
+off=32 pause
+off=33 len=1 span[header_value]="3"
+off=36 header_value complete
+off=38 headers complete status=200 v=1/1 flags=20 content_length=3
+off=38 len=3 span[body]="abc"
+off=41 message complete
+```
+
+### on_header_value_complete
+
+<!-- meta={"type": "response", "pause": "on_header_value_complete"} -->
+```http
+HTTP/1.1 200 OK
+Content-Length: 3
+
+abc
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=2 span[status]="OK"
+off=17 status complete
+off=17 len=14 span[header_field]="Content-Length"
+off=32 header_field complete
+off=33 len=1 span[header_value]="3"
+off=36 header_value complete
+off=36 pause
+off=38 headers complete status=200 v=1/1 flags=20 content_length=3
+off=38 len=3 span[body]="abc"
+off=41 message complete
+```
+
+### on_headers_complete
+
+<!-- meta={"type": "response", "pause": "on_headers_complete"} -->
+```http
+HTTP/1.1 200 OK
+Content-Length: 3
+
+abc
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=2 span[status]="OK"
+off=17 status complete
+off=17 len=14 span[header_field]="Content-Length"
+off=32 header_field complete
+off=33 len=1 span[header_value]="3"
+off=36 header_value complete
+off=38 headers complete status=200 v=1/1 flags=20 content_length=3
+off=38 pause
+off=38 len=3 span[body]="abc"
+off=41 message complete
+```
+
+### on_chunk_header
+
+<!-- meta={"type": "response", "pause": "on_chunk_header"} -->
+```http
+HTTP/1.1 200 OK
+Transfer-Encoding: chunked
+
+a
+0123456789
+0
+
+
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=2 span[status]="OK"
+off=17 status complete
+off=17 len=17 span[header_field]="Transfer-Encoding"
+off=35 header_field complete
+off=36 len=7 span[header_value]="chunked"
+off=45 header_value complete
+off=47 headers complete status=200 v=1/1 flags=208 content_length=0
+off=50 chunk header len=10
+off=50 pause
+off=50 len=10 span[body]="0123456789"
+off=62 chunk complete
+off=65 chunk header len=0
+off=65 pause
+off=67 chunk complete
+off=67 message complete
+```
+
+### on_chunk_extension_name
+
+<!-- meta={"type": "response", "pause": "on_chunk_extension_name"} -->
+```http
+HTTP/1.1 200 OK
+Transfer-Encoding: chunked
+
+a;foo=bar
+0123456789
+0
+
+
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=2 span[status]="OK"
+off=17 status complete
+off=17 len=17 span[header_field]="Transfer-Encoding"
+off=35 header_field complete
+off=36 len=7 span[header_value]="chunked"
+off=45 header_value complete
+off=47 headers complete status=200 v=1/1 flags=208 content_length=0
+off=49 len=3 span[chunk_extension_name]="foo"
+off=53 chunk_extension_name complete
+off=53 pause
+off=53 len=3 span[chunk_extension_value]="bar"
+off=57 chunk_extension_value complete
+off=58 chunk header len=10
+off=58 len=10 span[body]="0123456789"
+off=70 chunk complete
+off=73 chunk header len=0
+off=75 chunk complete
+off=75 message complete
+```
+
+### on_chunk_extension_value
+
+<!-- meta={"type": "response", "pause": "on_chunk_extension_value"} -->
+```http
+HTTP/1.1 200 OK
+Transfer-Encoding: chunked
+
+a;foo=bar
+0123456789
+0
+
+
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=2 span[status]="OK"
+off=17 status complete
+off=17 len=17 span[header_field]="Transfer-Encoding"
+off=35 header_field complete
+off=36 len=7 span[header_value]="chunked"
+off=45 header_value complete
+off=47 headers complete status=200 v=1/1 flags=208 content_length=0
+off=49 len=3 span[chunk_extension_name]="foo"
+off=53 chunk_extension_name complete
+off=53 len=3 span[chunk_extension_value]="bar"
+off=57 chunk_extension_value complete
+off=57 pause
+off=58 chunk header len=10
+off=58 len=10 span[body]="0123456789"
+off=70 chunk complete
+off=73 chunk header len=0
+off=75 chunk complete
+off=75 message complete
+```
+
+### on_chunk_complete
+
+<!-- meta={"type": "response", "pause": "on_chunk_complete"} -->
+```http
+HTTP/1.1 200 OK
+Transfer-Encoding: chunked
+
+a
+0123456789
+0
+
+
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=2 span[status]="OK"
+off=17 status complete
+off=17 len=17 span[header_field]="Transfer-Encoding"
+off=35 header_field complete
+off=36 len=7 span[header_value]="chunked"
+off=45 header_value complete
+off=47 headers complete status=200 v=1/1 flags=208 content_length=0
+off=50 chunk header len=10
+off=50 len=10 span[body]="0123456789"
+off=62 chunk complete
+off=62 pause
+off=65 chunk header len=0
+off=67 chunk complete
+off=67 pause
+off=67 message complete
+```
diff --git a/llhttp/test/response/pipelining.md b/llhttp/test/response/pipelining.md
new file mode 100644
index 0000000..01e007a
--- /dev/null
+++ b/llhttp/test/response/pipelining.md
@@ -0,0 +1,60 @@
+Pipelining
+==========
+
+## Should parse multiple events
+
+<!-- meta={"type": "response"} -->
+```http
+HTTP/1.1 200 OK
+Content-Length: 3
+
+AAA
+HTTP/1.1 201 Created
+Content-Length: 4
+
+BBBB
+HTTP/1.1 202 Accepted
+Content-Length: 5
+
+CCCC
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=2 span[status]="OK"
+off=17 status complete
+off=17 len=14 span[header_field]="Content-Length"
+off=32 header_field complete
+off=33 len=1 span[header_value]="3"
+off=36 header_value complete
+off=38 headers complete status=200 v=1/1 flags=20 content_length=3
+off=38 len=3 span[body]="AAA"
+off=41 message complete
+off=43 reset
+off=43 message begin
+off=48 len=3 span[version]="1.1"
+off=51 version complete
+off=56 len=7 span[status]="Created"
+off=65 status complete
+off=65 len=14 span[header_field]="Content-Length"
+off=80 header_field complete
+off=81 len=1 span[header_value]="4"
+off=84 header_value complete
+off=86 headers complete status=201 v=1/1 flags=20 content_length=4
+off=86 len=4 span[body]="BBBB"
+off=90 message complete
+off=92 reset
+off=92 message begin
+off=97 len=3 span[version]="1.1"
+off=100 version complete
+off=105 len=8 span[status]="Accepted"
+off=115 status complete
+off=115 len=14 span[header_field]="Content-Length"
+off=130 header_field complete
+off=131 len=1 span[header_value]="5"
+off=134 header_value complete
+off=136 headers complete status=202 v=1/1 flags=20 content_length=5
+off=136 len=4 span[body]="CCCC"
+``` \ No newline at end of file
diff --git a/llhttp/test/response/sample.md b/llhttp/test/response/sample.md
new file mode 100644
index 0000000..be2e82d
--- /dev/null
+++ b/llhttp/test/response/sample.md
@@ -0,0 +1,653 @@
+Sample responses
+================
+
+## Simple response
+
+<!-- meta={"type": "response"} -->
+```http
+HTTP/1.1 200 OK
+Header1: Value1
+Header2:\t Value2
+Content-Length: 0
+
+
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=2 span[status]="OK"
+off=17 status complete
+off=17 len=7 span[header_field]="Header1"
+off=25 header_field complete
+off=26 len=6 span[header_value]="Value1"
+off=34 header_value complete
+off=34 len=7 span[header_field]="Header2"
+off=42 header_field complete
+off=44 len=6 span[header_value]="Value2"
+off=52 header_value complete
+off=52 len=14 span[header_field]="Content-Length"
+off=67 header_field complete
+off=68 len=1 span[header_value]="0"
+off=71 header_value complete
+off=73 headers complete status=200 v=1/1 flags=20 content_length=0
+off=73 message complete
+```
+
+## Error on invalid response start
+
+Every response must start with `HTTP/`.
+
+<!-- meta={"type": "response"} -->
+```http
+HTTPER/1.1 200 OK
+
+
+```
+
+```log
+off=0 message begin
+off=4 error code=8 reason="Expected HTTP/"
+```
+
+## Empty body should not trigger spurious span callbacks
+
+<!-- meta={"type": "response"} -->
+```http
+HTTP/1.1 200 OK
+
+
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=2 span[status]="OK"
+off=17 status complete
+off=19 headers complete status=200 v=1/1 flags=0 content_length=0
+```
+
+## Google 301
+
+<!-- meta={"type": "response"} -->
+```http
+HTTP/1.1 301 Moved Permanently
+Location: http://www.google.com/
+Content-Type: text/html; charset=UTF-8
+Date: Sun, 26 Apr 2009 11:11:49 GMT
+Expires: Tue, 26 May 2009 11:11:49 GMT
+X-$PrototypeBI-Version: 1.6.0.3
+Cache-Control: public, max-age=2592000
+Server: gws
+Content-Length: 219
+
+<HTML><HEAD><meta http-equiv=content-type content=text/html;charset=utf-8>\n\
+<TITLE>301 Moved</TITLE></HEAD><BODY>\n\
+<H1>301 Moved</H1>\n\
+The document has moved\n\
+<A HREF="http://www.google.com/">here</A>.
+</BODY></HTML>
+```
+_(Note the `$` char in header field)_
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=17 span[status]="Moved Permanently"
+off=32 status complete
+off=32 len=8 span[header_field]="Location"
+off=41 header_field complete
+off=42 len=22 span[header_value]="http://www.google.com/"
+off=66 header_value complete
+off=66 len=12 span[header_field]="Content-Type"
+off=79 header_field complete
+off=80 len=24 span[header_value]="text/html; charset=UTF-8"
+off=106 header_value complete
+off=106 len=4 span[header_field]="Date"
+off=111 header_field complete
+off=112 len=29 span[header_value]="Sun, 26 Apr 2009 11:11:49 GMT"
+off=143 header_value complete
+off=143 len=7 span[header_field]="Expires"
+off=151 header_field complete
+off=152 len=29 span[header_value]="Tue, 26 May 2009 11:11:49 GMT"
+off=183 header_value complete
+off=183 len=22 span[header_field]="X-$PrototypeBI-Version"
+off=206 header_field complete
+off=207 len=7 span[header_value]="1.6.0.3"
+off=216 header_value complete
+off=216 len=13 span[header_field]="Cache-Control"
+off=230 header_field complete
+off=231 len=23 span[header_value]="public, max-age=2592000"
+off=256 header_value complete
+off=256 len=6 span[header_field]="Server"
+off=263 header_field complete
+off=264 len=3 span[header_value]="gws"
+off=269 header_value complete
+off=269 len=14 span[header_field]="Content-Length"
+off=284 header_field complete
+off=286 len=5 span[header_value]="219 "
+off=293 header_value complete
+off=295 headers complete status=301 v=1/1 flags=20 content_length=219
+off=295 len=74 span[body]="<HTML><HEAD><meta http-equiv=content-type content=text/html;charset=utf-8>"
+off=369 len=1 span[body]=lf
+off=370 len=37 span[body]="<TITLE>301 Moved</TITLE></HEAD><BODY>"
+off=407 len=1 span[body]=lf
+off=408 len=18 span[body]="<H1>301 Moved</H1>"
+off=426 len=1 span[body]=lf
+off=427 len=22 span[body]="The document has moved"
+off=449 len=1 span[body]=lf
+off=450 len=42 span[body]="<A HREF="http://www.google.com/">here</A>."
+off=492 len=1 span[body]=cr
+off=493 len=1 span[body]=lf
+off=494 len=14 span[body]="</BODY></HTML>"
+```
+
+## amazon.com
+
+<!-- meta={"type": "response"} -->
+```http
+HTTP/1.1 301 MovedPermanently
+Date: Wed, 15 May 2013 17:06:33 GMT
+Server: Server
+x-amz-id-1: 0GPHKXSJQ826RK7GZEB2
+p3p: policyref="http://www.amazon.com/w3c/p3p.xml",CP="CAO DSP LAW CUR ADM IVAo IVDo CONo OTPo OUR DELi PUBi OTRi BUS PHY ONL UNI PUR FIN COM NAV INT DEM CNT STA HEA PRE LOC GOV OTC "
+x-amz-id-2: STN69VZxIFSz9YJLbz1GDbxpbjG6Qjmmq5E3DxRhOUw+Et0p4hr7c/Q8qNcx4oAD
+Location: http://www.amazon.com/Dan-Brown/e/B000AP9DSU/ref=s9_pop_gw_al1?_encoding=UTF8&refinementId=618073011&pf_rd_m=ATVPDKIKX0DER&pf_rd_s=center-2&pf_rd_r=0SHYY5BZXN3KR20BNFAY&pf_rd_t=101&pf_rd_p=1263340922&pf_rd_i=507846
+Vary: Accept-Encoding,User-Agent
+Content-Type: text/html; charset=ISO-8859-1
+Transfer-Encoding: chunked
+
+1
+\n
+0
+
+
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=16 span[status]="MovedPermanently"
+off=31 status complete
+off=31 len=4 span[header_field]="Date"
+off=36 header_field complete
+off=37 len=29 span[header_value]="Wed, 15 May 2013 17:06:33 GMT"
+off=68 header_value complete
+off=68 len=6 span[header_field]="Server"
+off=75 header_field complete
+off=76 len=6 span[header_value]="Server"
+off=84 header_value complete
+off=84 len=10 span[header_field]="x-amz-id-1"
+off=95 header_field complete
+off=96 len=20 span[header_value]="0GPHKXSJQ826RK7GZEB2"
+off=118 header_value complete
+off=118 len=3 span[header_field]="p3p"
+off=122 header_field complete
+off=123 len=178 span[header_value]="policyref="http://www.amazon.com/w3c/p3p.xml",CP="CAO DSP LAW CUR ADM IVAo IVDo CONo OTPo OUR DELi PUBi OTRi BUS PHY ONL UNI PUR FIN COM NAV INT DEM CNT STA HEA PRE LOC GOV OTC ""
+off=303 header_value complete
+off=303 len=10 span[header_field]="x-amz-id-2"
+off=314 header_field complete
+off=315 len=64 span[header_value]="STN69VZxIFSz9YJLbz1GDbxpbjG6Qjmmq5E3DxRhOUw+Et0p4hr7c/Q8qNcx4oAD"
+off=381 header_value complete
+off=381 len=8 span[header_field]="Location"
+off=390 header_field complete
+off=391 len=214 span[header_value]="http://www.amazon.com/Dan-Brown/e/B000AP9DSU/ref=s9_pop_gw_al1?_encoding=UTF8&refinementId=618073011&pf_rd_m=ATVPDKIKX0DER&pf_rd_s=center-2&pf_rd_r=0SHYY5BZXN3KR20BNFAY&pf_rd_t=101&pf_rd_p=1263340922&pf_rd_i=507846"
+off=607 header_value complete
+off=607 len=4 span[header_field]="Vary"
+off=612 header_field complete
+off=613 len=26 span[header_value]="Accept-Encoding,User-Agent"
+off=641 header_value complete
+off=641 len=12 span[header_field]="Content-Type"
+off=654 header_field complete
+off=655 len=29 span[header_value]="text/html; charset=ISO-8859-1"
+off=686 header_value complete
+off=686 len=17 span[header_field]="Transfer-Encoding"
+off=704 header_field complete
+off=705 len=7 span[header_value]="chunked"
+off=714 header_value complete
+off=716 headers complete status=301 v=1/1 flags=208 content_length=0
+off=719 chunk header len=1
+off=719 len=1 span[body]=lf
+off=722 chunk complete
+off=725 chunk header len=0
+off=727 chunk complete
+off=727 message complete
+```
+
+## No headers and no body
+
+<!-- meta={"type": "response"} -->
+```http
+HTTP/1.1 404 Not Found
+
+
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=9 span[status]="Not Found"
+off=24 status complete
+off=26 headers complete status=404 v=1/1 flags=0 content_length=0
+```
+
+## No reason phrase
+
+<!-- meta={"type": "response"} -->
+```http
+HTTP/1.1 301
+
+
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=14 status complete
+off=16 headers complete status=301 v=1/1 flags=0 content_length=0
+```
+
+## Empty reason phrase after space
+
+<!-- meta={"type": "response"} -->
+```http
+HTTP/1.1 200 \r\n\
+
+
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=0 span[status]=""
+off=15 status complete
+off=17 headers complete status=200 v=1/1 flags=0 content_length=0
+```
+
+## No carriage ret
+
+<!-- meta={"type": "response"} -->
+```http
+HTTP/1.1 200 OK\n\
+Content-Type: text/html; charset=utf-8\n\
+Connection: close\n\
+\n\
+these headers are from http://news.ycombinator.com/
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=2 span[status]="OK"
+off=16 error code=25 reason="Missing expected CR after response line"
+```
+
+## No carriage ret (lenient)
+
+<!-- meta={"type": "response-lenient-optional-cr-before-lf"} -->
+```http
+HTTP/1.1 200 OK\n\
+Content-Type: text/html; charset=utf-8\n\
+Connection: close\n\
+\n\
+these headers are from http://news.ycombinator.com/
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=2 span[status]="OK"
+off=16 status complete
+off=16 len=12 span[header_field]="Content-Type"
+off=29 header_field complete
+off=30 len=24 span[header_value]="text/html; charset=utf-8"
+off=55 header_value complete
+off=55 len=10 span[header_field]="Connection"
+off=66 header_field complete
+off=67 len=5 span[header_value]="close"
+off=73 header_value complete
+off=74 headers complete status=200 v=1/1 flags=2 content_length=0
+off=74 len=51 span[body]="these headers are from http://news.ycombinator.com/"
+```
+
+## Underscore in header key
+
+Shown by: `curl -o /dev/null -v "http://ad.doubleclick.net/pfadx/DARTSHELLCONFIGXML;dcmt=text/xml;"`
+
+<!-- meta={"type": "response"} -->
+```http
+HTTP/1.1 200 OK
+Server: DCLK-AdSvr
+Content-Type: text/xml
+Content-Length: 0
+DCLK_imp: v7;x;114750856;0-0;0;17820020;0/0;21603567/21621457/1;;~okv=;dcmt=text/xml;;~cs=o
+
+
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=2 span[status]="OK"
+off=17 status complete
+off=17 len=6 span[header_field]="Server"
+off=24 header_field complete
+off=25 len=10 span[header_value]="DCLK-AdSvr"
+off=37 header_value complete
+off=37 len=12 span[header_field]="Content-Type"
+off=50 header_field complete
+off=51 len=8 span[header_value]="text/xml"
+off=61 header_value complete
+off=61 len=14 span[header_field]="Content-Length"
+off=76 header_field complete
+off=77 len=1 span[header_value]="0"
+off=80 header_value complete
+off=80 len=8 span[header_field]="DCLK_imp"
+off=89 header_field complete
+off=90 len=81 span[header_value]="v7;x;114750856;0-0;0;17820020;0/0;21603567/21621457/1;;~okv=;dcmt=text/xml;;~cs=o"
+off=173 header_value complete
+off=175 headers complete status=200 v=1/1 flags=20 content_length=0
+off=175 message complete
+```
+
+## bonjourmadame.fr
+
+The client should not merge two headers fields when the first one doesn't
+have a value.
+
+<!-- meta={"type": "response"} -->
+```http
+HTTP/1.0 301 Moved Permanently
+Date: Thu, 03 Jun 2010 09:56:32 GMT
+Server: Apache/2.2.3 (Red Hat)
+Cache-Control: public
+Pragma: \r\n\
+Location: http://www.bonjourmadame.fr/
+Vary: Accept-Encoding
+Content-Length: 0
+Content-Type: text/html; charset=UTF-8
+Connection: keep-alive
+
+
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.0"
+off=8 version complete
+off=13 len=17 span[status]="Moved Permanently"
+off=32 status complete
+off=32 len=4 span[header_field]="Date"
+off=37 header_field complete
+off=38 len=29 span[header_value]="Thu, 03 Jun 2010 09:56:32 GMT"
+off=69 header_value complete
+off=69 len=6 span[header_field]="Server"
+off=76 header_field complete
+off=77 len=22 span[header_value]="Apache/2.2.3 (Red Hat)"
+off=101 header_value complete
+off=101 len=13 span[header_field]="Cache-Control"
+off=115 header_field complete
+off=116 len=6 span[header_value]="public"
+off=124 header_value complete
+off=124 len=6 span[header_field]="Pragma"
+off=131 header_field complete
+off=134 len=0 span[header_value]=""
+off=134 header_value complete
+off=134 len=8 span[header_field]="Location"
+off=143 header_field complete
+off=144 len=28 span[header_value]="http://www.bonjourmadame.fr/"
+off=174 header_value complete
+off=174 len=4 span[header_field]="Vary"
+off=179 header_field complete
+off=180 len=15 span[header_value]="Accept-Encoding"
+off=197 header_value complete
+off=197 len=14 span[header_field]="Content-Length"
+off=212 header_field complete
+off=213 len=1 span[header_value]="0"
+off=216 header_value complete
+off=216 len=12 span[header_field]="Content-Type"
+off=229 header_field complete
+off=230 len=24 span[header_value]="text/html; charset=UTF-8"
+off=256 header_value complete
+off=256 len=10 span[header_field]="Connection"
+off=267 header_field complete
+off=268 len=10 span[header_value]="keep-alive"
+off=280 header_value complete
+off=282 headers complete status=301 v=1/0 flags=21 content_length=0
+off=282 message complete
+```
+
+## Spaces in header value
+
+<!-- meta={"type": "response"} -->
+```http
+HTTP/1.1 200 OK
+Date: Tue, 28 Sep 2010 01:14:13 GMT
+Server: Apache
+Cache-Control: no-cache, must-revalidate
+Expires: Mon, 26 Jul 1997 05:00:00 GMT
+.et-Cookie: PlaxoCS=1274804622353690521; path=/; domain=.plaxo.com
+Vary: Accept-Encoding
+_eep-Alive: timeout=45
+_onnection: Keep-Alive
+Transfer-Encoding: chunked
+Content-Type: text/html
+Connection: close
+
+0
+
+
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=2 span[status]="OK"
+off=17 status complete
+off=17 len=4 span[header_field]="Date"
+off=22 header_field complete
+off=23 len=29 span[header_value]="Tue, 28 Sep 2010 01:14:13 GMT"
+off=54 header_value complete
+off=54 len=6 span[header_field]="Server"
+off=61 header_field complete
+off=62 len=6 span[header_value]="Apache"
+off=70 header_value complete
+off=70 len=13 span[header_field]="Cache-Control"
+off=84 header_field complete
+off=85 len=25 span[header_value]="no-cache, must-revalidate"
+off=112 header_value complete
+off=112 len=7 span[header_field]="Expires"
+off=120 header_field complete
+off=121 len=29 span[header_value]="Mon, 26 Jul 1997 05:00:00 GMT"
+off=152 header_value complete
+off=152 len=10 span[header_field]=".et-Cookie"
+off=163 header_field complete
+off=164 len=54 span[header_value]="PlaxoCS=1274804622353690521; path=/; domain=.plaxo.com"
+off=220 header_value complete
+off=220 len=4 span[header_field]="Vary"
+off=225 header_field complete
+off=226 len=15 span[header_value]="Accept-Encoding"
+off=243 header_value complete
+off=243 len=10 span[header_field]="_eep-Alive"
+off=254 header_field complete
+off=255 len=10 span[header_value]="timeout=45"
+off=267 header_value complete
+off=267 len=10 span[header_field]="_onnection"
+off=278 header_field complete
+off=279 len=10 span[header_value]="Keep-Alive"
+off=291 header_value complete
+off=291 len=17 span[header_field]="Transfer-Encoding"
+off=309 header_field complete
+off=310 len=7 span[header_value]="chunked"
+off=319 header_value complete
+off=319 len=12 span[header_field]="Content-Type"
+off=332 header_field complete
+off=333 len=9 span[header_value]="text/html"
+off=344 header_value complete
+off=344 len=10 span[header_field]="Connection"
+off=355 header_field complete
+off=356 len=5 span[header_value]="close"
+off=363 header_value complete
+off=365 headers complete status=200 v=1/1 flags=20a content_length=0
+off=368 chunk header len=0
+off=370 chunk complete
+off=370 message complete
+```
+
+## Spaces in header name
+
+<!-- meta={"type": "response", "noScan": true} -->
+```http
+HTTP/1.1 200 OK
+Server: Microsoft-IIS/6.0
+X-Powered-By: ASP.NET
+en-US Content-Type: text/xml
+Content-Type: text/xml
+Content-Length: 16
+Date: Fri, 23 Jul 2010 18:45:38 GMT
+Connection: keep-alive
+
+<xml>hello</xml>
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=2 span[status]="OK"
+off=17 status complete
+off=17 len=6 span[header_field]="Server"
+off=24 header_field complete
+off=25 len=17 span[header_value]="Microsoft-IIS/6.0"
+off=44 header_value complete
+off=44 len=12 span[header_field]="X-Powered-By"
+off=57 header_field complete
+off=58 len=7 span[header_value]="ASP.NET"
+off=67 header_value complete
+off=72 error code=10 reason="Invalid header token"
+```
+
+## Non ASCII in status line
+
+<!-- meta={"type": "response", "noScan": true} -->
+```http
+HTTP/1.1 500 Oriëntatieprobleem
+Date: Fri, 5 Nov 2010 23:07:12 GMT+2
+Content-Length: 0
+Connection: close
+
+
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=19 span[status]="Oriëntatieprobleem"
+off=34 status complete
+off=34 len=4 span[header_field]="Date"
+off=39 header_field complete
+off=40 len=30 span[header_value]="Fri, 5 Nov 2010 23:07:12 GMT+2"
+off=72 header_value complete
+off=72 len=14 span[header_field]="Content-Length"
+off=87 header_field complete
+off=88 len=1 span[header_value]="0"
+off=91 header_value complete
+off=91 len=10 span[header_field]="Connection"
+off=102 header_field complete
+off=103 len=5 span[header_value]="close"
+off=110 header_value complete
+off=112 headers complete status=500 v=1/1 flags=22 content_length=0
+off=112 message complete
+```
+
+## HTTP version 0.9
+
+<!-- meta={"type": "response"} -->
+```http
+HTTP/0.9 200 OK
+
+
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="0.9"
+off=8 version complete
+off=13 len=2 span[status]="OK"
+off=17 status complete
+off=19 headers complete status=200 v=0/9 flags=0 content_length=0
+```
+
+## No Content-Length, no Transfer-Encoding
+
+The client should wait for the server's EOF. That is, when neither
+content-length nor transfer-encoding is specified, the end of body
+is specified by the EOF.
+
+<!-- meta={"type": "response"} -->
+```http
+HTTP/1.1 200 OK
+Content-Type: text/plain
+
+hello world
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=2 span[status]="OK"
+off=17 status complete
+off=17 len=12 span[header_field]="Content-Type"
+off=30 header_field complete
+off=31 len=10 span[header_value]="text/plain"
+off=43 header_value complete
+off=45 headers complete status=200 v=1/1 flags=0 content_length=0
+off=45 len=11 span[body]="hello world"
+```
+
+## Response starting with CRLF
+
+<!-- meta={"type": "response"} -->
+```http
+\r\nHTTP/1.1 200 OK
+Header1: Value1
+Header2:\t Value2
+Content-Length: 0
+
+
+```
+
+```log
+off=2 message begin
+off=7 len=3 span[version]="1.1"
+off=10 version complete
+off=15 len=2 span[status]="OK"
+off=19 status complete
+off=19 len=7 span[header_field]="Header1"
+off=27 header_field complete
+off=28 len=6 span[header_value]="Value1"
+off=36 header_value complete
+off=36 len=7 span[header_field]="Header2"
+off=44 header_field complete
+off=46 len=6 span[header_value]="Value2"
+off=54 header_value complete
+off=54 len=14 span[header_field]="Content-Length"
+off=69 header_field complete
+off=70 len=1 span[header_value]="0"
+off=73 header_value complete
+off=75 headers complete status=200 v=1/1 flags=20 content_length=0
+off=75 message complete
+```
diff --git a/llhttp/test/response/transfer-encoding.md b/llhttp/test/response/transfer-encoding.md
new file mode 100644
index 0000000..e1fd10a
--- /dev/null
+++ b/llhttp/test/response/transfer-encoding.md
@@ -0,0 +1,410 @@
+Transfer-Encoding header
+========================
+
+## Trailing space on chunked body
+
+<!-- meta={"type": "response"} -->
+```http
+HTTP/1.1 200 OK
+Content-Type: text/plain
+Transfer-Encoding: chunked
+
+25 \r\n\
+This is the data in the first chunk
+
+1C
+and this is the second one
+
+0 \r\n\
+
+
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=2 span[status]="OK"
+off=17 status complete
+off=17 len=12 span[header_field]="Content-Type"
+off=30 header_field complete
+off=31 len=10 span[header_value]="text/plain"
+off=43 header_value complete
+off=43 len=17 span[header_field]="Transfer-Encoding"
+off=61 header_field complete
+off=62 len=7 span[header_value]="chunked"
+off=71 header_value complete
+off=73 headers complete status=200 v=1/1 flags=208 content_length=0
+off=76 error code=12 reason="Invalid character in chunk size"
+```
+
+## `chunked` before other transfer-encoding
+
+<!-- meta={"type": "response"} -->
+```http
+HTTP/1.1 200 OK
+Accept: */*
+Transfer-Encoding: chunked, deflate
+
+World
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=2 span[status]="OK"
+off=17 status complete
+off=17 len=6 span[header_field]="Accept"
+off=24 header_field complete
+off=25 len=3 span[header_value]="*/*"
+off=30 header_value complete
+off=30 len=17 span[header_field]="Transfer-Encoding"
+off=48 header_field complete
+off=49 len=16 span[header_value]="chunked, deflate"
+off=67 header_value complete
+off=69 headers complete status=200 v=1/1 flags=200 content_length=0
+off=69 len=5 span[body]="World"
+```
+
+## multiple transfer-encoding where chunked is not the last one
+
+<!-- meta={"type": "response"} -->
+```http
+HTTP/1.1 200 OK
+Accept: */*
+Transfer-Encoding: chunked
+Transfer-Encoding: identity
+
+World
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=2 span[status]="OK"
+off=17 status complete
+off=17 len=6 span[header_field]="Accept"
+off=24 header_field complete
+off=25 len=3 span[header_value]="*/*"
+off=30 header_value complete
+off=30 len=17 span[header_field]="Transfer-Encoding"
+off=48 header_field complete
+off=49 len=7 span[header_value]="chunked"
+off=58 header_value complete
+off=58 len=17 span[header_field]="Transfer-Encoding"
+off=76 header_field complete
+off=77 len=8 span[header_value]="identity"
+off=87 header_value complete
+off=89 headers complete status=200 v=1/1 flags=200 content_length=0
+off=89 len=5 span[body]="World"
+```
+
+## `chunkedchunked` transfer-encoding does not enable chunked enconding
+
+This check that the word `chunked` repeat more than once (with or without spaces) does not mistakenly enables chunked encoding.
+
+<!-- meta={"type": "response"} -->
+```http
+HTTP/1.1 200 OK
+Accept: */*
+Transfer-Encoding: chunkedchunked
+
+2
+OK
+0
+
+
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=2 span[status]="OK"
+off=17 status complete
+off=17 len=6 span[header_field]="Accept"
+off=24 header_field complete
+off=25 len=3 span[header_value]="*/*"
+off=30 header_value complete
+off=30 len=17 span[header_field]="Transfer-Encoding"
+off=48 header_field complete
+off=49 len=14 span[header_value]="chunkedchunked"
+off=65 header_value complete
+off=67 headers complete status=200 v=1/1 flags=200 content_length=0
+off=67 len=1 span[body]="2"
+off=68 len=1 span[body]=cr
+off=69 len=1 span[body]=lf
+off=70 len=2 span[body]="OK"
+off=72 len=1 span[body]=cr
+off=73 len=1 span[body]=lf
+off=74 len=1 span[body]="0"
+off=75 len=1 span[body]=cr
+off=76 len=1 span[body]=lf
+off=77 len=1 span[body]=cr
+off=78 len=1 span[body]=lf
+```
+
+## Chunk extensions
+
+<!-- meta={"type": "response"} -->
+```http
+HTTP/1.1 200 OK
+Host: localhost
+Transfer-encoding: chunked
+
+5;ilovew3;somuchlove=aretheseparametersfor
+hello
+6;blahblah;blah
+ world
+0
+
+
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=2 span[status]="OK"
+off=17 status complete
+off=17 len=4 span[header_field]="Host"
+off=22 header_field complete
+off=23 len=9 span[header_value]="localhost"
+off=34 header_value complete
+off=34 len=17 span[header_field]="Transfer-encoding"
+off=52 header_field complete
+off=53 len=7 span[header_value]="chunked"
+off=62 header_value complete
+off=64 headers complete status=200 v=1/1 flags=208 content_length=0
+off=66 len=7 span[chunk_extension_name]="ilovew3"
+off=74 chunk_extension_name complete
+off=74 len=10 span[chunk_extension_name]="somuchlove"
+off=85 chunk_extension_name complete
+off=85 len=21 span[chunk_extension_value]="aretheseparametersfor"
+off=107 chunk_extension_value complete
+off=108 chunk header len=5
+off=108 len=5 span[body]="hello"
+off=115 chunk complete
+off=117 len=8 span[chunk_extension_name]="blahblah"
+off=126 chunk_extension_name complete
+off=126 len=4 span[chunk_extension_name]="blah"
+off=131 chunk_extension_name complete
+off=132 chunk header len=6
+off=132 len=6 span[body]=" world"
+off=140 chunk complete
+off=143 chunk header len=0
+off=145 chunk complete
+off=145 message complete
+```
+
+## No semicolon before chunk extensions
+
+<!-- meta={"type": "response"} -->
+```http
+HTTP/1.1 200 OK
+Host: localhost
+Transfer-encoding: chunked
+
+2 erfrferferf
+aa
+0 rrrr
+
+
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=2 span[status]="OK"
+off=17 status complete
+off=17 len=4 span[header_field]="Host"
+off=22 header_field complete
+off=23 len=9 span[header_value]="localhost"
+off=34 header_value complete
+off=34 len=17 span[header_field]="Transfer-encoding"
+off=52 header_field complete
+off=53 len=7 span[header_value]="chunked"
+off=62 header_value complete
+off=64 headers complete status=200 v=1/1 flags=208 content_length=0
+off=66 error code=12 reason="Invalid character in chunk size"
+```
+
+
+## No extension after semicolon
+
+<!-- meta={"type": "response"} -->
+```http
+HTTP/1.1 200 OK
+Host: localhost
+Transfer-encoding: chunked
+
+2;
+aa
+0
+
+
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=2 span[status]="OK"
+off=17 status complete
+off=17 len=4 span[header_field]="Host"
+off=22 header_field complete
+off=23 len=9 span[header_value]="localhost"
+off=34 header_value complete
+off=34 len=17 span[header_field]="Transfer-encoding"
+off=52 header_field complete
+off=53 len=7 span[header_value]="chunked"
+off=62 header_value complete
+off=64 headers complete status=200 v=1/1 flags=208 content_length=0
+off=67 error code=2 reason="Invalid character in chunk extensions"
+```
+
+
+## Chunk extensions quoting
+
+<!-- meta={"type": "response"} -->
+```http
+HTTP/1.1 200 OK
+Host: localhost
+Transfer-Encoding: chunked
+
+5;ilovew3="I love; extensions";somuchlove="aretheseparametersfor";blah;foo=bar
+hello
+6;blahblah;blah
+ world
+0
+
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=2 span[status]="OK"
+off=17 status complete
+off=17 len=4 span[header_field]="Host"
+off=22 header_field complete
+off=23 len=9 span[header_value]="localhost"
+off=34 header_value complete
+off=34 len=17 span[header_field]="Transfer-Encoding"
+off=52 header_field complete
+off=53 len=7 span[header_value]="chunked"
+off=62 header_value complete
+off=64 headers complete status=200 v=1/1 flags=208 content_length=0
+off=66 len=7 span[chunk_extension_name]="ilovew3"
+off=74 chunk_extension_name complete
+off=74 len=20 span[chunk_extension_value]=""I love; extensions""
+off=94 chunk_extension_value complete
+off=95 len=10 span[chunk_extension_name]="somuchlove"
+off=106 chunk_extension_name complete
+off=106 len=23 span[chunk_extension_value]=""aretheseparametersfor""
+off=129 chunk_extension_value complete
+off=130 len=4 span[chunk_extension_name]="blah"
+off=135 chunk_extension_name complete
+off=135 len=3 span[chunk_extension_name]="foo"
+off=139 chunk_extension_name complete
+off=139 len=3 span[chunk_extension_value]="bar"
+off=143 chunk_extension_value complete
+off=144 chunk header len=5
+off=144 len=5 span[body]="hello"
+off=151 chunk complete
+off=153 len=8 span[chunk_extension_name]="blahblah"
+off=162 chunk_extension_name complete
+off=162 len=4 span[chunk_extension_name]="blah"
+off=167 chunk_extension_name complete
+off=168 chunk header len=6
+off=168 len=6 span[body]=" world"
+off=176 chunk complete
+off=179 chunk header len=0
+```
+
+
+## Unbalanced chunk extensions quoting
+
+<!-- meta={"type": "response"} -->
+```http
+HTTP/1.1 200 OK
+Host: localhost
+Transfer-Encoding: chunked
+
+5;ilovew3="abc";somuchlove="def; ghi
+hello
+6;blahblah;blah
+ world
+0
+
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=2 span[status]="OK"
+off=17 status complete
+off=17 len=4 span[header_field]="Host"
+off=22 header_field complete
+off=23 len=9 span[header_value]="localhost"
+off=34 header_value complete
+off=34 len=17 span[header_field]="Transfer-Encoding"
+off=52 header_field complete
+off=53 len=7 span[header_value]="chunked"
+off=62 header_value complete
+off=64 headers complete status=200 v=1/1 flags=208 content_length=0
+off=66 len=7 span[chunk_extension_name]="ilovew3"
+off=74 chunk_extension_name complete
+off=74 len=5 span[chunk_extension_value]=""abc""
+off=79 chunk_extension_value complete
+off=80 len=10 span[chunk_extension_name]="somuchlove"
+off=91 chunk_extension_name complete
+off=91 len=9 span[chunk_extension_value]=""def; ghi"
+off=101 error code=2 reason="Invalid character in chunk extensions quoted value"
+```
+
+
+## Invalid OBS fold after chunked value
+
+<!-- meta={"type": "response" } -->
+```http
+HTTP/1.1 200 OK
+Transfer-Encoding: chunked
+ abc
+
+5
+World
+0
+
+
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=2 span[status]="OK"
+off=17 status complete
+off=17 len=17 span[header_field]="Transfer-Encoding"
+off=35 header_field complete
+off=36 len=7 span[header_value]="chunked"
+off=45 len=5 span[header_value]=" abc"
+off=52 header_value complete
+off=54 headers complete status=200 v=1/1 flags=200 content_length=0
+off=54 len=1 span[body]="5"
+off=55 len=1 span[body]=cr
+off=56 len=1 span[body]=lf
+off=57 len=5 span[body]="World"
+off=62 len=1 span[body]=cr
+off=63 len=1 span[body]=lf
+off=64 len=1 span[body]="0"
+off=65 len=1 span[body]=cr
+off=66 len=1 span[body]=lf
+off=67 len=1 span[body]=cr
+off=68 len=1 span[body]=lf
+```
+
diff --git a/llhttp/test/url.md b/llhttp/test/url.md
new file mode 100644
index 0000000..13a1b01
--- /dev/null
+++ b/llhttp/test/url.md
@@ -0,0 +1,261 @@
+# URL tests
+
+## Absolute URL
+
+```url
+http://example.com/path?query=value#schema
+```
+
+```log
+off=0 len=4 span[url.schema]="http"
+off=7 len=11 span[url.host]="example.com"
+off=18 len=5 span[url.path]="/path"
+off=24 len=11 span[url.query]="query=value"
+off=36 len=6 span[url.fragment]="schema"
+```
+
+## Relative URL
+
+```url
+/path?query=value#schema
+```
+
+```log
+off=0 len=5 span[url.path]="/path"
+off=6 len=11 span[url.query]="query=value"
+off=18 len=6 span[url.fragment]="schema"
+```
+
+## Failing on broken schema
+
+<!-- meta={"noScan": true} -->
+```url
+schema:/path?query=value#schema
+```
+
+```log
+off=0 len=6 span[url.schema]="schema"
+off=8 error code=7 reason="Unexpected char in url schema"
+```
+
+## Proxy request
+
+```url
+http://hostname/
+```
+
+```log
+off=0 len=4 span[url.schema]="http"
+off=7 len=8 span[url.host]="hostname"
+off=15 len=1 span[url.path]="/"
+```
+
+## Proxy request with port
+
+```url
+http://hostname:444/
+```
+
+```log
+off=0 len=4 span[url.schema]="http"
+off=7 len=12 span[url.host]="hostname:444"
+off=19 len=1 span[url.path]="/"
+```
+
+## Proxy IPv6 request
+
+```url
+http://[1:2::3:4]/
+```
+
+```log
+off=0 len=4 span[url.schema]="http"
+off=7 len=10 span[url.host]="[1:2::3:4]"
+off=17 len=1 span[url.path]="/"
+```
+
+## Proxy IPv6 request with port
+
+```url
+http://[1:2::3:4]:67/
+```
+
+```log
+off=0 len=4 span[url.schema]="http"
+off=7 len=13 span[url.host]="[1:2::3:4]:67"
+off=20 len=1 span[url.path]="/"
+```
+
+## IPv4 in IPv6 address
+
+```url
+http://[2001:0000:0000:0000:0000:0000:1.9.1.1]/
+```
+
+```log
+off=0 len=4 span[url.schema]="http"
+off=7 len=39 span[url.host]="[2001:0000:0000:0000:0000:0000:1.9.1.1]"
+off=46 len=1 span[url.path]="/"
+```
+
+## Extra `?` in query string
+
+```url
+http://a.tbcdn.cn/p/fp/2010c/??fp-header-min.css,fp-base-min.css,\
+fp-channel-min.css,fp-product-min.css,fp-mall-min.css,fp-category-min.css,\
+fp-sub-min.css,fp-gdp4p-min.css,fp-css3-min.css,fp-misc-min.css?t=20101022.css
+```
+
+```log
+off=0 len=4 span[url.schema]="http"
+off=7 len=10 span[url.host]="a.tbcdn.cn"
+off=17 len=12 span[url.path]="/p/fp/2010c/"
+off=30 len=187 span[url.query]="?fp-header-min.css,fp-base-min.css,fp-channel-min.css,fp-product-min.css,fp-mall-min.css,fp-category-min.css,fp-sub-min.css,fp-gdp4p-min.css,fp-css3-min.css,fp-misc-min.css?t=20101022.css"
+```
+
+## URL encoded space
+
+```url
+/toto.html?toto=a%20b
+```
+
+```log
+off=0 len=10 span[url.path]="/toto.html"
+off=11 len=10 span[url.query]="toto=a%20b"
+```
+
+## URL fragment
+
+```url
+/toto.html#titi
+```
+
+```log
+off=0 len=10 span[url.path]="/toto.html"
+off=11 len=4 span[url.fragment]="titi"
+```
+
+## Complex URL fragment
+
+```url
+http://www.webmasterworld.com/r.cgi?f=21&d=8405&url=\
+http://www.example.com/index.html?foo=bar&hello=world#midpage
+```
+
+```log
+off=0 len=4 span[url.schema]="http"
+off=7 len=22 span[url.host]="www.webmasterworld.com"
+off=29 len=6 span[url.path]="/r.cgi"
+off=36 len=69 span[url.query]="f=21&d=8405&url=http://www.example.com/index.html?foo=bar&hello=world"
+off=106 len=7 span[url.fragment]="midpage"
+```
+
+## Complex URL from node.js url parser doc
+
+```url
+http://host.com:8080/p/a/t/h?query=string#hash
+```
+
+```log
+off=0 len=4 span[url.schema]="http"
+off=7 len=13 span[url.host]="host.com:8080"
+off=20 len=8 span[url.path]="/p/a/t/h"
+off=29 len=12 span[url.query]="query=string"
+off=42 len=4 span[url.fragment]="hash"
+```
+
+## Complex URL with basic auth from node.js url parser doc
+
+```url
+http://a:b@host.com:8080/p/a/t/h?query=string#hash
+```
+
+```log
+off=0 len=4 span[url.schema]="http"
+off=7 len=17 span[url.host]="a:b@host.com:8080"
+off=24 len=8 span[url.path]="/p/a/t/h"
+off=33 len=12 span[url.query]="query=string"
+off=46 len=4 span[url.fragment]="hash"
+```
+
+## Double `@`
+
+<!-- meta={"noScan": true} -->
+```url
+http://a:b@@hostname:443/
+```
+
+```log
+off=0 len=4 span[url.schema]="http"
+off=12 error code=7 reason="Double @ in url"
+```
+
+## Proxy basic auth with url encoded space
+
+```url
+http://a%20:b@host.com/
+```
+
+```log
+off=0 len=4 span[url.schema]="http"
+off=7 len=15 span[url.host]="a%20:b@host.com"
+off=22 len=1 span[url.path]="/"
+```
+
+## Proxy basic auth with unreserved chars
+
+```url
+http://a!;-_!=+$@host.com/
+```
+
+```log
+off=0 len=4 span[url.schema]="http"
+off=7 len=18 span[url.host]="a!;-_!=+$@host.com"
+off=25 len=1 span[url.path]="/"
+```
+
+## IPv6 address with Zone ID
+
+```url
+http://[fe80::a%25eth0]/
+```
+
+```log
+off=0 len=4 span[url.schema]="http"
+off=7 len=16 span[url.host]="[fe80::a%25eth0]"
+off=23 len=1 span[url.path]="/"
+```
+
+## IPv6 address with Zone ID, but `%` is not percent-encoded
+
+```url
+http://[fe80::a%eth0]/
+```
+
+```log
+off=0 len=4 span[url.schema]="http"
+off=7 len=14 span[url.host]="[fe80::a%eth0]"
+off=21 len=1 span[url.path]="/"
+```
+
+## Disallow tab in URL
+
+<!-- meta={ "noScan": true} -->
+```url
+/foo\tbar/
+```
+
+```log
+off=5 error code=7 reason="Invalid characters in url"
+```
+
+## Disallow form-feed in URL
+
+<!-- meta={ "noScan": true} -->
+```url
+/foo\fbar/
+```
+
+```log
+off=5 error code=7 reason="Invalid characters in url"
+```
diff --git a/llhttp/tsconfig.json b/llhttp/tsconfig.json
new file mode 100644
index 0000000..01ec7c2
--- /dev/null
+++ b/llhttp/tsconfig.json
@@ -0,0 +1,15 @@
+{
+ "compilerOptions": {
+ "strict": true,
+ "target": "es2017",
+ "module": "commonjs",
+ "moduleResolution": "node",
+ "outDir": "./lib",
+ "declaration": true,
+ "pretty": true,
+ "sourceMap": true
+ },
+ "include": [
+ "src/**/*.ts"
+ ]
+}
diff --git a/llhttp/tslint.json b/llhttp/tslint.json
new file mode 100644
index 0000000..b0aaf97
--- /dev/null
+++ b/llhttp/tslint.json
@@ -0,0 +1,14 @@
+{
+ "defaultSeverity": "error",
+ "extends": [
+ "tslint:recommended"
+ ],
+ "jsRules": {},
+ "rules": {
+ "no-bitwise": null,
+ "quotemark": [
+ true, "single", "avoid-escape", "avoid-template"
+ ]
+ },
+ "rulesDirectory": []
+}
diff --git a/llparse-builder/.gitignore b/llparse-builder/.gitignore
new file mode 100644
index 0000000..5e67ab3
--- /dev/null
+++ b/llparse-builder/.gitignore
@@ -0,0 +1,3 @@
+node_modules/
+npm-debug.log
+lib/
diff --git a/llparse-builder/.travis.yml b/llparse-builder/.travis.yml
new file mode 100644
index 0000000..b5efd79
--- /dev/null
+++ b/llparse-builder/.travis.yml
@@ -0,0 +1,4 @@
+sudo: false
+language: node_js
+node_js:
+ - "stable"
diff --git a/llparse-builder/README.md b/llparse-builder/README.md
new file mode 100644
index 0000000..522fba2
--- /dev/null
+++ b/llparse-builder/README.md
@@ -0,0 +1,32 @@
+# llparse-builder
+[![Build Status](https://secure.travis-ci.org/indutny/llparse-builder.svg)](http://travis-ci.org/indutny/llparse-builder)
+[![NPM version](https://badge.fury.io/js/llparse-builder.svg)](https://badge.fury.io/js/llparse-builder)
+
+See [llparse][0].
+
+#### LICENSE
+
+This software is licensed under the MIT License.
+
+Copyright Fedor Indutny, 2018.
+
+Permission is hereby granted, free of charge, to any person obtaining a
+copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to permit
+persons to whom the Software is furnished to do so, subject to the
+following conditions:
+
+The above copyright notice and this permission notice shall be included
+in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
+NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
+USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+[0]: https://github.com/indutny/llparse
diff --git a/llparse-builder/package-lock.json b/llparse-builder/package-lock.json
new file mode 100644
index 0000000..5e76f34
--- /dev/null
+++ b/llparse-builder/package-lock.json
@@ -0,0 +1,1466 @@
+{
+ "name": "llparse-builder",
+ "version": "1.5.2",
+ "lockfileVersion": 1,
+ "requires": true,
+ "dependencies": {
+ "@babel/code-frame": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz",
+ "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==",
+ "dev": true,
+ "requires": {
+ "@babel/highlight": "^7.10.4"
+ }
+ },
+ "@babel/helper-validator-identifier": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz",
+ "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==",
+ "dev": true
+ },
+ "@babel/highlight": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz",
+ "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==",
+ "dev": true,
+ "requires": {
+ "@babel/helper-validator-identifier": "^7.10.4",
+ "chalk": "^2.0.0",
+ "js-tokens": "^4.0.0"
+ }
+ },
+ "@types/debug": {
+ "version": "4.1.5",
+ "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.5.tgz",
+ "integrity": "sha512-Q1y515GcOdTHgagaVFhHnIFQ38ygs/kmxdNpvpou+raI9UO3YZcHDngBSYKQklcKlvA7iuQlmIKbzvmxcOE9CQ=="
+ },
+ "@types/mocha": {
+ "version": "8.0.3",
+ "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-8.0.3.tgz",
+ "integrity": "sha512-vyxR57nv8NfcU0GZu8EUXZLTbCMupIUwy95LJ6lllN+JRPG25CwMHoB1q5xKh8YKhQnHYRAn4yW2yuHbf/5xgg==",
+ "dev": true
+ },
+ "@types/node": {
+ "version": "14.11.8",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-14.11.8.tgz",
+ "integrity": "sha512-KPcKqKm5UKDkaYPTuXSx8wEP7vE9GnuaXIZKijwRYcePpZFDVuy2a57LarFKiORbHOuTOOwYzxVxcUzsh2P2Pw==",
+ "dev": true
+ },
+ "ansi-colors": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz",
+ "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==",
+ "dev": true
+ },
+ "ansi-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
+ "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=",
+ "dev": true
+ },
+ "ansi-styles": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+ "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+ "dev": true,
+ "requires": {
+ "color-convert": "^1.9.0"
+ }
+ },
+ "anymatch": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz",
+ "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==",
+ "dev": true,
+ "requires": {
+ "normalize-path": "^3.0.0",
+ "picomatch": "^2.0.4"
+ }
+ },
+ "arg": {
+ "version": "4.1.3",
+ "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
+ "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
+ "dev": true
+ },
+ "argparse": {
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
+ "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
+ "dev": true,
+ "requires": {
+ "sprintf-js": "~1.0.2"
+ }
+ },
+ "array.prototype.map": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/array.prototype.map/-/array.prototype.map-1.0.2.tgz",
+ "integrity": "sha512-Az3OYxgsa1g7xDYp86l0nnN4bcmuEITGe1rbdEBVkrqkzMgDcbdQ2R7r41pNzti+4NMces3H8gMmuioZUilLgw==",
+ "dev": true,
+ "requires": {
+ "define-properties": "^1.1.3",
+ "es-abstract": "^1.17.0-next.1",
+ "es-array-method-boxes-properly": "^1.0.0",
+ "is-string": "^1.0.4"
+ }
+ },
+ "balanced-match": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
+ "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
+ "dev": true
+ },
+ "binary-extensions": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.1.0.tgz",
+ "integrity": "sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ==",
+ "dev": true
+ },
+ "binary-search": {
+ "version": "1.3.6",
+ "resolved": "https://registry.npmjs.org/binary-search/-/binary-search-1.3.6.tgz",
+ "integrity": "sha512-nbE1WxOTTrUWIfsfZ4aHGYu5DOuNkbxGokjV6Z2kxfJK3uaAb8zNK1muzOeipoLHZjInT4Br88BHpzevc681xA=="
+ },
+ "brace-expansion": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "dev": true,
+ "requires": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "braces": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
+ "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
+ "dev": true,
+ "requires": {
+ "fill-range": "^7.0.1"
+ }
+ },
+ "browser-stdout": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz",
+ "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==",
+ "dev": true
+ },
+ "buffer-from": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz",
+ "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==",
+ "dev": true
+ },
+ "builtin-modules": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz",
+ "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=",
+ "dev": true
+ },
+ "camelcase": {
+ "version": "5.3.1",
+ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
+ "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
+ "dev": true
+ },
+ "chalk": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.2.tgz",
+ "integrity": "sha512-ZM4j2/ld/YZDc3Ma8PgN7gyAk+kHMMMyzLNryCPGhWrsfAuDVeuid5bpRFTDgMH9JBK2lA4dyyAkkZYF/WcqDQ==",
+ "dev": true,
+ "requires": {
+ "ansi-styles": "^3.2.1",
+ "escape-string-regexp": "^1.0.5",
+ "supports-color": "^5.3.0"
+ },
+ "dependencies": {
+ "has-flag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+ "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
+ "dev": true
+ },
+ "supports-color": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.3.0.tgz",
+ "integrity": "sha512-0aP01LLIskjKs3lq52EC0aGBAJhLq7B2Rd8HC/DR/PtNNpcLilNmHC12O+hu0usQpo7wtHNRqtrhBwtDb0+dNg==",
+ "dev": true,
+ "requires": {
+ "has-flag": "^3.0.0"
+ }
+ }
+ }
+ },
+ "chokidar": {
+ "version": "3.4.2",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.4.2.tgz",
+ "integrity": "sha512-IZHaDeBeI+sZJRX7lGcXsdzgvZqKv6sECqsbErJA4mHWfpRrD8B97kSFN4cQz6nGBGiuFia1MKR4d6c1o8Cv7A==",
+ "dev": true,
+ "requires": {
+ "anymatch": "~3.1.1",
+ "braces": "~3.0.2",
+ "fsevents": "~2.1.2",
+ "glob-parent": "~5.1.0",
+ "is-binary-path": "~2.1.0",
+ "is-glob": "~4.0.1",
+ "normalize-path": "~3.0.0",
+ "readdirp": "~3.4.0"
+ }
+ },
+ "cliui": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz",
+ "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==",
+ "dev": true,
+ "requires": {
+ "string-width": "^3.1.0",
+ "strip-ansi": "^5.2.0",
+ "wrap-ansi": "^5.1.0"
+ },
+ "dependencies": {
+ "ansi-regex": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
+ "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
+ "dev": true
+ },
+ "string-width": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
+ "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==",
+ "dev": true,
+ "requires": {
+ "emoji-regex": "^7.0.1",
+ "is-fullwidth-code-point": "^2.0.0",
+ "strip-ansi": "^5.1.0"
+ }
+ },
+ "strip-ansi": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
+ "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
+ "dev": true,
+ "requires": {
+ "ansi-regex": "^4.1.0"
+ }
+ }
+ }
+ },
+ "color-convert": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.1.tgz",
+ "integrity": "sha512-mjGanIiwQJskCC18rPR6OmrZ6fm2Lc7PeGFYwCmy5J34wC6F1PzdGL6xeMfmgicfYcNLGuVFA3WzXtIDCQSZxQ==",
+ "dev": true,
+ "requires": {
+ "color-name": "^1.1.1"
+ }
+ },
+ "color-name": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
+ "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
+ "dev": true
+ },
+ "commander": {
+ "version": "2.15.1",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz",
+ "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==",
+ "dev": true
+ },
+ "concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
+ "dev": true
+ },
+ "debug": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz",
+ "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==",
+ "requires": {
+ "ms": "2.1.2"
+ }
+ },
+ "decamelize": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
+ "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=",
+ "dev": true
+ },
+ "define-properties": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz",
+ "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==",
+ "requires": {
+ "object-keys": "^1.0.12"
+ }
+ },
+ "diff": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
+ "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
+ "dev": true
+ },
+ "emoji-regex": {
+ "version": "7.0.3",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz",
+ "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==",
+ "dev": true
+ },
+ "es-abstract": {
+ "version": "1.17.7",
+ "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.7.tgz",
+ "integrity": "sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==",
+ "requires": {
+ "es-to-primitive": "^1.2.1",
+ "function-bind": "^1.1.1",
+ "has": "^1.0.3",
+ "has-symbols": "^1.0.1",
+ "is-callable": "^1.2.2",
+ "is-regex": "^1.1.1",
+ "object-inspect": "^1.8.0",
+ "object-keys": "^1.1.1",
+ "object.assign": "^4.1.1",
+ "string.prototype.trimend": "^1.0.1",
+ "string.prototype.trimstart": "^1.0.1"
+ },
+ "dependencies": {
+ "es-abstract": {
+ "version": "1.18.0-next.1",
+ "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.1.tgz",
+ "integrity": "sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA==",
+ "requires": {
+ "es-to-primitive": "^1.2.1",
+ "function-bind": "^1.1.1",
+ "has": "^1.0.3",
+ "has-symbols": "^1.0.1",
+ "is-callable": "^1.2.2",
+ "is-negative-zero": "^2.0.0",
+ "is-regex": "^1.1.1",
+ "object-inspect": "^1.8.0",
+ "object-keys": "^1.1.1",
+ "object.assign": "^4.1.1",
+ "string.prototype.trimend": "^1.0.1",
+ "string.prototype.trimstart": "^1.0.1"
+ }
+ },
+ "object.assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.1.tgz",
+ "integrity": "sha512-VT/cxmx5yaoHSOTSyrCygIDFco+RsibY2NM0a4RdEeY/4KgqezwFtK1yr3U67xYhqJSlASm2pKhLVzPj2lr4bA==",
+ "requires": {
+ "define-properties": "^1.1.3",
+ "es-abstract": "^1.18.0-next.0",
+ "has-symbols": "^1.0.1",
+ "object-keys": "^1.1.1"
+ },
+ "dependencies": {
+ "es-abstract": {
+ "version": "1.18.0-next.1",
+ "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.1.tgz",
+ "integrity": "sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA==",
+ "requires": {
+ "es-to-primitive": "^1.2.1",
+ "function-bind": "^1.1.1",
+ "has": "^1.0.3",
+ "has-symbols": "^1.0.1",
+ "is-callable": "^1.2.2",
+ "is-negative-zero": "^2.0.0",
+ "is-regex": "^1.1.1",
+ "object-inspect": "^1.8.0",
+ "object-keys": "^1.1.1",
+ "object.assign": "^4.1.1",
+ "string.prototype.trimend": "^1.0.1",
+ "string.prototype.trimstart": "^1.0.1"
+ }
+ }
+ }
+ }
+ }
+ },
+ "es-array-method-boxes-properly": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz",
+ "integrity": "sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA==",
+ "dev": true
+ },
+ "es-get-iterator": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.0.tgz",
+ "integrity": "sha512-UfrmHuWQlNMTs35e1ypnvikg6jCz3SK8v8ImvmDsh36fCVUR1MqoFDiyn0/k52C8NqO3YsO8Oe0azeesNuqSsQ==",
+ "dev": true,
+ "requires": {
+ "es-abstract": "^1.17.4",
+ "has-symbols": "^1.0.1",
+ "is-arguments": "^1.0.4",
+ "is-map": "^2.0.1",
+ "is-set": "^2.0.1",
+ "is-string": "^1.0.5",
+ "isarray": "^2.0.5"
+ }
+ },
+ "es-to-primitive": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz",
+ "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==",
+ "requires": {
+ "is-callable": "^1.1.4",
+ "is-date-object": "^1.0.1",
+ "is-symbol": "^1.0.2"
+ }
+ },
+ "escape-string-regexp": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+ "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=",
+ "dev": true
+ },
+ "esprima": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
+ "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
+ "dev": true
+ },
+ "fill-range": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
+ "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
+ "dev": true,
+ "requires": {
+ "to-regex-range": "^5.0.1"
+ }
+ },
+ "find-up": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+ "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+ "dev": true,
+ "requires": {
+ "locate-path": "^6.0.0",
+ "path-exists": "^4.0.0"
+ }
+ },
+ "flat": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/flat/-/flat-4.1.0.tgz",
+ "integrity": "sha512-Px/TiLIznH7gEDlPXcUD4KnBusa6kR6ayRUVcnEAbreRIuhkqow/mun59BuRXwoYk7ZQOLW1ZM05ilIvK38hFw==",
+ "dev": true,
+ "requires": {
+ "is-buffer": "~2.0.3"
+ }
+ },
+ "fs.realpath": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+ "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
+ "dev": true
+ },
+ "fsevents": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz",
+ "integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==",
+ "dev": true,
+ "optional": true
+ },
+ "function-bind": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
+ "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
+ },
+ "get-caller-file": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
+ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
+ "dev": true
+ },
+ "glob": {
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
+ "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==",
+ "dev": true,
+ "requires": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.0.4",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ }
+ },
+ "glob-parent": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz",
+ "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==",
+ "dev": true,
+ "requires": {
+ "is-glob": "^4.0.1"
+ }
+ },
+ "growl": {
+ "version": "1.10.5",
+ "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz",
+ "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==",
+ "dev": true
+ },
+ "has": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
+ "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
+ "requires": {
+ "function-bind": "^1.1.1"
+ }
+ },
+ "has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true
+ },
+ "has-symbols": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz",
+ "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg=="
+ },
+ "he": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
+ "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
+ "dev": true
+ },
+ "inflight": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+ "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
+ "dev": true,
+ "requires": {
+ "once": "^1.3.0",
+ "wrappy": "1"
+ }
+ },
+ "inherits": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
+ "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=",
+ "dev": true
+ },
+ "is-arguments": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.0.4.tgz",
+ "integrity": "sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA==",
+ "dev": true
+ },
+ "is-binary-path": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+ "dev": true,
+ "requires": {
+ "binary-extensions": "^2.0.0"
+ }
+ },
+ "is-buffer": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.4.tgz",
+ "integrity": "sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A==",
+ "dev": true
+ },
+ "is-callable": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.2.tgz",
+ "integrity": "sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA=="
+ },
+ "is-date-object": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.2.tgz",
+ "integrity": "sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g=="
+ },
+ "is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
+ "dev": true
+ },
+ "is-fullwidth-code-point": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
+ "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=",
+ "dev": true
+ },
+ "is-glob": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz",
+ "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==",
+ "dev": true,
+ "requires": {
+ "is-extglob": "^2.1.1"
+ }
+ },
+ "is-map": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.1.tgz",
+ "integrity": "sha512-T/S49scO8plUiAOA2DBTBG3JHpn1yiw0kRp6dgiZ0v2/6twi5eiB0rHtHFH9ZIrvlWc6+4O+m4zg5+Z833aXgw==",
+ "dev": true
+ },
+ "is-negative-zero": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.0.tgz",
+ "integrity": "sha1-lVOxIbD6wohp2p7UWeIMdUN4hGE="
+ },
+ "is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "dev": true
+ },
+ "is-plain-obj": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz",
+ "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=",
+ "dev": true
+ },
+ "is-regex": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz",
+ "integrity": "sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==",
+ "requires": {
+ "has-symbols": "^1.0.1"
+ }
+ },
+ "is-set": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.1.tgz",
+ "integrity": "sha512-eJEzOtVyenDs1TMzSQ3kU3K+E0GUS9sno+F0OBT97xsgcJsF9nXMBtkT9/kut5JEpM7oL7X/0qxR17K3mcwIAA==",
+ "dev": true
+ },
+ "is-string": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.5.tgz",
+ "integrity": "sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ==",
+ "dev": true
+ },
+ "is-symbol": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz",
+ "integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==",
+ "requires": {
+ "has-symbols": "^1.0.1"
+ }
+ },
+ "isarray": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
+ "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==",
+ "dev": true
+ },
+ "isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=",
+ "dev": true
+ },
+ "iterate-iterator": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/iterate-iterator/-/iterate-iterator-1.0.1.tgz",
+ "integrity": "sha512-3Q6tudGN05kbkDQDI4CqjaBf4qf85w6W6GnuZDtUVYwKgtC1q8yxYX7CZed7N+tLzQqS6roujWvszf13T+n9aw==",
+ "dev": true
+ },
+ "iterate-value": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/iterate-value/-/iterate-value-1.0.2.tgz",
+ "integrity": "sha512-A6fMAio4D2ot2r/TYzr4yUWrmwNdsN5xL7+HUiyACE4DXm+q8HtPcnFTp+NnW3k4N05tZ7FVYFFb2CR13NxyHQ==",
+ "dev": true,
+ "requires": {
+ "es-get-iterator": "^1.0.2",
+ "iterate-iterator": "^1.0.1"
+ }
+ },
+ "js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "dev": true
+ },
+ "js-yaml": {
+ "version": "3.14.0",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.0.tgz",
+ "integrity": "sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A==",
+ "dev": true,
+ "requires": {
+ "argparse": "^1.0.7",
+ "esprima": "^4.0.0"
+ }
+ },
+ "locate-path": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+ "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+ "dev": true,
+ "requires": {
+ "p-locate": "^5.0.0"
+ }
+ },
+ "log-symbols": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.0.0.tgz",
+ "integrity": "sha512-FN8JBzLx6CzeMrB0tg6pqlGU1wCrXW+ZXGH481kfsBqer0hToTIiHdjH4Mq8xJUbvATujKCvaREGWpGUionraA==",
+ "dev": true,
+ "requires": {
+ "chalk": "^4.0.0"
+ },
+ "dependencies": {
+ "ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "requires": {
+ "color-convert": "^2.0.1"
+ }
+ },
+ "chalk": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
+ "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==",
+ "dev": true,
+ "requires": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ }
+ },
+ "color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "requires": {
+ "color-name": "~1.1.4"
+ }
+ },
+ "color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ }
+ }
+ },
+ "make-error": {
+ "version": "1.3.6",
+ "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
+ "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
+ "dev": true
+ },
+ "minimatch": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
+ "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
+ "dev": true,
+ "requires": {
+ "brace-expansion": "^1.1.7"
+ }
+ },
+ "mkdirp": {
+ "version": "0.5.5",
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
+ "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
+ "dev": true,
+ "requires": {
+ "minimist": "^1.2.5"
+ },
+ "dependencies": {
+ "minimist": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
+ "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
+ "dev": true
+ }
+ }
+ },
+ "mocha": {
+ "version": "8.1.3",
+ "resolved": "https://registry.npmjs.org/mocha/-/mocha-8.1.3.tgz",
+ "integrity": "sha512-ZbaYib4hT4PpF4bdSO2DohooKXIn4lDeiYqB+vTmCdr6l2woW0b6H3pf5x4sM5nwQMru9RvjjHYWVGltR50ZBw==",
+ "dev": true,
+ "requires": {
+ "ansi-colors": "4.1.1",
+ "browser-stdout": "1.3.1",
+ "chokidar": "3.4.2",
+ "debug": "4.1.1",
+ "diff": "4.0.2",
+ "escape-string-regexp": "4.0.0",
+ "find-up": "5.0.0",
+ "glob": "7.1.6",
+ "growl": "1.10.5",
+ "he": "1.2.0",
+ "js-yaml": "3.14.0",
+ "log-symbols": "4.0.0",
+ "minimatch": "3.0.4",
+ "ms": "2.1.2",
+ "object.assign": "4.1.0",
+ "promise.allsettled": "1.0.2",
+ "serialize-javascript": "4.0.0",
+ "strip-json-comments": "3.0.1",
+ "supports-color": "7.1.0",
+ "which": "2.0.2",
+ "wide-align": "1.1.3",
+ "workerpool": "6.0.0",
+ "yargs": "13.3.2",
+ "yargs-parser": "13.1.2",
+ "yargs-unparser": "1.6.1"
+ },
+ "dependencies": {
+ "debug": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
+ "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
+ "dev": true,
+ "requires": {
+ "ms": "^2.1.1"
+ }
+ },
+ "diff": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
+ "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
+ "dev": true
+ },
+ "escape-string-regexp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "dev": true
+ },
+ "glob": {
+ "version": "7.1.6",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
+ "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
+ "dev": true,
+ "requires": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.0.4",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ }
+ }
+ }
+ },
+ "ms": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
+ },
+ "normalize-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+ "dev": true
+ },
+ "object-inspect": {
+ "version": "1.8.0",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.8.0.tgz",
+ "integrity": "sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA=="
+ },
+ "object-keys": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
+ "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="
+ },
+ "object.assign": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz",
+ "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==",
+ "dev": true,
+ "requires": {
+ "define-properties": "^1.1.2",
+ "function-bind": "^1.1.1",
+ "has-symbols": "^1.0.0",
+ "object-keys": "^1.0.11"
+ }
+ },
+ "once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
+ "dev": true,
+ "requires": {
+ "wrappy": "1"
+ }
+ },
+ "p-limit": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.0.2.tgz",
+ "integrity": "sha512-iwqZSOoWIW+Ew4kAGUlN16J4M7OB3ysMLSZtnhmqx7njIHFPlxWBX8xo3lVTyFVq6mI/lL9qt2IsN1sHwaxJkg==",
+ "dev": true,
+ "requires": {
+ "p-try": "^2.0.0"
+ }
+ },
+ "p-locate": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+ "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+ "dev": true,
+ "requires": {
+ "p-limit": "^3.0.2"
+ }
+ },
+ "p-try": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
+ "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
+ "dev": true
+ },
+ "path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "dev": true
+ },
+ "path-is-absolute": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+ "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
+ "dev": true
+ },
+ "path-parse": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz",
+ "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==",
+ "dev": true
+ },
+ "picomatch": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz",
+ "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==",
+ "dev": true
+ },
+ "promise.allsettled": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/promise.allsettled/-/promise.allsettled-1.0.2.tgz",
+ "integrity": "sha512-UpcYW5S1RaNKT6pd+s9jp9K9rlQge1UXKskec0j6Mmuq7UJCvlS2J2/s/yuPN8ehftf9HXMxWlKiPbGGUzpoRg==",
+ "dev": true,
+ "requires": {
+ "array.prototype.map": "^1.0.1",
+ "define-properties": "^1.1.3",
+ "es-abstract": "^1.17.0-next.1",
+ "function-bind": "^1.1.1",
+ "iterate-value": "^1.0.0"
+ }
+ },
+ "randombytes": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
+ "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
+ "dev": true,
+ "requires": {
+ "safe-buffer": "^5.1.0"
+ }
+ },
+ "readdirp": {
+ "version": "3.4.0",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.4.0.tgz",
+ "integrity": "sha512-0xe001vZBnJEK+uKcj8qOhyAKPzIT+gStxWr3LCB0DwcXR5NZJ3IaC+yGnHCYzB/S7ov3m3EEbZI2zeNvX+hGQ==",
+ "dev": true,
+ "requires": {
+ "picomatch": "^2.2.1"
+ }
+ },
+ "require-directory": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
+ "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=",
+ "dev": true
+ },
+ "require-main-filename": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
+ "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
+ "dev": true
+ },
+ "resolve": {
+ "version": "1.17.0",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz",
+ "integrity": "sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==",
+ "dev": true,
+ "requires": {
+ "path-parse": "^1.0.6"
+ }
+ },
+ "safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "dev": true
+ },
+ "semver": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
+ "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
+ "dev": true
+ },
+ "serialize-javascript": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz",
+ "integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==",
+ "dev": true,
+ "requires": {
+ "randombytes": "^2.1.0"
+ }
+ },
+ "set-blocking": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
+ "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=",
+ "dev": true
+ },
+ "source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "dev": true
+ },
+ "source-map-support": {
+ "version": "0.5.19",
+ "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz",
+ "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==",
+ "dev": true,
+ "requires": {
+ "buffer-from": "^1.0.0",
+ "source-map": "^0.6.0"
+ }
+ },
+ "sprintf-js": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
+ "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=",
+ "dev": true
+ },
+ "string-width": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz",
+ "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==",
+ "dev": true,
+ "requires": {
+ "is-fullwidth-code-point": "^2.0.0",
+ "strip-ansi": "^4.0.0"
+ }
+ },
+ "string.prototype.trimend": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz",
+ "integrity": "sha512-LRPxFUaTtpqYsTeNKaFOw3R4bxIzWOnbQ837QfBylo8jIxtcbK/A/sMV7Q+OAV/vWo+7s25pOE10KYSjaSO06g==",
+ "requires": {
+ "define-properties": "^1.1.3",
+ "es-abstract": "^1.17.5"
+ }
+ },
+ "string.prototype.trimstart": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz",
+ "integrity": "sha512-XxZn+QpvrBI1FOcg6dIpxUPgWCPuNXvMD72aaRaUQv1eD4e/Qy8i/hFTe0BUmD60p/QA6bh1avmuPTfNjqVWRw==",
+ "requires": {
+ "define-properties": "^1.1.3",
+ "es-abstract": "^1.17.5"
+ }
+ },
+ "strip-ansi": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
+ "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
+ "dev": true,
+ "requires": {
+ "ansi-regex": "^3.0.0"
+ }
+ },
+ "strip-json-comments": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.0.1.tgz",
+ "integrity": "sha512-VTyMAUfdm047mwKl+u79WIdrZxtFtn+nBxHeb844XBQ9uMNTuTHdx2hc5RiAJYqwTj3wc/xe5HLSdJSkJ+WfZw==",
+ "dev": true
+ },
+ "supports-color": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz",
+ "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==",
+ "dev": true,
+ "requires": {
+ "has-flag": "^4.0.0"
+ }
+ },
+ "to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "dev": true,
+ "requires": {
+ "is-number": "^7.0.0"
+ }
+ },
+ "ts-node": {
+ "version": "9.0.0",
+ "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-9.0.0.tgz",
+ "integrity": "sha512-/TqB4SnererCDR/vb4S/QvSZvzQMJN8daAslg7MeaiHvD8rDZsSfXmNeNumyZZzMned72Xoq/isQljYSt8Ynfg==",
+ "dev": true,
+ "requires": {
+ "arg": "^4.1.0",
+ "diff": "^4.0.1",
+ "make-error": "^1.1.1",
+ "source-map-support": "^0.5.17",
+ "yn": "3.1.1"
+ }
+ },
+ "tslib": {
+ "version": "1.13.0",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz",
+ "integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==",
+ "dev": true
+ },
+ "tslint": {
+ "version": "5.20.1",
+ "resolved": "https://registry.npmjs.org/tslint/-/tslint-5.20.1.tgz",
+ "integrity": "sha512-EcMxhzCFt8k+/UP5r8waCf/lzmeSyVlqxqMEDQE7rWYiQky8KpIBz1JAoYXfROHrPZ1XXd43q8yQnULOLiBRQg==",
+ "dev": true,
+ "requires": {
+ "@babel/code-frame": "^7.0.0",
+ "builtin-modules": "^1.1.1",
+ "chalk": "^2.3.0",
+ "commander": "^2.12.1",
+ "diff": "^4.0.1",
+ "glob": "^7.1.1",
+ "js-yaml": "^3.13.1",
+ "minimatch": "^3.0.4",
+ "mkdirp": "^0.5.1",
+ "resolve": "^1.3.2",
+ "semver": "^5.3.0",
+ "tslib": "^1.8.0",
+ "tsutils": "^2.29.0"
+ },
+ "dependencies": {
+ "diff": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
+ "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
+ "dev": true
+ }
+ }
+ },
+ "tsutils": {
+ "version": "2.29.0",
+ "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.29.0.tgz",
+ "integrity": "sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA==",
+ "dev": true,
+ "requires": {
+ "tslib": "^1.8.1"
+ }
+ },
+ "typescript": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.0.3.tgz",
+ "integrity": "sha512-tEu6DGxGgRJPb/mVPIZ48e69xCn2yRmCgYmDugAVwmJ6o+0u1RI18eO7E7WBTLYLaEVVOhwQmcdhQHweux/WPg==",
+ "dev": true
+ },
+ "which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dev": true,
+ "requires": {
+ "isexe": "^2.0.0"
+ }
+ },
+ "which-module": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz",
+ "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=",
+ "dev": true
+ },
+ "wide-align": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz",
+ "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==",
+ "dev": true,
+ "requires": {
+ "string-width": "^1.0.2 || 2"
+ }
+ },
+ "workerpool": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.0.0.tgz",
+ "integrity": "sha512-fU2OcNA/GVAJLLyKUoHkAgIhKb0JoCpSjLC/G2vYKxUjVmQwGbRVeoPJ1a8U4pnVofz4AQV5Y/NEw8oKqxEBtA==",
+ "dev": true
+ },
+ "wrap-ansi": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz",
+ "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==",
+ "dev": true,
+ "requires": {
+ "ansi-styles": "^3.2.0",
+ "string-width": "^3.0.0",
+ "strip-ansi": "^5.0.0"
+ },
+ "dependencies": {
+ "ansi-regex": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
+ "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
+ "dev": true
+ },
+ "string-width": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
+ "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==",
+ "dev": true,
+ "requires": {
+ "emoji-regex": "^7.0.1",
+ "is-fullwidth-code-point": "^2.0.0",
+ "strip-ansi": "^5.1.0"
+ }
+ },
+ "strip-ansi": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
+ "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
+ "dev": true,
+ "requires": {
+ "ansi-regex": "^4.1.0"
+ }
+ }
+ }
+ },
+ "wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
+ "dev": true
+ },
+ "y18n": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz",
+ "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==",
+ "dev": true
+ },
+ "yargs": {
+ "version": "13.3.2",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz",
+ "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==",
+ "dev": true,
+ "requires": {
+ "cliui": "^5.0.0",
+ "find-up": "^3.0.0",
+ "get-caller-file": "^2.0.1",
+ "require-directory": "^2.1.1",
+ "require-main-filename": "^2.0.0",
+ "set-blocking": "^2.0.0",
+ "string-width": "^3.0.0",
+ "which-module": "^2.0.0",
+ "y18n": "^4.0.0",
+ "yargs-parser": "^13.1.2"
+ },
+ "dependencies": {
+ "ansi-regex": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
+ "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
+ "dev": true
+ },
+ "find-up": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz",
+ "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==",
+ "dev": true,
+ "requires": {
+ "locate-path": "^3.0.0"
+ }
+ },
+ "locate-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz",
+ "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==",
+ "dev": true,
+ "requires": {
+ "p-locate": "^3.0.0",
+ "path-exists": "^3.0.0"
+ }
+ },
+ "p-limit": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
+ "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
+ "dev": true,
+ "requires": {
+ "p-try": "^2.0.0"
+ }
+ },
+ "p-locate": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz",
+ "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==",
+ "dev": true,
+ "requires": {
+ "p-limit": "^2.0.0"
+ }
+ },
+ "path-exists": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz",
+ "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=",
+ "dev": true
+ },
+ "string-width": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
+ "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==",
+ "dev": true,
+ "requires": {
+ "emoji-regex": "^7.0.1",
+ "is-fullwidth-code-point": "^2.0.0",
+ "strip-ansi": "^5.1.0"
+ }
+ },
+ "strip-ansi": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
+ "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
+ "dev": true,
+ "requires": {
+ "ansi-regex": "^4.1.0"
+ }
+ }
+ }
+ },
+ "yargs-parser": {
+ "version": "13.1.2",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz",
+ "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==",
+ "dev": true,
+ "requires": {
+ "camelcase": "^5.0.0",
+ "decamelize": "^1.2.0"
+ }
+ },
+ "yargs-unparser": {
+ "version": "1.6.1",
+ "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-1.6.1.tgz",
+ "integrity": "sha512-qZV14lK9MWsGCmcr7u5oXGH0dbGqZAIxTDrWXZDo5zUr6b6iUmelNKO6x6R1dQT24AH3LgRxJpr8meWy2unolA==",
+ "dev": true,
+ "requires": {
+ "camelcase": "^5.3.1",
+ "decamelize": "^1.2.0",
+ "flat": "^4.1.0",
+ "is-plain-obj": "^1.1.0",
+ "yargs": "^14.2.3"
+ },
+ "dependencies": {
+ "ansi-regex": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
+ "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
+ "dev": true
+ },
+ "find-up": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz",
+ "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==",
+ "dev": true,
+ "requires": {
+ "locate-path": "^3.0.0"
+ }
+ },
+ "locate-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz",
+ "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==",
+ "dev": true,
+ "requires": {
+ "p-locate": "^3.0.0",
+ "path-exists": "^3.0.0"
+ }
+ },
+ "p-limit": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
+ "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
+ "dev": true,
+ "requires": {
+ "p-try": "^2.0.0"
+ }
+ },
+ "p-locate": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz",
+ "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==",
+ "dev": true,
+ "requires": {
+ "p-limit": "^2.0.0"
+ }
+ },
+ "path-exists": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz",
+ "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=",
+ "dev": true
+ },
+ "string-width": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
+ "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==",
+ "dev": true,
+ "requires": {
+ "emoji-regex": "^7.0.1",
+ "is-fullwidth-code-point": "^2.0.0",
+ "strip-ansi": "^5.1.0"
+ }
+ },
+ "strip-ansi": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
+ "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
+ "dev": true,
+ "requires": {
+ "ansi-regex": "^4.1.0"
+ }
+ },
+ "yargs": {
+ "version": "14.2.3",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-14.2.3.tgz",
+ "integrity": "sha512-ZbotRWhF+lkjijC/VhmOT9wSgyBQ7+zr13+YLkhfsSiTriYsMzkTUFP18pFhWwBeMa5gUc1MzbhrO6/VB7c9Xg==",
+ "dev": true,
+ "requires": {
+ "cliui": "^5.0.0",
+ "decamelize": "^1.2.0",
+ "find-up": "^3.0.0",
+ "get-caller-file": "^2.0.1",
+ "require-directory": "^2.1.1",
+ "require-main-filename": "^2.0.0",
+ "set-blocking": "^2.0.0",
+ "string-width": "^3.0.0",
+ "which-module": "^2.0.0",
+ "y18n": "^4.0.0",
+ "yargs-parser": "^15.0.1"
+ }
+ },
+ "yargs-parser": {
+ "version": "15.0.1",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-15.0.1.tgz",
+ "integrity": "sha512-0OAMV2mAZQrs3FkNpDQcBk1x5HXb8X4twADss4S0Iuk+2dGnLOE/fRHrsYm542GduMveyA77OF4wrNJuanRCWw==",
+ "dev": true,
+ "requires": {
+ "camelcase": "^5.0.0",
+ "decamelize": "^1.2.0"
+ }
+ }
+ }
+ },
+ "yn": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
+ "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
+ "dev": true
+ }
+ }
+}
diff --git a/llparse-builder/package.json b/llparse-builder/package.json
new file mode 100644
index 0000000..1c1ac54
--- /dev/null
+++ b/llparse-builder/package.json
@@ -0,0 +1,48 @@
+{
+ "name": "llparse-builder",
+ "version": "1.5.2",
+ "description": "Build graph for consumption in LLParse",
+ "main": "lib/builder.js",
+ "types": "lib/builder.d.ts",
+ "files": [
+ "lib",
+ "src"
+ ],
+ "scripts": {
+ "build": "tsc",
+ "clean": "rm -rf lib",
+ "prepare": "npm run clean && npm run build",
+ "lint": "tslint -c tslint.json src/*.ts src/**/*.ts src/**/**/*.ts test/*.ts",
+ "mocha": "mocha -r ts-node/register/type-check --reporter spec test/*-test.ts",
+ "test": "npm run mocha && npm run lint"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+ssh://git@github.com/indutny/llparse-builder.git"
+ },
+ "keywords": [
+ "llparse",
+ "builder",
+ "llvm",
+ "bitcode"
+ ],
+ "author": "Fedor Indutny <fedor@indutny.com> (http://darksi.de/)",
+ "license": "MIT",
+ "bugs": {
+ "url": "https://github.com/indutny/llparse-builder/issues"
+ },
+ "homepage": "https://github.com/indutny/llparse-builder#readme",
+ "devDependencies": {
+ "@types/mocha": "^8.0.3",
+ "@types/node": "^14.11.8",
+ "mocha": "^8.1.3",
+ "ts-node": "^9.0.0",
+ "tslint": "^5.20.1",
+ "typescript": "^4.0.3"
+ },
+ "dependencies": {
+ "@types/debug": "4.1.5 ",
+ "binary-search": "^1.3.6",
+ "debug": "^4.2.0"
+ }
+}
diff --git a/llparse-builder/src/builder.ts b/llparse-builder/src/builder.ts
new file mode 100644
index 0000000..a335a85
--- /dev/null
+++ b/llparse-builder/src/builder.ts
@@ -0,0 +1,147 @@
+import * as code from './code';
+import * as node from './node';
+import { Property, PropertyType } from './property';
+import { Span } from './span';
+import * as transform from './transform';
+
+export { code, node, transform, Property, PropertyType, Span };
+export { Edge } from './edge';
+export { LoopChecker } from './loop-checker';
+export { ISpanAllocatorResult, SpanAllocator } from './span-allocator';
+export { Reachability } from './reachability';
+
+/**
+ * Construct parsing graph for later use in `llparse`.
+ */
+export class Builder {
+ /**
+ * API for creating external callbacks and intrinsic operations.
+ */
+ public readonly code: code.Creator = new code.Creator();
+
+ /**
+ * API for creating character transforms for use in nodes created with
+ * `builder.node()`
+ */
+ public readonly transform: transform.Creator = new transform.Creator();
+
+ private readonly privProperties: Map<string, Property> = new Map();
+
+ // Various nodes
+
+ /**
+ * Create regular node for matching characters and sequences.
+ *
+ * @param name Node name
+ */
+ public node(name: string): node.Match {
+ return new node.Match(name);
+ }
+
+ /**
+ * Create terminal error node. Returns error code to user, and sets reason
+ * in the parser's state object.
+ *
+ * This node does not consume any bytes upon execution.
+ *
+ * @param errorCode Integer error code
+ * @param reason Error description
+ */
+ public error(errorCode: number, reason: string): node.Error {
+ return new node.Error(errorCode, reason);
+ }
+
+ /**
+ * Create invoke node that calls either external user callback or an
+ * intrinsic operation.
+ *
+ * This node does not consume any bytes upon execution.
+ *
+ * NOTE: When `.invoke()` is a target of `node().select()` - callback must
+ * have signature that accepts `.select()`'s value, otherwise it must be of
+ * the signature that takes no such value.
+ *
+ * @param fn Code instance to invoke
+ * @param map Object with integer keys and `Node` values. Describes
+ * nodes that are visited upon receiving particular
+ * return integer value
+ * @param otherwise Convenience `Node` argument. Effect is the same as calling
+ * `p.invoke(...).otherwise(node)`
+ */
+ public invoke(fn: code.Code, map?: node.IInvokeMap | node.Node,
+ otherwise?: node.Node): node.Invoke {
+ let res: node.Invoke;
+
+ // `.invoke(name)`
+ if (map === undefined) {
+ res = new node.Invoke(fn, {});
+ // `.invoke(name, otherwise)`
+ } else if (map instanceof node.Node) {
+ res = new node.Invoke(fn, {});
+ otherwise = map;
+ } else {
+ res = new node.Invoke(fn, map as node.IInvokeMap);
+ }
+
+ if (otherwise !== undefined) {
+ res.otherwise(otherwise);
+ }
+ return res;
+ }
+
+ /**
+ * Create node that consumes number of bytes specified by value of the
+ * state's property with name in `field` argument.
+ *
+ * @param field Property name to use
+ */
+ public consume(field: string): node.Consume {
+ return new node.Consume(field);
+ }
+
+ /**
+ * Create non-terminal node that returns `errorCode` as error number to
+ * user, but still allows feeding more data to the parser.
+ *
+ * This node does not consume any bytes upon execution.
+ *
+ * @param errorCode Integer error code
+ * @param reason Error description
+ */
+ public pause(errorCode: number, reason: string): node.Pause {
+ return new node.Pause(errorCode, reason);
+ }
+
+ // Span
+
+ /**
+ * Create Span with given `callback`.
+ *
+ * @param callback External span callback, must be result of
+ * `.code.span(...)`
+ */
+ public span(callback: code.Span): Span {
+ return new Span(callback);
+ }
+
+ // Custom property API
+
+ /**
+ * Allocate space for property in parser's state.
+ */
+ public property(ty: PropertyType, name: string): void {
+ if (this.privProperties.has(name)) {
+ throw new Error(`Duplicate property with a name: "${name}"`);
+ }
+
+ const prop = new Property(ty, name);
+ this.privProperties.set(name, prop);
+ }
+
+ /**
+ * Return list of all allocated properties in parser's state.
+ */
+ public get properties(): ReadonlyArray<Property> {
+ return Array.from(this.privProperties.values());
+ }
+}
diff --git a/llparse-builder/src/code/and.ts b/llparse-builder/src/code/and.ts
new file mode 100644
index 0000000..5f78675
--- /dev/null
+++ b/llparse-builder/src/code/and.ts
@@ -0,0 +1,7 @@
+import { FieldValue } from './field-value';
+
+export class And extends FieldValue {
+ constructor(field: string, value: number) {
+ super('match', 'and', field, value);
+ }
+}
diff --git a/llparse-builder/src/code/base.ts b/llparse-builder/src/code/base.ts
new file mode 100644
index 0000000..00b479f
--- /dev/null
+++ b/llparse-builder/src/code/base.ts
@@ -0,0 +1,16 @@
+export type Signature = 'match' | 'value';
+
+/**
+ * Base code class.
+ */
+export abstract class Code {
+ /**
+ * @param signature Code signature to be used. `match` means that code takes
+ * no input value (from `.select()`), otherwise it must be
+ * `value`
+ * @param name External function or intrinsic name.
+ */
+ constructor(public readonly signature: Signature,
+ public readonly name: string) {
+ }
+}
diff --git a/llparse-builder/src/code/creator.ts b/llparse-builder/src/code/creator.ts
new file mode 100644
index 0000000..98f9296
--- /dev/null
+++ b/llparse-builder/src/code/creator.ts
@@ -0,0 +1,184 @@
+import * as code from './';
+
+/**
+ * API for creating external callbacks and intrinsic operations.
+ */
+export class Creator {
+ // Callbacks to external C functions
+
+ /**
+ * Create an external callback that **has no** `value` argument.
+ *
+ * This callback can be used in all `Invoke` nodes except those that are
+ * targets of `.select()` method.
+ *
+ * C signature of callback must be:
+ *
+ * ```c
+ * int name(llparse_t* state, const char* p, const char* endp)
+ * ```
+ *
+ * Where `llparse_t` is parser state's type name.
+ *
+ * @param name External function name.
+ */
+ public match(name: string): code.Match {
+ return new code.Match(name);
+ }
+
+ /**
+ * Create an external callback that **has** `value` argument.
+ *
+ * This callback can be used only in `Invoke` nodes that are targets of
+ * `.select()` method.
+ *
+ * C signature of callback must be:
+ *
+ * ```c
+ * int name(llparse_t* state, const char* p, const char* endp, int value)
+ * ```
+ *
+ * Where `llparse_t` is parser state's type name.
+ *
+ * @param name External function name.
+ */
+ public value(name: string): code.Value {
+ return new code.Value(name);
+ }
+
+ /**
+ * Create an external span callback.
+ *
+ * This callback can be used only in `Span` constructor.
+ *
+ * C signature of callback must be:
+ *
+ * ```c
+ * int name(llparse_t* state, const char* p, const char* endp)
+ * ```
+ *
+ * NOTE: non-zero return value is treated as resumable error.
+ *
+ * @param name External function name.
+ */
+ public span(name: string): code.Span {
+ return new code.Span(name);
+ }
+
+ // Helpers
+
+ /**
+ * Intrinsic operation. Stores `value` from `.select()` node into the state's
+ * property with the name specified by `field`, returns zero.
+ *
+ * state[field] = value;
+ * return 0;
+ *
+ * @param field Property name
+ */
+ public store(field: string): code.Store {
+ return new code.Store(field);
+ }
+
+ /**
+ * Intrinsic operation. Loads and returns state's property with the name
+ * specified by `field`.
+ *
+ * The value of the property is either truncated or zero-extended to fit into
+ * 32-bit unsigned integer.
+ *
+ * return state[field];
+ *
+ * @param field Property name.
+ */
+ public load(field: string): code.Load {
+ return new code.Load(field);
+ }
+
+ /**
+ * Intrinsic operation. Takes `value` from `.select()`, state's property
+ * with the name `field` and does:
+ *
+ * field = state[field];
+ * field *= options.base;
+ * field += value;
+ * state[field] = field;
+ * return 0; // or 1 on overflow
+ *
+ * Return values are:
+ *
+ * - 0 - success
+ * - 1 - overflow
+ *
+ * @param field Property name
+ * @param options See `code.MulAdd` documentation.
+ */
+ public mulAdd(field: string, options: code.IMulAddOptions): code.MulAdd {
+ return new code.MulAdd(field, options);
+ }
+
+ /**
+ * Intrinsic operation. Puts `value` integer into the state's property with
+ * the name specified by `field`.
+ *
+ * state[field] = value;
+ * return 0;
+ *
+ * @param field Property name
+ * @param value Integer value to be stored into the property.
+ */
+ public update(field: string, value: number): code.Update {
+ return new code.Update(field, value);
+ }
+
+ /**
+ * Intrinsic operation. Returns 1 if the integer `value` is equal to the
+ * state's property with the name specified by `field`.
+ *
+ * return state[field] === value ? 1 : 0;
+ *
+ * @param field Property name
+ * @param value Integer value to be checked against.
+ */
+ public isEqual(field: string, value: number): code.IsEqual {
+ return new code.IsEqual(field, value);
+ }
+
+ /**
+ * Intrinsic operation.
+ *
+ * state[field] &= value
+ * return 0;
+ *
+ * @param field Property name
+ * @param value Integer value
+ */
+ public and(field: string, value: number): code.And {
+ return new code.And(field, value);
+ }
+
+ /**
+ * Intrinsic operation.
+ *
+ * state[field] |= value
+ * return 0;
+ *
+ * @param field Property name
+ * @param value Integer value
+ */
+ public or(field: string, value: number): code.Or {
+ return new code.Or(field, value);
+ }
+
+ /**
+ * Intrinsic operation.
+ *
+ * return (state[field] & value) == value ? 1 : 0;
+ *
+ * @param field Property name
+ * @param value Integer value
+ */
+ public test(field: string, value: number): code.Test {
+ return new code.Test(field, value);
+ }
+}
diff --git a/llparse-builder/src/code/field-value.ts b/llparse-builder/src/code/field-value.ts
new file mode 100644
index 0000000..2ceea69
--- /dev/null
+++ b/llparse-builder/src/code/field-value.ts
@@ -0,0 +1,9 @@
+import { Signature } from './base';
+import { Field } from './field';
+
+export abstract class FieldValue extends Field {
+ constructor(signature: Signature, name: string, field: string,
+ public readonly value: number) {
+ super(signature, name, field);
+ }
+}
diff --git a/llparse-builder/src/code/field.ts b/llparse-builder/src/code/field.ts
new file mode 100644
index 0000000..af58c84
--- /dev/null
+++ b/llparse-builder/src/code/field.ts
@@ -0,0 +1,10 @@
+import * as assert from 'assert';
+import { Code, Signature } from './base';
+
+export abstract class Field extends Code {
+ constructor(signature: Signature, name: string,
+ public readonly field: string) {
+ super(signature, name + '_' + field);
+ assert(!/^_/.test(field), 'Can\'t access internal field from user code');
+ }
+}
diff --git a/llparse-builder/src/code/index.ts b/llparse-builder/src/code/index.ts
new file mode 100644
index 0000000..7a651e3
--- /dev/null
+++ b/llparse-builder/src/code/index.ts
@@ -0,0 +1,15 @@
+export { Code } from './base';
+export { Creator } from './creator';
+export { Field } from './field';
+export { FieldValue } from './field-value';
+export { IsEqual } from './is-equal';
+export { Load } from './load';
+export { Match } from './match';
+export { IMulAddOptions, MulAdd } from './mul-add';
+export { Or } from './or';
+export { And } from './and';
+export { Span } from './span';
+export { Store } from './store';
+export { Test } from './test';
+export { Update } from './update';
+export { Value } from './value';
diff --git a/llparse-builder/src/code/is-equal.ts b/llparse-builder/src/code/is-equal.ts
new file mode 100644
index 0000000..91bb957
--- /dev/null
+++ b/llparse-builder/src/code/is-equal.ts
@@ -0,0 +1,7 @@
+import { FieldValue } from './field-value';
+
+export class IsEqual extends FieldValue {
+ constructor(field: string, value: number) {
+ super('match', 'is_equal', field, value);
+ }
+}
diff --git a/llparse-builder/src/code/load.ts b/llparse-builder/src/code/load.ts
new file mode 100644
index 0000000..9f3df2e
--- /dev/null
+++ b/llparse-builder/src/code/load.ts
@@ -0,0 +1,7 @@
+import { Field } from './field';
+
+export class Load extends Field {
+ constructor(field: string) {
+ super('match', 'load', field);
+ }
+}
diff --git a/llparse-builder/src/code/match.ts b/llparse-builder/src/code/match.ts
new file mode 100644
index 0000000..631376a
--- /dev/null
+++ b/llparse-builder/src/code/match.ts
@@ -0,0 +1,7 @@
+import { Code } from './base';
+
+export class Match extends Code {
+ constructor(name: string) {
+ super('match', name);
+ }
+}
diff --git a/llparse-builder/src/code/mul-add.ts b/llparse-builder/src/code/mul-add.ts
new file mode 100644
index 0000000..fd648ed
--- /dev/null
+++ b/llparse-builder/src/code/mul-add.ts
@@ -0,0 +1,28 @@
+import { Field } from './field';
+
+/**
+ * Options for `code.mulAdd()`.
+ */
+export interface IMulAddOptions {
+ /** Value to multiply the property with in the first step */
+ readonly base: number;
+
+ /**
+ * Maximum value of the property. If at any point of computation the
+ * intermediate result exceeds it - `mulAdd` returns 1 (overflow).
+ */
+ readonly max?: number;
+
+ /**
+ * If `true` - all arithmetics perfomed by `mulAdd` will be signed.
+ *
+ * Default value: `false`
+ */
+ readonly signed?: boolean;
+}
+
+export class MulAdd extends Field {
+ constructor(field: string, public readonly options: IMulAddOptions) {
+ super('value', 'mul_add', field);
+ }
+}
diff --git a/llparse-builder/src/code/or.ts b/llparse-builder/src/code/or.ts
new file mode 100644
index 0000000..33bd402
--- /dev/null
+++ b/llparse-builder/src/code/or.ts
@@ -0,0 +1,7 @@
+import { FieldValue } from './field-value';
+
+export class Or extends FieldValue {
+ constructor(field: string, value: number) {
+ super('match', 'or', field, value);
+ }
+}
diff --git a/llparse-builder/src/code/span.ts b/llparse-builder/src/code/span.ts
new file mode 100644
index 0000000..b97e09e
--- /dev/null
+++ b/llparse-builder/src/code/span.ts
@@ -0,0 +1,5 @@
+import { Match } from './match';
+
+export class Span extends Match {
+ // no-op
+}
diff --git a/llparse-builder/src/code/store.ts b/llparse-builder/src/code/store.ts
new file mode 100644
index 0000000..84abfef
--- /dev/null
+++ b/llparse-builder/src/code/store.ts
@@ -0,0 +1,7 @@
+import { Field } from './field';
+
+export class Store extends Field {
+ constructor(field: string) {
+ super('value', 'store', field);
+ }
+}
diff --git a/llparse-builder/src/code/test.ts b/llparse-builder/src/code/test.ts
new file mode 100644
index 0000000..a9d0a22
--- /dev/null
+++ b/llparse-builder/src/code/test.ts
@@ -0,0 +1,7 @@
+import { FieldValue } from './field-value';
+
+export class Test extends FieldValue {
+ constructor(field: string, value: number) {
+ super('match', 'test', field, value);
+ }
+}
diff --git a/llparse-builder/src/code/update.ts b/llparse-builder/src/code/update.ts
new file mode 100644
index 0000000..de62476
--- /dev/null
+++ b/llparse-builder/src/code/update.ts
@@ -0,0 +1,7 @@
+import { FieldValue } from './field-value';
+
+export class Update extends FieldValue {
+ constructor(field: string, value: number) {
+ super('match', 'update', field, value);
+ }
+}
diff --git a/llparse-builder/src/code/value.ts b/llparse-builder/src/code/value.ts
new file mode 100644
index 0000000..06c6fd7
--- /dev/null
+++ b/llparse-builder/src/code/value.ts
@@ -0,0 +1,7 @@
+import { Code } from './base';
+
+export class Value extends Code {
+ constructor(name: string) {
+ super('value', name);
+ }
+}
diff --git a/llparse-builder/src/edge.ts b/llparse-builder/src/edge.ts
new file mode 100644
index 0000000..f6b55cc
--- /dev/null
+++ b/llparse-builder/src/edge.ts
@@ -0,0 +1,54 @@
+import * as assert from 'assert';
+
+import { Buffer } from 'buffer';
+import { Invoke, Node } from './node';
+
+/**
+ * This class represents an edge in the parser graph.
+ */
+export class Edge {
+ /**
+ * Comparator for `.sort()` function.
+ */
+ public static compare(a: Edge, b: Edge): number {
+ if (typeof a.key === 'number') {
+ return a.key - (b.key as number);
+ }
+ return a.key!.compare(b.key as Buffer);
+ }
+
+ /**
+ * @param node Edge target
+ * @param noAdvance If `true` - the parent should not consume bytes before
+ * moving to the target `node`
+ * @param key `Buffer` for `node.Match`, `number` for `node.Invoke`,
+ * `undefined` for edges created with `.otherwise()`
+ * @param value `.select()` value associated with the edge
+ */
+ constructor(public readonly node: Node,
+ public readonly noAdvance: boolean,
+ public readonly key: Buffer | number | undefined,
+ public readonly value: number | undefined) {
+ if (node instanceof Invoke) {
+ if (value === undefined) {
+ assert.strictEqual(node.code.signature, 'match',
+ 'Invalid Invoke\'s code signature');
+ } else {
+ assert.strictEqual(node.code.signature, 'value',
+ 'Invalid Invoke\'s code signature');
+ }
+ } else {
+ assert.strictEqual(value, undefined,
+ 'Attempting to pass value to non-Invoke node');
+ }
+
+ if (Buffer.isBuffer(key)) {
+ assert(key.length > 0, 'Invalid edge buffer length');
+
+ if (noAdvance) {
+ assert.strictEqual(key.length, 1,
+ 'Only 1-char keys are allowed in `noAdvance` edges');
+ }
+ }
+ }
+}
diff --git a/llparse-builder/src/loop-checker/index.ts b/llparse-builder/src/loop-checker/index.ts
new file mode 100644
index 0000000..5751955
--- /dev/null
+++ b/llparse-builder/src/loop-checker/index.ts
@@ -0,0 +1,205 @@
+import * as assert from 'assert';
+import * as debugAPI from 'debug';
+
+import { Node } from '../node';
+import { Reachability } from '../reachability';
+import { Lattice } from './lattice';
+
+const debug = debugAPI('llparse-builder:loop-checker');
+
+const EMPTY_VALUE = new Lattice('empty');
+const ANY_VALUE = new Lattice('any');
+
+/**
+ * This class implements a loop checker pass. The goal of this pass is to verify
+ * that the graph doesn't contain infinite loops.
+ */
+export class LoopChecker {
+ private readonly lattice: Map<Node, Lattice> = new Map();
+
+ // Just a cache of terminated keys
+ private readonly terminatedCache: Map<Node, Lattice> = new Map();
+
+ /**
+ * Run loop checker pass on a graph starting from `root`.
+ *
+ * Throws on failure.
+ *
+ * @param root Graph root node
+ */
+ public check(root: Node): void {
+ const r = new Reachability();
+
+ const nodes = r.build(root);
+
+ for (const node of nodes) {
+ debug('checking loops starting from %j', node.name);
+
+ // Set initial lattice value for all nodes
+ this.clear(nodes);
+
+ // Mark root as reachable with any value
+ this.lattice.set(node, ANY_VALUE);
+
+ // Raise lattice values
+ let changed: Set<Node> = new Set([ root ]);
+ while (changed.size !== 0) {
+ if (debug.enabled) {
+ debug('changed %j', Array.from(changed).map((other) => other.name));
+ }
+
+ const next: Set<Node> = new Set();
+ for (const changedNode of changed) {
+ this.propagate(changedNode, next);
+ }
+ changed = next;
+ }
+
+ debug('lattice stabilized');
+
+ // Visit nodes and walk through reachable edges to detect loops
+ this.visit(node, []);
+ }
+ }
+
+ private clear(nodes: ReadonlyArray<Node>): void {
+ for (const node of nodes) {
+ this.lattice.set(node, EMPTY_VALUE);
+ }
+ }
+
+ private propagate(node: Node, changed: Set<Node>): void {
+ let value: Lattice = this.lattice.get(node)!;
+ debug('propagate(%j), initial value %j', node.name, value);
+
+ // Terminate values that are consumed by `match`/`select`
+ const terminated = this.terminate(node, value, changed);
+ if (!terminated.isEqual(EMPTY_VALUE)) {
+ debug('node %j terminates %j', node.name, terminated);
+ value = value.subtract(terminated);
+ if (value.isEqual(EMPTY_VALUE)) {
+ return;
+ }
+ }
+
+ const keysByTarget: Map<Node, Lattice> = new Map();
+ // Propagate value through `.peek()`/`.otherwise()` edges
+ for (const edge of node.getAllEdges()) {
+ if (!edge.noAdvance) {
+ continue;
+ }
+
+ let targetValue: Lattice;
+ if (keysByTarget.has(edge.node)) {
+ targetValue = keysByTarget.get(edge.node)!;
+ } else {
+ targetValue = this.lattice.get(edge.node)!;
+ }
+
+ // `otherwise` or `Invoke`'s edges
+ if (edge.key === undefined || typeof edge.key === 'number') {
+ targetValue = targetValue.union(value);
+ } else {
+ // `.peek()`
+ const edgeValue = new Lattice([ edge.key[0] ]).intersect(value);
+ if (edgeValue.isEqual(EMPTY_VALUE)) {
+ continue;
+ }
+
+ targetValue = targetValue.union(edgeValue);
+ }
+
+ keysByTarget.set(edge.node, targetValue);
+ }
+
+ for (const [ child, childValue ] of keysByTarget) {
+ debug('node %j propagates %j to %j', node.name, childValue,
+ child.name);
+ this.update(child, childValue, changed);
+ }
+ }
+
+ private update(node: Node, newValue: Lattice, changed: Set<Node>): boolean {
+ const value = this.lattice.get(node)!;
+ if (newValue.isEqual(value)) {
+ return false;
+ }
+
+ this.lattice.set(node, newValue);
+ changed.add(node);
+ return true;
+ }
+
+ private terminate(node: Node, value: Lattice, changed: Set<Node>): Lattice {
+ if (this.terminatedCache.has(node)) {
+ return this.terminatedCache.get(node)!;
+ }
+
+ const terminated: number[] = [];
+ for (const edge of node.getAllEdges()) {
+ if (edge.noAdvance) {
+ continue;
+ }
+
+ // Ignore `otherwise` and `Invoke`'s edges
+ if (edge.key === undefined || typeof edge.key === 'number') {
+ continue;
+ }
+
+ terminated.push(edge.key[0]);
+ }
+
+ const result = new Lattice(terminated);
+ this.terminatedCache.set(node, result);
+ return result;
+ }
+
+ private visit(node: Node, path: ReadonlyArray<Node>): void {
+ let value = this.lattice.get(node)!;
+ debug('enter %j, value is %j', node.name, value);
+
+ const terminated = this.terminatedCache.has(node) ?
+ this.terminatedCache.get(node)! : EMPTY_VALUE;
+ if (!terminated.isEqual(EMPTY_VALUE)) {
+ debug('subtract terminated %j', terminated);
+ value = value.subtract(terminated);
+ if (value.isEqual(EMPTY_VALUE)) {
+ debug('terminated everything');
+ return;
+ }
+ }
+
+ for (const edge of node.getAllEdges()) {
+ if (!edge.noAdvance) {
+ continue;
+ }
+
+ let edgeValue = value;
+
+ // `otherwise` or `Invoke`'s edges
+ if (edge.key === undefined || typeof edge.key === 'number') {
+ // nothing to do
+ // `.peek()`
+ } else {
+ edgeValue = edgeValue.intersect(new Lattice([ edge.key[0] ]));
+ }
+
+ // Ignore unreachable edges
+ if (edgeValue.isEqual(EMPTY_VALUE)) {
+ continue;
+ }
+ if (path.indexOf(edge.node) !== -1) {
+ if (path.length === 0) {
+ throw new Error(
+ `Detected loop in "${edge.node.name}" through "${edge.node.name}"`);
+ }
+ throw new Error(
+ `Detected loop in "${edge.node.name}" through chain ` +
+ `${path.map((parent) => '"' + parent.name + '"').join(' -> ')}`);
+ }
+ this.visit(edge.node, path.concat(edge.node));
+ }
+
+ debug('leave %j', node.name);
+ }
+}
diff --git a/llparse-builder/src/loop-checker/lattice.ts b/llparse-builder/src/loop-checker/lattice.ts
new file mode 100644
index 0000000..8d2a7fe
--- /dev/null
+++ b/llparse-builder/src/loop-checker/lattice.ts
@@ -0,0 +1,115 @@
+import * as assert from 'assert';
+
+const MAX_VALUE = 256;
+const WORD_SIZE = 32;
+const SIZE = (MAX_VALUE / WORD_SIZE) | 0;
+const WORD_FILL = -1 | 0;
+
+assert.strictEqual(MAX_VALUE % WORD_SIZE, 0);
+
+export type LatticeValue = 'empty' | ReadonlyArray<number> | 'any';
+
+/**
+ * A fixed-size bitfield, really
+ */
+export class Lattice {
+ protected readonly words: number[];
+
+ constructor(value: LatticeValue) {
+ this.words = new Array(SIZE).fill(value === 'any' ? WORD_FILL : 0);
+
+ if (Array.isArray(value)) {
+ for (const single of value) {
+ this.add(single);
+ }
+ }
+ }
+
+ public check(bit: number): boolean {
+ assert(0 <= bit && bit < MAX_VALUE, 'Invalid bit');
+ const index = (bit / WORD_SIZE) | 0;
+ const off = bit % WORD_SIZE;
+ return (this.words[index] & (1 << off)) !== 0;
+ }
+
+ public union(other: Lattice): Lattice {
+ const result = new Lattice('empty');
+
+ for (let i = 0; i < SIZE; i++) {
+ result.words[i] = this.words[i] | other.words[i];
+ }
+
+ return result;
+ }
+
+ public intersect(other: Lattice): Lattice {
+ const result = new Lattice('empty');
+
+ for (let i = 0; i < SIZE; i++) {
+ result.words[i] = this.words[i] & other.words[i];
+ }
+
+ return result;
+ }
+
+ public subtract(other: Lattice): Lattice {
+ const result = new Lattice('empty');
+
+ for (let i = 0; i < SIZE; i++) {
+ result.words[i] = this.words[i] & (~other.words[i]);
+ }
+
+ return result;
+ }
+
+ public isEqual(other: Lattice): boolean {
+ if (this === other) {
+ return true;
+ }
+
+ for (let i = 0; i < SIZE; i++) {
+ if (this.words[i] !== other.words[i]) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ public *[Symbol.iterator](): Iterator<number> {
+ // TODO(indutny): improve speed if needed
+ for (let i = 0; i < MAX_VALUE; i++) {
+ if (this.check(i)) {
+ yield i;
+ }
+ }
+ }
+
+ public toJSON(): any {
+ let isEmpty = true;
+ let isFull = true;
+ for (let i = 0; i < SIZE; i++) {
+ if (this.words[i] !== 0) {
+ isEmpty = false;
+ }
+ if (this.words[i] !== WORD_FILL) {
+ isFull = false;
+ }
+ }
+ if (isEmpty) {
+ return 'empty';
+ }
+ if (isFull) {
+ return 'any';
+ }
+ return Array.from(this);
+ }
+
+ // Private
+
+ private add(bit: number): void {
+ assert(0 <= bit && bit < MAX_VALUE, 'Invalid bit');
+ const index = (bit / WORD_SIZE) | 0;
+ const off = bit % WORD_SIZE;
+ this.words[index] |= 1 << off;
+ }
+}
diff --git a/llparse-builder/src/node/base.ts b/llparse-builder/src/node/base.ts
new file mode 100644
index 0000000..9840f16
--- /dev/null
+++ b/llparse-builder/src/node/base.ts
@@ -0,0 +1,96 @@
+import * as assert from 'assert';
+import binarySearch = require('binary-search');
+import { Edge } from '../edge';
+
+/**
+ * Base class for all graph nodes.
+ */
+export abstract class Node {
+ private otherwiseEdge: Edge | undefined;
+ private privEdges: Edge[] = [];
+
+ /**
+ * @param name Node name
+ */
+ constructor(public readonly name: string) {
+ // no-op
+ }
+
+ /**
+ * Create an otherwise edge to node `node`.
+ *
+ * This edge is executed when no other edges match current input. No
+ * characters are consumed upon transition.
+ *
+ * NOTE: At most one otherwise (skipping or not) edge can be set, most nodes
+ * except `Error` require it.
+ *
+ * @param node Target node
+ */
+ public otherwise(node: Node): this {
+ if (this.otherwiseEdge !== undefined) {
+ throw new Error('Node already has `otherwise` or `skipTo`');
+ }
+
+ this.otherwiseEdge = new Edge(node, true, undefined, undefined);
+ return this;
+ }
+
+ /**
+ * Create a skipping otherwise edge to node `node`.
+ *
+ * This edge is executed when no other edges match current input. Single
+ * character is consumed upon transition.
+ *
+ * NOTE: At most one otherwise (skipping or not) edge can be set, most nodes
+ * except `Error` require it.
+ *
+ * @param node Target node
+ */
+ public skipTo(node: Node): this {
+ if (this.otherwiseEdge !== undefined) {
+ throw new Error('Node already has `otherwise` or `skipTo`');
+ }
+
+ this.otherwiseEdge = new Edge(node, false, undefined, undefined);
+ return this;
+ }
+
+ // Limited public use
+
+ /** Get otherwise edge. */
+ public getOtherwiseEdge(): Edge | undefined {
+ return this.otherwiseEdge;
+ }
+
+ /** Get list of all non-otherwise edges. */
+ public getEdges(): ReadonlyArray<Edge> {
+ return this.privEdges;
+ }
+
+ /** Get list of all edges (including otherwise, if present). */
+ public getAllEdges(): ReadonlyArray<Edge> {
+ const res = this.privEdges;
+ if (this.otherwiseEdge === undefined) {
+ return res;
+ } else {
+ return res.concat(this.otherwiseEdge);
+ }
+ }
+
+ /** Get iterator through all non-otherwise edges. */
+ public *[Symbol.iterator](): Iterator<Edge> {
+ yield* this.privEdges;
+ }
+
+ // Internal
+
+ protected addEdge(edge: Edge): void {
+ assert.notStrictEqual(edge.key, undefined);
+
+ const index = binarySearch(this.privEdges, edge, Edge.compare);
+ assert(index < 0, 'Attempting to create duplicate edge');
+
+ this.privEdges.splice(-1 - index, 0, edge);
+ }
+}
diff --git a/llparse-builder/src/node/consume.ts b/llparse-builder/src/node/consume.ts
new file mode 100644
index 0000000..eff4037
--- /dev/null
+++ b/llparse-builder/src/node/consume.ts
@@ -0,0 +1,19 @@
+import * as assert from 'assert';
+import { Node } from './base';
+
+/**
+ * This node consumes number of characters specified by state's property with
+ * name `field` from the input, and forwards execution to `otherwise` node.
+ */
+export class Consume extends Node {
+ /**
+ * @param field State's property name
+ */
+ constructor(public readonly field: string) {
+ super('consume_' + field);
+
+ if (/^_/.test(field)) {
+ throw new Error(`Can't use internal field in \`consume()\`: "${field}"`);
+ }
+ }
+}
diff --git a/llparse-builder/src/node/error.ts b/llparse-builder/src/node/error.ts
new file mode 100644
index 0000000..393f566
--- /dev/null
+++ b/llparse-builder/src/node/error.ts
@@ -0,0 +1,24 @@
+import * as assert from 'assert';
+import { Node } from './base';
+
+/**
+ * This node terminates the execution with an error
+ */
+class NodeError extends Node {
+ /**
+ * @param code Error code to return to user
+ * @param reason Error description to store in parser's state
+ */
+ constructor(public readonly code: number, public readonly reason: string) {
+ super('error');
+ assert.strictEqual(code, code | 0, 'code must be integer');
+ }
+
+ /** `.otherwise()` is not supported on this type of node */
+ public otherwise(node: Node): this { throw new Error('Not supported'); }
+
+ /** `.skipTo()` is not supported on this type of node */
+ public skipTo(node: Node): this { throw new Error('Not supported'); }
+}
+
+export { NodeError as Error };
diff --git a/llparse-builder/src/node/index.ts b/llparse-builder/src/node/index.ts
new file mode 100644
index 0000000..e3d5fe5
--- /dev/null
+++ b/llparse-builder/src/node/index.ts
@@ -0,0 +1,8 @@
+export { Node } from './base';
+export { Consume } from './consume';
+export { Error } from './error';
+export { Invoke, IInvokeMap } from './invoke';
+export { Match } from './match';
+export { Pause } from './pause';
+export { SpanStart } from './span-start';
+export { SpanEnd } from './span-end';
diff --git a/llparse-builder/src/node/invoke.ts b/llparse-builder/src/node/invoke.ts
new file mode 100644
index 0000000..d6791a7
--- /dev/null
+++ b/llparse-builder/src/node/invoke.ts
@@ -0,0 +1,39 @@
+import * as assert from 'assert';
+
+import { Code } from '../code';
+import { Edge } from '../edge';
+import { Node } from './base';
+
+/**
+ * Map of return codes of the callback. Each key is a return code,
+ * value is the target node that must be executed upon getting such return code.
+ */
+export interface IInvokeMap {
+ readonly [key: number]: Node;
+}
+
+/**
+ * This node invokes either external callback or intrinsic code and passes the
+ * execution to either a target from a `map` (if the return code matches one of
+ * registered in it), or to `otherwise` node.
+ */
+export class Invoke extends Node {
+ /**
+ * @param code External callback or intrinsic code. Can be created with
+ * `builder.code.*()` methods.
+ * @param map Map from callback return codes to target nodes
+ */
+ constructor(public readonly code: Code, map: IInvokeMap) {
+ super('invoke_' + code.name);
+
+ Object.keys(map).forEach((mapKey) => {
+ const numKey: number = parseInt(mapKey, 10);
+ const targetNode = map[numKey]!;
+
+ assert.strictEqual(numKey, numKey | 0,
+ 'Invoke\'s map keys must be integers');
+
+ this.addEdge(new Edge(targetNode, true, numKey, undefined));
+ });
+ }
+}
diff --git a/llparse-builder/src/node/match.ts b/llparse-builder/src/node/match.ts
new file mode 100644
index 0000000..617a659
--- /dev/null
+++ b/llparse-builder/src/node/match.ts
@@ -0,0 +1,162 @@
+import * as assert from 'assert';
+import { Buffer } from 'buffer';
+
+import { Edge } from '../edge';
+import { Transform } from '../transform';
+import { toBuffer } from '../utils';
+import { Node } from './base';
+
+/**
+ * Character/sequence to match.
+ *
+ * May have following types:
+ *
+ * * `number` - for single character
+ * * `string` - for printable character sequence
+ * * `Buffer` - for raw byte sequence
+ */
+export type MatchSingleValue = string | number | Buffer;
+
+/**
+ * Convenience type for passing several characters/sequences to match methods.
+ */
+export type MatchValue = MatchSingleValue | ReadonlyArray<MatchSingleValue>;
+
+/**
+ * A map from characters/sequences to `.select()`'s values. Used for specifying
+ * the value to be passed to `.select()'`s targets.
+ */
+export interface IMatchSelect {
+ readonly [key: string]: number;
+}
+
+/**
+ * This node matches characters/sequences and forwards the execution according
+ * to matched character with optional attached value (See `.select()`).
+ */
+export class Match extends Node {
+ private transformFn: Transform | undefined;
+
+ /**
+ * Set character transformation function.
+ *
+ * @param transform Transformation to apply. Can be created with
+ * `builder.transform.*()` methods.
+ */
+ public transform(transformFn: Transform): this {
+ this.transformFn = transformFn;
+ return this;
+ }
+
+ /**
+ * Match sequence/character and forward execution to `next` on success,
+ * consuming matched bytes of the input.
+ *
+ * No value is attached on such execution forwarding, and the target node
+ * **must not** be an `Invoke` node with a callback expecting the value.
+ *
+ * @param value Sequence/character to be matched
+ * @param next Target node to be executed on success.
+ */
+ public match(value: MatchValue, next: Node): this {
+ if (Array.isArray(value)) {
+ for (const subvalue of value) {
+ this.match(subvalue, next);
+ }
+ return this;
+ }
+
+ const buffer = toBuffer(value as MatchSingleValue);
+ const edge = new Edge(next, false, buffer, undefined);
+ this.addEdge(edge);
+ return this;
+ }
+
+ /**
+ * Match character and forward execution to `next` on success
+ * without consuming one byte of the input.
+ *
+ * No value is attached on such execution forwarding, and the target node
+ * **must not** be an `Invoke` with a callback expecting the value.
+ *
+ * @param value Character to be matched
+ * @param next Target node to be executed on success.
+ */
+ public peek(value: MatchValue, next: Node): this {
+ if (Array.isArray(value)) {
+ for (const subvalue of value) {
+ this.peek(subvalue, next);
+ }
+ return this;
+ }
+
+ const buffer = toBuffer(value as MatchSingleValue);
+ assert.strictEqual(buffer.length, 1,
+ '`.peek()` accepts only single character keys');
+
+ const edge = new Edge(next, true, buffer, undefined);
+ this.addEdge(edge);
+ return this;
+ }
+
+ /**
+ * Match character/sequence and forward execution to `next` on success
+ * consumed matched bytes of the input.
+ *
+ * Value is attached on such execution forwarding, and the target node
+ * **must** be an `Invoke` with a callback expecting the value.
+ *
+ * Possible signatures:
+ *
+ * * `.select(key, value [, next ])`
+ * * `.select({ key: value } [, next])`
+ *
+ * @param keyOrMap Either a sequence to match, or a map from sequences to
+ * values
+ * @param valueOrNext Either an integer value to be forwarded to the target
+ * node, or an otherwise node
+ * @param next Convenience param. Same as calling `.otherwise(...)`
+ */
+ public select(keyOrMap: MatchSingleValue | IMatchSelect,
+ valueOrNext?: number | Node, next?: Node): this {
+ // .select({ key: value, ... }, next)
+ if (typeof keyOrMap === 'object') {
+ assert(valueOrNext instanceof Node,
+ 'Invalid `next` argument of `.select()`');
+ assert.strictEqual(next, undefined,
+ 'Invalid argument count of `.select()`');
+
+ const map: IMatchSelect = keyOrMap as IMatchSelect;
+ next = valueOrNext as Node | undefined;
+
+ Object.keys(map).forEach((mapKey) => {
+ const numKey: number = mapKey as any;
+
+ this.select(numKey, map[numKey]!, next);
+ });
+ return this;
+ }
+
+ // .select(key, value, next)
+ assert.strictEqual(typeof valueOrNext, 'number',
+ 'Invalid `value` argument of `.select()`');
+ assert.notStrictEqual(next, undefined,
+ 'Invalid `next` argument of `.select()`');
+
+ const key = toBuffer(keyOrMap as MatchSingleValue);
+ const value = valueOrNext as number;
+
+ const edge = new Edge(next!, false, key, value);
+ this.addEdge(edge);
+ return this;
+ }
+
+ // Limited public use
+
+ /**
+ * Get tranformation function
+ */
+ public getTransform(): Transform | undefined {
+ return this.transformFn;
+ }
+}
diff --git a/llparse-builder/src/node/pause.ts b/llparse-builder/src/node/pause.ts
new file mode 100644
index 0000000..2dcf5d1
--- /dev/null
+++ b/llparse-builder/src/node/pause.ts
@@ -0,0 +1,25 @@
+import * as assert from 'assert';
+import { Node } from './base';
+
+/**
+ * This returns the specified error code, but makes the resumption to
+ * `otherwise` target possible.
+ */
+export class Pause extends Node {
+ /**
+ * @param code Error code to return
+ * @param reason Error description
+ */
+ constructor(public readonly code: number, public readonly reason: string) {
+ super('pause');
+ assert.strictEqual(code, code | 0, 'code must be integer');
+ }
+
+ /**
+ * `.skipTo()` is not supported on this type of node, please use
+ * `.otherwise()`
+ */
+ public skipTo(node: Node): this {
+ throw new Error('Not supported, please use `pause.otherwise()`');
+ }
+}
diff --git a/llparse-builder/src/node/span-end.ts b/llparse-builder/src/node/span-end.ts
new file mode 100644
index 0000000..377cd73
--- /dev/null
+++ b/llparse-builder/src/node/span-end.ts
@@ -0,0 +1,19 @@
+import { Span } from '../span';
+import { Node } from './base';
+
+/**
+ * Indicates span end.
+ *
+ * A callback will be invoked with all input data since the most recent of:
+ *
+ * * Span start invocation
+ * * Parser execution
+ */
+export class SpanEnd extends Node {
+ /**
+ * @param span Span instance
+ */
+ constructor(public readonly span: Span) {
+ super(`span_end_${span.callback.name}`);
+ }
+}
diff --git a/llparse-builder/src/node/span-start.ts b/llparse-builder/src/node/span-start.ts
new file mode 100644
index 0000000..f81b432
--- /dev/null
+++ b/llparse-builder/src/node/span-start.ts
@@ -0,0 +1,16 @@
+import { Span } from '../span';
+import { Node } from './base';
+
+/**
+ * Indicates span start.
+ *
+ * See `SpanEnd` for details on callback invocation.
+ */
+export class SpanStart extends Node {
+ /**
+ * @param span Span instance
+ */
+ constructor(public readonly span: Span) {
+ super(`span_start_${span.callback.name}`);
+ }
+}
diff --git a/llparse-builder/src/property.ts b/llparse-builder/src/property.ts
new file mode 100644
index 0000000..cf2fe4b
--- /dev/null
+++ b/llparse-builder/src/property.ts
@@ -0,0 +1,12 @@
+export type PropertyType = 'i8' | 'i16' | 'i32' | 'i64' | 'ptr';
+
+/**
+ * Class describing allocated property in parser's state
+ */
+export class Property {
+ constructor(public readonly ty: PropertyType, public readonly name: string) {
+ if (/^_/.test(name)) {
+ throw new Error(`Can't use internal property name: "${name}"`);
+ }
+ }
+}
diff --git a/llparse-builder/src/reachability.ts b/llparse-builder/src/reachability.ts
new file mode 100644
index 0000000..88bcd65
--- /dev/null
+++ b/llparse-builder/src/reachability.ts
@@ -0,0 +1,31 @@
+import { Node } from './node';
+
+/**
+ * This class finds all reachable nodes
+ */
+export class Reachability {
+ /**
+ * Build and return list of reachable nodes.
+ */
+ public build(root: Node): ReadonlyArray<Node> {
+ const res = new Set();
+ const queue = [ root ];
+ while (queue.length !== 0) {
+ const node = queue.pop()!;
+ if (res.has(node)) {
+ continue;
+ }
+ res.add(node);
+
+ for (const edge of node) {
+ queue.push(edge.node);
+ }
+
+ const otherwise = node.getOtherwiseEdge();
+ if (otherwise !== undefined) {
+ queue.push(otherwise.node);
+ }
+ }
+ return Array.from(res) as ReadonlyArray<Node>;
+ }
+}
diff --git a/llparse-builder/src/span-allocator.ts b/llparse-builder/src/span-allocator.ts
new file mode 100644
index 0000000..b3e8f6b
--- /dev/null
+++ b/llparse-builder/src/span-allocator.ts
@@ -0,0 +1,182 @@
+import * as assert from 'assert';
+import * as debugAPI from 'debug';
+
+import { Node, SpanEnd, SpanStart } from './node';
+import { Reachability } from './reachability';
+import { Span } from './span';
+
+const debug = debugAPI('llparse-builder:span-allocator');
+
+type SpanSet = Set<Span>;
+
+interface ISpanActiveInfo {
+ readonly active: Map<Node, SpanSet>;
+ readonly spans: ReadonlyArray<Span>;
+}
+
+type SpanOverlap = Map<Span, SpanSet>;
+
+export interface ISpanAllocatorResult {
+ readonly colors: ReadonlyMap<Span, number>;
+ readonly concurrency: ReadonlyArray<ReadonlyArray<Span> >;
+ readonly max: number;
+}
+
+function id(node: SpanStart | SpanEnd): Span {
+ return node.span;
+}
+
+export class SpanAllocator {
+ public allocate(root: Node): ISpanAllocatorResult {
+ const r = new Reachability();
+ const nodes = r.build(root);
+ const info = this.computeActive(nodes);
+ this.check(info);
+ const overlap = this.computeOverlap(info);
+ return this.color(info.spans, overlap);
+ }
+
+ private computeActive(nodes: ReadonlyArray<Node>): ISpanActiveInfo {
+ const activeMap: Map<Node, SpanSet> = new Map();
+ nodes.forEach((node) => activeMap.set(node, new Set()));
+
+ const queue: Set<Node> = new Set(nodes);
+ const spans: SpanSet = new Set();
+ for (const node of queue) {
+ queue.delete(node);
+
+ const active = activeMap.get(node)!;
+
+ if (node instanceof SpanStart) {
+ const span = id(node);
+ spans.add(span);
+ active.add(span);
+ }
+
+ active.forEach((span) => {
+ // Don't propagate span past the spanEnd
+ if (node instanceof SpanEnd && span === id(node)) {
+ return;
+ }
+
+ node.getAllEdges().forEach((edge) => {
+ const edgeNode = edge.node;
+
+ // Disallow loops
+ if (edgeNode instanceof SpanStart) {
+ assert.notStrictEqual(id(edgeNode), span,
+ `Detected loop in span "${span.callback.name}", started ` +
+ `at "${node.name}"`);
+ }
+
+ const edgeActive = activeMap.get(edgeNode)!;
+ if (edgeActive.has(span)) {
+ return;
+ }
+
+ edgeActive.add(span);
+ queue.add(edgeNode);
+ });
+ });
+ }
+
+ return { active: activeMap, spans: Array.from(spans) };
+ }
+
+ private check(info: ISpanActiveInfo): void {
+ debug('check start');
+ for (const [ node, spans ] of info.active) {
+ for (const edge of node.getAllEdges()) {
+ if (edge.node instanceof SpanStart) {
+ continue;
+ }
+
+ // Skip terminal nodes
+ if (edge.node.getAllEdges().length === 0) {
+ continue;
+ }
+
+ debug('checking edge from %j to %j', node.name, edge.node.name);
+
+ const edgeSpans = info.active.get(edge.node)!;
+ for (const subSpan of edgeSpans) {
+ assert(spans.has(subSpan),
+ `Unmatched span end for "${subSpan.callback.name}" ` +
+ `at "${edge.node.name}", coming from "${node.name}"`);
+ }
+
+ if (edge.node instanceof SpanEnd) {
+ const span = id(edge.node);
+ assert(spans.has(span),
+ `Unmatched span end for "${span.callback.name}"`);
+ }
+ }
+ }
+ }
+
+ private computeOverlap(info: ISpanActiveInfo): SpanOverlap {
+ const active = info.active;
+ const overlap: SpanOverlap = new Map();
+
+ info.spans.forEach((span) => overlap.set(span, new Set()));
+
+ active.forEach((spans) => {
+ spans.forEach((one) => {
+ const set = overlap.get(one)!;
+ spans.forEach((other) => {
+ if (other !== one) {
+ set.add(other);
+ }
+ });
+ });
+ });
+
+ return overlap;
+ }
+
+ private color(spans: ReadonlyArray<Span>, overlapMap: SpanOverlap)
+ : ISpanAllocatorResult {
+ let max = -1;
+ const colors: Map<Span, number> = new Map();
+
+ const allocate = (span: Span): number => {
+ if (colors.has(span)) {
+ return colors.get(span)!;
+ }
+
+ const overlap = overlapMap.get(span)!;
+
+ // See which colors are already used
+ const used: Set<number> = new Set();
+ for (const subSpan of overlap) {
+ if (colors.has(subSpan)) {
+ used.add(colors.get(subSpan)!);
+ }
+ }
+
+ // Find minimum available color
+ let i;
+ for (i = 0; used.has(i); i++) {
+ // no-op
+ }
+
+ max = Math.max(max, i);
+ colors.set(span, i);
+
+ return i;
+ };
+
+ const map: Map<Span, number> = new Map();
+
+ spans.forEach((span) => map.set(span, allocate(span)));
+
+ const concurrency: Span[][] = new Array(max + 1);
+ for (let i = 0; i < concurrency.length; i++) {
+ concurrency[i] = [];
+ }
+
+ spans.forEach((span) => concurrency[allocate(span)].push(span));
+
+ return { colors: map, concurrency, max };
+ }
+}
diff --git a/llparse-builder/src/span.ts b/llparse-builder/src/span.ts
new file mode 100644
index 0000000..99cafb0
--- /dev/null
+++ b/llparse-builder/src/span.ts
@@ -0,0 +1,57 @@
+import * as assert from 'assert';
+
+import { Span as SpanCallback } from './code';
+import { Node, SpanEnd, SpanStart } from './node';
+
+/**
+ * Spans are used for notifying parser user about matched data. Each byte after
+ * span start will be sent to the span callback until span end is called.
+ */
+export class Span {
+ private readonly startCache: Map<Node, SpanStart> = new Map();
+ private readonly endCache: Map<Node, SpanEnd> = new Map();
+
+ /**
+ * @param callback External callback, must be `code.span(...)` result.
+ */
+ constructor(public readonly callback: SpanCallback) {
+ }
+
+ /**
+ * Create `SpanStart` that indicates the start of the span.
+ *
+ * @param otherwise Optional convenience value. Same as calling
+ * `span.start().otherwise(...)`
+ */
+ public start(otherwise?: Node) {
+ if (otherwise !== undefined && this.startCache.has(otherwise)) {
+ return this.startCache.get(otherwise)!;
+ }
+
+ const res = new SpanStart(this);
+ if (otherwise !== undefined) {
+ res.otherwise(otherwise);
+ this.startCache.set(otherwise, res);
+ }
+ return res;
+ }
+
+ /**
+ * Create `SpanEnd` that indicates the end of the span.
+ *
+ * @param otherwise Optional convenience value. Same as calling
+ * `span.end().otherwise(...)`
+ */
+ public end(otherwise?: Node) {
+ if (otherwise !== undefined && this.endCache.has(otherwise)) {
+ return this.endCache.get(otherwise)!;
+ }
+
+ const res = new SpanEnd(this);
+ if (otherwise !== undefined) {
+ res.otherwise(otherwise);
+ this.endCache.set(otherwise, res);
+ }
+ return res;
+ }
+}
diff --git a/llparse-builder/src/transform/base.ts b/llparse-builder/src/transform/base.ts
new file mode 100644
index 0000000..902199c
--- /dev/null
+++ b/llparse-builder/src/transform/base.ts
@@ -0,0 +1,12 @@
+export type TransformName = 'to_lower_unsafe' | 'to_lower';
+
+/**
+ * Character transformation.
+ */
+export abstract class Transform {
+ /**
+ * @param name Transform name
+ */
+ constructor(public readonly name: TransformName) {
+ }
+}
diff --git a/llparse-builder/src/transform/creator.ts b/llparse-builder/src/transform/creator.ts
new file mode 100644
index 0000000..eaf3d5c
--- /dev/null
+++ b/llparse-builder/src/transform/creator.ts
@@ -0,0 +1,28 @@
+import { Transform } from './base';
+import { ToLower } from './to-lower';
+import { ToLowerUnsafe } from './to-lower-unsafe';
+
+/**
+ * API for creating character transformations.
+ *
+ * The results of methods of this class can be used as an argument to:
+ * `p.node().transform(...)`.
+ */
+export class Creator {
+ /**
+ * Unsafe transform to lowercase.
+ *
+ * The operation of this transformation is equivalent to:
+ * `String.fromCharCode(input.charCodeAt(0) | 0x20)`.
+ */
+ public toLowerUnsafe(): Transform {
+ return new ToLowerUnsafe();
+ }
+
+ /**
+ * Safe transform to lowercase.
+ */
+ public toLower(): Transform {
+ return new ToLower();
+ }
+}
diff --git a/llparse-builder/src/transform/index.ts b/llparse-builder/src/transform/index.ts
new file mode 100644
index 0000000..acdcf01
--- /dev/null
+++ b/llparse-builder/src/transform/index.ts
@@ -0,0 +1,3 @@
+export { Transform } from './base';
+export { Creator } from './creator';
+export { ToLowerUnsafe } from './to-lower-unsafe';
diff --git a/llparse-builder/src/transform/to-lower-unsafe.ts b/llparse-builder/src/transform/to-lower-unsafe.ts
new file mode 100644
index 0000000..99d9618
--- /dev/null
+++ b/llparse-builder/src/transform/to-lower-unsafe.ts
@@ -0,0 +1,7 @@
+import { Transform } from './base';
+
+export class ToLowerUnsafe extends Transform {
+ constructor() {
+ super('to_lower_unsafe');
+ }
+}
diff --git a/llparse-builder/src/transform/to-lower.ts b/llparse-builder/src/transform/to-lower.ts
new file mode 100644
index 0000000..b333fce
--- /dev/null
+++ b/llparse-builder/src/transform/to-lower.ts
@@ -0,0 +1,7 @@
+import { Transform } from './base';
+
+export class ToLower extends Transform {
+ constructor() {
+ super('to_lower');
+ }
+}
diff --git a/llparse-builder/src/utils.ts b/llparse-builder/src/utils.ts
new file mode 100644
index 0000000..3521b20
--- /dev/null
+++ b/llparse-builder/src/utils.ts
@@ -0,0 +1,19 @@
+import * as assert from 'assert';
+import { Buffer } from 'buffer';
+
+/**
+ * Internal
+ */
+export function toBuffer(value: number | string | Buffer): Buffer {
+ let res: Buffer;
+ if (Buffer.isBuffer(value)) {
+ res = value;
+ } else if (typeof value === 'string') {
+ res = Buffer.from(value);
+ } else {
+ assert(0 <= value && value <= 0xff, 'Invalid byte value');
+ res = Buffer.from([ value ]);
+ }
+ assert(res.length >= 1, 'Invalid key length');
+ return res;
+}
diff --git a/llparse-builder/test/builder-test.ts b/llparse-builder/test/builder-test.ts
new file mode 100644
index 0000000..82723ec
--- /dev/null
+++ b/llparse-builder/test/builder-test.ts
@@ -0,0 +1,94 @@
+import * as assert from 'assert';
+
+import { Builder } from '../src/builder';
+
+describe('LLParse/Builder', () => {
+ let b: Builder;
+ beforeEach(() => {
+ b = new Builder();
+ });
+
+ it('should build primitive graph', () => {
+ const start = b.node('start');
+ const end = b.node('end');
+
+ start
+ .peek('e', end)
+ .match('a', start)
+ .otherwise(b.error(1, 'error'));
+
+ end
+ .skipTo(start);
+
+ const edges = start.getEdges();
+ assert.strictEqual(edges.length, 2);
+
+ assert(!edges[0].noAdvance);
+ assert.strictEqual(edges[0].node, start);
+
+ assert(edges[1].noAdvance);
+ assert.strictEqual(edges[1].node, end);
+ });
+
+ it('should disallow duplicate edges', () => {
+ const start = b.node('start');
+
+ start.peek('e', start);
+
+ assert.throws(() => {
+ start.peek('e', start);
+ }, /duplicate edge/);
+ });
+
+ it('should disallow select to non-invoke', () => {
+ const start = b.node('start');
+
+ assert.throws(() => {
+ start.select('a', 1, start);
+ }, /value to non-Invoke/);
+ });
+
+ it('should disallow select to match-invoke', () => {
+ const start = b.node('start');
+ const invoke = b.invoke(b.code.match('something'));
+
+ assert.throws(() => {
+ start.select('a', 1, invoke);
+ }, /Invalid.*code signature/);
+ });
+
+ it('should disallow peek to value-invoke', () => {
+ const start = b.node('start');
+ const invoke = b.invoke(b.code.value('something'));
+
+ assert.throws(() => {
+ start.peek('a', invoke);
+ }, /Invalid.*code signature/);
+ });
+
+ it('should allow select to value-invoke', () => {
+ const start = b.node('start');
+ const invoke = b.invoke(b.code.value('something'));
+
+ assert.doesNotThrow(() => {
+ start.select('a', 1, invoke);
+ });
+ });
+
+ it('should create edges for Invoke', () => {
+ const start = b.node('start');
+ const invoke = b.invoke(b.code.value('something'), {
+ '-1': start,
+ '1': start,
+ '10': start,
+ });
+
+ const edges = invoke.getEdges();
+ const keys = edges.map((edge) => edge.key!);
+ assert.deepStrictEqual(keys, [
+ -1,
+ 1,
+ 10,
+ ]);
+ });
+});
diff --git a/llparse-builder/test/loop-checker-test.ts b/llparse-builder/test/loop-checker-test.ts
new file mode 100644
index 0000000..0df6064
--- /dev/null
+++ b/llparse-builder/test/loop-checker-test.ts
@@ -0,0 +1,118 @@
+import * as assert from 'assert';
+
+import { Builder, LoopChecker } from '../src/builder';
+
+describe('LLParse/LoopChecker', () => {
+ let b: Builder;
+ let lc: LoopChecker;
+ beforeEach(() => {
+ b = new Builder();
+ lc = new LoopChecker();
+ });
+
+ it('should detect shallow loops', () => {
+ const start = b.node('start');
+
+ start
+ .otherwise(start);
+
+ assert.throws(() => {
+ lc.check(start);
+ }, /Detected loop in "start".*"start"/);
+ });
+
+ it('should detect loops', () => {
+ const start = b.node('start');
+ const a = b.node('a');
+ const invoke = b.invoke(b.code.match('nop'), {
+ 0: start,
+ }, b.error(1, 'error'));
+
+ start
+ .peek('a', a)
+ .otherwise(b.error(1, 'error'));
+
+ a.otherwise(invoke);
+
+ assert.throws(() => {
+ lc.check(start);
+ }, /Detected loop in "a".*"a" -> "invoke_nop"/);
+ });
+
+ it('should detect seemingly unreachable keys', () => {
+ const start = b.node('start');
+ const loop = b.node('loop');
+
+ start
+ .peek('a', loop)
+ .otherwise(b.error(1, 'error'));
+
+ loop
+ .match('a', loop)
+ .otherwise(loop);
+
+ assert.throws(() => {
+ lc.check(start);
+ }, /Detected loop in "loop" through.*"loop"/);
+ });
+
+ it('should ignore loops through `peek` to `match`', () => {
+ const start = b.node('start');
+ const a = b.node('a');
+ const invoke = b.invoke(b.code.match('nop'), {
+ 0: start,
+ }, b.error(1, 'error'));
+
+ start
+ .peek('a', a)
+ .otherwise(b.error(1, 'error'));
+
+ a
+ .match('abc', invoke)
+ .otherwise(start);
+
+ assert.doesNotThrow(() => lc.check(start));
+ });
+
+ it('should ignore irrelevant `peek`s', () => {
+ const start = b.node('start');
+ const a = b.node('a');
+
+ start
+ .peek('a', a)
+ .otherwise(b.error(1, 'error'));
+
+ a
+ .peek('b', start)
+ .otherwise(b.error(1, 'error'));
+
+ assert.doesNotThrow(() => lc.check(start));
+ });
+
+ it('should ignore loops with multi `peek`/`match`', () => {
+ const start = b.node('start');
+ const another = b.node('another');
+
+ const NUM: ReadonlyArray<string> = [
+ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
+ ];
+
+ const ALPHA: ReadonlyArray<string> = [
+ 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o',
+ 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
+ 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O',
+ 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
+ ];
+
+ start
+ .match(ALPHA, start)
+ .peek(NUM, another)
+ .skipTo(start);
+
+ another
+ .match(NUM, another)
+ .otherwise(start);
+
+ assert.doesNotThrow(() => lc.check(start));
+ });
+});
diff --git a/llparse-builder/test/span-allocator-test.ts b/llparse-builder/test/span-allocator-test.ts
new file mode 100644
index 0000000..bc8f656
--- /dev/null
+++ b/llparse-builder/test/span-allocator-test.ts
@@ -0,0 +1,146 @@
+import * as assert from 'assert';
+
+import { Builder, SpanAllocator } from '../src/builder';
+
+describe('LLParse/LoopChecker', () => {
+ let b: Builder;
+ let sa: SpanAllocator;
+ beforeEach(() => {
+ b = new Builder();
+ sa = new SpanAllocator();
+ });
+
+ it('should allocate single span', () => {
+ const span = b.span(b.code.span('span'));
+ const start = b.node('start');
+ const body = b.node('body');
+
+ start
+ .otherwise(span.start(body));
+
+ body
+ .skipTo(span.end(start));
+
+ const res = sa.allocate(start);
+
+ assert.strictEqual(res.max, 0);
+
+ assert.strictEqual(res.concurrency.length, 1);
+ assert.ok(res.concurrency[0].includes(span));
+
+ assert.strictEqual(res.colors.size, 1);
+ assert.strictEqual(res.colors.get(span), 0);
+ });
+
+ it('should allocate overlapping spans', () => {
+ const span1 = b.span(b.code.span('span1'));
+ const span2 = b.span(b.code.span('span2'));
+
+ const start = b.node('start');
+ const body1 = b.node('body1');
+ const body2 = b.node('body2');
+
+ start
+ .otherwise(span1.start(body1));
+
+ body1
+ .otherwise(span2.start(body2));
+
+ body2
+ .skipTo(span2.end(span1.end(start)));
+
+ const res = sa.allocate(start);
+
+ assert.strictEqual(res.max, 1);
+
+ assert.strictEqual(res.concurrency.length, 2);
+ assert.ok(res.concurrency[0].includes(span1));
+ assert.ok(res.concurrency[1].includes(span2));
+
+ assert.strictEqual(res.colors.size, 2);
+ assert.strictEqual(res.colors.get(span1), 0);
+ assert.strictEqual(res.colors.get(span2), 1);
+ });
+
+ it('should allocate non-overlapping spans', () => {
+ const span1 = b.span(b.code.span('span1'));
+ const span2 = b.span(b.code.span('span2'));
+
+ const start = b.node('start');
+ const body1 = b.node('body1');
+ const body2 = b.node('body2');
+
+ start
+ .match('a', span1.start(body1))
+ .otherwise(span2.start(body2));
+
+ body1
+ .skipTo(span1.end(start));
+
+ body2
+ .skipTo(span2.end(start));
+
+ const res = sa.allocate(start);
+
+ assert.strictEqual(res.max, 0);
+
+ assert.strictEqual(res.concurrency.length, 1);
+ assert.ok(res.concurrency[0].includes(span1));
+ assert.ok(res.concurrency[0].includes(span2));
+
+ assert.strictEqual(res.colors.size, 2);
+ assert.strictEqual(res.colors.get(span1), 0);
+ assert.strictEqual(res.colors.get(span2), 0);
+ });
+
+ it('should throw on loops', () => {
+ const span = b.span(b.code.span('span_name'));
+
+ const start = b.node('start');
+
+ start
+ .otherwise(span.start(start));
+
+ assert.throws(() => {
+ sa.allocate(start);
+ }, /loop.*span_name/);
+ });
+
+ it('should throw on unmatched ends', () => {
+ const start = b.node('start');
+ const span = b.span(b.code.span('on_data'));
+
+ start.otherwise(span.end().skipTo(start));
+
+ assert.throws(() => sa.allocate(start), /unmatched.*on_data/i);
+ });
+
+ it('should throw on branched unmatched ends', () => {
+ const start = b.node('start');
+ const end = b.node('end');
+ const span = b.span(b.code.span('on_data'));
+
+ start
+ .match('a', end)
+ .match('b', span.start(end))
+ .otherwise(b.error(1, 'error'));
+
+ end
+ .otherwise(span.end(start));
+
+ assert.throws(() => sa.allocate(start), /unmatched.*on_data/i);
+ });
+
+ it('should propagate through the Invoke map', () => {
+ const start = b.node('start');
+ const span = b.span(b.code.span('llparse__on_data'));
+
+ b.property('i8', 'custom');
+
+ start.otherwise(b.invoke(b.code.load('custom'), {
+ 0: span.end().skipTo(start),
+ }, span.end().skipTo(start)));
+
+ assert.doesNotThrow(() => sa.allocate(span.start(start)));
+ });
+});
diff --git a/llparse-builder/tsconfig.json b/llparse-builder/tsconfig.json
new file mode 100644
index 0000000..01ec7c2
--- /dev/null
+++ b/llparse-builder/tsconfig.json
@@ -0,0 +1,15 @@
+{
+ "compilerOptions": {
+ "strict": true,
+ "target": "es2017",
+ "module": "commonjs",
+ "moduleResolution": "node",
+ "outDir": "./lib",
+ "declaration": true,
+ "pretty": true,
+ "sourceMap": true
+ },
+ "include": [
+ "src/**/*.ts"
+ ]
+}
diff --git a/llparse-builder/tslint.json b/llparse-builder/tslint.json
new file mode 100644
index 0000000..b0aaf97
--- /dev/null
+++ b/llparse-builder/tslint.json
@@ -0,0 +1,14 @@
+{
+ "defaultSeverity": "error",
+ "extends": [
+ "tslint:recommended"
+ ],
+ "jsRules": {},
+ "rules": {
+ "no-bitwise": null,
+ "quotemark": [
+ true, "single", "avoid-escape", "avoid-template"
+ ]
+ },
+ "rulesDirectory": []
+}
diff --git a/llparse-frontend/.gitignore b/llparse-frontend/.gitignore
new file mode 100644
index 0000000..88edb62
--- /dev/null
+++ b/llparse-frontend/.gitignore
@@ -0,0 +1,2 @@
+node_modules/
+lib/
diff --git a/llparse-frontend/.travis.yml b/llparse-frontend/.travis.yml
new file mode 100644
index 0000000..03f4af5
--- /dev/null
+++ b/llparse-frontend/.travis.yml
@@ -0,0 +1,6 @@
+sudo: false
+language: node_js
+node_js:
+ - "stable"
+script:
+ npm test
diff --git a/llparse-frontend/README.md b/llparse-frontend/README.md
new file mode 100644
index 0000000..359dd9b
--- /dev/null
+++ b/llparse-frontend/README.md
@@ -0,0 +1,30 @@
+# llparse-frontend
+[![Build Status](https://secure.travis-ci.org/indutny/llparse-frontend.svg)](http://travis-ci.org/indutny/llparse-frontend)
+[![NPM version](https://badge.fury.io/js/llparse-frontend.svg)](https://badge.fury.io/js/llparse-frontend)
+
+WIP
+
+#### LICENSE
+
+This software is licensed under the MIT License.
+
+Copyright Fedor Indutny, 2018.
+
+Permission is hereby granted, free of charge, to any person obtaining a
+copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to permit
+persons to whom the Software is furnished to do so, subject to the
+following conditions:
+
+The above copyright notice and this permission notice shall be included
+in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
+NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
+USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/llparse-frontend/package-lock.json b/llparse-frontend/package-lock.json
new file mode 100644
index 0000000..3cfef7a
--- /dev/null
+++ b/llparse-frontend/package-lock.json
@@ -0,0 +1,1516 @@
+{
+ "name": "llparse-frontend",
+ "version": "3.0.0",
+ "lockfileVersion": 1,
+ "requires": true,
+ "dependencies": {
+ "@babel/code-frame": {
+ "version": "7.10.3",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.3.tgz",
+ "integrity": "sha512-fDx9eNW0qz0WkUeqL6tXEXzVlPh6Y5aCDEZesl0xBGA8ndRukX91Uk44ZqnkECp01NAZUdCAl+aiQNGi0k88Eg==",
+ "dev": true,
+ "requires": {
+ "@babel/highlight": "^7.10.3"
+ }
+ },
+ "@babel/helper-validator-identifier": {
+ "version": "7.10.3",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.3.tgz",
+ "integrity": "sha512-bU8JvtlYpJSBPuj1VUmKpFGaDZuLxASky3LhaKj3bmpSTY6VWooSM8msk+Z0CZoErFye2tlABF6yDkT3FOPAXw==",
+ "dev": true
+ },
+ "@babel/highlight": {
+ "version": "7.10.3",
+ "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.3.tgz",
+ "integrity": "sha512-Ih9B/u7AtgEnySE2L2F0Xm0GaM729XqqLfHkalTsbjXGyqmf/6M0Cu0WpvqueUlW+xk88BHw9Nkpj49naU+vWw==",
+ "dev": true,
+ "requires": {
+ "@babel/helper-validator-identifier": "^7.10.3",
+ "chalk": "^2.0.0",
+ "js-tokens": "^4.0.0"
+ }
+ },
+ "@types/debug": {
+ "version": "4.1.5",
+ "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.5.tgz",
+ "integrity": "sha512-Q1y515GcOdTHgagaVFhHnIFQ38ygs/kmxdNpvpou+raI9UO3YZcHDngBSYKQklcKlvA7iuQlmIKbzvmxcOE9CQ==",
+ "dev": true
+ },
+ "@types/mocha": {
+ "version": "8.0.3",
+ "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-8.0.3.tgz",
+ "integrity": "sha512-vyxR57nv8NfcU0GZu8EUXZLTbCMupIUwy95LJ6lllN+JRPG25CwMHoB1q5xKh8YKhQnHYRAn4yW2yuHbf/5xgg==",
+ "dev": true
+ },
+ "@types/node": {
+ "version": "14.11.8",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-14.11.8.tgz",
+ "integrity": "sha512-KPcKqKm5UKDkaYPTuXSx8wEP7vE9GnuaXIZKijwRYcePpZFDVuy2a57LarFKiORbHOuTOOwYzxVxcUzsh2P2Pw==",
+ "dev": true
+ },
+ "ansi-colors": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz",
+ "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==",
+ "dev": true
+ },
+ "ansi-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
+ "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=",
+ "dev": true
+ },
+ "ansi-styles": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+ "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+ "dev": true,
+ "requires": {
+ "color-convert": "^1.9.0"
+ }
+ },
+ "anymatch": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz",
+ "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==",
+ "dev": true,
+ "requires": {
+ "normalize-path": "^3.0.0",
+ "picomatch": "^2.0.4"
+ }
+ },
+ "arg": {
+ "version": "4.1.3",
+ "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
+ "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
+ "dev": true
+ },
+ "argparse": {
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
+ "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
+ "dev": true,
+ "requires": {
+ "sprintf-js": "~1.0.2"
+ }
+ },
+ "array.prototype.map": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/array.prototype.map/-/array.prototype.map-1.0.2.tgz",
+ "integrity": "sha512-Az3OYxgsa1g7xDYp86l0nnN4bcmuEITGe1rbdEBVkrqkzMgDcbdQ2R7r41pNzti+4NMces3H8gMmuioZUilLgw==",
+ "dev": true,
+ "requires": {
+ "define-properties": "^1.1.3",
+ "es-abstract": "^1.17.0-next.1",
+ "es-array-method-boxes-properly": "^1.0.0",
+ "is-string": "^1.0.4"
+ }
+ },
+ "balanced-match": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
+ "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
+ "dev": true
+ },
+ "binary-extensions": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.1.0.tgz",
+ "integrity": "sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ==",
+ "dev": true
+ },
+ "binary-search": {
+ "version": "1.3.6",
+ "resolved": "https://registry.npmjs.org/binary-search/-/binary-search-1.3.6.tgz",
+ "integrity": "sha512-nbE1WxOTTrUWIfsfZ4aHGYu5DOuNkbxGokjV6Z2kxfJK3uaAb8zNK1muzOeipoLHZjInT4Br88BHpzevc681xA=="
+ },
+ "brace-expansion": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "dev": true,
+ "requires": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "braces": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
+ "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
+ "dev": true,
+ "requires": {
+ "fill-range": "^7.0.1"
+ }
+ },
+ "browser-stdout": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz",
+ "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==",
+ "dev": true
+ },
+ "buffer-from": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz",
+ "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==",
+ "dev": true
+ },
+ "builtin-modules": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz",
+ "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=",
+ "dev": true
+ },
+ "camelcase": {
+ "version": "5.3.1",
+ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
+ "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
+ "dev": true
+ },
+ "chalk": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
+ "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
+ "dev": true,
+ "requires": {
+ "ansi-styles": "^3.2.1",
+ "escape-string-regexp": "^1.0.5",
+ "supports-color": "^5.3.0"
+ }
+ },
+ "chokidar": {
+ "version": "3.4.2",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.4.2.tgz",
+ "integrity": "sha512-IZHaDeBeI+sZJRX7lGcXsdzgvZqKv6sECqsbErJA4mHWfpRrD8B97kSFN4cQz6nGBGiuFia1MKR4d6c1o8Cv7A==",
+ "dev": true,
+ "requires": {
+ "anymatch": "~3.1.1",
+ "braces": "~3.0.2",
+ "fsevents": "~2.1.2",
+ "glob-parent": "~5.1.0",
+ "is-binary-path": "~2.1.0",
+ "is-glob": "~4.0.1",
+ "normalize-path": "~3.0.0",
+ "readdirp": "~3.4.0"
+ }
+ },
+ "cliui": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz",
+ "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==",
+ "dev": true,
+ "requires": {
+ "string-width": "^3.1.0",
+ "strip-ansi": "^5.2.0",
+ "wrap-ansi": "^5.1.0"
+ },
+ "dependencies": {
+ "ansi-regex": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
+ "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
+ "dev": true
+ },
+ "string-width": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
+ "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==",
+ "dev": true,
+ "requires": {
+ "emoji-regex": "^7.0.1",
+ "is-fullwidth-code-point": "^2.0.0",
+ "strip-ansi": "^5.1.0"
+ }
+ },
+ "strip-ansi": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
+ "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
+ "dev": true,
+ "requires": {
+ "ansi-regex": "^4.1.0"
+ }
+ }
+ }
+ },
+ "color-convert": {
+ "version": "1.9.3",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
+ "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
+ "dev": true,
+ "requires": {
+ "color-name": "1.1.3"
+ }
+ },
+ "color-name": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
+ "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
+ "dev": true
+ },
+ "commander": {
+ "version": "2.15.1",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz",
+ "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==",
+ "dev": true
+ },
+ "concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
+ "dev": true
+ },
+ "debug": {
+ "version": "3.2.6",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
+ "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
+ "requires": {
+ "ms": "^2.1.1"
+ }
+ },
+ "decamelize": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
+ "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=",
+ "dev": true
+ },
+ "define-properties": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz",
+ "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==",
+ "requires": {
+ "object-keys": "^1.0.12"
+ }
+ },
+ "diff": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
+ "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
+ "dev": true
+ },
+ "emoji-regex": {
+ "version": "7.0.3",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz",
+ "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==",
+ "dev": true
+ },
+ "es-abstract": {
+ "version": "1.17.7",
+ "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.7.tgz",
+ "integrity": "sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==",
+ "requires": {
+ "es-to-primitive": "^1.2.1",
+ "function-bind": "^1.1.1",
+ "has": "^1.0.3",
+ "has-symbols": "^1.0.1",
+ "is-callable": "^1.2.2",
+ "is-regex": "^1.1.1",
+ "object-inspect": "^1.8.0",
+ "object-keys": "^1.1.1",
+ "object.assign": "^4.1.1",
+ "string.prototype.trimend": "^1.0.1",
+ "string.prototype.trimstart": "^1.0.1"
+ },
+ "dependencies": {
+ "es-abstract": {
+ "version": "1.18.0-next.1",
+ "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.1.tgz",
+ "integrity": "sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA==",
+ "requires": {
+ "es-to-primitive": "^1.2.1",
+ "function-bind": "^1.1.1",
+ "has": "^1.0.3",
+ "has-symbols": "^1.0.1",
+ "is-callable": "^1.2.2",
+ "is-negative-zero": "^2.0.0",
+ "is-regex": "^1.1.1",
+ "object-inspect": "^1.8.0",
+ "object-keys": "^1.1.1",
+ "object.assign": "^4.1.1",
+ "string.prototype.trimend": "^1.0.1",
+ "string.prototype.trimstart": "^1.0.1"
+ }
+ },
+ "object.assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.1.tgz",
+ "integrity": "sha512-VT/cxmx5yaoHSOTSyrCygIDFco+RsibY2NM0a4RdEeY/4KgqezwFtK1yr3U67xYhqJSlASm2pKhLVzPj2lr4bA==",
+ "requires": {
+ "define-properties": "^1.1.3",
+ "es-abstract": "^1.18.0-next.0",
+ "has-symbols": "^1.0.1",
+ "object-keys": "^1.1.1"
+ },
+ "dependencies": {
+ "es-abstract": {
+ "version": "1.18.0-next.1",
+ "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.1.tgz",
+ "integrity": "sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA==",
+ "requires": {
+ "es-to-primitive": "^1.2.1",
+ "function-bind": "^1.1.1",
+ "has": "^1.0.3",
+ "has-symbols": "^1.0.1",
+ "is-callable": "^1.2.2",
+ "is-negative-zero": "^2.0.0",
+ "is-regex": "^1.1.1",
+ "object-inspect": "^1.8.0",
+ "object-keys": "^1.1.1",
+ "object.assign": "^4.1.1",
+ "string.prototype.trimend": "^1.0.1",
+ "string.prototype.trimstart": "^1.0.1"
+ }
+ }
+ }
+ }
+ }
+ },
+ "es-array-method-boxes-properly": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz",
+ "integrity": "sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA==",
+ "dev": true
+ },
+ "es-get-iterator": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.0.tgz",
+ "integrity": "sha512-UfrmHuWQlNMTs35e1ypnvikg6jCz3SK8v8ImvmDsh36fCVUR1MqoFDiyn0/k52C8NqO3YsO8Oe0azeesNuqSsQ==",
+ "dev": true,
+ "requires": {
+ "es-abstract": "^1.17.4",
+ "has-symbols": "^1.0.1",
+ "is-arguments": "^1.0.4",
+ "is-map": "^2.0.1",
+ "is-set": "^2.0.1",
+ "is-string": "^1.0.5",
+ "isarray": "^2.0.5"
+ }
+ },
+ "es-to-primitive": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz",
+ "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==",
+ "requires": {
+ "is-callable": "^1.1.4",
+ "is-date-object": "^1.0.1",
+ "is-symbol": "^1.0.2"
+ }
+ },
+ "escape-string-regexp": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+ "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=",
+ "dev": true
+ },
+ "esprima": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
+ "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
+ "dev": true
+ },
+ "fill-range": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
+ "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
+ "dev": true,
+ "requires": {
+ "to-regex-range": "^5.0.1"
+ }
+ },
+ "find-up": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+ "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+ "dev": true,
+ "requires": {
+ "locate-path": "^6.0.0",
+ "path-exists": "^4.0.0"
+ }
+ },
+ "flat": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/flat/-/flat-4.1.0.tgz",
+ "integrity": "sha512-Px/TiLIznH7gEDlPXcUD4KnBusa6kR6ayRUVcnEAbreRIuhkqow/mun59BuRXwoYk7ZQOLW1ZM05ilIvK38hFw==",
+ "dev": true,
+ "requires": {
+ "is-buffer": "~2.0.3"
+ }
+ },
+ "fs.realpath": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+ "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
+ "dev": true
+ },
+ "fsevents": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz",
+ "integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==",
+ "dev": true,
+ "optional": true
+ },
+ "function-bind": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
+ "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
+ },
+ "get-caller-file": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
+ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
+ "dev": true
+ },
+ "glob": {
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
+ "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==",
+ "dev": true,
+ "requires": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.0.4",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ }
+ },
+ "glob-parent": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz",
+ "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==",
+ "dev": true,
+ "requires": {
+ "is-glob": "^4.0.1"
+ }
+ },
+ "growl": {
+ "version": "1.10.5",
+ "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz",
+ "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==",
+ "dev": true
+ },
+ "has": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
+ "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
+ "requires": {
+ "function-bind": "^1.1.1"
+ }
+ },
+ "has-flag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+ "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
+ "dev": true
+ },
+ "has-symbols": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz",
+ "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg=="
+ },
+ "he": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
+ "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
+ "dev": true
+ },
+ "inflight": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+ "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
+ "dev": true,
+ "requires": {
+ "once": "^1.3.0",
+ "wrappy": "1"
+ }
+ },
+ "inherits": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
+ "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=",
+ "dev": true
+ },
+ "is-arguments": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.0.4.tgz",
+ "integrity": "sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA==",
+ "dev": true
+ },
+ "is-binary-path": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+ "dev": true,
+ "requires": {
+ "binary-extensions": "^2.0.0"
+ }
+ },
+ "is-buffer": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.4.tgz",
+ "integrity": "sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A==",
+ "dev": true
+ },
+ "is-callable": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.2.tgz",
+ "integrity": "sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA=="
+ },
+ "is-date-object": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.2.tgz",
+ "integrity": "sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g=="
+ },
+ "is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
+ "dev": true
+ },
+ "is-fullwidth-code-point": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
+ "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=",
+ "dev": true
+ },
+ "is-glob": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz",
+ "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==",
+ "dev": true,
+ "requires": {
+ "is-extglob": "^2.1.1"
+ }
+ },
+ "is-map": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.1.tgz",
+ "integrity": "sha512-T/S49scO8plUiAOA2DBTBG3JHpn1yiw0kRp6dgiZ0v2/6twi5eiB0rHtHFH9ZIrvlWc6+4O+m4zg5+Z833aXgw==",
+ "dev": true
+ },
+ "is-negative-zero": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.0.tgz",
+ "integrity": "sha1-lVOxIbD6wohp2p7UWeIMdUN4hGE="
+ },
+ "is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "dev": true
+ },
+ "is-plain-obj": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz",
+ "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=",
+ "dev": true
+ },
+ "is-regex": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz",
+ "integrity": "sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==",
+ "requires": {
+ "has-symbols": "^1.0.1"
+ }
+ },
+ "is-set": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.1.tgz",
+ "integrity": "sha512-eJEzOtVyenDs1TMzSQ3kU3K+E0GUS9sno+F0OBT97xsgcJsF9nXMBtkT9/kut5JEpM7oL7X/0qxR17K3mcwIAA==",
+ "dev": true
+ },
+ "is-string": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.5.tgz",
+ "integrity": "sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ==",
+ "dev": true
+ },
+ "is-symbol": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz",
+ "integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==",
+ "requires": {
+ "has-symbols": "^1.0.1"
+ }
+ },
+ "isarray": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
+ "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==",
+ "dev": true
+ },
+ "isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=",
+ "dev": true
+ },
+ "iterate-iterator": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/iterate-iterator/-/iterate-iterator-1.0.1.tgz",
+ "integrity": "sha512-3Q6tudGN05kbkDQDI4CqjaBf4qf85w6W6GnuZDtUVYwKgtC1q8yxYX7CZed7N+tLzQqS6roujWvszf13T+n9aw==",
+ "dev": true
+ },
+ "iterate-value": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/iterate-value/-/iterate-value-1.0.2.tgz",
+ "integrity": "sha512-A6fMAio4D2ot2r/TYzr4yUWrmwNdsN5xL7+HUiyACE4DXm+q8HtPcnFTp+NnW3k4N05tZ7FVYFFb2CR13NxyHQ==",
+ "dev": true,
+ "requires": {
+ "es-get-iterator": "^1.0.2",
+ "iterate-iterator": "^1.0.1"
+ }
+ },
+ "js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "dev": true
+ },
+ "js-yaml": {
+ "version": "3.14.0",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.0.tgz",
+ "integrity": "sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A==",
+ "dev": true,
+ "requires": {
+ "argparse": "^1.0.7",
+ "esprima": "^4.0.0"
+ }
+ },
+ "llparse-builder": {
+ "version": "1.5.2",
+ "resolved": "https://registry.npmjs.org/llparse-builder/-/llparse-builder-1.5.2.tgz",
+ "integrity": "sha512-i862UNC3YUEdlfK/NUCJxlKjtWjgAI9AJXDRgjcfRHfwFt4Sf8eFPTRsc91/2R9MBZ0kyFdfhi8SVhMsZf1gNQ==",
+ "requires": {
+ "@types/debug": "4.1.5 ",
+ "binary-search": "^1.3.6",
+ "debug": "^4.2.0"
+ },
+ "dependencies": {
+ "@types/debug": {
+ "version": "4.1.5",
+ "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.5.tgz",
+ "integrity": "sha512-Q1y515GcOdTHgagaVFhHnIFQ38ygs/kmxdNpvpou+raI9UO3YZcHDngBSYKQklcKlvA7iuQlmIKbzvmxcOE9CQ=="
+ },
+ "debug": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz",
+ "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==",
+ "requires": {
+ "ms": "2.1.2"
+ }
+ },
+ "ms": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
+ }
+ }
+ },
+ "locate-path": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+ "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+ "dev": true,
+ "requires": {
+ "p-locate": "^5.0.0"
+ }
+ },
+ "log-symbols": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.0.0.tgz",
+ "integrity": "sha512-FN8JBzLx6CzeMrB0tg6pqlGU1wCrXW+ZXGH481kfsBqer0hToTIiHdjH4Mq8xJUbvATujKCvaREGWpGUionraA==",
+ "dev": true,
+ "requires": {
+ "chalk": "^4.0.0"
+ },
+ "dependencies": {
+ "ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "requires": {
+ "color-convert": "^2.0.1"
+ }
+ },
+ "chalk": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
+ "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==",
+ "dev": true,
+ "requires": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ }
+ },
+ "color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "requires": {
+ "color-name": "~1.1.4"
+ }
+ },
+ "color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
+ "has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true
+ },
+ "supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "requires": {
+ "has-flag": "^4.0.0"
+ }
+ }
+ }
+ },
+ "make-error": {
+ "version": "1.3.6",
+ "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
+ "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
+ "dev": true
+ },
+ "minimatch": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
+ "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
+ "dev": true,
+ "requires": {
+ "brace-expansion": "^1.1.7"
+ }
+ },
+ "mkdirp": {
+ "version": "0.5.5",
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
+ "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
+ "dev": true,
+ "requires": {
+ "minimist": "^1.2.5"
+ },
+ "dependencies": {
+ "minimist": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
+ "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
+ "dev": true
+ }
+ }
+ },
+ "mocha": {
+ "version": "8.1.3",
+ "resolved": "https://registry.npmjs.org/mocha/-/mocha-8.1.3.tgz",
+ "integrity": "sha512-ZbaYib4hT4PpF4bdSO2DohooKXIn4lDeiYqB+vTmCdr6l2woW0b6H3pf5x4sM5nwQMru9RvjjHYWVGltR50ZBw==",
+ "dev": true,
+ "requires": {
+ "ansi-colors": "4.1.1",
+ "browser-stdout": "1.3.1",
+ "chokidar": "3.4.2",
+ "debug": "4.1.1",
+ "diff": "4.0.2",
+ "escape-string-regexp": "4.0.0",
+ "find-up": "5.0.0",
+ "glob": "7.1.6",
+ "growl": "1.10.5",
+ "he": "1.2.0",
+ "js-yaml": "3.14.0",
+ "log-symbols": "4.0.0",
+ "minimatch": "3.0.4",
+ "ms": "2.1.2",
+ "object.assign": "4.1.0",
+ "promise.allsettled": "1.0.2",
+ "serialize-javascript": "4.0.0",
+ "strip-json-comments": "3.0.1",
+ "supports-color": "7.1.0",
+ "which": "2.0.2",
+ "wide-align": "1.1.3",
+ "workerpool": "6.0.0",
+ "yargs": "13.3.2",
+ "yargs-parser": "13.1.2",
+ "yargs-unparser": "1.6.1"
+ },
+ "dependencies": {
+ "debug": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
+ "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
+ "dev": true,
+ "requires": {
+ "ms": "^2.1.1"
+ }
+ },
+ "diff": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
+ "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
+ "dev": true
+ },
+ "escape-string-regexp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "dev": true
+ },
+ "glob": {
+ "version": "7.1.6",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
+ "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
+ "dev": true,
+ "requires": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.0.4",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ }
+ },
+ "has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true
+ },
+ "ms": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
+ "dev": true
+ },
+ "supports-color": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz",
+ "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==",
+ "dev": true,
+ "requires": {
+ "has-flag": "^4.0.0"
+ }
+ }
+ }
+ },
+ "ms": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
+ "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg=="
+ },
+ "normalize-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+ "dev": true
+ },
+ "object-inspect": {
+ "version": "1.8.0",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.8.0.tgz",
+ "integrity": "sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA=="
+ },
+ "object-keys": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
+ "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="
+ },
+ "object.assign": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz",
+ "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==",
+ "dev": true,
+ "requires": {
+ "define-properties": "^1.1.2",
+ "function-bind": "^1.1.1",
+ "has-symbols": "^1.0.0",
+ "object-keys": "^1.0.11"
+ }
+ },
+ "once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
+ "dev": true,
+ "requires": {
+ "wrappy": "1"
+ }
+ },
+ "p-limit": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.0.2.tgz",
+ "integrity": "sha512-iwqZSOoWIW+Ew4kAGUlN16J4M7OB3ysMLSZtnhmqx7njIHFPlxWBX8xo3lVTyFVq6mI/lL9qt2IsN1sHwaxJkg==",
+ "dev": true,
+ "requires": {
+ "p-try": "^2.0.0"
+ }
+ },
+ "p-locate": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+ "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+ "dev": true,
+ "requires": {
+ "p-limit": "^3.0.2"
+ }
+ },
+ "p-try": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
+ "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
+ "dev": true
+ },
+ "path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "dev": true
+ },
+ "path-is-absolute": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+ "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
+ "dev": true
+ },
+ "path-parse": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz",
+ "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==",
+ "dev": true
+ },
+ "picomatch": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz",
+ "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==",
+ "dev": true
+ },
+ "promise.allsettled": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/promise.allsettled/-/promise.allsettled-1.0.2.tgz",
+ "integrity": "sha512-UpcYW5S1RaNKT6pd+s9jp9K9rlQge1UXKskec0j6Mmuq7UJCvlS2J2/s/yuPN8ehftf9HXMxWlKiPbGGUzpoRg==",
+ "dev": true,
+ "requires": {
+ "array.prototype.map": "^1.0.1",
+ "define-properties": "^1.1.3",
+ "es-abstract": "^1.17.0-next.1",
+ "function-bind": "^1.1.1",
+ "iterate-value": "^1.0.0"
+ }
+ },
+ "randombytes": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
+ "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
+ "dev": true,
+ "requires": {
+ "safe-buffer": "^5.1.0"
+ }
+ },
+ "readdirp": {
+ "version": "3.4.0",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.4.0.tgz",
+ "integrity": "sha512-0xe001vZBnJEK+uKcj8qOhyAKPzIT+gStxWr3LCB0DwcXR5NZJ3IaC+yGnHCYzB/S7ov3m3EEbZI2zeNvX+hGQ==",
+ "dev": true,
+ "requires": {
+ "picomatch": "^2.2.1"
+ }
+ },
+ "require-directory": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
+ "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=",
+ "dev": true
+ },
+ "require-main-filename": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
+ "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
+ "dev": true
+ },
+ "resolve": {
+ "version": "1.17.0",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz",
+ "integrity": "sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==",
+ "dev": true,
+ "requires": {
+ "path-parse": "^1.0.6"
+ }
+ },
+ "safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "dev": true
+ },
+ "semver": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
+ "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
+ "dev": true
+ },
+ "serialize-javascript": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz",
+ "integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==",
+ "dev": true,
+ "requires": {
+ "randombytes": "^2.1.0"
+ }
+ },
+ "set-blocking": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
+ "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=",
+ "dev": true
+ },
+ "source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "dev": true
+ },
+ "source-map-support": {
+ "version": "0.5.19",
+ "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz",
+ "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==",
+ "dev": true,
+ "requires": {
+ "buffer-from": "^1.0.0",
+ "source-map": "^0.6.0"
+ }
+ },
+ "sprintf-js": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
+ "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=",
+ "dev": true
+ },
+ "string-width": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz",
+ "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==",
+ "dev": true,
+ "requires": {
+ "is-fullwidth-code-point": "^2.0.0",
+ "strip-ansi": "^4.0.0"
+ }
+ },
+ "string.prototype.trimend": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz",
+ "integrity": "sha512-LRPxFUaTtpqYsTeNKaFOw3R4bxIzWOnbQ837QfBylo8jIxtcbK/A/sMV7Q+OAV/vWo+7s25pOE10KYSjaSO06g==",
+ "requires": {
+ "define-properties": "^1.1.3",
+ "es-abstract": "^1.17.5"
+ }
+ },
+ "string.prototype.trimstart": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz",
+ "integrity": "sha512-XxZn+QpvrBI1FOcg6dIpxUPgWCPuNXvMD72aaRaUQv1eD4e/Qy8i/hFTe0BUmD60p/QA6bh1avmuPTfNjqVWRw==",
+ "requires": {
+ "define-properties": "^1.1.3",
+ "es-abstract": "^1.17.5"
+ }
+ },
+ "strip-ansi": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
+ "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
+ "dev": true,
+ "requires": {
+ "ansi-regex": "^3.0.0"
+ }
+ },
+ "strip-json-comments": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.0.1.tgz",
+ "integrity": "sha512-VTyMAUfdm047mwKl+u79WIdrZxtFtn+nBxHeb844XBQ9uMNTuTHdx2hc5RiAJYqwTj3wc/xe5HLSdJSkJ+WfZw==",
+ "dev": true
+ },
+ "supports-color": {
+ "version": "5.4.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz",
+ "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==",
+ "dev": true,
+ "requires": {
+ "has-flag": "^3.0.0"
+ }
+ },
+ "to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "dev": true,
+ "requires": {
+ "is-number": "^7.0.0"
+ }
+ },
+ "ts-node": {
+ "version": "9.0.0",
+ "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-9.0.0.tgz",
+ "integrity": "sha512-/TqB4SnererCDR/vb4S/QvSZvzQMJN8daAslg7MeaiHvD8rDZsSfXmNeNumyZZzMned72Xoq/isQljYSt8Ynfg==",
+ "dev": true,
+ "requires": {
+ "arg": "^4.1.0",
+ "diff": "^4.0.1",
+ "make-error": "^1.1.1",
+ "source-map-support": "^0.5.17",
+ "yn": "3.1.1"
+ }
+ },
+ "tslib": {
+ "version": "1.13.0",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz",
+ "integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==",
+ "dev": true
+ },
+ "tslint": {
+ "version": "5.20.1",
+ "resolved": "https://registry.npmjs.org/tslint/-/tslint-5.20.1.tgz",
+ "integrity": "sha512-EcMxhzCFt8k+/UP5r8waCf/lzmeSyVlqxqMEDQE7rWYiQky8KpIBz1JAoYXfROHrPZ1XXd43q8yQnULOLiBRQg==",
+ "dev": true,
+ "requires": {
+ "@babel/code-frame": "^7.0.0",
+ "builtin-modules": "^1.1.1",
+ "chalk": "^2.3.0",
+ "commander": "^2.12.1",
+ "diff": "^4.0.1",
+ "glob": "^7.1.1",
+ "js-yaml": "^3.13.1",
+ "minimatch": "^3.0.4",
+ "mkdirp": "^0.5.1",
+ "resolve": "^1.3.2",
+ "semver": "^5.3.0",
+ "tslib": "^1.8.0",
+ "tsutils": "^2.29.0"
+ },
+ "dependencies": {
+ "diff": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
+ "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
+ "dev": true
+ }
+ }
+ },
+ "tsutils": {
+ "version": "2.29.0",
+ "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.29.0.tgz",
+ "integrity": "sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA==",
+ "dev": true,
+ "requires": {
+ "tslib": "^1.8.1"
+ }
+ },
+ "typescript": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.0.3.tgz",
+ "integrity": "sha512-tEu6DGxGgRJPb/mVPIZ48e69xCn2yRmCgYmDugAVwmJ6o+0u1RI18eO7E7WBTLYLaEVVOhwQmcdhQHweux/WPg==",
+ "dev": true
+ },
+ "which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dev": true,
+ "requires": {
+ "isexe": "^2.0.0"
+ }
+ },
+ "which-module": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz",
+ "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=",
+ "dev": true
+ },
+ "wide-align": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz",
+ "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==",
+ "dev": true,
+ "requires": {
+ "string-width": "^1.0.2 || 2"
+ }
+ },
+ "workerpool": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.0.0.tgz",
+ "integrity": "sha512-fU2OcNA/GVAJLLyKUoHkAgIhKb0JoCpSjLC/G2vYKxUjVmQwGbRVeoPJ1a8U4pnVofz4AQV5Y/NEw8oKqxEBtA==",
+ "dev": true
+ },
+ "wrap-ansi": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz",
+ "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==",
+ "dev": true,
+ "requires": {
+ "ansi-styles": "^3.2.0",
+ "string-width": "^3.0.0",
+ "strip-ansi": "^5.0.0"
+ },
+ "dependencies": {
+ "ansi-regex": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
+ "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
+ "dev": true
+ },
+ "string-width": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
+ "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==",
+ "dev": true,
+ "requires": {
+ "emoji-regex": "^7.0.1",
+ "is-fullwidth-code-point": "^2.0.0",
+ "strip-ansi": "^5.1.0"
+ }
+ },
+ "strip-ansi": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
+ "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
+ "dev": true,
+ "requires": {
+ "ansi-regex": "^4.1.0"
+ }
+ }
+ }
+ },
+ "wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
+ "dev": true
+ },
+ "y18n": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz",
+ "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==",
+ "dev": true
+ },
+ "yargs": {
+ "version": "13.3.2",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz",
+ "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==",
+ "dev": true,
+ "requires": {
+ "cliui": "^5.0.0",
+ "find-up": "^3.0.0",
+ "get-caller-file": "^2.0.1",
+ "require-directory": "^2.1.1",
+ "require-main-filename": "^2.0.0",
+ "set-blocking": "^2.0.0",
+ "string-width": "^3.0.0",
+ "which-module": "^2.0.0",
+ "y18n": "^4.0.0",
+ "yargs-parser": "^13.1.2"
+ },
+ "dependencies": {
+ "ansi-regex": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
+ "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
+ "dev": true
+ },
+ "find-up": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz",
+ "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==",
+ "dev": true,
+ "requires": {
+ "locate-path": "^3.0.0"
+ }
+ },
+ "locate-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz",
+ "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==",
+ "dev": true,
+ "requires": {
+ "p-locate": "^3.0.0",
+ "path-exists": "^3.0.0"
+ }
+ },
+ "p-limit": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
+ "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
+ "dev": true,
+ "requires": {
+ "p-try": "^2.0.0"
+ }
+ },
+ "p-locate": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz",
+ "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==",
+ "dev": true,
+ "requires": {
+ "p-limit": "^2.0.0"
+ }
+ },
+ "path-exists": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz",
+ "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=",
+ "dev": true
+ },
+ "string-width": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
+ "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==",
+ "dev": true,
+ "requires": {
+ "emoji-regex": "^7.0.1",
+ "is-fullwidth-code-point": "^2.0.0",
+ "strip-ansi": "^5.1.0"
+ }
+ },
+ "strip-ansi": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
+ "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
+ "dev": true,
+ "requires": {
+ "ansi-regex": "^4.1.0"
+ }
+ }
+ }
+ },
+ "yargs-parser": {
+ "version": "13.1.2",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz",
+ "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==",
+ "dev": true,
+ "requires": {
+ "camelcase": "^5.0.0",
+ "decamelize": "^1.2.0"
+ }
+ },
+ "yargs-unparser": {
+ "version": "1.6.1",
+ "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-1.6.1.tgz",
+ "integrity": "sha512-qZV14lK9MWsGCmcr7u5oXGH0dbGqZAIxTDrWXZDo5zUr6b6iUmelNKO6x6R1dQT24AH3LgRxJpr8meWy2unolA==",
+ "dev": true,
+ "requires": {
+ "camelcase": "^5.3.1",
+ "decamelize": "^1.2.0",
+ "flat": "^4.1.0",
+ "is-plain-obj": "^1.1.0",
+ "yargs": "^14.2.3"
+ },
+ "dependencies": {
+ "ansi-regex": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
+ "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
+ "dev": true
+ },
+ "find-up": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz",
+ "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==",
+ "dev": true,
+ "requires": {
+ "locate-path": "^3.0.0"
+ }
+ },
+ "locate-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz",
+ "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==",
+ "dev": true,
+ "requires": {
+ "p-locate": "^3.0.0",
+ "path-exists": "^3.0.0"
+ }
+ },
+ "p-limit": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
+ "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
+ "dev": true,
+ "requires": {
+ "p-try": "^2.0.0"
+ }
+ },
+ "p-locate": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz",
+ "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==",
+ "dev": true,
+ "requires": {
+ "p-limit": "^2.0.0"
+ }
+ },
+ "path-exists": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz",
+ "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=",
+ "dev": true
+ },
+ "string-width": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
+ "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==",
+ "dev": true,
+ "requires": {
+ "emoji-regex": "^7.0.1",
+ "is-fullwidth-code-point": "^2.0.0",
+ "strip-ansi": "^5.1.0"
+ }
+ },
+ "strip-ansi": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
+ "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
+ "dev": true,
+ "requires": {
+ "ansi-regex": "^4.1.0"
+ }
+ },
+ "yargs": {
+ "version": "14.2.3",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-14.2.3.tgz",
+ "integrity": "sha512-ZbotRWhF+lkjijC/VhmOT9wSgyBQ7+zr13+YLkhfsSiTriYsMzkTUFP18pFhWwBeMa5gUc1MzbhrO6/VB7c9Xg==",
+ "dev": true,
+ "requires": {
+ "cliui": "^5.0.0",
+ "decamelize": "^1.2.0",
+ "find-up": "^3.0.0",
+ "get-caller-file": "^2.0.1",
+ "require-directory": "^2.1.1",
+ "require-main-filename": "^2.0.0",
+ "set-blocking": "^2.0.0",
+ "string-width": "^3.0.0",
+ "which-module": "^2.0.0",
+ "y18n": "^4.0.0",
+ "yargs-parser": "^15.0.1"
+ }
+ },
+ "yargs-parser": {
+ "version": "15.0.1",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-15.0.1.tgz",
+ "integrity": "sha512-0OAMV2mAZQrs3FkNpDQcBk1x5HXb8X4twADss4S0Iuk+2dGnLOE/fRHrsYm542GduMveyA77OF4wrNJuanRCWw==",
+ "dev": true,
+ "requires": {
+ "camelcase": "^5.0.0",
+ "decamelize": "^1.2.0"
+ }
+ }
+ }
+ },
+ "yn": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
+ "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
+ "dev": true
+ }
+ }
+}
diff --git a/llparse-frontend/package.json b/llparse-frontend/package.json
new file mode 100644
index 0000000..8afea88
--- /dev/null
+++ b/llparse-frontend/package.json
@@ -0,0 +1,43 @@
+{
+ "name": "llparse-frontend",
+ "version": "3.0.0",
+ "description": "Frontend for LLParse compiler",
+ "main": "lib/frontend.js",
+ "types": "lib/frontend.d.ts",
+ "scripts": {
+ "build": "tsc",
+ "clean": "rm -rf lib",
+ "prepare": "npm run clean && npm run build",
+ "lint": "tslint -c tslint.json src/**/*.ts test/**/*.ts",
+ "fix-lint": "npm run lint -- --fix",
+ "mocha": "mocha --timeout=10000 -r ts-node/register/type-check --reporter spec test/*-test.ts",
+ "test": "npm run mocha && npm run lint"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+ssh://git@github.com/indutny/llparse-frontend.git"
+ },
+ "keywords": [
+ "llparse",
+ "frontend"
+ ],
+ "author": "Fedor Indutny <fedor@indutny.com> (http://darksi.de/)",
+ "license": "MIT",
+ "bugs": {
+ "url": "https://github.com/indutny/llparse-frontend/issues"
+ },
+ "homepage": "https://github.com/indutny/llparse-frontend#readme",
+ "dependencies": {
+ "debug": "^3.2.6",
+ "llparse-builder": "^1.5.2"
+ },
+ "devDependencies": {
+ "@types/debug": "^4.1.5",
+ "@types/mocha": "^8.0.3",
+ "@types/node": "^14.11.8",
+ "mocha": "^8.1.3",
+ "ts-node": "^9.0.0",
+ "tslint": "^5.20.1",
+ "typescript": "^4.0.3"
+ }
+}
diff --git a/llparse-frontend/src/code/and.ts b/llparse-frontend/src/code/and.ts
new file mode 100644
index 0000000..54dc5fd
--- /dev/null
+++ b/llparse-frontend/src/code/and.ts
@@ -0,0 +1,8 @@
+import { toCacheKey } from '../utils';
+import { FieldValue } from './field-value';
+
+export class And extends FieldValue {
+ constructor(name: string, field: string, value: number) {
+ super('match', `and_${field}_${toCacheKey(value)}`, name, field, value);
+ }
+}
diff --git a/llparse-frontend/src/code/base.ts b/llparse-frontend/src/code/base.ts
new file mode 100644
index 0000000..cde4b6d
--- /dev/null
+++ b/llparse-frontend/src/code/base.ts
@@ -0,0 +1,8 @@
+export type Signature = 'match' | 'value' | 'span';
+
+export abstract class Code {
+ constructor(public readonly signature: Signature,
+ public readonly cacheKey: string,
+ public readonly name: string) {
+ }
+}
diff --git a/llparse-frontend/src/code/external.ts b/llparse-frontend/src/code/external.ts
new file mode 100644
index 0000000..f4254c1
--- /dev/null
+++ b/llparse-frontend/src/code/external.ts
@@ -0,0 +1,7 @@
+import { Code, Signature } from './base';
+
+export abstract class External extends Code {
+ constructor(signature: Signature, name: string) {
+ super(signature, 'external_' + name, name);
+ }
+}
diff --git a/llparse-frontend/src/code/field-value.ts b/llparse-frontend/src/code/field-value.ts
new file mode 100644
index 0000000..1c7c109
--- /dev/null
+++ b/llparse-frontend/src/code/field-value.ts
@@ -0,0 +1,13 @@
+import * as assert from 'assert';
+
+import { Signature } from './base';
+import { Field } from './field';
+
+export abstract class FieldValue extends Field {
+ constructor(signature: Signature, cacheKey: string, name: string,
+ field: string, public readonly value: number) {
+ super(signature, cacheKey, name, field);
+
+ assert.strictEqual(value, value | 0, 'FieldValue `value` must be integer');
+ }
+}
diff --git a/llparse-frontend/src/code/field.ts b/llparse-frontend/src/code/field.ts
new file mode 100644
index 0000000..c60b8ef
--- /dev/null
+++ b/llparse-frontend/src/code/field.ts
@@ -0,0 +1,8 @@
+import { Code, Signature } from './base';
+
+export abstract class Field extends Code {
+ constructor(signature: Signature, cacheKey: string, name: string,
+ public readonly field: string) {
+ super(signature, cacheKey, name);
+ }
+}
diff --git a/llparse-frontend/src/code/index.ts b/llparse-frontend/src/code/index.ts
new file mode 100644
index 0000000..c7d5c69
--- /dev/null
+++ b/llparse-frontend/src/code/index.ts
@@ -0,0 +1,15 @@
+export * from './and';
+export * from './base';
+export * from './external';
+export * from './field-value';
+export * from './field';
+export * from './is-equal';
+export * from './load';
+export * from './match';
+export * from './mul-add';
+export * from './or';
+export * from './span';
+export * from './store';
+export * from './test';
+export * from './update';
+export * from './value';
diff --git a/llparse-frontend/src/code/is-equal.ts b/llparse-frontend/src/code/is-equal.ts
new file mode 100644
index 0000000..16a2ee2
--- /dev/null
+++ b/llparse-frontend/src/code/is-equal.ts
@@ -0,0 +1,9 @@
+import { toCacheKey } from '../utils';
+import { FieldValue } from './field-value';
+
+export class IsEqual extends FieldValue {
+ constructor(name: string, field: string, value: number) {
+ super('match', `is_equal_${field}_${toCacheKey(value)}`, name, field,
+ value);
+ }
+}
diff --git a/llparse-frontend/src/code/load.ts b/llparse-frontend/src/code/load.ts
new file mode 100644
index 0000000..76b715a
--- /dev/null
+++ b/llparse-frontend/src/code/load.ts
@@ -0,0 +1,7 @@
+import { Field } from './field';
+
+export class Load extends Field {
+ constructor(name: string, field: string) {
+ super('match', `load_${field}`, name, field);
+ }
+}
diff --git a/llparse-frontend/src/code/match.ts b/llparse-frontend/src/code/match.ts
new file mode 100644
index 0000000..819d2af
--- /dev/null
+++ b/llparse-frontend/src/code/match.ts
@@ -0,0 +1,7 @@
+import { External } from './external';
+
+export class Match extends External {
+ constructor(name: string) {
+ super('match', name);
+ }
+}
diff --git a/llparse-frontend/src/code/mul-add.ts b/llparse-frontend/src/code/mul-add.ts
new file mode 100644
index 0000000..c99be0d
--- /dev/null
+++ b/llparse-frontend/src/code/mul-add.ts
@@ -0,0 +1,26 @@
+import { toCacheKey } from '../utils';
+import { Field } from './field';
+
+export interface IMulAddOptions {
+ readonly base: number;
+ readonly max?: number;
+ readonly signed: boolean;
+}
+
+function toOptionsKey(options: IMulAddOptions): string {
+ let res = `base_${toCacheKey(options.base)}`;
+ if (options.max !== undefined) {
+ res += `_max_${toCacheKey(options.max)}`;
+ }
+ if (options.signed !== undefined) {
+ res += `_signed_${toCacheKey(options.signed)}`;
+ }
+ return res;
+}
+
+export class MulAdd extends Field {
+ constructor(name: string, field: string,
+ public readonly options: IMulAddOptions) {
+ super('value', `mul_add_${field}_${toOptionsKey(options)}`, name, field);
+ }
+}
diff --git a/llparse-frontend/src/code/or.ts b/llparse-frontend/src/code/or.ts
new file mode 100644
index 0000000..2328a9f
--- /dev/null
+++ b/llparse-frontend/src/code/or.ts
@@ -0,0 +1,8 @@
+import { toCacheKey } from '../utils';
+import { FieldValue } from './field-value';
+
+export class Or extends FieldValue {
+ constructor(name: string, field: string, value: number) {
+ super('match', `or_${field}_${toCacheKey(value)}`, name, field, value);
+ }
+}
diff --git a/llparse-frontend/src/code/span.ts b/llparse-frontend/src/code/span.ts
new file mode 100644
index 0000000..6241e03
--- /dev/null
+++ b/llparse-frontend/src/code/span.ts
@@ -0,0 +1,7 @@
+import { External } from './external';
+
+export class Span extends External {
+ constructor(name: string) {
+ super('span', name);
+ }
+}
diff --git a/llparse-frontend/src/code/store.ts b/llparse-frontend/src/code/store.ts
new file mode 100644
index 0000000..c2cb9ea
--- /dev/null
+++ b/llparse-frontend/src/code/store.ts
@@ -0,0 +1,7 @@
+import { Field } from './field';
+
+export class Store extends Field {
+ constructor(name: string, field: string) {
+ super('value', `store_${field}`, name, field);
+ }
+}
diff --git a/llparse-frontend/src/code/test.ts b/llparse-frontend/src/code/test.ts
new file mode 100644
index 0000000..21339e9
--- /dev/null
+++ b/llparse-frontend/src/code/test.ts
@@ -0,0 +1,8 @@
+import { toCacheKey } from '../utils';
+import { FieldValue } from './field-value';
+
+export class Test extends FieldValue {
+ constructor(name: string, field: string, value: number) {
+ super('match', `test_${field}_${toCacheKey(value)}`, name, field, value);
+ }
+}
diff --git a/llparse-frontend/src/code/update.ts b/llparse-frontend/src/code/update.ts
new file mode 100644
index 0000000..5fa5eec
--- /dev/null
+++ b/llparse-frontend/src/code/update.ts
@@ -0,0 +1,8 @@
+import { toCacheKey } from '../utils';
+import { FieldValue } from './field-value';
+
+export class Update extends FieldValue {
+ constructor(name: string, field: string, value: number) {
+ super('match', `update_${field}_${toCacheKey(value)}`, name, field, value);
+ }
+}
diff --git a/llparse-frontend/src/code/value.ts b/llparse-frontend/src/code/value.ts
new file mode 100644
index 0000000..4f32ae8
--- /dev/null
+++ b/llparse-frontend/src/code/value.ts
@@ -0,0 +1,7 @@
+import { External } from './external';
+
+export class Value extends External {
+ constructor(name: string) {
+ super('value', name);
+ }
+}
diff --git a/llparse-frontend/src/container/index.ts b/llparse-frontend/src/container/index.ts
new file mode 100644
index 0000000..a62aac8
--- /dev/null
+++ b/llparse-frontend/src/container/index.ts
@@ -0,0 +1,84 @@
+import * as assert from 'assert';
+
+import { ICodeImplementation } from '../implementation/code';
+import { IImplementation } from '../implementation/full';
+import { INodeImplementation } from '../implementation/node';
+import { ITransformImplementation } from '../implementation/transform';
+import { IWrap } from '../wrap';
+import { ContainerWrap } from './wrap';
+
+export { ContainerWrap };
+
+export class Container {
+ private readonly map: Map<string, IImplementation> = new Map();
+
+ public add(key: string, impl: IImplementation): void {
+ assert(!this.map.has(key), `Duplicate implementation key: "${key}"`);
+ this.map.set(key, impl);
+ }
+
+ public build(): IImplementation {
+ return {
+ code: this.buildCode(),
+ node: this.buildNode(),
+ transform: this.buildTransform(),
+ };
+ }
+
+ public buildCode(): ICodeImplementation {
+ return {
+ And: this.combine((impl) => impl.code.And),
+ IsEqual: this.combine((impl) => impl.code.IsEqual),
+ Load: this.combine((impl) => impl.code.Load),
+ Match: this.combine((impl) => impl.code.Match),
+ MulAdd: this.combine((impl) => impl.code.MulAdd),
+ Or: this.combine((impl) => impl.code.Or),
+ Span: this.combine((impl) => impl.code.Span),
+ Store: this.combine((impl) => impl.code.Store),
+ Test: this.combine((impl) => impl.code.Test),
+ Update: this.combine((impl) => impl.code.Update),
+ Value: this.combine((impl) => impl.code.Value),
+ };
+ }
+
+ public buildNode(): INodeImplementation {
+ return {
+ Consume: this.combine((impl) => impl.node.Consume),
+ Empty: this.combine((impl) => impl.node.Empty),
+ Error: this.combine((impl) => impl.node.Error),
+ Invoke: this.combine((impl) => impl.node.Invoke),
+ Pause: this.combine((impl) => impl.node.Pause),
+ Sequence: this.combine((impl) => impl.node.Sequence),
+ Single: this.combine((impl) => impl.node.Single),
+ SpanEnd: this.combine((impl) => impl.node.SpanEnd),
+ SpanStart: this.combine((impl) => impl.node.SpanStart),
+ TableLookup: this.combine((impl) => impl.node.TableLookup),
+ };
+ }
+
+ public buildTransform(): ITransformImplementation {
+ return {
+ ID: this.combine((impl) => impl.transform.ID),
+ ToLower: this.combine((impl) => impl.transform.ToLower),
+ ToLowerUnsafe: this.combine((impl) => impl.transform.ToLowerUnsafe),
+ };
+ }
+
+ private combine<T>(gather: (impl: IImplementation) => new(n: T) => IWrap<T>)
+ : new(n: T) => ContainerWrap<T> {
+ const wraps: Map<string, new(n: T) => IWrap<T>> = new Map();
+ for (const [ key, impl ] of this.map) {
+ wraps.set(key, gather(impl));
+ }
+
+ return class ContainerWrapSingle extends ContainerWrap<T> {
+ constructor(ref: T) {
+ super(ref);
+
+ for (const [ key, impl ] of wraps) {
+ this.map.set(key, new impl(ref));
+ }
+ }
+ };
+ }
+}
diff --git a/llparse-frontend/src/container/wrap.ts b/llparse-frontend/src/container/wrap.ts
new file mode 100644
index 0000000..f3b886c
--- /dev/null
+++ b/llparse-frontend/src/container/wrap.ts
@@ -0,0 +1,15 @@
+import * as assert from 'assert';
+
+import { IWrap } from '../wrap';
+
+export class ContainerWrap<T> {
+ protected readonly map: Map<string, IWrap<T>> = new Map();
+
+ constructor(public readonly ref: T) {
+ }
+
+ public get<R extends IWrap<T>>(key: string): R {
+ assert(this.map.has(key), `Unknown implementation key "${key}"`);
+ return this.map.get(key)! as R;
+ }
+}
diff --git a/llparse-frontend/src/enumerator.ts b/llparse-frontend/src/enumerator.ts
new file mode 100644
index 0000000..f2940a2
--- /dev/null
+++ b/llparse-frontend/src/enumerator.ts
@@ -0,0 +1,23 @@
+import { Node } from './node';
+import { IWrap } from './wrap';
+
+export class Enumerator {
+ public getAllNodes(root: IWrap<Node>): ReadonlyArray<IWrap<Node>> {
+ const nodes: Set<IWrap<Node>> = new Set();
+ const queue = [ root ];
+
+ while (queue.length !== 0) {
+ const node = queue.pop()!;
+ for (const slot of node.ref.getSlots()) {
+ if (nodes.has(slot.node)) {
+ continue;
+ }
+
+ nodes.add(slot.node);
+ queue.push(slot.node);
+ }
+ }
+
+ return Array.from(nodes);
+ }
+}
diff --git a/llparse-frontend/src/frontend.ts b/llparse-frontend/src/frontend.ts
new file mode 100644
index 0000000..91c5224
--- /dev/null
+++ b/llparse-frontend/src/frontend.ts
@@ -0,0 +1,513 @@
+import * as assert from 'assert';
+import * as debugAPI from 'debug';
+import * as source from 'llparse-builder';
+
+import * as frontend from './namespace/frontend';
+import { Container, ContainerWrap } from './container';
+import { IImplementation } from './implementation';
+import { SpanField } from './span-field';
+import { Trie, TrieEmpty, TrieNode, TrieSequence, TrieSingle } from './trie';
+import { Identifier, IUniqueName } from './utils';
+import { IWrap } from './wrap';
+import { Enumerator } from './enumerator';
+import { Peephole } from './peephole';
+
+const debug = debugAPI('llparse:translator');
+
+export { code, node, transform } from './namespace/frontend';
+
+export {
+ source,
+ Identifier,
+ IUniqueName,
+ IWrap,
+ SpanField,
+ Container,
+ ContainerWrap,
+};
+
+// Minimum number of cases of `single` node to make it eligable for
+// `TableLookup` optimization
+export const DEFAULT_MIN_TABLE_SIZE = 32;
+
+// Maximum width of entry in a table for a `TableLookup` optimization
+export const DEFAULT_MAX_TABLE_WIDTH = 4;
+
+type WrappedNode = IWrap<frontend.node.Node>;
+type WrappedCode = IWrap<frontend.code.Code>;
+
+export interface IFrontendLazyOptions {
+ readonly maxTableElemWidth?: number;
+ readonly minTableSize?: number;
+}
+
+export interface IFrontendResult {
+ readonly prefix: string;
+ readonly properties: ReadonlyArray<source.Property>;
+ readonly root: IWrap<frontend.node.Node>;
+ readonly spans: ReadonlyArray<SpanField>;
+ readonly resumptionTargets: ReadonlySet<WrappedNode>;
+}
+
+interface IFrontendOptions {
+ readonly maxTableElemWidth: number;
+ readonly minTableSize: number;
+}
+
+type MatchChildren = WrappedNode[];
+type MatchResult = WrappedNode | ReadonlyArray<WrappedNode>;
+
+interface ITableLookupTarget {
+ readonly keys: number[];
+ readonly noAdvance: boolean;
+ readonly trie: TrieEmpty;
+}
+
+export class Frontend {
+ private readonly options: IFrontendOptions;
+
+ private readonly id: Identifier = new Identifier(this.prefix + '__n_');
+ private readonly codeId: Identifier = new Identifier(this.prefix + '__c_');
+ private readonly map: Map<source.node.Node, WrappedNode> = new Map();
+ private readonly spanMap: Map<source.Span, SpanField> = new Map();
+ private readonly codeCache: Map<string, WrappedCode> = new Map();
+ private readonly resumptionTargets: Set<WrappedNode> = new Set();
+
+ constructor(private readonly prefix: string,
+ private readonly implementation: IImplementation,
+ options: IFrontendLazyOptions = {}) {
+ this.options = {
+ maxTableElemWidth: options.maxTableElemWidth === undefined ?
+ DEFAULT_MAX_TABLE_WIDTH : options.maxTableElemWidth,
+ minTableSize: options.minTableSize === undefined ?
+ DEFAULT_MIN_TABLE_SIZE : options.minTableSize,
+ };
+
+ assert(0 < this.options.maxTableElemWidth,
+ 'Invalid `options.maxTableElemWidth`, must be positive');
+ }
+
+ public compile(root: source.node.Node,
+ properties: ReadonlyArray<source.Property>): IFrontendResult {
+ debug('checking loops');
+ const lc = new source.LoopChecker();
+ lc.check(root);
+
+ debug('allocating spans');
+ const spanAllocator = new source.SpanAllocator();
+ const sourceSpans = spanAllocator.allocate(root);
+
+ const spans = sourceSpans.concurrency.map((concurrent, index) => {
+ const span = new SpanField(index, concurrent.map((sourceSpan) => {
+ return this.translateSpanCode(sourceSpan.callback);
+ }));
+
+ for (const sourceSpan of concurrent) {
+ this.spanMap.set(sourceSpan, span);
+ }
+
+ return span;
+ });
+
+ debug('translating');
+ let out = this.translate(root);
+
+ debug('enumerating');
+ const enumerator = new Enumerator();
+ let nodes = enumerator.getAllNodes(out);
+
+ debug('peephole optimization');
+ const peephole = new Peephole();
+ out = peephole.optimize(out, nodes);
+
+ debug('re-enumerating');
+ nodes = enumerator.getAllNodes(out);
+
+ debug('registering resumption targets');
+ this.resumptionTargets.add(out);
+ for (const node of nodes) {
+ this.registerNode(node);
+ }
+
+ return {
+ prefix: this.prefix,
+ properties,
+ resumptionTargets: this.resumptionTargets,
+ root: out,
+ spans,
+ };
+ }
+
+ // TODO(indutny): remove this in the next major release
+ public getResumptionTargets(): ReadonlySet<WrappedNode> {
+ return this.resumptionTargets;
+ }
+
+ private translate(node: source.node.Node): WrappedNode {
+ if (this.map.has(node)) {
+ return this.map.get(node)!;
+ }
+
+ const id = () => this.id.id(node.name);
+
+ const nodeImpl = this.implementation.node;
+
+ // Instantiate target class
+ let result: MatchResult;
+ if (node instanceof source.node.Error) {
+ result = new nodeImpl.Error(
+ new frontend.node.Error(id(), node.code, node.reason));
+ } else if (node instanceof source.node.Pause) {
+ result = new nodeImpl.Pause(
+ new frontend.node.Pause(id(), node.code, node.reason));
+ } else if (node instanceof source.node.Consume) {
+ result = new nodeImpl.Consume(
+ new frontend.node.Consume(id(), node.field));
+ } else if (node instanceof source.node.SpanStart) {
+ result = new nodeImpl.SpanStart(
+ new frontend.node.SpanStart(id(), this.spanMap.get(node.span)!,
+ this.translateSpanCode(node.span.callback)));
+ } else if (node instanceof source.node.SpanEnd) {
+ result = new nodeImpl.SpanEnd(
+ new frontend.node.SpanEnd(id(), this.spanMap.get(node.span)!,
+ this.translateSpanCode(node.span.callback)));
+ } else if (node instanceof source.node.Invoke) {
+ assert(node.code.signature === 'match' || node.code.signature === 'value',
+ 'Passing `span` callback to `invoke` is not allowed');
+ result = new nodeImpl.Invoke(
+ new frontend.node.Invoke(id(), this.translateCode(node.code)));
+ } else if (node instanceof source.node.Match) {
+ result = this.translateMatch(node);
+ } else {
+ throw new Error(`Unknown node type for "${node.name}" ${node.constructor.toString()}`);
+ }
+
+ // Initialize result
+ const otherwise = node.getOtherwiseEdge();
+
+ if (Array.isArray(result)) {
+ assert(node instanceof source.node.Match);
+ const match = node as source.node.Match;
+
+ // TODO(indutny): move this to llparse-builder?
+ assert.notStrictEqual(otherwise, undefined,
+ `Node "${node.name}" has no \`.otherwise()\``);
+
+ // Assign otherwise to every node of Trie
+ if (otherwise !== undefined) {
+ for (const child of result) {
+ if (!child.ref.otherwise) {
+ child.ref.setOtherwise(this.translate(otherwise.node),
+ otherwise.noAdvance);
+ }
+ }
+ }
+
+ // Assign transform to every node of Trie
+ const transform = this.translateTransform(match.getTransform());
+ for (const child of result) {
+ child.ref.setTransform(transform);
+ }
+
+ assert(result.length >= 1);
+ return result[0];
+ } else {
+ const single: WrappedNode = result as WrappedNode;
+ assert(single.ref instanceof frontend.node.Node);
+
+ // Break loops
+ this.map.set(node, single);
+
+ if (otherwise !== undefined) {
+ single.ref.setOtherwise(this.translate(otherwise.node),
+ otherwise.noAdvance);
+ } else {
+ // TODO(indutny): move this to llparse-builder?
+ assert(node instanceof source.node.Error,
+ `Node "${node.name}" has no \`.otherwise()\``);
+ }
+
+ if (single.ref instanceof frontend.node.Invoke) {
+ for (const edge of node) {
+ single.ref.addEdge(edge.key as number, this.translate(edge.node));
+ }
+ } else {
+ assert.strictEqual(Array.from(node).length, 0);
+ }
+
+ return single;
+ }
+ }
+
+ private registerNode(node: any): void {
+ const nodeImpl = this.implementation.node;
+
+ // Nodes with prologue check (start_pos != end_pos)
+ if (node instanceof nodeImpl.Consume ||
+ node instanceof nodeImpl.Empty ||
+ node instanceof nodeImpl.Sequence ||
+ node instanceof nodeImpl.Single ||
+ node instanceof nodeImpl.SpanStart ||
+ node instanceof nodeImpl.TableLookup) {
+ this.resumptionTargets.add(node);
+
+ // Nodes that can interrupt the execution to be resumed at different node
+ } else if (node instanceof nodeImpl.Pause ||
+ node instanceof nodeImpl.SpanEnd) {
+ this.resumptionTargets.add(node.ref.otherwise!.node);
+ }
+ }
+
+ private translateMatch(node: source.node.Match): MatchResult {
+ const trie = new Trie(node.name);
+
+ const otherwise = node.getOtherwiseEdge();
+ const trieNode = trie.build(Array.from(node));
+ if (trieNode === undefined) {
+ return new this.implementation.node.Empty(
+ new frontend.node.Empty(this.id.id(node.name)));
+ }
+
+ const children: MatchChildren = [];
+ this.translateTrie(node, trieNode, children);
+ assert(children.length >= 1);
+
+ return children;
+ }
+
+ private translateTrie(node: source.node.Match, trie: TrieNode,
+ children: MatchChildren): WrappedNode {
+ if (trie instanceof TrieEmpty) {
+ assert(this.map.has(node));
+ return this.translate(trie.node);
+ } else if (trie instanceof TrieSingle) {
+ return this.translateSingle(node, trie, children);
+ } else if (trie instanceof TrieSequence) {
+ return this.translateSequence(node, trie, children);
+ } else {
+ throw new Error('Unknown trie node');
+ }
+ }
+
+ private translateSingle(node: source.node.Match, trie: TrieSingle,
+ children: MatchChildren)
+ : IWrap<frontend.node.Match> {
+ // See if we can apply TableLookup optimization
+ const maybeTable = this.maybeTableLookup(node, trie, children);
+ if (maybeTable !== undefined) {
+ return maybeTable;
+ }
+
+ const single = new this.implementation.node.Single(
+ new frontend.node.Single(this.id.id(node.name)));
+ children.push(single);
+
+ // Break the loop
+ if (!this.map.has(node)) {
+ this.map.set(node, single);
+ }
+ for (const child of trie.children) {
+ const childNode = this.translateTrie(node, child.node, children);
+
+ single.ref.addEdge({
+ key: child.key,
+ noAdvance: child.noAdvance,
+ node: childNode,
+ value: child.node instanceof TrieEmpty ? child.node.value : undefined,
+ });
+ }
+
+ const otherwise = trie.otherwise;
+ if (otherwise) {
+ single.ref.setOtherwise(
+ this.translateTrie(node, otherwise, children),
+ true,
+ otherwise.value);
+ }
+
+ return single;
+ }
+
+ private maybeTableLookup(node: source.node.Match, trie: TrieSingle,
+ children: MatchChildren)
+ : IWrap<frontend.node.Match> | undefined {
+ if (trie.children.length < this.options.minTableSize) {
+ debug('not enough children of "%s" to allocate table, got %d need %d',
+ node.name, trie.children.length, this.options.minTableSize);
+ return undefined;
+ }
+
+ const targets: Map<source.node.Node, ITableLookupTarget> = new Map();
+
+ const bailout = !trie.children.every((child) => {
+ if (!(child.node instanceof TrieEmpty)) {
+ debug('non-leaf trie child of "%s" prevents table allocation',
+ node.name);
+ return false;
+ }
+
+ const empty: TrieEmpty = child.node;
+
+ // We can't pass values from the table yet
+ if (empty.value !== undefined) {
+ debug('value passing trie leaf of "%s" prevents table allocation',
+ node.name);
+ return false;
+ }
+
+ const target = empty.node;
+ if (!targets.has(target)) {
+ targets.set(target, {
+ keys: [ child.key ],
+ noAdvance: child.noAdvance,
+ trie: empty,
+ });
+ return true;
+ }
+
+ const existing = targets.get(target)!;
+
+ // TODO(indutny): just use it as a sub-key?
+ if (existing.noAdvance !== child.noAdvance) {
+ debug(
+ 'noAdvance mismatch in a trie leaf of "%s" prevents ' +
+ 'table allocation',
+ node.name);
+ return false;
+ }
+
+ existing.keys.push(child.key);
+ return true;
+ });
+
+ if (bailout) {
+ return undefined;
+ }
+
+ // We've width limit for this optimization
+ if (targets.size >= (1 << this.options.maxTableElemWidth)) {
+ debug('too many different trie targets of "%s" for a table allocation',
+ node.name);
+ return undefined;
+ }
+
+ const table = new this.implementation.node.TableLookup(
+ new frontend.node.TableLookup(this.id.id(node.name)));
+ children.push(table);
+
+ // Break the loop
+ if (!this.map.has(node)) {
+ this.map.set(node, table);
+ }
+
+ targets.forEach((target) => {
+ const next = this.translateTrie(node, target.trie, children);
+
+ table.ref.addEdge({
+ keys: target.keys,
+ noAdvance: target.noAdvance,
+ node: next,
+ });
+ });
+
+ debug('optimized "%s" to a table lookup node', node.name);
+ return table;
+ }
+
+ private translateSequence(node: source.node.Match, trie: TrieSequence,
+ children: MatchChildren)
+ : IWrap<frontend.node.Match> {
+ const sequence = new this.implementation.node.Sequence(
+ new frontend.node.Sequence(this.id.id(node.name), trie.select));
+ children.push(sequence);
+
+ // Break the loop
+ if (!this.map.has(node)) {
+ this.map.set(node, sequence);
+ }
+
+ const childNode = this.translateTrie(node, trie.child, children);
+
+ const value = trie.child instanceof TrieEmpty ?
+ trie.child.value : undefined;
+
+ sequence.ref.setEdge(childNode, value);
+
+ return sequence;
+ }
+
+ private translateCode(code: source.code.Code): WrappedCode {
+ const prefixed = this.codeId.id(code.name).name;
+ const codeImpl = this.implementation.code;
+
+ let res: WrappedCode;
+ if (code instanceof source.code.IsEqual) {
+ res = new codeImpl.IsEqual(
+ new frontend.code.IsEqual(prefixed, code.field, code.value));
+ } else if (code instanceof source.code.Load) {
+ res = new codeImpl.Load(
+ new frontend.code.Load(prefixed, code.field));
+ } else if (code instanceof source.code.MulAdd) {
+ // TODO(indutny): verify property type
+ const m = new frontend.code.MulAdd(prefixed, code.field, {
+ base: code.options.base,
+ max: code.options.max,
+ signed: code.options.signed === undefined ? true : code.options.signed,
+ });
+ res = new codeImpl.MulAdd(m);
+ } else if (code instanceof source.code.And) {
+ res = new codeImpl.And(
+ new frontend.code.Or(prefixed, code.field, code.value));
+ } else if (code instanceof source.code.Or) {
+ res = new codeImpl.Or(
+ new frontend.code.Or(prefixed, code.field, code.value));
+ } else if (code instanceof source.code.Store) {
+ res = new codeImpl.Store(
+ new frontend.code.Store(prefixed, code.field));
+ } else if (code instanceof source.code.Test) {
+ res = new codeImpl.Test(
+ new frontend.code.Test(prefixed, code.field, code.value));
+ } else if (code instanceof source.code.Update) {
+ res = new codeImpl.Update(
+ new frontend.code.Update(prefixed, code.field, code.value));
+
+ // External callbacks
+ } else if (code instanceof source.code.Span) {
+ res = new codeImpl.Span(new frontend.code.Span(code.name));
+ } else if (code instanceof source.code.Match) {
+ res = new codeImpl.Match(new frontend.code.Match(code.name));
+ } else if (code instanceof source.code.Value) {
+ res = new codeImpl.Value(new frontend.code.Value(code.name));
+ } else {
+ throw new Error(`Unsupported code: "${code.name}"`);
+ }
+
+ // Re-use instances to build them just once
+ if (this.codeCache.has(res.ref.cacheKey)) {
+ return this.codeCache.get(res.ref.cacheKey)!;
+ }
+
+ this.codeCache.set(res.ref.cacheKey, res);
+ return res;
+ }
+
+ private translateSpanCode(code: source.code.Span): IWrap<frontend.code.Span> {
+ return this.translateCode(code) as IWrap<frontend.code.Span>;
+ }
+
+ private translateTransform(transform?: source.transform.Transform)
+ : IWrap<frontend.transform.Transform> {
+ const transformImpl = this.implementation.transform;
+ if (transform === undefined) {
+ return new transformImpl.ID(new frontend.transform.ID());
+ } else if (transform.name === 'to_lower') {
+ return new transformImpl.ToLower(
+ new frontend.transform.ToLower());
+ } else if (transform.name === 'to_lower_unsafe') {
+ return new transformImpl.ToLowerUnsafe(
+ new frontend.transform.ToLowerUnsafe());
+ } else {
+ throw new Error(`Unsupported transform: "${transform.name}"`);
+ }
+ }
+}
diff --git a/llparse-frontend/src/implementation/code.ts b/llparse-frontend/src/implementation/code.ts
new file mode 100644
index 0000000..c467ced
--- /dev/null
+++ b/llparse-frontend/src/implementation/code.ts
@@ -0,0 +1,16 @@
+import * as code from '../code';
+import { IWrap } from '../wrap';
+
+export interface ICodeImplementation {
+ readonly And: new(c: code.And) => IWrap<code.And>;
+ readonly IsEqual: new(c: code.IsEqual) => IWrap<code.IsEqual>;
+ readonly Load: new(c: code.Load) => IWrap<code.Load>;
+ readonly Match: new(c: code.Match) => IWrap<code.Match>;
+ readonly MulAdd: new(c: code.MulAdd) => IWrap<code.MulAdd>;
+ readonly Or: new(c: code.Or) => IWrap<code.Or>;
+ readonly Span: new(c: code.Span) => IWrap<code.Span>;
+ readonly Store: new(c: code.Store) => IWrap<code.Store>;
+ readonly Test: new(c: code.Test) => IWrap<code.Test>;
+ readonly Update: new(c: code.Update) => IWrap<code.Update>;
+ readonly Value: new(c: code.Value) => IWrap<code.Value>;
+}
diff --git a/llparse-frontend/src/implementation/full.ts b/llparse-frontend/src/implementation/full.ts
new file mode 100644
index 0000000..08c4c03
--- /dev/null
+++ b/llparse-frontend/src/implementation/full.ts
@@ -0,0 +1,9 @@
+import { ICodeImplementation } from './code';
+import { INodeImplementation } from './node';
+import { ITransformImplementation } from './transform';
+
+export interface IImplementation {
+ readonly code: ICodeImplementation;
+ readonly node: INodeImplementation;
+ readonly transform: ITransformImplementation;
+}
diff --git a/llparse-frontend/src/implementation/index.ts b/llparse-frontend/src/implementation/index.ts
new file mode 100644
index 0000000..2b5411b
--- /dev/null
+++ b/llparse-frontend/src/implementation/index.ts
@@ -0,0 +1,4 @@
+export * from './code';
+export * from './full';
+export * from './node';
+export * from './transform';
diff --git a/llparse-frontend/src/implementation/node.ts b/llparse-frontend/src/implementation/node.ts
new file mode 100644
index 0000000..af0b3df
--- /dev/null
+++ b/llparse-frontend/src/implementation/node.ts
@@ -0,0 +1,15 @@
+import * as node from '../node';
+import { IWrap } from '../wrap';
+
+export interface INodeImplementation {
+ readonly Consume: new(n: node.Consume) => IWrap<node.Consume>;
+ readonly Empty: new(n: node.Empty) => IWrap<node.Empty>;
+ readonly Error: new(n: node.Error) => IWrap<node.Error>;
+ readonly Invoke: new(n: node.Invoke) => IWrap<node.Invoke>;
+ readonly Pause: new(n: node.Pause) => IWrap<node.Pause>;
+ readonly Sequence: new(n: node.Sequence) => IWrap<node.Sequence>;
+ readonly Single: new(n: node.Single) => IWrap<node.Single>;
+ readonly SpanEnd: new(n: node.SpanEnd) => IWrap<node.SpanEnd>;
+ readonly SpanStart: new(n: node.SpanStart) => IWrap<node.SpanStart>;
+ readonly TableLookup: new(n: node.TableLookup) => IWrap<node.TableLookup>;
+}
diff --git a/llparse-frontend/src/implementation/transform.ts b/llparse-frontend/src/implementation/transform.ts
new file mode 100644
index 0000000..4382284
--- /dev/null
+++ b/llparse-frontend/src/implementation/transform.ts
@@ -0,0 +1,9 @@
+import * as transform from '../transform';
+import { IWrap } from '../wrap';
+
+export interface ITransformImplementation {
+ readonly ID: new(t: transform.ID) => IWrap<transform.ID>;
+ readonly ToLower: new(t: transform.ToLower) => IWrap<transform.ToLower>;
+ readonly ToLowerUnsafe: new(t: transform.ToLowerUnsafe)
+ => IWrap<transform.ToLowerUnsafe>;
+}
diff --git a/llparse-frontend/src/namespace/frontend.ts b/llparse-frontend/src/namespace/frontend.ts
new file mode 100644
index 0000000..2f89093
--- /dev/null
+++ b/llparse-frontend/src/namespace/frontend.ts
@@ -0,0 +1,5 @@
+import * as code from '../code';
+import * as node from '../node';
+import * as transform from '../transform';
+
+export { code, node, transform };
diff --git a/llparse-frontend/src/node/base.ts b/llparse-frontend/src/node/base.ts
new file mode 100644
index 0000000..1e93c49
--- /dev/null
+++ b/llparse-frontend/src/node/base.ts
@@ -0,0 +1,46 @@
+import { IUniqueName } from '../utils';
+import { IWrap } from '../wrap';
+import { Slot } from './slot';
+
+export interface IReadonlyOtherwiseEdge {
+ readonly node: IWrap<Node>;
+ readonly noAdvance: boolean;
+ readonly value: number | undefined;
+}
+
+interface IOtherwiseEdge {
+ node: IWrap<Node>;
+ readonly noAdvance: boolean;
+ readonly value: number | undefined;
+}
+
+export abstract class Node {
+ private privOtherwise: IOtherwiseEdge | undefined;
+ private privSlots: ReadonlyArray<Slot> | undefined;
+
+ constructor(public readonly id: IUniqueName) {
+ }
+
+ public setOtherwise(node: IWrap<Node>, noAdvance: boolean, value?: number) {
+ this.privOtherwise = { node, noAdvance, value };
+ }
+
+ public get otherwise(): IReadonlyOtherwiseEdge | undefined {
+ return this.privOtherwise;
+ }
+
+ public *getSlots() {
+ if (this.privSlots === undefined) {
+ this.privSlots = Array.from(this.buildSlots());
+ }
+
+ yield* this.privSlots;
+ }
+
+ protected *buildSlots() {
+ const otherwise = this.privOtherwise;
+ if (otherwise !== undefined) {
+ yield new Slot(otherwise.node, (value) => otherwise.node = value);
+ }
+ }
+}
diff --git a/llparse-frontend/src/node/consume.ts b/llparse-frontend/src/node/consume.ts
new file mode 100644
index 0000000..6ab49ac
--- /dev/null
+++ b/llparse-frontend/src/node/consume.ts
@@ -0,0 +1,8 @@
+import { IUniqueName } from '../utils';
+import { Node } from './base';
+
+export class Consume extends Node {
+ constructor(id: IUniqueName, readonly field: string) {
+ super(id);
+ }
+}
diff --git a/llparse-frontend/src/node/empty.ts b/llparse-frontend/src/node/empty.ts
new file mode 100644
index 0000000..45c552c
--- /dev/null
+++ b/llparse-frontend/src/node/empty.ts
@@ -0,0 +1,4 @@
+import { Node } from './base';
+
+export class Empty extends Node {
+}
diff --git a/llparse-frontend/src/node/error.ts b/llparse-frontend/src/node/error.ts
new file mode 100644
index 0000000..c4e6faf
--- /dev/null
+++ b/llparse-frontend/src/node/error.ts
@@ -0,0 +1,9 @@
+import { IUniqueName } from '../utils';
+import { Node } from './base';
+
+export class Error extends Node {
+ constructor(id: IUniqueName, public readonly code: number,
+ public readonly reason: string) {
+ super(id);
+ }
+}
diff --git a/llparse-frontend/src/node/index.ts b/llparse-frontend/src/node/index.ts
new file mode 100644
index 0000000..bd11015
--- /dev/null
+++ b/llparse-frontend/src/node/index.ts
@@ -0,0 +1,13 @@
+export * from './base';
+export * from './consume';
+export * from './empty';
+export * from './error';
+export * from './invoke';
+export * from './match';
+export * from './pause';
+export * from './sequence';
+export * from './single';
+export * from './slot';
+export * from './span-end';
+export * from './span-start';
+export * from './table-lookup';
diff --git a/llparse-frontend/src/node/invoke.ts b/llparse-frontend/src/node/invoke.ts
new file mode 100644
index 0000000..ba6ef53
--- /dev/null
+++ b/llparse-frontend/src/node/invoke.ts
@@ -0,0 +1,39 @@
+import { Code } from '../code';
+import { IUniqueName } from '../utils';
+import { IWrap } from '../wrap';
+import { Node } from './base';
+import { Slot } from './slot';
+
+interface IInvokeEdge {
+ readonly code: number;
+ node: IWrap<Node>;
+}
+
+export interface IReadonlyInvokeEdge {
+ readonly code: number;
+ readonly node: IWrap<Node>;
+}
+
+export class Invoke extends Node {
+ private readonly privEdges: IInvokeEdge[] = [];
+
+ constructor(id: IUniqueName, public readonly code: IWrap<Code>) {
+ super(id);
+ }
+
+ public addEdge(code: number, node: IWrap<Node>): void {
+ this.privEdges.push({ code, node });
+ }
+
+ public get edges(): ReadonlyArray<IReadonlyInvokeEdge> {
+ return this.privEdges;
+ }
+
+ protected *buildSlots() {
+ for (const edge of this.privEdges) {
+ yield new Slot(edge.node, (value) => edge.node = value);
+ }
+
+ yield* super.buildSlots();
+ }
+}
diff --git a/llparse-frontend/src/node/match.ts b/llparse-frontend/src/node/match.ts
new file mode 100644
index 0000000..8a499d3
--- /dev/null
+++ b/llparse-frontend/src/node/match.ts
@@ -0,0 +1,11 @@
+import { Transform } from '../transform';
+import { IWrap } from '../wrap';
+import { Node } from './base';
+
+export class Match extends Node {
+ public transform?: IWrap<Transform>;
+
+ public setTransform(transform: IWrap<Transform>): void {
+ this.transform = transform;
+ }
+}
diff --git a/llparse-frontend/src/node/pause.ts b/llparse-frontend/src/node/pause.ts
new file mode 100644
index 0000000..b9923d7
--- /dev/null
+++ b/llparse-frontend/src/node/pause.ts
@@ -0,0 +1,4 @@
+import { Error as ErrorNode } from './error';
+
+export class Pause extends ErrorNode {
+}
diff --git a/llparse-frontend/src/node/sequence.ts b/llparse-frontend/src/node/sequence.ts
new file mode 100644
index 0000000..c9105b3
--- /dev/null
+++ b/llparse-frontend/src/node/sequence.ts
@@ -0,0 +1,44 @@
+import * as assert from 'assert';
+import { Buffer } from 'buffer';
+
+import { IUniqueName } from '../utils';
+import { IWrap } from '../wrap';
+import { Node } from './base';
+import { Match } from './match';
+import { Slot } from './slot';
+
+interface ISequenceEdge {
+ node: IWrap<Node>;
+ readonly value: number | undefined;
+}
+
+export interface IReadonlySequenceEdge {
+ readonly node: IWrap<Node>;
+ readonly value: number | undefined;
+}
+
+export class Sequence extends Match {
+ private privEdge?: ISequenceEdge;
+
+ constructor(id: IUniqueName, public readonly select: Buffer) {
+ super(id);
+ }
+
+ public setEdge(node: IWrap<Node>, value?: number | undefined) {
+ assert.strictEqual(this.privEdge, undefined);
+ this.privEdge = { node, value };
+ }
+
+ public get edge(): IReadonlySequenceEdge | undefined {
+ return this.privEdge;
+ }
+
+ protected *buildSlots() {
+ const edge = this.privEdge;
+ if (edge !== undefined) {
+ yield new Slot(edge.node, (value) => edge.node = value);
+ }
+
+ yield* super.buildSlots();
+ }
+}
diff --git a/llparse-frontend/src/node/single.ts b/llparse-frontend/src/node/single.ts
new file mode 100644
index 0000000..0acf715
--- /dev/null
+++ b/llparse-frontend/src/node/single.ts
@@ -0,0 +1,46 @@
+import * as assert from 'assert';
+
+import { IUniqueName } from '../utils';
+import { IWrap } from '../wrap';
+import { Node } from './base';
+import { Match } from './match';
+import { Slot } from './slot';
+
+interface ISingleEdge {
+ readonly key: number;
+ node: IWrap<Node>;
+ readonly noAdvance: boolean;
+ readonly value: number | undefined;
+}
+
+export interface IReadonlySingleEdge {
+ readonly key: number;
+ node: IWrap<Node>;
+ readonly noAdvance: boolean;
+ readonly value: number | undefined;
+}
+
+export class Single extends Match {
+ private readonly privEdges: ISingleEdge[] = [];
+
+ public addEdge(edge: IReadonlySingleEdge): void {
+ this.privEdges.push({
+ key: edge.key,
+ noAdvance: edge.noAdvance,
+ node: edge.node,
+ value: edge.value,
+ });
+ }
+
+ public get edges(): ReadonlyArray<IReadonlySingleEdge> {
+ return this.privEdges;
+ }
+
+ protected *buildSlots() {
+ for (const edge of this.privEdges) {
+ yield new Slot(edge.node, (value) => edge.node = value);
+ }
+
+ yield* super.buildSlots();
+ }
+}
diff --git a/llparse-frontend/src/node/slot.ts b/llparse-frontend/src/node/slot.ts
new file mode 100644
index 0000000..923da86
--- /dev/null
+++ b/llparse-frontend/src/node/slot.ts
@@ -0,0 +1,20 @@
+import { IWrap } from '../wrap';
+import { Node } from './base';
+
+export class Slot {
+ private privNode: IWrap<Node>;
+
+ constructor(node: IWrap<Node>,
+ private readonly privUpdate: (value: IWrap<Node>) => void) {
+ this.privNode = node;
+ }
+
+ public get node(): IWrap<Node> {
+ return this.privNode;
+ }
+
+ public set node(value: IWrap<Node>) {
+ this.privNode = value;
+ this.privUpdate(value);
+ }
+}
diff --git a/llparse-frontend/src/node/span-end.ts b/llparse-frontend/src/node/span-end.ts
new file mode 100644
index 0000000..bf8d5cc
--- /dev/null
+++ b/llparse-frontend/src/node/span-end.ts
@@ -0,0 +1,12 @@
+import { Span } from '../code';
+import { SpanField } from '../span-field';
+import { IUniqueName } from '../utils';
+import { IWrap } from '../wrap';
+import { Node } from './base';
+
+export class SpanEnd extends Node {
+ constructor(id: IUniqueName, public readonly field: SpanField,
+ public readonly callback: IWrap<Span>) {
+ super(id);
+ }
+}
diff --git a/llparse-frontend/src/node/span-start.ts b/llparse-frontend/src/node/span-start.ts
new file mode 100644
index 0000000..89690f1
--- /dev/null
+++ b/llparse-frontend/src/node/span-start.ts
@@ -0,0 +1,12 @@
+import { Span } from '../code';
+import { SpanField } from '../span-field';
+import { IUniqueName } from '../utils';
+import { IWrap } from '../wrap';
+import { Node } from './base';
+
+export class SpanStart extends Node {
+ constructor(id: IUniqueName, public readonly field: SpanField,
+ public readonly callback: IWrap<Span>) {
+ super(id);
+ }
+}
diff --git a/llparse-frontend/src/node/table-lookup.ts b/llparse-frontend/src/node/table-lookup.ts
new file mode 100644
index 0000000..9880fc7
--- /dev/null
+++ b/llparse-frontend/src/node/table-lookup.ts
@@ -0,0 +1,43 @@
+import * as assert from 'assert';
+
+import { IUniqueName } from '../utils';
+import { IWrap } from '../wrap';
+import { Node } from './base';
+import { Match } from './match';
+import { Slot } from './slot';
+
+interface ITableEdge {
+ readonly keys: ReadonlyArray<number>;
+ node: IWrap<Node>;
+ readonly noAdvance: boolean;
+}
+
+export interface IReadonlyTableEdge {
+ readonly keys: ReadonlyArray<number>;
+ readonly node: IWrap<Node>;
+ readonly noAdvance: boolean;
+}
+
+export class TableLookup extends Match {
+ private readonly privEdges: ITableEdge[] = [];
+
+ public addEdge(edge: IReadonlyTableEdge): void {
+ this.privEdges.push({
+ keys: edge.keys,
+ noAdvance: edge.noAdvance,
+ node: edge.node,
+ });
+ }
+
+ public get edges(): ReadonlyArray<IReadonlyTableEdge> {
+ return this.privEdges;
+ }
+
+ protected *buildSlots() {
+ for (const edge of this.privEdges) {
+ yield new Slot(edge.node, (value) => edge.node = value);
+ }
+
+ yield* super.buildSlots();
+ }
+}
diff --git a/llparse-frontend/src/peephole.ts b/llparse-frontend/src/peephole.ts
new file mode 100644
index 0000000..19ac13f
--- /dev/null
+++ b/llparse-frontend/src/peephole.ts
@@ -0,0 +1,52 @@
+import { Node, Empty } from './node';
+import { IWrap } from './wrap';
+
+type WrapNode = IWrap<Node>;
+type WrapList = ReadonlyArray<WrapNode>;
+
+export class Peephole {
+ public optimize(root: WrapNode, nodes: WrapList): WrapNode {
+ let changed = new Set(nodes);
+
+ while (changed.size !== 0) {
+ const previous = changed;
+ changed = new Set();
+
+ for (const node of previous) {
+ if (this.optimizeNode(node)) {
+ changed.add(node);
+ }
+ }
+ }
+
+ while (root.ref instanceof Empty) {
+ if (!root.ref.otherwise!.noAdvance) {
+ break;
+ }
+
+ root = root.ref.otherwise!.node;
+ }
+
+ return root;
+ }
+
+ public optimizeNode(node: WrapNode): boolean {
+ let changed = false;
+ for (const slot of node.ref.getSlots()) {
+ if (!(slot.node.ref instanceof Empty)) {
+ continue;
+ }
+
+ const otherwise = slot.node.ref.otherwise!;
+
+ // Node actively skips, can't optimize!
+ if (!otherwise.noAdvance) {
+ continue;
+ }
+
+ slot.node = otherwise.node;
+ changed = true;
+ }
+ return changed;
+ }
+}
diff --git a/llparse-frontend/src/span-field.ts b/llparse-frontend/src/span-field.ts
new file mode 100644
index 0000000..0652f77
--- /dev/null
+++ b/llparse-frontend/src/span-field.ts
@@ -0,0 +1,8 @@
+import { Span } from './code';
+import { IWrap } from './wrap';
+
+export class SpanField {
+ constructor(public readonly index: number,
+ public readonly callbacks: ReadonlyArray<IWrap<Span>>) {
+ }
+}
diff --git a/llparse-frontend/src/transform/base.ts b/llparse-frontend/src/transform/base.ts
new file mode 100644
index 0000000..5397326
--- /dev/null
+++ b/llparse-frontend/src/transform/base.ts
@@ -0,0 +1,4 @@
+export abstract class Transform {
+ constructor(public readonly name: string) {
+ }
+}
diff --git a/llparse-frontend/src/transform/id.ts b/llparse-frontend/src/transform/id.ts
new file mode 100644
index 0000000..d86e3c1
--- /dev/null
+++ b/llparse-frontend/src/transform/id.ts
@@ -0,0 +1,7 @@
+import { Transform } from './base';
+
+export class ID extends Transform {
+ constructor() {
+ super('id');
+ }
+}
diff --git a/llparse-frontend/src/transform/index.ts b/llparse-frontend/src/transform/index.ts
new file mode 100644
index 0000000..f103b3b
--- /dev/null
+++ b/llparse-frontend/src/transform/index.ts
@@ -0,0 +1,4 @@
+export * from './base';
+export * from './id';
+export * from './to-lower';
+export * from './to-lower-unsafe';
diff --git a/llparse-frontend/src/transform/to-lower-unsafe.ts b/llparse-frontend/src/transform/to-lower-unsafe.ts
new file mode 100644
index 0000000..99d9618
--- /dev/null
+++ b/llparse-frontend/src/transform/to-lower-unsafe.ts
@@ -0,0 +1,7 @@
+import { Transform } from './base';
+
+export class ToLowerUnsafe extends Transform {
+ constructor() {
+ super('to_lower_unsafe');
+ }
+}
diff --git a/llparse-frontend/src/transform/to-lower.ts b/llparse-frontend/src/transform/to-lower.ts
new file mode 100644
index 0000000..b333fce
--- /dev/null
+++ b/llparse-frontend/src/transform/to-lower.ts
@@ -0,0 +1,7 @@
+import { Transform } from './base';
+
+export class ToLower extends Transform {
+ constructor() {
+ super('to_lower');
+ }
+}
diff --git a/llparse-frontend/src/trie/empty.ts b/llparse-frontend/src/trie/empty.ts
new file mode 100644
index 0000000..aba52ea
--- /dev/null
+++ b/llparse-frontend/src/trie/empty.ts
@@ -0,0 +1,9 @@
+import { node as api } from 'llparse-builder';
+import { TrieNode } from './node';
+
+export class TrieEmpty extends TrieNode {
+ constructor(public readonly node: api.Node,
+ public readonly value: number | undefined) {
+ super();
+ }
+}
diff --git a/llparse-frontend/src/trie/index.ts b/llparse-frontend/src/trie/index.ts
new file mode 100644
index 0000000..391c6a3
--- /dev/null
+++ b/llparse-frontend/src/trie/index.ts
@@ -0,0 +1,136 @@
+import * as assert from 'assert';
+import { Buffer } from 'buffer';
+import { Edge, node as api } from 'llparse-builder';
+
+import { TrieEmpty } from './empty';
+import { TrieNode } from './node';
+import { TrieSequence } from './sequence';
+import { ITrieSingleChild, TrieSingle } from './single';
+
+export { TrieEmpty, TrieNode, TrieSequence, TrieSingle };
+
+interface IEdge {
+ readonly key: Buffer;
+ readonly node: api.Node;
+ readonly noAdvance: boolean;
+ readonly value: number | undefined;
+}
+
+type Path = ReadonlyArray<Buffer>;
+type EdgeArray = ReadonlyArray<IEdge>;
+
+export class Trie {
+ constructor(private readonly name: string) {
+ }
+
+ public build(edges: ReadonlyArray<Edge>): undefined | TrieNode {
+ if (edges.length === 0) {
+ return undefined;
+ }
+
+ const internalEdges: IEdge[] = [];
+ for (const edge of edges) {
+ internalEdges.push({
+ key: edge.key as Buffer,
+ noAdvance: edge.noAdvance,
+ node: edge.node,
+ value: edge.value,
+ });
+ }
+
+ return this.level(internalEdges, []);
+ }
+
+ private level(edges: EdgeArray, path: Path): TrieNode {
+ const first = edges[0].key;
+ const last = edges[edges.length - 1].key;
+
+ // Leaf
+ if (edges.length === 1 && edges[0].key.length === 0) {
+ return new TrieEmpty(edges[0].node, edges[0].value);
+ }
+
+ // Find the longest common sub-string
+ let common = 0;
+ for (; common < first.length; common++) {
+ if (first[common] !== last[common]) {
+ break;
+ }
+ }
+
+ // Sequence
+ if (common > 1) {
+ return this.sequence(edges, first.slice(0, common), path);
+ }
+
+ // Single
+ return this.single(edges, path);
+ }
+
+ private slice(edges: EdgeArray, off: number): EdgeArray {
+ return edges.map((edge) => {
+ return {
+ key: edge.key.slice(off),
+ noAdvance: edge.noAdvance,
+ node: edge.node,
+ value: edge.value,
+ };
+ }).sort((a, b) => {
+ return a.key.compare(b.key);
+ });
+ }
+
+ private sequence(edges: EdgeArray, prefix: Buffer, path: Path): TrieNode {
+ const sliced = this.slice(edges, prefix.length);
+ const noAdvance = sliced.some((edge) => edge.noAdvance);
+ assert(!noAdvance);
+ const child = this.level(sliced, path.concat(prefix));
+
+ return new TrieSequence(prefix, child);
+ }
+
+ private single(edges: EdgeArray, path: Path): TrieNode {
+ // Check for duplicates
+ if (edges[0].key.length === 0) {
+ assert(path.length !== 0, `Empty root entry at "${this.name}"`);
+ assert(edges.length === 1 || edges[1].key.length !== 0,
+ `Duplicate entries in "${this.name}" at [ ${path.join(', ')} ]`);
+ }
+
+ let otherwise: TrieEmpty | undefined;
+ const keys: Map<number, IEdge[]> = new Map();
+ for (const edge of edges) {
+ if (edge.key.length === 0) {
+ otherwise = new TrieEmpty(edge.node, edge.value);
+ continue;
+ }
+ const key = edge.key[0];
+
+ if (keys.has(key)) {
+ keys.get(key)!.push(edge);
+ } else {
+ keys.set(key, [ edge ]);
+ }
+ }
+
+ const children: ITrieSingleChild[] = [];
+ keys.forEach((subEdges, key) => {
+ const sliced = this.slice(subEdges, 1);
+ const subpath = path.concat(Buffer.from([ key ]));
+
+ const noAdvance = subEdges.some((edge) => edge.noAdvance);
+ const allSame = subEdges.every((edge) => edge.noAdvance === noAdvance);
+
+ assert(allSame || subEdges.length === 0,
+ 'Conflicting `.peek()` and `.match()` entries in ' +
+ `"${this.name}" at [ ${subpath.join(', ')} ]`);
+
+ children.push({
+ key,
+ noAdvance,
+ node: this.level(sliced, subpath),
+ });
+ });
+ return new TrieSingle(children, otherwise);
+ }
+}
diff --git a/llparse-frontend/src/trie/node.ts b/llparse-frontend/src/trie/node.ts
new file mode 100644
index 0000000..31f327c
--- /dev/null
+++ b/llparse-frontend/src/trie/node.ts
@@ -0,0 +1,2 @@
+export abstract class TrieNode {
+}
diff --git a/llparse-frontend/src/trie/sequence.ts b/llparse-frontend/src/trie/sequence.ts
new file mode 100644
index 0000000..6b17e02
--- /dev/null
+++ b/llparse-frontend/src/trie/sequence.ts
@@ -0,0 +1,9 @@
+import { node as api } from 'llparse-builder';
+import { TrieNode } from './node';
+
+export class TrieSequence extends TrieNode {
+ constructor(public readonly select: Buffer,
+ public readonly child: TrieNode) {
+ super();
+ }
+}
diff --git a/llparse-frontend/src/trie/single.ts b/llparse-frontend/src/trie/single.ts
new file mode 100644
index 0000000..c984af0
--- /dev/null
+++ b/llparse-frontend/src/trie/single.ts
@@ -0,0 +1,16 @@
+import { node as api } from 'llparse-builder';
+import { TrieEmpty } from './empty';
+import { TrieNode } from './node';
+
+export interface ITrieSingleChild {
+ readonly key: number;
+ readonly noAdvance: boolean;
+ readonly node: TrieNode;
+}
+
+export class TrieSingle extends TrieNode {
+ constructor(public readonly children: ReadonlyArray<ITrieSingleChild>,
+ public readonly otherwise: TrieEmpty | undefined) {
+ super();
+ }
+}
diff --git a/llparse-frontend/src/utils/identifier.ts b/llparse-frontend/src/utils/identifier.ts
new file mode 100644
index 0000000..c9ba6ad
--- /dev/null
+++ b/llparse-frontend/src/utils/identifier.ts
@@ -0,0 +1,32 @@
+export interface IUniqueName {
+ readonly name: string;
+ readonly originalName: string;
+}
+
+export class Identifier {
+ private readonly ns: Set<string> = new Set();
+
+ constructor(private readonly prefix: string = '',
+ private readonly postfix: string = '') {
+ }
+
+ public id(name: string): IUniqueName {
+ let target = this.prefix + name + this.postfix;
+ if (this.ns.has(target)) {
+ let i = 1;
+ for (; i < this.ns.size; i++) {
+ if (!this.ns.has(target + '_' + i)) {
+ break;
+ }
+ }
+
+ target += '_' + i;
+ }
+
+ this.ns.add(target);
+ return {
+ name: target,
+ originalName: name,
+ };
+ }
+}
diff --git a/llparse-frontend/src/utils/index.ts b/llparse-frontend/src/utils/index.ts
new file mode 100644
index 0000000..06e86f1
--- /dev/null
+++ b/llparse-frontend/src/utils/index.ts
@@ -0,0 +1,19 @@
+export { Identifier, IUniqueName } from './identifier';
+
+export function toCacheKey(value: number | boolean): string {
+ if (typeof value === 'number') {
+ if (value < 0) {
+ return 'm' + (-value);
+ } else {
+ return value.toString();
+ }
+ } else if (typeof value === 'boolean') {
+ if (value === true) {
+ return 'true';
+ } else {
+ return 'false';
+ }
+ } else {
+ throw new Error(`Unsupported value: "${value}"`);
+ }
+}
diff --git a/llparse-frontend/src/wrap.ts b/llparse-frontend/src/wrap.ts
new file mode 100644
index 0000000..013adb3
--- /dev/null
+++ b/llparse-frontend/src/wrap.ts
@@ -0,0 +1,3 @@
+export interface IWrap<T> {
+ readonly ref: T;
+}
diff --git a/llparse-frontend/test/container-test.ts b/llparse-frontend/test/container-test.ts
new file mode 100644
index 0000000..28b7f1b
--- /dev/null
+++ b/llparse-frontend/test/container-test.ts
@@ -0,0 +1,46 @@
+import * as assert from 'assert';
+
+import { Builder } from 'llparse-builder';
+
+import { Container, ContainerWrap, Frontend, node } from '../src/frontend';
+import implementation from './fixtures/a-implementation';
+import { Node } from './fixtures/implementation/node/base';
+
+describe('llparse-frontend/Container', () => {
+ let b: Builder;
+ beforeEach(() => {
+ b = new Builder();
+ });
+
+ it('should translate nodes to implementation', () => {
+ const comb = new Container();
+ comb.add('a', implementation);
+ comb.add('b', implementation);
+
+ const f = new Frontend('llparse', comb.build());
+
+ const root = b.node('root');
+
+ root.match('ab', root);
+ root.match('acd', root);
+ root.match('efg', root);
+ root.otherwise(b.error(123, 'hello'));
+
+ const fRoot = f.compile(root, []).root as ContainerWrap<node.Node>;
+
+ const out: string[] = [];
+ (fRoot.get('a') as Node<node.Node>).build(out);
+
+ assert.deepStrictEqual(out, [
+ '<Single name=llparse__n_root k97=llparse__n_root_1 ' +
+ 'k101=llparse__n_root_3 otherwise-no_adv=llparse__n_error/>',
+ '<Single name=llparse__n_root_1 k98=llparse__n_root ' +
+ 'k99=llparse__n_root_2 otherwise-no_adv=llparse__n_error/>',
+ '<Single name=llparse__n_root_2 k100=llparse__n_root ' +
+ 'otherwise-no_adv=llparse__n_error/>',
+ '<ErrorNode name=llparse__n_error code=123 reason="hello"/>',
+ '<Sequence name=llparse__n_root_3 select="6667" ' +
+ 'otherwise-no_adv=llparse__n_error/>',
+ ]);
+ });
+});
diff --git a/llparse-frontend/test/fixtures/a-implementation/code/and.ts b/llparse-frontend/test/fixtures/a-implementation/code/and.ts
new file mode 100644
index 0000000..c1df821
--- /dev/null
+++ b/llparse-frontend/test/fixtures/a-implementation/code/and.ts
@@ -0,0 +1,8 @@
+import { code } from '../../../../src/frontend';
+import { Code } from './base';
+
+export class And extends Code<code.And> {
+ public build(): string {
+ return '';
+ }
+}
diff --git a/llparse-frontend/test/fixtures/a-implementation/code/base.ts b/llparse-frontend/test/fixtures/a-implementation/code/base.ts
new file mode 100644
index 0000000..d9a7ace
--- /dev/null
+++ b/llparse-frontend/test/fixtures/a-implementation/code/base.ts
@@ -0,0 +1,6 @@
+export abstract class Code<T> {
+ constructor(public readonly ref: T) {
+ }
+
+ public abstract build(): string;
+}
diff --git a/llparse-frontend/test/fixtures/a-implementation/code/index.ts b/llparse-frontend/test/fixtures/a-implementation/code/index.ts
new file mode 100644
index 0000000..855a5cf
--- /dev/null
+++ b/llparse-frontend/test/fixtures/a-implementation/code/index.ts
@@ -0,0 +1,15 @@
+import { And } from './and';
+import { IsEqual } from './is-equal';
+import { Load } from './load';
+import { Match } from './match';
+import { MulAdd } from './mul-add';
+import { Or } from './or';
+import { Span } from './span';
+import { Store } from './store';
+import { Test } from './test';
+import { Update } from './update';
+import { Value } from './value';
+
+export default {
+ And, IsEqual, Load, Match, MulAdd, Or, Span, Store, Test, Update, Value,
+};
diff --git a/llparse-frontend/test/fixtures/a-implementation/code/is-equal.ts b/llparse-frontend/test/fixtures/a-implementation/code/is-equal.ts
new file mode 100644
index 0000000..13a1737
--- /dev/null
+++ b/llparse-frontend/test/fixtures/a-implementation/code/is-equal.ts
@@ -0,0 +1,8 @@
+import { code } from '../../../../src/frontend';
+import { Code } from './base';
+
+export class IsEqual extends Code<code.IsEqual> {
+ public build(): string {
+ return '';
+ }
+}
diff --git a/llparse-frontend/test/fixtures/a-implementation/code/load.ts b/llparse-frontend/test/fixtures/a-implementation/code/load.ts
new file mode 100644
index 0000000..bc97f27
--- /dev/null
+++ b/llparse-frontend/test/fixtures/a-implementation/code/load.ts
@@ -0,0 +1,8 @@
+import { code } from '../../../../src/frontend';
+import { Code } from './base';
+
+export class Load extends Code<code.Load> {
+ public build(): string {
+ return '';
+ }
+}
diff --git a/llparse-frontend/test/fixtures/a-implementation/code/match.ts b/llparse-frontend/test/fixtures/a-implementation/code/match.ts
new file mode 100644
index 0000000..e933a71
--- /dev/null
+++ b/llparse-frontend/test/fixtures/a-implementation/code/match.ts
@@ -0,0 +1,8 @@
+import { code } from '../../../../src/frontend';
+import { Code } from './base';
+
+export class Match extends Code<code.Match> {
+ public build(): string {
+ return '';
+ }
+}
diff --git a/llparse-frontend/test/fixtures/a-implementation/code/mul-add.ts b/llparse-frontend/test/fixtures/a-implementation/code/mul-add.ts
new file mode 100644
index 0000000..e06a217
--- /dev/null
+++ b/llparse-frontend/test/fixtures/a-implementation/code/mul-add.ts
@@ -0,0 +1,8 @@
+import { code } from '../../../../src/frontend';
+import { Code } from './base';
+
+export class MulAdd extends Code<code.MulAdd> {
+ public build(): string {
+ return '';
+ }
+}
diff --git a/llparse-frontend/test/fixtures/a-implementation/code/or.ts b/llparse-frontend/test/fixtures/a-implementation/code/or.ts
new file mode 100644
index 0000000..a569db4
--- /dev/null
+++ b/llparse-frontend/test/fixtures/a-implementation/code/or.ts
@@ -0,0 +1,8 @@
+import { code } from '../../../../src/frontend';
+import { Code } from './base';
+
+export class Or extends Code<code.Or> {
+ public build(): string {
+ return '';
+ }
+}
diff --git a/llparse-frontend/test/fixtures/a-implementation/code/span.ts b/llparse-frontend/test/fixtures/a-implementation/code/span.ts
new file mode 100644
index 0000000..46fc410
--- /dev/null
+++ b/llparse-frontend/test/fixtures/a-implementation/code/span.ts
@@ -0,0 +1,8 @@
+import { code } from '../../../../src/frontend';
+import { Code } from './base';
+
+export class Span extends Code<code.Span> {
+ public build(): string {
+ return '';
+ }
+}
diff --git a/llparse-frontend/test/fixtures/a-implementation/code/store.ts b/llparse-frontend/test/fixtures/a-implementation/code/store.ts
new file mode 100644
index 0000000..7a1ca9f
--- /dev/null
+++ b/llparse-frontend/test/fixtures/a-implementation/code/store.ts
@@ -0,0 +1,8 @@
+import { code } from '../../../../src/frontend';
+import { Code } from './base';
+
+export class Store extends Code<code.Store> {
+ public build(): string {
+ return '';
+ }
+}
diff --git a/llparse-frontend/test/fixtures/a-implementation/code/test.ts b/llparse-frontend/test/fixtures/a-implementation/code/test.ts
new file mode 100644
index 0000000..4fc8ddb
--- /dev/null
+++ b/llparse-frontend/test/fixtures/a-implementation/code/test.ts
@@ -0,0 +1,8 @@
+import { code } from '../../../../src/frontend';
+import { Code } from './base';
+
+export class Test extends Code<code.Test> {
+ public build(): string {
+ return '';
+ }
+}
diff --git a/llparse-frontend/test/fixtures/a-implementation/code/update.ts b/llparse-frontend/test/fixtures/a-implementation/code/update.ts
new file mode 100644
index 0000000..16b20e2
--- /dev/null
+++ b/llparse-frontend/test/fixtures/a-implementation/code/update.ts
@@ -0,0 +1,8 @@
+import { code } from '../../../../src/frontend';
+import { Code } from './base';
+
+export class Update extends Code<code.Update> {
+ public build(): string {
+ return '';
+ }
+}
diff --git a/llparse-frontend/test/fixtures/a-implementation/code/value.ts b/llparse-frontend/test/fixtures/a-implementation/code/value.ts
new file mode 100644
index 0000000..8e76e2a
--- /dev/null
+++ b/llparse-frontend/test/fixtures/a-implementation/code/value.ts
@@ -0,0 +1,8 @@
+import { code } from '../../../../src/frontend';
+import { Code } from './base';
+
+export class Value extends Code<code.Value> {
+ public build(): string {
+ return '';
+ }
+}
diff --git a/llparse-frontend/test/fixtures/a-implementation/index.ts b/llparse-frontend/test/fixtures/a-implementation/index.ts
new file mode 100644
index 0000000..1d8d29a
--- /dev/null
+++ b/llparse-frontend/test/fixtures/a-implementation/index.ts
@@ -0,0 +1,5 @@
+import code from './code';
+import node from './node';
+import transform from './transform';
+
+export default { code, node, transform };
diff --git a/llparse-frontend/test/fixtures/a-implementation/node/base.ts b/llparse-frontend/test/fixtures/a-implementation/node/base.ts
new file mode 100644
index 0000000..04c8285
--- /dev/null
+++ b/llparse-frontend/test/fixtures/a-implementation/node/base.ts
@@ -0,0 +1,38 @@
+import { ContainerWrap, node } from '../../../../src/frontend';
+
+export abstract class Node<T extends node.Node> {
+ private built: boolean = false;
+
+ constructor(public readonly ref: T) {
+ }
+
+ public build(out: string[]): void {
+ if (this.built) {
+ return;
+ }
+
+ this.built = true;
+ this.doBuild(out);
+
+ if (this.ref.otherwise !== undefined) {
+ const cwrap = this.ref.otherwise.node as ContainerWrap<T>;
+ const otherwise = cwrap.get<Node<T>>('a');
+ otherwise.build(out);
+ }
+ }
+
+ protected format(value: string): string {
+ let otherwise: string = '';
+ if (this.ref.otherwise !== undefined) {
+ const otherwiseRef = this.ref.otherwise.node.ref;
+ otherwise = ' otherwise' +
+ `${this.ref.otherwise.noAdvance ? '-no_adv' : ''}=` +
+ `${otherwiseRef.id.name}`;
+ }
+
+ return `<${this.constructor.name} name=${this.ref.id.name} ` +
+ `${value}${otherwise}/>`;
+ }
+
+ protected abstract doBuild(out: string[]): void;
+}
diff --git a/llparse-frontend/test/fixtures/a-implementation/node/consume.ts b/llparse-frontend/test/fixtures/a-implementation/node/consume.ts
new file mode 100644
index 0000000..cdc6cef
--- /dev/null
+++ b/llparse-frontend/test/fixtures/a-implementation/node/consume.ts
@@ -0,0 +1,8 @@
+import { node } from '../../../../src/frontend';
+import { Node } from './base';
+
+export class Consume extends Node<node.Consume> {
+ protected doBuild(out: string[]): void {
+ out.push(this.format(`field=${this.ref.field}`));
+ }
+}
diff --git a/llparse-frontend/test/fixtures/a-implementation/node/empty.ts b/llparse-frontend/test/fixtures/a-implementation/node/empty.ts
new file mode 100644
index 0000000..ef1499b
--- /dev/null
+++ b/llparse-frontend/test/fixtures/a-implementation/node/empty.ts
@@ -0,0 +1,8 @@
+import { node } from '../../../../src/frontend';
+import { Node } from './base';
+
+export class Empty extends Node<node.Empty> {
+ protected doBuild(out: string[]): void {
+ out.push(this.format(''));
+ }
+}
diff --git a/llparse-frontend/test/fixtures/a-implementation/node/error.ts b/llparse-frontend/test/fixtures/a-implementation/node/error.ts
new file mode 100644
index 0000000..1a4f31d
--- /dev/null
+++ b/llparse-frontend/test/fixtures/a-implementation/node/error.ts
@@ -0,0 +1,10 @@
+import { node } from '../../../../src/frontend';
+import { Node } from './base';
+
+class ErrorNode extends Node<node.Error> {
+ protected doBuild(out: string[]): void {
+ out.push(this.format(`code=${this.ref.code} reason="${this.ref.reason}"`));
+ }
+}
+
+export { ErrorNode as Error };
diff --git a/llparse-frontend/test/fixtures/a-implementation/node/index.ts b/llparse-frontend/test/fixtures/a-implementation/node/index.ts
new file mode 100644
index 0000000..31dbc5e
--- /dev/null
+++ b/llparse-frontend/test/fixtures/a-implementation/node/index.ts
@@ -0,0 +1,15 @@
+import { Consume } from './consume';
+import { Empty } from './empty';
+import { Error } from './error';
+import { Invoke } from './invoke';
+import { Pause } from './pause';
+import { Sequence } from './sequence';
+import { Single } from './single';
+import { SpanEnd } from './span-end';
+import { SpanStart } from './span-start';
+import { TableLookup } from './table-lookup';
+
+export default {
+ Consume, Empty, Error, Invoke, Pause, Sequence, Single, SpanEnd,
+ SpanStart, TableLookup,
+};
diff --git a/llparse-frontend/test/fixtures/a-implementation/node/invoke.ts b/llparse-frontend/test/fixtures/a-implementation/node/invoke.ts
new file mode 100644
index 0000000..674be5f
--- /dev/null
+++ b/llparse-frontend/test/fixtures/a-implementation/node/invoke.ts
@@ -0,0 +1,8 @@
+import { node } from '../../../../src/frontend';
+import { Node } from './base';
+
+export class Invoke extends Node<node.Invoke> {
+ protected doBuild(out: string[]): void {
+ out.push(this.format(''));
+ }
+}
diff --git a/llparse-frontend/test/fixtures/a-implementation/node/pause.ts b/llparse-frontend/test/fixtures/a-implementation/node/pause.ts
new file mode 100644
index 0000000..94da63c
--- /dev/null
+++ b/llparse-frontend/test/fixtures/a-implementation/node/pause.ts
@@ -0,0 +1,8 @@
+import { node } from '../../../../src/frontend';
+import { Node } from './base';
+
+export class Pause extends Node<node.Pause> {
+ protected doBuild(out: string[]): void {
+ out.push(this.format(''));
+ }
+}
diff --git a/llparse-frontend/test/fixtures/a-implementation/node/sequence.ts b/llparse-frontend/test/fixtures/a-implementation/node/sequence.ts
new file mode 100644
index 0000000..13fd336
--- /dev/null
+++ b/llparse-frontend/test/fixtures/a-implementation/node/sequence.ts
@@ -0,0 +1,8 @@
+import { node } from '../../../../src/frontend';
+import { Node } from './base';
+
+export class Sequence extends Node<node.Sequence> {
+ protected doBuild(out: string[]): void {
+ out.push(this.format(`select="${this.ref.select.toString('hex')}"`));
+ }
+}
diff --git a/llparse-frontend/test/fixtures/a-implementation/node/single.ts b/llparse-frontend/test/fixtures/a-implementation/node/single.ts
new file mode 100644
index 0000000..d7bcc72
--- /dev/null
+++ b/llparse-frontend/test/fixtures/a-implementation/node/single.ts
@@ -0,0 +1,18 @@
+import { ContainerWrap, node } from '../../../../src/frontend';
+import { Node } from './base';
+
+export class Single extends Node<node.Single> {
+ protected doBuild(out: string[]): void {
+ const edges: string[] = [];
+ for (const edge of this.ref.edges) {
+ edges.push(`k${edge.key}${edge.noAdvance ? '-no_adv-' : ''}=` +
+ `${edge.node.ref.id.name}`);
+ }
+ out.push(this.format(edges.join(' ')));
+
+ for (const edge of this.ref.edges) {
+ const edgeNode = edge.node as ContainerWrap<node.Node>;
+ edgeNode.get<Node<node.Node>>('a').build(out);
+ }
+ }
+}
diff --git a/llparse-frontend/test/fixtures/a-implementation/node/span-end.ts b/llparse-frontend/test/fixtures/a-implementation/node/span-end.ts
new file mode 100644
index 0000000..dc79b81
--- /dev/null
+++ b/llparse-frontend/test/fixtures/a-implementation/node/span-end.ts
@@ -0,0 +1,8 @@
+import { node } from '../../../../src/frontend';
+import { Node } from './base';
+
+export class SpanEnd extends Node<node.SpanEnd> {
+ protected doBuild(out: string[]): void {
+ out.push(this.format(''));
+ }
+}
diff --git a/llparse-frontend/test/fixtures/a-implementation/node/span-start.ts b/llparse-frontend/test/fixtures/a-implementation/node/span-start.ts
new file mode 100644
index 0000000..32e373c
--- /dev/null
+++ b/llparse-frontend/test/fixtures/a-implementation/node/span-start.ts
@@ -0,0 +1,8 @@
+import { node } from '../../../../src/frontend';
+import { Node } from './base';
+
+export class SpanStart extends Node<node.SpanStart> {
+ protected doBuild(out: string[]): void {
+ out.push(this.format(''));
+ }
+}
diff --git a/llparse-frontend/test/fixtures/a-implementation/node/table-lookup.ts b/llparse-frontend/test/fixtures/a-implementation/node/table-lookup.ts
new file mode 100644
index 0000000..e6166d0
--- /dev/null
+++ b/llparse-frontend/test/fixtures/a-implementation/node/table-lookup.ts
@@ -0,0 +1,8 @@
+import { node } from '../../../../src/frontend';
+import { Node } from './base';
+
+export class TableLookup extends Node<node.TableLookup> {
+ protected doBuild(out: string[]): void {
+ out.push(this.format(''));
+ }
+}
diff --git a/llparse-frontend/test/fixtures/a-implementation/transform/base.ts b/llparse-frontend/test/fixtures/a-implementation/transform/base.ts
new file mode 100644
index 0000000..96dc27d
--- /dev/null
+++ b/llparse-frontend/test/fixtures/a-implementation/transform/base.ts
@@ -0,0 +1,6 @@
+export abstract class Transform<T> {
+ constructor(public readonly ref: T) {
+ }
+
+ public abstract build(): string;
+}
diff --git a/llparse-frontend/test/fixtures/a-implementation/transform/id.ts b/llparse-frontend/test/fixtures/a-implementation/transform/id.ts
new file mode 100644
index 0000000..e6c1adc
--- /dev/null
+++ b/llparse-frontend/test/fixtures/a-implementation/transform/id.ts
@@ -0,0 +1,8 @@
+import { transform } from '../../../../src/frontend';
+import { Transform } from './base';
+
+export class ID extends Transform<transform.ID> {
+ public build(): string {
+ return '';
+ }
+}
diff --git a/llparse-frontend/test/fixtures/a-implementation/transform/index.ts b/llparse-frontend/test/fixtures/a-implementation/transform/index.ts
new file mode 100644
index 0000000..bed8bc9
--- /dev/null
+++ b/llparse-frontend/test/fixtures/a-implementation/transform/index.ts
@@ -0,0 +1,5 @@
+import { ID } from './id';
+import { ToLower } from './to-lower';
+import { ToLowerUnsafe } from './to-lower-unsafe';
+
+export default { ID, ToLower, ToLowerUnsafe };
diff --git a/llparse-frontend/test/fixtures/a-implementation/transform/to-lower-unsafe.ts b/llparse-frontend/test/fixtures/a-implementation/transform/to-lower-unsafe.ts
new file mode 100644
index 0000000..9d175a9
--- /dev/null
+++ b/llparse-frontend/test/fixtures/a-implementation/transform/to-lower-unsafe.ts
@@ -0,0 +1,8 @@
+import { transform } from '../../../../src/frontend';
+import { Transform } from './base';
+
+export class ToLowerUnsafe extends Transform<transform.ToLowerUnsafe> {
+ public build(): string {
+ return '';
+ }
+}
diff --git a/llparse-frontend/test/fixtures/a-implementation/transform/to-lower.ts b/llparse-frontend/test/fixtures/a-implementation/transform/to-lower.ts
new file mode 100644
index 0000000..cbe6456
--- /dev/null
+++ b/llparse-frontend/test/fixtures/a-implementation/transform/to-lower.ts
@@ -0,0 +1,8 @@
+import { transform } from '../../../../src/frontend';
+import { Transform } from './base';
+
+export class ToLower extends Transform<transform.ToLower> {
+ public build(): string {
+ return '';
+ }
+}
diff --git a/llparse-frontend/test/fixtures/implementation/code/and.ts b/llparse-frontend/test/fixtures/implementation/code/and.ts
new file mode 100644
index 0000000..c1df821
--- /dev/null
+++ b/llparse-frontend/test/fixtures/implementation/code/and.ts
@@ -0,0 +1,8 @@
+import { code } from '../../../../src/frontend';
+import { Code } from './base';
+
+export class And extends Code<code.And> {
+ public build(): string {
+ return '';
+ }
+}
diff --git a/llparse-frontend/test/fixtures/implementation/code/base.ts b/llparse-frontend/test/fixtures/implementation/code/base.ts
new file mode 100644
index 0000000..d9a7ace
--- /dev/null
+++ b/llparse-frontend/test/fixtures/implementation/code/base.ts
@@ -0,0 +1,6 @@
+export abstract class Code<T> {
+ constructor(public readonly ref: T) {
+ }
+
+ public abstract build(): string;
+}
diff --git a/llparse-frontend/test/fixtures/implementation/code/index.ts b/llparse-frontend/test/fixtures/implementation/code/index.ts
new file mode 100644
index 0000000..855a5cf
--- /dev/null
+++ b/llparse-frontend/test/fixtures/implementation/code/index.ts
@@ -0,0 +1,15 @@
+import { And } from './and';
+import { IsEqual } from './is-equal';
+import { Load } from './load';
+import { Match } from './match';
+import { MulAdd } from './mul-add';
+import { Or } from './or';
+import { Span } from './span';
+import { Store } from './store';
+import { Test } from './test';
+import { Update } from './update';
+import { Value } from './value';
+
+export default {
+ And, IsEqual, Load, Match, MulAdd, Or, Span, Store, Test, Update, Value,
+};
diff --git a/llparse-frontend/test/fixtures/implementation/code/is-equal.ts b/llparse-frontend/test/fixtures/implementation/code/is-equal.ts
new file mode 100644
index 0000000..13a1737
--- /dev/null
+++ b/llparse-frontend/test/fixtures/implementation/code/is-equal.ts
@@ -0,0 +1,8 @@
+import { code } from '../../../../src/frontend';
+import { Code } from './base';
+
+export class IsEqual extends Code<code.IsEqual> {
+ public build(): string {
+ return '';
+ }
+}
diff --git a/llparse-frontend/test/fixtures/implementation/code/load.ts b/llparse-frontend/test/fixtures/implementation/code/load.ts
new file mode 100644
index 0000000..bc97f27
--- /dev/null
+++ b/llparse-frontend/test/fixtures/implementation/code/load.ts
@@ -0,0 +1,8 @@
+import { code } from '../../../../src/frontend';
+import { Code } from './base';
+
+export class Load extends Code<code.Load> {
+ public build(): string {
+ return '';
+ }
+}
diff --git a/llparse-frontend/test/fixtures/implementation/code/match.ts b/llparse-frontend/test/fixtures/implementation/code/match.ts
new file mode 100644
index 0000000..e933a71
--- /dev/null
+++ b/llparse-frontend/test/fixtures/implementation/code/match.ts
@@ -0,0 +1,8 @@
+import { code } from '../../../../src/frontend';
+import { Code } from './base';
+
+export class Match extends Code<code.Match> {
+ public build(): string {
+ return '';
+ }
+}
diff --git a/llparse-frontend/test/fixtures/implementation/code/mul-add.ts b/llparse-frontend/test/fixtures/implementation/code/mul-add.ts
new file mode 100644
index 0000000..e06a217
--- /dev/null
+++ b/llparse-frontend/test/fixtures/implementation/code/mul-add.ts
@@ -0,0 +1,8 @@
+import { code } from '../../../../src/frontend';
+import { Code } from './base';
+
+export class MulAdd extends Code<code.MulAdd> {
+ public build(): string {
+ return '';
+ }
+}
diff --git a/llparse-frontend/test/fixtures/implementation/code/or.ts b/llparse-frontend/test/fixtures/implementation/code/or.ts
new file mode 100644
index 0000000..a569db4
--- /dev/null
+++ b/llparse-frontend/test/fixtures/implementation/code/or.ts
@@ -0,0 +1,8 @@
+import { code } from '../../../../src/frontend';
+import { Code } from './base';
+
+export class Or extends Code<code.Or> {
+ public build(): string {
+ return '';
+ }
+}
diff --git a/llparse-frontend/test/fixtures/implementation/code/span.ts b/llparse-frontend/test/fixtures/implementation/code/span.ts
new file mode 100644
index 0000000..46fc410
--- /dev/null
+++ b/llparse-frontend/test/fixtures/implementation/code/span.ts
@@ -0,0 +1,8 @@
+import { code } from '../../../../src/frontend';
+import { Code } from './base';
+
+export class Span extends Code<code.Span> {
+ public build(): string {
+ return '';
+ }
+}
diff --git a/llparse-frontend/test/fixtures/implementation/code/store.ts b/llparse-frontend/test/fixtures/implementation/code/store.ts
new file mode 100644
index 0000000..7a1ca9f
--- /dev/null
+++ b/llparse-frontend/test/fixtures/implementation/code/store.ts
@@ -0,0 +1,8 @@
+import { code } from '../../../../src/frontend';
+import { Code } from './base';
+
+export class Store extends Code<code.Store> {
+ public build(): string {
+ return '';
+ }
+}
diff --git a/llparse-frontend/test/fixtures/implementation/code/test.ts b/llparse-frontend/test/fixtures/implementation/code/test.ts
new file mode 100644
index 0000000..4fc8ddb
--- /dev/null
+++ b/llparse-frontend/test/fixtures/implementation/code/test.ts
@@ -0,0 +1,8 @@
+import { code } from '../../../../src/frontend';
+import { Code } from './base';
+
+export class Test extends Code<code.Test> {
+ public build(): string {
+ return '';
+ }
+}
diff --git a/llparse-frontend/test/fixtures/implementation/code/update.ts b/llparse-frontend/test/fixtures/implementation/code/update.ts
new file mode 100644
index 0000000..16b20e2
--- /dev/null
+++ b/llparse-frontend/test/fixtures/implementation/code/update.ts
@@ -0,0 +1,8 @@
+import { code } from '../../../../src/frontend';
+import { Code } from './base';
+
+export class Update extends Code<code.Update> {
+ public build(): string {
+ return '';
+ }
+}
diff --git a/llparse-frontend/test/fixtures/implementation/code/value.ts b/llparse-frontend/test/fixtures/implementation/code/value.ts
new file mode 100644
index 0000000..8e76e2a
--- /dev/null
+++ b/llparse-frontend/test/fixtures/implementation/code/value.ts
@@ -0,0 +1,8 @@
+import { code } from '../../../../src/frontend';
+import { Code } from './base';
+
+export class Value extends Code<code.Value> {
+ public build(): string {
+ return '';
+ }
+}
diff --git a/llparse-frontend/test/fixtures/implementation/index.ts b/llparse-frontend/test/fixtures/implementation/index.ts
new file mode 100644
index 0000000..1d8d29a
--- /dev/null
+++ b/llparse-frontend/test/fixtures/implementation/index.ts
@@ -0,0 +1,5 @@
+import code from './code';
+import node from './node';
+import transform from './transform';
+
+export default { code, node, transform };
diff --git a/llparse-frontend/test/fixtures/implementation/node/base.ts b/llparse-frontend/test/fixtures/implementation/node/base.ts
new file mode 100644
index 0000000..c9fd589
--- /dev/null
+++ b/llparse-frontend/test/fixtures/implementation/node/base.ts
@@ -0,0 +1,39 @@
+import { node } from '../../../../src/frontend';
+
+export abstract class Node<T extends node.Node> {
+ private built: boolean = false;
+
+ constructor(public readonly ref: T) {
+ }
+
+ public build(out: string[]): void {
+ if (this.built) {
+ return;
+ }
+
+ this.built = true;
+ this.doBuild(out);
+
+ if (this.ref.otherwise !== undefined) {
+ (this.ref.otherwise.node as Node<T>).build(out);
+ }
+ }
+
+ protected format(value: string): string {
+ let otherwise: string = '';
+ if (this.ref.otherwise !== undefined) {
+ const otherwiseRef = this.ref.otherwise.node.ref;
+ otherwise = ' otherwise' +
+ `${this.ref.otherwise.noAdvance ? '-no_adv' : ''}=` +
+ `${otherwiseRef.id.name}`;
+ if (this.ref.otherwise.value !== undefined) {
+ otherwise += `:${this.ref.otherwise.value}`;
+ }
+ }
+
+ return `<${this.constructor.name} name=${this.ref.id.name} ` +
+ `${value}${otherwise}/>`;
+ }
+
+ protected abstract doBuild(out: string[]): void;
+}
diff --git a/llparse-frontend/test/fixtures/implementation/node/consume.ts b/llparse-frontend/test/fixtures/implementation/node/consume.ts
new file mode 100644
index 0000000..cdc6cef
--- /dev/null
+++ b/llparse-frontend/test/fixtures/implementation/node/consume.ts
@@ -0,0 +1,8 @@
+import { node } from '../../../../src/frontend';
+import { Node } from './base';
+
+export class Consume extends Node<node.Consume> {
+ protected doBuild(out: string[]): void {
+ out.push(this.format(`field=${this.ref.field}`));
+ }
+}
diff --git a/llparse-frontend/test/fixtures/implementation/node/empty.ts b/llparse-frontend/test/fixtures/implementation/node/empty.ts
new file mode 100644
index 0000000..ef1499b
--- /dev/null
+++ b/llparse-frontend/test/fixtures/implementation/node/empty.ts
@@ -0,0 +1,8 @@
+import { node } from '../../../../src/frontend';
+import { Node } from './base';
+
+export class Empty extends Node<node.Empty> {
+ protected doBuild(out: string[]): void {
+ out.push(this.format(''));
+ }
+}
diff --git a/llparse-frontend/test/fixtures/implementation/node/error.ts b/llparse-frontend/test/fixtures/implementation/node/error.ts
new file mode 100644
index 0000000..1a4f31d
--- /dev/null
+++ b/llparse-frontend/test/fixtures/implementation/node/error.ts
@@ -0,0 +1,10 @@
+import { node } from '../../../../src/frontend';
+import { Node } from './base';
+
+class ErrorNode extends Node<node.Error> {
+ protected doBuild(out: string[]): void {
+ out.push(this.format(`code=${this.ref.code} reason="${this.ref.reason}"`));
+ }
+}
+
+export { ErrorNode as Error };
diff --git a/llparse-frontend/test/fixtures/implementation/node/index.ts b/llparse-frontend/test/fixtures/implementation/node/index.ts
new file mode 100644
index 0000000..31dbc5e
--- /dev/null
+++ b/llparse-frontend/test/fixtures/implementation/node/index.ts
@@ -0,0 +1,15 @@
+import { Consume } from './consume';
+import { Empty } from './empty';
+import { Error } from './error';
+import { Invoke } from './invoke';
+import { Pause } from './pause';
+import { Sequence } from './sequence';
+import { Single } from './single';
+import { SpanEnd } from './span-end';
+import { SpanStart } from './span-start';
+import { TableLookup } from './table-lookup';
+
+export default {
+ Consume, Empty, Error, Invoke, Pause, Sequence, Single, SpanEnd,
+ SpanStart, TableLookup,
+};
diff --git a/llparse-frontend/test/fixtures/implementation/node/invoke.ts b/llparse-frontend/test/fixtures/implementation/node/invoke.ts
new file mode 100644
index 0000000..674be5f
--- /dev/null
+++ b/llparse-frontend/test/fixtures/implementation/node/invoke.ts
@@ -0,0 +1,8 @@
+import { node } from '../../../../src/frontend';
+import { Node } from './base';
+
+export class Invoke extends Node<node.Invoke> {
+ protected doBuild(out: string[]): void {
+ out.push(this.format(''));
+ }
+}
diff --git a/llparse-frontend/test/fixtures/implementation/node/pause.ts b/llparse-frontend/test/fixtures/implementation/node/pause.ts
new file mode 100644
index 0000000..94da63c
--- /dev/null
+++ b/llparse-frontend/test/fixtures/implementation/node/pause.ts
@@ -0,0 +1,8 @@
+import { node } from '../../../../src/frontend';
+import { Node } from './base';
+
+export class Pause extends Node<node.Pause> {
+ protected doBuild(out: string[]): void {
+ out.push(this.format(''));
+ }
+}
diff --git a/llparse-frontend/test/fixtures/implementation/node/sequence.ts b/llparse-frontend/test/fixtures/implementation/node/sequence.ts
new file mode 100644
index 0000000..bb745f5
--- /dev/null
+++ b/llparse-frontend/test/fixtures/implementation/node/sequence.ts
@@ -0,0 +1,15 @@
+import { node } from '../../../../src/frontend';
+import { Node } from './base';
+
+export class Sequence extends Node<node.Sequence> {
+ protected doBuild(out: string[]): void {
+ let str = `select="${this.ref.select.toString('hex')}" ` +
+ `edge="${this.ref.edge!.node.ref.id.name}"`;
+ if (this.ref.edge!.value !== undefined) {
+ str += `:${this.ref.edge!.value}`;
+ }
+ out.push(this.format(str));
+ const edgeNode = this.ref.edge!.node as Node<node.Node>;
+ edgeNode.build(out);
+ }
+}
diff --git a/llparse-frontend/test/fixtures/implementation/node/single.ts b/llparse-frontend/test/fixtures/implementation/node/single.ts
new file mode 100644
index 0000000..b24ef93
--- /dev/null
+++ b/llparse-frontend/test/fixtures/implementation/node/single.ts
@@ -0,0 +1,22 @@
+import { node } from '../../../../src/frontend';
+import { Node } from './base';
+
+export class Single extends Node<node.Single> {
+ protected doBuild(out: string[]): void {
+ const edges: string[] = [];
+ for (const edge of this.ref.edges) {
+ let str = `k${edge.key}${edge.noAdvance ? '-no_adv-' : ''}=` +
+ `${edge.node.ref.id.name}`;
+ if (edge.value !== undefined) {
+ str += `:${edge.value}`;
+ }
+ edges.push(str);
+ }
+ out.push(this.format(edges.join(' ')));
+
+ for (const edge of this.ref.edges) {
+ const edgeNode = edge.node as Node<node.Node>;
+ edgeNode.build(out);
+ }
+ }
+}
diff --git a/llparse-frontend/test/fixtures/implementation/node/span-end.ts b/llparse-frontend/test/fixtures/implementation/node/span-end.ts
new file mode 100644
index 0000000..dc79b81
--- /dev/null
+++ b/llparse-frontend/test/fixtures/implementation/node/span-end.ts
@@ -0,0 +1,8 @@
+import { node } from '../../../../src/frontend';
+import { Node } from './base';
+
+export class SpanEnd extends Node<node.SpanEnd> {
+ protected doBuild(out: string[]): void {
+ out.push(this.format(''));
+ }
+}
diff --git a/llparse-frontend/test/fixtures/implementation/node/span-start.ts b/llparse-frontend/test/fixtures/implementation/node/span-start.ts
new file mode 100644
index 0000000..32e373c
--- /dev/null
+++ b/llparse-frontend/test/fixtures/implementation/node/span-start.ts
@@ -0,0 +1,8 @@
+import { node } from '../../../../src/frontend';
+import { Node } from './base';
+
+export class SpanStart extends Node<node.SpanStart> {
+ protected doBuild(out: string[]): void {
+ out.push(this.format(''));
+ }
+}
diff --git a/llparse-frontend/test/fixtures/implementation/node/table-lookup.ts b/llparse-frontend/test/fixtures/implementation/node/table-lookup.ts
new file mode 100644
index 0000000..e6166d0
--- /dev/null
+++ b/llparse-frontend/test/fixtures/implementation/node/table-lookup.ts
@@ -0,0 +1,8 @@
+import { node } from '../../../../src/frontend';
+import { Node } from './base';
+
+export class TableLookup extends Node<node.TableLookup> {
+ protected doBuild(out: string[]): void {
+ out.push(this.format(''));
+ }
+}
diff --git a/llparse-frontend/test/fixtures/implementation/transform/base.ts b/llparse-frontend/test/fixtures/implementation/transform/base.ts
new file mode 100644
index 0000000..96dc27d
--- /dev/null
+++ b/llparse-frontend/test/fixtures/implementation/transform/base.ts
@@ -0,0 +1,6 @@
+export abstract class Transform<T> {
+ constructor(public readonly ref: T) {
+ }
+
+ public abstract build(): string;
+}
diff --git a/llparse-frontend/test/fixtures/implementation/transform/id.ts b/llparse-frontend/test/fixtures/implementation/transform/id.ts
new file mode 100644
index 0000000..e6c1adc
--- /dev/null
+++ b/llparse-frontend/test/fixtures/implementation/transform/id.ts
@@ -0,0 +1,8 @@
+import { transform } from '../../../../src/frontend';
+import { Transform } from './base';
+
+export class ID extends Transform<transform.ID> {
+ public build(): string {
+ return '';
+ }
+}
diff --git a/llparse-frontend/test/fixtures/implementation/transform/index.ts b/llparse-frontend/test/fixtures/implementation/transform/index.ts
new file mode 100644
index 0000000..bed8bc9
--- /dev/null
+++ b/llparse-frontend/test/fixtures/implementation/transform/index.ts
@@ -0,0 +1,5 @@
+import { ID } from './id';
+import { ToLower } from './to-lower';
+import { ToLowerUnsafe } from './to-lower-unsafe';
+
+export default { ID, ToLower, ToLowerUnsafe };
diff --git a/llparse-frontend/test/fixtures/implementation/transform/to-lower-unsafe.ts b/llparse-frontend/test/fixtures/implementation/transform/to-lower-unsafe.ts
new file mode 100644
index 0000000..9d175a9
--- /dev/null
+++ b/llparse-frontend/test/fixtures/implementation/transform/to-lower-unsafe.ts
@@ -0,0 +1,8 @@
+import { transform } from '../../../../src/frontend';
+import { Transform } from './base';
+
+export class ToLowerUnsafe extends Transform<transform.ToLowerUnsafe> {
+ public build(): string {
+ return '';
+ }
+}
diff --git a/llparse-frontend/test/fixtures/implementation/transform/to-lower.ts b/llparse-frontend/test/fixtures/implementation/transform/to-lower.ts
new file mode 100644
index 0000000..cbe6456
--- /dev/null
+++ b/llparse-frontend/test/fixtures/implementation/transform/to-lower.ts
@@ -0,0 +1,8 @@
+import { transform } from '../../../../src/frontend';
+import { Transform } from './base';
+
+export class ToLower extends Transform<transform.ToLower> {
+ public build(): string {
+ return '';
+ }
+}
diff --git a/llparse-frontend/test/frontend-test.ts b/llparse-frontend/test/frontend-test.ts
new file mode 100644
index 0000000..69e075c
--- /dev/null
+++ b/llparse-frontend/test/frontend-test.ts
@@ -0,0 +1,187 @@
+import * as assert from 'assert';
+
+import * as source from 'llparse-builder';
+
+import { Frontend, node } from '../src/frontend';
+import implementation from './fixtures/implementation';
+import { Node } from './fixtures/implementation/node/base';
+
+function checkNodes(f: Frontend, root: source.node.Node,
+ expected: ReadonlyArray<string>) {
+ const fRoot = f.compile(root, []).root as Node<node.Node>;
+
+ const out: string[] = [];
+ fRoot.build(out);
+
+ assert.deepStrictEqual(out, expected);
+
+ return fRoot;
+}
+
+function checkResumptionTargets(f: Frontend, expected: ReadonlyArray<string>) {
+ const targets = Array.from(f.getResumptionTargets()).map((t) => {
+ return t.ref.id.name;
+ });
+
+ assert.deepStrictEqual(targets, expected);
+}
+
+describe('llparse-frontend', () => {
+ let b: source.Builder;
+ let f: Frontend;
+ beforeEach(() => {
+ b = new source.Builder();
+ f = new Frontend('llparse', implementation);
+ });
+
+ it('should translate nodes to implementation', () => {
+ const root = b.node('root');
+
+ root.match('ab', root);
+ root.match('acd', root);
+ root.match('efg', root);
+ root.otherwise(b.error(123, 'hello'));
+
+ checkNodes(f, root, [
+ '<Single name=llparse__n_root k97=llparse__n_root_1 ' +
+ 'k101=llparse__n_root_3 otherwise-no_adv=llparse__n_error/>',
+ '<Single name=llparse__n_root_1 k98=llparse__n_root ' +
+ 'k99=llparse__n_root_2 otherwise-no_adv=llparse__n_error/>',
+ '<Single name=llparse__n_root_2 k100=llparse__n_root ' +
+ 'otherwise-no_adv=llparse__n_error/>',
+ '<ErrorNode name=llparse__n_error code=123 reason="hello"/>',
+ '<Sequence name=llparse__n_root_3 select="6667" ' +
+ 'edge=\"llparse__n_root\" ' +
+ 'otherwise-no_adv=llparse__n_error/>',
+ ]);
+
+ checkResumptionTargets(f, [
+ 'llparse__n_root',
+ 'llparse__n_root_1',
+ 'llparse__n_root_3',
+ 'llparse__n_root_2',
+ ]);
+ });
+
+ it('should do peephole optimization', () => {
+ const root = b.node('root');
+ const root1 = b.node('a');
+ const root2 = b.node('b');
+ const node1 = b.node('c');
+ const node2 = b.node('d');
+
+ root.otherwise(root1);
+ root1.otherwise(root2);
+ root2.skipTo(node1);
+ node1.otherwise(node2);
+ node2.otherwise(root);
+
+ checkNodes(f, root, [
+ '<Empty name=llparse__n_b otherwise=llparse__n_b/>',
+ ]);
+
+ checkResumptionTargets(f, [
+ 'llparse__n_b',
+ ]);
+ });
+
+ it('should generate proper resumption targets', () => {
+ b.property('i64', 'counter');
+
+ const root = b.node('root');
+ const end = b.node('end');
+ const store = b.invoke(b.code.store('counter'));
+
+ root.select({ a: 1, b: 2 }, store);
+ root.otherwise(b.error(1, 'okay'));
+
+ store.otherwise(end);
+
+ end.match('ohai', root);
+ end.match('paus', b.pause(1, 'paused').otherwise(
+ b.pause(2, 'paused').otherwise(root)));
+ end.otherwise(b.error(2, 'ohai'));
+
+ checkNodes(f, root, [
+ '<Single name=llparse__n_root k97=llparse__n_invoke_store_counter:1 ' +
+ 'k98=llparse__n_invoke_store_counter:2 ' +
+ 'otherwise-no_adv=llparse__n_error_1/>',
+ '<Invoke name=llparse__n_invoke_store_counter ' +
+ 'otherwise-no_adv=llparse__n_end/>',
+ '<Single name=llparse__n_end k111=llparse__n_end_1 ' +
+ 'k112=llparse__n_end_2 otherwise-no_adv=llparse__n_error/>',
+ '<Sequence name=llparse__n_end_1 select="686169" ' +
+ 'edge="llparse__n_root" otherwise-no_adv=llparse__n_error/>',
+ '<ErrorNode name=llparse__n_error code=2 reason="ohai"/>',
+ '<Sequence name=llparse__n_end_2 select="617573" ' +
+ 'edge="llparse__n_pause" otherwise-no_adv=llparse__n_error/>',
+ '<Pause name=llparse__n_pause otherwise-no_adv=llparse__n_pause_1/>',
+ '<Pause name=llparse__n_pause_1 otherwise-no_adv=llparse__n_root/>',
+ '<ErrorNode name=llparse__n_error_1 code=1 reason="okay"/>',
+ ]);
+
+ checkResumptionTargets(f, [
+ 'llparse__n_root',
+ 'llparse__n_end',
+ 'llparse__n_end_1',
+ 'llparse__n_end_2',
+ 'llparse__n_pause_1',
+ ]);
+ });
+
+ it('should translate Span code into Span', () => {
+ const root = b.invoke(b.code.span('my_span'));
+ root.otherwise(b.error(1, 'okay'));
+
+ const fRoot = checkNodes(f, root, [
+ '<Invoke name=llparse__n_invoke_my_span ' +
+ 'otherwise-no_adv=llparse__n_error/>',
+ '<ErrorNode name=llparse__n_error code=1 reason="okay"/>',
+ ]);
+
+ assert((fRoot.ref as any).code instanceof implementation.code.Span);
+ });
+
+ it('should translate overlapping matches', () => {
+ const root = b.node('root');
+
+ root.match('ab', root);
+ root.match('abc', root);
+ root.otherwise(b.error(123, 'hello'));
+
+ checkNodes(f, root, [
+ '<Sequence name=llparse__n_root select="6162" edge="llparse__n_root_1" otherwise-no_adv=llparse__n_error/>',
+ '<Single name=llparse__n_root_1 k99=llparse__n_root otherwise-no_adv=llparse__n_root/>',
+ '<ErrorNode name=llparse__n_error code=123 reason="hello"/>',
+ ]);
+
+ checkResumptionTargets(f, [
+ 'llparse__n_root',
+ 'llparse__n_root_1',
+ ]);
+ });
+
+ it('should translate overlapping matches with values', () => {
+ const root = b.node('root');
+ const store = b.invoke(b.code.store('counter'));
+
+ root.select({
+ ab: 1,
+ abc: 2,
+ }, store);
+ store.otherwise(root);
+ root.otherwise(b.error(123, 'hello'));
+
+ checkNodes(f, root, [
+ '<Sequence name=llparse__n_root select="6162" edge="llparse__n_root_1" otherwise-no_adv=llparse__n_error/>',
+ '<Single name=llparse__n_root_1 k99=llparse__n_invoke_store_counter:2 otherwise-no_adv=llparse__n_invoke_store_counter:1/>',
+ '<Invoke name=llparse__n_invoke_store_counter otherwise-no_adv=llparse__n_root/>',
+ '<ErrorNode name=llparse__n_error code=123 reason="hello"/>',
+ ]);
+
+ checkResumptionTargets(f, [
+ 'llparse__n_root',
+ 'llparse__n_root_1',
+ ]);
+ });
+});
diff --git a/llparse-frontend/tsconfig.json b/llparse-frontend/tsconfig.json
new file mode 100644
index 0000000..01ec7c2
--- /dev/null
+++ b/llparse-frontend/tsconfig.json
@@ -0,0 +1,15 @@
+{
+ "compilerOptions": {
+ "strict": true,
+ "target": "es2017",
+ "module": "commonjs",
+ "moduleResolution": "node",
+ "outDir": "./lib",
+ "declaration": true,
+ "pretty": true,
+ "sourceMap": true
+ },
+ "include": [
+ "src/**/*.ts"
+ ]
+}
diff --git a/llparse-frontend/tslint.json b/llparse-frontend/tslint.json
new file mode 100644
index 0000000..24fec09
--- /dev/null
+++ b/llparse-frontend/tslint.json
@@ -0,0 +1,16 @@
+{
+ "defaultSeverity": "error",
+ "extends": [
+ "tslint:recommended"
+ ],
+ "jsRules": {},
+ "rules": {
+ "no-bitwise": null,
+ "max-line-length": [true, 80],
+ "max-classes-per-file": [true, 1, "exclude-class-expressions"],
+ "quotemark": [
+ true, "single", "avoid-escape", "avoid-template"
+ ]
+ },
+ "rulesDirectory": []
+}
diff --git a/llparse/.gitignore b/llparse/.gitignore
new file mode 100644
index 0000000..88b2771
--- /dev/null
+++ b/llparse/.gitignore
@@ -0,0 +1,4 @@
+node_modules/
+npm-debug.log
+lib/
+test/tmp/
diff --git a/llparse/.travis.yml b/llparse/.travis.yml
new file mode 100644
index 0000000..b381e1b
--- /dev/null
+++ b/llparse/.travis.yml
@@ -0,0 +1,6 @@
+sudo: false
+language: node_js
+node_js:
+ - "stable"
+script:
+ CFLAGS="-O0" npm test
diff --git a/llparse/CNAME b/llparse/CNAME
new file mode 100644
index 0000000..e39566e
--- /dev/null
+++ b/llparse/CNAME
@@ -0,0 +1 @@
+llparse.org \ No newline at end of file
diff --git a/llparse/CODE_OF_CONDUCT.md b/llparse/CODE_OF_CONDUCT.md
new file mode 100644
index 0000000..4c21140
--- /dev/null
+++ b/llparse/CODE_OF_CONDUCT.md
@@ -0,0 +1,4 @@
+# Code of Conduct
+
+* [Node.js Code of Conduct](https://github.com/nodejs/admin/blob/master/CODE_OF_CONDUCT.md)
+* [Node.js Moderation Policy](https://github.com/nodejs/admin/blob/master/Moderation-Policy.md)
diff --git a/llparse/LICENSE-MIT b/llparse/LICENSE-MIT
new file mode 100644
index 0000000..6c1512d
--- /dev/null
+++ b/llparse/LICENSE-MIT
@@ -0,0 +1,22 @@
+This software is licensed under the MIT License.
+
+Copyright Fedor Indutny, 2018.
+
+Permission is hereby granted, free of charge, to any person obtaining a
+copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to permit
+persons to whom the Software is furnished to do so, subject to the
+following conditions:
+
+The above copyright notice and this permission notice shall be included
+in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
+NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
+USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/llparse/README.md b/llparse/README.md
new file mode 100644
index 0000000..afbe4aa
--- /dev/null
+++ b/llparse/README.md
@@ -0,0 +1,86 @@
+# llparse
+[![Build Status](https://secure.travis-ci.org/nodejs/llparse.svg)](http://travis-ci.org/nodejs/llparse)
+[![NPM version](https://badge.fury.io/js/llparse.svg)](https://badge.fury.io/js/llparse)
+
+An API for compiling an incremental parser into a C output.
+
+## Usage
+
+```ts
+import { LLParse } from 'llparse';
+
+const p = new LLParse('http_parser');
+
+const method = p.node('method');
+const beforeUrl = p.node('before_url');
+const urlSpan = p.span(p.code.span('on_url'));
+const url = p.node('url');
+const http = p.node('http');
+
+// Add custom uint8_t property to the state
+p.property('i8', 'method');
+
+// Store method inside a custom property
+const onMethod = p.invoke(p.code.store('method'), beforeUrl);
+
+// Invoke custom C function
+const complete = p.invoke(p.code.match('on_complete'), {
+ // Restart
+ 0: method
+}, p.error(4, '`on_complete` error'));
+
+method
+ .select({
+ 'HEAD': 0, 'GET': 1, 'POST': 2, 'PUT': 3,
+ 'DELETE': 4, 'OPTIONS': 5, 'CONNECT': 6,
+ 'TRACE': 7, 'PATCH': 8
+ }, onMethod)
+ .otherwise(p.error(5, 'Expected method'));
+
+beforeUrl
+ .match(' ', beforeUrl)
+ .otherwise(urlSpan.start(url));
+
+url
+ .peek(' ', urlSpan.end(http))
+ .skipTo(url);
+
+http
+ .match(' HTTP/1.1\r\n\r\n', complete)
+ .otherwise(p.error(6, 'Expected HTTP/1.1 and two newlines'));
+
+const artifacts = p.build(method);
+console.log('----- C -----');
+console.log(artifacts.c); // string
+console.log('----- C END -----');
+console.log('----- HEADER -----');
+console.log(artifacts.header);
+console.log('----- HEADER END -----');
+```
+
+#### LICENSE
+
+This software is licensed under the MIT License.
+
+Copyright Fedor Indutny, 2020.
+
+Permission is hereby granted, free of charge, to any person obtaining a
+copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to permit
+persons to whom the Software is furnished to do so, subject to the
+following conditions:
+
+The above copyright notice and this permission notice shall be included
+in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
+NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
+USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+[3]: https://llvm.org/docs/LangRef.html
diff --git a/llparse/_config.yml b/llparse/_config.yml
new file mode 100644
index 0000000..1885487
--- /dev/null
+++ b/llparse/_config.yml
@@ -0,0 +1 @@
+theme: jekyll-theme-midnight \ No newline at end of file
diff --git a/llparse/examples/http/.gitignore b/llparse/examples/http/.gitignore
new file mode 100644
index 0000000..fcfe02e
--- /dev/null
+++ b/llparse/examples/http/.gitignore
@@ -0,0 +1,6 @@
+http
+*.c
+*.ll
+*.h
+*.o
+*.dSYM
diff --git a/llparse/examples/http/Makefile b/llparse/examples/http/Makefile
new file mode 100644
index 0000000..323d2e3
--- /dev/null
+++ b/llparse/examples/http/Makefile
@@ -0,0 +1,11 @@
+CC ?= clang
+
+all: http
+
+http: main.c http_parser.bc
+ $(CC) -g3 -flto -Os -fvisibility=hidden -Wall -I. http_parser.c main.c -o $@
+
+http_parser.bc: index.ts
+ npx ts-node $<
+
+.PHONY = all
diff --git a/llparse/examples/http/index.ts b/llparse/examples/http/index.ts
new file mode 100644
index 0000000..dc7f28a
--- /dev/null
+++ b/llparse/examples/http/index.ts
@@ -0,0 +1,51 @@
+import { LLParse } from '../../src/api';
+
+const p = new LLParse('http_parser');
+
+const method = p.node('method');
+const beforeUrl = p.node('before_url');
+const urlSpan = p.span(p.code.span('on_url'));
+const url = p.node('url');
+const http = p.node('http');
+
+// Add custom uint8_t property to the state
+p.property('i8', 'method');
+
+// Store method inside a custom property
+const onMethod = p.invoke(p.code.store('method'), beforeUrl);
+
+// Invoke custom C function
+const complete = p.invoke(p.code.match('on_complete'), {
+ // Restart
+ 0: method
+}, p.error(4, '`on_complete` error'));
+
+method
+ .select({
+ 'HEAD': 0, 'GET': 1, 'POST': 2, 'PUT': 3,
+ 'DELETE': 4, 'OPTIONS': 5, 'CONNECT': 6,
+ 'TRACE': 7, 'PATCH': 8
+ }, onMethod)
+ .otherwise(p.error(5, 'Expected method'));
+
+beforeUrl
+ .match(' ', beforeUrl)
+ .otherwise(urlSpan.start(url));
+
+url
+ .peek(' ', urlSpan.end(http))
+ .skipTo(url);
+
+http
+ .match(' HTTP/1.1\r\n\r\n', complete)
+ .match(' HTTP/1.1\n\n', complete)
+ .otherwise(p.error(6, 'Expected HTTP/1.1 and two newlines'));
+
+// Build
+
+const fs = require('fs');
+const path = require('path');
+
+const artifacts = p.build(method);
+fs.writeFileSync(path.join(__dirname, 'http_parser.h'), artifacts.header);
+fs.writeFileSync(path.join(__dirname, 'http_parser.c'), artifacts.c);
diff --git a/llparse/examples/http/main.c b/llparse/examples/http/main.c
new file mode 100644
index 0000000..4721a19
--- /dev/null
+++ b/llparse/examples/http/main.c
@@ -0,0 +1,48 @@
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/time.h>
+
+#include "http_parser.h"
+
+int on_url(http_parser_t* s, const char* p, const char* endp) {
+ if (p == endp)
+ return 0;
+
+ fprintf(stdout, "method=%d url_part=\"%.*s\"\n", s->method,
+ (int) (endp - p), p);
+ return 0;
+}
+
+
+int on_complete(http_parser_t* s, const char* p, const char* endp) {
+ fprintf(stdout, "on_complete\n");
+ return 0;
+}
+
+
+int main(int argc, char** argv) {
+ http_parser_t s;
+
+ http_parser_init(&s);
+
+ for (;;) {
+ char buf[16384];
+ const char* input;
+ const char* endp;
+ int code;
+
+ input = fgets(buf, sizeof(buf), stdin);
+ if (input == NULL)
+ break;
+
+ endp = input + strlen(input);
+ code = http_parser_execute(&s, input, endp);
+ if (code != 0) {
+ fprintf(stderr, "code=%d error=%d reason=%s\n", code, s.error, s.reason);
+ return -1;
+ }
+ }
+
+ return 0;
+}
diff --git a/llparse/package-lock.json b/llparse/package-lock.json
new file mode 100644
index 0000000..e4c2b29
--- /dev/null
+++ b/llparse/package-lock.json
@@ -0,0 +1,1802 @@
+{
+ "name": "llparse",
+ "version": "7.1.1",
+ "lockfileVersion": 1,
+ "requires": true,
+ "dependencies": {
+ "@babel/code-frame": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz",
+ "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==",
+ "dev": true,
+ "requires": {
+ "@babel/highlight": "^7.10.4"
+ }
+ },
+ "@babel/helper-validator-identifier": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz",
+ "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==",
+ "dev": true
+ },
+ "@babel/highlight": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz",
+ "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==",
+ "dev": true,
+ "requires": {
+ "@babel/helper-validator-identifier": "^7.10.4",
+ "chalk": "^2.0.0",
+ "js-tokens": "^4.0.0"
+ }
+ },
+ "@types/color-name": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz",
+ "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==",
+ "dev": true
+ },
+ "@types/debug": {
+ "version": "4.1.5",
+ "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.5.tgz",
+ "integrity": "sha512-Q1y515GcOdTHgagaVFhHnIFQ38ygs/kmxdNpvpou+raI9UO3YZcHDngBSYKQklcKlvA7iuQlmIKbzvmxcOE9CQ==",
+ "dev": true
+ },
+ "@types/mocha": {
+ "version": "8.0.3",
+ "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-8.0.3.tgz",
+ "integrity": "sha512-vyxR57nv8NfcU0GZu8EUXZLTbCMupIUwy95LJ6lllN+JRPG25CwMHoB1q5xKh8YKhQnHYRAn4yW2yuHbf/5xgg==",
+ "dev": true
+ },
+ "@types/node": {
+ "version": "14.11.8",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-14.11.8.tgz",
+ "integrity": "sha512-KPcKqKm5UKDkaYPTuXSx8wEP7vE9GnuaXIZKijwRYcePpZFDVuy2a57LarFKiORbHOuTOOwYzxVxcUzsh2P2Pw==",
+ "dev": true
+ },
+ "ansi-colors": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz",
+ "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==",
+ "dev": true
+ },
+ "ansi-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
+ "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=",
+ "dev": true
+ },
+ "ansi-styles": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+ "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+ "dev": true,
+ "requires": {
+ "color-convert": "^1.9.0"
+ }
+ },
+ "anymatch": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz",
+ "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==",
+ "dev": true,
+ "requires": {
+ "normalize-path": "^3.0.0",
+ "picomatch": "^2.0.4"
+ }
+ },
+ "arg": {
+ "version": "4.1.3",
+ "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
+ "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
+ "dev": true
+ },
+ "argparse": {
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
+ "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
+ "dev": true,
+ "requires": {
+ "sprintf-js": "~1.0.2"
+ }
+ },
+ "array.prototype.map": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/array.prototype.map/-/array.prototype.map-1.0.2.tgz",
+ "integrity": "sha512-Az3OYxgsa1g7xDYp86l0nnN4bcmuEITGe1rbdEBVkrqkzMgDcbdQ2R7r41pNzti+4NMces3H8gMmuioZUilLgw==",
+ "dev": true,
+ "requires": {
+ "define-properties": "^1.1.3",
+ "es-abstract": "^1.17.0-next.1",
+ "es-array-method-boxes-properly": "^1.0.0",
+ "is-string": "^1.0.4"
+ }
+ },
+ "balanced-match": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
+ "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
+ "dev": true
+ },
+ "binary-extensions": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.1.0.tgz",
+ "integrity": "sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ==",
+ "dev": true
+ },
+ "binary-search": {
+ "version": "1.3.6",
+ "resolved": "https://registry.npmjs.org/binary-search/-/binary-search-1.3.6.tgz",
+ "integrity": "sha512-nbE1WxOTTrUWIfsfZ4aHGYu5DOuNkbxGokjV6Z2kxfJK3uaAb8zNK1muzOeipoLHZjInT4Br88BHpzevc681xA=="
+ },
+ "bitcode": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/bitcode/-/bitcode-1.2.0.tgz",
+ "integrity": "sha512-cWgZK/ri/1ZUJ+UKEwP9Cqw10WY5wHz+boMxVO4vvc0btmxa2tMc2m2Zk9HYdCyx4b5+sgQM1/NCJPTIPO1XOw==",
+ "dev": true,
+ "requires": {
+ "bitcode-builder": "^1.2.0"
+ }
+ },
+ "bitcode-builder": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/bitcode-builder/-/bitcode-builder-1.2.0.tgz",
+ "integrity": "sha512-biuJIhrog5d1IFMaKtHMJ8PJ1L3zxiWdclwYErjOBWf8Gwyqa4XwflvMufzcQw/OUeAArO1AqOrqsOFsWJ94OA==",
+ "dev": true
+ },
+ "brace-expansion": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "dev": true,
+ "requires": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "braces": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
+ "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
+ "dev": true,
+ "requires": {
+ "fill-range": "^7.0.1"
+ }
+ },
+ "browser-stdout": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz",
+ "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==",
+ "dev": true
+ },
+ "buffer-from": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz",
+ "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==",
+ "dev": true
+ },
+ "builtin-modules": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz",
+ "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=",
+ "dev": true
+ },
+ "camelcase": {
+ "version": "5.3.1",
+ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
+ "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
+ "dev": true
+ },
+ "chalk": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
+ "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
+ "dev": true,
+ "requires": {
+ "ansi-styles": "^3.2.1",
+ "escape-string-regexp": "^1.0.5",
+ "supports-color": "^5.3.0"
+ },
+ "dependencies": {
+ "supports-color": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+ "dev": true,
+ "requires": {
+ "has-flag": "^3.0.0"
+ }
+ }
+ }
+ },
+ "chokidar": {
+ "version": "3.4.2",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.4.2.tgz",
+ "integrity": "sha512-IZHaDeBeI+sZJRX7lGcXsdzgvZqKv6sECqsbErJA4mHWfpRrD8B97kSFN4cQz6nGBGiuFia1MKR4d6c1o8Cv7A==",
+ "dev": true,
+ "requires": {
+ "anymatch": "~3.1.1",
+ "braces": "~3.0.2",
+ "fsevents": "~2.1.2",
+ "glob-parent": "~5.1.0",
+ "is-binary-path": "~2.1.0",
+ "is-glob": "~4.0.1",
+ "normalize-path": "~3.0.0",
+ "readdirp": "~3.4.0"
+ }
+ },
+ "cliui": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
+ "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
+ "dev": true,
+ "requires": {
+ "string-width": "^4.2.0",
+ "strip-ansi": "^6.0.0",
+ "wrap-ansi": "^6.2.0"
+ },
+ "dependencies": {
+ "ansi-regex": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
+ "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==",
+ "dev": true
+ },
+ "emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true
+ },
+ "is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "dev": true
+ },
+ "string-width": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz",
+ "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==",
+ "dev": true,
+ "requires": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.0"
+ }
+ },
+ "strip-ansi": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
+ "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==",
+ "dev": true,
+ "requires": {
+ "ansi-regex": "^5.0.0"
+ }
+ }
+ }
+ },
+ "color-convert": {
+ "version": "1.9.3",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
+ "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
+ "dev": true,
+ "requires": {
+ "color-name": "1.1.3"
+ }
+ },
+ "color-name": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
+ "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
+ "dev": true
+ },
+ "commander": {
+ "version": "2.20.3",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
+ "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
+ "dev": true
+ },
+ "concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
+ "dev": true
+ },
+ "debug": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz",
+ "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==",
+ "requires": {
+ "ms": "2.1.2"
+ }
+ },
+ "decamelize": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
+ "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=",
+ "dev": true
+ },
+ "define-properties": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz",
+ "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==",
+ "requires": {
+ "object-keys": "^1.0.12"
+ }
+ },
+ "diff": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
+ "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
+ "dev": true
+ },
+ "emoji-regex": {
+ "version": "7.0.3",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz",
+ "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==",
+ "dev": true
+ },
+ "es-abstract": {
+ "version": "1.17.7",
+ "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.7.tgz",
+ "integrity": "sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==",
+ "requires": {
+ "es-to-primitive": "^1.2.1",
+ "function-bind": "^1.1.1",
+ "has": "^1.0.3",
+ "has-symbols": "^1.0.1",
+ "is-callable": "^1.2.2",
+ "is-regex": "^1.1.1",
+ "object-inspect": "^1.8.0",
+ "object-keys": "^1.1.1",
+ "object.assign": "^4.1.1",
+ "string.prototype.trimend": "^1.0.1",
+ "string.prototype.trimstart": "^1.0.1"
+ },
+ "dependencies": {
+ "es-abstract": {
+ "version": "1.18.0-next.1",
+ "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.1.tgz",
+ "integrity": "sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA==",
+ "requires": {
+ "es-to-primitive": "^1.2.1",
+ "function-bind": "^1.1.1",
+ "has": "^1.0.3",
+ "has-symbols": "^1.0.1",
+ "is-callable": "^1.2.2",
+ "is-negative-zero": "^2.0.0",
+ "is-regex": "^1.1.1",
+ "object-inspect": "^1.8.0",
+ "object-keys": "^1.1.1",
+ "object.assign": "^4.1.1",
+ "string.prototype.trimend": "^1.0.1",
+ "string.prototype.trimstart": "^1.0.1"
+ }
+ },
+ "object.assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.1.tgz",
+ "integrity": "sha512-VT/cxmx5yaoHSOTSyrCygIDFco+RsibY2NM0a4RdEeY/4KgqezwFtK1yr3U67xYhqJSlASm2pKhLVzPj2lr4bA==",
+ "requires": {
+ "define-properties": "^1.1.3",
+ "es-abstract": "^1.18.0-next.0",
+ "has-symbols": "^1.0.1",
+ "object-keys": "^1.1.1"
+ },
+ "dependencies": {
+ "es-abstract": {
+ "version": "1.18.0-next.1",
+ "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.1.tgz",
+ "integrity": "sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA==",
+ "requires": {
+ "es-to-primitive": "^1.2.1",
+ "function-bind": "^1.1.1",
+ "has": "^1.0.3",
+ "has-symbols": "^1.0.1",
+ "is-callable": "^1.2.2",
+ "is-negative-zero": "^2.0.0",
+ "is-regex": "^1.1.1",
+ "object-inspect": "^1.8.0",
+ "object-keys": "^1.1.1",
+ "object.assign": "^4.1.1",
+ "string.prototype.trimend": "^1.0.1",
+ "string.prototype.trimstart": "^1.0.1"
+ }
+ }
+ }
+ }
+ }
+ },
+ "es-array-method-boxes-properly": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz",
+ "integrity": "sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA==",
+ "dev": true
+ },
+ "es-get-iterator": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.0.tgz",
+ "integrity": "sha512-UfrmHuWQlNMTs35e1ypnvikg6jCz3SK8v8ImvmDsh36fCVUR1MqoFDiyn0/k52C8NqO3YsO8Oe0azeesNuqSsQ==",
+ "dev": true,
+ "requires": {
+ "es-abstract": "^1.17.4",
+ "has-symbols": "^1.0.1",
+ "is-arguments": "^1.0.4",
+ "is-map": "^2.0.1",
+ "is-set": "^2.0.1",
+ "is-string": "^1.0.5",
+ "isarray": "^2.0.5"
+ }
+ },
+ "es-to-primitive": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz",
+ "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==",
+ "requires": {
+ "is-callable": "^1.1.4",
+ "is-date-object": "^1.0.1",
+ "is-symbol": "^1.0.2"
+ }
+ },
+ "escape-string-regexp": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+ "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=",
+ "dev": true
+ },
+ "esm": {
+ "version": "3.2.25",
+ "resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz",
+ "integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==",
+ "dev": true
+ },
+ "esprima": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
+ "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
+ "dev": true
+ },
+ "fill-range": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
+ "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
+ "dev": true,
+ "requires": {
+ "to-regex-range": "^5.0.1"
+ }
+ },
+ "find-up": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+ "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+ "dev": true,
+ "requires": {
+ "locate-path": "^6.0.0",
+ "path-exists": "^4.0.0"
+ }
+ },
+ "flat": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/flat/-/flat-4.1.0.tgz",
+ "integrity": "sha512-Px/TiLIznH7gEDlPXcUD4KnBusa6kR6ayRUVcnEAbreRIuhkqow/mun59BuRXwoYk7ZQOLW1ZM05ilIvK38hFw==",
+ "dev": true,
+ "requires": {
+ "is-buffer": "~2.0.3"
+ }
+ },
+ "fs.realpath": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+ "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
+ "dev": true
+ },
+ "fsevents": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz",
+ "integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==",
+ "dev": true,
+ "optional": true
+ },
+ "function-bind": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
+ "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
+ },
+ "get-caller-file": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
+ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
+ "dev": true
+ },
+ "glob": {
+ "version": "7.1.3",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz",
+ "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==",
+ "dev": true,
+ "requires": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.0.4",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ }
+ },
+ "glob-parent": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz",
+ "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==",
+ "dev": true,
+ "requires": {
+ "is-glob": "^4.0.1"
+ }
+ },
+ "growl": {
+ "version": "1.10.5",
+ "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz",
+ "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==",
+ "dev": true
+ },
+ "has": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
+ "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
+ "requires": {
+ "function-bind": "^1.1.1"
+ }
+ },
+ "has-flag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+ "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
+ "dev": true
+ },
+ "has-symbols": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz",
+ "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg=="
+ },
+ "he": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
+ "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
+ "dev": true
+ },
+ "inflight": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+ "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
+ "dev": true,
+ "requires": {
+ "once": "^1.3.0",
+ "wrappy": "1"
+ }
+ },
+ "inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "dev": true
+ },
+ "is-arguments": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.0.4.tgz",
+ "integrity": "sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA==",
+ "dev": true
+ },
+ "is-binary-path": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+ "dev": true,
+ "requires": {
+ "binary-extensions": "^2.0.0"
+ }
+ },
+ "is-buffer": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.4.tgz",
+ "integrity": "sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A==",
+ "dev": true
+ },
+ "is-callable": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.2.tgz",
+ "integrity": "sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA=="
+ },
+ "is-date-object": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.2.tgz",
+ "integrity": "sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g=="
+ },
+ "is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
+ "dev": true
+ },
+ "is-fullwidth-code-point": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
+ "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=",
+ "dev": true
+ },
+ "is-glob": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz",
+ "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==",
+ "dev": true,
+ "requires": {
+ "is-extglob": "^2.1.1"
+ }
+ },
+ "is-map": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.1.tgz",
+ "integrity": "sha512-T/S49scO8plUiAOA2DBTBG3JHpn1yiw0kRp6dgiZ0v2/6twi5eiB0rHtHFH9ZIrvlWc6+4O+m4zg5+Z833aXgw==",
+ "dev": true
+ },
+ "is-negative-zero": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.0.tgz",
+ "integrity": "sha1-lVOxIbD6wohp2p7UWeIMdUN4hGE="
+ },
+ "is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "dev": true
+ },
+ "is-plain-obj": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz",
+ "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=",
+ "dev": true
+ },
+ "is-regex": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz",
+ "integrity": "sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==",
+ "requires": {
+ "has-symbols": "^1.0.1"
+ }
+ },
+ "is-set": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.1.tgz",
+ "integrity": "sha512-eJEzOtVyenDs1TMzSQ3kU3K+E0GUS9sno+F0OBT97xsgcJsF9nXMBtkT9/kut5JEpM7oL7X/0qxR17K3mcwIAA==",
+ "dev": true
+ },
+ "is-string": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.5.tgz",
+ "integrity": "sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ==",
+ "dev": true
+ },
+ "is-symbol": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz",
+ "integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==",
+ "requires": {
+ "has-symbols": "^1.0.1"
+ }
+ },
+ "isarray": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
+ "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==",
+ "dev": true
+ },
+ "isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=",
+ "dev": true
+ },
+ "iterate-iterator": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/iterate-iterator/-/iterate-iterator-1.0.1.tgz",
+ "integrity": "sha512-3Q6tudGN05kbkDQDI4CqjaBf4qf85w6W6GnuZDtUVYwKgtC1q8yxYX7CZed7N+tLzQqS6roujWvszf13T+n9aw==",
+ "dev": true
+ },
+ "iterate-value": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/iterate-value/-/iterate-value-1.0.2.tgz",
+ "integrity": "sha512-A6fMAio4D2ot2r/TYzr4yUWrmwNdsN5xL7+HUiyACE4DXm+q8HtPcnFTp+NnW3k4N05tZ7FVYFFb2CR13NxyHQ==",
+ "dev": true,
+ "requires": {
+ "es-get-iterator": "^1.0.2",
+ "iterate-iterator": "^1.0.1"
+ }
+ },
+ "js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "dev": true
+ },
+ "js-yaml": {
+ "version": "3.13.1",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz",
+ "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==",
+ "dev": true,
+ "requires": {
+ "argparse": "^1.0.7",
+ "esprima": "^4.0.0"
+ }
+ },
+ "llparse": {
+ "version": "6.4.0",
+ "resolved": "https://registry.npmjs.org/llparse/-/llparse-6.4.0.tgz",
+ "integrity": "sha512-ySA+bj2wOLXrKmohAVMw0Nq84oHDPLdg+sUx4+VeSk1U72MEKfKAXS7zh82n15BRjWc/cVgWBN9RQAFdgk0g5Q==",
+ "dev": true,
+ "requires": {
+ "bitcode": "^1.2.0",
+ "debug": "^3.2.6",
+ "llparse-frontend": "^1.4.0"
+ },
+ "dependencies": {
+ "debug": {
+ "version": "3.2.6",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
+ "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
+ "dev": true,
+ "requires": {
+ "ms": "^2.1.1"
+ }
+ },
+ "llparse-frontend": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/llparse-frontend/-/llparse-frontend-1.4.0.tgz",
+ "integrity": "sha512-lUpGvGU9MDPb3k4Wbb0S7FgpceCirXVeFQQZjsYWB3fIEGU0Q6IEiTO91J6MLLN75gsxvGiWZaKVnmcHb7jh6g==",
+ "dev": true,
+ "requires": {
+ "debug": "^3.2.6",
+ "llparse-builder": "^1.3.2"
+ }
+ }
+ }
+ },
+ "llparse-builder": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/llparse-builder/-/llparse-builder-1.4.0.tgz",
+ "integrity": "sha512-mu0/zgAc1KdD6r+tjmRvF+YgoToQvBun4iXISRfSmx66b5qurckRpYjzBUYpHn0XVqKPRrGg86gMQKv8ogY3Rw==",
+ "dev": true,
+ "requires": {
+ "@types/debug": "0.0.30",
+ "binary-search": "^1.3.6",
+ "debug": "^3.2.6"
+ },
+ "dependencies": {
+ "@types/debug": {
+ "version": "0.0.30",
+ "resolved": "https://registry.npmjs.org/@types/debug/-/debug-0.0.30.tgz",
+ "integrity": "sha512-orGL5LXERPYsLov6CWs3Fh6203+dXzJkR7OnddIr2514Hsecwc8xRpzCapshBbKFImCsvS/mk6+FWiN5LyZJAQ==",
+ "dev": true
+ },
+ "debug": {
+ "version": "3.2.6",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
+ "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
+ "dev": true,
+ "requires": {
+ "ms": "^2.1.1"
+ }
+ }
+ }
+ },
+ "llparse-frontend": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/llparse-frontend/-/llparse-frontend-3.0.0.tgz",
+ "integrity": "sha512-G/o0Po2C+G5OtP8MJeQDjDf5qwDxcO7K6x4r6jqGsJwxk7yblbJnRqpmye7G/lZ8dD0Hv5neY4/KB5BhDmEc9Q==",
+ "requires": {
+ "debug": "^3.2.6",
+ "llparse-builder": "^1.5.2"
+ },
+ "dependencies": {
+ "@types/debug": {
+ "version": "4.1.5",
+ "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.5.tgz",
+ "integrity": "sha512-Q1y515GcOdTHgagaVFhHnIFQ38ygs/kmxdNpvpou+raI9UO3YZcHDngBSYKQklcKlvA7iuQlmIKbzvmxcOE9CQ=="
+ },
+ "debug": {
+ "version": "3.2.6",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
+ "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
+ "requires": {
+ "ms": "^2.1.1"
+ }
+ },
+ "llparse-builder": {
+ "version": "1.5.2",
+ "resolved": "https://registry.npmjs.org/llparse-builder/-/llparse-builder-1.5.2.tgz",
+ "integrity": "sha512-i862UNC3YUEdlfK/NUCJxlKjtWjgAI9AJXDRgjcfRHfwFt4Sf8eFPTRsc91/2R9MBZ0kyFdfhi8SVhMsZf1gNQ==",
+ "requires": {
+ "@types/debug": "4.1.5 ",
+ "binary-search": "^1.3.6",
+ "debug": "^4.2.0"
+ },
+ "dependencies": {
+ "debug": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz",
+ "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==",
+ "requires": {
+ "ms": "2.1.2"
+ }
+ }
+ }
+ }
+ }
+ },
+ "llparse-test-fixture": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/llparse-test-fixture/-/llparse-test-fixture-5.0.1.tgz",
+ "integrity": "sha512-BrnS70lxODcTXttLkfoSqn8DPbNuuSLFR48JnwxLimFkr8QRNBVbUku+bumIIo5Z7gAbIGNQXDOiSi2crMzS8Q==",
+ "dev": true,
+ "requires": {
+ "esm": "^3.2.25",
+ "llparse": "^6.4.0",
+ "yargs": "^15.4.1"
+ }
+ },
+ "locate-path": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+ "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+ "dev": true,
+ "requires": {
+ "p-locate": "^5.0.0"
+ }
+ },
+ "log-symbols": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.0.0.tgz",
+ "integrity": "sha512-FN8JBzLx6CzeMrB0tg6pqlGU1wCrXW+ZXGH481kfsBqer0hToTIiHdjH4Mq8xJUbvATujKCvaREGWpGUionraA==",
+ "dev": true,
+ "requires": {
+ "chalk": "^4.0.0"
+ },
+ "dependencies": {
+ "ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "requires": {
+ "color-convert": "^2.0.1"
+ }
+ },
+ "chalk": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
+ "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==",
+ "dev": true,
+ "requires": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ }
+ },
+ "color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "requires": {
+ "color-name": "~1.1.4"
+ }
+ },
+ "color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ }
+ }
+ },
+ "make-error": {
+ "version": "1.3.6",
+ "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
+ "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
+ "dev": true
+ },
+ "minimatch": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
+ "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
+ "dev": true,
+ "requires": {
+ "brace-expansion": "^1.1.7"
+ }
+ },
+ "minimist": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
+ "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
+ "dev": true
+ },
+ "mkdirp": {
+ "version": "0.5.5",
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
+ "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
+ "dev": true,
+ "requires": {
+ "minimist": "^1.2.5"
+ }
+ },
+ "mocha": {
+ "version": "8.1.3",
+ "resolved": "https://registry.npmjs.org/mocha/-/mocha-8.1.3.tgz",
+ "integrity": "sha512-ZbaYib4hT4PpF4bdSO2DohooKXIn4lDeiYqB+vTmCdr6l2woW0b6H3pf5x4sM5nwQMru9RvjjHYWVGltR50ZBw==",
+ "dev": true,
+ "requires": {
+ "ansi-colors": "4.1.1",
+ "browser-stdout": "1.3.1",
+ "chokidar": "3.4.2",
+ "debug": "4.1.1",
+ "diff": "4.0.2",
+ "escape-string-regexp": "4.0.0",
+ "find-up": "5.0.0",
+ "glob": "7.1.6",
+ "growl": "1.10.5",
+ "he": "1.2.0",
+ "js-yaml": "3.14.0",
+ "log-symbols": "4.0.0",
+ "minimatch": "3.0.4",
+ "ms": "2.1.2",
+ "object.assign": "4.1.0",
+ "promise.allsettled": "1.0.2",
+ "serialize-javascript": "4.0.0",
+ "strip-json-comments": "3.0.1",
+ "supports-color": "7.1.0",
+ "which": "2.0.2",
+ "wide-align": "1.1.3",
+ "workerpool": "6.0.0",
+ "yargs": "13.3.2",
+ "yargs-parser": "13.1.2",
+ "yargs-unparser": "1.6.1"
+ },
+ "dependencies": {
+ "ansi-regex": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
+ "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
+ "dev": true
+ },
+ "cliui": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz",
+ "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==",
+ "dev": true,
+ "requires": {
+ "string-width": "^3.1.0",
+ "strip-ansi": "^5.2.0",
+ "wrap-ansi": "^5.1.0"
+ }
+ },
+ "debug": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
+ "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
+ "dev": true,
+ "requires": {
+ "ms": "^2.1.1"
+ }
+ },
+ "escape-string-regexp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "dev": true
+ },
+ "glob": {
+ "version": "7.1.6",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
+ "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
+ "dev": true,
+ "requires": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.0.4",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ }
+ },
+ "js-yaml": {
+ "version": "3.14.0",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.0.tgz",
+ "integrity": "sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A==",
+ "dev": true,
+ "requires": {
+ "argparse": "^1.0.7",
+ "esprima": "^4.0.0"
+ }
+ },
+ "locate-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz",
+ "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==",
+ "dev": true,
+ "requires": {
+ "p-locate": "^3.0.0",
+ "path-exists": "^3.0.0"
+ }
+ },
+ "p-locate": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz",
+ "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==",
+ "dev": true,
+ "requires": {
+ "p-limit": "^2.0.0"
+ }
+ },
+ "path-exists": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz",
+ "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=",
+ "dev": true
+ },
+ "string-width": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
+ "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==",
+ "dev": true,
+ "requires": {
+ "emoji-regex": "^7.0.1",
+ "is-fullwidth-code-point": "^2.0.0",
+ "strip-ansi": "^5.1.0"
+ }
+ },
+ "strip-ansi": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
+ "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
+ "dev": true,
+ "requires": {
+ "ansi-regex": "^4.1.0"
+ }
+ },
+ "wrap-ansi": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz",
+ "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==",
+ "dev": true,
+ "requires": {
+ "ansi-styles": "^3.2.0",
+ "string-width": "^3.0.0",
+ "strip-ansi": "^5.0.0"
+ }
+ },
+ "yargs": {
+ "version": "13.3.2",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz",
+ "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==",
+ "dev": true,
+ "requires": {
+ "cliui": "^5.0.0",
+ "find-up": "^3.0.0",
+ "get-caller-file": "^2.0.1",
+ "require-directory": "^2.1.1",
+ "require-main-filename": "^2.0.0",
+ "set-blocking": "^2.0.0",
+ "string-width": "^3.0.0",
+ "which-module": "^2.0.0",
+ "y18n": "^4.0.0",
+ "yargs-parser": "^13.1.2"
+ },
+ "dependencies": {
+ "find-up": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz",
+ "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==",
+ "dev": true,
+ "requires": {
+ "locate-path": "^3.0.0"
+ }
+ }
+ }
+ },
+ "yargs-parser": {
+ "version": "13.1.2",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz",
+ "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==",
+ "dev": true,
+ "requires": {
+ "camelcase": "^5.0.0",
+ "decamelize": "^1.2.0"
+ }
+ }
+ }
+ },
+ "ms": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
+ },
+ "normalize-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+ "dev": true
+ },
+ "object-inspect": {
+ "version": "1.8.0",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.8.0.tgz",
+ "integrity": "sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA=="
+ },
+ "object-keys": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
+ "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="
+ },
+ "object.assign": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz",
+ "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==",
+ "dev": true,
+ "requires": {
+ "define-properties": "^1.1.2",
+ "function-bind": "^1.1.1",
+ "has-symbols": "^1.0.0",
+ "object-keys": "^1.0.11"
+ }
+ },
+ "once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
+ "dev": true,
+ "requires": {
+ "wrappy": "1"
+ }
+ },
+ "p-limit": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.0.tgz",
+ "integrity": "sha512-pZbTJpoUsCzV48Mc9Nh51VbwO0X9cuPFE8gYwx9BTCt9SF8/b7Zljd2fVgOxhIF/HDTKgpVzs+GPhyKfjLLFRQ==",
+ "dev": true,
+ "requires": {
+ "p-try": "^2.0.0"
+ }
+ },
+ "p-locate": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+ "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+ "dev": true,
+ "requires": {
+ "p-limit": "^3.0.2"
+ },
+ "dependencies": {
+ "p-limit": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.0.2.tgz",
+ "integrity": "sha512-iwqZSOoWIW+Ew4kAGUlN16J4M7OB3ysMLSZtnhmqx7njIHFPlxWBX8xo3lVTyFVq6mI/lL9qt2IsN1sHwaxJkg==",
+ "dev": true,
+ "requires": {
+ "p-try": "^2.0.0"
+ }
+ }
+ }
+ },
+ "p-try": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
+ "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
+ "dev": true
+ },
+ "path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "dev": true
+ },
+ "path-is-absolute": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+ "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
+ "dev": true
+ },
+ "path-parse": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz",
+ "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==",
+ "dev": true
+ },
+ "picomatch": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz",
+ "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==",
+ "dev": true
+ },
+ "promise.allsettled": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/promise.allsettled/-/promise.allsettled-1.0.2.tgz",
+ "integrity": "sha512-UpcYW5S1RaNKT6pd+s9jp9K9rlQge1UXKskec0j6Mmuq7UJCvlS2J2/s/yuPN8ehftf9HXMxWlKiPbGGUzpoRg==",
+ "dev": true,
+ "requires": {
+ "array.prototype.map": "^1.0.1",
+ "define-properties": "^1.1.3",
+ "es-abstract": "^1.17.0-next.1",
+ "function-bind": "^1.1.1",
+ "iterate-value": "^1.0.0"
+ }
+ },
+ "randombytes": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
+ "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
+ "dev": true,
+ "requires": {
+ "safe-buffer": "^5.1.0"
+ }
+ },
+ "readdirp": {
+ "version": "3.4.0",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.4.0.tgz",
+ "integrity": "sha512-0xe001vZBnJEK+uKcj8qOhyAKPzIT+gStxWr3LCB0DwcXR5NZJ3IaC+yGnHCYzB/S7ov3m3EEbZI2zeNvX+hGQ==",
+ "dev": true,
+ "requires": {
+ "picomatch": "^2.2.1"
+ }
+ },
+ "require-directory": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
+ "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=",
+ "dev": true
+ },
+ "require-main-filename": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
+ "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
+ "dev": true
+ },
+ "resolve": {
+ "version": "1.17.0",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz",
+ "integrity": "sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==",
+ "dev": true,
+ "requires": {
+ "path-parse": "^1.0.6"
+ }
+ },
+ "safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "dev": true
+ },
+ "semver": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
+ "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
+ "dev": true
+ },
+ "serialize-javascript": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz",
+ "integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==",
+ "dev": true,
+ "requires": {
+ "randombytes": "^2.1.0"
+ }
+ },
+ "set-blocking": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
+ "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=",
+ "dev": true
+ },
+ "source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "dev": true
+ },
+ "source-map-support": {
+ "version": "0.5.19",
+ "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz",
+ "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==",
+ "dev": true,
+ "requires": {
+ "buffer-from": "^1.0.0",
+ "source-map": "^0.6.0"
+ }
+ },
+ "sprintf-js": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
+ "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=",
+ "dev": true
+ },
+ "string-width": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz",
+ "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==",
+ "dev": true,
+ "requires": {
+ "is-fullwidth-code-point": "^2.0.0",
+ "strip-ansi": "^4.0.0"
+ }
+ },
+ "string.prototype.trimend": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz",
+ "integrity": "sha512-LRPxFUaTtpqYsTeNKaFOw3R4bxIzWOnbQ837QfBylo8jIxtcbK/A/sMV7Q+OAV/vWo+7s25pOE10KYSjaSO06g==",
+ "requires": {
+ "define-properties": "^1.1.3",
+ "es-abstract": "^1.17.5"
+ }
+ },
+ "string.prototype.trimstart": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz",
+ "integrity": "sha512-XxZn+QpvrBI1FOcg6dIpxUPgWCPuNXvMD72aaRaUQv1eD4e/Qy8i/hFTe0BUmD60p/QA6bh1avmuPTfNjqVWRw==",
+ "requires": {
+ "define-properties": "^1.1.3",
+ "es-abstract": "^1.17.5"
+ }
+ },
+ "strip-ansi": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
+ "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
+ "dev": true,
+ "requires": {
+ "ansi-regex": "^3.0.0"
+ }
+ },
+ "strip-json-comments": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.0.1.tgz",
+ "integrity": "sha512-VTyMAUfdm047mwKl+u79WIdrZxtFtn+nBxHeb844XBQ9uMNTuTHdx2hc5RiAJYqwTj3wc/xe5HLSdJSkJ+WfZw==",
+ "dev": true
+ },
+ "supports-color": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz",
+ "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==",
+ "dev": true,
+ "requires": {
+ "has-flag": "^4.0.0"
+ },
+ "dependencies": {
+ "has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true
+ }
+ }
+ },
+ "to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "dev": true,
+ "requires": {
+ "is-number": "^7.0.0"
+ }
+ },
+ "ts-node": {
+ "version": "9.0.0",
+ "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-9.0.0.tgz",
+ "integrity": "sha512-/TqB4SnererCDR/vb4S/QvSZvzQMJN8daAslg7MeaiHvD8rDZsSfXmNeNumyZZzMned72Xoq/isQljYSt8Ynfg==",
+ "dev": true,
+ "requires": {
+ "arg": "^4.1.0",
+ "diff": "^4.0.1",
+ "make-error": "^1.1.1",
+ "source-map-support": "^0.5.17",
+ "yn": "3.1.1"
+ }
+ },
+ "tslib": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
+ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
+ "dev": true
+ },
+ "tslint": {
+ "version": "6.1.3",
+ "resolved": "https://registry.npmjs.org/tslint/-/tslint-6.1.3.tgz",
+ "integrity": "sha512-IbR4nkT96EQOvKE2PW/djGz8iGNeJ4rF2mBfiYaR/nvUWYKJhLwimoJKgjIFEIDibBtOevj7BqCRL4oHeWWUCg==",
+ "dev": true,
+ "requires": {
+ "@babel/code-frame": "^7.0.0",
+ "builtin-modules": "^1.1.1",
+ "chalk": "^2.3.0",
+ "commander": "^2.12.1",
+ "diff": "^4.0.1",
+ "glob": "^7.1.1",
+ "js-yaml": "^3.13.1",
+ "minimatch": "^3.0.4",
+ "mkdirp": "^0.5.3",
+ "resolve": "^1.3.2",
+ "semver": "^5.3.0",
+ "tslib": "^1.13.0",
+ "tsutils": "^2.29.0"
+ },
+ "dependencies": {
+ "diff": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
+ "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
+ "dev": true
+ }
+ }
+ },
+ "tsutils": {
+ "version": "2.29.0",
+ "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.29.0.tgz",
+ "integrity": "sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA==",
+ "dev": true,
+ "requires": {
+ "tslib": "^1.8.1"
+ }
+ },
+ "typescript": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.0.3.tgz",
+ "integrity": "sha512-tEu6DGxGgRJPb/mVPIZ48e69xCn2yRmCgYmDugAVwmJ6o+0u1RI18eO7E7WBTLYLaEVVOhwQmcdhQHweux/WPg==",
+ "dev": true
+ },
+ "which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dev": true,
+ "requires": {
+ "isexe": "^2.0.0"
+ }
+ },
+ "which-module": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz",
+ "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=",
+ "dev": true
+ },
+ "wide-align": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz",
+ "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==",
+ "dev": true,
+ "requires": {
+ "string-width": "^1.0.2 || 2"
+ }
+ },
+ "workerpool": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.0.0.tgz",
+ "integrity": "sha512-fU2OcNA/GVAJLLyKUoHkAgIhKb0JoCpSjLC/G2vYKxUjVmQwGbRVeoPJ1a8U4pnVofz4AQV5Y/NEw8oKqxEBtA==",
+ "dev": true
+ },
+ "wrap-ansi": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
+ "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
+ "dev": true,
+ "requires": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "dependencies": {
+ "ansi-regex": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
+ "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==",
+ "dev": true
+ },
+ "ansi-styles": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz",
+ "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==",
+ "dev": true,
+ "requires": {
+ "@types/color-name": "^1.1.1",
+ "color-convert": "^2.0.1"
+ }
+ },
+ "color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "requires": {
+ "color-name": "~1.1.4"
+ }
+ },
+ "color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
+ "emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true
+ },
+ "is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "dev": true
+ },
+ "string-width": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz",
+ "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==",
+ "dev": true,
+ "requires": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.0"
+ }
+ },
+ "strip-ansi": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
+ "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==",
+ "dev": true,
+ "requires": {
+ "ansi-regex": "^5.0.0"
+ }
+ }
+ }
+ },
+ "wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
+ "dev": true
+ },
+ "y18n": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.1.tgz",
+ "integrity": "sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==",
+ "dev": true
+ },
+ "yargs": {
+ "version": "15.4.1",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
+ "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
+ "dev": true,
+ "requires": {
+ "cliui": "^6.0.0",
+ "decamelize": "^1.2.0",
+ "find-up": "^4.1.0",
+ "get-caller-file": "^2.0.1",
+ "require-directory": "^2.1.1",
+ "require-main-filename": "^2.0.0",
+ "set-blocking": "^2.0.0",
+ "string-width": "^4.2.0",
+ "which-module": "^2.0.0",
+ "y18n": "^4.0.0",
+ "yargs-parser": "^18.1.2"
+ },
+ "dependencies": {
+ "ansi-regex": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
+ "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==",
+ "dev": true
+ },
+ "emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true
+ },
+ "find-up": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
+ "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
+ "dev": true,
+ "requires": {
+ "locate-path": "^5.0.0",
+ "path-exists": "^4.0.0"
+ }
+ },
+ "is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "dev": true
+ },
+ "locate-path": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
+ "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
+ "dev": true,
+ "requires": {
+ "p-locate": "^4.1.0"
+ }
+ },
+ "p-locate": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
+ "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
+ "dev": true,
+ "requires": {
+ "p-limit": "^2.2.0"
+ }
+ },
+ "path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "dev": true
+ },
+ "string-width": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz",
+ "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==",
+ "dev": true,
+ "requires": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.0"
+ }
+ },
+ "strip-ansi": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
+ "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==",
+ "dev": true,
+ "requires": {
+ "ansi-regex": "^5.0.0"
+ }
+ }
+ }
+ },
+ "yargs-parser": {
+ "version": "18.1.3",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
+ "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
+ "dev": true,
+ "requires": {
+ "camelcase": "^5.0.0",
+ "decamelize": "^1.2.0"
+ }
+ },
+ "yargs-unparser": {
+ "version": "1.6.1",
+ "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-1.6.1.tgz",
+ "integrity": "sha512-qZV14lK9MWsGCmcr7u5oXGH0dbGqZAIxTDrWXZDo5zUr6b6iUmelNKO6x6R1dQT24AH3LgRxJpr8meWy2unolA==",
+ "dev": true,
+ "requires": {
+ "camelcase": "^5.3.1",
+ "decamelize": "^1.2.0",
+ "flat": "^4.1.0",
+ "is-plain-obj": "^1.1.0",
+ "yargs": "^14.2.3"
+ },
+ "dependencies": {
+ "ansi-regex": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
+ "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
+ "dev": true
+ },
+ "cliui": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz",
+ "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==",
+ "dev": true,
+ "requires": {
+ "string-width": "^3.1.0",
+ "strip-ansi": "^5.2.0",
+ "wrap-ansi": "^5.1.0"
+ }
+ },
+ "find-up": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz",
+ "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==",
+ "dev": true,
+ "requires": {
+ "locate-path": "^3.0.0"
+ }
+ },
+ "locate-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz",
+ "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==",
+ "dev": true,
+ "requires": {
+ "p-locate": "^3.0.0",
+ "path-exists": "^3.0.0"
+ }
+ },
+ "p-locate": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz",
+ "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==",
+ "dev": true,
+ "requires": {
+ "p-limit": "^2.0.0"
+ }
+ },
+ "path-exists": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz",
+ "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=",
+ "dev": true
+ },
+ "string-width": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
+ "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==",
+ "dev": true,
+ "requires": {
+ "emoji-regex": "^7.0.1",
+ "is-fullwidth-code-point": "^2.0.0",
+ "strip-ansi": "^5.1.0"
+ }
+ },
+ "strip-ansi": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
+ "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
+ "dev": true,
+ "requires": {
+ "ansi-regex": "^4.1.0"
+ }
+ },
+ "wrap-ansi": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz",
+ "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==",
+ "dev": true,
+ "requires": {
+ "ansi-styles": "^3.2.0",
+ "string-width": "^3.0.0",
+ "strip-ansi": "^5.0.0"
+ }
+ },
+ "yargs": {
+ "version": "14.2.3",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-14.2.3.tgz",
+ "integrity": "sha512-ZbotRWhF+lkjijC/VhmOT9wSgyBQ7+zr13+YLkhfsSiTriYsMzkTUFP18pFhWwBeMa5gUc1MzbhrO6/VB7c9Xg==",
+ "dev": true,
+ "requires": {
+ "cliui": "^5.0.0",
+ "decamelize": "^1.2.0",
+ "find-up": "^3.0.0",
+ "get-caller-file": "^2.0.1",
+ "require-directory": "^2.1.1",
+ "require-main-filename": "^2.0.0",
+ "set-blocking": "^2.0.0",
+ "string-width": "^3.0.0",
+ "which-module": "^2.0.0",
+ "y18n": "^4.0.0",
+ "yargs-parser": "^15.0.1"
+ }
+ },
+ "yargs-parser": {
+ "version": "15.0.1",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-15.0.1.tgz",
+ "integrity": "sha512-0OAMV2mAZQrs3FkNpDQcBk1x5HXb8X4twADss4S0Iuk+2dGnLOE/fRHrsYm542GduMveyA77OF4wrNJuanRCWw==",
+ "dev": true,
+ "requires": {
+ "camelcase": "^5.0.0",
+ "decamelize": "^1.2.0"
+ }
+ }
+ }
+ },
+ "yn": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
+ "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
+ "dev": true
+ }
+ }
+}
diff --git a/llparse/package.json b/llparse/package.json
new file mode 100644
index 0000000..ee35dc4
--- /dev/null
+++ b/llparse/package.json
@@ -0,0 +1,49 @@
+{
+ "name": "llparse",
+ "version": "7.1.1",
+ "description": "Compile incremental parsers to C code",
+ "main": "lib/api.js",
+ "types": "lib/api.d.ts",
+ "files": [
+ "lib",
+ "src"
+ ],
+ "scripts": {
+ "build": "tsc",
+ "clean": "rm -rf lib",
+ "prepare": "npm run clean && npm run build",
+ "lint": "tslint -c tslint.json src/**/*.ts test/**/*.ts",
+ "fix-lint": "npm run lint -- --fix",
+ "mocha": "mocha --timeout=10000 -r ts-node/register/type-check --reporter spec test/*-test.ts",
+ "test": "npm run mocha && npm run lint"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+ssh://git@github.com/nodejs/llparse.git"
+ },
+ "keywords": [
+ "llparse",
+ "compiler"
+ ],
+ "author": "Fedor Indutny <fedor@indutny.com> (http://darksi.de/)",
+ "license": "MIT",
+ "bugs": {
+ "url": "https://github.com/nodejs/llparse/issues"
+ },
+ "homepage": "https://github.com/nodejs/llparse#readme",
+ "devDependencies": {
+ "@types/debug": "^4.1.5",
+ "@types/mocha": "^8.0.3",
+ "@types/node": "^14.11.8",
+ "esm": "^3.2.25",
+ "llparse-test-fixture": "^5.0.1",
+ "mocha": "^8.1.3",
+ "ts-node": "^9.0.0",
+ "tslint": "^6.1.3",
+ "typescript": "^4.0.3"
+ },
+ "dependencies": {
+ "debug": "^4.2.0",
+ "llparse-frontend": "^3.0.0"
+ }
+}
diff --git a/llparse/src/api.ts b/llparse/src/api.ts
new file mode 100644
index 0000000..a34f5bc
--- /dev/null
+++ b/llparse/src/api.ts
@@ -0,0 +1,47 @@
+import * as frontend from 'llparse-frontend';
+
+import source = frontend.source;
+
+import { Compiler, ICompilerOptions, ICompilerResult } from './compiler';
+
+export { source, ICompilerOptions, ICompilerResult };
+
+// TODO(indutny): API for disabling/short-circuiting spans
+
+/**
+ * LLParse graph builder and compiler.
+ */
+export class LLParse extends source.Builder {
+ /**
+ * The prefix controls the names of methods and state struct in generated
+ * public C headers:
+ *
+ * ```c
+ * // state struct
+ * struct PREFIX_t {
+ * ...
+ * }
+ *
+ * int PREFIX_init(PREFIX_t* state);
+ * int PREFIX_execute(PREFIX_t* state, const char* p, const char* endp);
+ * ```
+ *
+ * @param prefix Prefix to be used when generating public API.
+ */
+ constructor(private readonly prefix: string = 'llparse') {
+ super();
+ }
+
+ /**
+ * Compile LLParse graph to the C code and C headers
+ *
+ * @param root Root node of the parse graph (see `.node()`)
+ * @param options Compiler options.
+ */
+ public build(root: source.node.Node, options: ICompilerOptions = {})
+ : ICompilerResult {
+ const c = new Compiler(this.prefix, options);
+
+ return c.compile(root, this.properties);
+ }
+}
diff --git a/llparse/src/compiler/header-builder.ts b/llparse/src/compiler/header-builder.ts
new file mode 100644
index 0000000..9f5bee7
--- /dev/null
+++ b/llparse/src/compiler/header-builder.ts
@@ -0,0 +1,80 @@
+import * as frontend from 'llparse-frontend';
+import source = frontend.source;
+
+export interface IHeaderBuilderOptions {
+ readonly prefix: string;
+ readonly headerGuard?: string;
+ readonly properties: ReadonlyArray<source.Property>;
+ readonly spans: ReadonlyArray<frontend.SpanField>;
+}
+
+export class HeaderBuilder {
+ public build(options: IHeaderBuilderOptions): string {
+ let res = '';
+ const PREFIX = options.prefix.toUpperCase().replace(/[^a-z]/gi, '_');
+ const DEFINE = options.headerGuard === undefined ?
+ `INCLUDE_${PREFIX}_H_` : options.headerGuard;
+
+ res += `#ifndef ${DEFINE}\n`;
+ res += `#define ${DEFINE}\n`;
+ res += '#ifdef __cplusplus\n';
+ res += 'extern "C" {\n';
+ res += '#endif\n';
+ res += '\n';
+
+ res += '#include <stdint.h>\n';
+ res += '\n';
+
+ // Structure
+ res += `typedef struct ${options.prefix}_s ${options.prefix}_t;\n`;
+ res += `struct ${options.prefix}_s {\n`;
+ res += ' int32_t _index;\n';
+
+ for (const [ index, field ] of options.spans.entries()) {
+ res += ` void* _span_pos${index};\n`;
+ if (field.callbacks.length > 1) {
+ res += ` void* _span_cb${index};\n`;
+ }
+ }
+
+ res += ' int32_t error;\n';
+ res += ' const char* reason;\n';
+ res += ' const char* error_pos;\n';
+ res += ' void* data;\n';
+ res += ' void* _current;\n';
+
+ for (const prop of options.properties) {
+ let ty: string;
+ if (prop.ty === 'i8') {
+ ty = 'uint8_t';
+ } else if (prop.ty === 'i16') {
+ ty = 'uint16_t';
+ } else if (prop.ty === 'i32') {
+ ty = 'uint32_t';
+ } else if (prop.ty === 'i64') {
+ ty = 'uint64_t';
+ } else if (prop.ty === 'ptr') {
+ ty = 'void*';
+ } else {
+ throw new Error(
+ `Unknown state property type: "${prop.ty}"`);
+ }
+ res += ` ${ty} ${prop.name};\n`;
+ }
+ res += '};\n';
+
+ res += '\n';
+
+ res += `int ${options.prefix}_init(${options.prefix}_t* s);\n`;
+ res += `int ${options.prefix}_execute(${options.prefix}_t* s, ` +
+ 'const char* p, const char* endp);\n';
+
+ res += '\n';
+ res += '#ifdef __cplusplus\n';
+ res += '} /* extern "C" *\/\n';
+ res += '#endif\n';
+ res += `#endif /* ${DEFINE} *\/\n`;
+
+ return res;
+ }
+}
diff --git a/llparse/src/compiler/index.ts b/llparse/src/compiler/index.ts
new file mode 100644
index 0000000..89c258a
--- /dev/null
+++ b/llparse/src/compiler/index.ts
@@ -0,0 +1,88 @@
+import * as debugAPI from 'debug';
+import * as frontend from 'llparse-frontend';
+
+import source = frontend.source;
+
+import * as cImpl from '../implementation/c';
+import { HeaderBuilder } from './header-builder';
+
+const debug = debugAPI('llparse:compiler');
+
+export interface ICompilerOptions {
+ /**
+ * Debug method name
+ *
+ * The method must have following signature:
+ *
+ * ```c
+ * void debug(llparse_t* state, const char* p, const char* endp,
+ * const char* msg);
+ * ```
+ *
+ * Where `llparse_t` is a parser state type.
+ */
+ readonly debug?: string;
+
+ /**
+ * What guard define to use in `#ifndef` in C headers.
+ *
+ * Default value: `prefix` argument
+ */
+ readonly headerGuard?: string;
+
+ /** Optional frontend configuration */
+ readonly frontend?: frontend.IFrontendLazyOptions;
+
+ /** Optional C-backend configuration */
+ readonly c?: cImpl.ICPublicOptions;
+}
+
+export interface ICompilerResult {
+ /**
+ * Textual C code
+ */
+ readonly c: string;
+
+ /**
+ * Textual C header file
+ */
+ readonly header: string;
+}
+
+export class Compiler {
+ constructor(public readonly prefix: string,
+ public readonly options: ICompilerOptions) {
+ }
+
+ public compile(root: source.node.Node,
+ properties: ReadonlyArray<source.Property>): ICompilerResult {
+ debug('Combining implementations');
+ const container = new frontend.Container();
+
+ const c = new cImpl.CCompiler(container, Object.assign({
+ debug: this.options.debug,
+ }, this.options.c));
+
+ debug('Running frontend pass');
+ const f = new frontend.Frontend(this.prefix,
+ container.build(),
+ this.options.frontend);
+ const info = f.compile(root, properties);
+
+ debug('Building header');
+ const hb = new HeaderBuilder();
+
+ const header = hb.build({
+ headerGuard: this.options.headerGuard,
+ prefix: this.prefix,
+ properties,
+ spans: info.spans,
+ });
+
+ debug('Building C');
+ return {
+ header,
+ c: c.compile(info),
+ };
+ }
+}
diff --git a/llparse/src/implementation/c/code/and.ts b/llparse/src/implementation/c/code/and.ts
new file mode 100644
index 0000000..fdd5434
--- /dev/null
+++ b/llparse/src/implementation/c/code/and.ts
@@ -0,0 +1,11 @@
+import * as frontend from 'llparse-frontend';
+
+import { Compilation } from '../compilation';
+import { Field } from './field';
+
+export class And extends Field<frontend.code.And> {
+ protected doBuild(ctx: Compilation, out: string[]): void {
+ out.push(`${this.field(ctx)} &= ${this.ref.value};`);
+ out.push('return 0;');
+ }
+}
diff --git a/llparse/src/implementation/c/code/base.ts b/llparse/src/implementation/c/code/base.ts
new file mode 100644
index 0000000..888330d
--- /dev/null
+++ b/llparse/src/implementation/c/code/base.ts
@@ -0,0 +1,12 @@
+import * as frontend from 'llparse-frontend';
+
+import { Compilation } from '../compilation';
+
+export abstract class Code<T extends frontend.code.Code> {
+ protected cachedDecl: string | undefined;
+
+ constructor(public readonly ref: T) {
+ }
+
+ public abstract build(ctx: Compilation, out: string[]): void;
+}
diff --git a/llparse/src/implementation/c/code/external.ts b/llparse/src/implementation/c/code/external.ts
new file mode 100644
index 0000000..494fc5a
--- /dev/null
+++ b/llparse/src/implementation/c/code/external.ts
@@ -0,0 +1,19 @@
+import * as frontend from 'llparse-frontend';
+
+import { Compilation } from '../compilation';
+import { Code } from './base';
+
+export abstract class External<T extends frontend.code.External>
+ extends Code<T> {
+
+ public build(ctx: Compilation, out: string[]): void {
+ out.push(`int ${this.ref.name}(`);
+ out.push(` ${ctx.prefix}_t* s, const unsigned char* p,`);
+ if (this.ref.signature === 'value') {
+ out.push(' const unsigned char* endp,');
+ out.push(' int value);');
+ } else {
+ out.push(' const unsigned char* endp);');
+ }
+ }
+}
diff --git a/llparse/src/implementation/c/code/field.ts b/llparse/src/implementation/c/code/field.ts
new file mode 100644
index 0000000..51f4439
--- /dev/null
+++ b/llparse/src/implementation/c/code/field.ts
@@ -0,0 +1,28 @@
+import * as frontend from 'llparse-frontend';
+
+import { Compilation } from '../compilation';
+import { Code } from './base';
+
+export abstract class Field<T extends frontend.code.Field> extends Code<T> {
+ public build(ctx: Compilation, out: string[]): void {
+ out.push(`int ${this.ref.name}(`);
+ out.push(` ${ctx.prefix}_t* ${ctx.stateArg()},`);
+ out.push(` const unsigned char* ${ctx.posArg()},`);
+ if (this.ref.signature === 'value') {
+ out.push(` const unsigned char* ${ctx.endPosArg()},`);
+ out.push(` int ${ctx.matchVar()}) {`);
+ } else {
+ out.push(` const unsigned char* ${ctx.endPosArg()}) {`);
+ }
+ const tmp: string[] = [];
+ this.doBuild(ctx, tmp);
+ ctx.indent(out, tmp, ' ');
+ out.push('}');
+ }
+
+ protected abstract doBuild(ctx: Compilation, out: string[]): void;
+
+ protected field(ctx: Compilation): string {
+ return `${ctx.stateArg()}->${this.ref.field}`;
+ }
+}
diff --git a/llparse/src/implementation/c/code/index.ts b/llparse/src/implementation/c/code/index.ts
new file mode 100644
index 0000000..0de5de5
--- /dev/null
+++ b/llparse/src/implementation/c/code/index.ts
@@ -0,0 +1,27 @@
+import * as frontend from 'llparse-frontend';
+
+import { And } from './and';
+import { External } from './external';
+import { IsEqual } from './is-equal';
+import { Load } from './load';
+import { MulAdd } from './mul-add';
+import { Or } from './or';
+import { Store } from './store';
+import { Test } from './test';
+import { Update } from './update';
+
+export * from './base';
+
+export default {
+ And,
+ IsEqual,
+ Load,
+ Match: class Match extends External<frontend.code.External> {},
+ MulAdd,
+ Or,
+ Span: class Span extends External<frontend.code.Span> {},
+ Store,
+ Test,
+ Update,
+ Value: class Value extends External<frontend.code.Value> {},
+};
diff --git a/llparse/src/implementation/c/code/is-equal.ts b/llparse/src/implementation/c/code/is-equal.ts
new file mode 100644
index 0000000..f76c2c1
--- /dev/null
+++ b/llparse/src/implementation/c/code/is-equal.ts
@@ -0,0 +1,10 @@
+import * as frontend from 'llparse-frontend';
+
+import { Compilation } from '../compilation';
+import { Field } from './field';
+
+export class IsEqual extends Field<frontend.code.IsEqual> {
+ protected doBuild(ctx: Compilation, out: string[]): void {
+ out.push(`return ${this.field(ctx)} == ${this.ref.value};`);
+ }
+}
diff --git a/llparse/src/implementation/c/code/load.ts b/llparse/src/implementation/c/code/load.ts
new file mode 100644
index 0000000..b913f23
--- /dev/null
+++ b/llparse/src/implementation/c/code/load.ts
@@ -0,0 +1,10 @@
+import * as frontend from 'llparse-frontend';
+
+import { Compilation } from '../compilation';
+import { Field } from './field';
+
+export class Load extends Field<frontend.code.Load> {
+ protected doBuild(ctx: Compilation, out: string[]): void {
+ out.push(`return ${this.field(ctx)};`);
+ }
+}
diff --git a/llparse/src/implementation/c/code/mul-add.ts b/llparse/src/implementation/c/code/mul-add.ts
new file mode 100644
index 0000000..fd5ce8c
--- /dev/null
+++ b/llparse/src/implementation/c/code/mul-add.ts
@@ -0,0 +1,67 @@
+import * as assert from 'assert';
+import * as frontend from 'llparse-frontend';
+
+import { Compilation } from '../compilation';
+import { SIGNED_LIMITS, UNSIGNED_LIMITS, SIGNED_TYPES } from '../constants';
+import { Field } from './field';
+
+export class MulAdd extends Field<frontend.code.MulAdd> {
+ protected doBuild(ctx: Compilation, out: string[]): void {
+ const options = this.ref.options;
+ const ty = ctx.getFieldType(this.ref.field);
+
+ let field = this.field(ctx);
+ if (options.signed) {
+ assert(SIGNED_TYPES.has(ty), `Unexpected mulAdd type "${ty}"`);
+ const targetTy = SIGNED_TYPES.get(ty)!;
+ out.push(`${targetTy}* field = (${targetTy}*) &${field};`);
+ field = '(*field)';
+ }
+
+ const match = ctx.matchVar();
+
+ const limits = options.signed ? SIGNED_LIMITS : UNSIGNED_LIMITS;
+ assert(limits.has(ty), `Unexpected mulAdd type "${ty}"`);
+ const [ min, max ] = limits.get(ty)!;
+
+ const mulMax = `${max} / ${options.base}`;
+ const mulMin = `${min} / ${options.base}`;
+
+ out.push('/* Multiplication overflow */');
+ out.push(`if (${field} > ${mulMax}) {`);
+ out.push(' return 1;');
+ out.push('}');
+ if (options.signed) {
+ out.push(`if (${field} < ${mulMin}) {`);
+ out.push(' return 1;');
+ out.push('}');
+ }
+ out.push('');
+
+ out.push(`${field} *= ${options.base};`);
+ out.push('');
+
+ out.push('/* Addition overflow */');
+ out.push(`if (${match} >= 0) {`);
+ out.push(` if (${field} > ${max} - ${match}) {`);
+ out.push(' return 1;');
+ out.push(' }');
+ out.push('} else {');
+ out.push(` if (${field} < ${min} - ${match}) {`);
+ out.push(' return 1;');
+ out.push(' }');
+ out.push('}');
+
+ out.push(`${field} += ${match};`);
+
+ if (options.max !== undefined) {
+ out.push('');
+ out.push('/* Enforce maximum */');
+ out.push(`if (${field} > ${options.max}) {`);
+ out.push(' return 1;');
+ out.push('}');
+ }
+
+ out.push('return 0;');
+ }
+}
diff --git a/llparse/src/implementation/c/code/or.ts b/llparse/src/implementation/c/code/or.ts
new file mode 100644
index 0000000..76b16f9
--- /dev/null
+++ b/llparse/src/implementation/c/code/or.ts
@@ -0,0 +1,11 @@
+import * as frontend from 'llparse-frontend';
+
+import { Compilation } from '../compilation';
+import { Field } from './field';
+
+export class Or extends Field<frontend.code.Or> {
+ protected doBuild(ctx: Compilation, out: string[]): void {
+ out.push(`${this.field(ctx)} |= ${this.ref.value};`);
+ out.push('return 0;');
+ }
+}
diff --git a/llparse/src/implementation/c/code/store.ts b/llparse/src/implementation/c/code/store.ts
new file mode 100644
index 0000000..a37d963
--- /dev/null
+++ b/llparse/src/implementation/c/code/store.ts
@@ -0,0 +1,11 @@
+import * as frontend from 'llparse-frontend';
+
+import { Compilation } from '../compilation';
+import { Field } from './field';
+
+export class Store extends Field<frontend.code.Store> {
+ protected doBuild(ctx: Compilation, out: string[]): void {
+ out.push(`${this.field(ctx)} = ${ctx.matchVar()};`);
+ out.push('return 0;');
+ }
+}
diff --git a/llparse/src/implementation/c/code/test.ts b/llparse/src/implementation/c/code/test.ts
new file mode 100644
index 0000000..36126f5
--- /dev/null
+++ b/llparse/src/implementation/c/code/test.ts
@@ -0,0 +1,11 @@
+import * as frontend from 'llparse-frontend';
+
+import { Compilation } from '../compilation';
+import { Field } from './field';
+
+export class Test extends Field<frontend.code.Test> {
+ protected doBuild(ctx: Compilation, out: string[]): void {
+ const value = this.ref.value;
+ out.push(`return (${this.field(ctx)} & ${value}) == ${value};`);
+ }
+}
diff --git a/llparse/src/implementation/c/code/update.ts b/llparse/src/implementation/c/code/update.ts
new file mode 100644
index 0000000..89efedf
--- /dev/null
+++ b/llparse/src/implementation/c/code/update.ts
@@ -0,0 +1,11 @@
+import * as frontend from 'llparse-frontend';
+
+import { Compilation } from '../compilation';
+import { Field } from './field';
+
+export class Update extends Field<frontend.code.Update> {
+ protected doBuild(ctx: Compilation, out: string[]): void {
+ out.push(`${this.field(ctx)} = ${this.ref.value};`);
+ out.push('return 0;');
+ }
+}
diff --git a/llparse/src/implementation/c/compilation.ts b/llparse/src/implementation/c/compilation.ts
new file mode 100644
index 0000000..4df05a6
--- /dev/null
+++ b/llparse/src/implementation/c/compilation.ts
@@ -0,0 +1,336 @@
+import * as assert from 'assert';
+import { Buffer } from 'buffer';
+import * as frontend from 'llparse-frontend';
+
+import {
+ CONTAINER_KEY, STATE_ERROR,
+ ARG_STATE, ARG_POS, ARG_ENDPOS,
+ VAR_MATCH,
+ STATE_PREFIX, LABEL_PREFIX, BLOB_PREFIX,
+ SEQUENCE_COMPLETE, SEQUENCE_MISMATCH, SEQUENCE_PAUSE,
+} from './constants';
+import { Code } from './code';
+import { Node } from './node';
+import { Transform } from './transform';
+import { MatchSequence } from './helpers/match-sequence';
+
+// Number of hex words per line of blob declaration
+const BLOB_GROUP_SIZE = 11;
+
+type WrappedNode = frontend.IWrap<frontend.node.Node>;
+
+interface IBlob {
+ readonly alignment: number | undefined;
+ readonly buffer: Buffer;
+ readonly name: string;
+}
+
+// TODO(indutny): deduplicate
+export interface ICompilationOptions {
+ readonly debug?: string;
+}
+
+// TODO(indutny): deduplicate
+export interface ICompilationProperty {
+ readonly name: string;
+ readonly ty: string;
+}
+
+export class Compilation {
+ private readonly stateMap: Map<string, ReadonlyArray<string>> = new Map();
+ private readonly blobs: Map<Buffer, IBlob> = new Map();
+ private readonly codeMap: Map<string, Code<frontend.code.Code>> = new Map();
+ private readonly matchSequence:
+ Map<string, MatchSequence> = new Map();
+ private readonly resumptionTargets: Set<string> = new Set();
+
+ constructor(public readonly prefix: string,
+ private readonly properties: ReadonlyArray<ICompilationProperty>,
+ resumptionTargets: ReadonlySet<WrappedNode>,
+ private readonly options: ICompilationOptions) {
+ for (const node of resumptionTargets) {
+ this.resumptionTargets.add(STATE_PREFIX + node.ref.id.name);
+ }
+ }
+
+ private buildStateEnum(out: string[]): void {
+ out.push('enum llparse_state_e {');
+ out.push(` ${STATE_ERROR},`);
+ for (const stateName of this.stateMap.keys()) {
+ if (this.resumptionTargets.has(stateName)) {
+ out.push(` ${stateName},`);
+ }
+ }
+ out.push('};');
+ out.push('typedef enum llparse_state_e llparse_state_t;');
+ }
+
+ private buildBlobs(out: string[]): void {
+ if (this.blobs.size === 0) {
+ return;
+ }
+
+ for (const blob of this.blobs.values()) {
+ const buffer = blob.buffer;
+ let align = '';
+ if (blob.alignment) {
+ align = ` ALIGN(${blob.alignment})`;
+ }
+
+ if (blob.alignment) {
+ out.push('#ifdef __SSE4_2__');
+ }
+ out.push(`static const unsigned char${align} ${blob.name}[] = {`);
+
+ for (let i = 0; i < buffer.length; i += BLOB_GROUP_SIZE) {
+ const limit = Math.min(buffer.length, i + BLOB_GROUP_SIZE);
+ const hex: string[] = [];
+ for (let j = i; j < limit; j++) {
+ const value = buffer[j] as number;
+
+ const ch = String.fromCharCode(value);
+ // `'`, `\`
+ if (value === 0x27 || value === 0x5c) {
+ hex.push(`'\\${ch}'`);
+ } else if (value >= 0x20 && value <= 0x7e) {
+ hex.push(`'${ch}'`);
+ } else {
+ hex.push(`0x${value.toString(16)}`);
+ }
+ }
+ let line = ' ' + hex.join(', ');
+ if (limit !== buffer.length) {
+ line += ',';
+ }
+ out.push(line);
+ }
+
+ out.push(`};`);
+ if (blob.alignment) {
+ out.push('#endif /* __SSE4_2__ */');
+ }
+ }
+ out.push('');
+ }
+
+ private buildMatchSequence(out: string[]): void {
+ if (this.matchSequence.size === 0) {
+ return;
+ }
+
+ MatchSequence.buildGlobals(out);
+ out.push('');
+
+ for (const match of this.matchSequence.values()) {
+ match.build(this, out);
+ out.push('');
+ }
+ }
+
+ public reserveSpans(spans: ReadonlyArray<frontend.SpanField>): void {
+ for (const span of spans) {
+ for (const callback of span.callbacks) {
+ this.buildCode(this.unwrapCode(callback));
+ }
+ }
+ }
+
+ public debug(out: string[], message: string): void {
+ if (this.options.debug === undefined) {
+ return;
+ }
+
+ const args = [
+ this.stateArg(),
+ `(const char*) ${this.posArg()}`,
+ `(const char*) ${this.endPosArg()}`,
+ ];
+
+ out.push(`${this.options.debug}(${args.join(', ')},`);
+ out.push(` ${this.cstring(message)});`);
+ }
+
+ public buildGlobals(out: string[]): void {
+ if (this.options.debug !== undefined) {
+ out.push(`void ${this.options.debug}(`);
+ out.push(` ${this.prefix}_t* s, const char* p, const char* endp,`);
+ out.push(' const char* msg);');
+ }
+
+ this.buildBlobs(out);
+ this.buildMatchSequence(out);
+ this.buildStateEnum(out);
+
+ for (const code of this.codeMap.values()) {
+ out.push('');
+ code.build(this, out);
+ }
+ }
+
+ public buildResumptionStates(out: string[]): void {
+ this.stateMap.forEach((lines, name) => {
+ if (!this.resumptionTargets.has(name)) {
+ return;
+ }
+ out.push(`case ${name}:`);
+ out.push(`${LABEL_PREFIX}${name}: {`);
+ lines.forEach((line) => out.push(` ${line}`));
+ out.push(' /* UNREACHABLE */;');
+ out.push(' abort();');
+ out.push('}');
+ });
+ }
+
+ public buildInternalStates(out: string[]): void {
+ this.stateMap.forEach((lines, name) => {
+ if (this.resumptionTargets.has(name)) {
+ return;
+ }
+ out.push(`${LABEL_PREFIX}${name}: {`);
+ lines.forEach((line) => out.push(` ${line}`));
+ out.push(' /* UNREACHABLE */;');
+ out.push(' abort();');
+ out.push('}');
+ });
+ }
+
+ public addState(state: string, lines: ReadonlyArray<string>): void {
+ assert(!this.stateMap.has(state));
+ this.stateMap.set(state, lines);
+ }
+
+ public buildCode(code: Code<frontend.code.Code>): string {
+ if (this.codeMap.has(code.ref.name)) {
+ assert.strictEqual(this.codeMap.get(code.ref.name)!, code,
+ `Code name conflict for "${code.ref.name}"`);
+ } else {
+ this.codeMap.set(code.ref.name, code);
+ }
+ return code.ref.name;
+ }
+
+ public getFieldType(field: string): string {
+ for (const property of this.properties) {
+ if (property.name === field) {
+ return property.ty;
+ }
+ }
+ throw new Error(`Field "${field}" not found`);
+ }
+
+ // Helpers
+
+ public unwrapCode(code: frontend.IWrap<frontend.code.Code>)
+ : Code<frontend.code.Code> {
+ const container = code as frontend.ContainerWrap<frontend.code.Code>;
+ return container.get(CONTAINER_KEY);
+ }
+
+ public unwrapNode(node: WrappedNode): Node<frontend.node.Node> {
+ const container = node as frontend.ContainerWrap<frontend.node.Node>;
+ return container.get(CONTAINER_KEY);
+ }
+
+ public unwrapTransform(node: frontend.IWrap<frontend.transform.Transform>)
+ : Transform<frontend.transform.Transform> {
+ const container =
+ node as frontend.ContainerWrap<frontend.transform.Transform>;
+ return container.get(CONTAINER_KEY);
+ }
+
+ public indent(out: string[], lines: ReadonlyArray<string>, pad: string) {
+ for (const line of lines) {
+ out.push(`${pad}${line}`);
+ }
+ }
+
+ // MatchSequence cache
+
+ public getMatchSequence(
+ transform: frontend.IWrap<frontend.transform.Transform>, select: Buffer)
+ : string {
+ const wrap = this.unwrapTransform(transform);
+
+ let res: MatchSequence;
+ if (this.matchSequence.has(wrap.ref.name)) {
+ res = this.matchSequence.get(wrap.ref.name)!;
+ } else {
+ res = new MatchSequence(wrap);
+ this.matchSequence.set(wrap.ref.name, res);
+ }
+
+ return res.getName();
+ }
+
+ // Arguments
+
+ public stateArg(): string {
+ return ARG_STATE;
+ }
+
+ public posArg(): string {
+ return ARG_POS;
+ }
+
+ public endPosArg(): string {
+ return ARG_ENDPOS;
+ }
+
+ public matchVar(): string {
+ return VAR_MATCH;
+ }
+
+ // State fields
+
+ public indexField(): string {
+ return this.stateField('_index');
+ }
+
+ public currentField(): string {
+ return this.stateField('_current');
+ }
+
+ public errorField(): string {
+ return this.stateField('error');
+ }
+
+ public reasonField(): string {
+ return this.stateField('reason');
+ }
+
+ public errorPosField(): string {
+ return this.stateField('error_pos');
+ }
+
+ public spanPosField(index: number): string {
+ return this.stateField(`_span_pos${index}`);
+ }
+
+ public spanCbField(index: number): string {
+ return this.stateField(`_span_cb${index}`);
+ }
+
+ public stateField(name: string): string {
+ return `${this.stateArg()}->${name}`;
+ }
+
+ // Globals
+
+ public cstring(value: string): string {
+ return JSON.stringify(value);
+ }
+
+ public blob(value: Buffer, alignment?: number): string {
+ if (this.blobs.has(value)) {
+ return this.blobs.get(value)!.name;
+ }
+
+ const res = BLOB_PREFIX + this.blobs.size;
+ this.blobs.set(value, {
+ alignment,
+ buffer: value,
+ name: res,
+ });
+ return res;
+ }
+}
diff --git a/llparse/src/implementation/c/constants.ts b/llparse/src/implementation/c/constants.ts
new file mode 100644
index 0000000..bfd5be3
--- /dev/null
+++ b/llparse/src/implementation/c/constants.ts
@@ -0,0 +1,45 @@
+export const CONTAINER_KEY = 'c';
+
+export const LABEL_PREFIX = '';
+export const STATE_PREFIX = 's_n_';
+export const STATE_ERROR = 's_error';
+
+export const BLOB_PREFIX = 'llparse_blob';
+
+export const ARG_STATE = 'state';
+export const ARG_POS = 'p';
+export const ARG_ENDPOS = 'endp';
+
+export const VAR_MATCH = 'match';
+
+// MatchSequence
+
+export const SEQUENCE_COMPLETE = 'kMatchComplete';
+export const SEQUENCE_MISMATCH = 'kMatchMismatch';
+export const SEQUENCE_PAUSE = 'kMatchPause';
+
+export const SIGNED_LIMITS: Map<string, [ string, string ]> = new Map();
+SIGNED_LIMITS.set('i8', [ '-0x80', '0x7f' ]);
+SIGNED_LIMITS.set('i16', [ '-0x8000', '0x7fff' ]);
+SIGNED_LIMITS.set('i32', [ '(-0x7fffffff - 1)', '0x7fffffff' ]);
+SIGNED_LIMITS.set('i64', [ '(-0x7fffffffffffffffLL - 1)',
+ '0x7fffffffffffffffLL' ]);
+
+export const UNSIGNED_LIMITS: Map<string, [ string, string ]> = new Map();
+UNSIGNED_LIMITS.set('i8', [ '0', '0xff' ]);
+UNSIGNED_LIMITS.set('i8', [ '0', '0xff' ]);
+UNSIGNED_LIMITS.set('i16', [ '0', '0xffff' ]);
+UNSIGNED_LIMITS.set('i32', [ '0', '0xffffffff' ]);
+UNSIGNED_LIMITS.set('i64', [ '0ULL', '0xffffffffffffffffULL' ]);
+
+export const UNSIGNED_TYPES: Map<string, string> = new Map();
+UNSIGNED_TYPES.set('i8', 'uint8_t');
+UNSIGNED_TYPES.set('i16', 'uint16_t');
+UNSIGNED_TYPES.set('i32', 'uint32_t');
+UNSIGNED_TYPES.set('i64', 'uint64_t');
+
+export const SIGNED_TYPES: Map<string, string> = new Map();
+SIGNED_TYPES.set('i8', 'int8_t');
+SIGNED_TYPES.set('i16', 'int16_t');
+SIGNED_TYPES.set('i32', 'int32_t');
+SIGNED_TYPES.set('i64', 'int64_t');
diff --git a/llparse/src/implementation/c/helpers/match-sequence.ts b/llparse/src/implementation/c/helpers/match-sequence.ts
new file mode 100644
index 0000000..278f4b5
--- /dev/null
+++ b/llparse/src/implementation/c/helpers/match-sequence.ts
@@ -0,0 +1,75 @@
+import * as assert from 'assert';
+import { Buffer } from 'buffer';
+import * as frontend from 'llparse-frontend';
+
+import {
+ SEQUENCE_COMPLETE, SEQUENCE_MISMATCH, SEQUENCE_PAUSE,
+} from '../constants';
+import { Transform } from '../transform';
+import { Compilation } from '../compilation';
+
+type TransformWrap = Transform<frontend.transform.Transform>;
+
+export class MatchSequence {
+ constructor(private readonly transform: TransformWrap) {
+ }
+
+ public static buildGlobals(out: string[]): void {
+ out.push('enum llparse_match_status_e {');
+ out.push(` ${SEQUENCE_COMPLETE},`);
+ out.push(` ${SEQUENCE_PAUSE},`);
+ out.push(` ${SEQUENCE_MISMATCH}`);
+ out.push('};');
+ out.push('typedef enum llparse_match_status_e llparse_match_status_t;');
+ out.push('');
+ out.push('struct llparse_match_s {');
+ out.push(' llparse_match_status_t status;');
+ out.push(' const unsigned char* current;');
+ out.push('};');
+ out.push('typedef struct llparse_match_s llparse_match_t;');
+ }
+
+ public getName(): string {
+ return `llparse__match_sequence_${this.transform.ref.name}`;
+ }
+
+ public build(ctx: Compilation, out: string[]): void {
+ out.push(`static llparse_match_t ${this.getName()}(`);
+ out.push(` ${ctx.prefix}_t* s, const unsigned char* p,`);
+ out.push(' const unsigned char* endp,');
+ out.push(' const unsigned char* seq, uint32_t seq_len) {');
+
+ // Vars
+ out.push(' uint32_t index;');
+ out.push(' llparse_match_t res;');
+ out.push('');
+
+ // Body
+ out.push(' index = s->_index;');
+ out.push(' for (; p != endp; p++) {');
+ out.push(' unsigned char current;');
+ out.push('');
+ out.push(` current = ${this.transform.build(ctx, '*p')};`);
+ out.push(' if (current == seq[index]) {');
+ out.push(' if (++index == seq_len) {');
+ out.push(` res.status = ${SEQUENCE_COMPLETE};`);
+ out.push(' goto reset;');
+ out.push(' }');
+ out.push(' } else {');
+ out.push(` res.status = ${SEQUENCE_MISMATCH};`);
+ out.push(' goto reset;');
+ out.push(' }');
+ out.push(' }');
+
+ out.push(' s->_index = index;');
+ out.push(` res.status = ${SEQUENCE_PAUSE};`);
+ out.push(' res.current = p;');
+ out.push(' return res;');
+
+ out.push('reset:');
+ out.push(' s->_index = 0;');
+ out.push(' res.current = p;');
+ out.push(' return res;');
+ out.push('}');
+ }
+}
diff --git a/llparse/src/implementation/c/index.ts b/llparse/src/implementation/c/index.ts
new file mode 100644
index 0000000..ae94d34
--- /dev/null
+++ b/llparse/src/implementation/c/index.ts
@@ -0,0 +1,199 @@
+import * as frontend from 'llparse-frontend';
+
+import {
+ ARG_STATE, ARG_POS, ARG_ENDPOS,
+ STATE_ERROR,
+ VAR_MATCH,
+ CONTAINER_KEY,
+} from './constants';
+import { Compilation } from './compilation';
+import code from './code';
+import node from './node';
+import { Node } from './node';
+import transform from './transform';
+
+export interface ICCompilerOptions {
+ readonly debug?: string;
+ readonly header?: string;
+}
+
+export interface ICPublicOptions {
+ readonly header?: string;
+}
+
+export class CCompiler {
+ constructor(container: frontend.Container,
+ public readonly options: ICCompilerOptions) {
+ container.add(CONTAINER_KEY, { code, node, transform });
+ }
+
+ public compile(info: frontend.IFrontendResult): string {
+ const compilation = new Compilation(info.prefix, info.properties,
+ info.resumptionTargets, this.options);
+ const out: string[] = [];
+
+ out.push('#include <stdlib.h>');
+ out.push('#include <stdint.h>');
+ out.push('#include <string.h>');
+ out.push('');
+
+ // NOTE: Inspired by https://github.com/h2o/picohttpparser
+ // TODO(indutny): Windows support for SSE4.2.
+ // See: https://github.com/nodejs/llparse/pull/24#discussion_r299789676
+ // (There is no `__SSE4_2__` define for MSVC)
+ out.push('#ifdef __SSE4_2__');
+ out.push(' #ifdef _MSC_VER');
+ out.push(' #include <nmmintrin.h>');
+ out.push(' #else /* !_MSC_VER */');
+ out.push(' #include <x86intrin.h>');
+ out.push(' #endif /* _MSC_VER */');
+ out.push('#endif /* __SSE4_2__ */');
+ out.push('');
+
+ out.push('#ifdef _MSC_VER');
+ out.push(' #define ALIGN(n) _declspec(align(n))');
+ out.push('#else /* !_MSC_VER */');
+ out.push(' #define ALIGN(n) __attribute__((aligned(n)))');
+ out.push('#endif /* _MSC_VER */');
+
+ out.push('');
+ out.push(`#include "${this.options.header || info.prefix}.h"`);
+ out.push(``);
+ out.push(`typedef int (*${info.prefix}__span_cb)(`);
+ out.push(` ${info.prefix}_t*, const char*, const char*);`);
+ out.push('');
+
+ // Queue span callbacks to be built before `executeSpans()` code gets called
+ // below.
+ compilation.reserveSpans(info.spans);
+
+ const root = info.root as frontend.ContainerWrap<frontend.node.Node>;
+ const rootState = root.get<Node<frontend.node.Node>>(CONTAINER_KEY)
+ .build(compilation);
+
+ compilation.buildGlobals(out);
+ out.push('');
+
+ out.push(`int ${info.prefix}_init(${info.prefix}_t* ${ARG_STATE}) {`);
+ out.push(` memset(${ARG_STATE}, 0, sizeof(*${ARG_STATE}));`);
+ out.push(` ${ARG_STATE}->_current = (void*) (intptr_t) ${rootState};`);
+ out.push(' return 0;');
+ out.push('}');
+ out.push('');
+
+ out.push(`static llparse_state_t ${info.prefix}__run(`);
+ out.push(` ${info.prefix}_t* ${ARG_STATE},`);
+ out.push(` const unsigned char* ${ARG_POS},`);
+ out.push(` const unsigned char* ${ARG_ENDPOS}) {`);
+ out.push(` int ${VAR_MATCH};`);
+ out.push(` switch ((llparse_state_t) (intptr_t) ` +
+ `${compilation.currentField()}) {`);
+
+ let tmp: string[] = [];
+ compilation.buildResumptionStates(tmp);
+ compilation.indent(out, tmp, ' ');
+
+ out.push(' default:');
+ out.push(' /* UNREACHABLE */');
+ out.push(' abort();');
+ out.push(' }');
+
+ tmp = [];
+ compilation.buildInternalStates(tmp);
+ compilation.indent(out, tmp, ' ');
+
+ out.push('}');
+ out.push('');
+
+
+ out.push(`int ${info.prefix}_execute(${info.prefix}_t* ${ARG_STATE}, ` +
+ `const char* ${ARG_POS}, const char* ${ARG_ENDPOS}) {`);
+ out.push(' llparse_state_t next;');
+ out.push('');
+
+ out.push(' /* check lingering errors */');
+ out.push(` if (${compilation.errorField()} != 0) {`);
+ out.push(` return ${compilation.errorField()};`);
+ out.push(' }');
+ out.push('');
+
+ tmp = [];
+ this.restartSpans(compilation, info, tmp);
+ compilation.indent(out, tmp, ' ');
+
+ const args = [
+ compilation.stateArg(),
+ `(const unsigned char*) ${compilation.posArg()}`,
+ `(const unsigned char*) ${compilation.endPosArg()}`,
+ ];
+ out.push(` next = ${info.prefix}__run(${args.join(', ')});`);
+ out.push(` if (next == ${STATE_ERROR}) {`);
+ out.push(` return ${compilation.errorField()};`);
+ out.push(' }');
+ out.push(` ${compilation.currentField()} = (void*) (intptr_t) next;`);
+ out.push('');
+
+ tmp = [];
+ this.executeSpans(compilation, info, tmp);
+ compilation.indent(out, tmp, ' ');
+
+ out.push(' return 0;');
+ out.push('}');
+
+ return out.join('\n');
+ }
+
+ private restartSpans(ctx: Compilation, info: frontend.IFrontendResult,
+ out: string[]): void {
+ if (info.spans.length === 0) {
+ return;
+ }
+
+ out.push('/* restart spans */');
+ for (const span of info.spans) {
+ const posField = ctx.spanPosField(span.index);
+
+ out.push(`if (${posField} != NULL) {`);
+ out.push(` ${posField} = (void*) ${ctx.posArg()};`);
+ out.push('}');
+ }
+ out.push('');
+ }
+
+ private executeSpans(ctx: Compilation, info: frontend.IFrontendResult,
+ out: string[]): void {
+ if (info.spans.length === 0) {
+ return;
+ }
+
+ out.push('/* execute spans */');
+ for (const span of info.spans) {
+ const posField = ctx.spanPosField(span.index);
+ let callback: string;
+ if (span.callbacks.length === 1) {
+ callback = ctx.buildCode(ctx.unwrapCode(span.callbacks[0]));
+ } else {
+ callback = `(${info.prefix}__span_cb) ` + ctx.spanCbField(span.index);
+ callback = `(${callback})`;
+ }
+
+ const args = [
+ ctx.stateArg(), posField, `(const char*) ${ctx.endPosArg()}`,
+ ];
+
+ out.push(`if (${posField} != NULL) {`);
+ out.push(' int error;');
+ out.push('');
+ out.push(` error = ${callback}(${args.join(', ')});`);
+
+ // TODO(indutny): de-duplicate this here and in SpanEnd
+ out.push(' if (error != 0) {');
+ out.push(` ${ctx.errorField()} = error;`);
+ out.push(` ${ctx.errorPosField()} = ${ctx.endPosArg()};`);
+ out.push(' return error;');
+ out.push(' }');
+ out.push('}');
+ }
+ out.push('');
+ }
+}
diff --git a/llparse/src/implementation/c/node/base.ts b/llparse/src/implementation/c/node/base.ts
new file mode 100644
index 0000000..51f90bb
--- /dev/null
+++ b/llparse/src/implementation/c/node/base.ts
@@ -0,0 +1,77 @@
+import * as assert from 'assert';
+import * as frontend from 'llparse-frontend';
+
+import { Compilation } from '../compilation';
+import {
+ STATE_PREFIX, LABEL_PREFIX,
+} from '../constants';
+
+export interface INodeEdge {
+ readonly node: frontend.IWrap<frontend.node.Node>;
+ readonly noAdvance: boolean;
+ readonly value?: number;
+}
+
+export abstract class Node<T extends frontend.node.Node> {
+ protected cachedDecl: string | undefined;
+ protected privCompilation: Compilation | undefined;
+
+ constructor(public readonly ref: T) {
+ }
+
+ public build(compilation: Compilation): string {
+ if (this.cachedDecl !== undefined) {
+ return this.cachedDecl;
+ }
+
+ const res = STATE_PREFIX + this.ref.id.name;
+ this.cachedDecl = res;
+
+ this.privCompilation = compilation;
+
+ const out: string[] = [];
+ compilation.debug(out,
+ `Entering node "${this.ref.id.originalName}" ("${this.ref.id.name}")`);
+ this.doBuild(out);
+
+ compilation.addState(res, out);
+
+ return res;
+ }
+
+ protected get compilation(): Compilation {
+ assert(this.privCompilation !== undefined);
+ return this.privCompilation!;
+ }
+
+ protected prologue(out: string[]): void {
+ const ctx = this.compilation;
+
+ out.push(`if (${ctx.posArg()} == ${ctx.endPosArg()}) {`);
+
+ const tmp: string[] = [];
+ this.pause(tmp);
+ this.compilation.indent(out, tmp, ' ');
+
+ out.push('}');
+ }
+
+ protected pause(out: string[]): void {
+ out.push(`return ${this.cachedDecl};`);
+ }
+
+ protected tailTo(out: string[], edge: INodeEdge): void {
+ const ctx = this.compilation;
+ const target = ctx.unwrapNode(edge.node).build(ctx);
+
+ if (!edge.noAdvance) {
+ out.push(`${ctx.posArg()}++;`);
+ }
+ if (edge.value !== undefined) {
+ out.push(`${ctx.matchVar()} = ${edge.value};`);
+ }
+ out.push(`goto ${LABEL_PREFIX}${target};`);
+ }
+
+ protected abstract doBuild(out: string[]): void;
+}
diff --git a/llparse/src/implementation/c/node/consume.ts b/llparse/src/implementation/c/node/consume.ts
new file mode 100644
index 0000000..658a00e
--- /dev/null
+++ b/llparse/src/implementation/c/node/consume.ts
@@ -0,0 +1,48 @@
+import * as frontend from 'llparse-frontend';
+
+import { Compilation } from '../compilation';
+import { Node } from './base';
+
+export class Consume extends Node<frontend.node.Consume> {
+ public doBuild(out: string[]): void {
+ const ctx = this.compilation;
+
+ const index = ctx.stateField(this.ref.field);
+ const ty = ctx.getFieldType(this.ref.field);
+
+ let fieldTy: string;
+ if (ty === 'i64') {
+ fieldTy = 'uint64_t';
+ } else if (ty === 'i32') {
+ fieldTy = 'uint32_t';
+ } else if (ty === 'i16') {
+ fieldTy = 'uint16_t';
+ } else if (ty === 'i8') {
+ fieldTy = 'uint8_t';
+ } else {
+ throw new Error(
+ `Unsupported type ${ty} of field ${this.ref.field} for consume node`);
+ }
+
+ out.push('size_t avail;');
+ out.push(`${fieldTy} need;`);
+
+ out.push('');
+ out.push(`avail = ${ctx.endPosArg()} - ${ctx.posArg()};`);
+ out.push(`need = ${index};`);
+
+ // Note: `avail` or `need` are going to coerced to the largest
+ // datatype needed to hold either of the values.
+ out.push('if (avail >= need) {');
+ out.push(` p += need;`);
+ out.push(` ${index} = 0;`);
+ const tmp: string[] = [];
+ this.tailTo(tmp, this.ref.otherwise!);
+ ctx.indent(out, tmp, ' ');
+ out.push('}');
+ out.push('');
+
+ out.push(`${index} -= avail;`);
+ this.pause(out);
+ }
+}
diff --git a/llparse/src/implementation/c/node/empty.ts b/llparse/src/implementation/c/node/empty.ts
new file mode 100644
index 0000000..e28ecb5
--- /dev/null
+++ b/llparse/src/implementation/c/node/empty.ts
@@ -0,0 +1,16 @@
+import * as frontend from 'llparse-frontend';
+
+import { Compilation } from '../compilation';
+import { Node } from './base';
+
+export class Empty extends Node<frontend.node.Empty> {
+ public doBuild(out: string[]): void {
+ const otherwise = this.ref.otherwise!;
+
+ if (!otherwise.noAdvance) {
+ this.prologue(out);
+ }
+
+ this.tailTo(out, otherwise);
+ }
+}
diff --git a/llparse/src/implementation/c/node/error.ts b/llparse/src/implementation/c/node/error.ts
new file mode 100644
index 0000000..29dce63
--- /dev/null
+++ b/llparse/src/implementation/c/node/error.ts
@@ -0,0 +1,33 @@
+import * as frontend from 'llparse-frontend';
+
+import { Compilation } from '../compilation';
+import { STATE_ERROR } from '../constants';
+import { Node } from './base';
+
+class ErrorNode<T extends frontend.node.Error> extends Node<T> {
+ protected storeError(out: string[]): void {
+ const ctx = this.compilation;
+
+ let hexCode: string;
+ if (this.ref.code < 0) {
+ hexCode = `-0x` + this.ref.code.toString(16);
+ } else {
+ hexCode = '0x' + this.ref.code.toString(16);
+ }
+
+ out.push(`${ctx.errorField()} = ${hexCode};`);
+ out.push(`${ctx.reasonField()} = ${ctx.cstring(this.ref.reason)};`);
+ out.push(`${ctx.errorPosField()} = (const char*) ${ctx.posArg()};`);
+ }
+
+ public doBuild(out: string[]): void {
+ this.storeError(out);
+
+ // Non-recoverable state
+ out.push(`${this.compilation.currentField()} = ` +
+ `(void*) (intptr_t) ${STATE_ERROR};`);
+ out.push(`return ${STATE_ERROR};`);
+ }
+}
+
+export { ErrorNode as Error };
diff --git a/llparse/src/implementation/c/node/index.ts b/llparse/src/implementation/c/node/index.ts
new file mode 100644
index 0000000..ba751d9
--- /dev/null
+++ b/llparse/src/implementation/c/node/index.ts
@@ -0,0 +1,27 @@
+import * as frontend from 'llparse-frontend';
+
+import { Consume } from './consume';
+import { Empty } from './empty';
+import { Error as ErrorNode } from './error';
+import { Invoke } from './invoke';
+import { Pause } from './pause';
+import { Sequence } from './sequence';
+import { Single } from './single';
+import { SpanEnd } from './span-end';
+import { SpanStart } from './span-start';
+import { TableLookup } from './table-lookup';
+
+export { Node } from './base';
+
+export default {
+ Consume,
+ Empty,
+ Error: class Error extends ErrorNode<frontend.node.Error> {},
+ Invoke,
+ Pause,
+ Sequence,
+ Single,
+ SpanEnd,
+ SpanStart,
+ TableLookup,
+};
diff --git a/llparse/src/implementation/c/node/invoke.ts b/llparse/src/implementation/c/node/invoke.ts
new file mode 100644
index 0000000..ee917e9
--- /dev/null
+++ b/llparse/src/implementation/c/node/invoke.ts
@@ -0,0 +1,44 @@
+import * as frontend from 'llparse-frontend';
+
+import { Compilation } from '../compilation';
+import { Node } from './base';
+
+export class Invoke extends Node<frontend.node.Invoke> {
+ public doBuild(out: string[]): void {
+ const ctx = this.compilation;
+
+ const code = ctx.unwrapCode(this.ref.code);
+ const codeDecl = ctx.buildCode(code);
+
+ const args: string[] = [
+ ctx.stateArg(),
+ ctx.posArg(),
+ ctx.endPosArg(),
+ ];
+
+ const signature = code.ref.signature;
+ if (signature === 'value') {
+ args.push(ctx.matchVar());
+ }
+
+ out.push(`switch (${codeDecl}(${args.join(', ')})) {`);
+ let tmp: string[];
+
+ for (const edge of this.ref.edges) {
+ out.push(` case ${edge.code}:`);
+ tmp = [];
+ this.tailTo(tmp, {
+ noAdvance: true,
+ node: edge.node,
+ value: undefined,
+ });
+ ctx.indent(out, tmp, ' ');
+ }
+
+ out.push(' default:');
+ tmp = [];
+ this.tailTo(tmp, this.ref.otherwise!);
+ ctx.indent(out, tmp, ' ');
+ out.push('}');
+ }
+}
diff --git a/llparse/src/implementation/c/node/pause.ts b/llparse/src/implementation/c/node/pause.ts
new file mode 100644
index 0000000..c239b46
--- /dev/null
+++ b/llparse/src/implementation/c/node/pause.ts
@@ -0,0 +1,19 @@
+import * as frontend from 'llparse-frontend';
+
+import { Compilation } from '../compilation';
+import { STATE_ERROR } from '../constants';
+import { Error as ErrorNode } from './error';
+
+export class Pause extends ErrorNode<frontend.node.Pause> {
+ public doBuild(out: string[]): void {
+ const ctx = this.compilation;
+
+ this.storeError(out);
+
+ // Recoverable state
+ const otherwise = ctx.unwrapNode(this.ref.otherwise!.node).build(ctx);
+ out.push(`${ctx.currentField()} = ` +
+ `(void*) (intptr_t) ${otherwise};`);
+ out.push(`return ${STATE_ERROR};`);
+ }
+}
diff --git a/llparse/src/implementation/c/node/sequence.ts b/llparse/src/implementation/c/node/sequence.ts
new file mode 100644
index 0000000..73d8816
--- /dev/null
+++ b/llparse/src/implementation/c/node/sequence.ts
@@ -0,0 +1,55 @@
+import * as frontend from 'llparse-frontend';
+
+import { Compilation } from '../compilation';
+import {
+ SEQUENCE_COMPLETE, SEQUENCE_MISMATCH, SEQUENCE_PAUSE,
+} from '../constants';
+import { Node } from './base';
+
+export class Sequence extends Node<frontend.node.Sequence> {
+ public doBuild(out: string[]): void {
+ const ctx = this.compilation;
+
+ out.push('llparse_match_t match_seq;');
+ out.push('');
+
+ this.prologue(out);
+
+ const matchSequence = ctx.getMatchSequence(this.ref.transform!,
+ this.ref.select);
+
+ out.push(`match_seq = ${matchSequence}(${ctx.stateArg()}, ` +
+ `${ctx.posArg()}, ` +
+ `${ctx.endPosArg()}, ${ctx.blob(this.ref.select)}, ` +
+ `${this.ref.select.length});`);
+ out.push('p = match_seq.current;');
+
+ let tmp: string[];
+
+ out.push('switch (match_seq.status) {');
+
+ out.push(` case ${SEQUENCE_COMPLETE}: {`);
+ tmp = [];
+ this.tailTo(tmp, {
+ noAdvance: false,
+ node: this.ref.edge!.node,
+ value: this.ref.edge!.value,
+ });
+ ctx.indent(out, tmp, ' ');
+ out.push(' }');
+
+ out.push(` case ${SEQUENCE_PAUSE}: {`);
+ tmp = [];
+ this.pause(tmp);
+ ctx.indent(out, tmp, ' ');
+ out.push(' }');
+
+ out.push(` case ${SEQUENCE_MISMATCH}: {`);
+ tmp = [];
+ this.tailTo(tmp, this.ref.otherwise!);
+ ctx.indent(out, tmp, ' ');
+ out.push(' }');
+
+ out.push('}');
+ }
+}
diff --git a/llparse/src/implementation/c/node/single.ts b/llparse/src/implementation/c/node/single.ts
new file mode 100644
index 0000000..b9c8811
--- /dev/null
+++ b/llparse/src/implementation/c/node/single.ts
@@ -0,0 +1,47 @@
+import * as frontend from 'llparse-frontend';
+
+import { Compilation } from '../compilation';
+import { Node } from './base';
+
+export class Single extends Node<frontend.node.Single> {
+ public doBuild(out: string[]): void {
+ const ctx = this.compilation;
+
+ const otherwise = this.ref.otherwise!;
+
+ this.prologue(out);
+
+ const transform = ctx.unwrapTransform(this.ref.transform!);
+ const current = transform.build(ctx, `*${ctx.posArg()}`);
+
+ out.push(`switch (${current}) {`)
+ this.ref.edges.forEach((edge) => {
+ let ch: string;
+
+ // Non-printable ASCII, or single-quote, or forward slash
+ if (edge.key < 0x20 || edge.key > 0x7e || edge.key === 0x27 ||
+ edge.key === 0x5c) {
+ ch = edge.key.toString();
+ } else {
+ ch = `'${String.fromCharCode(edge.key)}'`;
+ }
+ out.push(` case ${ch}: {`);
+
+ const tmp: string[] = [];
+ this.tailTo(tmp, edge);
+ ctx.indent(out, tmp, ' ');
+
+ out.push(' }');
+ });
+
+ out.push(` default: {`);
+
+ const tmp: string[] = [];
+ this.tailTo(tmp, otherwise);
+ ctx.indent(out, tmp, ' ');
+
+ out.push(' }');
+
+ out.push(`}`);
+ }
+}
diff --git a/llparse/src/implementation/c/node/span-end.ts b/llparse/src/implementation/c/node/span-end.ts
new file mode 100644
index 0000000..09f97e5
--- /dev/null
+++ b/llparse/src/implementation/c/node/span-end.ts
@@ -0,0 +1,56 @@
+import * as frontend from 'llparse-frontend';
+
+import { Compilation } from '../compilation';
+import { STATE_ERROR } from '../constants';
+import { Node } from './base';
+
+export class SpanEnd extends Node<frontend.node.SpanEnd> {
+ public doBuild(out: string[]): void {
+ out.push('const unsigned char* start;');
+ out.push('int err;');
+ out.push('');
+
+ const ctx = this.compilation;
+ const field = this.ref.field;
+ const posField = ctx.spanPosField(field.index);
+
+ // Load start position
+ out.push(`start = ${posField};`);
+
+ // ...and reset
+ out.push(`${posField} = NULL;`);
+
+ // Invoke callback
+ const callback = ctx.buildCode(ctx.unwrapCode(this.ref.callback));
+ out.push(`err = ${callback}(${ctx.stateArg()}, start, ${ctx.posArg()});`);
+
+ out.push('if (err != 0) {');
+ const tmp: string[] = [];
+ this.buildError(tmp, 'err');
+ ctx.indent(out, tmp, ' ');
+ out.push('}');
+
+ const otherwise = this.ref.otherwise!;
+ this.tailTo(out, otherwise);
+ }
+
+ private buildError(out: string[], code: string) {
+ const ctx = this.compilation;
+
+ out.push(`${ctx.errorField()} = ${code};`);
+
+ const otherwise = this.ref.otherwise!;
+ let resumePos = ctx.posArg();
+ if (!otherwise.noAdvance) {
+ resumePos = `(${resumePos} + 1)`;
+ }
+
+ out.push(`${ctx.errorPosField()} = (const char*) ${resumePos};`);
+
+ const resumptionTarget = ctx.unwrapNode(otherwise.node).build(ctx);
+ out.push(`${ctx.currentField()} = ` +
+ `(void*) (intptr_t) ${resumptionTarget};`);
+
+ out.push(`return ${STATE_ERROR};`);
+ }
+}
diff --git a/llparse/src/implementation/c/node/span-start.ts b/llparse/src/implementation/c/node/span-start.ts
new file mode 100644
index 0000000..445da67
--- /dev/null
+++ b/llparse/src/implementation/c/node/span-start.ts
@@ -0,0 +1,26 @@
+import * as frontend from 'llparse-frontend';
+
+import { Compilation } from '../compilation';
+import { Node } from './base';
+
+export class SpanStart extends Node<frontend.node.SpanStart> {
+ public doBuild(out: string[]): void {
+ // Prevent spurious empty spans
+ this.prologue(out);
+
+ const ctx = this.compilation;
+ const field = this.ref.field;
+
+ const posField = ctx.spanPosField(field.index);
+ out.push(`${posField} = (void*) ${ctx.posArg()};`);
+
+ if (field.callbacks.length > 1) {
+ const cbField = ctx.spanCbField(field.index);
+ const callback = ctx.unwrapCode(this.ref.callback);
+ out.push(`${cbField} = ${ctx.buildCode(callback)};`);
+ }
+
+ const otherwise = this.ref.otherwise!;
+ this.tailTo(out, otherwise);
+ }
+}
diff --git a/llparse/src/implementation/c/node/table-lookup.ts b/llparse/src/implementation/c/node/table-lookup.ts
new file mode 100644
index 0000000..6a400a3
--- /dev/null
+++ b/llparse/src/implementation/c/node/table-lookup.ts
@@ -0,0 +1,196 @@
+import * as assert from 'assert';
+import * as frontend from 'llparse-frontend';
+
+import { Compilation } from '../compilation';
+import { Node } from './base';
+
+const MAX_CHAR = 0xff;
+const TABLE_GROUP = 16;
+
+// _mm_cmpestri takes 8 ranges
+const SSE_RANGES_LEN = 16;
+// _mm_cmpestri takes 128bit input
+const SSE_RANGES_PAD = 16;
+const MAX_SSE_CALLS = 2;
+const SSE_ALIGNMENT = 16;
+
+interface ITable {
+ readonly name: string;
+ readonly declaration: ReadonlyArray<string>;
+}
+
+export class TableLookup extends Node<frontend.node.TableLookup> {
+ public doBuild(out: string[]): void {
+ const ctx = this.compilation;
+
+ const table = this.buildTable();
+ for (const line of table.declaration) {
+ out.push(line);
+ }
+
+ this.prologue(out);
+
+ const transform = ctx.unwrapTransform(this.ref.transform!);
+
+ // Try to vectorize nodes matching characters and looping to themselves
+ // NOTE: `switch` below triggers when there is not enough characters in the
+ // stream for vectorized processing.
+ this.buildSSE(out);
+
+ const current = transform.build(ctx, `*${ctx.posArg()}`);
+ out.push(`switch (${table.name}[(uint8_t) ${current}]) {`);
+
+ for (const [ index, edge ] of this.ref.edges.entries()) {
+ out.push(` case ${index + 1}: {`);
+
+ const tmp: string[] = [];
+ const edge = this.ref.edges[index];
+ this.tailTo(tmp, {
+ noAdvance: edge.noAdvance,
+ node: edge.node,
+ value: undefined,
+ });
+ ctx.indent(out, tmp, ' ');
+
+ out.push(' }');
+ }
+
+ out.push(` default: {`);
+
+ const tmp: string[] = [];
+ this.tailTo(tmp, this.ref.otherwise!);
+ ctx.indent(out, tmp, ' ');
+
+ out.push(' }');
+ out.push('}');
+ }
+
+ private buildSSE(out: string[]): boolean {
+ const ctx = this.compilation;
+
+ // Transformation is not supported atm
+ if (this.ref.transform && this.ref.transform.ref.name !== 'id') {
+ return false;
+ }
+
+ if (this.ref.edges.length !== 1) {
+ return false;
+ }
+
+ const edge = this.ref.edges[0];
+ if (edge.node.ref !== this.ref) {
+ return false;
+ }
+
+ // NOTE: keys are sorted
+ let ranges: number[] = [];
+ let first: number | undefined;
+ let last: number | undefined;
+ for (const key of edge.keys) {
+ if (first === undefined) {
+ first = key;
+ }
+ if (last === undefined) {
+ last = key;
+ }
+
+ if (key - last > 1) {
+ ranges.push(first, last);
+ first = key;
+ }
+ last = key;
+ }
+ if (first !== undefined && last !== undefined) {
+ ranges.push(first, last);
+ }
+
+ if (ranges.length === 0) {
+ return false;
+ }
+
+ // Way too many calls would be required
+ if (ranges.length > MAX_SSE_CALLS * SSE_RANGES_LEN) {
+ return false;
+ }
+
+ out.push('#ifdef __SSE4_2__');
+ out.push(`if (${ctx.endPosArg()} - ${ctx.posArg()} >= 16) {`);
+ out.push(' __m128i ranges;');
+ out.push(' __m128i input;');
+ out.push(' int avail;');
+ out.push(' int match_len;');
+ out.push('');
+ out.push(' /* Load input */');
+ out.push(` input = _mm_loadu_si128((__m128i const*) ${ctx.posArg()});`);
+ for (let off = 0; off < ranges.length; off += SSE_RANGES_LEN) {
+ const subRanges = ranges.slice(off, off + SSE_RANGES_LEN);
+
+ let paddedRanges = subRanges.slice();
+ while (paddedRanges.length < SSE_RANGES_PAD) {
+ paddedRanges.push(0);
+ }
+
+ const blob = ctx.blob(Buffer.from(paddedRanges), SSE_ALIGNMENT);
+ out.push(` ranges = _mm_loadu_si128((__m128i const*) ${blob});`);
+ out.push('');
+
+ out.push(' /* Find first character that does not match `ranges` */');
+ out.push(` match_len = _mm_cmpestri(ranges, ${subRanges.length},`);
+ out.push(' input, 16,');
+ out.push(' _SIDD_UBYTE_OPS | _SIDD_CMP_RANGES |');
+ out.push(' _SIDD_NEGATIVE_POLARITY);');
+ out.push('');
+ out.push(' if (match_len != 0) {');
+ out.push(` ${ctx.posArg()} += match_len;`);
+
+ const tmp: string[] = [];
+ assert.strictEqual(edge.noAdvance, false);
+ this.tailTo(tmp, {
+ noAdvance: true,
+ node: edge.node,
+ });
+ ctx.indent(out, tmp, ' ');
+
+ out.push(' }');
+ }
+
+ {
+ const tmp: string[] = [];
+ this.tailTo(tmp, this.ref.otherwise!);
+ ctx.indent(out, tmp, ' ');
+ }
+ out.push('}');
+
+ out.push('#endif /* __SSE4_2__ */');
+
+ return true;
+ }
+
+ private buildTable(): ITable {
+ const table: number[] = new Array(MAX_CHAR + 1).fill(0);
+
+ for (const [ index, edge ] of this.ref.edges.entries()) {
+ edge.keys.forEach((key) => {
+ assert.strictEqual(table[key], 0);
+ table[key] = index + 1;
+ });
+ }
+
+ const lines = [
+ 'static uint8_t lookup_table[] = {',
+ ];
+ for (let i = 0; i < table.length; i += TABLE_GROUP) {
+ let line = ` ${table.slice(i, i + TABLE_GROUP).join(', ')}`;
+ if (i + TABLE_GROUP < table.length) {
+ line += ',';
+ }
+ lines.push(line);
+ }
+ lines.push('};');
+
+ return {
+ name: 'lookup_table',
+ declaration: lines,
+ };
+ }
+}
diff --git a/llparse/src/implementation/c/transform/base.ts b/llparse/src/implementation/c/transform/base.ts
new file mode 100644
index 0000000..82028d5
--- /dev/null
+++ b/llparse/src/implementation/c/transform/base.ts
@@ -0,0 +1,10 @@
+import * as frontend from 'llparse-frontend';
+
+import { Compilation } from '../compilation';
+
+export abstract class Transform<T extends frontend.transform.Transform> {
+ constructor(public readonly ref: T) {
+ }
+
+ public abstract build(ctx: Compilation, value: string): string;
+}
diff --git a/llparse/src/implementation/c/transform/id.ts b/llparse/src/implementation/c/transform/id.ts
new file mode 100644
index 0000000..6c6105f
--- /dev/null
+++ b/llparse/src/implementation/c/transform/id.ts
@@ -0,0 +1,11 @@
+import * as frontend from 'llparse-frontend';
+
+import { Compilation } from '../compilation';
+import { Transform } from './base';
+
+export class ID extends Transform<frontend.transform.ID> {
+ public build(ctx: Compilation, value: string): string {
+ // Identity transformation
+ return value;
+ }
+}
diff --git a/llparse/src/implementation/c/transform/index.ts b/llparse/src/implementation/c/transform/index.ts
new file mode 100644
index 0000000..c13ba50
--- /dev/null
+++ b/llparse/src/implementation/c/transform/index.ts
@@ -0,0 +1,11 @@
+import { ID } from './id';
+import { ToLower } from './to-lower';
+import { ToLowerUnsafe } from './to-lower-unsafe';
+
+export { Transform } from './base';
+
+export default {
+ ID,
+ ToLower,
+ ToLowerUnsafe,
+};
diff --git a/llparse/src/implementation/c/transform/to-lower-unsafe.ts b/llparse/src/implementation/c/transform/to-lower-unsafe.ts
new file mode 100644
index 0000000..27f608c
--- /dev/null
+++ b/llparse/src/implementation/c/transform/to-lower-unsafe.ts
@@ -0,0 +1,10 @@
+import * as frontend from 'llparse-frontend';
+
+import { Compilation } from '../compilation';
+import { Transform } from './base';
+
+export class ToLowerUnsafe extends Transform<frontend.transform.ToLowerUnsafe> {
+ public build(ctx: Compilation, value: string): string {
+ return `((${value}) | 0x20)`;
+ }
+}
diff --git a/llparse/src/implementation/c/transform/to-lower.ts b/llparse/src/implementation/c/transform/to-lower.ts
new file mode 100644
index 0000000..f639ef1
--- /dev/null
+++ b/llparse/src/implementation/c/transform/to-lower.ts
@@ -0,0 +1,11 @@
+import * as frontend from 'llparse-frontend';
+
+import { Compilation } from '../compilation';
+import { Transform } from './base';
+
+export class ToLower extends Transform<frontend.transform.ToLower> {
+ public build(ctx: Compilation, value: string): string {
+ return `((${value}) >= 'A' && (${value}) <= 'Z' ? ` +
+ `(${value} | 0x20) : (${value}))`;
+ }
+}
diff --git a/llparse/test/code-test.ts b/llparse/test/code-test.ts
new file mode 100644
index 0000000..54c3f85
--- /dev/null
+++ b/llparse/test/code-test.ts
@@ -0,0 +1,168 @@
+import * as assert from 'assert';
+
+import { LLParse } from '../src/api';
+
+import { build, NUM_SELECT, printMatch, printOff } from './fixtures';
+
+describe('llparse/code', () => {
+ let p: LLParse;
+
+ beforeEach(() => {
+ p = new LLParse();
+ });
+
+ describe('`.mulAdd()`', () => {
+ it('should operate normally', async () => {
+ const start = p.node('start');
+ const dot = p.node('dot');
+
+ p.property('i64', 'counter');
+
+ const is1337 = p.invoke(p.code.load('counter'), {
+ 1337: printOff(p, p.invoke(p.code.update('counter', 0), start)),
+ }, p.error(1, 'Invalid result'));
+
+ const count = p.invoke(p.code.mulAdd('counter', { base: 10 }), start);
+
+ start
+ .select(NUM_SELECT, count)
+ .otherwise(dot);
+
+ dot
+ .match('.', is1337)
+ .otherwise(p.error(1, 'Unexpected'));
+
+ const binary = await build(p, start, 'mul-add');
+ await binary.check('1337.', 'off=5\n');
+ });
+
+ it('should operate fail on overflow', async () => {
+ const start = p.node('start');
+
+ p.property('i8', 'counter');
+
+ const count = p.invoke(p.code.mulAdd('counter', { base: 10 }), {
+ 1: printOff(p, start),
+ }, start);
+
+ start
+ .select(NUM_SELECT, count)
+ .otherwise(p.error(1, 'Unexpected'));
+
+ const binary = await build(p, start, 'mul-add-overflow');
+ await binary.check('1111', 'off=4\n');
+ });
+
+ it('should operate fail on greater than max', async () => {
+ const start = p.node('start');
+
+ p.property('i64', 'counter');
+
+ const count = p.invoke(p.code.mulAdd('counter', {
+ base: 10,
+ max: 1000,
+ }), {
+ 1: printOff(p, start),
+ }, start);
+
+ start
+ .select(NUM_SELECT, count)
+ .otherwise(p.error(1, 'Unexpected'));
+
+ const binary = await build(p, start, 'mul-add-max-overflow');
+ await binary.check('1111', 'off=4\n');
+ });
+ });
+
+ describe('`.update()`', () => {
+ it('should operate normally', async () => {
+ const start = p.node('start');
+
+ p.property('i64', 'counter');
+
+ const update = p.invoke(p.code.update('counter', 42));
+
+ start
+ .skipTo(update);
+
+ update
+ .otherwise(p.invoke(p.code.load('counter'), {
+ 42: printOff(p, start),
+ }, p.error(1, 'Unexpected')));
+
+ const binary = await build(p, start, 'update');
+ await binary.check('.', 'off=1\n');
+ });
+ });
+
+ describe('`.isEqual()`', () => {
+ it('should operate normally', async () => {
+ const start = p.node('start');
+
+ p.property('i64', 'counter');
+
+ const check = p.invoke(p.code.isEqual('counter', 1), {
+ 0: printOff(p, start),
+ 1: start,
+ }, p.error(1, 'Unexpected'));
+
+ start
+ .select(NUM_SELECT, p.invoke(p.code.store('counter'), check))
+ .otherwise(p.error(1, 'Unexpected'));
+
+ const binary = await build(p, start, 'is-equal');
+ await binary.check('010', 'off=1\noff=3\n');
+ });
+ });
+
+ describe('`.or()`/`.and()`/`.test()`', () => {
+ it('should set and retrieve bits', async () => {
+ const start = p.node('start');
+ const test = p.node('test');
+
+ p.property('i64', 'flag');
+
+ start
+ .match('1', p.invoke(p.code.or('flag', 1), start))
+ .match('2', p.invoke(p.code.or('flag', 2), start))
+ .match('4', p.invoke(p.code.or('flag', 4), start))
+ // Reset
+ .match('r', p.invoke(p.code.update('flag', 0), start))
+ // Partial Reset
+ .match('p', p.invoke(p.code.and('flag', ~1), start))
+ // Test
+ .match('-', test)
+ .otherwise(p.error(1, 'start'));
+
+ test
+ .match('1', p.invoke(p.code.test('flag', 1), {
+ 0: test,
+ 1: printOff(p, test),
+ }, p.error(2, 'test-1')))
+ .match('2', p.invoke(p.code.test('flag', 2), {
+ 0: test,
+ 1: printOff(p, test),
+ }, p.error(3, 'test-2')))
+ .match('4', p.invoke(p.code.test('flag', 4), {
+ 0: test,
+ 1: printOff(p, test),
+ }, p.error(4, 'test-3')))
+ .match('7', p.invoke(p.code.test('flag', 7), {
+ 0: test,
+ 1: printOff(p, test),
+ }, p.error(5, 'test-7')))
+ // Restart
+ .match('.', start)
+ .otherwise(p.error(6, 'test'));
+
+ const binary = await build(p, start, 'or-test');
+ await binary.check('1-124.2-1247.4-1247.r4-124.r12p-12', [
+ 'off=3',
+ 'off=9', 'off=10',
+ 'off=16', 'off=17', 'off=18', 'off=19',
+ 'off=26',
+ 'off=34',
+ ]);
+ });
+ });
+});
diff --git a/llparse/test/compiler-test.ts b/llparse/test/compiler-test.ts
new file mode 100644
index 0000000..39bb69f
--- /dev/null
+++ b/llparse/test/compiler-test.ts
@@ -0,0 +1,289 @@
+import { LLParse } from '../src/api';
+
+import {
+ ALPHA, build, NUM, NUM_SELECT, printMatch, printOff,
+} from './fixtures';
+
+describe('llparse/Compiler', () => {
+ let p: LLParse;
+
+ beforeEach(() => {
+ p = new LLParse();
+ });
+
+ it('should compile simple parser', async () => {
+ const start = p.node('start');
+
+ start.match(' ', start);
+
+ start.match('HTTP', printOff(p, start));
+
+ start.select({
+ CONNECT: 6,
+ DELETE: 4,
+ GET: 1,
+ HEAD: 0,
+ OPTIONS: 5,
+ PATCH: 8,
+ POST: 2,
+ PUT: 3,
+ TRACE: 7,
+ }, printMatch(p, start));
+
+ start.otherwise(p.error(3, 'Invalid word'));
+
+ const binary = await build(p, start, 'simple');
+ await binary.check('GET', 'off=3 match=1\n');
+ });
+
+ it('should optimize shallow select', async () => {
+ const start = p.node('start');
+
+ start.select(NUM_SELECT, printMatch(p, start));
+
+ start.otherwise(p.error(3, 'Invalid word'));
+
+ const binary = await build(p, start, 'shallow');
+ await binary.check('012', 'off=1 match=0\noff=2 match=1\noff=3 match=2\n');
+ });
+
+ it('should support key-value select', async () => {
+ const start = p.node('start');
+
+ start.select('0', 0, printMatch(p, start));
+ start.select('1', 1, printMatch(p, start));
+ start.select('2', 2, printMatch(p, start));
+
+ start.otherwise(p.error(3, 'Invalid word'));
+
+ const binary = await build(p, start, 'kv-select');
+ await binary.check('012', 'off=1 match=0\noff=2 match=1\noff=3 match=2\n');
+ });
+
+ it('should support multi-match', async () => {
+ const start = p.node('start');
+
+ start.match([ ' ', '\t', '\r', '\n' ], start);
+
+ start.select({
+ A: 0,
+ B: 1,
+ }, printMatch(p, start));
+
+ start.otherwise(p.error(3, 'Invalid word'));
+
+ const binary = await build(p, start, 'multi-match');
+ await binary.check(
+ 'A B\t\tA\r\nA',
+ 'off=1 match=0\noff=3 match=1\noff=6 match=0\noff=9 match=0\n');
+ });
+
+ it('should support numeric-match', async () => {
+ const start = p.node('start');
+
+ start.match(32, start);
+
+ start.select({
+ A: 0,
+ B: 1,
+ }, printMatch(p, start));
+
+ start.otherwise(p.error(3, 'Invalid word'));
+
+ const binary = await build(p, start, 'multi-match');
+ await binary.check(
+ 'A B A A',
+ 'off=1 match=0\noff=3 match=1\noff=6 match=0\noff=9 match=0\n');
+ });
+
+ it('should support custom state properties', async () => {
+ const start = p.node('start');
+ const error = p.error(3, 'Invalid word');
+
+ p.property('i8', 'custom');
+
+ const second = p.invoke(p.code.load('custom'), {
+ 0: p.invoke(p.code.match('llparse__print_zero'), { 0: start }, error),
+ 1: p.invoke(p.code.match('llparse__print_one'), { 0: start }, error),
+ }, error);
+
+ start
+ .select({
+ 0: 0,
+ 1: 1,
+ }, p.invoke(p.code.store('custom'), second))
+ .otherwise(error);
+
+ const binary = await build(p, start, 'custom-prop');
+ await binary.check('0110', 'off=1 0\noff=2 1\noff=3 1\noff=4 0\n');
+ });
+
+ it('should return error code/reason', async () => {
+ const start = p.node('start');
+
+ start.match('a', start);
+ start.otherwise(p.error(42, 'some reason'));
+
+ const binary = await build(p, start, 'error');
+ await binary.check('aab', 'off=2 error code=42 reason="some reason"\n');
+ });
+
+ it('should not merge `.match()` with `.peek()`', async () => {
+ const maybeCr = p.node('maybeCr');
+ const lf = p.node('lf');
+
+ maybeCr.peek('\n', lf);
+ maybeCr.match('\r', lf);
+ maybeCr.otherwise(p.error(1, 'error'));
+
+ lf.match('\n', printOff(p, maybeCr));
+ lf.otherwise(p.error(2, 'error'));
+
+ const binary = await build(p, maybeCr, 'no-merge');
+ await binary.check('\r\n\n', 'off=2\noff=3\n');
+ });
+
+ describe('`.match()`', () => {
+ it('should compile to a single-bit table-lookup node', async () => {
+ const start = p.node('start');
+
+ start
+ .match(ALPHA, start)
+ .skipTo(printOff(p, start));
+
+ // TODO(indutny): validate compilation result?
+ const binary = await build(p, start, 'match-bit-check');
+ await binary.check('pecan.is.dead.', 'off=6\noff=9\noff=14\n');
+ });
+
+ it('should compile to a multi-bit table-lookup node', async () => {
+ const start = p.node('start');
+ const another = p.node('another');
+
+ start
+ .match(ALPHA, start)
+ .peek(NUM, another)
+ .skipTo(printOff(p, start));
+
+ another
+ .match(NUM, another)
+ .otherwise(start);
+
+ // TODO(indutny): validate compilation result?
+ const binary = await build(p, start, 'match-multi-bit-check');
+ await binary.check('pecan.135.is.dead.',
+ 'off=6\noff=10\noff=13\noff=18\n');
+ });
+
+ it('should not overflow on signed char in table-lookup node', async () => {
+ const start = p.node('start');
+
+ start
+ .match(ALPHA, start)
+ .match([ 0xc3, 0xbc ], start)
+ .skipTo(printOff(p, start));
+
+ // TODO(indutny): validate compilation result?
+ const binary = await build(p, start, 'match-bit-check');
+ await binary.check('Düsseldorf.', 'off=12\n');
+ });
+
+ it('should match single quotes and forward slashes', async () => {
+ const start = p.node('start');
+
+ start
+ .match('\'', printOff(p, start))
+ .match('\\', printOff(p, start))
+ .otherwise(p.error(3, 'Invalid char'));
+
+ // TODO(indutny): validate compilation result?
+ const binary = await build(p, start, 'escape-char');
+ await binary.check('\\\'', 'off=1\noff=2\n');
+ });
+
+ it('should hit SSE4.2 optimization for table-lookup', async () => {
+ const start = p.node('start');
+
+ start
+ .match(ALPHA, start)
+ .skipTo(printOff(p, start));
+
+ // TODO(indutny): validate compilation result?
+ const binary = await build(p, start, 'match-bit-check-sse');
+ await binary.check('abcdabcdabcdabcdabcdabcdabcd.abcd.',
+ 'off=29\noff=34\n');
+ });
+
+ it('should compile overlapping matches', async () => {
+ const start = p.node('start');
+
+ start.select({
+ aa: 1,
+ aab: 2,
+ }, printMatch(p, start));
+
+ start.otherwise(p.error(3, 'Invalid word'));
+
+ const binary = await build(p, start, 'overlapping-matches');
+ await binary.check('aaaabaa', 'off=2 match=1\noff=5 match=2\n');
+ });
+ });
+
+ describe('`.peek()`', () => {
+ it('should not advance position', async () => {
+ const start = p.node('start');
+ const ab = p.node('ab');
+ const error = p.error(3, 'Invalid word');
+
+ start
+ .peek([ 'a', 'b' ], ab)
+ .otherwise(error);
+
+ ab
+ .match([ 'a', 'b' ], printOff(p, start))
+ .otherwise(error);
+
+ const binary = await build(p, start, 'peek');
+ await binary.check('ab', 'off=1\noff=2\n');
+ });
+ });
+
+ describe('`.otherwise()`', () => {
+ it('should not advance position by default', async () => {
+ const a = p.node('a');
+ const b = p.node('b');
+
+ a
+ .match('A', a)
+ .otherwise(b);
+
+ b
+ .match('B', printOff(p, b))
+ .skipTo(a);
+
+ const binary = await build(p, a, 'otherwise-noadvance');
+ await binary.check('AABAB', 'off=3\noff=5\n');
+ });
+
+ it('should advance when it is `.skipTo()`', async () => {
+ const start = p.node('start');
+
+ start
+ .match(' ', printOff(p, start))
+ .skipTo(start);
+
+ const binary = await build(p, start, 'otherwise-skip');
+ await binary.check('HELLO WORLD', 'off=6\n');
+ });
+
+ it('should skip everything with `.skipTo()`', async () => {
+ const start = p.node('start');
+
+ start
+ .skipTo(start);
+
+ const binary = await build(p, start, 'all-skip');
+ await binary.check('HELLO WORLD', '\n');
+ });
+ });
+});
diff --git a/llparse/test/consume-test.ts b/llparse/test/consume-test.ts
new file mode 100644
index 0000000..f9fb383
--- /dev/null
+++ b/llparse/test/consume-test.ts
@@ -0,0 +1,69 @@
+import * as assert from 'assert';
+
+import { LLParse } from '../src/api';
+
+import { build, NUM_SELECT, printMatch, printOff } from './fixtures';
+
+describe('llparse/consume', () => {
+ let p: LLParse;
+
+ beforeEach(() => {
+ p = new LLParse();
+ });
+
+ it('should consume bytes with i8 field', async () => {
+ p.property('i8', 'to_consume');
+
+ const start = p.node('start');
+ const consume = p.consume('to_consume');
+
+ start.select(NUM_SELECT, p.invoke(p.code.store('to_consume'), consume));
+
+ start
+ .otherwise(p.error(1, 'unexpected'));
+
+ consume
+ .otherwise(printOff(p, start));
+
+ const binary = await build(p, start, 'consume');
+ await binary.check('3aaa2bb1a01b', 'off=4\noff=7\noff=9\noff=10\noff=12\n');
+ });
+
+ it('should consume bytes with i64 field', async () => {
+ p.property('i64', 'to_consume');
+
+ const start = p.node('start');
+ const consume = p.consume('to_consume');
+
+ start.select(NUM_SELECT, p.invoke(p.code.store('to_consume'), consume));
+
+ start
+ .otherwise(p.error(1, 'unexpected'));
+
+ consume
+ .otherwise(printOff(p, start));
+
+ const binary = await build(p, start, 'consume-i64');
+ await binary.check('3aaa2bb1a01b', 'off=4\noff=7\noff=9\noff=10\noff=12\n');
+ });
+
+ it('should consume bytes with untruncated i64 field', async () => {
+ p.property('i64', 'to_consume');
+
+ const start = p.node('start');
+ const consume = p.consume('to_consume');
+
+ start
+ .select(
+ NUM_SELECT,
+ p.invoke(p.code.mulAdd('to_consume', { base: 10 }), start)
+ )
+ .skipTo(consume);
+
+ consume
+ .otherwise(printOff(p, start));
+
+ const binary = await build(p, start, 'consume-untruncated-i64');
+ await binary.check('4294967297.xxxxxxxx', '\n');
+ });
+});
diff --git a/llparse/test/fixtures/extra.c b/llparse/test/fixtures/extra.c
new file mode 100644
index 0000000..79cdff9
--- /dev/null
+++ b/llparse/test/fixtures/extra.c
@@ -0,0 +1,84 @@
+#include "fixture.h"
+
+int llparse__print_zero(llparse_t* s, const char* p, const char* endp) {
+ if (llparse__in_bench)
+ return 0;
+ llparse__print(p, endp, "0");
+ return 0;
+}
+
+
+int llparse__print_one(llparse_t* s, const char* p, const char* endp) {
+ if (llparse__in_bench)
+ return 0;
+ llparse__print(p, endp, "1");
+ return 0;
+}
+
+
+int llparse__print_off(llparse_t* s, const char* p, const char* endp) {
+ if (llparse__in_bench)
+ return 0;
+ llparse__print(p, endp, "");
+ return 0;
+}
+
+
+int llparse__print_match(llparse_t* s, const char* p, const char* endp,
+ int value) {
+ if (llparse__in_bench)
+ return 0;
+ llparse__print(p, endp, "match=%d", value);
+ return 0;
+}
+
+
+int llparse__on_dot(llparse_t* s, const char* p, const char* endp) {
+ if (llparse__in_bench)
+ return 0;
+ return llparse__print_span("dot", p, endp);
+}
+
+
+int llparse__on_dash(llparse_t* s, const char* p, const char* endp) {
+ if (llparse__in_bench)
+ return 0;
+ return llparse__print_span("dash", p, endp);
+}
+
+
+int llparse__on_underscore(llparse_t* s, const char* p,
+ const char* endp) {
+ if (llparse__in_bench)
+ return 0;
+ return llparse__print_span("underscore", p, endp);
+}
+
+
+/* A span callback, really */
+int llparse__please_fail(llparse_t* s, const char* p, const char* endp) {
+ s->reason = "please fail";
+ if (llparse__in_bench)
+ return 1;
+ return 1;
+}
+
+
+/* A span callback, really */
+static int llparse__pause_once_counter;
+
+int llparse__pause_once(llparse_t* s, const char* p, const char* endp) {
+ if (!llparse__in_bench)
+ llparse__print_span("pause", p, endp);
+
+ if (llparse__pause_once_counter != 0)
+ return 0;
+ llparse__pause_once_counter = 1;
+
+ return LLPARSE__ERROR_PAUSE;
+}
+
+
+int llparse__test_init() {
+ llparse__pause_once_counter = 0;
+}
diff --git a/llparse/test/fixtures/index.ts b/llparse/test/fixtures/index.ts
new file mode 100644
index 0000000..d8a7336
--- /dev/null
+++ b/llparse/test/fixtures/index.ts
@@ -0,0 +1,52 @@
+import { source } from 'llparse-frontend';
+import { Fixture, FixtureResult } from 'llparse-test-fixture';
+import * as path from 'path';
+
+import { LLParse } from '../../src/api';
+
+export { ERROR_PAUSE } from 'llparse-test-fixture';
+
+const fixtures = new Fixture({
+ buildDir: path.join(__dirname, '..', 'tmp'),
+ extra: [
+ '-msse4.2',
+ '-DLLPARSE__TEST_INIT=llparse__test_init',
+ path.join(__dirname, 'extra.c'),
+ ],
+});
+
+export function build(llparse: LLParse, node: source.node.Node, outFile: string)
+ : Promise<FixtureResult> {
+ return fixtures.build(llparse.build(node, {
+ c: {
+ header: outFile,
+ },
+ }), outFile);
+}
+
+export function printMatch(p: LLParse, next: source.node.Node)
+ : source.node.Node {
+ const code = p.code.value('llparse__print_match');
+ const res = p.invoke(code, next);
+ return res;
+}
+
+export function printOff(p: LLParse, next: source.node.Node): source.node.Node {
+ const code = p.code.match('llparse__print_off');
+ return p.invoke(code, next);
+}
+
+export const NUM_SELECT: { readonly [key: string]: number } = {
+ 0: 0, 1: 1, 2: 2, 3: 3, 4: 4, 5: 5, 6: 6, 7: 7, 8: 8, 9: 9,
+};
+
+export const NUM: ReadonlyArray<string> = [
+ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
+];
+
+export const ALPHA: ReadonlyArray<string> = [
+ 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o',
+ 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
+ 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O',
+ 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
+];
diff --git a/llparse/test/resumption-test.ts b/llparse/test/resumption-test.ts
new file mode 100644
index 0000000..438b7fd
--- /dev/null
+++ b/llparse/test/resumption-test.ts
@@ -0,0 +1,55 @@
+import * as assert from 'assert';
+
+import { LLParse } from '../src/api';
+
+import { build, ERROR_PAUSE, printMatch, printOff } from './fixtures';
+
+describe('llparse/resumption', () => {
+ let p: LLParse;
+
+ beforeEach(() => {
+ p = new LLParse();
+ });
+
+ it('should resume after span end pause', async () => {
+ const start = p.node('start');
+ const a = p.node('a');
+ const span = p.span(p.code.span('llparse__pause_once'));
+
+ start
+ .peek('a', span.start(a))
+ .skipTo(start);
+
+ a
+ .match('a', a)
+ .otherwise(span.end(start));
+
+ const binary = await build(p, start, 'resume-span');
+
+ await binary.check('baaab',
+ new RegExp(
+ '^(' +
+ 'off=\\d+ pause\\noff=1 len=3 span\\[pause\\]="aaa"' +
+ '|' +
+ 'off=1 len=3 span\\[pause\\]="aaa"\noff=4 pause' +
+ ')\\n$'
+ , 'g'));
+ });
+
+ it('should resume after `pause` node', async () => {
+ const start = p.node('start');
+ const pause = p.pause(ERROR_PAUSE, 'paused');
+
+ start
+ .match('p', pause)
+ .skipTo(start);
+
+ pause
+ .otherwise(printOff(p, start));
+
+ const binary = await build(p, start, 'resume-pause');
+
+ await binary.check('..p....p..',
+ 'off=3 pause\noff=3\noff=8 pause\noff=8\n');
+ });
+});
diff --git a/llparse/test/span-test.ts b/llparse/test/span-test.ts
new file mode 100644
index 0000000..b01ad51
--- /dev/null
+++ b/llparse/test/span-test.ts
@@ -0,0 +1,107 @@
+import * as assert from 'assert';
+
+import { LLParse } from '../src/api';
+
+import { build, printMatch, printOff } from './fixtures';
+
+describe('llparse/spans', () => {
+ let p: LLParse;
+
+ beforeEach(() => {
+ p = new LLParse();
+ });
+
+ it('should invoke span callback', async () => {
+ const start = p.node('start');
+ const dot = p.node('dot');
+ const dash = p.node('dash');
+ const underscore = p.node('underscore');
+
+ const span = {
+ dash: p.span(p.code.span('llparse__on_dash')),
+ dot: p.span(p.code.span('llparse__on_dot')),
+ underscore: p.span(p.code.span('llparse__on_underscore')),
+ };
+
+ start.otherwise(span.dot.start(dot));
+
+ dot
+ .match('.', dot)
+ .peek('-', span.dash.start(dash))
+ .peek('_', span.underscore.start(underscore))
+ .skipTo(span.dot.end(start));
+
+ dash
+ .match('-', dash)
+ .otherwise(span.dash.end(dot));
+
+ underscore
+ .match('_', underscore)
+ .otherwise(span.underscore.end(dot));
+
+ const binary = await build(p, start, 'span');
+ await binary.check('..--..__..',
+ 'off=2 len=2 span[dash]="--"\n' +
+ 'off=6 len=2 span[underscore]="__"\n' +
+ 'off=0 len=10 span[dot]="..--..__.."\n');
+ });
+
+ it('should return error', async () => {
+ const start = p.node('start');
+ const dot = p.node('dot');
+
+ const span = {
+ pleaseFail: p.span(p.code.span('llparse__please_fail')),
+ };
+
+ start.otherwise(span.pleaseFail.start(dot));
+
+ dot
+ .match('.', dot)
+ .skipTo(span.pleaseFail.end(start));
+
+ const binary = await build(p, start, 'span-error');
+
+ await binary.check(
+ '....a',
+ /off=\d+ error code=1 reason="please fail"\n/);
+ });
+
+ it('should return error at `executeSpans()`', async () => {
+ const start = p.node('start');
+ const dot = p.node('dot');
+
+ const span = {
+ pleaseFail: p.span(p.code.span('llparse__please_fail')),
+ };
+
+ start.otherwise(span.pleaseFail.start(dot));
+
+ dot
+ .match('.', dot)
+ .skipTo(span.pleaseFail.end(start));
+
+ const binary = await build(p, start, 'span-error-execute');
+
+ await binary.check(
+ '.........',
+ /off=9 error code=1 reason="please fail"\n/, { scan: 100 });
+ });
+
+ it('should not invoke spurious span callback', async () => {
+ const start = p.node('start');
+ const dot = p.node('dot');
+ const span = p.span(p.code.span('llparse__on_dot'));
+
+ start
+ .match('hello', span.start(dot))
+ .skipTo(start);
+
+ dot
+ .match('.', dot)
+ .skipTo(span.end(start));
+
+ const binary = await build(p, start, 'span-spurious');
+ await binary.check('hello', [ '' ]);
+ });
+});
diff --git a/llparse/test/transform-test.ts b/llparse/test/transform-test.ts
new file mode 100644
index 0000000..d30381e
--- /dev/null
+++ b/llparse/test/transform-test.ts
@@ -0,0 +1,41 @@
+import * as assert from 'assert';
+
+import { LLParse } from '../src/api';
+
+import { build, printMatch, printOff } from './fixtures';
+
+describe('llparse/transform', () => {
+ let p: LLParse;
+
+ beforeEach(() => {
+ p = new LLParse();
+ });
+
+ it('should apply transformation before the match', async () => {
+ const start = p.node('start');
+
+ start
+ .transform(p.transform.toLowerUnsafe())
+ .match('connect', printOff(p, start))
+ .match('close', printOff(p, start))
+ .otherwise(p.error(1, 'error'));
+
+ const binary = await build(p, start, 'transform-lower');
+ await binary.check('connectCLOSEcOnNeCt', 'off=7\noff=12\noff=19\n');
+ });
+
+ it('should apply safe `toLower()` transformation', async () => {
+ const start = p.node('start');
+
+ start
+ .transform(p.transform.toLower())
+ .select({
+ 'a-b': 1,
+ 'a\rb': 2,
+ }, printMatch(p, start))
+ .otherwise(p.error(1, 'error'));
+
+ const binary = await build(p, start, 'transform-safe-lower');
+ await binary.check('A-ba\rB', 'off=3 match=1\noff=6 match=2\n');
+ });
+});
diff --git a/llparse/tsconfig.json b/llparse/tsconfig.json
new file mode 100644
index 0000000..01ec7c2
--- /dev/null
+++ b/llparse/tsconfig.json
@@ -0,0 +1,15 @@
+{
+ "compilerOptions": {
+ "strict": true,
+ "target": "es2017",
+ "module": "commonjs",
+ "moduleResolution": "node",
+ "outDir": "./lib",
+ "declaration": true,
+ "pretty": true,
+ "sourceMap": true
+ },
+ "include": [
+ "src/**/*.ts"
+ ]
+}
diff --git a/llparse/tslint.json b/llparse/tslint.json
new file mode 100644
index 0000000..24fec09
--- /dev/null
+++ b/llparse/tslint.json
@@ -0,0 +1,16 @@
+{
+ "defaultSeverity": "error",
+ "extends": [
+ "tslint:recommended"
+ ],
+ "jsRules": {},
+ "rules": {
+ "no-bitwise": null,
+ "max-line-length": [true, 80],
+ "max-classes-per-file": [true, 1, "exclude-class-expressions"],
+ "quotemark": [
+ true, "single", "avoid-escape", "avoid-template"
+ ]
+ },
+ "rulesDirectory": []
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..2b64daf
--- /dev/null
+++ b/package.json
@@ -0,0 +1,167 @@
+{
+ "name": "undici",
+ "version": "5.28.2",
+ "description": "An HTTP/1.1 client, written from scratch for Node.js",
+ "homepage": "https://undici.nodejs.org",
+ "bugs": {
+ "url": "https://github.com/nodejs/undici/issues"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/nodejs/undici.git"
+ },
+ "license": "MIT",
+ "contributors": [
+ {
+ "name": "Daniele Belardi",
+ "url": "https://github.com/dnlup",
+ "author": true
+ },
+ {
+ "name": "Ethan Arrowood",
+ "url": "https://github.com/ethan-arrowood",
+ "author": true
+ },
+ {
+ "name": "Matteo Collina",
+ "url": "https://github.com/mcollina",
+ "author": true
+ },
+ {
+ "name": "Matthew Aitken",
+ "url": "https://github.com/KhafraDev",
+ "author": true
+ },
+ {
+ "name": "Robert Nagy",
+ "url": "https://github.com/ronag",
+ "author": true
+ },
+ {
+ "name": "Szymon Marczak",
+ "url": "https://github.com/szmarczak",
+ "author": true
+ },
+ {
+ "name": "Tomas Della Vedova",
+ "url": "https://github.com/delvedor",
+ "author": true
+ }
+ ],
+ "keywords": [
+ "fetch",
+ "http",
+ "https",
+ "promise",
+ "request",
+ "curl",
+ "wget",
+ "xhr",
+ "whatwg"
+ ],
+ "main": "index.js",
+ "types": "index.d.ts",
+ "files": [
+ "*.d.ts",
+ "index.js",
+ "index-fetch.js",
+ "lib",
+ "types",
+ "docs"
+ ],
+ "scripts": {
+ "build:node": "npx esbuild@0.19.4 index-fetch.js --bundle --platform=node --outfile=undici-fetch.js --define:esbuildDetection=1 --keep-names",
+ "prebuild:wasm": "node build/wasm.js --prebuild",
+ "build:wasm": "node build/wasm.js --docker",
+ "lint": "standard | snazzy",
+ "lint:fix": "standard --fix | snazzy",
+ "test": "node scripts/generate-pem && npm run test:tap && npm run test:node-fetch && npm run test:fetch && npm run test:cookies && npm run test:wpt && npm run test:websocket && npm run test:jest && npm run test:typescript",
+ "test:cookies": "node scripts/verifyVersion 16 || tap test/cookie/*.js",
+ "test:node-fetch": "node scripts/verifyVersion.js 16 || mocha --exit test/node-fetch",
+ "test:fetch": "node scripts/verifyVersion.js 16 || (npm run build:node && tap --expose-gc test/fetch/*.js && tap test/webidl/*.js)",
+ "test:jest": "node scripts/verifyVersion.js 14 || jest",
+ "test:tap": "tap test/*.js test/diagnostics-channel/*.js",
+ "test:tdd": "tap test/*.js test/diagnostics-channel/*.js -w",
+ "test:typescript": "node scripts/verifyVersion.js 14 || tsd && tsc --skipLibCheck test/imports/undici-import.ts",
+ "test:websocket": "node scripts/verifyVersion.js 18 || tap test/websocket/*.js",
+ "test:wpt": "node scripts/verifyVersion 18 || (node test/wpt/start-fetch.mjs && node test/wpt/start-FileAPI.mjs && node test/wpt/start-mimesniff.mjs && node test/wpt/start-xhr.mjs && node test/wpt/start-websockets.mjs)",
+ "coverage": "nyc --reporter=text --reporter=html npm run test",
+ "coverage:ci": "nyc --reporter=lcov npm run test",
+ "bench": "PORT=3042 concurrently -k -s first npm:bench:server npm:bench:run",
+ "bench:server": "node benchmarks/server.js",
+ "prebench:run": "node benchmarks/wait.js",
+ "bench:run": "CONNECTIONS=1 node benchmarks/benchmark.js; CONNECTIONS=50 node benchmarks/benchmark.js",
+ "serve:website": "docsify serve .",
+ "prepare": "husky install",
+ "fuzz": "jsfuzz test/fuzzing/fuzz.js corpus"
+ },
+ "devDependencies": {
+ "@sinonjs/fake-timers": "^11.1.0",
+ "@types/node": "^18.0.3",
+ "abort-controller": "^3.0.0",
+ "atomic-sleep": "^1.0.0",
+ "chai": "^4.3.4",
+ "chai-as-promised": "^7.1.1",
+ "chai-iterator": "^3.0.2",
+ "chai-string": "^1.5.0",
+ "concurrently": "^8.0.1",
+ "cronometro": "^1.0.5",
+ "delay": "^5.0.0",
+ "dns-packet": "^5.4.0",
+ "docsify-cli": "^4.4.3",
+ "form-data": "^4.0.0",
+ "formdata-node": "^6.0.3",
+ "https-pem": "^3.0.0",
+ "husky": "^8.0.1",
+ "import-fresh": "^3.3.0",
+ "jest": "^29.0.2",
+ "jsdom": "^23.0.0",
+ "jsfuzz": "^1.0.15",
+ "mocha": "^10.0.0",
+ "mockttp": "^3.9.2",
+ "p-timeout": "^3.2.0",
+ "pre-commit": "^1.2.2",
+ "proxy": "^1.0.2",
+ "proxyquire": "^2.1.3",
+ "semver": "^7.5.4",
+ "sinon": "^17.0.1",
+ "snazzy": "^9.0.0",
+ "standard": "^17.0.0",
+ "table": "^6.8.0",
+ "tap": "^16.1.0",
+ "tsd": "^0.29.0",
+ "typescript": "^5.0.2",
+ "wait-on": "^7.0.1",
+ "ws": "^8.11.0"
+ },
+ "engines": {
+ "node": ">=14.0"
+ },
+ "standard": {
+ "env": [
+ "mocha"
+ ],
+ "ignore": [
+ "lib/llhttp/constants.js",
+ "lib/llhttp/utils.js",
+ "test/wpt/tests"
+ ]
+ },
+ "tsd": {
+ "directory": "test/types",
+ "compilerOptions": {
+ "esModuleInterop": true,
+ "lib": [
+ "esnext"
+ ]
+ }
+ },
+ "jest": {
+ "testMatch": [
+ "<rootDir>/test/jest/**"
+ ]
+ },
+ "dependencies": {
+ "@fastify/busboy": "^2.0.0"
+ }
+}
diff --git a/scripts/generate-pem.js b/scripts/generate-pem.js
new file mode 100644
index 0000000..0d7e628
--- /dev/null
+++ b/scripts/generate-pem.js
@@ -0,0 +1,3 @@
+/* istanbul ignore file */
+
+require('https-pem/install')
diff --git a/scripts/generate-undici-types-package-json.js b/scripts/generate-undici-types-package-json.js
new file mode 100644
index 0000000..78095ae
--- /dev/null
+++ b/scripts/generate-undici-types-package-json.js
@@ -0,0 +1,28 @@
+const fs = require('node:fs')
+const path = require('node:path')
+
+const packageJSONPath = path.join(__dirname, '..', 'package.json')
+const packageJSONRaw = fs.readFileSync(packageJSONPath, 'utf-8')
+const packageJSON = JSON.parse(packageJSONRaw)
+
+const licensePath = path.join(__dirname, '..', 'LICENSE')
+const licenseRaw = fs.readFileSync(licensePath, 'utf-8')
+
+const packageTypesJSON = {
+ name: 'undici-types',
+ version: packageJSON.version,
+ description: 'A stand-alone types package for Undici',
+ homepage: packageJSON.homepage,
+ bugs: packageJSON.bugs,
+ repository: packageJSON.repository,
+ license: packageJSON.license,
+ types: 'index.d.ts',
+ files: ['*.d.ts'],
+ contributors: packageJSON.contributors
+}
+
+const packageTypesPath = path.join(__dirname, '..', 'types', 'package.json')
+const licenseTypesPath = path.join(__dirname, '..', 'types', 'LICENSE')
+
+fs.writeFileSync(packageTypesPath, JSON.stringify(packageTypesJSON, null, 2))
+fs.writeFileSync(licenseTypesPath, licenseRaw)
diff --git a/scripts/verifyVersion.js b/scripts/verifyVersion.js
new file mode 100644
index 0000000..8ad2d19
--- /dev/null
+++ b/scripts/verifyVersion.js
@@ -0,0 +1,15 @@
+/* istanbul ignore file */
+
+const [major, minor, patch] = process.versions.node.split('.').map(v => Number(v))
+const required = process.argv.pop().split('.').map(v => Number(v))
+
+const badMajor = major < required[0]
+const badMinor = major === required[0] && minor < required[1]
+const badPatch = major === required[0] && minor === required[1] && patch < required[2]
+
+if (badMajor || badMinor || badPatch) {
+ console.log(`Required Node.js >=${required.join('.')}, got ${process.versions.node}`)
+ console.log('Skipping')
+} else {
+ process.exit(1)
+}
diff --git a/test/abort-controller.js b/test/abort-controller.js
new file mode 100644
index 0000000..4658686
--- /dev/null
+++ b/test/abort-controller.js
@@ -0,0 +1,238 @@
+'use strict'
+
+const { test } = require('tap')
+const { AbortController: NPMAbortController } = require('abort-controller')
+const { Client, errors } = require('..')
+const { createServer } = require('http')
+const { createReadStream } = require('fs')
+const { wrapWithAsyncIterable } = require('./utils/async-iterators')
+
+const controllers = [{
+ AbortControllerImpl: NPMAbortController,
+ controllerName: 'npm-abortcontroller-shim'
+}]
+
+if (global.AbortController) {
+ controllers.push({
+ AbortControllerImpl: global.AbortController,
+ controllerName: 'native-abortcontroller'
+ })
+}
+for (const { AbortControllerImpl, controllerName } of controllers) {
+ test(`Abort ${controllerName} before creating request`, (t) => {
+ t.plan(1)
+
+ const server = createServer((req, res) => {
+ t.fail()
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ const abortController = new AbortControllerImpl()
+ t.teardown(client.destroy.bind(client))
+
+ abortController.abort()
+
+ client.request({ path: '/', method: 'GET', signal: abortController.signal }, (err, response) => {
+ t.type(err, errors.RequestAbortedError)
+ })
+ })
+ })
+
+ test(`Abort ${controllerName} before sending request (no body)`, (t) => {
+ t.plan(3)
+
+ let count = 0
+ const server = createServer((req, res) => {
+ if (count === 1) {
+ t.fail('The second request should never be executed')
+ }
+ count += 1
+ res.end('hello')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ const abortController = new AbortControllerImpl()
+ t.teardown(client.destroy.bind(client))
+
+ client.request({ path: '/', method: 'GET' }, (err, response) => {
+ t.error(err)
+ const bufs = []
+ response.body.on('data', (buf) => {
+ bufs.push(buf)
+ })
+ response.body.on('end', () => {
+ t.equal('hello', Buffer.concat(bufs).toString('utf8'))
+ })
+ })
+
+ client.request({ path: '/', method: 'GET', signal: abortController.signal }, (err, response) => {
+ t.type(err, errors.RequestAbortedError)
+ })
+
+ abortController.abort()
+ })
+ })
+
+ test(`Abort ${controllerName} while waiting response (no body)`, (t) => {
+ t.plan(1)
+
+ const abortController = new AbortControllerImpl()
+ const server = createServer((req, res) => {
+ abortController.abort()
+ res.setHeader('content-type', 'text/plain')
+ res.end('hello world')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ client.request({ path: '/', method: 'GET', signal: abortController.signal }, (err, response) => {
+ t.type(err, errors.RequestAbortedError)
+ })
+ })
+ })
+
+ test(`Abort ${controllerName} while waiting response (write headers started) (no body)`, (t) => {
+ t.plan(1)
+
+ const abortController = new AbortControllerImpl()
+ const server = createServer((req, res) => {
+ res.writeHead(200, { 'content-type': 'text/plain' })
+ res.flushHeaders()
+ abortController.abort()
+ res.end('hello world')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ client.request({ path: '/', method: 'GET', signal: abortController.signal }, (err, response) => {
+ t.type(err, errors.RequestAbortedError)
+ })
+ })
+ })
+
+ test(`Abort ${controllerName} while waiting response (write headers and write body started) (no body)`, (t) => {
+ t.plan(2)
+
+ const abortController = new AbortControllerImpl()
+ const server = createServer((req, res) => {
+ res.writeHead(200, { 'content-type': 'text/plain' })
+ res.write('hello')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ client.request({ path: '/', method: 'GET', signal: abortController.signal }, (err, response) => {
+ t.error(err)
+ response.body.on('data', () => {
+ abortController.abort()
+ })
+ response.body.on('error', err => {
+ t.type(err, errors.RequestAbortedError)
+ })
+ })
+ })
+ })
+
+ function waitingWithBody (body, type) { // eslint-disable-line
+ test(`Abort ${controllerName} while waiting response (with body ${type})`, (t) => {
+ t.plan(1)
+
+ const abortController = new AbortControllerImpl()
+ const server = createServer((req, res) => {
+ abortController.abort()
+ res.setHeader('content-type', 'text/plain')
+ res.end('hello world')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ client.request({ path: '/', method: 'POST', body, signal: abortController.signal }, (err, response) => {
+ t.type(err, errors.RequestAbortedError)
+ })
+ })
+ })
+ }
+
+ waitingWithBody('hello', 'string')
+ waitingWithBody(createReadStream(__filename), 'stream')
+ waitingWithBody(new Uint8Array([42]), 'Uint8Array')
+ waitingWithBody(wrapWithAsyncIterable(createReadStream(__filename)), 'async-iterator')
+
+ function writeHeadersStartedWithBody (body, type) { // eslint-disable-line
+ test(`Abort ${controllerName} while waiting response (write headers started) (with body ${type})`, (t) => {
+ t.plan(1)
+
+ const abortController = new AbortControllerImpl()
+ const server = createServer((req, res) => {
+ res.writeHead(200, { 'content-type': 'text/plain' })
+ res.flushHeaders()
+ abortController.abort()
+ res.end('hello world')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ client.request({ path: '/', method: 'POST', body, signal: abortController.signal }, (err, response) => {
+ t.type(err, errors.RequestAbortedError)
+ })
+ })
+ })
+ }
+
+ writeHeadersStartedWithBody('hello', 'string')
+ writeHeadersStartedWithBody(createReadStream(__filename), 'stream')
+ writeHeadersStartedWithBody(new Uint8Array([42]), 'Uint8Array')
+ writeHeadersStartedWithBody(wrapWithAsyncIterable(createReadStream(__filename)), 'async-iterator')
+
+ function writeBodyStartedWithBody (body, type) { // eslint-disable-line
+ test(`Abort ${controllerName} while waiting response (write headers and write body started) (with body ${type})`, (t) => {
+ t.plan(2)
+
+ const abortController = new AbortControllerImpl()
+ const server = createServer((req, res) => {
+ res.writeHead(200, { 'content-type': 'text/plain' })
+ res.write('hello')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ client.request({ path: '/', method: 'POST', body, signal: abortController.signal }, (err, response) => {
+ t.error(err)
+ response.body.on('data', () => {
+ abortController.abort()
+ })
+ response.body.on('error', err => {
+ t.type(err, errors.RequestAbortedError)
+ })
+ })
+ })
+ })
+ }
+
+ writeBodyStartedWithBody('hello', 'string')
+ writeBodyStartedWithBody(createReadStream(__filename), 'stream')
+ writeBodyStartedWithBody(new Uint8Array([42]), 'Uint8Array')
+ writeBodyStartedWithBody(wrapWithAsyncIterable(createReadStream(__filename), 'async-iterator'))
+}
diff --git a/test/abort-event-emitter.js b/test/abort-event-emitter.js
new file mode 100644
index 0000000..a5397e4
--- /dev/null
+++ b/test/abort-event-emitter.js
@@ -0,0 +1,259 @@
+'use strict'
+
+const { test } = require('tap')
+const EventEmitter = require('events')
+const { Client, errors } = require('..')
+const { createServer } = require('http')
+const { createReadStream } = require('fs')
+const { Readable } = require('stream')
+const { wrapWithAsyncIterable } = require('./utils/async-iterators')
+
+test('Abort before sending request (no body)', (t) => {
+ t.plan(4)
+
+ let count = 0
+ const server = createServer((req, res) => {
+ if (count === 1) {
+ t.fail('The second request should never be executed')
+ }
+ count += 1
+ res.end('hello')
+ })
+
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ const ee = new EventEmitter()
+ t.teardown(client.destroy.bind(client))
+
+ client.request({ path: '/', method: 'GET' }, (err, response) => {
+ t.error(err)
+ const bufs = []
+ response.body.on('data', (buf) => {
+ bufs.push(buf)
+ })
+ response.body.on('end', () => {
+ t.equal('hello', Buffer.concat(bufs).toString('utf8'))
+ })
+ })
+
+ const body = new Readable({ read () { } })
+ body.on('error', (err) => {
+ t.type(err, errors.RequestAbortedError)
+ })
+ client.request({
+ path: '/',
+ method: 'GET',
+ signal: ee,
+ body
+ }, (err, response) => {
+ t.type(err, errors.RequestAbortedError)
+ })
+
+ ee.emit('abort')
+ })
+})
+
+test('Abort before sending request (no body) async iterator', (t) => {
+ t.plan(3)
+
+ let count = 0
+ const server = createServer((req, res) => {
+ if (count === 1) {
+ t.fail('The second request should never be executed')
+ }
+ count += 1
+ res.end('hello')
+ })
+
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ const ee = new EventEmitter()
+ t.teardown(client.destroy.bind(client))
+
+ client.request({ path: '/', method: 'GET' }, (err, response) => {
+ t.error(err)
+ const bufs = []
+ response.body.on('data', (buf) => {
+ bufs.push(buf)
+ })
+ response.body.on('end', () => {
+ t.equal('hello', Buffer.concat(bufs).toString('utf8'))
+ })
+ })
+
+ const body = wrapWithAsyncIterable(new Readable({ read () { } }))
+ client.request({
+ path: '/',
+ method: 'GET',
+ signal: ee,
+ body
+ }, (err, response) => {
+ t.type(err, errors.RequestAbortedError)
+ })
+
+ ee.emit('abort')
+ })
+})
+
+test('Abort while waiting response (no body)', (t) => {
+ t.plan(1)
+
+ const ee = new EventEmitter()
+ const server = createServer((req, res) => {
+ ee.emit('abort')
+ res.setHeader('content-type', 'text/plain')
+ res.end('hello world')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ client.request({ path: '/', method: 'GET', signal: ee }, (err, response) => {
+ t.type(err, errors.RequestAbortedError)
+ })
+ })
+})
+
+test('Abort while waiting response (write headers started) (no body)', (t) => {
+ t.plan(1)
+
+ const ee = new EventEmitter()
+ const server = createServer((req, res) => {
+ res.writeHead(200, { 'content-type': 'text/plain' })
+ res.flushHeaders()
+ ee.emit('abort')
+ res.end('hello world')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ client.request({ path: '/', method: 'GET', signal: ee }, (err, response) => {
+ t.type(err, errors.RequestAbortedError)
+ })
+ })
+})
+
+test('Abort while waiting response (write headers and write body started) (no body)', (t) => {
+ t.plan(2)
+
+ const ee = new EventEmitter()
+ const server = createServer((req, res) => {
+ res.writeHead(200, { 'content-type': 'text/plain' })
+ res.write('hello')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ client.request({ path: '/', method: 'GET', signal: ee }, (err, response) => {
+ t.error(err)
+ response.body.on('data', () => {
+ ee.emit('abort')
+ })
+ response.body.on('error', err => {
+ t.type(err, errors.RequestAbortedError)
+ })
+ })
+ })
+})
+
+function waitingWithBody (body, type) {
+ test(`Abort while waiting response (with body ${type})`, (t) => {
+ t.plan(1)
+
+ const ee = new EventEmitter()
+ const server = createServer((req, res) => {
+ ee.emit('abort')
+ res.setHeader('content-type', 'text/plain')
+ res.end('hello world')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ client.request({ path: '/', method: 'POST', body, signal: ee }, (err, response) => {
+ t.type(err, errors.RequestAbortedError)
+ })
+ })
+ })
+}
+
+waitingWithBody('hello', 'string')
+waitingWithBody(createReadStream(__filename), 'stream')
+waitingWithBody(new Uint8Array([42]), 'Uint8Array')
+waitingWithBody(wrapWithAsyncIterable(createReadStream(__filename)), 'async-iterator')
+
+function writeHeadersStartedWithBody (body, type) {
+ test(`Abort while waiting response (write headers started) (with body ${type})`, (t) => {
+ t.plan(1)
+
+ const ee = new EventEmitter()
+ const server = createServer((req, res) => {
+ res.writeHead(200, { 'content-type': 'text/plain' })
+ res.flushHeaders()
+ ee.emit('abort')
+ res.end('hello world')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ client.request({ path: '/', method: 'POST', body, signal: ee }, (err, response) => {
+ t.type(err, errors.RequestAbortedError)
+ })
+ })
+ })
+}
+
+writeHeadersStartedWithBody('hello', 'string')
+writeHeadersStartedWithBody(createReadStream(__filename), 'stream')
+writeHeadersStartedWithBody(new Uint8Array([42]), 'Uint8Array')
+writeHeadersStartedWithBody(wrapWithAsyncIterable(createReadStream(__filename)), 'async-iterator')
+
+function writeBodyStartedWithBody (body, type) {
+ test(`Abort while waiting response (write headers and write body started) (with body ${type})`, (t) => {
+ t.plan(2)
+
+ const ee = new EventEmitter()
+ const server = createServer((req, res) => {
+ res.writeHead(200, { 'content-type': 'text/plain' })
+ res.write('hello')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ client.request({ path: '/', method: 'POST', body, signal: ee }, (err, response) => {
+ t.error(err)
+ response.body.on('data', () => {
+ ee.emit('abort')
+ })
+ response.body.on('error', err => {
+ t.type(err, errors.RequestAbortedError)
+ })
+ })
+ })
+ })
+}
+
+writeBodyStartedWithBody('hello', 'string')
+writeBodyStartedWithBody(createReadStream(__filename), 'stream')
+writeBodyStartedWithBody(new Uint8Array([42]), 'Uint8Array')
+writeBodyStartedWithBody(wrapWithAsyncIterable(createReadStream(__filename)), 'async-iterator')
diff --git a/test/agent.js b/test/agent.js
new file mode 100644
index 0000000..65afd8b
--- /dev/null
+++ b/test/agent.js
@@ -0,0 +1,782 @@
+'use strict'
+
+const { test, teardown } = require('tap')
+const http = require('http')
+const { PassThrough } = require('stream')
+const { kRunning } = require('../lib/core/symbols')
+const {
+ Agent,
+ errors,
+ request,
+ stream,
+ pipeline,
+ Pool,
+ setGlobalDispatcher,
+ getGlobalDispatcher
+} = require('../')
+const importFresh = require('import-fresh')
+
+test('setGlobalDispatcher', t => {
+ t.plan(2)
+
+ t.test('fails if agent does not implement `get` method', t => {
+ t.plan(1)
+ t.throws(() => setGlobalDispatcher({ dispatch: 'not a function' }), errors.InvalidArgumentError)
+ })
+
+ t.test('sets global agent', t => {
+ t.plan(2)
+ t.doesNotThrow(() => setGlobalDispatcher(new Agent()))
+ t.doesNotThrow(() => setGlobalDispatcher({ dispatch: () => {} }))
+ })
+
+ t.teardown(() => {
+ // reset globalAgent to a fresh Agent instance for later tests
+ setGlobalDispatcher(new Agent())
+ })
+})
+
+test('Agent', t => {
+ t.plan(1)
+
+ t.doesNotThrow(() => new Agent())
+})
+
+test('agent should call callback after closing internal pools', t => {
+ t.plan(2)
+
+ const wanted = 'payload'
+
+ const server = http.createServer((req, res) => {
+ res.setHeader('Content-Type', 'text/plain')
+ res.end(wanted)
+ })
+
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const dispatcher = new Agent()
+
+ const origin = `http://localhost:${server.address().port}`
+
+ request(origin, { dispatcher })
+ .then(() => {
+ t.pass('first request should resolve')
+ })
+ .catch(err => {
+ t.fail(err)
+ })
+
+ dispatcher.once('connect', () => {
+ dispatcher.close(() => {
+ request(origin, { dispatcher })
+ .then(() => {
+ t.fail('second request should not resolve')
+ })
+ .catch(err => {
+ t.type(err, errors.ClientDestroyedError)
+ })
+ })
+ })
+ })
+})
+
+test('agent close throws when callback is not a function', t => {
+ t.plan(1)
+ const dispatcher = new Agent()
+ try {
+ dispatcher.close({})
+ } catch (err) {
+ t.type(err, errors.InvalidArgumentError)
+ }
+})
+
+test('agent should close internal pools', t => {
+ t.plan(2)
+
+ const wanted = 'payload'
+
+ const server = http.createServer((req, res) => {
+ res.setHeader('Content-Type', 'text/plain')
+ res.end(wanted)
+ })
+
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const dispatcher = new Agent()
+
+ const origin = `http://localhost:${server.address().port}`
+
+ request(origin, { dispatcher })
+ .then(() => {
+ t.pass('first request should resolve')
+ })
+ .catch(err => {
+ t.fail(err)
+ })
+
+ dispatcher.once('connect', () => {
+ dispatcher.close()
+ .then(() => request(origin, { dispatcher }))
+ .then(() => {
+ t.fail('second request should not resolve')
+ })
+ .catch(err => {
+ t.type(err, errors.ClientDestroyedError)
+ })
+ })
+ })
+})
+
+test('agent should destroy internal pools and call callback', t => {
+ t.plan(2)
+
+ const wanted = 'payload'
+
+ const server = http.createServer((req, res) => {
+ res.setHeader('Content-Type', 'text/plain')
+ res.end(wanted)
+ })
+
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const dispatcher = new Agent()
+
+ const origin = `http://localhost:${server.address().port}`
+
+ request(origin, { dispatcher })
+ .then(() => {
+ t.fail()
+ })
+ .catch(err => {
+ t.type(err, errors.ClientDestroyedError)
+ })
+
+ dispatcher.once('connect', () => {
+ dispatcher.destroy(() => {
+ request(origin, { dispatcher })
+ .then(() => {
+ t.fail()
+ })
+ .catch(err => {
+ t.type(err, errors.ClientDestroyedError)
+ })
+ })
+ })
+ })
+})
+
+test('agent destroy throws when callback is not a function', t => {
+ t.plan(1)
+ const dispatcher = new Agent()
+ try {
+ dispatcher.destroy(new Error('mock error'), {})
+ } catch (err) {
+ t.type(err, errors.InvalidArgumentError)
+ }
+})
+
+test('agent close/destroy callback with error', t => {
+ t.plan(4)
+ const dispatcher = new Agent()
+ t.equal(dispatcher.closed, false)
+ dispatcher.close()
+ t.equal(dispatcher.closed, true)
+ t.equal(dispatcher.destroyed, false)
+ dispatcher.destroy(new Error('mock error'))
+ t.equal(dispatcher.destroyed, true)
+})
+
+test('agent should destroy internal pools', t => {
+ t.plan(2)
+
+ const wanted = 'payload'
+
+ const server = http.createServer((req, res) => {
+ res.setHeader('Content-Type', 'text/plain')
+ res.end(wanted)
+ })
+
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const dispatcher = new Agent()
+
+ const origin = `http://localhost:${server.address().port}`
+
+ request(origin, { dispatcher })
+ .then(() => {
+ t.fail()
+ })
+ .catch(err => {
+ t.type(err, errors.ClientDestroyedError)
+ })
+
+ dispatcher.once('connect', () => {
+ dispatcher.destroy()
+ .then(() => request(origin, { dispatcher }))
+ .then(() => {
+ t.fail()
+ })
+ .catch(err => {
+ t.type(err, errors.ClientDestroyedError)
+ })
+ })
+ })
+})
+
+test('multiple connections', t => {
+ const connections = 3
+ t.plan(6 * connections)
+
+ const server = http.createServer((req, res) => {
+ res.writeHead(200, {
+ Connection: 'keep-alive',
+ 'Keep-Alive': 'timeout=1s'
+ })
+ res.end('ok')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, async () => {
+ const origin = `http://localhost:${server.address().port}`
+ const dispatcher = new Agent({ connections })
+
+ t.teardown(dispatcher.close.bind(dispatcher))
+
+ dispatcher.on('connect', (origin, [dispatcher]) => {
+ t.ok(dispatcher)
+ })
+ dispatcher.on('disconnect', (origin, [dispatcher], error) => {
+ t.ok(dispatcher)
+ t.type(error, errors.InformationalError)
+ t.equal(error.code, 'UND_ERR_INFO')
+ t.equal(error.message, 'reset')
+ })
+
+ for (let i = 0; i < connections; i++) {
+ await request(origin, { dispatcher })
+ .then(() => {
+ t.pass('should pass')
+ })
+ .catch(err => {
+ t.fail(err)
+ })
+ }
+ })
+})
+
+test('agent factory supports URL parameter', (t) => {
+ t.plan(2)
+
+ const noopHandler = {
+ onConnect () {},
+ onHeaders () {},
+ onData () {},
+ onComplete () {
+ server.close()
+ },
+ onError (err) {
+ throw err
+ }
+ }
+
+ const dispatcher = new Agent({
+ factory: (origin, opts) => {
+ t.ok(origin instanceof URL)
+ return new Pool(origin, opts)
+ }
+ })
+
+ const server = http.createServer((req, res) => {
+ res.setHeader('Content-Type', 'text/plain')
+ res.end('asd')
+ })
+
+ server.listen(0, () => {
+ t.doesNotThrow(() => dispatcher.dispatch({
+ origin: new URL(`http://localhost:${server.address().port}`),
+ path: '/',
+ method: 'GET'
+ }, noopHandler))
+ })
+})
+
+test('agent factory supports string parameter', (t) => {
+ t.plan(2)
+
+ const noopHandler = {
+ onConnect () {},
+ onHeaders () {},
+ onData () {},
+ onComplete () {
+ server.close()
+ },
+ onError (err) {
+ throw err
+ }
+ }
+
+ const dispatcher = new Agent({
+ factory: (origin, opts) => {
+ t.ok(typeof origin === 'string')
+ return new Pool(origin, opts)
+ }
+ })
+
+ const server = http.createServer((req, res) => {
+ res.setHeader('Content-Type', 'text/plain')
+ res.end('asd')
+ })
+
+ server.listen(0, () => {
+ t.doesNotThrow(() => dispatcher.dispatch({
+ origin: `http://localhost:${server.address().port}`,
+ path: '/',
+ method: 'GET'
+ }, noopHandler))
+ })
+})
+
+test('with globalAgent', t => {
+ t.plan(6)
+ const wanted = 'payload'
+
+ const server = http.createServer((req, res) => {
+ t.equal('/', req.url)
+ t.equal('GET', req.method)
+ t.equal(`localhost:${server.address().port}`, req.headers.host)
+ res.setHeader('Content-Type', 'text/plain')
+ res.end(wanted)
+ })
+
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ request(`http://localhost:${server.address().port}`)
+ .then(({ statusCode, headers, body }) => {
+ t.equal(statusCode, 200)
+ t.equal(headers['content-type'], 'text/plain')
+ const bufs = []
+ body.on('data', (buf) => {
+ bufs.push(buf)
+ })
+ body.on('end', () => {
+ t.equal(wanted, Buffer.concat(bufs).toString('utf8'))
+ })
+ })
+ .catch(err => {
+ t.fail(err)
+ })
+ })
+})
+
+test('with local agent', t => {
+ t.plan(6)
+ const wanted = 'payload'
+
+ const server = http.createServer((req, res) => {
+ t.equal('/', req.url)
+ t.equal('GET', req.method)
+ t.equal(`localhost:${server.address().port}`, req.headers.host)
+ res.setHeader('Content-Type', 'text/plain')
+ res.end(wanted)
+ })
+
+ t.teardown(server.close.bind(server))
+
+ const dispatcher = new Agent({
+ connect: {
+ servername: 'agent1'
+ }
+ })
+
+ server.listen(0, () => {
+ request(`http://localhost:${server.address().port}`, { dispatcher })
+ .then(({ statusCode, headers, body }) => {
+ t.equal(statusCode, 200)
+ t.equal(headers['content-type'], 'text/plain')
+ const bufs = []
+ body.on('data', (buf) => {
+ bufs.push(buf)
+ })
+ body.on('end', () => {
+ t.equal(wanted, Buffer.concat(bufs).toString('utf8'))
+ })
+ })
+ .catch(err => {
+ t.fail(err)
+ })
+ })
+})
+
+test('fails with invalid args', t => {
+ t.throws(() => request(), errors.InvalidArgumentError, 'throws on missing url argument')
+ t.throws(() => request(''), errors.InvalidArgumentError, 'throws on invalid url')
+ t.throws(() => request({}), errors.InvalidArgumentError, 'throws on missing url.origin argument')
+ t.throws(() => request({ origin: '' }), errors.InvalidArgumentError, 'throws on invalid url.origin argument')
+ t.throws(() => request('https://example.com', { path: 0 }), errors.InvalidArgumentError, 'throws on opts.path argument')
+ t.throws(() => request('https://example.com', { agent: new Agent() }), errors.InvalidArgumentError, 'throws on opts.path argument')
+ t.throws(() => request('https://example.com', 'asd'), errors.InvalidArgumentError, 'throws on non object opts argument')
+ t.end()
+})
+
+test('with globalAgent', t => {
+ t.plan(6)
+ const wanted = 'payload'
+
+ const server = http.createServer((req, res) => {
+ t.equal('/', req.url)
+ t.equal('GET', req.method)
+ t.equal(`localhost:${server.address().port}`, req.headers.host)
+ res.setHeader('Content-Type', 'text/plain')
+ res.end(wanted)
+ })
+
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ stream(
+ `http://localhost:${server.address().port}`,
+ {
+ opaque: new PassThrough()
+ },
+ ({ statusCode, headers, opaque: pt }) => {
+ t.equal(statusCode, 200)
+ t.equal(headers['content-type'], 'text/plain')
+ const bufs = []
+ pt.on('data', (buf) => {
+ bufs.push(buf)
+ })
+ pt.on('end', () => {
+ t.equal(wanted, Buffer.concat(bufs).toString('utf8'))
+ })
+ pt.on('error', () => {
+ t.fail()
+ })
+ return pt
+ }
+ )
+ })
+})
+
+test('with a local agent', t => {
+ t.plan(9)
+ const wanted = 'payload'
+
+ const server = http.createServer((req, res) => {
+ t.equal('/', req.url)
+ t.equal('GET', req.method)
+ t.equal(`localhost:${server.address().port}`, req.headers.host)
+ res.setHeader('Content-Type', 'text/plain')
+ res.end(wanted)
+ })
+
+ t.teardown(server.close.bind(server))
+
+ const dispatcher = new Agent()
+
+ dispatcher.on('connect', (origin, [dispatcher]) => {
+ t.ok(dispatcher)
+ t.equal(dispatcher[kRunning], 0)
+ process.nextTick(() => {
+ t.equal(dispatcher[kRunning], 1)
+ })
+ })
+
+ server.listen(0, () => {
+ stream(
+ `http://localhost:${server.address().port}`,
+ {
+ dispatcher,
+ opaque: new PassThrough()
+ },
+ ({ statusCode, headers, opaque: pt }) => {
+ t.equal(statusCode, 200)
+ t.equal(headers['content-type'], 'text/plain')
+ const bufs = []
+ pt.on('data', (buf) => {
+ bufs.push(buf)
+ })
+ pt.on('end', () => {
+ t.equal(wanted, Buffer.concat(bufs).toString('utf8'))
+ })
+ pt.on('error', () => {
+ t.fail()
+ })
+ return pt
+ }
+ )
+ })
+})
+
+test('stream: fails with invalid URL', t => {
+ t.plan(4)
+ t.throws(() => stream(), errors.InvalidArgumentError, 'throws on missing url argument')
+ t.throws(() => stream(''), errors.InvalidArgumentError, 'throws on invalid url')
+ t.throws(() => stream({}), errors.InvalidArgumentError, 'throws on missing url.origin argument')
+ t.throws(() => stream({ origin: '' }), errors.InvalidArgumentError, 'throws on invalid url.origin argument')
+})
+
+test('with globalAgent', t => {
+ t.plan(6)
+ const wanted = 'payload'
+
+ const server = http.createServer((req, res) => {
+ t.equal('/', req.url)
+ t.equal('GET', req.method)
+ t.equal(`localhost:${server.address().port}`, req.headers.host)
+ res.setHeader('Content-Type', 'text/plain')
+ res.end(wanted)
+ })
+
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const bufs = []
+
+ pipeline(
+ `http://localhost:${server.address().port}`,
+ {},
+ ({ statusCode, headers, body }) => {
+ t.equal(statusCode, 200)
+ t.equal(headers['content-type'], 'text/plain')
+ return body
+ }
+ )
+ .end()
+ .on('data', buf => {
+ bufs.push(buf)
+ })
+ .on('end', () => {
+ t.equal(wanted, Buffer.concat(bufs).toString('utf8'))
+ })
+ .on('error', () => {
+ t.fail()
+ })
+ })
+})
+
+test('with a local agent', t => {
+ t.plan(6)
+ const wanted = 'payload'
+
+ const server = http.createServer((req, res) => {
+ t.equal('/', req.url)
+ t.equal('GET', req.method)
+ t.equal(`localhost:${server.address().port}`, req.headers.host)
+ res.setHeader('Content-Type', 'text/plain')
+ res.end(wanted)
+ })
+
+ t.teardown(server.close.bind(server))
+
+ const dispatcher = new Agent()
+
+ server.listen(0, () => {
+ const bufs = []
+
+ pipeline(
+ `http://localhost:${server.address().port}`,
+ { dispatcher },
+ ({ statusCode, headers, body }) => {
+ t.equal(statusCode, 200)
+ t.equal(headers['content-type'], 'text/plain')
+ return body
+ }
+ )
+ .end()
+ .on('data', buf => {
+ bufs.push(buf)
+ })
+ .on('end', () => {
+ t.equal(wanted, Buffer.concat(bufs).toString('utf8'))
+ })
+ .on('error', () => {
+ t.fail()
+ })
+ })
+})
+
+test('pipeline: fails with invalid URL', t => {
+ t.plan(4)
+ t.throws(() => pipeline(), errors.InvalidArgumentError, 'throws on missing url argument')
+ t.throws(() => pipeline(''), errors.InvalidArgumentError, 'throws on invalid url')
+ t.throws(() => pipeline({}), errors.InvalidArgumentError, 'throws on missing url.origin argument')
+ t.throws(() => pipeline({ origin: '' }), errors.InvalidArgumentError, 'throws on invalid url.origin argument')
+})
+
+test('pipeline: fails with invalid onInfo', (t) => {
+ t.plan(2)
+ pipeline({ origin: 'http://localhost', path: '/', onInfo: 'foo' }, () => {}).on('error', (err) => {
+ t.type(err, errors.InvalidArgumentError)
+ t.equal(err.message, 'invalid onInfo callback')
+ })
+})
+
+test('request: fails with invalid onInfo', async (t) => {
+ try {
+ await request({ origin: 'http://localhost', path: '/', onInfo: 'foo' })
+ t.fail('should throw')
+ } catch (e) {
+ t.ok(e)
+ t.equal(e.message, 'invalid onInfo callback')
+ }
+ t.end()
+})
+
+test('stream: fails with invalid onInfo', async (t) => {
+ try {
+ await stream({ origin: 'http://localhost', path: '/', onInfo: 'foo' }, () => new PassThrough())
+ t.fail('should throw')
+ } catch (e) {
+ t.ok(e)
+ t.equal(e.message, 'invalid onInfo callback')
+ }
+ t.end()
+})
+
+test('constructor validations', t => {
+ t.plan(4)
+ t.throws(() => new Agent({ factory: 'ASD' }), errors.InvalidArgumentError, 'throws on invalid opts argument')
+ t.throws(() => new Agent({ maxRedirections: 'ASD' }), errors.InvalidArgumentError, 'throws on invalid opts argument')
+ t.throws(() => new Agent({ maxRedirections: -1 }), errors.InvalidArgumentError, 'throws on invalid opts argument')
+ t.throws(() => new Agent({ maxRedirections: null }), errors.InvalidArgumentError, 'throws on invalid opts argument')
+})
+
+test('dispatch validations', t => {
+ const dispatcher = new Agent()
+
+ const noopHandler = {
+ onConnect () {},
+ onHeaders () {},
+ onData () {},
+ onComplete () {
+ server.close()
+ },
+ onError (err) {
+ throw err
+ }
+ }
+
+ const server = http.createServer((req, res) => {
+ res.setHeader('Content-Type', 'text/plain')
+ res.end('asd')
+ })
+
+ t.plan(6)
+ t.throws(() => dispatcher.dispatch('ASD'), errors.InvalidArgumentError, 'throws on missing handler')
+ t.throws(() => dispatcher.dispatch('ASD', noopHandler), errors.InvalidArgumentError, 'throws on invalid opts argument type')
+ t.throws(() => dispatcher.dispatch({}, noopHandler), errors.InvalidArgumentError, 'throws on invalid opts.origin argument')
+ t.throws(() => dispatcher.dispatch({ origin: '' }, noopHandler), errors.InvalidArgumentError, 'throws on invalid opts.origin argument')
+ t.throws(() => dispatcher.dispatch({}, {}), errors.InvalidArgumentError, 'throws on invalid handler.onError')
+
+ server.listen(0, () => {
+ t.doesNotThrow(() => dispatcher.dispatch({
+ origin: new URL(`http://localhost:${server.address().port}`),
+ path: '/',
+ method: 'GET'
+ }, noopHandler))
+ })
+})
+
+test('drain', t => {
+ t.plan(2)
+
+ const dispatcher = new Agent({
+ connections: 1,
+ pipelining: 1
+ })
+
+ dispatcher.on('drain', () => {
+ t.pass()
+ })
+
+ class Handler {
+ onConnect () {}
+ onHeaders () {}
+ onData () {}
+ onComplete () {}
+ onError () {
+ t.fail()
+ }
+ }
+
+ const server = http.createServer((req, res) => {
+ res.setHeader('Content-Type', 'text/plain')
+ res.end('asd')
+ })
+
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ t.equal(dispatcher.dispatch({
+ origin: `http://localhost:${server.address().port}`,
+ method: 'GET',
+ path: '/'
+ }, new Handler()), false)
+ })
+})
+
+test('global api', t => {
+ t.plan(6 * 2)
+
+ const server = http.createServer((req, res) => {
+ if (req.url === '/bar') {
+ t.equal(req.method, 'PUT')
+ t.equal(req.url, '/bar')
+ } else {
+ t.equal(req.method, 'GET')
+ t.equal(req.url, '/foo')
+ }
+ req.pipe(res)
+ })
+
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, async () => {
+ const origin = `http://localhost:${server.address().port}`
+ await request(origin, { path: '/foo' })
+ await request(`${origin}/foo`)
+ await request({ origin, path: '/foo' })
+ await stream({ origin, path: '/foo' }, () => new PassThrough())
+ await request({ protocol: 'http:', hostname: 'localhost', port: server.address().port, path: '/foo' })
+ await request(`${origin}/bar`, { body: 'asd' })
+ })
+})
+
+test('global api throws', t => {
+ const origin = 'http://asd'
+ t.throws(() => request(`${origin}/foo`, { path: '/foo' }), errors.InvalidArgumentError)
+ t.throws(() => request({ origin, path: 0 }, { path: '/foo' }), errors.InvalidArgumentError)
+ t.throws(() => request({ origin, pathname: 0 }, { path: '/foo' }), errors.InvalidArgumentError)
+ t.throws(() => request({ origin: 0 }, { path: '/foo' }), errors.InvalidArgumentError)
+ t.throws(() => request(0), errors.InvalidArgumentError)
+ t.throws(() => request(1), errors.InvalidArgumentError)
+ t.end()
+})
+
+test('unreachable request rejects and can be caught', t => {
+ t.plan(1)
+
+ request('https://thisis.not/avalid/url').catch(() => {
+ t.pass()
+ })
+})
+
+test('connect is not valid', t => {
+ t.plan(1)
+
+ t.throws(() => new Agent({ connect: false }), errors.InvalidArgumentError, 'connect must be a function or an object')
+})
+
+test('the dispatcher is truly global', t => {
+ const agent = getGlobalDispatcher()
+ const undiciFresh = importFresh('../index.js')
+ t.equal(agent, undiciFresh.getGlobalDispatcher())
+ t.end()
+})
+
+teardown(() => process.exit())
diff --git a/test/async_hooks.js b/test/async_hooks.js
new file mode 100644
index 0000000..2e8533d
--- /dev/null
+++ b/test/async_hooks.js
@@ -0,0 +1,206 @@
+'use strict'
+
+const { test } = require('tap')
+const { Client } = require('..')
+const { createServer } = require('http')
+const { createHook, executionAsyncId } = require('async_hooks')
+const { readFile } = require('fs')
+const { PassThrough } = require('stream')
+
+const transactions = new Map()
+
+function getCurrentTransaction () {
+ const asyncId = executionAsyncId()
+ return transactions.has(asyncId) ? transactions.get(asyncId) : null
+}
+
+function setCurrentTransaction (trans) {
+ const asyncId = executionAsyncId()
+ transactions.set(asyncId, trans)
+}
+
+const hook = createHook({
+ init (asyncId, type, triggerAsyncId, resource) {
+ if (type === 'TIMERWRAP') return
+ // process._rawDebug(type + ' ' + asyncId)
+ transactions.set(asyncId, getCurrentTransaction())
+ },
+ destroy (asyncId) {
+ transactions.delete(asyncId)
+ }
+})
+
+hook.enable()
+
+test('async hooks', (t) => {
+ t.plan(31)
+
+ const server = createServer((req, res) => {
+ res.setHeader('content-type', 'text/plain')
+ readFile(__filename, (err, buf) => {
+ t.error(err)
+ const buf1 = buf.slice(0, buf.length / 2)
+ const buf2 = buf.slice(buf.length / 2)
+ // we split the file so that it's received in 2 chunks
+ // and it should restore the state on the second
+ res.write(buf1)
+ setTimeout(() => {
+ res.end(buf2)
+ }, 10)
+ })
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ client.request({ path: '/', method: 'GET' }, (err, { statusCode, headers, body }) => {
+ t.error(err)
+ body.resume()
+ t.strictSame(getCurrentTransaction(), null)
+
+ setCurrentTransaction({ hello: 'world2' })
+
+ client.request({ path: '/', method: 'GET' }, (err, { statusCode, headers, body }) => {
+ t.error(err)
+ t.strictSame(getCurrentTransaction(), { hello: 'world2' })
+
+ body.once('data', () => {
+ t.pass()
+ body.resume()
+ })
+
+ body.on('end', () => {
+ t.pass()
+ })
+ })
+ })
+
+ client.request({ path: '/', method: 'GET' }, (err, { statusCode, headers, body }) => {
+ t.error(err)
+ body.resume()
+ t.strictSame(getCurrentTransaction(), null)
+
+ setCurrentTransaction({ hello: 'world' })
+
+ client.request({ path: '/', method: 'GET' }, (err, { statusCode, headers, body }) => {
+ t.error(err)
+ t.strictSame(getCurrentTransaction(), { hello: 'world' })
+
+ body.once('data', () => {
+ t.pass()
+ body.resume()
+ })
+
+ body.on('end', () => {
+ t.pass()
+ })
+ })
+ })
+
+ client.request({ path: '/', method: 'HEAD' }, (err, { statusCode, headers, body }) => {
+ t.error(err)
+ body.resume()
+ t.strictSame(getCurrentTransaction(), null)
+
+ setCurrentTransaction({ hello: 'world' })
+
+ client.request({ path: '/', method: 'HEAD' }, (err, { statusCode, headers, body }) => {
+ t.error(err)
+ t.strictSame(getCurrentTransaction(), { hello: 'world' })
+
+ body.once('data', () => {
+ t.pass()
+ body.resume()
+ })
+
+ body.on('end', () => {
+ t.pass()
+ })
+ })
+ })
+
+ client.stream({ path: '/', method: 'GET' }, () => {
+ t.strictSame(getCurrentTransaction(), null)
+ return new PassThrough().resume()
+ }, (err) => {
+ t.error(err)
+ t.strictSame(getCurrentTransaction(), null)
+
+ setCurrentTransaction({ hello: 'world' })
+
+ client.stream({ path: '/', method: 'GET' }, () => {
+ t.strictSame(getCurrentTransaction(), { hello: 'world' })
+ return new PassThrough().resume()
+ }, (err) => {
+ t.error(err)
+ t.strictSame(getCurrentTransaction(), { hello: 'world' })
+ })
+ })
+ })
+})
+
+test('async hooks client is destroyed', (t) => {
+ t.plan(7)
+
+ const server = createServer((req, res) => {
+ res.setHeader('content-type', 'text/plain')
+ readFile(__filename, (err, buf) => {
+ t.error(err)
+ res.write('asd')
+ })
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ client.request({ path: '/', method: 'GET', throwOnError: true }, (err, { body }) => {
+ t.error(err)
+ body.resume()
+ body.on('error', (err) => {
+ t.ok(err)
+ })
+ t.strictSame(getCurrentTransaction(), null)
+
+ setCurrentTransaction({ hello: 'world2' })
+
+ client.request({ path: '/', method: 'GET' }, (err) => {
+ t.equal(err.message, 'The client is destroyed')
+ t.strictSame(getCurrentTransaction(), { hello: 'world2' })
+ })
+ client.destroy((err) => {
+ t.error(err)
+ })
+ })
+ })
+})
+
+test('async hooks pipeline handler', (t) => {
+ t.plan(2)
+
+ const server = createServer((req, res) => {
+ res.end('hello')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.close.bind(client))
+
+ setCurrentTransaction({ hello: 'world2' })
+
+ client
+ .pipeline({ path: '/', method: 'GET' }, ({ body }) => {
+ t.strictSame(getCurrentTransaction(), { hello: 'world2' })
+ return body
+ })
+ .on('close', () => {
+ t.pass()
+ })
+ .resume()
+ .end()
+ })
+})
diff --git a/test/autoselectfamily.js b/test/autoselectfamily.js
new file mode 100644
index 0000000..0b44a3e
--- /dev/null
+++ b/test/autoselectfamily.js
@@ -0,0 +1,198 @@
+'use strict'
+
+const { test, skip } = require('tap')
+const dgram = require('dgram')
+const { Resolver } = require('dns')
+const dnsPacket = require('dns-packet')
+const { createServer } = require('http')
+const { Client, Agent, request } = require('..')
+const { nodeHasAutoSelectFamily } = require('../lib/core/util')
+
+/*
+ * IMPORTANT
+ *
+ * As only some version of Node have autoSelectFamily enabled by default (>= 20), make sure the option is always
+ * explicitly passed in tests in this file to avoid compatibility problems across release lines.
+ *
+ */
+
+if (!nodeHasAutoSelectFamily) {
+ skip('autoSelectFamily is not supportee')
+ process.exit()
+}
+
+function _lookup (resolver, hostname, options, cb) {
+ resolver.resolve(hostname, 'ANY', (err, replies) => {
+ if (err) {
+ return cb(err)
+ }
+
+ const hosts = replies
+ .map((r) => ({ address: r.address, family: r.type === 'AAAA' ? 6 : 4 }))
+ .sort((a, b) => b.family - a.family)
+
+ if (options.all === true) {
+ return cb(null, hosts)
+ }
+
+ return cb(null, hosts[0].address, hosts[0].family)
+ })
+}
+
+function createDnsServer (ipv6Addr, ipv4Addr, cb) {
+ // Create a DNS server which replies with a AAAA and a A record for the same host
+ const socket = dgram.createSocket('udp4')
+
+ socket.on('message', (msg, { address, port }) => {
+ const parsed = dnsPacket.decode(msg)
+
+ const response = dnsPacket.encode({
+ type: 'answer',
+ id: parsed.id,
+ questions: parsed.questions,
+ answers: [
+ { type: 'AAAA', class: 'IN', name: 'example.org', data: '::1', ttl: 123 },
+ { type: 'A', class: 'IN', name: 'example.org', data: '127.0.0.1', ttl: 123 }
+ ]
+ })
+
+ socket.send(response, port, address)
+ })
+
+ socket.bind(0, () => {
+ const resolver = new Resolver()
+ resolver.setServers([`127.0.0.1:${socket.address().port}`])
+
+ cb(null, { dnsServer: socket, lookup: _lookup.bind(null, resolver) })
+ })
+}
+
+test('with autoSelectFamily enable the request succeeds when using request', (t) => {
+ t.plan(3)
+
+ createDnsServer('::1', '127.0.0.1', function (_, { dnsServer, lookup }) {
+ const server = createServer((req, res) => {
+ res.end('hello')
+ })
+
+ t.teardown(() => {
+ server.close()
+ dnsServer.close()
+ })
+
+ server.listen(0, '127.0.0.1', () => {
+ const agent = new Agent({ connect: { lookup }, autoSelectFamily: true })
+
+ request(
+ `http://example.org:${server.address().port}/`, {
+ method: 'GET',
+ dispatcher: agent
+ }, (err, { statusCode, body }) => {
+ t.error(err)
+
+ let response = Buffer.alloc(0)
+
+ body.on('data', chunk => {
+ response = Buffer.concat([response, chunk])
+ })
+
+ body.on('end', () => {
+ t.strictSame(statusCode, 200)
+ t.strictSame(response.toString('utf-8'), 'hello')
+ })
+ })
+ })
+ })
+})
+
+test('with autoSelectFamily enable the request succeeds when using a client', (t) => {
+ t.plan(3)
+
+ createDnsServer('::1', '127.0.0.1', function (_, { dnsServer, lookup }) {
+ const server = createServer((req, res) => {
+ res.end('hello')
+ })
+
+ t.teardown(() => {
+ server.close()
+ dnsServer.close()
+ })
+
+ server.listen(0, '127.0.0.1', () => {
+ const client = new Client(`http://example.org:${server.address().port}`, { connect: { lookup }, autoSelectFamily: true })
+
+ t.teardown(client.destroy.bind(client))
+
+ client.request({
+ path: '/',
+ method: 'GET'
+ }, (err, { statusCode, body }) => {
+ t.error(err)
+
+ let response = Buffer.alloc(0)
+
+ body.on('data', chunk => {
+ response = Buffer.concat([response, chunk])
+ })
+
+ body.on('end', () => {
+ t.strictSame(statusCode, 200)
+ t.strictSame(response.toString('utf-8'), 'hello')
+ })
+ })
+ })
+ })
+})
+
+test('with autoSelectFamily disabled the request fails when using request', (t) => {
+ t.plan(1)
+
+ createDnsServer('::1', '127.0.0.1', function (_, { dnsServer, lookup }) {
+ const server = createServer((req, res) => {
+ res.end('hello')
+ })
+
+ t.teardown(() => {
+ server.close()
+ dnsServer.close()
+ })
+
+ server.listen(0, '127.0.0.1', () => {
+ const agent = new Agent({ connect: { lookup, autoSelectFamily: false } })
+
+ request(`http://example.org:${server.address().port}`, {
+ method: 'GET',
+ dispatcher: agent
+ }, (err, { statusCode, body }) => {
+ t.ok(['ECONNREFUSED', 'EAFNOSUPPORT'].includes(err.code))
+ })
+ })
+ })
+})
+
+test('with autoSelectFamily disabled the request fails when using a client', (t) => {
+ t.plan(1)
+
+ createDnsServer('::1', '127.0.0.1', function (_, { dnsServer, lookup }) {
+ const server = createServer((req, res) => {
+ res.end('hello')
+ })
+
+ t.teardown(() => {
+ server.close()
+ dnsServer.close()
+ })
+
+ server.listen(0, '127.0.0.1', () => {
+ const client = new Client(`http://example.org:${server.address().port}`, { connect: { lookup, autoSelectFamily: false } })
+ t.teardown(client.destroy.bind(client))
+
+ client.request({
+ path: '/',
+ method: 'GET'
+ }, (err, { statusCode, body }) => {
+ t.ok(['ECONNREFUSED', 'EAFNOSUPPORT'].includes(err.code))
+ })
+ })
+ })
+})
diff --git a/test/balanced-pool.js b/test/balanced-pool.js
new file mode 100644
index 0000000..d20f926
--- /dev/null
+++ b/test/balanced-pool.js
@@ -0,0 +1,566 @@
+'use strict'
+
+const { test } = require('tap')
+const { BalancedPool, Pool, Client, errors } = require('..')
+const { nodeMajor } = require('../lib/core/util')
+const { createServer } = require('http')
+const { promisify } = require('util')
+
+test('throws when factory is not a function', (t) => {
+ t.plan(2)
+
+ try {
+ new BalancedPool(null, { factory: '' }) // eslint-disable-line
+ } catch (err) {
+ t.type(err, errors.InvalidArgumentError)
+ t.equal(err.message, 'factory must be a function.')
+ }
+})
+
+test('add/remove upstreams', (t) => {
+ t.plan(7)
+
+ const upstream01 = 'http://localhost:1'
+ const upstream02 = 'http://localhost:2'
+
+ const pool = new BalancedPool()
+ t.same(pool.upstreams, [])
+
+ // try to remove non-existent upstream
+ pool.removeUpstream(upstream01)
+ t.same(pool.upstreams, [])
+
+ pool.addUpstream(upstream01)
+ t.same(pool.upstreams, [upstream01])
+
+ // try to add the same upstream
+ pool.addUpstream(upstream01)
+ t.same(pool.upstreams, [upstream01])
+
+ pool.addUpstream(upstream02)
+ t.same(pool.upstreams, [upstream01, upstream02])
+
+ pool.removeUpstream(upstream02)
+ t.same(pool.upstreams, [upstream01])
+
+ pool.removeUpstream(upstream01)
+ t.same(pool.upstreams, [])
+})
+
+test('basic get', async (t) => {
+ t.plan(16)
+
+ let server1Called = 0
+ const server1 = createServer((req, res) => {
+ server1Called++
+ t.equal('/', req.url)
+ t.equal('GET', req.method)
+ res.setHeader('content-type', 'text/plain')
+ res.end('hello')
+ })
+ t.teardown(server1.close.bind(server1))
+
+ await promisify(server1.listen).call(server1, 0)
+
+ let server2Called = 0
+ const server2 = createServer((req, res) => {
+ server2Called++
+ t.equal('/', req.url)
+ t.equal('GET', req.method)
+ res.setHeader('content-type', 'text/plain')
+ res.end('hello')
+ })
+ t.teardown(server2.close.bind(server2))
+
+ await promisify(server2.listen).call(server2, 0)
+
+ const client = new BalancedPool()
+ client.addUpstream(`http://localhost:${server1.address().port}`)
+ client.addUpstream(`http://localhost:${server2.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ {
+ const { statusCode, headers, body } = await client.request({ path: '/', method: 'GET' })
+ t.equal(statusCode, 200)
+ t.equal(headers['content-type'], 'text/plain')
+ t.equal('hello', await body.text())
+ }
+
+ {
+ const { statusCode, headers, body } = await client.request({ path: '/', method: 'GET' })
+ t.equal(statusCode, 200)
+ t.equal(headers['content-type'], 'text/plain')
+ t.equal('hello', await body.text())
+ }
+
+ t.equal(server1Called, 1)
+ t.equal(server2Called, 1)
+
+ t.equal(client.destroyed, false)
+ t.equal(client.closed, false)
+ await client.close()
+ t.equal(client.destroyed, true)
+ t.equal(client.closed, true)
+})
+
+test('connect/disconnect event(s)', (t) => {
+ const clients = 2
+
+ t.plan(clients * 5)
+
+ const server = createServer((req, res) => {
+ res.writeHead(200, {
+ Connection: 'keep-alive',
+ 'Keep-Alive': 'timeout=1s'
+ })
+ res.end('ok')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const pool = new BalancedPool(`http://localhost:${server.address().port}`, {
+ connections: clients,
+ keepAliveTimeoutThreshold: 100
+ })
+ t.teardown(pool.close.bind(pool))
+
+ pool.on('connect', (origin, [pool, pool2, client]) => {
+ t.equal(client instanceof Client, true)
+ })
+ pool.on('disconnect', (origin, [pool, pool2, client], error) => {
+ t.ok(client instanceof Client)
+ t.type(error, errors.InformationalError)
+ t.equal(error.code, 'UND_ERR_INFO')
+ })
+
+ for (let i = 0; i < clients; i++) {
+ pool.request({
+ path: '/',
+ method: 'GET'
+ }, (err, { headers, body }) => {
+ t.error(err)
+ body.resume()
+ })
+ }
+ })
+})
+
+test('busy', (t) => {
+ t.plan(8 * 6 + 2 + 1)
+
+ const server = createServer((req, res) => {
+ t.equal('/', req.url)
+ t.equal('GET', req.method)
+ res.setHeader('content-type', 'text/plain')
+ res.end('hello')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, async () => {
+ const client = new BalancedPool(`http://localhost:${server.address().port}`, {
+ connections: 2,
+ pipelining: 2
+ })
+ client.on('drain', () => {
+ t.pass()
+ })
+ client.on('connect', () => {
+ t.pass()
+ })
+ t.teardown(client.destroy.bind(client))
+
+ for (let n = 1; n <= 8; ++n) {
+ client.request({ path: '/', method: 'GET' }, (err, { statusCode, headers, body }) => {
+ t.error(err)
+ t.equal(statusCode, 200)
+ t.equal(headers['content-type'], 'text/plain')
+ const bufs = []
+ body.on('data', (buf) => {
+ bufs.push(buf)
+ })
+ body.on('end', () => {
+ t.equal('hello', Buffer.concat(bufs).toString('utf8'))
+ })
+ })
+ }
+ })
+})
+
+test('factory option with basic get request', async (t) => {
+ t.plan(12)
+
+ let factoryCalled = 0
+ const opts = {
+ factory: (origin, opts) => {
+ factoryCalled++
+ return new Pool(origin, opts)
+ }
+ }
+
+ const client = new BalancedPool([], opts) // eslint-disable-line
+
+ let serverCalled = 0
+ const server = createServer((req, res) => {
+ serverCalled++
+ t.equal('/', req.url)
+ t.equal('GET', req.method)
+ res.setHeader('content-type', 'text/plain')
+ res.end('hello')
+ })
+ t.teardown(server.close.bind(server))
+
+ await promisify(server.listen).call(server, 0)
+
+ client.addUpstream(`http://localhost:${server.address().port}`)
+
+ t.same(client.upstreams, [`http://localhost:${server.address().port}`])
+
+ t.teardown(client.destroy.bind(client))
+
+ {
+ const { statusCode, headers, body } = await client.request({ path: '/', method: 'GET' })
+ t.equal(statusCode, 200)
+ t.equal(headers['content-type'], 'text/plain')
+ t.equal('hello', await body.text())
+ }
+
+ t.equal(serverCalled, 1)
+ t.equal(factoryCalled, 1)
+
+ t.equal(client.destroyed, false)
+ t.equal(client.closed, false)
+ await client.close()
+ t.equal(client.destroyed, true)
+ t.equal(client.closed, true)
+})
+
+test('throws when upstream is missing', async (t) => {
+ t.plan(2)
+
+ const pool = new BalancedPool()
+
+ try {
+ await pool.request({ path: '/', method: 'GET' })
+ } catch (e) {
+ t.type(e, errors.BalancedPoolMissingUpstreamError)
+ t.equal(e.message, 'No upstream has been added to the BalancedPool')
+ }
+})
+
+class TestServer {
+ constructor ({ config: { server, socketHangup, downOnRequests, socketHangupOnRequests }, onRequest }) {
+ this.config = {
+ downOnRequests: downOnRequests || [],
+ socketHangupOnRequests: socketHangupOnRequests || [],
+ socketHangup
+ }
+ this.name = server
+ // start a server listening to any port available on the host
+ this.port = 0
+ this.iteration = 0
+ this.requestsCount = 0
+ this.onRequest = onRequest
+ this.server = null
+ }
+
+ _shouldHangupOnClient () {
+ if (this.config.socketHangup) {
+ return true
+ }
+ if (this.config.socketHangupOnRequests.includes(this.requestsCount)) {
+ return true
+ }
+
+ return false
+ }
+
+ _shouldStopServer () {
+ if (this.config.upstreamDown === true || this.config.downOnRequests.includes(this.requestsCount)) {
+ return true
+ }
+ return false
+ }
+
+ async prepareForIteration (iteration) {
+ // set current iteration
+ this.iteration = iteration
+
+ if (this._shouldStopServer()) {
+ await this.stop()
+ } else if (!this.isRunning()) {
+ await this.start()
+ }
+ }
+
+ start () {
+ this.server = createServer((req, res) => {
+ if (this._shouldHangupOnClient()) {
+ req.destroy(new Error('(ツ)'))
+ return
+ }
+ this.requestsCount++
+ res.end('server is running!')
+
+ this.onRequest(this)
+ }).listen(this.port)
+
+ this.server.keepAliveTimeout = 2000
+
+ return new Promise((resolve) => {
+ this.server.on('listening', () => {
+ // store the used port to use it again if the server was stopped as part of test and then started again
+ this.port = this.server.address().port
+
+ return resolve()
+ })
+ })
+ }
+
+ isRunning () {
+ return !!this.server.address()
+ }
+
+ stop () {
+ if (!this.isRunning()) {
+ return
+ }
+
+ return new Promise(resolve => {
+ this.server.close(() => resolve())
+ })
+ }
+}
+
+const cases = [
+
+ // 0
+
+ {
+ iterations: 100,
+ maxWeightPerServer: 100,
+ errorPenalty: 7,
+ config: [{ server: 'A' }, { server: 'B' }, { server: 'C' }],
+ expected: ['A', 'B', 'C', 'A', 'B', 'C', 'A', 'B', 'C', 'A', 'B', 'C'],
+ expectedConnectionRefusedErrors: 0,
+ expectedSocketErrors: 0,
+ expectedRatios: [0.34, 0.33, 0.33]
+ },
+
+ // 1
+
+ {
+ iterations: 100,
+ maxWeightPerServer: 100,
+ errorPenalty: 15,
+ config: [{ server: 'A', downOnRequests: [0] }, { server: 'B' }, { server: 'C' }],
+ expected: ['A/connectionRefused', 'B', 'C', 'B', 'C', 'B', 'C', 'A', 'B', 'C', 'A'],
+ expectedConnectionRefusedErrors: 1,
+ expectedSocketErrors: 0,
+ expectedRatios: [0.32, 0.34, 0.34]
+ },
+
+ // 2
+
+ {
+ iterations: 100,
+ maxWeightPerServer: 100,
+ errorPenalty: 15,
+ config: [{ server: 'A' }, { server: 'B', downOnRequests: [0] }, { server: 'C' }],
+ expected: ['A', 'B/connectionRefused', 'C', 'A', 'C', 'A', 'C', 'A', 'B', 'C'],
+ expectedConnectionRefusedErrors: 1,
+ expectedSocketErrors: 0,
+ expectedRatios: [0.34, 0.32, 0.34]
+ },
+
+ // 3
+
+ {
+ iterations: 100,
+ maxWeightPerServer: 100,
+ errorPenalty: 15,
+ config: [{ server: 'A' }, { server: 'B', downOnRequests: [0] }, { server: 'C', downOnRequests: [0] }],
+ expected: ['A', 'B/connectionRefused', 'C/connectionRefused', 'A', 'A', 'A', 'B', 'C'],
+ expectedConnectionRefusedErrors: 2,
+ expectedSocketErrors: 0,
+ expectedRatios: [0.35, 0.33, 0.32]
+ },
+
+ // 4
+
+ {
+ iterations: 100,
+ maxWeightPerServer: 100,
+ errorPenalty: 15,
+ config: [{ server: 'A', downOnRequests: [0] }, { server: 'B', downOnRequests: [0] }, { server: 'C', downOnRequests: [0] }],
+ expected: ['A/connectionRefused', 'B/connectionRefused', 'C/connectionRefused', 'A', 'B', 'C', 'A', 'B', 'C'],
+ expectedConnectionRefusedErrors: 3,
+ expectedSocketErrors: 0,
+ expectedRatios: [0.34, 0.33, 0.33]
+ },
+
+ // 5
+
+ {
+ iterations: 100,
+ maxWeightPerServer: 100,
+ errorPenalty: 15,
+ config: [{ server: 'A', downOnRequests: [0, 1, 2] }, { server: 'B', downOnRequests: [0, 1, 2] }, { server: 'C', downOnRequests: [0, 1, 2] }],
+ expected: ['A/connectionRefused', 'B/connectionRefused', 'C/connectionRefused', 'A/connectionRefused', 'B/connectionRefused', 'C/connectionRefused', 'A/connectionRefused', 'B/connectionRefused', 'C/connectionRefused', 'A', 'B', 'C', 'A', 'B', 'C'],
+ expectedConnectionRefusedErrors: 9,
+ expectedSocketErrors: 0,
+ expectedRatios: [0.34, 0.33, 0.33]
+ },
+
+ // 6
+
+ {
+ iterations: 100,
+ maxWeightPerServer: 100,
+ errorPenalty: 15,
+ config: [{ server: 'A', downOnRequests: [0] }, { server: 'B', downOnRequests: [0, 1] }, { server: 'C', downOnRequests: [0] }],
+ expected: ['A/connectionRefused', 'B/connectionRefused', 'C/connectionRefused', 'A', 'B/connectionRefused', 'C', 'A', 'B', 'C', 'A', 'B', 'C', 'A', 'C', 'A', 'C', 'A', 'C', 'A', 'B'],
+ expectedConnectionRefusedErrors: 4,
+ expectedSocketErrors: 0,
+ expectedRatios: [0.36, 0.29, 0.35]
+ },
+
+ // 7
+
+ {
+ iterations: 100,
+ maxWeightPerServer: 100,
+ errorPenalty: 15,
+ config: [{ server: 'A' }, { server: 'B' }, { server: 'C', downOnRequests: [1] }],
+ expected: ['A', 'B', 'C', 'A', 'B', 'C/connectionRefused', 'A', 'B', 'A', 'B', 'A', 'B', 'C', 'A', 'B', 'C'],
+ expectedConnectionRefusedErrors: 1,
+ expectedSocketErrors: 0,
+ expectedRatios: [0.34, 0.34, 0.32],
+
+ // Skip because the behavior of Node.js has changed
+ skip: nodeMajor >= 19
+ },
+
+ // 8
+
+ {
+ iterations: 100,
+ maxWeightPerServer: 100,
+ errorPenalty: 15,
+ config: [{ server: 'A', socketHangupOnRequests: [1] }, { server: 'B' }, { server: 'C' }],
+ expected: ['A', 'B', 'C', 'A/socketError', 'B', 'C', 'B', 'C', 'B', 'C', 'A'],
+ expectedConnectionRefusedErrors: 0,
+ expectedSocketErrors: 1,
+ expectedRatios: [0.32, 0.34, 0.34]
+ },
+
+ // 9
+
+ {
+ iterations: 100,
+ maxWeightPerServer: 100,
+ errorPenalty: 7,
+ config: [{ server: 'A' }, { server: 'B' }, { server: 'C' }, { server: 'D' }, { server: 'E' }],
+ expected: ['A', 'B', 'C', 'D', 'E', 'A', 'B', 'C', 'D', 'E'],
+ expectedConnectionRefusedErrors: 0,
+ expectedSocketErrors: 0,
+ expectedRatios: [0.2, 0.2, 0.2, 0.2, 0.2]
+ },
+
+ // 10
+ {
+ iterations: 100,
+ maxWeightPerServer: 100,
+ errorPenalty: 15,
+ config: [{ server: 'A', downOnRequests: [0, 1, 2, 3] }, { server: 'B' }, { server: 'C' }],
+ expected: ['A/connectionRefused', 'B', 'C', 'B', 'C', 'B', 'C', 'A/connectionRefused', 'B', 'C', 'B', 'C', 'A/connectionRefused', 'B', 'C', 'B', 'C', 'A/connectionRefused', 'B', 'C', 'A', 'B', 'C', 'A', 'B', 'C'],
+ expectedConnectionRefusedErrors: 4,
+ expectedSocketErrors: 0,
+ expectedRatios: [0.18, 0.41, 0.41]
+ }
+
+]
+
+for (const [index, { config, expected, expectedRatios, iterations = 9, expectedConnectionRefusedErrors = 0, expectedSocketErrors = 0, maxWeightPerServer, errorPenalty = 10, only = false, skip = false }] of cases.entries()) {
+ test(`weighted round robin - case ${index}`, { only, skip }, async (t) => {
+ // cerate an array to store succesfull reqeusts
+ const requestLog = []
+
+ // create instances of the test servers according to the config
+ const servers = config.map((serverConfig) => new TestServer({
+ config: serverConfig,
+ onRequest: (server) => {
+ requestLog.push(server.name)
+ }
+ }))
+ t.teardown(() => servers.map(server => server.stop()))
+
+ // start all servers to get a port so that we can build the upstream urls to supply them to undici
+ await Promise.all(servers.map(server => server.start()))
+
+ // build upstream urls
+ const urls = servers.map(server => `http://localhost:${server.port}`)
+
+ // add upstreams
+ const client = new BalancedPool(urls[0], { maxWeightPerServer, errorPenalty })
+ urls.slice(1).map(url => client.addUpstream(url))
+
+ let connectionRefusedErrors = 0
+ let socketErrors = 0
+ for (let i = 0; i < iterations; i++) {
+ // setup test servers for the next iteration
+
+ await Promise.all(servers.map(server => server.prepareForIteration(i)))
+
+ // send a request using undinci
+ try {
+ await client.request({ path: '/', method: 'GET' })
+ } catch (e) {
+ const serverWithError =
+ servers.find(server => server.port === e.port) ||
+ servers.find(server => {
+ if (typeof AggregateError === 'function' && e instanceof AggregateError) {
+ return e.errors.some(e => server.port === (e.socket?.remotePort ?? e.port))
+ }
+
+ return server.port === e.socket.remotePort
+ })
+
+ serverWithError.requestsCount++
+
+ if (e.code === 'ECONNREFUSED') {
+ requestLog.push(`${serverWithError.name}/connectionRefused`)
+ connectionRefusedErrors++
+ }
+ if (e.code === 'UND_ERR_SOCKET') {
+ requestLog.push(`${serverWithError.name}/socketError`)
+
+ socketErrors++
+ }
+ }
+ }
+ const totalRequests = servers.reduce((acc, server) => {
+ return acc + server.requestsCount
+ }, 0)
+
+ t.equal(totalRequests, iterations)
+
+ t.equal(connectionRefusedErrors, expectedConnectionRefusedErrors)
+ t.equal(socketErrors, expectedSocketErrors)
+
+ if (expectedRatios) {
+ const ratios = servers.reduce((acc, el) => {
+ acc[el.name] = 0
+ return acc
+ }, {})
+ requestLog.map(el => ratios[el[0]]++)
+
+ t.match(Object.keys(ratios).map(k => ratios[k] / iterations), expectedRatios)
+ }
+
+ if (expected) {
+ t.match(requestLog.slice(0, expected.length), expected)
+ }
+
+ await client.close()
+ })
+}
diff --git a/test/ca-fingerprint.js b/test/ca-fingerprint.js
new file mode 100644
index 0000000..f71063f
--- /dev/null
+++ b/test/ca-fingerprint.js
@@ -0,0 +1,126 @@
+'use strict'
+
+const crypto = require('crypto')
+const https = require('https')
+const { test } = require('tap')
+const { Client, buildConnector } = require('..')
+const pem = require('https-pem')
+
+const caFingerprint = getFingerprint(pem.cert.toString()
+ .split('\n')
+ .slice(1, -1)
+ .map(line => line.trim())
+ .join('')
+)
+
+test('Validate CA fingerprint with a custom connector', t => {
+ t.plan(2)
+
+ const server = https.createServer(pem, (req, res) => {
+ res.setHeader('Content-Type', 'text/plain')
+ res.end('hello')
+ })
+
+ server.listen(0, function () {
+ const connector = buildConnector({ rejectUnauthorized: false })
+ const client = new Client(`https://localhost:${server.address().port}`, {
+ connect (opts, cb) {
+ connector(opts, (err, socket) => {
+ if (err) {
+ cb(err)
+ } else if (getIssuerCertificate(socket).fingerprint256 !== caFingerprint) {
+ socket.destroy()
+ cb(new Error('Fingerprint does not match'))
+ } else {
+ cb(null, socket)
+ }
+ })
+ }
+ })
+
+ t.teardown(() => {
+ client.close()
+ server.close()
+ })
+
+ client.request({
+ path: '/',
+ method: 'GET'
+ }, (err, data) => {
+ t.error(err)
+
+ data.body
+ .resume()
+ .on('end', () => {
+ t.pass()
+ })
+ })
+ })
+})
+
+test('Bad CA fingerprint with a custom connector', t => {
+ t.plan(2)
+
+ const server = https.createServer(pem, (req, res) => {
+ res.setHeader('Content-Type', 'text/plain')
+ res.end('hello')
+ })
+
+ server.listen(0, function () {
+ const connector = buildConnector({ rejectUnauthorized: false })
+ const client = new Client(`https://localhost:${server.address().port}`, {
+ connect (opts, cb) {
+ connector(opts, (err, socket) => {
+ if (err) {
+ cb(err)
+ } else if (getIssuerCertificate(socket).fingerprint256 !== 'FO:OB:AR') {
+ socket.destroy()
+ cb(new Error('Fingerprint does not match'))
+ } else {
+ cb(null, socket)
+ }
+ })
+ }
+ })
+
+ t.teardown(() => {
+ client.close()
+ server.close()
+ })
+
+ client.request({
+ path: '/',
+ method: 'GET'
+ }, (err, data) => {
+ t.equal(err.message, 'Fingerprint does not match')
+ t.equal(data.body, undefined)
+ })
+ })
+})
+
+function getIssuerCertificate (socket) {
+ let certificate = socket.getPeerCertificate(true)
+ while (certificate && Object.keys(certificate).length > 0) {
+ // invalid certificate
+ if (certificate.issuerCertificate == null) {
+ return null
+ }
+
+ // We have reached the root certificate.
+ // In case of self-signed certificates, `issuerCertificate` may be a circular reference.
+ if (certificate.fingerprint256 === certificate.issuerCertificate.fingerprint256) {
+ break
+ }
+
+ // continue the loop
+ certificate = certificate.issuerCertificate
+ }
+ return certificate
+}
+
+function getFingerprint (content, inputEncoding = 'base64', outputEncoding = 'hex') {
+ const shasum = crypto.createHash('sha256')
+ shasum.update(content, inputEncoding)
+ const res = shasum.digest(outputEncoding)
+ return res.toUpperCase().match(/.{1,2}/g).join(':')
+}
diff --git a/test/client-abort.js b/test/client-abort.js
new file mode 100644
index 0000000..5854bc2
--- /dev/null
+++ b/test/client-abort.js
@@ -0,0 +1,213 @@
+'use strict'
+
+const { test } = require('tap')
+const { Client, errors } = require('..')
+const { createServer } = require('http')
+const { Readable } = require('stream')
+
+class OnAbortError extends Error {}
+
+test('aborted response errors', (t) => {
+ t.plan(3)
+
+ const server = createServer()
+ server.once('request', (req, res) => {
+ // TODO: res.write will cause body to emit 'error' twice
+ // due to bug in readable-stream.
+ res.end('asd')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ client.request({ path: '/', method: 'GET' }, (err, { statusCode, headers, body }) => {
+ t.error(err)
+ body.destroy()
+ body
+ .on('error', err => {
+ t.type(err, errors.RequestAbortedError)
+ })
+ .on('close', () => {
+ t.pass()
+ })
+ })
+ })
+})
+
+test('aborted req', (t) => {
+ t.plan(1)
+
+ const server = createServer((req, res) => {
+ res.end(Buffer.alloc(4 + 1, 'a'))
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ client.request({
+ method: 'POST',
+ path: '/',
+ body: new Readable({
+ read () {
+ setImmediate(() => {
+ this.destroy()
+ })
+ }
+ })
+ }, (err) => {
+ t.type(err, errors.RequestAbortedError)
+ })
+ })
+})
+
+test('abort', (t) => {
+ t.plan(2)
+
+ const server = createServer((req, res) => {
+ res.end()
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ client.dispatch({
+ method: 'GET',
+ path: '/'
+ }, {
+ onConnect (abort) {
+ setImmediate(abort)
+ },
+ onHeaders () {
+ t.fail()
+ },
+ onData () {
+ t.fail()
+ },
+ onComplete () {
+ t.fail()
+ },
+ onError (err) {
+ t.type(err, errors.RequestAbortedError)
+ }
+ })
+
+ client.on('disconnect', () => {
+ t.pass()
+ })
+ })
+})
+
+test('abort pipelined', (t) => {
+ t.plan(6)
+
+ const server = createServer((req, res) => {
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, {
+ pipelining: 2
+ })
+ t.teardown(client.destroy.bind(client))
+
+ let counter = 0
+ client.dispatch({
+ method: 'GET',
+ path: '/'
+ }, {
+ onConnect (abort) {
+ // This request will be retried
+ if (counter++ === 1) {
+ abort()
+ }
+ t.pass()
+ },
+ onHeaders () {
+ t.fail()
+ },
+ onData () {
+ t.fail()
+ },
+ onComplete () {
+ t.fail()
+ },
+ onError (err) {
+ t.type(err, errors.RequestAbortedError)
+ }
+ })
+
+ client.dispatch({
+ method: 'GET',
+ path: '/'
+ }, {
+ onConnect (abort) {
+ abort()
+ },
+ onHeaders () {
+ t.fail()
+ },
+ onData () {
+ t.fail()
+ },
+ onComplete () {
+ t.fail()
+ },
+ onError (err) {
+ t.type(err, errors.RequestAbortedError)
+ }
+ })
+
+ client.on('disconnect', () => {
+ t.pass()
+ })
+ })
+})
+
+test('propagate unallowed throws in request.onError', (t) => {
+ t.plan(2)
+
+ const server = createServer((req, res) => {
+ res.end()
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ client.dispatch({
+ method: 'GET',
+ path: '/'
+ }, {
+ onConnect (abort) {
+ setImmediate(abort)
+ },
+ onHeaders () {
+ t.pass()
+ },
+ onData () {
+ t.pass()
+ },
+ onComplete () {
+ t.pass()
+ },
+ onError () {
+ throw new OnAbortError('error')
+ }
+ })
+
+ client.on('error', (err) => {
+ t.type(err, OnAbortError)
+ })
+
+ client.on('disconnect', () => {
+ t.pass()
+ })
+ })
+})
diff --git a/test/client-connect.js b/test/client-connect.js
new file mode 100644
index 0000000..7c8ca5e
--- /dev/null
+++ b/test/client-connect.js
@@ -0,0 +1,308 @@
+'use strict'
+
+const { test } = require('tap')
+const { Client, errors } = require('..')
+const http = require('http')
+const EE = require('events')
+const { kBusy } = require('../lib/core/symbols')
+
+test('basic connect', (t) => {
+ t.plan(3)
+
+ const server = http.createServer((c) => {
+ t.fail()
+ })
+ server.on('connect', (req, socket, firstBodyChunk) => {
+ socket.write('HTTP/1.1 200 Connection established\r\n\r\n')
+
+ let data = firstBodyChunk.toString()
+ socket.on('data', (buf) => {
+ data += buf.toString()
+ })
+
+ socket.on('end', () => {
+ socket.end(data)
+ })
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, async () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.close.bind(client))
+
+ const signal = new EE()
+ const promise = client.connect({
+ signal,
+ path: '/'
+ })
+ t.equal(signal.listenerCount('abort'), 1)
+ const { socket } = await promise
+ t.equal(signal.listenerCount('abort'), 0)
+
+ let recvData = ''
+ socket.on('data', (d) => {
+ recvData += d
+ })
+
+ socket.on('end', () => {
+ t.equal(recvData.toString(), 'Body')
+ })
+
+ socket.write('Body')
+ socket.end()
+ })
+})
+
+test('connect error', (t) => {
+ t.plan(1)
+
+ const server = http.createServer((c) => {
+ t.fail()
+ })
+ server.on('connect', (req, socket, firstBodyChunk) => {
+ socket.destroy()
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, async () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.close.bind(client))
+
+ try {
+ await client.connect({
+ path: '/'
+ })
+ } catch (err) {
+ t.ok(err)
+ }
+ })
+})
+
+test('connect invalid opts', (t) => {
+ t.plan(6)
+
+ const client = new Client('http://localhost:5432')
+
+ client.connect(null, err => {
+ t.type(err, errors.InvalidArgumentError)
+ t.equal(err.message, 'invalid opts')
+ })
+
+ try {
+ client.connect(null, null)
+ t.fail()
+ } catch (err) {
+ t.type(err, errors.InvalidArgumentError)
+ t.equal(err.message, 'invalid opts')
+ }
+
+ try {
+ client.connect({ path: '/' }, null)
+ t.fail()
+ } catch (err) {
+ t.type(err, errors.InvalidArgumentError)
+ t.equal(err.message, 'invalid callback')
+ }
+})
+
+test('connect wait for empty pipeline', (t) => {
+ t.plan(7)
+
+ let canConnect = false
+ const server = http.createServer((req, res) => {
+ res.end()
+ canConnect = true
+ })
+ server.on('connect', (req, socket, firstBodyChunk) => {
+ t.equal(canConnect, true)
+ socket.write('HTTP/1.1 200 Connection established\r\n\r\n')
+
+ let data = firstBodyChunk.toString()
+ socket.on('data', (buf) => {
+ data += buf.toString()
+ })
+
+ socket.on('end', () => {
+ socket.end(data)
+ })
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, async () => {
+ const client = new Client(`http://localhost:${server.address().port}`, {
+ pipelining: 3
+ })
+ t.teardown(client.close.bind(client))
+
+ client.request({
+ path: '/',
+ method: 'GET'
+ }, (err) => {
+ t.error(err)
+ })
+ client.once('connect', () => {
+ process.nextTick(() => {
+ t.equal(client[kBusy], false)
+
+ client.connect({
+ path: '/'
+ }, (err, { socket }) => {
+ t.error(err)
+ let recvData = ''
+ socket.on('data', (d) => {
+ recvData += d
+ })
+
+ socket.on('end', () => {
+ t.equal(recvData.toString(), 'Body')
+ })
+
+ socket.write('Body')
+ socket.end()
+ })
+ t.equal(client[kBusy], true)
+
+ client.request({
+ path: '/',
+ method: 'GET'
+ }, (err) => {
+ t.error(err)
+ })
+ })
+ })
+ })
+})
+
+test('connect aborted', (t) => {
+ t.plan(6)
+
+ const server = http.createServer((req, res) => {
+ t.fail()
+ })
+ server.on('connect', (req, c, firstBodyChunk) => {
+ t.fail()
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, {
+ pipelining: 3
+ })
+ t.teardown(client.destroy.bind(client))
+
+ const signal = new EE()
+ client.connect({
+ path: '/',
+ signal,
+ opaque: 'asd'
+ }, (err, { opaque }) => {
+ t.equal(opaque, 'asd')
+ t.equal(signal.listenerCount('abort'), 0)
+ t.type(err, errors.RequestAbortedError)
+ })
+ t.equal(client[kBusy], true)
+ t.equal(signal.listenerCount('abort'), 1)
+ signal.emit('abort')
+
+ client.close(() => {
+ t.pass()
+ })
+ })
+})
+
+test('basic connect error', (t) => {
+ t.plan(2)
+
+ const server = http.createServer((c) => {
+ t.fail()
+ })
+ server.on('connect', (req, socket, firstBodyChunk) => {
+ socket.write('HTTP/1.1 200 Connection established\r\n\r\n')
+
+ let data = firstBodyChunk.toString()
+ socket.on('data', (buf) => {
+ data += buf.toString()
+ })
+
+ socket.on('end', () => {
+ socket.end(data)
+ })
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, async () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.close.bind(client))
+
+ const _err = new Error()
+ client.connect({
+ path: '/'
+ }, (err, { socket }) => {
+ t.error(err)
+ socket.on('error', (err) => {
+ t.equal(err, _err)
+ })
+ throw _err
+ })
+ })
+})
+
+test('connect invalid signal', (t) => {
+ t.plan(2)
+
+ const server = http.createServer((req, res) => {
+ t.fail()
+ })
+ server.on('connect', (req, c, firstBodyChunk) => {
+ t.fail()
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ client.on('disconnect', () => {
+ t.fail()
+ })
+
+ client.connect({
+ path: '/',
+ signal: 'error',
+ opaque: 'asd'
+ }, (err, { opaque }) => {
+ t.equal(opaque, 'asd')
+ t.type(err, errors.InvalidArgumentError)
+ })
+ })
+})
+
+test('connect aborted after connect', (t) => {
+ t.plan(3)
+
+ const signal = new EE()
+ const server = http.createServer((req, res) => {
+ t.fail()
+ })
+ server.on('connect', (req, c, firstBodyChunk) => {
+ signal.emit('abort')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, {
+ pipelining: 3
+ })
+ t.teardown(client.destroy.bind(client))
+
+ client.connect({
+ path: '/',
+ signal,
+ opaque: 'asd'
+ }, (err, { opaque }) => {
+ t.equal(opaque, 'asd')
+ t.type(err, errors.RequestAbortedError)
+ })
+ t.equal(client[kBusy], true)
+ })
+})
diff --git a/test/client-dispatch.js b/test/client-dispatch.js
new file mode 100644
index 0000000..c3de37a
--- /dev/null
+++ b/test/client-dispatch.js
@@ -0,0 +1,815 @@
+'use strict'
+
+const { test } = require('tap')
+const http = require('http')
+const { Client, Pool, errors } = require('..')
+const stream = require('stream')
+
+test('dispatch invalid opts', (t) => {
+ t.plan(14)
+
+ const client = new Client('http://localhost:5000')
+
+ try {
+ client.dispatch({
+ path: '/',
+ method: 'GET',
+ upgrade: 1
+ }, null)
+ } catch (err) {
+ t.type(err, errors.InvalidArgumentError)
+ t.equal(err.message, 'handler must be an object')
+ }
+
+ try {
+ client.dispatch({
+ path: '/',
+ method: 'GET',
+ upgrade: 1
+ }, 'asd')
+ } catch (err) {
+ t.type(err, errors.InvalidArgumentError)
+ t.equal(err.message, 'handler must be an object')
+ }
+
+ client.dispatch({
+ path: '/',
+ method: 'GET',
+ upgrade: 1
+ }, {
+ onError (err) {
+ t.type(err, errors.InvalidArgumentError)
+ t.equal(err.message, 'upgrade must be a string')
+ }
+ })
+
+ client.dispatch({
+ path: '/',
+ method: 'GET',
+ headersTimeout: 'asd'
+ }, {
+ onError (err) {
+ t.type(err, errors.InvalidArgumentError)
+ t.equal(err.message, 'invalid headersTimeout')
+ }
+ })
+
+ client.dispatch({
+ path: '/',
+ method: 'GET',
+ bodyTimeout: 'asd'
+ }, {
+ onError (err) {
+ t.type(err, errors.InvalidArgumentError)
+ t.equal(err.message, 'invalid bodyTimeout')
+ }
+ })
+
+ client.dispatch({
+ origin: 'another',
+ path: '/',
+ method: 'GET',
+ bodyTimeout: 'asd'
+ }, {
+ onError (err) {
+ t.type(err, errors.InvalidArgumentError)
+ t.equal(err.message, 'invalid bodyTimeout')
+ }
+ })
+
+ client.dispatch(null, {
+ onError (err) {
+ t.type(err, errors.InvalidArgumentError)
+ t.equal(err.message, 'opts must be an object.')
+ }
+ })
+})
+
+test('basic dispatch get', (t) => {
+ t.plan(11)
+
+ const server = http.createServer((req, res) => {
+ t.equal('/', req.url)
+ t.equal('GET', req.method)
+ t.equal(`localhost:${server.address().port}`, req.headers.host)
+ t.equal(undefined, req.headers.foo)
+ t.equal('bar', req.headers.bar)
+ t.equal('', req.headers.baz)
+ t.equal(undefined, req.headers['content-length'])
+ res.end('hello')
+ })
+ t.teardown(server.close.bind(server))
+
+ const reqHeaders = {
+ foo: undefined,
+ bar: 'bar',
+ baz: null
+ }
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.close.bind(client))
+
+ const bufs = []
+ client.dispatch({
+ path: '/',
+ method: 'GET',
+ headers: reqHeaders
+ }, {
+ onConnect () {
+ },
+ onHeaders (statusCode, headers) {
+ t.equal(statusCode, 200)
+ t.equal(Array.isArray(headers), true)
+ },
+ onData (buf) {
+ bufs.push(buf)
+ },
+ onComplete (trailers) {
+ t.same(trailers, [])
+ t.equal('hello', Buffer.concat(bufs).toString('utf8'))
+ },
+ onError () {
+ t.fail()
+ }
+ })
+ })
+})
+
+test('trailers dispatch get', (t) => {
+ t.plan(12)
+
+ const server = http.createServer((req, res) => {
+ t.equal('/', req.url)
+ t.equal('GET', req.method)
+ t.equal(`localhost:${server.address().port}`, req.headers.host)
+ t.equal(undefined, req.headers.foo)
+ t.equal('bar', req.headers.bar)
+ t.equal(undefined, req.headers['content-length'])
+ res.addTrailers({ 'Content-MD5': 'test' })
+ res.setHeader('Content-Type', 'text/plain')
+ res.setHeader('Trailer', 'Content-MD5')
+ res.end('hello')
+ })
+ t.teardown(server.close.bind(server))
+
+ const reqHeaders = {
+ foo: undefined,
+ bar: 'bar'
+ }
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.close.bind(client))
+
+ const bufs = []
+ client.dispatch({
+ path: '/',
+ method: 'GET',
+ headers: reqHeaders
+ }, {
+ onConnect () {
+ },
+ onHeaders (statusCode, headers) {
+ t.equal(statusCode, 200)
+ t.equal(Array.isArray(headers), true)
+ {
+ const contentTypeIdx = headers.findIndex(x => x.toString() === 'Content-Type')
+ t.equal(headers[contentTypeIdx + 1].toString(), 'text/plain')
+ }
+ },
+ onData (buf) {
+ bufs.push(buf)
+ },
+ onComplete (trailers) {
+ t.equal(Array.isArray(trailers), true)
+ {
+ const contentMD5Idx = trailers.findIndex(x => x.toString() === 'Content-MD5')
+ t.equal(trailers[contentMD5Idx + 1].toString(), 'test')
+ }
+ t.equal('hello', Buffer.concat(bufs).toString('utf8'))
+ },
+ onError () {
+ t.fail()
+ }
+ })
+ })
+})
+
+test('dispatch onHeaders error', (t) => {
+ t.plan(1)
+
+ const server = http.createServer((req, res) => {
+ res.end()
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.close.bind(client))
+
+ const _err = new Error()
+ client.dispatch({
+ path: '/',
+ method: 'GET'
+ }, {
+ onConnect () {
+ },
+ onHeaders (statusCode, headers) {
+ throw _err
+ },
+ onData (buf) {
+ t.fail()
+ },
+ onComplete (trailers) {
+ t.fail()
+ },
+ onError (err) {
+ t.equal(err, _err)
+ }
+ })
+ })
+})
+
+test('dispatch onComplete error', (t) => {
+ t.plan(2)
+
+ const server = http.createServer((req, res) => {
+ res.end()
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.close.bind(client))
+
+ const _err = new Error()
+ client.dispatch({
+ path: '/',
+ method: 'GET'
+ }, {
+ onConnect () {
+ },
+ onHeaders (statusCode, headers) {
+ t.pass()
+ },
+ onData (buf) {
+ t.fail()
+ },
+ onComplete (trailers) {
+ throw _err
+ },
+ onError (err) {
+ t.equal(err, _err)
+ }
+ })
+ })
+})
+
+test('dispatch onData error', (t) => {
+ t.plan(2)
+
+ const server = http.createServer((req, res) => {
+ res.end('ad')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.close.bind(client))
+
+ const _err = new Error()
+ client.dispatch({
+ path: '/',
+ method: 'GET'
+ }, {
+ onConnect () {
+ },
+ onHeaders (statusCode, headers) {
+ t.pass()
+ },
+ onData (buf) {
+ throw _err
+ },
+ onComplete (trailers) {
+ t.fail()
+ },
+ onError (err) {
+ t.equal(err, _err)
+ }
+ })
+ })
+})
+
+test('dispatch onConnect error', (t) => {
+ t.plan(1)
+
+ const server = http.createServer((req, res) => {
+ res.end('ad')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.close.bind(client))
+
+ const _err = new Error()
+ client.dispatch({
+ path: '/',
+ method: 'GET'
+ }, {
+ onConnect () {
+ throw _err
+ },
+ onHeaders (statusCode, headers) {
+ t.fail()
+ },
+ onData (buf) {
+ t.fail()
+ },
+ onComplete (trailers) {
+ t.fail()
+ },
+ onError (err) {
+ t.equal(err, _err)
+ }
+ })
+ })
+})
+
+test('connect call onUpgrade once', (t) => {
+ t.plan(2)
+
+ const server = http.createServer((c) => {
+ t.fail()
+ })
+ server.on('connect', (req, socket, firstBodyChunk) => {
+ socket.write('HTTP/1.1 200 Connection established\r\n\r\n')
+
+ let data = firstBodyChunk.toString()
+ socket.on('data', (buf) => {
+ data += buf.toString()
+ })
+
+ socket.on('end', () => {
+ socket.end(data)
+ })
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, async () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.close.bind(client))
+
+ let recvData = ''
+ let count = 0
+ client.dispatch({
+ method: 'CONNECT',
+ path: '/'
+ }, {
+ onConnect () {
+ },
+ onHeaders (statusCode, headers) {
+ t.pass('should not throw')
+ },
+ onUpgrade (statusCode, headers, socket) {
+ t.equal(count++, 0)
+
+ socket.on('data', (d) => {
+ recvData += d
+ })
+
+ socket.on('end', () => {
+ t.equal(recvData.toString(), 'Body')
+ })
+
+ socket.write('Body')
+ socket.end()
+ },
+ onData (buf) {
+ t.fail()
+ },
+ onComplete (trailers) {
+ t.fail()
+ },
+ onError () {
+ t.fail()
+ }
+ })
+ })
+})
+
+test('dispatch onConnect missing', (t) => {
+ t.plan(1)
+
+ const server = http.createServer((req, res) => {
+ res.end('ad')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.close.bind(client))
+
+ client.dispatch({
+ path: '/',
+ method: 'GET'
+ }, {
+ onHeaders (statusCode, headers) {
+ t.pass('should not throw')
+ },
+ onData (buf) {
+ t.pass('should not throw')
+ },
+ onComplete (trailers) {
+ t.pass('should not throw')
+ },
+ onError (err) {
+ t.equal(err.code, 'UND_ERR_INVALID_ARG')
+ }
+ })
+ })
+})
+
+test('dispatch onHeaders missing', (t) => {
+ t.plan(1)
+
+ const server = http.createServer((req, res) => {
+ res.end('ad')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.close.bind(client))
+
+ client.dispatch({
+ path: '/',
+ method: 'GET'
+ }, {
+ onConnect () {
+ },
+ onData (buf) {
+ t.fail('should not throw')
+ },
+ onComplete (trailers) {
+ t.fail('should not throw')
+ },
+ onError (err) {
+ t.equal(err.code, 'UND_ERR_INVALID_ARG')
+ }
+ })
+ })
+})
+
+test('dispatch onData missing', (t) => {
+ t.plan(1)
+
+ const server = http.createServer((req, res) => {
+ res.end('ad')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.close.bind(client))
+
+ client.dispatch({
+ path: '/',
+ method: 'GET'
+ }, {
+ onConnect () {
+ },
+ onHeaders (statusCode, headers) {
+ t.fail('should not throw')
+ },
+ onComplete (trailers) {
+ t.fail('should not throw')
+ },
+ onError (err) {
+ t.equal(err.code, 'UND_ERR_INVALID_ARG')
+ }
+ })
+ })
+})
+
+test('dispatch onComplete missing', (t) => {
+ t.plan(1)
+
+ const server = http.createServer((req, res) => {
+ res.end('ad')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.close.bind(client))
+
+ client.dispatch({
+ path: '/',
+ method: 'GET'
+ }, {
+ onConnect () {
+ },
+ onHeaders (statusCode, headers) {
+ t.fail()
+ },
+ onData (buf) {
+ t.fail()
+ },
+ onError (err) {
+ t.equal(err.code, 'UND_ERR_INVALID_ARG')
+ }
+ })
+ })
+})
+
+test('dispatch onError missing', (t) => {
+ t.plan(1)
+
+ const server = http.createServer((req, res) => {
+ res.end('ad')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.close.bind(client))
+
+ try {
+ client.dispatch({
+ path: '/',
+ method: 'GET'
+ }, {
+ onConnect () {
+ },
+ onHeaders (statusCode, headers) {
+ t.fail()
+ },
+ onData (buf) {
+ t.fail()
+ },
+ onComplete (trailers) {
+ t.fail()
+ }
+ })
+ } catch (err) {
+ t.equal(err.code, 'UND_ERR_INVALID_ARG')
+ }
+ })
+})
+
+test('dispatch CONNECT onUpgrade missing', (t) => {
+ t.plan(2)
+
+ const server = http.createServer((req, res) => {
+ res.end('ad')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ client.dispatch({
+ path: '/',
+ method: 'GET',
+ upgrade: 'Websocket'
+ }, {
+ onConnect () {
+ },
+ onHeaders (statusCode, headers) {
+ },
+ onError (err) {
+ t.equal(err.code, 'UND_ERR_INVALID_ARG')
+ t.equal(err.message, 'invalid onUpgrade method')
+ }
+ })
+ })
+})
+
+test('dispatch upgrade onUpgrade missing', (t) => {
+ t.plan(2)
+
+ const server = http.createServer((req, res) => {
+ res.end('ad')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.close.bind(client))
+
+ client.dispatch({
+ path: '/',
+ method: 'GET',
+ upgrade: 'Websocket'
+ }, {
+ onConnect () {
+ },
+ onHeaders (statusCode, headers) {
+ },
+ onError (err) {
+ t.equal(err.code, 'UND_ERR_INVALID_ARG')
+ t.equal(err.message, 'invalid onUpgrade method')
+ }
+ })
+ })
+})
+
+test('dispatch pool onError missing', (t) => {
+ t.plan(2)
+
+ const server = http.createServer((req, res) => {
+ res.end('ad')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Pool(`http://localhost:${server.address().port}`)
+ t.teardown(client.close.bind(client))
+
+ try {
+ client.dispatch({
+ path: '/',
+ method: 'GET',
+ upgrade: 'Websocket'
+ }, {
+ })
+ } catch (err) {
+ t.equal(err.code, 'UND_ERR_INVALID_ARG')
+ t.equal(err.message, 'invalid onError method')
+ }
+ })
+})
+
+test('dispatch onBodySent not a function', (t) => {
+ t.plan(2)
+ const server = http.createServer((req, res) => {
+ res.end('ad')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Pool(`http://localhost:${server.address().port}`)
+ t.teardown(client.close.bind(client))
+
+ client.dispatch({
+ path: '/',
+ method: 'GET'
+ }, {
+ onBodySent: '42',
+ onConnect () {},
+ onHeaders () {},
+ onData () {},
+ onError (err) {
+ t.equal(err.code, 'UND_ERR_INVALID_ARG')
+ t.equal(err.message, 'invalid onBodySent method')
+ }
+ })
+ })
+})
+
+test('dispatch onBodySent buffer', (t) => {
+ t.plan(3)
+
+ const server = http.createServer((req, res) => {
+ res.end('ad')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Pool(`http://localhost:${server.address().port}`)
+ t.teardown(client.close.bind(client))
+ const body = 'hello 🚀'
+ client.dispatch({
+ path: '/',
+ method: 'POST',
+ body
+ }, {
+ onBodySent (chunk) {
+ t.equal(chunk.toString(), body)
+ },
+ onRequestSent () {
+ t.pass()
+ },
+ onError (err) {
+ throw err
+ },
+ onConnect () {},
+ onHeaders () {},
+ onData () {},
+ onComplete () {
+ t.pass()
+ }
+ })
+ })
+})
+
+test('dispatch onBodySent stream', (t) => {
+ t.plan(8)
+ const server = http.createServer((req, res) => {
+ res.end('ad')
+ })
+ t.teardown(server.close.bind(server))
+ const chunks = ['he', 'llo', 'world', '🚀']
+ const toSendBytes = chunks.reduce((a, b) => a + Buffer.byteLength(b), 0)
+ const body = stream.Readable.from(chunks)
+ server.listen(0, () => {
+ const client = new Pool(`http://localhost:${server.address().port}`)
+ t.teardown(client.close.bind(client))
+ let sentBytes = 0
+ let currentChunk = 0
+ client.dispatch({
+ path: '/',
+ method: 'POST',
+ body
+ }, {
+ onBodySent (chunk) {
+ t.equal(chunks[currentChunk++], chunk)
+ sentBytes += Buffer.byteLength(chunk)
+ },
+ onRequestSent () {
+ t.pass()
+ },
+ onError (err) {
+ throw err
+ },
+ onConnect () {},
+ onHeaders () {},
+ onData () {},
+ onComplete () {
+ t.equal(currentChunk, chunks.length)
+ t.equal(sentBytes, toSendBytes)
+ t.pass()
+ }
+ })
+ })
+})
+
+test('dispatch onBodySent async-iterable', (t) => {
+ const server = http.createServer((req, res) => {
+ res.end('ad')
+ })
+ t.teardown(server.close.bind(server))
+ const chunks = ['he', 'llo', 'world', '🚀']
+ const toSendBytes = chunks.reduce((a, b) => a + Buffer.byteLength(b), 0)
+ server.listen(0, () => {
+ const client = new Pool(`http://localhost:${server.address().port}`)
+ t.teardown(client.close.bind(client))
+ let sentBytes = 0
+ let currentChunk = 0
+ client.dispatch({
+ path: '/',
+ method: 'POST',
+ body: chunks
+ }, {
+ onBodySent (chunk) {
+ t.equal(chunks[currentChunk++], chunk)
+ sentBytes += Buffer.byteLength(chunk)
+ },
+ onError (err) {
+ throw err
+ },
+ onConnect () {},
+ onHeaders () {},
+ onData () {},
+ onComplete () {
+ t.equal(currentChunk, chunks.length)
+ t.equal(sentBytes, toSendBytes)
+ t.end()
+ }
+ })
+ })
+})
+
+test('dispatch onBodySent throws error', (t) => {
+ const server = http.createServer((req, res) => {
+ res.end('ended')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Pool(`http://localhost:${server.address().port}`)
+ t.teardown(client.close.bind(client))
+ const body = 'hello'
+ client.dispatch({
+ path: '/',
+ method: 'POST',
+ body
+ }, {
+ onBodySent (chunk) {
+ throw new Error('fail')
+ },
+ onError (err) {
+ t.type(err, Error)
+ t.equal(err.message, 'fail')
+ t.end()
+ },
+ onConnect () {},
+ onHeaders () {},
+ onData () {},
+ onComplete () {}
+ })
+ })
+})
diff --git a/test/client-errors.js b/test/client-errors.js
new file mode 100644
index 0000000..cec7f37
--- /dev/null
+++ b/test/client-errors.js
@@ -0,0 +1,1285 @@
+'use strict'
+
+const { test } = require('tap')
+const { Client, Pool, errors } = require('..')
+const { createServer } = require('http')
+const https = require('https')
+const pem = require('https-pem')
+const net = require('net')
+const { Readable } = require('stream')
+
+const { kSocket } = require('../lib/core/symbols')
+const { wrapWithAsyncIterable, maybeWrapStream, consts } = require('./utils/async-iterators')
+
+class IteratorError extends Error {}
+
+test('GET errors and reconnect with pipelining 1', (t) => {
+ t.plan(9)
+
+ const server = createServer()
+
+ server.once('request', (req, res) => {
+ t.pass('first request received, destroying')
+ res.socket.destroy()
+
+ server.once('request', (req, res) => {
+ t.equal('/', req.url)
+ t.equal('GET', req.method)
+ res.setHeader('content-type', 'text/plain')
+ res.end('hello')
+ })
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, {
+ pipelining: 1
+ })
+ t.teardown(client.destroy.bind(client))
+
+ client.request({ path: '/', method: 'GET', idempotent: false, opaque: 'asd' }, (err, data) => {
+ t.type(err, Error) // we are expecting an error
+ t.equal(data.opaque, 'asd')
+ })
+
+ client.request({ path: '/', method: 'GET' }, (err, { statusCode, headers, body }) => {
+ t.error(err)
+ t.equal(statusCode, 200)
+ t.equal(headers['content-type'], 'text/plain')
+ const bufs = []
+ body.on('data', (buf) => {
+ bufs.push(buf)
+ })
+ body.on('end', () => {
+ t.equal('hello', Buffer.concat(bufs).toString('utf8'))
+ })
+ })
+ })
+})
+
+test('GET errors and reconnect with pipelining 3', (t) => {
+ const server = createServer()
+ const requestsThatWillError = 3
+ let requests = 0
+
+ t.plan(6 + requestsThatWillError * 3)
+
+ server.on('request', (req, res) => {
+ if (requests++ < requestsThatWillError) {
+ t.pass('request received, destroying')
+
+ // socket might not be there if it was destroyed by another
+ // pipelined request
+ if (res.socket) {
+ res.socket.destroy()
+ }
+ } else {
+ t.equal('/', req.url)
+ t.equal('GET', req.method)
+ res.setHeader('content-type', 'text/plain')
+ res.end('hello')
+ }
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, {
+ pipelining: 3
+ })
+ t.teardown(client.destroy.bind(client))
+
+ // all of these will error
+ for (let i = 0; i < 3; i++) {
+ client.request({ path: '/', method: 'GET', idempotent: false, opaque: 'asd' }, (err, data) => {
+ t.type(err, Error) // we are expecting an error
+ t.equal(data.opaque, 'asd')
+ })
+ }
+
+ // this will be queued up
+ client.request({ path: '/', method: 'GET', idempotent: false }, (err, { statusCode, headers, body }) => {
+ t.error(err)
+ t.equal(statusCode, 200)
+ t.equal(headers['content-type'], 'text/plain')
+ const bufs = []
+ body.on('data', (buf) => {
+ bufs.push(buf)
+ })
+ body.on('end', () => {
+ t.equal('hello', Buffer.concat(bufs).toString('utf8'))
+ })
+ })
+ })
+})
+
+function errorAndPipelining (type) {
+ test(`POST with a ${type} that errors and pipelining 1 should reconnect`, (t) => {
+ t.plan(12)
+
+ const server = createServer()
+ server.once('request', (req, res) => {
+ t.equal('/', req.url)
+ t.equal('POST', req.method)
+ t.equal('42', req.headers['content-length'])
+
+ const bufs = []
+ req.on('data', (buf) => {
+ bufs.push(buf)
+ })
+
+ req.on('aborted', () => {
+ // we will abruptly close the connection here
+ // but this will still end
+ t.equal('a string', Buffer.concat(bufs).toString('utf8'))
+ })
+
+ server.once('request', (req, res) => {
+ t.equal('/', req.url)
+ t.equal('GET', req.method)
+ res.setHeader('content-type', 'text/plain')
+ res.end('hello')
+ })
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ client.request({
+ path: '/',
+ method: 'POST',
+ headers: {
+ // higher than the length of the string
+ 'content-length': 42
+ },
+ opaque: 'asd',
+ body: maybeWrapStream(new Readable({
+ read () {
+ this.push('a string')
+ this.destroy(new Error('kaboom'))
+ }
+ }), type)
+ }, (err, data) => {
+ t.equal(err.message, 'kaboom')
+ t.equal(data.opaque, 'asd')
+ })
+
+ // this will be queued up
+ client.request({ path: '/', method: 'GET', idempotent: false }, (err, { statusCode, headers, body }) => {
+ t.error(err)
+ t.equal(statusCode, 200)
+ t.equal(headers['content-type'], 'text/plain')
+ const bufs = []
+ body.on('data', (buf) => {
+ bufs.push(buf)
+ })
+ body.on('end', () => {
+ t.equal('hello', Buffer.concat(bufs).toString('utf8'))
+ })
+ })
+ })
+ })
+}
+
+errorAndPipelining(consts.STREAM)
+errorAndPipelining(consts.ASYNC_ITERATOR)
+
+function errorAndChunkedEncodingPipelining (type) {
+ test(`POST with chunked encoding, ${type} body that errors and pipelining 1 should reconnect`, (t) => {
+ t.plan(12)
+
+ const server = createServer()
+ server.once('request', (req, res) => {
+ t.equal('/', req.url)
+ t.equal('POST', req.method)
+ t.equal(req.headers['content-length'], undefined)
+
+ const bufs = []
+ req.on('data', (buf) => {
+ bufs.push(buf)
+ })
+
+ req.on('aborted', () => {
+ // we will abruptly close the connection here
+ // but this will still end
+ t.equal('a string', Buffer.concat(bufs).toString('utf8'))
+ })
+
+ server.once('request', (req, res) => {
+ t.equal('/', req.url)
+ t.equal('GET', req.method)
+ res.setHeader('content-type', 'text/plain')
+ res.end('hello')
+ })
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ client.request({
+ path: '/',
+ method: 'POST',
+ opaque: 'asd',
+ body: maybeWrapStream(new Readable({
+ read () {
+ this.push('a string')
+ this.destroy(new Error('kaboom'))
+ }
+ }), type)
+ }, (err, data) => {
+ t.equal(err.message, 'kaboom')
+ t.equal(data.opaque, 'asd')
+ })
+
+ // this will be queued up
+ client.request({ path: '/', method: 'GET' }, (err, { statusCode, headers, body }) => {
+ t.error(err)
+ t.equal(statusCode, 200)
+ t.equal(headers['content-type'], 'text/plain')
+ const bufs = []
+ body.on('data', (buf) => {
+ bufs.push(buf)
+ })
+ body.on('end', () => {
+ t.equal('hello', Buffer.concat(bufs).toString('utf8'))
+ })
+ })
+ })
+ })
+}
+
+errorAndChunkedEncodingPipelining(consts.STREAM)
+errorAndChunkedEncodingPipelining(consts.ASYNC_ITERATOR)
+
+test('invalid options throws', (t) => {
+ try {
+ new Client({ port: 'foobar', protocol: 'https:' }) // eslint-disable-line
+ t.fail()
+ } catch (err) {
+ t.type(err, errors.InvalidArgumentError)
+ t.equal(err.message, 'Invalid URL: port must be a valid integer or a string representation of an integer.')
+ }
+
+ try {
+ new Client(new URL('http://asd:200/somepath')) // eslint-disable-line
+ t.fail()
+ } catch (err) {
+ t.type(err, errors.InvalidArgumentError)
+ t.equal(err.message, 'invalid url')
+ }
+
+ try {
+ new Client(new URL('http://asd:200?q=asd')) // eslint-disable-line
+ t.fail()
+ } catch (err) {
+ t.type(err, errors.InvalidArgumentError)
+ t.equal(err.message, 'invalid url')
+ }
+
+ try {
+ new Client(new URL('http://asd:200#asd')) // eslint-disable-line
+ t.fail()
+ } catch (err) {
+ t.type(err, errors.InvalidArgumentError)
+ t.equal(err.message, 'invalid url')
+ }
+
+ try {
+ new Client(new URL('http://localhost:200'), { // eslint-disable-line
+ socketPath: 1
+ })
+ t.fail()
+ } catch (err) {
+ t.type(err, errors.InvalidArgumentError)
+ t.equal(err.message, 'invalid socketPath')
+ }
+
+ try {
+ new Client(new URL('http://localhost:200'), { // eslint-disable-line
+ keepAliveTimeout: 'asd'
+ }) // eslint-disable-line
+ t.fail()
+ } catch (err) {
+ t.type(err, errors.InvalidArgumentError)
+ t.equal(err.message, 'invalid keepAliveTimeout')
+ }
+
+ try {
+ new Client(new URL('http://localhost:200'), { // eslint-disable-line
+ localAddress: 123
+ }) // eslint-disable-line
+ t.fail()
+ } catch (err) {
+ t.type(err, errors.InvalidArgumentError)
+ t.equal(err.message, 'localAddress must be valid string IP address')
+ }
+
+ try {
+ new Client(new URL('http://localhost:200'), { // eslint-disable-line
+ localAddress: 'abcd123'
+ }) // eslint-disable-line
+ t.fail()
+ } catch (err) {
+ t.type(err, errors.InvalidArgumentError)
+ t.equal(err.message, 'localAddress must be valid string IP address')
+ }
+
+ try {
+ new Client(new URL('http://localhost:200'), { // eslint-disable-line
+ keepAliveMaxTimeout: 'asd'
+ }) // eslint-disable-line
+ t.fail()
+ } catch (err) {
+ t.type(err, errors.InvalidArgumentError)
+ t.equal(err.message, 'invalid keepAliveMaxTimeout')
+ }
+
+ try {
+ new Client(new URL('http://localhost:200'), { // eslint-disable-line
+ keepAliveMaxTimeout: 0
+ }) // eslint-disable-line
+ t.fail()
+ } catch (err) {
+ t.type(err, errors.InvalidArgumentError)
+ t.equal(err.message, 'invalid keepAliveMaxTimeout')
+ }
+
+ try {
+ new Client(new URL('http://localhost:200'), { // eslint-disable-line
+ keepAliveTimeoutThreshold: 'asd'
+ }) // eslint-disable-line
+ t.fail()
+ } catch (err) {
+ t.type(err, errors.InvalidArgumentError)
+ t.equal(err.message, 'invalid keepAliveTimeoutThreshold')
+ }
+
+ try {
+ new Client({ // eslint-disable-line
+ protocol: 'asd'
+ })
+ t.fail()
+ } catch (err) {
+ t.type(err, errors.InvalidArgumentError)
+ t.equal(err.message, 'Invalid URL protocol: the URL must start with `http:` or `https:`.')
+ }
+
+ try {
+ new Client({ // eslint-disable-line
+ hostname: 1
+ })
+ t.fail()
+ } catch (err) {
+ t.type(err, errors.InvalidArgumentError)
+ t.equal(err.message, 'Invalid URL protocol: the URL must start with `http:` or `https:`.')
+ }
+
+ try {
+ new Client(new URL('http://localhost:200'), { // eslint-disable-line
+ maxHeaderSize: 'asd'
+ })
+ t.fail()
+ } catch (err) {
+ t.type(err, errors.InvalidArgumentError)
+ t.equal(err.message, 'invalid maxHeaderSize')
+ }
+
+ try {
+ new Client(1) // eslint-disable-line
+ t.fail()
+ } catch (err) {
+ t.type(err, errors.InvalidArgumentError)
+ t.equal(err.message, 'Invalid URL: The URL argument must be a non-null object.')
+ }
+
+ try {
+ const client = new Client(new URL('http://localhost:200')) // eslint-disable-line
+ client.destroy(null, null)
+ t.fail()
+ } catch (err) {
+ t.type(err, errors.InvalidArgumentError)
+ t.equal(err.message, 'invalid callback')
+ }
+
+ try {
+ const client = new Client(new URL('http://localhost:200')) // eslint-disable-line
+ client.close(null, null)
+ t.fail()
+ } catch (err) {
+ t.type(err, errors.InvalidArgumentError)
+ t.equal(err.message, 'invalid callback')
+ }
+
+ try {
+ new Client(new URL('http://localhost:200'), { maxKeepAliveTimeout: 1e3 }) // eslint-disable-line
+ t.fail()
+ } catch (err) {
+ t.type(err, errors.InvalidArgumentError)
+ t.equal(err.message, 'unsupported maxKeepAliveTimeout, use keepAliveMaxTimeout instead')
+ }
+
+ try {
+ new Client(new URL('http://localhost:200'), { keepAlive: false }) // eslint-disable-line
+ t.fail()
+ } catch (err) {
+ t.type(err, errors.InvalidArgumentError)
+ t.equal(err.message, 'unsupported keepAlive, use pipelining=0 instead')
+ }
+
+ try {
+ new Client(new URL('http://localhost:200'), { idleTimeout: 30e3 }) // eslint-disable-line
+ t.fail()
+ } catch (err) {
+ t.type(err, errors.InvalidArgumentError)
+ t.equal(err.message, 'unsupported idleTimeout, use keepAliveTimeout instead')
+ }
+
+ try {
+ new Client(new URL('http://localhost:200'), { socketTimeout: 30e3 }) // eslint-disable-line
+ t.fail()
+ } catch (err) {
+ t.type(err, errors.InvalidArgumentError)
+ t.equal(err.message, 'unsupported socketTimeout, use headersTimeout & bodyTimeout instead')
+ }
+
+ try {
+ new Client(new URL('http://localhost:200'), { requestTimeout: 30e3 }) // eslint-disable-line
+ t.fail()
+ } catch (err) {
+ t.type(err, errors.InvalidArgumentError)
+ t.equal(err.message, 'unsupported requestTimeout, use headersTimeout & bodyTimeout instead')
+ }
+
+ try {
+ new Client(new URL('http://localhost:200'), { connectTimeout: -1 }) // eslint-disable-line
+ t.fail()
+ } catch (err) {
+ t.type(err, errors.InvalidArgumentError)
+ t.equal(err.message, 'invalid connectTimeout')
+ }
+
+ try {
+ new Client(new URL('http://localhost:200'), { connectTimeout: Infinity }) // eslint-disable-line
+ t.fail()
+ } catch (err) {
+ t.type(err, errors.InvalidArgumentError)
+ t.equal(err.message, 'invalid connectTimeout')
+ }
+
+ try {
+ new Client(new URL('http://localhost:200'), { connectTimeout: 'asd' }) // eslint-disable-line
+ t.fail()
+ } catch (err) {
+ t.type(err, errors.InvalidArgumentError)
+ t.equal(err.message, 'invalid connectTimeout')
+ }
+
+ try {
+ new Client(new URL('http://localhost:200'), { connect: 'asd' }) // eslint-disable-line
+ t.fail()
+ } catch (err) {
+ t.type(err, errors.InvalidArgumentError)
+ t.equal(err.message, 'connect must be a function or an object')
+ }
+
+ try {
+ new Client(new URL('http://localhost:200'), { connect: -1 }) // eslint-disable-line
+ t.fail()
+ } catch (err) {
+ t.type(err, errors.InvalidArgumentError)
+ t.equal(err.message, 'connect must be a function or an object')
+ }
+
+ try {
+ new Pool(new URL('http://localhost:200'), { connect: 'asd' }) // eslint-disable-line
+ t.fail()
+ } catch (err) {
+ t.type(err, errors.InvalidArgumentError)
+ t.equal(err.message, 'connect must be a function or an object')
+ }
+
+ try {
+ new Pool(new URL('http://localhost:200'), { connect: -1 }) // eslint-disable-line
+ t.fail()
+ } catch (err) {
+ t.type(err, errors.InvalidArgumentError)
+ t.equal(err.message, 'connect must be a function or an object')
+ }
+
+ try {
+ new Client(new URL('http://localhost:200'), { maxCachedSessions: -10 }) // eslint-disable-line
+ t.fail()
+ } catch (err) {
+ t.type(err, errors.InvalidArgumentError)
+ t.equal(err.message, 'maxCachedSessions must be a positive integer or zero')
+ }
+
+ try {
+ new Client(new URL('http://localhost:200'), { maxCachedSessions: 'foo' }) // eslint-disable-line
+ t.fail()
+ } catch (err) {
+ t.type(err, errors.InvalidArgumentError)
+ t.equal(err.message, 'maxCachedSessions must be a positive integer or zero')
+ }
+
+ try {
+ new Client(new URL('http://localhost:200'), { maxRequestsPerClient: 'foo' }) // eslint-disable-line
+ t.fail()
+ } catch (err) {
+ t.type(err, errors.InvalidArgumentError)
+ t.equal(err.message, 'maxRequestsPerClient must be a positive number')
+ }
+
+ try {
+ new Client(new URL('http://localhost:200'), { autoSelectFamilyAttemptTimeout: 'foo' }) // eslint-disable-line
+ t.fail()
+ } catch (err) {
+ t.type(err, errors.InvalidArgumentError)
+ t.equal(err.message, 'autoSelectFamilyAttemptTimeout must be a positive number')
+ }
+
+ t.end()
+})
+
+test('POST which fails should error response', (t) => {
+ t.plan(6)
+
+ const server = createServer()
+ server.on('request', (req, res) => {
+ req.once('data', () => {
+ res.destroy()
+ })
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ function checkError (err) {
+ // Different platforms error with different codes...
+ t.ok(
+ err.code === 'EPIPE' ||
+ err.code === 'ECONNRESET' ||
+ err.code === 'UND_ERR_SOCKET' ||
+ err.message === 'other side closed'
+ )
+ }
+
+ {
+ const body = new Readable({ read () {} })
+ body.push('asd')
+ body.on('error', (err) => {
+ checkError(err)
+ })
+
+ client.request({
+ path: '/',
+ method: 'POST',
+ body
+ }, (err) => {
+ checkError(err)
+ })
+ }
+
+ {
+ const body = new Readable({ read () {} })
+ body.push('asd')
+ body.on('error', (err) => {
+ checkError(err)
+ })
+
+ client.request({
+ path: '/',
+ method: 'POST',
+ headers: {
+ 'content-length': 100
+ },
+ body
+ }, (err) => {
+ checkError(err)
+ })
+ }
+
+ {
+ const body = wrapWithAsyncIterable(['asd'], true)
+
+ client.request({
+ path: '/',
+ method: 'POST',
+ body
+ }, (err) => {
+ checkError(err)
+ })
+ }
+
+ {
+ const body = wrapWithAsyncIterable(['asd'], true)
+
+ client.request({
+ path: '/',
+ method: 'POST',
+ headers: {
+ 'content-length': 100
+ },
+ body
+ }, (err) => {
+ checkError(err)
+ })
+ }
+ })
+})
+
+test('client destroy cleanup', (t) => {
+ t.plan(3)
+
+ const _err = new Error('kaboom')
+ let client
+ const server = createServer()
+ server.once('request', (req, res) => {
+ req.once('data', () => {
+ client.destroy(_err, (err) => {
+ t.error(err)
+ })
+ })
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ const body = new Readable({ read () {} })
+ body.push('asd')
+ body.on('error', (err) => {
+ t.equal(err, _err)
+ })
+
+ client.request({
+ path: '/',
+ method: 'POST',
+ body
+ }, (err, data) => {
+ t.equal(err, _err)
+ })
+ })
+})
+
+test('throwing async-iterator causes error', (t) => {
+ t.plan(1)
+
+ const server = createServer((req, res) => {
+ res.end(Buffer.alloc(4 + 1, 'a'))
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ client.request({
+ method: 'POST',
+ path: '/',
+ body: (async function * () {
+ yield 'hello'
+ throw new IteratorError('bad iterator')
+ })()
+ }, (err) => {
+ t.type(err, IteratorError)
+ })
+ })
+})
+
+test('client async-iterator destroy cleanup', (t) => {
+ t.plan(2)
+
+ const _err = new Error('kaboom')
+ let client
+ const server = createServer()
+ server.once('request', (req, res) => {
+ req.once('data', () => {
+ client.destroy(_err, (err) => {
+ t.error(err)
+ })
+ })
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ const body = wrapWithAsyncIterable(['asd'], true)
+
+ client.request({
+ path: '/',
+ method: 'POST',
+ body
+ }, (err, data) => {
+ t.equal(err, _err)
+ })
+ })
+})
+
+test('GET errors body', (t) => {
+ t.plan(2)
+
+ const server = createServer()
+ server.once('request', (req, res) => {
+ res.write('asd')
+ setTimeout(() => {
+ res.destroy()
+ }, 19)
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ client.request({ path: '/', method: 'GET' }, (err, { statusCode, headers, body }) => {
+ t.error(err)
+ body.resume()
+ body.on('error', err => (
+ t.ok(err)
+ ))
+ })
+ })
+})
+
+test('validate request body', (t) => {
+ t.plan(6)
+
+ const server = createServer((req, res) => {
+ res.end('asd')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.close.bind(client))
+
+ client.request({
+ path: '/',
+ method: 'POST',
+ body: /asdasd/
+ }, (err, data) => {
+ t.type(err, errors.InvalidArgumentError)
+ })
+
+ client.request({
+ path: '/',
+ method: 'POST',
+ body: 0
+ }, (err, data) => {
+ t.type(err, errors.InvalidArgumentError)
+ })
+
+ client.request({
+ path: '/',
+ method: 'POST',
+ body: false
+ }, (err, data) => {
+ t.type(err, errors.InvalidArgumentError)
+ })
+
+ client.request({
+ path: '/',
+ method: 'POST',
+ body: ''
+ }, (err, data) => {
+ t.error(err)
+ data.body.resume()
+ })
+
+ client.request({
+ path: '/',
+ method: 'POST',
+ body: new Uint8Array()
+ }, (err, data) => {
+ t.error(err)
+ data.body.resume()
+ })
+
+ client.request({
+ path: '/',
+ method: 'POST',
+ body: Buffer.alloc(10)
+ }, (err, data) => {
+ t.error(err)
+ data.body.resume()
+ })
+ })
+})
+
+test('parser error', (t) => {
+ t.plan(2)
+
+ const server = net.createServer()
+ server.once('connection', (socket) => {
+ socket.write('asd\n\r213123')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ client.request({ path: '/', method: 'GET' }, (err) => {
+ t.ok(err)
+ client.close((err) => {
+ t.error(err)
+ })
+ })
+ })
+})
+
+function socketFailWrite (type) {
+ test(`socket fail while writing ${type} request body`, (t) => {
+ t.plan(2)
+
+ const server = createServer()
+ server.once('request', (req, res) => {
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ const preBody = new Readable({ read () {} })
+ preBody.push('asd')
+ const body = maybeWrapStream(preBody, type)
+ client.on('connect', () => {
+ process.nextTick(() => {
+ client[kSocket].destroy('kaboom')
+ })
+ })
+
+ client.request({
+ path: '/',
+ method: 'POST',
+ body
+ }, (err) => {
+ t.ok(err)
+ })
+ client.close((err) => {
+ t.error(err)
+ })
+ })
+ })
+}
+socketFailWrite(consts.STREAM)
+socketFailWrite(consts.ASYNC_ITERATOR)
+
+function socketFailEndWrite (type) {
+ test(`socket fail while ending ${type} request body`, (t) => {
+ t.plan(3)
+
+ const server = createServer()
+ server.once('request', (req, res) => {
+ res.end()
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, {
+ pipelining: 2
+ })
+ t.teardown(client.destroy.bind(client))
+
+ const _err = new Error('kaboom')
+ client.on('connect', () => {
+ process.nextTick(() => {
+ client[kSocket].destroy(_err)
+ })
+ })
+ const preBody = new Readable({ read () {} })
+ preBody.push(null)
+ const body = maybeWrapStream(preBody, type)
+
+ client.request({
+ path: '/',
+ method: 'POST',
+ body
+ }, (err) => {
+ t.equal(err, _err)
+ })
+ client.close((err) => {
+ t.error(err)
+ client.close((err) => {
+ t.type(err, errors.ClientDestroyedError)
+ })
+ })
+ })
+ })
+}
+
+socketFailEndWrite(consts.STREAM)
+socketFailEndWrite(consts.ASYNC_ITERATOR)
+
+test('queued request should not fail on socket destroy', (t) => {
+ t.plan(4)
+
+ const server = createServer()
+ server.on('request', (req, res) => {
+ res.end()
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, {
+ pipelining: 1
+ })
+ t.teardown(client.destroy.bind(client))
+
+ client.request({
+ path: '/',
+ method: 'GET'
+ }, (err, data) => {
+ t.error(err)
+ data.body.resume().on('error', () => {
+ t.pass()
+ })
+ client[kSocket].destroy()
+ client.request({
+ path: '/',
+ method: 'GET'
+ }, (err, data) => {
+ t.error(err)
+ data.body.resume().on('end', () => {
+ t.pass()
+ })
+ })
+ })
+ })
+})
+
+test('queued request should fail on client destroy', (t) => {
+ t.plan(6)
+
+ const server = createServer()
+ server.on('request', (req, res) => {
+ res.end()
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, {
+ pipelining: 1
+ })
+ t.teardown(client.destroy.bind(client))
+
+ let requestErrored = false
+ client.request({
+ path: '/',
+ method: 'GET'
+ }, (err, data) => {
+ t.error(err)
+ data.body.resume()
+ .on('error', () => {
+ t.pass()
+ })
+ client.destroy((err) => {
+ t.error(err)
+ t.equal(requestErrored, true)
+ })
+ })
+ client.request({
+ path: '/',
+ method: 'GET',
+ opaque: 'asd'
+ }, (err, data) => {
+ requestErrored = true
+ t.ok(err)
+ t.equal(data.opaque, 'asd')
+ })
+ })
+})
+
+test('retry idempotent inflight', (t) => {
+ t.plan(3)
+
+ const server = createServer()
+ server.on('request', (req, res) => {
+ res.end()
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, {
+ pipelining: 3
+ })
+ t.teardown(client.close.bind(client))
+
+ client.request({
+ path: '/',
+ method: 'POST',
+ body: new Readable({
+ read () {
+ this.destroy(new Error('kaboom'))
+ }
+ })
+ }, (err) => {
+ t.ok(err)
+ })
+ client.request({
+ path: '/',
+ method: 'GET'
+ }, (err, data) => {
+ t.error(err)
+ data.body.resume()
+ })
+ client.request({
+ path: '/',
+ method: 'GET'
+ }, (err, data) => {
+ t.error(err)
+ data.body.resume()
+ })
+ })
+})
+
+test('invalid opts', (t) => {
+ t.plan(2)
+
+ const client = new Client('http://localhost:5000')
+ client.request(null, (err) => {
+ t.type(err, errors.InvalidArgumentError)
+ })
+ client.pipeline(null).on('error', (err) => {
+ t.type(err, errors.InvalidArgumentError)
+ })
+})
+
+test('default port for http and https', (t) => {
+ t.plan(4)
+
+ try {
+ new Client(new URL('http://localhost:80')) // eslint-disable-line
+ t.pass('Should not throw')
+ } catch (err) {
+ t.fail(err)
+ }
+
+ try {
+ new Client(new URL('http://localhost')) // eslint-disable-line
+ t.pass('Should not throw')
+ } catch (err) {
+ t.fail(err)
+ }
+
+ try {
+ new Client(new URL('https://localhost:443')) // eslint-disable-line
+ t.pass('Should not throw')
+ } catch (err) {
+ t.fail(err)
+ }
+
+ try {
+ new Client(new URL('https://localhost')) // eslint-disable-line
+ t.pass('Should not throw')
+ } catch (err) {
+ t.fail(err)
+ }
+})
+
+test('CONNECT throws in next tick', (t) => {
+ t.plan(3)
+
+ const server = createServer()
+ server.on('request', (req, res) => {
+ res.end()
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ client.request({
+ path: '/',
+ method: 'GET'
+ }, (err, data) => {
+ t.error(err)
+ data.body
+ .on('end', () => {
+ let ticked = false
+ client.request({
+ path: '/',
+ method: 'CONNECT'
+ }, (err) => {
+ t.ok(err)
+ t.strictSame(ticked, true)
+ })
+ ticked = true
+ })
+ .resume()
+ })
+ })
+})
+
+test('invalid signal', (t) => {
+ t.plan(8)
+
+ const client = new Client('http://localhost:3333')
+ t.teardown(client.destroy.bind(client))
+
+ let ticked = false
+ client.request({ path: '/', method: 'GET', signal: {}, opaque: 'asd' }, (err, { opaque }) => {
+ t.equal(ticked, true)
+ t.equal(opaque, 'asd')
+ t.type(err, errors.InvalidArgumentError)
+ })
+ client.pipeline({ path: '/', method: 'GET', signal: {} }, () => {})
+ .on('error', (err) => {
+ t.equal(ticked, true)
+ t.type(err, errors.InvalidArgumentError)
+ })
+ client.stream({ path: '/', method: 'GET', signal: {}, opaque: 'asd' }, () => {}, (err, { opaque }) => {
+ t.equal(ticked, true)
+ t.equal(opaque, 'asd')
+ t.type(err, errors.InvalidArgumentError)
+ })
+ ticked = true
+})
+
+test('invalid body chunk does not crash', (t) => {
+ t.plan(1)
+
+ const server = createServer()
+ server.on('request', (req, res) => {
+ res.end()
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ client.request({
+ path: '/',
+ body: new Readable({
+ objectMode: true,
+ read () {
+ this.push({})
+ }
+ }),
+ method: 'GET'
+ }, (err) => {
+ t.equal(err.code, 'ERR_INVALID_ARG_TYPE')
+ })
+ })
+})
+
+test('socket errors', t => {
+ t.plan(2)
+ const client = new Client('http://localhost:5554')
+ t.teardown(client.destroy.bind(client))
+
+ client.request({ path: '/', method: 'GET' }, (err, data) => {
+ t.ok(err)
+ // TODO: Why UND_ERR_SOCKET?
+ t.ok(err.code === 'ECONNREFUSED' || err.code === 'UND_ERR_SOCKET', err.code)
+ t.end()
+ })
+})
+
+test('headers overflow', t => {
+ t.plan(2)
+ const server = createServer()
+ server.on('request', (req, res) => {
+ res.writeHead(200, {
+ 'x-test-1': '1',
+ 'x-test-2': '2'
+ })
+ res.end()
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, {
+ maxHeaderSize: 10
+ })
+ t.teardown(client.destroy.bind(client))
+
+ client.request({ path: '/', method: 'GET' }, (err, data) => {
+ t.ok(err)
+ t.equal(err.code, 'UND_ERR_HEADERS_OVERFLOW')
+ t.end()
+ })
+ })
+})
+
+test('SocketError should expose socket details (net)', (t) => {
+ t.plan(8)
+
+ const server = createServer()
+
+ server.once('request', (req, res) => {
+ res.destroy()
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ client.request({ path: '/', method: 'GET' }, (err, data) => {
+ t.ok(err instanceof errors.SocketError)
+ if (err.socket.remoteFamily === 'IPv4') {
+ t.equal(err.socket.remoteFamily, 'IPv4')
+ t.equal(err.socket.localAddress, '127.0.0.1')
+ t.equal(err.socket.remoteAddress, '127.0.0.1')
+ } else {
+ t.equal(err.socket.remoteFamily, 'IPv6')
+ t.equal(err.socket.localAddress, '::1')
+ t.equal(err.socket.remoteAddress, '::1')
+ }
+ t.type(err.socket.localPort, 'number')
+ t.type(err.socket.remotePort, 'number')
+ t.type(err.socket.bytesWritten, 'number')
+ t.type(err.socket.bytesRead, 'number')
+ })
+ })
+})
+
+test('SocketError should expose socket details (tls)', (t) => {
+ t.plan(8)
+
+ const server = https.createServer(pem)
+
+ server.once('request', (req, res) => {
+ res.destroy()
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`https://localhost:${server.address().port}`, {
+ tls: {
+ rejectUnauthorized: false
+ }
+ })
+ t.teardown(client.destroy.bind(client))
+
+ client.request({ path: '/', method: 'GET' }, (err, data) => {
+ t.ok(err instanceof errors.SocketError)
+ if (err.socket.remoteFamily === 'IPv4') {
+ t.equal(err.socket.remoteFamily, 'IPv4')
+ t.equal(err.socket.localAddress, '127.0.0.1')
+ t.equal(err.socket.remoteAddress, '127.0.0.1')
+ } else {
+ t.equal(err.socket.remoteFamily, 'IPv6')
+ t.equal(err.socket.localAddress, '::1')
+ t.equal(err.socket.remoteAddress, '::1')
+ }
+ t.type(err.socket.localPort, 'number')
+ t.type(err.socket.remotePort, 'number')
+ t.type(err.socket.bytesWritten, 'number')
+ t.type(err.socket.bytesRead, 'number')
+ })
+ })
+})
diff --git a/test/client-head-reset-override.js b/test/client-head-reset-override.js
new file mode 100644
index 0000000..a7d79e2
--- /dev/null
+++ b/test/client-head-reset-override.js
@@ -0,0 +1,62 @@
+'use strict'
+
+const { createServer } = require('http')
+const { test } = require('tap')
+const { Client } = require('..')
+
+test('override HEAD reset', (t) => {
+ const expected = 'testing123'
+ const server = createServer((req, res) => {
+ if (req.method === 'GET') {
+ res.write(expected)
+ }
+ res.end()
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.close.bind(client))
+
+ let done
+ client.on('disconnect', () => {
+ if (!done) {
+ t.fail()
+ }
+ })
+
+ client.request({
+ path: '/',
+ method: 'HEAD',
+ reset: false
+ }, (err, res) => {
+ t.error(err)
+ res.body.resume()
+ })
+
+ client.request({
+ path: '/',
+ method: 'HEAD',
+ reset: false
+ }, (err, res) => {
+ t.error(err)
+ res.body.resume()
+ })
+
+ client.request({
+ path: '/',
+ method: 'GET',
+ reset: false
+ }, (err, res) => {
+ t.error(err)
+ let str = ''
+ res.body.on('data', (data) => {
+ str += data
+ }).on('end', () => {
+ t.same(str, expected)
+ done = true
+ t.end()
+ })
+ })
+ })
+})
diff --git a/test/client-idempotent-body.js b/test/client-idempotent-body.js
new file mode 100644
index 0000000..99e5371
--- /dev/null
+++ b/test/client-idempotent-body.js
@@ -0,0 +1,43 @@
+'use strict'
+
+const { test } = require('tap')
+const { Client } = require('..')
+const { createServer } = require('http')
+
+test('idempotent retry', (t) => {
+ t.plan(11)
+
+ const body = 'world'
+ const server = createServer((req, res) => {
+ let buf = ''
+ req.on('data', data => {
+ buf += data
+ }).on('end', () => {
+ t.strictSame(buf, body)
+ res.end()
+ })
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, {
+ pipelining: 2
+ })
+ t.teardown(client.close.bind(client))
+
+ const _err = new Error()
+
+ for (let n = 0; n < 4; ++n) {
+ client.stream({
+ path: '/',
+ method: 'PUT',
+ idempotent: true,
+ body
+ }, () => {
+ throw _err
+ }, (err) => {
+ t.equal(err, _err)
+ })
+ }
+ })
+})
diff --git a/test/client-keep-alive.js b/test/client-keep-alive.js
new file mode 100644
index 0000000..393807b
--- /dev/null
+++ b/test/client-keep-alive.js
@@ -0,0 +1,359 @@
+'use strict'
+
+const { test } = require('tap')
+const { Client } = require('..')
+const timers = require('../lib/timers')
+const { kConnect } = require('../lib/core/symbols')
+const { createServer } = require('net')
+const http = require('http')
+const FakeTimers = require('@sinonjs/fake-timers')
+
+test('keep-alive header', (t) => {
+ t.plan(2)
+
+ const server = createServer((socket) => {
+ socket.write('HTTP/1.1 200 OK\r\n')
+ socket.write('Content-Length: 0\r\n')
+ socket.write('Keep-Alive: timeout=2s\r\n')
+ socket.write('Connection: keep-alive\r\n')
+ socket.write('\r\n\r\n')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ client.request({
+ path: '/',
+ method: 'GET'
+ }, (err, { body }) => {
+ t.error(err)
+ body.on('end', () => {
+ const timeout = setTimeout(() => {
+ t.fail()
+ }, 4e3)
+ client.on('disconnect', () => {
+ t.pass()
+ clearTimeout(timeout)
+ })
+ }).resume()
+ })
+ })
+})
+
+test('keep-alive header 0', (t) => {
+ t.plan(2)
+
+ const clock = FakeTimers.install()
+ t.teardown(clock.uninstall.bind(clock))
+
+ const orgTimers = { ...timers }
+ Object.assign(timers, { setTimeout, clearTimeout })
+ t.teardown(() => {
+ Object.assign(timers, orgTimers)
+ })
+
+ const server = createServer((socket) => {
+ socket.write('HTTP/1.1 200 OK\r\n')
+ socket.write('Content-Length: 0\r\n')
+ socket.write('Keep-Alive: timeout=1s\r\n')
+ socket.write('Connection: keep-alive\r\n')
+ socket.write('\r\n\r\n')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, {
+ keepAliveTimeoutThreshold: 500
+ })
+ t.teardown(client.destroy.bind(client))
+
+ client.request({
+ path: '/',
+ method: 'GET'
+ }, (err, { body }) => {
+ t.error(err)
+ body.on('end', () => {
+ client.on('disconnect', () => {
+ t.pass()
+ })
+ clock.tick(600)
+ }).resume()
+ })
+ })
+})
+
+test('keep-alive header 1', (t) => {
+ t.plan(2)
+
+ const server = createServer((socket) => {
+ socket.write('HTTP/1.1 200 OK\r\n')
+ socket.write('Content-Length: 0\r\n')
+ socket.write('Keep-Alive: timeout=1s\r\n')
+ socket.write('Connection: keep-alive\r\n')
+ socket.write('\r\n\r\n')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ client.request({
+ path: '/',
+ method: 'GET'
+ }, (err, { body }) => {
+ t.error(err)
+ body.on('end', () => {
+ const timeout = setTimeout(() => {
+ t.fail()
+ }, 0)
+ client.on('disconnect', () => {
+ t.pass()
+ clearTimeout(timeout)
+ })
+ }).resume()
+ })
+ })
+})
+
+test('keep-alive header no postfix', (t) => {
+ t.plan(2)
+
+ const server = createServer((socket) => {
+ socket.write('HTTP/1.1 200 OK\r\n')
+ socket.write('Content-Length: 0\r\n')
+ socket.write('Keep-Alive: timeout=2\r\n')
+ socket.write('Connection: keep-alive\r\n')
+ socket.write('\r\n\r\n')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ client.request({
+ path: '/',
+ method: 'GET'
+ }, (err, { body }) => {
+ t.error(err)
+ body.on('end', () => {
+ const timeout = setTimeout(() => {
+ t.fail()
+ }, 4e3)
+ client.on('disconnect', () => {
+ t.pass()
+ clearTimeout(timeout)
+ })
+ }).resume()
+ })
+ })
+})
+
+test('keep-alive not timeout', (t) => {
+ t.plan(2)
+
+ const server = createServer((socket) => {
+ socket.write('HTTP/1.1 200 OK\r\n')
+ socket.write('Content-Length: 0\r\n')
+ socket.write('Keep-Alive: timeoutasdasd=1s\r\n')
+ socket.write('Connection: keep-alive\r\n')
+ socket.write('\r\n\r\n')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, {
+ keepAliveTimeout: 1e3
+ })
+ t.teardown(client.destroy.bind(client))
+
+ client.request({
+ path: '/',
+ method: 'GET'
+ }, (err, { body }) => {
+ t.error(err)
+ body.on('end', () => {
+ const timeout = setTimeout(() => {
+ t.fail()
+ }, 3e3)
+ client.on('disconnect', () => {
+ t.pass()
+ clearTimeout(timeout)
+ })
+ }).resume()
+ })
+ })
+})
+
+test('keep-alive threshold', (t) => {
+ t.plan(2)
+
+ const server = createServer((socket) => {
+ socket.write('HTTP/1.1 200 OK\r\n')
+ socket.write('Content-Length: 0\r\n')
+ socket.write('Keep-Alive: timeout=30s\r\n')
+ socket.write('Connection: keep-alive\r\n')
+ socket.write('\r\n\r\n')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, {
+ keepAliveTimeout: 30e3,
+ keepAliveTimeoutThreshold: 29e3
+ })
+ t.teardown(client.destroy.bind(client))
+
+ client.request({
+ path: '/',
+ method: 'GET'
+ }, (err, { body }) => {
+ t.error(err)
+ body.on('end', () => {
+ const timeout = setTimeout(() => {
+ t.fail()
+ }, 3e3)
+ client.on('disconnect', () => {
+ t.pass()
+ clearTimeout(timeout)
+ })
+ }).resume()
+ })
+ })
+})
+
+test('keep-alive max keepalive', (t) => {
+ t.plan(2)
+
+ const server = createServer((socket) => {
+ socket.write('HTTP/1.1 200 OK\r\n')
+ socket.write('Content-Length: 0\r\n')
+ socket.write('Keep-Alive: timeout=30s\r\n')
+ socket.write('Connection: keep-alive\r\n')
+ socket.write('\r\n\r\n')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, {
+ keepAliveTimeout: 30e3,
+ keepAliveMaxTimeout: 1e3
+ })
+ t.teardown(client.destroy.bind(client))
+
+ client.request({
+ path: '/',
+ method: 'GET'
+ }, (err, { body }) => {
+ t.error(err)
+ body.on('end', () => {
+ const timeout = setTimeout(() => {
+ t.fail()
+ }, 3e3)
+ client.on('disconnect', () => {
+ t.pass()
+ clearTimeout(timeout)
+ })
+ }).resume()
+ })
+ })
+})
+
+test('connection close', (t) => {
+ t.plan(4)
+
+ let close = false
+ const server = createServer((socket) => {
+ if (close) {
+ return
+ }
+ close = true
+ socket.write('HTTP/1.1 200 OK\r\n')
+ socket.write('Content-Length: 0\r\n')
+ socket.write('Connection: close\r\n')
+ socket.write('\r\n\r\n')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, {
+ pipelining: 2
+ })
+ t.teardown(client.destroy.bind(client))
+
+ client[kConnect](() => {
+ client.request({
+ path: '/',
+ method: 'GET'
+ }, (err, { body }) => {
+ t.error(err)
+ body.on('end', () => {
+ const timeout = setTimeout(() => {
+ t.fail()
+ }, 3e3)
+ client.once('disconnect', () => {
+ close = false
+ t.pass()
+ clearTimeout(timeout)
+ })
+ }).resume()
+ })
+
+ client.request({
+ path: '/',
+ method: 'GET'
+ }, (err, { body }) => {
+ t.error(err)
+ body.on('end', () => {
+ const timeout = setTimeout(() => {
+ t.fail()
+ }, 3e3)
+ client.once('disconnect', () => {
+ t.pass()
+ clearTimeout(timeout)
+ })
+ }).resume()
+ })
+ })
+ })
+})
+
+test('Disable keep alive', (t) => {
+ t.plan(7)
+
+ const ports = []
+ const server = http.createServer((req, res) => {
+ t.notOk(ports.includes(req.socket.remotePort))
+ ports.push(req.socket.remotePort)
+ t.match(req.headers, { connection: 'close' })
+ res.writeHead(200, { connection: 'close' })
+ res.end()
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, { pipelining: 0 })
+ t.teardown(client.destroy.bind(client))
+
+ client.request({
+ path: '/',
+ method: 'GET'
+ }, (err, { body }) => {
+ t.error(err)
+ body.on('end', () => {
+ client.request({
+ path: '/',
+ method: 'GET'
+ }, (err, { body }) => {
+ t.error(err)
+ body.on('end', () => {
+ t.pass()
+ }).resume()
+ })
+ }).resume()
+ })
+ })
+})
diff --git a/test/client-node-max-header-size.js b/test/client-node-max-header-size.js
new file mode 100644
index 0000000..b537490
--- /dev/null
+++ b/test/client-node-max-header-size.js
@@ -0,0 +1,23 @@
+'use strict'
+
+const { execSync } = require('node:child_process')
+const { test } = require('tap')
+
+const command = 'node -e "require(\'.\').request(\'https://httpbin.org/get\')"'
+
+test("respect Node.js' --max-http-header-size", async (t) => {
+ t.throws(
+ // TODO: Drop the `--unhandled-rejections=throw` once we drop Node.js 14
+ () => execSync(`${command} --max-http-header-size=1 --unhandled-rejections=throw`),
+ /UND_ERR_HEADERS_OVERFLOW/,
+ 'max-http-header-size=1 should throw'
+ )
+
+ t.doesNotThrow(
+ () => execSync(command),
+ /UND_ERR_HEADERS_OVERFLOW/,
+ 'default max-http-header-size should not throw'
+ )
+
+ t.end()
+})
diff --git a/test/client-pipeline.js b/test/client-pipeline.js
new file mode 100644
index 0000000..9b677a0
--- /dev/null
+++ b/test/client-pipeline.js
@@ -0,0 +1,1042 @@
+'use strict'
+
+const { test } = require('tap')
+const { Client, errors } = require('..')
+const EE = require('events')
+const { createServer } = require('http')
+const {
+ pipeline,
+ Readable,
+ Transform,
+ Writable,
+ PassThrough
+} = require('stream')
+const { nodeMajor } = require('../lib/core/util')
+
+test('pipeline get', (t) => {
+ t.plan(17)
+
+ const server = createServer((req, res) => {
+ t.equal('/', req.url)
+ t.equal('GET', req.method)
+ t.equal(`localhost:${server.address().port}`, req.headers.host)
+ t.equal(undefined, req.headers['content-length'])
+ res.setHeader('Content-Type', 'text/plain')
+ res.end('hello')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.close.bind(client))
+
+ {
+ const bufs = []
+ const signal = new EE()
+ client.pipeline({ signal, path: '/', method: 'GET' }, ({ statusCode, headers, body }) => {
+ t.equal(statusCode, 200)
+ t.equal(headers['content-type'], 'text/plain')
+ t.equal(signal.listenerCount('abort'), 1)
+ return body
+ })
+ .end()
+ .on('data', (buf) => {
+ bufs.push(buf)
+ })
+ .on('end', () => {
+ t.equal('hello', Buffer.concat(bufs).toString('utf8'))
+ })
+ .on('close', () => {
+ t.equal(signal.listenerCount('abort'), 0)
+ })
+ t.equal(signal.listenerCount('abort'), 1)
+ }
+
+ {
+ const bufs = []
+ client.pipeline({ path: '/', method: 'GET' }, ({ statusCode, headers, body }) => {
+ t.equal(statusCode, 200)
+ t.equal(headers['content-type'], 'text/plain')
+ return body
+ })
+ .end()
+ .on('data', (buf) => {
+ bufs.push(buf)
+ })
+ .on('end', () => {
+ t.equal('hello', Buffer.concat(bufs).toString('utf8'))
+ })
+ }
+ })
+})
+
+test('pipeline echo', (t) => {
+ t.plan(2)
+
+ const server = createServer((req, res) => {
+ req.pipe(res)
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.close.bind(client))
+
+ let res = ''
+ const buf1 = Buffer.alloc(1e3).toString()
+ const buf2 = Buffer.alloc(1e6).toString()
+ pipeline(
+ new Readable({
+ read () {
+ this.push(buf1)
+ this.push(buf2)
+ this.push(null)
+ }
+ }),
+ client.pipeline({
+ path: '/',
+ method: 'PUT'
+ }, ({ body }) => {
+ return pipeline(body, new PassThrough(), () => {})
+ }),
+ new Writable({
+ write (chunk, encoding, callback) {
+ res += chunk.toString()
+ callback()
+ },
+ final (callback) {
+ t.equal(res, buf1 + buf2)
+ callback()
+ }
+ }),
+ (err) => {
+ t.error(err)
+ }
+ )
+ })
+})
+
+test('pipeline ignore request body', (t) => {
+ t.plan(2)
+
+ let done
+ const server = createServer((req, res) => {
+ res.write('asd')
+ res.end()
+ done()
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.close.bind(client))
+
+ let res = ''
+ const buf1 = Buffer.alloc(1e3).toString()
+ const buf2 = Buffer.alloc(1e6).toString()
+ pipeline(
+ new Readable({
+ read () {
+ this.push(buf1)
+ this.push(buf2)
+ done = () => this.push(null)
+ }
+ }),
+ client.pipeline({
+ path: '/',
+ method: 'PUT'
+ }, ({ body }) => {
+ return pipeline(body, new PassThrough(), () => {})
+ }),
+ new Writable({
+ write (chunk, encoding, callback) {
+ res += chunk.toString()
+ callback()
+ },
+ final (callback) {
+ t.equal(res, 'asd')
+ callback()
+ }
+ }),
+ (err) => {
+ t.error(err)
+ }
+ )
+ })
+})
+
+test('pipeline invalid handler', (t) => {
+ t.plan(1)
+
+ const client = new Client('http://localhost:5000')
+ client.pipeline({}, null).on('error', (err) => {
+ t.ok(/handler/.test(err))
+ })
+})
+
+test('pipeline invalid handler return after destroy should not error', (t) => {
+ t.plan(3)
+
+ const server = createServer((req, res) => {
+ req.pipe(res)
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, {
+ pipelining: 3
+ })
+ t.teardown(client.destroy.bind(client))
+
+ const dup = client.pipeline({
+ path: '/',
+ method: 'GET'
+ }, ({ body }) => {
+ body.on('error', (err) => {
+ t.equal(err.message, 'asd')
+ })
+ dup.destroy(new Error('asd'))
+ return {}
+ })
+ .on('error', (err) => {
+ t.equal(err.message, 'asd')
+ })
+ .on('close', () => {
+ t.pass()
+ })
+ .end()
+ })
+})
+
+test('pipeline error body', (t) => {
+ t.plan(2)
+
+ const server = createServer((req, res) => {
+ req.pipe(res)
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.close.bind(client))
+
+ const buf = Buffer.alloc(1e6).toString()
+ pipeline(
+ new Readable({
+ read () {
+ this.push(buf)
+ }
+ }),
+ client.pipeline({
+ path: '/',
+ method: 'PUT'
+ }, ({ body }) => {
+ const pt = new PassThrough()
+ process.nextTick(() => {
+ pt.destroy(new Error('asd'))
+ })
+ body.on('error', (err) => {
+ t.ok(err)
+ })
+ return pipeline(body, pt, () => {})
+ }),
+ new PassThrough(),
+ (err) => {
+ t.ok(err)
+ }
+ )
+ })
+})
+
+test('pipeline destroy body', (t) => {
+ t.plan(2)
+
+ const server = createServer((req, res) => {
+ req.pipe(res)
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.close.bind(client))
+
+ const buf = Buffer.alloc(1e6).toString()
+ pipeline(
+ new Readable({
+ read () {
+ this.push(buf)
+ }
+ }),
+ client.pipeline({
+ path: '/',
+ method: 'PUT'
+ }, ({ body }) => {
+ const pt = new PassThrough()
+ process.nextTick(() => {
+ pt.destroy()
+ })
+ body.on('error', (err) => {
+ t.ok(err)
+ })
+ return pipeline(body, pt, () => {})
+ }),
+ new PassThrough(),
+ (err) => {
+ t.ok(err)
+ }
+ )
+ })
+})
+
+test('pipeline backpressure', (t) => {
+ t.plan(1)
+
+ const server = createServer((req, res) => {
+ req.pipe(res)
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ const buf = Buffer.alloc(1e6).toString()
+ const duplex = client.pipeline({
+ path: '/',
+ method: 'PUT'
+ }, ({ body }) => {
+ const pt = new PassThrough()
+ return pipeline(body, pt, () => {})
+ })
+
+ duplex.end(buf)
+ duplex.on('data', () => {
+ duplex.pause()
+ setImmediate(() => {
+ duplex.resume()
+ })
+ }).on('end', () => {
+ t.pass()
+ })
+ })
+})
+
+test('pipeline invalid handler return', (t) => {
+ t.plan(2)
+
+ const server = createServer((req, res) => {
+ req.pipe(res)
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ client.pipeline({
+ path: '/',
+ method: 'GET'
+ }, ({ body }) => {
+ // TODO: Should body cause unhandled exception?
+ body.on('error', () => {})
+ })
+ .on('error', (err) => {
+ t.type(err, errors.InvalidReturnValueError)
+ })
+ .end()
+
+ client.pipeline({
+ path: '/',
+ method: 'GET'
+ }, ({ body }) => {
+ // TODO: Should body cause unhandled exception?
+ body.on('error', () => {})
+ return {}
+ })
+ .on('error', (err) => {
+ t.type(err, errors.InvalidReturnValueError)
+ })
+ .end()
+ })
+})
+
+test('pipeline throw handler', (t) => {
+ t.plan(1)
+
+ const server = createServer((req, res) => {
+ req.pipe(res)
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ client.pipeline({
+ path: '/',
+ method: 'GET'
+ }, ({ body }) => {
+ // TODO: Should body cause unhandled exception?
+ body.on('error', () => {})
+ throw new Error('asd')
+ })
+ .on('error', (err) => {
+ t.equal(err.message, 'asd')
+ })
+ .end()
+ })
+})
+
+test('pipeline destroy and throw handler', (t) => {
+ t.plan(2)
+
+ const server = createServer((req, res) => {
+ req.pipe(res)
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ const dup = client.pipeline({
+ path: '/',
+ method: 'GET'
+ }, ({ body }) => {
+ dup.destroy()
+ // TODO: Should body cause unhandled exception?
+ body.on('error', () => {})
+ throw new Error('asd')
+ })
+ .end()
+ .on('error', (err) => {
+ t.type(err, errors.RequestAbortedError)
+ })
+ .on('close', () => {
+ t.pass()
+ })
+ })
+})
+
+test('pipeline abort res', (t) => {
+ t.plan(2)
+
+ let _res
+ const server = createServer((req, res) => {
+ res.write('asd')
+ _res = res
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ client.pipeline({
+ path: '/',
+ method: 'GET'
+ }, ({ body }) => {
+ setImmediate(() => {
+ body.destroy()
+ _res.write('asdasdadasd')
+ const timeout = setTimeout(() => {
+ t.fail()
+ }, 100)
+ client.on('disconnect', () => {
+ clearTimeout(timeout)
+ t.pass()
+ })
+ })
+ return body
+ })
+ .on('error', (err) => {
+ t.type(err, errors.RequestAbortedError)
+ })
+ .end()
+ })
+})
+
+test('pipeline abort server res', (t) => {
+ t.plan(1)
+
+ const server = createServer((req, res) => {
+ res.destroy()
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ client.pipeline({
+ path: '/',
+ method: 'GET'
+ }, () => {
+ t.fail()
+ })
+ .on('error', (err) => {
+ t.type(err, errors.SocketError)
+ })
+ .end()
+ })
+})
+
+test('pipeline abort duplex', (t) => {
+ t.plan(2)
+
+ const server = createServer((req, res) => {
+ res.end()
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ client.request({
+ path: '/',
+ method: 'PUT'
+ }, (err, data) => {
+ t.error(err)
+ data.body.resume()
+
+ client.pipeline({
+ path: '/',
+ method: 'PUT'
+ }, () => {
+ t.fail()
+ }).destroy().on('error', (err) => {
+ t.type(err, errors.RequestAbortedError)
+ })
+ })
+ })
+})
+
+test('pipeline abort piped res', (t) => {
+ t.plan(1)
+
+ const server = createServer((req, res) => {
+ res.write('asd')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ client.pipeline({
+ path: '/',
+ method: 'GET'
+ }, ({ body }) => {
+ const pt = new PassThrough()
+ setImmediate(() => {
+ pt.destroy()
+ })
+ return pipeline(body, pt, () => {})
+ })
+ .on('error', (err) => {
+ // Node < 13 doesn't always detect premature close.
+ if (nodeMajor < 13) {
+ t.ok(err)
+ } else {
+ t.equal(err.code, 'UND_ERR_ABORTED')
+ }
+ })
+ .end()
+ })
+})
+
+test('pipeline abort piped res 2', (t) => {
+ t.plan(2)
+
+ const server = createServer((req, res) => {
+ res.write('asd')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ client.pipeline({
+ path: '/',
+ method: 'GET'
+ }, ({ body }) => {
+ const pt = new PassThrough()
+ body.on('error', (err) => {
+ t.type(err, errors.RequestAbortedError)
+ })
+ setImmediate(() => {
+ pt.destroy()
+ })
+ body.pipe(pt)
+ return pt
+ })
+ .on('error', (err) => {
+ t.type(err, errors.RequestAbortedError)
+ })
+ .end()
+ })
+})
+
+test('pipeline abort piped res 3', (t) => {
+ t.plan(2)
+
+ const server = createServer((req, res) => {
+ res.write('asd')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ client.pipeline({
+ path: '/',
+ method: 'GET'
+ }, ({ body }) => {
+ const pt = new PassThrough()
+ body.on('error', (err) => {
+ t.equal(err.message, 'asd')
+ })
+ setImmediate(() => {
+ pt.destroy(new Error('asd'))
+ })
+ body.pipe(pt)
+ return pt
+ })
+ .on('error', (err) => {
+ t.equal(err.message, 'asd')
+ })
+ .end()
+ })
+})
+
+test('pipeline abort server res after headers', (t) => {
+ t.plan(1)
+
+ let _res
+ const server = createServer((req, res) => {
+ res.write('asd')
+ _res = res
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ client.pipeline({
+ path: '/',
+ method: 'GET'
+ }, (data) => {
+ _res.destroy()
+ return data.body
+ })
+ .on('error', (err) => {
+ t.type(err, errors.SocketError)
+ })
+ .end()
+ })
+})
+
+test('pipeline w/ write abort server res after headers', (t) => {
+ t.plan(1)
+
+ let _res
+ const server = createServer((req, res) => {
+ req.pipe(res)
+ _res = res
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ client.pipeline({
+ path: '/',
+ method: 'PUT'
+ }, (data) => {
+ _res.destroy()
+ return data.body
+ })
+ .on('error', (err) => {
+ t.type(err, errors.SocketError)
+ })
+ .resume()
+ .write('asd')
+ })
+})
+
+test('destroy in push', (t) => {
+ t.plan(3)
+
+ let _res
+ const server = createServer((req, res) => {
+ res.write('asd')
+ _res = res
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.close.bind(client))
+
+ client.pipeline({ path: '/', method: 'GET' }, ({ body }) => {
+ body.once('data', () => {
+ _res.write('asd')
+ body.on('data', (buf) => {
+ body.destroy()
+ _res.end()
+ }).on('error', (err) => {
+ t.ok(err)
+ })
+ })
+ return body
+ }).on('error', (err) => {
+ t.ok(err)
+ }).resume().end()
+
+ client.pipeline({ path: '/', method: 'GET' }, ({ body }) => {
+ let buf = ''
+ body.on('data', (chunk) => {
+ buf = chunk.toString()
+ _res.end()
+ }).on('end', () => {
+ t.equal('asd', buf)
+ })
+ return body
+ }).resume().end()
+ })
+})
+
+test('pipeline args validation', (t) => {
+ t.plan(2)
+
+ const client = new Client('http://localhost:5000')
+
+ const ret = client.pipeline(null, () => {})
+ ret.on('error', (err) => {
+ t.ok(/opts/.test(err.message))
+ t.type(err, errors.InvalidArgumentError)
+ })
+})
+
+test('pipeline factory throw not unhandled', (t) => {
+ t.plan(1)
+
+ const server = createServer((req, res) => {
+ res.write('asd')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ client.pipeline({
+ path: '/',
+ method: 'GET'
+ }, (data) => {
+ throw new Error('asd')
+ })
+ .on('error', (err) => {
+ t.ok(err)
+ })
+ .end()
+ })
+})
+
+test('pipeline destroy before dispatch', (t) => {
+ t.plan(1)
+
+ const server = createServer((req, res) => {
+ res.end('hello')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ client
+ .pipeline({ path: '/', method: 'GET' }, ({ body }) => {
+ return body
+ })
+ .on('error', (err) => {
+ t.ok(err)
+ })
+ .end()
+ .destroy()
+ })
+})
+
+test('pipeline legacy stream', (t) => {
+ t.plan(1)
+
+ const server = createServer((req, res) => {
+ res.write(Buffer.alloc(16e3))
+ setImmediate(() => {
+ res.end(Buffer.alloc(16e3))
+ })
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.close.bind(client))
+
+ client
+ .pipeline({ path: '/', method: 'GET' }, ({ body }) => {
+ const pt = new PassThrough()
+ pt.pause = null
+ return body.pipe(pt)
+ })
+ .resume()
+ .on('end', () => {
+ t.pass()
+ })
+ .end()
+ })
+})
+
+test('pipeline objectMode', (t) => {
+ t.plan(1)
+
+ const server = createServer((req, res) => {
+ res.end(JSON.stringify({ asd: 1 }))
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.close.bind(client))
+
+ client
+ .pipeline({ path: '/', method: 'GET', objectMode: true }, ({ body }) => {
+ return pipeline(body, new Transform({
+ readableObjectMode: true,
+ transform (chunk, encoding, callback) {
+ callback(null, JSON.parse(chunk))
+ }
+ }), () => {})
+ })
+ .on('data', data => {
+ t.strictSame(data, { asd: 1 })
+ })
+ .end()
+ })
+})
+
+test('pipeline invalid opts', (t) => {
+ t.plan(2)
+
+ const server = createServer((req, res) => {
+ res.end(JSON.stringify({ asd: 1 }))
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ client.close((err) => {
+ t.error(err)
+ })
+ client
+ .pipeline({ path: '/', method: 'GET', objectMode: true }, ({ body }) => {
+ t.fail()
+ })
+ .on('error', (err) => {
+ t.ok(err)
+ })
+ })
+})
+
+test('pipeline CONNECT throw', (t) => {
+ t.plan(1)
+
+ const server = createServer((req, res) => {
+ res.end('asd')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ client.pipeline({
+ path: '/',
+ method: 'CONNECT'
+ }, () => {
+ t.fail()
+ }).on('error', (err) => {
+ t.type(err, errors.InvalidArgumentError)
+ })
+ client.on('disconnect', () => {
+ t.fail()
+ })
+ })
+})
+
+test('pipeline body without destroy', (t) => {
+ t.plan(1)
+
+ const server = createServer((req, res) => {
+ res.end('asd')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ client.pipeline({
+ path: '/',
+ method: 'GET'
+ }, ({ body }) => {
+ const pt = new PassThrough({ autoDestroy: false })
+ pt.destroy = null
+ return body.pipe(pt)
+ })
+ .end()
+ .on('end', () => {
+ t.pass()
+ })
+ .resume()
+ })
+})
+
+test('pipeline ignore 1xx', (t) => {
+ t.plan(1)
+
+ const server = createServer((req, res) => {
+ res.writeProcessing()
+ res.end('hello')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.close.bind(client))
+
+ let buf = ''
+ client.pipeline({
+ path: '/',
+ method: 'GET'
+ }, ({ body }) => body)
+ .on('data', (chunk) => {
+ buf += chunk
+ })
+ .on('end', () => {
+ t.equal(buf, 'hello')
+ })
+ .end()
+ })
+})
+test('pipeline ignore 1xx and use onInfo', (t) => {
+ t.plan(3)
+
+ const infos = []
+ const server = createServer((req, res) => {
+ res.writeProcessing()
+ res.end('hello')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.close.bind(client))
+
+ let buf = ''
+ client.pipeline({
+ path: '/',
+ method: 'GET',
+ onInfo: (x) => {
+ infos.push(x)
+ }
+ }, ({ body }) => body)
+ .on('data', (chunk) => {
+ buf += chunk
+ })
+ .on('end', () => {
+ t.equal(buf, 'hello')
+ t.equal(infos.length, 1)
+ t.equal(infos[0].statusCode, 102)
+ })
+ .end()
+ })
+})
+
+test('pipeline backpressure', (t) => {
+ t.plan(1)
+
+ const expected = Buffer.alloc(1e6).toString()
+
+ const server = createServer((req, res) => {
+ res.writeProcessing()
+ res.end(expected)
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.close.bind(client))
+
+ let buf = ''
+ client.pipeline({
+ path: '/',
+ method: 'GET'
+ }, ({ body }) => body)
+ .end()
+ .pipe(new Transform({
+ highWaterMark: 1,
+ transform (chunk, encoding, callback) {
+ setImmediate(() => {
+ callback(null, chunk)
+ })
+ }
+ }))
+ .on('data', chunk => {
+ buf += chunk
+ })
+ .on('end', () => {
+ t.equal(buf, expected)
+ })
+ })
+})
+
+test('pipeline abort after headers', (t) => {
+ t.plan(1)
+
+ const server = createServer((req, res) => {
+ res.writeProcessing()
+ res.write('asd')
+ setImmediate(() => {
+ res.write('asd')
+ })
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ const signal = new EE()
+ client.pipeline({
+ path: '/',
+ method: 'GET',
+ signal
+ }, ({ body }) => {
+ process.nextTick(() => {
+ signal.emit('abort')
+ })
+ return body
+ })
+ .end()
+ .on('error', (err) => {
+ t.type(err, errors.RequestAbortedError)
+ })
+ })
+})
diff --git a/test/client-pipelining.js b/test/client-pipelining.js
new file mode 100644
index 0000000..8cd21fe
--- /dev/null
+++ b/test/client-pipelining.js
@@ -0,0 +1,752 @@
+'use strict'
+
+const { test } = require('tap')
+const { Client } = require('..')
+const { createServer } = require('http')
+const { finished, Readable } = require('stream')
+const { kConnect } = require('../lib/core/symbols')
+const EE = require('events')
+const { kBusy, kRunning, kSize } = require('../lib/core/symbols')
+const { maybeWrapStream, consts } = require('./utils/async-iterators')
+
+test('20 times GET with pipelining 10', (t) => {
+ const num = 20
+ t.plan(3 * num + 1)
+
+ let count = 0
+ let countGreaterThanOne = false
+ const server = createServer((req, res) => {
+ count++
+ setTimeout(function () {
+ countGreaterThanOne = countGreaterThanOne || count > 1
+ res.end(req.url)
+ }, 10)
+ })
+ t.teardown(server.close.bind(server))
+
+ // needed to check for a warning on the maxListeners on the socket
+ function onWarning (warning) {
+ if (!/ExperimentalWarning/.test(warning)) {
+ t.fail()
+ }
+ }
+ process.on('warning', onWarning)
+ t.teardown(() => {
+ process.removeListener('warning', onWarning)
+ })
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, {
+ pipelining: 10
+ })
+ t.teardown(client.close.bind(client))
+
+ for (let i = 0; i < num; i++) {
+ makeRequest(i)
+ }
+
+ function makeRequest (i) {
+ makeRequestAndExpectUrl(client, i, t, () => {
+ count--
+
+ if (i === num - 1) {
+ t.ok(countGreaterThanOne, 'seen more than one parallel request')
+ }
+ })
+ }
+ })
+})
+
+function makeRequestAndExpectUrl (client, i, t, cb) {
+ return client.request({ path: '/' + i, method: 'GET' }, (err, { statusCode, headers, body }) => {
+ cb()
+ t.error(err)
+ t.equal(statusCode, 200)
+ const bufs = []
+ body.on('data', (buf) => {
+ bufs.push(buf)
+ })
+ body.on('end', () => {
+ t.equal('/' + i, Buffer.concat(bufs).toString('utf8'))
+ })
+ })
+}
+
+test('A client should enqueue as much as twice its pipelining factor', (t) => {
+ const num = 10
+ let sent = 0
+ // x * 6 + 1 t.ok + 5 drain
+ t.plan(num * 6 + 1 + 5 + 2)
+
+ let count = 0
+ let countGreaterThanOne = false
+ const server = createServer((req, res) => {
+ count++
+ t.ok(count <= 5)
+ setTimeout(function () {
+ countGreaterThanOne = countGreaterThanOne || count > 1
+ res.end(req.url)
+ }, 10)
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, {
+ pipelining: 2
+ })
+ t.teardown(client.close.bind(client))
+
+ for (; sent < 2;) {
+ t.notOk(client[kSize] > client.pipelining, 'client is not full')
+ makeRequest()
+ t.ok(client[kSize] <= client.pipelining, 'we can send more requests')
+ }
+
+ t.ok(client[kBusy], 'client is busy')
+ t.notOk(client[kSize] > client.pipelining, 'client is full')
+ makeRequest()
+ t.ok(client[kBusy], 'we must stop now')
+ t.ok(client[kBusy], 'client is busy')
+ t.ok(client[kSize] > client.pipelining, 'client is full')
+
+ function makeRequest () {
+ makeRequestAndExpectUrl(client, sent++, t, () => {
+ count--
+ setImmediate(() => {
+ if (client[kSize] === 0) {
+ t.ok(countGreaterThanOne, 'seen more than one parallel request')
+ const start = sent
+ for (; sent < start + 2 && sent < num;) {
+ t.notOk(client[kSize] > client.pipelining, 'client is not full')
+ t.ok(makeRequest())
+ }
+ }
+ })
+ })
+ return client[kSize] <= client.pipelining
+ }
+ })
+})
+
+test('pipeline 1 is 1 active request', (t) => {
+ t.plan(9)
+
+ let res2
+ const server = createServer((req, res) => {
+ res.write('asd')
+ res2 = res
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, {
+ pipelining: 1
+ })
+ t.teardown(client.destroy.bind(client))
+ client.request({
+ path: '/',
+ method: 'GET'
+ }, (err, data) => {
+ t.equal(client[kSize], 1)
+ t.error(err)
+ t.notOk(client.request({
+ path: '/',
+ method: 'GET'
+ }, (err, data) => {
+ t.error(err)
+ finished(data.body, (err) => {
+ t.ok(err)
+ client.close((err) => {
+ t.error(err)
+ })
+ })
+ data.body.destroy()
+ res2.end()
+ }))
+ data.body.resume()
+ res2.end()
+ })
+ t.ok(client[kSize] <= client.pipelining)
+ t.ok(client[kBusy])
+ t.equal(client[kSize], 1)
+ })
+})
+
+test('pipelined chunked POST stream', (t) => {
+ t.plan(4 + 8 + 8)
+
+ let a = 0
+ let b = 0
+
+ const server = createServer((req, res) => {
+ req.on('data', chunk => {
+ // Make sure a and b don't interleave.
+ t.ok(a === 9 || b === 0)
+ res.write(chunk)
+ }).on('end', () => {
+ res.end()
+ })
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, {
+ pipelining: 2
+ })
+ t.teardown(client.close.bind(client))
+
+ client.request({
+ path: '/',
+ method: 'GET'
+ }, (err, { body }) => {
+ body.resume()
+ t.error(err)
+ })
+
+ client.request({
+ path: '/',
+ method: 'POST',
+ body: new Readable({
+ read () {
+ this.push(++a > 8 ? null : 'a')
+ }
+ })
+ }, (err, { body }) => {
+ body.resume()
+ t.error(err)
+ })
+
+ client.request({
+ path: '/',
+ method: 'GET'
+ }, (err, { body }) => {
+ body.resume()
+ t.error(err)
+ })
+
+ client.request({
+ path: '/',
+ method: 'POST',
+ body: new Readable({
+ read () {
+ this.push(++b > 8 ? null : 'b')
+ }
+ })
+ }, (err, { body }) => {
+ body.resume()
+ t.error(err)
+ })
+ })
+})
+
+test('pipelined chunked POST iterator', (t) => {
+ t.plan(4 + 8 + 8)
+
+ let a = 0
+ let b = 0
+
+ const server = createServer((req, res) => {
+ req.on('data', chunk => {
+ // Make sure a and b don't interleave.
+ t.ok(a === 9 || b === 0)
+ res.write(chunk)
+ }).on('end', () => {
+ res.end()
+ })
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, {
+ pipelining: 2
+ })
+ t.teardown(client.close.bind(client))
+
+ client.request({
+ path: '/',
+ method: 'GET'
+ }, (err, { body }) => {
+ body.resume()
+ t.error(err)
+ })
+
+ client.request({
+ path: '/',
+ method: 'POST',
+ body: (async function * () {
+ while (++a <= 8) {
+ yield 'a'
+ }
+ })()
+ }, (err, { body }) => {
+ body.resume()
+ t.error(err)
+ })
+
+ client.request({
+ path: '/',
+ method: 'GET'
+ }, (err, { body }) => {
+ body.resume()
+ t.error(err)
+ })
+
+ client.request({
+ path: '/',
+ method: 'POST',
+ body: (async function * () {
+ while (++b <= 8) {
+ yield 'b'
+ }
+ })()
+ }, (err, { body }) => {
+ body.resume()
+ t.error(err)
+ })
+ })
+})
+
+function errordInflightPost (bodyType) {
+ test(`errored POST body lets inflight complete ${bodyType}`, (t) => {
+ t.plan(6)
+
+ let serverRes
+ const server = createServer()
+ server.on('request', (req, res) => {
+ serverRes = res
+ res.write('asd')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, {
+ pipelining: 2
+ })
+ t.teardown(client.destroy.bind(client))
+
+ client.request({
+ path: '/',
+ method: 'GET'
+ }, (err, data) => {
+ t.error(err)
+ data.body
+ .resume()
+ .once('data', () => {
+ client.request({
+ path: '/',
+ method: 'POST',
+ opaque: 'asd',
+ body: maybeWrapStream(new Readable({
+ read () {
+ this.destroy(new Error('kaboom'))
+ }
+ }).once('error', (err) => {
+ t.ok(err)
+ }).on('error', () => {
+ // Readable emits error twice...
+ }), bodyType)
+ }, (err, data) => {
+ t.ok(err)
+ t.equal(data.opaque, 'asd')
+ })
+ client.close((err) => {
+ t.error(err)
+ })
+ serverRes.end()
+ })
+ .on('end', () => {
+ t.pass()
+ })
+ })
+ })
+ })
+}
+
+errordInflightPost(consts.STREAM)
+errordInflightPost(consts.ASYNC_ITERATOR)
+
+test('pipelining non-idempotent', (t) => {
+ t.plan(4)
+
+ const server = createServer()
+ server.on('request', (req, res) => {
+ setTimeout(() => {
+ res.end('asd')
+ }, 10)
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, {
+ pipelining: 2
+ })
+ t.teardown(client.close.bind(client))
+
+ let ended = false
+ client.request({
+ path: '/',
+ method: 'GET'
+ }, (err, data) => {
+ t.error(err)
+ data.body
+ .resume()
+ .on('end', () => {
+ t.pass()
+ ended = true
+ })
+ })
+
+ client.request({
+ path: '/',
+ method: 'GET',
+ idempotent: false
+ }, (err, data) => {
+ t.error(err)
+ t.equal(ended, true)
+ data.body.resume()
+ })
+ })
+})
+
+function pipeliningNonIdempotentWithBody (bodyType) {
+ test(`pipelining non-idempotent w body ${bodyType}`, (t) => {
+ t.plan(4)
+
+ const server = createServer()
+ server.on('request', (req, res) => {
+ setImmediate(() => {
+ res.end('asd')
+ })
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, {
+ pipelining: 2
+ })
+ t.teardown(client.close.bind(client))
+
+ let ended = false
+ let reading = false
+ client.request({
+ path: '/',
+ method: 'POST',
+ body: maybeWrapStream(new Readable({
+ read () {
+ if (reading) {
+ return
+ }
+ reading = true
+ this.push('asd')
+ setImmediate(() => {
+ this.push(null)
+ ended = true
+ })
+ }
+ }), bodyType)
+ }, (err, data) => {
+ t.error(err)
+ data.body
+ .resume()
+ .on('end', () => {
+ t.pass()
+ })
+ })
+
+ client.request({
+ path: '/',
+ method: 'GET',
+ idempotent: false
+ }, (err, data) => {
+ t.error(err)
+ t.equal(ended, true)
+ data.body.resume()
+ })
+ })
+ })
+}
+
+pipeliningNonIdempotentWithBody(consts.STREAM)
+pipeliningNonIdempotentWithBody(consts.ASYNC_ITERATOR)
+
+function pipeliningHeadBusy (bodyType) {
+ test(`pipelining HEAD busy ${bodyType}`, (t) => {
+ t.plan(7)
+
+ const server = createServer()
+ server.on('request', (req, res) => {
+ res.end('asd')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, {
+ pipelining: 10
+ })
+ t.teardown(client.close.bind(client))
+
+ client[kConnect](() => {
+ let ended = false
+ client.once('disconnect', () => {
+ t.equal(ended, true)
+ })
+
+ {
+ const body = new Readable({
+ read () { }
+ })
+ client.request({
+ path: '/',
+ method: 'GET',
+ body: maybeWrapStream(body, bodyType)
+ }, (err, data) => {
+ t.error(err)
+ data.body
+ .resume()
+ .on('end', () => {
+ t.pass()
+ })
+ })
+ body.push(null)
+ t.equal(client[kBusy], true)
+ }
+
+ {
+ const body = new Readable({
+ read () { }
+ })
+ client.request({
+ path: '/',
+ method: 'HEAD',
+ body: maybeWrapStream(body, bodyType)
+ }, (err, data) => {
+ t.error(err)
+ data.body
+ .resume()
+ .on('end', () => {
+ ended = true
+ t.pass()
+ })
+ })
+ body.push(null)
+ t.equal(client[kBusy], true)
+ }
+ })
+ })
+ })
+}
+
+pipeliningHeadBusy(consts.STREAM)
+pipeliningHeadBusy(consts.ASYNC_ITERATOR)
+
+test('pipelining empty pipeline before reset', (t) => {
+ t.plan(8)
+
+ let c = 0
+ const server = createServer()
+ server.on('request', (req, res) => {
+ if (c++ === 0) {
+ res.end('asd')
+ } else {
+ setTimeout(() => {
+ res.end('asd')
+ }, 100)
+ }
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, {
+ pipelining: 10
+ })
+ t.teardown(client.close.bind(client))
+
+ client[kConnect](() => {
+ let ended = false
+ client.once('disconnect', () => {
+ t.equal(ended, true)
+ })
+
+ client.request({
+ path: '/',
+ method: 'GET'
+ }, (err, data) => {
+ t.error(err)
+ data.body
+ .resume()
+ .on('end', () => {
+ t.pass()
+ })
+ })
+ t.equal(client[kBusy], false)
+
+ client.request({
+ path: '/',
+ method: 'HEAD',
+ body: 'asd'
+ }, (err, data) => {
+ t.error(err)
+ data.body
+ .resume()
+ .on('end', () => {
+ ended = true
+ t.pass()
+ })
+ })
+ t.equal(client[kBusy], true)
+ t.equal(client[kRunning], 2)
+ })
+ })
+})
+
+function pipeliningIdempotentBusy (bodyType) {
+ test(`pipelining idempotent busy ${bodyType}`, (t) => {
+ t.plan(12)
+
+ const server = createServer()
+ server.on('request', (req, res) => {
+ res.end('asd')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, {
+ pipelining: 10
+ })
+ t.teardown(client.close.bind(client))
+
+ {
+ const body = new Readable({
+ read () { }
+ })
+ client.request({
+ path: '/',
+ method: 'GET',
+ body: maybeWrapStream(body, bodyType)
+ }, (err, data) => {
+ t.error(err)
+ data.body
+ .resume()
+ .on('end', () => {
+ t.pass()
+ })
+ })
+ body.push(null)
+ t.equal(client[kBusy], true)
+ }
+
+ client[kConnect](() => {
+ {
+ const body = new Readable({
+ read () { }
+ })
+ client.request({
+ path: '/',
+ method: 'GET',
+ body: maybeWrapStream(body, bodyType)
+ }, (err, data) => {
+ t.error(err)
+ data.body
+ .resume()
+ .on('end', () => {
+ t.pass()
+ })
+ })
+ body.push(null)
+ t.equal(client[kBusy], true)
+ }
+
+ {
+ const signal = new EE()
+ const body = new Readable({
+ read () { }
+ })
+ client.request({
+ path: '/',
+ method: 'GET',
+ body: maybeWrapStream(body, bodyType),
+ signal
+ }, (err, data) => {
+ t.ok(err)
+ })
+ t.equal(client[kBusy], true)
+ signal.emit('abort')
+ t.equal(client[kBusy], true)
+ }
+
+ {
+ const body = new Readable({
+ read () { }
+ })
+ client.request({
+ path: '/',
+ method: 'GET',
+ idempotent: false,
+ body: maybeWrapStream(body, bodyType)
+ }, (err, data) => {
+ t.error(err)
+ data.body
+ .resume()
+ .on('end', () => {
+ t.pass()
+ })
+ })
+ body.push(null)
+ t.equal(client[kBusy], true)
+ }
+ })
+ })
+ })
+}
+
+pipeliningIdempotentBusy(consts.STREAM)
+pipeliningIdempotentBusy(consts.ASYNC_ITERATOR)
+
+test('pipelining blocked', (t) => {
+ t.plan(6)
+
+ const server = createServer()
+
+ let blocking = true
+ let count = 0
+
+ server.on('request', (req, res) => {
+ t.ok(!count || !blocking)
+ count++
+ setImmediate(() => {
+ res.end('asd')
+ })
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, {
+ pipelining: 10
+ })
+ t.teardown(client.close.bind(client))
+ client.request({
+ path: '/',
+ method: 'GET',
+ blocking: true
+ }, (err, data) => {
+ t.error(err)
+ blocking = false
+ data.body
+ .resume()
+ .on('end', () => {
+ t.pass()
+ })
+ })
+ client.request({
+ path: '/',
+ method: 'GET'
+ }, (err, data) => {
+ t.error(err)
+ data.body
+ .resume()
+ .on('end', () => {
+ t.pass()
+ })
+ })
+ })
+})
diff --git a/test/client-post.js b/test/client-post.js
new file mode 100644
index 0000000..363b43c
--- /dev/null
+++ b/test/client-post.js
@@ -0,0 +1,73 @@
+'use strict'
+
+const { test } = require('tap')
+const { Client } = require('..')
+const { createServer } = require('http')
+const { Blob } = require('buffer')
+
+test('request post blob', { skip: !Blob }, (t) => {
+ t.plan(4)
+
+ const server = createServer(async (req, res) => {
+ t.equal(req.headers['content-type'], 'application/json')
+ let str = ''
+ for await (const chunk of req) {
+ str += chunk
+ }
+ t.equal(str, 'asd')
+ res.end()
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ client.request({
+ path: '/',
+ method: 'GET',
+ body: new Blob(['asd'], {
+ type: 'application/json'
+ })
+ }, (err, data) => {
+ t.error(err)
+ data.body.resume().on('end', () => {
+ t.pass()
+ })
+ })
+ })
+})
+
+test('request post arrayBuffer', { skip: !Blob }, (t) => {
+ t.plan(3)
+
+ const server = createServer(async (req, res) => {
+ let str = ''
+ for await (const chunk of req) {
+ str += chunk
+ }
+ t.equal(str, 'asd')
+ res.end()
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ const buf = Buffer.from('asd')
+ const dst = new ArrayBuffer(buf.byteLength)
+ buf.copy(new Uint8Array(dst))
+
+ client.request({
+ path: '/',
+ method: 'GET',
+ body: dst
+ }, (err, data) => {
+ t.error(err)
+ data.body.resume().on('end', () => {
+ t.pass()
+ })
+ })
+ })
+})
diff --git a/test/client-reconnect.js b/test/client-reconnect.js
new file mode 100644
index 0000000..ae1a206
--- /dev/null
+++ b/test/client-reconnect.js
@@ -0,0 +1,54 @@
+'use strict'
+
+const { test } = require('tap')
+const { Client } = require('..')
+const { createServer } = require('http')
+const FakeTimers = require('@sinonjs/fake-timers')
+const timers = require('../lib/timers')
+
+test('multiple reconnect', (t) => {
+ t.plan(5)
+
+ let n = 0
+ const clock = FakeTimers.install()
+ t.teardown(clock.uninstall.bind(clock))
+
+ const orgTimers = { ...timers }
+ Object.assign(timers, { setTimeout, clearTimeout })
+ t.teardown(() => {
+ Object.assign(timers, orgTimers)
+ })
+
+ const server = createServer((req, res) => {
+ n === 0 ? res.destroy() : res.end('ok')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ client.request({ path: '/', method: 'GET' }, (err, data) => {
+ t.ok(err)
+ t.equal(err.code, 'UND_ERR_SOCKET')
+ })
+
+ client.request({ path: '/', method: 'GET' }, (err, data) => {
+ t.error(err)
+ data.body
+ .resume()
+ .on('end', () => {
+ t.pass()
+ })
+ })
+
+ client.on('disconnect', () => {
+ if (++n === 1) {
+ t.pass()
+ }
+ process.nextTick(() => {
+ clock.tick(1000)
+ })
+ })
+ })
+})
diff --git a/test/client-request.js b/test/client-request.js
new file mode 100644
index 0000000..3e66705
--- /dev/null
+++ b/test/client-request.js
@@ -0,0 +1,997 @@
+/* globals AbortController */
+
+'use strict'
+
+const { test } = require('tap')
+const { Client, errors } = require('..')
+const { createServer } = require('http')
+const EE = require('events')
+const { kConnect } = require('../lib/core/symbols')
+const { Readable } = require('stream')
+const net = require('net')
+const { promisify } = require('util')
+const { NotSupportedError } = require('../lib/core/errors')
+const { nodeMajor } = require('../lib/core/util')
+const { parseFormDataString } = require('./utils/formdata')
+
+test('request dump', (t) => {
+ t.plan(3)
+
+ const server = createServer((req, res) => {
+ res.end('hello')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ let dumped = false
+ client.on('disconnect', () => {
+ t.equal(dumped, true)
+ })
+ client.request({
+ path: '/',
+ method: 'GET'
+ }, (err, { body }) => {
+ t.error(err)
+ body.dump().then(() => {
+ dumped = true
+ t.pass()
+ })
+ })
+ })
+})
+
+test('request dump with abort signal', (t) => {
+ t.plan(2)
+ const server = createServer((req, res) => {
+ res.write('hello')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ client.request({
+ path: '/',
+ method: 'GET'
+ }, (err, { body }) => {
+ t.error(err)
+ let ac
+ if (!global.AbortController) {
+ const { AbortController } = require('abort-controller')
+ ac = new AbortController()
+ } else {
+ ac = new AbortController()
+ }
+ body.dump({ signal: ac.signal }).catch((err) => {
+ t.equal(err.name, 'AbortError')
+ server.close()
+ })
+ ac.abort()
+ })
+ })
+})
+
+test('request hwm', (t) => {
+ t.plan(2)
+ const server = createServer((req, res) => {
+ res.write('hello')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ client.request({
+ path: '/',
+ method: 'GET',
+ highWaterMark: 1000
+ }, (err, { body }) => {
+ t.error(err)
+ t.same(body.readableHighWaterMark, 1000)
+ body.dump()
+ })
+ })
+})
+
+test('request abort before headers', (t) => {
+ t.plan(6)
+
+ const signal = new EE()
+ const server = createServer((req, res) => {
+ res.end('hello')
+ signal.emit('abort')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ client[kConnect](() => {
+ client.request({
+ path: '/',
+ method: 'GET',
+ signal
+ }, (err) => {
+ t.type(err, errors.RequestAbortedError)
+ t.equal(signal.listenerCount('abort'), 0)
+ })
+ t.equal(signal.listenerCount('abort'), 1)
+
+ client.request({
+ path: '/',
+ method: 'GET',
+ signal
+ }, (err) => {
+ t.type(err, errors.RequestAbortedError)
+ t.equal(signal.listenerCount('abort'), 0)
+ })
+ t.equal(signal.listenerCount('abort'), 2)
+ })
+ })
+})
+
+test('request body destroyed on invalid callback', (t) => {
+ t.plan(1)
+
+ const server = createServer((req, res) => {
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ const body = new Readable({
+ read () {}
+ })
+ try {
+ client.request({
+ path: '/',
+ method: 'GET',
+ body
+ }, null)
+ } catch (err) {
+ t.equal(body.destroyed, true)
+ }
+ })
+})
+
+test('trailers', (t) => {
+ t.plan(1)
+
+ const server = createServer((req, res) => {
+ res.writeHead(200, { Trailer: 'Content-MD5' })
+ res.addTrailers({ 'Content-MD5': 'test' })
+ res.end()
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, async () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.close.bind(client))
+
+ const { body, trailers } = await client.request({
+ path: '/',
+ method: 'GET'
+ })
+
+ body
+ .on('data', () => t.fail())
+ .on('end', () => {
+ t.strictSame(trailers, { 'content-md5': 'test' })
+ })
+ })
+})
+
+test('destroy socket abruptly', { skip: true }, async (t) => {
+ t.plan(2)
+
+ const server = net.createServer((socket) => {
+ const lines = [
+ 'HTTP/1.1 200 OK',
+ 'Date: Sat, 09 Oct 2010 14:28:02 GMT',
+ 'Connection: close',
+ '',
+ 'the body'
+ ]
+ socket.end(lines.join('\r\n'))
+
+ // Unfortunately calling destroy synchronously might get us flaky results,
+ // therefore we delay it to the next event loop run.
+ setImmediate(socket.destroy.bind(socket))
+ })
+ t.teardown(server.close.bind(server))
+
+ await promisify(server.listen.bind(server))(0)
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.close.bind(client))
+
+ const { statusCode, body } = await client.request({
+ path: '/',
+ method: 'GET'
+ })
+
+ t.equal(statusCode, 200)
+
+ body.setEncoding('utf8')
+
+ let actual = ''
+
+ for await (const chunk of body) {
+ actual += chunk
+ }
+
+ t.equal(actual, 'the body')
+})
+
+test('destroy socket abruptly with keep-alive', { skip: true }, async (t) => {
+ t.plan(2)
+
+ const server = net.createServer((socket) => {
+ const lines = [
+ 'HTTP/1.1 200 OK',
+ 'Date: Sat, 09 Oct 2010 14:28:02 GMT',
+ 'Connection: keep-alive',
+ 'Content-Length: 42',
+ '',
+ 'the body'
+ ]
+ socket.end(lines.join('\r\n'))
+
+ // Unfortunately calling destroy synchronously might get us flaky results,
+ // therefore we delay it to the next event loop run.
+ setImmediate(socket.destroy.bind(socket))
+ })
+ t.teardown(server.close.bind(server))
+
+ await promisify(server.listen.bind(server))(0)
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.close.bind(client))
+
+ const { statusCode, body } = await client.request({
+ path: '/',
+ method: 'GET'
+ })
+
+ t.equal(statusCode, 200)
+
+ body.setEncoding('utf8')
+
+ try {
+ /* eslint-disable */
+ for await (const _ of body) {
+ // empty on purpose
+ }
+ /* eslint-enable */
+ t.fail('no error')
+ } catch (err) {
+ t.pass('error happened')
+ }
+})
+
+test('request json', (t) => {
+ t.plan(1)
+
+ const obj = { asd: true }
+ const server = createServer((req, res) => {
+ res.end(JSON.stringify(obj))
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, async () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ const { body } = await client.request({
+ path: '/',
+ method: 'GET'
+ })
+ t.strictSame(obj, await body.json())
+ })
+})
+
+test('request long multibyte json', (t) => {
+ t.plan(1)
+
+ const obj = { asd: 'ã‚'.repeat(100000) }
+ const server = createServer((req, res) => {
+ res.end(JSON.stringify(obj))
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, async () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ const { body } = await client.request({
+ path: '/',
+ method: 'GET'
+ })
+ t.strictSame(obj, await body.json())
+ })
+})
+
+test('request text', (t) => {
+ t.plan(1)
+
+ const obj = { asd: true }
+ const server = createServer((req, res) => {
+ res.end(JSON.stringify(obj))
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, async () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ const { body } = await client.request({
+ path: '/',
+ method: 'GET'
+ })
+ t.strictSame(JSON.stringify(obj), await body.text())
+ })
+})
+
+test('empty host header', (t) => {
+ t.plan(3)
+
+ const server = createServer((req, res) => {
+ res.end(req.headers.host)
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, async () => {
+ const serverAddress = `localhost:${server.address().port}`
+ const client = new Client(`http://${serverAddress}`)
+ t.teardown(client.destroy.bind(client))
+
+ const getWithHost = async (host, wanted) => {
+ const { body } = await client.request({
+ path: '/',
+ method: 'GET',
+ headers: { host }
+ })
+ t.strictSame(await body.text(), wanted)
+ }
+
+ await getWithHost('test', 'test')
+ await getWithHost(undefined, serverAddress)
+ await getWithHost('', '')
+ })
+})
+
+test('request long multibyte text', (t) => {
+ t.plan(1)
+
+ const obj = { asd: 'ã‚'.repeat(100000) }
+ const server = createServer((req, res) => {
+ res.end(JSON.stringify(obj))
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, async () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ const { body } = await client.request({
+ path: '/',
+ method: 'GET'
+ })
+ t.strictSame(JSON.stringify(obj), await body.text())
+ })
+})
+
+test('request blob', { skip: nodeMajor < 16 }, (t) => {
+ t.plan(2)
+
+ const obj = { asd: true }
+ const server = createServer((req, res) => {
+ res.setHeader('Content-Type', 'application/json')
+ res.end(JSON.stringify(obj))
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, async () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ const { body } = await client.request({
+ path: '/',
+ method: 'GET'
+ })
+
+ const blob = await body.blob()
+ t.strictSame(obj, JSON.parse(await blob.text()))
+ t.equal(blob.type, 'application/json')
+ })
+})
+
+test('request arrayBuffer', (t) => {
+ t.plan(2)
+
+ const obj = { asd: true }
+ const server = createServer((req, res) => {
+ res.end(JSON.stringify(obj))
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, async () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ const { body } = await client.request({
+ path: '/',
+ method: 'GET'
+ })
+ const ab = await body.arrayBuffer()
+
+ t.strictSame(Buffer.from(JSON.stringify(obj)), Buffer.from(ab))
+ t.ok(ab instanceof ArrayBuffer)
+ })
+})
+
+test('request body', { skip: nodeMajor < 16 }, (t) => {
+ t.plan(1)
+
+ const obj = { asd: true }
+ const server = createServer((req, res) => {
+ res.end(JSON.stringify(obj))
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, async () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ const { body } = await client.request({
+ path: '/',
+ method: 'GET'
+ })
+
+ let x = ''
+ for await (const chunk of body.body) {
+ x += Buffer.from(chunk)
+ }
+ t.strictSame(JSON.stringify(obj), x)
+ })
+})
+
+test('request post body no missing data', { skip: nodeMajor < 16 }, (t) => {
+ t.plan(2)
+
+ const server = createServer(async (req, res) => {
+ let ret = ''
+ for await (const chunk of req) {
+ ret += chunk
+ }
+ t.equal(ret, 'asd')
+ res.end()
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, async () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ const { body } = await client.request({
+ path: '/',
+ method: 'GET',
+ body: new Readable({
+ read () {
+ this.push('asd')
+ this.push(null)
+ }
+ }),
+ maxRedirections: 2
+ })
+ await body.text()
+ t.pass()
+ })
+})
+
+test('request post body no extra data handler', { skip: nodeMajor < 16 }, (t) => {
+ t.plan(3)
+
+ const server = createServer(async (req, res) => {
+ let ret = ''
+ for await (const chunk of req) {
+ ret += chunk
+ }
+ t.equal(ret, 'asd')
+ res.end()
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, async () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ const reqBody = new Readable({
+ read () {
+ this.push('asd')
+ this.push(null)
+ }
+ })
+ process.nextTick(() => {
+ t.equal(reqBody.listenerCount('data'), 0)
+ })
+ const { body } = await client.request({
+ path: '/',
+ method: 'GET',
+ body: reqBody,
+ maxRedirections: 0
+ })
+ await body.text()
+ t.pass()
+ })
+})
+
+test('request with onInfo callback', (t) => {
+ t.plan(3)
+ const infos = []
+ const server = createServer((req, res) => {
+ res.writeProcessing()
+ res.setHeader('Content-Type', 'application/json')
+ res.end(JSON.stringify({ foo: 'bar' }))
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, async () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ await client.request({
+ path: '/',
+ method: 'GET',
+ onInfo: (x) => { infos.push(x) }
+ })
+ t.equal(infos.length, 1)
+ t.equal(infos[0].statusCode, 102)
+ t.pass()
+ })
+})
+
+test('request with onInfo callback but socket is destroyed before end of response', (t) => {
+ t.plan(5)
+ const infos = []
+ let response
+ const server = createServer((req, res) => {
+ response = res
+ res.writeProcessing()
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, async () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+ try {
+ await client.request({
+ path: '/',
+ method: 'GET',
+ onInfo: (x) => {
+ infos.push(x)
+ response.destroy()
+ }
+ })
+ t.error()
+ } catch (e) {
+ t.ok(e)
+ t.equal(e.message, 'other side closed')
+ }
+ t.equal(infos.length, 1)
+ t.equal(infos[0].statusCode, 102)
+ t.pass()
+ })
+})
+
+test('request onInfo callback headers parsing', async (t) => {
+ t.plan(4)
+ const infos = []
+
+ const server = net.createServer((socket) => {
+ const lines = [
+ 'HTTP/1.1 103 Early Hints',
+ 'Link: </style.css>; rel=preload; as=style',
+ '',
+ 'HTTP/1.1 200 OK',
+ 'Date: Sat, 09 Oct 2010 14:28:02 GMT',
+ 'Connection: close',
+ '',
+ 'the body'
+ ]
+ socket.end(lines.join('\r\n'))
+ })
+ t.teardown(server.close.bind(server))
+
+ await promisify(server.listen.bind(server))(0)
+
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.close.bind(client))
+
+ const { body } = await client.request({
+ path: '/',
+ method: 'GET',
+ onInfo: (x) => { infos.push(x) }
+ })
+ await body.dump()
+ t.equal(infos.length, 1)
+ t.equal(infos[0].statusCode, 103)
+ t.same(infos[0].headers, { link: '</style.css>; rel=preload; as=style' })
+ t.pass()
+})
+
+test('request raw responseHeaders', async (t) => {
+ t.plan(4)
+ const infos = []
+
+ const server = net.createServer((socket) => {
+ const lines = [
+ 'HTTP/1.1 103 Early Hints',
+ 'Link: </style.css>; rel=preload; as=style',
+ '',
+ 'HTTP/1.1 200 OK',
+ 'Date: Sat, 09 Oct 2010 14:28:02 GMT',
+ 'Connection: close',
+ '',
+ 'the body'
+ ]
+ socket.end(lines.join('\r\n'))
+ })
+ t.teardown(server.close.bind(server))
+
+ await promisify(server.listen.bind(server))(0)
+
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.close.bind(client))
+
+ const { body, headers } = await client.request({
+ path: '/',
+ method: 'GET',
+ responseHeaders: 'raw',
+ onInfo: (x) => { infos.push(x) }
+ })
+ await body.dump()
+ t.equal(infos.length, 1)
+ t.same(infos[0].headers, ['Link', '</style.css>; rel=preload; as=style'])
+ t.same(headers, ['Date', 'Sat, 09 Oct 2010 14:28:02 GMT', 'Connection', 'close'])
+ t.pass()
+})
+
+test('request formData', { skip: nodeMajor < 16 }, (t) => {
+ t.plan(1)
+
+ const obj = { asd: true }
+ const server = createServer((req, res) => {
+ res.end(JSON.stringify(obj))
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, async () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ const { body } = await client.request({
+ path: '/',
+ method: 'GET'
+ })
+
+ try {
+ await body.formData()
+ t.fail('should throw NotSupportedError')
+ } catch (error) {
+ t.ok(error instanceof NotSupportedError)
+ }
+ })
+})
+
+test('request text2', (t) => {
+ t.plan(2)
+
+ const obj = { asd: true }
+ const server = createServer((req, res) => {
+ res.end(JSON.stringify(obj))
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, async () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ const { body } = await client.request({
+ path: '/',
+ method: 'GET'
+ })
+ const p = body.text()
+ let ret = ''
+ body.on('data', chunk => {
+ ret += chunk
+ }).on('end', () => {
+ t.equal(JSON.stringify(obj), ret)
+ })
+ t.strictSame(JSON.stringify(obj), await p)
+ })
+})
+
+test('request with FormData body', { skip: nodeMajor < 16 }, (t) => {
+ const { FormData } = require('../')
+ const { Blob } = require('buffer')
+
+ const fd = new FormData()
+ fd.set('key', 'value')
+ fd.set('file', new Blob(['Hello, world!']), 'hello_world.txt')
+
+ const server = createServer(async (req, res) => {
+ const contentType = req.headers['content-type']
+ // ensure we received a multipart/form-data header
+ t.ok(/^multipart\/form-data; boundary=-+formdata-undici-0\d+$/.test(contentType))
+
+ const chunks = []
+
+ for await (const chunk of req) {
+ chunks.push(chunk)
+ }
+
+ const { fileMap, fields } = await parseFormDataString(
+ Buffer.concat(chunks),
+ contentType
+ )
+
+ t.same(fields[0], { key: 'key', value: 'value' })
+ t.ok(fileMap.has('file'))
+ t.equal(fileMap.get('file').data.toString(), 'Hello, world!')
+ t.same(fileMap.get('file').info, {
+ filename: 'hello_world.txt',
+ encoding: '7bit',
+ mimeType: 'application/octet-stream'
+ })
+
+ return res.end()
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, async () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ await client.request({
+ path: '/',
+ method: 'POST',
+ body: fd
+ })
+
+ t.end()
+ })
+})
+
+test('request with FormData body on node < 16', { skip: nodeMajor >= 16 }, async (t) => {
+ t.plan(1)
+
+ // a FormData polyfill, for example
+ class FormData {}
+
+ const fd = new FormData()
+
+ const client = new Client('http://localhost:3000')
+ t.teardown(client.destroy.bind(client))
+
+ await t.rejects(client.request({
+ path: '/',
+ method: 'POST',
+ body: fd
+ }), errors.InvalidArgumentError)
+})
+
+test('request post body Buffer from string', (t) => {
+ t.plan(2)
+ const requestBody = Buffer.from('abcdefghijklmnopqrstuvwxyz')
+
+ const server = createServer(async (req, res) => {
+ let ret = ''
+ for await (const chunk of req) {
+ ret += chunk
+ }
+ t.equal(ret, 'abcdefghijklmnopqrstuvwxyz')
+ res.end()
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, async () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ const { body } = await client.request({
+ path: '/',
+ method: 'POST',
+ body: requestBody,
+ maxRedirections: 2
+ })
+ await body.text()
+ t.pass()
+ })
+})
+
+test('request post body Buffer from buffer', (t) => {
+ t.plan(2)
+ const fullBuffer = new TextEncoder().encode('abcdefghijklmnopqrstuvwxyz')
+ const requestBody = Buffer.from(fullBuffer.buffer, 8, 16)
+
+ const server = createServer(async (req, res) => {
+ let ret = ''
+ for await (const chunk of req) {
+ ret += chunk
+ }
+ t.equal(ret, 'ijklmnopqrstuvwx')
+ res.end()
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, async () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ const { body } = await client.request({
+ path: '/',
+ method: 'POST',
+ body: requestBody,
+ maxRedirections: 2
+ })
+ await body.text()
+ t.pass()
+ })
+})
+
+test('request post body Uint8Array', (t) => {
+ t.plan(2)
+ const fullBuffer = new TextEncoder().encode('abcdefghijklmnopqrstuvwxyz')
+ const requestBody = new Uint8Array(fullBuffer.buffer, 8, 16)
+
+ const server = createServer(async (req, res) => {
+ let ret = ''
+ for await (const chunk of req) {
+ ret += chunk
+ }
+ t.equal(ret, 'ijklmnopqrstuvwx')
+ res.end()
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, async () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ const { body } = await client.request({
+ path: '/',
+ method: 'POST',
+ body: requestBody,
+ maxRedirections: 2
+ })
+ await body.text()
+ t.pass()
+ })
+})
+
+test('request post body Uint32Array', (t) => {
+ t.plan(2)
+ const fullBuffer = new TextEncoder().encode('abcdefghijklmnopqrstuvwxyz')
+ const requestBody = new Uint32Array(fullBuffer.buffer, 8, 4)
+
+ const server = createServer(async (req, res) => {
+ let ret = ''
+ for await (const chunk of req) {
+ ret += chunk
+ }
+ t.equal(ret, 'ijklmnopqrstuvwx')
+ res.end()
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, async () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ const { body } = await client.request({
+ path: '/',
+ method: 'POST',
+ body: requestBody,
+ maxRedirections: 2
+ })
+ await body.text()
+ t.pass()
+ })
+})
+
+test('request post body Float64Array', (t) => {
+ t.plan(2)
+ const fullBuffer = new TextEncoder().encode('abcdefghijklmnopqrstuvwxyz')
+ const requestBody = new Float64Array(fullBuffer.buffer, 8, 2)
+
+ const server = createServer(async (req, res) => {
+ let ret = ''
+ for await (const chunk of req) {
+ ret += chunk
+ }
+ t.equal(ret, 'ijklmnopqrstuvwx')
+ res.end()
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, async () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ const { body } = await client.request({
+ path: '/',
+ method: 'POST',
+ body: requestBody,
+ maxRedirections: 2
+ })
+ await body.text()
+ t.pass()
+ })
+})
+
+test('request post body BigUint64Array', (t) => {
+ t.plan(2)
+ const fullBuffer = new TextEncoder().encode('abcdefghijklmnopqrstuvwxyz')
+ const requestBody = new BigUint64Array(fullBuffer.buffer, 8, 2)
+
+ const server = createServer(async (req, res) => {
+ let ret = ''
+ for await (const chunk of req) {
+ ret += chunk
+ }
+ t.equal(ret, 'ijklmnopqrstuvwx')
+ res.end()
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, async () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ const { body } = await client.request({
+ path: '/',
+ method: 'POST',
+ body: requestBody,
+ maxRedirections: 2
+ })
+ await body.text()
+ t.pass()
+ })
+})
+
+test('request post body DataView', (t) => {
+ t.plan(2)
+ const fullBuffer = new TextEncoder().encode('abcdefghijklmnopqrstuvwxyz')
+ const requestBody = new DataView(fullBuffer.buffer, 8, 16)
+
+ const server = createServer(async (req, res) => {
+ let ret = ''
+ for await (const chunk of req) {
+ ret += chunk
+ }
+ t.equal(ret, 'ijklmnopqrstuvwx')
+ res.end()
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, async () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ const { body } = await client.request({
+ path: '/',
+ method: 'POST',
+ body: requestBody,
+ maxRedirections: 2
+ })
+ await body.text()
+ t.pass()
+ })
+})
diff --git a/test/client-stream.js b/test/client-stream.js
new file mode 100644
index 0000000..a230c44
--- /dev/null
+++ b/test/client-stream.js
@@ -0,0 +1,847 @@
+'use strict'
+
+const { test } = require('tap')
+const { Client, errors } = require('..')
+const { createServer } = require('http')
+const { PassThrough, Writable, Readable } = require('stream')
+const EE = require('events')
+
+test('stream get', (t) => {
+ t.plan(9)
+
+ const server = createServer((req, res) => {
+ t.equal('/', req.url)
+ t.equal('GET', req.method)
+ t.equal(`localhost:${server.address().port}`, req.headers.host)
+ res.setHeader('content-type', 'text/plain')
+ res.end('hello')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.close.bind(client))
+
+ const signal = new EE()
+ client.stream({
+ signal,
+ path: '/',
+ method: 'GET',
+ opaque: new PassThrough()
+ }, ({ statusCode, headers, opaque: pt }) => {
+ t.equal(statusCode, 200)
+ t.equal(headers['content-type'], 'text/plain')
+ const bufs = []
+ pt.on('data', (buf) => {
+ bufs.push(buf)
+ })
+ pt.on('end', () => {
+ t.equal('hello', Buffer.concat(bufs).toString('utf8'))
+ })
+ return pt
+ }, (err) => {
+ t.equal(signal.listenerCount('abort'), 0)
+ t.error(err)
+ })
+ t.equal(signal.listenerCount('abort'), 1)
+ })
+})
+
+test('stream promise get', (t) => {
+ t.plan(6)
+
+ const server = createServer((req, res) => {
+ t.equal('/', req.url)
+ t.equal('GET', req.method)
+ t.equal(`localhost:${server.address().port}`, req.headers.host)
+ res.setHeader('content-type', 'text/plain')
+ res.end('hello')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, async () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.close.bind(client))
+
+ await client.stream({
+ path: '/',
+ method: 'GET',
+ opaque: new PassThrough()
+ }, ({ statusCode, headers, opaque: pt }) => {
+ t.equal(statusCode, 200)
+ t.equal(headers['content-type'], 'text/plain')
+ const bufs = []
+ pt.on('data', (buf) => {
+ bufs.push(buf)
+ })
+ pt.on('end', () => {
+ t.equal('hello', Buffer.concat(bufs).toString('utf8'))
+ })
+ return pt
+ })
+ })
+})
+
+test('stream GET destroy res', (t) => {
+ t.plan(14)
+
+ const server = createServer((req, res) => {
+ t.equal('/', req.url)
+ t.equal('GET', req.method)
+ t.equal(`localhost:${server.address().port}`, req.headers.host)
+ res.setHeader('content-type', 'text/plain')
+ res.end('hello')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.close.bind(client))
+
+ client.stream({
+ path: '/',
+ method: 'GET'
+ }, ({ statusCode, headers }) => {
+ t.equal(statusCode, 200)
+ t.equal(headers['content-type'], 'text/plain')
+
+ const pt = new PassThrough()
+ .on('error', (err) => {
+ t.ok(err)
+ })
+ .on('data', () => {
+ pt.destroy(new Error('kaboom'))
+ })
+
+ return pt
+ }, (err) => {
+ t.ok(err)
+ })
+
+ client.stream({
+ path: '/',
+ method: 'GET'
+ }, ({ statusCode, headers }) => {
+ t.equal(statusCode, 200)
+ t.equal(headers['content-type'], 'text/plain')
+
+ let ret = ''
+ const pt = new PassThrough()
+ pt.on('data', chunk => {
+ ret += chunk
+ }).on('end', () => {
+ t.equal(ret, 'hello')
+ })
+
+ return pt
+ }, (err) => {
+ t.error(err)
+ })
+ })
+})
+
+test('stream GET remote destroy', (t) => {
+ t.plan(4)
+
+ const server = createServer((req, res) => {
+ res.write('asd')
+ setImmediate(() => {
+ res.destroy()
+ })
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.close.bind(client))
+
+ client.stream({
+ path: '/',
+ method: 'GET'
+ }, () => {
+ const pt = new PassThrough()
+ pt.on('error', (err) => {
+ t.ok(err)
+ })
+ return pt
+ }, (err) => {
+ t.ok(err)
+ })
+
+ client.stream({
+ path: '/',
+ method: 'GET'
+ }, () => {
+ const pt = new PassThrough()
+ pt.on('error', (err) => {
+ t.ok(err)
+ })
+ return pt
+ }).catch((err) => {
+ t.ok(err)
+ })
+ })
+})
+
+test('stream response resume back pressure and non standard error', (t) => {
+ t.plan(5)
+
+ const server = createServer((req, res) => {
+ res.write(Buffer.alloc(1e3))
+ setImmediate(() => {
+ res.write(Buffer.alloc(1e7))
+ res.end()
+ })
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.close.bind(client))
+
+ const pt = new PassThrough()
+ client.stream({
+ path: '/',
+ method: 'GET'
+ }, () => {
+ pt.on('data', () => {
+ pt.emit('error', new Error('kaboom'))
+ }).once('error', (err) => {
+ t.equal(err.message, 'kaboom')
+ })
+ return pt
+ }, (err) => {
+ t.ok(err)
+ t.equal(pt.destroyed, true)
+ })
+
+ client.once('disconnect', (err) => {
+ t.ok(err)
+ })
+
+ client.stream({
+ path: '/',
+ method: 'GET'
+ }, () => {
+ const pt = new PassThrough()
+ pt.resume()
+ return pt
+ }, (err) => {
+ t.error(err)
+ })
+ })
+})
+
+test('stream waits only for writable side', (t) => {
+ t.plan(2)
+
+ const server = createServer((req, res) => {
+ res.end(Buffer.alloc(1e3))
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.close.bind(client))
+
+ const pt = new PassThrough({ autoDestroy: false })
+ client.stream({
+ path: '/',
+ method: 'GET'
+ }, () => pt, (err) => {
+ t.error(err)
+ t.equal(pt.destroyed, false)
+ })
+ })
+})
+
+test('stream args validation', (t) => {
+ t.plan(3)
+
+ const client = new Client('http://localhost:5000')
+ client.stream({
+ path: '/',
+ method: 'GET'
+ }, null, (err) => {
+ t.type(err, errors.InvalidArgumentError)
+ })
+
+ client.stream(null, null, (err) => {
+ t.type(err, errors.InvalidArgumentError)
+ })
+
+ try {
+ client.stream(null, null, 'asd')
+ } catch (err) {
+ t.type(err, errors.InvalidArgumentError)
+ }
+})
+
+test('stream args validation promise', (t) => {
+ t.plan(2)
+
+ const client = new Client('http://localhost:5000')
+ client.stream({
+ path: '/',
+ method: 'GET'
+ }, null).catch((err) => {
+ t.type(err, errors.InvalidArgumentError)
+ })
+
+ client.stream(null, null).catch((err) => {
+ t.type(err, errors.InvalidArgumentError)
+ })
+})
+
+test('stream destroy if not readable', (t) => {
+ t.plan(2)
+
+ const server = createServer((req, res) => {
+ res.end()
+ })
+ t.teardown(server.close.bind(server))
+
+ const pt = new PassThrough()
+ pt.readable = false
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ client.stream({
+ path: '/',
+ method: 'GET'
+ }, () => {
+ return pt
+ }, (err) => {
+ t.error(err)
+ t.equal(pt.destroyed, true)
+ })
+ })
+})
+
+test('stream server side destroy', (t) => {
+ t.plan(1)
+
+ const server = createServer((req, res) => {
+ res.destroy()
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ client.stream({
+ path: '/',
+ method: 'GET'
+ }, () => {
+ t.fail()
+ }, (err) => {
+ t.type(err, errors.SocketError)
+ })
+ })
+})
+
+test('stream invalid return', (t) => {
+ t.plan(1)
+
+ const server = createServer((req, res) => {
+ res.write('asd')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ client.stream({
+ path: '/',
+ method: 'GET'
+ }, () => {
+ return {}
+ }, (err) => {
+ t.type(err, errors.InvalidReturnValueError)
+ })
+ })
+})
+
+test('stream body without destroy', (t) => {
+ t.plan(1)
+
+ const server = createServer((req, res) => {
+ res.end('asd')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ client.stream({
+ path: '/',
+ method: 'GET'
+ }, () => {
+ const pt = new PassThrough({ autoDestroy: false })
+ pt.destroy = null
+ pt.resume()
+ return pt
+ }, (err) => {
+ t.error(err)
+ })
+ })
+})
+
+test('stream factory abort', (t) => {
+ t.plan(3)
+
+ const server = createServer((req, res) => {
+ res.end('asd')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ const signal = new EE()
+ client.stream({
+ path: '/',
+ method: 'GET',
+ signal
+ }, () => {
+ signal.emit('abort')
+ return new PassThrough()
+ }, (err) => {
+ t.equal(signal.listenerCount('abort'), 0)
+ t.type(err, errors.RequestAbortedError)
+ })
+ t.equal(signal.listenerCount('abort'), 1)
+ })
+})
+
+test('stream factory throw', (t) => {
+ t.plan(3)
+
+ const server = createServer((req, res) => {
+ res.end('asd')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ client.stream({
+ path: '/',
+ method: 'GET'
+ }, () => {
+ throw new Error('asd')
+ }, (err) => {
+ t.equal(err.message, 'asd')
+ })
+ client.stream({
+ path: '/',
+ method: 'GET'
+ }, () => {
+ throw new Error('asd')
+ }, (err) => {
+ t.equal(err.message, 'asd')
+ })
+ client.stream({
+ path: '/',
+ method: 'GET'
+ }, () => {
+ return new PassThrough()
+ }, (err) => {
+ t.error(err)
+ })
+ })
+})
+
+test('stream CONNECT throw', (t) => {
+ t.plan(1)
+
+ const server = createServer((req, res) => {
+ res.end('asd')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ client.stream({
+ path: '/',
+ method: 'CONNECT'
+ }, () => {
+ }, (err) => {
+ t.type(err, errors.InvalidArgumentError)
+ })
+ })
+})
+
+test('stream abort after complete', (t) => {
+ t.plan(1)
+
+ const server = createServer((req, res) => {
+ res.end('asd')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ const pt = new PassThrough()
+ const signal = new EE()
+ client.stream({
+ path: '/',
+ method: 'GET',
+ signal
+ }, () => {
+ return pt
+ }, (err) => {
+ t.error(err)
+ signal.emit('abort')
+ })
+ })
+})
+
+test('stream abort before dispatch', (t) => {
+ t.plan(1)
+
+ const server = createServer((req, res) => {
+ res.end('asd')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ const pt = new PassThrough()
+ const signal = new EE()
+ client.stream({
+ path: '/',
+ method: 'GET',
+ signal
+ }, () => {
+ return pt
+ }, (err) => {
+ t.type(err, errors.RequestAbortedError)
+ })
+ signal.emit('abort')
+ })
+})
+
+test('trailers', (t) => {
+ t.plan(2)
+
+ const server = createServer((req, res) => {
+ res.writeHead(200, { Trailer: 'Content-MD5' })
+ res.addTrailers({ 'Content-MD5': 'test' })
+ res.end()
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.close.bind(client))
+
+ client.stream({
+ path: '/',
+ method: 'GET'
+ }, () => new PassThrough(), (err, data) => {
+ t.error(err)
+ t.strictSame(data.trailers, { 'content-md5': 'test' })
+ })
+ })
+})
+
+test('stream ignore 1xx', (t) => {
+ t.plan(2)
+
+ const server = createServer((req, res) => {
+ res.writeProcessing()
+ res.end('hello')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.close.bind(client))
+
+ let buf = ''
+ client.stream({
+ path: '/',
+ method: 'GET'
+ }, () => new Writable({
+ write (chunk, encoding, callback) {
+ buf += chunk
+ callback()
+ }
+ }), (err, data) => {
+ t.error(err)
+ t.equal(buf, 'hello')
+ })
+ })
+})
+
+test('stream ignore 1xx and use onInfo', (t) => {
+ t.plan(4)
+
+ const infos = []
+ const server = createServer((req, res) => {
+ res.writeProcessing()
+ res.end('hello')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.close.bind(client))
+
+ let buf = ''
+ client.stream({
+ path: '/',
+ method: 'GET',
+ onInfo: (x) => {
+ infos.push(x)
+ }
+ }, () => new Writable({
+ write (chunk, encoding, callback) {
+ buf += chunk
+ callback()
+ }
+ }), (err, data) => {
+ t.error(err)
+ t.equal(buf, 'hello')
+ t.equal(infos.length, 1)
+ t.equal(infos[0].statusCode, 102)
+ })
+ })
+})
+
+test('stream backpressure', (t) => {
+ t.plan(2)
+
+ const expected = Buffer.alloc(1e6).toString()
+
+ const server = createServer((req, res) => {
+ res.writeProcessing()
+ res.end(expected)
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.close.bind(client))
+
+ let buf = ''
+ client.stream({
+ path: '/',
+ method: 'GET'
+ }, () => new Writable({
+ highWaterMark: 1,
+ write (chunk, encoding, callback) {
+ buf += chunk
+ process.nextTick(callback)
+ }
+ }), (err, data) => {
+ t.error(err)
+ t.equal(buf, expected)
+ })
+ })
+})
+
+test('stream body destroyed on invalid callback', (t) => {
+ t.plan(1)
+
+ const server = createServer((req, res) => {
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ const body = new Readable({
+ read () {}
+ })
+ try {
+ client.stream({
+ path: '/',
+ method: 'GET',
+ body
+ }, () => {}, null)
+ } catch (err) {
+ t.equal(body.destroyed, true)
+ }
+ })
+})
+
+test('stream needDrain', (t) => {
+ t.plan(3)
+
+ const server = createServer((req, res) => {
+ res.end(Buffer.alloc(4096))
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(() => {
+ client.destroy()
+ })
+
+ const dst = new PassThrough()
+ dst.pause()
+
+ if (dst.writableNeedDrain === undefined) {
+ Object.defineProperty(dst, 'writableNeedDrain', {
+ get () {
+ return this._writableState.needDrain
+ }
+ })
+ }
+
+ while (dst.write(Buffer.alloc(4096))) {
+ // Do nothing.
+ }
+
+ const orgWrite = dst.write
+ dst.write = () => t.fail()
+ const p = client.stream({
+ path: '/',
+ method: 'GET'
+ }, () => {
+ t.equal(dst._writableState.needDrain, true)
+ t.equal(dst.writableNeedDrain, true)
+
+ setImmediate(() => {
+ dst.write = (...args) => {
+ orgWrite.call(dst, ...args)
+ }
+ dst.resume()
+ })
+
+ return dst
+ })
+
+ p.then(() => {
+ t.pass()
+ })
+ })
+})
+
+test('stream legacy needDrain', (t) => {
+ t.plan(3)
+
+ const server = createServer((req, res) => {
+ res.end(Buffer.alloc(4096))
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(() => {
+ client.destroy()
+ })
+
+ const dst = new PassThrough()
+ dst.pause()
+
+ if (dst.writableNeedDrain !== undefined) {
+ Object.defineProperty(dst, 'writableNeedDrain', {
+ get () {
+ }
+ })
+ }
+
+ while (dst.write(Buffer.alloc(4096))) {
+ // Do nothing
+ }
+
+ const orgWrite = dst.write
+ dst.write = () => t.fail()
+ const p = client.stream({
+ path: '/',
+ method: 'GET'
+ }, () => {
+ t.equal(dst._writableState.needDrain, true)
+ t.equal(dst.writableNeedDrain, undefined)
+
+ setImmediate(() => {
+ dst.write = (...args) => {
+ orgWrite.call(dst, ...args)
+ }
+ dst.resume()
+ })
+
+ return dst
+ })
+
+ p.then(() => {
+ t.pass()
+ })
+ })
+
+ test('stream throwOnError', (t) => {
+ t.plan(2)
+
+ const errStatusCode = 500
+ const errMessage = 'Internal Server Error'
+
+ const server = createServer((req, res) => {
+ res.writeHead(errStatusCode, { 'Content-Type': 'text/plain' })
+ res.end(errMessage)
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, async () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.close.bind(client))
+
+ client.stream({
+ path: '/',
+ method: 'GET',
+ throwOnError: true,
+ opaque: new PassThrough()
+ }, ({ opaque: pt }) => {
+ pt.on('data', () => {
+ t.fail()
+ })
+ return pt
+ }, (e) => {
+ t.equal(e.status, errStatusCode)
+ t.equal(e.body, errMessage)
+ t.end()
+ })
+ })
+ })
+
+ test('steam throwOnError=true, error on stream', (t) => {
+ t.plan(1)
+
+ const server = createServer((req, res) => {
+ res.end('asd')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, async () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.close.bind(client))
+
+ client.stream({
+ path: '/',
+ method: 'GET',
+ throwOnError: true,
+ opaque: new PassThrough()
+ }, () => {
+ throw new Error('asd')
+ }, (e) => {
+ t.equal(e.message, 'asd')
+ })
+ })
+ })
+})
diff --git a/test/client-timeout.js b/test/client-timeout.js
new file mode 100644
index 0000000..5f1686a
--- /dev/null
+++ b/test/client-timeout.js
@@ -0,0 +1,197 @@
+'use strict'
+
+const { test } = require('tap')
+const { Client, errors } = require('..')
+const { createServer } = require('http')
+const { Readable } = require('stream')
+const FakeTimers = require('@sinonjs/fake-timers')
+const timers = require('../lib/timers')
+
+test('refresh timeout on pause', (t) => {
+ t.plan(1)
+
+ const server = createServer((req, res) => {
+ res.flushHeaders()
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, {
+ bodyTimeout: 500
+ })
+ t.teardown(client.destroy.bind(client))
+
+ client.dispatch({
+ path: '/',
+ method: 'GET'
+ }, {
+ onConnect () {
+ },
+ onHeaders (statusCode, headers, resume) {
+ setTimeout(() => {
+ resume()
+ }, 1000)
+ return false
+ },
+ onData () {
+
+ },
+ onComplete () {
+
+ },
+ onError (err) {
+ t.type(err, errors.BodyTimeoutError)
+ }
+ })
+ })
+})
+
+test('start headers timeout after request body', (t) => {
+ t.plan(2)
+
+ const clock = FakeTimers.install()
+ t.teardown(clock.uninstall.bind(clock))
+
+ const orgTimers = { ...timers }
+ Object.assign(timers, { setTimeout, clearTimeout })
+ t.teardown(() => {
+ Object.assign(timers, orgTimers)
+ })
+
+ const server = createServer((req, res) => {
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, {
+ bodyTimeout: 0,
+ headersTimeout: 100
+ })
+ t.teardown(client.destroy.bind(client))
+
+ const body = new Readable({ read () {} })
+ client.dispatch({
+ path: '/',
+ body,
+ method: 'GET'
+ }, {
+ onConnect () {
+ process.nextTick(() => {
+ clock.tick(200)
+ })
+ queueMicrotask(() => {
+ body.push(null)
+ body.on('end', () => {
+ clock.tick(200)
+ })
+ })
+ },
+ onHeaders (statusCode, headers, resume) {
+ },
+ onData () {
+
+ },
+ onComplete () {
+
+ },
+ onError (err) {
+ t.equal(body.readableEnded, true)
+ t.type(err, errors.HeadersTimeoutError)
+ }
+ })
+ })
+})
+
+test('start headers timeout after async iterator request body', (t) => {
+ t.plan(1)
+
+ const clock = FakeTimers.install()
+ t.teardown(clock.uninstall.bind(clock))
+
+ const orgTimers = { ...timers }
+ Object.assign(timers, { setTimeout, clearTimeout })
+ t.teardown(() => {
+ Object.assign(timers, orgTimers)
+ })
+
+ const server = createServer((req, res) => {
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, {
+ bodyTimeout: 0,
+ headersTimeout: 100
+ })
+ t.teardown(client.destroy.bind(client))
+ let res
+ const body = (async function * () {
+ await new Promise((resolve) => { res = resolve })
+ process.nextTick(() => {
+ clock.tick(200)
+ })
+ })()
+ client.dispatch({
+ path: '/',
+ body,
+ method: 'GET'
+ }, {
+ onConnect () {
+ process.nextTick(() => {
+ clock.tick(200)
+ })
+ queueMicrotask(() => {
+ res()
+ })
+ },
+ onHeaders (statusCode, headers, resume) {
+ },
+ onData () {
+
+ },
+ onComplete () {
+
+ },
+ onError (err) {
+ t.type(err, errors.HeadersTimeoutError)
+ }
+ })
+ })
+})
+
+test('parser resume with no body timeout', (t) => {
+ t.plan(1)
+
+ const server = createServer((req, res) => {
+ res.end('asd')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, {
+ bodyTimeout: 0
+ })
+ t.teardown(client.destroy.bind(client))
+
+ client.dispatch({
+ path: '/',
+ method: 'GET'
+ }, {
+ onConnect () {
+ },
+ onHeaders (statusCode, headers, resume) {
+ setTimeout(resume, 2000)
+ return false
+ },
+ onData () {
+
+ },
+ onComplete () {
+ t.pass()
+ },
+ onError (err) {
+ t.error(err)
+ }
+ })
+ })
+})
diff --git a/test/client-unref.js b/test/client-unref.js
new file mode 100644
index 0000000..c7e3a5d
--- /dev/null
+++ b/test/client-unref.js
@@ -0,0 +1,47 @@
+'use strict'
+
+const { Worker, isMainThread, workerData } = require('worker_threads')
+
+if (isMainThread) {
+ const tap = require('tap')
+ const { createServer } = require('http')
+
+ tap.test('client automatically closes itself when idle', t => {
+ t.plan(1)
+
+ const server = createServer((req, res) => {
+ res.end()
+ })
+ t.teardown(server.close.bind(server))
+ server.keepAliveTimeout = 9999
+
+ server.listen(0, () => {
+ const url = `http://localhost:${server.address().port}`
+ const worker = new Worker(__filename, { workerData: { url } })
+ worker.on('exit', code => {
+ t.equal(code, 0)
+ })
+ })
+ })
+
+ tap.test('client automatically closes itself if the server is not there', t => {
+ t.plan(1)
+
+ const url = 'http://localhost:4242' // hopefully empty port
+ const worker = new Worker(__filename, { workerData: { url } })
+ worker.on('exit', code => {
+ t.equal(code, 0)
+ })
+ })
+} else {
+ const { Client } = require('..')
+
+ const client = new Client(workerData.url)
+ client.request({ path: '/', method: 'GET' }, () => {
+ // We do not care about Errors
+
+ setTimeout(() => {
+ throw new Error()
+ }, 1e3).unref()
+ })
+}
diff --git a/test/client-upgrade.js b/test/client-upgrade.js
new file mode 100644
index 0000000..4ccbcce
--- /dev/null
+++ b/test/client-upgrade.js
@@ -0,0 +1,452 @@
+'use strict'
+
+const { test } = require('tap')
+const { Client, errors } = require('..')
+const net = require('net')
+const http = require('http')
+const EE = require('events')
+const { kBusy } = require('../lib/core/symbols')
+
+test('basic upgrade', (t) => {
+ t.plan(6)
+
+ const server = net.createServer((c) => {
+ c.on('data', (d) => {
+ t.ok(/upgrade: websocket/i.test(d))
+ c.write('HTTP/1.1 101\r\n')
+ c.write('hello: world\r\n')
+ c.write('connection: upgrade\r\n')
+ c.write('upgrade: websocket\r\n')
+ c.write('\r\n')
+ c.write('Body')
+ })
+
+ c.on('end', () => {
+ c.end()
+ })
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.close.bind(client))
+
+ const signal = new EE()
+ client.upgrade({
+ signal,
+ path: '/',
+ method: 'GET',
+ protocol: 'Websocket'
+ }, (err, data) => {
+ t.error(err)
+
+ t.equal(signal.listenerCount('abort'), 0)
+
+ const { headers, socket } = data
+
+ let recvData = ''
+ data.socket.on('data', (d) => {
+ recvData += d
+ })
+
+ socket.on('close', () => {
+ t.equal(recvData.toString(), 'Body')
+ })
+
+ t.same(headers, {
+ hello: 'world',
+ connection: 'upgrade',
+ upgrade: 'websocket'
+ })
+ socket.end()
+ })
+ t.equal(signal.listenerCount('abort'), 1)
+ })
+})
+
+test('basic upgrade promise', (t) => {
+ t.plan(2)
+
+ const server = net.createServer((c) => {
+ c.on('data', (d) => {
+ c.write('HTTP/1.1 101\r\n')
+ c.write('hello: world\r\n')
+ c.write('connection: upgrade\r\n')
+ c.write('upgrade: websocket\r\n')
+ c.write('\r\n')
+ c.write('Body')
+ })
+
+ c.on('end', () => {
+ c.end()
+ })
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, async () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.close.bind(client))
+
+ const { headers, socket } = await client.upgrade({
+ path: '/',
+ method: 'GET',
+ protocol: 'Websocket'
+ })
+
+ let recvData = ''
+ socket.on('data', (d) => {
+ recvData += d
+ })
+
+ socket.on('close', () => {
+ t.equal(recvData.toString(), 'Body')
+ })
+
+ t.same(headers, {
+ hello: 'world',
+ connection: 'upgrade',
+ upgrade: 'websocket'
+ })
+ socket.end()
+ })
+})
+
+test('upgrade error', (t) => {
+ t.plan(1)
+
+ const server = net.createServer((c) => {
+ c.on('data', (d) => {
+ c.write('HTTP/1.1 101\r\n')
+ c.write('hello: world\r\n')
+ c.write('connection: upgrade\r\n')
+ c.write('\r\n')
+ c.write('Body')
+ })
+ c.on('error', () => {
+ // Whether we get an error, end or close is undefined.
+ // Ignore error.
+ })
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, async () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.close.bind(client))
+
+ try {
+ await client.upgrade({
+ path: '/',
+ method: 'GET',
+ protocol: 'Websocket'
+ })
+ } catch (err) {
+ t.ok(err)
+ }
+ })
+})
+
+test('upgrade invalid opts', (t) => {
+ t.plan(6)
+
+ const client = new Client('http://localhost:5432')
+
+ client.upgrade(null, err => {
+ t.type(err, errors.InvalidArgumentError)
+ t.equal(err.message, 'invalid opts')
+ })
+
+ try {
+ client.upgrade(null, null)
+ t.fail()
+ } catch (err) {
+ t.type(err, errors.InvalidArgumentError)
+ t.equal(err.message, 'invalid opts')
+ }
+
+ try {
+ client.upgrade({ path: '/' }, null)
+ t.fail()
+ } catch (err) {
+ t.type(err, errors.InvalidArgumentError)
+ t.equal(err.message, 'invalid callback')
+ }
+})
+
+test('basic upgrade2', (t) => {
+ t.plan(3)
+
+ const server = http.createServer()
+ server.on('upgrade', (req, c, head) => {
+ c.write('HTTP/1.1 101\r\n')
+ c.write('hello: world\r\n')
+ c.write('connection: upgrade\r\n')
+ c.write('upgrade: websocket\r\n')
+ c.write('\r\n')
+ c.write('Body')
+ c.end()
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.close.bind(client))
+
+ client.upgrade({
+ path: '/',
+ method: 'GET',
+ protocol: 'Websocket'
+ }, (err, data) => {
+ t.error(err)
+
+ const { headers, socket } = data
+
+ let recvData = ''
+ data.socket.on('data', (d) => {
+ recvData += d
+ })
+
+ socket.on('close', () => {
+ t.equal(recvData.toString(), 'Body')
+ })
+
+ t.same(headers, {
+ hello: 'world',
+ connection: 'upgrade',
+ upgrade: 'websocket'
+ })
+ socket.end()
+ })
+ })
+})
+
+test('upgrade wait for empty pipeline', (t) => {
+ t.plan(7)
+
+ let canConnect = false
+ const server = http.createServer((req, res) => {
+ res.end()
+ canConnect = true
+ })
+ server.on('upgrade', (req, c, firstBodyChunk) => {
+ t.equal(canConnect, true)
+ c.write('HTTP/1.1 101\r\n')
+ c.write('hello: world\r\n')
+ c.write('connection: upgrade\r\n')
+ c.write('upgrade: websocket\r\n')
+ c.write('\r\n')
+ c.write('Body')
+ c.end()
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, async () => {
+ const client = new Client(`http://localhost:${server.address().port}`, {
+ pipelining: 3
+ })
+ t.teardown(client.close.bind(client))
+
+ client.request({
+ path: '/',
+ method: 'GET'
+ }, (err) => {
+ t.error(err)
+ })
+ client.once('connect', () => {
+ process.nextTick(() => {
+ t.equal(client[kBusy], false)
+
+ client.upgrade({
+ path: '/'
+ }, (err, { socket }) => {
+ t.error(err)
+ let recvData = ''
+ socket.on('data', (d) => {
+ recvData += d
+ })
+
+ socket.on('end', () => {
+ t.equal(recvData.toString(), 'Body')
+ })
+
+ socket.write('Body')
+ socket.end()
+ })
+ t.equal(client[kBusy], true)
+
+ client.request({
+ path: '/',
+ method: 'GET'
+ }, (err) => {
+ t.error(err)
+ })
+ })
+ })
+ })
+})
+
+test('upgrade aborted', (t) => {
+ t.plan(6)
+
+ const server = http.createServer((req, res) => {
+ t.fail()
+ })
+ server.on('upgrade', (req, c, firstBodyChunk) => {
+ t.fail()
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, async () => {
+ const client = new Client(`http://localhost:${server.address().port}`, {
+ pipelining: 3
+ })
+ t.teardown(client.destroy.bind(client))
+
+ const signal = new EE()
+ client.upgrade({
+ path: '/',
+ signal,
+ opaque: 'asd'
+ }, (err, { opaque }) => {
+ t.equal(opaque, 'asd')
+ t.type(err, errors.RequestAbortedError)
+ t.equal(signal.listenerCount('abort'), 0)
+ })
+ t.equal(client[kBusy], true)
+ t.equal(signal.listenerCount('abort'), 1)
+ signal.emit('abort')
+
+ client.close(() => {
+ t.pass()
+ })
+ })
+})
+
+test('basic aborted after res', (t) => {
+ t.plan(1)
+
+ const signal = new EE()
+ const server = http.createServer()
+ server.on('upgrade', (req, c, head) => {
+ c.write('HTTP/1.1 101\r\n')
+ c.write('hello: world\r\n')
+ c.write('connection: upgrade\r\n')
+ c.write('upgrade: websocket\r\n')
+ c.write('\r\n')
+ c.write('Body')
+ c.end()
+ c.on('error', () => {
+
+ })
+ signal.emit('abort')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.close.bind(client))
+
+ client.upgrade({
+ path: '/',
+ method: 'GET',
+ protocol: 'Websocket',
+ signal
+ }, (err) => {
+ t.type(err, errors.RequestAbortedError)
+ })
+ })
+})
+
+test('basic upgrade error', (t) => {
+ t.plan(2)
+
+ const server = net.createServer((c) => {
+ c.on('data', (d) => {
+ c.write('HTTP/1.1 101\r\n')
+ c.write('hello: world\r\n')
+ c.write('connection: upgrade\r\n')
+ c.write('upgrade: websocket\r\n')
+ c.write('\r\n')
+ c.write('Body')
+ })
+ c.on('error', () => {
+
+ })
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.close.bind(client))
+
+ const _err = new Error()
+ client.upgrade({
+ path: '/',
+ method: 'GET',
+ protocol: 'Websocket'
+ }, (err, data) => {
+ t.error(err)
+ data.socket.on('error', (err) => {
+ t.equal(err, _err)
+ })
+ throw _err
+ })
+ })
+})
+
+test('upgrade disconnect', (t) => {
+ t.plan(3)
+
+ const server = net.createServer(connection => {
+ connection.destroy()
+ })
+
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.close.bind(client))
+
+ client.on('disconnect', (origin, [self], error) => {
+ t.equal(client, self)
+ t.type(error, Error)
+ })
+
+ client
+ .upgrade({ path: '/', method: 'GET' })
+ .then(() => {
+ t.fail()
+ })
+ .catch(error => {
+ t.type(error, Error)
+ })
+ })
+})
+
+test('upgrade invalid signal', (t) => {
+ t.plan(2)
+
+ const server = net.createServer(() => {
+ t.fail()
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ client.on('disconnect', () => {
+ t.fail()
+ })
+
+ client.upgrade({
+ path: '/',
+ method: 'GET',
+ protocol: 'Websocket',
+ signal: 'error',
+ opaque: 'asd'
+ }, (err, { opaque }) => {
+ t.equal(opaque, 'asd')
+ t.type(err, errors.InvalidArgumentError)
+ })
+ })
+})
diff --git a/test/client-write-max-listeners.js b/test/client-write-max-listeners.js
new file mode 100644
index 0000000..118cdaa
--- /dev/null
+++ b/test/client-write-max-listeners.js
@@ -0,0 +1,51 @@
+'use strict'
+
+const { test } = require('tap')
+const { Client } = require('..')
+const { createServer } = require('http')
+const { Readable } = require('stream')
+
+test('socket close listener does not leak', (t) => {
+ t.plan(32)
+
+ const server = createServer()
+
+ server.on('request', (req, res) => {
+ res.end('hello')
+ })
+ t.teardown(server.close.bind(server))
+
+ const makeBody = () => {
+ return new Readable({
+ read () {
+ process.nextTick(() => {
+ this.push(null)
+ })
+ }
+ })
+ }
+
+ const onRequest = (err, data) => {
+ t.error(err)
+ data.body.on('end', () => t.pass()).resume()
+ }
+
+ function onWarning (warning) {
+ if (!/ExperimentalWarning/.test(warning)) {
+ t.fail()
+ }
+ }
+ process.on('warning', onWarning)
+ t.teardown(() => {
+ process.removeListener('warning', onWarning)
+ })
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ for (let n = 0; n < 16; ++n) {
+ client.request({ path: '/', method: 'GET', body: makeBody() }, onRequest)
+ }
+ })
+})
diff --git a/test/client.js b/test/client.js
new file mode 100644
index 0000000..92315d6
--- /dev/null
+++ b/test/client.js
@@ -0,0 +1,2096 @@
+'use strict'
+
+const { readFileSync, createReadStream } = require('fs')
+const { createServer } = require('http')
+const { Readable } = require('stream')
+const { test } = require('tap')
+const { Client, errors } = require('..')
+const { kSocket } = require('../lib/core/symbols')
+const { wrapWithAsyncIterable } = require('./utils/async-iterators')
+const EE = require('events')
+const { kUrl, kSize, kConnect, kBusy, kConnected, kRunning } = require('../lib/core/symbols')
+
+const hasIPv6 = (() => {
+ const iFaces = require('os').networkInterfaces()
+ const re = process.platform === 'win32' ? /Loopback Pseudo-Interface/ : /lo/
+ return Object.keys(iFaces).some(
+ (name) => re.test(name) && iFaces[name].some(({ family }) => family === 6)
+ )
+})()
+
+test('basic get', (t) => {
+ t.plan(24)
+
+ const server = createServer((req, res) => {
+ t.equal('/', req.url)
+ t.equal('GET', req.method)
+ t.equal(`localhost:${server.address().port}`, req.headers.host)
+ t.equal(undefined, req.headers.foo)
+ t.equal('bar', req.headers.bar)
+ t.equal(undefined, req.headers['content-length'])
+ res.setHeader('Content-Type', 'text/plain')
+ res.end('hello')
+ })
+ t.teardown(server.close.bind(server))
+
+ const reqHeaders = {
+ foo: undefined,
+ bar: 'bar'
+ }
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, {
+ keepAliveTimeout: 300e3
+ })
+ t.teardown(client.close.bind(client))
+
+ t.equal(client[kUrl].origin, `http://localhost:${server.address().port}`)
+
+ const signal = new EE()
+ client.request({
+ signal,
+ path: '/',
+ method: 'GET',
+ headers: reqHeaders
+ }, (err, data) => {
+ t.error(err)
+ const { statusCode, headers, body } = data
+ t.equal(statusCode, 200)
+ t.equal(signal.listenerCount('abort'), 1)
+ t.equal(headers['content-type'], 'text/plain')
+ const bufs = []
+ body.on('data', (buf) => {
+ bufs.push(buf)
+ })
+ body.on('end', () => {
+ t.equal(signal.listenerCount('abort'), 0)
+ t.equal('hello', Buffer.concat(bufs).toString('utf8'))
+ })
+ })
+ t.equal(signal.listenerCount('abort'), 1)
+
+ client.request({
+ path: '/',
+ method: 'GET',
+ headers: reqHeaders
+ }, (err, { statusCode, headers, body }) => {
+ t.error(err)
+ t.equal(statusCode, 200)
+ t.equal(headers['content-type'], 'text/plain')
+ const bufs = []
+ body.on('data', (buf) => {
+ bufs.push(buf)
+ })
+ body.on('end', () => {
+ t.equal('hello', Buffer.concat(bufs).toString('utf8'))
+ })
+ })
+ })
+})
+
+test('basic get with custom request.reset=true', (t) => {
+ t.plan(26)
+
+ const server = createServer((req, res) => {
+ t.equal('/', req.url)
+ t.equal('GET', req.method)
+ t.equal(`localhost:${server.address().port}`, req.headers.host)
+ t.equal(req.headers.connection, 'close')
+ t.equal(undefined, req.headers.foo)
+ t.equal('bar', req.headers.bar)
+ t.equal(undefined, req.headers['content-length'])
+ res.setHeader('Content-Type', 'text/plain')
+ res.end('hello')
+ })
+ t.teardown(server.close.bind(server))
+
+ const reqHeaders = {
+ foo: undefined,
+ bar: 'bar'
+ }
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, {})
+ t.teardown(client.close.bind(client))
+
+ t.equal(client[kUrl].origin, `http://localhost:${server.address().port}`)
+
+ const signal = new EE()
+ client.request({
+ signal,
+ path: '/',
+ method: 'GET',
+ reset: true,
+ headers: reqHeaders
+ }, (err, data) => {
+ t.error(err)
+ const { statusCode, headers, body } = data
+ t.equal(statusCode, 200)
+ t.equal(signal.listenerCount('abort'), 1)
+ t.equal(headers['content-type'], 'text/plain')
+ const bufs = []
+ body.on('data', (buf) => {
+ bufs.push(buf)
+ })
+ body.on('end', () => {
+ t.equal(signal.listenerCount('abort'), 0)
+ t.equal('hello', Buffer.concat(bufs).toString('utf8'))
+ })
+ })
+ t.equal(signal.listenerCount('abort'), 1)
+
+ client.request({
+ path: '/',
+ reset: true,
+ method: 'GET',
+ headers: reqHeaders
+ }, (err, { statusCode, headers, body }) => {
+ t.error(err)
+ t.equal(statusCode, 200)
+ t.equal(headers['content-type'], 'text/plain')
+ const bufs = []
+ body.on('data', (buf) => {
+ bufs.push(buf)
+ })
+ body.on('end', () => {
+ t.equal('hello', Buffer.concat(bufs).toString('utf8'))
+ })
+ })
+ })
+})
+
+test('basic get with query params', (t) => {
+ t.plan(4)
+
+ const server = createServer((req, res) => {
+ const searchParamsObject = buildParams(req.url)
+ t.strictSame(searchParamsObject, {
+ bool: 'true',
+ foo: '1',
+ bar: 'bar',
+ '%60~%3A%24%2C%2B%5B%5D%40%5E*()-': '%60~%3A%24%2C%2B%5B%5D%40%5E*()-',
+ multi: ['1', '2'],
+ nullVal: '',
+ undefinedVal: ''
+ })
+
+ res.statusCode = 200
+ res.end('hello')
+ })
+ t.teardown(server.close.bind(server))
+
+ const query = {
+ bool: true,
+ foo: 1,
+ bar: 'bar',
+ nullVal: null,
+ undefinedVal: undefined,
+ '`~:$,+[]@^*()-': '`~:$,+[]@^*()-',
+ multi: [1, 2]
+ }
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, {
+ keepAliveTimeout: 300e3
+ })
+ t.teardown(client.close.bind(client))
+
+ const signal = new EE()
+ client.request({
+ signal,
+ path: '/',
+ method: 'GET',
+ query
+ }, (err, data) => {
+ t.error(err)
+ const { statusCode } = data
+ t.equal(statusCode, 200)
+ })
+ t.equal(signal.listenerCount('abort'), 1)
+ })
+})
+
+test('basic get with query params fails if url includes hashmark', (t) => {
+ t.plan(1)
+
+ const server = createServer((req, res) => {
+ t.fail()
+ })
+ t.teardown(server.close.bind(server))
+
+ const query = {
+ foo: 1,
+ bar: 'bar',
+ multi: [1, 2]
+ }
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, {
+ keepAliveTimeout: 300e3
+ })
+ t.teardown(client.close.bind(client))
+
+ const signal = new EE()
+ client.request({
+ signal,
+ path: '/#',
+ method: 'GET',
+ query
+ }, (err, data) => {
+ t.equal(err.message, 'Query params cannot be passed when url already contains "?" or "#".')
+ })
+ })
+})
+
+test('basic get with empty query params', (t) => {
+ t.plan(4)
+
+ const server = createServer((req, res) => {
+ const searchParamsObject = buildParams(req.url)
+ t.strictSame(searchParamsObject, {})
+
+ res.statusCode = 200
+ res.end('hello')
+ })
+ t.teardown(server.close.bind(server))
+
+ const query = {}
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, {
+ keepAliveTimeout: 300e3
+ })
+ t.teardown(client.close.bind(client))
+
+ const signal = new EE()
+ client.request({
+ signal,
+ path: '/',
+ method: 'GET',
+ query
+ }, (err, data) => {
+ t.error(err)
+ const { statusCode } = data
+ t.equal(statusCode, 200)
+ })
+ t.equal(signal.listenerCount('abort'), 1)
+ })
+})
+
+test('basic get with query params partially in path', (t) => {
+ t.plan(1)
+
+ const server = createServer((req, res) => {
+ t.fail()
+ })
+ t.teardown(server.close.bind(server))
+
+ const query = {
+ foo: 1
+ }
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, {
+ keepAliveTimeout: 300e3
+ })
+ t.teardown(client.close.bind(client))
+
+ const signal = new EE()
+ client.request({
+ signal,
+ path: '/?bar=2',
+ method: 'GET',
+ query
+ }, (err, data) => {
+ t.equal(err.message, 'Query params cannot be passed when url already contains "?" or "#".')
+ })
+ })
+})
+
+test('basic get returns 400 when configured to throw on errors (callback)', (t) => {
+ t.plan(7)
+
+ const server = createServer((req, res) => {
+ res.statusCode = 400
+ res.end('hello')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, {
+ keepAliveTimeout: 300e3
+ })
+ t.teardown(client.close.bind(client))
+
+ const signal = new EE()
+ client.request({
+ signal,
+ path: '/',
+ method: 'GET',
+ throwOnError: true
+ }, (err) => {
+ t.equal(err.message, 'Response status code 400: Bad Request')
+ t.equal(err.status, 400)
+ t.equal(err.statusCode, 400)
+ t.equal(err.headers.connection, 'keep-alive')
+ t.equal(err.headers['content-length'], '5')
+ t.same(err.body, null)
+ })
+ t.equal(signal.listenerCount('abort'), 1)
+ })
+})
+
+test('basic get returns 400 when configured to throw on errors and correctly handles malformed json (callback)', (t) => {
+ t.plan(6)
+
+ const server = createServer((req, res) => {
+ res.writeHead(400, 'Invalid params', { 'content-type': 'application/json' })
+ res.end('Invalid params')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, {
+ keepAliveTimeout: 300e3
+ })
+ t.teardown(client.close.bind(client))
+
+ const signal = new EE()
+ client.request({
+ signal,
+ path: '/',
+ method: 'GET',
+ throwOnError: true
+ }, (err) => {
+ t.equal(err.message, 'Response status code 400: Invalid params')
+ t.equal(err.status, 400)
+ t.equal(err.statusCode, 400)
+ t.equal(err.headers.connection, 'keep-alive')
+ t.same(err.body, null)
+ })
+ t.equal(signal.listenerCount('abort'), 1)
+ })
+})
+
+test('basic get returns 400 when configured to throw on errors (promise)', (t) => {
+ t.plan(6)
+
+ const server = createServer((req, res) => {
+ res.writeHead(400, 'Invalid params', { 'content-type': 'text/plain' })
+ res.end('Invalid params')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, async () => {
+ const client = new Client(`http://localhost:${server.address().port}`, {
+ keepAliveTimeout: 300e3
+ })
+ t.teardown(client.close.bind(client))
+
+ const signal = new EE()
+ try {
+ await client.request({
+ signal,
+ path: '/',
+ method: 'GET',
+ throwOnError: true
+ })
+ t.fail('Should throw an error')
+ } catch (err) {
+ t.equal(err.message, 'Response status code 400: Invalid params')
+ t.equal(err.status, 400)
+ t.equal(err.statusCode, 400)
+ t.equal(err.body, 'Invalid params')
+ t.equal(err.headers.connection, 'keep-alive')
+ t.equal(err.headers['content-type'], 'text/plain')
+ }
+ })
+})
+
+test('basic get returns error body when configured to throw on errors', (t) => {
+ t.plan(6)
+
+ const server = createServer((req, res) => {
+ const body = { msg: 'Error', details: { code: 94 } }
+ const bodyAsString = JSON.stringify(body)
+ res.writeHead(400, 'Invalid params', {
+ 'Content-Type': 'application/json'
+ })
+ res.end(bodyAsString)
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, async () => {
+ const client = new Client(`http://localhost:${server.address().port}`, {
+ keepAliveTimeout: 300e3
+ })
+ t.teardown(client.close.bind(client))
+
+ const signal = new EE()
+ try {
+ await client.request({
+ signal,
+ path: '/',
+ method: 'GET',
+ throwOnError: true
+ })
+ t.fail('Should throw an error')
+ } catch (err) {
+ t.equal(err.message, 'Response status code 400: Invalid params')
+ t.equal(err.status, 400)
+ t.equal(err.statusCode, 400)
+ t.equal(err.headers.connection, 'keep-alive')
+ t.equal(err.headers['content-type'], 'application/json')
+ t.same(err.body, { msg: 'Error', details: { code: 94 } })
+ }
+ })
+})
+
+test('basic head', (t) => {
+ t.plan(14)
+
+ const server = createServer((req, res) => {
+ t.equal('/123', req.url)
+ t.equal('HEAD', req.method)
+ t.equal(`localhost:${server.address().port}`, req.headers.host)
+ res.setHeader('content-type', 'text/plain')
+ res.end('hello')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.close.bind(client))
+
+ client.request({ path: '/123', method: 'HEAD' }, (err, { statusCode, headers, body }) => {
+ t.error(err)
+ t.equal(statusCode, 200)
+ t.equal(headers['content-type'], 'text/plain')
+ body
+ .resume()
+ .on('end', () => {
+ t.pass()
+ })
+ })
+
+ client.request({ path: '/123', method: 'HEAD' }, (err, { statusCode, headers, body }) => {
+ t.error(err)
+ t.equal(statusCode, 200)
+ t.equal(headers['content-type'], 'text/plain')
+ body
+ .resume()
+ .on('end', () => {
+ t.pass()
+ })
+ })
+ })
+})
+
+test('basic head (IPv6)', { skip: !hasIPv6 }, (t) => {
+ t.plan(14)
+
+ const server = createServer((req, res) => {
+ t.equal('/123', req.url)
+ t.equal('HEAD', req.method)
+ t.equal(`[::1]:${server.address().port}`, req.headers.host)
+ res.setHeader('content-type', 'text/plain')
+ res.end('hello')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, '::', () => {
+ const client = new Client(`http://[::1]:${server.address().port}`)
+ t.teardown(client.close.bind(client))
+
+ client.request({ path: '/123', method: 'HEAD' }, (err, { statusCode, headers, body }) => {
+ t.error(err)
+ t.equal(statusCode, 200)
+ t.equal(headers['content-type'], 'text/plain')
+ body
+ .resume()
+ .on('end', () => {
+ t.pass()
+ })
+ })
+
+ client.request({ path: '/123', method: 'HEAD' }, (err, { statusCode, headers, body }) => {
+ t.error(err)
+ t.equal(statusCode, 200)
+ t.equal(headers['content-type'], 'text/plain')
+ body
+ .resume()
+ .on('end', () => {
+ t.pass()
+ })
+ })
+ })
+})
+
+test('get with host header', (t) => {
+ t.plan(7)
+
+ const server = createServer((req, res) => {
+ t.equal('/', req.url)
+ t.equal('GET', req.method)
+ t.equal('example.com', req.headers.host)
+ res.setHeader('content-type', 'text/plain')
+ res.end('hello from ' + req.headers.host)
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.close.bind(client))
+
+ client.request({ path: '/', method: 'GET', headers: { host: 'example.com' } }, (err, { statusCode, headers, body }) => {
+ t.error(err)
+ t.equal(statusCode, 200)
+ t.equal(headers['content-type'], 'text/plain')
+ const bufs = []
+ body.on('data', (buf) => {
+ bufs.push(buf)
+ })
+ body.on('end', () => {
+ t.equal('hello from example.com', Buffer.concat(bufs).toString('utf8'))
+ })
+ })
+ })
+})
+
+test('get with host header (IPv6)', { skip: !hasIPv6 }, (t) => {
+ t.plan(7)
+
+ const server = createServer((req, res) => {
+ t.equal('/', req.url)
+ t.equal('GET', req.method)
+ t.equal('[::1]', req.headers.host)
+ res.setHeader('content-type', 'text/plain')
+ res.end('hello from ' + req.headers.host)
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, '::', () => {
+ const client = new Client(`http://[::1]:${server.address().port}`)
+ t.teardown(client.close.bind(client))
+
+ client.request({ path: '/', method: 'GET', headers: { host: '[::1]' } }, (err, { statusCode, headers, body }) => {
+ t.error(err)
+ t.equal(statusCode, 200)
+ t.equal(headers['content-type'], 'text/plain')
+ const bufs = []
+ body.on('data', (buf) => {
+ bufs.push(buf)
+ })
+ body.on('end', () => {
+ t.equal('hello from [::1]', Buffer.concat(bufs).toString('utf8'))
+ })
+ })
+ })
+})
+
+test('head with host header', (t) => {
+ t.plan(7)
+
+ const server = createServer((req, res) => {
+ t.equal('/', req.url)
+ t.equal('HEAD', req.method)
+ t.equal('example.com', req.headers.host)
+ res.setHeader('content-type', 'text/plain')
+ res.end('hello from ' + req.headers.host)
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.close.bind(client))
+
+ client.request({ path: '/', method: 'HEAD', headers: { host: 'example.com' } }, (err, { statusCode, headers, body }) => {
+ t.error(err)
+ t.equal(statusCode, 200)
+ t.equal(headers['content-type'], 'text/plain')
+ body
+ .resume()
+ .on('end', () => {
+ t.pass()
+ })
+ })
+ })
+})
+
+function postServer (t, expected) {
+ return function (req, res) {
+ t.equal(req.url, '/')
+ t.equal(req.method, 'POST')
+ t.notSame(req.headers['content-length'], null)
+
+ req.setEncoding('utf8')
+ let data = ''
+
+ req.on('data', function (d) { data += d })
+
+ req.on('end', () => {
+ t.equal(data, expected)
+ res.end('hello')
+ })
+ }
+}
+
+test('basic POST with string', (t) => {
+ t.plan(7)
+
+ const expected = readFileSync(__filename, 'utf8')
+
+ const server = createServer(postServer(t, expected))
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.close.bind(client))
+
+ client.request({ path: '/', method: 'POST', body: expected }, (err, data) => {
+ t.error(err)
+ t.equal(data.statusCode, 200)
+ const bufs = []
+ data.body
+ .on('data', (buf) => {
+ bufs.push(buf)
+ })
+ .on('end', () => {
+ t.equal('hello', Buffer.concat(bufs).toString('utf8'))
+ })
+ })
+ })
+})
+
+test('basic POST with empty string', (t) => {
+ t.plan(7)
+
+ const server = createServer(postServer(t, ''))
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.close.bind(client))
+
+ client.request({ path: '/', method: 'POST', body: '' }, (err, { statusCode, headers, body }) => {
+ t.error(err)
+ t.equal(statusCode, 200)
+ const bufs = []
+ body.on('data', (buf) => {
+ bufs.push(buf)
+ })
+ body.on('end', () => {
+ t.equal('hello', Buffer.concat(bufs).toString('utf8'))
+ })
+ })
+ })
+})
+
+test('basic POST with string and content-length', (t) => {
+ t.plan(7)
+
+ const expected = readFileSync(__filename, 'utf8')
+
+ const server = createServer(postServer(t, expected))
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.close.bind(client))
+
+ client.request({
+ path: '/',
+ method: 'POST',
+ headers: {
+ 'content-length': Buffer.byteLength(expected)
+ },
+ body: expected
+ }, (err, { statusCode, headers, body }) => {
+ t.error(err)
+ t.equal(statusCode, 200)
+ const bufs = []
+ body.on('data', (buf) => {
+ bufs.push(buf)
+ })
+ body.on('end', () => {
+ t.equal('hello', Buffer.concat(bufs).toString('utf8'))
+ })
+ })
+ })
+})
+
+test('basic POST with Buffer', (t) => {
+ t.plan(7)
+
+ const expected = readFileSync(__filename)
+
+ const server = createServer(postServer(t, expected.toString()))
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.close.bind(client))
+
+ client.request({ path: '/', method: 'POST', body: expected }, (err, { statusCode, headers, body }) => {
+ t.error(err)
+ t.equal(statusCode, 200)
+ const bufs = []
+ body.on('data', (buf) => {
+ bufs.push(buf)
+ })
+ body.on('end', () => {
+ t.equal('hello', Buffer.concat(bufs).toString('utf8'))
+ })
+ })
+ })
+})
+
+test('basic POST with stream', (t) => {
+ t.plan(7)
+
+ const expected = readFileSync(__filename, 'utf8')
+
+ const server = createServer(postServer(t, expected))
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.close.bind(client))
+
+ client.request({
+ path: '/',
+ method: 'POST',
+ headers: {
+ 'content-length': Buffer.byteLength(expected)
+ },
+ headersTimeout: 0,
+ body: createReadStream(__filename)
+ }, (err, { statusCode, headers, body }) => {
+ t.error(err)
+ t.equal(statusCode, 200)
+ const bufs = []
+ body.on('data', (buf) => {
+ bufs.push(buf)
+ })
+ body.on('end', () => {
+ t.equal('hello', Buffer.concat(bufs).toString('utf8'))
+ })
+ })
+ })
+})
+
+test('basic POST with paused stream', (t) => {
+ t.plan(7)
+
+ const expected = readFileSync(__filename, 'utf8')
+
+ const server = createServer(postServer(t, expected))
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.close.bind(client))
+
+ const stream = createReadStream(__filename)
+ stream.pause()
+ client.request({
+ path: '/',
+ method: 'POST',
+ headers: {
+ 'content-length': Buffer.byteLength(expected)
+ },
+ headersTimeout: 0,
+ body: stream
+ }, (err, { statusCode, headers, body }) => {
+ t.error(err)
+ t.equal(statusCode, 200)
+ const bufs = []
+ body.on('data', (buf) => {
+ bufs.push(buf)
+ })
+ body.on('end', () => {
+ t.equal('hello', Buffer.concat(bufs).toString('utf8'))
+ })
+ })
+ })
+})
+
+test('basic POST with custom stream', (t) => {
+ t.plan(4)
+
+ const server = createServer((req, res) => {
+ req.resume().on('end', () => {
+ res.end('hello')
+ })
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.close.bind(client))
+
+ const body = new EE()
+ body.pipe = () => {}
+ client.request({
+ path: '/',
+ method: 'POST',
+ headersTimeout: 0,
+ body
+ }, (err, data) => {
+ t.error(err)
+ t.equal(data.statusCode, 200)
+ const bufs = []
+ data.body.on('data', (buf) => {
+ bufs.push(buf)
+ })
+ data.body.on('end', () => {
+ t.equal('hello', Buffer.concat(bufs).toString('utf8'))
+ })
+ })
+ t.strictSame(client[kBusy], true)
+
+ body.on('close', () => {
+ body.emit('end')
+ })
+
+ client.on('connect', () => {
+ setImmediate(() => {
+ body.emit('data', '')
+ while (!client[kSocket]._writableState.needDrain) {
+ body.emit('data', Buffer.alloc(4096))
+ }
+ client[kSocket].on('drain', () => {
+ body.emit('data', Buffer.alloc(4096))
+ body.emit('close')
+ })
+ })
+ })
+ })
+})
+
+test('basic POST with iterator', (t) => {
+ t.plan(3)
+
+ const expected = 'hello'
+
+ const server = createServer((req, res) => {
+ req.resume().on('end', () => {
+ res.end(expected)
+ })
+ })
+ t.teardown(server.close.bind(server))
+
+ const iterable = {
+ [Symbol.iterator]: function * () {
+ for (let i = 0; i < expected.length - 1; i++) {
+ yield expected[i]
+ }
+ return expected[expected.length - 1]
+ }
+ }
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.close.bind(client))
+
+ client.request({
+ path: '/',
+ method: 'POST',
+ requestTimeout: 0,
+ body: iterable
+ }, (err, { statusCode, body }) => {
+ t.error(err)
+ t.equal(statusCode, 200)
+ const bufs = []
+ body.on('data', (buf) => {
+ bufs.push(buf)
+ })
+ body.on('end', () => {
+ t.equal('hello', Buffer.concat(bufs).toString('utf8'))
+ })
+ })
+ })
+})
+
+test('basic POST with iterator with invalid data', (t) => {
+ t.plan(1)
+
+ const server = createServer(() => {})
+ t.teardown(server.close.bind(server))
+
+ const iterable = {
+ [Symbol.iterator]: function * () {
+ yield 0
+ }
+ }
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.close.bind(client))
+
+ client.request({
+ path: '/',
+ method: 'POST',
+ requestTimeout: 0,
+ body: iterable
+ }, err => {
+ t.ok(err instanceof TypeError)
+ })
+ })
+})
+
+test('basic POST with async iterator', (t) => {
+ t.plan(7)
+
+ const expected = readFileSync(__filename, 'utf8')
+
+ const server = createServer(postServer(t, expected))
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.close.bind(client))
+
+ client.request({
+ path: '/',
+ method: 'POST',
+ headers: {
+ 'content-length': Buffer.byteLength(expected)
+ },
+ headersTimeout: 0,
+ body: wrapWithAsyncIterable(createReadStream(__filename))
+ }, (err, { statusCode, headers, body }) => {
+ t.error(err)
+ t.equal(statusCode, 200)
+ const bufs = []
+ body.on('data', (buf) => {
+ bufs.push(buf)
+ })
+ body.on('end', () => {
+ t.equal('hello', Buffer.concat(bufs).toString('utf8'))
+ })
+ })
+ })
+})
+
+test('basic POST with transfer encoding: chunked', (t) => {
+ t.plan(8)
+
+ let body
+ const server = createServer(function (req, res) {
+ t.equal(req.url, '/')
+ t.equal(req.method, 'POST')
+ t.same(req.headers['content-length'], null)
+ t.equal(req.headers['transfer-encoding'], 'chunked')
+
+ body.push(null)
+
+ req.setEncoding('utf8')
+ let data = ''
+
+ req.on('data', function (d) { data += d })
+
+ req.on('end', () => {
+ t.equal(data, 'asd')
+ res.end('hello')
+ })
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.close.bind(client))
+
+ body = new Readable({
+ read () { }
+ })
+ body.push('asd')
+ client.request({
+ path: '/',
+ method: 'POST',
+ // no content-length header
+ body
+ }, (err, { statusCode, headers, body }) => {
+ t.error(err)
+ t.equal(statusCode, 200)
+ const bufs = []
+ body.on('data', (buf) => {
+ bufs.push(buf)
+ })
+ body.on('end', () => {
+ t.equal('hello', Buffer.concat(bufs).toString('utf8'))
+ })
+ })
+ })
+})
+
+test('basic POST with empty stream', (t) => {
+ t.plan(4)
+
+ const server = createServer(function (req, res) {
+ t.same(req.headers['content-length'], 0)
+ req.pipe(res)
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.close.bind(client))
+
+ const body = new Readable({
+ autoDestroy: false,
+ read () {
+ },
+ destroy (err, callback) {
+ callback(!this._readableState.endEmitted ? new Error('asd') : err)
+ }
+ }).on('end', () => {
+ process.nextTick(() => {
+ t.equal(body.destroyed, true)
+ })
+ })
+ body.push(null)
+ client.request({
+ path: '/',
+ method: 'POST',
+ body
+ }, (err, { statusCode, headers, body }) => {
+ t.error(err)
+ body
+ .on('data', () => {
+ t.fail()
+ })
+ .on('end', () => {
+ t.pass()
+ })
+ })
+ })
+})
+
+test('10 times GET', (t) => {
+ const num = 10
+ t.plan(3 * 10)
+
+ const server = createServer((req, res) => {
+ res.end(req.url)
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.close.bind(client))
+
+ for (let i = 0; i < num; i++) {
+ makeRequest(i)
+ }
+
+ function makeRequest (i) {
+ client.request({ path: '/' + i, method: 'GET' }, (err, { statusCode, headers, body }) => {
+ t.error(err)
+ t.equal(statusCode, 200)
+ const bufs = []
+ body.on('data', (buf) => {
+ bufs.push(buf)
+ })
+ body.on('end', () => {
+ t.equal('/' + i, Buffer.concat(bufs).toString('utf8'))
+ })
+ })
+ }
+ })
+})
+
+test('10 times HEAD', (t) => {
+ const num = 10
+ t.plan(3 * 10)
+
+ const server = createServer((req, res) => {
+ res.end(req.url)
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.close.bind(client))
+
+ for (let i = 0; i < num; i++) {
+ makeRequest(i)
+ }
+
+ function makeRequest (i) {
+ client.request({ path: '/' + i, method: 'HEAD' }, (err, { statusCode, headers, body }) => {
+ t.error(err)
+ t.equal(statusCode, 200)
+ body
+ .resume()
+ .on('end', () => {
+ t.pass()
+ })
+ })
+ }
+ })
+})
+
+test('Set-Cookie', (t) => {
+ t.plan(4)
+
+ const server = createServer((req, res) => {
+ res.setHeader('content-type', 'text/plain')
+ res.setHeader('Set-Cookie', ['a cookie', 'another cookie', 'more cookies'])
+ res.end('hello')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.close.bind(client))
+
+ client.request({ path: '/', method: 'GET' }, (err, { statusCode, headers, body }) => {
+ t.error(err)
+ t.equal(statusCode, 200)
+ t.strictSame(headers['set-cookie'], ['a cookie', 'another cookie', 'more cookies'])
+ const bufs = []
+ body.on('data', (buf) => {
+ bufs.push(buf)
+ })
+ body.on('end', () => {
+ t.equal('hello', Buffer.concat(bufs).toString('utf8'))
+ })
+ })
+ })
+})
+
+test('ignore request header mutations', (t) => {
+ t.plan(2)
+
+ const server = createServer((req, res) => {
+ t.equal(req.headers.test, 'test')
+ res.end()
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.close.bind(client))
+
+ const headers = { test: 'test' }
+ client.request({
+ path: '/',
+ method: 'GET',
+ headers
+ }, (err, { body }) => {
+ t.error(err)
+ body.resume()
+ })
+ headers.test = 'asd'
+ })
+})
+
+test('url-like url', (t) => {
+ t.plan(1)
+
+ const server = createServer((req, res) => {
+ res.end()
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client({
+ hostname: 'localhost',
+ port: server.address().port,
+ protocol: 'http:'
+ })
+ t.teardown(client.close.bind(client))
+
+ client.request({ path: '/', method: 'GET' }, (err, data) => {
+ t.error(err)
+ data.body.resume()
+ })
+ })
+})
+
+test('an absolute url as path', (t) => {
+ t.plan(2)
+
+ const path = 'http://example.com'
+
+ const server = createServer((req, res) => {
+ t.equal(req.url, path)
+ res.end()
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client({
+ hostname: 'localhost',
+ port: server.address().port,
+ protocol: 'http:'
+ })
+ t.teardown(client.close.bind(client))
+
+ client.request({ path, method: 'GET' }, (err, data) => {
+ t.error(err)
+ data.body.resume()
+ })
+ })
+})
+
+test('multiple destroy callback', (t) => {
+ t.plan(4)
+
+ const server = createServer((req, res) => {
+ res.end()
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client({
+ hostname: 'localhost',
+ port: server.address().port,
+ protocol: 'http:'
+ })
+ t.teardown(client.destroy.bind(client))
+
+ client.request({ path: '/', method: 'GET' }, (err, data) => {
+ t.error(err)
+ data.body
+ .resume()
+ .on('error', () => {
+ t.pass()
+ })
+ client.destroy(new Error(), (err) => {
+ t.error(err)
+ })
+ client.destroy(new Error(), (err) => {
+ t.error(err)
+ })
+ })
+ })
+})
+
+test('only one streaming req at a time', (t) => {
+ t.plan(7)
+
+ const server = createServer((req, res) => {
+ req.pipe(res)
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, {
+ pipelining: 4
+ })
+ t.teardown(client.destroy.bind(client))
+
+ client.request({
+ path: '/',
+ method: 'GET'
+ }, (err, data) => {
+ t.error(err)
+ data.body.resume()
+
+ client.request({
+ path: '/',
+ method: 'GET'
+ }, (err, data) => {
+ t.error(err)
+ data.body.resume()
+ })
+
+ client.request({
+ path: '/',
+ method: 'PUT',
+ idempotent: true,
+ body: new Readable({
+ read () {
+ setImmediate(() => {
+ t.equal(client[kBusy], true)
+ this.push(null)
+ })
+ }
+ }).on('resume', () => {
+ t.equal(client[kSize], 1)
+ })
+ }, (err, data) => {
+ t.error(err)
+ data.body
+ .resume()
+ .on('end', () => {
+ t.pass()
+ })
+ })
+ t.equal(client[kBusy], true)
+ })
+ })
+})
+
+test('only one async iterating req at a time', (t) => {
+ t.plan(6)
+
+ const server = createServer((req, res) => {
+ req.pipe(res)
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, {
+ pipelining: 4
+ })
+ t.teardown(client.destroy.bind(client))
+
+ client.request({
+ path: '/',
+ method: 'GET'
+ }, (err, data) => {
+ t.error(err)
+ data.body.resume()
+
+ client.request({
+ path: '/',
+ method: 'GET'
+ }, (err, data) => {
+ t.error(err)
+ data.body.resume()
+ })
+ const body = wrapWithAsyncIterable(new Readable({
+ read () {
+ setImmediate(() => {
+ t.equal(client[kBusy], true)
+ this.push(null)
+ })
+ }
+ }))
+ client.request({
+ path: '/',
+ method: 'PUT',
+ idempotent: true,
+ body
+ }, (err, data) => {
+ t.error(err)
+ data.body
+ .resume()
+ .on('end', () => {
+ t.pass()
+ })
+ })
+ t.equal(client[kBusy], true)
+ })
+ })
+})
+
+test('300 requests succeed', (t) => {
+ t.plan(300 * 3)
+
+ const server = createServer((req, res) => {
+ res.end('asd')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ for (let n = 0; n < 300; ++n) {
+ client.request({
+ path: '/',
+ method: 'GET'
+ }, (err, data) => {
+ t.error(err)
+ data.body.on('data', (chunk) => {
+ t.equal(chunk.toString(), 'asd')
+ }).on('end', () => {
+ t.pass()
+ })
+ })
+ }
+ })
+})
+
+test('request args validation', (t) => {
+ t.plan(2)
+
+ const client = new Client('http://localhost:5000')
+
+ client.request(null, (err) => {
+ t.type(err, errors.InvalidArgumentError)
+ })
+
+ try {
+ client.request(null, 'asd')
+ } catch (err) {
+ t.type(err, errors.InvalidArgumentError)
+ }
+})
+
+test('request args validation promise', (t) => {
+ t.plan(1)
+
+ const client = new Client('http://localhost:5000')
+
+ client.request(null).catch((err) => {
+ t.type(err, errors.InvalidArgumentError)
+ })
+})
+
+test('increase pipelining', (t) => {
+ t.plan(4)
+
+ const server = createServer((req, res) => {
+ req.resume()
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ client.request({
+ path: '/',
+ method: 'GET'
+ }, () => {
+ if (!client.destroyed) {
+ t.fail()
+ }
+ })
+
+ client.request({
+ path: '/',
+ method: 'GET'
+ }, () => {
+ if (!client.destroyed) {
+ t.fail()
+ }
+ })
+
+ t.equal(client[kRunning], 0)
+ client.on('connect', () => {
+ t.equal(client[kRunning], 0)
+ process.nextTick(() => {
+ t.equal(client[kRunning], 1)
+ client.pipelining = 3
+ t.equal(client[kRunning], 2)
+ })
+ })
+ })
+})
+
+test('destroy in push', (t) => {
+ t.plan(4)
+
+ let _res
+ const server = createServer((req, res) => {
+ res.write('asd')
+ _res = res
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.close.bind(client))
+
+ client.request({ path: '/', method: 'GET' }, (err, { body }) => {
+ t.error(err)
+ body.once('data', () => {
+ _res.write('asd')
+ body.on('data', (buf) => {
+ body.destroy()
+ _res.end()
+ }).on('error', (err) => {
+ t.ok(err)
+ })
+ })
+ })
+
+ client.request({ path: '/', method: 'GET' }, (err, { body }) => {
+ t.error(err)
+ let buf = ''
+ body.on('data', (chunk) => {
+ buf = chunk.toString()
+ _res.end()
+ }).on('end', () => {
+ t.equal('asd', buf)
+ })
+ })
+ })
+})
+
+test('non recoverable socket error fails pending request', (t) => {
+ t.plan(2)
+
+ const server = createServer((req, res) => {
+ res.end()
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.close.bind(client))
+
+ client.request({ path: '/', method: 'GET' }, (err, data) => {
+ t.equal(err.message, 'kaboom')
+ })
+ client.request({ path: '/', method: 'GET' }, (err, data) => {
+ t.equal(err.message, 'kaboom')
+ })
+ client.on('connect', () => {
+ client[kSocket].destroy(new Error('kaboom'))
+ })
+ })
+})
+
+test('POST empty with error', (t) => {
+ t.plan(1)
+
+ const server = createServer((req, res) => {
+ req.pipe(res)
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.close.bind(client))
+
+ const body = new Readable({
+ read () {
+ }
+ })
+ body.push(null)
+ client.on('connect', () => {
+ process.nextTick(() => {
+ body.emit('error', new Error('asd'))
+ })
+ })
+
+ client.request({ path: '/', method: 'POST', body }, (err, data) => {
+ t.equal(err.message, 'asd')
+ })
+ })
+})
+
+test('busy', (t) => {
+ t.plan(2)
+
+ const server = createServer((req, res) => {
+ req.pipe(res)
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, {
+ pipelining: 1
+ })
+ t.teardown(client.close.bind(client))
+
+ client[kConnect](() => {
+ client.request({
+ path: '/',
+ method: 'GET'
+ }, (err) => {
+ t.error(err)
+ })
+ t.equal(client[kBusy], true)
+ })
+ })
+})
+
+test('connected', (t) => {
+ t.plan(7)
+
+ const server = createServer((req, res) => {
+ // needed so that disconnect is emitted
+ res.setHeader('connection', 'close')
+ req.pipe(res)
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const url = new URL(`http://localhost:${server.address().port}`)
+ const client = new Client(url, {
+ pipelining: 1
+ })
+ t.teardown(client.close.bind(client))
+
+ client.on('connect', (origin, [self]) => {
+ t.equal(origin, url)
+ t.equal(client, self)
+ })
+ client.on('disconnect', (origin, [self]) => {
+ t.equal(origin, url)
+ t.equal(client, self)
+ })
+
+ t.equal(client[kConnected], false)
+ client[kConnect](() => {
+ client.request({
+ path: '/',
+ method: 'GET'
+ }, (err) => {
+ t.error(err)
+ })
+ t.equal(client[kConnected], true)
+ })
+ })
+})
+
+test('emit disconnect after destroy', t => {
+ t.plan(4)
+
+ const server = createServer((req, res) => {
+ req.pipe(res)
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const url = new URL(`http://localhost:${server.address().port}`)
+ const client = new Client(url)
+
+ t.equal(client[kConnected], false)
+ client[kConnect](() => {
+ t.equal(client[kConnected], true)
+ let disconnected = false
+ client.on('disconnect', () => {
+ disconnected = true
+ t.pass()
+ })
+ client.destroy(() => {
+ t.equal(disconnected, true)
+ })
+ })
+ })
+})
+
+test('end response before request', t => {
+ t.plan(2)
+
+ const server = createServer((req, res) => {
+ res.end()
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, async () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ const readable = new Readable({
+ read () {
+ this.push('asd')
+ }
+ })
+ const { body } = await client.request({
+ method: 'GET',
+ path: '/',
+ body: readable
+ })
+ body
+ .on('error', () => {
+ t.fail()
+ })
+ .on('end', () => {
+ t.pass()
+ })
+ .resume()
+ client.on('disconnect', (url, targets, err) => {
+ t.equal(err.code, 'UND_ERR_INFO')
+ })
+ })
+})
+
+test('parser pause with no body timeout', (t) => {
+ t.plan(2)
+ const server = createServer((req, res) => {
+ let counter = 0
+ const t = setInterval(() => {
+ counter++
+ const payload = Buffer.alloc(counter * 4096).fill(0)
+ if (counter === 3) {
+ clearInterval(t)
+ res.end(payload)
+ } else {
+ res.write(payload)
+ }
+ }, 20)
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, {
+ bodyTimeout: 0
+ })
+ t.teardown(client.close.bind(client))
+
+ client.request({ path: '/', method: 'GET' }, (err, { statusCode, body }) => {
+ t.error(err)
+ t.equal(statusCode, 200)
+ body.resume()
+ })
+ })
+})
+
+test('TypedArray and DataView body', (t) => {
+ t.plan(3)
+ const server = createServer((req, res) => {
+ t.equal(req.headers['content-length'], '8')
+ res.end()
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, {
+ bodyTimeout: 0
+ })
+ t.teardown(client.close.bind(client))
+
+ const body = Uint8Array.from(Buffer.alloc(8))
+ client.request({ path: '/', method: 'POST', body }, (err, { statusCode, body }) => {
+ t.error(err)
+ t.equal(statusCode, 200)
+ body.resume()
+ })
+ })
+})
+
+test('async iterator empty chunk continues', (t) => {
+ t.plan(5)
+ const serverChunks = ['hello', 'world']
+ const server = createServer((req, res) => {
+ let str = ''
+ let i = 0
+ req.on('data', (chunk) => {
+ const content = chunk.toString()
+ t.equal(serverChunks[i++], content)
+ str += content
+ }).on('end', () => {
+ t.equal(str, serverChunks.join(''))
+ res.end()
+ })
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, {
+ bodyTimeout: 0
+ })
+ t.teardown(client.close.bind(client))
+
+ const body = (async function * () {
+ yield serverChunks[0]
+ yield ''
+ yield serverChunks[1]
+ })()
+ client.request({ path: '/', method: 'POST', body }, (err, { statusCode, body }) => {
+ t.error(err)
+ t.equal(statusCode, 200)
+ body.resume()
+ })
+ })
+})
+
+test('async iterator error from server destroys early', (t) => {
+ t.plan(3)
+ const server = createServer((req, res) => {
+ req.on('data', (chunk) => {
+ res.destroy()
+ })
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, {
+ bodyTimeout: 0
+ })
+ t.teardown(client.close.bind(client))
+ let gotDestroyed
+ const body = (async function * () {
+ try {
+ const promise = new Promise(resolve => {
+ gotDestroyed = resolve
+ })
+ yield 'hello'
+ await promise
+ yield 'inner-value'
+ t.fail('should not get here, iterator should be destroyed')
+ } finally {
+ t.ok(true)
+ }
+ })()
+ client.request({ path: '/', method: 'POST', body }, (err, { statusCode, body }) => {
+ t.ok(err)
+ t.equal(statusCode, undefined)
+ gotDestroyed()
+ })
+ })
+})
+
+test('regular iterator error from server closes early', (t) => {
+ t.plan(3)
+ const server = createServer((req, res) => {
+ req.on('data', () => {
+ process.nextTick(() => {
+ res.destroy()
+ })
+ })
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, {
+ bodyTimeout: 0
+ })
+ t.teardown(client.close.bind(client))
+ let gotDestroyed = false
+ const body = (function * () {
+ try {
+ yield 'start'
+ while (!gotDestroyed) {
+ yield 'zzz'
+ // for eslint
+ gotDestroyed = gotDestroyed || false
+ }
+ yield 'zzz'
+ t.fail('should not get here, iterator should be destroyed')
+ yield 'zzz'
+ } finally {
+ t.ok(true)
+ }
+ })()
+ client.request({ path: '/', method: 'POST', body }, (err, { statusCode, body }) => {
+ t.ok(err)
+ t.equal(statusCode, undefined)
+ gotDestroyed = true
+ })
+ })
+})
+
+test('async iterator early return closes early', (t) => {
+ t.plan(3)
+ const server = createServer((req, res) => {
+ req.on('data', () => {
+ res.writeHead(200)
+ res.end()
+ })
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, {
+ bodyTimeout: 0
+ })
+ t.teardown(client.close.bind(client))
+ let gotDestroyed
+ const body = (async function * () {
+ try {
+ const promise = new Promise(resolve => {
+ gotDestroyed = resolve
+ })
+ yield 'hello'
+ await promise
+ yield 'inner-value'
+ t.fail('should not get here, iterator should be destroyed')
+ } finally {
+ t.ok(true)
+ }
+ })()
+ client.request({ path: '/', method: 'POST', body }, (err, { statusCode, body }) => {
+ t.error(err)
+ t.equal(statusCode, 200)
+ gotDestroyed()
+ })
+ })
+})
+
+test('async iterator yield unsupported TypedArray', (t) => {
+ t.plan(3)
+ const server = createServer((req, res) => {
+ req.on('end', () => {
+ res.writeHead(200)
+ res.end()
+ })
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, {
+ bodyTimeout: 0
+ })
+ t.teardown(client.close.bind(client))
+ const body = (async function * () {
+ try {
+ yield new Int32Array([1])
+ t.fail('should not get here, iterator should be destroyed')
+ } finally {
+ t.ok(true)
+ }
+ })()
+ client.request({ path: '/', method: 'POST', body }, (err) => {
+ t.ok(err)
+ t.equal(err.code, 'ERR_INVALID_ARG_TYPE')
+ })
+ })
+})
+
+test('async iterator yield object error', (t) => {
+ t.plan(3)
+ const server = createServer((req, res) => {
+ req.on('end', () => {
+ res.writeHead(200)
+ res.end()
+ })
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, {
+ bodyTimeout: 0
+ })
+ t.teardown(client.close.bind(client))
+ const body = (async function * () {
+ try {
+ yield {}
+ t.fail('should not get here, iterator should be destroyed')
+ } finally {
+ t.ok(true)
+ }
+ })()
+ client.request({ path: '/', method: 'POST', body }, (err) => {
+ t.ok(err)
+ t.equal(err.code, 'ERR_INVALID_ARG_TYPE')
+ })
+ })
+})
+
+function buildParams (path) {
+ const cleanPath = path.replace('/?', '').replace('/', '').split('&')
+ const builtParams = cleanPath.reduce((acc, entry) => {
+ const [key, value] = entry.split('=')
+ if (key.length === 0) {
+ return acc
+ }
+
+ if (acc[key]) {
+ if (Array.isArray(acc[key])) {
+ acc[key].push(value)
+ } else {
+ acc[key] = [acc[key], value]
+ }
+ } else {
+ acc[key] = value
+ }
+ return acc
+ }, {})
+
+ return builtParams
+}
+
+test('\\r\\n in Headers', (t) => {
+ t.plan(1)
+
+ const reqHeaders = {
+ bar: '\r\nbar'
+ }
+
+ const client = new Client('http://localhost:4242', {
+ keepAliveTimeout: 300e3
+ })
+ t.teardown(client.close.bind(client))
+
+ client.request({
+ path: '/',
+ method: 'GET',
+ headers: reqHeaders
+ }, (err) => {
+ t.equal(err.message, 'invalid bar header')
+ })
+})
+
+test('\\r in Headers', (t) => {
+ t.plan(1)
+
+ const reqHeaders = {
+ bar: '\rbar'
+ }
+
+ const client = new Client('http://localhost:4242', {
+ keepAliveTimeout: 300e3
+ })
+ t.teardown(client.close.bind(client))
+
+ client.request({
+ path: '/',
+ method: 'GET',
+ headers: reqHeaders
+ }, (err) => {
+ t.equal(err.message, 'invalid bar header')
+ })
+})
+
+test('\\n in Headers', (t) => {
+ t.plan(1)
+
+ const reqHeaders = {
+ bar: '\nbar'
+ }
+
+ const client = new Client('http://localhost:4242', {
+ keepAliveTimeout: 300e3
+ })
+ t.teardown(client.close.bind(client))
+
+ client.request({
+ path: '/',
+ method: 'GET',
+ headers: reqHeaders
+ }, (err) => {
+ t.equal(err.message, 'invalid bar header')
+ })
+})
+
+test('\\n in Headers', (t) => {
+ t.plan(1)
+
+ const reqHeaders = {
+ '\nbar': 'foo'
+ }
+
+ const client = new Client('http://localhost:4242', {
+ keepAliveTimeout: 300e3
+ })
+ t.teardown(client.close.bind(client))
+
+ client.request({
+ path: '/',
+ method: 'GET',
+ headers: reqHeaders
+ }, (err) => {
+ t.equal(err.message, 'invalid header key')
+ })
+})
+
+test('\\n in Path', (t) => {
+ t.plan(1)
+
+ const client = new Client('http://localhost:4242', {
+ keepAliveTimeout: 300e3
+ })
+ t.teardown(client.close.bind(client))
+
+ client.request({
+ path: '/\n',
+ method: 'GET'
+ }, (err) => {
+ t.equal(err.message, 'invalid request path')
+ })
+})
+
+test('\\n in Method', (t) => {
+ t.plan(1)
+
+ const client = new Client('http://localhost:4242', {
+ keepAliveTimeout: 300e3
+ })
+ t.teardown(client.close.bind(client))
+
+ client.request({
+ path: '/',
+ method: 'GET\n'
+ }, (err) => {
+ t.equal(err.message, 'invalid request method')
+ })
+})
diff --git a/test/close-and-destroy.js b/test/close-and-destroy.js
new file mode 100644
index 0000000..bd50ebb
--- /dev/null
+++ b/test/close-and-destroy.js
@@ -0,0 +1,344 @@
+'use strict'
+
+const { test } = require('tap')
+const { Client, errors } = require('..')
+const { createServer } = require('http')
+const { kSocket, kSize } = require('../lib/core/symbols')
+
+test('close waits for queued requests to finish', (t) => {
+ t.plan(16)
+
+ const server = createServer()
+
+ server.on('request', (req, res) => {
+ t.pass('request received')
+ res.end('hello')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ client.request({ path: '/', method: 'GET' }, function (err, data) {
+ onRequest(err, data)
+
+ client.request({ path: '/', method: 'GET' }, onRequest)
+ client.request({ path: '/', method: 'GET' }, onRequest)
+ client.request({ path: '/', method: 'GET' }, onRequest)
+
+ // needed because the next element in the queue will be called
+ // after the current function completes
+ process.nextTick(function () {
+ client.close()
+ })
+ })
+ })
+
+ function onRequest (err, { statusCode, headers, body }) {
+ t.error(err)
+ t.equal(statusCode, 200)
+ const bufs = []
+ body.on('data', (buf) => {
+ bufs.push(buf)
+ })
+ body.on('end', () => {
+ t.equal('hello', Buffer.concat(bufs).toString('utf8'))
+ })
+ }
+})
+
+test('destroy invoked all pending callbacks', (t) => {
+ t.plan(4)
+
+ const server = createServer()
+
+ server.on('request', (req, res) => {
+ res.write('hello')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, {
+ pipelining: 2
+ })
+ t.teardown(client.destroy.bind(client))
+
+ client.request({ path: '/', method: 'GET' }, (err, data) => {
+ t.error(err)
+ data.body.on('error', (err) => {
+ t.ok(err)
+ }).resume()
+ client.destroy()
+ })
+ client.request({ path: '/', method: 'GET' }, (err) => {
+ t.type(err, errors.ClientDestroyedError)
+ })
+ client.request({ path: '/', method: 'GET' }, (err) => {
+ t.type(err, errors.ClientDestroyedError)
+ })
+ })
+})
+
+test('destroy invoked all pending callbacks ticked', (t) => {
+ t.plan(4)
+
+ const server = createServer()
+
+ server.on('request', (req, res) => {
+ res.write('hello')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, {
+ pipelining: 2
+ })
+ t.teardown(client.destroy.bind(client))
+
+ let ticked = false
+ client.request({ path: '/', method: 'GET' }, (err) => {
+ t.equal(ticked, true)
+ t.type(err, errors.ClientDestroyedError)
+ })
+ client.request({ path: '/', method: 'GET' }, (err) => {
+ t.equal(ticked, true)
+ t.type(err, errors.ClientDestroyedError)
+ })
+ client.destroy()
+ ticked = true
+ })
+})
+
+test('close waits until socket is destroyed', (t) => {
+ t.plan(4)
+
+ const server = createServer((req, res) => {
+ res.end(req.url)
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ makeRequest()
+
+ client.once('connect', () => {
+ let done = false
+ client[kSocket].on('close', () => {
+ done = true
+ })
+ client.close((err) => {
+ t.error(err)
+ t.equal(client.closed, true)
+ t.equal(done, true)
+ })
+ })
+
+ function makeRequest () {
+ client.request({ path: '/', method: 'GET' }, (err, data) => {
+ t.error(err)
+ })
+ return client[kSize] <= client.pipelining
+ }
+ })
+})
+
+test('close should still reconnect', (t) => {
+ t.plan(6)
+
+ const server = createServer((req, res) => {
+ res.end(req.url)
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ t.ok(makeRequest())
+ t.ok(!makeRequest())
+
+ client.close((err) => {
+ t.error(err)
+ t.equal(client.closed, true)
+ })
+ client.once('connect', () => {
+ client[kSocket].destroy()
+ })
+
+ function makeRequest () {
+ client.request({ path: '/', method: 'GET' }, (err, data) => {
+ t.error(err)
+ data.body.resume()
+ })
+ return client[kSize] <= client.pipelining
+ }
+ })
+})
+
+test('close should call callback once finished', (t) => {
+ t.plan(6)
+
+ const server = createServer((req, res) => {
+ setImmediate(function () {
+ res.end(req.url)
+ })
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ t.ok(makeRequest())
+ t.ok(!makeRequest())
+
+ client.close((err) => {
+ t.error(err)
+ t.equal(client.closed, true)
+ })
+
+ function makeRequest () {
+ client.request({ path: '/', method: 'GET' }, (err, data) => {
+ t.error(err)
+ data.body.resume()
+ })
+ return client[kSize] <= client.pipelining
+ }
+ })
+})
+
+test('closed and destroyed errors', (t) => {
+ t.plan(4)
+
+ const client = new Client('http://localhost:4000')
+ t.teardown(client.destroy.bind(client))
+
+ client.request({ path: '/', method: 'GET' }, (err) => {
+ t.ok(err)
+ })
+ client.close((err) => {
+ t.error(err)
+ })
+ client.request({ path: '/', method: 'GET' }, (err) => {
+ t.type(err, errors.ClientClosedError)
+ client.destroy()
+ client.request({ path: '/', method: 'GET' }, (err) => {
+ t.type(err, errors.ClientDestroyedError)
+ })
+ })
+})
+
+test('close after and destroy should error', (t) => {
+ t.plan(2)
+
+ const client = new Client('http://localhost:4000')
+ t.teardown(client.destroy.bind(client))
+
+ client.destroy()
+ client.close((err) => {
+ t.type(err, errors.ClientDestroyedError)
+ })
+ client.close().catch((err) => {
+ t.type(err, errors.ClientDestroyedError)
+ })
+})
+
+test('close socket and reconnect after maxRequestsPerClient reached', (t) => {
+ t.plan(5)
+
+ const server = createServer((req, res) => {
+ res.end(req.url)
+ })
+
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, async () => {
+ let connections = 0
+ server.on('connection', () => {
+ connections++
+ })
+ const client = new Client(
+ `http://localhost:${server.address().port}`,
+ { maxRequestsPerClient: 2 }
+ )
+ t.teardown(client.destroy.bind(client))
+
+ await t.resolves(makeRequest())
+ await t.resolves(makeRequest())
+ await t.resolves(makeRequest())
+ await t.resolves(makeRequest())
+ t.equal(connections, 2)
+
+ function makeRequest () {
+ return client.request({ path: '/', method: 'GET' })
+ }
+ })
+})
+
+test('close socket and reconnect after maxRequestsPerClient reached (async)', (t) => {
+ t.plan(2)
+
+ const server = createServer((req, res) => {
+ res.end(req.url)
+ })
+
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, async () => {
+ let connections = 0
+ server.on('connection', () => {
+ connections++
+ })
+ const client = new Client(
+ `http://localhost:${server.address().port}`,
+ { maxRequestsPerClient: 2 }
+ )
+ t.teardown(client.destroy.bind(client))
+
+ await t.resolves(
+ Promise.all([
+ makeRequest(),
+ makeRequest(),
+ makeRequest(),
+ makeRequest()
+ ])
+ )
+ t.equal(connections, 2)
+
+ function makeRequest () {
+ return client.request({ path: '/', method: 'GET' })
+ }
+ })
+})
+
+test('should not close socket when no maxRequestsPerClient is provided', (t) => {
+ t.plan(5)
+
+ const server = createServer((req, res) => {
+ res.end(req.url)
+ })
+
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, async () => {
+ let connections = 0
+ server.on('connection', () => {
+ connections++
+ })
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ await t.resolves(makeRequest())
+ await t.resolves(makeRequest())
+ await t.resolves(makeRequest())
+ await t.resolves(makeRequest())
+ t.equal(connections, 1)
+
+ function makeRequest () {
+ return client.request({ path: '/', method: 'GET' })
+ }
+ })
+})
diff --git a/test/connect-abort.js b/test/connect-abort.js
new file mode 100644
index 0000000..6eb3624
--- /dev/null
+++ b/test/connect-abort.js
@@ -0,0 +1,28 @@
+'use strict'
+
+const { test } = require('tap')
+const { Client } = require('..')
+const { PassThrough } = require('stream')
+
+test(t => {
+ t.plan(2)
+
+ const client = new Client('http://localhost:1234', {
+ connect: (_, cb) => {
+ client.destroy()
+ cb(null, new PassThrough({
+ destroy (err, cb) {
+ t.same(err?.name, 'ClientDestroyedError')
+ cb(null)
+ }
+ }))
+ }
+ })
+
+ client.request({
+ path: '/',
+ method: 'GET'
+ }, (err, data) => {
+ t.same(err?.name, 'ClientDestroyedError')
+ })
+})
diff --git a/test/connect-errconnect.js b/test/connect-errconnect.js
new file mode 100644
index 0000000..defeda3
--- /dev/null
+++ b/test/connect-errconnect.js
@@ -0,0 +1,32 @@
+'use strict'
+
+const { test } = require('tap')
+const { Client } = require('..')
+const net = require('net')
+
+test('connect-connectionError', t => {
+ t.plan(2)
+
+ const client = new Client('http://localhost:9000')
+ t.teardown(client.close.bind(client))
+
+ client.once('connectionError', () => {
+ t.pass()
+ })
+
+ const _err = new Error('kaboom')
+ net.connect = function (options) {
+ const socket = new net.Socket(options)
+ setImmediate(() => {
+ socket.destroy(_err)
+ })
+ return socket
+ }
+
+ client.request({
+ path: '/',
+ method: 'GET'
+ }, (err) => {
+ t.equal(err, _err)
+ })
+})
diff --git a/test/connect-timeout.js b/test/connect-timeout.js
new file mode 100644
index 0000000..a736a54
--- /dev/null
+++ b/test/connect-timeout.js
@@ -0,0 +1,68 @@
+'use strict'
+
+const { test } = require('tap')
+const { Client, Pool, errors } = require('..')
+const net = require('net')
+const sleep = require('atomic-sleep')
+
+test('priotorise socket errors over timeouts', (t) => {
+ t.plan(1)
+ const connectTimeout = 1000
+ const client = new Pool('http://foobar.bar:1234', { connectTimeout: 2 })
+
+ client.request({ method: 'GET', path: '/foobar' })
+ .then(() => t.fail())
+ .catch((err) => {
+ t.equal(err.code, 'ENOTFOUND')
+ })
+
+ // block for 1s which is enough for the dns lookup to complete and TO to fire
+ sleep(connectTimeout)
+})
+
+// never connect
+net.connect = function (options) {
+ return new net.Socket(options)
+}
+
+test('connect-timeout', t => {
+ t.plan(1)
+
+ const client = new Client('http://localhost:9000', {
+ connectTimeout: 1e3
+ })
+ t.teardown(client.close.bind(client))
+
+ const timeout = setTimeout(() => {
+ t.fail()
+ }, 2e3)
+
+ client.request({
+ path: '/',
+ method: 'GET'
+ }, (err) => {
+ t.type(err, errors.ConnectTimeoutError)
+ clearTimeout(timeout)
+ })
+})
+
+test('connect-timeout', t => {
+ t.plan(1)
+
+ const client = new Pool('http://localhost:9000', {
+ connectTimeout: 1e3
+ })
+ t.teardown(client.close.bind(client))
+
+ const timeout = setTimeout(() => {
+ t.fail()
+ }, 2e3)
+
+ client.request({
+ path: '/',
+ method: 'GET'
+ }, (err) => {
+ t.type(err, errors.ConnectTimeoutError)
+ clearTimeout(timeout)
+ })
+})
diff --git a/test/content-length.js b/test/content-length.js
new file mode 100644
index 0000000..9ce7405
--- /dev/null
+++ b/test/content-length.js
@@ -0,0 +1,445 @@
+'use strict'
+
+const { test } = require('tap')
+const { Client, errors } = require('..')
+const { createServer } = require('http')
+const { Readable } = require('stream')
+const { maybeWrapStream, consts } = require('./utils/async-iterators')
+
+test('request invalid content-length', (t) => {
+ t.plan(7)
+
+ const server = createServer((req, res) => {
+ res.end()
+ })
+ t.teardown(server.close.bind(server))
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ client.request({
+ path: '/',
+ method: 'PUT',
+ headers: {
+ 'content-length': 10
+ },
+ body: 'asd'
+ }, (err, data) => {
+ t.type(err, errors.RequestContentLengthMismatchError)
+ })
+
+ client.request({
+ path: '/',
+ method: 'PUT',
+ headers: {
+ 'content-length': 10
+ },
+ body: 'asdasdasdasdasdasda'
+ }, (err, data) => {
+ t.type(err, errors.RequestContentLengthMismatchError)
+ })
+
+ client.request({
+ path: '/',
+ method: 'PUT',
+ headers: {
+ 'content-length': 10
+ },
+ body: Buffer.alloc(9)
+ }, (err, data) => {
+ t.type(err, errors.RequestContentLengthMismatchError)
+ })
+
+ client.request({
+ path: '/',
+ method: 'PUT',
+ headers: {
+ 'content-length': 10
+ },
+ body: Buffer.alloc(11)
+ }, (err, data) => {
+ t.type(err, errors.RequestContentLengthMismatchError)
+ })
+
+ client.request({
+ path: '/',
+ method: 'GET',
+ headers: {
+ 'content-length': 4
+ },
+ body: ['asd']
+ }, (err, data) => {
+ t.type(err, errors.RequestContentLengthMismatchError)
+ })
+
+ client.request({
+ path: '/',
+ method: 'GET',
+ headers: {
+ 'content-length': 4
+ },
+ body: ['asasdasdasdd']
+ }, (err, data) => {
+ t.type(err, errors.RequestContentLengthMismatchError)
+ })
+
+ client.request({
+ path: '/',
+ method: 'DELETE',
+ headers: {
+ 'content-length': 4
+ },
+ body: ['asasdasdasdd']
+ }, (err, data) => {
+ t.type(err, errors.RequestContentLengthMismatchError)
+ })
+ })
+})
+
+function invalidContentLength (bodyType) {
+ test(`request streaming ${bodyType} invalid content-length`, (t) => {
+ t.plan(4)
+
+ const server = createServer((req, res) => {
+ res.end()
+ })
+ t.teardown(server.close.bind(server))
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ client.once('disconnect', () => {
+ t.pass()
+ client.once('disconnect', () => {
+ t.pass()
+ })
+ })
+
+ client.request({
+ path: '/',
+ method: 'PUT',
+ headers: {
+ 'content-length': 10
+ },
+ body: maybeWrapStream(new Readable({
+ read () {
+ setImmediate(() => {
+ this.push('asdasdasdkajsdnasdkjasnd')
+ this.push(null)
+ })
+ }
+ }), bodyType)
+ }, (err, data) => {
+ t.type(err, errors.RequestContentLengthMismatchError)
+ })
+
+ client.request({
+ path: '/',
+ method: 'PUT',
+ headers: {
+ 'content-length': 10
+ },
+ body: maybeWrapStream(new Readable({
+ read () {
+ setImmediate(() => {
+ this.push('asd')
+ this.push(null)
+ })
+ }
+ }), bodyType)
+ }, (err, data) => {
+ t.type(err, errors.RequestContentLengthMismatchError)
+ })
+ })
+ })
+}
+
+invalidContentLength(consts.STREAM)
+invalidContentLength(consts.ASYNC_ITERATOR)
+
+function zeroContentLength (bodyType) {
+ test(`request ${bodyType} streaming data when content-length=0`, (t) => {
+ t.plan(1)
+
+ const server = createServer((req, res) => {
+ res.end()
+ })
+ t.teardown(server.close.bind(server))
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ client.request({
+ path: '/',
+ method: 'PUT',
+ headers: {
+ 'content-length': 0
+ },
+ body: maybeWrapStream(new Readable({
+ read () {
+ setImmediate(() => {
+ this.push('asdasdasdkajsdnasdkjasnd')
+ this.push(null)
+ })
+ }
+ }), bodyType)
+ }, (err, data) => {
+ t.type(err, errors.RequestContentLengthMismatchError)
+ })
+ })
+ })
+}
+
+zeroContentLength(consts.STREAM)
+zeroContentLength(consts.ASYNC_ITERATOR)
+
+test('request streaming no body data when content-length=0', (t) => {
+ t.plan(2)
+
+ const server = createServer((req, res) => {
+ req.pipe(res)
+ })
+ t.teardown(server.close.bind(server))
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ client.request({
+ path: '/',
+ method: 'PUT',
+ headers: {
+ 'content-length': 0
+ }
+ }, (err, data) => {
+ t.error(err)
+ data.body
+ .on('data', () => {
+ t.fail()
+ })
+ .on('end', () => {
+ t.pass()
+ })
+ })
+ })
+})
+
+test('response invalid content length with close', (t) => {
+ t.plan(3)
+
+ const server = createServer((req, res) => {
+ res.writeHead(200, {
+ 'content-length': 10
+ })
+ res.end('123')
+ })
+ t.teardown(server.close.bind(server))
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, {
+ pipelining: 0
+ })
+ t.teardown(client.destroy.bind(client))
+
+ client.on('disconnect', (origin, client, err) => {
+ t.equal(err.code, 'UND_ERR_RES_CONTENT_LENGTH_MISMATCH')
+ })
+
+ client.request({
+ path: '/',
+ method: 'GET'
+ }, (err, data) => {
+ t.error(err)
+ data.body
+ .on('end', () => {
+ t.fail()
+ })
+ .on('error', (err) => {
+ t.equal(err.code, 'UND_ERR_RES_CONTENT_LENGTH_MISMATCH')
+ })
+ .resume()
+ })
+ })
+})
+
+test('request streaming with Readable.from(buf)', (t) => {
+ const server = createServer((req, res) => {
+ req.pipe(res)
+ })
+ t.teardown(server.close.bind(server))
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ client.request({
+ path: '/',
+ method: 'PUT',
+ body: Readable.from(Buffer.from('hello'))
+ }, (err, data) => {
+ const chunks = []
+ t.error(err)
+ data.body
+ .on('data', (chunk) => {
+ chunks.push(chunk)
+ })
+ .on('end', () => {
+ t.equal(Buffer.concat(chunks).toString(), 'hello')
+ t.pass()
+ t.end()
+ })
+ })
+ })
+})
+
+test('request DELETE, content-length=0, with body', (t) => {
+ t.plan(5)
+ const server = createServer((req, res) => {
+ res.end()
+ })
+ server.on('request', (req, res) => {
+ t.equal(req.headers['content-length'], undefined)
+ })
+ t.teardown(server.close.bind(server))
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ client.request({
+ path: '/',
+ method: 'DELETE',
+ headers: {
+ 'content-length': 0
+ },
+ body: new Readable({
+ read () {
+ this.push('asd')
+ this.push(null)
+ }
+ })
+ }, (err) => {
+ t.type(err, errors.RequestContentLengthMismatchError)
+ })
+
+ client.request({
+ path: '/',
+ method: 'DELETE',
+ headers: {
+ 'content-length': 0
+ }
+ }, (err, resp) => {
+ t.equal(resp.headers['content-length'], '0')
+ t.error(err)
+ })
+
+ client.on('disconnect', () => {
+ t.pass()
+ })
+ })
+})
+
+test('content-length shouldSendContentLength=false', (t) => {
+ t.plan(15)
+ const server = createServer((req, res) => {
+ res.end()
+ })
+ server.on('request', (req, res) => {
+ switch (req.url) {
+ case '/put0':
+ t.equal(req.headers['content-length'], '0')
+ break
+ case '/head':
+ t.equal(req.headers['content-length'], undefined)
+ break
+ case '/get':
+ t.equal(req.headers['content-length'], undefined)
+ break
+ }
+ })
+ t.teardown(server.close.bind(server))
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ client.request({
+ path: '/put0',
+ method: 'PUT',
+ headers: {
+ 'content-length': 0
+ }
+ }, (err, resp) => {
+ t.equal(resp.headers['content-length'], '0')
+ t.error(err)
+ })
+
+ client.request({
+ path: '/head',
+ method: 'HEAD',
+ headers: {
+ 'content-length': 10
+ }
+ }, (err, resp) => {
+ t.equal(resp.headers['content-length'], undefined)
+ t.error(err)
+ })
+
+ client.request({
+ path: '/get',
+ method: 'GET',
+ headers: {
+ 'content-length': 0
+ }
+ }, (err) => {
+ t.error(err)
+ })
+
+ client.request({
+ path: '/',
+ method: 'GET',
+ headers: {
+ 'content-length': 4
+ },
+ body: new Readable({
+ read () {
+ this.push('asd')
+ this.push(null)
+ }
+ })
+ }, (err) => {
+ t.error(err)
+ })
+
+ client.request({
+ path: '/',
+ method: 'GET',
+ headers: {
+ 'content-length': 4
+ },
+ body: new Readable({
+ read () {
+ this.push('asasdasdasdd')
+ this.push(null)
+ }
+ })
+ }, (err) => {
+ t.error(err)
+ })
+
+ client.request({
+ path: '/',
+ method: 'HEAD',
+ headers: {
+ 'content-length': 4
+ },
+ body: new Readable({
+ read () {
+ this.push('asasdasdasdd')
+ this.push(null)
+ }
+ })
+ }, (err) => {
+ t.error(err)
+ })
+
+ client.on('disconnect', () => {
+ t.pass()
+ })
+ })
+})
diff --git a/test/cookie/cookies.js b/test/cookie/cookies.js
new file mode 100644
index 0000000..70222fa
--- /dev/null
+++ b/test/cookie/cookies.js
@@ -0,0 +1,616 @@
+// MIT License
+//
+// Copyright 2018-2022 the Deno authors.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+'use strict'
+
+const { test } = require('tap')
+const {
+ deleteCookie,
+ getCookies,
+ getSetCookies,
+ setCookie,
+ Headers
+} = require('../..')
+
+// https://raw.githubusercontent.com/denoland/deno_std/b4239898d6c6b4cdbfd659a4ea1838cf4e656336/http/cookie_test.ts
+
+test('Cookie parser', (t) => {
+ let headers = new Headers()
+ t.same(getCookies(headers), {})
+ headers = new Headers()
+ headers.set('Cookie', 'foo=bar')
+ t.same(getCookies(headers), { foo: 'bar' })
+
+ headers = new Headers()
+ headers.set('Cookie', 'full=of ; tasty=chocolate')
+ t.same(getCookies(headers), { full: 'of ', tasty: 'chocolate' })
+
+ headers = new Headers()
+ headers.set('Cookie', 'igot=99; problems=but...')
+ t.same(getCookies(headers), { igot: '99', problems: 'but...' })
+
+ headers = new Headers()
+ headers.set('Cookie', 'PREF=al=en-GB&f1=123; wide=1; SID=123')
+ t.same(getCookies(headers), {
+ PREF: 'al=en-GB&f1=123',
+ wide: '1',
+ SID: '123'
+ })
+
+ t.end()
+})
+
+test('Cookie Name Validation', (t) => {
+ const tokens = [
+ '"id"',
+ 'id\t',
+ 'i\td',
+ 'i d',
+ 'i;d',
+ '{id}',
+ '[id]',
+ '"',
+ 'id\u0091'
+ ]
+ const headers = new Headers()
+ tokens.forEach((name) => {
+ t.throws(
+ () => {
+ setCookie(headers, {
+ name,
+ value: 'Cat',
+ httpOnly: true,
+ secure: true,
+ maxAge: 3
+ })
+ },
+ Error
+ )
+ })
+
+ t.end()
+})
+
+test('Cookie Value Validation', (t) => {
+ const tokens = [
+ '1f\tWa',
+ '\t',
+ '1f Wa',
+ '1f;Wa',
+ '"1fWa',
+ '1f\\Wa',
+ '1f"Wa',
+ '"',
+ '1fWa\u0005',
+ '1f\u0091Wa'
+ ]
+
+ const headers = new Headers()
+ tokens.forEach((value) => {
+ t.throws(
+ () => {
+ setCookie(
+ headers,
+ {
+ name: 'Space',
+ value,
+ httpOnly: true,
+ secure: true,
+ maxAge: 3
+ }
+ )
+ },
+ Error,
+ "RFC2616 cookie 'Space'"
+ )
+ })
+
+ t.throws(
+ () => {
+ setCookie(headers, {
+ name: 'location',
+ value: 'United Kingdom'
+ })
+ },
+ Error,
+ "RFC2616 cookie 'location' cannot contain character ' '"
+ )
+
+ t.end()
+})
+
+test('Cookie Path Validation', (t) => {
+ const path = '/;domain=sub.domain.com'
+ const headers = new Headers()
+ t.throws(
+ () => {
+ setCookie(headers, {
+ name: 'Space',
+ value: 'Cat',
+ httpOnly: true,
+ secure: true,
+ path,
+ maxAge: 3
+ })
+ },
+ Error,
+ path + ": Invalid cookie path char ';'"
+ )
+
+ t.end()
+})
+
+test('Cookie Domain Validation', (t) => {
+ const tokens = ['-domain.com', 'domain.org.', 'domain.org-']
+ const headers = new Headers()
+ tokens.forEach((domain) => {
+ t.throws(
+ () => {
+ setCookie(headers, {
+ name: 'Space',
+ value: 'Cat',
+ httpOnly: true,
+ secure: true,
+ domain,
+ maxAge: 3
+ })
+ },
+ Error,
+ 'Invalid first/last char in cookie domain: ' + domain
+ )
+ })
+
+ t.end()
+})
+
+test('Cookie Delete', (t) => {
+ let headers = new Headers()
+ deleteCookie(headers, 'deno')
+ t.equal(
+ headers.get('Set-Cookie'),
+ 'deno=; Expires=Thu, 01 Jan 1970 00:00:00 GMT'
+ )
+ headers = new Headers()
+ setCookie(headers, {
+ name: 'Space',
+ value: 'Cat',
+ domain: 'deno.land',
+ path: '/'
+ })
+ deleteCookie(headers, 'Space', { domain: '', path: '' })
+ t.equal(
+ headers.get('Set-Cookie'),
+ 'Space=Cat; Domain=deno.land; Path=/, Space=; Expires=Thu, 01 Jan 1970 00:00:00 GMT'
+ )
+
+ t.end()
+})
+
+test('Cookie Set', (t) => {
+ let headers = new Headers()
+ setCookie(headers, { name: 'Space', value: 'Cat' })
+ t.equal(headers.get('Set-Cookie'), 'Space=Cat')
+
+ headers = new Headers()
+ setCookie(headers, { name: 'Space', value: 'Cat', secure: true })
+ t.equal(headers.get('Set-Cookie'), 'Space=Cat; Secure')
+
+ headers = new Headers()
+ setCookie(headers, { name: 'Space', value: 'Cat', httpOnly: true })
+ t.equal(headers.get('Set-Cookie'), 'Space=Cat; HttpOnly')
+
+ headers = new Headers()
+ setCookie(headers, {
+ name: 'Space',
+ value: 'Cat',
+ httpOnly: true,
+ secure: true
+ })
+ t.equal(headers.get('Set-Cookie'), 'Space=Cat; Secure; HttpOnly')
+
+ headers = new Headers()
+ setCookie(headers, {
+ name: 'Space',
+ value: 'Cat',
+ httpOnly: true,
+ secure: true,
+ maxAge: 2
+ })
+ t.equal(
+ headers.get('Set-Cookie'),
+ 'Space=Cat; Secure; HttpOnly; Max-Age=2'
+ )
+
+ headers = new Headers()
+ setCookie(headers, {
+ name: 'Space',
+ value: 'Cat',
+ httpOnly: true,
+ secure: true,
+ maxAge: 0
+ })
+ t.equal(
+ headers.get('Set-Cookie'),
+ 'Space=Cat; Secure; HttpOnly; Max-Age=0'
+ )
+
+ let error = false
+ headers = new Headers()
+ try {
+ setCookie(headers, {
+ name: 'Space',
+ value: 'Cat',
+ httpOnly: true,
+ secure: true,
+ maxAge: -1
+ })
+ } catch {
+ error = true
+ }
+ t.ok(error)
+
+ headers = new Headers()
+ setCookie(headers, {
+ name: 'Space',
+ value: 'Cat',
+ httpOnly: true,
+ secure: true,
+ maxAge: 2,
+ domain: 'deno.land'
+ })
+ t.equal(
+ headers.get('Set-Cookie'),
+ 'Space=Cat; Secure; HttpOnly; Max-Age=2; Domain=deno.land'
+ )
+
+ headers = new Headers()
+ setCookie(headers, {
+ name: 'Space',
+ value: 'Cat',
+ httpOnly: true,
+ secure: true,
+ maxAge: 2,
+ domain: 'deno.land',
+ sameSite: 'Strict'
+ })
+ t.equal(
+ headers.get('Set-Cookie'),
+ 'Space=Cat; Secure; HttpOnly; Max-Age=2; Domain=deno.land; ' +
+ 'SameSite=Strict'
+ )
+
+ headers = new Headers()
+ setCookie(headers, {
+ name: 'Space',
+ value: 'Cat',
+ httpOnly: true,
+ secure: true,
+ maxAge: 2,
+ domain: 'deno.land',
+ sameSite: 'Lax'
+ })
+ t.equal(
+ headers.get('Set-Cookie'),
+ 'Space=Cat; Secure; HttpOnly; Max-Age=2; Domain=deno.land; SameSite=Lax'
+ )
+
+ headers = new Headers()
+ setCookie(headers, {
+ name: 'Space',
+ value: 'Cat',
+ httpOnly: true,
+ secure: true,
+ maxAge: 2,
+ domain: 'deno.land',
+ path: '/'
+ })
+ t.equal(
+ headers.get('Set-Cookie'),
+ 'Space=Cat; Secure; HttpOnly; Max-Age=2; Domain=deno.land; Path=/'
+ )
+
+ headers = new Headers()
+ setCookie(headers, {
+ name: 'Space',
+ value: 'Cat',
+ httpOnly: true,
+ secure: true,
+ maxAge: 2,
+ domain: 'deno.land',
+ path: '/',
+ unparsed: ['unparsed=keyvalue', 'batman=Bruce']
+ })
+ t.equal(
+ headers.get('Set-Cookie'),
+ 'Space=Cat; Secure; HttpOnly; Max-Age=2; Domain=deno.land; Path=/; ' +
+ 'unparsed=keyvalue; batman=Bruce'
+ )
+
+ headers = new Headers()
+ setCookie(headers, {
+ name: 'Space',
+ value: 'Cat',
+ httpOnly: true,
+ secure: true,
+ maxAge: 2,
+ domain: 'deno.land',
+ path: '/',
+ expires: new Date(Date.UTC(1983, 0, 7, 15, 32))
+ })
+ t.equal(
+ headers.get('Set-Cookie'),
+ 'Space=Cat; Secure; HttpOnly; Max-Age=2; Domain=deno.land; Path=/; ' +
+ 'Expires=Fri, 07 Jan 1983 15:32:00 GMT'
+ )
+
+ headers = new Headers()
+ setCookie(headers, {
+ name: 'Space',
+ value: 'Cat',
+ expires: Date.UTC(1983, 0, 7, 15, 32)
+ })
+ t.equal(
+ headers.get('Set-Cookie'),
+ 'Space=Cat; Expires=Fri, 07 Jan 1983 15:32:00 GMT'
+ )
+
+ headers = new Headers()
+ setCookie(headers, { name: '__Secure-Kitty', value: 'Meow' })
+ t.equal(headers.get('Set-Cookie'), '__Secure-Kitty=Meow; Secure')
+
+ headers = new Headers()
+ setCookie(headers, {
+ name: '__Host-Kitty',
+ value: 'Meow',
+ domain: 'deno.land'
+ })
+ t.equal(
+ headers.get('Set-Cookie'),
+ '__Host-Kitty=Meow; Secure; Path=/'
+ )
+
+ headers = new Headers()
+ setCookie(headers, { name: 'cookie-1', value: 'value-1', secure: true })
+ setCookie(headers, { name: 'cookie-2', value: 'value-2', maxAge: 3600 })
+ t.equal(
+ headers.get('Set-Cookie'),
+ 'cookie-1=value-1; Secure, cookie-2=value-2; Max-Age=3600'
+ )
+
+ headers = new Headers()
+ setCookie(headers, { name: '', value: '' })
+ t.equal(headers.get('Set-Cookie'), null)
+
+ t.end()
+})
+
+test('Set-Cookie parser', (t) => {
+ let headers = new Headers({ 'set-cookie': 'Space=Cat' })
+ t.same(getSetCookies(headers), [{
+ name: 'Space',
+ value: 'Cat'
+ }])
+
+ headers = new Headers({ 'set-cookie': 'Space=Cat; Secure' })
+ t.same(getSetCookies(headers), [{
+ name: 'Space',
+ value: 'Cat',
+ secure: true
+ }])
+
+ headers = new Headers({ 'set-cookie': 'Space=Cat; HttpOnly' })
+ t.same(getSetCookies(headers), [{
+ name: 'Space',
+ value: 'Cat',
+ httpOnly: true
+ }])
+
+ headers = new Headers({ 'set-cookie': 'Space=Cat; Secure; HttpOnly' })
+ t.same(getSetCookies(headers), [{
+ name: 'Space',
+ value: 'Cat',
+ secure: true,
+ httpOnly: true
+ }])
+
+ headers = new Headers({
+ 'set-cookie': 'Space=Cat; Secure; HttpOnly; Max-Age=2'
+ })
+ t.same(getSetCookies(headers), [{
+ name: 'Space',
+ value: 'Cat',
+ secure: true,
+ httpOnly: true,
+ maxAge: 2
+ }])
+
+ headers = new Headers({
+ 'set-cookie': 'Space=Cat; Secure; HttpOnly; Max-Age=0'
+ })
+ t.same(getSetCookies(headers), [{
+ name: 'Space',
+ value: 'Cat',
+ secure: true,
+ httpOnly: true,
+ maxAge: 0
+ }])
+
+ headers = new Headers({
+ 'set-cookie': 'Space=Cat; Secure; HttpOnly; Max-Age=-1'
+ })
+ t.same(getSetCookies(headers), [{
+ name: 'Space',
+ value: 'Cat',
+ secure: true,
+ httpOnly: true
+ }])
+
+ headers = new Headers({
+ 'set-cookie': 'Space=Cat; Secure; HttpOnly; Max-Age=2; Domain=deno.land'
+ })
+ t.same(getSetCookies(headers), [{
+ name: 'Space',
+ value: 'Cat',
+ secure: true,
+ httpOnly: true,
+ maxAge: 2,
+ domain: 'deno.land'
+ }])
+
+ headers = new Headers({
+ 'set-cookie':
+ 'Space=Cat; Secure; HttpOnly; Max-Age=2; Domain=deno.land; SameSite=Strict'
+ })
+ t.same(getSetCookies(headers), [{
+ name: 'Space',
+ value: 'Cat',
+ secure: true,
+ httpOnly: true,
+ maxAge: 2,
+ domain: 'deno.land',
+ sameSite: 'Strict'
+ }])
+
+ headers = new Headers({
+ 'set-cookie':
+ 'Space=Cat; Secure; HttpOnly; Max-Age=2; Domain=deno.land; SameSite=Lax'
+ })
+ t.same(getSetCookies(headers), [{
+ name: 'Space',
+ value: 'Cat',
+ secure: true,
+ httpOnly: true,
+ maxAge: 2,
+ domain: 'deno.land',
+ sameSite: 'Lax'
+ }])
+
+ headers = new Headers({
+ 'set-cookie':
+ 'Space=Cat; Secure; HttpOnly; Max-Age=2; Domain=deno.land; Path=/'
+ })
+ t.same(getSetCookies(headers), [{
+ name: 'Space',
+ value: 'Cat',
+ secure: true,
+ httpOnly: true,
+ maxAge: 2,
+ domain: 'deno.land',
+ path: '/'
+ }])
+
+ headers = new Headers({
+ 'set-cookie':
+ 'Space=Cat; Secure; HttpOnly; Max-Age=2; Domain=deno.land; Path=/; unparsed=keyvalue; batman=Bruce'
+ })
+ t.same(getSetCookies(headers), [{
+ name: 'Space',
+ value: 'Cat',
+ secure: true,
+ httpOnly: true,
+ maxAge: 2,
+ domain: 'deno.land',
+ path: '/',
+ unparsed: ['unparsed=keyvalue', 'batman=Bruce']
+ }])
+
+ headers = new Headers({
+ 'set-cookie':
+ 'Space=Cat; Secure; HttpOnly; Max-Age=2; Domain=deno.land; Path=/; ' +
+ 'Expires=Fri, 07 Jan 1983 15:32:00 GMT'
+ })
+ t.same(getSetCookies(headers), [{
+ name: 'Space',
+ value: 'Cat',
+ secure: true,
+ httpOnly: true,
+ maxAge: 2,
+ domain: 'deno.land',
+ path: '/',
+ expires: new Date(Date.UTC(1983, 0, 7, 15, 32))
+ }])
+
+ headers = new Headers({ 'set-cookie': '__Secure-Kitty=Meow; Secure' })
+ t.same(getSetCookies(headers), [{
+ name: '__Secure-Kitty',
+ value: 'Meow',
+ secure: true
+ }])
+
+ headers = new Headers({ 'set-cookie': '__Secure-Kitty=Meow' })
+ t.same(getSetCookies(headers), [{
+ name: '__Secure-Kitty',
+ value: 'Meow'
+ }])
+
+ headers = new Headers({
+ 'set-cookie': '__Host-Kitty=Meow; Secure; Path=/'
+ })
+ t.same(getSetCookies(headers), [{
+ name: '__Host-Kitty',
+ value: 'Meow',
+ secure: true,
+ path: '/'
+ }])
+
+ headers = new Headers({ 'set-cookie': '__Host-Kitty=Meow; Path=/' })
+ t.same(getSetCookies(headers), [{
+ name: '__Host-Kitty',
+ value: 'Meow',
+ path: '/'
+ }])
+
+ headers = new Headers({
+ 'set-cookie': '__Host-Kitty=Meow; Secure; Domain=deno.land; Path=/'
+ })
+ t.same(getSetCookies(headers), [{
+ name: '__Host-Kitty',
+ value: 'Meow',
+ secure: true,
+ domain: 'deno.land',
+ path: '/'
+ }])
+
+ headers = new Headers({
+ 'set-cookie': '__Host-Kitty=Meow; Secure; Path=/not-root'
+ })
+ t.same(getSetCookies(headers), [{
+ name: '__Host-Kitty',
+ value: 'Meow',
+ secure: true,
+ path: '/not-root'
+ }])
+
+ headers = new Headers([
+ ['set-cookie', 'cookie-1=value-1; Secure'],
+ ['set-cookie', 'cookie-2=value-2; Max-Age=3600']
+ ])
+ t.same(getSetCookies(headers), [
+ { name: 'cookie-1', value: 'value-1', secure: true },
+ { name: 'cookie-2', value: 'value-2', maxAge: 3600 }
+ ])
+
+ headers = new Headers()
+ t.same(getSetCookies(headers), [])
+
+ t.end()
+})
diff --git a/test/cookie/global-headers.js b/test/cookie/global-headers.js
new file mode 100644
index 0000000..1d58dce
--- /dev/null
+++ b/test/cookie/global-headers.js
@@ -0,0 +1,70 @@
+'use strict'
+
+const { test, skip } = require('tap')
+const {
+ deleteCookie,
+ getCookies,
+ getSetCookies,
+ setCookie
+} = require('../..')
+const { getHeadersList } = require('../../lib/cookies/util')
+
+/* global Headers */
+
+if (!globalThis.Headers) {
+ skip('No global Headers to test')
+ process.exit(0)
+}
+
+test('Using global Headers', (t) => {
+ t.test('deleteCookies', (t) => {
+ const headers = new Headers()
+
+ t.equal(headers.get('set-cookie'), null)
+ deleteCookie(headers, 'undici')
+ t.equal(headers.get('set-cookie'), 'undici=; Expires=Thu, 01 Jan 1970 00:00:00 GMT')
+
+ t.end()
+ })
+
+ t.test('getCookies', (t) => {
+ const headers = new Headers({
+ cookie: 'get=cookies; and=attributes'
+ })
+
+ t.same(getCookies(headers), { get: 'cookies', and: 'attributes' })
+ t.end()
+ })
+
+ t.test('getSetCookies', (t) => {
+ const headers = new Headers({
+ 'set-cookie': 'undici=getSetCookies; Secure'
+ })
+
+ const supportsCookies = getHeadersList(headers).cookies
+
+ if (!supportsCookies) {
+ t.same(getSetCookies(headers), [])
+ } else {
+ t.same(getSetCookies(headers), [
+ {
+ name: 'undici',
+ value: 'getSetCookies',
+ secure: true
+ }
+ ])
+ }
+
+ t.end()
+ })
+
+ t.test('setCookie', (t) => {
+ const headers = new Headers()
+
+ setCookie(headers, { name: 'undici', value: 'setCookie' })
+ t.equal(headers.get('Set-Cookie'), 'undici=setCookie')
+ t.end()
+ })
+
+ t.end()
+})
diff --git a/test/diagnostics-channel/connect-error.js b/test/diagnostics-channel/connect-error.js
new file mode 100644
index 0000000..f7e842d
--- /dev/null
+++ b/test/diagnostics-channel/connect-error.js
@@ -0,0 +1,61 @@
+'use strict'
+
+const t = require('tap')
+
+let diagnosticsChannel
+
+try {
+ diagnosticsChannel = require('diagnostics_channel')
+} catch {
+ t.skip('missing diagnostics_channel')
+ process.exit(0)
+}
+
+const { Client } = require('../..')
+
+t.plan(16)
+
+const connectError = new Error('custom error')
+
+let _connector
+diagnosticsChannel.channel('undici:client:beforeConnect').subscribe(({ connectParams, connector }) => {
+ _connector = connector
+
+ t.equal(typeof _connector, 'function')
+ t.equal(Object.keys(connectParams).length, 6)
+
+ const { host, hostname, protocol, port, servername } = connectParams
+
+ t.equal(host, 'localhost:1234')
+ t.equal(hostname, 'localhost')
+ t.equal(port, '1234')
+ t.equal(protocol, 'http:')
+ t.equal(servername, null)
+})
+
+diagnosticsChannel.channel('undici:client:connectError').subscribe(({ error, connectParams, connector }) => {
+ t.equal(Object.keys(connectParams).length, 6)
+ t.equal(_connector, connector)
+
+ const { host, hostname, protocol, port, servername } = connectParams
+
+ t.equal(error, connectError)
+ t.equal(host, 'localhost:1234')
+ t.equal(hostname, 'localhost')
+ t.equal(port, '1234')
+ t.equal(protocol, 'http:')
+ t.equal(servername, null)
+})
+
+const client = new Client('http://localhost:1234', {
+ connect: (_, cb) => { cb(connectError, null) }
+})
+
+t.teardown(client.close.bind(client))
+
+client.request({
+ path: '/',
+ method: 'GET'
+}, (err, data) => {
+ t.equal(err, connectError)
+})
diff --git a/test/diagnostics-channel/error.js b/test/diagnostics-channel/error.js
new file mode 100644
index 0000000..1f350b1
--- /dev/null
+++ b/test/diagnostics-channel/error.js
@@ -0,0 +1,52 @@
+'use strict'
+
+const t = require('tap')
+
+let diagnosticsChannel
+
+try {
+ diagnosticsChannel = require('diagnostics_channel')
+} catch {
+ t.skip('missing diagnostics_channel')
+ process.exit(0)
+}
+
+const { Client } = require('../..')
+const { createServer } = require('http')
+
+t.plan(3)
+
+const server = createServer((req, res) => {
+ res.destroy()
+})
+t.teardown(server.close.bind(server))
+
+const reqHeaders = {
+ foo: undefined,
+ bar: 'bar'
+}
+
+let _req
+diagnosticsChannel.channel('undici:request:create').subscribe(({ request }) => {
+ _req = request
+})
+
+diagnosticsChannel.channel('undici:request:error').subscribe(({ request, error }) => {
+ t.equal(_req, request)
+ t.equal(error.code, 'UND_ERR_SOCKET')
+})
+
+server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, {
+ keepAliveTimeout: 300e3
+ })
+ t.teardown(client.close.bind(client))
+
+ client.request({
+ path: '/',
+ method: 'GET',
+ headers: reqHeaders
+ }, (err, data) => {
+ t.equal(err.code, 'UND_ERR_SOCKET')
+ })
+})
diff --git a/test/diagnostics-channel/get.js b/test/diagnostics-channel/get.js
new file mode 100644
index 0000000..9d868c3
--- /dev/null
+++ b/test/diagnostics-channel/get.js
@@ -0,0 +1,141 @@
+'use strict'
+
+const t = require('tap')
+
+let diagnosticsChannel
+
+try {
+ diagnosticsChannel = require('diagnostics_channel')
+} catch {
+ t.skip('missing diagnostics_channel')
+ process.exit(0)
+}
+
+const { Client } = require('../..')
+const { createServer } = require('http')
+
+t.plan(32)
+
+const server = createServer((req, res) => {
+ res.setHeader('Content-Type', 'text/plain')
+ res.setHeader('trailer', 'foo')
+ res.write('hello')
+ res.addTrailers({
+ foo: 'oof'
+ })
+ res.end()
+})
+t.teardown(server.close.bind(server))
+
+const reqHeaders = {
+ foo: undefined,
+ bar: 'bar'
+}
+
+let _req
+diagnosticsChannel.channel('undici:request:create').subscribe(({ request }) => {
+ _req = request
+ t.equal(request.origin, `http://localhost:${server.address().port}`)
+ t.equal(request.completed, false)
+ t.equal(request.method, 'GET')
+ t.equal(request.path, '/')
+ t.equal(request.headers, 'bar: bar\r\n')
+ request.addHeader('hello', 'world')
+ t.equal(request.headers, 'bar: bar\r\nhello: world\r\n')
+})
+
+let _connector
+diagnosticsChannel.channel('undici:client:beforeConnect').subscribe(({ connectParams, connector }) => {
+ _connector = connector
+
+ t.equal(typeof _connector, 'function')
+ t.equal(Object.keys(connectParams).length, 6)
+
+ const { host, hostname, protocol, port, servername } = connectParams
+
+ t.equal(host, `localhost:${server.address().port}`)
+ t.equal(hostname, 'localhost')
+ t.equal(port, String(server.address().port))
+ t.equal(protocol, 'http:')
+ t.equal(servername, null)
+})
+
+let _socket
+diagnosticsChannel.channel('undici:client:connected').subscribe(({ connectParams, socket, connector }) => {
+ _socket = socket
+
+ t.equal(_connector, connector)
+ t.equal(Object.keys(connectParams).length, 6)
+
+ const { host, hostname, protocol, port, servername } = connectParams
+
+ t.equal(host, `localhost:${server.address().port}`)
+ t.equal(hostname, 'localhost')
+ t.equal(port, String(server.address().port))
+ t.equal(protocol, 'http:')
+ t.equal(servername, null)
+})
+
+diagnosticsChannel.channel('undici:client:sendHeaders').subscribe(({ request, headers, socket }) => {
+ t.equal(_req, request)
+ t.equal(_socket, socket)
+
+ const expectedHeaders = [
+ 'GET / HTTP/1.1',
+ `host: localhost:${server.address().port}`,
+ 'connection: keep-alive',
+ 'bar: bar',
+ 'hello: world'
+ ]
+
+ t.equal(headers, expectedHeaders.join('\r\n') + '\r\n')
+})
+
+diagnosticsChannel.channel('undici:request:headers').subscribe(({ request, response }) => {
+ t.equal(_req, request)
+ t.equal(response.statusCode, 200)
+ const expectedHeaders = [
+ Buffer.from('Content-Type'),
+ Buffer.from('text/plain'),
+ Buffer.from('trailer'),
+ Buffer.from('foo'),
+ Buffer.from('Date'),
+ response.headers[5], // This is a date
+ Buffer.from('Connection'),
+ Buffer.from('keep-alive'),
+ Buffer.from('Keep-Alive'),
+ Buffer.from('timeout=5'),
+ Buffer.from('Transfer-Encoding'),
+ Buffer.from('chunked')
+ ]
+ t.same(response.headers, expectedHeaders)
+ t.equal(response.statusText, 'OK')
+})
+
+let endEmitted = false
+diagnosticsChannel.channel('undici:request:trailers').subscribe(({ request, trailers }) => {
+ t.equal(request.completed, true)
+ t.equal(_req, request)
+ // This event is emitted after the last chunk has been added to the body stream,
+ // not when it was consumed by the application
+ t.equal(endEmitted, false)
+ t.same(trailers, [Buffer.from('foo'), Buffer.from('oof')])
+})
+
+server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, {
+ keepAliveTimeout: 300e3
+ })
+ t.teardown(client.close.bind(client))
+
+ client.request({
+ path: '/',
+ method: 'GET',
+ headers: reqHeaders
+ }, (err, data) => {
+ t.error(err)
+ data.body.on('end', function () {
+ endEmitted = true
+ })
+ })
+})
diff --git a/test/diagnostics-channel/post-stream.js b/test/diagnostics-channel/post-stream.js
new file mode 100644
index 0000000..236b9bb
--- /dev/null
+++ b/test/diagnostics-channel/post-stream.js
@@ -0,0 +1,149 @@
+'use strict'
+
+const t = require('tap')
+const { Readable } = require('stream')
+
+let diagnosticsChannel
+
+try {
+ diagnosticsChannel = require('diagnostics_channel')
+} catch {
+ t.skip('missing diagnostics_channel')
+ process.exit(0)
+}
+
+const { Client } = require('../..')
+const { createServer } = require('http')
+
+t.plan(33)
+
+const server = createServer((req, res) => {
+ req.resume()
+ res.setHeader('Content-Type', 'text/plain')
+ res.setHeader('trailer', 'foo')
+ res.write('hello')
+ res.addTrailers({
+ foo: 'oof'
+ })
+ res.end()
+})
+t.teardown(server.close.bind(server))
+
+const reqHeaders = {
+ foo: undefined,
+ bar: 'bar'
+}
+const body = Readable.from(['hello', ' ', 'world'])
+
+let _req
+diagnosticsChannel.channel('undici:request:create').subscribe(({ request }) => {
+ _req = request
+ t.equal(request.completed, false)
+ t.equal(request.method, 'POST')
+ t.equal(request.path, '/')
+ t.equal(request.headers, 'bar: bar\r\n')
+ request.addHeader('hello', 'world')
+ t.equal(request.headers, 'bar: bar\r\nhello: world\r\n')
+ t.same(request.body, body)
+})
+
+let _connector
+diagnosticsChannel.channel('undici:client:beforeConnect').subscribe(({ connectParams, connector }) => {
+ _connector = connector
+
+ t.equal(typeof _connector, 'function')
+ t.equal(Object.keys(connectParams).length, 6)
+
+ const { host, hostname, protocol, port, servername } = connectParams
+
+ t.equal(host, `localhost:${server.address().port}`)
+ t.equal(hostname, 'localhost')
+ t.equal(port, String(server.address().port))
+ t.equal(protocol, 'http:')
+ t.equal(servername, null)
+})
+
+let _socket
+diagnosticsChannel.channel('undici:client:connected').subscribe(({ connectParams, socket, connector }) => {
+ _socket = socket
+
+ t.equal(Object.keys(connectParams).length, 6)
+ t.equal(_connector, connector)
+
+ const { host, hostname, protocol, port, servername } = connectParams
+
+ t.equal(host, `localhost:${server.address().port}`)
+ t.equal(hostname, 'localhost')
+ t.equal(port, String(server.address().port))
+ t.equal(protocol, 'http:')
+ t.equal(servername, null)
+})
+
+diagnosticsChannel.channel('undici:client:sendHeaders').subscribe(({ request, headers, socket }) => {
+ t.equal(_req, request)
+ t.equal(_socket, socket)
+
+ const expectedHeaders = [
+ 'POST / HTTP/1.1',
+ `host: localhost:${server.address().port}`,
+ 'connection: keep-alive',
+ 'bar: bar',
+ 'hello: world'
+ ]
+
+ t.equal(headers, expectedHeaders.join('\r\n') + '\r\n')
+})
+
+diagnosticsChannel.channel('undici:request:headers').subscribe(({ request, response }) => {
+ t.equal(_req, request)
+ t.equal(response.statusCode, 200)
+ const expectedHeaders = [
+ Buffer.from('Content-Type'),
+ Buffer.from('text/plain'),
+ Buffer.from('trailer'),
+ Buffer.from('foo'),
+ Buffer.from('Date'),
+ response.headers[5], // This is a date
+ Buffer.from('Connection'),
+ Buffer.from('keep-alive'),
+ Buffer.from('Keep-Alive'),
+ Buffer.from('timeout=5'),
+ Buffer.from('Transfer-Encoding'),
+ Buffer.from('chunked')
+ ]
+ t.same(response.headers, expectedHeaders)
+ t.equal(response.statusText, 'OK')
+})
+
+diagnosticsChannel.channel('undici:request:bodySent').subscribe(({ request }) => {
+ t.equal(_req, request)
+})
+
+let endEmitted = false
+diagnosticsChannel.channel('undici:request:trailers').subscribe(({ request, trailers }) => {
+ t.equal(request.completed, true)
+ t.equal(_req, request)
+ // This event is emitted after the last chunk has been added to the body stream,
+ // not when it was consumed by the application
+ t.equal(endEmitted, false)
+ t.same(trailers, [Buffer.from('foo'), Buffer.from('oof')])
+})
+
+server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, {
+ keepAliveTimeout: 300e3
+ })
+ t.teardown(client.close.bind(client))
+
+ client.request({
+ path: '/',
+ method: 'POST',
+ headers: reqHeaders,
+ body
+ }, (err, data) => {
+ t.error(err)
+ data.body.on('end', function () {
+ endEmitted = true
+ })
+ })
+})
diff --git a/test/diagnostics-channel/post.js b/test/diagnostics-channel/post.js
new file mode 100644
index 0000000..fc02eb5
--- /dev/null
+++ b/test/diagnostics-channel/post.js
@@ -0,0 +1,147 @@
+'use strict'
+
+const t = require('tap')
+
+let diagnosticsChannel
+
+try {
+ diagnosticsChannel = require('diagnostics_channel')
+} catch {
+ t.skip('missing diagnostics_channel')
+ process.exit(0)
+}
+
+const { Client } = require('../..')
+const { createServer } = require('http')
+
+t.plan(33)
+
+const server = createServer((req, res) => {
+ req.resume()
+ res.setHeader('Content-Type', 'text/plain')
+ res.setHeader('trailer', 'foo')
+ res.write('hello')
+ res.addTrailers({
+ foo: 'oof'
+ })
+ res.end()
+})
+t.teardown(server.close.bind(server))
+
+const reqHeaders = {
+ foo: undefined,
+ bar: 'bar'
+}
+
+let _req
+diagnosticsChannel.channel('undici:request:create').subscribe(({ request }) => {
+ _req = request
+ t.equal(request.completed, false)
+ t.equal(request.method, 'POST')
+ t.equal(request.path, '/')
+ t.equal(request.headers, 'bar: bar\r\n')
+ request.addHeader('hello', 'world')
+ t.equal(request.headers, 'bar: bar\r\nhello: world\r\n')
+ t.same(request.body, Buffer.from('hello world'))
+})
+
+let _connector
+diagnosticsChannel.channel('undici:client:beforeConnect').subscribe(({ connectParams, connector }) => {
+ _connector = connector
+
+ t.equal(typeof _connector, 'function')
+ t.equal(Object.keys(connectParams).length, 6)
+
+ const { host, hostname, protocol, port, servername } = connectParams
+
+ t.equal(host, `localhost:${server.address().port}`)
+ t.equal(hostname, 'localhost')
+ t.equal(port, String(server.address().port))
+ t.equal(protocol, 'http:')
+ t.equal(servername, null)
+})
+
+let _socket
+diagnosticsChannel.channel('undici:client:connected').subscribe(({ connectParams, socket, connector }) => {
+ _socket = socket
+
+ t.equal(Object.keys(connectParams).length, 6)
+ t.equal(_connector, connector)
+
+ const { host, hostname, protocol, port, servername } = connectParams
+
+ t.equal(host, `localhost:${server.address().port}`)
+ t.equal(hostname, 'localhost')
+ t.equal(port, String(server.address().port))
+ t.equal(protocol, 'http:')
+ t.equal(servername, null)
+})
+
+diagnosticsChannel.channel('undici:client:sendHeaders').subscribe(({ request, headers, socket }) => {
+ t.equal(_req, request)
+ t.equal(_socket, socket)
+
+ const expectedHeaders = [
+ 'POST / HTTP/1.1',
+ `host: localhost:${server.address().port}`,
+ 'connection: keep-alive',
+ 'bar: bar',
+ 'hello: world'
+ ]
+
+ t.equal(headers, expectedHeaders.join('\r\n') + '\r\n')
+})
+
+diagnosticsChannel.channel('undici:request:headers').subscribe(({ request, response }) => {
+ t.equal(_req, request)
+ t.equal(response.statusCode, 200)
+ const expectedHeaders = [
+ Buffer.from('Content-Type'),
+ Buffer.from('text/plain'),
+ Buffer.from('trailer'),
+ Buffer.from('foo'),
+ Buffer.from('Date'),
+ response.headers[5], // This is a date
+ Buffer.from('Connection'),
+ Buffer.from('keep-alive'),
+ Buffer.from('Keep-Alive'),
+ Buffer.from('timeout=5'),
+ Buffer.from('Transfer-Encoding'),
+ Buffer.from('chunked')
+ ]
+ t.same(response.headers, expectedHeaders)
+ t.equal(response.statusText, 'OK')
+})
+
+diagnosticsChannel.channel('undici:request:bodySent').subscribe(({ request }) => {
+ t.equal(_req, request)
+})
+
+let endEmitted = false
+diagnosticsChannel.channel('undici:request:trailers').subscribe(({ request, trailers }) => {
+ t.equal(request.completed, true)
+ t.equal(_req, request)
+ // This event is emitted after the last chunk has been added to the body stream,
+ // not when it was consumed by the application
+ t.equal(endEmitted, false)
+ t.same(trailers, [Buffer.from('foo'), Buffer.from('oof')])
+})
+
+server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, {
+ keepAliveTimeout: 300e3
+ })
+ t.teardown(client.close.bind(client))
+
+ client.request({
+ path: '/',
+ method: 'POST',
+ headers: reqHeaders,
+ body: 'hello world'
+ }, (err, data) => {
+ t.error(err)
+ data.body.on('end', function () {
+ endEmitted = true
+ })
+ })
+})
diff --git a/test/dispatcher.js b/test/dispatcher.js
new file mode 100644
index 0000000..22750a1
--- /dev/null
+++ b/test/dispatcher.js
@@ -0,0 +1,22 @@
+'use strict'
+
+const t = require('tap')
+const { test } = t
+
+const Dispatcher = require('../lib/dispatcher')
+
+class PoorImplementation extends Dispatcher {}
+
+test('dispatcher implementation', (t) => {
+ t.plan(6)
+
+ const dispatcher = new Dispatcher()
+ t.throws(() => dispatcher.dispatch(), Error, 'throws on unimplemented dispatch')
+ t.throws(() => dispatcher.close(), Error, 'throws on unimplemented close')
+ t.throws(() => dispatcher.destroy(), Error, 'throws on unimplemented destroy')
+
+ const poorImplementation = new PoorImplementation()
+ t.throws(() => poorImplementation.dispatch(), Error, 'throws on unimplemented dispatch')
+ t.throws(() => poorImplementation.close(), Error, 'throws on unimplemented close')
+ t.throws(() => poorImplementation.destroy(), Error, 'throws on unimplemented destroy')
+})
diff --git a/test/errors.js b/test/errors.js
new file mode 100644
index 0000000..a6f17ef
--- /dev/null
+++ b/test/errors.js
@@ -0,0 +1,81 @@
+'use strict'
+
+const t = require('tap')
+const { test } = t
+
+const errors = require('../lib/core/errors')
+
+const createScenario = (ErrorClass, defaultMessage, name, code) => ({
+ ErrorClass,
+ defaultMessage,
+ name,
+ code
+})
+
+const scenarios = [
+ createScenario(errors.UndiciError, '', 'UndiciError', 'UND_ERR'),
+ createScenario(errors.ConnectTimeoutError, 'Connect Timeout Error', 'ConnectTimeoutError', 'UND_ERR_CONNECT_TIMEOUT'),
+ createScenario(errors.HeadersTimeoutError, 'Headers Timeout Error', 'HeadersTimeoutError', 'UND_ERR_HEADERS_TIMEOUT'),
+ createScenario(errors.HeadersOverflowError, 'Headers Overflow Error', 'HeadersOverflowError', 'UND_ERR_HEADERS_OVERFLOW'),
+ createScenario(errors.InvalidArgumentError, 'Invalid Argument Error', 'InvalidArgumentError', 'UND_ERR_INVALID_ARG'),
+ createScenario(errors.InvalidReturnValueError, 'Invalid Return Value Error', 'InvalidReturnValueError', 'UND_ERR_INVALID_RETURN_VALUE'),
+ createScenario(errors.RequestAbortedError, 'Request aborted', 'AbortError', 'UND_ERR_ABORTED'),
+ createScenario(errors.InformationalError, 'Request information', 'InformationalError', 'UND_ERR_INFO'),
+ createScenario(errors.RequestContentLengthMismatchError, 'Request body length does not match content-length header', 'RequestContentLengthMismatchError', 'UND_ERR_REQ_CONTENT_LENGTH_MISMATCH'),
+ createScenario(errors.ClientDestroyedError, 'The client is destroyed', 'ClientDestroyedError', 'UND_ERR_DESTROYED'),
+ createScenario(errors.ClientClosedError, 'The client is closed', 'ClientClosedError', 'UND_ERR_CLOSED'),
+ createScenario(errors.SocketError, 'Socket error', 'SocketError', 'UND_ERR_SOCKET'),
+ createScenario(errors.NotSupportedError, 'Not supported error', 'NotSupportedError', 'UND_ERR_NOT_SUPPORTED'),
+ createScenario(errors.ResponseContentLengthMismatchError, 'Response body length does not match content-length header', 'ResponseContentLengthMismatchError', 'UND_ERR_RES_CONTENT_LENGTH_MISMATCH'),
+ createScenario(errors.ResponseExceededMaxSizeError, 'Response content exceeded max size', 'ResponseExceededMaxSizeError', 'UND_ERR_RES_EXCEEDED_MAX_SIZE')
+]
+
+scenarios.forEach(scenario => {
+ test(scenario.name, t => {
+ const SAMPLE_MESSAGE = 'sample message'
+
+ const errorWithDefaultMessage = () => new scenario.ErrorClass()
+ const errorWithProvidedMessage = () => new scenario.ErrorClass(SAMPLE_MESSAGE)
+
+ test('should use default message', t => {
+ t.plan(1)
+
+ const error = errorWithDefaultMessage()
+
+ t.equal(error.message, scenario.defaultMessage)
+ })
+
+ test('should use provided message', t => {
+ t.plan(1)
+
+ const error = errorWithProvidedMessage()
+
+ t.equal(error.message, SAMPLE_MESSAGE)
+ })
+
+ test('should have proper fields', t => {
+ t.plan(6)
+ const errorInstances = [errorWithDefaultMessage(), errorWithProvidedMessage()]
+ errorInstances.forEach(error => {
+ t.equal(error.name, scenario.name)
+ t.equal(error.code, scenario.code)
+ t.ok(error.stack)
+ })
+ })
+
+ t.end()
+ })
+})
+
+test('Default HTTPParseError Codes', t => {
+ test('code and data should be undefined when not set', t => {
+ t.plan(2)
+
+ const error = new errors.HTTPParserError('HTTPParserError')
+
+ t.equal(error.code, undefined)
+ t.equal(error.data, undefined)
+ })
+
+ t.end()
+})
diff --git a/test/esm-wrapper.js b/test/esm-wrapper.js
new file mode 100644
index 0000000..a593fbd
--- /dev/null
+++ b/test/esm-wrapper.js
@@ -0,0 +1,19 @@
+'use strict'
+const { nodeMajor, nodeMinor } = require('../lib/core/util')
+
+if (!((nodeMajor > 14 || (nodeMajor === 14 && nodeMajor > 13)) || (nodeMajor === 12 && nodeMinor > 20))) {
+ require('tap') // shows skipped
+} else {
+ ;(async () => {
+ try {
+ await import('./utils/esm-wrapper.mjs')
+ } catch (e) {
+ if (e.message === 'Not supported') {
+ require('tap') // shows skipped
+ return
+ }
+ console.error(e.stack)
+ process.exitCode = 1
+ }
+ })()
+}
diff --git a/test/fetch/407-statuscode-window-null.js b/test/fetch/407-statuscode-window-null.js
new file mode 100644
index 0000000..e22554f
--- /dev/null
+++ b/test/fetch/407-statuscode-window-null.js
@@ -0,0 +1,20 @@
+'use strict'
+
+const { fetch } = require('../..')
+const { createServer } = require('http')
+const { once } = require('events')
+const { test } = require('tap')
+
+test('Receiving a 407 status code w/ a window option present should reject', async (t) => {
+ const server = createServer((req, res) => {
+ res.statusCode = 407
+ res.end()
+ }).listen(0)
+
+ t.teardown(server.close.bind(server))
+ await once(server, 'listening')
+
+ // if init.window exists, the spec tells us to set request.window to 'no-window',
+ // which later causes the request to be rejected if the status code is 407
+ await t.rejects(fetch(`http://localhost:${server.address().port}`, { window: null }))
+})
diff --git a/test/fetch/abort.js b/test/fetch/abort.js
new file mode 100644
index 0000000..e1ca1eb
--- /dev/null
+++ b/test/fetch/abort.js
@@ -0,0 +1,82 @@
+'use strict'
+
+const { test } = require('tap')
+const { fetch } = require('../..')
+const { createServer } = require('http')
+const { once } = require('events')
+const { DOMException } = require('../../lib/fetch/constants')
+const { nodeMajor } = require('../../lib/core/util')
+
+const { AbortController: NPMAbortController } = require('abort-controller')
+
+test('Allow the usage of custom implementation of AbortController', async (t) => {
+ const body = {
+ fixes: 1605
+ }
+
+ const server = createServer((req, res) => {
+ res.statusCode = 200
+ res.end(JSON.stringify(body))
+ })
+
+ t.teardown(server.close.bind(server))
+
+ server.listen(0)
+ await once(server, 'listening')
+
+ const controller = new NPMAbortController()
+ const signal = controller.signal
+ controller.abort()
+
+ try {
+ await fetch(`http://localhost:${server.address().port}`, {
+ signal
+ })
+ } catch (e) {
+ t.equal(e.code, DOMException.ABORT_ERR)
+ }
+})
+
+test('allows aborting with custom errors', { skip: nodeMajor === 16 }, async (t) => {
+ const server = createServer().listen(0)
+
+ t.teardown(server.close.bind(server))
+ await once(server, 'listening')
+
+ t.test('Using AbortSignal.timeout with cause', async (t) => {
+ t.plan(2)
+
+ try {
+ await fetch(`http://localhost:${server.address().port}`, {
+ signal: AbortSignal.timeout(50)
+ })
+ t.fail('should throw')
+ } catch (err) {
+ if (err.name === 'TypeError') {
+ const cause = err.cause
+ t.equal(cause.name, 'HeadersTimeoutError')
+ t.equal(cause.code, 'UND_ERR_HEADERS_TIMEOUT')
+ } else if (err.name === 'TimeoutError') {
+ t.equal(err.code, DOMException.TIMEOUT_ERR)
+ t.equal(err.cause, undefined)
+ } else {
+ t.error(err)
+ }
+ }
+ })
+
+ t.test('Error defaults to an AbortError DOMException', async (t) => {
+ const ac = new AbortController()
+ ac.abort() // no reason
+
+ await t.rejects(
+ fetch(`http://localhost:${server.address().port}`, {
+ signal: ac.signal
+ }),
+ {
+ name: 'AbortError',
+ code: DOMException.ABORT_ERR
+ }
+ )
+ })
+})
diff --git a/test/fetch/abort2.js b/test/fetch/abort2.js
new file mode 100644
index 0000000..5f3853b
--- /dev/null
+++ b/test/fetch/abort2.js
@@ -0,0 +1,60 @@
+'use strict'
+
+const { test } = require('tap')
+const { fetch } = require('../..')
+const { createServer } = require('http')
+const { once } = require('events')
+const { DOMException } = require('../../lib/fetch/constants')
+
+/* global AbortController */
+
+test('parallel fetch with the same AbortController works as expected', async (t) => {
+ const body = {
+ fixes: 1389,
+ bug: 'Ensure request is not aborted before enqueueing bytes into stream.'
+ }
+
+ const server = createServer((req, res) => {
+ res.statusCode = 200
+ res.end(JSON.stringify(body))
+ })
+
+ t.teardown(server.close.bind(server))
+
+ const abortController = new AbortController()
+
+ async function makeRequest () {
+ const result = await fetch(`http://localhost:${server.address().port}`, {
+ signal: abortController.signal
+ }).then(response => response.json())
+
+ abortController.abort()
+ return result
+ }
+
+ server.listen(0)
+ await once(server, 'listening')
+
+ const requests = Array.from({ length: 10 }, makeRequest)
+ const result = await Promise.allSettled(requests)
+
+ // since the requests are running parallel, any of them could resolve first.
+ // therefore we cannot rely on the order of the requests sent.
+ const { resolved, rejected } = result.reduce((a, b) => {
+ if (b.status === 'rejected') {
+ a.rejected.push(b)
+ } else {
+ a.resolved.push(b)
+ }
+
+ return a
+ }, { resolved: [], rejected: [] })
+
+ t.equal(rejected.length, 9) // out of 10 requests, only 1 should succeed
+ t.equal(resolved.length, 1)
+
+ t.ok(rejected.every(rej => rej.reason?.code === DOMException.ABORT_ERR))
+ t.same(resolved[0].value, body)
+
+ t.end()
+})
diff --git a/test/fetch/about-uri.js b/test/fetch/about-uri.js
new file mode 100644
index 0000000..ac9cbf2
--- /dev/null
+++ b/test/fetch/about-uri.js
@@ -0,0 +1,21 @@
+'use strict'
+
+const { test } = require('tap')
+const { fetch } = require('../..')
+
+test('fetching about: uris', async (t) => {
+ t.test('about:blank', async (t) => {
+ await t.rejects(fetch('about:blank'))
+ })
+
+ t.test('All other about: urls should return an error', async (t) => {
+ try {
+ await fetch('about:config')
+ t.fail('fetching about:config should fail')
+ } catch (e) {
+ t.ok(e, 'this error was expected')
+ } finally {
+ t.end()
+ }
+ })
+})
diff --git a/test/fetch/blob-uri.js b/test/fetch/blob-uri.js
new file mode 100644
index 0000000..f9db96c
--- /dev/null
+++ b/test/fetch/blob-uri.js
@@ -0,0 +1,100 @@
+'use strict'
+
+const { test } = require('tap')
+const { fetch } = require('../..')
+const { Blob } = require('buffer')
+
+test('fetching blob: uris', async (t) => {
+ const blobContents = 'hello world'
+ /** @type {import('buffer').Blob} */
+ let blob
+ /** @type {string} */
+ let objectURL
+
+ t.beforeEach(() => {
+ blob = new Blob([blobContents])
+ objectURL = URL.createObjectURL(blob)
+ })
+
+ t.test('a normal fetch request works', async (t) => {
+ const res = await fetch(objectURL)
+
+ t.equal(blobContents, await res.text())
+ t.equal(blob.type, res.headers.get('Content-Type'))
+ t.equal(`${blob.size}`, res.headers.get('Content-Length'))
+ t.end()
+ })
+
+ t.test('non-GET method to blob: fails', async (t) => {
+ try {
+ await fetch(objectURL, {
+ method: 'POST'
+ })
+ t.fail('expected POST to blob: uri to fail')
+ } catch (e) {
+ t.ok(e, 'Got the expected error')
+ } finally {
+ t.end()
+ }
+ })
+
+ // https://github.com/web-platform-tests/wpt/blob/7b0ebaccc62b566a1965396e5be7bb2bc06f841f/FileAPI/url/resources/fetch-tests.js#L36-L41
+ t.test('fetching revoked URL should fail', async (t) => {
+ URL.revokeObjectURL(objectURL)
+
+ try {
+ await fetch(objectURL)
+ t.fail('expected revoked blob: url to fail')
+ } catch (e) {
+ t.ok(e, 'Got the expected error')
+ } finally {
+ t.end()
+ }
+ })
+
+ // https://github.com/web-platform-tests/wpt/blob/7b0ebaccc62b566a1965396e5be7bb2bc06f841f/FileAPI/url/resources/fetch-tests.js#L28-L34
+ t.test('works with a fragment', async (t) => {
+ const res = await fetch(objectURL + '#fragment')
+
+ t.equal(blobContents, await res.text())
+ t.end()
+ })
+
+ // https://github.com/web-platform-tests/wpt/blob/7b0ebaccc62b566a1965396e5be7bb2bc06f841f/FileAPI/url/resources/fetch-tests.js#L52-L56
+ t.test('Appending a query string to blob: url should cause fetch to fail', async (t) => {
+ try {
+ await fetch(objectURL + '?querystring')
+ t.fail('expected ?querystring blob: url to fail')
+ } catch (e) {
+ t.ok(e, 'Got the expected error')
+ } finally {
+ t.end()
+ }
+ })
+
+ // https://github.com/web-platform-tests/wpt/blob/7b0ebaccc62b566a1965396e5be7bb2bc06f841f/FileAPI/url/resources/fetch-tests.js#L58-L62
+ t.test('Appending a path should cause fetch to fail', async (t) => {
+ try {
+ await fetch(objectURL + '/path')
+ t.fail('expected /path blob: url to fail')
+ } catch (e) {
+ t.ok(e, 'Got the expected error')
+ } finally {
+ t.end()
+ }
+ })
+
+ // https://github.com/web-platform-tests/wpt/blob/7b0ebaccc62b566a1965396e5be7bb2bc06f841f/FileAPI/url/resources/fetch-tests.js#L64-L70
+ t.test('these http methods should fail', async (t) => {
+ for (const method of ['HEAD', 'POST', 'DELETE', 'OPTIONS', 'PUT', 'CUSTOM']) {
+ try {
+ await fetch(objectURL, { method })
+ t.fail(`${method} fetch should have failed`)
+ } catch (e) {
+ t.ok(e, `${method} blob url - test succeeded`)
+ }
+ }
+
+ t.end()
+ })
+})
diff --git a/test/fetch/bundle.js b/test/fetch/bundle.js
new file mode 100644
index 0000000..aa1257a
--- /dev/null
+++ b/test/fetch/bundle.js
@@ -0,0 +1,41 @@
+'use strict'
+
+const { test, skip } = require('tap')
+const { nodeMajor } = require('../../lib/core/util')
+
+if (nodeMajor === 16) {
+ skip('esbuild uses static blocks with --keep-names which node 16.8 does not have')
+ process.exit()
+}
+
+const { Response, Request, FormData, Headers } = require('../../undici-fetch')
+
+test('bundle sets constructor.name and .name properly', (t) => {
+ t.equal(new Response().constructor.name, 'Response')
+ t.equal(Response.name, 'Response')
+
+ t.equal(new Request('http://a').constructor.name, 'Request')
+ t.equal(Request.name, 'Request')
+
+ t.equal(new Headers().constructor.name, 'Headers')
+ t.equal(Headers.name, 'Headers')
+
+ t.equal(new FormData().constructor.name, 'FormData')
+ t.equal(FormData.name, 'FormData')
+
+ t.end()
+})
+
+test('regression test for https://github.com/nodejs/node/issues/50263', (t) => {
+ const request = new Request('https://a', {
+ headers: {
+ test: 'abc'
+ },
+ method: 'POST'
+ })
+
+ const request1 = new Request(request, { body: 'does not matter' })
+
+ t.equal(request1.headers.get('test'), 'abc')
+ t.end()
+})
diff --git a/test/fetch/client-error-stack-trace.js b/test/fetch/client-error-stack-trace.js
new file mode 100644
index 0000000..7d94aa8
--- /dev/null
+++ b/test/fetch/client-error-stack-trace.js
@@ -0,0 +1,21 @@
+'use strict'
+
+const { test } = require('tap')
+const { fetch } = require('../..')
+const { fetch: fetchIndex } = require('../../index-fetch')
+
+test('FETCH: request errors and prints trimmed stack trace', async (t) => {
+ try {
+ await fetch('http://a.com')
+ } catch (error) {
+ t.match(error.stack, `at Test.<anonymous> (${__filename}`)
+ }
+})
+
+test('FETCH-index: request errors and prints trimmed stack trace', async (t) => {
+ try {
+ await fetchIndex('http://a.com')
+ } catch (error) {
+ t.match(error.stack, `at Test.<anonymous> (${__filename}`)
+ }
+})
diff --git a/test/fetch/client-fetch.js b/test/fetch/client-fetch.js
new file mode 100644
index 0000000..9009d54
--- /dev/null
+++ b/test/fetch/client-fetch.js
@@ -0,0 +1,688 @@
+/* globals AbortController */
+
+'use strict'
+
+const { test, teardown } = require('tap')
+const { createServer } = require('http')
+const { ReadableStream } = require('stream/web')
+const { Blob } = require('buffer')
+const { fetch, Response, Request, FormData, File } = require('../..')
+const { Client, setGlobalDispatcher, Agent } = require('../..')
+const { nodeMajor, nodeMinor } = require('../../lib/core/util')
+const nodeFetch = require('../../index-fetch')
+const { once } = require('events')
+const { gzipSync } = require('zlib')
+const { promisify } = require('util')
+const { randomFillSync, createHash } = require('crypto')
+
+setGlobalDispatcher(new Agent({
+ keepAliveTimeout: 1,
+ keepAliveMaxTimeout: 1
+}))
+
+test('function signature', (t) => {
+ t.plan(2)
+
+ t.equal(fetch.name, 'fetch')
+ t.equal(fetch.length, 1)
+})
+
+test('args validation', async (t) => {
+ t.plan(2)
+
+ await t.rejects(fetch(), TypeError)
+ await t.rejects(fetch('ftp://unsupported'), TypeError)
+})
+
+test('request json', (t) => {
+ t.plan(1)
+
+ const obj = { asd: true }
+ const server = createServer((req, res) => {
+ res.end(JSON.stringify(obj))
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, async () => {
+ const body = await fetch(`http://localhost:${server.address().port}`)
+ t.strictSame(obj, await body.json())
+ })
+})
+
+test('request text', (t) => {
+ t.plan(1)
+
+ const obj = { asd: true }
+ const server = createServer((req, res) => {
+ res.end(JSON.stringify(obj))
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, async () => {
+ const body = await fetch(`http://localhost:${server.address().port}`)
+ t.strictSame(JSON.stringify(obj), await body.text())
+ })
+})
+
+test('request arrayBuffer', (t) => {
+ t.plan(1)
+
+ const obj = { asd: true }
+ const server = createServer((req, res) => {
+ res.end(JSON.stringify(obj))
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, async () => {
+ const body = await fetch(`http://localhost:${server.address().port}`)
+ t.strictSame(Buffer.from(JSON.stringify(obj)), Buffer.from(await body.arrayBuffer()))
+ })
+})
+
+test('should set type of blob object to the value of the `Content-Type` header from response', (t) => {
+ t.plan(1)
+
+ const obj = { asd: true }
+ const server = createServer((req, res) => {
+ res.setHeader('Content-Type', 'application/json')
+ res.end(JSON.stringify(obj))
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, async () => {
+ const response = await fetch(`http://localhost:${server.address().port}`)
+ t.equal('application/json', (await response.blob()).type)
+ })
+})
+
+test('pre aborted with readable request body', (t) => {
+ t.plan(2)
+
+ const server = createServer((req, res) => {
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, async () => {
+ const ac = new AbortController()
+ ac.abort()
+ await fetch(`http://localhost:${server.address().port}`, {
+ signal: ac.signal,
+ method: 'POST',
+ body: new ReadableStream({
+ async cancel (reason) {
+ t.equal(reason.name, 'AbortError')
+ }
+ }),
+ duplex: 'half'
+ }).catch(err => {
+ t.equal(err.name, 'AbortError')
+ })
+ })
+})
+
+test('pre aborted with closed readable request body', (t) => {
+ t.plan(2)
+
+ const server = createServer((req, res) => {
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, async () => {
+ const ac = new AbortController()
+ ac.abort()
+ const body = new ReadableStream({
+ async start (c) {
+ t.pass()
+ c.close()
+ },
+ async cancel (reason) {
+ t.fail()
+ }
+ })
+ queueMicrotask(() => {
+ fetch(`http://localhost:${server.address().port}`, {
+ signal: ac.signal,
+ method: 'POST',
+ body,
+ duplex: 'half'
+ }).catch(err => {
+ t.equal(err.name, 'AbortError')
+ })
+ })
+ })
+})
+
+test('unsupported formData 1', (t) => {
+ t.plan(1)
+
+ const server = createServer((req, res) => {
+ res.setHeader('content-type', 'asdasdsad')
+ res.end()
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ fetch(`http://localhost:${server.address().port}`)
+ .then(res => res.formData())
+ .catch(err => {
+ t.equal(err.name, 'TypeError')
+ })
+ })
+})
+
+test('multipart formdata not base64', async (t) => {
+ t.plan(2)
+ // Construct example form data, with text and blob fields
+ const formData = new FormData()
+ formData.append('field1', 'value1')
+ const blob = new Blob(['example\ntext file'], { type: 'text/plain' })
+ formData.append('field2', blob, 'file.txt')
+
+ const tempRes = new Response(formData)
+ const boundary = tempRes.headers.get('content-type').split('boundary=')[1]
+ const formRaw = await tempRes.text()
+
+ const server = createServer((req, res) => {
+ res.setHeader('content-type', 'multipart/form-data; boundary=' + boundary)
+ res.write(formRaw)
+ res.end()
+ })
+ t.teardown(server.close.bind(server))
+
+ const listen = promisify(server.listen.bind(server))
+ await listen(0)
+
+ const res = await fetch(`http://localhost:${server.address().port}`)
+ const form = await res.formData()
+ t.equal(form.get('field1'), 'value1')
+
+ const text = await form.get('field2').text()
+ t.equal(text, 'example\ntext file')
+})
+
+// TODO(@KhafraDev): re-enable this test once the issue is fixed
+// See https://github.com/nodejs/node/issues/47301
+test('multipart formdata base64', { skip: nodeMajor >= 19 && nodeMinor >= 8 }, (t) => {
+ t.plan(1)
+
+ // Example form data with base64 encoding
+ const data = randomFillSync(Buffer.alloc(256))
+ const formRaw = `------formdata-undici-0.5786922755719377\r\nContent-Disposition: form-data; name="file"; filename="test.txt"\r\nContent-Type: application/octet-stream\r\nContent-Transfer-Encoding: base64\r\n\r\n${data.toString('base64')}\r\n------formdata-undici-0.5786922755719377--`
+ const server = createServer(async (req, res) => {
+ res.setHeader('content-type', 'multipart/form-data; boundary=----formdata-undici-0.5786922755719377')
+
+ for (let offset = 0; offset < formRaw.length;) {
+ res.write(formRaw.slice(offset, offset += 2))
+ await new Promise(resolve => setTimeout(resolve))
+ }
+ res.end()
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ fetch(`http://localhost:${server.address().port}`)
+ .then(res => res.formData())
+ .then(form => form.get('file').arrayBuffer())
+ .then(buffer => createHash('sha256').update(Buffer.from(buffer)).digest('base64'))
+ .then(digest => {
+ t.equal(createHash('sha256').update(data).digest('base64'), digest)
+ })
+ })
+})
+
+test('multipart fromdata non-ascii filed names', async (t) => {
+ t.plan(1)
+
+ const request = new Request('http://localhost', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'multipart/form-data; boundary=----formdata-undici-0.6204674738279623'
+ },
+ body:
+ '------formdata-undici-0.6204674738279623\r\n' +
+ 'Content-Disposition: form-data; name="fiÅo"\r\n' +
+ '\r\n' +
+ 'value1\r\n' +
+ '------formdata-undici-0.6204674738279623--'
+ })
+
+ const form = await request.formData()
+ t.equal(form.get('fiÅo'), 'value1')
+})
+
+test('busboy emit error', async (t) => {
+ t.plan(1)
+ const formData = new FormData()
+ formData.append('field1', 'value1')
+
+ const tempRes = new Response(formData)
+ const formRaw = await tempRes.text()
+
+ const server = createServer((req, res) => {
+ res.setHeader('content-type', 'multipart/form-data; boundary=wrongboundary')
+ res.write(formRaw)
+ res.end()
+ })
+ t.teardown(server.close.bind(server))
+
+ const listen = promisify(server.listen.bind(server))
+ await listen(0)
+
+ const res = await fetch(`http://localhost:${server.address().port}`)
+ await t.rejects(res.formData(), 'Unexpected end of multipart data')
+})
+
+// https://github.com/nodejs/undici/issues/2244
+test('parsing formData preserve full path on files', async (t) => {
+ t.plan(1)
+ const formData = new FormData()
+ formData.append('field1', new File(['foo'], 'a/b/c/foo.txt'))
+
+ const tempRes = new Response(formData)
+ const form = await tempRes.formData()
+
+ t.equal(form.get('field1').name, 'a/b/c/foo.txt')
+})
+
+test('urlencoded formData', (t) => {
+ t.plan(2)
+
+ const server = createServer((req, res) => {
+ res.setHeader('content-type', 'application/x-www-form-urlencoded')
+ res.end('field1=value1&field2=value2')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ fetch(`http://localhost:${server.address().port}`)
+ .then(res => res.formData())
+ .then(formData => {
+ t.equal(formData.get('field1'), 'value1')
+ t.equal(formData.get('field2'), 'value2')
+ })
+ })
+})
+
+test('text with BOM', (t) => {
+ t.plan(1)
+
+ const server = createServer((req, res) => {
+ res.setHeader('content-type', 'application/x-www-form-urlencoded')
+ res.end('\uFEFFtest=\uFEFF')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ fetch(`http://localhost:${server.address().port}`)
+ .then(res => res.text())
+ .then(text => {
+ t.equal(text, 'test=\uFEFF')
+ })
+ })
+})
+
+test('formData with BOM', (t) => {
+ t.plan(1)
+
+ const server = createServer((req, res) => {
+ res.setHeader('content-type', 'application/x-www-form-urlencoded')
+ res.end('\uFEFFtest=\uFEFF')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ fetch(`http://localhost:${server.address().port}`)
+ .then(res => res.formData())
+ .then(formData => {
+ t.equal(formData.get('\uFEFFtest'), '\uFEFF')
+ })
+ })
+})
+
+test('locked blob body', (t) => {
+ t.plan(1)
+
+ const server = createServer((req, res) => {
+ res.end()
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, async () => {
+ const res = await fetch(`http://localhost:${server.address().port}`)
+ const reader = res.body.getReader()
+ res.blob().catch(err => {
+ t.equal(err.message, 'Body is unusable')
+ reader.cancel()
+ })
+ })
+})
+
+test('disturbed blob body', (t) => {
+ t.plan(2)
+
+ const server = createServer((req, res) => {
+ res.end()
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, async () => {
+ const res = await fetch(`http://localhost:${server.address().port}`)
+ res.blob().then(() => {
+ t.pass(2)
+ })
+ res.blob().catch(err => {
+ t.equal(err.message, 'Body is unusable')
+ })
+ })
+})
+
+test('redirect with body', (t) => {
+ t.plan(3)
+
+ let count = 0
+ const server = createServer(async (req, res) => {
+ let body = ''
+ for await (const chunk of req) {
+ body += chunk
+ }
+ t.equal(body, 'asd')
+ if (count++ === 0) {
+ res.setHeader('location', 'asd')
+ res.statusCode = 302
+ res.end()
+ } else {
+ res.end(String(count))
+ }
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, async () => {
+ const res = await fetch(`http://localhost:${server.address().port}`, {
+ method: 'PUT',
+ body: 'asd'
+ })
+ t.equal(await res.text(), '2')
+ })
+})
+
+test('redirect with stream', (t) => {
+ t.plan(3)
+
+ const location = '/asd'
+ const body = 'hello!'
+ const server = createServer(async (req, res) => {
+ res.writeHead(302, { location })
+ let count = 0
+ const l = setInterval(() => {
+ res.write(body[count++])
+ if (count === body.length) {
+ res.end()
+ clearInterval(l)
+ }
+ }, 50)
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, async () => {
+ const res = await fetch(`http://localhost:${server.address().port}`, {
+ redirect: 'manual'
+ })
+ t.equal(res.status, 302)
+ t.equal(res.headers.get('location'), location)
+ t.equal(await res.text(), body)
+ })
+})
+
+test('fail to extract locked body', (t) => {
+ t.plan(1)
+
+ const stream = new ReadableStream({})
+ const reader = stream.getReader()
+ try {
+ // eslint-disable-next-line
+ new Response(stream)
+ } catch (err) {
+ t.equal(err.name, 'TypeError')
+ }
+ reader.cancel()
+})
+
+test('fail to extract locked body', (t) => {
+ t.plan(1)
+
+ const stream = new ReadableStream({})
+ const reader = stream.getReader()
+ try {
+ // eslint-disable-next-line
+ new Request('http://asd', {
+ method: 'PUT',
+ body: stream,
+ keepalive: true
+ })
+ } catch (err) {
+ t.equal(err.message, 'keepalive')
+ }
+ reader.cancel()
+})
+
+test('post FormData with Blob', (t) => {
+ t.plan(1)
+
+ const body = new FormData()
+ body.append('field1', new Blob(['asd1']))
+
+ const server = createServer((req, res) => {
+ req.pipe(res)
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, async () => {
+ const res = await fetch(`http://localhost:${server.address().port}`, {
+ method: 'PUT',
+ body
+ })
+ t.ok(/asd1/.test(await res.text()))
+ })
+})
+
+test('post FormData with File', (t) => {
+ t.plan(2)
+
+ const body = new FormData()
+ body.append('field1', new File(['asd1'], 'filename123'))
+
+ const server = createServer((req, res) => {
+ req.pipe(res)
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, async () => {
+ const res = await fetch(`http://localhost:${server.address().port}`, {
+ method: 'PUT',
+ body
+ })
+ const result = await res.text()
+ t.ok(/asd1/.test(result))
+ t.ok(/filename123/.test(result))
+ })
+})
+
+test('invalid url', async (t) => {
+ t.plan(1)
+
+ try {
+ await fetch('http://invalid')
+ } catch (e) {
+ t.match(e.cause.message, 'invalid')
+ }
+})
+
+test('custom agent', (t) => {
+ t.plan(2)
+
+ const obj = { asd: true }
+ const server = createServer((req, res) => {
+ res.end(JSON.stringify(obj))
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, async () => {
+ const dispatcher = new Client('http://localhost:' + server.address().port, {
+ keepAliveTimeout: 1,
+ keepAliveMaxTimeout: 1
+ })
+ const oldDispatch = dispatcher.dispatch
+ dispatcher.dispatch = function (options, handler) {
+ t.pass('custom dispatcher')
+ return oldDispatch.call(this, options, handler)
+ }
+ t.teardown(server.close.bind(server))
+ const body = await fetch(`http://localhost:${server.address().port}`, {
+ dispatcher
+ })
+ t.strictSame(obj, await body.json())
+ })
+})
+
+test('custom agent node fetch', (t) => {
+ t.plan(2)
+
+ const obj = { asd: true }
+ const server = createServer((req, res) => {
+ res.end(JSON.stringify(obj))
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, async () => {
+ const dispatcher = new Client('http://localhost:' + server.address().port, {
+ keepAliveTimeout: 1,
+ keepAliveMaxTimeout: 1
+ })
+ const oldDispatch = dispatcher.dispatch
+ dispatcher.dispatch = function (options, handler) {
+ t.pass('custom dispatcher')
+ return oldDispatch.call(this, options, handler)
+ }
+ t.teardown(server.close.bind(server))
+ const body = await nodeFetch.fetch(`http://localhost:${server.address().port}`, {
+ dispatcher
+ })
+ t.strictSame(obj, await body.json())
+ })
+})
+
+test('error on redirect', async (t) => {
+ const server = createServer((req, res) => {
+ res.statusCode = 302
+ res.end()
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, async () => {
+ const errorCause = await fetch(`http://localhost:${server.address().port}`, {
+ redirect: 'error'
+ }).catch((e) => e.cause)
+
+ t.equal(errorCause.message, 'unexpected redirect')
+ })
+})
+
+// https://github.com/nodejs/undici/issues/1527
+test('fetching with Request object - issue #1527', async (t) => {
+ const server = createServer((req, res) => {
+ t.pass()
+ res.end()
+ }).listen(0)
+
+ t.teardown(server.close.bind(server))
+ await once(server, 'listening')
+
+ const body = JSON.stringify({ foo: 'bar' })
+ const request = new Request(`http://localhost:${server.address().port}`, {
+ method: 'POST',
+ body
+ })
+
+ await t.resolves(fetch(request))
+ t.end()
+})
+
+test('do not decode redirect body', (t) => {
+ t.plan(3)
+
+ const obj = { asd: true }
+ const server = createServer((req, res) => {
+ if (req.url === '/resource') {
+ t.pass('we redirect')
+ res.statusCode = 301
+ res.setHeader('location', '/resource/')
+ // Some dumb http servers set the content-encoding gzip
+ // even if there is no response
+ res.setHeader('content-encoding', 'gzip')
+ res.end()
+ return
+ }
+ t.pass('actual response')
+ res.setHeader('content-encoding', 'gzip')
+ res.end(gzipSync(JSON.stringify(obj)))
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, async () => {
+ const body = await fetch(`http://localhost:${server.address().port}/resource`)
+ t.strictSame(JSON.stringify(obj), await body.text())
+ })
+})
+
+test('decode non-redirect body with location header', (t) => {
+ t.plan(2)
+
+ const obj = { asd: true }
+ const server = createServer((req, res) => {
+ t.pass('response')
+ res.statusCode = 201
+ res.setHeader('location', '/resource/')
+ res.setHeader('content-encoding', 'gzip')
+ res.end(gzipSync(JSON.stringify(obj)))
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, async () => {
+ const body = await fetch(`http://localhost:${server.address().port}/resource`)
+ t.strictSame(JSON.stringify(obj), await body.text())
+ })
+})
+
+test('Receiving non-Latin1 headers', async (t) => {
+ const ContentDisposition = [
+ 'inline; filename=rock&roll.png',
+ 'inline; filename="rock\'n\'roll.png"',
+ 'inline; filename="image â\x80\x94 copy (1).png"; filename*=UTF-8\'\'image%20%E2%80%94%20copy%20(1).png',
+ 'inline; filename="_å\x9C\x96ç\x89\x87_ð\x9F\x96¼_image_.png"; filename*=UTF-8\'\'_%E5%9C%96%E7%89%87_%F0%9F%96%BC_image_.png',
+ 'inline; filename="100 % loading&perf.png"; filename*=UTF-8\'\'100%20%25%20loading%26perf.png'
+ ]
+
+ const server = createServer((req, res) => {
+ for (let i = 0; i < ContentDisposition.length; i++) {
+ res.setHeader(`Content-Disposition-${i + 1}`, ContentDisposition[i])
+ }
+
+ res.end()
+ }).listen(0)
+
+ t.teardown(server.close.bind(server))
+ await once(server, 'listening')
+
+ const url = `http://localhost:${server.address().port}`
+ const response = await fetch(url, { method: 'HEAD' })
+ const cdHeaders = [...response.headers]
+ .filter(([k]) => k.startsWith('content-disposition'))
+ .map(([, v]) => v)
+ const lengths = cdHeaders.map(h => h.length)
+
+ t.same(cdHeaders, ContentDisposition)
+ t.same(lengths, [30, 34, 94, 104, 90])
+ t.end()
+})
+
+teardown(() => process.exit())
diff --git a/test/fetch/client-node-max-header-size.js b/test/fetch/client-node-max-header-size.js
new file mode 100644
index 0000000..737bae8
--- /dev/null
+++ b/test/fetch/client-node-max-header-size.js
@@ -0,0 +1,29 @@
+'use strict'
+
+const { execSync } = require('node:child_process')
+const { test, skip } = require('tap')
+const { nodeMajor } = require('../../lib/core/util')
+
+if (nodeMajor === 16) {
+ skip('esbuild uses static blocks with --keep-names which node 16.8 does not have')
+ process.exit()
+}
+
+const command = 'node -e "require(\'./undici-fetch.js\').fetch(\'https://httpbin.org/get\')"'
+
+test("respect Node.js' --max-http-header-size", async (t) => {
+ t.throws(
+ // TODO: Drop the `--unhandled-rejections=throw` once we drop Node.js 14
+ () => execSync(`${command} --max-http-header-size=1 --unhandled-rejections=throw`),
+ /UND_ERR_HEADERS_OVERFLOW/,
+ 'max-http-header-size=1 should throw'
+ )
+
+ t.doesNotThrow(
+ () => execSync(command),
+ /UND_ERR_HEADERS_OVERFLOW/,
+ 'default max-http-header-size should not throw'
+ )
+
+ t.end()
+})
diff --git a/test/fetch/content-length.js b/test/fetch/content-length.js
new file mode 100644
index 0000000..9264091
--- /dev/null
+++ b/test/fetch/content-length.js
@@ -0,0 +1,29 @@
+'use strict'
+
+const { test } = require('tap')
+const { createServer } = require('http')
+const { once } = require('events')
+const { Blob } = require('buffer')
+const { fetch, FormData } = require('../..')
+
+// https://github.com/nodejs/undici/issues/1783
+test('Content-Length is set when using a FormData body with fetch', async (t) => {
+ const server = createServer((req, res) => {
+ // TODO: check the length's value once the boundary has a fixed length
+ t.ok('content-length' in req.headers) // request has content-length header
+ t.ok(!Number.isNaN(Number(req.headers['content-length'])))
+ res.end()
+ }).listen(0)
+
+ await once(server, 'listening')
+ t.teardown(server.close.bind(server))
+
+ const fd = new FormData()
+ fd.set('file', new Blob(['hello world 👋'], { type: 'text/plain' }), 'readme.md')
+ fd.set('string', 'some string value')
+
+ await fetch(`http://localhost:${server.address().port}`, {
+ method: 'POST',
+ body: fd
+ })
+})
diff --git a/test/fetch/cookies.js b/test/fetch/cookies.js
new file mode 100644
index 0000000..18b001d
--- /dev/null
+++ b/test/fetch/cookies.js
@@ -0,0 +1,69 @@
+'use strict'
+
+const { once } = require('events')
+const { createServer } = require('http')
+const { test } = require('tap')
+const { fetch, Headers } = require('../..')
+
+test('Can receive set-cookie headers from a server using fetch - issue #1262', async (t) => {
+ const server = createServer((req, res) => {
+ res.setHeader('set-cookie', 'name=value; Domain=example.com')
+ res.end()
+ }).listen(0)
+
+ t.teardown(server.close.bind(server))
+ await once(server, 'listening')
+
+ const response = await fetch(`http://localhost:${server.address().port}`)
+
+ t.equal(response.headers.get('set-cookie'), 'name=value; Domain=example.com')
+
+ const response2 = await fetch(`http://localhost:${server.address().port}`, {
+ credentials: 'include'
+ })
+
+ t.equal(response2.headers.get('set-cookie'), 'name=value; Domain=example.com')
+
+ t.end()
+})
+
+test('Can send cookies to a server with fetch - issue #1463', async (t) => {
+ const server = createServer((req, res) => {
+ t.equal(req.headers.cookie, 'value')
+ res.end()
+ }).listen(0)
+
+ t.teardown(server.close.bind(server))
+ await once(server, 'listening')
+
+ const headersInit = [
+ new Headers([['cookie', 'value']]),
+ { cookie: 'value' },
+ [['cookie', 'value']]
+ ]
+
+ for (const headers of headersInit) {
+ await fetch(`http://localhost:${server.address().port}`, { headers })
+ }
+
+ t.end()
+})
+
+test('Cookie header is delimited with a semicolon rather than a comma - issue #1905', async (t) => {
+ t.plan(1)
+
+ const server = createServer((req, res) => {
+ t.equal(req.headers.cookie, 'FOO=lorem-ipsum-dolor-sit-amet; BAR=the-quick-brown-fox')
+ res.end()
+ }).listen(0)
+
+ t.teardown(server.close.bind(server))
+ await once(server, 'listening')
+
+ await fetch(`http://localhost:${server.address().port}`, {
+ headers: [
+ ['cookie', 'FOO=lorem-ipsum-dolor-sit-amet'],
+ ['cookie', 'BAR=the-quick-brown-fox']
+ ]
+ })
+})
diff --git a/test/fetch/data-uri.js b/test/fetch/data-uri.js
new file mode 100644
index 0000000..6191bfe
--- /dev/null
+++ b/test/fetch/data-uri.js
@@ -0,0 +1,214 @@
+'use strict'
+
+const { test } = require('tap')
+const {
+ URLSerializer,
+ collectASequenceOfCodePoints,
+ stringPercentDecode,
+ parseMIMEType,
+ collectAnHTTPQuotedString
+} = require('../../lib/fetch/dataURL')
+const { fetch } = require('../..')
+
+test('https://url.spec.whatwg.org/#concept-url-serializer', (t) => {
+ t.test('url scheme gets appended', (t) => {
+ const url = new URL('https://www.google.com/')
+ const serialized = URLSerializer(url)
+
+ t.ok(serialized.startsWith(url.protocol))
+ t.end()
+ })
+
+ t.test('non-null url host with authentication', (t) => {
+ const url = new URL('https://username:password@google.com')
+ const serialized = URLSerializer(url)
+
+ t.ok(serialized.includes(`//${url.username}:${url.password}`))
+ t.ok(serialized.endsWith('@google.com/'))
+ t.end()
+ })
+
+ t.test('null url host', (t) => {
+ for (const url of ['web+demo:/.//not-a-host/', 'web+demo:/path/..//not-a-host/']) {
+ t.equal(
+ URLSerializer(new URL(url)),
+ 'web+demo:/.//not-a-host/'
+ )
+ }
+
+ t.end()
+ })
+
+ t.test('url with query works', (t) => {
+ t.equal(
+ URLSerializer(new URL('https://www.google.com/?fetch=undici')),
+ 'https://www.google.com/?fetch=undici'
+ )
+
+ t.end()
+ })
+
+ t.test('exclude fragment', (t) => {
+ t.equal(
+ URLSerializer(new URL('https://www.google.com/#frag')),
+ 'https://www.google.com/#frag'
+ )
+
+ t.equal(
+ URLSerializer(new URL('https://www.google.com/#frag'), true),
+ 'https://www.google.com/'
+ )
+
+ t.end()
+ })
+
+ t.end()
+})
+
+test('https://infra.spec.whatwg.org/#collect-a-sequence-of-code-points', (t) => {
+ const input = 'text/plain;base64,'
+ const position = { position: 0 }
+ const result = collectASequenceOfCodePoints(
+ (char) => char !== ';',
+ input,
+ position
+ )
+
+ t.strictSame(result, 'text/plain')
+ t.strictSame(position.position, input.indexOf(';'))
+ t.end()
+})
+
+test('https://url.spec.whatwg.org/#string-percent-decode', (t) => {
+ t.test('encodes %{2} in range properly', (t) => {
+ const input = '%FF'
+ const percentDecoded = stringPercentDecode(input)
+
+ t.same(percentDecoded, new Uint8Array([255]))
+ t.end()
+ })
+
+ t.test('encodes %{2} not in range properly', (t) => {
+ const input = 'Hello %XD World'
+ const percentDecoded = stringPercentDecode(input)
+ const expected = [...input].map(c => c.charCodeAt(0))
+
+ t.same(percentDecoded, expected)
+ t.end()
+ })
+
+ t.test('normal string works', (t) => {
+ const input = 'Hello world'
+ const percentDecoded = stringPercentDecode(input)
+ const expected = [...input].map(c => c.charCodeAt(0))
+
+ t.same(percentDecoded, Uint8Array.from(expected))
+ t.end()
+ })
+
+ t.end()
+})
+
+test('https://mimesniff.spec.whatwg.org/#parse-a-mime-type', (t) => {
+ t.same(parseMIMEType('text/plain'), {
+ type: 'text',
+ subtype: 'plain',
+ parameters: new Map(),
+ essence: 'text/plain'
+ })
+
+ t.same(parseMIMEType('text/html;charset="shift_jis"iso-2022-jp'), {
+ type: 'text',
+ subtype: 'html',
+ parameters: new Map([['charset', 'shift_jis']]),
+ essence: 'text/html'
+ })
+
+ t.same(parseMIMEType('application/javascript'), {
+ type: 'application',
+ subtype: 'javascript',
+ parameters: new Map(),
+ essence: 'application/javascript'
+ })
+
+ t.end()
+})
+
+test('https://fetch.spec.whatwg.org/#collect-an-http-quoted-string', (t) => {
+ // https://fetch.spec.whatwg.org/#example-http-quoted-string
+ t.test('first', (t) => {
+ const position = { position: 0 }
+
+ t.strictSame(collectAnHTTPQuotedString('"\\', {
+ position: 0
+ }), '"\\')
+ t.strictSame(collectAnHTTPQuotedString('"\\', position, true), '\\')
+ t.strictSame(position.position, 2)
+ t.end()
+ })
+
+ t.test('second', (t) => {
+ const position = { position: 0 }
+ const input = '"Hello" World'
+
+ t.strictSame(collectAnHTTPQuotedString(input, {
+ position: 0
+ }), '"Hello"')
+ t.strictSame(collectAnHTTPQuotedString(input, position, true), 'Hello')
+ t.strictSame(position.position, 7)
+ t.end()
+ })
+
+ t.end()
+})
+
+// https://github.com/nodejs/undici/issues/1574
+test('too long base64 url', async (t) => {
+ const inputStr = 'a'.repeat(1 << 20)
+ const base64 = Buffer.from(inputStr).toString('base64')
+ const dataURIPrefix = 'data:application/octet-stream;base64,'
+ const dataURL = dataURIPrefix + base64
+ try {
+ const res = await fetch(dataURL)
+ const buf = await res.arrayBuffer()
+ const outputStr = Buffer.from(buf).toString('ascii')
+ t.same(outputStr, inputStr)
+ } catch (e) {
+ t.fail(`failed to fetch ${dataURL}`)
+ }
+})
+
+test('https://domain.com/#', (t) => {
+ t.plan(1)
+ const domain = 'https://domain.com/#a'
+ const serialized = URLSerializer(new URL(domain))
+ t.equal(serialized, domain)
+})
+
+test('https://domain.com/?', (t) => {
+ t.plan(1)
+ const domain = 'https://domain.com/?a=b'
+ const serialized = URLSerializer(new URL(domain))
+ t.equal(serialized, domain)
+})
+
+// https://github.com/nodejs/undici/issues/2474
+test('hash url', (t) => {
+ t.plan(1)
+ const domain = 'https://domain.com/#a#b'
+ const url = new URL(domain)
+ const serialized = URLSerializer(url, true)
+ t.equal(serialized, url.href.substring(0, url.href.length - url.hash.length))
+})
+
+// https://github.com/nodejs/undici/issues/2474
+test('data url that includes the hash', async (t) => {
+ t.plan(1)
+ const dataURL = 'data:,node#js#'
+ try {
+ const res = await fetch(dataURL)
+ t.equal(await res.text(), 'node')
+ } catch (error) {
+ t.fail(`failed to fetch ${dataURL}`)
+ }
+})
diff --git a/test/fetch/encoding.js b/test/fetch/encoding.js
new file mode 100644
index 0000000..75d8fc3
--- /dev/null
+++ b/test/fetch/encoding.js
@@ -0,0 +1,58 @@
+'use strict'
+
+const { test } = require('tap')
+const { createServer } = require('http')
+const { once } = require('events')
+const { fetch } = require('../..')
+const { createBrotliCompress, createGzip, createDeflate } = require('zlib')
+
+test('content-encoding header is case-iNsENsITIve', async (t) => {
+ const contentCodings = 'GZiP, bR'
+ const text = 'Hello, World!'
+
+ const server = createServer((req, res) => {
+ const gzip = createGzip()
+ const brotli = createBrotliCompress()
+
+ res.setHeader('Content-Encoding', contentCodings)
+ res.setHeader('Content-Type', 'text/plain')
+
+ brotli.pipe(gzip).pipe(res)
+
+ brotli.write(text)
+ brotli.end()
+ }).listen(0)
+
+ t.teardown(server.close.bind(server))
+ await once(server, 'listening')
+
+ const response = await fetch(`http://localhost:${server.address().port}`)
+
+ t.equal(await response.text(), text)
+ t.equal(response.headers.get('content-encoding'), contentCodings)
+})
+
+test('response decompression according to content-encoding should be handled in a correct order', async (t) => {
+ const contentCodings = 'deflate, gzip'
+ const text = 'Hello, World!'
+
+ const server = createServer((req, res) => {
+ const gzip = createGzip()
+ const deflate = createDeflate()
+
+ res.setHeader('Content-Encoding', contentCodings)
+ res.setHeader('Content-Type', 'text/plain')
+
+ gzip.pipe(deflate).pipe(res)
+
+ gzip.write(text)
+ gzip.end()
+ }).listen(0)
+
+ t.teardown(server.close.bind(server))
+ await once(server, 'listening')
+
+ const response = await fetch(`http://localhost:${server.address().port}`)
+
+ t.equal(await response.text(), text)
+})
diff --git a/test/fetch/fetch-leak.js b/test/fetch/fetch-leak.js
new file mode 100644
index 0000000..b8e6b16
--- /dev/null
+++ b/test/fetch/fetch-leak.js
@@ -0,0 +1,44 @@
+'use strict'
+
+const { test } = require('tap')
+const { fetch } = require('../..')
+const { createServer } = require('http')
+
+test('do not leak', (t) => {
+ t.plan(1)
+
+ const server = createServer((req, res) => {
+ res.end()
+ })
+ t.teardown(server.close.bind(server))
+
+ let url
+ let done = false
+ server.listen(0, function attack () {
+ if (done) {
+ return
+ }
+ url ??= new URL(`http://127.0.0.1:${server.address().port}`)
+ const controller = new AbortController()
+ fetch(url, { signal: controller.signal })
+ .then(res => res.arrayBuffer())
+ .catch(() => {})
+ .then(attack)
+ })
+
+ let prev = Infinity
+ let count = 0
+ const interval = setInterval(() => {
+ done = true
+ global.gc()
+ const next = process.memoryUsage().heapUsed
+ if (next <= prev) {
+ t.pass()
+ } else if (count++ > 20) {
+ t.fail()
+ } else {
+ prev = next
+ }
+ }, 1e3)
+ t.teardown(() => clearInterval(interval))
+})
diff --git a/test/fetch/fetch-timeouts.js b/test/fetch/fetch-timeouts.js
new file mode 100644
index 0000000..b659aaa
--- /dev/null
+++ b/test/fetch/fetch-timeouts.js
@@ -0,0 +1,56 @@
+'use strict'
+
+const { test } = require('tap')
+
+const { fetch, Agent } = require('../..')
+const timers = require('../../lib/timers')
+const { createServer } = require('http')
+const FakeTimers = require('@sinonjs/fake-timers')
+
+test('Fetch very long request, timeout overridden so no error', (t) => {
+ const minutes = 6
+ const msToDelay = 1000 * 60 * minutes
+
+ t.setTimeout(undefined)
+ t.plan(1)
+
+ const clock = FakeTimers.install()
+ t.teardown(clock.uninstall.bind(clock))
+
+ const orgTimers = { ...timers }
+ Object.assign(timers, { setTimeout, clearTimeout })
+ t.teardown(() => {
+ Object.assign(timers, orgTimers)
+ })
+
+ const server = createServer((req, res) => {
+ setTimeout(() => {
+ res.end('hello')
+ }, msToDelay)
+ clock.tick(msToDelay + 1)
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ fetch(`http://localhost:${server.address().port}`, {
+ path: '/',
+ method: 'GET',
+ dispatcher: new Agent({
+ headersTimeout: 0,
+ connectTimeout: 0,
+ bodyTimeout: 0
+ })
+ })
+ .then((response) => response.text())
+ .then((response) => {
+ t.equal('hello', response)
+ t.end()
+ })
+ .catch((err) => {
+ // This should not happen, a timeout error should not occur
+ t.error(err)
+ })
+
+ clock.tick(msToDelay - 1)
+ })
+})
diff --git a/test/fetch/file.js b/test/fetch/file.js
new file mode 100644
index 0000000..5901541
--- /dev/null
+++ b/test/fetch/file.js
@@ -0,0 +1,190 @@
+'use strict'
+
+const { Blob } = require('buffer')
+const { test } = require('tap')
+const { File, FileLike } = require('../../lib/fetch/file')
+
+test('args validation', (t) => {
+ t.plan(14)
+
+ t.throws(() => {
+ File.prototype.name.toString()
+ }, TypeError)
+ t.throws(() => {
+ File.prototype.lastModified.toString()
+ }, TypeError)
+ t.doesNotThrow(() => {
+ File.prototype[Symbol.toStringTag].charAt(0)
+ }, TypeError)
+
+ t.throws(() => {
+ FileLike.prototype.stream.call(null)
+ }, TypeError)
+ t.throws(() => {
+ FileLike.prototype.arrayBuffer.call(null)
+ }, TypeError)
+ t.throws(() => {
+ FileLike.prototype.slice.call(null)
+ }, TypeError)
+ t.throws(() => {
+ FileLike.prototype.text.call(null)
+ }, TypeError)
+ t.throws(() => {
+ FileLike.prototype.size.toString()
+ }, TypeError)
+ t.throws(() => {
+ FileLike.prototype.type.toString()
+ }, TypeError)
+ t.throws(() => {
+ FileLike.prototype.name.toString()
+ }, TypeError)
+ t.throws(() => {
+ FileLike.prototype.lastModified.toString()
+ }, TypeError)
+ t.doesNotThrow(() => {
+ FileLike.prototype[Symbol.toStringTag].charAt(0)
+ }, TypeError)
+
+ t.equal(File.prototype[Symbol.toStringTag], 'File')
+ t.equal(FileLike.prototype[Symbol.toStringTag], 'File')
+})
+
+test('return value of File.lastModified', (t) => {
+ t.plan(2)
+
+ const f = new File(['asd1'], 'filename123')
+ const lastModified = f.lastModified
+ t.ok(typeof lastModified === typeof Date.now())
+ t.ok(lastModified >= 0 && lastModified <= Date.now())
+})
+
+test('Symbol.toStringTag', (t) => {
+ t.plan(2)
+ t.equal(new File([], '')[Symbol.toStringTag], 'File')
+ t.equal(new FileLike()[Symbol.toStringTag], 'File')
+})
+
+test('arguments', (t) => {
+ t.throws(() => {
+ new File() // eslint-disable-line no-new
+ }, TypeError)
+
+ t.throws(() => {
+ new File([]) // eslint-disable-line no-new
+ }, TypeError)
+
+ t.end()
+})
+
+test('lastModified', (t) => {
+ const file = new File([], '')
+ const lastModified = Date.now() - 69_000
+
+ t.notOk(file === 0)
+
+ const file1 = new File([], '', { lastModified })
+ t.equal(file1.lastModified, lastModified)
+
+ t.equal(new File([], '', { lastModified: 0 }).lastModified, 0)
+
+ t.equal(
+ new File([], '', {
+ lastModified: true
+ }).lastModified,
+ 1
+ )
+
+ t.end()
+})
+
+test('File.prototype.text', async (t) => {
+ t.test('With Blob', async (t) => {
+ const blob1 = new Blob(['hello'])
+ const blob2 = new Blob([' '])
+ const blob3 = new Blob(['world'])
+
+ const file = new File([blob1, blob2, blob3], 'hello_world.txt')
+
+ t.equal(await file.text(), 'hello world')
+ t.end()
+ })
+
+ /* eslint-disable camelcase */
+ t.test('With TypedArray', async (t) => {
+ const uint8_1 = new Uint8Array(Buffer.from('hello'))
+ const uint8_2 = new Uint8Array(Buffer.from(' '))
+ const uint8_3 = new Uint8Array(Buffer.from('world'))
+
+ const file = new File([uint8_1, uint8_2, uint8_3], 'hello_world.txt')
+
+ t.equal(await file.text(), 'hello world')
+ t.end()
+ })
+
+ t.test('With TypedArray range', async (t) => {
+ const uint8_1 = new Uint8Array(Buffer.from('hello world'))
+ const uint8_2 = new Uint8Array(uint8_1.buffer, 1, 4)
+
+ const file = new File([uint8_2], 'hello_world.txt')
+
+ t.equal(await file.text(), 'ello')
+ t.end()
+ })
+ /* eslint-enable camelcase */
+
+ t.test('With ArrayBuffer', async (t) => {
+ const uint8 = new Uint8Array([65, 66, 67])
+ const ab = uint8.buffer
+
+ const file = new File([ab], 'file.txt')
+
+ t.equal(await file.text(), 'ABC')
+ t.end()
+ })
+
+ t.test('With string', async (t) => {
+ const string = 'hello world'
+ const file = new File([string], 'hello_world.txt')
+
+ t.equal(await file.text(), 'hello world')
+ t.end()
+ })
+
+ t.test('With Buffer', async (t) => {
+ const buffer = Buffer.from('hello world')
+
+ const file = new File([buffer], 'hello_world.txt')
+
+ t.equal(await file.text(), 'hello world')
+ t.end()
+ })
+
+ t.test('Mixed', async (t) => {
+ const blob = new Blob(['Hello, '])
+ const uint8 = new Uint8Array(Buffer.from('world! This'))
+ const string = ' is a test! Hope it passes!'
+
+ const file = new File([blob, uint8, string], 'mixed-messages.txt')
+
+ t.equal(
+ await file.text(),
+ 'Hello, world! This is a test! Hope it passes!'
+ )
+ t.end()
+ })
+
+ t.end()
+})
+
+test('endings=native', async (t) => {
+ const file = new File(['Hello\nWorld'], 'text.txt', { endings: 'native' })
+ const text = await file.text()
+
+ if (process.platform === 'win32') {
+ t.equal(text, 'Hello\r\nWorld', 'on windows, LF is replace with CRLF')
+ } else {
+ t.equal(text, 'Hello\nWorld', `on ${process.platform} LF stays LF`)
+ }
+
+ t.end()
+})
diff --git a/test/fetch/formdata.js b/test/fetch/formdata.js
new file mode 100644
index 0000000..fed95bf
--- /dev/null
+++ b/test/fetch/formdata.js
@@ -0,0 +1,401 @@
+'use strict'
+
+const { test } = require('tap')
+const { FormData, File, Response } = require('../../')
+const { Blob: ThirdPartyBlob } = require('formdata-node')
+const { Blob } = require('buffer')
+const { isFormDataLike } = require('../../lib/core/util')
+const ThirdPartyFormDataInvalid = require('form-data')
+
+test('arg validation', (t) => {
+ const form = new FormData()
+
+ // constructor
+ t.throws(() => {
+ // eslint-disable-next-line
+ new FormData('asd')
+ }, TypeError)
+
+ // append
+ t.throws(() => {
+ FormData.prototype.append.call(null)
+ }, TypeError)
+ t.throws(() => {
+ form.append()
+ }, TypeError)
+ t.throws(() => {
+ form.append('k', 'not usv', '')
+ }, TypeError)
+
+ // delete
+ t.throws(() => {
+ FormData.prototype.delete.call(null)
+ }, TypeError)
+ t.throws(() => {
+ form.delete()
+ }, TypeError)
+
+ // get
+ t.throws(() => {
+ FormData.prototype.get.call(null)
+ }, TypeError)
+ t.throws(() => {
+ form.get()
+ }, TypeError)
+
+ // getAll
+ t.throws(() => {
+ FormData.prototype.getAll.call(null)
+ }, TypeError)
+ t.throws(() => {
+ form.getAll()
+ }, TypeError)
+
+ // has
+ t.throws(() => {
+ FormData.prototype.has.call(null)
+ }, TypeError)
+ t.throws(() => {
+ form.has()
+ }, TypeError)
+
+ // set
+ t.throws(() => {
+ FormData.prototype.set.call(null)
+ }, TypeError)
+ t.throws(() => {
+ form.set('k')
+ }, TypeError)
+ t.throws(() => {
+ form.set('k', 'not usv', '')
+ }, TypeError)
+
+ // iterator
+ t.throws(() => {
+ Reflect.apply(FormData.prototype[Symbol.iterator], null)
+ }, TypeError)
+
+ // toStringTag
+ t.doesNotThrow(() => {
+ FormData.prototype[Symbol.toStringTag].charAt(0)
+ })
+
+ t.end()
+})
+
+test('append file', (t) => {
+ const form = new FormData()
+ form.set('asd', new File([], 'asd1', { type: 'text/plain' }), 'asd2')
+ form.append('asd2', new File([], 'asd1'), 'asd2')
+
+ t.equal(form.has('asd'), true)
+ t.equal(form.has('asd2'), true)
+ t.equal(form.get('asd').name, 'asd2')
+ t.equal(form.get('asd2').name, 'asd2')
+ t.equal(form.get('asd').type, 'text/plain')
+ form.delete('asd')
+ t.equal(form.get('asd'), null)
+ t.equal(form.has('asd2'), true)
+ t.equal(form.has('asd'), false)
+
+ t.end()
+})
+
+test('append blob', async (t) => {
+ const form = new FormData()
+ form.set('asd', new Blob(['asd1'], { type: 'text/plain' }))
+
+ t.equal(form.has('asd'), true)
+ t.equal(form.get('asd').type, 'text/plain')
+ t.equal(await form.get('asd').text(), 'asd1')
+ form.delete('asd')
+ t.equal(form.get('asd'), null)
+
+ t.end()
+})
+
+test('append third-party blob', async (t) => {
+ const form = new FormData()
+ form.set('asd', new ThirdPartyBlob(['asd1'], { type: 'text/plain' }))
+
+ t.equal(form.has('asd'), true)
+ t.equal(form.get('asd').type, 'text/plain')
+ t.equal(await form.get('asd').text(), 'asd1')
+ form.delete('asd')
+ t.equal(form.get('asd'), null)
+
+ t.end()
+})
+
+test('append string', (t) => {
+ const form = new FormData()
+ form.set('k1', 'v1')
+ form.set('k2', 'v2')
+ t.same([...form], [['k1', 'v1'], ['k2', 'v2']])
+ t.equal(form.has('k1'), true)
+ t.equal(form.get('k1'), 'v1')
+ form.append('k1', 'v1+')
+ t.same(form.getAll('k1'), ['v1', 'v1+'])
+ form.set('k2', 'v1++')
+ t.equal(form.get('k2'), 'v1++')
+ form.delete('asd')
+ t.equal(form.get('asd'), null)
+ t.end()
+})
+
+test('formData.entries', (t) => {
+ t.plan(2)
+ const form = new FormData()
+
+ t.test('with 0 entries', (t) => {
+ t.plan(1)
+
+ const entries = [...form.entries()]
+ t.same(entries, [])
+ })
+
+ t.test('with 1+ entries', (t) => {
+ t.plan(2)
+
+ form.set('k1', 'v1')
+ form.set('k2', 'v2')
+
+ const entries = [...form.entries()]
+ const entries2 = [...form.entries()]
+ t.same(entries, [['k1', 'v1'], ['k2', 'v2']])
+ t.same(entries, entries2)
+ })
+})
+
+test('formData.keys', (t) => {
+ t.plan(2)
+ const form = new FormData()
+
+ t.test('with 0 keys', (t) => {
+ t.plan(1)
+
+ const keys = [...form.entries()]
+ t.same(keys, [])
+ })
+
+ t.test('with 1+ keys', (t) => {
+ t.plan(2)
+
+ form.set('k1', 'v1')
+ form.set('k2', 'v2')
+
+ const keys = [...form.keys()]
+ const keys2 = [...form.keys()]
+ t.same(keys, ['k1', 'k2'])
+ t.same(keys, keys2)
+ })
+})
+
+test('formData.values', (t) => {
+ t.plan(2)
+ const form = new FormData()
+
+ t.test('with 0 values', (t) => {
+ t.plan(1)
+
+ const values = [...form.values()]
+ t.same(values, [])
+ })
+
+ t.test('with 1+ values', (t) => {
+ t.plan(2)
+
+ form.set('k1', 'v1')
+ form.set('k2', 'v2')
+
+ const values = [...form.values()]
+ const values2 = [...form.values()]
+ t.same(values, ['v1', 'v2'])
+ t.same(values, values2)
+ })
+})
+
+test('formData forEach', (t) => {
+ t.test('invalid arguments', (t) => {
+ t.throws(() => {
+ FormData.prototype.forEach.call({})
+ }, TypeError('Illegal invocation'))
+
+ t.throws(() => {
+ const fd = new FormData()
+
+ fd.forEach({})
+ }, TypeError)
+
+ t.end()
+ })
+
+ t.test('with a callback', (t) => {
+ const fd = new FormData()
+
+ fd.set('a', 'b')
+ fd.set('c', 'd')
+
+ let i = 0
+ fd.forEach((value, key, self) => {
+ if (i++ === 0) {
+ t.equal(value, 'b')
+ t.equal(key, 'a')
+ } else {
+ t.equal(value, 'd')
+ t.equal(key, 'c')
+ }
+
+ t.equal(fd, self)
+ })
+
+ t.end()
+ })
+
+ t.test('with a thisArg', (t) => {
+ const fd = new FormData()
+ fd.set('b', 'a')
+
+ fd.forEach(function (value, key, self) {
+ t.equal(this, globalThis)
+ t.equal(fd, self)
+ t.equal(key, 'b')
+ t.equal(value, 'a')
+ })
+
+ const thisArg = Symbol('thisArg')
+ fd.forEach(function () {
+ t.equal(this, thisArg)
+ }, thisArg)
+
+ t.end()
+ })
+
+ t.end()
+})
+
+test('formData toStringTag', (t) => {
+ const form = new FormData()
+ t.equal(form[Symbol.toStringTag], 'FormData')
+ t.equal(FormData.prototype[Symbol.toStringTag], 'FormData')
+ t.end()
+})
+
+test('formData.constructor.name', (t) => {
+ const form = new FormData()
+ t.equal(form.constructor.name, 'FormData')
+ t.end()
+})
+
+test('formData should be an instance of FormData', (t) => {
+ t.plan(3)
+
+ t.test('Invalid class FormData', (t) => {
+ class FormData {
+ constructor () {
+ this.data = []
+ }
+
+ append (key, value) {
+ this.data.push([key, value])
+ }
+
+ get (key) {
+ return this.data.find(([k]) => k === key)
+ }
+ }
+
+ const form = new FormData()
+ t.equal(isFormDataLike(form), false)
+ t.end()
+ })
+
+ t.test('Invalid function FormData', (t) => {
+ function FormData () {
+ const data = []
+ return {
+ append (key, value) {
+ data.push([key, value])
+ },
+ get (key) {
+ return data.find(([k]) => k === key)
+ }
+ }
+ }
+
+ const form = new FormData()
+ t.equal(isFormDataLike(form), false)
+ t.end()
+ })
+
+ test('Invalid third-party FormData', (t) => {
+ const form = new ThirdPartyFormDataInvalid()
+ t.equal(isFormDataLike(form), false)
+ t.end()
+ })
+
+ t.test('Valid FormData', (t) => {
+ const form = new FormData()
+ t.equal(isFormDataLike(form), true)
+ t.end()
+ })
+})
+
+test('FormData should be compatible with third-party libraries', (t) => {
+ t.plan(1)
+
+ class FormData {
+ constructor () {
+ this.data = []
+ }
+
+ get [Symbol.toStringTag] () {
+ return 'FormData'
+ }
+
+ append () {}
+ delete () {}
+ get () {}
+ getAll () {}
+ has () {}
+ set () {}
+ entries () {}
+ keys () {}
+ values () {}
+ forEach () {}
+ }
+
+ const form = new FormData()
+ t.equal(isFormDataLike(form), true)
+})
+
+test('arguments', (t) => {
+ t.equal(FormData.constructor.length, 1)
+ t.equal(FormData.prototype.append.length, 2)
+ t.equal(FormData.prototype.delete.length, 1)
+ t.equal(FormData.prototype.get.length, 1)
+ t.equal(FormData.prototype.getAll.length, 1)
+ t.equal(FormData.prototype.has.length, 1)
+ t.equal(FormData.prototype.set.length, 2)
+
+ t.end()
+})
+
+// https://github.com/nodejs/undici/pull/1814
+test('FormData returned from bodyMixin.formData is not a clone', async (t) => {
+ const fd = new FormData()
+ fd.set('foo', 'bar')
+
+ const res = new Response(fd)
+ fd.set('foo', 'foo')
+
+ const fd2 = await res.formData()
+
+ t.equal(fd2.get('foo'), 'bar')
+ t.equal(fd.get('foo'), 'foo')
+
+ fd2.set('foo', 'baz')
+
+ t.equal(fd2.get('foo'), 'baz')
+ t.equal(fd.get('foo'), 'foo')
+})
diff --git a/test/fetch/general.js b/test/fetch/general.js
new file mode 100644
index 0000000..0469875
--- /dev/null
+++ b/test/fetch/general.js
@@ -0,0 +1,30 @@
+'use strict'
+
+const { test } = require('tap')
+const {
+ File,
+ FormData,
+ Headers,
+ Request,
+ Response
+} = require('../../index')
+
+test('Symbol.toStringTag descriptor', (t) => {
+ for (const cls of [
+ File,
+ FormData,
+ Headers,
+ Request,
+ Response
+ ]) {
+ const desc = Object.getOwnPropertyDescriptor(cls.prototype, Symbol.toStringTag)
+ t.same(desc, {
+ value: cls.name,
+ writable: false,
+ enumerable: false,
+ configurable: true
+ })
+ }
+
+ t.end()
+})
diff --git a/test/fetch/headers.js b/test/fetch/headers.js
new file mode 100644
index 0000000..4846110
--- /dev/null
+++ b/test/fetch/headers.js
@@ -0,0 +1,743 @@
+'use strict'
+
+const tap = require('tap')
+const { Headers, fill } = require('../../lib/fetch/headers')
+const { kGuard } = require('../../lib/fetch/symbols')
+const { once } = require('events')
+const { fetch } = require('../..')
+const { createServer } = require('http')
+
+tap.test('Headers initialization', t => {
+ t.plan(8)
+
+ t.test('allows undefined', t => {
+ t.plan(1)
+
+ t.doesNotThrow(() => new Headers())
+ })
+
+ t.test('with array of header entries', t => {
+ t.plan(3)
+
+ t.test('fails on invalid array-based init', t => {
+ t.plan(3)
+ t.throws(
+ () => new Headers([['undici', 'fetch'], ['fetch']]),
+ TypeError('Headers constructor: expected name/value pair to be length 2, found 1.')
+ )
+ t.throws(() => new Headers(['undici', 'fetch', 'fetch']), TypeError)
+ t.throws(
+ () => new Headers([0, 1, 2]),
+ TypeError('Sequence: Value of type Number is not an Object.')
+ )
+ })
+
+ t.test('allows even length init', t => {
+ t.plan(1)
+ const init = [['undici', 'fetch'], ['fetch', 'undici']]
+ t.doesNotThrow(() => new Headers(init))
+ })
+
+ t.test('fails for event flattened init', t => {
+ t.plan(1)
+ const init = ['undici', 'fetch', 'fetch', 'undici']
+ t.throws(
+ () => new Headers(init),
+ TypeError('Sequence: Value of type String is not an Object.')
+ )
+ })
+ })
+
+ t.test('with object of header entries', t => {
+ t.plan(1)
+ const init = {
+ undici: 'fetch',
+ fetch: 'undici'
+ }
+ t.doesNotThrow(() => new Headers(init))
+ })
+
+ t.test('fails silently if a boxed primitive object is passed', t => {
+ t.plan(3)
+ /* eslint-disable no-new-wrappers */
+ t.doesNotThrow(() => new Headers(new Number()))
+ t.doesNotThrow(() => new Headers(new Boolean()))
+ t.doesNotThrow(() => new Headers(new String()))
+ /* eslint-enable no-new-wrappers */
+ })
+
+ t.test('fails if primitive is passed', t => {
+ t.plan(2)
+ const expectedTypeError = TypeError
+ t.throws(() => new Headers(1), expectedTypeError)
+ t.throws(() => new Headers('1'), expectedTypeError)
+ })
+
+ t.test('allows some weird stuff (because of webidl)', t => {
+ t.doesNotThrow(() => {
+ new Headers(function () {}) // eslint-disable-line no-new
+ })
+
+ t.doesNotThrow(() => {
+ new Headers(Function) // eslint-disable-line no-new
+ })
+
+ t.end()
+ })
+
+ t.test('allows a myriad of header values to be passed', t => {
+ t.plan(4)
+
+ // Headers constructor uses Headers.append
+
+ t.doesNotThrow(() => new Headers([
+ ['a', ['b', 'c']],
+ ['d', ['e', 'f']]
+ ]), 'allows any array values')
+ t.doesNotThrow(() => new Headers([
+ ['key', null]
+ ]), 'allows null values')
+ t.throws(() => new Headers([
+ ['key']
+ ]), 'throws when 2 arguments are not passed')
+ t.throws(() => new Headers([
+ ['key', 'value', 'value2']
+ ]), 'throws when too many arguments are passed')
+ })
+
+ t.test('accepts headers as objects with array values', t => {
+ t.plan(1)
+ const headers = new Headers({
+ c: '5',
+ b: ['3', '4'],
+ a: ['1', '2']
+ })
+
+ t.same([...headers.entries()], [
+ ['a', '1,2'],
+ ['b', '3,4'],
+ ['c', '5']
+ ])
+ })
+})
+
+tap.test('Headers append', t => {
+ t.plan(3)
+
+ t.test('adds valid header entry to instance', t => {
+ t.plan(2)
+ const headers = new Headers()
+
+ const name = 'undici'
+ const value = 'fetch'
+ t.doesNotThrow(() => headers.append(name, value))
+ t.equal(headers.get(name), value)
+ })
+
+ t.test('adds valid header to existing entry', t => {
+ t.plan(4)
+ const headers = new Headers()
+
+ const name = 'undici'
+ const value1 = 'fetch1'
+ const value2 = 'fetch2'
+ const value3 = 'fetch3'
+ headers.append(name, value1)
+ t.equal(headers.get(name), value1)
+ t.doesNotThrow(() => headers.append(name, value2))
+ t.doesNotThrow(() => headers.append(name, value3))
+ t.equal(headers.get(name), [value1, value2, value3].join(', '))
+ })
+
+ t.test('throws on invalid entry', t => {
+ t.plan(3)
+ const headers = new Headers()
+
+ t.throws(() => headers.append(), 'throws on missing name and value')
+ t.throws(() => headers.append('undici'), 'throws on missing value')
+ t.throws(() => headers.append('invalid @ header ? name', 'valid value'), 'throws on invalid name')
+ })
+})
+
+tap.test('Headers delete', t => {
+ t.plan(4)
+
+ t.test('deletes valid header entry from instance', t => {
+ t.plan(3)
+ const headers = new Headers()
+
+ const name = 'undici'
+ const value = 'fetch'
+ headers.append(name, value)
+ t.equal(headers.get(name), value)
+ t.doesNotThrow(() => headers.delete(name))
+ t.equal(headers.get(name), null)
+ })
+
+ t.test('does not mutate internal list when no match is found', t => {
+ t.plan(3)
+
+ const headers = new Headers()
+ const name = 'undici'
+ const value = 'fetch'
+ headers.append(name, value)
+ t.equal(headers.get(name), value)
+ t.doesNotThrow(() => headers.delete('not-undici'))
+ t.equal(headers.get(name), value)
+ })
+
+ t.test('throws on invalid entry', t => {
+ t.plan(2)
+ const headers = new Headers()
+
+ t.throws(() => headers.delete(), 'throws on missing namee')
+ t.throws(() => headers.delete('invalid @ header ? name'), 'throws on invalid name')
+ })
+
+ // https://github.com/nodejs/undici/issues/2429
+ t.test('`Headers#delete` returns undefined', t => {
+ t.plan(2)
+ const headers = new Headers({ test: 'test' })
+
+ t.same(headers.delete('test'), undefined)
+ t.same(headers.delete('test2'), undefined)
+ })
+})
+
+tap.test('Headers get', t => {
+ t.plan(3)
+
+ t.test('returns null if not found in instance', t => {
+ t.plan(1)
+ const headers = new Headers()
+ headers.append('undici', 'fetch')
+
+ t.equal(headers.get('not-undici'), null)
+ })
+
+ t.test('returns header values from valid header name', t => {
+ t.plan(2)
+ const headers = new Headers()
+
+ const name = 'undici'; const value1 = 'fetch1'; const value2 = 'fetch2'
+ headers.append(name, value1)
+ t.equal(headers.get(name), value1)
+ headers.append(name, value2)
+ t.equal(headers.get(name), [value1, value2].join(', '))
+ })
+
+ t.test('throws on invalid entry', t => {
+ t.plan(2)
+ const headers = new Headers()
+
+ t.throws(() => headers.get(), 'throws on missing name')
+ t.throws(() => headers.get('invalid @ header ? name'), 'throws on invalid name')
+ })
+})
+
+tap.test('Headers has', t => {
+ t.plan(2)
+
+ t.test('returns boolean existence for a header name', t => {
+ t.plan(2)
+ const headers = new Headers()
+
+ const name = 'undici'
+ headers.append('not-undici', 'fetch')
+ t.equal(headers.has(name), false)
+ headers.append(name, 'fetch')
+ t.equal(headers.has(name), true)
+ })
+
+ t.test('throws on invalid entry', t => {
+ t.plan(2)
+ const headers = new Headers()
+
+ t.throws(() => headers.has(), 'throws on missing name')
+ t.throws(() => headers.has('invalid @ header ? name'), 'throws on invalid name')
+ })
+})
+
+tap.test('Headers set', t => {
+ t.plan(5)
+
+ t.test('sets valid header entry to instance', t => {
+ t.plan(2)
+ const headers = new Headers()
+
+ const name = 'undici'
+ const value = 'fetch'
+ headers.append('not-undici', 'fetch')
+ t.doesNotThrow(() => headers.set(name, value))
+ t.equal(headers.get(name), value)
+ })
+
+ t.test('overwrites existing entry', t => {
+ t.plan(4)
+ const headers = new Headers()
+
+ const name = 'undici'
+ const value1 = 'fetch1'
+ const value2 = 'fetch2'
+ t.doesNotThrow(() => headers.set(name, value1))
+ t.equal(headers.get(name), value1)
+ t.doesNotThrow(() => headers.set(name, value2))
+ t.equal(headers.get(name), value2)
+ })
+
+ t.test('allows setting a myriad of values', t => {
+ t.plan(4)
+ const headers = new Headers()
+
+ t.doesNotThrow(() => headers.set('a', ['b', 'c']), 'sets array values properly')
+ t.doesNotThrow(() => headers.set('b', null), 'allows setting null values')
+ t.throws(() => headers.set('c'), 'throws when 2 arguments are not passed')
+ t.doesNotThrow(() => headers.set('c', 'd', 'e'), 'ignores extra arguments')
+ })
+
+ t.test('throws on invalid entry', t => {
+ t.plan(3)
+ const headers = new Headers()
+
+ t.throws(() => headers.set(), 'throws on missing name and value')
+ t.throws(() => headers.set('undici'), 'throws on missing value')
+ t.throws(() => headers.set('invalid @ header ? name', 'valid value'), 'throws on invalid name')
+ })
+
+ // https://github.com/nodejs/undici/issues/2431
+ t.test('`Headers#set` returns undefined', t => {
+ t.plan(2)
+ const headers = new Headers()
+
+ t.same(headers.set('a', 'b'), undefined)
+
+ t.notOk(headers.set('c', 'd') instanceof Map)
+ })
+})
+
+tap.test('Headers forEach', t => {
+ const headers = new Headers([['a', 'b'], ['c', 'd']])
+
+ t.test('standard', t => {
+ t.equal(typeof headers.forEach, 'function')
+
+ headers.forEach((value, key, headerInstance) => {
+ t.ok(value === 'b' || value === 'd')
+ t.ok(key === 'a' || key === 'c')
+ t.equal(headers, headerInstance)
+ })
+
+ t.end()
+ })
+
+ t.test('when no thisArg is set, it is globalThis', (t) => {
+ headers.forEach(function () {
+ t.equal(this, globalThis)
+ })
+
+ t.end()
+ })
+
+ t.test('with thisArg', t => {
+ const thisArg = { a: Math.random() }
+ headers.forEach(function () {
+ t.equal(this, thisArg)
+ }, thisArg)
+
+ t.end()
+ })
+
+ t.end()
+})
+
+tap.test('Headers as Iterable', t => {
+ t.plan(7)
+
+ t.test('should freeze values while iterating', t => {
+ t.plan(1)
+ const init = [
+ ['foo', '123'],
+ ['bar', '456']
+ ]
+ const expected = [
+ ['foo', '123'],
+ ['x-x-bar', '456']
+ ]
+ const headers = new Headers(init)
+ for (const [key, val] of headers) {
+ headers.delete(key)
+ headers.set(`x-${key}`, val)
+ }
+ t.strictSame([...headers], expected)
+ })
+
+ t.test('returns combined and sorted entries using .forEach()', t => {
+ t.plan(8)
+ const init = [
+ ['a', '1'],
+ ['b', '2'],
+ ['c', '3'],
+ ['abc', '4'],
+ ['b', '5']
+ ]
+ const expected = [
+ ['a', '1'],
+ ['abc', '4'],
+ ['b', '2, 5'],
+ ['c', '3']
+ ]
+ const headers = new Headers(init)
+ const that = {}
+ let i = 0
+ headers.forEach(function (value, key, _headers) {
+ t.strictSame(expected[i++], [key, value])
+ t.equal(this, that)
+ }, that)
+ })
+
+ t.test('returns combined and sorted entries using .entries()', t => {
+ t.plan(4)
+ const init = [
+ ['a', '1'],
+ ['b', '2'],
+ ['c', '3'],
+ ['abc', '4'],
+ ['b', '5']
+ ]
+ const expected = [
+ ['a', '1'],
+ ['abc', '4'],
+ ['b', '2, 5'],
+ ['c', '3']
+ ]
+ const headers = new Headers(init)
+ let i = 0
+ for (const header of headers.entries()) {
+ t.strictSame(header, expected[i++])
+ }
+ })
+
+ t.test('returns combined and sorted keys using .keys()', t => {
+ t.plan(4)
+ const init = [
+ ['a', '1'],
+ ['b', '2'],
+ ['c', '3'],
+ ['abc', '4'],
+ ['b', '5']
+ ]
+ const expected = ['a', 'abc', 'b', 'c']
+ const headers = new Headers(init)
+ let i = 0
+ for (const key of headers.keys()) {
+ t.strictSame(key, expected[i++])
+ }
+ })
+
+ t.test('returns combined and sorted values using .values()', t => {
+ t.plan(4)
+ const init = [
+ ['a', '1'],
+ ['b', '2'],
+ ['c', '3'],
+ ['abc', '4'],
+ ['b', '5']
+ ]
+ const expected = ['1', '4', '2, 5', '3']
+ const headers = new Headers(init)
+ let i = 0
+ for (const value of headers.values()) {
+ t.strictSame(value, expected[i++])
+ }
+ })
+
+ t.test('returns combined and sorted entries using for...of loop', t => {
+ t.plan(5)
+ const init = [
+ ['a', '1'],
+ ['b', '2'],
+ ['c', '3'],
+ ['abc', '4'],
+ ['b', '5'],
+ ['d', ['6', '7']]
+ ]
+ const expected = [
+ ['a', '1'],
+ ['abc', '4'],
+ ['b', '2, 5'],
+ ['c', '3'],
+ ['d', '6,7']
+ ]
+ let i = 0
+ for (const header of new Headers(init)) {
+ t.strictSame(header, expected[i++])
+ }
+ })
+
+ t.test('validate append ordering', t => {
+ t.plan(1)
+ const headers = new Headers([['b', '2'], ['c', '3'], ['e', '5']])
+ headers.append('d', '4')
+ headers.append('a', '1')
+ headers.append('f', '6')
+ headers.append('c', '7')
+ headers.append('abc', '8')
+
+ const expected = [...new Map([
+ ['a', '1'],
+ ['abc', '8'],
+ ['b', '2'],
+ ['c', '3, 7'],
+ ['d', '4'],
+ ['e', '5'],
+ ['f', '6']
+ ])]
+
+ t.same([...headers], expected)
+ })
+})
+
+tap.test('arg validation', (t) => {
+ // fill
+ t.throws(() => {
+ fill({}, 0)
+ }, TypeError)
+
+ const headers = new Headers()
+
+ // constructor
+ t.throws(() => {
+ // eslint-disable-next-line
+ new Headers(0)
+ }, TypeError)
+
+ // get [Symbol.toStringTag]
+ t.doesNotThrow(() => {
+ Object.prototype.toString.call(Headers.prototype)
+ })
+
+ // toString
+ t.doesNotThrow(() => {
+ Headers.prototype.toString.call(null)
+ })
+
+ // append
+ t.throws(() => {
+ Headers.prototype.append.call(null)
+ }, TypeError)
+ t.throws(() => {
+ headers.append()
+ }, TypeError)
+
+ // delete
+ t.throws(() => {
+ Headers.prototype.delete.call(null)
+ }, TypeError)
+ t.throws(() => {
+ headers.delete()
+ }, TypeError)
+
+ // get
+ t.throws(() => {
+ Headers.prototype.get.call(null)
+ }, TypeError)
+ t.throws(() => {
+ headers.get()
+ }, TypeError)
+
+ // has
+ t.throws(() => {
+ Headers.prototype.has.call(null)
+ }, TypeError)
+ t.throws(() => {
+ headers.has()
+ }, TypeError)
+
+ // set
+ t.throws(() => {
+ Headers.prototype.set.call(null)
+ }, TypeError)
+ t.throws(() => {
+ headers.set()
+ }, TypeError)
+
+ // forEach
+ t.throws(() => {
+ Headers.prototype.forEach.call(null)
+ }, TypeError)
+ t.throws(() => {
+ headers.forEach()
+ }, TypeError)
+ t.throws(() => {
+ headers.forEach(1)
+ }, TypeError)
+
+ // inspect
+ t.throws(() => {
+ Headers.prototype[Symbol.for('nodejs.util.inspect.custom')].call(null)
+ }, TypeError)
+
+ t.end()
+})
+
+tap.test('function signature verification', (t) => {
+ t.test('function length', (t) => {
+ t.equal(Headers.prototype.append.length, 2)
+ t.equal(Headers.prototype.constructor.length, 0)
+ t.equal(Headers.prototype.delete.length, 1)
+ t.equal(Headers.prototype.entries.length, 0)
+ t.equal(Headers.prototype.forEach.length, 1)
+ t.equal(Headers.prototype.get.length, 1)
+ t.equal(Headers.prototype.has.length, 1)
+ t.equal(Headers.prototype.keys.length, 0)
+ t.equal(Headers.prototype.set.length, 2)
+ t.equal(Headers.prototype.values.length, 0)
+ t.equal(Headers.prototype[Symbol.iterator].length, 0)
+ t.equal(Headers.prototype.toString.length, 0)
+
+ t.end()
+ })
+
+ t.test('function equality', (t) => {
+ t.equal(Headers.prototype.entries, Headers.prototype[Symbol.iterator])
+ t.equal(Headers.prototype.toString, Object.prototype.toString)
+
+ t.end()
+ })
+
+ t.test('toString and Symbol.toStringTag', (t) => {
+ t.equal(Object.prototype.toString.call(Headers.prototype), '[object Headers]')
+ t.equal(Headers.prototype[Symbol.toStringTag], 'Headers')
+ t.equal(Headers.prototype.toString.call(null), '[object Null]')
+
+ t.end()
+ })
+
+ t.end()
+})
+
+tap.test('various init paths of Headers', (t) => {
+ const h1 = new Headers()
+ const h2 = new Headers({})
+ const h3 = new Headers(undefined)
+ t.equal([...h1.entries()].length, 0)
+ t.equal([...h2.entries()].length, 0)
+ t.equal([...h3.entries()].length, 0)
+
+ t.end()
+})
+
+tap.test('immutable guard', (t) => {
+ const headers = new Headers()
+ headers.set('key', 'val')
+ headers[kGuard] = 'immutable'
+
+ t.throws(() => {
+ headers.set('asd', 'asd')
+ })
+ t.throws(() => {
+ headers.append('asd', 'asd')
+ })
+ t.throws(() => {
+ headers.delete('asd')
+ })
+ t.equal(headers.get('key'), 'val')
+ t.equal(headers.has('key'), true)
+
+ t.end()
+})
+
+tap.test('request-no-cors guard', (t) => {
+ const headers = new Headers()
+ headers[kGuard] = 'request-no-cors'
+ t.doesNotThrow(() => { headers.set('key', 'val') })
+ t.doesNotThrow(() => { headers.append('key', 'val') })
+ t.doesNotThrow(() => { headers.delete('key') })
+ t.end()
+})
+
+tap.test('invalid headers', (t) => {
+ t.doesNotThrow(() => new Headers({ "abcdefghijklmnopqrstuvwxyz0123456789!#$%&'*+-.^_`|~": 'test' }))
+
+ const chars = '"(),/:;<=>?@[\\]{}'.split('')
+
+ for (const char of chars) {
+ t.throws(() => new Headers({ [char]: 'test' }), TypeError, `The string "${char}" should throw an error.`)
+ }
+
+ for (const byte of ['\r', '\n', '\t', ' ', String.fromCharCode(128), '']) {
+ t.throws(() => {
+ new Headers().set(byte, 'test')
+ }, TypeError, 'invalid header name')
+ }
+
+ for (const byte of [
+ '\0',
+ '\r',
+ '\n'
+ ]) {
+ t.throws(() => {
+ new Headers().set('a', `a${byte}b`)
+ }, TypeError, 'not allowed at all in header value')
+ }
+
+ t.doesNotThrow(() => {
+ new Headers().set('a', '\r')
+ })
+
+ t.doesNotThrow(() => {
+ new Headers().set('a', '\n')
+ })
+
+ t.throws(() => {
+ new Headers().set('a', Symbol('symbol'))
+ }, TypeError, 'symbols should throw')
+
+ t.end()
+})
+
+tap.test('headers that might cause a ReDoS', (t) => {
+ t.doesNotThrow(() => {
+ // This test will time out if the ReDoS attack is successful.
+ const headers = new Headers()
+ const attack = 'a' + '\t'.repeat(500_000) + '\ta'
+ headers.append('fhqwhgads', attack)
+ })
+
+ t.end()
+})
+
+tap.test('Headers.prototype.getSetCookie', (t) => {
+ t.test('Mutating the returned list does not affect the set-cookie list', (t) => {
+ const h = new Headers([
+ ['set-cookie', 'a=b'],
+ ['set-cookie', 'c=d']
+ ])
+
+ const old = h.getSetCookie()
+ h.getSetCookie().push('oh=no')
+ const now = h.getSetCookie()
+
+ t.same(old, now)
+ t.end()
+ })
+
+ // https://github.com/nodejs/undici/issues/1935
+ t.test('When Headers are cloned, so are the cookies', async (t) => {
+ const server = createServer((req, res) => {
+ res.setHeader('Set-Cookie', 'test=onetwo')
+ res.end('Hello World!')
+ }).listen(0)
+
+ await once(server, 'listening')
+ t.teardown(server.close.bind(server))
+
+ const res = await fetch(`http://localhost:${server.address().port}`)
+ const entries = Object.fromEntries(res.headers.entries())
+
+ t.same(res.headers.getSetCookie(), ['test=onetwo'])
+ t.ok('set-cookie' in entries)
+ })
+
+ t.end()
+})
diff --git a/test/fetch/http2.js b/test/fetch/http2.js
new file mode 100644
index 0000000..9f6997f
--- /dev/null
+++ b/test/fetch/http2.js
@@ -0,0 +1,415 @@
+'use strict'
+
+const { createSecureServer } = require('node:http2')
+const { createReadStream, readFileSync } = require('node:fs')
+const { once } = require('node:events')
+const { Blob } = require('node:buffer')
+const { Readable } = require('node:stream')
+
+const { test, plan } = require('tap')
+const pem = require('https-pem')
+
+const { Client, fetch, Headers } = require('../..')
+
+const nodeVersion = Number(process.version.split('v')[1].split('.')[0])
+
+plan(7)
+
+test('[Fetch] Issue#2311', async t => {
+ const expectedBody = 'hello from client!'
+
+ const server = createSecureServer(pem, async (req, res) => {
+ let body = ''
+
+ req.setEncoding('utf8')
+
+ res.writeHead(200, {
+ 'content-type': 'text/plain; charset=utf-8',
+ 'x-custom-h2': req.headers['x-my-header']
+ })
+
+ for await (const chunk of req) {
+ body += chunk
+ }
+
+ res.end(body)
+ })
+
+ t.plan(1)
+
+ server.listen()
+ await once(server, 'listening')
+
+ const client = new Client(`https://localhost:${server.address().port}`, {
+ connect: {
+ rejectUnauthorized: false
+ },
+ allowH2: true
+ })
+
+ const response = await fetch(
+ `https://localhost:${server.address().port}/`,
+ // Needs to be passed to disable the reject unauthorized
+ {
+ method: 'POST',
+ dispatcher: client,
+ headers: {
+ 'x-my-header': 'foo',
+ 'content-type': 'text-plain'
+ },
+ body: expectedBody
+ }
+ )
+
+ const responseBody = await response.text()
+
+ t.teardown(server.close.bind(server))
+ t.teardown(client.close.bind(client))
+
+ t.equal(responseBody, expectedBody)
+})
+
+test('[Fetch] Simple GET with h2', async t => {
+ const server = createSecureServer(pem)
+ const expectedRequestBody = 'hello h2!'
+
+ server.on('stream', async (stream, headers) => {
+ stream.respond({
+ 'content-type': 'text/plain; charset=utf-8',
+ 'x-custom-h2': headers['x-my-header'],
+ 'x-method': headers[':method'],
+ ':status': 200
+ })
+
+ stream.end(expectedRequestBody)
+ })
+
+ t.plan(5)
+
+ server.listen()
+ await once(server, 'listening')
+
+ const client = new Client(`https://localhost:${server.address().port}`, {
+ connect: {
+ rejectUnauthorized: false
+ },
+ allowH2: true
+ })
+
+ const response = await fetch(
+ `https://localhost:${server.address().port}/`,
+ // Needs to be passed to disable the reject unauthorized
+ {
+ method: 'GET',
+ dispatcher: client,
+ headers: {
+ 'x-my-header': 'foo',
+ 'content-type': 'text-plain'
+ }
+ }
+ )
+
+ const responseBody = await response.text()
+
+ t.teardown(server.close.bind(server))
+ t.teardown(client.close.bind(client))
+
+ t.equal(responseBody, expectedRequestBody)
+ t.equal(response.headers.get('x-method'), 'GET')
+ t.equal(response.headers.get('x-custom-h2'), 'foo')
+ // https://github.com/nodejs/undici/issues/2415
+ t.throws(() => {
+ response.headers.get(':status')
+ }, TypeError)
+
+ // See https://fetch.spec.whatwg.org/#concept-response-status-message
+ t.equal(response.statusText, '')
+})
+
+test('[Fetch] Should handle h2 request with body (string or buffer)', async t => {
+ const server = createSecureServer(pem)
+ const expectedBody = 'hello from client!'
+ const expectedRequestBody = 'hello h2!'
+ const requestBody = []
+
+ server.on('stream', async (stream, headers) => {
+ stream.on('data', chunk => requestBody.push(chunk))
+
+ stream.respond({
+ 'content-type': 'text/plain; charset=utf-8',
+ 'x-custom-h2': headers['x-my-header'],
+ ':status': 200
+ })
+
+ stream.end(expectedRequestBody)
+ })
+
+ t.plan(2)
+
+ server.listen()
+ await once(server, 'listening')
+
+ const client = new Client(`https://localhost:${server.address().port}`, {
+ connect: {
+ rejectUnauthorized: false
+ },
+ allowH2: true
+ })
+
+ const response = await fetch(
+ `https://localhost:${server.address().port}/`,
+ // Needs to be passed to disable the reject unauthorized
+ {
+ method: 'POST',
+ dispatcher: client,
+ headers: {
+ 'x-my-header': 'foo',
+ 'content-type': 'text-plain'
+ },
+ body: expectedBody
+ }
+ )
+
+ const responseBody = await response.text()
+
+ t.teardown(server.close.bind(server))
+ t.teardown(client.close.bind(client))
+
+ t.equal(Buffer.concat(requestBody).toString('utf-8'), expectedBody)
+ t.equal(responseBody, expectedRequestBody)
+})
+
+// Skipping for now, there is something odd in the way the body is handled
+test(
+ '[Fetch] Should handle h2 request with body (stream)',
+ { skip: nodeVersion === 16 },
+ async t => {
+ const server = createSecureServer(pem)
+ const expectedBody = readFileSync(__filename, 'utf-8')
+ const stream = createReadStream(__filename)
+ const requestChunks = []
+
+ server.on('stream', async (stream, headers) => {
+ t.equal(headers[':method'], 'PUT')
+ t.equal(headers[':path'], '/')
+ t.equal(headers[':scheme'], 'https')
+
+ stream.respond({
+ 'content-type': 'text/plain; charset=utf-8',
+ 'x-custom-h2': headers['x-my-header'],
+ ':status': 200
+ })
+
+ for await (const chunk of stream) {
+ requestChunks.push(chunk)
+ }
+
+ stream.end('hello h2!')
+ })
+
+ t.plan(8)
+
+ server.listen(0)
+ await once(server, 'listening')
+
+ const client = new Client(`https://localhost:${server.address().port}`, {
+ connect: {
+ rejectUnauthorized: false
+ },
+ allowH2: true
+ })
+
+ t.teardown(server.close.bind(server))
+ t.teardown(client.close.bind(client))
+
+ const response = await fetch(
+ `https://localhost:${server.address().port}/`,
+ // Needs to be passed to disable the reject unauthorized
+ {
+ method: 'PUT',
+ dispatcher: client,
+ headers: {
+ 'x-my-header': 'foo',
+ 'content-type': 'text-plain'
+ },
+ body: Readable.toWeb(stream),
+ duplex: 'half'
+ }
+ )
+
+ const responseBody = await response.text()
+
+ t.equal(response.status, 200)
+ t.equal(response.headers.get('content-type'), 'text/plain; charset=utf-8')
+ t.equal(response.headers.get('x-custom-h2'), 'foo')
+ t.equal(responseBody, 'hello h2!')
+ t.equal(Buffer.concat(requestChunks).toString('utf-8'), expectedBody)
+ }
+)
+test('Should handle h2 request with body (Blob)', { skip: !Blob }, async t => {
+ const server = createSecureServer(pem)
+ const expectedBody = 'asd'
+ const requestChunks = []
+ const body = new Blob(['asd'], {
+ type: 'text/plain'
+ })
+
+ server.on('stream', async (stream, headers) => {
+ t.equal(headers[':method'], 'POST')
+ t.equal(headers[':path'], '/')
+ t.equal(headers[':scheme'], 'https')
+
+ stream.on('data', chunk => requestChunks.push(chunk))
+
+ stream.respond({
+ 'content-type': 'text/plain; charset=utf-8',
+ 'x-custom-h2': headers['x-my-header'],
+ ':status': 200
+ })
+
+ stream.end('hello h2!')
+ })
+
+ t.plan(8)
+
+ server.listen(0)
+ await once(server, 'listening')
+
+ const client = new Client(`https://localhost:${server.address().port}`, {
+ connect: {
+ rejectUnauthorized: false
+ },
+ allowH2: true
+ })
+
+ t.teardown(server.close.bind(server))
+ t.teardown(client.close.bind(client))
+
+ const response = await fetch(
+ `https://localhost:${server.address().port}/`,
+ // Needs to be passed to disable the reject unauthorized
+ {
+ body,
+ method: 'POST',
+ dispatcher: client,
+ headers: {
+ 'x-my-header': 'foo',
+ 'content-type': 'text-plain'
+ }
+ }
+ )
+
+ const responseBody = await response.arrayBuffer()
+
+ t.equal(response.status, 200)
+ t.equal(response.headers.get('content-type'), 'text/plain; charset=utf-8')
+ t.equal(response.headers.get('x-custom-h2'), 'foo')
+ t.same(new TextDecoder().decode(responseBody).toString(), 'hello h2!')
+ t.equal(Buffer.concat(requestChunks).toString('utf-8'), expectedBody)
+})
+
+test(
+ 'Should handle h2 request with body (Blob:ArrayBuffer)',
+ { skip: !Blob },
+ async t => {
+ const server = createSecureServer(pem)
+ const expectedBody = 'hello'
+ const requestChunks = []
+ const expectedResponseBody = { hello: 'h2' }
+ const buf = Buffer.from(expectedBody)
+ const body = new ArrayBuffer(buf.byteLength)
+
+ buf.copy(new Uint8Array(body))
+
+ server.on('stream', async (stream, headers) => {
+ t.equal(headers[':method'], 'PUT')
+ t.equal(headers[':path'], '/')
+ t.equal(headers[':scheme'], 'https')
+
+ stream.on('data', chunk => requestChunks.push(chunk))
+
+ stream.respond({
+ 'content-type': 'application/json',
+ 'x-custom-h2': headers['x-my-header'],
+ ':status': 200
+ })
+
+ stream.end(JSON.stringify(expectedResponseBody))
+ })
+
+ t.plan(8)
+
+ server.listen(0)
+ await once(server, 'listening')
+
+ const client = new Client(`https://localhost:${server.address().port}`, {
+ connect: {
+ rejectUnauthorized: false
+ },
+ allowH2: true
+ })
+
+ t.teardown(server.close.bind(server))
+ t.teardown(client.close.bind(client))
+
+ const response = await fetch(
+ `https://localhost:${server.address().port}/`,
+ // Needs to be passed to disable the reject unauthorized
+ {
+ body,
+ method: 'PUT',
+ dispatcher: client,
+ headers: {
+ 'x-my-header': 'foo',
+ 'content-type': 'text-plain'
+ }
+ }
+ )
+
+ const responseBody = await response.json()
+
+ t.equal(response.status, 200)
+ t.equal(response.headers.get('content-type'), 'application/json')
+ t.equal(response.headers.get('x-custom-h2'), 'foo')
+ t.same(responseBody, expectedResponseBody)
+ t.equal(Buffer.concat(requestChunks).toString('utf-8'), expectedBody)
+ }
+)
+
+test('Issue#2415', async (t) => {
+ t.plan(1)
+ const server = createSecureServer(pem)
+
+ server.on('stream', async (stream, headers) => {
+ stream.respond({
+ ':status': 200
+ })
+ stream.end('test')
+ })
+
+ server.listen()
+ await once(server, 'listening')
+
+ const client = new Client(`https://localhost:${server.address().port}`, {
+ connect: {
+ rejectUnauthorized: false
+ },
+ allowH2: true
+ })
+
+ const response = await fetch(
+ `https://localhost:${server.address().port}/`,
+ // Needs to be passed to disable the reject unauthorized
+ {
+ method: 'GET',
+ dispatcher: client
+ }
+ )
+
+ await response.text()
+
+ t.teardown(server.close.bind(server))
+ t.teardown(client.close.bind(client))
+
+ t.doesNotThrow(() => new Headers(response.headers))
+})
diff --git a/test/fetch/integrity.js b/test/fetch/integrity.js
new file mode 100644
index 0000000..f91f693
--- /dev/null
+++ b/test/fetch/integrity.js
@@ -0,0 +1,150 @@
+'use strict'
+
+const { test } = require('tap')
+const { createServer } = require('http')
+const { createHash, getHashes } = require('crypto')
+const { gzipSync } = require('zlib')
+const { fetch, setGlobalDispatcher, Agent } = require('../..')
+const { once } = require('events')
+
+const supportedHashes = getHashes()
+
+setGlobalDispatcher(new Agent({
+ keepAliveTimeout: 1,
+ keepAliveMaxTimeout: 1
+}))
+
+test('request with correct integrity checksum', (t) => {
+ const body = 'Hello world!'
+ const hash = createHash('sha256').update(body).digest('base64')
+
+ const server = createServer((req, res) => {
+ res.end(body)
+ })
+
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, async () => {
+ const response = await fetch(`http://localhost:${server.address().port}`, {
+ integrity: `sha256-${hash}`
+ })
+ t.strictSame(body, await response.text())
+ t.end()
+ })
+})
+
+test('request with wrong integrity checksum', (t) => {
+ const body = 'Hello world!'
+ const hash = 'c0535e4be2b79ffd93291305436bf889314e4a3faec05ecffcbb7df31ad9e51b'
+
+ const server = createServer((req, res) => {
+ res.end(body)
+ })
+
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ fetch(`http://localhost:${server.address().port}`, {
+ integrity: `sha256-${hash}`
+ }).then(response => {
+ t.pass('request did not fail')
+ }).catch((err) => {
+ t.equal(err.cause.message, 'integrity mismatch')
+ }).finally(() => {
+ t.end()
+ })
+ })
+})
+
+test('request with integrity checksum on encoded body', (t) => {
+ const body = 'Hello world!'
+ const hash = createHash('sha256').update(body).digest('base64')
+
+ const server = createServer((req, res) => {
+ res.setHeader('content-encoding', 'gzip')
+ res.end(gzipSync(body))
+ })
+
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, async () => {
+ const response = await fetch(`http://localhost:${server.address().port}`, {
+ integrity: `sha256-${hash}`
+ })
+ t.strictSame(body, await response.text())
+ t.end()
+ })
+})
+
+test('request with a totally incorrect integrity', async (t) => {
+ const server = createServer((req, res) => {
+ res.end()
+ }).listen(0)
+
+ t.teardown(server.close.bind(server))
+ await once(server, 'listening')
+
+ await t.resolves(fetch(`http://localhost:${server.address().port}`, {
+ integrity: 'what-integrityisthis'
+ }))
+})
+
+test('request with mixed in/valid integrities', async (t) => {
+ const body = 'Hello world!'
+ const hash = createHash('sha256').update(body).digest('base64')
+
+ const server = createServer((req, res) => {
+ res.end(body)
+ }).listen(0)
+
+ t.teardown(server.close.bind(server))
+ await once(server, 'listening')
+
+ await t.resolves(fetch(`http://localhost:${server.address().port}`, {
+ integrity: `invalid-integrity sha256-${hash}`
+ }))
+})
+
+test('request with sha384 hash', { skip: !supportedHashes.includes('sha384') }, async (t) => {
+ const body = 'Hello world!'
+ const hash = createHash('sha384').update(body).digest('base64')
+
+ const server = createServer((req, res) => {
+ res.end(body)
+ }).listen(0)
+
+ t.teardown(server.close.bind(server))
+ await once(server, 'listening')
+
+ // request should succeed
+ await t.resolves(fetch(`http://localhost:${server.address().port}`, {
+ integrity: `sha384-${hash}`
+ }))
+
+ // request should fail
+ await t.rejects(fetch(`http://localhost:${server.address().port}`, {
+ integrity: 'sha384-ypeBEsobvcr6wjGzmiPcTaeG7/gUfE5yuYB3ha/uSLs='
+ }))
+})
+
+test('request with sha512 hash', { skip: !supportedHashes.includes('sha512') }, async (t) => {
+ const body = 'Hello world!'
+ const hash = createHash('sha512').update(body).digest('base64')
+
+ const server = createServer((req, res) => {
+ res.end(body)
+ }).listen(0)
+
+ t.teardown(server.close.bind(server))
+ await once(server, 'listening')
+
+ // request should succeed
+ await t.resolves(fetch(`http://localhost:${server.address().port}`, {
+ integrity: `sha512-${hash}`
+ }))
+
+ // request should fail
+ await t.rejects(fetch(`http://localhost:${server.address().port}`, {
+ integrity: 'sha512-ypeBEsobvcr6wjGzmiPcTaeG7/gUfE5yuYB3ha/uSLs='
+ }))
+})
diff --git a/test/fetch/issue-1447.js b/test/fetch/issue-1447.js
new file mode 100644
index 0000000..503b344
--- /dev/null
+++ b/test/fetch/issue-1447.js
@@ -0,0 +1,46 @@
+'use strict'
+
+const { test, skip } = require('tap')
+const { nodeMajor } = require('../../lib/core/util')
+
+if (nodeMajor === 16) {
+ skip('esbuild uses static blocks with --keep-names which node 16.8 does not have')
+ process.exit()
+}
+
+const undici = require('../..')
+const { fetch: theoreticalGlobalFetch } = require('../../undici-fetch')
+
+test('Mocking works with both fetches', async (t) => {
+ const mockAgent = new undici.MockAgent()
+ const body = JSON.stringify({ foo: 'bar' })
+
+ mockAgent.disableNetConnect()
+ undici.setGlobalDispatcher(mockAgent)
+ const pool = mockAgent.get('https://example.com')
+
+ pool.intercept({
+ path: '/path',
+ method: 'POST',
+ body (bodyString) {
+ t.equal(bodyString, body)
+ return true
+ }
+ }).reply(200, { ok: 1 }).times(2)
+
+ const url = new URL('https://example.com/path').href
+
+ // undici fetch from node_modules
+ await undici.fetch(url, {
+ method: 'POST',
+ body
+ })
+
+ // the global fetch bundled with esbuild
+ await theoreticalGlobalFetch(url, {
+ method: 'POST',
+ body
+ })
+
+ t.end()
+})
diff --git a/test/fetch/issue-2009.js b/test/fetch/issue-2009.js
new file mode 100644
index 0000000..0b7b3e9
--- /dev/null
+++ b/test/fetch/issue-2009.js
@@ -0,0 +1,28 @@
+'use strict'
+
+const { test } = require('tap')
+const { fetch } = require('../..')
+const { createServer } = require('http')
+const { once } = require('events')
+
+test('issue 2009', async (t) => {
+ const server = createServer((req, res) => {
+ res.setHeader('a', 'b')
+ res.flushHeaders()
+
+ res.socket.end()
+ }).listen(0)
+
+ t.teardown(server.close.bind(server))
+ await once(server, 'listening')
+
+ for (let i = 0; i < 10; i++) {
+ await t.resolves(
+ fetch(`http://localhost:${server.address().port}`).then(
+ async (resp) => {
+ await resp.body.cancel('Some message')
+ }
+ )
+ )
+ }
+})
diff --git a/test/fetch/issue-2021.js b/test/fetch/issue-2021.js
new file mode 100644
index 0000000..cd28a71
--- /dev/null
+++ b/test/fetch/issue-2021.js
@@ -0,0 +1,32 @@
+'use strict'
+
+const { test } = require('tap')
+const { once } = require('events')
+const { createServer } = require('http')
+const { fetch } = require('../..')
+
+// https://github.com/nodejs/undici/issues/2021
+test('content-length header is removed on redirect', async (t) => {
+ const server = createServer((req, res) => {
+ if (req.url === '/redirect') {
+ res.writeHead(302, { Location: '/redirect2' })
+ res.end()
+ return
+ }
+
+ res.end()
+ }).listen(0).unref()
+
+ t.teardown(server.close.bind(server))
+ await once(server, 'listening')
+
+ const body = 'a+b+c'
+
+ await t.resolves(fetch(`http://localhost:${server.address().port}/redirect`, {
+ method: 'POST',
+ body,
+ headers: {
+ 'content-length': Buffer.byteLength(body)
+ }
+ }))
+})
diff --git a/test/fetch/issue-2171.js b/test/fetch/issue-2171.js
new file mode 100644
index 0000000..b04ae0e
--- /dev/null
+++ b/test/fetch/issue-2171.js
@@ -0,0 +1,25 @@
+'use strict'
+
+const { fetch } = require('../..')
+const { DOMException } = require('../../lib/fetch/constants')
+const { once } = require('events')
+const { createServer } = require('http')
+const { test } = require('tap')
+
+test('error reason is forwarded - issue #2171', { skip: !AbortSignal.timeout }, async (t) => {
+ const server = createServer(() => {}).listen(0)
+
+ t.teardown(server.close.bind(server))
+ await once(server, 'listening')
+
+ const timeout = AbortSignal.timeout(100)
+ await t.rejects(
+ fetch(`http://localhost:${server.address().port}`, {
+ signal: timeout
+ }),
+ {
+ name: 'TimeoutError',
+ code: DOMException.TIMEOUT_ERR
+ }
+ )
+})
diff --git a/test/fetch/issue-2242.js b/test/fetch/issue-2242.js
new file mode 100644
index 0000000..fe70412
--- /dev/null
+++ b/test/fetch/issue-2242.js
@@ -0,0 +1,8 @@
+'use strict'
+
+const { test } = require('tap')
+const { fetch } = require('../..')
+
+test('fetch with signal already aborted', async (t) => {
+ await t.rejects(fetch('http://localhost', { signal: AbortSignal.abort('Already aborted') }), 'Already aborted')
+})
diff --git a/test/fetch/issue-2318.js b/test/fetch/issue-2318.js
new file mode 100644
index 0000000..e4f610d
--- /dev/null
+++ b/test/fetch/issue-2318.js
@@ -0,0 +1,25 @@
+'use strict'
+
+const { test } = require('tap')
+const { once } = require('events')
+const { createServer } = require('http')
+const { fetch } = require('../..')
+
+test('Undici overrides user-provided `Host` header', async (t) => {
+ t.plan(1)
+
+ const server = createServer((req, res) => {
+ t.equal(req.headers.host, `localhost:${server.address().port}`)
+
+ res.end()
+ }).listen(0)
+
+ t.teardown(server.close.bind(server))
+ await once(server, 'listening')
+
+ await fetch(`http://localhost:${server.address().port}`, {
+ headers: {
+ host: 'www.idk.org'
+ }
+ })
+})
diff --git a/test/fetch/issue-node-46525.js b/test/fetch/issue-node-46525.js
new file mode 100644
index 0000000..6fd9810
--- /dev/null
+++ b/test/fetch/issue-node-46525.js
@@ -0,0 +1,28 @@
+'use strict'
+
+const { once } = require('events')
+const { createServer } = require('http')
+const { test } = require('tap')
+const { fetch } = require('../..')
+
+// https://github.com/nodejs/node/issues/46525
+test('No warning when reusing AbortController', async (t) => {
+ function onWarning (error) {
+ t.error(error, 'Got warning')
+ }
+
+ const server = createServer((req, res) => res.end()).listen(0)
+
+ await once(server, 'listening')
+
+ process.on('warning', onWarning)
+ t.teardown(() => {
+ process.off('warning', onWarning)
+ return server.close()
+ })
+
+ const controller = new AbortController()
+ for (let i = 0; i < 15; i++) {
+ await fetch(`http://localhost:${server.address().port}`, { signal: controller.signal })
+ }
+})
diff --git a/test/fetch/iterators.js b/test/fetch/iterators.js
new file mode 100644
index 0000000..6c6761d
--- /dev/null
+++ b/test/fetch/iterators.js
@@ -0,0 +1,140 @@
+'use strict'
+
+const { test } = require('tap')
+const { Headers, FormData } = require('../..')
+
+test('Implements " Iterator" properly', (t) => {
+ t.test('all Headers iterators implement Headers Iterator', (t) => {
+ const headers = new Headers([['a', 'b'], ['c', 'd']])
+
+ for (const iterable of ['keys', 'values', 'entries', Symbol.iterator]) {
+ const gen = headers[iterable]()
+ // https://tc39.es/ecma262/#sec-%25iteratorprototype%25-object
+ const IteratorPrototype = Object.getPrototypeOf(Object.getPrototypeOf([][Symbol.iterator]()))
+ const iteratorProto = Object.getPrototypeOf(gen)
+
+ t.ok(gen.constructor === Object)
+ t.ok(gen.prototype === undefined)
+ // eslint-disable-next-line no-proto
+ t.equal(gen.__proto__[Symbol.toStringTag], 'Headers Iterator')
+ // https://github.com/node-fetch/node-fetch/issues/1119#issuecomment-100222049
+ t.notOk(Headers.prototype[iterable] instanceof function * () {}.constructor)
+ // eslint-disable-next-line no-proto
+ t.ok(gen.__proto__.next.__proto__ === Function.prototype)
+ // https://webidl.spec.whatwg.org/#dfn-iterator-prototype-object
+ // "The [[Prototype]] internal slot of an iterator prototype object must be %IteratorPrototype%."
+ t.equal(gen[Symbol.iterator], IteratorPrototype[Symbol.iterator])
+ t.equal(Object.getPrototypeOf(iteratorProto), IteratorPrototype)
+ }
+
+ t.end()
+ })
+
+ t.test('all FormData iterators implement FormData Iterator', (t) => {
+ const fd = new FormData()
+
+ for (const iterable of ['keys', 'values', 'entries', Symbol.iterator]) {
+ const gen = fd[iterable]()
+ // https://tc39.es/ecma262/#sec-%25iteratorprototype%25-object
+ const IteratorPrototype = Object.getPrototypeOf(Object.getPrototypeOf([][Symbol.iterator]()))
+ const iteratorProto = Object.getPrototypeOf(gen)
+
+ t.ok(gen.constructor === Object)
+ t.ok(gen.prototype === undefined)
+ // eslint-disable-next-line no-proto
+ t.equal(gen.__proto__[Symbol.toStringTag], 'FormData Iterator')
+ // https://github.com/node-fetch/node-fetch/issues/1119#issuecomment-100222049
+ t.notOk(Headers.prototype[iterable] instanceof function * () {}.constructor)
+ // eslint-disable-next-line no-proto
+ t.ok(gen.__proto__.next.__proto__ === Function.prototype)
+ // https://webidl.spec.whatwg.org/#dfn-iterator-prototype-object
+ // "The [[Prototype]] internal slot of an iterator prototype object must be %IteratorPrototype%."
+ t.equal(gen[Symbol.iterator], IteratorPrototype[Symbol.iterator])
+ t.equal(Object.getPrototypeOf(iteratorProto), IteratorPrototype)
+ }
+
+ t.end()
+ })
+
+ t.test('Iterator symbols are properly set', (t) => {
+ t.test('Headers', (t) => {
+ const headers = new Headers([['a', 'b'], ['c', 'd']])
+ const gen = headers.entries()
+
+ t.equal(typeof gen[Symbol.toStringTag], 'string')
+ t.equal(typeof gen[Symbol.iterator], 'function')
+ t.end()
+ })
+
+ t.test('FormData', (t) => {
+ const fd = new FormData()
+ const gen = fd.entries()
+
+ t.equal(typeof gen[Symbol.toStringTag], 'string')
+ t.equal(typeof gen[Symbol.iterator], 'function')
+ t.end()
+ })
+
+ t.end()
+ })
+
+ t.test('Iterator does not inherit Generator prototype methods', (t) => {
+ t.test('Headers', (t) => {
+ const headers = new Headers([['a', 'b'], ['c', 'd']])
+ const gen = headers.entries()
+
+ t.equal(gen.return, undefined)
+ t.equal(gen.throw, undefined)
+ t.equal(typeof gen.next, 'function')
+
+ t.end()
+ })
+
+ t.test('FormData', (t) => {
+ const fd = new FormData()
+ const gen = fd.entries()
+
+ t.equal(gen.return, undefined)
+ t.equal(gen.throw, undefined)
+ t.equal(typeof gen.next, 'function')
+
+ t.end()
+ })
+
+ t.end()
+ })
+
+ t.test('Symbol.iterator', (t) => {
+ // Headers
+ const headerValues = new Headers([['a', 'b']]).entries()[Symbol.iterator]()
+ t.same(Array.from(headerValues), [['a', 'b']])
+
+ // FormData
+ const formdata = new FormData()
+ formdata.set('a', 'b')
+ const formdataValues = formdata.entries()[Symbol.iterator]()
+ t.same(Array.from(formdataValues), [['a', 'b']])
+
+ t.end()
+ })
+
+ t.test('brand check', (t) => {
+ // Headers
+ t.throws(() => {
+ const gen = new Headers().entries()
+ // eslint-disable-next-line no-proto
+ gen.__proto__.next()
+ }, TypeError)
+
+ // FormData
+ t.throws(() => {
+ const gen = new FormData().entries()
+ // eslint-disable-next-line no-proto
+ gen.__proto__.next()
+ }, TypeError)
+
+ t.end()
+ })
+
+ t.end()
+})
diff --git a/test/fetch/jsdom-abortcontroller-1910-1464495619.js b/test/fetch/jsdom-abortcontroller-1910-1464495619.js
new file mode 100644
index 0000000..e5a86ab
--- /dev/null
+++ b/test/fetch/jsdom-abortcontroller-1910-1464495619.js
@@ -0,0 +1,26 @@
+'use strict'
+
+const { test } = require('tap')
+const { createServer } = require('http')
+const { once } = require('events')
+const { fetch } = require('../..')
+const { JSDOM } = require('jsdom')
+
+// https://github.com/nodejs/undici/pull/1910#issuecomment-1464495619
+test('third party AbortControllers', async (t) => {
+ const server = createServer((_, res) => res.end()).listen(0)
+
+ const { AbortController } = new JSDOM().window
+ let controller = new AbortController()
+
+ t.teardown(() => {
+ controller.abort()
+ controller = null
+ return server.close()
+ })
+ await once(server, 'listening')
+
+ await t.resolves(fetch(`http://localhost:${server.address().port}`, {
+ signal: controller.signal
+ }))
+})
diff --git a/test/fetch/redirect-cross-origin-header.js b/test/fetch/redirect-cross-origin-header.js
new file mode 100644
index 0000000..fca48c4
--- /dev/null
+++ b/test/fetch/redirect-cross-origin-header.js
@@ -0,0 +1,48 @@
+'use strict'
+
+const { test } = require('tap')
+const { createServer } = require('http')
+const { once } = require('events')
+const { fetch } = require('../..')
+
+test('Cross-origin redirects clear forbidden headers', async (t) => {
+ t.plan(5)
+
+ const server1 = createServer((req, res) => {
+ t.equal(req.headers.cookie, undefined)
+ t.equal(req.headers.authorization, undefined)
+
+ res.end('redirected')
+ }).listen(0)
+
+ const server2 = createServer((req, res) => {
+ t.equal(req.headers.authorization, 'test')
+ t.equal(req.headers.cookie, 'ddd=dddd')
+
+ res.writeHead(302, {
+ ...req.headers,
+ Location: `http://localhost:${server1.address().port}`
+ })
+ res.end()
+ }).listen(0)
+
+ t.teardown(() => {
+ server1.close()
+ server2.close()
+ })
+
+ await Promise.all([
+ once(server1, 'listening'),
+ once(server2, 'listening')
+ ])
+
+ const res = await fetch(`http://localhost:${server2.address().port}`, {
+ headers: {
+ Authorization: 'test',
+ Cookie: 'ddd=dddd'
+ }
+ })
+
+ const text = await res.text()
+ t.equal(text, 'redirected')
+})
diff --git a/test/fetch/redirect.js b/test/fetch/redirect.js
new file mode 100644
index 0000000..7e3681b
--- /dev/null
+++ b/test/fetch/redirect.js
@@ -0,0 +1,50 @@
+'use strict'
+
+const { test } = require('tap')
+const { createServer } = require('http')
+const { once } = require('events')
+const { fetch } = require('../..')
+
+// https://github.com/nodejs/undici/issues/1776
+test('Redirecting with a body does not cancel the current request - #1776', async (t) => {
+ const server = createServer((req, res) => {
+ if (req.url === '/redirect') {
+ res.statusCode = 301
+ res.setHeader('location', '/redirect/')
+ res.write('<a href="/redirect/">Moved Permanently</a>')
+ setTimeout(() => res.end(), 500)
+ return
+ }
+
+ res.write(req.url)
+ res.end()
+ }).listen(0)
+
+ t.teardown(server.close.bind(server))
+ await once(server, 'listening')
+
+ const resp = await fetch(`http://localhost:${server.address().port}/redirect`)
+ t.equal(await resp.text(), '/redirect/')
+ t.ok(resp.redirected)
+})
+
+test('Redirecting with an empty body does not throw an error - #2027', async (t) => {
+ const server = createServer((req, res) => {
+ if (req.url === '/redirect') {
+ res.statusCode = 307
+ res.setHeader('location', '/redirect/')
+ res.write('<a href="/redirect/">Moved Permanently</a>')
+ res.end()
+ return
+ }
+ res.write(req.url)
+ res.end()
+ }).listen(0)
+
+ t.teardown(server.close.bind(server))
+ await once(server, 'listening')
+
+ const resp = await fetch(`http://localhost:${server.address().port}/redirect`, { method: 'PUT', body: '' })
+ t.equal(await resp.text(), '/redirect/')
+ t.ok(resp.redirected)
+})
diff --git a/test/fetch/relative-url.js b/test/fetch/relative-url.js
new file mode 100644
index 0000000..1a4f819
--- /dev/null
+++ b/test/fetch/relative-url.js
@@ -0,0 +1,110 @@
+'use strict'
+
+const { test, afterEach } = require('tap')
+const { createServer } = require('http')
+const { once } = require('events')
+const {
+ getGlobalOrigin,
+ setGlobalOrigin,
+ Response,
+ Request,
+ fetch
+} = require('../..')
+
+afterEach(() => setGlobalOrigin(undefined))
+
+test('setGlobalOrigin & getGlobalOrigin', (t) => {
+ t.equal(getGlobalOrigin(), undefined)
+
+ setGlobalOrigin('http://localhost:3000')
+ t.same(getGlobalOrigin(), new URL('http://localhost:3000'))
+
+ setGlobalOrigin(undefined)
+ t.equal(getGlobalOrigin(), undefined)
+
+ setGlobalOrigin(new URL('http://localhost:3000'))
+ t.same(getGlobalOrigin(), new URL('http://localhost:3000'))
+
+ t.throws(() => {
+ setGlobalOrigin('invalid.url')
+ }, TypeError)
+
+ t.throws(() => {
+ setGlobalOrigin('wss://invalid.protocol')
+ }, TypeError)
+
+ t.throws(() => setGlobalOrigin(true))
+
+ t.end()
+})
+
+test('Response.redirect', (t) => {
+ t.throws(() => {
+ Response.redirect('/relative/path', 302)
+ }, TypeError('Failed to parse URL from /relative/path'))
+
+ t.doesNotThrow(() => {
+ setGlobalOrigin('http://localhost:3000')
+ Response.redirect('/relative/path', 302)
+ })
+
+ setGlobalOrigin('http://localhost:3000')
+ const response = Response.redirect('/relative/path', 302)
+ // See step #7 of https://fetch.spec.whatwg.org/#dom-response-redirect
+ t.equal(response.headers.get('location'), 'http://localhost:3000/relative/path')
+
+ t.end()
+})
+
+test('new Request', (t) => {
+ t.throws(
+ () => new Request('/relative/path'),
+ TypeError('Failed to parse URL from /relative/path')
+ )
+
+ t.doesNotThrow(() => {
+ setGlobalOrigin('http://localhost:3000')
+ // eslint-disable-next-line no-new
+ new Request('/relative/path')
+ })
+
+ setGlobalOrigin('http://localhost:3000')
+ const request = new Request('/relative/path')
+ t.equal(request.url, 'http://localhost:3000/relative/path')
+
+ t.end()
+})
+
+test('fetch', async (t) => {
+ await t.rejects(async () => {
+ await fetch('/relative/path')
+ }, TypeError('Failed to parse URL from /relative/path'))
+
+ t.test('Basic fetch', async (t) => {
+ const server = createServer((req, res) => {
+ t.equal(req.url, '/relative/path')
+ res.end()
+ }).listen(0)
+
+ setGlobalOrigin(`http://localhost:${server.address().port}`)
+ t.teardown(server.close.bind(server))
+ await once(server, 'listening')
+
+ await t.resolves(fetch('/relative/path'))
+ })
+
+ t.test('fetch return', async (t) => {
+ const server = createServer((req, res) => {
+ t.equal(req.url, '/relative/path')
+ res.end()
+ }).listen(0)
+
+ setGlobalOrigin(`http://localhost:${server.address().port}`)
+ t.teardown(server.close.bind(server))
+ await once(server, 'listening')
+
+ const response = await fetch('/relative/path')
+
+ t.equal(response.url, `http://localhost:${server.address().port}/relative/path`)
+ })
+})
diff --git a/test/fetch/request.js b/test/fetch/request.js
new file mode 100644
index 0000000..db2c8e8
--- /dev/null
+++ b/test/fetch/request.js
@@ -0,0 +1,514 @@
+/* globals AbortController */
+
+'use strict'
+
+const { test, teardown } = require('tap')
+const {
+ Request,
+ Headers,
+ fetch
+} = require('../../')
+const {
+ Blob: ThirdPartyBlob,
+ FormData: ThirdPartyFormData
+} = require('formdata-node')
+
+const hasSignalReason = 'reason' in AbortSignal.prototype
+
+test('arg validation', async (t) => {
+ // constructor
+ t.throws(() => {
+ // eslint-disable-next-line
+ new Request()
+ }, TypeError)
+ t.throws(() => {
+ // eslint-disable-next-line
+ new Request('http://asd', 0)
+ }, TypeError)
+ t.throws(() => {
+ const url = new URL('http://asd')
+ url.password = 'asd'
+ // eslint-disable-next-line
+ new Request(url)
+ }, TypeError)
+ t.throws(() => {
+ const url = new URL('http://asd')
+ url.username = 'asd'
+ // eslint-disable-next-line
+ new Request(url)
+ }, TypeError)
+ t.doesNotThrow(() => {
+ // eslint-disable-next-line
+ new Request('http://asd', undefined)
+ }, TypeError)
+ t.throws(() => {
+ // eslint-disable-next-line
+ new Request('http://asd', {
+ window: {}
+ })
+ }, TypeError)
+ t.throws(() => {
+ // eslint-disable-next-line
+ new Request('http://asd', {
+ window: 1
+ })
+ }, TypeError)
+ t.throws(() => {
+ // eslint-disable-next-line
+ new Request('http://asd', {
+ mode: 'navigate'
+ })
+ })
+
+ t.throws(() => {
+ // eslint-disable-next-line
+ new Request('http://asd', {
+ referrerPolicy: 'agjhagna'
+ })
+ }, TypeError)
+
+ t.throws(() => {
+ // eslint-disable-next-line
+ new Request('http://asd', {
+ mode: 'agjhagna'
+ })
+ }, TypeError)
+
+ t.throws(() => {
+ // eslint-disable-next-line
+ new Request('http://asd', {
+ credentials: 'agjhagna'
+ })
+ }, TypeError)
+
+ t.throws(() => {
+ // eslint-disable-next-line
+ new Request('http://asd', {
+ cache: 'agjhagna'
+ })
+ }, TypeError)
+
+ t.throws(() => {
+ // eslint-disable-next-line
+ new Request('http://asd', {
+ method: 'agjhagnaöööö'
+ })
+ }, TypeError)
+
+ t.throws(() => {
+ // eslint-disable-next-line
+ new Request('http://asd', {
+ method: 'TRACE'
+ })
+ }, TypeError)
+
+ t.throws(() => {
+ Request.prototype.destination.toString()
+ }, TypeError)
+
+ t.throws(() => {
+ Request.prototype.referrer.toString()
+ }, TypeError)
+
+ t.throws(() => {
+ Request.prototype.referrerPolicy.toString()
+ }, TypeError)
+
+ t.throws(() => {
+ Request.prototype.mode.toString()
+ }, TypeError)
+
+ t.throws(() => {
+ Request.prototype.credentials.toString()
+ }, TypeError)
+
+ t.throws(() => {
+ Request.prototype.cache.toString()
+ }, TypeError)
+
+ t.throws(() => {
+ Request.prototype.redirect.toString()
+ }, TypeError)
+
+ t.throws(() => {
+ Request.prototype.integrity.toString()
+ }, TypeError)
+
+ t.throws(() => {
+ Request.prototype.keepalive.toString()
+ }, TypeError)
+
+ t.throws(() => {
+ Request.prototype.isReloadNavigation.toString()
+ }, TypeError)
+
+ t.throws(() => {
+ Request.prototype.isHistoryNavigation.toString()
+ }, TypeError)
+
+ t.throws(() => {
+ Request.prototype.signal.toString()
+ }, TypeError)
+
+ t.throws(() => {
+ // eslint-disable-next-line no-unused-expressions
+ Request.prototype.body
+ }, TypeError)
+
+ t.throws(() => {
+ // eslint-disable-next-line no-unused-expressions
+ Request.prototype.bodyUsed
+ }, TypeError)
+
+ t.throws(() => {
+ Request.prototype.clone.call(null)
+ }, TypeError)
+
+ t.doesNotThrow(() => {
+ Request.prototype[Symbol.toStringTag].charAt(0)
+ })
+
+ for (const method of [
+ 'text',
+ 'json',
+ 'arrayBuffer',
+ 'blob',
+ 'formData'
+ ]) {
+ await t.rejects(async () => {
+ await new Request('http://localhost')[method].call({
+ blob () {
+ return {
+ text () {
+ return Promise.resolve('emulating this')
+ }
+ }
+ }
+ })
+ }, TypeError)
+ }
+
+ t.end()
+})
+
+test('undefined window', t => {
+ t.doesNotThrow(() => new Request('http://asd', { window: undefined }))
+ t.end()
+})
+
+test('undefined body', t => {
+ const req = new Request('http://asd', { body: undefined })
+ t.equal(req.body, null)
+ t.end()
+})
+
+test('undefined method', t => {
+ const req = new Request('http://asd', { method: undefined })
+ t.equal(req.method, 'GET')
+ t.end()
+})
+
+test('undefined headers', t => {
+ const req = new Request('http://asd', { headers: undefined })
+ t.equal([...req.headers.entries()].length, 0)
+ t.end()
+})
+
+test('undefined referrer', t => {
+ const req = new Request('http://asd', { referrer: undefined })
+ t.equal(req.referrer, 'about:client')
+ t.end()
+})
+
+test('undefined referrerPolicy', t => {
+ const req = new Request('http://asd', { referrerPolicy: undefined })
+ t.equal(req.referrerPolicy, '')
+ t.end()
+})
+
+test('undefined mode', t => {
+ const req = new Request('http://asd', { mode: undefined })
+ t.equal(req.mode, 'cors')
+ t.end()
+})
+
+test('undefined credentials', t => {
+ const req = new Request('http://asd', { credentials: undefined })
+ t.equal(req.credentials, 'same-origin')
+ t.end()
+})
+
+test('undefined cache', t => {
+ const req = new Request('http://asd', { cache: undefined })
+ t.equal(req.cache, 'default')
+ t.end()
+})
+
+test('undefined redirect', t => {
+ const req = new Request('http://asd', { redirect: undefined })
+ t.equal(req.redirect, 'follow')
+ t.end()
+})
+
+test('undefined keepalive', t => {
+ const req = new Request('http://asd', { keepalive: undefined })
+ t.equal(req.keepalive, false)
+ t.end()
+})
+
+test('undefined integrity', t => {
+ const req = new Request('http://asd', { integrity: undefined })
+ t.equal(req.integrity, '')
+ t.end()
+})
+
+test('null integrity', t => {
+ const req = new Request('http://asd', { integrity: null })
+ t.equal(req.integrity, 'null')
+ t.end()
+})
+
+test('undefined signal', t => {
+ const req = new Request('http://asd', { signal: undefined })
+ t.equal(req.signal.aborted, false)
+ t.end()
+})
+
+test('pre aborted signal', t => {
+ const ac = new AbortController()
+ ac.abort('gwak')
+ const req = new Request('http://asd', { signal: ac.signal })
+ t.equal(req.signal.aborted, true)
+ if (hasSignalReason) {
+ t.equal(req.signal.reason, 'gwak')
+ }
+ t.end()
+})
+
+test('post aborted signal', t => {
+ t.plan(2)
+
+ const ac = new AbortController()
+ const req = new Request('http://asd', { signal: ac.signal })
+ t.equal(req.signal.aborted, false)
+ ac.signal.addEventListener('abort', () => {
+ if (hasSignalReason) {
+ t.equal(req.signal.reason, 'gwak')
+ } else {
+ t.pass()
+ }
+ }, { once: true })
+ ac.abort('gwak')
+})
+
+test('pre aborted signal cloned', t => {
+ const ac = new AbortController()
+ ac.abort('gwak')
+ const req = new Request('http://asd', { signal: ac.signal }).clone()
+ t.equal(req.signal.aborted, true)
+ if (hasSignalReason) {
+ t.equal(req.signal.reason, 'gwak')
+ }
+ t.end()
+})
+
+test('URLSearchParams body with Headers object - issue #1407', async (t) => {
+ const body = new URLSearchParams({
+ abc: 123
+ })
+
+ const request = new Request(
+ 'http://localhost',
+ {
+ method: 'POST',
+ body,
+ headers: {
+ Authorization: 'test'
+ }
+ }
+ )
+
+ t.equal(request.headers.get('content-type'), 'application/x-www-form-urlencoded;charset=UTF-8')
+ t.equal(request.headers.get('authorization'), 'test')
+ t.equal(await request.text(), 'abc=123')
+})
+
+test('post aborted signal cloned', t => {
+ t.plan(2)
+
+ const ac = new AbortController()
+ const req = new Request('http://asd', { signal: ac.signal }).clone()
+ t.equal(req.signal.aborted, false)
+ ac.signal.addEventListener('abort', () => {
+ if (hasSignalReason) {
+ t.equal(req.signal.reason, 'gwak')
+ } else {
+ t.pass()
+ }
+ }, { once: true })
+ ac.abort('gwak')
+})
+
+test('Passing headers in init', (t) => {
+ // https://github.com/nodejs/undici/issues/1400
+ t.test('Headers instance', (t) => {
+ const req = new Request('http://localhost', {
+ headers: new Headers({ key: 'value' })
+ })
+
+ t.equal(req.headers.get('key'), 'value')
+ t.end()
+ })
+
+ t.test('key:value object', (t) => {
+ const req = new Request('http://localhost', {
+ headers: { key: 'value' }
+ })
+
+ t.equal(req.headers.get('key'), 'value')
+ t.end()
+ })
+
+ t.test('[key, value][]', (t) => {
+ const req = new Request('http://localhost', {
+ headers: [['key', 'value']]
+ })
+
+ t.equal(req.headers.get('key'), 'value')
+ t.end()
+ })
+
+ t.end()
+})
+
+test('Symbol.toStringTag', (t) => {
+ const req = new Request('http://localhost')
+
+ t.equal(req[Symbol.toStringTag], 'Request')
+ t.equal(Request.prototype[Symbol.toStringTag], 'Request')
+ t.end()
+})
+
+test('invalid RequestInit values', (t) => {
+ /* eslint-disable no-new */
+ t.throws(() => {
+ new Request('http://l', { mode: 'CoRs' })
+ }, TypeError, 'not exact case = error')
+
+ t.throws(() => {
+ new Request('http://l', { mode: 'random' })
+ }, TypeError)
+
+ t.throws(() => {
+ new Request('http://l', { credentials: 'OMIt' })
+ }, TypeError, 'not exact case = error')
+
+ t.throws(() => {
+ new Request('http://l', { credentials: 'random' })
+ }, TypeError)
+
+ t.throws(() => {
+ new Request('http://l', { cache: 'DeFaULt' })
+ }, TypeError, 'not exact case = error')
+
+ t.throws(() => {
+ new Request('http://l', { cache: 'random' })
+ }, TypeError)
+
+ t.throws(() => {
+ new Request('http://l', { redirect: 'FOllOW' })
+ }, TypeError, 'not exact case = error')
+
+ t.throws(() => {
+ new Request('http://l', { redirect: 'random' })
+ }, TypeError)
+ /* eslint-enable no-new */
+
+ t.end()
+})
+
+test('RequestInit.signal option', async (t) => {
+ t.throws(() => {
+ // eslint-disable-next-line no-new
+ new Request('http://asd', {
+ signal: true
+ })
+ }, TypeError)
+
+ await t.rejects(fetch('http://asd', {
+ signal: false
+ }), TypeError)
+})
+
+test('constructing Request with third party Blob body', async (t) => {
+ const blob = new ThirdPartyBlob(['text'])
+ const req = new Request('http://asd', {
+ method: 'POST',
+ body: blob
+ })
+ t.equal(await req.text(), 'text')
+})
+test('constructing Request with third party FormData body', async (t) => {
+ const form = new ThirdPartyFormData()
+ form.set('key', 'value')
+ const req = new Request('http://asd', {
+ method: 'POST',
+ body: form
+ })
+ const contentType = req.headers.get('content-type').split('=')
+ t.equal(contentType[0], 'multipart/form-data; boundary')
+ t.ok((await req.text()).startsWith(`--${contentType[1]}`))
+})
+
+// https://github.com/nodejs/undici/issues/2050
+test('set-cookie headers get cleared when passing a Request as first param', (t) => {
+ const req1 = new Request('http://localhost', {
+ headers: {
+ 'set-cookie': 'a=1'
+ }
+ })
+
+ t.same([...req1.headers], [['set-cookie', 'a=1']])
+ const req2 = new Request(req1, { headers: {} })
+
+ t.same([...req2.headers], [])
+ t.same(req2.headers.getSetCookie(), [])
+ t.end()
+})
+
+// https://github.com/nodejs/undici/issues/2124
+test('request.referrer', (t) => {
+ for (const referrer of ['about://client', 'about://client:1234']) {
+ const request = new Request('http://a', { referrer })
+
+ t.equal(request.referrer, 'about:client')
+ }
+
+ t.end()
+})
+
+// https://github.com/nodejs/undici/issues/2445
+test('Clone the set-cookie header when Request is passed as the first parameter and no header is passed.', (t) => {
+ t.plan(2)
+ const request = new Request('http://localhost', { headers: { 'set-cookie': 'A' } })
+ const request2 = new Request(request)
+ request2.headers.append('set-cookie', 'B')
+ t.equal(request.headers.getSetCookie().join(', '), request.headers.get('set-cookie'))
+ t.equal(request2.headers.getSetCookie().join(', '), request2.headers.get('set-cookie'))
+})
+
+// Tests for optimization introduced in https://github.com/nodejs/undici/pull/2456
+test('keys to object prototypes method', (t) => {
+ t.plan(1)
+ const request = new Request('http://localhost', { method: 'hasOwnProperty' })
+ t.ok(typeof request.method === 'string')
+})
+
+// https://github.com/nodejs/undici/issues/2465
+test('Issue#2465', async (t) => {
+ t.plan(1)
+ const request = new Request('http://localhost', { body: new SharedArrayBuffer(0), method: 'POST' })
+ t.equal(await request.text(), '[object SharedArrayBuffer]')
+})
+
+teardown(() => process.exit())
diff --git a/test/fetch/resource-timing.js b/test/fetch/resource-timing.js
new file mode 100644
index 0000000..d266f28
--- /dev/null
+++ b/test/fetch/resource-timing.js
@@ -0,0 +1,72 @@
+'use strict'
+
+const { test } = require('tap')
+const { createServer } = require('http')
+const { nodeMajor, nodeMinor } = require('../../lib/core/util')
+const { fetch } = require('../..')
+
+const {
+ PerformanceObserver,
+ performance
+} = require('perf_hooks')
+
+const skip = nodeMajor < 18 || (nodeMajor === 18 && nodeMinor < 2)
+
+test('should create a PerformanceResourceTiming after each fetch request', { skip }, (t) => {
+ t.plan(8)
+
+ const obs = new PerformanceObserver(list => {
+ const expectedResourceEntryName = `http://localhost:${server.address().port}/`
+
+ const entries = list.getEntries()
+ t.equal(entries.length, 1)
+ const [entry] = entries
+ t.same(entry.name, expectedResourceEntryName)
+ t.strictSame(entry.entryType, 'resource')
+
+ t.ok(entry.duration >= 0)
+ t.ok(entry.startTime >= 0)
+
+ const entriesByName = list.getEntriesByName(expectedResourceEntryName)
+ t.equal(entriesByName.length, 1)
+ t.strictSame(entriesByName[0], entry)
+
+ obs.disconnect()
+ performance.clearResourceTimings()
+ })
+
+ obs.observe({ entryTypes: ['resource'] })
+
+ const server = createServer((req, res) => {
+ res.end('ok')
+ }).listen(0, async () => {
+ const body = await fetch(`http://localhost:${server.address().port}`)
+ t.strictSame('ok', await body.text())
+ })
+
+ t.teardown(server.close.bind(server))
+})
+
+test('should include encodedBodySize in performance entry', { skip }, (t) => {
+ t.plan(4)
+ const obs = new PerformanceObserver(list => {
+ const [entry] = list.getEntries()
+ t.equal(entry.encodedBodySize, 2)
+ t.equal(entry.decodedBodySize, 2)
+ t.equal(entry.transferSize, 2 + 300)
+
+ obs.disconnect()
+ performance.clearResourceTimings()
+ })
+
+ obs.observe({ entryTypes: ['resource'] })
+
+ const server = createServer((req, res) => {
+ res.end('ok')
+ }).listen(0, async () => {
+ const body = await fetch(`http://localhost:${server.address().port}`)
+ t.strictSame('ok', await body.text())
+ })
+
+ t.teardown(server.close.bind(server))
+})
diff --git a/test/fetch/response-json.js b/test/fetch/response-json.js
new file mode 100644
index 0000000..6244fbf
--- /dev/null
+++ b/test/fetch/response-json.js
@@ -0,0 +1,113 @@
+'use strict'
+
+const { test } = require('tap')
+const { Response } = require('../../')
+
+// https://github.com/web-platform-tests/wpt/pull/32825/
+
+const APPLICATION_JSON = 'application/json'
+const FOO_BAR = 'foo/bar'
+
+const INIT_TESTS = [
+ [undefined, 200, '', APPLICATION_JSON, {}],
+ [{ status: 400 }, 400, '', APPLICATION_JSON, {}],
+ [{ statusText: 'foo' }, 200, 'foo', APPLICATION_JSON, {}],
+ [{ headers: {} }, 200, '', APPLICATION_JSON, {}],
+ [{ headers: { 'content-type': FOO_BAR } }, 200, '', FOO_BAR, {}],
+ [{ headers: { 'x-foo': 'bar' } }, 200, '', APPLICATION_JSON, { 'x-foo': 'bar' }]
+]
+
+test('Check response returned by static json() with init', async (t) => {
+ for (const [init, expectedStatus, expectedStatusText, expectedContentType, expectedHeaders] of INIT_TESTS) {
+ const response = Response.json('hello world', init)
+ t.equal(response.type, 'default', "Response's type is default")
+ t.equal(response.status, expectedStatus, "Response's status is " + expectedStatus)
+ t.equal(response.statusText, expectedStatusText, "Response's statusText is " + JSON.stringify(expectedStatusText))
+ t.equal(response.headers.get('content-type'), expectedContentType, "Response's content-type is " + expectedContentType)
+ for (const key in expectedHeaders) {
+ t.equal(response.headers.get(key), expectedHeaders[key], "Response's header " + key + ' is ' + JSON.stringify(expectedHeaders[key]))
+ }
+
+ const data = await response.json()
+ t.equal(data, 'hello world', "Response's body is 'hello world'")
+ }
+
+ t.end()
+})
+
+test('Throws TypeError when calling static json() with an invalid status', (t) => {
+ const nullBodyStatus = [204, 205, 304]
+
+ for (const status of nullBodyStatus) {
+ t.throws(() => {
+ Response.json('hello world', { status })
+ }, TypeError, `Throws TypeError when calling static json() with a status of ${status}`)
+ }
+
+ t.end()
+})
+
+test('Check static json() encodes JSON objects correctly', async (t) => {
+ const response = Response.json({ foo: 'bar' })
+ const data = await response.json()
+ t.equal(typeof data, 'object', "Response's json body is an object")
+ t.equal(data.foo, 'bar', "Response's json body is { foo: 'bar' }")
+
+ t.end()
+})
+
+test('Check static json() throws when data is not encodable', (t) => {
+ t.throws(() => {
+ Response.json(Symbol('foo'))
+ }, TypeError)
+
+ t.end()
+})
+
+test('Check static json() throws when data is circular', (t) => {
+ const a = { b: 1 }
+ a.a = a
+
+ t.throws(() => {
+ Response.json(a)
+ }, TypeError)
+
+ t.end()
+})
+
+test('Check static json() propagates JSON serializer errors', (t) => {
+ class CustomError extends Error {
+ name = 'CustomError'
+ }
+
+ t.throws(() => {
+ Response.json({ get foo () { throw new CustomError('bar') } })
+ }, CustomError)
+
+ t.end()
+})
+
+// note: these tests are not part of any WPTs
+test('unserializable values', (t) => {
+ t.throws(() => {
+ Response.json(Symbol('symbol'))
+ }, TypeError)
+
+ t.throws(() => {
+ Response.json(undefined)
+ }, TypeError)
+
+ t.throws(() => {
+ Response.json()
+ }, TypeError)
+
+ t.end()
+})
+
+test('invalid init', (t) => {
+ t.throws(() => {
+ Response.json(null, 3)
+ }, TypeError)
+
+ t.end()
+})
diff --git a/test/fetch/response.js b/test/fetch/response.js
new file mode 100644
index 0000000..422c7ef
--- /dev/null
+++ b/test/fetch/response.js
@@ -0,0 +1,257 @@
+'use strict'
+
+const { test } = require('tap')
+const {
+ Response
+} = require('../../')
+const { ReadableStream } = require('stream/web')
+const {
+ Blob: ThirdPartyBlob,
+ FormData: ThirdPartyFormData
+} = require('formdata-node')
+
+test('arg validation', async (t) => {
+ // constructor
+ t.throws(() => {
+ // eslint-disable-next-line
+ new Response(null, 0)
+ }, TypeError)
+ t.throws(() => {
+ // eslint-disable-next-line
+ new Response(null, {
+ status: 99
+ })
+ }, RangeError)
+ t.throws(() => {
+ // eslint-disable-next-line
+ new Response(null, {
+ status: 600
+ })
+ }, RangeError)
+ t.throws(() => {
+ // eslint-disable-next-line
+ new Response(null, {
+ status: '600'
+ })
+ }, RangeError)
+ t.throws(() => {
+ // eslint-disable-next-line
+ new Response(null, {
+ statusText: '\u0000'
+ })
+ }, TypeError)
+
+ for (const nullStatus of [204, 205, 304]) {
+ t.throws(() => {
+ // eslint-disable-next-line
+ new Response(new ArrayBuffer(16), {
+ status: nullStatus
+ })
+ }, TypeError)
+ }
+
+ t.doesNotThrow(() => {
+ Response.prototype[Symbol.toStringTag].charAt(0)
+ }, TypeError)
+
+ t.throws(() => {
+ Response.prototype.type.toString()
+ }, TypeError)
+
+ t.throws(() => {
+ Response.prototype.url.toString()
+ }, TypeError)
+
+ t.throws(() => {
+ Response.prototype.redirected.toString()
+ }, TypeError)
+
+ t.throws(() => {
+ Response.prototype.status.toString()
+ }, TypeError)
+
+ t.throws(() => {
+ Response.prototype.ok.toString()
+ }, TypeError)
+
+ t.throws(() => {
+ Response.prototype.statusText.toString()
+ }, TypeError)
+
+ t.throws(() => {
+ Response.prototype.headers.toString()
+ }, TypeError)
+
+ t.throws(() => {
+ // eslint-disable-next-line no-unused-expressions
+ Response.prototype.body
+ }, TypeError)
+
+ t.throws(() => {
+ // eslint-disable-next-line no-unused-expressions
+ Response.prototype.bodyUsed
+ }, TypeError)
+
+ t.throws(() => {
+ Response.prototype.clone.call(null)
+ }, TypeError)
+
+ await t.rejects(async () => {
+ await new Response('http://localhost').text.call({
+ blob () {
+ return {
+ text () {
+ return Promise.resolve('emulating response.blob()')
+ }
+ }
+ }
+ })
+ }, TypeError)
+
+ t.end()
+})
+
+test('response clone', (t) => {
+ // https://github.com/nodejs/undici/issues/1122
+ const response1 = new Response(null, { status: 201 })
+ const response2 = new Response(undefined, { status: 201 })
+
+ t.equal(response1.body, response1.clone().body)
+ t.equal(response2.body, response2.clone().body)
+ t.equal(response2.body, null)
+ t.end()
+})
+
+test('Symbol.toStringTag', (t) => {
+ const resp = new Response()
+
+ t.equal(resp[Symbol.toStringTag], 'Response')
+ t.equal(Response.prototype[Symbol.toStringTag], 'Response')
+ t.end()
+})
+
+test('async iterable body', async (t) => {
+ const asyncIterable = {
+ async * [Symbol.asyncIterator] () {
+ yield 'a'
+ yield 'b'
+ yield 'c'
+ }
+ }
+
+ const response = new Response(asyncIterable)
+ t.equal(await response.text(), 'abc')
+ t.end()
+})
+
+// https://github.com/nodejs/node/pull/43752#issuecomment-1179678544
+test('Modifying headers using Headers.prototype.set', (t) => {
+ const response = new Response('body', {
+ headers: {
+ 'content-type': 'test/test',
+ 'Content-Encoding': 'hello/world'
+ }
+ })
+
+ const response2 = response.clone()
+
+ response.headers.set('content-type', 'application/wasm')
+ response.headers.set('Content-Encoding', 'world/hello')
+
+ t.equal(response.headers.get('content-type'), 'application/wasm')
+ t.equal(response.headers.get('Content-Encoding'), 'world/hello')
+
+ response2.headers.delete('content-type')
+ response2.headers.delete('Content-Encoding')
+
+ t.equal(response2.headers.get('content-type'), null)
+ t.equal(response2.headers.get('Content-Encoding'), null)
+
+ t.end()
+})
+
+// https://github.com/nodejs/node/issues/43838
+test('constructing a Response with a ReadableStream body', { skip: process.version.startsWith('v16.') }, async (t) => {
+ const text = '{"foo":"bar"}'
+ const uint8 = new TextEncoder().encode(text)
+
+ t.test('Readable stream with Uint8Array chunks', async (t) => {
+ const readable = new ReadableStream({
+ start (controller) {
+ controller.enqueue(uint8)
+ controller.close()
+ }
+ })
+
+ const response1 = new Response(readable)
+ const response2 = response1.clone()
+ const response3 = response1.clone()
+
+ t.equal(await response1.text(), text)
+ t.same(await response2.arrayBuffer(), uint8.buffer)
+ t.same(await response3.json(), JSON.parse(text))
+
+ t.end()
+ })
+
+ t.test('Readable stream with non-Uint8Array chunks', async (t) => {
+ const readable = new ReadableStream({
+ start (controller) {
+ controller.enqueue(text) // string
+ controller.close()
+ }
+ })
+
+ const response = new Response(readable)
+
+ await t.rejects(response.text(), TypeError)
+
+ t.end()
+ })
+
+ t.test('Readable with ArrayBuffer chunk still throws', { skip: process.version.startsWith('v16.') }, async (t) => {
+ const readable = new ReadableStream({
+ start (controller) {
+ controller.enqueue(uint8.buffer)
+ controller.close()
+ }
+ })
+
+ const response1 = new Response(readable)
+ const response2 = response1.clone()
+ const response3 = response1.clone()
+ // const response4 = response1.clone()
+
+ await t.rejects(response1.arrayBuffer(), TypeError)
+ await t.rejects(response2.text(), TypeError)
+ await t.rejects(response3.json(), TypeError)
+ // TODO: on Node v16.8.0, this throws a TypeError
+ // because the body is detected as disturbed.
+ // await t.rejects(response4.blob(), TypeError)
+
+ t.end()
+ })
+
+ t.end()
+})
+
+test('constructing Response with third party Blob body', async (t) => {
+ const blob = new ThirdPartyBlob(['text'])
+ const res = new Response(blob)
+ t.equal(await res.text(), 'text')
+})
+test('constructing Response with third party FormData body', async (t) => {
+ const form = new ThirdPartyFormData()
+ form.set('key', 'value')
+ const res = new Response(form)
+ const contentType = res.headers.get('content-type').split('=')
+ t.equal(contentType[0], 'multipart/form-data; boundary')
+ t.ok((await res.text()).startsWith(`--${contentType[1]}`))
+})
+
+// https://github.com/nodejs/undici/issues/2465
+test('Issue#2465', async (t) => {
+ t.plan(1)
+ const response = new Response(new SharedArrayBuffer(0))
+ t.equal(await response.text(), '[object SharedArrayBuffer]')
+})
diff --git a/test/fetch/user-agent.js b/test/fetch/user-agent.js
new file mode 100644
index 0000000..2e37ea5
--- /dev/null
+++ b/test/fetch/user-agent.js
@@ -0,0 +1,32 @@
+'use strict'
+
+const { test, skip } = require('tap')
+const events = require('events')
+const http = require('http')
+const undici = require('../../')
+const { nodeMajor } = require('../../lib/core/util')
+
+if (nodeMajor === 16) {
+ skip('esbuild uses static blocks with --keep-names which node 16.8 does not have')
+ process.exit()
+}
+
+const nodeBuild = require('../../undici-fetch.js')
+
+test('user-agent defaults correctly', async (t) => {
+ const server = http.createServer((req, res) => {
+ res.end(JSON.stringify({ userAgentHeader: req.headers['user-agent'] }))
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0)
+ await events.once(server, 'listening')
+ const url = `http://localhost:${server.address().port}`
+ const [nodeBuildJSON, undiciJSON] = await Promise.all([
+ nodeBuild.fetch(url).then((body) => body.json()),
+ undici.fetch(url).then((body) => body.json())
+ ])
+
+ t.same(nodeBuildJSON.userAgentHeader, 'node')
+ t.same(undiciJSON.userAgentHeader, 'undici')
+})
diff --git a/test/fetch/util.js b/test/fetch/util.js
new file mode 100644
index 0000000..02b75bc
--- /dev/null
+++ b/test/fetch/util.js
@@ -0,0 +1,281 @@
+'use strict'
+
+const t = require('tap')
+const { test } = t
+
+const util = require('../../lib/fetch/util')
+const { HeadersList } = require('../../lib/fetch/headers')
+
+test('responseURL', (t) => {
+ t.plan(2)
+
+ t.ok(util.responseURL({
+ urlList: [
+ new URL('http://asd'),
+ new URL('http://fgh')
+ ]
+ }))
+ t.notOk(util.responseURL({
+ urlList: []
+ }))
+})
+
+test('responseLocationURL', (t) => {
+ t.plan(3)
+
+ const acceptHeaderList = new HeadersList()
+ acceptHeaderList.append('Accept', '*/*')
+
+ const locationHeaderList = new HeadersList()
+ locationHeaderList.append('Location', 'http://asd')
+
+ t.notOk(util.responseLocationURL({
+ status: 200
+ }))
+ t.notOk(util.responseLocationURL({
+ status: 301,
+ headersList: acceptHeaderList
+ }))
+ t.ok(util.responseLocationURL({
+ status: 301,
+ headersList: locationHeaderList,
+ urlList: [
+ new URL('http://asd'),
+ new URL('http://fgh')
+ ]
+ }))
+})
+
+test('requestBadPort', (t) => {
+ t.plan(3)
+
+ t.equal('allowed', util.requestBadPort({
+ urlList: [new URL('https://asd')]
+ }))
+ t.equal('blocked', util.requestBadPort({
+ urlList: [new URL('http://asd:7')]
+ }))
+ t.equal('blocked', util.requestBadPort({
+ urlList: [new URL('https://asd:7')]
+ }))
+})
+
+// https://html.spec.whatwg.org/multipage/origin.html#same-origin
+// look at examples
+test('sameOrigin', (t) => {
+ t.test('first test', (t) => {
+ const A = {
+ protocol: 'https:',
+ hostname: 'example.org',
+ port: ''
+ }
+
+ const B = {
+ protocol: 'https:',
+ hostname: 'example.org',
+ port: ''
+ }
+
+ t.ok(util.sameOrigin(A, B))
+ t.end()
+ })
+
+ t.test('second test', (t) => {
+ const A = {
+ protocol: 'https:',
+ hostname: 'example.org',
+ port: '314'
+ }
+
+ const B = {
+ protocol: 'https:',
+ hostname: 'example.org',
+ port: '420'
+ }
+
+ t.notOk(util.sameOrigin(A, B))
+ t.end()
+ })
+
+ t.test('obviously shouldn\'t be equal', (t) => {
+ t.notOk(util.sameOrigin(
+ { protocol: 'http:', hostname: 'example.org' },
+ { protocol: 'https:', hostname: 'example.org' }
+ ))
+
+ t.notOk(util.sameOrigin(
+ { protocol: 'https:', hostname: 'example.org' },
+ { protocol: 'https:', hostname: 'example.com' }
+ ))
+
+ t.end()
+ })
+
+ t.test('file:// urls', (t) => {
+ // urls with opaque origins should return true
+
+ const a = new URL('file:///C:/undici')
+ const b = new URL('file:///var/undici')
+
+ t.ok(util.sameOrigin(a, b))
+ t.end()
+ })
+
+ t.end()
+})
+
+test('isURLPotentiallyTrustworthy', (t) => {
+ const valid = ['http://127.0.0.1', 'http://localhost.localhost',
+ 'http://[::1]', 'http://adb.localhost', 'https://something.com', 'wss://hello.com',
+ 'file:///link/to/file.txt', 'data:text/plain;base64,randomstring', 'about:blank', 'about:srcdoc']
+ const invalid = ['http://121.3.4.5:55', 'null:8080', 'something:8080']
+
+ t.plan(valid.length + invalid.length + 1)
+ t.notOk(util.isURLPotentiallyTrustworthy('string'))
+
+ for (const url of valid) {
+ const instance = new URL(url)
+ t.ok(util.isURLPotentiallyTrustworthy(instance))
+ }
+
+ for (const url of invalid) {
+ const instance = new URL(url)
+ t.notOk(util.isURLPotentiallyTrustworthy(instance))
+ }
+})
+
+test('setRequestReferrerPolicyOnRedirect', nested => {
+ nested.plan(7)
+
+ nested.test('should set referrer policy from response headers on redirect', t => {
+ const request = {
+ referrerPolicy: 'no-referrer, strict-origin-when-cross-origin'
+ }
+
+ const actualResponse = {
+ headersList: new HeadersList()
+ }
+
+ t.plan(1)
+
+ actualResponse.headersList.append('Connection', 'close')
+ actualResponse.headersList.append('Location', 'https://some-location.com/redirect')
+ actualResponse.headersList.append('Referrer-Policy', 'origin')
+ util.setRequestReferrerPolicyOnRedirect(request, actualResponse)
+
+ t.equal(request.referrerPolicy, 'origin')
+ })
+
+ nested.test('should select the first valid policy from a response', t => {
+ const request = {
+ referrerPolicy: 'no-referrer, strict-origin-when-cross-origin'
+ }
+
+ const actualResponse = {
+ headersList: new HeadersList()
+ }
+
+ t.plan(1)
+
+ actualResponse.headersList.append('Connection', 'close')
+ actualResponse.headersList.append('Location', 'https://some-location.com/redirect')
+ actualResponse.headersList.append('Referrer-Policy', 'asdas, origin')
+ util.setRequestReferrerPolicyOnRedirect(request, actualResponse)
+
+ t.equal(request.referrerPolicy, 'origin')
+ })
+
+ nested.test('should select the first valid policy from a response#2', t => {
+ const request = {
+ referrerPolicy: 'no-referrer, strict-origin-when-cross-origin'
+ }
+
+ const actualResponse = {
+ headersList: new HeadersList()
+ }
+
+ t.plan(1)
+
+ actualResponse.headersList.append('Connection', 'close')
+ actualResponse.headersList.append('Location', 'https://some-location.com/redirect')
+ actualResponse.headersList.append('Referrer-Policy', 'no-referrer, asdas, origin, 0943sd')
+ util.setRequestReferrerPolicyOnRedirect(request, actualResponse)
+
+ t.equal(request.referrerPolicy, 'origin')
+ })
+
+ nested.test('should pick the last fallback over invalid policy tokens', t => {
+ const request = {
+ referrerPolicy: 'no-referrer, strict-origin-when-cross-origin'
+ }
+
+ const actualResponse = {
+ headersList: new HeadersList()
+ }
+
+ t.plan(1)
+
+ actualResponse.headersList.append('Connection', 'close')
+ actualResponse.headersList.append('Location', 'https://some-location.com/redirect')
+ actualResponse.headersList.append('Referrer-Policy', 'origin, asdas, asdaw34')
+ util.setRequestReferrerPolicyOnRedirect(request, actualResponse)
+
+ t.equal(request.referrerPolicy, 'origin')
+ })
+
+ nested.test('should set not change request referrer policy if no Referrer-Policy from initial redirect response', t => {
+ const request = {
+ referrerPolicy: 'no-referrer, strict-origin-when-cross-origin'
+ }
+
+ const actualResponse = {
+ headersList: new HeadersList()
+ }
+
+ t.plan(1)
+
+ actualResponse.headersList.append('Connection', 'close')
+ actualResponse.headersList.append('Location', 'https://some-location.com/redirect')
+ util.setRequestReferrerPolicyOnRedirect(request, actualResponse)
+
+ t.equal(request.referrerPolicy, 'no-referrer, strict-origin-when-cross-origin')
+ })
+
+ nested.test('should set not change request referrer policy if the policy is a non-valid Referrer Policy', t => {
+ const initial = 'no-referrer, strict-origin-when-cross-origin'
+ const request = {
+ referrerPolicy: initial
+ }
+ const actualResponse = {
+ headersList: new HeadersList()
+ }
+
+ t.plan(1)
+
+ actualResponse.headersList.append('Connection', 'close')
+ actualResponse.headersList.append('Location', 'https://some-location.com/redirect')
+ actualResponse.headersList.append('Referrer-Policy', 'asdasd')
+ util.setRequestReferrerPolicyOnRedirect(request, actualResponse)
+
+ t.equal(request.referrerPolicy, initial)
+ })
+
+ nested.test('should set not change request referrer policy if the policy is a non-valid Referrer Policy', t => {
+ const initial = 'no-referrer, strict-origin-when-cross-origin'
+ const request = {
+ referrerPolicy: initial
+ }
+ const actualResponse = {
+ headersList: new HeadersList()
+ }
+
+ t.plan(1)
+
+ actualResponse.headersList.append('Connection', 'close')
+ actualResponse.headersList.append('Location', 'https://some-location.com/redirect')
+ actualResponse.headersList.append('Referrer-Policy', 'asdasd, asdasa, 12daw,')
+ util.setRequestReferrerPolicyOnRedirect(request, actualResponse)
+
+ t.equal(request.referrerPolicy, initial)
+ })
+})
diff --git a/test/fixed-queue.js b/test/fixed-queue.js
new file mode 100644
index 0000000..812f421
--- /dev/null
+++ b/test/fixed-queue.js
@@ -0,0 +1,38 @@
+'use strict'
+
+const { test } = require('tap')
+
+const FixedQueue = require('../lib/node/fixed-queue')
+
+test('fixed queue 1', (t) => {
+ t.plan(5)
+
+ const queue = new FixedQueue()
+ t.equal(queue.head, queue.tail)
+ t.ok(queue.isEmpty())
+ queue.push('a')
+ t.ok(!queue.isEmpty())
+ t.equal(queue.shift(), 'a')
+ t.equal(queue.shift(), null)
+})
+
+test('fixed queue 2', (t) => {
+ t.plan(7 + 2047)
+
+ const queue = new FixedQueue()
+ for (let i = 0; i < 2047; i++) {
+ queue.push('a')
+ }
+ t.ok(queue.head.isFull())
+ queue.push('a')
+ t.ok(!queue.head.isFull())
+
+ t.not(queue.head, queue.tail)
+ for (let i = 0; i < 2047; i++) {
+ t.equal(queue.shift(), 'a')
+ }
+ t.equal(queue.head, queue.tail)
+ t.ok(!queue.isEmpty())
+ t.equal(queue.shift(), 'a')
+ t.ok(queue.isEmpty())
+})
diff --git a/test/fixtures/ca.pem b/test/fixtures/ca.pem
new file mode 100644
index 0000000..c126543
--- /dev/null
+++ b/test/fixtures/ca.pem
@@ -0,0 +1,16 @@
+-----BEGIN CERTIFICATE-----
+MIIChDCCAe2gAwIBAgIJAMsVOuISYJ/GMA0GCSqGSIb3DQEBCwUAMHoxCzAJBgNV
+BAYTAlVTMQswCQYDVQQIDAJDQTELMAkGA1UEBwwCU0YxDzANBgNVBAoMBkpveWVu
+dDEQMA4GA1UECwwHTm9kZS5qczEMMAoGA1UEAwwDY2ExMSAwHgYJKoZIhvcNAQkB
+FhFyeUB0aW55Y2xvdWRzLm9yZzAgFw0xODExMTYxODQyMjBaGA8yMjkyMDgzMDE4
+NDIyMFowejELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMQswCQYDVQQHDAJTRjEP
+MA0GA1UECgwGSm95ZW50MRAwDgYDVQQLDAdOb2RlLmpzMQwwCgYDVQQDDANjYTEx
+IDAeBgkqhkiG9w0BCQEWEXJ5QHRpbnljbG91ZHMub3JnMIGfMA0GCSqGSIb3DQEB
+AQUAA4GNADCBiQKBgQDrNdKjVKhbxKbrDRLdy45u9vsU3IH8C3qFcLF5wqf+g7OC
+vMOOrFDM6mL5iYwuYaLRvAtsC0mtGPzBGyFflxGhiBYaOhi7nCKEsUkFuNYlCzX+
+FflT04JYT3qWPLL7rT32GXpABND/8DEnj5D5liYYNR05PjV1fUnGg1gPqXVxbwID
+AQABoxAwDjAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBCwUAA4GBAHhsWFy6m6VO
+AjK14n0XCSM66ltk9qMKpOryXneLhmmkOQbJd7oavueUWzMdszWLMKhrBoXjmvuW
+QceutP9IUq1Kzw7a/B+lLPD90xfLMr7tNLAxZoJmq/NAUI63M3nJGpX0HkjnYwoU
+ekzNkKt5TggwcqqzK+cCSG1wDvJ+wjiD
+-----END CERTIFICATE-----
diff --git a/test/fixtures/cert.pem b/test/fixtures/cert.pem
new file mode 100644
index 0000000..664d00c
--- /dev/null
+++ b/test/fixtures/cert.pem
@@ -0,0 +1,18 @@
+-----BEGIN CERTIFICATE-----
+MIIC2DCCAkGgAwIBAgIJAOzJuFYnDamoMA0GCSqGSIb3DQEBCwUAMHoxCzAJBgNV
+BAYTAlVTMQswCQYDVQQIDAJDQTELMAkGA1UEBwwCU0YxDzANBgNVBAoMBkpveWVu
+dDEQMA4GA1UECwwHTm9kZS5qczEMMAoGA1UEAwwDY2ExMSAwHgYJKoZIhvcNAQkB
+FhFyeUB0aW55Y2xvdWRzLm9yZzAgFw0xODExMTYxODQyMjFaGA8yMjkyMDgzMDE4
+NDIyMVowfTELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMQswCQYDVQQHDAJTRjEP
+MA0GA1UECgwGSm95ZW50MRAwDgYDVQQLDAdOb2RlLmpzMQ8wDQYDVQQDDAZhZ2Vu
+dDExIDAeBgkqhkiG9w0BCQEWEXJ5QHRpbnljbG91ZHMub3JnMIGfMA0GCSqGSIb3
+DQEBAQUAA4GNADCBiQKBgQDvVEBwFjfiirsDjlZB+CjYNMNCqdJe27hqK/b72AnL
+jgN6mLcXCOABJC5N61TGFkiF9Zndh6IyFXRZVb4gQX4zxNDRuAydo95BmiYHGV0v
+t1ZXsLv7XrfQu6USLRtpZMe1cNULjsAB7raN+1hEN1CPMSmSjWc7MKPgv09QYJ5j
+cQIDAQABo2EwXzBdBggrBgEFBQcBAQRRME8wIwYIKwYBBQUHMAGGF2h0dHA6Ly9v
+Y3NwLm5vZGVqcy5vcmcvMCgGCCsGAQUFBzAChhxodHRwOi8vY2Eubm9kZWpzLm9y
+Zy9jYS5jZXJ0MA0GCSqGSIb3DQEBCwUAA4GBAHrKvx2Z4fsF7b3VRgiIbdbFCfxY
+ICvoJ0+BObYPjqIZZm9+/5c36SpzKzGO9CN9qUEj3KxPmijnb+Zjsm1CSCrG1m04
+C73+AjAIPnQ+eWZnF1K4L2kuEDTpv8nQzYKYiGxsmW58PSMeAq1TmaFwtSW3TxHX
+7ROnqBX0uXQlOo1m
+-----END CERTIFICATE-----
diff --git a/test/fixtures/client-ca-crt.pem b/test/fixtures/client-ca-crt.pem
new file mode 100644
index 0000000..3abfd04
--- /dev/null
+++ b/test/fixtures/client-ca-crt.pem
@@ -0,0 +1,17 @@
+-----BEGIN CERTIFICATE-----
+MIICqDCCAZACCQC0Hman8CosTDANBgkqhkiG9w0BAQsFADAVMRMwEQYDVQQDDApu
+b2RlanMub3JnMCAXDTIyMDcxOTE2MzQwMloYDzIxMjIwNzIwMTYzNDAyWjAVMRMw
+EQYDVQQDDApub2RlanMub3JnMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC
+AQEAyrmvIOhsVJAinUZ0Np4o5cPz09arWAZnnDsMnU0d+NtI0lWOFCnpzJbER9eB
+gJpRkOdkcsQFr0OcalExG4lQrj+yGdtLGSXVcE0aNsVSBNbNgaLbOFWfpA4c7pTF
+SBLJdJ7pZ2LDrM2mXaQA30di3INsZOvuTnDSAEE8bwxnM7jDnTCOGD4asgzgknHa
+NqYWJqrfEPoMcEtThX9XjBLlRq5X3YFAR8SRbMQDt2xbDLWO8mGo/y4Ezp+ol9dP
+OdkX3f728EIgfk8fM7rpvHzJb8E6NPdKK/kqCjQxRJ4RMsRqKwiTgPcEqut0L6Kg
+jGoDvOnc3dZ2QBrxGTYPrgZF2QIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQA2DC4n
+GNqQIABC82e3CovVH/LYB8M/PaqMwmXDI8kAKwk3j3lTHYD0WIyaFtCL4z/2GyDs
+sgRmMlx5xVgXNv+8e793TMOqJ/0zixijguatR8r9GWdPAPhqCyCNrmUA26eyHEUV
+Hx9mU7RNjv+qVe7fNXBkDorsyecclnDcxUd9k2C+RbjitnSKvhP64XqxAGk49HUH
+3gw5uZw9uVlmD/dPSeKeSO4TX1HECH+WmPBKrBrcFGXNwGNzst8pFe3YVLLuseIq
+4d5ngaOThGzVDJdsGIxhDfDBfH5FzDTMgEJxQQ3yXYwPR3zF4Ntn13oDkIu/vgbH
+4n1eYIau6/1Y9OLX
+-----END CERTIFICATE-----
diff --git a/test/fixtures/client-crt-2048.pem b/test/fixtures/client-crt-2048.pem
new file mode 100644
index 0000000..6d07ec1
--- /dev/null
+++ b/test/fixtures/client-crt-2048.pem
@@ -0,0 +1,22 @@
+-----BEGIN CERTIFICATE-----
+MIIDkzCCAnugAwIBAgIUF2CLbUCxPnxARRlO7pANiXtZoLIwDQYJKoZIhvcNAQEL
+BQAwWTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM
+GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDESMBAGA1UEAwwJbG9jYWxob3N0MB4X
+DTIyMDYwOTE0Mzc0N1oXDTI1MDMwNDE0Mzc0N1owWTELMAkGA1UEBhMCQVUxEzAR
+BgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5
+IEx0ZDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
+MIIBCgKCAQEA4PbcFnMY0FC1wzsyMf04GhOx/KNcOalHu4Wy76Wys+WoJ6hO5z87
+ZIcmsg0hbys1l6DGxloTXeZwcBDoOndUg3FBZvAXRKimhXA7Qf31a9efq9GXic2W
+7Kyn1jPa724Vkr/zzlWb5I/Qkk6xcQmEFCDhilbMtpnPz/BwOwn/2vbcbiHNirUk
+Dn+s0pUcQlin1f2AR4Jq7/K1xsqjjB6cU0chuzrwzwrglQS7jpXQxCsRaAAIZQJB
+DTVQBEo/skqWwv8xABlVQgolxABIX3Wc3RUk7xRItdWCMe92/BJCGhWVXb2hUCBu
+y/yz5hX9p353JlxmXEKQlhfPzhcdDv2sdwIDAQABo1MwUTAdBgNVHQ4EFgQUQ0di
+dFnBDLhSDgHpM+/KBn+WmI4wHwYDVR0jBBgwFoAUQ0didFnBDLhSDgHpM+/KBn+W
+mI4wDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAoCQJci8G+cUF
+n030frY/OgnLJXUGC2ed9Tu+dYhEE7XQkX9WO3IK8As+jGY0kKzX7ZsvWAHHbSa3
+8qmHh1vWflU9HEc0MO0Toy6Ale2/BCjs0Oy3q2vd6t9pl3Pq2JTHyJNYu44h45we
+ufQ+ttylHGZSmAqeHz4yGp1xVvjbfriDYuc0kW9UTwMpdpzR9RmqQEVD4ySxpuYV
+FTj/ZiY89GdIJvsz1pmAhTUcUfuMgSlWS1nt0YR4yMkFS8KqQ1iKEApjrdDCU48W
+eABaPeTCUlBCFEDuKxFVPduYVVvOHtkX/8LPH3CO7EDMoSZ1iCDZ7b2+AZbwh9j+
+dXqw+WFi7w==
+-----END CERTIFICATE-----
diff --git a/test/fixtures/client-crt.pem b/test/fixtures/client-crt.pem
new file mode 100644
index 0000000..2bd94df
--- /dev/null
+++ b/test/fixtures/client-crt.pem
@@ -0,0 +1,17 @@
+-----BEGIN CERTIFICATE-----
+MIICpDCCAYwCCQCWvC2NnLEpZjANBgkqhkiG9w0BAQUFADAVMRMwEQYDVQQDDApu
+b2RlanMub3JnMCAXDTIyMDcxOTE2NDE1OFoYDzIxMjIwNzIwMTY0MTU4WjARMQ8w
+DQYDVQQLDAZVbmRpY2kwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDR
+SJvCSXTHrmnGz/CN94nxgmnUD17jYzfJH+lbcJkw4RDHpb6KZ85LEijeKoYoGw+c
+Z7a4LfmpIR4rcN3sJWGvafJyFx4DtLYPZiNrCaMsdMWiHbbMwrpvSsf5Fq3vVeUz
+Py7wxzSRiM4VOwZ7fhCJdj2YIeQJgeIZh+NN/4mpyWehS4hQSHG+cbS4c44vkET0
+Hv48G7m+4ULFCZzmG2AIW8Drh73Wymmm3kymD3kDCAY4SDSJDArxNt6lJ3sGJGO6
+jobefLFyqvLj5544Lvk4C8hD3O+e9M3OHcdyqRXf55dZ8SIWgpoGVGXb5V5g3WL/
+ncXF87jm05pMZXqOz0wdAgMBAAEwDQYJKoZIhvcNAQEFBQADggEBAK2YxxGEDgqG
+tp8uX/n0nFAj1p8sfkuD+FqYg7+PN/HYqCq6Ibrz/vVABL5Khb4qQzZN/ckJhY3k
+bfwEjRTOoXMhPv+IkShMDdbTunwSQUXqeLe+qmPbLt5ZccxcYVIzEhJMlnjeJ4nk
+NHg3BXt8y6mIIfY0Sv4znTkV995GHLK3Ax/Fd/2aio6aRCzkBCdaXY8j0SOzFHVy
++AvgRj04K2yBEEHd4bQTdLCJQR/gFQnGj37gXQp9I4qq+/1qj4sTs8BufnGKTDVT
+/jYeycIY3l4A8/72NmDSIohaJTPwFUoXNBYywOnW71+Y05PXT45lJuaOJUf2s9iH
+p/eTiEsfHsk=
+-----END CERTIFICATE-----
diff --git a/test/fixtures/client-key-2048.pem b/test/fixtures/client-key-2048.pem
new file mode 100644
index 0000000..b7dffa6
--- /dev/null
+++ b/test/fixtures/client-key-2048.pem
@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEpAIBAAKCAQEA4PbcFnMY0FC1wzsyMf04GhOx/KNcOalHu4Wy76Wys+WoJ6hO
+5z87ZIcmsg0hbys1l6DGxloTXeZwcBDoOndUg3FBZvAXRKimhXA7Qf31a9efq9GX
+ic2W7Kyn1jPa724Vkr/zzlWb5I/Qkk6xcQmEFCDhilbMtpnPz/BwOwn/2vbcbiHN
+irUkDn+s0pUcQlin1f2AR4Jq7/K1xsqjjB6cU0chuzrwzwrglQS7jpXQxCsRaAAI
+ZQJBDTVQBEo/skqWwv8xABlVQgolxABIX3Wc3RUk7xRItdWCMe92/BJCGhWVXb2h
+UCBuy/yz5hX9p353JlxmXEKQlhfPzhcdDv2sdwIDAQABAoIBAFVfeaCPZ2BO8Nu5
+UFBGP48t4EL3H93GDzHsCD8IC+xXgFwkdGUvyvNYkufJMeIFbN4xJp5JusXM2Oi+
+kdL2TD1hsqdFAB+PPTqwn9xoa0XU24SSEsc6HUeOMleI8FIi3c8GR5kLRhEUPtv3
+P0GdkeEtpUohrKizcHkCTyUoo09N35MFoH3Nb1iyMd10uq0iQlusljkTuukcHstK
+MZQAYYcslqzyz9468O/cvsk23Ynd5FfjLgYKmdJ09qaxm4ptnF9NNJ2cLqwElbUF
+xI3H5L/t1zxdwI0xZFFgDA4Ccpeq9QsRhRJGAOV94tN+4PxWXEPeQk4PM1EFDrNU
+yysi/XkCgYEA+ElKG6cWQZydsb5Tk1vdJ/k18gZa5sv+WUGXkfm9EVecftGjtKQO
+c7GwHO1IsLoZkhKfPpa/oifBR97DZRzw1ManEQPS980TZYei3Y9/8uPEpvgvRmm9
+MCHA5wp6YMlkZ5VN0SBRWnPhLtZ8L2/cqHOUCQf6YsIJU9/fewufrbUCgYEA5/QU
+/tDBDl/f4A2R1HlIkGd1jS//CJLCc3riy0SQxcWIq6/cqflyfvRWiax5DwcO7qfh
+3WbJldu9H0IWZjBCqX0v/jHvWBzaKNQCKbFFcL76Lr8bJCwlUMTH9MOhHf3uCOHD
+J7YSTVJdvgzLN8K6yFhc0gI4VYQtnQTWJENObPsCgYEAlawAq6jO5uCVw3dbhGKF
+cDpwBaVFGQpyGrZKu6nUCudIpL6VtCiNubqs0tNL1ZVqIr9tFdrkTMkwX7XvDj4j
+A/F49u3aOJ18iuD4Eh4WYIJjos/MF+NYM/K1CdIsMbpV94dusJmN0Tw3y/dqR2Jk
+n3uFCuivTOdxngk//DnmmV0CgYEA1CXNUiZSfLg5xe4DVEc9lD3cKS8d3pSEXySk
+6+8hTpHV59moRJpPG0iVIcRq0NDO2n8YOOy7MWJSPpWucPZw8h362E6Jr5hr/G20
+MLffYDh8EGdgBpyN4Kqqi/allQ3cOalrWhXP9YKBFMMU10I2nekbtESti6GiKnvy
+9CXPRCMCgYBZ2w+VVdhUUBA/elbuEdfbPwIYVDAk31PYg0c9jvQVusmfD1CuY/51
+JVsF5oJSosiN7WdDIETkklth0q3lAsQBKoYYMUw54RBf6FawoumB6MVdc3u4y9Ko
+l9JC9czdEqb/e0LBqFiWsrtPk9WQf2gyN1mIXQPbyTT1O1J+DvUIbQ==
+-----END RSA PRIVATE KEY-----
diff --git a/test/fixtures/client-key.pem b/test/fixtures/client-key.pem
new file mode 100644
index 0000000..6b47524
--- /dev/null
+++ b/test/fixtures/client-key.pem
@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEowIBAAKCAQEA0Uibwkl0x65pxs/wjfeJ8YJp1A9e42M3yR/pW3CZMOEQx6W+
+imfOSxIo3iqGKBsPnGe2uC35qSEeK3Dd7CVhr2nychceA7S2D2YjawmjLHTFoh22
+zMK6b0rH+Rat71XlMz8u8Mc0kYjOFTsGe34QiXY9mCHkCYHiGYfjTf+JqclnoUuI
+UEhxvnG0uHOOL5BE9B7+PBu5vuFCxQmc5htgCFvA64e91spppt5Mpg95AwgGOEg0
+iQwK8TbepSd7BiRjuo6G3nyxcqry4+eeOC75OAvIQ9zvnvTNzh3HcqkV3+eXWfEi
+FoKaBlRl2+VeYN1i/53FxfO45tOaTGV6js9MHQIDAQABAoIBACOp2+Ef42ajsiLP
+DI8kv70IHECm3eSh47/CUGHkrjZGJDXhaLbtOZpRXeV+GZ57/g0JH3oDW6gWnK2K
+bkbvl9XsmAQZLGQ1R1EYdrCm08efno4hwiTiiiKs+6bW1o0Sdhxlh/o/+BVU2smD
+ZXdl5CuImrZyEAoOuBjhrzp7cVodSOYYK2RIAL35oAtKLR6NE40XGcxQSCdm+1eU
+PzRo8TimQxujyIHrd1QV2FirmLfDFGg3LN8DS72n26bhvDg3PF6PVMF20BKTDqiu
+xAyKg3weBsee2QoyegDRdgTD1PvjwWqqnsntPbvY5V8PR1DDmssfotYToNPVuJd2
+6usmBAECgYEA/21NZPZJdxRKwCiWXoqBUIY0VFajxihVxZ9pIZPXOFhpGmyj/jf6
+jBiHAqtucRdABtNxqsztGbEzJsMyNv7MqEVTAWUPH804OwW/C6Z2011GZ1AUN05n
+zTxPR4eCYlxvSM+wwC8q+4mSo7hAZj5HltUI0kfEahZnGXqG4FRC1TUCgYEA0cDO
+DuTrytk6EoYYCsS7ps87MYUlU97RHFrRGwf+V1Rz2RCz+XAkYCI1/tOpb0VeF1de
+fX1mlM3edkLX2ooylYxv5HKPpICzPXeGK/u/HaJBRyZEq6Ms0HK8XyJOdG/UyuiZ
+p9nc8eaZYvco24bT4dWe5oZ43mnydAwyK2tOgEkCgYEA/blJg9zSJSNXDYJDvC3B
+PofRO2XE0XYHnYM4H06IH0RTQxhf3oskqj1C/3fjARujUiR/aLafX0ISGZMUMmTw
+TsZuKZiFaYWlMZwHpj75EgQ5hy6YpkeP/OLHrboB3ksLkDweywkPnUWPEGpaLjX3
+TvDXDmqTxP3z8+8uQ2/v43ECgYB5/3BaTV+vviT+vSuip8aVQRcmuFB7ta9elJvm
+4wFV/fLbn9FuFYGywHMzYhy8cVZGsTRuPM+7YPoxQrOVkqfVP7ec4d0WSxz1dV1+
+m5APRl49ac6rHd9k5jcWBjgnlRvpYNxuOlM+B2fTnfoPpR37zmn7nt8STgEM6kML
+6f/gsQKBgFJH95hEgqhfEHmP23+ZWH0Dl7zD5sJJe4CYTgYriNeKKzpz2G6OVv+U
+xNc8eGbnr4raPTxCCLKz6XJhuQuPQDpkoHvkhjOqZ5Tbb4fCaLdcVE0vwqBE1gGk
+ryKSvahgHIykq3+RYpL4u2xypx81IBOMk7EM++Z6gdYMq0ZTN/fL
+-----END RSA PRIVATE KEY-----
diff --git a/test/fixtures/key.pem b/test/fixtures/key.pem
new file mode 100644
index 0000000..fe750de
--- /dev/null
+++ b/test/fixtures/key.pem
@@ -0,0 +1,15 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIICXQIBAAKBgQDvVEBwFjfiirsDjlZB+CjYNMNCqdJe27hqK/b72AnLjgN6mLcX
+COABJC5N61TGFkiF9Zndh6IyFXRZVb4gQX4zxNDRuAydo95BmiYHGV0vt1ZXsLv7
+XrfQu6USLRtpZMe1cNULjsAB7raN+1hEN1CPMSmSjWc7MKPgv09QYJ5jcQIDAQAB
+AoGAbqk3TlyHpKFfDarf6Yr0X9wtuQJK+n+ACt+fSR3AkbVtmF9KsUTyRrTTEEZT
+IXCmQgKpDYysi5nt/WyvB70gu6xGYbT6PzZaf1RmcpWd1pLcdyBOppY6y7nTMZA3
+BVFfmIPSmAvtCuzZwQFFnNoKH3d6cqna+ZQJ0zvCLCSLcw0CQQD6tswNlhCIfguh
+tvhw7hJB5vZPWWEzyTQl8nVdY6SbxAT8FTx0UjxsKgOiJFzAGAVoCi40oRKIHhrw
+pKwHsEqTAkEA9GABbi2xqAmhPn66e0AiU8t2uv69PISBSt2tXbUAburJFj+4rYZW
+71QIbSKEYceveb7wm0NP+adgZqJlxn7oawJBAOjfK4+fCIJPWWx+8Cqs5yZxae1w
+HrokNBzfJSZ2bCoGm36uFvYQgHETYUaUsdX3OeZWNm7KAdWO6QUGX4fQtqMCQGXv
+OgmEY+utAKZ55D2PFgKQB1me8r6wouHgr/U7kA+0Peba86TmOZMhIVaspD3JNqf4
+/pI1NMH1kF+fdAalXzsCQQCelwr9I3FWhx336CWrfAY20xbiMOWMyAhrjVrexgUD
+53Y6AhSaRC725pZTgO2PQ4AjkGLIP61sZKgTrXS85KmJ
+-----END RSA PRIVATE KEY-----
diff --git a/test/fuzzing/client/client-fuzz-body.js b/test/fuzzing/client/client-fuzz-body.js
new file mode 100644
index 0000000..6643dda
--- /dev/null
+++ b/test/fuzzing/client/client-fuzz-body.js
@@ -0,0 +1,28 @@
+'use strict'
+
+const { request, errors } = require('../../..')
+
+const acceptableCodes = [
+ 'ERR_INVALID_ARG_TYPE'
+]
+
+// TODO: could make this a class with some inbuilt functionality that we can inherit
+async function fuzz (netServer, results, buf) {
+ const body = buf
+ results.body = body
+ try {
+ const data = await request(`http://localhost:${netServer.address().port}`, { body })
+ data.body.destroy().on('error', () => {})
+ } catch (err) {
+ results.err = err
+ // Handle any undici errors
+ if (Object.values(errors).some(undiciError => err instanceof undiciError)) {
+ // Okay error
+ } else if (!acceptableCodes.includes(err.code)) {
+ console.log(`=== Headers: ${JSON.stringify(body)} ===`)
+ throw err
+ }
+ }
+}
+
+module.exports = fuzz
diff --git a/test/fuzzing/client/client-fuzz-headers.js b/test/fuzzing/client/client-fuzz-headers.js
new file mode 100644
index 0000000..84f3390
--- /dev/null
+++ b/test/fuzzing/client/client-fuzz-headers.js
@@ -0,0 +1,27 @@
+'use strict'
+
+const { request, errors } = require('../../..')
+
+const acceptableCodes = [
+ 'ERR_INVALID_ARG_TYPE'
+]
+
+async function fuzz (netServer, results, buf) {
+ const headers = { buf: buf.toString() }
+ results.body = headers
+ try {
+ const data = await request(`http://localhost:${netServer.address().port}`, { headers })
+ data.body.destroy().on('error', () => {})
+ } catch (err) {
+ results.err = err
+ // Handle any undici errors
+ if (Object.values(errors).some(undiciError => err instanceof undiciError)) {
+ // Okay error
+ } else if (!acceptableCodes.includes(err.code)) {
+ console.log(`=== Headers: ${JSON.stringify(headers)} ===`)
+ throw err
+ }
+ }
+}
+
+module.exports = fuzz
diff --git a/test/fuzzing/client/client-fuzz-options.js b/test/fuzzing/client/client-fuzz-options.js
new file mode 100644
index 0000000..5be81b6
--- /dev/null
+++ b/test/fuzzing/client/client-fuzz-options.js
@@ -0,0 +1,38 @@
+'use strict'
+
+const { request, errors } = require('../../..')
+
+const acceptableCodes = [
+ 'ERR_INVALID_URL',
+ // These are included because '\\ABC' is interpreted as a Windows UNC path and can cause these errors.
+ 'ENOTFOUND',
+ 'EAI_AGAIN',
+ 'ECONNREFUSED'
+ // ----
+]
+
+async function fuzz (netServer, results, buf) {
+ const optionKeys = ['body', 'path', 'method', 'opaque', 'upgrade', buf]
+ const options = {}
+ for (const optionKey of optionKeys) {
+ if (Math.random() < 0.5) {
+ options[optionKey] = buf.toString()
+ }
+ }
+ results.options = options
+ try {
+ const data = await request(`http://localhost:${netServer.address().port}`, options)
+ data.body.destroy().on('error', () => {})
+ } catch (err) {
+ results.err = err
+ // Handle any undici errors
+ if (Object.values(errors).some(undiciError => err instanceof undiciError)) {
+ // Okay error
+ } else if (!acceptableCodes.includes(err.code)) {
+ console.log(`=== Options: ${JSON.stringify(options)} ===`)
+ throw err
+ }
+ }
+}
+
+module.exports = fuzz
diff --git a/test/fuzzing/client/index.js b/test/fuzzing/client/index.js
new file mode 100644
index 0000000..dac3d98
--- /dev/null
+++ b/test/fuzzing/client/index.js
@@ -0,0 +1,7 @@
+'use strict'
+
+module.exports = {
+ clientFuzzBody: require('./client-fuzz-body'),
+ clientFuzzHeaders: require('./client-fuzz-headers'),
+ clientFuzzOptions: require('./client-fuzz-options')
+}
diff --git a/test/fuzzing/fuzz.js b/test/fuzzing/fuzz.js
new file mode 100644
index 0000000..c268178
--- /dev/null
+++ b/test/fuzzing/fuzz.js
@@ -0,0 +1,66 @@
+'use strict'
+
+const net = require('net')
+const fs = require('fs/promises')
+const path = require('path')
+const serverFuzzFnMap = require('./server')
+const clientFuzzFnMap = require('./client')
+
+const port = process.env.PORT || 0
+const timeout = parseInt(process.env.TIMEOUT, 10) || 300_000 // 5 minutes by default
+
+const netServer = net.createServer((socket) => {
+ socket.on('data', (data) => {
+ // Select server fuzz fn
+ const serverFuzzFns = Object.values(serverFuzzFnMap)
+ const serverFuzzFn = serverFuzzFns[Math.floor(Math.random() * serverFuzzFns.length)]
+
+ serverFuzzFn(socket, data)
+ })
+})
+const waitForNetServer = netServer.listen(port)
+
+// Set script to exit gracefully after a set period of time.
+const timer = setTimeout(() => {
+ process.kill(process.pid, 'SIGINT')
+}, timeout)
+
+async function writeResults (resultsPath, data) {
+ try {
+ await fs.writeFile(resultsPath, JSON.stringify(data, null, 2))
+ console.log(`=== Written results to ${resultsPath} ===`)
+ } catch (err) {
+ console.log(`=== Unable to write results to ${resultsPath}`, err, '===')
+ }
+}
+
+async function fuzz (buf) {
+ // Wait for net server to be ready
+ await waitForNetServer
+
+ // Select client fuzz fn based on the buf input
+ await Promise.all(
+ Object.entries(clientFuzzFnMap).map(async ([clientFuzzFnName, clientFuzzFn]) => {
+ const results = {}
+ try {
+ await clientFuzzFn(netServer, results, buf)
+ } catch (err) {
+ clearTimeout(timer)
+ const output = { clientFuzzFnName, buf: { raw: buf, string: buf.toString() }, raw: JSON.stringify({ clientFuzzFnName, buf: { raw: buf, string: buf.toString() }, err, ...results }), err, ...results }
+
+ console.log(`=== Failed fuzz ${clientFuzzFnName} with input '${buf}' ===`)
+ console.log('=== Fuzz results start ===')
+ console.log(output)
+ console.log('=== Fuzz results end ===')
+
+ await writeResults(path.resolve(`fuzz-results-${Date.now()}.json`), output)
+
+ throw err
+ }
+ })
+ )
+}
+
+module.exports = {
+ fuzz
+}
diff --git a/test/fuzzing/server/index.js b/test/fuzzing/server/index.js
new file mode 100644
index 0000000..4bef554
--- /dev/null
+++ b/test/fuzzing/server/index.js
@@ -0,0 +1,6 @@
+'use strict'
+
+module.exports = {
+ splitData: require('./server-fuzz-split-data'),
+ appendData: require('./server-fuzz-append-data')
+}
diff --git a/test/fuzzing/server/server-fuzz-append-data.js b/test/fuzzing/server/server-fuzz-append-data.js
new file mode 100644
index 0000000..8ef6c45
--- /dev/null
+++ b/test/fuzzing/server/server-fuzz-append-data.js
@@ -0,0 +1,7 @@
+'use strict'
+
+function appendData (socket, data) {
+ socket.end('HTTP/1.1 200 OK' + data)
+}
+
+module.exports = appendData
diff --git a/test/fuzzing/server/server-fuzz-split-data.js b/test/fuzzing/server/server-fuzz-split-data.js
new file mode 100644
index 0000000..5e057dc
--- /dev/null
+++ b/test/fuzzing/server/server-fuzz-split-data.js
@@ -0,0 +1,17 @@
+'use strict'
+
+function splitData (socket, data) {
+ const lines = [
+ 'HTTP/1.1 200 OK',
+ 'Date: Sat, 09 Oct 2010 14:28:02 GMT',
+ 'Connection: close',
+ '',
+ data
+ ]
+ for (const line of lines.join('\r\n').split(data)) {
+ socket.write(line)
+ }
+ socket.end()
+}
+
+module.exports = splitData
diff --git a/test/gc.js b/test/gc.js
new file mode 100644
index 0000000..c1ceecf
--- /dev/null
+++ b/test/gc.js
@@ -0,0 +1,98 @@
+'use strict'
+/* global WeakRef, FinalizationRegistry */
+
+const { test } = require('tap')
+const { createServer } = require('net')
+const { Client, Pool } = require('..')
+
+const SKIP = typeof WeakRef === 'undefined' || typeof FinalizationRegistry === 'undefined'
+
+setInterval(() => {
+ global.gc()
+}, 100).unref()
+
+test('gc should collect the client if, and only if, there are no active sockets', { skip: SKIP }, t => {
+ t.plan(4)
+
+ const server = createServer((socket) => {
+ socket.write('HTTP/1.1 200 OK\r\n')
+ socket.write('Content-Length: 0\r\n')
+ socket.write('Keep-Alive: timeout=1s\r\n')
+ socket.write('Connection: keep-alive\r\n')
+ socket.write('\r\n\r\n')
+ })
+ t.teardown(server.close.bind(server))
+
+ let weakRef
+ let disconnected = false
+
+ const registry = new FinalizationRegistry((data) => {
+ t.equal(data, 'test')
+ t.equal(disconnected, true)
+ t.equal(weakRef.deref(), undefined)
+ })
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, {
+ keepAliveTimeoutThreshold: 100
+ })
+ client.once('disconnect', () => {
+ disconnected = true
+ })
+
+ weakRef = new WeakRef(client)
+ registry.register(client, 'test')
+
+ client.request({
+ path: '/',
+ method: 'GET'
+ }, (err, { body }) => {
+ t.error(err)
+ body.resume()
+ })
+ })
+})
+
+test('gc should collect the pool if, and only if, there are no active sockets', { skip: SKIP }, t => {
+ t.plan(4)
+
+ const server = createServer((socket) => {
+ socket.write('HTTP/1.1 200 OK\r\n')
+ socket.write('Content-Length: 0\r\n')
+ socket.write('Keep-Alive: timeout=1s\r\n')
+ socket.write('Connection: keep-alive\r\n')
+ socket.write('\r\n\r\n')
+ })
+ t.teardown(server.close.bind(server))
+
+ let weakRef
+ let disconnected = false
+
+ const registry = new FinalizationRegistry((data) => {
+ t.equal(data, 'test')
+ t.equal(disconnected, true)
+ t.equal(weakRef.deref(), undefined)
+ })
+
+ server.listen(0, () => {
+ const pool = new Pool(`http://localhost:${server.address().port}`, {
+ connections: 1,
+ keepAliveTimeoutThreshold: 500
+ })
+
+ pool.once('disconnect', () => {
+ disconnected = true
+ })
+
+ weakRef = new WeakRef(pool)
+ registry.register(pool, 'test')
+
+ pool.request({
+ path: '/',
+ method: 'GET'
+ }, (err, { body }) => {
+ t.error(err)
+ body.resume()
+ })
+ })
+})
diff --git a/test/get-head-body.js b/test/get-head-body.js
new file mode 100644
index 0000000..3e86b13
--- /dev/null
+++ b/test/get-head-body.js
@@ -0,0 +1,184 @@
+'use strict'
+
+const { test } = require('tap')
+const { Client } = require('..')
+const { createServer } = require('http')
+const { Readable } = require('stream')
+const { kConnect } = require('../lib/core/symbols')
+const { kBusy } = require('../lib/core/symbols')
+const { wrapWithAsyncIterable } = require('./utils/async-iterators')
+
+test('GET and HEAD with body should reset connection', (t) => {
+ t.plan(8 + 2)
+
+ const server = createServer((req, res) => {
+ res.end('asd')
+ })
+
+ t.teardown(server.close.bind(server))
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ client.on('disconnect', () => {
+ t.pass()
+ })
+
+ client.request({
+ path: '/',
+ body: 'asd',
+ method: 'GET'
+ }, (err, data) => {
+ t.error(err)
+ data.body.resume()
+ })
+
+ const emptyBody = new Readable({
+ read () {}
+ })
+ emptyBody.push(null)
+ client.request({
+ path: '/',
+ body: emptyBody,
+ method: 'GET'
+ }, (err, data) => {
+ t.error(err)
+ data.body.resume()
+ })
+
+ client.request({
+ path: '/',
+ body: new Readable({
+ read () {
+ this.push(null)
+ }
+ }),
+ method: 'GET'
+ }, (err, data) => {
+ t.error(err)
+ data.body.resume()
+ })
+
+ client.request({
+ path: '/',
+ body: new Readable({
+ read () {
+ this.push('asd')
+ this.push(null)
+ }
+ }),
+ method: 'GET'
+ }, (err, data) => {
+ t.error(err)
+ data.body.resume()
+ })
+
+ client.request({
+ path: '/',
+ body: [],
+ method: 'GET'
+ }, (err, data) => {
+ t.error(err)
+ data.body.resume()
+ })
+
+ client.request({
+ path: '/',
+ body: wrapWithAsyncIterable(new Readable({
+ read () {
+ this.push(null)
+ }
+ })),
+ method: 'GET'
+ }, (err, data) => {
+ t.error(err)
+ data.body.resume()
+ })
+
+ client.request({
+ path: '/',
+ body: wrapWithAsyncIterable(new Readable({
+ read () {
+ this.push('asd')
+ this.push(null)
+ }
+ })),
+ method: 'GET'
+ }, (err, data) => {
+ t.error(err)
+ data.body.resume()
+ })
+ })
+})
+
+// TODO: Avoid external dependency.
+// test('GET with body should work when target parses body as request', (t) => {
+// t.plan(4)
+
+// // This URL will send double responses when receiving a
+// // GET request with body.
+// const client = new Client('http://feeds.bbci.co.uk')
+// t.teardown(client.close.bind(client))
+
+// client.request({ method: 'GET', path: '/news/rss.xml', body: 'asd' }, (err, data) => {
+// t.error(err)
+// t.equal(data.statusCode, 200)
+// data.body.resume()
+// })
+// client.request({ method: 'GET', path: '/news/rss.xml', body: 'asd' }, (err, data) => {
+// t.error(err)
+// t.equal(data.statusCode, 200)
+// data.body.resume()
+// })
+// })
+
+test('HEAD should reset connection', (t) => {
+ t.plan(8)
+
+ const server = createServer((req, res) => {
+ res.end('asd')
+ })
+
+ t.teardown(server.close.bind(server))
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ client.once('disconnect', () => {
+ t.pass()
+ })
+
+ client.request({
+ path: '/',
+ method: 'HEAD'
+ }, (err, data) => {
+ t.error(err)
+ data.body.resume()
+ })
+ t.equal(client[kBusy], true)
+
+ client.request({
+ path: '/',
+ method: 'HEAD'
+ }, (err, data) => {
+ t.error(err)
+ data.body.resume()
+ client.once('disconnect', () => {
+ client[kConnect](() => {
+ client.request({
+ path: '/',
+ method: 'HEAD'
+ }, (err, data) => {
+ t.error(err)
+ data.body.resume()
+ data.body.on('end', () => {
+ t.pass()
+ })
+ })
+ t.equal(client[kBusy], true)
+ })
+ })
+ })
+ t.equal(client[kBusy], true)
+ })
+})
diff --git a/test/headers-as-array.js b/test/headers-as-array.js
new file mode 100644
index 0000000..fd8bb5d
--- /dev/null
+++ b/test/headers-as-array.js
@@ -0,0 +1,131 @@
+'use strict'
+
+const { test } = require('tap')
+const { Client, errors } = require('..')
+const { createServer } = require('http')
+
+test('handle headers as array', (t) => {
+ t.plan(1)
+ const headers = ['a', '1', 'b', '2', 'c', '3']
+
+ const server = createServer((req, res) => {
+ t.match(req.headers, { a: '1', b: '2', c: '3' })
+ res.end()
+ })
+ t.teardown(server.close.bind(server))
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ client.request({
+ path: '/',
+ method: 'GET',
+ headers
+ }, () => {})
+ })
+})
+
+test('handle multi-valued headers as array', (t) => {
+ t.plan(1)
+ const headers = ['a', '1', 'b', '2', 'c', '3', 'd', '4', 'd', '5']
+
+ const server = createServer((req, res) => {
+ t.match(req.headers, { a: '1', b: '2', c: '3', d: '4, 5' })
+ res.end()
+ })
+ t.teardown(server.close.bind(server))
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ client.request({
+ path: '/',
+ method: 'GET',
+ headers
+ }, () => {})
+ })
+})
+
+test('handle headers with array', (t) => {
+ t.plan(1)
+ const headers = { a: '1', b: '2', c: '3', d: ['4'] }
+
+ const server = createServer((req, res) => {
+ t.match(req.headers, { a: '1', b: '2', c: '3', d: '4' })
+ res.end()
+ })
+ t.teardown(server.close.bind(server))
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ client.request({
+ path: '/',
+ method: 'GET',
+ headers
+ }, () => {})
+ })
+})
+
+test('handle multi-valued headers', (t) => {
+ t.plan(1)
+ const headers = { a: '1', b: '2', c: '3', d: ['4', '5'] }
+
+ const server = createServer((req, res) => {
+ t.match(req.headers, { a: '1', b: '2', c: '3', d: '4, 5' })
+ res.end()
+ })
+ t.teardown(server.close.bind(server))
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ client.request({
+ path: '/',
+ method: 'GET',
+ headers
+ }, () => {})
+ })
+})
+
+test('fail if headers array is odd', (t) => {
+ t.plan(2)
+ const headers = ['a', '1', 'b', '2', 'c', '3', 'd']
+
+ const server = createServer((req, res) => { res.end() })
+ t.teardown(server.close.bind(server))
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ client.request({
+ path: '/',
+ method: 'GET',
+ headers
+ }, (err) => {
+ t.type(err, errors.InvalidArgumentError)
+ t.equal(err.message, 'headers array must be even')
+ })
+ })
+})
+
+test('fail if headers is not an object or an array', (t) => {
+ t.plan(2)
+ const headers = 'not an object or an array'
+
+ const server = createServer((req, res) => { res.end() })
+ t.teardown(server.close.bind(server))
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ client.request({
+ path: '/',
+ method: 'GET',
+ headers
+ }, (err) => {
+ t.ok(err instanceof errors.InvalidArgumentError)
+ t.equal(err.message, 'headers must be an object or an array')
+ })
+ })
+})
diff --git a/test/headers-crlf.js b/test/headers-crlf.js
new file mode 100644
index 0000000..b24fd39
--- /dev/null
+++ b/test/headers-crlf.js
@@ -0,0 +1,36 @@
+'use strict'
+
+const { test } = require('tap')
+const { Client } = require('..')
+const { createServer } = require('http')
+
+test('CRLF Injection in Nodejs ‘undici’ via host', (t) => {
+ t.plan(1)
+
+ const server = createServer(async (req, res) => {
+ res.end()
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, async () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.close.bind(client))
+
+ const unsanitizedContentTypeInput = '12 \r\n\r\naaa:aaa'
+
+ try {
+ const { body } = await client.request({
+ path: '/',
+ method: 'POST',
+ headers: {
+ 'content-type': 'application/json',
+ host: unsanitizedContentTypeInput
+ },
+ body: 'asd'
+ })
+ await body.dump()
+ } catch (err) {
+ t.same(err.code, 'UND_ERR_INVALID_ARG')
+ }
+ })
+})
diff --git a/test/http-100.js b/test/http-100.js
new file mode 100644
index 0000000..1662a8d
--- /dev/null
+++ b/test/http-100.js
@@ -0,0 +1,141 @@
+'use strict'
+
+const { test } = require('tap')
+const { Client } = require('..')
+const { createServer } = require('http')
+const net = require('net')
+
+test('ignore informational response', (t) => {
+ t.plan(2)
+
+ const server = createServer((req, res) => {
+ res.writeProcessing()
+ req.pipe(res)
+ })
+ t.teardown(server.close.bind(server))
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ client.request({
+ path: '/',
+ method: 'POST',
+ body: 'hello'
+ }, (err, response) => {
+ t.error(err)
+ const bufs = []
+ response.body.on('data', (buf) => {
+ bufs.push(buf)
+ })
+ response.body.on('end', () => {
+ t.equal('hello', Buffer.concat(bufs).toString('utf8'))
+ })
+ })
+ })
+})
+
+test('error 103 body', (t) => {
+ t.plan(2)
+
+ const server = net.createServer((socket) => {
+ socket.write('HTTP/1.1 103 Early Hints\r\n')
+ socket.write('Content-Length: 1\r\n')
+ socket.write('\r\n')
+ socket.write('a\r\n')
+ })
+ t.teardown(server.close.bind(server))
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ client.request({
+ path: '/',
+ method: 'GET'
+ }, (err) => {
+ t.equal(err.code, 'HPE_INVALID_CONSTANT')
+ })
+ client.on('disconnect', () => {
+ t.pass()
+ })
+ })
+})
+
+test('error 100 body', (t) => {
+ t.plan(2)
+
+ const server = net.createServer((socket) => {
+ socket.write('HTTP/1.1 100 Early Hints\r\n')
+ socket.write('\r\n')
+ })
+ t.teardown(server.close.bind(server))
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ client.request({
+ path: '/',
+ method: 'GET'
+ }, (err) => {
+ t.equal(err.message, 'bad response')
+ })
+ client.on('disconnect', () => {
+ t.pass()
+ })
+ })
+})
+
+test('error 101 upgrade', (t) => {
+ t.plan(2)
+
+ const server = net.createServer((socket) => {
+ socket.write('HTTP/1.1 101 Switching Protocols\r\nUpgrade: example/1\r\nConnection: Upgrade\r\n')
+ socket.write('\r\n')
+ })
+ t.teardown(server.close.bind(server))
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ client.request({
+ path: '/',
+ method: 'GET'
+ }, (err) => {
+ t.equal(err.message, 'bad upgrade')
+ })
+ client.on('disconnect', () => {
+ t.pass()
+ })
+ })
+})
+
+test('1xx response without timeouts', t => {
+ t.plan(2)
+
+ const server = createServer((req, res) => {
+ res.writeProcessing()
+ setTimeout(() => req.pipe(res), 2000)
+ })
+ t.teardown(server.close.bind(server))
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, {
+ bodyTimeout: 0,
+ headersTimeout: 0
+ })
+ t.teardown(client.destroy.bind(client))
+
+ client.request({
+ path: '/',
+ method: 'POST',
+ body: 'hello'
+ }, (err, response) => {
+ t.error(err)
+ const bufs = []
+ response.body.on('data', (buf) => {
+ bufs.push(buf)
+ })
+ response.body.on('end', () => {
+ t.equal('hello', Buffer.concat(bufs).toString('utf8'))
+ })
+ })
+ })
+})
diff --git a/test/http-req-destroy.js b/test/http-req-destroy.js
new file mode 100644
index 0000000..29ec98e
--- /dev/null
+++ b/test/http-req-destroy.js
@@ -0,0 +1,69 @@
+'use strict'
+
+const { test } = require('tap')
+const undici = require('..')
+const { createServer } = require('http')
+const { Readable } = require('stream')
+const { maybeWrapStream, consts } = require('./utils/async-iterators')
+
+function doNotKillReqSocket (bodyType) {
+ test(`do not kill req socket ${bodyType}`, (t) => {
+ t.plan(3)
+
+ const server1 = createServer((req, res) => {
+ const client = new undici.Client(`http://localhost:${server2.address().port}`)
+ t.teardown(client.close.bind(client))
+ client.request({
+ path: '/',
+ method: 'POST',
+ body: req
+ }, (err, response) => {
+ t.error(err)
+ setTimeout(() => {
+ response.body.on('data', buf => {
+ res.write(buf)
+ setTimeout(() => {
+ res.end()
+ }, 100)
+ })
+ }, 100)
+ })
+ })
+ t.teardown(server1.close.bind(server1))
+
+ const server2 = createServer((req, res) => {
+ setTimeout(() => {
+ req.pipe(res)
+ }, 100)
+ })
+ t.teardown(server2.close.bind(server2))
+
+ server1.listen(0, () => {
+ const client = new undici.Client(`http://localhost:${server1.address().port}`)
+ t.teardown(client.close.bind(client))
+
+ const r = new Readable({ read () {} })
+ r.push('hello')
+ client.request({
+ path: '/',
+ method: 'POST',
+ body: maybeWrapStream(r, bodyType)
+ }, (err, response) => {
+ t.error(err)
+ const bufs = []
+ response.body.on('data', (buf) => {
+ bufs.push(buf)
+ r.push(null)
+ })
+ response.body.on('end', () => {
+ t.equal('hello', Buffer.concat(bufs).toString('utf8'))
+ })
+ })
+ })
+
+ server2.listen(0)
+ })
+}
+
+doNotKillReqSocket(consts.STREAM)
+doNotKillReqSocket(consts.ASYNC_ITERATOR)
diff --git a/test/http2-alpn.js b/test/http2-alpn.js
new file mode 100644
index 0000000..04b8cb6
--- /dev/null
+++ b/test/http2-alpn.js
@@ -0,0 +1,277 @@
+'use strict'
+
+const https = require('node:https')
+const { once } = require('node:events')
+const { createSecureServer } = require('node:http2')
+const { readFileSync } = require('node:fs')
+const { join } = require('node:path')
+const { test } = require('tap')
+
+const { Client } = require('..')
+
+// get the crypto fixtures
+const key = readFileSync(join(__dirname, 'fixtures', 'key.pem'), 'utf8')
+const cert = readFileSync(join(__dirname, 'fixtures', 'cert.pem'), 'utf8')
+const ca = readFileSync(join(__dirname, 'fixtures', 'ca.pem'), 'utf8')
+
+test('Should upgrade to HTTP/2 when HTTPS/1 is available for GET', async (t) => {
+ t.plan(10)
+
+ const body = []
+ const httpsBody = []
+
+ // create the server and server stream handler
+ const server = createSecureServer(
+ {
+ key,
+ cert,
+ allowHTTP1: true
+ },
+ (req, res) => {
+ const { socket: { alpnProtocol } } = req.httpVersion === '2.0' ? req.stream.session : req
+
+ // handle http/1 requests
+ res.writeHead(200, {
+ 'content-type': 'application/json; charset=utf-8',
+ 'x-custom-request-header': req.headers['x-custom-request-header'] || '',
+ 'x-custom-response-header': `using ${req.httpVersion}`
+ })
+ res.end(JSON.stringify({
+ alpnProtocol,
+ httpVersion: req.httpVersion
+ }))
+ }
+ )
+
+ server.listen(0)
+ await once(server, 'listening')
+
+ // close the server on teardown
+ t.teardown(server.close.bind(server))
+
+ // set the port
+ const port = server.address().port
+
+ // test undici against http/2
+ const client = new Client(`https://localhost:${port}`, {
+ connect: {
+ ca,
+ servername: 'agent1'
+ },
+ allowH2: true
+ })
+
+ // close the client on teardown
+ t.teardown(client.close.bind(client))
+
+ // make an undici request using where it wants http/2
+ const response = await client.request({
+ path: '/',
+ method: 'GET',
+ headers: {
+ 'x-custom-request-header': 'want 2.0'
+ }
+ })
+
+ response.body.on('data', chunk => {
+ body.push(chunk)
+ })
+
+ await once(response.body, 'end')
+
+ t.equal(response.statusCode, 200)
+ t.equal(response.headers['content-type'], 'application/json; charset=utf-8')
+ t.equal(response.headers['x-custom-request-header'], 'want 2.0')
+ t.equal(response.headers['x-custom-response-header'], 'using 2.0')
+ t.equal(Buffer.concat(body).toString('utf8'), JSON.stringify({
+ alpnProtocol: 'h2',
+ httpVersion: '2.0'
+ }))
+
+ // make an https request for http/1 to confirm undici is using http/2
+ const httpsOptions = {
+ ca,
+ servername: 'agent1',
+ headers: {
+ 'x-custom-request-header': 'want 1.1'
+ }
+ }
+
+ const httpsResponse = await new Promise((resolve, reject) => {
+ const httpsRequest = https.get(`https://localhost:${port}/`, httpsOptions, (res) => {
+ res.on('data', (chunk) => {
+ httpsBody.push(chunk)
+ })
+
+ res.on('end', () => {
+ resolve(res)
+ })
+ }).on('error', (err) => {
+ reject(err)
+ })
+
+ t.teardown(httpsRequest.destroy.bind(httpsRequest))
+ })
+
+ t.equal(httpsResponse.statusCode, 200)
+ t.equal(httpsResponse.headers['content-type'], 'application/json; charset=utf-8')
+ t.equal(httpsResponse.headers['x-custom-request-header'], 'want 1.1')
+ t.equal(httpsResponse.headers['x-custom-response-header'], 'using 1.1')
+ t.equal(Buffer.concat(httpsBody).toString('utf8'), JSON.stringify({
+ alpnProtocol: false,
+ httpVersion: '1.1'
+ }))
+})
+
+test('Should upgrade to HTTP/2 when HTTPS/1 is available for POST', async (t) => {
+ t.plan(15)
+
+ const requestChunks = []
+ const responseBody = []
+
+ const httpsRequestChunks = []
+ const httpsResponseBody = []
+
+ const expectedBody = 'hello'
+ const buf = Buffer.from(expectedBody)
+ const body = new ArrayBuffer(buf.byteLength)
+
+ buf.copy(new Uint8Array(body))
+
+ // create the server and server stream handler
+ const server = createSecureServer(
+ {
+ key,
+ cert,
+ allowHTTP1: true
+ },
+ (req, res) => {
+ // use the stream handler for http2
+ if (req.httpVersion === '2.0') {
+ return
+ }
+
+ const { socket: { alpnProtocol } } = req
+
+ req.on('data', (chunk) => {
+ httpsRequestChunks.push(chunk)
+ })
+
+ req.on('end', () => {
+ // handle http/1 requests
+ res.writeHead(201, {
+ 'content-type': 'text/plain; charset=utf-8',
+ 'x-custom-request-header': req.headers['x-custom-request-header'] || '',
+ 'x-custom-alpn-protocol': alpnProtocol
+ })
+ res.end('hello http/1!')
+ })
+ }
+ )
+
+ server.on('stream', (stream, headers) => {
+ t.equal(headers[':method'], 'POST')
+ t.equal(headers[':path'], '/')
+ t.equal(headers[':scheme'], 'https')
+
+ const { socket: { alpnProtocol } } = stream.session
+
+ stream.on('data', (chunk) => {
+ requestChunks.push(chunk)
+ })
+
+ stream.respond({
+ ':status': 201,
+ 'content-type': 'text/plain; charset=utf-8',
+ 'x-custom-request-header': headers['x-custom-request-header'] || '',
+ 'x-custom-alpn-protocol': alpnProtocol
+ })
+
+ stream.end('hello h2!')
+ })
+
+ server.listen(0)
+ await once(server, 'listening')
+
+ // close the server on teardown
+ t.teardown(server.close.bind(server))
+
+ // set the port
+ const port = server.address().port
+
+ // test undici against http/2
+ const client = new Client(`https://localhost:${port}`, {
+ connect: {
+ ca,
+ servername: 'agent1'
+ },
+ allowH2: true
+ })
+
+ // close the client on teardown
+ t.teardown(client.close.bind(client))
+
+ // make an undici request using where it wants http/2
+ const response = await client.request({
+ path: '/',
+ method: 'POST',
+ headers: {
+ 'x-custom-request-header': 'want 2.0'
+ },
+ body
+ })
+
+ response.body.on('data', (chunk) => {
+ responseBody.push(chunk)
+ })
+
+ await once(response.body, 'end')
+
+ t.equal(response.statusCode, 201)
+ t.equal(response.headers['content-type'], 'text/plain; charset=utf-8')
+ t.equal(response.headers['x-custom-request-header'], 'want 2.0')
+ t.equal(response.headers['x-custom-alpn-protocol'], 'h2')
+ t.equal(Buffer.concat(responseBody).toString('utf-8'), 'hello h2!')
+ t.equal(Buffer.concat(requestChunks).toString('utf-8'), expectedBody)
+
+ // make an https request for http/1 to confirm undici is using http/2
+ const httpsOptions = {
+ ca,
+ servername: 'agent1',
+ method: 'POST',
+ headers: {
+ 'content-type': 'text/plain; charset=utf-8',
+ 'content-length': Buffer.byteLength(body),
+ 'x-custom-request-header': 'want 1.1'
+ }
+ }
+
+ const httpsResponse = await new Promise((resolve, reject) => {
+ const httpsRequest = https.request(`https://localhost:${port}/`, httpsOptions, (res) => {
+ res.on('data', (chunk) => {
+ httpsResponseBody.push(chunk)
+ })
+
+ res.on('end', () => {
+ resolve(res)
+ })
+ }).on('error', (err) => {
+ reject(err)
+ })
+
+ httpsRequest.on('error', (err) => {
+ reject(err)
+ })
+
+ httpsRequest.write(Buffer.from(body))
+
+ t.teardown(httpsRequest.destroy.bind(httpsRequest))
+ })
+
+ t.equal(httpsResponse.statusCode, 201)
+ t.equal(httpsResponse.headers['content-type'], 'text/plain; charset=utf-8')
+ t.equal(httpsResponse.headers['x-custom-request-header'], 'want 1.1')
+ t.equal(httpsResponse.headers['x-custom-alpn-protocol'], 'false')
+ t.equal(Buffer.concat(httpsResponseBody).toString('utf-8'), 'hello http/1!')
+ t.equal(Buffer.concat(httpsRequestChunks).toString('utf-8'), expectedBody)
+})
diff --git a/test/http2.js b/test/http2.js
new file mode 100644
index 0000000..71b7749
--- /dev/null
+++ b/test/http2.js
@@ -0,0 +1,1191 @@
+'use strict'
+
+const { createSecureServer } = require('node:http2')
+const { createReadStream, readFileSync } = require('node:fs')
+const { once } = require('node:events')
+const { Blob } = require('node:buffer')
+const { Writable, pipeline, PassThrough, Readable } = require('node:stream')
+
+const { test, plan } = require('tap')
+const { gte } = require('semver')
+const pem = require('https-pem')
+
+const { Client, Agent } = require('..')
+
+const isGreaterThanv20 = gte(process.version.slice(1), '20.0.0')
+// NOTE: node versions <16.14.1 have a bug which changes the order of pseudo-headers
+// https://github.com/nodejs/node/pull/41735
+const hasPseudoHeadersOrderFix = gte(process.version.slice(1), '16.14.1')
+
+plan(23)
+
+test('Should support H2 connection', async t => {
+ const body = []
+ const server = createSecureServer(pem)
+
+ server.on('stream', (stream, headers, _flags, rawHeaders) => {
+ t.equal(headers['x-my-header'], 'foo')
+ t.equal(headers[':method'], 'GET')
+ stream.respond({
+ 'content-type': 'text/plain; charset=utf-8',
+ 'x-custom-h2': 'hello',
+ ':status': 200
+ })
+ stream.end('hello h2!')
+ })
+
+ server.listen(0)
+ await once(server, 'listening')
+
+ const client = new Client(`https://localhost:${server.address().port}`, {
+ connect: {
+ rejectUnauthorized: false
+ },
+ allowH2: true
+ })
+
+ t.plan(6)
+ t.teardown(server.close.bind(server))
+ t.teardown(client.close.bind(client))
+
+ const response = await client.request({
+ path: '/',
+ method: 'GET',
+ headers: {
+ 'x-my-header': 'foo'
+ }
+ })
+
+ response.body.on('data', chunk => {
+ body.push(chunk)
+ })
+
+ await once(response.body, 'end')
+ t.equal(response.statusCode, 200)
+ t.equal(response.headers['content-type'], 'text/plain; charset=utf-8')
+ t.equal(response.headers['x-custom-h2'], 'hello')
+ t.equal(Buffer.concat(body).toString('utf8'), 'hello h2!')
+})
+
+test('Should support H2 connection(multiple requests)', async t => {
+ const server = createSecureServer(pem)
+
+ server.on('stream', async (stream, headers, _flags, rawHeaders) => {
+ t.equal(headers['x-my-header'], 'foo')
+ t.equal(headers[':method'], 'POST')
+ const reqData = []
+ stream.on('data', chunk => reqData.push(chunk.toString()))
+ await once(stream, 'end')
+ const reqBody = reqData.join('')
+ t.equal(reqBody.length > 0, true)
+ stream.respond({
+ 'content-type': 'text/plain; charset=utf-8',
+ 'x-custom-h2': 'hello',
+ ':status': 200
+ })
+ stream.end(`hello h2! ${reqBody}`)
+ })
+
+ server.listen(0)
+ await once(server, 'listening')
+
+ const client = new Client(`https://localhost:${server.address().port}`, {
+ connect: {
+ rejectUnauthorized: false
+ },
+ allowH2: true
+ })
+
+ t.plan(21)
+ t.teardown(server.close.bind(server))
+ t.teardown(client.close.bind(client))
+
+ for (let i = 0; i < 3; i++) {
+ const sendBody = `seq ${i}`
+ const body = []
+ const response = await client.request({
+ path: '/',
+ method: 'POST',
+ headers: {
+ 'content-type': 'text/plain; charset=utf-8',
+ 'x-my-header': 'foo'
+ },
+ body: Readable.from(sendBody)
+ })
+
+ response.body.on('data', chunk => {
+ body.push(chunk)
+ })
+
+ await once(response.body, 'end')
+ t.equal(response.statusCode, 200)
+ t.equal(response.headers['content-type'], 'text/plain; charset=utf-8')
+ t.equal(response.headers['x-custom-h2'], 'hello')
+ t.equal(Buffer.concat(body).toString('utf8'), `hello h2! ${sendBody}`)
+ }
+})
+
+test('Should support H2 connection (headers as array)', async t => {
+ const body = []
+ const server = createSecureServer(pem)
+
+ server.on('stream', (stream, headers) => {
+ t.equal(headers['x-my-header'], 'foo')
+ t.equal(headers['x-my-drink'], 'coffee,tea')
+ t.equal(headers[':method'], 'GET')
+ stream.respond({
+ 'content-type': 'text/plain; charset=utf-8',
+ 'x-custom-h2': 'hello',
+ ':status': 200
+ })
+ stream.end('hello h2!')
+ })
+
+ server.listen(0)
+ await once(server, 'listening')
+
+ const client = new Client(`https://localhost:${server.address().port}`, {
+ connect: {
+ rejectUnauthorized: false
+ },
+ allowH2: true
+ })
+
+ t.plan(7)
+ t.teardown(server.close.bind(server))
+ t.teardown(client.close.bind(client))
+
+ const response = await client.request({
+ path: '/',
+ method: 'GET',
+ headers: ['x-my-header', 'foo', 'x-my-drink', ['coffee', 'tea']]
+ })
+
+ response.body.on('data', chunk => {
+ body.push(chunk)
+ })
+
+ await once(response.body, 'end')
+ t.equal(response.statusCode, 200)
+ t.equal(response.headers['content-type'], 'text/plain; charset=utf-8')
+ t.equal(response.headers['x-custom-h2'], 'hello')
+ t.equal(Buffer.concat(body).toString('utf8'), 'hello h2!')
+})
+
+test('Should support H2 connection(POST Buffer)', async t => {
+ const server = createSecureServer({ ...pem, allowHTTP1: false })
+
+ server.on('stream', async (stream, headers, _flags, rawHeaders) => {
+ t.equal(headers[':method'], 'POST')
+ const reqData = []
+ stream.on('data', chunk => reqData.push(chunk.toString()))
+ await once(stream, 'end')
+ t.equal(reqData.join(''), 'hello!')
+ stream.respond({
+ 'content-type': 'text/plain; charset=utf-8',
+ 'x-custom-h2': 'hello',
+ ':status': 200
+ })
+ stream.end('hello h2!')
+ })
+
+ server.listen(0)
+ await once(server, 'listening')
+
+ const client = new Client(`https://localhost:${server.address().port}`, {
+ connect: {
+ rejectUnauthorized: false
+ },
+ allowH2: true
+ })
+
+ t.plan(6)
+ t.teardown(server.close.bind(server))
+ t.teardown(client.close.bind(client))
+
+ const sendBody = 'hello!'
+ const body = []
+ const response = await client.request({
+ path: '/',
+ method: 'POST',
+ body: sendBody
+ })
+
+ response.body.on('data', chunk => {
+ body.push(chunk)
+ })
+
+ await once(response.body, 'end')
+ t.equal(response.statusCode, 200)
+ t.equal(response.headers['content-type'], 'text/plain; charset=utf-8')
+ t.equal(response.headers['x-custom-h2'], 'hello')
+ t.equal(Buffer.concat(body).toString('utf8'), 'hello h2!')
+})
+
+test('Should support H2 GOAWAY (server-side)', async t => {
+ const body = []
+ const server = createSecureServer(pem)
+
+ server.on('stream', (stream, headers) => {
+ t.equal(headers['x-my-header'], 'foo')
+ t.equal(headers[':method'], 'GET')
+ stream.respond({
+ 'content-type': 'text/plain; charset=utf-8',
+ 'x-custom-h2': 'hello',
+ ':status': 200
+ })
+ stream.end('hello h2!')
+ })
+
+ server.on('session', session => {
+ setTimeout(() => {
+ session.goaway(204)
+ }, 1000)
+ })
+
+ server.listen(0)
+ await once(server, 'listening')
+
+ const client = new Client(`https://localhost:${server.address().port}`, {
+ connect: {
+ rejectUnauthorized: false
+ },
+ allowH2: true
+ })
+
+ t.plan(9)
+ t.teardown(server.close.bind(server))
+ t.teardown(client.close.bind(client))
+
+ const response = await client.request({
+ path: '/',
+ method: 'GET',
+ headers: {
+ 'x-my-header': 'foo'
+ }
+ })
+
+ response.body.on('data', chunk => {
+ body.push(chunk)
+ })
+
+ await once(response.body, 'end')
+ t.equal(response.statusCode, 200)
+ t.equal(response.headers['content-type'], 'text/plain; charset=utf-8')
+ t.equal(response.headers['x-custom-h2'], 'hello')
+ t.equal(Buffer.concat(body).toString('utf8'), 'hello h2!')
+
+ const [url, disconnectClient, err] = await once(client, 'disconnect')
+
+ t.type(url, URL)
+ t.same(disconnectClient, [client])
+ t.equal(err.message, 'HTTP/2: "GOAWAY" frame received with code 204')
+})
+
+test('Should throw if bad allowH2 has been pased', async t => {
+ try {
+ // eslint-disable-next-line
+ new Client('https://localhost:1000', {
+ allowH2: 'true'
+ })
+ t.fail()
+ } catch (error) {
+ t.equal(error.message, 'allowH2 must be a valid boolean value')
+ }
+})
+
+test('Should throw if bad maxConcurrentStreams has been pased', async t => {
+ try {
+ // eslint-disable-next-line
+ new Client('https://localhost:1000', {
+ allowH2: true,
+ maxConcurrentStreams: {}
+ })
+ t.fail()
+ } catch (error) {
+ t.equal(
+ error.message,
+ 'maxConcurrentStreams must be a possitive integer, greater than 0'
+ )
+ }
+
+ try {
+ // eslint-disable-next-line
+ new Client('https://localhost:1000', {
+ allowH2: true,
+ maxConcurrentStreams: -1
+ })
+ t.fail()
+ } catch (error) {
+ t.equal(
+ error.message,
+ 'maxConcurrentStreams must be a possitive integer, greater than 0'
+ )
+ }
+})
+
+test(
+ 'Request should fail if allowH2 is false and server advertises h1 only',
+ { skip: isGreaterThanv20 },
+ async t => {
+ const server = createSecureServer(
+ {
+ ...pem,
+ allowHTTP1: false,
+ ALPNProtocols: ['http/1.1']
+ },
+ (req, res) => {
+ t.fail('Should not create a valid h2 stream')
+ }
+ )
+
+ server.listen(0)
+ await once(server, 'listening')
+
+ const client = new Client(`https://localhost:${server.address().port}`, {
+ allowH2: false,
+ connect: {
+ rejectUnauthorized: false
+ }
+ })
+
+ t.teardown(server.close.bind(server))
+ t.teardown(client.close.bind(client))
+
+ const response = await client.request({
+ path: '/',
+ method: 'GET',
+ headers: {
+ 'x-my-header': 'foo'
+ }
+ })
+
+ t.equal(response.statusCode, 403)
+ }
+)
+
+test(
+ '[v20] Request should fail if allowH2 is false and server advertises h1 only',
+ { skip: !isGreaterThanv20 },
+ async t => {
+ const server = createSecureServer(
+ {
+ ...pem,
+ allowHTTP1: false,
+ ALPNProtocols: ['http/1.1']
+ },
+ (req, res) => {
+ t.fail('Should not create a valid h2 stream')
+ }
+ )
+
+ server.listen(0)
+ await once(server, 'listening')
+
+ const client = new Client(`https://localhost:${server.address().port}`, {
+ allowH2: false,
+ connect: {
+ rejectUnauthorized: false
+ }
+ })
+
+ t.teardown(server.close.bind(server))
+ t.teardown(client.close.bind(client))
+ t.plan(2)
+
+ try {
+ await client.request({
+ path: '/',
+ method: 'GET',
+ headers: {
+ 'x-my-header': 'foo'
+ }
+ })
+ } catch (error) {
+ t.equal(
+ error.message,
+ 'Client network socket disconnected before secure TLS connection was established'
+ )
+ t.equal(error.code, 'ECONNRESET')
+ }
+ }
+)
+
+test('Should handle h2 continue', async t => {
+ const requestBody = []
+ const server = createSecureServer(pem, () => {})
+ const responseBody = []
+
+ server.on('checkContinue', (request, response) => {
+ t.equal(request.headers.expect, '100-continue')
+ t.equal(request.headers['x-my-header'], 'foo')
+ t.equal(request.headers[':method'], 'POST')
+ response.writeContinue()
+
+ request.on('data', chunk => requestBody.push(chunk))
+
+ response.writeHead(200, {
+ 'content-type': 'text/plain; charset=utf-8',
+ 'x-custom-h2': 'foo'
+ })
+ response.end('hello h2!')
+ })
+
+ t.plan(7)
+
+ server.listen(0)
+ await once(server, 'listening')
+
+ const client = new Client(`https://localhost:${server.address().port}`, {
+ connect: {
+ rejectUnauthorized: false
+ },
+ expectContinue: true,
+ allowH2: true
+ })
+
+ t.teardown(server.close.bind(server))
+ t.teardown(client.close.bind(client))
+
+ const response = await client.request({
+ path: '/',
+ method: 'POST',
+ headers: {
+ 'x-my-header': 'foo'
+ },
+ expectContinue: true
+ })
+
+ response.body.on('data', chunk => {
+ responseBody.push(chunk)
+ })
+
+ await once(response.body, 'end')
+
+ t.equal(response.statusCode, 200)
+ t.equal(response.headers['content-type'], 'text/plain; charset=utf-8')
+ t.equal(response.headers['x-custom-h2'], 'foo')
+ t.equal(Buffer.concat(responseBody).toString('utf-8'), 'hello h2!')
+})
+
+test('Dispatcher#Stream', t => {
+ const server = createSecureServer(pem)
+ const expectedBody = 'hello from client!'
+ const bufs = []
+ let requestBody = ''
+
+ server.on('stream', async (stream, headers) => {
+ stream.setEncoding('utf-8')
+ stream.on('data', chunk => {
+ requestBody += chunk
+ })
+
+ stream.respond({ ':status': 200, 'x-custom': 'custom-header' })
+ stream.end('hello h2!')
+ })
+
+ t.plan(4)
+
+ server.listen(0, async () => {
+ const client = new Client(`https://localhost:${server.address().port}`, {
+ connect: {
+ rejectUnauthorized: false
+ },
+ allowH2: true
+ })
+
+ t.teardown(server.close.bind(server))
+ t.teardown(client.close.bind(client))
+
+ await client.stream(
+ { path: '/', opaque: { bufs }, method: 'POST', body: expectedBody },
+ ({ statusCode, headers, opaque: { bufs } }) => {
+ t.equal(statusCode, 200)
+ t.equal(headers['x-custom'], 'custom-header')
+
+ return new Writable({
+ write (chunk, _encoding, cb) {
+ bufs.push(chunk)
+ cb()
+ }
+ })
+ }
+ )
+
+ t.equal(Buffer.concat(bufs).toString('utf-8'), 'hello h2!')
+ t.equal(requestBody, expectedBody)
+ })
+})
+
+test('Dispatcher#Pipeline', t => {
+ const server = createSecureServer(pem)
+ const expectedBody = 'hello from client!'
+ const bufs = []
+ let requestBody = ''
+
+ server.on('stream', async (stream, headers) => {
+ stream.setEncoding('utf-8')
+ stream.on('data', chunk => {
+ requestBody += chunk
+ })
+
+ stream.respond({ ':status': 200, 'x-custom': 'custom-header' })
+ stream.end('hello h2!')
+ })
+
+ t.plan(5)
+
+ server.listen(0, () => {
+ const client = new Client(`https://localhost:${server.address().port}`, {
+ connect: {
+ rejectUnauthorized: false
+ },
+ allowH2: true
+ })
+
+ t.teardown(server.close.bind(server))
+ t.teardown(client.close.bind(client))
+
+ pipeline(
+ new Readable({
+ read () {
+ this.push(Buffer.from(expectedBody))
+ this.push(null)
+ }
+ }),
+ client.pipeline(
+ { path: '/', method: 'POST', body: expectedBody },
+ ({ statusCode, headers, body }) => {
+ t.equal(statusCode, 200)
+ t.equal(headers['x-custom'], 'custom-header')
+
+ return pipeline(body, new PassThrough(), () => {})
+ }
+ ),
+ new Writable({
+ write (chunk, _, cb) {
+ bufs.push(chunk)
+ cb()
+ }
+ }),
+ err => {
+ t.error(err)
+ t.equal(Buffer.concat(bufs).toString('utf-8'), 'hello h2!')
+ t.equal(requestBody, expectedBody)
+ }
+ )
+ })
+})
+
+test('Dispatcher#Connect', t => {
+ const server = createSecureServer(pem)
+ const expectedBody = 'hello from client!'
+ let requestBody = ''
+
+ server.on('stream', async (stream, headers) => {
+ stream.setEncoding('utf-8')
+ stream.on('data', chunk => {
+ requestBody += chunk
+ })
+
+ stream.respond({ ':status': 200, 'x-custom': 'custom-header' })
+ stream.end('hello h2!')
+ })
+
+ t.plan(6)
+
+ server.listen(0, () => {
+ const client = new Client(`https://localhost:${server.address().port}`, {
+ connect: {
+ rejectUnauthorized: false
+ },
+ allowH2: true
+ })
+
+ t.teardown(server.close.bind(server))
+ t.teardown(client.close.bind(client))
+
+ let result = ''
+ client.connect({ path: '/' }, (err, { socket }) => {
+ t.error(err)
+ socket.on('data', chunk => {
+ result += chunk
+ })
+ socket.on('response', headers => {
+ t.equal(headers[':status'], 200)
+ t.equal(headers['x-custom'], 'custom-header')
+ t.notOk(socket.closed)
+ })
+
+ // We need to handle the error event although
+ // is not controlled by Undici, the fact that a session
+ // is destroyed and destroys subsequent streams, causes
+ // unhandled errors to surface if not handling this event.
+ socket.on('error', () => {})
+
+ socket.once('end', () => {
+ t.equal(requestBody, expectedBody)
+ t.equal(result, 'hello h2!')
+ })
+ socket.end(expectedBody)
+ })
+ })
+})
+
+test('Dispatcher#Upgrade', t => {
+ const server = createSecureServer(pem)
+
+ server.on('stream', async (stream, headers) => {
+ stream.end()
+ })
+
+ t.plan(1)
+
+ server.listen(0, async () => {
+ const client = new Client(`https://localhost:${server.address().port}`, {
+ connect: {
+ rejectUnauthorized: false
+ },
+ allowH2: true
+ })
+
+ t.teardown(server.close.bind(server))
+ t.teardown(client.close.bind(client))
+
+ try {
+ await client.upgrade({ path: '/' })
+ } catch (error) {
+ t.equal(error.message, 'Upgrade not supported for H2')
+ }
+ })
+})
+
+test('Dispatcher#destroy', async t => {
+ const promises = []
+ const server = createSecureServer(pem)
+
+ server.on('stream', (stream, headers) => {
+ setTimeout(stream.end.bind(stream), 1500)
+ })
+
+ server.listen(0)
+ await once(server, 'listening')
+
+ const client = new Client(`https://localhost:${server.address().port}`, {
+ connect: {
+ rejectUnauthorized: false
+ },
+ allowH2: true
+ })
+
+ t.plan(4)
+ t.teardown(server.close.bind(server))
+
+ promises.push(
+ client.request({
+ path: '/',
+ method: 'GET',
+ headers: {
+ 'x-my-header': 'foo'
+ }
+ })
+ )
+
+ promises.push(
+ client.request({
+ path: '/',
+ method: 'GET',
+ headers: {
+ 'x-my-header': 'foo'
+ }
+ })
+ )
+
+ promises.push(
+ client.request({
+ path: '/',
+ method: 'GET',
+ headers: {
+ 'x-my-header': 'foo'
+ }
+ })
+ )
+
+ promises.push(
+ client.request({
+ path: '/',
+ method: 'GET',
+ headers: {
+ 'x-my-header': 'foo'
+ }
+ })
+ )
+
+ await client.destroy()
+
+ const results = await Promise.allSettled(promises)
+
+ t.equal(results[0].status, 'rejected')
+ t.equal(results[1].status, 'rejected')
+ t.equal(results[2].status, 'rejected')
+ t.equal(results[3].status, 'rejected')
+})
+
+test('Should handle h2 request with body (string or buffer) - dispatch', t => {
+ const server = createSecureServer(pem)
+ const expectedBody = 'hello from client!'
+ const response = []
+ const requestBody = []
+
+ server.on('stream', async (stream, headers) => {
+ stream.on('data', chunk => requestBody.push(chunk))
+
+ stream.respond({
+ 'content-type': 'text/plain; charset=utf-8',
+ 'x-custom-h2': headers['x-my-header'],
+ ':status': 200
+ })
+
+ stream.end('hello h2!')
+ })
+
+ t.plan(7)
+
+ server.listen(0, () => {
+ const client = new Client(`https://localhost:${server.address().port}`, {
+ connect: {
+ rejectUnauthorized: false
+ },
+ allowH2: true
+ })
+
+ t.teardown(server.close.bind(server))
+ t.teardown(client.close.bind(client))
+
+ client.dispatch(
+ {
+ path: '/',
+ method: 'POST',
+ headers: {
+ 'x-my-header': 'foo',
+ 'content-type': 'text/plain'
+ },
+ body: expectedBody
+ },
+ {
+ onConnect () {
+ t.ok(true)
+ },
+ onError (err) {
+ t.error(err)
+ },
+ onHeaders (statusCode, headers) {
+ t.equal(statusCode, 200)
+ t.equal(headers['content-type'], 'text/plain; charset=utf-8')
+ t.equal(headers['x-custom-h2'], 'foo')
+ },
+ onData (chunk) {
+ response.push(chunk)
+ },
+ onBodySent (body) {
+ t.equal(body.toString('utf-8'), expectedBody)
+ },
+ onComplete () {
+ t.equal(Buffer.concat(response).toString('utf-8'), 'hello h2!')
+ t.equal(
+ Buffer.concat(requestBody).toString('utf-8'),
+ 'hello from client!'
+ )
+ }
+ }
+ )
+ })
+})
+
+test('Should handle h2 request with body (stream)', async t => {
+ const server = createSecureServer(pem)
+ const expectedBody = readFileSync(__filename, 'utf-8')
+ const stream = createReadStream(__filename)
+ const requestChunks = []
+ const responseBody = []
+
+ server.on('stream', async (stream, headers) => {
+ t.equal(headers[':method'], 'PUT')
+ t.equal(headers[':path'], '/')
+ t.equal(headers[':scheme'], 'https')
+
+ stream.respond({
+ 'content-type': 'text/plain; charset=utf-8',
+ 'x-custom-h2': headers['x-my-header'],
+ ':status': 200
+ })
+
+ for await (const chunk of stream) {
+ requestChunks.push(chunk)
+ }
+
+ stream.end('hello h2!')
+ })
+
+ t.plan(8)
+
+ server.listen(0)
+ await once(server, 'listening')
+
+ const client = new Client(`https://localhost:${server.address().port}`, {
+ connect: {
+ rejectUnauthorized: false
+ },
+ allowH2: true
+ })
+
+ t.teardown(server.close.bind(server))
+ t.teardown(client.close.bind(client))
+
+ const response = await client.request({
+ path: '/',
+ method: 'PUT',
+ headers: {
+ 'x-my-header': 'foo'
+ },
+ body: stream
+ })
+
+ for await (const chunk of response.body) {
+ responseBody.push(chunk)
+ }
+
+ t.equal(response.statusCode, 200)
+ t.equal(response.headers['content-type'], 'text/plain; charset=utf-8')
+ t.equal(response.headers['x-custom-h2'], 'foo')
+ t.equal(Buffer.concat(responseBody).toString('utf-8'), 'hello h2!')
+ t.equal(Buffer.concat(requestChunks).toString('utf-8'), expectedBody)
+})
+
+test('Should handle h2 request with body (iterable)', async t => {
+ const server = createSecureServer(pem)
+ const expectedBody = 'hello'
+ const requestChunks = []
+ const responseBody = []
+ const iterableBody = {
+ [Symbol.iterator]: function * () {
+ const end = expectedBody.length - 1
+ for (let i = 0; i < end + 1; i++) {
+ yield expectedBody[i]
+ }
+
+ return expectedBody[end]
+ }
+ }
+
+ server.on('stream', async (stream, headers) => {
+ t.equal(headers[':method'], 'POST')
+ t.equal(headers[':path'], '/')
+ t.equal(headers[':scheme'], 'https')
+
+ stream.on('data', chunk => requestChunks.push(chunk))
+
+ stream.respond({
+ 'content-type': 'text/plain; charset=utf-8',
+ 'x-custom-h2': headers['x-my-header'],
+ ':status': 200
+ })
+
+ stream.end('hello h2!')
+ })
+
+ t.plan(8)
+
+ server.listen(0)
+ await once(server, 'listening')
+
+ const client = new Client(`https://localhost:${server.address().port}`, {
+ connect: {
+ rejectUnauthorized: false
+ },
+ allowH2: true
+ })
+
+ t.teardown(server.close.bind(server))
+ t.teardown(client.close.bind(client))
+
+ const response = await client.request({
+ path: '/',
+ method: 'POST',
+ headers: {
+ 'x-my-header': 'foo'
+ },
+ body: iterableBody
+ })
+
+ response.body.on('data', chunk => {
+ responseBody.push(chunk)
+ })
+
+ await once(response.body, 'end')
+
+ t.equal(response.statusCode, 200)
+ t.equal(response.headers['content-type'], 'text/plain; charset=utf-8')
+ t.equal(response.headers['x-custom-h2'], 'foo')
+ t.equal(Buffer.concat(responseBody).toString('utf-8'), 'hello h2!')
+ t.equal(Buffer.concat(requestChunks).toString('utf-8'), expectedBody)
+})
+
+test('Should handle h2 request with body (Blob)', { skip: !Blob }, async t => {
+ const server = createSecureServer(pem)
+ const expectedBody = 'asd'
+ const requestChunks = []
+ const responseBody = []
+ const body = new Blob(['asd'], {
+ type: 'application/json'
+ })
+
+ server.on('stream', async (stream, headers) => {
+ t.equal(headers[':method'], 'POST')
+ t.equal(headers[':path'], '/')
+ t.equal(headers[':scheme'], 'https')
+
+ stream.on('data', chunk => requestChunks.push(chunk))
+
+ stream.respond({
+ 'content-type': 'text/plain; charset=utf-8',
+ 'x-custom-h2': headers['x-my-header'],
+ ':status': 200
+ })
+
+ stream.end('hello h2!')
+ })
+
+ t.plan(8)
+
+ server.listen(0)
+ await once(server, 'listening')
+
+ const client = new Client(`https://localhost:${server.address().port}`, {
+ connect: {
+ rejectUnauthorized: false
+ },
+ allowH2: true
+ })
+
+ t.teardown(server.close.bind(server))
+ t.teardown(client.close.bind(client))
+
+ const response = await client.request({
+ path: '/',
+ method: 'POST',
+ headers: {
+ 'x-my-header': 'foo'
+ },
+ body
+ })
+
+ response.body.on('data', chunk => {
+ responseBody.push(chunk)
+ })
+
+ await once(response.body, 'end')
+
+ t.equal(response.statusCode, 200)
+ t.equal(response.headers['content-type'], 'text/plain; charset=utf-8')
+ t.equal(response.headers['x-custom-h2'], 'foo')
+ t.equal(Buffer.concat(responseBody).toString('utf-8'), 'hello h2!')
+ t.equal(Buffer.concat(requestChunks).toString('utf-8'), expectedBody)
+})
+
+test(
+ 'Should handle h2 request with body (Blob:ArrayBuffer)',
+ { skip: !Blob },
+ async t => {
+ const server = createSecureServer(pem)
+ const expectedBody = 'hello'
+ const requestChunks = []
+ const responseBody = []
+ const buf = Buffer.from(expectedBody)
+ const body = new ArrayBuffer(buf.byteLength)
+
+ buf.copy(new Uint8Array(body))
+
+ server.on('stream', async (stream, headers) => {
+ t.equal(headers[':method'], 'POST')
+ t.equal(headers[':path'], '/')
+ t.equal(headers[':scheme'], 'https')
+
+ stream.on('data', chunk => requestChunks.push(chunk))
+
+ stream.respond({
+ 'content-type': 'text/plain; charset=utf-8',
+ 'x-custom-h2': headers['x-my-header'],
+ ':status': 200
+ })
+
+ stream.end('hello h2!')
+ })
+
+ t.plan(8)
+
+ server.listen(0)
+ await once(server, 'listening')
+
+ const client = new Client(`https://localhost:${server.address().port}`, {
+ connect: {
+ rejectUnauthorized: false
+ },
+ allowH2: true
+ })
+
+ t.teardown(server.close.bind(server))
+ t.teardown(client.close.bind(client))
+
+ const response = await client.request({
+ path: '/',
+ method: 'POST',
+ headers: {
+ 'x-my-header': 'foo'
+ },
+ body
+ })
+
+ response.body.on('data', chunk => {
+ responseBody.push(chunk)
+ })
+
+ await once(response.body, 'end')
+
+ t.equal(response.statusCode, 200)
+ t.equal(response.headers['content-type'], 'text/plain; charset=utf-8')
+ t.equal(response.headers['x-custom-h2'], 'foo')
+ t.equal(Buffer.concat(responseBody).toString('utf-8'), 'hello h2!')
+ t.equal(Buffer.concat(requestChunks).toString('utf-8'), expectedBody)
+ }
+)
+
+test('Agent should support H2 connection', async t => {
+ const body = []
+ const server = createSecureServer(pem)
+
+ server.on('stream', (stream, headers) => {
+ t.equal(headers['x-my-header'], 'foo')
+ t.equal(headers[':method'], 'GET')
+ stream.respond({
+ 'content-type': 'text/plain; charset=utf-8',
+ 'x-custom-h2': 'hello',
+ ':status': 200
+ })
+ stream.end('hello h2!')
+ })
+
+ server.listen(0)
+ await once(server, 'listening')
+
+ const client = new Agent({
+ connect: {
+ rejectUnauthorized: false
+ },
+ allowH2: true
+ })
+
+ t.plan(6)
+ t.teardown(server.close.bind(server))
+ t.teardown(client.close.bind(client))
+
+ const response = await client.request({
+ origin: `https://localhost:${server.address().port}`,
+ path: '/',
+ method: 'GET',
+ headers: {
+ 'x-my-header': 'foo'
+ }
+ })
+
+ response.body.on('data', chunk => {
+ body.push(chunk)
+ })
+
+ await once(response.body, 'end')
+ t.equal(response.statusCode, 200)
+ t.equal(response.headers['content-type'], 'text/plain; charset=utf-8')
+ t.equal(response.headers['x-custom-h2'], 'hello')
+ t.equal(Buffer.concat(body).toString('utf8'), 'hello h2!')
+})
+
+test(
+ 'Should provide pseudo-headers in proper order',
+ { skip: !hasPseudoHeadersOrderFix },
+ async t => {
+ const server = createSecureServer(pem)
+ server.on('stream', (stream, _headers, _flags, rawHeaders) => {
+ t.same(rawHeaders, [
+ ':authority',
+ `localhost:${server.address().port}`,
+ ':method',
+ 'GET',
+ ':path',
+ '/',
+ ':scheme',
+ 'https'
+ ])
+
+ stream.respond({
+ 'content-type': 'text/plain; charset=utf-8',
+ ':status': 200
+ })
+ stream.end()
+ })
+
+ server.listen(0)
+ await once(server, 'listening')
+
+ const client = new Client(`https://localhost:${server.address().port}`, {
+ connect: {
+ rejectUnauthorized: false
+ },
+ allowH2: true
+ })
+
+ t.teardown(server.close.bind(server))
+ t.teardown(client.close.bind(client))
+
+ const response = await client.request({
+ path: '/',
+ method: 'GET'
+ })
+
+ t.equal(response.statusCode, 200)
+ }
+)
+
+test('The h2 pseudo-headers is not included in the headers', async t => {
+ const server = createSecureServer(pem)
+
+ server.on('stream', (stream, headers) => {
+ stream.respond({
+ ':status': 200
+ })
+ stream.end('hello h2!')
+ })
+
+ server.listen(0)
+ await once(server, 'listening')
+
+ const client = new Client(`https://localhost:${server.address().port}`, {
+ connect: {
+ rejectUnauthorized: false
+ },
+ allowH2: true
+ })
+
+ t.plan(2)
+ t.teardown(server.close.bind(server))
+ t.teardown(client.close.bind(client))
+
+ const response = await client.request({
+ path: '/',
+ method: 'GET'
+ })
+
+ await response.body.text()
+
+ t.equal(response.statusCode, 200)
+ t.equal(response.headers[':status'], undefined)
+})
diff --git a/test/https.js b/test/https.js
new file mode 100644
index 0000000..1ba492c
--- /dev/null
+++ b/test/https.js
@@ -0,0 +1,74 @@
+'use strict'
+
+const { test } = require('tap')
+const { Client } = require('..')
+const { createServer } = require('https')
+const pem = require('https-pem')
+
+test('https get with tls opts', (t) => {
+ t.plan(6)
+
+ const server = createServer(pem, (req, res) => {
+ t.equal('/', req.url)
+ t.equal('GET', req.method)
+ res.setHeader('content-type', 'text/plain')
+ res.end('hello')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`https://localhost:${server.address().port}`, {
+ tls: {
+ rejectUnauthorized: false
+ }
+ })
+ t.teardown(client.close.bind(client))
+
+ client.request({ path: '/', method: 'GET' }, (err, { statusCode, headers, body }) => {
+ t.error(err)
+ t.equal(statusCode, 200)
+ t.equal(headers['content-type'], 'text/plain')
+ const bufs = []
+ body.on('data', (buf) => {
+ bufs.push(buf)
+ })
+ body.on('end', () => {
+ t.equal('hello', Buffer.concat(bufs).toString('utf8'))
+ })
+ })
+ })
+})
+
+test('https get with tls opts ip', (t) => {
+ t.plan(6)
+
+ const server = createServer(pem, (req, res) => {
+ t.equal('/', req.url)
+ t.equal('GET', req.method)
+ res.setHeader('content-type', 'text/plain')
+ res.end('hello')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`https://127.0.0.1:${server.address().port}`, {
+ tls: {
+ rejectUnauthorized: false
+ }
+ })
+ t.teardown(client.close.bind(client))
+
+ client.request({ path: '/', method: 'GET' }, (err, { statusCode, headers, body }) => {
+ t.error(err)
+ t.equal(statusCode, 200)
+ t.equal(headers['content-type'], 'text/plain')
+ const bufs = []
+ body.on('data', (buf) => {
+ bufs.push(buf)
+ })
+ body.on('end', () => {
+ t.equal('hello', Buffer.concat(bufs).toString('utf8'))
+ })
+ })
+ })
+})
diff --git a/test/imports/undici-import.ts b/test/imports/undici-import.ts
new file mode 100644
index 0000000..fb7344e
--- /dev/null
+++ b/test/imports/undici-import.ts
@@ -0,0 +1,5 @@
+import { request } from '../../'
+
+async function exampleCode() {
+ await request('http://localhost:3000/foo')
+}
diff --git a/test/inflight-and-close.js b/test/inflight-and-close.js
new file mode 100644
index 0000000..49fbb10
--- /dev/null
+++ b/test/inflight-and-close.js
@@ -0,0 +1,31 @@
+'use strict'
+
+const t = require('tap')
+const { request } = require('..')
+const http = require('http')
+
+const server = http.createServer((req, res) => {
+ res.writeHead(200)
+ res.end('Response body')
+ res.socket.end() // Close the connection immediately with every response
+}).listen(0, '127.0.0.1', function () {
+ const url = `http://127.0.0.1:${this.address().port}`
+ request(url)
+ .then(({ statusCode, headers, body }) => {
+ t.pass('first response')
+ body.resume()
+ body.on('close', function () {
+ t.pass('first body closed')
+ })
+ return request(url)
+ .then(({ statusCode, headers, body }) => {
+ t.pass('second response')
+ body.resume()
+ body.on('close', function () {
+ server.close()
+ })
+ })
+ }).catch((err) => {
+ t.error(err)
+ })
+})
diff --git a/test/invalid-headers.js b/test/invalid-headers.js
new file mode 100644
index 0000000..caf9e0a
--- /dev/null
+++ b/test/invalid-headers.js
@@ -0,0 +1,108 @@
+'use strict'
+
+const { test } = require('tap')
+const { Client, errors } = require('..')
+
+test('invalid headers', (t) => {
+ t.plan(10)
+
+ const client = new Client('http://localhost:3000')
+ t.teardown(client.destroy.bind(client))
+ client.request({
+ path: '/',
+ method: 'GET',
+ headers: {
+ 'content-length': 'asd'
+ }
+ }, (err, data) => {
+ t.type(err, errors.InvalidArgumentError)
+ })
+
+ client.request({
+ path: '/',
+ method: 'GET',
+ headers: 1
+ }, (err, data) => {
+ t.type(err, errors.InvalidArgumentError)
+ })
+
+ client.request({
+ path: '/',
+ method: 'GET',
+ headers: {
+ 'transfer-encoding': 'chunked'
+ }
+ }, (err, data) => {
+ t.type(err, errors.InvalidArgumentError)
+ })
+
+ client.request({
+ path: '/',
+ method: 'GET',
+ headers: {
+ upgrade: 'asd'
+ }
+ }, (err, data) => {
+ t.type(err, errors.InvalidArgumentError)
+ })
+
+ client.request({
+ path: '/',
+ method: 'GET',
+ headers: {
+ connection: 'asd'
+ }
+ }, (err, data) => {
+ t.type(err, errors.InvalidArgumentError)
+ })
+
+ client.request({
+ path: '/',
+ method: 'GET',
+ headers: {
+ 'keep-alive': 'timeout=5'
+ }
+ }, (err, data) => {
+ t.type(err, errors.InvalidArgumentError)
+ })
+
+ client.request({
+ path: '/',
+ method: 'GET',
+ headers: {
+ foo: {}
+ }
+ }, (err, data) => {
+ t.type(err, errors.InvalidArgumentError)
+ })
+
+ client.request({
+ path: '/',
+ method: 'GET',
+ headers: {
+ expect: '100-continue'
+ }
+ }, (err, data) => {
+ t.type(err, errors.NotSupportedError)
+ })
+
+ client.request({
+ path: '/',
+ method: 'GET',
+ headers: {
+ Expect: '100-continue'
+ }
+ }, (err, data) => {
+ t.type(err, errors.NotSupportedError)
+ })
+
+ client.request({
+ path: '/',
+ method: 'GET',
+ headers: {
+ expect: 'asd'
+ }
+ }, (err, data) => {
+ t.type(err, errors.NotSupportedError)
+ })
+})
diff --git a/test/issue-1670.js b/test/issue-1670.js
new file mode 100644
index 0000000..c27bdb2
--- /dev/null
+++ b/test/issue-1670.js
@@ -0,0 +1,12 @@
+'use strict'
+
+const { test } = require('tap')
+const { request } = require('..')
+
+test('https://github.com/mcollina/undici/issues/810', async (t) => {
+ const { body } = await request('https://api.github.com/user/emails')
+
+ await body.text()
+
+ t.end()
+})
diff --git a/test/issue-1903.js b/test/issue-1903.js
new file mode 100644
index 0000000..76ac81e
--- /dev/null
+++ b/test/issue-1903.js
@@ -0,0 +1,78 @@
+'use strict'
+
+const { createServer } = require('http')
+const { test } = require('tap')
+const { request } = require('..')
+const { nodeMajor } = require('../lib/core/util')
+
+function createPromise () {
+ const result = {}
+ result.promise = new Promise((resolve) => {
+ result.resolve = resolve
+ })
+ return result
+}
+
+test('should parse content-disposition consistently', { skip: nodeMajor >= 18 }, async (t) => {
+ t.plan(5)
+
+ // create promise to allow server spinup in parallel
+ const spinup1 = createPromise()
+ const spinup2 = createPromise()
+ const spinup3 = createPromise()
+
+ // variables to store content-disposition header
+ const header = []
+
+ const server = createServer((req, res) => {
+ res.writeHead(200, {
+ 'content-length': 2,
+ 'content-disposition': "attachment; filename='Ã¥r.pdf'"
+ })
+ header.push("attachment; filename='Ã¥r.pdf'")
+ res.end('OK', spinup1.resolve)
+ })
+ t.teardown(server.close.bind(server))
+ server.listen(0, spinup1.resolve)
+
+ const proxy1 = createServer(async (req, res) => {
+ const { statusCode, headers, body } = await request(`http://localhost:${server.address().port}`, {
+ method: 'GET'
+ })
+ header.push(headers['content-disposition'])
+ delete headers['transfer-encoding']
+ res.writeHead(statusCode, headers)
+ body.pipe(res)
+ })
+ t.teardown(proxy1.close.bind(proxy1))
+ proxy1.listen(0, spinup2.resolve)
+
+ const proxy2 = createServer(async (req, res) => {
+ const { statusCode, headers, body } = await request(`http://localhost:${proxy1.address().port}`, {
+ method: 'GET'
+ })
+ header.push(headers['content-disposition'])
+ delete headers['transfer-encoding']
+ res.writeHead(statusCode, headers)
+ body.pipe(res)
+ })
+ t.teardown(proxy2.close.bind(proxy2))
+ proxy2.listen(0, spinup3.resolve)
+
+ // wait until all server spinup
+ await Promise.all([spinup1.promise, spinup2.promise, spinup3.promise])
+
+ const { statusCode, headers, body } = await request(`http://localhost:${proxy2.address().port}`, {
+ method: 'GET'
+ })
+ header.push(headers['content-disposition'])
+ t.equal(statusCode, 200)
+ t.equal(await body.text(), 'OK')
+
+ // we check header
+ // must not be the same in first proxy
+ t.notSame(header[0], header[1])
+ // chaining always the same value
+ t.equal(header[1], header[2])
+ t.equal(header[2], header[3])
+})
diff --git a/test/issue-2065.js b/test/issue-2065.js
new file mode 100644
index 0000000..cc288c4
--- /dev/null
+++ b/test/issue-2065.js
@@ -0,0 +1,71 @@
+'use strict'
+
+const { test, skip } = require('tap')
+const { nodeMajor, nodeMinor } = require('../lib/core/util')
+const { createServer } = require('http')
+const { once } = require('events')
+const { createReadStream } = require('fs')
+const { File, FormData, request } = require('..')
+
+if (nodeMajor < 16 || (nodeMajor === 16 && nodeMinor < 8)) {
+ skip('FormData is not available in node < v16.8.0')
+ process.exit()
+}
+
+test('undici.request with a FormData body should set content-length header', async (t) => {
+ const server = createServer((req, res) => {
+ t.ok(req.headers['content-length'])
+ res.end()
+ }).listen(0)
+
+ t.teardown(server.close.bind(server))
+ await once(server, 'listening')
+
+ const body = new FormData()
+ body.set('file', new File(['abc'], 'abc.txt'))
+
+ await request(`http://localhost:${server.address().port}`, {
+ method: 'POST',
+ body
+ })
+})
+
+test('undici.request with a FormData stream value should set transfer-encoding header', async (t) => {
+ const server = createServer((req, res) => {
+ t.equal(req.headers['transfer-encoding'], 'chunked')
+ res.end()
+ }).listen(0)
+
+ t.teardown(server.close.bind(server))
+ await once(server, 'listening')
+
+ class BlobFromStream {
+ #stream
+ #type
+ constructor (stream, type) {
+ this.#stream = stream
+ this.#type = type
+ }
+
+ stream () {
+ return this.#stream
+ }
+
+ get type () {
+ return this.#type
+ }
+
+ get [Symbol.toStringTag] () {
+ return 'Blob'
+ }
+ }
+
+ const body = new FormData()
+ const fileReadable = createReadStream(__filename)
+ body.set('file', new BlobFromStream(fileReadable, '.js'), 'streamfile')
+
+ await request(`http://localhost:${server.address().port}`, {
+ method: 'POST',
+ body
+ })
+})
diff --git a/test/issue-2078.js b/test/issue-2078.js
new file mode 100644
index 0000000..d3aa868
--- /dev/null
+++ b/test/issue-2078.js
@@ -0,0 +1,30 @@
+'use strict'
+
+const { test, skip } = require('tap')
+const { nodeMajor, nodeMinor } = require('../lib/core/util')
+const { MockAgent, getGlobalDispatcher, setGlobalDispatcher, fetch } = require('..')
+
+if (nodeMajor < 16 || (nodeMajor === 16 && nodeMinor < 8)) {
+ skip('fetch is not supported in node < v16.8.0')
+ process.exit()
+}
+
+test('MockPool.reply headers are an object, not an array - issue #2078', async (t) => {
+ const global = getGlobalDispatcher()
+ const mockAgent = new MockAgent()
+ const mockPool = mockAgent.get('http://localhost')
+
+ t.teardown(() => setGlobalDispatcher(global))
+ setGlobalDispatcher(mockAgent)
+
+ mockPool.intercept({
+ path: '/foo',
+ method: 'GET'
+ }).reply((options) => {
+ t.ok(!Array.isArray(options.headers))
+
+ return { statusCode: 200 }
+ })
+
+ await t.resolves(fetch('http://localhost/foo'))
+})
diff --git a/test/issue-2349.js b/test/issue-2349.js
new file mode 100644
index 0000000..a82bb74
--- /dev/null
+++ b/test/issue-2349.js
@@ -0,0 +1,53 @@
+'use strict'
+
+const { test, skip } = require('tap')
+const { nodeMajor } = require('../lib/core/util')
+const { Writable } = require('stream')
+const { MockAgent, errors, stream } = require('..')
+
+if (nodeMajor < 16) {
+ skip('only for node 16')
+ process.exit(0)
+}
+
+test('stream() does not fail after request has been aborted', async (t) => {
+ t.plan(1)
+
+ const mockAgent = new MockAgent()
+
+ mockAgent.disableNetConnect()
+ mockAgent
+ .get('http://localhost:3333')
+ .intercept({
+ path: '/'
+ })
+ .reply(200, 'ok')
+ .delay(10)
+
+ const parts = []
+ const ac = new AbortController()
+
+ setTimeout(() => ac.abort('nevermind'), 5)
+
+ try {
+ await stream(
+ 'http://localhost:3333/',
+ {
+ opaque: { parts },
+ signal: ac.signal,
+ dispatcher: mockAgent
+ },
+ ({ opaque: { parts } }) => {
+ return new Writable({
+ write (chunk, _encoding, callback) {
+ parts.push(chunk)
+ callback()
+ }
+ })
+ }
+ )
+ } catch (error) {
+ console.log(error)
+ t.equal(error instanceof errors.RequestAbortedError, true)
+ }
+})
diff --git a/test/issue-803.js b/test/issue-803.js
new file mode 100644
index 0000000..70f64cc
--- /dev/null
+++ b/test/issue-803.js
@@ -0,0 +1,47 @@
+'use strict'
+
+const { test } = require('tap')
+const { Client } = require('..')
+const { createServer } = require('http')
+const EE = require('events')
+
+test('https://github.com/nodejs/undici/issues/803', (t) => {
+ t.plan(2)
+
+ const SIZE = 5900373096
+
+ const server = createServer(async (req, res) => {
+ res.setHeader('content-length', SIZE)
+ let pos = 0
+ while (pos < SIZE) {
+ const len = Math.min(SIZE - pos, 65536)
+ if (!res.write(Buffer.allocUnsafe(len))) {
+ await EE.once(res, 'drain')
+ }
+ pos += len
+ }
+
+ res.end()
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.close.bind(client))
+
+ client.request({
+ path: '/',
+ method: 'GET'
+ }, (err, data) => {
+ t.error(err)
+
+ let pos = 0
+ data.body.on('data', (buf) => {
+ pos += buf.length
+ })
+ data.body.on('end', () => {
+ t.equal(pos, SIZE)
+ })
+ })
+ })
+})
diff --git a/test/issue-810.js b/test/issue-810.js
new file mode 100644
index 0000000..226a5aa
--- /dev/null
+++ b/test/issue-810.js
@@ -0,0 +1,135 @@
+'use strict'
+
+const { test } = require('tap')
+const { Client, errors } = require('..')
+const net = require('net')
+
+test('https://github.com/mcollina/undici/issues/810', (t) => {
+ t.plan(3)
+
+ let x = 0
+ const server = net.createServer(socket => {
+ if (x++ === 0) {
+ socket.write('HTTP/1.1 200 OK\r\n')
+ socket.write('Content-Length: 1\r\n\r\n')
+ socket.write('11111\r\n')
+ } else {
+ socket.write('HTTP/1.1 200 OK\r\n')
+ socket.write('Content-Length: 0\r\n\r\n')
+ socket.write('\r\n')
+ }
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, { pipelining: 2 })
+ t.teardown(client.destroy.bind(client))
+
+ client.request({
+ path: '/',
+ method: 'GET'
+ }, (err, data) => {
+ t.error(err)
+ data.body.resume().on('end', () => {
+ // t.fail() FIX: Should fail.
+ t.pass()
+ }).on('error', err => (
+ t.type(err, errors.HTTPParserError)
+ ))
+ })
+ client.request({
+ path: '/',
+ method: 'GET'
+ }, (err, data) => {
+ t.type(err, errors.HTTPParserError)
+ })
+ })
+})
+
+test('https://github.com/mcollina/undici/issues/810 no pipelining', (t) => {
+ t.plan(2)
+
+ const server = net.createServer(socket => {
+ socket.write('HTTP/1.1 200 OK\r\n')
+ socket.write('Content-Length: 1\r\n\r\n')
+ socket.write('11111\r\n')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+
+ client.request({
+ path: '/',
+ method: 'GET'
+ }, (err, data) => {
+ t.error(err)
+ data.body.resume().on('end', () => {
+ // t.fail() FIX: Should fail.
+ t.pass()
+ })
+ })
+ })
+})
+
+test('https://github.com/mcollina/undici/issues/810 pipelining', (t) => {
+ t.plan(2)
+
+ const server = net.createServer(socket => {
+ socket.write('HTTP/1.1 200 OK\r\n')
+ socket.write('Content-Length: 1\r\n\r\n')
+ socket.write('11111\r\n')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, { pipelining: true })
+ t.teardown(client.destroy.bind(client))
+
+ client.request({
+ path: '/',
+ method: 'GET'
+ }, (err, data) => {
+ t.error(err)
+ data.body.resume().on('end', () => {
+ // t.fail() FIX: Should fail.
+ t.pass()
+ })
+ })
+ })
+})
+
+test('https://github.com/mcollina/undici/issues/810 pipelining 2', (t) => {
+ t.plan(4)
+
+ const server = net.createServer(socket => {
+ socket.write('HTTP/1.1 200 OK\r\n')
+ socket.write('Content-Length: 1\r\n\r\n')
+ socket.write('11111\r\n')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, { pipelining: true })
+ t.teardown(client.destroy.bind(client))
+
+ client.request({
+ path: '/',
+ method: 'GET'
+ }, (err, data) => {
+ t.error(err)
+ data.body.resume().on('end', () => {
+ // t.fail() FIX: Should fail.
+ t.pass()
+ })
+ })
+
+ client.request({
+ path: '/',
+ method: 'GET'
+ }, (err, data) => {
+ t.equal(err.code, 'HPE_INVALID_CONSTANT')
+ t.type(err, errors.HTTPParserError)
+ })
+ })
+})
diff --git a/test/jest/instanceof-error.test.js b/test/jest/instanceof-error.test.js
new file mode 100644
index 0000000..8bb36d2
--- /dev/null
+++ b/test/jest/instanceof-error.test.js
@@ -0,0 +1,44 @@
+'use strict'
+
+const { createServer } = require('http')
+const { once } = require('events')
+
+/* global expect, it, jest, AbortController */
+
+// https://github.com/facebook/jest/issues/11607#issuecomment-899068995
+jest.useRealTimers()
+
+const runIf = (condition) => condition ? it : it.skip
+const nodeMajor = Number(process.versions.node.split('.', 1)[0])
+
+runIf(nodeMajor >= 16)('isErrorLike sanity check', () => {
+ const { isErrorLike } = require('../../lib/fetch/util')
+ const { DOMException } = require('../../lib/fetch/constants')
+ const error = new DOMException('')
+
+ // https://github.com/facebook/jest/issues/2549
+ expect(error instanceof Error).toBeFalsy()
+ expect(isErrorLike(error)).toBeTruthy()
+})
+
+runIf(nodeMajor >= 16)('Real use-case', async () => {
+ const { fetch } = require('../..')
+
+ const ac = new AbortController()
+ ac.abort()
+
+ const server = createServer((req, res) => {
+ res.end()
+ }).listen(0)
+
+ await once(server, 'listening')
+
+ const promise = fetch(`https://localhost:${server.address().port}`, {
+ signal: ac.signal
+ })
+
+ await expect(promise).rejects.toThrowError(/^Th(e|is) operation was aborted\.?$/)
+
+ server.close()
+ await once(server, 'close')
+})
diff --git a/test/jest/interceptor.test.js b/test/jest/interceptor.test.js
new file mode 100644
index 0000000..73d70b7
--- /dev/null
+++ b/test/jest/interceptor.test.js
@@ -0,0 +1,197 @@
+'use strict'
+
+const { createServer } = require('http')
+const { Agent, request } = require('../../index')
+const DecoratorHandler = require('../../lib/handler/DecoratorHandler')
+/* global expect */
+
+const defaultOpts = { keepAliveTimeout: 10, keepAliveMaxTimeout: 10 }
+
+describe('interceptors', () => {
+ let server
+ beforeEach(async () => {
+ server = createServer((req, res) => {
+ res.setHeader('Content-Type', 'text/plain')
+ res.end('hello')
+ })
+ await new Promise((resolve) => { server.listen(0, resolve) })
+ })
+ afterEach(async () => {
+ await new Promise((resolve) => server.close(resolve))
+ })
+
+ test('interceptors are applied on client from an agent', async () => {
+ const interceptors = []
+ const buildInterceptor = dispatch => {
+ const interceptorContext = { requestCount: 0 }
+ interceptors.push(interceptorContext)
+ return (opts, handler) => {
+ interceptorContext.requestCount++
+ return dispatch(opts, handler)
+ }
+ }
+
+ const opts = { interceptors: { Client: [buildInterceptor] }, ...defaultOpts }
+ const agent = new Agent(opts)
+ const origin = new URL(`http://localhost:${server.address().port}`)
+ await Promise.all([
+ request(origin, { dispatcher: agent }),
+ request(origin, { dispatcher: agent })
+ ])
+
+ // Assert that the requests are run on different interceptors (different Clients)
+ const requestCounts = interceptors.map(x => x.requestCount)
+ expect(requestCounts).toEqual([1, 1])
+ })
+
+ test('interceptors are applied in the correct order', async () => {
+ const setHeaderInterceptor = (dispatch) => {
+ return (opts, handler) => {
+ opts.headers.push('foo', 'bar')
+ return dispatch(opts, handler)
+ }
+ }
+
+ const assertHeaderInterceptor = (dispatch) => {
+ return (opts, handler) => {
+ expect(opts.headers).toEqual(['foo', 'bar'])
+ return dispatch(opts, handler)
+ }
+ }
+
+ const opts = { interceptors: { Pool: [setHeaderInterceptor, assertHeaderInterceptor] }, ...defaultOpts }
+ const agent = new Agent(opts)
+ const origin = new URL(`http://localhost:${server.address().port}`)
+ await request(origin, { dispatcher: agent, headers: [] })
+ })
+
+ test('interceptors handlers are called in reverse order', async () => {
+ const clearResponseHeadersInterceptor = (dispatch) => {
+ return (opts, handler) => {
+ class ResultInterceptor extends DecoratorHandler {
+ onHeaders (statusCode, headers, resume) {
+ return super.onHeaders(statusCode, [], resume)
+ }
+ }
+
+ return dispatch(opts, new ResultInterceptor(handler))
+ }
+ }
+
+ const assertHeaderInterceptor = (dispatch) => {
+ return (opts, handler) => {
+ class ResultInterceptor extends DecoratorHandler {
+ onHeaders (statusCode, headers, resume) {
+ expect(headers).toEqual([])
+ return super.onHeaders(statusCode, headers, resume)
+ }
+ }
+
+ return dispatch(opts, new ResultInterceptor(handler))
+ }
+ }
+
+ const opts = { interceptors: { Agent: [assertHeaderInterceptor, clearResponseHeadersInterceptor] }, ...defaultOpts }
+ const agent = new Agent(opts)
+ const origin = new URL(`http://localhost:${server.address().port}`)
+ await request(origin, { dispatcher: agent, headers: [] })
+ })
+})
+
+describe('interceptors with NtlmRequestHandler', () => {
+ class FakeNtlmRequestHandler {
+ constructor (dispatch, opts, handler) {
+ this.dispatch = dispatch
+ this.opts = opts
+ this.handler = handler
+ this.requestCount = 0
+ }
+
+ onConnect (...args) {
+ return this.handler.onConnect(...args)
+ }
+
+ onError (...args) {
+ return this.handler.onError(...args)
+ }
+
+ onUpgrade (...args) {
+ return this.handler.onUpgrade(...args)
+ }
+
+ onHeaders (statusCode, headers, resume, statusText) {
+ this.requestCount++
+ if (this.requestCount < 2) {
+ // Do nothing
+ } else {
+ return this.handler.onHeaders(statusCode, headers, resume, statusText)
+ }
+ }
+
+ onData (...args) {
+ if (this.requestCount < 2) {
+ // Do nothing
+ } else {
+ return this.handler.onData(...args)
+ }
+ }
+
+ onComplete (...args) {
+ if (this.requestCount < 2) {
+ this.dispatch(this.opts, this)
+ } else {
+ return this.handler.onComplete(...args)
+ }
+ }
+
+ onBodySent (...args) {
+ if (this.requestCount < 2) {
+ // Do nothing
+ } else {
+ return this.handler.onBodySent(...args)
+ }
+ }
+ }
+ let server
+
+ beforeEach(async () => {
+ // This Test is important because NTLM and Negotiate require several
+ // http requests in sequence to run on the same keepAlive socket
+
+ const socketRequestCountSymbol = Symbol('Socket Request Count')
+ server = createServer((req, res) => {
+ if (req.socket[socketRequestCountSymbol] === undefined) {
+ req.socket[socketRequestCountSymbol] = 0
+ }
+ req.socket[socketRequestCountSymbol]++
+ res.setHeader('Content-Type', 'text/plain')
+
+ // Simulate NTLM/Negotiate logic, by returning 200
+ // on the second request of each socket
+ if (req.socket[socketRequestCountSymbol] >= 2) {
+ res.statusCode = 200
+ res.end()
+ } else {
+ res.statusCode = 401
+ res.end()
+ }
+ })
+ await new Promise((resolve) => { server.listen(0, resolve) })
+ })
+ afterEach(async () => {
+ await new Promise((resolve) => server.close(resolve))
+ })
+
+ test('Retry interceptor on Client will use the same socket', async () => {
+ const interceptor = dispatch => {
+ return (opts, handler) => {
+ return dispatch(opts, new FakeNtlmRequestHandler(dispatch, opts, handler))
+ }
+ }
+ const opts = { interceptors: { Client: [interceptor] }, ...defaultOpts }
+ const agent = new Agent(opts)
+ const origin = new URL(`http://localhost:${server.address().port}`)
+ const { statusCode } = await request(origin, { dispatcher: agent, headers: [] })
+ expect(statusCode).toEqual(200)
+ })
+})
diff --git a/test/jest/issue-1757.test.js b/test/jest/issue-1757.test.js
new file mode 100644
index 0000000..b6519d9
--- /dev/null
+++ b/test/jest/issue-1757.test.js
@@ -0,0 +1,61 @@
+'use strict'
+
+const { Dispatcher, setGlobalDispatcher, MockAgent } = require('../..')
+
+/* global expect, it */
+
+class MiniflareDispatcher extends Dispatcher {
+ constructor (inner, options) {
+ super(options)
+ this.inner = inner
+ }
+
+ dispatch (options, handler) {
+ return this.inner.dispatch(options, handler)
+ }
+
+ close (...args) {
+ return this.inner.close(...args)
+ }
+
+ destroy (...args) {
+ return this.inner.destroy(...args)
+ }
+}
+
+const runIf = (condition) => condition ? it : it.skip
+const nodeMajor = Number(process.versions.node.split('.', 1)[0])
+
+runIf(nodeMajor >= 16)('https://github.com/nodejs/undici/issues/1757', async () => {
+ // fetch isn't exported in <16.8
+ const { fetch } = require('../..')
+
+ const mockAgent = new MockAgent()
+ const mockClient = mockAgent.get('http://localhost:3000')
+ mockAgent.disableNetConnect()
+ setGlobalDispatcher(new MiniflareDispatcher(mockAgent))
+
+ mockClient.intercept({
+ path: () => true,
+ method: () => true
+ }).reply(200, async (opts) => {
+ if (opts.body?.[Symbol.asyncIterator]) {
+ const chunks = []
+ for await (const chunk of opts.body) {
+ chunks.push(chunk)
+ }
+
+ return Buffer.concat(chunks)
+ }
+
+ return opts.body
+ })
+
+ const response = await fetch('http://localhost:3000', {
+ method: 'POST',
+ body: JSON.stringify({ foo: 'bar' })
+ })
+
+ expect(response.json()).resolves.toMatchObject({ foo: 'bar' })
+ expect(response.status).toBe(200)
+})
diff --git a/test/jest/mock-agent.test.js b/test/jest/mock-agent.test.js
new file mode 100644
index 0000000..6f6bac2
--- /dev/null
+++ b/test/jest/mock-agent.test.js
@@ -0,0 +1,46 @@
+'use strict'
+
+const { request, setGlobalDispatcher, MockAgent } = require('../..')
+const { getResponse } = require('../../lib/mock/mock-utils')
+
+/* global describe, it, expect */
+
+describe('MockAgent', () => {
+ let mockAgent
+
+ afterEach(() => {
+ mockAgent.close()
+ })
+
+ it('should work in jest', async () => {
+ expect.assertions(4)
+
+ const baseUrl = 'http://localhost:9999'
+
+ mockAgent = new MockAgent()
+ setGlobalDispatcher(mockAgent)
+ const mockClient = mockAgent.get(baseUrl)
+
+ mockClient.intercept({
+ path: '/foo?hello=there&see=ya',
+ method: 'POST',
+ body: 'form1=data1&form2=data2'
+ }).reply(200, { foo: 'bar' }, {
+ headers: {
+ 'content-type': 'application/json'
+ },
+ trailers: { 'Content-MD5': 'test' }
+ })
+
+ const { statusCode, headers, trailers, body } = await request(`${baseUrl}/foo?hello=there&see=ya`, {
+ method: 'POST',
+ body: 'form1=data1&form2=data2'
+ })
+ expect(statusCode).toBe(200)
+ expect(headers).toEqual({ 'content-type': 'application/json' })
+ expect(trailers).toEqual({ 'content-md5': 'test' })
+
+ const jsonResponse = JSON.parse(await getResponse(body))
+ expect(jsonResponse).toEqual({ foo: 'bar' })
+ })
+})
diff --git a/test/jest/mock-scope.test.js b/test/jest/mock-scope.test.js
new file mode 100644
index 0000000..cab77f6
--- /dev/null
+++ b/test/jest/mock-scope.test.js
@@ -0,0 +1,32 @@
+const { MockAgent, setGlobalDispatcher, request } = require('../../index')
+
+/* global afterAll, expect, it, AbortController */
+
+const runIf = (condition) => condition ? it : it.skip
+
+const nodeMajor = Number(process.versions.node.split('.', 1)[0])
+const mockAgent = new MockAgent()
+
+afterAll(async () => {
+ await mockAgent.close()
+})
+
+runIf(nodeMajor >= 16)('Jest works with MockScope.delay - issue #1327', async () => {
+ mockAgent.disableNetConnect()
+ setGlobalDispatcher(mockAgent)
+
+ const mockPool = mockAgent.get('http://localhost:3333')
+
+ mockPool.intercept({
+ path: '/jest-bugs',
+ method: 'GET'
+ }).reply(200, 'Hello').delay(100)
+
+ const ac = new AbortController()
+ setTimeout(() => ac.abort(), 5)
+ const promise = request('http://localhost:3333/jest-bugs', {
+ signal: ac.signal
+ })
+
+ await expect(promise).rejects.toThrowError('Request aborted')
+}, 1000)
diff --git a/test/jest/test.js b/test/jest/test.js
new file mode 100644
index 0000000..079a41f
--- /dev/null
+++ b/test/jest/test.js
@@ -0,0 +1,36 @@
+'use strict'
+
+const { Client } = require('../..')
+const { createServer } = require('http')
+/* global test, expect */
+
+test('should work in jest', async () => {
+ const server = createServer((req, res) => {
+ expect(req.url).toBe('/')
+ expect(req.method).toBe('POST')
+ expect(req.headers.host).toBe(`localhost:${server.address().port}`)
+ res.setHeader('Content-Type', 'text/plain')
+ res.end('hello')
+ })
+ await expect(new Promise((resolve, reject) => {
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ client.request({
+ path: '/',
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: '{}'
+ }, (err, result) => {
+ server.close()
+ client.close()
+ if (err) {
+ reject(err)
+ } else {
+ resolve(result.body.text())
+ }
+ })
+ })
+ })).resolves.toBe('hello')
+})
diff --git a/test/max-headers.js b/test/max-headers.js
new file mode 100644
index 0000000..a08b931
--- /dev/null
+++ b/test/max-headers.js
@@ -0,0 +1,41 @@
+'use strict'
+
+const { test } = require('tap')
+const { Client } = require('..')
+const { createServer } = require('http')
+
+test('handle a lot of headers', (t) => {
+ t.plan(3)
+
+ const headers = {}
+ for (let n = 0; n < 64; ++n) {
+ headers[n] = String(n)
+ }
+
+ const server = createServer((req, res) => {
+ res.writeHead(200, headers)
+ res.end()
+ })
+ t.teardown(server.close.bind(server))
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ client.request({
+ path: '/',
+ method: 'GET'
+ }, (err, data) => {
+ t.error(err)
+ const headers2 = {}
+ for (let n = 0; n < 64; ++n) {
+ headers2[n] = data.headers[n]
+ }
+ t.strictSame(headers2, headers)
+ data.body
+ .resume()
+ .on('end', () => {
+ t.pass()
+ })
+ })
+ })
+})
diff --git a/test/max-response-size.js b/test/max-response-size.js
new file mode 100644
index 0000000..75bfade
--- /dev/null
+++ b/test/max-response-size.js
@@ -0,0 +1,105 @@
+'use strict'
+
+const { test } = require('tap')
+const { Client, errors } = require('..')
+const { createServer } = require('http')
+
+test('max response size', (t) => {
+ t.plan(4)
+
+ t.test('default max default size should allow all responses', (t) => {
+ t.plan(3)
+
+ const server = createServer()
+ t.teardown(server.close.bind(server))
+
+ server.on('request', (req, res) => {
+ res.end('hello')
+ })
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, { maxResponseSize: -1 })
+ t.teardown(client.close.bind(client))
+
+ client.request({ path: '/', method: 'GET' }, (err, { statusCode, body }) => {
+ t.error(err)
+ t.equal(statusCode, 200)
+ const bufs = []
+ body.on('data', (buf) => {
+ bufs.push(buf)
+ })
+ body.on('end', () => {
+ t.equal('hello', Buffer.concat(bufs).toString('utf8'))
+ })
+ })
+ })
+ })
+
+ t.test('max response size set to zero should allow only empty responses', (t) => {
+ t.plan(3)
+
+ const server = createServer()
+ t.teardown(server.close.bind(server))
+
+ server.on('request', (req, res) => {
+ res.end()
+ })
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, { maxResponseSize: 0 })
+ t.teardown(client.close.bind(client))
+
+ client.request({ path: '/', method: 'GET' }, (err, { statusCode, body }) => {
+ t.error(err)
+ t.equal(statusCode, 200)
+ const bufs = []
+ body.on('data', (buf) => {
+ bufs.push(buf)
+ })
+ body.on('end', () => {
+ t.equal('', Buffer.concat(bufs).toString('utf8'))
+ })
+ })
+ })
+ })
+
+ t.test('should throw an error if the response is too big', (t) => {
+ t.plan(3)
+
+ const server = createServer()
+ t.teardown(server.close.bind(server))
+
+ server.on('request', (req, res) => {
+ res.end('hello')
+ })
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, {
+ maxResponseSize: 1
+ })
+
+ t.teardown(client.close.bind(client))
+
+ client.request({ path: '/', method: 'GET' }, (err, { body }) => {
+ t.error(err)
+ body.on('error', (err) => {
+ t.ok(err)
+ t.type(err, errors.ResponseExceededMaxSizeError)
+ })
+ })
+ })
+ })
+
+ t.test('invalid max response size should throw an error', (t) => {
+ t.plan(2)
+
+ t.throws(() => {
+ // eslint-disable-next-line no-new
+ new Client('http://localhost:3000', { maxResponseSize: 'hello' })
+ }, 'maxResponseSize must be a number')
+ t.throws(() => {
+ // eslint-disable-next-line no-new
+ new Client('http://localhost:3000', { maxResponseSize: -2 })
+ }, 'maxResponseSize must be greater than or equal to -1')
+ })
+})
diff --git a/test/mock-agent.js b/test/mock-agent.js
new file mode 100644
index 0000000..c9ffda4
--- /dev/null
+++ b/test/mock-agent.js
@@ -0,0 +1,2637 @@
+'use strict'
+
+const { test } = require('tap')
+const { createServer } = require('http')
+const { promisify } = require('util')
+const { request, setGlobalDispatcher, MockAgent, Agent } = require('..')
+const { getResponse } = require('../lib/mock/mock-utils')
+const { kClients, kConnected } = require('../lib/core/symbols')
+const { InvalidArgumentError, ClientDestroyedError } = require('../lib/core/errors')
+const { nodeMajor } = require('../lib/core/util')
+const MockClient = require('../lib/mock/mock-client')
+const MockPool = require('../lib/mock/mock-pool')
+const { kAgent } = require('../lib/mock/mock-symbols')
+const Dispatcher = require('../lib/dispatcher')
+const { MockNotMatchedError } = require('../lib/mock/mock-errors')
+
+test('MockAgent - constructor', t => {
+ t.plan(5)
+
+ t.test('sets up mock agent', t => {
+ t.plan(1)
+ t.doesNotThrow(() => new MockAgent())
+ })
+
+ t.test('should implement the Dispatcher API', t => {
+ t.plan(1)
+
+ const mockAgent = new MockAgent()
+ t.type(mockAgent, Dispatcher)
+ })
+
+ t.test('sets up mock agent with single connection', t => {
+ t.plan(1)
+ t.doesNotThrow(() => new MockAgent({ connections: 1 }))
+ })
+
+ t.test('should error passed agent is not valid', t => {
+ t.plan(2)
+ t.throws(() => new MockAgent({ agent: {} }), new InvalidArgumentError('Argument opts.agent must implement Agent'))
+ t.throws(() => new MockAgent({ agent: { dispatch: '' } }), new InvalidArgumentError('Argument opts.agent must implement Agent'))
+ })
+
+ t.test('should be able to specify the agent to mock', t => {
+ t.plan(1)
+ const agent = new Agent()
+ t.teardown(agent.close.bind(agent))
+ const mockAgent = new MockAgent({ agent })
+
+ t.equal(mockAgent[kAgent], agent)
+ })
+})
+
+test('MockAgent - get', t => {
+ t.plan(3)
+
+ t.test('should return MockClient', (t) => {
+ t.plan(1)
+
+ const baseUrl = 'http://localhost:9999'
+
+ const mockAgent = new MockAgent({ connections: 1 })
+ t.teardown(mockAgent.close.bind(mockAgent))
+
+ const mockClient = mockAgent.get(baseUrl)
+ t.type(mockClient, MockClient)
+ })
+
+ t.test('should return MockPool', (t) => {
+ t.plan(1)
+
+ const baseUrl = 'http://localhost:9999'
+
+ const mockAgent = new MockAgent()
+ t.teardown(mockAgent.close.bind(mockAgent))
+
+ const mockPool = mockAgent.get(baseUrl)
+ t.type(mockPool, MockPool)
+ })
+
+ t.test('should return the same instance if already created', (t) => {
+ t.plan(1)
+
+ const baseUrl = 'http://localhost:9999'
+
+ const mockAgent = new MockAgent()
+ t.teardown(mockAgent.close.bind(mockAgent))
+
+ const mockPool1 = mockAgent.get(baseUrl)
+ const mockPool2 = mockAgent.get(baseUrl)
+ t.equal(mockPool1, mockPool2)
+ })
+})
+
+test('MockAgent - dispatch', t => {
+ t.plan(3)
+
+ t.test('should call the dispatch method of the MockPool', (t) => {
+ t.plan(1)
+
+ const baseUrl = 'http://localhost:9999'
+
+ const mockAgent = new MockAgent()
+ t.teardown(mockAgent.close.bind(mockAgent))
+
+ const mockPool = mockAgent.get(baseUrl)
+
+ mockPool.intercept({
+ path: '/foo',
+ method: 'GET'
+ }).reply(200, 'hello')
+
+ t.doesNotThrow(() => mockAgent.dispatch({
+ origin: baseUrl,
+ path: '/foo',
+ method: 'GET'
+ }, {
+ onHeaders: (_statusCode, _headers, resume) => resume(),
+ onData: () => {},
+ onComplete: () => {},
+ onError: () => {}
+ }))
+ })
+
+ t.test('should call the dispatch method of the MockClient', (t) => {
+ t.plan(1)
+
+ const baseUrl = 'http://localhost:9999'
+
+ const mockAgent = new MockAgent({ connections: 1 })
+ t.teardown(mockAgent.close.bind(mockAgent))
+
+ const mockClient = mockAgent.get(baseUrl)
+
+ mockClient.intercept({
+ path: '/foo',
+ method: 'GET'
+ }).reply(200, 'hello')
+
+ t.doesNotThrow(() => mockAgent.dispatch({
+ origin: baseUrl,
+ path: '/foo',
+ method: 'GET'
+ }, {
+ onHeaders: (_statusCode, _headers, resume) => resume(),
+ onData: () => {},
+ onComplete: () => {},
+ onError: () => {}
+ }))
+ })
+
+ t.test('should throw if handler is not valid on redirect', (t) => {
+ t.plan(7)
+
+ const baseUrl = 'http://localhost:9999'
+
+ const mockAgent = new MockAgent()
+ t.teardown(mockAgent.close.bind(mockAgent))
+
+ t.throws(() => mockAgent.dispatch({
+ origin: baseUrl,
+ path: '/foo',
+ method: 'GET'
+ }, {
+ onError: 'INVALID'
+ }), new InvalidArgumentError('invalid onError method'))
+
+ t.throws(() => mockAgent.dispatch({
+ origin: baseUrl,
+ path: '/foo',
+ method: 'GET'
+ }, {
+ onError: (err) => { throw err },
+ onConnect: 'INVALID'
+ }), new InvalidArgumentError('invalid onConnect method'))
+
+ t.throws(() => mockAgent.dispatch({
+ origin: baseUrl,
+ path: '/foo',
+ method: 'GET'
+ }, {
+ onError: (err) => { throw err },
+ onConnect: () => {},
+ onBodySent: 'INVALID'
+ }), new InvalidArgumentError('invalid onBodySent method'))
+
+ t.throws(() => mockAgent.dispatch({
+ origin: baseUrl,
+ path: '/foo',
+ method: 'CONNECT'
+ }, {
+ onError: (err) => { throw err },
+ onConnect: () => {},
+ onBodySent: () => {},
+ onUpgrade: 'INVALID'
+ }), new InvalidArgumentError('invalid onUpgrade method'))
+
+ t.throws(() => mockAgent.dispatch({
+ origin: baseUrl,
+ path: '/foo',
+ method: 'GET'
+ }, {
+ onError: (err) => { throw err },
+ onConnect: () => {},
+ onBodySent: () => {},
+ onHeaders: 'INVALID'
+ }), new InvalidArgumentError('invalid onHeaders method'))
+
+ t.throws(() => mockAgent.dispatch({
+ origin: baseUrl,
+ path: '/foo',
+ method: 'GET'
+ }, {
+ onError: (err) => { throw err },
+ onConnect: () => {},
+ onBodySent: () => {},
+ onHeaders: () => {},
+ onData: 'INVALID'
+ }), new InvalidArgumentError('invalid onData method'))
+
+ t.throws(() => mockAgent.dispatch({
+ origin: baseUrl,
+ path: '/foo',
+ method: 'GET'
+ }, {
+ onError: (err) => { throw err },
+ onConnect: () => {},
+ onBodySent: () => {},
+ onHeaders: () => {},
+ onData: () => {},
+ onComplete: 'INVALID'
+ }), new InvalidArgumentError('invalid onComplete method'))
+ })
+})
+
+test('MockAgent - .close should clean up registered pools', async (t) => {
+ t.plan(5)
+
+ const baseUrl = 'http://localhost:9999'
+
+ const mockAgent = new MockAgent()
+
+ // Register a pool
+ const mockPool = mockAgent.get(baseUrl)
+ t.type(mockPool, MockPool)
+
+ t.equal(mockPool[kConnected], 1)
+ t.equal(mockAgent[kClients].size, 1)
+ await mockAgent.close()
+ t.equal(mockPool[kConnected], 0)
+ t.equal(mockAgent[kClients].size, 0)
+})
+
+test('MockAgent - .close should clean up registered clients', async (t) => {
+ t.plan(5)
+
+ const baseUrl = 'http://localhost:9999'
+
+ const mockAgent = new MockAgent({ connections: 1 })
+
+ // Register a pool
+ const mockClient = mockAgent.get(baseUrl)
+ t.type(mockClient, MockClient)
+
+ t.equal(mockClient[kConnected], 1)
+ t.equal(mockAgent[kClients].size, 1)
+ await mockAgent.close()
+ t.equal(mockClient[kConnected], 0)
+ t.equal(mockAgent[kClients].size, 0)
+})
+
+test('MockAgent - [kClients] should match encapsulated agent', async (t) => {
+ t.plan(1)
+
+ const server = createServer((req, res) => {
+ res.setHeader('content-type', 'text/plain')
+ res.end('should not be called')
+ t.fail('should not be called')
+ t.end()
+ })
+ t.teardown(server.close.bind(server))
+
+ await promisify(server.listen.bind(server))(0)
+
+ const baseUrl = `http://localhost:${server.address().port}`
+
+ const agent = new Agent()
+ t.teardown(agent.close.bind(agent))
+
+ const mockAgent = new MockAgent({ agent })
+
+ const mockPool = mockAgent.get(baseUrl)
+ mockPool.intercept({
+ path: '/foo',
+ method: 'GET'
+ }).reply(200, 'hello')
+
+ // The MockAgent should encapsulate the input agent clients
+ t.equal(mockAgent[kClients].size, agent[kClients].size)
+})
+
+test('MockAgent - basic intercept with MockAgent.request', async (t) => {
+ t.plan(4)
+
+ const server = createServer((req, res) => {
+ res.setHeader('content-type', 'text/plain')
+ res.end('should not be called')
+ t.fail('should not be called')
+ t.end()
+ })
+ t.teardown(server.close.bind(server))
+
+ await promisify(server.listen.bind(server))(0)
+
+ const baseUrl = `http://localhost:${server.address().port}`
+
+ const mockAgent = new MockAgent()
+ t.teardown(mockAgent.close.bind(mockAgent))
+ const mockPool = mockAgent.get(baseUrl)
+
+ mockPool.intercept({
+ path: '/foo?hello=there&see=ya',
+ method: 'POST',
+ body: 'form1=data1&form2=data2'
+ }).reply(200, { foo: 'bar' }, {
+ headers: { 'content-type': 'application/json' },
+ trailers: { 'Content-MD5': 'test' }
+ })
+
+ const { statusCode, headers, trailers, body } = await mockAgent.request({
+ origin: baseUrl,
+ path: '/foo?hello=there&see=ya',
+ method: 'POST',
+ body: 'form1=data1&form2=data2'
+ })
+ t.equal(statusCode, 200)
+ t.equal(headers['content-type'], 'application/json')
+ t.same(trailers, { 'content-md5': 'test' })
+
+ const jsonResponse = JSON.parse(await getResponse(body))
+ t.same(jsonResponse, {
+ foo: 'bar'
+ })
+})
+
+test('MockAgent - basic intercept with request', async (t) => {
+ t.plan(4)
+
+ const server = createServer((req, res) => {
+ res.setHeader('content-type', 'text/plain')
+ res.end('should not be called')
+ t.fail('should not be called')
+ t.end()
+ })
+ t.teardown(server.close.bind(server))
+
+ await promisify(server.listen.bind(server))(0)
+
+ const baseUrl = `http://localhost:${server.address().port}`
+
+ const mockAgent = new MockAgent()
+ setGlobalDispatcher(mockAgent)
+ t.teardown(mockAgent.close.bind(mockAgent))
+ const mockPool = mockAgent.get(baseUrl)
+
+ mockPool.intercept({
+ path: '/foo?hello=there&see=ya',
+ method: 'POST',
+ body: 'form1=data1&form2=data2'
+ }).reply(200, { foo: 'bar' }, {
+ headers: { 'content-type': 'application/json' },
+ trailers: { 'Content-MD5': 'test' }
+ })
+
+ const { statusCode, headers, trailers, body } = await request(`${baseUrl}/foo?hello=there&see=ya`, {
+ method: 'POST',
+ body: 'form1=data1&form2=data2'
+ })
+ t.equal(statusCode, 200)
+ t.equal(headers['content-type'], 'application/json')
+ t.same(trailers, { 'content-md5': 'test' })
+
+ const jsonResponse = JSON.parse(await getResponse(body))
+ t.same(jsonResponse, {
+ foo: 'bar'
+ })
+})
+
+test('MockAgent - should support local agents', async (t) => {
+ t.plan(4)
+
+ const server = createServer((req, res) => {
+ res.setHeader('content-type', 'text/plain')
+ res.end('should not be called')
+ t.fail('should not be called')
+ t.end()
+ })
+ t.teardown(server.close.bind(server))
+
+ await promisify(server.listen.bind(server))(0)
+
+ const baseUrl = `http://localhost:${server.address().port}`
+
+ const mockAgent = new MockAgent()
+
+ t.teardown(mockAgent.close.bind(mockAgent))
+ const mockPool = mockAgent.get(baseUrl)
+
+ mockPool.intercept({
+ path: '/foo?hello=there&see=ya',
+ method: 'POST',
+ body: 'form1=data1&form2=data2'
+ }).reply(200, { foo: 'bar' }, {
+ headers: {
+ 'content-type': 'application/json'
+ },
+ trailers: { 'Content-MD5': 'test' }
+ })
+
+ const { statusCode, headers, trailers, body } = await request(`${baseUrl}/foo?hello=there&see=ya`, {
+ method: 'POST',
+ body: 'form1=data1&form2=data2',
+ dispatcher: mockAgent
+ })
+ t.equal(statusCode, 200)
+ t.equal(headers['content-type'], 'application/json')
+ t.same(trailers, { 'content-md5': 'test' })
+
+ const jsonResponse = JSON.parse(await getResponse(body))
+ t.same(jsonResponse, {
+ foo: 'bar'
+ })
+})
+
+test('MockAgent - should support specifying custom agents to mock', async (t) => {
+ t.plan(4)
+
+ const server = createServer((req, res) => {
+ res.setHeader('content-type', 'text/plain')
+ res.end('should not be called')
+ t.fail('should not be called')
+ t.end()
+ })
+ t.teardown(server.close.bind(server))
+
+ await promisify(server.listen.bind(server))(0)
+
+ const baseUrl = `http://localhost:${server.address().port}`
+
+ const agent = new Agent()
+ t.teardown(agent.close.bind(agent))
+
+ const mockAgent = new MockAgent({ agent })
+ setGlobalDispatcher(mockAgent)
+
+ const mockPool = mockAgent.get(baseUrl)
+ mockPool.intercept({
+ path: '/foo?hello=there&see=ya',
+ method: 'POST',
+ body: 'form1=data1&form2=data2'
+ }).reply(200, { foo: 'bar' }, {
+ headers: {
+ 'content-type': 'application/json'
+ },
+ trailers: { 'Content-MD5': 'test' }
+ })
+
+ const { statusCode, headers, trailers, body } = await request(`${baseUrl}/foo?hello=there&see=ya`, {
+ method: 'POST',
+ body: 'form1=data1&form2=data2'
+ })
+ t.equal(statusCode, 200)
+ t.equal(headers['content-type'], 'application/json')
+ t.same(trailers, { 'content-md5': 'test' })
+
+ const jsonResponse = JSON.parse(await getResponse(body))
+ t.same(jsonResponse, {
+ foo: 'bar'
+ })
+})
+
+test('MockAgent - basic Client intercept with request', async (t) => {
+ t.plan(4)
+
+ const server = createServer((req, res) => {
+ res.setHeader('content-type', 'text/plain')
+ res.end('should not be called')
+ t.fail('should not be called')
+ t.end()
+ })
+ t.teardown(server.close.bind(server))
+
+ await promisify(server.listen.bind(server))(0)
+
+ const baseUrl = `http://localhost:${server.address().port}`
+
+ const mockAgent = new MockAgent({ connections: 1 })
+ setGlobalDispatcher(mockAgent)
+ t.teardown(mockAgent.close.bind(mockAgent))
+
+ const mockClient = mockAgent.get(baseUrl)
+ mockClient.intercept({
+ path: '/foo?hello=there&see=ya',
+ method: 'POST',
+ body: 'form1=data1&form2=data2'
+ }).reply(200, { foo: 'bar' }, {
+ headers: {
+ 'content-type': 'application/json'
+ },
+ trailers: { 'Content-MD5': 'test' }
+ })
+
+ const { statusCode, headers, trailers, body } = await request(`${baseUrl}/foo?hello=there&see=ya`, {
+ method: 'POST',
+ body: 'form1=data1&form2=data2'
+ })
+ t.equal(statusCode, 200)
+ t.equal(headers['content-type'], 'application/json')
+ t.same(trailers, { 'content-md5': 'test' })
+
+ const jsonResponse = JSON.parse(await getResponse(body))
+ t.same(jsonResponse, {
+ foo: 'bar'
+ })
+})
+
+test('MockAgent - basic intercept with multiple pools', async (t) => {
+ t.plan(4)
+
+ const server = createServer((req, res) => {
+ res.setHeader('content-type', 'text/plain')
+ res.end('should not be called')
+ t.fail('should not be called')
+ t.end()
+ })
+ t.teardown(server.close.bind(server))
+
+ await promisify(server.listen.bind(server))(0)
+
+ const baseUrl = `http://localhost:${server.address().port}`
+
+ const mockAgent = new MockAgent()
+ setGlobalDispatcher(mockAgent)
+ t.teardown(mockAgent.close.bind(mockAgent))
+ const mockPool1 = mockAgent.get(baseUrl)
+ const mockPool2 = mockAgent.get('http://localhost:9999')
+
+ mockPool1.intercept({
+ path: '/foo?hello=there&see=ya',
+ method: 'POST',
+ body: 'form1=data1&form2=data2'
+ }).reply(200, { foo: 'bar-1' }, {
+ headers: {
+ 'content-type': 'application/json'
+ },
+ trailers: { 'Content-MD5': 'test' }
+ })
+
+ mockPool2.intercept({
+ path: '/foo?hello=there&see=ya',
+ method: 'GET',
+ body: 'form1=data1&form2=data2'
+ }).reply(200, { foo: 'bar-2' })
+
+ const { statusCode, headers, trailers, body } = await request(`${baseUrl}/foo?hello=there&see=ya`, {
+ method: 'POST',
+ body: 'form1=data1&form2=data2'
+ })
+ t.equal(statusCode, 200)
+ t.equal(headers['content-type'], 'application/json')
+ t.same(trailers, { 'content-md5': 'test' })
+
+ const jsonResponse = JSON.parse(await getResponse(body))
+ t.same(jsonResponse, {
+ foo: 'bar-1'
+ })
+})
+
+test('MockAgent - should handle multiple responses for an interceptor', async (t) => {
+ t.plan(6)
+
+ const server = createServer((req, res) => {
+ res.setHeader('content-type', 'text/plain')
+ res.end('should not be called')
+ t.fail('should not be called')
+ t.end()
+ })
+ t.teardown(server.close.bind(server))
+
+ await promisify(server.listen.bind(server))(0)
+
+ const baseUrl = `http://localhost:${server.address().port}`
+
+ const mockAgent = new MockAgent()
+ setGlobalDispatcher(mockAgent)
+ t.teardown(mockAgent.close.bind(mockAgent))
+
+ const mockPool = mockAgent.get(baseUrl)
+
+ const interceptor = mockPool.intercept({
+ path: '/foo',
+ method: 'POST'
+ })
+ interceptor.reply(200, { foo: 'bar' }, {
+ headers: {
+ 'content-type': 'application/json'
+ }
+ })
+ interceptor.reply(200, { hello: 'there' }, {
+ headers: {
+ 'content-type': 'application/json'
+ }
+ })
+
+ {
+ const { statusCode, headers, body } = await request(`${baseUrl}/foo`, {
+ method: 'POST'
+ })
+ t.equal(statusCode, 200)
+ t.equal(headers['content-type'], 'application/json')
+
+ const jsonResponse = JSON.parse(await getResponse(body))
+ t.same(jsonResponse, {
+ foo: 'bar'
+ })
+ }
+
+ {
+ const { statusCode, headers, body } = await request(`${baseUrl}/foo`, {
+ method: 'POST'
+ })
+ t.equal(statusCode, 200)
+ t.equal(headers['content-type'], 'application/json')
+
+ const jsonResponse = JSON.parse(await getResponse(body))
+ t.same(jsonResponse, {
+ hello: 'there'
+ })
+ }
+})
+
+test('MockAgent - should call original Pool dispatch if request not found', async (t) => {
+ t.plan(5)
+
+ const server = createServer((req, res) => {
+ t.equal(req.url, '/foo')
+ t.equal(req.method, 'GET')
+ res.setHeader('content-type', 'text/plain')
+ res.end('hello')
+ })
+ t.teardown(server.close.bind(server))
+
+ await promisify(server.listen.bind(server))(0)
+
+ const baseUrl = `http://localhost:${server.address().port}`
+
+ const mockAgent = new MockAgent()
+ setGlobalDispatcher(mockAgent)
+ t.teardown(mockAgent.close.bind(mockAgent))
+
+ const { statusCode, headers, body } = await request(`${baseUrl}/foo`, {
+ method: 'GET'
+ })
+ t.equal(statusCode, 200)
+ t.equal(headers['content-type'], 'text/plain')
+
+ const response = await getResponse(body)
+ t.equal(response, 'hello')
+})
+
+test('MockAgent - should call original Client dispatch if request not found', async (t) => {
+ t.plan(5)
+
+ const server = createServer((req, res) => {
+ t.equal(req.url, '/foo')
+ t.equal(req.method, 'GET')
+ res.setHeader('content-type', 'text/plain')
+ res.end('hello')
+ })
+ t.teardown(server.close.bind(server))
+
+ await promisify(server.listen.bind(server))(0)
+
+ const baseUrl = `http://localhost:${server.address().port}`
+
+ const mockAgent = new MockAgent({ connections: 1 })
+ setGlobalDispatcher(mockAgent)
+ t.teardown(mockAgent.close.bind(mockAgent))
+
+ const { statusCode, headers, body } = await request(`${baseUrl}/foo`, {
+ method: 'GET'
+ })
+ t.equal(statusCode, 200)
+ t.equal(headers['content-type'], 'text/plain')
+
+ const response = await getResponse(body)
+ t.equal(response, 'hello')
+})
+
+test('MockAgent - should handle string responses', async (t) => {
+ t.plan(2)
+
+ const server = createServer((req, res) => {
+ res.setHeader('content-type', 'text/plain')
+ res.end('should not be called')
+ t.fail('should not be called')
+ t.end()
+ })
+ t.teardown(server.close.bind(server))
+
+ await promisify(server.listen.bind(server))(0)
+
+ const baseUrl = `http://localhost:${server.address().port}`
+
+ const mockAgent = new MockAgent()
+ setGlobalDispatcher(mockAgent)
+ t.teardown(mockAgent.close.bind(mockAgent))
+
+ const mockPool = mockAgent.get(baseUrl)
+ mockPool.intercept({
+ path: '/foo',
+ method: 'POST'
+ }).reply(200, 'hello')
+
+ const { statusCode, body } = await request(`${baseUrl}/foo`, {
+ method: 'POST'
+ })
+ t.equal(statusCode, 200)
+
+ const response = await getResponse(body)
+ t.equal(response, 'hello')
+})
+
+test('MockAgent - should handle basic concurrency for requests', { jobs: 5 }, async (t) => {
+ t.plan(5)
+
+ const mockAgent = new MockAgent()
+ setGlobalDispatcher(mockAgent)
+ t.teardown(mockAgent.close.bind(mockAgent))
+
+ await Promise.all([...Array(5).keys()].map(idx =>
+ t.test(`concurrent job (${idx})`, async (innerTest) => {
+ innerTest.plan(2)
+
+ const baseUrl = 'http://localhost:9999'
+
+ const mockPool = mockAgent.get(baseUrl)
+ mockPool.intercept({
+ path: '/foo',
+ method: 'POST'
+ }).reply(200, { foo: `bar ${idx}` })
+
+ const { statusCode, body } = await request(`${baseUrl}/foo`, {
+ method: 'POST'
+ })
+ innerTest.equal(statusCode, 200)
+
+ const jsonResponse = JSON.parse(await getResponse(body))
+ innerTest.same(jsonResponse, {
+ foo: `bar ${idx}`
+ })
+ })
+ ))
+})
+
+test('MockAgent - handle delays to simulate work', async (t) => {
+ t.plan(3)
+
+ const server = createServer((req, res) => {
+ res.setHeader('content-type', 'text/plain')
+ res.end('should not be called')
+ t.fail('should not be called')
+ t.end()
+ })
+ t.teardown(server.close.bind(server))
+
+ await promisify(server.listen.bind(server))(0)
+
+ const baseUrl = `http://localhost:${server.address().port}`
+
+ const mockAgent = new MockAgent()
+ setGlobalDispatcher(mockAgent)
+ t.teardown(mockAgent.close.bind(mockAgent))
+
+ const mockPool = mockAgent.get(baseUrl)
+ mockPool.intercept({
+ path: '/foo',
+ method: 'POST'
+ }).reply(200, 'hello').delay(50)
+
+ const start = process.hrtime()
+
+ const { statusCode, body } = await request(`${baseUrl}/foo`, {
+ method: 'POST'
+ })
+ t.equal(statusCode, 200)
+
+ const response = await getResponse(body)
+ t.equal(response, 'hello')
+ const elapsedInMs = process.hrtime(start)[1] / 1e6
+ t.ok(elapsedInMs >= 50, `Elapsed time is not greater than 50ms: ${elapsedInMs}`)
+})
+
+test('MockAgent - should persist requests', async (t) => {
+ t.plan(8)
+
+ const server = createServer((req, res) => {
+ res.setHeader('content-type', 'text/plain')
+ res.end('should not be called')
+ t.fail('should not be called')
+ t.end()
+ })
+ t.teardown(server.close.bind(server))
+
+ await promisify(server.listen.bind(server))(0)
+
+ const baseUrl = `http://localhost:${server.address().port}`
+
+ const mockAgent = new MockAgent()
+ setGlobalDispatcher(mockAgent)
+ t.teardown(mockAgent.close.bind(mockAgent))
+
+ const mockPool = mockAgent.get(baseUrl)
+ mockPool.intercept({
+ path: '/foo?hello=there&see=ya',
+ method: 'POST',
+ body: 'form1=data1&form2=data2'
+ }).reply(200, { foo: 'bar' }, {
+ headers: {
+ 'content-type': 'application/json'
+ },
+ trailers: { 'Content-MD5': 'test' }
+ }).persist()
+
+ {
+ const { statusCode, headers, trailers, body } = await request(`${baseUrl}/foo?hello=there&see=ya`, {
+ method: 'POST',
+ body: 'form1=data1&form2=data2'
+ })
+ t.equal(statusCode, 200)
+ t.equal(headers['content-type'], 'application/json')
+ t.same(trailers, { 'content-md5': 'test' })
+
+ const jsonResponse = JSON.parse(await getResponse(body))
+ t.same(jsonResponse, {
+ foo: 'bar'
+ })
+ }
+
+ {
+ const { statusCode, headers, trailers, body } = await request(`${baseUrl}/foo?hello=there&see=ya`, {
+ method: 'POST',
+ body: 'form1=data1&form2=data2'
+ })
+ t.equal(statusCode, 200)
+ t.equal(headers['content-type'], 'application/json')
+ t.same(trailers, { 'content-md5': 'test' })
+
+ const jsonResponse = JSON.parse(await getResponse(body))
+ t.same(jsonResponse, {
+ foo: 'bar'
+ })
+ }
+})
+
+test('MockAgent - handle persists with delayed requests', async (t) => {
+ t.plan(4)
+
+ const server = createServer((req, res) => {
+ res.setHeader('content-type', 'text/plain')
+ res.end('should not be called')
+ t.fail('should not be called')
+ t.end()
+ })
+ t.teardown(server.close.bind(server))
+
+ await promisify(server.listen.bind(server))(0)
+
+ const baseUrl = `http://localhost:${server.address().port}`
+
+ const mockAgent = new MockAgent()
+ setGlobalDispatcher(mockAgent)
+ t.teardown(mockAgent.close.bind(mockAgent))
+
+ const mockPool = mockAgent.get(baseUrl)
+ mockPool.intercept({
+ path: '/foo',
+ method: 'POST'
+ }).reply(200, 'hello').delay(1).persist()
+
+ {
+ const { statusCode, body } = await request(`${baseUrl}/foo`, {
+ method: 'POST'
+ })
+ t.equal(statusCode, 200)
+
+ const response = await getResponse(body)
+ t.equal(response, 'hello')
+ }
+
+ {
+ const { statusCode, body } = await request(`${baseUrl}/foo`, {
+ method: 'POST'
+ })
+ t.equal(statusCode, 200)
+
+ const response = await getResponse(body)
+ t.equal(response, 'hello')
+ }
+})
+
+test('MockAgent - calling close on a mock pool should not affect other mock pools', async (t) => {
+ t.plan(4)
+
+ const server = createServer((req, res) => {
+ res.setHeader('content-type', 'text/plain')
+ res.end('should not be called')
+ t.fail('should not be called')
+ t.end()
+ })
+ t.teardown(server.close.bind(server))
+
+ await promisify(server.listen.bind(server))(0)
+
+ const baseUrl = `http://localhost:${server.address().port}`
+
+ const mockAgent = new MockAgent()
+ setGlobalDispatcher(mockAgent)
+ t.teardown(mockAgent.close.bind(mockAgent))
+
+ const mockPoolToClose = mockAgent.get('http://localhost:9999')
+ mockPoolToClose.intercept({
+ path: '/foo',
+ method: 'GET'
+ }).reply(200, 'should-not-be-returned')
+
+ const mockPool = mockAgent.get(baseUrl)
+ mockPool.intercept({
+ path: '/foo',
+ method: 'GET'
+ }).reply(200, 'foo')
+ mockPool.intercept({
+ path: '/bar',
+ method: 'POST'
+ }).reply(200, 'bar')
+
+ await mockPoolToClose.close()
+
+ {
+ const { statusCode, body } = await request(`${baseUrl}/foo`, {
+ method: 'GET'
+ })
+ t.equal(statusCode, 200)
+
+ const response = await getResponse(body)
+ t.equal(response, 'foo')
+ }
+
+ {
+ const { statusCode, body } = await request(`${baseUrl}/bar`, {
+ method: 'POST'
+ })
+ t.equal(statusCode, 200)
+
+ const response = await getResponse(body)
+ t.equal(response, 'bar')
+ }
+})
+
+test('MockAgent - close removes all registered mock clients', async (t) => {
+ t.plan(2)
+
+ const server = createServer((req, res) => {
+ res.setHeader('content-type', 'text/plain')
+ res.end('should not be called')
+ t.fail('should not be called')
+ t.end()
+ })
+ t.teardown(server.close.bind(server))
+
+ await promisify(server.listen.bind(server))(0)
+
+ const baseUrl = `http://localhost:${server.address().port}`
+
+ const mockAgent = new MockAgent({ connections: 1 })
+ setGlobalDispatcher(mockAgent)
+
+ const mockClient = mockAgent.get(baseUrl)
+ mockClient.intercept({
+ path: '/foo',
+ method: 'GET'
+ }).reply(200, 'foo')
+
+ await mockAgent.close()
+ t.equal(mockAgent[kClients].size, 0)
+
+ try {
+ await request(`${baseUrl}/foo`, { method: 'GET' })
+ } catch (err) {
+ t.type(err, ClientDestroyedError)
+ }
+})
+
+test('MockAgent - close removes all registered mock pools', async (t) => {
+ t.plan(2)
+
+ const server = createServer((req, res) => {
+ res.setHeader('content-type', 'text/plain')
+ res.end('should not be called')
+ t.fail('should not be called')
+ t.end()
+ })
+ t.teardown(server.close.bind(server))
+
+ await promisify(server.listen.bind(server))(0)
+
+ const baseUrl = `http://localhost:${server.address().port}`
+
+ const mockAgent = new MockAgent()
+ setGlobalDispatcher(mockAgent)
+
+ const mockPool = mockAgent.get(baseUrl)
+ mockPool.intercept({
+ path: '/foo',
+ method: 'GET'
+ }).reply(200, 'foo')
+
+ await mockAgent.close()
+ t.equal(mockAgent[kClients].size, 0)
+
+ try {
+ await request(`${baseUrl}/foo`, { method: 'GET' })
+ } catch (err) {
+ t.type(err, ClientDestroyedError)
+ }
+})
+
+test('MockAgent - should handle replyWithError', async (t) => {
+ t.plan(1)
+
+ const server = createServer((req, res) => {
+ res.setHeader('content-type', 'text/plain')
+ res.end('should not be called')
+ t.fail('should not be called')
+ t.end()
+ })
+ t.teardown(server.close.bind(server))
+
+ await promisify(server.listen.bind(server))(0)
+
+ const baseUrl = `http://localhost:${server.address().port}`
+
+ const mockAgent = new MockAgent()
+ setGlobalDispatcher(mockAgent)
+ t.teardown(mockAgent.close.bind(mockAgent))
+
+ const mockPool = mockAgent.get(baseUrl)
+ mockPool.intercept({
+ path: '/foo',
+ method: 'GET'
+ }).replyWithError(new Error('kaboom'))
+
+ await t.rejects(request(`${baseUrl}/foo`, { method: 'GET' }), new Error('kaboom'))
+})
+
+test('MockAgent - should support setting a reply to respond a set amount of times', async (t) => {
+ t.plan(9)
+
+ const server = createServer((req, res) => {
+ t.equal(req.url, '/foo')
+ t.equal(req.method, 'GET')
+ res.setHeader('content-type', 'text/plain')
+ res.end('hello')
+ })
+ t.teardown(server.close.bind(server))
+
+ await promisify(server.listen.bind(server))(0)
+
+ const baseUrl = `http://localhost:${server.address().port}`
+
+ const mockAgent = new MockAgent()
+ setGlobalDispatcher(mockAgent)
+ t.teardown(mockAgent.close.bind(mockAgent))
+
+ const mockPool = mockAgent.get(baseUrl)
+ mockPool.intercept({
+ path: '/foo',
+ method: 'GET'
+ }).reply(200, 'foo').times(2)
+
+ {
+ const { statusCode, body } = await request(`${baseUrl}/foo`)
+ t.equal(statusCode, 200)
+
+ const response = await getResponse(body)
+ t.equal(response, 'foo')
+ }
+
+ {
+ const { statusCode, body } = await request(`${baseUrl}/foo`)
+ t.equal(statusCode, 200)
+
+ const response = await getResponse(body)
+ t.equal(response, 'foo')
+ }
+
+ {
+ const { statusCode, headers, body } = await request(`${baseUrl}/foo`)
+ t.equal(statusCode, 200)
+ t.equal(headers['content-type'], 'text/plain')
+
+ const response = await getResponse(body)
+ t.equal(response, 'hello')
+ }
+})
+
+test('MockAgent - persist overrides times', async (t) => {
+ t.plan(6)
+
+ const server = createServer((req, res) => {
+ res.setHeader('content-type', 'text/plain')
+ res.end('should not be called')
+ t.fail('should not be called')
+ t.end()
+ })
+ t.teardown(server.close.bind(server))
+
+ await promisify(server.listen.bind(server))(0)
+
+ const baseUrl = `http://localhost:${server.address().port}`
+
+ const mockAgent = new MockAgent()
+ setGlobalDispatcher(mockAgent)
+ t.teardown(mockAgent.close.bind(mockAgent))
+
+ const mockPool = mockAgent.get(baseUrl)
+ mockPool.intercept({
+ path: '/foo',
+ method: 'GET'
+ }).reply(200, 'foo').times(2).persist()
+
+ {
+ const { statusCode, body } = await request(`${baseUrl}/foo`, {
+ method: 'GET'
+ })
+ t.equal(statusCode, 200)
+
+ const response = await getResponse(body)
+ t.equal(response, 'foo')
+ }
+
+ {
+ const { statusCode, body } = await request(`${baseUrl}/foo`, {
+ method: 'GET'
+ })
+ t.equal(statusCode, 200)
+
+ const response = await getResponse(body)
+ t.equal(response, 'foo')
+ }
+
+ {
+ const { statusCode, body } = await request(`${baseUrl}/foo`, {
+ method: 'GET'
+ })
+ t.equal(statusCode, 200)
+
+ const response = await getResponse(body)
+ t.equal(response, 'foo')
+ }
+})
+
+test('MockAgent - matcher should not find mock dispatch if path is of unsupported type', async (t) => {
+ t.plan(4)
+
+ const server = createServer((req, res) => {
+ t.equal(req.url, '/foo')
+ t.equal(req.method, 'GET')
+ res.end('hello')
+ })
+ t.teardown(server.close.bind(server))
+
+ await promisify(server.listen.bind(server))(0)
+
+ const baseUrl = `http://localhost:${server.address().port}`
+
+ const mockAgent = new MockAgent()
+ setGlobalDispatcher(mockAgent)
+ t.teardown(mockAgent.close.bind(mockAgent))
+
+ const mockPool = mockAgent.get(baseUrl)
+ mockPool.intercept({
+ path: {},
+ method: 'GET'
+ }).reply(200, 'foo')
+
+ const { statusCode, body } = await request(`${baseUrl}/foo`, {
+ method: 'GET'
+ })
+ t.equal(statusCode, 200)
+
+ const response = await getResponse(body)
+ t.equal(response, 'hello')
+})
+
+test('MockAgent - should match path with regex', async (t) => {
+ t.plan(4)
+
+ const server = createServer((req, res) => {
+ res.setHeader('content-type', 'text/plain')
+ res.end('should not be called')
+ t.fail('should not be called')
+ t.end()
+ })
+ t.teardown(server.close.bind(server))
+
+ await promisify(server.listen.bind(server))(0)
+
+ const baseUrl = `http://localhost:${server.address().port}`
+
+ const mockAgent = new MockAgent()
+ setGlobalDispatcher(mockAgent)
+ t.teardown(mockAgent.close.bind(mockAgent))
+
+ const mockPool = mockAgent.get(baseUrl)
+ mockPool.intercept({
+ path: /foo/,
+ method: 'GET'
+ }).reply(200, 'foo').persist()
+
+ {
+ const { statusCode, body } = await request(`${baseUrl}/foo`, {
+ method: 'GET'
+ })
+ t.equal(statusCode, 200)
+
+ const response = await getResponse(body)
+ t.equal(response, 'foo')
+ }
+
+ {
+ const { statusCode, body } = await request(`${baseUrl}/hello/foobar`, {
+ method: 'GET'
+ })
+ t.equal(statusCode, 200)
+
+ const response = await getResponse(body)
+ t.equal(response, 'foo')
+ }
+})
+
+test('MockAgent - should match path with function', async (t) => {
+ t.plan(2)
+
+ const server = createServer((req, res) => {
+ res.setHeader('content-type', 'text/plain')
+ res.end('should not be called')
+ t.fail('should not be called')
+ t.end()
+ })
+ t.teardown(server.close.bind(server))
+
+ await promisify(server.listen.bind(server))(0)
+
+ const baseUrl = `http://localhost:${server.address().port}`
+
+ const mockAgent = new MockAgent()
+ setGlobalDispatcher(mockAgent)
+ t.teardown(mockAgent.close.bind(mockAgent))
+
+ const mockPool = mockAgent.get(baseUrl)
+ mockPool.intercept({
+ path: (value) => value === '/foo',
+ method: 'GET'
+ }).reply(200, 'foo')
+
+ const { statusCode, body } = await request(`${baseUrl}/foo`, {
+ method: 'GET'
+ })
+ t.equal(statusCode, 200)
+
+ const response = await getResponse(body)
+ t.equal(response, 'foo')
+})
+
+test('MockAgent - should match method with regex', async (t) => {
+ t.plan(2)
+
+ const server = createServer((req, res) => {
+ res.setHeader('content-type', 'text/plain')
+ res.end('should not be called')
+ t.fail('should not be called')
+ t.end()
+ })
+ t.teardown(server.close.bind(server))
+
+ await promisify(server.listen.bind(server))(0)
+
+ const baseUrl = `http://localhost:${server.address().port}`
+
+ const mockAgent = new MockAgent()
+ setGlobalDispatcher(mockAgent)
+ t.teardown(mockAgent.close.bind(mockAgent))
+
+ const mockPool = mockAgent.get(baseUrl)
+ mockPool.intercept({
+ path: '/foo',
+ method: /^GET$/
+ }).reply(200, 'foo')
+
+ const { statusCode, body } = await request(`${baseUrl}/foo`, {
+ method: 'GET'
+ })
+ t.equal(statusCode, 200)
+
+ const response = await getResponse(body)
+ t.equal(response, 'foo')
+})
+
+test('MockAgent - should match method with function', async (t) => {
+ t.plan(2)
+
+ const server = createServer((req, res) => {
+ res.setHeader('content-type', 'text/plain')
+ res.end('should not be called')
+ t.fail('should not be called')
+ t.end()
+ })
+ t.teardown(server.close.bind(server))
+
+ await promisify(server.listen.bind(server))(0)
+
+ const baseUrl = `http://localhost:${server.address().port}`
+
+ const mockAgent = new MockAgent()
+ setGlobalDispatcher(mockAgent)
+ t.teardown(mockAgent.close.bind(mockAgent))
+
+ const mockPool = mockAgent.get(baseUrl)
+ mockPool.intercept({
+ path: '/foo',
+ method: (value) => value === 'GET'
+ }).reply(200, 'foo')
+
+ const { statusCode, body } = await request(`${baseUrl}/foo`, {
+ method: 'GET'
+ })
+ t.equal(statusCode, 200)
+
+ const response = await getResponse(body)
+ t.equal(response, 'foo')
+})
+
+test('MockAgent - should match body with regex', async (t) => {
+ t.plan(2)
+
+ const server = createServer((req, res) => {
+ res.setHeader('content-type', 'text/plain')
+ res.end('should not be called')
+ t.fail('should not be called')
+ t.end()
+ })
+ t.teardown(server.close.bind(server))
+
+ await promisify(server.listen.bind(server))(0)
+
+ const baseUrl = `http://localhost:${server.address().port}`
+
+ const mockAgent = new MockAgent()
+ setGlobalDispatcher(mockAgent)
+ t.teardown(mockAgent.close.bind(mockAgent))
+
+ const mockPool = mockAgent.get(baseUrl)
+ mockPool.intercept({
+ path: '/foo',
+ method: 'GET',
+ body: /hello/
+ }).reply(200, 'foo')
+
+ const { statusCode, body } = await request(`${baseUrl}/foo`, {
+ method: 'GET',
+ body: 'hello=there'
+ })
+ t.equal(statusCode, 200)
+
+ const response = await getResponse(body)
+ t.equal(response, 'foo')
+})
+
+test('MockAgent - should match body with function', async (t) => {
+ t.plan(2)
+
+ const server = createServer((req, res) => {
+ res.setHeader('content-type', 'text/plain')
+ res.end('should not be called')
+ t.fail('should not be called')
+ t.end()
+ })
+ t.teardown(server.close.bind(server))
+
+ await promisify(server.listen.bind(server))(0)
+
+ const baseUrl = `http://localhost:${server.address().port}`
+
+ const mockAgent = new MockAgent()
+ setGlobalDispatcher(mockAgent)
+ t.teardown(mockAgent.close.bind(mockAgent))
+
+ const mockPool = mockAgent.get(baseUrl)
+ mockPool.intercept({
+ path: '/foo',
+ method: 'GET',
+ body: (value) => value.startsWith('hello')
+ }).reply(200, 'foo')
+
+ const { statusCode, body } = await request(`${baseUrl}/foo`, {
+ method: 'GET',
+ body: 'hello=there'
+ })
+ t.equal(statusCode, 200)
+
+ const response = await getResponse(body)
+ t.equal(response, 'foo')
+})
+
+test('MockAgent - should match headers with string', async (t) => {
+ t.plan(6)
+
+ const server = createServer((req, res) => {
+ res.end('should not be called')
+ t.fail('should not be called')
+ t.end()
+ })
+ t.teardown(server.close.bind(server))
+
+ await promisify(server.listen.bind(server))(0)
+
+ const baseUrl = `http://localhost:${server.address().port}`
+
+ const mockAgent = new MockAgent()
+ setGlobalDispatcher(mockAgent)
+ t.teardown(mockAgent.close.bind(mockAgent))
+
+ const mockPool = mockAgent.get(baseUrl)
+ mockPool.intercept({
+ path: '/foo',
+ method: 'GET',
+ headers: {
+ 'User-Agent': 'undici',
+ Host: 'example.com'
+ }
+ }).reply(200, 'foo')
+
+ // Disable net connect so we can make sure it matches properly
+ mockAgent.disableNetConnect()
+
+ await t.rejects(request(`${baseUrl}/foo`, {
+ method: 'GET'
+ }), MockNotMatchedError, 'should reject with MockNotMatchedError')
+
+ await t.rejects(request(`${baseUrl}/foo`, {
+ method: 'GET',
+ headers: {
+ foo: 'bar'
+ }
+ }), MockNotMatchedError, 'should reject with MockNotMatchedError')
+
+ await t.rejects(request(`${baseUrl}/foo`, {
+ method: 'GET',
+ headers: {
+ foo: 'bar',
+ 'User-Agent': 'undici'
+ }
+ }), MockNotMatchedError, 'should reject with MockNotMatchedError')
+
+ await t.rejects(request(`${baseUrl}/foo`, {
+ method: 'GET',
+ headers: {
+ foo: 'bar',
+ 'User-Agent': 'undici',
+ Host: 'wrong'
+ }
+ }), MockNotMatchedError, 'should reject with MockNotMatchedError')
+
+ const { statusCode, body } = await request(`${baseUrl}/foo`, {
+ method: 'GET',
+ headers: {
+ foo: 'bar',
+ 'User-Agent': 'undici',
+ Host: 'example.com'
+ }
+ })
+ t.equal(statusCode, 200)
+
+ const response = await getResponse(body)
+ t.equal(response, 'foo')
+})
+
+test('MockAgent - should match headers with regex', async (t) => {
+ t.plan(6)
+
+ const server = createServer((req, res) => {
+ res.end('should not be called')
+ t.fail('should not be called')
+ t.end()
+ })
+ t.teardown(server.close.bind(server))
+
+ await promisify(server.listen.bind(server))(0)
+
+ const baseUrl = `http://localhost:${server.address().port}`
+
+ const mockAgent = new MockAgent()
+ setGlobalDispatcher(mockAgent)
+ t.teardown(mockAgent.close.bind(mockAgent))
+
+ const mockPool = mockAgent.get(baseUrl)
+ mockPool.intercept({
+ path: '/foo',
+ method: 'GET',
+ headers: {
+ 'User-Agent': /^undici$/,
+ Host: /^example.com$/
+ }
+ }).reply(200, 'foo')
+
+ // Disable net connect so we can make sure it matches properly
+ mockAgent.disableNetConnect()
+
+ await t.rejects(request(`${baseUrl}/foo`, {
+ method: 'GET'
+ }), MockNotMatchedError, 'should reject with MockNotMatchedError')
+
+ await t.rejects(request(`${baseUrl}/foo`, {
+ method: 'GET',
+ headers: {
+ foo: 'bar'
+ }
+ }), MockNotMatchedError, 'should reject with MockNotMatchedError')
+
+ await t.rejects(request(`${baseUrl}/foo`, {
+ method: 'GET',
+ headers: {
+ foo: 'bar',
+ 'User-Agent': 'undici'
+ }
+ }), MockNotMatchedError, 'should reject with MockNotMatchedError')
+
+ await t.rejects(request(`${baseUrl}/foo`, {
+ method: 'GET',
+ headers: {
+ foo: 'bar',
+ 'User-Agent': 'undici',
+ Host: 'wrong'
+ }
+ }), MockNotMatchedError, 'should reject with MockNotMatchedError')
+
+ const { statusCode, body } = await request(`${baseUrl}/foo`, {
+ method: 'GET',
+ headers: {
+ foo: 'bar',
+ 'User-Agent': 'undici',
+ Host: 'example.com'
+ }
+ })
+ t.equal(statusCode, 200)
+
+ const response = await getResponse(body)
+ t.equal(response, 'foo')
+})
+
+test('MockAgent - should match headers with function', async (t) => {
+ t.plan(6)
+
+ const server = createServer((req, res) => {
+ res.end('should not be called')
+ t.fail('should not be called')
+ t.end()
+ })
+ t.teardown(server.close.bind(server))
+
+ await promisify(server.listen.bind(server))(0)
+
+ const baseUrl = `http://localhost:${server.address().port}`
+
+ const mockAgent = new MockAgent()
+ setGlobalDispatcher(mockAgent)
+ t.teardown(mockAgent.close.bind(mockAgent))
+
+ const mockPool = mockAgent.get(baseUrl)
+ mockPool.intercept({
+ path: '/foo',
+ method: 'GET',
+ headers: {
+ 'User-Agent': (value) => value === 'undici',
+ Host: (value) => value === 'example.com'
+ }
+ }).reply(200, 'foo')
+
+ // Disable net connect so we can make sure it matches properly
+ mockAgent.disableNetConnect()
+
+ await t.rejects(request(`${baseUrl}/foo`, {
+ method: 'GET'
+ }), MockNotMatchedError, 'should reject with MockNotMatchedError')
+
+ await t.rejects(request(`${baseUrl}/foo`, {
+ method: 'GET',
+ headers: {
+ foo: 'bar'
+ }
+ }), MockNotMatchedError, 'should reject with MockNotMatchedError')
+
+ await t.rejects(request(`${baseUrl}/foo`, {
+ method: 'GET',
+ headers: {
+ foo: 'bar',
+ 'User-Agent': 'undici'
+ }
+ }), MockNotMatchedError, 'should reject with MockNotMatchedError')
+
+ await t.rejects(request(`${baseUrl}/foo`, {
+ method: 'GET',
+ headers: {
+ foo: 'bar',
+ 'User-Agent': 'undici',
+ Host: 'wrong'
+ }
+ }), MockNotMatchedError, 'should reject with MockNotMatchedError')
+
+ const { statusCode, body } = await request(`${baseUrl}/foo`, {
+ method: 'GET',
+ headers: {
+ foo: 'bar',
+ 'User-Agent': 'undici',
+ Host: 'example.com'
+ }
+ })
+ t.equal(statusCode, 200)
+
+ const response = await getResponse(body)
+ t.equal(response, 'foo')
+})
+
+test('MockAgent - should match url with regex', async (t) => {
+ t.plan(2)
+
+ const server = createServer((req, res) => {
+ res.setHeader('content-type', 'text/plain')
+ res.end('should not be called')
+ t.fail('should not be called')
+ t.end()
+ })
+ t.teardown(server.close.bind(server))
+
+ await promisify(server.listen.bind(server))(0)
+
+ const baseUrl = `http://localhost:${server.address().port}`
+
+ const mockAgent = new MockAgent()
+ setGlobalDispatcher(mockAgent)
+ t.teardown(mockAgent.close.bind(mockAgent))
+
+ const mockPool = mockAgent.get(new RegExp(baseUrl))
+ mockPool.intercept({
+ path: '/foo',
+ method: 'GET'
+ }).reply(200, 'foo')
+
+ const { statusCode, body } = await request(`${baseUrl}/foo`, {
+ method: 'GET'
+ })
+ t.equal(statusCode, 200)
+
+ const response = await getResponse(body)
+ t.equal(response, 'foo')
+})
+
+test('MockAgent - should match url with function', async (t) => {
+ t.plan(2)
+
+ const server = createServer((req, res) => {
+ res.setHeader('content-type', 'text/plain')
+ res.end('should not be called')
+ t.fail('should not be called')
+ t.end()
+ })
+ t.teardown(server.close.bind(server))
+
+ await promisify(server.listen.bind(server))(0)
+
+ const baseUrl = `http://localhost:${server.address().port}`
+
+ const mockAgent = new MockAgent()
+ setGlobalDispatcher(mockAgent)
+ t.teardown(mockAgent.close.bind(mockAgent))
+
+ const mockPool = mockAgent.get((value) => baseUrl === value)
+ mockPool.intercept({
+ path: '/foo',
+ method: 'GET'
+ }).reply(200, 'foo')
+
+ const { statusCode, body } = await request(`${baseUrl}/foo`, {
+ method: 'GET'
+ })
+ t.equal(statusCode, 200)
+
+ const response = await getResponse(body)
+ t.equal(response, 'foo')
+})
+
+test('MockAgent - handle default reply headers', async (t) => {
+ t.plan(3)
+
+ const server = createServer((req, res) => {
+ res.setHeader('content-type', 'text/plain')
+ res.end('should not be called')
+ t.fail('should not be called')
+ t.end()
+ })
+ t.teardown(server.close.bind(server))
+
+ await promisify(server.listen.bind(server))(0)
+
+ const baseUrl = `http://localhost:${server.address().port}`
+
+ const mockAgent = new MockAgent()
+ setGlobalDispatcher(mockAgent)
+ t.teardown(mockAgent.close.bind(mockAgent))
+
+ const mockPool = mockAgent.get(baseUrl)
+ mockPool.intercept({
+ path: '/foo',
+ method: 'GET'
+ }).defaultReplyHeaders({ foo: 'bar' }).reply(200, 'foo', { headers: { hello: 'there' } })
+
+ const { statusCode, headers, body } = await request(`${baseUrl}/foo`, {
+ method: 'GET'
+ })
+ t.equal(statusCode, 200)
+ t.same(headers, {
+ foo: 'bar',
+ hello: 'there'
+ })
+
+ const response = await getResponse(body)
+ t.equal(response, 'foo')
+})
+
+test('MockAgent - handle default reply trailers', async (t) => {
+ t.plan(3)
+
+ const server = createServer((req, res) => {
+ res.setHeader('content-type', 'text/plain')
+ res.end('should not be called')
+ t.fail('should not be called')
+ t.end()
+ })
+ t.teardown(server.close.bind(server))
+
+ await promisify(server.listen.bind(server))(0)
+
+ const baseUrl = `http://localhost:${server.address().port}`
+
+ const mockAgent = new MockAgent()
+ setGlobalDispatcher(mockAgent)
+ t.teardown(mockAgent.close.bind(mockAgent))
+
+ const mockPool = mockAgent.get(baseUrl)
+ mockPool.intercept({
+ path: '/foo',
+ method: 'GET'
+ }).defaultReplyTrailers({ foo: 'bar' }).reply(200, 'foo', { trailers: { hello: 'there' } })
+
+ const { statusCode, trailers, body } = await request(`${baseUrl}/foo`, {
+ method: 'GET'
+ })
+ t.equal(statusCode, 200)
+ t.same(trailers, {
+ foo: 'bar',
+ hello: 'there'
+ })
+
+ const response = await getResponse(body)
+ t.equal(response, 'foo')
+})
+
+test('MockAgent - return calculated content-length if specified', async (t) => {
+ t.plan(3)
+
+ const server = createServer((req, res) => {
+ res.setHeader('content-type', 'text/plain')
+ res.end('should not be called')
+ t.fail('should not be called')
+ t.end()
+ })
+ t.teardown(server.close.bind(server))
+
+ await promisify(server.listen.bind(server))(0)
+
+ const baseUrl = `http://localhost:${server.address().port}`
+
+ const mockAgent = new MockAgent()
+ setGlobalDispatcher(mockAgent)
+ t.teardown(mockAgent.close.bind(mockAgent))
+
+ const mockPool = mockAgent.get(baseUrl)
+ mockPool.intercept({
+ path: '/foo',
+ method: 'GET'
+ }).replyContentLength().reply(200, 'foo', { headers: { hello: 'there' } })
+
+ const { statusCode, headers, body } = await request(`${baseUrl}/foo`, {
+ method: 'GET'
+ })
+ t.equal(statusCode, 200)
+ t.same(headers, {
+ hello: 'there',
+ 'content-length': 3
+ })
+
+ const response = await getResponse(body)
+ t.equal(response, 'foo')
+})
+
+test('MockAgent - return calculated content-length for object response if specified', async (t) => {
+ t.plan(3)
+
+ const server = createServer((req, res) => {
+ res.setHeader('content-type', 'text/plain')
+ res.end('should not be called')
+ t.fail('should not be called')
+ t.end()
+ })
+ t.teardown(server.close.bind(server))
+
+ await promisify(server.listen.bind(server))(0)
+
+ const baseUrl = `http://localhost:${server.address().port}`
+
+ const mockAgent = new MockAgent()
+ setGlobalDispatcher(mockAgent)
+ t.teardown(mockAgent.close.bind(mockAgent))
+
+ const mockPool = mockAgent.get(baseUrl)
+ mockPool.intercept({
+ path: '/foo',
+ method: 'GET'
+ }).replyContentLength().reply(200, { foo: 'bar' }, { headers: { hello: 'there' } })
+
+ const { statusCode, headers, body } = await request(`${baseUrl}/foo`, {
+ method: 'GET'
+ })
+ t.equal(statusCode, 200)
+ t.same(headers, {
+ hello: 'there',
+ 'content-length': 13
+ })
+
+ const jsonResponse = JSON.parse(await getResponse(body))
+ t.same(jsonResponse, { foo: 'bar' })
+})
+
+test('MockAgent - should activate and deactivate mock clients', async (t) => {
+ t.plan(9)
+
+ const server = createServer((req, res) => {
+ t.equal(req.url, '/foo')
+ t.equal(req.method, 'GET')
+ res.setHeader('content-type', 'text/plain')
+ res.end('hello')
+ })
+ t.teardown(server.close.bind(server))
+
+ await promisify(server.listen.bind(server))(0)
+
+ const baseUrl = `http://localhost:${server.address().port}`
+
+ const mockAgent = new MockAgent()
+ setGlobalDispatcher(mockAgent)
+ t.teardown(mockAgent.close.bind(mockAgent))
+
+ const mockPool = mockAgent.get(baseUrl)
+ mockPool.intercept({
+ path: '/foo',
+ method: 'GET'
+ }).reply(200, 'foo').persist()
+
+ {
+ const { statusCode, body } = await request(`${baseUrl}/foo`, {
+ method: 'GET'
+ })
+ t.equal(statusCode, 200)
+
+ const response = await getResponse(body)
+ t.equal(response, 'foo')
+ }
+
+ mockAgent.deactivate()
+
+ {
+ const { statusCode, headers, body } = await request(`${baseUrl}/foo`, {
+ method: 'GET'
+ })
+ t.equal(statusCode, 200)
+ t.equal(headers['content-type'], 'text/plain')
+
+ const response = await getResponse(body)
+ t.equal(response, 'hello')
+ }
+
+ mockAgent.activate()
+
+ {
+ const { statusCode, body } = await request(`${baseUrl}/foo`, {
+ method: 'GET'
+ })
+ t.equal(statusCode, 200)
+
+ const response = await getResponse(body)
+ t.equal(response, 'foo')
+ }
+})
+
+test('MockAgent - enableNetConnect should allow all original dispatches to be called if dispatch not found', async (t) => {
+ t.plan(5)
+
+ const server = createServer((req, res) => {
+ t.equal(req.url, '/foo')
+ t.equal(req.method, 'GET')
+ res.setHeader('content-type', 'text/plain')
+ res.end('hello')
+ })
+ t.teardown(server.close.bind(server))
+
+ await promisify(server.listen.bind(server))(0)
+
+ const baseUrl = `http://localhost:${server.address().port}`
+
+ const mockAgent = new MockAgent()
+ setGlobalDispatcher(mockAgent)
+ t.teardown(mockAgent.close.bind(mockAgent))
+
+ const mockPool = mockAgent.get(baseUrl)
+ mockPool.intercept({
+ path: '/wrong',
+ method: 'GET'
+ }).reply(200, 'foo')
+
+ mockAgent.enableNetConnect()
+
+ const { statusCode, headers, body } = await request(`${baseUrl}/foo`, {
+ method: 'GET'
+ })
+ t.equal(statusCode, 200)
+ t.equal(headers['content-type'], 'text/plain')
+
+ const response = await getResponse(body)
+ t.equal(response, 'hello')
+})
+
+test('MockAgent - enableNetConnect with a host string should allow all original dispatches to be called if mockDispatch not found', async (t) => {
+ t.plan(5)
+
+ const server = createServer((req, res) => {
+ t.equal(req.url, '/foo')
+ t.equal(req.method, 'GET')
+ res.setHeader('content-type', 'text/plain')
+ res.end('hello')
+ })
+ t.teardown(server.close.bind(server))
+
+ await promisify(server.listen.bind(server))(0)
+
+ const baseUrl = `http://localhost:${server.address().port}`
+
+ const mockAgent = new MockAgent()
+ setGlobalDispatcher(mockAgent)
+ t.teardown(mockAgent.close.bind(mockAgent))
+
+ const mockPool = mockAgent.get(baseUrl)
+ mockPool.intercept({
+ path: '/wrong',
+ method: 'GET'
+ }).reply(200, 'foo')
+
+ mockAgent.enableNetConnect(`localhost:${server.address().port}`)
+
+ const { statusCode, headers, body } = await request(`${baseUrl}/foo`, {
+ method: 'GET'
+ })
+ t.equal(statusCode, 200)
+ t.equal(headers['content-type'], 'text/plain')
+
+ const response = await getResponse(body)
+ t.equal(response, 'hello')
+})
+
+test('MockAgent - enableNetConnect when called with host string multiple times should allow all original dispatches to be called if mockDispatch not found', async (t) => {
+ t.plan(5)
+
+ const server = createServer((req, res) => {
+ t.equal(req.url, '/foo')
+ t.equal(req.method, 'GET')
+ res.setHeader('content-type', 'text/plain')
+ res.end('hello')
+ })
+ t.teardown(server.close.bind(server))
+
+ await promisify(server.listen.bind(server))(0)
+
+ const baseUrl = `http://localhost:${server.address().port}`
+
+ const mockAgent = new MockAgent()
+ setGlobalDispatcher(mockAgent)
+ t.teardown(mockAgent.close.bind(mockAgent))
+
+ const mockPool = mockAgent.get(baseUrl)
+ mockPool.intercept({
+ path: '/wrong',
+ method: 'GET'
+ }).reply(200, 'foo')
+
+ mockAgent.enableNetConnect('example.com:9999')
+ mockAgent.enableNetConnect(`localhost:${server.address().port}`)
+
+ const { statusCode, headers, body } = await request(`${baseUrl}/foo`, {
+ method: 'GET'
+ })
+ t.equal(statusCode, 200)
+ t.equal(headers['content-type'], 'text/plain')
+
+ const response = await getResponse(body)
+ t.equal(response, 'hello')
+})
+
+test('MockAgent - enableNetConnect with a host regex should allow all original dispatches to be called if mockDispatch not found', async (t) => {
+ t.plan(5)
+
+ const server = createServer((req, res) => {
+ t.equal(req.url, '/foo')
+ t.equal(req.method, 'GET')
+ res.setHeader('content-type', 'text/plain')
+ res.end('hello')
+ })
+ t.teardown(server.close.bind(server))
+
+ await promisify(server.listen.bind(server))(0)
+
+ const baseUrl = `http://localhost:${server.address().port}`
+
+ const mockAgent = new MockAgent()
+ setGlobalDispatcher(mockAgent)
+ t.teardown(mockAgent.close.bind(mockAgent))
+
+ const mockPool = mockAgent.get(baseUrl)
+ mockPool.intercept({
+ path: '/wrong',
+ method: 'GET'
+ }).reply(200, 'foo')
+
+ mockAgent.enableNetConnect(new RegExp(`localhost:${server.address().port}`))
+
+ const { statusCode, headers, body } = await request(`${baseUrl}/foo`, {
+ method: 'GET'
+ })
+ t.equal(statusCode, 200)
+ t.equal(headers['content-type'], 'text/plain')
+
+ const response = await getResponse(body)
+ t.equal(response, 'hello')
+})
+
+test('MockAgent - enableNetConnect with a function should allow all original dispatches to be called if mockDispatch not found', async (t) => {
+ t.plan(5)
+
+ const server = createServer((req, res) => {
+ t.equal(req.url, '/foo')
+ t.equal(req.method, 'GET')
+ res.setHeader('content-type', 'text/plain')
+ res.end('hello')
+ })
+ t.teardown(server.close.bind(server))
+
+ await promisify(server.listen.bind(server))(0)
+
+ const baseUrl = `http://localhost:${server.address().port}`
+
+ const mockAgent = new MockAgent()
+ setGlobalDispatcher(mockAgent)
+ t.teardown(mockAgent.close.bind(mockAgent))
+
+ const mockPool = mockAgent.get(baseUrl)
+ mockPool.intercept({
+ path: '/wrong',
+ method: 'GET'
+ }).reply(200, 'foo')
+
+ mockAgent.enableNetConnect((value) => value === `localhost:${server.address().port}`)
+
+ const { statusCode, headers, body } = await request(`${baseUrl}/foo`, {
+ method: 'GET'
+ })
+ t.equal(statusCode, 200)
+ t.equal(headers['content-type'], 'text/plain')
+
+ const response = await getResponse(body)
+ t.equal(response, 'hello')
+})
+
+test('MockAgent - enableNetConnect with an unknown input should throw', async (t) => {
+ t.plan(1)
+
+ const mockAgent = new MockAgent()
+ setGlobalDispatcher(mockAgent)
+ t.teardown(mockAgent.close.bind(mockAgent))
+
+ const mockPool = mockAgent.get('http://localhost:9999')
+ mockPool.intercept({
+ path: '/wrong',
+ method: 'GET'
+ }).reply(200, 'foo')
+
+ t.throws(() => mockAgent.enableNetConnect({}), new InvalidArgumentError('Unsupported matcher. Must be one of String|Function|RegExp.'))
+})
+
+test('MockAgent - enableNetConnect should throw if dispatch not matched for path and the origin was not allowed by net connect', async (t) => {
+ t.plan(1)
+
+ const server = createServer((req, res) => {
+ t.fail('should not be called')
+ t.end()
+ res.end('should not be called')
+ })
+ t.teardown(server.close.bind(server))
+
+ await promisify(server.listen.bind(server))(0)
+
+ const baseUrl = `http://localhost:${server.address().port}`
+
+ const mockAgent = new MockAgent()
+ setGlobalDispatcher(mockAgent)
+ t.teardown(mockAgent.close.bind(mockAgent))
+
+ const mockPool = mockAgent.get(baseUrl)
+ mockPool.intercept({
+ path: '/foo',
+ method: 'GET'
+ }).reply(200, 'foo')
+
+ mockAgent.enableNetConnect('example.com:9999')
+
+ await t.rejects(request(`${baseUrl}/wrong`, {
+ method: 'GET'
+ }), new MockNotMatchedError(`Mock dispatch not matched for path '/wrong': subsequent request to origin ${baseUrl} was not allowed (net.connect is not enabled for this origin)`))
+})
+
+test('MockAgent - enableNetConnect should throw if dispatch not matched for method and the origin was not allowed by net connect', async (t) => {
+ t.plan(1)
+
+ const server = createServer((req, res) => {
+ t.fail('should not be called')
+ t.end()
+ res.end('should not be called')
+ })
+ t.teardown(server.close.bind(server))
+
+ await promisify(server.listen.bind(server))(0)
+
+ const baseUrl = `http://localhost:${server.address().port}`
+
+ const mockAgent = new MockAgent()
+ setGlobalDispatcher(mockAgent)
+ t.teardown(mockAgent.close.bind(mockAgent))
+
+ const mockPool = mockAgent.get(baseUrl)
+ mockPool.intercept({
+ path: '/foo',
+ method: 'GET'
+ }).reply(200, 'foo')
+
+ mockAgent.enableNetConnect('example.com:9999')
+
+ await t.rejects(request(`${baseUrl}/foo`, {
+ method: 'WRONG'
+ }), new MockNotMatchedError(`Mock dispatch not matched for method 'WRONG': subsequent request to origin ${baseUrl} was not allowed (net.connect is not enabled for this origin)`))
+})
+
+test('MockAgent - enableNetConnect should throw if dispatch not matched for body and the origin was not allowed by net connect', async (t) => {
+ t.plan(1)
+
+ const server = createServer((req, res) => {
+ t.fail('should not be called')
+ t.end()
+ res.end('should not be called')
+ })
+ t.teardown(server.close.bind(server))
+
+ await promisify(server.listen.bind(server))(0)
+
+ const baseUrl = `http://localhost:${server.address().port}`
+
+ const mockAgent = new MockAgent()
+ setGlobalDispatcher(mockAgent)
+ t.teardown(mockAgent.close.bind(mockAgent))
+
+ const mockPool = mockAgent.get(baseUrl)
+ mockPool.intercept({
+ path: '/foo',
+ method: 'GET',
+ body: 'hello'
+ }).reply(200, 'foo')
+
+ mockAgent.enableNetConnect('example.com:9999')
+
+ await t.rejects(request(`${baseUrl}/foo`, {
+ method: 'GET',
+ body: 'wrong'
+ }), new MockNotMatchedError(`Mock dispatch not matched for body 'wrong': subsequent request to origin ${baseUrl} was not allowed (net.connect is not enabled for this origin)`))
+})
+
+test('MockAgent - enableNetConnect should throw if dispatch not matched for headers and the origin was not allowed by net connect', async (t) => {
+ t.plan(1)
+
+ const server = createServer((req, res) => {
+ t.fail('should not be called')
+ t.end()
+ res.end('should not be called')
+ })
+ t.teardown(server.close.bind(server))
+
+ await promisify(server.listen.bind(server))(0)
+
+ const baseUrl = `http://localhost:${server.address().port}`
+
+ const mockAgent = new MockAgent()
+ setGlobalDispatcher(mockAgent)
+ t.teardown(mockAgent.close.bind(mockAgent))
+
+ const mockPool = mockAgent.get(baseUrl)
+ mockPool.intercept({
+ path: '/foo',
+ method: 'GET',
+ headers: {
+ 'User-Agent': 'undici'
+ }
+ }).reply(200, 'foo')
+
+ mockAgent.enableNetConnect('example.com:9999')
+
+ await t.rejects(request(`${baseUrl}/foo`, {
+ method: 'GET',
+ headers: {
+ 'User-Agent': 'wrong'
+ }
+ }), new MockNotMatchedError(`Mock dispatch not matched for headers '{"User-Agent":"wrong"}': subsequent request to origin ${baseUrl} was not allowed (net.connect is not enabled for this origin)`))
+})
+
+test('MockAgent - disableNetConnect should throw if dispatch not found by net connect', async (t) => {
+ t.plan(1)
+
+ const server = createServer((req, res) => {
+ t.equal(req.url, '/foo')
+ t.equal(req.method, 'GET')
+ res.setHeader('content-type', 'text/plain')
+ res.end('hello')
+ })
+ t.teardown(server.close.bind(server))
+
+ await promisify(server.listen.bind(server))(0)
+
+ const baseUrl = `http://localhost:${server.address().port}`
+
+ const mockAgent = new MockAgent()
+ setGlobalDispatcher(mockAgent)
+ t.teardown(mockAgent.close.bind(mockAgent))
+
+ const mockPool = mockAgent.get(baseUrl)
+ mockPool.intercept({
+ path: '/wrong',
+ method: 'GET'
+ }).reply(200, 'foo')
+
+ mockAgent.disableNetConnect()
+
+ await t.rejects(request(`${baseUrl}/foo`, {
+ method: 'GET'
+ }), new MockNotMatchedError(`Mock dispatch not matched for path '/foo': subsequent request to origin ${baseUrl} was not allowed (net.connect disabled)`))
+})
+
+test('MockAgent - headers function interceptor', async (t) => {
+ t.plan(7)
+
+ const server = createServer((req, res) => {
+ t.fail('should not be called')
+ t.end()
+ res.end('should not be called')
+ })
+ t.teardown(server.close.bind(server))
+
+ await promisify(server.listen.bind(server))(0)
+
+ const baseUrl = `http://localhost:${server.address().port}`
+
+ const mockAgent = new MockAgent()
+ setGlobalDispatcher(mockAgent)
+ t.teardown(mockAgent.close.bind(mockAgent))
+ const mockPool = mockAgent.get(baseUrl)
+
+ // Disable net connect so we can make sure it matches properly
+ mockAgent.disableNetConnect()
+
+ mockPool.intercept({
+ path: '/foo',
+ method: 'GET',
+ headers (headers) {
+ t.equal(typeof headers, 'object')
+ return !Object.keys(headers).includes('authorization')
+ }
+ }).reply(200, 'foo').times(2)
+
+ await t.rejects(request(`${baseUrl}/foo`, {
+ method: 'GET',
+ headers: {
+ Authorization: 'Bearer foo'
+ }
+ }), new MockNotMatchedError(`Mock dispatch not matched for headers '{"Authorization":"Bearer foo"}': subsequent request to origin ${baseUrl} was not allowed (net.connect disabled)`))
+
+ {
+ const { statusCode } = await request(`${baseUrl}/foo`, {
+ method: 'GET',
+ headers: {
+ foo: 'bar'
+ }
+ })
+ t.equal(statusCode, 200)
+ }
+
+ {
+ const { statusCode } = await request(`${baseUrl}/foo`, {
+ method: 'GET'
+ })
+ t.equal(statusCode, 200)
+ }
+})
+
+test('MockAgent - clients are not garbage collected', async (t) => {
+ const samples = 250
+ t.plan(2)
+
+ const server = createServer((req, res) => {
+ t.fail('should not be called')
+ t.end()
+ res.end('should not be called')
+ })
+ t.teardown(server.close.bind(server))
+
+ await promisify(server.listen.bind(server))(0)
+
+ const baseUrl = `http://localhost:${server.address().port}`
+
+ // Create the dispatcher and isable net connect so we can make sure it matches properly
+ const dispatcher = new MockAgent()
+ dispatcher.disableNetConnect()
+
+ // When Node 16 is the minimum supported, this can be replaced by simply requiring setTimeout from timers/promises
+ function sleep (delay) {
+ return new Promise(resolve => {
+ setTimeout(resolve, delay)
+ })
+ }
+
+ // Purposely create the pool inside a function so that the reference is lost
+ function intercept () {
+ // Create the pool and add a lot of intercepts
+ const pool = dispatcher.get(baseUrl)
+
+ for (let i = 0; i < samples; i++) {
+ pool.intercept({
+ path: `/foo/${i}`,
+ method: 'GET'
+ }).reply(200, Buffer.alloc(1024 * 1024))
+ }
+ }
+
+ intercept()
+
+ const results = new Set()
+ for (let i = 0; i < samples; i++) {
+ // Let's make some time pass to allow garbage collection to happen
+ await sleep(10)
+
+ const { statusCode } = await request(`${baseUrl}/foo/${i}`, { method: 'GET', dispatcher })
+ results.add(statusCode)
+ }
+
+ t.equal(results.size, 1)
+ t.ok(results.has(200))
+})
+
+// https://github.com/nodejs/undici/issues/1321
+test('MockAgent - using fetch yields correct statusText', { skip: nodeMajor < 16 }, async (t) => {
+ const { fetch } = require('..')
+
+ const mockAgent = new MockAgent()
+ mockAgent.disableNetConnect()
+ setGlobalDispatcher(mockAgent)
+ t.teardown(mockAgent.close.bind(mockAgent))
+
+ const mockPool = mockAgent.get('http://localhost:3000')
+
+ mockPool.intercept({
+ path: '/statusText',
+ method: 'GET'
+ }).reply(200, 'Body')
+
+ const { status, statusText } = await fetch('http://localhost:3000/statusText')
+
+ t.equal(status, 200)
+ t.equal(statusText, 'OK')
+
+ mockPool.intercept({
+ path: '/unknownStatusText',
+ method: 'GET'
+ }).reply(420, 'Everyday')
+
+ const unknownStatusCodeRes = await fetch('http://localhost:3000/unknownStatusText')
+ t.equal(unknownStatusCodeRes.status, 420)
+ t.equal(unknownStatusCodeRes.statusText, 'unknown')
+
+ t.end()
+})
+
+// https://github.com/nodejs/undici/issues/1556
+test('MockAgent - using fetch yields a headers object in the reply callback', { skip: nodeMajor < 16 }, async (t) => {
+ const { fetch } = require('..')
+
+ const mockAgent = new MockAgent()
+ mockAgent.disableNetConnect()
+ t.teardown(mockAgent.close.bind(mockAgent))
+
+ const mockPool = mockAgent.get('http://localhost:3000')
+
+ mockPool.intercept({
+ path: '/headers',
+ method: 'GET'
+ }).reply(200, (opts) => {
+ t.same(opts.headers, {
+ accept: '*/*',
+ 'accept-language': '*',
+ 'sec-fetch-mode': 'cors',
+ 'user-agent': 'undici',
+ 'accept-encoding': 'gzip, deflate'
+ })
+
+ return {}
+ })
+
+ await fetch('http://localhost:3000/headers', {
+ dispatcher: mockAgent
+ })
+
+ t.end()
+})
+
+// https://github.com/nodejs/undici/issues/1579
+test('MockAgent - headers in mock dispatcher intercept should be case-insensitive', { skip: nodeMajor < 16 }, async (t) => {
+ const { fetch } = require('..')
+
+ const mockAgent = new MockAgent()
+ mockAgent.disableNetConnect()
+ setGlobalDispatcher(mockAgent)
+ t.teardown(mockAgent.close.bind(mockAgent))
+
+ const mockPool = mockAgent.get('https://example.com')
+
+ mockPool
+ .intercept({
+ path: '/',
+ headers: {
+ authorization: 'Bearer 12345',
+ 'USER-agent': 'undici'
+ }
+ })
+ .reply(200)
+
+ await fetch('https://example.com', {
+ headers: {
+ Authorization: 'Bearer 12345',
+ 'user-AGENT': 'undici'
+ }
+ })
+
+ t.end()
+})
+
+// https://github.com/nodejs/undici/issues/1757
+test('MockAgent - reply callback can be asynchronous', { skip: nodeMajor < 16 }, async (t) => {
+ const { fetch } = require('..')
+ const ReadableStream = globalThis.ReadableStream || require('stream/web').ReadableStream
+
+ class MiniflareDispatcher extends Dispatcher {
+ constructor (inner, options) {
+ super(options)
+ this.inner = inner
+ }
+
+ dispatch (options, handler) {
+ return this.inner.dispatch(options, handler)
+ }
+
+ close (...args) {
+ return this.inner.close(...args)
+ }
+
+ destroy (...args) {
+ return this.inner.destroy(...args)
+ }
+ }
+
+ const mockAgent = new MockAgent()
+ const mockClient = mockAgent.get('http://localhost:3000')
+ mockAgent.disableNetConnect()
+ setGlobalDispatcher(new MiniflareDispatcher(mockAgent))
+
+ t.teardown(mockAgent.close.bind(mockAgent))
+
+ mockClient.intercept({
+ path: () => true,
+ method: () => true
+ }).reply(200, async (opts) => {
+ if (opts.body && opts.body[Symbol.asyncIterator]) {
+ const chunks = []
+ for await (const chunk of opts.body) {
+ chunks.push(chunk)
+ }
+
+ return Buffer.concat(chunks)
+ }
+
+ return opts.body
+ }).persist()
+
+ {
+ const response = await fetch('http://localhost:3000', {
+ method: 'POST',
+ body: JSON.stringify({ foo: 'bar' })
+ })
+
+ t.same(await response.json(), { foo: 'bar' })
+ }
+
+ {
+ const response = await fetch('http://localhost:3000', {
+ method: 'POST',
+ body: new ReadableStream({
+ start (controller) {
+ controller.enqueue(new TextEncoder().encode('{"foo":'))
+
+ setTimeout(() => {
+ controller.enqueue(new TextEncoder().encode('"bar"}'))
+ controller.close()
+ }, 100)
+ }
+ }),
+ duplex: 'half'
+ })
+
+ t.same(await response.json(), { foo: 'bar' })
+ }
+})
+
+test('MockAgent - headers should be array of strings', async (t) => {
+ const mockAgent = new MockAgent()
+ mockAgent.disableNetConnect()
+ setGlobalDispatcher(mockAgent)
+
+ const mockPool = mockAgent.get('http://localhost:3000')
+
+ mockPool.intercept({
+ path: '/foo',
+ method: 'GET'
+ }).reply(200, 'foo', {
+ headers: {
+ 'set-cookie': [
+ 'foo=bar',
+ 'bar=baz',
+ 'baz=qux'
+ ]
+ }
+ })
+
+ const { headers } = await request('http://localhost:3000/foo', {
+ method: 'GET'
+ })
+
+ t.same(headers['set-cookie'], [
+ 'foo=bar',
+ 'bar=baz',
+ 'baz=qux'
+ ])
+})
+
+// https://github.com/nodejs/undici/issues/2418
+test('MockAgent - Sending ReadableStream body', { skip: nodeMajor < 16 }, async (t) => {
+ t.plan(1)
+ const { fetch } = require('..')
+ const ReadableStream = globalThis.ReadableStream || require('stream/web').ReadableStream
+
+ const mockAgent = new MockAgent()
+ setGlobalDispatcher(mockAgent)
+
+ const server = createServer((req, res) => {
+ res.setHeader('content-type', 'text/plain')
+ req.pipe(res)
+ })
+
+ t.teardown(mockAgent.close.bind(mockAgent))
+ t.teardown(server.close.bind(server))
+
+ await promisify(server.listen.bind(server))(0)
+
+ const url = `http://localhost:${server.address().port}`
+
+ const response = await fetch(url, {
+ method: 'POST',
+ body: new ReadableStream({
+ start (controller) {
+ controller.enqueue('test')
+ controller.close()
+ }
+ }),
+ duplex: 'half'
+ })
+
+ t.same(await response.text(), 'test')
+})
diff --git a/test/mock-client.js b/test/mock-client.js
new file mode 100644
index 0000000..ef0600e
--- /dev/null
+++ b/test/mock-client.js
@@ -0,0 +1,446 @@
+'use strict'
+
+const { test } = require('tap')
+const { createServer } = require('http')
+const { promisify } = require('util')
+const { MockAgent, MockClient, setGlobalDispatcher, request } = require('..')
+const { kUrl } = require('../lib/core/symbols')
+const { kDispatches } = require('../lib/mock/mock-symbols')
+const { InvalidArgumentError } = require('../lib/core/errors')
+const { MockInterceptor } = require('../lib/mock/mock-interceptor')
+const { getResponse } = require('../lib/mock/mock-utils')
+const Dispatcher = require('../lib/dispatcher')
+
+test('MockClient - constructor', t => {
+ t.plan(3)
+
+ t.test('fails if opts.agent does not implement `get` method', t => {
+ t.plan(1)
+ t.throws(() => new MockClient('http://localhost:9999', { agent: { get: 'not a function' } }), InvalidArgumentError)
+ })
+
+ t.test('sets agent', t => {
+ t.plan(1)
+ t.doesNotThrow(() => new MockClient('http://localhost:9999', { agent: new MockAgent({ connections: 1 }) }))
+ })
+
+ t.test('should implement the Dispatcher API', t => {
+ t.plan(1)
+
+ const mockClient = new MockClient('http://localhost:9999', { agent: new MockAgent({ connections: 1 }) })
+ t.type(mockClient, Dispatcher)
+ })
+})
+
+test('MockClient - dispatch', t => {
+ t.plan(2)
+
+ t.test('should handle a single interceptor', (t) => {
+ t.plan(1)
+
+ const baseUrl = 'http://localhost:9999'
+
+ const mockAgent = new MockAgent({ connections: 1 })
+ t.teardown(mockAgent.close.bind(mockAgent))
+
+ const mockClient = mockAgent.get(baseUrl)
+
+ this[kUrl] = new URL('http://localhost:9999')
+ mockClient[kDispatches] = [
+ {
+ path: '/foo',
+ method: 'GET',
+ data: {
+ statusCode: 200,
+ data: 'hello',
+ headers: {},
+ trailers: {},
+ error: null
+ }
+ }
+ ]
+
+ t.doesNotThrow(() => mockClient.dispatch({
+ path: '/foo',
+ method: 'GET'
+ }, {
+ onHeaders: (_statusCode, _headers, resume) => resume(),
+ onData: () => {},
+ onComplete: () => {}
+ }))
+ })
+
+ t.test('should directly throw error from mockDispatch function if error is not a MockNotMatchedError', (t) => {
+ t.plan(1)
+
+ const baseUrl = 'http://localhost:9999'
+
+ const mockAgent = new MockAgent({ connections: 1 })
+ t.teardown(mockAgent.close.bind(mockAgent))
+
+ const mockClient = mockAgent.get(baseUrl)
+
+ this[kUrl] = new URL('http://localhost:9999')
+ mockClient[kDispatches] = [
+ {
+ path: '/foo',
+ method: 'GET',
+ data: {
+ statusCode: 200,
+ data: 'hello',
+ headers: {},
+ trailers: {},
+ error: null
+ }
+ }
+ ]
+
+ t.throws(() => mockClient.dispatch({
+ path: '/foo',
+ method: 'GET'
+ }, {
+ onHeaders: (_statusCode, _headers, resume) => { throw new Error('kaboom') },
+ onData: () => {},
+ onComplete: () => {}
+ }), new Error('kaboom'))
+ })
+})
+
+test('MockClient - intercept should return a MockInterceptor', (t) => {
+ t.plan(1)
+
+ const baseUrl = 'http://localhost:9999'
+
+ const mockAgent = new MockAgent({ connections: 1 })
+ t.teardown(mockAgent.close.bind(mockAgent))
+
+ const mockClient = mockAgent.get(baseUrl)
+
+ const interceptor = mockClient.intercept({
+ path: '/foo',
+ method: 'GET'
+ })
+
+ t.type(interceptor, MockInterceptor)
+})
+
+test('MockClient - intercept validation', (t) => {
+ t.plan(4)
+
+ t.test('it should error if no options specified in the intercept', t => {
+ t.plan(1)
+ const mockAgent = new MockAgent({ connections: 1 })
+ t.teardown(mockAgent.close.bind(mockAgent))
+
+ const mockClient = mockAgent.get('http://localhost:9999')
+
+ t.throws(() => mockClient.intercept(), new InvalidArgumentError('opts must be an object'))
+ })
+
+ t.test('it should error if no path specified in the intercept', t => {
+ t.plan(1)
+ const mockAgent = new MockAgent({ connections: 1 })
+ t.teardown(mockAgent.close.bind(mockAgent))
+
+ const mockClient = mockAgent.get('http://localhost:9999')
+
+ t.throws(() => mockClient.intercept({}), new InvalidArgumentError('opts.path must be defined'))
+ })
+
+ t.test('it should default to GET if no method specified in the intercept', t => {
+ t.plan(1)
+ const mockAgent = new MockAgent({ connections: 1 })
+ t.teardown(mockAgent.close.bind(mockAgent))
+
+ const mockClient = mockAgent.get('http://localhost:9999')
+ t.doesNotThrow(() => mockClient.intercept({ path: '/foo' }))
+ })
+
+ t.test('it should uppercase the method - https://github.com/nodejs/undici/issues/1320', t => {
+ t.plan(1)
+
+ const mockAgent = new MockAgent()
+ const mockClient = mockAgent.get('http://localhost:3000')
+
+ t.teardown(mockAgent.close.bind(mockAgent))
+
+ mockClient.intercept({
+ path: '/test',
+ method: 'patch'
+ }).reply(200, 'Hello!')
+
+ t.equal(mockClient[kDispatches][0].method, 'PATCH')
+ })
+})
+
+test('MockClient - close should run without error', async (t) => {
+ t.plan(1)
+
+ const baseUrl = 'http://localhost:9999'
+
+ const mockAgent = new MockAgent({ connections: 1 })
+ t.teardown(mockAgent.close.bind(mockAgent))
+
+ const mockClient = mockAgent.get(baseUrl)
+ mockClient[kDispatches] = [
+ {
+ path: '/foo',
+ method: 'GET',
+ data: {
+ statusCode: 200,
+ data: 'hello',
+ headers: {},
+ trailers: {},
+ error: null
+ }
+ }
+ ]
+
+ await t.resolves(mockClient.close())
+})
+
+test('MockClient - should be able to set as globalDispatcher', async (t) => {
+ t.plan(3)
+
+ const server = createServer((req, res) => {
+ res.setHeader('content-type', 'text/plain')
+ res.end('should not be called')
+ t.fail('should not be called')
+ t.end()
+ })
+ t.teardown(server.close.bind(server))
+
+ await promisify(server.listen.bind(server))(0)
+
+ const baseUrl = `http://localhost:${server.address().port}`
+
+ const mockAgent = new MockAgent({ connections: 1 })
+ t.teardown(mockAgent.close.bind(mockAgent))
+
+ const mockClient = mockAgent.get(baseUrl)
+ t.type(mockClient, MockClient)
+ setGlobalDispatcher(mockClient)
+
+ mockClient.intercept({
+ path: '/foo',
+ method: 'GET'
+ }).reply(200, 'hello')
+
+ const { statusCode, body } = await request(`${baseUrl}/foo`, {
+ method: 'GET'
+ })
+ t.equal(statusCode, 200)
+
+ const response = await getResponse(body)
+ t.same(response, 'hello')
+})
+
+test('MockClient - should support query params', async (t) => {
+ t.plan(3)
+
+ const server = createServer((req, res) => {
+ res.setHeader('content-type', 'text/plain')
+ res.end('should not be called')
+ t.fail('should not be called')
+ t.end()
+ })
+ t.teardown(server.close.bind(server))
+
+ await promisify(server.listen.bind(server))(0)
+
+ const baseUrl = `http://localhost:${server.address().port}`
+
+ const mockAgent = new MockAgent({ connections: 1 })
+ t.teardown(mockAgent.close.bind(mockAgent))
+
+ const mockClient = mockAgent.get(baseUrl)
+ t.type(mockClient, MockClient)
+ setGlobalDispatcher(mockClient)
+
+ const query = {
+ pageNum: 1
+ }
+ mockClient.intercept({
+ path: '/foo',
+ query,
+ method: 'GET'
+ }).reply(200, 'hello')
+
+ const { statusCode, body } = await request(`${baseUrl}/foo`, {
+ method: 'GET',
+ query
+ })
+ t.equal(statusCode, 200)
+
+ const response = await getResponse(body)
+ t.same(response, 'hello')
+})
+
+test('MockClient - should intercept query params with hardcoded path', async (t) => {
+ t.plan(3)
+
+ const server = createServer((req, res) => {
+ res.setHeader('content-type', 'text/plain')
+ res.end('should not be called')
+ t.fail('should not be called')
+ t.end()
+ })
+ t.teardown(server.close.bind(server))
+
+ await promisify(server.listen.bind(server))(0)
+
+ const baseUrl = `http://localhost:${server.address().port}`
+
+ const mockAgent = new MockAgent({ connections: 1 })
+ t.teardown(mockAgent.close.bind(mockAgent))
+
+ const mockClient = mockAgent.get(baseUrl)
+ t.type(mockClient, MockClient)
+ setGlobalDispatcher(mockClient)
+
+ const query = {
+ pageNum: 1
+ }
+ mockClient.intercept({
+ path: '/foo?pageNum=1',
+ method: 'GET'
+ }).reply(200, 'hello')
+
+ const { statusCode, body } = await request(`${baseUrl}/foo`, {
+ method: 'GET',
+ query
+ })
+ t.equal(statusCode, 200)
+
+ const response = await getResponse(body)
+ t.same(response, 'hello')
+})
+
+test('MockClient - should intercept query params regardless of key ordering', async (t) => {
+ t.plan(3)
+
+ const server = createServer((req, res) => {
+ res.setHeader('content-type', 'text/plain')
+ res.end('should not be called')
+ t.fail('should not be called')
+ t.end()
+ })
+ t.teardown(server.close.bind(server))
+
+ await promisify(server.listen.bind(server))(0)
+
+ const baseUrl = `http://localhost:${server.address().port}`
+
+ const mockAgent = new MockAgent({ connections: 1 })
+ t.teardown(mockAgent.close.bind(mockAgent))
+
+ const mockClient = mockAgent.get(baseUrl)
+ t.type(mockClient, MockClient)
+ setGlobalDispatcher(mockClient)
+
+ const query = {
+ pageNum: 1,
+ limit: 100,
+ ordering: [false, true]
+ }
+
+ mockClient.intercept({
+ path: '/foo',
+ query: {
+ ordering: query.ordering,
+ pageNum: query.pageNum,
+ limit: query.limit
+ },
+ method: 'GET'
+ }).reply(200, 'hello')
+
+ const { statusCode, body } = await request(`${baseUrl}/foo`, {
+ method: 'GET',
+ query
+ })
+ t.equal(statusCode, 200)
+
+ const response = await getResponse(body)
+ t.same(response, 'hello')
+})
+
+test('MockClient - should be able to use as a local dispatcher', async (t) => {
+ t.plan(3)
+
+ const server = createServer((req, res) => {
+ res.setHeader('content-type', 'text/plain')
+ res.end('should not be called')
+ t.fail('should not be called')
+ t.end()
+ })
+ t.teardown(server.close.bind(server))
+
+ await promisify(server.listen.bind(server))(0)
+
+ const baseUrl = `http://localhost:${server.address().port}`
+
+ const mockAgent = new MockAgent({ connections: 1 })
+ t.teardown(mockAgent.close.bind(mockAgent))
+
+ const mockClient = mockAgent.get(baseUrl)
+ t.type(mockClient, MockClient)
+
+ mockClient.intercept({
+ path: '/foo',
+ method: 'GET'
+ }).reply(200, 'hello')
+
+ const { statusCode, body } = await request(`${baseUrl}/foo`, {
+ method: 'GET',
+ dispatcher: mockClient
+ })
+ t.equal(statusCode, 200)
+
+ const response = await getResponse(body)
+ t.same(response, 'hello')
+})
+
+test('MockClient - basic intercept with MockClient.request', async (t) => {
+ t.plan(5)
+
+ const server = createServer((req, res) => {
+ res.setHeader('content-type', 'text/plain')
+ res.end('should not be called')
+ t.fail('should not be called')
+ t.end()
+ })
+ t.teardown(server.close.bind(server))
+
+ await promisify(server.listen.bind(server))(0)
+
+ const baseUrl = `http://localhost:${server.address().port}`
+
+ const mockAgent = new MockAgent({ connections: 1 })
+ t.teardown(mockAgent.close.bind(mockAgent))
+ const mockClient = mockAgent.get(baseUrl)
+ t.type(mockClient, MockClient)
+
+ mockClient.intercept({
+ path: '/foo?hello=there&see=ya',
+ method: 'POST',
+ body: 'form1=data1&form2=data2'
+ }).reply(200, { foo: 'bar' }, {
+ headers: { 'content-type': 'application/json' },
+ trailers: { 'Content-MD5': 'test' }
+ })
+
+ const { statusCode, headers, trailers, body } = await mockClient.request({
+ origin: baseUrl,
+ path: '/foo?hello=there&see=ya',
+ method: 'POST',
+ body: 'form1=data1&form2=data2'
+ })
+ t.equal(statusCode, 200)
+ t.equal(headers['content-type'], 'application/json')
+ t.same(trailers, { 'content-md5': 'test' })
+
+ const jsonResponse = JSON.parse(await getResponse(body))
+ t.same(jsonResponse, {
+ foo: 'bar'
+ })
+})
diff --git a/test/mock-errors.js b/test/mock-errors.js
new file mode 100644
index 0000000..a96de0b
--- /dev/null
+++ b/test/mock-errors.js
@@ -0,0 +1,32 @@
+'use strict'
+
+const { test } = require('tap')
+const { mockErrors, errors } = require('..')
+
+test('mockErrors', (t) => {
+ t.plan(1)
+
+ t.test('MockNotMatchedError', t => {
+ t.plan(2)
+
+ t.test('should implement an UndiciError', t => {
+ t.plan(4)
+
+ const mockError = new mockErrors.MockNotMatchedError()
+ t.type(mockError, errors.UndiciError)
+ t.same(mockError.name, 'MockNotMatchedError')
+ t.same(mockError.code, 'UND_MOCK_ERR_MOCK_NOT_MATCHED')
+ t.same(mockError.message, 'The request does not match any registered mock dispatches')
+ })
+
+ t.test('should set a custom message', t => {
+ t.plan(4)
+
+ const mockError = new mockErrors.MockNotMatchedError('custom message')
+ t.type(mockError, errors.UndiciError)
+ t.same(mockError.name, 'MockNotMatchedError')
+ t.same(mockError.code, 'UND_MOCK_ERR_MOCK_NOT_MATCHED')
+ t.same(mockError.message, 'custom message')
+ })
+ })
+})
diff --git a/test/mock-interceptor-unused-assertions.js b/test/mock-interceptor-unused-assertions.js
new file mode 100644
index 0000000..bfa2275
--- /dev/null
+++ b/test/mock-interceptor-unused-assertions.js
@@ -0,0 +1,219 @@
+'use strict'
+
+const { test, beforeEach, afterEach } = require('tap')
+const { MockAgent, setGlobalDispatcher } = require('..')
+const PendingInterceptorsFormatter = require('../lib/mock/pending-interceptors-formatter')
+
+// Avoid colors in the output for inline snapshots.
+const pendingInterceptorsFormatter = new PendingInterceptorsFormatter({ disableColors: true })
+
+let originalGlobalDispatcher
+
+const origin = 'https://localhost:9999'
+
+beforeEach(() => {
+ // Disallow all network activity by default by using a mock agent as the global dispatcher
+ const globalDispatcher = new MockAgent()
+ globalDispatcher.disableNetConnect()
+ setGlobalDispatcher(globalDispatcher)
+ originalGlobalDispatcher = globalDispatcher
+})
+
+afterEach(() => {
+ setGlobalDispatcher(originalGlobalDispatcher)
+})
+
+function mockAgentWithOneInterceptor () {
+ const agent = new MockAgent()
+ agent.disableNetConnect()
+
+ agent
+ .get('https://example.com')
+ .intercept({ method: 'GET', path: '/' })
+ .reply(200, '')
+
+ return agent
+}
+
+test('1 pending interceptor', t => {
+ t.plan(2)
+
+ const err = t.throws(() => mockAgentWithOneInterceptor().assertNoPendingInterceptors({ pendingInterceptorsFormatter }))
+
+ t.same(err.message, `
+1 interceptor is pending:
+
+┌─────────┬────────┬───────────────────────┬──────┬─────────────┬────────────┬─────────────┬───────────â”
+│ (index) │ Method │ Origin │ Path │ Status code │ Persistent │ Invocations │ Remaining │
+├─────────┼────────┼───────────────────────┼──────┼─────────────┼────────────┼─────────────┼───────────┤
+│ 0 │ 'GET' │ 'https://example.com' │ '/' │ 200 │ 'âŒ' │ 0 │ 1 │
+└─────────┴────────┴───────────────────────┴──────┴─────────────┴────────────┴─────────────┴───────────┘
+`.trim())
+})
+
+test('2 pending interceptors', t => {
+ t.plan(2)
+
+ const withTwoInterceptors = mockAgentWithOneInterceptor()
+ withTwoInterceptors
+ .get(origin)
+ .intercept({ method: 'get', path: '/some/path' })
+ .reply(204, 'OK')
+ const err = t.throws(() => withTwoInterceptors.assertNoPendingInterceptors({ pendingInterceptorsFormatter }))
+
+ t.same(err.message, `
+2 interceptors are pending:
+
+┌─────────┬────────┬──────────────────────────┬──────────────┬─────────────┬────────────┬─────────────┬───────────â”
+│ (index) │ Method │ Origin │ Path │ Status code │ Persistent │ Invocations │ Remaining │
+├─────────┼────────┼──────────────────────────┼──────────────┼─────────────┼────────────┼─────────────┼───────────┤
+│ 0 │ 'GET' │ 'https://example.com' │ '/' │ 200 │ 'âŒ' │ 0 │ 1 │
+│ 1 │ 'GET' │ 'https://localhost:9999' │ '/some/path' │ 204 │ 'âŒ' │ 0 │ 1 │
+└─────────┴────────┴──────────────────────────┴──────────────┴─────────────┴────────────┴─────────────┴───────────┘
+`.trim())
+})
+
+test('Variations of persist(), times(), and pending status', async t => {
+ t.plan(7)
+
+ // Agent with unused interceptor
+ const agent = mockAgentWithOneInterceptor()
+
+ // Unused with persist()
+ agent
+ .get(origin)
+ .intercept({ method: 'get', path: '/persistent/unused' })
+ .reply(200, 'OK')
+ .persist()
+
+ // Used with persist()
+ agent
+ .get(origin)
+ .intercept({ method: 'GET', path: '/persistent/used' })
+ .reply(200, 'OK')
+ .persist()
+ t.same((await agent.request({ origin, method: 'GET', path: '/persistent/used' })).statusCode, 200)
+
+ // Consumed without persist()
+ agent.get(origin)
+ .intercept({ method: 'post', path: '/transient/pending' })
+ .reply(201, 'Created')
+ t.same((await agent.request({ origin, method: 'POST', path: '/transient/pending' })).statusCode, 201)
+
+ // Partially pending with times()
+ agent.get(origin)
+ .intercept({ method: 'get', path: '/times/partial' })
+ .reply(200, 'OK')
+ .times(5)
+ t.same((await agent.request({ origin, method: 'GET', path: '/times/partial' })).statusCode, 200)
+
+ // Unused with times()
+ agent.get(origin)
+ .intercept({ method: 'get', path: '/times/unused' })
+ .reply(200, 'OK')
+ .times(2)
+
+ // Fully pending with times()
+ agent.get(origin)
+ .intercept({ method: 'get', path: '/times/pending' })
+ .reply(200, 'OK')
+ .times(2)
+ t.same((await agent.request({ origin, method: 'GET', path: '/times/pending' })).statusCode, 200)
+ t.same((await agent.request({ origin, method: 'GET', path: '/times/pending' })).statusCode, 200)
+
+ const err = t.throws(() => agent.assertNoPendingInterceptors({ pendingInterceptorsFormatter }))
+
+ t.same(err.message, `
+4 interceptors are pending:
+
+┌─────────┬────────┬──────────────────────────┬──────────────────────┬─────────────┬────────────┬─────────────┬───────────â”
+│ (index) │ Method │ Origin │ Path │ Status code │ Persistent │ Invocations │ Remaining │
+├─────────┼────────┼──────────────────────────┼──────────────────────┼─────────────┼────────────┼─────────────┼───────────┤
+│ 0 │ 'GET' │ 'https://example.com' │ '/' │ 200 │ 'âŒ' │ 0 │ 1 │
+│ 1 │ 'GET' │ 'https://localhost:9999' │ '/persistent/unused' │ 200 │ '✅' │ 0 │ Infinity │
+│ 2 │ 'GET' │ 'https://localhost:9999' │ '/times/partial' │ 200 │ 'âŒ' │ 1 │ 4 │
+│ 3 │ 'GET' │ 'https://localhost:9999' │ '/times/unused' │ 200 │ 'âŒ' │ 0 │ 2 │
+└─────────┴────────┴──────────────────────────┴──────────────────────┴─────────────┴────────────┴─────────────┴───────────┘
+`.trim())
+})
+
+test('works when no interceptors are registered', t => {
+ t.plan(2)
+
+ const agent = new MockAgent()
+ agent.disableNetConnect()
+
+ t.same(agent.pendingInterceptors(), [])
+ t.doesNotThrow(() => agent.assertNoPendingInterceptors())
+})
+
+test('works when all interceptors are pending', async t => {
+ t.plan(4)
+
+ const agent = new MockAgent()
+ agent.disableNetConnect()
+
+ agent.get(origin).intercept({ method: 'get', path: '/' }).reply(200, 'OK')
+ t.same((await agent.request({ origin, method: 'GET', path: '/' })).statusCode, 200)
+
+ agent.get(origin).intercept({ method: 'get', path: '/persistent' }).reply(200, 'OK')
+ t.same((await agent.request({ origin, method: 'GET', path: '/persistent' })).statusCode, 200)
+
+ t.same(agent.pendingInterceptors(), [])
+ t.doesNotThrow(() => agent.assertNoPendingInterceptors())
+})
+
+test('defaults to rendering output with terminal color when process.env.CI is unset', t => {
+ t.plan(2)
+
+ // This ensures that the test works in an environment where the CI env var is set.
+ const oldCiEnvVar = process.env.CI
+ delete process.env.CI
+
+ const err = t.throws(
+ () => mockAgentWithOneInterceptor().assertNoPendingInterceptors())
+ t.same(err.message, `
+1 interceptor is pending:
+
+┌─────────┬────────┬───────────────────────┬──────┬─────────────┬────────────┬─────────────┬───────────â”
+│ (index) │ Method │ Origin │ Path │ Status code │ Persistent │ Invocations │ Remaining │
+├─────────┼────────┼───────────────────────┼──────┼─────────────┼────────────┼─────────────┼───────────┤
+│ 0 │ \u001b[32m'GET'\u001b[39m │ \u001b[32m'https://example.com'\u001b[39m │ \u001b[32m'/'\u001b[39m │ \u001b[33m200\u001b[39m │ \u001b[32m'âŒ'\u001b[39m │ \u001b[33m0\u001b[39m │ \u001b[33m1\u001b[39m │
+└─────────┴────────┴───────────────────────┴──────┴─────────────┴────────────┴─────────────┴───────────┘
+`.trim())
+
+ // Re-set the CI env var if it were set.
+ // Assigning `undefined` does not work,
+ // because reading the env var afterwards yields the string 'undefined',
+ // so we need to re-set it conditionally.
+ if (oldCiEnvVar != null) {
+ process.env.CI = oldCiEnvVar
+ }
+})
+
+test('returns unused interceptors', t => {
+ t.plan(1)
+
+ t.same(mockAgentWithOneInterceptor().pendingInterceptors(), [
+ {
+ timesInvoked: 0,
+ times: 1,
+ persist: false,
+ consumed: false,
+ pending: true,
+ path: '/',
+ method: 'GET',
+ body: undefined,
+ query: undefined,
+ headers: undefined,
+ data: {
+ error: null,
+ statusCode: 200,
+ data: '',
+ headers: {},
+ trailers: {}
+ },
+ origin: 'https://example.com'
+ }
+ ])
+})
diff --git a/test/mock-interceptor.js b/test/mock-interceptor.js
new file mode 100644
index 0000000..a11377d
--- /dev/null
+++ b/test/mock-interceptor.js
@@ -0,0 +1,258 @@
+'use strict'
+
+const { test } = require('tap')
+const { MockInterceptor, MockScope } = require('../lib/mock/mock-interceptor')
+const MockAgent = require('../lib/mock/mock-agent')
+const { kDispatchKey } = require('../lib/mock/mock-symbols')
+const { InvalidArgumentError } = require('../lib/core/errors')
+
+test('MockInterceptor - path', t => {
+ t.plan(1)
+ t.test('should remove hash fragment from paths', t => {
+ t.plan(1)
+ const mockInterceptor = new MockInterceptor({
+ path: '#foobar',
+ method: ''
+ }, [])
+ t.equal(mockInterceptor[kDispatchKey].path, '')
+ })
+})
+
+test('MockInterceptor - reply', t => {
+ t.plan(2)
+
+ t.test('should return MockScope', t => {
+ t.plan(1)
+ const mockInterceptor = new MockInterceptor({
+ path: '',
+ method: ''
+ }, [])
+ const result = mockInterceptor.reply(200, 'hello')
+ t.type(result, MockScope)
+ })
+
+ t.test('should error if passed options invalid', t => {
+ t.plan(2)
+
+ const mockInterceptor = new MockInterceptor({
+ path: '',
+ method: ''
+ }, [])
+ t.throws(() => mockInterceptor.reply(), new InvalidArgumentError('statusCode must be defined'))
+ t.throws(() => mockInterceptor.reply(200, '', 'hello'), new InvalidArgumentError('responseOptions must be an object'))
+ })
+})
+
+test('MockInterceptor - reply callback', t => {
+ t.plan(2)
+
+ t.test('should return MockScope', t => {
+ t.plan(1)
+ const mockInterceptor = new MockInterceptor({
+ path: '',
+ method: ''
+ }, [])
+ const result = mockInterceptor.reply(200, () => 'hello')
+ t.type(result, MockScope)
+ })
+
+ t.test('should error if passed options invalid', t => {
+ t.plan(2)
+
+ const mockInterceptor = new MockInterceptor({
+ path: '',
+ method: ''
+ }, [])
+ t.throws(() => mockInterceptor.reply(), new InvalidArgumentError('statusCode must be defined'))
+ t.throws(() => mockInterceptor.reply(200, () => {}, 'hello'), new InvalidArgumentError('responseOptions must be an object'))
+ })
+})
+
+test('MockInterceptor - reply options callback', t => {
+ t.plan(2)
+
+ t.test('should return MockScope', t => {
+ t.plan(2)
+
+ const mockInterceptor = new MockInterceptor({
+ path: '',
+ method: ''
+ }, [])
+ const result = mockInterceptor.reply((options) => ({
+ statusCode: 200,
+ data: 'hello'
+ }))
+ t.type(result, MockScope)
+
+ // Test parameters
+
+ const baseUrl = 'http://localhost:9999'
+ const mockAgent = new MockAgent()
+ t.teardown(mockAgent.close.bind(mockAgent))
+
+ const mockPool = mockAgent.get(baseUrl)
+
+ mockPool.intercept({
+ path: '/test',
+ method: 'GET'
+ }).reply((options) => {
+ t.strictSame(options, { path: '/test', method: 'GET', headers: { foo: 'bar' } })
+ return { statusCode: 200, data: 'hello' }
+ })
+
+ mockPool.dispatch({
+ path: '/test',
+ method: 'GET',
+ headers: { foo: 'bar' }
+ }, {
+ onHeaders: () => {},
+ onData: () => {},
+ onComplete: () => {}
+ })
+ })
+
+ t.test('should error if passed options invalid', async (t) => {
+ t.plan(3)
+
+ const baseUrl = 'http://localhost:9999'
+ const mockAgent = new MockAgent()
+ t.teardown(mockAgent.close.bind(mockAgent))
+
+ const mockPool = mockAgent.get(baseUrl)
+
+ mockPool.intercept({
+ path: '/test',
+ method: 'GET'
+ }).reply(() => {})
+
+ mockPool.intercept({
+ path: '/test3',
+ method: 'GET'
+ }).reply(() => ({
+ statusCode: 200,
+ data: 'hello',
+ responseOptions: 42
+ }))
+
+ mockPool.intercept({
+ path: '/test4',
+ method: 'GET'
+ }).reply(() => ({
+ data: 'hello',
+ responseOptions: 42
+ }))
+
+ t.throws(() => mockPool.dispatch({
+ path: '/test',
+ method: 'GET'
+ }, {
+ onHeaders: () => {},
+ onData: () => {},
+ onComplete: () => {}
+ }), new InvalidArgumentError('reply options callback must return an object'))
+
+ t.throws(() => mockPool.dispatch({
+ path: '/test3',
+ method: 'GET'
+ }, {
+ onHeaders: () => {},
+ onData: () => {},
+ onComplete: () => {}
+ }), new InvalidArgumentError('responseOptions must be an object'))
+
+ t.throws(() => mockPool.dispatch({
+ path: '/test4',
+ method: 'GET'
+ }, {
+ onHeaders: () => {},
+ onData: () => {},
+ onComplete: () => {}
+ }), new InvalidArgumentError('statusCode must be defined'))
+ })
+})
+
+test('MockInterceptor - replyWithError', t => {
+ t.plan(2)
+
+ t.test('should return MockScope', t => {
+ t.plan(1)
+ const mockInterceptor = new MockInterceptor({
+ path: '',
+ method: ''
+ }, [])
+ const result = mockInterceptor.replyWithError(new Error('kaboom'))
+ t.type(result, MockScope)
+ })
+
+ t.test('should error if passed options invalid', t => {
+ t.plan(1)
+
+ const mockInterceptor = new MockInterceptor({
+ path: '',
+ method: ''
+ }, [])
+ t.throws(() => mockInterceptor.replyWithError(), new InvalidArgumentError('error must be defined'))
+ })
+})
+
+test('MockInterceptor - defaultReplyHeaders', t => {
+ t.plan(2)
+
+ t.test('should return MockInterceptor', t => {
+ t.plan(1)
+ const mockInterceptor = new MockInterceptor({
+ path: '',
+ method: ''
+ }, [])
+ const result = mockInterceptor.defaultReplyHeaders({})
+ t.type(result, MockInterceptor)
+ })
+
+ t.test('should error if passed options invalid', t => {
+ t.plan(1)
+
+ const mockInterceptor = new MockInterceptor({
+ path: '',
+ method: ''
+ }, [])
+ t.throws(() => mockInterceptor.defaultReplyHeaders(), new InvalidArgumentError('headers must be defined'))
+ })
+})
+
+test('MockInterceptor - defaultReplyTrailers', t => {
+ t.plan(2)
+
+ t.test('should return MockInterceptor', t => {
+ t.plan(1)
+ const mockInterceptor = new MockInterceptor({
+ path: '',
+ method: ''
+ }, [])
+ const result = mockInterceptor.defaultReplyTrailers({})
+ t.type(result, MockInterceptor)
+ })
+
+ t.test('should error if passed options invalid', t => {
+ t.plan(1)
+
+ const mockInterceptor = new MockInterceptor({
+ path: '',
+ method: ''
+ }, [])
+ t.throws(() => mockInterceptor.defaultReplyTrailers(), new InvalidArgumentError('trailers must be defined'))
+ })
+})
+
+test('MockInterceptor - replyContentLength', t => {
+ t.plan(1)
+
+ t.test('should return MockInterceptor', t => {
+ t.plan(1)
+ const mockInterceptor = new MockInterceptor({
+ path: '',
+ method: ''
+ }, [])
+ const result = mockInterceptor.defaultReplyTrailers({})
+ t.type(result, MockInterceptor)
+ })
+})
diff --git a/test/mock-pool.js b/test/mock-pool.js
new file mode 100644
index 0000000..0ac1aac
--- /dev/null
+++ b/test/mock-pool.js
@@ -0,0 +1,369 @@
+'use strict'
+
+const { test } = require('tap')
+const { createServer } = require('http')
+const { promisify } = require('util')
+const { MockAgent, MockPool, getGlobalDispatcher, setGlobalDispatcher, request } = require('..')
+const { kUrl } = require('../lib/core/symbols')
+const { nodeMajor } = require('../lib/core/util')
+const { kDispatches } = require('../lib/mock/mock-symbols')
+const { InvalidArgumentError } = require('../lib/core/errors')
+const { MockInterceptor } = require('../lib/mock/mock-interceptor')
+const { getResponse } = require('../lib/mock/mock-utils')
+const Dispatcher = require('../lib/dispatcher')
+
+test('MockPool - constructor', t => {
+ t.plan(3)
+
+ t.test('fails if opts.agent does not implement `get` method', t => {
+ t.plan(1)
+ t.throws(() => new MockPool('http://localhost:9999', { agent: { get: 'not a function' } }), InvalidArgumentError)
+ })
+
+ t.test('sets agent', t => {
+ t.plan(1)
+ t.doesNotThrow(() => new MockPool('http://localhost:9999', { agent: new MockAgent() }))
+ })
+
+ t.test('should implement the Dispatcher API', t => {
+ t.plan(1)
+
+ const mockPool = new MockPool('http://localhost:9999', { agent: new MockAgent() })
+ t.type(mockPool, Dispatcher)
+ })
+})
+
+test('MockPool - dispatch', t => {
+ t.plan(2)
+
+ t.test('should handle a single interceptor', (t) => {
+ t.plan(1)
+
+ const baseUrl = 'http://localhost:9999'
+
+ const mockAgent = new MockAgent()
+ t.teardown(mockAgent.close.bind(mockAgent))
+
+ const mockPool = mockAgent.get(baseUrl)
+
+ this[kUrl] = new URL('http://localhost:9999')
+ mockPool[kDispatches] = [
+ {
+ path: '/foo',
+ method: 'GET',
+ data: {
+ statusCode: 200,
+ data: 'hello',
+ headers: {},
+ trailers: {},
+ error: null
+ }
+ }
+ ]
+
+ t.doesNotThrow(() => mockPool.dispatch({
+ path: '/foo',
+ method: 'GET'
+ }, {
+ onHeaders: (_statusCode, _headers, resume) => resume(),
+ onData: () => {},
+ onComplete: () => {}
+ }))
+ })
+
+ t.test('should directly throw error from mockDispatch function if error is not a MockNotMatchedError', (t) => {
+ t.plan(1)
+
+ const baseUrl = 'http://localhost:9999'
+
+ const mockAgent = new MockAgent()
+ t.teardown(mockAgent.close.bind(mockAgent))
+
+ const mockPool = mockAgent.get(baseUrl)
+
+ this[kUrl] = new URL('http://localhost:9999')
+ mockPool[kDispatches] = [
+ {
+ path: '/foo',
+ method: 'GET',
+ data: {
+ statusCode: 200,
+ data: 'hello',
+ headers: {},
+ trailers: {},
+ error: null
+ }
+ }
+ ]
+
+ t.throws(() => mockPool.dispatch({
+ path: '/foo',
+ method: 'GET'
+ }, {
+ onHeaders: (_statusCode, _headers, resume) => { throw new Error('kaboom') },
+ onData: () => {},
+ onComplete: () => {}
+ }), new Error('kaboom'))
+ })
+})
+
+test('MockPool - intercept should return a MockInterceptor', (t) => {
+ t.plan(1)
+
+ const baseUrl = 'http://localhost:9999'
+
+ const mockAgent = new MockAgent()
+ t.teardown(mockAgent.close.bind(mockAgent))
+
+ const mockPool = mockAgent.get(baseUrl)
+
+ const interceptor = mockPool.intercept({
+ path: '/foo',
+ method: 'GET'
+ })
+
+ t.ok(interceptor instanceof MockInterceptor)
+})
+
+test('MockPool - intercept validation', (t) => {
+ t.plan(3)
+
+ t.test('it should error if no options specified in the intercept', t => {
+ t.plan(1)
+ const mockAgent = new MockAgent()
+ t.teardown(mockAgent.close.bind(mockAgent))
+
+ const mockPool = mockAgent.get('http://localhost:9999')
+
+ t.throws(() => mockPool.intercept(), new InvalidArgumentError('opts must be an object'))
+ })
+
+ t.test('it should error if no path specified in the intercept', t => {
+ t.plan(1)
+ const mockAgent = new MockAgent()
+ t.teardown(mockAgent.close.bind(mockAgent))
+
+ const mockPool = mockAgent.get('http://localhost:9999')
+
+ t.throws(() => mockPool.intercept({}), new InvalidArgumentError('opts.path must be defined'))
+ })
+
+ t.test('it should default to GET if no method specified in the intercept', t => {
+ t.plan(1)
+ const mockAgent = new MockAgent()
+ t.teardown(mockAgent.close.bind(mockAgent))
+
+ const mockPool = mockAgent.get('http://localhost:9999')
+ t.doesNotThrow(() => mockPool.intercept({ path: '/foo' }))
+ })
+})
+
+test('MockPool - close should run without error', async (t) => {
+ t.plan(1)
+
+ const baseUrl = 'http://localhost:9999'
+
+ const mockAgent = new MockAgent()
+ t.teardown(mockAgent.close.bind(mockAgent))
+
+ const mockPool = mockAgent.get(baseUrl)
+
+ mockPool[kDispatches] = [
+ {
+ path: '/foo',
+ method: 'GET',
+ data: {
+ statusCode: 200,
+ data: 'hello',
+ headers: {},
+ trailers: {},
+ error: null
+ }
+ }
+ ]
+
+ await t.resolves(mockPool.close())
+})
+
+test('MockPool - should be able to set as globalDispatcher', async (t) => {
+ t.plan(3)
+
+ const server = createServer((req, res) => {
+ res.setHeader('content-type', 'text/plain')
+ res.end('should not be called')
+ t.fail('should not be called')
+ t.end()
+ })
+ t.teardown(server.close.bind(server))
+
+ await promisify(server.listen.bind(server))(0)
+
+ const baseUrl = `http://localhost:${server.address().port}`
+
+ const mockAgent = new MockAgent()
+ t.teardown(mockAgent.close.bind(mockAgent))
+
+ const mockPool = mockAgent.get(baseUrl)
+ t.type(mockPool, MockPool)
+ setGlobalDispatcher(mockPool)
+
+ mockPool.intercept({
+ path: '/foo',
+ method: 'GET'
+ }).reply(200, 'hello')
+
+ const { statusCode, body } = await request(`${baseUrl}/foo`, {
+ method: 'GET'
+ })
+ t.equal(statusCode, 200)
+
+ const response = await getResponse(body)
+ t.same(response, 'hello')
+})
+
+test('MockPool - should be able to use as a local dispatcher', async (t) => {
+ t.plan(3)
+
+ const server = createServer((req, res) => {
+ res.setHeader('content-type', 'text/plain')
+ res.end('should not be called')
+ t.fail('should not be called')
+ t.end()
+ })
+ t.teardown(server.close.bind(server))
+
+ await promisify(server.listen.bind(server))(0)
+
+ const baseUrl = `http://localhost:${server.address().port}`
+
+ const mockAgent = new MockAgent()
+ t.teardown(mockAgent.close.bind(mockAgent))
+
+ const mockPool = mockAgent.get(baseUrl)
+ t.type(mockPool, MockPool)
+
+ mockPool.intercept({
+ path: '/foo',
+ method: 'GET'
+ }).reply(200, 'hello')
+
+ const { statusCode, body } = await request(`${baseUrl}/foo`, {
+ method: 'GET',
+ dispatcher: mockPool
+ })
+ t.equal(statusCode, 200)
+
+ const response = await getResponse(body)
+ t.same(response, 'hello')
+})
+
+test('MockPool - basic intercept with MockPool.request', async (t) => {
+ t.plan(5)
+
+ const server = createServer((req, res) => {
+ res.setHeader('content-type', 'text/plain')
+ res.end('should not be called')
+ t.fail('should not be called')
+ t.end()
+ })
+ t.teardown(server.close.bind(server))
+
+ await promisify(server.listen.bind(server))(0)
+
+ const baseUrl = `http://localhost:${server.address().port}`
+
+ const mockAgent = new MockAgent()
+ t.teardown(mockAgent.close.bind(mockAgent))
+ const mockPool = mockAgent.get(baseUrl)
+ t.type(mockPool, MockPool)
+
+ mockPool.intercept({
+ path: '/foo?hello=there&see=ya',
+ method: 'POST',
+ body: 'form1=data1&form2=data2'
+ }).reply(200, { foo: 'bar' }, {
+ headers: { 'content-type': 'application/json' },
+ trailers: { 'Content-MD5': 'test' }
+ })
+
+ const { statusCode, headers, trailers, body } = await mockPool.request({
+ origin: baseUrl,
+ path: '/foo?hello=there&see=ya',
+ method: 'POST',
+ body: 'form1=data1&form2=data2'
+ })
+ t.equal(statusCode, 200)
+ t.equal(headers['content-type'], 'application/json')
+ t.same(trailers, { 'content-md5': 'test' })
+
+ const jsonResponse = JSON.parse(await getResponse(body))
+ t.same(jsonResponse, {
+ foo: 'bar'
+ })
+})
+
+// https://github.com/nodejs/undici/issues/1546
+test('MockPool - correct errors when consuming invalid JSON body', async (t) => {
+ const oldDispatcher = getGlobalDispatcher()
+
+ const mockAgent = new MockAgent()
+ mockAgent.disableNetConnect()
+ setGlobalDispatcher(mockAgent)
+
+ t.teardown(() => setGlobalDispatcher(oldDispatcher))
+
+ const mockPool = mockAgent.get('https://google.com')
+ mockPool.intercept({
+ path: 'https://google.com'
+ }).reply(200, 'it\'s just a text')
+
+ const { body } = await request('https://google.com')
+ await t.rejects(body.json(), SyntaxError)
+
+ t.end()
+})
+
+test('MockPool - allows matching headers in fetch', { skip: nodeMajor < 16 }, async (t) => {
+ const { fetch } = require('../index')
+
+ const oldDispatcher = getGlobalDispatcher()
+
+ const baseUrl = 'http://localhost:9999'
+ const mockAgent = new MockAgent()
+ mockAgent.disableNetConnect()
+ setGlobalDispatcher(mockAgent)
+
+ t.teardown(async () => {
+ await mockAgent.close()
+ setGlobalDispatcher(oldDispatcher)
+ })
+
+ const pool = mockAgent.get(baseUrl)
+ pool.intercept({
+ path: '/foo',
+ method: 'GET',
+ headers: {
+ accept: 'application/json'
+ }
+ }).reply(200, { ok: 1 }).times(3)
+
+ await t.resolves(
+ fetch(`${baseUrl}/foo`, {
+ headers: {
+ accept: 'application/json'
+ }
+ })
+ )
+
+ // no 'accept: application/json' header sent, not matched
+ await t.rejects(fetch(`${baseUrl}/foo`))
+
+ // not 'accept: application/json', not matched
+ await t.rejects(fetch(`${baseUrl}/foo`), {
+ headers: {
+ accept: 'text/plain'
+ }
+ }, TypeError)
+
+ t.end()
+})
diff --git a/test/mock-scope.js b/test/mock-scope.js
new file mode 100644
index 0000000..605ba58
--- /dev/null
+++ b/test/mock-scope.js
@@ -0,0 +1,73 @@
+'use strict'
+
+const { test } = require('tap')
+const { MockScope } = require('../lib/mock/mock-interceptor')
+const { InvalidArgumentError } = require('../lib/core/errors')
+
+test('MockScope - delay', t => {
+ t.plan(2)
+
+ t.test('should return MockScope', t => {
+ t.plan(1)
+ const mockScope = new MockScope({
+ path: '',
+ method: ''
+ }, [])
+ const result = mockScope.delay(200)
+ t.type(result, MockScope)
+ })
+
+ t.test('should error if passed options invalid', t => {
+ t.plan(4)
+
+ const mockScope = new MockScope({
+ path: '',
+ method: ''
+ }, [])
+ t.throws(() => mockScope.delay(), new InvalidArgumentError('waitInMs must be a valid integer > 0'))
+ t.throws(() => mockScope.delay(200.1), new InvalidArgumentError('waitInMs must be a valid integer > 0'))
+ t.throws(() => mockScope.delay(0), new InvalidArgumentError('waitInMs must be a valid integer > 0'))
+ t.throws(() => mockScope.delay(-1), new InvalidArgumentError('waitInMs must be a valid integer > 0'))
+ })
+})
+
+test('MockScope - persist', t => {
+ t.plan(1)
+
+ t.test('should return MockScope', t => {
+ t.plan(1)
+ const mockScope = new MockScope({
+ path: '',
+ method: ''
+ }, [])
+ const result = mockScope.persist()
+ t.type(result, MockScope)
+ })
+})
+
+test('MockScope - times', t => {
+ t.plan(2)
+
+ t.test('should return MockScope', t => {
+ t.plan(1)
+ const mockScope = new MockScope({
+ path: '',
+ method: ''
+ }, [])
+ const result = mockScope.times(200)
+ t.type(result, MockScope)
+ })
+
+ t.test('should error if passed options invalid', t => {
+ t.plan(4)
+
+ const mockScope = new MockScope({
+ path: '',
+ method: ''
+ }, [])
+ t.throws(() => mockScope.times(), new InvalidArgumentError('repeatTimes must be a valid integer > 0'))
+ t.throws(() => mockScope.times(200.1), new InvalidArgumentError('repeatTimes must be a valid integer > 0'))
+ t.throws(() => mockScope.times(0), new InvalidArgumentError('repeatTimes must be a valid integer > 0'))
+ t.throws(() => mockScope.times(-1), new InvalidArgumentError('repeatTimes must be a valid integer > 0'))
+ })
+})
diff --git a/test/mock-utils.js b/test/mock-utils.js
new file mode 100644
index 0000000..7799803
--- /dev/null
+++ b/test/mock-utils.js
@@ -0,0 +1,160 @@
+'use strict'
+
+const { test } = require('tap')
+const { nodeMajor } = require('../lib/core/util')
+const { MockNotMatchedError } = require('../lib/mock/mock-errors')
+const {
+ deleteMockDispatch,
+ getMockDispatch,
+ getResponseData,
+ getStatusText,
+ getHeaderByName
+} = require('../lib/mock/mock-utils')
+
+test('deleteMockDispatch - should do nothing if not able to find mock dispatch', (t) => {
+ t.plan(1)
+
+ const key = {
+ url: 'url',
+ path: 'path',
+ method: 'method',
+ body: 'body'
+ }
+
+ t.doesNotThrow(() => deleteMockDispatch([], key))
+})
+
+test('getMockDispatch', (t) => {
+ t.plan(3)
+
+ t.test('it should find a mock dispatch', (t) => {
+ t.plan(1)
+ const dispatches = [
+ {
+ path: 'path',
+ method: 'method',
+ consumed: false
+ }
+ ]
+
+ const result = getMockDispatch(dispatches, {
+ path: 'path',
+ method: 'method'
+ })
+ t.same(result, {
+ path: 'path',
+ method: 'method',
+ consumed: false
+ })
+ })
+
+ t.test('it should skip consumed dispatches', (t) => {
+ t.plan(1)
+ const dispatches = [
+ {
+ path: 'path',
+ method: 'method',
+ consumed: true
+ },
+ {
+ path: 'path',
+ method: 'method',
+ consumed: false
+ }
+ ]
+
+ const result = getMockDispatch(dispatches, {
+ path: 'path',
+ method: 'method'
+ })
+ t.same(result, {
+ path: 'path',
+ method: 'method',
+ consumed: false
+ })
+ })
+
+ t.test('it should throw if dispatch not found', (t) => {
+ t.plan(1)
+ const dispatches = [
+ {
+ path: 'path',
+ method: 'method',
+ consumed: false
+ }
+ ]
+
+ t.throws(() => getMockDispatch(dispatches, {
+ path: 'wrong',
+ method: 'wrong'
+ }), new MockNotMatchedError('Mock dispatch not matched for path \'wrong\''))
+ })
+})
+
+test('getResponseData', (t) => {
+ t.plan(3)
+
+ t.test('it should stringify objects', (t) => {
+ t.plan(1)
+ const responseData = getResponseData({ str: 'string', num: 42 })
+ t.equal(responseData, '{"str":"string","num":42}')
+ })
+
+ t.test('it should return strings untouched', (t) => {
+ t.plan(1)
+ const responseData = getResponseData('test')
+ t.equal(responseData, 'test')
+ })
+
+ t.test('it should return buffers untouched', (t) => {
+ t.plan(1)
+ const responseData = getResponseData(Buffer.from('test'))
+ t.ok(Buffer.isBuffer(responseData))
+ })
+})
+
+test('getStatusText', (t) => {
+ for (const statusCode of [
+ 100, 101, 102, 103, 200, 201, 202, 203,
+ 204, 205, 206, 207, 208, 226, 300, 301,
+ 302, 303, 304, 305, 306, 307, 308, 400,
+ 401, 402, 403, 404, 405, 406, 407, 408,
+ 409, 410, 411, 412, 413, 414, 415, 416,
+ 417, 418, 421, 422, 423, 424, 425, 426,
+ 428, 429, 431, 451, 500, 501, 502, 503,
+ 504, 505, 506, 507, 508, 510, 511
+ ]) {
+ t.ok(getStatusText(statusCode))
+ }
+
+ t.equal(getStatusText(420), 'unknown')
+
+ t.end()
+})
+
+test('getHeaderByName', (t) => {
+ const headersRecord = {
+ key: 'value'
+ }
+
+ t.equal(getHeaderByName(headersRecord, 'key'), 'value')
+ t.equal(getHeaderByName(headersRecord, 'anotherKey'), undefined)
+
+ const headersArray = ['key', 'value']
+
+ t.equal(getHeaderByName(headersArray, 'key'), 'value')
+ t.equal(getHeaderByName(headersArray, 'anotherKey'), undefined)
+
+ if (nodeMajor >= 16) {
+ const { Headers } = require('../index')
+
+ const headers = new Headers([
+ ['key', 'value']
+ ])
+
+ t.equal(getHeaderByName(headers, 'key'), 'value')
+ t.equal(getHeaderByName(headers, 'anotherKey'), null)
+ }
+
+ t.end()
+})
diff --git a/test/no-strict-content-length.js b/test/no-strict-content-length.js
new file mode 100644
index 0000000..993b0fd
--- /dev/null
+++ b/test/no-strict-content-length.js
@@ -0,0 +1,349 @@
+'use strict'
+
+const tap = require('tap')
+const { Client } = require('..')
+const { createServer } = require('http')
+const { Readable } = require('stream')
+const sinon = require('sinon')
+const { wrapWithAsyncIterable } = require('./utils/async-iterators')
+
+tap.test('strictContentLength: false', (t) => {
+ t.plan(7)
+
+ const emitWarningStub = sinon.stub(process, 'emitWarning')
+
+ function assertEmitWarningCalledAndReset () {
+ sinon.assert.called(emitWarningStub)
+ emitWarningStub.resetHistory()
+ }
+
+ t.teardown(() => {
+ emitWarningStub.restore()
+ })
+
+ t.test('request invalid content-length', (t) => {
+ t.plan(8)
+
+ const server = createServer((req, res) => {
+ res.end()
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, {
+ strictContentLength: false
+ })
+ t.teardown(client.close.bind(client))
+
+ client.request({
+ path: '/',
+ method: 'PUT',
+ headers: {
+ 'content-length': 10
+ },
+ body: 'asd'
+ }, (err, data) => {
+ assertEmitWarningCalledAndReset()
+ t.error(err)
+ })
+
+ client.request({
+ path: '/',
+ method: 'PUT',
+ headers: {
+ 'content-length': 10
+ },
+ body: 'asdasdasdasdasdasda'
+ }, (err, data) => {
+ assertEmitWarningCalledAndReset()
+ t.error(err)
+ })
+
+ client.request({
+ path: '/',
+ method: 'PUT',
+ headers: {
+ 'content-length': 10
+ },
+ body: Buffer.alloc(9)
+ }, (err, data) => {
+ assertEmitWarningCalledAndReset()
+ t.error(err)
+ })
+
+ client.request({
+ path: '/',
+ method: 'PUT',
+ headers: {
+ 'content-length': 10
+ },
+ body: Buffer.alloc(11)
+ }, (err, data) => {
+ assertEmitWarningCalledAndReset()
+ t.error(err)
+ })
+
+ client.request({
+ path: '/',
+ method: 'HEAD',
+ headers: {
+ 'content-length': 10
+ }
+ }, (err, data) => {
+ t.error(err)
+ })
+
+ client.request({
+ path: '/',
+ method: 'GET',
+ headers: {
+ 'content-length': 0
+ }
+ }, (err, data) => {
+ t.error(err)
+ })
+
+ client.request({
+ path: '/',
+ method: 'GET',
+ headers: {
+ 'content-length': 4
+ },
+ body: new Readable({
+ read () {
+ this.push('asd')
+ this.push(null)
+ }
+ })
+ }, (err, data) => {
+ t.error(err)
+ })
+
+ client.request({
+ path: '/',
+ method: 'GET',
+ headers: {
+ 'content-length': 4
+ },
+ body: new Readable({
+ read () {
+ this.push('asasdasdasdd')
+ this.push(null)
+ }
+ })
+ }, (err, data) => {
+ t.error(err)
+ })
+ })
+ })
+
+ t.test('request streaming content-length less than body size', (t) => {
+ t.plan(1)
+
+ const server = createServer((req, res) => {
+ res.end()
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, {
+ strictContentLength: false
+ })
+ t.teardown(client.close.bind(client))
+
+ client.request({
+ path: '/',
+ method: 'PUT',
+ headers: {
+ 'content-length': 2
+ },
+ body: new Readable({
+ read () {
+ setImmediate(() => {
+ this.push('abcd')
+ this.push(null)
+ })
+ }
+ })
+ }, (err) => {
+ assertEmitWarningCalledAndReset()
+ t.error(err)
+ })
+ })
+ })
+
+ t.test('request streaming content-length greater than body size', (t) => {
+ t.plan(1)
+
+ const server = createServer((req, res) => {
+ res.end()
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, {
+ strictContentLength: false
+ })
+ t.teardown(client.close.bind(client))
+
+ client.request({
+ path: '/',
+ method: 'PUT',
+ headers: {
+ 'content-length': 10
+ },
+ body: new Readable({
+ read () {
+ setImmediate(() => {
+ this.push('abcd')
+ this.push(null)
+ })
+ }
+ })
+ }, (err) => {
+ assertEmitWarningCalledAndReset()
+ t.error(err)
+ })
+ })
+ })
+
+ t.test('request streaming data when content-length=0', (t) => {
+ t.plan(1)
+
+ const server = createServer((req, res) => {
+ res.end()
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, {
+ strictContentLength: false
+ })
+ t.teardown(client.close.bind(client))
+
+ client.request({
+ path: '/',
+ method: 'PUT',
+ headers: {
+ 'content-length': 0
+ },
+ body: new Readable({
+ read () {
+ setImmediate(() => {
+ this.push('asdasdasdkajsdnasdkjasnd')
+ this.push(null)
+ })
+ }
+ })
+ }, (err) => {
+ assertEmitWarningCalledAndReset()
+ t.error(err)
+ })
+ })
+ })
+
+ t.test('request async iterating content-length less than body size', (t) => {
+ t.plan(1)
+
+ const server = createServer((req, res) => {
+ res.end()
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, {
+ strictContentLength: false
+ })
+ t.teardown(client.close.bind(client))
+
+ client.request({
+ path: '/',
+ method: 'PUT',
+ headers: {
+ 'content-length': 2
+ },
+ body: wrapWithAsyncIterable(new Readable({
+ read () {
+ setImmediate(() => {
+ this.push('abcd')
+ this.push(null)
+ })
+ }
+ }))
+ }, (err) => {
+ assertEmitWarningCalledAndReset()
+ t.error(err)
+ })
+ })
+ })
+
+ t.test('request async iterator content-length greater than body size', (t) => {
+ t.plan(1)
+
+ const server = createServer((req, res) => {
+ res.end()
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, {
+ strictContentLength: false
+ })
+ t.teardown(client.close.bind(client))
+
+ client.request({
+ path: '/',
+ method: 'PUT',
+ headers: {
+ 'content-length': 10
+ },
+ body: wrapWithAsyncIterable(new Readable({
+ read () {
+ setImmediate(() => {
+ this.push('abcd')
+ this.push(null)
+ })
+ }
+ }))
+ }, (err) => {
+ assertEmitWarningCalledAndReset()
+ t.error(err)
+ })
+ })
+ })
+
+ t.test('request async iterator data when content-length=0', (t) => {
+ t.plan(1)
+
+ const server = createServer((req, res) => {
+ res.end()
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, {
+ strictContentLength: false
+ })
+ t.teardown(client.close.bind(client))
+
+ client.request({
+ path: '/',
+ method: 'PUT',
+ headers: {
+ 'content-length': 0
+ },
+ body: wrapWithAsyncIterable(new Readable({
+ read () {
+ setImmediate(() => {
+ this.push('asdasdasdkajsdnasdkjasnd')
+ this.push(null)
+ })
+ }
+ }))
+ }, (err) => {
+ assertEmitWarningCalledAndReset()
+ t.error(err)
+ })
+ })
+ })
+})
diff --git a/test/node-fetch/LICENSE b/test/node-fetch/LICENSE
new file mode 100644
index 0000000..41ca1b6
--- /dev/null
+++ b/test/node-fetch/LICENSE
@@ -0,0 +1,22 @@
+The MIT License (MIT)
+
+Copyright (c) 2016 - 2020 Node Fetch Team
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
diff --git a/test/node-fetch/headers.js b/test/node-fetch/headers.js
new file mode 100644
index 0000000..e509fd8
--- /dev/null
+++ b/test/node-fetch/headers.js
@@ -0,0 +1,282 @@
+/* eslint no-unused-expressions: "off" */
+
+const { format } = require('util')
+const chai = require('chai')
+const chaiIterator = require('chai-iterator')
+const { Headers } = require('../../lib/fetch/headers.js')
+
+chai.use(chaiIterator)
+
+const { expect } = chai
+
+describe('Headers', () => {
+ it('should have attributes conforming to Web IDL', () => {
+ const headers = new Headers()
+ expect(Object.getOwnPropertyNames(headers)).to.be.empty
+ const enumerableProperties = []
+
+ for (const property in headers) {
+ enumerableProperties.push(property)
+ }
+
+ for (const toCheck of [
+ 'append',
+ 'delete',
+ 'entries',
+ 'forEach',
+ 'get',
+ 'has',
+ 'keys',
+ 'set',
+ 'values'
+ ]) {
+ expect(enumerableProperties).to.contain(toCheck)
+ }
+ })
+
+ it('should allow iterating through all headers with forEach', () => {
+ const headers = new Headers([
+ ['b', '2'],
+ ['c', '4'],
+ ['b', '3'],
+ ['a', '1']
+ ])
+ expect(headers).to.have.property('forEach')
+
+ const result = []
+ for (const [key, value] of headers.entries()) {
+ result.push([key, value])
+ }
+
+ expect(result).to.deep.equal([
+ ['a', '1'],
+ ['b', '2, 3'],
+ ['c', '4']
+ ])
+ })
+
+ it('should be iterable with forEach', () => {
+ const headers = new Headers()
+ headers.append('Accept', 'application/json')
+ headers.append('Accept', 'text/plain')
+ headers.append('Content-Type', 'text/html')
+
+ const results = []
+ headers.forEach((value, key, object) => {
+ results.push({ value, key, object })
+ })
+
+ expect(results.length).to.equal(2)
+ expect({ key: 'accept', value: 'application/json, text/plain', object: headers }).to.deep.equal(results[0])
+ expect({ key: 'content-type', value: 'text/html', object: headers }).to.deep.equal(results[1])
+ })
+
+ xit('should set "this" to undefined by default on forEach', () => {
+ const headers = new Headers({ Accept: 'application/json' })
+ headers.forEach(function () {
+ expect(this).to.be.undefined
+ })
+ })
+
+ it('should accept thisArg as a second argument for forEach', () => {
+ const headers = new Headers({ Accept: 'application/json' })
+ const thisArg = {}
+ headers.forEach(function () {
+ expect(this).to.equal(thisArg)
+ }, thisArg)
+ })
+
+ it('should allow iterating through all headers with for-of loop', () => {
+ const headers = new Headers([
+ ['b', '2'],
+ ['c', '4'],
+ ['a', '1']
+ ])
+ headers.append('b', '3')
+ expect(headers).to.be.iterable
+
+ const result = []
+ for (const pair of headers) {
+ result.push(pair)
+ }
+
+ expect(result).to.deep.equal([
+ ['a', '1'],
+ ['b', '2, 3'],
+ ['c', '4']
+ ])
+ })
+
+ it('should allow iterating through all headers with entries()', () => {
+ const headers = new Headers([
+ ['b', '2'],
+ ['c', '4'],
+ ['a', '1']
+ ])
+ headers.append('b', '3')
+
+ expect(headers.entries()).to.be.iterable
+ .and.to.deep.iterate.over([
+ ['a', '1'],
+ ['b', '2, 3'],
+ ['c', '4']
+ ])
+ })
+
+ it('should allow iterating through all headers with keys()', () => {
+ const headers = new Headers([
+ ['b', '2'],
+ ['c', '4'],
+ ['a', '1']
+ ])
+ headers.append('b', '3')
+
+ expect(headers.keys()).to.be.iterable
+ .and.to.iterate.over(['a', 'b', 'c'])
+ })
+
+ it('should allow iterating through all headers with values()', () => {
+ const headers = new Headers([
+ ['b', '2'],
+ ['c', '4'],
+ ['a', '1']
+ ])
+ headers.append('b', '3')
+
+ expect(headers.values()).to.be.iterable
+ .and.to.iterate.over(['1', '2, 3', '4'])
+ })
+
+ it('should reject illegal header', () => {
+ const headers = new Headers()
+ expect(() => new Headers({ 'He y': 'ok' })).to.throw(TypeError)
+ expect(() => new Headers({ 'Hé-y': 'ok' })).to.throw(TypeError)
+ expect(() => new Headers({ 'He-y': 'ăk' })).to.throw(TypeError)
+ expect(() => headers.append('Hé-y', 'ok')).to.throw(TypeError)
+ expect(() => headers.delete('Hé-y')).to.throw(TypeError)
+ expect(() => headers.get('Hé-y')).to.throw(TypeError)
+ expect(() => headers.has('Hé-y')).to.throw(TypeError)
+ expect(() => headers.set('Hé-y', 'ok')).to.throw(TypeError)
+ // Should reject empty header
+ expect(() => headers.append('', 'ok')).to.throw(TypeError)
+ })
+
+ xit('should ignore unsupported attributes while reading headers', () => {
+ const FakeHeader = function () {}
+ // Prototypes are currently ignored
+ // This might change in the future: #181
+ FakeHeader.prototype.z = 'fake'
+
+ const res = new FakeHeader()
+ res.a = 'string'
+ res.b = ['1', '2']
+ res.c = ''
+ res.d = []
+ res.e = 1
+ res.f = [1, 2]
+ res.g = { a: 1 }
+ res.h = undefined
+ res.i = null
+ res.j = Number.NaN
+ res.k = true
+ res.l = false
+ res.m = Buffer.from('test')
+
+ const h1 = new Headers(res)
+ h1.set('n', [1, 2])
+ h1.append('n', ['3', 4])
+
+ const h1Raw = h1.raw()
+
+ expect(h1Raw.a).to.include('string')
+ expect(h1Raw.b).to.include('1,2')
+ expect(h1Raw.c).to.include('')
+ expect(h1Raw.d).to.include('')
+ expect(h1Raw.e).to.include('1')
+ expect(h1Raw.f).to.include('1,2')
+ expect(h1Raw.g).to.include('[object Object]')
+ expect(h1Raw.h).to.include('undefined')
+ expect(h1Raw.i).to.include('null')
+ expect(h1Raw.j).to.include('NaN')
+ expect(h1Raw.k).to.include('true')
+ expect(h1Raw.l).to.include('false')
+ expect(h1Raw.m).to.include('test')
+ expect(h1Raw.n).to.include('1,2')
+ expect(h1Raw.n).to.include('3,4')
+
+ expect(h1Raw.z).to.be.undefined
+ })
+
+ xit('should wrap headers', () => {
+ const h1 = new Headers({
+ a: '1'
+ })
+ const h1Raw = h1.raw()
+
+ const h2 = new Headers(h1)
+ h2.set('b', '1')
+ const h2Raw = h2.raw()
+
+ const h3 = new Headers(h2)
+ h3.append('a', '2')
+ const h3Raw = h3.raw()
+
+ expect(h1Raw.a).to.include('1')
+ expect(h1Raw.a).to.not.include('2')
+
+ expect(h2Raw.a).to.include('1')
+ expect(h2Raw.a).to.not.include('2')
+ expect(h2Raw.b).to.include('1')
+
+ expect(h3Raw.a).to.include('1')
+ expect(h3Raw.a).to.include('2')
+ expect(h3Raw.b).to.include('1')
+ })
+
+ it('should accept headers as an iterable of tuples', () => {
+ let headers
+
+ headers = new Headers([
+ ['a', '1'],
+ ['b', '2'],
+ ['a', '3']
+ ])
+ expect(headers.get('a')).to.equal('1, 3')
+ expect(headers.get('b')).to.equal('2')
+
+ headers = new Headers([
+ new Set(['a', '1']),
+ ['b', '2'],
+ new Map([['a', null], ['3', null]]).keys()
+ ])
+ expect(headers.get('a')).to.equal('1, 3')
+ expect(headers.get('b')).to.equal('2')
+
+ headers = new Headers(new Map([
+ ['a', '1'],
+ ['b', '2']
+ ]))
+ expect(headers.get('a')).to.equal('1')
+ expect(headers.get('b')).to.equal('2')
+ })
+
+ it('should throw a TypeError if non-tuple exists in a headers initializer', () => {
+ expect(() => new Headers([['b', '2', 'huh?']])).to.throw(TypeError)
+ expect(() => new Headers(['b2'])).to.throw(TypeError)
+ expect(() => new Headers('b2')).to.throw(TypeError)
+ expect(() => new Headers({ [Symbol.iterator]: 42 })).to.throw(TypeError)
+ })
+
+ xit('should use a custom inspect function', () => {
+ const headers = new Headers([
+ ['Host', 'thehost'],
+ ['Host', 'notthehost'],
+ ['a', '1'],
+ ['b', '2'],
+ ['a', '3']
+ ])
+
+ // eslint-disable-next-line quotes
+ expect(format(headers)).to.equal("{ a: [ '1', '3' ], b: '2', host: 'thehost' }")
+ })
+})
diff --git a/test/node-fetch/main.js b/test/node-fetch/main.js
new file mode 100644
index 0000000..358a969
--- /dev/null
+++ b/test/node-fetch/main.js
@@ -0,0 +1,1661 @@
+/* eslint no-unused-expressions: "off" */
+/* globals AbortController */
+
+// Test tools
+const zlib = require('zlib')
+const stream = require('stream')
+const vm = require('vm')
+const chai = require('chai')
+const crypto = require('crypto')
+const chaiPromised = require('chai-as-promised')
+const chaiIterator = require('chai-iterator')
+const chaiString = require('chai-string')
+const delay = require('delay')
+const { Blob } = require('buffer')
+
+const {
+ fetch,
+ Headers,
+ Request,
+ FormData,
+ Response,
+ setGlobalDispatcher,
+ Agent
+} = require('../../index.js')
+const HeadersOrig = require('../../lib/fetch/headers.js').Headers
+const RequestOrig = require('../../lib/fetch/request.js').Request
+const ResponseOrig = require('../../lib/fetch/response.js').Response
+const TestServer = require('./utils/server.js')
+const chaiTimeout = require('./utils/chai-timeout.js')
+const { ReadableStream } = require('stream/web')
+
+function isNodeLowerThan (version) {
+ return !~process.version.localeCompare(version, undefined, { numeric: true })
+}
+
+const {
+ Uint8Array: VMUint8Array
+} = vm.runInNewContext('this')
+
+chai.use(chaiPromised)
+chai.use(chaiIterator)
+chai.use(chaiString)
+chai.use(chaiTimeout)
+const { expect } = chai
+
+describe('node-fetch', () => {
+ const local = new TestServer()
+ let base
+
+ before(async () => {
+ await local.start()
+ setGlobalDispatcher(new Agent({
+ connect: {
+ rejectUnauthorized: false
+ }
+ }))
+ base = `http://${local.hostname}:${local.port}/`
+ })
+
+ after(async () => {
+ return local.stop()
+ })
+
+ it('should return a promise', () => {
+ const url = `${base}hello`
+ const p = fetch(url)
+ expect(p).to.be.an.instanceof(Promise)
+ expect(p).to.have.property('then')
+ })
+
+ it('should expose Headers, Response and Request constructors', () => {
+ expect(Headers).to.equal(HeadersOrig)
+ expect(Response).to.equal(ResponseOrig)
+ expect(Request).to.equal(RequestOrig)
+ })
+
+ it('should support proper toString output for Headers, Response and Request objects', () => {
+ expect(new Headers().toString()).to.equal('[object Headers]')
+ expect(new Response().toString()).to.equal('[object Response]')
+ expect(new Request(base).toString()).to.equal('[object Request]')
+ })
+
+ it('should reject with error if url is protocol relative', () => {
+ const url = '//example.com/'
+ return expect(fetch(url)).to.eventually.be.rejectedWith(TypeError)
+ })
+
+ it('should reject with error if url is relative path', () => {
+ const url = '/some/path'
+ return expect(fetch(url)).to.eventually.be.rejectedWith(TypeError)
+ })
+
+ it('should reject with error if protocol is unsupported', () => {
+ const url = 'ftp://example.com/'
+ return expect(fetch(url)).to.eventually.be.rejectedWith(TypeError)
+ })
+
+ it('should reject with error on network failure', function () {
+ this.timeout(5000)
+ const url = 'http://localhost:50000/'
+ return expect(fetch(url)).to.eventually.be.rejected
+ .and.be.an.instanceOf(TypeError)
+ })
+
+ it('should resolve into response', () => {
+ const url = `${base}hello`
+ return fetch(url).then(res => {
+ expect(res).to.be.an.instanceof(Response)
+ expect(res.headers).to.be.an.instanceof(Headers)
+ expect(res.body).to.be.an.instanceof(ReadableStream)
+ expect(res.bodyUsed).to.be.false
+
+ expect(res.url).to.equal(url)
+ expect(res.ok).to.be.true
+ expect(res.status).to.equal(200)
+ expect(res.statusText).to.equal('OK')
+ })
+ })
+
+ it('Response.redirect should resolve into response', () => {
+ const res = Response.redirect('http://localhost')
+ expect(res).to.be.an.instanceof(Response)
+ expect(res.headers).to.be.an.instanceof(Headers)
+ expect(res.headers.get('location')).to.equal('http://localhost/')
+ expect(res.status).to.equal(302)
+ })
+
+ it('Response.redirect /w invalid url should fail', () => {
+ expect(() => {
+ Response.redirect('localhost')
+ }).to.throw()
+ })
+
+ it('Response.redirect /w invalid status should fail', () => {
+ expect(() => {
+ Response.redirect('http://localhost', 200)
+ }).to.throw()
+ })
+
+ it('should accept plain text response', () => {
+ const url = `${base}plain`
+ return fetch(url).then(res => {
+ expect(res.headers.get('content-type')).to.equal('text/plain')
+ return res.text().then(result => {
+ expect(res.bodyUsed).to.be.true
+ expect(result).to.be.a('string')
+ expect(result).to.equal('text')
+ })
+ })
+ })
+
+ it('should accept html response (like plain text)', () => {
+ const url = `${base}html`
+ return fetch(url).then(res => {
+ expect(res.headers.get('content-type')).to.equal('text/html')
+ return res.text().then(result => {
+ expect(res.bodyUsed).to.be.true
+ expect(result).to.be.a('string')
+ expect(result).to.equal('<html></html>')
+ })
+ })
+ })
+
+ it('should accept json response', () => {
+ const url = `${base}json`
+ return fetch(url).then(res => {
+ expect(res.headers.get('content-type')).to.equal('application/json')
+ return res.json().then(result => {
+ expect(res.bodyUsed).to.be.true
+ expect(result).to.be.an('object')
+ expect(result).to.deep.equal({ name: 'value' })
+ })
+ })
+ })
+
+ it('should send request with custom headers', () => {
+ const url = `${base}inspect`
+ const options = {
+ headers: { 'x-custom-header': 'abc' }
+ }
+ return fetch(url, options).then(res => {
+ return res.json()
+ }).then(res => {
+ expect(res.headers['x-custom-header']).to.equal('abc')
+ })
+ })
+
+ it('should send request with custom headers array', () => {
+ const url = `${base}inspect`
+ const options = {
+ headers: { 'x-custom-header': ['abc'] }
+ }
+ return fetch(url, options).then(res => {
+ return res.json()
+ }).then(res => {
+ expect(res.headers['x-custom-header']).to.equal('abc')
+ })
+ })
+
+ it('should send request with multi-valued headers', () => {
+ const url = `${base}inspect`
+ const options = {
+ headers: { 'x-custom-header': ['abc', '123'] }
+ }
+ return fetch(url, options).then(res => {
+ return res.json()
+ }).then(res => {
+ expect(res.headers['x-custom-header']).to.equal('abc,123')
+ })
+ })
+
+ it('should accept headers instance', () => {
+ const url = `${base}inspect`
+ const options = {
+ headers: new Headers({ 'x-custom-header': 'abc' })
+ }
+ return fetch(url, options).then(res => {
+ return res.json()
+ }).then(res => {
+ expect(res.headers['x-custom-header']).to.equal('abc')
+ })
+ })
+
+ it('should follow redirect code 301', () => {
+ const url = `${base}redirect/301`
+ return fetch(url).then(res => {
+ expect(res.url).to.equal(`${base}inspect`)
+ expect(res.status).to.equal(200)
+ expect(res.ok).to.be.true
+ })
+ })
+
+ it('should follow redirect code 302', () => {
+ const url = `${base}redirect/302`
+ return fetch(url).then(res => {
+ expect(res.url).to.equal(`${base}inspect`)
+ expect(res.status).to.equal(200)
+ })
+ })
+
+ it('should follow redirect code 303', () => {
+ const url = `${base}redirect/303`
+ return fetch(url).then(res => {
+ expect(res.url).to.equal(`${base}inspect`)
+ expect(res.status).to.equal(200)
+ })
+ })
+
+ it('should follow redirect code 307', () => {
+ const url = `${base}redirect/307`
+ return fetch(url).then(res => {
+ expect(res.url).to.equal(`${base}inspect`)
+ expect(res.status).to.equal(200)
+ })
+ })
+
+ it('should follow redirect code 308', () => {
+ const url = `${base}redirect/308`
+ return fetch(url).then(res => {
+ expect(res.url).to.equal(`${base}inspect`)
+ expect(res.status).to.equal(200)
+ })
+ })
+
+ it('should follow redirect chain', () => {
+ const url = `${base}redirect/chain`
+ return fetch(url).then(res => {
+ expect(res.url).to.equal(`${base}inspect`)
+ expect(res.status).to.equal(200)
+ })
+ })
+
+ it('should follow POST request redirect code 301 with GET', () => {
+ const url = `${base}redirect/301`
+ const options = {
+ method: 'POST',
+ body: 'a=1'
+ }
+ return fetch(url, options).then(res => {
+ expect(res.url).to.equal(`${base}inspect`)
+ expect(res.status).to.equal(200)
+ return res.json().then(result => {
+ expect(result.method).to.equal('GET')
+ expect(result.body).to.equal('')
+ })
+ })
+ })
+
+ it('should follow PATCH request redirect code 301 with PATCH', () => {
+ const url = `${base}redirect/301`
+ const options = {
+ method: 'PATCH',
+ body: 'a=1'
+ }
+ return fetch(url, options).then(res => {
+ expect(res.url).to.equal(`${base}inspect`)
+ expect(res.status).to.equal(200)
+ return res.json().then(res => {
+ expect(res.method).to.equal('PATCH')
+ expect(res.body).to.equal('a=1')
+ })
+ })
+ })
+
+ it('should follow POST request redirect code 302 with GET', () => {
+ const url = `${base}redirect/302`
+ const options = {
+ method: 'POST',
+ body: 'a=1'
+ }
+ return fetch(url, options).then(res => {
+ expect(res.url).to.equal(`${base}inspect`)
+ expect(res.status).to.equal(200)
+ return res.json().then(result => {
+ expect(result.method).to.equal('GET')
+ expect(result.body).to.equal('')
+ })
+ })
+ })
+
+ it('should follow PATCH request redirect code 302 with PATCH', () => {
+ const url = `${base}redirect/302`
+ const options = {
+ method: 'PATCH',
+ body: 'a=1'
+ }
+ return fetch(url, options).then(res => {
+ expect(res.url).to.equal(`${base}inspect`)
+ expect(res.status).to.equal(200)
+ return res.json().then(res => {
+ expect(res.method).to.equal('PATCH')
+ expect(res.body).to.equal('a=1')
+ })
+ })
+ })
+
+ it('should follow redirect code 303 with GET', () => {
+ const url = `${base}redirect/303`
+ const options = {
+ method: 'PUT',
+ body: 'a=1'
+ }
+ return fetch(url, options).then(res => {
+ expect(res.url).to.equal(`${base}inspect`)
+ expect(res.status).to.equal(200)
+ return res.json().then(result => {
+ expect(result.method).to.equal('GET')
+ expect(result.body).to.equal('')
+ })
+ })
+ })
+
+ it('should follow PATCH request redirect code 307 with PATCH', () => {
+ const url = `${base}redirect/307`
+ const options = {
+ method: 'PATCH',
+ body: 'a=1'
+ }
+ return fetch(url, options).then(res => {
+ expect(res.url).to.equal(`${base}inspect`)
+ expect(res.status).to.equal(200)
+ return res.json().then(result => {
+ expect(result.method).to.equal('PATCH')
+ expect(result.body).to.equal('a=1')
+ })
+ })
+ })
+
+ it('should not follow non-GET redirect if body is a readable stream', () => {
+ const url = `${base}redirect/307`
+ const options = {
+ method: 'PATCH',
+ body: stream.Readable.from('tada')
+ }
+ return expect(fetch(url, options)).to.eventually.be.rejected
+ .and.be.an.instanceOf(TypeError)
+ })
+
+ it('should obey maximum redirect, reject case', () => {
+ const url = `${base}redirect/chain/20`
+ return expect(fetch(url)).to.eventually.be.rejected
+ .and.be.an.instanceOf(TypeError)
+ })
+
+ it('should obey redirect chain, resolve case', () => {
+ const url = `${base}redirect/chain/19`
+ return fetch(url).then(res => {
+ expect(res.url).to.equal(`${base}inspect`)
+ expect(res.status).to.equal(200)
+ })
+ })
+
+ it('should support redirect mode, error flag', () => {
+ const url = `${base}redirect/301`
+ const options = {
+ redirect: 'error'
+ }
+ return expect(fetch(url, options)).to.eventually.be.rejected
+ .and.be.an.instanceOf(TypeError)
+ })
+
+ it('should support redirect mode, manual flag when there is no redirect', () => {
+ const url = `${base}hello`
+ const options = {
+ redirect: 'manual'
+ }
+ return fetch(url, options).then(res => {
+ expect(res.url).to.equal(url)
+ expect(res.status).to.equal(200)
+ expect(res.headers.get('location')).to.be.null
+ })
+ })
+
+ it('should follow redirect code 301 and keep existing headers', () => {
+ const url = `${base}redirect/301`
+ const options = {
+ headers: new Headers({ 'x-custom-header': 'abc' })
+ }
+ return fetch(url, options).then(res => {
+ expect(res.url).to.equal(`${base}inspect`)
+ return res.json()
+ }).then(res => {
+ expect(res.headers['x-custom-header']).to.equal('abc')
+ })
+ })
+
+ it('should treat broken redirect as ordinary response (follow)', () => {
+ const url = `${base}redirect/no-location`
+ return fetch(url).then(res => {
+ expect(res.url).to.equal(url)
+ expect(res.status).to.equal(301)
+ expect(res.headers.get('location')).to.be.null
+ })
+ })
+
+ it('should treat broken redirect as ordinary response (manual)', () => {
+ const url = `${base}redirect/no-location`
+ const options = {
+ redirect: 'manual'
+ }
+ return fetch(url, options).then(res => {
+ expect(res.url).to.equal(url)
+ expect(res.status).to.equal(301)
+ expect(res.headers.get('location')).to.be.null
+ })
+ })
+
+ it('should throw a TypeError on an invalid redirect option', () => {
+ const url = `${base}redirect/301`
+ const options = {
+ redirect: 'foobar'
+ }
+ return fetch(url, options).then(() => {
+ expect.fail()
+ }, error => {
+ expect(error).to.be.an.instanceOf(TypeError)
+ })
+ })
+
+ it('should set redirected property on response when redirect', () => {
+ const url = `${base}redirect/301`
+ return fetch(url).then(res => {
+ expect(res.redirected).to.be.true
+ })
+ })
+
+ it('should not set redirected property on response without redirect', () => {
+ const url = `${base}hello`
+ return fetch(url).then(res => {
+ expect(res.redirected).to.be.false
+ })
+ })
+
+ it('should handle client-error response', () => {
+ const url = `${base}error/400`
+ return fetch(url).then(res => {
+ expect(res.headers.get('content-type')).to.equal('text/plain')
+ expect(res.status).to.equal(400)
+ expect(res.statusText).to.equal('Bad Request')
+ expect(res.ok).to.be.false
+ return res.text().then(result => {
+ expect(res.bodyUsed).to.be.true
+ expect(result).to.be.a('string')
+ expect(result).to.equal('client error')
+ })
+ })
+ })
+
+ it('should handle server-error response', () => {
+ const url = `${base}error/500`
+ return fetch(url).then(res => {
+ expect(res.headers.get('content-type')).to.equal('text/plain')
+ expect(res.status).to.equal(500)
+ expect(res.statusText).to.equal('Internal Server Error')
+ expect(res.ok).to.be.false
+ return res.text().then(result => {
+ expect(res.bodyUsed).to.be.true
+ expect(result).to.be.a('string')
+ expect(result).to.equal('server error')
+ })
+ })
+ })
+
+ it('should handle network-error response', () => {
+ const url = `${base}error/reset`
+ return expect(fetch(url)).to.eventually.be.rejectedWith(TypeError)
+ })
+
+ it('should handle network-error partial response', () => {
+ const url = `${base}error/premature`
+ return fetch(url).then(res => {
+ expect(res.status).to.equal(200)
+ expect(res.ok).to.be.true
+ return expect(res.text()).to.eventually.be.rejectedWith(Error)
+ })
+ })
+
+ it('should handle network-error in chunked response async iterator', () => {
+ const url = `${base}error/premature/chunked`
+ return fetch(url).then(res => {
+ expect(res.status).to.equal(200)
+ expect(res.ok).to.be.true
+
+ const read = async body => {
+ const chunks = []
+ for await (const chunk of body) {
+ chunks.push(chunk)
+ }
+
+ return chunks
+ }
+
+ return expect(read(res.body))
+ .to.eventually.be.rejectedWith(Error)
+ })
+ })
+
+ it('should handle network-error in chunked response in consumeBody', () => {
+ const url = `${base}error/premature/chunked`
+ return fetch(url).then(res => {
+ expect(res.status).to.equal(200)
+ expect(res.ok).to.be.true
+
+ return expect(res.text()).to.eventually.be.rejectedWith(Error)
+ })
+ })
+
+ it('should handle DNS-error response', () => {
+ const url = 'http://domain.invalid'
+ return expect(fetch(url)).to.eventually.be.rejectedWith(TypeError)
+ })
+
+ it('should reject invalid json response', () => {
+ const url = `${base}error/json`
+ return fetch(url).then(res => {
+ expect(res.headers.get('content-type')).to.equal('application/json')
+ return expect(res.json()).to.eventually.be.rejectedWith(Error)
+ })
+ })
+
+ it('should handle response with no status text', () => {
+ const url = `${base}no-status-text`
+ return fetch(url).then(res => {
+ expect(res.statusText).to.equal('')
+ })
+ })
+
+ it('should handle no content response', () => {
+ const url = `${base}no-content`
+ return fetch(url).then(res => {
+ expect(res.status).to.equal(204)
+ expect(res.statusText).to.equal('No Content')
+ expect(res.ok).to.be.true
+ return res.text().then(result => {
+ expect(result).to.be.a('string')
+ expect(result).to.be.empty
+ })
+ })
+ })
+
+ it('should reject when trying to parse no content response as json', () => {
+ const url = `${base}no-content`
+ return fetch(url).then(res => {
+ expect(res.status).to.equal(204)
+ expect(res.statusText).to.equal('No Content')
+ expect(res.ok).to.be.true
+ return expect(res.json()).to.eventually.be.rejectedWith(Error)
+ })
+ })
+
+ it('should handle no content response with gzip encoding', () => {
+ const url = `${base}no-content/gzip`
+ return fetch(url).then(res => {
+ expect(res.status).to.equal(204)
+ expect(res.statusText).to.equal('No Content')
+ expect(res.headers.get('content-encoding')).to.equal('gzip')
+ expect(res.ok).to.be.true
+ return res.text().then(result => {
+ expect(result).to.be.a('string')
+ expect(result).to.be.empty
+ })
+ })
+ })
+
+ it('should handle not modified response', () => {
+ const url = `${base}not-modified`
+ return fetch(url).then(res => {
+ expect(res.status).to.equal(304)
+ expect(res.statusText).to.equal('Not Modified')
+ expect(res.ok).to.be.false
+ return res.text().then(result => {
+ expect(result).to.be.a('string')
+ expect(result).to.be.empty
+ })
+ })
+ })
+
+ it('should handle not modified response with gzip encoding', () => {
+ const url = `${base}not-modified/gzip`
+ return fetch(url).then(res => {
+ expect(res.status).to.equal(304)
+ expect(res.statusText).to.equal('Not Modified')
+ expect(res.headers.get('content-encoding')).to.equal('gzip')
+ expect(res.ok).to.be.false
+ return res.text().then(result => {
+ expect(result).to.be.a('string')
+ expect(result).to.be.empty
+ })
+ })
+ })
+
+ it('should decompress gzip response', () => {
+ const url = `${base}gzip`
+ return fetch(url).then(res => {
+ expect(res.headers.get('content-type')).to.equal('text/plain')
+ return res.text().then(result => {
+ expect(result).to.be.a('string')
+ expect(result).to.equal('hello world')
+ })
+ })
+ })
+
+ it('should decompress slightly invalid gzip response', async () => {
+ const url = `${base}gzip-truncated`
+ const res = await fetch(url)
+ expect(res.headers.get('content-type')).to.equal('text/plain')
+ const result = await res.text()
+ expect(result).to.be.a('string')
+ expect(result).to.equal('hello world')
+ })
+
+ it('should decompress deflate response', () => {
+ const url = `${base}deflate`
+ return fetch(url).then(res => {
+ expect(res.headers.get('content-type')).to.equal('text/plain')
+ return res.text().then(result => {
+ expect(result).to.be.a('string')
+ expect(result).to.equal('hello world')
+ })
+ })
+ })
+
+ xit('should decompress deflate raw response from old apache server', () => {
+ const url = `${base}deflate-raw`
+ return fetch(url).then(res => {
+ expect(res.headers.get('content-type')).to.equal('text/plain')
+ return res.text().then(result => {
+ expect(result).to.be.a('string')
+ expect(result).to.equal('hello world')
+ })
+ })
+ })
+
+ it('should decompress brotli response', function () {
+ if (typeof zlib.createBrotliDecompress !== 'function') {
+ this.skip()
+ }
+
+ const url = `${base}brotli`
+ return fetch(url).then(res => {
+ expect(res.headers.get('content-type')).to.equal('text/plain')
+ return res.text().then(result => {
+ expect(result).to.be.a('string')
+ expect(result).to.equal('hello world')
+ })
+ })
+ })
+
+ it('should handle no content response with brotli encoding', function () {
+ if (typeof zlib.createBrotliDecompress !== 'function') {
+ this.skip()
+ }
+
+ const url = `${base}no-content/brotli`
+ return fetch(url).then(res => {
+ expect(res.status).to.equal(204)
+ expect(res.statusText).to.equal('No Content')
+ expect(res.headers.get('content-encoding')).to.equal('br')
+ expect(res.ok).to.be.true
+ return res.text().then(result => {
+ expect(result).to.be.a('string')
+ expect(result).to.be.empty
+ })
+ })
+ })
+
+ it('should skip decompression if unsupported', () => {
+ const url = `${base}sdch`
+ return fetch(url).then(res => {
+ expect(res.headers.get('content-type')).to.equal('text/plain')
+ return res.text().then(result => {
+ expect(result).to.be.a('string')
+ expect(result).to.equal('fake sdch string')
+ })
+ })
+ })
+
+ it('should skip decompression if unsupported codings', () => {
+ const url = `${base}multiunsupported`
+ return fetch(url).then(res => {
+ expect(res.headers.get('content-type')).to.equal('text/plain')
+ return res.text().then(result => {
+ expect(result).to.be.a('string')
+ expect(result).to.equal('multiunsupported')
+ })
+ })
+ })
+
+ it('should decompress multiple coding', () => {
+ const url = `${base}multisupported`
+ return fetch(url).then(res => {
+ expect(res.headers.get('content-type')).to.equal('text/plain')
+ return res.text().then(result => {
+ expect(result).to.be.a('string')
+ expect(result).to.equal('hello world')
+ })
+ })
+ })
+
+ it('should reject if response compression is invalid', () => {
+ const url = `${base}invalid-content-encoding`
+ return fetch(url).then(res => {
+ expect(res.headers.get('content-type')).to.equal('text/plain')
+ return expect(res.text()).to.eventually.be.rejected
+ })
+ })
+
+ it('should handle errors on the body stream even if it is not used', done => {
+ const url = `${base}invalid-content-encoding`
+ fetch(url)
+ .then(res => {
+ expect(res.status).to.equal(200)
+ })
+ .catch(() => {})
+ .then(() => {
+ // Wait a few ms to see if a uncaught error occurs
+ setTimeout(() => {
+ done()
+ }, 20)
+ })
+ })
+
+ it('should collect handled errors on the body stream to reject if the body is used later', () => {
+ const url = `${base}invalid-content-encoding`
+ return fetch(url).then(delay(20)).then(res => {
+ expect(res.headers.get('content-type')).to.equal('text/plain')
+ return expect(res.text()).to.eventually.be.rejected
+ })
+ })
+
+ it('should not overwrite existing accept-encoding header when auto decompression is true', () => {
+ const url = `${base}inspect`
+ const options = {
+ compress: true,
+ headers: {
+ 'Accept-Encoding': 'gzip'
+ }
+ }
+ return fetch(url, options).then(res => res.json()).then(res => {
+ expect(res.headers['accept-encoding']).to.equal('gzip')
+ })
+ })
+
+ describe('AbortController', () => {
+ let controller
+
+ beforeEach(() => {
+ controller = new AbortController()
+ })
+
+ it('should support request cancellation with signal', () => {
+ const fetches = [
+ fetch(
+ `${base}timeout`,
+ {
+ method: 'POST',
+ signal: controller.signal,
+ headers: {
+ 'Content-Type': 'application/json',
+ body: JSON.stringify({ hello: 'world' })
+ }
+ }
+ )
+ ]
+
+ controller.abort()
+
+ return Promise.all(fetches.map(fetched => expect(fetched)
+ .to.eventually.be.rejected
+ .and.be.an.instanceOf(Error)
+ .and.have.property('name', 'AbortError')
+ ))
+ })
+
+ it('should support multiple request cancellation with signal', () => {
+ const fetches = [
+ fetch(`${base}timeout`, { signal: controller.signal }),
+ fetch(
+ `${base}timeout`,
+ {
+ method: 'POST',
+ signal: controller.signal,
+ headers: {
+ 'Content-Type': 'application/json',
+ body: JSON.stringify({ hello: 'world' })
+ }
+ }
+ )
+ ]
+
+ controller.abort()
+
+ return Promise.all(fetches.map(fetched => expect(fetched)
+ .to.eventually.be.rejected
+ .and.be.an.instanceOf(Error)
+ .and.have.property('name', 'AbortError')
+ ))
+ })
+
+ it('should reject immediately if signal has already been aborted', () => {
+ const url = `${base}timeout`
+ const options = {
+ signal: controller.signal
+ }
+ controller.abort()
+ const fetched = fetch(url, options)
+ return expect(fetched).to.eventually.be.rejected
+ .and.be.an.instanceOf(Error)
+ .and.have.property('name', 'AbortError')
+ })
+
+ it('should allow redirects to be aborted', () => {
+ const request = new Request(`${base}redirect/slow`, {
+ signal: controller.signal
+ })
+ setTimeout(() => {
+ controller.abort()
+ }, 20)
+ return expect(fetch(request)).to.be.eventually.rejected
+ .and.be.an.instanceOf(Error)
+ .and.have.property('name', 'AbortError')
+ })
+
+ it('should allow redirected response body to be aborted', () => {
+ const request = new Request(`${base}redirect/slow-stream`, {
+ signal: controller.signal
+ })
+ return expect(fetch(request).then(res => {
+ expect(res.headers.get('content-type')).to.equal('text/plain')
+ const result = res.text()
+ controller.abort()
+ return result
+ })).to.be.eventually.rejected
+ .and.be.an.instanceOf(Error)
+ .and.have.property('name', 'AbortError')
+ })
+
+ it('should reject response body with AbortError when aborted before stream has been read completely', () => {
+ return expect(fetch(
+ `${base}slow`,
+ { signal: controller.signal }
+ ))
+ .to.eventually.be.fulfilled
+ .then(res => {
+ const promise = res.text()
+ controller.abort()
+ return expect(promise)
+ .to.eventually.be.rejected
+ .and.be.an.instanceof(Error)
+ .and.have.property('name', 'AbortError')
+ })
+ })
+
+ it('should reject response body methods immediately with AbortError when aborted before stream is disturbed', () => {
+ return expect(fetch(
+ `${base}slow`,
+ { signal: controller.signal }
+ ))
+ .to.eventually.be.fulfilled
+ .then(res => {
+ controller.abort()
+ return expect(res.text())
+ .to.eventually.be.rejected
+ .and.be.an.instanceof(Error)
+ .and.have.property('name', 'AbortError')
+ })
+ })
+ })
+
+ it('should throw a TypeError if a signal is not of type AbortSignal or EventTarget', () => {
+ return Promise.all([
+ expect(fetch(`${base}inspect`, { signal: {} }))
+ .to.be.eventually.rejected
+ .and.be.an.instanceof(TypeError),
+ expect(fetch(`${base}inspect`, { signal: '' }))
+ .to.be.eventually.rejected
+ .and.be.an.instanceof(TypeError),
+ expect(fetch(`${base}inspect`, { signal: Object.create(null) }))
+ .to.be.eventually.rejected
+ .and.be.an.instanceof(TypeError)
+ ])
+ })
+
+ it('should gracefully handle a null signal', () => {
+ return fetch(`${base}hello`, { signal: null }).then(res => {
+ return expect(res.ok).to.be.true
+ })
+ })
+
+ it('should allow setting User-Agent', () => {
+ const url = `${base}inspect`
+ const options = {
+ headers: {
+ 'user-agent': 'faked'
+ }
+ }
+ return fetch(url, options).then(res => res.json()).then(res => {
+ expect(res.headers['user-agent']).to.equal('faked')
+ })
+ })
+
+ it('should set default Accept header', () => {
+ const url = `${base}inspect`
+ fetch(url).then(res => res.json()).then(res => {
+ expect(res.headers.accept).to.equal('*/*')
+ })
+ })
+
+ it('should allow setting Accept header', () => {
+ const url = `${base}inspect`
+ const options = {
+ headers: {
+ accept: 'application/json'
+ }
+ }
+ return fetch(url, options).then(res => res.json()).then(res => {
+ expect(res.headers.accept).to.equal('application/json')
+ })
+ })
+
+ it('should allow POST request', () => {
+ const url = `${base}inspect`
+ const options = {
+ method: 'POST'
+ }
+ return fetch(url, options).then(res => {
+ return res.json()
+ }).then(res => {
+ expect(res.method).to.equal('POST')
+ expect(res.headers['transfer-encoding']).to.be.undefined
+ expect(res.headers['content-type']).to.be.undefined
+ expect(res.headers['content-length']).to.equal('0')
+ })
+ })
+
+ it('should allow POST request with string body', () => {
+ const url = `${base}inspect`
+ const options = {
+ method: 'POST',
+ body: 'a=1'
+ }
+ return fetch(url, options).then(res => {
+ return res.json()
+ }).then(res => {
+ expect(res.method).to.equal('POST')
+ expect(res.body).to.equal('a=1')
+ expect(res.headers['transfer-encoding']).to.be.undefined
+ expect(res.headers['content-type']).to.equal('text/plain;charset=UTF-8')
+ expect(res.headers['content-length']).to.equal('3')
+ })
+ })
+
+ it('should allow POST request with buffer body', () => {
+ const url = `${base}inspect`
+ const options = {
+ method: 'POST',
+ body: Buffer.from('a=1', 'utf-8')
+ }
+ return fetch(url, options).then(res => {
+ return res.json()
+ }).then(res => {
+ expect(res.method).to.equal('POST')
+ expect(res.body).to.equal('a=1')
+ expect(res.headers['transfer-encoding']).to.be.undefined
+ expect(res.headers['content-type']).to.be.undefined
+ expect(res.headers['content-length']).to.equal('3')
+ })
+ })
+
+ it('should allow POST request with ArrayBuffer body', () => {
+ const encoder = new TextEncoder()
+ const url = `${base}inspect`
+ const options = {
+ method: 'POST',
+ body: encoder.encode('Hello, world!\n').buffer
+ }
+ return fetch(url, options).then(res => res.json()).then(res => {
+ expect(res.method).to.equal('POST')
+ expect(res.body).to.equal('Hello, world!\n')
+ expect(res.headers['transfer-encoding']).to.be.undefined
+ expect(res.headers['content-type']).to.be.undefined
+ expect(res.headers['content-length']).to.equal('14')
+ })
+ })
+
+ it('should allow POST request with ArrayBuffer body from a VM context', () => {
+ const url = `${base}inspect`
+ const options = {
+ method: 'POST',
+ body: new VMUint8Array(Buffer.from('Hello, world!\n')).buffer
+ }
+ return fetch(url, options).then(res => res.json()).then(res => {
+ expect(res.method).to.equal('POST')
+ expect(res.body).to.equal('Hello, world!\n')
+ expect(res.headers['transfer-encoding']).to.be.undefined
+ expect(res.headers['content-type']).to.be.undefined
+ expect(res.headers['content-length']).to.equal('14')
+ })
+ })
+
+ it('should allow POST request with ArrayBufferView (Uint8Array) body', () => {
+ const encoder = new TextEncoder()
+ const url = `${base}inspect`
+ const options = {
+ method: 'POST',
+ body: encoder.encode('Hello, world!\n')
+ }
+ return fetch(url, options).then(res => res.json()).then(res => {
+ expect(res.method).to.equal('POST')
+ expect(res.body).to.equal('Hello, world!\n')
+ expect(res.headers['transfer-encoding']).to.be.undefined
+ expect(res.headers['content-type']).to.be.undefined
+ expect(res.headers['content-length']).to.equal('14')
+ })
+ })
+
+ it('should allow POST request with ArrayBufferView (BigUint64Array) body', () => {
+ const encoder = new TextEncoder()
+ const url = `${base}inspect`
+ const options = {
+ method: 'POST',
+ body: new BigUint64Array(encoder.encode('0123456789abcdef').buffer)
+ }
+ return fetch(url, options).then(res => res.json()).then(res => {
+ expect(res.method).to.equal('POST')
+ expect(res.body).to.equal('0123456789abcdef')
+ expect(res.headers['transfer-encoding']).to.be.undefined
+ expect(res.headers['content-type']).to.be.undefined
+ expect(res.headers['content-length']).to.equal('16')
+ })
+ })
+
+ it('should allow POST request with ArrayBufferView (DataView) body', () => {
+ const encoder = new TextEncoder()
+ const url = `${base}inspect`
+ const options = {
+ method: 'POST',
+ body: new DataView(encoder.encode('Hello, world!\n').buffer)
+ }
+ return fetch(url, options).then(res => res.json()).then(res => {
+ expect(res.method).to.equal('POST')
+ expect(res.body).to.equal('Hello, world!\n')
+ expect(res.headers['transfer-encoding']).to.be.undefined
+ expect(res.headers['content-type']).to.be.undefined
+ expect(res.headers['content-length']).to.equal('14')
+ })
+ })
+
+ it('should allow POST request with ArrayBufferView (Uint8Array) body from a VM context', () => {
+ const url = `${base}inspect`
+ const options = {
+ method: 'POST',
+ body: new VMUint8Array(Buffer.from('Hello, world!\n'))
+ }
+ return fetch(url, options).then(res => res.json()).then(res => {
+ expect(res.method).to.equal('POST')
+ expect(res.body).to.equal('Hello, world!\n')
+ expect(res.headers['transfer-encoding']).to.be.undefined
+ expect(res.headers['content-type']).to.be.undefined
+ expect(res.headers['content-length']).to.equal('14')
+ })
+ })
+
+ it('should allow POST request with ArrayBufferView (Uint8Array, offset, length) body', () => {
+ const encoder = new TextEncoder()
+ const url = `${base}inspect`
+ const options = {
+ method: 'POST',
+ body: encoder.encode('Hello, world!\n').subarray(7, 13)
+ }
+ return fetch(url, options).then(res => res.json()).then(res => {
+ expect(res.method).to.equal('POST')
+ expect(res.body).to.equal('world!')
+ expect(res.headers['transfer-encoding']).to.be.undefined
+ expect(res.headers['content-type']).to.be.undefined
+ expect(res.headers['content-length']).to.equal('6')
+ })
+ })
+
+ it('should allow POST request with blob body without type', () => {
+ const url = `${base}inspect`
+ const options = {
+ method: 'POST',
+ body: new Blob(['a=1'])
+ }
+ return fetch(url, options).then(res => {
+ return res.json()
+ }).then(res => {
+ expect(res.method).to.equal('POST')
+ expect(res.body).to.equal('a=1')
+ expect(res.headers['transfer-encoding']).to.be.undefined
+ // expect(res.headers['content-type']).to.be.undefined
+ expect(res.headers['content-length']).to.equal('3')
+ })
+ })
+
+ it('should allow POST request with blob body with type', () => {
+ const url = `${base}inspect`
+ const options = {
+ method: 'POST',
+ body: new Blob(['a=1'], {
+ type: 'text/plain;charset=UTF-8'
+ })
+ }
+ return fetch(url, options).then(res => {
+ return res.json()
+ }).then(res => {
+ expect(res.method).to.equal('POST')
+ expect(res.body).to.equal('a=1')
+ expect(res.headers['transfer-encoding']).to.be.undefined
+ expect(res.headers['content-type']).to.equal('text/plain;charset=utf-8')
+ expect(res.headers['content-length']).to.equal('3')
+ })
+ })
+
+ it('should allow POST request with readable stream as body', () => {
+ const url = `${base}inspect`
+ const options = {
+ method: 'POST',
+ body: stream.Readable.from('a=1'),
+ duplex: 'half'
+ }
+ return fetch(url, options).then(res => {
+ return res.json()
+ }).then(res => {
+ expect(res.method).to.equal('POST')
+ expect(res.body).to.equal('a=1')
+ expect(res.headers['transfer-encoding']).to.equal('chunked')
+ expect(res.headers['content-type']).to.be.undefined
+ expect(res.headers['content-length']).to.be.undefined
+ })
+ })
+
+ it('should allow POST request with object body', () => {
+ const url = `${base}inspect`
+ // Note that fetch simply calls tostring on an object
+ const options = {
+ method: 'POST',
+ body: { a: 1 }
+ }
+ return fetch(url, options).then(res => {
+ return res.json()
+ }).then(res => {
+ expect(res.method).to.equal('POST')
+ expect(res.body).to.equal('[object Object]')
+ expect(res.headers['content-type']).to.equal('text/plain;charset=UTF-8')
+ expect(res.headers['content-length']).to.equal('15')
+ })
+ })
+
+ it('should allow POST request with form-data as body', () => {
+ const form = new FormData()
+ form.append('a', '1')
+
+ const url = `${base}multipart`
+ const options = {
+ method: 'POST',
+ body: form
+ }
+ return fetch(url, options).then(res => {
+ return res.json()
+ }).then(res => {
+ expect(res.method).to.equal('POST')
+ expect(res.headers['content-type']).to.startWith('multipart/form-data; boundary=')
+ expect(res.body).to.equal('a=1')
+ })
+ })
+
+ it('constructing a Response with URLSearchParams as body should have a Content-Type', () => {
+ const parameters = new URLSearchParams()
+ const res = new Response(parameters)
+ res.headers.get('Content-Type')
+ expect(res.headers.get('Content-Type')).to.equal('application/x-www-form-urlencoded;charset=UTF-8')
+ })
+
+ it('constructing a Request with URLSearchParams as body should have a Content-Type', () => {
+ const parameters = new URLSearchParams()
+ const request = new Request(base, { method: 'POST', body: parameters })
+ expect(request.headers.get('Content-Type')).to.equal('application/x-www-form-urlencoded;charset=UTF-8')
+ })
+
+ it('Reading a body with URLSearchParams should echo back the result', () => {
+ const parameters = new URLSearchParams()
+ parameters.append('a', '1')
+ return new Response(parameters).text().then(text => {
+ expect(text).to.equal('a=1')
+ })
+ })
+
+ // Body should been cloned...
+ it('constructing a Request/Response with URLSearchParams and mutating it should not affected body', () => {
+ const parameters = new URLSearchParams()
+ const request = new Request(`${base}inspect`, { method: 'POST', body: parameters })
+ parameters.append('a', '1')
+ return request.text().then(text => {
+ expect(text).to.equal('')
+ })
+ })
+
+ it('should allow POST request with URLSearchParams as body', () => {
+ const parameters = new URLSearchParams()
+ parameters.append('a', '1')
+
+ const url = `${base}inspect`
+ const options = {
+ method: 'POST',
+ body: parameters
+ }
+ return fetch(url, options).then(res => {
+ return res.json()
+ }).then(res => {
+ expect(res.method).to.equal('POST')
+ expect(res.headers['content-type']).to.equal('application/x-www-form-urlencoded;charset=UTF-8')
+ expect(res.headers['content-length']).to.equal('3')
+ expect(res.body).to.equal('a=1')
+ })
+ })
+
+ it('should still recognize URLSearchParams when extended', () => {
+ class CustomSearchParameters extends URLSearchParams {}
+ const parameters = new CustomSearchParameters()
+ parameters.append('a', '1')
+
+ const url = `${base}inspect`
+ const options = {
+ method: 'POST',
+ body: parameters
+ }
+ return fetch(url, options).then(res => {
+ return res.json()
+ }).then(res => {
+ expect(res.method).to.equal('POST')
+ expect(res.headers['content-type']).to.equal('application/x-www-form-urlencoded;charset=UTF-8')
+ expect(res.headers['content-length']).to.equal('3')
+ expect(res.body).to.equal('a=1')
+ })
+ })
+
+ it('should allow PUT request', () => {
+ const url = `${base}inspect`
+ const options = {
+ method: 'PUT',
+ body: 'a=1'
+ }
+ return fetch(url, options).then(res => {
+ return res.json()
+ }).then(res => {
+ expect(res.method).to.equal('PUT')
+ expect(res.body).to.equal('a=1')
+ })
+ })
+
+ it('should allow DELETE request', () => {
+ const url = `${base}inspect`
+ const options = {
+ method: 'DELETE'
+ }
+ return fetch(url, options).then(res => {
+ return res.json()
+ }).then(res => {
+ expect(res.method).to.equal('DELETE')
+ })
+ })
+
+ it('should allow DELETE request with string body', () => {
+ const url = `${base}inspect`
+ const options = {
+ method: 'DELETE',
+ body: 'a=1'
+ }
+ return fetch(url, options).then(res => {
+ return res.json()
+ }).then(res => {
+ expect(res.method).to.equal('DELETE')
+ expect(res.body).to.equal('a=1')
+ expect(res.headers['transfer-encoding']).to.be.undefined
+ expect(res.headers['content-length']).to.equal('3')
+ })
+ })
+
+ it('should allow PATCH request', () => {
+ const url = `${base}inspect`
+ const options = {
+ method: 'PATCH',
+ body: 'a=1'
+ }
+ return fetch(url, options).then(res => {
+ return res.json()
+ }).then(res => {
+ expect(res.method).to.equal('PATCH')
+ expect(res.body).to.equal('a=1')
+ })
+ })
+
+ it('should allow HEAD request', () => {
+ const url = `${base}hello`
+ const options = {
+ method: 'HEAD'
+ }
+ return fetch(url, options).then(res => {
+ expect(res.status).to.equal(200)
+ expect(res.statusText).to.equal('OK')
+ expect(res.headers.get('content-type')).to.equal('text/plain')
+ // expect(res.body).to.be.an.instanceof(stream.Transform)
+ return res.text()
+ }).then(text => {
+ expect(text).to.equal('')
+ })
+ })
+
+ it('should allow HEAD request with content-encoding header', () => {
+ const url = `${base}error/404`
+ const options = {
+ method: 'HEAD'
+ }
+ return fetch(url, options).then(res => {
+ expect(res.status).to.equal(404)
+ expect(res.headers.get('content-encoding')).to.equal('gzip')
+ return res.text()
+ }).then(text => {
+ expect(text).to.equal('')
+ })
+ })
+
+ it('should allow OPTIONS request', () => {
+ const url = `${base}options`
+ const options = {
+ method: 'OPTIONS'
+ }
+ return fetch(url, options).then(res => {
+ expect(res.status).to.equal(200)
+ expect(res.statusText).to.equal('OK')
+ expect(res.headers.get('allow')).to.equal('GET, HEAD, OPTIONS')
+ // expect(res.body).to.be.an.instanceof(stream.Transform)
+ })
+ })
+
+ it('should reject decoding body twice', () => {
+ const url = `${base}plain`
+ return fetch(url).then(res => {
+ expect(res.headers.get('content-type')).to.equal('text/plain')
+ return res.text().then(() => {
+ expect(res.bodyUsed).to.be.true
+ return expect(res.text()).to.eventually.be.rejectedWith(Error)
+ })
+ })
+ })
+
+ it('should allow cloning a json response and log it as text response', () => {
+ const url = `${base}json`
+ return fetch(url).then(res => {
+ const r1 = res.clone()
+ return Promise.all([res.json(), r1.text()]).then(results => {
+ expect(results[0]).to.deep.equal({ name: 'value' })
+ expect(results[1]).to.equal('{"name":"value"}')
+ })
+ })
+ })
+
+ it('should allow cloning a json response, and then log it as text response', () => {
+ const url = `${base}json`
+ return fetch(url).then(res => {
+ const r1 = res.clone()
+ return res.json().then(result => {
+ expect(result).to.deep.equal({ name: 'value' })
+ return r1.text().then(result => {
+ expect(result).to.equal('{"name":"value"}')
+ })
+ })
+ })
+ })
+
+ it('should allow cloning a json response, first log as text response, then return json object', () => {
+ const url = `${base}json`
+ return fetch(url).then(res => {
+ const r1 = res.clone()
+ return r1.text().then(result => {
+ expect(result).to.equal('{"name":"value"}')
+ return res.json().then(result => {
+ expect(result).to.deep.equal({ name: 'value' })
+ })
+ })
+ })
+ })
+
+ it('should not allow cloning a response after its been used', () => {
+ const url = `${base}hello`
+ return fetch(url).then(res =>
+ res.text().then(() => {
+ expect(() => {
+ res.clone()
+ }).to.throw(Error)
+ })
+ )
+ })
+
+ xit('should timeout on cloning response without consuming one of the streams when the second packet size is equal default highWaterMark', function () {
+ this.timeout(300)
+ const url = local.mockState(res => {
+ // Observed behavior of TCP packets splitting:
+ // - response body size <= 65438 → single packet sent
+ // - response body size > 65438 → multiple packets sent
+ // Max TCP packet size is 64kB (http://stackoverflow.com/a/2614188/5763764),
+ // but first packet probably transfers more than the response body.
+ const firstPacketMaxSize = 65438
+ const secondPacketSize = 16 * 1024 // = defaultHighWaterMark
+ res.end(crypto.randomBytes(firstPacketMaxSize + secondPacketSize))
+ })
+ return expect(
+ fetch(url).then(res => res.clone().buffer())
+ ).to.timeout
+ })
+
+ xit('should timeout on cloning response without consuming one of the streams when the second packet size is equal custom highWaterMark', function () {
+ this.timeout(300)
+ const url = local.mockState(res => {
+ const firstPacketMaxSize = 65438
+ const secondPacketSize = 10
+ res.end(crypto.randomBytes(firstPacketMaxSize + secondPacketSize))
+ })
+ return expect(
+ fetch(url, { highWaterMark: 10 }).then(res => res.clone().buffer())
+ ).to.timeout
+ })
+
+ xit('should not timeout on cloning response without consuming one of the streams when the second packet size is less than default highWaterMark', function () {
+ // TODO: fix test.
+ if (!isNodeLowerThan('v16.0.0')) {
+ this.skip()
+ }
+
+ this.timeout(300)
+ const url = local.mockState(res => {
+ const firstPacketMaxSize = 65438
+ const secondPacketSize = 16 * 1024 // = defaultHighWaterMark
+ res.end(crypto.randomBytes(firstPacketMaxSize + secondPacketSize - 1))
+ })
+ return expect(
+ fetch(url).then(res => res.clone().buffer())
+ ).not.to.timeout
+ })
+
+ xit('should not timeout on cloning response without consuming one of the streams when the second packet size is less than custom highWaterMark', function () {
+ // TODO: fix test.
+ if (!isNodeLowerThan('v16.0.0')) {
+ this.skip()
+ }
+
+ this.timeout(300)
+ const url = local.mockState(res => {
+ const firstPacketMaxSize = 65438
+ const secondPacketSize = 10
+ res.end(crypto.randomBytes(firstPacketMaxSize + secondPacketSize - 1))
+ })
+ return expect(
+ fetch(url, { highWaterMark: 10 }).then(res => res.clone().buffer())
+ ).not.to.timeout
+ })
+
+ xit('should not timeout on cloning response without consuming one of the streams when the response size is double the custom large highWaterMark - 1', function () {
+ // TODO: fix test.
+ if (!isNodeLowerThan('v16.0.0')) {
+ this.skip()
+ }
+
+ this.timeout(300)
+ const url = local.mockState(res => {
+ res.end(crypto.randomBytes((2 * 512 * 1024) - 1))
+ })
+ return expect(
+ fetch(url, { highWaterMark: 512 * 1024 }).then(res => res.clone().buffer())
+ ).not.to.timeout
+ })
+
+ xit('should allow get all responses of a header', () => {
+ // TODO: fix test.
+ const url = `${base}cookie`
+ return fetch(url).then(res => {
+ const expected = 'a=1, b=1'
+ expect(res.headers.get('set-cookie')).to.equal(expected)
+ expect(res.headers.get('Set-Cookie')).to.equal(expected)
+ })
+ })
+
+ it('should support fetch with Request instance', () => {
+ const url = `${base}hello`
+ const request = new Request(url)
+ return fetch(request).then(res => {
+ expect(res.url).to.equal(url)
+ expect(res.ok).to.be.true
+ expect(res.status).to.equal(200)
+ })
+ })
+
+ it('should support fetch with Node.js URL object', () => {
+ const url = `${base}hello`
+ const urlObject = new URL(url)
+ const request = new Request(urlObject)
+ return fetch(request).then(res => {
+ expect(res.url).to.equal(url)
+ expect(res.ok).to.be.true
+ expect(res.status).to.equal(200)
+ })
+ })
+
+ it('should support fetch with WHATWG URL object', () => {
+ const url = `${base}hello`
+ const urlObject = new URL(url)
+ const request = new Request(urlObject)
+ return fetch(request).then(res => {
+ expect(res.url).to.equal(url)
+ expect(res.ok).to.be.true
+ expect(res.status).to.equal(200)
+ })
+ })
+
+ it('if params are given, do not modify anything', () => {
+ const url = `${base}question?a=1`
+ const urlObject = new URL(url)
+ const request = new Request(urlObject)
+ return fetch(request).then(res => {
+ expect(res.url).to.equal(url)
+ expect(res.ok).to.be.true
+ expect(res.status).to.equal(200)
+ })
+ })
+
+ it('should support reading blob as text', () => {
+ return new Response('hello')
+ .blob()
+ .then(blob => blob.text())
+ .then(body => {
+ expect(body).to.equal('hello')
+ })
+ })
+
+ it('should support reading blob as arrayBuffer', () => {
+ return new Response('hello')
+ .blob()
+ .then(blob => blob.arrayBuffer())
+ .then(ab => {
+ const string = String.fromCharCode.apply(null, new Uint8Array(ab))
+ expect(string).to.equal('hello')
+ })
+ })
+
+ it('should support blob round-trip', () => {
+ const url = `${base}hello`
+
+ let length
+ let type
+
+ return fetch(url).then(res => res.blob()).then(async blob => {
+ const url = `${base}inspect`
+ length = blob.size
+ type = blob.type
+ return fetch(url, {
+ method: 'POST',
+ body: blob
+ })
+ }).then(res => res.json()).then(({ body, headers }) => {
+ expect(body).to.equal('world')
+ expect(headers['content-type']).to.equal(type)
+ expect(headers['content-length']).to.equal(String(length))
+ })
+ })
+
+ it('should support overwrite Request instance', () => {
+ const url = `${base}inspect`
+ const request = new Request(url, {
+ method: 'POST',
+ headers: {
+ a: '1'
+ }
+ })
+ return fetch(request, {
+ method: 'GET',
+ headers: {
+ a: '2'
+ }
+ }).then(res => {
+ return res.json()
+ }).then(body => {
+ expect(body.method).to.equal('GET')
+ expect(body.headers.a).to.equal('2')
+ })
+ })
+
+ it('should support http request', function () {
+ this.timeout(5000)
+ const url = 'https://github.com/'
+ const options = {
+ method: 'HEAD'
+ }
+ return fetch(url, options).then(res => {
+ expect(res.status).to.equal(200)
+ expect(res.ok).to.be.true
+ })
+ })
+
+ it('should encode URLs as UTF-8', async () => {
+ const url = `${base}möbius`
+ const res = await fetch(url)
+ expect(res.url).to.equal(`${base}m%C3%B6bius`)
+ })
+
+ it('should allow manual redirect handling', function () {
+ this.timeout(5000)
+ const url = `${base}redirect/302`
+ const options = {
+ redirect: 'manual'
+ }
+ return fetch(url, options).then(res => {
+ expect(res.status).to.equal(302)
+ expect(res.url).to.equal(url)
+ expect(res.type).to.equal('basic')
+ expect(res.headers.get('Location')).to.equal('/inspect')
+ expect(res.ok).to.be.false
+ })
+ })
+})
diff --git a/test/node-fetch/mock.js b/test/node-fetch/mock.js
new file mode 100644
index 0000000..a53f464
--- /dev/null
+++ b/test/node-fetch/mock.js
@@ -0,0 +1,112 @@
+/* eslint no-unused-expressions: "off" */
+
+// Test tools
+const chai = require('chai')
+
+const {
+ fetch,
+ MockAgent,
+ setGlobalDispatcher,
+ Headers
+} = require('../../index.js')
+
+const { expect } = chai
+
+describe('node-fetch with MockAgent', () => {
+ it('should match the url', async () => {
+ const mockAgent = new MockAgent()
+ setGlobalDispatcher(mockAgent)
+ const mockPool = mockAgent.get('http://localhost:3000')
+
+ mockPool
+ .intercept({
+ path: '/test',
+ method: 'GET'
+ })
+ .reply(200, { success: true })
+ .persist()
+
+ const res = await fetch('http://localhost:3000/test', {
+ method: 'GET'
+ })
+
+ expect(res.status).to.equal(200)
+ expect(await res.json()).to.deep.equal({ success: true })
+ })
+
+ it('should match the body', async () => {
+ const mockAgent = new MockAgent()
+ setGlobalDispatcher(mockAgent)
+ const mockPool = mockAgent.get('http://localhost:3000')
+
+ mockPool
+ .intercept({
+ path: '/test',
+ method: 'POST',
+ body: (value) => {
+ return value === 'request body'
+ }
+ })
+ .reply(200, { success: true })
+ .persist()
+
+ const res = await fetch('http://localhost:3000/test', {
+ method: 'POST',
+ body: 'request body'
+ })
+
+ expect(res.status).to.equal(200)
+ expect(await res.json()).to.deep.equal({ success: true })
+ })
+
+ it('should match the headers', async () => {
+ const mockAgent = new MockAgent()
+ setGlobalDispatcher(mockAgent)
+ const mockPool = mockAgent.get('http://localhost:3000')
+
+ mockPool
+ .intercept({
+ path: '/test',
+ method: 'GET',
+ headers: (h) => {
+ return h['user-agent'] === 'undici'
+ }
+ })
+ .reply(200, { success: true })
+ .persist()
+
+ const res = await fetch('http://localhost:3000/test', {
+ method: 'GET',
+ headers: new Headers({ 'User-Agent': 'undici' })
+ })
+ expect(res.status).to.equal(200)
+ expect(await res.json()).to.deep.equal({ success: true })
+ })
+
+ it('should match the headers with a matching function', async () => {
+ const mockAgent = new MockAgent()
+ setGlobalDispatcher(mockAgent)
+ const mockPool = mockAgent.get('http://localhost:3000')
+
+ mockPool
+ .intercept({
+ path: '/test',
+ method: 'GET',
+ headers (headers) {
+ expect(headers).to.be.an('object')
+ expect(headers).to.have.property('user-agent', 'undici')
+ return true
+ }
+ })
+ .reply(200, { success: true })
+ .persist()
+
+ const res = await fetch('http://localhost:3000/test', {
+ method: 'GET',
+ headers: new Headers({ 'User-Agent': 'undici' })
+ })
+
+ expect(res.status).to.equal(200)
+ expect(await res.json()).to.deep.equal({ success: true })
+ })
+})
diff --git a/test/node-fetch/request.js b/test/node-fetch/request.js
new file mode 100644
index 0000000..2d29d51
--- /dev/null
+++ b/test/node-fetch/request.js
@@ -0,0 +1,281 @@
+const stream = require('stream')
+const http = require('http')
+
+const chai = require('chai')
+const { Blob } = require('buffer')
+
+const Request = require('../../lib/fetch/request.js').Request
+const TestServer = require('./utils/server.js')
+
+const { expect } = chai
+
+describe('Request', () => {
+ const local = new TestServer()
+ let base
+
+ before(async () => {
+ await local.start()
+ base = `http://${local.hostname}:${local.port}/`
+ })
+
+ after(async () => {
+ return local.stop()
+ })
+
+ it('should have attributes conforming to Web IDL', () => {
+ const request = new Request('http://github.com/')
+ const enumerableProperties = []
+ for (const property in request) {
+ enumerableProperties.push(property)
+ }
+
+ for (const toCheck of [
+ 'body',
+ 'bodyUsed',
+ 'arrayBuffer',
+ 'blob',
+ 'json',
+ 'text',
+ 'method',
+ 'url',
+ 'headers',
+ 'redirect',
+ 'clone',
+ 'signal'
+ ]) {
+ expect(enumerableProperties).to.contain(toCheck)
+ }
+
+ // for (const toCheck of [
+ // 'body', 'bodyUsed', 'method', 'url', 'headers', 'redirect', 'signal'
+ // ]) {
+ // expect(() => {
+ // request[toCheck] = 'abc'
+ // }).to.throw()
+ // }
+ })
+
+ // it('should support wrapping Request instance', () => {
+ // const url = `${base}hello`
+
+ // const form = new FormData()
+ // form.append('a', '1')
+ // const { signal } = new AbortController()
+
+ // const r1 = new Request(url, {
+ // method: 'POST',
+ // follow: 1,
+ // body: form,
+ // signal
+ // })
+ // const r2 = new Request(r1, {
+ // follow: 2
+ // })
+
+ // expect(r2.url).to.equal(url)
+ // expect(r2.method).to.equal('POST')
+ // expect(r2.signal).to.equal(signal)
+ // // Note that we didn't clone the body
+ // expect(r2.body).to.equal(form)
+ // expect(r1.follow).to.equal(1)
+ // expect(r2.follow).to.equal(2)
+ // expect(r1.counter).to.equal(0)
+ // expect(r2.counter).to.equal(0)
+ // })
+
+ xit('should override signal on derived Request instances', () => {
+ const parentAbortController = new AbortController()
+ const derivedAbortController = new AbortController()
+ const parentRequest = new Request(`${base}hello`, {
+ signal: parentAbortController.signal
+ })
+ const derivedRequest = new Request(parentRequest, {
+ signal: derivedAbortController.signal
+ })
+ expect(parentRequest.signal).to.equal(parentAbortController.signal)
+ expect(derivedRequest.signal).to.equal(derivedAbortController.signal)
+ })
+
+ xit('should allow removing signal on derived Request instances', () => {
+ const parentAbortController = new AbortController()
+ const parentRequest = new Request(`${base}hello`, {
+ signal: parentAbortController.signal
+ })
+ const derivedRequest = new Request(parentRequest, {
+ signal: null
+ })
+ expect(parentRequest.signal).to.equal(parentAbortController.signal)
+ expect(derivedRequest.signal).to.equal(null)
+ })
+
+ it('should throw error with GET/HEAD requests with body', () => {
+ expect(() => new Request(base, { body: '' }))
+ .to.throw(TypeError)
+ expect(() => new Request(base, { body: 'a' }))
+ .to.throw(TypeError)
+ expect(() => new Request(base, { body: '', method: 'HEAD' }))
+ .to.throw(TypeError)
+ expect(() => new Request(base, { body: 'a', method: 'HEAD' }))
+ .to.throw(TypeError)
+ expect(() => new Request(base, { body: 'a', method: 'get' }))
+ .to.throw(TypeError)
+ expect(() => new Request(base, { body: 'a', method: 'head' }))
+ .to.throw(TypeError)
+ })
+
+ it('should default to null as body', () => {
+ const request = new Request(base)
+ expect(request.body).to.equal(null)
+ return request.text().then(result => expect(result).to.equal(''))
+ })
+
+ it('should support parsing headers', () => {
+ const url = base
+ const request = new Request(url, {
+ headers: {
+ a: '1'
+ }
+ })
+ expect(request.url).to.equal(url)
+ expect(request.headers.get('a')).to.equal('1')
+ })
+
+ it('should support arrayBuffer() method', () => {
+ const url = base
+ const request = new Request(url, {
+ method: 'POST',
+ body: 'a=1'
+ })
+ expect(request.url).to.equal(url)
+ return request.arrayBuffer().then(result => {
+ expect(result).to.be.an.instanceOf(ArrayBuffer)
+ const string = String.fromCharCode.apply(null, new Uint8Array(result))
+ expect(string).to.equal('a=1')
+ })
+ })
+
+ it('should support text() method', () => {
+ const url = base
+ const request = new Request(url, {
+ method: 'POST',
+ body: 'a=1'
+ })
+ expect(request.url).to.equal(url)
+ return request.text().then(result => {
+ expect(result).to.equal('a=1')
+ })
+ })
+
+ it('should support json() method', () => {
+ const url = base
+ const request = new Request(url, {
+ method: 'POST',
+ body: '{"a":1}'
+ })
+ expect(request.url).to.equal(url)
+ return request.json().then(result => {
+ expect(result.a).to.equal(1)
+ })
+ })
+
+ it('should support blob() method', () => {
+ const url = base
+ const request = new Request(url, {
+ method: 'POST',
+ body: Buffer.from('a=1')
+ })
+ expect(request.url).to.equal(url)
+ return request.blob().then(result => {
+ expect(result).to.be.an.instanceOf(Blob)
+ expect(result.size).to.equal(3)
+ expect(result.type).to.equal('')
+ })
+ })
+
+ it('should support clone() method', () => {
+ const url = base
+ const body = stream.Readable.from('a=1')
+ const agent = new http.Agent()
+ const { signal } = new AbortController()
+ const request = new Request(url, {
+ body,
+ method: 'POST',
+ redirect: 'manual',
+ headers: {
+ b: '2'
+ },
+ follow: 3,
+ compress: false,
+ agent,
+ signal,
+ duplex: 'half'
+ })
+ const cl = request.clone()
+ expect(cl.url).to.equal(url)
+ expect(cl.method).to.equal('POST')
+ expect(cl.redirect).to.equal('manual')
+ expect(cl.headers.get('b')).to.equal('2')
+ expect(cl.method).to.equal('POST')
+ // Clone body shouldn't be the same body
+ expect(cl.body).to.not.equal(body)
+ return Promise.all([cl.text(), request.text()]).then(results => {
+ expect(results[0]).to.equal('a=1')
+ expect(results[1]).to.equal('a=1')
+ })
+ })
+
+ it('should support ArrayBuffer as body', () => {
+ const encoder = new TextEncoder()
+ const body = encoder.encode('a=12345678901234').buffer
+ const request = new Request(base, {
+ method: 'POST',
+ body
+ })
+ new Uint8Array(body)[0] = 0
+ return request.text().then(result => {
+ expect(result).to.equal('a=12345678901234')
+ })
+ })
+
+ it('should support Uint8Array as body', () => {
+ const encoder = new TextEncoder()
+ const fullbuffer = encoder.encode('a=12345678901234').buffer
+ const body = new Uint8Array(fullbuffer, 2, 9)
+ const request = new Request(base, {
+ method: 'POST',
+ body
+ })
+ body[0] = 0
+ return request.text().then(result => {
+ expect(result).to.equal('123456789')
+ })
+ })
+
+ it('should support BigUint64Array as body', () => {
+ const encoder = new TextEncoder()
+ const fullbuffer = encoder.encode('a=12345678901234').buffer
+ const body = new BigUint64Array(fullbuffer, 8, 1)
+ const request = new Request(base, {
+ method: 'POST',
+ body
+ })
+ body[0] = 0n
+ return request.text().then(result => {
+ expect(result).to.equal('78901234')
+ })
+ })
+
+ it('should support DataView as body', () => {
+ const encoder = new TextEncoder()
+ const fullbuffer = encoder.encode('a=12345678901234').buffer
+ const body = new Uint8Array(fullbuffer, 2, 9)
+ const request = new Request(base, {
+ method: 'POST',
+ body
+ })
+ body[0] = 0
+ return request.text().then(result => {
+ expect(result).to.equal('123456789')
+ })
+ })
+})
diff --git a/test/node-fetch/response.js b/test/node-fetch/response.js
new file mode 100644
index 0000000..4bb7c42
--- /dev/null
+++ b/test/node-fetch/response.js
@@ -0,0 +1,251 @@
+/* eslint no-unused-expressions: "off" */
+
+const chai = require('chai')
+const stream = require('stream')
+const { Response } = require('../../lib/fetch/response.js')
+const TestServer = require('./utils/server.js')
+const { Blob } = require('buffer')
+const { kState } = require('../../lib/fetch/symbols.js')
+
+const { expect } = chai
+
+describe('Response', () => {
+ const local = new TestServer()
+ let base
+
+ before(async () => {
+ await local.start()
+ base = `http://${local.hostname}:${local.port}/`
+ })
+
+ after(async () => {
+ return local.stop()
+ })
+
+ it('should have attributes conforming to Web IDL', () => {
+ const res = new Response()
+ const enumerableProperties = []
+ for (const property in res) {
+ enumerableProperties.push(property)
+ }
+
+ for (const toCheck of [
+ 'body',
+ 'bodyUsed',
+ 'arrayBuffer',
+ 'blob',
+ 'json',
+ 'text',
+ 'type',
+ 'url',
+ 'status',
+ 'ok',
+ 'redirected',
+ 'statusText',
+ 'headers',
+ 'clone'
+ ]) {
+ expect(enumerableProperties).to.contain(toCheck)
+ }
+
+ // TODO
+ // for (const toCheck of [
+ // 'body',
+ // 'bodyUsed',
+ // 'type',
+ // 'url',
+ // 'status',
+ // 'ok',
+ // 'redirected',
+ // 'statusText',
+ // 'headers'
+ // ]) {
+ // expect(() => {
+ // res[toCheck] = 'abc'
+ // }).to.throw()
+ // }
+ })
+
+ it('should support empty options', () => {
+ const res = new Response(stream.Readable.from('a=1'))
+ return res.text().then(result => {
+ expect(result).to.equal('a=1')
+ })
+ })
+
+ it('should support parsing headers', () => {
+ const res = new Response(null, {
+ headers: {
+ a: '1'
+ }
+ })
+ expect(res.headers.get('a')).to.equal('1')
+ })
+
+ it('should support text() method', () => {
+ const res = new Response('a=1')
+ return res.text().then(result => {
+ expect(result).to.equal('a=1')
+ })
+ })
+
+ it('should support json() method', () => {
+ const res = new Response('{"a":1}')
+ return res.json().then(result => {
+ expect(result.a).to.equal(1)
+ })
+ })
+
+ if (Blob) {
+ it('should support blob() method', () => {
+ const res = new Response('a=1', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'text/plain'
+ }
+ })
+ return res.blob().then(result => {
+ expect(result).to.be.an.instanceOf(Blob)
+ expect(result.size).to.equal(3)
+ expect(result.type).to.equal('text/plain')
+ })
+ })
+ }
+
+ it('should support clone() method', () => {
+ const body = stream.Readable.from('a=1')
+ const res = new Response(body, {
+ headers: {
+ a: '1'
+ },
+ status: 346,
+ statusText: 'production'
+ })
+ res[kState].urlList = [new URL(base)]
+ const cl = res.clone()
+ expect(cl.headers.get('a')).to.equal('1')
+ expect(cl.type).to.equal('default')
+ expect(cl.url).to.equal(base)
+ expect(cl.status).to.equal(346)
+ expect(cl.statusText).to.equal('production')
+ expect(cl.ok).to.be.false
+ // Clone body shouldn't be the same body
+ expect(cl.body).to.not.equal(body)
+ return Promise.all([cl.text(), res.text()]).then(results => {
+ expect(results[0]).to.equal('a=1')
+ expect(results[1]).to.equal('a=1')
+ })
+ })
+
+ it('should support stream as body', () => {
+ const body = stream.Readable.from('a=1')
+ const res = new Response(body)
+ return res.text().then(result => {
+ expect(result).to.equal('a=1')
+ })
+ })
+
+ it('should support string as body', () => {
+ const res = new Response('a=1')
+ return res.text().then(result => {
+ expect(result).to.equal('a=1')
+ })
+ })
+
+ it('should support buffer as body', () => {
+ const res = new Response(Buffer.from('a=1'))
+ return res.text().then(result => {
+ expect(result).to.equal('a=1')
+ })
+ })
+
+ it('should support ArrayBuffer as body', () => {
+ const encoder = new TextEncoder()
+ const fullbuffer = encoder.encode('a=12345678901234').buffer
+ const res = new Response(fullbuffer)
+ new Uint8Array(fullbuffer)[0] = 0
+ return res.text().then(result => {
+ expect(result).to.equal('a=12345678901234')
+ })
+ })
+
+ it('should support blob as body', async () => {
+ const res = new Response(new Blob(['a=1']))
+ return res.text().then(result => {
+ expect(result).to.equal('a=1')
+ })
+ })
+
+ it('should support Uint8Array as body', () => {
+ const encoder = new TextEncoder()
+ const fullbuffer = encoder.encode('a=12345678901234').buffer
+ const body = new Uint8Array(fullbuffer, 2, 9)
+ const res = new Response(body)
+ body[0] = 0
+ return res.text().then(result => {
+ expect(result).to.equal('123456789')
+ })
+ })
+
+ it('should support BigUint64Array as body', () => {
+ const encoder = new TextEncoder()
+ const fullbuffer = encoder.encode('a=12345678901234').buffer
+ const body = new BigUint64Array(fullbuffer, 8, 1)
+ const res = new Response(body)
+ body[0] = 0n
+ return res.text().then(result => {
+ expect(result).to.equal('78901234')
+ })
+ })
+
+ it('should support DataView as body', () => {
+ const encoder = new TextEncoder()
+ const fullbuffer = encoder.encode('a=12345678901234').buffer
+ const body = new Uint8Array(fullbuffer, 2, 9)
+ const res = new Response(body)
+ body[0] = 0
+ return res.text().then(result => {
+ expect(result).to.equal('123456789')
+ })
+ })
+
+ it('should default to null as body', () => {
+ const res = new Response()
+ expect(res.body).to.equal(null)
+
+ return res.text().then(result => expect(result).to.equal(''))
+ })
+
+ it('should default to 200 as status code', () => {
+ const res = new Response(null)
+ expect(res.status).to.equal(200)
+ })
+
+ it('should default to empty string as url', () => {
+ const res = new Response()
+ expect(res.url).to.equal('')
+ })
+
+ it('should support error() static method', () => {
+ const res = Response.error()
+ expect(res).to.be.an.instanceof(Response)
+ expect(res.type).to.equal('error')
+ expect(res.status).to.equal(0)
+ expect(res.statusText).to.equal('')
+ })
+
+ it('should support undefined status', () => {
+ const res = new Response(null, { status: undefined })
+ expect(res.status).to.equal(200)
+ })
+
+ it('should support undefined statusText', () => {
+ const res = new Response(null, { statusText: undefined })
+ expect(res.statusText).to.equal('')
+ })
+
+ it('should not set bodyUsed to undefined', () => {
+ const res = new Response()
+ expect(res.bodyUsed).to.be.false
+ })
+})
diff --git a/test/node-fetch/utils/chai-timeout.js b/test/node-fetch/utils/chai-timeout.js
new file mode 100644
index 0000000..6838a4c
--- /dev/null
+++ b/test/node-fetch/utils/chai-timeout.js
@@ -0,0 +1,15 @@
+const pTimeout = require('p-timeout')
+
+module.exports = ({ Assertion }, utils) => {
+ utils.addProperty(Assertion.prototype, 'timeout', async function () {
+ let timeouted = false
+ await pTimeout(this._obj, 150, () => {
+ timeouted = true
+ })
+ return this.assert(
+ timeouted,
+ 'expected promise to timeout but it was resolved',
+ 'expected promise not to timeout but it timed out'
+ )
+ })
+}
diff --git a/test/node-fetch/utils/dummy.txt b/test/node-fetch/utils/dummy.txt
new file mode 100644
index 0000000..5ca5191
--- /dev/null
+++ b/test/node-fetch/utils/dummy.txt
@@ -0,0 +1 @@
+i am a dummy \ No newline at end of file
diff --git a/test/node-fetch/utils/read-stream.js b/test/node-fetch/utils/read-stream.js
new file mode 100644
index 0000000..7d79153
--- /dev/null
+++ b/test/node-fetch/utils/read-stream.js
@@ -0,0 +1,9 @@
+module.exports = async function readStream (stream) {
+ const chunks = []
+
+ for await (const chunk of stream) {
+ chunks.push(chunk instanceof Buffer ? chunk : Buffer.from(chunk))
+ }
+
+ return Buffer.concat(chunks)
+}
diff --git a/test/node-fetch/utils/server.js b/test/node-fetch/utils/server.js
new file mode 100644
index 0000000..46dc983
--- /dev/null
+++ b/test/node-fetch/utils/server.js
@@ -0,0 +1,467 @@
+const http = require('http')
+const zlib = require('zlib')
+const { once } = require('events')
+const Busboy = require('@fastify/busboy')
+
+module.exports = class TestServer {
+ constructor () {
+ this.server = http.createServer(this.router)
+ // Node 8 default keepalive timeout is 5000ms
+ // make it shorter here as we want to close server quickly at the end of tests
+ this.server.keepAliveTimeout = 1000
+ this.server.on('error', err => {
+ console.log(err.stack)
+ })
+ this.server.on('connection', socket => {
+ socket.setTimeout(1500)
+ })
+ }
+
+ async start () {
+ this.server.listen(0, 'localhost')
+ return once(this.server, 'listening')
+ }
+
+ async stop () {
+ this.server.close()
+ return once(this.server, 'close')
+ }
+
+ get port () {
+ return this.server.address().port
+ }
+
+ get hostname () {
+ return 'localhost'
+ }
+
+ mockState (responseHandler) {
+ this.server.nextResponseHandler = responseHandler
+ return `http://${this.hostname}:${this.port}/mocked`
+ }
+
+ router (request, res) {
+ const p = request.url
+
+ if (p === '/mocked') {
+ if (this.nextResponseHandler) {
+ this.nextResponseHandler(res)
+ this.nextResponseHandler = undefined
+ } else {
+ throw new Error('No mocked response. Use ’TestServer.mockState()’.')
+ }
+ }
+
+ if (p === '/hello') {
+ res.statusCode = 200
+ res.setHeader('Content-Type', 'text/plain')
+ res.end('world')
+ }
+
+ if (p.includes('question')) {
+ res.statusCode = 200
+ res.setHeader('Content-Type', 'text/plain')
+ res.end('ok')
+ }
+
+ if (p === '/plain') {
+ res.statusCode = 200
+ res.setHeader('Content-Type', 'text/plain')
+ res.end('text')
+ }
+
+ if (p === '/no-status-text') {
+ res.writeHead(200, '', {}).end()
+ }
+
+ if (p === '/options') {
+ res.statusCode = 200
+ res.setHeader('Allow', 'GET, HEAD, OPTIONS')
+ res.end('hello world')
+ }
+
+ if (p === '/html') {
+ res.statusCode = 200
+ res.setHeader('Content-Type', 'text/html')
+ res.end('<html></html>')
+ }
+
+ if (p === '/json') {
+ res.statusCode = 200
+ res.setHeader('Content-Type', 'application/json')
+ res.end(JSON.stringify({
+ name: 'value'
+ }))
+ }
+
+ if (p === '/gzip') {
+ res.statusCode = 200
+ res.setHeader('Content-Type', 'text/plain')
+ res.setHeader('Content-Encoding', 'gzip')
+ zlib.gzip('hello world', (err, buffer) => {
+ if (err) {
+ throw err
+ }
+
+ res.end(buffer)
+ })
+ }
+
+ if (p === '/gzip-truncated') {
+ res.statusCode = 200
+ res.setHeader('Content-Type', 'text/plain')
+ res.setHeader('Content-Encoding', 'gzip')
+ zlib.gzip('hello world', (err, buffer) => {
+ if (err) {
+ throw err
+ }
+
+ // Truncate the CRC checksum and size check at the end of the stream
+ res.end(buffer.slice(0, -8))
+ })
+ }
+
+ if (p === '/gzip-capital') {
+ res.statusCode = 200
+ res.setHeader('Content-Type', 'text/plain')
+ res.setHeader('Content-Encoding', 'GZip')
+ zlib.gzip('hello world', (err, buffer) => {
+ if (err) {
+ throw err
+ }
+
+ res.end(buffer)
+ })
+ }
+
+ if (p === '/deflate') {
+ res.statusCode = 200
+ res.setHeader('Content-Type', 'text/plain')
+ res.setHeader('Content-Encoding', 'deflate')
+ zlib.deflate('hello world', (err, buffer) => {
+ if (err) {
+ throw err
+ }
+
+ res.end(buffer)
+ })
+ }
+
+ if (p === '/brotli') {
+ res.statusCode = 200
+ res.setHeader('Content-Type', 'text/plain')
+ if (typeof zlib.createBrotliDecompress === 'function') {
+ res.setHeader('Content-Encoding', 'br')
+ zlib.brotliCompress('hello world', (err, buffer) => {
+ if (err) {
+ throw err
+ }
+
+ res.end(buffer)
+ })
+ }
+ }
+
+ if (p === '/multiunsupported') {
+ res.statusCode = 200
+ res.setHeader('Content-Type', 'text/plain')
+ if (typeof zlib.createBrotliDecompress === 'function') {
+ res.setHeader('Content-Encoding', 'br,asd,br')
+ res.end('multiunsupported')
+ }
+ }
+
+ if (p === '/multisupported') {
+ res.statusCode = 200
+ res.setHeader('Content-Type', 'text/plain')
+ if (typeof zlib.createBrotliDecompress === 'function') {
+ res.setHeader('Content-Encoding', 'br,br')
+ zlib.brotliCompress('hello world', (err, buffer) => {
+ if (err) {
+ throw err
+ }
+
+ zlib.brotliCompress(buffer, (err, buffer) => {
+ if (err) {
+ throw err
+ }
+
+ res.end(buffer)
+ })
+ })
+ }
+ }
+
+ if (p === '/deflate-raw') {
+ res.statusCode = 200
+ res.setHeader('Content-Type', 'text/plain')
+ res.setHeader('Content-Encoding', 'deflate')
+ zlib.deflateRaw('hello world', (err, buffer) => {
+ if (err) {
+ throw err
+ }
+
+ res.end(buffer)
+ })
+ }
+
+ if (p === '/sdch') {
+ res.statusCode = 200
+ res.setHeader('Content-Type', 'text/plain')
+ res.setHeader('Content-Encoding', 'sdch')
+ res.end('fake sdch string')
+ }
+
+ if (p === '/invalid-content-encoding') {
+ res.statusCode = 200
+ res.setHeader('Content-Type', 'text/plain')
+ res.setHeader('Content-Encoding', 'gzip')
+ res.end('fake gzip string')
+ }
+
+ if (p === '/timeout') {
+ setTimeout(() => {
+ res.statusCode = 200
+ res.setHeader('Content-Type', 'text/plain')
+ res.end('text')
+ }, 1000)
+ }
+
+ if (p === '/slow') {
+ res.statusCode = 200
+ res.setHeader('Content-Type', 'text/plain')
+ res.write('test')
+ setTimeout(() => {
+ res.end('test')
+ }, 1000)
+ }
+
+ if (p === '/cookie') {
+ res.statusCode = 200
+ res.setHeader('Set-Cookie', ['a=1', 'b=1'])
+ res.end('cookie')
+ }
+
+ if (p === '/size/chunk') {
+ res.statusCode = 200
+ res.setHeader('Content-Type', 'text/plain')
+ setTimeout(() => {
+ res.write('test')
+ }, 10)
+ setTimeout(() => {
+ res.end('test')
+ }, 20)
+ }
+
+ if (p === '/size/long') {
+ res.statusCode = 200
+ res.setHeader('Content-Type', 'text/plain')
+ res.end('testtest')
+ }
+
+ if (p === '/redirect/301') {
+ res.statusCode = 301
+ res.setHeader('Location', '/inspect')
+ res.end()
+ }
+
+ if (p === '/redirect/302') {
+ res.statusCode = 302
+ res.setHeader('Location', '/inspect')
+ res.end()
+ }
+
+ if (p === '/redirect/303') {
+ res.statusCode = 303
+ res.setHeader('Location', '/inspect')
+ res.end()
+ }
+
+ if (p === '/redirect/307') {
+ res.statusCode = 307
+ res.setHeader('Location', '/inspect')
+ res.end()
+ }
+
+ if (p === '/redirect/308') {
+ res.statusCode = 308
+ res.setHeader('Location', '/inspect')
+ res.end()
+ }
+
+ if (p === '/redirect/chain') {
+ res.statusCode = 301
+ res.setHeader('Location', '/redirect/301')
+ res.end()
+ }
+
+ if (p.startsWith('/redirect/chain/')) {
+ const count = parseInt(p.split('/').pop()) - 1
+ res.statusCode = 301
+ res.setHeader('Location', count ? `/redirect/chain/${count}` : '/redirect/301')
+ res.end()
+ }
+
+ if (p === '/redirect/no-location') {
+ res.statusCode = 301
+ res.end()
+ }
+
+ if (p === '/redirect/slow') {
+ res.statusCode = 301
+ res.setHeader('Location', '/redirect/301')
+ setTimeout(() => {
+ res.end()
+ }, 1000)
+ }
+
+ if (p === '/redirect/slow-chain') {
+ res.statusCode = 301
+ res.setHeader('Location', '/redirect/slow')
+ setTimeout(() => {
+ res.end()
+ }, 10)
+ }
+
+ if (p === '/redirect/slow-stream') {
+ res.statusCode = 301
+ res.setHeader('Location', '/slow')
+ res.end()
+ }
+
+ if (p === '/redirect/bad-location') {
+ res.socket.write('HTTP/1.1 301\r\nLocation: ☃\r\nContent-Length: 0\r\n')
+ res.socket.end('\r\n')
+ }
+
+ if (p === '/error/400') {
+ res.statusCode = 400
+ res.setHeader('Content-Type', 'text/plain')
+ res.end('client error')
+ }
+
+ if (p === '/error/404') {
+ res.statusCode = 404
+ res.setHeader('Content-Encoding', 'gzip')
+ res.end()
+ }
+
+ if (p === '/error/500') {
+ res.statusCode = 500
+ res.setHeader('Content-Type', 'text/plain')
+ res.end('server error')
+ }
+
+ if (p === '/error/reset') {
+ res.destroy()
+ }
+
+ if (p === '/error/premature') {
+ res.writeHead(200, { 'content-length': 50 })
+ res.write('foo')
+ setTimeout(() => {
+ res.destroy()
+ }, 100)
+ }
+
+ if (p === '/error/premature/chunked') {
+ res.writeHead(200, {
+ 'Content-Type': 'application/json',
+ 'Transfer-Encoding': 'chunked'
+ })
+
+ res.write(`${JSON.stringify({ data: 'hi' })}\n`)
+
+ setTimeout(() => {
+ res.write(`${JSON.stringify({ data: 'bye' })}\n`)
+ }, 200)
+
+ setTimeout(() => {
+ res.destroy()
+ }, 400)
+ }
+
+ if (p === '/error/json') {
+ res.statusCode = 200
+ res.setHeader('Content-Type', 'application/json')
+ res.end('invalid json')
+ }
+
+ if (p === '/no-content') {
+ res.statusCode = 204
+ res.end()
+ }
+
+ if (p === '/no-content/gzip') {
+ res.statusCode = 204
+ res.setHeader('Content-Encoding', 'gzip')
+ res.end()
+ }
+
+ if (p === '/no-content/brotli') {
+ res.statusCode = 204
+ res.setHeader('Content-Encoding', 'br')
+ res.end()
+ }
+
+ if (p === '/not-modified') {
+ res.statusCode = 304
+ res.end()
+ }
+
+ if (p === '/not-modified/gzip') {
+ res.statusCode = 304
+ res.setHeader('Content-Encoding', 'gzip')
+ res.end()
+ }
+
+ if (p === '/inspect') {
+ res.statusCode = 200
+ res.setHeader('Content-Type', 'application/json')
+ let body = ''
+ request.on('data', c => {
+ body += c
+ })
+ request.on('end', () => {
+ res.end(JSON.stringify({
+ method: request.method,
+ url: request.url,
+ headers: request.headers,
+ body
+ }))
+ })
+ }
+
+ if (p === '/multipart') {
+ res.statusCode = 200
+ res.setHeader('Content-Type', 'application/json')
+ const busboy = new Busboy({ headers: request.headers })
+ let body = ''
+ busboy.on('file', async (fieldName, file, fileName) => {
+ body += `${fieldName}=${fileName}`
+ // consume file data
+ // eslint-disable-next-line no-empty, no-unused-vars
+ for await (const c of file) {}
+ })
+
+ busboy.on('field', (fieldName, value) => {
+ body += `${fieldName}=${value}`
+ })
+ busboy.on('finish', () => {
+ res.end(JSON.stringify({
+ method: request.method,
+ url: request.url,
+ headers: request.headers,
+ body
+ }))
+ })
+ request.pipe(busboy)
+ }
+
+ if (p === '/m%C3%B6bius') {
+ res.statusCode = 200
+ res.setHeader('Content-Type', 'text/plain')
+ res.end('ok')
+ }
+ }
+}
diff --git a/test/parser-issues.js b/test/parser-issues.js
new file mode 100644
index 0000000..b98edf1
--- /dev/null
+++ b/test/parser-issues.js
@@ -0,0 +1,114 @@
+const net = require('net')
+const { test } = require('tap')
+const { Client, errors } = require('..')
+
+test('https://github.com/mcollina/undici/issues/268', (t) => {
+ t.plan(2)
+
+ const server = net.createServer(socket => {
+ socket.write('HTTP/1.1 200 OK\r\n')
+ socket.write('Transfer-Encoding: chunked\r\n\r\n')
+ setTimeout(() => {
+ socket.write('1\r\n')
+ socket.write('\n\r\n')
+ setTimeout(() => {
+ socket.write('1\r\n')
+ socket.write('\n\r\n')
+ }, 500)
+ }, 500)
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ client.request({
+ method: 'GET',
+ path: '/nxt/_changes?feed=continuous&heartbeat=5000',
+ headersTimeout: 1e3
+ }, (err, data) => {
+ t.error(err)
+ data.body
+ .resume()
+ setTimeout(() => {
+ t.pass()
+ data.body.on('error', () => {})
+ }, 2e3)
+ })
+ })
+})
+
+test('parser fail', (t) => {
+ t.plan(2)
+
+ const server = net.createServer(socket => {
+ socket.write('HTT/1.1 200 OK\r\n')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ client.request({
+ method: 'GET',
+ path: '/'
+ }, (err, data) => {
+ t.ok(err)
+ t.type(err, errors.HTTPParserError)
+ })
+ })
+})
+
+test('split header field', (t) => {
+ t.plan(2)
+
+ const server = net.createServer(socket => {
+ socket.write('HTTP/1.1 200 OK\r\nA')
+ setTimeout(() => {
+ socket.write('SD: asd,asd\r\n\r\n\r\n')
+ }, 100)
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ client.request({
+ method: 'GET',
+ path: '/'
+ }, (err, data) => {
+ t.error(err)
+ t.equal(data.headers.asd, 'asd,asd')
+ data.body.destroy().on('error', () => {})
+ })
+ })
+})
+
+test('split header value', (t) => {
+ t.plan(2)
+
+ const server = net.createServer(socket => {
+ socket.write('HTTP/1.1 200 OK\r\nASD: asd')
+ setTimeout(() => {
+ socket.write(',asd\r\n\r\n\r\n')
+ }, 100)
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ client.request({
+ method: 'GET',
+ path: '/'
+ }, (err, data) => {
+ t.error(err)
+ t.equal(data.headers.asd, 'asd,asd')
+ data.body.destroy().on('error', () => {})
+ })
+ })
+})
diff --git a/test/pipeline-pipelining.js b/test/pipeline-pipelining.js
new file mode 100644
index 0000000..52a29d7
--- /dev/null
+++ b/test/pipeline-pipelining.js
@@ -0,0 +1,108 @@
+'use strict'
+
+const { test } = require('tap')
+const { Client } = require('..')
+const { createServer } = require('http')
+const { kConnect } = require('../lib/core/symbols')
+const { kBusy, kPending, kRunning } = require('../lib/core/symbols')
+
+test('pipeline pipelining', (t) => {
+ t.plan(10)
+
+ const server = createServer((req, res) => {
+ t.strictSame(req.headers['transfer-encoding'], undefined)
+ res.end()
+ })
+
+ t.teardown(server.close.bind(server))
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, {
+ pipelining: 2
+ })
+ t.teardown(client.close.bind(client))
+
+ client[kConnect](() => {
+ t.equal(client[kRunning], 0)
+ client.pipeline({
+ method: 'GET',
+ path: '/'
+ }, ({ body }) => body).end().resume()
+ t.equal(client[kBusy], true)
+ t.strictSame(client[kRunning], 0)
+ t.strictSame(client[kPending], 1)
+
+ client.pipeline({
+ method: 'GET',
+ path: '/'
+ }, ({ body }) => body).end().resume()
+ t.equal(client[kBusy], true)
+ t.strictSame(client[kRunning], 0)
+ t.strictSame(client[kPending], 2)
+ process.nextTick(() => {
+ t.equal(client[kRunning], 2)
+ })
+ })
+ })
+})
+
+test('pipeline pipelining retry', (t) => {
+ t.plan(13)
+
+ let count = 0
+ const server = createServer((req, res) => {
+ if (count++ === 0) {
+ res.destroy()
+ } else {
+ res.end()
+ }
+ })
+
+ t.teardown(server.close.bind(server))
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, {
+ pipelining: 3
+ })
+ t.teardown(client.destroy.bind(client))
+
+ client.once('disconnect', () => {
+ t.pass()
+ })
+
+ client[kConnect](() => {
+ client.pipeline({
+ method: 'GET',
+ path: '/'
+ }, ({ body }) => body).end().resume()
+ .on('error', (err) => {
+ t.ok(err)
+ })
+ t.equal(client[kBusy], true)
+ t.strictSame(client[kRunning], 0)
+ t.strictSame(client[kPending], 1)
+
+ client.pipeline({
+ method: 'GET',
+ path: '/'
+ }, ({ body }) => body).end().resume()
+ t.equal(client[kBusy], true)
+ t.strictSame(client[kRunning], 0)
+ t.strictSame(client[kPending], 2)
+
+ client.pipeline({
+ method: 'GET',
+ path: '/'
+ }, ({ body }) => body).end().resume()
+ t.equal(client[kBusy], true)
+ t.strictSame(client[kRunning], 0)
+ t.strictSame(client[kPending], 3)
+
+ process.nextTick(() => {
+ t.equal(client[kRunning], 3)
+ })
+
+ client.close(() => {
+ t.pass()
+ })
+ })
+ })
+})
diff --git a/test/pool.js b/test/pool.js
new file mode 100644
index 0000000..8cf7195
--- /dev/null
+++ b/test/pool.js
@@ -0,0 +1,1101 @@
+'use strict'
+
+const { EventEmitter } = require('events')
+const { createServer } = require('http')
+const net = require('net')
+const {
+ finished,
+ PassThrough,
+ Readable
+} = require('stream')
+const { promisify } = require('util')
+const proxyquire = require('proxyquire')
+const { test } = require('tap')
+const {
+ kBusy,
+ kPending,
+ kRunning,
+ kSize,
+ kUrl
+} = require('../lib/core/symbols')
+const {
+ Client,
+ Pool,
+ errors
+} = require('..')
+
+test('throws when connection is inifinite', (t) => {
+ t.plan(2)
+
+ try {
+ new Pool(null, { connections: 0 / 0 }) // eslint-disable-line
+ } catch (e) {
+ t.type(e, errors.InvalidArgumentError)
+ t.equal(e.message, 'invalid connections')
+ }
+})
+
+test('throws when connections is negative', (t) => {
+ t.plan(2)
+
+ try {
+ new Pool(null, { connections: -1 }) // eslint-disable-line no-new
+ } catch (e) {
+ t.type(e, errors.InvalidArgumentError)
+ t.equal(e.message, 'invalid connections')
+ }
+})
+
+test('throws when connection is not number', (t) => {
+ t.plan(2)
+
+ try {
+ new Pool(null, { connections: true }) // eslint-disable-line no-new
+ } catch (e) {
+ t.type(e, errors.InvalidArgumentError)
+ t.equal(e.message, 'invalid connections')
+ }
+})
+
+test('throws when factory is not a function', (t) => {
+ t.plan(2)
+
+ try {
+ new Pool(null, { factory: '' }) // eslint-disable-line no-new
+ } catch (e) {
+ t.type(e, errors.InvalidArgumentError)
+ t.equal(e.message, 'factory must be a function.')
+ }
+})
+
+test('does not throw when connect is a function', (t) => {
+ t.plan(1)
+
+ t.doesNotThrow(() => new Pool('http://localhost', { connect: () => {} }))
+})
+
+test('connect/disconnect event(s)', (t) => {
+ const clients = 2
+
+ t.plan(clients * 6)
+
+ const server = createServer((req, res) => {
+ res.writeHead(200, {
+ Connection: 'keep-alive',
+ 'Keep-Alive': 'timeout=1s'
+ })
+ res.end('ok')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const pool = new Pool(`http://localhost:${server.address().port}`, {
+ connections: clients,
+ keepAliveTimeoutThreshold: 100
+ })
+ t.teardown(pool.close.bind(pool))
+
+ pool.on('connect', (origin, [pool, client]) => {
+ t.equal(client instanceof Client, true)
+ })
+ pool.on('disconnect', (origin, [pool, client], error) => {
+ t.ok(client instanceof Client)
+ t.type(error, errors.InformationalError)
+ t.equal(error.code, 'UND_ERR_INFO')
+ t.equal(error.message, 'socket idle timeout')
+ })
+
+ for (let i = 0; i < clients; i++) {
+ pool.request({
+ path: '/',
+ method: 'GET'
+ }, (err, { headers, body }) => {
+ t.error(err)
+ body.resume()
+ })
+ }
+ })
+})
+
+test('basic get', (t) => {
+ t.plan(14)
+
+ const server = createServer((req, res) => {
+ t.equal('/', req.url)
+ t.equal('GET', req.method)
+ res.setHeader('content-type', 'text/plain')
+ res.end('hello')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, async () => {
+ const client = new Pool(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ t.equal(client[kUrl].origin, `http://localhost:${server.address().port}`)
+
+ client.request({ path: '/', method: 'GET' }, (err, { statusCode, headers, body }) => {
+ t.error(err)
+ t.equal(statusCode, 200)
+ t.equal(headers['content-type'], 'text/plain')
+ const bufs = []
+ body.on('data', (buf) => {
+ bufs.push(buf)
+ })
+ body.on('end', () => {
+ t.equal('hello', Buffer.concat(bufs).toString('utf8'))
+ })
+ })
+
+ t.equal(client.destroyed, false)
+ t.equal(client.closed, false)
+ client.close((err) => {
+ t.error(err)
+ t.equal(client.destroyed, true)
+ client.destroy((err) => {
+ t.error(err)
+ client.close((err) => {
+ t.type(err, errors.ClientDestroyedError)
+ })
+ })
+ })
+ t.equal(client.closed, true)
+ })
+})
+
+test('URL as arg', (t) => {
+ t.plan(9)
+
+ const server = createServer((req, res) => {
+ t.equal('/', req.url)
+ t.equal('GET', req.method)
+ res.setHeader('content-type', 'text/plain')
+ res.end('hello')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, async () => {
+ const url = new URL('http://localhost')
+ url.port = server.address().port
+ const client = new Pool(url)
+ t.teardown(client.destroy.bind(client))
+
+ client.request({ path: '/', method: 'GET' }, (err, { statusCode, headers, body }) => {
+ t.error(err)
+ t.equal(statusCode, 200)
+ t.equal(headers['content-type'], 'text/plain')
+ const bufs = []
+ body.on('data', (buf) => {
+ bufs.push(buf)
+ })
+ body.on('end', () => {
+ t.equal('hello', Buffer.concat(bufs).toString('utf8'))
+ })
+ })
+
+ client.close((err) => {
+ t.error(err)
+ client.destroy((err) => {
+ t.error(err)
+ client.close((err) => {
+ t.type(err, errors.ClientDestroyedError)
+ })
+ })
+ })
+ })
+})
+
+test('basic get error async/await', (t) => {
+ t.plan(2)
+
+ const server = createServer((req, res) => {
+ res.destroy()
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, async () => {
+ const client = new Pool(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ await client.request({ path: '/', method: 'GET' })
+ .catch((err) => {
+ t.ok(err)
+ })
+
+ await client.destroy()
+
+ await client.close().catch((err) => {
+ t.type(err, errors.ClientDestroyedError)
+ })
+ })
+})
+
+test('basic get with async/await', async (t) => {
+ const server = createServer((req, res) => {
+ t.equal('/', req.url)
+ t.equal('GET', req.method)
+ res.setHeader('content-type', 'text/plain')
+ res.end('hello')
+ })
+ t.teardown(server.close.bind(server))
+
+ await promisify(server.listen.bind(server))(0)
+ const client = new Pool(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ const { statusCode, headers, body } = await client.request({ path: '/', method: 'GET' })
+ t.equal(statusCode, 200)
+ t.equal(headers['content-type'], 'text/plain')
+
+ body.resume()
+ await promisify(finished)(body)
+
+ await client.close()
+ await client.destroy()
+})
+
+test('stream get async/await', async (t) => {
+ const server = createServer((req, res) => {
+ t.equal('/', req.url)
+ t.equal('GET', req.method)
+ res.setHeader('content-type', 'text/plain')
+ res.end('hello')
+ })
+ t.teardown(server.close.bind(server))
+
+ await promisify(server.listen.bind(server))(0)
+ const client = new Pool(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ await client.stream({ path: '/', method: 'GET' }, ({ statusCode, headers }) => {
+ t.equal(statusCode, 200)
+ t.equal(headers['content-type'], 'text/plain')
+ return new PassThrough()
+ })
+})
+
+test('stream get error async/await', (t) => {
+ t.plan(1)
+
+ const server = createServer((req, res) => {
+ res.destroy()
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, async () => {
+ const client = new Pool(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ await client.stream({ path: '/', method: 'GET' }, () => {
+
+ })
+ .catch((err) => {
+ t.ok(err)
+ })
+ })
+})
+
+test('pipeline get', (t) => {
+ t.plan(5)
+
+ const server = createServer((req, res) => {
+ t.equal('/', req.url)
+ t.equal('GET', req.method)
+ res.setHeader('content-type', 'text/plain')
+ res.end('hello')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, async () => {
+ const client = new Pool(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ const bufs = []
+ client.pipeline({ path: '/', method: 'GET' }, ({ statusCode, headers, body }) => {
+ t.equal(statusCode, 200)
+ t.equal(headers['content-type'], 'text/plain')
+ return body
+ })
+ .end()
+ .on('data', (buf) => {
+ bufs.push(buf)
+ })
+ .on('end', () => {
+ t.equal('hello', Buffer.concat(bufs).toString('utf8'))
+ })
+ })
+})
+
+test('backpressure algorithm', (t) => {
+ const seen = []
+ let total = 0
+
+ let writeMore = true
+
+ class FakeClient extends EventEmitter {
+ constructor () {
+ super()
+
+ this.id = total++
+ }
+
+ dispatch (req, handler) {
+ seen.push({ req, client: this, id: this.id })
+ return writeMore
+ }
+ }
+
+ const Pool = proxyquire('../lib/pool', {
+ './client': FakeClient
+ })
+
+ const noopHandler = {
+ onError (err) {
+ throw err
+ }
+ }
+
+ const pool = new Pool('http://notahost')
+
+ pool.dispatch({}, noopHandler)
+ pool.dispatch({}, noopHandler)
+
+ const d1 = seen.shift() // d1 = c0
+ t.equal(d1.id, 0)
+ const d2 = seen.shift() // d2 = c0
+ t.equal(d2.id, 0)
+
+ t.equal(d1.id, d2.id)
+
+ writeMore = false
+
+ pool.dispatch({}, noopHandler) // d3 = c0
+
+ pool.dispatch({}, noopHandler) // d4 = c1
+
+ const d3 = seen.shift()
+ t.equal(d3.id, 0)
+ const d4 = seen.shift()
+ t.equal(d4.id, 1)
+
+ t.equal(d3.id, d2.id)
+ t.not(d3.id, d4.id)
+
+ writeMore = true
+
+ d4.client.emit('drain', new URL('http://notahost'), [])
+
+ pool.dispatch({}, noopHandler) // d5 = c1
+
+ d3.client.emit('drain', new URL('http://notahost'), [])
+
+ pool.dispatch({}, noopHandler) // d6 = c0
+
+ const d5 = seen.shift()
+ t.equal(d5.id, 1)
+ const d6 = seen.shift()
+ t.equal(d6.id, 0)
+
+ t.equal(d5.id, d4.id)
+ t.equal(d3.id, d6.id)
+
+ t.equal(total, 3)
+
+ t.end()
+})
+
+test('busy', (t) => {
+ t.plan(8 * 16 + 2 + 1)
+
+ const server = createServer((req, res) => {
+ t.equal('/', req.url)
+ t.equal('GET', req.method)
+ res.setHeader('content-type', 'text/plain')
+ res.end('hello')
+ })
+ t.teardown(server.close.bind(server))
+
+ const connections = 2
+
+ server.listen(0, async () => {
+ const client = new Pool(`http://localhost:${server.address().port}`, {
+ connections,
+ pipelining: 2
+ })
+ client.on('drain', () => {
+ t.pass()
+ })
+ client.on('connect', () => {
+ t.pass()
+ })
+ t.teardown(client.destroy.bind(client))
+
+ for (let n = 1; n <= 8; ++n) {
+ client.request({ path: '/', method: 'GET' }, (err, { statusCode, headers, body }) => {
+ t.error(err)
+ t.equal(statusCode, 200)
+ t.equal(headers['content-type'], 'text/plain')
+ const bufs = []
+ body.on('data', (buf) => {
+ bufs.push(buf)
+ })
+ body.on('end', () => {
+ t.equal('hello', Buffer.concat(bufs).toString('utf8'))
+ })
+ })
+ t.equal(client[kPending], n)
+ t.equal(client[kBusy], n > 1)
+ t.equal(client[kSize], n)
+ t.equal(client[kRunning], 0)
+
+ t.equal(client.stats.connected, 0)
+ t.equal(client.stats.free, 0)
+ t.equal(client.stats.queued, Math.max(n - connections, 0))
+ t.equal(client.stats.pending, n)
+ t.equal(client.stats.size, n)
+ t.equal(client.stats.running, 0)
+ }
+ })
+})
+
+test('invalid pool dispatch options', (t) => {
+ t.plan(2)
+ const pool = new Pool('http://notahost')
+ t.throws(() => pool.dispatch({}), errors.InvalidArgumentError, 'throws on invalid handler')
+ t.throws(() => pool.dispatch({}, {}), errors.InvalidArgumentError, 'throws on invalid handler')
+})
+
+test('pool upgrade promise', (t) => {
+ t.plan(2)
+
+ const server = net.createServer((c) => {
+ c.on('data', (d) => {
+ c.write('HTTP/1.1 101\r\n')
+ c.write('hello: world\r\n')
+ c.write('connection: upgrade\r\n')
+ c.write('upgrade: websocket\r\n')
+ c.write('\r\n')
+ c.write('Body')
+ })
+
+ c.on('end', () => {
+ c.end()
+ })
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, async () => {
+ const client = new Pool(`http://localhost:${server.address().port}`)
+ t.teardown(client.close.bind(client))
+
+ const { headers, socket } = await client.upgrade({
+ path: '/',
+ method: 'GET',
+ protocol: 'Websocket'
+ })
+
+ let recvData = ''
+ socket.on('data', (d) => {
+ recvData += d
+ })
+
+ socket.on('close', () => {
+ t.equal(recvData.toString(), 'Body')
+ })
+
+ t.same(headers, {
+ hello: 'world',
+ connection: 'upgrade',
+ upgrade: 'websocket'
+ })
+ socket.end()
+ })
+})
+
+test('pool connect', (t) => {
+ t.plan(1)
+
+ const server = createServer((c) => {
+ t.fail()
+ })
+ server.on('connect', (req, socket, firstBodyChunk) => {
+ socket.write('HTTP/1.1 200 Connection established\r\n\r\n')
+
+ let data = firstBodyChunk.toString()
+ socket.on('data', (buf) => {
+ data += buf.toString()
+ })
+
+ socket.on('end', () => {
+ socket.end(data)
+ })
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, async () => {
+ const client = new Pool(`http://localhost:${server.address().port}`)
+ t.teardown(client.close.bind(client))
+
+ const { socket } = await client.connect({
+ path: '/'
+ })
+
+ let recvData = ''
+ socket.on('data', (d) => {
+ recvData += d
+ })
+
+ socket.on('end', () => {
+ t.equal(recvData.toString(), 'Body')
+ })
+
+ socket.write('Body')
+ socket.end()
+ })
+})
+
+test('pool dispatch', (t) => {
+ t.plan(2)
+
+ const server = createServer((req, res) => {
+ res.end('asd')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, async () => {
+ const client = new Pool(`http://localhost:${server.address().port}`)
+ t.teardown(client.close.bind(client))
+
+ let buf = ''
+ client.dispatch({
+ path: '/',
+ method: 'GET'
+ }, {
+ onConnect () {
+ },
+ onHeaders (statusCode, headers) {
+ t.equal(statusCode, 200)
+ },
+ onData (chunk) {
+ buf += chunk
+ },
+ onComplete () {
+ t.equal(buf, 'asd')
+ },
+ onError () {
+ }
+ })
+ })
+})
+
+test('pool pipeline args validation', (t) => {
+ t.plan(2)
+
+ const client = new Pool('http://localhost:5000')
+
+ const ret = client.pipeline(null, () => {})
+ ret.on('error', (err) => {
+ t.ok(/opts/.test(err.message))
+ t.type(err, errors.InvalidArgumentError)
+ })
+})
+
+test('300 requests succeed', (t) => {
+ t.plan(300 * 3)
+
+ const server = createServer((req, res) => {
+ res.end('asd')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Pool(`http://localhost:${server.address().port}`, {
+ connections: 1
+ })
+ t.teardown(client.destroy.bind(client))
+
+ for (let n = 0; n < 300; ++n) {
+ client.request({
+ path: '/',
+ method: 'GET'
+ }, (err, data) => {
+ t.error(err)
+ data.body.on('data', (chunk) => {
+ t.equal(chunk.toString(), 'asd')
+ }).on('end', () => {
+ t.pass()
+ })
+ })
+ }
+ })
+})
+
+test('pool connect error', (t) => {
+ t.plan(1)
+
+ const server = createServer((c) => {
+ t.fail()
+ })
+ server.on('connect', (req, socket, firstBodyChunk) => {
+ socket.destroy()
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, async () => {
+ const client = new Pool(`http://localhost:${server.address().port}`)
+ t.teardown(client.close.bind(client))
+
+ try {
+ await client.connect({
+ path: '/'
+ })
+ } catch (err) {
+ t.ok(err)
+ }
+ })
+})
+
+test('pool upgrade error', (t) => {
+ t.plan(1)
+
+ const server = net.createServer((c) => {
+ c.on('data', (d) => {
+ c.write('HTTP/1.1 101\r\n')
+ c.write('hello: world\r\n')
+ c.write('connection: upgrade\r\n')
+ c.write('\r\n')
+ c.write('Body')
+ })
+ c.on('error', () => {
+ // Whether we get an error, end or close is undefined.
+ // Ignore error.
+ })
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, async () => {
+ const client = new Pool(`http://localhost:${server.address().port}`)
+ t.teardown(client.close.bind(client))
+
+ try {
+ await client.upgrade({
+ path: '/',
+ method: 'GET',
+ protocol: 'Websocket'
+ })
+ } catch (err) {
+ t.ok(err)
+ }
+ })
+})
+
+test('pool dispatch error', (t) => {
+ t.plan(3)
+
+ const server = createServer((req, res) => {
+ res.end('asd')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, async () => {
+ const client = new Pool(`http://localhost:${server.address().port}`, {
+ connections: 1,
+ pipelining: 1
+ })
+ t.teardown(client.close.bind(client))
+
+ client.dispatch({
+ path: '/',
+ method: 'GET'
+ }, {
+ onConnect () {
+ },
+ onHeaders (statusCode, headers) {
+ t.equal(statusCode, 200)
+ },
+ onData (chunk) {
+ },
+ onComplete () {
+ t.pass()
+ },
+ onError () {
+ }
+ })
+
+ client.dispatch({
+ path: '/',
+ method: 'GET',
+ headers: {
+ 'transfer-encoding': 'fail'
+ }
+ }, {
+ onConnect () {
+ t.fail()
+ },
+ onHeaders (statusCode, headers) {
+ t.fail()
+ },
+ onData (chunk) {
+ t.fail()
+ },
+ onError (err) {
+ t.equal(err.code, 'UND_ERR_INVALID_ARG')
+ }
+ })
+ })
+})
+
+test('pool request abort in queue', (t) => {
+ t.plan(3)
+
+ const server = createServer((req, res) => {
+ res.end('asd')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, async () => {
+ const client = new Pool(`http://localhost:${server.address().port}`, {
+ connections: 1,
+ pipelining: 1
+ })
+ t.teardown(client.close.bind(client))
+
+ client.dispatch({
+ path: '/',
+ method: 'GET'
+ }, {
+ onConnect () {
+ },
+ onHeaders (statusCode, headers) {
+ t.equal(statusCode, 200)
+ },
+ onData (chunk) {
+ },
+ onComplete () {
+ t.pass()
+ },
+ onError () {
+ }
+ })
+
+ const signal = new EventEmitter()
+ client.request({
+ path: '/',
+ method: 'GET',
+ signal
+ }, (err) => {
+ t.equal(err.code, 'UND_ERR_ABORTED')
+ })
+ signal.emit('abort')
+ })
+})
+
+test('pool stream abort in queue', (t) => {
+ t.plan(3)
+
+ const server = createServer((req, res) => {
+ res.end('asd')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, async () => {
+ const client = new Pool(`http://localhost:${server.address().port}`, {
+ connections: 1,
+ pipelining: 1
+ })
+ t.teardown(client.close.bind(client))
+
+ client.dispatch({
+ path: '/',
+ method: 'GET'
+ }, {
+ onConnect () {
+ },
+ onHeaders (statusCode, headers) {
+ t.equal(statusCode, 200)
+ },
+ onData (chunk) {
+ },
+ onComplete () {
+ t.pass()
+ },
+ onError () {
+ }
+ })
+
+ const signal = new EventEmitter()
+ client.stream({
+ path: '/',
+ method: 'GET',
+ signal
+ }, ({ body }) => body, (err) => {
+ t.equal(err.code, 'UND_ERR_ABORTED')
+ })
+ signal.emit('abort')
+ })
+})
+
+test('pool pipeline abort in queue', (t) => {
+ t.plan(3)
+
+ const server = createServer((req, res) => {
+ res.end('asd')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, async () => {
+ const client = new Pool(`http://localhost:${server.address().port}`, {
+ connections: 1,
+ pipelining: 1
+ })
+ t.teardown(client.close.bind(client))
+
+ client.dispatch({
+ path: '/',
+ method: 'GET'
+ }, {
+ onConnect () {
+ },
+ onHeaders (statusCode, headers) {
+ t.equal(statusCode, 200)
+ },
+ onData (chunk) {
+ },
+ onComplete () {
+ t.pass()
+ },
+ onError () {
+ }
+ })
+
+ const signal = new EventEmitter()
+ client.pipeline({
+ path: '/',
+ method: 'GET',
+ signal
+ }, ({ body }) => body).end().on('error', (err) => {
+ t.equal(err.code, 'UND_ERR_ABORTED')
+ })
+ signal.emit('abort')
+ })
+})
+
+test('pool stream constructor error destroy body', (t) => {
+ t.plan(4)
+
+ const server = createServer((req, res) => {
+ res.end('asd')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, async () => {
+ const client = new Pool(`http://localhost:${server.address().port}`, {
+ connections: 1,
+ pipelining: 1
+ })
+ t.teardown(client.close.bind(client))
+
+ {
+ const body = new Readable({
+ read () {
+ }
+ })
+ client.stream({
+ path: '/',
+ method: 'GET',
+ body,
+ headers: {
+ 'transfer-encoding': 'fail'
+ }
+ }, () => {
+ t.fail()
+ }, (err) => {
+ t.equal(err.code, 'UND_ERR_INVALID_ARG')
+ t.equal(body.destroyed, true)
+ })
+ }
+
+ {
+ const body = new Readable({
+ read () {
+ }
+ })
+ client.stream({
+ path: '/',
+ method: 'CONNECT',
+ body
+ }, () => {
+ t.fail()
+ }, (err) => {
+ t.equal(err.code, 'UND_ERR_INVALID_ARG')
+ t.equal(body.destroyed, true)
+ })
+ }
+ })
+})
+
+test('pool request constructor error destroy body', (t) => {
+ t.plan(4)
+
+ const server = createServer((req, res) => {
+ res.end('asd')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, async () => {
+ const client = new Pool(`http://localhost:${server.address().port}`, {
+ connections: 1,
+ pipelining: 1
+ })
+ t.teardown(client.close.bind(client))
+
+ {
+ const body = new Readable({
+ read () {
+ }
+ })
+ client.request({
+ path: '/',
+ method: 'GET',
+ body,
+ headers: {
+ 'transfer-encoding': 'fail'
+ }
+ }, (err) => {
+ t.equal(err.code, 'UND_ERR_INVALID_ARG')
+ t.equal(body.destroyed, true)
+ })
+ }
+
+ {
+ const body = new Readable({
+ read () {
+ }
+ })
+ client.request({
+ path: '/',
+ method: 'CONNECT',
+ body
+ }, (err) => {
+ t.equal(err.code, 'UND_ERR_INVALID_ARG')
+ t.equal(body.destroyed, true)
+ })
+ }
+ })
+})
+
+test('pool close waits for all requests', (t) => {
+ t.plan(5)
+
+ const server = createServer((req, res) => {
+ res.end('asd')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Pool(`http://localhost:${server.address().port}`, {
+ connections: 1,
+ pipelining: 1
+ })
+ t.teardown(client.destroy.bind(client))
+
+ client.request({
+ path: '/',
+ method: 'GET'
+ }, (err) => {
+ t.error(err)
+ })
+
+ client.request({
+ path: '/',
+ method: 'GET'
+ }, (err) => {
+ t.error(err)
+ })
+
+ client.close(() => {
+ t.pass()
+ })
+
+ client.close(() => {
+ t.pass()
+ })
+
+ client.request({
+ path: '/',
+ method: 'GET'
+ }, (err) => {
+ t.type(err, errors.ClientClosedError)
+ })
+ })
+})
+
+test('pool destroyed', (t) => {
+ t.plan(1)
+
+ const server = createServer((req, res) => {
+ res.end('asd')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Pool(`http://localhost:${server.address().port}`, {
+ connections: 1,
+ pipelining: 1
+ })
+ t.teardown(client.destroy.bind(client))
+
+ client.destroy()
+ client.request({
+ path: '/',
+ method: 'GET'
+ }, (err) => {
+ t.type(err, errors.ClientDestroyedError)
+ })
+ })
+})
+
+test('pool destroy fails queued requests', (t) => {
+ t.plan(6)
+
+ const server = createServer((req, res) => {
+ res.end('asd')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, async () => {
+ const client = new Pool(`http://localhost:${server.address().port}`, {
+ connections: 1,
+ pipelining: 1
+ })
+ t.teardown(client.destroy.bind(client))
+
+ const _err = new Error()
+ client.request({
+ path: '/',
+ method: 'GET'
+ }, (err) => {
+ t.equal(err, _err)
+ })
+
+ client.request({
+ path: '/',
+ method: 'GET'
+ }, (err) => {
+ t.equal(err, _err)
+ })
+
+ t.equal(client.destroyed, false)
+ client.destroy(_err, () => {
+ t.pass()
+ })
+ t.equal(client.destroyed, true)
+
+ client.request({
+ path: '/',
+ method: 'GET'
+ }, (err) => {
+ t.type(err, errors.ClientDestroyedError)
+ })
+ })
+})
diff --git a/test/promises.js b/test/promises.js
new file mode 100644
index 0000000..524fdfc
--- /dev/null
+++ b/test/promises.js
@@ -0,0 +1,280 @@
+'use strict'
+
+const { test } = require('tap')
+const { Client, Pool } = require('..')
+const { createServer } = require('http')
+const { readFileSync, createReadStream } = require('fs')
+const { wrapWithAsyncIterable } = require('./utils/async-iterators')
+
+test('basic get, async await support', (t) => {
+ t.plan(5)
+
+ const server = createServer((req, res) => {
+ t.equal('/', req.url)
+ t.equal('GET', req.method)
+ res.setHeader('content-type', 'text/plain')
+ res.end('hello')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, async () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.close.bind(client))
+
+ try {
+ const { statusCode, headers, body } = await client.request({ path: '/', method: 'GET' })
+ t.equal(statusCode, 200)
+ t.equal(headers['content-type'], 'text/plain')
+ const bufs = []
+ body.on('data', (buf) => {
+ bufs.push(buf)
+ })
+ body.on('end', () => {
+ t.equal('hello', Buffer.concat(bufs).toString('utf8'))
+ })
+ } catch (err) {
+ t.fail(err)
+ }
+ })
+})
+
+function postServer (t, expected) {
+ return function (req, res) {
+ t.equal(req.url, '/')
+ t.equal(req.method, 'POST')
+
+ req.setEncoding('utf8')
+ let data = ''
+
+ req.on('data', function (d) { data += d })
+
+ req.on('end', () => {
+ t.equal(data, expected)
+ res.end('hello')
+ })
+ }
+}
+
+test('basic POST with string, async await support', (t) => {
+ t.plan(5)
+
+ const expected = readFileSync(__filename, 'utf8')
+
+ const server = createServer(postServer(t, expected))
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, async () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.close.bind(client))
+
+ try {
+ const { statusCode, body } = await client.request({ path: '/', method: 'POST', body: expected })
+ t.equal(statusCode, 200)
+ const bufs = []
+ body.on('data', (buf) => {
+ bufs.push(buf)
+ })
+ body.on('end', () => {
+ t.equal('hello', Buffer.concat(bufs).toString('utf8'))
+ })
+ } catch (err) {
+ t.fail(err)
+ }
+ })
+})
+
+test('basic POST with Buffer, async await support', (t) => {
+ t.plan(5)
+
+ const expected = readFileSync(__filename)
+
+ const server = createServer(postServer(t, expected.toString()))
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, async () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.close.bind(client))
+
+ try {
+ const { statusCode, body } = await client.request({ path: '/', method: 'POST', body: expected })
+ t.equal(statusCode, 200)
+ const bufs = []
+ body.on('data', (buf) => {
+ bufs.push(buf)
+ })
+ body.on('end', () => {
+ t.equal('hello', Buffer.concat(bufs).toString('utf8'))
+ })
+ } catch (err) {
+ t.fail(err)
+ }
+ })
+})
+
+test('basic POST with stream, async await support', (t) => {
+ t.plan(5)
+
+ const expected = readFileSync(__filename, 'utf8')
+
+ const server = createServer(postServer(t, expected))
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, async () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.close.bind(client))
+
+ try {
+ const { statusCode, body } = await client.request({
+ path: '/',
+ method: 'POST',
+ headers: {
+ 'content-length': Buffer.byteLength(expected)
+ },
+ body: createReadStream(__filename)
+ })
+ t.equal(statusCode, 200)
+ const bufs = []
+ body.on('data', (buf) => {
+ bufs.push(buf)
+ })
+ body.on('end', () => {
+ t.equal('hello', Buffer.concat(bufs).toString('utf8'))
+ })
+ } catch (err) {
+ t.fail(err)
+ }
+ })
+})
+
+test('basic POST with async-iterator, async await support', (t) => {
+ t.plan(5)
+
+ const expected = readFileSync(__filename, 'utf8')
+
+ const server = createServer(postServer(t, expected))
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, async () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.close.bind(client))
+
+ try {
+ const { statusCode, body } = await client.request({
+ path: '/',
+ method: 'POST',
+ headers: {
+ 'content-length': Buffer.byteLength(expected)
+ },
+ body: wrapWithAsyncIterable(createReadStream(__filename))
+ })
+ t.equal(statusCode, 200)
+ const bufs = []
+ body.on('data', (buf) => {
+ bufs.push(buf)
+ })
+ body.on('end', () => {
+ t.equal('hello', Buffer.concat(bufs).toString('utf8'))
+ })
+ } catch (err) {
+ t.fail(err)
+ }
+ })
+})
+
+test('20 times GET with pipelining 10, async await support', (t) => {
+ const num = 20
+ t.plan(2 * num + 1)
+
+ const sleep = ms => new Promise((resolve, reject) => {
+ setTimeout(resolve, ms)
+ })
+
+ let count = 0
+ let countGreaterThanOne = false
+ const server = createServer(async (req, res) => {
+ count++
+ await sleep(10)
+ countGreaterThanOne = countGreaterThanOne || count > 1
+ res.end(req.url)
+ })
+ t.teardown(server.close.bind(server))
+
+ // needed to check for a warning on the maxListeners on the socket
+ function onWarning (warning) {
+ if (!/ExperimentalWarning/.test(warning)) {
+ t.fail()
+ }
+ }
+ process.on('warning', onWarning)
+ t.teardown(() => {
+ process.removeListener('warning', onWarning)
+ })
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, {
+ pipelining: 10
+ })
+ t.teardown(client.close.bind(client))
+
+ for (let i = 0; i < num; i++) {
+ makeRequest(i)
+ }
+
+ async function makeRequest (i) {
+ await makeRequestAndExpectUrl(client, i, t)
+ count--
+ if (i === num - 1) {
+ t.ok(countGreaterThanOne, 'seen more than one parallel request')
+ }
+ }
+ })
+})
+
+async function makeRequestAndExpectUrl (client, i, t) {
+ try {
+ const { statusCode, body } = await client.request({ path: '/' + i, method: 'GET' })
+ t.equal(statusCode, 200)
+ const bufs = []
+ body.on('data', (buf) => {
+ bufs.push(buf)
+ })
+ body.on('end', () => {
+ t.equal('/' + i, Buffer.concat(bufs).toString('utf8'))
+ })
+ } catch (err) {
+ t.fail(err)
+ }
+ return true
+}
+
+test('pool, async await support', (t) => {
+ t.plan(5)
+
+ const server = createServer((req, res) => {
+ t.equal('/', req.url)
+ t.equal('GET', req.method)
+ res.setHeader('content-type', 'text/plain')
+ res.end('hello')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, async () => {
+ const client = new Pool(`http://localhost:${server.address().port}`)
+ t.teardown(client.close.bind(client))
+
+ try {
+ const { statusCode, headers, body } = await client.request({ path: '/', method: 'GET' })
+ t.equal(statusCode, 200)
+ t.equal(headers['content-type'], 'text/plain')
+ const bufs = []
+ body.on('data', (buf) => {
+ bufs.push(buf)
+ })
+ body.on('end', () => {
+ t.equal('hello', Buffer.concat(bufs).toString('utf8'))
+ })
+ } catch (err) {
+ t.fail(err)
+ }
+ })
+})
diff --git a/test/proxy-agent.js b/test/proxy-agent.js
new file mode 100644
index 0000000..0a92126
--- /dev/null
+++ b/test/proxy-agent.js
@@ -0,0 +1,720 @@
+'use strict'
+
+const { test, teardown } = require('tap')
+const { request, fetch, setGlobalDispatcher, getGlobalDispatcher } = require('..')
+const { InvalidArgumentError } = require('../lib/core/errors')
+const { nodeMajor } = require('../lib/core/util')
+const { readFileSync } = require('fs')
+const { join } = require('path')
+const ProxyAgent = require('../lib/proxy-agent')
+const Pool = require('../lib/pool')
+const { createServer } = require('http')
+const https = require('https')
+const proxy = require('proxy')
+
+test('should throw error when no uri is provided', (t) => {
+ t.plan(2)
+ t.throws(() => new ProxyAgent(), InvalidArgumentError)
+ t.throws(() => new ProxyAgent({}), InvalidArgumentError)
+})
+
+test('using auth in combination with token should throw', (t) => {
+ t.plan(1)
+ t.throws(() => new ProxyAgent({
+ auth: 'foo',
+ token: 'Bearer bar',
+ uri: 'http://example.com'
+ }),
+ InvalidArgumentError
+ )
+})
+
+test('should accept string and object as options', (t) => {
+ t.plan(2)
+ t.doesNotThrow(() => new ProxyAgent('http://example.com'))
+ t.doesNotThrow(() => new ProxyAgent({ uri: 'http://example.com' }))
+})
+
+test('use proxy-agent to connect through proxy', async (t) => {
+ t.plan(6)
+ const server = await buildServer()
+ const proxy = await buildProxy()
+ delete proxy.authenticate
+
+ const serverUrl = `http://localhost:${server.address().port}`
+ const proxyUrl = `http://localhost:${proxy.address().port}`
+ const proxyAgent = new ProxyAgent(proxyUrl)
+ const parsedOrigin = new URL(serverUrl)
+
+ proxy.on('connect', () => {
+ t.pass('should connect to proxy')
+ })
+
+ server.on('request', (req, res) => {
+ t.equal(req.url, '/')
+ t.equal(req.headers.host, parsedOrigin.host, 'should not use proxyUrl as host')
+ res.setHeader('content-type', 'application/json')
+ res.end(JSON.stringify({ hello: 'world' }))
+ })
+
+ const {
+ statusCode,
+ headers,
+ body
+ } = await request(serverUrl, { dispatcher: proxyAgent })
+ const json = await body.json()
+
+ t.equal(statusCode, 200)
+ t.same(json, { hello: 'world' })
+ t.equal(headers.connection, 'keep-alive', 'should remain the connection open')
+
+ server.close()
+ proxy.close()
+ proxyAgent.close()
+})
+
+test('use proxy agent to connect through proxy using Pool', async (t) => {
+ t.plan(3)
+ const server = await buildServer()
+ const proxy = await buildProxy()
+ let resolveFirstConnect
+ let connectCount = 0
+
+ proxy.authenticate = async function (req, fn) {
+ if (++connectCount === 2) {
+ t.pass('second connect should arrive while first is still inflight')
+ resolveFirstConnect()
+ fn(null, true)
+ } else {
+ await new Promise((resolve) => {
+ resolveFirstConnect = resolve
+ })
+ fn(null, true)
+ }
+ }
+
+ server.on('request', (req, res) => {
+ res.end()
+ })
+
+ const serverUrl = `http://localhost:${server.address().port}`
+ const proxyUrl = `http://localhost:${proxy.address().port}`
+ const clientFactory = (url, options) => {
+ return new Pool(url, options)
+ }
+ const proxyAgent = new ProxyAgent({ auth: Buffer.from('user:pass').toString('base64'), uri: proxyUrl, clientFactory })
+ const firstRequest = request(`${serverUrl}`, { dispatcher: proxyAgent })
+ const secondRequest = await request(`${serverUrl}`, { dispatcher: proxyAgent })
+ t.equal((await firstRequest).statusCode, 200)
+ t.equal(secondRequest.statusCode, 200)
+ server.close()
+ proxy.close()
+ proxyAgent.close()
+})
+
+test('use proxy-agent to connect through proxy using path with params', async (t) => {
+ t.plan(6)
+ const server = await buildServer()
+ const proxy = await buildProxy()
+
+ const serverUrl = `http://localhost:${server.address().port}`
+ const proxyUrl = `http://localhost:${proxy.address().port}`
+ const proxyAgent = new ProxyAgent(proxyUrl)
+ const parsedOrigin = new URL(serverUrl)
+
+ proxy.on('connect', () => {
+ t.pass('should call proxy')
+ })
+ server.on('request', (req, res) => {
+ t.equal(req.url, '/hello?foo=bar')
+ t.equal(req.headers.host, parsedOrigin.host, 'should not use proxyUrl as host')
+ res.setHeader('content-type', 'application/json')
+ res.end(JSON.stringify({ hello: 'world' }))
+ })
+
+ const {
+ statusCode,
+ headers,
+ body
+ } = await request(serverUrl + '/hello?foo=bar', { dispatcher: proxyAgent })
+ const json = await body.json()
+
+ t.equal(statusCode, 200)
+ t.same(json, { hello: 'world' })
+ t.equal(headers.connection, 'keep-alive', 'should remain the connection open')
+
+ server.close()
+ proxy.close()
+ proxyAgent.close()
+})
+
+test('use proxy-agent with auth', async (t) => {
+ t.plan(7)
+ const server = await buildServer()
+ const proxy = await buildProxy()
+
+ const serverUrl = `http://localhost:${server.address().port}`
+ const proxyUrl = `http://localhost:${proxy.address().port}`
+ const proxyAgent = new ProxyAgent({
+ auth: Buffer.from('user:pass').toString('base64'),
+ uri: proxyUrl
+ })
+ const parsedOrigin = new URL(serverUrl)
+
+ proxy.authenticate = function (req, fn) {
+ t.pass('authentication should be called')
+ fn(null, req.headers['proxy-authorization'] === `Basic ${Buffer.from('user:pass').toString('base64')}`)
+ }
+ proxy.on('connect', () => {
+ t.pass('proxy should be called')
+ })
+
+ server.on('request', (req, res) => {
+ t.equal(req.url, '/hello?foo=bar')
+ t.equal(req.headers.host, parsedOrigin.host, 'should not use proxyUrl as host')
+ res.setHeader('content-type', 'application/json')
+ res.end(JSON.stringify({ hello: 'world' }))
+ })
+
+ const {
+ statusCode,
+ headers,
+ body
+ } = await request(serverUrl + '/hello?foo=bar', { dispatcher: proxyAgent })
+ const json = await body.json()
+
+ t.equal(statusCode, 200)
+ t.same(json, { hello: 'world' })
+ t.equal(headers.connection, 'keep-alive', 'should remain the connection open')
+
+ server.close()
+ proxy.close()
+ proxyAgent.close()
+})
+
+test('use proxy-agent with token', async (t) => {
+ t.plan(7)
+ const server = await buildServer()
+ const proxy = await buildProxy()
+
+ const serverUrl = `http://localhost:${server.address().port}`
+ const proxyUrl = `http://localhost:${proxy.address().port}`
+ const proxyAgent = new ProxyAgent({
+ token: `Bearer ${Buffer.from('user:pass').toString('base64')}`,
+ uri: proxyUrl
+ })
+ const parsedOrigin = new URL(serverUrl)
+
+ proxy.authenticate = function (req, fn) {
+ t.pass('authentication should be called')
+ fn(null, req.headers['proxy-authorization'] === `Bearer ${Buffer.from('user:pass').toString('base64')}`)
+ }
+ proxy.on('connect', () => {
+ t.pass('proxy should be called')
+ })
+
+ server.on('request', (req, res) => {
+ t.equal(req.url, '/hello?foo=bar')
+ t.equal(req.headers.host, parsedOrigin.host, 'should not use proxyUrl as host')
+ res.setHeader('content-type', 'application/json')
+ res.end(JSON.stringify({ hello: 'world' }))
+ })
+
+ const {
+ statusCode,
+ headers,
+ body
+ } = await request(serverUrl + '/hello?foo=bar', { dispatcher: proxyAgent })
+ const json = await body.json()
+
+ t.equal(statusCode, 200)
+ t.same(json, { hello: 'world' })
+ t.equal(headers.connection, 'keep-alive', 'should remain the connection open')
+
+ server.close()
+ proxy.close()
+ proxyAgent.close()
+})
+
+test('use proxy-agent with custom headers', async (t) => {
+ t.plan(2)
+ const server = await buildServer()
+ const proxy = await buildProxy()
+
+ const serverUrl = `http://localhost:${server.address().port}`
+ const proxyUrl = `http://localhost:${proxy.address().port}`
+ const proxyAgent = new ProxyAgent({
+ uri: proxyUrl,
+ headers: {
+ 'User-Agent': 'Foobar/1.0.0'
+ }
+ })
+
+ proxy.on('connect', (req) => {
+ t.equal(req.headers['user-agent'], 'Foobar/1.0.0')
+ })
+
+ server.on('request', (req, res) => {
+ t.equal(req.headers['user-agent'], 'BarBaz/1.0.0')
+ res.end()
+ })
+
+ await request(serverUrl + '/hello?foo=bar', {
+ headers: { 'user-agent': 'BarBaz/1.0.0' },
+ dispatcher: proxyAgent
+ })
+
+ server.close()
+ proxy.close()
+ proxyAgent.close()
+})
+
+test('sending proxy-authorization in request headers should throw', async (t) => {
+ t.plan(3)
+ const server = await buildServer()
+ const proxy = await buildProxy()
+
+ const serverUrl = `http://localhost:${server.address().port}`
+ const proxyUrl = `http://localhost:${proxy.address().port}`
+ const proxyAgent = new ProxyAgent(proxyUrl)
+
+ server.on('request', (req, res) => {
+ res.end(JSON.stringify({ hello: 'world' }))
+ })
+
+ await t.rejects(
+ request(
+ serverUrl + '/hello?foo=bar',
+ {
+ dispatcher: proxyAgent,
+ headers: {
+ 'proxy-authorization': Buffer.from('user:pass').toString('base64')
+ }
+ }
+ ),
+ 'Proxy-Authorization should be sent in ProxyAgent'
+ )
+
+ await t.rejects(
+ request(
+ serverUrl + '/hello?foo=bar',
+ {
+ dispatcher: proxyAgent,
+ headers: {
+ 'PROXY-AUTHORIZATION': Buffer.from('user:pass').toString('base64')
+ }
+ }
+ ),
+ 'Proxy-Authorization should be sent in ProxyAgent'
+ )
+
+ await t.rejects(
+ request(
+ serverUrl + '/hello?foo=bar',
+ {
+ dispatcher: proxyAgent,
+ headers: {
+ 'Proxy-Authorization': Buffer.from('user:pass').toString('base64')
+ }
+ }
+ ),
+ 'Proxy-Authorization should be sent in ProxyAgent'
+ )
+
+ server.close()
+ proxy.close()
+ proxyAgent.close()
+})
+
+test('use proxy-agent with setGlobalDispatcher', async (t) => {
+ t.plan(6)
+ const defaultDispatcher = getGlobalDispatcher()
+
+ const server = await buildServer()
+ const proxy = await buildProxy()
+
+ const serverUrl = `http://localhost:${server.address().port}`
+ const proxyUrl = `http://localhost:${proxy.address().port}`
+ const proxyAgent = new ProxyAgent(proxyUrl)
+ const parsedOrigin = new URL(serverUrl)
+ setGlobalDispatcher(proxyAgent)
+
+ t.teardown(() => setGlobalDispatcher(defaultDispatcher))
+
+ proxy.on('connect', () => {
+ t.pass('should call proxy')
+ })
+ server.on('request', (req, res) => {
+ t.equal(req.url, '/hello?foo=bar')
+ t.equal(req.headers.host, parsedOrigin.host, 'should not use proxyUrl as host')
+ res.setHeader('content-type', 'application/json')
+ res.end(JSON.stringify({ hello: 'world' }))
+ })
+
+ const {
+ statusCode,
+ headers,
+ body
+ } = await request(serverUrl + '/hello?foo=bar')
+ const json = await body.json()
+
+ t.equal(statusCode, 200)
+ t.same(json, { hello: 'world' })
+ t.equal(headers.connection, 'keep-alive', 'should remain the connection open')
+
+ server.close()
+ proxy.close()
+ proxyAgent.close()
+})
+
+test('ProxyAgent correctly sends headers when using fetch - #1355, #1623', { skip: nodeMajor < 16 }, async (t) => {
+ t.plan(2)
+ const defaultDispatcher = getGlobalDispatcher()
+
+ const server = await buildServer()
+ const proxy = await buildProxy()
+
+ const serverUrl = `http://localhost:${server.address().port}`
+ const proxyUrl = `http://localhost:${proxy.address().port}`
+
+ const proxyAgent = new ProxyAgent(proxyUrl)
+ setGlobalDispatcher(proxyAgent)
+
+ t.teardown(() => setGlobalDispatcher(defaultDispatcher))
+
+ const expectedHeaders = {
+ host: `localhost:${server.address().port}`,
+ connection: 'keep-alive',
+ 'test-header': 'value',
+ accept: '*/*',
+ 'accept-language': '*',
+ 'sec-fetch-mode': 'cors',
+ 'user-agent': 'undici',
+ 'accept-encoding': 'gzip, deflate'
+ }
+
+ const expectedProxyHeaders = {
+ host: `localhost:${proxy.address().port}`,
+ connection: 'close'
+ }
+
+ proxy.on('connect', (req, res) => {
+ t.same(req.headers, expectedProxyHeaders)
+ })
+
+ server.on('request', (req, res) => {
+ t.same(req.headers, expectedHeaders)
+ res.end('goodbye')
+ })
+
+ await fetch(serverUrl, {
+ headers: { 'Test-header': 'value' }
+ })
+
+ server.close()
+ proxy.close()
+ proxyAgent.close()
+ t.end()
+})
+
+test('should throw when proxy does not return 200', async (t) => {
+ const server = await buildServer()
+ const proxy = await buildProxy()
+
+ const serverUrl = `http://localhost:${server.address().port}`
+ const proxyUrl = `http://localhost:${proxy.address().port}`
+
+ proxy.authenticate = function (req, fn) {
+ fn(null, false)
+ }
+
+ const proxyAgent = new ProxyAgent(proxyUrl)
+ try {
+ await request(serverUrl, { dispatcher: proxyAgent })
+ t.fail()
+ } catch (e) {
+ t.pass()
+ t.ok(e)
+ }
+
+ server.close()
+ proxy.close()
+ proxyAgent.close()
+ t.end()
+})
+
+test('pass ProxyAgent proxy status code error when using fetch - #2161', { skip: nodeMajor < 16 }, async (t) => {
+ const server = await buildServer()
+ const proxy = await buildProxy()
+
+ const serverUrl = `http://localhost:${server.address().port}`
+ const proxyUrl = `http://localhost:${proxy.address().port}`
+
+ proxy.authenticate = function (req, fn) {
+ fn(null, false)
+ }
+
+ const proxyAgent = new ProxyAgent(proxyUrl)
+ try {
+ await fetch(serverUrl, { dispatcher: proxyAgent })
+ } catch (e) {
+ t.hasProp(e, 'cause')
+ }
+
+ server.close()
+ proxy.close()
+ proxyAgent.close()
+ t.end()
+})
+
+test('Proxy via HTTP to HTTPS endpoint', async (t) => {
+ t.plan(4)
+
+ const server = await buildSSLServer()
+ const proxy = await buildProxy()
+
+ const serverUrl = `https://localhost:${server.address().port}`
+ const proxyUrl = `http://localhost:${proxy.address().port}`
+ const proxyAgent = new ProxyAgent({
+ uri: proxyUrl,
+ requestTls: {
+ ca: [
+ readFileSync(join(__dirname, 'fixtures', 'ca.pem'), 'utf8')
+ ],
+ key: readFileSync(join(__dirname, 'fixtures', 'client-key-2048.pem'), 'utf8'),
+ cert: readFileSync(join(__dirname, 'fixtures', 'client-crt-2048.pem'), 'utf8'),
+ servername: 'agent1'
+ }
+ })
+
+ server.on('request', function (req, res) {
+ t.ok(req.connection.encrypted)
+ res.end(JSON.stringify(req.headers))
+ })
+
+ server.on('secureConnection', () => {
+ t.pass('server should be connected secured')
+ })
+
+ proxy.on('secureConnection', () => {
+ t.fail('proxy over http should not call secureConnection')
+ })
+
+ proxy.on('connect', function () {
+ t.pass('proxy should be connected')
+ })
+
+ proxy.on('request', function () {
+ t.fail('proxy should never receive requests')
+ })
+
+ const data = await request(serverUrl, { dispatcher: proxyAgent })
+ const json = await data.body.json()
+ t.strictSame(json, {
+ host: `localhost:${server.address().port}`,
+ connection: 'keep-alive'
+ })
+
+ server.close()
+ proxy.close()
+ proxyAgent.close()
+})
+
+test('Proxy via HTTPS to HTTPS endpoint', async (t) => {
+ t.plan(5)
+ const server = await buildSSLServer()
+ const proxy = await buildSSLProxy()
+
+ const serverUrl = `https://localhost:${server.address().port}`
+ const proxyUrl = `https://localhost:${proxy.address().port}`
+ const proxyAgent = new ProxyAgent({
+ uri: proxyUrl,
+ proxyTls: {
+ ca: [
+ readFileSync(join(__dirname, 'fixtures', 'ca.pem'), 'utf8')
+ ],
+ key: readFileSync(join(__dirname, 'fixtures', 'client-key-2048.pem'), 'utf8'),
+ cert: readFileSync(join(__dirname, 'fixtures', 'client-crt-2048.pem'), 'utf8'),
+ servername: 'agent1',
+ rejectUnauthorized: false
+ },
+ requestTls: {
+ ca: [
+ readFileSync(join(__dirname, 'fixtures', 'ca.pem'), 'utf8')
+ ],
+ key: readFileSync(join(__dirname, 'fixtures', 'client-key-2048.pem'), 'utf8'),
+ cert: readFileSync(join(__dirname, 'fixtures', 'client-crt-2048.pem'), 'utf8'),
+ servername: 'agent1'
+ }
+ })
+
+ server.on('request', function (req, res) {
+ t.ok(req.connection.encrypted)
+ res.end(JSON.stringify(req.headers))
+ })
+
+ server.on('secureConnection', () => {
+ t.pass('server should be connected secured')
+ })
+
+ proxy.on('secureConnection', () => {
+ t.pass('proxy over http should call secureConnection')
+ })
+
+ proxy.on('connect', function () {
+ t.pass('proxy should be connected')
+ })
+
+ proxy.on('request', function () {
+ t.fail('proxy should never receive requests')
+ })
+
+ const data = await request(serverUrl, { dispatcher: proxyAgent })
+ const json = await data.body.json()
+ t.strictSame(json, {
+ host: `localhost:${server.address().port}`,
+ connection: 'keep-alive'
+ })
+
+ server.close()
+ proxy.close()
+ proxyAgent.close()
+})
+
+test('Proxy via HTTPS to HTTP endpoint', async (t) => {
+ t.plan(3)
+ const server = await buildServer()
+ const proxy = await buildSSLProxy()
+
+ const serverUrl = `http://localhost:${server.address().port}`
+ const proxyUrl = `https://localhost:${proxy.address().port}`
+ const proxyAgent = new ProxyAgent({
+ uri: proxyUrl,
+ proxyTls: {
+ ca: [
+ readFileSync(join(__dirname, 'fixtures', 'ca.pem'), 'utf8')
+ ],
+ key: readFileSync(join(__dirname, 'fixtures', 'client-key-2048.pem'), 'utf8'),
+ cert: readFileSync(join(__dirname, 'fixtures', 'client-crt-2048.pem'), 'utf8'),
+ servername: 'agent1',
+ rejectUnauthorized: false
+ }
+ })
+
+ server.on('request', function (req, res) {
+ t.ok(!req.connection.encrypted)
+ res.end(JSON.stringify(req.headers))
+ })
+
+ server.on('secureConnection', () => {
+ t.fail('server is http')
+ })
+
+ proxy.on('secureConnection', () => {
+ t.pass('proxy over http should call secureConnection')
+ })
+
+ proxy.on('request', function () {
+ t.fail('proxy should never receive requests')
+ })
+
+ const data = await request(serverUrl, { dispatcher: proxyAgent })
+ const json = await data.body.json()
+ t.strictSame(json, {
+ host: `localhost:${server.address().port}`,
+ connection: 'keep-alive'
+ })
+
+ server.close()
+ proxy.close()
+ proxyAgent.close()
+})
+
+test('Proxy via HTTP to HTTP endpoint', async (t) => {
+ t.plan(3)
+ const server = await buildServer()
+ const proxy = await buildProxy()
+
+ const serverUrl = `http://localhost:${server.address().port}`
+ const proxyUrl = `http://localhost:${proxy.address().port}`
+ const proxyAgent = new ProxyAgent(proxyUrl)
+
+ server.on('request', function (req, res) {
+ t.ok(!req.connection.encrypted)
+ res.end(JSON.stringify(req.headers))
+ })
+
+ server.on('secureConnection', () => {
+ t.fail('server is http')
+ })
+
+ proxy.on('secureConnection', () => {
+ t.fail('proxy is http')
+ })
+
+ proxy.on('connect', () => {
+ t.pass('connect to proxy')
+ })
+
+ proxy.on('request', function () {
+ t.fail('proxy should never receive requests')
+ })
+
+ const data = await request(serverUrl, { dispatcher: proxyAgent })
+ const json = await data.body.json()
+ t.strictSame(json, {
+ host: `localhost:${server.address().port}`,
+ connection: 'keep-alive'
+ })
+
+ server.close()
+ proxy.close()
+ proxyAgent.close()
+})
+
+function buildServer () {
+ return new Promise((resolve) => {
+ const server = createServer()
+ server.listen(0, () => resolve(server))
+ })
+}
+
+function buildSSLServer () {
+ const serverOptions = {
+ ca: [
+ readFileSync(join(__dirname, 'fixtures', 'client-ca-crt.pem'), 'utf8')
+ ],
+ key: readFileSync(join(__dirname, 'fixtures', 'key.pem'), 'utf8'),
+ cert: readFileSync(join(__dirname, 'fixtures', 'cert.pem'), 'utf8')
+ }
+ return new Promise((resolve) => {
+ const server = https.createServer(serverOptions)
+ server.listen(0, () => resolve(server))
+ })
+}
+
+function buildProxy (listener) {
+ return new Promise((resolve) => {
+ const server = listener
+ ? proxy(createServer(listener))
+ : proxy(createServer())
+ server.listen(0, () => resolve(server))
+ })
+}
+
+function buildSSLProxy () {
+ const serverOptions = {
+ ca: [
+ readFileSync(join(__dirname, 'fixtures', 'client-ca-crt.pem'), 'utf8')
+ ],
+ key: readFileSync(join(__dirname, 'fixtures', 'key.pem'), 'utf8'),
+ cert: readFileSync(join(__dirname, 'fixtures', 'cert.pem'), 'utf8')
+ }
+
+ return new Promise((resolve) => {
+ const server = proxy(https.createServer(serverOptions))
+ server.listen(0, () => resolve(server))
+ })
+}
+
+teardown(() => process.exit())
diff --git a/test/proxy.js b/test/proxy.js
new file mode 100644
index 0000000..d6d8d42
--- /dev/null
+++ b/test/proxy.js
@@ -0,0 +1,132 @@
+'use strict'
+
+const { test } = require('tap')
+const { Client, Pool } = require('..')
+const { createServer } = require('http')
+const proxy = require('proxy')
+
+test('connect through proxy', async (t) => {
+ t.plan(3)
+
+ const server = await buildServer()
+ const proxy = await buildProxy()
+
+ const serverUrl = `http://localhost:${server.address().port}`
+ const proxyUrl = `http://localhost:${proxy.address().port}`
+
+ server.on('request', (req, res) => {
+ t.equal(req.url, '/hello?foo=bar')
+ res.setHeader('content-type', 'application/json')
+ res.end(JSON.stringify({ hello: 'world' }))
+ })
+
+ const client = new Client(proxyUrl)
+
+ const response = await client.request({
+ method: 'GET',
+ path: serverUrl + '/hello?foo=bar'
+ })
+
+ response.body.setEncoding('utf8')
+ let data = ''
+ for await (const chunk of response.body) {
+ data += chunk
+ }
+ t.equal(response.statusCode, 200)
+ t.same(JSON.parse(data), { hello: 'world' })
+
+ server.close()
+ proxy.close()
+ client.close()
+})
+
+test('connect through proxy with auth', async (t) => {
+ t.plan(3)
+
+ const server = await buildServer()
+ const proxy = await buildProxy()
+
+ const serverUrl = `http://localhost:${server.address().port}`
+ const proxyUrl = `http://localhost:${proxy.address().port}`
+
+ proxy.authenticate = function (req, fn) {
+ fn(null, req.headers['proxy-authorization'] === `Basic ${Buffer.from('user:pass').toString('base64')}`)
+ }
+
+ server.on('request', (req, res) => {
+ t.equal(req.url, '/hello?foo=bar')
+ res.setHeader('content-type', 'application/json')
+ res.end(JSON.stringify({ hello: 'world' }))
+ })
+
+ const client = new Client(proxyUrl)
+
+ const response = await client.request({
+ method: 'GET',
+ path: serverUrl + '/hello?foo=bar',
+ headers: {
+ 'proxy-authorization': `Basic ${Buffer.from('user:pass').toString('base64')}`
+ }
+ })
+
+ response.body.setEncoding('utf8')
+ let data = ''
+ for await (const chunk of response.body) {
+ data += chunk
+ }
+ t.equal(response.statusCode, 200)
+ t.same(JSON.parse(data), { hello: 'world' })
+
+ server.close()
+ proxy.close()
+ client.close()
+})
+
+test('connect through proxy (with pool)', async (t) => {
+ t.plan(3)
+
+ const server = await buildServer()
+ const proxy = await buildProxy()
+
+ const serverUrl = `http://localhost:${server.address().port}`
+ const proxyUrl = `http://localhost:${proxy.address().port}`
+
+ server.on('request', (req, res) => {
+ t.equal(req.url, '/hello?foo=bar')
+ res.setHeader('content-type', 'application/json')
+ res.end(JSON.stringify({ hello: 'world' }))
+ })
+
+ const pool = new Pool(proxyUrl)
+
+ const response = await pool.request({
+ method: 'GET',
+ path: serverUrl + '/hello?foo=bar'
+ })
+
+ response.body.setEncoding('utf8')
+ let data = ''
+ for await (const chunk of response.body) {
+ data += chunk
+ }
+ t.equal(response.statusCode, 200)
+ t.same(JSON.parse(data), { hello: 'world' })
+
+ server.close()
+ proxy.close()
+ pool.close()
+})
+
+function buildServer () {
+ return new Promise((resolve, reject) => {
+ const server = createServer()
+ server.listen(0, () => resolve(server))
+ })
+}
+
+function buildProxy () {
+ return new Promise((resolve, reject) => {
+ const server = proxy(createServer())
+ server.listen(0, () => resolve(server))
+ })
+}
diff --git a/test/readable.test.js b/test/readable.test.js
new file mode 100644
index 0000000..3f4f793
--- /dev/null
+++ b/test/readable.test.js
@@ -0,0 +1,23 @@
+'use strict'
+
+const { test } = require('tap')
+const Readable = require('../lib/api/readable')
+
+test('avoid body reordering', async function (t) {
+ function resume () {
+ }
+ function abort () {
+ }
+ const r = new Readable({ resume, abort })
+
+ r.push(Buffer.from('hello'))
+
+ process.nextTick(() => {
+ r.push(Buffer.from('world'))
+ r.push(null)
+ })
+
+ const text = await r.text()
+
+ t.equal(text, 'helloworld')
+})
diff --git a/test/redirect-pipeline.js b/test/redirect-pipeline.js
new file mode 100644
index 0000000..e4be837
--- /dev/null
+++ b/test/redirect-pipeline.js
@@ -0,0 +1,50 @@
+'use strict'
+
+const t = require('tap')
+const { pipeline: undiciPipeline } = require('..')
+const { pipeline: streamPipelineCb } = require('stream')
+const { promisify } = require('util')
+const { createReadable, createWritable } = require('./utils/stream')
+const { startRedirectingServer } = require('./utils/redirecting-servers')
+
+const streamPipeline = promisify(streamPipelineCb)
+
+t.test('should not follow redirection by default if not using RedirectAgent', async t => {
+ t.plan(3)
+
+ const body = []
+ const serverRoot = await startRedirectingServer(t)
+
+ await streamPipeline(
+ createReadable('REQUEST'),
+ undiciPipeline(`http://${serverRoot}/`, {}, ({ statusCode, headers, body }) => {
+ t.equal(statusCode, 302)
+ t.equal(headers.location, `http://${serverRoot}/302/1`)
+
+ return body
+ }),
+ createWritable(body)
+ )
+
+ t.equal(body.length, 0)
+})
+
+t.test('should not follow redirects when using RedirectAgent within pipeline', async t => {
+ t.plan(3)
+
+ const body = []
+ const serverRoot = await startRedirectingServer(t)
+
+ await streamPipeline(
+ createReadable('REQUEST'),
+ undiciPipeline(`http://${serverRoot}/`, { maxRedirections: 1 }, ({ statusCode, headers, body }) => {
+ t.equal(statusCode, 302)
+ t.equal(headers.location, `http://${serverRoot}/302/1`)
+
+ return body
+ }),
+ createWritable(body)
+ )
+
+ t.equal(body.length, 0)
+})
diff --git a/test/redirect-relative.js b/test/redirect-relative.js
new file mode 100644
index 0000000..ca9c541
--- /dev/null
+++ b/test/redirect-relative.js
@@ -0,0 +1,22 @@
+'use strict'
+
+const t = require('tap')
+const { request } = require('..')
+const {
+ startRedirectingWithRelativePath
+} = require('./utils/redirecting-servers')
+
+t.test('should redirect to relative URL according to RFC 7231', async t => {
+ t.plan(2)
+
+ const server = await startRedirectingWithRelativePath(t)
+
+ const { statusCode, body } = await request(`http://${server}`, {
+ maxRedirections: 3
+ })
+
+ const finalPath = await body.text()
+
+ t.equal(statusCode, 200)
+ t.equal(finalPath, '/absolute/b')
+})
diff --git a/test/redirect-request.js b/test/redirect-request.js
new file mode 100644
index 0000000..5a1ae6d
--- /dev/null
+++ b/test/redirect-request.js
@@ -0,0 +1,420 @@
+'use strict'
+
+const t = require('tap')
+const undici = require('..')
+const { nodeMajor } = require('../lib/core/util')
+const {
+ startRedirectingServer,
+ startRedirectingWithBodyServer,
+ startRedirectingChainServers,
+ startRedirectingWithoutLocationServer,
+ startRedirectingWithAuthorization,
+ startRedirectingWithCookie,
+ startRedirectingWithQueryParams
+} = require('./utils/redirecting-servers')
+const { createReadable, createReadableStream } = require('./utils/stream')
+
+for (const factory of [
+ (server, opts) => new undici.Agent(opts),
+ (server, opts) => new undici.Pool(`http://${server}`, opts),
+ (server, opts) => new undici.Client(`http://${server}`, opts)
+]) {
+ const request = (t, server, opts, ...args) => {
+ const dispatcher = factory(server, opts)
+ t.teardown(() => dispatcher.close())
+ return undici.request(args[0], { ...args[1], dispatcher }, args[2])
+ }
+
+ t.test('should always have a history with the final URL even if no redirections were followed', async t => {
+ const server = await startRedirectingServer(t)
+
+ const { statusCode, headers, body: bodyStream, context: { history } } = await request(t, server, undefined, `http://${server}/200?key=value`, {
+ maxRedirections: 10
+ })
+
+ const body = await bodyStream.text()
+
+ t.equal(statusCode, 200)
+ t.notOk(headers.location)
+ t.same(history.map(x => x.toString()), [`http://${server}/200?key=value`])
+ t.equal(body, `GET /5 key=value :: host@${server} connection@keep-alive`)
+ })
+
+ t.test('should not follow redirection by default if not using RedirectAgent', async t => {
+ const server = await startRedirectingServer(t)
+
+ const { statusCode, headers, body: bodyStream } = await request(t, server, undefined, `http://${server}`)
+ const body = await bodyStream.text()
+
+ t.equal(statusCode, 302)
+ t.equal(headers.location, `http://${server}/302/1`)
+ t.equal(body.length, 0)
+ })
+
+ t.test('should follow redirection after a HTTP 300', async t => {
+ const server = await startRedirectingServer(t)
+
+ const { statusCode, headers, body: bodyStream, context: { history } } = await request(t, server, undefined, `http://${server}/300?key=value`, {
+ maxRedirections: 10
+ })
+
+ const body = await bodyStream.text()
+
+ t.equal(statusCode, 200)
+ t.notOk(headers.location)
+ t.same(history.map(x => x.toString()), [
+ `http://${server}/300?key=value`,
+ `http://${server}/300/1?key=value`,
+ `http://${server}/300/2?key=value`,
+ `http://${server}/300/3?key=value`,
+ `http://${server}/300/4?key=value`,
+ `http://${server}/300/5?key=value`
+ ])
+ t.equal(body, `GET /5 key=value :: host@${server} connection@keep-alive`)
+ })
+
+ t.test('should follow redirection after a HTTP 300 default', async t => {
+ const server = await startRedirectingServer(t)
+
+ const { statusCode, headers, body: bodyStream, context: { history } } = await request(t, server, { maxRedirections: 10 }, `http://${server}/300?key=value`)
+ const body = await bodyStream.text()
+
+ t.equal(statusCode, 200)
+ t.notOk(headers.location)
+ t.same(history.map(x => x.toString()), [
+ `http://${server}/300?key=value`,
+ `http://${server}/300/1?key=value`,
+ `http://${server}/300/2?key=value`,
+ `http://${server}/300/3?key=value`,
+ `http://${server}/300/4?key=value`,
+ `http://${server}/300/5?key=value`
+ ])
+ t.equal(body, `GET /5 key=value :: host@${server} connection@keep-alive`)
+ })
+
+ t.test('should follow redirection after a HTTP 301', async t => {
+ const server = await startRedirectingServer(t)
+
+ const { statusCode, headers, body: bodyStream } = await request(t, server, undefined, `http://${server}/301`, {
+ method: 'POST',
+ body: 'REQUEST',
+ maxRedirections: 10
+ })
+
+ const body = await bodyStream.text()
+
+ t.equal(statusCode, 200)
+ t.notOk(headers.location)
+ t.equal(body, `POST /5 :: host@${server} connection@keep-alive content-length@7 :: REQUEST`)
+ })
+
+ t.test('should follow redirection after a HTTP 302', async t => {
+ const server = await startRedirectingServer(t)
+
+ const { statusCode, headers, body: bodyStream } = await request(t, server, undefined, `http://${server}/302`, {
+ method: 'PUT',
+ body: Buffer.from('REQUEST'),
+ maxRedirections: 10
+ })
+
+ const body = await bodyStream.text()
+
+ t.equal(statusCode, 200)
+ t.notOk(headers.location)
+ t.equal(body, `PUT /5 :: host@${server} connection@keep-alive content-length@7 :: REQUEST`)
+ })
+
+ t.test('should follow redirection after a HTTP 303 changing method to GET', async t => {
+ const server = await startRedirectingServer(t)
+
+ const { statusCode, headers, body: bodyStream } = await request(t, server, undefined, `http://${server}/303`, {
+ method: 'PATCH',
+ body: 'REQUEST',
+ maxRedirections: 10
+ })
+
+ const body = await bodyStream.text()
+
+ t.equal(statusCode, 200)
+ t.notOk(headers.location)
+ t.equal(body, `GET /5 :: host@${server} connection@keep-alive`)
+ })
+
+ t.test('should remove Host and request body related headers when following HTTP 303 (array)', async t => {
+ const server = await startRedirectingServer(t)
+
+ const { statusCode, headers, body: bodyStream } = await request(t, server, undefined, `http://${server}/303`, {
+ method: 'PATCH',
+ headers: [
+ 'Content-Encoding',
+ 'gzip',
+ 'X-Foo1',
+ '1',
+ 'X-Foo2',
+ '2',
+ 'Content-Type',
+ 'application/json',
+ 'X-Foo3',
+ '3',
+ 'Host',
+ 'localhost',
+ 'X-Bar',
+ '4'
+ ],
+ maxRedirections: 10
+ })
+
+ const body = await bodyStream.text()
+
+ t.equal(statusCode, 200)
+ t.notOk(headers.location)
+ t.equal(body, `GET /5 :: host@${server} connection@keep-alive x-foo1@1 x-foo2@2 x-foo3@3 x-bar@4`)
+ })
+
+ t.test('should remove Host and request body related headers when following HTTP 303 (object)', async t => {
+ const server = await startRedirectingServer(t)
+
+ const { statusCode, headers, body: bodyStream } = await request(t, server, undefined, `http://${server}/303`, {
+ method: 'PATCH',
+ headers: {
+ 'Content-Encoding': 'gzip',
+ 'X-Foo1': '1',
+ 'X-Foo2': '2',
+ 'Content-Type': 'application/json',
+ 'X-Foo3': '3',
+ Host: 'localhost',
+ 'X-Bar': '4'
+ },
+ maxRedirections: 10
+ })
+
+ const body = await bodyStream.text()
+
+ t.equal(statusCode, 200)
+ t.notOk(headers.location)
+ t.equal(body, `GET /5 :: host@${server} connection@keep-alive x-foo1@1 x-foo2@2 x-foo3@3 x-bar@4`)
+ })
+
+ t.test('should follow redirection after a HTTP 307', async t => {
+ const server = await startRedirectingServer(t)
+
+ const { statusCode, headers, body: bodyStream } = await request(t, server, undefined, `http://${server}/307`, {
+ method: 'DELETE',
+ maxRedirections: 10
+ })
+
+ const body = await bodyStream.text()
+
+ t.equal(statusCode, 200)
+ t.notOk(headers.location)
+ t.equal(body, `DELETE /5 :: host@${server} connection@keep-alive`)
+ })
+
+ t.test('should follow redirection after a HTTP 308', async t => {
+ const server = await startRedirectingServer(t)
+
+ const { statusCode, headers, body: bodyStream } = await request(t, server, undefined, `http://${server}/308`, {
+ method: 'OPTIONS',
+ maxRedirections: 10
+ })
+
+ const body = await bodyStream.text()
+
+ t.equal(statusCode, 200)
+ t.notOk(headers.location)
+ t.equal(body, `OPTIONS /5 :: host@${server} connection@keep-alive`)
+ })
+
+ t.test('should ignore HTTP 3xx response bodies', async t => {
+ const server = await startRedirectingWithBodyServer(t)
+
+ const { statusCode, headers, body: bodyStream, context: { history } } = await request(t, server, undefined, `http://${server}/`, {
+ maxRedirections: 10
+ })
+
+ const body = await bodyStream.text()
+
+ t.equal(statusCode, 200)
+ t.notOk(headers.location)
+ t.same(history.map(x => x.toString()), [`http://${server}/`, `http://${server}/end`])
+ t.equal(body, 'FINAL')
+ })
+
+ t.test('should ignore query after redirection', async t => {
+ const server = await startRedirectingWithQueryParams(t)
+
+ const { statusCode, headers, context: { history } } = await request(t, server, undefined, `http://${server}/`, {
+ maxRedirections: 10,
+ query: { param1: 'first' }
+ })
+
+ t.equal(statusCode, 200)
+ t.notOk(headers.location)
+ t.same(history.map(x => x.toString()), [`http://${server}/`, `http://${server}/?param2=second`])
+ })
+
+ t.test('should follow a redirect chain up to the allowed number of times', async t => {
+ const server = await startRedirectingServer(t)
+
+ const { statusCode, headers, body: bodyStream, context: { history } } = await request(t, server, undefined, `http://${server}/300`, {
+ maxRedirections: 2
+ })
+
+ const body = await bodyStream.text()
+
+ t.equal(statusCode, 300)
+ t.equal(headers.location, `http://${server}/300/3`)
+ t.same(history.map(x => x.toString()), [`http://${server}/300`, `http://${server}/300/1`, `http://${server}/300/2`])
+ t.equal(body.length, 0)
+ })
+
+ t.test('when a Location response header is NOT present', async t => {
+ const redirectCodes = [300, 301, 302, 303, 307, 308]
+ const server = await startRedirectingWithoutLocationServer(t)
+
+ for (const code of redirectCodes) {
+ const { statusCode, headers, body: bodyStream } = await request(t, server, undefined, `http://${server}/${code}`, {
+ maxRedirections: 10
+ })
+
+ const body = await bodyStream.text()
+
+ t.equal(statusCode, code)
+ t.notOk(headers.location)
+ t.equal(body.length, 0)
+ }
+ })
+
+ t.test('should not allow invalid maxRedirections arguments', async t => {
+ try {
+ await request(t, 'localhost', undefined, 'http://localhost', {
+ method: 'GET',
+ maxRedirections: 'INVALID'
+ })
+
+ t.fail('Did not throw')
+ } catch (err) {
+ t.equal(err.message, 'maxRedirections must be a positive number')
+ }
+ })
+
+ t.test('should not allow invalid maxRedirections arguments default', async t => {
+ try {
+ await request(t, 'localhost', {
+ maxRedirections: 'INVALID'
+ }, 'http://localhost', {
+ method: 'GET'
+ })
+
+ t.fail('Did not throw')
+ } catch (err) {
+ t.equal(err.message, 'maxRedirections must be a positive number')
+ }
+ })
+
+ t.test('should not follow redirects when using ReadableStream request bodies', { skip: nodeMajor < 16 }, async t => {
+ const server = await startRedirectingServer(t)
+
+ const { statusCode, headers, body: bodyStream } = await request(t, server, undefined, `http://${server}/301`, {
+ method: 'POST',
+ body: createReadableStream('REQUEST'),
+ maxRedirections: 10
+ })
+
+ const body = await bodyStream.text()
+
+ t.equal(statusCode, 301)
+ t.equal(headers.location, `http://${server}/301/2`)
+ t.equal(body.length, 0)
+ })
+
+ t.test('should not follow redirects when using Readable request bodies', async t => {
+ const server = await startRedirectingServer(t)
+
+ const { statusCode, headers, body: bodyStream } = await request(t, server, undefined, `http://${server}/301`, {
+ method: 'POST',
+ body: createReadable('REQUEST'),
+ maxRedirections: 10
+ })
+
+ const body = await bodyStream.text()
+
+ t.equal(statusCode, 301)
+ t.equal(headers.location, `http://${server}/301/1`)
+ t.equal(body.length, 0)
+ })
+}
+
+t.test('should follow redirections when going cross origin', async t => {
+ const [server1, server2, server3] = await startRedirectingChainServers(t)
+
+ const { statusCode, headers, body: bodyStream, context: { history } } = await undici.request(`http://${server1}`, {
+ method: 'POST',
+ maxRedirections: 10
+ })
+
+ const body = await bodyStream.text()
+
+ t.equal(statusCode, 200)
+ t.notOk(headers.location)
+ t.same(history.map(x => x.toString()), [
+ `http://${server1}/`,
+ `http://${server2}/`,
+ `http://${server3}/`,
+ `http://${server2}/end`,
+ `http://${server3}/end`,
+ `http://${server1}/end`
+ ])
+ t.equal(body, 'POST')
+})
+
+t.test('should handle errors (callback)', t => {
+ t.plan(1)
+
+ undici.request(
+ 'http://localhost:0',
+ {
+ maxRedirections: 10
+ },
+ error => {
+ t.match(error.code, /EADDRNOTAVAIL|ECONNREFUSED/)
+ }
+ )
+})
+
+t.test('should handle errors (promise)', async t => {
+ try {
+ await undici.request('http://localhost:0', { maxRedirections: 10 })
+ t.fail('Did not throw')
+ } catch (error) {
+ t.match(error.code, /EADDRNOTAVAIL|ECONNREFUSED/)
+ }
+})
+
+t.test('removes authorization header on third party origin', async t => {
+ const [server1] = await startRedirectingWithAuthorization(t, 'secret')
+ const { body: bodyStream } = await undici.request(`http://${server1}`, {
+ maxRedirections: 10,
+ headers: {
+ authorization: 'secret'
+ }
+ })
+
+ const body = await bodyStream.text()
+
+ t.equal(body, '')
+})
+
+t.test('removes cookie header on third party origin', async t => {
+ const [server1] = await startRedirectingWithCookie(t, 'a=b')
+ const { body: bodyStream } = await undici.request(`http://${server1}`, {
+ maxRedirections: 10,
+ headers: {
+ cookie: 'a=b'
+ }
+ })
+
+ const body = await bodyStream.text()
+
+ t.equal(body, '')
+})
diff --git a/test/redirect-stream.js b/test/redirect-stream.js
new file mode 100644
index 0000000..55dd97b
--- /dev/null
+++ b/test/redirect-stream.js
@@ -0,0 +1,423 @@
+'use strict'
+
+const t = require('tap')
+const { stream } = require('..')
+const {
+ startRedirectingServer,
+ startRedirectingWithBodyServer,
+ startRedirectingChainServers,
+ startRedirectingWithoutLocationServer,
+ startRedirectingWithAuthorization,
+ startRedirectingWithCookie
+} = require('./utils/redirecting-servers')
+const { createReadable, createWritable } = require('./utils/stream')
+
+t.test('should always have a history with the final URL even if no redirections were followed', async t => {
+ t.plan(4)
+
+ const body = []
+ const server = await startRedirectingServer(t)
+
+ await stream(
+ `http://${server}/200?key=value`,
+ { opaque: body, maxRedirections: 10 },
+ ({ statusCode, headers, opaque, context: { history } }) => {
+ t.equal(statusCode, 200)
+ t.notOk(headers.location)
+ t.same(history.map(x => x.toString()), [
+ `http://${server}/200?key=value`
+ ])
+
+ return createWritable(opaque)
+ }
+ )
+
+ t.equal(body.join(''), `GET /5 key=value :: host@${server} connection@keep-alive`)
+})
+
+t.test('should not follow redirection by default if not using RedirectAgent', async t => {
+ t.plan(3)
+
+ const body = []
+ const server = await startRedirectingServer(t)
+
+ await stream(`http://${server}`, { opaque: body }, ({ statusCode, headers, opaque }) => {
+ t.equal(statusCode, 302)
+ t.equal(headers.location, `http://${server}/302/1`)
+
+ return createWritable(opaque)
+ })
+
+ t.equal(body.length, 0)
+})
+
+t.test('should follow redirection after a HTTP 300', async t => {
+ t.plan(4)
+
+ const body = []
+ const server = await startRedirectingServer(t)
+
+ await stream(
+ `http://${server}/300?key=value`,
+ { opaque: body, maxRedirections: 10 },
+ ({ statusCode, headers, opaque, context: { history } }) => {
+ t.equal(statusCode, 200)
+ t.notOk(headers.location)
+ t.same(history.map(x => x.toString()), [
+ `http://${server}/300?key=value`,
+ `http://${server}/300/1?key=value`,
+ `http://${server}/300/2?key=value`,
+ `http://${server}/300/3?key=value`,
+ `http://${server}/300/4?key=value`,
+ `http://${server}/300/5?key=value`
+ ])
+
+ return createWritable(opaque)
+ }
+ )
+
+ t.equal(body.join(''), `GET /5 key=value :: host@${server} connection@keep-alive`)
+})
+
+t.test('should follow redirection after a HTTP 301', async t => {
+ t.plan(3)
+
+ const body = []
+ const server = await startRedirectingServer(t)
+
+ await stream(
+ `http://${server}/301`,
+ { method: 'POST', body: 'REQUEST', opaque: body, maxRedirections: 10 },
+ ({ statusCode, headers, opaque }) => {
+ t.equal(statusCode, 200)
+ t.notOk(headers.location)
+
+ return createWritable(opaque)
+ }
+ )
+
+ t.equal(body.join(''), `POST /5 :: host@${server} connection@keep-alive content-length@7 :: REQUEST`)
+})
+
+t.test('should follow redirection after a HTTP 302', async t => {
+ t.plan(3)
+
+ const body = []
+ const server = await startRedirectingServer(t)
+
+ await stream(
+ `http://${server}/302`,
+ { method: 'PUT', body: Buffer.from('REQUEST'), opaque: body, maxRedirections: 10 },
+ ({ statusCode, headers, opaque }) => {
+ t.equal(statusCode, 200)
+ t.notOk(headers.location)
+
+ return createWritable(opaque)
+ }
+ )
+
+ t.equal(body.join(''), `PUT /5 :: host@${server} connection@keep-alive content-length@7 :: REQUEST`)
+})
+
+t.test('should follow redirection after a HTTP 303 changing method to GET', async t => {
+ t.plan(3)
+
+ const body = []
+ const server = await startRedirectingServer(t)
+
+ await stream(`http://${server}/303`, { opaque: body, maxRedirections: 10 }, ({ statusCode, headers, opaque }) => {
+ t.equal(statusCode, 200)
+ t.notOk(headers.location)
+
+ return createWritable(opaque)
+ })
+
+ t.equal(body.join(''), `GET /5 :: host@${server} connection@keep-alive`)
+})
+
+t.test('should remove Host and request body related headers when following HTTP 303 (array)', async t => {
+ t.plan(3)
+
+ const body = []
+ const server = await startRedirectingServer(t)
+
+ await stream(
+ `http://${server}/303`,
+ {
+ method: 'PATCH',
+ headers: [
+ 'Content-Encoding',
+ 'gzip',
+ 'X-Foo1',
+ '1',
+ 'X-Foo2',
+ '2',
+ 'Content-Type',
+ 'application/json',
+ 'X-Foo3',
+ '3',
+ 'Host',
+ 'localhost',
+ 'X-Bar',
+ '4'
+ ],
+ opaque: body,
+ maxRedirections: 10
+ },
+ ({ statusCode, headers, opaque }) => {
+ t.equal(statusCode, 200)
+ t.notOk(headers.location)
+
+ return createWritable(opaque)
+ }
+ )
+
+ t.equal(body.join(''), `GET /5 :: host@${server} connection@keep-alive x-foo1@1 x-foo2@2 x-foo3@3 x-bar@4`)
+})
+
+t.test('should remove Host and request body related headers when following HTTP 303 (object)', async t => {
+ t.plan(3)
+
+ const body = []
+ const server = await startRedirectingServer(t)
+
+ await stream(
+ `http://${server}/303`,
+ {
+ method: 'PATCH',
+ headers: {
+ 'Content-Encoding': 'gzip',
+ 'X-Foo1': '1',
+ 'X-Foo2': '2',
+ 'Content-Type': 'application/json',
+ 'X-Foo3': '3',
+ Host: 'localhost',
+ 'X-Bar': '4'
+ },
+ opaque: body,
+ maxRedirections: 10
+ },
+ ({ statusCode, headers, opaque }) => {
+ t.equal(statusCode, 200)
+ t.notOk(headers.location)
+
+ return createWritable(opaque)
+ }
+ )
+
+ t.equal(body.join(''), `GET /5 :: host@${server} connection@keep-alive x-foo1@1 x-foo2@2 x-foo3@3 x-bar@4`)
+})
+
+t.test('should follow redirection after a HTTP 307', async t => {
+ t.plan(3)
+
+ const body = []
+ const server = await startRedirectingServer(t)
+
+ await stream(
+ `http://${server}/307`,
+ { method: 'DELETE', opaque: body, maxRedirections: 10 },
+ ({ statusCode, headers, opaque }) => {
+ t.equal(statusCode, 200)
+ t.notOk(headers.location)
+
+ return createWritable(opaque)
+ }
+ )
+
+ t.equal(body.join(''), `DELETE /5 :: host@${server} connection@keep-alive`)
+})
+
+t.test('should follow redirection after a HTTP 308', async t => {
+ t.plan(3)
+
+ const body = []
+ const server = await startRedirectingServer(t)
+
+ await stream(
+ `http://${server}/308`,
+ { method: 'OPTIONS', opaque: body, maxRedirections: 10 },
+ ({ statusCode, headers, opaque }) => {
+ t.equal(statusCode, 200)
+ t.notOk(headers.location)
+
+ return createWritable(opaque)
+ }
+ )
+
+ t.equal(body.join(''), `OPTIONS /5 :: host@${server} connection@keep-alive`)
+})
+
+t.test('should ignore HTTP 3xx response bodies', async t => {
+ t.plan(4)
+
+ const body = []
+ const server = await startRedirectingWithBodyServer(t)
+
+ await stream(
+ `http://${server}/`,
+ { opaque: body, maxRedirections: 10 },
+ ({ statusCode, headers, opaque, context: { history } }) => {
+ t.equal(statusCode, 200)
+ t.notOk(headers.location)
+ t.same(history.map(x => x.toString()), [`http://${server}/`, `http://${server}/end`])
+
+ return createWritable(opaque)
+ }
+ )
+
+ t.equal(body.join(''), 'FINAL')
+})
+
+t.test('should follow a redirect chain up to the allowed number of times', async t => {
+ t.plan(4)
+
+ const body = []
+ const server = await startRedirectingServer(t)
+
+ await stream(
+ `http://${server}/300`,
+ { opaque: body, maxRedirections: 2 },
+ ({ statusCode, headers, opaque, context: { history } }) => {
+ t.equal(statusCode, 300)
+ t.equal(headers.location, `http://${server}/300/3`)
+ t.same(history.map(x => x.toString()), [`http://${server}/300`, `http://${server}/300/1`, `http://${server}/300/2`])
+
+ return createWritable(opaque)
+ }
+ )
+
+ t.equal(body.length, 0)
+})
+
+t.test('should follow redirections when going cross origin', async t => {
+ t.plan(4)
+
+ const [server1, server2, server3] = await startRedirectingChainServers(t)
+ const body = []
+
+ await stream(
+ `http://${server1}`,
+ { method: 'POST', opaque: body, maxRedirections: 10 },
+ ({ statusCode, headers, opaque, context: { history } }) => {
+ t.equal(statusCode, 200)
+ t.notOk(headers.location)
+ t.same(history.map(x => x.toString()), [
+ `http://${server1}/`,
+ `http://${server2}/`,
+ `http://${server3}/`,
+ `http://${server2}/end`,
+ `http://${server3}/end`,
+ `http://${server1}/end`
+ ])
+
+ return createWritable(opaque)
+ }
+ )
+
+ t.equal(body.join(''), 'POST')
+})
+
+t.test('when a Location response header is NOT present', async t => {
+ const redirectCodes = [300, 301, 302, 303, 307, 308]
+ const server = await startRedirectingWithoutLocationServer(t)
+
+ for (const code of redirectCodes) {
+ t.test(`should return the original response after a HTTP ${code}`, async t => {
+ t.plan(3)
+
+ const body = []
+
+ await stream(
+ `http://${server}/${code}`,
+ { opaque: body, maxRedirections: 10 },
+ ({ statusCode, headers, opaque }) => {
+ t.equal(statusCode, code)
+ t.notOk(headers.location)
+
+ return createWritable(opaque)
+ }
+ )
+
+ t.equal(body.length, 0)
+ })
+ }
+})
+
+t.test('should not follow redirects when using Readable request bodies', async t => {
+ t.plan(3)
+
+ const body = []
+ const server = await startRedirectingServer(t)
+
+ await stream(
+ `http://${server}`,
+ {
+ method: 'POST',
+ body: createReadable('REQUEST'),
+ opaque: body,
+ maxRedirections: 10
+ },
+ ({ statusCode, headers, opaque }) => {
+ t.equal(statusCode, 302)
+ t.equal(headers.location, `http://${server}/302/1`)
+
+ return createWritable(opaque)
+ }
+ )
+
+ t.equal(body.length, 0)
+})
+
+t.test('should handle errors', async t => {
+ t.plan(2)
+
+ const body = []
+
+ try {
+ await stream('http://localhost:0', { opaque: body, maxRedirections: 10 }, ({ statusCode, headers, opaque }) => {
+ return createWritable(opaque)
+ })
+
+ throw new Error('Did not throw')
+ } catch (error) {
+ t.match(error.code, /EADDRNOTAVAIL|ECONNREFUSED/)
+ t.equal(body.length, 0)
+ }
+})
+
+t.test('removes authorization header on third party origin', async t => {
+ t.plan(1)
+
+ const body = []
+
+ const [server1] = await startRedirectingWithAuthorization(t, 'secret')
+ await stream(`http://${server1}`, {
+ maxRedirections: 10,
+ opaque: body,
+ headers: {
+ authorization: 'secret'
+ }
+ }, ({ statusCode, headers, opaque }) => createWritable(opaque))
+
+ t.equal(body.length, 0)
+})
+
+t.test('removes cookie header on third party origin', async t => {
+ t.plan(1)
+
+ const body = []
+
+ const [server1] = await startRedirectingWithCookie(t, 'a=b')
+ await stream(`http://${server1}`, {
+ maxRedirections: 10,
+ opaque: body,
+ headers: {
+ cookie: 'a=b'
+ }
+ }, ({ statusCode, headers, opaque }) => createWritable(opaque))
+
+ t.equal(body.length, 0)
+})
+
+t.teardown(() => process.exit())
diff --git a/test/redirect-upgrade.js b/test/redirect-upgrade.js
new file mode 100644
index 0000000..dbe5840
--- /dev/null
+++ b/test/redirect-upgrade.js
@@ -0,0 +1,34 @@
+'use strict'
+
+const t = require('tap')
+const { upgrade } = require('..')
+const { startServer } = require('./utils/redirecting-servers')
+
+t.test('should upgrade the connection when no redirects are present', async t => {
+ t.plan(2)
+
+ const server = await startServer(t, (req, res) => {
+ if (req.url === '/') {
+ res.statusCode = 301
+ res.setHeader('Location', `http://${server}/end`)
+ res.end('REDIRECT')
+ return
+ }
+
+ res.statusCode = 101
+ res.setHeader('Connection', 'upgrade')
+ res.setHeader('Upgrade', req.headers.upgrade)
+ res.end('')
+ })
+
+ const { headers, socket } = await upgrade(`http://${server}/`, {
+ method: 'GET',
+ protocol: 'foo/1',
+ maxRedirections: 10
+ })
+
+ socket.end()
+
+ t.equal(headers.connection, 'upgrade')
+ t.equal(headers.upgrade, 'foo/1')
+})
diff --git a/test/request-crlf.js b/test/request-crlf.js
new file mode 100644
index 0000000..abcecf0
--- /dev/null
+++ b/test/request-crlf.js
@@ -0,0 +1,32 @@
+'use strict'
+
+const { createServer } = require('http')
+const { test } = require('tap')
+const { request, errors } = require('..')
+
+test('should validate content-type CRLF Injection', (t) => {
+ t.plan(2)
+
+ const server = createServer((req, res) => {
+ t.fail('should not receive any request')
+ res.statusCode = 200
+ res.end('hello')
+ })
+
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, async () => {
+ try {
+ await request(`http://localhost:${server.address().port}`, {
+ method: 'GET',
+ headers: {
+ 'content-type': 'application/json\r\n\r\nGET /foo2 HTTP/1.1'
+ }
+ })
+ t.fail('request should fail')
+ } catch (e) {
+ t.type(e, errors.InvalidArgumentError)
+ t.equal(e.message, 'invalid content-type header')
+ }
+ })
+})
diff --git a/test/request-timeout.js b/test/request-timeout.js
new file mode 100644
index 0000000..3ec5c10
--- /dev/null
+++ b/test/request-timeout.js
@@ -0,0 +1,820 @@
+'use strict'
+
+const { test } = require('tap')
+const { createReadStream, writeFileSync, unlinkSync } = require('fs')
+const { Client, errors } = require('..')
+const { kConnect } = require('../lib/core/symbols')
+const { nodeMajor } = require('../lib/core/util')
+const timers = require('../lib/timers')
+const { createServer } = require('http')
+const EventEmitter = require('events')
+const FakeTimers = require('@sinonjs/fake-timers')
+const { AbortController } = require('abort-controller')
+const {
+ pipeline,
+ Readable,
+ Writable,
+ PassThrough
+} = require('stream')
+
+test('request timeout', (t) => {
+ t.plan(1)
+
+ const server = createServer((req, res) => {
+ setTimeout(() => {
+ res.end('hello')
+ }, 1000)
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, { headersTimeout: 500 })
+ t.teardown(client.destroy.bind(client))
+
+ client.request({ path: '/', method: 'GET' }, (err, response) => {
+ t.type(err, errors.HeadersTimeoutError)
+ })
+ })
+})
+
+test('request timeout with readable body', (t) => {
+ t.plan(1)
+
+ const server = createServer((req, res) => {
+ })
+ t.teardown(server.close.bind(server))
+
+ const tempfile = `${__filename}.10mb.txt`
+ writeFileSync(tempfile, Buffer.alloc(10 * 1024 * 1024))
+ t.teardown(() => unlinkSync(tempfile))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, { headersTimeout: 1e3 })
+ t.teardown(client.destroy.bind(client))
+
+ const body = createReadStream(tempfile)
+ client.request({ path: '/', method: 'POST', body }, (err, response) => {
+ t.type(err, errors.HeadersTimeoutError)
+ })
+ })
+}, { skip: nodeMajor < 14 })
+
+test('body timeout', (t) => {
+ t.plan(2)
+
+ const clock = FakeTimers.install()
+ t.teardown(clock.uninstall.bind(clock))
+
+ const orgTimers = { ...timers }
+ Object.assign(timers, { setTimeout, clearTimeout })
+ t.teardown(() => {
+ Object.assign(timers, orgTimers)
+ })
+
+ const server = createServer((req, res) => {
+ res.write('hello')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, { bodyTimeout: 50 })
+ t.teardown(client.destroy.bind(client))
+
+ client.request({ path: '/', method: 'GET' }, (err, { body }) => {
+ t.error(err)
+ body.on('data', () => {
+ clock.tick(100)
+ }).on('error', (err) => {
+ t.type(err, errors.BodyTimeoutError)
+ })
+ })
+
+ clock.tick(50)
+ })
+})
+
+test('overridden request timeout', (t) => {
+ t.plan(1)
+
+ const clock = FakeTimers.install()
+ t.teardown(clock.uninstall.bind(clock))
+
+ const orgTimers = { ...timers }
+ Object.assign(timers, { setTimeout, clearTimeout })
+ t.teardown(() => {
+ Object.assign(timers, orgTimers)
+ })
+
+ const server = createServer((req, res) => {
+ setTimeout(() => {
+ res.end('hello')
+ }, 100)
+ clock.tick(100)
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, { headersTimeout: 500 })
+ t.teardown(client.destroy.bind(client))
+
+ client.request({ path: '/', method: 'GET', headersTimeout: 50 }, (err, response) => {
+ t.type(err, errors.HeadersTimeoutError)
+ })
+
+ clock.tick(50)
+ })
+})
+
+test('overridden body timeout', (t) => {
+ t.plan(2)
+
+ const clock = FakeTimers.install()
+ t.teardown(clock.uninstall.bind(clock))
+
+ const orgTimers = { ...timers }
+ Object.assign(timers, { setTimeout, clearTimeout })
+ t.teardown(() => {
+ Object.assign(timers, orgTimers)
+ })
+
+ const server = createServer((req, res) => {
+ res.write('hello')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, { bodyTimeout: 500 })
+ t.teardown(client.destroy.bind(client))
+
+ client.request({ path: '/', method: 'GET', bodyTimeout: 50 }, (err, { body }) => {
+ t.error(err)
+ body.on('data', () => {
+ clock.tick(100)
+ }).on('error', (err) => {
+ t.type(err, errors.BodyTimeoutError)
+ })
+ })
+
+ clock.tick(50)
+ })
+})
+
+test('With EE signal', (t) => {
+ t.plan(1)
+
+ const clock = FakeTimers.install()
+ t.teardown(clock.uninstall.bind(clock))
+
+ const orgTimers = { ...timers }
+ Object.assign(timers, { setTimeout, clearTimeout })
+ t.teardown(() => {
+ Object.assign(timers, orgTimers)
+ })
+
+ const server = createServer((req, res) => {
+ setTimeout(() => {
+ res.end('hello')
+ }, 100)
+ clock.tick(100)
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, {
+ headersTimeout: 50
+ })
+ const ee = new EventEmitter()
+ t.teardown(client.destroy.bind(client))
+
+ client.request({ path: '/', method: 'GET', signal: ee }, (err, response) => {
+ t.type(err, errors.HeadersTimeoutError)
+ })
+
+ clock.tick(50)
+ })
+})
+
+test('With abort-controller signal', (t) => {
+ t.plan(1)
+
+ const clock = FakeTimers.install()
+ t.teardown(clock.uninstall.bind(clock))
+
+ const orgTimers = { ...timers }
+ Object.assign(timers, { setTimeout, clearTimeout })
+ t.teardown(() => {
+ Object.assign(timers, orgTimers)
+ })
+
+ const server = createServer((req, res) => {
+ setTimeout(() => {
+ res.end('hello')
+ }, 100)
+ clock.tick(100)
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, {
+ headersTimeout: 50
+ })
+ const abortController = new AbortController()
+ t.teardown(client.destroy.bind(client))
+
+ client.request({ path: '/', method: 'GET', signal: abortController.signal }, (err, response) => {
+ t.type(err, errors.HeadersTimeoutError)
+ })
+
+ clock.tick(50)
+ })
+})
+
+test('Abort before timeout (EE)', (t) => {
+ t.plan(1)
+
+ const clock = FakeTimers.install()
+ t.teardown(clock.uninstall.bind(clock))
+
+ const orgTimers = { ...timers }
+ Object.assign(timers, { setTimeout, clearTimeout })
+ t.teardown(() => {
+ Object.assign(timers, orgTimers)
+ })
+
+ const ee = new EventEmitter()
+ const server = createServer((req, res) => {
+ setTimeout(() => {
+ res.end('hello')
+ }, 100)
+ ee.emit('abort')
+ clock.tick(50)
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, {
+ headersTimeout: 50
+ })
+ t.teardown(client.destroy.bind(client))
+
+ client.request({ path: '/', method: 'GET', signal: ee }, (err, response) => {
+ t.type(err, errors.RequestAbortedError)
+ clock.tick(100)
+ })
+ })
+})
+
+test('Abort before timeout (abort-controller)', (t) => {
+ t.plan(1)
+
+ const clock = FakeTimers.install()
+ t.teardown(clock.uninstall.bind(clock))
+
+ const orgTimers = { ...timers }
+ Object.assign(timers, { setTimeout, clearTimeout })
+ t.teardown(() => {
+ Object.assign(timers, orgTimers)
+ })
+
+ const abortController = new AbortController()
+ const server = createServer((req, res) => {
+ setTimeout(() => {
+ res.end('hello')
+ }, 100)
+ abortController.abort()
+ clock.tick(50)
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, {
+ headersTimeout: 50
+ })
+ t.teardown(client.destroy.bind(client))
+
+ client.request({ path: '/', method: 'GET', signal: abortController.signal }, (err, response) => {
+ t.type(err, errors.RequestAbortedError)
+ clock.tick(100)
+ })
+ })
+})
+
+test('Timeout with pipelining', (t) => {
+ t.plan(3)
+
+ const clock = FakeTimers.install()
+ t.teardown(clock.uninstall.bind(clock))
+
+ const orgTimers = { ...timers }
+ Object.assign(timers, { setTimeout, clearTimeout })
+ t.teardown(() => {
+ Object.assign(timers, orgTimers)
+ })
+
+ const server = createServer((req, res) => {
+ setTimeout(() => {
+ res.end('hello')
+ }, 100)
+ clock.tick(50)
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, {
+ pipelining: 10,
+ headersTimeout: 50
+ })
+ t.teardown(client.destroy.bind(client))
+
+ client.request({ path: '/', method: 'GET' }, (err, response) => {
+ t.type(err, errors.HeadersTimeoutError)
+ })
+
+ client.request({ path: '/', method: 'GET' }, (err, response) => {
+ t.type(err, errors.HeadersTimeoutError)
+ })
+
+ client.request({ path: '/', method: 'GET' }, (err, response) => {
+ t.type(err, errors.HeadersTimeoutError)
+ })
+ })
+})
+
+test('Global option', (t) => {
+ t.plan(1)
+
+ const clock = FakeTimers.install()
+ t.teardown(clock.uninstall.bind(clock))
+
+ const orgTimers = { ...timers }
+ Object.assign(timers, { setTimeout, clearTimeout })
+ t.teardown(() => {
+ Object.assign(timers, orgTimers)
+ })
+
+ const server = createServer((req, res) => {
+ setTimeout(() => {
+ res.end('hello')
+ }, 100)
+ clock.tick(100)
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, {
+ headersTimeout: 50
+ })
+ t.teardown(client.destroy.bind(client))
+
+ client.request({ path: '/', method: 'GET' }, (err, response) => {
+ t.type(err, errors.HeadersTimeoutError)
+ })
+
+ clock.tick(50)
+ })
+})
+
+test('Request options overrides global option', (t) => {
+ t.plan(1)
+
+ const clock = FakeTimers.install()
+ t.teardown(clock.uninstall.bind(clock))
+
+ const orgTimers = { ...timers }
+ Object.assign(timers, { setTimeout, clearTimeout })
+ t.teardown(() => {
+ Object.assign(timers, orgTimers)
+ })
+
+ const server = createServer((req, res) => {
+ setTimeout(() => {
+ res.end('hello')
+ }, 100)
+ clock.tick(100)
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, {
+ headersTimeout: 50
+ })
+ t.teardown(client.destroy.bind(client))
+
+ client.request({ path: '/', method: 'GET' }, (err, response) => {
+ t.type(err, errors.HeadersTimeoutError)
+ })
+
+ clock.tick(50)
+ })
+})
+
+test('client.destroy should cancel the timeout', (t) => {
+ t.plan(2)
+
+ const server = createServer((req, res) => {
+ res.end('hello')
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, {
+ headersTimeout: 100
+ })
+
+ client.request({ path: '/', method: 'GET' }, (err, response) => {
+ t.type(err, errors.ClientDestroyedError)
+ })
+
+ client.destroy(err => {
+ t.error(err)
+ })
+ })
+})
+
+test('client.close should wait for the timeout', (t) => {
+ t.plan(2)
+
+ const clock = FakeTimers.install()
+ t.teardown(clock.uninstall.bind(clock))
+
+ const orgTimers = { ...timers }
+ Object.assign(timers, { setTimeout, clearTimeout })
+ t.teardown(() => {
+ Object.assign(timers, orgTimers)
+ })
+
+ const server = createServer((req, res) => {
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, {
+ headersTimeout: 100
+ })
+ t.teardown(client.destroy.bind(client))
+
+ client.request({ path: '/', method: 'GET' }, (err, response) => {
+ t.type(err, errors.HeadersTimeoutError)
+ })
+
+ client.close((err) => {
+ t.error(err)
+ })
+
+ client.on('connect', () => {
+ process.nextTick(() => {
+ clock.tick(100)
+ })
+ })
+ })
+})
+
+test('Validation', (t) => {
+ t.plan(4)
+
+ try {
+ const client = new Client('http://localhost:3000', {
+ headersTimeout: 'foobar'
+ })
+ t.teardown(client.destroy.bind(client))
+ } catch (err) {
+ t.type(err, errors.InvalidArgumentError)
+ }
+
+ try {
+ const client = new Client('http://localhost:3000', {
+ headersTimeout: -1
+ })
+ t.teardown(client.destroy.bind(client))
+ } catch (err) {
+ t.type(err, errors.InvalidArgumentError)
+ }
+
+ try {
+ const client = new Client('http://localhost:3000', {
+ bodyTimeout: 'foobar'
+ })
+ t.teardown(client.destroy.bind(client))
+ } catch (err) {
+ t.type(err, errors.InvalidArgumentError)
+ }
+
+ try {
+ const client = new Client('http://localhost:3000', {
+ bodyTimeout: -1
+ })
+ t.teardown(client.destroy.bind(client))
+ } catch (err) {
+ t.type(err, errors.InvalidArgumentError)
+ }
+})
+
+test('Disable request timeout', (t) => {
+ t.plan(2)
+
+ const clock = FakeTimers.install()
+ t.teardown(clock.uninstall.bind(clock))
+
+ const orgTimers = { ...timers }
+ Object.assign(timers, { setTimeout, clearTimeout })
+ t.teardown(() => {
+ Object.assign(timers, orgTimers)
+ })
+
+ const server = createServer((req, res) => {
+ setTimeout(() => {
+ res.end('hello')
+ }, 32e3)
+ clock.tick(33e3)
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, {
+ headersTimeout: 0,
+ connectTimeout: 0
+ })
+ t.teardown(client.destroy.bind(client))
+
+ client.request({ path: '/', method: 'GET' }, (err, response) => {
+ t.error(err)
+ const bufs = []
+ response.body.on('data', (buf) => {
+ bufs.push(buf)
+ })
+ response.body.on('end', () => {
+ t.equal('hello', Buffer.concat(bufs).toString('utf8'))
+ })
+ })
+
+ clock.tick(31e3)
+ })
+})
+
+test('Disable request timeout for a single request', (t) => {
+ t.plan(2)
+
+ const clock = FakeTimers.install()
+ t.teardown(clock.uninstall.bind(clock))
+
+ const orgTimers = { ...timers }
+ Object.assign(timers, { setTimeout, clearTimeout })
+ t.teardown(() => {
+ Object.assign(timers, orgTimers)
+ })
+
+ const server = createServer((req, res) => {
+ setTimeout(() => {
+ res.end('hello')
+ }, 32e3)
+ clock.tick(33e3)
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, {
+ headersTimeout: 0,
+ connectTimeout: 0
+ })
+ t.teardown(client.destroy.bind(client))
+
+ client.request({ path: '/', method: 'GET' }, (err, response) => {
+ t.error(err)
+ const bufs = []
+ response.body.on('data', (buf) => {
+ bufs.push(buf)
+ })
+ response.body.on('end', () => {
+ t.equal('hello', Buffer.concat(bufs).toString('utf8'))
+ })
+ })
+
+ clock.tick(31e3)
+ })
+})
+
+test('stream timeout', (t) => {
+ t.plan(1)
+
+ const clock = FakeTimers.install()
+ t.teardown(clock.uninstall.bind(clock))
+
+ const orgTimers = { ...timers }
+ Object.assign(timers, { setTimeout, clearTimeout })
+ t.teardown(() => {
+ Object.assign(timers, orgTimers)
+ })
+
+ const server = createServer((req, res) => {
+ setTimeout(() => {
+ res.end('hello')
+ }, 301e3)
+ clock.tick(301e3)
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, { connectTimeout: 0 })
+ t.teardown(client.destroy.bind(client))
+
+ client.stream({
+ path: '/',
+ method: 'GET',
+ opaque: new PassThrough()
+ }, (result) => {
+ t.fail('Should not be called')
+ }, (err) => {
+ t.type(err, errors.HeadersTimeoutError)
+ })
+ })
+})
+
+test('stream custom timeout', (t) => {
+ t.plan(1)
+
+ const clock = FakeTimers.install()
+ t.teardown(clock.uninstall.bind(clock))
+
+ const orgTimers = { ...timers }
+ Object.assign(timers, { setTimeout, clearTimeout })
+ t.teardown(() => {
+ Object.assign(timers, orgTimers)
+ })
+
+ const server = createServer((req, res) => {
+ setTimeout(() => {
+ res.end('hello')
+ }, 31e3)
+ clock.tick(31e3)
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, {
+ headersTimeout: 30e3
+ })
+ t.teardown(client.destroy.bind(client))
+
+ client.stream({
+ path: '/',
+ method: 'GET',
+ opaque: new PassThrough()
+ }, (result) => {
+ t.fail('Should not be called')
+ }, (err) => {
+ t.type(err, errors.HeadersTimeoutError)
+ })
+ })
+})
+
+test('pipeline timeout', (t) => {
+ t.plan(1)
+
+ const clock = FakeTimers.install()
+ t.teardown(clock.uninstall.bind(clock))
+
+ const orgTimers = { ...timers }
+ Object.assign(timers, { setTimeout, clearTimeout })
+ t.teardown(() => {
+ Object.assign(timers, orgTimers)
+ })
+
+ const server = createServer((req, res) => {
+ setTimeout(() => {
+ req.pipe(res)
+ }, 301e3)
+ clock.tick(301e3)
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ const buf = Buffer.alloc(1e6).toString()
+ pipeline(
+ new Readable({
+ read () {
+ this.push(buf)
+ this.push(null)
+ }
+ }),
+ client.pipeline({
+ path: '/',
+ method: 'PUT'
+ }, (result) => {
+ t.fail('Should not be called')
+ }, (e) => {
+ t.fail('Should not be called')
+ }),
+ new Writable({
+ write (chunk, encoding, callback) {
+ callback()
+ },
+ final (callback) {
+ callback()
+ }
+ }),
+ (err) => {
+ t.type(err, errors.HeadersTimeoutError)
+ }
+ )
+ })
+})
+
+test('pipeline timeout', (t) => {
+ t.plan(1)
+
+ const clock = FakeTimers.install()
+ t.teardown(clock.uninstall.bind(clock))
+
+ const orgTimers = { ...timers }
+ Object.assign(timers, { setTimeout, clearTimeout })
+ t.teardown(() => {
+ Object.assign(timers, orgTimers)
+ })
+
+ const server = createServer((req, res) => {
+ setTimeout(() => {
+ req.pipe(res)
+ }, 31e3)
+ clock.tick(31e3)
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, {
+ headersTimeout: 30e3
+ })
+ t.teardown(client.destroy.bind(client))
+
+ const buf = Buffer.alloc(1e6).toString()
+ pipeline(
+ new Readable({
+ read () {
+ this.push(buf)
+ this.push(null)
+ }
+ }),
+ client.pipeline({
+ path: '/',
+ method: 'PUT'
+ }, (result) => {
+ t.fail('Should not be called')
+ }, (e) => {
+ t.fail('Should not be called')
+ }),
+ new Writable({
+ write (chunk, encoding, callback) {
+ callback()
+ },
+ final (callback) {
+ callback()
+ }
+ }),
+ (err) => {
+ t.type(err, errors.HeadersTimeoutError)
+ }
+ )
+ })
+})
+
+test('client.close should not deadlock', (t) => {
+ t.plan(2)
+
+ const clock = FakeTimers.install()
+ t.teardown(clock.uninstall.bind(clock))
+
+ const orgTimers = { ...timers }
+ Object.assign(timers, { setTimeout, clearTimeout })
+ t.teardown(() => {
+ Object.assign(timers, orgTimers)
+ })
+
+ const server = createServer((req, res) => {
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, {
+ bodyTimeout: 200,
+ headersTimeout: 100
+ })
+ t.teardown(client.destroy.bind(client))
+
+ client[kConnect](() => {
+ client.request({
+ path: '/',
+ method: 'GET'
+ }, (err, response) => {
+ t.type(err, errors.HeadersTimeoutError)
+ })
+
+ client.close((err) => {
+ t.error(err)
+ })
+
+ clock.tick(100)
+ })
+ })
+})
diff --git a/test/request-timeout2.js b/test/request-timeout2.js
new file mode 100644
index 0000000..53943fb
--- /dev/null
+++ b/test/request-timeout2.js
@@ -0,0 +1,48 @@
+'use strict'
+
+const { test } = require('tap')
+const { Client } = require('..')
+const { createServer } = require('http')
+const { Readable } = require('stream')
+
+test('request timeout with slow readable body', (t) => {
+ t.plan(1)
+
+ const server = createServer(async (req, res) => {
+ let str = ''
+ for await (const x of req) {
+ str += x
+ }
+ res.end(str)
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, { headersTimeout: 50 })
+ t.teardown(client.close.bind(client))
+
+ const body = new Readable({
+ read () {
+ if (this._reading) {
+ return
+ }
+ this._reading = true
+
+ this.push('asd')
+ setTimeout(() => {
+ this.push('asd')
+ this.push(null)
+ }, 2e3)
+ }
+ })
+ client.request({
+ path: '/',
+ method: 'POST',
+ headersTimeout: 1e3,
+ body
+ }, async (err, response) => {
+ t.error(err)
+ await response.body.dump()
+ })
+ })
+})
diff --git a/test/request.js b/test/request.js
new file mode 100644
index 0000000..d3a2f74
--- /dev/null
+++ b/test/request.js
@@ -0,0 +1,248 @@
+'use strict'
+
+const { createServer } = require('http')
+const { test } = require('tap')
+const { request } = require('..')
+
+test('no-slash/one-slash pathname should be included in req.path', async (t) => {
+ const pathServer = createServer((req, res) => {
+ t.fail('it shouldn\'t be called')
+ res.statusCode = 200
+ res.end('hello')
+ })
+
+ const requestedServer = createServer((req, res) => {
+ t.equal(`/localhost:${pathServer.address().port}`, req.url)
+ t.equal('GET', req.method)
+ t.equal(`localhost:${requestedServer.address().port}`, req.headers.host)
+ res.statusCode = 200
+ res.end('hello')
+ })
+
+ t.teardown(requestedServer.close.bind(requestedServer))
+ t.teardown(pathServer.close.bind(pathServer))
+
+ await Promise.all([
+ requestedServer.listen(0),
+ pathServer.listen(0)
+ ])
+
+ const noSlashPathname = await request({
+ method: 'GET',
+ origin: `http://localhost:${requestedServer.address().port}`,
+ pathname: `localhost:${pathServer.address().port}`
+ })
+ t.equal(noSlashPathname.statusCode, 200)
+ const noSlashPath = await request({
+ method: 'GET',
+ origin: `http://localhost:${requestedServer.address().port}`,
+ path: `localhost:${pathServer.address().port}`
+ })
+ t.equal(noSlashPath.statusCode, 200)
+ const noSlashPath2Arg = await request(
+ `http://localhost:${requestedServer.address().port}`,
+ { path: `localhost:${pathServer.address().port}` }
+ )
+ t.equal(noSlashPath2Arg.statusCode, 200)
+ const oneSlashPathname = await request({
+ method: 'GET',
+ origin: `http://localhost:${requestedServer.address().port}`,
+ pathname: `/localhost:${pathServer.address().port}`
+ })
+ t.equal(oneSlashPathname.statusCode, 200)
+ const oneSlashPath = await request({
+ method: 'GET',
+ origin: `http://localhost:${requestedServer.address().port}`,
+ path: `/localhost:${pathServer.address().port}`
+ })
+ t.equal(oneSlashPath.statusCode, 200)
+ const oneSlashPath2Arg = await request(
+ `http://localhost:${requestedServer.address().port}`,
+ { path: `/localhost:${pathServer.address().port}` }
+ )
+ t.equal(oneSlashPath2Arg.statusCode, 200)
+ t.end()
+})
+
+test('protocol-relative URL as pathname should be included in req.path', async (t) => {
+ const pathServer = createServer((req, res) => {
+ t.fail('it shouldn\'t be called')
+ res.statusCode = 200
+ res.end('hello')
+ })
+
+ const requestedServer = createServer((req, res) => {
+ t.equal(`//localhost:${pathServer.address().port}`, req.url)
+ t.equal('GET', req.method)
+ t.equal(`localhost:${requestedServer.address().port}`, req.headers.host)
+ res.statusCode = 200
+ res.end('hello')
+ })
+
+ t.teardown(requestedServer.close.bind(requestedServer))
+ t.teardown(pathServer.close.bind(pathServer))
+
+ await Promise.all([
+ requestedServer.listen(0),
+ pathServer.listen(0)
+ ])
+
+ const noSlashPathname = await request({
+ method: 'GET',
+ origin: `http://localhost:${requestedServer.address().port}`,
+ pathname: `//localhost:${pathServer.address().port}`
+ })
+ t.equal(noSlashPathname.statusCode, 200)
+ const noSlashPath = await request({
+ method: 'GET',
+ origin: `http://localhost:${requestedServer.address().port}`,
+ path: `//localhost:${pathServer.address().port}`
+ })
+ t.equal(noSlashPath.statusCode, 200)
+ const noSlashPath2Arg = await request(
+ `http://localhost:${requestedServer.address().port}`,
+ { path: `//localhost:${pathServer.address().port}` }
+ )
+ t.equal(noSlashPath2Arg.statusCode, 200)
+ t.end()
+})
+
+test('Absolute URL as pathname should be included in req.path', async (t) => {
+ const pathServer = createServer((req, res) => {
+ t.fail('it shouldn\'t be called')
+ res.statusCode = 200
+ res.end('hello')
+ })
+
+ const requestedServer = createServer((req, res) => {
+ t.equal(`/http://localhost:${pathServer.address().port}`, req.url)
+ t.equal('GET', req.method)
+ t.equal(`localhost:${requestedServer.address().port}`, req.headers.host)
+ res.statusCode = 200
+ res.end('hello')
+ })
+
+ t.teardown(requestedServer.close.bind(requestedServer))
+ t.teardown(pathServer.close.bind(pathServer))
+
+ await Promise.all([
+ requestedServer.listen(0),
+ pathServer.listen(0)
+ ])
+
+ const noSlashPathname = await request({
+ method: 'GET',
+ origin: `http://localhost:${requestedServer.address().port}`,
+ pathname: `http://localhost:${pathServer.address().port}`
+ })
+ t.equal(noSlashPathname.statusCode, 200)
+ const noSlashPath = await request({
+ method: 'GET',
+ origin: `http://localhost:${requestedServer.address().port}`,
+ path: `http://localhost:${pathServer.address().port}`
+ })
+ t.equal(noSlashPath.statusCode, 200)
+ const noSlashPath2Arg = await request(
+ `http://localhost:${requestedServer.address().port}`,
+ { path: `http://localhost:${pathServer.address().port}` }
+ )
+ t.equal(noSlashPath2Arg.statusCode, 200)
+ t.end()
+})
+
+test('DispatchOptions#reset', scope => {
+ scope.plan(4)
+
+ scope.test('Should throw if invalid reset option', t => {
+ t.plan(1)
+
+ t.rejects(request({
+ method: 'GET',
+ origin: 'http://somehost.xyz',
+ reset: 0
+ }), 'invalid reset')
+ })
+
+ scope.test('Should include "connection:close" if reset true', async t => {
+ const server = createServer((req, res) => {
+ t.equal('GET', req.method)
+ t.equal(`localhost:${server.address().port}`, req.headers.host)
+ t.equal(req.headers.connection, 'close')
+ res.statusCode = 200
+ res.end('hello')
+ })
+
+ t.plan(3)
+
+ t.teardown(server.close.bind(server))
+
+ await new Promise((resolve, reject) => {
+ server.listen(0, (err) => {
+ if (err != null) reject(err)
+ else resolve()
+ })
+ })
+
+ await request({
+ method: 'GET',
+ origin: `http://localhost:${server.address().port}`,
+ reset: true
+ })
+ })
+
+ scope.test('Should include "connection:keep-alive" if reset false', async t => {
+ const server = createServer((req, res) => {
+ t.equal('GET', req.method)
+ t.equal(`localhost:${server.address().port}`, req.headers.host)
+ t.equal(req.headers.connection, 'keep-alive')
+ res.statusCode = 200
+ res.end('hello')
+ })
+
+ t.plan(3)
+
+ t.teardown(server.close.bind(server))
+
+ await new Promise((resolve, reject) => {
+ server.listen(0, (err) => {
+ if (err != null) reject(err)
+ else resolve()
+ })
+ })
+
+ await request({
+ method: 'GET',
+ origin: `http://localhost:${server.address().port}`,
+ reset: false
+ })
+ })
+
+ scope.test('Should react to manual set of "connection:close" header', async t => {
+ const server = createServer((req, res) => {
+ t.equal('GET', req.method)
+ t.equal(`localhost:${server.address().port}`, req.headers.host)
+ t.equal(req.headers.connection, 'close')
+ res.statusCode = 200
+ res.end('hello')
+ })
+
+ t.plan(3)
+
+ t.teardown(server.close.bind(server))
+
+ await new Promise((resolve, reject) => {
+ server.listen(0, (err) => {
+ if (err != null) reject(err)
+ else resolve()
+ })
+ })
+
+ await request({
+ method: 'GET',
+ origin: `http://localhost:${server.address().port}`,
+ headers: {
+ connection: 'close'
+ }
+ })
+ })
+})
diff --git a/test/retry-handler.js b/test/retry-handler.js
new file mode 100644
index 0000000..a4577a6
--- /dev/null
+++ b/test/retry-handler.js
@@ -0,0 +1,622 @@
+'use strict'
+const { createServer } = require('node:http')
+const { once } = require('node:events')
+
+const tap = require('tap')
+
+const { RetryHandler, Client } = require('..')
+const { RequestHandler } = require('../lib/api/api-request')
+
+tap.test('Should retry status code', t => {
+ let counter = 0
+ const chunks = []
+ const server = createServer()
+ const dispatchOptions = {
+ retryOptions: {
+ retry: (err, { state, opts }, done) => {
+ counter++
+
+ if (
+ err.statusCode === 500 ||
+ err.message.includes('other side closed')
+ ) {
+ setTimeout(done, 500)
+ return
+ }
+
+ return done(err)
+ }
+ },
+ method: 'GET',
+ path: '/',
+ headers: {
+ 'content-type': 'application/json'
+ }
+ }
+
+ t.plan(4)
+
+ server.on('request', (req, res) => {
+ switch (counter) {
+ case 0:
+ req.destroy()
+ return
+ case 1:
+ res.writeHead(500)
+ res.end('failed')
+ return
+ case 2:
+ res.writeHead(200)
+ res.end('hello world!')
+ return
+ default:
+ t.fail()
+ }
+ })
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ const handler = new RetryHandler(dispatchOptions, {
+ dispatch: client.dispatch.bind(client),
+ handler: {
+ onConnect () {
+ t.pass()
+ },
+ onBodySent () {
+ t.pass()
+ },
+ onHeaders (status, _rawHeaders, resume, _statusMessage) {
+ t.equal(status, 200)
+ return true
+ },
+ onData (chunk) {
+ chunks.push(chunk)
+ return true
+ },
+ onComplete () {
+ t.equal(Buffer.concat(chunks).toString('utf-8'), 'hello world!')
+ t.equal(counter, 2)
+ },
+ onError () {
+ t.fail()
+ }
+ }
+ })
+
+ t.teardown(async () => {
+ await client.close()
+ server.close()
+
+ await once(server, 'close')
+ })
+
+ client.dispatch(
+ {
+ method: 'GET',
+ path: '/',
+ headers: {
+ 'content-type': 'application/json'
+ }
+ },
+ handler
+ )
+ })
+})
+
+tap.test('Should use retry-after header for retries', t => {
+ let counter = 0
+ const chunks = []
+ const server = createServer()
+ let checkpoint
+ const dispatchOptions = {
+ method: 'PUT',
+ path: '/',
+ headers: {
+ 'content-type': 'application/json'
+ }
+ }
+
+ t.plan(4)
+
+ server.on('request', (req, res) => {
+ switch (counter) {
+ case 0:
+ res.writeHead(429, {
+ 'retry-after': 1
+ })
+ res.end('rate limit')
+ checkpoint = Date.now()
+ counter++
+ return
+ case 1:
+ res.writeHead(200)
+ res.end('hello world!')
+ t.ok(Date.now() - checkpoint >= 500)
+ counter++
+ return
+ default:
+ t.fail('unexpected request')
+ }
+ })
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ const handler = new RetryHandler(dispatchOptions, {
+ dispatch: client.dispatch.bind(client),
+ handler: {
+ onConnect () {
+ t.pass()
+ },
+ onBodySent () {
+ t.pass()
+ },
+ onHeaders (status, _rawHeaders, resume, _statusMessage) {
+ t.equal(status, 200)
+ return true
+ },
+ onData (chunk) {
+ chunks.push(chunk)
+ return true
+ },
+ onComplete () {
+ t.equal(Buffer.concat(chunks).toString('utf-8'), 'hello world!')
+ },
+ onError (err) {
+ t.error(err)
+ }
+ }
+ })
+
+ t.teardown(async () => {
+ await client.close()
+ server.close()
+
+ await once(server, 'close')
+ })
+
+ client.dispatch(
+ {
+ method: 'PUT',
+ path: '/',
+ headers: {
+ 'content-type': 'application/json'
+ }
+ },
+ handler
+ )
+ })
+})
+
+tap.test('Should use retry-after header for retries (date)', t => {
+ let counter = 0
+ const chunks = []
+ const server = createServer()
+ let checkpoint
+ const dispatchOptions = {
+ method: 'PUT',
+ path: '/',
+ headers: {
+ 'content-type': 'application/json'
+ }
+ }
+
+ t.plan(4)
+
+ server.on('request', (req, res) => {
+ switch (counter) {
+ case 0:
+ res.writeHead(429, {
+ 'retry-after': new Date(
+ new Date().setSeconds(new Date().getSeconds() + 1)
+ ).toUTCString()
+ })
+ res.end('rate limit')
+ checkpoint = Date.now()
+ counter++
+ return
+ case 1:
+ res.writeHead(200)
+ res.end('hello world!')
+ t.ok(Date.now() - checkpoint >= 1)
+ counter++
+ return
+ default:
+ t.fail('unexpected request')
+ }
+ })
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ const handler = new RetryHandler(dispatchOptions, {
+ dispatch: client.dispatch.bind(client),
+ handler: {
+ onConnect () {
+ t.pass()
+ },
+ onBodySent () {
+ t.pass()
+ },
+ onHeaders (status, _rawHeaders, resume, _statusMessage) {
+ t.equal(status, 200)
+ return true
+ },
+ onData (chunk) {
+ chunks.push(chunk)
+ return true
+ },
+ onComplete () {
+ t.equal(Buffer.concat(chunks).toString('utf-8'), 'hello world!')
+ },
+ onError (err) {
+ t.error(err)
+ }
+ }
+ })
+
+ t.teardown(async () => {
+ await client.close()
+ server.close()
+
+ await once(server, 'close')
+ })
+
+ client.dispatch(
+ {
+ method: 'PUT',
+ path: '/',
+ headers: {
+ 'content-type': 'application/json'
+ }
+ },
+ handler
+ )
+ })
+})
+
+tap.test('Should retry with defaults', t => {
+ let counter = 0
+ const chunks = []
+ const server = createServer()
+ const dispatchOptions = {
+ method: 'GET',
+ path: '/',
+ headers: {
+ 'content-type': 'application/json'
+ }
+ }
+
+ server.on('request', (req, res) => {
+ switch (counter) {
+ case 0:
+ req.destroy()
+ counter++
+ return
+ case 1:
+ res.writeHead(500)
+ res.end('failed')
+ counter++
+ return
+ case 2:
+ res.writeHead(200)
+ res.end('hello world!')
+ counter++
+ return
+ default:
+ t.fail()
+ }
+ })
+
+ t.plan(3)
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ const handler = new RetryHandler(dispatchOptions, {
+ dispatch: client.dispatch.bind(client),
+ handler: {
+ onConnect () {
+ t.pass()
+ },
+ onBodySent () {
+ t.pass()
+ },
+ onHeaders (status, _rawHeaders, resume, _statusMessage) {
+ t.equal(status, 200)
+ return true
+ },
+ onData (chunk) {
+ chunks.push(chunk)
+ return true
+ },
+ onComplete () {
+ t.equal(Buffer.concat(chunks).toString('utf-8'), 'hello world!')
+ },
+ onError (err) {
+ t.error(err)
+ }
+ }
+ })
+
+ t.teardown(async () => {
+ await client.close()
+ server.close()
+
+ await once(server, 'close')
+ })
+
+ client.dispatch(
+ {
+ method: 'GET',
+ path: '/',
+ headers: {
+ 'content-type': 'application/json'
+ }
+ },
+ handler
+ )
+ })
+})
+
+tap.test('Should handle 206 partial content', t => {
+ const chunks = []
+ let counter = 0
+
+ // Took from: https://github.com/nxtedition/nxt-lib/blob/4b001ebc2f22cf735a398f35ff800dd553fe5933/test/undici/retry.js#L47
+ let x = 0
+ const server = createServer((req, res) => {
+ if (x === 0) {
+ t.pass()
+ res.setHeader('etag', 'asd')
+ res.write('abc')
+ setTimeout(() => {
+ res.destroy()
+ }, 1e2)
+ } else if (x === 1) {
+ t.same(req.headers.range, 'bytes=3-')
+ res.setHeader('content-range', 'bytes 3-6/6')
+ res.setHeader('etag', 'asd')
+ res.statusCode = 206
+ res.end('def')
+ }
+ x++
+ })
+
+ const dispatchOptions = {
+ retryOptions: {
+ retry: function (err, _, done) {
+ counter++
+
+ if (err.code && err.code === 'UND_ERR_DESTROYED') {
+ return done(false)
+ }
+
+ if (err.statusCode === 206) return done(err)
+
+ setTimeout(done, 800)
+ }
+ },
+ method: 'GET',
+ path: '/',
+ headers: {
+ 'content-type': 'application/json'
+ }
+ }
+
+ t.plan(8)
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ const handler = new RetryHandler(dispatchOptions, {
+ dispatch: (...args) => {
+ return client.dispatch(...args)
+ },
+ handler: {
+ onRequestSent () {
+ t.pass()
+ },
+ onConnect () {
+ t.pass()
+ },
+ onBodySent () {
+ t.pass()
+ },
+ onHeaders (status, _rawHeaders, resume, _statusMessage) {
+ t.equal(status, 200)
+ return true
+ },
+ onData (chunk) {
+ chunks.push(chunk)
+ return true
+ },
+ onComplete () {
+ t.equal(Buffer.concat(chunks).toString('utf-8'), 'abcdef')
+ t.equal(counter, 1)
+ },
+ onError () {
+ t.fail()
+ }
+ }
+ })
+
+ client.dispatch(
+ {
+ method: 'GET',
+ path: '/',
+ headers: {
+ 'content-type': 'application/json'
+ }
+ },
+ handler
+ )
+
+ t.teardown(async () => {
+ await client.close()
+
+ server.close()
+ await once(server, 'close')
+ })
+ })
+})
+
+tap.test('Should handle 206 partial content - bad-etag', t => {
+ const chunks = []
+
+ // Took from: https://github.com/nxtedition/nxt-lib/blob/4b001ebc2f22cf735a398f35ff800dd553fe5933/test/undici/retry.js#L47
+ let x = 0
+ const server = createServer((req, res) => {
+ if (x === 0) {
+ t.pass()
+ res.setHeader('etag', 'asd')
+ res.write('abc')
+ setTimeout(() => {
+ res.destroy()
+ }, 1e2)
+ } else if (x === 1) {
+ t.same(req.headers.range, 'bytes=3-')
+ res.setHeader('content-range', 'bytes 3-6/6')
+ res.setHeader('etag', 'erwsd')
+ res.statusCode = 206
+ res.end('def')
+ }
+ x++
+ })
+
+ const dispatchOptions = {
+ method: 'GET',
+ path: '/',
+ headers: {
+ 'content-type': 'application/json'
+ }
+ }
+
+ t.plan(6)
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ const handler = new RetryHandler(
+ dispatchOptions,
+ {
+ dispatch: (...args) => {
+ return client.dispatch(...args)
+ },
+ handler: {
+ onConnect () {
+ t.pass()
+ },
+ onBodySent () {
+ t.pass()
+ },
+ onHeaders (status, _rawHeaders, resume, _statusMessage) {
+ t.pass()
+ return true
+ },
+ onData (chunk) {
+ chunks.push(chunk)
+ return true
+ },
+ onComplete () {
+ t.error('should not complete')
+ },
+ onError (err) {
+ t.equal(Buffer.concat(chunks).toString('utf-8'), 'abc')
+ t.equal(err.code, 'UND_ERR_REQ_RETRY')
+ }
+ }
+ }
+ )
+
+ client.dispatch(
+ {
+ method: 'GET',
+ path: '/',
+ headers: {
+ 'content-type': 'application/json'
+ }
+ },
+ handler
+ )
+
+ t.teardown(async () => {
+ await client.close()
+
+ server.close()
+ await once(server, 'close')
+ })
+ })
+})
+
+tap.test('retrying a request with a body', t => {
+ let counter = 0
+ const server = createServer()
+ const dispatchOptions = {
+ retryOptions: {
+ retry: (err, { state, opts }, done) => {
+ counter++
+
+ if (
+ err.statusCode === 500 ||
+ err.message.includes('other side closed')
+ ) {
+ setTimeout(done, 500)
+ return
+ }
+
+ return done(err)
+ }
+ },
+ method: 'POST',
+ path: '/',
+ headers: {
+ 'content-type': 'application/json'
+ },
+ body: JSON.stringify({ hello: 'world' })
+ }
+
+ t.plan(1)
+
+ server.on('request', (req, res) => {
+ switch (counter) {
+ case 0:
+ req.destroy()
+ return
+ case 1:
+ res.writeHead(500)
+ res.end('failed')
+ return
+ case 2:
+ res.writeHead(200)
+ res.end('hello world!')
+ return
+ default:
+ t.fail()
+ }
+ })
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ const handler = new RetryHandler(dispatchOptions, {
+ dispatch: client.dispatch.bind(client),
+ handler: new RequestHandler(dispatchOptions, (err, data) => {
+ t.error(err)
+ })
+ })
+
+ t.teardown(async () => {
+ await client.close()
+ server.close()
+
+ await once(server, 'close')
+ })
+
+ client.dispatch(
+ {
+ method: 'POST',
+ path: '/',
+ headers: {
+ 'content-type': 'application/json'
+ },
+ body: JSON.stringify({ hello: 'world' })
+ },
+ handler
+ )
+ })
+})
diff --git a/test/socket-back-pressure.js b/test/socket-back-pressure.js
new file mode 100644
index 0000000..9e774b3
--- /dev/null
+++ b/test/socket-back-pressure.js
@@ -0,0 +1,54 @@
+'use strict'
+
+const { Client } = require('..')
+const { createServer } = require('http')
+const { Readable } = require('stream')
+const { test } = require('tap')
+
+test('socket back-pressure', (t) => {
+ t.plan(3)
+
+ const server = createServer()
+ let bytesWritten = 0
+
+ const buf = Buffer.allocUnsafe(16384)
+ const src = new Readable({
+ read () {
+ bytesWritten += buf.length
+ this.push(buf)
+ if (bytesWritten >= 1e6) {
+ this.push(null)
+ }
+ }
+ })
+
+ server.on('request', (req, res) => {
+ src.pipe(res)
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, {
+ pipelining: 1
+ })
+ t.teardown(client.destroy.bind(client))
+
+ client.request({ path: '/', method: 'GET', opaque: 'asd' }, (err, data) => {
+ t.error(err)
+ data.body
+ .resume()
+ .once('data', () => {
+ data.body.pause()
+ // TODO: Try to avoid timeout.
+ setTimeout(() => {
+ t.ok(data.body._readableState.length < bytesWritten - data.body._readableState.highWaterMark)
+ src.push(null)
+ data.body.resume()
+ }, 1e3)
+ })
+ .on('end', () => {
+ t.pass()
+ })
+ })
+ })
+})
diff --git a/test/socket-timeout.js b/test/socket-timeout.js
new file mode 100644
index 0000000..8019c74
--- /dev/null
+++ b/test/socket-timeout.js
@@ -0,0 +1,100 @@
+'use strict'
+
+const { test } = require('tap')
+const { Client, errors } = require('..')
+const timers = require('../lib/timers')
+const { createServer } = require('http')
+const FakeTimers = require('@sinonjs/fake-timers')
+
+test('timeout with pipelining 1', (t) => {
+ t.plan(9)
+
+ const server = createServer()
+
+ server.once('request', (req, res) => {
+ t.pass('first request received, we are letting this timeout on the client')
+
+ server.once('request', (req, res) => {
+ t.equal('/', req.url)
+ t.equal('GET', req.method)
+ res.setHeader('content-type', 'text/plain')
+ res.end('hello')
+ })
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, {
+ pipelining: 1,
+ headersTimeout: 500,
+ bodyTimeout: 500
+ })
+ t.teardown(client.close.bind(client))
+
+ client.request({
+ path: '/',
+ method: 'GET',
+ opaque: 'asd'
+ }, (err, data) => {
+ t.type(err, errors.HeadersTimeoutError) // we are expecting an error
+ t.equal(data.opaque, 'asd')
+ })
+
+ client.request({
+ path: '/',
+ method: 'GET'
+ }, (err, { statusCode, headers, body }) => {
+ t.error(err)
+ t.equal(statusCode, 200)
+ t.equal(headers['content-type'], 'text/plain')
+ const bufs = []
+ body.on('data', (buf) => {
+ bufs.push(buf)
+ })
+ body.on('end', () => {
+ t.equal('hello', Buffer.concat(bufs).toString('utf8'))
+ })
+ })
+ })
+})
+
+test('Disable socket timeout', (t) => {
+ t.plan(2)
+
+ const server = createServer()
+ const clock = FakeTimers.install()
+ t.teardown(clock.uninstall.bind(clock))
+
+ const orgTimers = { ...timers }
+ Object.assign(timers, { setTimeout, clearTimeout })
+ t.teardown(() => {
+ Object.assign(timers, orgTimers)
+ })
+
+ server.once('request', (req, res) => {
+ setTimeout(() => {
+ res.end('hello')
+ }, 31e3)
+ clock.tick(32e3)
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`, {
+ bodyTimeout: 0,
+ headersTimeout: 0
+ })
+ t.teardown(client.close.bind(client))
+
+ client.request({ path: '/', method: 'GET' }, (err, result) => {
+ t.error(err)
+ const bufs = []
+ result.body.on('data', (buf) => {
+ bufs.push(buf)
+ })
+ result.body.on('end', () => {
+ t.equal('hello', Buffer.concat(bufs).toString('utf8'))
+ })
+ })
+ })
+})
diff --git a/test/stream-compat.js b/test/stream-compat.js
new file mode 100644
index 0000000..71d2410
--- /dev/null
+++ b/test/stream-compat.js
@@ -0,0 +1,75 @@
+'use strict'
+
+const { test } = require('tap')
+const { Client } = require('..')
+const { createServer } = require('http')
+const { Readable } = require('stream')
+const EE = require('events')
+
+test('stream body without destroy', (t) => {
+ t.plan(2)
+
+ const server = createServer((req, res) => {
+ res.end()
+ })
+ t.teardown(server.close.bind(server))
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ const signal = new EE()
+ const body = new Readable({ read () {} })
+ body.destroy = undefined
+ body.on('error', (err) => {
+ t.ok(err)
+ })
+ client.request({
+ path: '/',
+ method: 'PUT',
+ signal,
+ body
+ }, (err, data) => {
+ t.ok(err)
+ })
+ signal.emit('abort')
+ })
+})
+
+test('IncomingMessage', (t) => {
+ t.plan(2)
+
+ const server = createServer((req, res) => {
+ res.end()
+ })
+ t.teardown(server.close.bind(server))
+
+ server.listen(0, () => {
+ const proxyClient = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(proxyClient.destroy.bind(proxyClient))
+
+ const proxy = createServer((req, res) => {
+ proxyClient.request({
+ path: '/',
+ method: 'PUT',
+ body: req
+ }, (err, data) => {
+ t.error(err)
+ data.body.pipe(res)
+ })
+ })
+ t.teardown(proxy.close.bind(proxy))
+
+ proxy.listen(0, () => {
+ const client = new Client(`http://localhost:${proxy.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ client.request({
+ path: '/',
+ method: 'PUT',
+ body: 'hello world'
+ }, (err, data) => {
+ t.error(err)
+ })
+ })
+ })
+})
diff --git a/test/tls-client-cert.js b/test/tls-client-cert.js
new file mode 100644
index 0000000..8ae301d
--- /dev/null
+++ b/test/tls-client-cert.js
@@ -0,0 +1,70 @@
+'use strict'
+
+const { readFileSync } = require('fs')
+const { join } = require('path')
+const https = require('https')
+const { test } = require('tap')
+const { Client } = require('..')
+const { kSocket } = require('../lib/core/symbols')
+const { nodeMajor } = require('../lib/core/util')
+
+const serverOptions = {
+ ca: [
+ readFileSync(join(__dirname, 'fixtures', 'client-ca-crt.pem'), 'utf8')
+ ],
+ key: readFileSync(join(__dirname, 'fixtures', 'key.pem'), 'utf8'),
+ cert: readFileSync(join(__dirname, 'fixtures', 'cert.pem'), 'utf8'),
+ requestCert: true,
+ rejectUnauthorized: false
+}
+
+test('Client using valid client certificate', { skip: nodeMajor > 16 }, t => {
+ t.plan(5)
+
+ const server = https.createServer(serverOptions, (req, res) => {
+ const authorized = req.client.authorized
+ t.ok(authorized)
+
+ res.setHeader('Content-Type', 'text/plain')
+ res.end('hello')
+ })
+
+ server.listen(0, function () {
+ const tls = {
+ ca: [
+ readFileSync(join(__dirname, 'fixtures', 'ca.pem'), 'utf8')
+ ],
+ key: readFileSync(join(__dirname, 'fixtures', 'client-key.pem'), 'utf8'),
+ cert: readFileSync(join(__dirname, 'fixtures', 'client-crt.pem'), 'utf8'),
+ rejectUnauthorized: false,
+ servername: 'agent1'
+ }
+ const client = new Client(`https://localhost:${server.address().port}`, {
+ connect: tls
+ })
+
+ t.teardown(() => {
+ client.close()
+ server.close()
+ })
+
+ client.request({
+ path: '/',
+ method: 'GET'
+ }, (err, { statusCode, body }) => {
+ t.error(err)
+ t.equal(statusCode, 200)
+
+ const authorized = client[kSocket].authorized
+ t.ok(authorized)
+
+ const bufs = []
+ body.on('data', (buf) => {
+ bufs.push(buf)
+ })
+ body.on('end', () => {
+ t.equal('hello', Buffer.concat(bufs).toString('utf8'))
+ })
+ })
+ })
+})
diff --git a/test/tls-session-reuse.js b/test/tls-session-reuse.js
new file mode 100644
index 0000000..ab012f1
--- /dev/null
+++ b/test/tls-session-reuse.js
@@ -0,0 +1,185 @@
+'use strict'
+
+const { readFileSync } = require('fs')
+const { join } = require('path')
+const https = require('https')
+const crypto = require('crypto')
+const { test, teardown } = require('tap')
+const { Client, Pool } = require('..')
+const { kSocket } = require('../lib/core/symbols')
+const { nodeMajor } = require('../lib/core/util')
+
+const options = {
+ key: readFileSync(join(__dirname, 'fixtures', 'key.pem'), 'utf8'),
+ cert: readFileSync(join(__dirname, 'fixtures', 'cert.pem'), 'utf8')
+}
+const ca = readFileSync(join(__dirname, 'fixtures', 'ca.pem'), 'utf8')
+
+test('A client should disable session caching', {
+ skip: nodeMajor < 11 // tls socket session event has been added in Node 11. Cf. https://nodejs.org/api/tls.html#tls_event_session
+}, t => {
+ const clientSessions = {}
+ let serverRequests = 0
+
+ t.test('Prepare request', t => {
+ t.plan(3)
+ const server = https.createServer(options, (req, res) => {
+ if (req.url === '/drop-key') {
+ server.setTicketKeys(crypto.randomBytes(48))
+ }
+ serverRequests++
+ res.end()
+ })
+
+ server.listen(0, function () {
+ const tls = {
+ ca,
+ rejectUnauthorized: false,
+ servername: 'agent1'
+ }
+ const client = new Client(`https://localhost:${server.address().port}`, {
+ pipelining: 0,
+ tls,
+ maxCachedSessions: 0
+ })
+
+ t.teardown(() => {
+ client.close()
+ server.close()
+ })
+
+ const queue = [{
+ name: 'first',
+ method: 'GET',
+ path: '/'
+ }, {
+ name: 'second',
+ method: 'GET',
+ path: '/'
+ }]
+
+ function request () {
+ const options = queue.shift()
+ if (options.ciphers) {
+ // Choose different cipher to use different cache entry
+ tls.ciphers = options.ciphers
+ } else {
+ delete tls.ciphers
+ }
+ client.request(options, (err, data) => {
+ t.error(err)
+ clientSessions[options.name] = client[kSocket].getSession()
+ data.body.resume().on('end', () => {
+ if (queue.length !== 0) {
+ return request()
+ }
+ t.pass()
+ })
+ })
+ }
+ request()
+ })
+ })
+
+ t.test('Verify cached sessions', t => {
+ t.plan(2)
+ t.equal(serverRequests, 2)
+ t.not(
+ clientSessions.first.toString('hex'),
+ clientSessions.second.toString('hex')
+ )
+ })
+
+ t.end()
+})
+
+test('A pool should be able to reuse TLS sessions between clients', {
+ skip: nodeMajor < 11 // tls socket session event has been added in Node 11. Cf. https://nodejs.org/api/tls.html#tls_event_session
+}, t => {
+ let serverRequests = 0
+
+ const REQ_COUNT = 10
+ const ASSERT_PERFORMANCE_GAIN = false
+
+ t.test('Prepare request', t => {
+ t.plan(2 + 1 + (ASSERT_PERFORMANCE_GAIN ? 1 : 0))
+ const server = https.createServer(options, (req, res) => {
+ serverRequests++
+ res.end()
+ })
+
+ let numSessions = 0
+ const sessions = []
+
+ server.listen(0, async () => {
+ const poolWithSessionReuse = new Pool(`https://localhost:${server.address().port}`, {
+ pipelining: 0,
+ connections: 100,
+ maxCachedSessions: 1,
+ tls: {
+ ca,
+ rejectUnauthorized: false,
+ servername: 'agent1'
+ }
+ })
+ const poolWithoutSessionReuse = new Pool(`https://localhost:${server.address().port}`, {
+ pipelining: 0,
+ connections: 100,
+ maxCachedSessions: 0,
+ tls: {
+ ca,
+ rejectUnauthorized: false,
+ servername: 'agent1'
+ }
+ })
+
+ poolWithSessionReuse.on('connect', (url, targets) => {
+ const y = targets[1][kSocket].getSession()
+ if (sessions.some(x => x.equals(y))) {
+ return
+ }
+ sessions.push(y)
+ numSessions++
+ })
+
+ t.teardown(() => {
+ poolWithSessionReuse.close()
+ poolWithoutSessionReuse.close()
+ server.close()
+ })
+
+ function request (pool, expectTLSSessionCache) {
+ return new Promise((resolve, reject) => {
+ pool.request({
+ method: 'GET',
+ path: '/'
+ }, (err, data) => {
+ if (err) return reject(err)
+ data.body.resume().on('end', resolve)
+ })
+ })
+ }
+
+ async function runRequests (pool, numIterations, expectTLSSessionCache) {
+ const requests = []
+ // For the session reuse, we first need one client to connect to receive a valid tls session to reuse
+ await request(pool, false)
+ while (numIterations--) {
+ requests.push(request(pool, expectTLSSessionCache))
+ }
+ return await Promise.all(requests)
+ }
+
+ await runRequests(poolWithoutSessionReuse, REQ_COUNT, false)
+ await runRequests(poolWithSessionReuse, REQ_COUNT, true)
+
+ t.equal(numSessions, 2)
+ t.equal(serverRequests, 2 + REQ_COUNT * 2)
+ t.pass()
+ })
+ })
+
+ t.end()
+})
+
+teardown(() => process.exit())
diff --git a/test/tls.js b/test/tls.js
new file mode 100644
index 0000000..fbe07b0
--- /dev/null
+++ b/test/tls.js
@@ -0,0 +1,188 @@
+'use strict'
+
+// TODO: Don't depend on external URLs.
+
+// const { test } = require('tap')
+// const { Client } = require('..')
+// const { kSocket } = require('../lib/core/symbols')
+// const { Readable } = require('stream')
+// const { kRunning } = require('../lib/core/symbols')
+
+// test('tls get 1', (t) => {
+// t.plan(4)
+
+// const client = new Client('https://www.github.com')
+// t.teardown(client.close.bind(client))
+
+// client.request({ method: 'GET', path: '/' }, (err, data) => {
+// t.error(err)
+// t.equal(data.statusCode, 301)
+// t.equal(client[kSocket].authorized, true)
+
+// data.body
+// .resume()
+// .on('end', () => {
+// t.pass()
+// })
+// })
+// })
+
+// test('tls get 2', (t) => {
+// t.plan(4)
+
+// const client = new Client('https://140.82.112.4', {
+// tls: {
+// servername: 'www.github.com'
+// }
+// })
+// t.teardown(client.close.bind(client))
+
+// client.request({ method: 'GET', path: '/' }, (err, data) => {
+// t.error(err)
+// t.equal(data.statusCode, 301)
+// t.equal(client[kSocket].authorized, true)
+
+// data.body
+// .resume()
+// .on('end', () => {
+// t.pass()
+// })
+// })
+// })
+
+// test('tls get 3', (t) => {
+// t.plan(8)
+
+// const client = new Client('https://140.82.112.4')
+// t.teardown(client.destroy.bind(client))
+
+// let didDisconnect = false
+// client.request({
+// method: 'GET',
+// path: '/',
+// headers: {
+// host: 'www.github.com'
+// }
+// }, (err, data) => {
+// t.error(err)
+// t.equal(data.statusCode, 301)
+// t.equal(client[kSocket].authorized, true)
+
+// data.body
+// .resume()
+// .on('end', () => {
+// t.pass()
+// })
+// client.once('disconnect', () => {
+// t.pass()
+// didDisconnect = true
+// })
+// })
+
+// const body = new Readable({ read () {} })
+// body.on('error', (err) => {
+// t.ok(err)
+// })
+// client.request({
+// method: 'POST',
+// path: '/',
+// body,
+// headers: {
+// host: 'www.asd.com'
+// }
+// }, (err, data) => {
+// t.equal(didDisconnect, true)
+// t.ok(err)
+// })
+// })
+
+// test('tls get 4', (t) => {
+// t.plan(9)
+
+// const client = new Client('https://140.82.112.4', {
+// tls: {
+// servername: 'www.github.com'
+// },
+// pipelining: 2
+// })
+// t.teardown(client.close.bind(client))
+
+// client.request({
+// method: 'GET',
+// path: '/',
+// headers: {
+// host: '140.82.112.4'
+// }
+// }, (err, data) => {
+// t.error(err)
+// t.equal(client[kRunning], 1)
+// t.equal(data.statusCode, 301)
+// t.equal(client[kSocket].authorized, true)
+
+// client.request({
+// method: 'GET',
+// path: '/',
+// headers: {
+// host: 'www.github.com'
+// }
+// }, (err, data) => {
+// t.error(err)
+// t.equal(data.statusCode, 301)
+// t.equal(client[kSocket].authorized, true)
+
+// data.body
+// .resume()
+// .on('end', () => {
+// t.pass()
+// })
+// })
+
+// data.body
+// .resume()
+// .on('end', () => {
+// t.pass()
+// })
+// })
+// })
+
+// test('tls get 5', (t) => {
+// t.plan(7)
+
+// const client = new Client('https://140.82.112.4')
+// t.teardown(client.destroy.bind(client))
+
+// let didDisconnect = false
+// client.request({
+// method: 'GET',
+// path: '/',
+// headers: {
+// host: 'www.github.com'
+// }
+// }, (err, data) => {
+// t.error(err)
+// t.equal(data.statusCode, 301)
+// t.equal(client[kSocket].authorized, true)
+
+// data.body
+// .resume()
+// .on('end', () => {
+// t.pass()
+// })
+// client.once('disconnect', () => {
+// t.pass()
+// didDisconnect = true
+// })
+// })
+
+// client.request({
+// method: 'POST',
+// path: '/',
+// body: [],
+// headers: {
+// host: 'www.asd.com'
+// }
+// }, (err, data) => {
+// t.equal(didDisconnect, true)
+// t.ok(err)
+// })
+// })
diff --git a/test/trailers.js b/test/trailers.js
new file mode 100644
index 0000000..ca56de2
--- /dev/null
+++ b/test/trailers.js
@@ -0,0 +1,57 @@
+'use strict'
+
+const { test } = require('tap')
+const { Client } = require('..')
+const { createServer } = require('http')
+
+test('response trailers missing is OK', (t) => {
+ t.plan(1)
+
+ const server = createServer((req, res) => {
+ res.writeHead(200, {
+ Trailer: 'content-length'
+ })
+ res.end('response')
+ })
+ t.teardown(server.close.bind(server))
+ server.listen(0, async () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ const { body } = await client.request({
+ path: '/',
+ method: 'GET',
+ body: 'asd'
+ })
+
+ t.equal(await body.text(), 'response')
+ })
+})
+
+test('response trailers missing w trailers is OK', (t) => {
+ t.plan(2)
+
+ const server = createServer((req, res) => {
+ res.writeHead(200, {
+ Trailer: 'content-length'
+ })
+ res.addTrailers({
+ asd: 'foo'
+ })
+ res.end('response')
+ })
+ t.teardown(server.close.bind(server))
+ server.listen(0, async () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.destroy.bind(client))
+
+ const { body, trailers } = await client.request({
+ path: '/',
+ method: 'GET',
+ body: 'asd'
+ })
+
+ t.equal(await body.text(), 'response')
+ t.same(trailers, { asd: 'foo' })
+ })
+})
diff --git a/test/types/agent.test-d.ts b/test/types/agent.test-d.ts
new file mode 100644
index 0000000..5e5275f
--- /dev/null
+++ b/test/types/agent.test-d.ts
@@ -0,0 +1,110 @@
+import { Duplex, Readable, Writable } from 'stream'
+import { expectAssignable } from 'tsd'
+import { Agent, Dispatcher } from '../..'
+import { URL } from 'url'
+
+expectAssignable<Agent>(new Agent())
+expectAssignable<Agent>(new Agent({}))
+expectAssignable<Agent>(new Agent({ maxRedirections: 1 }))
+expectAssignable<Agent>(new Agent({ factory: () => new Dispatcher() }))
+
+{
+ const agent = new Agent()
+
+ // properties
+ expectAssignable<boolean>(agent.closed)
+ expectAssignable<boolean>(agent.destroyed)
+
+ // request
+ expectAssignable<Promise<Dispatcher.ResponseData>>(agent.request({ origin: '', path: '', method: 'GET' }))
+ expectAssignable<Promise<Dispatcher.ResponseData>>(agent.request({ origin: '', path: '', method: 'GET', onInfo: ((info) => {}) }))
+ expectAssignable<Promise<Dispatcher.ResponseData>>(agent.request({ origin: new URL('http://localhost'), path: '', method: 'GET' }))
+ expectAssignable<void>(agent.request({ origin: '', path: '', method: 'GET' }, (err, data) => {
+ expectAssignable<Error | null>(err)
+ expectAssignable<Dispatcher.ResponseData>(data)
+ }))
+ expectAssignable<void>(agent.request({ origin: new URL('http://localhost'), path: '', method: 'GET' }, (err, data) => {
+ expectAssignable<Error | null>(err)
+ expectAssignable<Dispatcher.ResponseData>(data)
+ }))
+
+ // stream
+ expectAssignable<Promise<Dispatcher.StreamData>>(agent.stream({ origin: '', path: '', method: 'GET' }, data => {
+ expectAssignable<Dispatcher.StreamFactoryData>(data)
+ return new Writable()
+ }))
+ expectAssignable<Promise<Dispatcher.StreamData>>(agent.stream({ origin: '', path: '', method: 'GET', onInfo: ((info) => {}) }, data => {
+ expectAssignable<Dispatcher.StreamFactoryData>(data)
+ return new Writable()
+ }))
+ expectAssignable<Promise<Dispatcher.StreamData>>(agent.stream({ origin: new URL('http://localhost'), path: '', method: 'GET' }, data => {
+ expectAssignable<Dispatcher.StreamFactoryData>(data)
+ return new Writable()
+ }))
+ expectAssignable<void>(agent.stream(
+ { origin: '', path: '', method: 'GET' },
+ data => {
+ expectAssignable<Dispatcher.StreamFactoryData>(data)
+ return new Writable()
+ },
+ (err, data) => {
+ expectAssignable<Error | null>(err)
+ expectAssignable<Dispatcher.StreamData>(data)
+ }
+ ))
+ expectAssignable<void>(agent.stream(
+ { origin: new URL('http://localhost'), path: '', method: 'GET' },
+ data => {
+ expectAssignable<Dispatcher.StreamFactoryData>(data)
+ return new Writable()
+ },
+ (err, data) => {
+ expectAssignable<Error | null>(err)
+ expectAssignable<Dispatcher.StreamData>(data)
+ }
+ ))
+
+ // pipeline
+ expectAssignable<Duplex>(agent.pipeline({ origin: '', path: '', method: 'GET' }, data => {
+ expectAssignable<Dispatcher.PipelineHandlerData>(data)
+ return new Readable()
+ }))
+ expectAssignable<Duplex>(agent.pipeline({ origin: '', path: '', method: 'GET', onInfo: ((info) => {}) }, data => {
+ expectAssignable<Dispatcher.PipelineHandlerData>(data)
+ return new Readable()
+ }))
+ expectAssignable<Duplex>(agent.pipeline({ origin: new URL('http://localhost'), path: '', method: 'GET' }, data => {
+ expectAssignable<Dispatcher.PipelineHandlerData>(data)
+ return new Readable()
+ }))
+
+ // upgrade
+ expectAssignable<Promise<Dispatcher.UpgradeData>>(agent.upgrade({ path: '' }))
+ expectAssignable<void>(agent.upgrade({ path: '' }, (err, data) => {
+ expectAssignable<Error | null>(err)
+ expectAssignable<Dispatcher.UpgradeData>(data)
+ }))
+
+ // connect
+ expectAssignable<Promise<Dispatcher.ConnectData>>(agent.connect({ path: '' }))
+ expectAssignable<void>(agent.connect({ path: '' }, (err, data) => {
+ expectAssignable<Error | null>(err)
+ expectAssignable<Dispatcher.ConnectData>(data)
+ }))
+
+ // dispatch
+ expectAssignable<boolean>(agent.dispatch({ origin: '', path: '', method: 'GET' }, {}))
+ expectAssignable<boolean>(agent.dispatch({ origin: '', path: '', method: 'GET', maxRedirections: 1 }, {}))
+
+ // close
+ expectAssignable<Promise<void>>(agent.close())
+ expectAssignable<void>(agent.close(() => {}))
+
+ // destroy
+ expectAssignable<Promise<void>>(agent.destroy())
+ expectAssignable<Promise<void>>(agent.destroy(new Error()))
+ expectAssignable<Promise<void>>(agent.destroy(null))
+ expectAssignable<void>(agent.destroy(() => {}))
+ expectAssignable<void>(agent.destroy(new Error(), () => {}))
+ expectAssignable<void>(agent.destroy(null, () => {}))
+}
diff --git a/test/types/api.test-d.ts b/test/types/api.test-d.ts
new file mode 100644
index 0000000..c64b131
--- /dev/null
+++ b/test/types/api.test-d.ts
@@ -0,0 +1,28 @@
+import { Duplex, Readable, Writable } from 'stream'
+import { expectAssignable } from 'tsd'
+import { Dispatcher, request, stream, pipeline, connect, upgrade } from '../..'
+
+// request
+expectAssignable<Promise<Dispatcher.ResponseData>>(request(''))
+expectAssignable<Promise<Dispatcher.ResponseData>>(request('', { }))
+expectAssignable<Promise<Dispatcher.ResponseData>>(request('', { method: 'GET', reset: false }))
+
+// stream
+expectAssignable<Promise<Dispatcher.StreamData>>(stream('', { method: 'GET' }, data => {
+ expectAssignable<Dispatcher.StreamFactoryData>(data)
+ return new Writable()
+}))
+
+// pipeline
+expectAssignable<Duplex>(pipeline('', { method: 'GET' }, data => {
+ expectAssignable<Dispatcher.PipelineHandlerData>(data)
+ return new Readable()
+}))
+
+// connect
+expectAssignable<Promise<Dispatcher.ConnectData>>(connect(''))
+expectAssignable<Promise<Dispatcher.ConnectData>>(connect('', {}))
+
+// upgrade
+expectAssignable<Promise<Dispatcher.UpgradeData>>(upgrade(''))
+expectAssignable<Promise<Dispatcher.UpgradeData>>(upgrade('', {}))
diff --git a/test/types/balanced-pool.test-d.ts b/test/types/balanced-pool.test-d.ts
new file mode 100644
index 0000000..d7ccf7b
--- /dev/null
+++ b/test/types/balanced-pool.test-d.ts
@@ -0,0 +1,113 @@
+import { Duplex, Readable, Writable } from 'stream'
+import { expectAssignable } from 'tsd'
+import { Dispatcher, BalancedPool, Client } from '../..'
+import { URL } from 'url'
+
+expectAssignable<BalancedPool>(new BalancedPool(''))
+expectAssignable<BalancedPool>(new BalancedPool('', {}))
+expectAssignable<BalancedPool>(new BalancedPool(new URL('http://localhost'), {}))
+expectAssignable<BalancedPool>(new BalancedPool('', { factory: () => new Dispatcher() }))
+expectAssignable<BalancedPool>(new BalancedPool('', { factory: (origin, opts) => new Client(origin, opts) }))
+expectAssignable<BalancedPool>(new BalancedPool('', { connections: 1 }))
+expectAssignable<BalancedPool>(new BalancedPool(['http://localhost:4242', 'http://www.nodejs.org']))
+expectAssignable<BalancedPool>(new BalancedPool([new URL('http://localhost:4242'),new URL('http://www.nodejs.org')], {}))
+
+{
+ const pool = new BalancedPool('', {})
+
+ // properties
+ expectAssignable<boolean>(pool.closed)
+ expectAssignable<boolean>(pool.destroyed)
+
+ // upstreams
+ expectAssignable<BalancedPool>(pool.addUpstream('http://www.nodejs.org'))
+ expectAssignable<BalancedPool>(pool.removeUpstream('http://www.nodejs.org'))
+ expectAssignable<BalancedPool>(pool.addUpstream(new URL('http://www.nodejs.org')))
+ expectAssignable<BalancedPool>(pool.removeUpstream(new URL('http://www.nodejs.org')))
+ expectAssignable<string[]>(pool.upstreams)
+
+
+ // request
+ expectAssignable<Promise<Dispatcher.ResponseData>>(pool.request({ origin: '', path: '', method: 'GET' }))
+ expectAssignable<Promise<Dispatcher.ResponseData>>(pool.request({ origin: new URL('http://localhost'), path: '', method: 'GET' }))
+ expectAssignable<void>(pool.request({ origin: '', path: '', method: 'GET' }, (err, data) => {
+ expectAssignable<Error | null>(err)
+ expectAssignable<Dispatcher.ResponseData>(data)
+ }))
+ expectAssignable<void>(pool.request({ origin: new URL('http://localhost'), path: '', method: 'GET' }, (err, data) => {
+ expectAssignable<Error | null>(err)
+ expectAssignable<Dispatcher.ResponseData>(data)
+ }))
+
+ // stream
+ expectAssignable<Promise<Dispatcher.StreamData>>(pool.stream({ origin: '', path: '', method: 'GET' }, data => {
+ expectAssignable<Dispatcher.StreamFactoryData>(data)
+ return new Writable()
+ }))
+ expectAssignable<Promise<Dispatcher.StreamData>>(pool.stream({ origin: new URL('http://localhost'), path: '', method: 'GET' }, data => {
+ expectAssignable<Dispatcher.StreamFactoryData>(data)
+ return new Writable()
+ }))
+ expectAssignable<void>(pool.stream(
+ { origin: '', path: '', method: 'GET' },
+ data => {
+ expectAssignable<Dispatcher.StreamFactoryData>(data)
+ return new Writable()
+ },
+ (err, data) => {
+ expectAssignable<Error | null>(err)
+ expectAssignable<Dispatcher.StreamData>(data)
+ }
+ ))
+ expectAssignable<void>(pool.stream(
+ { origin: new URL('http://localhost'), path: '', method: 'GET' },
+ data => {
+ expectAssignable<Dispatcher.StreamFactoryData>(data)
+ return new Writable()
+ },
+ (err, data) => {
+ expectAssignable<Error | null>(err)
+ expectAssignable<Dispatcher.StreamData>(data)
+ }
+ ))
+
+ // pipeline
+ expectAssignable<Duplex>(pool.pipeline({ origin: '', path: '', method: 'GET' }, data => {
+ expectAssignable<Dispatcher.PipelineHandlerData>(data)
+ return new Readable()
+ }))
+ expectAssignable<Duplex>(pool.pipeline({ origin: new URL('http://localhost'), path: '', method: 'GET' }, data => {
+ expectAssignable<Dispatcher.PipelineHandlerData>(data)
+ return new Readable()
+ }))
+
+ // upgrade
+ expectAssignable<Promise<Dispatcher.UpgradeData>>(pool.upgrade({ path: '' }))
+ expectAssignable<void>(pool.upgrade({ path: '' }, (err, data) => {
+ expectAssignable<Error | null>(err)
+ expectAssignable<Dispatcher.UpgradeData>(data)
+ }))
+
+ // connect
+ expectAssignable<Promise<Dispatcher.ConnectData>>(pool.connect({ path: '' }))
+ expectAssignable<void>(pool.connect({ path: '' }, (err, data) => {
+ expectAssignable<Error | null>(err)
+ expectAssignable<Dispatcher.ConnectData>(data)
+ }))
+
+ // dispatch
+ expectAssignable<boolean>(pool.dispatch({ origin: '', path: '', method: 'GET' }, {}))
+ expectAssignable<boolean>(pool.dispatch({ origin: new URL('http://localhost'), path: '', method: 'GET' }, {}))
+
+ // close
+ expectAssignable<Promise<void>>(pool.close())
+ expectAssignable<void>(pool.close(() => {}))
+
+ // destroy
+ expectAssignable<Promise<void>>(pool.destroy())
+ expectAssignable<Promise<void>>(pool.destroy(new Error()))
+ expectAssignable<Promise<void>>(pool.destroy(null))
+ expectAssignable<void>(pool.destroy(() => {}))
+ expectAssignable<void>(pool.destroy(new Error(), () => {}))
+ expectAssignable<void>(pool.destroy(null, () => {}))
+}
diff --git a/test/types/cache-storage.test-d.ts b/test/types/cache-storage.test-d.ts
new file mode 100644
index 0000000..c21efbd
--- /dev/null
+++ b/test/types/cache-storage.test-d.ts
@@ -0,0 +1,39 @@
+import { expectAssignable } from 'tsd'
+import {
+ caches,
+ CacheStorage,
+ Cache,
+ CacheQueryOptions,
+ MultiCacheQueryOptions,
+ RequestInfo,
+ Request,
+ Response
+} from '../..'
+
+declare const response: Response
+declare const request: Request
+declare const options: RequestInfo
+declare const cache: Cache
+
+expectAssignable<CacheStorage>(caches)
+expectAssignable<MultiCacheQueryOptions>({})
+expectAssignable<MultiCacheQueryOptions>({ cacheName: 'v1' })
+expectAssignable<MultiCacheQueryOptions>({ ignoreMethod: false, ignoreSearch: true })
+
+expectAssignable<CacheQueryOptions>({})
+expectAssignable<CacheQueryOptions>({ ignoreVary: false, ignoreMethod: true, ignoreSearch: true })
+
+expectAssignable<Promise<Cache>>(caches.open('v1'))
+expectAssignable<Promise<Response | undefined>>(caches.match(options))
+expectAssignable<Promise<Response | undefined>>(caches.match(request))
+expectAssignable<Promise<boolean>>(caches.has('v1'))
+expectAssignable<Promise<boolean>>(caches.delete('v1'))
+expectAssignable<Promise<string[]>>(caches.keys())
+
+expectAssignable<Promise<Response | undefined>>(cache.match(options))
+expectAssignable<Promise<readonly Response[]>>(cache.matchAll('v1'))
+expectAssignable<Promise<boolean>>(cache.delete('v1'))
+expectAssignable<Promise<readonly Request[]>>(cache.keys())
+expectAssignable<Promise<undefined>>(cache.add(options))
+expectAssignable<Promise<undefined>>(cache.addAll([options]))
+expectAssignable<Promise<undefined>>(cache.put(options, response))
diff --git a/test/types/client.test-d.ts b/test/types/client.test-d.ts
new file mode 100644
index 0000000..c416d77
--- /dev/null
+++ b/test/types/client.test-d.ts
@@ -0,0 +1,185 @@
+import { Duplex, Readable, Writable } from 'stream'
+import { expectAssignable } from 'tsd'
+import { Client, Dispatcher } from '../..'
+import { URL } from 'url'
+
+expectAssignable<Client>(new Client(''))
+expectAssignable<Client>(new Client('', {}))
+expectAssignable<Client>(new Client('', {
+ maxRequestsPerClient: 10
+}))
+expectAssignable<Client>(new Client('', {
+ connect: { rejectUnauthorized: false }
+}))
+expectAssignable<Client>(new Client(new URL('http://localhost'), {}))
+
+/**
+ * Tests for Client.Options:
+ */
+{
+ expectAssignable<Client>(new Client('', {
+ maxHeaderSize: 16384
+ }))
+ expectAssignable<Client>(new Client('', {
+ headersTimeout: 300e3
+ }))
+ expectAssignable<Client>(new Client('', {
+ connectTimeout: 300e3
+ }))
+ expectAssignable<Client>(new Client('', {
+ bodyTimeout: 300e3
+ }))
+ expectAssignable<Client>(new Client('', {
+ keepAliveTimeout: 4e3
+ }))
+ expectAssignable<Client>(new Client('', {
+ keepAliveMaxTimeout: 600e3
+ }))
+ expectAssignable<Client>(new Client('', {
+ keepAliveTimeoutThreshold: 1e3
+ }))
+ expectAssignable<Client>(new Client('', {
+ socketPath: '/var/run/docker.sock'
+ }))
+ expectAssignable<Client>(new Client('', {
+ pipelining: 1
+ }))
+ expectAssignable<Client>(new Client('', {
+ strictContentLength: true
+ }))
+ expectAssignable<Client>(new Client('', {
+ maxCachedSessions: 1
+ }))
+ expectAssignable<Client>(new Client('', {
+ maxRedirections: 1
+ }))
+ expectAssignable<Client>(new Client('', {
+ maxRequestsPerClient: 1
+ }))
+ expectAssignable<Client>(new Client('', {
+ localAddress: '127.0.0.1'
+ }))
+ expectAssignable<Client>(new Client('', {
+ maxResponseSize: -1
+ }))
+ expectAssignable<Client>(new Client('', {
+ autoSelectFamily: true
+ }))
+ expectAssignable<Client>(new Client('', {
+ autoSelectFamilyAttemptTimeout: 300e3
+ }))
+ expectAssignable<Client>(new Client('', {
+ interceptors: {
+ Client: [(dispatcher) => {
+ expectAssignable<Dispatcher['dispatch']>(dispatcher);
+ return (opts, handlers) => {
+ expectAssignable<Dispatcher.DispatchOptions>(opts);
+ expectAssignable<Dispatcher.DispatchHandlers>(handlers);
+ return dispatcher(opts, handlers)
+ }
+ }]
+ }
+ }))
+}
+
+{
+ const client = new Client('')
+
+ // properties
+ expectAssignable<number>(client.pipelining)
+ expectAssignable<boolean>(client.closed)
+ expectAssignable<boolean>(client.destroyed)
+
+ // request
+ expectAssignable<Promise<Dispatcher.ResponseData>>(client.request({ origin: '', path: '', method: 'GET' }))
+ expectAssignable<Promise<Dispatcher.ResponseData>>(client.request({ origin: new URL('http://localhost:3000'), path: '', method: 'GET' }))
+ expectAssignable<void>(client.request({ origin: '', path: '', method: 'GET' }, (err, data) => {
+ expectAssignable<Error | null>(err)
+ expectAssignable<Dispatcher.ResponseData>(data)
+ }))
+ expectAssignable<void>(client.request({ origin: new URL('http://localhost:3000'), path: '', method: 'GET' }, (err, data) => {
+ expectAssignable<Error | null>(err)
+ expectAssignable<Dispatcher.ResponseData>(data)
+ }))
+
+ // stream
+ expectAssignable<Promise<Dispatcher.StreamData>>(client.stream({ origin: '', path: '', method: 'GET' }, data => {
+ expectAssignable<Dispatcher.StreamFactoryData>(data)
+ return new Writable()
+ }))
+ expectAssignable<Promise<Dispatcher.StreamData>>(client.stream({ origin: new URL('http://localhost'), path: '', method: 'GET' }, data => {
+ expectAssignable<Dispatcher.StreamFactoryData>(data)
+ return new Writable()
+ }))
+ expectAssignable<void>(client.stream(
+ { origin: '', path: '', method: 'GET' },
+ data => {
+ expectAssignable<Dispatcher.StreamFactoryData>(data)
+ return new Writable()
+ },
+ (err, data) => {
+ expectAssignable<Error | null>(err)
+ expectAssignable<Dispatcher.StreamData>(data)
+ }
+ ))
+ expectAssignable<void>(client.stream(
+ { origin: new URL('http://localhost'), path: '', method: 'GET' },
+ data => {
+ expectAssignable<Dispatcher.StreamFactoryData>(data)
+ return new Writable()
+ },
+ (err, data) => {
+ expectAssignable<Error | null>(err)
+ expectAssignable<Dispatcher.StreamData>(data)
+ }
+ ))
+
+ // pipeline
+ expectAssignable<Duplex>(client.pipeline({ origin: '', path: '', method: 'GET' }, data => {
+ expectAssignable<Dispatcher.PipelineHandlerData>(data)
+ return new Readable()
+ }))
+ expectAssignable<Duplex>(client.pipeline({ origin: new URL('http://localhost'), path: '', method: 'GET' }, data => {
+ expectAssignable<Dispatcher.PipelineHandlerData>(data)
+ return new Readable()
+ }))
+
+ // upgrade
+ expectAssignable<Promise<Dispatcher.UpgradeData>>(client.upgrade({ path: '' }))
+ expectAssignable<Promise<Dispatcher.UpgradeData>>(client.upgrade({ path: '', headers: [] }))
+ expectAssignable<Promise<Dispatcher.UpgradeData>>(client.upgrade({ path: '', headers: {} }))
+ expectAssignable<Promise<Dispatcher.UpgradeData>>(client.upgrade({ path: '', headers: null }))
+ expectAssignable<void>(client.upgrade({ path: '' }, (err, data) => {
+ expectAssignable<Error | null>(err)
+ expectAssignable<Dispatcher.UpgradeData>(data)
+ }))
+
+ // connect
+ expectAssignable<Promise<Dispatcher.ConnectData>>(client.connect({ path: '' }))
+ expectAssignable<Promise<Dispatcher.ConnectData>>(client.connect({ path: '', headers: [] }))
+ expectAssignable<Promise<Dispatcher.ConnectData>>(client.connect({ path: '', headers: {} }))
+ expectAssignable<Promise<Dispatcher.ConnectData>>(client.connect({ path: '', headers: null }))
+ expectAssignable<void>(client.connect({ path: '' }, (err, data) => {
+ expectAssignable<Error | null>(err)
+ expectAssignable<Dispatcher.ConnectData>(data)
+ }))
+
+ // dispatch
+ expectAssignable<boolean>(client.dispatch({ origin: '', path: '', method: 'GET' }, {}))
+ expectAssignable<boolean>(client.dispatch({ origin: '', path: '', method: 'GET', headers: [] }, {}))
+ expectAssignable<boolean>(client.dispatch({ origin: '', path: '', method: 'GET', headers: {} }, {}))
+ expectAssignable<boolean>(client.dispatch({ origin: '', path: '', method: 'GET', headers: null }, {}))
+ expectAssignable<boolean>(client.dispatch({ origin: new URL('http://localhost'), path: '', method: 'GET' }, {}))
+
+ // close
+ expectAssignable<Promise<void>>(client.close())
+ expectAssignable<void>(client.close(() => {}))
+
+ // destroy
+ expectAssignable<Promise<void>>(client.destroy())
+ expectAssignable<Promise<void>>(client.destroy(new Error()))
+ expectAssignable<Promise<void>>(client.destroy(null))
+ expectAssignable<void>(client.destroy(() => {}))
+ expectAssignable<void>(client.destroy(new Error(), () => {}))
+ expectAssignable<void>(client.destroy(null, () => {}))
+}
diff --git a/test/types/connector.test-d.ts b/test/types/connector.test-d.ts
new file mode 100644
index 0000000..9236569
--- /dev/null
+++ b/test/types/connector.test-d.ts
@@ -0,0 +1,38 @@
+import {expectAssignable} from 'tsd'
+import { Client, buildConnector } from '../..'
+import {ConnectionOptions, TLSSocket} from 'tls'
+import {Socket} from 'net'
+import {IpcNetConnectOpts, NetConnectOpts, TcpNetConnectOpts} from "net";
+
+const connector = buildConnector({ rejectUnauthorized: false, allowH2: false })
+expectAssignable<Client>(new Client('', {
+ connect (opts: buildConnector.Options, cb: buildConnector.Callback) {
+ connector(opts, (...args) => {
+ if (args[0]) {
+ return cb(args[0], null)
+ }
+ if (args[1] instanceof TLSSocket) {
+ if (args[1].getPeerCertificate().fingerprint256 !== 'FO:OB:AR') {
+ args[1].destroy()
+ return cb(new Error('Fingerprint does not match'), null)
+ }
+ }
+ return cb(null, args[1])
+ })
+ }
+}))
+
+expectAssignable<buildConnector.BuildOptions>({
+ checkServerIdentity: () => undefined, // Test if ConnectionOptions is assignable
+ localPort: 1234, // Test if TcpNetConnectOpts is assignable
+ keepAlive: true,
+ keepAliveInitialDelay: 12345,
+});
+
+expectAssignable<buildConnector.Options>({
+ protocol: "http",
+ hostname: "example.com",
+ port: "",
+ localAddress: "127.0.0.1",
+ httpSocket: new Socket(),
+});
diff --git a/test/types/diagnostics-channel.test-d.ts b/test/types/diagnostics-channel.test-d.ts
new file mode 100644
index 0000000..334404c
--- /dev/null
+++ b/test/types/diagnostics-channel.test-d.ts
@@ -0,0 +1,72 @@
+import { Socket } from "net";
+import { expectAssignable } from "tsd";
+import { DiagnosticsChannel, buildConnector } from "../..";
+
+const request = {
+ origin: "",
+ completed: true,
+ method: "GET" as const,
+ path: "",
+ headers: "",
+ addHeader: (key: string, value: string) => {
+ return request;
+ },
+};
+
+const response = {
+ statusCode: 200,
+ statusText: "OK",
+ headers: [Buffer.from(""), Buffer.from("")],
+};
+
+const connectParams = {
+ host: "",
+ hostname: "",
+ protocol: "",
+ port: "",
+ servername: "",
+};
+
+expectAssignable<DiagnosticsChannel.RequestCreateMessage>({ request });
+expectAssignable<DiagnosticsChannel.RequestBodySentMessage>({ request });
+expectAssignable<DiagnosticsChannel.RequestHeadersMessage>({
+ request,
+ response,
+});
+expectAssignable<DiagnosticsChannel.RequestTrailersMessage>({
+ request,
+ trailers: [Buffer.from(""), Buffer.from("")],
+});
+expectAssignable<DiagnosticsChannel.RequestErrorMessage>({
+ request,
+ error: new Error("Error"),
+});
+expectAssignable<DiagnosticsChannel.ClientSendHeadersMessage>({
+ request,
+ headers: "",
+ socket: new Socket(),
+});
+expectAssignable<DiagnosticsChannel.ClientBeforeConnectMessage>({
+ connectParams,
+ connector: (
+ options: buildConnector.Options,
+ callback: buildConnector.Callback
+ ) => new Socket(),
+});
+expectAssignable<DiagnosticsChannel.ClientConnectedMessage>({
+ socket: new Socket(),
+ connectParams,
+ connector: (
+ options: buildConnector.Options,
+ callback: buildConnector.Callback
+ ) => new Socket(),
+});
+expectAssignable<DiagnosticsChannel.ClientConnectErrorMessage>({
+ error: new Error("Error"),
+ socket: new Socket(),
+ connectParams,
+ connector: (
+ options: buildConnector.Options,
+ callback: buildConnector.Callback
+ ) => new Socket(),
+});
diff --git a/test/types/dispatcher.events.test-d.ts b/test/types/dispatcher.events.test-d.ts
new file mode 100644
index 0000000..71057e7
--- /dev/null
+++ b/test/types/dispatcher.events.test-d.ts
@@ -0,0 +1,45 @@
+import { Dispatcher } from '../..'
+import {expectAssignable} from "tsd";
+import {URL} from "url";
+import Errors from "../../types/errors";
+
+interface EventHandler {
+ connect(origin: URL, targets: readonly Dispatcher[]): void
+ disconnect(origin: URL, targets: readonly Dispatcher[], error: Errors.UndiciError): void
+ connectionError(origin: URL, targets: readonly Dispatcher[], error: Errors.UndiciError): void
+ drain(origin: URL): void
+}
+
+{
+ const dispatcher = new Dispatcher()
+ const eventHandler: EventHandler = {} as EventHandler
+
+ expectAssignable<EventHandler['connect'][]>(dispatcher.rawListeners('connect'))
+ expectAssignable<EventHandler['disconnect'][]>(dispatcher.rawListeners('disconnect'))
+ expectAssignable<EventHandler['connectionError'][]>(dispatcher.rawListeners('connectionError'))
+ expectAssignable<EventHandler['drain'][]>(dispatcher.rawListeners('drain'))
+
+ expectAssignable<EventHandler['connect'][]>(dispatcher.listeners('connect'))
+ expectAssignable<EventHandler['disconnect'][]>(dispatcher.listeners('disconnect'))
+ expectAssignable<EventHandler['connectionError'][]>(dispatcher.listeners('connectionError'))
+ expectAssignable<EventHandler['drain'][]>(dispatcher.listeners('drain'))
+
+ const eventHandlerMethods: ['on', 'once', 'off', 'addListener', "removeListener", "prependListener", "prependOnceListener"]
+ = ['on', 'once', 'off', 'addListener', "removeListener", "prependListener", "prependOnceListener"]
+
+ for (const method of eventHandlerMethods) {
+ expectAssignable<Dispatcher>(dispatcher[method]('connect', eventHandler["connect"]))
+ expectAssignable<Dispatcher>(dispatcher[method]('disconnect', eventHandler["disconnect"]))
+ expectAssignable<Dispatcher>(dispatcher[method]('connectionError', eventHandler["connectionError"]))
+ expectAssignable<Dispatcher>(dispatcher[method]('drain', eventHandler["drain"]))
+ }
+
+ const origin = new URL('')
+ const targets = new Array<Dispatcher>()
+ const error = new Errors.UndiciError()
+ expectAssignable<boolean>(dispatcher.emit('connect', origin, targets))
+ expectAssignable<boolean>(dispatcher.emit('disconnect', origin, targets, error))
+ expectAssignable<boolean>(dispatcher.emit('connectionError', origin, targets, error))
+ expectAssignable<boolean>(dispatcher.emit('drain', origin))
+}
+
diff --git a/test/types/dispatcher.test-d.ts b/test/types/dispatcher.test-d.ts
new file mode 100644
index 0000000..cd4ebfd
--- /dev/null
+++ b/test/types/dispatcher.test-d.ts
@@ -0,0 +1,123 @@
+import { IncomingHttpHeaders } from 'http'
+import { Duplex, Readable, Writable } from 'stream'
+import { expectAssignable, expectType } from 'tsd'
+import { Dispatcher } from '../..'
+import { URL } from 'url'
+import { Blob } from 'buffer'
+
+expectAssignable<Dispatcher>(new Dispatcher())
+
+{
+ const dispatcher = new Dispatcher()
+
+ const nodeCoreHeaders = {
+ authorization: undefined,
+ ['content-type']: 'application/json'
+ } satisfies IncomingHttpHeaders;
+
+ // dispatch
+ expectAssignable<boolean>(dispatcher.dispatch({ path: '', method: 'GET' }, {}))
+ expectAssignable<boolean>(dispatcher.dispatch({ origin: '', path: '', method: 'GET' }, {}))
+ expectAssignable<boolean>(dispatcher.dispatch({ origin: '', path: '', method: 'GET', headers: { authorization: undefined } }, {}))
+ expectAssignable<boolean>(dispatcher.dispatch({ origin: '', path: '', method: 'GET', headers: [] }, {}))
+ expectAssignable<boolean>(dispatcher.dispatch({ origin: '', path: '', method: 'GET', headers: {} }, {}))
+ expectAssignable<boolean>(dispatcher.dispatch({ origin: '', path: '', method: 'GET', headers: nodeCoreHeaders }, {}))
+ expectAssignable<boolean>(dispatcher.dispatch({ origin: '', path: '', method: 'GET', headers: null, reset: true }, {}))
+ expectAssignable<boolean>(dispatcher.dispatch({ origin: new URL('http://localhost'), path: '', method: 'GET' }, {}))
+
+ // connect
+ expectAssignable<Promise<Dispatcher.ConnectData>>(dispatcher.connect({ path: '', maxRedirections: 0 }))
+ expectAssignable<void>(dispatcher.connect({ path: '' }, (err, data) => {
+ expectAssignable<Error | null>(err)
+ expectAssignable<Dispatcher.ConnectData>(data)
+ }))
+
+ // request
+ expectAssignable<Promise<Dispatcher.ResponseData>>(dispatcher.request({ origin: '', path: '', method: 'GET', maxRedirections: 0 }))
+ expectAssignable<Promise<Dispatcher.ResponseData>>(dispatcher.request({ origin: '', path: '', method: 'GET', maxRedirections: 0, query: {} }))
+ expectAssignable<Promise<Dispatcher.ResponseData>>(dispatcher.request({ origin: '', path: '', method: 'GET', maxRedirections: 0, query: { pageNum: 1, id: 'abc' } }))
+ expectAssignable<Promise<Dispatcher.ResponseData>>(dispatcher.request({ origin: '', path: '', method: 'GET', maxRedirections: 0, throwOnError: true }))
+ expectAssignable<Promise<Dispatcher.ResponseData>>(dispatcher.request({ origin: new URL('http://localhost'), path: '', method: 'GET' }))
+ expectAssignable<void>(dispatcher.request({ origin: '', path: '', method: 'GET', reset: true }, (err, data) => {
+ expectAssignable<Error | null>(err)
+ expectAssignable<Dispatcher.ResponseData>(data)
+ }))
+ expectAssignable<void>(dispatcher.request({ origin: new URL('http://localhost'), path: '', method: 'GET' }, (err, data) => {
+ expectAssignable<Error | null>(err)
+ expectAssignable<Dispatcher.ResponseData>(data)
+ }))
+
+ // pipeline
+ expectAssignable<Duplex>(dispatcher.pipeline({ origin: '', path: '', method: 'GET', maxRedirections: 0 }, data => {
+ expectAssignable<Dispatcher.PipelineHandlerData>(data)
+ return new Readable()
+ }))
+ expectAssignable<Duplex>(dispatcher.pipeline({ origin: new URL('http://localhost'), path: '', method: 'GET' }, data => {
+ expectAssignable<Dispatcher.PipelineHandlerData>(data)
+ return new Readable()
+ }))
+
+ // stream
+ expectAssignable<Promise<Dispatcher.StreamData>>(dispatcher.stream({ origin: '', path: '', method: 'GET', maxRedirections: 0 }, data => {
+ expectAssignable<Dispatcher.StreamFactoryData>(data)
+ return new Writable()
+ }))
+ expectAssignable<Promise<Dispatcher.StreamData>>(dispatcher.stream({ origin: new URL('http://localhost'), path: '', method: 'GET' }, data => {
+ expectAssignable<Dispatcher.StreamFactoryData>(data)
+ return new Writable()
+ }))
+ expectAssignable<void>(dispatcher.stream(
+ { origin: '', path: '', method: 'GET', reset: false },
+ data => {
+ expectAssignable<Dispatcher.StreamFactoryData>(data)
+ return new Writable()
+ },
+ (err, data) => {
+ expectAssignable<Error | null>(err)
+ expectAssignable<Dispatcher.StreamData>(data)
+ }
+ ))
+ expectAssignable<void>(dispatcher.stream(
+ { origin: new URL('http://localhost'), path: '', method: 'GET' },
+ data => {
+ expectAssignable<Dispatcher.StreamFactoryData>(data)
+ return new Writable()
+ },
+ (err, data) => {
+ expectAssignable<Error | null>(err)
+ expectAssignable<Dispatcher.StreamData>(data)
+ }
+ ))
+
+ // upgrade
+ expectAssignable<Promise<Dispatcher.UpgradeData>>(dispatcher.upgrade({ path: '', maxRedirections: 0 }))
+ expectAssignable<void>(dispatcher.upgrade({ path: '' }, (err, data) => {
+ expectAssignable<Error | null>(err)
+ expectAssignable<Dispatcher.UpgradeData>(data)
+ }))
+
+ // close
+ expectAssignable<Promise<void>>(dispatcher.close())
+ expectAssignable<void>(dispatcher.close(() => {}))
+
+ // destroy
+ expectAssignable<Promise<void>>(dispatcher.destroy())
+ expectAssignable<Promise<void>>(dispatcher.destroy(new Error()))
+ expectAssignable<Promise<void>>(dispatcher.destroy(null))
+ expectAssignable<void>(dispatcher.destroy(() => {}))
+ expectAssignable<void>(dispatcher.destroy(new Error(), () => {}))
+ expectAssignable<void>(dispatcher.destroy(null, () => {}))
+}
+
+declare const { body }: Dispatcher.ResponseData;
+
+{
+ // body mixin tests
+ expectType<never | undefined>(body.body)
+ expectType<boolean>(body.bodyUsed)
+ expectType<Promise<ArrayBuffer>>(body.arrayBuffer())
+ expectType<Promise<Blob>>(body.blob())
+ expectType<Promise<never>>(body.formData())
+ expectType<Promise<string>>(body.text())
+ expectType<Promise<unknown>>(body.json())
+}
diff --git a/test/types/errors.test-d.ts b/test/types/errors.test-d.ts
new file mode 100644
index 0000000..837dbf8
--- /dev/null
+++ b/test/types/errors.test-d.ts
@@ -0,0 +1,115 @@
+import { expectAssignable } from 'tsd'
+import { errors } from '../..'
+import Client from '../../types/client'
+import { IncomingHttpHeaders } from "../../types/header";
+
+expectAssignable<errors.UndiciError>(new errors.UndiciError())
+expectAssignable<string>(new errors.UndiciError().name)
+expectAssignable<string>(new errors.UndiciError().code)
+
+expectAssignable<errors.UndiciError>(new errors.ConnectTimeoutError())
+expectAssignable<errors.ConnectTimeoutError>(new errors.ConnectTimeoutError())
+expectAssignable<'ConnectTimeoutError'>(new errors.ConnectTimeoutError().name)
+expectAssignable<'UND_ERR_CONNECT_TIMEOUT'>(new errors.ConnectTimeoutError().code)
+
+expectAssignable<errors.UndiciError>(new errors.HeadersTimeoutError())
+expectAssignable<errors.HeadersTimeoutError>(new errors.HeadersTimeoutError())
+expectAssignable<'HeadersTimeoutError'>(new errors.HeadersTimeoutError().name)
+expectAssignable<'UND_ERR_HEADERS_TIMEOUT'>(new errors.HeadersTimeoutError().code)
+
+expectAssignable<errors.UndiciError>(new errors.HeadersOverflowError())
+expectAssignable<errors.HeadersOverflowError>(new errors.HeadersOverflowError())
+expectAssignable<'HeadersOverflowError'>(new errors.HeadersOverflowError().name)
+expectAssignable<'UND_ERR_HEADERS_OVERFLOW'>(new errors.HeadersOverflowError().code)
+
+expectAssignable<errors.UndiciError>(new errors.BodyTimeoutError())
+expectAssignable<errors.BodyTimeoutError>(new errors.BodyTimeoutError())
+expectAssignable<'BodyTimeoutError'>(new errors.BodyTimeoutError().name)
+expectAssignable<'UND_ERR_BODY_TIMEOUT'>(new errors.BodyTimeoutError().code)
+
+expectAssignable<errors.UndiciError>(new errors.ResponseStatusCodeError())
+expectAssignable<errors.ResponseStatusCodeError>(new errors.ResponseStatusCodeError())
+expectAssignable<'ResponseStatusCodeError'>(new errors.ResponseStatusCodeError().name)
+expectAssignable<'UND_ERR_RESPONSE_STATUS_CODE'>(new errors.ResponseStatusCodeError().code)
+expectAssignable<number>(new errors.ResponseStatusCodeError().status)
+expectAssignable<number>(new errors.ResponseStatusCodeError().statusCode)
+expectAssignable<IncomingHttpHeaders | string[] | null>(new errors.ResponseStatusCodeError().headers)
+expectAssignable<null | Record<string, any> | string>(new errors.ResponseStatusCodeError().body)
+
+expectAssignable<errors.UndiciError>(new errors.InvalidArgumentError())
+expectAssignable<errors.InvalidArgumentError>(new errors.InvalidArgumentError())
+expectAssignable<'InvalidArgumentError'>(new errors.InvalidArgumentError().name)
+expectAssignable<'UND_ERR_INVALID_ARG'>(new errors.InvalidArgumentError().code)
+
+expectAssignable<errors.UndiciError>(new errors.InvalidReturnValueError())
+expectAssignable<errors.InvalidReturnValueError>(new errors.InvalidReturnValueError())
+expectAssignable<'InvalidReturnValueError'>(new errors.InvalidReturnValueError().name)
+expectAssignable<'UND_ERR_INVALID_RETURN_VALUE'>(new errors.InvalidReturnValueError().code)
+
+expectAssignable<errors.UndiciError>(new errors.RequestAbortedError())
+expectAssignable<errors.RequestAbortedError>(new errors.RequestAbortedError())
+expectAssignable<'AbortError'>(new errors.RequestAbortedError().name)
+expectAssignable<'UND_ERR_ABORTED'>(new errors.RequestAbortedError().code)
+
+expectAssignable<errors.UndiciError>(new errors.InformationalError())
+expectAssignable<errors.InformationalError>(new errors.InformationalError())
+expectAssignable<'InformationalError'>(new errors.InformationalError().name)
+expectAssignable<'UND_ERR_INFO'>(new errors.InformationalError().code)
+
+expectAssignable<errors.UndiciError>(new errors.RequestContentLengthMismatchError())
+expectAssignable<errors.RequestContentLengthMismatchError>(new errors.RequestContentLengthMismatchError())
+expectAssignable<'RequestContentLengthMismatchError'>(new errors.RequestContentLengthMismatchError().name)
+expectAssignable<'UND_ERR_REQ_CONTENT_LENGTH_MISMATCH'>(new errors.RequestContentLengthMismatchError().code)
+
+expectAssignable<errors.UndiciError>(new errors.ResponseContentLengthMismatchError())
+expectAssignable<errors.ResponseContentLengthMismatchError>(new errors.ResponseContentLengthMismatchError())
+expectAssignable<'ResponseContentLengthMismatchError'>(new errors.ResponseContentLengthMismatchError().name)
+expectAssignable<'UND_ERR_RES_CONTENT_LENGTH_MISMATCH'>(new errors.ResponseContentLengthMismatchError().code)
+
+expectAssignable<errors.UndiciError>(new errors.ClientDestroyedError())
+expectAssignable<errors.ClientDestroyedError>(new errors.ClientDestroyedError())
+expectAssignable<'ClientDestroyedError'>(new errors.ClientDestroyedError().name)
+expectAssignable<'UND_ERR_DESTROYED'>(new errors.ClientDestroyedError().code)
+
+expectAssignable<errors.UndiciError>(new errors.ClientClosedError())
+expectAssignable<errors.ClientClosedError>(new errors.ClientClosedError())
+expectAssignable<'ClientClosedError'>(new errors.ClientClosedError().name)
+expectAssignable<'UND_ERR_CLOSED'>(new errors.ClientClosedError().code)
+
+expectAssignable<errors.UndiciError>(new errors.SocketError())
+expectAssignable<errors.SocketError>(new errors.SocketError())
+expectAssignable<'SocketError'>(new errors.SocketError().name)
+expectAssignable<'UND_ERR_SOCKET'>(new errors.SocketError().code)
+expectAssignable<Client.SocketInfo | null>(new errors.SocketError().socket)
+
+expectAssignable<errors.UndiciError>(new errors.NotSupportedError())
+expectAssignable<errors.NotSupportedError>(new errors.NotSupportedError())
+expectAssignable<'NotSupportedError'>(new errors.NotSupportedError().name)
+expectAssignable<'UND_ERR_NOT_SUPPORTED'>(new errors.NotSupportedError().code)
+
+expectAssignable<errors.UndiciError>(new errors.BalancedPoolMissingUpstreamError())
+expectAssignable<errors.BalancedPoolMissingUpstreamError>(new errors.BalancedPoolMissingUpstreamError())
+expectAssignable<'MissingUpstreamError'>(new errors.BalancedPoolMissingUpstreamError().name)
+expectAssignable<'UND_ERR_BPL_MISSING_UPSTREAM'>(new errors.BalancedPoolMissingUpstreamError().code)
+
+expectAssignable<errors.UndiciError>(new errors.HTTPParserError())
+expectAssignable<errors.HTTPParserError>(new errors.HTTPParserError())
+expectAssignable<'HTTPParserError'>(new errors.HTTPParserError().name)
+
+expectAssignable<errors.UndiciError>(new errors.ResponseExceededMaxSizeError())
+expectAssignable<errors.ResponseExceededMaxSizeError>(new errors.ResponseExceededMaxSizeError())
+expectAssignable<'ResponseExceededMaxSizeError'>(new errors.ResponseExceededMaxSizeError().name)
+expectAssignable<'UND_ERR_RES_EXCEEDED_MAX_SIZE'>(new errors.ResponseExceededMaxSizeError().code)
+
+{
+ // @ts-ignore
+ function f (): errors.HeadersTimeoutError | errors.ConnectTimeoutError { return }
+
+ const e = f()
+
+ if (e.code === 'UND_ERR_HEADERS_TIMEOUT') {
+ expectAssignable<errors.HeadersTimeoutError>(e)
+ } else if (e.code === 'UND_ERR_CONNECT_TIMEOUT') {
+ expectAssignable<errors.ConnectTimeoutError>(e)
+ }
+}
diff --git a/test/types/fetch.test-d.ts b/test/types/fetch.test-d.ts
new file mode 100644
index 0000000..59fb49f
--- /dev/null
+++ b/test/types/fetch.test-d.ts
@@ -0,0 +1,173 @@
+import { URL } from 'url'
+import { Blob } from 'buffer'
+import { ReadableStream } from 'stream/web'
+import { expectType, expectError, expectAssignable, expectNotAssignable } from 'tsd'
+import {
+ Agent,
+ BodyInit,
+ fetch,
+ FormData,
+ Headers,
+ HeadersInit,
+ SpecIterableIterator,
+ Request,
+ RequestCache,
+ RequestCredentials,
+ RequestDestination,
+ RequestInit,
+ RequestMode,
+ RequestRedirect,
+ Response,
+ ResponseInit,
+ ResponseType,
+ ReferrerPolicy
+} from '../..'
+
+const requestInit: RequestInit = {}
+const responseInit: ResponseInit = { status: 200, statusText: 'OK' }
+const requestInit2: RequestInit = {
+ dispatcher: new Agent()
+}
+const requestInit3: RequestInit = {}
+// Test assignment. See https://github.com/whatwg/fetch/issues/1445
+requestInit3.credentials = 'include'
+
+declare const request: Request
+declare const headers: Headers
+declare const response: Response
+
+expectType<string | undefined>(requestInit.method)
+expectType<boolean | undefined>(requestInit.keepalive)
+expectType<HeadersInit | undefined>(requestInit.headers)
+expectType<BodyInit | undefined>(requestInit.body)
+expectType<RequestRedirect | undefined>(requestInit.redirect)
+expectType<string | undefined>(requestInit.integrity)
+expectType<AbortSignal | null | undefined>(requestInit.signal)
+expectType<RequestCredentials | undefined>(requestInit.credentials)
+expectType<RequestMode | undefined>(requestInit.mode)
+expectType<string | undefined>(requestInit.referrer);
+expectType<ReferrerPolicy | undefined>(requestInit.referrerPolicy)
+expectType<null | undefined>(requestInit.window)
+
+expectType<number | undefined>(responseInit.status)
+expectType<string | undefined>(responseInit.statusText)
+expectType<HeadersInit | undefined>(responseInit.headers)
+
+expectType<Headers>(new Headers())
+expectType<Headers>(new Headers({}))
+expectType<Headers>(new Headers([]))
+expectType<Headers>(new Headers(headers))
+expectType<Headers>(new Headers(undefined))
+
+expectType<Request>(new Request(request))
+expectType<Request>(new Request('https://example.com'))
+expectType<Request>(new Request(new URL('https://example.com')))
+expectType<Request>(new Request(request, requestInit))
+expectType<Request>(new Request('https://example.com', requestInit))
+expectType<Request>(new Request(new URL('https://example.com'), requestInit))
+
+expectType<Promise<Response>>(fetch(request))
+expectType<Promise<Response>>(fetch('https://example.com'))
+expectType<Promise<Response>>(fetch(new URL('https://example.com')))
+expectType<Promise<Response>>(fetch(request, requestInit))
+expectType<Promise<Response>>(fetch('https://example.com', requestInit))
+expectType<Promise<Response>>(fetch(new URL('https://example.com'), requestInit))
+
+expectType<Response>(new Response())
+expectType<Response>(new Response(null))
+expectType<Response>(new Response('string'))
+expectType<Response>(new Response(new Blob([])))
+expectType<Response>(new Response(new FormData()))
+expectType<Response>(new Response(new Int8Array()))
+expectType<Response>(new Response(new Uint8Array()))
+expectType<Response>(new Response(new Uint8ClampedArray()))
+expectType<Response>(new Response(new Int16Array()))
+expectType<Response>(new Response(new Uint16Array()))
+expectType<Response>(new Response(new Int32Array()))
+expectType<Response>(new Response(new Uint32Array()))
+expectType<Response>(new Response(new Float32Array()))
+expectType<Response>(new Response(new Float64Array()))
+expectType<Response>(new Response(new BigInt64Array()))
+expectType<Response>(new Response(new BigUint64Array()))
+expectType<Response>(new Response(new ArrayBuffer(0)))
+expectType<Response>(new Response(null, responseInit))
+expectType<Response>(new Response('string', responseInit))
+expectType<Response>(new Response(new Blob([]), responseInit))
+expectType<Response>(new Response(new FormData(), responseInit))
+expectType<Response>(new Response(new Int8Array(), responseInit))
+expectType<Response>(new Response(new Uint8Array(), responseInit))
+expectType<Response>(new Response(new Uint8ClampedArray(), responseInit))
+expectType<Response>(new Response(new Int16Array(), responseInit))
+expectType<Response>(new Response(new Uint16Array(), responseInit))
+expectType<Response>(new Response(new Int32Array(), responseInit))
+expectType<Response>(new Response(new Uint32Array(), responseInit))
+expectType<Response>(new Response(new Float32Array(), responseInit))
+expectType<Response>(new Response(new Float64Array(), responseInit))
+expectType<Response>(new Response(new BigInt64Array(), responseInit))
+expectType<Response>(new Response(new BigUint64Array(), responseInit))
+expectType<Response>(new Response(new ArrayBuffer(0), responseInit))
+expectType<Response>(Response.error())
+expectType<Response>(Response.json({ a: 'b' }))
+expectType<Response>(Response.json({}, { status: 200 }))
+expectType<Response>(Response.json({}, { statusText: 'OK' }))
+expectType<Response>(Response.json({}, { headers: {} }))
+expectType<Response>(Response.json(null))
+expectType<Response>(Response.redirect('https://example.com', 301))
+expectType<Response>(Response.redirect('https://example.com', 302))
+expectType<Response>(Response.redirect('https://example.com', 303))
+expectType<Response>(Response.redirect('https://example.com', 307))
+expectType<Response>(Response.redirect('https://example.com', 308))
+expectError(Response.redirect('https://example.com', NaN))
+expectError(Response.json())
+expectError(Response.json(null, 3))
+
+expectType<void>(headers.append('key', 'value'))
+expectType<void>(headers.delete('key'))
+expectType<string | null>(headers.get('key'))
+expectType<boolean>(headers.has('key'))
+expectType<void>(headers.set('key', 'value'))
+expectType<SpecIterableIterator<string>>(headers.keys())
+expectType<SpecIterableIterator<string>>(headers.values())
+expectType<SpecIterableIterator<[string, string]>>(headers.entries())
+
+expectType<RequestCache>(request.cache)
+expectType<RequestCredentials>(request.credentials)
+expectType<RequestDestination>(request.destination)
+expectType<Headers>(request.headers)
+expectType<string>(request.integrity)
+expectType<string>(request.method)
+expectType<RequestMode>(request.mode)
+expectType<RequestRedirect>(request.redirect)
+expectType<string>(request.referrerPolicy)
+expectType<string>(request.url)
+expectType<boolean>(request.keepalive)
+expectType<AbortSignal>(request.signal)
+expectType<boolean>(request.bodyUsed)
+expectType<Promise<ArrayBuffer>>(request.arrayBuffer())
+expectType<Promise<Blob>>(request.blob())
+expectType<Promise<FormData>>(request.formData())
+expectType<Promise<unknown>>(request.json())
+expectType<Promise<string>>(request.text())
+expectType<Request>(request.clone())
+
+expectType<Headers>(response.headers)
+expectType<boolean>(response.ok)
+expectType<number>(response.status)
+expectType<string>(response.statusText)
+expectType<ResponseType>(response.type)
+expectType<string>(response.url)
+expectType<boolean>(response.redirected)
+expectType<ReadableStream | null>(response.body)
+expectType<boolean>(response.bodyUsed)
+expectType<Promise<ArrayBuffer>>(response.arrayBuffer())
+expectType<Promise<Blob>>(response.blob())
+expectType<Promise<FormData>>(response.formData())
+expectType<Promise<unknown>>(response.json())
+expectType<Promise<string>>(response.text())
+expectType<Response>(response.clone())
+
+expectType<Request>(new Request('https://example.com', { body: 'Hello, world', duplex: 'half' }))
+expectAssignable<RequestInit>({ duplex: 'half' })
+expectNotAssignable<RequestInit>({ duplex: 'not valid' })
+
+expectType<string[]>(headers.getSetCookie())
diff --git a/test/types/formdata.test-d.ts b/test/types/formdata.test-d.ts
new file mode 100644
index 0000000..79058c4
--- /dev/null
+++ b/test/types/formdata.test-d.ts
@@ -0,0 +1,27 @@
+import { Blob } from 'buffer'
+import { Readable } from 'stream'
+import { expectAssignable, expectType } from 'tsd'
+import { File, FormData, SpecIterableIterator } from '../..'
+import Dispatcher from '../../types/dispatcher'
+
+declare const dispatcherOptions: Dispatcher.DispatchOptions
+
+declare const blob: Blob
+const formData = new FormData()
+expectType<FormData>(formData)
+
+expectType<void>(formData.append('key', 'value'))
+expectType<void>(formData.append('key', blob))
+expectType<void>(formData.set('key', 'value'))
+expectType<void>(formData.set('key', blob))
+expectType<File | string | null>(formData.get('key'))
+expectType<File | string | null>(formData.get('key'))
+expectType<Array<File | string>>(formData.getAll('key'))
+expectType<Array<File | string>>(formData.getAll('key'))
+expectType<boolean>(formData.has('key'))
+expectType<void>(formData.delete('key'))
+expectAssignable<SpecIterableIterator<string>>(formData.keys())
+expectAssignable<SpecIterableIterator<File | string>>(formData.values())
+expectAssignable<SpecIterableIterator<[string, File | string]>>(formData.entries())
+expectAssignable<SpecIterableIterator<[string, File | string]>>(formData[Symbol.iterator]())
+expectAssignable<string | Buffer | Uint8Array | FormData | Readable | undefined | null>(dispatcherOptions.body)
diff --git a/test/types/global-dispatcher.test-d.ts b/test/types/global-dispatcher.test-d.ts
new file mode 100644
index 0000000..428b809
--- /dev/null
+++ b/test/types/global-dispatcher.test-d.ts
@@ -0,0 +1,12 @@
+import { expectAssignable } from 'tsd'
+import { setGlobalDispatcher, Dispatcher, getGlobalDispatcher } from '../..'
+
+{
+ expectAssignable<void>(setGlobalDispatcher(new Dispatcher()))
+ class CustomDispatcher extends Dispatcher {}
+ expectAssignable<void>(setGlobalDispatcher(new CustomDispatcher()))
+}
+
+{
+ expectAssignable<Dispatcher>(getGlobalDispatcher())
+}
diff --git a/test/types/header.test-d.ts b/test/types/header.test-d.ts
new file mode 100644
index 0000000..38ac9f6
--- /dev/null
+++ b/test/types/header.test-d.ts
@@ -0,0 +1,16 @@
+import { IncomingHttpHeaders as CoreIncomingHttpHeaders } from "http";
+import { expectAssignable, expectNotAssignable } from "tsd";
+import { IncomingHttpHeaders } from "../../types/header";
+
+const headers = {
+ authorization: undefined,
+ ["content-type"]: "application/json",
+} satisfies CoreIncomingHttpHeaders;
+
+expectAssignable<IncomingHttpHeaders>(headers);
+
+// It is why we do not need to add ` | null` to `IncomingHttpHeaders`:
+expectNotAssignable<CoreIncomingHttpHeaders>({
+ authorization: null,
+ ["content-type"]: "application/json",
+});
diff --git a/test/types/index.test-d.ts b/test/types/index.test-d.ts
new file mode 100644
index 0000000..3827e61
--- /dev/null
+++ b/test/types/index.test-d.ts
@@ -0,0 +1,23 @@
+import { expectAssignable } from 'tsd'
+import Undici, {Pool, Client, errors, fetch, Interceptable, RedirectHandler, DecoratorHandler, Headers, Response, Request, FormData, File, FileReader} from '../..'
+import Dispatcher from "../../types/dispatcher";
+
+expectAssignable<Pool>(new Undici.Pool('', {}))
+expectAssignable<Client>(new Undici.Client('', {}))
+expectAssignable<Interceptable>(new Undici.MockAgent().get(''))
+expectAssignable<typeof errors>(Undici.errors)
+expectAssignable<typeof fetch>(Undici.fetch)
+expectAssignable<typeof Headers>(Undici.Headers)
+expectAssignable<typeof Response>(Undici.Response)
+expectAssignable<typeof Request>(Undici.Request)
+expectAssignable<typeof FormData>(Undici.FormData)
+expectAssignable<typeof File>(Undici.File)
+expectAssignable<typeof FileReader>(Undici.FileReader)
+
+const client = new Undici.Client('', {})
+const handler: Dispatcher.DispatchHandlers = {}
+
+expectAssignable<RedirectHandler>(new Undici.RedirectHandler(client, 10, {
+ path: '/', method: 'GET'
+}, handler))
+expectAssignable<DecoratorHandler>(new Undici.DecoratorHandler(handler))
diff --git a/test/types/interceptor.test-d.ts b/test/types/interceptor.test-d.ts
new file mode 100644
index 0000000..ba242bf
--- /dev/null
+++ b/test/types/interceptor.test-d.ts
@@ -0,0 +1,5 @@
+import {expectAssignable} from "tsd";
+import Undici from "../..";
+import Dispatcher from "../../types/dispatcher";
+
+expectAssignable<Dispatcher.DispatchInterceptor>(Undici.createRedirectInterceptor({ maxRedirections: 3 }))
diff --git a/test/types/mock-agent.test-d.ts b/test/types/mock-agent.test-d.ts
new file mode 100644
index 0000000..5f7f968
--- /dev/null
+++ b/test/types/mock-agent.test-d.ts
@@ -0,0 +1,75 @@
+import {expectAssignable, expectType} from 'tsd'
+import {Agent, Dispatcher, MockAgent, MockClient, MockPool, setGlobalDispatcher} from '../..'
+import {MockInterceptor} from '../../types/mock-interceptor'
+import MockDispatch = MockInterceptor.MockDispatch;
+
+expectAssignable<MockAgent>(new MockAgent())
+expectAssignable<MockAgent>(new MockAgent({}))
+
+{
+ const mockAgent = new MockAgent()
+ expectAssignable<void>(setGlobalDispatcher(mockAgent))
+
+ // get
+ expectAssignable<MockPool>(mockAgent.get(''))
+ expectAssignable<MockPool>(mockAgent.get(new RegExp('')))
+ expectAssignable<MockPool>(mockAgent.get((origin) => {
+ expectAssignable<string>(origin)
+ return true
+ }))
+ expectAssignable<Dispatcher>(mockAgent.get(''))
+
+ // close
+ expectAssignable<Promise<void>>(mockAgent.close())
+
+ // deactivate
+ expectAssignable<void>(mockAgent.deactivate())
+
+ // activate
+ expectAssignable<void>(mockAgent.activate())
+
+ // enableNetConnect
+ expectAssignable<void>(mockAgent.enableNetConnect())
+ expectAssignable<void>(mockAgent.enableNetConnect(''))
+ expectAssignable<void>(mockAgent.enableNetConnect(new RegExp('')))
+ expectAssignable<void>(mockAgent.enableNetConnect((host) => {
+ expectAssignable<string>(host)
+ return true
+ }))
+
+ // disableNetConnect
+ expectAssignable<void>(mockAgent.disableNetConnect())
+
+ // dispatch
+ expectAssignable<boolean>(mockAgent.dispatch({origin: '', path: '', method: 'GET'}, {}))
+
+ // intercept
+ expectAssignable<MockInterceptor>((mockAgent.get('foo')).intercept({path: '', method: 'GET'}))
+}
+
+{
+ const mockAgent = new MockAgent({connections: 1})
+ expectAssignable<void>(setGlobalDispatcher(mockAgent))
+ expectAssignable<MockClient>(mockAgent.get(''))
+}
+
+{
+ const agent = new Agent()
+ const mockAgent = new MockAgent({agent})
+ expectAssignable<void>(setGlobalDispatcher(mockAgent))
+ expectAssignable<MockPool>(mockAgent.get(''))
+}
+
+{
+ interface PendingInterceptor extends MockDispatch {
+ origin: string;
+ }
+
+ const agent = new MockAgent({agent: new Agent()})
+ expectType<() => PendingInterceptor[]>(agent.pendingInterceptors)
+ expectType<(options?: {
+ pendingInterceptorsFormatter?: {
+ format(pendingInterceptors: readonly PendingInterceptor[]): string;
+ }
+ }) => void>(agent.assertNoPendingInterceptors)
+}
diff --git a/test/types/mock-client.test-d.ts b/test/types/mock-client.test-d.ts
new file mode 100644
index 0000000..9e92b8e
--- /dev/null
+++ b/test/types/mock-client.test-d.ts
@@ -0,0 +1,43 @@
+import { expectAssignable } from 'tsd'
+import { MockAgent, MockClient } from '../..'
+import { MockInterceptor } from '../../types/mock-interceptor'
+
+{
+ const mockClient: MockClient = new MockAgent({ connections: 1 }).get('')
+
+ // intercept
+ expectAssignable<MockInterceptor>(mockClient.intercept({ path: '', method: 'GET' }))
+ expectAssignable<MockInterceptor>(mockClient.intercept({ path: '', method: 'GET', body: '', headers: { 'User-Agent': '' } }))
+ expectAssignable<MockInterceptor>(mockClient.intercept({ path: '', method: 'GET', query: { id: 1 } }))
+ expectAssignable<MockInterceptor>(mockClient.intercept({ path: new RegExp(''), method: new RegExp(''), body: new RegExp(''), headers: { 'User-Agent': new RegExp('') } }))
+ expectAssignable<MockInterceptor>(mockClient.intercept({
+ path: (path) => {
+ expectAssignable<string>(path)
+ return true
+ },
+ method: (method) => {
+ expectAssignable<string>(method)
+ return true
+ },
+ body: (body) => {
+ expectAssignable<string>(body)
+ return true
+ },
+ headers: {
+ 'User-Agent': (header) => {
+ expectAssignable<string>(header)
+ return true
+ }
+ }
+ }))
+
+ // dispatch
+ expectAssignable<boolean>(mockClient.dispatch({ origin: '', path: '', method: 'GET' }, {}))
+
+ // close
+ expectAssignable<Promise<void>>(mockClient.close())
+}
+
+{
+ expectAssignable<MockClient>(new MockClient('', {agent: new MockAgent({ connections: 1})}))
+}
diff --git a/test/types/mock-errors.test-d.ts b/test/types/mock-errors.test-d.ts
new file mode 100644
index 0000000..2cf3e5e
--- /dev/null
+++ b/test/types/mock-errors.test-d.ts
@@ -0,0 +1,19 @@
+import { expectAssignable } from 'tsd'
+import { mockErrors, errors } from '../..'
+
+expectAssignable<errors.UndiciError>(new mockErrors.MockNotMatchedError())
+expectAssignable<mockErrors.MockNotMatchedError>(new mockErrors.MockNotMatchedError())
+expectAssignable<mockErrors.MockNotMatchedError>(new mockErrors.MockNotMatchedError('kaboom'))
+expectAssignable<'MockNotMatchedError'>(new mockErrors.MockNotMatchedError().name)
+expectAssignable<'UND_MOCK_ERR_MOCK_NOT_MATCHED'>(new mockErrors.MockNotMatchedError().code)
+
+{
+ // @ts-ignore
+ function f (): mockErrors.MockNotMatchedError { return }
+
+ const e = f()
+
+ if (e.code === 'UND_MOCK_ERR_MOCK_NOT_MATCHED') {
+ expectAssignable<mockErrors.MockNotMatchedError>(e)
+ }
+}
diff --git a/test/types/mock-interceptor.test-d.ts b/test/types/mock-interceptor.test-d.ts
new file mode 100644
index 0000000..24d29e1
--- /dev/null
+++ b/test/types/mock-interceptor.test-d.ts
@@ -0,0 +1,80 @@
+import { expectAssignable } from 'tsd'
+import { MockAgent, MockPool, BodyInit, Dispatcher } from '../..'
+import { MockInterceptor, MockScope } from '../../types/mock-interceptor'
+
+declare const mockResponseCallbackOptions: MockInterceptor.MockResponseCallbackOptions;
+
+expectAssignable<BodyInit | Dispatcher.DispatchOptions['body']>(mockResponseCallbackOptions.body)
+
+{
+ const mockPool: MockPool = new MockAgent().get('')
+ const mockInterceptor = mockPool.intercept({ path: '', method: 'GET' })
+ const mockInterceptorDefaultMethod = mockPool.intercept({ path: '' })
+
+ // reply
+ expectAssignable<MockScope>(mockInterceptor.reply(200))
+ expectAssignable<MockScope>(mockInterceptor.reply(200, ''))
+ expectAssignable<MockScope>(mockInterceptor.reply(200, Buffer))
+ expectAssignable<MockScope>(mockInterceptor.reply(200, {}))
+ expectAssignable<MockScope>(mockInterceptor.reply(200, () => ({})))
+ expectAssignable<MockScope>(mockInterceptor.reply(200, {}, {}))
+ expectAssignable<MockScope>(mockInterceptor.reply(200, () => ({}), {}))
+ expectAssignable<MockScope>(mockInterceptor.reply(200, {}, { headers: { foo: 'bar' }}))
+ expectAssignable<MockScope>(mockInterceptor.reply(200, () => ({}), { headers: { foo: 'bar' }}))
+ expectAssignable<MockScope>(mockInterceptor.reply(200, {}, { trailers: { foo: 'bar' }}))
+ expectAssignable<MockScope>(mockInterceptor.reply(200, () => ({}), { trailers: { foo: 'bar' }}))
+ expectAssignable<MockScope<{ foo: string }>>(mockInterceptor.reply<{ foo: string }>(200, { foo: 'bar' }))
+ expectAssignable<MockScope<{ foo: string }>>(mockInterceptor.reply<{ foo: string }>(200, () => ({ foo: 'bar' })))
+ expectAssignable<MockScope>(mockInterceptor.reply(() => ({ statusCode: 200, data: { foo: 'bar' }})))
+ expectAssignable<MockScope>(mockInterceptor.reply(() => ({ statusCode: 200, data: { foo: 'bar' }, responseOptions: {
+ headers: { foo: 'bar' }
+ }})))
+ expectAssignable<MockScope>(mockInterceptor.reply((options) => {
+ expectAssignable<MockInterceptor.MockResponseCallbackOptions>(options);
+ return { statusCode: 200, data: { foo: 'bar'}
+ }}))
+ expectAssignable<MockScope>(mockInterceptor.reply(() => ({ statusCode: 200, data: { foo: 'bar' }, responseOptions: {
+ trailers: { foo: 'bar' }
+ }})))
+ mockInterceptor.reply((options) => {
+ expectAssignable<MockInterceptor.MockResponseCallbackOptions['headers']>(options.headers);
+ return { statusCode: 200, data: { foo: 'bar' } }
+ })
+
+ // replyWithError
+ class CustomError extends Error {
+ hello(): void {}
+ }
+ expectAssignable<MockScope>(mockInterceptor.replyWithError(new Error('')))
+ expectAssignable<MockScope>(mockInterceptor.replyWithError<CustomError>(new CustomError('')))
+
+ // defaultReplyHeaders
+ expectAssignable<MockInterceptor>(mockInterceptor.defaultReplyHeaders({ foo: 'bar' }))
+
+ // defaultReplyTrailers
+ expectAssignable<MockInterceptor>(mockInterceptor.defaultReplyTrailers({ foo: 'bar' }))
+
+ // replyContentLength
+ expectAssignable<MockInterceptor>(mockInterceptor.replyContentLength())
+}
+
+{
+ const mockPool: MockPool = new MockAgent().get('')
+ const mockScope = mockPool.intercept({ path: '', method: 'GET' }).reply(200)
+
+ // delay
+ expectAssignable<MockScope>(mockScope.delay(1))
+
+ // persist
+ expectAssignable<MockScope>(mockScope.persist())
+
+ // times
+ expectAssignable<MockScope>(mockScope.times(2))
+}
+
+{
+ const mockPool: MockPool = new MockAgent().get('')
+ mockPool.intercept({ path: '', method: 'GET', headers: () => true })
+ mockPool.intercept({ path: '', method: 'GET', headers: () => false })
+ mockPool.intercept({ path: '', method: 'GET', headers: (headers) => Object.keys(headers).includes('authorization') })
+}
diff --git a/test/types/mock-pool.test-d.ts b/test/types/mock-pool.test-d.ts
new file mode 100644
index 0000000..b51779b
--- /dev/null
+++ b/test/types/mock-pool.test-d.ts
@@ -0,0 +1,42 @@
+import { expectAssignable } from 'tsd'
+import { MockAgent, MockPool } from '../..'
+import { MockInterceptor } from '../../types/mock-interceptor'
+
+{
+ const mockPool: MockPool = new MockAgent({ connections: 1 }).get('')
+
+ // intercept
+ expectAssignable<MockInterceptor>(mockPool.intercept({ path: '', method: 'GET' }))
+ expectAssignable<MockInterceptor>(mockPool.intercept({ path: '', method: 'GET', body: '', headers: { 'User-Agent': '' } }))
+ expectAssignable<MockInterceptor>(mockPool.intercept({ path: new RegExp(''), method: new RegExp(''), body: new RegExp(''), headers: { 'User-Agent': new RegExp('') } }))
+ expectAssignable<MockInterceptor>(mockPool.intercept({
+ path: (path) => {
+ expectAssignable<string>(path)
+ return true
+ },
+ method: (method) => {
+ expectAssignable<string>(method)
+ return true
+ },
+ body: (body) => {
+ expectAssignable<string>(body)
+ return true
+ },
+ headers: {
+ 'User-Agent': (header) => {
+ expectAssignable<string>(header)
+ return true
+ }
+ }
+ }))
+
+ // dispatch
+ expectAssignable<boolean>(mockPool.dispatch({ origin: '', path: '', method: 'GET' }, {}))
+
+ // close
+ expectAssignable<Promise<void>>(mockPool.close())
+}
+
+{
+ expectAssignable<MockPool>(new MockPool('', {agent: new MockAgent({ connections: 1})}))
+}
diff --git a/test/types/pool.test-d.ts b/test/types/pool.test-d.ts
new file mode 100644
index 0000000..c237468
--- /dev/null
+++ b/test/types/pool.test-d.ts
@@ -0,0 +1,112 @@
+import { Duplex, Readable, Writable } from 'stream'
+import { expectAssignable, expectType } from 'tsd'
+import { Dispatcher, Pool, Client } from '../..'
+import { URL } from 'url'
+
+expectAssignable<Pool>(new Pool(''))
+expectAssignable<Pool>(new Pool('', {}))
+expectAssignable<Pool>(new Pool(new URL('http://localhost'), {}))
+expectAssignable<Pool>(new Pool('', { factory: () => new Dispatcher() }))
+expectAssignable<Pool>(new Pool('', { factory: (origin, opts) => new Client(origin, opts) }))
+expectAssignable<Pool>(new Pool('', { connections: 1 }))
+
+{
+ const pool = new Pool('', {})
+
+ // properties
+ expectAssignable<boolean>(pool.closed)
+ expectAssignable<boolean>(pool.destroyed)
+ expectAssignable<Pool.PoolStats>(pool.stats)
+
+ // request
+ expectAssignable<Promise<Dispatcher.ResponseData>>(pool.request({ origin: '', path: '', method: 'GET' }))
+ expectAssignable<Promise<Dispatcher.ResponseData>>(pool.request({ origin: new URL('http://localhost'), path: '', method: 'GET' }))
+ expectAssignable<void>(pool.request({ origin: '', path: '', method: 'GET' }, (err, data) => {
+ expectAssignable<Error | null>(err)
+ expectAssignable<Dispatcher.ResponseData>(data)
+ }))
+ expectAssignable<void>(pool.request({ origin: new URL('http://localhost'), path: '', method: 'GET' }, (err, data) => {
+ expectAssignable<Error | null>(err)
+ expectAssignable<Dispatcher.ResponseData>(data)
+ }))
+
+ // stream
+ expectAssignable<Promise<Dispatcher.StreamData>>(pool.stream({ origin: '', path: '', method: 'GET' }, data => {
+ expectAssignable<Dispatcher.StreamFactoryData>(data)
+ return new Writable()
+ }))
+ expectAssignable<Promise<Dispatcher.StreamData>>(pool.stream({ origin: new URL('http://localhost'), path: '', method: 'GET' }, data => {
+ expectAssignable<Dispatcher.StreamFactoryData>(data)
+ return new Writable()
+ }))
+ expectAssignable<void>(pool.stream(
+ { origin: '', path: '', method: 'GET' },
+ data => {
+ expectAssignable<Dispatcher.StreamFactoryData>(data)
+ return new Writable()
+ },
+ (err, data) => {
+ expectAssignable<Error | null>(err)
+ expectAssignable<Dispatcher.StreamData>(data)
+ }
+ ))
+ expectAssignable<void>(pool.stream(
+ { origin: new URL('http://localhost'), path: '', method: 'GET' },
+ data => {
+ expectAssignable<Dispatcher.StreamFactoryData>(data)
+ return new Writable()
+ },
+ (err, data) => {
+ expectAssignable<Error | null>(err)
+ expectAssignable<Dispatcher.StreamData>(data)
+ }
+ ))
+
+ // pipeline
+ expectAssignable<Duplex>(pool.pipeline({ origin: '', path: '', method: 'GET' }, data => {
+ expectAssignable<Dispatcher.PipelineHandlerData>(data)
+ return new Readable()
+ }))
+ expectAssignable<Duplex>(pool.pipeline({ origin: new URL('http://localhost'), path: '', method: 'GET' }, data => {
+ expectAssignable<Dispatcher.PipelineHandlerData>(data)
+ return new Readable()
+ }))
+
+ // upgrade
+ expectAssignable<Promise<Dispatcher.UpgradeData>>(pool.upgrade({ path: '' }))
+ expectAssignable<void>(pool.upgrade({ path: '' }, (err, data) => {
+ expectAssignable<Error | null>(err)
+ expectAssignable<Dispatcher.UpgradeData>(data)
+ }))
+
+ // connect
+ expectAssignable<Promise<Dispatcher.ConnectData>>(pool.connect({ path: '' }))
+ expectAssignable<void>(pool.connect({ path: '' }, (err, data) => {
+ expectAssignable<Error | null>(err)
+ expectAssignable<Dispatcher.ConnectData>(data)
+ }))
+
+ // dispatch
+ expectAssignable<boolean>(pool.dispatch({ origin: '', path: '', method: 'GET' }, {}))
+ expectAssignable<boolean>(pool.dispatch({ origin: new URL('http://localhost'), path: '', method: 'GET' }, {}))
+
+ // close
+ expectAssignable<Promise<void>>(pool.close())
+ expectAssignable<void>(pool.close(() => {}))
+
+ // destroy
+ expectAssignable<Promise<void>>(pool.destroy())
+ expectAssignable<Promise<void>>(pool.destroy(new Error()))
+ expectAssignable<Promise<void>>(pool.destroy(null))
+ expectAssignable<void>(pool.destroy(() => {}))
+ expectAssignable<void>(pool.destroy(new Error(), () => {}))
+ expectAssignable<void>(pool.destroy(null, () => {}))
+
+ // stats
+ expectType<number>(pool.stats.connected)
+ expectType<number>(pool.stats.free)
+ expectType<number>(pool.stats.pending)
+ expectType<number>(pool.stats.queued)
+ expectType<number>(pool.stats.running)
+ expectType<number>(pool.stats.size)
+}
diff --git a/test/types/proxy-agent.test-d.ts b/test/types/proxy-agent.test-d.ts
new file mode 100644
index 0000000..7cc092b
--- /dev/null
+++ b/test/types/proxy-agent.test-d.ts
@@ -0,0 +1,43 @@
+import { expectAssignable } from 'tsd'
+import { URL } from 'url'
+import { ProxyAgent, setGlobalDispatcher, getGlobalDispatcher, Agent, Pool } from '../..'
+
+expectAssignable<ProxyAgent>(new ProxyAgent(''))
+expectAssignable<ProxyAgent>(new ProxyAgent({ uri: '' }))
+expectAssignable<ProxyAgent>(
+ new ProxyAgent({
+ connections: 1,
+ uri: '',
+ auth: '',
+ token: '',
+ maxRedirections: 1,
+ factory: (_origin: URL, opts: Object) => new Agent(opts),
+ requestTls: {
+ ca: [''],
+ key: '',
+ cert: '',
+ servername: '',
+ timeout: 1
+ },
+ proxyTls: {
+ ca: [''],
+ key: '',
+ cert: '',
+ servername: '',
+ timeout: 1
+ },
+ clientFactory: (origin: URL, opts: object) => new Pool(origin, opts)
+ })
+)
+
+{
+ const proxyAgent = new ProxyAgent('')
+ expectAssignable<void>(setGlobalDispatcher(proxyAgent))
+ expectAssignable<ProxyAgent>(getGlobalDispatcher())
+
+ // close
+ expectAssignable<Promise<void>>(proxyAgent.close())
+
+ // dispatch
+ expectAssignable<boolean>(proxyAgent.dispatch({ origin: '', path: '', method: 'GET' }, {}))
+}
diff --git a/test/types/readable.test-d.ts b/test/types/readable.test-d.ts
new file mode 100644
index 0000000..d004b70
--- /dev/null
+++ b/test/types/readable.test-d.ts
@@ -0,0 +1,34 @@
+import { expectAssignable } from 'tsd'
+import BodyReadable from '../../types/readable'
+import { Blob } from 'buffer'
+
+expectAssignable<BodyReadable>(new BodyReadable())
+
+{
+ const readable = new BodyReadable()
+
+ // dump
+ expectAssignable<Promise<void>>(readable.dump())
+ expectAssignable<Promise<void>>(readable.dump({ limit: 123 }))
+
+ // text
+ expectAssignable<Promise<string>>(readable.text())
+
+ // json
+ expectAssignable<Promise<unknown>>(readable.json())
+
+ // blob
+ expectAssignable<Promise<Blob>>(readable.blob())
+
+ // arrayBuffer
+ expectAssignable<Promise<ArrayBuffer>>(readable.arrayBuffer())
+
+ // formData
+ expectAssignable<Promise<never>>(readable.formData())
+
+ // bodyUsed
+ expectAssignable<boolean>(readable.bodyUsed)
+
+ // body
+ expectAssignable<never | undefined>(readable.body)
+}
diff --git a/test/unix.js b/test/unix.js
new file mode 100644
index 0000000..019f654
--- /dev/null
+++ b/test/unix.js
@@ -0,0 +1,141 @@
+'use strict'
+
+const { test } = require('tap')
+const { Client, Pool } = require('..')
+const http = require('http')
+const https = require('https')
+const pem = require('https-pem')
+const fs = require('fs')
+
+if (process.platform !== 'win32') {
+ test('http unix get', (t) => {
+ t.plan(7)
+
+ const server = http.createServer((req, res) => {
+ t.equal('/', req.url)
+ t.equal('GET', req.method)
+ t.equal('localhost', req.headers.host)
+ res.setHeader('Content-Type', 'text/plain')
+ res.end('hello')
+ })
+ t.teardown(server.close.bind(server))
+
+ try {
+ fs.unlinkSync('/var/tmp/test3.sock')
+ } catch (err) {
+
+ }
+
+ server.listen('/var/tmp/test3.sock', () => {
+ const client = new Client({
+ hostname: 'localhost',
+ protocol: 'http:'
+ }, {
+ socketPath: '/var/tmp/test3.sock'
+ })
+ t.teardown(client.close.bind(client))
+
+ client.request({ path: '/', method: 'GET' }, (err, data) => {
+ t.error(err)
+ const { statusCode, headers, body } = data
+ t.equal(statusCode, 200)
+ t.equal(headers['content-type'], 'text/plain')
+ const bufs = []
+ body.on('data', (buf) => {
+ bufs.push(buf)
+ })
+ body.on('end', () => {
+ t.equal('hello', Buffer.concat(bufs).toString('utf8'))
+ })
+ })
+ })
+ })
+
+ test('http unix get pool', (t) => {
+ t.plan(7)
+
+ const server = http.createServer((req, res) => {
+ t.equal('/', req.url)
+ t.equal('GET', req.method)
+ t.equal('localhost', req.headers.host)
+ res.setHeader('Content-Type', 'text/plain')
+ res.end('hello')
+ })
+ t.teardown(server.close.bind(server))
+
+ try {
+ fs.unlinkSync('/var/tmp/test3.sock')
+ } catch (err) {
+
+ }
+
+ server.listen('/var/tmp/test3.sock', () => {
+ const client = new Pool({
+ hostname: 'localhost',
+ protocol: 'http:'
+ }, {
+ socketPath: '/var/tmp/test3.sock'
+ })
+ t.teardown(client.close.bind(client))
+
+ client.request({ path: '/', method: 'GET' }, (err, data) => {
+ t.error(err)
+ const { statusCode, headers, body } = data
+ t.equal(statusCode, 200)
+ t.equal(headers['content-type'], 'text/plain')
+ const bufs = []
+ body.on('data', (buf) => {
+ bufs.push(buf)
+ })
+ body.on('end', () => {
+ t.equal('hello', Buffer.concat(bufs).toString('utf8'))
+ })
+ })
+ })
+ })
+
+ test('https get with tls opts', (t) => {
+ t.plan(6)
+
+ const server = https.createServer(pem, (req, res) => {
+ t.equal('/', req.url)
+ t.equal('GET', req.method)
+ res.setHeader('content-type', 'text/plain')
+ res.end('hello')
+ })
+ t.teardown(server.close.bind(server))
+
+ try {
+ fs.unlinkSync('/var/tmp/test3.sock')
+ } catch (err) {
+
+ }
+
+ server.listen('/var/tmp/test8.sock', () => {
+ const client = new Client({
+ hostname: 'localhost',
+ protocol: 'https:'
+ }, {
+ socketPath: '/var/tmp/test8.sock',
+ tls: {
+ rejectUnauthorized: false
+ }
+ })
+ t.teardown(client.close.bind(client))
+
+ client.request({ path: '/', method: 'GET' }, (err, data) => {
+ t.error(err)
+ const { statusCode, headers, body } = data
+ t.equal(statusCode, 200)
+ t.equal(headers['content-type'], 'text/plain')
+ const bufs = []
+ body.on('data', (buf) => {
+ bufs.push(buf)
+ })
+ body.on('end', () => {
+ t.equal('hello', Buffer.concat(bufs).toString('utf8'))
+ })
+ })
+ })
+ })
+}
diff --git a/test/util.js b/test/util.js
new file mode 100644
index 0000000..794c68e
--- /dev/null
+++ b/test/util.js
@@ -0,0 +1,123 @@
+'use strict'
+
+const t = require('tap')
+const { test } = t
+const { Stream } = require('stream')
+const { EventEmitter } = require('events')
+
+const util = require('../lib/core/util')
+const { InvalidArgumentError } = require('../lib/core/errors')
+
+test('isStream', (t) => {
+ t.plan(3)
+
+ const stream = new Stream()
+ t.ok(util.isStream(stream))
+
+ const buffer = Buffer.alloc(0)
+ t.notOk(util.isStream(buffer))
+
+ const ee = new EventEmitter()
+ t.notOk(util.isStream(ee))
+})
+
+test('getServerName', (t) => {
+ t.plan(6)
+ t.equal(util.getServerName('1.1.1.1'), '')
+ t.equal(util.getServerName('1.1.1.1:443'), '')
+ t.equal(util.getServerName('example.com'), 'example.com')
+ t.equal(util.getServerName('example.com:80'), 'example.com')
+ t.equal(util.getServerName('[2606:4700:4700::1111]'), '')
+ t.equal(util.getServerName('[2606:4700:4700::1111]:443'), '')
+})
+
+test('validateHandler', (t) => {
+ t.plan(9)
+
+ t.throws(() => util.validateHandler(null), InvalidArgumentError, 'handler must be an object')
+ t.throws(() => util.validateHandler({
+ onConnect: null
+ }), InvalidArgumentError, 'invalid onConnect method')
+ t.throws(() => util.validateHandler({
+ onConnect: () => {},
+ onError: null
+ }), InvalidArgumentError, 'invalid onError method')
+ t.throws(() => util.validateHandler({
+ onConnect: () => {},
+ onError: () => {},
+ onBodySent: null
+ }), InvalidArgumentError, 'invalid onBodySent method')
+ t.throws(() => util.validateHandler({
+ onConnect: () => {},
+ onError: () => {},
+ onBodySent: () => {},
+ onHeaders: null
+ }), InvalidArgumentError, 'invalid onHeaders method')
+ t.throws(() => util.validateHandler({
+ onConnect: () => {},
+ onError: () => {},
+ onBodySent: () => {},
+ onHeaders: () => {},
+ onData: null
+ }), InvalidArgumentError, 'invalid onData method')
+ t.throws(() => util.validateHandler({
+ onConnect: () => {},
+ onError: () => {},
+ onBodySent: () => {},
+ onHeaders: () => {},
+ onData: () => {},
+ onComplete: null
+ }), InvalidArgumentError, 'invalid onComplete method')
+ t.throws(() => util.validateHandler({
+ onConnect: () => {},
+ onError: () => {},
+ onBodySent: () => {},
+ onUpgrade: 'null'
+ }, 'CONNECT'), InvalidArgumentError, 'invalid onUpgrade method')
+ t.throws(() => util.validateHandler({
+ onConnect: () => {},
+ onError: () => {},
+ onBodySent: () => {},
+ onUpgrade: 'null'
+ }, 'CONNECT', () => {}), InvalidArgumentError, 'invalid onUpgrade method')
+})
+
+test('parseHeaders', (t) => {
+ t.plan(6)
+ t.same(util.parseHeaders(['key', 'value']), { key: 'value' })
+ t.same(util.parseHeaders([Buffer.from('key'), Buffer.from('value')]), { key: 'value' })
+ t.same(util.parseHeaders(['Key', 'Value']), { key: 'Value' })
+ t.same(util.parseHeaders(['Key', 'value', 'key', 'Value']), { key: ['value', 'Value'] })
+ t.same(util.parseHeaders(['key', ['value1', 'value2', 'value3']]), { key: ['value1', 'value2', 'value3'] })
+ t.same(util.parseHeaders([Buffer.from('key'), [Buffer.from('value1'), Buffer.from('value2'), Buffer.from('value3')]]), { key: ['value1', 'value2', 'value3'] })
+})
+
+test('parseRawHeaders', (t) => {
+ t.plan(1)
+ t.same(util.parseRawHeaders(['key', 'value', Buffer.from('key'), Buffer.from('value')]), ['key', 'value', 'key', 'value'])
+})
+
+test('buildURL', { skip: util.nodeMajor >= 12 }, (t) => {
+ const tests = [
+ [{ id: BigInt(123456) }, 'id=123456'],
+ [{ date: new Date() }, 'date='],
+ [{ obj: { id: 1 } }, 'obj='],
+ [{ params: ['a', 'b', 'c'] }, 'params=a&params=b&params=c'],
+ [{ bool: true }, 'bool=true'],
+ [{ number: 123456 }, 'number=123456'],
+ [{ string: 'hello' }, 'string=hello'],
+ [{ null: null }, 'null='],
+ [{ void: undefined }, 'void='],
+ [{ fn: function () {} }, 'fn='],
+ [{}, '']
+ ]
+
+ const base = 'https://www.google.com'
+
+ for (const [input, output] of tests) {
+ const expected = `${base}${output ? `?${output}` : output}`
+ t.equal(util.buildURL(base, input), expected)
+ }
+
+ t.end()
+})
diff --git a/test/utils/async-iterators.js b/test/utils/async-iterators.js
new file mode 100644
index 0000000..da7e0a8
--- /dev/null
+++ b/test/utils/async-iterators.js
@@ -0,0 +1,25 @@
+'use strict'
+
+async function * wrapWithAsyncIterable (asyncIterable, indefinite = false) {
+ for await (const chunk of asyncIterable) {
+ yield chunk
+ }
+ if (indefinite) {
+ await new Promise(() => {})
+ }
+}
+
+const STREAM = 'stream'
+const ASYNC_ITERATOR = 'async-iterator'
+function maybeWrapStream (stream, type) {
+ if (type === STREAM) {
+ return stream
+ }
+ if (type === ASYNC_ITERATOR) {
+ return wrapWithAsyncIterable(stream)
+ }
+
+ throw new Error(`bad input ${type} should be ${STREAM} or ${ASYNC_ITERATOR}`)
+}
+
+module.exports = { wrapWithAsyncIterable, maybeWrapStream, consts: { STREAM, ASYNC_ITERATOR } }
diff --git a/test/utils/esm-wrapper.mjs b/test/utils/esm-wrapper.mjs
new file mode 100644
index 0000000..51f8572
--- /dev/null
+++ b/test/utils/esm-wrapper.mjs
@@ -0,0 +1,102 @@
+import { createServer } from 'http'
+import tap from 'tap'
+import {
+ Agent,
+ Client,
+ errors,
+ pipeline,
+ Pool,
+ request,
+ connect,
+ upgrade,
+ setGlobalDispatcher,
+ getGlobalDispatcher,
+ stream
+} from '../../index.js'
+
+const { test } = tap
+
+test('imported Client works with basic GET', (t) => {
+ t.plan(10)
+
+ const server = createServer((req, res) => {
+ t.equal('/', req.url)
+ t.equal('GET', req.method)
+ t.equal(`localhost:${server.address().port}`, req.headers.host)
+ t.equal(undefined, req.headers.foo)
+ t.equal('bar', req.headers.bar)
+ t.equal(undefined, req.headers['content-length'])
+ res.setHeader('Content-Type', 'text/plain')
+ res.end('hello')
+ })
+
+ t.teardown(server.close.bind(server))
+
+ const reqHeaders = {
+ foo: undefined,
+ bar: 'bar'
+ }
+
+ server.listen(0, () => {
+ const client = new Client(`http://localhost:${server.address().port}`)
+ t.teardown(client.close.bind(client))
+
+ client.request({
+ path: '/',
+ method: 'GET',
+ headers: reqHeaders
+ }, (err, data) => {
+ t.error(err)
+ const { statusCode, headers, body } = data
+ t.equal(statusCode, 200)
+ t.equal(headers['content-type'], 'text/plain')
+ const bufs = []
+ body.on('data', (buf) => {
+ bufs.push(buf)
+ })
+ body.on('end', () => {
+ t.equal('hello', Buffer.concat(bufs).toString('utf8'))
+ })
+ })
+ })
+})
+
+test('imported errors work with request args validation', (t) => {
+ t.plan(2)
+
+ const client = new Client('http://localhost:5000')
+
+ client.request(null, (err) => {
+ t.type(err, errors.InvalidArgumentError)
+ })
+
+ try {
+ client.request(null, 'asd')
+ } catch (err) {
+ t.type(err, errors.InvalidArgumentError)
+ }
+})
+
+test('imported errors work with request args validation promise', (t) => {
+ t.plan(1)
+
+ const client = new Client('http://localhost:5000')
+
+ client.request(null).catch((err) => {
+ t.type(err, errors.InvalidArgumentError)
+ })
+})
+
+test('named exports', (t) => {
+ t.equal(typeof Client, 'function')
+ t.equal(typeof Pool, 'function')
+ t.equal(typeof Agent, 'function')
+ t.equal(typeof request, 'function')
+ t.equal(typeof stream, 'function')
+ t.equal(typeof pipeline, 'function')
+ t.equal(typeof connect, 'function')
+ t.equal(typeof upgrade, 'function')
+ t.equal(typeof setGlobalDispatcher, 'function')
+ t.equal(typeof getGlobalDispatcher, 'function')
+ t.end()
+})
diff --git a/test/utils/formdata.js b/test/utils/formdata.js
new file mode 100644
index 0000000..edd8854
--- /dev/null
+++ b/test/utils/formdata.js
@@ -0,0 +1,49 @@
+const Busboy = require('@fastify/busboy')
+
+function parseFormDataString (
+ body,
+ contentType
+) {
+ const cache = {
+ fileMap: new Map(),
+ fields: []
+ }
+
+ const bb = new Busboy({
+ headers: {
+ 'content-type': contentType
+ }
+ })
+
+ return new Promise((resolve, reject) => {
+ bb.on('file', (name, file, filename, encoding, mimeType) => {
+ cache.fileMap.set(name, { data: [], info: { filename, encoding, mimeType } })
+
+ file.on('data', (data) => {
+ const old = cache.fileMap.get(name)
+
+ cache.fileMap.set(name, {
+ data: [...old.data, data],
+ info: old.info
+ })
+ }).on('end', () => {
+ const old = cache.fileMap.get(name)
+
+ cache.fileMap.set(name, {
+ data: Buffer.concat(old.data),
+ info: old.info
+ })
+ })
+ })
+
+ bb.on('field', (key, value) => cache.fields.push({ key, value }))
+ bb.on('finish', () => resolve(cache))
+ bb.on('error', (e) => reject(e))
+
+ bb.end(body)
+ })
+}
+
+module.exports = {
+ parseFormDataString
+}
diff --git a/test/utils/redirecting-servers.js b/test/utils/redirecting-servers.js
new file mode 100644
index 0000000..ad8aa58
--- /dev/null
+++ b/test/utils/redirecting-servers.js
@@ -0,0 +1,265 @@
+'use strict'
+
+const { createServer } = require('http')
+
+const isNode20 = process.version.startsWith('v20.')
+
+function close (server) {
+ return function () {
+ return new Promise(resolve => {
+ if (isNode20) {
+ server.closeAllConnections()
+ }
+ server.close(resolve)
+ })
+ }
+}
+
+function startServer (t, handler) {
+ return new Promise(resolve => {
+ const server = createServer(handler)
+
+ server.listen(0, () => {
+ resolve(`localhost:${server.address().port}`)
+ })
+
+ t.teardown(close(server))
+ })
+}
+
+async function startRedirectingServer (t) {
+ const server = await startServer(t, (req, res) => {
+ // Parse the path and normalize arguments
+ let [code, redirections, query] = req.url
+ .slice(1)
+ .split(/[/?]/)
+
+ if (req.url.indexOf('?') !== -1 && !query) {
+ query = redirections
+ redirections = 0
+ }
+
+ code = parseInt(code, 10)
+ redirections = parseInt(redirections, 10)
+
+ if (isNaN(code) || code < 0) {
+ code = 302
+ } else if (code < 300) {
+ res.statusCode = code
+ redirections = 5
+ }
+
+ if (isNaN(redirections) || redirections < 0) {
+ redirections = 0
+ }
+
+ // On 303, the method must be GET or HEAD after the first redirect
+ if (code === 303 && redirections > 0 && req.method !== 'GET' && req.method !== 'HEAD') {
+ res.statusCode = 400
+ res.setHeader('Connection', 'close')
+ res.end('Did not switch to GET')
+ return
+ }
+
+ // End the chain at some point
+ if (redirections === 5) {
+ res.setHeader('Connection', 'close')
+ res.write(
+ `${req.method} /${redirections}${query ? ` ${query}` : ''} :: ${Object.entries(req.headers)
+ .map(([k, v]) => `${k}@${v}`)
+ .join(' ')}`
+ )
+
+ if (parseInt(req.headers['content-length']) > 0) {
+ res.write(' :: ')
+ req.pipe(res)
+ } else {
+ res.end('')
+ }
+
+ return
+ }
+
+ // Redirect by default
+ res.statusCode = code
+ res.setHeader('Connection', 'close')
+ res.setHeader('Location', `http://${server}/${code}/${++redirections}${query ? `?${query}` : ''}`)
+ res.end('')
+ })
+
+ return server
+}
+
+async function startRedirectingWithBodyServer (t) {
+ const server = await startServer(t, (req, res) => {
+ if (req.url === '/') {
+ res.statusCode = 301
+ res.setHeader('Connection', 'close')
+ res.setHeader('Location', `http://${server}/end`)
+ res.end('REDIRECT')
+ return
+ }
+
+ res.setHeader('Connection', 'close')
+ res.end('FINAL')
+ })
+
+ return server
+}
+
+function startRedirectingWithoutLocationServer (t) {
+ return startServer(t, (req, res) => {
+ // Parse the path and normalize arguments
+ let [code] = req.url
+ .slice(1)
+ .split('/')
+ .map(r => parseInt(r, 10))
+
+ if (isNaN(code) || code < 0) {
+ code = 302
+ }
+
+ res.statusCode = code
+ res.setHeader('Connection', 'close')
+ res.end('')
+ })
+}
+
+async function startRedirectingChainServers (t) {
+ const server1 = await startServer(t, (req, res) => {
+ if (req.url === '/') {
+ res.statusCode = 301
+ res.setHeader('Connection', 'close')
+ res.setHeader('Location', `http://${server2}/`)
+ res.end('')
+ return
+ }
+
+ res.setHeader('Connection', 'close')
+ res.end(req.method)
+ })
+
+ const server2 = await startServer(t, (req, res) => {
+ res.statusCode = 301
+ res.setHeader('Connection', 'close')
+
+ if (req.url === '/') {
+ res.setHeader('Location', `http://${server3}/`)
+ } else {
+ res.setHeader('Location', `http://${server3}/end`)
+ }
+
+ res.end('')
+ })
+
+ const server3 = await startServer(t, (req, res) => {
+ res.statusCode = 301
+ res.setHeader('Connection', 'close')
+
+ if (req.url === '/') {
+ res.setHeader('Location', `http://${server2}/end`)
+ } else {
+ res.setHeader('Location', `http://${server1}/end`)
+ }
+
+ res.end('')
+ })
+
+ return [server1, server2, server3]
+}
+
+async function startRedirectingWithAuthorization (t, authorization) {
+ const server1 = await startServer(t, (req, res) => {
+ if (req.headers.authorization !== authorization) {
+ res.statusCode = 403
+ res.setHeader('Connection', 'close')
+ res.end('')
+ return
+ }
+
+ res.statusCode = 301
+ res.setHeader('Connection', 'close')
+
+ res.setHeader('Location', `http://${server2}`)
+ res.end('')
+ })
+
+ const server2 = await startServer(t, (req, res) => {
+ res.end(req.headers.authorization || '')
+ })
+
+ return [server1, server2]
+}
+
+async function startRedirectingWithCookie (t, cookie) {
+ const server1 = await startServer(t, (req, res) => {
+ if (req.headers.cookie !== cookie) {
+ res.statusCode = 403
+ res.setHeader('Connection', 'close')
+ res.end('')
+ return
+ }
+
+ res.statusCode = 301
+ res.setHeader('Connection', 'close')
+
+ res.setHeader('Location', `http://${server2}`)
+ res.end('')
+ })
+
+ const server2 = await startServer(t, (req, res) => {
+ res.end(req.headers.cookie || '')
+ })
+
+ return [server1, server2]
+}
+
+async function startRedirectingWithRelativePath (t) {
+ const server = await startServer(t, (req, res) => {
+ res.setHeader('Connection', 'close')
+
+ if (req.url === '/') {
+ res.statusCode = 301
+ res.setHeader('Location', '/absolute/a')
+ res.end('')
+ } else if (req.url === '/absolute/a') {
+ res.statusCode = 301
+ res.setHeader('Location', 'b')
+ res.end('')
+ } else {
+ res.statusCode = 200
+ res.end(req.url)
+ }
+ })
+
+ return server
+}
+
+async function startRedirectingWithQueryParams (t) {
+ const server = await startServer(t, (req, res) => {
+ if (req.url === '/?param1=first') {
+ res.statusCode = 301
+ res.setHeader('Connection', 'close')
+ res.setHeader('Location', `http://${server}/?param2=second`)
+ res.end('REDIRECT')
+ return
+ }
+
+ res.setHeader('Connection', 'close')
+ res.end('')
+ })
+
+ return server
+}
+
+module.exports = {
+ startServer,
+ startRedirectingServer,
+ startRedirectingWithBodyServer,
+ startRedirectingWithoutLocationServer,
+ startRedirectingChainServers,
+ startRedirectingWithAuthorization,
+ startRedirectingWithCookie,
+ startRedirectingWithRelativePath,
+ startRedirectingWithQueryParams
+}
diff --git a/test/utils/stream.js b/test/utils/stream.js
new file mode 100644
index 0000000..b78ff5c
--- /dev/null
+++ b/test/utils/stream.js
@@ -0,0 +1,48 @@
+'use strict'
+
+const { Readable, Writable } = require('stream')
+
+let ReadableStream
+
+function createReadable (data) {
+ return new Readable({
+ read () {
+ this.push(Buffer.from(data))
+ this.push(null)
+ }
+ })
+}
+
+function createWritable (target) {
+ return new Writable({
+ write (chunk, _, callback) {
+ target.push(chunk.toString())
+ callback()
+ },
+ final (callback) {
+ callback()
+ }
+ })
+}
+
+class Source {
+ constructor (data) {
+ this.data = data
+ }
+
+ async start (controller) {
+ this.controller = controller
+ }
+
+ async pull (controller) {
+ controller.enqueue(this.data)
+ controller.close()
+ }
+}
+
+function createReadableStream (data) {
+ ReadableStream = require('stream/web').ReadableStream
+ return new ReadableStream(new Source(data))
+}
+
+module.exports = { createReadableStream, createReadable, createWritable }
diff --git a/test/validations.js b/test/validations.js
new file mode 100644
index 0000000..d1b3409
--- /dev/null
+++ b/test/validations.js
@@ -0,0 +1,63 @@
+'use strict'
+
+const t = require('tap')
+const { test } = t
+const { createServer } = require('http')
+const { Client, errors } = require('..')
+
+const server = createServer((req, res) => {
+ res.setHeader('content-type', 'text/plain')
+ res.end('hello')
+ t.fail('server should never be called')
+})
+t.teardown(server.close.bind(server))
+
+server.listen(0, () => {
+ const url = `http://localhost:${server.address().port}`
+
+ test('path', (t) => {
+ t.plan(4)
+
+ const client = new Client(url)
+ t.teardown(client.close.bind(client))
+
+ client.request({ path: null, method: 'GET' }, (err, res) => {
+ t.type(err, errors.InvalidArgumentError)
+ t.equal(err.message, 'path must be a string')
+ })
+
+ client.request({ path: 'aaa', method: 'GET' }, (err, res) => {
+ t.type(err, errors.InvalidArgumentError)
+ t.equal(err.message, 'path must be an absolute URL or start with a slash')
+ })
+ })
+
+ test('method', (t) => {
+ t.plan(2)
+
+ const client = new Client(url)
+ t.teardown(client.close.bind(client))
+
+ client.request({ path: '/', method: null }, (err, res) => {
+ t.type(err, errors.InvalidArgumentError)
+ t.equal(err.message, 'method must be a string')
+ })
+ })
+
+ test('body', (t) => {
+ t.plan(4)
+
+ const client = new Client(url)
+ t.teardown(client.close.bind(client))
+
+ client.request({ path: '/', method: 'POST', body: 42 }, (err, res) => {
+ t.type(err, errors.InvalidArgumentError)
+ t.equal(err.message, 'body must be a string, a Buffer, a Readable stream, an iterable, or an async iterable')
+ })
+
+ client.request({ path: '/', method: 'POST', body: { hello: 'world' } }, (err, res) => {
+ t.type(err, errors.InvalidArgumentError)
+ t.equal(err.message, 'body must be a string, a Buffer, a Readable stream, an iterable, or an async iterable')
+ })
+ })
+})
diff --git a/test/webidl/converters.js b/test/webidl/converters.js
new file mode 100644
index 0000000..95bea88
--- /dev/null
+++ b/test/webidl/converters.js
@@ -0,0 +1,202 @@
+'use strict'
+
+const { test } = require('tap')
+const { webidl } = require('../../lib/fetch/webidl')
+
+test('sequence', (t) => {
+ const converter = webidl.sequenceConverter(
+ webidl.converters.DOMString
+ )
+
+ t.same(converter([1, 2, 3]), ['1', '2', '3'])
+
+ t.throws(() => {
+ converter(3)
+ }, TypeError, 'disallows non-objects')
+
+ t.throws(() => {
+ converter(null)
+ }, TypeError)
+
+ t.throws(() => {
+ converter(undefined)
+ }, TypeError)
+
+ t.throws(() => {
+ converter({})
+ }, TypeError, 'no Symbol.iterator')
+
+ t.throws(() => {
+ converter({
+ [Symbol.iterator]: 42
+ })
+ }, TypeError, 'invalid Symbol.iterator')
+
+ t.throws(() => {
+ converter(webidl.converters.sequence({
+ [Symbol.iterator] () {
+ return {
+ next: 'never!'
+ }
+ }
+ }))
+ }, TypeError, 'invalid generator')
+
+ t.end()
+})
+
+test('webidl.dictionaryConverter', (t) => {
+ t.test('arguments', (t) => {
+ const converter = webidl.dictionaryConverter([])
+
+ t.throws(() => {
+ converter(true)
+ }, TypeError)
+
+ for (const value of [{}, undefined, null]) {
+ t.doesNotThrow(() => {
+ converter(value)
+ })
+ }
+
+ t.end()
+ })
+
+ t.test('required key', (t) => {
+ const converter = webidl.dictionaryConverter([
+ {
+ converter: () => true,
+ key: 'Key',
+ required: true
+ }
+ ])
+
+ t.throws(() => {
+ converter({ wrongKey: 'key' })
+ }, TypeError)
+
+ t.doesNotThrow(() => {
+ converter({ Key: 'this key was required!' })
+ })
+
+ t.end()
+ })
+
+ t.end()
+})
+
+test('ArrayBuffer', (t) => {
+ t.throws(() => {
+ webidl.converters.ArrayBuffer(true)
+ }, TypeError)
+
+ t.throws(() => {
+ webidl.converters.ArrayBuffer({})
+ }, TypeError)
+
+ t.throws(() => {
+ const sab = new SharedArrayBuffer(1024)
+ webidl.converters.ArrayBuffer(sab, { allowShared: false })
+ }, TypeError)
+
+ t.doesNotThrow(() => {
+ const sab = new SharedArrayBuffer(1024)
+ webidl.converters.ArrayBuffer(sab)
+ })
+
+ t.doesNotThrow(() => {
+ const ab = new ArrayBuffer(8)
+ webidl.converters.ArrayBuffer(ab)
+ })
+
+ t.end()
+})
+
+test('TypedArray', (t) => {
+ t.throws(() => {
+ webidl.converters.TypedArray(3)
+ }, TypeError)
+
+ t.throws(() => {
+ webidl.converters.TypedArray({})
+ }, TypeError)
+
+ t.throws(() => {
+ const uint8 = new Uint8Array([1, 2, 3])
+ Object.defineProperty(uint8, 'buffer', {
+ get () {
+ return new SharedArrayBuffer(8)
+ }
+ })
+
+ webidl.converters.TypedArray(uint8, Uint8Array, {
+ allowShared: false
+ })
+ }, TypeError)
+
+ t.end()
+})
+
+test('DataView', (t) => {
+ t.throws(() => {
+ webidl.converters.DataView(3)
+ }, TypeError)
+
+ t.throws(() => {
+ webidl.converters.DataView({})
+ }, TypeError)
+
+ t.throws(() => {
+ const buffer = new ArrayBuffer(16)
+ const view = new DataView(buffer, 0)
+
+ Object.defineProperty(view, 'buffer', {
+ get () {
+ return new SharedArrayBuffer(8)
+ }
+ })
+
+ webidl.converters.DataView(view, {
+ allowShared: false
+ })
+ })
+
+ const buffer = new ArrayBuffer(16)
+ const view = new DataView(buffer, 0)
+
+ t.equal(webidl.converters.DataView(view), view)
+
+ t.end()
+})
+
+test('BufferSource', (t) => {
+ t.doesNotThrow(() => {
+ const buffer = new ArrayBuffer(16)
+ const view = new DataView(buffer, 0)
+
+ webidl.converters.BufferSource(view)
+ })
+
+ t.throws(() => {
+ webidl.converters.BufferSource(3)
+ }, TypeError)
+
+ t.end()
+})
+
+test('ByteString', (t) => {
+ t.doesNotThrow(() => {
+ webidl.converters.ByteString('')
+ })
+
+ // https://github.com/nodejs/undici/issues/1590
+ t.throws(() => {
+ const char = String.fromCharCode(256)
+ webidl.converters.ByteString(`invalid${char}char`)
+ }, {
+ message: 'Cannot convert argument to a ByteString because the character at ' +
+ 'index 7 has a value of 256 which is greater than 255.'
+ })
+
+ t.end()
+})
diff --git a/test/webidl/helpers.js b/test/webidl/helpers.js
new file mode 100644
index 0000000..f44d501
--- /dev/null
+++ b/test/webidl/helpers.js
@@ -0,0 +1,75 @@
+'use strict'
+
+const { test } = require('tap')
+const { webidl } = require('../../lib/fetch/webidl')
+
+test('webidl.interfaceConverter', (t) => {
+ class A {}
+ class B {}
+
+ const converter = webidl.interfaceConverter(A)
+
+ t.throws(() => {
+ converter(new B())
+ }, TypeError)
+
+ t.doesNotThrow(() => {
+ converter(new A())
+ })
+
+ t.end()
+})
+
+test('webidl.dictionaryConverter', (t) => {
+ t.test('extraneous keys are provided', (t) => {
+ const converter = webidl.dictionaryConverter([
+ {
+ key: 'key',
+ converter: webidl.converters.USVString,
+ defaultValue: 420,
+ required: true
+ }
+ ])
+
+ t.same(
+ converter({
+ a: 'b',
+ key: 'string',
+ c: 'd',
+ get value () {
+ return 6
+ }
+ }),
+ { key: 'string' }
+ )
+
+ t.end()
+ })
+
+ t.test('defaultValue with key = null', (t) => {
+ const converter = webidl.dictionaryConverter([
+ {
+ key: 'key',
+ converter: webidl.converters['unsigned short'],
+ defaultValue: 200
+ }
+ ])
+
+ t.same(converter({ key: null }), { key: 0 })
+ t.end()
+ })
+
+ t.test('no defaultValue and optional', (t) => {
+ const converter = webidl.dictionaryConverter([
+ {
+ key: 'key',
+ converter: webidl.converters.ByteString
+ }
+ ])
+
+ t.same(converter({ a: 'b', c: 'd' }), {})
+ t.end()
+ })
+
+ t.end()
+})
diff --git a/test/webidl/util.js b/test/webidl/util.js
new file mode 100644
index 0000000..c451590
--- /dev/null
+++ b/test/webidl/util.js
@@ -0,0 +1,106 @@
+'use strict'
+
+const { test } = require('tap')
+const { webidl } = require('../../lib/fetch/webidl')
+
+test('Type(V)', (t) => {
+ const Type = webidl.util.Type
+
+ t.equal(Type(undefined), 'Undefined')
+ t.equal(Type(null), 'Null')
+ t.equal(Type(true), 'Boolean')
+ t.equal(Type('string'), 'String')
+ t.equal(Type(Symbol('symbol')), 'Symbol')
+ t.equal(Type(1.23), 'Number')
+ t.equal(Type(1n), 'BigInt')
+ t.equal(Type({ a: 'b' }), 'Object')
+
+ t.end()
+})
+
+test('ConvertToInt(V)', (t) => {
+ const ConvertToInt = webidl.util.ConvertToInt
+
+ t.equal(ConvertToInt(63, 64, 'signed'), 63, 'odd int')
+ t.equal(ConvertToInt(64.49, 64, 'signed'), 64)
+ t.equal(ConvertToInt(64.51, 64, 'signed'), 64)
+
+ const max = 2 ** 53
+ t.equal(ConvertToInt(max + 1, 64, 'signed'), max, 'signed pos')
+ t.equal(ConvertToInt(-max - 1, 64, 'signed'), -max, 'signed neg')
+
+ t.equal(ConvertToInt(max + 1, 64, 'unsigned'), max + 1, 'unsigned pos')
+ t.equal(ConvertToInt(-max - 1, 64, 'unsigned'), -max - 1, 'unsigned neg')
+
+ for (const signedness of ['signed', 'unsigned']) {
+ t.equal(ConvertToInt(Infinity, 64, signedness), 0)
+ t.equal(ConvertToInt(-Infinity, 64, signedness), 0)
+ t.equal(ConvertToInt(NaN, 64, signedness), 0)
+ }
+
+ for (const signedness of ['signed', 'unsigned']) {
+ t.throws(() => {
+ ConvertToInt(NaN, 64, signedness, {
+ enforceRange: true
+ })
+ }, TypeError)
+
+ t.throws(() => {
+ ConvertToInt(Infinity, 64, signedness, {
+ enforceRange: true
+ })
+ }, TypeError)
+
+ t.throws(() => {
+ ConvertToInt(-Infinity, 64, signedness, {
+ enforceRange: true
+ })
+ }, TypeError)
+
+ t.throws(() => {
+ ConvertToInt(2 ** 53 + 1, 32, 'signed', {
+ enforceRange: true
+ })
+ }, TypeError)
+
+ t.throws(() => {
+ ConvertToInt(-(2 ** 53 + 1), 32, 'unsigned', {
+ enforceRange: true
+ })
+ }, TypeError)
+
+ t.equal(
+ ConvertToInt(65.5, 64, signedness, {
+ enforceRange: true
+ }),
+ 65
+ )
+ }
+
+ for (const signedness of ['signed', 'unsigned']) {
+ t.equal(
+ ConvertToInt(63.49, 64, signedness, {
+ clamp: true
+ }),
+ 64
+ )
+
+ t.equal(
+ ConvertToInt(63.51, 64, signedness, {
+ clamp: true
+ }),
+ 64
+ )
+
+ t.equal(
+ ConvertToInt(-0, 64, signedness, {
+ clamp: true
+ }),
+ 0
+ )
+ }
+
+ t.equal(ConvertToInt(111, 2, 'signed'), -1)
+
+ t.end()
+})
diff --git a/test/websocket/close.js b/test/websocket/close.js
new file mode 100644
index 0000000..4d314a4
--- /dev/null
+++ b/test/websocket/close.js
@@ -0,0 +1,130 @@
+'use strict'
+
+const { test } = require('tap')
+const { WebSocketServer } = require('ws')
+const { WebSocket } = require('../..')
+
+test('Close', (t) => {
+ t.plan(6)
+
+ t.test('Close with code', (t) => {
+ t.plan(1)
+
+ const server = new WebSocketServer({ port: 0 })
+
+ server.on('connection', (ws) => {
+ ws.on('close', (code) => {
+ t.equal(code, 1000)
+ })
+ })
+
+ t.teardown(server.close.bind(server))
+
+ const ws = new WebSocket(`ws://localhost:${server.address().port}`)
+ ws.addEventListener('open', () => ws.close(1000))
+ })
+
+ t.test('Close with code and reason', (t) => {
+ t.plan(2)
+
+ const server = new WebSocketServer({ port: 0 })
+
+ server.on('connection', (ws) => {
+ ws.on('close', (code, reason) => {
+ t.equal(code, 1000)
+ t.same(reason, Buffer.from('Goodbye'))
+ })
+ })
+
+ t.teardown(server.close.bind(server))
+
+ const ws = new WebSocket(`ws://localhost:${server.address().port}`)
+ ws.addEventListener('open', () => ws.close(1000, 'Goodbye'))
+ })
+
+ t.test('Close with invalid code', (t) => {
+ t.plan(2)
+
+ const server = new WebSocketServer({ port: 0 })
+
+ t.teardown(server.close.bind(server))
+
+ const ws = new WebSocket(`ws://localhost:${server.address().port}`)
+ ws.addEventListener('open', () => {
+ t.throws(
+ () => ws.close(2999),
+ {
+ name: 'InvalidAccessError',
+ constructor: DOMException
+ }
+ )
+
+ t.throws(
+ () => ws.close(5000),
+ {
+ name: 'InvalidAccessError',
+ constructor: DOMException
+ }
+ )
+
+ ws.close()
+ })
+ })
+
+ t.test('Close with invalid reason', (t) => {
+ t.plan(1)
+
+ const server = new WebSocketServer({ port: 0 })
+
+ t.teardown(server.close.bind(server))
+ const ws = new WebSocket(`ws://localhost:${server.address().port}`)
+
+ ws.addEventListener('open', () => {
+ t.throws(
+ () => ws.close(1000, 'a'.repeat(124)),
+ {
+ name: 'SyntaxError',
+ constructor: DOMException
+ }
+ )
+
+ ws.close(1000)
+ })
+ })
+
+ t.test('Close with no code or reason', (t) => {
+ t.plan(2)
+
+ const server = new WebSocketServer({ port: 0 })
+
+ server.on('connection', (ws) => {
+ ws.on('close', (code, reason) => {
+ t.equal(code, 1005)
+ t.same(reason, Buffer.alloc(0))
+ })
+ })
+
+ t.teardown(server.close.bind(server))
+
+ const ws = new WebSocket(`ws://localhost:${server.address().port}`)
+ ws.addEventListener('open', () => ws.close())
+ })
+
+ t.test('Close with a 3000 status code', (t) => {
+ t.plan(2)
+
+ const server = new WebSocketServer({ port: 0 })
+
+ server.on('connection', (ws) => {
+ ws.on('close', (code, reason) => {
+ t.equal(code, 3000)
+ t.same(reason, Buffer.alloc(0))
+ })
+ })
+
+ t.teardown(server.close.bind(server))
+
+ const ws = new WebSocket(`ws://localhost:${server.address().port}`)
+ ws.addEventListener('open', () => ws.close(3000))
+ })
+})
diff --git a/test/websocket/constructor.js b/test/websocket/constructor.js
new file mode 100644
index 0000000..dd87dea
--- /dev/null
+++ b/test/websocket/constructor.js
@@ -0,0 +1,48 @@
+'use strict'
+
+const { test } = require('tap')
+const { WebSocket } = require('../..')
+
+test('Constructor', (t) => {
+ t.throws(
+ () => new WebSocket('abc'),
+ {
+ name: 'SyntaxError',
+ constructor: DOMException
+ }
+ )
+
+ t.throws(
+ () => new WebSocket('wss://echo.websocket.events/#a'),
+ {
+ name: 'SyntaxError',
+ constructor: DOMException
+ }
+ )
+
+ t.throws(
+ () => new WebSocket('wss://echo.websocket.events', ''),
+ {
+ name: 'SyntaxError',
+ constructor: DOMException
+ }
+ )
+
+ t.throws(
+ () => new WebSocket('wss://echo.websocket.events', ['chat', 'chat']),
+ {
+ name: 'SyntaxError',
+ constructor: DOMException
+ }
+ )
+
+ t.throws(
+ () => new WebSocket('wss://echo.websocket.events', ['<>@,;:\\"/[]?={}\t']),
+ {
+ name: 'SyntaxError',
+ constructor: DOMException
+ }
+ )
+
+ t.end()
+})
diff --git a/test/websocket/custom-headers.js b/test/websocket/custom-headers.js
new file mode 100644
index 0000000..01f1830
--- /dev/null
+++ b/test/websocket/custom-headers.js
@@ -0,0 +1,30 @@
+'use strict'
+
+const { test } = require('tap')
+const assert = require('assert')
+const { Agent, WebSocket } = require('../..')
+
+test('Setting custom headers', (t) => {
+ t.plan(1)
+
+ const headers = {
+ 'x-khafra-hello': 'hi',
+ Authorization: 'Bearer base64orsomethingitreallydoesntmatter'
+ }
+
+ class TestAgent extends Agent {
+ dispatch (options) {
+ t.match(options.headers, headers)
+
+ return false
+ }
+ }
+
+ const ws = new WebSocket('wss://echo.websocket.events', {
+ headers,
+ dispatcher: new TestAgent()
+ })
+
+ // We don't want to make a request, just ensure the headers are set.
+ ws.onclose = ws.onerror = ws.onmessage = assert.fail
+})
diff --git a/test/websocket/diagnostics-channel.js b/test/websocket/diagnostics-channel.js
new file mode 100644
index 0000000..c3bf05a
--- /dev/null
+++ b/test/websocket/diagnostics-channel.js
@@ -0,0 +1,71 @@
+'use strict'
+
+const t = require('tap')
+const dc = require('diagnostics_channel')
+const { WebSocketServer } = require('ws')
+const { WebSocket } = require('../..')
+
+t.test('diagnostics channel', { jobs: 1 }, (t) => {
+ t.plan(2)
+
+ t.test('undici:websocket:open', (t) => {
+ t.plan(3)
+
+ const server = new WebSocketServer({ port: 0 })
+
+ server.on('connection', (ws) => {
+ ws.close(1000, 'goodbye')
+ })
+
+ const listener = ({ extensions, protocol }) => {
+ t.equal(extensions, null)
+ t.equal(protocol, 'chat')
+ }
+
+ t.teardown(() => {
+ dc.channel('undici:websocket:open').unsubscribe(listener)
+ return server.close()
+ })
+
+ const { port } = server.address()
+
+ dc.channel('undici:websocket:open').subscribe(listener)
+
+ const ws = new WebSocket(`ws://localhost:${port}`, 'chat')
+
+ ws.addEventListener('open', () => {
+ t.pass('Emitted open')
+ })
+ })
+
+ t.test('undici:websocket:close', (t) => {
+ t.plan(4)
+
+ const server = new WebSocketServer({ port: 0 })
+
+ server.on('connection', (ws) => {
+ ws.close(1000, 'goodbye')
+ })
+
+ const listener = ({ websocket, code, reason }) => {
+ t.type(websocket, WebSocket)
+ t.equal(code, 1000)
+ t.equal(reason, 'goodbye')
+ }
+
+ t.teardown(() => {
+ dc.channel('undici:websocket:close').unsubscribe(listener)
+ return server.close()
+ })
+
+ const { port } = server.address()
+
+ dc.channel('undici:websocket:close').subscribe(listener)
+
+ const ws = new WebSocket(`ws://localhost:${port}`, 'chat')
+
+ ws.addEventListener('close', () => {
+ t.pass('Emitted open')
+ })
+ })
+})
diff --git a/test/websocket/events.js b/test/websocket/events.js
new file mode 100644
index 0000000..e5b565c
--- /dev/null
+++ b/test/websocket/events.js
@@ -0,0 +1,204 @@
+'use strict'
+
+const { test } = require('tap')
+const { WebSocketServer } = require('ws')
+const { MessageEvent, CloseEvent, ErrorEvent } = require('../../lib/websocket/events')
+const { WebSocket } = require('../..')
+
+test('MessageEvent', (t) => {
+ t.throws(() => new MessageEvent(), TypeError, 'no arguments')
+ t.throws(() => new MessageEvent('').initMessageEvent(), TypeError)
+
+ const noInitEvent = new MessageEvent('message')
+
+ t.equal(noInitEvent.origin, '')
+ t.equal(noInitEvent.data, null)
+ t.equal(noInitEvent.lastEventId, '')
+ t.equal(noInitEvent.source, null)
+ t.ok(Array.isArray(noInitEvent.ports))
+ t.ok(Object.isFrozen(noInitEvent.ports))
+ t.type(new MessageEvent('').initMessageEvent('message'), MessageEvent)
+
+ t.end()
+})
+
+test('CloseEvent', (t) => {
+ t.throws(() => new CloseEvent(), TypeError)
+
+ const noInitEvent = new CloseEvent('close')
+
+ t.equal(noInitEvent.wasClean, false)
+ t.equal(noInitEvent.code, 0)
+ t.equal(noInitEvent.reason, '')
+
+ t.end()
+})
+
+test('ErrorEvent', (t) => {
+ t.throws(() => new ErrorEvent(), TypeError)
+
+ const noInitEvent = new ErrorEvent('error')
+
+ t.equal(noInitEvent.message, '')
+ t.equal(noInitEvent.filename, '')
+ t.equal(noInitEvent.lineno, 0)
+ t.equal(noInitEvent.colno, 0)
+ t.equal(noInitEvent.error, undefined)
+
+ t.end()
+})
+
+test('Event handlers', (t) => {
+ t.plan(4)
+
+ const server = new WebSocketServer({ port: 0 })
+ const ws = new WebSocket(`ws://localhost:${server.address().port}`)
+
+ function listen () {}
+
+ t.teardown(server.close.bind(server))
+ t.teardown(() => ws.close())
+
+ t.test('onopen', (t) => {
+ t.plan(3)
+
+ t.equal(ws.onopen, null)
+ ws.onopen = 3
+ t.equal(ws.onopen, null)
+ ws.onopen = listen
+ t.equal(ws.onopen, listen)
+ })
+
+ t.test('onerror', (t) => {
+ t.plan(3)
+
+ t.equal(ws.onerror, null)
+ ws.onerror = 3
+ t.equal(ws.onerror, null)
+ ws.onerror = listen
+ t.equal(ws.onerror, listen)
+ })
+
+ t.test('onclose', (t) => {
+ t.plan(3)
+
+ t.equal(ws.onclose, null)
+ ws.onclose = 3
+ t.equal(ws.onclose, null)
+ ws.onclose = listen
+ t.equal(ws.onclose, listen)
+ })
+
+ t.test('onmessage', (t) => {
+ t.plan(3)
+
+ t.equal(ws.onmessage, null)
+ ws.onmessage = 3
+ t.equal(ws.onmessage, null)
+ ws.onmessage = listen
+ t.equal(ws.onmessage, listen)
+ })
+})
+
+test('CloseEvent WPTs ported', (t) => {
+ t.test('initCloseEvent', (t) => {
+ // Taken from websockets/interfaces/CloseEvent/historical.html
+ t.notOk('initCloseEvent' in CloseEvent.prototype)
+ t.notOk('initCloseEvent' in new CloseEvent('close'))
+
+ t.end()
+ })
+
+ t.test('CloseEvent constructor', (t) => {
+ // Taken from websockets/interfaces/CloseEvent/constructor.html
+
+ {
+ const event = new CloseEvent('foo')
+
+ t.ok(event instanceof CloseEvent, 'should be a CloseEvent')
+ t.equal(event.type, 'foo')
+ t.notOk(event.bubbles, 'bubbles')
+ t.notOk(event.cancelable, 'cancelable')
+ t.notOk(event.wasClean, 'wasClean')
+ t.equal(event.code, 0)
+ t.equal(event.reason, '')
+ }
+
+ {
+ const event = new CloseEvent('foo', {
+ bubbles: true,
+ cancelable: true,
+ wasClean: true,
+ code: 7,
+ reason: 'x'
+ })
+ t.ok(event instanceof CloseEvent, 'should be a CloseEvent')
+ t.equal(event.type, 'foo')
+ t.ok(event.bubbles, 'bubbles')
+ t.ok(event.cancelable, 'cancelable')
+ t.ok(event.wasClean, 'wasClean')
+ t.equal(event.code, 7)
+ t.equal(event.reason, 'x')
+ }
+
+ t.end()
+ })
+
+ t.end()
+})
+
+test('ErrorEvent WPTs ported', (t) => {
+ t.test('Synthetic ErrorEvent', (t) => {
+ // Taken from html/webappapis/scripting/events/event-handler-processing-algorithm-error/document-synthetic-errorevent.html
+
+ {
+ const e = new ErrorEvent('error')
+ t.equal(e.message, '')
+ t.equal(e.filename, '')
+ t.equal(e.lineno, 0)
+ t.equal(e.colno, 0)
+ t.equal(e.error, undefined)
+ }
+
+ {
+ const e = new ErrorEvent('error', { error: null })
+ t.equal(e.error, null)
+ }
+
+ {
+ const e = new ErrorEvent('error', { error: undefined })
+ t.equal(e.error, undefined)
+ }
+
+ {
+ const e = new ErrorEvent('error', { error: 'foo' })
+ t.equal(e.error, 'foo')
+ }
+
+ t.end()
+ })
+
+ t.test('webidl', (t) => {
+ // Taken from webidl/ecmascript-binding/no-regexp-special-casing.any.js
+
+ const regExp = new RegExp()
+ regExp.message = 'some message'
+
+ const errorEvent = new ErrorEvent('type', regExp)
+
+ t.equal(errorEvent.message, 'some message')
+
+ t.end()
+ })
+
+ t.test('initErrorEvent', (t) => {
+ // Taken from workers/Worker_dispatchEvent_ErrorEvent.htm
+
+ const e = new ErrorEvent('error')
+ t.notOk('initErrorEvent' in e, 'should not be supported')
+
+ t.end()
+ })
+
+ t.end()
+})
diff --git a/test/websocket/fragments.js b/test/websocket/fragments.js
new file mode 100644
index 0000000..d51db4b
--- /dev/null
+++ b/test/websocket/fragments.js
@@ -0,0 +1,40 @@
+'use strict'
+
+const { test } = require('tap')
+const { WebSocketServer } = require('ws')
+const { WebSocket } = require('../..')
+const diagnosticsChannel = require('diagnostics_channel')
+
+test('Fragmented frame with a ping frame in the middle of it', (t) => {
+ t.plan(2)
+
+ const server = new WebSocketServer({ port: 0 })
+
+ server.on('connection', (ws) => {
+ const socket = ws._socket
+
+ socket.write(Buffer.from([0x01, 0x03, 0x48, 0x65, 0x6c])) // Text frame "Hel"
+ socket.write(Buffer.from([0x89, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f])) // ping "Hello"
+ socket.write(Buffer.from([0x80, 0x02, 0x6c, 0x6f])) // Text frame "lo"
+ })
+
+ t.teardown(() => {
+ for (const client of server.clients) {
+ client.close()
+ }
+
+ server.close()
+ })
+
+ const ws = new WebSocket(`ws://localhost:${server.address().port}`)
+
+ ws.addEventListener('message', ({ data }) => {
+ t.same(data, 'Hello')
+
+ ws.close()
+ })
+
+ diagnosticsChannel.channel('undici:websocket:ping').subscribe(
+ ({ payload }) => t.same(payload, Buffer.from('Hello'))
+ )
+})
diff --git a/test/websocket/frame.js b/test/websocket/frame.js
new file mode 100644
index 0000000..b4b73b7
--- /dev/null
+++ b/test/websocket/frame.js
@@ -0,0 +1,24 @@
+'use strict'
+
+const { test } = require('tap')
+const { WebsocketFrameSend } = require('../../lib/websocket/frame')
+const { opcodes } = require('../../lib/websocket/constants')
+
+test('Writing 16-bit frame length value at correct offset when buffer has a non-zero byteOffset', (t) => {
+ /*
+ When writing 16-bit frame lengths, a `DataView` was being used without setting a `byteOffset` into the buffer:
+ i.e. `new DataView(buffer.buffer)` instead of `new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength)`.
+ Small `Buffers` returned by `allocUnsafe` are usually returned from the buffer pool, and thus have a non-zero `byteOffset`.
+ Invalid frames were therefore being returned in that case.
+ */
+ t.plan(3)
+
+ const payloadLength = 126 // 126 bytes is the smallest payload to trigger a 16-bit length field
+ const smallBuffer = Buffer.allocUnsafe(1) // make it very likely that the next buffer returned by allocUnsafe DOESN'T have a zero byteOffset
+ const payload = Buffer.allocUnsafe(payloadLength).fill(0)
+ const frame = new WebsocketFrameSend(payload).createFrame(opcodes.BINARY)
+
+ t.equal(frame[2], payloadLength >>> 8)
+ t.equal(frame[3], payloadLength & 0xff)
+ t.equal(smallBuffer.length, 1) // ensure smallBuffer can't be garbage-collected too soon
+})
diff --git a/test/websocket/opening-handshake.js b/test/websocket/opening-handshake.js
new file mode 100644
index 0000000..b9a7989
--- /dev/null
+++ b/test/websocket/opening-handshake.js
@@ -0,0 +1,215 @@
+'use strict'
+
+const { test } = require('tap')
+const { createServer } = require('http')
+const { WebSocketServer } = require('ws')
+const { WebSocket } = require('../..')
+
+test('WebSocket connecting to server that isn\'t a Websocket server', (t) => {
+ t.plan(5)
+
+ const server = createServer((req, res) => {
+ t.equal(req.headers.connection, 'upgrade')
+ t.equal(req.headers.upgrade, 'websocket')
+ t.ok(req.headers['sec-websocket-key'])
+ t.equal(req.headers['sec-websocket-version'], '13')
+
+ res.end()
+ server.unref()
+ }).listen(0, () => {
+ const ws = new WebSocket(`ws://localhost:${server.address().port}`)
+
+ // Server isn't a websocket server
+ ws.onmessage = ws.onopen = t.fail
+
+ ws.addEventListener('error', t.pass)
+ })
+
+ t.teardown(server.close.bind(server))
+})
+
+test('Open event is emitted', (t) => {
+ t.plan(1)
+
+ const server = new WebSocketServer({ port: 0 })
+
+ server.on('connection', (ws) => {
+ ws.close(1000)
+ })
+
+ t.teardown(server.close.bind(server))
+
+ const ws = new WebSocket(`ws://localhost:${server.address().port}`)
+
+ ws.onmessage = ws.onerror = t.fail
+ ws.addEventListener('open', t.pass)
+})
+
+test('Multiple protocols are joined by a comma', (t) => {
+ t.plan(1)
+
+ const server = new WebSocketServer({ port: 0 })
+
+ server.on('connection', (ws, req) => {
+ t.equal(req.headers['sec-websocket-protocol'], 'chat, echo')
+
+ ws.close(1000)
+ server.close()
+ })
+
+ t.teardown(server.close.bind(server))
+
+ const ws = new WebSocket(`ws://localhost:${server.address().port}`, ['chat', 'echo'])
+
+ ws.addEventListener('open', () => ws.close())
+})
+
+test('Server doesn\'t send Sec-WebSocket-Protocol header when protocols are used', (t) => {
+ t.plan(1)
+
+ const server = createServer((req, res) => {
+ res.statusCode = 101
+
+ req.socket.destroy()
+ }).listen(0, () => {
+ const ws = new WebSocket(`ws://localhost:${server.address().port}`, 'chat')
+
+ ws.onopen = t.fail
+
+ ws.addEventListener('error', ({ error }) => {
+ t.ok(error)
+ })
+ })
+
+ t.teardown(server.close.bind(server))
+})
+
+test('Server sends invalid Upgrade header', (t) => {
+ t.plan(1)
+
+ const server = createServer((req, res) => {
+ res.setHeader('Upgrade', 'NotWebSocket')
+ res.statusCode = 101
+
+ req.socket.destroy()
+ }).listen(0, () => {
+ const ws = new WebSocket(`ws://localhost:${server.address().port}`)
+
+ ws.onopen = t.fail
+
+ ws.addEventListener('error', ({ error }) => {
+ t.ok(error)
+ })
+ })
+
+ t.teardown(server.close.bind(server))
+})
+
+test('Server sends invalid Connection header', (t) => {
+ t.plan(1)
+
+ const server = createServer((req, res) => {
+ res.setHeader('Upgrade', 'websocket')
+ res.setHeader('Connection', 'downgrade')
+ res.statusCode = 101
+
+ req.socket.destroy()
+ }).listen(0, () => {
+ const ws = new WebSocket(`ws://localhost:${server.address().port}`)
+
+ ws.onopen = t.fail
+
+ ws.addEventListener('error', ({ error }) => {
+ t.ok(error)
+ })
+ })
+
+ t.teardown(server.close.bind(server))
+})
+
+test('Server sends invalid Sec-WebSocket-Accept header', (t) => {
+ t.plan(1)
+
+ const server = createServer((req, res) => {
+ res.setHeader('Upgrade', 'websocket')
+ res.setHeader('Connection', 'upgrade')
+ res.setHeader('Sec-WebSocket-Accept', 'abc')
+ res.statusCode = 101
+
+ req.socket.destroy()
+ }).listen(0, () => {
+ const ws = new WebSocket(`ws://localhost:${server.address().port}`)
+
+ ws.onopen = t.fail
+
+ ws.addEventListener('error', ({ error }) => {
+ t.ok(error)
+ })
+ })
+
+ t.teardown(server.close.bind(server))
+})
+
+test('Server sends invalid Sec-WebSocket-Extensions header', (t) => {
+ const uid = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
+ const { createHash } = require('crypto')
+
+ t.plan(2)
+
+ const server = createServer((req, res) => {
+ const key = req.headers['sec-websocket-key']
+ t.ok(key)
+
+ const accept = createHash('sha1').update(key + uid).digest('base64')
+
+ res.setHeader('Upgrade', 'websocket')
+ res.setHeader('Connection', 'upgrade')
+ res.setHeader('Sec-WebSocket-Accept', accept)
+ res.setHeader('Sec-WebSocket-Extensions', 'InvalidExtension')
+ res.statusCode = 101
+
+ res.end()
+ }).listen(0, () => {
+ const ws = new WebSocket(`ws://localhost:${server.address().port}`)
+
+ ws.onopen = t.fail
+
+ ws.addEventListener('error', ({ error }) => {
+ t.ok(error)
+ })
+ })
+
+ t.teardown(server.close.bind(server))
+})
+
+test('Server sends invalid Sec-WebSocket-Extensions header', (t) => {
+ const uid = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
+ const { createHash } = require('crypto')
+
+ t.plan(2)
+
+ const server = createServer((req, res) => {
+ const key = req.headers['sec-websocket-key']
+ t.ok(key)
+
+ const accept = createHash('sha1').update(key + uid).digest('base64')
+
+ res.setHeader('Upgrade', 'websocket')
+ res.setHeader('Connection', 'upgrade')
+ res.setHeader('Sec-WebSocket-Accept', accept)
+ res.setHeader('Sec-WebSocket-Protocol', 'echo') // <--
+ res.statusCode = 101
+
+ res.end()
+ }).listen(0, () => {
+ const ws = new WebSocket(`ws://localhost:${server.address().port}`, 'chat')
+
+ ws.onopen = t.fail
+
+ ws.addEventListener('error', ({ error }) => {
+ t.ok(error)
+ })
+ })
+
+ t.teardown(server.close.bind(server))
+})
diff --git a/test/websocket/ping-pong.js b/test/websocket/ping-pong.js
new file mode 100644
index 0000000..b7c4694
--- /dev/null
+++ b/test/websocket/ping-pong.js
@@ -0,0 +1,46 @@
+'use strict'
+
+const { test } = require('tap')
+const { WebSocketServer } = require('ws')
+const diagnosticsChannel = require('diagnostics_channel')
+const { WebSocket } = require('../..')
+
+test('Receives ping and parses body', (t) => {
+ t.plan(1)
+
+ const server = new WebSocketServer({ port: 0 })
+
+ server.on('connection', (ws) => {
+ ws.ping('Hello, world')
+ })
+
+ t.teardown(server.close.bind(server))
+
+ const ws = new WebSocket(`ws://localhost:${server.address().port}`)
+ ws.onerror = ws.onmessage = t.fail
+
+ diagnosticsChannel.channel('undici:websocket:ping').subscribe(({ payload }) => {
+ t.same(payload, Buffer.from('Hello, world'))
+ ws.close()
+ })
+})
+
+test('Receives pong and parses body', (t) => {
+ t.plan(1)
+
+ const server = new WebSocketServer({ port: 0 })
+
+ server.on('connection', (ws) => {
+ ws.pong('Pong')
+ })
+
+ t.teardown(server.close.bind(server))
+
+ const ws = new WebSocket(`ws://localhost:${server.address().port}`)
+ ws.onerror = ws.onmessage = t.fail
+
+ diagnosticsChannel.channel('undici:websocket:pong').subscribe(({ payload }) => {
+ t.same(payload, Buffer.from('Pong'))
+ ws.close()
+ })
+})
diff --git a/test/websocket/receive.js b/test/websocket/receive.js
new file mode 100644
index 0000000..a669022
--- /dev/null
+++ b/test/websocket/receive.js
@@ -0,0 +1,60 @@
+'use strict'
+
+const { test } = require('tap')
+const { WebSocketServer } = require('ws')
+const { WebSocket } = require('../..')
+
+test('Receiving a frame with a payload length > 2^31-1 bytes', (t) => {
+ t.plan(1)
+
+ const server = new WebSocketServer({ port: 0 })
+
+ server.on('connection', (ws) => {
+ const socket = ws._socket
+
+ socket.write(Buffer.from([0x81, 0x7F, 0xCA, 0xE5, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00]))
+ })
+
+ const ws = new WebSocket(`ws://localhost:${server.address().port}`)
+
+ t.teardown(() => {
+ ws.close()
+ server.close()
+ })
+
+ ws.onmessage = t.fail
+
+ ws.addEventListener('error', (event) => {
+ t.type(event.error, Error) // error event is emitted
+ })
+})
+
+test('Receiving an ArrayBuffer', (t) => {
+ t.plan(3)
+
+ const server = new WebSocketServer({ port: 0 })
+
+ server.on('connection', (ws) => {
+ ws.on('message', (data, isBinary) => {
+ ws.send(data, { binary: true })
+
+ ws.close(1000)
+ })
+ })
+
+ t.teardown(server.close.bind(server))
+ const ws = new WebSocket(`ws://localhost:${server.address().port}`)
+
+ ws.addEventListener('open', () => {
+ ws.binaryType = 'what'
+ t.equal(ws.binaryType, 'blob')
+
+ ws.binaryType = 'arraybuffer' // <--
+ ws.send('Hello')
+ })
+
+ ws.addEventListener('message', ({ data }) => {
+ t.type(data, ArrayBuffer)
+ t.same(Buffer.from(data), Buffer.from('Hello'))
+ })
+})
diff --git a/test/websocket/send.js b/test/websocket/send.js
new file mode 100644
index 0000000..ac295fd
--- /dev/null
+++ b/test/websocket/send.js
@@ -0,0 +1,216 @@
+'use strict'
+
+const { test } = require('tap')
+const { WebSocketServer } = require('ws')
+const { Blob } = require('buffer')
+const { WebSocket } = require('../..')
+
+// the following three tests exercise different code paths because of the three
+// different ways a payload length may be specified in a WebSocket frame
+// (https://datatracker.ietf.org/doc/html/rfc6455#section-5.2)
+
+test('Sending >= 2^16 bytes', (t) => {
+ t.plan(3)
+
+ const server = new WebSocketServer({ port: 0 })
+
+ server.on('connection', (ws) => {
+ ws.on('message', (m, isBinary) => {
+ ws.send(m, { binary: isBinary })
+ })
+ })
+
+ const payload = Buffer.allocUnsafe(2 ** 16).fill('Hello')
+
+ const ws = new WebSocket(`ws://localhost:${server.address().port}`)
+
+ ws.addEventListener('open', () => {
+ ws.send(payload)
+ })
+
+ ws.addEventListener('message', async ({ data }) => {
+ t.type(data, Blob)
+ t.equal(data.size, payload.length)
+ t.same(Buffer.from(await data.arrayBuffer()), payload)
+
+ ws.close()
+ server.close()
+ })
+})
+
+test('Sending >= 126, < 2^16 bytes', (t) => {
+ t.plan(3)
+
+ const server = new WebSocketServer({ port: 0 })
+
+ server.on('connection', (ws) => {
+ ws.on('message', (m, isBinary) => {
+ ws.send(m, { binary: isBinary })
+ })
+ })
+
+ const payload = Buffer.allocUnsafe(126).fill('Hello')
+
+ const ws = new WebSocket(`ws://localhost:${server.address().port}`)
+
+ ws.addEventListener('open', () => {
+ ws.send(payload)
+ })
+
+ ws.addEventListener('message', async ({ data }) => {
+ t.type(data, Blob)
+ t.equal(data.size, payload.length)
+ t.same(Buffer.from(await data.arrayBuffer()), payload)
+
+ ws.close()
+ server.close()
+ })
+})
+
+test('Sending < 126 bytes', (t) => {
+ t.plan(3)
+
+ const server = new WebSocketServer({ port: 0 })
+
+ server.on('connection', (ws) => {
+ ws.on('message', (m, isBinary) => {
+ ws.send(m, { binary: isBinary })
+ })
+ })
+
+ const payload = Buffer.allocUnsafe(125).fill('Hello')
+
+ const ws = new WebSocket(`ws://localhost:${server.address().port}`)
+
+ ws.addEventListener('open', () => {
+ ws.send(payload)
+ })
+
+ ws.addEventListener('message', async ({ data }) => {
+ t.type(data, Blob)
+ t.equal(data.size, payload.length)
+ t.same(Buffer.from(await data.arrayBuffer()), payload)
+
+ ws.close()
+ server.close()
+ })
+})
+
+test('Sending data after close', (t) => {
+ t.plan(2)
+
+ const server = new WebSocketServer({ port: 0 })
+
+ server.on('connection', (ws) => {
+ t.pass()
+
+ ws.on('message', t.fail)
+ })
+
+ t.teardown(server.close.bind(server))
+ const ws = new WebSocket(`ws://localhost:${server.address().port}`)
+
+ ws.addEventListener('open', () => {
+ ws.close()
+ ws.send('Some message')
+
+ t.pass()
+ })
+
+ ws.addEventListener('error', t.fail)
+})
+
+test('Sending data before connected', (t) => {
+ t.plan(2)
+
+ const server = new WebSocketServer({ port: 0 })
+
+ t.teardown(server.close.bind(server))
+ const ws = new WebSocket(`ws://localhost:${server.address().port}`)
+
+ t.throws(
+ () => ws.send('Not sent'),
+ {
+ name: 'InvalidStateError',
+ constructor: DOMException
+ }
+ )
+
+ t.equal(ws.readyState, WebSocket.CONNECTING)
+})
+
+test('Sending data to a server', (t) => {
+ t.plan(3)
+
+ t.test('Send with string', (t) => {
+ t.plan(2)
+
+ const server = new WebSocketServer({ port: 0 })
+
+ server.on('connection', (ws) => {
+ ws.on('message', (data, isBinary) => {
+ t.notOk(isBinary, 'Received text frame')
+ t.same(data, Buffer.from('message'))
+
+ ws.close(1000)
+ })
+ })
+
+ t.teardown(server.close.bind(server))
+
+ const ws = new WebSocket(`ws://localhost:${server.address().port}`)
+
+ ws.addEventListener('open', () => {
+ ws.send('message')
+ })
+ })
+
+ t.test('Send with ArrayBuffer', (t) => {
+ t.plan(2)
+
+ const message = new TextEncoder().encode('message')
+ const ab = new ArrayBuffer(7)
+ new Uint8Array(ab).set(message)
+
+ const server = new WebSocketServer({ port: 0 })
+
+ server.on('connection', (ws) => {
+ ws.on('message', (data, isBinary) => {
+ t.ok(isBinary)
+ t.same(new Uint8Array(data), message)
+
+ ws.close(1000)
+ })
+ })
+
+ t.teardown(server.close.bind(server))
+ const ws = new WebSocket(`ws://localhost:${server.address().port}`)
+
+ ws.addEventListener('open', () => {
+ ws.send(ab)
+ })
+ })
+
+ t.test('Send with Blob', (t) => {
+ t.plan(2)
+
+ const blob = new Blob(['hello'])
+ const server = new WebSocketServer({ port: 0 })
+
+ server.on('connection', (ws) => {
+ ws.on('message', (data, isBinary) => {
+ t.ok(isBinary)
+ t.same(data, Buffer.from('hello'))
+
+ ws.close(1000)
+ })
+ })
+
+ t.teardown(server.close.bind(server))
+ const ws = new WebSocket(`ws://localhost:${server.address().port}`)
+
+ ws.addEventListener('open', () => {
+ ws.send(blob)
+ })
+ })
+})
diff --git a/test/websocket/websocketinit.js b/test/websocket/websocketinit.js
new file mode 100644
index 0000000..4dda3b4
--- /dev/null
+++ b/test/websocket/websocketinit.js
@@ -0,0 +1,45 @@
+'use strict'
+
+const { test } = require('tap')
+const { WebSocketServer } = require('ws')
+const { WebSocket, Dispatcher, Agent } = require('../..')
+
+test('WebSocketInit', (t) => {
+ t.plan(2)
+
+ class WsDispatcher extends Dispatcher {
+ constructor () {
+ super()
+ this.agent = new Agent()
+ }
+
+ dispatch () {
+ t.pass()
+ return this.agent.dispatch(...arguments)
+ }
+ }
+
+ t.test('WebSocketInit as 2nd param', (t) => {
+ t.plan(1)
+
+ const server = new WebSocketServer({ port: 0 })
+
+ server.on('connection', (ws) => {
+ ws.send(Buffer.from('hello, world'))
+ })
+
+ t.teardown(server.close.bind(server))
+
+ const ws = new WebSocket(`ws://localhost:${server.address().port}`, {
+ dispatcher: new WsDispatcher()
+ })
+
+ ws.onerror = t.fail
+
+ ws.addEventListener('message', async (event) => {
+ t.equal(await event.data.text(), 'hello, world')
+ server.close()
+ ws.close()
+ })
+ })
+})
diff --git a/test/wpt/runner/runner.mjs b/test/wpt/runner/runner.mjs
new file mode 100644
index 0000000..5bec326
--- /dev/null
+++ b/test/wpt/runner/runner.mjs
@@ -0,0 +1,356 @@
+import { EventEmitter, once } from 'node:events'
+import { isAbsolute, join, resolve } from 'node:path'
+import { existsSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs'
+import { fileURLToPath } from 'node:url'
+import { Worker } from 'node:worker_threads'
+import { colors, handlePipes, normalizeName, parseMeta, resolveStatusPath } from './util.mjs'
+
+const basePath = fileURLToPath(join(import.meta.url, '../..'))
+const testPath = join(basePath, 'tests')
+const statusPath = join(basePath, 'status')
+
+// https://github.com/web-platform-tests/wpt/blob/b24eedd/resources/testharness.js#L3705
+function sanitizeUnpairedSurrogates (str) {
+ return str.replace(
+ /([\ud800-\udbff]+)(?![\udc00-\udfff])|(^|[^\ud800-\udbff])([\udc00-\udfff]+)/g,
+ function (_, low, prefix, high) {
+ let output = prefix || '' // Prefix may be undefined
+ const string = low || high // Only one of these alternates can match
+ for (let i = 0; i < string.length; i++) {
+ output += codeUnitStr(string[i])
+ }
+ return output
+ })
+}
+
+function codeUnitStr (char) {
+ return 'U+' + char.charCodeAt(0).toString(16)
+}
+
+export class WPTRunner extends EventEmitter {
+ /** @type {string} */
+ #folderName
+
+ /** @type {string} */
+ #folderPath
+
+ /** @type {string[]} */
+ #files = []
+
+ /** @type {string[]} */
+ #initScripts = []
+
+ /** @type {string} */
+ #url
+
+ /** @type {import('../../status/fetch.status.json')} */
+ #status
+
+ /** Tests that have expectedly failed mapped by file name */
+ #statusOutput = {}
+
+ #uncaughtExceptions = []
+
+ /** @type {boolean} */
+ #appendReport
+
+ /** @type {string} */
+ #reportPath
+
+ #stats = {
+ completed: 0,
+ failed: 0,
+ success: 0,
+ expectedFailures: 0,
+ skipped: 0
+ }
+
+ constructor (folder, url, { appendReport = false, reportPath } = {}) {
+ super()
+
+ this.#folderName = folder
+ this.#folderPath = join(testPath, folder)
+ this.#files.push(
+ ...WPTRunner.walk(
+ this.#folderPath,
+ (file) => file.endsWith('.any.js')
+ )
+ )
+
+ if (appendReport) {
+ if (!reportPath) {
+ throw new TypeError('reportPath must be provided when appendReport is true')
+ }
+ if (!existsSync(reportPath)) {
+ throw new TypeError('reportPath is invalid')
+ }
+ }
+
+ this.#appendReport = appendReport
+ this.#reportPath = reportPath
+
+ this.#status = JSON.parse(readFileSync(join(statusPath, `${folder}.status.json`)))
+ this.#url = url
+
+ if (this.#files.length === 0) {
+ queueMicrotask(() => {
+ this.emit('completion')
+ })
+ }
+
+ this.once('completion', () => {
+ for (const { error, test } of this.#uncaughtExceptions) {
+ console.log(colors(`Uncaught exception in "${test}":`, 'red'))
+ console.log(colors(`${error.stack}`, 'red'))
+ console.log('='.repeat(96))
+ }
+ })
+ }
+
+ static walk (dir, fn) {
+ const ini = new Set(readdirSync(dir))
+ const files = new Set()
+
+ while (ini.size !== 0) {
+ for (const d of ini) {
+ const path = resolve(dir, d)
+ ini.delete(d) // remove from set
+ const stats = statSync(path)
+
+ if (stats.isDirectory()) {
+ for (const f of readdirSync(path)) {
+ ini.add(resolve(path, f))
+ }
+ } else if (stats.isFile() && fn(d)) {
+ files.add(path)
+ }
+ }
+ }
+
+ return [...files].sort()
+ }
+
+ async run () {
+ const workerPath = fileURLToPath(join(import.meta.url, '../worker.mjs'))
+ /** @type {Set<Worker>} */
+ const activeWorkers = new Set()
+ let finishedFiles = 1
+ let total = this.#files.length
+
+ const files = this.#files.map((test) => {
+ const code = test.includes('.sub.')
+ ? handlePipes(readFileSync(test, 'utf-8'), this.#url)
+ : readFileSync(test, 'utf-8')
+ const meta = this.resolveMeta(code, test)
+
+ if (meta.variant.length) {
+ total += meta.variant.length - 1
+ }
+
+ return [test, code, meta]
+ })
+
+ console.log('='.repeat(96))
+
+ for (const [test, code, meta] of files) {
+ console.log(`Started ${test}`)
+
+ const status = resolveStatusPath(test, this.#status)
+
+ if (status.file.skip || status.topLevel.skip) {
+ this.#stats.skipped += 1
+
+ console.log(colors(`[${finishedFiles}/${total}] SKIPPED - ${test}`, 'yellow'))
+ console.log('='.repeat(96))
+
+ finishedFiles++
+ continue
+ }
+
+ const start = performance.now()
+
+ for (const variant of meta.variant.length ? meta.variant : ['']) {
+ const url = new URL(this.#url)
+ if (variant) {
+ url.search = variant
+ }
+ const worker = new Worker(workerPath, {
+ workerData: {
+ // Code to load before the test harness and tests.
+ initScripts: this.#initScripts,
+ // The test file.
+ test: code,
+ // Parsed META tag information
+ meta,
+ url: url.href,
+ path: test
+ }
+ })
+
+ let result, report
+ if (this.#appendReport) {
+ report = JSON.parse(readFileSync(this.#reportPath))
+
+ const fileUrl = new URL(`/${this.#folderName}${test.slice(this.#folderPath.length)}`, 'http://wpt')
+ fileUrl.pathname = fileUrl.pathname.replace(/\.js$/, '.html')
+ fileUrl.search = variant
+
+ result = {
+ test: fileUrl.href.slice(fileUrl.origin.length),
+ subtests: [],
+ status: 'OK'
+ }
+ report.results.push(result)
+ }
+
+ activeWorkers.add(worker)
+ // These values come directly from the web-platform-tests
+ const timeout = meta.timeout === 'long' ? 60_000 : 10_000
+
+ worker.on('message', (message) => {
+ if (message.type === 'result') {
+ this.handleIndividualTestCompletion(message, status, test, meta, result)
+ } else if (message.type === 'completion') {
+ this.handleTestCompletion(worker)
+ } else if (message.type === 'error') {
+ this.#uncaughtExceptions.push({ error: message.error, test })
+ this.#stats.failed += 1
+ this.#stats.success -= 1
+ }
+ })
+
+ try {
+ await once(worker, 'exit', {
+ signal: AbortSignal.timeout(timeout)
+ })
+
+ console.log(colors(`[${finishedFiles}/${total}] PASSED - ${test}`, 'green'))
+ if (variant) console.log('Variant:', variant)
+ console.log(`Test took ${(performance.now() - start).toFixed(2)}ms`)
+ console.log('='.repeat(96))
+ } catch (e) {
+ console.log(`${test} timed out after ${timeout}ms`)
+ } finally {
+ if (result?.subtests.length > 0) {
+ writeFileSync(this.#reportPath, JSON.stringify(report))
+ }
+
+ finishedFiles++
+ activeWorkers.delete(worker)
+ }
+ }
+ }
+
+ this.handleRunnerCompletion()
+ }
+
+ /**
+ * Called after a test has succeeded or failed.
+ */
+ handleIndividualTestCompletion (message, status, path, meta, wptResult) {
+ const { file, topLevel } = status
+
+ if (message.type === 'result') {
+ this.#stats.completed += 1
+
+ if (message.result.status === 1) {
+ this.#stats.failed += 1
+
+ wptResult?.subtests.push({
+ status: 'FAIL',
+ name: sanitizeUnpairedSurrogates(message.result.name),
+ message: sanitizeUnpairedSurrogates(message.result.message)
+ })
+
+ const name = normalizeName(message.result.name)
+
+ if (file.flaky?.includes(name)) {
+ this.#stats.expectedFailures += 1
+ } else if (file.allowUnexpectedFailures || topLevel.allowUnexpectedFailures || file.fail?.includes(name)) {
+ if (!file.allowUnexpectedFailures && !topLevel.allowUnexpectedFailures) {
+ if (Array.isArray(file.fail)) {
+ this.#statusOutput[path] ??= []
+ this.#statusOutput[path].push(name)
+ }
+ }
+
+ this.#stats.expectedFailures += 1
+ } else {
+ process.exitCode = 1
+ console.error(message.result)
+ }
+ } else {
+ wptResult?.subtests.push({
+ status: 'PASS',
+ name: sanitizeUnpairedSurrogates(message.result.name)
+ })
+ this.#stats.success += 1
+ }
+ }
+ }
+
+ /**
+ * Called after all the tests in a worker are completed.
+ * @param {Worker} worker
+ */
+ handleTestCompletion (worker) {
+ worker.terminate()
+ }
+
+ /**
+ * Called after every test has completed.
+ */
+ handleRunnerCompletion () {
+ console.log(this.#statusOutput) // tests that failed
+
+ this.emit('completion')
+ const { completed, failed, success, expectedFailures, skipped } = this.#stats
+ console.log(
+ `[${this.#folderName}]: ` +
+ `Completed: ${completed}, failed: ${failed}, success: ${success}, ` +
+ `expected failures: ${expectedFailures}, ` +
+ `unexpected failures: ${failed - expectedFailures}, ` +
+ `skipped: ${skipped}`
+ )
+
+ process.exit(0)
+ }
+
+ addInitScript (code) {
+ this.#initScripts.push(code)
+ }
+
+ /**
+ * Parses META tags and resolves any script file paths.
+ * @param {string} code
+ * @param {string} path The absolute path of the test
+ */
+ resolveMeta (code, path) {
+ const meta = parseMeta(code)
+ const scripts = meta.scripts.map((filePath) => {
+ let content = ''
+
+ if (filePath === '/resources/WebIDLParser.js') {
+ // See https://github.com/web-platform-tests/wpt/pull/731
+ return readFileSync(join(testPath, '/resources/webidl2/lib/webidl2.js'), 'utf-8')
+ } else if (isAbsolute(filePath)) {
+ content = readFileSync(join(testPath, filePath), 'utf-8')
+ } else {
+ content = readFileSync(resolve(path, '..', filePath), 'utf-8')
+ }
+
+ // If the file has any built-in pipes.
+ if (filePath.includes('.sub.')) {
+ content = handlePipes(content, this.#url)
+ }
+
+ return content
+ })
+
+ return {
+ ...meta,
+ resourcePaths: meta.scripts,
+ scripts
+ }
+ }
+}
diff --git a/test/wpt/runner/util.mjs b/test/wpt/runner/util.mjs
new file mode 100644
index 0000000..ec284df
--- /dev/null
+++ b/test/wpt/runner/util.mjs
@@ -0,0 +1,172 @@
+import assert from 'node:assert'
+import { exit } from 'node:process'
+import { inspect } from 'node:util'
+import tty from 'node:tty'
+import { sep } from 'node:path'
+
+/**
+ * Parse the `Meta:` tags sometimes included in tests.
+ * These can include resources to inject, how long it should
+ * take to timeout, and which globals to expose.
+ * @example
+ * // META: timeout=long
+ * // META: global=window,worker
+ * // META: script=/common/utils.js
+ * // META: script=/common/get-host-info.sub.js
+ * // META: script=../request/request-error.js
+ * @see https://nodejs.org/api/readline.html#readline_example_read_file_stream_line_by_line
+ * @param {string} fileContents
+ */
+export function parseMeta (fileContents) {
+ const lines = fileContents.split(/\r?\n/g)
+
+ const meta = {
+ /** @type {string|null} */
+ timeout: null,
+ /** @type {string[]} */
+ global: [],
+ /** @type {string[]} */
+ scripts: [],
+ /** @type {string[]} */
+ variant: []
+ }
+
+ for (const line of lines) {
+ if (!line.startsWith('// META: ')) {
+ break
+ }
+
+ const groups = /^\/\/ META: (?<type>.*?)=(?<match>.*)$/.exec(line)?.groups
+
+ if (!groups) {
+ console.log(`Failed to parse META tag: ${line}`)
+ exit(1)
+ }
+
+ switch (groups.type) {
+ case 'variant':
+ meta[groups.type].push(groups.match)
+ break
+ case 'title':
+ case 'timeout': {
+ meta[groups.type] = groups.match
+ break
+ }
+ case 'global': {
+ // window,worker -> ['window', 'worker']
+ meta.global.push(...groups.match.split(','))
+ break
+ }
+ case 'script': {
+ // A relative or absolute file path to the resources
+ // needed for the current test.
+ meta.scripts.push(groups.match)
+ break
+ }
+ default: {
+ console.log(`Unknown META tag: ${groups.type}`)
+ exit(1)
+ }
+ }
+ }
+
+ return meta
+}
+
+/**
+ * @param {string} sub
+ */
+function parseSubBlock (sub) {
+ const subName = sub.includes('[') ? sub.slice(0, sub.indexOf('[')) : sub
+ const options = sub.matchAll(/\[(.*?)\]/gm)
+
+ return {
+ sub: subName,
+ options: [...options].map(match => match[1])
+ }
+}
+
+/**
+ * @see https://web-platform-tests.org/writing-tests/server-pipes.html?highlight=sub#built-in-pipes
+ * @param {string} code
+ * @param {string} url
+ */
+export function handlePipes (code, url) {
+ const server = new URL(url)
+
+ // "Substitutions are marked in a file using a block delimited by
+ // {{ and }}. Inside the block the following variables are available:"
+ return code.replace(/{{(.*?)}}/gm, (_, match) => {
+ const { sub } = parseSubBlock(match)
+
+ switch (sub) {
+ // "The host name of the server excluding any subdomain part."
+ // eslint-disable-next-line no-fallthrough
+ case 'host':
+ // "The domain name of a particular subdomain e.g.
+ // {{domains[www]}} for the www subdomain."
+ // eslint-disable-next-line no-fallthrough
+ case 'domains':
+ // "The domain name of a particular subdomain for a particular host.
+ // The first key may be empty (designating the “default†host) or
+ // the value alt; i.e., {{hosts[alt][]}} (designating the alternate
+ // host)."
+ // eslint-disable-next-line no-fallthrough
+ case 'hosts': {
+ return 'localhost'
+ }
+ // "The port number of servers, by protocol e.g. {{ports[http][0]}}
+ // for the first (and, depending on setup, possibly only) http server"
+ case 'ports': {
+ return server.port
+ }
+ default: {
+ throw new TypeError(`Unknown substitute "${sub}".`)
+ }
+ }
+ })
+}
+
+/**
+ * Some test names may contain characters that JSON cannot handle.
+ * @param {string} name
+ */
+export function normalizeName (name) {
+ return name.replace(/(\v)/g, (_, match) => {
+ switch (inspect(match)) {
+ case '\'\\x0B\'': return '\\x0B'
+ default: return match
+ }
+ })
+}
+
+export function colors (str, color) {
+ assert(Object.hasOwn(inspect.colors, color), `Missing color ${color}`)
+
+ if (!tty.WriteStream.prototype.hasColors()) {
+ return str
+ }
+
+ const [start, end] = inspect.colors[color]
+
+ return `\u001b[${start}m${str}\u001b[${end}m`
+}
+
+/** @param {string} path */
+export function resolveStatusPath (path, status) {
+ const paths = path
+ .slice(process.cwd().length + sep.length)
+ .split(sep)
+ .slice(3) // [test, wpt, tests, fetch, b, c.js] -> [fetch, b, c.js]
+
+ // skip the first folder name
+ for (let i = 1; i < paths.length - 1; i++) {
+ status = status[paths[i]]
+
+ if (!status) {
+ break
+ }
+ }
+
+ return { topLevel: status ?? {}, file: status?.[paths.at(-1)] ?? {} }
+}
diff --git a/test/wpt/runner/worker.mjs b/test/wpt/runner/worker.mjs
new file mode 100644
index 0000000..90bfcf6
--- /dev/null
+++ b/test/wpt/runner/worker.mjs
@@ -0,0 +1,164 @@
+import buffer from 'node:buffer'
+import { readFileSync } from 'node:fs'
+import { join } from 'node:path'
+import { setFlagsFromString } from 'node:v8'
+import { runInNewContext, runInThisContext } from 'node:vm'
+import { parentPort, workerData } from 'node:worker_threads'
+import {
+ fetch, File, FileReader, FormData, Headers, Request, Response, setGlobalOrigin
+} from '../../../index.js'
+import { CloseEvent } from '../../../lib/websocket/events.js'
+import { WebSocket } from '../../../lib/websocket/websocket.js'
+import { Cache } from '../../../lib/cache/cache.js'
+import { CacheStorage } from '../../../lib/cache/cachestorage.js'
+import { kConstruct } from '../../../lib/cache/symbols.js'
+
+const { initScripts, meta, test, url, path } = workerData
+
+process.on('uncaughtException', (err) => {
+ parentPort.postMessage({
+ type: 'error',
+ error: {
+ message: err.message,
+ name: err.name,
+ stack: err.stack
+ }
+ })
+})
+
+const basePath = join(process.cwd(), 'test/wpt/tests')
+const urlPath = path.slice(basePath.length)
+
+const globalPropertyDescriptors = {
+ writable: true,
+ enumerable: false,
+ configurable: true
+}
+
+Object.defineProperties(globalThis, {
+ fetch: {
+ ...globalPropertyDescriptors,
+ enumerable: true,
+ value: fetch
+ },
+ File: {
+ ...globalPropertyDescriptors,
+ value: buffer.File ?? File
+ },
+ FormData: {
+ ...globalPropertyDescriptors,
+ value: FormData
+ },
+ Headers: {
+ ...globalPropertyDescriptors,
+ value: Headers
+ },
+ Request: {
+ ...globalPropertyDescriptors,
+ value: Request
+ },
+ Response: {
+ ...globalPropertyDescriptors,
+ value: Response
+ },
+ FileReader: {
+ ...globalPropertyDescriptors,
+ value: FileReader
+ },
+ WebSocket: {
+ ...globalPropertyDescriptors,
+ value: WebSocket
+ },
+ CloseEvent: {
+ ...globalPropertyDescriptors,
+ value: CloseEvent
+ },
+ Blob: {
+ ...globalPropertyDescriptors,
+ // See https://github.com/nodejs/node/pull/45659
+ value: buffer.Blob
+ },
+ caches: {
+ ...globalPropertyDescriptors,
+ value: new CacheStorage(kConstruct)
+ },
+ Cache: {
+ ...globalPropertyDescriptors,
+ value: Cache
+ },
+ CacheStorage: {
+ ...globalPropertyDescriptors,
+ value: CacheStorage
+ }
+})
+
+// self is required by testharness
+// GLOBAL is required by self
+runInThisContext(`
+ globalThis.self = globalThis
+ globalThis.GLOBAL = {
+ isWorker () {
+ return false
+ },
+ isShadowRealm () {
+ return false
+ },
+ isWindow () {
+ return false
+ }
+ }
+ globalThis.window = globalThis
+ globalThis.location = new URL('${urlPath.replace(/\\/g, '/')}', '${url}')
+ globalThis.Window = Object.getPrototypeOf(globalThis).constructor
+`)
+
+if (meta.title) {
+ runInThisContext(`globalThis.META_TITLE = "${meta.title}"`)
+}
+
+const harness = readFileSync(join(basePath, '/resources/testharness.js'), 'utf-8')
+runInThisContext(harness)
+
+// add_*_callback comes from testharness
+// stolen from node's wpt test runner
+// eslint-disable-next-line no-undef
+add_result_callback((result) => {
+ parentPort.postMessage({
+ type: 'result',
+ result: {
+ status: result.status,
+ name: result.name,
+ message: result.message,
+ stack: result.stack
+ }
+ })
+})
+
+// eslint-disable-next-line no-undef
+add_completion_callback((_, status) => {
+ parentPort.postMessage({
+ type: 'completion',
+ status
+ })
+})
+
+setGlobalOrigin(globalThis.location)
+
+// Inject any script the user provided before
+// running the tests.
+for (const initScript of initScripts) {
+ runInThisContext(initScript)
+}
+
+// Inject any files from the META tags
+for (const script of meta.scripts) {
+ runInThisContext(script)
+}
+
+// A few tests require gc, which can't be passed to a Worker.
+// see https://github.com/nodejs/node/issues/16595#issuecomment-340288680
+setFlagsFromString('--expose-gc')
+globalThis.gc = runInNewContext('gc')
+
+// Finally, run the test.
+runInThisContext(test)
diff --git a/test/wpt/server/routes/network-partition-key.mjs b/test/wpt/server/routes/network-partition-key.mjs
new file mode 100644
index 0000000..f1203f7
--- /dev/null
+++ b/test/wpt/server/routes/network-partition-key.mjs
@@ -0,0 +1,111 @@
+const stash = new Map()
+
+/**
+ * @see https://github.com/web-platform-tests/wpt/blob/master/fetch/connection-pool/resources/network-partition-key.py
+ * @param {Parameters<import('http').RequestListener>[0]} req
+ * @param {Parameters<import('http').RequestListener>[1]} res
+ * @param {URL} url
+ */
+export function route (req, res, { searchParams, port }) {
+ res.setHeader('Cache-Control', 'no-store')
+
+ const dispatch = searchParams.get('dispatch')
+ const uuid = searchParams.get('uuid')
+ const partitionId = searchParams.get('partition_id')
+
+ if (!uuid || !dispatch || !partitionId) {
+ res.statusCode = 404
+ res.end('Invalid query parameters')
+ return
+ }
+
+ let testFailed = false
+ let requestCount = 0
+ let connectionCount = 0
+
+ if (searchParams.get('nocheck_partition') !== 'True') {
+ const addressKey = `${req.socket.localAddress}|${port}`
+ const serverState = stash.get(uuid) ?? {
+ testFailed: false,
+ requestCount: 0,
+ connectionCount: 0
+ }
+
+ stash.delete(uuid)
+ requestCount = serverState.requestCount + 1
+ serverState.requestCount = requestCount
+
+ if (Object.hasOwn(serverState, addressKey)) {
+ if (serverState[addressKey] !== partitionId) {
+ serverState.testFailed = true
+ }
+ } else {
+ connectionCount = serverState.connectionCount + 1
+ serverState.connectionCount = connectionCount
+ }
+
+ serverState[addressKey] = partitionId
+ testFailed = serverState.testFailed
+ stash.set(uuid, serverState)
+ }
+
+ const origin = req.headers.origin
+ if (origin) {
+ res.setHeader('Access-Control-Allow-Origin', origin)
+ res.setHeader('Access-Control-Allow-Credentials', 'true')
+ }
+
+ if (req.method === 'OPTIONS') {
+ return handlePreflight(req, res)
+ }
+
+ if (dispatch === 'fetch_file') {
+ res.end()
+ return
+ }
+
+ if (dispatch === 'check_partition') {
+ const status = searchParams.get('status') ?? 200
+
+ if (testFailed) {
+ res.statusCode = status
+ res.end('Multiple partition IDs used on a socket')
+ return
+ }
+
+ let body = 'ok'
+ if (searchParams.get('addcounter')) {
+ body += `. Request was sent ${requestCount} times. ${connectionCount} connections were created.`
+ res.statusCode = status
+ res.end(body)
+ return
+ }
+ }
+
+ if (dispatch === 'clean_up') {
+ stash.delete(uuid)
+ res.statusCode = 200
+ if (testFailed) {
+ res.end('Test failed, but cleanup completed.')
+ } else {
+ res.end('cleanup complete')
+ }
+
+ return
+ }
+
+ res.statusCode = 404
+ res.end('Unrecognized dispatch parameter: ' + dispatch)
+}
+
+/**
+ * @param {Parameters<import('http').RequestListener>[0]} req
+ * @param {Parameters<import('http').RequestListener>[1]} res
+ */
+function handlePreflight (req, res) {
+ res.statusCode = 200
+ res.setHeader('Access-Control-Allow-Methods', 'GET')
+ res.setHeader('Access-Control-Allow-Headers', 'header-to-force-cors')
+ res.setHeader('Access-Control-Max-Age', '86400')
+ res.end('Preflight request')
+}
diff --git a/test/wpt/server/routes/redirect.mjs b/test/wpt/server/routes/redirect.mjs
new file mode 100644
index 0000000..46770cf
--- /dev/null
+++ b/test/wpt/server/routes/redirect.mjs
@@ -0,0 +1,104 @@
+import { setTimeout } from 'timers/promises'
+
+const stash = new Map()
+
+/**
+ * @see https://github.com/web-platform-tests/wpt/blob/master/fetch/connection-pool/resources/network-partition-key.py
+ * @param {Parameters<import('http').RequestListener>[0]} req
+ * @param {Parameters<import('http').RequestListener>[1]} res
+ * @param {URL} fullUrl
+ */
+export async function route (req, res, fullUrl) {
+ const { searchParams } = fullUrl
+
+ let stashedData = { count: 0, preflight: 0 }
+ let status = 302
+ res.setHeader('Content-Type', 'text/plain')
+ res.setHeader('Cache-Control', 'no-cache')
+ res.setHeader('Pragma', 'no-cache')
+
+ if (Object.hasOwn(req.headers, 'origin')) {
+ res.setHeader('Access-Control-Allow-Origin', req.headers.origin ?? '')
+ res.setHeader('Access-Control-Allow-Credentials', 'true')
+ } else {
+ res.setHeader('Access-Control-Allow-Origin', '*')
+ }
+
+ let token = null
+ if (searchParams.has('token')) {
+ token = searchParams.get('token')
+ const data = stash.get(token)
+ stash.delete(token)
+ if (data) {
+ stashedData = data
+ }
+ }
+
+ if (req.method === 'OPTIONS') {
+ if (searchParams.has('allow_headers')) {
+ res.setHeader('Access-Control-Allow-Headers', searchParams.get('allow_headers'))
+ }
+
+ stashedData.preflight = '1'
+
+ if (!searchParams.has('redirect_preflight')) {
+ if (token) {
+ stash.set(searchParams.get('token'), stashedData)
+ }
+
+ res.statusCode = 200
+ res.end('')
+ return
+ }
+ }
+
+ if (searchParams.has('redirect_status')) {
+ status = parseInt(searchParams.get('redirect_status'))
+ }
+
+ stashedData.count += 1
+
+ if (searchParams.has('location')) {
+ let url = decodeURIComponent(searchParams.get('location'))
+
+ if (!searchParams.has('simple')) {
+ const scheme = new URL(url, fullUrl).protocol
+
+ if (scheme === 'http:' || scheme === 'https:') {
+ url += url.includes('?') ? '&' : '?'
+
+ for (const [key, value] of searchParams) {
+ url += '&' + encodeURIComponent(key) + '=' + encodeURIComponent(value)
+ }
+
+ url += '&count=' + stashedData.count
+ }
+ }
+
+ res.setHeader('location', url)
+ }
+
+ if (searchParams.has('redirect_referrerpolicy')) {
+ res.setHeader('Referrer-Policy', searchParams.get('redirect_referrerpolicy'))
+ }
+
+ if (searchParams.has('delay')) {
+ await setTimeout(parseFloat(searchParams.get('delay') ?? 0))
+ }
+
+ if (token) {
+ stash.set(searchParams.get('token'), stashedData)
+
+ if (searchParams.has('max_count')) {
+ const maxCount = parseInt(searchParams.get('max_count'))
+
+ if (stashedData.count > maxCount) {
+ res.end((stashedData.count - 1).toString())
+ return
+ }
+ }
+ }
+
+ res.statusCode = status
+ res.end('')
+}
diff --git a/test/wpt/server/server.mjs b/test/wpt/server/server.mjs
new file mode 100644
index 0000000..82b9080
--- /dev/null
+++ b/test/wpt/server/server.mjs
@@ -0,0 +1,397 @@
+import { once } from 'node:events'
+import { createServer } from 'node:http'
+import { join } from 'node:path'
+import process from 'node:process'
+import { fileURLToPath } from 'node:url'
+import { createReadStream, readFileSync, existsSync } from 'node:fs'
+import { setTimeout as sleep } from 'node:timers/promises'
+import { route as networkPartitionRoute } from './routes/network-partition-key.mjs'
+import { route as redirectRoute } from './routes/redirect.mjs'
+
+const tests = fileURLToPath(join(import.meta.url, '../../tests'))
+
+// https://web-platform-tests.org/tools/wptserve/docs/stash.html
+class Stash extends Map {
+ take (key) {
+ if (this.has(key)) {
+ const value = this.get(key)
+
+ this.delete(key)
+ return value.value
+ }
+ }
+
+ put (key, value, path) {
+ this.set(key, { value, path })
+ }
+}
+
+const stash = new Stash()
+
+const server = createServer(async (req, res) => {
+ const fullUrl = new URL(req.url, `http://localhost:${server.address().port}`)
+
+ switch (fullUrl.pathname) {
+ case '/service-workers/cache-storage/resources/blank.html': {
+ res.setHeader('content-type', 'text/html')
+ // fall through
+ }
+ case '/service-workers/cache-storage/resources/simple.txt':
+ case '/fetch/content-encoding/resources/foo.octetstream.gz':
+ case '/fetch/content-encoding/resources/foo.text.gz':
+ case '/fetch/api/resources/cors-top.txt':
+ case '/fetch/api/resources/top.txt':
+ case '/mimesniff/mime-types/resources/generated-mime-types.json':
+ case '/mimesniff/mime-types/resources/mime-types.json':
+ case '/interfaces/dom.idl':
+ case '/interfaces/url.idl':
+ case '/interfaces/html.idl':
+ case '/interfaces/fetch.idl':
+ case '/interfaces/FileAPI.idl':
+ case '/interfaces/websockets.idl':
+ case '/interfaces/referrer-policy.idl':
+ case '/xhr/resources/utf16-bom.json':
+ case '/fetch/data-urls/resources/base64.json':
+ case '/fetch/data-urls/resources/data-urls.json':
+ case '/fetch/api/resources/empty.txt':
+ case '/fetch/api/resources/data.json': {
+ // If this specific resources requires custom headers
+ const customHeadersPath = join(tests, fullUrl.pathname + '.headers')
+ if (existsSync(customHeadersPath)) {
+ const headers = readFileSync(customHeadersPath, 'utf-8')
+ .trim()
+ .split(/\r?\n/g)
+ .map((h) => h.split(': '))
+
+ for (const [key, value] of headers) {
+ if (!key || !value) {
+ console.warn(`Skipping ${key}:${value} header pair`)
+ continue
+ }
+ res.setHeader(key, value)
+ }
+ }
+
+ // https://github.com/web-platform-tests/wpt/blob/6ae3f702a332e8399fab778c831db6b7dca3f1c6/fetch/api/resources/data.json
+ return createReadStream(join(tests, fullUrl.pathname))
+ .on('end', () => res.end())
+ .pipe(res)
+ }
+ case '/fetch/api/resources/trickle.py': {
+ // Note: python's time.sleep(...) takes seconds, while setTimeout
+ // takes ms.
+ const delay = parseFloat(fullUrl.searchParams.get('ms') ?? 500)
+ const count = parseInt(fullUrl.searchParams.get('count') ?? 50)
+
+ // eslint-disable-next-line no-unused-vars
+ for await (const chunk of req); // read request body
+
+ await sleep(delay)
+
+ if (!fullUrl.searchParams.has('notype')) {
+ res.setHeader('Content-type', 'text/plain')
+ }
+
+ res.statusCode = 200
+ await sleep(delay)
+
+ for (let i = 0; i < count; i++) {
+ res.write('TEST_TRICKLE\n')
+ await sleep(delay)
+ }
+
+ res.end()
+ break
+ }
+ case '/fetch/api/resources/infinite-slow-response.py': {
+ // https://github.com/web-platform-tests/wpt/blob/master/fetch/api/resources/infinite-slow-response.py
+ const stateKey = fullUrl.searchParams.get('stateKey') ?? ''
+ const abortKey = fullUrl.searchParams.get('abortKey') ?? ''
+
+ if (stateKey) {
+ stash.put(stateKey, 'open', fullUrl.pathname)
+ }
+
+ res.setHeader('Content-Type', 'text/plain')
+ res.statusCode = 200
+
+ res.write('.'.repeat(2048))
+
+ while (true) {
+ if (!res.write('.')) {
+ break
+ } else if (abortKey && stash.take(abortKey)) {
+ break
+ }
+
+ await sleep(100)
+ }
+
+ if (stateKey) {
+ stash.put(stateKey, 'closed', fullUrl.pathname)
+ }
+
+ res.end()
+ return
+ }
+ case '/fetch/api/resources/stash-take.py': {
+ // https://github.com/web-platform-tests/wpt/blob/6ae3f702a332e8399fab778c831db6b7dca3f1c6/fetch/api/resources/stash-take.py
+
+ const key = fullUrl.searchParams.get('key')
+ res.setHeader('Access-Control-Allow-Origin', '*')
+
+ const took = stash.take(key, fullUrl.pathname) ?? null
+
+ res.write(JSON.stringify(took))
+ return res.end()
+ }
+ case '/fetch/api/resources/echo-content.py': {
+ res.setHeader('X-Request-Method', req.method)
+ res.setHeader('X-Request-Content-Length', req.headers['content-length'] ?? 'NO')
+ res.setHeader('X-Request-Content-Type', req.headers['content-type'] ?? 'NO')
+ res.setHeader('Content-Type', 'text/plain')
+
+ for await (const chunk of req) {
+ res.write(chunk)
+ }
+
+ res.end()
+ break
+ }
+ case '/fetch/api/resources/status.py': {
+ const code = parseInt(fullUrl.searchParams.get('code') ?? 200)
+ const text = fullUrl.searchParams.get('text') ?? 'OMG'
+ const content = fullUrl.searchParams.get('content') ?? ''
+ const type = fullUrl.searchParams.get('type') ?? ''
+ res.statusCode = code
+ res.statusMessage = text
+ res.setHeader('Content-Type', type)
+ res.setHeader('X-Request-Method', req.method)
+ res.end(content)
+ break
+ }
+ case '/fetch/api/resources/inspect-headers.py': {
+ const query = fullUrl.searchParams
+ const checkedHeaders = query.get('headers')
+ ?.split('|')
+ .map(h => h.toLowerCase()) ?? []
+
+ if (query.has('headers')) {
+ for (const header of checkedHeaders) {
+ if (Object.hasOwn(req.headers, header)) {
+ res.setHeader(`x-request-${header}`, req.headers[header] ?? '')
+ }
+ }
+ }
+
+ if (query.has('cors')) {
+ if (Object.hasOwn(req.headers, 'origin')) {
+ res.setHeader('Access-Control-Allow-Origin', req.headers.origin ?? '')
+ } else {
+ res.setHeader('Access-Control-Allow-Origin', '*')
+ }
+
+ res.setHeader('Access-Control-Allow-Credentials', 'true')
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, HEAD')
+ const exposedHeaders = checkedHeaders.map(h => `x-request-${h}`).join(', ')
+ res.setHeader('Access-Control-Expose-Headers', exposedHeaders)
+ if (query.has('allow_headers')) {
+ res.setHeader('Access-Control-Allow-Headers', query.get('allowed_headers'))
+ } else {
+ res.setHeader('Access-Control-Allow-Headers', Object.keys(req.headers).join(', '))
+ }
+ }
+
+ res.setHeader('content-type', 'text/plain')
+ res.end('')
+ break
+ }
+ case '/xhr/resources/parse-headers.py': {
+ if (fullUrl.searchParams.has('my-custom-header')) {
+ const val = fullUrl.searchParams.get('my-custom-header').toLowerCase()
+ // res.setHeader does validation which may prevent some tests from running.
+ res.socket.write(
+ `HTTP/1.1 200 OK\r\nmy-custom-header: ${val}\r\n\r\n`
+ )
+ }
+ res.end('')
+ break
+ }
+ case '/fetch/api/resources/bad-chunk-encoding.py': {
+ const query = fullUrl.searchParams
+
+ const delay = parseFloat(query.get('ms') ?? 1000)
+ const count = parseInt(query.get('count') ?? 50)
+ await sleep(delay)
+ res.socket.write(
+ 'HTTP/1.1 200 OK\r\ntransfer-encoding: chunked\r\n\r\n'
+ )
+ await sleep(delay)
+
+ for (let i = 0; i < count; i++) {
+ res.socket.write('a\r\nTEST_CHUNK\r\n')
+ await sleep(delay)
+ }
+
+ res.end('garbage')
+ break
+ }
+ case '/xhr/resources/headers-www-authenticate.asis':
+ case '/xhr/resources/headers-some-are-empty.asis':
+ case '/xhr/resources/headers-basic':
+ case '/xhr/resources/headers-double-empty.asis':
+ case '/xhr/resources/header-content-length-twice.asis':
+ case '/xhr/resources/header-content-length.asis': {
+ let asis = readFileSync(join(tests, fullUrl.pathname), 'utf-8')
+ asis = asis.replace(/\n/g, '\r\n')
+ asis = `${asis}\r\n`
+
+ res.socket.write(asis)
+ res.end()
+ break
+ }
+ case '/fetch/connection-pool/resources/network-partition-key.py': {
+ return networkPartitionRoute(req, res, fullUrl)
+ }
+ case '/resources/top.txt': {
+ return createReadStream(join(tests, 'fetch/api/', fullUrl.pathname))
+ .on('end', () => res.end())
+ .pipe(res)
+ }
+ case '/fetch/api/resources/redirect.py': {
+ return redirectRoute(req, res, fullUrl)
+ }
+ case '/fetch/api/resources/method.py': {
+ if (fullUrl.searchParams.has('cors')) {
+ res.setHeader('Access-Control-Allow-Origin', '*')
+ res.setHeader('Access-Control-Allow-Credentials', 'true')
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, FOO')
+ res.setHeader('Access-Control-Allow-Headers', 'x-test, x-foo')
+ res.setHeader('Access-Control-Expose-Headers', 'x-request-method')
+ }
+
+ res.setHeader('x-request-method', req.method)
+ res.setHeader('x-request-content-type', req.headers['content-type'] ?? 'NO')
+ res.setHeader('x-request-content-length', req.headers['content-length'] ?? 'NO')
+ res.setHeader('x-request-content-encoding', req.headers['content-encoding'] ?? 'NO')
+ res.setHeader('x-request-content-language', req.headers['content-language'] ?? 'NO')
+ res.setHeader('x-request-content-location', req.headers['content-location'] ?? 'NO')
+
+ for await (const chunk of req) {
+ res.write(chunk)
+ }
+
+ res.end()
+ return
+ }
+ case '/fetch/api/resources/clean-stash.py': {
+ const token = fullUrl.searchParams.get('token')
+ const took = stash.take(token)
+
+ if (took) {
+ res.end('1')
+ } else {
+ res.end('0')
+ }
+
+ break
+ }
+ case '/fetch/content-encoding/resources/bad-gzip-body.py': {
+ res.setHeader('Content-Encoding', 'gzip')
+ res.end('not actually gzip')
+ break
+ }
+ case '/fetch/api/resources/dump-authorization-header.py': {
+ res.setHeader('Content-Type', 'text/html')
+ res.setHeader('Cache-Control', 'no-cache')
+
+ if (req.headers.origin) {
+ res.setHeader('Access-Control-Allow-Origin', req.headers.origin)
+ res.setHeader('Access-Control-Allow-Credentials', 'true')
+ } else {
+ res.setHeader('Access-Control-Allow-Origin', '*')
+ }
+
+ res.setHeader('Access-Control-Allow-Headers', 'Authorization')
+ res.statusCode = 200
+
+ if (req.headers.authorization) {
+ res.end(req.headers.authorization)
+ return
+ }
+
+ res.end('none')
+ break
+ }
+ case '/xhr/resources/echo-headers.py': {
+ res.statusCode = 200
+ res.setHeader('Content-Type', 'text/plain')
+
+ // wpt runner sends this as 1 chunk
+ let body = ''
+
+ for (let i = 0; i < req.rawHeaders.length; i += 2) {
+ const key = req.rawHeaders[i]
+ const value = req.rawHeaders[i + 1]
+
+ body += `${key}: ${value}`
+ }
+
+ res.end(body)
+ break
+ }
+ case '/fetch/api/resources/authentication.py': {
+ const auth = Buffer.from(req.headers.authorization.slice('Basic '.length), 'base64')
+ const [user, password] = auth.toString().split(':')
+
+ if (user === 'user' && password === 'password') {
+ res.end('Authentication done')
+ return
+ }
+
+ const realm = fullUrl.searchParams.get('realm') ?? 'test'
+
+ res.statusCode = 401
+ res.setHeader('WWW-Authenticate', `Basic realm="${realm}"`)
+ res.end('Please login with credentials \'user\' and \'password\'')
+ return
+ }
+ case '/fetch/api/resources/redirect-empty-location.py': {
+ res.setHeader('location', '')
+ res.statusCode = 302
+ res.end('')
+ return
+ }
+ case '/service-workers/cache-storage/resources/fetch-status.py': {
+ const status = Number(fullUrl.searchParams.get('status'))
+
+ res.statusCode = status
+ res.end()
+ return
+ }
+ default: {
+ res.statusCode = 200
+ res.end(fullUrl.toString())
+ }
+ }
+}).listen(0)
+
+await once(server, 'listening')
+
+const send = (message) => {
+ if (typeof process.send === 'function') {
+ process.send(message)
+ }
+}
+
+const url = `http://localhost:${server.address().port}`
+console.log('server opened ' + url)
+send({ server: url })
+
+process.on('message', (message) => {
+ if (message === 'shutdown') {
+ server.close((err) => process.exit(err ? 1 : 0))
+ }
+})
+
+export { server }
diff --git a/test/wpt/server/websocket.mjs b/test/wpt/server/websocket.mjs
new file mode 100644
index 0000000..cc8ce78
--- /dev/null
+++ b/test/wpt/server/websocket.mjs
@@ -0,0 +1,46 @@
+import { WebSocketServer } from 'ws'
+import { server } from './server.mjs'
+
+// The file router server handles sending the url, closing,
+// and sending messages back to the main process for us.
+// The types for WebSocketServer don't include a `request`
+// event, so I'm unsure if we can stop relying on server.
+
+const wss = new WebSocketServer({
+ server,
+ handleProtocols: (protocols) => [...protocols].join(', ')
+})
+
+wss.on('connection', (ws, request) => {
+ ws.on('message', (data, isBinary) => {
+ const str = data.toString('utf-8')
+
+ if (request.url === '/receive-many-with-backpressure') {
+ setTimeout(() => {
+ ws.send(str.length.toString(), { binary: false })
+ }, 100)
+ return
+ }
+
+ if (str === 'Goodbye') {
+ // Close-server-initiated-close.any.js sends a "Goodbye" message
+ // when it wants the server to close the connection.
+ ws.close(1000)
+ return
+ }
+
+ ws.send(data, { binary: isBinary })
+ })
+
+ // Some tests, such as `Create-blocked-port.any.js` do NOT
+ // close the connection automatically.
+ const timeout = setTimeout(() => {
+ if (ws.readyState !== ws.CLOSED && ws.readyState !== ws.CLOSING) {
+ ws.close()
+ }
+ }, 2500)
+
+ ws.on('close', () => {
+ clearTimeout(timeout)
+ })
+})
diff --git a/test/wpt/start-FileAPI.mjs b/test/wpt/start-FileAPI.mjs
new file mode 100644
index 0000000..5a92ab8
--- /dev/null
+++ b/test/wpt/start-FileAPI.mjs
@@ -0,0 +1,26 @@
+import { WPTRunner } from './runner/runner.mjs'
+import { join } from 'path'
+import { fileURLToPath } from 'url'
+import { fork } from 'child_process'
+import { on } from 'events'
+
+const serverPath = fileURLToPath(join(import.meta.url, '../server/server.mjs'))
+
+const child = fork(serverPath, [], {
+ stdio: ['pipe', 'pipe', 'pipe', 'ipc']
+})
+
+child.on('exit', (code) => process.exit(code))
+
+for await (const [message] of on(child, 'message')) {
+ if (message.server) {
+ const runner = new WPTRunner('FileAPI', message.server)
+ runner.run()
+
+ runner.once('completion', () => {
+ if (child.connected) {
+ child.send('shutdown')
+ }
+ })
+ }
+}
diff --git a/test/wpt/start-cacheStorage.mjs b/test/wpt/start-cacheStorage.mjs
new file mode 100644
index 0000000..a630e05
--- /dev/null
+++ b/test/wpt/start-cacheStorage.mjs
@@ -0,0 +1,26 @@
+import { WPTRunner } from './runner/runner.mjs'
+import { join } from 'path'
+import { fileURLToPath } from 'url'
+import { fork } from 'child_process'
+import { on } from 'events'
+
+const serverPath = fileURLToPath(join(import.meta.url, '../server/server.mjs'))
+
+const child = fork(serverPath, [], {
+ stdio: ['pipe', 'pipe', 'pipe', 'ipc']
+})
+
+child.on('exit', (code) => process.exit(code))
+
+for await (const [message] of on(child, 'message')) {
+ if (message.server) {
+ const runner = new WPTRunner('service-workers/cache-storage', message.server)
+ runner.run()
+
+ runner.once('completion', () => {
+ if (child.connected) {
+ child.send('shutdown')
+ }
+ })
+ }
+}
diff --git a/test/wpt/start-fetch.mjs b/test/wpt/start-fetch.mjs
new file mode 100644
index 0000000..59c9f83
--- /dev/null
+++ b/test/wpt/start-fetch.mjs
@@ -0,0 +1,31 @@
+import { WPTRunner } from './runner/runner.mjs'
+import { join } from 'path'
+import { fileURLToPath } from 'url'
+import { fork } from 'child_process'
+import { on } from 'events'
+
+const { WPT_REPORT } = process.env
+
+const serverPath = fileURLToPath(join(import.meta.url, '../server/server.mjs'))
+
+const child = fork(serverPath, [], {
+ stdio: ['pipe', 'pipe', 'pipe', 'ipc']
+})
+
+child.on('exit', (code) => process.exit(code))
+
+for await (const [message] of on(child, 'message')) {
+ if (message.server) {
+ const runner = new WPTRunner('fetch', message.server, {
+ appendReport: !!WPT_REPORT,
+ reportPath: WPT_REPORT
+ })
+ runner.run()
+
+ runner.once('completion', () => {
+ if (child.connected) {
+ child.send('shutdown')
+ }
+ })
+ }
+}
diff --git a/test/wpt/start-mimesniff.mjs b/test/wpt/start-mimesniff.mjs
new file mode 100644
index 0000000..fbdb9bf
--- /dev/null
+++ b/test/wpt/start-mimesniff.mjs
@@ -0,0 +1,31 @@
+import { WPTRunner } from './runner/runner.mjs'
+import { join } from 'path'
+import { fileURLToPath } from 'url'
+import { fork } from 'child_process'
+import { on } from 'events'
+
+const { WPT_REPORT } = process.env
+
+const serverPath = fileURLToPath(join(import.meta.url, '../server/server.mjs'))
+
+const child = fork(serverPath, [], {
+ stdio: ['pipe', 'pipe', 'pipe', 'ipc']
+})
+
+child.on('exit', (code) => process.exit(code))
+
+for await (const [message] of on(child, 'message')) {
+ if (message.server) {
+ const runner = new WPTRunner('mimesniff', message.server, {
+ appendReport: !!WPT_REPORT,
+ reportPath: WPT_REPORT
+ })
+ runner.run()
+
+ runner.once('completion', () => {
+ if (child.connected) {
+ child.send('shutdown')
+ }
+ })
+ }
+}
diff --git a/test/wpt/start-websockets.mjs b/test/wpt/start-websockets.mjs
new file mode 100644
index 0000000..79aa297
--- /dev/null
+++ b/test/wpt/start-websockets.mjs
@@ -0,0 +1,47 @@
+import { WPTRunner } from './runner/runner.mjs'
+import { join } from 'path'
+import { fileURLToPath } from 'url'
+import { fork } from 'child_process'
+import { on } from 'events'
+
+const { WPT_REPORT } = process.env
+
+function isGlobalAvailable () {
+ if (typeof WebSocket !== 'undefined') {
+ return true
+ }
+
+ const [nodeMajor, nodeMinor] = process.versions.node.split('.').map(v => Number(v))
+
+ // TODO: keep this up to date when backports to earlier majors happen
+ return nodeMajor >= 21 || (nodeMajor === 20 && nodeMinor >= 10)
+}
+
+if (process.env.CI) {
+ // TODO(@KhafraDev): figure out *why* these tests are flaky in the CI.
+ // process.exit(0)
+}
+
+const serverPath = fileURLToPath(join(import.meta.url, '../server/websocket.mjs'))
+
+const child = fork(serverPath, [], {
+ stdio: ['pipe', 'pipe', 'pipe', 'ipc']
+})
+
+child.on('exit', (code) => process.exit(code))
+
+for await (const [message] of on(child, 'message')) {
+ if (message.server) {
+ const runner = new WPTRunner('websockets', message.server, {
+ appendReport: !!WPT_REPORT && isGlobalAvailable(),
+ reportPath: WPT_REPORT
+ })
+ runner.run()
+
+ runner.once('completion', () => {
+ if (child.connected) {
+ child.send('shutdown')
+ }
+ })
+ }
+}
diff --git a/test/wpt/start-xhr.mjs b/test/wpt/start-xhr.mjs
new file mode 100644
index 0000000..08f82eb
--- /dev/null
+++ b/test/wpt/start-xhr.mjs
@@ -0,0 +1,12 @@
+import { WPTRunner } from './runner/runner.mjs'
+import { once } from 'events'
+
+const { WPT_REPORT } = process.env
+
+const runner = new WPTRunner('xhr/formdata', 'http://localhost:3333', {
+ appendReport: !!WPT_REPORT,
+ reportPath: WPT_REPORT
+})
+runner.run()
+
+await once(runner, 'completion')
diff --git a/test/wpt/status/FileAPI.status.json b/test/wpt/status/FileAPI.status.json
new file mode 100644
index 0000000..c64d255
--- /dev/null
+++ b/test/wpt/status/FileAPI.status.json
@@ -0,0 +1,75 @@
+{
+ "file": {
+ "File-constructor.any.js": {
+ "flaky": [
+ "Using type in File constructor: nonparsable"
+ ]
+ }
+ },
+ "blob": {
+ "Blob-constructor.any.js": {
+ "skip": true
+ },
+ "Blob-stream.any.js": {
+ "fail": [
+ "Reading Blob.stream() with BYOB reader"
+ ]
+ }
+ },
+ "url": {
+ "url-with-xhr.any.js": {
+ "skip": true
+ },
+ "url-with-fetch.any.js": {
+ "note": "needs investigation",
+ "fail": [
+ "Only exact matches should revoke URLs, using fetch",
+ "Revoke blob URL after creating Request, will fetch",
+ "Revoke blob URL after creating Request, then clone Request, will fetch"
+ ]
+ },
+ "url-format.any.js": {
+ "fail": [
+ "Origin of Blob URL matches our origin",
+ "Blob URL parses correctly",
+ "Origin of Blob URL matches our origin for Files"
+ ]
+ }
+ },
+ "reading-data-section": {
+ "filereader_result.any.js": {
+ "note": "has to do with html microtask queue being different than queueMicrotask",
+ "skip": true
+ },
+ "filereader_events.any.js": {
+ "note": "has to do with html microtask queue being different than queueMicrotask",
+ "fail": [
+ "events are dispatched in the correct order for an empty blob",
+ "events are dispatched in the correct order for a non-empty blob"
+ ]
+ }
+ },
+ "idlharness.any.js": {
+ "note": "These flaky tests only fail in < node v19; add in a way to mark them as such eventually",
+ "flaky": [
+ "Blob interface: attribute size",
+ "Blob interface: attribute type",
+ "Blob interface: operation slice(optional long long, optional long long, optional DOMString)",
+ "Blob interface: operation stream()",
+ "Blob interface: operation text()",
+ "Blob interface: operation arrayBuffer()",
+ "URL interface: operation createObjectURL((Blob or MediaSource))",
+ "URL interface: operation revokeObjectURL(DOMString)"
+ ],
+ "fail": [
+ "FileList interface: existence and properties of interface object",
+ "FileList interface object length",
+ "FileList interface object name",
+ "FileList interface: existence and properties of interface prototype object",
+ "FileList interface: existence and properties of interface prototype object's \"constructor\" property",
+ "FileList interface: existence and properties of interface prototype object's @@unscopables property",
+ "FileList interface: operation item(unsigned long)",
+ "FileList interface: attribute length"
+ ]
+ }
+}
diff --git a/test/wpt/status/fetch.status.json b/test/wpt/status/fetch.status.json
new file mode 100644
index 0000000..5910bf3
--- /dev/null
+++ b/test/wpt/status/fetch.status.json
@@ -0,0 +1,457 @@
+{
+ "api": {
+ "abort": {
+ "general.any.js": {
+ "note": "TODO(@KhafraDev): Clone aborts with original controller can probably be fixed",
+ "fail": [
+ "Already aborted signal rejects immediately",
+ "Underlying connection is closed when aborting after receiving response - no-cors",
+ "Stream errors once aborted. Underlying connection closed.",
+ "Readable stream synchronously cancels with AbortError if aborted before reading",
+ "Clone aborts with original controller"
+ ]
+ },
+ "cache.https.any.js": {
+ "note": "undici doesn't implement http caching",
+ "skip": true
+ }
+ },
+ "basic": {
+ "conditional-get.any.js": {
+ "fail": [
+ "Testing conditional GET with ETags"
+ ]
+ },
+ "header-value-combining.any.js": {
+ "fail": [
+ "response.headers.get('content-length') expects 0, 0",
+ "response.headers.get('foo-test') expects 1, 2, 3",
+ "response.headers.get('heya') expects , \\x0B\f, 1, , , 2"
+ ],
+ "flaky": [
+ "response.headers.get('content-length') expects 0",
+ "response.headers.get('double-trouble') expects , ",
+ "response.headers.get('www-authenticate') expects 1, 2, 3, 4"
+ ]
+ },
+ "integrity.sub.any.js": {
+ "fail": [
+ "Empty string integrity for opaque response"
+ ]
+ },
+ "keepalive.any.js": {
+ "note": "document is not defined",
+ "skip": true
+ },
+ "mode-no-cors.sub.any.js": {
+ "note": "undici doesn't implement CORs",
+ "skip": true
+ },
+ "mode-same-origin.any.js": {
+ "note": "undici doesn't respect RequestInit.mode",
+ "skip": true
+ },
+ "referrer.any.js": {
+ "fail": [
+ "origin-when-cross-origin policy on a cross-origin URL",
+ "origin-when-cross-origin policy on a cross-origin URL after same-origin redirection",
+ "origin-when-cross-origin policy on a same-origin URL after cross-origin redirection",
+ "origin-when-cross-origin policy on a same-origin URL"
+ ]
+ },
+ "request-forbidden-headers.any.js": {
+ "note": "undici doesn't filter headers",
+ "skip": true
+ },
+ "request-headers.any.js": {
+ "fail": [
+ "Fetch with Chicken",
+ "Fetch with Chicken with body",
+ "Fetch with TacO and mode \"same-origin\" needs an Origin header",
+ "Fetch with TacO and mode \"cors\" needs an Origin header"
+ ]
+ },
+ "request-referrer.any.js": {
+ "note": "TODO(@KhafraDev): url referrer test could probably be fixed",
+ "fail": [
+ "about:client referrer",
+ "url referrer"
+ ]
+ },
+ "request-upload.any.js": {
+ "fail": [
+ "Fetch with POST with text body on 421 response should be retried once on new connection."
+ ]
+ },
+ "request-upload.h2.any.js": {
+ "note": "undici doesn't support http/2",
+ "skip": true
+ },
+ "status.h2.any.js": {
+ "note": "undici doesn't support http/2",
+ "skip": true
+ },
+ "stream-safe-creation.any.js": {
+ "note": "tests are very finnicky",
+ "fail": [
+ "throwing Object.prototype.type accessor should not affect stream creation by 'fetch'",
+ "Object.prototype.type accessor returning invalid value should not affect stream creation by 'fetch'",
+ "throwing Object.prototype.highWaterMark accessor should not affect stream creation by 'fetch'",
+ "Object.prototype.highWaterMark accessor returning invalid value should not affect stream creation by 'fetch'"
+ ]
+ }
+ },
+ "body": {
+ "mime-type.any.js": {
+ "note": "fails on all platforms, https://wpt.fyi/results/fetch/api/body/mime-type.any.html?label=master&label=experimental&product=chrome&product=firefox&product=safari&product=node.js&product=deno&aligned",
+ "fail": [
+ "Response: Extract a MIME type with clone"
+ ]
+ }
+ },
+ "cors": {
+ "note": "undici doesn't implement CORs",
+ "skip": true
+ },
+ "credentials": {
+ "authentication-redirection.any.js": {
+ "note": "connects to https server",
+ "fail": [
+ "getAuthorizationHeaderValue - cross origin redirection",
+ "getAuthorizationHeaderValue - same origin redirection"
+ ]
+ },
+ "cookies.any.js": {
+ "fail": [
+ "Include mode: 1 cookie",
+ "Include mode: 2 cookies",
+ "Same-origin mode: 1 cookie",
+ "Same-origin mode: 2 cookies"
+ ]
+ }
+ },
+ "fetch-later": {
+ "note": "this is not part of the spec, only a proposal",
+ "skip": true
+ },
+ "headers": {
+ "header-setcookie.any.js": {
+ "note": "undici doesn't filter headers",
+ "fail": [
+ "Set-Cookie is a forbidden response header"
+ ]
+ },
+ "header-values-normalize.any.js": {
+ "note": "TODO(@KhafraDev): https://github.com/nodejs/undici/issues/1680",
+ "fail": [
+ "XMLHttpRequest with value %00",
+ "XMLHttpRequest with value %01",
+ "XMLHttpRequest with value %02",
+ "XMLHttpRequest with value %03",
+ "XMLHttpRequest with value %04",
+ "XMLHttpRequest with value %05",
+ "XMLHttpRequest with value %06",
+ "XMLHttpRequest with value %07",
+ "XMLHttpRequest with value %08",
+ "XMLHttpRequest with value %09",
+ "XMLHttpRequest with value %0A",
+ "XMLHttpRequest with value %0D",
+ "XMLHttpRequest with value %0E",
+ "XMLHttpRequest with value %0F",
+ "XMLHttpRequest with value %10",
+ "XMLHttpRequest with value %11",
+ "XMLHttpRequest with value %12",
+ "XMLHttpRequest with value %13",
+ "XMLHttpRequest with value %14",
+ "XMLHttpRequest with value %15",
+ "XMLHttpRequest with value %16",
+ "XMLHttpRequest with value %17",
+ "XMLHttpRequest with value %18",
+ "XMLHttpRequest with value %19",
+ "XMLHttpRequest with value %1A",
+ "XMLHttpRequest with value %1B",
+ "XMLHttpRequest with value %1C",
+ "XMLHttpRequest with value %1D",
+ "XMLHttpRequest with value %1E",
+ "XMLHttpRequest with value %1F",
+ "XMLHttpRequest with value %20",
+ "fetch() with value %01",
+ "fetch() with value %02",
+ "fetch() with value %03",
+ "fetch() with value %04",
+ "fetch() with value %05",
+ "fetch() with value %06",
+ "fetch() with value %07",
+ "fetch() with value %08",
+ "fetch() with value %0E",
+ "fetch() with value %0F",
+ "fetch() with value %10",
+ "fetch() with value %11",
+ "fetch() with value %12",
+ "fetch() with value %13",
+ "fetch() with value %14",
+ "fetch() with value %15",
+ "fetch() with value %16",
+ "fetch() with value %17",
+ "fetch() with value %18",
+ "fetch() with value %19",
+ "fetch() with value %1A",
+ "fetch() with value %1B",
+ "fetch() with value %1C",
+ "fetch() with value %1D",
+ "fetch() with value %1E",
+ "fetch() with value %1F"
+ ]
+ },
+ "header-values.any.js": {
+ "fail": [
+ "XMLHttpRequest with value x%00x needs to throw",
+ "XMLHttpRequest with value x%0Ax needs to throw",
+ "XMLHttpRequest with value x%0Dx needs to throw",
+ "XMLHttpRequest with all valid values",
+ "fetch() with all valid values"
+ ]
+ },
+ "headers-no-cors.any.js": {
+ "note": "undici doesn't implement CORs",
+ "skip": true
+ }
+ },
+ "redirect": {
+ "redirect-empty-location.any.js": {
+ "note": "undici handles redirect: manual differently than browsers",
+ "fail": [
+ "redirect response with empty Location, manual mode"
+ ]
+ },
+ "redirect-keepalive.any.js": {
+ "note": "document is not defined",
+ "skip": true
+ },
+ "redirect-location-escape.tentative.any.js": {
+ "note": "TODO(@KhafraDev): crashes runner",
+ "skip": true
+ },
+ "redirect-location.any.js": {
+ "note": "undici handles redirect: manual differently than browsers",
+ "fail": [
+ "Redirect 301 in \"manual\" mode without location",
+ "Redirect 301 in \"manual\" mode with invalid location",
+ "Redirect 301 in \"manual\" mode with data location",
+ "Redirect 302 in \"manual\" mode without location",
+ "Redirect 302 in \"manual\" mode with invalid location",
+ "Redirect 302 in \"manual\" mode with data location",
+ "Redirect 303 in \"manual\" mode without location",
+ "Redirect 303 in \"manual\" mode with invalid location",
+ "Redirect 303 in \"manual\" mode with data location",
+ "Redirect 307 in \"manual\" mode without location",
+ "Redirect 307 in \"manual\" mode with invalid location",
+ "Redirect 307 in \"manual\" mode with data location",
+ "Redirect 308 in \"manual\" mode without location",
+ "Redirect 308 in \"manual\" mode with invalid location",
+ "Redirect 308 in \"manual\" mode with data location",
+ "Redirect 301 in \"manual\" mode with valid location",
+ "Redirect 302 in \"manual\" mode with valid location",
+ "Redirect 303 in \"manual\" mode with valid location",
+ "Redirect 307 in \"manual\" mode with valid location",
+ "Redirect 308 in \"manual\" mode with valid location"
+ ]
+ },
+ "redirect-method.any.js": {
+ "fail": [
+ "Redirect 303 with TESTING"
+ ]
+ },
+ "redirect-mode.any.js": {
+ "note": "mode isn't respected",
+ "skip": true
+ },
+ "redirect-origin.any.js": {
+ "note": "TODO(@KhafraDev): investigate",
+ "skip": true
+ },
+ "redirect-referrer-override.any.js": {
+ "note": "TODO(@KhafraDev): investigate",
+ "skip": true
+ },
+ "redirect-referrer.any.js": {
+ "note": "TODO(@KhafraDev): investigate",
+ "skip": true
+ },
+ "redirect-upload.h2.any.js": {
+ "note": "undici doesn't support http/2",
+ "skip": true
+ }
+ },
+ "request": {
+ "request-cache-default-conditional.any.js": {
+ "note": "undici doesn't implement an http cache",
+ "skip": true
+ },
+ "request-cache-default.any.js": {
+ "note": "undici doesn't implement an http cache",
+ "skip": true
+ },
+ "request-cache-force-cache.any.js": {
+ "note": "undici doesn't implement an http cache",
+ "skip": true
+ },
+ "request-cache-no-cache.any.js": {
+ "note": "undici doesn't implement an http cache",
+ "skip": true
+ },
+ "request-cache-no-store.any.js": {
+ "note": "undici doesn't implement an http cache",
+ "skip": true
+ },
+ "request-cache-only-if-cached.any.js": {
+ "note": "undici doesn't implement an http cache",
+ "skip": true
+ },
+ "request-cache-reload.any.js": {
+ "note": "undici doesn't implement an http cache",
+ "skip": true
+ },
+ "request-consume-empty.any.js": {
+ "note": "the semantics about this test are being discussed - https://github.com/web-platform-tests/wpt/pull/3950",
+ "fail": [
+ "Consume empty FormData request body as text"
+ ]
+ },
+ "request-disturbed.any.js": {
+ "note": "this test fails in all other platforms - https://wpt.fyi/results/fetch/api/request/request-disturbed.any.html?label=master&label=experimental&product=chrome&product=firefox&product=safari&product=deno&aligned&view=subtest",
+ "fail": [
+ "Input request used for creating new request became disturbed even if body is not used"
+ ]
+ },
+ "request-headers.any.js": {
+ "note": "undici doesn't filter headers",
+ "fail": [
+ "Adding invalid request header \"Accept-Charset: KO\"",
+ "Adding invalid request header \"accept-charset: KO\"",
+ "Adding invalid request header \"ACCEPT-ENCODING: KO\"",
+ "Adding invalid request header \"Accept-Encoding: KO\"",
+ "Adding invalid request header \"Access-Control-Request-Headers: KO\"",
+ "Adding invalid request header \"Access-Control-Request-Method: KO\"",
+ "Adding invalid request header \"Access-Control-Request-Private-Network: KO\"",
+ "Adding invalid request header \"Connection: KO\"",
+ "Adding invalid request header \"Content-Length: KO\"",
+ "Adding invalid request header \"Cookie: KO\"",
+ "Adding invalid request header \"Cookie2: KO\"",
+ "Adding invalid request header \"Date: KO\"",
+ "Adding invalid request header \"DNT: KO\"",
+ "Adding invalid request header \"Expect: KO\"",
+ "Adding invalid request header \"Host: KO\"",
+ "Adding invalid request header \"Keep-Alive: KO\"",
+ "Adding invalid request header \"Origin: KO\"",
+ "Adding invalid request header \"Referer: KO\"",
+ "Adding invalid request header \"Set-Cookie: KO\"",
+ "Adding invalid request header \"TE: KO\"",
+ "Adding invalid request header \"Trailer: KO\"",
+ "Adding invalid request header \"Transfer-Encoding: KO\"",
+ "Adding invalid request header \"Upgrade: KO\"",
+ "Adding invalid request header \"Via: KO\"",
+ "Adding invalid request header \"Proxy-: KO\"",
+ "Adding invalid request header \"proxy-a: KO\"",
+ "Adding invalid request header \"Sec-: KO\"",
+ "Adding invalid request header \"sec-b: KO\"",
+ "Adding invalid no-cors request header \"Content-Type: KO\"",
+ "Adding invalid no-cors request header \"Potato: KO\"",
+ "Adding invalid no-cors request header \"proxy: KO\"",
+ "Adding invalid no-cors request header \"proxya: KO\"",
+ "Adding invalid no-cors request header \"sec: KO\"",
+ "Adding invalid no-cors request header \"secb: KO\"",
+ "Adding invalid no-cors request header \"Empty-Value: \"",
+ "Check that request constructor is filtering headers provided as init parameter",
+ "Check that no-cors request constructor is filtering headers provided as init parameter",
+ "Check that no-cors request constructor is filtering headers provided as part of request parameter"
+ ]
+ },
+ "request-init-priority.any.js": {
+ "note": "undici doesn't implement priority hints, yet(?)",
+ "skip": true
+ }
+ },
+ "response": {
+ "response-clone.any.js": {
+ "fail": [
+ "Check response clone use structureClone for teed ReadableStreams (ArrayBufferchunk)",
+ "Check response clone use structureClone for teed ReadableStreams (DataViewchunk)"
+ ]
+ },
+ "response-consume-empty.any.js": {
+ "fail": [
+ "Consume empty FormData response body as text"
+ ]
+ },
+ "response-consume-stream.any.js": {
+ "fail": [
+ "Read blob response's body as readableStream with mode=byob",
+ "Read text response's body as readableStream with mode=byob",
+ "Read URLSearchParams response's body as readableStream with mode=byob",
+ "Read array buffer response's body as readableStream with mode=byob",
+ "Read form data response's body as readableStream with mode=byob"
+ ]
+ },
+ "response-error-from-stream.any.js": {
+ "fail": [
+ "ReadableStream start() Error propagates to Response.formData() Promise",
+ "ReadableStream pull() Error propagates to Response.formData() Promise"
+ ]
+ },
+ "response-stream-with-broken-then.any.js": {
+ "note": "this is a bug in webstreams, see https://github.com/nodejs/node/issues/46786",
+ "skip": true
+ }
+ }
+ },
+ "content-length": {
+ "api-and-duplicate-headers.any.js": {
+ "fail": [
+ "XMLHttpRequest and duplicate Content-Length/Content-Type headers",
+ "fetch() and duplicate Content-Length/Content-Type headers"
+ ]
+ }
+ },
+ "cross-origin-resource-policy": {
+ "note": "undici doesn't implement CORs",
+ "skip": true
+ },
+ "http-cache": {
+ "note": "undici doesn't implement http caching",
+ "skip": true
+ },
+ "metadata": {
+ "note": "undici doesn't respect RequestInit.mode",
+ "skip": true
+ },
+ "orb": {
+ "tentative": {
+ "note": "undici doesn't implement orb",
+ "skip": true
+ }
+ },
+ "range": {
+ "note": "undici doesn't respect range header",
+ "skip": true
+ },
+ "security": {
+ "1xx-response.any.js": {
+ "fail": [
+ "Status(100) should be ignored.",
+ "Status(101) should be accepted, with removing body.",
+ "Status(103) should be ignored.",
+ "Status(199) should be ignored."
+ ]
+ }
+ },
+ "stale-while-revalidate": {
+ "note": "undici doesn't implement http caching",
+ "skip": true
+ },
+ "idlharness.any.js": {
+ "flaky": [
+ "Window interface: operation fetch(RequestInfo, optional RequestInit)"
+ ]
+ }
+}
diff --git a/test/wpt/status/mimesniff.status.json b/test/wpt/status/mimesniff.status.json
new file mode 100644
index 0000000..ab9a3d3
--- /dev/null
+++ b/test/wpt/status/mimesniff.status.json
@@ -0,0 +1,7 @@
+{
+ "mime-types": {
+ "parsing.any.js": {
+ "allowUnexpectedFailures": true
+ }
+ }
+}
diff --git a/test/wpt/status/service-workers/cache-storage.status.json b/test/wpt/status/service-workers/cache-storage.status.json
new file mode 100644
index 0000000..09a291e
--- /dev/null
+++ b/test/wpt/status/service-workers/cache-storage.status.json
@@ -0,0 +1,24 @@
+{
+ "cache-storage": {
+ "cache-abort.https.any.js": {
+ "skip": true
+ },
+ "cache-storage-buckets.https.any.js": {
+ "skip": true,
+ "note": "navigator is not defined"
+ },
+ "cache-put.https.any.js": {
+ "note": "probably can be fixed",
+ "fail": [
+ "Cache.put with a VARY:* opaque response should not reject",
+ "Cache.put with opaque-filtered HTTP 206 response"
+ ]
+ },
+ "cache-match.https.any.js": {
+ "note": "requires https server",
+ "fail": [
+ "cors-exposed header should be stored correctly."
+ ]
+ }
+ }
+}
diff --git a/test/wpt/status/websockets.status.json b/test/wpt/status/websockets.status.json
new file mode 100644
index 0000000..68bc6e2
--- /dev/null
+++ b/test/wpt/status/websockets.status.json
@@ -0,0 +1,115 @@
+{
+ "stream": {
+ "tentative": {
+ "skip": true
+ }
+ },
+ "Create-blocked-port.any.js": {
+ "note": "TODO(@KhafraDev): investigate failure",
+ "fail": [
+ "Basic check"
+ ]
+ },
+ "Send-binary-arraybufferview-float32.any.js": {
+ "note": "TODO(@KhafraDev): investigate failure",
+ "fail": [
+ "Send binary data on a WebSocket - ArrayBufferView - Float32Array - Connection should be closed"
+ ]
+ },
+ "Send-binary-arraybufferview-float64.any.js": {
+ "note": "TODO(@KhafraDev): investigate failure",
+ "fail": [
+ "Send binary data on a WebSocket - ArrayBufferView - Float64Array - Connection should be closed"
+ ]
+ },
+ "Send-binary-arraybufferview-int16-offset.any.js": {
+ "note": "TODO(@KhafraDev): investigate failure",
+ "fail": [
+ "Send binary data on a WebSocket - ArrayBufferView - Int16Array with offset - Connection should be closed"
+ ]
+ },
+ "Send-binary-arraybufferview-int32.any.js": {
+ "note": "TODO(@KhafraDev): investigate failure",
+ "fail": [
+ "Send binary data on a WebSocket - ArrayBufferView - Int32Array - Connection should be closed"
+ ]
+ },
+ "Send-binary-arraybufferview-uint16-offset-length.any.js": {
+ "note": "TODO(@KhafraDev): investigate failure",
+ "fail": [
+ "Send binary data on a WebSocket - ArrayBufferView - Uint16Array with offset and length - Connection should be closed"
+ ]
+ },
+ "Send-binary-arraybufferview-uint32-offset.any.js": {
+ "note": "TODO(@KhafraDev): investigate failure",
+ "fail": [
+ "Send binary data on a WebSocket - ArrayBufferView - Uint32Array with offset - Connection should be closed"
+ ]
+ },
+ "basic-auth.any.js": {
+ "note": "TODO(@KhafraDev): investigate failure",
+ "fail": [
+ "HTTP basic authentication should work with WebSockets"
+ ]
+ },
+ "Create-on-worker-shutdown.any.js": {
+ "skip": true,
+ "//": "Node.js workers are different from web workers & don't work with blob: urls"
+ },
+ "Close-delayed.any.js": {
+ "skip": true
+ },
+ "bufferedAmount-unchanged-by-sync-xhr.any.js": {
+ "skip": true,
+ "//": "Node.js doesn't have XMLHttpRequest nor does this test make sense regardless"
+ },
+ "referrer.any.js": {
+ "skip": true
+ },
+ "Send-binary-blob.any.js": {
+ "flaky": [
+ "Send binary data on a WebSocket - Blob - Connection should be closed"
+ ]
+ },
+ "Send-65K-data.any.js": {
+ "flaky": [
+ "Send 65K data on a WebSocket - Connection should be closed"
+ ]
+ },
+ "Send-binary-65K-arraybuffer.any.js": {
+ "flaky": [
+ "Send 65K binary data on a WebSocket - ArrayBuffer - Connection should be closed"
+ ]
+ },
+ "Send-0byte-data.any.js": {
+ "flaky": [
+ "Send 0 byte data on a WebSocket - Connection should be closed"
+ ]
+ },
+ "send-many-64K-messages-with-backpressure.any.js": {
+ "note": "probably flaky based on other flaky tests.",
+ "flaky": [
+ "sending 50 messages of size 65536 with backpressure applied should not hang"
+ ]
+ },
+ "back-forward-cache-with-closed-websocket-connection-ccns.tentative.window.js": {
+ "skip": true,
+ "note": "browser-only test"
+ },
+ "back-forward-cache-with-closed-websocket-connection.window.js": {
+ "skip": true,
+ "note": "browser-only test"
+ },
+ "back-forward-cache-with-open-websocket-connection-ccns.tentative.window.js": {
+ "skip": true,
+ "note": "browser-only test"
+ },
+ "back-forward-cache-with-open-websocket-connection.window.js": {
+ "skip": true,
+ "note": "browser-only test"
+ },
+ "mixed-content.https.any.js": {
+ "note": "node has no concept of origin, thus there is no 'secure' or 'insecure' contexts",
+ "skip": true
+ }
+}
diff --git a/test/wpt/status/xhr/formdata.status.json b/test/wpt/status/xhr/formdata.status.json
new file mode 100644
index 0000000..0967ef4
--- /dev/null
+++ b/test/wpt/status/xhr/formdata.status.json
@@ -0,0 +1 @@
+{}
diff --git a/test/wpt/tests/.azure-pipelines.yml b/test/wpt/tests/.azure-pipelines.yml
new file mode 100644
index 0000000..75a87df
--- /dev/null
+++ b/test/wpt/tests/.azure-pipelines.yml
@@ -0,0 +1,595 @@
+# This is the configuration file for Azure Pipelines, used to run tests on
+# macOS and Windows. Documentation to help understand this setup:
+# https://docs.microsoft.com/en-us/azure/devops/pipelines/yaml-schema
+# https://docs.microsoft.com/en-us/azure/devops/pipelines/build/triggers
+# https://docs.microsoft.com/en-us/azure/devops/pipelines/process/multiple-phases
+# https://docs.microsoft.com/en-us/azure/devops/pipelines/process/templates
+# https://docs.microsoft.com/en-us/azure/devops/pipelines/process/variables
+# https://docs.microsoft.com/en-us/azure/devops/pipelines/tasks/index
+#
+# In addition to this configuration file, some setup in the Azure DevOps
+# project is required:
+# - The "Build pull requests from forks of this repository" setting must be
+# enabled: https://docs.microsoft.com/en-us/azure/devops/pipelines/repos/github#validate-contributions-from-forks
+
+trigger:
+# These are all the branches referenced in the jobs that follow.
+- epochs/daily
+- epochs/three_hourly
+- triggers/edge_stable
+- triggers/edge_dev
+- triggers/edge_canary
+- triggers/safari_stable
+- triggers/safari_preview
+- triggers/wktr_preview
+
+# Set safaridriver_diagnose to true to enable safaridriver diagnostics. The
+# logs won't appear in `./wpt run` output but will be uploaded as an artifact.
+variables:
+ safaridriver_diagnose: false
+
+jobs:
+# The affected tests jobs are unconditional for speed, as most PRs have one or
+# more affected tests: https://github.com/web-platform-tests/wpt/issues/13936.
+- job: affected_safari_preview
+ displayName: 'affected tests: Safari Technology Preview'
+ condition: eq(variables['Build.Reason'], 'PullRequest')
+ pool:
+ vmImage: 'macOS-13'
+ steps:
+ - task: UsePythonVersion@0
+ inputs:
+ versionSpec: '3.11'
+ - template: tools/ci/azure/affected_tests.yml
+ parameters:
+ artifactName: 'safari-preview-affected-tests'
+- template: tools/ci/azure/fyi_hook.yml
+ parameters:
+ dependsOn: affected_safari_preview
+ artifactName: safari-preview-affected-tests
+
+- job: affected_without_changes_safari_preview
+ displayName: 'affected tests without changes: Safari Technology Preview'
+ condition: eq(variables['Build.Reason'], 'PullRequest')
+ pool:
+ vmImage: 'macOS-13'
+ steps:
+ - task: UsePythonVersion@0
+ inputs:
+ versionSpec: '3.11'
+ - template: tools/ci/azure/affected_tests.yml
+ parameters:
+ checkoutCommit: 'HEAD^1'
+ affectedRange: 'HEAD@{1}'
+ artifactName: 'safari-preview-affected-tests-without-changes'
+- template: tools/ci/azure/fyi_hook.yml
+ parameters:
+ dependsOn: affected_without_changes_safari_preview
+ artifactName: safari-preview-affected-tests-without-changes
+
+# The decision jobs runs `./wpt test-jobs` to determine which jobs to run,
+# and all following jobs wait for it to finish and depend on its output.
+- job: decision
+ displayName: './wpt test-jobs'
+ condition: eq(variables['Build.Reason'], 'PullRequest')
+ pool:
+ vmImage: 'ubuntu-20.04'
+ steps:
+ - task: UsePythonVersion@0
+ inputs:
+ versionSpec: '3.11'
+ - template: tools/ci/azure/checkout.yml
+ - script: |
+ set -eux -o pipefail
+ git fetch --depth 50 --quiet origin master
+ ./wpt test-jobs | while read job; do
+ echo "$job"
+ echo "##vso[task.setvariable variable=$job;isOutput=true]true";
+ done
+ name: test_jobs
+ displayName: 'Run ./wpt test-jobs'
+
+- job: infrastructure_mac
+ displayName: 'infrastructure/ tests: macOS'
+ dependsOn: decision
+ condition: dependencies.decision.outputs['test_jobs.wptrunner_infrastructure']
+ pool:
+ vmImage: 'macOS-13'
+ steps:
+ - task: UsePythonVersion@0
+ 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
+ - template: tools/ci/azure/install_chrome.yml
+ - template: tools/ci/azure/install_firefox.yml
+ - template: tools/ci/azure/install_safari.yml
+ - template: tools/ci/azure/update_hosts.yml
+ - template: tools/ci/azure/update_manifest.yml
+ - script: |
+ set -eux -o pipefail
+ ./wpt run --yes --no-manifest-update --manifest MANIFEST.json --metadata infrastructure/metadata/ --log-mach - --log-mach-level info --log-wptreport $(Build.ArtifactStagingDirectory)/wpt_report_macos_chrome.json --channel dev chrome infrastructure/
+ condition: succeededOrFailed()
+ displayName: 'Run tests (Chrome Dev)'
+ - script: |
+ set -eux -o pipefail
+ ./wpt run --yes --no-manifest-update --manifest MANIFEST.json --metadata infrastructure/metadata/ --log-mach - --log-mach-level info --log-wptreport $(Build.ArtifactStagingDirectory)/wpt_report_macos_firefox.json --channel nightly firefox infrastructure/
+ condition: succeededOrFailed()
+ displayName: 'Run tests (Firefox Nightly)'
+ - script: |
+ set -eux -o pipefail
+ export SYSTEM_VERSION_COMPAT=0
+ ./wpt run --yes --no-manifest-update --manifest MANIFEST.json --metadata infrastructure/metadata/ --log-mach - --log-mach-level info --log-wptreport $(Build.ArtifactStagingDirectory)/wpt_report_macos_safari.json --channel preview safari infrastructure/
+ condition: succeededOrFailed()
+ displayName: 'Run tests (Safari Technology Preview)'
+ - task: PublishBuildArtifacts@1
+ condition: succeededOrFailed()
+ displayName: 'Publish results'
+ inputs:
+ artifactName: 'infrastructure-results'
+ - template: tools/ci/azure/publish_logs.yml
+ - template: tools/ci/azure/sysdiagnose.yml
+
+- job: tools_unittest_mac_py37
+ displayName: 'tools/ unittests: macOS + Python 3.7'
+ dependsOn: decision
+ condition: dependencies.decision.outputs['test_jobs.tools_unittest']
+ pool:
+ vmImage: 'macOS-13'
+ 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'
+ - template: tools/ci/azure/checkout.yml
+ - template: tools/ci/azure/tox_pytest.yml
+ parameters:
+ directory: tools/
+ toxenv: py37
+
+- job: tools_unittest_mac_py311
+ displayName: 'tools/ unittests: macOS + Python 3.11'
+ dependsOn: decision
+ condition: dependencies.decision.outputs['test_jobs.tools_unittest']
+ pool:
+ vmImage: 'macOS-13'
+ steps:
+ - task: UsePythonVersion@0
+ inputs:
+ versionSpec: '3.11'
+ - template: tools/ci/azure/checkout.yml
+ - template: tools/ci/azure/tox_pytest.yml
+ parameters:
+ directory: tools/
+ toxenv: py311
+
+- job: wptrunner_unittest_mac_py37
+ displayName: 'tools/wptrunner/ unittests: macOS + Python 3.7'
+ dependsOn: decision
+ condition: dependencies.decision.outputs['test_jobs.wptrunner_unittest']
+ pool:
+ vmImage: 'macOS-13'
+ 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'
+ - template: tools/ci/azure/checkout.yml
+ - template: tools/ci/azure/tox_pytest.yml
+ parameters:
+ directory: tools/wptrunner/
+ toxenv: py37
+
+- job: wptrunner_unittest_mac_py311
+ displayName: 'tools/wptrunner/ unittests: macOS + Python 3.11'
+ dependsOn: decision
+ condition: dependencies.decision.outputs['test_jobs.wptrunner_unittest']
+ pool:
+ vmImage: 'macOS-13'
+ steps:
+ - task: UsePythonVersion@0
+ inputs:
+ versionSpec: '3.11'
+ - template: tools/ci/azure/checkout.yml
+ - template: tools/ci/azure/tox_pytest.yml
+ parameters:
+ directory: tools/wptrunner/
+ toxenv: py311
+
+- job: wpt_integration_mac_py37
+ displayName: 'tools/wpt/ tests: macOS + Python 3.7'
+ dependsOn: decision
+ condition: dependencies.decision.outputs['test_jobs.wpt_integration']
+ pool:
+ vmImage: 'macOS-13'
+ steps:
+ # 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'
+ - template: tools/ci/azure/install_chrome.yml
+ - template: tools/ci/azure/install_firefox.yml
+ - template: tools/ci/azure/update_hosts.yml
+ - template: tools/ci/azure/update_manifest.yml
+ - template: tools/ci/azure/tox_pytest.yml
+ parameters:
+ directory: tools/wpt/
+ toxenv: py37
+
+- job: wpt_integration_mac_py311
+ displayName: 'tools/wpt/ tests: macOS + Python 3.11'
+ dependsOn: decision
+ condition: dependencies.decision.outputs['test_jobs.wpt_integration']
+ pool:
+ vmImage: 'macOS-13'
+ steps:
+ # full checkout required
+ - task: UsePythonVersion@0
+ inputs:
+ versionSpec: '3.11'
+ - template: tools/ci/azure/install_chrome.yml
+ - template: tools/ci/azure/install_firefox.yml
+ - template: tools/ci/azure/update_hosts.yml
+ - template: tools/ci/azure/update_manifest.yml
+ - template: tools/ci/azure/tox_pytest.yml
+ parameters:
+ directory: tools/wpt/
+ toxenv: py311
+
+- job: tools_unittest_win_py37
+ displayName: 'tools/ unittests: Windows + Python 3.7'
+ dependsOn: decision
+ condition: dependencies.decision.outputs['test_jobs.tools_unittest']
+ pool:
+ vmImage: 'windows-2019'
+ variables:
+ HYPOTHESIS_PROFILE: ci
+ steps:
+ - task: UsePythonVersion@0
+ inputs:
+ versionSpec: '3.7'
+ addToPath: false
+ - template: tools/ci/azure/checkout.yml
+ - template: tools/ci/azure/tox_pytest.yml
+ parameters:
+ directory: tools/
+ toxenv: py37
+
+- job: tools_unittest_win_py311
+ displayName: 'tools/ unittests: Windows + Python 3.11'
+ dependsOn: decision
+ condition: dependencies.decision.outputs['test_jobs.tools_unittest']
+ pool:
+ vmImage: 'windows-2019'
+ steps:
+ - task: UsePythonVersion@0
+ inputs:
+ versionSpec: '3.11'
+ addToPath: false
+ - template: tools/ci/azure/checkout.yml
+ - template: tools/ci/azure/tox_pytest.yml
+ parameters:
+ directory: tools/
+ toxenv: py311
+
+- job: wptrunner_unittest_win_py37
+ displayName: 'tools/wptrunner/ unittests: Windows + Python 3.7'
+ dependsOn: decision
+ condition: dependencies.decision.outputs['test_jobs.wptrunner_unittest']
+ pool:
+ vmImage: 'windows-2019'
+ steps:
+ - task: UsePythonVersion@0
+ inputs:
+ versionSpec: '3.7'
+ addToPath: false
+ - template: tools/ci/azure/checkout.yml
+ - template: tools/ci/azure/tox_pytest.yml
+ parameters:
+ directory: tools/wptrunner/
+ toxenv: py37
+
+- job: wptrunner_unittest_win_py311
+ displayName: 'tools/wptrunner/ unittests: Windows + Python 3.11'
+ dependsOn: decision
+ condition: dependencies.decision.outputs['test_jobs.wptrunner_unittest']
+ pool:
+ vmImage: 'windows-2019'
+ steps:
+ - task: UsePythonVersion@0
+ inputs:
+ versionSpec: '3.11'
+ addToPath: false
+ - template: tools/ci/azure/checkout.yml
+ - template: tools/ci/azure/tox_pytest.yml
+ parameters:
+ directory: tools/wptrunner/
+ toxenv: py311
+
+- job: wpt_integration_win_py37
+ displayName: 'tools/wpt/ tests: Windows + Python 3.7'
+ dependsOn: decision
+ condition: dependencies.decision.outputs['test_jobs.wpt_integration']
+ pool:
+ vmImage: 'windows-2019'
+ steps:
+ # full checkout required
+ - task: UsePythonVersion@0
+ inputs:
+ versionSpec: '3.7'
+ # currently just using the outdated Chrome/Firefox on the VM rather than
+ # figuring out how to install Chrome Dev channel on Windows
+ # - template: tools/ci/azure/install_chrome.yml
+ # - template: tools/ci/azure/install_firefox.yml
+ - template: tools/ci/azure/update_hosts.yml
+ - template: tools/ci/azure/update_manifest.yml
+ - template: tools/ci/azure/tox_pytest.yml
+ parameters:
+ directory: tools/wpt/
+ toxenv: py37
+
+- job: wpt_integration_win_py311
+ displayName: 'tools/wpt/ tests: Windows + Python 3.11'
+ dependsOn: decision
+ condition: dependencies.decision.outputs['test_jobs.wpt_integration']
+ pool:
+ vmImage: 'windows-2019'
+ steps:
+ # full checkout required
+ - task: UsePythonVersion@0
+ inputs:
+ versionSpec: '3.11'
+ # currently just using the outdated Chrome/Firefox on the VM rather than
+ # figuring out how to install Chrome Dev channel on Windows
+ # - template: tools/ci/azure/install_chrome.yml
+ # - template: tools/ci/azure/install_firefox.yml
+ - template: tools/ci/azure/update_hosts.yml
+ - template: tools/ci/azure/update_manifest.yml
+ - template: tools/ci/azure/tox_pytest.yml
+ parameters:
+ directory: tools/wpt/
+ toxenv: py311
+
+- job: results_edge_stable
+ displayName: 'all tests: Edge Stable'
+ condition: |
+ or(eq(variables['Build.SourceBranch'], 'refs/heads/epochs/daily'),
+ eq(variables['Build.SourceBranch'], 'refs/heads/triggers/edge_stable'),
+ and(eq(variables['Build.Reason'], 'Manual'), variables['run_all_edge_stable']))
+ strategy:
+ parallel: 8 # chosen to make runtime ~2h
+ timeoutInMinutes: 180
+ pool:
+ vmImage: 'windows-2019'
+ steps:
+ - task: UsePythonVersion@0
+ inputs:
+ 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:
+ channel: stable
+ - template: tools/ci/azure/update_hosts.yml
+ - template: tools/ci/azure/update_manifest.yml
+ - script: python ./wpt run --yes --no-manifest-update --no-restart-on-unexpected --no-fail-on-unexpected --install-fonts --this-chunk $(System.JobPositionInPhase) --total-chunks $(System.TotalJobsInPhase) --chunk-type hash --log-wptreport $(Build.ArtifactStagingDirectory)/wpt_report_$(System.JobPositionInPhase).json --log-wptscreenshot $(Build.ArtifactStagingDirectory)/wpt_screenshot_$(System.JobPositionInPhase).txt --log-mach - --log-mach-level info --channel stable edgechromium
+ displayName: 'Run tests (Edge Stable)'
+ - task: PublishBuildArtifacts@1
+ displayName: 'Publish results'
+ inputs:
+ artifactName: 'edge-stable-results'
+ - template: tools/ci/azure/publish_logs.yml
+ - template: tools/ci/azure/sysdiagnose.yml
+- template: tools/ci/azure/fyi_hook.yml
+ parameters:
+ dependsOn: results_edge_stable
+ artifactName: edge-stable-results
+
+- job: results_edge_dev
+ displayName: 'all tests: Edge Dev'
+ condition: |
+ or(eq(variables['Build.SourceBranch'], 'refs/heads/epochs/three_hourly'),
+ eq(variables['Build.SourceBranch'], 'refs/heads/triggers/edge_dev'),
+ and(eq(variables['Build.Reason'], 'Manual'), variables['run_all_edge_dev']))
+ strategy:
+ parallel: 8 # chosen to make runtime ~2h
+ timeoutInMinutes: 180
+ pool:
+ vmImage: 'windows-2019'
+ steps:
+ - task: UsePythonVersion@0
+ inputs:
+ 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:
+ channel: dev
+ - template: tools/ci/azure/update_hosts.yml
+ - template: tools/ci/azure/update_manifest.yml
+ - script: python ./wpt run --yes --no-manifest-update --no-restart-on-unexpected --no-fail-on-unexpected --install-fonts --this-chunk $(System.JobPositionInPhase) --total-chunks $(System.TotalJobsInPhase) --chunk-type hash --log-wptreport $(Build.ArtifactStagingDirectory)/wpt_report_$(System.JobPositionInPhase).json --log-wptscreenshot $(Build.ArtifactStagingDirectory)/wpt_screenshot_$(System.JobPositionInPhase).txt --log-mach - --log-mach-level info --channel dev edgechromium
+ displayName: 'Run tests (Edge Dev)'
+ - task: PublishBuildArtifacts@1
+ displayName: 'Publish results'
+ inputs:
+ artifactName: 'edge-dev-results'
+ - template: tools/ci/azure/publish_logs.yml
+ - template: tools/ci/azure/sysdiagnose.yml
+- template: tools/ci/azure/fyi_hook.yml
+ parameters:
+ dependsOn: results_edge_dev
+ artifactName: edge-dev-results
+
+- job: results_edge_canary
+ displayName: 'all tests: Edge Canary'
+ condition: |
+ or(eq(variables['Build.SourceBranch'], 'refs/heads/epochs/weekly'),
+ eq(variables['Build.SourceBranch'], 'refs/heads/triggers/edge_canary'),
+ and(eq(variables['Build.Reason'], 'Manual'), variables['run_all_edge_canary']))
+ strategy:
+ parallel: 8 # chosen to make runtime ~2h
+ timeoutInMinutes: 180
+ pool:
+ vmImage: 'windows-2019'
+ steps:
+ - task: UsePythonVersion@0
+ 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:
+ channel: canary
+ - template: tools/ci/azure/update_hosts.yml
+ - template: tools/ci/azure/update_manifest.yml
+ - script: python ./wpt run --yes --no-manifest-update --no-restart-on-unexpected --no-fail-on-unexpected --install-fonts --this-chunk $(System.JobPositionInPhase) --total-chunks $(System.TotalJobsInPhase) --chunk-type hash --log-wptreport $(Build.ArtifactStagingDirectory)/wpt_report_$(System.JobPositionInPhase).json --log-wptscreenshot $(Build.ArtifactStagingDirectory)/wpt_screenshot_$(System.JobPositionInPhase).txt --log-mach - --log-mach-level info --channel canary edgechromium
+ displayName: 'Run tests (Edge Canary)'
+ - task: PublishBuildArtifacts@1
+ displayName: 'Publish results'
+ inputs:
+ artifactName: 'edge-canary-results'
+ - template: tools/ci/azure/publish_logs.yml
+ - template: tools/ci/azure/sysdiagnose.yml
+- template: tools/ci/azure/fyi_hook.yml
+ parameters:
+ dependsOn: results_edge_canary
+ artifactName: edge-canary-results
+
+- job: results_safari
+ displayName: 'all tests: Safari'
+ condition: |
+ or(eq(variables['Build.SourceBranch'], 'refs/heads/epochs/daily'),
+ eq(variables['Build.SourceBranch'], 'refs/heads/triggers/safari_stable'),
+ and(eq(variables['Build.Reason'], 'Manual'), variables['run_all_safari']))
+ strategy:
+ parallel: 8 # chosen to make runtime ~2h
+ timeoutInMinutes: 180
+ pool:
+ vmImage: 'macOS-13'
+ steps:
+ - task: UsePythonVersion@0
+ 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
+ parameters:
+ channel: stable
+ - template: tools/ci/azure/update_hosts.yml
+ - template: tools/ci/azure/update_manifest.yml
+ - script: |
+ set -eux -o pipefail
+ export SYSTEM_VERSION_COMPAT=0
+ ./wpt run --no-manifest-update --no-restart-on-unexpected --no-fail-on-unexpected --this-chunk=$(System.JobPositionInPhase) --total-chunks=$(System.TotalJobsInPhase) --chunk-type hash --log-wptreport $(Build.ArtifactStagingDirectory)/wpt_report_$(System.JobPositionInPhase).json --log-wptscreenshot $(Build.ArtifactStagingDirectory)/wpt_screenshot_$(System.JobPositionInPhase).txt --log-mach - --log-mach-level info --channel stable --kill-safari --max-restarts 100 safari
+ displayName: 'Run tests'
+ retryCountOnTaskFailure: 2
+ - task: PublishBuildArtifacts@1
+ displayName: 'Publish results'
+ inputs:
+ artifactName: 'safari-results'
+ - template: tools/ci/azure/publish_logs.yml
+ - template: tools/ci/azure/sysdiagnose.yml
+- template: tools/ci/azure/fyi_hook.yml
+ parameters:
+ dependsOn: results_safari
+ artifactName: safari-results
+
+- job: results_safari_preview
+ displayName: 'all tests: Safari Technology Preview'
+ condition: |
+ or(eq(variables['Build.SourceBranch'], 'refs/heads/epochs/three_hourly'),
+ eq(variables['Build.SourceBranch'], 'refs/heads/triggers/safari_preview'),
+ and(eq(variables['Build.Reason'], 'Manual'), variables['run_all_safari_preview']))
+ strategy:
+ parallel: 8 # chosen to make runtime ~2h
+ timeoutInMinutes: 180
+ pool:
+ vmImage: 'macOS-13'
+ steps:
+ - task: UsePythonVersion@0
+ 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
+ - template: tools/ci/azure/update_hosts.yml
+ - template: tools/ci/azure/update_manifest.yml
+ - script: |
+ set -eux -o pipefail
+ export SYSTEM_VERSION_COMPAT=0
+ ./wpt run --no-manifest-update --no-restart-on-unexpected --no-fail-on-unexpected --this-chunk=$(System.JobPositionInPhase) --total-chunks=$(System.TotalJobsInPhase) --chunk-type hash --log-wptreport $(Build.ArtifactStagingDirectory)/wpt_report_$(System.JobPositionInPhase).json --log-wptscreenshot $(Build.ArtifactStagingDirectory)/wpt_screenshot_$(System.JobPositionInPhase).txt --log-mach - --log-mach-level info --channel preview --kill-safari --max-restarts 100 safari
+ displayName: 'Run tests'
+ retryCountOnTaskFailure: 2
+ - task: PublishBuildArtifacts@1
+ displayName: 'Publish results'
+ inputs:
+ artifactName: 'safari-preview-results'
+ - template: tools/ci/azure/publish_logs.yml
+ - template: tools/ci/azure/sysdiagnose.yml
+- template: tools/ci/azure/fyi_hook.yml
+ parameters:
+ dependsOn: results_safari_preview
+ artifactName: safari-preview-results
+
+- job: results_wktr_preview
+ displayName: 'all tests: WebKitTestRunner'
+ condition: |
+ or(eq(variables['Build.SourceBranch'], 'refs/heads/triggers/wktr_preview'),
+ and(eq(variables['Build.Reason'], 'Manual'), variables['run_all_wktr_preview']))
+ strategy:
+ parallel: 8 # chosen to make runtime ~2h
+ timeoutInMinutes: 180
+ pool:
+ vmImage: 'macOS-13'
+ steps:
+ - task: UsePythonVersion@0
+ 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
+ - template: tools/ci/azure/update_manifest.yml
+ - script: |
+ set -eux -o pipefail
+ export SYSTEM_VERSION_COMPAT=0
+ ./wpt run --no-manifest-update --no-restart-on-unexpected --no-fail-on-unexpected --this-chunk=$(System.JobPositionInPhase) --total-chunks=$(System.TotalJobsInPhase) --chunk-type hash --log-wptreport $(Build.ArtifactStagingDirectory)/wpt_report_$(System.JobPositionInPhase).json --log-wptscreenshot $(Build.ArtifactStagingDirectory)/wpt_screenshot_$(System.JobPositionInPhase).txt --log-mach - --log-mach-level info --channel experimental --install-browser --yes wktr
+ displayName: 'Run tests'
+ - task: PublishBuildArtifacts@1
+ displayName: 'Publish results'
+ inputs:
+ artifactName: 'wktr-preview-results'
+ - template: tools/ci/azure/publish_logs.yml
+ - template: tools/ci/azure/sysdiagnose.yml
+- template: tools/ci/azure/fyi_hook.yml
+ parameters:
+ dependsOn: results_wktr_preview
+ artifactName: wktr-preview-results
diff --git a/test/wpt/tests/.gitattributes b/test/wpt/tests/.gitattributes
new file mode 100644
index 0000000..5c11e4e
--- /dev/null
+++ b/test/wpt/tests/.gitattributes
@@ -0,0 +1 @@
+* -text
diff --git a/test/wpt/tests/.gitignore b/test/wpt/tests/.gitignore
new file mode 100644
index 0000000..061700a
--- /dev/null
+++ b/test/wpt/tests/.gitignore
@@ -0,0 +1,52 @@
+# Python
+*.py[co]
+.cache/
+.coverage*
+.mypy_cache/
+.pytest_cache/
+.tox/
+.virtualenv/
+_venv*/
+_virtualenv/
+
+# Node
+node_modules/
+
+# WPT repo stuff
+.wptcache/
+/MANIFEST.json
+/_certs
+/config.json
+
+# Files generated when regenerating pre-generated certs
+/tools/certs/0*.pem
+/tools/certs/index.txt*
+/tools/certs/serial*
+
+# Various OS/editor specific files
+*#
+*.orig
+*.rej
+*.svn
+*.sw[po]
+*.xcodeproj
+*Thumbs.db
+*~
+.DS_Store
+.directory*
+.idea/
+.vscode/
+\#*
+scratch
+
+# Testsuite-specific rules
+/conformance-checkers/vnu.jar
+/cors/resources/log.txt
+/css/build-temp
+/css/dist
+/css/dist_last
+/url/tools/IdnaTestV2.txt
+/webaudio/idl/*
+
+# w3c-test.org PR-branch mirroring
+/submissions/
diff --git a/test/wpt/tests/.mailmap b/test/wpt/tests/.mailmap
new file mode 100644
index 0000000..5293948
--- /dev/null
+++ b/test/wpt/tests/.mailmap
@@ -0,0 +1,9 @@
+# People who've changed name:
+
+# Sam Sneddon:
+Sam Sneddon <me@gsnedders.com>
+Sam Sneddon <me@gsnedders.com> <geoffers@gmail.com>
+
+# Theresa O'Connor:
+Theresa O'Connor <eoconnor@apple.com>
+Theresa O'Connor <hober0@gmail.com>
diff --git a/test/wpt/tests/.taskcluster.yml b/test/wpt/tests/.taskcluster.yml
new file mode 100644
index 0000000..c817999
--- /dev/null
+++ b/test/wpt/tests/.taskcluster.yml
@@ -0,0 +1,82 @@
+version: 1
+reporting: checks-v1
+policy:
+ pullRequests: public
+tasks:
+ $let:
+ 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"]'
+ then: true
+ else: false
+ else:
+ $if: 'tasks_for == "github-pull-request"'
+ then:
+ $if: 'event.action in ["opened", "reopened", "synchronize"]'
+ then: true
+ else: false
+ else: false
+ in:
+ - $if: run_task
+ then:
+ $let:
+ event_str: {$json: {$eval: event}}
+ scopes:
+ $if: 'tasks_for == "github-push"'
+ then:
+ $let:
+ branch:
+ $if: "event.ref[:11] == 'refs/heads/'"
+ then: "${event.ref[11:]}"
+ else: "${event.ref}"
+ in: "assume:repo:github.com/${event.repository.full_name}:branch:${branch}"
+ else: "assume:repo:github.com/${event.repository.full_name}:pull-request"
+ rev:
+ $if: 'tasks_for == "github-pull-request"'
+ then: "refs/pull/${event.number}/merge"
+ else: "${event.after}"
+ owner:
+ $if: 'tasks_for == "github-push"'
+ then:
+ $if: 'event.pusher.email'
+ then:
+ $if: '"@" in event.pusher.email'
+ then: ${event.pusher.email}
+ else: web-platform-tests@users.noreply.github.com
+ else: web-platform-tests@users.noreply.github.com
+ else: web-platform-tests@users.noreply.github.com
+ in:
+ created: {$fromNow: ''}
+ deadline: {$fromNow: '24 hours'}
+ provisionerId: proj-wpt
+ workerType: ci
+ metadata:
+ name: "wpt-decision-task"
+ description: "The task that creates all of the other tasks in the task graph"
+ owner: ${owner}
+ source: ${event.repository.clone_url}
+ payload:
+ image: webplatformtests/wpt:0.54
+ maxRunTime: 7200
+ artifacts:
+ public/results:
+ path: /home/test/artifacts
+ type: directory
+ command:
+ - /bin/bash
+ - --login
+ - -c
+ - set -ex;
+ ~/start.sh
+ ${event.repository.clone_url}
+ ${rev};
+ cd ~/web-platform-tests;
+ ./wpt tc-decision --tasks-path=/home/test/artifacts/tasks.json
+ features :
+ taskclusterProxy: true
+ scopes:
+ - ${scopes}
+ extra:
+ github_event: "${event_str}"
+
diff --git a/test/wpt/tests/CODEOWNERS b/test/wpt/tests/CODEOWNERS
new file mode 100644
index 0000000..140e0c6
--- /dev/null
+++ b/test/wpt/tests/CODEOWNERS
@@ -0,0 +1,6 @@
+# Require review for changes that often need an RFC
+/resources/testdriver* @web-platform-tests/wpt-core-team
+/resources/testharness* @web-platform-tests/wpt-core-team
+
+# Prevent accidentally touching tools/third_party
+/tools/third_party/ @web-platform-tests/wpt-core-team
diff --git a/test/wpt/tests/CODE_OF_CONDUCT.md b/test/wpt/tests/CODE_OF_CONDUCT.md
new file mode 100644
index 0000000..dae98ee
--- /dev/null
+++ b/test/wpt/tests/CODE_OF_CONDUCT.md
@@ -0,0 +1,138 @@
+# Code of Conduct
+
+Contact: a moderator ([see below](#moderators)), or a member of the WPT
+community that you feel you can trust.
+
+## Our Pledge
+
+We as members, contributors, and leaders pledge to make participation in our
+community a harassment-free experience for everyone, regardless of age, body
+size, visible or invisible disability, ethnicity, sex characteristics, gender
+identity and expression, level of experience, education, socio-economic status,
+nationality, personal appearance, race, religion, sexual identity and
+orientation, or any other dimension of diversity.
+
+We pledge to act and interact in ways that contribute to an open, welcoming,
+diverse, inclusive, and healthy community.
+
+## Our Standards
+
+Examples of behavior that contributes to a positive environment for our
+community include:
+
+* Demonstrating empathy and kindness toward other people
+* Being respectful of differing opinions, viewpoints, and experiences
+* Giving and gracefully accepting constructive feedback
+* Accepting responsibility and apologizing to those affected by our mistakes,
+ and learning from the experience
+* Focusing on what is best not just for us as individuals, but for the
+ overall community
+
+Examples of unacceptable behavior include:
+
+* The use of sexualized language or imagery, and sexual attention or
+ advances of any kind
+* Trolling, insulting or derogatory comments, and personal or political attacks
+* Public or private harassment
+* Publishing others' private information, such as a physical or email
+ address, without their explicit permission
+* Other conduct which could reasonably be considered inappropriate in a
+ professional setting
+
+## Enforcement Responsibilities
+
+Moderators are responsible for clarifying and enforcing our standards of
+acceptable behavior and will take appropriate and fair corrective action in
+response to any behavior that they deem inappropriate, threatening, offensive,
+or harmful.
+
+Moderators have the right and responsibility to remove, edit, or reject
+comments, commits, code, wiki edits, issues, and other contributions that are
+not aligned to this Code of Conduct, and will communicate reasons for moderation
+decisions when appropriate.
+
+Moderators are held to a higher standard than other community members. If a
+moderator creates an inappropriate situation, they should expect less leeway
+than others.
+
+## Scope
+
+This Code of Conduct applies within all community spaces, and also
+applies when an individual is officially representing the community in
+public spaces. Examples of representing our community include
+the official Matrix channel (wpt:matrix.org); GitHub repositories under
+the web-platform-tests organization; and the public-test-infra@w3.org
+mailing list.
+
+There may arise situations where both the WPT code of conduct and that of
+another organization (such as the WHATWG or W3C) may apply.
+For example, a WPT-focused meeting at
+[TPAC](https://www.w3.org/2002/09/TPOverview.html) would involve both the WPT
+code of conduct and the [W3C code of ethics and professional
+conduct](https://www.w3.org/Consortium/cepc/).
+In such situations we operate under all code of conducts involved.
+If you are placed in a situation where you feel this is inappropriate (e.g. if
+you believe the code of conducts involved contradict one another), please
+contact a [moderator](#moderators).
+
+## Enforcement
+
+Instances of abusive, harassing, or otherwise unacceptable behavior may be
+reported to the [moderators](#moderators).
+All complaints will be reviewed and investigated promptly and fairly.
+
+All moderators are obligated to respect the privacy and security of the
+reporter of any incident.
+
+Moderators will recuse themselves if they are directly involved in a report of
+a code of conduct violation.
+
+## Enforcement Guidelines
+
+Moderators will follow these Community Impact Guidelines in determining
+the consequences for any action they deem in violation of this Code of Conduct.
+These are not a consecutive set of steps; a single incident may be sufficient
+enough to proceed straight to a temporary ban, for example.
+
+### 1. Warning
+
+**Community Impact**: A violation through a single incident or series
+of actions.
+
+**Consequence**: A warning with consequences for continued behavior, providing
+clarity around the nature of the violation and an explanation of why the
+behavior was inappropriate. A public apology may be requested.
+
+### 2. Temporary Ban
+
+**Community Impact**: A serious violation of community standards, including
+sustained inappropriate behavior.
+
+**Consequence**: A temporary ban from any sort of interaction or public
+communication with the community for a specified period of time. No public or
+private interaction with the people involved, including unsolicited interaction
+with those enforcing the Code of Conduct, is allowed during this period.
+Violating these terms may lead to a permanent ban.
+
+### 3. Permanent Ban
+
+**Community Impact**: Demonstrating a pattern of violation of community
+standards, including sustained inappropriate behavior, harassment of an
+individual, or aggression toward or disparagement of classes of individuals.
+
+**Consequence**: A permanent ban from any sort of public interaction within
+the community.
+
+## Moderators
+
+This section lists the current moderators and how to reach them.
+
+* Nina Satragno - [nso@google.com](mailto:nso@google.com). Languages: English, Spanish.
+* Boaz Sender - [boaz@bocoup.com](mailto:boaz@bocoup.com). Languages: English, Hebrew.
+* Jory Burson - [jory@bocoup.education](mailto:jory@bocoup.education). Languages: English (fluent), Spanish (conversational).
+
+## Attribution
+
+This Code of Conduct is adapted from the Contributor Covenant,
+version 2.0, available at
+https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
diff --git a/test/wpt/tests/CONTRIBUTING.md b/test/wpt/tests/CONTRIBUTING.md
new file mode 100644
index 0000000..cb80d8d
--- /dev/null
+++ b/test/wpt/tests/CONTRIBUTING.md
@@ -0,0 +1,11 @@
+All contributions are licensed under the terms of the [3-Clause BSD License](LICENSE.md).
+
+Documentation
+-------------
+
+See [web-platform-tests.org](https://web-platform-tests.org/).
+
+Code of Conduct
+---------------
+
+See [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md).
diff --git a/test/wpt/tests/FileAPI/Blob-methods-from-detached-frame.html b/test/wpt/tests/FileAPI/Blob-methods-from-detached-frame.html
new file mode 100644
index 0000000..37efd5e
--- /dev/null
+++ b/test/wpt/tests/FileAPI/Blob-methods-from-detached-frame.html
@@ -0,0 +1,59 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Blob methods from detached frame work as expected</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<iframe id="emptyDocumentIframe" src="../support/empty-document.html"></iframe>
+
+<script>
+const BlobPrototypeFromDetachedFramePromise = new Promise(resolve => {
+ emptyDocumentIframe.onload = () => {
+ const BlobPrototype = emptyDocumentIframe.contentWindow.Blob.prototype;
+ emptyDocumentIframe.remove();
+ resolve(BlobPrototype);
+ };
+});
+
+const charCodeArrayToString = charCodeArray => Array.from(charCodeArray, c => String.fromCharCode(c)).join("");
+const charCodeBufferToString = charCodeBuffer => charCodeArrayToString(new Uint8Array(charCodeBuffer));
+
+promise_test(async () => {
+ const { slice } = await BlobPrototypeFromDetachedFramePromise;
+ const blob = new Blob(["foobar"]);
+
+ const slicedBlob = slice.call(blob, 1, 3);
+ assert_true(slicedBlob instanceof Blob);
+
+ assert_equals(await slicedBlob.text(), "oo");
+ assert_equals(charCodeBufferToString(await slicedBlob.arrayBuffer()), "oo");
+
+ const reader = slicedBlob.stream().getReader();
+ const { value } = await reader.read();
+ assert_equals(charCodeArrayToString(value), "oo");
+}, "slice()");
+
+promise_test(async () => {
+ const { text } = await BlobPrototypeFromDetachedFramePromise;
+ const blob = new Blob(["foo"]);
+
+ assert_equals(await text.call(blob), "foo");
+}, "text()");
+
+promise_test(async () => {
+ const { arrayBuffer } = await BlobPrototypeFromDetachedFramePromise;
+ const blob = new Blob(["bar"]);
+
+ const charCodeBuffer = await arrayBuffer.call(blob);
+ assert_equals(charCodeBufferToString(charCodeBuffer), "bar");
+}, "arrayBuffer()");
+
+promise_test(async () => {
+ const { stream } = await BlobPrototypeFromDetachedFramePromise;
+ const blob = new Blob(["baz"]);
+
+ const reader = stream.call(blob).getReader();
+ const { value } = await reader.read();
+ assert_equals(charCodeArrayToString(value), "baz");
+}, "stream()");
+</script>
diff --git a/test/wpt/tests/FileAPI/BlobURL/cross-partition.tentative.https.html b/test/wpt/tests/FileAPI/BlobURL/cross-partition.tentative.https.html
new file mode 100644
index 0000000..c75ce07
--- /dev/null
+++ b/test/wpt/tests/FileAPI/BlobURL/cross-partition.tentative.https.html
@@ -0,0 +1,276 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<!-- Pull in executor_path needed by newPopup / newIframe -->
+<script src="/html/cross-origin-embedder-policy/credentialless/resources/common.js"></script>
+<!-- Pull in importScript / newPopup / newIframe -->
+<script src="/html/anonymous-iframe/resources/common.js"></script>
+<body>
+<script>
+
+const did_revoke_response = "URL.revokeObjectURL did revoke";
+const did_not_revoke_response = "URL.revokeObjectURL did not revoke";
+
+const can_blob_url_be_revoked_js = (blob_url, response_queue_name) => `
+ async function test() {
+ if (!('revokeObjectURL' in URL)) {
+ return send("${response_queue_name}", "URL.revokeObjectURL is not exposed");
+ }
+ try {
+ var blob = await fetch("${blob_url}").then(response => response.blob());
+ await blob.text();
+ } catch {
+ return send("${response_queue_name}", "Blob URL invalid");
+ }
+ try {
+ URL.revokeObjectURL("${blob_url}");
+ } catch(e) {
+ return send("${response_queue_name}", e.toString());
+ }
+ try {
+ const blob = await fetch("${blob_url}").then(response => response.blob());
+ } catch(e) {
+ return send("${response_queue_name}", "${did_revoke_response}");
+ }
+ return send("${response_queue_name}", "${did_not_revoke_response}");
+ }
+ await test();
+`;
+
+const add_iframe_js = (iframe_origin, response_queue_uuid) => `
+ const importScript = ${importScript};
+ await importScript("/html/cross-origin-embedder-policy/credentialless" +
+ "/resources/common.js");
+ await importScript("/html/anonymous-iframe/resources/common.js");
+ await importScript("/common/utils.js");
+ await send("${response_queue_uuid}", newIframe("${iframe_origin}"));
+`;
+
+const same_site_origin = get_host_info().HTTPS_ORIGIN;
+const cross_site_origin = get_host_info().HTTPS_NOTSAMESITE_ORIGIN;
+
+async function create_test_iframes(t, response_queue_uuid) {
+
+ // Create a same-origin iframe in a cross-site popup.
+ const not_same_site_popup_uuid = newPopup(t, cross_site_origin);
+ await send(not_same_site_popup_uuid,
+ add_iframe_js(same_site_origin, response_queue_uuid));
+ const iframe_1_uuid = await receive(response_queue_uuid);
+
+ // Create a same-origin iframe in a same-site popup.
+ const same_origin_popup_uuid = newPopup(t, same_site_origin);
+ await send(same_origin_popup_uuid,
+ add_iframe_js(same_site_origin, response_queue_uuid));
+ const iframe_2_uuid = await receive(response_queue_uuid);
+
+ return [iframe_1_uuid, iframe_2_uuid];
+}
+
+promise_test(t => {
+ return new Promise(async (resolve, reject) => {
+ try {
+ const response_queue_uuid = token();
+
+ const [iframe_1_uuid, iframe_2_uuid] =
+ await create_test_iframes(t, response_queue_uuid);
+
+ const blob = new Blob(["blob data"], {type : "text/plain"});
+ const blob_url = window.URL.createObjectURL(blob);
+ t.add_cleanup(() => window.URL.revokeObjectURL(blob_url));
+
+ await send(iframe_1_uuid,
+ can_blob_url_be_revoked_js(blob_url, response_queue_uuid));
+ var response = await receive(response_queue_uuid);
+ if (response !== did_not_revoke_response) {
+ reject(`Blob URL was revoked in not-same-top-level-site iframe: ${response}`);
+ }
+
+ await send(iframe_2_uuid,
+ can_blob_url_be_revoked_js(blob_url, response_queue_uuid));
+ response = await receive(response_queue_uuid);
+ if (response !== did_revoke_response) {
+ reject(`Blob URL wasn't revoked in same-top-level-site iframe: ${response}`);
+ }
+
+ resolve();
+ } catch (e) {
+ reject(e);
+ }
+ });
+}, "Blob URL shouldn't be revocable from a cross-partition iframe");
+
+const newWorker = (origin) => {
+ const worker_token = token();
+ const worker_url = origin + executor_worker_path + `&uuid=${worker_token}`;
+ const worker = new Worker(worker_url);
+ return worker_token;
+}
+
+promise_test(t => {
+ return new Promise(async (resolve, reject) => {
+ try {
+ const response_queue_uuid = token();
+
+ const create_worker_js = (origin) => `
+ const importScript = ${importScript};
+ await importScript("/html/cross-origin-embedder-policy/credentialless" +
+ "/resources/common.js");
+ await importScript("/html/anonymous-iframe/resources/common.js");
+ await importScript("/common/utils.js");
+ const newWorker = ${newWorker};
+ await send("${response_queue_uuid}", newWorker("${origin}"));
+ `;
+
+ const [iframe_1_uuid, iframe_2_uuid] =
+ await create_test_iframes(t, response_queue_uuid);
+
+ // Create a dedicated worker in the cross-top-level-site iframe.
+ await send(iframe_1_uuid, create_worker_js(same_site_origin));
+ const worker_1_uuid = await receive(response_queue_uuid);
+
+ // Create a dedicated worker in the same-top-level-site iframe.
+ await send(iframe_2_uuid, create_worker_js(same_site_origin));
+ const worker_2_uuid = await receive(response_queue_uuid);
+
+ const blob = new Blob(["blob data"], {type : "text/plain"});
+ const blob_url = window.URL.createObjectURL(blob);
+ t.add_cleanup(() => window.URL.revokeObjectURL(blob_url));
+
+ await send(worker_1_uuid,
+ can_blob_url_be_revoked_js(blob_url, response_queue_uuid));
+ var response = await receive(response_queue_uuid);
+ if (response !== did_not_revoke_response) {
+ reject(`Blob URL was revoked in not-same-top-level-site dedicated worker: ${response}`);
+ }
+
+ await send(worker_2_uuid,
+ can_blob_url_be_revoked_js(blob_url, response_queue_uuid));
+ response = await receive(response_queue_uuid);
+ if (response !== did_revoke_response) {
+ reject(`Blob URL wasn't revoked in same-top-level-site dedicated worker: ${response}`);
+ }
+
+ resolve();
+ } catch (e) {
+ reject(e);
+ }
+ });
+}, "Blob URL shouldn't be revocable from a cross-partition dedicated worker");
+
+const newSharedWorker = (origin) => {
+ const worker_token = token();
+ const worker_url = origin + executor_worker_path + `&uuid=${worker_token}`;
+ const worker = new SharedWorker(worker_url, worker_token);
+ return worker_token;
+}
+
+promise_test(t => {
+ return new Promise(async (resolve, reject) => {
+ try {
+ const response_queue_uuid = token();
+
+ const create_worker_js = (origin) => `
+ const importScript = ${importScript};
+ await importScript("/html/cross-origin-embedder-policy/credentialless" +
+ "/resources/common.js");
+ await importScript("/html/anonymous-iframe/resources/common.js");
+ await importScript("/common/utils.js");
+ const newSharedWorker = ${newSharedWorker};
+ await send("${response_queue_uuid}", newSharedWorker("${origin}"));
+ `;
+
+ const [iframe_1_uuid, iframe_2_uuid] =
+ await create_test_iframes(t, response_queue_uuid);
+
+ // Create a shared worker in the cross-top-level-site iframe.
+ await send(iframe_1_uuid, create_worker_js(same_site_origin));
+ const worker_1_uuid = await receive(response_queue_uuid);
+
+ // Create a shared worker in the same-top-level-site iframe.
+ await send(iframe_2_uuid, create_worker_js(same_site_origin));
+ const worker_2_uuid = await receive(response_queue_uuid);
+
+ const blob = new Blob(["blob data"], {type : "text/plain"});
+ const blob_url = window.URL.createObjectURL(blob);
+ t.add_cleanup(() => window.URL.revokeObjectURL(blob_url));
+
+ await send(worker_1_uuid,
+ can_blob_url_be_revoked_js(blob_url, response_queue_uuid));
+ var response = await receive(response_queue_uuid);
+ if (response !== did_not_revoke_response) {
+ reject(`Blob URL was revoked in not-same-top-level-site shared worker: ${response}`);
+ }
+
+ await send(worker_2_uuid,
+ can_blob_url_be_revoked_js(blob_url, response_queue_uuid));
+ response = await receive(response_queue_uuid);
+ if (response !== did_revoke_response) {
+ reject(`Blob URL wasn't revoked in same-top-level-site shared worker: ${response}`);
+ }
+
+ resolve();
+ } catch (e) {
+ reject(e);
+ }
+ });
+}, "Blob URL shouldn't be revocable from a cross-partition shared worker");
+
+const newServiceWorker = async (origin) => {
+ const worker_token = token();
+ const worker_url = origin + executor_service_worker_path +
+ `&uuid=${worker_token}`;
+ const worker_url_path = executor_service_worker_path.substring(0,
+ executor_service_worker_path.lastIndexOf('/'));
+ const scope = worker_url_path + "/not-used/";
+ const reg = await navigator.serviceWorker.register(worker_url,
+ {'scope': scope});
+ return worker_token;
+}
+
+promise_test(t => {
+ return new Promise(async (resolve, reject) => {
+ try {
+ const response_queue_uuid = token();
+
+ const create_worker_js = (origin) => `
+ const importScript = ${importScript};
+ await importScript("/html/cross-origin-embedder-policy/credentialless" +
+ "/resources/common.js");
+ await importScript("/html/anonymous-iframe/resources/common.js");
+ await importScript("/common/utils.js");
+ const newServiceWorker = ${newServiceWorker};
+ await send("${response_queue_uuid}", await newServiceWorker("${origin}"));
+ `;
+
+ const [iframe_1_uuid, iframe_2_uuid] =
+ await create_test_iframes(t, response_queue_uuid);
+
+ // Create a service worker in either iframe.
+ await send(iframe_1_uuid, create_worker_js(same_site_origin));
+ var worker_1_uuid = await receive(response_queue_uuid);
+ t.add_cleanup(() =>
+ send(worker_1_uuid, "self.registration.unregister();"));
+
+ const blob = new Blob(["blob data"], {type : "text/plain"});
+ const blob_url = window.URL.createObjectURL(blob);
+ t.add_cleanup(() => window.URL.revokeObjectURL(blob_url));
+
+ await send(worker_1_uuid,
+ can_blob_url_be_revoked_js(blob_url, response_queue_uuid));
+ const response = await receive(response_queue_uuid);
+ if (response !== "URL.revokeObjectURL is not exposed") {
+ reject(`URL.revokeObjectURL is exposed in a Service Worker context: ${response}`);
+ }
+ resolve();
+ } catch (e) {
+ reject(e);
+ }
+ });
+}, "Blob URL shouldn't be revocable from a service worker");
+</script>
+</body>
diff --git a/test/wpt/tests/FileAPI/BlobURL/support/file_test2.txt b/test/wpt/tests/FileAPI/BlobURL/support/file_test2.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/wpt/tests/FileAPI/BlobURL/support/file_test2.txt
diff --git a/test/wpt/tests/FileAPI/BlobURL/test2-manual.html b/test/wpt/tests/FileAPI/BlobURL/test2-manual.html
new file mode 100644
index 0000000..07fb27e
--- /dev/null
+++ b/test/wpt/tests/FileAPI/BlobURL/test2-manual.html
@@ -0,0 +1,62 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Blob and File reference URL Test(2)</title>
+ <link rel=help href="http://dev.w3.org/2006/webapi/FileAPI/#convenienceAPI">
+ <link rel=author title="Breezewish" href="mailto:me@breeswish.org">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+</head>
+<body>
+ <form name="upload">
+ <input type="file" id="fileChooser"><br><input type="button" id="start" value="start">
+ </form>
+
+ <div>
+ <p>Test steps:</p>
+ <ol>
+ <li>Download the <a href="support/file_test2.txt">file</a>.</li>
+ <li>Select the file in the file inputbox.</li>
+ <li>Delete the file.</li>
+ <li>Click the 'start' button.</li>
+ </ol>
+ </div>
+
+ <div id="log"></div>
+
+ <script>
+
+ var fileChooser = document.querySelector('#fileChooser');
+
+ setup({explicit_done: true});
+ setup({explicit_timeout: true});
+
+ on_event(document.querySelector('#start'), 'click', function() {
+
+ async_test(function(t) {
+
+ var url = URL.createObjectURL(fileChooser.files[0]);
+
+ var xhr = new XMLHttpRequest();
+ xhr.open('GET', url, true);
+ xhr.onreadystatechange = t.step_func(function() {
+ switch (xhr.readyState) {
+ case xhr.DONE:
+ assert_equals(xhr.status, 500, 'status code should be 500.');
+ t.done();
+ return;
+ }
+ });
+
+ xhr.send();
+
+ }, 'Check whether the browser response 500 in XHR if the selected file which File/Blob URL refered is not found');
+
+ done();
+
+ });
+
+ </script>
+</body>
+</html>
diff --git a/test/wpt/tests/FileAPI/FileReader/progress_event_bubbles_cancelable.html b/test/wpt/tests/FileAPI/FileReader/progress_event_bubbles_cancelable.html
new file mode 100644
index 0000000..6a03243
--- /dev/null
+++ b/test/wpt/tests/FileAPI/FileReader/progress_event_bubbles_cancelable.html
@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>File API Test: Progress Event - bubbles, cancelable</title>
+<link rel="author" title="Intel" href="http://www.intel.com">
+<link rel="help" href="http://www.w3.org/TR/FileAPI/#events">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+ async_test(function(){
+ var blob = new Blob(["TEST"]);
+ var reader = new FileReader();
+
+ reader.onloadstart = this.step_func(function(evt) {
+ assert_false(evt.bubbles, "The bubbles must be false when the event is dispatched");
+ assert_false(evt.cancelable, "The cancelable must be false when the event is dispatched");
+ });
+
+ reader.onload = this.step_func(function(evt) {
+ assert_false(evt.bubbles, "The bubbles must be false when the event is dispatched");
+ assert_false(evt.cancelable, "The cancelable must be false when the event is dispatched");
+ });
+
+ reader.onloadend = this.step_func(function(evt) {
+ assert_false(evt.bubbles, "The bubbles must be false when the event is dispatched");
+ assert_false(evt.cancelable, "The cancelable must be false when the event is dispatched");
+ this.done();
+ });
+
+ reader.readAsText(blob);
+ }, "Check the values of bubbles and cancelable are false when the progress event is dispatched");
+</script>
+
diff --git a/test/wpt/tests/FileAPI/FileReader/support/file_test1.txt b/test/wpt/tests/FileAPI/FileReader/support/file_test1.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/wpt/tests/FileAPI/FileReader/support/file_test1.txt
diff --git a/test/wpt/tests/FileAPI/FileReader/test_errors-manual.html b/test/wpt/tests/FileAPI/FileReader/test_errors-manual.html
new file mode 100644
index 0000000..b8c3f84
--- /dev/null
+++ b/test/wpt/tests/FileAPI/FileReader/test_errors-manual.html
@@ -0,0 +1,72 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>FileReader Errors Test</title>
+ <link rel=help href="http://dev.w3.org/2006/webapi/FileAPI/#convenienceAPI">
+ <link rel=author title="Breezewish" href="mailto:me@breeswish.org">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+</head>
+<body>
+ <form name="upload">
+ <input type="file" id="fileChooser"><br><input type="button" id="start" value="start">
+ </form>
+
+ <div>
+ <p>Test steps:</p>
+ <ol>
+ <li>Download the <a href="support/file_test1.txt">file</a>.</li>
+ <li>Select the file in the file inputbox.</li>
+ <li>Delete the file.</li>
+ <li>Click the 'start' button.</li>
+ </ol>
+ </div>
+
+ <div id="log"></div>
+
+ <script>
+
+ var fileChooser = document.querySelector('#fileChooser');
+
+ setup({explicit_done: true});
+ setup({explicit_timeout: true});
+
+ on_event(fileChooser, 'change', function() {
+
+ async_test(function(t) {
+
+ var reader = new FileReader();
+ reader.readAsArrayBuffer(fileChooser.files[0]);
+
+ reader.onloadend = t.step_func_done(function(event) {
+ assert_equals(event.target.readyState, FileReader.DONE);
+ assert_equals(reader.error, null);
+ });
+
+ }, 'FileReader.error should be null if there are no errors when reading');
+
+ });
+
+ on_event(document.querySelector('#start'), 'click', function() {
+
+ async_test(function(t) {
+
+ var reader = new FileReader();
+ reader.readAsArrayBuffer(fileChooser.files[0]);
+
+ reader.onloadend = t.step_func_done(function(event) {
+ assert_equals(event.target.readyState, FileReader.DONE);
+ assert_equals(reader.error.code, 8);
+ assert_true(reader.error instanceof DOMException);
+ });
+
+ }, 'FileReader.error should be NOT_FOUND_ERR if the file is not found when reading');
+
+ done();
+
+ });
+
+ </script>
+</body>
+</html>
diff --git a/test/wpt/tests/FileAPI/FileReader/test_notreadableerrors-manual.html b/test/wpt/tests/FileAPI/FileReader/test_notreadableerrors-manual.html
new file mode 100644
index 0000000..46d7359
--- /dev/null
+++ b/test/wpt/tests/FileAPI/FileReader/test_notreadableerrors-manual.html
@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>FileReader NotReadableError Test</title>
+<link rel="author" title="Intel" href="http://www.intel.com">
+<link rel="help" href="https://w3c.github.io/FileAPI/#dfn-error-codes">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<form name="upload">
+ <input type="file" id="fileChooser"><br><input type="button" id="start" value="start">
+</form>
+
+<div>
+ <p>Test steps:</p>
+ <ol>
+ <li>Download the <a href="support/file_test1.txt">file</a>.</li>
+ <li>Select the file in the file inputbox.</li>
+ <li>Delete the file's readable permission.</li>
+ <li>Click the 'start' button.</li>
+ </ol>
+</div>
+
+<script>
+
+ const fileChooser = document.querySelector('#fileChooser');
+
+ setup({explicit_done: true});
+ setup({explicit_timeout: true});
+
+ on_event(document.querySelector('#start'), 'click', () => {
+ async_test(t => {
+ const reader = new FileReader();
+ reader.readAsArrayBuffer(fileChooser.files[0]);
+ reader.onloadend = t.step_func_done(event => {
+ assert_equals(event.target.readyState, FileReader.DONE);
+ assert_equals(reader.error.name, "NotReadableError");
+ });
+ }, 'FileReader.error should be NotReadableError if the file is unreadable');
+ done();
+ });
+
+</script>
+
diff --git a/test/wpt/tests/FileAPI/FileReader/test_securityerrors-manual.html b/test/wpt/tests/FileAPI/FileReader/test_securityerrors-manual.html
new file mode 100644
index 0000000..add93ed
--- /dev/null
+++ b/test/wpt/tests/FileAPI/FileReader/test_securityerrors-manual.html
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>FileReader SecurityError Test</title>
+<link rel="author" title="Intel" href="http://www.intel.com">
+<link rel="help" href="https://w3c.github.io/FileAPI/#dfn-error-codes">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<form name="upload">
+ <input type="file" id="fileChooser"><br><input type="button" id="start" value="start">
+</form>
+
+<div>
+ <p>Test steps:</p>
+ <ol>
+ <li>Select a system sensitive file (e.g. files in /usr/bin, password files,
+ and other native operating system executables) in the file inputbox.</li>
+ <li>Click the 'start' button.</li>
+ </ol>
+</div>
+
+<script>
+
+ const fileChooser = document.querySelector('#fileChooser');
+
+ setup({explicit_done: true});
+ setup({explicit_timeout: true});
+
+ on_event(document.querySelector('#start'), 'click', () => {
+ async_test(t => {
+ const reader = new FileReader();
+ reader.readAsArrayBuffer(fileChooser.files[0]);
+ reader.onloadend = t.step_func_done(event => {
+ assert_equals(event.target.readyState, FileReader.DONE);
+ assert_equals(reader.error.name, "SecurityError");
+ });
+ }, 'FileReader.error should be SECURITY_ERROR if the file is a system sensitive file');
+ done();
+ });
+
+</script>
diff --git a/test/wpt/tests/FileAPI/FileReader/workers.html b/test/wpt/tests/FileAPI/FileReader/workers.html
new file mode 100644
index 0000000..8e114ee
--- /dev/null
+++ b/test/wpt/tests/FileAPI/FileReader/workers.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+
+async_test(t => {
+ function workerCode() {
+ close();
+ var blob = new Blob([123]);
+ var fr = new FileReader();
+ fr.readAsText(blob);
+ fr.abort()
+ fr.readAsArrayBuffer(blob);
+ postMessage(true);
+ }
+
+ var workerBlob = new Blob([workerCode.toString() + ";workerCode();"], {type:"application/javascript"});
+
+ var w = new Worker(URL.createObjectURL(workerBlob));
+ w.onmessage = function(e) {
+ assert_true(e.data, "FileReader created during worker shutdown.");
+ t.done();
+ }
+}, 'FileReader created after a worker self.close()');
+
+</script>
diff --git a/test/wpt/tests/FileAPI/FileReaderSync.worker.js b/test/wpt/tests/FileAPI/FileReaderSync.worker.js
new file mode 100644
index 0000000..3d7a022
--- /dev/null
+++ b/test/wpt/tests/FileAPI/FileReaderSync.worker.js
@@ -0,0 +1,56 @@
+importScripts("/resources/testharness.js");
+
+var blob, empty_blob, readerSync;
+setup(() => {
+ readerSync = new FileReaderSync();
+ blob = new Blob(["test"]);
+ empty_blob = new Blob();
+});
+
+test(() => {
+ assert_true(readerSync instanceof FileReaderSync);
+}, "Interface");
+
+test(() => {
+ var text = readerSync.readAsText(blob);
+ assert_equals(text, "test");
+}, "readAsText");
+
+test(() => {
+ var text = readerSync.readAsText(empty_blob);
+ assert_equals(text, "");
+}, "readAsText with empty blob");
+
+test(() => {
+ var data = readerSync.readAsDataURL(blob);
+ assert_equals(data.indexOf("data:"), 0);
+}, "readAsDataURL");
+
+test(() => {
+ var data = readerSync.readAsDataURL(empty_blob);
+ assert_equals(data.indexOf("data:"), 0);
+}, "readAsDataURL with empty blob");
+
+test(() => {
+ var data = readerSync.readAsBinaryString(blob);
+ assert_equals(data, "test");
+}, "readAsBinaryString");
+
+test(() => {
+ var data = readerSync.readAsBinaryString(empty_blob);
+ assert_equals(data, "");
+}, "readAsBinaryString with empty blob");
+
+test(() => {
+ var data = readerSync.readAsArrayBuffer(blob);
+ assert_true(data instanceof ArrayBuffer);
+ assert_equals(data.byteLength, "test".length);
+}, "readAsArrayBuffer");
+
+test(() => {
+ var data = readerSync.readAsArrayBuffer(empty_blob);
+ assert_true(data instanceof ArrayBuffer);
+ assert_equals(data.byteLength, 0);
+}, "readAsArrayBuffer with empty blob");
+
+done();
diff --git a/test/wpt/tests/FileAPI/META.yml b/test/wpt/tests/FileAPI/META.yml
new file mode 100644
index 0000000..506a59f
--- /dev/null
+++ b/test/wpt/tests/FileAPI/META.yml
@@ -0,0 +1,6 @@
+spec: https://w3c.github.io/FileAPI/
+suggested_reviewers:
+ - inexorabletash
+ - zqzhang
+ - jdm
+ - mkruisselbrink
diff --git a/test/wpt/tests/FileAPI/blob/Blob-array-buffer.any.js b/test/wpt/tests/FileAPI/blob/Blob-array-buffer.any.js
new file mode 100644
index 0000000..2310646
--- /dev/null
+++ b/test/wpt/tests/FileAPI/blob/Blob-array-buffer.any.js
@@ -0,0 +1,45 @@
+// META: title=Blob Array Buffer
+// META: script=../support/Blob.js
+'use strict';
+
+promise_test(async () => {
+ const input_arr = new TextEncoder().encode("PASS");
+ const blob = new Blob([input_arr]);
+ const array_buffer = await blob.arrayBuffer();
+ assert_true(array_buffer instanceof ArrayBuffer);
+ assert_equals_typed_array(new Uint8Array(array_buffer), input_arr);
+}, "Blob.arrayBuffer()")
+
+promise_test(async () => {
+ const input_arr = new TextEncoder().encode("");
+ const blob = new Blob([input_arr]);
+ const array_buffer = await blob.arrayBuffer();
+ assert_true(array_buffer instanceof ArrayBuffer);
+ assert_equals_typed_array(new Uint8Array(array_buffer), input_arr);
+}, "Blob.arrayBuffer() empty Blob data")
+
+promise_test(async () => {
+ const input_arr = new TextEncoder().encode("\u08B8\u000a");
+ const blob = new Blob([input_arr]);
+ const array_buffer = await blob.arrayBuffer();
+ assert_equals_typed_array(new Uint8Array(array_buffer), input_arr);
+}, "Blob.arrayBuffer() non-ascii input")
+
+promise_test(async () => {
+ const input_arr = [8, 241, 48, 123, 151];
+ const typed_arr = new Uint8Array(input_arr);
+ const blob = new Blob([typed_arr]);
+ const array_buffer = await blob.arrayBuffer();
+ assert_equals_typed_array(new Uint8Array(array_buffer), typed_arr);
+}, "Blob.arrayBuffer() non-unicode input")
+
+promise_test(async () => {
+ const input_arr = new TextEncoder().encode("PASS");
+ const blob = new Blob([input_arr]);
+ const array_buffer_results = await Promise.all([blob.arrayBuffer(),
+ blob.arrayBuffer(), blob.arrayBuffer()]);
+ for (let array_buffer of array_buffer_results) {
+ assert_true(array_buffer instanceof ArrayBuffer);
+ assert_equals_typed_array(new Uint8Array(array_buffer), input_arr);
+ }
+}, "Blob.arrayBuffer() concurrent reads")
diff --git a/test/wpt/tests/FileAPI/blob/Blob-constructor-dom.window.js b/test/wpt/tests/FileAPI/blob/Blob-constructor-dom.window.js
new file mode 100644
index 0000000..4fd4a43
--- /dev/null
+++ b/test/wpt/tests/FileAPI/blob/Blob-constructor-dom.window.js
@@ -0,0 +1,53 @@
+// META: title=Blob constructor
+// META: script=../support/Blob.js
+'use strict';
+
+var test_error = {
+ name: "test",
+ message: "test error",
+};
+
+test(function() {
+ var args = [
+ document.createElement("div"),
+ window,
+ ];
+ args.forEach(function(arg) {
+ assert_throws_js(TypeError, function() {
+ new Blob(arg);
+ }, "Should throw for argument " + format_value(arg) + ".");
+ });
+}, "Passing platform objects for blobParts should throw a TypeError.");
+
+test(function() {
+ var element = document.createElement("div");
+ element.appendChild(document.createElement("div"));
+ element.appendChild(document.createElement("p"));
+ var list = element.children;
+ Object.defineProperty(list, "length", {
+ get: function() { throw test_error; }
+ });
+ assert_throws_exactly(test_error, function() {
+ new Blob(list);
+ });
+}, "A platform object that supports indexed properties should be treated as a sequence for the blobParts argument (overwritten 'length'.)");
+
+test_blob(function() {
+ var select = document.createElement("select");
+ select.appendChild(document.createElement("option"));
+ return new Blob(select);
+}, {
+ expected: "[object HTMLOptionElement]",
+ type: "",
+ desc: "Passing an platform object that supports indexed properties as the blobParts array should work (select)."
+});
+
+test_blob(function() {
+ var elm = document.createElement("div");
+ elm.setAttribute("foo", "bar");
+ return new Blob(elm.attributes);
+}, {
+ expected: "[object Attr]",
+ type: "",
+ desc: "Passing an platform object that supports indexed properties as the blobParts array should work (attributes)."
+}); \ No newline at end of file
diff --git a/test/wpt/tests/FileAPI/blob/Blob-constructor-endings.html b/test/wpt/tests/FileAPI/blob/Blob-constructor-endings.html
new file mode 100644
index 0000000..04edd2a
--- /dev/null
+++ b/test/wpt/tests/FileAPI/blob/Blob-constructor-endings.html
@@ -0,0 +1,104 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Blob constructor: endings option</title>
+<link rel=help href="https://w3c.github.io/FileAPI/#constructorBlob">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+
+// Windows platforms use CRLF as the native line ending. All others use LF.
+const crlf = navigator.platform.startsWith('Win');
+const native_ending = crlf ? '\r\n' : '\n';
+
+function readBlobAsPromise(blob) {
+ return new Promise((resolve, reject) => {
+ const reader = new FileReader();
+ reader.readAsText(blob);
+ reader.onload = e => resolve(reader.result);
+ reader.onerror = e => reject(reader.error);
+ });
+}
+
+[
+ 'transparent',
+ 'native'
+].forEach(value => test(t => {
+ assert_class_string(new Blob([], {endings: value}), 'Blob',
+ `Constructor should allow "${value}" endings`);
+}, `Valid "endings" value: ${JSON.stringify(value)}`));
+
+[
+ null,
+ '',
+ 'invalidEnumValue',
+ 'Transparent',
+ 'NATIVE',
+ 0,
+ {}
+].forEach(value => test(t => {
+ assert_throws_js(TypeError, () => new Blob([], {endings: value}),
+ 'Blob constructor should throw');
+}, `Invalid "endings" value: ${JSON.stringify(value)}`));
+
+test(t => {
+ const test_error = {name: 'test'};
+ assert_throws_exactly(
+ test_error,
+ () => new Blob([], { get endings() { throw test_error; }}),
+ 'Blob constructor should propagate exceptions from "endings" property');
+}, 'Exception propagation from options');
+
+test(t => {
+ let got = false;
+ new Blob([], { get endings() { got = true; } });
+ assert_true(got, 'The "endings" property was accessed during construction.');
+}, 'The "endings" options property is used');
+
+[
+ {name: 'LF', input: '\n', native: native_ending},
+ {name: 'CR', input: '\r', native: native_ending},
+
+ {name: 'CRLF', input: '\r\n', native: native_ending},
+ {name: 'CRCR', input: '\r\r', native: native_ending.repeat(2)},
+ {name: 'LFCR', input: '\n\r', native: native_ending.repeat(2)},
+ {name: 'LFLF', input: '\n\n', native: native_ending.repeat(2)},
+
+ {name: 'CRCRLF', input: '\r\r\n', native: native_ending.repeat(2)},
+ {name: 'CRLFLF', input: '\r\n\n', native: native_ending.repeat(2)},
+ {name: 'CRLFCR', input: '\r\n\r\n', native: native_ending.repeat(2)},
+
+ {name: 'CRLFCRLF', input: '\r\n\r\n', native: native_ending.repeat(2)},
+ {name: 'LFCRLFCR', input: '\n\r\n\r', native: native_ending.repeat(3)},
+
+].forEach(testCase => {
+ promise_test(async t => {
+ const blob = new Blob([testCase.input]);
+ assert_equals(
+ await readBlobAsPromise(blob), testCase.input,
+ 'Newlines should not change with endings unspecified');
+ }, `Input ${testCase.name} with endings unspecified`);
+
+ promise_test(async t => {
+ const blob = new Blob([testCase.input], {endings: 'transparent'});
+ assert_equals(
+ await readBlobAsPromise(blob), testCase.input,
+ 'Newlines should not change with endings "transparent"');
+ }, `Input ${testCase.name} with endings 'transparent'`);
+
+ promise_test(async t => {
+ const blob = new Blob([testCase.input], {endings: 'native'});
+ assert_equals(
+ await readBlobAsPromise(blob), testCase.native,
+ 'Newlines should match the platform with endings "native"');
+ }, `Input ${testCase.name} with endings 'native'`);
+});
+
+promise_test(async t => {
+ const blob = new Blob(['\r', '\n'], {endings: 'native'});
+ const expected = native_ending.repeat(2);
+ assert_equals(
+ await readBlobAsPromise(blob), expected,
+ 'CR/LF in adjacent strings should be converted to two platform newlines');
+}, `CR/LF in adjacent input strings`);
+
+</script>
diff --git a/test/wpt/tests/FileAPI/blob/Blob-constructor.any.js b/test/wpt/tests/FileAPI/blob/Blob-constructor.any.js
new file mode 100644
index 0000000..d16f760
--- /dev/null
+++ b/test/wpt/tests/FileAPI/blob/Blob-constructor.any.js
@@ -0,0 +1,468 @@
+// META: title=Blob constructor
+// META: script=../support/Blob.js
+'use strict';
+
+test(function() {
+ assert_true("Blob" in globalThis, "globalThis should have a Blob property.");
+ assert_equals(Blob.length, 0, "Blob.length should be 0.");
+ assert_true(Blob instanceof Function, "Blob should be a function.");
+}, "Blob interface object");
+
+// Step 1.
+test(function() {
+ var blob = new Blob();
+ assert_true(blob instanceof Blob);
+ assert_equals(String(blob), '[object Blob]');
+ assert_equals(blob.size, 0);
+ assert_equals(blob.type, "");
+}, "Blob constructor with no arguments");
+test(function() {
+ assert_throws_js(TypeError, function() { var blob = Blob(); });
+}, "Blob constructor with no arguments, without 'new'");
+test(function() {
+ var blob = new Blob;
+ assert_true(blob instanceof Blob);
+ assert_equals(blob.size, 0);
+ assert_equals(blob.type, "");
+}, "Blob constructor without brackets");
+test(function() {
+ var blob = new Blob(undefined);
+ assert_true(blob instanceof Blob);
+ assert_equals(String(blob), '[object Blob]');
+ assert_equals(blob.size, 0);
+ assert_equals(blob.type, "");
+}, "Blob constructor with undefined as first argument");
+
+// blobParts argument (WebIDL).
+test(function() {
+ var args = [
+ null,
+ true,
+ false,
+ 0,
+ 1,
+ 1.5,
+ "FAIL",
+ new Date(),
+ new RegExp(),
+ {},
+ { 0: "FAIL", length: 1 },
+ ];
+ args.forEach(function(arg) {
+ assert_throws_js(TypeError, function() {
+ new Blob(arg);
+ }, "Should throw for argument " + format_value(arg) + ".");
+ });
+}, "Passing non-objects, Dates and RegExps for blobParts should throw a TypeError.");
+
+test_blob(function() {
+ return new Blob({
+ [Symbol.iterator]: Array.prototype[Symbol.iterator],
+ });
+}, {
+ expected: "",
+ type: "",
+ desc: "A plain object with @@iterator should be treated as a sequence for the blobParts argument."
+});
+test(t => {
+ const blob = new Blob({
+ [Symbol.iterator]() {
+ var i = 0;
+ return {next: () => [
+ {done:false, value:'ab'},
+ {done:false, value:'cde'},
+ {done:true}
+ ][i++]
+ };
+ }
+ });
+ assert_equals(blob.size, 5, 'Custom @@iterator should be treated as a sequence');
+}, "A plain object with custom @@iterator should be treated as a sequence for the blobParts argument.");
+test_blob(function() {
+ return new Blob({
+ [Symbol.iterator]: Array.prototype[Symbol.iterator],
+ 0: "PASS",
+ length: 1
+ });
+}, {
+ expected: "PASS",
+ type: "",
+ desc: "A plain object with @@iterator and a length property should be treated as a sequence for the blobParts argument."
+});
+test_blob(function() {
+ return new Blob(new String("xyz"));
+}, {
+ expected: "xyz",
+ type: "",
+ desc: "A String object should be treated as a sequence for the blobParts argument."
+});
+test_blob(function() {
+ return new Blob(new Uint8Array([1, 2, 3]));
+}, {
+ expected: "123",
+ type: "",
+ desc: "A Uint8Array object should be treated as a sequence for the blobParts argument."
+});
+
+var test_error = {
+ name: "test",
+ message: "test error",
+};
+
+test(function() {
+ var obj = {
+ [Symbol.iterator]: Array.prototype[Symbol.iterator],
+ get length() { throw test_error; }
+ };
+ assert_throws_exactly(test_error, function() {
+ new Blob(obj);
+ });
+}, "The length getter should be invoked and any exceptions should be propagated.");
+
+test(function() {
+ assert_throws_exactly(test_error, function() {
+ var obj = {
+ [Symbol.iterator]: Array.prototype[Symbol.iterator],
+ length: {
+ valueOf: null,
+ toString: function() { throw test_error; }
+ }
+ };
+ new Blob(obj);
+ });
+ assert_throws_exactly(test_error, function() {
+ var obj = {
+ [Symbol.iterator]: Array.prototype[Symbol.iterator],
+ length: { valueOf: function() { throw test_error; } }
+ };
+ new Blob(obj);
+ });
+}, "ToUint32 should be applied to the length and any exceptions should be propagated.");
+
+test(function() {
+ var received = [];
+ var obj = {
+ get [Symbol.iterator]() {
+ received.push("Symbol.iterator");
+ return Array.prototype[Symbol.iterator];
+ },
+ get length() {
+ received.push("length getter");
+ return {
+ valueOf: function() {
+ received.push("length valueOf");
+ return 3;
+ }
+ };
+ },
+ get 0() {
+ received.push("0 getter");
+ return {
+ toString: function() {
+ received.push("0 toString");
+ return "a";
+ }
+ };
+ },
+ get 1() {
+ received.push("1 getter");
+ throw test_error;
+ },
+ get 2() {
+ received.push("2 getter");
+ assert_unreached("Should not call the getter for 2 if the getter for 1 threw.");
+ }
+ };
+ assert_throws_exactly(test_error, function() {
+ new Blob(obj);
+ });
+ assert_array_equals(received, [
+ "Symbol.iterator",
+ "length getter",
+ "length valueOf",
+ "0 getter",
+ "0 toString",
+ "length getter",
+ "length valueOf",
+ "1 getter",
+ ]);
+}, "Getters and value conversions should happen in order until an exception is thrown.");
+
+// XXX should add tests edge cases of ToLength(length)
+
+test(function() {
+ assert_throws_exactly(test_error, function() {
+ new Blob([{ toString: function() { throw test_error; } }]);
+ }, "Throwing toString");
+ assert_throws_exactly(test_error, function() {
+ new Blob([{ toString: undefined, valueOf: function() { throw test_error; } }]);
+ }, "Throwing valueOf");
+ assert_throws_exactly(test_error, function() {
+ new Blob([{
+ toString: function() { throw test_error; },
+ valueOf: function() { assert_unreached("Should not call valueOf if toString is present."); }
+ }]);
+ }, "Throwing toString and valueOf");
+ assert_throws_js(TypeError, function() {
+ new Blob([{toString: null, valueOf: null}]);
+ }, "Null toString and valueOf");
+}, "ToString should be called on elements of the blobParts array and any exceptions should be propagated.");
+
+test_blob(function() {
+ var arr = [
+ { toString: function() { arr.pop(); return "PASS"; } },
+ { toString: function() { assert_unreached("Should have removed the second element of the array rather than called toString() on it."); } }
+ ];
+ return new Blob(arr);
+}, {
+ expected: "PASS",
+ type: "",
+ desc: "Changes to the blobParts array should be reflected in the returned Blob (pop)."
+});
+
+test_blob(function() {
+ var arr = [
+ {
+ toString: function() {
+ if (arr.length === 3) {
+ return "A";
+ }
+ arr.unshift({
+ toString: function() {
+ assert_unreached("Should only access index 0 once.");
+ }
+ });
+ return "P";
+ }
+ },
+ {
+ toString: function() {
+ return "SS";
+ }
+ }
+ ];
+ return new Blob(arr);
+}, {
+ expected: "PASS",
+ type: "",
+ desc: "Changes to the blobParts array should be reflected in the returned Blob (unshift)."
+});
+
+test_blob(function() {
+ // https://www.w3.org/Bugs/Public/show_bug.cgi?id=17652
+ return new Blob([
+ null,
+ undefined,
+ true,
+ false,
+ 0,
+ 1,
+ new String("stringobject"),
+ [],
+ ['x', 'y'],
+ {},
+ { 0: "FAIL", length: 1 },
+ { toString: function() { return "stringA"; } },
+ { toString: undefined, valueOf: function() { return "stringB"; } },
+ { valueOf: function() { assert_unreached("Should not call valueOf if toString is present on the prototype."); } }
+ ]);
+}, {
+ expected: "nullundefinedtruefalse01stringobjectx,y[object Object][object Object]stringAstringB[object Object]",
+ type: "",
+ desc: "ToString should be called on elements of the blobParts array."
+});
+
+test_blob(function() {
+ return new Blob([
+ new ArrayBuffer(8)
+ ]);
+}, {
+ expected: "\0\0\0\0\0\0\0\0",
+ type: "",
+ desc: "ArrayBuffer elements of the blobParts array should be supported."
+});
+
+test_blob(function() {
+ return new Blob([
+ new Uint8Array([0x50, 0x41, 0x53, 0x53]),
+ new Int8Array([0x50, 0x41, 0x53, 0x53]),
+ new Uint16Array([0x4150, 0x5353]),
+ new Int16Array([0x4150, 0x5353]),
+ new Uint32Array([0x53534150]),
+ new Int32Array([0x53534150]),
+ new Float32Array([0xD341500000])
+ ]);
+}, {
+ expected: "PASSPASSPASSPASSPASSPASSPASS",
+ type: "",
+ desc: "Passing typed arrays as elements of the blobParts array should work."
+});
+test_blob(function() {
+ return new Blob([
+ // 0x535 3415053534150
+ // 0x535 = 0b010100110101 -> Sign = +, Exponent = 1333 - 1023 = 310
+ // 0x13415053534150 * 2**(-52)
+ // ==> 0x13415053534150 * 2**258 = 2510297372767036725005267563121821874921913208671273727396467555337665343087229079989707079680
+ new Float64Array([2510297372767036725005267563121821874921913208671273727396467555337665343087229079989707079680])
+ ]);
+}, {
+ expected: "PASSPASS",
+ type: "",
+ desc: "Passing a Float64Array as element of the blobParts array should work."
+});
+
+test_blob(function() {
+ return new Blob([
+ new BigInt64Array([BigInt("0x5353415053534150")]),
+ new BigUint64Array([BigInt("0x5353415053534150")])
+ ]);
+}, {
+ expected: "PASSPASSPASSPASS",
+ type: "",
+ desc: "Passing BigInt typed arrays as elements of the blobParts array should work."
+});
+
+var t_ports = async_test("Passing a FrozenArray as the blobParts array should work (FrozenArray<MessagePort>).");
+t_ports.step(function() {
+ var channel = new MessageChannel();
+ channel.port2.onmessage = this.step_func(function(e) {
+ var b_ports = new Blob(e.ports);
+ assert_equals(b_ports.size, "[object MessagePort]".length);
+ this.done();
+ });
+ var channel2 = new MessageChannel();
+ channel.port1.postMessage('', [channel2.port1]);
+});
+
+test_blob(function() {
+ var blob = new Blob(['foo']);
+ return new Blob([blob, blob]);
+}, {
+ expected: "foofoo",
+ type: "",
+ desc: "Array with two blobs"
+});
+
+test_blob_binary(function() {
+ var view = new Uint8Array([0, 255, 0]);
+ return new Blob([view.buffer, view.buffer]);
+}, {
+ expected: [0, 255, 0, 0, 255, 0],
+ type: "",
+ desc: "Array with two buffers"
+});
+
+test_blob_binary(function() {
+ var view = new Uint8Array([0, 255, 0, 4]);
+ var blob = new Blob([view, view]);
+ assert_equals(blob.size, 8);
+ var view1 = new Uint16Array(view.buffer, 2);
+ return new Blob([view1, view.buffer, view1]);
+}, {
+ expected: [0, 4, 0, 255, 0, 4, 0, 4],
+ type: "",
+ desc: "Array with two bufferviews"
+});
+
+test_blob(function() {
+ var view = new Uint8Array([0]);
+ var blob = new Blob(["fo"]);
+ return new Blob([view.buffer, blob, "foo"]);
+}, {
+ expected: "\0fofoo",
+ type: "",
+ desc: "Array with mixed types"
+});
+
+test(function() {
+ const accessed = [];
+ const stringified = [];
+
+ new Blob([], {
+ get type() { accessed.push('type'); },
+ get endings() { accessed.push('endings'); }
+ });
+ new Blob([], {
+ type: { toString: () => { stringified.push('type'); return ''; } },
+ endings: { toString: () => { stringified.push('endings'); return 'transparent'; } }
+ });
+ assert_array_equals(accessed, ['endings', 'type']);
+ assert_array_equals(stringified, ['endings', 'type']);
+}, "options properties should be accessed in lexicographic order.");
+
+test(function() {
+ assert_throws_exactly(test_error, function() {
+ new Blob(
+ [{ toString: function() { throw test_error } }],
+ {
+ get type() { assert_unreached("type getter should not be called."); }
+ }
+ );
+ });
+}, "Arguments should be evaluated from left to right.");
+
+[
+ null,
+ undefined,
+ {},
+ { unrecognized: true },
+ /regex/,
+ function() {}
+].forEach(function(arg, idx) {
+ test_blob(function() {
+ return new Blob([], arg);
+ }, {
+ expected: "",
+ type: "",
+ desc: "Passing " + format_value(arg) + " (index " + idx + ") for options should use the defaults."
+ });
+ test_blob(function() {
+ return new Blob(["\na\r\nb\n\rc\r"], arg);
+ }, {
+ expected: "\na\r\nb\n\rc\r",
+ type: "",
+ desc: "Passing " + format_value(arg) + " (index " + idx + ") for options should use the defaults (with newlines)."
+ });
+});
+
+[
+ 123,
+ 123.4,
+ true,
+ 'abc'
+].forEach(arg => {
+ test(t => {
+ assert_throws_js(TypeError, () => new Blob([], arg),
+ 'Blob constructor should throw with invalid property bag');
+ }, `Passing ${JSON.stringify(arg)} for options should throw`);
+});
+
+var type_tests = [
+ // blobParts, type, expected type
+ [[], '', ''],
+ [[], 'a', 'a'],
+ [[], 'A', 'a'],
+ [[], 'text/html', 'text/html'],
+ [[], 'TEXT/HTML', 'text/html'],
+ [[], 'text/plain;charset=utf-8', 'text/plain;charset=utf-8'],
+ [[], '\u00E5', ''],
+ [[], '\uD801\uDC7E', ''], // U+1047E
+ [[], ' image/gif ', ' image/gif '],
+ [[], '\timage/gif\t', ''],
+ [[], 'image/gif;\u007f', ''],
+ [[], '\u0130mage/gif', ''], // uppercase i with dot
+ [[], '\u0131mage/gif', ''], // lowercase dotless i
+ [[], 'image/gif\u0000', ''],
+ // check that type isn't changed based on sniffing
+ [[0x3C, 0x48, 0x54, 0x4D, 0x4C, 0x3E], 'unknown/unknown', 'unknown/unknown'], // "<HTML>"
+ [[0x00, 0xFF], 'text/plain', 'text/plain'],
+ [[0x47, 0x49, 0x46, 0x38, 0x39, 0x61], 'image/png', 'image/png'], // "GIF89a"
+];
+
+type_tests.forEach(function(t) {
+ test(function() {
+ var arr = new Uint8Array([t[0]]).buffer;
+ var b = new Blob([arr], {type:t[1]});
+ assert_equals(b.type, t[2]);
+ }, "Blob with type " + format_value(t[1]));
+});
diff --git a/test/wpt/tests/FileAPI/blob/Blob-in-worker.worker.js b/test/wpt/tests/FileAPI/blob/Blob-in-worker.worker.js
new file mode 100644
index 0000000..a0ca845
--- /dev/null
+++ b/test/wpt/tests/FileAPI/blob/Blob-in-worker.worker.js
@@ -0,0 +1,9 @@
+importScripts("/resources/testharness.js");
+
+promise_test(async () => {
+ const data = "TEST";
+ const blob = new Blob([data], {type: "text/plain"});
+ assert_equals(await blob.text(), data);
+}, 'Create Blob in Worker');
+
+done();
diff --git a/test/wpt/tests/FileAPI/blob/Blob-slice-overflow.any.js b/test/wpt/tests/FileAPI/blob/Blob-slice-overflow.any.js
new file mode 100644
index 0000000..388fd92
--- /dev/null
+++ b/test/wpt/tests/FileAPI/blob/Blob-slice-overflow.any.js
@@ -0,0 +1,32 @@
+// META: title=Blob slice overflow
+'use strict';
+
+var text = '';
+
+for (var i = 0; i < 2000; ++i) {
+ text += 'A';
+}
+
+test(function() {
+ var blob = new Blob([text]);
+ var sliceBlob = blob.slice(-1, blob.size);
+ assert_equals(sliceBlob.size, 1, "Blob slice size");
+}, "slice start is negative, relativeStart will be max((size + start), 0)");
+
+test(function() {
+ var blob = new Blob([text]);
+ var sliceBlob = blob.slice(blob.size + 1, blob.size);
+ assert_equals(sliceBlob.size, 0, "Blob slice size");
+}, "slice start is greater than blob size, relativeStart will be min(start, size)");
+
+test(function() {
+ var blob = new Blob([text]);
+ var sliceBlob = blob.slice(blob.size - 2, -1);
+ assert_equals(sliceBlob.size, 1, "Blob slice size");
+}, "slice end is negative, relativeEnd will be max((size + end), 0)");
+
+test(function() {
+ var blob = new Blob([text]);
+ var sliceBlob = blob.slice(blob.size - 2, blob.size + 999);
+ assert_equals(sliceBlob.size, 2, "Blob slice size");
+}, "slice end is greater than blob size, relativeEnd will be min(end, size)");
diff --git a/test/wpt/tests/FileAPI/blob/Blob-slice.any.js b/test/wpt/tests/FileAPI/blob/Blob-slice.any.js
new file mode 100644
index 0000000..1f85d44
--- /dev/null
+++ b/test/wpt/tests/FileAPI/blob/Blob-slice.any.js
@@ -0,0 +1,231 @@
+// META: title=Blob slice
+// META: script=../support/Blob.js
+'use strict';
+
+test_blob(function() {
+ var blobTemp = new Blob(["PASS"]);
+ return blobTemp.slice();
+}, {
+ expected: "PASS",
+ type: "",
+ desc: "no-argument Blob slice"
+});
+
+test(function() {
+ var blob1, blob2;
+
+ test_blob(function() {
+ return blob1 = new Blob(["squiggle"]);
+ }, {
+ expected: "squiggle",
+ type: "",
+ desc: "blob1."
+ });
+
+ test_blob(function() {
+ return blob2 = new Blob(["steak"], {type: "content/type"});
+ }, {
+ expected: "steak",
+ type: "content/type",
+ desc: "blob2."
+ });
+
+ test_blob(function() {
+ return new Blob().slice(0,0,null);
+ }, {
+ expected: "",
+ type: "null",
+ desc: "null type Blob slice"
+ });
+
+ test_blob(function() {
+ return new Blob().slice(0,0,undefined);
+ }, {
+ expected: "",
+ type: "",
+ desc: "undefined type Blob slice"
+ });
+
+ test_blob(function() {
+ return new Blob().slice(0,0);
+ }, {
+ expected: "",
+ type: "",
+ desc: "no type Blob slice"
+ });
+
+ var arrayBuffer = new ArrayBuffer(16);
+ var int8View = new Int8Array(arrayBuffer);
+ for (var i = 0; i < 16; i++) {
+ int8View[i] = i + 65;
+ }
+
+ var testData = [
+ [
+ ["PASSSTRING"],
+ [{start: -6, contents: "STRING"},
+ {start: -12, contents: "PASSSTRING"},
+ {start: 4, contents: "STRING"},
+ {start: 12, contents: ""},
+ {start: 0, end: -6, contents: "PASS"},
+ {start: 0, end: -12, contents: ""},
+ {start: 0, end: 4, contents: "PASS"},
+ {start: 0, end: 12, contents: "PASSSTRING"},
+ {start: 7, end: 4, contents: ""}]
+ ],
+
+ // Test 3 strings
+ [
+ ["foo", "bar", "baz"],
+ [{start: 0, end: 9, contents: "foobarbaz"},
+ {start: 0, end: 3, contents: "foo"},
+ {start: 3, end: 9, contents: "barbaz"},
+ {start: 6, end: 9, contents: "baz"},
+ {start: 6, end: 12, contents: "baz"},
+ {start: 0, end: 9, contents: "foobarbaz"},
+ {start: 0, end: 11, contents: "foobarbaz"},
+ {start: 10, end: 15, contents: ""}]
+ ],
+
+ // Test string, Blob, string
+ [
+ ["foo", blob1, "baz"],
+ [{start: 0, end: 3, contents: "foo"},
+ {start: 3, end: 11, contents: "squiggle"},
+ {start: 2, end: 4, contents: "os"},
+ {start: 10, end: 12, contents: "eb"}]
+ ],
+
+ // Test blob, string, blob
+ [
+ [blob1, "foo", blob1],
+ [{start: 0, end: 8, contents: "squiggle"},
+ {start: 7, end: 9, contents: "ef"},
+ {start: 10, end: 12, contents: "os"},
+ {start: 1, end: 4, contents: "qui"},
+ {start: 12, end: 15, contents: "qui"},
+ {start: 40, end: 60, contents: ""}]
+ ],
+
+ // Test blobs all the way down
+ [
+ [blob2, blob1, blob2],
+ [{start: 0, end: 5, contents: "steak"},
+ {start: 5, end: 13, contents: "squiggle"},
+ {start: 13, end: 18, contents: "steak"},
+ {start: 1, end: 3, contents: "te"},
+ {start: 6, end: 10, contents: "quig"}]
+ ],
+
+ // Test an ArrayBufferView
+ [
+ [int8View, blob1, "foo"],
+ [{start: 0, end: 8, contents: "ABCDEFGH"},
+ {start: 8, end: 18, contents: "IJKLMNOPsq"},
+ {start: 17, end: 20, contents: "qui"},
+ {start: 4, end: 12, contents: "EFGHIJKL"}]
+ ],
+
+ // Test a partial ArrayBufferView
+ [
+ [new Uint8Array(arrayBuffer, 3, 5), blob1, "foo"],
+ [{start: 0, end: 8, contents: "DEFGHsqu"},
+ {start: 8, end: 18, contents: "igglefoo"},
+ {start: 4, end: 12, contents: "Hsquiggl"}]
+ ],
+
+ // Test type coercion of a number
+ [
+ [3, int8View, "foo"],
+ [{start: 0, end: 8, contents: "3ABCDEFG"},
+ {start: 8, end: 18, contents: "HIJKLMNOPf"},
+ {start: 17, end: 21, contents: "foo"},
+ {start: 4, end: 12, contents: "DEFGHIJK"}]
+ ],
+
+ [
+ [(new Uint8Array([0, 255, 0])).buffer,
+ new Blob(['abcd']),
+ 'efgh',
+ 'ijklmnopqrstuvwxyz'],
+ [{start: 1, end: 4, contents: "\uFFFD\u0000a"},
+ {start: 4, end: 8, contents: "bcde"},
+ {start: 8, end: 12, contents: "fghi"},
+ {start: 1, end: 12, contents: "\uFFFD\u0000abcdefghi"}]
+ ]
+ ];
+
+ testData.forEach(function(data, i) {
+ var blobs = data[0];
+ var tests = data[1];
+ tests.forEach(function(expectations, j) {
+ test(function() {
+ var blob = new Blob(blobs);
+ assert_true(blob instanceof Blob);
+ assert_false(blob instanceof File);
+
+ test_blob(function() {
+ return expectations.end === undefined
+ ? blob.slice(expectations.start)
+ : blob.slice(expectations.start, expectations.end);
+ }, {
+ expected: expectations.contents,
+ type: "",
+ desc: "Slicing test: slice (" + i + "," + j + ")."
+ });
+ }, "Slicing test (" + i + "," + j + ").");
+ });
+ });
+}, "Slices");
+
+var invalidTypes = [
+ "\xFF",
+ "te\x09xt/plain",
+ "te\x00xt/plain",
+ "te\x1Fxt/plain",
+ "te\x7Fxt/plain"
+];
+invalidTypes.forEach(function(type) {
+ test_blob(function() {
+ var blob = new Blob(["PASS"]);
+ return blob.slice(0, 4, type);
+ }, {
+ expected: "PASS",
+ type: "",
+ desc: "Invalid contentType (" + format_value(type) + ")"
+ });
+});
+
+var validTypes = [
+ "te(xt/plain",
+ "te)xt/plain",
+ "te<xt/plain",
+ "te>xt/plain",
+ "te@xt/plain",
+ "te,xt/plain",
+ "te;xt/plain",
+ "te:xt/plain",
+ "te\\xt/plain",
+ "te\"xt/plain",
+ "te/xt/plain",
+ "te[xt/plain",
+ "te]xt/plain",
+ "te?xt/plain",
+ "te=xt/plain",
+ "te{xt/plain",
+ "te}xt/plain",
+ "te\x20xt/plain",
+ "TEXT/PLAIN",
+ "text/plain;charset = UTF-8",
+ "text/plain;charset=UTF-8"
+];
+validTypes.forEach(function(type) {
+ test_blob(function() {
+ var blob = new Blob(["PASS"]);
+ return blob.slice(0, 4, type);
+ }, {
+ expected: "PASS",
+ type: type.toLowerCase(),
+ desc: "Valid contentType (" + format_value(type) + ")"
+ });
+});
diff --git a/test/wpt/tests/FileAPI/blob/Blob-stream-byob-crash.html b/test/wpt/tests/FileAPI/blob/Blob-stream-byob-crash.html
new file mode 100644
index 0000000..5992ed1
--- /dev/null
+++ b/test/wpt/tests/FileAPI/blob/Blob-stream-byob-crash.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<script type="module">
+ let a = new Blob(['', '', undefined], { })
+ let b = a.stream()
+ let c = new ReadableStreamBYOBReader(b)
+ let d = new Int16Array(8)
+ await c.read(d)
+ c.releaseLock()
+ await a.text()
+ await b.cancel()
+</script>
diff --git a/test/wpt/tests/FileAPI/blob/Blob-stream-sync-xhr-crash.html b/test/wpt/tests/FileAPI/blob/Blob-stream-sync-xhr-crash.html
new file mode 100644
index 0000000..fe54fb6
--- /dev/null
+++ b/test/wpt/tests/FileAPI/blob/Blob-stream-sync-xhr-crash.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<script>
+ const blob = new Blob([1, 2]);
+ const readable = blob.stream()
+ const writable = new WritableStream({}, {
+ size() {
+ let xhr = new XMLHttpRequest()
+ xhr.open("POST", "1", false)
+ xhr.send()
+ }
+ })
+ readable.pipeThrough({ readable, writable })
+</script>
diff --git a/test/wpt/tests/FileAPI/blob/Blob-stream.any.js b/test/wpt/tests/FileAPI/blob/Blob-stream.any.js
new file mode 100644
index 0000000..87710a1
--- /dev/null
+++ b/test/wpt/tests/FileAPI/blob/Blob-stream.any.js
@@ -0,0 +1,83 @@
+// META: title=Blob Stream
+// META: script=../support/Blob.js
+// META: script=/common/gc.js
+'use strict';
+
+// Helper function that triggers garbage collection while reading a chunk
+// if perform_gc is true.
+async function read_and_gc(reader, perform_gc) {
+ // Passing Uint8Array for byte streams; non-byte streams will simply ignore it
+ const read_promise = reader.read(new Uint8Array(64));
+ if (perform_gc) {
+ await garbageCollect();
+ }
+ return read_promise;
+}
+
+// Takes in a ReadableStream and reads from it until it is done, returning
+// an array that contains the results of each read operation. If perform_gc
+// is true, garbage collection is triggered while reading every chunk.
+async function read_all_chunks(stream, { perform_gc = false, mode } = {}) {
+ assert_true(stream instanceof ReadableStream);
+ assert_true('getReader' in stream);
+ const reader = stream.getReader({ mode });
+
+ assert_true('read' in reader);
+ let read_value = await read_and_gc(reader, perform_gc);
+
+ let out = [];
+ let i = 0;
+ while (!read_value.done) {
+ for (let val of read_value.value) {
+ out[i++] = val;
+ }
+ read_value = await read_and_gc(reader, perform_gc);
+ }
+ return out;
+}
+
+promise_test(async () => {
+ const blob = new Blob(["PASS"]);
+ const stream = blob.stream();
+ const chunks = await read_all_chunks(stream);
+ for (let [index, value] of chunks.entries()) {
+ assert_equals(value, "PASS".charCodeAt(index));
+ }
+}, "Blob.stream()")
+
+promise_test(async () => {
+ const blob = new Blob();
+ const stream = blob.stream();
+ const chunks = await read_all_chunks(stream);
+ assert_array_equals(chunks, []);
+}, "Blob.stream() empty Blob")
+
+promise_test(async () => {
+ const input_arr = [8, 241, 48, 123, 151];
+ const typed_arr = new Uint8Array(input_arr);
+ const blob = new Blob([typed_arr]);
+ const stream = blob.stream();
+ const chunks = await read_all_chunks(stream);
+ assert_array_equals(chunks, input_arr);
+}, "Blob.stream() non-unicode input")
+
+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 stream = blob.stream();
+ blob = null;
+ 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" +
+ "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 stream = blob.stream();
+ const chunks = await read_all_chunks(stream, { mode: "byob" });
+ assert_array_equals(chunks, input_arr);
+}, "Reading Blob.stream() with BYOB reader")
diff --git a/test/wpt/tests/FileAPI/blob/Blob-text.any.js b/test/wpt/tests/FileAPI/blob/Blob-text.any.js
new file mode 100644
index 0000000..d04fa97
--- /dev/null
+++ b/test/wpt/tests/FileAPI/blob/Blob-text.any.js
@@ -0,0 +1,64 @@
+// META: title=Blob Text
+// META: script=../support/Blob.js
+'use strict';
+
+promise_test(async () => {
+ const blob = new Blob(["PASS"]);
+ const text = await blob.text();
+ assert_equals(text, "PASS");
+}, "Blob.text()")
+
+promise_test(async () => {
+ const blob = new Blob();
+ const text = await blob.text();
+ assert_equals(text, "");
+}, "Blob.text() empty blob data")
+
+promise_test(async () => {
+ const blob = new Blob(["P", "A", "SS"]);
+ const text = await blob.text();
+ assert_equals(text, "PASS");
+}, "Blob.text() multi-element array in constructor")
+
+promise_test(async () => {
+ const non_unicode = "\u0061\u030A";
+ const input_arr = new TextEncoder().encode(non_unicode);
+ const blob = new Blob([input_arr]);
+ const text = await blob.text();
+ assert_equals(text, non_unicode);
+}, "Blob.text() non-unicode")
+
+promise_test(async () => {
+ const blob = new Blob(["PASS"], { type: "text/plain;charset=utf-16le" });
+ const text = await blob.text();
+ assert_equals(text, "PASS");
+}, "Blob.text() different charset param in type option")
+
+promise_test(async () => {
+ const non_unicode = "\u0061\u030A";
+ const input_arr = new TextEncoder().encode(non_unicode);
+ const blob = new Blob([input_arr], { type: "text/plain;charset=utf-16le" });
+ const text = await blob.text();
+ assert_equals(text, non_unicode);
+}, "Blob.text() different charset param with non-ascii input")
+
+promise_test(async () => {
+ const input_arr = new Uint8Array([192, 193, 245, 246, 247, 248, 249, 250, 251,
+ 252, 253, 254, 255]);
+ const blob = new Blob([input_arr]);
+ const text = await blob.text();
+ assert_equals(text, "\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd" +
+ "\ufffd\ufffd\ufffd\ufffd");
+}, "Blob.text() invalid utf-8 input")
+
+promise_test(async () => {
+ const input_arr = new Uint8Array([192, 193, 245, 246, 247, 248, 249, 250, 251,
+ 252, 253, 254, 255]);
+ const blob = new Blob([input_arr]);
+ const text_results = await Promise.all([blob.text(), blob.text(),
+ blob.text()]);
+ for (let text of text_results) {
+ assert_equals(text, "\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd" +
+ "\ufffd\ufffd\ufffd\ufffd");
+ }
+}, "Blob.text() concurrent reads")
diff --git a/test/wpt/tests/FileAPI/file/File-constructor-endings.html b/test/wpt/tests/FileAPI/file/File-constructor-endings.html
new file mode 100644
index 0000000..1282b6c
--- /dev/null
+++ b/test/wpt/tests/FileAPI/file/File-constructor-endings.html
@@ -0,0 +1,104 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>File constructor: endings option</title>
+<link rel=help href="https://w3c.github.io/FileAPI/#file-constructor">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+
+// Windows platforms use CRLF as the native line ending. All others use LF.
+const crlf = navigator.platform.startsWith('Win');
+const native_ending = crlf ? '\r\n' : '\n';
+
+function readBlobAsPromise(blob) {
+ return new Promise((resolve, reject) => {
+ const reader = new FileReader();
+ reader.readAsText(blob);
+ reader.onload = e => resolve(reader.result);
+ reader.onerror = e => reject(reader.error);
+ });
+}
+
+[
+ 'transparent',
+ 'native'
+].forEach(value => test(t => {
+ assert_class_string(new File([], "name", {endings: value}), 'File',
+ `Constructor should allow "${value}" endings`);
+}, `Valid "endings" value: ${JSON.stringify(value)}`));
+
+[
+ null,
+ '',
+ 'invalidEnumValue',
+ 'Transparent',
+ 'NATIVE',
+ 0,
+ {}
+].forEach(value => test(t => {
+ assert_throws_js(TypeError, () => new File([], "name", {endings: value}),
+ 'File constructor should throw');
+}, `Invalid "endings" value: ${JSON.stringify(value)}`));
+
+test(t => {
+ const test_error = {name: 'test'};
+ assert_throws_exactly(
+ test_error,
+ () => new File([], "name", { get endings() { throw test_error; }}),
+ 'File constructor should propagate exceptions from "endings" property');
+}, 'Exception propagation from options');
+
+test(t => {
+ let got = false;
+ new File([], "name", { get endings() { got = true; } });
+ assert_true(got, 'The "endings" property was accessed during construction.');
+}, 'The "endings" options property is used');
+
+[
+ {name: 'LF', input: '\n', native: native_ending},
+ {name: 'CR', input: '\r', native: native_ending},
+
+ {name: 'CRLF', input: '\r\n', native: native_ending},
+ {name: 'CRCR', input: '\r\r', native: native_ending.repeat(2)},
+ {name: 'LFCR', input: '\n\r', native: native_ending.repeat(2)},
+ {name: 'LFLF', input: '\n\n', native: native_ending.repeat(2)},
+
+ {name: 'CRCRLF', input: '\r\r\n', native: native_ending.repeat(2)},
+ {name: 'CRLFLF', input: '\r\n\n', native: native_ending.repeat(2)},
+ {name: 'CRLFCR', input: '\r\n\r\n', native: native_ending.repeat(2)},
+
+ {name: 'CRLFCRLF', input: '\r\n\r\n', native: native_ending.repeat(2)},
+ {name: 'LFCRLFCR', input: '\n\r\n\r', native: native_ending.repeat(3)},
+
+].forEach(testCase => {
+ promise_test(async t => {
+ const file = new File([testCase.input], "name");
+ assert_equals(
+ await readBlobAsPromise(file), testCase.input,
+ 'Newlines should not change with endings unspecified');
+ }, `Input ${testCase.name} with endings unspecified`);
+
+ promise_test(async t => {
+ const file = new File([testCase.input], "name", {endings: 'transparent'});
+ assert_equals(
+ await readBlobAsPromise(file), testCase.input,
+ 'Newlines should not change with endings "transparent"');
+ }, `Input ${testCase.name} with endings 'transparent'`);
+
+ promise_test(async t => {
+ const file = new File([testCase.input], "name", {endings: 'native'});
+ assert_equals(
+ await readBlobAsPromise(file), testCase.native,
+ 'Newlines should match the platform with endings "native"');
+ }, `Input ${testCase.name} with endings 'native'`);
+});
+
+promise_test(async t => {
+ const file = new File(['\r', '\n'], "name", {endings: 'native'});
+ const expected = native_ending.repeat(2);
+ assert_equals(
+ await readBlobAsPromise(file), expected,
+ 'CR/LF in adjacent strings should be converted to two platform newlines');
+}, `CR/LF in adjacent input strings`);
+
+</script>
diff --git a/test/wpt/tests/FileAPI/file/File-constructor.any.js b/test/wpt/tests/FileAPI/file/File-constructor.any.js
new file mode 100644
index 0000000..0b0185c
--- /dev/null
+++ b/test/wpt/tests/FileAPI/file/File-constructor.any.js
@@ -0,0 +1,155 @@
+// META: title=File constructor
+
+const to_string_obj = { toString: () => 'a string' };
+const to_string_throws = { toString: () => { throw new Error('expected'); } };
+
+test(function() {
+ assert_true("File" in globalThis, "globalThis should have a File property.");
+}, "File interface object exists");
+
+test(t => {
+ assert_throws_js(TypeError, () => new File(),
+ 'Bits argument is required');
+ assert_throws_js(TypeError, () => new File([]),
+ 'Name argument is required');
+}, 'Required arguments');
+
+function test_first_argument(arg1, expectedSize, testName) {
+ test(function() {
+ var file = new File(arg1, "dummy");
+ assert_true(file instanceof File);
+ assert_equals(file.name, "dummy");
+ assert_equals(file.size, expectedSize);
+ assert_equals(file.type, "");
+ // assert_false(file.isClosed); XXX: File.isClosed doesn't seem to be implemented
+ assert_not_equals(file.lastModified, "");
+ }, testName);
+}
+
+test_first_argument([], 0, "empty fileBits");
+test_first_argument(["bits"], 4, "DOMString fileBits");
+test_first_argument(["ð“½ð“®ð”ð“½"], 16, "Unicode DOMString fileBits");
+test_first_argument([new String('string object')], 13, "String object fileBits");
+test_first_argument([new Blob()], 0, "Empty Blob fileBits");
+test_first_argument([new Blob(["bits"])], 4, "Blob fileBits");
+test_first_argument([new File([], 'world.txt')], 0, "Empty File fileBits");
+test_first_argument([new File(["bits"], 'world.txt')], 4, "File fileBits");
+test_first_argument([new ArrayBuffer(8)], 8, "ArrayBuffer fileBits");
+test_first_argument([new Uint8Array([0x50, 0x41, 0x53, 0x53])], 4, "Typed array fileBits");
+test_first_argument(["bits", new Blob(["bits"]), new Blob(), new Uint8Array([0x50, 0x41]),
+ new Uint16Array([0x5353]), new Uint32Array([0x53534150])], 16, "Various fileBits");
+test_first_argument([12], 2, "Number in fileBits");
+test_first_argument([[1,2,3]], 5, "Array in fileBits");
+test_first_argument([{}], 15, "Object in fileBits"); // "[object Object]"
+if (globalThis.document !== undefined) {
+ test_first_argument([document.body], 24, "HTMLBodyElement in fileBits"); // "[object HTMLBodyElement]"
+}
+test_first_argument([to_string_obj], 8, "Object with toString in fileBits");
+test_first_argument({[Symbol.iterator]() {
+ let i = 0;
+ return {next: () => [
+ {done:false, value:'ab'},
+ {done:false, value:'cde'},
+ {done:true}
+ ][i++]};
+}}, 5, 'Custom @@iterator');
+
+[
+ 'hello',
+ 0,
+ null
+].forEach(arg => {
+ test(t => {
+ assert_throws_js(TypeError, () => new File(arg, 'world.html'),
+ 'Constructor should throw for invalid bits argument');
+ }, `Invalid bits argument: ${JSON.stringify(arg)}`);
+});
+
+test(t => {
+ assert_throws_js(Error, () => new File([to_string_throws], 'name.txt'),
+ 'Constructor should propagate exceptions');
+}, 'Bits argument: object that throws');
+
+
+function test_second_argument(arg2, expectedFileName, testName) {
+ test(function() {
+ var file = new File(["bits"], arg2);
+ assert_true(file instanceof File);
+ assert_equals(file.name, expectedFileName);
+ }, testName);
+}
+
+test_second_argument("dummy", "dummy", "Using fileName");
+test_second_argument("dummy/foo", "dummy/foo",
+ "No replacement when using special character in fileName");
+test_second_argument(null, "null", "Using null fileName");
+test_second_argument(1, "1", "Using number fileName");
+test_second_argument('', '', "Using empty string fileName");
+if (globalThis.document !== undefined) {
+ test_second_argument(document.body, '[object HTMLBodyElement]', "Using object fileName");
+}
+
+// testing the third argument
+[
+ {type: 'text/plain', expected: 'text/plain'},
+ {type: 'text/plain;charset=UTF-8', expected: 'text/plain;charset=utf-8'},
+ {type: 'TEXT/PLAIN', expected: 'text/plain'},
+ {type: 'ð“½ð“®ð”ð“½/ð”­ð”©ð”žð”¦ð”«', expected: ''},
+ {type: 'ascii/nonprintable\u001F', expected: ''},
+ {type: 'ascii/nonprintable\u007F', expected: ''},
+ {type: 'nonascii\u00EE', expected: ''},
+ {type: 'nonascii\u1234', expected: ''},
+ {type: 'nonparsable', expected: 'nonparsable'}
+].forEach(testCase => {
+ test(t => {
+ var file = new File(["bits"], "dummy", { type: testCase.type});
+ assert_true(file instanceof File);
+ assert_equals(file.type, testCase.expected);
+ }, `Using type in File constructor: ${testCase.type}`);
+});
+test(function() {
+ var file = new File(["bits"], "dummy", { lastModified: 42 });
+ assert_true(file instanceof File);
+ assert_equals(file.lastModified, 42);
+}, "Using lastModified");
+test(function() {
+ var file = new File(["bits"], "dummy", { name: "foo" });
+ assert_true(file instanceof File);
+ assert_equals(file.name, "dummy");
+}, "Misusing name");
+test(function() {
+ var file = new File(["bits"], "dummy", { unknownKey: "value" });
+ assert_true(file instanceof File);
+ assert_equals(file.name, "dummy");
+}, "Unknown properties are ignored");
+
+[
+ 123,
+ 123.4,
+ true,
+ 'abc'
+].forEach(arg => {
+ test(t => {
+ assert_throws_js(TypeError, () => new File(['bits'], 'name.txt', arg),
+ 'Constructor should throw for invalid property bag type');
+ }, `Invalid property bag: ${JSON.stringify(arg)}`);
+});
+
+[
+ null,
+ undefined,
+ [1,2,3],
+ /regex/,
+ function() {}
+].forEach(arg => {
+ test(t => {
+ assert_equals(new File(['bits'], 'name.txt', arg).size, 4,
+ 'Constructor should accept object-ish property bag type');
+ }, `Unusual but valid property bag: ${arg}`);
+});
+
+test(t => {
+ assert_throws_js(Error,
+ () => new File(['bits'], 'name.txt', {type: to_string_throws}),
+ 'Constructor should propagate exceptions');
+}, 'Property bag propagates exceptions');
diff --git a/test/wpt/tests/FileAPI/file/Worker-read-file-constructor.worker.js b/test/wpt/tests/FileAPI/file/Worker-read-file-constructor.worker.js
new file mode 100644
index 0000000..4e003b3
--- /dev/null
+++ b/test/wpt/tests/FileAPI/file/Worker-read-file-constructor.worker.js
@@ -0,0 +1,15 @@
+importScripts("/resources/testharness.js");
+
+async_test(function() {
+ var file = new File(["bits"], "dummy", { 'type': 'text/plain', lastModified: 42 });
+ var reader = new FileReader();
+ reader.onload = this.step_func_done(function() {
+ assert_equals(file.name, "dummy", "file name");
+ assert_equals(reader.result, "bits", "file content");
+ assert_equals(file.lastModified, 42, "file lastModified");
+ });
+ reader.onerror = this.unreached_func("Unexpected error event");
+ reader.readAsText(file);
+}, "FileReader in Worker");
+
+done();
diff --git a/test/wpt/tests/FileAPI/file/resources/echo-content-escaped.py b/test/wpt/tests/FileAPI/file/resources/echo-content-escaped.py
new file mode 100644
index 0000000..5370e1e
--- /dev/null
+++ b/test/wpt/tests/FileAPI/file/resources/echo-content-escaped.py
@@ -0,0 +1,26 @@
+from wptserve.utils import isomorphic_encode
+
+# Outputs the request body, with controls and non-ASCII bytes escaped
+# (b"\n" becomes b"\\x0a"), and with backslashes doubled.
+# As a convenience, CRLF newlines are left as is.
+
+def escape_byte(byte):
+ # Convert int byte into a single-char binary string.
+ byte = bytes([byte])
+ if b"\0" <= byte <= b"\x1F" or byte >= b"\x7F":
+ return b"\\x%02x" % ord(byte)
+ if byte == b"\\":
+ return b"\\\\"
+ return byte
+
+def main(request, response):
+
+ headers = [(b"X-Request-Method", isomorphic_encode(request.method)),
+ (b"X-Request-Content-Length", request.headers.get(b"Content-Length", b"NO")),
+ (b"X-Request-Content-Type", request.headers.get(b"Content-Type", b"NO")),
+ # Avoid any kind of content sniffing on the response.
+ (b"Content-Type", b"text/plain; charset=UTF-8")]
+
+ content = b"".join(map(escape_byte, request.body)).replace(b"\\x0d\\x0a", b"\r\n")
+
+ return headers, content
diff --git a/test/wpt/tests/FileAPI/file/send-file-form-controls.html b/test/wpt/tests/FileAPI/file/send-file-form-controls.html
new file mode 100644
index 0000000..6347065
--- /dev/null
+++ b/test/wpt/tests/FileAPI/file/send-file-form-controls.html
@@ -0,0 +1,113 @@
+<!DOCTYPE html>
+<meta charset="utf-8" />
+<title>Upload files named using controls</title>
+<link
+ rel="help"
+ href="https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#multipart-form-data"
+/>
+<link
+ rel="help"
+ href="https://html.spec.whatwg.org/multipage/dnd.html#datatransferitemlist"
+/>
+<link rel="help" href="https://w3c.github.io/FileAPI/#file-constructor" />
+<link
+ rel="author"
+ title="Benjamin C. Wiley Sittler"
+ href="mailto:bsittler@chromium.org"
+/>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../support/send-file-form-helper.js"></script>
+<script>
+ "use strict";
+
+ formPostFileUploadTest({
+ fileNameSource: "ASCII",
+ fileBaseName: "file-for-upload-in-form-NUL-[\0].txt",
+ formEncoding: "UTF-8",
+ expectedEncodedBaseName: "file-for-upload-in-form-NUL-[\0].txt",
+ });
+
+ formPostFileUploadTest({
+ fileNameSource: "ASCII",
+ fileBaseName: "file-for-upload-in-form-BS-[\b].txt",
+ formEncoding: "UTF-8",
+ expectedEncodedBaseName: "file-for-upload-in-form-BS-[\b].txt",
+ });
+
+ formPostFileUploadTest({
+ fileNameSource: "ASCII",
+ fileBaseName: "file-for-upload-in-form-VT-[\v].txt",
+ formEncoding: "UTF-8",
+ expectedEncodedBaseName: "file-for-upload-in-form-VT-[\v].txt",
+ });
+
+ // These have characters that undergo processing in name=,
+ // filename=, and/or value; formPostFileUploadTest postprocesses
+ // expectedEncodedBaseName for these internally.
+
+ formPostFileUploadTest({
+ fileNameSource: "ASCII",
+ fileBaseName: "file-for-upload-in-form-LF-[\n].txt",
+ formEncoding: "UTF-8",
+ expectedEncodedBaseName: "file-for-upload-in-form-LF-[\n].txt",
+ });
+
+ formPostFileUploadTest({
+ fileNameSource: "ASCII",
+ fileBaseName: "file-for-upload-in-form-LF-CR-[\n\r].txt",
+ formEncoding: "UTF-8",
+ expectedEncodedBaseName: "file-for-upload-in-form-LF-CR-[\n\r].txt",
+ });
+
+ formPostFileUploadTest({
+ fileNameSource: "ASCII",
+ fileBaseName: "file-for-upload-in-form-CR-[\r].txt",
+ formEncoding: "UTF-8",
+ expectedEncodedBaseName: "file-for-upload-in-form-CR-[\r].txt",
+ });
+
+ formPostFileUploadTest({
+ fileNameSource: "ASCII",
+ fileBaseName: "file-for-upload-in-form-CR-LF-[\r\n].txt",
+ formEncoding: "UTF-8",
+ expectedEncodedBaseName: "file-for-upload-in-form-CR-LF-[\r\n].txt",
+ });
+
+ formPostFileUploadTest({
+ fileNameSource: "ASCII",
+ fileBaseName: "file-for-upload-in-form-HT-[\t].txt",
+ formEncoding: "UTF-8",
+ expectedEncodedBaseName: "file-for-upload-in-form-HT-[\t].txt",
+ });
+
+ formPostFileUploadTest({
+ fileNameSource: "ASCII",
+ fileBaseName: "file-for-upload-in-form-FF-[\f].txt",
+ formEncoding: "UTF-8",
+ expectedEncodedBaseName: "file-for-upload-in-form-FF-[\f].txt",
+ });
+
+ formPostFileUploadTest({
+ fileNameSource: "ASCII",
+ fileBaseName: "file-for-upload-in-form-DEL-[\x7F].txt",
+ formEncoding: "UTF-8",
+ expectedEncodedBaseName: "file-for-upload-in-form-DEL-[\x7F].txt",
+ });
+
+ // The rest should be passed through unmodified:
+
+ formPostFileUploadTest({
+ fileNameSource: "ASCII",
+ fileBaseName: "file-for-upload-in-form-ESC-[\x1B].txt",
+ formEncoding: "UTF-8",
+ expectedEncodedBaseName: "file-for-upload-in-form-ESC-[\x1B].txt",
+ });
+
+ formPostFileUploadTest({
+ fileNameSource: "ASCII",
+ fileBaseName: "file-for-upload-in-form-SPACE-[ ].txt",
+ formEncoding: "UTF-8",
+ expectedEncodedBaseName: "file-for-upload-in-form-SPACE-[ ].txt",
+ });
+</script>
diff --git a/test/wpt/tests/FileAPI/file/send-file-form-iso-2022-jp.html b/test/wpt/tests/FileAPI/file/send-file-form-iso-2022-jp.html
new file mode 100644
index 0000000..c931c9b
--- /dev/null
+++ b/test/wpt/tests/FileAPI/file/send-file-form-iso-2022-jp.html
@@ -0,0 +1,65 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<meta name=timeout content=long>
+<title>Upload files in ISO-2022-JP form</title>
+<link rel="help"
+ href="https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#multipart-form-data">
+<link rel="help"
+ href="https://html.spec.whatwg.org/multipage/dnd.html#datatransferitemlist">
+<link rel="help"
+ href="https://w3c.github.io/FileAPI/#file-constructor">
+<link rel="author" title="Benjamin C. Wiley Sittler"
+ href="mailto:bsittler@chromium.org">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../support/send-file-form-helper.js"></script>
+<script>
+'use strict';
+
+formPostFileUploadTest({
+ fileNameSource: 'ASCII',
+ fileBaseName: 'file-for-upload-in-form.txt',
+ formEncoding: 'ISO-2022-JP',
+ expectedEncodedBaseName: 'file-for-upload-in-form.txt',
+});
+
+formPostFileUploadTest({
+ fileNameSource: 'x-user-defined',
+ fileBaseName: 'file-for-upload-in-form-\uF7F0\uF793\uF783\uF7A0.txt',
+ formEncoding: 'ISO-2022-JP',
+ expectedEncodedBaseName: (
+ 'file-for-upload-in-form-&#63472;&#63379;&#63363;&#63392;.txt'),
+});
+
+formPostFileUploadTest({
+ fileNameSource: 'windows-1252',
+ fileBaseName: 'file-for-upload-in-form-☺😂.txt',
+ formEncoding: 'ISO-2022-JP',
+ expectedEncodedBaseName: (
+ 'file-for-upload-in-form-&#226;&#732;&#186;&#240;&#376;&#732;&#8218;.txt'),
+});
+
+formPostFileUploadTest({
+ fileNameSource: 'JIS X 0201 and JIS X 0208',
+ fileBaseName: 'file-for-upload-in-form-★星★.txt',
+ formEncoding: 'ISO-2022-JP',
+ expectedEncodedBaseName: 'file-for-upload-in-form-\x1B$B!z@1!z\x1B(B.txt',
+});
+
+formPostFileUploadTest({
+ fileNameSource: 'Unicode',
+ fileBaseName: 'file-for-upload-in-form-☺😂.txt',
+ formEncoding: 'ISO-2022-JP',
+ expectedEncodedBaseName: 'file-for-upload-in-form-&#9786;&#128514;.txt',
+});
+
+formPostFileUploadTest({
+ fileNameSource: 'Unicode',
+ fileBaseName: `file-for-upload-in-form-${kTestChars}.txt`,
+ formEncoding: 'ISO-2022-JP',
+ expectedEncodedBaseName: `file-for-upload-in-form-${
+ kTestFallbackIso2022jp
+ }.txt`,
+});
+
+</script>
diff --git a/test/wpt/tests/FileAPI/file/send-file-form-punctuation.html b/test/wpt/tests/FileAPI/file/send-file-form-punctuation.html
new file mode 100644
index 0000000..a6568e2
--- /dev/null
+++ b/test/wpt/tests/FileAPI/file/send-file-form-punctuation.html
@@ -0,0 +1,226 @@
+<!DOCTYPE html>
+<meta charset="utf-8" />
+<title>Upload files named using punctuation</title>
+<link
+ rel="help"
+ href="https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#multipart-form-data"
+/>
+<link
+ rel="help"
+ href="https://html.spec.whatwg.org/multipage/dnd.html#datatransferitemlist"
+/>
+<link rel="help" href="https://w3c.github.io/FileAPI/#file-constructor" />
+<link
+ rel="author"
+ title="Benjamin C. Wiley Sittler"
+ href="mailto:bsittler@chromium.org"
+/>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../support/send-file-form-helper.js"></script>
+<script>
+ "use strict";
+
+ // These have characters that undergo processing in name=,
+ // filename=, and/or value; formPostFileUploadTest postprocesses
+ // expectedEncodedBaseName for these internally.
+
+ formPostFileUploadTest({
+ fileNameSource: "ASCII",
+ fileBaseName: "file-for-upload-in-form-QUOTATION-MARK-[\x22].txt",
+ formEncoding: "UTF-8",
+ expectedEncodedBaseName:
+ "file-for-upload-in-form-QUOTATION-MARK-[\x22].txt",
+ });
+
+ formPostFileUploadTest({
+ fileNameSource: "ASCII",
+ fileBaseName: '"file-for-upload-in-form-double-quoted.txt"',
+ formEncoding: "UTF-8",
+ expectedEncodedBaseName: '"file-for-upload-in-form-double-quoted.txt"',
+ });
+
+ formPostFileUploadTest({
+ fileNameSource: "ASCII",
+ fileBaseName: "file-for-upload-in-form-REVERSE-SOLIDUS-[\\].txt",
+ formEncoding: "UTF-8",
+ expectedEncodedBaseName:
+ "file-for-upload-in-form-REVERSE-SOLIDUS-[\\].txt",
+ });
+
+ // The rest should be passed through unmodified:
+
+ formPostFileUploadTest({
+ fileNameSource: "ASCII",
+ fileBaseName: "file-for-upload-in-form-EXCLAMATION-MARK-[!].txt",
+ formEncoding: "UTF-8",
+ expectedEncodedBaseName: "file-for-upload-in-form-EXCLAMATION-MARK-[!].txt",
+ });
+
+ formPostFileUploadTest({
+ fileNameSource: "ASCII",
+ fileBaseName: "file-for-upload-in-form-DOLLAR-SIGN-[$].txt",
+ formEncoding: "UTF-8",
+ expectedEncodedBaseName: "file-for-upload-in-form-DOLLAR-SIGN-[$].txt",
+ });
+
+ formPostFileUploadTest({
+ fileNameSource: "ASCII",
+ fileBaseName: "file-for-upload-in-form-PERCENT-SIGN-[%].txt",
+ formEncoding: "UTF-8",
+ expectedEncodedBaseName: "file-for-upload-in-form-PERCENT-SIGN-[%].txt",
+ });
+
+ formPostFileUploadTest({
+ fileNameSource: "ASCII",
+ fileBaseName: "file-for-upload-in-form-AMPERSAND-[&].txt",
+ formEncoding: "UTF-8",
+ expectedEncodedBaseName: "file-for-upload-in-form-AMPERSAND-[&].txt",
+ });
+
+ formPostFileUploadTest({
+ fileNameSource: "ASCII",
+ fileBaseName: "file-for-upload-in-form-APOSTROPHE-['].txt",
+ formEncoding: "UTF-8",
+ expectedEncodedBaseName: "file-for-upload-in-form-APOSTROPHE-['].txt",
+ });
+
+ formPostFileUploadTest({
+ fileNameSource: "ASCII",
+ fileBaseName: "file-for-upload-in-form-LEFT-PARENTHESIS-[(].txt",
+ formEncoding: "UTF-8",
+ expectedEncodedBaseName: "file-for-upload-in-form-LEFT-PARENTHESIS-[(].txt",
+ });
+
+ formPostFileUploadTest({
+ fileNameSource: "ASCII",
+ fileBaseName: "file-for-upload-in-form-RIGHT-PARENTHESIS-[)].txt",
+ formEncoding: "UTF-8",
+ expectedEncodedBaseName:
+ "file-for-upload-in-form-RIGHT-PARENTHESIS-[)].txt",
+ });
+
+ formPostFileUploadTest({
+ fileNameSource: "ASCII",
+ fileBaseName: "file-for-upload-in-form-ASTERISK-[*].txt",
+ formEncoding: "UTF-8",
+ expectedEncodedBaseName: "file-for-upload-in-form-ASTERISK-[*].txt",
+ });
+
+ formPostFileUploadTest({
+ fileNameSource: "ASCII",
+ fileBaseName: "file-for-upload-in-form-PLUS-SIGN-[+].txt",
+ formEncoding: "UTF-8",
+ expectedEncodedBaseName: "file-for-upload-in-form-PLUS-SIGN-[+].txt",
+ });
+
+ formPostFileUploadTest({
+ fileNameSource: "ASCII",
+ fileBaseName: "file-for-upload-in-form-COMMA-[,].txt",
+ formEncoding: "UTF-8",
+ expectedEncodedBaseName: "file-for-upload-in-form-COMMA-[,].txt",
+ });
+
+ formPostFileUploadTest({
+ fileNameSource: "ASCII",
+ fileBaseName: "file-for-upload-in-form-FULL-STOP-[.].txt",
+ formEncoding: "UTF-8",
+ expectedEncodedBaseName: "file-for-upload-in-form-FULL-STOP-[.].txt",
+ });
+
+ formPostFileUploadTest({
+ fileNameSource: "ASCII",
+ fileBaseName: "file-for-upload-in-form-SOLIDUS-[/].txt",
+ formEncoding: "UTF-8",
+ expectedEncodedBaseName: "file-for-upload-in-form-SOLIDUS-[/].txt",
+ });
+
+ formPostFileUploadTest({
+ fileNameSource: "ASCII",
+ fileBaseName: "file-for-upload-in-form-COLON-[:].txt",
+ formEncoding: "UTF-8",
+ expectedEncodedBaseName: "file-for-upload-in-form-COLON-[:].txt",
+ });
+
+ formPostFileUploadTest({
+ fileNameSource: "ASCII",
+ fileBaseName: "file-for-upload-in-form-SEMICOLON-[;].txt",
+ formEncoding: "UTF-8",
+ expectedEncodedBaseName: "file-for-upload-in-form-SEMICOLON-[;].txt",
+ });
+
+ formPostFileUploadTest({
+ fileNameSource: "ASCII",
+ fileBaseName: "file-for-upload-in-form-EQUALS-SIGN-[=].txt",
+ formEncoding: "UTF-8",
+ expectedEncodedBaseName: "file-for-upload-in-form-EQUALS-SIGN-[=].txt",
+ });
+
+ formPostFileUploadTest({
+ fileNameSource: "ASCII",
+ fileBaseName: "file-for-upload-in-form-QUESTION-MARK-[?].txt",
+ formEncoding: "UTF-8",
+ expectedEncodedBaseName: "file-for-upload-in-form-QUESTION-MARK-[?].txt",
+ });
+
+ formPostFileUploadTest({
+ fileNameSource: "ASCII",
+ fileBaseName: "file-for-upload-in-form-CIRCUMFLEX-ACCENT-[^].txt",
+ formEncoding: "UTF-8",
+ expectedEncodedBaseName:
+ "file-for-upload-in-form-CIRCUMFLEX-ACCENT-[^].txt",
+ });
+
+ formPostFileUploadTest({
+ fileNameSource: "ASCII",
+ fileBaseName: "file-for-upload-in-form-LEFT-SQUARE-BRACKET-[[].txt",
+ formEncoding: "UTF-8",
+ expectedEncodedBaseName:
+ "file-for-upload-in-form-LEFT-SQUARE-BRACKET-[[].txt",
+ });
+
+ formPostFileUploadTest({
+ fileNameSource: "ASCII",
+ fileBaseName: "file-for-upload-in-form-RIGHT-SQUARE-BRACKET-[]].txt",
+ formEncoding: "UTF-8",
+ expectedEncodedBaseName:
+ "file-for-upload-in-form-RIGHT-SQUARE-BRACKET-[]].txt",
+ });
+
+ formPostFileUploadTest({
+ fileNameSource: "ASCII",
+ fileBaseName: "file-for-upload-in-form-LEFT-CURLY-BRACKET-[{].txt",
+ formEncoding: "UTF-8",
+ expectedEncodedBaseName:
+ "file-for-upload-in-form-LEFT-CURLY-BRACKET-[{].txt",
+ });
+
+ formPostFileUploadTest({
+ fileNameSource: "ASCII",
+ fileBaseName: "file-for-upload-in-form-VERTICAL-LINE-[|].txt",
+ formEncoding: "UTF-8",
+ expectedEncodedBaseName: "file-for-upload-in-form-VERTICAL-LINE-[|].txt",
+ });
+
+ formPostFileUploadTest({
+ fileNameSource: "ASCII",
+ fileBaseName: "file-for-upload-in-form-RIGHT-CURLY-BRACKET-[}].txt",
+ formEncoding: "UTF-8",
+ expectedEncodedBaseName:
+ "file-for-upload-in-form-RIGHT-CURLY-BRACKET-[}].txt",
+ });
+
+ formPostFileUploadTest({
+ fileNameSource: "ASCII",
+ fileBaseName: "file-for-upload-in-form-TILDE-[~].txt",
+ formEncoding: "UTF-8",
+ expectedEncodedBaseName: "file-for-upload-in-form-TILDE-[~].txt",
+ });
+
+ formPostFileUploadTest({
+ fileNameSource: "ASCII",
+ fileBaseName: "'file-for-upload-in-form-single-quoted.txt'",
+ formEncoding: "UTF-8",
+ expectedEncodedBaseName: "'file-for-upload-in-form-single-quoted.txt'",
+ });
+</script>
diff --git a/test/wpt/tests/FileAPI/file/send-file-form-utf-8.html b/test/wpt/tests/FileAPI/file/send-file-form-utf-8.html
new file mode 100644
index 0000000..1be44f4
--- /dev/null
+++ b/test/wpt/tests/FileAPI/file/send-file-form-utf-8.html
@@ -0,0 +1,62 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Upload files in UTF-8 form</title>
+<link rel="help"
+ href="https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#multipart-form-data">
+<link rel="help"
+ href="https://html.spec.whatwg.org/multipage/dnd.html#datatransferitemlist">
+<link rel="help"
+ href="https://w3c.github.io/FileAPI/#file-constructor">
+<link rel="author" title="Benjamin C. Wiley Sittler"
+ href="mailto:bsittler@chromium.org">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../support/send-file-form-helper.js"></script>
+<script>
+'use strict';
+
+formPostFileUploadTest({
+ fileNameSource: 'ASCII',
+ fileBaseName: 'file-for-upload-in-form.txt',
+ formEncoding: 'UTF-8',
+ expectedEncodedBaseName: 'file-for-upload-in-form.txt',
+});
+
+formPostFileUploadTest({
+ fileNameSource: 'x-user-defined',
+ fileBaseName: 'file-for-upload-in-form-\uF7F0\uF793\uF783\uF7A0.txt',
+ formEncoding: 'UTF-8',
+ expectedEncodedBaseName: (
+ 'file-for-upload-in-form-\xEF\x9F\xB0\xEF\x9E\x93\xEF\x9E\x83\xEF\x9E\xA0.txt'),
+});
+
+formPostFileUploadTest({
+ fileNameSource: 'windows-1252',
+ fileBaseName: 'file-for-upload-in-form-☺😂.txt',
+ formEncoding: 'UTF-8',
+ expectedEncodedBaseName: (
+ 'file-for-upload-in-form-\xC3\xA2\xCB\x9C\xC2\xBA\xC3\xB0\xC5\xB8\xCB\x9C\xE2\x80\x9A.txt'),
+});
+
+formPostFileUploadTest({
+ fileNameSource: 'JIS X 0201 and JIS X 0208',
+ fileBaseName: 'file-for-upload-in-form-★星★.txt',
+ formEncoding: 'UTF-8',
+ expectedEncodedBaseName: 'file-for-upload-in-form-\xE2\x98\x85\xE6\x98\x9F\xE2\x98\x85.txt',
+});
+
+formPostFileUploadTest({
+ fileNameSource: 'Unicode',
+ fileBaseName: 'file-for-upload-in-form-☺😂.txt',
+ formEncoding: 'UTF-8',
+ expectedEncodedBaseName: 'file-for-upload-in-form-\xE2\x98\xBA\xF0\x9F\x98\x82.txt',
+});
+
+formPostFileUploadTest({
+ fileNameSource: 'Unicode',
+ fileBaseName: `file-for-upload-in-form-${kTestChars}.txt`,
+ formEncoding: 'UTF-8',
+ expectedEncodedBaseName: `file-for-upload-in-form-${kTestFallbackUtf8}.txt`,
+});
+
+</script>
diff --git a/test/wpt/tests/FileAPI/file/send-file-form-windows-1252.html b/test/wpt/tests/FileAPI/file/send-file-form-windows-1252.html
new file mode 100644
index 0000000..21b219f
--- /dev/null
+++ b/test/wpt/tests/FileAPI/file/send-file-form-windows-1252.html
@@ -0,0 +1,62 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Upload files in Windows-1252 form</title>
+<link rel="help"
+ href="https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#multipart-form-data">
+<link rel="help"
+ href="https://html.spec.whatwg.org/multipage/dnd.html#datatransferitemlist">
+<link rel="help"
+ href="https://w3c.github.io/FileAPI/#file-constructor">
+<link rel="author" title="Benjamin C. Wiley Sittler"
+ href="mailto:bsittler@chromium.org">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../support/send-file-form-helper.js"></script>
+<script>
+'use strict';
+
+formPostFileUploadTest({
+ fileNameSource: 'ASCII',
+ fileBaseName: 'file-for-upload-in-form.txt',
+ formEncoding: 'windows-1252',
+ expectedEncodedBaseName: 'file-for-upload-in-form.txt',
+});
+
+formPostFileUploadTest({
+ fileNameSource: 'x-user-defined',
+ fileBaseName: 'file-for-upload-in-form-\uF7F0\uF793\uF783\uF7A0.txt',
+ formEncoding: 'windows-1252',
+ expectedEncodedBaseName: 'file-for-upload-in-form-&#63472;&#63379;&#63363;&#63392;.txt',
+});
+
+formPostFileUploadTest({
+ fileNameSource: 'windows-1252',
+ fileBaseName: 'file-for-upload-in-form-☺😂.txt',
+ formEncoding: 'windows-1252',
+ expectedEncodedBaseName: 'file-for-upload-in-form-\xE2\x98\xBA\xF0\x9F\x98\x82.txt',
+});
+
+formPostFileUploadTest({
+ fileNameSource: 'JIS X 0201 and JIS X 0208',
+ fileBaseName: 'file-for-upload-in-form-★星★.txt',
+ formEncoding: 'windows-1252',
+ expectedEncodedBaseName: 'file-for-upload-in-form-&#9733;&#26143;&#9733;.txt',
+});
+
+formPostFileUploadTest({
+ fileNameSource: 'Unicode',
+ fileBaseName: 'file-for-upload-in-form-☺😂.txt',
+ formEncoding: 'windows-1252',
+ expectedEncodedBaseName: 'file-for-upload-in-form-&#9786;&#128514;.txt',
+});
+
+formPostFileUploadTest({
+ fileNameSource: 'Unicode',
+ fileBaseName: `file-for-upload-in-form-${kTestChars}.txt`,
+ formEncoding: 'windows-1252',
+ expectedEncodedBaseName: `file-for-upload-in-form-${
+ kTestFallbackWindows1252
+ }.txt`,
+});
+
+</script>
diff --git a/test/wpt/tests/FileAPI/file/send-file-form-x-user-defined.html b/test/wpt/tests/FileAPI/file/send-file-form-x-user-defined.html
new file mode 100644
index 0000000..8d6605d
--- /dev/null
+++ b/test/wpt/tests/FileAPI/file/send-file-form-x-user-defined.html
@@ -0,0 +1,63 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Upload files in x-user-defined form</title>
+<link rel="help"
+ href="https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#multipart-form-data">
+<link rel="help"
+ href="https://html.spec.whatwg.org/multipage/dnd.html#datatransferitemlist">
+<link rel="help"
+ href="https://w3c.github.io/FileAPI/#file-constructor">
+<link rel="author" title="Benjamin C. Wiley Sittler"
+ href="mailto:bsittler@chromium.org">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../support/send-file-form-helper.js"></script>
+<script>
+'use strict';
+
+formPostFileUploadTest({
+ fileNameSource: 'ASCII',
+ fileBaseName: 'file-for-upload-in-form.txt',
+ formEncoding: 'x-user-defined',
+ expectedEncodedBaseName: 'file-for-upload-in-form.txt',
+});
+
+formPostFileUploadTest({
+ fileNameSource: 'x-user-defined',
+ fileBaseName: 'file-for-upload-in-form-\uF7F0\uF793\uF783\uF7A0.txt',
+ formEncoding: 'x-user-defined',
+ expectedEncodedBaseName: 'file-for-upload-in-form-\xF0\x93\x83\xA0.txt',
+});
+
+formPostFileUploadTest({
+ fileNameSource: 'windows-1252',
+ fileBaseName: 'file-for-upload-in-form-☺😂.txt',
+ formEncoding: 'x-user-defined',
+ expectedEncodedBaseName: ('file-for-upload-in-form-' +
+ '&#226;&#732;&#186;&#240;&#376;&#732;&#8218;.txt'),
+});
+
+formPostFileUploadTest({
+ fileNameSource: 'JIS X 0201 and JIS X 0208',
+ fileBaseName: 'file-for-upload-in-form-★星★.txt',
+ formEncoding: 'x-user-defined',
+ expectedEncodedBaseName: 'file-for-upload-in-form-&#9733;&#26143;&#9733;.txt',
+});
+
+formPostFileUploadTest({
+ fileNameSource: 'Unicode',
+ fileBaseName: 'file-for-upload-in-form-☺😂.txt',
+ formEncoding: 'x-user-defined',
+ expectedEncodedBaseName: 'file-for-upload-in-form-&#9786;&#128514;.txt',
+});
+
+formPostFileUploadTest({
+ fileNameSource: 'Unicode',
+ fileBaseName: `file-for-upload-in-form-${kTestChars}.txt`,
+ formEncoding: 'x-user-defined',
+ expectedEncodedBaseName: `file-for-upload-in-form-${
+ kTestFallbackXUserDefined
+ }.txt`,
+});
+
+</script>
diff --git a/test/wpt/tests/FileAPI/file/send-file-form.html b/test/wpt/tests/FileAPI/file/send-file-form.html
new file mode 100644
index 0000000..baa8d42
--- /dev/null
+++ b/test/wpt/tests/FileAPI/file/send-file-form.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Upload ASCII-named file in UTF-8 form</title>
+<link rel="help"
+ href="https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#multipart-form-data">
+<link rel="help"
+ href="https://html.spec.whatwg.org/multipage/dnd.html#datatransferitemlist">
+<link rel="help"
+ href="https://w3c.github.io/FileAPI/#file-constructor">
+<link rel="author" title="Benjamin C. Wiley Sittler"
+ href="mailto:bsittler@chromium.org">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../support/send-file-form-helper.js"></script>
+<script>
+'use strict';
+
+formPostFileUploadTest({
+ fileNameSource: 'ASCII',
+ fileBaseName: 'file-for-upload-in-form.txt',
+ formEncoding: 'UTF-8',
+ expectedEncodedBaseName: 'file-for-upload-in-form.txt',
+});
+
+</script>
diff --git a/test/wpt/tests/FileAPI/file/send-file-formdata-controls.any.js b/test/wpt/tests/FileAPI/file/send-file-formdata-controls.any.js
new file mode 100644
index 0000000..e95d3aa
--- /dev/null
+++ b/test/wpt/tests/FileAPI/file/send-file-formdata-controls.any.js
@@ -0,0 +1,69 @@
+// META: title=FormData: FormData: Upload files named using controls
+// META: script=../support/send-file-formdata-helper.js
+ "use strict";
+
+ formDataPostFileUploadTest({
+ fileNameSource: "ASCII",
+ fileBaseName: "file-for-upload-in-form-NUL-[\0].txt",
+ });
+
+ formDataPostFileUploadTest({
+ fileNameSource: "ASCII",
+ fileBaseName: "file-for-upload-in-form-BS-[\b].txt",
+ });
+
+ formDataPostFileUploadTest({
+ fileNameSource: "ASCII",
+ fileBaseName: "file-for-upload-in-form-VT-[\v].txt",
+ });
+
+ // These have characters that undergo processing in name=,
+ // filename=, and/or value; formDataPostFileUploadTest postprocesses
+ // expectedEncodedBaseName for these internally.
+
+ formDataPostFileUploadTest({
+ fileNameSource: "ASCII",
+ fileBaseName: "file-for-upload-in-form-LF-[\n].txt",
+ });
+
+ formDataPostFileUploadTest({
+ fileNameSource: "ASCII",
+ fileBaseName: "file-for-upload-in-form-LF-CR-[\n\r].txt",
+ });
+
+ formDataPostFileUploadTest({
+ fileNameSource: "ASCII",
+ fileBaseName: "file-for-upload-in-form-CR-[\r].txt",
+ });
+
+ formDataPostFileUploadTest({
+ fileNameSource: "ASCII",
+ fileBaseName: "file-for-upload-in-form-CR-LF-[\r\n].txt",
+ });
+
+ formDataPostFileUploadTest({
+ fileNameSource: "ASCII",
+ fileBaseName: "file-for-upload-in-form-HT-[\t].txt",
+ });
+
+ formDataPostFileUploadTest({
+ fileNameSource: "ASCII",
+ fileBaseName: "file-for-upload-in-form-FF-[\f].txt",
+ });
+
+ formDataPostFileUploadTest({
+ fileNameSource: "ASCII",
+ fileBaseName: "file-for-upload-in-form-DEL-[\x7F].txt",
+ });
+
+ // The rest should be passed through unmodified:
+
+ formDataPostFileUploadTest({
+ fileNameSource: "ASCII",
+ fileBaseName: "file-for-upload-in-form-ESC-[\x1B].txt",
+ });
+
+ formDataPostFileUploadTest({
+ fileNameSource: "ASCII",
+ fileBaseName: "file-for-upload-in-form-SPACE-[ ].txt",
+ });
diff --git a/test/wpt/tests/FileAPI/file/send-file-formdata-punctuation.any.js b/test/wpt/tests/FileAPI/file/send-file-formdata-punctuation.any.js
new file mode 100644
index 0000000..987dba3
--- /dev/null
+++ b/test/wpt/tests/FileAPI/file/send-file-formdata-punctuation.any.js
@@ -0,0 +1,144 @@
+// META: title=FormData: FormData: Upload files named using punctuation
+// META: script=../support/send-file-formdata-helper.js
+ "use strict";
+
+ // These have characters that undergo processing in name=,
+ // filename=, and/or value; formDataPostFileUploadTest postprocesses
+ // expectedEncodedBaseName for these internally.
+
+ formDataPostFileUploadTest({
+ fileNameSource: "ASCII",
+ fileBaseName: "file-for-upload-in-form-QUOTATION-MARK-[\x22].txt",
+ });
+
+ formDataPostFileUploadTest({
+ fileNameSource: "ASCII",
+ fileBaseName: '"file-for-upload-in-form-double-quoted.txt"',
+ });
+
+ formDataPostFileUploadTest({
+ fileNameSource: "ASCII",
+ fileBaseName: "file-for-upload-in-form-REVERSE-SOLIDUS-[\\].txt",
+ });
+
+ // The rest should be passed through unmodified:
+
+ formDataPostFileUploadTest({
+ fileNameSource: "ASCII",
+ fileBaseName: "file-for-upload-in-form-EXCLAMATION-MARK-[!].txt",
+ });
+
+ formDataPostFileUploadTest({
+ fileNameSource: "ASCII",
+ fileBaseName: "file-for-upload-in-form-DOLLAR-SIGN-[$].txt",
+ });
+
+ formDataPostFileUploadTest({
+ fileNameSource: "ASCII",
+ fileBaseName: "file-for-upload-in-form-PERCENT-SIGN-[%].txt",
+ });
+
+ formDataPostFileUploadTest({
+ fileNameSource: "ASCII",
+ fileBaseName: "file-for-upload-in-form-AMPERSAND-[&].txt",
+ });
+
+ formDataPostFileUploadTest({
+ fileNameSource: "ASCII",
+ fileBaseName: "file-for-upload-in-form-APOSTROPHE-['].txt",
+ });
+
+ formDataPostFileUploadTest({
+ fileNameSource: "ASCII",
+ fileBaseName: "file-for-upload-in-form-LEFT-PARENTHESIS-[(].txt",
+ });
+
+ formDataPostFileUploadTest({
+ fileNameSource: "ASCII",
+ fileBaseName: "file-for-upload-in-form-RIGHT-PARENTHESIS-[)].txt",
+ });
+
+ formDataPostFileUploadTest({
+ fileNameSource: "ASCII",
+ fileBaseName: "file-for-upload-in-form-ASTERISK-[*].txt",
+ });
+
+ formDataPostFileUploadTest({
+ fileNameSource: "ASCII",
+ fileBaseName: "file-for-upload-in-form-PLUS-SIGN-[+].txt",
+ });
+
+ formDataPostFileUploadTest({
+ fileNameSource: "ASCII",
+ fileBaseName: "file-for-upload-in-form-COMMA-[,].txt",
+ });
+
+ formDataPostFileUploadTest({
+ fileNameSource: "ASCII",
+ fileBaseName: "file-for-upload-in-form-FULL-STOP-[.].txt",
+ });
+
+ formDataPostFileUploadTest({
+ fileNameSource: "ASCII",
+ fileBaseName: "file-for-upload-in-form-SOLIDUS-[/].txt",
+ });
+
+ formDataPostFileUploadTest({
+ fileNameSource: "ASCII",
+ fileBaseName: "file-for-upload-in-form-COLON-[:].txt",
+ });
+
+ formDataPostFileUploadTest({
+ fileNameSource: "ASCII",
+ fileBaseName: "file-for-upload-in-form-SEMICOLON-[;].txt",
+ });
+
+ formDataPostFileUploadTest({
+ fileNameSource: "ASCII",
+ fileBaseName: "file-for-upload-in-form-EQUALS-SIGN-[=].txt",
+ });
+
+ formDataPostFileUploadTest({
+ fileNameSource: "ASCII",
+ fileBaseName: "file-for-upload-in-form-QUESTION-MARK-[?].txt",
+ });
+
+ formDataPostFileUploadTest({
+ fileNameSource: "ASCII",
+ fileBaseName: "file-for-upload-in-form-CIRCUMFLEX-ACCENT-[^].txt",
+ });
+
+ formDataPostFileUploadTest({
+ fileNameSource: "ASCII",
+ fileBaseName: "file-for-upload-in-form-LEFT-SQUARE-BRACKET-[[].txt",
+ });
+
+ formDataPostFileUploadTest({
+ fileNameSource: "ASCII",
+ fileBaseName: "file-for-upload-in-form-RIGHT-SQUARE-BRACKET-[]].txt",
+ });
+
+ formDataPostFileUploadTest({
+ fileNameSource: "ASCII",
+ fileBaseName: "file-for-upload-in-form-LEFT-CURLY-BRACKET-[{].txt",
+ });
+
+ formDataPostFileUploadTest({
+ fileNameSource: "ASCII",
+ fileBaseName: "file-for-upload-in-form-VERTICAL-LINE-[|].txt",
+ });
+
+ formDataPostFileUploadTest({
+ fileNameSource: "ASCII",
+ fileBaseName: "file-for-upload-in-form-RIGHT-CURLY-BRACKET-[}].txt",
+ });
+
+ formDataPostFileUploadTest({
+ fileNameSource: "ASCII",
+ fileBaseName: "file-for-upload-in-form-TILDE-[~].txt",
+ });
+
+ formDataPostFileUploadTest({
+ fileNameSource: "ASCII",
+ fileBaseName: "'file-for-upload-in-form-single-quoted.txt'",
+ });
diff --git a/test/wpt/tests/FileAPI/file/send-file-formdata-utf-8.any.js b/test/wpt/tests/FileAPI/file/send-file-formdata-utf-8.any.js
new file mode 100644
index 0000000..b8bd74c
--- /dev/null
+++ b/test/wpt/tests/FileAPI/file/send-file-formdata-utf-8.any.js
@@ -0,0 +1,33 @@
+// META: title=FormData: FormData: Upload files in UTF-8 fetch()
+// META: script=../support/send-file-formdata-helper.js
+ "use strict";
+
+ formDataPostFileUploadTest({
+ fileNameSource: "ASCII",
+ fileBaseName: "file-for-upload-in-form.txt",
+ });
+
+ formDataPostFileUploadTest({
+ fileNameSource: "x-user-defined",
+ fileBaseName: "file-for-upload-in-form-\uF7F0\uF793\uF783\uF7A0.txt",
+ });
+
+ formDataPostFileUploadTest({
+ fileNameSource: "windows-1252",
+ fileBaseName: "file-for-upload-in-form-☺😂.txt",
+ });
+
+ formDataPostFileUploadTest({
+ fileNameSource: "JIS X 0201 and JIS X 0208",
+ fileBaseName: "file-for-upload-in-form-★星★.txt",
+ });
+
+ formDataPostFileUploadTest({
+ fileNameSource: "Unicode",
+ fileBaseName: "file-for-upload-in-form-☺😂.txt",
+ });
+
+ formDataPostFileUploadTest({
+ fileNameSource: "Unicode",
+ fileBaseName: `file-for-upload-in-form-${kTestChars}.txt`,
+ });
diff --git a/test/wpt/tests/FileAPI/file/send-file-formdata.any.js b/test/wpt/tests/FileAPI/file/send-file-formdata.any.js
new file mode 100644
index 0000000..e13a348
--- /dev/null
+++ b/test/wpt/tests/FileAPI/file/send-file-formdata.any.js
@@ -0,0 +1,8 @@
+// META: title=FormData: Upload ASCII-named file in UTF-8 form
+// META: script=../support/send-file-formdata-helper.js
+ "use strict";
+
+ formDataPostFileUploadTest({
+ fileNameSource: "ASCII",
+ fileBaseName: "file-for-upload-in-form.txt",
+ });
diff --git a/test/wpt/tests/FileAPI/fileReader.any.js b/test/wpt/tests/FileAPI/fileReader.any.js
new file mode 100644
index 0000000..2876dcb
--- /dev/null
+++ b/test/wpt/tests/FileAPI/fileReader.any.js
@@ -0,0 +1,59 @@
+// META: title=FileReader States
+
+'use strict';
+
+test(function () {
+ assert_true(
+ "FileReader" in globalThis,
+ "globalThis should have a FileReader property.",
+ );
+}, "FileReader interface object");
+
+test(function () {
+ var fileReader = new FileReader();
+ assert_true(fileReader instanceof FileReader);
+}, "no-argument FileReader constructor");
+
+var t_abort = async_test("FileReader States -- abort");
+t_abort.step(function () {
+ var fileReader = new FileReader();
+ assert_equals(fileReader.readyState, 0);
+ assert_equals(fileReader.readyState, FileReader.EMPTY);
+
+ var blob = new Blob();
+ fileReader.readAsArrayBuffer(blob);
+ assert_equals(fileReader.readyState, 1);
+ assert_equals(fileReader.readyState, FileReader.LOADING);
+
+ fileReader.onabort = this.step_func(function (e) {
+ assert_equals(fileReader.readyState, 2);
+ assert_equals(fileReader.readyState, FileReader.DONE);
+ t_abort.done();
+ });
+ fileReader.abort();
+ fileReader.onabort = this.unreached_func("abort event should fire sync");
+});
+
+var t_event = async_test("FileReader States -- events");
+t_event.step(function () {
+ var fileReader = new FileReader();
+
+ var blob = new Blob();
+ fileReader.readAsArrayBuffer(blob);
+
+ fileReader.onloadstart = this.step_func(function (e) {
+ assert_equals(fileReader.readyState, 1);
+ assert_equals(fileReader.readyState, FileReader.LOADING);
+ });
+
+ fileReader.onprogress = this.step_func(function (e) {
+ assert_equals(fileReader.readyState, 1);
+ assert_equals(fileReader.readyState, FileReader.LOADING);
+ });
+
+ fileReader.onloadend = this.step_func(function (e) {
+ assert_equals(fileReader.readyState, 2);
+ assert_equals(fileReader.readyState, FileReader.DONE);
+ t_event.done();
+ });
+});
diff --git a/test/wpt/tests/FileAPI/filelist-section/filelist.html b/test/wpt/tests/FileAPI/filelist-section/filelist.html
new file mode 100644
index 0000000..b97dcde
--- /dev/null
+++ b/test/wpt/tests/FileAPI/filelist-section/filelist.html
@@ -0,0 +1,57 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset='utf-8'>
+ <title>FileAPI Test: filelist</title>
+ <link rel='author' title='Intel' href='http://www.intel.com'>
+ <link rel='help' href='http://dev.w3.org/2006/webapi/FileAPI/#filelist-section'>
+ <link rel="help" href="http://dev.w3.org/2006/webapi/FileAPI/#dfn-length">
+ <link rel="help" href="http://dev.w3.org/2006/webapi/FileAPI/#dfn-item">
+ <script src='/resources/testharness.js'></script>
+ <script src='/resources/testharnessreport.js'></script>
+ </head>
+
+ <body>
+ <form name='uploadData' style="display:none">
+ <input type='file' id='fileChooser'>
+ </form>
+ <div id='log'></div>
+
+ <script>
+ var fileList;
+
+ setup(function () {
+ fileList = document.querySelector('#fileChooser').files;
+ });
+
+ test(function () {
+ assert_true('FileList' in window, 'window has a FileList property');
+ }, 'Check if window has a FileList property');
+
+ test(function () {
+ assert_equals(FileList.length, 0, 'FileList.length is 0');
+ }, 'Check if FileList.length is 0');
+
+ test(function () {
+ assert_true(fileList.item instanceof Function, 'item is a instanceof Function');
+ }, 'Check if item is a instanceof Function');
+
+ test(function() {
+ assert_inherits(fileList, 'item', 'item is a method of fileList');
+ }, 'Check if item is a method of fileList');
+
+ test(function() {
+ assert_equals(fileList.item(0), null, 'item method returns null');
+ }, 'Check if the item method returns null when no file selected');
+
+ test(function() {
+ assert_inherits(fileList, 'length', 'length is fileList attribute');
+ }, 'Check if length is fileList\'s attribute');
+
+ test(function() {
+ assert_equals(fileList.length, 0, 'fileList length is 0');
+ }, 'Check if the fileList length is 0 when no file selected');
+ </script>
+
+ </body>
+</html>
diff --git a/test/wpt/tests/FileAPI/filelist-section/filelist_multiple_selected_files-manual.html b/test/wpt/tests/FileAPI/filelist-section/filelist_multiple_selected_files-manual.html
new file mode 100644
index 0000000..2efaa05
--- /dev/null
+++ b/test/wpt/tests/FileAPI/filelist-section/filelist_multiple_selected_files-manual.html
@@ -0,0 +1,64 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset='utf-8'>
+ <title>FileAPI Test: filelist_multiple_selected_files</title>
+ <link rel='author' title='Intel' href='http://www.intel.com'>
+ <link rel='help' href='http://dev.w3.org/2006/webapi/FileAPI/#filelist-section'>
+ <link rel="help" href="http://dev.w3.org/2006/webapi/FileAPI/#dfn-length">
+ <link rel="help" href="http://dev.w3.org/2006/webapi/FileAPI/#dfn-item">
+ <script src='/resources/testharness.js'></script>
+ <script src='/resources/testharnessreport.js'></script>
+ </head>
+
+ <body>
+ <form name='uploadData'>
+ <input type='file' id='fileChooser' multiple>
+ </form>
+ <div>
+ <p>Test steps:</p>
+ <ol>
+ <li>Download <a href='support/upload.txt'>upload.txt</a>, <a href="support/upload.zip">upload.zip</a> to local.</li>
+ <li>Select the local two files (upload.txt, upload.zip) to run the test.</li>
+ </ol>
+ </div>
+
+ <div id='log'></div>
+
+ <script>
+ var fileInput = document.querySelector('#fileChooser');
+ var fileList;
+
+ setup({explicit_done: true, explicit_timeout: true});
+
+ on_event(fileInput, 'change', function(evt) {
+ test(function() {
+ fileList = document.querySelector('#fileChooser').files;
+ assert_equals(fileList.length, 2, 'fileList length is 2');
+ }, 'Check if the fileList length is 2 when selected two files');
+
+ test(function() {
+ fileList = document.querySelector('#fileChooser').files;
+ assert_true(fileList.item(0) instanceof File, 'item method is instanceof File');
+ }, 'Check if the item method returns the File interface when selected two files');
+
+ test(function() {
+ fileList = document.querySelector('#fileChooser').files;
+ assert_not_equals(fileList.item(1), null, 'item(1) is not null');
+ }, 'Check if item(1) is not null when selected two files. Index must be treated by user agents as value for the position of a File object in the FileList, with 0 representing the first file.');
+
+ test(function() {
+ fileList = document.querySelector('#fileChooser').files;
+ assert_equals(fileList.item(2), null, 'item(2) is null');
+ }, 'Check if item(2) is null when selected two files');
+
+ test(function() {
+ fileList = document.querySelector('#fileChooser').files;
+ assert_array_equals([fileList.item(0).name, fileList.item(1).name], ['upload.txt', 'upload.zip'], 'file name string is the name of selected files "upload.txt", "upload.zip"');
+ }, 'Check if the file name string is the name of selected files');
+
+ done();
+ });
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/FileAPI/filelist-section/filelist_selected_file-manual.html b/test/wpt/tests/FileAPI/filelist-section/filelist_selected_file-manual.html
new file mode 100644
index 0000000..966aadd
--- /dev/null
+++ b/test/wpt/tests/FileAPI/filelist-section/filelist_selected_file-manual.html
@@ -0,0 +1,64 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset='utf-8'>
+ <title>FileAPI Test: filelist_selected_file</title>
+ <link rel='author' title='Intel' href='http://www.intel.com'>
+ <link rel='help' href='http://dev.w3.org/2006/webapi/FileAPI/#filelist-section'>
+ <link rel="help" href="http://dev.w3.org/2006/webapi/FileAPI/#dfn-length">
+ <link rel="help" href="http://dev.w3.org/2006/webapi/FileAPI/#dfn-item">
+ <script src='/resources/testharness.js'></script>
+ <script src='/resources/testharnessreport.js'></script>
+ </head>
+
+ <body>
+ <form name='uploadData'>
+ <input type='file' id='fileChooser'>
+ </form>
+ <div>
+ <p>Test steps:</p>
+ <ol>
+ <li>Download <a href='support/upload.txt'>upload.txt</a> to local.</li>
+ <li>Select the local upload.txt file to run the test.</li>
+ </ol>
+ </div>
+
+ <div id='log'></div>
+
+ <script>
+ var fileInput = document.querySelector('#fileChooser');
+ var fileList;
+
+ setup({explicit_done: true, explicit_timeout: true});
+
+ on_event(fileInput, 'change', function(evt) {
+ test(function() {
+ fileList = document.querySelector('#fileChooser').files;
+ assert_equals(fileList.length, 1, 'fileList length is 1');
+ }, 'Check if the fileList length is 1 when selected one file');
+
+ test(function() {
+ fileList = document.querySelector('#fileChooser').files;
+ assert_true(fileList.item(0) instanceof File, 'item method is instanceof File');
+ }, 'Check if the item method returns the File interface when selected one file');
+
+ test(function() {
+ fileList = document.querySelector('#fileChooser').files;
+ assert_not_equals(fileList.item(0), null, 'item(0) is not null');
+ }, 'Check if item(0) is not null when selected one file. Index must be treated by user agents as value for the position of a File object in the FileList, with 0 representing the first file.');
+
+ test(function() {
+ fileList = document.querySelector('#fileChooser').files;
+ assert_equals(fileList.item(1), null, 'item(1) is null');
+ }, 'Check if item(1) is null when selected one file only');
+
+ test(function() {
+ fileList = document.querySelector('#fileChooser').files;
+ assert_equals(fileList.item(0).name, 'upload.txt', 'file name string is "upload.txt"');
+ }, 'Check if the file name string is the selected "upload.txt"');
+
+ done();
+ });
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/FileAPI/filelist-section/support/upload.txt b/test/wpt/tests/FileAPI/filelist-section/support/upload.txt
new file mode 100644
index 0000000..f45965b
--- /dev/null
+++ b/test/wpt/tests/FileAPI/filelist-section/support/upload.txt
@@ -0,0 +1 @@
+Hello, this is test file for file upload.
diff --git a/test/wpt/tests/FileAPI/filelist-section/support/upload.zip b/test/wpt/tests/FileAPI/filelist-section/support/upload.zip
new file mode 100644
index 0000000..a933d6a
--- /dev/null
+++ b/test/wpt/tests/FileAPI/filelist-section/support/upload.zip
Binary files differ
diff --git a/test/wpt/tests/FileAPI/historical.https.html b/test/wpt/tests/FileAPI/historical.https.html
new file mode 100644
index 0000000..4f841f1
--- /dev/null
+++ b/test/wpt/tests/FileAPI/historical.https.html
@@ -0,0 +1,65 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>Historical features</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script>
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ var removedFromWindow = [
+ 'toNativeLineEndings',
+ 'FileError',
+ 'FileException',
+ 'FileHandle',
+ 'FileRequest',
+ 'MutableFile',
+ ];
+
+ removedFromWindow.forEach(function(name) {
+ test(function() {
+ assert_false(name in window);
+ }, '"' + name + '" should not be supported');
+ });
+
+ test(function() {
+ var b = new Blob();
+ var prefixes = ['op', 'moz', 'webkit', 'ms'];
+ for (var i = 0; i < prefixes.length; ++i) {
+ assert_false(prefixes[i]+'Slice' in b, "'"+prefixes[i]+"Slice' in b");
+ assert_false(prefixes[i]+'Slice' in Blob.prototype, "'"+prefixes[i]+"Slice in Blob.prototype");
+ }
+ }, 'Blob should not support slice prefixed');
+
+ test(function() {
+ var prefixes = ['', 'O', 'Moz', 'WebKit', 'MS'];
+ for (var i = 0; i < prefixes.length; ++i) {
+ assert_false(prefixes[i]+'BlobBuilder' in window, prefixes[i]+'BlobBuilder');
+ }
+ }, 'BlobBuilder should not be supported.');
+
+ test(function() {
+ assert_false('createFor' in URL);
+ }, 'createFor method should not be supported');
+
+ test(function() {
+ var b = new Blob();
+ assert_false('close' in b, 'close in b');
+ assert_false('close' in Blob.prototype, 'close in Blob.prototype');
+ assert_false('isClosed' in b, 'isClosed in b');
+ assert_false('isClosed' in Blob.prototype, 'isClosed in Blob.prototype');
+ }, 'Blob.close() should not be supported');
+
+ test(() => {
+ const f = new File([], "");
+ assert_false("lastModifiedDate" in f);
+ assert_false("lastModifiedDate" in File.prototype);
+ }, "File's lastModifiedDate should not be supported");
+
+ service_worker_test('support/historical-serviceworker.js', 'Service worker test setup');
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/FileAPI/idlharness-manual.html b/test/wpt/tests/FileAPI/idlharness-manual.html
new file mode 100644
index 0000000..c1d8b0c
--- /dev/null
+++ b/test/wpt/tests/FileAPI/idlharness-manual.html
@@ -0,0 +1,45 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>File API manual IDL tests</title>
+ <link rel="author" title="Intel" href="http://www.intel.com">
+ <link rel="help" href="http://dev.w3.org/2006/webapi/FileAPI/#conformance">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/resources/WebIDLParser.js"></script>
+ <script src="/resources/idlharness.js"></script>
+ </head>
+ <body>
+ <h1>File API manual IDL tests</h1>
+
+ <p>Either download <a href="support/upload.txt">upload.txt</a> and select it below or select an
+ arbitrary local file.</p>
+
+ <form name="uploadData">
+ <input type="file" id="fileChooser">
+ </form>
+
+ <div id="log"></div>
+
+ <script>
+ const fileInput = document.querySelector("#fileChooser");
+
+ setup({explicit_timeout: true});
+
+ idl_test(
+ ['FileAPI'],
+ ['dom', 'html', 'url'],
+ async idl_array => {
+ await new Promise(resolve => {
+ on_event(fileInput, "change", resolve);
+ });
+ idl_array.add_objects({
+ FileList: [fileInput.files],
+ File: [fileInput.files[0]],
+ });
+ }
+ );
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/FileAPI/idlharness.any.js b/test/wpt/tests/FileAPI/idlharness.any.js
new file mode 100644
index 0000000..1744242
--- /dev/null
+++ b/test/wpt/tests/FileAPI/idlharness.any.js
@@ -0,0 +1,19 @@
+// META: script=/resources/WebIDLParser.js
+// META: script=/resources/idlharness.js
+// META: timeout=long
+
+'use strict';
+
+// https://w3c.github.io/FileAPI/
+
+idl_test(
+ ['FileAPI'],
+ ['dom', 'html', 'url'],
+ idl_array => {
+ idl_array.add_objects({
+ Blob: ['new Blob(["TEST"])'],
+ File: ['new File(["myFileBits"], "myFileName")'],
+ FileReader: ['new FileReader()']
+ });
+ }
+);
diff --git a/test/wpt/tests/FileAPI/idlharness.html b/test/wpt/tests/FileAPI/idlharness.html
new file mode 100644
index 0000000..45e8684
--- /dev/null
+++ b/test/wpt/tests/FileAPI/idlharness.html
@@ -0,0 +1,37 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>File API automated IDL tests (requiring dom)</title>
+ <link rel="author" title="Intel" href="http://www.intel.com">
+ <link rel="help" href="http://dev.w3.org/2006/webapi/FileAPI/#conformance">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/resources/WebIDLParser.js"></script>
+ <script src="/resources/idlharness.js"></script>
+ </head>
+ <body>
+ <h1>File API automated IDL tests</h1>
+
+ <div id="log"></div>
+
+ <form name="uploadData">
+ <input type="file" id="fileChooser">
+ </form>
+
+ <script>
+ 'use strict';
+
+ idl_test(
+ ['FileAPI'],
+ ['dom', 'html', 'url'],
+ idl_array => {
+ idl_array.add_objects({
+ FileList: ['document.querySelector("#fileChooser").files']
+ });
+ }
+ );
+ </script>
+
+ </body>
+</html>
diff --git a/test/wpt/tests/FileAPI/idlharness.worker.js b/test/wpt/tests/FileAPI/idlharness.worker.js
new file mode 100644
index 0000000..002aaed
--- /dev/null
+++ b/test/wpt/tests/FileAPI/idlharness.worker.js
@@ -0,0 +1,17 @@
+importScripts("/resources/testharness.js");
+importScripts("/resources/WebIDLParser.js", "/resources/idlharness.js");
+
+'use strict';
+
+// https://w3c.github.io/FileAPI/
+
+idl_test(
+ ['FileAPI'],
+ ['dom', 'html', 'url'],
+ idl_array => {
+ idl_array.add_objects({
+ FileReaderSync: ['new FileReaderSync()']
+ });
+ }
+);
+done();
diff --git a/test/wpt/tests/FileAPI/progress-manual.html b/test/wpt/tests/FileAPI/progress-manual.html
new file mode 100644
index 0000000..b2e03b3
--- /dev/null
+++ b/test/wpt/tests/FileAPI/progress-manual.html
@@ -0,0 +1,49 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Process Events for FileReader</title>
+<link rel=help href="http://dev.w3.org/2006/webapi/FileAPI/#event-handler-attributes-section">
+<link rel=author title="Jinks Zhao" href="mailto:jinks@maxthon.com">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+Please choose one file through this input below.<br>
+<input type="file" id="filer">
+<div id="log"></div>
+<script>
+var input, reader, progressEventCounter, progressEventTimeList,
+ lastProgressEventTime;
+setup(function() {
+ input = document.getElementById('filer');
+ reader = new FileReader();
+ progressEventCounter = 0;
+ progressEventTimeList = [];
+ lastProgressEventTime;
+}, { explicit_timeout: true });
+
+var t = async_test("FileReader progress events.")
+
+reader.onprogress = t.step_func(function () {
+ var newTime = new Date;
+ var timeout = newTime - lastProgressEventTime;
+
+ progressEventTimeList.push(timeout);
+ lastProgressEventTime = newTime;
+ progressEventCounter++;
+
+ assert_less_than_equal(timeout, 50, "The progress event should be fired every 50ms.");
+});
+
+reader.onload = t.step_func_done(function () {
+ assert_greater_than_equal(progressEventCounter, 1,
+ "When read completely, the progress event must be fired at least once.")
+});
+
+input.onchange = t.step_func(function () {
+ var files = input.files;
+
+ assert_greater_than(files.length, 0);
+ var file = files[0];
+
+ lastProgressEventTime = new Date;
+ reader.readAsArrayBuffer(file);
+});
+</script>
diff --git a/test/wpt/tests/FileAPI/reading-data-section/Determining-Encoding.any.js b/test/wpt/tests/FileAPI/reading-data-section/Determining-Encoding.any.js
new file mode 100644
index 0000000..5b69f7e
--- /dev/null
+++ b/test/wpt/tests/FileAPI/reading-data-section/Determining-Encoding.any.js
@@ -0,0 +1,81 @@
+// META: title=FileAPI Test: Blob Determining Encoding
+
+var t = async_test("Blob Determing Encoding with encoding argument");
+t.step(function() {
+ // string 'hello'
+ var data = [0xFE,0xFF,0x00,0x68,0x00,0x65,0x00,0x6C,0x00,0x6C,0x00,0x6F];
+ var blob = new Blob([new Uint8Array(data)]);
+ var reader = new FileReader();
+
+ reader.onloadend = t.step_func_done (function(event) {
+ assert_equals(this.result, "hello", "The FileReader should read the ArrayBuffer through UTF-16BE.")
+ }, reader);
+
+ reader.readAsText(blob, "UTF-16BE");
+});
+
+var t = async_test("Blob Determing Encoding with type attribute");
+t.step(function() {
+ var data = [0xFE,0xFF,0x00,0x68,0x00,0x65,0x00,0x6C,0x00,0x6C,0x00,0x6F];
+ var blob = new Blob([new Uint8Array(data)], {type:"text/plain;charset=UTF-16BE"});
+ var reader = new FileReader();
+
+ reader.onloadend = t.step_func_done (function(event) {
+ assert_equals(this.result, "hello", "The FileReader should read the ArrayBuffer through UTF-16BE.")
+ }, reader);
+
+ reader.readAsText(blob);
+});
+
+
+var t = async_test("Blob Determing Encoding with UTF-8 BOM");
+t.step(function() {
+ var data = [0xEF,0xBB,0xBF,0x68,0x65,0x6C,0x6C,0xC3,0xB6];
+ var blob = new Blob([new Uint8Array(data)]);
+ var reader = new FileReader();
+
+ reader.onloadend = t.step_func_done (function(event) {
+ assert_equals(this.result, "hellö", "The FileReader should read the blob with UTF-8.");
+ }, reader);
+
+ reader.readAsText(blob);
+});
+
+var t = async_test("Blob Determing Encoding without anything implying charset.");
+t.step(function() {
+ var data = [0x68,0x65,0x6C,0x6C,0xC3,0xB6];
+ var blob = new Blob([new Uint8Array(data)]);
+ var reader = new FileReader();
+
+ reader.onloadend = t.step_func_done (function(event) {
+ assert_equals(this.result, "hellö", "The FileReader should read the blob by default with UTF-8.");
+ }, reader);
+
+ reader.readAsText(blob);
+});
+
+var t = async_test("Blob Determing Encoding with UTF-16BE BOM");
+t.step(function() {
+ var data = [0xFE,0xFF,0x00,0x68,0x00,0x65,0x00,0x6C,0x00,0x6C,0x00,0x6F];
+ var blob = new Blob([new Uint8Array(data)]);
+ var reader = new FileReader();
+
+ reader.onloadend = t.step_func_done (function(event) {
+ assert_equals(this.result, "hello", "The FileReader should read the ArrayBuffer through UTF-16BE.");
+ }, reader);
+
+ reader.readAsText(blob);
+});
+
+var t = async_test("Blob Determing Encoding with UTF-16LE BOM");
+t.step(function() {
+ var data = [0xFF,0xFE,0x68,0x00,0x65,0x00,0x6C,0x00,0x6C,0x00,0x6F,0x00];
+ var blob = new Blob([new Uint8Array(data)]);
+ var reader = new FileReader();
+
+ reader.onloadend = t.step_func_done (function(event) {
+ assert_equals(this.result, "hello", "The FileReader should read the ArrayBuffer through UTF-16LE.");
+ }, reader);
+
+ reader.readAsText(blob);
+});
diff --git a/test/wpt/tests/FileAPI/reading-data-section/FileReader-event-handler-attributes.any.js b/test/wpt/tests/FileAPI/reading-data-section/FileReader-event-handler-attributes.any.js
new file mode 100644
index 0000000..fc71c64
--- /dev/null
+++ b/test/wpt/tests/FileAPI/reading-data-section/FileReader-event-handler-attributes.any.js
@@ -0,0 +1,17 @@
+// META: title=FileReader event handler attributes
+
+var attributes = [
+ "onloadstart",
+ "onprogress",
+ "onload",
+ "onabort",
+ "onerror",
+ "onloadend",
+];
+attributes.forEach(function(a) {
+ test(function() {
+ var reader = new FileReader();
+ assert_equals(reader[a], null,
+ "event handler attribute should initially be null");
+ }, "FileReader." + a + ": initial value");
+});
diff --git a/test/wpt/tests/FileAPI/reading-data-section/FileReader-multiple-reads.any.js b/test/wpt/tests/FileAPI/reading-data-section/FileReader-multiple-reads.any.js
new file mode 100644
index 0000000..4b19c69
--- /dev/null
+++ b/test/wpt/tests/FileAPI/reading-data-section/FileReader-multiple-reads.any.js
@@ -0,0 +1,81 @@
+// META: title=FileReader: starting new reads while one is in progress
+
+test(function() {
+ var blob_1 = new Blob(['TEST000000001'])
+ var blob_2 = new Blob(['TEST000000002'])
+ var reader = new FileReader();
+ reader.readAsText(blob_1)
+ assert_equals(reader.readyState, FileReader.LOADING, "readyState Must be LOADING")
+ assert_throws_dom("InvalidStateError", function () {
+ reader.readAsText(blob_2)
+ })
+}, 'test FileReader InvalidStateError exception for readAsText');
+
+test(function() {
+ var blob_1 = new Blob(['TEST000000001'])
+ var blob_2 = new Blob(['TEST000000002'])
+ var reader = new FileReader();
+ reader.readAsDataURL(blob_1)
+ assert_equals(reader.readyState, FileReader.LOADING, "readyState Must be LOADING")
+ assert_throws_dom("InvalidStateError", function () {
+ reader.readAsDataURL(blob_2)
+ })
+}, 'test FileReader InvalidStateError exception for readAsDataURL');
+
+test(function() {
+ var blob_1 = new Blob(['TEST000000001'])
+ var blob_2 = new Blob(['TEST000000002'])
+ var reader = new FileReader();
+ reader.readAsArrayBuffer(blob_1)
+ assert_equals(reader.readyState, FileReader.LOADING, "readyState Must be LOADING")
+ assert_throws_dom("InvalidStateError", function () {
+ reader.readAsArrayBuffer(blob_2)
+ })
+}, 'test FileReader InvalidStateError exception for readAsArrayBuffer');
+
+async_test(function() {
+ var blob_1 = new Blob(['TEST000000001'])
+ var blob_2 = new Blob(['TEST000000002'])
+ var reader = new FileReader();
+ var triggered = false;
+ reader.onloadstart = this.step_func_done(function() {
+ assert_false(triggered, "Only one loadstart event should be dispatched");
+ triggered = true;
+ assert_equals(reader.readyState, FileReader.LOADING,
+ "readyState must be LOADING")
+ assert_throws_dom("InvalidStateError", function () {
+ reader.readAsArrayBuffer(blob_2)
+ })
+ });
+ reader.readAsArrayBuffer(blob_1)
+ assert_equals(reader.readyState, FileReader.LOADING, "readyState Must be LOADING")
+}, 'test FileReader InvalidStateError exception in onloadstart event for readAsArrayBuffer');
+
+async_test(function() {
+ var blob_1 = new Blob(['TEST000000001'])
+ var blob_2 = new Blob(['TEST000000002'])
+ var reader = new FileReader();
+ reader.onloadend = this.step_func_done(function() {
+ assert_equals(reader.readyState, FileReader.DONE,
+ "readyState must be DONE")
+ reader.readAsArrayBuffer(blob_2)
+ assert_equals(reader.readyState, FileReader.LOADING, "readyState Must be LOADING")
+ });
+ reader.readAsArrayBuffer(blob_1)
+ assert_equals(reader.readyState, FileReader.LOADING, "readyState Must be LOADING")
+}, 'test FileReader no InvalidStateError exception in loadend event handler for readAsArrayBuffer');
+
+async_test(function() {
+ var blob_1 = new Blob([new Uint8Array(0x414141)]);
+ var blob_2 = new Blob(['TEST000000002']);
+ var reader = new FileReader();
+ reader.onloadstart = this.step_func(function() {
+ reader.abort();
+ reader.onloadstart = null;
+ reader.onloadend = this.step_func_done(function() {
+ assert_equals('TEST000000002', reader.result);
+ });
+ reader.readAsText(blob_2);
+ });
+ reader.readAsText(blob_1);
+}, 'test abort and restart in onloadstart event for readAsText');
diff --git a/test/wpt/tests/FileAPI/reading-data-section/filereader_abort.any.js b/test/wpt/tests/FileAPI/reading-data-section/filereader_abort.any.js
new file mode 100644
index 0000000..c778ae5
--- /dev/null
+++ b/test/wpt/tests/FileAPI/reading-data-section/filereader_abort.any.js
@@ -0,0 +1,38 @@
+// META: title=FileAPI Test: filereader_abort
+
+ test(function() {
+ var readerNoRead = new FileReader();
+ readerNoRead.abort();
+ assert_equals(readerNoRead.readyState, readerNoRead.EMPTY);
+ assert_equals(readerNoRead.result, null);
+ }, "Aborting before read");
+
+ promise_test(t => {
+ var blob = new Blob(["TEST THE ABORT METHOD"]);
+ var readerAbort = new FileReader();
+
+ var eventWatcher = new EventWatcher(t, readerAbort,
+ ['abort', 'loadstart', 'loadend', 'error', 'load']);
+
+ // EventWatcher doesn't let us inspect the state after the abort event,
+ // so add an extra event handler for that.
+ readerAbort.addEventListener('abort', t.step_func(e => {
+ assert_equals(readerAbort.readyState, readerAbort.DONE);
+ }));
+
+ readerAbort.readAsText(blob);
+ return eventWatcher.wait_for('loadstart')
+ .then(() => {
+ assert_equals(readerAbort.readyState, readerAbort.LOADING);
+ // 'abort' and 'loadend' events are dispatched synchronously, so
+ // call wait_for before calling abort.
+ var nextEvent = eventWatcher.wait_for(['abort', 'loadend']);
+ readerAbort.abort();
+ return nextEvent;
+ })
+ .then(() => {
+ // https://www.w3.org/Bugs/Public/show_bug.cgi?id=24401
+ assert_equals(readerAbort.result, null);
+ assert_equals(readerAbort.readyState, readerAbort.DONE);
+ });
+ }, "Aborting after read");
diff --git a/test/wpt/tests/FileAPI/reading-data-section/filereader_error.any.js b/test/wpt/tests/FileAPI/reading-data-section/filereader_error.any.js
new file mode 100644
index 0000000..9845962
--- /dev/null
+++ b/test/wpt/tests/FileAPI/reading-data-section/filereader_error.any.js
@@ -0,0 +1,19 @@
+// META: title=FileAPI Test: filereader_error
+
+ async_test(function() {
+ var blob = new Blob(["TEST THE ERROR ATTRIBUTE AND ERROR EVENT"]);
+ var reader = new FileReader();
+ assert_equals(reader.error, null, "The error is null when no error occurred");
+
+ reader.onload = this.step_func(function(evt) {
+ assert_unreached("Should not dispatch the load event");
+ });
+
+ reader.onloadend = this.step_func(function(evt) {
+ assert_equals(reader.result, null, "The result is null");
+ this.done();
+ });
+
+ reader.readAsText(blob);
+ reader.abort();
+ });
diff --git a/test/wpt/tests/FileAPI/reading-data-section/filereader_events.any.js b/test/wpt/tests/FileAPI/reading-data-section/filereader_events.any.js
new file mode 100644
index 0000000..ac69290
--- /dev/null
+++ b/test/wpt/tests/FileAPI/reading-data-section/filereader_events.any.js
@@ -0,0 +1,19 @@
+promise_test(async t => {
+ var reader = new FileReader();
+ var eventWatcher = new EventWatcher(t, reader, ['loadstart', 'progress', 'abort', 'error', 'load', 'loadend']);
+ reader.readAsText(new Blob([]));
+ await eventWatcher.wait_for('loadstart');
+ // No progress event for an empty blob, as no data is loaded.
+ await eventWatcher.wait_for('load');
+ await eventWatcher.wait_for('loadend');
+}, 'events are dispatched in the correct order for an empty blob');
+
+promise_test(async t => {
+ var reader = new FileReader();
+ var eventWatcher = new EventWatcher(t, reader, ['loadstart', 'progress', 'abort', 'error', 'load', 'loadend']);
+ reader.readAsText(new Blob(['a']));
+ await eventWatcher.wait_for('loadstart');
+ await eventWatcher.wait_for('progress');
+ await eventWatcher.wait_for('load');
+ await eventWatcher.wait_for('loadend');
+}, 'events are dispatched in the correct order for a non-empty blob');
diff --git a/test/wpt/tests/FileAPI/reading-data-section/filereader_file-manual.html b/test/wpt/tests/FileAPI/reading-data-section/filereader_file-manual.html
new file mode 100644
index 0000000..702ca9a
--- /dev/null
+++ b/test/wpt/tests/FileAPI/reading-data-section/filereader_file-manual.html
@@ -0,0 +1,69 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>FileAPI Test: filereader_file</title>
+ <link rel="author" title="Intel" href="http://www.intel.com">
+ <link rel="help" href="http://dev.w3.org/2006/webapi/FileAPI/#FileReader-interface">
+ <link rel="help" href="http://dev.w3.org/2006/webapi/FileAPI/#file">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ </head>
+ <body>
+ <div>
+ <p>Test step:</p>
+ <ol>
+ <li>Download <a href="support/blue-100x100.png">blue-100x100.png</a> to local.</li>
+ <li>Select the local file (blue-100x100.png) to run the test.</li>
+ </ol>
+ </div>
+
+ <form name="uploadData">
+ <input type="file" id="fileChooser">
+ </form>
+
+ <div id="log"></div>
+ <script>
+ var fileInput = document.querySelector('#fileChooser');
+ var reader = new FileReader();
+
+ //readType: 1-> ArrayBuffer, 2-> Text, 3-> DataURL
+ var readType = 1;
+
+ setup({
+ explicit_done: true,
+ explicit_timeout: true,
+ });
+
+ on_event(fileInput, "change", function(evt) {
+ reader.readAsArrayBuffer(fileInput.files[0]);
+ });
+
+ on_event(reader, "load", function(evt) {
+ if (readType == 1) {
+ test(function() {
+ assert_true(reader.result instanceof ArrayBuffer, "The result is instanceof ArrayBuffer");
+ }, "Check if the readAsArrayBuffer works");
+
+ readType++;
+ reader.readAsText(fileInput.files[0]);
+ } else if (readType == 2) {
+ test(function() {
+ assert_equals(typeof reader.result, "string", "The result is typeof string");
+ }, "Check if the readAsText works");
+
+ readType++;
+ reader.readAsDataURL(fileInput.files[0]);
+ } else if (readType == 3) {
+ test(function() {
+ assert_equals(typeof reader.result, "string", "The result is typeof string");
+ assert_equals(reader.result.indexOf("data"), 0, "The result starts with 'data'");
+ assert_true(reader.result.indexOf("base64") > 0, "The result contains 'base64'");
+ }, "Check if the readAsDataURL works");
+
+ done();
+ }
+ });
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/FileAPI/reading-data-section/filereader_file_img-manual.html b/test/wpt/tests/FileAPI/reading-data-section/filereader_file_img-manual.html
new file mode 100644
index 0000000..fca42c7
--- /dev/null
+++ b/test/wpt/tests/FileAPI/reading-data-section/filereader_file_img-manual.html
@@ -0,0 +1,47 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>FileAPI Test: filereader_file_img</title>
+ <link rel="author" title="Intel" href="http://www.intel.com">
+ <link rel="help" href="http://dev.w3.org/2006/webapi/FileAPI/#FileReader-interface">
+ <link rel="help" href="http://dev.w3.org/2006/webapi/FileAPI/#file">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ </head>
+ <body>
+ <div>
+ <p>Test step:</p>
+ <ol>
+ <li>Download <a href="support/blue-100x100.png">blue-100x100.png</a> to local.</li>
+ <li>Select the local file (blue-100x100.png) to run the test.</li>
+ </ol>
+ </div>
+
+ <form name="uploadData">
+ <input type="file" id="fileChooser">
+ </form>
+
+ <div id="log"></div>
+ <script>
+ var fileInput = document.querySelector('#fileChooser');
+ var reader = new FileReader();
+
+ setup({
+ explicit_done: true,
+ explicit_timeout: true,
+ });
+
+ fileInput.addEventListener("change", function(evt) {
+ reader.readAsDataURL(fileInput.files[0]);
+ }, false);
+
+ reader.addEventListener("loadend", function(evt) {
+ test(function () {
+ assert_true(reader.result.indexOf("iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAYAAABw4pVUAAAAqklEQVR42u3RsREAMAgDMe+/M4E7ZkhBoeI9gJWkWpfaeToTECACAkRAgAgIEAEB4gQgAgJEQIAICBABASIgAgJEQIAICBABASIgAgJEQIAICBABASIgAgJEQIAICBABASIgAgJEQIAICBABASIgAgJEQIAICBABASIgAgJEQIAICBABASIgAgJEQIAICBABASIgAgJEQIAICBABASIgQJwARECACAgQ/W4AQauujc8IdAoAAAAASUVORK5CYII=") != -1, "Encoded image")
+ }, "Check if readAsDataURL returns correct image");
+ done();
+ }, false);
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/FileAPI/reading-data-section/filereader_readAsArrayBuffer.any.js b/test/wpt/tests/FileAPI/reading-data-section/filereader_readAsArrayBuffer.any.js
new file mode 100644
index 0000000..d06e317
--- /dev/null
+++ b/test/wpt/tests/FileAPI/reading-data-section/filereader_readAsArrayBuffer.any.js
@@ -0,0 +1,23 @@
+// META: title=FileAPI Test: filereader_readAsArrayBuffer
+
+ async_test(function() {
+ var blob = new Blob(["TEST"]);
+ var reader = new FileReader();
+
+ reader.onload = this.step_func(function(evt) {
+ assert_equals(reader.result.byteLength, 4, "The byteLength is 4");
+ assert_true(reader.result instanceof ArrayBuffer, "The result is instanceof ArrayBuffer");
+ assert_equals(reader.readyState, reader.DONE);
+ this.done();
+ });
+
+ reader.onloadstart = this.step_func(function(evt) {
+ assert_equals(reader.readyState, reader.LOADING);
+ });
+
+ reader.onprogress = this.step_func(function(evt) {
+ assert_equals(reader.readyState, reader.LOADING);
+ });
+
+ reader.readAsArrayBuffer(blob);
+ });
diff --git a/test/wpt/tests/FileAPI/reading-data-section/filereader_readAsBinaryString.any.js b/test/wpt/tests/FileAPI/reading-data-section/filereader_readAsBinaryString.any.js
new file mode 100644
index 0000000..e69ff15
--- /dev/null
+++ b/test/wpt/tests/FileAPI/reading-data-section/filereader_readAsBinaryString.any.js
@@ -0,0 +1,23 @@
+// META: title=FileAPI Test: filereader_readAsBinaryString
+
+async_test(t => {
+ const blob = new Blob(["σ"]);
+ const reader = new FileReader();
+
+ reader.onload = t.step_func_done(() => {
+ assert_equals(typeof reader.result, "string", "The result is string");
+ assert_equals(reader.result.length, 2, "The result length is 2");
+ assert_equals(reader.result, "\xcf\x83", "The result is \xcf\x83");
+ assert_equals(reader.readyState, reader.DONE);
+ });
+
+ reader.onloadstart = t.step_func(() => {
+ assert_equals(reader.readyState, reader.LOADING);
+ });
+
+ reader.onprogress = t.step_func(() => {
+ assert_equals(reader.readyState, reader.LOADING);
+ });
+
+ reader.readAsBinaryString(blob);
+});
diff --git a/test/wpt/tests/FileAPI/reading-data-section/filereader_readAsDataURL.any.js b/test/wpt/tests/FileAPI/reading-data-section/filereader_readAsDataURL.any.js
new file mode 100644
index 0000000..4f9dbf7
--- /dev/null
+++ b/test/wpt/tests/FileAPI/reading-data-section/filereader_readAsDataURL.any.js
@@ -0,0 +1,54 @@
+// META: title=FileAPI Test: FileReader.readAsDataURL
+
+async_test(function(testCase) {
+ var blob = new Blob(["TEST"]);
+ var reader = new FileReader();
+
+ reader.onload = this.step_func(function(evt) {
+ assert_equals(reader.readyState, reader.DONE);
+ testCase.done();
+ });
+ reader.onloadstart = this.step_func(function(evt) {
+ assert_equals(reader.readyState, reader.LOADING);
+ });
+ reader.onprogress = this.step_func(function(evt) {
+ assert_equals(reader.readyState, reader.LOADING);
+ });
+
+ reader.readAsDataURL(blob);
+}, 'FileReader readyState during readAsDataURL');
+
+async_test(function(testCase) {
+ var blob = new Blob(["TEST"], { type: 'text/plain' });
+ var reader = new FileReader();
+
+ reader.onload = this.step_func(function() {
+ assert_equals(reader.result, "data:text/plain;base64,VEVTVA==");
+ testCase.done();
+ });
+ reader.readAsDataURL(blob);
+}, 'readAsDataURL result for Blob with specified MIME type');
+
+async_test(function(testCase) {
+ var blob = new Blob(["TEST"]);
+ var reader = new FileReader();
+
+ reader.onload = this.step_func(function() {
+ assert_equals(reader.result,
+ "data:application/octet-stream;base64,VEVTVA==");
+ testCase.done();
+ });
+ reader.readAsDataURL(blob);
+}, 'readAsDataURL result for Blob with unspecified MIME type');
+
+async_test(function(testCase) {
+ var blob = new Blob([]);
+ var reader = new FileReader();
+
+ reader.onload = this.step_func(function() {
+ assert_equals(reader.result,
+ "data:application/octet-stream;base64,");
+ testCase.done();
+ });
+ reader.readAsDataURL(blob);
+}, 'readAsDataURL result for empty Blob'); \ No newline at end of file
diff --git a/test/wpt/tests/FileAPI/reading-data-section/filereader_readAsText.any.js b/test/wpt/tests/FileAPI/reading-data-section/filereader_readAsText.any.js
new file mode 100644
index 0000000..4d0fa11
--- /dev/null
+++ b/test/wpt/tests/FileAPI/reading-data-section/filereader_readAsText.any.js
@@ -0,0 +1,36 @@
+// META: title=FileAPI Test: filereader_readAsText
+
+ async_test(function() {
+ var blob = new Blob(["TEST"]);
+ var reader = new FileReader();
+
+ reader.onload = this.step_func(function(evt) {
+ assert_equals(typeof reader.result, "string", "The result is typeof string");
+ assert_equals(reader.result, "TEST", "The result is TEST");
+ this.done();
+ });
+
+ reader.onloadstart = this.step_func(function(evt) {
+ assert_equals(reader.readyState, reader.LOADING, "The readyState");
+ });
+
+ reader.onprogress = this.step_func(function(evt) {
+ assert_equals(reader.readyState, reader.LOADING);
+ });
+
+ reader.readAsText(blob);
+ }, "readAsText should correctly read UTF-8.");
+
+ async_test(function() {
+ var blob = new Blob(["TEST"]);
+ var reader = new FileReader();
+ var reader_UTF16 = new FileReader();
+ reader_UTF16.onload = this.step_func(function(evt) {
+ // "TEST" in UTF-8 is 0x54 0x45 0x53 0x54.
+ // Decoded as utf-16 (little-endian), we get 0x4554 0x5453.
+ assert_equals(reader_UTF16.readyState, reader.DONE, "The readyState");
+ assert_equals(reader_UTF16.result, "\u4554\u5453", "The result is not TEST");
+ this.done();
+ });
+ reader_UTF16.readAsText(blob, "UTF-16");
+ }, "readAsText should correctly read UTF-16.");
diff --git a/test/wpt/tests/FileAPI/reading-data-section/filereader_readystate.any.js b/test/wpt/tests/FileAPI/reading-data-section/filereader_readystate.any.js
new file mode 100644
index 0000000..3cb36ab
--- /dev/null
+++ b/test/wpt/tests/FileAPI/reading-data-section/filereader_readystate.any.js
@@ -0,0 +1,19 @@
+// META: title=FileAPI Test: filereader_readystate
+
+ async_test(function() {
+ var blob = new Blob(["THIS TEST THE READYSTATE WHEN READ BLOB"]);
+ var reader = new FileReader();
+
+ assert_equals(reader.readyState, reader.EMPTY);
+
+ reader.onloadstart = this.step_func(function(evt) {
+ assert_equals(reader.readyState, reader.LOADING);
+ });
+
+ reader.onloadend = this.step_func(function(evt) {
+ assert_equals(reader.readyState, reader.DONE);
+ this.done();
+ });
+
+ reader.readAsDataURL(blob);
+ });
diff --git a/test/wpt/tests/FileAPI/reading-data-section/filereader_result.any.js b/test/wpt/tests/FileAPI/reading-data-section/filereader_result.any.js
new file mode 100644
index 0000000..28c068b
--- /dev/null
+++ b/test/wpt/tests/FileAPI/reading-data-section/filereader_result.any.js
@@ -0,0 +1,82 @@
+// META: title=FileAPI Test: filereader_result
+
+ var blob, blob2;
+ setup(function() {
+ blob = new Blob(["This test the result attribute"]);
+ blob2 = new Blob(["This is a second blob"]);
+ });
+
+ async_test(function() {
+ var readText = new FileReader();
+ assert_equals(readText.result, null);
+
+ readText.onloadend = this.step_func(function(evt) {
+ assert_equals(typeof readText.result, "string", "The result type is string");
+ assert_equals(readText.result, "This test the result attribute", "The result is correct");
+ this.done();
+ });
+
+ readText.readAsText(blob);
+ }, "readAsText");
+
+ async_test(function() {
+ var readDataURL = new FileReader();
+ assert_equals(readDataURL.result, null);
+
+ readDataURL.onloadend = this.step_func(function(evt) {
+ assert_equals(typeof readDataURL.result, "string", "The result type is string");
+ assert_true(readDataURL.result.indexOf("VGhpcyB0ZXN0IHRoZSByZXN1bHQgYXR0cmlidXRl") != -1, "return the right base64 string");
+ this.done();
+ });
+
+ readDataURL.readAsDataURL(blob);
+ }, "readAsDataURL");
+
+ async_test(function() {
+ var readArrayBuffer = new FileReader();
+ assert_equals(readArrayBuffer.result, null);
+
+ readArrayBuffer.onloadend = this.step_func(function(evt) {
+ assert_true(readArrayBuffer.result instanceof ArrayBuffer, "The result is instanceof ArrayBuffer");
+ this.done();
+ });
+
+ readArrayBuffer.readAsArrayBuffer(blob);
+ }, "readAsArrayBuffer");
+
+ async_test(function() {
+ var readBinaryString = new FileReader();
+ assert_equals(readBinaryString.result, null);
+
+ readBinaryString.onloadend = this.step_func(function(evt) {
+ assert_equals(typeof readBinaryString.result, "string", "The result type is string");
+ assert_equals(readBinaryString.result, "This test the result attribute", "The result is correct");
+ this.done();
+ });
+
+ readBinaryString.readAsBinaryString(blob);
+ }, "readAsBinaryString");
+
+
+ for (let event of ['loadstart', 'progress']) {
+ for (let method of ['readAsText', 'readAsDataURL', 'readAsArrayBuffer', 'readAsBinaryString']) {
+ promise_test(async function(t) {
+ var reader = new FileReader();
+ assert_equals(reader.result, null, 'result is null before read');
+
+ var eventWatcher = new EventWatcher(t, reader,
+ [event, 'loadend']);
+
+ reader[method](blob);
+ assert_equals(reader.result, null, 'result is null after first read call');
+ await eventWatcher.wait_for(event);
+ assert_equals(reader.result, null, 'result is null during event');
+ await eventWatcher.wait_for('loadend');
+ assert_not_equals(reader.result, null);
+ reader[method](blob);
+ assert_equals(reader.result, null, 'result is null after second read call');
+ await eventWatcher.wait_for(event);
+ assert_equals(reader.result, null, 'result is null during second read event');
+ }, 'result is null during "' + event + '" event for ' + method);
+ }
+ }
diff --git a/test/wpt/tests/FileAPI/reading-data-section/support/blue-100x100.png b/test/wpt/tests/FileAPI/reading-data-section/support/blue-100x100.png
new file mode 100644
index 0000000..5748719
--- /dev/null
+++ b/test/wpt/tests/FileAPI/reading-data-section/support/blue-100x100.png
Binary files differ
diff --git a/test/wpt/tests/FileAPI/support/Blob.js b/test/wpt/tests/FileAPI/support/Blob.js
new file mode 100644
index 0000000..2c24974
--- /dev/null
+++ b/test/wpt/tests/FileAPI/support/Blob.js
@@ -0,0 +1,70 @@
+'use strict'
+
+self.test_blob = (fn, expectations) => {
+ var expected = expectations.expected,
+ type = expectations.type,
+ desc = expectations.desc;
+
+ var t = async_test(desc);
+ t.step(function() {
+ var blob = fn();
+ assert_true(blob instanceof Blob);
+ assert_false(blob instanceof File);
+ assert_equals(blob.type, type);
+ assert_equals(blob.size, expected.length);
+
+ var fr = new FileReader();
+ fr.onload = t.step_func_done(function(event) {
+ assert_equals(this.result, expected);
+ }, fr);
+ fr.onerror = t.step_func(function(e) {
+ assert_unreached("got error event on FileReader");
+ });
+ fr.readAsText(blob, "UTF-8");
+ });
+}
+
+self.test_blob_binary = (fn, expectations) => {
+ var expected = expectations.expected,
+ type = expectations.type,
+ desc = expectations.desc;
+
+ var t = async_test(desc);
+ t.step(function() {
+ var blob = fn();
+ assert_true(blob instanceof Blob);
+ assert_false(blob instanceof File);
+ assert_equals(blob.type, type);
+ assert_equals(blob.size, expected.length);
+
+ var fr = new FileReader();
+ fr.onload = t.step_func_done(function(event) {
+ assert_true(this.result instanceof ArrayBuffer,
+ "Result should be an ArrayBuffer");
+ assert_array_equals(new Uint8Array(this.result), expected);
+ }, fr);
+ fr.onerror = t.step_func(function(e) {
+ assert_unreached("got error event on FileReader");
+ });
+ fr.readAsArrayBuffer(blob);
+ });
+}
+
+// Assert that two TypedArray objects have the same byte values
+self.assert_equals_typed_array = (array1, array2) => {
+ const [view1, view2] = [array1, array2].map((array) => {
+ assert_true(array.buffer instanceof ArrayBuffer,
+ 'Expect input ArrayBuffers to contain field `buffer`');
+ return new DataView(array.buffer, array.byteOffset, array.byteLength);
+ });
+
+ assert_equals(view1.byteLength, view2.byteLength,
+ 'Expect both arrays to be of the same byte length');
+
+ const byteLength = view1.byteLength;
+
+ for (let i = 0; i < byteLength; ++i) {
+ assert_equals(view1.getUint8(i), view2.getUint8(i),
+ `Expect byte at buffer position ${i} to be equal`);
+ }
+}
diff --git a/test/wpt/tests/FileAPI/support/document-domain-setter.sub.html b/test/wpt/tests/FileAPI/support/document-domain-setter.sub.html
new file mode 100644
index 0000000..61aebdf
--- /dev/null
+++ b/test/wpt/tests/FileAPI/support/document-domain-setter.sub.html
@@ -0,0 +1,7 @@
+<!DOCTYPE html>
+<title>Relevant/current/blob source page used as a test helper</title>
+
+<script>
+"use strict";
+document.domain = "{{host}}";
+</script>
diff --git a/test/wpt/tests/FileAPI/support/empty-document.html b/test/wpt/tests/FileAPI/support/empty-document.html
new file mode 100644
index 0000000..b9cd130
--- /dev/null
+++ b/test/wpt/tests/FileAPI/support/empty-document.html
@@ -0,0 +1,3 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<body>
diff --git a/test/wpt/tests/FileAPI/support/historical-serviceworker.js b/test/wpt/tests/FileAPI/support/historical-serviceworker.js
new file mode 100644
index 0000000..8bd89a2
--- /dev/null
+++ b/test/wpt/tests/FileAPI/support/historical-serviceworker.js
@@ -0,0 +1,5 @@
+importScripts('/resources/testharness.js');
+
+test(() => {
+ assert_false('FileReaderSync' in self);
+}, '"FileReaderSync" should not be supported in service workers');
diff --git a/test/wpt/tests/FileAPI/support/incumbent.sub.html b/test/wpt/tests/FileAPI/support/incumbent.sub.html
new file mode 100644
index 0000000..63a81cd
--- /dev/null
+++ b/test/wpt/tests/FileAPI/support/incumbent.sub.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<title>Incumbent page used as a test helper</title>
+
+<iframe src="//{{domains[www1]}}:{{location[port]}}/FileAPI/support/document-domain-setter.sub.html" id="c"></iframe>
+<iframe src="//{{domains[www2]}}:{{location[port]}}/FileAPI/support/document-domain-setter.sub.html" id="r"></iframe>
+<iframe src="//{{domains[élève]}}:{{location[port]}}/FileAPI/support/document-domain-setter.sub.html" id="bs"></iframe>
+
+<script>
+"use strict";
+document.domain = "{{host}}";
+
+window.createBlobURL = () => {
+ const current = document.querySelector("#c").contentWindow;
+ const relevant = document.querySelector("#r").contentWindow;
+ const blobSource = document.querySelector("#bs").contentWindow;
+
+ const blob = new blobSource.Blob(["Test Blob"]);
+
+ return current.URL.createObjectURL.call(relevant, blob);
+};
+
+</script>
diff --git a/test/wpt/tests/FileAPI/support/send-file-form-helper.js b/test/wpt/tests/FileAPI/support/send-file-form-helper.js
new file mode 100644
index 0000000..d6adf21
--- /dev/null
+++ b/test/wpt/tests/FileAPI/support/send-file-form-helper.js
@@ -0,0 +1,282 @@
+'use strict';
+
+// See /FileAPI/file/resources/echo-content-escaped.py
+function escapeString(string) {
+ return string.replace(/\\/g, "\\\\").replace(
+ /[^\x20-\x7E]/g,
+ (x) => {
+ let hex = x.charCodeAt(0).toString(16);
+ if (hex.length < 2) hex = "0" + hex;
+ return `\\x${hex}`;
+ },
+ ).replace(/\\x0d\\x0a/g, "\r\n");
+}
+
+// Rationale for this particular test character sequence, which is
+// used in filenames and also in file contents:
+//
+// - ABC~ ensures the string starts with something we can read to
+// ensure it is from the correct source; ~ is used because even
+// some 1-byte otherwise-ASCII-like parts of ISO-2022-JP
+// interpret it differently.
+// - ‾¥ are inside a single-byte range of ISO-2022-JP and help
+// diagnose problems due to filesystem encoding or locale
+// - ≈ is inside IBM437 and helps diagnose problems due to filesystem
+// encoding or locale
+// - ¤ is inside Latin-1 and helps diagnose problems due to
+// filesystem encoding or locale; it is also the "simplest" case
+// needing substitution in ISO-2022-JP
+// - ï½¥ is inside a single-byte range of ISO-2022-JP in some variants
+// and helps diagnose problems due to filesystem encoding or locale;
+// on the web it is distinct when decoding but unified when encoding
+// - ・ is inside a double-byte range of ISO-2022-JP and helps
+// diagnose problems due to filesystem encoding or locale
+// - • is inside Windows-1252 and helps diagnose problems due to
+// filesystem encoding or locale and also ensures these aren't
+// accidentally turned into e.g. control codes
+// - ∙ is inside IBM437 and helps diagnose problems due to filesystem
+// encoding or locale
+// - · is inside Latin-1 and helps diagnose problems due to
+// filesystem encoding or locale and also ensures HTML named
+// character references (e.g. &middot;) are not used
+// - ☼ is inside IBM437 shadowing C0 and helps diagnose problems due to
+// filesystem encoding or locale and also ensures these aren't
+// accidentally turned into e.g. control codes
+// - ★ is inside ISO-2022-JP on a non-Kanji page and makes correct
+// output easier to spot
+// - 星 is inside ISO-2022-JP on a Kanji page and makes correct
+// output easier to spot
+// - 🌟 is outside the BMP and makes incorrect surrogate pair
+// substitution detectable and ensures substitutions work
+// correctly immediately after Kanji 2-byte ISO-2022-JP
+// - 星 repeated here ensures the correct codec state is used
+// after a non-BMP substitution
+// - ★ repeated here also makes correct output easier to spot
+// - ☼ is inside IBM437 shadowing C0 and helps diagnose problems due to
+// filesystem encoding or locale and also ensures these aren't
+// accidentally turned into e.g. control codes and also ensures
+// substitutions work correctly immediately after non-Kanji
+// 2-byte ISO-2022-JP
+// - · is inside Latin-1 and helps diagnose problems due to
+// filesystem encoding or locale and also ensures HTML named
+// character references (e.g. &middot;) are not used
+// - ∙ is inside IBM437 and helps diagnose problems due to filesystem
+// encoding or locale
+// - • is inside Windows-1252 and again helps diagnose problems
+// due to filesystem encoding or locale
+// - ・ is inside a double-byte range of ISO-2022-JP and helps
+// diagnose problems due to filesystem encoding or locale
+// - ï½¥ is inside a single-byte range of ISO-2022-JP in some variants
+// and helps diagnose problems due to filesystem encoding or locale;
+// on the web it is distinct when decoding but unified when encoding
+// - ¤ is inside Latin-1 and helps diagnose problems due to
+// filesystem encoding or locale; again it is a "simple"
+// substitution case
+// - ≈ is inside IBM437 and helps diagnose problems due to filesystem
+// encoding or locale
+// - ¥‾ are inside a single-byte range of ISO-2022-JP and help
+// diagnose problems due to filesystem encoding or locale
+// - ~XYZ ensures earlier errors don't lead to misencoding of
+// simple ASCII
+//
+// Overall the near-symmetry makes common I18N mistakes like
+// off-by-1-after-non-BMP easier to spot. All the characters
+// are also allowed in Windows Unicode filenames.
+const kTestChars = 'ABC~‾¥≈¤・・•∙·☼★星🌟星★☼·∙•・・¤≈¥‾~XYZ';
+
+// The kTestFallback* strings represent the expected byte sequence from
+// encoding kTestChars with the given encoding with "html" replacement
+// mode, isomorphic-decoded. That means, characters that can't be
+// encoded in that encoding get HTML-escaped, but no further
+// `escapeString`-like escapes are needed.
+const kTestFallbackUtf8 = (
+ "ABC~\xE2\x80\xBE\xC2\xA5\xE2\x89\x88\xC2\xA4\xEF\xBD\xA5\xE3\x83\xBB\xE2" +
+ "\x80\xA2\xE2\x88\x99\xC2\xB7\xE2\x98\xBC\xE2\x98\x85\xE6\x98\x9F\xF0\x9F" +
+ "\x8C\x9F\xE6\x98\x9F\xE2\x98\x85\xE2\x98\xBC\xC2\xB7\xE2\x88\x99\xE2\x80" +
+ "\xA2\xE3\x83\xBB\xEF\xBD\xA5\xC2\xA4\xE2\x89\x88\xC2\xA5\xE2\x80\xBE~XYZ"
+);
+
+const kTestFallbackIso2022jp = (
+ ("ABC~\x1B(J~\\≈¤\x1B$B!&!&\x1B(B•∙·☼\x1B$B!z@1\x1B(B🌟" +
+ "\x1B$B@1!z\x1B(B☼·∙•\x1B$B!&!&\x1B(B¤≈\x1B(J\\~\x1B(B~XYZ")
+ .replace(/[^\0-\x7F]/gu, (x) => `&#${x.codePointAt(0)};`)
+);
+
+const kTestFallbackWindows1252 = (
+ "ABC~‾\xA5≈\xA4・・\x95∙\xB7☼★星🌟星★☼\xB7∙\x95・・\xA4≈\xA5‾~XYZ".replace(
+ /[^\0-\xFF]/gu,
+ (x) => `&#${x.codePointAt(0)};`,
+ )
+);
+
+const kTestFallbackXUserDefined = kTestChars.replace(
+ /[^\0-\x7F]/gu,
+ (x) => `&#${x.codePointAt(0)};`,
+);
+
+// formPostFileUploadTest - verifies multipart upload structure and
+// numeric character reference replacement for filenames, field names,
+// and field values using form submission.
+//
+// Uses /FileAPI/file/resources/echo-content-escaped.py to echo the
+// upload POST with controls and non-ASCII bytes escaped. This is done
+// because navigations whose response body contains [\0\b\v] may get
+// treated as a download, which is not what we want. Use the
+// `escapeString` function to replicate that kind of escape (note that
+// it takes an isomorphic-decoded string, not a byte sequence).
+//
+// Fields in the parameter object:
+//
+// - fileNameSource: purely explanatory and gives a clue about which
+// character encoding is the source for the non-7-bit-ASCII parts of
+// the fileBaseName, or Unicode if no smaller-than-Unicode source
+// contains all the characters. Used in the test name.
+// - fileBaseName: the not-necessarily-just-7-bit-ASCII file basename
+// used for the constructed test file. Used in the test name.
+// - formEncoding: the acceptCharset of the form used to submit the
+// test file. Used in the test name.
+// - expectedEncodedBaseName: the expected formEncoding-encoded
+// version of fileBaseName, isomorphic-decoded. That means, characters
+// that can't be encoded in that encoding get HTML-escaped, but no
+// further `escapeString`-like escapes are needed.
+const formPostFileUploadTest = ({
+ fileNameSource,
+ fileBaseName,
+ formEncoding,
+ expectedEncodedBaseName,
+}) => {
+ promise_test(async testCase => {
+
+ if (document.readyState !== 'complete') {
+ await new Promise(resolve => addEventListener('load', resolve));
+ }
+
+ const formTargetFrame = Object.assign(document.createElement('iframe'), {
+ name: 'formtargetframe',
+ });
+ document.body.append(formTargetFrame);
+ testCase.add_cleanup(() => {
+ document.body.removeChild(formTargetFrame);
+ });
+
+ const form = Object.assign(document.createElement('form'), {
+ acceptCharset: formEncoding,
+ action: '/FileAPI/file/resources/echo-content-escaped.py',
+ method: 'POST',
+ enctype: 'multipart/form-data',
+ target: formTargetFrame.name,
+ });
+ document.body.append(form);
+ testCase.add_cleanup(() => {
+ document.body.removeChild(form);
+ });
+
+ // Used to verify that the browser agrees with the test about
+ // which form charset is used.
+ form.append(Object.assign(document.createElement('input'), {
+ type: 'hidden',
+ name: '_charset_',
+ }));
+
+ // Used to verify that the browser agrees with the test about
+ // field value replacement and encoding independently of file system
+ // idiosyncracies.
+ form.append(Object.assign(document.createElement('input'), {
+ type: 'hidden',
+ name: 'filename',
+ value: fileBaseName,
+ }));
+
+ // Same, but with name and value reversed to ensure field names
+ // get the same treatment.
+ form.append(Object.assign(document.createElement('input'), {
+ type: 'hidden',
+ name: fileBaseName,
+ value: 'filename',
+ }));
+
+ const fileInput = Object.assign(document.createElement('input'), {
+ type: 'file',
+ name: 'file',
+ });
+ form.append(fileInput);
+
+ // Removes c:\fakepath\ or other pseudofolder and returns just the
+ // final component of filePath; allows both / and \ as segment
+ // delimiters.
+ const baseNameOfFilePath = filePath => filePath.split(/[\/\\]/).pop();
+ await new Promise(resolve => {
+ const dataTransfer = new DataTransfer;
+ dataTransfer.items.add(
+ new File([kTestChars], fileBaseName, {type: 'text/plain'}));
+ fileInput.files = dataTransfer.files;
+ // For historical reasons .value will be prefixed with
+ // c:\fakepath\, but the basename should match the file name
+ // exposed through the newer .files[0].name API. This check
+ // verifies that assumption.
+ assert_equals(
+ baseNameOfFilePath(fileInput.files[0].name),
+ baseNameOfFilePath(fileInput.value),
+ `The basename of the field's value should match its files[0].name`);
+ form.submit();
+ formTargetFrame.onload = resolve;
+ });
+
+ const formDataText = formTargetFrame.contentDocument.body.textContent;
+ const formDataLines = formDataText.split('\n');
+ if (formDataLines.length && !formDataLines[formDataLines.length - 1]) {
+ --formDataLines.length;
+ }
+ assert_greater_than(
+ formDataLines.length,
+ 2,
+ `${fileBaseName}: multipart form data must have at least 3 lines: ${
+ JSON.stringify(formDataText)
+ }`);
+ const boundary = formDataLines[0];
+ assert_equals(
+ formDataLines[formDataLines.length - 1],
+ boundary + '--',
+ `${fileBaseName}: multipart form data must end with ${boundary}--: ${
+ JSON.stringify(formDataText)
+ }`);
+
+ const asValue = expectedEncodedBaseName.replace(/\r\n?|\n/g, "\r\n");
+ const asName = asValue.replace(/[\r\n"]/g, encodeURIComponent);
+ const asFilename = expectedEncodedBaseName.replace(/[\r\n"]/g, encodeURIComponent);
+
+ // The response body from echo-content-escaped.py has controls and non-ASCII
+ // bytes escaped, so any caller-provided field that might contain such bytes
+ // must be passed to `escapeString`, after any other expected
+ // transformations.
+ const expectedText = [
+ boundary,
+ 'Content-Disposition: form-data; name="_charset_"',
+ '',
+ formEncoding,
+ boundary,
+ 'Content-Disposition: form-data; name="filename"',
+ '',
+ // Unlike for names and filenames, multipart/form-data values don't escape
+ // \r\n linebreaks, and when they're read from an iframe they become \n.
+ escapeString(asValue).replace(/\r\n/g, "\n"),
+ boundary,
+ `Content-Disposition: form-data; name="${escapeString(asName)}"`,
+ '',
+ 'filename',
+ boundary,
+ `Content-Disposition: form-data; name="file"; ` +
+ `filename="${escapeString(asFilename)}"`,
+ 'Content-Type: text/plain',
+ '',
+ escapeString(kTestFallbackUtf8),
+ boundary + '--',
+ ].join('\n');
+
+ assert_true(
+ formDataText.startsWith(expectedText),
+ `Unexpected multipart-shaped form data received:\n${
+ formDataText
+ }\nExpected:\n${expectedText}`);
+ }, `Upload ${fileBaseName} (${fileNameSource}) in ${formEncoding} form`);
+};
diff --git a/test/wpt/tests/FileAPI/support/send-file-formdata-helper.js b/test/wpt/tests/FileAPI/support/send-file-formdata-helper.js
new file mode 100644
index 0000000..53c8cca
--- /dev/null
+++ b/test/wpt/tests/FileAPI/support/send-file-formdata-helper.js
@@ -0,0 +1,99 @@
+"use strict";
+
+const kTestChars = "ABC~‾¥≈¤・・•∙·☼★星🌟星★☼·∙•・・¤≈¥‾~XYZ";
+
+// formDataPostFileUploadTest - verifies multipart upload structure and
+// numeric character reference replacement for filenames, field names,
+// and field values using FormData and fetch().
+//
+// Uses /fetch/api/resources/echo-content.py to echo the upload
+// POST (unlike in send-file-form-helper.js, here we expect all
+// multipart/form-data request bodies to be UTF-8, so we don't need to
+// escape controls and non-ASCII bytes).
+//
+// Fields in the parameter object:
+//
+// - fileNameSource: purely explanatory and gives a clue about which
+// character encoding is the source for the non-7-bit-ASCII parts of
+// the fileBaseName, or Unicode if no smaller-than-Unicode source
+// contains all the characters. Used in the test name.
+// - fileBaseName: the not-necessarily-just-7-bit-ASCII file basename
+// used for the constructed test file. Used in the test name.
+const formDataPostFileUploadTest = ({
+ fileNameSource,
+ fileBaseName,
+}) => {
+ promise_test(async (testCase) => {
+ const formData = new FormData();
+ let file = new Blob([kTestChars], { type: "text/plain" });
+ try {
+ // Switch to File in browsers that allow this
+ file = new File([file], fileBaseName, { type: file.type });
+ } catch (ignoredException) {
+ }
+
+ // Used to verify that the browser agrees with the test about
+ // field value replacement and encoding independently of file system
+ // idiosyncracies.
+ formData.append("filename", fileBaseName);
+
+ // Same, but with name and value reversed to ensure field names
+ // get the same treatment.
+ formData.append(fileBaseName, "filename");
+
+ formData.append("file", file, fileBaseName);
+
+ const formDataText = await (await fetch(
+ `/fetch/api/resources/echo-content.py`,
+ {
+ method: "POST",
+ body: formData,
+ },
+ )).text();
+ const formDataLines = formDataText.split("\r\n");
+ if (formDataLines.length && !formDataLines[formDataLines.length - 1]) {
+ --formDataLines.length;
+ }
+ assert_greater_than(
+ formDataLines.length,
+ 2,
+ `${fileBaseName}: multipart form data must have at least 3 lines: ${
+ JSON.stringify(formDataText)
+ }`,
+ );
+ const boundary = formDataLines[0];
+ assert_equals(
+ formDataLines[formDataLines.length - 1],
+ boundary + "--",
+ `${fileBaseName}: multipart form data must end with ${boundary}--: ${
+ JSON.stringify(formDataText)
+ }`,
+ );
+
+ const asValue = fileBaseName.replace(/\r\n?|\n/g, "\r\n");
+ const asName = asValue.replace(/[\r\n"]/g, encodeURIComponent);
+ const asFilename = fileBaseName.replace(/[\r\n"]/g, encodeURIComponent);
+ const expectedText = [
+ boundary,
+ 'Content-Disposition: form-data; name="filename"',
+ "",
+ asValue,
+ boundary,
+ `Content-Disposition: form-data; name="${asName}"`,
+ "",
+ "filename",
+ boundary,
+ `Content-Disposition: form-data; name="file"; ` +
+ `filename="${asFilename}"`,
+ "Content-Type: text/plain",
+ "",
+ kTestChars,
+ boundary + "--",
+ ].join("\r\n");
+
+ assert_true(
+ formDataText.startsWith(expectedText),
+ `Unexpected multipart-shaped form data received:\n${formDataText}\nExpected:\n${expectedText}`,
+ );
+ }, `Upload ${fileBaseName} (${fileNameSource}) in fetch with FormData`);
+};
diff --git a/test/wpt/tests/FileAPI/support/upload.txt b/test/wpt/tests/FileAPI/support/upload.txt
new file mode 100644
index 0000000..5ab2f8a
--- /dev/null
+++ b/test/wpt/tests/FileAPI/support/upload.txt
@@ -0,0 +1 @@
+Hello \ No newline at end of file
diff --git a/test/wpt/tests/FileAPI/support/url-origin.html b/test/wpt/tests/FileAPI/support/url-origin.html
new file mode 100644
index 0000000..6375511
--- /dev/null
+++ b/test/wpt/tests/FileAPI/support/url-origin.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<script>
+const blob = new Blob(["Test Blob"]);
+const url = URL.createObjectURL(blob);
+window.parent.postMessage({url: url}, '*');
+</script>
diff --git a/test/wpt/tests/FileAPI/unicode.html b/test/wpt/tests/FileAPI/unicode.html
new file mode 100644
index 0000000..ce3e357
--- /dev/null
+++ b/test/wpt/tests/FileAPI/unicode.html
@@ -0,0 +1,46 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Blob/Unicode interaction: normalization and encoding</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+'use strict';
+
+const OMICRON_WITH_OXIA = '\u1F79'; // NFC normalized to U+3CC
+const CONTAINS_UNPAIRED_SURROGATES = 'abc\uDC00def\uD800ghi';
+const REPLACED = 'abc\uFFFDdef\uFFFDghi';
+
+function readBlobAsPromise(blob) {
+ return new Promise((resolve, reject) => {
+ const reader = new FileReader();
+ reader.readAsText(blob);
+ reader.onload = () => resolve(reader.result);
+ reader.onerror = () => reject(reader.error);
+ });
+}
+
+promise_test(async t => {
+ const blob = new Blob([OMICRON_WITH_OXIA]);
+ const result = await readBlobAsPromise(blob);
+ assert_equals(result, OMICRON_WITH_OXIA, 'String should not be normalized');
+}, 'Test that strings are not NFC normalized by Blob constructor');
+
+promise_test(async t => {
+ const file = new File([OMICRON_WITH_OXIA], 'name');
+ const result = await readBlobAsPromise(file);
+ assert_equals(result, OMICRON_WITH_OXIA, 'String should not be normalized');
+}, 'Test that strings are not NFC normalized by File constructor');
+
+promise_test(async t => {
+ const blob = new Blob([CONTAINS_UNPAIRED_SURROGATES]);
+ const result = await readBlobAsPromise(blob);
+ assert_equals(result, REPLACED, 'Unpaired surrogates should be replaced.');
+}, 'Test that unpaired surrogates are replaced by Blob constructor');
+
+promise_test(async t => {
+ const file = new File([CONTAINS_UNPAIRED_SURROGATES], 'name');
+ const result = await readBlobAsPromise(file);
+ assert_equals(result, REPLACED, 'Unpaired surrogates should be replaced.');
+}, 'Test that unpaired surrogates are replaced by File constructor');
+
+</script>
diff --git a/test/wpt/tests/FileAPI/url/cross-global-revoke.sub.html b/test/wpt/tests/FileAPI/url/cross-global-revoke.sub.html
new file mode 100644
index 0000000..ce9d680
--- /dev/null
+++ b/test/wpt/tests/FileAPI/url/cross-global-revoke.sub.html
@@ -0,0 +1,62 @@
+<!doctype html>
+<meta charset="utf-8">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<body>
+<script>
+async_test(t => {
+ const blob_contents = 'test blob contents';
+ const blob = new Blob([blob_contents]);
+ const url = URL.createObjectURL(blob);
+ const frame = document.createElement('iframe');
+ frame.setAttribute('style', 'display:none;');
+ frame.src = 'resources/revoke-helper.html';
+ document.body.appendChild(frame);
+
+ frame.onload = t.step_func(e => {
+ frame.contentWindow.postMessage({url: url}, '*');
+ });
+
+ self.addEventListener('message', t.step_func(e => {
+ if (e.source !== frame.contentWindow) return;
+ assert_equals(e.data, 'revoked');
+ promise_rejects_js(t, TypeError, fetch(url)).then(t.step_func_done());
+ }));
+}, 'It is possible to revoke same-origin blob URLs from different frames.');
+
+async_test(t => {
+ const blob_contents = 'test blob contents';
+ const blob = new Blob([blob_contents]);
+ const url = URL.createObjectURL(blob);
+ const worker = new Worker('resources/revoke-helper.js');
+ worker.onmessage = t.step_func(e => {
+ assert_equals(e.data, 'revoked');
+ promise_rejects_js(t, TypeError, fetch(url)).then(t.step_func_done());
+ });
+ worker.postMessage({url: url});
+}, 'It is possible to revoke same-origin blob URLs from a different worker global.');
+
+async_test(t => {
+ const blob_contents = 'test blob contents';
+ const blob = new Blob([blob_contents]);
+ const url = URL.createObjectURL(blob);
+ const frame = document.createElement('iframe');
+ frame.setAttribute('style', 'display:none;');
+ frame.src = get_host_info().HTTP_REMOTE_ORIGIN + '/FileAPI/url/resources/revoke-helper.html';
+ document.body.appendChild(frame);
+
+ frame.onload = t.step_func(e => {
+ frame.contentWindow.postMessage({url: url}, '*');
+ });
+
+ self.addEventListener('message', t.step_func(e => {
+ if (e.source !== frame.contentWindow) return;
+ assert_equals(e.data, 'revoked');
+ fetch(url).then(response => response.text()).then(t.step_func_done(text => {
+ assert_equals(text, blob_contents);
+ }), t.unreached_func('Unexpected promise rejection'));
+ }));
+}, 'It is not possible to revoke cross-origin blob URLs.');
+
+</script>
diff --git a/test/wpt/tests/FileAPI/url/multi-global-origin-serialization.sub.html b/test/wpt/tests/FileAPI/url/multi-global-origin-serialization.sub.html
new file mode 100644
index 0000000..0052b26
--- /dev/null
+++ b/test/wpt/tests/FileAPI/url/multi-global-origin-serialization.sub.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Blob URL serialization (specifically the origin) in multi-global situations</title>
+<link rel="help" href="https://w3c.github.io/FileAPI/#unicodeBlobURL">
+<link rel="author" title="Domenic Denicola" href="mailto:d@domenic.me">
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<!-- this page is the entry global -->
+
+<iframe src="//{{domains[www]}}:{{location[port]}}/FileAPI/support/incumbent.sub.html"></iframe>
+
+<script>
+"use strict";
+setup({ single_test: true });
+document.domain = "{{host}}";
+
+window.onload = () => {
+ const url = frames[0].createBlobURL();
+ const desired = "blob:{{location[scheme]}}://www1";
+ assert_equals(url.substring(0, desired.length), desired,
+ "Origin should contain www1, from the current settings object");
+ done();
+};
+</script>
diff --git a/test/wpt/tests/FileAPI/url/resources/create-helper.html b/test/wpt/tests/FileAPI/url/resources/create-helper.html
new file mode 100644
index 0000000..fa6cf4e
--- /dev/null
+++ b/test/wpt/tests/FileAPI/url/resources/create-helper.html
@@ -0,0 +1,7 @@
+<!doctype html>
+<script>
+self.addEventListener('message', e => {
+ let url = URL.createObjectURL(e.data.blob);
+ e.source.postMessage({url: url}, '*');
+});
+</script> \ No newline at end of file
diff --git a/test/wpt/tests/FileAPI/url/resources/create-helper.js b/test/wpt/tests/FileAPI/url/resources/create-helper.js
new file mode 100644
index 0000000..e6344f7
--- /dev/null
+++ b/test/wpt/tests/FileAPI/url/resources/create-helper.js
@@ -0,0 +1,4 @@
+self.addEventListener('message', e => {
+ let url = URL.createObjectURL(e.data.blob);
+ self.postMessage({url: url});
+});
diff --git a/test/wpt/tests/FileAPI/url/resources/fetch-tests.js b/test/wpt/tests/FileAPI/url/resources/fetch-tests.js
new file mode 100644
index 0000000..a81ea1e
--- /dev/null
+++ b/test/wpt/tests/FileAPI/url/resources/fetch-tests.js
@@ -0,0 +1,71 @@
+// This method generates a number of tests verifying fetching of blob URLs,
+// allowing the same tests to be used both with fetch() and XMLHttpRequest.
+//
+// |fetch_method| is only used in test names, and should describe the
+// (javascript) method being used by the other two arguments (i.e. 'fetch' or 'XHR').
+//
+// |fetch_should_succeed| is a callback that is called with the Test and a URL.
+// Fetching the URL is expected to succeed. The callback should return a promise
+// resolved with whatever contents were fetched.
+//
+// |fetch_should_fail| similarly is a callback that is called with the Test, a URL
+// to fetch, and optionally a method to use to do the fetch. If no method is
+// specified the callback should use the 'GET' method. Fetching of these URLs is
+// expected to fail, and the callback should return a promise that resolves iff
+// fetching did indeed fail.
+function fetch_tests(fetch_method, fetch_should_succeed, fetch_should_fail) {
+ const blob_contents = 'test blob contents';
+ const blob = new Blob([blob_contents]);
+
+ promise_test(t => {
+ const url = URL.createObjectURL(blob);
+
+ return fetch_should_succeed(t, url).then(text => {
+ assert_equals(text, blob_contents);
+ });
+ }, 'Blob URLs can be used in ' + fetch_method);
+
+ promise_test(t => {
+ const url = URL.createObjectURL(blob);
+
+ return fetch_should_succeed(t, url + '#fragment').then(text => {
+ assert_equals(text, blob_contents);
+ });
+ }, fetch_method + ' with a fragment should succeed');
+
+ promise_test(t => {
+ const url = URL.createObjectURL(blob);
+ URL.revokeObjectURL(url);
+
+ return fetch_should_fail(t, url);
+ }, fetch_method + ' of a revoked URL should fail');
+
+ promise_test(t => {
+ const url = URL.createObjectURL(blob);
+ URL.revokeObjectURL(url + '#fragment');
+
+ return fetch_should_succeed(t, url).then(text => {
+ assert_equals(text, blob_contents);
+ });
+ }, 'Only exact matches should revoke URLs, using ' + fetch_method);
+
+ promise_test(t => {
+ const url = URL.createObjectURL(blob);
+
+ return fetch_should_fail(t, url + '?querystring');
+ }, 'Appending a query string should cause ' + fetch_method + ' to fail');
+
+ promise_test(t => {
+ const url = URL.createObjectURL(blob);
+
+ return fetch_should_fail(t, url + '/path');
+ }, 'Appending a path should cause ' + fetch_method + ' to fail');
+
+ for (const method of ['HEAD', 'POST', 'DELETE', 'OPTIONS', 'PUT', 'CUSTOM']) {
+ const url = URL.createObjectURL(blob);
+
+ promise_test(t => {
+ return fetch_should_fail(t, url, method);
+ }, fetch_method + ' with method "' + method + '" should fail');
+ }
+} \ No newline at end of file
diff --git a/test/wpt/tests/FileAPI/url/resources/revoke-helper.html b/test/wpt/tests/FileAPI/url/resources/revoke-helper.html
new file mode 100644
index 0000000..adf5a01
--- /dev/null
+++ b/test/wpt/tests/FileAPI/url/resources/revoke-helper.html
@@ -0,0 +1,7 @@
+<!doctype html>
+<script>
+self.addEventListener('message', e => {
+ URL.revokeObjectURL(e.data.url);
+ e.source.postMessage('revoked', '*');
+});
+</script> \ No newline at end of file
diff --git a/test/wpt/tests/FileAPI/url/resources/revoke-helper.js b/test/wpt/tests/FileAPI/url/resources/revoke-helper.js
new file mode 100644
index 0000000..c3e05b6
--- /dev/null
+++ b/test/wpt/tests/FileAPI/url/resources/revoke-helper.js
@@ -0,0 +1,9 @@
+self.addEventListener('message', e => {
+ URL.revokeObjectURL(e.data.url);
+ // Registering a new object URL will make absolutely sure that the revocation
+ // has propagated. Without this at least in chrome it is possible for the
+ // below postMessage to arrive at its destination before the revocation has
+ // been fully processed.
+ URL.createObjectURL(new Blob([]));
+ self.postMessage('revoked');
+});
diff --git a/test/wpt/tests/FileAPI/url/sandboxed-iframe.html b/test/wpt/tests/FileAPI/url/sandboxed-iframe.html
new file mode 100644
index 0000000..a52939a
--- /dev/null
+++ b/test/wpt/tests/FileAPI/url/sandboxed-iframe.html
@@ -0,0 +1,32 @@
+<!doctype html>
+<meta charset="utf-8">
+<title>FileAPI Test: Verify behavior of Blob URL in unique origins</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<iframe id="sandboxed-iframe" sandbox="allow-scripts"></iframe>
+
+<script>
+
+const iframe_scripts = [
+ 'resources/fetch-tests.js',
+ 'url-format.any.js',
+ 'url-in-tags.window.js',
+ 'url-with-xhr.any.js',
+ 'url-with-fetch.any.js',
+];
+
+let html = '<!doctype html>\n<meta charset="utf-8">\n<body>\n';
+html = html + '<script src="/resources/testharness.js"></' + 'script>\n';
+html = html + '<script>setup({"explicit_timeout": true});</' + 'script>\n';
+for (const script of iframe_scripts)
+ html = html + '<script src="' + script + '"></' + 'script>\n';
+
+const frame = document.querySelector('#sandboxed-iframe');
+frame.setAttribute('srcdoc', html);
+frame.setAttribute('style', 'display:none;');
+
+fetch_tests_from_window(frame.contentWindow);
+
+</script>
diff --git a/test/wpt/tests/FileAPI/url/unicode-origin.sub.html b/test/wpt/tests/FileAPI/url/unicode-origin.sub.html
new file mode 100644
index 0000000..2c4921c
--- /dev/null
+++ b/test/wpt/tests/FileAPI/url/unicode-origin.sub.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>FileAPI Test: Verify origin of Blob URL</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<body>
+<script>
+async_test(t => {
+ const frame = document.createElement('iframe');
+ self.addEventListener('message', t.step_func(e => {
+ if (e.source != frame.contentWindow) return;
+ const url = e.data.url;
+ assert_false(url.includes('天気ã®è‰¯ã„æ—¥'),
+ 'Origin should be ascii rather than unicode');
+ assert_equals(new URL(url).origin, e.origin,
+ 'Origin of URL should match origin of frame');
+ assert_true(url.startsWith('blob:{{location[scheme]}}://xn--'));
+ t.done();
+ }));
+ frame.src = '{{location[scheme]}}://{{domains[天気ã®è‰¯ã„æ—¥]}}:{{location[port]}}/FileAPI/support/url-origin.html';
+ document.body.appendChild(frame);
+}, 'Verify serialization of non-ascii origin in Blob URLs');
+</script>
diff --git a/test/wpt/tests/FileAPI/url/url-charset.window.js b/test/wpt/tests/FileAPI/url/url-charset.window.js
new file mode 100644
index 0000000..777709b
--- /dev/null
+++ b/test/wpt/tests/FileAPI/url/url-charset.window.js
@@ -0,0 +1,34 @@
+async_test(t => {
+ // This could be detected as ISO-2022-JP, in which case there would be no
+ // <textarea>, and thus the script inside would be interpreted as actual
+ // script.
+ const blob = new Blob(
+ [
+ `aaa\u001B$@<textarea>\u001B(B<script>/* xss */<\/script></textarea>bbb`
+ ],
+ {type: 'text/html;charset=utf-8'});
+ const url = URL.createObjectURL(blob);
+ const win = window.open(url);
+ t.add_cleanup(() => {
+ win.close();
+ });
+
+ win.onload = t.step_func_done(() => {
+ assert_equals(win.document.charset, 'UTF-8');
+ });
+}, 'Blob charset should override any auto-detected charset.');
+
+async_test(t => {
+ const blob = new Blob(
+ [`<!doctype html>\n<meta charset="ISO-8859-1">`],
+ {type: 'text/html;charset=utf-8'});
+ const url = URL.createObjectURL(blob);
+ const win = window.open(url);
+ t.add_cleanup(() => {
+ win.close();
+ });
+
+ win.onload = t.step_func_done(() => {
+ assert_equals(win.document.charset, 'UTF-8');
+ });
+}, 'Blob charset should override <meta charset>.');
diff --git a/test/wpt/tests/FileAPI/url/url-format.any.js b/test/wpt/tests/FileAPI/url/url-format.any.js
new file mode 100644
index 0000000..69c5111
--- /dev/null
+++ b/test/wpt/tests/FileAPI/url/url-format.any.js
@@ -0,0 +1,70 @@
+// META: timeout=long
+const blob = new Blob(['test']);
+const file = new File(['test'], 'name');
+
+test(t => {
+ const url_count = 5000;
+ let list = [];
+
+ t.add_cleanup(() => {
+ for (let url of list) {
+ URL.revokeObjectURL(url);
+ }
+ });
+
+ for (let i = 0; i < url_count; ++i)
+ list.push(URL.createObjectURL(blob));
+
+ list.sort();
+
+ for (let i = 1; i < list.length; ++i)
+ assert_not_equals(list[i], list[i-1], 'generated Blob URLs should be unique');
+}, 'Generated Blob URLs are unique');
+
+test(() => {
+ const url = URL.createObjectURL(blob);
+ assert_equals(typeof url, 'string');
+ assert_true(url.startsWith('blob:'));
+}, 'Blob URL starts with "blob:"');
+
+test(() => {
+ const url = URL.createObjectURL(file);
+ assert_equals(typeof url, 'string');
+ assert_true(url.startsWith('blob:'));
+}, 'Blob URL starts with "blob:" for Files');
+
+test(() => {
+ const url = URL.createObjectURL(blob);
+ assert_equals(new URL(url).origin, location.origin);
+ if (location.origin !== 'null') {
+ assert_true(url.includes(location.origin));
+ assert_true(url.startsWith('blob:' + location.protocol));
+ }
+}, 'Origin of Blob URL matches our origin');
+
+test(() => {
+ const url = URL.createObjectURL(blob);
+ const url_record = new URL(url);
+ assert_equals(url_record.protocol, 'blob:');
+ assert_equals(url_record.origin, location.origin);
+ assert_equals(url_record.host, '', 'host should be an empty string');
+ assert_equals(url_record.port, '', 'port should be an empty string');
+ const uuid_path_re = /\/[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
+ assert_true(uuid_path_re.test(url_record.pathname), 'Path must end with a valid UUID');
+ if (location.origin !== 'null') {
+ const nested_url = new URL(url_record.pathname);
+ assert_equals(nested_url.origin, location.origin);
+ assert_equals(nested_url.pathname.search(uuid_path_re), 0, 'Path must be a valid UUID');
+ assert_true(url.includes(location.origin));
+ assert_true(url.startsWith('blob:' + location.protocol));
+ }
+}, 'Blob URL parses correctly');
+
+test(() => {
+ const url = URL.createObjectURL(file);
+ assert_equals(new URL(url).origin, location.origin);
+ if (location.origin !== 'null') {
+ assert_true(url.includes(location.origin));
+ assert_true(url.startsWith('blob:' + location.protocol));
+ }
+}, 'Origin of Blob URL matches our origin for Files');
diff --git a/test/wpt/tests/FileAPI/url/url-in-tags-revoke.window.js b/test/wpt/tests/FileAPI/url/url-in-tags-revoke.window.js
new file mode 100644
index 0000000..1cdad79
--- /dev/null
+++ b/test/wpt/tests/FileAPI/url/url-in-tags-revoke.window.js
@@ -0,0 +1,115 @@
+// META: timeout=long
+async_test(t => {
+ const run_result = 'test_frame_OK';
+ const blob_contents = '<!doctype html>\n<meta charset="utf-8">\n' +
+ '<script>window.test_result = "' + run_result + '";</script>';
+ const blob = new Blob([blob_contents], {type: 'text/html'});
+ const url = URL.createObjectURL(blob);
+
+ const frame = document.createElement('iframe');
+ frame.setAttribute('src', url);
+ frame.setAttribute('style', 'display:none;');
+ document.body.appendChild(frame);
+ URL.revokeObjectURL(url);
+
+ frame.onload = t.step_func_done(() => {
+ assert_equals(frame.contentWindow.test_result, run_result);
+ });
+}, 'Fetching a blob URL immediately before revoking it works in an iframe.');
+
+async_test(t => {
+ const run_result = 'test_frame_OK';
+ const blob_contents = '<!doctype html>\n<meta charset="utf-8">\n' +
+ '<script>window.test_result = "' + run_result + '";</script>';
+ const blob = new Blob([blob_contents], {type: 'text/html'});
+ const url = URL.createObjectURL(blob);
+
+ const frame = document.createElement('iframe');
+ frame.setAttribute('src', '/common/blank.html');
+ frame.setAttribute('style', 'display:none;');
+ document.body.appendChild(frame);
+
+ frame.onload = t.step_func(() => {
+ frame.contentWindow.location = url;
+ URL.revokeObjectURL(url);
+ frame.onload = t.step_func_done(() => {
+ assert_equals(frame.contentWindow.test_result, run_result);
+ });
+ });
+}, 'Fetching a blob URL immediately before revoking it works in an iframe navigation.');
+
+async_test(t => {
+ const run_result = 'test_frame_OK';
+ const blob_contents = '<!doctype html>\n<meta charset="utf-8">\n' +
+ '<script>window.test_result = "' + run_result + '";</script>';
+ const blob = new Blob([blob_contents], {type: 'text/html'});
+ const url = URL.createObjectURL(blob);
+ const win = window.open(url);
+ URL.revokeObjectURL(url);
+ add_completion_callback(() => { win.close(); });
+
+ win.onload = t.step_func_done(() => {
+ assert_equals(win.test_result, run_result);
+ });
+}, 'Opening a blob URL in a new window immediately before revoking it works.');
+
+function receive_message_on_channel(t, channel_name) {
+ const channel = new BroadcastChannel(channel_name);
+ return new Promise(resolve => {
+ channel.addEventListener('message', t.step_func(e => {
+ resolve(e.data);
+ }));
+ });
+}
+
+function window_contents_for_channel(channel_name) {
+ return '<!doctype html>\n' +
+ '<script>\n' +
+ 'new BroadcastChannel("' + channel_name + '").postMessage("foobar");\n' +
+ 'self.close();\n' +
+ '</script>';
+}
+
+async_test(t => {
+ const channel_name = 'noopener-window-test';
+ const blob = new Blob([window_contents_for_channel(channel_name)], {type: 'text/html'});
+ receive_message_on_channel(t, channel_name).then(t.step_func_done(t => {
+ assert_equals(t, 'foobar');
+ }));
+ const url = URL.createObjectURL(blob);
+ const win = window.open();
+ win.opener = null;
+ win.location = url;
+ URL.revokeObjectURL(url);
+}, 'Opening a blob URL in a noopener about:blank window immediately before revoking it works.');
+
+async_test(t => {
+ const run_result = 'test_script_OK';
+ const blob_contents = 'window.script_test_result = "' + run_result + '";';
+ const blob = new Blob([blob_contents]);
+ const url = URL.createObjectURL(blob);
+
+ const e = document.createElement('script');
+ e.setAttribute('src', url);
+ e.onload = t.step_func_done(() => {
+ assert_equals(window.script_test_result, run_result);
+ });
+
+ document.body.appendChild(e);
+ URL.revokeObjectURL(url);
+}, 'Fetching a blob URL immediately before revoking it works in <script> tags.');
+
+async_test(t => {
+ const channel_name = 'a-click-test';
+ const blob = new Blob([window_contents_for_channel(channel_name)], {type: 'text/html'});
+ receive_message_on_channel(t, channel_name).then(t.step_func_done(t => {
+ assert_equals(t, 'foobar');
+ }));
+ const url = URL.createObjectURL(blob);
+ const anchor = document.createElement('a');
+ anchor.href = url;
+ anchor.target = '_blank';
+ document.body.appendChild(anchor);
+ anchor.click();
+ URL.revokeObjectURL(url);
+}, 'Opening a blob URL in a new window by clicking an <a> tag works immediately before revoking the URL.');
diff --git a/test/wpt/tests/FileAPI/url/url-in-tags.window.js b/test/wpt/tests/FileAPI/url/url-in-tags.window.js
new file mode 100644
index 0000000..f20b359
--- /dev/null
+++ b/test/wpt/tests/FileAPI/url/url-in-tags.window.js
@@ -0,0 +1,48 @@
+async_test(t => {
+ const run_result = 'test_script_OK';
+ const blob_contents = 'window.test_result = "' + run_result + '";';
+ const blob = new Blob([blob_contents]);
+ const url = URL.createObjectURL(blob);
+
+ const e = document.createElement('script');
+ e.setAttribute('src', url);
+ e.onload = t.step_func_done(() => {
+ assert_equals(window.test_result, run_result);
+ });
+
+ document.body.appendChild(e);
+}, 'Blob URLs can be used in <script> tags');
+
+async_test(t => {
+ const run_result = 'test_frame_OK';
+ const blob_contents = '<!doctype html>\n<meta charset="utf-8">\n' +
+ '<script>window.test_result = "' + run_result + '";</script>';
+ const blob = new Blob([blob_contents], {type: 'text/html'});
+ const url = URL.createObjectURL(blob);
+
+ const frame = document.createElement('iframe');
+ frame.setAttribute('src', url);
+ frame.setAttribute('style', 'display:none;');
+ document.body.appendChild(frame);
+
+ frame.onload = t.step_func_done(() => {
+ assert_equals(frame.contentWindow.test_result, run_result);
+ });
+}, 'Blob URLs can be used in iframes, and are treated same origin');
+
+async_test(t => {
+ const blob_contents = '<!doctype html>\n<meta charset="utf-8">\n' +
+ '<style>body { margin: 0; } .block { height: 5000px; }</style>\n' +
+ '<body>\n' +
+ '<a id="block1"></a><div class="block"></div>\n' +
+ '<a id="block2"></a><div class="block"></div>';
+ const blob = new Blob([blob_contents], {type: 'text/html'});
+ const url = URL.createObjectURL(blob);
+
+ const frame = document.createElement('iframe');
+ frame.setAttribute('src', url + '#block2');
+ document.body.appendChild(frame);
+ frame.contentWindow.onscroll = t.step_func_done(() => {
+ assert_equals(frame.contentWindow.scrollY, 5000);
+ });
+}, 'Blob URL fragment is implemented.');
diff --git a/test/wpt/tests/FileAPI/url/url-lifetime.html b/test/wpt/tests/FileAPI/url/url-lifetime.html
new file mode 100644
index 0000000..ad5d667
--- /dev/null
+++ b/test/wpt/tests/FileAPI/url/url-lifetime.html
@@ -0,0 +1,56 @@
+<!doctype html>
+<meta charset="utf-8">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<body>
+<script>
+promise_test(t => {
+ const blob_contents = 'test blob contents';
+ const blob = new Blob([blob_contents]);
+ const worker = new Worker('resources/create-helper.js');
+ let url;
+ return new Promise(resolve => {
+ worker.onmessage = e => resolve(e.data);
+ worker.postMessage({blob: blob});
+ }).then(data => {
+ url = data.url;
+ let result = fetch(url);
+ worker.terminate();
+ return result;
+ }).then(response => response.text()).then(text => {
+ assert_equals(text, blob_contents);
+ return new Promise(resolve => t.step_timeout(resolve, 100));
+ }).then(() => promise_rejects_js(t, TypeError, fetch(url)));
+}, 'Terminating worker revokes its URLs');
+
+promise_test(t => {
+ const blob_contents = 'test blob contents';
+ const blob = new Blob([blob_contents]);
+ const frame = document.createElement('iframe');
+ frame.setAttribute('style', 'display:none;');
+ frame.src = 'resources/create-helper.html';
+ document.body.appendChild(frame);
+
+ let url;
+ return new Promise(resolve => {
+ frame.onload = t.step_func(e => {
+ resolve(e);
+ });
+ }).then(e => {
+ frame.contentWindow.postMessage({blob: blob}, '*');
+ return new Promise(resolve => {
+ self.addEventListener('message', t.step_func(e => {
+ if (e.source === frame.contentWindow) resolve(e);
+ }));
+ });
+ }).then(e => {
+ url = e.data.url;
+ let fetch_result = fetch(url);
+ document.body.removeChild(frame);
+ return fetch_result;
+ }).then(response => response.text()).then(text => {
+ assert_equals(text, blob_contents);
+ return new Promise(resolve => t.step_timeout(resolve, 100));
+ }).then(() => promise_rejects_js(t, TypeError, fetch(url)));
+}, 'Removing an iframe revokes its URLs');
+</script> \ No newline at end of file
diff --git a/test/wpt/tests/FileAPI/url/url-reload.window.js b/test/wpt/tests/FileAPI/url/url-reload.window.js
new file mode 100644
index 0000000..d333b3a
--- /dev/null
+++ b/test/wpt/tests/FileAPI/url/url-reload.window.js
@@ -0,0 +1,36 @@
+function blob_url_reload_test(t, revoke_before_reload) {
+ const run_result = 'test_frame_OK';
+ const blob_contents = '<!doctype html>\n<meta charset="utf-8">\n' +
+ '<script>window.test_result = "' + run_result + '";</script>';
+ const blob = new Blob([blob_contents], {type: 'text/html'});
+ const url = URL.createObjectURL(blob);
+
+ const frame = document.createElement('iframe');
+ frame.setAttribute('src', url);
+ frame.setAttribute('style', 'display:none;');
+ document.body.appendChild(frame);
+
+ frame.onload = t.step_func(() => {
+ if (revoke_before_reload)
+ URL.revokeObjectURL(url);
+ assert_equals(frame.contentWindow.test_result, run_result);
+ frame.contentWindow.test_result = null;
+ frame.onload = t.step_func_done(() => {
+ assert_equals(frame.contentWindow.test_result, run_result);
+ });
+ // Slight delay before reloading to ensure revoke actually has had a chance
+ // to be processed.
+ t.step_timeout(() => {
+ frame.contentWindow.location.reload();
+ }, 250);
+ });
+}
+
+async_test(t => {
+ blob_url_reload_test(t, false);
+}, 'Reloading a blob URL succeeds.');
+
+
+async_test(t => {
+ blob_url_reload_test(t, true);
+}, 'Reloading a blob URL succeeds even if the URL was revoked.');
diff --git a/test/wpt/tests/FileAPI/url/url-with-fetch.any.js b/test/wpt/tests/FileAPI/url/url-with-fetch.any.js
new file mode 100644
index 0000000..54e6a3d
--- /dev/null
+++ b/test/wpt/tests/FileAPI/url/url-with-fetch.any.js
@@ -0,0 +1,72 @@
+// META: script=resources/fetch-tests.js
+// META: script=/common/gc.js
+
+function fetch_should_succeed(test, request) {
+ return fetch(request).then(response => response.text());
+}
+
+function fetch_should_fail(test, url, method = 'GET') {
+ return promise_rejects_js(test, TypeError, fetch(url, {method: method}));
+}
+
+fetch_tests('fetch', fetch_should_succeed, fetch_should_fail);
+
+promise_test(t => {
+ const blob_contents = 'test blob contents';
+ const blob_type = 'image/png';
+ const blob = new Blob([blob_contents], {type: blob_type});
+ const url = URL.createObjectURL(blob);
+
+ return fetch(url).then(response => {
+ assert_equals(response.headers.get('Content-Type'), blob_type);
+ });
+}, 'fetch should return Content-Type from Blob');
+
+promise_test(t => {
+ const blob_contents = 'test blob contents';
+ const blob = new Blob([blob_contents]);
+ const url = URL.createObjectURL(blob);
+ const request = new Request(url);
+
+ // Revoke the object URL. Request should take a reference to the blob as
+ // soon as it receives it in open(), so the request succeeds even though we
+ // revoke the URL before calling fetch().
+ URL.revokeObjectURL(url);
+
+ return fetch_should_succeed(t, request).then(text => {
+ assert_equals(text, blob_contents);
+ });
+}, 'Revoke blob URL after creating Request, will fetch');
+
+promise_test(async t => {
+ const blob_contents = 'test blob contents';
+ const blob = new Blob([blob_contents]);
+ const url = URL.createObjectURL(blob);
+ let request = new Request(url);
+
+ // Revoke the object URL. Request should take a reference to the blob as
+ // soon as it receives it in open(), so the request succeeds even though we
+ // revoke the URL before calling fetch().
+ URL.revokeObjectURL(url);
+
+ request = request.clone();
+ await garbageCollect();
+
+ const text = await fetch_should_succeed(t, request);
+ assert_equals(text, blob_contents);
+}, 'Revoke blob URL after creating Request, then clone Request, will fetch');
+
+promise_test(function(t) {
+ const blob_contents = 'test blob contents';
+ const blob = new Blob([blob_contents]);
+ const url = URL.createObjectURL(blob);
+
+ const result = fetch_should_succeed(t, url).then(text => {
+ assert_equals(text, blob_contents);
+ });
+
+ // Revoke the object URL. fetch should have already resolved the blob URL.
+ URL.revokeObjectURL(url);
+
+ return result;
+}, 'Revoke blob URL after calling fetch, fetch should succeed');
diff --git a/test/wpt/tests/FileAPI/url/url-with-xhr.any.js b/test/wpt/tests/FileAPI/url/url-with-xhr.any.js
new file mode 100644
index 0000000..29d8308
--- /dev/null
+++ b/test/wpt/tests/FileAPI/url/url-with-xhr.any.js
@@ -0,0 +1,68 @@
+// META: script=resources/fetch-tests.js
+
+function xhr_should_succeed(test, url) {
+ return new Promise((resolve, reject) => {
+ const xhr = new XMLHttpRequest();
+ xhr.open('GET', url);
+ xhr.onload = test.step_func(() => {
+ assert_equals(xhr.status, 200);
+ assert_equals(xhr.statusText, 'OK');
+ resolve(xhr.response);
+ });
+ xhr.onerror = () => reject('Got unexpected error event');
+ xhr.send();
+ });
+}
+
+function xhr_should_fail(test, url, method = 'GET') {
+ const xhr = new XMLHttpRequest();
+ xhr.open(method, url);
+ const result1 = new Promise((resolve, reject) => {
+ xhr.onload = () => reject('Got unexpected load event');
+ xhr.onerror = resolve;
+ });
+ const result2 = new Promise(resolve => {
+ xhr.onreadystatechange = test.step_func(() => {
+ if (xhr.readyState !== xhr.DONE) return;
+ assert_equals(xhr.status, 0);
+ resolve();
+ });
+ });
+ xhr.send();
+ return Promise.all([result1, result2]);
+}
+
+fetch_tests('XHR', xhr_should_succeed, xhr_should_fail);
+
+async_test(t => {
+ const blob_contents = 'test blob contents';
+ const blob_type = 'image/png';
+ const blob = new Blob([blob_contents], {type: blob_type});
+ const url = URL.createObjectURL(blob);
+ const xhr = new XMLHttpRequest();
+ xhr.open('GET', url);
+ xhr.onloadend = t.step_func_done(() => {
+ assert_equals(xhr.getResponseHeader('Content-Type'), blob_type);
+ });
+ xhr.send();
+}, 'XHR should return Content-Type from Blob');
+
+async_test(t => {
+ const blob_contents = 'test blob contents';
+ const blob = new Blob([blob_contents]);
+ const url = URL.createObjectURL(blob);
+ const xhr = new XMLHttpRequest();
+ xhr.open('GET', url);
+
+ // Revoke the object URL. XHR should take a reference to the blob as soon as
+ // it receives it in open(), so the request succeeds even though we revoke the
+ // URL before calling send().
+ URL.revokeObjectURL(url);
+
+ xhr.onload = t.step_func_done(() => {
+ assert_equals(xhr.response, blob_contents);
+ });
+ xhr.onerror = t.unreached_func('Got unexpected error event');
+
+ xhr.send();
+}, 'Revoke blob URL after open(), will fetch');
diff --git a/test/wpt/tests/FileAPI/url/url_createobjecturl_file-manual.html b/test/wpt/tests/FileAPI/url/url_createobjecturl_file-manual.html
new file mode 100644
index 0000000..7ae3251
--- /dev/null
+++ b/test/wpt/tests/FileAPI/url/url_createobjecturl_file-manual.html
@@ -0,0 +1,45 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>FileAPI Test: Creating Blob URL with File</title>
+<link rel="author" title="Intel" href="http://www.intel.com">
+<link rel="author" title="JunChen Xia" href="mailto:xjconlyme@gmail.com">
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<div>
+ <p>Test steps:</p>
+ <ol>
+ <li>Download <a href="/images/blue96x96.png">blue96x96.png</a> to local.</li>
+ <li>Select the local file (blue96x96.png) to run the test.</li>
+ </ol>
+</div>
+
+<form name="uploadData">
+ <input type="file" id="fileChooser">
+</form>
+
+<div id="log"></div>
+
+<script>
+ async_test(function(t) {
+ var fileInput = document.querySelector('#fileChooser');
+
+ fileInput.onchange = t.step_func(function(e) {
+ var blobURL, file = fileInput.files[0];
+
+ test(function() {
+ assert_true(file instanceof File, "FileList contains File");
+ }, "Check if FileList contains File");
+
+ test(function() {
+ blobURL = window.URL.createObjectURL(file);
+ assert_equals(typeof blobURL, "string", "Blob URL is type of string");
+ assert_equals(blobURL.indexOf("blob"), 0, "Blob URL's scheme is blob");
+ }, "Check if URL.createObjectURL(File) returns a Blob URL");
+
+ t.done();
+ });
+ });
+</script>
+
diff --git a/test/wpt/tests/FileAPI/url/url_createobjecturl_file_img-manual.html b/test/wpt/tests/FileAPI/url/url_createobjecturl_file_img-manual.html
new file mode 100644
index 0000000..534c1de
--- /dev/null
+++ b/test/wpt/tests/FileAPI/url/url_createobjecturl_file_img-manual.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>FileAPI Test: Creating Blob URL with File as image source</title>
+<link rel="author" title="Intel" href="http://www.intel.com">
+<link rel="author" title="JunChen Xia" href="mailto:xjconlyme@gmail.com">
+
+<div>
+ <p>Test steps:</p>
+ <ol>
+ <li>Download <a href="/images/blue96x96.png">blue96x96.png</a> to local.</li>
+ <li>Select the local file (blue96x96.png) to run the test.</li>
+ </ol>
+ <p>Pass/fail criteria:</p>
+ <p>Test passes if there is a filled blue square.</p>
+
+ <p><input type="file" accept="image/*" id="fileChooser"></p>
+ <p><img id="displayImage"></img></p>
+</div>
+
+<script>
+ var fileInput = document.querySelector("#fileChooser");
+ var img = document.querySelector("#displayImage");
+
+ fileInput.addEventListener("change", function(evt) {
+ img.src = window.URL.createObjectURL(fileInput.files[0]);
+ }, false);
+</script>
+
diff --git a/test/wpt/tests/FileAPI/url/url_xmlhttprequest_img-ref.html b/test/wpt/tests/FileAPI/url/url_xmlhttprequest_img-ref.html
new file mode 100644
index 0000000..7d73904
--- /dev/null
+++ b/test/wpt/tests/FileAPI/url/url_xmlhttprequest_img-ref.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>FileAPI Reference File</title>
+<link rel="author" title="Intel" href="http://www.intel.com">
+<link rel="author" title="JunChen Xia" href="mailto:xjconlyme@gmail.com">
+
+<p>Test passes if there is a filled blue square.</p>
+
+<p>
+ <img id="fileDisplay" src="/images/blue96x96.png">
+</p>
+
diff --git a/test/wpt/tests/FileAPI/url/url_xmlhttprequest_img.html b/test/wpt/tests/FileAPI/url/url_xmlhttprequest_img.html
new file mode 100644
index 0000000..468dcb0
--- /dev/null
+++ b/test/wpt/tests/FileAPI/url/url_xmlhttprequest_img.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<meta charset="utf-8">
+<title>FileAPI Test: Creating Blob URL via XMLHttpRequest as image source</title>
+<link rel="author" title="Intel" href="http://www.intel.com">
+<link rel="author" title="JunChen Xia" href="mailto:xjconlyme@gmail.com">
+<link rel="match" href="url_xmlhttprequest_img-ref.html">
+
+<p>Test passes if there is a filled blue square.</p>
+
+<p>
+ <img id="fileDisplay">
+</p>
+
+<script src="/common/reftest-wait.js"></script>
+<script>
+ var http = new XMLHttpRequest();
+ http.open("GET", "/images/blue96x96.png", true);
+ http.responseType = "blob";
+ http.onloadend = function() {
+ var fileDisplay = document.querySelector("#fileDisplay");
+ fileDisplay.src = window.URL.createObjectURL(http.response);
+ fileDisplay.onload = takeScreenshot;
+ };
+ http.send();
+</script>
+</html>
diff --git a/test/wpt/tests/LICENSE.md b/test/wpt/tests/LICENSE.md
new file mode 100644
index 0000000..39c46d0
--- /dev/null
+++ b/test/wpt/tests/LICENSE.md
@@ -0,0 +1,11 @@
+# The 3-Clause BSD License
+
+Copyright © web-platform-tests contributors
+
+Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
+2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
+3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/test/wpt/tests/README.md b/test/wpt/tests/README.md
new file mode 100644
index 0000000..7e8e994
--- /dev/null
+++ b/test/wpt/tests/README.md
@@ -0,0 +1,124 @@
+The web-platform-tests Project
+==============================
+
+[![Taskcluster CI Status](https://community-tc.services.mozilla.com/api/github/v1/repository/web-platform-tests/wpt/master/badge.svg)](https://community-tc.services.mozilla.com/api/github/v1/repository/web-platform-tests/wpt/master/latest) [![documentation](https://github.com/web-platform-tests/wpt/workflows/documentation/badge.svg)](https://github.com/web-platform-tests/wpt/actions?query=workflow%3Adocumentation+branch%3Amaster) [![manifest](https://github.com/web-platform-tests/wpt/workflows/manifest/badge.svg)](https://github.com/web-platform-tests/wpt/actions?query=workflow%3Amanifest+branch%3Amaster) [![Python 3](https://pyup.io/repos/github/web-platform-tests/wpt/python-3-shield.svg)](https://pyup.io/repos/github/web-platform-tests/wpt/)
+
+The web-platform-tests Project is a cross-browser test suite for the
+Web-platform stack. Writing tests in a way that allows them to be run in all
+browsers gives browser projects confidence that they are shipping software that
+is compatible with other implementations, and that later implementations will
+be compatible with their implementations. This in turn gives Web
+authors/developers confidence that they can actually rely on the Web platform
+to deliver on the promise of working across browsers and devices without
+needing extra layers of abstraction to paper over the gaps left by
+specification editors and implementors.
+
+The most important sources of information and activity are:
+
+- [github.com/web-platform-tests/wpt](https://github.com/web-platform-tests/wpt):
+ the canonical location of the project's source code revision history and the
+ discussion forum for changes to the code
+- [web-platform-tests.org](https://web-platform-tests.org): the documentation
+ website; details how to set up the project, how to write tests, how to give
+ and receive peer review, how to serve as an administrator, and more
+- [wpt.live](https://wpt.live): a public deployment of the test suite,
+ allowing anyone to run the tests by visiting from an
+ Internet-enabled browser of their choice
+- [wpt.fyi](https://wpt.fyi): an archive of test results collected from an
+ array of web browsers on a regular basis
+- [Real-time chat room](https://app.element.io/#/room/#wpt:matrix.org): the
+ `wpt:matrix.org` matrix channel; includes participants located
+ around the world, but busiest during the European working day.
+- [Mailing list](https://lists.w3.org/Archives/Public/public-test-infra/): a
+ public and low-traffic discussion list
+- [RFCs](https://github.com/web-platform-tests/rfcs): a repo for requesting
+ comments on substantial changes that would impact other stakeholders or
+ users; people who work on WPT infra are encouraged to watch the repo.
+
+**If you'd like clarification about anything**, don't hesitate to ask in the
+chat room or on the mailing list.
+
+Setting Up the Repo
+===================
+
+Clone or otherwise get https://github.com/web-platform-tests/wpt.
+
+Note: because of the frequent creation and deletion of branches in this
+repo, it is recommended to "prune" stale branches when fetching updates,
+i.e. use `git pull --prune` (or `git fetch -p && git merge`).
+
+Running the Tests
+=================
+
+See the [documentation website](https://web-platform-tests.org/running-tests/)
+and in particular the
+[system setup for running tests locally](https://web-platform-tests.org/running-tests/from-local-system.html#system-setup).
+
+Command Line Tools
+==================
+
+The `wpt` command provides a frontend to a variety of tools for
+working with and running web-platform-tests. Some of the most useful
+commands are:
+
+* `wpt serve` - For starting the wpt http server
+* `wpt run` - For running tests in a browser
+* `wpt lint` - For running the lint against all tests
+* `wpt manifest` - For updating or generating a `MANIFEST.json` test manifest
+* `wpt install` - For installing the latest release of a browser or
+ webdriver server on the local machine.
+* `wpt serve-wave` - For starting the wpt http server and the WAVE test runner.
+For more details on how to use the WAVE test runner see the [documentation](./tools/wave/docs/usage/usage.md).
+
+<span id="windows-notes">Windows Notes</span>
+=============================================
+
+On Windows `wpt` commands must be prefixed with `python` or the path
+to the python binary (if `python` is not in your `%PATH%`).
+
+```bash
+python wpt [command]
+```
+
+Alternatively, you may also use
+[Bash on Ubuntu on Windows](https://msdn.microsoft.com/en-us/commandline/wsl/about)
+in the Windows 10 Anniversary Update build, then access your windows
+partition from there to launch `wpt` commands.
+
+Please make sure git and your text editor do not automatically convert
+line endings, as it will cause lint errors. For git, please set
+`git config core.autocrlf false` in your working tree.
+
+Publication
+===========
+
+The master branch is automatically synced to [wpt.live](https://wpt.live/) and
+[w3c-test.org](https://w3c-test.org/).
+
+Contributing
+============
+
+Save the Web, Write Some Tests!
+
+Absolutely everyone is welcome to contribute to test development. No
+test is too small or too simple, especially if it corresponds to
+something for which you've noted an interoperability bug in a browser.
+
+The way to contribute is just as usual:
+
+* Fork this repository (and make sure you're still relatively in sync
+ with it if you forked a while ago).
+* Create a branch for your changes:
+ `git checkout -b topic`.
+* Make your changes.
+* Run `./wpt lint` as described above.
+* Commit locally and push that to your repo.
+* Create a pull request based on the above.
+
+Issues with web-platform-tests
+------------------------------
+
+If you spot an issue with a test and are not comfortable providing a
+pull request per above to fix it, please
+[file a new issue](https://github.com/web-platform-tests/wpt/issues/new).
+Thank you!
diff --git a/test/wpt/tests/common/CustomCorsResponse.py b/test/wpt/tests/common/CustomCorsResponse.py
new file mode 100644
index 0000000..fc4d122
--- /dev/null
+++ b/test/wpt/tests/common/CustomCorsResponse.py
@@ -0,0 +1,30 @@
+import json
+
+def main(request, response):
+ '''Handler for getting an HTTP response customised by the given query
+ parameters.
+
+ The returned response will have
+ - HTTP headers defined by the 'headers' query parameter
+ - Must be a serialized JSON dictionary mapping header names to header
+ values
+ - HTTP status code defined by the 'status' query parameter
+ - Must be a positive serialized JSON integer like the string '200'
+ - Response content defined by the 'content' query parameter
+ - Must be a serialized JSON string representing the desired response body
+ '''
+ def query_parameter_or_default(param, default):
+ return request.GET.first(param) if param in request.GET else default
+
+ headers = json.loads(query_parameter_or_default(b'headers', b'"{}"'))
+ for k, v in headers.items():
+ response.headers.set(k, v)
+
+ # Note that, in order to have out-of-the-box support for tests that don't call
+ # setup({'allow_uncaught_exception': true})
+ # we return a no-op JS payload. This approach will avoid syntax errors in
+ # script resources that would otherwise cause the test harness to fail.
+ response.content = json.loads(query_parameter_or_default(b'content',
+ b'"/* CustomCorsResponse.py content */"'))
+ response.status_code = json.loads(query_parameter_or_default(b'status',
+ b'200'))
diff --git a/test/wpt/tests/common/META.yml b/test/wpt/tests/common/META.yml
new file mode 100644
index 0000000..ca4d2e5
--- /dev/null
+++ b/test/wpt/tests/common/META.yml
@@ -0,0 +1,3 @@
+suggested_reviewers:
+ - zqzhang
+ - deniak
diff --git a/test/wpt/tests/common/PrefixedLocalStorage.js b/test/wpt/tests/common/PrefixedLocalStorage.js
new file mode 100644
index 0000000..2f4e7b6
--- /dev/null
+++ b/test/wpt/tests/common/PrefixedLocalStorage.js
@@ -0,0 +1,116 @@
+/**
+ * Supports pseudo-"namespacing" localStorage for a given test
+ * by generating and using a unique prefix for keys. Why trounce on other
+ * tests' localStorage items when you can keep it "separated"?
+ *
+ * PrefixedLocalStorageTest: Instantiate in testharness.js tests to generate
+ * a new unique-ish prefix
+ * PrefixedLocalStorageResource: Instantiate in supporting test resource
+ * files to use/share a prefix generated by a test.
+ */
+var PrefixedLocalStorage = function () {
+ this.prefix = ''; // Prefix for localStorage keys
+ this.param = 'prefixedLocalStorage'; // Param to use in querystrings
+};
+
+PrefixedLocalStorage.prototype.clear = function () {
+ if (this.prefix === '') { return; }
+ Object.keys(localStorage).forEach(sKey => {
+ if (sKey.indexOf(this.prefix) === 0) {
+ localStorage.removeItem(sKey);
+ }
+ });
+};
+
+/**
+ * Append/replace prefix parameter and value in URI querystring
+ * Use to generate URLs to resource files that will share the prefix.
+ */
+PrefixedLocalStorage.prototype.url = function (uri) {
+ function updateUrlParameter (uri, key, value) {
+ var i = uri.indexOf('#');
+ var hash = (i === -1) ? '' : uri.substr(i);
+ uri = (i === -1) ? uri : uri.substr(0, i);
+ var re = new RegExp(`([?&])${key}=.*?(&|$)`, 'i');
+ var separator = uri.indexOf('?') !== -1 ? '&' : '?';
+ uri = (uri.match(re)) ? uri.replace(re, `$1${key}=${value}$2`) :
+ `${uri}${separator}${key}=${value}`;
+ return uri + hash;
+ }
+ return updateUrlParameter(uri, this.param, this.prefix);
+};
+
+PrefixedLocalStorage.prototype.prefixedKey = function (baseKey) {
+ return `${this.prefix}${baseKey}`;
+};
+
+PrefixedLocalStorage.prototype.setItem = function (baseKey, value) {
+ localStorage.setItem(this.prefixedKey(baseKey), value);
+};
+
+/**
+ * Listen for `storage` events pertaining to a particular key,
+ * prefixed with this object's prefix. Ignore when value is being set to null
+ * (i.e. removeItem).
+ */
+PrefixedLocalStorage.prototype.onSet = function (baseKey, fn) {
+ window.addEventListener('storage', e => {
+ var match = this.prefixedKey(baseKey);
+ if (e.newValue !== null && e.key.indexOf(match) === 0) {
+ fn.call(this, e);
+ }
+ });
+};
+
+/*****************************************************************************
+ * Use in a testharnessjs test to generate a new key prefix.
+ * async_test(t => {
+ * var prefixedStorage = new PrefixedLocalStorageTest();
+ * t.add_cleanup(() => prefixedStorage.cleanup());
+ * /...
+ * });
+ */
+var PrefixedLocalStorageTest = function () {
+ PrefixedLocalStorage.call(this);
+ this.prefix = `${document.location.pathname}-${Math.random()}-${Date.now()}-`;
+};
+PrefixedLocalStorageTest.prototype = Object.create(PrefixedLocalStorage.prototype);
+PrefixedLocalStorageTest.prototype.constructor = PrefixedLocalStorageTest;
+
+/**
+ * Use in a cleanup function to clear out prefixed entries in localStorage
+ */
+PrefixedLocalStorageTest.prototype.cleanup = function () {
+ this.setItem('closeAll', 'true');
+ this.clear();
+};
+
+/*****************************************************************************
+ * Use in test resource files to share a prefix generated by a
+ * PrefixedLocalStorageTest. Will look in URL querystring for prefix.
+ * Setting `close_on_cleanup` opt truthy will make this script's window listen
+ * for storage `closeAll` event from controlling test and close itself.
+ *
+ * var PrefixedLocalStorageResource({ close_on_cleanup: true });
+ */
+var PrefixedLocalStorageResource = function (options) {
+ PrefixedLocalStorage.call(this);
+ this.options = Object.assign({}, {
+ close_on_cleanup: false
+ }, options || {});
+ // Check URL querystring for prefix to use
+ var regex = new RegExp(`[?&]${this.param}(=([^&#]*)|&|#|$)`),
+ results = regex.exec(document.location.href);
+ if (results && results[2]) {
+ this.prefix = results[2];
+ }
+ // Optionally have this window close itself when the PrefixedLocalStorageTest
+ // sets a `closeAll` item.
+ if (this.options.close_on_cleanup) {
+ this.onSet('closeAll', () => {
+ window.close();
+ });
+ }
+};
+PrefixedLocalStorageResource.prototype = Object.create(PrefixedLocalStorage.prototype);
+PrefixedLocalStorageResource.prototype.constructor = PrefixedLocalStorageResource;
diff --git a/test/wpt/tests/common/PrefixedLocalStorage.js.headers b/test/wpt/tests/common/PrefixedLocalStorage.js.headers
new file mode 100644
index 0000000..6805c32
--- /dev/null
+++ b/test/wpt/tests/common/PrefixedLocalStorage.js.headers
@@ -0,0 +1 @@
+Content-Type: text/javascript; charset=utf-8
diff --git a/test/wpt/tests/common/PrefixedPostMessage.js b/test/wpt/tests/common/PrefixedPostMessage.js
new file mode 100644
index 0000000..674b528
--- /dev/null
+++ b/test/wpt/tests/common/PrefixedPostMessage.js
@@ -0,0 +1,100 @@
+/**
+ * Supports pseudo-"namespacing" for window-posted messages for a given test
+ * by generating and using a unique prefix that gets wrapped into message
+ * objects. This makes it more feasible to have multiple tests that use
+ * `window.postMessage` in a single test file. Basically, make it possible
+ * for the each test to listen for only the messages that are pertinent to it.
+ *
+ * 'Prefix' not an elegant term to use here but this models itself after
+ * PrefixedLocalStorage.
+ *
+ * PrefixedMessageTest: Instantiate in testharness.js tests to generate
+ * a new unique-ish prefix that can be used by other test support files
+ * PrefixedMessageResource: Instantiate in supporting test resource
+ * files to use/share a prefix generated by a test.
+ */
+var PrefixedMessage = function () {
+ this.prefix = '';
+ this.param = 'prefixedMessage'; // Param to use in querystrings
+};
+
+/**
+ * Generate a URL that adds/replaces param with this object's prefix
+ * Use to link to test support files that make use of
+ * PrefixedMessageResource.
+ */
+PrefixedMessage.prototype.url = function (uri) {
+ function updateUrlParameter (uri, key, value) {
+ var i = uri.indexOf('#');
+ var hash = (i === -1) ? '' : uri.substr(i);
+ uri = (i === -1) ? uri : uri.substr(0, i);
+ var re = new RegExp(`([?&])${key}=.*?(&|$)`, 'i');
+ var separator = uri.indexOf('?') !== -1 ? '&' : '?';
+ uri = (uri.match(re)) ? uri.replace(re, `$1${key}=${value}$2`) :
+ `${uri}${separator}${key}=${value}`;
+ return uri + hash;
+ }
+ return updateUrlParameter(uri, this.param, this.prefix);
+};
+
+/**
+ * Add an eventListener on `message` but only invoke the given callback
+ * for messages whose object contains this object's prefix. Remove the
+ * event listener once the anticipated message has been received.
+ */
+PrefixedMessage.prototype.onMessage = function (fn) {
+ window.addEventListener('message', e => {
+ if (typeof e.data === 'object' && e.data.hasOwnProperty('prefix')) {
+ if (e.data.prefix === this.prefix) {
+ // Only invoke callback when `data` is an object containing
+ // a `prefix` key with this object's prefix value
+ // Note fn is invoked with "unwrapped" data first, then the event `e`
+ // (which contains the full, wrapped e.data should it be needed)
+ fn.call(this, e.data.data, e);
+ window.removeEventListener('message', fn);
+ }
+ }
+ });
+};
+
+/**
+ * Instantiate in a test file (e.g. during `setup`) to create a unique-ish
+ * prefix that can be shared by support files
+ */
+var PrefixedMessageTest = function () {
+ PrefixedMessage.call(this);
+ this.prefix = `${document.location.pathname}-${Math.random()}-${Date.now()}-`;
+};
+PrefixedMessageTest.prototype = Object.create(PrefixedMessage.prototype);
+PrefixedMessageTest.prototype.constructor = PrefixedMessageTest;
+
+/**
+ * Instantiate in a test support script to use a "prefix" generated by a
+ * PrefixedMessageTest in a controlling test file. It will look for
+ * the prefix in a URL param (see also PrefixedMessage#url)
+ */
+var PrefixedMessageResource = function () {
+ PrefixedMessage.call(this);
+ // Check URL querystring for prefix to use
+ var regex = new RegExp(`[?&]${this.param}(=([^&#]*)|&|#|$)`),
+ results = regex.exec(document.location.href);
+ if (results && results[2]) {
+ this.prefix = results[2];
+ }
+};
+PrefixedMessageResource.prototype = Object.create(PrefixedMessage.prototype);
+PrefixedMessageResource.prototype.constructor = PrefixedMessageResource;
+
+/**
+ * This is how a test resource document can "send info" to its
+ * opener context. It will whatever message is being sent (`data`) in
+ * an object that injects the prefix.
+ */
+PrefixedMessageResource.prototype.postToOpener = function (data) {
+ if (window.opener) {
+ window.opener.postMessage({
+ prefix: this.prefix,
+ data: data
+ }, '*');
+ }
+};
diff --git a/test/wpt/tests/common/PrefixedPostMessage.js.headers b/test/wpt/tests/common/PrefixedPostMessage.js.headers
new file mode 100644
index 0000000..6805c32
--- /dev/null
+++ b/test/wpt/tests/common/PrefixedPostMessage.js.headers
@@ -0,0 +1 @@
+Content-Type: text/javascript; charset=utf-8
diff --git a/test/wpt/tests/common/README.md b/test/wpt/tests/common/README.md
new file mode 100644
index 0000000..9aef19c
--- /dev/null
+++ b/test/wpt/tests/common/README.md
@@ -0,0 +1,10 @@
+The files in this directory are non-infrastructure support files that can be used by tests.
+
+* `blank.html` - An empty HTML document.
+* `domain-setter.sub.html` - An HTML document that sets `document.domain`.
+* `dummy.xhtml` - An XHTML document.
+* `dummy.xml` - An XML document.
+* `text-plain.txt` - A text/plain document.
+* `*.js` - Utility scripts. These are documented in the source.
+* `*.py` - wptserve [Python Handlers](https://web-platform-tests.org/writing-tests/python-handlers/). These are documented in the source.
+* `security-features` - Documented in `security-features/README.md`.
diff --git a/test/wpt/tests/common/__init__.py b/test/wpt/tests/common/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/wpt/tests/common/__init__.py
diff --git a/test/wpt/tests/common/arrays.js b/test/wpt/tests/common/arrays.js
new file mode 100644
index 0000000..2b31bb4
--- /dev/null
+++ b/test/wpt/tests/common/arrays.js
@@ -0,0 +1,31 @@
+/**
+ * Callback for checking equality of c and d.
+ *
+ * @callback equalityCallback
+ * @param {*} c
+ * @param {*} d
+ * @returns {boolean}
+ */
+
+/**
+ * Returns true if the given arrays are equal. Optionally can pass an equality function.
+ * @param {Array} a
+ * @param {Array} b
+ * @param {equalityCallback} callbackFunction - defaults to `c === d`
+ * @returns {boolean}
+ */
+export function areArraysEqual(a, b, equalityFunction = (c, d) => { return c === d; }) {
+ try {
+ if (a.length !== b.length)
+ return false;
+
+ for (let i = 0; i < a.length; i++) {
+ if (!equalityFunction(a[i], b[i]))
+ return false;
+ }
+ } catch (ex) {
+ return false;
+ }
+
+ return true;
+}
diff --git a/test/wpt/tests/common/blank-with-cors.html b/test/wpt/tests/common/blank-with-cors.html
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/wpt/tests/common/blank-with-cors.html
diff --git a/test/wpt/tests/common/blank-with-cors.html.headers b/test/wpt/tests/common/blank-with-cors.html.headers
new file mode 100644
index 0000000..cb762ef
--- /dev/null
+++ b/test/wpt/tests/common/blank-with-cors.html.headers
@@ -0,0 +1 @@
+Access-Control-Allow-Origin: *
diff --git a/test/wpt/tests/common/blank.html b/test/wpt/tests/common/blank.html
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/wpt/tests/common/blank.html
diff --git a/test/wpt/tests/common/custom-cors-response.js b/test/wpt/tests/common/custom-cors-response.js
new file mode 100644
index 0000000..be9c7ce
--- /dev/null
+++ b/test/wpt/tests/common/custom-cors-response.js
@@ -0,0 +1,32 @@
+const custom_cors_response = (payload, base_url) => {
+ base_url = base_url || new URL(location.href);
+
+ // Clone the given `payload` so that, as we modify it, we won't be mutating
+ // the caller's value in unexpected ways.
+ payload = Object.assign({}, payload);
+ payload.headers = payload.headers || {};
+ // Note that, in order to have out-of-the-box support for tests that don't
+ // call `setup({'allow_uncaught_exception': true})` we return a no-op JS
+ // payload. This approach will avoid hitting syntax errors if the resource is
+ // interpreted as script. Without this workaround, the SyntaxError would be
+ // caught by the test harness and trigger a test failure.
+ payload.content = payload.content || '/* custom-cors-response.js content */';
+ payload.status_code = payload.status_code || 200;
+
+ // Assume that we'll be doing a CORS-enabled fetch so we'll need to set ACAO.
+ const acao = "Access-Control-Allow-Origin";
+ if (!(acao in payload.headers)) {
+ payload.headers[acao] = '*';
+ }
+
+ if (!("Content-Type" in payload.headers)) {
+ payload.headers["Content-Type"] = "text/javascript";
+ }
+
+ let ret = new URL("/common/CustomCorsResponse.py", base_url);
+ for (const key in payload) {
+ ret.searchParams.append(key, JSON.stringify(payload[key]));
+ }
+
+ return ret;
+};
diff --git a/test/wpt/tests/common/dispatcher/README.md b/test/wpt/tests/common/dispatcher/README.md
new file mode 100644
index 0000000..cfaafb6
--- /dev/null
+++ b/test/wpt/tests/common/dispatcher/README.md
@@ -0,0 +1,228 @@
+# `RemoteContext`: API for script execution in another context
+
+`RemoteContext` in `/common/dispatcher/dispatcher.js` provides an interface to
+execute JavaScript in another global object (page or worker, the "executor"),
+based on:
+
+- [WPT RFC 88: context IDs from uuid searchParams in URL](https://github.com/web-platform-tests/rfcs/pull/88),
+- [WPT RFC 89: execute_script](https://github.com/web-platform-tests/rfcs/pull/89) and
+- [WPT RFC 91: RemoteContext](https://github.com/web-platform-tests/rfcs/pull/91).
+
+Tests can send arbitrary javascript to executors to evaluate in its global
+object, like:
+
+```
+// injector.html
+const argOnLocalContext = ...;
+
+async function execute() {
+ window.open('executor.html?uuid=' + uuid);
+ const ctx = new RemoteContext(uuid);
+ await ctx.execute_script(
+ (arg) => functionOnRemoteContext(arg),
+ [argOnLocalContext]);
+};
+```
+
+and on executor:
+
+```
+// executor.html
+function functionOnRemoteContext(arg) { ... }
+
+const uuid = new URLSearchParams(window.location.search).get('uuid');
+const executor = new Executor(uuid);
+```
+
+For concrete examples, see
+[events.html](../../html/browsers/browsing-the-web/back-forward-cache/events.html)
+and
+[executor.html](../../html/browsers/browsing-the-web/back-forward-cache/resources/executor.html)
+in back-forward cache tests.
+
+Note that `executor*` files under `/common/dispatcher/` are NOT for
+`RemoteContext.execute_script()`. Use `remote-executor.html` instead.
+
+This is universal and avoids introducing many specific `XXX-helper.html`
+resources.
+Moreover, tests are easier to read, because the whole logic of the test can be
+defined in a single file.
+
+## `new RemoteContext(uuid)`
+
+- `uuid` is a UUID string that identifies the remote context and should match
+ with the `uuid` parameter of the URL of the remote context.
+- Callers should create the remote context outside this constructor (e.g.
+ `window.open('executor.html?uuid=' + uuid)`).
+
+## `RemoteContext.execute_script(fn, args)`
+
+- `fn` is a JavaScript function to execute on the remote context, which is
+ converted to a string using `toString()` and sent to the remote context.
+- `args` is null or an array of arguments to pass to the function on the
+ remote context. Arguments are passed as JSON.
+- If the return value of `fn` when executed in the remote context is a promise,
+ the promise returned by `execute_script` resolves to the resolved value of
+ that promise. Otherwise the `execute_script` promise resolves to the return
+ value of `fn`.
+
+Note that `fn` is evaluated on the remote context (`executor.html` in the
+example above), while `args` are evaluated on the caller context
+(`injector.html`) and then passed to the remote context.
+
+## Return value of injected functions and `execute_script()`
+
+If the return value of the injected function when executed in the remote
+context is a promise, the promise returned by `execute_script` resolves to the
+resolved value of that promise. Otherwise the `execute_script` promise resolves
+to the return value of the function.
+
+When the return value of an injected script is a Promise, it should be resolved
+before any navigation starts on the remote context. For example, it shouldn't
+be resolved after navigating out and navigating back to the page again.
+It's fine to create a Promise to be resolved after navigations, if it's not the
+return value of the injected function.
+
+## Calling timing of `execute_script()`
+
+When `RemoteContext.execute_script()` is called when the remote context is not
+active (for example before it is created, before navigation to the page, or
+during the page is in back-forward cache), the injected script is evaluated
+after the remote context becomes active.
+
+Multiple calls to `RemoteContext.execute_script()` will result in multiple scripts
+being executed in remote context and ordering will be maintained.
+
+## Errors from `execute_script()`
+
+Errors from `execute_script()` will result in promise rejections, so it is
+important to await the result. This can be `await ctx.execute_script(...)` for
+every call but if there are multiple scripts to executed, it may be preferable
+to wait on them in parallel to avoid incurring full round-trip time for each,
+e.g.
+
+```js
+await Promise.all(
+ ctx1.execute_script(...),
+ ctx1.execute_script(...),
+ ctx2.execute_script(...),
+ ctx2.execute_script(...),
+ ...
+)
+```
+
+## Evaluation timing of injected functions
+
+The script injected by `RemoteContext.execute_script()` can be evaluated any
+time during the remote context is active.
+For example, even before DOMContentLoaded events or even during navigation.
+It's the responsibility of test-specific code/helpers to ensure evaluation
+timing constraints (which can be also test-specific), if any needed.
+
+### Ensuring evaluation timing around page load
+
+For example, to ensure that injected functions (`mainFunction` below) are
+evaluated after the first `pageshow` event, we can use pure JavaScript code
+like below:
+
+```
+// executor.html
+window.pageShowPromise = new Promise(resolve =>
+ window.addEventListener('pageshow', resolve, {once: true}));
+
+
+// injector.html
+const waitForPageShow = async () => {
+ while (!window.pageShowPromise) {
+ await new Promise(resolve => setTimeout(resolve, 100));
+ }
+ await window.pageShowPromise;
+};
+
+await ctx.execute(waitForPageShow);
+await ctx.execute(mainFunction);
+```
+
+### Ensuring evaluation timing around navigation out/unloading
+
+It can be important to ensure there are no injected functions nor code behind
+`RemoteContext` (such as Fetch APIs accessing server-side stash) running after
+navigation is initiated, for example in the case of back-forward cache testing.
+
+To ensure this,
+
+- Do not call the next `RemoteContext.execute()` for the remote context after
+ triggering the navigation, until we are sure that the remote context is not
+ active (e.g. after we confirm that the new page is loaded).
+- Call `Executor.suspend(callback)` synchronously within the injected script.
+ This suspends executor-related code, and calls `callback` when it is ready
+ to start navigation.
+
+The code on the injector side would be like:
+
+```
+// injector.html
+await ctx.execute_script(() => {
+ executor.suspend(() => {
+ location.href = 'new-url.html';
+ });
+});
+```
+
+## Future Work: Possible integration with `test_driver`
+
+Currently `RemoteContext` is implemented by JavaScript and WPT-server-side
+stash, and not integrated with `test_driver` nor `testharness`.
+There is a proposal of `test_driver`-integrated version (see the RFCs listed
+above).
+
+The API semantics and guidelines in this document are designed to be applicable
+to both the current stash-based `RemoteContext` and `test_driver`-based
+version, and thus the tests using `RemoteContext` will be migrated with minimum
+modifications (mostly in `/common/dispatcher/dispatcher.js` and executors), for
+example in a
+[draft CL](https://chromium-review.googlesource.com/c/chromium/src/+/3082215/).
+
+
+# `send()`/`receive()` Message passing APIs
+
+`dispatcher.js` (and its server-side backend `dispatcher.py`) provides a
+universal queue-based message passing API.
+Each queue is identified by a UUID, and accessed via the following APIs:
+
+- `send(uuid, message)` pushes a string `message` to the queue `uuid`.
+- `receive(uuid)` pops the first item from the queue `uuid`.
+- `showRequestHeaders(origin, uuid)` and
+ `cacheableShowRequestHeaders(origin, uuid)` return URLs, that push request
+ headers to the queue `uuid` upon fetching.
+
+It works cross-origin, and even access different browser context groups.
+
+Messages are queued, this means one doesn't need to wait for the receiver to
+listen, before sending the first message
+(but still need to wait for the resolution of the promise returned by `send()`
+to ensure the order between `send()`s).
+
+## Executors
+
+Similar to `RemoteContext.execute_script()`, `send()`/`receive()` can be used
+for sending arbitrary javascript to be evaluated in another page or worker.
+
+- `executor.html` (as a Document),
+- `executor-worker.js` (as a Web Worker), and
+- `executor-service-worker.js` (as a Service Worker)
+
+are examples of executors.
+Note that these executors are NOT compatible with
+`RemoteContext.execute_script()`.
+
+## Future Work
+
+`send()`, `receive()` and the executors below are kept for COEP/COOP tests.
+
+For remote script execution, new tests should use
+`RemoteContext.execute_script()` instead.
+
+For message passing,
+[WPT RFC 90](https://github.com/web-platform-tests/rfcs/pull/90) is still under
+discussion.
diff --git a/test/wpt/tests/common/dispatcher/dispatcher.js b/test/wpt/tests/common/dispatcher/dispatcher.js
new file mode 100644
index 0000000..a0f9f43
--- /dev/null
+++ b/test/wpt/tests/common/dispatcher/dispatcher.js
@@ -0,0 +1,256 @@
+// 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;
+
+// Return a promise, limiting the number of concurrent accesses to a shared
+// resources to |max_concurrent_access|.
+const concurrencyLimiter = (max_concurrency) => {
+ let pending = 0;
+ let waiting = [];
+ return async (task) => {
+ pending++;
+ if (pending > max_concurrency)
+ await new Promise(resolve => waiting.push(resolve));
+ let result = await task();
+ pending--;
+ waiting.shift()?.();
+ return result;
+ };
+}
+
+// Wait for a random amount of time in the range [10ms,100ms].
+const randomDelay = () => {
+ return new Promise(resolve => setTimeout(resolve, 10 + 90*Math.random()));
+}
+
+// Sending too many requests in parallel causes congestion. Limiting it improves
+// throughput.
+//
+// Note: The following table has been determined on the test:
+// ../cache-storage.tentative.https.html
+// using Chrome with a 64 core CPU / 64GB ram, in release mode:
+// ┌───────────┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬────â”
+// │concurrency│ 1 │ 2 │ 3 │ 4 │ 5 │ 6 │ 10│ 15│ 20│ 30│ 50│ 100│
+// ├───────────┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼────┤
+// │time (s) │ 54│ 38│ 31│ 29│ 26│ 24│ 22│ 22│ 22│ 22│ 34│ 36 │
+// └───────────┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴────┘
+const limiter = concurrencyLimiter(6);
+
+// While requests to different remote contexts can go in parallel, we need to
+// ensure that requests to each remote context are done in order. This maps a
+// uuid to a queue of requests to send. A queue is processed until it is empty
+// and then is deleted from the map.
+const sendQueues = new Map();
+
+// Sends a single item (with rate-limiting) and calls the associated resolver
+// when it is successfully sent.
+const sendItem = async function (uuid, resolver, message) {
+ await limiter(async () => {
+ // Requests might be dropped. Retry until getting a confirmation it has been
+ // processed.
+ while(1) {
+ try {
+ let response = await fetch(dispatcher_url + `?uuid=${uuid}`, {
+ method: 'POST',
+ body: message
+ })
+ if (await response.text() == "done") {
+ resolver();
+ return;
+ }
+ } catch (fetch_error) {}
+ await randomDelay();
+ };
+ });
+}
+
+// While the queue is non-empty, send the next item. This is async and new items
+// may be added to the queue while others are being sent.
+const processQueue = async function (uuid, queue) {
+ while (queue.length) {
+ const [resolver, message] = queue.shift();
+ await sendItem(uuid, resolver, message);
+ }
+ // The queue is empty, delete it.
+ sendQueues.delete(uuid);
+}
+
+const send = async function (uuid, message) {
+ const itemSentPromise = new Promise((resolve) => {
+ const item = [resolve, message];
+ if (sendQueues.has(uuid)) {
+ // There is already a queue for `uuid`, just add to it and it will be processed.
+ sendQueues.get(uuid).push(item);
+ } else {
+ // There is no queue for `uuid`, create it and start processing.
+ const queue = [item];
+ sendQueues.set(uuid, queue);
+ processQueue(uuid, queue);
+ }
+ });
+ // Wait until the item has been successfully sent.
+ await itemSentPromise;
+}
+
+const receive = async function (uuid) {
+ while(1) {
+ let data = "not ready";
+ try {
+ data = await limiter(async () => {
+ let response = await fetch(dispatcher_url + `?uuid=${uuid}`);
+ return await response.text();
+ });
+ } catch (fetch_error) {}
+
+ if (data == "not ready") {
+ await randomDelay();
+ continue;
+ }
+
+ return data;
+ }
+}
+
+// Returns an URL. When called, the server sends toward the `uuid` queue the
+// request headers. Useful for determining if something was requested with
+// Cookies.
+const showRequestHeaders = function(origin, uuid) {
+ return origin + dispatcher_path + `?uuid=${uuid}&show-headers`;
+}
+
+// Same as above, except for the response is cacheable.
+const cacheableShowRequestHeaders = function(origin, uuid) {
+ return origin + dispatcher_path + `?uuid=${uuid}&cacheable&show-headers`;
+}
+
+// This script requires
+// - `/common/utils.js` for `token()`.
+
+// Returns the URL of a document that can be used as a `RemoteContext`.
+//
+// `uuid` should be a UUID uniquely identifying the given remote context.
+// `options` has the following shape:
+//
+// {
+// host: (optional) Sets the returned URL's `host` property. Useful for
+// cross-origin executors.
+// protocol: (optional) Sets the returned URL's `protocol` property.
+// }
+function remoteExecutorUrl(uuid, options) {
+ const url = new URL("/common/dispatcher/remote-executor.html", location);
+ url.searchParams.set("uuid", uuid);
+
+ if (options?.host) {
+ url.host = options.host;
+ }
+
+ if (options?.protocol) {
+ url.protocol = options.protocol;
+ }
+
+ return url;
+}
+
+// Represents a remote executor. For more detailed explanation see `README.md`.
+class RemoteContext {
+ // `uuid` is a UUID string that identifies the remote context and should
+ // match with the `uuid` parameter of the URL of the remote context.
+ constructor(uuid) {
+ this.context_id = uuid;
+ }
+
+ // Evaluates the script `expr` on the executor.
+ // - If `expr` is evaluated to a Promise that is resolved with a value:
+ // `execute_script()` returns a Promise resolved with the value.
+ // - If `expr` is evaluated to a non-Promise value:
+ // `execute_script()` returns a Promise resolved with the value.
+ // - If `expr` throws an error or is evaluated to a Promise that is rejected:
+ // `execute_script()` returns a rejected Promise with the error's
+ // `message`.
+ // Note that currently the type of error (e.g. DOMException) is not
+ // preserved, except for `TypeError`.
+ // The values should be able to be serialized by JSON.stringify().
+ async execute_script(fn, args) {
+ const receiver = token();
+ await this.send({receiver: receiver, fn: fn.toString(), args: args});
+ const response = JSON.parse(await receive(receiver));
+ if (response.status === 'success') {
+ return response.value;
+ }
+
+ // exception
+ if (response.name === 'TypeError') {
+ throw new TypeError(response.value);
+ }
+ throw new Error(response.value);
+ }
+
+ async send(msg) {
+ return await send(this.context_id, JSON.stringify(msg));
+ }
+};
+
+class Executor {
+ constructor(uuid) {
+ this.uuid = uuid;
+
+ // If `suspend_callback` is not `null`, the executor should be suspended
+ // when there are no ongoing tasks.
+ this.suspend_callback = null;
+
+ this.execute();
+ }
+
+ // Wait until there are no ongoing tasks nor fetch requests for polling
+ // tasks, and then suspend the executor and call `callback()`.
+ // Navigation from the executor page should be triggered inside `callback()`,
+ // to avoid conflict with in-flight fetch requests.
+ suspend(callback) {
+ this.suspend_callback = callback;
+ }
+
+ resume() {
+ }
+
+ async execute() {
+ while(true) {
+ if (this.suspend_callback !== null) {
+ this.suspend_callback();
+ this.suspend_callback = null;
+ // Wait for `resume()` to be called.
+ await new Promise(resolve => this.resume = resolve);
+
+ // Workaround for https://crbug.com/1244230.
+ // Without this workaround, the executor is resumed and the fetch
+ // request to poll the next task is initiated synchronously from
+ // pageshow event after the page restored from BFCache, and the fetch
+ // request promise is never resolved (and thus the test results in
+ // timeout) due to https://crbug.com/1244230. The root cause is not yet
+ // known, but setTimeout() with 0ms causes the resume triggered on
+ // another task and seems to resolve the issue.
+ await new Promise(resolve => setTimeout(resolve, 0));
+
+ continue;
+ }
+
+ const task = JSON.parse(await receive(this.uuid));
+
+ let response;
+ try {
+ const value = await eval(task.fn).apply(null, task.args);
+ response = JSON.stringify({
+ status: 'success',
+ value: value
+ });
+ } catch(e) {
+ response = JSON.stringify({
+ status: 'exception',
+ name: e.name,
+ value: e.message
+ });
+ }
+ await send(task.receiver, response);
+ }
+ }
+}
diff --git a/test/wpt/tests/common/dispatcher/dispatcher.py b/test/wpt/tests/common/dispatcher/dispatcher.py
new file mode 100644
index 0000000..9fe7a38
--- /dev/null
+++ b/test/wpt/tests/common/dispatcher/dispatcher.py
@@ -0,0 +1,53 @@
+import json
+from wptserve.utils import isomorphic_decode
+
+# A server used to store and retrieve arbitrary data.
+# This is used by: ./dispatcher.js
+def main(request, response):
+ # This server is configured so that is accept to receive any requests and
+ # any cookies the web browser is willing to send.
+ response.headers.set(b"Access-Control-Allow-Credentials", b"true")
+ response.headers.set(b'Access-Control-Allow-Methods', b'OPTIONS, GET, POST')
+ response.headers.set(b'Access-Control-Allow-Headers', b'Content-Type')
+ response.headers.set(b"Access-Control-Allow-Origin", request.headers.get(b"origin") or '*')
+
+ if b"cacheable" in request.GET:
+ response.headers.set(b"Cache-Control", b"max-age=31536000")
+ else:
+ response.headers.set(b'Cache-Control', b'no-cache, no-store, must-revalidate')
+
+ # CORS preflight
+ if request.method == u'OPTIONS':
+ return b''
+
+ uuid = request.GET[b'uuid']
+ stash = request.server.stash;
+
+ # The stash is accessed concurrently by many clients. A lock is used to
+ # avoid unterleaved read/write from different clients.
+ with stash.lock:
+ queue = stash.take(uuid, '/common/dispatcher') or [];
+
+ # Push into the |uuid| queue, the requested headers.
+ if b"show-headers" in request.GET:
+ headers = {};
+ for key, value in request.headers.items():
+ headers[isomorphic_decode(key)] = isomorphic_decode(request.headers[key])
+ headers = json.dumps(headers);
+ queue.append(headers);
+ ret = b'';
+
+ # Push into the |uuid| queue, the posted data.
+ elif request.method == u'POST':
+ queue.append(request.body)
+ ret = b'done'
+
+ # Pull from the |uuid| queue, the posted data.
+ else:
+ if len(queue) == 0:
+ ret = b'not ready'
+ else:
+ ret = queue.pop(0)
+
+ stash.put(uuid, queue, '/common/dispatcher')
+ return ret;
diff --git a/test/wpt/tests/common/dispatcher/executor-service-worker.js b/test/wpt/tests/common/dispatcher/executor-service-worker.js
new file mode 100644
index 0000000..0b47d66
--- /dev/null
+++ b/test/wpt/tests/common/dispatcher/executor-service-worker.js
@@ -0,0 +1,24 @@
+importScripts('./dispatcher.js');
+
+const params = new URLSearchParams(location.search);
+const uuid = params.get('uuid');
+
+// The fetch handler must be registered before parsing the main script response.
+// So do it here, for future use.
+fetchHandler = () => {}
+addEventListener('fetch', e => {
+ fetchHandler(e);
+});
+
+// Force ServiceWorker to immediately activate itself.
+addEventListener('install', event => {
+ skipWaiting();
+});
+
+let executeOrders = async function() {
+ while(true) {
+ let task = await receive(uuid);
+ eval(`(async () => {${task}})()`);
+ }
+};
+executeOrders();
diff --git a/test/wpt/tests/common/dispatcher/executor-worker.js b/test/wpt/tests/common/dispatcher/executor-worker.js
new file mode 100644
index 0000000..ea065a6
--- /dev/null
+++ b/test/wpt/tests/common/dispatcher/executor-worker.js
@@ -0,0 +1,12 @@
+importScripts('./dispatcher.js');
+
+const params = new URLSearchParams(location.search);
+const uuid = params.get('uuid');
+
+let executeOrders = async function() {
+ while(true) {
+ let task = await receive(uuid);
+ eval(`(async () => {${task}})()`);
+ }
+};
+executeOrders();
diff --git a/test/wpt/tests/common/dispatcher/executor.html b/test/wpt/tests/common/dispatcher/executor.html
new file mode 100644
index 0000000..5fe6a95
--- /dev/null
+++ b/test/wpt/tests/common/dispatcher/executor.html
@@ -0,0 +1,15 @@
+<script src="./dispatcher.js"></script>
+<script>
+
+const params = new URLSearchParams(window.location.search);
+const uuid = params.get('uuid');
+
+let executeOrders = async function() {
+ while(true) {
+ let task = await receive(uuid);
+ eval(`(async () => {${task}})()`);
+ }
+};
+executeOrders();
+
+</script>
diff --git a/test/wpt/tests/common/dispatcher/remote-executor.html b/test/wpt/tests/common/dispatcher/remote-executor.html
new file mode 100644
index 0000000..8b00303
--- /dev/null
+++ b/test/wpt/tests/common/dispatcher/remote-executor.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html>
+<meta charset="utf-8">
+<body>
+</body>
+<script src="./dispatcher.js"></script>
+<script>
+ const params = new URLSearchParams(window.location.search);
+ const uuid = params.get('uuid');
+ const executor = new Executor(uuid); // `execute()` is called in constructor.
+</script>
+</html>
diff --git a/test/wpt/tests/common/domain-setter.sub.html b/test/wpt/tests/common/domain-setter.sub.html
new file mode 100644
index 0000000..ad3b9f8
--- /dev/null
+++ b/test/wpt/tests/common/domain-setter.sub.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>A page that will likely be same-origin-domain but not same-origin</title>
+
+<script>
+"use strict";
+document.domain = "{{host}}";
+</script>
diff --git a/test/wpt/tests/common/dummy.xhtml b/test/wpt/tests/common/dummy.xhtml
new file mode 100644
index 0000000..dba6945
--- /dev/null
+++ b/test/wpt/tests/common/dummy.xhtml
@@ -0,0 +1,2 @@
+<!DOCTYPE html>
+<html xmlns="http://www.w3.org/1999/xhtml"><head><title>Dummy XHTML document</title></head><body /></html>
diff --git a/test/wpt/tests/common/dummy.xml b/test/wpt/tests/common/dummy.xml
new file mode 100644
index 0000000..4a60c30
--- /dev/null
+++ b/test/wpt/tests/common/dummy.xml
@@ -0,0 +1 @@
+<foo>Dummy XML document</foo>
diff --git a/test/wpt/tests/common/echo.py b/test/wpt/tests/common/echo.py
new file mode 100644
index 0000000..911b54a
--- /dev/null
+++ b/test/wpt/tests/common/echo.py
@@ -0,0 +1,6 @@
+def main(request, response):
+ # Without X-XSS-Protection to disable non-standard XSS protection the functionality this
+ # resource offers is useless
+ response.headers.set(b"X-XSS-Protection", b"0")
+ response.headers.set(b"Content-Type", b"text/html")
+ response.content = request.GET.first(b"content")
diff --git a/test/wpt/tests/common/gc.js b/test/wpt/tests/common/gc.js
new file mode 100644
index 0000000..ac43a4c
--- /dev/null
+++ b/test/wpt/tests/common/gc.js
@@ -0,0 +1,52 @@
+/**
+ * Does a best-effort attempt at invoking garbage collection. Attempts to use
+ * the standardized `TestUtils.gc()` function, but falls back to other
+ * environment-specific nonstandard functions, with a final result of just
+ * creating a lot of garbage (in which case you will get a console warning).
+ *
+ * This should generally only be used to attempt to trigger bugs and crashes
+ * inside tests, i.e. cases where if garbage collection happened, then this
+ * should not trigger some misbehavior. You cannot rely on garbage collection
+ * successfully trigger, or that any particular unreachable object will be
+ * collected.
+ *
+ * @returns {Promise<undefined>} A promise you should await to ensure garbage
+ * collection has had a chance to complete.
+ */
+self.garbageCollect = async () => {
+ // https://testutils.spec.whatwg.org/#the-testutils-namespace
+ if (self.TestUtils?.gc) {
+ return TestUtils.gc();
+ }
+
+ // Use --expose_gc for V8 (and Node.js)
+ // to pass this flag at chrome launch use: --js-flags="--expose-gc"
+ // Exposed in SpiderMonkey shell as well
+ if (self.gc) {
+ return self.gc();
+ }
+
+ // Present in some WebKit development environments
+ if (self.GCController) {
+ return GCController.collect();
+ }
+
+ console.warn(
+ 'Tests are running without the ability to do manual garbage collection. ' +
+ 'They will still work, but coverage will be suboptimal.');
+
+ for (var i = 0; i < 1000; i++) {
+ gcRec(10);
+ }
+
+ function gcRec(n) {
+ if (n < 1) {
+ return {};
+ }
+
+ let temp = { i: "ab" + i + i / 100000 };
+ temp += "foo";
+
+ gcRec(n - 1);
+ }
+};
diff --git a/test/wpt/tests/common/get-host-info.sub.js b/test/wpt/tests/common/get-host-info.sub.js
new file mode 100644
index 0000000..9b8c2b5
--- /dev/null
+++ b/test/wpt/tests/common/get-host-info.sub.js
@@ -0,0 +1,63 @@
+/**
+ * Host information for cross-origin tests.
+ * @returns {Object} with properties for different host information.
+ */
+function get_host_info() {
+
+ var HTTP_PORT = '{{ports[http][0]}}';
+ var HTTP_PORT2 = '{{ports[http][1]}}';
+ var HTTPS_PORT = '{{ports[https][0]}}';
+ var HTTPS_PORT2 = '{{ports[https][1]}}';
+ var PROTOCOL = self.location.protocol;
+ var IS_HTTPS = (PROTOCOL == "https:");
+ var PORT = IS_HTTPS ? HTTPS_PORT : HTTP_PORT;
+ var PORT2 = IS_HTTPS ? HTTPS_PORT2 : HTTP_PORT2;
+ var HTTP_PORT_ELIDED = HTTP_PORT == "80" ? "" : (":" + HTTP_PORT);
+ var HTTP_PORT2_ELIDED = HTTP_PORT2 == "80" ? "" : (":" + HTTP_PORT2);
+ var HTTPS_PORT_ELIDED = HTTPS_PORT == "443" ? "" : (":" + HTTPS_PORT);
+ var PORT_ELIDED = IS_HTTPS ? HTTPS_PORT_ELIDED : HTTP_PORT_ELIDED;
+ var ORIGINAL_HOST = '{{host}}';
+ var REMOTE_HOST = (ORIGINAL_HOST === 'localhost') ? '127.0.0.1' : ('www1.' + ORIGINAL_HOST);
+ var OTHER_HOST = '{{domains[www2]}}';
+ var NOTSAMESITE_HOST = (ORIGINAL_HOST === 'localhost') ? '127.0.0.1' : ('{{hosts[alt][]}}');
+
+ return {
+ HTTP_PORT: HTTP_PORT,
+ HTTP_PORT2: HTTP_PORT2,
+ HTTPS_PORT: HTTPS_PORT,
+ HTTPS_PORT2: HTTPS_PORT2,
+ PORT: PORT,
+ PORT2: PORT2,
+ ORIGINAL_HOST: ORIGINAL_HOST,
+ REMOTE_HOST: REMOTE_HOST,
+
+ ORIGIN: PROTOCOL + "//" + ORIGINAL_HOST + PORT_ELIDED,
+ HTTP_ORIGIN: 'http://' + ORIGINAL_HOST + HTTP_PORT_ELIDED,
+ HTTPS_ORIGIN: 'https://' + ORIGINAL_HOST + HTTPS_PORT_ELIDED,
+ HTTPS_ORIGIN_WITH_CREDS: 'https://foo:bar@' + ORIGINAL_HOST + HTTPS_PORT_ELIDED,
+ HTTP_ORIGIN_WITH_DIFFERENT_PORT: 'http://' + ORIGINAL_HOST + HTTP_PORT2_ELIDED,
+ REMOTE_ORIGIN: PROTOCOL + "//" + REMOTE_HOST + PORT_ELIDED,
+ OTHER_ORIGIN: PROTOCOL + "//" + OTHER_HOST + PORT_ELIDED,
+ HTTP_REMOTE_ORIGIN: 'http://' + REMOTE_HOST + HTTP_PORT_ELIDED,
+ HTTP_NOTSAMESITE_ORIGIN: 'http://' + NOTSAMESITE_HOST + HTTP_PORT_ELIDED,
+ HTTP_REMOTE_ORIGIN_WITH_DIFFERENT_PORT: 'http://' + REMOTE_HOST + HTTP_PORT2_ELIDED,
+ HTTPS_REMOTE_ORIGIN: 'https://' + REMOTE_HOST + HTTPS_PORT_ELIDED,
+ HTTPS_REMOTE_ORIGIN_WITH_CREDS: 'https://foo:bar@' + REMOTE_HOST + HTTPS_PORT_ELIDED,
+ HTTPS_NOTSAMESITE_ORIGIN: 'https://' + NOTSAMESITE_HOST + HTTPS_PORT_ELIDED,
+ UNAUTHENTICATED_ORIGIN: 'http://' + OTHER_HOST + HTTP_PORT_ELIDED,
+ AUTHENTICATED_ORIGIN: 'https://' + OTHER_HOST + HTTPS_PORT_ELIDED
+ };
+}
+
+/**
+ * When a default port is used, location.port returns the empty string.
+ * This function attempts to provide an exact port, assuming we are running under wptserve.
+ * @param {*} loc - can be Location/<a>/<area>/URL, but assumes http/https only.
+ * @returns {string} The port number.
+ */
+function get_port(loc) {
+ if (loc.port) {
+ return loc.port;
+ }
+ return loc.protocol === 'https:' ? '443' : '80';
+}
diff --git a/test/wpt/tests/common/get-host-info.sub.js.headers b/test/wpt/tests/common/get-host-info.sub.js.headers
new file mode 100644
index 0000000..6805c32
--- /dev/null
+++ b/test/wpt/tests/common/get-host-info.sub.js.headers
@@ -0,0 +1 @@
+Content-Type: text/javascript; charset=utf-8
diff --git a/test/wpt/tests/common/media.js b/test/wpt/tests/common/media.js
new file mode 100644
index 0000000..800593f
--- /dev/null
+++ b/test/wpt/tests/common/media.js
@@ -0,0 +1,61 @@
+/**
+ * Returns the URL of a supported video source based on the user agent
+ * @param {string} base - media URL without file extension
+ * @returns {string}
+ */
+function getVideoURI(base)
+{
+ var extension = '.mp4';
+
+ var videotag = document.createElement("video");
+
+ if ( videotag.canPlayType )
+ {
+ if (videotag.canPlayType('video/webm; codecs="vp9, opus"') )
+ {
+ extension = '.webm';
+ } else if ( videotag.canPlayType('video/ogg; codecs="theora, vorbis"') )
+ {
+ extension = '.ogv';
+ }
+ }
+
+ return base + extension;
+}
+
+/**
+ * Returns the URL of a supported audio source based on the user agent
+ * @param {string} base - media URL without file extension
+ * @returns {string}
+ */
+function getAudioURI(base)
+{
+ var extension = '.mp3';
+
+ var audiotag = document.createElement("audio");
+
+ if ( audiotag.canPlayType &&
+ audiotag.canPlayType('audio/ogg') )
+ {
+ extension = '.oga';
+ }
+
+ return base + extension;
+}
+
+/**
+ * Returns the MIME type for a media URL based on the file extension.
+ * @param {string} url
+ * @returns {string}
+ */
+function getMediaContentType(url) {
+ var extension = new URL(url, location).pathname.split(".").pop();
+ var map = {
+ "mp4" : "video/mp4",
+ "ogv" : "application/ogg",
+ "webm": "video/webm",
+ "mp3" : "audio/mp3",
+ "oga" : "application/ogg",
+ };
+ return map[extension];
+}
diff --git a/test/wpt/tests/common/media.js.headers b/test/wpt/tests/common/media.js.headers
new file mode 100644
index 0000000..6805c32
--- /dev/null
+++ b/test/wpt/tests/common/media.js.headers
@@ -0,0 +1 @@
+Content-Type: text/javascript; charset=utf-8
diff --git a/test/wpt/tests/common/object-association.js b/test/wpt/tests/common/object-association.js
new file mode 100644
index 0000000..669c17c
--- /dev/null
+++ b/test/wpt/tests/common/object-association.js
@@ -0,0 +1,74 @@
+"use strict";
+
+// This is for testing whether an object (e.g., a global property) is associated with Window, or
+// with Document. Recall that Window and Document are 1:1 except when doing a same-origin navigation
+// away from the initial about:blank. In that case the Window object gets reused for the new
+// Document.
+//
+// So:
+// - If something is per-Window, then it should maintain its identity across an about:blank
+// navigation.
+// - If something is per-Document, then it should be recreated across an about:blank navigation.
+
+window.testIsPerWindow = propertyName => {
+ runTests(propertyName, assert_equals, "must not");
+};
+
+window.testIsPerDocument = propertyName => {
+ runTests(propertyName, assert_not_equals, "must");
+};
+
+function runTests(propertyName, equalityOrInequalityAsserter, mustOrMustNotReplace) {
+ async_test(t => {
+ const iframe = document.createElement("iframe");
+ document.body.appendChild(iframe);
+ const frame = iframe.contentWindow;
+
+ const before = frame[propertyName];
+ assert_implements(before, `window.${propertyName} must be implemented`);
+
+ iframe.onload = t.step_func_done(() => {
+ const after = frame[propertyName];
+ equalityOrInequalityAsserter(after, before);
+ });
+
+ iframe.src = "/common/blank.html";
+ }, `Navigating from the initial about:blank ${mustOrMustNotReplace} replace window.${propertyName}`);
+
+ // Per spec, discarding a browsing context should not change any of the global objects.
+ test(() => {
+ const iframe = document.createElement("iframe");
+ document.body.appendChild(iframe);
+ const frame = iframe.contentWindow;
+
+ const before = frame[propertyName];
+ assert_implements(before, `window.${propertyName} must be implemented`);
+
+ iframe.remove();
+
+ const after = frame[propertyName];
+ assert_equals(after, before, `window.${propertyName} should not change after iframe.remove()`);
+ }, `Discarding the browsing context must not change window.${propertyName}`);
+
+ // Per spec, document.open() should not change any of the global objects. In historical versions
+ // of the spec, it did, so we test here.
+ async_test(t => {
+ const iframe = document.createElement("iframe");
+
+ iframe.onload = t.step_func_done(() => {
+ const frame = iframe.contentWindow;
+ const before = frame[propertyName];
+ assert_implements(before, `window.${propertyName} must be implemented`);
+
+ frame.document.open();
+
+ const after = frame[propertyName];
+ assert_equals(after, before);
+
+ frame.document.close();
+ });
+
+ iframe.src = "/common/blank.html";
+ document.body.appendChild(iframe);
+ }, `document.open() must not replace window.${propertyName}`);
+}
diff --git a/test/wpt/tests/common/object-association.js.headers b/test/wpt/tests/common/object-association.js.headers
new file mode 100644
index 0000000..6805c32
--- /dev/null
+++ b/test/wpt/tests/common/object-association.js.headers
@@ -0,0 +1 @@
+Content-Type: text/javascript; charset=utf-8
diff --git a/test/wpt/tests/common/performance-timeline-utils.js b/test/wpt/tests/common/performance-timeline-utils.js
new file mode 100644
index 0000000..b20241c
--- /dev/null
+++ b/test/wpt/tests/common/performance-timeline-utils.js
@@ -0,0 +1,56 @@
+/*
+author: W3C http://www.w3.org/
+help: http://www.w3.org/TR/navigation-timing/#sec-window.performance-attribute
+*/
+var performanceNamespace = window.performance;
+var namespace_check = false;
+function wp_test(func, msg, properties)
+{
+ // only run the namespace check once
+ if (!namespace_check)
+ {
+ namespace_check = true;
+
+ if (performanceNamespace === undefined || performanceNamespace == null)
+ {
+ // show a single error that window.performance is undefined
+ // The window.performance attribute provides a hosting area for performance related attributes.
+ test(function() { assert_true(performanceNamespace !== undefined && performanceNamespace != null, "window.performance is defined and not null"); }, "window.performance is defined and not null.");
+ }
+ }
+
+ test(func, msg, properties);
+}
+
+function test_true(value, msg, properties)
+{
+ wp_test(function () { assert_true(value, msg); }, msg, properties);
+}
+
+function test_equals(value, equals, msg, properties)
+{
+ wp_test(function () { assert_equals(value, equals, msg); }, msg, properties);
+}
+
+// assert for every entry in `expectedEntries`, there is a matching entry _somewhere_ in `actualEntries`
+function test_entries(actualEntries, expectedEntries) {
+ test_equals(actualEntries.length, expectedEntries.length)
+ expectedEntries.forEach(function (expectedEntry) {
+ var foundEntry = actualEntries.find(function (actualEntry) {
+ return typeof Object.keys(expectedEntry).find(function (key) {
+ return actualEntry[key] !== expectedEntry[key]
+ }) === 'undefined'
+ })
+ test_true(!!foundEntry, `Entry ${JSON.stringify(expectedEntry)} could not be found.`)
+ if (foundEntry) {
+ assert_object_equals(foundEntry.toJSON(), expectedEntry)
+ }
+ })
+}
+
+function delayedLoadListener(callback) {
+ window.addEventListener('load', function() {
+ // TODO(cvazac) Remove this setTimeout when spec enforces sync entries.
+ step_timeout(callback, 0)
+ })
+}
diff --git a/test/wpt/tests/common/performance-timeline-utils.js.headers b/test/wpt/tests/common/performance-timeline-utils.js.headers
new file mode 100644
index 0000000..6805c32
--- /dev/null
+++ b/test/wpt/tests/common/performance-timeline-utils.js.headers
@@ -0,0 +1 @@
+Content-Type: text/javascript; charset=utf-8
diff --git a/test/wpt/tests/common/proxy-all.sub.pac b/test/wpt/tests/common/proxy-all.sub.pac
new file mode 100644
index 0000000..de601e5
--- /dev/null
+++ b/test/wpt/tests/common/proxy-all.sub.pac
@@ -0,0 +1,3 @@
+function FindProxyForURL(url, host) {
+ return "PROXY {{host}}:{{ports[http][0]}}"
+}
diff --git a/test/wpt/tests/common/redirect-opt-in.py b/test/wpt/tests/common/redirect-opt-in.py
new file mode 100644
index 0000000..b5e674a
--- /dev/null
+++ b/test/wpt/tests/common/redirect-opt-in.py
@@ -0,0 +1,20 @@
+def main(request, response):
+ """Simple handler that causes redirection.
+
+ The request should typically have two query parameters:
+ status - The status to use for the redirection. Defaults to 302.
+ location - The resource to redirect to.
+ """
+ status = 302
+ if b"status" in request.GET:
+ try:
+ status = int(request.GET.first(b"status"))
+ except ValueError:
+ pass
+
+ response.status = status
+
+ location = request.GET.first(b"location")
+
+ response.headers.set(b"Location", location)
+ response.headers.set(b"Timing-Allow-Origin", b"*")
diff --git a/test/wpt/tests/common/redirect.py b/test/wpt/tests/common/redirect.py
new file mode 100644
index 0000000..f2fd1eb
--- /dev/null
+++ b/test/wpt/tests/common/redirect.py
@@ -0,0 +1,19 @@
+def main(request, response):
+ """Simple handler that causes redirection.
+
+ The request should typically have two query parameters:
+ status - The status to use for the redirection. Defaults to 302.
+ location - The resource to redirect to.
+ """
+ status = 302
+ if b"status" in request.GET:
+ try:
+ status = int(request.GET.first(b"status"))
+ except ValueError:
+ pass
+
+ response.status = status
+
+ location = request.GET.first(b"location")
+
+ response.headers.set(b"Location", location)
diff --git a/test/wpt/tests/common/refresh.py b/test/wpt/tests/common/refresh.py
new file mode 100644
index 0000000..0d30990
--- /dev/null
+++ b/test/wpt/tests/common/refresh.py
@@ -0,0 +1,11 @@
+def main(request, response):
+ """
+ Respond with a blank HTML document and a `Refresh` header which describes
+ an immediate redirect to the URL specified by the requests `location` query
+ string parameter
+ """
+ headers = [
+ (b'Content-Type', b'text/html'),
+ (b'Refresh', b'0; URL=' + request.GET.first(b'location'))
+ ]
+ return (200, headers, b'')
diff --git a/test/wpt/tests/common/reftest-wait.js b/test/wpt/tests/common/reftest-wait.js
new file mode 100644
index 0000000..64fe9bf
--- /dev/null
+++ b/test/wpt/tests/common/reftest-wait.js
@@ -0,0 +1,39 @@
+/**
+ * Remove the `reftest-wait` class on the document element.
+ * The reftest runner will wait with taking a screenshot while
+ * this class is present.
+ *
+ * See https://web-platform-tests.org/writing-tests/reftests.html#controlling-when-comparison-occurs
+ */
+function takeScreenshot() {
+ document.documentElement.classList.remove("reftest-wait");
+}
+
+/**
+ * Call `takeScreenshot()` after a delay of at least |timeout| milliseconds.
+ * @param {number} timeout - milliseconds
+ */
+function takeScreenshotDelayed(timeout) {
+ setTimeout(function() {
+ takeScreenshot();
+ }, timeout);
+}
+
+/**
+ * Ensure that a precondition is met before waiting for a screenshot.
+ * @param {bool} condition - Fail the test if this evaluates to false
+ * @param {string} msg - Error message to write to the screenshot
+ */
+function failIfNot(condition, msg) {
+ const fail = () => {
+ (document.body || document.documentElement).textContent = `Precondition Failed: ${msg}`;
+ takeScreenshot();
+ };
+ if (!condition) {
+ if (document.readyState == "interactive") {
+ fail();
+ } else {
+ document.addEventListener("DOMContentLoaded", fail, false);
+ }
+ }
+}
diff --git a/test/wpt/tests/common/reftest-wait.js.headers b/test/wpt/tests/common/reftest-wait.js.headers
new file mode 100644
index 0000000..6805c32
--- /dev/null
+++ b/test/wpt/tests/common/reftest-wait.js.headers
@@ -0,0 +1 @@
+Content-Type: text/javascript; charset=utf-8
diff --git a/test/wpt/tests/common/rendering-utils.js b/test/wpt/tests/common/rendering-utils.js
new file mode 100644
index 0000000..46283bd
--- /dev/null
+++ b/test/wpt/tests/common/rendering-utils.js
@@ -0,0 +1,19 @@
+"use strict";
+
+/**
+ * Waits until we have at least one frame rendered, regardless of the engine.
+ *
+ * @returns {Promise}
+ */
+function waitForAtLeastOneFrame() {
+ return new Promise(resolve => {
+ // Different web engines work slightly different on this area but waiting
+ // for two requestAnimationFrames() to happen, one after another, should be
+ // sufficient to ensure at least one frame has been generated anywhere.
+ window.requestAnimationFrame(() => {
+ window.requestAnimationFrame(() => {
+ resolve();
+ });
+ });
+ });
+}
diff --git a/test/wpt/tests/common/sab.js b/test/wpt/tests/common/sab.js
new file mode 100644
index 0000000..a3ea610
--- /dev/null
+++ b/test/wpt/tests/common/sab.js
@@ -0,0 +1,21 @@
+const createBuffer = (() => {
+ // See https://github.com/whatwg/html/issues/5380 for why not `new SharedArrayBuffer()`
+ let sabConstructor;
+ try {
+ sabConstructor = new WebAssembly.Memory({ shared:true, initial:0, maximum:0 }).buffer.constructor;
+ } catch(e) {
+ sabConstructor = null;
+ }
+ return (type, length, opts) => {
+ if (type === "ArrayBuffer") {
+ return new ArrayBuffer(length, opts);
+ } else if (type === "SharedArrayBuffer") {
+ if (sabConstructor && sabConstructor.name !== "SharedArrayBuffer") {
+ throw new Error("WebAssembly.Memory does not support shared:true");
+ }
+ return new sabConstructor(length, opts);
+ } else {
+ throw new Error("type has to be ArrayBuffer or SharedArrayBuffer");
+ }
+ }
+})();
diff --git a/test/wpt/tests/common/security-features/README.md b/test/wpt/tests/common/security-features/README.md
new file mode 100644
index 0000000..f957541
--- /dev/null
+++ b/test/wpt/tests/common/security-features/README.md
@@ -0,0 +1,460 @@
+This directory contains the common infrastructure for the following tests (also referred below as projects).
+
+- referrer-policy/
+- mixed-content/
+- upgrade-insecure-requests/
+
+Subdirectories:
+
+- `resources`:
+ Serves JavaScript test helpers.
+- `subresource`:
+ Serves subresources, with support for redirects, stash, etc.
+ The subresource paths are managed by `subresourceMap` and
+ fetched in `requestVia*()` functions in `resources/common.js`.
+- `scope`:
+ Serves nested contexts, such as iframe documents or workers.
+ Used from `invokeFrom*()` functions in `resources/common.js`.
+- `tools`:
+ Scripts that generate test HTML files. Not used while running tests.
+- `/referrer-policy/generic/subresource-test`:
+ Sanity checking tests for subresource invocation
+ (This is still placed outside common/)
+
+# Test generator
+
+The test generator ([common/security-features/tools/generate.py](tools/generate.py)) generates test HTML files from templates and a seed (`spec.src.json`) that defines all the test scenarios.
+
+The project (i.e. a WPT subdirectory, for example `referrer-policy/`) that uses the generator should define per-project data and invoke the common generator logic in `common/security-features/tools`.
+
+This is the overview of the project structure:
+
+```
+common/security-features/
+└── tools/ - the common test generator logic
+ ├── spec.src.json
+ └── template/ - the test files templates
+project-directory/ (e.g. referrer-policy/)
+├── spec.src.json
+├── generic/
+│ ├── test-case.sub.js - Per-project test helper
+│ ├── sanity-checker.js (Used by debug target only)
+│ └── spec_json.js (Used by debug target only)
+└── gen/ - generated tests
+```
+
+## Generating the tests
+
+Note: When the repository already contains generated tests, [remove all generated tests](#removing-all-generated-tests) first.
+
+```bash
+# Install json5 module if needed.
+pip install --user json5
+
+# Generate the test files under gen/ (HTMLs and .headers files).
+path/to/common/security-features/tools/generate.py --spec path/to/project-directory/
+
+# Add all generated tests to the repo.
+git add path/to/project-directory/gen/ && git commit -m "Add generated tests"
+```
+
+This will parse the spec JSON5 files and determine which tests to generate (or skip) while using templates.
+
+- The default spec JSON5: `common/security-features/tools/spec.src.json`.
+ - Describes common configurations, such as subresource types, source context types, etc.
+- The per-project spec JSON5: `project-directory/spec.src.json`.
+ - Describes project-specific configurations, particularly those related to test generation patterns (`specification`), policy deliveries (e.g. `delivery_type`, `delivery_value`) and `expectation`.
+
+For how these two spec JSON5 files are merged, see [Sub projects](#sub-projects) section.
+
+Note: `spec.src.json` is transitioning to JSON5 [#21710](https://github.com/web-platform-tests/wpt/issues/21710).
+
+During the generation, the spec is validated by ```common/security-features/tools/spec_validator.py```. This is specially important when you're making changes to `spec.src.json`. Make sure it's a valid JSON (no comments or trailing commas). The validator reports specific errors (missing keys etc.), if any.
+
+### Removing all generated tests
+
+Simply remove all files under `project-directory/gen/`.
+
+```bash
+rm -r path/to/project-directory/gen/
+```
+
+### Options for generating tests
+
+Note: this section is currently obsolete. Only the release template is working.
+
+The generator script has two targets: ```release``` and ```debug```.
+
+* Using **release** for the target will produce tests using a template for optimizing size and performance. The release template is intended for the official web-platform-tests and possibly other test suites. No sanity checking is done in release mode. Use this option whenever you're checking into web-platform-tests.
+
+* When generating for ```debug```, the produced tests will contain more verbosity and sanity checks. Use this target to identify problems with the test suites when making changes locally. Make sure you don't check in tests generated with the debug target.
+
+Note that **release** is the default target when invoking ```generate.py```.
+
+
+## Sub projects
+
+Projects can be nested, for example to reuse a single `spec.src.json` across similar but slightly different sets of generated tests.
+The directory structure would look like:
+
+```
+project-directory/ (e.g. referrer-policy/)
+├── spec.src.json - Parent project's spec JSON
+├── generic/
+│ └── test-case.sub.js - Parent project's test helper
+├── gen/ - parent project's generated tests
+└── sub-project-directory/ (e.g. 4K)
+ ├── spec.src.json - Child project's spec JSON
+ ├── generic/
+ │ └── test-case.sub.js - Child project's test helper
+ └── gen/ - child project's generated tests
+```
+
+`generate.py --spec project-directory/sub-project-directory` generates test files under `project-directory/sub-project-directory/gen`, based on `project-directory/spec.src.json` and `project-directory/sub-project-directory/spec.src.json`.
+
+- The child project's `spec.src.json` is merged into parent project's `spec.src.json`.
+ - Two spec JSON objects are merged recursively.
+ - If a same key exists in both objects, the child's value overwrites the parent's value.
+ - If both (child's and parent's) values are arrays, then the child's value is concatenated to the parent's value.
+ - For debugging, `generate.py` dumps the merged spec JSON object as `generic/debug-output.spec.src.json`.
+- The child project's generated tests include both of the parent and child project's `test-case.sub.js`:
+ ```html
+ <script src="project-directory/test-case.sub.js"></script>
+ <script src="project-directory/sub-project-directory/test-case.sub.js"></script>
+ <script>
+ TestCase(...);
+ </script>
+ ```
+
+
+## Updating the tests
+
+The main test logic lives in ```project-directory/generic/test-case.sub.js``` with helper functions defined in ```/common/security-features/resources/common.js``` so you should probably start there.
+
+For updating the test suites you will most likely do **a subset** of the following:
+
+* Add a new subresource type:
+
+ * Add a new sub-resource python script to `/common/security-features/subresource/`.
+ * Add a sanity check test for a sub-resource to `referrer-policy/generic/subresource-test/`.
+ * Add a new entry to `subresourceMap` in `/common/security-features/resources/common.js`.
+ * Add a new entry to `valid_subresource_names` in `/common/security-features/tools/spec_validator.py`.
+ * Add a new entry to `subresource_schema` in `spec.src.json`.
+ * Update `source_context_schema` to specify in which source context the subresource can be used.
+
+* Add a new subresource redirection type
+
+ * TODO: to be documented. Example: [https://github.com/web-platform-tests/wpt/pull/18939](https://github.com/web-platform-tests/wpt/pull/18939)
+
+* Add a new subresource origin type
+
+ * TODO: to be documented. Example: [https://github.com/web-platform-tests/wpt/pull/18940](https://github.com/web-platform-tests/wpt/pull/18940)
+
+* Add a new source context (e.g. "module sharedworker global scope")
+
+ * TODO: to be documented. Example: [https://github.com/web-platform-tests/wpt/pull/18904](https://github.com/web-platform-tests/wpt/pull/18904)
+
+* Add a new source context list (e.g. "subresource request from a dedicated worker in a `<iframe srcdoc>`")
+
+ * TODO: to be documented.
+
+* Implement new or update existing assertions in ```project-directory/generic/test-case.sub.js```.
+
+* Exclude or add some tests by updating ```spec.src.json``` test expansions.
+
+* Implement a new delivery method.
+
+ * TODO: to be documented. Currently the support for delivery methods are implemented in many places across `common/security-features/`.
+
+* Regenerate the tests and MANIFEST.json
+
+## How the generator works
+
+This section describes how `spec.src.json` is turned into scenario data in test HTML files which are then processed by JavaScript test helpers and server-side scripts, and describes the objects/types used in the process.
+
+### The spec JSON
+
+`spec.src.json` is the input for the generator that defines what to generate. For examples of spec JSON files, see [referrer-policy/spec.src.json](../../referrer-policy/spec.src.json) or [mixed-content/spec.src.json](../../mixed-content/spec.src.json).
+
+#### Main sections
+
+* **`specification`**
+
+ Top level requirements with description fields and a ```test_expansion``` rule.
+ This is closely mimicking the [Referrer Policy specification](http://w3c.github.io/webappsec/specs/referrer-policy/) structure.
+
+* **`excluded_tests`**
+
+ List of ```test_expansion``` patterns expanding into selections which get skipped when generating the tests (aka. blocklisting/suppressing)
+
+* **`test_expansion_schema`**
+
+ Provides valid values for each field.
+ Each test expansion can only contain fields and values defined by this schema (or `"*"` values that indicate all the valid values defined this schema).
+
+* **`subresource_schema`**
+
+ Provides metadata of subresources, e.g. supported delivery types for each subresource.
+
+* **`source_context_schema`**
+
+ Provides metadata of each single source context, e.g. supported delivery types and subresources that can be sent from the context.
+
+* **`source_context_list_schema`**
+
+ Provides possible nested combinations of source contexts. See [SourceContexts Resolution](#sourcecontexts-resolution) section below for details.
+
+### Test Expansion Pattern Object
+
+Test expansion patterns (`test_expansion`s in `specification` section) define the combinations of test configurations (*selections*) to be generated.
+Each field in a test expansion can be in one of the following formats:
+
+* Single match: ```"value"```
+
+* Match any of: ```["value1", "value2", ...]```
+
+* Match all: ```"*"```
+
+The following fields have special meaning:
+
+- **`name`**: just ignored. (Previously this was used as a part of filenames but now this is merely a label for human and is never used by generator. This field might be removed in the future (https://github.com/web-platform-tests/wpt/issues/21708))
+- **`expansion`**: if there is more than one pattern expanding into a same selection, the pattern appearing later in the spec JSON will overwrite a previously generated selection. To make clear this is intentional, set the value of the ```expansion``` field to ```default``` for an expansion appearing earlier and ```override``` for the one appearing later.
+
+For example a test expansion pattern (taken from [referrer-policy/spec.src.json](../../referrer-policy/spec.src.json), sorted/formatted for explanation):
+
+```json
+{
+ "name": "insecure-protocol",
+ "expansion": "default",
+
+ "delivery_type": "*",
+ "delivery_value": "no-referrer-when-downgrade",
+ "source_context_list": "*",
+
+ "expectation": "stripped-referrer",
+ "origin": ["same-http", "cross-http"],
+ "redirection": "*",
+ "source_scheme": "http",
+ "subresource": "*"
+}
+```
+
+means: "All combinations with all possible `delivery_type`, `delivery_value`=`no-referrer-when-downgrade`, all possible `source_context_list`, `expectation`=`stripped-referrer`, `origin`=`same-http` or `cross-http`, all possible `redirection`, `source_scheme`=`http`, and all possible `subresource`.
+
+### Selection Object
+
+A selection is an object that defines a single test, with keys/values from `test_expansion_schema`.
+
+A single test expansion pattern gets expanded into a list of selections as follows:
+
+* Expand each field's pattern (single, any of, or all) to list of allowed values (defined by the ```test_expansion_schema```)
+
+* Permute - Recursively enumerate all selections across all fields
+
+The following field has special meaning:
+
+- **`delivery_key`**: This doesn't exist in test expansion patterns, and instead is taken from `delivery_key` field of the spec JSON and added into selections. (TODO(https://github.com/web-platform-tests/wpt/issues/21708): probably this should be added to test expansion patterns to remove this special handling)
+
+For example, the test expansion in the example above generates selections like the following selection (which eventually generates [this test file](../../referrer-policy/gen/worker-classic.http-rp/no-referrer-when-downgrade/fetch/same-http.no-redirect.http.html )):
+
+```json
+{
+ "delivery_type": "http-rp",
+ "delivery_key": "referrerPolicy",
+ "delivery_value": "no-referrer-when-downgrade",
+ "source_context_list": "worker-classic",
+
+ "expectation": "stripped-referrer",
+ "origin": "same-http",
+ "redirection": "no-redirect",
+ "source_scheme": "http",
+ "subresource": "fetch"
+}
+```
+
+### Excluding Test Expansion Patterns
+
+The ```excluded_tests``` section have objects with the same format as [Test Expansion Patterns](#test-expansion-patterns) that define selections to be excluded.
+
+Taking the spec JSON, the generator follows this algorithm:
+
+* Expand all ```excluded_tests``` to create a denylist of selections
+
+* For each `specification` entries: Expand the ```test_expansion``` pattern into selections and check each against the denylist, if not marked as suppresed, generate the test resources for the selection
+
+### SourceContext Resolution
+
+The `source_context_list_schema` section of `spec.src.json` defines templates of policy deliveries and source contexts.
+The `source_context_list` value in a selection specifies the key of the template to be used in `source_context_list_schema`, and the following fields in the selection are filled into the template (these three values define the **target policy delivery** to be tested):
+
+- `delivery_type`
+- `delivery_key`
+- `delivery_value`
+
+#### Source Context List Schema
+
+Each entry of **`source_context_list_schema`**, defines a single template of how/what policies to be delivered in what source contexts (See also [PolicyDelivery](types.md#policydelivery) and [SourceContext](types.md#sourcecontext)).
+
+- The key: the name of the template which matches with the `source_context_list` value in a selection.
+- `sourceContextList`: an array of `SourceContext` objects that represents a (possibly nested) context.
+ - `sourceContextType` of the first entry of `sourceContextList` should be always `"top"`, which represents the top-level generated test HTML. This entry is omitted in the scenario JSON object passed to JavaScript runtime, but the policy deliveries specified here are handled by the generator, e.g. written as `<meta>` elements in the generated test HTML.
+- `subresourcePolicyDeliveries`: an array of `PolicyDelivery` objects that represents policies specified at subresource requests (e.g. `referrerPolicy` attribute of `<img>` elements).
+
+#### PolicyDelivery placeholders
+
+Instead to ordinal `PolicyDelivery` objects, the following placeholder strings can be used in `sourceContextList` or `subresourcePolicyDeliveries`.
+
+- `"policy"`:
+ - Replaced with the target policy delivery.
+- `"policyIfNonNull"`:
+ - Replaced with the target policy delivery, only if `delivery_value` is not `null`.
+ If `delivery_value` is `null`, then the test is not generated.
+- `"anotherPolicy"`:
+ - Replaced with a `PolicyDelivery` object that has a different value from
+ the target policy delivery.
+ - Can be used to specify e.g. a policy that should be overridden by
+ the target policy delivery.
+
+#### `source_context_schema` and `subresource_schema`
+
+These represent supported delivery types and subresources
+for each source context or subresource type. These are used
+
+- To filter out test files for unsupported combinations of delivery types,
+ source contexts and subresources during SourceContext resolution.
+- To determine what delivery types can be used for `anotherPolicy`
+ placeholder.
+
+#### Example
+
+For example, the following entry in `source_context_list_schema`:
+
+```json
+"worker-classic": {
+ "sourceContextList": [
+ {
+ "sourceContextType": "top",
+ "policyDeliveries": [
+ "anotherPolicy"
+ ]
+ },
+ {
+ "sourceContextType": "worker-classic",
+ "policyDeliveries": [
+ "policy"
+ ]
+ }
+ ],
+ "subresourcePolicyDeliveries": []
+}
+```
+
+Defines a template to be instantiated with `delivery_key`, `delivery_type` and `delivery_value` values defined outside `source_context_list_schema`, which reads:
+
+- A classic `WorkerGlobalScope` is created under the top-level Document, and has a policy defined by `delivery_key`, `delivery_type` and `delivery_value`.
+- The top-level Document has a policy different from the policy given to the classic worker (to confirm that the policy of the classic worker, not of the top-level Document, is used).
+- The subresource request is sent from the classic `WorkerGlobalScope`, with no additional policies specified at the subresource request.
+
+And when filled with the following values from a selection:
+
+- `delivery_type`: `"http-rp"`
+- `delivery_key`: `"referrerPolicy"`
+- `delivery_value`: `"no-referrer-when-downgrade"`
+
+This becomes:
+
+```json
+"worker-classic": {
+ "sourceContextList": [
+ {
+ "sourceContextType": "top",
+ "policyDeliveries": [
+ {
+ "deliveryType": "meta",
+ "key": "referrerPolicy",
+ "value": "no-referrer"
+ }
+ ]
+ },
+ {
+ "sourceContextType": "worker-classic",
+ "policyDeliveries": [
+ {
+ "deliveryType": "http-rp",
+ "key": "referrerPolicy",
+ "value": "no-referrer-when-downgrade"
+ }
+ ]
+ }
+ ],
+ "subresourcePolicyDeliveries": []
+}
+```
+
+which means
+
+- The top-level Document has `<meta name="referrer" content="no-referrer">`.
+- The classic worker is created with
+ `Referrer-Policy: no-referrer-when-downgrade` HTTP response headers.
+
+### Scenario Object
+
+The **scenario** object is the JSON object written to the generated HTML files, and passed to JavaScript test runtime (as an argument of `TestCase`).
+A scenario object is an selection object, minus the keys used in [SourceContext Resolution](#sourceContext-resolution):
+
+- `source_context_list`
+- `delivery_type`
+- `delivery_key`
+- `delivery_value`
+
+plus the keys instantiated by [SourceContext Resolution](#sourceContext-resolution):
+
+- `source_context_list`, except for the first `"top"` entry.
+- `subresource_policy_deliveries`
+
+For example:
+
+```json
+{
+ "source_context_list": [
+ {
+ "sourceContextType": "worker-classic",
+ "policyDeliveries": [
+ {
+ "deliveryType": "http-rp",
+ "key": "referrerPolicy",
+ "value": "no-referrer-when-downgrade"
+ }
+ ]
+ }
+ ],
+ "subresource_policy_deliveries": [],
+
+ "expectation": "stripped-referrer",
+ "origin": "same-http",
+ "redirection": "no-redirect",
+ "source_scheme": "http",
+ "subresource": "fetch"
+}
+```
+
+### TopLevelPolicyDelivery Object
+
+The ***TopLevelPolicyDelivery** object is the first `"top"` entry of `SourceContextList` instantiated by [SourceContext Resolution](#sourceContext-resolution), which represents the policy delivery of the top-level HTML Document.
+
+The generator generates `<meta>` elements and `.headers` files of the top-level HTML files from the TopLevelPolicyDelivery object.
+
+This is handled separately by the generator from other parts of selection objects and scenario objects, because the `<meta>` and `.headers` are hard-coded directly to the files in the WPT repository, while policies of subcontexts are generated via server-side `common/security-features/scope` scripts.
+
+TODO(https://github.com/web-platform-tests/wpt/issues/21710): Currently the name `TopLevelPolicyDelivery` doesn't appear in the code.
+
+## How the test runtime works
+
+All the information needed at runtime is contained in an scenario object. See the code/comments of the following files.
+
+- `project-directory/generic/test-case.js` defines `TestCase`, the entry point that receives a scenario object. `resources/common.sub.js` does the most of common JavaScript work.
+ - Subresource URLs (which point to `subresource/` scripts) are calculated from `origin` and `redirection` values.
+ - Initiating fetch requests based on `subresource` and `subresource_policy_deliveries`.
+- `scope/` server-side scripts serve non-toplevel contexts, while the top-level Document is generated by the generator.
+ TODO(https://github.com/web-platform-tests/wpt/issues/21709): Merge the logics of `scope/` and the generator.
+- `subresource/` server-side scripts serve subresource responses.
diff --git a/test/wpt/tests/common/security-features/__init__.py b/test/wpt/tests/common/security-features/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/wpt/tests/common/security-features/__init__.py
diff --git a/test/wpt/tests/common/security-features/resources/common.sub.js b/test/wpt/tests/common/security-features/resources/common.sub.js
new file mode 100644
index 0000000..96ca280
--- /dev/null
+++ b/test/wpt/tests/common/security-features/resources/common.sub.js
@@ -0,0 +1,1311 @@
+/**
+ * @fileoverview Utilities for mixed-content in web-platform-tests.
+ * @author burnik@google.com (Kristijan Burnik)
+ * Disclaimer: Some methods of other authors are annotated in the corresponding
+ * method's JSDoc.
+ */
+
+// ===============================================================
+// Types
+// ===============================================================
+// Objects of the following types are used to represent what kind of
+// subresource requests should be sent with what kind of policies,
+// from what kind of possibly nested source contexts.
+// The objects are represented as JSON objects (not JavaScript/Python classes
+// in a strict sense) to be passed between JavaScript/Python code.
+//
+// See also common/security-features/Types.md for high-level description.
+
+/**
+ @typedef PolicyDelivery
+ @type {object}
+ Referrer policy etc. can be applied/delivered in several ways.
+ A PolicyDelivery object specifies what policy is delivered and how.
+
+ @property {string} deliveryType
+ Specifies how the policy is delivered.
+ The valid deliveryType are:
+
+ "attr"
+ [A] DOM attributes e.g. referrerPolicy.
+
+ "rel-noref"
+ [A] <link rel="noreferrer"> (referrer-policy only).
+
+ "http-rp"
+ [B] HTTP response headers.
+
+ "meta"
+ [B] <meta> elements.
+
+ @property {string} key
+ @property {string} value
+ Specifies what policy to be delivered. The valid keys are:
+
+ "referrerPolicy"
+ Referrer Policy
+ https://w3c.github.io/webappsec-referrer-policy/
+ Valid values are those listed in
+ https://w3c.github.io/webappsec-referrer-policy/#referrer-policy
+ (except that "" is represented as null/None)
+
+ A PolicyDelivery can be specified in several ways:
+
+ - (for [A]) Associated with an individual subresource request and
+ specified in `Subresource.policies`,
+ e.g. referrerPolicy attributes of DOM elements.
+ This is handled in invokeRequest().
+
+ - (for [B]) Associated with an nested environmental settings object and
+ specified in `SourceContext.policies`,
+ e.g. HTTP referrer-policy response headers of HTML/worker scripts.
+ This is handled in server-side under /common/security-features/scope/.
+
+ - (for [B]) Associated with the top-level HTML document.
+ This is handled by the generators.d
+*/
+
+/**
+ @typedef Subresource
+ @type {object}
+ A Subresource represents how a subresource request is sent.
+
+ @property{SubresourceType} subresourceType
+ How the subresource request is sent,
+ e.g. "img-tag" for sending a request via <img src>.
+ See the keys of `subresourceMap` for valid values.
+
+ @property{string} url
+ subresource's URL.
+ Typically this is constructed by getRequestURLs() below.
+
+ @property{PolicyDelivery} policyDeliveries
+ Policies delivered specific to the subresource request.
+*/
+
+/**
+ @typedef SourceContext
+ @type {object}
+
+ @property {string} sourceContextType
+ Kind of the source context to be used.
+ Valid values are the keys of `sourceContextMap` below.
+
+ @property {Array<PolicyDelivery>} policyDeliveries
+ A list of PolicyDelivery applied to the source context.
+*/
+
+// ===============================================================
+// General utility functions
+// ===============================================================
+
+function timeoutPromise(t, ms) {
+ return new Promise(resolve => { t.step_timeout(resolve, ms); });
+}
+
+/**
+ * Normalizes the target port for use in a URL. For default ports, this is the
+ * empty string (omitted port), otherwise it's a colon followed by the port
+ * number. Ports 80, 443 and an empty string are regarded as default ports.
+ * @param {number} targetPort The port to use
+ * @return {string} The port portion for using as part of a URL.
+ */
+function getNormalizedPort(targetPort) {
+ return ([80, 443, ""].indexOf(targetPort) >= 0) ? "" : ":" + targetPort;
+}
+
+/**
+ * Creates a GUID.
+ * See: https://en.wikipedia.org/wiki/Globally_unique_identifier
+ * Original author: broofa (http://www.broofa.com/)
+ * Sourced from: http://stackoverflow.com/a/2117523/4949715
+ * @return {string} A pseudo-random GUID.
+ */
+function guid() {
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
+ var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
+ return v.toString(16);
+ });
+}
+
+/**
+ * Initiates a new XHR via GET.
+ * @param {string} url The endpoint URL for the XHR.
+ * @param {string} responseType Optional - how should the response be parsed.
+ * Default is "json".
+ * See: https://xhr.spec.whatwg.org/#dom-xmlhttprequest-responsetype
+ * @return {Promise} A promise wrapping the success and error events.
+ */
+function xhrRequest(url, responseType) {
+ return new Promise(function(resolve, reject) {
+ var xhr = new XMLHttpRequest();
+ xhr.open('GET', url, true);
+ xhr.responseType = responseType || "json";
+
+ xhr.addEventListener("error", function() {
+ reject(Error("Network Error"));
+ });
+
+ xhr.addEventListener("load", function() {
+ if (xhr.status != 200)
+ reject(Error(xhr.statusText));
+ else
+ resolve(xhr.response);
+ });
+
+ xhr.send();
+ });
+}
+
+/**
+ * Sets attributes on a given DOM element.
+ * @param {DOMElement} element The element on which to set the attributes.
+ * @param {object} An object with keys (serving as attribute names) and values.
+ */
+function setAttributes(el, attrs) {
+ attrs = attrs || {}
+ for (var attr in attrs) {
+ if (attr !== 'src')
+ el.setAttribute(attr, attrs[attr]);
+ }
+ // Workaround for Chromium: set <img>'s src attribute after all other
+ // attributes to ensure the policy is applied.
+ for (var attr in attrs) {
+ if (attr === 'src')
+ el.setAttribute(attr, attrs[attr]);
+ }
+}
+
+/**
+ * Binds to success and error events of an object wrapping them into a promise
+ * available through {@code element.eventPromise}. The success event
+ * resolves and error event rejects.
+ * This method adds event listeners, and then removes all the added listeners
+ * when one of listened event is fired.
+ * @param {object} element An object supporting events on which to bind the
+ * promise.
+ * @param {string} resolveEventName [="load"] The event name to bind resolve to.
+ * @param {string} rejectEventName [="error"] The event name to bind reject to.
+ */
+function bindEvents(element, resolveEventName, rejectEventName) {
+ element.eventPromise =
+ bindEvents2(element, resolveEventName, element, rejectEventName);
+}
+
+// Returns a promise wrapping success and error events of objects.
+// This is a variant of bindEvents that can accept separate objects for each
+// events and two events to reject, and doesn't set `eventPromise`.
+//
+// When `resolveObject`'s `resolveEventName` event (default: "load") is
+// fired, the promise is resolved with the event.
+//
+// When `rejectObject`'s `rejectEventName` event (default: "error") or
+// `rejectObject2`'s `rejectEventName2` event (default: "error") is
+// fired, the promise is rejected.
+//
+// `rejectObject2` is optional.
+function bindEvents2(resolveObject, resolveEventName, rejectObject, rejectEventName, rejectObject2, rejectEventName2) {
+ return new Promise(function(resolve, reject) {
+ const actualResolveEventName = resolveEventName || "load";
+ const actualRejectEventName = rejectEventName || "error";
+ const actualRejectEventName2 = rejectEventName2 || "error";
+
+ const resolveHandler = function(event) {
+ cleanup();
+ resolve(event);
+ };
+
+ const rejectHandler = function(event) {
+ // Chromium starts propagating errors from worker.onerror to
+ // window.onerror. This handles the uncaught exceptions in tests.
+ event.preventDefault();
+ cleanup();
+ reject(event);
+ };
+
+ const cleanup = function() {
+ resolveObject.removeEventListener(actualResolveEventName, resolveHandler);
+ rejectObject.removeEventListener(actualRejectEventName, rejectHandler);
+ if (rejectObject2) {
+ rejectObject2.removeEventListener(actualRejectEventName2, rejectHandler);
+ }
+ };
+
+ resolveObject.addEventListener(actualResolveEventName, resolveHandler);
+ rejectObject.addEventListener(actualRejectEventName, rejectHandler);
+ if (rejectObject2) {
+ rejectObject2.addEventListener(actualRejectEventName2, rejectHandler);
+ }
+ });
+}
+
+/**
+ * Creates a new DOM element.
+ * @param {string} tagName The type of the DOM element.
+ * @param {object} attrs A JSON with attributes to apply to the element.
+ * @param {DOMElement} parent Optional - an existing DOM element to append to
+ * If not provided, the returned element will remain orphaned.
+ * @param {boolean} doBindEvents Optional - Whether to bind to load and error
+ * events and provide the promise wrapping the events via the element's
+ * {@code eventPromise} property. Default value evaluates to false.
+ * @return {DOMElement} The newly created DOM element.
+ */
+function createElement(tagName, attrs, parentNode, doBindEvents) {
+ var element = document.createElement(tagName);
+
+ if (doBindEvents) {
+ bindEvents(element);
+ if (element.tagName == "IFRAME" && !('srcdoc' in attrs || 'src' in attrs)) {
+ // If we're loading a frame, ensure we spin the event loop after load to
+ // paper over the different event timing in Gecko vs Blink/WebKit
+ // see https://github.com/whatwg/html/issues/4965
+ element.eventPromise = element.eventPromise.then(() => {
+ return new Promise(resolve => setTimeout(resolve, 0))
+ });
+ }
+ }
+ // We set the attributes after binding to events to catch any
+ // event-triggering attribute changes. E.g. form submission.
+ //
+ // But be careful with images: unlike other elements they will start the load
+ // as soon as the attr is set, even if not in the document yet, and sometimes
+ // complete it synchronously, so the append doesn't have the effect we want.
+ // So for images, we want to set the attrs after appending, whereas for other
+ // elements we want to do it before appending.
+ var isImg = (tagName == "img");
+ if (!isImg)
+ setAttributes(element, attrs);
+
+ if (parentNode)
+ parentNode.appendChild(element);
+
+ if (isImg)
+ setAttributes(element, attrs);
+
+ return element;
+}
+
+function createRequestViaElement(tagName, attrs, parentNode) {
+ return createElement(tagName, attrs, parentNode, true).eventPromise;
+}
+
+function wrapResult(server_data) {
+ if (typeof(server_data) === "string") {
+ throw server_data;
+ }
+ return {
+ referrer: server_data.headers.referer,
+ headers: server_data.headers
+ }
+}
+
+// ===============================================================
+// Subresources
+// ===============================================================
+
+/**
+ @typedef RequestResult
+ @type {object}
+ Represents the result of sending an request.
+ All properties are optional. See the comments for
+ requestVia*() and invokeRequest() below to see which properties are set.
+
+ @property {Array<Object<string, string>>} headers
+ HTTP request headers sent to server.
+ @property {string} referrer - Referrer.
+ @property {string} location - The URL of the subresource.
+ @property {string} sourceContextUrl
+ the URL of the global object where the actual request is sent.
+*/
+
+/**
+ requestVia*(url, additionalAttributes) functions send a subresource
+ request from the current environment settings object.
+
+ @param {string} url
+ The URL of the subresource.
+ @param {Object<string, string>} additionalAttributes
+ Additional attributes set to DOM elements
+ (element-initiated requests only).
+
+ @returns {Promise} that are resolved with a RequestResult object
+ on successful requests.
+
+ - Category 1:
+ `headers`: set.
+ `referrer`: set via `document.referrer`.
+ `location`: set via `document.location`.
+ See `template/document.html.template`.
+ - Category 2:
+ `headers`: set.
+ `referrer`: set to `headers.referer` by `wrapResult()`.
+ `location`: not set.
+ - Category 3:
+ All the keys listed above are NOT set.
+ `sourceContextUrl` is not set here.
+
+ -------------------------------- -------- --------------------------
+ Function name Category Used in
+ -------- ------- ---------
+ referrer mixed- upgrade-
+ policy content insecure-
+ policy content request
+ -------------------------------- -------- -------- ------- ---------
+ requestViaAnchor 1 Y Y -
+ requestViaArea 1 Y Y -
+ requestViaAudio 3 - Y -
+ requestViaDedicatedWorker 2 Y Y Y
+ requestViaFetch 2 Y Y -
+ requestViaForm 2 - Y -
+ requestViaIframe 1 Y Y -
+ requestViaImage 2 Y Y -
+ requestViaLinkPrefetch 3 - Y -
+ requestViaLinkStylesheet 3 - Y -
+ requestViaObject 3 - Y -
+ requestViaPicture 3 - Y -
+ requestViaScript 2 Y Y -
+ requestViaSendBeacon 3 - Y -
+ requestViaSharedWorker 2 Y Y Y
+ requestViaVideo 3 - Y -
+ requestViaWebSocket 3 - Y -
+ requestViaWorklet 3 - Y Y
+ requestViaXhr 2 Y Y -
+ -------------------------------- -------- -------- ------- ---------
+*/
+
+/**
+ * Creates a new iframe, binds load and error events, sets the src attribute and
+ * appends it to {@code document.body} .
+ * @param {string} url The src for the iframe.
+ * @return {Promise} The promise for success/error events.
+ */
+function requestViaIframe(url, additionalAttributes) {
+ const iframe = createElement(
+ "iframe",
+ Object.assign({"src": url}, additionalAttributes),
+ document.body,
+ false);
+ return bindEvents2(window, "message", iframe, "error", window, "error")
+ .then(event => {
+ if (event.source !== iframe.contentWindow)
+ return Promise.reject(new Error('Unexpected event.source'));
+ return event.data;
+ });
+}
+
+/**
+ * Creates a new image, binds load and error events, sets the src attribute and
+ * appends it to {@code document.body} .
+ * @param {string} url The src for the image.
+ * @return {Promise} The promise for success/error events.
+ */
+function requestViaImage(url, additionalAttributes) {
+ const img = createElement(
+ "img",
+ // crossOrigin attribute is added to read the pixel data of the response.
+ Object.assign({"src": url, "crossOrigin": "Anonymous"}, additionalAttributes),
+ document.body, true);
+ return img.eventPromise.then(() => wrapResult(decodeImageData(img)));
+}
+
+// Helper for requestViaImage().
+function decodeImageData(img) {
+ var canvas = document.createElement("canvas");
+ var context = canvas.getContext('2d');
+ context.drawImage(img, 0, 0);
+ var imgData = context.getImageData(0, 0, img.clientWidth, img.clientHeight);
+ const rgba = imgData.data;
+
+ let decodedBytes = new Uint8ClampedArray(rgba.length);
+ let decodedLength = 0;
+
+ for (var i = 0; i + 12 <= rgba.length; i += 12) {
+ // A single byte is encoded in three pixels. 8 pixel octets (among
+ // 9 octets = 3 pixels * 3 channels) are used to encode 8 bits,
+ // the most significant bit first, where `0` and `255` in pixel values
+ // represent `0` and `1` in bits, respectively.
+ // This encoding is used to avoid errors due to different color spaces.
+ const bits = [];
+ for (let j = 0; j < 3; ++j) {
+ bits.push(rgba[i + j * 4 + 0]);
+ bits.push(rgba[i + j * 4 + 1]);
+ bits.push(rgba[i + j * 4 + 2]);
+ // rgba[i + j * 4 + 3]: Skip alpha channel.
+ }
+ // The last one element is not used.
+ bits.pop();
+
+ // Decode a single byte.
+ let byte = 0;
+ for (let j = 0; j < 8; ++j) {
+ byte <<= 1;
+ if (bits[j] >= 128)
+ byte |= 1;
+ }
+
+ // Zero is the string terminator.
+ if (byte == 0)
+ break;
+
+ decodedBytes[decodedLength++] = byte;
+ }
+
+ // Remove trailing nulls from data.
+ decodedBytes = decodedBytes.subarray(0, decodedLength);
+ var string_data = (new TextDecoder("ascii")).decode(decodedBytes);
+
+ return JSON.parse(string_data);
+}
+
+/**
+ * Initiates a new XHR GET request to provided URL.
+ * @param {string} url The endpoint URL for the XHR.
+ * @return {Promise} The promise for success/error events.
+ */
+function requestViaXhr(url) {
+ return xhrRequest(url).then(result => wrapResult(result));
+}
+
+/**
+ * Initiates a new GET request to provided URL via the Fetch API.
+ * @param {string} url The endpoint URL for the Fetch.
+ * @return {Promise} The promise for success/error events.
+ */
+function requestViaFetch(url) {
+ return fetch(url)
+ .then(res => res.json())
+ .then(j => wrapResult(j));
+}
+
+function dedicatedWorkerUrlThatFetches(url) {
+ return `data:text/javascript,
+ fetch('${url}')
+ .then(r => r.json())
+ .then(j => postMessage(j))
+ .catch((e) => postMessage(e.message));`;
+}
+
+function workerUrlThatImports(url, additionalAttributes) {
+ let csp = "";
+ if (additionalAttributes && additionalAttributes.contentSecurityPolicy) {
+ csp=`&contentSecurityPolicy=${additionalAttributes.contentSecurityPolicy}`;
+ }
+ return `/common/security-features/subresource/static-import.py` +
+ `?import_url=${encodeURIComponent(url)}${csp}`;
+}
+
+function workerDataUrlThatImports(url) {
+ return `data:text/javascript,import '${url}';`;
+}
+
+/**
+ * Creates a new Worker, binds message and error events wrapping them into.
+ * {@code worker.eventPromise} and posts an empty string message to start
+ * the worker.
+ * @param {string} url The endpoint URL for the worker script.
+ * @param {object} options The options for Worker constructor.
+ * @return {Promise} The promise for success/error events.
+ */
+function requestViaDedicatedWorker(url, options) {
+ var worker;
+ try {
+ worker = new Worker(url, options);
+ } catch (e) {
+ return Promise.reject(e);
+ }
+ worker.postMessage('');
+ return bindEvents2(worker, "message", worker, "error")
+ .then(event => wrapResult(event.data));
+}
+
+function requestViaSharedWorker(url, options) {
+ var worker;
+ try {
+ worker = new SharedWorker(url, options);
+ } catch(e) {
+ return Promise.reject(e);
+ }
+ const promise = bindEvents2(worker.port, "message", worker, "error")
+ .then(event => wrapResult(event.data));
+ worker.port.start();
+ return promise;
+}
+
+// Returns a reference to a worklet object corresponding to a given type.
+function get_worklet(type) {
+ if (type == 'animation')
+ return CSS.animationWorklet;
+ if (type == 'layout')
+ return CSS.layoutWorklet;
+ if (type == 'paint')
+ return CSS.paintWorklet;
+ if (type == 'audio')
+ return new OfflineAudioContext(2,44100*40,44100).audioWorklet;
+
+ throw new Error('unknown worklet type is passed.');
+}
+
+function requestViaWorklet(type, url) {
+ try {
+ return get_worklet(type).addModule(url);
+ } catch (e) {
+ return Promise.reject(e);
+ }
+}
+
+/**
+ * Creates a navigable element with the name `navigableElementName`
+ * (<a>, <area>, or <form>) under `parentNode`, and
+ * performs a navigation by `trigger()` (e.g. clicking <a>).
+ * To avoid navigating away from the current execution context,
+ * a target attribute is set to point to a new helper iframe.
+ * @param {string} navigableElementName
+ * @param {object} additionalAttributes The attributes of the navigable element.
+ * @param {DOMElement} parentNode
+ * @param {function(DOMElement} trigger A callback called after the navigable
+ * element is inserted and should trigger navigation using the element.
+ * @return {Promise} The promise for success/error events.
+ */
+function requestViaNavigable(navigableElementName, additionalAttributes,
+ parentNode, trigger) {
+ const name = guid();
+
+ const iframe =
+ createElement("iframe", {"name": name, "id": name}, parentNode, false);
+
+ const navigable = createElement(
+ navigableElementName,
+ Object.assign({"target": name}, additionalAttributes),
+ parentNode, false);
+
+ const promise =
+ bindEvents2(window, "message", iframe, "error", window, "error")
+ .then(event => {
+ if (event.source !== iframe.contentWindow)
+ return Promise.reject(new Error('Unexpected event.source'));
+ return event.data;
+ });
+ trigger(navigable);
+ return promise;
+}
+
+/**
+ * Creates a new anchor element, appends it to {@code document.body} and
+ * performs the navigation.
+ * @param {string} url The URL to navigate to.
+ * @return {Promise} The promise for success/error events.
+ */
+function requestViaAnchor(url, additionalAttributes) {
+ return requestViaNavigable(
+ "a",
+ Object.assign({"href": url, "innerHTML": "Link to resource"},
+ additionalAttributes),
+ document.body, a => a.click());
+}
+
+/**
+ * Creates a new area element, appends it to {@code document.body} and performs
+ * the navigation.
+ * @param {string} url The URL to navigate to.
+ * @return {Promise} The promise for success/error events.
+ */
+function requestViaArea(url, additionalAttributes) {
+ // TODO(kristijanburnik): Append to map and add image.
+ return requestViaNavigable(
+ "area",
+ Object.assign({"href": url}, additionalAttributes),
+ document.body, area => area.click());
+}
+
+/**
+ * Creates a new script element, sets the src to url, and appends it to
+ * {@code document.body}.
+ * @param {string} url The src URL.
+ * @return {Promise} The promise for success/error events.
+ */
+function requestViaScript(url, additionalAttributes) {
+ const script = createElement(
+ "script",
+ Object.assign({"src": url}, additionalAttributes),
+ document.body,
+ false);
+
+ return bindEvents2(window, "message", script, "error", window, "error")
+ .then(event => wrapResult(event.data));
+}
+
+/**
+ * Creates a new script element that performs a dynamic import to `url`, and
+ * appends the script element to {@code document.body}.
+ * @param {string} url The src URL.
+ * @return {Promise} The promise for success/error events.
+ */
+function requestViaDynamicImport(url, additionalAttributes) {
+ const scriptUrl = `data:text/javascript,import("${url}");`;
+ const script = createElement(
+ "script",
+ Object.assign({"src": scriptUrl}, additionalAttributes),
+ document.body,
+ false);
+
+ return bindEvents2(window, "message", script, "error", window, "error")
+ .then(event => wrapResult(event.data));
+}
+
+/**
+ * Creates a new form element, sets attributes, appends it to
+ * {@code document.body} and submits the form.
+ * @param {string} url The URL to submit to.
+ * @return {Promise} The promise for success/error events.
+ */
+function requestViaForm(url, additionalAttributes) {
+ return requestViaNavigable(
+ "form",
+ Object.assign({"action": url, "method": "POST"}, additionalAttributes),
+ document.body, form => form.submit());
+}
+
+/**
+ * Creates a new link element for a stylesheet, binds load and error events,
+ * sets the href to url and appends it to {@code document.head}.
+ * @param {string} url The URL for a stylesheet.
+ * @return {Promise} The promise for success/error events.
+ */
+function requestViaLinkStylesheet(url) {
+ return createRequestViaElement("link",
+ {"rel": "stylesheet", "href": url},
+ document.head);
+}
+
+/**
+ * Creates a new link element for a prefetch, binds load and error events, sets
+ * the href to url and appends it to {@code document.head}.
+ * @param {string} url The URL of a resource to prefetch.
+ * @return {Promise} The promise for success/error events.
+ */
+function requestViaLinkPrefetch(url) {
+ var link = document.createElement('link');
+ if (link.relList && link.relList.supports && link.relList.supports("prefetch")) {
+ return createRequestViaElement("link",
+ {"rel": "prefetch", "href": url},
+ document.head);
+ } else {
+ return Promise.reject("This browser does not support 'prefetch'.");
+ }
+}
+
+/**
+ * Initiates a new beacon request.
+ * @param {string} url The URL of a resource to prefetch.
+ * @return {Promise} The promise for success/error events.
+ */
+async function requestViaSendBeacon(url) {
+ function wait(ms) {
+ return new Promise(resolve => step_timeout(resolve, ms));
+ }
+ if (!navigator.sendBeacon(url)) {
+ // If mixed-content check fails, it should return false.
+ throw new Error('sendBeacon() fails.');
+ }
+ // We don't have a means to see the result of sendBeacon() request
+ // for sure. Let's wait for a while and let the generic test function
+ // ask the server for the result.
+ await wait(500);
+ return 'allowed';
+}
+
+/**
+ * Creates a new media element with a child source element, binds loadeddata and
+ * error events, sets attributes and appends to document.body.
+ * @param {string} type The type of the media element (audio/video/picture).
+ * @param {object} media_attrs The attributes for the media element.
+ * @param {object} source_attrs The attributes for the child source element.
+ * @return {DOMElement} The newly created media element.
+ */
+function createMediaElement(type, media_attrs, source_attrs) {
+ var mediaElement = createElement(type, {});
+
+ var sourceElement = createElement("source", {});
+
+ mediaElement.eventPromise = new Promise(function(resolve, reject) {
+ mediaElement.addEventListener("loadeddata", function (e) {
+ resolve(e);
+ });
+
+ // Safari doesn't fire an `error` event when blocking mixed content.
+ mediaElement.addEventListener("stalled", function(e) {
+ reject(e);
+ });
+
+ sourceElement.addEventListener("error", function(e) {
+ reject(e);
+ });
+ });
+
+ setAttributes(mediaElement, media_attrs);
+ setAttributes(sourceElement, source_attrs);
+
+ mediaElement.appendChild(sourceElement);
+ document.body.appendChild(mediaElement);
+
+ return mediaElement;
+}
+
+/**
+ * Creates a new video element, binds loadeddata and error events, sets
+ * attributes and source URL and appends to {@code document.body}.
+ * @param {string} url The URL of the video.
+ * @return {Promise} The promise for success/error events.
+ */
+function requestViaVideo(url) {
+ return createMediaElement("video",
+ {},
+ {"src": url}).eventPromise;
+}
+
+/**
+ * Creates a new audio element, binds loadeddata and error events, sets
+ * attributes and source URL and appends to {@code document.body}.
+ * @param {string} url The URL of the audio.
+ * @return {Promise} The promise for success/error events.
+ */
+function requestViaAudio(url) {
+ return createMediaElement("audio",
+ {},
+ {"type": "audio/wav", "src": url}).eventPromise;
+}
+
+/**
+ * Creates a new picture element, binds loadeddata and error events, sets
+ * attributes and source URL and appends to {@code document.body}. Also
+ * creates new image element appending it to the picture
+ * @param {string} url The URL of the image for the source and image elements.
+ * @return {Promise} The promise for success/error events.
+ */
+function requestViaPicture(url) {
+ var picture = createMediaElement("picture", {}, {"srcset": url,
+ "type": "image/png"});
+ return createRequestViaElement("img", {"src": url}, picture);
+}
+
+/**
+ * Creates a new object element, binds load and error events, sets the data to
+ * url, and appends it to {@code document.body}.
+ * @param {string} url The data URL.
+ * @return {Promise} The promise for success/error events.
+ */
+function requestViaObject(url) {
+ return createRequestViaElement("object", {"data": url, "type": "text/html"}, document.body);
+}
+
+/**
+ * Creates a new WebSocket pointing to {@code url} and sends a message string
+ * "echo". The {@code message} and {@code error} events are triggering the
+ * returned promise resolve/reject events.
+ * @param {string} url The URL for WebSocket to connect to.
+ * @return {Promise} The promise for success/error events.
+ */
+function requestViaWebSocket(url) {
+ return new Promise(function(resolve, reject) {
+ var websocket = new WebSocket(url);
+
+ websocket.addEventListener("message", function(e) {
+ resolve(e.data);
+ });
+
+ websocket.addEventListener("open", function(e) {
+ websocket.send("echo");
+ });
+
+ websocket.addEventListener("error", function(e) {
+ reject(e)
+ });
+ })
+ .then(data => {
+ return JSON.parse(data);
+ });
+}
+
+/**
+ @typedef SubresourceType
+ @type {string}
+
+ Represents how a subresource is sent.
+ The keys of `subresourceMap` below are the valid values.
+*/
+
+// Subresource paths and invokers.
+const subresourceMap = {
+ "a-tag": {
+ path: "/common/security-features/subresource/document.py",
+ invoker: requestViaAnchor,
+ },
+ "area-tag": {
+ path: "/common/security-features/subresource/document.py",
+ invoker: requestViaArea,
+ },
+ "audio-tag": {
+ path: "/common/security-features/subresource/audio.py",
+ invoker: requestViaAudio,
+ },
+ "beacon": {
+ path: "/common/security-features/subresource/empty.py",
+ invoker: requestViaSendBeacon,
+ },
+ "fetch": {
+ path: "/common/security-features/subresource/xhr.py",
+ invoker: requestViaFetch,
+ },
+ "form-tag": {
+ path: "/common/security-features/subresource/document.py",
+ invoker: requestViaForm,
+ },
+ "iframe-tag": {
+ path: "/common/security-features/subresource/document.py",
+ invoker: requestViaIframe,
+ },
+ "img-tag": {
+ path: "/common/security-features/subresource/image.py",
+ invoker: requestViaImage,
+ },
+ "link-css-tag": {
+ path: "/common/security-features/subresource/empty.py",
+ invoker: requestViaLinkStylesheet,
+ },
+ "link-prefetch-tag": {
+ path: "/common/security-features/subresource/empty.py",
+ invoker: requestViaLinkPrefetch,
+ },
+ "object-tag": {
+ path: "/common/security-features/subresource/empty.py",
+ invoker: requestViaObject,
+ },
+ "picture-tag": {
+ path: "/common/security-features/subresource/image.py",
+ invoker: requestViaPicture,
+ },
+ "script-tag": {
+ path: "/common/security-features/subresource/script.py",
+ invoker: requestViaScript,
+ },
+ "script-tag-dynamic-import": {
+ path: "/common/security-features/subresource/script.py",
+ invoker: requestViaDynamicImport,
+ },
+ "video-tag": {
+ path: "/common/security-features/subresource/video.py",
+ invoker: requestViaVideo,
+ },
+ "xhr": {
+ path: "/common/security-features/subresource/xhr.py",
+ invoker: requestViaXhr,
+ },
+
+ "worker-classic": {
+ path: "/common/security-features/subresource/worker.py",
+ invoker: url => requestViaDedicatedWorker(url),
+ },
+ "worker-module": {
+ path: "/common/security-features/subresource/worker.py",
+ invoker: url => requestViaDedicatedWorker(url, {type: "module"}),
+ },
+ "worker-import": {
+ path: "/common/security-features/subresource/worker.py",
+ invoker: (url, additionalAttributes) =>
+ requestViaDedicatedWorker(workerUrlThatImports(url, additionalAttributes), {type: "module"}),
+ },
+ "worker-import-data": {
+ path: "/common/security-features/subresource/worker.py",
+ invoker: url =>
+ requestViaDedicatedWorker(workerDataUrlThatImports(url), {type: "module"}),
+ },
+ "sharedworker-classic": {
+ path: "/common/security-features/subresource/shared-worker.py",
+ invoker: url => requestViaSharedWorker(url),
+ },
+ "sharedworker-module": {
+ path: "/common/security-features/subresource/shared-worker.py",
+ invoker: url => requestViaSharedWorker(url, {type: "module"}),
+ },
+ "sharedworker-import": {
+ path: "/common/security-features/subresource/shared-worker.py",
+ invoker: (url, additionalAttributes) =>
+ requestViaSharedWorker(workerUrlThatImports(url, additionalAttributes), {type: "module"}),
+ },
+ "sharedworker-import-data": {
+ path: "/common/security-features/subresource/shared-worker.py",
+ invoker: url =>
+ requestViaSharedWorker(workerDataUrlThatImports(url), {type: "module"}),
+ },
+
+ "websocket": {
+ path: "/stash_responder",
+ invoker: requestViaWebSocket,
+ },
+};
+for (const workletType of ['animation', 'audio', 'layout', 'paint']) {
+ subresourceMap[`worklet-${workletType}`] = {
+ path: "/common/security-features/subresource/worker.py",
+ invoker: url => requestViaWorklet(workletType, url)
+ };
+ subresourceMap[`worklet-${workletType}-import-data`] = {
+ path: "/common/security-features/subresource/worker.py",
+ invoker: url =>
+ requestViaWorklet(workletType, workerDataUrlThatImports(url))
+ };
+}
+
+/**
+ @typedef RedirectionType
+ @type {string}
+
+ Represents what redirects should occur to the subresource request
+ after initial request.
+ See preprocess_redirection() in
+ /common/security-features/subresource/subresource.py for valid values.
+*/
+
+/**
+ Construct subresource (and related) origin.
+
+ @param {string} originType
+ @returns {object} the origin of the subresource.
+*/
+function getSubresourceOrigin(originType) {
+ const httpProtocol = "http";
+ const httpsProtocol = "https";
+ const wsProtocol = "ws";
+ const wssProtocol = "wss";
+
+ const sameOriginHost = "{{host}}";
+ const crossOriginHost = "{{domains[www1]}}";
+
+ // These values can evaluate to either empty strings or a ":port" string.
+ const httpPort = getNormalizedPort(parseInt("{{ports[http][0]}}", 10));
+ const httpsRawPort = parseInt("{{ports[https][0]}}", 10);
+ const httpsPort = getNormalizedPort(httpsRawPort);
+ const wsPort = getNormalizedPort(parseInt("{{ports[ws][0]}}", 10));
+ const wssRawPort = parseInt("{{ports[wss][0]}}", 10);
+ const wssPort = getNormalizedPort(wssRawPort);
+
+ /**
+ @typedef OriginType
+ @type {string}
+
+ Represents the origin of the subresource request URL.
+ The keys of `originMap` below are the valid values.
+
+ Note that there can be redirects from the specified origin
+ (see RedirectionType), and thus the origin of the subresource
+ response URL might be different from what is specified by OriginType.
+ */
+ const originMap = {
+ "same-https": httpsProtocol + "://" + sameOriginHost + httpsPort,
+ "same-http": httpProtocol + "://" + sameOriginHost + httpPort,
+ "cross-https": httpsProtocol + "://" + crossOriginHost + httpsPort,
+ "cross-http": httpProtocol + "://" + crossOriginHost + httpPort,
+ "same-wss": wssProtocol + "://" + sameOriginHost + wssPort,
+ "same-ws": wsProtocol + "://" + sameOriginHost + wsPort,
+ "cross-wss": wssProtocol + "://" + crossOriginHost + wssPort,
+ "cross-ws": wsProtocol + "://" + crossOriginHost + wsPort,
+
+ // The following origin types are used for upgrade-insecure-requests tests:
+ // These rely on some unintuitive cleverness due to WPT's test setup:
+ // 'Upgrade-Insecure-Requests' does not upgrade the port number,
+ // so we use URLs in the form `http://[domain]:[https-port]`,
+ // which will be upgraded to `https://[domain]:[https-port]`.
+ // If the upgrade fails, the load will fail, as we don't serve HTTP over
+ // the secure port.
+ "same-http-downgrade":
+ httpProtocol + "://" + sameOriginHost + ":" + httpsRawPort,
+ "cross-http-downgrade":
+ httpProtocol + "://" + crossOriginHost + ":" + httpsRawPort,
+ "same-ws-downgrade":
+ wsProtocol + "://" + sameOriginHost + ":" + wssRawPort,
+ "cross-ws-downgrade":
+ wsProtocol + "://" + crossOriginHost + ":" + wssRawPort,
+ };
+
+ return originMap[originType];
+}
+
+/**
+ Construct subresource (and related) URLs.
+
+ @param {SubresourceType} subresourceType
+ @param {OriginType} originType
+ @param {RedirectionType} redirectionType
+ @returns {object} with following properties:
+ {string} testUrl
+ The subresource request URL.
+ {string} announceUrl
+ {string} assertUrl
+ The URLs to be used for detecting whether `testUrl` is actually sent
+ to the server.
+ 1. Fetch `announceUrl` first,
+ 2. then possibly fetch `testUrl`, and
+ 3. finally fetch `assertUrl`.
+ The fetch result of `assertUrl` should indicate whether
+ `testUrl` is actually sent to the server or not.
+*/
+function getRequestURLs(subresourceType, originType, redirectionType) {
+ const key = guid();
+ const value = guid();
+
+ // We use the same stash path for both HTTP/S and WS/S stash requests.
+ const stashPath = encodeURIComponent("/mixed-content");
+
+ const stashEndpoint = "/common/security-features/subresource/xhr.py?key=" +
+ key + "&path=" + stashPath;
+ return {
+ testUrl:
+ getSubresourceOrigin(originType) +
+ subresourceMap[subresourceType].path +
+ "?redirection=" + encodeURIComponent(redirectionType) +
+ "&action=purge&key=" + key +
+ "&path=" + stashPath,
+ announceUrl: stashEndpoint + "&action=put&value=" + value,
+ assertUrl: stashEndpoint + "&action=take",
+ };
+}
+
+// ===============================================================
+// Source Context
+// ===============================================================
+// Requests can be sent from several source contexts,
+// such as the main documents, iframes, workers, or so,
+// possibly nested, and possibly with <meta>/http headers added.
+// invokeRequest() and invokeFrom*() functions handles
+// SourceContext-related setup in client-side.
+
+/**
+ invokeRequest() invokes a subresource request
+ (specified as `subresource`)
+ from a (possibly nested) environment settings object
+ (specified as `sourceContextList`).
+
+ For nested contexts, invokeRequest() calls an invokeFrom*() function
+ that creates a nested environment settings object using
+ /common/security-features/scope/, which calls invokeRequest()
+ again inside the nested environment settings object.
+ This cycle continues until all specified
+ nested environment settings object are created, and
+ finally invokeRequest() calls a requestVia*() function to start the
+ subresource request from the inner-most environment settings object.
+
+ @param {Subresource} subresource
+ @param {Array<SourceContext>} sourceContextList
+
+ @returns {Promise} A promise that is resolved with an RequestResult object.
+ `sourceContextUrl` is always set. For whether other properties are set,
+ see the comments for requestVia*() above.
+*/
+function invokeRequest(subresource, sourceContextList) {
+ if (sourceContextList.length === 0) {
+ // No further nested global objects. Send the subresource request here.
+
+ const additionalAttributes = {};
+ /** @type {PolicyDelivery} policyDelivery */
+ for (const policyDelivery of (subresource.policyDeliveries || [])) {
+ // Depending on the delivery method, extend the subresource element with
+ // these attributes.
+ if (policyDelivery.deliveryType === "attr") {
+ additionalAttributes[policyDelivery.key] = policyDelivery.value;
+ } else if (policyDelivery.deliveryType === "rel-noref") {
+ additionalAttributes["rel"] = "noreferrer";
+ } else if (policyDelivery.deliveryType === "http-rp") {
+ additionalAttributes[policyDelivery.key] = policyDelivery.value;
+ } else if (policyDelivery.deliveryType === "meta") {
+ additionalAttributes[policyDelivery.key] = policyDelivery.value;
+ }
+ }
+
+ return subresourceMap[subresource.subresourceType].invoker(
+ subresource.url,
+ additionalAttributes)
+ .then(result => Object.assign(
+ {sourceContextUrl: location.toString()},
+ result));
+ }
+
+ // Defines invokers for each valid SourceContext.sourceContextType.
+ const sourceContextMap = {
+ "srcdoc": { // <iframe srcdoc></iframe>
+ invoker: invokeFromIframe,
+ },
+ "iframe": { // <iframe src="same-origin-URL"></iframe>
+ invoker: invokeFromIframe,
+ },
+ "iframe-blank": { // <iframe></iframe>
+ invoker: invokeFromIframe,
+ },
+ "worker-classic": {
+ // Classic dedicated worker loaded from same-origin.
+ invoker: invokeFromWorker.bind(undefined, "worker", false, {}),
+ },
+ "worker-classic-data": {
+ // Classic dedicated worker loaded from data: URL.
+ invoker: invokeFromWorker.bind(undefined, "worker", true, {}),
+ },
+ "worker-module": {
+ // Module dedicated worker loaded from same-origin.
+ invoker: invokeFromWorker.bind(undefined, "worker", false, {type: 'module'}),
+ },
+ "worker-module-data": {
+ // Module dedicated worker loaded from data: URL.
+ invoker: invokeFromWorker.bind(undefined, "worker", true, {type: 'module'}),
+ },
+ "sharedworker-classic": {
+ // Classic shared worker loaded from same-origin.
+ invoker: invokeFromWorker.bind(undefined, "sharedworker", false, {}),
+ },
+ "sharedworker-classic-data": {
+ // Classic shared worker loaded from data: URL.
+ invoker: invokeFromWorker.bind(undefined, "sharedworker", true, {}),
+ },
+ "sharedworker-module": {
+ // Module shared worker loaded from same-origin.
+ invoker: invokeFromWorker.bind(undefined, "sharedworker", false, {type: 'module'}),
+ },
+ "sharedworker-module-data": {
+ // Module shared worker loaded from data: URL.
+ invoker: invokeFromWorker.bind(undefined, "sharedworker", true, {type: 'module'}),
+ },
+ };
+
+ return sourceContextMap[sourceContextList[0].sourceContextType].invoker(
+ subresource, sourceContextList);
+}
+
+// Quick hack to expose invokeRequest when common.sub.js is loaded either
+// as a classic or module script.
+self.invokeRequest = invokeRequest;
+
+/**
+ invokeFrom*() functions are helper functions with the same parameters
+ and return values as invokeRequest(), that are tied to specific types
+ of top-most environment settings objects.
+ For example, invokeFromIframe() is the helper function for the cases where
+ sourceContextList[0] is an iframe.
+*/
+
+/**
+ @param {string} workerType
+ "worker" (for dedicated worker) or "sharedworker".
+ @param {boolean} isDataUrl
+ true if the worker script is loaded from data: URL.
+ Otherwise, the script is loaded from same-origin.
+ @param {object} workerOptions
+ The `options` argument for Worker constructor.
+
+ Other parameters and return values are the same as those of invokeRequest().
+*/
+function invokeFromWorker(workerType, isDataUrl, workerOptions,
+ subresource, sourceContextList) {
+ const currentSourceContext = sourceContextList[0];
+ let workerUrl =
+ "/common/security-features/scope/worker.py?policyDeliveries=" +
+ encodeURIComponent(JSON.stringify(
+ currentSourceContext.policyDeliveries || []));
+ if (workerOptions.type === 'module') {
+ workerUrl += "&type=module";
+ }
+
+ let promise;
+ if (isDataUrl) {
+ promise = fetch(workerUrl)
+ .then(r => r.text())
+ .then(source => {
+ return 'data:text/javascript;base64,' + btoa(source);
+ });
+ } else {
+ promise = Promise.resolve(workerUrl);
+ }
+
+ return promise
+ .then(url => {
+ if (workerType === "worker") {
+ const worker = new Worker(url, workerOptions);
+ worker.postMessage({subresource: subresource,
+ sourceContextList: sourceContextList.slice(1)});
+ return bindEvents2(worker, "message", worker, "error", window, "error");
+ } else if (workerType === "sharedworker") {
+ const worker = new SharedWorker(url, workerOptions);
+ worker.port.start();
+ worker.port.postMessage({subresource: subresource,
+ sourceContextList: sourceContextList.slice(1)});
+ return bindEvents2(worker.port, "message", worker, "error", window, "error");
+ } else {
+ throw new Error('Invalid worker type: ' + workerType);
+ }
+ })
+ .then(event => {
+ if (event.data.error)
+ return Promise.reject(event.data.error);
+ return event.data;
+ });
+}
+
+function invokeFromIframe(subresource, sourceContextList) {
+ const currentSourceContext = sourceContextList[0];
+ const frameUrl =
+ "/common/security-features/scope/document.py?policyDeliveries=" +
+ encodeURIComponent(JSON.stringify(
+ currentSourceContext.policyDeliveries || []));
+
+ let iframe;
+ let promise;
+ if (currentSourceContext.sourceContextType === 'srcdoc') {
+ promise = fetch(frameUrl)
+ .then(r => r.text())
+ .then(srcdoc => {
+ iframe = createElement(
+ "iframe", {srcdoc: srcdoc}, document.body, true);
+ return iframe.eventPromise;
+ });
+ } else if (currentSourceContext.sourceContextType === 'iframe') {
+ iframe = createElement("iframe", {src: frameUrl}, document.body, true);
+ promise = iframe.eventPromise;
+ } else if (currentSourceContext.sourceContextType === 'iframe-blank') {
+ let frameContent;
+ promise = fetch(frameUrl)
+ .then(r => r.text())
+ .then(t => {
+ frameContent = t;
+ iframe = createElement("iframe", {}, document.body, true);
+ return iframe.eventPromise;
+ })
+ .then(() => {
+ // Reinitialize `iframe.eventPromise` with a new promise
+ // that catches the load event for the document.write() below.
+ bindEvents(iframe);
+
+ iframe.contentDocument.write(frameContent);
+ iframe.contentDocument.close();
+ return iframe.eventPromise;
+ });
+ }
+
+ return promise
+ .then(() => {
+ const promise = bindEvents2(
+ window, "message", iframe, "error", window, "error");
+ iframe.contentWindow.postMessage(
+ {subresource: subresource,
+ sourceContextList: sourceContextList.slice(1)},
+ "*");
+ return promise;
+ })
+ .then(event => {
+ if (event.data.error)
+ return Promise.reject(event.data.error);
+ return event.data;
+ });
+}
+
+// SanityChecker does nothing in release mode. See sanity-checker.js for debug
+// mode.
+function SanityChecker() {}
+SanityChecker.prototype.checkScenario = function() {};
+SanityChecker.prototype.setFailTimeout = function(test, timeout) {};
+SanityChecker.prototype.checkSubresourceResult = function() {};
diff --git a/test/wpt/tests/common/security-features/resources/common.sub.js.headers b/test/wpt/tests/common/security-features/resources/common.sub.js.headers
new file mode 100644
index 0000000..cb762ef
--- /dev/null
+++ b/test/wpt/tests/common/security-features/resources/common.sub.js.headers
@@ -0,0 +1 @@
+Access-Control-Allow-Origin: *
diff --git a/test/wpt/tests/common/security-features/scope/__init__.py b/test/wpt/tests/common/security-features/scope/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/wpt/tests/common/security-features/scope/__init__.py
diff --git a/test/wpt/tests/common/security-features/scope/document.py b/test/wpt/tests/common/security-features/scope/document.py
new file mode 100644
index 0000000..9a9f045
--- /dev/null
+++ b/test/wpt/tests/common/security-features/scope/document.py
@@ -0,0 +1,36 @@
+import os, sys, json
+
+from wptserve.utils import isomorphic_decode, isomorphic_encode
+
+import importlib
+util = importlib.import_module("common.security-features.scope.util")
+
+def main(request, response):
+ policyDeliveries = json.loads(request.GET.first(b"policyDeliveries", b"[]"))
+ maybe_additional_headers = {}
+ meta = u''
+ error = u''
+ for delivery in policyDeliveries:
+ if delivery[u'deliveryType'] == u'meta':
+ if delivery[u'key'] == u'referrerPolicy':
+ meta += u'<meta name="referrer" content="%s">' % delivery[u'value']
+ else:
+ error = u'invalid delivery key'
+ elif delivery[u'deliveryType'] == u'http-rp':
+ if delivery[u'key'] == u'referrerPolicy':
+ maybe_additional_headers[b'Referrer-Policy'] = isomorphic_encode(delivery[u'value'])
+ else:
+ error = u'invalid delivery key'
+ else:
+ error = u'invalid deliveryType'
+
+ handler = lambda: util.get_template(u"document.html.template") % ({
+ u"meta": meta,
+ u"error": error
+ })
+ util.respond(
+ request,
+ response,
+ payload_generator=handler,
+ content_type=b"text/html",
+ maybe_additional_headers=maybe_additional_headers)
diff --git a/test/wpt/tests/common/security-features/scope/template/document.html.template b/test/wpt/tests/common/security-features/scope/template/document.html.template
new file mode 100644
index 0000000..37e29f8
--- /dev/null
+++ b/test/wpt/tests/common/security-features/scope/template/document.html.template
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ %(meta)s
+ <script src="/common/security-features/resources/common.sub.js"></script>
+ <script>
+ // Receive a message from the parent and start the test.
+ function onMessageFromParent(event) {
+ // Because this window might receive messages from child iframe during
+ // tests, we first remove the listener here before staring the test.
+ window.removeEventListener('message', onMessageFromParent);
+
+ const configurationError = "%(error)s";
+ if (configurationError.length > 0) {
+ parent.postMessage({error: configurationError}, "*");
+ return;
+ }
+
+ invokeRequest(event.data.subresource,
+ event.data.sourceContextList)
+ .then(result => parent.postMessage(result, "*"))
+ .catch(e => {
+ const message = (e.error && e.error.stack) || e.message || "Error";
+ parent.postMessage({error: message}, "*");
+ });
+ }
+ window.addEventListener('message', onMessageFromParent);
+ </script>
+ </head>
+</html>
diff --git a/test/wpt/tests/common/security-features/scope/template/worker.js.template b/test/wpt/tests/common/security-features/scope/template/worker.js.template
new file mode 100644
index 0000000..7a2a6e0
--- /dev/null
+++ b/test/wpt/tests/common/security-features/scope/template/worker.js.template
@@ -0,0 +1,29 @@
+%(import)s
+
+if ('DedicatedWorkerGlobalScope' in self &&
+ self instanceof DedicatedWorkerGlobalScope) {
+ self.onmessage = event => onMessageFromParent(event, self);
+} else if ('SharedWorkerGlobalScope' in self &&
+ self instanceof SharedWorkerGlobalScope) {
+ onconnect = event => {
+ const port = event.ports[0];
+ port.onmessage = event => onMessageFromParent(event, port);
+ };
+}
+
+// Receive a message from the parent and start the test.
+function onMessageFromParent(event, port) {
+ const configurationError = "%(error)s";
+ if (configurationError.length > 0) {
+ port.postMessage({error: configurationError});
+ return;
+ }
+
+ invokeRequest(event.data.subresource,
+ event.data.sourceContextList)
+ .then(result => port.postMessage(result))
+ .catch(e => {
+ const message = (e.error && e.error.stack) || e.message || "Error";
+ port.postMessage({error: message});
+ });
+}
diff --git a/test/wpt/tests/common/security-features/scope/util.py b/test/wpt/tests/common/security-features/scope/util.py
new file mode 100644
index 0000000..da5aacf
--- /dev/null
+++ b/test/wpt/tests/common/security-features/scope/util.py
@@ -0,0 +1,43 @@
+import os
+
+from wptserve.utils import isomorphic_decode
+
+def get_template(template_basename):
+ script_directory = os.path.dirname(os.path.abspath(isomorphic_decode(__file__)))
+ template_directory = os.path.abspath(
+ os.path.join(script_directory, u"template"))
+ template_filename = os.path.join(template_directory, template_basename)
+
+ with open(template_filename, "r") as f:
+ return f.read()
+
+
+def __noop(request, response):
+ return u""
+
+
+def respond(request,
+ response,
+ status_code=200,
+ content_type=b"text/html",
+ payload_generator=__noop,
+ cache_control=b"no-cache; must-revalidate",
+ access_control_allow_origin=b"*",
+ maybe_additional_headers=None):
+ response.add_required_headers = False
+ response.writer.write_status(status_code)
+
+ if access_control_allow_origin != None:
+ response.writer.write_header(b"access-control-allow-origin",
+ access_control_allow_origin)
+ response.writer.write_header(b"content-type", content_type)
+ response.writer.write_header(b"cache-control", cache_control)
+
+ additional_headers = maybe_additional_headers or {}
+ for header, value in additional_headers.items():
+ response.writer.write_header(header, value)
+
+ response.writer.end_headers()
+
+ payload = payload_generator()
+ response.writer.write(payload)
diff --git a/test/wpt/tests/common/security-features/scope/worker.py b/test/wpt/tests/common/security-features/scope/worker.py
new file mode 100644
index 0000000..6b321e7
--- /dev/null
+++ b/test/wpt/tests/common/security-features/scope/worker.py
@@ -0,0 +1,44 @@
+import os, sys, json
+
+from wptserve.utils import isomorphic_decode, isomorphic_encode
+import importlib
+util = importlib.import_module("common.security-features.scope.util")
+
+def main(request, response):
+ policyDeliveries = json.loads(request.GET.first(b'policyDeliveries', b'[]'))
+ worker_type = request.GET.first(b'type', b'classic')
+ commonjs_url = u'%s://%s:%s/common/security-features/resources/common.sub.js' % (
+ request.url_parts.scheme, request.url_parts.hostname,
+ request.url_parts.port)
+ if worker_type == b'classic':
+ import_line = u'importScripts("%s");' % commonjs_url
+ else:
+ import_line = u'import "%s";' % commonjs_url
+
+ maybe_additional_headers = {}
+ error = u''
+ for delivery in policyDeliveries:
+ if delivery[u'deliveryType'] == u'meta':
+ error = u'<meta> cannot be used in WorkerGlobalScope'
+ elif delivery[u'deliveryType'] == u'http-rp':
+ if delivery[u'key'] == u'referrerPolicy':
+ maybe_additional_headers[b'Referrer-Policy'] = isomorphic_encode(delivery[u'value'])
+ elif delivery[u'key'] == u'mixedContent' and delivery[u'value'] == u'opt-in':
+ maybe_additional_headers[b'Content-Security-Policy'] = b'block-all-mixed-content'
+ elif delivery[u'key'] == u'upgradeInsecureRequests' and delivery[u'value'] == u'upgrade':
+ maybe_additional_headers[b'Content-Security-Policy'] = b'upgrade-insecure-requests'
+ else:
+ error = u'invalid delivery key for http-rp: %s' % delivery[u'key']
+ else:
+ error = u'invalid deliveryType: %s' % delivery[u'deliveryType']
+
+ handler = lambda: util.get_template(u'worker.js.template') % ({
+ u'import': import_line,
+ u'error': error
+ })
+ util.respond(
+ request,
+ response,
+ payload_generator=handler,
+ content_type=b'text/javascript',
+ maybe_additional_headers=maybe_additional_headers)
diff --git a/test/wpt/tests/common/security-features/subresource/__init__.py b/test/wpt/tests/common/security-features/subresource/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/wpt/tests/common/security-features/subresource/__init__.py
diff --git a/test/wpt/tests/common/security-features/subresource/audio.py b/test/wpt/tests/common/security-features/subresource/audio.py
new file mode 100644
index 0000000..f16a0f7
--- /dev/null
+++ b/test/wpt/tests/common/security-features/subresource/audio.py
@@ -0,0 +1,18 @@
+import os, sys
+from wptserve.utils import isomorphic_decode
+import importlib
+subresource = importlib.import_module("common.security-features.subresource.subresource")
+
+def generate_payload(request, server_data):
+ file = os.path.join(request.doc_root, u"webaudio", u"resources",
+ u"sin_440Hz_-6dBFS_1s.wav")
+ return open(file, "rb").read()
+
+
+def main(request, response):
+ handler = lambda data: generate_payload(request, data)
+ subresource.respond(request,
+ response,
+ payload_generator = handler,
+ access_control_allow_origin = b"*",
+ content_type = b"audio/wav")
diff --git a/test/wpt/tests/common/security-features/subresource/document.py b/test/wpt/tests/common/security-features/subresource/document.py
new file mode 100644
index 0000000..52b684a
--- /dev/null
+++ b/test/wpt/tests/common/security-features/subresource/document.py
@@ -0,0 +1,12 @@
+import os, sys
+from wptserve.utils import isomorphic_decode
+import importlib
+subresource = importlib.import_module("common.security-features.subresource.subresource")
+
+def generate_payload(server_data):
+ return subresource.get_template(u"document.html.template") % server_data
+
+def main(request, response):
+ subresource.respond(request,
+ response,
+ payload_generator = generate_payload)
diff --git a/test/wpt/tests/common/security-features/subresource/empty.py b/test/wpt/tests/common/security-features/subresource/empty.py
new file mode 100644
index 0000000..312e12c
--- /dev/null
+++ b/test/wpt/tests/common/security-features/subresource/empty.py
@@ -0,0 +1,14 @@
+import os, sys
+from wptserve.utils import isomorphic_decode
+import importlib
+subresource = importlib.import_module("common.security-features.subresource.subresource")
+
+def generate_payload(server_data):
+ return u''
+
+def main(request, response):
+ subresource.respond(request,
+ response,
+ payload_generator = generate_payload,
+ access_control_allow_origin = b"*",
+ content_type = b"text/plain")
diff --git a/test/wpt/tests/common/security-features/subresource/font.py b/test/wpt/tests/common/security-features/subresource/font.py
new file mode 100644
index 0000000..7900079
--- /dev/null
+++ b/test/wpt/tests/common/security-features/subresource/font.py
@@ -0,0 +1,76 @@
+import os, sys
+from base64 import decodebytes
+
+from wptserve.utils import isomorphic_decode
+import importlib
+subresource = importlib.import_module("common.security-features.subresource.subresource")
+
+
+def generate_payload(request, server_data):
+ data = (u'{"headers": %(headers)s}') % server_data
+ if b"id" in request.GET:
+ request.server.stash.put(request.GET[b"id"], data)
+ # Simple base64 encoded .tff font
+ return decodebytes(b"AAEAAAANAIAAAwBQRkZUTU6u6MkAAAXcAAAAHE9TLzJWYW"
+ b"QKAAABWAAAAFZjbWFwAA8D7wAAAcAAAAFCY3Z0IAAhAnkA"
+ b"AAMEAAAABGdhc3D//wADAAAF1AAAAAhnbHlmCC6aTwAAAx"
+ b"QAAACMaGVhZO8ooBcAAADcAAAANmhoZWEIkAV9AAABFAAA"
+ b"ACRobXR4EZQAhQAAAbAAAAAQbG9jYQBwAFQAAAMIAAAACm"
+ b"1heHAASQA9AAABOAAAACBuYW1lehAVOgAAA6AAAAIHcG9z"
+ b"dP+uADUAAAWoAAAAKgABAAAAAQAAMhPyuV8PPPUACwPoAA"
+ b"AAAMU4Lm0AAAAAxTgubQAh/5wFeAK8AAAACAACAAAAAAAA"
+ b"AAEAAAK8/5wAWgXcAAAAAAV4AAEAAAAAAAAAAAAAAAAAAA"
+ b"AEAAEAAAAEAAwAAwAAAAAAAgAAAAEAAQAAAEAALgAAAAAA"
+ b"AQXcAfQABQAAAooCvAAAAIwCigK8AAAB4AAxAQIAAAIABg"
+ b"kAAAAAAAAAAAABAAAAAAAAAAAAAAAAUGZFZABAAEEAQQMg"
+ b"/zgAWgK8AGQAAAABAAAAAAAABdwAIQAAAAAF3AAABdwAZA"
+ b"AAAAMAAAADAAAAHAABAAAAAAA8AAMAAQAAABwABAAgAAAA"
+ b"BAAEAAEAAABB//8AAABB////wgABAAAAAAAAAQYAAAEAAA"
+ b"AAAAAAAQIAAAACAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAA"
+ b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAAAAAA"
+ b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
+ b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
+ b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
+ b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
+ b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
+ b"AAAAAAAAAAAAAAAAAAAhAnkAAAAqACoAKgBGAAAAAgAhAA"
+ b"ABKgKaAAMABwAusQEALzyyBwQA7TKxBgXcPLIDAgDtMgCx"
+ b"AwAvPLIFBADtMrIHBgH8PLIBAgDtMjMRIREnMxEjIQEJ6M"
+ b"fHApr9ZiECWAAAAwBk/5wFeAK8AAMABwALAAABNSEVATUh"
+ b"FQE1IRUB9AH0/UQDhPu0BRQB9MjI/tTIyP7UyMgAAAAAAA"
+ b"4ArgABAAAAAAAAACYATgABAAAAAAABAAUAgQABAAAAAAAC"
+ b"AAYAlQABAAAAAAADACEA4AABAAAAAAAEAAUBDgABAAAAAA"
+ b"AFABABNgABAAAAAAAGAAUBUwADAAEECQAAAEwAAAADAAEE"
+ b"CQABAAoAdQADAAEECQACAAwAhwADAAEECQADAEIAnAADAA"
+ b"EECQAEAAoBAgADAAEECQAFACABFAADAAEECQAGAAoBRwBD"
+ b"AG8AcAB5AHIAaQBnAGgAdAAgACgAYwApACAAMgAwADAAOA"
+ b"AgAE0AbwB6AGkAbABsAGEAIABDAG8AcgBwAG8AcgBhAHQA"
+ b"aQBvAG4AAENvcHlyaWdodCAoYykgMjAwOCBNb3ppbGxhIE"
+ b"NvcnBvcmF0aW9uAABNAGEAcgBrAEEAAE1hcmtBAABNAGUA"
+ b"ZABpAHUAbQAATWVkaXVtAABGAG8AbgB0AEYAbwByAGcAZQ"
+ b"AgADIALgAwACAAOgAgAE0AYQByAGsAQQAgADoAIAA1AC0A"
+ b"MQAxAC0AMgAwADAAOAAARm9udEZvcmdlIDIuMCA6IE1hcm"
+ b"tBIDogNS0xMS0yMDA4AABNAGEAcgBrAEEAAE1hcmtBAABW"
+ b"AGUAcgBzAGkAbwBuACAAMAAwADEALgAwADAAMAAgAABWZX"
+ b"JzaW9uIDAwMS4wMDAgAABNAGEAcgBrAEEAAE1hcmtBAAAA"
+ b"AgAAAAAAAP+DADIAAAABAAAAAAAAAAAAAAAAAAAAAAAEAA"
+ b"AAAQACACQAAAAAAAH//wACAAAAAQAAAADEPovuAAAAAMU4"
+ b"Lm0AAAAAxTgubQ==")
+
+def generate_report_headers_payload(request, server_data):
+ stashed_data = request.server.stash.take(request.GET[b"id"])
+ return stashed_data
+
+def main(request, response):
+ handler = lambda data: generate_payload(request, data)
+ content_type = b'application/x-font-truetype'
+
+ if b"report-headers" in request.GET:
+ handler = lambda data: generate_report_headers_payload(request, data)
+ content_type = b'application/json'
+
+ subresource.respond(request,
+ response,
+ payload_generator = handler,
+ content_type = content_type,
+ access_control_allow_origin = b"*")
diff --git a/test/wpt/tests/common/security-features/subresource/image.py b/test/wpt/tests/common/security-features/subresource/image.py
new file mode 100644
index 0000000..5c9a0c0
--- /dev/null
+++ b/test/wpt/tests/common/security-features/subresource/image.py
@@ -0,0 +1,116 @@
+import os, sys, array, math
+
+from io import BytesIO
+
+from wptserve.utils import isomorphic_decode
+
+import importlib
+subresource = importlib.import_module("common.security-features.subresource.subresource")
+
+class Image:
+ """This class partially implements the interface of the PIL.Image.Image.
+ One day in the future WPT might support the PIL module or another imaging
+ library, so this hacky BMP implementation will no longer be required.
+ """
+ def __init__(self, width, height):
+ self.width = width
+ self.height = height
+ self.img = bytearray([0 for i in range(3 * width * height)])
+
+ @staticmethod
+ def new(mode, size, color=0):
+ return Image(size[0], size[1])
+
+ def _int_to_bytes(self, number):
+ packed_bytes = [0, 0, 0, 0]
+ for i in range(4):
+ packed_bytes[i] = number & 0xFF
+ number >>= 8
+
+ return packed_bytes
+
+ def putdata(self, color_data):
+ for y in range(self.height):
+ for x in range(self.width):
+ i = x + y * self.width
+ if i > len(color_data) - 1:
+ return
+
+ self.img[i * 3: i * 3 + 3] = color_data[i][::-1]
+
+ def save(self, f, type):
+ assert type == "BMP"
+ # 54 bytes of preambule + image color data.
+ filesize = 54 + 3 * self.width * self.height
+ # 14 bytes of header.
+ bmpfileheader = bytearray([ord('B'), ord('M')] + self._int_to_bytes(filesize) +
+ [0, 0, 0, 0, 54, 0, 0, 0])
+ # 40 bytes of info.
+ bmpinfoheader = bytearray([40, 0, 0, 0] +
+ self._int_to_bytes(self.width) +
+ self._int_to_bytes(self.height) +
+ [1, 0, 24] + (25 * [0]))
+
+ padlength = (4 - (self.width * 3) % 4) % 4
+ bmppad = bytearray([0, 0, 0])
+ padding = bmppad[0 : padlength]
+
+ f.write(bmpfileheader)
+ f.write(bmpinfoheader)
+
+ for i in range(self.height):
+ offset = self.width * (self.height - i - 1) * 3
+ f.write(self.img[offset : offset + 3 * self.width])
+ f.write(padding)
+
+def encode_string_as_bmp_image(string_data):
+ data_bytes = array.array("B", string_data.encode("utf-8"))
+
+ num_bytes = len(data_bytes)
+
+ # Encode data bytes to color data (RGB), one bit per channel.
+ # This is to avoid errors due to different color spaces used in decoding.
+ color_data = []
+ for byte in data_bytes:
+ p = [int(x) * 255 for x in '{0:08b}'.format(byte)]
+ color_data.append((p[0], p[1], p[2]))
+ color_data.append((p[3], p[4], p[5]))
+ color_data.append((p[6], p[7], 0))
+
+ # Render image.
+ num_pixels = len(color_data)
+ sqrt = int(math.ceil(math.sqrt(num_pixels)))
+ img = Image.new("RGB", (sqrt, sqrt), "black")
+ img.putdata(color_data)
+
+ # Flush image to string.
+ f = BytesIO()
+ img.save(f, "BMP")
+ f.seek(0)
+
+ return f.read()
+
+def generate_payload(request, server_data):
+ data = (u'{"headers": %(headers)s}') % server_data
+ if b"id" in request.GET:
+ request.server.stash.put(request.GET[b"id"], data)
+ data = encode_string_as_bmp_image(data)
+ return data
+
+def generate_report_headers_payload(request, server_data):
+ stashed_data = request.server.stash.take(request.GET[b"id"])
+ return stashed_data
+
+def main(request, response):
+ handler = lambda data: generate_payload(request, data)
+ content_type = b'image/bmp'
+
+ if b"report-headers" in request.GET:
+ handler = lambda data: generate_report_headers_payload(request, data)
+ content_type = b'application/json'
+
+ subresource.respond(request,
+ response,
+ payload_generator = handler,
+ content_type = content_type,
+ access_control_allow_origin = b"*")
diff --git a/test/wpt/tests/common/security-features/subresource/referrer.py b/test/wpt/tests/common/security-features/subresource/referrer.py
new file mode 100644
index 0000000..e366314
--- /dev/null
+++ b/test/wpt/tests/common/security-features/subresource/referrer.py
@@ -0,0 +1,4 @@
+def main(request, response):
+ referrer = request.headers.get(b"referer", b"")
+ response_headers = [(b"Content-Type", b"text/javascript")]
+ return (200, response_headers, b"window.referrer = '" + referrer + b"'")
diff --git a/test/wpt/tests/common/security-features/subresource/script.py b/test/wpt/tests/common/security-features/subresource/script.py
new file mode 100644
index 0000000..9701816
--- /dev/null
+++ b/test/wpt/tests/common/security-features/subresource/script.py
@@ -0,0 +1,14 @@
+import os, sys
+from wptserve.utils import isomorphic_decode
+
+import importlib
+subresource = importlib.import_module("common.security-features.subresource.subresource")
+
+def generate_payload(server_data):
+ return subresource.get_template(u"script.js.template") % server_data
+
+def main(request, response):
+ subresource.respond(request,
+ response,
+ payload_generator = generate_payload,
+ content_type = b"application/javascript")
diff --git a/test/wpt/tests/common/security-features/subresource/shared-worker.py b/test/wpt/tests/common/security-features/subresource/shared-worker.py
new file mode 100644
index 0000000..bdfb61b
--- /dev/null
+++ b/test/wpt/tests/common/security-features/subresource/shared-worker.py
@@ -0,0 +1,13 @@
+import os, sys
+from wptserve.utils import isomorphic_decode
+import importlib
+subresource = importlib.import_module("common.security-features.subresource.subresource")
+
+def generate_payload(server_data):
+ return subresource.get_template(u"shared-worker.js.template") % server_data
+
+def main(request, response):
+ subresource.respond(request,
+ response,
+ payload_generator = generate_payload,
+ content_type = b"application/javascript")
diff --git a/test/wpt/tests/common/security-features/subresource/static-import.py b/test/wpt/tests/common/security-features/subresource/static-import.py
new file mode 100644
index 0000000..3c3a6f6
--- /dev/null
+++ b/test/wpt/tests/common/security-features/subresource/static-import.py
@@ -0,0 +1,61 @@
+import os, sys, json
+from urllib.parse import unquote
+
+from wptserve.utils import isomorphic_decode
+import importlib
+subresource = importlib.import_module("common.security-features.subresource.subresource")
+
+def get_csp_value(value):
+ '''
+ Returns actual CSP header values (e.g. "worker-src 'self'") for the
+ given string used in PolicyDelivery's value (e.g. "worker-src-self").
+ '''
+
+ # script-src
+ # Test-related scripts like testharness.js and inline scripts containing
+ # test bodies.
+ # 'unsafe-inline' is added as a workaround here. This is probably not so
+ # bad, as it shouldn't intefere non-inline-script requests that we want to
+ # test.
+ if value == 'script-src-wildcard':
+ return "script-src * 'unsafe-inline'"
+ if value == 'script-src-self':
+ return "script-src 'self' 'unsafe-inline'"
+ # Workaround for "script-src 'none'" would be more complicated, because
+ # - "script-src 'none' 'unsafe-inline'" is handled somehow differently from
+ # "script-src 'none'", i.e.
+ # https://w3c.github.io/webappsec-csp/#match-url-to-source-list Step 3
+ # handles the latter but not the former.
+ # - We need nonce- or path-based additional values to allow same-origin
+ # test scripts like testharness.js.
+ # Therefore, we disable 'script-src-none' tests for now in
+ # `/content-security-policy/spec.src.json`.
+ if value == 'script-src-none':
+ return "script-src 'none'"
+
+ # worker-src
+ if value == 'worker-src-wildcard':
+ return 'worker-src *'
+ if value == 'worker-src-self':
+ return "worker-src 'self'"
+ if value == 'worker-src-none':
+ return "worker-src 'none'"
+ raise Exception('Invalid delivery_value: %s' % value)
+
+def generate_payload(request):
+ import_url = unquote(isomorphic_decode(request.GET[b'import_url']))
+ return subresource.get_template(u"static-import.js.template") % {
+ u"import_url": import_url
+ }
+
+def main(request, response):
+ def payload_generator(_): return generate_payload(request)
+ maybe_additional_headers = {}
+ if b'contentSecurityPolicy' in request.GET:
+ csp = unquote(isomorphic_decode(request.GET[b'contentSecurityPolicy']))
+ maybe_additional_headers[b'Content-Security-Policy'] = get_csp_value(csp)
+ subresource.respond(request,
+ response,
+ payload_generator = payload_generator,
+ content_type = b"application/javascript",
+ maybe_additional_headers = maybe_additional_headers)
diff --git a/test/wpt/tests/common/security-features/subresource/stylesheet.py b/test/wpt/tests/common/security-features/subresource/stylesheet.py
new file mode 100644
index 0000000..05db249
--- /dev/null
+++ b/test/wpt/tests/common/security-features/subresource/stylesheet.py
@@ -0,0 +1,61 @@
+import os, sys
+from wptserve.utils import isomorphic_decode
+import importlib
+subresource = importlib.import_module("common.security-features.subresource.subresource")
+
+def generate_payload(request, server_data):
+ data = (u'{"headers": %(headers)s}') % server_data
+ type = b'image'
+ if b"type" in request.GET:
+ type = request.GET[b"type"]
+
+ if b"id" in request.GET:
+ request.server.stash.put(request.GET[b"id"], data)
+
+ if type == b'image':
+ return subresource.get_template(u"image.css.template") % {u"id": isomorphic_decode(request.GET[b"id"])}
+
+ elif type == b'font':
+ return subresource.get_template(u"font.css.template") % {u"id": isomorphic_decode(request.GET[b"id"])}
+
+ elif type == b'svg':
+ return subresource.get_template(u"svg.css.template") % {
+ u"id": isomorphic_decode(request.GET[b"id"]),
+ u"property": isomorphic_decode(request.GET[b"property"])}
+
+ # A `'stylesheet-only'`-type stylesheet has no nested resources; this is
+ # useful in tests that cover referrers for stylesheet fetches (e.g. fetches
+ # triggered by `@import` statements).
+ elif type == b'stylesheet-only':
+ return u''
+
+def generate_import_rule(request, server_data):
+ return u"@import url('%(url)s');" % {
+ u"url": subresource.create_url(request, swap_origin=True,
+ query_parameter_to_remove=u"import-rule")
+ }
+
+def generate_report_headers_payload(request, server_data):
+ stashed_data = request.server.stash.take(request.GET[b"id"])
+ return stashed_data
+
+def main(request, response):
+ payload_generator = lambda data: generate_payload(request, data)
+ content_type = b"text/css"
+ referrer_policy = b"unsafe-url"
+ if b"import-rule" in request.GET:
+ payload_generator = lambda data: generate_import_rule(request, data)
+
+ if b"report-headers" in request.GET:
+ payload_generator = lambda data: generate_report_headers_payload(request, data)
+ content_type = b'application/json'
+
+ if b"referrer-policy" in request.GET:
+ referrer_policy = request.GET[b"referrer-policy"]
+
+ subresource.respond(
+ request,
+ response,
+ payload_generator = payload_generator,
+ content_type = content_type,
+ maybe_additional_headers = { b"Referrer-Policy": referrer_policy })
diff --git a/test/wpt/tests/common/security-features/subresource/subresource.py b/test/wpt/tests/common/security-features/subresource/subresource.py
new file mode 100644
index 0000000..b3c055a
--- /dev/null
+++ b/test/wpt/tests/common/security-features/subresource/subresource.py
@@ -0,0 +1,199 @@
+import os, json
+from urllib.parse import parse_qsl, SplitResult, urlencode, urlsplit, urlunsplit
+
+from wptserve.utils import isomorphic_decode, isomorphic_encode
+
+def get_template(template_basename):
+ script_directory = os.path.dirname(os.path.abspath(isomorphic_decode(__file__)))
+ template_directory = os.path.abspath(os.path.join(script_directory,
+ u"template"))
+ template_filename = os.path.join(template_directory, template_basename)
+
+ with open(template_filename, "r") as f:
+ return f.read()
+
+
+def redirect(url, response):
+ response.add_required_headers = False
+ response.writer.write_status(301)
+ response.writer.write_header(b"access-control-allow-origin", b"*")
+ response.writer.write_header(b"location", isomorphic_encode(url))
+ response.writer.end_headers()
+ response.writer.write(u"")
+
+
+# TODO(kristijanburnik): subdomain_prefix is a hardcoded value aligned with
+# referrer-policy-test-case.js. The prefix should be configured in one place.
+def __get_swapped_origin_netloc(netloc, subdomain_prefix = u"www1."):
+ if netloc.startswith(subdomain_prefix):
+ return netloc[len(subdomain_prefix):]
+ else:
+ return subdomain_prefix + netloc
+
+
+# Creates a URL (typically a redirect target URL) that is the same as the
+# current request URL `request.url`, except for:
+# - When `swap_scheme` or `swap_origin` is True, its scheme/origin is changed
+# to the other one. (http <-> https, ws <-> wss, etc.)
+# - For `downgrade`, we redirect to a URL that would be successfully loaded
+# if and only if upgrade-insecure-request is applied.
+# - `query_parameter_to_remove` parameter is removed from query part.
+# Its default is "redirection" to avoid redirect loops.
+def create_url(request,
+ swap_scheme=False,
+ swap_origin=False,
+ downgrade=False,
+ query_parameter_to_remove=u"redirection"):
+ parsed = urlsplit(request.url)
+ destination_netloc = parsed.netloc
+
+ scheme = parsed.scheme
+ if swap_scheme:
+ scheme = u"http" if parsed.scheme == u"https" else u"https"
+ hostname = parsed.netloc.split(u':')[0]
+ port = request.server.config[u"ports"][scheme][0]
+ destination_netloc = u":".join([hostname, str(port)])
+
+ if downgrade:
+ # These rely on some unintuitive cleverness due to WPT's test setup:
+ # 'Upgrade-Insecure-Requests' does not upgrade the port number,
+ # so we use URLs in the form `http://[domain]:[https-port]`,
+ # which will be upgraded to `https://[domain]:[https-port]`.
+ # If the upgrade fails, the load will fail, as we don't serve HTTP over
+ # the secure port.
+ if parsed.scheme == u"https":
+ scheme = u"http"
+ elif parsed.scheme == u"wss":
+ scheme = u"ws"
+ else:
+ raise ValueError(u"Downgrade redirection: Invalid scheme '%s'" %
+ parsed.scheme)
+ hostname = parsed.netloc.split(u':')[0]
+ port = request.server.config[u"ports"][parsed.scheme][0]
+ destination_netloc = u":".join([hostname, str(port)])
+
+ if swap_origin:
+ destination_netloc = __get_swapped_origin_netloc(destination_netloc)
+
+ parsed_query = parse_qsl(parsed.query, keep_blank_values=True)
+ parsed_query = [x for x in parsed_query if x[0] != query_parameter_to_remove]
+
+ destination_url = urlunsplit(SplitResult(
+ scheme = scheme,
+ netloc = destination_netloc,
+ path = parsed.path,
+ query = urlencode(parsed_query),
+ fragment = None))
+
+ return destination_url
+
+
+def preprocess_redirection(request, response):
+ if b"redirection" not in request.GET:
+ return False
+
+ redirection = request.GET[b"redirection"]
+
+ if redirection == b"no-redirect":
+ return False
+ elif redirection == b"keep-scheme":
+ redirect_url = create_url(request, swap_scheme=False)
+ elif redirection == b"swap-scheme":
+ redirect_url = create_url(request, swap_scheme=True)
+ elif redirection == b"downgrade":
+ redirect_url = create_url(request, downgrade=True)
+ elif redirection == b"keep-origin":
+ redirect_url = create_url(request, swap_origin=False)
+ elif redirection == b"swap-origin":
+ redirect_url = create_url(request, swap_origin=True)
+ else:
+ raise ValueError(u"Invalid redirection type '%s'" % isomorphic_decode(redirection))
+
+ redirect(redirect_url, response)
+ return True
+
+
+def preprocess_stash_action(request, response):
+ if b"action" not in request.GET:
+ return False
+
+ action = request.GET[b"action"]
+
+ key = request.GET[b"key"]
+ stash = request.server.stash
+ path = request.GET[b"path"] if b"path" in request.GET \
+ else isomorphic_encode(request.url.split(u'?')[0])
+
+ if action == b"put":
+ value = isomorphic_decode(request.GET[b"value"])
+ stash.take(key=key, path=path)
+ stash.put(key=key, value=value, path=path)
+ response_data = json.dumps({u"status": u"success", u"result": isomorphic_decode(key)})
+ elif action == b"purge":
+ value = stash.take(key=key, path=path)
+ return False
+ elif action == b"take":
+ value = stash.take(key=key, path=path)
+ if value is None:
+ status = u"allowed"
+ else:
+ status = u"blocked"
+ response_data = json.dumps({u"status": status, u"result": value})
+ else:
+ return False
+
+ response.add_required_headers = False
+ response.writer.write_status(200)
+ response.writer.write_header(b"content-type", b"text/javascript")
+ response.writer.write_header(b"cache-control", b"no-cache; must-revalidate")
+ response.writer.end_headers()
+ response.writer.write(response_data)
+ return True
+
+
+def __noop(request, response):
+ return u""
+
+
+def respond(request,
+ response,
+ status_code = 200,
+ content_type = b"text/html",
+ payload_generator = __noop,
+ cache_control = b"no-cache; must-revalidate",
+ access_control_allow_origin = b"*",
+ maybe_additional_headers = None):
+ if preprocess_redirection(request, response):
+ return
+
+ if preprocess_stash_action(request, response):
+ return
+
+ response.add_required_headers = False
+ response.writer.write_status(status_code)
+
+ if access_control_allow_origin != None:
+ response.writer.write_header(b"access-control-allow-origin",
+ access_control_allow_origin)
+ response.writer.write_header(b"content-type", content_type)
+ response.writer.write_header(b"cache-control", cache_control)
+
+ additional_headers = maybe_additional_headers or {}
+ for header, value in additional_headers.items():
+ response.writer.write_header(header, value)
+
+ response.writer.end_headers()
+
+ new_headers = {}
+ new_val = []
+ for key, val in request.headers.items():
+ if len(val) == 1:
+ new_val = isomorphic_decode(val[0])
+ else:
+ new_val = [isomorphic_decode(x) for x in val]
+ new_headers[isomorphic_decode(key)] = new_val
+
+ server_data = {u"headers": json.dumps(new_headers, indent = 4)}
+
+ payload = payload_generator(server_data)
+ response.writer.write(payload)
diff --git a/test/wpt/tests/common/security-features/subresource/svg.py b/test/wpt/tests/common/security-features/subresource/svg.py
new file mode 100644
index 0000000..9c569e3
--- /dev/null
+++ b/test/wpt/tests/common/security-features/subresource/svg.py
@@ -0,0 +1,37 @@
+import os, sys
+from wptserve.utils import isomorphic_decode
+import importlib
+subresource = importlib.import_module("common.security-features.subresource.subresource")
+
+def generate_payload(request, server_data):
+ data = (u'{"headers": %(headers)s}') % server_data
+ if b"id" in request.GET:
+ with request.server.stash.lock:
+ request.server.stash.take(request.GET[b"id"])
+ request.server.stash.put(request.GET[b"id"], data)
+ return u"<svg xmlns='http://www.w3.org/2000/svg'></svg>"
+
+def generate_payload_embedded(request, server_data):
+ return subresource.get_template(u"svg.embedded.template") % {
+ u"id": isomorphic_decode(request.GET[b"id"]),
+ u"property": isomorphic_decode(request.GET[b"property"])}
+
+def generate_report_headers_payload(request, server_data):
+ stashed_data = request.server.stash.take(request.GET[b"id"])
+ return stashed_data
+
+def main(request, response):
+ handler = lambda data: generate_payload(request, data)
+ content_type = b'image/svg+xml'
+
+ if b"embedded-svg" in request.GET:
+ handler = lambda data: generate_payload_embedded(request, data)
+
+ if b"report-headers" in request.GET:
+ handler = lambda data: generate_report_headers_payload(request, data)
+ content_type = b'application/json'
+
+ subresource.respond(request,
+ response,
+ payload_generator = handler,
+ content_type = content_type)
diff --git a/test/wpt/tests/common/security-features/subresource/template/document.html.template b/test/wpt/tests/common/security-features/subresource/template/document.html.template
new file mode 100644
index 0000000..141711c
--- /dev/null
+++ b/test/wpt/tests/common/security-features/subresource/template/document.html.template
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>This page reports back it's request details to the parent frame</title>
+ </head>
+ <body>
+ <script>
+ var result = {
+ location: document.location.toString(),
+ referrer: document.referrer.length > 0 ? document.referrer : undefined,
+ headers: %(headers)s
+ };
+ parent.postMessage(result, "*");
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/common/security-features/subresource/template/font.css.template b/test/wpt/tests/common/security-features/subresource/template/font.css.template
new file mode 100644
index 0000000..9d1e9c4
--- /dev/null
+++ b/test/wpt/tests/common/security-features/subresource/template/font.css.template
@@ -0,0 +1,9 @@
+@font-face {
+ font-family: 'wpt';
+ font-style: normal;
+ font-weight: normal;
+ src: url(/common/security-features/subresource/font.py?id=%(id)s) format('truetype');
+}
+body {
+ font-family: 'wpt';
+}
diff --git a/test/wpt/tests/common/security-features/subresource/template/image.css.template b/test/wpt/tests/common/security-features/subresource/template/image.css.template
new file mode 100644
index 0000000..dfe41f1
--- /dev/null
+++ b/test/wpt/tests/common/security-features/subresource/template/image.css.template
@@ -0,0 +1,3 @@
+div.styled::before {
+ content:url(/common/security-features/subresource/image.py?id=%(id)s)
+}
diff --git a/test/wpt/tests/common/security-features/subresource/template/script.js.template b/test/wpt/tests/common/security-features/subresource/template/script.js.template
new file mode 100644
index 0000000..e2edf21
--- /dev/null
+++ b/test/wpt/tests/common/security-features/subresource/template/script.js.template
@@ -0,0 +1,3 @@
+postMessage({
+ "headers": %(headers)s
+}, "*");
diff --git a/test/wpt/tests/common/security-features/subresource/template/shared-worker.js.template b/test/wpt/tests/common/security-features/subresource/template/shared-worker.js.template
new file mode 100644
index 0000000..c3f109e
--- /dev/null
+++ b/test/wpt/tests/common/security-features/subresource/template/shared-worker.js.template
@@ -0,0 +1,5 @@
+onconnect = function(e) {
+ e.ports[0].postMessage({
+ "headers": %(headers)s
+ });
+};
diff --git a/test/wpt/tests/common/security-features/subresource/template/static-import.js.template b/test/wpt/tests/common/security-features/subresource/template/static-import.js.template
new file mode 100644
index 0000000..095459b
--- /dev/null
+++ b/test/wpt/tests/common/security-features/subresource/template/static-import.js.template
@@ -0,0 +1 @@
+import '%(import_url)s';
diff --git a/test/wpt/tests/common/security-features/subresource/template/svg.css.template b/test/wpt/tests/common/security-features/subresource/template/svg.css.template
new file mode 100644
index 0000000..c2e509c
--- /dev/null
+++ b/test/wpt/tests/common/security-features/subresource/template/svg.css.template
@@ -0,0 +1,3 @@
+path {
+ %(property)s: url(/common/security-features/subresource/svg.py?id=%(id)s#invalidFragment);
+}
diff --git a/test/wpt/tests/common/security-features/subresource/template/svg.embedded.template b/test/wpt/tests/common/security-features/subresource/template/svg.embedded.template
new file mode 100644
index 0000000..5986c48
--- /dev/null
+++ b/test/wpt/tests/common/security-features/subresource/template/svg.embedded.template
@@ -0,0 +1,5 @@
+<?xml version='1.0' standalone='no'?>
+<?xml-stylesheet href='stylesheet.py?id=%(id)s&amp;type=svg&amp;property=%(property)s' type='text/css'?>
+<svg xmlns='http://www.w3.org/2000/svg'>
+ <path d='M 50,5 95,100 5,100 z' />
+</svg>
diff --git a/test/wpt/tests/common/security-features/subresource/template/worker.js.template b/test/wpt/tests/common/security-features/subresource/template/worker.js.template
new file mode 100644
index 0000000..817dd8c
--- /dev/null
+++ b/test/wpt/tests/common/security-features/subresource/template/worker.js.template
@@ -0,0 +1,3 @@
+postMessage({
+ "headers": %(headers)s
+});
diff --git a/test/wpt/tests/common/security-features/subresource/video.py b/test/wpt/tests/common/security-features/subresource/video.py
new file mode 100644
index 0000000..7cfbbfa
--- /dev/null
+++ b/test/wpt/tests/common/security-features/subresource/video.py
@@ -0,0 +1,17 @@
+import os, sys
+from wptserve.utils import isomorphic_decode
+import importlib
+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")
+ return open(file, "rb").read()
+
+
+def main(request, response):
+ handler = lambda data: generate_payload(request, data)
+ subresource.respond(request,
+ response,
+ payload_generator = handler,
+ access_control_allow_origin = b"*",
+ content_type = b"video/ogg")
diff --git a/test/wpt/tests/common/security-features/subresource/worker.py b/test/wpt/tests/common/security-features/subresource/worker.py
new file mode 100644
index 0000000..f655633
--- /dev/null
+++ b/test/wpt/tests/common/security-features/subresource/worker.py
@@ -0,0 +1,13 @@
+import os, sys
+from wptserve.utils import isomorphic_decode
+import importlib
+subresource = importlib.import_module("common.security-features.subresource.subresource")
+
+def generate_payload(server_data):
+ return subresource.get_template(u"worker.js.template") % server_data
+
+def main(request, response):
+ subresource.respond(request,
+ response,
+ payload_generator = generate_payload,
+ content_type = b"application/javascript")
diff --git a/test/wpt/tests/common/security-features/subresource/xhr.py b/test/wpt/tests/common/security-features/subresource/xhr.py
new file mode 100644
index 0000000..75921e9
--- /dev/null
+++ b/test/wpt/tests/common/security-features/subresource/xhr.py
@@ -0,0 +1,16 @@
+import os, sys
+from wptserve.utils import isomorphic_decode
+import importlib
+subresource = importlib.import_module("common.security-features.subresource.subresource")
+
+def generate_payload(server_data):
+ data = (u'{"headers": %(headers)s}') % server_data
+ return data
+
+def main(request, response):
+ subresource.respond(request,
+ response,
+ payload_generator = generate_payload,
+ access_control_allow_origin = b"*",
+ content_type = b"application/json",
+ cache_control = b"no-store")
diff --git a/test/wpt/tests/common/security-features/tools/format_spec_src_json.py b/test/wpt/tests/common/security-features/tools/format_spec_src_json.py
new file mode 100644
index 0000000..d1bf581
--- /dev/null
+++ b/test/wpt/tests/common/security-features/tools/format_spec_src_json.py
@@ -0,0 +1,24 @@
+import collections
+import json
+import os
+
+
+def main():
+ '''Formats spec.src.json.'''
+ script_directory = os.path.dirname(os.path.abspath(__file__))
+ for dir in [
+ 'mixed-content', 'referrer-policy', 'referrer-policy/4K-1',
+ 'referrer-policy/4K', 'referrer-policy/4K+1',
+ 'upgrade-insecure-requests'
+ ]:
+ filename = os.path.join(script_directory, '..', '..', '..', dir,
+ 'spec.src.json')
+ spec = json.load(
+ open(filename, 'r'), object_pairs_hook=collections.OrderedDict)
+ with open(filename, 'w') as f:
+ f.write(json.dumps(spec, indent=2, separators=(',', ': ')))
+ f.write('\n')
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/wpt/tests/common/security-features/tools/generate.py b/test/wpt/tests/common/security-features/tools/generate.py
new file mode 100644
index 0000000..409b4f1
--- /dev/null
+++ b/test/wpt/tests/common/security-features/tools/generate.py
@@ -0,0 +1,462 @@
+#!/usr/bin/env python3
+
+import argparse
+import collections
+import copy
+import json
+import os
+import sys
+
+import spec_validator
+import util
+
+
+def expand_pattern(expansion_pattern, test_expansion_schema):
+ expansion = {}
+ for artifact_key in expansion_pattern:
+ artifact_value = expansion_pattern[artifact_key]
+ if artifact_value == '*':
+ expansion[artifact_key] = test_expansion_schema[artifact_key]
+ elif isinstance(artifact_value, list):
+ expansion[artifact_key] = artifact_value
+ elif isinstance(artifact_value, dict):
+ # Flattened expansion.
+ expansion[artifact_key] = []
+ values_dict = expand_pattern(artifact_value,
+ test_expansion_schema[artifact_key])
+ for sub_key in values_dict.keys():
+ expansion[artifact_key] += values_dict[sub_key]
+ else:
+ expansion[artifact_key] = [artifact_value]
+
+ return expansion
+
+
+def permute_expansion(expansion,
+ artifact_order,
+ selection={},
+ artifact_index=0):
+ assert isinstance(artifact_order, list), "artifact_order should be a list"
+
+ if artifact_index >= len(artifact_order):
+ yield selection
+ return
+
+ artifact_key = artifact_order[artifact_index]
+
+ for artifact_value in expansion[artifact_key]:
+ selection[artifact_key] = artifact_value
+ for next_selection in permute_expansion(expansion, artifact_order,
+ selection, artifact_index + 1):
+ yield next_selection
+
+
+# Dumps the test config `selection` into a serialized JSON string.
+def dump_test_parameters(selection):
+ return json.dumps(
+ selection,
+ indent=2,
+ separators=(',', ': '),
+ sort_keys=True,
+ cls=util.CustomEncoder)
+
+
+def get_test_filename(spec_directory, spec_json, selection):
+ '''Returns the filname for the main test HTML file'''
+
+ selection_for_filename = copy.deepcopy(selection)
+ # Use 'unset' rather than 'None' in test filenames.
+ if selection_for_filename['delivery_value'] is None:
+ selection_for_filename['delivery_value'] = 'unset'
+
+ return os.path.join(
+ spec_directory,
+ spec_json['test_file_path_pattern'] % selection_for_filename)
+
+
+def get_csp_value(value):
+ '''
+ Returns actual CSP header values (e.g. "worker-src 'self'") for the
+ given string used in PolicyDelivery's value (e.g. "worker-src-self").
+ '''
+
+ # script-src
+ # Test-related scripts like testharness.js and inline scripts containing
+ # test bodies.
+ # 'unsafe-inline' is added as a workaround here. This is probably not so
+ # bad, as it shouldn't intefere non-inline-script requests that we want to
+ # test.
+ if value == 'script-src-wildcard':
+ return "script-src * 'unsafe-inline'"
+ if value == 'script-src-self':
+ return "script-src 'self' 'unsafe-inline'"
+ # Workaround for "script-src 'none'" would be more complicated, because
+ # - "script-src 'none' 'unsafe-inline'" is handled somehow differently from
+ # "script-src 'none'", i.e.
+ # https://w3c.github.io/webappsec-csp/#match-url-to-source-list Step 3
+ # handles the latter but not the former.
+ # - We need nonce- or path-based additional values to allow same-origin
+ # test scripts like testharness.js.
+ # Therefore, we disable 'script-src-none' tests for now in
+ # `/content-security-policy/spec.src.json`.
+ if value == 'script-src-none':
+ return "script-src 'none'"
+
+ # worker-src
+ if value == 'worker-src-wildcard':
+ return 'worker-src *'
+ if value == 'worker-src-self':
+ return "worker-src 'self'"
+ if value == 'worker-src-none':
+ return "worker-src 'none'"
+ raise Exception('Invalid delivery_value: %s' % value)
+
+def handle_deliveries(policy_deliveries):
+ '''
+ Generate <meta> elements and HTTP headers for the given list of
+ PolicyDelivery.
+ TODO(hiroshige): Merge duplicated code here, scope/document.py, etc.
+ '''
+
+ meta = ''
+ headers = {}
+
+ for delivery in policy_deliveries:
+ if delivery.value is None:
+ continue
+ if delivery.key == 'referrerPolicy':
+ if delivery.delivery_type == 'meta':
+ meta += \
+ '<meta name="referrer" content="%s">' % delivery.value
+ elif delivery.delivery_type == 'http-rp':
+ headers['Referrer-Policy'] = delivery.value
+ # TODO(kristijanburnik): Limit to WPT origins.
+ headers['Access-Control-Allow-Origin'] = '*'
+ else:
+ raise Exception(
+ 'Invalid delivery_type: %s' % delivery.delivery_type)
+ elif delivery.key == 'mixedContent':
+ assert (delivery.value == 'opt-in')
+ if delivery.delivery_type == 'meta':
+ meta += '<meta http-equiv="Content-Security-Policy" ' + \
+ 'content="block-all-mixed-content">'
+ elif delivery.delivery_type == 'http-rp':
+ headers['Content-Security-Policy'] = 'block-all-mixed-content'
+ else:
+ raise Exception(
+ 'Invalid delivery_type: %s' % delivery.delivery_type)
+ elif delivery.key == 'contentSecurityPolicy':
+ csp_value = get_csp_value(delivery.value)
+ if delivery.delivery_type == 'meta':
+ meta += '<meta http-equiv="Content-Security-Policy" ' + \
+ 'content="' + csp_value + '">'
+ elif delivery.delivery_type == 'http-rp':
+ headers['Content-Security-Policy'] = csp_value
+ else:
+ raise Exception(
+ 'Invalid delivery_type: %s' % delivery.delivery_type)
+ elif delivery.key == 'upgradeInsecureRequests':
+ # https://w3c.github.io/webappsec-upgrade-insecure-requests/#delivery
+ assert (delivery.value == 'upgrade')
+ if delivery.delivery_type == 'meta':
+ meta += '<meta http-equiv="Content-Security-Policy" ' + \
+ 'content="upgrade-insecure-requests">'
+ elif delivery.delivery_type == 'http-rp':
+ headers[
+ 'Content-Security-Policy'] = 'upgrade-insecure-requests'
+ else:
+ raise Exception(
+ 'Invalid delivery_type: %s' % delivery.delivery_type)
+ else:
+ raise Exception('Invalid delivery_key: %s' % delivery.key)
+ return {"meta": meta, "headers": headers}
+
+
+def generate_selection(spec_json, selection):
+ '''
+ Returns a scenario object (with a top-level source_context_list entry,
+ which will be removed in generate_test_file() later).
+ '''
+
+ target_policy_delivery = util.PolicyDelivery(selection['delivery_type'],
+ selection['delivery_key'],
+ selection['delivery_value'])
+ del selection['delivery_type']
+ del selection['delivery_key']
+ del selection['delivery_value']
+
+ # Parse source context list and policy deliveries of source contexts.
+ # `util.ShouldSkip()` exceptions are raised if e.g. unsuppported
+ # combinations of source contexts and policy deliveries are used.
+ source_context_list_scheme = spec_json['source_context_list_schema'][
+ selection['source_context_list']]
+ selection['source_context_list'] = [
+ util.SourceContext.from_json(source_context, target_policy_delivery,
+ spec_json['source_context_schema'])
+ for source_context in source_context_list_scheme['sourceContextList']
+ ]
+
+ # Check if the subresource is supported by the innermost source context.
+ innermost_source_context = selection['source_context_list'][-1]
+ supported_subresource = spec_json['source_context_schema'][
+ 'supported_subresource'][innermost_source_context.source_context_type]
+ if supported_subresource != '*':
+ if selection['subresource'] not in supported_subresource:
+ raise util.ShouldSkip()
+
+ # Parse subresource policy deliveries.
+ selection[
+ 'subresource_policy_deliveries'] = util.PolicyDelivery.list_from_json(
+ source_context_list_scheme['subresourcePolicyDeliveries'],
+ target_policy_delivery, spec_json['subresource_schema']
+ ['supported_delivery_type'][selection['subresource']])
+
+ # Generate per-scenario test description.
+ selection['test_description'] = spec_json[
+ 'test_description_template'] % selection
+
+ return selection
+
+
+def generate_test_file(spec_directory, test_helper_filenames,
+ test_html_template_basename, test_filename, scenarios):
+ '''
+ Generates a test HTML file (and possibly its associated .headers file)
+ from `scenarios`.
+ '''
+
+ # Scenarios for the same file should have the same `source_context_list`,
+ # including the top-level one.
+ # Note: currently, non-top-level source contexts aren't necessarily required
+ # to be the same, but we set this requirement as it will be useful e.g. when
+ # we e.g. reuse a worker among multiple scenarios.
+ for scenario in scenarios:
+ assert (scenario['source_context_list'] == scenarios[0]
+ ['source_context_list'])
+
+ # We process the top source context below, and do not include it in
+ # the JSON objects (i.e. `scenarios`) in generated HTML files.
+ top_source_context = scenarios[0]['source_context_list'].pop(0)
+ assert (top_source_context.source_context_type == 'top')
+ for scenario in scenarios[1:]:
+ assert (scenario['source_context_list'].pop(0) == top_source_context)
+
+ parameters = {}
+
+ # Sort scenarios, to avoid unnecessary diffs due to different orders in
+ # `scenarios`.
+ serialized_scenarios = sorted(
+ [dump_test_parameters(scenario) for scenario in scenarios])
+
+ parameters['scenarios'] = ",\n".join(serialized_scenarios).replace(
+ "\n", "\n" + " " * 10)
+
+ test_directory = os.path.dirname(test_filename)
+
+ parameters['helper_js'] = ""
+ for test_helper_filename in test_helper_filenames:
+ parameters['helper_js'] += ' <script src="%s"></script>\n' % (
+ os.path.relpath(test_helper_filename, test_directory))
+ parameters['sanity_checker_js'] = os.path.relpath(
+ os.path.join(spec_directory, 'generic', 'sanity-checker.js'),
+ test_directory)
+ parameters['spec_json_js'] = os.path.relpath(
+ os.path.join(spec_directory, 'generic', 'spec_json.js'),
+ test_directory)
+
+ test_headers_filename = test_filename + ".headers"
+
+ test_html_template = util.get_template(test_html_template_basename)
+ disclaimer_template = util.get_template('disclaimer.template')
+
+ html_template_filename = os.path.join(util.template_directory,
+ test_html_template_basename)
+ generated_disclaimer = disclaimer_template \
+ % {'generating_script_filename': os.path.relpath(sys.argv[0],
+ util.test_root_directory),
+ 'spec_directory': os.path.relpath(spec_directory,
+ util.test_root_directory)}
+
+ # Adjust the template for the test invoking JS. Indent it to look nice.
+ parameters['generated_disclaimer'] = generated_disclaimer.rstrip()
+
+ # Directory for the test files.
+ try:
+ os.makedirs(test_directory)
+ except:
+ pass
+
+ delivery = handle_deliveries(top_source_context.policy_deliveries)
+
+ if len(delivery['headers']) > 0:
+ with open(test_headers_filename, "w") as f:
+ for header in delivery['headers']:
+ f.write('%s: %s\n' % (header, delivery['headers'][header]))
+
+ parameters['meta_delivery_method'] = delivery['meta']
+ # Obey the lint and pretty format.
+ if len(parameters['meta_delivery_method']) > 0:
+ parameters['meta_delivery_method'] = "\n " + \
+ parameters['meta_delivery_method']
+
+ # Write out the generated HTML file.
+ util.write_file(test_filename, test_html_template % parameters)
+
+
+def generate_test_source_files(spec_directory, test_helper_filenames,
+ spec_json, target):
+ test_expansion_schema = spec_json['test_expansion_schema']
+ specification = spec_json['specification']
+
+ if target == "debug":
+ spec_json_js_template = util.get_template('spec_json.js.template')
+ util.write_file(
+ os.path.join(spec_directory, "generic", "spec_json.js"),
+ spec_json_js_template % {'spec_json': json.dumps(spec_json)})
+ util.write_file(
+ os.path.join(spec_directory, "generic",
+ "debug-output.spec.src.json"),
+ json.dumps(spec_json, indent=2, separators=(',', ': ')))
+
+ # Choose a debug/release template depending on the target.
+ html_template = "test.%s.html.template" % target
+
+ artifact_order = test_expansion_schema.keys()
+ artifact_order.remove('expansion')
+
+ excluded_selection_pattern = ''
+ for key in artifact_order:
+ excluded_selection_pattern += '%(' + key + ')s/'
+
+ # Create list of excluded tests.
+ exclusion_dict = set()
+ for excluded_pattern in spec_json['excluded_tests']:
+ excluded_expansion = \
+ expand_pattern(excluded_pattern, test_expansion_schema)
+ for excluded_selection in permute_expansion(excluded_expansion,
+ artifact_order):
+ excluded_selection['delivery_key'] = spec_json['delivery_key']
+ exclusion_dict.add(excluded_selection_pattern % excluded_selection)
+
+ # `scenarios[filename]` represents the list of scenario objects to be
+ # generated into `filename`.
+ scenarios = {}
+
+ for spec in specification:
+ # Used to make entries with expansion="override" override preceding
+ # entries with the same |selection_path|.
+ output_dict = {}
+
+ for expansion_pattern in spec['test_expansion']:
+ expansion = expand_pattern(expansion_pattern,
+ test_expansion_schema)
+ for selection in permute_expansion(expansion, artifact_order):
+ selection['delivery_key'] = spec_json['delivery_key']
+ selection_path = spec_json['selection_pattern'] % selection
+ if selection_path in output_dict:
+ if expansion_pattern['expansion'] != 'override':
+ print("Error: expansion is default in:")
+ print(dump_test_parameters(selection))
+ print("but overrides:")
+ print(dump_test_parameters(
+ output_dict[selection_path]))
+ sys.exit(1)
+ output_dict[selection_path] = copy.deepcopy(selection)
+
+ for selection_path in output_dict:
+ selection = output_dict[selection_path]
+ if (excluded_selection_pattern % selection) in exclusion_dict:
+ print('Excluding selection:', selection_path)
+ continue
+ try:
+ test_filename = get_test_filename(spec_directory, spec_json,
+ selection)
+ scenario = generate_selection(spec_json, selection)
+ scenarios[test_filename] = scenarios.get(test_filename,
+ []) + [scenario]
+ except util.ShouldSkip:
+ continue
+
+ for filename in scenarios:
+ generate_test_file(spec_directory, test_helper_filenames,
+ html_template, filename, scenarios[filename])
+
+
+def merge_json(base, child):
+ for key in child:
+ if key not in base:
+ base[key] = child[key]
+ continue
+ # `base[key]` and `child[key]` both exists.
+ if isinstance(base[key], list) and isinstance(child[key], list):
+ base[key].extend(child[key])
+ elif isinstance(base[key], dict) and isinstance(child[key], dict):
+ merge_json(base[key], child[key])
+ else:
+ base[key] = child[key]
+
+
+def main():
+ parser = argparse.ArgumentParser(
+ description='Test suite generator utility')
+ parser.add_argument(
+ '-t',
+ '--target',
+ type=str,
+ choices=("release", "debug"),
+ default="release",
+ help='Sets the appropriate template for generating tests')
+ parser.add_argument(
+ '-s',
+ '--spec',
+ type=str,
+ default=os.getcwd(),
+ help='Specify a file used for describing and generating the tests')
+ # TODO(kristijanburnik): Add option for the spec_json file.
+ args = parser.parse_args()
+
+ spec_directory = os.path.abspath(args.spec)
+
+ # Read `spec.src.json` files, starting from `spec_directory`, and
+ # continuing to parent directories as long as `spec.src.json` exists.
+ spec_filenames = []
+ test_helper_filenames = []
+ spec_src_directory = spec_directory
+ while len(spec_src_directory) >= len(util.test_root_directory):
+ spec_filename = os.path.join(spec_src_directory, "spec.src.json")
+ if not os.path.exists(spec_filename):
+ break
+ spec_filenames.append(spec_filename)
+ test_filename = os.path.join(spec_src_directory, 'generic',
+ 'test-case.sub.js')
+ assert (os.path.exists(test_filename))
+ test_helper_filenames.append(test_filename)
+ spec_src_directory = os.path.abspath(
+ os.path.join(spec_src_directory, ".."))
+
+ spec_filenames = list(reversed(spec_filenames))
+ test_helper_filenames = list(reversed(test_helper_filenames))
+
+ if len(spec_filenames) == 0:
+ print('Error: No spec.src.json is found at %s.' % spec_directory)
+ return
+
+ # Load the default spec JSON file, ...
+ default_spec_filename = os.path.join(util.script_directory,
+ 'spec.src.json')
+ spec_json = collections.OrderedDict()
+ if os.path.exists(default_spec_filename):
+ spec_json = util.load_spec_json(default_spec_filename)
+
+ # ... and then make spec JSON files in subdirectories override the default.
+ for spec_filename in spec_filenames:
+ child_spec_json = util.load_spec_json(spec_filename)
+ merge_json(spec_json, child_spec_json)
+
+ spec_validator.assert_valid_spec_json(spec_json)
+ generate_test_source_files(spec_directory, test_helper_filenames,
+ spec_json, args.target)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/wpt/tests/common/security-features/tools/spec.src.json b/test/wpt/tests/common/security-features/tools/spec.src.json
new file mode 100644
index 0000000..4a84493
--- /dev/null
+++ b/test/wpt/tests/common/security-features/tools/spec.src.json
@@ -0,0 +1,533 @@
+{
+ "selection_pattern": "%(source_context_list)s.%(delivery_type)s/%(delivery_value)s/%(subresource)s/%(origin)s.%(redirection)s.%(source_scheme)s",
+ "test_file_path_pattern": "gen/%(source_context_list)s.%(delivery_type)s/%(delivery_value)s/%(subresource)s.%(source_scheme)s.html",
+ "excluded_tests": [
+ {
+ // Workers are same-origin only
+ "expansion": "*",
+ "source_scheme": "*",
+ "source_context_list": "*",
+ "delivery_type": "*",
+ "delivery_value": "*",
+ "redirection": "*",
+ "subresource": [
+ "worker-classic",
+ "worker-module",
+ "sharedworker-classic",
+ "sharedworker-module"
+ ],
+ "origin": [
+ "cross-https",
+ "cross-http",
+ "cross-http-downgrade",
+ "cross-wss",
+ "cross-ws",
+ "cross-ws-downgrade"
+ ],
+ "expectation": "*"
+ },
+ {
+ // Workers are same-origin only (redirects)
+ "expansion": "*",
+ "source_scheme": "*",
+ "source_context_list": "*",
+ "delivery_type": "*",
+ "delivery_value": "*",
+ "redirection": [
+ "swap-origin",
+ "swap-scheme"
+ ],
+ "subresource": [
+ "worker-classic",
+ "worker-module",
+ "sharedworker-classic",
+ "sharedworker-module"
+ ],
+ "origin": "*",
+ "expectation": "*"
+ },
+ {
+ // Websockets are ws/wss-only
+ "expansion": "*",
+ "source_scheme": "*",
+ "source_context_list": "*",
+ "delivery_type": "*",
+ "delivery_value": "*",
+ "redirection": "*",
+ "subresource": "websocket",
+ "origin": [
+ "same-https",
+ "same-http",
+ "same-http-downgrade",
+ "cross-https",
+ "cross-http",
+ "cross-http-downgrade"
+ ],
+ "expectation": "*"
+ },
+ {
+ // Redirects are intentionally forbidden in browsers:
+ // https://fetch.spec.whatwg.org/#concept-websocket-establish
+ // Websockets are no-redirect only
+ "expansion": "*",
+ "source_scheme": "*",
+ "source_context_list": "*",
+ "delivery_type": "*",
+ "delivery_value": "*",
+ "redirection": [
+ "keep-origin",
+ "swap-origin",
+ "keep-scheme",
+ "swap-scheme",
+ "downgrade"
+ ],
+ "subresource": "websocket",
+ "origin": "*",
+ "expectation": "*"
+ },
+ {
+ // ws/wss are websocket-only
+ "expansion": "*",
+ "source_scheme": "*",
+ "source_context_list": "*",
+ "delivery_type": "*",
+ "delivery_value": "*",
+ "redirection": "*",
+ "subresource": [
+ "a-tag",
+ "area-tag",
+ "audio-tag",
+ "beacon",
+ "fetch",
+ "iframe-tag",
+ "img-tag",
+ "link-css-tag",
+ "link-prefetch-tag",
+ "object-tag",
+ "picture-tag",
+ "script-tag",
+ "script-tag-dynamic-import",
+ "sharedworker-classic",
+ "sharedworker-import",
+ "sharedworker-import-data",
+ "sharedworker-module",
+ "video-tag",
+ "worker-classic",
+ "worker-import",
+ "worker-import-data",
+ "worker-module",
+ "worklet-animation",
+ "worklet-animation-import-data",
+ "worklet-audio",
+ "worklet-audio-import-data",
+ "worklet-layout",
+ "worklet-layout-import-data",
+ "worklet-paint",
+ "worklet-paint-import-data",
+ "xhr"
+ ],
+ "origin": [
+ "same-wss",
+ "same-ws",
+ "same-ws-downgrade",
+ "cross-wss",
+ "cross-ws",
+ "cross-ws-downgrade"
+ ],
+ "expectation": "*"
+ },
+ {
+ // Worklets are HTTPS contexts only
+ "expansion": "*",
+ "source_scheme": "http",
+ "source_context_list": "*",
+ "delivery_type": "*",
+ "delivery_value": "*",
+ "redirection": "*",
+ "subresource": [
+ "worklet-animation",
+ "worklet-animation-import-data",
+ "worklet-audio",
+ "worklet-audio-import-data",
+ "worklet-layout",
+ "worklet-layout-import-data",
+ "worklet-paint",
+ "worklet-paint-import-data"
+ ],
+ "origin": "*",
+ "expectation": "*"
+ }
+ ],
+ "source_context_schema": {
+ "supported_subresource": {
+ "top": "*",
+ "iframe": "*",
+ "iframe-blank": "*",
+ "srcdoc": "*",
+ "worker-classic": [
+ "xhr",
+ "fetch",
+ "websocket",
+ "worker-classic",
+ "worker-module"
+ ],
+ "worker-module": [
+ "xhr",
+ "fetch",
+ "websocket",
+ "worker-classic",
+ "worker-module"
+ ],
+ "worker-classic-data": [
+ "xhr",
+ "fetch",
+ "websocket"
+ ],
+ "worker-module-data": [
+ "xhr",
+ "fetch",
+ "websocket"
+ ],
+ "sharedworker-classic": [
+ "xhr",
+ "fetch",
+ "websocket"
+ ],
+ "sharedworker-module": [
+ "xhr",
+ "fetch",
+ "websocket"
+ ],
+ "sharedworker-classic-data": [
+ "xhr",
+ "fetch",
+ "websocket"
+ ],
+ "sharedworker-module-data": [
+ "xhr",
+ "fetch",
+ "websocket"
+ ]
+ }
+ },
+ "source_context_list_schema": {
+ // Warning: Currently, some nested patterns of contexts have different
+ // inheritance rules for different kinds of policies.
+ // The generated tests will be used to test/investigate the policy
+ // inheritance rules, and eventually the policy inheritance rules will
+ // be unified (https://github.com/w3ctag/design-principles/issues/111).
+ "top": {
+ "description": "Policy set by the top-level Document",
+ "sourceContextList": [
+ {
+ "sourceContextType": "top",
+ "policyDeliveries": [
+ "policy"
+ ]
+ }
+ ],
+ "subresourcePolicyDeliveries": []
+ },
+ "req": {
+ "description": "Subresource request's policy should override Document's policy",
+ "sourceContextList": [
+ {
+ "sourceContextType": "top",
+ "policyDeliveries": [
+ "anotherPolicy"
+ ]
+ }
+ ],
+ "subresourcePolicyDeliveries": [
+ "nonNullPolicy"
+ ]
+ },
+ "srcdoc-inherit": {
+ "description": "srcdoc iframe without its own policy should inherit parent Document's policy",
+ "sourceContextList": [
+ {
+ "sourceContextType": "top",
+ "policyDeliveries": [
+ "policy"
+ ]
+ },
+ {
+ "sourceContextType": "srcdoc"
+ }
+ ],
+ "subresourcePolicyDeliveries": []
+ },
+ "srcdoc": {
+ "description": "srcdoc iframe's policy should override parent Document's policy",
+ "sourceContextList": [
+ {
+ "sourceContextType": "top",
+ "policyDeliveries": [
+ "anotherPolicy"
+ ]
+ },
+ {
+ "sourceContextType": "srcdoc",
+ "policyDeliveries": [
+ "nonNullPolicy"
+ ]
+ }
+ ],
+ "subresourcePolicyDeliveries": []
+ },
+ "iframe": {
+ "description": "external iframe's policy should override parent Document's policy",
+ "sourceContextList": [
+ {
+ "sourceContextType": "top",
+ "policyDeliveries": [
+ "anotherPolicy"
+ ]
+ },
+ {
+ "sourceContextType": "iframe",
+ "policyDeliveries": [
+ "policy"
+ ]
+ }
+ ],
+ "subresourcePolicyDeliveries": []
+ },
+ "iframe-blank-inherit": {
+ "description": "blank iframe should inherit parent Document's policy",
+ "sourceContextList": [
+ {
+ "sourceContextType": "top",
+ "policyDeliveries": [
+ "policy"
+ ]
+ },
+ {
+ "sourceContextType": "iframe-blank"
+ }
+ ],
+ "subresourcePolicyDeliveries": []
+ },
+ "worker-classic": {
+ // This is applicable to referrer-policy tests.
+ // Use "worker-classic-inherit" for CSP (mixed-content, etc.).
+ "description": "dedicated workers shouldn't inherit its parent's policy.",
+ "sourceContextList": [
+ {
+ "sourceContextType": "top",
+ "policyDeliveries": [
+ "anotherPolicy"
+ ]
+ },
+ {
+ "sourceContextType": "worker-classic",
+ "policyDeliveries": [
+ "policy"
+ ]
+ }
+ ],
+ "subresourcePolicyDeliveries": []
+ },
+ "worker-classic-data": {
+ "description": "data: dedicated workers should inherit its parent's policy.",
+ "sourceContextList": [
+ {
+ "sourceContextType": "top",
+ "policyDeliveries": [
+ "policy"
+ ]
+ },
+ {
+ "sourceContextType": "worker-classic-data",
+ "policyDeliveries": []
+ }
+ ],
+ "subresourcePolicyDeliveries": []
+ },
+ "worker-module": {
+ // This is applicable to referrer-policy tests.
+ "description": "dedicated workers shouldn't inherit its parent's policy.",
+ "sourceContextList": [
+ {
+ "sourceContextType": "top",
+ "policyDeliveries": [
+ "anotherPolicy"
+ ]
+ },
+ {
+ "sourceContextType": "worker-module",
+ "policyDeliveries": [
+ "policy"
+ ]
+ }
+ ],
+ "subresourcePolicyDeliveries": []
+ },
+ "worker-module-data": {
+ "description": "data: dedicated workers should inherit its parent's policy.",
+ "sourceContextList": [
+ {
+ "sourceContextType": "top",
+ "policyDeliveries": [
+ "policy"
+ ]
+ },
+ {
+ "sourceContextType": "worker-module-data",
+ "policyDeliveries": []
+ }
+ ],
+ "subresourcePolicyDeliveries": []
+ },
+ "sharedworker-classic": {
+ "description": "shared workers shouldn't inherit its parent's policy.",
+ "sourceContextList": [
+ {
+ "sourceContextType": "top",
+ "policyDeliveries": [
+ "anotherPolicy"
+ ]
+ },
+ {
+ "sourceContextType": "sharedworker-classic",
+ "policyDeliveries": [
+ "policy"
+ ]
+ }
+ ],
+ "subresourcePolicyDeliveries": []
+ },
+ "sharedworker-classic-data": {
+ "description": "data: shared workers should inherit its parent's policy.",
+ "sourceContextList": [
+ {
+ "sourceContextType": "top",
+ "policyDeliveries": [
+ "policy"
+ ]
+ },
+ {
+ "sourceContextType": "sharedworker-classic-data",
+ "policyDeliveries": []
+ }
+ ],
+ "subresourcePolicyDeliveries": []
+ },
+ "sharedworker-module": {
+ "description": "shared workers shouldn't inherit its parent's policy.",
+ "sourceContextList": [
+ {
+ "sourceContextType": "top",
+ "policyDeliveries": [
+ "anotherPolicy"
+ ]
+ },
+ {
+ "sourceContextType": "sharedworker-module",
+ "policyDeliveries": [
+ "policy"
+ ]
+ }
+ ],
+ "subresourcePolicyDeliveries": []
+ },
+ "sharedworker-module-data": {
+ "description": "data: shared workers should inherit its parent's policy.",
+ "sourceContextList": [
+ {
+ "sourceContextType": "top",
+ "policyDeliveries": [
+ "policy"
+ ]
+ },
+ {
+ "sourceContextType": "sharedworker-module-data",
+ "policyDeliveries": []
+ }
+ ],
+ "subresourcePolicyDeliveries": []
+ }
+ },
+ "test_expansion_schema": {
+ "expansion": [
+ "default",
+ "override"
+ ],
+ "source_scheme": [
+ "http",
+ "https"
+ ],
+ "source_context_list": [
+ "top",
+ "req",
+ "srcdoc-inherit",
+ "srcdoc",
+ "iframe",
+ "iframe-blank-inherit",
+ "worker-classic",
+ "worker-classic-data",
+ "worker-module",
+ "worker-module-data",
+ "sharedworker-classic",
+ "sharedworker-classic-data",
+ "sharedworker-module",
+ "sharedworker-module-data"
+ ],
+ "redirection": [
+ "no-redirect",
+ "keep-origin",
+ "swap-origin",
+ "keep-scheme",
+ "swap-scheme",
+ "downgrade"
+ ],
+ "origin": [
+ "same-https",
+ "same-http",
+ "same-http-downgrade",
+ "cross-https",
+ "cross-http",
+ "cross-http-downgrade",
+ "same-wss",
+ "same-ws",
+ "same-ws-downgrade",
+ "cross-wss",
+ "cross-ws",
+ "cross-ws-downgrade"
+ ],
+ "subresource": [
+ "a-tag",
+ "area-tag",
+ "audio-tag",
+ "beacon",
+ "fetch",
+ "iframe-tag",
+ "img-tag",
+ "link-css-tag",
+ "link-prefetch-tag",
+ "object-tag",
+ "picture-tag",
+ "script-tag",
+ "script-tag-dynamic-import",
+ "sharedworker-classic",
+ "sharedworker-import",
+ "sharedworker-import-data",
+ "sharedworker-module",
+ "video-tag",
+ "websocket",
+ "worker-classic",
+ "worker-import",
+ "worker-import-data",
+ "worker-module",
+ "worklet-animation",
+ "worklet-animation-import-data",
+ "worklet-audio",
+ "worklet-audio-import-data",
+ "worklet-layout",
+ "worklet-layout-import-data",
+ "worklet-paint",
+ "worklet-paint-import-data",
+ "xhr"
+ ]
+ }
+}
diff --git a/test/wpt/tests/common/security-features/tools/spec_validator.py b/test/wpt/tests/common/security-features/tools/spec_validator.py
new file mode 100644
index 0000000..f8a1390
--- /dev/null
+++ b/test/wpt/tests/common/security-features/tools/spec_validator.py
@@ -0,0 +1,251 @@
+#!/usr/bin/env python3
+
+import json, sys
+
+
+def assert_non_empty_string(obj, field):
+ assert field in obj, 'Missing field "%s"' % field
+ assert isinstance(obj[field], basestring), \
+ 'Field "%s" must be a string' % field
+ assert len(obj[field]) > 0, 'Field "%s" must not be empty' % field
+
+
+def assert_non_empty_list(obj, field):
+ assert isinstance(obj[field], list), \
+ '%s must be a list' % field
+ assert len(obj[field]) > 0, \
+ '%s list must not be empty' % field
+
+
+def assert_non_empty_dict(obj, field):
+ assert isinstance(obj[field], dict), \
+ '%s must be a dict' % field
+ assert len(obj[field]) > 0, \
+ '%s dict must not be empty' % field
+
+
+def assert_contains(obj, field):
+ assert field in obj, 'Must contain field "%s"' % field
+
+
+def assert_value_from(obj, field, items):
+ assert obj[field] in items, \
+ 'Field "%s" must be from: %s' % (field, str(items))
+
+
+def assert_atom_or_list_items_from(obj, field, items):
+ if isinstance(obj[field], basestring) or isinstance(
+ obj[field], int) or obj[field] is None:
+ assert_value_from(obj, field, items)
+ return
+
+ assert isinstance(obj[field], list), '%s must be a list' % field
+ for allowed_value in obj[field]:
+ assert allowed_value != '*', "Wildcard is not supported for lists!"
+ assert allowed_value in items, \
+ 'Field "%s" must be from: %s' % (field, str(items))
+
+
+def assert_contains_only_fields(obj, expected_fields):
+ for expected_field in expected_fields:
+ assert_contains(obj, expected_field)
+
+ for actual_field in obj:
+ assert actual_field in expected_fields, \
+ 'Unexpected field "%s".' % actual_field
+
+
+def leaf_values(schema):
+ if isinstance(schema, list):
+ return schema
+ ret = []
+ for _, sub_schema in schema.iteritems():
+ ret += leaf_values(sub_schema)
+ return ret
+
+
+def assert_value_unique_in(value, used_values):
+ assert value not in used_values, 'Duplicate value "%s"!' % str(value)
+ used_values[value] = True
+
+
+def assert_valid_artifact(exp_pattern, artifact_key, schema):
+ if isinstance(schema, list):
+ assert_atom_or_list_items_from(exp_pattern, artifact_key,
+ ["*"] + schema)
+ return
+
+ for sub_artifact_key, sub_schema in schema.iteritems():
+ assert_valid_artifact(exp_pattern[artifact_key], sub_artifact_key,
+ sub_schema)
+
+
+def validate(spec_json, details):
+ """ Validates the json specification for generating tests. """
+
+ details['object'] = spec_json
+ assert_contains_only_fields(spec_json, [
+ "selection_pattern", "test_file_path_pattern",
+ "test_description_template", "test_page_title_template",
+ "specification", "delivery_key", "subresource_schema",
+ "source_context_schema", "source_context_list_schema",
+ "test_expansion_schema", "excluded_tests"
+ ])
+ assert_non_empty_list(spec_json, "specification")
+ assert_non_empty_dict(spec_json, "test_expansion_schema")
+ assert_non_empty_list(spec_json, "excluded_tests")
+
+ specification = spec_json['specification']
+ test_expansion_schema = spec_json['test_expansion_schema']
+ excluded_tests = spec_json['excluded_tests']
+
+ valid_test_expansion_fields = test_expansion_schema.keys()
+
+ # Should be consistent with `sourceContextMap` in
+ # `/common/security-features/resources/common.sub.js`.
+ valid_source_context_names = [
+ "top", "iframe", "iframe-blank", "srcdoc", "worker-classic",
+ "worker-module", "worker-classic-data", "worker-module-data",
+ "sharedworker-classic", "sharedworker-module",
+ "sharedworker-classic-data", "sharedworker-module-data"
+ ]
+
+ valid_subresource_names = [
+ "a-tag", "area-tag", "audio-tag", "form-tag", "iframe-tag", "img-tag",
+ "link-css-tag", "link-prefetch-tag", "object-tag", "picture-tag",
+ "script-tag", "script-tag-dynamic-import", "video-tag"
+ ] + ["beacon", "fetch", "xhr", "websocket"] + [
+ "worker-classic", "worker-module", "worker-import",
+ "worker-import-data", "sharedworker-classic", "sharedworker-module",
+ "sharedworker-import", "sharedworker-import-data",
+ "serviceworker-classic", "serviceworker-module",
+ "serviceworker-import", "serviceworker-import-data"
+ ] + [
+ "worklet-animation", "worklet-audio", "worklet-layout",
+ "worklet-paint", "worklet-animation-import", "worklet-audio-import",
+ "worklet-layout-import", "worklet-paint-import",
+ "worklet-animation-import-data", "worklet-audio-import-data",
+ "worklet-layout-import-data", "worklet-paint-import-data"
+ ]
+
+ # Validate each single spec.
+ for spec in specification:
+ details['object'] = spec
+
+ # Validate required fields for a single spec.
+ assert_contains_only_fields(spec, [
+ 'title', 'description', 'specification_url', 'test_expansion'
+ ])
+ assert_non_empty_string(spec, 'title')
+ assert_non_empty_string(spec, 'description')
+ assert_non_empty_string(spec, 'specification_url')
+ assert_non_empty_list(spec, 'test_expansion')
+
+ for spec_exp in spec['test_expansion']:
+ details['object'] = spec_exp
+ assert_contains_only_fields(spec_exp, valid_test_expansion_fields)
+
+ for artifact in test_expansion_schema:
+ details['test_expansion_field'] = artifact
+ assert_valid_artifact(spec_exp, artifact,
+ test_expansion_schema[artifact])
+ del details['test_expansion_field']
+
+ # Validate source_context_schema.
+ details['object'] = spec_json['source_context_schema']
+ assert_contains_only_fields(
+ spec_json['source_context_schema'],
+ ['supported_delivery_type', 'supported_subresource'])
+ assert_contains_only_fields(
+ spec_json['source_context_schema']['supported_delivery_type'],
+ valid_source_context_names)
+ for source_context in spec_json['source_context_schema'][
+ 'supported_delivery_type']:
+ assert_valid_artifact(
+ spec_json['source_context_schema']['supported_delivery_type'],
+ source_context, test_expansion_schema['delivery_type'])
+ assert_contains_only_fields(
+ spec_json['source_context_schema']['supported_subresource'],
+ valid_source_context_names)
+ for source_context in spec_json['source_context_schema'][
+ 'supported_subresource']:
+ assert_valid_artifact(
+ spec_json['source_context_schema']['supported_subresource'],
+ source_context, leaf_values(test_expansion_schema['subresource']))
+
+ # Validate subresource_schema.
+ details['object'] = spec_json['subresource_schema']
+ assert_contains_only_fields(spec_json['subresource_schema'],
+ ['supported_delivery_type'])
+ assert_contains_only_fields(
+ spec_json['subresource_schema']['supported_delivery_type'],
+ leaf_values(test_expansion_schema['subresource']))
+ for subresource in spec_json['subresource_schema'][
+ 'supported_delivery_type']:
+ assert_valid_artifact(
+ spec_json['subresource_schema']['supported_delivery_type'],
+ subresource, test_expansion_schema['delivery_type'])
+
+ # Validate the test_expansion schema members.
+ details['object'] = test_expansion_schema
+ assert_contains_only_fields(test_expansion_schema, [
+ 'expansion', 'source_scheme', 'source_context_list', 'delivery_type',
+ 'delivery_value', 'redirection', 'subresource', 'origin', 'expectation'
+ ])
+ assert_atom_or_list_items_from(test_expansion_schema, 'expansion',
+ ['default', 'override'])
+ assert_atom_or_list_items_from(test_expansion_schema, 'source_scheme',
+ ['http', 'https'])
+ assert_atom_or_list_items_from(
+ test_expansion_schema, 'source_context_list',
+ spec_json['source_context_list_schema'].keys())
+
+ # Should be consistent with `preprocess_redirection` in
+ # `/common/security-features/subresource/subresource.py`.
+ assert_atom_or_list_items_from(test_expansion_schema, 'redirection', [
+ 'no-redirect', 'keep-origin', 'swap-origin', 'keep-scheme',
+ 'swap-scheme', 'downgrade'
+ ])
+ for subresource in leaf_values(test_expansion_schema['subresource']):
+ assert subresource in valid_subresource_names, "Invalid subresource %s" % subresource
+ # Should be consistent with getSubresourceOrigin() in
+ # `/common/security-features/resources/common.sub.js`.
+ assert_atom_or_list_items_from(test_expansion_schema, 'origin', [
+ 'same-http', 'same-https', 'same-ws', 'same-wss', 'cross-http',
+ 'cross-https', 'cross-ws', 'cross-wss', 'same-http-downgrade',
+ 'cross-http-downgrade', 'same-ws-downgrade', 'cross-ws-downgrade'
+ ])
+
+ # Validate excluded tests.
+ details['object'] = excluded_tests
+ for excluded_test_expansion in excluded_tests:
+ assert_contains_only_fields(excluded_test_expansion,
+ valid_test_expansion_fields)
+ details['object'] = excluded_test_expansion
+ for artifact in test_expansion_schema:
+ details['test_expansion_field'] = artifact
+ assert_valid_artifact(excluded_test_expansion, artifact,
+ test_expansion_schema[artifact])
+ del details['test_expansion_field']
+
+ del details['object']
+
+
+def assert_valid_spec_json(spec_json):
+ error_details = {}
+ try:
+ validate(spec_json, error_details)
+ except AssertionError as err:
+ print('ERROR:', err.message)
+ print(json.dumps(error_details, indent=4))
+ sys.exit(1)
+
+
+def main():
+ spec_json = load_spec_json()
+ assert_valid_spec_json(spec_json)
+ print("Spec JSON is valid.")
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/wpt/tests/common/security-features/tools/template/disclaimer.template b/test/wpt/tests/common/security-features/tools/template/disclaimer.template
new file mode 100644
index 0000000..ba9458c
--- /dev/null
+++ b/test/wpt/tests/common/security-features/tools/template/disclaimer.template
@@ -0,0 +1 @@
+<!-- DO NOT EDIT! Generated by `%(generating_script_filename)s --spec %(spec_directory)s/` -->
diff --git a/test/wpt/tests/common/security-features/tools/template/spec_json.js.template b/test/wpt/tests/common/security-features/tools/template/spec_json.js.template
new file mode 100644
index 0000000..e4cbd03
--- /dev/null
+++ b/test/wpt/tests/common/security-features/tools/template/spec_json.js.template
@@ -0,0 +1 @@
+var SPEC_JSON = %(spec_json)s;
diff --git a/test/wpt/tests/common/security-features/tools/template/test.debug.html.template b/test/wpt/tests/common/security-features/tools/template/test.debug.html.template
new file mode 100644
index 0000000..b6be088
--- /dev/null
+++ b/test/wpt/tests/common/security-features/tools/template/test.debug.html.template
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+%(generated_disclaimer)s
+<html>
+ <head>
+ <meta charset="utf-8">
+ <meta name="timeout" content="long">%(meta_delivery_method)s
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/common/security-features/resources/common.sub.js"></script>
+ <!-- The original specification JSON for validating the scenario. -->
+ <script src="%(spec_json_js)s"></script>
+ <!-- Internal checking of the tests -->
+ <script src="%(sanity_checker_js)s"></script>
+%(helper_js)s </head>
+ <body>
+ <script>
+ TestCase(
+ [
+ %(scenarios)s
+ ],
+ new SanityChecker()
+ ).start();
+ </script>
+ <div id="log"></div>
+ </body>
+</html>
diff --git a/test/wpt/tests/common/security-features/tools/template/test.release.html.template b/test/wpt/tests/common/security-features/tools/template/test.release.html.template
new file mode 100644
index 0000000..bac2d5b
--- /dev/null
+++ b/test/wpt/tests/common/security-features/tools/template/test.release.html.template
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+%(generated_disclaimer)s
+<html>
+ <head>
+ <meta charset="utf-8">
+ <meta name="timeout" content="long">%(meta_delivery_method)s
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/common/security-features/resources/common.sub.js"></script>
+%(helper_js)s </head>
+ <body>
+ <script>
+ TestCase(
+ [
+ %(scenarios)s
+ ],
+ new SanityChecker()
+ ).start();
+ </script>
+ <div id="log"></div>
+ </body>
+</html>
diff --git a/test/wpt/tests/common/security-features/tools/util.py b/test/wpt/tests/common/security-features/tools/util.py
new file mode 100644
index 0000000..5da06f9
--- /dev/null
+++ b/test/wpt/tests/common/security-features/tools/util.py
@@ -0,0 +1,228 @@
+import os, sys, json, json5, re
+import collections
+
+script_directory = os.path.dirname(os.path.abspath(__file__))
+template_directory = os.path.abspath(
+ os.path.join(script_directory, 'template'))
+test_root_directory = os.path.abspath(
+ os.path.join(script_directory, '..', '..', '..'))
+
+
+def get_template(basename):
+ with open(os.path.join(template_directory, basename), "r") as f:
+ return f.read()
+
+
+def write_file(filename, contents):
+ with open(filename, "w") as f:
+ f.write(contents)
+
+
+def read_nth_line(fp, line_number):
+ fp.seek(0)
+ for i, line in enumerate(fp):
+ if (i + 1) == line_number:
+ return line
+
+
+def load_spec_json(path_to_spec):
+ re_error_location = re.compile('line ([0-9]+) column ([0-9]+)')
+ with open(path_to_spec, "r") as f:
+ try:
+ return json5.load(f, object_pairs_hook=collections.OrderedDict)
+ except ValueError as ex:
+ print(ex.message)
+ match = re_error_location.search(ex.message)
+ if match:
+ line_number, column = int(match.group(1)), int(match.group(2))
+ print(read_nth_line(f, line_number).rstrip())
+ print(" " * (column - 1) + "^")
+ sys.exit(1)
+
+
+class ShouldSkip(Exception):
+ '''
+ Raised when the given combination of subresource type, source context type,
+ delivery type etc. are not supported and we should skip that configuration.
+ ShouldSkip is expected in normal generator execution (and thus subsequent
+ generation continues), as we first enumerate a broad range of configurations
+ first, and later raise ShouldSkip to filter out unsupported combinations.
+
+ ShouldSkip is distinguished from other general errors that cause immediate
+ termination of the generator and require fix.
+ '''
+ def __init__(self):
+ pass
+
+
+class PolicyDelivery(object):
+ '''
+ See `@typedef PolicyDelivery` comments in
+ `common/security-features/resources/common.sub.js`.
+ '''
+
+ def __init__(self, delivery_type, key, value):
+ self.delivery_type = delivery_type
+ self.key = key
+ self.value = value
+
+ def __eq__(self, other):
+ return type(self) is type(other) and self.__dict__ == other.__dict__
+
+ @classmethod
+ def list_from_json(cls, list, target_policy_delivery,
+ supported_delivery_types):
+ # type: (dict, PolicyDelivery, typing.List[str]) -> typing.List[PolicyDelivery]
+ '''
+ Parses a JSON object `list` that represents a list of `PolicyDelivery`
+ and returns a list of `PolicyDelivery`, plus supporting placeholders
+ (see `from_json()` comments below or
+ `common/security-features/README.md`).
+
+ Can raise `ShouldSkip`.
+ '''
+ if list is None:
+ return []
+
+ out = []
+ for obj in list:
+ policy_delivery = PolicyDelivery.from_json(
+ obj, target_policy_delivery, supported_delivery_types)
+ # Drop entries with null values.
+ if policy_delivery.value is None:
+ continue
+ out.append(policy_delivery)
+ return out
+
+ @classmethod
+ def from_json(cls, obj, target_policy_delivery, supported_delivery_types):
+ # type: (dict, PolicyDelivery, typing.List[str]) -> PolicyDelivery
+ '''
+ Parses a JSON object `obj` and returns a `PolicyDelivery` object.
+ In addition to dicts (in the same format as to_json() outputs),
+ this method accepts the following placeholders:
+ "policy":
+ `target_policy_delivery`
+ "policyIfNonNull":
+ `target_policy_delivery` if its value is not None.
+ "anotherPolicy":
+ A PolicyDelivery that has the same key as
+ `target_policy_delivery` but a different value.
+ The delivery type is selected from `supported_delivery_types`.
+
+ Can raise `ShouldSkip`.
+ '''
+
+ if obj == "policy":
+ policy_delivery = target_policy_delivery
+ elif obj == "nonNullPolicy":
+ if target_policy_delivery.value is None:
+ raise ShouldSkip()
+ policy_delivery = target_policy_delivery
+ elif obj == "anotherPolicy":
+ if len(supported_delivery_types) == 0:
+ raise ShouldSkip()
+ policy_delivery = target_policy_delivery.get_another_policy(
+ supported_delivery_types[0])
+ elif isinstance(obj, dict):
+ policy_delivery = PolicyDelivery(obj['deliveryType'], obj['key'],
+ obj['value'])
+ else:
+ raise Exception('policy delivery is invalid: ' + obj)
+
+ # Omit unsupported combinations of source contexts and delivery type.
+ if policy_delivery.delivery_type not in supported_delivery_types:
+ raise ShouldSkip()
+
+ return policy_delivery
+
+ def to_json(self):
+ # type: () -> dict
+ return {
+ "deliveryType": self.delivery_type,
+ "key": self.key,
+ "value": self.value
+ }
+
+ def get_another_policy(self, delivery_type):
+ # type: (str) -> PolicyDelivery
+ if self.key == 'referrerPolicy':
+ # Return 'unsafe-url' (i.e. more unsafe policy than `self.value`)
+ # as long as possible, to make sure the tests to fail if the
+ # returned policy is used unexpectedly instead of `self.value`.
+ # Using safer policy wouldn't be distinguishable from acceptable
+ # arbitrary policy enforcement by user agents, as specified at
+ # Step 7 of
+ # https://w3c.github.io/webappsec-referrer-policy/#determine-requests-referrer:
+ # "The user agent MAY alter referrerURL or referrerOrigin at this
+ # point to enforce arbitrary policy considerations in the
+ # interests of minimizing data leakage."
+ # See also the comments at `referrerUrlResolver` in
+ # `wpt/referrer-policy/generic/test-case.sub.js`.
+ if self.value != 'unsafe-url':
+ return PolicyDelivery(delivery_type, self.key, 'unsafe-url')
+ else:
+ return PolicyDelivery(delivery_type, self.key, 'no-referrer')
+ elif self.key == 'mixedContent':
+ if self.value == 'opt-in':
+ return PolicyDelivery(delivery_type, self.key, None)
+ else:
+ return PolicyDelivery(delivery_type, self.key, 'opt-in')
+ elif self.key == 'contentSecurityPolicy':
+ if self.value is not None:
+ return PolicyDelivery(delivery_type, self.key, None)
+ else:
+ return PolicyDelivery(delivery_type, self.key, 'worker-src-none')
+ elif self.key == 'upgradeInsecureRequests':
+ if self.value == 'upgrade':
+ return PolicyDelivery(delivery_type, self.key, None)
+ else:
+ return PolicyDelivery(delivery_type, self.key, 'upgrade')
+ else:
+ raise Exception('delivery key is invalid: ' + self.key)
+
+
+class SourceContext(object):
+ def __init__(self, source_context_type, policy_deliveries):
+ # type: (unicode, typing.List[PolicyDelivery]) -> None
+ self.source_context_type = source_context_type
+ self.policy_deliveries = policy_deliveries
+
+ def __eq__(self, other):
+ return type(self) is type(other) and self.__dict__ == other.__dict__
+
+ @classmethod
+ def from_json(cls, obj, target_policy_delivery, source_context_schema):
+ '''
+ Parses a JSON object `obj` and returns a `SourceContext` object.
+
+ `target_policy_delivery` and `source_context_schema` are used for
+ policy delivery placeholders and filtering out unsupported
+ delivery types.
+
+ Can raise `ShouldSkip`.
+ '''
+ source_context_type = obj.get('sourceContextType')
+ policy_deliveries = PolicyDelivery.list_from_json(
+ obj.get('policyDeliveries'), target_policy_delivery,
+ source_context_schema['supported_delivery_type']
+ [source_context_type])
+ return SourceContext(source_context_type, policy_deliveries)
+
+ def to_json(self):
+ return {
+ "sourceContextType": self.source_context_type,
+ "policyDeliveries": [x.to_json() for x in self.policy_deliveries]
+ }
+
+
+class CustomEncoder(json.JSONEncoder):
+ '''
+ Used to dump dicts containing `SourceContext`/`PolicyDelivery` into JSON.
+ '''
+ def default(self, obj):
+ if isinstance(obj, SourceContext):
+ return obj.to_json()
+ if isinstance(obj, PolicyDelivery):
+ return obj.to_json()
+ return json.JSONEncoder.default(self, obj)
diff --git a/test/wpt/tests/common/security-features/types.md b/test/wpt/tests/common/security-features/types.md
new file mode 100644
index 0000000..1707991
--- /dev/null
+++ b/test/wpt/tests/common/security-features/types.md
@@ -0,0 +1,62 @@
+# Types around the generator and generated tests
+
+This document describes types and concepts used across JavaScript and Python parts of this test framework.
+Please refer to the JSDoc in `common.sub.js` or docstrings in Python scripts (if any).
+
+## Scenario
+
+### Properties
+
+- All keys of `test_expansion_schema` in `spec.src.json`, except for `expansion`, `delivery_type`, `delivery_value`, and `source_context_list`. Their values are **string**s specified in `test_expansion_schema`.
+- `source_context_list`
+- `subresource_policy_deliveries`
+
+### Types
+
+- Generator (`spec.src.json`): JSON object
+- Generator (Python): `dict`
+- Runtime (JS): JSON object
+- Runtime (Python): N/A
+
+## `PolicyDelivery`
+
+### Types
+
+- Generator (`spec.src.json`): JSON object
+- Generator (Python): `util.PolicyDelivery`
+- Runtime (JS): JSON object (`@typedef PolicyDelivery` in `common.sub.js`)
+- Runtime (Python): N/A
+
+## `SourceContext`
+
+Subresource requests can be possibly sent from various kinds of fetch client's environment settings objects. For example:
+
+- top-level windows,
+- `<iframe>`s, or
+- `WorkerGlobalScope`s.
+
+A **`SourceContext`** object specifies one environment settings object, and an Array of `SourceContext` specifies a possibly nested context, from the outer-most to inner-most environment settings objects.
+
+Note: The top-level document is processed and trimmed by the generator, and is not included in the `sourceContextList` field of `Scenario` in the generated output.
+
+For example, `[{sourceContextType: "srcdoc"}, {sourceContextType: "worker-classic"}]` means that a subresource request is to be sent from a classic dedicated worker created from `<iframe srcdoc>` inside the top-level HTML Document.
+
+Note: A `SourceContext` (or an array of `SourceContext`) is set based on the fetch client's settings object that is used for the subresource fetch, NOT on the module map settings object nor on the inner-most settings object that appears in the test.
+For example, the `sourceContextList` field of `Scenario` is `[]` (indicating the top-level Window):
+
+- When testing top-level worker script fetch, e.g. `new Worker('worker.js')`. There is `WorkerGlobalScope` created from `worker.js`, but it isn't the fetch client's settings object used for fetching `worker.js` itself.
+- When testing worker script imported from the root worker script, e.g. `new Worker('top.js', {type: 'module'})` where `top.js` has `import 'worker.js'`. Again, the fetch client's settings object used for `worker.js` is the top-level Window, not `WorkerGlobalScope` created by `top.js`.
+
+### Properties
+
+- `sourceContextType`: A string specifying the kind of the source context to be used.
+ Valid values are the keys of `sourceContextMap` in `common.sub.js`, or `"top"` indicating the top-level Document (`"top"` is valid/used only in the generator).
+
+- `policyDeliveries`: A list of `PolicyDelivery` applied to the source context.
+
+### Types
+
+- Generator (`spec.src.json`): JSON object
+- Generator (Python): `util.SourceContext`
+- Runtime (JS): JSON object (`@typedef SourceContext` in `common.sub.js`)
+- Runtime (Python): N/A
diff --git a/test/wpt/tests/common/slow-redirect.py b/test/wpt/tests/common/slow-redirect.py
new file mode 100644
index 0000000..85c80e0
--- /dev/null
+++ b/test/wpt/tests/common/slow-redirect.py
@@ -0,0 +1,29 @@
+import time
+
+def main(request, response):
+ """Simple handler that causes redirection.
+
+ The request should typically have two query parameters:
+ status - The status to use for the redirection. Defaults to 302.
+ location - The resource to redirect to.
+ """
+ status = 302
+ delay = 2
+ if b"status" in request.GET:
+ try:
+ status = int(request.GET.first(b"status"))
+ except ValueError:
+ pass
+
+ if b"delay" in request.GET:
+ try:
+ delay = int(request.GET.first(b"delay"))
+ except ValueError:
+ pass
+
+ response.status = status
+ time.sleep(delay)
+
+ location = request.GET.first(b"location")
+
+ response.headers.set(b"Location", location)
diff --git a/test/wpt/tests/common/slow.py b/test/wpt/tests/common/slow.py
new file mode 100644
index 0000000..9be8aad
--- /dev/null
+++ b/test/wpt/tests/common/slow.py
@@ -0,0 +1,6 @@
+import time
+
+def main(request, response):
+ delay = float(request.GET.first(b"delay", 2000)) / 1000
+ time.sleep(delay)
+ return 200, [], b''
diff --git a/test/wpt/tests/common/square.png b/test/wpt/tests/common/square.png
new file mode 100644
index 0000000..01c9666
--- /dev/null
+++ b/test/wpt/tests/common/square.png
Binary files differ
diff --git a/test/wpt/tests/common/stringifiers.js b/test/wpt/tests/common/stringifiers.js
new file mode 100644
index 0000000..8dadac1
--- /dev/null
+++ b/test/wpt/tests/common/stringifiers.js
@@ -0,0 +1,57 @@
+/**
+ * Runs tests for <https://webidl.spec.whatwg.org/#es-stringifier>.
+ * @param {Object} aObject - object to test
+ * @param {string} aAttribute - IDL attribute name that is annotated with `stringifier`
+ * @param {boolean} aIsUnforgeable - whether the IDL attribute is `[LegacyUnforgeable]`
+ */
+function test_stringifier_attribute(aObject, aAttribute, aIsUnforgeable) {
+ // Step 1.
+ test(function() {
+ [null, undefined].forEach(function(v) {
+ assert_throws_js(TypeError, function() {
+ aObject.toString.call(v);
+ });
+ });
+ });
+
+ // TODO Step 2: security check.
+
+ // Step 3.
+ test(function() {
+ assert_false("Window" in window && aObject instanceof window.Window);
+ [{}, window].forEach(function(v) {
+ assert_throws_js(TypeError, function() {
+ aObject.toString.call(v)
+ });
+ });
+ });
+
+ // Step 4-6.
+ var expected_value;
+ test(function() {
+ expected_value = aObject[aAttribute];
+ assert_equals(aObject[aAttribute], expected_value,
+ "The attribute " + aAttribute + " should be pure.");
+ });
+
+ var test_error = { name: "test" };
+ test(function() {
+ if (!aIsUnforgeable) {
+ Object.defineProperty(aObject, aAttribute, {
+ configurable: true,
+ get: function() { throw test_error; }
+ });
+ }
+ assert_equals(aObject.toString(), expected_value);
+ });
+
+ test(function() {
+ if (!aIsUnforgeable) {
+ Object.defineProperty(aObject, aAttribute, {
+ configurable: true,
+ value: { toString: function() { throw test_error; } }
+ });
+ }
+ assert_equals(aObject.toString(), expected_value);
+ });
+}
diff --git a/test/wpt/tests/common/stringifiers.js.headers b/test/wpt/tests/common/stringifiers.js.headers
new file mode 100644
index 0000000..6805c32
--- /dev/null
+++ b/test/wpt/tests/common/stringifiers.js.headers
@@ -0,0 +1 @@
+Content-Type: text/javascript; charset=utf-8
diff --git a/test/wpt/tests/common/subset-tests-by-key.js b/test/wpt/tests/common/subset-tests-by-key.js
new file mode 100644
index 0000000..483017a
--- /dev/null
+++ b/test/wpt/tests/common/subset-tests-by-key.js
@@ -0,0 +1,83 @@
+(function() {
+ var subTestKeyPattern = null;
+ var match;
+ var collectKeys = false;
+ var collectCounts = false;
+ var keys = {};
+ var exclude = false;
+ if (location.search) {
+ match = /(?:^\?|&)(include|exclude)=([^&]+)?/.exec(location.search);
+ if (match) {
+ subTestKeyPattern = new RegExp(`^${match[2]}$`);
+ if (match[1] === 'exclude') {
+ exclude = true;
+ }
+ }
+ // Below is utility code to generate <meta> for copy/paste into tests.
+ // Sample usage:
+ // test.html?get-keys
+ match = /(?:^\?|&)get-keys(&get-counts)?(?:&|$)/.exec(location.search);
+ if (match) {
+ collectKeys = true;
+ if (match[1]) {
+ collectCounts = true;
+ }
+ add_completion_callback(() => {
+ var metas = [];
+ var template = '<meta name="variant" content="?include=%s">';
+ if (collectCounts) {
+ template += ' <!--%s-->';
+ }
+ for (var key in keys) {
+ var meta = template.replace("%s", key);
+ if (collectCounts) {
+ meta = meta.replace("%s", keys[key]);
+ }
+ metas.push(meta);
+ }
+ var pre = document.createElement('pre');
+ pre.textContent = metas.join('\n') + '\n';
+ document.body.insertBefore(pre, document.body.firstChild);
+ document.getSelection().selectAllChildren(pre);
+ });
+ }
+ }
+ /**
+ * Check if `key` is in the subset specified in the URL.
+ * @param {string} key
+ * @returns {boolean}
+ */
+ function shouldRunSubTest(key) {
+ if (key && subTestKeyPattern) {
+ var found = subTestKeyPattern.test(key);
+ if (exclude) {
+ return !found;
+ }
+ return found;
+ }
+ return true;
+ }
+ /**
+ * Only test a subset of tests with `?include=Foo` or `?exclude=Foo` in the URL.
+ * Can be used together with `<meta name="variant" content="...">`
+ * Sample usage:
+ * for (const test of tests) {
+ * subsetTestByKey("Foo", async_test, test.fn, test.name);
+ * }
+ */
+ function subsetTestByKey(key, testFunc, ...args) {
+ if (collectKeys) {
+ if (collectCounts && key in keys) {
+ keys[key]++;
+ } else {
+ keys[key] = 1;
+ }
+ }
+ if (shouldRunSubTest(key)) {
+ return testFunc(...args);
+ }
+ return null;
+ }
+ self.shouldRunSubTest = shouldRunSubTest;
+ self.subsetTestByKey = subsetTestByKey;
+})();
diff --git a/test/wpt/tests/common/subset-tests.js b/test/wpt/tests/common/subset-tests.js
new file mode 100644
index 0000000..58e9341
--- /dev/null
+++ b/test/wpt/tests/common/subset-tests.js
@@ -0,0 +1,60 @@
+(function() {
+ var subTestStart = 0;
+ var subTestEnd = Infinity;
+ var match;
+ if (location.search) {
+ match = /(?:^\?|&)(\d+)-(\d+|last)(?:&|$)/.exec(location.search);
+ if (match) {
+ subTestStart = parseInt(match[1], 10);
+ if (match[2] !== "last") {
+ subTestEnd = parseInt(match[2], 10);
+ }
+ }
+ // Below is utility code to generate <meta> for copy/paste into tests.
+ // Sample usage:
+ // test.html?split=1000
+ match = /(?:^\?|&)split=(\d+)(?:&|$)/.exec(location.search);
+ if (match) {
+ var testsPerVariant = parseInt(match[1], 10);
+ add_completion_callback(tests => {
+ var total = tests.length;
+ var template = '<meta name="variant" content="?%s-%s">';
+ var metas = [];
+ for (var i = 1; i < total - testsPerVariant; i = i + testsPerVariant) {
+ metas.push(template.replace("%s", i).replace("%s", i + testsPerVariant - 1));
+ }
+ metas.push(template.replace("%s", i).replace("%s", "last"));
+ var pre = document.createElement('pre');
+ pre.textContent = metas.join('\n');
+ document.body.insertBefore(pre, document.body.firstChild);
+ document.getSelection().selectAllChildren(pre);
+ });
+ }
+ }
+ /**
+ * Check if `currentSubTest` is in the subset specified in the URL.
+ * @param {number} currentSubTest
+ * @returns {boolean}
+ */
+ function shouldRunSubTest(currentSubTest) {
+ return currentSubTest >= subTestStart && currentSubTest <= subTestEnd;
+ }
+ var currentSubTest = 0;
+ /**
+ * Only test a subset of tests with, e.g., `?1-10` in the URL.
+ * Can be used together with `<meta name="variant" content="...">`
+ * Sample usage:
+ * for (const test of tests) {
+ * subsetTest(async_test, test.fn, test.name);
+ * }
+ */
+ function subsetTest(testFunc, ...args) {
+ currentSubTest++;
+ if (shouldRunSubTest(currentSubTest)) {
+ return testFunc(...args);
+ }
+ return null;
+ }
+ self.shouldRunSubTest = shouldRunSubTest;
+ self.subsetTest = subsetTest;
+})();
diff --git a/test/wpt/tests/common/test-setting-immutable-prototype.js b/test/wpt/tests/common/test-setting-immutable-prototype.js
new file mode 100644
index 0000000..de9bdd5
--- /dev/null
+++ b/test/wpt/tests/common/test-setting-immutable-prototype.js
@@ -0,0 +1,67 @@
+self.testSettingImmutablePrototypeToNewValueOnly =
+ (prefix, target, newValue, newValueString, { isSameOriginDomain },
+ targetGlobal = window) => {
+ test(() => {
+ assert_throws_js(TypeError, () => {
+ Object.setPrototypeOf(target, newValue);
+ });
+ }, `${prefix}: setting the prototype to ${newValueString} via Object.setPrototypeOf should throw a TypeError`);
+
+ let dunderProtoError = "SecurityError";
+ let dunderProtoErrorName = "\"SecurityError\" DOMException";
+ if (isSameOriginDomain) {
+ // We're going to end up calling the __proto__ setter, which will
+ // enter the Realm of targetGlobal before throwing.
+ dunderProtoError = targetGlobal.TypeError;
+ dunderProtoErrorName = "TypeError";
+ }
+
+ test(() => {
+ const func = function() {
+ target.__proto__ = newValue;
+ };
+ if (isSameOriginDomain) {
+ assert_throws_js(dunderProtoError, func);
+ } else {
+ assert_throws_dom(dunderProtoError, func);
+ }
+ }, `${prefix}: setting the prototype to ${newValueString} via __proto__ should throw a ${dunderProtoErrorName}`);
+
+ test(() => {
+ assert_false(Reflect.setPrototypeOf(target, newValue));
+ }, `${prefix}: setting the prototype to ${newValueString} via Reflect.setPrototypeOf should return false`);
+};
+
+self.testSettingImmutablePrototype =
+ (prefix, target, originalValue, { isSameOriginDomain }, targetGlobal = window) => {
+ const newValue = {};
+ const newValueString = "an empty object";
+ testSettingImmutablePrototypeToNewValueOnly(prefix, target, newValue, newValueString, { isSameOriginDomain }, targetGlobal);
+
+ const originalValueString = originalValue === null ? "null" : "its original value";
+
+ test(() => {
+ assert_equals(Object.getPrototypeOf(target), originalValue);
+ }, `${prefix}: the prototype must still be ${originalValueString}`);
+
+ test(() => {
+ Object.setPrototypeOf(target, originalValue);
+ }, `${prefix}: setting the prototype to ${originalValueString} via Object.setPrototypeOf should not throw`);
+
+ if (isSameOriginDomain) {
+ test(() => {
+ target.__proto__ = originalValue;
+ }, `${prefix}: setting the prototype to ${originalValueString} via __proto__ should not throw`);
+ } else {
+ test(() => {
+ assert_throws_dom("SecurityError", function() {
+ target.__proto__ = newValue;
+ });
+ }, `${prefix}: setting the prototype to ${originalValueString} via __proto__ should throw a "SecurityError" since ` +
+ `it ends up in CrossOriginGetOwnProperty`);
+ }
+
+ test(() => {
+ assert_true(Reflect.setPrototypeOf(target, originalValue));
+ }, `${prefix}: setting the prototype to ${originalValueString} via Reflect.setPrototypeOf should return true`);
+};
diff --git a/test/wpt/tests/common/test-setting-immutable-prototype.js.headers b/test/wpt/tests/common/test-setting-immutable-prototype.js.headers
new file mode 100644
index 0000000..6805c32
--- /dev/null
+++ b/test/wpt/tests/common/test-setting-immutable-prototype.js.headers
@@ -0,0 +1 @@
+Content-Type: text/javascript; charset=utf-8
diff --git a/test/wpt/tests/common/text-plain.txt b/test/wpt/tests/common/text-plain.txt
new file mode 100644
index 0000000..97ca870
--- /dev/null
+++ b/test/wpt/tests/common/text-plain.txt
@@ -0,0 +1,4 @@
+This is a sample text/plain document.
+
+This is not an HTML document.
+
diff --git a/test/wpt/tests/common/third_party/reftest-analyzer.xhtml b/test/wpt/tests/common/third_party/reftest-analyzer.xhtml
new file mode 100644
index 0000000..4c7b265
--- /dev/null
+++ b/test/wpt/tests/common/third_party/reftest-analyzer.xhtml
@@ -0,0 +1,934 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- -*- Mode: HTML; tab-width: 2; indent-tabs-mode: nil; -*- -->
+<!-- vim: set shiftwidth=2 tabstop=2 autoindent expandtab: -->
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<!--
+
+Features to add:
+* make the left and right parts of the viewer independently scrollable
+* make the test list filterable
+** default to only showing unexpecteds
+* add other ways to highlight differences other than circling?
+* add zoom/pan to images
+* Add ability to load log via XMLHttpRequest (also triggered via URL param)
+* color the test list based on pass/fail and expected/unexpected/random/skip
+* ability to load multiple logs ?
+** rename them by clicking on the name and editing
+** turn the test list into a collapsing tree view
+** move log loading into popup from viewer UI
+
+-->
+<!DOCTYPE html>
+<html lang="en-US" xml:lang="en-US" xmlns="http://www.w3.org/1999/xhtml">
+<head>
+ <title>Reftest analyzer</title>
+ <style type="text/css"><![CDATA[
+
+ html, body { margin: 0; }
+ html { padding: 0; }
+ body { padding: 4px; }
+
+ #pixelarea, #itemlist, #images { position: absolute; }
+ #itemlist, #images { overflow: auto; }
+ #pixelarea { top: 0; left: 0; width: 320px; height: 84px; overflow: visible }
+ #itemlist { top: 84px; left: 0; width: 320px; bottom: 0; }
+ #images { top: 0; bottom: 0; left: 320px; right: 0; }
+
+ #leftpane { width: 320px; }
+ #images { position: fixed; top: 10px; left: 340px; }
+
+ form#imgcontrols { margin: 0; display: block; }
+
+ #itemlist > table { border-collapse: collapse; }
+ #itemlist > table > tbody > tr > td { border: 1px solid; padding: 1px; }
+ #itemlist td.activeitem { background-color: yellow; }
+
+ /*
+ #itemlist > table > tbody > tr.pass > td.url { background: lime; }
+ #itemlist > table > tbody > tr.fail > td.url { background: red; }
+ */
+
+ #magnification > svg { display: block; width: 84px; height: 84px; }
+
+ #pixelinfo { font: small sans-serif; position: absolute; width: 200px; left: 84px; }
+ #pixelinfo table { border-collapse: collapse; }
+ #pixelinfo table th { white-space: nowrap; text-align: left; padding: 0; }
+ #pixelinfo table td { font-family: monospace; padding: 0 0 0 0.25em; }
+
+ #pixelhint { display: inline; color: #88f; cursor: help; }
+ #pixelhint > * { display: none; position: absolute; margin: 8px 0 0 8px; padding: 4px; width: 400px; background: #ffa; color: black; box-shadow: 3px 3px 2px #888; z-index: 1; }
+ #pixelhint:hover { color: #000; }
+ #pixelhint:hover > * { display: block; }
+ #pixelhint p { margin: 0; }
+ #pixelhint p + p { margin-top: 1em; }
+
+ ]]></style>
+ <script type="text/javascript"><![CDATA[
+
+var XLINK_NS = "http://www.w3.org/1999/xlink";
+var SVG_NS = "http://www.w3.org/2000/svg";
+var IMAGE_NOT_AVAILABLE = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAKAAAAASCAYAAADczdVTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAHy0lEQVRoge2aX2hb5xnGf2dYabROgqQkpMuKnWUJLmxHMFaa/SscteQiF5EvUgqLctEVrDJKK1+MolzkQr4IctgW+SLIheJc1BpFpswJw92FbaZsTCGTL0465AtntUekJdJ8lByVHbnnwLsLKbKdSJbiZBVjeuAYn+/P+z3fc97vfd9zbEVEhB566BK+1m0CPfx/o+eAPXQVbR3QqVapOl8FlR46h0O1Wu02iacCZfsasMKEz8vbx1JYE6fY/dXx6mEbFObPcvDVDBlznpc9G+2r8xNcvLqK2w39r4UI+fs7tFjmytgFFu718865EIebPGincI3zFz7Bcrtx97/GL0P+p+IPbSOgRwXtW3vpewqL/a/g5rgf39hit2m0hGUAHOHrrq3trmef4/lDB7Ay57n01zuPZXPX7jUunv+Yf9ktR7D/0CHca7/n3KXPsHbAuynkCWCZptgiImKLaVqP9NuW1bT9ceybpr3j+WJbYrVa3rbEatGZi2uixvWdrysilmWKae2M+5PqlktoosayLfubcrN10dAk24aynUsIxMVsadwUs+EX7dEyAlaXLqMoCj6fj5HkUqO9MD+Govjx+xXcXi+uoRAhvwuv182Z8Ws4AJUlxoZ8uNxuvF43ii/EtdXNNUuV68lR/IqC4gsxPj7KkE/BF5qmClRXrzFSt+/1ulDOjLNU6eQ4OcyPDqH4hhg5O4LicuN2K4xcvk6jjHUKJM8O1fvcKMoZkouFOq1VPp1OcuXGAvrvfsv0lWmSySTzN0sdH+jyYhK/ouB2e/G6XfjPJikBVG8SUhT8fl99nwVGfQp+vx+f4iO5VO1AtwJjfgXF58M/kqSVJP9ef0xuAI6NlwWmL41xxqeg+PyMXr72yBqW3cI4JaZHh1DcXrxeLy5liORiB7q1PiZFyeV0mQqz9TRZeUmFVUGLSjqdkgCIFp2RTCosEJOiiIihSyKWkDl9WYrFnCQCCNF0w0QmHhBQJTEzJ+nZSQmAoEYks2KIGBkJgASiM5I3LbGMnCSCCEQl38GJMvMZiag1e+nlFcmmIgKaZEwREaPGhWGZ1VfEMFZkNj4sgCSyhoihSzwSlqCGoAUlEo1IJByW+Oxyh+dZJJ+eklhiRnIrRcnrM6KCxLOmiNiipyICSGR2pTY2O1m7T2XEsNrrJmJLfjkn6amwoMbFaMEhG28eAVtzExErW3sOBCWVzkpmNiEqCOEZ2RyLTT3eJAKaMhVEUMOSXjHEtg3JTIUFkNTK9rGwbQrWm2xGb6QoWxIqEtdtEWO28aDtoi6JSFCAjUtL1AUzJA4SSW/IZ2VjjU0V0zEBJBiJSzwWk1g8IZEAAmrdidrBkoSKxB4IW08tGVNEzIxoIJM5a8v4SQ1RY5lGSy6x8xScz6QkHFBre1Zre49nH+y1KDEQLV7TcyU1LBCtHVppp9smxk2dYAMtHXA7blZWNJDZ4sZ4MxPbdHjrbc3WNuvOq4YlkYhLLBaXeKx2sLcrBUS2ScFtUbUBh3WgajvgOYgGuKjw4Rsqb1uvkssbWLbJXFQFqL/I9IEKa2WzYcqy16E2BNteB1R+cuwoRwcHGRx4nlfenWMuPclRDx3goSraqd+7Gj/Y5d76SrXLu3VKLYW1rMZbo/QpB4+9zt6fT1I0Law/LRMBaLzC7ePNuSgL7/2GpcotLr7+AZG5t9gH0Fa3zuFq1tiWG4DKs5tebV1NDDW1XYd26iWO9A8wODjAUfUN5ubm+Ch4ZFuuLRzQoVwqUCqXyN9fg3tFSuUShVIZhyr5O2vo94o42DwD/PP23fq8Bf5urLO+BoHBwxzc20c++wcmz+lAkWLFATwcf3+YDwIDhMYmuDw+wt5j5+C5ZwDYP/gSoLP6xX5+fOIkJ47/lIP8g49/Nc3tDj59OZUiRR3uFYsAVO/eZoE1yvkyeA6gAaff+zU3SxUcp8LilQucnoFTP3hhix19/garlQqFW9eZOBti9Mqt9mubXwBw+NALeDC4cfVDzgP3i3keUN/nf4uo+hEver/DRaK84/9mY/72uoFTKVMolVn5/HPgPvlSmVKhRL2bSrlEqVyidH8N/d7t2u/lakfcKneLgM4rvxhncbXA6tI8kTffB+0NjnrAqZYplcrk83ceXdtzgB+psHD7S/pfPs7JkydQB1x8dnWS2SVje9GaxkVLl+DmNNC4NJn/S6JxH5nJyNRwrW7Qi7oMgxBMyd9molvmRKO1cExgshG6l9NTEhkOynAkLlOJoKBuhPV8ZlK0h9aNTqVbv3ltEK/VIiAQEN0yZVLbuM+aImLoEgts3VdsJrfFil1M1/ZSv9RAROaWO8n/hkyF1Q3bgeFGygvPrDRG5Wcf1IJbq9rlNrrNbra96aqlUVMSWrNnNiw5uw23T/4o4Xq7FtA29h2My3K9WtETgRZr13UxdIk+pGswkpCcsX0N2OZD9BOgWqFsgWePp20KWb0ywkDgEIa8y55Gq0O5XKHP7cGz++l/haxWylgOuD17aG7eoVpxwL27RX8b27jZ42n1qdahXKrg2bfnUW0eQ7edoD232l+/LPp2pHvNfh8eT2f8/3sO2AZLyRAvns6gqToLOgxP6Uz87HvdoNJDF9E1B6ysLrLw5yW+3PUNvv3dH/L9wX3doNFDl9E1B+yhB+j9O1YPXcZ/AAl9BWJNvZE7AAAAAElFTkSuQmCC";
+
+var gPhases = null;
+
+var gIDCache = {};
+
+var gMagPixPaths = []; // 2D array of array-of-two <path> objects used in the pixel magnifier
+var gMagWidth = 5; // number of zoomed in pixels to show horizontally
+var gMagHeight = 5; // number of zoomed in pixels to show vertically
+var gMagZoom = 16; // size of the zoomed in pixels
+var gImage1Data; // ImageData object for the reference image
+var gImage2Data; // ImageData object for the test output image
+var gFlashingPixels = []; // array of <path> objects that should be flashed due to pixel color mismatch
+var gParams;
+
+function ID(id) {
+ if (!(id in gIDCache))
+ gIDCache[id] = document.getElementById(id);
+ return gIDCache[id];
+}
+
+function hash_parameters() {
+ var result = { };
+ var params = window.location.hash.substr(1).split(/[&;]/);
+ for (var i = 0; i < params.length; i++) {
+ var parts = params[i].split("=");
+ result[parts[0]] = unescape(unescape(parts[1]));
+ }
+ return result;
+}
+
+function load() {
+ gPhases = [ ID("entry"), ID("loading"), ID("viewer") ];
+ build_mag();
+ gParams = hash_parameters();
+ if (gParams.log) {
+ show_phase("loading");
+ process_log(gParams.log);
+ } else if (gParams.logurl) {
+ show_phase("loading");
+ var req = new XMLHttpRequest();
+ req.onreadystatechange = function() {
+ if (req.readyState === 4) {
+ process_log(req.responseText);
+ }
+ };
+ req.open('GET', gParams.logurl, true);
+ req.send();
+ }
+ window.addEventListener('keypress', handle_keyboard_shortcut);
+ window.addEventListener('keydown', handle_keydown);
+ ID("image1").addEventListener('error', image_load_error);
+ ID("image2").addEventListener('error', image_load_error);
+}
+
+function image_load_error(e) {
+ e.target.setAttributeNS(XLINK_NS, "xlink:href", IMAGE_NOT_AVAILABLE);
+}
+
+function build_mag() {
+ var mag = ID("mag");
+
+ var r = document.createElementNS(SVG_NS, "rect");
+ r.setAttribute("x", gMagZoom * -gMagWidth / 2);
+ r.setAttribute("y", gMagZoom * -gMagHeight / 2);
+ r.setAttribute("width", gMagZoom * gMagWidth);
+ r.setAttribute("height", gMagZoom * gMagHeight);
+ mag.appendChild(r);
+
+ mag.setAttribute("transform", "translate(" + (gMagZoom * (gMagWidth / 2) + 1) + "," + (gMagZoom * (gMagHeight / 2) + 1) + ")");
+
+ for (var x = 0; x < gMagWidth; x++) {
+ gMagPixPaths[x] = [];
+ for (var y = 0; y < gMagHeight; y++) {
+ var p1 = document.createElementNS(SVG_NS, "path");
+ p1.setAttribute("d", "M" + ((x - gMagWidth / 2) + 1) * gMagZoom + "," + (y - gMagHeight / 2) * gMagZoom + "h" + -gMagZoom + "v" + gMagZoom);
+ p1.setAttribute("stroke", "black");
+ p1.setAttribute("stroke-width", "1px");
+ p1.setAttribute("fill", "#aaa");
+
+ var p2 = document.createElementNS(SVG_NS, "path");
+ p2.setAttribute("d", "M" + ((x - gMagWidth / 2) + 1) * gMagZoom + "," + (y - gMagHeight / 2) * gMagZoom + "v" + gMagZoom + "h" + -gMagZoom);
+ p2.setAttribute("stroke", "black");
+ p2.setAttribute("stroke-width", "1px");
+ p2.setAttribute("fill", "#888");
+
+ mag.appendChild(p1);
+ mag.appendChild(p2);
+ gMagPixPaths[x][y] = [p1, p2];
+ }
+ }
+
+ var flashedOn = false;
+ setInterval(function() {
+ flashedOn = !flashedOn;
+ flash_pixels(flashedOn);
+ }, 500);
+}
+
+function show_phase(phaseid) {
+ for (var i in gPhases) {
+ var phase = gPhases[i];
+ phase.style.display = (phase.id == phaseid) ? "" : "none";
+ }
+
+ if (phase == "viewer")
+ ID("images").style.display = "none";
+}
+
+function fileentry_changed() {
+ show_phase("loading");
+ var input = ID("fileentry");
+ var files = input.files;
+ if (files.length > 0) {
+ // Only handle the first file; don't handle multiple selection.
+ // The parts of the log we care about are ASCII-only. Since we
+ // can ignore lines we don't care about, best to read in as
+ // iso-8859-1, which guarantees we don't get decoding errors.
+ var fileReader = new FileReader();
+ fileReader.onload = function(e) {
+ var log = null;
+
+ log = e.target.result;
+
+ if (log)
+ process_log(log);
+ else
+ show_phase("entry");
+ }
+ fileReader.readAsText(files[0], "iso-8859-1");
+ }
+ // So the user can process the same filename again (after
+ // overwriting the log), clear the value on the form input so we
+ // will always get an onchange event.
+ input.value = "";
+}
+
+function log_pasted() {
+ show_phase("loading");
+ var entry = ID("logentry");
+ var log = entry.value;
+ entry.value = "";
+ process_log(log);
+}
+
+var gTestItems;
+
+// This function is not used in production code, but can be invoked manually
+// from the devtools console in order to test changes to the parsing regexes
+// in process_log.
+function test_parsing() {
+ // Note that the logs in these testcases have been manually edited to strip
+ // out stuff for brevity.
+ var testcases = [
+ { "name": "empty log",
+ "log": "",
+ "expected": { "pass": 0, "unexpected": 0, "random": 0, "skip": 0 },
+ "expected_images": 0,
+ },
+ { "name": "android log",
+ "log": `[task 2018-12-28T10:36:45.718Z] 10:36:45 INFO - REFTEST TEST-START | a == b
+[task 2018-12-28T10:36:45.719Z] 10:36:45 INFO - REFTEST TEST-LOAD | a | 78 / 275 (28%)
+[task 2018-12-28T10:36:56.138Z] 10:36:56 INFO - REFTEST TEST-LOAD | b | 78 / 275 (28%)
+[task 2018-12-28T10:37:06.559Z] 10:37:06 INFO - REFTEST TEST-UNEXPECTED-FAIL | a == b | image comparison, max difference: 255, number of differing pixels: 5950
+[task 2018-12-28T10:37:06.568Z] 10:37:06 INFO - REFTEST IMAGE 1 (TEST): data:image/png;base64,
+[task 2018-12-28T10:37:06.577Z] 10:37:06 INFO - REFTEST IMAGE 2 (REFERENCE): data:image/png;base64,
+[task 2018-12-28T10:37:06.577Z] 10:37:06 INFO - REFTEST INFO | Saved log: stuff trimmed here
+[task 2018-12-28T10:37:06.582Z] 10:37:06 INFO - REFTEST TEST-END | a == b
+[task 2018-12-28T10:37:06.583Z] 10:37:06 INFO - REFTEST TEST-START | a2 == b2
+[task 2018-12-28T10:37:06.583Z] 10:37:06 INFO - REFTEST TEST-LOAD | a2 | 79 / 275 (28%)
+[task 2018-12-28T10:37:06.584Z] 10:37:06 INFO - REFTEST TEST-LOAD | b2 | 79 / 275 (28%)
+[task 2018-12-28T10:37:16.982Z] 10:37:16 INFO - REFTEST TEST-PASS | a2 == b2 | image comparison, max difference: 0, number of differing pixels: 0
+[task 2018-12-28T10:37:16.982Z] 10:37:16 INFO - REFTEST TEST-END | a2 == b2`,
+ "expected": { "pass": 1, "unexpected": 1, "random": 0, "skip": 0 },
+ "expected_images": 2,
+ },
+ { "name": "local reftest run (Linux)",
+ "log": `REFTEST TEST-START | file:///a == file:///b
+REFTEST TEST-LOAD | file:///a | 73 / 86 (84%)
+REFTEST TEST-LOAD | file:///b | 73 / 86 (84%)
+REFTEST TEST-PASS | file:///a == file:///b | image comparison, max difference: 0, number of differing pixels: 0
+REFTEST TEST-END | file:///a == file:///b`,
+ "expected": { "pass": 1, "unexpected": 0, "random": 0, "skip": 0 },
+ "expected_images": 0,
+ },
+ { "name": "wpt reftests (Linux automation)",
+ "log": `16:50:43 INFO - TEST-START | /a
+16:50:43 INFO - PID 4276 | 1548694243694 Marionette INFO Testing http://web-platform.test:8000/a == http://web-platform.test:8000/b
+16:50:43 INFO - PID 4276 | 1548694243963 Marionette INFO No differences allowed
+16:50:44 INFO - TEST-PASS | /a | took 370ms
+16:50:44 INFO - TEST-START | /a2
+16:50:44 INFO - PID 4276 | 1548694244066 Marionette INFO Testing http://web-platform.test:8000/a2 == http://web-platform.test:8000/b2
+16:50:44 INFO - PID 4276 | 1548694244792 Marionette INFO No differences allowed
+16:50:44 INFO - PID 4276 | 1548694244792 Marionette INFO Found 28 pixels different, maximum difference per channel 14
+16:50:44 INFO - TEST-UNEXPECTED-FAIL | /a2 | Testing http://web-platform.test:8000/a2 == http://web-platform.test:8000/b2
+16:50:44 INFO - REFTEST IMAGE 1 (TEST): data:image/png;base64,
+16:50:44 INFO - REFTEST IMAGE 2 (REFERENCE): data:image/png;base64,
+16:50:44 INFO - TEST-INFO took 840ms`,
+ "expected": { "pass": 1, "unexpected": 1, "random": 0, "skip": 0 },
+ "expected_images": 2,
+ },
+ { "name": "windows log",
+ "log": `12:17:14 INFO - REFTEST TEST-START | a == b
+12:17:14 INFO - REFTEST TEST-LOAD | a | 1603 / 2053 (78%)
+12:17:14 INFO - REFTEST TEST-LOAD | b | 1603 / 2053 (78%)
+12:17:14 INFO - REFTEST TEST-PASS(EXPECTED RANDOM) | a == b | image comparison, max difference: 0, number of differing pixels: 0
+12:17:14 INFO - REFTEST TEST-END | a == b
+12:17:14 INFO - REFTEST TEST-START | a2 == b2
+12:17:14 INFO - REFTEST TEST-LOAD | a2 | 1604 / 2053 (78%)
+12:17:14 INFO - REFTEST TEST-LOAD | b2 | 1604 / 2053 (78%)
+12:17:14 INFO - REFTEST TEST-UNEXPECTED-FAIL | a2 == b2 | image comparison, max difference: 255, number of differing pixels: 9976
+12:17:14 INFO - REFTEST IMAGE 1 (TEST): data:image/png;base64,
+12:17:14 INFO - REFTEST IMAGE 2 (REFERENCE): data:image/png;base64,
+12:17:14 INFO - REFTEST INFO | Saved log: stuff trimmed here
+12:17:14 INFO - REFTEST TEST-END | a2 == b2
+12:01:09 INFO - REFTEST TEST-START | a3 == b3
+12:01:09 INFO - REFTEST TEST-LOAD | a3 | 66 / 189 (34%)
+12:01:09 INFO - REFTEST TEST-LOAD | b3 | 66 / 189 (34%)
+12:01:09 INFO - REFTEST TEST-KNOWN-FAIL | a3 == b3 | image comparison, max difference: 255, number of differing pixels: 9654
+12:01:09 INFO - REFTEST TEST-END | a3 == b3`,
+ "expected": { "pass": 1, "unexpected": 1, "random": 1, "skip": 0 },
+ "expected_images": 2,
+ },
+ { "name": "webrender wrench log (windows)",
+ "log": `[task 2018-12-29T04:29:48.800Z] REFTEST a == b
+[task 2018-12-29T04:29:48.984Z] REFTEST a2 == b2
+[task 2018-12-29T04:29:49.053Z] REFTEST TEST-UNEXPECTED-FAIL | a2 == b2 | image comparison, max difference: 255, number of differing pixels: 3128
+[task 2018-12-29T04:29:49.053Z] REFTEST IMAGE 1 (TEST): data:image/png;
+[task 2018-12-29T04:29:49.053Z] REFTEST IMAGE 2 (REFERENCE): data:image/png;
+[task 2018-12-29T04:29:49.053Z] REFTEST TEST-END | a2 == b2`,
+ "expected": { "pass": 0, "unexpected": 1, "random": 0, "skip": 0 },
+ "expected_images": 2,
+ },
+ { "name": "wpt reftests (Linux local; Bug 1530008)",
+ "log": `SUITE-START | Running 1 tests
+TEST-START | /css/css-backgrounds/border-image-6.html
+TEST-UNEXPECTED-FAIL | /css/css-backgrounds/border-image-6.html | Testing http://web-platform.test:8000/css/css-backgrounds/border-image-6.html == http://web-platform.test:8000/css/css-backgrounds/border-image-6-ref.html
+REFTEST IMAGE 1 (TEST): data:image/png;base64,
+REFTEST IMAGE 2 (REFERENCE): data:image/png;base64,
+TEST-INFO took 425ms
+SUITE-END | took 2s`,
+ "expected": { "pass": 0, "unexpected": 1, "random": 0, "skip": 0 },
+ "expected_images": 2,
+ },
+ { "name": "wpt reftests (taskcluster log from macOS CI)",
+ "log": `[task 2020-06-26T01:35:29.065Z] 01:35:29 INFO - TEST-START | /html/rendering/non-replaced-elements/the-page/iframe-scrolling-attribute-values.html
+[task 2020-06-26T01:35:29.065Z] 01:35:29 INFO - PID 1353 | 1593135329040 Marionette INFO Testing http://web-platform.test:8000/html/rendering/non-replaced-elements/the-page/iframe-scrolling-attribute-values.html == http://web-platform.test:8000/html/rendering/non-replaced-elements/the-page/iframe-scrolling-attribute-values-ref.html
+[task 2020-06-26T01:35:29.673Z] 01:35:29 INFO - PID 1353 | 1593135329633 Marionette INFO No differences allowed
+[task 2020-06-26T01:35:29.726Z] 01:35:29 INFO - TEST-KNOWN-INTERMITTENT-FAIL | /html/rendering/non-replaced-elements/the-page/iframe-scrolling-attribute-values.html | took 649ms
+[task 2020-06-26T01:35:29.726Z] 01:35:29 INFO - REFTEST IMAGE 1 (TEST): data:image/png;
+[task 2020-06-26T01:35:29.726Z] 01:35:29 INFO - REFTEST IMAGE 2 (REFERENCE): data:image/png;`,
+ "expected": { "pass": 0, "unexpected": 0, "random": 1, "skip": 0 },
+ "expected_images": 2,
+ },
+ { "name": "wpt reftests (taskcluster log from Windows CI)",
+ "log": `[task 2020-06-26T01:41:19.205Z] 01:41:19 INFO - TEST-START | /html/rendering/non-replaced-elements/the-page/iframe-scrolling-attribute-values.html
+[task 2020-06-26T01:41:19.214Z] 01:41:19 INFO - PID 5920 | 1593135679202 Marionette WARN [24] http://web-platform.test:8000/css/WOFF2/metadatadisplay-schema-license-022-ref.xht overflows viewport (width: 783, height: 731)
+[task 2020-06-26T01:41:19.214Z] 01:41:19 INFO - PID 9692 | 1593135679208 Marionette INFO Testing http://web-platform.test:8000/html/rendering/non-replaced-elements/the-page/iframe-scrolling-attribute-values.html == http://web-platform.test:8000/html/rendering/non-replaced-elements/the-page/iframe-scrolling-attribute-values-ref.html
+[task 2020-06-26T01:41:19.638Z] 01:41:19 INFO - PID 9692 | 1593135679627 Marionette INFO No differences allowed
+[task 2020-06-26T01:41:19.688Z] 01:41:19 INFO - TEST-KNOWN-INTERMITTENT-PASS | /html/rendering/non-replaced-elements/the-page/iframe-scrolling-attribute-values.html | took 474ms
+[task 2020-06-26T01:41:19.688Z] 01:41:19 INFO - REFTEST IMAGE 1 (TEST): data:image/png;
+[task 2020-06-26T01:41:19.689Z] 01:41:19 INFO - REFTEST IMAGE 2 (REFERENCE): data:image/png;`,
+ "expected": { "pass": 1, "unexpected": 0, "random": 1, "skip": 0 },
+ "expected_images": 2,
+ },
+ { "name": "local reftest run with timestamps (Linux; Bug 1167712)",
+ "log": ` 0:05.21 REFTEST TEST-START | a
+ 0:05.21 REFTEST REFTEST TEST-LOAD | a | 0 / 1 (0%)
+ 0:05.27 REFTEST REFTEST TEST-LOAD | b | 0 / 1 (0%)
+ 0:05.66 REFTEST TEST-UNEXPECTED-FAIL | a | image comparison (==), max difference: 106, number of differing pixels: 800
+ 0:05.67 REFTEST REFTEST IMAGE 1 (TEST): data:image/png;base64,
+ 0:05.67 REFTEST REFTEST IMAGE 2 (REFERENCE): data:image/png;base64,
+ 0:05.73 REFTEST REFTEST TEST-END | a`,
+ "expected": { "pass": 0, "unexpected": 1, "random": 0, "skip": 0 },
+ "expected_images": 2,
+ },
+ { "name": "reftest run with whitespace compressed (Treeherder; Bug 1084322)",
+ "log": ` REFTEST TEST-START | a
+REFTEST TEST-LOAD | a | 0 / 1 (0%)
+REFTEST TEST-LOAD | b | 0 / 1 (0%)
+REFTEST TEST-UNEXPECTED-FAIL | a | image comparison (==), max difference: 106, number of differing pixels: 800
+REFTEST REFTEST IMAGE 1 (TEST): data:image/png;base64,
+REFTEST REFTEST IMAGE 2 (REFERENCE): data:image/png;base64,
+REFTEST REFTEST TEST-END | a`,
+ "expected": { "pass": 0, "unexpected": 1, "random": 0, "skip": 0 },
+ "expected_images": 2,
+ },
+ ];
+
+ var current_test = 0;
+
+ // Override the build_viewer function invoked at the end of process_log to
+ // actually just check the results of parsing.
+ build_viewer = function() {
+ var expected = testcases[current_test].expected;
+ var expected_images = testcases[current_test].expected_images;
+ for (var result of gTestItems) {
+ for (let type in expected) { // type is "pass", "unexpected" etc.
+ if (result[type]) {
+ expected[type]--;
+ }
+ }
+ }
+ var failed = false;
+ for (let type in expected) {
+ if (expected[type] != 0) {
+ console.log(`Failure: for testcase ${testcases[current_test].name} got ${expected[type]} fewer ${type} results than expected!`);
+ failed = true;
+ }
+ }
+
+ let total_images = 0;
+ for (var result of gTestItems) {
+ total_images += result.images.length;
+ }
+ if (total_images !== expected_images) {
+ console.log(`Failure: for testcase ${testcases[current_test].name} got ${total_images} images, expected ${expected_images}`);
+ failed = true;
+ }
+
+ if (!failed) {
+ console.log(`Success for testcase ${testcases[current_test].name}`);
+ }
+ };
+
+ while (current_test < testcases.length) {
+ process_log(testcases[current_test].log);
+ current_test++;
+ }
+}
+
+function process_log(contents) {
+ var lines = contents.split(/[\r\n]+/);
+ gTestItems = [];
+ for (var j in lines) {
+
+ // !!!!!!
+ // When making any changes to this code, please add a test to the
+ // test_parsing function above, and ensure all existing tests pass.
+ // !!!!!!
+
+ var line = lines[j];
+ // Ignore duplicated output in logcat.
+ if (line.match(/I\/Gecko.*?REFTEST/))
+ continue;
+ var match = line.match(/^.*?(?:REFTEST\s+)+(.*)$/);
+ if (!match) {
+ // WPT reftests don't always have the "REFTEST" prefix but do have
+ // mozharness prefixing. Trying to match both prefixes optionally with a
+ // single regex either makes an unreadable mess or matches everything so
+ // we do them separately.
+ match = line.match(/^(?:.*? (?:INFO|ERROR) -\s+)(.*)$/);
+ }
+ if (match)
+ line = match[1];
+ match = line.match(/^(TEST-PASS|TEST-UNEXPECTED-PASS|TEST-FAIL|TEST-KNOWN-FAIL|TEST-UNEXPECTED-FAIL|TEST-DEBUG-INFO|TEST-KNOWN-INTERMITTENT-FAIL|TEST-KNOWN-INTERMITTENT-PASS)(\(EXPECTED RANDOM\)|) \| ([^\|]+)(?: \|(.*)|$)/);
+ if (match) {
+ var state = match[1];
+ var random = match[2];
+ var url = match[3];
+ var extra = match[4];
+ gTestItems.push(
+ {
+ pass: !state.match(/DEBUG-INFO$|FAIL$/),
+ // only one of the following three should ever be true
+ unexpected: !!state.match(/^TEST-UNEXPECTED/),
+ random: (random == "(EXPECTED RANDOM)" || state == "TEST-KNOWN-INTERMITTENT-FAIL" || state == "TEST-KNOWN-INTERMITTENT-PASS"),
+ skip: (extra == " (SKIP)"),
+ url: url,
+ images: [],
+ imageLabels: []
+ });
+ continue;
+ }
+ match = line.match(/^IMAGE([^:]*): (data:.*)$/);
+ if (match) {
+ var item = gTestItems[gTestItems.length - 1];
+ item.images.push(match[2]);
+ item.imageLabels.push(match[1]);
+ }
+ }
+
+ build_viewer();
+}
+
+function build_viewer() {
+ if (gTestItems.length == 0) {
+ show_phase("entry");
+ return;
+ }
+
+ var cell = ID("itemlist");
+ while (cell.childNodes.length > 0)
+ cell.removeChild(cell.childNodes[cell.childNodes.length - 1]);
+
+ var table = document.createElement("table");
+ var tbody = document.createElement("tbody");
+ table.appendChild(tbody);
+
+ for (var i in gTestItems) {
+ var item = gTestItems[i];
+
+ // optional url filter for only showing unexpected results
+ if (parseInt(gParams.only_show_unexpected) && !item.unexpected)
+ continue;
+
+ // XXX regardless skip expected pass items until we have filtering UI
+ if (item.pass && !item.unexpected)
+ continue;
+
+ var tr = document.createElement("tr");
+ var rowclass = item.pass ? "pass" : "fail";
+ var td;
+ var text;
+
+ td = document.createElement("td");
+ text = "";
+ if (item.unexpected) { text += "!"; rowclass += " unexpected"; }
+ if (item.random) { text += "R"; rowclass += " random"; }
+ if (item.skip) { text += "S"; rowclass += " skip"; }
+ td.appendChild(document.createTextNode(text));
+ tr.appendChild(td);
+
+ td = document.createElement("td");
+ td.id = "item" + i;
+ td.className = "url";
+ // Only display part of URL after "/mozilla/".
+ var match = item.url.match(/\/mozilla\/(.*)/);
+ text = document.createTextNode(match ? match[1] : item.url);
+ if (item.images.length > 0) {
+ var a = document.createElement("a");
+ a.href = "javascript:show_images(" + i + ")";
+ a.appendChild(text);
+ td.appendChild(a);
+ } else {
+ td.appendChild(text);
+ }
+ tr.appendChild(td);
+
+ tbody.appendChild(tr);
+ }
+
+ cell.appendChild(table);
+
+ show_phase("viewer");
+}
+
+function get_image_data(src, whenReady) {
+ var img = new Image();
+ img.onload = function() {
+ var canvas = document.createElement("canvas");
+ canvas.width = img.naturalWidth;
+ canvas.height = img.naturalHeight;
+
+ var ctx = canvas.getContext("2d");
+ ctx.drawImage(img, 0, 0);
+
+ whenReady(ctx.getImageData(0, 0, img.naturalWidth, img.naturalHeight));
+ };
+ img.src = src;
+}
+
+function sync_svg_size(imageData) {
+ // We need the size of the 'svg' and its 'image' elements to match the size
+ // of the ImageData objects that we're going to read pixels from or else our
+ // magnify() function will be very broken.
+ ID("svg").setAttribute("width", imageData.width);
+ ID("svg").setAttribute("height", imageData.height);
+}
+
+function show_images(i) {
+ var item = gTestItems[i];
+ var cell = ID("images");
+
+ // Remove activeitem class from any existing elements
+ var activeItems = document.querySelectorAll(".activeitem");
+ for (var activeItemIdx = activeItems.length; activeItemIdx-- != 0;) {
+ activeItems[activeItemIdx].classList.remove("activeitem");
+ }
+
+ ID("item" + i).classList.add("activeitem");
+ ID("image1").style.display = "";
+ ID("image2").style.display = "none";
+ ID("diffrect").style.display = "none";
+ ID("imgcontrols").reset();
+ ID("pixel-differences").textContent = "";
+
+ ID("image1").setAttributeNS(XLINK_NS, "xlink:href", item.images[0]);
+ // Making the href be #image1 doesn't seem to work
+ ID("feimage1").setAttributeNS(XLINK_NS, "xlink:href", item.images[0]);
+ if (item.images.length == 1) {
+ ID("imgcontrols").style.display = "none";
+ } else {
+ ID("imgcontrols").style.display = "";
+
+ ID("image2").setAttributeNS(XLINK_NS, "xlink:href", item.images[1]);
+ // Making the href be #image2 doesn't seem to work
+ ID("feimage2").setAttributeNS(XLINK_NS, "xlink:href", item.images[1]);
+
+ ID("label1").textContent = 'Image ' + item.imageLabels[0];
+ ID("label2").textContent = 'Image ' + item.imageLabels[1];
+ }
+
+ cell.style.display = "";
+
+ let loaded = [false, false];
+
+ function images_loaded(id) {
+ loaded[id] = true;
+ if (loaded.every(x => x)) {
+ update_pixel_difference_text()
+ }
+ }
+
+ get_image_data(item.images[0], function(data) { gImage1Data = data; sync_svg_size(gImage1Data); images_loaded(0)});
+ get_image_data(item.images[1], function(data) { gImage2Data = data; images_loaded(1)});
+
+}
+
+function update_pixel_difference_text() {
+ let differenceText;
+ if (gImage1Data.height !== gImage2Data.height ||
+ gImage1Data.width !== gImage2Data.width) {
+ differenceText = "Images are different sizes"
+ } else {
+ let [numPixels, maxPerChannel] = get_pixel_differences();
+ if (!numPixels) {
+ differenceText = "Images are identical";
+ } else {
+ differenceText = `Maximum difference per channel ${maxPerChannel}, ${numPixels} pixels differ`;
+ }
+ }
+ // Disable this for now, because per bug 1633504, the numbers may be
+ // inaccurate and dependent on the browser's configuration.
+ // ID("pixel-differences").textContent = differenceText;
+}
+
+function get_pixel_differences() {
+ let numPixels = 0;
+ let maxPerChannel = 0;
+ for (var i=0; i<gImage1Data.data.length; i+=4) {
+ let r1 = gImage1Data.data[i];
+ let r2 = gImage2Data.data[i];
+ let g1 = gImage1Data.data[i+1];
+ let g2 = gImage2Data.data[i+1];
+ let b1 = gImage1Data.data[i+2];
+ let b2 = gImage2Data.data[i+2];
+ // Ignore alpha.
+ if (r1 == r2 && g1 == g2 && b1 == b2) {
+ continue;
+ }
+ numPixels += 1;
+ let maxDiff = Math.max(Math.abs(r1-r2),
+ Math.abs(g1-g2),
+ Math.abs(b1-b2));
+ if (maxDiff > maxPerChannel) {
+ maxPerChannel = maxDiff
+ }
+ }
+ return [numPixels, maxPerChannel];
+}
+
+function show_image(i) {
+ if (i == 1) {
+ ID("image1").style.display = "";
+ ID("image2").style.display = "none";
+ } else {
+ ID("image1").style.display = "none";
+ ID("image2").style.display = "";
+ }
+}
+
+function handle_keyboard_shortcut(event) {
+ switch (event.charCode) {
+ case 49: // "1" key
+ document.getElementById("radio1").checked = true;
+ show_image(1);
+ break;
+ case 50: // "2" key
+ document.getElementById("radio2").checked = true;
+ show_image(2);
+ break;
+ case 100: // "d" key
+ document.getElementById("differences").click();
+ break;
+ case 112: // "p" key
+ shift_images(-1);
+ break;
+ case 110: // "n" key
+ shift_images(1);
+ break;
+ }
+}
+
+function handle_keydown(event) {
+ switch (event.keyCode) {
+ case 37: // left arrow
+ move_pixel(-1, 0);
+ break;
+ case 38: // up arrow
+ move_pixel(0,-1);
+ break;
+ case 39: // right arrow
+ move_pixel(1, 0);
+ break;
+ case 40: // down arrow
+ move_pixel(0, 1);
+ break;
+ }
+}
+
+function shift_images(dir) {
+ var activeItem = document.querySelector(".activeitem");
+ if (!activeItem) {
+ return;
+ }
+ for (var elm = activeItem; elm; elm = elm.parentElement) {
+ if (elm.tagName != "tr") {
+ continue;
+ }
+ elm = dir > 0 ? elm.nextElementSibling : elm.previousElementSibling;
+ if (elm) {
+ elm.getElementsByTagName("a")[0].click();
+ }
+ return;
+ }
+}
+
+function show_differences(cb) {
+ ID("diffrect").style.display = cb.checked ? "" : "none";
+}
+
+function flash_pixels(on) {
+ var stroke = on ? "red" : "black";
+ var strokeWidth = on ? "2px" : "1px";
+ for (var i = 0; i < gFlashingPixels.length; i++) {
+ gFlashingPixels[i].setAttribute("stroke", stroke);
+ gFlashingPixels[i].setAttribute("stroke-width", strokeWidth);
+ }
+}
+
+function cursor_point(evt) {
+ var m = evt.target.getScreenCTM().inverse();
+ var p = ID("svg").createSVGPoint();
+ p.x = evt.clientX;
+ p.y = evt.clientY;
+ p = p.matrixTransform(m);
+ return { x: Math.floor(p.x), y: Math.floor(p.y) };
+}
+
+function hex2(i) {
+ return (i < 16 ? "0" : "") + i.toString(16);
+}
+
+function canvas_pixel_as_hex(data, x, y) {
+ var offset = (y * data.width + x) * 4;
+ var r = data.data[offset];
+ var g = data.data[offset + 1];
+ var b = data.data[offset + 2];
+ return "#" + hex2(r) + hex2(g) + hex2(b);
+}
+
+function hex_as_rgb(hex) {
+ return "rgb(" + [parseInt(hex.substring(1, 3), 16), parseInt(hex.substring(3, 5), 16), parseInt(hex.substring(5, 7), 16)] + ")";
+}
+
+function magnify(evt) {
+ var { x: x, y: y } = cursor_point(evt);
+ do_magnify(x, y);
+}
+
+function do_magnify(x, y) {
+ var centerPixelColor1, centerPixelColor2;
+
+ var dx_lo = -Math.floor(gMagWidth / 2);
+ var dx_hi = Math.floor(gMagWidth / 2);
+ var dy_lo = -Math.floor(gMagHeight / 2);
+ var dy_hi = Math.floor(gMagHeight / 2);
+
+ flash_pixels(false);
+ gFlashingPixels = [];
+ for (var j = dy_lo; j <= dy_hi; j++) {
+ for (var i = dx_lo; i <= dx_hi; i++) {
+ var px = x + i;
+ var py = y + j;
+ var p1 = gMagPixPaths[i + dx_hi][j + dy_hi][0];
+ var p2 = gMagPixPaths[i + dx_hi][j + dy_hi][1];
+ // Here we just use the dimensions of gImage1Data since we expect test
+ // and reference to have the same dimensions.
+ if (px < 0 || py < 0 || px >= gImage1Data.width || py >= gImage1Data.height) {
+ p1.setAttribute("fill", "#aaa");
+ p2.setAttribute("fill", "#888");
+ } else {
+ var color1 = canvas_pixel_as_hex(gImage1Data, x + i, y + j);
+ var color2 = canvas_pixel_as_hex(gImage2Data, x + i, y + j);
+ p1.setAttribute("fill", color1);
+ p2.setAttribute("fill", color2);
+ if (color1 != color2) {
+ gFlashingPixels.push(p1, p2);
+ p1.parentNode.appendChild(p1);
+ p2.parentNode.appendChild(p2);
+ }
+ if (i == 0 && j == 0) {
+ centerPixelColor1 = color1;
+ centerPixelColor2 = color2;
+ }
+ }
+ }
+ }
+ flash_pixels(true);
+ show_pixelinfo(x, y, centerPixelColor1, hex_as_rgb(centerPixelColor1), centerPixelColor2, hex_as_rgb(centerPixelColor2));
+}
+
+function show_pixelinfo(x, y, pix1rgb, pix1hex, pix2rgb, pix2hex) {
+ var pixelinfo = ID("pixelinfo");
+ ID("coords").textContent = [x, y];
+ ID("pix1hex").textContent = pix1hex;
+ ID("pix1rgb").textContent = pix1rgb;
+ ID("pix2hex").textContent = pix2hex;
+ ID("pix2rgb").textContent = pix2rgb;
+}
+
+function move_pixel(deltax, deltay) {
+ coords = ID("coords").textContent.split(',');
+ x = parseInt(coords[0]);
+ y = parseInt(coords[1]);
+ if (isNaN(x) || isNaN(y)) {
+ return;
+ }
+ x = x + deltax;
+ y = y + deltay;
+ if (x >= 0 && y >= 0 && x < gImage1Data.width && y < gImage1Data.height) {
+ do_magnify(x, y);
+ }
+}
+
+ ]]></script>
+
+</head>
+<body onload="load()">
+
+<div id="entry">
+
+<h1>Reftest analyzer: load reftest log</h1>
+
+<p>Either paste your log into this textarea:<br />
+<textarea cols="80" rows="10" id="logentry"/><br/>
+<input type="button" value="Process pasted log" onclick="log_pasted()" /></p>
+
+<p>... or load it from a file:<br/>
+<input type="file" id="fileentry" onchange="fileentry_changed()" />
+</p>
+</div>
+
+<div id="loading" style="display:none">Loading log...</div>
+
+<div id="viewer" style="display:none">
+ <div id="pixelarea">
+ <div id="pixelinfo">
+ <table>
+ <tbody>
+ <tr><th>Pixel at:</th><td colspan="2" id="coords"/></tr>
+ <tr><th>Image 1:</th><td id="pix1rgb"></td><td id="pix1hex"></td></tr>
+ <tr><th>Image 2:</th><td id="pix2rgb"></td><td id="pix2hex"></td></tr>
+ </tbody>
+ </table>
+ <div>
+ <div id="pixelhint">★
+ <div>
+ <p>Move the mouse over the reftest image on the right to show
+ magnified pixels on the left. The color information above is for
+ the pixel centered in the magnified view.</p>
+ <p>Image 1 is shown in the upper triangle of each pixel and Image 2
+ is shown in the lower triangle.</p>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div id="magnification">
+ <svg xmlns="http://www.w3.org/2000/svg" width="84" height="84" shape-rendering="optimizeSpeed">
+ <g id="mag"/>
+ </svg>
+ </div>
+ </div>
+ <div id="itemlist"></div>
+ <div id="images" style="display:none">
+ <form id="imgcontrols">
+ <input id="radio1" type="radio" name="which" value="0" onchange="show_image(1)" checked="checked" /><label id="label1" title="1" for="radio1">Image 1</label>
+ <input id="radio2" type="radio" name="which" value="1" onchange="show_image(2)" /><label id="label2" title="2" for="radio2">Image 2</label>
+ <label><input id="differences" type="checkbox" onchange="show_differences(this)" />Circle differences</label>
+ </form>
+ <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="800" height="1000" id="svg">
+ <defs>
+ <!-- use sRGB to avoid loss of data -->
+ <filter id="showDifferences" x="0%" y="0%" width="100%" height="100%"
+ style="color-interpolation-filters: sRGB">
+ <feImage id="feimage1" result="img1" xlink:href="#image1" />
+ <feImage id="feimage2" result="img2" xlink:href="#image2" />
+ <!-- inv1 and inv2 are the images with RGB inverted -->
+ <feComponentTransfer result="inv1" in="img1">
+ <feFuncR type="linear" slope="-1" intercept="1" />
+ <feFuncG type="linear" slope="-1" intercept="1" />
+ <feFuncB type="linear" slope="-1" intercept="1" />
+ </feComponentTransfer>
+ <feComponentTransfer result="inv2" in="img2">
+ <feFuncR type="linear" slope="-1" intercept="1" />
+ <feFuncG type="linear" slope="-1" intercept="1" />
+ <feFuncB type="linear" slope="-1" intercept="1" />
+ </feComponentTransfer>
+ <!-- w1 will have non-white pixels anywhere that img2
+ is brighter than img1, and w2 for the reverse.
+ It would be nice not to have to go through these
+ intermediate states, but feComposite
+ type="arithmetic" can't transform the RGB channels
+ and leave the alpha channel untouched. -->
+ <feComposite result="w1" in="img1" in2="inv2" operator="arithmetic" k2="1" k3="1" />
+ <feComposite result="w2" in="img2" in2="inv1" operator="arithmetic" k2="1" k3="1" />
+ <!-- c1 will have non-black pixels anywhere that img2
+ is brighter than img1, and c2 for the reverse -->
+ <feComponentTransfer result="c1" in="w1">
+ <feFuncR type="linear" slope="-1" intercept="1" />
+ <feFuncG type="linear" slope="-1" intercept="1" />
+ <feFuncB type="linear" slope="-1" intercept="1" />
+ </feComponentTransfer>
+ <feComponentTransfer result="c2" in="w2">
+ <feFuncR type="linear" slope="-1" intercept="1" />
+ <feFuncG type="linear" slope="-1" intercept="1" />
+ <feFuncB type="linear" slope="-1" intercept="1" />
+ </feComponentTransfer>
+ <!-- c will be nonblack (and fully on) for every pixel+component where there are differences -->
+ <feComposite result="c" in="c1" in2="c2" operator="arithmetic" k2="255" k3="255" />
+ <!-- a will be opaque for every pixel with differences and transparent for all others -->
+ <feColorMatrix result="a" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 0 0" />
+
+ <!-- a, dilated by 1 pixel -->
+ <feMorphology result="dila1" in="a" operator="dilate" radius="1" />
+ <!-- a, dilated by 2 pixels -->
+ <feMorphology result="dila2" in="dila1" operator="dilate" radius="1" />
+
+ <!-- all the pixels in the 2-pixel dilation of a but not in the 1-pixel dilation, to highlight the diffs -->
+ <feComposite result="highlight" in="dila2" in2="dila1" operator="out" />
+
+ <feFlood result="red" flood-color="red" />
+ <feComposite result="redhighlight" in="red" in2="highlight" operator="in" />
+ <feFlood result="black" flood-color="black" flood-opacity="0.5" />
+ <feMerge>
+ <feMergeNode in="black" />
+ <feMergeNode in="redhighlight" />
+ </feMerge>
+ </filter>
+ </defs>
+ <g onmousemove="magnify(evt)">
+ <image x="0" y="0" width="100%" height="100%" id="image1" />
+ <image x="0" y="0" width="100%" height="100%" id="image2" />
+ </g>
+ <rect id="diffrect" filter="url(#showDifferences)" pointer-events="none" x="0" y="0" width="100%" height="100%" />
+ </svg>
+ <div id="pixel-differences"></div>
+ </div>
+</div>
+
+</body>
+</html>
diff --git a/test/wpt/tests/common/utils.js b/test/wpt/tests/common/utils.js
new file mode 100644
index 0000000..62e742b
--- /dev/null
+++ b/test/wpt/tests/common/utils.js
@@ -0,0 +1,98 @@
+/**
+ * Create an absolute URL from `options` and defaulting unspecified properties to `window.location`.
+ * @param {Object} options - a `Location`-like object
+ * @param {string} options.hostname
+ * @param {string} options.subdomain - prepend subdomain to the hostname
+ * @param {string} options.port
+ * @param {string} options.path
+ * @param {string} options.query
+ * @param {string} options.hash
+ * @returns {string}
+ */
+function make_absolute_url(options) {
+ var loc = window.location;
+ var protocol = get(options, "protocol", loc.protocol);
+ if (protocol[protocol.length - 1] != ":") {
+ protocol += ":";
+ }
+
+ var hostname = get(options, "hostname", loc.hostname);
+
+ var subdomain = get(options, "subdomain");
+ if (subdomain) {
+ hostname = subdomain + "." + hostname;
+ }
+
+ var port = get(options, "port", loc.port)
+ var path = get(options, "path", loc.pathname);
+ var query = get(options, "query", loc.search);
+ var hash = get(options, "hash", loc.hash)
+
+ var url = protocol + "//" + hostname;
+ if (port) {
+ url += ":" + port;
+ }
+
+ if (path[0] != "/") {
+ url += "/";
+ }
+ url += path;
+ if (query) {
+ if (query[0] != "?") {
+ url += "?";
+ }
+ url += query;
+ }
+ if (hash) {
+ if (hash[0] != "#") {
+ url += "#";
+ }
+ url += hash;
+ }
+ return url;
+}
+
+/** @private */
+function get(obj, name, default_val) {
+ if (obj.hasOwnProperty(name)) {
+ return obj[name];
+ }
+ return default_val;
+}
+
+/**
+ * Generate a new UUID.
+ * @returns {string}
+ */
+function token() {
+ var uuid = [to_hex(rand_int(32), 8),
+ to_hex(rand_int(16), 4),
+ to_hex(0x4000 | rand_int(12), 4),
+ to_hex(0x8000 | rand_int(14), 4),
+ to_hex(rand_int(48), 12)].join("-")
+ return uuid;
+}
+
+/** @private */
+function rand_int(bits) {
+ if (bits < 1 || bits > 53) {
+ throw new TypeError();
+ } else {
+ if (bits >= 1 && bits <= 30) {
+ return 0 | ((1 << bits) * Math.random());
+ } else {
+ var high = (0 | ((1 << (bits - 30)) * Math.random())) * (1 << 30);
+ var low = 0 | ((1 << 30) * Math.random());
+ return high + low;
+ }
+ }
+}
+
+/** @private */
+function to_hex(x, length) {
+ var rv = x.toString(16);
+ while (rv.length < length) {
+ rv = "0" + rv;
+ }
+ return rv;
+}
diff --git a/test/wpt/tests/common/utils.js.headers b/test/wpt/tests/common/utils.js.headers
new file mode 100644
index 0000000..6805c32
--- /dev/null
+++ b/test/wpt/tests/common/utils.js.headers
@@ -0,0 +1 @@
+Content-Type: text/javascript; charset=utf-8
diff --git a/test/wpt/tests/common/window-name-setter.html b/test/wpt/tests/common/window-name-setter.html
new file mode 100644
index 0000000..c0603aa
--- /dev/null
+++ b/test/wpt/tests/common/window-name-setter.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>A page that sets window.name</title>
+
+<script>
+"use strict";
+
+window.onload = () => {
+ window.name = location.hash.slice(1); // Drop the first '#' character.
+ window.name = "spices";
+};
+</script>
diff --git a/test/wpt/tests/common/worklet-reftest.js b/test/wpt/tests/common/worklet-reftest.js
new file mode 100644
index 0000000..e05d4ee
--- /dev/null
+++ b/test/wpt/tests/common/worklet-reftest.js
@@ -0,0 +1,50 @@
+/**
+ * Imports code into a worklet. E.g.
+ *
+ * importWorklet(CSS.paintWorklet, {url: 'script.js'});
+ * importWorklet(CSS.paintWorklet, '(javascript string)');
+ *
+ * @param {Worklet} worklet
+ * @param {(Object|string)} code
+ */
+function importWorklet(worklet, code) {
+ let url;
+ if (typeof code === 'object') {
+ url = code.url;
+ } else {
+ const blob = new Blob([code], {type: 'text/javascript'});
+ url = URL.createObjectURL(blob);
+ }
+
+ return worklet.addModule(url);
+}
+
+/** @private */
+async function animationFrames(frames) {
+ for (let i = 0; i < frames; i++)
+ await new Promise(requestAnimationFrame);
+}
+
+/** @private */
+async function workletPainted() {
+ await animationFrames(2);
+}
+
+/**
+ * To make sure that we take the snapshot at the right time, we do double
+ * requestAnimationFrame. In the second frame, we take a screenshot, that makes
+ * sure that we already have a full frame.
+ *
+ * @param {Worklet} worklet
+ * @param {(Object|string)} code
+ */
+async function importWorkletAndTerminateTestAfterAsyncPaint(worklet, code) {
+ if (typeof worklet === 'undefined') {
+ takeScreenshot();
+ return;
+ }
+
+ await importWorklet(worklet, code);
+ await workletPainted();
+ takeScreenshot();
+}
diff --git a/test/wpt/tests/common/worklet-reftest.js.headers b/test/wpt/tests/common/worklet-reftest.js.headers
new file mode 100644
index 0000000..6805c32
--- /dev/null
+++ b/test/wpt/tests/common/worklet-reftest.js.headers
@@ -0,0 +1 @@
+Content-Type: text/javascript; charset=utf-8
diff --git a/test/wpt/tests/fetch/META.yml b/test/wpt/tests/fetch/META.yml
new file mode 100644
index 0000000..81432ff
--- /dev/null
+++ b/test/wpt/tests/fetch/META.yml
@@ -0,0 +1,7 @@
+spec: https://fetch.spec.whatwg.org/
+suggested_reviewers:
+ - jdm
+ - youennf
+ - annevk
+ - mnot
+ - yutakahirano
diff --git a/test/wpt/tests/fetch/README.md b/test/wpt/tests/fetch/README.md
new file mode 100644
index 0000000..dcaad02
--- /dev/null
+++ b/test/wpt/tests/fetch/README.md
@@ -0,0 +1,6 @@
+Tests for the [Fetch Standard](https://fetch.spec.whatwg.org/).
+
+More Fetch tests can be found in
+
+* /cors
+* /xhr
diff --git a/test/wpt/tests/fetch/api/abort/cache.https.any.js b/test/wpt/tests/fetch/api/abort/cache.https.any.js
new file mode 100644
index 0000000..bdaf0e6
--- /dev/null
+++ b/test/wpt/tests/fetch/api/abort/cache.https.any.js
@@ -0,0 +1,47 @@
+// META: title=Request signals &amp; the cache API
+// META: global=window,worker
+
+promise_test(async () => {
+ await caches.delete('test');
+ const controller = new AbortController();
+ const signal = controller.signal;
+ const request = new Request('../resources/data.json', { signal });
+
+ const cache = await caches.open('test');
+ await cache.put(request, new Response(''));
+
+ const requests = await cache.keys();
+
+ assert_equals(requests.length, 1, 'Ensuring cleanup worked');
+
+ const [cachedRequest] = requests;
+
+ controller.abort();
+
+ assert_false(cachedRequest.signal.aborted, "Request from cache shouldn't be aborted");
+
+ const data = await fetch(cachedRequest).then(r => r.json());
+ assert_equals(data.key, 'value', 'Fetch fully completes');
+}, "Signals are not stored in the cache API");
+
+promise_test(async () => {
+ await caches.delete('test');
+ const controller = new AbortController();
+ const signal = controller.signal;
+ const request = new Request('../resources/data.json', { signal });
+ controller.abort();
+
+ const cache = await caches.open('test');
+ await cache.put(request, new Response(''));
+
+ const requests = await cache.keys();
+
+ assert_equals(requests.length, 1, 'Ensuring cleanup worked');
+
+ const [cachedRequest] = requests;
+
+ assert_false(cachedRequest.signal.aborted, "Request from cache shouldn't be aborted");
+
+ const data = await fetch(cachedRequest).then(r => r.json());
+ assert_equals(data.key, 'value', 'Fetch fully completes');
+}, "Signals are not stored in the cache API, even if they're already aborted");
diff --git a/test/wpt/tests/fetch/api/abort/destroyed-context.html b/test/wpt/tests/fetch/api/abort/destroyed-context.html
new file mode 100644
index 0000000..161d39b
--- /dev/null
+++ b/test/wpt/tests/fetch/api/abort/destroyed-context.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+// This is a regression test for crbug.com/860063.
+window.controller = new AbortController();
+async_test(t => {
+ onmessage = t.step_func(event => {
+ assert_equals(event.data, 'started');
+ const iframe = document.querySelector('iframe');
+ document.body.removeChild(iframe);
+ controller.abort();
+ t.done();
+ });
+}, 'aborting a fetch in a destroyed context should not crash');
+</script>
+<iframe srcdoc="
+ <!DOCTYPE html>
+ <meta charset=utf-8>
+ <script>
+ fetch('../resources/infinite-slow-response.py', { signal: parent.controller.signal }).then(() => {
+ parent.postMessage('started', '*');
+ });
+ </script>
+ ">
+</iframe>
diff --git a/test/wpt/tests/fetch/api/abort/general.any.js b/test/wpt/tests/fetch/api/abort/general.any.js
new file mode 100644
index 0000000..3727bb4
--- /dev/null
+++ b/test/wpt/tests/fetch/api/abort/general.any.js
@@ -0,0 +1,572 @@
+// META: timeout=long
+// META: global=window,worker
+// META: script=/common/utils.js
+// META: script=/common/get-host-info.sub.js
+// META: script=../request/request-error.js
+
+const BODY_METHODS = ['arrayBuffer', 'blob', 'formData', 'json', 'text'];
+
+const error1 = new Error('error1');
+error1.name = 'error1';
+
+// This is used to close connections that weren't correctly closed during the tests,
+// otherwise you can end up running out of HTTP connections.
+let requestAbortKeys = [];
+
+function abortRequests() {
+ const keys = requestAbortKeys;
+ requestAbortKeys = [];
+ return Promise.all(
+ keys.map(key => fetch(`../resources/stash-put.py?key=${key}&value=close`))
+ );
+}
+
+const hostInfo = get_host_info();
+const urlHostname = hostInfo.REMOTE_HOST;
+
+promise_test(async t => {
+ const controller = new AbortController();
+ const signal = controller.signal;
+ controller.abort();
+
+ const fetchPromise = fetch('../resources/data.json', { signal });
+
+ await promise_rejects_dom(t, "AbortError", fetchPromise);
+}, "Aborting rejects with AbortError");
+
+promise_test(async t => {
+ const controller = new AbortController();
+ const signal = controller.signal;
+ controller.abort(error1);
+
+ const fetchPromise = fetch('../resources/data.json', { signal });
+
+ await promise_rejects_exactly(t, error1, fetchPromise, 'fetch() should reject with abort reason');
+}, "Aborting rejects with abort reason");
+
+promise_test(async t => {
+ const controller = new AbortController();
+ const signal = controller.signal;
+ controller.abort();
+
+ const url = new URL('../resources/data.json', location);
+ url.hostname = urlHostname;
+
+ const fetchPromise = fetch(url, {
+ signal,
+ mode: 'no-cors'
+ });
+
+ await promise_rejects_dom(t, "AbortError", fetchPromise);
+}, "Aborting rejects with AbortError - no-cors");
+
+// Test that errors thrown from the request constructor take priority over abort errors.
+// badRequestArgTests is from response-error.js
+for (const { args, testName } of badRequestArgTests) {
+ promise_test(async t => {
+ try {
+ // If this doesn't throw, we'll effectively skip the test.
+ // It'll fail properly in ../request/request-error.html
+ new Request(...args);
+ }
+ catch (err) {
+ const controller = new AbortController();
+ controller.abort();
+
+ // Add signal to 2nd arg
+ args[1] = args[1] || {};
+ args[1].signal = controller.signal;
+ await promise_rejects_js(t, TypeError, fetch(...args));
+ }
+ }, `TypeError from request constructor takes priority - ${testName}`);
+}
+
+test(() => {
+ const request = new Request('');
+ assert_true(Boolean(request.signal), "Signal member is present & truthy");
+ assert_equals(request.signal.constructor, AbortSignal);
+}, "Request objects have a signal property");
+
+promise_test(async t => {
+ const controller = new AbortController();
+ const signal = controller.signal;
+ controller.abort();
+
+ const request = new Request('../resources/data.json', { signal });
+
+ assert_true(Boolean(request.signal), "Signal member is present & truthy");
+ assert_equals(request.signal.constructor, AbortSignal);
+ assert_not_equals(request.signal, signal, 'Request has a new signal, not a reference');
+ assert_true(request.signal.aborted, `Request's signal has aborted`);
+
+ const fetchPromise = fetch(request);
+
+ await promise_rejects_dom(t, "AbortError", fetchPromise);
+}, "Signal on request object");
+
+promise_test(async t => {
+ const controller = new AbortController();
+ const signal = controller.signal;
+ controller.abort(error1);
+
+ const request = new Request('../resources/data.json', { signal });
+
+ assert_not_equals(request.signal, signal, 'Request has a new signal, not a reference');
+ assert_true(request.signal.aborted, `Request's signal has aborted`);
+ assert_equals(request.signal.reason, error1, `Request's signal's abort reason is error1`);
+
+ const fetchPromise = fetch(request);
+
+ await promise_rejects_exactly(t, error1, fetchPromise, "fetch() should reject with abort reason");
+}, "Signal on request object should also have abort reason");
+
+promise_test(async t => {
+ const controller = new AbortController();
+ const signal = controller.signal;
+ controller.abort();
+
+ const request = new Request('../resources/data.json', { signal });
+ const requestFromRequest = new Request(request);
+
+ const fetchPromise = fetch(requestFromRequest);
+
+ await promise_rejects_dom(t, "AbortError", fetchPromise);
+}, "Signal on request object created from request object");
+
+promise_test(async t => {
+ const controller = new AbortController();
+ const signal = controller.signal;
+ controller.abort();
+
+ const request = new Request('../resources/data.json');
+ const requestFromRequest = new Request(request, { signal });
+
+ const fetchPromise = fetch(requestFromRequest);
+
+ await promise_rejects_dom(t, "AbortError", fetchPromise);
+}, "Signal on request object created from request object, with signal on second request");
+
+promise_test(async t => {
+ const controller = new AbortController();
+ const signal = controller.signal;
+ controller.abort();
+
+ const request = new Request('../resources/data.json', { signal: new AbortController().signal });
+ const requestFromRequest = new Request(request, { signal });
+
+ const fetchPromise = fetch(requestFromRequest);
+
+ await promise_rejects_dom(t, "AbortError", fetchPromise);
+}, "Signal on request object created from request object, with signal on second request overriding another");
+
+promise_test(async t => {
+ const controller = new AbortController();
+ const signal = controller.signal;
+ controller.abort();
+
+ const request = new Request('../resources/data.json', { signal });
+
+ const fetchPromise = fetch(request, {method: 'POST'});
+
+ await promise_rejects_dom(t, "AbortError", fetchPromise);
+}, "Signal retained after unrelated properties are overridden by fetch");
+
+promise_test(async t => {
+ const controller = new AbortController();
+ const signal = controller.signal;
+ controller.abort();
+
+ const request = new Request('../resources/data.json', { signal });
+
+ const data = await fetch(request, { signal: null }).then(r => r.json());
+ assert_equals(data.key, 'value', 'Fetch fully completes');
+}, "Signal removed by setting to null");
+
+promise_test(async t => {
+ const controller = new AbortController();
+ const signal = controller.signal;
+ controller.abort();
+
+ const log = [];
+
+ await Promise.all([
+ fetch('../resources/data.json', { signal }).then(
+ () => assert_unreached("Fetch must not resolve"),
+ () => log.push('fetch-reject')
+ ),
+ Promise.resolve().then(() => log.push('next-microtask'))
+ ]);
+
+ assert_array_equals(log, ['fetch-reject', 'next-microtask']);
+}, "Already aborted signal rejects immediately");
+
+promise_test(async t => {
+ const controller = new AbortController();
+ const signal = controller.signal;
+ controller.abort();
+
+ const request = new Request('../resources/data.json', {
+ signal,
+ method: 'POST',
+ body: 'foo',
+ headers: { 'Content-Type': 'text/plain' }
+ });
+
+ await fetch(request).catch(() => {});
+
+ assert_true(request.bodyUsed, "Body has been used");
+}, "Request is still 'used' if signal is aborted before fetching");
+
+for (const bodyMethod of BODY_METHODS) {
+ promise_test(async t => {
+ const controller = new AbortController();
+ const signal = controller.signal;
+
+ const log = [];
+ const response = await fetch('../resources/data.json', { signal });
+
+ controller.abort();
+
+ const bodyPromise = response[bodyMethod]();
+
+ await Promise.all([
+ bodyPromise.catch(() => log.push(`${bodyMethod}-reject`)),
+ Promise.resolve().then(() => log.push('next-microtask'))
+ ]);
+
+ await promise_rejects_dom(t, "AbortError", bodyPromise);
+
+ assert_array_equals(log, [`${bodyMethod}-reject`, 'next-microtask']);
+ }, `response.${bodyMethod}() rejects if already aborted`);
+}
+
+promise_test(async (t) => {
+ const controller = new AbortController();
+ const signal = controller.signal;
+
+ const res = await fetch('../resources/data.json', { signal });
+ controller.abort();
+
+ await promise_rejects_dom(t, 'AbortError', res.text());
+ await promise_rejects_dom(t, 'AbortError', res.text());
+}, 'Call text() twice on aborted response');
+
+promise_test(async t => {
+ await abortRequests();
+
+ const controller = new AbortController();
+ const signal = controller.signal;
+ const stateKey = token();
+ const abortKey = token();
+ requestAbortKeys.push(abortKey);
+ controller.abort();
+
+ await fetch(`../resources/infinite-slow-response.py?stateKey=${stateKey}&abortKey=${abortKey}`, { signal }).catch(() => {});
+
+ // I'm hoping this will give the browser enough time to (incorrectly) make the request
+ // above, if it intends to.
+ await fetch('../resources/data.json').then(r => r.json());
+
+ const response = await fetch(`../resources/stash-take.py?key=${stateKey}`);
+ const data = await response.json();
+
+ assert_equals(data, null, "Request hasn't been made to the server");
+}, "Already aborted signal does not make request");
+
+promise_test(async t => {
+ await abortRequests();
+
+ const controller = new AbortController();
+ const signal = controller.signal;
+ controller.abort();
+
+ const fetches = [];
+
+ for (let i = 0; i < 3; i++) {
+ const abortKey = token();
+ requestAbortKeys.push(abortKey);
+
+ fetches.push(
+ fetch(`../resources/infinite-slow-response.py?${i}&abortKey=${abortKey}`, { signal })
+ );
+ }
+
+ for (const fetchPromise of fetches) {
+ await promise_rejects_dom(t, "AbortError", fetchPromise);
+ }
+}, "Already aborted signal can be used for many fetches");
+
+promise_test(async t => {
+ await abortRequests();
+
+ const controller = new AbortController();
+ const signal = controller.signal;
+
+ await fetch('../resources/data.json', { signal }).then(r => r.json());
+
+ controller.abort();
+
+ const fetches = [];
+
+ for (let i = 0; i < 3; i++) {
+ const abortKey = token();
+ requestAbortKeys.push(abortKey);
+
+ fetches.push(
+ fetch(`../resources/infinite-slow-response.py?${i}&abortKey=${abortKey}`, { signal })
+ );
+ }
+
+ for (const fetchPromise of fetches) {
+ await promise_rejects_dom(t, "AbortError", fetchPromise);
+ }
+}, "Signal can be used to abort other fetches, even if another fetch succeeded before aborting");
+
+promise_test(async t => {
+ await abortRequests();
+
+ const controller = new AbortController();
+ const signal = controller.signal;
+ const stateKey = token();
+ const abortKey = token();
+ requestAbortKeys.push(abortKey);
+
+ await fetch(`../resources/infinite-slow-response.py?stateKey=${stateKey}&abortKey=${abortKey}`, { signal });
+
+ const beforeAbortResult = await fetch(`../resources/stash-take.py?key=${stateKey}`).then(r => r.json());
+ assert_equals(beforeAbortResult, "open", "Connection is open");
+
+ controller.abort();
+
+ // The connection won't close immediately, but it should close at some point:
+ const start = Date.now();
+
+ while (true) {
+ // Stop spinning if 10 seconds have passed
+ if (Date.now() - start > 10000) throw Error('Timed out');
+
+ const afterAbortResult = await fetch(`../resources/stash-take.py?key=${stateKey}`).then(r => r.json());
+ if (afterAbortResult == 'closed') break;
+ }
+}, "Underlying connection is closed when aborting after receiving response");
+
+promise_test(async t => {
+ await abortRequests();
+
+ const controller = new AbortController();
+ const signal = controller.signal;
+ const stateKey = token();
+ const abortKey = token();
+ requestAbortKeys.push(abortKey);
+
+ const url = new URL(`../resources/infinite-slow-response.py?stateKey=${stateKey}&abortKey=${abortKey}`, location);
+ url.hostname = urlHostname;
+
+ await fetch(url, {
+ signal,
+ mode: 'no-cors'
+ });
+
+ const stashTakeURL = new URL(`../resources/stash-take.py?key=${stateKey}`, location);
+ stashTakeURL.hostname = urlHostname;
+
+ const beforeAbortResult = await fetch(stashTakeURL).then(r => r.json());
+ assert_equals(beforeAbortResult, "open", "Connection is open");
+
+ controller.abort();
+
+ // The connection won't close immediately, but it should close at some point:
+ const start = Date.now();
+
+ while (true) {
+ // Stop spinning if 10 seconds have passed
+ if (Date.now() - start > 10000) throw Error('Timed out');
+
+ const afterAbortResult = await fetch(stashTakeURL).then(r => r.json());
+ if (afterAbortResult == 'closed') break;
+ }
+}, "Underlying connection is closed when aborting after receiving response - no-cors");
+
+for (const bodyMethod of BODY_METHODS) {
+ promise_test(async t => {
+ await abortRequests();
+
+ const controller = new AbortController();
+ const signal = controller.signal;
+ const stateKey = token();
+ const abortKey = token();
+ requestAbortKeys.push(abortKey);
+
+ const response = await fetch(`../resources/infinite-slow-response.py?stateKey=${stateKey}&abortKey=${abortKey}`, { signal });
+
+ const beforeAbortResult = await fetch(`../resources/stash-take.py?key=${stateKey}`).then(r => r.json());
+ assert_equals(beforeAbortResult, "open", "Connection is open");
+
+ const bodyPromise = response[bodyMethod]();
+
+ controller.abort();
+
+ await promise_rejects_dom(t, "AbortError", bodyPromise);
+
+ const start = Date.now();
+
+ while (true) {
+ // Stop spinning if 10 seconds have passed
+ if (Date.now() - start > 10000) throw Error('Timed out');
+
+ const afterAbortResult = await fetch(`../resources/stash-take.py?key=${stateKey}`).then(r => r.json());
+ if (afterAbortResult == 'closed') break;
+ }
+ }, `Fetch aborted & connection closed when aborted after calling response.${bodyMethod}()`);
+}
+
+promise_test(async t => {
+ await abortRequests();
+
+ const controller = new AbortController();
+ const signal = controller.signal;
+ const stateKey = token();
+ const abortKey = token();
+ requestAbortKeys.push(abortKey);
+
+ const response = await fetch(`../resources/infinite-slow-response.py?stateKey=${stateKey}&abortKey=${abortKey}`, { signal });
+ const reader = response.body.getReader();
+
+ controller.abort();
+
+ await promise_rejects_dom(t, "AbortError", reader.read());
+ await promise_rejects_dom(t, "AbortError", reader.closed);
+
+ // The connection won't close immediately, but it should close at some point:
+ const start = Date.now();
+
+ while (true) {
+ // Stop spinning if 10 seconds have passed
+ if (Date.now() - start > 10000) throw Error('Timed out');
+
+ const afterAbortResult = await fetch(`../resources/stash-take.py?key=${stateKey}`).then(r => r.json());
+ if (afterAbortResult == 'closed') break;
+ }
+}, "Stream errors once aborted. Underlying connection closed.");
+
+promise_test(async t => {
+ await abortRequests();
+
+ const controller = new AbortController();
+ const signal = controller.signal;
+ const stateKey = token();
+ const abortKey = token();
+ requestAbortKeys.push(abortKey);
+
+ const response = await fetch(`../resources/infinite-slow-response.py?stateKey=${stateKey}&abortKey=${abortKey}`, { signal });
+ const reader = response.body.getReader();
+
+ await reader.read();
+
+ controller.abort();
+
+ await promise_rejects_dom(t, "AbortError", reader.read());
+ await promise_rejects_dom(t, "AbortError", reader.closed);
+
+ // The connection won't close immediately, but it should close at some point:
+ const start = Date.now();
+
+ while (true) {
+ // Stop spinning if 10 seconds have passed
+ if (Date.now() - start > 10000) throw Error('Timed out');
+
+ const afterAbortResult = await fetch(`../resources/stash-take.py?key=${stateKey}`).then(r => r.json());
+ if (afterAbortResult == 'closed') break;
+ }
+}, "Stream errors once aborted, after reading. Underlying connection closed.");
+
+promise_test(async t => {
+ await abortRequests();
+
+ const controller = new AbortController();
+ const signal = controller.signal;
+
+ const response = await fetch(`../resources/empty.txt`, { signal });
+
+ // Read whole response to ensure close signal has sent.
+ await response.clone().text();
+
+ const reader = response.body.getReader();
+
+ controller.abort();
+
+ const item = await reader.read();
+
+ assert_true(item.done, "Stream is done");
+}, "Stream will not error if body is empty. It's closed with an empty queue before it errors.");
+
+promise_test(async t => {
+ const controller = new AbortController();
+ const signal = controller.signal;
+ controller.abort();
+
+ let cancelReason;
+
+ const body = new ReadableStream({
+ pull(controller) {
+ controller.enqueue(new Uint8Array([42]));
+ },
+ cancel(reason) {
+ cancelReason = reason;
+ }
+ });
+
+ const fetchPromise = fetch('../resources/empty.txt', {
+ body, signal,
+ method: 'POST',
+ duplex: 'half',
+ headers: {
+ 'Content-Type': 'text/plain'
+ }
+ });
+
+ assert_true(!!cancelReason, 'Cancel called sync');
+ assert_equals(cancelReason.constructor, DOMException);
+ assert_equals(cancelReason.name, 'AbortError');
+
+ await promise_rejects_dom(t, "AbortError", fetchPromise);
+
+ const fetchErr = await fetchPromise.catch(e => e);
+
+ assert_equals(cancelReason, fetchErr, "Fetch rejects with same error instance");
+}, "Readable stream synchronously cancels with AbortError if aborted before reading");
+
+test(() => {
+ const controller = new AbortController();
+ const signal = controller.signal;
+ controller.abort();
+
+ const request = new Request('.', { signal });
+ const requestSignal = request.signal;
+
+ const clonedRequest = request.clone();
+
+ assert_equals(requestSignal, request.signal, "Original request signal the same after cloning");
+ assert_true(request.signal.aborted, "Original request signal aborted");
+ assert_not_equals(clonedRequest.signal, request.signal, "Cloned request has different signal");
+ assert_true(clonedRequest.signal.aborted, "Cloned request signal aborted");
+}, "Signal state is cloned");
+
+test(() => {
+ const controller = new AbortController();
+ const signal = controller.signal;
+
+ const request = new Request('.', { signal });
+ const clonedRequest = request.clone();
+
+ const log = [];
+
+ request.signal.addEventListener('abort', () => log.push('original-aborted'));
+ clonedRequest.signal.addEventListener('abort', () => log.push('clone-aborted'));
+
+ controller.abort();
+
+ assert_array_equals(log, ['original-aborted', 'clone-aborted'], "Abort events fired in correct order");
+ assert_true(request.signal.aborted, 'Signal aborted');
+ assert_true(clonedRequest.signal.aborted, 'Signal aborted');
+}, "Clone aborts with original controller");
diff --git a/test/wpt/tests/fetch/api/abort/keepalive.html b/test/wpt/tests/fetch/api/abort/keepalive.html
new file mode 100644
index 0000000..db12df0
--- /dev/null
+++ b/test/wpt/tests/fetch/api/abort/keepalive.html
@@ -0,0 +1,85 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script>
+// This controller must be on the window so it is visible to the iframe.
+window.sharedController = new AbortController();
+
+async function fetchJson(url) {
+ const response = await fetch(url);
+ assert_true(response.ok, 'response should be ok');
+ return response.json();
+}
+
+promise_test(async () => {
+ const stateKey = token();
+ const controller = new AbortController();
+ await fetch(`../resources/infinite-slow-response.py?stateKey=${stateKey}`,
+ {
+ signal: controller.signal,
+ keepalive: true
+ });
+ const before = await fetchJson(`../resources/stash-take.py?key=${stateKey}`);
+ assert_equals(before, 'open', 'connection should be open');
+
+ controller.abort();
+
+ // Spin until the abort completes.
+ while (true) {
+ const after = await fetchJson(`../resources/stash-take.py?key=${stateKey}`);
+ if (after) {
+ // stateKey='open' was removed from the dictionary by the first fetch of
+ // stash-take.py, so we should only ever see the value 'closed' here.
+ assert_equals(after, 'closed', 'connection should have closed');
+ break;
+ }
+ }
+}, 'aborting a keepalive fetch should work');
+
+promise_test(async t => {
+ const key = token();
+ const iframeEl = document.querySelector('iframe');
+
+ // Tell the iframe to start the fetch, and wait until it says it has.
+ await new Promise(resolve => {
+ onmessage = t.step_func(event => {
+ assert_equals(event.data, 'started', 'event data should be "started"');
+ resolve();
+ });
+ iframeEl.contentWindow.postMessage(key, '*');
+ });
+
+ // Detach the context of the fetch.
+ iframeEl.remove();
+
+ sharedController.abort();
+
+ // The abort should not do anything. The connection should stay open. Wait 1
+ // second to give time for the fetch to complete.
+ await new Promise(resolve => t.step_timeout(resolve, 1000));
+
+ const after = await fetchJson(`../resources/stash-take.py?key=${key}`);
+ assert_equals(after, 'on', 'fetch should have completed');
+}, 'aborting a detached keepalive fetch should not do anything');
+</script>
+
+<iframe srcdoc="
+ <!DOCTYPE html>
+ <meta charset=utf-8>
+ <script>
+ onmessage = async event => {
+ const key = event.data;
+ await fetch(
+ `../resources/redirect.py?delay=500&amp;location=` +
+ `../resources/stash-put.py%3fkey=${key}%26value=on`,
+ {
+ signal: parent.sharedController.signal,
+ keepalive: true
+ });
+ parent.postMessage('started', '*');
+ };
+ </script>
+ ">
+</iframe>
diff --git a/test/wpt/tests/fetch/api/abort/request.any.js b/test/wpt/tests/fetch/api/abort/request.any.js
new file mode 100644
index 0000000..dcc7803
--- /dev/null
+++ b/test/wpt/tests/fetch/api/abort/request.any.js
@@ -0,0 +1,85 @@
+// META: timeout=long
+// META: global=window,worker
+
+const BODY_FUNCTION_AND_DATA = {
+ arrayBuffer: null,
+ blob: null,
+ formData: new FormData(),
+ json: new Blob(["{}"]),
+ text: null,
+};
+
+for (const [bodyFunction, body] of Object.entries(BODY_FUNCTION_AND_DATA)) {
+ promise_test(async () => {
+ const controller = new AbortController();
+ const signal = controller.signal;
+ const request = new Request("../resources/data.json", {
+ method: "post",
+ signal,
+ body,
+ });
+
+ controller.abort();
+ await request[bodyFunction]();
+ assert_true(
+ true,
+ `An aborted request should still be able to run ${bodyFunction}()`
+ );
+ }, `Calling ${bodyFunction}() on an aborted request`);
+
+ promise_test(async () => {
+ const controller = new AbortController();
+ const signal = controller.signal;
+ const request = new Request("../resources/data.json", {
+ method: "post",
+ signal,
+ body,
+ });
+
+ const p = request[bodyFunction]();
+ controller.abort();
+ await p;
+ assert_true(
+ true,
+ `An aborted request should still be able to run ${bodyFunction}()`
+ );
+ }, `Aborting a request after calling ${bodyFunction}()`);
+
+ if (!body) {
+ promise_test(async () => {
+ const controller = new AbortController();
+ const signal = controller.signal;
+ const request = new Request("../resources/data.json", {
+ method: "post",
+ signal,
+ body,
+ });
+
+ // consuming happens synchronously, so don't wait
+ fetch(request).catch(() => {});
+
+ controller.abort();
+ await request[bodyFunction]();
+ assert_true(
+ true,
+ `An aborted consumed request should still be able to run ${bodyFunction}() when empty`
+ );
+ }, `Calling ${bodyFunction}() on an aborted consumed empty request`);
+ }
+
+ promise_test(async t => {
+ const controller = new AbortController();
+ const signal = controller.signal;
+ const request = new Request("../resources/data.json", {
+ method: "post",
+ signal,
+ body: body || new Blob(["foo"]),
+ });
+
+ // consuming happens synchronously, so don't wait
+ fetch(request).catch(() => {});
+
+ controller.abort();
+ await promise_rejects_js(t, TypeError, request[bodyFunction]());
+ }, `Calling ${bodyFunction}() on an aborted consumed nonempty request`);
+}
diff --git a/test/wpt/tests/fetch/api/abort/serviceworker-intercepted.https.html b/test/wpt/tests/fetch/api/abort/serviceworker-intercepted.https.html
new file mode 100644
index 0000000..ed9bc97
--- /dev/null
+++ b/test/wpt/tests/fetch/api/abort/serviceworker-intercepted.https.html
@@ -0,0 +1,212 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Aborting fetch when intercepted by a service worker</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="../../../service-workers/service-worker/resources/test-helpers.sub.js"></script>
+</head>
+<body>
+<script>
+ // Duplicating this resource to make service worker scoping simpler.
+ const SCOPE = '../resources/basic.html';
+ const BODY_METHODS = ['arrayBuffer', 'blob', 'formData', 'json', 'text'];
+
+ const error1 = new Error('error1');
+ error1.name = 'error1';
+
+ async function setupRegistration(t, scope, service_worker) {
+ const reg = await navigator.serviceWorker.register(service_worker, { scope });
+ await wait_for_state(t, reg.installing, 'activated');
+ add_completion_callback(_ => reg.unregister());
+ return reg;
+ }
+
+ promise_test(async t => {
+ const suffix = "?q=aborted-not-intercepted";
+ const scope = SCOPE + suffix;
+ await setupRegistration(t, scope, '../resources/sw-intercept.js');
+ const iframe = await with_iframe(scope);
+ add_completion_callback(_ => iframe.remove());
+ const w = iframe.contentWindow;
+
+ const controller = new w.AbortController();
+ const signal = controller.signal;
+ controller.abort();
+
+ const nextData = new Promise(resolve => {
+ w.navigator.serviceWorker.addEventListener('message', function once(event) {
+ // The message triggered by the iframe's document's fetch
+ // request cannot get dispatched by the time we add the event
+ // listener, so we have to guard against it.
+ if (!event.data.endsWith(suffix)) {
+ w.navigator.serviceWorker.removeEventListener('message', once);
+ resolve(event.data);
+ }
+ })
+ });
+
+ const fetchPromise = w.fetch('data.json', { signal });
+
+ await promise_rejects_dom(t, "AbortError", w.DOMException, fetchPromise);
+
+ await w.fetch('data.json?no-abort');
+
+ assert_true((await nextData).endsWith('?no-abort'), "Aborted request does not go through service worker");
+ }, "Already aborted request does not land in service worker");
+
+ for (const bodyMethod of BODY_METHODS) {
+ promise_test(async t => {
+ const scope = SCOPE + "?q=aborted-" + bodyMethod + "-rejects";
+ await setupRegistration(t, scope, '../resources/sw-intercept.js');
+ const iframe = await with_iframe(scope);
+ add_completion_callback(_ => iframe.remove());
+ const w = iframe.contentWindow;
+
+ const controller = new w.AbortController();
+ const signal = controller.signal;
+
+ const log = [];
+ const response = await w.fetch('data.json', { signal });
+
+ controller.abort();
+
+ const bodyPromise = response[bodyMethod]();
+
+ await Promise.all([
+ bodyPromise.catch(() => log.push(`${bodyMethod}-reject`)),
+ Promise.resolve().then(() => log.push('next-microtask'))
+ ]);
+
+ await promise_rejects_dom(t, "AbortError", w.DOMException, bodyPromise);
+
+ assert_array_equals(log, [`${bodyMethod}-reject`, 'next-microtask']);
+ }, `response.${bodyMethod}() rejects if already aborted`);
+ }
+
+ promise_test(async t => {
+ const scope = SCOPE + "?q=aborted-stream-errors";
+ await setupRegistration(t, scope, '../resources/sw-intercept.js');
+ const iframe = await with_iframe(scope);
+ add_completion_callback(_ => iframe.remove());
+ const w = iframe.contentWindow;
+
+ const controller = new w.AbortController();
+ const signal = controller.signal;
+
+ const response = await w.fetch('data.json', { signal });
+ const reader = response.body.getReader();
+
+ controller.abort();
+
+ await promise_rejects_dom(t, "AbortError", w.DOMException, reader.read());
+ await promise_rejects_dom(t, "AbortError", w.DOMException, reader.closed);
+ }, "Stream errors once aborted.");
+
+ promise_test(async t => {
+ const scope = SCOPE + "?q=aborted-with-abort-reason";
+ await setupRegistration(t, scope, '../resources/sw-intercept.js');
+ const iframe = await with_iframe(scope);
+ add_completion_callback(_ => iframe.remove());
+ const w = iframe.contentWindow;
+
+ const controller = new w.AbortController();
+ const signal = controller.signal;
+
+ const fetchPromise = w.fetch('data.json', { signal });
+
+ controller.abort(error1);
+
+ await promise_rejects_exactly(t, error1, fetchPromise);
+ }, "fetch() rejects with abort reason");
+
+
+ promise_test(async t => {
+ const scope = SCOPE + "?q=aborted-with-abort-reason-in-body";
+ await setupRegistration(t, scope, '../resources/sw-intercept.js');
+ const iframe = await with_iframe(scope);
+ add_completion_callback(_ => iframe.remove());
+ const w = iframe.contentWindow;
+
+ const controller = new w.AbortController();
+ const signal = controller.signal;
+
+ const fetchResponse = await w.fetch('data.json', { signal });
+ const bodyPromise = fetchResponse.body.getReader().read();
+ controller.abort(error1);
+
+ await promise_rejects_exactly(t, error1, bodyPromise);
+ }, "fetch() response body has abort reason");
+
+ promise_test(async t => {
+ const scope = SCOPE + "?q=service-worker-observes-abort-reason";
+ await setupRegistration(t, scope, '../resources/sw-intercept-abort.js');
+ const iframe = await with_iframe(scope);
+ add_completion_callback(_ => iframe.remove());
+ const w = iframe.contentWindow;
+
+ const controller = new w.AbortController();
+ const signal = controller.signal;
+
+ const fetchPromise = w.fetch('data.json', { signal });
+
+ await new Promise(resolve => {
+ w.navigator.serviceWorker.addEventListener('message', t.step_func(event => {
+ assert_equals(event.data, "fetch event has arrived");
+ resolve();
+ }), {once: true});
+ });
+
+ controller.abort(error1);
+
+ await new Promise(resolve => {
+ w.navigator.serviceWorker.addEventListener('message', t.step_func(event => {
+ assert_equals(event.data.message, error1.message);
+ resolve();
+ }), {once: true});
+ });
+
+ await promise_rejects_exactly(t, error1, fetchPromise);
+ }, "Service Worker can observe the fetch abort and associated abort reason");
+
+ promise_test(async t => {
+ let incrementing_error = new Error('error1');
+ incrementing_error.name = 'error1';
+
+ const scope = SCOPE + "?q=serialization-on-abort";
+ await setupRegistration(t, scope, '../resources/sw-intercept-abort.js');
+ const iframe = await with_iframe(scope);
+ add_completion_callback(_ => iframe.remove());
+ const w = iframe.contentWindow;
+
+ const controller = new w.AbortController();
+ const signal = controller.signal;
+
+ const fetchPromise = w.fetch('data.json', { signal });
+
+ await new Promise(resolve => {
+ w.navigator.serviceWorker.addEventListener('message', t.step_func(event => {
+ assert_equals(event.data, "fetch event has arrived");
+ resolve();
+ }), {once: true});
+ });
+
+ controller.abort(incrementing_error);
+
+ const original_error_name = incrementing_error.name;
+
+ incrementing_error.name = 'error2';
+
+ await new Promise(resolve => {
+ w.navigator.serviceWorker.addEventListener('message', t.step_func(event => {
+ assert_equals(event.data.name, original_error_name);
+ resolve();
+ }), {once: true});
+ });
+
+ await promise_rejects_exactly(t, incrementing_error, fetchPromise);
+ }, "Abort reason serialization happens on abort");
+</script>
+</body>
+</html>
diff --git a/test/wpt/tests/fetch/api/basic/accept-header.any.js b/test/wpt/tests/fetch/api/basic/accept-header.any.js
new file mode 100644
index 0000000..cd54cf2
--- /dev/null
+++ b/test/wpt/tests/fetch/api/basic/accept-header.any.js
@@ -0,0 +1,34 @@
+// META: global=window,worker
+// META: script=../resources/utils.js
+
+promise_test(function() {
+ return fetch(RESOURCES_DIR + "inspect-headers.py?headers=Accept").then(function(response) {
+ assert_equals(response.status, 200, "HTTP status is 200");
+ assert_equals(response.type , "basic", "Response's type is basic");
+ assert_equals(response.headers.get("x-request-accept"), "*/*", "Request has accept header with value '*/*'");
+ });
+}, "Request through fetch should have 'accept' header with value '*/*'");
+
+promise_test(function() {
+ return fetch(RESOURCES_DIR + "inspect-headers.py?headers=Accept", {"headers": [["Accept", "custom/*"]]}).then(function(response) {
+ assert_equals(response.status, 200, "HTTP status is 200");
+ assert_equals(response.type , "basic", "Response's type is basic");
+ assert_equals(response.headers.get("x-request-accept"), "custom/*", "Request has accept header with value 'custom/*'");
+ });
+}, "Request through fetch should have 'accept' header with value 'custom/*'");
+
+promise_test(function() {
+ return fetch(RESOURCES_DIR + "inspect-headers.py?headers=Accept-Language").then(function(response) {
+ assert_equals(response.status, 200, "HTTP status is 200");
+ assert_equals(response.type , "basic", "Response's type is basic");
+ assert_true(response.headers.has("x-request-accept-language"));
+ });
+}, "Request through fetch should have a 'accept-language' header");
+
+promise_test(function() {
+ return fetch(RESOURCES_DIR + "inspect-headers.py?headers=Accept-Language", {"headers": [["Accept-Language", "bzh"]]}).then(function(response) {
+ assert_equals(response.status, 200, "HTTP status is 200");
+ assert_equals(response.type , "basic", "Response's type is basic");
+ assert_equals(response.headers.get("x-request-accept-language"), "bzh", "Request has accept header with value 'bzh'");
+ });
+}, "Request through fetch should have 'accept-language' header with value 'bzh'");
diff --git a/test/wpt/tests/fetch/api/basic/block-mime-as-script.html b/test/wpt/tests/fetch/api/basic/block-mime-as-script.html
new file mode 100644
index 0000000..afc2bbb
--- /dev/null
+++ b/test/wpt/tests/fetch/api/basic/block-mime-as-script.html
@@ -0,0 +1,43 @@
+<!doctype html>
+<meta charset="utf-8">
+<title>Block mime type as script</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div></div>
+<script>
+ var noop = function() {};
+
+ ["non-empty", "empty"].forEach(function(content) {
+ ["text/csv",
+ "audio/aiff",
+ "audio/midi",
+ "audio/whatever",
+ "video/avi",
+ "video/fli",
+ "video/whatever",
+ "image/jpeg",
+ "image/gif",
+ "image/whatever"].forEach(function(test_case) {
+ async_test(function(t) {
+ var script = document.createElement("script");
+ script.onerror = t.step_func_done(noop);
+ script.onload = t.unreached_func("Unexpected load event");
+ script.src = "../resources/script-with-header.py?content=" + content +
+ "&mime=" + test_case;
+ document.body.appendChild(script);
+ }, "Should fail loading " + content + " script with " + test_case +
+ " MIME type");
+ });
+ });
+
+ ["html", "plain"].forEach(function(test_case) {
+ async_test(function(t) {
+ var script = document.createElement("script");
+ script.onerror = t.unreached_func("Unexpected error event");
+ script.onload = t.step_func_done(noop);
+ script.src = "../resources/script-with-header.py?mime=text/" + test_case;
+ document.body.appendChild(script);
+ }, "Should load script with text/" + test_case + " MIME type");
+ });
+
+</script>
diff --git a/test/wpt/tests/fetch/api/basic/conditional-get.any.js b/test/wpt/tests/fetch/api/basic/conditional-get.any.js
new file mode 100644
index 0000000..2f9fa81
--- /dev/null
+++ b/test/wpt/tests/fetch/api/basic/conditional-get.any.js
@@ -0,0 +1,38 @@
+// META: title=Request ETag
+// META: global=window,worker
+// META: script=/common/utils.js
+
+promise_test(function() {
+ var cacheBuster = token(); // ensures first request is uncached
+ var url = "../resources/cache.py?v=" + cacheBuster;
+ var etag;
+
+ // make the first request
+ return fetch(url).then(function(response) {
+ // ensure we're getting the regular, uncached response
+ assert_equals(response.status, 200);
+ assert_equals(response.headers.get("X-HTTP-STATUS"), null)
+
+ return response.text(); // consuming the body, just to be safe
+ }).then(function(body) {
+ // make a second request
+ return fetch(url);
+ }).then(function(response) {
+ // while the server responds with 304 if our browser sent the correct
+ // If-None-Match request header, at the JavaScript level this surfaces
+ // as 200
+ assert_equals(response.status, 200);
+ assert_equals(response.headers.get("X-HTTP-STATUS"), "304")
+
+ etag = response.headers.get("ETag")
+
+ return response.text(); // consuming the body, just to be safe
+ }).then(function(body) {
+ // make a third request, explicitly setting If-None-Match request header
+ var headers = { "If-None-Match": etag }
+ return fetch(url, { headers: headers })
+ }).then(function(response) {
+ // 304 now surfaces thanks to the explicit If-None-Match request header
+ assert_equals(response.status, 304);
+ });
+}, "Testing conditional GET with ETags");
diff --git a/test/wpt/tests/fetch/api/basic/error-after-response.any.js b/test/wpt/tests/fetch/api/basic/error-after-response.any.js
new file mode 100644
index 0000000..f711442
--- /dev/null
+++ b/test/wpt/tests/fetch/api/basic/error-after-response.any.js
@@ -0,0 +1,24 @@
+// META: title=Fetch: network timeout after receiving the HTTP response headers
+// META: global=window,worker
+// META: timeout=long
+// META: script=../resources/utils.js
+
+function checkReader(test, reader, promiseToTest)
+{
+ return reader.read().then((value) => {
+ validateBufferFromString(value.value, "TEST_CHUNK", "Should receive first chunk");
+ return promise_rejects_js(test, TypeError, promiseToTest(reader));
+ });
+}
+
+promise_test((test) => {
+ return fetch("../resources/bad-chunk-encoding.py?count=1").then((response) => {
+ return checkReader(test, response.body.getReader(), reader => reader.read());
+ });
+}, "Response reader read() promise should reject after a network error happening after resolving fetch promise");
+
+promise_test((test) => {
+ return fetch("../resources/bad-chunk-encoding.py?count=1").then((response) => {
+ return checkReader(test, response.body.getReader(), reader => reader.closed);
+ });
+}, "Response reader closed promise should reject after a network error happening after resolving fetch promise");
diff --git a/test/wpt/tests/fetch/api/basic/header-value-combining.any.js b/test/wpt/tests/fetch/api/basic/header-value-combining.any.js
new file mode 100644
index 0000000..bb70d87
--- /dev/null
+++ b/test/wpt/tests/fetch/api/basic/header-value-combining.any.js
@@ -0,0 +1,15 @@
+// META: global=window,worker
+
+[
+ ["content-length", "0", "header-content-length"],
+ ["content-length", "0, 0", "header-content-length-twice"],
+ ["double-trouble", ", ", "headers-double-empty"],
+ ["foo-test", "1, 2, 3", "headers-basic"],
+ ["heya", ", \u000B\u000C, 1, , , 2", "headers-some-are-empty"],
+ ["www-authenticate", "1, 2, 3, 4", "headers-www-authenticate"],
+].forEach(testValues => {
+ promise_test(async t => {
+ const response = await fetch("../../../xhr/resources/" + testValues[2] + ".asis");
+ assert_equals(response.headers.get(testValues[0]), testValues[1]);
+ }, "response.headers.get('" + testValues[0] + "') expects " + testValues[1]);
+});
diff --git a/test/wpt/tests/fetch/api/basic/header-value-null-byte.any.js b/test/wpt/tests/fetch/api/basic/header-value-null-byte.any.js
new file mode 100644
index 0000000..741d83b
--- /dev/null
+++ b/test/wpt/tests/fetch/api/basic/header-value-null-byte.any.js
@@ -0,0 +1,5 @@
+// META: global=window,worker
+
+promise_test(t => {
+ return promise_rejects_js(t, TypeError, fetch("../../../xhr/resources/parse-headers.py?my-custom-header="+encodeURIComponent("x\0x")));
+}, "Ensure fetch() rejects null bytes in headers");
diff --git a/test/wpt/tests/fetch/api/basic/historical.any.js b/test/wpt/tests/fetch/api/basic/historical.any.js
new file mode 100644
index 0000000..c808126
--- /dev/null
+++ b/test/wpt/tests/fetch/api/basic/historical.any.js
@@ -0,0 +1,17 @@
+// META: global=window,worker
+
+test(() => {
+ assert_false("getAll" in new Headers());
+ assert_false("getAll" in Headers.prototype);
+}, "Headers object no longer has a getAll() method");
+
+test(() => {
+ assert_false("type" in new Request("about:blank"));
+ assert_false("type" in Request.prototype);
+}, "'type' getter should not exist on Request objects");
+
+// See https://github.com/whatwg/fetch/pull/979 for the removal
+test(() => {
+ assert_false("trailer" in new Response());
+ assert_false("trailer" in Response.prototype);
+}, "Response object no longer has a trailer getter");
diff --git a/test/wpt/tests/fetch/api/basic/http-response-code.any.js b/test/wpt/tests/fetch/api/basic/http-response-code.any.js
new file mode 100644
index 0000000..1fd312a
--- /dev/null
+++ b/test/wpt/tests/fetch/api/basic/http-response-code.any.js
@@ -0,0 +1,14 @@
+// META: global=window,worker
+// META: script=../resources/utils.js
+// META: script=/common/utils.js
+// META: script=/common/get-host-info.sub.js
+
+promise_test(async (test) => {
+ const resp = await fetch(
+ "/fetch/connection-pool/resources/network-partition-key.py?"
+ + `status=425&uuid=${token()}&partition_id=${get_host_info().ORIGIN}`
+ + `&dispatch=check_partition&addcounter=true`);
+ assert_equals(resp.status, 425);
+ const text = await resp.text();
+ assert_equals(text, "ok. Request was sent 1 times. 1 connections were created.");
+}, "Fetch on 425 response should not be retried for non TLS early data.");
diff --git a/test/wpt/tests/fetch/api/basic/integrity.sub.any.js b/test/wpt/tests/fetch/api/basic/integrity.sub.any.js
new file mode 100644
index 0000000..e3cfd1b
--- /dev/null
+++ b/test/wpt/tests/fetch/api/basic/integrity.sub.any.js
@@ -0,0 +1,87 @@
+// META: global=window,dedicatedworker,sharedworker
+// META: script=../resources/utils.js
+
+function integrity(desc, url, integrity, initRequestMode, shouldPass) {
+ var fetchRequestInit = {'integrity': integrity}
+ if (!!initRequestMode && initRequestMode !== "") {
+ fetchRequestInit.mode = initRequestMode;
+ }
+
+ if (shouldPass) {
+ promise_test(function(test) {
+ return fetch(url, fetchRequestInit).then(function(resp) {
+ if (initRequestMode !== "no-cors") {
+ assert_equals(resp.status, 200, "Response's status is 200");
+ } else {
+ assert_equals(resp.status, 0, "Opaque response's status is 0");
+ assert_equals(resp.type, "opaque");
+ }
+ });
+ }, desc);
+ } else {
+ promise_test(function(test) {
+ return promise_rejects_js(test, TypeError, fetch(url, fetchRequestInit));
+ }, desc);
+ }
+}
+
+const topSha256 = "sha256-KHIDZcXnR2oBHk9DrAA+5fFiR6JjudYjqoXtMR1zvzk=";
+const topSha384 = "sha384-MgZYnnAzPM/MjhqfOIMfQK5qcFvGZsGLzx4Phd7/A8fHTqqLqXqKo8cNzY3xEPTL";
+const topSha512 = "sha512-D6yns0qxG0E7+TwkevZ4Jt5t7Iy3ugmAajG/dlf6Pado1JqTyneKXICDiqFIkLMRExgtvg8PlxbKTkYfRejSOg==";
+const topSha512wrongpadding = "sha512-D6yns0qxG0E7+TwkevZ4Jt5t7Iy3ugmAajG/dlf6Pado1JqTyneKXICDiqFIkLMRExgtvg8PlxbKTkYfRejSOg";
+const topSha512base64url = "sha512-D6yns0qxG0E7-TwkevZ4Jt5t7Iy3ugmAajG_dlf6Pado1JqTyneKXICDiqFIkLMRExgtvg8PlxbKTkYfRejSOg==";
+const topSha512base64url_nopadding = "sha512-D6yns0qxG0E7-TwkevZ4Jt5t7Iy3ugmAajG_dlf6Pado1JqTyneKXICDiqFIkLMRExgtvg8PlxbKTkYfRejSOg";
+const invalidSha256 = "sha256-dKUcPOn/AlUjWIwcHeHNqYXPlvyGiq+2dWOdFcE+24I=";
+const invalidSha512 = "sha512-oUceBRNxPxnY60g/VtPCj2syT4wo4EZh2CgYdWy9veW8+OsReTXoh7dizMGZafvx9+QhMS39L/gIkxnPIn41Zg==";
+
+const path = dirname(location.pathname) + RESOURCES_DIR + "top.txt";
+const url = path;
+const corsUrl =
+ `http://{{host}}:{{ports[http][1]}}${path}?pipe=header(Access-Control-Allow-Origin,*)`;
+const corsUrl2 = `https://{{host}}:{{ports[https][0]}}${path}`
+
+integrity("Empty string integrity", url, "", /* initRequestMode */ undefined,
+ /* shouldPass */ true);
+integrity("SHA-256 integrity", url, topSha256, /* initRequestMode */ undefined,
+ /* shouldPass */ true);
+integrity("SHA-384 integrity", url, topSha384, /* initRequestMode */ undefined,
+ /* shouldPass */ true);
+integrity("SHA-512 integrity", url, topSha512, /* initRequestMode */ undefined,
+ /* shouldPass */ true);
+integrity("SHA-512 integrity with missing padding", url, topSha512wrongpadding,
+ /* initRequestMode */ undefined, /* shouldPass */ true);
+integrity("SHA-512 integrity base64url encoded", url, topSha512base64url,
+ /* initRequestMode */ undefined, /* shouldPass */ true);
+integrity("SHA-512 integrity base64url encoded with missing padding", url,
+ topSha512base64url_nopadding, /* initRequestMode */ undefined,
+ /* shouldPass */ true);
+integrity("Invalid integrity", url, invalidSha256,
+ /* initRequestMode */ undefined, /* shouldPass */ false);
+integrity("Multiple integrities: valid stronger than invalid", url,
+ invalidSha256 + " " + topSha384, /* initRequestMode */ undefined,
+ /* shouldPass */ true);
+integrity("Multiple integrities: invalid stronger than valid",
+ url, invalidSha512 + " " + topSha384, /* initRequestMode */ undefined,
+ /* shouldPass */ false);
+integrity("Multiple integrities: invalid as strong as valid", url,
+ invalidSha512 + " " + topSha512, /* initRequestMode */ undefined,
+ /* shouldPass */ true);
+integrity("Multiple integrities: both are valid", url,
+ topSha384 + " " + topSha512, /* initRequestMode */ undefined,
+ /* shouldPass */ true);
+integrity("Multiple integrities: both are invalid", url,
+ invalidSha256 + " " + invalidSha512, /* initRequestMode */ undefined,
+ /* shouldPass */ false);
+integrity("CORS empty integrity", corsUrl, "", /* initRequestMode */ undefined,
+ /* shouldPass */ true);
+integrity("CORS SHA-512 integrity", corsUrl, topSha512,
+ /* initRequestMode */ undefined, /* shouldPass */ true);
+integrity("CORS invalid integrity", corsUrl, invalidSha512,
+ /* initRequestMode */ undefined, /* shouldPass */ false);
+
+integrity("Empty string integrity for opaque response", corsUrl2, "",
+ /* initRequestMode */ "no-cors", /* shouldPass */ true);
+integrity("SHA-* integrity for opaque response", corsUrl2, topSha512,
+ /* initRequestMode */ "no-cors", /* shouldPass */ false);
+
+done();
diff --git a/test/wpt/tests/fetch/api/basic/keepalive.any.js b/test/wpt/tests/fetch/api/basic/keepalive.any.js
new file mode 100644
index 0000000..899d41d
--- /dev/null
+++ b/test/wpt/tests/fetch/api/basic/keepalive.any.js
@@ -0,0 +1,43 @@
+// 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,
+ HTTP_REMOTE_ORIGIN,
+ HTTP_REMOTE_ORIGIN_WITH_DIFFERENT_PORT
+} = get_host_info();
+
+/**
+ * In a different-site iframe, test to fetch a keepalive URL on the specified
+ * document event.
+ */
+function keepaliveSimpleRequestTest(method) {
+ for (const evt of ['load', 'pagehide', 'unload']) {
+ const desc =
+ `[keepalive] simple ${method} request on '${evt}' [no payload]`;
+ promise_test(async (test) => {
+ const token1 = token();
+ const iframe = document.createElement('iframe');
+ iframe.src = getKeepAliveIframeUrl(token1, method, {sendOn: evt});
+ document.body.appendChild(iframe);
+ await iframeLoaded(iframe);
+ if (evt != 'load') {
+ iframe.remove();
+ }
+ assert_equals(await getTokenFromMessage(), token1);
+
+ assertStashedTokenAsync(desc, token1);
+ }, `${desc}; setting up`);
+ }
+}
+
+for (const method of ['GET', 'POST']) {
+ keepaliveSimpleRequestTest(method);
+}
diff --git a/test/wpt/tests/fetch/api/basic/mediasource.window.js b/test/wpt/tests/fetch/api/basic/mediasource.window.js
new file mode 100644
index 0000000..1f89595
--- /dev/null
+++ b/test/wpt/tests/fetch/api/basic/mediasource.window.js
@@ -0,0 +1,5 @@
+promise_test(t => {
+ const mediaSource = new MediaSource(),
+ mediaSourceURL = URL.createObjectURL(mediaSource);
+ return promise_rejects_js(t, TypeError, fetch(mediaSourceURL));
+}, "Cannot fetch blob: URL from a MediaSource");
diff --git a/test/wpt/tests/fetch/api/basic/mode-no-cors.sub.any.js b/test/wpt/tests/fetch/api/basic/mode-no-cors.sub.any.js
new file mode 100644
index 0000000..a4abcac
--- /dev/null
+++ b/test/wpt/tests/fetch/api/basic/mode-no-cors.sub.any.js
@@ -0,0 +1,29 @@
+// META: script=../resources/utils.js
+
+function fetchNoCors(url, isOpaqueFiltered) {
+ var urlQuery = "?pipe=header(x-is-filtered,value)"
+ promise_test(function(test) {
+ if (isOpaqueFiltered)
+ return fetch(url + urlQuery, {"mode": "no-cors"}).then(function(resp) {
+ assert_equals(resp.status, 0, "Opaque filter: status is 0");
+ assert_equals(resp.statusText, "", "Opaque filter: statusText is \"\"");
+ assert_equals(resp.url, "", "Opaque filter: url is \"\"");
+ assert_equals(resp.type , "opaque", "Opaque filter: response's type is opaque");
+ assert_equals(resp.headers.get("x-is-filtered"), null, "Header x-is-filtered is filtered");
+ });
+ else
+ return fetch(url + urlQuery, {"mode": "no-cors"}).then(function(resp) {
+ assert_equals(resp.status, 200, "HTTP status is 200");
+ assert_equals(resp.type , "basic", "Response's type is basic");
+ assert_equals(resp.headers.get("x-is-filtered"), "value", "Header x-is-filtered is not filtered");
+ });
+ }, "Fetch "+ url + " with no-cors mode");
+}
+
+fetchNoCors(RESOURCES_DIR + "top.txt", false);
+fetchNoCors("http://{{host}}:{{ports[http][0]}}/fetch/api/resources/top.txt", false);
+fetchNoCors("https://{{host}}:{{ports[https][0]}}/fetch/api/resources/top.txt", true);
+fetchNoCors("http://{{host}}:{{ports[http][1]}}/fetch/api/resources/top.txt", true);
+
+done();
+
diff --git a/test/wpt/tests/fetch/api/basic/mode-same-origin.any.js b/test/wpt/tests/fetch/api/basic/mode-same-origin.any.js
new file mode 100644
index 0000000..1457702
--- /dev/null
+++ b/test/wpt/tests/fetch/api/basic/mode-same-origin.any.js
@@ -0,0 +1,28 @@
+// META: script=../resources/utils.js
+// META: script=/common/get-host-info.sub.js
+
+function fetchSameOrigin(url, shouldPass) {
+ promise_test(function(test) {
+ if (shouldPass)
+ return fetch(url , {"mode": "same-origin"}).then(function(resp) {
+ assert_equals(resp.status, 200, "HTTP status is 200");
+ assert_equals(resp.type, "basic", "response type is basic");
+ });
+ else
+ return promise_rejects_js(test, TypeError, fetch(url, {mode: "same-origin"}));
+ }, "Fetch "+ url + " with same-origin mode");
+}
+
+var host_info = get_host_info();
+
+fetchSameOrigin(RESOURCES_DIR + "top.txt", true);
+fetchSameOrigin(host_info.HTTP_ORIGIN + "/fetch/api/resources/top.txt", true);
+fetchSameOrigin(host_info.HTTPS_ORIGIN + "/fetch/api/resources/top.txt", false);
+fetchSameOrigin(host_info.HTTP_REMOTE_ORIGIN + "/fetch/api/resources/top.txt", false);
+
+var redirPath = dirname(location.pathname) + RESOURCES_DIR + "redirect.py?location=";
+
+fetchSameOrigin(redirPath + RESOURCES_DIR + "top.txt", true);
+fetchSameOrigin(redirPath + host_info.HTTP_ORIGIN + "/fetch/api/resources/top.txt", true);
+fetchSameOrigin(redirPath + host_info.HTTPS_ORIGIN + "/fetch/api/resources/top.txt", false);
+fetchSameOrigin(redirPath + host_info.HTTP_REMOTE_ORIGIN + "/fetch/api/resources/top.txt", false);
diff --git a/test/wpt/tests/fetch/api/basic/referrer.any.js b/test/wpt/tests/fetch/api/basic/referrer.any.js
new file mode 100644
index 0000000..85745e6
--- /dev/null
+++ b/test/wpt/tests/fetch/api/basic/referrer.any.js
@@ -0,0 +1,29 @@
+// META: script=../resources/utils.js
+// META: script=/common/get-host-info.sub.js
+
+function runTest(url, init, expectedReferrer, title) {
+ promise_test(function(test) {
+ url += (url.indexOf('?') !== -1 ? '&' : '?') + "headers=referer&cors";
+
+ return fetch(url , init).then(function(resp) {
+ assert_equals(resp.status, 200, "HTTP status is 200");
+ assert_equals(resp.headers.get("x-request-referer"), expectedReferrer, "Request's referrer is correct");
+ });
+ }, title);
+}
+
+var fetchedUrl = RESOURCES_DIR + "inspect-headers.py";
+var corsFetchedUrl = get_host_info().HTTP_REMOTE_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "inspect-headers.py";
+var redirectUrl = RESOURCES_DIR + "redirect.py?location=" ;
+var corsRedirectUrl = get_host_info().HTTP_REMOTE_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "redirect.py?location=";
+
+runTest(fetchedUrl, { referrerPolicy: "origin-when-cross-origin"}, location.toString(), "origin-when-cross-origin policy on a same-origin URL");
+runTest(corsFetchedUrl, { referrerPolicy: "origin-when-cross-origin"}, get_host_info().HTTP_ORIGIN + "/", "origin-when-cross-origin policy on a cross-origin URL");
+runTest(redirectUrl + corsFetchedUrl, { referrerPolicy: "origin-when-cross-origin"}, get_host_info().HTTP_ORIGIN + "/", "origin-when-cross-origin policy on a cross-origin URL after same-origin redirection");
+runTest(corsRedirectUrl + fetchedUrl, { referrerPolicy: "origin-when-cross-origin"}, get_host_info().HTTP_ORIGIN + "/", "origin-when-cross-origin policy on a same-origin URL after cross-origin redirection");
+
+
+var referrerUrlWithCredentials = get_host_info().HTTP_ORIGIN.replace("http://", "http://username:password@");
+runTest(fetchedUrl, {referrer: referrerUrlWithCredentials}, get_host_info().HTTP_ORIGIN + "/", "Referrer with credentials should be stripped");
+var referrerUrlWithFragmentIdentifier = get_host_info().HTTP_ORIGIN + "#fragmentIdentifier";
+runTest(fetchedUrl, {referrer: referrerUrlWithFragmentIdentifier}, get_host_info().HTTP_ORIGIN + "/", "Referrer with fragment ID should be stripped");
diff --git a/test/wpt/tests/fetch/api/basic/request-forbidden-headers.any.js b/test/wpt/tests/fetch/api/basic/request-forbidden-headers.any.js
new file mode 100644
index 0000000..511ce60
--- /dev/null
+++ b/test/wpt/tests/fetch/api/basic/request-forbidden-headers.any.js
@@ -0,0 +1,100 @@
+// META: global=window,worker
+// META: script=../resources/utils.js
+
+function requestForbiddenHeaders(desc, forbiddenHeaders) {
+ var url = RESOURCES_DIR + "inspect-headers.py";
+ var requestInit = {"headers": forbiddenHeaders}
+ var urlParameters = "?headers=" + Object.keys(forbiddenHeaders).join("|");
+
+ promise_test(function(test){
+ return fetch(url + urlParameters, requestInit).then(function(resp) {
+ assert_equals(resp.status, 200, "HTTP status is 200");
+ assert_equals(resp.type , "basic", "Response's type is basic");
+ for (var header in forbiddenHeaders)
+ assert_not_equals(resp.headers.get("x-request-" + header), forbiddenHeaders[header], header + " does not have the value we defined");
+ });
+ }, desc);
+}
+
+function requestValidOverrideHeaders(desc, validHeaders) {
+ var url = RESOURCES_DIR + "inspect-headers.py";
+ var requestInit = {"headers": validHeaders}
+ var urlParameters = "?headers=" + Object.keys(validHeaders).join("|");
+
+ promise_test(function(test){
+ return fetch(url + urlParameters, requestInit).then(function(resp) {
+ assert_equals(resp.status, 200, "HTTP status is 200");
+ assert_equals(resp.type , "basic", "Response's type is basic");
+ for (var header in validHeaders)
+ assert_equals(resp.headers.get("x-request-" + header), validHeaders[header], header + "is not skipped for non-forbidden methods");
+ });
+ }, desc);
+}
+
+requestForbiddenHeaders("Accept-Charset is a forbidden request header", {"Accept-Charset": "utf-8"});
+requestForbiddenHeaders("Accept-Encoding is a forbidden request header", {"Accept-Encoding": ""});
+
+requestForbiddenHeaders("Access-Control-Request-Headers is a forbidden request header", {"Access-Control-Request-Headers": ""});
+requestForbiddenHeaders("Access-Control-Request-Method is a forbidden request header", {"Access-Control-Request-Method": ""});
+requestForbiddenHeaders(
+ 'Access-Control-Request-Private-Network is a forbidden request header',
+ {'Access-Control-Request-Private-Network': ''});
+requestForbiddenHeaders("Connection is a forbidden request header", {"Connection": "close"});
+requestForbiddenHeaders("Content-Length is a forbidden request header", {"Content-Length": "42"});
+requestForbiddenHeaders("Cookie is a forbidden request header", {"Cookie": "cookie=none"});
+requestForbiddenHeaders("Cookie2 is a forbidden request header", {"Cookie2": "cookie2=none"});
+requestForbiddenHeaders("Date is a forbidden request header", {"Date": "Wed, 04 May 1988 22:22:22 GMT"});
+requestForbiddenHeaders("DNT is a forbidden request header", {"DNT": "4"});
+requestForbiddenHeaders("Expect is a forbidden request header", {"Expect": "100-continue"});
+requestForbiddenHeaders("Host is a forbidden request header", {"Host": "http://wrong-host.com"});
+requestForbiddenHeaders("Keep-Alive is a forbidden request header", {"Keep-Alive": "timeout=15"});
+requestForbiddenHeaders("Origin is a forbidden request header", {"Origin": "http://wrong-origin.com"});
+requestForbiddenHeaders("Referer is a forbidden request header", {"Referer": "http://wrong-referer.com"});
+requestForbiddenHeaders("TE is a forbidden request header", {"TE": "trailers"});
+requestForbiddenHeaders("Trailer is a forbidden request header", {"Trailer": "Accept"});
+requestForbiddenHeaders("Transfer-Encoding is a forbidden request header", {"Transfer-Encoding": "chunked"});
+requestForbiddenHeaders("Upgrade is a forbidden request header", {"Upgrade": "HTTP/2.0"});
+requestForbiddenHeaders("Via is a forbidden request header", {"Via": "1.1 nowhere.com"});
+requestForbiddenHeaders("Proxy- is a forbidden request header", {"Proxy-": "value"});
+requestForbiddenHeaders("Proxy-Test is a forbidden request header", {"Proxy-Test": "value"});
+requestForbiddenHeaders("Sec- is a forbidden request header", {"Sec-": "value"});
+requestForbiddenHeaders("Sec-Test is a forbidden request header", {"Sec-Test": "value"});
+
+let forbiddenMethods = [
+ "TRACE",
+ "TRACK",
+ "CONNECT",
+ "trace",
+ "track",
+ "connect",
+ "trace,",
+ "GET,track ",
+ " connect",
+];
+
+let overrideHeaders = [
+ "x-http-method-override",
+ "x-http-method",
+ "x-method-override",
+ "X-HTTP-METHOD-OVERRIDE",
+ "X-HTTP-METHOD",
+ "X-METHOD-OVERRIDE",
+];
+
+for (forbiddenMethod of forbiddenMethods) {
+ for (overrideHeader of overrideHeaders) {
+ requestForbiddenHeaders(`header ${overrideHeader} is forbidden to use value ${forbiddenMethod}`, {[overrideHeader]: forbiddenMethod});
+ }
+}
+
+let permittedValues = [
+ "GETTRACE",
+ "GET",
+ "\",TRACE\",",
+];
+
+for (permittedValue of permittedValues) {
+ for (overrideHeader of overrideHeaders) {
+ requestValidOverrideHeaders(`header ${overrideHeader} is allowed to use value ${permittedValue}`, {[overrideHeader]: permittedValue});
+ }
+}
diff --git a/test/wpt/tests/fetch/api/basic/request-head.any.js b/test/wpt/tests/fetch/api/basic/request-head.any.js
new file mode 100644
index 0000000..e0b6afa
--- /dev/null
+++ b/test/wpt/tests/fetch/api/basic/request-head.any.js
@@ -0,0 +1,6 @@
+// META: global=window,worker
+
+promise_test(function(test) {
+ var requestInit = {"method": "HEAD", "body": "test"};
+ return promise_rejects_js(test, TypeError, fetch(".", requestInit));
+}, "Fetch with HEAD with body");
diff --git a/test/wpt/tests/fetch/api/basic/request-headers-case.any.js b/test/wpt/tests/fetch/api/basic/request-headers-case.any.js
new file mode 100644
index 0000000..4c10e71
--- /dev/null
+++ b/test/wpt/tests/fetch/api/basic/request-headers-case.any.js
@@ -0,0 +1,13 @@
+// META: global=window,worker
+
+promise_test(() => {
+ return fetch("/xhr/resources/echo-headers.py", {headers: [["THIS-is-A-test", 1], ["THIS-IS-A-TEST", 2]] }).then(res => res.text()).then(body => {
+ assert_regexp_match(body, /THIS-is-A-test: 1, 2/)
+ })
+}, "Multiple headers with the same name, different case (THIS-is-A-test first)")
+
+promise_test(() => {
+ return fetch("/xhr/resources/echo-headers.py", {headers: [["THIS-IS-A-TEST", 1], ["THIS-is-A-test", 2]] }).then(res => res.text()).then(body => {
+ assert_regexp_match(body, /THIS-IS-A-TEST: 1, 2/)
+ })
+}, "Multiple headers with the same name, different case (THIS-IS-A-TEST first)")
diff --git a/test/wpt/tests/fetch/api/basic/request-headers-nonascii.any.js b/test/wpt/tests/fetch/api/basic/request-headers-nonascii.any.js
new file mode 100644
index 0000000..4a9a801
--- /dev/null
+++ b/test/wpt/tests/fetch/api/basic/request-headers-nonascii.any.js
@@ -0,0 +1,29 @@
+// META: global=window,worker
+
+// This tests characters that are not
+// https://infra.spec.whatwg.org/#ascii-code-point
+// but are still
+// https://infra.spec.whatwg.org/#byte-value
+// in request header values.
+// Such request header values are valid and thus sent to servers.
+// Characters outside the #byte-value range are tested e.g. in
+// fetch/api/headers/headers-errors.html.
+
+promise_test(() => {
+ return fetch(
+ "../resources/inspect-headers.py?headers=accept|x-test",
+ {headers: {
+ "Accept": "before-æøå-after",
+ "X-Test": "before-ß-after"
+ }})
+ .then(res => {
+ assert_equals(
+ res.headers.get("x-request-accept"),
+ "before-æøå-after",
+ "Accept Header");
+ assert_equals(
+ res.headers.get("x-request-x-test"),
+ "before-ß-after",
+ "X-Test Header");
+ });
+}, "Non-ascii bytes in request headers");
diff --git a/test/wpt/tests/fetch/api/basic/request-headers.any.js b/test/wpt/tests/fetch/api/basic/request-headers.any.js
new file mode 100644
index 0000000..ac54256
--- /dev/null
+++ b/test/wpt/tests/fetch/api/basic/request-headers.any.js
@@ -0,0 +1,82 @@
+// META: global=window,worker
+// META: script=../resources/utils.js
+
+function checkContentType(contentType, body)
+{
+ if (self.FormData && body instanceof self.FormData) {
+ assert_true(contentType.startsWith("multipart/form-data; boundary="), "Request should have header content-type starting with multipart/form-data; boundary=, but got " + contentType);
+ return;
+ }
+
+ var expectedContentType = "text/plain;charset=UTF-8";
+ if(body === null || body instanceof ArrayBuffer || body.buffer instanceof ArrayBuffer)
+ expectedContentType = null;
+ else if (body instanceof Blob)
+ expectedContentType = body.type ? body.type : null;
+ else if (body instanceof URLSearchParams)
+ expectedContentType = "application/x-www-form-urlencoded;charset=UTF-8";
+
+ assert_equals(contentType , expectedContentType, "Request should have header content-type: " + expectedContentType);
+}
+
+function requestHeaders(desc, url, method, body, expectedOrigin, expectedContentLength) {
+ var urlParameters = "?headers=origin|user-agent|accept-charset|content-length|content-type";
+ var requestInit = {"method": method}
+ promise_test(function(test){
+ if (typeof body === "function")
+ body = body();
+ if (body)
+ requestInit["body"] = body;
+ return fetch(url + urlParameters, requestInit).then(function(resp) {
+ assert_equals(resp.status, 200, "HTTP status is 200");
+ assert_equals(resp.type , "basic", "Response's type is basic");
+ assert_true(resp.headers.has("x-request-user-agent"), "Request has header user-agent");
+ assert_false(resp.headers.has("accept-charset"), "Request has header accept-charset");
+ assert_equals(resp.headers.get("x-request-origin") , expectedOrigin, "Request should have header origin: " + expectedOrigin);
+ if (expectedContentLength !== undefined)
+ assert_equals(resp.headers.get("x-request-content-length") , expectedContentLength, "Request should have header content-length: " + expectedContentLength);
+ checkContentType(resp.headers.get("x-request-content-type"), body);
+ });
+ }, desc);
+}
+
+var url = RESOURCES_DIR + "inspect-headers.py"
+
+requestHeaders("Fetch with GET", url, "GET", null, null, null);
+requestHeaders("Fetch with HEAD", url, "HEAD", null, null, null);
+requestHeaders("Fetch with PUT without body", url, "POST", null, location.origin, "0");
+requestHeaders("Fetch with PUT with body", url, "PUT", "Request's body", location.origin, "14");
+requestHeaders("Fetch with POST without body", url, "POST", null, location.origin, "0");
+requestHeaders("Fetch with POST with text body", url, "POST", "Request's body", location.origin, "14");
+requestHeaders("Fetch with POST with FormData body", url, "POST", function() { return new FormData(); }, location.origin);
+requestHeaders("Fetch with POST with URLSearchParams body", url, "POST", function() { return new URLSearchParams("name=value"); }, location.origin, "10");
+requestHeaders("Fetch with POST with Blob body", url, "POST", new Blob(["Test"]), location.origin, "4");
+requestHeaders("Fetch with POST with ArrayBuffer body", url, "POST", new ArrayBuffer(4), location.origin, "4");
+requestHeaders("Fetch with POST with Uint8Array body", url, "POST", new Uint8Array(4), location.origin, "4");
+requestHeaders("Fetch with POST with Int8Array body", url, "POST", new Int8Array(4), location.origin, "4");
+requestHeaders("Fetch with POST with Float32Array body", url, "POST", new Float32Array(1), location.origin, "4");
+requestHeaders("Fetch with POST with Float64Array body", url, "POST", new Float64Array(1), location.origin, "8");
+requestHeaders("Fetch with POST with DataView body", url, "POST", new DataView(new ArrayBuffer(8), 0, 4), location.origin, "4");
+requestHeaders("Fetch with POST with Blob body with mime type", url, "POST", new Blob(["Test"], { type: "text/maybe" }), location.origin, "4");
+requestHeaders("Fetch with Chicken", url, "Chicken", null, location.origin, null);
+requestHeaders("Fetch with Chicken with body", url, "Chicken", "Request's body", location.origin, "14");
+
+function requestOriginHeader(method, mode, needsOrigin) {
+ promise_test(function(test){
+ return fetch(url + "?headers=origin", {method:method, mode:mode}).then(function(resp) {
+ assert_equals(resp.status, 200, "HTTP status is 200");
+ assert_equals(resp.type , "basic", "Response's type is basic");
+ if(needsOrigin)
+ assert_equals(resp.headers.get("x-request-origin") , location.origin, "Request should have an Origin header with origin: " + location.origin);
+ else
+ assert_equals(resp.headers.get("x-request-origin"), null, "Request should not have an Origin header")
+ });
+ }, "Fetch with " + method + " and mode \"" + mode + "\" " + (needsOrigin ? "needs" : "does not need") + " an Origin header");
+}
+
+requestOriginHeader("GET", "cors", false);
+requestOriginHeader("POST", "same-origin", true);
+requestOriginHeader("POST", "no-cors", true);
+requestOriginHeader("PUT", "same-origin", true);
+requestOriginHeader("TacO", "same-origin", true);
+requestOriginHeader("TacO", "cors", true);
diff --git a/test/wpt/tests/fetch/api/basic/request-referrer-redirected-worker.html b/test/wpt/tests/fetch/api/basic/request-referrer-redirected-worker.html
new file mode 100644
index 0000000..bdea1e1
--- /dev/null
+++ b/test/wpt/tests/fetch/api/basic/request-referrer-redirected-worker.html
@@ -0,0 +1,17 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>Fetch in worker: referrer header</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ </head>
+ <body>
+ <script>
+ let finalURL = "/fetch/api/basic/request-referrer.any.worker.js";
+ let url = "/fetch/api/resources/redirect.py?location=" +
+ encodeURIComponent(finalURL);
+ fetch_tests_from_worker(new Worker(url));
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/fetch/api/basic/request-referrer.any.js b/test/wpt/tests/fetch/api/basic/request-referrer.any.js
new file mode 100644
index 0000000..0c33576
--- /dev/null
+++ b/test/wpt/tests/fetch/api/basic/request-referrer.any.js
@@ -0,0 +1,24 @@
+// META: global=window,worker
+// META: script=../resources/utils.js
+
+function testReferrer(referrer, expected, desc) {
+ promise_test(function(test) {
+ var url = RESOURCES_DIR + "inspect-headers.py?headers=referer"
+ var req = new Request(url, { referrer: referrer });
+ return fetch(req).then(function(resp) {
+ var actual = resp.headers.get("x-request-referer");
+ if (expected) {
+ assert_equals(actual, expected, "request's referer should be: " + expected);
+ return;
+ }
+ if (actual) {
+ assert_equals(actual, "", "request's referer should be empty");
+ }
+ });
+ }, desc);
+}
+
+testReferrer("about:client", self.location.href, 'about:client referrer');
+
+var fooURL = new URL("./foo", self.location).href;
+testReferrer(fooURL, fooURL, 'url referrer');
diff --git a/test/wpt/tests/fetch/api/basic/request-upload.any.js b/test/wpt/tests/fetch/api/basic/request-upload.any.js
new file mode 100644
index 0000000..9168aa1
--- /dev/null
+++ b/test/wpt/tests/fetch/api/basic/request-upload.any.js
@@ -0,0 +1,135 @@
+// META: global=window,worker
+// META: script=../resources/utils.js
+// META: script=/common/utils.js
+// META: script=/common/get-host-info.sub.js
+
+function testUpload(desc, url, method, createBody, expectedBody) {
+ const requestInit = {method};
+ promise_test(function(test){
+ const body = createBody();
+ if (body) {
+ requestInit["body"] = body;
+ requestInit.duplex = "half";
+ }
+ return fetch(url, requestInit).then(function(resp) {
+ return resp.text().then((text)=> {
+ assert_equals(text, expectedBody);
+ });
+ });
+ }, desc);
+}
+
+function testUploadFailure(desc, url, method, createBody) {
+ const requestInit = {method};
+ promise_test(t => {
+ const body = createBody();
+ if (body) {
+ requestInit["body"] = body;
+ }
+ return promise_rejects_js(t, TypeError, fetch(url, requestInit));
+ }, desc);
+}
+
+const url = RESOURCES_DIR + "echo-content.py"
+
+testUpload("Fetch with PUT with body", url,
+ "PUT",
+ () => "Request's body",
+ "Request's body");
+testUpload("Fetch with POST with text body", url,
+ "POST",
+ () => "Request's body",
+ "Request's body");
+testUpload("Fetch with POST with URLSearchParams body", url,
+ "POST",
+ () => new URLSearchParams("name=value"),
+ "name=value");
+testUpload("Fetch with POST with Blob body", url,
+ "POST",
+ () => new Blob(["Test"]),
+ "Test");
+testUpload("Fetch with POST with ArrayBuffer body", url,
+ "POST",
+ () => new ArrayBuffer(4),
+ "\0\0\0\0");
+testUpload("Fetch with POST with Uint8Array body", url,
+ "POST",
+ () => new Uint8Array(4),
+ "\0\0\0\0");
+testUpload("Fetch with POST with Int8Array body", url,
+ "POST",
+ () => new Int8Array(4),
+ "\0\0\0\0");
+testUpload("Fetch with POST with Float32Array body", url,
+ "POST",
+ () => new Float32Array(1),
+ "\0\0\0\0");
+testUpload("Fetch with POST with Float64Array body", url,
+ "POST",
+ () => new Float64Array(1),
+ "\0\0\0\0\0\0\0\0");
+testUpload("Fetch with POST with DataView body", url,
+ "POST",
+ () => new DataView(new ArrayBuffer(8), 0, 4),
+ "\0\0\0\0");
+testUpload("Fetch with POST with Blob body with mime type", url,
+ "POST",
+ () => new Blob(["Test"], { type: "text/maybe" }),
+ "Test");
+
+testUploadFailure("Fetch with POST with ReadableStream containing String", url,
+ "POST",
+ () => {
+ return new ReadableStream({start: controller => {
+ controller.enqueue("Test");
+ controller.close();
+ }})
+ });
+testUploadFailure("Fetch with POST with ReadableStream containing null", url,
+ "POST",
+ () => {
+ return new ReadableStream({start: controller => {
+ controller.enqueue(null);
+ controller.close();
+ }})
+ });
+testUploadFailure("Fetch with POST with ReadableStream containing number", url,
+ "POST",
+ () => {
+ return new ReadableStream({start: controller => {
+ controller.enqueue(99);
+ controller.close();
+ }})
+ });
+testUploadFailure("Fetch with POST with ReadableStream containing ArrayBuffer", url,
+ "POST",
+ () => {
+ return new ReadableStream({start: controller => {
+ controller.enqueue(new ArrayBuffer());
+ controller.close();
+ }})
+ });
+testUploadFailure("Fetch with POST with ReadableStream containing Blob", url,
+ "POST",
+ () => {
+ return new ReadableStream({start: controller => {
+ controller.enqueue(new Blob());
+ controller.close();
+ }})
+ });
+
+promise_test(async (test) => {
+ const resp = await fetch(
+ "/fetch/connection-pool/resources/network-partition-key.py?"
+ + `status=421&uuid=${token()}&partition_id=${get_host_info().ORIGIN}`
+ + `&dispatch=check_partition&addcounter=true`,
+ {method: "POST", body: "foobar"});
+ assert_equals(resp.status, 421);
+ const text = await resp.text();
+ assert_equals(text, "ok. Request was sent 2 times. 2 connections were created.");
+}, "Fetch with POST with text body on 421 response should be retried once on new connection.");
+
+promise_test(async (test) => {
+ const body = new ReadableStream({start: c => c.close()});
+ await promise_rejects_js(test, TypeError, fetch('/', {method: 'POST', body}));
+}, "Streaming upload shouldn't work on Http/1.1.");
diff --git a/test/wpt/tests/fetch/api/basic/request-upload.h2.any.js b/test/wpt/tests/fetch/api/basic/request-upload.h2.any.js
new file mode 100644
index 0000000..eedc2bf
--- /dev/null
+++ b/test/wpt/tests/fetch/api/basic/request-upload.h2.any.js
@@ -0,0 +1,186 @@
+// META: global=window,worker
+// META: script=../resources/utils.js
+// META: script=/common/utils.js
+// META: script=/common/get-host-info.sub.js
+
+const duplex = "half";
+
+async function assertUpload(url, method, createBody, expectedBody) {
+ const requestInit = {method};
+ const body = createBody();
+ if (body) {
+ requestInit["body"] = body;
+ requestInit.duplex = "half";
+ }
+ const resp = await fetch(url, requestInit);
+ const text = await resp.text();
+ assert_equals(text, expectedBody);
+}
+
+function testUpload(desc, url, method, createBody, expectedBody) {
+ promise_test(async () => {
+ await assertUpload(url, method, createBody, expectedBody);
+ }, desc);
+}
+
+function createStream(chunks) {
+ return new ReadableStream({
+ start: (controller) => {
+ for (const chunk of chunks) {
+ controller.enqueue(chunk);
+ }
+ controller.close();
+ }
+ });
+}
+
+const url = RESOURCES_DIR + "echo-content.h2.py"
+
+testUpload("Fetch with POST with empty ReadableStream", url,
+ "POST",
+ () => {
+ return new ReadableStream({start: controller => {
+ controller.close();
+ }})
+ },
+ "");
+
+testUpload("Fetch with POST with ReadableStream", url,
+ "POST",
+ () => {
+ return new ReadableStream({start: controller => {
+ const encoder = new TextEncoder();
+ controller.enqueue(encoder.encode("Test"));
+ controller.close();
+ }})
+ },
+ "Test");
+
+promise_test(async (test) => {
+ const body = new ReadableStream({start: controller => {
+ const encoder = new TextEncoder();
+ controller.enqueue(encoder.encode("Test"));
+ controller.close();
+ }});
+ const resp = await fetch(
+ "/fetch/connection-pool/resources/network-partition-key.py?"
+ + `status=421&uuid=${token()}&partition_id=${self.origin}`
+ + `&dispatch=check_partition&addcounter=true`,
+ {method: "POST", body: body, duplex});
+ assert_equals(resp.status, 421);
+ const text = await resp.text();
+ assert_equals(text, "ok. Request was sent 1 times. 1 connections were created.");
+}, "Fetch with POST with ReadableStream on 421 response should return the response and not retry.");
+
+promise_test(async (test) => {
+ const request = new Request('', {
+ body: new ReadableStream(),
+ method: 'POST',
+ duplex,
+ });
+
+ assert_equals(request.headers.get('Content-Type'), null, `Request should not have a content-type set`);
+
+ const response = await fetch('data:a/a;charset=utf-8,test', {
+ method: 'POST',
+ body: new ReadableStream(),
+ duplex,
+ });
+
+ assert_equals(await response.text(), 'test', `Response has correct body`);
+}, "Feature detect for POST with ReadableStream");
+
+promise_test(async (test) => {
+ const request = new Request('data:a/a;charset=utf-8,test', {
+ body: new ReadableStream(),
+ method: 'POST',
+ duplex,
+ });
+
+ assert_equals(request.headers.get('Content-Type'), null, `Request should not have a content-type set`);
+ const response = await fetch(request);
+ assert_equals(await response.text(), 'test', `Response has correct body`);
+}, "Feature detect for POST with ReadableStream, using request object");
+
+test(() => {
+ let duplexAccessed = false;
+
+ const request = new Request("", {
+ body: new ReadableStream(),
+ method: "POST",
+ get duplex() {
+ duplexAccessed = true;
+ return "half";
+ },
+ });
+
+ assert_equals(
+ request.headers.get("Content-Type"),
+ null,
+ `Request should not have a content-type set`
+ );
+ assert_true(duplexAccessed, `duplex dictionary property should be accessed`);
+}, "Synchronous feature detect");
+
+// The asserts the synchronousFeatureDetect isn't broken by a partial implementation.
+// An earlier feature detect was broken by Safari implementing streaming bodies as part of Request,
+// but it failed when passed to fetch().
+// This tests ensures that UAs must not implement RequestInit.duplex and streaming request bodies without also implementing the fetch() parts.
+promise_test(async () => {
+ let duplexAccessed = false;
+
+ const request = new Request("", {
+ body: new ReadableStream(),
+ method: "POST",
+ get duplex() {
+ duplexAccessed = true;
+ return "half";
+ },
+ });
+
+ const supported =
+ request.headers.get("Content-Type") === null && duplexAccessed;
+
+ // If the feature detect fails, assume the browser is being truthful (other tests pick up broken cases here)
+ if (!supported) return false;
+
+ await assertUpload(
+ url,
+ "POST",
+ () =>
+ new ReadableStream({
+ start: (controller) => {
+ const encoder = new TextEncoder();
+ controller.enqueue(encoder.encode("Test"));
+ controller.close();
+ },
+ }),
+ "Test"
+ );
+}, "Synchronous feature detect fails if feature unsupported");
+
+promise_test(async (t) => {
+ const body = createStream(["hello"]);
+ const method = "POST";
+ await promise_rejects_js(t, TypeError, fetch(url, { method, body, duplex }));
+}, "Streaming upload with body containing a String");
+
+promise_test(async (t) => {
+ const body = createStream([null]);
+ const method = "POST";
+ await promise_rejects_js(t, TypeError, fetch(url, { method, body, duplex }));
+}, "Streaming upload with body containing null");
+
+promise_test(async (t) => {
+ const body = createStream([33]);
+ const method = "POST";
+ await promise_rejects_js(t, TypeError, fetch(url, { method, body, duplex }));
+}, "Streaming upload with body containing a number");
+
+promise_test(async (t) => {
+ const url = "/fetch/api/resources/authentication.py?realm=test";
+ const body = createStream([]);
+ const method = "POST";
+ await promise_rejects_js(t, TypeError, fetch(url, { method, body, duplex }));
+}, "Streaming upload should fail on a 401 response");
+
diff --git a/test/wpt/tests/fetch/api/basic/response-null-body.any.js b/test/wpt/tests/fetch/api/basic/response-null-body.any.js
new file mode 100644
index 0000000..bb05892
--- /dev/null
+++ b/test/wpt/tests/fetch/api/basic/response-null-body.any.js
@@ -0,0 +1,38 @@
+// META: global=window,worker
+// META: script=../resources/utils.js
+
+const nullBodyStatus = [204, 205, 304];
+const methods = ["GET", "POST", "OPTIONS"];
+
+for (const status of nullBodyStatus) {
+ for (const method of methods) {
+ promise_test(
+ async () => {
+ const url =
+ `${RESOURCES_DIR}status.py?code=${status}&content=hello-world`;
+ const resp = await fetch(url, { method });
+ assert_equals(resp.status, status);
+ assert_equals(resp.body, null, "the body should be null");
+ const text = await resp.text();
+ assert_equals(text, "", "null bodies result in empty text");
+ },
+ `Response.body is null for responses with status=${status} (method=${method})`,
+ );
+ }
+}
+
+promise_test(async () => {
+ const url = `${RESOURCES_DIR}status.py?code=200&content=hello-world`;
+ const resp = await fetch(url, { method: "HEAD" });
+ assert_equals(resp.status, 200);
+ assert_equals(resp.body, null, "the body should be null");
+ const text = await resp.text();
+ assert_equals(text, "", "null bodies result in empty text");
+}, `Response.body is null for responses with method=HEAD`);
+
+promise_test(async (t) => {
+ const integrity = "sha384-UT6f7WCFp32YJnp1is4l/ZYnOeQKpE8xjmdkLOwZ3nIP+tmT2aMRFQGJomjVf5cE";
+ const url = `${RESOURCES_DIR}status.py?code=204&content=hello-world`;
+ const promise = fetch(url, { method: "GET", integrity });
+ promise_rejects_js(t, TypeError, promise);
+}, "Null body status with subresource integrity should abort");
diff --git a/test/wpt/tests/fetch/api/basic/response-url.sub.any.js b/test/wpt/tests/fetch/api/basic/response-url.sub.any.js
new file mode 100644
index 0000000..0d123c4
--- /dev/null
+++ b/test/wpt/tests/fetch/api/basic/response-url.sub.any.js
@@ -0,0 +1,16 @@
+function checkResponseURL(fetchedURL, expectedURL)
+{
+ promise_test(function() {
+ return fetch(fetchedURL).then(function(response) {
+ assert_equals(response.url, expectedURL);
+ });
+ }, "Testing response url getter with " +fetchedURL);
+}
+
+var baseURL = "http://{{host}}:{{ports[http][0]}}";
+checkResponseURL(baseURL + "/ada", baseURL + "/ada");
+checkResponseURL(baseURL + "/#", baseURL + "/");
+checkResponseURL(baseURL + "/#ada", baseURL + "/");
+checkResponseURL(baseURL + "#ada", baseURL + "/");
+
+done();
diff --git a/test/wpt/tests/fetch/api/basic/scheme-about.any.js b/test/wpt/tests/fetch/api/basic/scheme-about.any.js
new file mode 100644
index 0000000..9ef4418
--- /dev/null
+++ b/test/wpt/tests/fetch/api/basic/scheme-about.any.js
@@ -0,0 +1,26 @@
+// META: global=window,worker
+// META: script=../resources/utils.js
+
+function checkNetworkError(url, method) {
+ method = method || "GET";
+ const desc = "Fetching " + url.substring(0, 45) + " with method " + method + " is KO"
+ promise_test(function(test) {
+ var promise = fetch(url, { method: method });
+ return promise_rejects_js(test, TypeError, promise);
+ }, desc);
+}
+
+checkNetworkError("about:blank", "GET");
+checkNetworkError("about:blank", "PUT");
+checkNetworkError("about:blank", "POST");
+checkNetworkError("about:invalid.com");
+checkNetworkError("about:config");
+checkNetworkError("about:unicorn");
+
+promise_test(function(test) {
+ var promise = fetch("about:blank", {
+ "method": "GET",
+ "Range": "bytes=1-10"
+ });
+ return promise_rejects_js(test, TypeError, promise);
+}, "Fetching about:blank with range header does not affect behavior");
diff --git a/test/wpt/tests/fetch/api/basic/scheme-blob.sub.any.js b/test/wpt/tests/fetch/api/basic/scheme-blob.sub.any.js
new file mode 100644
index 0000000..8afdc03
--- /dev/null
+++ b/test/wpt/tests/fetch/api/basic/scheme-blob.sub.any.js
@@ -0,0 +1,125 @@
+// META: script=../resources/utils.js
+
+function checkFetchResponse(url, data, mime, size, desc) {
+ promise_test(function(test) {
+ size = size.toString();
+ return fetch(url).then(function(resp) {
+ assert_equals(resp.status, 200, "HTTP status is 200");
+ assert_equals(resp.type, "basic", "response type is basic");
+ assert_equals(resp.headers.get("Content-Type"), mime, "Content-Type is " + resp.headers.get("Content-Type"));
+ assert_equals(resp.headers.get("Content-Length"), size, "Content-Length is " + resp.headers.get("Content-Length"));
+ return resp.text();
+ }).then(function(bodyAsText) {
+ assert_equals(bodyAsText, data, "Response's body is " + data);
+ });
+ }, desc);
+}
+
+var blob = new Blob(["Blob's data"], { "type" : "text/plain" });
+checkFetchResponse(URL.createObjectURL(blob), "Blob's data", "text/plain", blob.size,
+ "Fetching [GET] URL.createObjectURL(blob) is OK");
+
+function checkKoUrl(url, method, desc) {
+ promise_test(function(test) {
+ var promise = fetch(url, {"method": method});
+ return promise_rejects_js(test, TypeError, promise);
+ }, desc);
+}
+
+var blob2 = new Blob(["Blob's data"], { "type" : "text/plain" });
+checkKoUrl("blob:http://{{domains[www]}}:{{ports[http][0]}}/", "GET",
+ "Fetching [GET] blob:http://{{domains[www]}}:{{ports[http][0]}}/ is KO");
+
+var invalidRequestMethods = [
+ "POST",
+ "OPTIONS",
+ "HEAD",
+ "PUT",
+ "DELETE",
+ "INVALID",
+];
+invalidRequestMethods.forEach(function(method) {
+ checkKoUrl(URL.createObjectURL(blob2), method, "Fetching [" + method + "] URL.createObjectURL(blob) is KO");
+});
+
+checkKoUrl("blob:not-backed-by-a-blob/", "GET",
+ "Fetching [GET] blob:not-backed-by-a-blob/ is KO");
+
+let empty_blob = new Blob([]);
+checkFetchResponse(URL.createObjectURL(empty_blob), "", "", 0,
+ "Fetching URL.createObjectURL(empty_blob) is OK");
+
+let empty_type_blob = new Blob([], {type: ""});
+checkFetchResponse(URL.createObjectURL(empty_type_blob), "", "", 0,
+ "Fetching URL.createObjectURL(empty_type_blob) is OK");
+
+let empty_data_blob = new Blob([], {type: "text/plain"});
+checkFetchResponse(URL.createObjectURL(empty_data_blob), "", "text/plain", 0,
+ "Fetching URL.createObjectURL(empty_data_blob) is OK");
+
+let invalid_type_blob = new Blob([], {type: "invalid"});
+checkFetchResponse(URL.createObjectURL(invalid_type_blob), "", "", 0,
+ "Fetching URL.createObjectURL(invalid_type_blob) is OK");
+
+promise_test(function(test) {
+ return fetch("/images/blue.png").then(function(resp) {
+ return resp.arrayBuffer();
+ }).then(function(image_buffer) {
+ let blob = new Blob([image_buffer]);
+ return fetch(URL.createObjectURL(blob)).then(function(resp) {
+ assert_equals(resp.status, 200, "HTTP status is 200");
+ assert_equals(resp.type, "basic", "response type is basic");
+ assert_equals(resp.headers.get("Content-Type"), "", "Content-Type is " + resp.headers.get("Content-Type"));
+ })
+ });
+}, "Blob content is not sniffed for a content type [image/png]");
+
+let simple_xml_string = '<?xml version="1.0" encoding="UTF-8"?><x></x>';
+let xml_blob_no_type = new Blob([simple_xml_string]);
+checkFetchResponse(URL.createObjectURL(xml_blob_no_type), simple_xml_string, "", 45,
+ "Blob content is not sniffed for a content type [text/xml]");
+
+let simple_text_string = 'Hello, World!';
+promise_test(function(test) {
+ let blob = new Blob([simple_text_string], {"type": "text/plain"});
+ let slice = blob.slice(7, simple_text_string.length, "\0");
+ return fetch(URL.createObjectURL(slice)).then(function (resp) {
+ assert_equals(resp.status, 200, "HTTP status is 200");
+ assert_equals(resp.type, "basic", "response type is basic");
+ assert_equals(resp.headers.get("Content-Type"), "");
+ assert_equals(resp.headers.get("Content-Length"), "6");
+ return resp.text();
+ }).then(function(bodyAsText) {
+ assert_equals(bodyAsText, "World!");
+ });
+}, "Set content type to the empty string for slice with invalid content type");
+
+promise_test(function(test) {
+ let blob = new Blob([simple_text_string], {"type": "text/plain"});
+ let slice = blob.slice(7, simple_text_string.length, "\0");
+ return fetch(URL.createObjectURL(slice)).then(function (resp) {
+ assert_equals(resp.status, 200, "HTTP status is 200");
+ assert_equals(resp.type, "basic", "response type is basic");
+ assert_equals(resp.headers.get("Content-Type"), "");
+ assert_equals(resp.headers.get("Content-Length"), "6");
+ return resp.text();
+ }).then(function(bodyAsText) {
+ assert_equals(bodyAsText, "World!");
+ });
+}, "Set content type to the empty string for slice with no content type ");
+
+promise_test(function(test) {
+ let blob = new Blob([simple_xml_string]);
+ let slice = blob.slice(0, 38);
+ return fetch(URL.createObjectURL(slice)).then(function (resp) {
+ assert_equals(resp.status, 200, "HTTP status is 200");
+ assert_equals(resp.type, "basic", "response type is basic");
+ assert_equals(resp.headers.get("Content-Type"), "");
+ assert_equals(resp.headers.get("Content-Length"), "38");
+ return resp.text();
+ }).then(function(bodyAsText) {
+ assert_equals(bodyAsText, '<?xml version="1.0" encoding="UTF-8"?>');
+ });
+}, "Blob.slice should not sniff the content for a content type");
+
+done();
diff --git a/test/wpt/tests/fetch/api/basic/scheme-data.any.js b/test/wpt/tests/fetch/api/basic/scheme-data.any.js
new file mode 100644
index 0000000..55df43b
--- /dev/null
+++ b/test/wpt/tests/fetch/api/basic/scheme-data.any.js
@@ -0,0 +1,43 @@
+// META: global=window,worker
+// META: script=../resources/utils.js
+
+function checkFetchResponse(url, data, mime, fetchMode, method) {
+ var cut = (url.length >= 40) ? "[...]" : "";
+ var desc = "Fetching " + (method ? "[" + method + "] " : "") + url.substring(0, 40) + cut + " is OK";
+ var init = {"method": method || "GET"};
+ if (fetchMode) {
+ init.mode = fetchMode;
+ desc += " (" + fetchMode + ")";
+ }
+ promise_test(function(test) {
+ return fetch(url, init).then(function(resp) {
+ assert_equals(resp.status, 200, "HTTP status is 200");
+ assert_equals(resp.statusText, "OK", "HTTP statusText is OK");
+ assert_equals(resp.type, "basic", "response type is basic");
+ assert_equals(resp.headers.get("Content-Type"), mime, "Content-Type is " + resp.headers.get("Content-Type"));
+ return resp.text();
+ }).then(function(body) {
+ assert_equals(body, data, "Response's body is correct");
+ });
+ }, desc);
+}
+
+checkFetchResponse("data:,response%27s%20body", "response's body", "text/plain;charset=US-ASCII");
+checkFetchResponse("data:,response%27s%20body", "response's body", "text/plain;charset=US-ASCII", "same-origin");
+checkFetchResponse("data:,response%27s%20body", "response's body", "text/plain;charset=US-ASCII", "cors");
+checkFetchResponse("data:text/plain;base64,cmVzcG9uc2UncyBib2R5", "response's body", "text/plain");
+checkFetchResponse("data:image/png;base64,cmVzcG9uc2UncyBib2R5",
+ "response's body",
+ "image/png");
+checkFetchResponse("data:,response%27s%20body", "response's body", "text/plain;charset=US-ASCII", null, "POST");
+checkFetchResponse("data:,response%27s%20body", "", "text/plain;charset=US-ASCII", null, "HEAD");
+
+function checkKoUrl(url, method, desc) {
+ var cut = (url.length >= 40) ? "[...]" : "";
+ desc = "Fetching [" + method + "] " + url.substring(0, 45) + cut + " is KO"
+ promise_test(function(test) {
+ return promise_rejects_js(test, TypeError, fetch(url, {"method": method}));
+ }, desc);
+}
+
+checkKoUrl("data:notAdataUrl.com", "GET");
diff --git a/test/wpt/tests/fetch/api/basic/scheme-others.sub.any.js b/test/wpt/tests/fetch/api/basic/scheme-others.sub.any.js
new file mode 100644
index 0000000..550f69c
--- /dev/null
+++ b/test/wpt/tests/fetch/api/basic/scheme-others.sub.any.js
@@ -0,0 +1,31 @@
+// META: global=window,worker
+// META: script=../resources/utils.js
+
+function checkKoUrl(url, desc) {
+ if (!desc)
+ desc = "Fetching " + url.substring(0, 45) + " is KO"
+ promise_test(function(test) {
+ var promise = fetch(url);
+ return promise_rejects_js(test, TypeError, promise);
+ }, desc);
+}
+
+var urlWithoutScheme = "://{{host}}:{{ports[http][0]}}/";
+checkKoUrl("aaa" + urlWithoutScheme);
+checkKoUrl("cap" + urlWithoutScheme);
+checkKoUrl("cid" + urlWithoutScheme);
+checkKoUrl("dav" + urlWithoutScheme);
+checkKoUrl("dict" + urlWithoutScheme);
+checkKoUrl("dns" + urlWithoutScheme);
+checkKoUrl("geo" + urlWithoutScheme);
+checkKoUrl("im" + urlWithoutScheme);
+checkKoUrl("imap" + urlWithoutScheme);
+checkKoUrl("ipp" + urlWithoutScheme);
+checkKoUrl("ldap" + urlWithoutScheme);
+checkKoUrl("mailto" + urlWithoutScheme);
+checkKoUrl("nfs" + urlWithoutScheme);
+checkKoUrl("pop" + urlWithoutScheme);
+checkKoUrl("rtsp" + urlWithoutScheme);
+checkKoUrl("snmp" + urlWithoutScheme);
+
+done();
diff --git a/test/wpt/tests/fetch/api/basic/status.h2.any.js b/test/wpt/tests/fetch/api/basic/status.h2.any.js
new file mode 100644
index 0000000..99fec88
--- /dev/null
+++ b/test/wpt/tests/fetch/api/basic/status.h2.any.js
@@ -0,0 +1,17 @@
+// See also /xhr/status.h2.window.js
+
+[
+ 200,
+ 210,
+ 400,
+ 404,
+ 410,
+ 500,
+ 502
+].forEach(status => {
+ promise_test(async t => {
+ const response = await fetch("/xhr/resources/status.py?code=" + status);
+ assert_equals(response.status, status, "status should be " + status);
+ assert_equals(response.statusText, "", "statusText should be the empty string");
+ }, "statusText over H2 for status " + status + " should be the empty string");
+});
diff --git a/test/wpt/tests/fetch/api/basic/stream-response.any.js b/test/wpt/tests/fetch/api/basic/stream-response.any.js
new file mode 100644
index 0000000..d964dda
--- /dev/null
+++ b/test/wpt/tests/fetch/api/basic/stream-response.any.js
@@ -0,0 +1,40 @@
+// META: global=window,worker
+// META: script=../resources/utils.js
+
+function streamBody(reader, test, count = 0) {
+ return reader.read().then(function(data) {
+ if (!data.done && count < 2) {
+ count += 1;
+ return streamBody(reader, test, count);
+ } else {
+ test.step(function() {
+ assert_true(count >= 2, "Retrieve body progressively");
+ });
+ }
+ });
+}
+
+//simulate streaming:
+//count is large enough to let the UA deliver the body before it is completely retrieved
+promise_test(function(test) {
+ return fetch(RESOURCES_DIR + "trickle.py?ms=30&count=100").then(function(resp) {
+ if (resp.body)
+ return streamBody(resp.body.getReader(), test);
+ else
+ test.step(function() {
+ assert_unreached( "Body does not exist in response");
+ });
+ });
+}, "Stream response's body when content-type is present");
+
+// This test makes sure that the response body is not buffered if no content type is provided.
+promise_test(function(test) {
+ return fetch(RESOURCES_DIR + "trickle.py?ms=300&count=10&notype=true").then(function(resp) {
+ if (resp.body)
+ return streamBody(resp.body.getReader(), test);
+ else
+ test.step(function() {
+ assert_unreached( "Body does not exist in response");
+ });
+ });
+}, "Stream response's body when content-type is not present");
diff --git a/test/wpt/tests/fetch/api/basic/stream-safe-creation.any.js b/test/wpt/tests/fetch/api/basic/stream-safe-creation.any.js
new file mode 100644
index 0000000..382efc1
--- /dev/null
+++ b/test/wpt/tests/fetch/api/basic/stream-safe-creation.any.js
@@ -0,0 +1,54 @@
+// META: global=window,worker
+
+// These tests verify that stream creation is not affected by changes to
+// Object.prototype.
+
+const creationCases = {
+ fetch: async () => fetch(location.href),
+ request: () => new Request(location.href, {method: 'POST', body: 'hi'}),
+ response: () => new Response('bye'),
+ consumeEmptyResponse: () => new Response().text(),
+ consumeNonEmptyResponse: () => new Response(new Uint8Array([64])).text(),
+ consumeEmptyRequest: () => new Request(location.href).text(),
+ consumeNonEmptyRequest: () => new Request(location.href,
+ {method: 'POST', body: 'yes'}).arrayBuffer(),
+};
+
+for (const creationCase of Object.keys(creationCases)) {
+ for (const accessorName of ['start', 'type', 'size', 'highWaterMark']) {
+ promise_test(async t => {
+ Object.defineProperty(Object.prototype, accessorName, {
+ get() { throw Error(`Object.prototype.${accessorName} was accessed`); },
+ configurable: true
+ });
+ t.add_cleanup(() => {
+ delete Object.prototype[accessorName];
+ return Promise.resolve();
+ });
+ await creationCases[creationCase]();
+ }, `throwing Object.prototype.${accessorName} accessor should not affect ` +
+ `stream creation by '${creationCase}'`);
+
+ promise_test(async t => {
+ // -1 is a convenient value which is invalid, and should cause the
+ // constructor to throw, for all four fields.
+ Object.prototype[accessorName] = -1;
+ t.add_cleanup(() => {
+ delete Object.prototype[accessorName];
+ return Promise.resolve();
+ });
+ await creationCases[creationCase]();
+ }, `Object.prototype.${accessorName} accessor returning invalid value ` +
+ `should not affect stream creation by '${creationCase}'`);
+ }
+
+ promise_test(async t => {
+ Object.prototype.start = controller => controller.error(new Error('start'));
+ t.add_cleanup(() => {
+ delete Object.prototype.start;
+ return Promise.resolve();
+ });
+ await creationCases[creationCase]();
+ }, `Object.prototype.start function which errors the stream should not ` +
+ `affect stream creation by '${creationCase}'`);
+}
diff --git a/test/wpt/tests/fetch/api/basic/text-utf8.any.js b/test/wpt/tests/fetch/api/basic/text-utf8.any.js
new file mode 100644
index 0000000..05c8c88
--- /dev/null
+++ b/test/wpt/tests/fetch/api/basic/text-utf8.any.js
@@ -0,0 +1,74 @@
+// META: title=Fetch: Request and Response text() should decode as UTF-8
+// META: global=window,worker
+// META: script=../resources/utils.js
+
+function testTextDecoding(body, expectedText, urlParameter, title)
+{
+ var arrayBuffer = stringToArray(body);
+
+ promise_test(function(test) {
+ var request = new Request("", {method: "POST", body: arrayBuffer});
+ return request.text().then(function(value) {
+ assert_equals(value, expectedText, "Request.text() should decode data as UTF-8");
+ });
+ }, title + " with Request.text()");
+
+ promise_test(function(test) {
+ var response = new Response(arrayBuffer);
+ return response.text().then(function(value) {
+ assert_equals(value, expectedText, "Response.text() should decode data as UTF-8");
+ });
+ }, title + " with Response.text()");
+
+ promise_test(function(test) {
+ return fetch("../resources/status.py?code=200&type=text%2Fplain%3Bcharset%3DUTF-8&content=" + urlParameter).then(function(response) {
+ return response.text().then(function(value) {
+ assert_equals(value, expectedText, "Fetched Response.text() should decode data as UTF-8");
+ });
+ });
+ }, title + " with fetched data (UTF-8 charset)");
+
+ promise_test(function(test) {
+ return fetch("../resources/status.py?code=200&type=text%2Fplain%3Bcharset%3DUTF-16&content=" + urlParameter).then(function(response) {
+ return response.text().then(function(value) {
+ assert_equals(value, expectedText, "Fetched Response.text() should decode data as UTF-8");
+ });
+ });
+ }, title + " with fetched data (UTF-16 charset)");
+
+ promise_test(function(test) {
+ return new Response(body).arrayBuffer().then(function(buffer) {
+ assert_array_equals(new Uint8Array(buffer), encode_utf8(body), "Response.arrayBuffer() should contain data encoded as UTF-8");
+ });
+ }, title + " (Response object)");
+
+ promise_test(function(test) {
+ return new Request("", {method: "POST", body: body}).arrayBuffer().then(function(buffer) {
+ assert_array_equals(new Uint8Array(buffer), encode_utf8(body), "Request.arrayBuffer() should contain data encoded as UTF-8");
+ });
+ }, title + " (Request object)");
+
+}
+
+var utf8WithBOM = "\xef\xbb\xbf\xe4\xb8\x89\xe6\x9d\x91\xe3\x81\x8b\xe3\x81\xaa\xe5\xad\x90";
+var utf8WithBOMAsURLParameter = "%EF%BB%BF%E4%B8%89%E6%9D%91%E3%81%8B%E3%81%AA%E5%AD%90";
+var utf8WithoutBOM = "\xe4\xb8\x89\xe6\x9d\x91\xe3\x81\x8b\xe3\x81\xaa\xe5\xad\x90";
+var utf8WithoutBOMAsURLParameter = "%E4%B8%89%E6%9D%91%E3%81%8B%E3%81%AA%E5%AD%90";
+var utf8Decoded = "三æ‘ã‹ãªå­";
+testTextDecoding(utf8WithBOM, utf8Decoded, utf8WithBOMAsURLParameter, "UTF-8 with BOM");
+testTextDecoding(utf8WithoutBOM, utf8Decoded, utf8WithoutBOMAsURLParameter, "UTF-8 without BOM");
+
+var utf16BEWithBOM = "\xfe\xff\x4e\x09\x67\x51\x30\x4b\x30\x6a\x5b\x50";
+var utf16BEWithBOMAsURLParameter = "%fe%ff%4e%09%67%51%30%4b%30%6a%5b%50";
+var utf16BEWithBOMDecodedAsUTF8 = "��N\tgQ0K0j[P";
+testTextDecoding(utf16BEWithBOM, utf16BEWithBOMDecodedAsUTF8, utf16BEWithBOMAsURLParameter, "UTF-16BE with BOM decoded as UTF-8");
+
+var utf16LEWithBOM = "\xff\xfe\x09\x4e\x51\x67\x4b\x30\x6a\x30\x50\x5b";
+var utf16LEWithBOMAsURLParameter = "%ff%fe%09%4e%51%67%4b%30%6a%30%50%5b";
+var utf16LEWithBOMDecodedAsUTF8 = "��\tNQgK0j0P[";
+testTextDecoding(utf16LEWithBOM, utf16LEWithBOMDecodedAsUTF8, utf16LEWithBOMAsURLParameter, "UTF-16LE with BOM decoded as UTF-8");
+
+var utf16WithoutBOM = "\xe6\x00\xf8\x00\xe5\x00\x0a\x00\xc6\x30\xb9\x30\xc8\x30\x0a\x00";
+var utf16WithoutBOMAsURLParameter = "%E6%00%F8%00%E5%00%0A%00%C6%30%B9%30%C8%30%0A%00";
+var utf16WithoutBOMDecoded = "\ufffd\u0000\ufffd\u0000\ufffd\u0000\u000a\u0000\ufffd\u0030\ufffd\u0030\ufffd\u0030\u000a\u0000";
+testTextDecoding(utf16WithoutBOM, utf16WithoutBOMDecoded, utf16WithoutBOMAsURLParameter, "UTF-16 without BOM decoded as UTF-8");
diff --git a/test/wpt/tests/fetch/api/body/cloned-any.js b/test/wpt/tests/fetch/api/body/cloned-any.js
new file mode 100644
index 0000000..2bca96c
--- /dev/null
+++ b/test/wpt/tests/fetch/api/body/cloned-any.js
@@ -0,0 +1,50 @@
+// Changing the body after it have been passed to Response/Request
+// should not change the outcome of the consumed body
+
+const url = 'http://a';
+const method = 'post';
+
+promise_test(async t => {
+ const body = new FormData();
+ body.set('a', '1');
+ const res = new Response(body);
+ const req = new Request(url, { method, body });
+ body.set('a', '2');
+ assert_true((await res.formData()).get('a') === '1');
+ assert_true((await req.formData()).get('a') === '1');
+}, 'FormData is cloned');
+
+promise_test(async t => {
+ const body = new URLSearchParams({a: '1'});
+ const res = new Response(body);
+ const req = new Request(url, { method, body });
+ body.set('a', '2');
+ assert_true((await res.formData()).get('a') === '1');
+ assert_true((await req.formData()).get('a') === '1');
+}, 'URLSearchParams is cloned');
+
+promise_test(async t => {
+ const body = new Uint8Array([97]); // a
+ const res = new Response(body);
+ const req = new Request(url, { method, body });
+ body[0] = 98; // b
+ assert_true(await res.text() === 'a');
+ assert_true(await req.text() === 'a');
+}, 'TypedArray is cloned');
+
+promise_test(async t => {
+ const body = new Uint8Array([97]); // a
+ const res = new Response(body.buffer);
+ const req = new Request(url, { method, body: body.buffer });
+ body[0] = 98; // b
+ assert_true(await res.text() === 'a');
+ assert_true(await req.text() === 'a');
+}, 'ArrayBuffer is cloned');
+
+promise_test(async t => {
+ const body = new Blob(['a']);
+ const res = new Response(body);
+ const req = new Request(url, { method, body });
+ assert_true(await res.blob() !== body);
+ assert_true(await req.blob() !== body);
+}, 'Blob is cloned');
diff --git a/test/wpt/tests/fetch/api/body/formdata.any.js b/test/wpt/tests/fetch/api/body/formdata.any.js
new file mode 100644
index 0000000..e250359
--- /dev/null
+++ b/test/wpt/tests/fetch/api/body/formdata.any.js
@@ -0,0 +1,14 @@
+promise_test(async t => {
+ const res = new Response(new FormData());
+ const fd = await res.formData();
+ assert_true(fd instanceof FormData);
+}, 'Consume empty response.formData() as FormData');
+
+promise_test(async t => {
+ const req = new Request('about:blank', {
+ method: 'POST',
+ body: new FormData()
+ });
+ const fd = await req.formData();
+ assert_true(fd instanceof FormData);
+}, 'Consume empty request.formData() as FormData');
diff --git a/test/wpt/tests/fetch/api/body/mime-type.any.js b/test/wpt/tests/fetch/api/body/mime-type.any.js
new file mode 100644
index 0000000..67c9af7
--- /dev/null
+++ b/test/wpt/tests/fetch/api/body/mime-type.any.js
@@ -0,0 +1,127 @@
+[
+ () => new Request("about:blank", { headers: { "Content-Type": "text/plain" } }),
+ () => new Response("", { headers: { "Content-Type": "text/plain" } })
+].forEach(bodyContainerCreator => {
+ const bodyContainer = bodyContainerCreator();
+ promise_test(async t => {
+ assert_equals(bodyContainer.headers.get("Content-Type"), "text/plain");
+ const newMIMEType = "test/test";
+ bodyContainer.headers.set("Content-Type", newMIMEType);
+ const blob = await bodyContainer.blob();
+ assert_equals(blob.type, newMIMEType);
+ }, `${bodyContainer.constructor.name}: overriding explicit Content-Type`);
+});
+
+[
+ () => new Request("about:blank", { body: new URLSearchParams(), method: "POST" }),
+ () => new Response(new URLSearchParams()),
+].forEach(bodyContainerCreator => {
+ const bodyContainer = bodyContainerCreator();
+ promise_test(async t => {
+ assert_equals(bodyContainer.headers.get("Content-Type"), "application/x-www-form-urlencoded;charset=UTF-8");
+ bodyContainer.headers.delete("Content-Type");
+ const blob = await bodyContainer.blob();
+ assert_equals(blob.type, "");
+ }, `${bodyContainer.constructor.name}: removing implicit Content-Type`);
+});
+
+[
+ () => new Request("about:blank", { body: new ArrayBuffer(), method: "POST" }),
+ () => new Response(new ArrayBuffer()),
+].forEach(bodyContainerCreator => {
+ const bodyContainer = bodyContainerCreator();
+ promise_test(async t => {
+ assert_equals(bodyContainer.headers.get("Content-Type"), null);
+ const newMIMEType = "test/test";
+ bodyContainer.headers.set("Content-Type", newMIMEType);
+ const blob = await bodyContainer.blob();
+ assert_equals(blob.type, newMIMEType);
+ }, `${bodyContainer.constructor.name}: setting missing Content-Type`);
+});
+
+[
+ () => new Request("about:blank", { method: "POST" }),
+ () => new Response(),
+].forEach(bodyContainerCreator => {
+ const bodyContainer = bodyContainerCreator();
+ promise_test(async t => {
+ const blob = await bodyContainer.blob();
+ assert_equals(blob.type, "");
+ }, `${bodyContainer.constructor.name}: MIME type for Blob from empty body`);
+});
+
+[
+ () => new Request("about:blank", { method: "POST", headers: [["Content-Type", "Mytext/Plain"]] }),
+ () => new Response("", { headers: [["Content-Type", "Mytext/Plain"]] })
+].forEach(bodyContainerCreator => {
+ const bodyContainer = bodyContainerCreator();
+ promise_test(async t => {
+ const blob = await bodyContainer.blob();
+ assert_equals(blob.type, 'mytext/plain');
+ }, `${bodyContainer.constructor.name}: MIME type for Blob from empty body with Content-Type`);
+});
+
+[
+ () => new Request("about:blank", { body: new Blob([""]), method: "POST" }),
+ () => new Response(new Blob([""]))
+].forEach(bodyContainerCreator => {
+ const bodyContainer = bodyContainerCreator();
+ promise_test(async t => {
+ const blob = await bodyContainer.blob();
+ assert_equals(blob.type, "");
+ assert_equals(bodyContainer.headers.get("Content-Type"), null);
+ }, `${bodyContainer.constructor.name}: MIME type for Blob`);
+});
+
+[
+ () => new Request("about:blank", { body: new Blob([""], { type: "Text/Plain" }), method: "POST" }),
+ () => new Response(new Blob([""], { type: "Text/Plain" }))
+].forEach(bodyContainerCreator => {
+ const bodyContainer = bodyContainerCreator();
+ promise_test(async t => {
+ const blob = await bodyContainer.blob();
+ assert_equals(blob.type, "text/plain");
+ assert_equals(bodyContainer.headers.get("Content-Type"), "text/plain");
+ }, `${bodyContainer.constructor.name}: MIME type for Blob with non-empty type`);
+});
+
+[
+ () => new Request("about:blank", { method: "POST", body: new Blob([""], { type: "Text/Plain" }), headers: [["Content-Type", "Text/Html"]] }),
+ () => new Response(new Blob([""], { type: "Text/Plain" }, { headers: [["Content-Type", "Text/Html"]] }))
+].forEach(bodyContainerCreator => {
+ const bodyContainer = bodyContainerCreator();
+ const cloned = bodyContainer.clone();
+ promise_test(async t => {
+ const blobs = [await bodyContainer.blob(), await cloned.blob()];
+ assert_equals(blobs[0].type, "text/html");
+ assert_equals(blobs[1].type, "text/html");
+ assert_equals(bodyContainer.headers.get("Content-Type"), "Text/Html");
+ assert_equals(cloned.headers.get("Content-Type"), "Text/Html");
+ }, `${bodyContainer.constructor.name}: Extract a MIME type with clone`);
+});
+
+[
+ () => new Request("about:blank", { body: new Blob([], { type: "text/plain" }), method: "POST", headers: [["Content-Type", "text/html"]] }),
+ () => new Response(new Blob([], { type: "text/plain" }), { headers: [["Content-Type", "text/html"]] }),
+].forEach(bodyContainerCreator => {
+ const bodyContainer = bodyContainerCreator();
+ promise_test(async t => {
+ assert_equals(bodyContainer.headers.get("Content-Type"), "text/html");
+ const blob = await bodyContainer.blob();
+ assert_equals(blob.type, "text/html");
+ }, `${bodyContainer.constructor.name}: Content-Type in headers wins Blob"s type`);
+});
+
+[
+ () => new Request("about:blank", { body: new Blob([], { type: "text/plain" }), method: "POST" }),
+ () => new Response(new Blob([], { type: "text/plain" })),
+].forEach(bodyContainerCreator => {
+ const bodyContainer = bodyContainerCreator();
+ promise_test(async t => {
+ assert_equals(bodyContainer.headers.get("Content-Type"), "text/plain");
+ const newMIMEType = "text/html";
+ bodyContainer.headers.set("Content-Type", newMIMEType);
+ const blob = await bodyContainer.blob();
+ assert_equals(blob.type, newMIMEType);
+ }, `${bodyContainer.constructor.name}: setting missing Content-Type in headers and it wins Blob"s type`);
+});
diff --git a/test/wpt/tests/fetch/api/cors/cors-basic.any.js b/test/wpt/tests/fetch/api/cors/cors-basic.any.js
new file mode 100644
index 0000000..95de0af
--- /dev/null
+++ b/test/wpt/tests/fetch/api/cors/cors-basic.any.js
@@ -0,0 +1,43 @@
+// META: script=../resources/utils.js
+// META: script=/common/get-host-info.sub.js
+
+const {
+ HTTPS_ORIGIN,
+ HTTP_ORIGIN_WITH_DIFFERENT_PORT,
+ HTTP_REMOTE_ORIGIN,
+ HTTP_REMOTE_ORIGIN_WITH_DIFFERENT_PORT,
+ HTTPS_REMOTE_ORIGIN,
+} = get_host_info();
+
+function cors(desc, origin) {
+ const url = `${origin}${dirname(location.pathname)}${RESOURCES_DIR}top.txt`;
+ const urlAllowCors = `${url}?pipe=header(Access-Control-Allow-Origin,*)`;
+
+ promise_test((test) => {
+ return fetch(urlAllowCors, {'mode': 'no-cors'}).then((resp) => {
+ assert_equals(resp.status, 0, "Opaque filter: status is 0");
+ assert_equals(resp.statusText, "", "Opaque filter: statusText is \"\"");
+ assert_equals(resp.type , "opaque", "Opaque filter: response's type is opaque");
+ return resp.text().then((value) => {
+ assert_equals(value, "", "Opaque response should have an empty body");
+ });
+ });
+ }, `${desc} [no-cors mode]`);
+
+ promise_test((test) => {
+ return promise_rejects_js(test, TypeError, fetch(url, {'mode': 'cors'}));
+ }, `${desc} [server forbid CORS]`);
+
+ promise_test((test) => {
+ return fetch(urlAllowCors, {'mode': 'cors'}).then((resp) => {
+ assert_equals(resp.status, 200, "Fetch's response's status is 200");
+ assert_equals(resp.type , "cors", "CORS response's type is cors");
+ });
+ }, `${desc} [cors mode]`);
+}
+
+cors('Same domain different port', HTTP_ORIGIN_WITH_DIFFERENT_PORT);
+cors('Same domain different protocol different port', HTTPS_ORIGIN);
+cors('Cross domain basic usage', HTTP_REMOTE_ORIGIN);
+cors('Cross domain different port', HTTP_REMOTE_ORIGIN_WITH_DIFFERENT_PORT);
+cors('Cross domain different protocol', HTTPS_REMOTE_ORIGIN);
diff --git a/test/wpt/tests/fetch/api/cors/cors-cookies-redirect.any.js b/test/wpt/tests/fetch/api/cors/cors-cookies-redirect.any.js
new file mode 100644
index 0000000..f5217b4
--- /dev/null
+++ b/test/wpt/tests/fetch/api/cors/cors-cookies-redirect.any.js
@@ -0,0 +1,49 @@
+// META: script=/common/utils.js
+// META: script=../resources/utils.js
+// META: script=/common/get-host-info.sub.js
+
+var redirectUrl = get_host_info().HTTP_REMOTE_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "redirect.py";
+var urlSetCookies1 = get_host_info().HTTP_REMOTE_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "top.txt";
+var urlSetCookies2 = get_host_info().HTTP_ORIGIN_WITH_DIFFERENT_PORT + dirname(location.pathname) + RESOURCES_DIR + "top.txt";
+var urlCheckCookies = get_host_info().HTTP_ORIGIN_WITH_DIFFERENT_PORT + dirname(location.pathname) + RESOURCES_DIR + "inspect-headers.py?cors&headers=cookie";
+
+var urlSetCookiesParameters = "?pipe=header(Access-Control-Allow-Origin," + location.origin + ")";
+urlSetCookiesParameters += "|header(Access-Control-Allow-Credentials,true)";
+
+urlSetCookiesParameters1 = urlSetCookiesParameters + "|header(Set-Cookie,a=1)";
+urlSetCookiesParameters2 = urlSetCookiesParameters + "|header(Set-Cookie,a=2)";
+
+urlClearCookiesParameters1 = urlSetCookiesParameters + "|header(Set-Cookie,a=1%3B%20max-age=0)";
+urlClearCookiesParameters2 = urlSetCookiesParameters + "|header(Set-Cookie,a=2%3B%20max-age=0)";
+
+promise_test(async (test) => {
+ await fetch(urlSetCookies1 + urlSetCookiesParameters1, {"credentials": "include", "mode": "cors"});
+ await fetch(urlSetCookies2 + urlSetCookiesParameters2, {"credentials": "include", "mode": "cors"});
+}, "Set cookies");
+
+function doTest(usePreflight) {
+ promise_test(async (test) => {
+ var url = redirectUrl;
+ var uuid_token = token();
+ var urlParameters = "?token=" + uuid_token + "&max_age=0";
+ urlParameters += "&redirect_status=301";
+ urlParameters += "&location=" + encodeURIComponent(urlCheckCookies);
+ urlParameters += "&allow_headers=a&headers=Cookie";
+ headers = [];
+ if (usePreflight)
+ headers.push(["a", "b"]);
+
+ var requestInit = {"credentials": "include", "mode": "cors", "headers": headers};
+ var response = await fetch(url + urlParameters, requestInit);
+
+ assert_equals(response.headers.get("x-request-cookie") , "a=2", "Request includes cookie(s)");
+ }, "Testing credentials after cross-origin redirection with CORS and " + (usePreflight ? "" : "no ") + "preflight");
+}
+
+doTest(false);
+doTest(true);
+
+promise_test(async (test) => {
+ await fetch(urlSetCookies1 + urlClearCookiesParameters1, {"credentials": "include", "mode": "cors"});
+ await fetch(urlSetCookies2 + urlClearCookiesParameters2, {"credentials": "include", "mode": "cors"});
+}, "Clean cookies");
diff --git a/test/wpt/tests/fetch/api/cors/cors-cookies.any.js b/test/wpt/tests/fetch/api/cors/cors-cookies.any.js
new file mode 100644
index 0000000..8c666e4
--- /dev/null
+++ b/test/wpt/tests/fetch/api/cors/cors-cookies.any.js
@@ -0,0 +1,56 @@
+// META: script=../resources/utils.js
+// META: script=/common/get-host-info.sub.js
+
+function corsCookies(desc, baseURL1, baseURL2, credentialsMode, cookies) {
+ var urlSetCookie = baseURL1 + dirname(location.pathname) + RESOURCES_DIR + "top.txt";
+ var urlCheckCookies = baseURL2 + dirname(location.pathname) + RESOURCES_DIR + "inspect-headers.py?cors&headers=cookie";
+ //enable cors with credentials
+ var urlParameters = "?pipe=header(Access-Control-Allow-Origin," + location.origin + ")";
+ urlParameters += "|header(Access-Control-Allow-Credentials,true)";
+
+ var urlCleanParameters = "?pipe=header(Access-Control-Allow-Origin," + location.origin + ")";
+ urlCleanParameters += "|header(Access-Control-Allow-Credentials,true)";
+ if (cookies) {
+ urlParameters += "|header(Set-Cookie,";
+ urlParameters += cookies.join(",True)|header(Set-Cookie,") + ",True)";
+ urlCleanParameters += "|header(Set-Cookie,";
+ urlCleanParameters += cookies.join("%3B%20max-age=0,True)|header(Set-Cookie,") + "%3B%20max-age=0,True)";
+ }
+
+ var requestInit = {"credentials": credentialsMode, "mode": "cors"};
+
+ promise_test(function(test){
+ return fetch(urlSetCookie + urlParameters, requestInit).then(function(resp) {
+ assert_equals(resp.status, 200, "HTTP status is 200");
+ //check cookies sent
+ return fetch(urlCheckCookies, requestInit);
+ }).then(function(resp) {
+ assert_equals(resp.status, 200, "HTTP status is 200");
+ assert_false(resp.headers.has("Cookie") , "Cookie header is not exposed in response");
+ if (credentialsMode === "include" && baseURL1 === baseURL2) {
+ assert_equals(resp.headers.get("x-request-cookie") , cookies.join("; "), "Request includes cookie(s)");
+ }
+ else {
+ assert_false(resp.headers.has("x-request-cookie") , "Request should have no cookie");
+ }
+ //clean cookies
+ return fetch(urlSetCookie + urlCleanParameters, {"credentials": "include"});
+ }).catch(function(e) {
+ return fetch(urlSetCookie + urlCleanParameters, {"credentials": "include"}).then(function(resp) {
+ throw e;
+ })
+ });
+ }, desc);
+}
+
+var local = get_host_info().HTTP_ORIGIN;
+var remote = get_host_info().HTTP_REMOTE_ORIGIN;
+// FIXME: otherRemote might not be accessible on some test environments.
+var otherRemote = local.replace("http://", "http://www.");
+
+corsCookies("Omit mode: no cookie sent", local, local, "omit", ["g=7"]);
+corsCookies("Include mode: 1 cookie", remote, remote, "include", ["a=1"]);
+corsCookies("Include mode: local cookies are not sent with remote request", local, remote, "include", ["c=3"]);
+corsCookies("Include mode: remote cookies are not sent with local request", remote, local, "include", ["d=4"]);
+corsCookies("Same-origin mode: cookies are discarded in cors request", remote, remote, "same-origin", ["f=6"]);
+corsCookies("Include mode: remote cookies are not sent with other remote request", remote, otherRemote, "include", ["e=5"]);
diff --git a/test/wpt/tests/fetch/api/cors/cors-expose-star.sub.any.js b/test/wpt/tests/fetch/api/cors/cors-expose-star.sub.any.js
new file mode 100644
index 0000000..340e99a
--- /dev/null
+++ b/test/wpt/tests/fetch/api/cors/cors-expose-star.sub.any.js
@@ -0,0 +1,41 @@
+// META: script=../resources/utils.js
+
+const url = "http://{{host}}:{{ports[http][1]}}" + dirname(location.pathname) + RESOURCES_DIR + "top.txt",
+ sharedHeaders = "?pipe=header(Access-Control-Expose-Headers,*)|header(Test,X)|header(Set-Cookie,X)|header(*,whoa)|"
+
+promise_test(() => {
+ const headers = "header(Access-Control-Allow-Origin,*)"
+ return fetch(url + sharedHeaders + headers).then(resp => {
+ assert_equals(resp.status, 200)
+ assert_equals(resp.type , "cors")
+ assert_equals(resp.headers.get("test"), "X")
+ assert_equals(resp.headers.get("set-cookie"), null)
+ assert_equals(resp.headers.get("*"), "whoa")
+ })
+}, "Basic Access-Control-Expose-Headers: * support")
+
+promise_test(() => {
+ const origin = location.origin, // assuming an ASCII origin
+ headers = "header(Access-Control-Allow-Origin," + origin + ")|header(Access-Control-Allow-Credentials,true)"
+ return fetch(url + sharedHeaders + headers, { credentials:"include" }).then(resp => {
+ assert_equals(resp.status, 200)
+ assert_equals(resp.type , "cors")
+ assert_equals(resp.headers.get("content-type"), "text/plain") // safelisted
+ assert_equals(resp.headers.get("test"), null)
+ assert_equals(resp.headers.get("set-cookie"), null)
+ assert_equals(resp.headers.get("*"), "whoa")
+ })
+}, "* for credentialed fetches only matches literally")
+
+promise_test(() => {
+ const headers = "header(Access-Control-Allow-Origin,*)|header(Access-Control-Expose-Headers,set-cookie\\,*)"
+ return fetch(url + sharedHeaders + headers).then(resp => {
+ assert_equals(resp.status, 200)
+ assert_equals(resp.type , "cors")
+ assert_equals(resp.headers.get("test"), "X")
+ assert_equals(resp.headers.get("set-cookie"), null)
+ assert_equals(resp.headers.get("*"), "whoa")
+ })
+}, "* can be one of several values")
+
+done();
diff --git a/test/wpt/tests/fetch/api/cors/cors-filtering.sub.any.js b/test/wpt/tests/fetch/api/cors/cors-filtering.sub.any.js
new file mode 100644
index 0000000..a26eacc
--- /dev/null
+++ b/test/wpt/tests/fetch/api/cors/cors-filtering.sub.any.js
@@ -0,0 +1,69 @@
+// META: script=../resources/utils.js
+
+function corsFilter(corsUrl, headerName, headerValue, isFiltered) {
+ var url = corsUrl + "?pipe=header(" + headerName + "," + encodeURIComponent(headerValue) +")|header(Access-Control-Allow-Origin,*)";
+ promise_test(function(test) {
+ return fetch(url).then(function(resp) {
+ assert_equals(resp.status, 200, "Fetch success with code 200");
+ assert_equals(resp.type , "cors", "CORS fetch's response has cors type");
+ if (!isFiltered) {
+ assert_equals(resp.headers.get(headerName), headerValue,
+ headerName + " header should be included in response with value: " + headerValue);
+ } else {
+ assert_false(resp.headers.has(headerName), "UA should exclude " + headerName + " header from response");
+ }
+ test.done();
+ });
+ }, "CORS filter on " + headerName + " header");
+}
+
+function corsExposeFilter(corsUrl, headerName, headerValue, isForbidden, withCredentials) {
+ var url = corsUrl + "?pipe=header(" + headerName + "," + encodeURIComponent(headerValue) +")|" +
+ "header(Access-Control-Allow-Origin, http://{{host}}:{{ports[http][0]}})" +
+ "header(Access-Control-Allow-Credentials, true)" +
+ "header(Access-Control-Expose-Headers," + headerName + ")";
+
+ var title = "CORS filter on " + headerName + " header, header is " + (isForbidden ? "forbidden" : "exposed");
+ if (withCredentials)
+ title+= "(credentials = include)";
+ promise_test(function(test) {
+ return fetch(new Request(url, { credentials: withCredentials ? "include" : "omit" })).then(function(resp) {
+ assert_equals(resp.status, 200, "Fetch success with code 200");
+ assert_equals(resp.type , "cors", "CORS fetch's response has cors type");
+ if (!isForbidden) {
+ assert_equals(resp.headers.get(headerName), headerValue,
+ headerName + " header should be included in response with value: " + headerValue);
+ } else {
+ assert_false(resp.headers.has(headerName), "UA should exclude " + headerName + " header from response");
+ }
+ test.done();
+ });
+ }, title);
+}
+
+var url = "http://{{host}}:{{ports[http][1]}}" + dirname(location.pathname) + RESOURCES_DIR + "top.txt";
+
+corsFilter(url, "Cache-Control", "no-cache", false);
+corsFilter(url, "Content-Language", "fr", false);
+corsFilter(url, "Content-Type", "text/html", false);
+corsFilter(url, "Expires","04 May 1988 22:22:22 GMT" , false);
+corsFilter(url, "Last-Modified", "04 May 1988 22:22:22 GMT", false);
+corsFilter(url, "Pragma", "no-cache", false);
+corsFilter(url, "Content-Length", "3" , false); // top.txt contains "top"
+
+corsFilter(url, "Age", "27", true);
+corsFilter(url, "Server", "wptServe" , true);
+corsFilter(url, "Warning", "Mind the gap" , true);
+corsFilter(url, "Set-Cookie", "name=value" , true);
+corsFilter(url, "Set-Cookie2", "name=value" , true);
+
+corsExposeFilter(url, "Age", "27", false);
+corsExposeFilter(url, "Server", "wptServe" , false);
+corsExposeFilter(url, "Warning", "Mind the gap" , false);
+
+corsExposeFilter(url, "Set-Cookie", "name=value" , true);
+corsExposeFilter(url, "Set-Cookie2", "name=value" , true);
+corsExposeFilter(url, "Set-Cookie", "name=value" , true, true);
+corsExposeFilter(url, "Set-Cookie2", "name=value" , true, true);
+
+done();
diff --git a/test/wpt/tests/fetch/api/cors/cors-keepalive.any.js b/test/wpt/tests/fetch/api/cors/cors-keepalive.any.js
new file mode 100644
index 0000000..f68d90e
--- /dev/null
+++ b/test/wpt/tests/fetch/api/cors/cors-keepalive.any.js
@@ -0,0 +1,118 @@
+// META: global=window
+// META: timeout=long
+// 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
+// META: script=../resources/utils.js
+
+'use strict';
+
+const {
+ HTTP_NOTSAMESITE_ORIGIN,
+ HTTPS_ORIGIN,
+ HTTP_ORIGIN_WITH_DIFFERENT_PORT,
+ HTTP_REMOTE_ORIGIN,
+ HTTP_REMOTE_ORIGIN_WITH_DIFFERENT_PORT,
+ HTTPS_REMOTE_ORIGIN,
+} = get_host_info();
+
+/**
+ * Tests to cover the basic behaviors of keepalive + cors/no-cors mode requests
+ * to different `origin` when the initiator document is still alive. They should
+ * behave the same as without setting keepalive.
+ */
+function keepaliveCorsBasicTest(desc, origin) {
+ const url = `${origin}${dirname(location.pathname)}${RESOURCES_DIR}top.txt`;
+ const urlAllowCors = `${url}?pipe=header(Access-Control-Allow-Origin,*)`;
+
+ promise_test((test) => {
+ return fetch(urlAllowCors, {keepalive: true, 'mode': 'no-cors'})
+ .then((resp) => {
+ assert_equals(resp.status, 0, 'Opaque filter: status is 0');
+ assert_equals(resp.statusText, '', 'Opaque filter: statusText is ""');
+ assert_equals(
+ resp.type, 'opaque', 'Opaque filter: response\'s type is opaque');
+ return resp.text().then((value) => {
+ assert_equals(
+ value, '', 'Opaque response should have an empty body');
+ });
+ });
+ }, `${desc} [no-cors mode]`);
+
+ promise_test((test) => {
+ return promise_rejects_js(
+ test, TypeError, fetch(url, {keepalive: true, 'mode': 'cors'}));
+ }, `${desc} [cors mode, server forbid CORS]`);
+
+ promise_test((test) => {
+ return fetch(urlAllowCors, {keepalive: true, 'mode': 'cors'})
+ .then((resp) => {
+ assert_equals(resp.status, 200, 'Fetch\'s response\'s status is 200');
+ assert_equals(resp.type, 'cors', 'CORS response\'s type is cors');
+ });
+ }, `${desc} [cors mode]`);
+}
+
+keepaliveCorsBasicTest(
+ `[keepalive] Same domain different port`, HTTP_ORIGIN_WITH_DIFFERENT_PORT);
+keepaliveCorsBasicTest(
+ `[keepalive] Same domain different protocol different port`, HTTPS_ORIGIN);
+keepaliveCorsBasicTest(
+ `[keepalive] Cross domain basic usage`, HTTP_REMOTE_ORIGIN);
+keepaliveCorsBasicTest(
+ `[keepalive] Cross domain different port`,
+ HTTP_REMOTE_ORIGIN_WITH_DIFFERENT_PORT);
+keepaliveCorsBasicTest(
+ `[keepalive] Cross domain different protocol`, HTTPS_REMOTE_ORIGIN);
+
+/**
+ * In a same-site iframe, and in `unload` event handler, test to fetch
+ * a keepalive URL that involves in different cors modes.
+ */
+function keepaliveCorsInUnloadTest(description, origin, method) {
+ const evt = 'unload';
+ for (const mode of ['no-cors', 'cors']) {
+ for (const disallowOrigin of [false, true]) {
+ const desc = `${description} ${method} request in ${evt} [${mode} mode` +
+ (disallowOrigin ? `, server forbid CORS]` : `]`);
+ const shouldPass = !disallowOrigin || mode === 'no-cors';
+ promise_test(async (test) => {
+ const token1 = token();
+ const iframe = document.createElement('iframe');
+ iframe.src = getKeepAliveIframeUrl(token1, method, {
+ frameOrigin: '',
+ requestOrigin: origin,
+ sendOn: evt,
+ mode: mode,
+ disallowOrigin
+ });
+ document.body.appendChild(iframe);
+ await iframeLoaded(iframe);
+ iframe.remove();
+ assert_equals(await getTokenFromMessage(), token1);
+
+ assertStashedTokenAsync(desc, token1, {shouldPass});
+ }, `${desc}; setting up`);
+ }
+ }
+}
+
+for (const method of ['GET', 'POST']) {
+ keepaliveCorsInUnloadTest(
+ '[keepalive] Same domain different port', HTTP_ORIGIN_WITH_DIFFERENT_PORT,
+ method);
+ keepaliveCorsInUnloadTest(
+ `[keepalive] Same domain different protocol different port`, HTTPS_ORIGIN,
+ method);
+ keepaliveCorsInUnloadTest(
+ `[keepalive] Cross domain basic usage`, HTTP_REMOTE_ORIGIN, method);
+ keepaliveCorsInUnloadTest(
+ `[keepalive] Cross domain different port`,
+ HTTP_REMOTE_ORIGIN_WITH_DIFFERENT_PORT, method);
+ keepaliveCorsInUnloadTest(
+ `[keepalive] Cross domain different protocol`, HTTPS_REMOTE_ORIGIN,
+ method);
+}
diff --git a/test/wpt/tests/fetch/api/cors/cors-multiple-origins.sub.any.js b/test/wpt/tests/fetch/api/cors/cors-multiple-origins.sub.any.js
new file mode 100644
index 0000000..b3abb92
--- /dev/null
+++ b/test/wpt/tests/fetch/api/cors/cors-multiple-origins.sub.any.js
@@ -0,0 +1,22 @@
+// META: global=window,worker
+// META: script=../resources/utils.js
+
+function corsMultipleOrigins(originList) {
+ var urlParameters = "?origin=" + encodeURIComponent(originList.join(", "));
+ var url = "http://{{host}}:{{ports[http][1]}}" + dirname(location.pathname) + RESOURCES_DIR + "preflight.py";
+
+ promise_test(function(test) {
+ return promise_rejects_js(test, TypeError, fetch(url + urlParameters));
+ }, "Listing multiple origins is illegal: " + originList);
+}
+/* Actual origin */
+var origin = "http://{{host}}:{{ports[http][0]}}";
+
+corsMultipleOrigins(["\"\"", "http://example.com", origin]);
+corsMultipleOrigins(["\"\"", "http://example.com", "*"]);
+corsMultipleOrigins(["\"\"", origin, origin]);
+corsMultipleOrigins(["*", "http://example.com", "*"]);
+corsMultipleOrigins(["*", "http://example.com", origin]);
+corsMultipleOrigins(["", "http://example.com", "https://example2.com"]);
+
+done();
diff --git a/test/wpt/tests/fetch/api/cors/cors-no-preflight.any.js b/test/wpt/tests/fetch/api/cors/cors-no-preflight.any.js
new file mode 100644
index 0000000..7a0269a
--- /dev/null
+++ b/test/wpt/tests/fetch/api/cors/cors-no-preflight.any.js
@@ -0,0 +1,41 @@
+// META: script=/common/utils.js
+// META: script=../resources/utils.js
+// META: script=/common/get-host-info.sub.js
+
+function corsNoPreflight(desc, baseURL, method, headerName, headerValue) {
+
+ var uuid_token = token();
+ var url = baseURL + dirname(location.pathname) + RESOURCES_DIR + "preflight.py";
+ var urlParameters = "?token=" + uuid_token + "&max_age=0";
+ var requestInit = {"mode": "cors", "method": method, "headers":{}};
+ if (headerName)
+ requestInit["headers"][headerName] = headerValue;
+
+ promise_test(function(test) {
+ return fetch(RESOURCES_DIR + "clean-stash.py?token=" + uuid_token).then(function(resp) {
+ assert_equals(resp.status, 200, "Clean stash response's status is 200");
+ return fetch(url + urlParameters, requestInit).then(function(resp) {
+ assert_equals(resp.status, 200, "Response's status is 200");
+ assert_equals(resp.headers.get("x-did-preflight"), "0", "No preflight request has been made");
+ });
+ });
+ }, desc);
+}
+
+var host_info = get_host_info();
+
+corsNoPreflight("Cross domain basic usage [GET]", host_info.HTTP_REMOTE_ORIGIN, "GET");
+corsNoPreflight("Same domain different port [GET]", host_info.HTTP_ORIGIN_WITH_DIFFERENT_PORT, "GET");
+corsNoPreflight("Cross domain different port [GET]", host_info.HTTP_REMOTE_ORIGIN_WITH_DIFFERENT_PORT, "GET");
+corsNoPreflight("Cross domain different protocol [GET]", host_info.HTTPS_REMOTE_ORIGIN, "GET");
+corsNoPreflight("Same domain different protocol different port [GET]", host_info.HTTPS_ORIGIN, "GET");
+corsNoPreflight("Cross domain [POST]", host_info.HTTP_REMOTE_ORIGIN, "POST");
+corsNoPreflight("Cross domain [HEAD]", host_info.HTTP_REMOTE_ORIGIN, "HEAD");
+corsNoPreflight("Cross domain [GET] [Accept: */*]", host_info.HTTP_REMOTE_ORIGIN, "GET" , "Accept", "*/*");
+corsNoPreflight("Cross domain [GET] [Accept-Language: fr]", host_info.HTTP_REMOTE_ORIGIN, "GET" , "Accept-Language", "fr");
+corsNoPreflight("Cross domain [GET] [Content-Language: fr]", host_info.HTTP_REMOTE_ORIGIN, "GET" , "Content-Language", "fr");
+corsNoPreflight("Cross domain [GET] [Content-Type: application/x-www-form-urlencoded]", host_info.HTTP_REMOTE_ORIGIN, "GET" , "Content-Type", "application/x-www-form-urlencoded");
+corsNoPreflight("Cross domain [GET] [Content-Type: multipart/form-data]", host_info.HTTP_REMOTE_ORIGIN, "GET" , "Content-Type", "multipart/form-data");
+corsNoPreflight("Cross domain [GET] [Content-Type: text/plain]", host_info.HTTP_REMOTE_ORIGIN, "GET" , "Content-Type", "text/plain");
+corsNoPreflight("Cross domain [GET] [Content-Type: text/plain;charset=utf-8]", host_info.HTTP_REMOTE_ORIGIN, "GET" , "Content-Type", "text/plain;charset=utf-8");
+corsNoPreflight("Cross domain [GET] [Content-Type: Text/Plain;charset=utf-8]", host_info.HTTP_REMOTE_ORIGIN, "GET" , "Content-Type", "Text/Plain;charset=utf-8");
diff --git a/test/wpt/tests/fetch/api/cors/cors-origin.any.js b/test/wpt/tests/fetch/api/cors/cors-origin.any.js
new file mode 100644
index 0000000..30a02d9
--- /dev/null
+++ b/test/wpt/tests/fetch/api/cors/cors-origin.any.js
@@ -0,0 +1,51 @@
+// META: script=/common/utils.js
+// META: script=../resources/utils.js
+// META: script=/common/get-host-info.sub.js
+
+/* If origin is undefined, it is set to fetched url's origin*/
+function corsOrigin(desc, baseURL, method, origin, shouldPass) {
+ if (!origin)
+ origin = baseURL;
+
+ var uuid_token = token();
+ var urlParameters = "?token=" + uuid_token + "&max_age=0&origin=" + encodeURIComponent(origin) + "&allow_methods=" + method;
+ var url = baseURL + dirname(location.pathname) + RESOURCES_DIR + "preflight.py";
+ var requestInit = {"mode": "cors", "method": method};
+
+ promise_test(function(test) {
+ return fetch(RESOURCES_DIR + "clean-stash.py?token=" + uuid_token).then(function(resp) {
+ assert_equals(resp.status, 200, "Clean stash response's status is 200");
+ if (shouldPass) {
+ return fetch(url + urlParameters, requestInit).then(function(resp) {
+ assert_equals(resp.status, 200, "Response's status is 200");
+ });
+ } else {
+ return promise_rejects_js(test, TypeError, fetch(url + urlParameters, requestInit));
+ }
+ });
+ }, desc);
+
+}
+
+var host_info = get_host_info();
+
+/* Actual origin */
+var origin = host_info.HTTP_ORIGIN;
+
+corsOrigin("Cross domain different subdomain [origin OK]", host_info.HTTP_REMOTE_ORIGIN, "GET", origin, true);
+corsOrigin("Cross domain different subdomain [origin KO]", host_info.HTTP_REMOTE_ORIGIN, "GET", undefined, false);
+corsOrigin("Same domain different port [origin OK]", host_info.HTTP_ORIGIN_WITH_DIFFERENT_PORT, "GET", origin, true);
+corsOrigin("Same domain different port [origin KO]", host_info.HTTP_ORIGIN_WITH_DIFFERENT_PORT, "GET", undefined, false);
+corsOrigin("Cross domain different port [origin OK]", host_info.HTTP_REMOTE_ORIGIN_WITH_DIFFERENT_PORT, "GET", origin, true);
+corsOrigin("Cross domain different port [origin KO]", host_info.HTTP_REMOTE_ORIGIN_WITH_DIFFERENT_PORT, "GET", undefined, false);
+corsOrigin("Cross domain different protocol [origin OK]", host_info.HTTPS_REMOTE_ORIGIN, "GET", origin, true);
+corsOrigin("Cross domain different protocol [origin KO]", host_info.HTTPS_REMOTE_ORIGIN, "GET", undefined, false);
+corsOrigin("Same domain different protocol different port [origin OK]", host_info.HTTPS_ORIGIN, "GET", origin, true);
+corsOrigin("Same domain different protocol different port [origin KO]", host_info.HTTPS_ORIGIN, "GET", undefined, false);
+corsOrigin("Cross domain [POST] [origin OK]", host_info.HTTP_REMOTE_ORIGIN, "POST", origin, true);
+corsOrigin("Cross domain [POST] [origin KO]", host_info.HTTP_REMOTE_ORIGIN, "POST", undefined, false);
+corsOrigin("Cross domain [HEAD] [origin OK]", host_info.HTTP_REMOTE_ORIGIN, "HEAD", origin, true);
+corsOrigin("Cross domain [HEAD] [origin KO]", host_info.HTTP_REMOTE_ORIGIN, "HEAD", undefined, false);
+corsOrigin("CORS preflight [PUT] [origin OK]", host_info.HTTP_REMOTE_ORIGIN, "PUT", origin, true);
+corsOrigin("CORS preflight [PUT] [origin KO]", host_info.HTTP_REMOTE_ORIGIN, "PUT", undefined, false);
+corsOrigin("Allowed origin: \"\" [origin KO]", host_info.HTTP_REMOTE_ORIGIN, "GET", "" , false);
diff --git a/test/wpt/tests/fetch/api/cors/cors-preflight-cache.any.js b/test/wpt/tests/fetch/api/cors/cors-preflight-cache.any.js
new file mode 100644
index 0000000..ce6a169
--- /dev/null
+++ b/test/wpt/tests/fetch/api/cors/cors-preflight-cache.any.js
@@ -0,0 +1,46 @@
+// META: script=/common/utils.js
+// META: script=../resources/utils.js
+// META: script=/common/get-host-info.sub.js
+
+var cors_url = get_host_info().HTTP_REMOTE_ORIGIN +
+ dirname(location.pathname) +
+ RESOURCES_DIR +
+ "preflight.py";
+
+promise_test((test) => {
+ var uuid_token = token();
+ var request_url =
+ cors_url + "?token=" + uuid_token + "&max_age=12000&allow_methods=POST" +
+ "&allow_headers=x-test-header";
+ return fetch(cors_url + "?token=" + uuid_token + "&clear-stash")
+ .then(() => {
+ return fetch(
+ new Request(request_url,
+ {
+ mode: "cors",
+ method: "POST",
+ headers: [["x-test-header", "test1"]]
+ }));
+ })
+ .then((resp) => {
+ assert_equals(resp.status, 200, "Response's status is 200");
+ assert_equals(resp.headers.get("x-did-preflight"), "1", "Preflight request has been made");
+ return fetch(cors_url + "?token=" + uuid_token + "&clear-stash");
+ })
+ .then((res) => res.text())
+ .then((txt) => {
+ assert_equals(txt, "1", "Server stash must be cleared.");
+ return fetch(
+ new Request(request_url,
+ {
+ mode: "cors",
+ method: "POST",
+ headers: [["x-test-header", "test2"]]
+ }));
+ })
+ .then((resp) => {
+ assert_equals(resp.status, 200, "Response's status is 200");
+ assert_equals(resp.headers.get("x-did-preflight"), "0", "Preflight request has not been made");
+ return fetch(cors_url + "?token=" + uuid_token + "&clear-stash");
+ });
+});
diff --git a/test/wpt/tests/fetch/api/cors/cors-preflight-not-cors-safelisted.any.js b/test/wpt/tests/fetch/api/cors/cors-preflight-not-cors-safelisted.any.js
new file mode 100644
index 0000000..b2747cc
--- /dev/null
+++ b/test/wpt/tests/fetch/api/cors/cors-preflight-not-cors-safelisted.any.js
@@ -0,0 +1,19 @@
+// META: script=/common/utils.js
+// META: script=../resources/utils.js
+// META: script=/common/get-host-info.sub.js
+// META: script=resources/corspreflight.js
+
+const corsURL = get_host_info().HTTP_REMOTE_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "preflight.py";
+
+promise_test(() => fetch("resources/not-cors-safelisted.json").then(res => res.json().then(runTests)), "Loading data…");
+
+function runTests(testArray) {
+ testArray.forEach(testItem => {
+ const [headerName, headerValue] = testItem;
+ corsPreflight("Need CORS-preflight for " + headerName + "/" + headerValue + " header",
+ corsURL,
+ "GET",
+ true,
+ [[headerName, headerValue]]);
+ });
+}
diff --git a/test/wpt/tests/fetch/api/cors/cors-preflight-redirect.any.js b/test/wpt/tests/fetch/api/cors/cors-preflight-redirect.any.js
new file mode 100644
index 0000000..15f7659
--- /dev/null
+++ b/test/wpt/tests/fetch/api/cors/cors-preflight-redirect.any.js
@@ -0,0 +1,37 @@
+// META: global=window,worker
+// META: script=/common/utils.js
+// META: script=../resources/utils.js
+// META: script=/common/get-host-info.sub.js
+
+function corsPreflightRedirect(desc, redirectUrl, redirectLocation, redirectStatus, redirectPreflight) {
+ var uuid_token = token();
+ var url = redirectUrl;
+ var urlParameters = "?token=" + uuid_token + "&max_age=0";
+ urlParameters += "&redirect_status=" + redirectStatus;
+ urlParameters += "&location=" + encodeURIComponent(redirectLocation);
+
+ if (redirectPreflight)
+ urlParameters += "&redirect_preflight";
+ var requestInit = {"mode": "cors", "redirect": "follow"};
+
+ /* Force preflight */
+ requestInit["headers"] = {"x-force-preflight": ""};
+ urlParameters += "&allow_headers=x-force-preflight";
+
+ promise_test(function(test) {
+ return fetch(RESOURCES_DIR + "clean-stash.py?token=" + uuid_token).then(function(resp) {
+ assert_equals(resp.status, 200, "Clean stash response's status is 200");
+ return promise_rejects_js(test, TypeError, fetch(url + urlParameters, requestInit));
+ });
+ }, desc);
+}
+
+var redirectUrl = get_host_info().HTTP_REMOTE_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "redirect.py";
+var locationUrl = get_host_info().HTTP_REMOTE_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "preflight.py";
+
+for (var code of [301, 302, 303, 307, 308]) {
+ /* preflight should not follow the redirection */
+ corsPreflightRedirect("Redirection " + code + " on preflight failed", redirectUrl, locationUrl, code, true);
+ /* preflight is done before redirection: preflight force redirect to error */
+ corsPreflightRedirect("Redirection " + code + " after preflight failed", redirectUrl, locationUrl, code, false);
+}
diff --git a/test/wpt/tests/fetch/api/cors/cors-preflight-referrer.any.js b/test/wpt/tests/fetch/api/cors/cors-preflight-referrer.any.js
new file mode 100644
index 0000000..5df9fcf
--- /dev/null
+++ b/test/wpt/tests/fetch/api/cors/cors-preflight-referrer.any.js
@@ -0,0 +1,51 @@
+// META: script=/common/utils.js
+// META: script=../resources/utils.js
+// META: script=/common/get-host-info.sub.js
+
+function corsPreflightReferrer(desc, corsUrl, referrerPolicy, referrer, expectedReferrer) {
+ var uuid_token = token();
+ var url = corsUrl;
+ var urlParameters = "?token=" + uuid_token + "&max_age=0";
+ var requestInit = {"mode": "cors", "referrerPolicy": referrerPolicy};
+
+ if (referrer)
+ requestInit.referrer = referrer;
+
+ /* Force preflight */
+ requestInit["headers"] = {"x-force-preflight": ""};
+ urlParameters += "&allow_headers=x-force-preflight";
+
+ promise_test(function(test) {
+ return fetch(RESOURCES_DIR + "clean-stash.py?token=" + uuid_token).then(function(resp) {
+ assert_equals(resp.status, 200, "Clean stash response's status is 200");
+ return fetch(url + urlParameters, requestInit).then(function(resp) {
+ assert_equals(resp.status, 200, "Response's status is 200");
+ assert_equals(resp.headers.get("x-did-preflight"), "1", "Preflight request has been made");
+ assert_equals(resp.headers.get("x-preflight-referrer"), expectedReferrer, "Preflight's referrer is correct");
+ assert_equals(resp.headers.get("x-referrer"), expectedReferrer, "Request's referrer is correct");
+ assert_equals(resp.headers.get("x-control-request-headers"), "", "Access-Control-Allow-Headers value");
+ });
+ });
+ }, desc + " and referrer: " + (referrer ? "'" + referrer + "'" : "default"));
+}
+
+var corsUrl = get_host_info().HTTP_REMOTE_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "preflight.py";
+var origin = get_host_info().HTTP_ORIGIN + "/";
+
+corsPreflightReferrer("Referrer policy: no-referrer", corsUrl, "no-referrer", undefined, "");
+corsPreflightReferrer("Referrer policy: no-referrer", corsUrl, "no-referrer", "myreferrer", "");
+
+corsPreflightReferrer("Referrer policy: \"\"", corsUrl, "", undefined, origin);
+corsPreflightReferrer("Referrer policy: \"\"", corsUrl, "", "myreferrer", origin);
+
+corsPreflightReferrer("Referrer policy: no-referrer-when-downgrade", corsUrl, "no-referrer-when-downgrade", undefined, location.toString())
+corsPreflightReferrer("Referrer policy: no-referrer-when-downgrade", corsUrl, "no-referrer-when-downgrade", "myreferrer", new URL("myreferrer", location).toString());
+
+corsPreflightReferrer("Referrer policy: origin", corsUrl, "origin", undefined, origin);
+corsPreflightReferrer("Referrer policy: origin", corsUrl, "origin", "myreferrer", origin);
+
+corsPreflightReferrer("Referrer policy: origin-when-cross-origin", corsUrl, "origin-when-cross-origin", undefined, origin);
+corsPreflightReferrer("Referrer policy: origin-when-cross-origin", corsUrl, "origin-when-cross-origin", "myreferrer", origin);
+
+corsPreflightReferrer("Referrer policy: unsafe-url", corsUrl, "unsafe-url", undefined, location.toString());
+corsPreflightReferrer("Referrer policy: unsafe-url", corsUrl, "unsafe-url", "myreferrer", new URL("myreferrer", location).toString());
diff --git a/test/wpt/tests/fetch/api/cors/cors-preflight-response-validation.any.js b/test/wpt/tests/fetch/api/cors/cors-preflight-response-validation.any.js
new file mode 100644
index 0000000..718e351
--- /dev/null
+++ b/test/wpt/tests/fetch/api/cors/cors-preflight-response-validation.any.js
@@ -0,0 +1,33 @@
+// META: script=/common/utils.js
+// META: script=../resources/utils.js
+// META: script=/common/get-host-info.sub.js
+
+function corsPreflightResponseValidation(desc, corsUrl, allowHeaders, allowMethods) {
+ var uuid_token = token();
+ var url = corsUrl;
+ var requestInit = {"mode": "cors"};
+ /* Force preflight */
+ requestInit["headers"] = {"x-force-preflight": ""};
+
+ var urlParameters = "?token=" + uuid_token + "&max_age=0";
+ urlParameters += "&allow_headers=x-force-preflight";
+ if (allowHeaders)
+ urlParameters += "," + allowHeaders;
+ if (allowMethods)
+ urlParameters += "&allow_methods="+ allowMethods;
+
+ promise_test(function(test) {
+ return fetch(RESOURCES_DIR + "clean-stash.py?token=" + uuid_token).then(async function(resp) {
+ assert_equals(resp.status, 200, "Clean stash response's status is 200");
+ await promise_rejects_js(test, TypeError, fetch(url + urlParameters, requestInit));
+
+ return fetch(url + urlParameters).then(function(resp) {
+ assert_equals(resp.headers.get("x-did-preflight"), "1", "Preflight request has been made");
+ });
+ });
+ }, desc);
+}
+
+var corsUrl = get_host_info().HTTP_REMOTE_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "preflight.py";
+corsPreflightResponseValidation("Preflight response with a bad Access-Control-Allow-Headers", corsUrl, "Bad value", null);
+corsPreflightResponseValidation("Preflight response with a bad Access-Control-Allow-Methods", corsUrl, null, "Bad value");
diff --git a/test/wpt/tests/fetch/api/cors/cors-preflight-star.any.js b/test/wpt/tests/fetch/api/cors/cors-preflight-star.any.js
new file mode 100644
index 0000000..f9fb204
--- /dev/null
+++ b/test/wpt/tests/fetch/api/cors/cors-preflight-star.any.js
@@ -0,0 +1,86 @@
+// META: script=../resources/utils.js
+// META: script=/common/get-host-info.sub.js
+
+const url = get_host_info().HTTP_REMOTE_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "preflight.py",
+ origin = location.origin // assuming an ASCII origin
+
+function preflightTest(succeeds, withCredentials, allowMethod, allowHeader, useMethod, useHeader) {
+ return promise_test(t => {
+ let testURL = url + "?",
+ requestInit = {}
+ if (withCredentials) {
+ testURL += "origin=" + origin + "&"
+ testURL += "credentials&"
+ requestInit.credentials = "include"
+ }
+ if (useMethod) {
+ requestInit.method = useMethod
+ }
+ if (useHeader.length > 0) {
+ requestInit.headers = [useHeader]
+ }
+ testURL += "allow_methods=" + allowMethod + "&"
+ testURL += "allow_headers=" + allowHeader + "&"
+
+ if (succeeds) {
+ return fetch(testURL, requestInit).then(resp => {
+ assert_equals(resp.headers.get("x-origin"), origin)
+ })
+ } else {
+ return promise_rejects_js(t, TypeError, fetch(testURL, requestInit))
+ }
+ }, "CORS that " + (succeeds ? "succeeds" : "fails") + " with credentials: " + withCredentials + "; method: " + useMethod + " (allowed: " + allowMethod + "); header: " + useHeader + " (allowed: " + allowHeader + ")")
+}
+
+// "GET" does not pass the case-sensitive method check, but in the safe list.
+preflightTest(true, false, "get", "x-test", "GET", ["X-Test", "1"])
+// Headers check is case-insensitive, and "*" works as any for method.
+preflightTest(true, false, "*", "x-test", "SUPER", ["X-Test", "1"])
+// "*" works as any only without credentials.
+preflightTest(true, false, "*", "*", "OK", ["X-Test", "1"])
+preflightTest(false, true, "*", "*", "OK", ["X-Test", "1"])
+preflightTest(false, true, "*", "", "PUT", [])
+preflightTest(false, true, "get", "*", "GET", ["X-Test", "1"])
+preflightTest(false, true, "*", "*", "GET", ["X-Test", "1"])
+// Exact character match works even for "*" with credentials.
+preflightTest(true, true, "*", "*", "*", ["*", "1"])
+
+// The following methods are upper-cased for init["method"] by
+// https://fetch.spec.whatwg.org/#concept-method-normalize
+// but not in Access-Control-Allow-Methods response.
+// But they are https://fetch.spec.whatwg.org/#cors-safelisted-method,
+// CORS anyway passes regardless of the cases.
+for (const METHOD of ['GET', 'HEAD', 'POST']) {
+ const method = METHOD.toLowerCase();
+ preflightTest(true, true, METHOD, "*", METHOD, [])
+ preflightTest(true, true, METHOD, "*", method, [])
+ preflightTest(true, true, method, "*", METHOD, [])
+ preflightTest(true, true, method, "*", method, [])
+}
+
+// The following methods are upper-cased for init["method"] by
+// https://fetch.spec.whatwg.org/#concept-method-normalize
+// but not in Access-Control-Allow-Methods response.
+// As they are not https://fetch.spec.whatwg.org/#cors-safelisted-method,
+// Access-Control-Allow-Methods should contain upper-cased methods,
+// while init["method"] can be either in upper or lower case.
+for (const METHOD of ['DELETE', 'PUT']) {
+ const method = METHOD.toLowerCase();
+ preflightTest(true, true, METHOD, "*", METHOD, [])
+ preflightTest(true, true, METHOD, "*", method, [])
+ preflightTest(false, true, method, "*", METHOD, [])
+ preflightTest(false, true, method, "*", method, [])
+}
+
+// "PATCH" is NOT upper-cased in both places because it is not listed in
+// https://fetch.spec.whatwg.org/#concept-method-normalize.
+// So Access-Control-Allow-Methods value and init["method"] should match
+// case-sensitively.
+preflightTest(true, true, "PATCH", "*", "PATCH", [])
+preflightTest(false, true, "PATCH", "*", "patch", [])
+preflightTest(false, true, "patch", "*", "PATCH", [])
+preflightTest(true, true, "patch", "*", "patch", [])
+
+// "Authorization" header can't be wildcarded.
+preflightTest(false, false, "*", "*", "POST", ["Authorization", "123"])
+preflightTest(true, false, "*", "*, Authorization", "POST", ["Authorization", "123"])
diff --git a/test/wpt/tests/fetch/api/cors/cors-preflight-status.any.js b/test/wpt/tests/fetch/api/cors/cors-preflight-status.any.js
new file mode 100644
index 0000000..a4467a6
--- /dev/null
+++ b/test/wpt/tests/fetch/api/cors/cors-preflight-status.any.js
@@ -0,0 +1,37 @@
+// META: script=/common/utils.js
+// META: script=../resources/utils.js
+// META: script=/common/get-host-info.sub.js
+
+/* Check preflight is ok if status is ok status (200 to 299)*/
+function corsPreflightStatus(desc, corsUrl, preflightStatus) {
+ var uuid_token = token();
+ var url = corsUrl;
+ var requestInit = {"mode": "cors"};
+ /* Force preflight */
+ requestInit["headers"] = {"x-force-preflight": ""};
+
+ var urlParameters = "?token=" + uuid_token + "&max_age=0";
+ urlParameters += "&allow_headers=x-force-preflight";
+ urlParameters += "&preflight_status=" + preflightStatus;
+
+ promise_test(function(test) {
+ return fetch(RESOURCES_DIR + "clean-stash.py?token=" + uuid_token).then(function(resp) {
+ assert_equals(resp.status, 200, "Clean stash response's status is 200");
+ if (200 <= preflightStatus && 299 >= preflightStatus) {
+ return fetch(url + urlParameters, requestInit).then(function(resp) {
+ assert_equals(resp.status, 200, "Response's status is 200");
+ assert_equals(resp.headers.get("x-did-preflight"), "1", "Preflight request has been made");
+ });
+ } else {
+ return promise_rejects_js(test, TypeError, fetch(url + urlParameters, requestInit));
+ }
+ });
+ }, desc);
+}
+
+var corsUrl = get_host_info().HTTP_REMOTE_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "preflight.py";
+for (status of [200, 201, 202, 203, 204, 205, 206,
+ 300, 301, 302, 303, 304, 305, 306, 307, 308,
+ 400, 401, 402, 403, 404, 405,
+ 501, 502, 503, 504, 505])
+ corsPreflightStatus("Preflight answered with status " + status, corsUrl, status);
diff --git a/test/wpt/tests/fetch/api/cors/cors-preflight.any.js b/test/wpt/tests/fetch/api/cors/cors-preflight.any.js
new file mode 100644
index 0000000..045422f
--- /dev/null
+++ b/test/wpt/tests/fetch/api/cors/cors-preflight.any.js
@@ -0,0 +1,62 @@
+// META: script=/common/utils.js
+// META: script=../resources/utils.js
+// META: script=/common/get-host-info.sub.js
+// META: script=resources/corspreflight.js
+
+var corsUrl = get_host_info().HTTP_REMOTE_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "preflight.py";
+
+corsPreflight("CORS [DELETE], server allows", corsUrl, "DELETE", true);
+corsPreflight("CORS [DELETE], server refuses", corsUrl, "DELETE", false);
+corsPreflight("CORS [PUT], server allows", corsUrl, "PUT", true);
+corsPreflight("CORS [PUT], server allows, check preflight has user agent", corsUrl + "?checkUserAgentHeaderInPreflight", "PUT", true);
+corsPreflight("CORS [PUT], server refuses", corsUrl, "PUT", false);
+corsPreflight("CORS [PATCH], server allows", corsUrl, "PATCH", true);
+corsPreflight("CORS [PATCH], server refuses", corsUrl, "PATCH", false);
+corsPreflight("CORS [patcH], server allows", corsUrl, "patcH", true);
+corsPreflight("CORS [patcH], server refuses", corsUrl, "patcH", false);
+corsPreflight("CORS [NEW], server allows", corsUrl, "NEW", true);
+corsPreflight("CORS [NEW], server refuses", corsUrl, "NEW", false);
+corsPreflight("CORS [chicken], server allows", corsUrl, "chicken", true);
+corsPreflight("CORS [chicken], server refuses", corsUrl, "chicken", false);
+
+corsPreflight("CORS [GET] [x-test-header: allowed], server allows", corsUrl, "GET", true, [["x-test-header1", "allowed"]]);
+corsPreflight("CORS [GET] [x-test-header: refused], server refuses", corsUrl, "GET", false, [["x-test-header1", "refused"]]);
+
+var headers = [
+ ["x-test-header1", "allowedOrRefused"],
+ ["x-test-header2", "allowedOrRefused"],
+ ["X-test-header3", "allowedOrRefused"],
+ ["x-test-header-b", "allowedOrRefused"],
+ ["x-test-header-D", "allowedOrRefused"],
+ ["x-test-header-C", "allowedOrRefused"],
+ ["x-test-header-a", "allowedOrRefused"],
+ ["Content-Type", "allowedOrRefused"],
+];
+var safeHeaders= [
+ ["Accept", "*"],
+ ["Accept-Language", "bzh"],
+ ["Content-Language", "eu"],
+];
+
+corsPreflight("CORS [GET] [several headers], server allows", corsUrl, "GET", true, headers, safeHeaders);
+corsPreflight("CORS [GET] [several headers], server refuses", corsUrl, "GET", false, headers, safeHeaders);
+corsPreflight("CORS [PUT] [several headers], server allows", corsUrl, "PUT", true, headers, safeHeaders);
+corsPreflight("CORS [PUT] [several headers], server refuses", corsUrl, "PUT", false, headers, safeHeaders);
+
+corsPreflight("CORS [PUT] [only safe headers], server allows", corsUrl, "PUT", true, null, safeHeaders);
+
+promise_test(async t => {
+ const url = `${corsUrl}?allow_headers=*`;
+ await promise_rejects_js(t, TypeError, fetch(url, {
+ headers: {
+ authorization: 'foobar'
+ }
+ }));
+}, '"authorization" should not be covered by the wildcard symbol');
+
+promise_test(async t => {
+ const url = `${corsUrl}?allow_headers=authorization`;
+ await fetch(url, { headers: {
+ authorization: 'foobar'
+ }});
+}, '"authorization" should be covered by "authorization"'); \ No newline at end of file
diff --git a/test/wpt/tests/fetch/api/cors/cors-redirect-credentials.any.js b/test/wpt/tests/fetch/api/cors/cors-redirect-credentials.any.js
new file mode 100644
index 0000000..2aff313
--- /dev/null
+++ b/test/wpt/tests/fetch/api/cors/cors-redirect-credentials.any.js
@@ -0,0 +1,52 @@
+// META: timeout=long
+// META: script=../resources/utils.js
+// META: script=/common/get-host-info.sub.js
+
+function corsRedirectCredentials(desc, redirectUrl, redirectLocation, redirectStatus, locationCredentials) {
+ var url = redirectUrl
+ var urlParameters = "?redirect_status=" + redirectStatus;
+ urlParameters += "&location=" + redirectLocation.replace("://", "://" + locationCredentials + "@");
+
+ var requestInit = {"mode": "cors", "redirect": "follow"};
+
+ promise_test(t => {
+ const result = fetch(url + urlParameters, requestInit)
+ if(locationCredentials === "") {
+ return result;
+ } else {
+ return promise_rejects_js(t, TypeError, result);
+ }
+ }, desc);
+}
+
+var redirPath = dirname(location.pathname) + RESOURCES_DIR + "redirect.py";
+var preflightPath = dirname(location.pathname) + RESOURCES_DIR + "preflight.py";
+
+var host_info = get_host_info();
+
+var localRedirect = host_info.HTTP_ORIGIN + redirPath;
+var remoteRedirect = host_info.HTTP_ORIGIN_WITH_DIFFERENT_PORT + redirPath;
+
+var localLocation = host_info.HTTP_ORIGIN + preflightPath;
+var remoteLocation = host_info.HTTP_ORIGIN_WITH_DIFFERENT_PORT + preflightPath;
+var remoteLocation2 = host_info.HTTP_REMOTE_ORIGIN + preflightPath;
+
+for (var code of [301, 302, 303, 307, 308]) {
+ corsRedirectCredentials("Redirect " + code + " from same origin to remote without user and password", localRedirect, remoteLocation, code, "");
+
+ corsRedirectCredentials("Redirect " + code + " from same origin to remote with user and password", localRedirect, remoteLocation, code, "user:password");
+ corsRedirectCredentials("Redirect " + code + " from same origin to remote with user", localRedirect, remoteLocation, code, "user:");
+ corsRedirectCredentials("Redirect " + code + " from same origin to remote with password", localRedirect, remoteLocation, code, ":password");
+
+ corsRedirectCredentials("Redirect " + code + " from remote to same origin with user and password", remoteRedirect, localLocation, code, "user:password");
+ corsRedirectCredentials("Redirect " + code + " from remote to same origin with user", remoteRedirect, localLocation, code, "user:");
+ corsRedirectCredentials("Redirect " + code + " from remote to same origin with password", remoteRedirect, localLocation, code, ":password");
+
+ corsRedirectCredentials("Redirect " + code + " from remote to same remote with user and password", remoteRedirect, remoteLocation, code, "user:password");
+ corsRedirectCredentials("Redirect " + code + " from remote to same remote with user", remoteRedirect, remoteLocation, code, "user:");
+ corsRedirectCredentials("Redirect " + code + " from remote to same remote with password", remoteRedirect, remoteLocation, code, ":password");
+
+ corsRedirectCredentials("Redirect " + code + " from remote to another remote with user and password", remoteRedirect, remoteLocation2, code, "user:password");
+ corsRedirectCredentials("Redirect " + code + " from remote to another remote with user", remoteRedirect, remoteLocation2, code, "user:");
+ corsRedirectCredentials("Redirect " + code + " from remote to another remote with password", remoteRedirect, remoteLocation2, code, ":password");
+}
diff --git a/test/wpt/tests/fetch/api/cors/cors-redirect-preflight.any.js b/test/wpt/tests/fetch/api/cors/cors-redirect-preflight.any.js
new file mode 100644
index 0000000..5084817
--- /dev/null
+++ b/test/wpt/tests/fetch/api/cors/cors-redirect-preflight.any.js
@@ -0,0 +1,46 @@
+// META: timeout=long
+// META: script=/common/utils.js
+// META: script=../resources/utils.js
+// META: script=/common/get-host-info.sub.js
+
+function corsRedirect(desc, redirectUrl, redirectLocation, redirectStatus, expectSuccess) {
+ var urlBaseParameters = "&redirect_status=" + redirectStatus;
+ var urlParametersSuccess = urlBaseParameters + "&allow_headers=x-w3c&location=" + encodeURIComponent(redirectLocation + "?allow_headers=x-w3c");
+ var urlParametersFailure = urlBaseParameters + "&location=" + encodeURIComponent(redirectLocation);
+
+ var requestInit = {"mode": "cors", "redirect": "follow", "headers" : [["x-w3c", "test"]]};
+
+ promise_test(function(test) {
+ var uuid_token = token();
+ return fetch(RESOURCES_DIR + "clean-stash.py?token=" + uuid_token).then(function(resp) {
+ return fetch(redirectUrl + "?token=" + uuid_token + "&max_age=0" + urlParametersSuccess, requestInit).then(function(resp) {
+ assert_equals(resp.status, 200, "Response's status is 200");
+ assert_equals(resp.headers.get("x-did-preflight"), "1", "Preflight request has been made");
+ });
+ });
+ }, desc + " (preflight after redirection success case)");
+ promise_test(function(test) {
+ var uuid_token = token();
+ return fetch(RESOURCES_DIR + "clean-stash.py?token=" + uuid_token).then(function(resp) {
+ return promise_rejects_js(test, TypeError, fetch(redirectUrl + "?token=" + uuid_token + "&max_age=0" + urlParametersFailure, requestInit));
+ });
+ }, desc + " (preflight after redirection failure case)");
+}
+
+var redirPath = dirname(location.pathname) + RESOURCES_DIR + "redirect.py";
+var preflightPath = dirname(location.pathname) + RESOURCES_DIR + "preflight.py";
+
+var host_info = get_host_info();
+
+var localRedirect = host_info.HTTP_ORIGIN + redirPath;
+var remoteRedirect = host_info.HTTP_REMOTE_ORIGIN + redirPath;
+
+var localLocation = host_info.HTTP_ORIGIN + preflightPath;
+var remoteLocation = host_info.HTTP_REMOTE_ORIGIN + preflightPath;
+var remoteLocation2 = host_info.HTTP_ORIGIN_WITH_DIFFERENT_PORT + preflightPath;
+
+for (var code of [301, 302, 303, 307, 308]) {
+ corsRedirect("Redirect " + code + ": same origin to cors", localRedirect, remoteLocation, code);
+ corsRedirect("Redirect " + code + ": cors to same origin", remoteRedirect, localLocation, code);
+ corsRedirect("Redirect " + code + ": cors to another cors", remoteRedirect, remoteLocation2, code);
+}
diff --git a/test/wpt/tests/fetch/api/cors/cors-redirect.any.js b/test/wpt/tests/fetch/api/cors/cors-redirect.any.js
new file mode 100644
index 0000000..cdf4097
--- /dev/null
+++ b/test/wpt/tests/fetch/api/cors/cors-redirect.any.js
@@ -0,0 +1,42 @@
+// META: script=/common/utils.js
+// META: script=../resources/utils.js
+// META: script=/common/get-host-info.sub.js
+
+function corsRedirect(desc, redirectUrl, redirectLocation, redirectStatus, expectedOrigin) {
+ var uuid_token = token();
+ var url = redirectUrl;
+ var urlParameters = "?token=" + uuid_token + "&max_age=0";
+ urlParameters += "&redirect_status=" + redirectStatus;
+ urlParameters += "&location=" + encodeURIComponent(redirectLocation);
+
+ var requestInit = {"mode": "cors", "redirect": "follow"};
+
+ return promise_test(function(test) {
+ return fetch(RESOURCES_DIR + "clean-stash.py?token=" + uuid_token).then(function(resp) {
+ return fetch(url + urlParameters, requestInit).then(function(resp) {
+ assert_equals(resp.status, 200, "Response's status is 200");
+ assert_equals(resp.headers.get("x-did-preflight"), "0", "No preflight request has been made");
+ assert_equals(resp.headers.get("x-origin"), expectedOrigin, "Origin is correctly set after redirect");
+ });
+ });
+ }, desc);
+}
+
+var redirPath = dirname(location.pathname) + RESOURCES_DIR + "redirect.py";
+var preflightPath = dirname(location.pathname) + RESOURCES_DIR + "preflight.py";
+
+var host_info = get_host_info();
+
+var localRedirect = host_info.HTTP_ORIGIN + redirPath;
+var remoteRedirect = host_info.HTTP_REMOTE_ORIGIN + redirPath;
+
+var localLocation = host_info.HTTP_ORIGIN + preflightPath;
+var remoteLocation = host_info.HTTP_REMOTE_ORIGIN + preflightPath;
+var remoteLocation2 = host_info.HTTP_ORIGIN_WITH_DIFFERENT_PORT + preflightPath;
+
+for (var code of [301, 302, 303, 307, 308]) {
+ corsRedirect("Redirect " + code + ": cors to same cors", remoteRedirect, remoteLocation, code, location.origin);
+ corsRedirect("Redirect " + code + ": cors to another cors", remoteRedirect, remoteLocation2, code, "null");
+ corsRedirect("Redirect " + code + ": same origin to cors", localRedirect, remoteLocation, code, location.origin);
+ corsRedirect("Redirect " + code + ": cors to same origin", remoteRedirect, localLocation, code, "null");
+}
diff --git a/test/wpt/tests/fetch/api/cors/data-url-iframe.html b/test/wpt/tests/fetch/api/cors/data-url-iframe.html
new file mode 100644
index 0000000..217baa3
--- /dev/null
+++ b/test/wpt/tests/fetch/api/cors/data-url-iframe.html
@@ -0,0 +1,58 @@
+<!DOCTYPE html>
+<html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<body></body>
+<script>
+
+const createDataUrlIframe = (url, cors) => {
+ const iframe = document.createElement("iframe");
+ const fetchURL = new URL(url, location.href) +
+ `${cors === 'null-origin'
+ ? '?pipe=header(Access-Control-Allow-Origin, null)' : ''}`;
+ const tag_name = 'script';
+ iframe.src =
+ `data:text/html, <${tag_name}>` +
+ `async function test() {` +
+ ` let allowed = true;` +
+ ` try {` +
+ ` await fetch('${fetchURL}');` +
+ ` } catch (e) {` +
+ ` allowed = false;` +
+ ` }` +
+ ` parent.postMessage({allowed}, '*');` +
+ `}` +
+ `test(); </${tag_name}>`;
+ return iframe;
+};
+
+const fetch_from_data_url_iframe_test =
+ (url, cors, expectation, description) => {
+ promise_test(async () => {
+ const iframe = createDataUrlIframe(url, cors);
+ document.body.appendChild(iframe);
+ const msgEvent = await new Promise(resolve => window.onmessage = resolve);
+ assert_equals(msgEvent.data.allowed ? 'allowed' : 'rejected', expectation);
+ }, description);
+};
+
+fetch_from_data_url_iframe_test(
+ '../resources/top.txt',
+ 'acao-omitted',
+ 'rejected',
+ 'fetching "top.txt" without ACAO should be rejected.'
+);
+fetch_from_data_url_iframe_test(
+ '../resources/top.txt',
+ 'null-origin',
+ 'allowed',
+ 'fetching "top.txt" with CORS allowing null origin should be allowed.'
+);
+fetch_from_data_url_iframe_test(
+ 'data:text/plain, top',
+ 'acao-omitted',
+ 'allowed',
+ 'fetching data url script should be allowed.'
+);
+
+</script>
diff --git a/test/wpt/tests/fetch/api/cors/data-url-shared-worker.html b/test/wpt/tests/fetch/api/cors/data-url-shared-worker.html
new file mode 100644
index 0000000..d69748a
--- /dev/null
+++ b/test/wpt/tests/fetch/api/cors/data-url-shared-worker.html
@@ -0,0 +1,53 @@
+<!DOCTYPE html>
+<html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+
+const fetch_from_data_url_worker_test =
+ (url, cors, expectation, description) => {
+ promise_test(async () => {
+ const fetchURL = new URL(url, location.href) +
+ `${cors === 'null-origin'
+ ? '?pipe=header(Access-Control-Allow-Origin, null)' : ''}`;
+ const scriptURL =
+ `data:text/javascript,` +
+ `async function test(port) {` +
+ ` let allowed = true;` +
+ ` try {` +
+ ` await fetch('${fetchURL}');` +
+ ` } catch (e) {` +
+ ` allowed = false;` +
+ ` }` +
+ ` port.postMessage({allowed});` +
+ `}` +
+ `onconnect = e => {` +
+ ` test(e.ports[0]);` +
+ `};`;
+ const worker = new SharedWorker(scriptURL);
+ const msgEvent =
+ await new Promise(resolve => worker.port.onmessage = resolve);
+ assert_equals(msgEvent.data.allowed ? 'allowed' : 'rejected', expectation);
+ }, description);
+};
+
+fetch_from_data_url_worker_test(
+ '../resources/top.txt',
+ 'acao-omitted',
+ 'rejected',
+ 'fetching "top.txt" without ACAO should be rejected.'
+);
+fetch_from_data_url_worker_test(
+ '../resources/top.txt',
+ 'null-origin',
+ 'allowed',
+ 'fetching "top.txt" with CORS allowing null origin should be allowed.'
+);
+fetch_from_data_url_worker_test(
+ 'data:text/plain, top',
+ 'acao-omitted',
+ 'allowed',
+ 'fetching data url script should be allowed.'
+);
+
+</script>
diff --git a/test/wpt/tests/fetch/api/cors/data-url-worker.html b/test/wpt/tests/fetch/api/cors/data-url-worker.html
new file mode 100644
index 0000000..13113e6
--- /dev/null
+++ b/test/wpt/tests/fetch/api/cors/data-url-worker.html
@@ -0,0 +1,50 @@
+<!DOCTYPE html>
+<html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+
+const fetch_from_data_url_shared_worker_test =
+ (url, cors, expectation, description) => {
+ promise_test(async () => {
+ const fetchURL = new URL(url, location.href) +
+ `${cors === 'null-origin'
+ ? '?pipe=header(Access-Control-Allow-Origin, null)' : ''}`;
+ const scriptURL =
+ `data:text/javascript,` +
+ `async function test() {` +
+ ` let allowed = true;` +
+ ` try {` +
+ ` await fetch('${fetchURL}');` +
+ ` } catch (e) {` +
+ ` allowed = false;` +
+ ` }` +
+ ` postMessage({allowed});` +
+ `}` +
+ `test();`;
+ const worker = new Worker(scriptURL);
+ const msgEvent = await new Promise(resolve => worker.onmessage = resolve);
+ assert_equals(msgEvent.data.allowed ? 'allowed' : 'rejected', expectation);
+ }, description);
+};
+
+fetch_from_data_url_shared_worker_test(
+ '../resources/top.txt',
+ 'acao-omitted',
+ 'rejected',
+ 'fetching "top.txt" without ACAO should be rejected.'
+);
+fetch_from_data_url_shared_worker_test(
+ '../resources/top.txt',
+ 'null-origin',
+ 'allowed',
+ 'fetching "top.txt" with CORS allowing null origin should be allowed.'
+);
+fetch_from_data_url_shared_worker_test(
+ 'data:text/plain, top',
+ 'acao-omitted',
+ 'allowed',
+ 'fetching data url script should be allowed.'
+);
+
+</script>
diff --git a/test/wpt/tests/fetch/api/cors/resources/corspreflight.js b/test/wpt/tests/fetch/api/cors/resources/corspreflight.js
new file mode 100644
index 0000000..18b8f6d
--- /dev/null
+++ b/test/wpt/tests/fetch/api/cors/resources/corspreflight.js
@@ -0,0 +1,58 @@
+function headerNames(headers) {
+ let names = [];
+ for (let header of headers) {
+ names.push(header[0].toLowerCase());
+ }
+ return names;
+}
+
+/*
+ Check preflight is done
+ Control if server allows method and headers and check accordingly
+ Check control access headers added by UA (for method and headers)
+*/
+function corsPreflight(desc, corsUrl, method, allowed, headers, safeHeaders) {
+ return promise_test(function(test) {
+ var uuid_token = token();
+ return fetch(RESOURCES_DIR + "clean-stash.py?token=" + uuid_token).then(function(response) {
+ var url = corsUrl + (corsUrl.indexOf("?") === -1 ? "?" : "&");
+ var urlParameters = "token=" + uuid_token + "&max_age=0";
+ var requestInit = {"mode": "cors", "method": method};
+ var requestHeaders = [];
+ if (headers)
+ requestHeaders.push.apply(requestHeaders, headers);
+ if (safeHeaders)
+ requestHeaders.push.apply(requestHeaders, safeHeaders);
+ requestInit["headers"] = requestHeaders;
+
+ if (allowed) {
+ urlParameters += "&allow_methods=" + method + "&control_request_headers";
+ if (headers) {
+ //Make the server allow the headers
+ urlParameters += "&allow_headers=" + headerNames(headers).join("%20%2C");
+ }
+ return fetch(url + urlParameters, requestInit).then(function(resp) {
+ assert_equals(resp.status, 200, "Response's status is 200");
+ assert_equals(resp.headers.get("x-did-preflight"), "1", "Preflight request has been made");
+ if (headers) {
+ var actualHeaders = resp.headers.get("x-control-request-headers").toLowerCase().split(",");
+ for (var i in actualHeaders)
+ actualHeaders[i] = actualHeaders[i].trim();
+ for (var header of headers)
+ assert_in_array(header[0].toLowerCase(), actualHeaders, "Preflight asked permission for header: " + header);
+
+ let accessControlAllowHeaders = headerNames(headers).sort().join(",");
+ assert_equals(resp.headers.get("x-control-request-headers"), accessControlAllowHeaders, "Access-Control-Allow-Headers value");
+ return fetch(RESOURCES_DIR + "clean-stash.py?token=" + uuid_token);
+ } else {
+ assert_equals(resp.headers.get("x-control-request-headers"), null, "Access-Control-Request-Headers should be omitted")
+ }
+ });
+ } else {
+ return promise_rejects_js(test, TypeError, fetch(url + urlParameters, requestInit)).then(function(){
+ return fetch(RESOURCES_DIR + "clean-stash.py?token=" + uuid_token);
+ });
+ }
+ });
+ }, desc);
+}
diff --git a/test/wpt/tests/fetch/api/cors/resources/not-cors-safelisted.json b/test/wpt/tests/fetch/api/cors/resources/not-cors-safelisted.json
new file mode 100644
index 0000000..945dc0f
--- /dev/null
+++ b/test/wpt/tests/fetch/api/cors/resources/not-cors-safelisted.json
@@ -0,0 +1,13 @@
+[
+ ["accept", "\""],
+ ["accept", "012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678"],
+ ["accept-language", "\u0001"],
+ ["accept-language", "@"],
+ ["authorization", "basics"],
+ ["content-language", "\u0001"],
+ ["content-language", "@"],
+ ["content-type", "text/html"],
+ ["content-type", "text/plain; long=0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901"],
+ ["range", "bytes 0-"],
+ ["test", "hi"]
+]
diff --git a/test/wpt/tests/fetch/api/cors/sandboxed-iframe.html b/test/wpt/tests/fetch/api/cors/sandboxed-iframe.html
new file mode 100644
index 0000000..feb9f1f
--- /dev/null
+++ b/test/wpt/tests/fetch/api/cors/sandboxed-iframe.html
@@ -0,0 +1,14 @@
+<!doctype html>
+<html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<iframe sandbox="allow-scripts" src="../resources/sandboxed-iframe.html"></iframe>
+<script>
+promise_test(async (t) => {
+ const message = await new Promise((resolve) => {
+ window.addEventListener('message', e => resolve(e.data));
+ });
+ assert_equals(message, 'PASS');
+}, 'CORS with sandboxed iframe');
+</script>
+</html>
diff --git a/test/wpt/tests/fetch/api/crashtests/body-window-destroy.html b/test/wpt/tests/fetch/api/crashtests/body-window-destroy.html
new file mode 100644
index 0000000..646d3c5
--- /dev/null
+++ b/test/wpt/tests/fetch/api/crashtests/body-window-destroy.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<iframe srcdoc='
+ <script>
+ let a = new Blob(["a", "𢕾"], {})
+ let b = new Response(a)
+ try { let _ = b.body } catch (e) { }
+ frameElement.remove()
+ b.json().catch(() => {})
+ </script>
+'></iframe>
diff --git a/test/wpt/tests/fetch/api/crashtests/request.html b/test/wpt/tests/fetch/api/crashtests/request.html
new file mode 100644
index 0000000..2d21930
--- /dev/null
+++ b/test/wpt/tests/fetch/api/crashtests/request.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<script src="/common/utils.js"></script>
+<script>
+ // Cycle collection test for a case where the Request object is alive and accessible globally.
+ var req = new Request(`/`);
+ fetch(req)
+</script>
diff --git a/test/wpt/tests/fetch/api/credentials/authentication-basic.any.js b/test/wpt/tests/fetch/api/credentials/authentication-basic.any.js
new file mode 100644
index 0000000..31ccc38
--- /dev/null
+++ b/test/wpt/tests/fetch/api/credentials/authentication-basic.any.js
@@ -0,0 +1,17 @@
+// META: global=window,worker
+
+function basicAuth(desc, user, pass, mode, status) {
+ promise_test(function(test) {
+ var headers = { "Authorization": "Basic " + btoa(user + ":" + pass)};
+ var requestInit = {"credentials": mode, "headers": headers};
+ return fetch("../resources/authentication.py?realm=test", requestInit).then(function(resp) {
+ assert_equals(resp.status, status, "HTTP status is " + status);
+ assert_equals(resp.type , "basic", "Response's type is basic");
+ });
+ }, desc);
+}
+
+basicAuth("User-added Authorization header with include mode", "user", "password", "include", 200);
+basicAuth("User-added Authorization header with same-origin mode", "user", "password", "same-origin", 200);
+basicAuth("User-added Authorization header with omit mode", "user", "password", "omit", 200);
+basicAuth("User-added bogus Authorization header with omit mode", "notuser", "notpassword", "omit", 401);
diff --git a/test/wpt/tests/fetch/api/credentials/authentication-redirection.any.js b/test/wpt/tests/fetch/api/credentials/authentication-redirection.any.js
new file mode 100644
index 0000000..16656b5
--- /dev/null
+++ b/test/wpt/tests/fetch/api/credentials/authentication-redirection.any.js
@@ -0,0 +1,29 @@
+// META: global=window,worker
+// META: script=/common/get-host-info.sub.js
+
+const authorizationValue = "Basic " + btoa("user:pass");
+async function getAuthorizationHeaderValue(url)
+{
+ const headers = { "Authorization": authorizationValue};
+ const requestInit = {"headers": headers};
+ const response = await fetch(url, requestInit);
+ return response.text();
+}
+
+promise_test(async test => {
+ const result = await getAuthorizationHeaderValue("/fetch/api/resources/dump-authorization-header.py");
+ assert_equals(result, authorizationValue);
+}, "getAuthorizationHeaderValue - no redirection");
+
+promise_test(async test => {
+ result = await getAuthorizationHeaderValue("/fetch/api/resources/redirect.py?location=" + encodeURIComponent("/fetch/api/resources/dump-authorization-header.py"));
+ assert_equals(result, authorizationValue);
+
+ result = await getAuthorizationHeaderValue(get_host_info().HTTPS_REMOTE_ORIGIN + "/fetch/api/resources/redirect.py?allow_headers=Authorization&location=" + encodeURIComponent(get_host_info().HTTPS_REMOTE_ORIGIN + "/fetch/api/resources/dump-authorization-header.py"));
+ assert_equals(result, authorizationValue);
+}, "getAuthorizationHeaderValue - same origin redirection");
+
+promise_test(async (test) => {
+ const result = await getAuthorizationHeaderValue(get_host_info().HTTPS_REMOTE_ORIGIN + "/fetch/api/resources/redirect.py?allow_headers=Authorization&location=" + encodeURIComponent(get_host_info().HTTPS_ORIGIN + "/fetch/api/resources/dump-authorization-header.py"));
+ assert_equals(result, "none");
+}, "getAuthorizationHeaderValue - cross origin redirection");
diff --git a/test/wpt/tests/fetch/api/credentials/cookies.any.js b/test/wpt/tests/fetch/api/credentials/cookies.any.js
new file mode 100644
index 0000000..de30e47
--- /dev/null
+++ b/test/wpt/tests/fetch/api/credentials/cookies.any.js
@@ -0,0 +1,49 @@
+// META: global=window,worker
+// META: script=../resources/utils.js
+
+function cookies(desc, credentials1, credentials2 ,cookies) {
+ var url = RESOURCES_DIR + "top.txt"
+ var urlParameters = "";
+ var urlCleanParameters = "";
+ if (cookies) {
+ urlParameters +="?pipe=header(Set-Cookie,";
+ urlParameters += cookies.join(",True)|header(Set-Cookie,") + ",True)";
+ urlCleanParameters +="?pipe=header(Set-Cookie,";
+ urlCleanParameters += cookies.join("%3B%20max-age=0,True)|header(Set-Cookie,") + "%3B%20max-age=0,True)";
+ }
+
+ var requestInit = {"credentials": credentials1}
+ promise_test(function(test){
+ var requestInit = {"credentials": credentials1}
+ return fetch(url + urlParameters, requestInit).then(function(resp) {
+ assert_equals(resp.status, 200, "HTTP status is 200");
+ assert_equals(resp.type , "basic", "Response's type is basic");
+ //check cookies sent
+ return fetch(RESOURCES_DIR + "inspect-headers.py?headers=cookie" , {"credentials": credentials2});
+ }).then(function(resp) {
+ assert_equals(resp.status, 200, "HTTP status is 200");
+ assert_equals(resp.type , "basic", "Response's type is basic");
+ assert_false(resp.headers.has("Cookie") , "Cookie header is not exposed in response");
+ if (credentials1 != "omit" && credentials2 != "omit") {
+ assert_equals(resp.headers.get("x-request-cookie") , cookies.join("; "), "Request include cookie(s)");
+ }
+ else {
+ assert_false(resp.headers.has("x-request-cookie") , "Request does not have cookie(s)");
+ }
+ //clean cookies
+ return fetch(url + urlCleanParameters, {"credentials": "include"});
+ }).catch(function(e) {
+ return fetch(url + urlCleanParameters, {"credentials": "include"}).then(function() {
+ return Promise.reject(e);
+ });
+ });
+ }, desc);
+}
+
+cookies("Include mode: 1 cookie", "include", "include", ["a=1"]);
+cookies("Include mode: 2 cookies", "include", "include", ["b=2", "c=3"]);
+cookies("Omit mode: discard cookies", "omit", "omit", ["d=4"]);
+cookies("Omit mode: no cookie is stored", "omit", "include", ["e=5"]);
+cookies("Omit mode: no cookie is sent", "include", "omit", ["f=6"]);
+cookies("Same-origin mode: 1 cookie", "same-origin", "same-origin", ["a=1"]);
+cookies("Same-origin mode: 2 cookies", "same-origin", "same-origin", ["b=2", "c=3"]);
diff --git a/test/wpt/tests/fetch/api/headers/header-setcookie.any.js b/test/wpt/tests/fetch/api/headers/header-setcookie.any.js
new file mode 100644
index 0000000..cafb780
--- /dev/null
+++ b/test/wpt/tests/fetch/api/headers/header-setcookie.any.js
@@ -0,0 +1,266 @@
+// META: title=Headers set-cookie special cases
+// META: global=window,worker
+
+const headerList = [
+ ["set-cookie", "foo=bar"],
+ ["Set-Cookie", "fizz=buzz; domain=example.com"],
+];
+
+const setCookie2HeaderList = [
+ ["set-cookie2", "foo2=bar2"],
+ ["Set-Cookie2", "fizz2=buzz2; domain=example2.com"],
+];
+
+function assert_nested_array_equals(actual, expected) {
+ assert_equals(actual.length, expected.length, "Array length is not equal");
+ for (let i = 0; i < expected.length; i++) {
+ assert_array_equals(actual[i], expected[i]);
+ }
+}
+
+test(function () {
+ const headers = new Headers(headerList);
+ assert_equals(
+ headers.get("set-cookie"),
+ "foo=bar, fizz=buzz; domain=example.com",
+ );
+}, "Headers.prototype.get combines set-cookie headers in order");
+
+test(function () {
+ const headers = new Headers(headerList);
+ const list = [...headers];
+ assert_nested_array_equals(list, [
+ ["set-cookie", "foo=bar"],
+ ["set-cookie", "fizz=buzz; domain=example.com"],
+ ]);
+}, "Headers iterator does not combine set-cookie headers");
+
+test(function () {
+ const headers = new Headers(setCookie2HeaderList);
+ const list = [...headers];
+ assert_nested_array_equals(list, [
+ ["set-cookie2", "foo2=bar2, fizz2=buzz2; domain=example2.com"],
+ ]);
+}, "Headers iterator does not special case set-cookie2 headers");
+
+test(function () {
+ const headers = new Headers([...headerList, ...setCookie2HeaderList]);
+ const list = [...headers];
+ assert_nested_array_equals(list, [
+ ["set-cookie", "foo=bar"],
+ ["set-cookie", "fizz=buzz; domain=example.com"],
+ ["set-cookie2", "foo2=bar2, fizz2=buzz2; domain=example2.com"],
+ ]);
+}, "Headers iterator does not combine set-cookie & set-cookie2 headers");
+
+test(function () {
+ // Values are in non alphabetic order, and the iterator should yield in the
+ // headers in the exact order of the input.
+ const headers = new Headers([
+ ["set-cookie", "z=z"],
+ ["set-cookie", "a=a"],
+ ["set-cookie", "n=n"],
+ ]);
+ const list = [...headers];
+ assert_nested_array_equals(list, [
+ ["set-cookie", "z=z"],
+ ["set-cookie", "a=a"],
+ ["set-cookie", "n=n"],
+ ]);
+}, "Headers iterator preserves set-cookie ordering");
+
+test(
+ function () {
+ const headers = new Headers([
+ ["xylophone-header", "1"],
+ ["best-header", "2"],
+ ["set-cookie", "3"],
+ ["a-cool-header", "4"],
+ ["set-cookie", "5"],
+ ["a-cool-header", "6"],
+ ["best-header", "7"],
+ ]);
+ const list = [...headers];
+ assert_nested_array_equals(list, [
+ ["a-cool-header", "4, 6"],
+ ["best-header", "2, 7"],
+ ["set-cookie", "3"],
+ ["set-cookie", "5"],
+ ["xylophone-header", "1"],
+ ]);
+ },
+ "Headers iterator preserves per header ordering, but sorts keys alphabetically",
+);
+
+test(
+ function () {
+ const headers = new Headers([
+ ["xylophone-header", "7"],
+ ["best-header", "6"],
+ ["set-cookie", "5"],
+ ["a-cool-header", "4"],
+ ["set-cookie", "3"],
+ ["a-cool-header", "2"],
+ ["best-header", "1"],
+ ]);
+ const list = [...headers];
+ assert_nested_array_equals(list, [
+ ["a-cool-header", "4, 2"],
+ ["best-header", "6, 1"],
+ ["set-cookie", "5"],
+ ["set-cookie", "3"],
+ ["xylophone-header", "7"],
+ ]);
+ },
+ "Headers iterator preserves per header ordering, but sorts keys alphabetically (and ignores value ordering)",
+);
+
+test(function () {
+ const headers = new Headers([["fizz", "buzz"], ["X-Header", "test"]]);
+ const iterator = headers[Symbol.iterator]();
+ assert_array_equals(iterator.next().value, ["fizz", "buzz"]);
+ headers.append("Set-Cookie", "a=b");
+ assert_array_equals(iterator.next().value, ["set-cookie", "a=b"]);
+ headers.append("Accept", "text/html");
+ assert_array_equals(iterator.next().value, ["set-cookie", "a=b"]);
+ assert_array_equals(iterator.next().value, ["x-header", "test"]);
+ headers.append("set-cookie", "c=d");
+ assert_array_equals(iterator.next().value, ["x-header", "test"]);
+ assert_true(iterator.next().done);
+}, "Headers iterator is correctly updated with set-cookie changes");
+
+test(function () {
+ const headers = new Headers([
+ ["set-cookie", "a"],
+ ["set-cookie", "b"],
+ ["set-cookie", "c"]
+ ]);
+ const iterator = headers[Symbol.iterator]();
+ assert_array_equals(iterator.next().value, ["set-cookie", "a"]);
+ headers.delete("set-cookie");
+ headers.append("set-cookie", "d");
+ headers.append("set-cookie", "e");
+ headers.append("set-cookie", "f");
+ assert_array_equals(iterator.next().value, ["set-cookie", "e"]);
+ assert_array_equals(iterator.next().value, ["set-cookie", "f"]);
+ assert_true(iterator.next().done);
+}, "Headers iterator is correctly updated with set-cookie changes #2");
+
+test(function () {
+ const headers = new Headers(headerList);
+ assert_true(headers.has("sEt-cOoKiE"));
+}, "Headers.prototype.has works for set-cookie");
+
+test(function () {
+ const headers = new Headers(setCookie2HeaderList);
+ headers.append("set-Cookie", "foo=bar");
+ headers.append("sEt-cOoKiE", "fizz=buzz");
+ const list = [...headers];
+ assert_nested_array_equals(list, [
+ ["set-cookie", "foo=bar"],
+ ["set-cookie", "fizz=buzz"],
+ ["set-cookie2", "foo2=bar2, fizz2=buzz2; domain=example2.com"],
+ ]);
+}, "Headers.prototype.append works for set-cookie");
+
+test(function () {
+ const headers = new Headers(headerList);
+ headers.set("set-cookie", "foo2=bar2");
+ const list = [...headers];
+ assert_nested_array_equals(list, [
+ ["set-cookie", "foo2=bar2"],
+ ]);
+}, "Headers.prototype.set works for set-cookie");
+
+test(function () {
+ const headers = new Headers(headerList);
+ headers.delete("set-Cookie");
+ const list = [...headers];
+ assert_nested_array_equals(list, []);
+}, "Headers.prototype.delete works for set-cookie");
+
+test(function () {
+ const headers = new Headers();
+ assert_array_equals(headers.getSetCookie(), []);
+}, "Headers.prototype.getSetCookie with no headers present");
+
+test(function () {
+ const headers = new Headers([headerList[0]]);
+ assert_array_equals(headers.getSetCookie(), ["foo=bar"]);
+}, "Headers.prototype.getSetCookie with one header");
+
+test(function () {
+ const headers = new Headers({ "Set-Cookie": "foo=bar" });
+ assert_array_equals(headers.getSetCookie(), ["foo=bar"]);
+}, "Headers.prototype.getSetCookie with one header created from an object");
+
+test(function () {
+ const headers = new Headers(headerList);
+ assert_array_equals(headers.getSetCookie(), [
+ "foo=bar",
+ "fizz=buzz; domain=example.com",
+ ]);
+}, "Headers.prototype.getSetCookie with multiple headers");
+
+test(function () {
+ const headers = new Headers([["set-cookie", ""]]);
+ assert_array_equals(headers.getSetCookie(), [""]);
+}, "Headers.prototype.getSetCookie with an empty header");
+
+test(function () {
+ const headers = new Headers([["set-cookie", "x"], ["set-cookie", "x"]]);
+ assert_array_equals(headers.getSetCookie(), ["x", "x"]);
+}, "Headers.prototype.getSetCookie with two equal headers");
+
+test(function () {
+ const headers = new Headers([
+ ["set-cookie2", "x"],
+ ["set-cookie", "y"],
+ ["set-cookie2", "z"],
+ ]);
+ assert_array_equals(headers.getSetCookie(), ["y"]);
+}, "Headers.prototype.getSetCookie ignores set-cookie2 headers");
+
+test(function () {
+ // Values are in non alphabetic order, and the iterator should yield in the
+ // headers in the exact order of the input.
+ const headers = new Headers([
+ ["set-cookie", "z=z"],
+ ["set-cookie", "a=a"],
+ ["set-cookie", "n=n"],
+ ]);
+ assert_array_equals(headers.getSetCookie(), ["z=z", "a=a", "n=n"]);
+}, "Headers.prototype.getSetCookie preserves header ordering");
+
+test(function () {
+ const headers = new Headers({"Set-Cookie": " a=b\n"});
+ headers.append("set-cookie", "\n\rc=d ");
+ assert_nested_array_equals([...headers], [
+ ["set-cookie", "a=b"],
+ ["set-cookie", "c=d"]
+ ]);
+ headers.set("set-cookie", "\te=f ");
+ assert_nested_array_equals([...headers], [["set-cookie", "e=f"]]);
+}, "Adding Set-Cookie headers normalizes their value");
+
+test(function () {
+ assert_throws_js(TypeError, () => {
+ new Headers({"set-cookie": "\0"});
+ });
+
+ const headers = new Headers();
+ assert_throws_js(TypeError, () => {
+ headers.append("Set-Cookie", "a\nb");
+ });
+ assert_throws_js(TypeError, () => {
+ headers.set("Set-Cookie", "a\rb");
+ });
+}, "Adding invalid Set-Cookie headers throws");
+
+test(function () {
+ const response = new Response();
+ response.headers.append("Set-Cookie", "foo=bar");
+ assert_array_equals(response.headers.getSetCookie(), []);
+ response.headers.append("sEt-cOokIe", "bar=baz");
+ assert_array_equals(response.headers.getSetCookie(), []);
+}, "Set-Cookie is a forbidden response header");
diff --git a/test/wpt/tests/fetch/api/headers/header-values-normalize.any.js b/test/wpt/tests/fetch/api/headers/header-values-normalize.any.js
new file mode 100644
index 0000000..5710554
--- /dev/null
+++ b/test/wpt/tests/fetch/api/headers/header-values-normalize.any.js
@@ -0,0 +1,72 @@
+// META: title=Header value normalizing test
+// META: global=window,worker
+// META: timeout=long
+
+"use strict";
+
+for(let i = 0; i < 0x21; i++) {
+ let fail = false,
+ strip = false
+
+ // REMOVE 0x0B/0x0C exception once https://github.com/web-platform-tests/wpt/issues/8372 is fixed
+ if(i === 0x0B || i === 0x0C)
+ continue
+
+ if(i === 0) {
+ fail = true
+ }
+
+ if(i === 0x09 || i === 0x0A || i === 0x0D || i === 0x20) {
+ strip = true
+ }
+
+ let url = "../resources/inspect-headers.py?headers=val1|val2|val3",
+ val = String.fromCharCode(i),
+ expectedVal = strip ? "" : val,
+ val1 = val,
+ expectedVal1 = expectedVal,
+ val2 = "x" + val,
+ expectedVal2 = "x" + expectedVal,
+ val3 = val + "x",
+ expectedVal3 = expectedVal + "x"
+
+ // XMLHttpRequest is not available in service workers
+ if (!self.GLOBAL.isWorker()) {
+ async_test((t) => {
+ let xhr = new XMLHttpRequest()
+ xhr.open("POST", url)
+ if(fail) {
+ assert_throws_dom("SyntaxError", () => xhr.setRequestHeader("val1", val1))
+ assert_throws_dom("SyntaxError", () => xhr.setRequestHeader("val2", val2))
+ assert_throws_dom("SyntaxError", () => xhr.setRequestHeader("val3", val3))
+ t.done()
+ } else {
+ xhr.setRequestHeader("val1", val1)
+ xhr.setRequestHeader("val2", val2)
+ xhr.setRequestHeader("val3", val3)
+ xhr.onload = t.step_func_done(() => {
+ assert_equals(xhr.getResponseHeader("x-request-val1"), expectedVal1)
+ assert_equals(xhr.getResponseHeader("x-request-val2"), expectedVal2)
+ assert_equals(xhr.getResponseHeader("x-request-val3"), expectedVal3)
+ })
+ xhr.send()
+ }
+ }, "XMLHttpRequest with value " + encodeURI(val))
+ }
+
+ promise_test((t) => {
+ if(fail) {
+ return Promise.all([
+ promise_rejects_js(t, TypeError, fetch(url, { headers: {"val1": val1} })),
+ promise_rejects_js(t, TypeError, fetch(url, { headers: {"val2": val2} })),
+ promise_rejects_js(t, TypeError, fetch(url, { headers: {"val3": val3} }))
+ ])
+ } else {
+ return fetch(url, { headers: {"val1": val1, "val2": val2, "val3": val3} }).then((res) => {
+ assert_equals(res.headers.get("x-request-val1"), expectedVal1)
+ assert_equals(res.headers.get("x-request-val2"), expectedVal2)
+ assert_equals(res.headers.get("x-request-val3"), expectedVal3)
+ })
+ }
+ }, "fetch() with value " + encodeURI(val))
+}
diff --git a/test/wpt/tests/fetch/api/headers/header-values.any.js b/test/wpt/tests/fetch/api/headers/header-values.any.js
new file mode 100644
index 0000000..bb7570c
--- /dev/null
+++ b/test/wpt/tests/fetch/api/headers/header-values.any.js
@@ -0,0 +1,63 @@
+// META: title=Header value test
+// META: global=window,worker
+// META: timeout=long
+
+"use strict";
+
+// Invalid values
+[0, 0x0A, 0x0D].forEach(val => {
+ val = "x" + String.fromCharCode(val) + "x"
+
+ // XMLHttpRequest is not available in service workers
+ if (!self.GLOBAL.isWorker()) {
+ test(() => {
+ let xhr = new XMLHttpRequest()
+ xhr.open("POST", "/")
+ assert_throws_dom("SyntaxError", () => xhr.setRequestHeader("value-test", val))
+ }, "XMLHttpRequest with value " + encodeURI(val) + " needs to throw")
+ }
+
+ promise_test(t => promise_rejects_js(t, TypeError, fetch("/", { headers: {"value-test": val} })), "fetch() with value " + encodeURI(val) + " needs to throw")
+})
+
+// Valid values
+let headerValues =[]
+for(let i = 0; i < 0x100; i++) {
+ if(i === 0 || i === 0x0A || i === 0x0D) {
+ continue
+ }
+ headerValues.push("x" + String.fromCharCode(i) + "x")
+}
+var url = "../resources/inspect-headers.py?headers="
+headerValues.forEach((_, i) => {
+ url += "val" + i + "|"
+})
+
+// XMLHttpRequest is not available in service workers
+if (!self.GLOBAL.isWorker()) {
+ async_test((t) => {
+ let xhr = new XMLHttpRequest()
+ xhr.open("POST", url)
+ headerValues.forEach((val, i) => {
+ xhr.setRequestHeader("val" + i, val)
+ })
+ xhr.onload = t.step_func_done(() => {
+ headerValues.forEach((val, i) => {
+ assert_equals(xhr.getResponseHeader("x-request-val" + i), val)
+ })
+ })
+ xhr.send()
+ }, "XMLHttpRequest with all valid values")
+}
+
+promise_test((t) => {
+ const headers = new Headers
+ headerValues.forEach((val, i) => {
+ headers.append("val" + i, val)
+ })
+ return fetch(url, { headers }).then((res) => {
+ headerValues.forEach((val, i) => {
+ assert_equals(res.headers.get("x-request-val" + i), val)
+ })
+ })
+}, "fetch() with all valid values")
diff --git a/test/wpt/tests/fetch/api/headers/headers-basic.any.js b/test/wpt/tests/fetch/api/headers/headers-basic.any.js
new file mode 100644
index 0000000..ead1047
--- /dev/null
+++ b/test/wpt/tests/fetch/api/headers/headers-basic.any.js
@@ -0,0 +1,275 @@
+// META: title=Headers structure
+// META: global=window,worker
+
+"use strict";
+
+test(function() {
+ new Headers();
+}, "Create headers from no parameter");
+
+test(function() {
+ new Headers(undefined);
+}, "Create headers from undefined parameter");
+
+test(function() {
+ new Headers({});
+}, "Create headers from empty object");
+
+var parameters = [null, 1];
+parameters.forEach(function(parameter) {
+ test(function() {
+ assert_throws_js(TypeError, function() { new Headers(parameter) });
+ }, "Create headers with " + parameter + " should throw");
+});
+
+var headerDict = {"name1": "value1",
+ "name2": "value2",
+ "name3": "value3",
+ "name4": null,
+ "name5": undefined,
+ "name6": 1,
+ "Content-Type": "value4"
+};
+
+var headerSeq = [];
+for (var name in headerDict)
+ headerSeq.push([name, headerDict[name]]);
+
+test(function() {
+ var headers = new Headers(headerSeq);
+ for (name in headerDict) {
+ assert_equals(headers.get(name), String(headerDict[name]),
+ "name: " + name + " has value: " + headerDict[name]);
+ }
+ assert_equals(headers.get("length"), null, "init should be treated as a sequence, not as a dictionary");
+}, "Create headers with sequence");
+
+test(function() {
+ var headers = new Headers(headerDict);
+ for (name in headerDict) {
+ assert_equals(headers.get(name), String(headerDict[name]),
+ "name: " + name + " has value: " + headerDict[name]);
+ }
+}, "Create headers with record");
+
+test(function() {
+ var headers = new Headers(headerDict);
+ var headers2 = new Headers(headers);
+ for (name in headerDict) {
+ assert_equals(headers2.get(name), String(headerDict[name]),
+ "name: " + name + " has value: " + headerDict[name]);
+ }
+}, "Create headers with existing headers");
+
+test(function() {
+ var headers = new Headers()
+ headers[Symbol.iterator] = function *() {
+ yield ["test", "test"]
+ }
+ var headers2 = new Headers(headers)
+ assert_equals(headers2.get("test"), "test")
+}, "Create headers with existing headers with custom iterator");
+
+test(function() {
+ var headers = new Headers();
+ for (name in headerDict) {
+ headers.append(name, headerDict[name]);
+ assert_equals(headers.get(name), String(headerDict[name]),
+ "name: " + name + " has value: " + headerDict[name]);
+ }
+}, "Check append method");
+
+test(function() {
+ var headers = new Headers();
+ for (name in headerDict) {
+ headers.set(name, headerDict[name]);
+ assert_equals(headers.get(name), String(headerDict[name]),
+ "name: " + name + " has value: " + headerDict[name]);
+ }
+}, "Check set method");
+
+test(function() {
+ var headers = new Headers(headerDict);
+ for (name in headerDict)
+ assert_true(headers.has(name),"headers has name " + name);
+
+ assert_false(headers.has("nameNotInHeaders"),"headers do not have header: nameNotInHeaders");
+}, "Check has method");
+
+test(function() {
+ var headers = new Headers(headerDict);
+ for (name in headerDict) {
+ assert_true(headers.has(name),"headers have a header: " + name);
+ headers.delete(name)
+ assert_true(!headers.has(name),"headers do not have anymore a header: " + name);
+ }
+}, "Check delete method");
+
+test(function() {
+ var headers = new Headers(headerDict);
+ for (name in headerDict)
+ assert_equals(headers.get(name), String(headerDict[name]),
+ "name: " + name + " has value: " + headerDict[name]);
+
+ assert_equals(headers.get("nameNotInHeaders"), null, "header: nameNotInHeaders has no value");
+}, "Check get method");
+
+var headerEntriesDict = {"name1": "value1",
+ "Name2": "value2",
+ "name": "value3",
+ "content-Type": "value4",
+ "Content-Typ": "value5",
+ "Content-Types": "value6"
+};
+var sortedHeaderDict = {};
+var headerValues = [];
+var sortedHeaderKeys = Object.keys(headerEntriesDict).map(function(value) {
+ sortedHeaderDict[value.toLowerCase()] = headerEntriesDict[value];
+ headerValues.push(headerEntriesDict[value]);
+ return value.toLowerCase();
+}).sort();
+
+var iteratorPrototype = Object.getPrototypeOf(Object.getPrototypeOf([][Symbol.iterator]()));
+function checkIteratorProperties(iterator) {
+ var prototype = Object.getPrototypeOf(iterator);
+ assert_equals(Object.getPrototypeOf(prototype), iteratorPrototype);
+
+ var descriptor = Object.getOwnPropertyDescriptor(prototype, "next");
+ assert_true(descriptor.configurable, "configurable");
+ assert_true(descriptor.enumerable, "enumerable");
+ assert_true(descriptor.writable, "writable");
+}
+
+test(function() {
+ var headers = new Headers(headerEntriesDict);
+ var actual = headers.keys();
+ checkIteratorProperties(actual);
+
+ sortedHeaderKeys.forEach(function(key) {
+ const entry = actual.next();
+ assert_false(entry.done);
+ assert_equals(entry.value, key);
+ });
+ assert_true(actual.next().done);
+ assert_true(actual.next().done);
+
+ for (const key of headers.keys())
+ assert_true(sortedHeaderKeys.indexOf(key) != -1);
+}, "Check keys method");
+
+test(function() {
+ var headers = new Headers(headerEntriesDict);
+ var actual = headers.values();
+ checkIteratorProperties(actual);
+
+ sortedHeaderKeys.forEach(function(key) {
+ const entry = actual.next();
+ assert_false(entry.done);
+ assert_equals(entry.value, sortedHeaderDict[key]);
+ });
+ assert_true(actual.next().done);
+ assert_true(actual.next().done);
+
+ for (const value of headers.values())
+ assert_true(headerValues.indexOf(value) != -1);
+}, "Check values method");
+
+test(function() {
+ var headers = new Headers(headerEntriesDict);
+ var actual = headers.entries();
+ checkIteratorProperties(actual);
+
+ sortedHeaderKeys.forEach(function(key) {
+ const entry = actual.next();
+ assert_false(entry.done);
+ assert_equals(entry.value[0], key);
+ assert_equals(entry.value[1], sortedHeaderDict[key]);
+ });
+ assert_true(actual.next().done);
+ assert_true(actual.next().done);
+
+ for (const entry of headers.entries())
+ assert_equals(entry[1], sortedHeaderDict[entry[0]]);
+}, "Check entries method");
+
+test(function() {
+ var headers = new Headers(headerEntriesDict);
+ var actual = headers[Symbol.iterator]();
+
+ sortedHeaderKeys.forEach(function(key) {
+ const entry = actual.next();
+ assert_false(entry.done);
+ assert_equals(entry.value[0], key);
+ assert_equals(entry.value[1], sortedHeaderDict[key]);
+ });
+ assert_true(actual.next().done);
+ assert_true(actual.next().done);
+}, "Check Symbol.iterator method");
+
+test(function() {
+ var headers = new Headers(headerEntriesDict);
+ var reference = sortedHeaderKeys[Symbol.iterator]();
+ headers.forEach(function(value, key, container) {
+ assert_equals(headers, container);
+ const entry = reference.next();
+ assert_false(entry.done);
+ assert_equals(key, entry.value);
+ assert_equals(value, sortedHeaderDict[entry.value]);
+ });
+ assert_true(reference.next().done);
+}, "Check forEach method");
+
+test(() => {
+ const headers = new Headers({"foo": "2", "baz": "1", "BAR": "0"});
+ const actualKeys = [];
+ const actualValues = [];
+ for (const [header, value] of headers) {
+ actualKeys.push(header);
+ actualValues.push(value);
+ headers.delete("foo");
+ }
+ assert_array_equals(actualKeys, ["bar", "baz"]);
+ assert_array_equals(actualValues, ["0", "1"]);
+}, "Iteration skips elements removed while iterating");
+
+test(() => {
+ const headers = new Headers({"foo": "2", "baz": "1", "BAR": "0", "quux": "3"});
+ const actualKeys = [];
+ const actualValues = [];
+ for (const [header, value] of headers) {
+ actualKeys.push(header);
+ actualValues.push(value);
+ if (header === "baz")
+ headers.delete("bar");
+ }
+ assert_array_equals(actualKeys, ["bar", "baz", "quux"]);
+ assert_array_equals(actualValues, ["0", "1", "3"]);
+}, "Removing elements already iterated over causes an element to be skipped during iteration");
+
+test(() => {
+ const headers = new Headers({"foo": "2", "baz": "1", "BAR": "0", "quux": "3"});
+ const actualKeys = [];
+ const actualValues = [];
+ for (const [header, value] of headers) {
+ actualKeys.push(header);
+ actualValues.push(value);
+ if (header === "baz")
+ headers.append("X-yZ", "4");
+ }
+ assert_array_equals(actualKeys, ["bar", "baz", "foo", "quux", "x-yz"]);
+ assert_array_equals(actualValues, ["0", "1", "2", "3", "4"]);
+}, "Appending a value pair during iteration causes it to be reached during iteration");
+
+test(() => {
+ const headers = new Headers({"foo": "2", "baz": "1", "BAR": "0", "quux": "3"});
+ const actualKeys = [];
+ const actualValues = [];
+ for (const [header, value] of headers) {
+ actualKeys.push(header);
+ actualValues.push(value);
+ if (header === "baz")
+ headers.append("abc", "-1");
+ }
+ assert_array_equals(actualKeys, ["bar", "baz", "baz", "foo", "quux"]);
+ assert_array_equals(actualValues, ["0", "1", "1", "2", "3"]);
+}, "Prepending a value pair before the current element position causes it to be skipped during iteration and adds the current element a second time");
diff --git a/test/wpt/tests/fetch/api/headers/headers-casing.any.js b/test/wpt/tests/fetch/api/headers/headers-casing.any.js
new file mode 100644
index 0000000..20b8a9d
--- /dev/null
+++ b/test/wpt/tests/fetch/api/headers/headers-casing.any.js
@@ -0,0 +1,54 @@
+// META: title=Headers case management
+// META: global=window,worker
+
+"use strict";
+
+var headerDictCase = {"UPPERCASE": "value1",
+ "lowercase": "value2",
+ "mixedCase": "value3",
+ "Content-TYPE": "value4"
+ };
+
+function checkHeadersCase(originalName, headersToCheck, expectedDict) {
+ var lowCaseName = originalName.toLowerCase();
+ var upCaseName = originalName.toUpperCase();
+ var expectedValue = expectedDict[originalName];
+ assert_equals(headersToCheck.get(originalName), expectedValue,
+ "name: " + originalName + " has value: " + expectedValue);
+ assert_equals(headersToCheck.get(lowCaseName), expectedValue,
+ "name: " + lowCaseName + " has value: " + expectedValue);
+ assert_equals(headersToCheck.get(upCaseName), expectedValue,
+ "name: " + upCaseName + " has value: " + expectedValue);
+}
+
+test(function() {
+ var headers = new Headers(headerDictCase);
+ for (const name in headerDictCase)
+ checkHeadersCase(name, headers, headerDictCase)
+}, "Create headers, names use characters with different case");
+
+test(function() {
+ var headers = new Headers();
+ for (const name in headerDictCase) {
+ headers.append(name, headerDictCase[name]);
+ checkHeadersCase(name, headers, headerDictCase);
+ }
+}, "Check append method, names use characters with different case");
+
+test(function() {
+ var headers = new Headers();
+ for (const name in headerDictCase) {
+ headers.set(name, headerDictCase[name]);
+ checkHeadersCase(name, headers, headerDictCase);
+ }
+}, "Check set method, names use characters with different case");
+
+test(function() {
+ var headers = new Headers();
+ for (const name in headerDictCase)
+ headers.set(name, headerDictCase[name]);
+ for (const name in headerDictCase)
+ headers.delete(name.toLowerCase());
+ for (const name in headerDictCase)
+ assert_false(headers.has(name), "header " + name + " should have been deleted");
+}, "Check delete method, names use characters with different case");
diff --git a/test/wpt/tests/fetch/api/headers/headers-combine.any.js b/test/wpt/tests/fetch/api/headers/headers-combine.any.js
new file mode 100644
index 0000000..4f3b6d1
--- /dev/null
+++ b/test/wpt/tests/fetch/api/headers/headers-combine.any.js
@@ -0,0 +1,66 @@
+// META: title=Headers have combined (and sorted) values
+// META: global=window,worker
+
+"use strict";
+
+var headerSeqCombine = [["single", "singleValue"],
+ ["double", "doubleValue1"],
+ ["double", "doubleValue2"],
+ ["triple", "tripleValue1"],
+ ["triple", "tripleValue2"],
+ ["triple", "tripleValue3"]
+];
+var expectedDict = {"single": "singleValue",
+ "double": "doubleValue1, doubleValue2",
+ "triple": "tripleValue1, tripleValue2, tripleValue3"
+};
+
+test(function() {
+ var headers = new Headers(headerSeqCombine);
+ for (const name in expectedDict)
+ assert_equals(headers.get(name), expectedDict[name]);
+}, "Create headers using same name for different values");
+
+test(function() {
+ var headers = new Headers(headerSeqCombine);
+ for (const name in expectedDict) {
+ assert_true(headers.has(name), "name: " + name + " has value(s)");
+ headers.delete(name);
+ assert_false(headers.has(name), "name: " + name + " has no value(s) anymore");
+ }
+}, "Check delete and has methods when using same name for different values");
+
+test(function() {
+ var headers = new Headers(headerSeqCombine);
+ for (const name in expectedDict) {
+ headers.set(name,"newSingleValue");
+ assert_equals(headers.get(name), "newSingleValue", "name: " + name + " has value: newSingleValue");
+ }
+}, "Check set methods when called with already used name");
+
+test(function() {
+ var headers = new Headers(headerSeqCombine);
+ for (const name in expectedDict) {
+ var value = headers.get(name);
+ headers.append(name,"newSingleValue");
+ assert_equals(headers.get(name), (value + ", " + "newSingleValue"));
+ }
+}, "Check append methods when called with already used name");
+
+test(() => {
+ const headers = new Headers([["1", "a"],["1", "b"]]);
+ for(let header of headers) {
+ assert_array_equals(header, ["1", "a, b"]);
+ }
+}, "Iterate combined values");
+
+test(() => {
+ const headers = new Headers([["2", "a"], ["1", "b"], ["2", "b"]]),
+ expected = [["1", "b"], ["2", "a, b"]];
+ let i = 0;
+ for(let header of headers) {
+ assert_array_equals(header, expected[i]);
+ i++;
+ }
+ assert_equals(i, 2);
+}, "Iterate combined values in sorted order")
diff --git a/test/wpt/tests/fetch/api/headers/headers-errors.any.js b/test/wpt/tests/fetch/api/headers/headers-errors.any.js
new file mode 100644
index 0000000..82dadd8
--- /dev/null
+++ b/test/wpt/tests/fetch/api/headers/headers-errors.any.js
@@ -0,0 +1,96 @@
+// META: title=Headers errors
+// META: global=window,worker
+
+"use strict";
+
+test(function() {
+ assert_throws_js(TypeError, function() { new Headers([["name"]]); });
+}, "Create headers giving an array having one string as init argument");
+
+test(function() {
+ assert_throws_js(TypeError, function() { new Headers([["invalid", "invalidValue1", "invalidValue2"]]); });
+}, "Create headers giving an array having three strings as init argument");
+
+test(function() {
+ assert_throws_js(TypeError, function() { new Headers([["invalidĀ", "Value1"]]); });
+}, "Create headers giving bad header name as init argument");
+
+test(function() {
+ assert_throws_js(TypeError, function() { new Headers([["name", "invalidValueĀ"]]); });
+}, "Create headers giving bad header value as init argument");
+
+var badNames = ["invalidĀ", {}];
+var badValues = ["invalidĀ"];
+
+badNames.forEach(function(name) {
+ test(function() {
+ var headers = new Headers();
+ assert_throws_js(TypeError, function() { headers.get(name); });
+ }, "Check headers get with an invalid name " + name);
+});
+
+badNames.forEach(function(name) {
+ test(function() {
+ var headers = new Headers();
+ assert_throws_js(TypeError, function() { headers.delete(name); });
+ }, "Check headers delete with an invalid name " + name);
+});
+
+badNames.forEach(function(name) {
+ test(function() {
+ var headers = new Headers();
+ assert_throws_js(TypeError, function() { headers.has(name); });
+ }, "Check headers has with an invalid name " + name);
+});
+
+badNames.forEach(function(name) {
+ test(function() {
+ var headers = new Headers();
+ assert_throws_js(TypeError, function() { headers.set(name, "Value1"); });
+ }, "Check headers set with an invalid name " + name);
+});
+
+badValues.forEach(function(value) {
+ test(function() {
+ var headers = new Headers();
+ assert_throws_js(TypeError, function() { headers.set("name", value); });
+ }, "Check headers set with an invalid value " + value);
+});
+
+badNames.forEach(function(name) {
+ test(function() {
+ var headers = new Headers();
+ assert_throws_js(TypeError, function() { headers.append("invalidĀ", "Value1"); });
+ }, "Check headers append with an invalid name " + name);
+});
+
+badValues.forEach(function(value) {
+ test(function() {
+ var headers = new Headers();
+ assert_throws_js(TypeError, function() { headers.append("name", value); });
+ }, "Check headers append with an invalid value " + value);
+});
+
+test(function() {
+ var headers = new Headers([["name", "value"]]);
+ assert_throws_js(TypeError, function() { headers.forEach(); });
+ assert_throws_js(TypeError, function() { headers.forEach(undefined); });
+ assert_throws_js(TypeError, function() { headers.forEach(1); });
+}, "Headers forEach throws if argument is not callable");
+
+test(function() {
+ var headers = new Headers([["name1", "value1"], ["name2", "value2"], ["name3", "value3"]]);
+ var counter = 0;
+ try {
+ headers.forEach(function(value, name) {
+ counter++;
+ if (name == "name2")
+ throw "error";
+ });
+ } catch (e) {
+ assert_equals(counter, 2);
+ assert_equals(e, "error");
+ return;
+ }
+ assert_unreached();
+}, "Headers forEach loop should stop if callback is throwing exception");
diff --git a/test/wpt/tests/fetch/api/headers/headers-no-cors.any.js b/test/wpt/tests/fetch/api/headers/headers-no-cors.any.js
new file mode 100644
index 0000000..60dbb9e
--- /dev/null
+++ b/test/wpt/tests/fetch/api/headers/headers-no-cors.any.js
@@ -0,0 +1,59 @@
+// META: global=window,worker
+
+"use strict";
+
+promise_test(() => fetch("../cors/resources/not-cors-safelisted.json").then(res => res.json().then(runTests)), "Loading data…");
+
+const longValue = "s".repeat(127);
+
+[
+ {
+ "headers": ["accept", "accept-language", "content-language"],
+ "values": [longValue, "", longValue]
+ },
+ {
+ "headers": ["accept", "accept-language", "content-language"],
+ "values": ["", longValue]
+ },
+ {
+ "headers": ["content-type"],
+ "values": ["text/plain;" + "s".repeat(116), "text/plain"]
+ }
+].forEach(testItem => {
+ testItem.headers.forEach(header => {
+ test(() => {
+ const noCorsHeaders = new Request("about:blank", { mode: "no-cors" }).headers;
+ testItem.values.forEach((value) => {
+ noCorsHeaders.append(header, value);
+ assert_equals(noCorsHeaders.get(header), testItem.values[0], '1');
+ });
+ noCorsHeaders.set(header, testItem.values.join(", "));
+ assert_equals(noCorsHeaders.get(header), testItem.values[0], '2');
+ noCorsHeaders.delete(header);
+ assert_false(noCorsHeaders.has(header));
+ }, "\"no-cors\" Headers object cannot have " + header + " set to " + testItem.values.join(", "));
+ });
+});
+
+function runTests(testArray) {
+ testArray = testArray.concat([
+ ["dpr", "2"],
+ ["rtt", "1.0"],
+ ["downlink", "-1.0"],
+ ["ect", "6g"],
+ ["save-data", "on"],
+ ["viewport-width", "100"],
+ ["width", "100"],
+ ["unknown", "doesitmatter"]
+ ]);
+ testArray.forEach(testItem => {
+ const [headerName, headerValue] = testItem;
+ test(() => {
+ const noCorsHeaders = new Request("about:blank", { mode: "no-cors" }).headers;
+ noCorsHeaders.append(headerName, headerValue);
+ assert_false(noCorsHeaders.has(headerName));
+ noCorsHeaders.set(headerName, headerValue);
+ assert_false(noCorsHeaders.has(headerName));
+ }, "\"no-cors\" Headers object cannot have " + headerName + "/" + headerValue + " as header");
+ });
+}
diff --git a/test/wpt/tests/fetch/api/headers/headers-normalize.any.js b/test/wpt/tests/fetch/api/headers/headers-normalize.any.js
new file mode 100644
index 0000000..68cf5b8
--- /dev/null
+++ b/test/wpt/tests/fetch/api/headers/headers-normalize.any.js
@@ -0,0 +1,56 @@
+// META: title=Headers normalize values
+// META: global=window,worker
+
+"use strict";
+
+const expectations = {
+ "name1": [" space ", "space"],
+ "name2": ["\ttab\t", "tab"],
+ "name3": [" spaceAndTab\t", "spaceAndTab"],
+ "name4": ["\r\n newLine", "newLine"], //obs-fold cases
+ "name5": ["newLine\r\n ", "newLine"],
+ "name6": ["\r\n\tnewLine", "newLine"],
+ "name7": ["\t\f\tnewLine\n", "\f\tnewLine"],
+ "name8": ["newLine\xa0", "newLine\xa0"], // \xa0 == non breaking space
+};
+
+test(function () {
+ const headerDict = Object.fromEntries(
+ Object.entries(expectations).map(([name, [actual]]) => [name, actual]),
+ );
+ var headers = new Headers(headerDict);
+ for (const name in expectations) {
+ const expected = expectations[name][1];
+ assert_equals(
+ headers.get(name),
+ expected,
+ "name: " + name + " has normalized value: " + expected,
+ );
+ }
+}, "Create headers with not normalized values");
+
+test(function () {
+ var headers = new Headers();
+ for (const name in expectations) {
+ headers.append(name, expectations[name][0]);
+ const expected = expectations[name][1];
+ assert_equals(
+ headers.get(name),
+ expected,
+ "name: " + name + " has value: " + expected,
+ );
+ }
+}, "Check append method with not normalized values");
+
+test(function () {
+ var headers = new Headers();
+ for (const name in expectations) {
+ headers.set(name, expectations[name][0]);
+ const expected = expectations[name][1];
+ assert_equals(
+ headers.get(name),
+ expected,
+ "name: " + name + " has value: " + expected,
+ );
+ }
+}, "Check set method with not normalized values");
diff --git a/test/wpt/tests/fetch/api/headers/headers-record.any.js b/test/wpt/tests/fetch/api/headers/headers-record.any.js
new file mode 100644
index 0000000..fa85391
--- /dev/null
+++ b/test/wpt/tests/fetch/api/headers/headers-record.any.js
@@ -0,0 +1,357 @@
+// META: global=window,worker
+
+"use strict";
+
+var log = [];
+function clearLog() {
+ log = [];
+}
+function addLogEntry(name, args) {
+ log.push([ name, ...args ]);
+}
+
+var loggingHandler = {
+};
+
+setup(function() {
+ for (let prop of Object.getOwnPropertyNames(Reflect)) {
+ loggingHandler[prop] = function(...args) {
+ addLogEntry(prop, args);
+ return Reflect[prop](...args);
+ }
+ }
+});
+
+test(function() {
+ var h = new Headers();
+ assert_equals([...h].length, 0);
+}, "Passing nothing to Headers constructor");
+
+test(function() {
+ var h = new Headers(undefined);
+ assert_equals([...h].length, 0);
+}, "Passing undefined to Headers constructor");
+
+test(function() {
+ assert_throws_js(TypeError, function() {
+ var h = new Headers(null);
+ });
+}, "Passing null to Headers constructor");
+
+test(function() {
+ this.add_cleanup(clearLog);
+ var record = { a: "b" };
+ var proxy = new Proxy(record, loggingHandler);
+ var h = new Headers(proxy);
+
+ assert_equals(log.length, 4);
+ // The first thing is the [[Get]] of Symbol.iterator to figure out whether
+ // we're a sequence, during overload resolution.
+ assert_array_equals(log[0], ["get", record, Symbol.iterator, proxy]);
+ // Then we have the [[OwnPropertyKeys]] from
+ // https://webidl.spec.whatwg.org/#es-to-record step 4.
+ assert_array_equals(log[1], ["ownKeys", record]);
+ // Then the [[GetOwnProperty]] from step 5.1.
+ assert_array_equals(log[2], ["getOwnPropertyDescriptor", record, "a"]);
+ // Then the [[Get]] from step 5.2.
+ assert_array_equals(log[3], ["get", record, "a", proxy]);
+
+ // Check the results.
+ assert_equals([...h].length, 1);
+ assert_array_equals([...h.keys()], ["a"]);
+ assert_true(h.has("a"));
+ assert_equals(h.get("a"), "b");
+}, "Basic operation with one property");
+
+test(function() {
+ this.add_cleanup(clearLog);
+ var recordProto = { c: "d" };
+ var record = Object.create(recordProto, { a: { value: "b", enumerable: true } });
+ var proxy = new Proxy(record, loggingHandler);
+ var h = new Headers(proxy);
+
+ assert_equals(log.length, 4);
+ // The first thing is the [[Get]] of Symbol.iterator to figure out whether
+ // we're a sequence, during overload resolution.
+ assert_array_equals(log[0], ["get", record, Symbol.iterator, proxy]);
+ // Then we have the [[OwnPropertyKeys]] from
+ // https://webidl.spec.whatwg.org/#es-to-record step 4.
+ assert_array_equals(log[1], ["ownKeys", record]);
+ // Then the [[GetOwnProperty]] from step 5.1.
+ assert_array_equals(log[2], ["getOwnPropertyDescriptor", record, "a"]);
+ // Then the [[Get]] from step 5.2.
+ assert_array_equals(log[3], ["get", record, "a", proxy]);
+
+ // Check the results.
+ assert_equals([...h].length, 1);
+ assert_array_equals([...h.keys()], ["a"]);
+ assert_true(h.has("a"));
+ assert_equals(h.get("a"), "b");
+}, "Basic operation with one property and a proto");
+
+test(function() {
+ this.add_cleanup(clearLog);
+ var record = { a: "b", c: "d" };
+ var proxy = new Proxy(record, loggingHandler);
+ var h = new Headers(proxy);
+
+ assert_equals(log.length, 6);
+ // The first thing is the [[Get]] of Symbol.iterator to figure out whether
+ // we're a sequence, during overload resolution.
+ assert_array_equals(log[0], ["get", record, Symbol.iterator, proxy]);
+ // Then we have the [[OwnPropertyKeys]] from
+ // https://webidl.spec.whatwg.org/#es-to-record step 4.
+ assert_array_equals(log[1], ["ownKeys", record]);
+ // Then the [[GetOwnProperty]] from step 5.1.
+ assert_array_equals(log[2], ["getOwnPropertyDescriptor", record, "a"]);
+ // Then the [[Get]] from step 5.2.
+ assert_array_equals(log[3], ["get", record, "a", proxy]);
+ // Then the second [[GetOwnProperty]] from step 5.1.
+ assert_array_equals(log[4], ["getOwnPropertyDescriptor", record, "c"]);
+ // Then the second [[Get]] from step 5.2.
+ assert_array_equals(log[5], ["get", record, "c", proxy]);
+
+ // Check the results.
+ assert_equals([...h].length, 2);
+ assert_array_equals([...h.keys()], ["a", "c"]);
+ assert_true(h.has("a"));
+ assert_equals(h.get("a"), "b");
+ assert_true(h.has("c"));
+ assert_equals(h.get("c"), "d");
+}, "Correct operation ordering with two properties");
+
+test(function() {
+ this.add_cleanup(clearLog);
+ var record = { a: "b", "\uFFFF": "d" };
+ var proxy = new Proxy(record, loggingHandler);
+ assert_throws_js(TypeError, function() {
+ var h = new Headers(proxy);
+ });
+
+ assert_equals(log.length, 5);
+ // The first thing is the [[Get]] of Symbol.iterator to figure out whether
+ // we're a sequence, during overload resolution.
+ assert_array_equals(log[0], ["get", record, Symbol.iterator, proxy]);
+ // Then we have the [[OwnPropertyKeys]] from
+ // https://webidl.spec.whatwg.org/#es-to-record step 4.
+ assert_array_equals(log[1], ["ownKeys", record]);
+ // Then the [[GetOwnProperty]] from step 5.1.
+ assert_array_equals(log[2], ["getOwnPropertyDescriptor", record, "a"]);
+ // Then the [[Get]] from step 5.2.
+ assert_array_equals(log[3], ["get", record, "a", proxy]);
+ // Then the second [[GetOwnProperty]] from step 5.1.
+ assert_array_equals(log[4], ["getOwnPropertyDescriptor", record, "\uFFFF"]);
+ // The second [[Get]] never happens, because we convert the invalid name to a
+ // ByteString first and throw.
+}, "Correct operation ordering with two properties one of which has an invalid name");
+
+test(function() {
+ this.add_cleanup(clearLog);
+ var record = { a: "\uFFFF", c: "d" }
+ var proxy = new Proxy(record, loggingHandler);
+ assert_throws_js(TypeError, function() {
+ var h = new Headers(proxy);
+ });
+
+ assert_equals(log.length, 4);
+ // The first thing is the [[Get]] of Symbol.iterator to figure out whether
+ // we're a sequence, during overload resolution.
+ assert_array_equals(log[0], ["get", record, Symbol.iterator, proxy]);
+ // Then we have the [[OwnPropertyKeys]] from
+ // https://webidl.spec.whatwg.org/#es-to-record step 4.
+ assert_array_equals(log[1], ["ownKeys", record]);
+ // Then the [[GetOwnProperty]] from step 5.1.
+ assert_array_equals(log[2], ["getOwnPropertyDescriptor", record, "a"]);
+ // Then the [[Get]] from step 5.2.
+ assert_array_equals(log[3], ["get", record, "a", proxy]);
+ // Nothing else after this, because converting the result of that [[Get]] to a
+ // ByteString throws.
+}, "Correct operation ordering with two properties one of which has an invalid value");
+
+test(function() {
+ this.add_cleanup(clearLog);
+ var record = {};
+ Object.defineProperty(record, "a", { value: "b", enumerable: false });
+ Object.defineProperty(record, "c", { value: "d", enumerable: true });
+ Object.defineProperty(record, "e", { value: "f", enumerable: false });
+ var proxy = new Proxy(record, loggingHandler);
+ var h = new Headers(proxy);
+
+ assert_equals(log.length, 6);
+ // The first thing is the [[Get]] of Symbol.iterator to figure out whether
+ // we're a sequence, during overload resolution.
+ assert_array_equals(log[0], ["get", record, Symbol.iterator, proxy]);
+ // Then we have the [[OwnPropertyKeys]] from
+ // https://webidl.spec.whatwg.org/#es-to-record step 4.
+ assert_array_equals(log[1], ["ownKeys", record]);
+ // Then the [[GetOwnProperty]] from step 5.1.
+ assert_array_equals(log[2], ["getOwnPropertyDescriptor", record, "a"]);
+ // No [[Get]] because not enumerable
+ // Then the second [[GetOwnProperty]] from step 5.1.
+ assert_array_equals(log[3], ["getOwnPropertyDescriptor", record, "c"]);
+ // Then the [[Get]] from step 5.2.
+ assert_array_equals(log[4], ["get", record, "c", proxy]);
+ // Then the third [[GetOwnProperty]] from step 5.1.
+ assert_array_equals(log[5], ["getOwnPropertyDescriptor", record, "e"]);
+ // No [[Get]] because not enumerable
+
+ // Check the results.
+ assert_equals([...h].length, 1);
+ assert_array_equals([...h.keys()], ["c"]);
+ assert_true(h.has("c"));
+ assert_equals(h.get("c"), "d");
+}, "Correct operation ordering with non-enumerable properties");
+
+test(function() {
+ this.add_cleanup(clearLog);
+ var record = {a: "b", c: "d", e: "f"};
+ var lyingHandler = {
+ getOwnPropertyDescriptor: function(target, name) {
+ if (name == "a" || name == "e") {
+ return undefined;
+ }
+ return Reflect.getOwnPropertyDescriptor(target, name);
+ }
+ };
+ var lyingProxy = new Proxy(record, lyingHandler);
+ var proxy = new Proxy(lyingProxy, loggingHandler);
+ var h = new Headers(proxy);
+
+ assert_equals(log.length, 6);
+ // The first thing is the [[Get]] of Symbol.iterator to figure out whether
+ // we're a sequence, during overload resolution.
+ assert_array_equals(log[0], ["get", lyingProxy, Symbol.iterator, proxy]);
+ // Then we have the [[OwnPropertyKeys]] from
+ // https://webidl.spec.whatwg.org/#es-to-record step 4.
+ assert_array_equals(log[1], ["ownKeys", lyingProxy]);
+ // Then the [[GetOwnProperty]] from step 5.1.
+ assert_array_equals(log[2], ["getOwnPropertyDescriptor", lyingProxy, "a"]);
+ // No [[Get]] because no descriptor
+ // Then the second [[GetOwnProperty]] from step 5.1.
+ assert_array_equals(log[3], ["getOwnPropertyDescriptor", lyingProxy, "c"]);
+ // Then the [[Get]] from step 5.2.
+ assert_array_equals(log[4], ["get", lyingProxy, "c", proxy]);
+ // Then the third [[GetOwnProperty]] from step 5.1.
+ assert_array_equals(log[5], ["getOwnPropertyDescriptor", lyingProxy, "e"]);
+ // No [[Get]] because no descriptor
+
+ // Check the results.
+ assert_equals([...h].length, 1);
+ assert_array_equals([...h.keys()], ["c"]);
+ assert_true(h.has("c"));
+ assert_equals(h.get("c"), "d");
+}, "Correct operation ordering with undefined descriptors");
+
+test(function() {
+ this.add_cleanup(clearLog);
+ var record = {a: "b", c: "d"};
+ var lyingHandler = {
+ ownKeys: function() {
+ return [ "a", "c", "a", "c" ];
+ },
+ };
+ var lyingProxy = new Proxy(record, lyingHandler);
+ var proxy = new Proxy(lyingProxy, loggingHandler);
+
+ // Returning duplicate keys from ownKeys() throws a TypeError.
+ assert_throws_js(TypeError,
+ function() { var h = new Headers(proxy); });
+
+ assert_equals(log.length, 2);
+ // The first thing is the [[Get]] of Symbol.iterator to figure out whether
+ // we're a sequence, during overload resolution.
+ assert_array_equals(log[0], ["get", lyingProxy, Symbol.iterator, proxy]);
+ // Then we have the [[OwnPropertyKeys]] from
+ // https://webidl.spec.whatwg.org/#es-to-record step 4.
+ assert_array_equals(log[1], ["ownKeys", lyingProxy]);
+}, "Correct operation ordering with repeated keys");
+
+test(function() {
+ this.add_cleanup(clearLog);
+ var record = {
+ a: "b",
+ [Symbol.toStringTag]: {
+ // Make sure the ToString conversion of the value happens
+ // after the ToString conversion of the key.
+ toString: function () { addLogEntry("toString", [this]); return "nope"; }
+ },
+ c: "d" };
+ var proxy = new Proxy(record, loggingHandler);
+ assert_throws_js(TypeError,
+ function() { var h = new Headers(proxy); });
+
+ assert_equals(log.length, 7);
+ // The first thing is the [[Get]] of Symbol.iterator to figure out whether
+ // we're a sequence, during overload resolution.
+ assert_array_equals(log[0], ["get", record, Symbol.iterator, proxy]);
+ // Then we have the [[OwnPropertyKeys]] from
+ // https://webidl.spec.whatwg.org/#es-to-record step 4.
+ assert_array_equals(log[1], ["ownKeys", record]);
+ // Then the [[GetOwnProperty]] from step 5.1.
+ assert_array_equals(log[2], ["getOwnPropertyDescriptor", record, "a"]);
+ // Then the [[Get]] from step 5.2.
+ assert_array_equals(log[3], ["get", record, "a", proxy]);
+ // Then the second [[GetOwnProperty]] from step 5.1.
+ assert_array_equals(log[4], ["getOwnPropertyDescriptor", record, "c"]);
+ // Then the second [[Get]] from step 5.2.
+ assert_array_equals(log[5], ["get", record, "c", proxy]);
+ // Then the third [[GetOwnProperty]] from step 5.1.
+ assert_array_equals(log[6], ["getOwnPropertyDescriptor", record,
+ Symbol.toStringTag]);
+ // Then we throw an exception converting the Symbol to a string, before we do
+ // the third [[Get]].
+}, "Basic operation with Symbol keys");
+
+test(function() {
+ this.add_cleanup(clearLog);
+ var record = {
+ a: {
+ toString: function() { addLogEntry("toString", [this]); return "b"; }
+ },
+ [Symbol.toStringTag]: {
+ toString: function () { addLogEntry("toString", [this]); return "nope"; }
+ },
+ c: {
+ toString: function() { addLogEntry("toString", [this]); return "d"; }
+ }
+ };
+ // Now make that Symbol-named property not enumerable.
+ Object.defineProperty(record, Symbol.toStringTag, { enumerable: false });
+ assert_array_equals(Reflect.ownKeys(record),
+ ["a", "c", Symbol.toStringTag]);
+
+ var proxy = new Proxy(record, loggingHandler);
+ var h = new Headers(proxy);
+
+ assert_equals(log.length, 9);
+ // The first thing is the [[Get]] of Symbol.iterator to figure out whether
+ // we're a sequence, during overload resolution.
+ assert_array_equals(log[0], ["get", record, Symbol.iterator, proxy]);
+ // Then we have the [[OwnPropertyKeys]] from
+ // https://webidl.spec.whatwg.org/#es-to-record step 4.
+ assert_array_equals(log[1], ["ownKeys", record]);
+ // Then the [[GetOwnProperty]] from step 5.1.
+ assert_array_equals(log[2], ["getOwnPropertyDescriptor", record, "a"]);
+ // Then the [[Get]] from step 5.2.
+ assert_array_equals(log[3], ["get", record, "a", proxy]);
+ // Then the ToString on the value.
+ assert_array_equals(log[4], ["toString", record.a]);
+ // Then the second [[GetOwnProperty]] from step 5.1.
+ assert_array_equals(log[5], ["getOwnPropertyDescriptor", record, "c"]);
+ // Then the second [[Get]] from step 5.2.
+ assert_array_equals(log[6], ["get", record, "c", proxy]);
+ // Then the ToString on the value.
+ assert_array_equals(log[7], ["toString", record.c]);
+ // Then the third [[GetOwnProperty]] from step 5.1.
+ assert_array_equals(log[8], ["getOwnPropertyDescriptor", record,
+ Symbol.toStringTag]);
+ // No [[Get]] because not enumerable.
+
+ // Check the results.
+ assert_equals([...h].length, 2);
+ assert_array_equals([...h.keys()], ["a", "c"]);
+ assert_true(h.has("a"));
+ assert_equals(h.get("a"), "b");
+ assert_true(h.has("c"));
+ assert_equals(h.get("c"), "d");
+}, "Operation with non-enumerable Symbol keys");
diff --git a/test/wpt/tests/fetch/api/headers/headers-structure.any.js b/test/wpt/tests/fetch/api/headers/headers-structure.any.js
new file mode 100644
index 0000000..d826bca
--- /dev/null
+++ b/test/wpt/tests/fetch/api/headers/headers-structure.any.js
@@ -0,0 +1,20 @@
+// META: title=Headers basic
+// META: global=window,worker
+
+"use strict";
+
+var headers = new Headers();
+var methods = ["append",
+ "delete",
+ "get",
+ "has",
+ "set",
+ //Headers is iterable
+ "entries",
+ "keys",
+ "values"
+ ];
+for (var idx in methods)
+ test(function() {
+ assert_true(methods[idx] in headers, "headers has " + methods[idx] + " method");
+ }, "Headers has " + methods[idx] + " method");
diff --git a/test/wpt/tests/fetch/api/idlharness.any.js b/test/wpt/tests/fetch/api/idlharness.any.js
new file mode 100644
index 0000000..7b3c694
--- /dev/null
+++ b/test/wpt/tests/fetch/api/idlharness.any.js
@@ -0,0 +1,21 @@
+// META: global=window,worker
+// META: script=/resources/WebIDLParser.js
+// META: script=/resources/idlharness.js
+// META: timeout=long
+
+idl_test(
+ ['fetch'],
+ ['referrer-policy', 'html', 'dom'],
+ idl_array => {
+ idl_array.add_objects({
+ Headers: ["new Headers()"],
+ Request: ["new Request('about:blank')"],
+ Response: ["new Response()"],
+ });
+ if (self.GLOBAL.isWindow()) {
+ idl_array.add_objects({ Window: ['window'] });
+ } else if (self.GLOBAL.isWorker()) {
+ idl_array.add_objects({ WorkerGlobalScope: ['self'] });
+ }
+ }
+);
diff --git a/test/wpt/tests/fetch/api/policies/csp-blocked-worker.html b/test/wpt/tests/fetch/api/policies/csp-blocked-worker.html
new file mode 100644
index 0000000..e8660df
--- /dev/null
+++ b/test/wpt/tests/fetch/api/policies/csp-blocked-worker.html
@@ -0,0 +1,16 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>Fetch in worker: blocked by CSP</title>
+ <meta name="help" href="https://fetch.spec.whatwg.org/#main-fetch">
+ <meta name="author" title="Canon Research France" href="https://www.crf.canon.fr">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ </head>
+ <body>
+ <script>
+ fetch_tests_from_worker(new Worker("csp-blocked.js"));
+ </script>
+ </body>
+</html> \ No newline at end of file
diff --git a/test/wpt/tests/fetch/api/policies/csp-blocked.html b/test/wpt/tests/fetch/api/policies/csp-blocked.html
new file mode 100644
index 0000000..99e90df
--- /dev/null
+++ b/test/wpt/tests/fetch/api/policies/csp-blocked.html
@@ -0,0 +1,15 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>Fetch: blocked by CSP</title>
+ <meta name="help" href="https://fetch.spec.whatwg.org/#main-fetch">
+ <meta name="author" title="Canon Research France" href="https://www.crf.canon.fr">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ </head>
+ <body>
+ <script src="../resources/utils.js"></script>
+ <script src="csp-blocked.js"></script>
+ </body>
+</html> \ No newline at end of file
diff --git a/test/wpt/tests/fetch/api/policies/csp-blocked.html.headers b/test/wpt/tests/fetch/api/policies/csp-blocked.html.headers
new file mode 100644
index 0000000..c8c1e9f
--- /dev/null
+++ b/test/wpt/tests/fetch/api/policies/csp-blocked.html.headers
@@ -0,0 +1 @@
+Content-Security-Policy: connect-src 'none'; \ No newline at end of file
diff --git a/test/wpt/tests/fetch/api/policies/csp-blocked.js b/test/wpt/tests/fetch/api/policies/csp-blocked.js
new file mode 100644
index 0000000..28653ff
--- /dev/null
+++ b/test/wpt/tests/fetch/api/policies/csp-blocked.js
@@ -0,0 +1,13 @@
+if (this.document === undefined) {
+ importScripts("/resources/testharness.js");
+ importScripts("../resources/utils.js");
+}
+
+//Content-Security-Policy: connect-src 'none'; cf .headers file
+cspViolationUrl = RESOURCES_DIR + "top.txt";
+
+promise_test(function(test) {
+ return promise_rejects_js(test, TypeError, fetch(cspViolationUrl));
+}, "Fetch is blocked by CSP, got a TypeError");
+
+done();
diff --git a/test/wpt/tests/fetch/api/policies/csp-blocked.js.headers b/test/wpt/tests/fetch/api/policies/csp-blocked.js.headers
new file mode 100644
index 0000000..c8c1e9f
--- /dev/null
+++ b/test/wpt/tests/fetch/api/policies/csp-blocked.js.headers
@@ -0,0 +1 @@
+Content-Security-Policy: connect-src 'none'; \ No newline at end of file
diff --git a/test/wpt/tests/fetch/api/policies/nested-policy.js b/test/wpt/tests/fetch/api/policies/nested-policy.js
new file mode 100644
index 0000000..b0d1769
--- /dev/null
+++ b/test/wpt/tests/fetch/api/policies/nested-policy.js
@@ -0,0 +1 @@
+// empty, but referrer-policy set on this file
diff --git a/test/wpt/tests/fetch/api/policies/nested-policy.js.headers b/test/wpt/tests/fetch/api/policies/nested-policy.js.headers
new file mode 100644
index 0000000..7ffbf17
--- /dev/null
+++ b/test/wpt/tests/fetch/api/policies/nested-policy.js.headers
@@ -0,0 +1 @@
+Referrer-Policy: no-referrer
diff --git a/test/wpt/tests/fetch/api/policies/referrer-no-referrer-service-worker.https.html b/test/wpt/tests/fetch/api/policies/referrer-no-referrer-service-worker.https.html
new file mode 100644
index 0000000..af898aa
--- /dev/null
+++ b/test/wpt/tests/fetch/api/policies/referrer-no-referrer-service-worker.https.html
@@ -0,0 +1,18 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>Fetch in service worker: referrer with no-referrer policy</title>
+ <meta name="help" href="https://fetch.spec.whatwg.org/#main-fetch">
+ <meta name="help" href="https://fetch.spec.whatwg.org/#http-network-or-cache-fetch">
+ <meta name="author" title="Canon Research France" href="https://www.crf.canon.fr">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script>
+ </head>
+ <body>
+ <script>
+ service_worker_test("referrer-no-referrer.js");
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/fetch/api/policies/referrer-no-referrer-worker.html b/test/wpt/tests/fetch/api/policies/referrer-no-referrer-worker.html
new file mode 100644
index 0000000..dbef9bb
--- /dev/null
+++ b/test/wpt/tests/fetch/api/policies/referrer-no-referrer-worker.html
@@ -0,0 +1,17 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>Fetch in worker: referrer with no-referrer policy</title>
+ <meta name="help" href="https://fetch.spec.whatwg.org/#main-fetch">
+ <meta name="help" href="https://fetch.spec.whatwg.org/#http-network-or-cache-fetch">
+ <meta name="author" title="Canon Research France" href="https://www.crf.canon.fr">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ </head>
+ <body>
+ <script>
+ fetch_tests_from_worker(new Worker("referrer-no-referrer.js"));
+ </script>
+ </body>
+</html> \ No newline at end of file
diff --git a/test/wpt/tests/fetch/api/policies/referrer-no-referrer.html b/test/wpt/tests/fetch/api/policies/referrer-no-referrer.html
new file mode 100644
index 0000000..22a6f34
--- /dev/null
+++ b/test/wpt/tests/fetch/api/policies/referrer-no-referrer.html
@@ -0,0 +1,15 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>Fetch: referrer with no-referrer policy</title>
+ <meta name="help" href="https://fetch.spec.whatwg.org/#main-fetch">
+ <meta name="author" title="Canon Research France" href="https://www.crf.canon.fr">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ </head>
+ <body>
+ <script src="../resources/utils.js"></script>
+ <script src="referrer-no-referrer.js"></script>
+ </body>
+</html> \ No newline at end of file
diff --git a/test/wpt/tests/fetch/api/policies/referrer-no-referrer.html.headers b/test/wpt/tests/fetch/api/policies/referrer-no-referrer.html.headers
new file mode 100644
index 0000000..7ffbf17
--- /dev/null
+++ b/test/wpt/tests/fetch/api/policies/referrer-no-referrer.html.headers
@@ -0,0 +1 @@
+Referrer-Policy: no-referrer
diff --git a/test/wpt/tests/fetch/api/policies/referrer-no-referrer.js b/test/wpt/tests/fetch/api/policies/referrer-no-referrer.js
new file mode 100644
index 0000000..60600bf
--- /dev/null
+++ b/test/wpt/tests/fetch/api/policies/referrer-no-referrer.js
@@ -0,0 +1,19 @@
+if (this.document === undefined) {
+ importScripts("/resources/testharness.js");
+ importScripts("../resources/utils.js");
+}
+
+var fetchedUrl = RESOURCES_DIR + "inspect-headers.py?headers=origin";
+
+promise_test(function(test) {
+ return fetch(fetchedUrl).then(function(resp) {
+ assert_equals(resp.status, 200, "HTTP status is 200");
+ assert_equals(resp.type , "basic", "Response's type is basic");
+ var referrer = resp.headers.get("x-request-referer");
+ //Either no referrer header is sent or it is empty
+ if (referrer)
+ assert_equals(referrer, "", "request's referrer is empty");
+ });
+}, "Request's referrer is empty");
+
+done();
diff --git a/test/wpt/tests/fetch/api/policies/referrer-no-referrer.js.headers b/test/wpt/tests/fetch/api/policies/referrer-no-referrer.js.headers
new file mode 100644
index 0000000..7ffbf17
--- /dev/null
+++ b/test/wpt/tests/fetch/api/policies/referrer-no-referrer.js.headers
@@ -0,0 +1 @@
+Referrer-Policy: no-referrer
diff --git a/test/wpt/tests/fetch/api/policies/referrer-origin-service-worker.https.html b/test/wpt/tests/fetch/api/policies/referrer-origin-service-worker.https.html
new file mode 100644
index 0000000..4018b83
--- /dev/null
+++ b/test/wpt/tests/fetch/api/policies/referrer-origin-service-worker.https.html
@@ -0,0 +1,18 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>Fetch in service worker: referrer with no-referrer policy</title>
+ <meta name="help" href="https://fetch.spec.whatwg.org/#main-fetch">
+ <meta name="help" href="https://fetch.spec.whatwg.org/#http-network-or-cache-fetch">
+ <meta name="author" title="Canon Research France" href="https://www.crf.canon.fr">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script>
+ </head>
+ <body>
+ <script>
+ service_worker_test("referrer-origin.js?pipe=sub");
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/fetch/api/policies/referrer-origin-when-cross-origin-service-worker.https.html b/test/wpt/tests/fetch/api/policies/referrer-origin-when-cross-origin-service-worker.https.html
new file mode 100644
index 0000000..d87192e
--- /dev/null
+++ b/test/wpt/tests/fetch/api/policies/referrer-origin-when-cross-origin-service-worker.https.html
@@ -0,0 +1,17 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>Fetch in service worker: referrer with origin-when-cross-origin policy</title>
+ <meta name="help" href="https://fetch.spec.whatwg.org/#main-fetch">
+ <meta name="help" href="https://fetch.spec.whatwg.org/#http-network-or-cache-fetch">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script>
+ </head>
+ <body>
+ <script>
+ service_worker_test("referrer-origin-when-cross-origin.js?pipe=sub");
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/fetch/api/policies/referrer-origin-when-cross-origin-worker.html b/test/wpt/tests/fetch/api/policies/referrer-origin-when-cross-origin-worker.html
new file mode 100644
index 0000000..f95ae8c
--- /dev/null
+++ b/test/wpt/tests/fetch/api/policies/referrer-origin-when-cross-origin-worker.html
@@ -0,0 +1,16 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>Fetch in worker: referrer with origin-when-cross-origin policy</title>
+ <meta name="help" href="https://fetch.spec.whatwg.org/#main-fetch">
+ <meta name="help" href="https://fetch.spec.whatwg.org/#http-network-or-cache-fetch">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ </head>
+ <body>
+ <script>
+ fetch_tests_from_worker(new Worker("referrer-origin-when-cross-origin.js?pipe=sub"));
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/fetch/api/policies/referrer-origin-when-cross-origin.html b/test/wpt/tests/fetch/api/policies/referrer-origin-when-cross-origin.html
new file mode 100644
index 0000000..5cd79e4
--- /dev/null
+++ b/test/wpt/tests/fetch/api/policies/referrer-origin-when-cross-origin.html
@@ -0,0 +1,16 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>Fetch: referrer with origin-when-cross-origin policy</title>
+ <meta name="help" href="https://fetch.spec.whatwg.org/#main-fetch">
+ <meta name="help" href="https://fetch.spec.whatwg.org/#http-network-or-cache-fetch">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/common/get-host-info.sub.js"></script>
+ </head>
+ <body>
+ <script src="../resources/utils.js"></script>
+ <script src="referrer-origin-when-cross-origin.js?pipe=sub"></script>
+ </body>
+</html>
diff --git a/test/wpt/tests/fetch/api/policies/referrer-origin-when-cross-origin.html.headers b/test/wpt/tests/fetch/api/policies/referrer-origin-when-cross-origin.html.headers
new file mode 100644
index 0000000..ad768e6
--- /dev/null
+++ b/test/wpt/tests/fetch/api/policies/referrer-origin-when-cross-origin.html.headers
@@ -0,0 +1 @@
+Referrer-Policy: origin-when-cross-origin
diff --git a/test/wpt/tests/fetch/api/policies/referrer-origin-when-cross-origin.js b/test/wpt/tests/fetch/api/policies/referrer-origin-when-cross-origin.js
new file mode 100644
index 0000000..0adadbc
--- /dev/null
+++ b/test/wpt/tests/fetch/api/policies/referrer-origin-when-cross-origin.js
@@ -0,0 +1,21 @@
+if (this.document === undefined) {
+ importScripts("/resources/testharness.js");
+ importScripts("../resources/utils.js");
+ importScripts("/common/get-host-info.sub.js");
+
+ // A nested importScripts() with a referrer-policy should have no effect
+ // on overall worker policy.
+ importScripts("nested-policy.js");
+}
+
+var referrerOrigin = location.origin + '/';
+var fetchedUrl = get_host_info().HTTP_REMOTE_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "inspect-headers.py?cors&headers=referer";
+
+promise_test(function(test) {
+ return fetch(fetchedUrl).then(function(resp) {
+ assert_equals(resp.status, 200, "HTTP status is 200");
+ assert_equals(resp.headers.get("x-request-referer"), referrerOrigin, "request's referrer is " + referrerOrigin);
+ });
+}, "Request's referrer is origin");
+
+done();
diff --git a/test/wpt/tests/fetch/api/policies/referrer-origin-when-cross-origin.js.headers b/test/wpt/tests/fetch/api/policies/referrer-origin-when-cross-origin.js.headers
new file mode 100644
index 0000000..ad768e6
--- /dev/null
+++ b/test/wpt/tests/fetch/api/policies/referrer-origin-when-cross-origin.js.headers
@@ -0,0 +1 @@
+Referrer-Policy: origin-when-cross-origin
diff --git a/test/wpt/tests/fetch/api/policies/referrer-origin-worker.html b/test/wpt/tests/fetch/api/policies/referrer-origin-worker.html
new file mode 100644
index 0000000..bb80dd5
--- /dev/null
+++ b/test/wpt/tests/fetch/api/policies/referrer-origin-worker.html
@@ -0,0 +1,17 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>Fetch in worker: referrer with origin policy</title>
+ <meta name="help" href="https://fetch.spec.whatwg.org/#main-fetch">
+ <meta name="help" href="https://fetch.spec.whatwg.org/#http-network-or-cache-fetch">
+ <meta name="author" title="Canon Research France" href="https://www.crf.canon.fr">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ </head>
+ <body>
+ <script>
+ fetch_tests_from_worker(new Worker("referrer-origin.js?pipe=sub"));
+ </script>
+ </body>
+</html> \ No newline at end of file
diff --git a/test/wpt/tests/fetch/api/policies/referrer-origin.html b/test/wpt/tests/fetch/api/policies/referrer-origin.html
new file mode 100644
index 0000000..b164afe
--- /dev/null
+++ b/test/wpt/tests/fetch/api/policies/referrer-origin.html
@@ -0,0 +1,16 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>Fetch: referrer with origin policy</title>
+ <meta name="help" href="https://fetch.spec.whatwg.org/#main-fetch">
+ <meta name="help" href="https://fetch.spec.whatwg.org/#http-network-or-cache-fetch">
+ <meta name="author" title="Canon Research France" href="https://www.crf.canon.fr">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ </head>
+ <body>
+ <script src="../resources/utils.js"></script>
+ <script src="referrer-origin.js?pipe=sub"></script>
+ </body>
+</html>
diff --git a/test/wpt/tests/fetch/api/policies/referrer-origin.html.headers b/test/wpt/tests/fetch/api/policies/referrer-origin.html.headers
new file mode 100644
index 0000000..5b29739
--- /dev/null
+++ b/test/wpt/tests/fetch/api/policies/referrer-origin.html.headers
@@ -0,0 +1 @@
+Referrer-Policy: origin
diff --git a/test/wpt/tests/fetch/api/policies/referrer-origin.js b/test/wpt/tests/fetch/api/policies/referrer-origin.js
new file mode 100644
index 0000000..918f8f2
--- /dev/null
+++ b/test/wpt/tests/fetch/api/policies/referrer-origin.js
@@ -0,0 +1,30 @@
+if (this.document === undefined) {
+ importScripts("/resources/testharness.js");
+ importScripts("../resources/utils.js");
+
+ // A nested importScripts() with a referrer-policy should have no effect
+ // on overall worker policy.
+ importScripts("nested-policy.js");
+}
+
+var referrerOrigin = (new URL("/", location.href)).href;
+var fetchedUrl = RESOURCES_DIR + "inspect-headers.py?headers=referer";
+
+promise_test(function(test) {
+ return fetch(fetchedUrl).then(function(resp) {
+ assert_equals(resp.status, 200, "HTTP status is 200");
+ assert_equals(resp.type , "basic", "Response's type is basic");
+ assert_equals(resp.headers.get("x-request-referer"), referrerOrigin, "request's referrer is " + referrerOrigin);
+ });
+}, "Request's referrer is origin");
+
+promise_test(function(test) {
+ var referrerUrl = "https://{{domains[www]}}:{{ports[https][0]}}/";
+ return fetch(fetchedUrl, { "referrer": referrerUrl }).then(function(resp) {
+ assert_equals(resp.status, 200, "HTTP status is 200");
+ assert_equals(resp.type , "basic", "Response's type is basic");
+ assert_equals(resp.headers.get("x-request-referer"), referrerOrigin, "request's referrer is " + referrerOrigin);
+ });
+}, "Cross-origin referrer is overridden by client origin");
+
+done();
diff --git a/test/wpt/tests/fetch/api/policies/referrer-origin.js.headers b/test/wpt/tests/fetch/api/policies/referrer-origin.js.headers
new file mode 100644
index 0000000..5b29739
--- /dev/null
+++ b/test/wpt/tests/fetch/api/policies/referrer-origin.js.headers
@@ -0,0 +1 @@
+Referrer-Policy: origin
diff --git a/test/wpt/tests/fetch/api/policies/referrer-unsafe-url-service-worker.https.html b/test/wpt/tests/fetch/api/policies/referrer-unsafe-url-service-worker.https.html
new file mode 100644
index 0000000..634877e
--- /dev/null
+++ b/test/wpt/tests/fetch/api/policies/referrer-unsafe-url-service-worker.https.html
@@ -0,0 +1,18 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>Fetch in worker: referrer with unsafe-url policy</title>
+ <meta name="help" href="https://fetch.spec.whatwg.org/#main-fetch">
+ <meta name="help" href="https://fetch.spec.whatwg.org/#http-network-or-cache-fetch">
+ <meta name="author" title="Canon Research France" href="https://www.crf.canon.fr">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script>
+ </head>
+ <body>
+ <script>
+ service_worker_test("referrer-unsafe-url.js");
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/fetch/api/policies/referrer-unsafe-url-worker.html b/test/wpt/tests/fetch/api/policies/referrer-unsafe-url-worker.html
new file mode 100644
index 0000000..4204577
--- /dev/null
+++ b/test/wpt/tests/fetch/api/policies/referrer-unsafe-url-worker.html
@@ -0,0 +1,17 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>Fetch in worker: referrer with unsafe-url policy</title>
+ <meta name="help" href="https://fetch.spec.whatwg.org/#main-fetch">
+ <meta name="help" href="https://fetch.spec.whatwg.org/#http-network-or-cache-fetch">
+ <meta name="author" title="Canon Research France" href="https://www.crf.canon.fr">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ </head>
+ <body>
+ <script>
+ fetch_tests_from_worker(new Worker("referrer-unsafe-url.js"));
+ </script>
+ </body>
+</html> \ No newline at end of file
diff --git a/test/wpt/tests/fetch/api/policies/referrer-unsafe-url.html b/test/wpt/tests/fetch/api/policies/referrer-unsafe-url.html
new file mode 100644
index 0000000..10dd79e
--- /dev/null
+++ b/test/wpt/tests/fetch/api/policies/referrer-unsafe-url.html
@@ -0,0 +1,16 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>Fetch: referrer with unsafe-url policy</title>
+ <meta name="help" href="https://fetch.spec.whatwg.org/#main-fetch">
+ <meta name="help" href="https://fetch.spec.whatwg.org/#http-network-or-cache-fetch">
+ <meta name="author" title="Canon Research France" href="https://www.crf.canon.fr">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ </head>
+ <body>
+ <script src="../resources/utils.js"></script>
+ <script src="referrer-unsafe-url.js"></script>
+ </body>
+</html> \ No newline at end of file
diff --git a/test/wpt/tests/fetch/api/policies/referrer-unsafe-url.html.headers b/test/wpt/tests/fetch/api/policies/referrer-unsafe-url.html.headers
new file mode 100644
index 0000000..8e23770
--- /dev/null
+++ b/test/wpt/tests/fetch/api/policies/referrer-unsafe-url.html.headers
@@ -0,0 +1 @@
+Referrer-Policy: unsafe-url
diff --git a/test/wpt/tests/fetch/api/policies/referrer-unsafe-url.js b/test/wpt/tests/fetch/api/policies/referrer-unsafe-url.js
new file mode 100644
index 0000000..4d61172
--- /dev/null
+++ b/test/wpt/tests/fetch/api/policies/referrer-unsafe-url.js
@@ -0,0 +1,21 @@
+if (this.document === undefined) {
+ importScripts("/resources/testharness.js");
+ importScripts("../resources/utils.js");
+
+ // A nested importScripts() with a referrer-policy should have no effect
+ // on overall worker policy.
+ importScripts("nested-policy.js");
+}
+
+var referrerUrl = location.href;
+var fetchedUrl = RESOURCES_DIR + "inspect-headers.py?headers=referer";
+
+promise_test(function(test) {
+ return fetch(fetchedUrl).then(function(resp) {
+ assert_equals(resp.status, 200, "HTTP status is 200");
+ assert_equals(resp.type , "basic", "Response's type is basic");
+ assert_equals(resp.headers.get("x-request-referer"), referrerUrl, "request's referrer is " + referrerUrl);
+ });
+}, "Request's referrer is the full url of current document/worker");
+
+done();
diff --git a/test/wpt/tests/fetch/api/policies/referrer-unsafe-url.js.headers b/test/wpt/tests/fetch/api/policies/referrer-unsafe-url.js.headers
new file mode 100644
index 0000000..8e23770
--- /dev/null
+++ b/test/wpt/tests/fetch/api/policies/referrer-unsafe-url.js.headers
@@ -0,0 +1 @@
+Referrer-Policy: unsafe-url
diff --git a/test/wpt/tests/fetch/api/redirect/redirect-back-to-original-origin.any.js b/test/wpt/tests/fetch/api/redirect/redirect-back-to-original-origin.any.js
new file mode 100644
index 0000000..74d731f
--- /dev/null
+++ b/test/wpt/tests/fetch/api/redirect/redirect-back-to-original-origin.any.js
@@ -0,0 +1,38 @@
+// META: global=window,worker
+// META: script=/common/get-host-info.sub.js
+
+const BASE = location.href;
+const IS_HTTPS = new URL(BASE).protocol === 'https:';
+const REMOTE_HOST = get_host_info()['REMOTE_HOST'];
+const REMOTE_PORT =
+ IS_HTTPS ? get_host_info()['HTTPS_PORT'] : get_host_info()['HTTP_PORT'];
+
+const REMOTE_ORIGIN =
+ new URL(`//${REMOTE_HOST}:${REMOTE_PORT}`, BASE).origin;
+const DESTINATION = new URL('../resources/cors-top.txt', BASE);
+
+function CreateURL(url, BASE, params) {
+ const u = new URL(url, BASE);
+ for (const {name, value} of params) {
+ u.searchParams.append(name, value);
+ }
+ return u;
+}
+
+const redirect =
+ CreateURL('/fetch/api/resources/redirect.py', REMOTE_ORIGIN,
+ [{name: 'redirect_status', value: 303},
+ {name: 'location', value: DESTINATION.href}]);
+
+promise_test(async (test) => {
+ const res = await fetch(redirect.href, {mode: 'no-cors'});
+ // This is discussed at https://github.com/whatwg/fetch/issues/737.
+ assert_equals(res.type, 'opaque');
+}, 'original => remote => original with mode: "no-cors"');
+
+promise_test(async (test) => {
+ const res = await fetch(redirect.href, {mode: 'cors'});
+ assert_equals(res.type, 'cors');
+}, 'original => remote => original with mode: "cors"');
+
+done();
diff --git a/test/wpt/tests/fetch/api/redirect/redirect-count.any.js b/test/wpt/tests/fetch/api/redirect/redirect-count.any.js
new file mode 100644
index 0000000..420f9c0
--- /dev/null
+++ b/test/wpt/tests/fetch/api/redirect/redirect-count.any.js
@@ -0,0 +1,51 @@
+// META: global=window,worker
+// META: script=../resources/utils.js
+// META: script=/common/utils.js
+// META: timeout=long
+
+/**
+ * Fetches a target that returns response with HTTP status code `statusCode` to
+ * redirect `maxCount` times.
+ */
+function redirectCountTest(maxCount, {statusCode, shouldPass = true} = {}) {
+ const desc = `Redirect ${statusCode} ${maxCount} times`;
+
+ const fromUrl = `${RESOURCES_DIR}redirect.py`;
+ const toUrl = fromUrl;
+ const token1 = token();
+ const url = `${fromUrl}?token=${token1}` +
+ `&max_age=0` +
+ `&redirect_status=${statusCode}` +
+ `&max_count=${maxCount}` +
+ `&location=${encodeURIComponent(toUrl)}`;
+
+ const requestInit = {'redirect': 'follow'};
+
+ promise_test((test) => {
+ return fetch(`${RESOURCES_DIR}clean-stash.py?token=${token1}`)
+ .then((resp) => {
+ assert_equals(
+ resp.status, 200, 'Clean stash response\'s status is 200');
+
+ if (!shouldPass)
+ return promise_rejects_js(test, TypeError, fetch(url, requestInit));
+
+ return fetch(url, requestInit)
+ .then((resp) => {
+ assert_equals(resp.status, 200, 'Response\'s status is 200');
+ return resp.text();
+ })
+ .then((body) => {
+ assert_equals(
+ body, maxCount.toString(), `Redirected ${maxCount} times`);
+ });
+ });
+ }, desc);
+}
+
+for (const statusCode of [301, 302, 303, 307, 308]) {
+ redirectCountTest(20, {statusCode});
+ redirectCountTest(21, {statusCode, shouldPass: false});
+}
+
+done();
diff --git a/test/wpt/tests/fetch/api/redirect/redirect-empty-location.any.js b/test/wpt/tests/fetch/api/redirect/redirect-empty-location.any.js
new file mode 100644
index 0000000..487f4d4
--- /dev/null
+++ b/test/wpt/tests/fetch/api/redirect/redirect-empty-location.any.js
@@ -0,0 +1,21 @@
+// META: global=window,worker
+// META: script=../resources/utils.js
+
+// Tests receiving a redirect response with a Location header with an empty
+// value.
+
+const url = RESOURCES_DIR + 'redirect-empty-location.py';
+
+promise_test(t => {
+ return promise_rejects_js(t, TypeError, fetch(url, {redirect:'follow'}));
+}, 'redirect response with empty Location, follow mode');
+
+promise_test(t => {
+ return fetch(url, {redirect:'manual'})
+ .then(resp => {
+ assert_equals(resp.type, 'opaqueredirect');
+ assert_equals(resp.status, 0);
+ });
+}, 'redirect response with empty Location, manual mode');
+
+done();
diff --git a/test/wpt/tests/fetch/api/redirect/redirect-keepalive.any.js b/test/wpt/tests/fetch/api/redirect/redirect-keepalive.any.js
new file mode 100644
index 0000000..bcfc444
--- /dev/null
+++ b/test/wpt/tests/fetch/api/redirect/redirect-keepalive.any.js
@@ -0,0 +1,94 @@
+// 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,
+ HTTP_REMOTE_ORIGIN,
+ 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(
+ 'same-origin redirect + preflight', {withPreflight: true});
+keepaliveRedirectInUnloadTest('cross-origin redirect', {
+ origin1: HTTP_REMOTE_ORIGIN,
+ origin2: HTTP_REMOTE_ORIGIN_WITH_DIFFERENT_PORT
+});
+keepaliveRedirectInUnloadTest('cross-origin redirect + preflight', {
+ origin1: HTTP_REMOTE_ORIGIN,
+ origin2: HTTP_REMOTE_ORIGIN_WITH_DIFFERENT_PORT,
+ withPreflight: true
+});
+keepaliveRedirectInUnloadTest(
+ 'redirect to file URL', {url2: 'file://tmp/bar.txt', shouldPass: false});
+keepaliveRedirectInUnloadTest(
+ 'redirect to data URL',
+ {url2: 'data:text/plain;base64,cmVzcG9uc2UncyBib2R5', shouldPass: false});
diff --git a/test/wpt/tests/fetch/api/redirect/redirect-location-escape.tentative.any.js b/test/wpt/tests/fetch/api/redirect/redirect-location-escape.tentative.any.js
new file mode 100644
index 0000000..779ad70
--- /dev/null
+++ b/test/wpt/tests/fetch/api/redirect/redirect-location-escape.tentative.any.js
@@ -0,0 +1,46 @@
+// META: global=window,worker
+// META: script=../resources/utils.js
+
+// See https://github.com/whatwg/fetch/issues/883 for the behavior covered by
+// this test. As of writing, the Fetch spec has not been updated to cover these.
+
+// redirectLocation tests that a Location header of |locationHeader| is resolved
+// to a URL which ends in |expectedUrlSuffix|. |locationHeader| is interpreted
+// as a byte sequence via isomorphic encode, as described in [INFRA]. This
+// allows the caller to specify byte sequences which are not valid UTF-8.
+// However, this means, e.g., U+2603 must be passed in as "\xe2\x98\x83", its
+// UTF-8 encoding, not "\u2603".
+//
+// [INFRA] https://infra.spec.whatwg.org/#isomorphic-encode
+function redirectLocation(
+ desc, redirectUrl, locationHeader, expectedUrlSuffix) {
+ promise_test(function(test) {
+ // Note we use escape() instead of encodeURIComponent(), so that characters
+ // are escaped as bytes in the isomorphic encoding.
+ var url = redirectUrl + '?simple=1&location=' + escape(locationHeader);
+
+ return fetch(url, {'redirect': 'follow'}).then(function(resp) {
+ assert_true(
+ resp.url.endsWith(expectedUrlSuffix),
+ resp.url + ' ends with ' + expectedUrlSuffix);
+ });
+ }, desc);
+}
+
+var redirUrl = RESOURCES_DIR + 'redirect.py';
+redirectLocation(
+ 'Redirect to escaped UTF-8', redirUrl, 'top.txt?%E2%98%83%e2%98%83',
+ 'top.txt?%E2%98%83%e2%98%83');
+redirectLocation(
+ 'Redirect to unescaped UTF-8', redirUrl, 'top.txt?\xe2\x98\x83',
+ 'top.txt?%E2%98%83');
+redirectLocation(
+ 'Redirect to escaped and unescaped UTF-8', redirUrl,
+ 'top.txt?\xe2\x98\x83%e2%98%83', 'top.txt?%E2%98%83%e2%98%83');
+redirectLocation(
+ 'Escaping produces double-percent', redirUrl, 'top.txt?%\xe2\x98\x83',
+ 'top.txt?%%E2%98%83');
+redirectLocation(
+ 'Redirect to invalid UTF-8', redirUrl, 'top.txt?\xff', 'top.txt?%FF');
+
+done();
diff --git a/test/wpt/tests/fetch/api/redirect/redirect-location.any.js b/test/wpt/tests/fetch/api/redirect/redirect-location.any.js
new file mode 100644
index 0000000..3d483bd
--- /dev/null
+++ b/test/wpt/tests/fetch/api/redirect/redirect-location.any.js
@@ -0,0 +1,73 @@
+// META: global=window,worker
+// META: script=../resources/utils.js
+
+const VALID_URL = 'top.txt';
+const INVALID_URL = 'invalidurl:';
+const DATA_URL = 'data:text/plain;base64,cmVzcG9uc2UncyBib2R5';
+
+/**
+ * A test to fetch a URL that returns response redirecting to `toUrl` with
+ * `status` as its HTTP status code. `expectStatus` can be set to test the
+ * status code in fetch's Promise response.
+ */
+function redirectLocationTest(toUrlDesc, {
+ toUrl = undefined,
+ status,
+ expectStatus = undefined,
+ mode,
+ shouldPass = true
+} = {}) {
+ toUrlDesc = toUrl ? `with ${toUrlDesc}` : `without`;
+ const desc = `Redirect ${status} in "${mode}" mode ${toUrlDesc} location`;
+ const url = `${RESOURCES_DIR}redirect.py?redirect_status=${status}` +
+ (toUrl ? `&location=${encodeURIComponent(toUrl)}` : '');
+ const requestInit = {'redirect': mode};
+ if (!expectStatus)
+ expectStatus = status;
+
+ promise_test((test) => {
+ if (mode === 'error' || !shouldPass)
+ return promise_rejects_js(test, TypeError, fetch(url, requestInit));
+ if (mode === 'manual')
+ return fetch(url, requestInit).then((resp) => {
+ assert_equals(resp.status, 0, "Response's status is 0");
+ assert_equals(resp.type, "opaqueredirect", "Response's type is opaqueredirect");
+ assert_equals(resp.statusText, '', `Response's statusText is ""`);
+ assert_true(resp.headers.entries().next().done, "Headers should be empty");
+ });
+
+ if (mode === 'follow')
+ return fetch(url, requestInit).then((resp) => {
+ assert_equals(
+ resp.status, expectStatus, `Response's status is ${expectStatus}`);
+ });
+ assert_unreached(`${mode} is not a valid redirect mode`);
+ }, desc);
+}
+
+// FIXME: We may want to mix redirect-mode and cors-mode.
+for (const status of [301, 302, 303, 307, 308]) {
+ redirectLocationTest('without location', {status, mode: 'follow'});
+ redirectLocationTest('without location', {status, mode: 'manual'});
+ // FIXME: Add tests for "error" redirect-mode without location.
+
+ // When succeeded, `follow` mode should have followed all redirects.
+ redirectLocationTest(
+ 'valid', {toUrl: VALID_URL, status, expectStatus: 200, mode: 'follow'});
+ redirectLocationTest('valid', {toUrl: VALID_URL, status, mode: 'manual'});
+ redirectLocationTest('valid', {toUrl: VALID_URL, status, mode: 'error'});
+
+ redirectLocationTest(
+ 'invalid',
+ {toUrl: INVALID_URL, status, mode: 'follow', shouldPass: false});
+ redirectLocationTest('invalid', {toUrl: INVALID_URL, status, mode: 'manual'});
+ redirectLocationTest('invalid', {toUrl: INVALID_URL, status, mode: 'error'});
+
+ redirectLocationTest(
+ 'data', {toUrl: DATA_URL, status, mode: 'follow', shouldPass: false});
+ // FIXME: Should this pass?
+ redirectLocationTest('data', {toUrl: DATA_URL, status, mode: 'manual'});
+ redirectLocationTest('data', {toUrl: DATA_URL, status, mode: 'error'});
+}
+
+done();
diff --git a/test/wpt/tests/fetch/api/redirect/redirect-method.any.js b/test/wpt/tests/fetch/api/redirect/redirect-method.any.js
new file mode 100644
index 0000000..9fe086a
--- /dev/null
+++ b/test/wpt/tests/fetch/api/redirect/redirect-method.any.js
@@ -0,0 +1,112 @@
+// META: global=window,worker
+// META: script=../resources/utils.js
+
+// Creates a promise_test that fetches a URL that returns a redirect response.
+//
+// |opts| has additional options:
+// |opts.body|: the request body as a string or blob (default is empty body)
+// |opts.expectedBodyAsString|: the expected response body as a string. The
+// server is expected to echo the request body. The default is the empty string
+// if the request after redirection isn't POST; otherwise it's |opts.body|.
+// |opts.expectedRequestContentType|: the expected Content-Type of redirected
+// request.
+function redirectMethod(desc, redirectUrl, redirectLocation, redirectStatus, method, expectedMethod, opts) {
+ let url = redirectUrl;
+ let urlParameters = "?redirect_status=" + redirectStatus;
+ urlParameters += "&location=" + encodeURIComponent(redirectLocation);
+
+ let requestHeaders = {
+ "Content-Encoding": "Identity",
+ "Content-Language": "en-US",
+ "Content-Location": "foo",
+ };
+ let requestInit = {"method": method, "redirect": "follow", "headers" : requestHeaders};
+ opts = opts || {};
+ if (opts.body) {
+ requestInit.body = opts.body;
+ }
+
+ promise_test(function(test) {
+ return fetch(url + urlParameters, requestInit).then(function(resp) {
+ let expectedRequestContentType = "NO";
+ if (opts.expectedRequestContentType) {
+ expectedRequestContentType = opts.expectedRequestContentType;
+ }
+
+ assert_equals(resp.status, 200, "Response's status is 200");
+ assert_equals(resp.type, "basic", "Response's type basic");
+ assert_equals(
+ resp.headers.get("x-request-method"),
+ expectedMethod,
+ "Request method after redirection is " + expectedMethod);
+ let hasRequestBodyHeader = true;
+ if (opts.expectedStripRequestBodyHeader) {
+ hasRequestBodyHeader = !opts.expectedStripRequestBodyHeader;
+ }
+ assert_equals(
+ resp.headers.get("x-request-content-type"),
+ expectedRequestContentType,
+ "Request Content-Type after redirection is " + expectedRequestContentType);
+ [
+ "Content-Encoding",
+ "Content-Language",
+ "Content-Location"
+ ].forEach(header => {
+ let xHeader = "x-request-" + header.toLowerCase();
+ let expectedValue = hasRequestBodyHeader ? requestHeaders[header] : "NO";
+ assert_equals(
+ resp.headers.get(xHeader),
+ expectedValue,
+ "Request " + header + " after redirection is " + expectedValue);
+ });
+ assert_true(resp.redirected);
+ return resp.text().then(function(text) {
+ let expectedBody = "";
+ if (expectedMethod == "POST") {
+ expectedBody = opts.expectedBodyAsString || requestInit.body;
+ }
+ let expectedContentLength = expectedBody ? expectedBody.length.toString() : "NO";
+ assert_equals(text, expectedBody, "request body");
+ assert_equals(
+ resp.headers.get("x-request-content-length"),
+ expectedContentLength,
+ "Request Content-Length after redirection is " + expectedContentLength);
+ });
+ });
+ }, desc);
+}
+
+promise_test(function(test) {
+ assert_false(new Response().redirected);
+ return fetch(RESOURCES_DIR + "method.py").then(function(resp) {
+ assert_equals(resp.status, 200, "Response's status is 200");
+ assert_false(resp.redirected);
+ });
+}, "Response.redirected should be false on not-redirected responses");
+
+var redirUrl = RESOURCES_DIR + "redirect.py";
+var locationUrl = "method.py";
+
+const stringBody = "this is my body";
+const blobBody = new Blob(["it's me the blob!", " ", "and more blob!"]);
+const blobBodyAsString = "it's me the blob! and more blob!";
+
+redirectMethod("Redirect 301 with GET", redirUrl, locationUrl, 301, "GET", "GET");
+redirectMethod("Redirect 301 with POST", redirUrl, locationUrl, 301, "POST", "GET", { body: stringBody, expectedStripRequestBodyHeader: true });
+redirectMethod("Redirect 301 with HEAD", redirUrl, locationUrl, 301, "HEAD", "HEAD");
+
+redirectMethod("Redirect 302 with GET", redirUrl, locationUrl, 302, "GET", "GET");
+redirectMethod("Redirect 302 with POST", redirUrl, locationUrl, 302, "POST", "GET", { body: stringBody, expectedStripRequestBodyHeader: true });
+redirectMethod("Redirect 302 with HEAD", redirUrl, locationUrl, 302, "HEAD", "HEAD");
+
+redirectMethod("Redirect 303 with GET", redirUrl, locationUrl, 303, "GET", "GET");
+redirectMethod("Redirect 303 with POST", redirUrl, locationUrl, 303, "POST", "GET", { body: stringBody, expectedStripRequestBodyHeader: true });
+redirectMethod("Redirect 303 with HEAD", redirUrl, locationUrl, 303, "HEAD", "HEAD");
+redirectMethod("Redirect 303 with TESTING", redirUrl, locationUrl, 303, "TESTING", "GET", { expectedStripRequestBodyHeader: true });
+
+redirectMethod("Redirect 307 with GET", redirUrl, locationUrl, 307, "GET", "GET");
+redirectMethod("Redirect 307 with POST (string body)", redirUrl, locationUrl, 307, "POST", "POST", { body: stringBody , expectedRequestContentType: "text/plain;charset=UTF-8"});
+redirectMethod("Redirect 307 with POST (blob body)", redirUrl, locationUrl, 307, "POST", "POST", { body: blobBody, expectedBodyAsString: blobBodyAsString });
+redirectMethod("Redirect 307 with HEAD", redirUrl, locationUrl, 307, "HEAD", "HEAD");
+
+done();
diff --git a/test/wpt/tests/fetch/api/redirect/redirect-mode.any.js b/test/wpt/tests/fetch/api/redirect/redirect-mode.any.js
new file mode 100644
index 0000000..9f1ff98
--- /dev/null
+++ b/test/wpt/tests/fetch/api/redirect/redirect-mode.any.js
@@ -0,0 +1,59 @@
+// META: script=/common/get-host-info.sub.js
+
+var redirectLocation = "cors-top.txt";
+const { ORIGIN, REMOTE_ORIGIN } = get_host_info();
+
+function testRedirect(origin, redirectStatus, redirectMode, corsMode) {
+ var url = new URL("../resources/redirect.py", self.location);
+ if (origin === "cross-origin") {
+ url.host = get_host_info().REMOTE_HOST;
+ url.port = get_host_info().HTTP_PORT;
+ }
+
+ var urlParameters = "?redirect_status=" + redirectStatus;
+ urlParameters += "&location=" + encodeURIComponent(redirectLocation);
+
+ var requestInit = {redirect: redirectMode, mode: corsMode};
+
+ promise_test(function(test) {
+ if (redirectMode === "error" ||
+ (corsMode === "no-cors" && redirectMode !== "follow" && origin !== "same-origin"))
+ return promise_rejects_js(test, TypeError, fetch(url + urlParameters, requestInit));
+ if (redirectMode === "manual")
+ return fetch(url + urlParameters, requestInit).then(function(resp) {
+ assert_equals(resp.status, 0, "Response's status is 0");
+ assert_equals(resp.type, "opaqueredirect", "Response's type is opaqueredirect");
+ assert_equals(resp.statusText, "", "Response's statusText is \"\"");
+ assert_equals(resp.url, url + urlParameters, "Response URL should be the original one");
+ });
+ if (redirectMode === "follow")
+ return fetch(url + urlParameters, requestInit).then(function(resp) {
+ if (corsMode !== "no-cors" || origin === "same-origin") {
+ assert_true(new URL(resp.url).pathname.endsWith(redirectLocation), "Response's url should be the redirected one");
+ assert_equals(resp.status, 200, "Response's status is 200");
+ } else {
+ assert_equals(resp.type, "opaque", "Response is opaque");
+ }
+ });
+ assert_unreached(redirectMode + " is no a valid redirect mode");
+ }, origin + " redirect " + redirectStatus + " in " + redirectMode + " redirect and " + corsMode + " mode");
+}
+
+for (var origin of ["same-origin", "cross-origin"]) {
+ for (var statusCode of [301, 302, 303, 307, 308]) {
+ for (var redirect of ["error", "manual", "follow"]) {
+ for (var mode of ["cors", "no-cors"])
+ testRedirect(origin, statusCode, redirect, mode);
+ }
+ }
+}
+
+promise_test(async (t) => {
+ const destination = `${ORIGIN}/common/blank.html`;
+ // We use /common/redirect.py intentionally, as we want a CORS error.
+ const url =
+ `${REMOTE_ORIGIN}/common/redirect.py?location=${destination}`;
+ await promise_rejects_js(t, TypeError, fetch(url, { redirect: "manual" }));
+}, "manual redirect with a CORS error should be rejected");
+
+done();
diff --git a/test/wpt/tests/fetch/api/redirect/redirect-origin.any.js b/test/wpt/tests/fetch/api/redirect/redirect-origin.any.js
new file mode 100644
index 0000000..6001c50
--- /dev/null
+++ b/test/wpt/tests/fetch/api/redirect/redirect-origin.any.js
@@ -0,0 +1,68 @@
+// META: script=/common/utils.js
+// META: script=../resources/utils.js
+// META: script=/common/get-host-info.sub.js
+
+const {
+ HTTP_ORIGIN,
+ HTTP_REMOTE_ORIGIN,
+} = get_host_info();
+
+/**
+ * Fetches `fromUrl` with 'cors' and 'follow' modes that returns response to
+ * redirect to `toUrl`.
+ */
+function testOriginAfterRedirection(
+ desc, method, fromUrl, toUrl, statusCode, expectedOrigin) {
+ desc = `[${method}] Redirect ${statusCode} ${desc}`;
+ const token1 = token();
+ const url = `${fromUrl}?token=${token1}&max_age=0` +
+ `&redirect_status=${statusCode}` +
+ `&location=${encodeURIComponent(toUrl)}`;
+
+ const requestInit = {method, 'mode': 'cors', 'redirect': 'follow'};
+
+ promise_test(function(test) {
+ return fetch(`${RESOURCES_DIR}clean-stash.py?token=${token1}`)
+ .then((cleanResponse) => {
+ assert_equals(
+ cleanResponse.status, 200,
+ `Clean stash response's status is 200`);
+ return fetch(url, requestInit).then((redirectResponse) => {
+ assert_equals(
+ redirectResponse.status, 200,
+ `Inspect header response's status is 200`);
+ assert_equals(
+ redirectResponse.headers.get('x-request-origin'),
+ expectedOrigin, 'Check origin header');
+ });
+ });
+ }, desc);
+}
+
+const FROM_URL = `${RESOURCES_DIR}redirect.py`;
+const CORS_FROM_URL =
+ `${HTTP_REMOTE_ORIGIN}${dirname(location.pathname)}${FROM_URL}`;
+const TO_URL = `${HTTP_ORIGIN}${dirname(location.pathname)}${
+ RESOURCES_DIR}inspect-headers.py?headers=origin`;
+const CORS_TO_URL = `${HTTP_REMOTE_ORIGIN}${dirname(location.pathname)}${
+ RESOURCES_DIR}inspect-headers.py?cors&headers=origin`;
+
+for (const statusCode of [301, 302, 303, 307, 308]) {
+ for (const method of ['GET', 'POST']) {
+ testOriginAfterRedirection(
+ 'Same origin to same origin', method, FROM_URL, TO_URL, statusCode,
+ null);
+ testOriginAfterRedirection(
+ 'Same origin to other origin', method, FROM_URL, CORS_TO_URL,
+ statusCode, HTTP_ORIGIN);
+ testOriginAfterRedirection(
+ 'Other origin to other origin', method, CORS_FROM_URL, CORS_TO_URL,
+ statusCode, HTTP_ORIGIN);
+ // TODO(crbug.com/1432059): Fix broken tests.
+ testOriginAfterRedirection(
+ 'Other origin to same origin', method, CORS_FROM_URL, `${TO_URL}&cors`,
+ statusCode, 'null');
+ }
+}
+
+done();
diff --git a/test/wpt/tests/fetch/api/redirect/redirect-referrer-override.any.js b/test/wpt/tests/fetch/api/redirect/redirect-referrer-override.any.js
new file mode 100644
index 0000000..56e55d7
--- /dev/null
+++ b/test/wpt/tests/fetch/api/redirect/redirect-referrer-override.any.js
@@ -0,0 +1,104 @@
+// META: timeout=long
+// META: script=/common/utils.js
+// META: script=../resources/utils.js
+// META: script=/common/get-host-info.sub.js
+
+function getExpectation(expectations, init, initScenario, redirectPolicy, redirectScenario) {
+ let policies = [
+ expectations[initPolicy][initScenario],
+ expectations[redirectPolicy][redirectScenario]
+ ];
+
+ if (policies.includes("omitted")) {
+ return null;
+ } else if (policies.includes("origin")) {
+ return referrerOrigin;
+ } else {
+ // "stripped-referrer"
+ return referrerUrl;
+ }
+}
+
+function testReferrerAfterRedirection(desc, redirectUrl, redirectLocation, referrerPolicy, redirectReferrerPolicy, expectedReferrer) {
+ var url = redirectUrl;
+ var urlParameters = "?location=" + encodeURIComponent(redirectLocation);
+ var description = desc + ", " + referrerPolicy + " init, " + redirectReferrerPolicy + " redirect header ";
+
+ if (redirectReferrerPolicy)
+ urlParameters += "&redirect_referrerpolicy=" + redirectReferrerPolicy;
+
+ var requestInit = {"redirect": "follow", "referrerPolicy": referrerPolicy};
+ promise_test(function(test) {
+ return fetch(url + urlParameters, requestInit).then(function(response) {
+ assert_equals(response.status, 200, "Inspect header response's status is 200");
+ assert_equals(response.headers.get("x-request-referer"), expectedReferrer ? expectedReferrer : null, "Check referrer header");
+ });
+ }, description);
+}
+
+var referrerOrigin = get_host_info().HTTP_ORIGIN + "/";
+var referrerUrl = location.href;
+
+var redirectUrl = RESOURCES_DIR + "redirect.py";
+var locationUrl = get_host_info().HTTP_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "inspect-headers.py?headers=referer";
+var crossLocationUrl = get_host_info().HTTP_REMOTE_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "inspect-headers.py?cors&headers=referer";
+
+var expectations = {
+ "no-referrer": {
+ "same-origin": "omitted",
+ "cross-origin": "omitted"
+ },
+ "no-referrer-when-downgrade": {
+ "same-origin": "stripped-referrer",
+ "cross-origin": "stripped-referrer"
+ },
+ "origin": {
+ "same-origin": "origin",
+ "cross-origin": "origin"
+ },
+ "origin-when-cross-origin": {
+ "same-origin": "stripped-referrer",
+ "cross-origin": "origin",
+ },
+ "same-origin": {
+ "same-origin": "stripped-referrer",
+ "cross-origin": "omitted"
+ },
+ "strict-origin": {
+ "same-origin": "origin",
+ "cross-origin": "origin"
+ },
+ "strict-origin-when-cross-origin": {
+ "same-origin": "stripped-referrer",
+ "cross-origin": "origin"
+ },
+ "unsafe-url": {
+ "same-origin": "stripped-referrer",
+ "cross-origin": "stripped-referrer"
+ }
+};
+
+for (var initPolicy in expectations) {
+ for (var redirectPolicy in expectations) {
+
+ // Redirect to same-origin URL
+ testReferrerAfterRedirection(
+ "Same origin redirection",
+ redirectUrl,
+ locationUrl,
+ initPolicy,
+ redirectPolicy,
+ getExpectation(expectations, initPolicy, "same-origin", redirectPolicy, "same-origin"));
+
+ // Redirect to cross-origin URL
+ testReferrerAfterRedirection(
+ "Cross origin redirection",
+ redirectUrl,
+ crossLocationUrl,
+ initPolicy,
+ redirectPolicy,
+ getExpectation(expectations, initPolicy, "same-origin", redirectPolicy, "cross-origin"));
+ }
+}
+
+done();
diff --git a/test/wpt/tests/fetch/api/redirect/redirect-referrer.any.js b/test/wpt/tests/fetch/api/redirect/redirect-referrer.any.js
new file mode 100644
index 0000000..99fda42
--- /dev/null
+++ b/test/wpt/tests/fetch/api/redirect/redirect-referrer.any.js
@@ -0,0 +1,66 @@
+// META: timeout=long
+// META: script=/common/utils.js
+// META: script=../resources/utils.js
+// META: script=/common/get-host-info.sub.js
+
+function testReferrerAfterRedirection(desc, redirectUrl, redirectLocation, referrerPolicy, redirectReferrerPolicy, expectedReferrer) {
+ var url = redirectUrl;
+ var urlParameters = "?location=" + encodeURIComponent(redirectLocation);
+
+ if (redirectReferrerPolicy)
+ urlParameters += "&redirect_referrerpolicy=" + redirectReferrerPolicy;
+
+ var requestInit = {"redirect": "follow", "referrerPolicy": referrerPolicy};
+
+ promise_test(function(test) {
+ return fetch(url + urlParameters, requestInit).then(function(response) {
+ assert_equals(response.status, 200, "Inspect header response's status is 200");
+ assert_equals(response.headers.get("x-request-referer"), expectedReferrer ? expectedReferrer : null, "Check referrer header");
+ });
+ }, desc);
+}
+
+var referrerOrigin = get_host_info().HTTP_ORIGIN + "/";
+var referrerUrl = location.href;
+
+var redirectUrl = RESOURCES_DIR + "redirect.py";
+var locationUrl = get_host_info().HTTP_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "inspect-headers.py?headers=referer";
+var crossLocationUrl = get_host_info().HTTP_REMOTE_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "inspect-headers.py?cors&headers=referer";
+
+testReferrerAfterRedirection("Same origin redirection, empty init, unsafe-url redirect header ", redirectUrl, locationUrl, "", "unsafe-url", referrerUrl);
+testReferrerAfterRedirection("Same origin redirection, empty init, no-referrer-when-downgrade redirect header ", redirectUrl, locationUrl, "", "no-referrer-when-downgrade", referrerUrl);
+testReferrerAfterRedirection("Same origin redirection, empty init, same-origin redirect header ", redirectUrl, locationUrl, "", "same-origin", referrerUrl);
+testReferrerAfterRedirection("Same origin redirection, empty init, origin redirect header ", redirectUrl, locationUrl, "", "origin", referrerOrigin);
+testReferrerAfterRedirection("Same origin redirection, empty init, origin-when-cross-origin redirect header ", redirectUrl, locationUrl, "", "origin-when-cross-origin", referrerUrl);
+testReferrerAfterRedirection("Same origin redirection, empty init, no-referrer redirect header ", redirectUrl, locationUrl, "", "no-referrer", null);
+testReferrerAfterRedirection("Same origin redirection, empty init, strict-origin redirect header ", redirectUrl, locationUrl, "", "strict-origin", referrerOrigin);
+testReferrerAfterRedirection("Same origin redirection, empty init, strict-origin-when-cross-origin redirect header ", redirectUrl, locationUrl, "", "strict-origin-when-cross-origin", referrerUrl);
+
+testReferrerAfterRedirection("Same origin redirection, empty redirect header, unsafe-url init ", redirectUrl, locationUrl, "unsafe-url", "", referrerUrl);
+testReferrerAfterRedirection("Same origin redirection, empty redirect header, no-referrer-when-downgrade init ", redirectUrl, locationUrl, "no-referrer-when-downgrade", "", referrerUrl);
+testReferrerAfterRedirection("Same origin redirection, empty redirect header, same-origin init ", redirectUrl, locationUrl, "same-origin", "", referrerUrl);
+testReferrerAfterRedirection("Same origin redirection, empty redirect header, origin init ", redirectUrl, locationUrl, "origin", "", referrerOrigin);
+testReferrerAfterRedirection("Same origin redirection, empty redirect header, origin-when-cross-origin init ", redirectUrl, locationUrl, "origin-when-cross-origin", "", referrerUrl);
+testReferrerAfterRedirection("Same origin redirection, empty redirect header, no-referrer init ", redirectUrl, locationUrl, "no-referrer", "", null);
+testReferrerAfterRedirection("Same origin redirection, empty redirect header, strict-origin init ", redirectUrl, locationUrl, "strict-origin", "", referrerOrigin);
+testReferrerAfterRedirection("Same origin redirection, empty redirect header, strict-origin-when-cross-origin init ", redirectUrl, locationUrl, "strict-origin-when-cross-origin", "", referrerUrl);
+
+testReferrerAfterRedirection("Cross origin redirection, empty init, unsafe-url redirect header ", redirectUrl, crossLocationUrl, "", "unsafe-url", referrerUrl);
+testReferrerAfterRedirection("Cross origin redirection, empty init, no-referrer-when-downgrade redirect header ", redirectUrl, crossLocationUrl, "", "no-referrer-when-downgrade", referrerUrl);
+testReferrerAfterRedirection("Cross origin redirection, empty init, same-origin redirect header ", redirectUrl, crossLocationUrl, "", "same-origin", null);
+testReferrerAfterRedirection("Cross origin redirection, empty init, origin redirect header ", redirectUrl, crossLocationUrl, "", "origin", referrerOrigin);
+testReferrerAfterRedirection("Cross origin redirection, empty init, origin-when-cross-origin redirect header ", redirectUrl, crossLocationUrl, "", "origin-when-cross-origin", referrerOrigin);
+testReferrerAfterRedirection("Cross origin redirection, empty init, no-referrer redirect header ", redirectUrl, crossLocationUrl, "", "no-referrer", null);
+testReferrerAfterRedirection("Cross origin redirection, empty init, strict-origin redirect header ", redirectUrl, crossLocationUrl, "", "strict-origin", referrerOrigin);
+testReferrerAfterRedirection("Cross origin redirection, empty init, strict-origin-when-cross-origin redirect header ", redirectUrl, crossLocationUrl, "", "strict-origin-when-cross-origin", referrerOrigin);
+
+testReferrerAfterRedirection("Cross origin redirection, empty redirect header, unsafe-url init ", redirectUrl, crossLocationUrl, "unsafe-url", "", referrerUrl);
+testReferrerAfterRedirection("Cross origin redirection, empty redirect header, no-referrer-when-downgrade init ", redirectUrl, crossLocationUrl, "no-referrer-when-downgrade", "", referrerUrl);
+testReferrerAfterRedirection("Cross origin redirection, empty redirect header, same-origin init ", redirectUrl, crossLocationUrl, "same-origin", "", null);
+testReferrerAfterRedirection("Cross origin redirection, empty redirect header, origin init ", redirectUrl, crossLocationUrl, "origin", "", referrerOrigin);
+testReferrerAfterRedirection("Cross origin redirection, empty redirect header, origin-when-cross-origin init ", redirectUrl, crossLocationUrl, "origin-when-cross-origin", "", referrerOrigin);
+testReferrerAfterRedirection("Cross origin redirection, empty redirect header, no-referrer init ", redirectUrl, crossLocationUrl, "no-referrer", "", null);
+testReferrerAfterRedirection("Cross origin redirection, empty redirect header, strict-origin init ", redirectUrl, crossLocationUrl, "strict-origin", "", referrerOrigin);
+testReferrerAfterRedirection("Cross origin redirection, empty redirect header, strict-origin-when-cross-origin init ", redirectUrl, crossLocationUrl, "strict-origin-when-cross-origin", "", referrerOrigin);
+
+done();
diff --git a/test/wpt/tests/fetch/api/redirect/redirect-schemes.any.js b/test/wpt/tests/fetch/api/redirect/redirect-schemes.any.js
new file mode 100644
index 0000000..31ec124
--- /dev/null
+++ b/test/wpt/tests/fetch/api/redirect/redirect-schemes.any.js
@@ -0,0 +1,19 @@
+// META: title=Fetch: handling different schemes in redirects
+// META: global=window,worker
+// META: script=/common/get-host-info.sub.js
+
+// All non-HTTP(S) schemes cannot survive redirects
+var url = "../resources/redirect.py?location=";
+var tests = [
+ url + "mailto:a@a.com",
+ url + "data:,HI",
+ url + "facetime:a@a.org",
+ url + "about:blank",
+ url + "about:unicorn",
+ url + "blob:djfksfjs"
+];
+tests.forEach(function(url) {
+ promise_test(function(test) {
+ return promise_rejects_js(test, TypeError, fetch(url))
+ })
+})
diff --git a/test/wpt/tests/fetch/api/redirect/redirect-to-dataurl.any.js b/test/wpt/tests/fetch/api/redirect/redirect-to-dataurl.any.js
new file mode 100644
index 0000000..9d0f147
--- /dev/null
+++ b/test/wpt/tests/fetch/api/redirect/redirect-to-dataurl.any.js
@@ -0,0 +1,28 @@
+// META: global=window,worker
+// META: script=/common/get-host-info.sub.js
+
+var dataURL = "data:text/plain;base64,cmVzcG9uc2UncyBib2R5";
+var body = "response's body";
+var contentType = "text/plain";
+
+function redirectDataURL(desc, redirectUrl, mode) {
+ var url = redirectUrl + "?cors&location=" + encodeURIComponent(dataURL);
+
+ var requestInit = {"mode": mode};
+
+ promise_test(function(test) {
+ return promise_rejects_js(test, TypeError, fetch(url, requestInit));
+ }, desc);
+}
+
+var redirUrl = get_host_info().HTTP_ORIGIN + "/fetch/api/resources/redirect.py";
+var corsRedirUrl = get_host_info().HTTP_REMOTE_ORIGIN + "/fetch/api/resources/redirect.py";
+
+redirectDataURL("Testing data URL loading after same-origin redirection (cors mode)", redirUrl, "cors");
+redirectDataURL("Testing data URL loading after same-origin redirection (no-cors mode)", redirUrl, "no-cors");
+redirectDataURL("Testing data URL loading after same-origin redirection (same-origin mode)", redirUrl, "same-origin");
+
+redirectDataURL("Testing data URL loading after cross-origin redirection (cors mode)", corsRedirUrl, "cors");
+redirectDataURL("Testing data URL loading after cross-origin redirection (no-cors mode)", corsRedirUrl, "no-cors");
+
+done();
diff --git a/test/wpt/tests/fetch/api/redirect/redirect-upload.h2.any.js b/test/wpt/tests/fetch/api/redirect/redirect-upload.h2.any.js
new file mode 100644
index 0000000..521bd3a
--- /dev/null
+++ b/test/wpt/tests/fetch/api/redirect/redirect-upload.h2.any.js
@@ -0,0 +1,33 @@
+// META: global=window,worker
+// META: script=../resources/utils.js
+// META: script=/common/utils.js
+// META: script=/common/get-host-info.sub.js
+
+const redirectUrl = RESOURCES_DIR + "redirect.h2.py";
+const redirectLocation = "top.txt";
+
+async function fetchStreamRedirect(statusCode) {
+ const url = RESOURCES_DIR + "redirect.h2.py" +
+ `?redirect_status=${statusCode}&location=${redirectLocation}`;
+ const requestInit = {method: "POST"};
+ requestInit["body"] = new ReadableStream({start: controller => {
+ const encoder = new TextEncoder();
+ controller.enqueue(encoder.encode("Test"));
+ controller.close();
+ }});
+ requestInit.duplex = "half";
+ return fetch(url, requestInit);
+}
+
+promise_test(async () => {
+ const resp = await fetchStreamRedirect(303);
+ assert_equals(resp.status, 200);
+ assert_true(new URL(resp.url).pathname.endsWith(redirectLocation),
+ "Response's url should be the redirected one");
+}, "Fetch upload streaming should be accepted on 303");
+
+for (const statusCode of [301, 302, 307, 308]) {
+ promise_test(t => {
+ return promise_rejects_js(t, TypeError, fetchStreamRedirect(statusCode));
+ }, `Fetch upload streaming should fail on ${statusCode}`);
+}
diff --git a/test/wpt/tests/fetch/api/request/destination/fetch-destination-frame.https.html b/test/wpt/tests/fetch/api/request/destination/fetch-destination-frame.https.html
new file mode 100644
index 0000000..f3f9f78
--- /dev/null
+++ b/test/wpt/tests/fetch/api/request/destination/fetch-destination-frame.https.html
@@ -0,0 +1,51 @@
+<!DOCTYPE html>
+<title>Fetch destination tests for resources with no load event</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script>
+<script>
+let frame;
+const kScope = 'resources/dummy.html?dest=frame';
+
+// Set up the service worker and the frame.
+promise_test(t => {
+ const kScript = 'resources/fetch-destination-worker-frame.js';
+ return service_worker_unregister_and_register(t, kScript, kScope)
+ .then(registration => {
+ add_completion_callback(() => {
+ registration.unregister();
+ });
+
+ return wait_for_state(t, registration.installing, 'activated');
+ });
+ }, 'Initialize global state');
+
+var waitOnMessageFromSW = async t => {
+ await new Promise((resolve, reject) => {
+ navigator.serviceWorker.onmessage = t.step_func(event => {
+ if (event.data == "PASS") {
+ resolve();
+ } else {
+ reject();
+ }
+ });
+ }).catch(() => {;
+ assert_unreached("Wrong destination.");
+ });
+ t.add_cleanup(() => { frame.contentWindow.navigator.serviceWorker.onmessage = null; });
+}
+
+// Document destination
+///////////////////////
+promise_test(async t => {
+ var f = document.createElement('frame');
+ frame = f;
+ f.className = 'test-frame';
+ f.src = kScope;
+ document.body.appendChild(f);
+ await waitOnMessageFromSW(t);
+ add_completion_callback(() => { f.remove(); });
+}, 'frame fetches with a "frame" Request.destination');
+
+</script>
diff --git a/test/wpt/tests/fetch/api/request/destination/fetch-destination-iframe.https.html b/test/wpt/tests/fetch/api/request/destination/fetch-destination-iframe.https.html
new file mode 100644
index 0000000..1aa5a56
--- /dev/null
+++ b/test/wpt/tests/fetch/api/request/destination/fetch-destination-iframe.https.html
@@ -0,0 +1,51 @@
+<!DOCTYPE html>
+<title>Fetch destination tests for resources with no load event</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script>
+<script>
+let frame;
+const kScope = 'resources/dummy.html?dest=iframe';
+
+// Set up the service worker and the frame.
+promise_test(t => {
+ const kScript = 'resources/fetch-destination-worker-iframe.js';
+ return service_worker_unregister_and_register(t, kScript, kScope)
+ .then(registration => {
+ add_completion_callback(() => {
+ registration.unregister();
+ });
+
+ return wait_for_state(t, registration.installing, 'activated');
+ });
+ }, 'Initialize global state');
+
+var waitOnMessageFromSW = async t => {
+ await new Promise((resolve, reject) => {
+ navigator.serviceWorker.onmessage = t.step_func(event => {
+ if (event.data == "PASS") {
+ resolve();
+ } else {
+ reject();
+ }
+ });
+ }).catch(() => {;
+ assert_unreached("Wrong destination.");
+ });
+ t.add_cleanup(() => { frame.contentWindow.navigator.serviceWorker.onmessage = null; });
+}
+
+// Document destination
+///////////////////////
+promise_test(async t => {
+ var f = document.createElement('iframe');
+ frame = f;
+ f.className = 'test-iframe';
+ f.src = kScope;
+ document.body.appendChild(f);
+ await waitOnMessageFromSW(t);
+ add_completion_callback(() => { f.remove(); });
+}, 'iframe fetches with a "iframe" Request.destination');
+
+</script>
diff --git a/test/wpt/tests/fetch/api/request/destination/fetch-destination-no-load-event.https.html b/test/wpt/tests/fetch/api/request/destination/fetch-destination-no-load-event.https.html
new file mode 100644
index 0000000..1778bf2
--- /dev/null
+++ b/test/wpt/tests/fetch/api/request/destination/fetch-destination-no-load-event.https.html
@@ -0,0 +1,124 @@
+<!DOCTYPE html>
+<title>Fetch destination tests for resources with no load event</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script>
+<script>
+let frame;
+
+// Set up the service worker and the frame.
+promise_test(t => {
+ const kScope = 'resources/';
+ const kFrame = 'resources/empty.https.html';
+ const kScript = 'resources/fetch-destination-worker-no-load-event.js';
+ return service_worker_unregister_and_register(t, kScript, kScope)
+ .then(registration => {
+ add_completion_callback(() => {
+ registration.unregister();
+ });
+
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(() => {
+ return with_iframe(kFrame);
+ })
+ .then(f => {
+ frame = f;
+ add_completion_callback(() => { f.remove(); });
+ });
+ }, 'Initialize global state');
+
+var waitOnMessageFromSW = async t => {
+ await new Promise((resolve, reject) => {
+ frame.contentWindow.navigator.serviceWorker.onmessage = t.step_func(event => {
+ if (event.data == "PASS") {
+ resolve();
+ } else {
+ reject();
+ }
+ });
+ }).catch(() => {;
+ assert_unreached("Wrong destination.");
+ });
+ t.add_cleanup(() => { frame.contentWindow.navigator.serviceWorker.onmessage = null; });
+}
+// Actual tests
+
+// Image destination
+////////////////////
+
+// CSS background image - image destination
+promise_test(async t => {
+ let node = frame.contentWindow.document.createElement("div");
+ node.style = "background-image: url(dummy.png?t=bg2&dest=image)";
+ frame.contentWindow.document.body.appendChild(node);
+
+ await waitOnMessageFromSW(t);
+}, 'Background image fetches with an "image" Request.destination');
+
+// Font destination
+///////////////////
+
+// Font loading API - font destination
+promise_test(async t => {
+ let font = new frame.contentWindow.FontFace("foo", "url(dummy.ttf?t=api&dest=font)");
+ font.load();
+
+ await waitOnMessageFromSW(t);
+}, 'Font loading API fetches with an "font" Request.destination');
+
+// CSS font - font destination
+promise_test(async t => {
+ let style = frame.contentWindow.document.createElement("style");
+ style.innerHTML = "@font-face { font-family: foo; src: url(dummy.ttf?t=css&dest=font); }";
+ style.innerHTML += "div {font-family: foo; }";
+ let div = frame.contentWindow.document.createElement("div");
+ div.innerHTML = "bar";
+ frame.contentWindow.document.body.appendChild(style);
+ frame.contentWindow.document.body.appendChild(div);
+
+ await waitOnMessageFromSW(t);
+}, 'CSS font fetches with an "font" Request.destination');
+
+// Empty string destination
+///////////////////////////
+
+// sendBeacon() - empty string destination
+promise_test(async t => {
+ frame.contentWindow.navigator.sendBeacon("dummy?t=beacon&dest=", "foobar");
+
+ await waitOnMessageFromSW(t);
+}, 'sendBeacon() fetches with an empty string Request.destination');
+
+// Cache.add() - empty string destination
+promise_test(async t => {
+ frame.contentWindow.caches.open("foo").then(cache => {
+ cache.add("dummy?t=cache&dest=");
+ });
+
+ await waitOnMessageFromSW(t);
+}, 'Cache.add() fetches with an empty string Request.destination');
+
+// script destination
+/////////////////////
+
+// importScripts() - script destination
+promise_test(async t => {
+ let worker = new frame.contentWindow.Worker("importer.js");
+
+ await waitOnMessageFromSW(t);
+}, 'importScripts() fetches with a "script" Request.destination');
+
+// style destination
+/////////////////////
+// @import - style destination
+promise_test(async t => {
+ let node = frame.contentWindow.document.createElement("style");
+ node.innerHTML = '@import url("dummy?t=import&dest=style")';
+ frame.contentWindow.document.body.appendChild(node);
+
+ await waitOnMessageFromSW(t);
+}, '@import fetches with a "style" Request.destination');
+
+</script>
diff --git a/test/wpt/tests/fetch/api/request/destination/fetch-destination-prefetch.https.html b/test/wpt/tests/fetch/api/request/destination/fetch-destination-prefetch.https.html
new file mode 100644
index 0000000..db99202
--- /dev/null
+++ b/test/wpt/tests/fetch/api/request/destination/fetch-destination-prefetch.https.html
@@ -0,0 +1,46 @@
+<!DOCTYPE html>
+<title>Fetch destination test for prefetching</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="/common/media.js"></script>
+<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script>
+<script>
+let frame;
+
+// Set up the service worker and the frame.
+promise_test(t => {
+ const kScope = 'resources/empty.https.html';
+ const kScript = 'resources/fetch-destination-worker.js';
+ return service_worker_unregister_and_register(t, kScript, kScope)
+ .then(registration => {
+ add_completion_callback(() => {
+ registration.unregister();
+ });
+
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(() => {
+ return with_iframe(kScope);
+ })
+ .then(f => {
+ frame = f;
+ add_completion_callback(() => { f.remove(); });
+ });
+ }, 'Initialize global state');
+
+// HTMLLinkElement with rel=prefetch - empty string destination
+promise_test(async t => {
+ await new Promise((resolve, reject) => {
+ let node = frame.contentWindow.document.createElement("link");
+ node.rel = "prefetch";
+ node.onload = resolve;
+ node.onerror = reject;
+ node.href = "dummy?dest=";
+ frame.contentWindow.document.body.appendChild(node);
+ }).catch(() => {
+ assert_unreached("Fetch errored.");
+ });
+}, 'HTMLLinkElement with rel=prefetch fetches with an empty string Request.destination');
+
+</script>
diff --git a/test/wpt/tests/fetch/api/request/destination/fetch-destination-worker.https.html b/test/wpt/tests/fetch/api/request/destination/fetch-destination-worker.https.html
new file mode 100644
index 0000000..5935c1f
--- /dev/null
+++ b/test/wpt/tests/fetch/api/request/destination/fetch-destination-worker.https.html
@@ -0,0 +1,60 @@
+<!DOCTYPE html>
+<title>Fetch destination tests for resources with no load event</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script>
+<script>
+let frame;
+
+// Set up the service worker and the frame.
+promise_test(t => {
+ const kScope = 'resources/dummy.html';
+ const kScript = 'resources/fetch-destination-worker-no-load-event.js';
+ return service_worker_unregister_and_register(t, kScript, kScope)
+ .then(registration => {
+ add_completion_callback(() => {
+ registration.unregister();
+ });
+
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(() => {
+ return with_iframe(kScope);
+ })
+ .then(f => {
+ frame = f;
+ add_completion_callback(() => { f.remove(); });
+ });
+ }, 'Initialize global state');
+
+var waitOnMessageFromSW = async t => {
+ await new Promise((resolve, reject) => {
+ frame.contentWindow.navigator.serviceWorker.onmessage = t.step_func(event => {
+ if (event.data == "PASS") {
+ resolve();
+ } else {
+ reject();
+ }
+ });
+ }).catch(() => {;
+ assert_unreached("Wrong destination.");
+ });
+ t.add_cleanup(() => { frame.contentWindow.navigator.serviceWorker.onmessage = null; });
+}
+
+// worker destination
+/////////////////////
+promise_test(async t => {
+ // We can use an html file as we don't really care about the dedicated worker successfully loading.
+ let worker = new frame.contentWindow.Worker("dummy.html?t=worker&dest=worker");
+ await waitOnMessageFromSW(t);
+}, 'DedicatedWorker fetches with a "worker" Request.destination');
+
+promise_test(async t => {
+ // We can use an html file as we don't really care about the shared worker successfully loading.
+ let worker = new frame.contentWindow.SharedWorker("dummy.html?t=sharedworker&dest=sharedworker");
+ await waitOnMessageFromSW(t);
+}, 'SharedWorker fetches with a "sharedworker" Request.destination');
+
+</script>
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
new file mode 100644
index 0000000..0094b0b
--- /dev/null
+++ b/test/wpt/tests/fetch/api/request/destination/fetch-destination.https.html
@@ -0,0 +1,435 @@
+<!DOCTYPE html>
+<title>Fetch destination tests</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="/common/media.js"></script>
+<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script>
+<script>
+let frame;
+
+// Set up the service worker and the frame.
+promise_test(t => {
+ const kScope = 'resources/empty.https.html';
+ const kScript = 'resources/fetch-destination-worker.js';
+ return service_worker_unregister_and_register(t, kScript, kScope)
+ .then(registration => {
+ add_completion_callback(() => {
+ registration.unregister();
+ });
+
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(() => {
+ return with_iframe(kScope);
+ })
+ .then(f => {
+ frame = f;
+ add_completion_callback(() => { f.remove(); });
+ });
+ }, 'Initialize global state');
+
+// Actual tests
+
+// Image destination
+////////////////////
+
+// HTMLImageElement - image destination
+promise_test(async t => {
+ await new Promise((resolve, reject) => {
+ let node = frame.contentWindow.document.createElement("img");
+ node.onload = resolve;
+ node.onerror = reject;
+ node.src = "dummy.png?dest=image";
+ }).catch(() => {
+ assert_unreached("Fetch errored.");
+ });
+}, 'HTMLImageElement fetches with an "image" Request.destination');
+
+// HTMLImageElement with srcset attribute - image destination
+promise_test(async t => {
+ await new Promise((resolve, reject) => {
+ let node = frame.contentWindow.document.createElement("img");
+ node.onload = resolve;
+ node.onerror = reject;
+ node.srcset = "dummy.png?t=srcset&dest=image";
+ }).catch(() => {
+ assert_unreached("Fetch errored.");
+ });
+}, 'HTMLImageElement with srcset attribute fetches with an "image" Request.destination');
+
+// HTMLImageElement with srcset attribute - image destination
+promise_test(async t => {
+ await new Promise((resolve, reject) => {
+ let img = frame.contentWindow.document.createElement("img");
+ let picture = frame.contentWindow.document.createElement("picture");
+ let source = frame.contentWindow.document.createElement("source");
+ picture.appendChild(source);
+ picture.appendChild(img);
+ img.onload = resolve;
+ img.onerror = reject;
+ source.srcset = "dummy.png?t=picture&dest=image";
+ }).catch(() => {
+ assert_unreached("Fetch errored.");
+ });
+}, 'HTMLImageElement with a HTMLPictureElement parent attribute fetches with an "image" Request.destination');
+
+// SVGImageElement - image destination
+promise_test(async t => {
+ await new Promise((resolve, reject) => {
+ let svg = frame.contentWindow.document.createElementNS('http://www.w3.org/2000/svg','svg');
+ svg.setAttributeNS('http://www.w3.org/2000/svg','xlink','http://www.w3.org/1999/xlink');
+ let svgimg = frame.contentWindow.document.createElementNS('http://www.w3.org/2000/svg','image');
+ svgimg.onload = resolve;
+ svgimg.onerror = reject;
+ svgimg.setAttributeNS('http://www.w3.org/1999/xlink','href','dummy.png?t=svg&dest=image');
+ svg.appendChild(svgimg);
+ frame.contentWindow.document.documentElement.appendChild(svg);
+ }).catch(() => {
+ assert_unreached("Fetch errored.");
+ });
+}, 'SVGImageElement fetches with an "image" Request.destination');
+
+// Empty string destination
+///////////////////////////
+
+// fetch() - empty string destination
+promise_test(async t => {
+ let response = await frame.contentWindow.fetch("dummy?dest=");
+ assert_true(response.ok);
+}, 'fetch() fetches with an empty string Request.destination');
+
+// XMLHttpRequest - empty string destination
+promise_test(async t => {
+ let xhr;
+ await new Promise((resolve, reject) => {
+ xhr = new frame.contentWindow.XMLHttpRequest();
+ xhr.onload = resolve;
+ xhr.onerror = reject;
+ xhr.open("GET", "dummy?t=xhr&dest=");
+ xhr.send();
+ }).catch(() => {
+ assert_unreached("Fetch errored.");
+ });
+ assert_equals(xhr.status, 200);
+}, 'XMLHttpRequest() fetches with an empty string Request.destination');
+
+// EventSource - empty string destination
+promise_test(async t => {
+ let xhr;
+ await new Promise((resolve, reject) => {
+ eventSource = new frame.contentWindow.EventSource("dummy.es?t=eventsource&dest=");
+ eventSource.onopen = resolve;
+ eventSource.onerror = reject;
+ }).catch(() => {
+ assert_unreached("Fetch errored.");
+ });
+}, 'EventSource() fetches with an empty string Request.destination');
+
+// HTMLAudioElement - audio destination
+///////////////////////////////////////
+promise_test(async t => {
+ await new Promise((resolve, reject) => {
+ let audioURL = getAudioURI("dummy_audio");
+ let node = frame.contentWindow.document.createElement("audio");
+ node.onloadeddata = resolve;
+ node.onerror = reject;
+ node.src = audioURL + "?dest=audio";
+ }).catch(() => {
+ assert_unreached("Fetch errored.");
+ });
+}, 'HTMLAudioElement fetches with an "audio" Request.destination');
+
+// HTMLVideoElement - video destination
+///////////////////////////////////////
+promise_test(async t => {
+ await new Promise((resolve, reject) => {
+ let videoURL = getVideoURI("dummy_video");
+ let node = frame.contentWindow.document.createElement("video");
+ node.onloadeddata = resolve;
+ node.onerror = reject;
+ node.src = videoURL + "?dest=video";
+ }).catch(() => {
+ assert_unreached("Fetch errored.");
+ });
+}, 'HTMLVideoElement fetches with a "video" Request.destination');
+
+// script destinations
+//////////////////////
+
+// HTMLScriptElement - script destination
+promise_test(async t => {
+ await new Promise((resolve, reject) => {
+ let node = frame.contentWindow.document.createElement("script");
+ node.onload = resolve;
+ node.onerror = reject;
+ node.src = "dummy?dest=script";
+ frame.contentWindow.document.body.appendChild(node);
+ }).catch(() => {
+ assert_unreached("Fetch errored.");
+ });
+}, 'HTMLScriptElement fetches with a "script" Request.destination');
+
+// audioworklet destination
+//////////////////////
+promise_test(async t => {
+ let audioContext = new frame.contentWindow.AudioContext();
+ await audioContext.audioWorklet.addModule("dummy?dest=audioworklet");
+}, 'AudioWorklet module fetches with a "audioworklet" Request.destination');
+
+// Style destination
+////////////////////
+
+// HTMLLinkElement with rel=stylesheet - style destination
+promise_test(async t => {
+ await new Promise((resolve, reject) => {
+ let node = frame.contentWindow.document.createElement("link");
+ node.rel = "stylesheet";
+ node.onload = resolve;
+ node.onerror = reject;
+ node.href = "dummy?dest=style";
+ frame.contentWindow.document.body.appendChild(node);
+ }).catch(() => {
+ assert_unreached("Fetch errored.");
+ });
+}, 'HTMLLinkElement with rel=stylesheet fetches with a "style" Request.destination');
+
+// Preload tests
+////////////////
+// HTMLLinkElement with rel=preload and as=fetch - empty string destination
+promise_test(async t => {
+ await new Promise((resolve, reject) => {
+ let node = frame.contentWindow.document.createElement("link");
+ node.rel = "preload";
+ node.as = "fetch";
+ if (node.as != "fetch") {
+ resolve();
+ }
+ node.onload = resolve;
+ node.onerror = reject;
+ node.href = "dummy?t=2&dest=";
+ frame.contentWindow.document.body.appendChild(node);
+ }).catch(() => {
+ assert_unreached("Fetch errored.");
+ });
+}, 'HTMLLinkElement with rel=preload and as=fetch fetches with an empty string Request.destination');
+
+// HTMLLinkElement with rel=preload and as=style - style destination
+promise_test(async t => {
+ await new Promise((resolve, reject) => {
+ let node = frame.contentWindow.document.createElement("link");
+ node.rel = "preload";
+ node.as = "style";
+ if (node.as != "style") {
+ resolve();
+ }
+ node.onload = resolve;
+ node.onerror = reject;
+ node.href = "dummy?t=2&dest=style";
+ frame.contentWindow.document.body.appendChild(node);
+ }).catch(() => {
+ assert_unreached("Fetch errored.");
+ });
+}, 'HTMLLinkElement with rel=preload and as=style fetches with a "style" Request.destination');
+
+// HTMLLinkElement with rel=preload and as=script - script destination
+promise_test(async t => {
+ await new Promise((resolve, reject) => {
+ let node = frame.contentWindow.document.createElement("link");
+ node.rel = "preload";
+ node.as = "script";
+ if (node.as != "script") {
+ resolve();
+ }
+ node.onload = resolve;
+ node.onerror = reject;
+ node.href = "dummy?t=2&dest=script";
+ frame.contentWindow.document.body.appendChild(node);
+ }).catch(() => {
+ assert_unreached("Fetch errored.");
+ });
+}, 'HTMLLinkElement with rel=preload and as=script fetches with a "script" Request.destination');
+
+// HTMLLinkElement with rel=preload and as=font - font destination
+promise_test(async t => {
+ await new Promise((resolve, reject) => {
+ let node = frame.contentWindow.document.createElement("link");
+ node.rel = "preload";
+ node.as = "font";
+ if (node.as != "font") {
+ resolve();
+ }
+ node.onload = resolve;
+ node.onerror = reject;
+ node.href = "dummy?t=2&dest=font";
+ frame.contentWindow.document.body.appendChild(node);
+ }).catch(() => {
+ assert_unreached("Fetch errored.");
+ });
+}, 'HTMLLinkElement with rel=preload and as=font fetches with a "font" Request.destination');
+
+// HTMLLinkElement with rel=preload and as=image - image destination
+promise_test(async t => {
+ await new Promise((resolve, reject) => {
+ let node = frame.contentWindow.document.createElement("link");
+ node.rel = "preload";
+ node.as = "image";
+ if (node.as != "image") {
+ resolve();
+ }
+ node.onload = resolve;
+ node.onerror = reject;
+ node.href = "dummy.png?t=2&dest=image";
+ frame.contentWindow.document.body.appendChild(node);
+ }).catch(() => {
+ assert_unreached("Fetch errored.");
+ });
+}, 'HTMLLinkElement with rel=preload and as=image fetches with a "image" Request.destination');
+
+// HTMLLinkElement with rel=preload and as=audio - audio destination
+promise_test(async t => {
+ await new Promise((resolve, reject) => {
+ let audioURL = getAudioURI("dummy_audio");
+ let node = frame.contentWindow.document.createElement("link");
+ node.rel = "preload";
+ node.as = "audio";
+ if (node.as != "audio") {
+ resolve();
+ }
+ node.onload = resolve;
+ node.onerror = reject;
+ node.href = audioURL + "?dest=audio";
+ frame.contentWindow.document.body.appendChild(node);
+ }).catch(() => {
+ assert_unreached("Fetch errored.");
+ });
+}, 'HTMLLinkElement with rel=preload and as=audio fetches with a "audio" Request.destination');
+
+// HTMLLinkElement with rel=preload and as=video - video destination
+promise_test(async t => {
+ await new Promise((resolve, reject) => {
+ let videoURL = getVideoURI("dummy_video");
+ let node = frame.contentWindow.document.createElement("link");
+ node.rel = "preload";
+ node.as = "video";
+ if (node.as != "video") {
+ resolve();
+ }
+ node.onload = resolve;
+ node.onerror = reject;
+ node.href = videoURL + "?dest=video";
+ frame.contentWindow.document.body.appendChild(node);
+ }).catch(() => {
+ assert_unreached("Fetch errored.");
+ });
+}, 'HTMLLinkElement with rel=preload and as=video fetches with a "video" Request.destination');
+
+// HTMLLinkElement with rel=preload and as=track - track destination
+promise_test(async t => {
+ await new Promise((resolve, reject) => {
+ let node = frame.contentWindow.document.createElement("link");
+ node.rel = "preload";
+ node.as = "track";
+ if (node.as != "track") {
+ resolve();
+ }
+ node.onload = resolve;
+ node.onerror = reject;
+ node.href = "dummy?dest=track";
+ frame.contentWindow.document.body.appendChild(node);
+ }).catch(() => {
+ assert_unreached("Fetch errored.");
+ });
+}, 'HTMLLinkElement with rel=preload and as=track fetches with a "track" Request.destination');
+
+// HTMLLinkElement with rel=preload and as=document - document destination
+promise_test(async t => {
+ await new Promise((resolve, reject) => {
+ let node = frame.contentWindow.document.createElement("link");
+ node.rel = "preload";
+ node.as = "document";
+ if (node.as != "document") {
+ resolve();
+ }
+ node.onload = resolve;
+ node.onerror = reject;
+ node.href = "dummy?dest=document";
+ frame.contentWindow.document.body.appendChild(node);
+ }).catch(() => {
+ assert_unreached("Fetch errored.");
+ });
+}, 'HTMLLinkElement with rel=preload and as=document fetches with a "document" Request.destination');
+
+// HTMLLinkElement with rel=preload and as=worker - worker destination
+promise_test(async t => {
+ await new Promise((resolve, reject) => {
+ let node = frame.contentWindow.document.createElement("link");
+ node.rel = "preload";
+ node.as = "worker";
+ if (node.as != "worker") {
+ resolve();
+ }
+ node.onload = resolve;
+ node.onerror = reject;
+ node.href = "dummy?dest=worker";
+ frame.contentWindow.document.body.appendChild(node);
+ }).catch(() => {
+ assert_unreached("Fetch errored.");
+ });
+}, 'HTMLLinkElement with rel=preload and as=worker fetches with a "worker" Request.destination');
+
+// HTMLLinkElement with rel=preload and as=sharedworker - sharedworker destination
+promise_test(async t => {
+ await new Promise((resolve, reject) => {
+ let node = frame.contentWindow.document.createElement("link");
+ node.rel = "preload";
+ node.as = "sharedworker";
+ if (node.as != "sharedworker") {
+ resolve();
+ }
+ node.onload = resolve;
+ node.onerror = reject;
+ node.href = "dummy?dest=sharedworker";
+ frame.contentWindow.document.body.appendChild(node);
+ }).catch(() => {
+ assert_unreached("Fetch errored.");
+ });
+}, 'HTMLLinkElement with rel=preload and as=sharedworker fetches with a "sharedworker" Request.destination');
+
+// HTMLLinkElement with rel=preload and as=xslt - xslt destination
+promise_test(async t => {
+ await new Promise((resolve, reject) => {
+ let node = frame.contentWindow.document.createElement("link");
+ node.rel = "preload";
+ node.as = "xslt";
+ if (node.as != "xslt") {
+ resolve();
+ }
+ node.onload = resolve;
+ node.onerror = reject;
+ node.href = "dummy?dest=xslt";
+ frame.contentWindow.document.body.appendChild(node);
+ }).catch(() => {
+ assert_unreached("Fetch errored.");
+ });
+}, 'HTMLLinkElement with rel=preload and as=xslt fetches with a "xslt" Request.destination');
+
+// HTMLLinkElement with rel=preload and as=manifest - manifest destination
+promise_test(async t => {
+ await new Promise((resolve, reject) => {
+ let node = frame.contentWindow.document.createElement("link");
+ node.rel = "preload";
+ node.as = "manifest";
+ if (node.as != "manifest") {
+ resolve();
+ }
+ node.onload = resolve;
+ node.onerror = reject;
+ node.href = "dummy?dest=manifest";
+ frame.contentWindow.document.body.appendChild(node);
+ }).catch(() => {
+ assert_unreached("Fetch errored.");
+ });
+}, 'HTMLLinkElement with rel=preload and as=manifest fetches with a "manifest" Request.destination');
+
+</script>
diff --git a/test/wpt/tests/fetch/api/request/destination/resources/dummy b/test/wpt/tests/fetch/api/request/destination/resources/dummy
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/wpt/tests/fetch/api/request/destination/resources/dummy
diff --git a/test/wpt/tests/fetch/api/request/destination/resources/dummy.es b/test/wpt/tests/fetch/api/request/destination/resources/dummy.es
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/wpt/tests/fetch/api/request/destination/resources/dummy.es
diff --git a/test/wpt/tests/fetch/api/request/destination/resources/dummy.es.headers b/test/wpt/tests/fetch/api/request/destination/resources/dummy.es.headers
new file mode 100644
index 0000000..9bb8bad
--- /dev/null
+++ b/test/wpt/tests/fetch/api/request/destination/resources/dummy.es.headers
@@ -0,0 +1 @@
+Content-Type: text/event-stream
diff --git a/test/wpt/tests/fetch/api/request/destination/resources/dummy.html b/test/wpt/tests/fetch/api/request/destination/resources/dummy.html
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/wpt/tests/fetch/api/request/destination/resources/dummy.html
diff --git a/test/wpt/tests/fetch/api/request/destination/resources/dummy.png b/test/wpt/tests/fetch/api/request/destination/resources/dummy.png
new file mode 100644
index 0000000..01c9666
--- /dev/null
+++ b/test/wpt/tests/fetch/api/request/destination/resources/dummy.png
Binary files differ
diff --git a/test/wpt/tests/fetch/api/request/destination/resources/dummy.ttf b/test/wpt/tests/fetch/api/request/destination/resources/dummy.ttf
new file mode 100644
index 0000000..9023592
--- /dev/null
+++ b/test/wpt/tests/fetch/api/request/destination/resources/dummy.ttf
Binary files differ
diff --git a/test/wpt/tests/fetch/api/request/destination/resources/dummy_audio.mp3 b/test/wpt/tests/fetch/api/request/destination/resources/dummy_audio.mp3
new file mode 100644
index 0000000..0091330
--- /dev/null
+++ b/test/wpt/tests/fetch/api/request/destination/resources/dummy_audio.mp3
Binary files differ
diff --git a/test/wpt/tests/fetch/api/request/destination/resources/dummy_audio.oga b/test/wpt/tests/fetch/api/request/destination/resources/dummy_audio.oga
new file mode 100644
index 0000000..239ad2b
--- /dev/null
+++ b/test/wpt/tests/fetch/api/request/destination/resources/dummy_audio.oga
Binary files differ
diff --git a/test/wpt/tests/fetch/api/request/destination/resources/dummy_video.mp4 b/test/wpt/tests/fetch/api/request/destination/resources/dummy_video.mp4
new file mode 100644
index 0000000..7022e75
--- /dev/null
+++ b/test/wpt/tests/fetch/api/request/destination/resources/dummy_video.mp4
Binary files differ
diff --git a/test/wpt/tests/fetch/api/request/destination/resources/dummy_video.ogv b/test/wpt/tests/fetch/api/request/destination/resources/dummy_video.ogv
new file mode 100644
index 0000000..de99616
--- /dev/null
+++ b/test/wpt/tests/fetch/api/request/destination/resources/dummy_video.ogv
Binary files differ
diff --git a/test/wpt/tests/fetch/api/request/destination/resources/dummy_video.webm b/test/wpt/tests/fetch/api/request/destination/resources/dummy_video.webm
new file mode 100644
index 0000000..c3d433a
--- /dev/null
+++ b/test/wpt/tests/fetch/api/request/destination/resources/dummy_video.webm
Binary files differ
diff --git a/test/wpt/tests/fetch/api/request/destination/resources/empty.https.html b/test/wpt/tests/fetch/api/request/destination/resources/empty.https.html
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/wpt/tests/fetch/api/request/destination/resources/empty.https.html
diff --git a/test/wpt/tests/fetch/api/request/destination/resources/fetch-destination-worker-frame.js b/test/wpt/tests/fetch/api/request/destination/resources/fetch-destination-worker-frame.js
new file mode 100644
index 0000000..b69de0b
--- /dev/null
+++ b/test/wpt/tests/fetch/api/request/destination/resources/fetch-destination-worker-frame.js
@@ -0,0 +1,20 @@
+self.addEventListener('fetch', function(event) {
+ if (event.request.url.includes('dummy')) {
+ event.waitUntil(async function() {
+ let destination = new URL(event.request.url).searchParams.get("dest");
+ let clients = await self.clients.matchAll({"includeUncontrolled": true});
+ clients.forEach(function(client) {
+ if (client.url.includes("fetch-destination-frame")) {
+ if (event.request.destination == destination) {
+ client.postMessage("PASS");
+ } else {
+ client.postMessage("FAIL");
+ }
+ }
+ })
+ }());
+ }
+ event.respondWith(fetch(event.request));
+});
+
+
diff --git a/test/wpt/tests/fetch/api/request/destination/resources/fetch-destination-worker-iframe.js b/test/wpt/tests/fetch/api/request/destination/resources/fetch-destination-worker-iframe.js
new file mode 100644
index 0000000..7634583
--- /dev/null
+++ b/test/wpt/tests/fetch/api/request/destination/resources/fetch-destination-worker-iframe.js
@@ -0,0 +1,20 @@
+self.addEventListener('fetch', function(event) {
+ if (event.request.url.includes('dummy')) {
+ event.waitUntil(async function() {
+ let destination = new URL(event.request.url).searchParams.get("dest");
+ let clients = await self.clients.matchAll({"includeUncontrolled": true});
+ clients.forEach(function(client) {
+ if (client.url.includes("fetch-destination-iframe")) {
+ if (event.request.destination == destination) {
+ client.postMessage("PASS");
+ } else {
+ client.postMessage("FAIL");
+ }
+ }
+ })
+ }());
+ }
+ event.respondWith(fetch(event.request));
+});
+
+
diff --git a/test/wpt/tests/fetch/api/request/destination/resources/fetch-destination-worker-no-load-event.js b/test/wpt/tests/fetch/api/request/destination/resources/fetch-destination-worker-no-load-event.js
new file mode 100644
index 0000000..a583b12
--- /dev/null
+++ b/test/wpt/tests/fetch/api/request/destination/resources/fetch-destination-worker-no-load-event.js
@@ -0,0 +1,20 @@
+self.addEventListener('fetch', function(event) {
+ const url = event.request.url;
+ if (url.includes('dummy') && url.includes('?')) {
+ event.waitUntil(async function() {
+ let destination = new URL(url).searchParams.get("dest");
+ var result = "FAIL";
+ if (event.request.destination == destination ||
+ (event.request.destination == "empty" && destination == "")) {
+ result = "PASS";
+ }
+ let cl = await clients.matchAll({includeUncontrolled: true});
+ for (i = 0; i < cl.length; i++) {
+ cl[i].postMessage(result);
+ }
+ }())
+ }
+ event.respondWith(fetch(event.request));
+});
+
+
diff --git a/test/wpt/tests/fetch/api/request/destination/resources/fetch-destination-worker.js b/test/wpt/tests/fetch/api/request/destination/resources/fetch-destination-worker.js
new file mode 100644
index 0000000..904009c
--- /dev/null
+++ b/test/wpt/tests/fetch/api/request/destination/resources/fetch-destination-worker.js
@@ -0,0 +1,12 @@
+self.addEventListener('fetch', function(event) {
+ if (event.request.url.includes('dummy')) {
+ let destination = new URL(event.request.url).searchParams.get("dest");
+ if (event.request.destination == destination ||
+ (event.request.destination == "empty" && destination == "")) {
+ event.respondWith(fetch(event.request));
+ } else {
+ event.respondWith(Response.error());
+ }
+ }
+});
+
diff --git a/test/wpt/tests/fetch/api/request/destination/resources/importer.js b/test/wpt/tests/fetch/api/request/destination/resources/importer.js
new file mode 100644
index 0000000..9568474
--- /dev/null
+++ b/test/wpt/tests/fetch/api/request/destination/resources/importer.js
@@ -0,0 +1 @@
+importScripts("dummy?t=importScripts&dest=script");
diff --git a/test/wpt/tests/fetch/api/request/forbidden-method.any.js b/test/wpt/tests/fetch/api/request/forbidden-method.any.js
new file mode 100644
index 0000000..eb13f37
--- /dev/null
+++ b/test/wpt/tests/fetch/api/request/forbidden-method.any.js
@@ -0,0 +1,13 @@
+// META: global=window,worker
+
+// https://fetch.spec.whatwg.org/#forbidden-method
+for (const method of [
+ 'CONNECT', 'TRACE', 'TRACK',
+ 'connect', 'trace', 'track'
+ ]) {
+ test(function() {
+ assert_throws_js(TypeError,
+ function() { new Request('./', {method: method}); }
+ );
+ }, 'Request() with a forbidden method ' + method + ' must throw.');
+}
diff --git a/test/wpt/tests/fetch/api/request/multi-globals/construct-in-detached-frame.window.js b/test/wpt/tests/fetch/api/request/multi-globals/construct-in-detached-frame.window.js
new file mode 100644
index 0000000..b0d6ba5
--- /dev/null
+++ b/test/wpt/tests/fetch/api/request/multi-globals/construct-in-detached-frame.window.js
@@ -0,0 +1,11 @@
+// This is a regression test for Chromium issue https://crbug.com/1427266.
+test(() => {
+ const iframe = document.createElement('iframe');
+ document.body.append(iframe);
+ const otherRequest = iframe.contentWindow.Request;
+ iframe.remove();
+ const r1 = new otherRequest('resource', { method: 'POST', body: 'string' });
+ const r2 = new otherRequest(r1);
+ assert_true(r1.bodyUsed);
+ assert_false(r2.bodyUsed);
+}, 'creating a request from another request in a detached realm should work');
diff --git a/test/wpt/tests/fetch/api/request/multi-globals/current/current.html b/test/wpt/tests/fetch/api/request/multi-globals/current/current.html
new file mode 100644
index 0000000..9bb6e0b
--- /dev/null
+++ b/test/wpt/tests/fetch/api/request/multi-globals/current/current.html
@@ -0,0 +1,3 @@
+<!DOCTYPE html>
+<title>Current page used as a test helper</title>
+<base href="success/">
diff --git a/test/wpt/tests/fetch/api/request/multi-globals/incumbent/incumbent.html b/test/wpt/tests/fetch/api/request/multi-globals/incumbent/incumbent.html
new file mode 100644
index 0000000..a885b8a
--- /dev/null
+++ b/test/wpt/tests/fetch/api/request/multi-globals/incumbent/incumbent.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<title>Incumbent page used as a test helper</title>
+
+<iframe src="../current/current.html" id="c"></iframe>
+
+<script>
+'use strict';
+
+window.createRequest = (...args) => {
+ const current = document.querySelector('#c').contentWindow;
+ return new current.Request(...args);
+};
+
+</script>
diff --git a/test/wpt/tests/fetch/api/request/multi-globals/url-parsing.html b/test/wpt/tests/fetch/api/request/multi-globals/url-parsing.html
new file mode 100644
index 0000000..df60e72
--- /dev/null
+++ b/test/wpt/tests/fetch/api/request/multi-globals/url-parsing.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<title>Request constructor URL parsing, with multiple globals in play</title>
+<link rel="help" href="https://fetch.spec.whatwg.org/#dom-request">
+<link rel="author" title="Domenic Denicola" href="mailto:d@domenic.me">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<!-- This is the entry global -->
+
+<iframe src="incumbent/incumbent.html"></iframe>
+
+<script>
+'use strict';
+
+const loadPromise = new Promise(resolve => {
+ window.addEventListener("load", () => resolve());
+});
+
+promise_test(() => {
+ return loadPromise.then(() => {
+ const req = document.querySelector('iframe').contentWindow.createRequest("url");
+
+ assert_equals(req.url, new URL("current/success/url", location.href).href);
+ });
+}, "should parse the URL relative to the current settings object");
+
+</script>
diff --git a/test/wpt/tests/fetch/api/request/request-bad-port.any.js b/test/wpt/tests/fetch/api/request/request-bad-port.any.js
new file mode 100644
index 0000000..b0684d4
--- /dev/null
+++ b/test/wpt/tests/fetch/api/request/request-bad-port.any.js
@@ -0,0 +1,92 @@
+// META: global=window,worker
+
+// list of bad ports according to
+// https://fetch.spec.whatwg.org/#port-blocking
+var BLOCKED_PORTS_LIST = [
+ 1, // tcpmux
+ 7, // echo
+ 9, // discard
+ 11, // systat
+ 13, // daytime
+ 15, // netstat
+ 17, // qotd
+ 19, // chargen
+ 20, // ftp-data
+ 21, // ftp
+ 22, // ssh
+ 23, // telnet
+ 25, // smtp
+ 37, // time
+ 42, // name
+ 43, // nicname
+ 53, // domain
+ 69, // tftp
+ 77, // priv-rjs
+ 79, // finger
+ 87, // ttylink
+ 95, // supdup
+ 101, // hostriame
+ 102, // iso-tsap
+ 103, // gppitnp
+ 104, // acr-nema
+ 109, // pop2
+ 110, // pop3
+ 111, // sunrpc
+ 113, // auth
+ 115, // sftp
+ 117, // uucp-path
+ 119, // nntp
+ 123, // ntp
+ 135, // loc-srv / epmap
+ 137, // netbios-ns
+ 139, // netbios-ssn
+ 143, // imap2
+ 161, // snmp
+ 179, // bgp
+ 389, // ldap
+ 427, // afp (alternate)
+ 465, // smtp (alternate)
+ 512, // print / exec
+ 513, // login
+ 514, // shell
+ 515, // printer
+ 526, // tempo
+ 530, // courier
+ 531, // chat
+ 532, // netnews
+ 540, // uucp
+ 548, // afp
+ 554, // rtsp
+ 556, // remotefs
+ 563, // nntp+ssl
+ 587, // smtp (outgoing)
+ 601, // syslog-conn
+ 636, // ldap+ssl
+ 989, // ftps-data
+ 990, // ftps
+ 993, // ldap+ssl
+ 995, // pop3+ssl
+ 1719, // h323gatestat
+ 1720, // h323hostcall
+ 1723, // pptp
+ 2049, // nfs
+ 3659, // apple-sasl
+ 4045, // lockd
+ 5060, // sip
+ 5061, // sips
+ 6000, // x11
+ 6566, // sane-port
+ 6665, // irc (alternate)
+ 6666, // irc (alternate)
+ 6667, // irc (default)
+ 6668, // irc (alternate)
+ 6669, // irc (alternate)
+ 6697, // irc+tls
+ 10080, // amanda
+];
+
+BLOCKED_PORTS_LIST.map(function(a){
+ promise_test(function(t){
+ return promise_rejects_js(t, TypeError, fetch("http://example.com:" + a))
+ }, 'Request on bad port ' + a + ' should throw TypeError.');
+});
diff --git a/test/wpt/tests/fetch/api/request/request-cache-default-conditional.any.js b/test/wpt/tests/fetch/api/request/request-cache-default-conditional.any.js
new file mode 100644
index 0000000..c5b2001
--- /dev/null
+++ b/test/wpt/tests/fetch/api/request/request-cache-default-conditional.any.js
@@ -0,0 +1,170 @@
+// META: global=window,worker
+// META: title=Request cache - default with conditional requests
+// META: timeout=long
+// META: script=/common/utils.js
+// META: script=/common/get-host-info.sub.js
+// META: script=request-cache.js
+
+var tests = [
+ {
+ name: 'RequestCache "default" mode with an If-Modified-Since header (following a request without additional headers) is treated similarly to "no-store"',
+ state: "stale",
+ request_cache: ["default", "default"],
+ request_headers: [{}, {"If-Modified-Since": now.toGMTString()}],
+ expected_validation_headers: [false, false],
+ expected_no_cache_headers: [false, true],
+ },
+ {
+ name: 'RequestCache "default" mode with an If-Modified-Since header (following a request without additional headers) is treated similarly to "no-store"',
+ state: "fresh",
+ request_cache: ["default", "default"],
+ request_headers: [{}, {"If-Modified-Since": now.toGMTString()}],
+ expected_validation_headers: [false, false],
+ expected_no_cache_headers: [false, true],
+ },
+ {
+ name: 'RequestCache "default" mode with an If-Modified-Since header is treated similarly to "no-store"',
+ state: "stale",
+ request_cache: ["default", "default"],
+ request_headers: [{"If-Modified-Since": now.toGMTString()}, {}],
+ expected_validation_headers: [false, false],
+ expected_no_cache_headers: [true, false],
+ },
+ {
+ name: 'RequestCache "default" mode with an If-Modified-Since header is treated similarly to "no-store"',
+ state: "fresh",
+ request_cache: ["default", "default"],
+ request_headers: [{"If-Modified-Since": now.toGMTString()}, {}],
+ expected_validation_headers: [false, false],
+ expected_no_cache_headers: [true, false],
+ },
+ {
+ name: 'RequestCache "default" mode with an If-None-Match header (following a request without additional headers) is treated similarly to "no-store"',
+ state: "stale",
+ request_cache: ["default", "default"],
+ request_headers: [{}, {"If-None-Match": '"foo"'}],
+ expected_validation_headers: [false, false],
+ expected_no_cache_headers: [false, true],
+ },
+ {
+ name: 'RequestCache "default" mode with an If-None-Match header (following a request without additional headers) is treated similarly to "no-store"',
+ state: "fresh",
+ request_cache: ["default", "default"],
+ request_headers: [{}, {"If-None-Match": '"foo"'}],
+ expected_validation_headers: [false, false],
+ expected_no_cache_headers: [false, true],
+ },
+ {
+ name: 'RequestCache "default" mode with an If-None-Match header is treated similarly to "no-store"',
+ state: "stale",
+ request_cache: ["default", "default"],
+ request_headers: [{"If-None-Match": '"foo"'}, {}],
+ expected_validation_headers: [false, false],
+ expected_no_cache_headers: [true, false],
+ },
+ {
+ name: 'RequestCache "default" mode with an If-None-Match header is treated similarly to "no-store"',
+ state: "fresh",
+ request_cache: ["default", "default"],
+ request_headers: [{"If-None-Match": '"foo"'}, {}],
+ expected_validation_headers: [false, false],
+ expected_no_cache_headers: [true, false],
+ },
+ {
+ name: 'RequestCache "default" mode with an If-Unmodified-Since header (following a request without additional headers) is treated similarly to "no-store"',
+ state: "stale",
+ request_cache: ["default", "default"],
+ request_headers: [{}, {"If-Unmodified-Since": now.toGMTString()}],
+ expected_validation_headers: [false, false],
+ expected_no_cache_headers: [false, true],
+ },
+ {
+ name: 'RequestCache "default" mode with an If-Unmodified-Since header (following a request without additional headers) is treated similarly to "no-store"',
+ state: "fresh",
+ request_cache: ["default", "default"],
+ request_headers: [{}, {"If-Unmodified-Since": now.toGMTString()}],
+ expected_validation_headers: [false, false],
+ expected_no_cache_headers: [false, true],
+ },
+ {
+ name: 'RequestCache "default" mode with an If-Unmodified-Since header is treated similarly to "no-store"',
+ state: "stale",
+ request_cache: ["default", "default"],
+ request_headers: [{"If-Unmodified-Since": now.toGMTString()}, {}],
+ expected_validation_headers: [false, false],
+ expected_no_cache_headers: [true, false],
+ },
+ {
+ name: 'RequestCache "default" mode with an If-Unmodified-Since header is treated similarly to "no-store"',
+ state: "fresh",
+ request_cache: ["default", "default"],
+ request_headers: [{"If-Unmodified-Since": now.toGMTString()}, {}],
+ expected_validation_headers: [false, false],
+ expected_no_cache_headers: [true, false],
+ },
+ {
+ name: 'RequestCache "default" mode with an If-Match header (following a request without additional headers) is treated similarly to "no-store"',
+ state: "stale",
+ request_cache: ["default", "default"],
+ request_headers: [{}, {"If-Match": '"foo"'}],
+ expected_validation_headers: [false, false],
+ expected_no_cache_headers: [false, true],
+ },
+ {
+ name: 'RequestCache "default" mode with an If-Match header (following a request without additional headers) is treated similarly to "no-store"',
+ state: "fresh",
+ request_cache: ["default", "default"],
+ request_headers: [{}, {"If-Match": '"foo"'}],
+ expected_validation_headers: [false, false],
+ expected_no_cache_headers: [false, true],
+ },
+ {
+ name: 'RequestCache "default" mode with an If-Match header is treated similarly to "no-store"',
+ state: "stale",
+ request_cache: ["default", "default"],
+ request_headers: [{"If-Match": '"foo"'}, {}],
+ expected_validation_headers: [false, false],
+ expected_no_cache_headers: [true, false],
+ },
+ {
+ name: 'RequestCache "default" mode with an If-Match header is treated similarly to "no-store"',
+ state: "fresh",
+ request_cache: ["default", "default"],
+ request_headers: [{"If-Match": '"foo"'}, {}],
+ expected_validation_headers: [false, false],
+ expected_no_cache_headers: [true, false],
+ },
+ {
+ name: 'RequestCache "default" mode with an If-Range header (following a request without additional headers) is treated similarly to "no-store"',
+ state: "stale",
+ request_cache: ["default", "default"],
+ request_headers: [{}, {"If-Range": '"foo"'}],
+ expected_validation_headers: [false, false],
+ expected_no_cache_headers: [false, true],
+ },
+ {
+ name: 'RequestCache "default" mode with an If-Range header (following a request without additional headers) is treated similarly to "no-store"',
+ state: "fresh",
+ request_cache: ["default", "default"],
+ request_headers: [{}, {"If-Range": '"foo"'}],
+ expected_validation_headers: [false, false],
+ expected_no_cache_headers: [false, true],
+ },
+ {
+ name: 'RequestCache "default" mode with an If-Range header is treated similarly to "no-store"',
+ state: "stale",
+ request_cache: ["default", "default"],
+ request_headers: [{"If-Range": '"foo"'}, {}],
+ expected_validation_headers: [false, false],
+ expected_no_cache_headers: [true, false],
+ },
+ {
+ name: 'RequestCache "default" mode with an If-Range header is treated similarly to "no-store"',
+ state: "fresh",
+ request_cache: ["default", "default"],
+ request_headers: [{"If-Range": '"foo"'}, {}],
+ expected_validation_headers: [false, false],
+ expected_no_cache_headers: [true, false],
+ },
+];
+run_tests(tests);
diff --git a/test/wpt/tests/fetch/api/request/request-cache-default.any.js b/test/wpt/tests/fetch/api/request/request-cache-default.any.js
new file mode 100644
index 0000000..dfa8369
--- /dev/null
+++ b/test/wpt/tests/fetch/api/request/request-cache-default.any.js
@@ -0,0 +1,39 @@
+// META: global=window,worker
+// META: title=Request cache - default
+// META: script=/common/utils.js
+// META: script=/common/get-host-info.sub.js
+// META: script=request-cache.js
+
+var tests = [
+ {
+ name: 'RequestCache "default" mode checks the cache for previously cached content and goes to the network for stale responses',
+ state: "stale",
+ request_cache: ["default", "default"],
+ expected_validation_headers: [false, true],
+ expected_no_cache_headers: [false, false],
+ },
+ {
+ name: 'RequestCache "default" mode checks the cache for previously cached content and avoids going to the network if a fresh response exists',
+ state: "fresh",
+ request_cache: ["default", "default"],
+ expected_validation_headers: [false],
+ expected_no_cache_headers: [false],
+ },
+ {
+ name: 'Responses with the "Cache-Control: no-store" header are not stored in the cache',
+ state: "stale",
+ cache_control: "no-store",
+ request_cache: ["default", "default"],
+ expected_validation_headers: [false, false],
+ expected_no_cache_headers: [false, false],
+ },
+ {
+ name: 'Responses with the "Cache-Control: no-store" header are not stored in the cache',
+ state: "fresh",
+ cache_control: "no-store",
+ request_cache: ["default", "default"],
+ expected_validation_headers: [false, false],
+ expected_no_cache_headers: [false, false],
+ },
+];
+run_tests(tests);
diff --git a/test/wpt/tests/fetch/api/request/request-cache-force-cache.any.js b/test/wpt/tests/fetch/api/request/request-cache-force-cache.any.js
new file mode 100644
index 0000000..00dce09
--- /dev/null
+++ b/test/wpt/tests/fetch/api/request/request-cache-force-cache.any.js
@@ -0,0 +1,67 @@
+// META: global=window,worker
+// META: title=Request cache - force-cache
+// META: script=/common/utils.js
+// META: script=/common/get-host-info.sub.js
+// META: script=request-cache.js
+
+var tests = [
+ {
+ name: 'RequestCache "force-cache" mode checks the cache for previously cached content and avoid revalidation for stale responses',
+ state: "stale",
+ request_cache: ["default", "force-cache"],
+ expected_validation_headers: [false],
+ expected_no_cache_headers: [false],
+ },
+ {
+ name: 'RequestCache "force-cache" mode checks the cache for previously cached content and avoid revalidation for fresh responses',
+ state: "fresh",
+ request_cache: ["default", "force-cache"],
+ expected_validation_headers: [false],
+ expected_no_cache_headers: [false],
+ },
+ {
+ name: 'RequestCache "force-cache" mode checks the cache for previously cached content and goes to the network if a cached response is not found',
+ state: "stale",
+ request_cache: ["force-cache"],
+ expected_validation_headers: [false],
+ expected_no_cache_headers: [false],
+ },
+ {
+ name: 'RequestCache "force-cache" mode checks the cache for previously cached content and goes to the network if a cached response is not found',
+ state: "fresh",
+ request_cache: ["force-cache"],
+ expected_validation_headers: [false],
+ expected_no_cache_headers: [false],
+ },
+ {
+ name: 'RequestCache "force-cache" mode checks the cache for previously cached content and goes to the network if a cached response would vary',
+ state: "stale",
+ vary: "*",
+ request_cache: ["default", "force-cache"],
+ expected_validation_headers: [false, true],
+ expected_no_cache_headers: [false, false],
+ },
+ {
+ name: 'RequestCache "force-cache" mode checks the cache for previously cached content and goes to the network if a cached response would vary',
+ state: "fresh",
+ vary: "*",
+ request_cache: ["default", "force-cache"],
+ expected_validation_headers: [false, true],
+ expected_no_cache_headers: [false, false],
+ },
+ {
+ name: 'RequestCache "force-cache" stores the response in the cache if it goes to the network',
+ state: "stale",
+ request_cache: ["force-cache", "default"],
+ expected_validation_headers: [false, true],
+ expected_no_cache_headers: [false, false],
+ },
+ {
+ name: 'RequestCache "force-cache" stores the response in the cache if it goes to the network',
+ state: "fresh",
+ request_cache: ["force-cache", "default"],
+ expected_validation_headers: [false],
+ expected_no_cache_headers: [false],
+ },
+];
+run_tests(tests);
diff --git a/test/wpt/tests/fetch/api/request/request-cache-no-cache.any.js b/test/wpt/tests/fetch/api/request/request-cache-no-cache.any.js
new file mode 100644
index 0000000..41fc22b
--- /dev/null
+++ b/test/wpt/tests/fetch/api/request/request-cache-no-cache.any.js
@@ -0,0 +1,25 @@
+// META: global=window,worker
+// META: title=Request cache : no-cache
+// META: script=/common/utils.js
+// META: script=/common/get-host-info.sub.js
+// META: script=request-cache.js
+
+var tests = [
+ {
+ name: 'RequestCache "no-cache" mode revalidates stale responses found in the cache',
+ state: "stale",
+ request_cache: ["default", "no-cache"],
+ expected_validation_headers: [false, true],
+ expected_no_cache_headers: [false, false],
+ expected_max_age_headers: [false, true],
+ },
+ {
+ name: 'RequestCache "no-cache" mode revalidates fresh responses found in the cache',
+ state: "fresh",
+ request_cache: ["default", "no-cache"],
+ expected_validation_headers: [false, true],
+ expected_no_cache_headers: [false, false],
+ expected_max_age_headers: [false, true],
+ },
+];
+run_tests(tests);
diff --git a/test/wpt/tests/fetch/api/request/request-cache-no-store.any.js b/test/wpt/tests/fetch/api/request/request-cache-no-store.any.js
new file mode 100644
index 0000000..9a28718
--- /dev/null
+++ b/test/wpt/tests/fetch/api/request/request-cache-no-store.any.js
@@ -0,0 +1,37 @@
+// META: global=window,worker
+// META: title=Request cache - no store
+// META: script=/common/utils.js
+// META: script=/common/get-host-info.sub.js
+// META: script=request-cache.js
+
+var tests = [
+ {
+ name: 'RequestCache "no-store" mode does not check the cache for previously cached content and goes to the network regardless',
+ state: "stale",
+ request_cache: ["default", "no-store"],
+ expected_validation_headers: [false, false],
+ expected_no_cache_headers: [false, true],
+ },
+ {
+ name: 'RequestCache "no-store" mode does not check the cache for previously cached content and goes to the network regardless',
+ state: "fresh",
+ request_cache: ["default", "no-store"],
+ expected_validation_headers: [false, false],
+ expected_no_cache_headers: [false, true],
+ },
+ {
+ name: 'RequestCache "no-store" mode does not store the response in the cache',
+ state: "stale",
+ request_cache: ["no-store", "default"],
+ expected_validation_headers: [false, false],
+ expected_no_cache_headers: [true, false],
+ },
+ {
+ name: 'RequestCache "no-store" mode does not store the response in the cache',
+ state: "fresh",
+ request_cache: ["no-store", "default"],
+ expected_validation_headers: [false, false],
+ expected_no_cache_headers: [true, false],
+ },
+];
+run_tests(tests);
diff --git a/test/wpt/tests/fetch/api/request/request-cache-only-if-cached.any.js b/test/wpt/tests/fetch/api/request/request-cache-only-if-cached.any.js
new file mode 100644
index 0000000..1305787
--- /dev/null
+++ b/test/wpt/tests/fetch/api/request/request-cache-only-if-cached.any.js
@@ -0,0 +1,66 @@
+// META: global=window,dedicatedworker,sharedworker
+// META: title=Request cache - only-if-cached
+// META: script=/common/utils.js
+// META: script=/common/get-host-info.sub.js
+// META: script=request-cache.js
+
+// FIXME: avoid mixed content requests to enable service worker global
+var tests = [
+ {
+ name: 'RequestCache "only-if-cached" mode checks the cache for previously cached content and avoids revalidation for stale responses',
+ state: "stale",
+ request_cache: ["default", "only-if-cached"],
+ expected_validation_headers: [false],
+ expected_no_cache_headers: [false]
+ },
+ {
+ name: 'RequestCache "only-if-cached" mode checks the cache for previously cached content and avoids revalidation for fresh responses',
+ state: "fresh",
+ request_cache: ["default", "only-if-cached"],
+ expected_validation_headers: [false],
+ expected_no_cache_headers: [false]
+ },
+ {
+ name: 'RequestCache "only-if-cached" mode checks the cache for previously cached content and does not go to the network if a cached response is not found',
+ state: "fresh",
+ request_cache: ["only-if-cached"],
+ response: ["error"],
+ expected_validation_headers: [],
+ expected_no_cache_headers: []
+ },
+ {
+ name: 'RequestCache "only-if-cached" (with "same-origin") uses cached same-origin redirects to same-origin content',
+ state: "fresh",
+ request_cache: ["default", "only-if-cached"],
+ redirect: "same-origin",
+ expected_validation_headers: [false, false],
+ expected_no_cache_headers: [false, false],
+ },
+ {
+ name: 'RequestCache "only-if-cached" (with "same-origin") uses cached same-origin redirects to same-origin content',
+ state: "stale",
+ request_cache: ["default", "only-if-cached"],
+ redirect: "same-origin",
+ expected_validation_headers: [false, false],
+ expected_no_cache_headers: [false, false],
+ },
+ {
+ name: 'RequestCache "only-if-cached" (with "same-origin") does not follow redirects across origins and rejects',
+ state: "fresh",
+ request_cache: ["default", "only-if-cached"],
+ redirect: "cross-origin",
+ response: [null, "error"],
+ expected_validation_headers: [false, false],
+ expected_no_cache_headers: [false, false],
+ },
+ {
+ name: 'RequestCache "only-if-cached" (with "same-origin") does not follow redirects across origins and rejects',
+ state: "stale",
+ request_cache: ["default", "only-if-cached"],
+ redirect: "cross-origin",
+ response: [null, "error"],
+ expected_validation_headers: [false, false],
+ expected_no_cache_headers: [false, false],
+ },
+];
+run_tests(tests);
diff --git a/test/wpt/tests/fetch/api/request/request-cache-reload.any.js b/test/wpt/tests/fetch/api/request/request-cache-reload.any.js
new file mode 100644
index 0000000..c7bfffb
--- /dev/null
+++ b/test/wpt/tests/fetch/api/request/request-cache-reload.any.js
@@ -0,0 +1,51 @@
+// META: global=window,worker
+// META: title=Request cache - reload
+// META: script=/common/utils.js
+// META: script=/common/get-host-info.sub.js
+// META: script=request-cache.js
+
+var tests = [
+ {
+ name: 'RequestCache "reload" mode does not check the cache for previously cached content and goes to the network regardless',
+ state: "stale",
+ request_cache: ["default", "reload"],
+ expected_validation_headers: [false, false],
+ expected_no_cache_headers: [false, true],
+ },
+ {
+ name: 'RequestCache "reload" mode does not check the cache for previously cached content and goes to the network regardless',
+ state: "fresh",
+ request_cache: ["default", "reload"],
+ expected_validation_headers: [false, false],
+ expected_no_cache_headers: [false, true],
+ },
+ {
+ name: 'RequestCache "reload" mode does store the response in the cache',
+ state: "stale",
+ request_cache: ["reload", "default"],
+ expected_validation_headers: [false, true],
+ expected_no_cache_headers: [true, false],
+ },
+ {
+ name: 'RequestCache "reload" mode does store the response in the cache',
+ state: "fresh",
+ request_cache: ["reload", "default"],
+ expected_validation_headers: [false],
+ expected_no_cache_headers: [true],
+ },
+ {
+ name: 'RequestCache "reload" mode does store the response in the cache even if a previous response is already stored',
+ state: "stale",
+ request_cache: ["default", "reload", "default"],
+ expected_validation_headers: [false, false, true],
+ expected_no_cache_headers: [false, true, false],
+ },
+ {
+ name: 'RequestCache "reload" mode does store the response in the cache even if a previous response is already stored',
+ state: "fresh",
+ request_cache: ["default", "reload", "default"],
+ expected_validation_headers: [false, false],
+ expected_no_cache_headers: [false, true],
+ },
+];
+run_tests(tests);
diff --git a/test/wpt/tests/fetch/api/request/request-cache.js b/test/wpt/tests/fetch/api/request/request-cache.js
new file mode 100644
index 0000000..f2fbecf
--- /dev/null
+++ b/test/wpt/tests/fetch/api/request/request-cache.js
@@ -0,0 +1,223 @@
+/**
+ * Each test is run twice: once using etag/If-None-Match and once with
+ * date/If-Modified-Since. Each test run gets its own URL and randomized
+ * content and operates independently.
+ *
+ * The test steps are run with request_cache.length fetch requests issued
+ * and their immediate results sanity-checked. The cache.py server script
+ * stashes an entry containing any If-None-Match, If-Modified-Since, Pragma,
+ * and Cache-Control observed headers for each request it receives. When
+ * the test fetches have run, this state is retrieved from cache.py and the
+ * expected_* lists are checked, including their length.
+ *
+ * This means that if a request_* fetch is expected to hit the cache and not
+ * touch the network, then there will be no entry for it in the expect_*
+ * lists. AKA (request_cache.length - expected_validation_headers.length)
+ * should equal the number of cache hits that didn't touch the network.
+ *
+ * Test dictionary keys:
+ * - state: required string that determines whether the Expires response for
+ * the fetched document should be set in the future ("fresh") or past
+ * ("stale").
+ * - vary: optional string to be passed to the server for it to quote back
+ * in a Vary header on the response to us.
+ * - cache_control: optional string to be passed to the server for it to
+ * quote back in a Cache-Control header on the response to us.
+ * - redirect: optional string "same-origin" or "cross-origin". If
+ * provided, the server will issue an absolute redirect to the script on
+ * the same or a different origin, as appropriate. The redirected
+ * location is the script with the redirect parameter removed, so the
+ * content/state/etc. will be as if you hadn't specified a redirect.
+ * - request_cache: required array of cache modes to use (via `cache`).
+ * - request_headers: optional array of explicit fetch `headers` arguments.
+ * If provided, the server will log an empty dictionary for each request
+ * instead of the request headers it would normally log.
+ * - response: optional array of specialized response handling. Right now,
+ * "error" array entries indicate a network error response is expected
+ * which will reject with a TypeError.
+ * - expected_validation_headers: required boolean array indicating whether
+ * the server should have seen an If-None-Match/If-Modified-Since header
+ * in the request.
+ * - expected_no_cache_headers: required boolean array indicating whether
+ * the server should have seen Pragma/Cache-control:no-cache headers in
+ * the request.
+ * - expected_max_age_headers: optional boolean array indicating whether
+ * the server should have seen a Cache-Control:max-age=0 header in the
+ * request.
+ */
+
+var now = new Date();
+
+function base_path() {
+ return location.pathname.replace(/\/[^\/]*$/, '/');
+}
+function make_url(uuid, id, value, content, info) {
+ var dates = {
+ fresh: new Date(now.getFullYear() + 1, now.getMonth(), now.getDay()).toGMTString(),
+ stale: new Date(now.getFullYear() - 1, now.getMonth(), now.getDay()).toGMTString(),
+ };
+ var vary = "";
+ if ("vary" in info) {
+ vary = "&vary=" + info.vary;
+ }
+ var cache_control = "";
+ if ("cache_control" in info) {
+ cache_control = "&cache_control=" + info.cache_control;
+ }
+ var redirect = "";
+
+ var ignore_request_headers = "";
+ if ("request_headers" in info) {
+ // Ignore the request headers that we send since they may be synthesized by the test.
+ ignore_request_headers = "&ignore";
+ }
+ var url_sans_redirect = "resources/cache.py?token=" + uuid +
+ "&content=" + content +
+ "&" + id + "=" + value +
+ "&expires=" + dates[info.state] +
+ vary + cache_control + ignore_request_headers;
+ // If there's a redirect, the target is the script without any redirect at
+ // either the same domain or a different domain.
+ if ("redirect" in info) {
+ var host_info = get_host_info();
+ var origin;
+ switch (info.redirect) {
+ case "same-origin":
+ origin = host_info['HTTP_ORIGIN'];
+ break;
+ case "cross-origin":
+ origin = host_info['HTTP_REMOTE_ORIGIN'];
+ break;
+ }
+ var redirected_url = origin + base_path() + url_sans_redirect;
+ return url_sans_redirect + "&redirect=" + encodeURIComponent(redirected_url);
+ } else {
+ return url_sans_redirect;
+ }
+}
+function expected_status(type, identifier, init) {
+ if (type == "date" &&
+ init.headers &&
+ init.headers["If-Modified-Since"] == identifier) {
+ // The server will respond with a 304 in this case.
+ return [304, "Not Modified"];
+ }
+ return [200, "OK"];
+}
+function expected_response_text(type, identifier, init, content) {
+ if (type == "date" &&
+ init.headers &&
+ init.headers["If-Modified-Since"] == identifier) {
+ // The server will respond with a 304 in this case.
+ return "";
+ }
+ return content;
+}
+function server_state(uuid) {
+ return fetch("resources/cache.py?querystate&token=" + uuid)
+ .then(function(response) {
+ return response.text();
+ }).then(function(text) {
+ // null will be returned if the server never received any requests
+ // for the given uuid. Normalize that to an empty list consistent
+ // with our representation.
+ return JSON.parse(text) || [];
+ });
+}
+function make_test(type, info) {
+ return function(test) {
+ var uuid = token();
+ var identifier = (type == "tag" ? Math.random() : now.toGMTString());
+ var content = Math.random().toString();
+ var url = make_url(uuid, type, identifier, content, info);
+ var fetch_functions = [];
+ for (var i = 0; i < info.request_cache.length; ++i) {
+ fetch_functions.push(function(idx) {
+ var init = {cache: info.request_cache[idx]};
+ if ("request_headers" in info) {
+ init.headers = info.request_headers[idx];
+ }
+ if (init.cache === "only-if-cached") {
+ // only-if-cached requires we use same-origin mode.
+ init.mode = "same-origin";
+ }
+ return fetch(url, init)
+ .then(function(response) {
+ if ("response" in info && info.response[idx] === "error") {
+ assert_true(false, "fetch should have been an error");
+ return;
+ }
+ assert_array_equals([response.status, response.statusText],
+ expected_status(type, identifier, init));
+ return response.text();
+ }).then(function(text) {
+ assert_equals(text, expected_response_text(type, identifier, init, content));
+ }, function(reason) {
+ if ("response" in info && info.response[idx] === "error") {
+ assert_throws_js(TypeError, function() { throw reason; });
+ } else {
+ throw reason;
+ }
+ });
+ });
+ }
+ var i = 0;
+ function run_next_step() {
+ if (fetch_functions.length) {
+ return fetch_functions.shift()(i++)
+ .then(run_next_step);
+ } else {
+ return Promise.resolve();
+ }
+ }
+ return run_next_step()
+ .then(function() {
+ // Now, query the server state
+ return server_state(uuid);
+ }).then(function(state) {
+ var expectedState = [];
+ info.expected_validation_headers.forEach(function (validate) {
+ if (validate) {
+ if (type == "tag") {
+ expectedState.push({"If-None-Match": '"' + identifier + '"'});
+ } else {
+ expectedState.push({"If-Modified-Since": identifier});
+ }
+ } else {
+ expectedState.push({});
+ }
+ });
+ for (var i = 0; i < info.expected_no_cache_headers.length; ++i) {
+ if (info.expected_no_cache_headers[i]) {
+ expectedState[i]["Pragma"] = "no-cache";
+ expectedState[i]["Cache-Control"] = "no-cache";
+ }
+ }
+ if ("expected_max_age_headers" in info) {
+ for (var i = 0; i < info.expected_max_age_headers.length; ++i) {
+ if (info.expected_max_age_headers[i]) {
+ expectedState[i]["Cache-Control"] = "max-age=0";
+ }
+ }
+ }
+ assert_equals(state.length, expectedState.length);
+ for (var i = 0; i < state.length; ++i) {
+ for (var header in state[i]) {
+ assert_equals(state[i][header], expectedState[i][header]);
+ delete expectedState[i][header];
+ }
+ for (var header in expectedState[i]) {
+ assert_false(header in state[i]);
+ }
+ }
+ });
+ };
+}
+
+function run_tests(tests)
+{
+ tests.forEach(function(info) {
+ promise_test(make_test("tag", info), info.name + " with Etag and " + info.state + " response");
+ promise_test(make_test("date", info), info.name + " with Last-Modified and " + info.state + " response");
+ });
+}
diff --git a/test/wpt/tests/fetch/api/request/request-clone.sub.html b/test/wpt/tests/fetch/api/request/request-clone.sub.html
new file mode 100644
index 0000000..c690bb3
--- /dev/null
+++ b/test/wpt/tests/fetch/api/request/request-clone.sub.html
@@ -0,0 +1,63 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>Request clone</title>
+ <meta name="help" href="https://fetch.spec.whatwg.org/#request">
+ <meta name="author" title="Canon Research France" href="https://www.crf.canon.fr">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="../resources/utils.js"></script>
+ </head>
+ <body>
+ <script>
+ var headers = new Headers({"name" : "value"});
+ var emptyHeaders = new Headers();
+
+ var initValuesDict = {"method" : "POST",
+ "referrer" : "http://{{host}}:{{ports[http][0]}}/",
+ "referrerPolicy" : "origin",
+ "mode" : "same-origin",
+ "credentials" : "include",
+ "cache" : "no-cache",
+ "redirect" : "error",
+ "integrity" : "Request's Integrity",
+ "headers" : headers,
+ "body" : "Request's body"
+ };
+
+ var expectedInitialized = {"method" : "POST",
+ "referrer" : "http://{{host}}:{{ports[http][0]}}/",
+ "referrerPolicy" : "origin",
+ "mode" : "same-origin",
+ "credentials" : "include",
+ "cache" : "no-cache",
+ "redirect" : "error",
+ "integrity" : "Request's Integrity",
+ "headers" : headers,
+ "body" : "Request's body"
+ };
+
+ test(function() {
+ var RequestInitialized = new Request("", initValuesDict);
+ var requestToCheck = RequestInitialized.clone();
+ checkRequest(requestToCheck, expectedInitialized);
+ }, "Check cloning a request");
+
+ test(function() {
+ var initialRequest = new Request("", {"headers" : new Headers({"a": "1", "b" : "2"})});
+ var request = initialRequest.clone();
+ assert_equals(request.headers.get("a"), "1", "cloned request should have header 'a'");
+ assert_equals(request.headers.get("b"), "2", "cloned request should have header 'b'");
+
+ initialRequest.headers.delete("a");
+ assert_equals(request.headers.get("a"), "1", "cloned request should still have header 'a'");
+
+ request.headers.delete("a");
+ assert_equals(initialRequest.headers.get("b"), "2", "initial request should have header 'b'");
+
+ }, "Check cloning a request copies the headers");
+
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/fetch/api/request/request-consume-empty.any.js b/test/wpt/tests/fetch/api/request/request-consume-empty.any.js
new file mode 100644
index 0000000..034a860
--- /dev/null
+++ b/test/wpt/tests/fetch/api/request/request-consume-empty.any.js
@@ -0,0 +1,101 @@
+// META: global=window,worker
+// META: title=Request consume empty bodies
+
+function checkBodyText(test, request) {
+ return request.text().then(function(bodyAsText) {
+ assert_equals(bodyAsText, "", "Resolved value should be empty");
+ assert_false(request.bodyUsed);
+ });
+}
+
+function checkBodyBlob(test, request) {
+ return request.blob().then(function(bodyAsBlob) {
+ var promise = new Promise(function(resolve, reject) {
+ var reader = new FileReader();
+ reader.onload = function(evt) {
+ resolve(reader.result)
+ };
+ reader.onerror = function() {
+ reject("Blob's reader failed");
+ };
+ reader.readAsText(bodyAsBlob);
+ });
+ return promise.then(function(body) {
+ assert_equals(body, "", "Resolved value should be empty");
+ assert_false(request.bodyUsed);
+ });
+ });
+}
+
+function checkBodyArrayBuffer(test, request) {
+ return request.arrayBuffer().then(function(bodyAsArrayBuffer) {
+ assert_equals(bodyAsArrayBuffer.byteLength, 0, "Resolved value should be empty");
+ assert_false(request.bodyUsed);
+ });
+}
+
+function checkBodyJSON(test, request) {
+ return request.json().then(
+ function(bodyAsJSON) {
+ assert_unreached("JSON parsing should fail");
+ },
+ function() {
+ assert_false(request.bodyUsed);
+ });
+}
+
+function checkBodyFormData(test, request) {
+ return request.formData().then(function(bodyAsFormData) {
+ assert_true(bodyAsFormData instanceof FormData, "Should receive a FormData");
+ assert_false(request.bodyUsed);
+ });
+}
+
+function checkBodyFormDataError(test, request) {
+ return promise_rejects_js(test, TypeError, request.formData()).then(function() {
+ assert_false(request.bodyUsed);
+ });
+}
+
+function checkRequestWithNoBody(bodyType, checkFunction, headers = []) {
+ promise_test(function(test) {
+ var request = new Request("", {"method": "POST", "headers": headers});
+ assert_false(request.bodyUsed);
+ return checkFunction(test, request);
+ }, "Consume request's body as " + bodyType);
+}
+
+checkRequestWithNoBody("text", checkBodyText);
+checkRequestWithNoBody("blob", checkBodyBlob);
+checkRequestWithNoBody("arrayBuffer", checkBodyArrayBuffer);
+checkRequestWithNoBody("json (error case)", checkBodyJSON);
+checkRequestWithNoBody("formData with correct multipart type (error case)", checkBodyFormDataError, [["Content-Type", 'multipart/form-data; boundary="boundary"']]);
+checkRequestWithNoBody("formData with correct urlencoded type", checkBodyFormData, [["Content-Type", "application/x-www-form-urlencoded;charset=UTF-8"]]);
+checkRequestWithNoBody("formData without correct type (error case)", checkBodyFormDataError);
+
+function checkRequestWithEmptyBody(bodyType, body, asText) {
+ promise_test(function(test) {
+ var request = new Request("", {"method": "POST", "body": body});
+ assert_false(request.bodyUsed, "bodyUsed is false at init");
+ if (asText) {
+ return request.text().then(function(bodyAsString) {
+ assert_equals(bodyAsString.length, 0, "Resolved value should be empty");
+ assert_true(request.bodyUsed, "bodyUsed is true after being consumed");
+ });
+ }
+ return request.arrayBuffer().then(function(bodyAsArrayBuffer) {
+ assert_equals(bodyAsArrayBuffer.byteLength, 0, "Resolved value should be empty");
+ assert_true(request.bodyUsed, "bodyUsed is true after being consumed");
+ });
+ }, "Consume empty " + bodyType + " request body as " + (asText ? "text" : "arrayBuffer"));
+}
+
+// FIXME: Add BufferSource, FormData and URLSearchParams.
+checkRequestWithEmptyBody("blob", new Blob([], { "type" : "text/plain" }), false);
+checkRequestWithEmptyBody("text", "", false);
+checkRequestWithEmptyBody("blob", new Blob([], { "type" : "text/plain" }), true);
+checkRequestWithEmptyBody("text", "", true);
+checkRequestWithEmptyBody("URLSearchParams", new URLSearchParams(""), true);
+// FIXME: This test assumes that the empty string be returned but it is not clear whether that is right. See https://github.com/web-platform-tests/wpt/pull/3950.
+checkRequestWithEmptyBody("FormData", new FormData(), true);
+checkRequestWithEmptyBody("ArrayBuffer", new ArrayBuffer(), true);
diff --git a/test/wpt/tests/fetch/api/request/request-consume.any.js b/test/wpt/tests/fetch/api/request/request-consume.any.js
new file mode 100644
index 0000000..aff5d65
--- /dev/null
+++ b/test/wpt/tests/fetch/api/request/request-consume.any.js
@@ -0,0 +1,145 @@
+// META: global=window,worker
+// META: title=Request consume
+// META: script=../resources/utils.js
+
+function checkBodyText(request, expectedBody) {
+ return request.text().then(function(bodyAsText) {
+ assert_equals(bodyAsText, expectedBody, "Retrieve and verify request's body");
+ assert_true(request.bodyUsed, "body as text: bodyUsed turned true");
+ });
+}
+
+function checkBodyBlob(request, expectedBody, checkContentType) {
+ return request.blob().then(function(bodyAsBlob) {
+ if (checkContentType)
+ assert_equals(bodyAsBlob.type, "text/plain", "Blob body type should be computed from the request Content-Type");
+
+ var promise = new Promise(function (resolve, reject) {
+ var reader = new FileReader();
+ reader.onload = function(evt) {
+ resolve(reader.result)
+ };
+ reader.onerror = function() {
+ reject("Blob's reader failed");
+ };
+ reader.readAsText(bodyAsBlob);
+ });
+ return promise.then(function(body) {
+ assert_equals(body, expectedBody, "Retrieve and verify request's body");
+ assert_true(request.bodyUsed, "body as blob: bodyUsed turned true");
+ });
+ });
+}
+
+function checkBodyArrayBuffer(request, expectedBody) {
+ return request.arrayBuffer().then(function(bodyAsArrayBuffer) {
+ validateBufferFromString(bodyAsArrayBuffer, expectedBody, "Retrieve and verify request's body");
+ assert_true(request.bodyUsed, "body as arrayBuffer: bodyUsed turned true");
+ });
+}
+
+function checkBodyJSON(request, expectedBody) {
+ return request.json().then(function(bodyAsJSON) {
+ var strBody = JSON.stringify(bodyAsJSON)
+ assert_equals(strBody, expectedBody, "Retrieve and verify request's body");
+ assert_true(request.bodyUsed, "body as json: bodyUsed turned true");
+ });
+}
+
+function checkBodyFormData(request, expectedBody) {
+ return request.formData().then(function(bodyAsFormData) {
+ assert_true(bodyAsFormData instanceof FormData, "Should receive a FormData");
+ assert_true(request.bodyUsed, "body as formData: bodyUsed turned true");
+ });
+}
+
+function checkRequestBody(body, expected, bodyType) {
+ promise_test(function(test) {
+ var request = new Request("", {"method": "POST", "body": body, "headers": [["Content-Type", "text/PLAIN"]] });
+ assert_false(request.bodyUsed, "bodyUsed is false at init");
+ return checkBodyText(request, expected);
+ }, "Consume " + bodyType + " request's body as text");
+ promise_test(function(test) {
+ var request = new Request("", {"method": "POST", "body": body });
+ assert_false(request.bodyUsed, "bodyUsed is false at init");
+ return checkBodyBlob(request, expected);
+ }, "Consume " + bodyType + " request's body as blob");
+ promise_test(function(test) {
+ var request = new Request("", {"method": "POST", "body": body });
+ assert_false(request.bodyUsed, "bodyUsed is false at init");
+ return checkBodyArrayBuffer(request, expected);
+ }, "Consume " + bodyType + " request's body as arrayBuffer");
+ promise_test(function(test) {
+ var request = new Request("", {"method": "POST", "body": body });
+ assert_false(request.bodyUsed, "bodyUsed is false at init");
+ return checkBodyJSON(request, expected);
+ }, "Consume " + bodyType + " request's body as JSON");
+}
+
+var textData = JSON.stringify("This is response's body");
+var blob = new Blob([textData], { "type" : "text/plain" });
+
+checkRequestBody(textData, textData, "String");
+
+var string = "\"123456\"";
+function getArrayBuffer() {
+ var arrayBuffer = new ArrayBuffer(8);
+ var int8Array = new Int8Array(arrayBuffer);
+ for (var cptr = 0; cptr < 8; cptr++)
+ int8Array[cptr] = string.charCodeAt(cptr);
+ return arrayBuffer;
+}
+
+function getArrayBufferWithZeros() {
+ var arrayBuffer = new ArrayBuffer(10);
+ var int8Array = new Int8Array(arrayBuffer);
+ for (var cptr = 0; cptr < 8; cptr++)
+ int8Array[cptr + 1] = string.charCodeAt(cptr);
+ return arrayBuffer;
+}
+
+checkRequestBody(getArrayBuffer(), string, "ArrayBuffer");
+checkRequestBody(new Uint8Array(getArrayBuffer()), string, "Uint8Array");
+checkRequestBody(new Int8Array(getArrayBufferWithZeros(), 1, 8), string, "Int8Array");
+checkRequestBody(new Float32Array(getArrayBuffer()), string, "Float32Array");
+checkRequestBody(new DataView(getArrayBufferWithZeros(), 1, 8), string, "DataView");
+
+promise_test(function(test) {
+ var formData = new FormData();
+ formData.append("name", "value")
+ var request = new Request("", {"method": "POST", "body": formData });
+ assert_false(request.bodyUsed, "bodyUsed is false at init");
+ return checkBodyFormData(request, formData);
+}, "Consume FormData request's body as FormData");
+
+function checkBlobResponseBody(blobBody, blobData, bodyType, checkFunction) {
+ promise_test(function(test) {
+ var response = new Response(blobBody);
+ assert_false(response.bodyUsed, "bodyUsed is false at init");
+ return checkFunction(response, blobData);
+ }, "Consume blob response's body as " + bodyType);
+}
+
+checkBlobResponseBody(blob, textData, "blob", checkBodyBlob);
+checkBlobResponseBody(blob, textData, "text", checkBodyText);
+checkBlobResponseBody(blob, textData, "json", checkBodyJSON);
+checkBlobResponseBody(blob, textData, "arrayBuffer", checkBodyArrayBuffer);
+checkBlobResponseBody(new Blob([""]), "", "blob (empty blob as input)", checkBodyBlob);
+
+var goodJSONValues = ["null", "1", "true", "\"string\""];
+goodJSONValues.forEach(function(value) {
+ promise_test(function(test) {
+ var request = new Request("", {"method": "POST", "body": value});
+ return request.json().then(function(v) {
+ assert_equals(v, JSON.parse(value));
+ });
+ }, "Consume JSON from text: '" + JSON.stringify(value) + "'");
+});
+
+var badJSONValues = ["undefined", "{", "a", "["];
+badJSONValues.forEach(function(value) {
+ promise_test(function(test) {
+ var request = new Request("", {"method": "POST", "body": value});
+ return promise_rejects_js(test, SyntaxError, request.json());
+ }, "Trying to consume bad JSON text as JSON: '" + value + "'");
+});
diff --git a/test/wpt/tests/fetch/api/request/request-disturbed.any.js b/test/wpt/tests/fetch/api/request/request-disturbed.any.js
new file mode 100644
index 0000000..8a11de7
--- /dev/null
+++ b/test/wpt/tests/fetch/api/request/request-disturbed.any.js
@@ -0,0 +1,109 @@
+// META: global=window,worker
+// META: title=Request disturbed
+// META: script=../resources/utils.js
+
+var initValuesDict = {"method" : "POST",
+ "body" : "Request's body"
+};
+
+var noBodyConsumed = new Request("");
+var bodyConsumed = new Request("", initValuesDict);
+
+test(() => {
+ assert_equals(noBodyConsumed.body, null, "body's default value is null");
+ assert_false(noBodyConsumed.bodyUsed , "bodyUsed is false when request is not disturbed");
+ assert_not_equals(bodyConsumed.body, null, "non-null body");
+ assert_true(bodyConsumed.body instanceof ReadableStream, "non-null body type");
+ assert_false(noBodyConsumed.bodyUsed, "bodyUsed is false when request is not disturbed");
+}, "Request's body: initial state");
+
+noBodyConsumed.blob();
+bodyConsumed.blob();
+
+test(function() {
+ assert_false(noBodyConsumed.bodyUsed , "bodyUsed is false when request is not disturbed");
+ try {
+ noBodyConsumed.clone();
+ } catch (e) {
+ assert_unreached("Can use request not disturbed for creating or cloning request");
+ }
+}, "Request without body cannot be disturbed");
+
+test(function() {
+ assert_true(bodyConsumed.bodyUsed , "bodyUsed is true when request is disturbed");
+ assert_throws_js(TypeError, function() { bodyConsumed.clone(); });
+}, "Check cloning a disturbed request");
+
+test(function() {
+ assert_true(bodyConsumed.bodyUsed , "bodyUsed is true when request is disturbed");
+ assert_throws_js(TypeError, function() { new Request(bodyConsumed); });
+}, "Check creating a new request from a disturbed request");
+
+promise_test(function() {
+ assert_true(bodyConsumed.bodyUsed , "bodyUsed is true when request is disturbed");
+ const originalBody = bodyConsumed.body;
+ const bodyReplaced = new Request(bodyConsumed, { body: "Replaced body" });
+ assert_not_equals(bodyReplaced.body, originalBody, "new request's body is new");
+ assert_false(bodyReplaced.bodyUsed, "bodyUsed is false when request is not disturbed");
+ return bodyReplaced.text().then(text => {
+ assert_equals(text, "Replaced body");
+ });
+}, "Check creating a new request with a new body from a disturbed request");
+
+promise_test(function() {
+ var bodyRequest = new Request("", initValuesDict);
+ const originalBody = bodyRequest.body;
+ assert_false(bodyRequest.bodyUsed , "bodyUsed is false when request is not disturbed");
+ var requestFromRequest = new Request(bodyRequest);
+ assert_true(bodyRequest.bodyUsed , "bodyUsed is true when request is disturbed");
+ assert_equals(bodyRequest.body, originalBody, "body should not change");
+ assert_not_equals(originalBody, undefined, "body should not be undefined");
+ assert_not_equals(originalBody, null, "body should not be null");
+ assert_not_equals(requestFromRequest.body, originalBody, "new request's body is new");
+ return requestFromRequest.text().then(text => {
+ assert_equals(text, "Request's body");
+ });
+}, "Input request used for creating new request became disturbed");
+
+promise_test(() => {
+ const bodyRequest = new Request("", initValuesDict);
+ const originalBody = bodyRequest.body;
+ assert_false(bodyRequest.bodyUsed , "bodyUsed is false when request is not disturbed");
+ const requestFromRequest = new Request(bodyRequest, { body : "init body" });
+ assert_true(bodyRequest.bodyUsed , "bodyUsed is true when request is disturbed");
+ assert_equals(bodyRequest.body, originalBody, "body should not change");
+ assert_not_equals(originalBody, undefined, "body should not be undefined");
+ assert_not_equals(originalBody, null, "body should not be null");
+ assert_not_equals(requestFromRequest.body, originalBody, "new request's body is new");
+
+ return requestFromRequest.text().then(text => {
+ assert_equals(text, "init body");
+ });
+}, "Input request used for creating new request became disturbed even if body is not used");
+
+promise_test(function(test) {
+ assert_true(bodyConsumed.bodyUsed , "bodyUsed is true when request is disturbed");
+ return promise_rejects_js(test, TypeError, bodyConsumed.blob());
+}, "Check consuming a disturbed request");
+
+test(function() {
+ var req = new Request(URL, {method: 'POST', body: 'hello'});
+ assert_false(req.bodyUsed,
+ 'Request should not be flagged as used if it has not been ' +
+ 'consumed.');
+ assert_throws_js(TypeError,
+ function() { new Request(req, {method: 'GET'}); },
+ 'A get request may not have body.');
+
+ assert_false(req.bodyUsed, 'After the GET case');
+
+ assert_throws_js(TypeError,
+ function() { new Request(req, {method: 'CONNECT'}); },
+ 'Request() with a forbidden method must throw.');
+
+ assert_false(req.bodyUsed, 'After the forbidden method case');
+
+ var req2 = new Request(req);
+ assert_true(req.bodyUsed,
+ 'Request should be flagged as used if it has been consumed.');
+}, 'Request construction failure should not set "bodyUsed"');
diff --git a/test/wpt/tests/fetch/api/request/request-error.any.js b/test/wpt/tests/fetch/api/request/request-error.any.js
new file mode 100644
index 0000000..9ec8015
--- /dev/null
+++ b/test/wpt/tests/fetch/api/request/request-error.any.js
@@ -0,0 +1,56 @@
+// META: global=window,worker
+// META: title=Request error
+// META: script=request-error.js
+
+// badRequestArgTests is from response-error.js
+for (const { args, testName } of badRequestArgTests) {
+ test(() => {
+ assert_throws_js(
+ TypeError,
+ () => new Request(...args),
+ "Expect TypeError exception"
+ );
+ }, testName);
+}
+
+test(function() {
+ assert_throws_js(
+ TypeError,
+ () => Request("about:blank"),
+ "Calling Request constructor without 'new' must throw"
+ );
+});
+
+test(function() {
+ var initialHeaders = new Headers([["Content-Type", "potato"]]);
+ var initialRequest = new Request("", {"headers" : initialHeaders});
+ var request = new Request(initialRequest);
+ assert_equals(request.headers.get("Content-Type"), "potato");
+}, "Request should get its content-type from the init request");
+
+test(function() {
+ var initialHeaders = new Headers([["Content-Type", "potato"]]);
+ var initialRequest = new Request("", {"headers" : initialHeaders});
+ var headers = new Headers([]);
+ var request = new Request(initialRequest, {"headers" : headers});
+ assert_false(request.headers.has("Content-Type"));
+}, "Request should not get its content-type from the init request if init headers are provided");
+
+test(function() {
+ var initialHeaders = new Headers([["Content-Type-Extra", "potato"]]);
+ var initialRequest = new Request("", {"headers" : initialHeaders, "body" : "this is my plate", "method" : "POST"});
+ var request = new Request(initialRequest);
+ assert_equals(request.headers.get("Content-Type"), "text/plain;charset=UTF-8");
+}, "Request should get its content-type from the body if none is provided");
+
+test(function() {
+ var initialHeaders = new Headers([["Content-Type", "potato"]]);
+ var initialRequest = new Request("", {"headers" : initialHeaders, "body" : "this is my plate", "method" : "POST"});
+ var request = new Request(initialRequest);
+ assert_equals(request.headers.get("Content-Type"), "potato");
+}, "Request should get its content-type from init headers if one is provided");
+
+test(function() {
+ var options = {"cache": "only-if-cached", "mode": "same-origin"};
+ new Request("test", options);
+}, "Request with cache mode: only-if-cached and fetch mode: same-origin");
diff --git a/test/wpt/tests/fetch/api/request/request-error.js b/test/wpt/tests/fetch/api/request/request-error.js
new file mode 100644
index 0000000..cf77313
--- /dev/null
+++ b/test/wpt/tests/fetch/api/request/request-error.js
@@ -0,0 +1,57 @@
+const badRequestArgTests = [
+ {
+ args: ["", { "window": "http://test.url" }],
+ testName: "RequestInit's window is not null"
+ },
+ {
+ args: ["http://:not a valid URL"],
+ testName: "Input URL is not valid"
+ },
+ {
+ args: ["http://user:pass@test.url"],
+ testName: "Input URL has credentials"
+ },
+ {
+ args: ["", { "mode": "navigate" }],
+ testName: "RequestInit's mode is navigate"
+ },
+ {
+ args: ["", { "referrer": "http://:not a valid URL" }],
+ testName: "RequestInit's referrer is invalid"
+ },
+ {
+ args: ["", { "method": "IN VALID" }],
+ testName: "RequestInit's method is invalid"
+ },
+ {
+ args: ["", { "method": "TRACE" }],
+ testName: "RequestInit's method is forbidden"
+ },
+ {
+ args: ["", { "mode": "no-cors", "method": "PUT" }],
+ testName: "RequestInit's mode is no-cors and method is not simple"
+ },
+ {
+ args: ["", { "mode": "cors", "cache": "only-if-cached" }],
+ testName: "RequestInit's cache mode is only-if-cached and mode is not same-origin"
+ },
+ {
+ args: ["test", { "cache": "only-if-cached", "mode": "cors" }],
+ testName: "Request with cache mode: only-if-cached and fetch mode cors"
+ },
+ {
+ args: ["test", { "cache": "only-if-cached", "mode": "no-cors" }],
+ testName: "Request with cache mode: only-if-cached and fetch mode no-cors"
+ }
+];
+
+badRequestArgTests.push(
+ ...["referrerPolicy", "mode", "credentials", "cache", "redirect"].map(optionProp => {
+ const options = {};
+ options[optionProp] = "BAD";
+ return {
+ args: ["", options],
+ testName: `Bad ${optionProp} init parameter value`
+ };
+ })
+);
diff --git a/test/wpt/tests/fetch/api/request/request-headers.any.js b/test/wpt/tests/fetch/api/request/request-headers.any.js
new file mode 100644
index 0000000..22925e0
--- /dev/null
+++ b/test/wpt/tests/fetch/api/request/request-headers.any.js
@@ -0,0 +1,178 @@
+// META: global=window,worker
+// META: title=Request Headers
+
+var validRequestHeaders = [
+ ["Content-Type", "OK"],
+ ["Potato", "OK"],
+ ["proxy", "OK"],
+ ["proxya", "OK"],
+ ["sec", "OK"],
+ ["secb", "OK"],
+ ["Set-Cookie2", "OK"],
+ ["User-Agent", "OK"],
+];
+var invalidRequestHeaders = [
+ ["Accept-Charset", "KO"],
+ ["accept-charset", "KO"],
+ ["ACCEPT-ENCODING", "KO"],
+ ["Accept-Encoding", "KO"],
+ ["Access-Control-Request-Headers", "KO"],
+ ["Access-Control-Request-Method", "KO"],
+ ["Access-Control-Request-Private-Network", "KO"],
+ ["Connection", "KO"],
+ ["Content-Length", "KO"],
+ ["Cookie", "KO"],
+ ["Cookie2", "KO"],
+ ["Date", "KO"],
+ ["DNT", "KO"],
+ ["Expect", "KO"],
+ ["Host", "KO"],
+ ["Keep-Alive", "KO"],
+ ["Origin", "KO"],
+ ["Referer", "KO"],
+ ["Set-Cookie", "KO"],
+ ["TE", "KO"],
+ ["Trailer", "KO"],
+ ["Transfer-Encoding", "KO"],
+ ["Upgrade", "KO"],
+ ["Via", "KO"],
+ ["Proxy-", "KO"],
+ ["proxy-a", "KO"],
+ ["Sec-", "KO"],
+ ["sec-b", "KO"],
+];
+
+var validRequestNoCorsHeaders = [
+ ["Accept", "OK"],
+ ["Accept-Language", "OK"],
+ ["content-language", "OK"],
+ ["content-type", "application/x-www-form-urlencoded"],
+ ["content-type", "application/x-www-form-urlencoded;charset=UTF-8"],
+ ["content-type", "multipart/form-data"],
+ ["content-type", "multipart/form-data;charset=UTF-8"],
+ ["content-TYPE", "text/plain"],
+ ["CONTENT-type", "text/plain;charset=UTF-8"],
+];
+var invalidRequestNoCorsHeaders = [
+ ["Content-Type", "KO"],
+ ["Potato", "KO"],
+ ["proxy", "KO"],
+ ["proxya", "KO"],
+ ["sec", "KO"],
+ ["secb", "KO"],
+ ["Empty-Value", ""],
+];
+
+validRequestHeaders.forEach(function(header) {
+ test(function() {
+ var request = new Request("");
+ request.headers.set(header[0], header[1]);
+ assert_equals(request.headers.get(header[0]), header[1]);
+ }, "Adding valid request header \"" + header[0] + ": " + header[1] + "\"");
+});
+invalidRequestHeaders.forEach(function(header) {
+ test(function() {
+ var request = new Request("");
+ request.headers.set(header[0], header[1]);
+ assert_equals(request.headers.get(header[0]), null);
+ }, "Adding invalid request header \"" + header[0] + ": " + header[1] + "\"");
+});
+
+validRequestNoCorsHeaders.forEach(function(header) {
+ test(function() {
+ var requestNoCors = new Request("", {"mode": "no-cors"});
+ requestNoCors.headers.set(header[0], header[1]);
+ assert_equals(requestNoCors.headers.get(header[0]), header[1]);
+ }, "Adding valid no-cors request header \"" + header[0] + ": " + header[1] + "\"");
+});
+invalidRequestNoCorsHeaders.forEach(function(header) {
+ test(function() {
+ var requestNoCors = new Request("", {"mode": "no-cors"});
+ requestNoCors.headers.set(header[0], header[1]);
+ assert_equals(requestNoCors.headers.get(header[0]), null);
+ }, "Adding invalid no-cors request header \"" + header[0] + ": " + header[1] + "\"");
+});
+
+test(function() {
+ var headers = new Headers([["Cookie2", "potato"]]);
+ var request = new Request("", {"headers": headers});
+ assert_equals(request.headers.get("Cookie2"), null);
+}, "Check that request constructor is filtering headers provided as init parameter");
+
+test(function() {
+ var headers = new Headers([["Content-Type", "potato"]]);
+ var request = new Request("", {"headers": headers, "mode": "no-cors"});
+ assert_equals(request.headers.get("Content-Type"), null);
+}, "Check that no-cors request constructor is filtering headers provided as init parameter");
+
+test(function() {
+ var headers = new Headers([["Content-Type", "potato"]]);
+ var initialRequest = new Request("", {"headers": headers});
+ var request = new Request(initialRequest, {"mode": "no-cors"});
+ assert_equals(request.headers.get("Content-Type"), null);
+}, "Check that no-cors request constructor is filtering headers provided as part of request parameter");
+
+test(function() {
+ var initialHeaders = new Headers([["Content-Type", "potato"]]);
+ var initialRequest = new Request("", {"headers" : initialHeaders});
+ var request = new Request(initialRequest);
+ assert_equals(request.headers.get("Content-Type"), "potato");
+}, "Request should get its content-type from the init request");
+
+test(function() {
+ var initialHeaders = new Headers([["Content-Type", "potato"]]);
+ var initialRequest = new Request("", {"headers" : initialHeaders});
+ var headers = new Headers([]);
+ var request = new Request(initialRequest, {"headers" : headers});
+ assert_false(request.headers.has("Content-Type"));
+}, "Request should not get its content-type from the init request if init headers are provided");
+
+test(function() {
+ var initialHeaders = new Headers([["Content-Type-Extra", "potato"]]);
+ var initialRequest = new Request("", {"headers" : initialHeaders, "body" : "this is my plate", "method" : "POST"});
+ var request = new Request(initialRequest);
+ assert_equals(request.headers.get("Content-Type"), "text/plain;charset=UTF-8");
+}, "Request should get its content-type from the body if none is provided");
+
+test(function() {
+ var initialHeaders = new Headers([["Content-Type", "potato"]]);
+ var initialRequest = new Request("", {"headers" : initialHeaders, "body" : "this is my plate", "method" : "POST"});
+ var request = new Request(initialRequest);
+ assert_equals(request.headers.get("Content-Type"), "potato");
+}, "Request should get its content-type from init headers if one is provided");
+
+test(function() {
+ var array = [["hello", "worldAHH"]];
+ var object = {"hello": 'worldOOH'};
+ var headers = new Headers(array);
+
+ assert_equals(headers.get("hello"), "worldAHH");
+
+ var request1 = new Request("", {"headers": headers});
+ var request2 = new Request("", {"headers": array});
+ var request3 = new Request("", {"headers": object});
+
+ assert_equals(request1.headers.get("hello"), "worldAHH");
+ assert_equals(request2.headers.get("hello"), "worldAHH");
+ assert_equals(request3.headers.get("hello"), "worldOOH");
+}, "Testing request header creations with various objects");
+
+promise_test(function(test) {
+ var request = new Request("", {"headers" : [["Content-Type", ""]], "body" : "this is my plate", "method" : "POST"});
+ return request.blob().then(function(blob) {
+ assert_equals(blob.type, "", "Blob type should be the empty string");
+ });
+}, "Testing empty Request Content-Type header");
+
+test(function() {
+ const request1 = new Request("");
+ assert_equals(request1.headers, request1.headers);
+
+ const request2 = new Request("", {"headers": {"X-Foo": "bar"}});
+ assert_equals(request2.headers, request2.headers);
+ const headers = request2.headers;
+ request2.headers.set("X-Foo", "quux");
+ assert_equals(headers, request2.headers);
+ headers.set("X-Other-Header", "baz");
+ assert_equals(headers, request2.headers);
+}, "Test that Request.headers has the [SameObject] extended attribute");
diff --git a/test/wpt/tests/fetch/api/request/request-init-001.sub.html b/test/wpt/tests/fetch/api/request/request-init-001.sub.html
new file mode 100644
index 0000000..cc495a6
--- /dev/null
+++ b/test/wpt/tests/fetch/api/request/request-init-001.sub.html
@@ -0,0 +1,112 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>Request init: simple cases</title>
+ <meta name="help" href="https://fetch.spec.whatwg.org/#request">
+ <meta name="author" title="Canon Research France" href="https://www.crf.canon.fr">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ </head>
+ <body>
+ <script>
+ // https://fetch.spec.whatwg.org/#concept-method-normalize
+ var methods = {
+ "givenValues" : [
+ "GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS",
+ "get", "head", "post", "put", "delete", "options",
+ "Get", "hEad", "poSt", "Put", "deleTe", "optionS",
+ "PATCH", "patch", "patCh"
+ ],
+ "expectedValues" : [
+ "GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS",
+ "GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS",
+ "GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS",
+ "PATCH", "patch", "patCh"
+ ]
+ };
+ var referrers = {"givenValues" : ["/relative/ressource",
+ "http://{{host}}:{{ports[http][0]}}/relative/ressource?query=true#fragment",
+ "http://{{host}}:{{ports[http][0]}}/",
+ "http://test.url",
+ "about:client",
+ ""
+ ],
+ "expectedValues" : ["http://{{host}}:{{ports[http][0]}}/relative/ressource",
+ "http://{{host}}:{{ports[http][0]}}/relative/ressource?query=true#fragment",
+ "http://{{host}}:{{ports[http][0]}}/",
+ "about:client",
+ "about:client",
+ ""
+ ]
+ };
+ var referrerPolicies = {"givenValues" : [ "",
+ "no-referrer",
+ "no-referrer-when-downgrade",
+ "origin",
+ "origin-when-cross-origin",
+ "unsafe-url",
+ "same-origin",
+ "strict-origin",
+ "strict-origin-when-cross-origin"
+ ],
+ "expectedValues" : ["",
+ "no-referrer",
+ "no-referrer-when-downgrade",
+ "origin",
+ "origin-when-cross-origin",
+ "unsafe-url",
+ "same-origin",
+ "strict-origin",
+ "strict-origin-when-cross-origin"
+ ]
+ };
+ var modes = {"givenValues" : ["same-origin", "no-cors", "cors"],
+ "expectedValues" : ["same-origin", "no-cors", "cors"]
+ };
+ var credentials = {"givenValues" : ["omit", "same-origin", "include"],
+ "expectedValues" : ["omit", "same-origin", "include"]
+ };
+ var caches = {"givenValues" : [ "default", "no-store", "reload", "no-cache", "force-cache"],
+ "expectedValues" : [ "default", "no-store", "reload", "no-cache", "force-cache"]
+ };
+ var redirects = {"givenValues" : ["follow", "error", "manual"],
+ "expectedValues" : ["follow", "error", "manual"]
+ };
+ var integrities = {"givenValues" : ["", "AZERTYUIOP1234567890" ],
+ "expectedValues" : ["", "AZERTYUIOP1234567890"]
+ };
+
+ //there is no getter for window, init's window might be null
+ var windows = {"givenValues" : [ null ],
+ "expectedValues" : [undefined]
+ };
+
+ var initValuesDict = { "method" : methods,
+ "referrer" : referrers,
+ "referrerPolicy" : referrerPolicies,
+ "mode" : modes,
+ "credentials" : credentials,
+ "cache" : caches,
+ "redirect" : redirects,
+ "integrity" : integrities,
+ "window" : windows
+ };
+
+ for (var attributeName in initValuesDict) {
+ var valuesToTest = initValuesDict[attributeName];
+ for (var valueIdx in valuesToTest["givenValues"]) {
+ var givenValue = valuesToTest["givenValues"][valueIdx];
+ var expectedValue = valuesToTest["expectedValues"][valueIdx];
+ test(function() {
+ var requestInit = {};
+ requestInit[attributeName] = givenValue
+ var request = new Request("", requestInit);
+ assert_equals(request[attributeName], expectedValue,
+ "Expect request's " + attributeName + " is " + expectedValue + " when initialized with " + givenValue);
+ }, "Check " + attributeName + " init value of " + givenValue + " and associated getter");
+ }
+ }
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/fetch/api/request/request-init-002.any.js b/test/wpt/tests/fetch/api/request/request-init-002.any.js
new file mode 100644
index 0000000..abb6689
--- /dev/null
+++ b/test/wpt/tests/fetch/api/request/request-init-002.any.js
@@ -0,0 +1,60 @@
+// META: global=window,worker
+// META: title=Request init: headers and body
+
+test(function() {
+ var headerDict = {"name1": "value1",
+ "name2": "value2",
+ "name3": "value3"
+ };
+ var headers = new Headers(headerDict);
+ var request = new Request("", { "headers" : headers })
+ for (var name in headerDict) {
+ assert_equals(request.headers.get(name), headerDict[name],
+ "request's headers has " + name + " : " + headerDict[name]);
+ }
+}, "Initialize Request with headers values");
+
+function makeRequestInit(body, method) {
+ return {"method": method, "body": body};
+}
+
+function checkRequestInit(body, bodyType, expectedTextBody) {
+ promise_test(function(test) {
+ var request = new Request("", makeRequestInit(body, "POST"));
+ if (body) {
+ assert_throws_js(TypeError, function() { new Request("", makeRequestInit(body, "GET")); });
+ assert_throws_js(TypeError, function() { new Request("", makeRequestInit(body, "HEAD")); });
+ } else {
+ new Request("", makeRequestInit(body, "GET")); // should not throw
+ }
+ var reqHeaders = request.headers;
+ var mime = reqHeaders.get("Content-Type");
+ assert_true(!body || (mime && mime.search(bodyType) > -1), "Content-Type header should be \"" + bodyType + "\", not \"" + mime + "\"");
+ return request.text().then(function(bodyAsText) {
+ //not equals: cannot guess formData exact value
+ assert_true( bodyAsText.search(expectedTextBody) > -1, "Retrieve and verify request body");
+ });
+ }, `Initialize Request's body with "${body}", ${bodyType}`);
+}
+
+var blob = new Blob(["This is a blob"], {type: "application/octet-binary"});
+var formaData = new FormData();
+formaData.append("name", "value");
+var usvString = "This is a USVString"
+
+checkRequestInit(undefined, undefined, "");
+checkRequestInit(null, null, "");
+checkRequestInit(blob, "application/octet-binary", "This is a blob");
+checkRequestInit(formaData, "multipart/form-data", "name=\"name\"\r\n\r\nvalue");
+checkRequestInit(usvString, "text/plain;charset=UTF-8", "This is a USVString");
+checkRequestInit({toString: () => "hi!"}, "text/plain;charset=UTF-8", "hi!");
+
+// Ensure test does not time out in case of missing URLSearchParams support.
+if (self.URLSearchParams) {
+ var urlSearchParams = new URLSearchParams("name=value");
+ checkRequestInit(urlSearchParams, "application/x-www-form-urlencoded;charset=UTF-8", "name=value");
+} else {
+ promise_test(function(test) {
+ return Promise.reject("URLSearchParams not supported");
+ }, "Initialize Request's body with application/x-www-form-urlencoded;charset=UTF-8");
+}
diff --git a/test/wpt/tests/fetch/api/request/request-init-003.sub.html b/test/wpt/tests/fetch/api/request/request-init-003.sub.html
new file mode 100644
index 0000000..79c91cd
--- /dev/null
+++ b/test/wpt/tests/fetch/api/request/request-init-003.sub.html
@@ -0,0 +1,84 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>Request: init with request or url</title>
+ <meta name="help" href="https://fetch.spec.whatwg.org/#request">
+ <meta name="help" href="https://url.spec.whatwg.org/#concept-url-serializer">
+ <meta name="author" title="Canon Research France" href="https://www.crf.canon.fr">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ </head>
+ <body>
+ <script src="../resources/utils.js"></script>
+ <script>
+ var headers = new Headers( {"name":"value"} );
+ var emptyHeaders = new Headers();
+
+ var initValuesDict = {"method" : "POST",
+ "referrer" : "http://{{host}}:{{ports[http][0]}}/",
+ "referrerPolicy" : "origin",
+ "mode" : "same-origin",
+ "credentials" : "include",
+ "cache" : "no-cache",
+ "redirect" : "error",
+ "integrity" : "Request's Integrity",
+ "headers" : headers,
+ "body" : "Request's body"
+ };
+
+ var expectedInitialized = {"method" : "POST",
+ "referrer" : "http://{{host}}:{{ports[http][0]}}/",
+ "referrerPolicy" : "origin",
+ "mode" : "same-origin",
+ "credentials" : "include",
+ "cache" : "no-cache",
+ "redirect" : "error",
+ "integrity" : "Request's Integrity",
+ "headers" : headers,
+ "body" : "Request's body"
+ };
+
+ var expectedDefault = {"method" : "GET",
+ "url" : location.href,
+ "referrer" : "about:client",
+ "referrerPolicy" : "",
+ "mode" : "cors",
+ "credentials" : "same-origin",
+ "cache" : "default",
+ "redirect" : "follow",
+ "integrity" : "",
+ "headers" : emptyHeaders
+ };
+
+ var requestDefault = new Request("");
+ var requestInitialized = new Request("", initValuesDict);
+
+ test(function() {
+ var requestToCheck = new Request(requestInitialized);
+ checkRequest(requestToCheck, expectedInitialized);
+ }, "Check request values when initialized from Request");
+
+ test(function() {
+ var requestToCheck = new Request(requestDefault, initValuesDict);
+ checkRequest(requestToCheck, expectedInitialized);
+ }, "Check request values when initialized from Request and init values");
+
+ test(function() {
+ var url = "http://url.test:1234/path/subpath?query=true";
+ url += "#fragment";
+ expectedDefault["url"] = url;
+ var requestToCheck = new Request(url);
+ checkRequest(requestToCheck, expectedDefault);
+ }, "Check request values when initialized from url string");
+
+ test(function() {
+ var url = "http://url.test:1234/path/subpath?query=true";
+ url += "#fragment";
+ expectedInitialized["url"] = url;
+ var requestToCheck = new Request(url , initValuesDict);
+ checkRequest(requestToCheck, expectedInitialized);
+ }, "Check request values when initialized from url and init values");
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/fetch/api/request/request-init-contenttype.any.js b/test/wpt/tests/fetch/api/request/request-init-contenttype.any.js
new file mode 100644
index 0000000..18a6969
--- /dev/null
+++ b/test/wpt/tests/fetch/api/request/request-init-contenttype.any.js
@@ -0,0 +1,141 @@
+function requestFromBody(body) {
+ return new Request(
+ "https://example.com",
+ {
+ method: "POST",
+ body,
+ duplex: "half",
+ },
+ );
+}
+
+test(() => {
+ const request = requestFromBody(undefined);
+ assert_equals(request.headers.get("Content-Type"), null);
+}, "Default Content-Type for Request with empty body");
+
+test(() => {
+ const blob = new Blob([]);
+ const request = requestFromBody(blob);
+ assert_equals(request.headers.get("Content-Type"), null);
+}, "Default Content-Type for Request with Blob body (no type set)");
+
+test(() => {
+ const blob = new Blob([], { type: "" });
+ const request = requestFromBody(blob);
+ assert_equals(request.headers.get("Content-Type"), null);
+}, "Default Content-Type for Request with Blob body (empty type)");
+
+test(() => {
+ const blob = new Blob([], { type: "a/b; c=d" });
+ const request = requestFromBody(blob);
+ assert_equals(request.headers.get("Content-Type"), "a/b; c=d");
+}, "Default Content-Type for Request with Blob body (set type)");
+
+test(() => {
+ const buffer = new Uint8Array();
+ const request = requestFromBody(buffer);
+ assert_equals(request.headers.get("Content-Type"), null);
+}, "Default Content-Type for Request with buffer source body");
+
+promise_test(async () => {
+ const formData = new FormData();
+ formData.append("a", "b");
+ const request = requestFromBody(formData);
+ const boundary = (await request.text()).split("\r\n")[0].slice(2);
+ assert_equals(
+ request.headers.get("Content-Type"),
+ `multipart/form-data; boundary=${boundary}`,
+ );
+}, "Default Content-Type for Request with FormData body");
+
+test(() => {
+ const usp = new URLSearchParams();
+ const request = requestFromBody(usp);
+ assert_equals(
+ request.headers.get("Content-Type"),
+ "application/x-www-form-urlencoded;charset=UTF-8",
+ );
+}, "Default Content-Type for Request with URLSearchParams body");
+
+test(() => {
+ const request = requestFromBody("");
+ assert_equals(
+ request.headers.get("Content-Type"),
+ "text/plain;charset=UTF-8",
+ );
+}, "Default Content-Type for Request with string body");
+
+test(() => {
+ const stream = new ReadableStream();
+ const request = requestFromBody(stream);
+ assert_equals(request.headers.get("Content-Type"), null);
+}, "Default Content-Type for Request with ReadableStream body");
+
+// -----------------------------------------------------------------------------
+
+const OVERRIDE_MIME = "test/only; mime=type";
+
+function requestFromBodyWithOverrideMime(body) {
+ return new Request(
+ "https://example.com",
+ {
+ method: "POST",
+ body,
+ headers: { "Content-Type": OVERRIDE_MIME },
+ duplex: "half",
+ },
+ );
+}
+
+test(() => {
+ const request = requestFromBodyWithOverrideMime(undefined);
+ assert_equals(request.headers.get("Content-Type"), OVERRIDE_MIME);
+}, "Can override Content-Type for Request with empty body");
+
+test(() => {
+ const blob = new Blob([]);
+ const request = requestFromBodyWithOverrideMime(blob);
+ assert_equals(request.headers.get("Content-Type"), OVERRIDE_MIME);
+}, "Can override Content-Type for Request with Blob body (no type set)");
+
+test(() => {
+ const blob = new Blob([], { type: "" });
+ const request = requestFromBodyWithOverrideMime(blob);
+ assert_equals(request.headers.get("Content-Type"), OVERRIDE_MIME);
+}, "Can override Content-Type for Request with Blob body (empty type)");
+
+test(() => {
+ const blob = new Blob([], { type: "a/b; c=d" });
+ const request = requestFromBodyWithOverrideMime(blob);
+ assert_equals(request.headers.get("Content-Type"), OVERRIDE_MIME);
+}, "Can override Content-Type for Request with Blob body (set type)");
+
+test(() => {
+ const buffer = new Uint8Array();
+ const request = requestFromBodyWithOverrideMime(buffer);
+ assert_equals(request.headers.get("Content-Type"), OVERRIDE_MIME);
+}, "Can override Content-Type for Request with buffer source body");
+
+test(() => {
+ const formData = new FormData();
+ const request = requestFromBodyWithOverrideMime(formData);
+ assert_equals(request.headers.get("Content-Type"), OVERRIDE_MIME);
+}, "Can override Content-Type for Request with FormData body");
+
+test(() => {
+ const usp = new URLSearchParams();
+ const request = requestFromBodyWithOverrideMime(usp);
+ assert_equals(request.headers.get("Content-Type"), OVERRIDE_MIME);
+}, "Can override Content-Type for Request with URLSearchParams body");
+
+test(() => {
+ const request = requestFromBodyWithOverrideMime("");
+ assert_equals(request.headers.get("Content-Type"), OVERRIDE_MIME);
+}, "Can override Content-Type for Request with string body");
+
+test(() => {
+ const stream = new ReadableStream();
+ const request = requestFromBodyWithOverrideMime(stream);
+ assert_equals(request.headers.get("Content-Type"), OVERRIDE_MIME);
+}, "Can override Content-Type for Request with ReadableStream body");
diff --git a/test/wpt/tests/fetch/api/request/request-init-priority.any.js b/test/wpt/tests/fetch/api/request/request-init-priority.any.js
new file mode 100644
index 0000000..eb5073c
--- /dev/null
+++ b/test/wpt/tests/fetch/api/request/request-init-priority.any.js
@@ -0,0 +1,26 @@
+var priorities = ["high",
+ "low",
+ "auto"
+ ];
+
+for (idx in priorities) {
+ test(() => {
+ new Request("", {priority: priorities[idx]});
+ }, "new Request() with a '" + priorities[idx] + "' priority does not throw an error");
+}
+
+test(() => {
+ assert_throws_js(TypeError, () => {
+ new Request("", {priority: 'invalid'});
+ }, "a new Request() must throw a TypeError if RequestInit's priority is an invalid value");
+}, "new Request() throws a TypeError if any of RequestInit's members' values are invalid");
+
+for (idx in priorities) {
+ promise_test(function(t) {
+ return fetch('hello.txt', { priority: priorities[idx] });
+ }, "fetch() with a '" + priorities[idx] + "' priority completes successfully");
+}
+
+promise_test(function(t) {
+ return promise_rejects_js(t, TypeError, fetch('hello.txt', { priority: 'invalid' }));
+}, "fetch() with an invalid priority returns a rejected promise with a TypeError");
diff --git a/test/wpt/tests/fetch/api/request/request-init-stream.any.js b/test/wpt/tests/fetch/api/request/request-init-stream.any.js
new file mode 100644
index 0000000..f0ae441
--- /dev/null
+++ b/test/wpt/tests/fetch/api/request/request-init-stream.any.js
@@ -0,0 +1,147 @@
+// META: global=window,worker
+
+"use strict";
+
+const duplex = "half";
+const method = "POST";
+
+test(() => {
+ const body = new ReadableStream();
+ const request = new Request("...", { method, body, duplex });
+ assert_equals(request.body, body);
+}, "Constructing a Request with a stream holds the original object.");
+
+test((t) => {
+ const body = new ReadableStream();
+ body.getReader();
+ assert_throws_js(TypeError,
+ () => new Request("...", { method, body, duplex }));
+}, "Constructing a Request with a stream on which getReader() is called");
+
+test((t) => {
+ const body = new ReadableStream();
+ body.getReader().read();
+ assert_throws_js(TypeError,
+ () => new Request("...", { method, body, duplex }));
+}, "Constructing a Request with a stream on which read() is called");
+
+promise_test(async (t) => {
+ const body = new ReadableStream({ pull: c => c.enqueue(new Uint8Array()) });
+ const reader = body.getReader();
+ await reader.read();
+ reader.releaseLock();
+ assert_throws_js(TypeError,
+ () => new Request("...", { method, body, duplex }));
+}, "Constructing a Request with a stream on which read() and releaseLock() are called");
+
+test((t) => {
+ const request = new Request("...", { method: "POST", body: "..." });
+ request.body.getReader();
+ assert_throws_js(TypeError, () => new Request(request));
+ // This doesn't throw.
+ new Request(request, { body: "..." });
+}, "Constructing a Request with a Request on which body.getReader() is called");
+
+test((t) => {
+ const request = new Request("...", { method: "POST", body: "..." });
+ request.body.getReader().read();
+ assert_throws_js(TypeError, () => new Request(request));
+ // This doesn't throw.
+ new Request(request, { body: "..." });
+}, "Constructing a Request with a Request on which body.getReader().read() is called");
+
+promise_test(async (t) => {
+ const request = new Request("...", { method: "POST", body: "..." });
+ const reader = request.body.getReader();
+ await reader.read();
+ reader.releaseLock();
+ assert_throws_js(TypeError, () => new Request(request));
+ // This doesn't throw.
+ new Request(request, { body: "..." });
+}, "Constructing a Request with a Request on which read() and releaseLock() are called");
+
+test((t) => {
+ new Request("...", { method, body: null });
+}, "It is OK to omit .duplex when the body is null.");
+
+test((t) => {
+ new Request("...", { method, body: "..." });
+}, "It is OK to omit .duplex when the body is a string.");
+
+test((t) => {
+ new Request("...", { method, body: new Uint8Array(3) });
+}, "It is OK to omit .duplex when the body is a Uint8Array.");
+
+test((t) => {
+ new Request("...", { method, body: new Blob([]) });
+}, "It is OK to omit .duplex when the body is a Blob.");
+
+test((t) => {
+ const body = new ReadableStream();
+ assert_throws_js(TypeError,
+ () => new Request("...", { method, body }));
+}, "It is error to omit .duplex when the body is a ReadableStream.");
+
+test((t) => {
+ new Request("...", { method, body: null, duplex: "half" });
+}, "It is OK to set .duplex = 'half' when the body is null.");
+
+test((t) => {
+ new Request("...", { method, body: "...", duplex: "half" });
+}, "It is OK to set .duplex = 'half' when the body is a string.");
+
+test((t) => {
+ new Request("...", { method, body: new Uint8Array(3), duplex: "half" });
+}, "It is OK to set .duplex = 'half' when the body is a Uint8Array.");
+
+test((t) => {
+ new Request("...", { method, body: new Blob([]), duplex: "half" });
+}, "It is OK to set .duplex = 'half' when the body is a Blob.");
+
+test((t) => {
+ const body = new ReadableStream();
+ new Request("...", { method, body, duplex: "half" });
+}, "It is OK to set .duplex = 'half' when the body is a ReadableStream.");
+
+test((t) => {
+ const body = null;
+ const duplex = "full";
+ assert_throws_js(TypeError,
+ () => new Request("...", { method, body, duplex }));
+}, "It is error to set .duplex = 'full' when the body is null.");
+
+test((t) => {
+ const body = "...";
+ const duplex = "full";
+ assert_throws_js(TypeError,
+ () => new Request("...", { method, body, duplex }));
+}, "It is error to set .duplex = 'full' when the body is a string.");
+
+test((t) => {
+ const body = new Uint8Array(3);
+ const duplex = "full";
+ assert_throws_js(TypeError,
+ () => new Request("...", { method, body, duplex }));
+}, "It is error to set .duplex = 'full' when the body is a Uint8Array.");
+
+test((t) => {
+ const body = new Blob([]);
+ const duplex = "full";
+ assert_throws_js(TypeError,
+ () => new Request("...", { method, body, duplex }));
+}, "It is error to set .duplex = 'full' when the body is a Blob.");
+
+test((t) => {
+ const body = new ReadableStream();
+ const duplex = "full";
+ assert_throws_js(TypeError,
+ () => new Request("...", { method, body, duplex }));
+}, "It is error to set .duplex = 'full' when the body is a ReadableStream.");
+
+test((t) => {
+ const body = new ReadableStream();
+ const duplex = "half";
+ const req1 = new Request("...", { method, body, duplex });
+ const req2 = new Request(req1);
+}, "It is OK to omit duplex when init.body is not given and input.body is given.");
+
diff --git a/test/wpt/tests/fetch/api/request/request-keepalive-quota.html b/test/wpt/tests/fetch/api/request/request-keepalive-quota.html
new file mode 100644
index 0000000..548ab38
--- /dev/null
+++ b/test/wpt/tests/fetch/api/request/request-keepalive-quota.html
@@ -0,0 +1,97 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>Request Keepalive Quota Tests</title>
+ <meta name="timeout" content="long">
+ <meta name="help" href="https://fetch.spec.whatwg.org/#request">
+ <meta name="help" href="https://fetch.spec.whatwg.org/#body-mixin">
+ <meta name="author" title="Microsoft Edge" href="https://www.microsoft.com">
+ <meta name="variant" content="?include=fast">
+ <meta name="variant" content="?include=slow-1">
+ <meta name="variant" content="?include=slow-2">
+ <meta name="variant" content="?include=slow-3">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/common/subset-tests-by-key.js"></script>
+ </head>
+ <body>
+ <script>
+ 'use strict';
+
+ // We want to ensure that our keepalive requests hang slightly before completing so we can validate
+ // the effects of a rolling quota. To do this we will utilize trickle.py with a 1s delay. This should
+ // prevent any of the Fetch's from finishing in this window.
+ const trickleURL = '../resources/trickle.py?count=1&ms=';
+ const noDelay = 0;
+ const standardDelay = 1000;
+ function wait(ms) {
+ return new Promise(resolve => step_timeout(resolve, ms));
+ }
+
+ // We should expect 64KiB of rolling quota for any type of keep-alive request sent.
+ const expectedQuota = 65536;
+
+ function fetchKeepAliveRequest(delay, bodySize) {
+ // Create a body of the specified size that's filled with *'s
+ const body = '*'.repeat(bodySize);
+ return fetch(trickleURL + delay, {keepalive: true, body, method: 'POST'}).then(res => {
+ return res.text();
+ }).then(() => {
+ return wait(1);
+ });
+ }
+
+ // Test 1 Byte
+ subsetTestByKey("fast", promise_test, function(test) {
+ return fetchKeepAliveRequest(noDelay, 1 /* bodySize */);
+ }, 'A Keep-Alive fetch() with a small body should succeed.');
+
+ // Test Quota full limit
+ subsetTestByKey("fast", promise_test, function(test) {
+ return fetchKeepAliveRequest(noDelay, expectedQuota /* bodySize */);
+ }, 'A Keep-Alive fetch() with a body at the Quota Limit should succeed.');
+
+ // Test Quota + 1 Byte
+ subsetTestByKey("fast", promise_test, function(test) {
+ return promise_rejects_js(test, TypeError, fetchKeepAliveRequest(noDelay, expectedQuota + 1));
+ }, 'A Keep-Alive fetch() with a body over the Quota Limit should reject.');
+
+ // Test the Quota becomes available upon promise completion.
+ subsetTestByKey("slow-1", promise_test, function (test) {
+ // Fill our Quota then try to send a second fetch.
+ return fetchKeepAliveRequest(standardDelay, expectedQuota).then(() => {
+ // Now validate that we can send another Keep-Alive fetch for the full size of the quota.
+ return fetchKeepAliveRequest(noDelay, expectedQuota);
+ });
+ }, 'A Keep-Alive fetch() should return its allocated Quota upon promise resolution.');
+
+ // Ensure only the correct amount of Quota becomes available when a fetch completes.
+ subsetTestByKey("slow-2", promise_test, function(test) {
+ // Create a fetch that uses all but 1 Byte of the Quota and runs for 2x as long as the other requests.
+ const first = fetchKeepAliveRequest(standardDelay * 2, expectedQuota - 1);
+
+ // Now create a single Byte request that will complete quicker.
+ const second = fetchKeepAliveRequest(standardDelay, 1 /* bodySize */).then(() => {
+ // We shouldn't be able to create a 2 Byte request right now as only 1 Byte should have freed up.
+ return promise_rejects_js(test, TypeError, fetchKeepAliveRequest(noDelay, 2 /* bodySize */));
+ }).then(() => {
+ // Now validate that we can send another Keep-Alive fetch for just 1 Byte.
+ return fetchKeepAliveRequest(noDelay, 1 /* bodySize */);
+ });
+
+ return Promise.all([first, second]);
+ }, 'A Keep-Alive fetch() should return only its allocated Quota upon promise resolution.');
+
+ // Test rejecting a fetch() after the quota is used up.
+ subsetTestByKey("slow-3", promise_test, function (test) {
+ // Fill our Quota then try to send a second fetch.
+ const p = fetchKeepAliveRequest(standardDelay, expectedQuota);
+
+ const q = promise_rejects_js(test, TypeError, fetchKeepAliveRequest(noDelay, 1 /* bodySize */));
+ return Promise.all([p, q]);
+ }, 'A Keep-Alive fetch() should not be allowed if the Quota is used up.');
+
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/fetch/api/request/request-keepalive.any.js b/test/wpt/tests/fetch/api/request/request-keepalive.any.js
new file mode 100644
index 0000000..cb4506d
--- /dev/null
+++ b/test/wpt/tests/fetch/api/request/request-keepalive.any.js
@@ -0,0 +1,17 @@
+// META: global=window,worker
+// META: title=Request keepalive
+// META: script=/common/utils.js
+// META: script=/common/get-host-info.sub.js
+
+test(() => {
+ assert_false(new Request('/').keepalive, 'default');
+ assert_true(new Request('/', {keepalive: true}).keepalive, 'true');
+ assert_false(new Request('/', {keepalive: false}).keepalive, 'false');
+ assert_true(new Request('/', {keepalive: 1}).keepalive, 'truish');
+ assert_false(new Request('/', {keepalive: 0}).keepalive, 'falsy');
+}, 'keepalive flag');
+
+test(() => {
+ const init = {method: 'POST', keepalive: true, body: new ReadableStream()};
+ assert_throws_js(TypeError, () => {new Request('/', init)});
+}, 'keepalive flag with stream body');
diff --git a/test/wpt/tests/fetch/api/request/request-reset-attributes.https.html b/test/wpt/tests/fetch/api/request/request-reset-attributes.https.html
new file mode 100644
index 0000000..7be3608
--- /dev/null
+++ b/test/wpt/tests/fetch/api/request/request-reset-attributes.https.html
@@ -0,0 +1,96 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script>
+<body>
+<script>
+const worker = 'resources/request-reset-attributes-worker.js';
+
+function wait(ms) {
+ return new Promise(resolve => step_timeout(resolve, ms));
+}
+
+promise_test(async (t) => {
+ const scope = 'resources/hello.txt?name=isReloadNavigation';
+ let frame;
+ let reg;
+
+ try {
+ reg = await service_worker_unregister_and_register(t, worker, scope);
+ await wait_for_state(t, reg.installing, 'activated');
+ frame = await with_iframe(scope);
+ assert_equals(frame.contentDocument.body.textContent,
+ 'old: false, new: false');
+ await new Promise((resolve) => {
+ frame.onload = resolve;
+ frame.contentWindow.location.reload();
+ });
+ assert_equals(frame.contentDocument.body.textContent,
+ 'old: true, new: false');
+ } finally {
+ if (frame) {
+ frame.remove();
+ }
+ if (reg) {
+ await reg.unregister();
+ }
+ }
+ }, 'Request.isReloadNavigation is reset with non-empty RequestInit');
+
+promise_test(async (t) => {
+ const scope = 'resources/hello.html?name=isHistoryNavigation';
+ let frame;
+ let reg;
+
+ try {
+ reg = await service_worker_unregister_and_register(t, worker, scope);
+ await wait_for_state(t, reg.installing, 'activated');
+ frame = await with_iframe(scope);
+ assert_equals(frame.contentDocument.body.textContent,
+ 'old: false, new: false');
+ // Use step_timeout(0) to ensure the history entry is created for Blink
+ // and WebKit. See https://bugs.webkit.org/show_bug.cgi?id=42861.
+ await wait(0);
+ await new Promise((resolve) => {
+ frame.onload = resolve;
+ frame.src = 'resources/hello.html?ignore';
+ });
+ await wait(0);
+ await new Promise((resolve) => {
+ frame.onload = resolve;
+ frame.contentWindow.history.go(-1);
+ });
+ assert_equals(frame.contentDocument.body.textContent,
+ 'old: true, new: false');
+ } finally {
+ if (frame) {
+ frame.remove();
+ }
+ if (reg) {
+ await reg.unregister();
+ }
+ }
+}, 'Request.isHistoryNavigation is reset with non-empty RequestInit');
+
+promise_test(async (t) => {
+ const scope = 'resources/hello.txt?name=mode';
+ let frame;
+ let reg;
+
+ try {
+ reg = await service_worker_unregister_and_register(t, worker, scope);
+ await wait_for_state(t, reg.installing, 'activated');
+ frame = await with_iframe(scope);
+ assert_equals(frame.contentDocument.body.textContent,
+ 'old: navigate, new: same-origin');
+ } finally {
+ if (frame) {
+ frame.remove();
+ }
+ if (reg) {
+ await reg.unregister();
+ }
+ }
+ }, 'Request.mode is reset with non-empty RequestInit when it\'s "navigate"');
+</script>
diff --git a/test/wpt/tests/fetch/api/request/request-structure.any.js b/test/wpt/tests/fetch/api/request/request-structure.any.js
new file mode 100644
index 0000000..5e78553
--- /dev/null
+++ b/test/wpt/tests/fetch/api/request/request-structure.any.js
@@ -0,0 +1,143 @@
+// META: global=window,worker
+// META: title=Request structure
+
+var request = new Request("");
+var methods = ["clone",
+ //Request implements Body
+ "arrayBuffer",
+ "blob",
+ "formData",
+ "json",
+ "text"
+ ];
+var attributes = ["method",
+ "url",
+ "headers",
+ "destination",
+ "referrer",
+ "referrerPolicy",
+ "mode",
+ "credentials",
+ "cache",
+ "redirect",
+ "integrity",
+ "isReloadNavigation",
+ "isHistoryNavigation",
+ "duplex",
+ //Request implements Body
+ "bodyUsed"
+ ];
+var internalAttributes = ["priority",
+ "internalpriority",
+ "blocking"
+ ];
+
+function isReadOnly(request, attributeToCheck) {
+ var defaultValue = undefined;
+ var newValue = undefined;
+ switch (attributeToCheck) {
+ case "method":
+ defaultValue = "GET";
+ newValue = "POST";
+ break;
+
+ case "url":
+ //default value is base url
+ //i.e http://example.com/fetch/api/request-structure.html
+ newValue = "http://url.test";
+ break;
+
+ case "headers":
+ request.headers = new Headers ({"name":"value"});
+ assert_false(request.headers.has("name"), "Headers attribute is read only");
+ return;
+
+ case "destination":
+ defaultValue = "";
+ newValue = "worker";
+ break;
+
+ case "referrer":
+ defaultValue = "about:client";
+ newValue = "http://url.test";
+ break;
+
+ case "referrerPolicy":
+ defaultValue = "";
+ newValue = "unsafe-url";
+ break;
+
+ case "mode":
+ defaultValue = "cors";
+ newValue = "navigate";
+ break;
+
+ case "credentials":
+ defaultValue = "same-origin";
+ newValue = "cors";
+ break;
+
+ case "cache":
+ defaultValue = "default";
+ newValue = "reload";
+ break;
+
+ case "redirect":
+ defaultValue = "follow";
+ newValue = "manual";
+ break;
+
+ case "integrity":
+ newValue = "CannotWriteIntegrity";
+ break;
+
+ case "bodyUsed":
+ defaultValue = false;
+ newValue = true;
+ break;
+
+ case "isReloadNavigation":
+ defaultValue = false;
+ newValue = true;
+ break;
+
+ case "isHistoryNavigation":
+ defaultValue = false;
+ newValue = true;
+ break;
+
+ case "duplex":
+ defaultValue = "half";
+ newValue = "full";
+ break;
+
+ default:
+ return;
+ }
+
+ request[attributeToCheck] = newValue;
+ if (defaultValue === undefined)
+ assert_not_equals(request[attributeToCheck], newValue, "Attribute " + attributeToCheck + " is read only");
+ else
+ assert_equals(request[attributeToCheck], defaultValue,
+ "Attribute " + attributeToCheck + " is read only. Default value is " + defaultValue);
+}
+
+for (var idx in methods) {
+ test(function() {
+ assert_true(methods[idx] in request, "request has " + methods[idx] + " method");
+ }, "Request has " + methods[idx] + " method");
+}
+
+for (var idx in attributes) {
+ test(function() {
+ assert_true(attributes[idx] in request, "request has " + attributes[idx] + " attribute");
+ isReadOnly(request, attributes[idx]);
+ }, "Check " + attributes[idx] + " attribute");
+}
+
+for (var idx in internalAttributes) {
+ test(function() {
+ assert_false(internalAttributes[idx] in request, "request does not expose " + internalAttributes[idx] + " attribute");
+ }, "Request does not expose " + internalAttributes[idx] + " attribute");
+} \ No newline at end of file
diff --git a/test/wpt/tests/fetch/api/request/resources/cache.py b/test/wpt/tests/fetch/api/request/resources/cache.py
new file mode 100644
index 0000000..ca0bd64
--- /dev/null
+++ b/test/wpt/tests/fetch/api/request/resources/cache.py
@@ -0,0 +1,67 @@
+from wptserve.utils import isomorphic_decode
+
+def main(request, response):
+ token = request.GET.first(b"token", None)
+ if b"querystate" in request.GET:
+ from json import JSONEncoder
+ response.headers.set(b"Content-Type", b"text/plain")
+ return JSONEncoder().encode(request.server.stash.take(token))
+ content = request.GET.first(b"content", None)
+ tag = request.GET.first(b"tag", None)
+ date = request.GET.first(b"date", None)
+ expires = request.GET.first(b"expires", None)
+ vary = request.GET.first(b"vary", None)
+ cc = request.GET.first(b"cache_control", None)
+ redirect = request.GET.first(b"redirect", None)
+ inm = request.headers.get(b"If-None-Match", None)
+ ims = request.headers.get(b"If-Modified-Since", None)
+ pragma = request.headers.get(b"Pragma", None)
+ cache_control = request.headers.get(b"Cache-Control", None)
+ ignore = b"ignore" in request.GET
+
+ if tag:
+ tag = b'"%s"' % tag
+
+ server_state = request.server.stash.take(token)
+ if not server_state:
+ server_state = []
+ state = dict()
+ if not ignore:
+ if inm:
+ state[u"If-None-Match"] = isomorphic_decode(inm)
+ if ims:
+ state[u"If-Modified-Since"] = isomorphic_decode(ims)
+ if pragma:
+ state[u"Pragma"] = isomorphic_decode(pragma)
+ if cache_control:
+ state[u"Cache-Control"] = isomorphic_decode(cache_control)
+ server_state.append(state)
+ request.server.stash.put(token, server_state)
+
+ if tag:
+ response.headers.set(b"ETag", b'%s' % tag)
+ elif date:
+ response.headers.set(b"Last-Modified", date)
+ if expires:
+ response.headers.set(b"Expires", expires)
+ if vary:
+ response.headers.set(b"Vary", vary)
+ if cc:
+ response.headers.set(b"Cache-Control", cc)
+
+ # The only-if-cached redirect tests wants CORS to be okay, the other tests
+ # are all same-origin anyways and don't care.
+ response.headers.set(b"Access-Control-Allow-Origin", b"*")
+
+ if redirect:
+ response.headers.set(b"Location", redirect)
+ response.status = (302, b"Redirect")
+ return b""
+ elif ((inm is not None and inm == tag) or
+ (ims is not None and ims == date)):
+ response.status = (304, b"Not Modified")
+ return b""
+ else:
+ response.status = (200, b"OK")
+ response.headers.set(b"Content-Type", b"text/plain")
+ return content
diff --git a/test/wpt/tests/fetch/api/request/resources/hello.txt b/test/wpt/tests/fetch/api/request/resources/hello.txt
new file mode 100644
index 0000000..ce01362
--- /dev/null
+++ b/test/wpt/tests/fetch/api/request/resources/hello.txt
@@ -0,0 +1 @@
+hello
diff --git a/test/wpt/tests/fetch/api/request/resources/request-reset-attributes-worker.js b/test/wpt/tests/fetch/api/request/resources/request-reset-attributes-worker.js
new file mode 100644
index 0000000..4b264ca
--- /dev/null
+++ b/test/wpt/tests/fetch/api/request/resources/request-reset-attributes-worker.js
@@ -0,0 +1,19 @@
+self.addEventListener('fetch', (event) => {
+ const params = new URL(event.request.url).searchParams;
+ if (params.has('ignore')) {
+ return;
+ }
+ if (!params.has('name')) {
+ event.respondWith(Promise.reject(TypeError('No name is provided.')));
+ return;
+ }
+
+ const name = params.get('name');
+ const old_attribute = event.request[name];
+ // If any of |init|'s member is present...
+ const init = {cache: 'no-store'}
+ const new_attribute = (new Request(event.request, init))[name];
+
+ event.respondWith(
+ new Response(`old: ${old_attribute}, new: ${new_attribute}`));
+ });
diff --git a/test/wpt/tests/fetch/api/request/url-encoding.html b/test/wpt/tests/fetch/api/request/url-encoding.html
new file mode 100644
index 0000000..31c1ed3
--- /dev/null
+++ b/test/wpt/tests/fetch/api/request/url-encoding.html
@@ -0,0 +1,25 @@
+<!doctype html>
+<meta charset=windows-1252>
+<title>Fetch: URL encoding</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script>
+const expectedURL = new URL("?%C3%9F", location.href).href;
+const expectedURL2 = new URL("?%EF%BF%BD", location.href).href;
+test(() => {
+ let r = new Request("?\u00DF");
+ assert_equals(r.url, expectedURL);
+
+ r = new Request("?\uD83D");
+ assert_equals(r.url, expectedURL2);
+}, "URL encoding and Request");
+
+promise_test(() => {
+ return fetch("?\u00DF").then(res => {
+ assert_equals(res.url, expectedURL);
+ return fetch("?\uD83D").then(res2 => {
+ assert_equals(res2.url, expectedURL2);
+ });
+ });
+}, "URL encoding and fetch()");
+</script>
diff --git a/test/wpt/tests/fetch/api/resources/authentication.py b/test/wpt/tests/fetch/api/resources/authentication.py
new file mode 100644
index 0000000..8b6b00b
--- /dev/null
+++ b/test/wpt/tests/fetch/api/resources/authentication.py
@@ -0,0 +1,14 @@
+def main(request, response):
+ user = request.auth.username
+ password = request.auth.password
+
+ if user == b"user" and password == b"password":
+ return b"Authentication done"
+
+ realm = b"test"
+ if b"realm" in request.GET:
+ realm = request.GET.first(b"realm")
+
+ return ((401, b"Unauthorized"),
+ [(b"WWW-Authenticate", b'Basic realm="' + realm + b'"')],
+ b"Please login with credentials 'user' and 'password'")
diff --git a/test/wpt/tests/fetch/api/resources/bad-chunk-encoding.py b/test/wpt/tests/fetch/api/resources/bad-chunk-encoding.py
new file mode 100644
index 0000000..94a77ad
--- /dev/null
+++ b/test/wpt/tests/fetch/api/resources/bad-chunk-encoding.py
@@ -0,0 +1,13 @@
+import time
+
+def main(request, response):
+ delay = float(request.GET.first(b"ms", 1000)) / 1E3
+ count = int(request.GET.first(b"count", 50))
+ time.sleep(delay)
+ response.headers.set(b"Transfer-Encoding", b"chunked")
+ response.write_status_headers()
+ time.sleep(delay)
+ for i in range(count):
+ response.writer.write_content(b"a\r\nTEST_CHUNK\r\n")
+ time.sleep(delay)
+ response.writer.write_content(b"garbage")
diff --git a/test/wpt/tests/fetch/api/resources/basic.html b/test/wpt/tests/fetch/api/resources/basic.html
new file mode 100644
index 0000000..e23afd4
--- /dev/null
+++ b/test/wpt/tests/fetch/api/resources/basic.html
@@ -0,0 +1,5 @@
+<!DOCTYPE html>
+<!--
+ Duplicating /common/blank.html to make service worker scoping simpler in
+ ../abort/serviceworker-intercepted.https.html
+-->
diff --git a/test/wpt/tests/fetch/api/resources/cache.py b/test/wpt/tests/fetch/api/resources/cache.py
new file mode 100644
index 0000000..4de751e
--- /dev/null
+++ b/test/wpt/tests/fetch/api/resources/cache.py
@@ -0,0 +1,18 @@
+ETAG = b'"123abc"'
+CONTENT_TYPE = b"text/plain"
+CONTENT = b"lorem ipsum dolor sit amet"
+
+
+def main(request, response):
+ # let caching kick in if possible (conditional GET)
+ etag = request.headers.get(b"If-None-Match", None)
+ if etag == ETAG:
+ response.headers.set(b"X-HTTP-STATUS", 304)
+ response.status = (304, b"Not Modified")
+ return b""
+
+ # cache miss, so respond with the actual content
+ response.status = (200, b"OK")
+ response.headers.set(b"ETag", ETAG)
+ response.headers.set(b"Content-Type", CONTENT_TYPE)
+ return CONTENT
diff --git a/test/wpt/tests/fetch/api/resources/clean-stash.py b/test/wpt/tests/fetch/api/resources/clean-stash.py
new file mode 100644
index 0000000..ee8c69a
--- /dev/null
+++ b/test/wpt/tests/fetch/api/resources/clean-stash.py
@@ -0,0 +1,6 @@
+def main(request, response):
+ token = request.GET.first(b"token")
+ if request.server.stash.take(token) is not None:
+ return b"1"
+ else:
+ return b"0"
diff --git a/test/wpt/tests/fetch/api/resources/cors-top.txt b/test/wpt/tests/fetch/api/resources/cors-top.txt
new file mode 100644
index 0000000..83a3157
--- /dev/null
+++ b/test/wpt/tests/fetch/api/resources/cors-top.txt
@@ -0,0 +1 @@
+top \ No newline at end of file
diff --git a/test/wpt/tests/fetch/api/resources/cors-top.txt.headers b/test/wpt/tests/fetch/api/resources/cors-top.txt.headers
new file mode 100644
index 0000000..cb762ef
--- /dev/null
+++ b/test/wpt/tests/fetch/api/resources/cors-top.txt.headers
@@ -0,0 +1 @@
+Access-Control-Allow-Origin: *
diff --git a/test/wpt/tests/fetch/api/resources/data.json b/test/wpt/tests/fetch/api/resources/data.json
new file mode 100644
index 0000000..76519fa
--- /dev/null
+++ b/test/wpt/tests/fetch/api/resources/data.json
@@ -0,0 +1 @@
+{"key": "value"}
diff --git a/test/wpt/tests/fetch/api/resources/dump-authorization-header.py b/test/wpt/tests/fetch/api/resources/dump-authorization-header.py
new file mode 100644
index 0000000..a651aeb
--- /dev/null
+++ b/test/wpt/tests/fetch/api/resources/dump-authorization-header.py
@@ -0,0 +1,14 @@
+def main(request, response):
+ headers = [(b"Content-Type", "text/html"),
+ (b"Cache-Control", b"no-cache")]
+
+ if b"Origin" in request.headers:
+ headers.append((b"Access-Control-Allow-Origin", request.headers.get(b"Origin", b"")))
+ headers.append((b"Access-Control-Allow-Credentials", b"true"))
+ else:
+ headers.append((b"Access-Control-Allow-Origin", b"*"))
+ headers.append((b"Access-Control-Allow-Headers", b'Authorization'))
+
+ if b"authorization" in request.headers:
+ return 200, headers, request.headers.get(b"Authorization")
+ return 200, headers, "none"
diff --git a/test/wpt/tests/fetch/api/resources/echo-content.h2.py b/test/wpt/tests/fetch/api/resources/echo-content.h2.py
new file mode 100644
index 0000000..0be3ece
--- /dev/null
+++ b/test/wpt/tests/fetch/api/resources/echo-content.h2.py
@@ -0,0 +1,7 @@
+def handle_headers(frame, request, response):
+ response.status = 200
+ response.headers.update([('Content-Type', 'text/plain')])
+ response.write_status_headers()
+
+def handle_data(frame, request, response):
+ response.writer.write_data(frame.data)
diff --git a/test/wpt/tests/fetch/api/resources/echo-content.py b/test/wpt/tests/fetch/api/resources/echo-content.py
new file mode 100644
index 0000000..5e137e1
--- /dev/null
+++ b/test/wpt/tests/fetch/api/resources/echo-content.py
@@ -0,0 +1,12 @@
+from wptserve.utils import isomorphic_encode
+
+def main(request, response):
+
+ headers = [(b"X-Request-Method", isomorphic_encode(request.method)),
+ (b"X-Request-Content-Length", request.headers.get(b"Content-Length", b"NO")),
+ (b"X-Request-Content-Type", request.headers.get(b"Content-Type", b"NO")),
+ # Avoid any kind of content sniffing on the response.
+ (b"Content-Type", b"text/plain")]
+ content = request.body
+
+ return headers, content
diff --git a/test/wpt/tests/fetch/api/resources/empty.txt b/test/wpt/tests/fetch/api/resources/empty.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/wpt/tests/fetch/api/resources/empty.txt
diff --git a/test/wpt/tests/fetch/api/resources/infinite-slow-response.py b/test/wpt/tests/fetch/api/resources/infinite-slow-response.py
new file mode 100644
index 0000000..a26cd80
--- /dev/null
+++ b/test/wpt/tests/fetch/api/resources/infinite-slow-response.py
@@ -0,0 +1,35 @@
+import time
+
+
+def url_dir(request):
+ return u'/'.join(request.url_parts.path.split(u'/')[:-1]) + u'/'
+
+
+def stash_write(request, key, value):
+ """Write to the stash, overwriting any previous value"""
+ request.server.stash.take(key, url_dir(request))
+ request.server.stash.put(key, value, url_dir(request))
+
+
+def main(request, response):
+ stateKey = request.GET.first(b"stateKey", b"")
+ abortKey = request.GET.first(b"abortKey", b"")
+
+ if stateKey:
+ stash_write(request, stateKey, 'open')
+
+ response.headers.set(b"Content-type", b"text/plain")
+ response.write_status_headers()
+
+ # Writing an initial 2k so browsers realise it's there. *shrug*
+ response.writer.write(b"." * 2048)
+
+ while True:
+ if not response.writer.write(b"."):
+ break
+ if abortKey and request.server.stash.take(abortKey, url_dir(request)):
+ break
+ time.sleep(0.01)
+
+ if stateKey:
+ stash_write(request, stateKey, 'closed')
diff --git a/test/wpt/tests/fetch/api/resources/inspect-headers.py b/test/wpt/tests/fetch/api/resources/inspect-headers.py
new file mode 100644
index 0000000..9ed566e
--- /dev/null
+++ b/test/wpt/tests/fetch/api/resources/inspect-headers.py
@@ -0,0 +1,24 @@
+def main(request, response):
+ headers = []
+ if b"headers" in request.GET:
+ checked_headers = request.GET.first(b"headers").split(b"|")
+ for header in checked_headers:
+ if header in request.headers:
+ headers.append((b"x-request-" + header, request.headers.get(header, b"")))
+
+ if b"cors" in request.GET:
+ if b"Origin" in request.headers:
+ headers.append((b"Access-Control-Allow-Origin", request.headers.get(b"Origin", b"")))
+ else:
+ headers.append((b"Access-Control-Allow-Origin", b"*"))
+ headers.append((b"Access-Control-Allow-Credentials", b"true"))
+ headers.append((b"Access-Control-Allow-Methods", b"GET, POST, HEAD"))
+ exposed_headers = [b"x-request-" + header for header in checked_headers]
+ headers.append((b"Access-Control-Expose-Headers", b", ".join(exposed_headers)))
+ if b"allow_headers" in request.GET:
+ headers.append((b"Access-Control-Allow-Headers", request.GET[b'allow_headers']))
+ else:
+ headers.append((b"Access-Control-Allow-Headers", b", ".join(request.headers)))
+
+ headers.append((b"content-type", b"text/plain"))
+ return headers, b""
diff --git a/test/wpt/tests/fetch/api/resources/keepalive-helper.js b/test/wpt/tests/fetch/api/resources/keepalive-helper.js
new file mode 100644
index 0000000..ad1d4b2
--- /dev/null
+++ b/test/wpt/tests/fetch/api/resources/keepalive-helper.js
@@ -0,0 +1,99 @@
+// Utility functions to help testing keepalive requests.
+
+// Returns a URL to an iframe that loads a keepalive URL on iframe loaded.
+//
+// The keepalive URL points to a target that stores `token`. The token will then
+// be posted back on iframe loaded to the parent document.
+// `method` defaults to GET.
+// `frameOrigin` to specify the origin of the iframe to load. If not set,
+// default to a different site origin.
+// `requestOrigin` to specify the origin of the fetch request target.
+// `sendOn` to specify the name of the event when the keepalive request should
+// be sent instead of the default 'load'.
+// `mode` to specify the fetch request's CORS mode.
+// `disallowOrigin` to ask the iframe to set up a server that forbids CORS
+// requests.
+function getKeepAliveIframeUrl(token, method, {
+ frameOrigin = 'DEFAULT',
+ requestOrigin = '',
+ sendOn = 'load',
+ mode = 'cors',
+ disallowOrigin = false
+} = {}) {
+ const https = location.protocol.startsWith('https');
+ frameOrigin = frameOrigin === 'DEFAULT' ?
+ get_host_info()[https ? 'HTTPS_NOTSAMESITE_ORIGIN' : 'HTTP_NOTSAMESITE_ORIGIN'] :
+ frameOrigin;
+ return `${frameOrigin}/fetch/api/resources/keepalive-iframe.html?` +
+ `token=${token}&` +
+ `method=${method}&` +
+ `sendOn=${sendOn}&` +
+ `mode=${mode}&` + (disallowOrigin ? `disallowOrigin=1&` : ``) +
+ `origin=${requestOrigin}`;
+}
+
+// Returns a different-site URL to an iframe that loads a keepalive URL.
+//
+// By default, the keepalive URL points to a target that redirects to another
+// same-origin destination storing `token`. The token will then be posted back
+// to parent document.
+//
+// The URL redirects can be customized from `origin1` to `origin2` if provided.
+// Sets `withPreflight` to true to get URL enabling preflight.
+function getKeepAliveAndRedirectIframeUrl(
+ token, origin1, origin2, withPreflight) {
+ const https = location.protocol.startsWith('https');
+ const frameOrigin =
+ get_host_info()[https ? 'HTTPS_NOTSAMESITE_ORIGIN' : 'HTTP_NOTSAMESITE_ORIGIN'];
+ return `${frameOrigin}/fetch/api/resources/keepalive-redirect-iframe.html?` +
+ `token=${token}&` +
+ `origin1=${origin1}&` +
+ `origin2=${origin2}&` + (withPreflight ? `with-headers` : ``);
+}
+
+async function iframeLoaded(iframe) {
+ return new Promise((resolve) => iframe.addEventListener('load', resolve));
+}
+
+// Obtains the token from the message posted by iframe after loading
+// `getKeepAliveAndRedirectIframeUrl()`.
+async function getTokenFromMessage() {
+ return new Promise((resolve) => {
+ window.addEventListener('message', (event) => {
+ resolve(event.data);
+ }, {once: true});
+ });
+}
+
+// Tells if `token` has been stored in the server.
+async function queryToken(token) {
+ const response = await fetch(`../resources/stash-take.py?key=${token}`);
+ const json = await response.json();
+ return json;
+}
+
+// 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(() => {
+ return queryToken(token);
+ })
+ .then((result) => {
+ assert_equals(result, 'on');
+ })
+ .then(() => {
+ test.done();
+ })
+ .catch(test.step_func((e) => {
+ if (shouldPass) {
+ assert_unreached(e);
+ } else {
+ test.done();
+ }
+ }));
+ }, testName);
+}
diff --git a/test/wpt/tests/fetch/api/resources/keepalive-iframe.html b/test/wpt/tests/fetch/api/resources/keepalive-iframe.html
new file mode 100644
index 0000000..335a1f8
--- /dev/null
+++ b/test/wpt/tests/fetch/api/resources/keepalive-iframe.html
@@ -0,0 +1,21 @@
+<!doctype html>
+<html>
+<meta charset="utf-8">
+<script>
+const SEARCH_PARAMS = new URL(location.href).searchParams;
+const ORIGIN = SEARCH_PARAMS.get('origin') || '';
+const TOKEN = SEARCH_PARAMS.get('token') || '';
+const METHOD = SEARCH_PARAMS.get('method') || 'GET';
+const SEND_ON_EVENT = SEARCH_PARAMS.get('sendOn') || 'load';
+const MODE = SEARCH_PARAMS.get('mode') || 'cors';
+const DISALLOW_ORIGIN = SEARCH_PARAMS.get('disallow_origin') || '';
+// CORS requests are allowed by this URL by default.
+const url = `${ORIGIN}/fetch/api/resources/stash-put.py?key=${TOKEN}&value=on` +
+(DISALLOW_ORIGIN ? `&disallow_origin=1` : '');
+
+addEventListener(SEND_ON_EVENT, () => {
+ let p = fetch(url, {keepalive: true, method: METHOD, mode: MODE});
+ window.parent.postMessage(TOKEN, '*');
+});
+</script>
+</html>
diff --git a/test/wpt/tests/fetch/api/resources/keepalive-redirect-iframe.html b/test/wpt/tests/fetch/api/resources/keepalive-redirect-iframe.html
new file mode 100644
index 0000000..fdee00f
--- /dev/null
+++ b/test/wpt/tests/fetch/api/resources/keepalive-redirect-iframe.html
@@ -0,0 +1,23 @@
+<!doctype html>
+<html>
+<meta charset="utf-8">
+<script>
+const SEARCH_PARAMS = new URL(location.href).searchParams;
+const ORIGIN1 = SEARCH_PARAMS.get('origin1') || '';
+const ORIGIN2 = SEARCH_PARAMS.get('origin2') || '';
+const WITH_HEADERS = !!SEARCH_PARAMS.has('with-headers');
+const TOKEN = SEARCH_PARAMS.get('token') || '';
+
+const url =
+ `${ORIGIN1}/fetch/api/resources/redirect.py?` +
+ `delay=500&` +
+ `allow_headers=foo&` +
+ `location=${ORIGIN2}/fetch/api/resources/stash-put.py?key=${TOKEN}%26value=on`;
+
+addEventListener('load', () => {
+ const headers = WITH_HEADERS ? {'foo': 'bar'} : undefined;
+ let p = fetch(url, {keepalive: true, headers});
+ window.parent.postMessage(TOKEN, '*');
+});
+</script>
+</html>
diff --git a/test/wpt/tests/fetch/api/resources/keepalive-redirect-window.html b/test/wpt/tests/fetch/api/resources/keepalive-redirect-window.html
new file mode 100644
index 0000000..c186507
--- /dev/null
+++ b/test/wpt/tests/fetch/api/resources/keepalive-redirect-window.html
@@ -0,0 +1,42 @@
+<!doctype html>
+<html>
+<meta charset="utf-8">
+<script src="/common/utils.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script>
+const TOKEN = token();
+const {
+ HTTP_NOTSAMESITE_ORIGIN,
+ HTTP_REMOTE_ORIGIN,
+ HTTP_REMOTE_ORIGIN_WITH_DIFFERENT_PORT
+} = get_host_info();
+
+const SEARCH_PARAMS = new URL(location.href).searchParams;
+const WITH_HEADERS = !!SEARCH_PARAMS.has('with-headers');
+const ORIGIN1 = SEARCH_PARAMS.get('origin1') || '';
+const ORIGIN2 = SEARCH_PARAMS.get('origin2') || '';
+const URL2 = SEARCH_PARAMS.get('url2') || '';
+
+const REDIRECT_DESTINATION = URL2 ? URL2 :
+ `${ORIGIN2}/fetch/api/resources/stash-put.py` +
+ `?key=${TOKEN}&value=on`;
+const FROM_URL =
+ `${ORIGIN1}/fetch/api/resources/redirect.py?` +
+ `delay=500&` +
+ `allow_headers=foo&` +
+ `location=${encodeURIComponent(REDIRECT_DESTINATION)}`;
+
+addEventListener('load', () => {
+ const headers = WITH_HEADERS ? {'foo': 'bar'} : undefined;
+ const iframe = document.createElement('iframe');
+ document.body.appendChild(iframe);
+ iframe.contentWindow.addEventListener('unload', () => {
+ iframe.contentWindow.fetch(FROM_URL, {keepalive: true, headers});
+ });
+
+ window.opener.postMessage(TOKEN, '*');
+ // Do NOT remove `iframe` here. We want to check the case where the nested
+ // frame is implicitly closed by window closure.
+});
+</script>
+</html>
diff --git a/test/wpt/tests/fetch/api/resources/method.py b/test/wpt/tests/fetch/api/resources/method.py
new file mode 100644
index 0000000..c1a111b
--- /dev/null
+++ b/test/wpt/tests/fetch/api/resources/method.py
@@ -0,0 +1,18 @@
+from wptserve.utils import isomorphic_encode
+
+def main(request, response):
+ headers = []
+ if b"cors" in request.GET:
+ headers.append((b"Access-Control-Allow-Origin", b"*"))
+ headers.append((b"Access-Control-Allow-Credentials", b"true"))
+ headers.append((b"Access-Control-Allow-Methods", b"GET, POST, PUT, FOO"))
+ headers.append((b"Access-Control-Allow-Headers", b"x-test, x-foo"))
+ headers.append((b"Access-Control-Expose-Headers", b"x-request-method"))
+
+ headers.append((b"x-request-method", isomorphic_encode(request.method)))
+ headers.append((b"x-request-content-type", request.headers.get(b"Content-Type", b"NO")))
+ headers.append((b"x-request-content-length", request.headers.get(b"Content-Length", b"NO")))
+ headers.append((b"x-request-content-encoding", request.headers.get(b"Content-Encoding", b"NO")))
+ headers.append((b"x-request-content-language", request.headers.get(b"Content-Language", b"NO")))
+ headers.append((b"x-request-content-location", request.headers.get(b"Content-Location", b"NO")))
+ return headers, request.body
diff --git a/test/wpt/tests/fetch/api/resources/preflight.py b/test/wpt/tests/fetch/api/resources/preflight.py
new file mode 100644
index 0000000..f983ef9
--- /dev/null
+++ b/test/wpt/tests/fetch/api/resources/preflight.py
@@ -0,0 +1,78 @@
+def main(request, response):
+ headers = [(b"Content-Type", b"text/plain")]
+ stashed_data = {b'control_request_headers': b"", b'preflight': b"0", b'preflight_referrer': b""}
+
+ token = None
+ if b"token" in request.GET:
+ token = request.GET.first(b"token")
+
+ if b"origin" in request.GET:
+ for origin in request.GET[b'origin'].split(b", "):
+ headers.append((b"Access-Control-Allow-Origin", origin))
+ else:
+ headers.append((b"Access-Control-Allow-Origin", b"*"))
+
+ if b"clear-stash" in request.GET:
+ if request.server.stash.take(token) is not None:
+ return headers, b"1"
+ else:
+ return headers, b"0"
+
+ if b"credentials" in request.GET:
+ headers.append((b"Access-Control-Allow-Credentials", b"true"))
+
+ if request.method == u"OPTIONS":
+ if not b"Access-Control-Request-Method" in request.headers:
+ response.set_error(400, u"No Access-Control-Request-Method header")
+ return b"ERROR: No access-control-request-method in preflight!"
+
+ if request.headers.get(b"Accept", b"") != b"*/*":
+ response.set_error(400, u"Request does not have 'Accept: */*' header")
+ return b"ERROR: Invalid access in preflight!"
+
+ if b"control_request_headers" in request.GET:
+ stashed_data[b'control_request_headers'] = request.headers.get(b"Access-Control-Request-Headers", None)
+
+ if b"max_age" in request.GET:
+ headers.append((b"Access-Control-Max-Age", request.GET[b'max_age']))
+
+ if b"allow_headers" in request.GET:
+ headers.append((b"Access-Control-Allow-Headers", request.GET[b'allow_headers']))
+
+ if b"allow_methods" in request.GET:
+ headers.append((b"Access-Control-Allow-Methods", request.GET[b'allow_methods']))
+
+ preflight_status = 200
+ if b"preflight_status" in request.GET:
+ preflight_status = int(request.GET.first(b"preflight_status"))
+
+ stashed_data[b'preflight'] = b"1"
+ stashed_data[b'preflight_referrer'] = request.headers.get(b"Referer", b"")
+ stashed_data[b'preflight_user_agent'] = request.headers.get(b"User-Agent", b"")
+ if token:
+ request.server.stash.put(token, stashed_data)
+
+ return preflight_status, headers, b""
+
+
+ if token:
+ data = request.server.stash.take(token)
+ if data:
+ stashed_data = data
+
+ if b"checkUserAgentHeaderInPreflight" in request.GET and request.headers.get(b"User-Agent") != stashed_data[b'preflight_user_agent']:
+ return 400, headers, b"ERROR: No user-agent header in preflight"
+
+ #use x-* headers for returning value to bodyless responses
+ headers.append((b"Access-Control-Expose-Headers", b"x-did-preflight, x-control-request-headers, x-referrer, x-preflight-referrer, x-origin"))
+ headers.append((b"x-did-preflight", stashed_data[b'preflight']))
+ if stashed_data[b'control_request_headers'] != None:
+ headers.append((b"x-control-request-headers", stashed_data[b'control_request_headers']))
+ headers.append((b"x-preflight-referrer", stashed_data[b'preflight_referrer']))
+ headers.append((b"x-referrer", request.headers.get(b"Referer", b"")))
+ headers.append((b"x-origin", request.headers.get(b"Origin", b"")))
+
+ if token:
+ request.server.stash.put(token, stashed_data)
+
+ return headers, b""
diff --git a/test/wpt/tests/fetch/api/resources/redirect-empty-location.py b/test/wpt/tests/fetch/api/resources/redirect-empty-location.py
new file mode 100644
index 0000000..1a5f7fe
--- /dev/null
+++ b/test/wpt/tests/fetch/api/resources/redirect-empty-location.py
@@ -0,0 +1,3 @@
+def main(request, response):
+ headers = [(b"Location", b"")]
+ return 302, headers, b""
diff --git a/test/wpt/tests/fetch/api/resources/redirect.h2.py b/test/wpt/tests/fetch/api/resources/redirect.h2.py
new file mode 100644
index 0000000..6937014
--- /dev/null
+++ b/test/wpt/tests/fetch/api/resources/redirect.h2.py
@@ -0,0 +1,14 @@
+from wptserve.utils import isomorphic_decode, isomorphic_encode
+
+def handle_headers(frame, request, response):
+ status = 302
+ if b'redirect_status' in request.GET:
+ status = int(request.GET[b'redirect_status'])
+ response.status = status
+
+ if b'location' in request.GET:
+ url = isomorphic_decode(request.GET[b'location'])
+ response.headers[b'Location'] = isomorphic_encode(url)
+
+ response.headers.update([('Content-Type', 'text/plain')])
+ response.write_status_headers()
diff --git a/test/wpt/tests/fetch/api/resources/redirect.py b/test/wpt/tests/fetch/api/resources/redirect.py
new file mode 100644
index 0000000..d52ab5f
--- /dev/null
+++ b/test/wpt/tests/fetch/api/resources/redirect.py
@@ -0,0 +1,73 @@
+import time
+
+from urllib.parse import urlencode, urlparse
+
+from wptserve.utils import isomorphic_decode, isomorphic_encode
+
+def main(request, response):
+ stashed_data = {b'count': 0, b'preflight': b"0"}
+ status = 302
+ headers = [(b"Content-Type", b"text/plain"),
+ (b"Cache-Control", b"no-cache"),
+ (b"Pragma", b"no-cache")]
+ if b"Origin" in request.headers:
+ headers.append((b"Access-Control-Allow-Origin", request.headers.get(b"Origin", b"")))
+ headers.append((b"Access-Control-Allow-Credentials", b"true"))
+ else:
+ headers.append((b"Access-Control-Allow-Origin", b"*"))
+
+ token = None
+ if b"token" in request.GET:
+ token = request.GET.first(b"token")
+ data = request.server.stash.take(token)
+ if data:
+ stashed_data = data
+
+ if request.method == u"OPTIONS":
+ if b"allow_headers" in request.GET:
+ headers.append((b"Access-Control-Allow-Headers", request.GET[b'allow_headers']))
+ stashed_data[b'preflight'] = b"1"
+ #Preflight is not redirected: return 200
+ if not b"redirect_preflight" in request.GET:
+ if token:
+ request.server.stash.put(request.GET.first(b"token"), stashed_data)
+ return 200, headers, u""
+
+ if b"redirect_status" in request.GET:
+ status = int(request.GET[b'redirect_status'])
+ elif b"redirect_status" in request.POST:
+ status = int(request.POST[b'redirect_status'])
+
+ stashed_data[b'count'] += 1
+
+ if b"location" in request.GET:
+ url = isomorphic_decode(request.GET[b'location'])
+ if b"simple" not in request.GET:
+ scheme = urlparse(url).scheme
+ if scheme == u"" or scheme == u"http" or scheme == u"https":
+ url += u"&" if u'?' in url else u"?"
+ #keep url parameters in location
+ url_parameters = {}
+ for item in request.GET.items():
+ url_parameters[isomorphic_decode(item[0])] = isomorphic_decode(item[1][0])
+ url += urlencode(url_parameters)
+ #make sure location changes during redirection loop
+ url += u"&count=" + str(stashed_data[b'count'])
+ headers.append((b"Location", isomorphic_encode(url)))
+
+ if b"redirect_referrerpolicy" in request.GET:
+ headers.append((b"Referrer-Policy", request.GET[b'redirect_referrerpolicy']))
+
+ if b"delay" in request.GET:
+ time.sleep(float(request.GET.first(b"delay", 0)) / 1E3)
+
+ if token:
+ request.server.stash.put(request.GET.first(b"token"), stashed_data)
+ if b"max_count" in request.GET:
+ max_count = int(request.GET[b'max_count'])
+ #stop redirecting and return count
+ if stashed_data[b'count'] > max_count:
+ # -1 because the last is not a redirection
+ return str(stashed_data[b'count'] - 1)
+
+ return status, headers, u""
diff --git a/test/wpt/tests/fetch/api/resources/sandboxed-iframe.html b/test/wpt/tests/fetch/api/resources/sandboxed-iframe.html
new file mode 100644
index 0000000..6e5d506
--- /dev/null
+++ b/test/wpt/tests/fetch/api/resources/sandboxed-iframe.html
@@ -0,0 +1,34 @@
+<!doctype html>
+<html>
+<script>
+async function no_cors_should_be_rejected() {
+ let thrown = false;
+ try {
+ const resp = await fetch('top.txt');
+ } catch (e) {
+ thrown = true;
+ }
+ if (!thrown) {
+ throw Error('fetching "top.txt" should be rejected.');
+ }
+}
+
+async function null_origin_should_be_accepted() {
+ const url = 'top.txt?pipe=header(access-control-allow-origin,null)|' +
+ 'header(cache-control,no-store)';
+ const resp = await fetch(url);
+}
+
+async function test() {
+ try {
+ await no_cors_should_be_rejected();
+ await null_origin_should_be_accepted();
+ parent.postMessage('PASS', '*');
+ } catch (e) {
+ parent.postMessage(e.message, '*');
+ }
+}
+
+test();
+</script>
+</html>
diff --git a/test/wpt/tests/fetch/api/resources/script-with-header.py b/test/wpt/tests/fetch/api/resources/script-with-header.py
new file mode 100644
index 0000000..9a9c70e
--- /dev/null
+++ b/test/wpt/tests/fetch/api/resources/script-with-header.py
@@ -0,0 +1,7 @@
+def main(request, response):
+ headers = [(b"Content-type", request.GET.first(b"mime"))]
+ if b"content" in request.GET and request.GET.first(b"content") == b"empty":
+ content = b''
+ else:
+ content = b"console.log('Script loaded')"
+ return 200, headers, content
diff --git a/test/wpt/tests/fetch/api/resources/stash-put.py b/test/wpt/tests/fetch/api/resources/stash-put.py
new file mode 100644
index 0000000..0530e1b
--- /dev/null
+++ b/test/wpt/tests/fetch/api/resources/stash-put.py
@@ -0,0 +1,19 @@
+from wptserve.utils import isomorphic_decode
+
+def main(request, response):
+ if request.method == u'OPTIONS':
+ # CORS preflight
+ response.headers.set(b'Access-Control-Allow-Origin', b'*')
+ response.headers.set(b'Access-Control-Allow-Methods', b'*')
+ response.headers.set(b'Access-Control-Allow-Headers', b'*')
+ return 'done'
+
+ url_dir = u'/'.join(request.url_parts.path.split(u'/')[:-1]) + u'/'
+ key = request.GET.first(b'key')
+ value = request.GET.first(b'value')
+ # value here must be a text string. It will be json.dump()'ed in stash-take.py.
+ request.server.stash.put(key, isomorphic_decode(value), url_dir)
+
+ if b'disallow_origin' not in request.GET:
+ response.headers.set(b'Access-Control-Allow-Origin', b'*')
+ return 'done'
diff --git a/test/wpt/tests/fetch/api/resources/stash-take.py b/test/wpt/tests/fetch/api/resources/stash-take.py
new file mode 100644
index 0000000..e6db80d
--- /dev/null
+++ b/test/wpt/tests/fetch/api/resources/stash-take.py
@@ -0,0 +1,9 @@
+from wptserve.handlers import json_handler
+
+
+@json_handler
+def main(request, response):
+ dir = u'/'.join(request.url_parts.path.split(u'/')[:-1]) + u'/'
+ key = request.GET.first(b"key")
+ response.headers.set(b'Access-Control-Allow-Origin', b'*')
+ return request.server.stash.take(key, dir)
diff --git a/test/wpt/tests/fetch/api/resources/status.py b/test/wpt/tests/fetch/api/resources/status.py
new file mode 100644
index 0000000..05a59d5
--- /dev/null
+++ b/test/wpt/tests/fetch/api/resources/status.py
@@ -0,0 +1,11 @@
+from wptserve.utils import isomorphic_encode
+
+def main(request, response):
+ code = int(request.GET.first(b"code", 200))
+ text = request.GET.first(b"text", b"OMG")
+ content = request.GET.first(b"content", b"")
+ type = request.GET.first(b"type", b"")
+ status = (code, text)
+ headers = [(b"Content-Type", type),
+ (b"X-Request-Method", isomorphic_encode(request.method))]
+ return status, headers, content
diff --git a/test/wpt/tests/fetch/api/resources/sw-intercept-abort.js b/test/wpt/tests/fetch/api/resources/sw-intercept-abort.js
new file mode 100644
index 0000000..19d4b18
--- /dev/null
+++ b/test/wpt/tests/fetch/api/resources/sw-intercept-abort.js
@@ -0,0 +1,19 @@
+async function messageClient(clientId, message) {
+ const client = await clients.get(clientId);
+ client.postMessage(message);
+}
+
+addEventListener('fetch', event => {
+ let resolve;
+ const promise = new Promise(r => resolve = r);
+
+ function onAborted() {
+ messageClient(event.clientId, event.request.signal.reason);
+ resolve();
+ }
+
+ messageClient(event.clientId, 'fetch event has arrived');
+
+ event.respondWith(promise.then(() => new Response('hello')));
+ event.request.signal.addEventListener('abort', onAborted);
+});
diff --git a/test/wpt/tests/fetch/api/resources/sw-intercept.js b/test/wpt/tests/fetch/api/resources/sw-intercept.js
new file mode 100644
index 0000000..b8166b6
--- /dev/null
+++ b/test/wpt/tests/fetch/api/resources/sw-intercept.js
@@ -0,0 +1,10 @@
+async function broadcast(msg) {
+ for (const client of await clients.matchAll()) {
+ client.postMessage(msg);
+ }
+}
+
+addEventListener('fetch', event => {
+ event.waitUntil(broadcast(event.request.url));
+ event.respondWith(fetch(event.request));
+});
diff --git a/test/wpt/tests/fetch/api/resources/top.txt b/test/wpt/tests/fetch/api/resources/top.txt
new file mode 100644
index 0000000..83a3157
--- /dev/null
+++ b/test/wpt/tests/fetch/api/resources/top.txt
@@ -0,0 +1 @@
+top \ No newline at end of file
diff --git a/test/wpt/tests/fetch/api/resources/trickle.py b/test/wpt/tests/fetch/api/resources/trickle.py
new file mode 100644
index 0000000..99833f1
--- /dev/null
+++ b/test/wpt/tests/fetch/api/resources/trickle.py
@@ -0,0 +1,15 @@
+import time
+
+def main(request, response):
+ delay = float(request.GET.first(b"ms", 500)) / 1E3
+ count = int(request.GET.first(b"count", 50))
+ # Read request body
+ request.body
+ time.sleep(delay)
+ if not b"notype" in request.GET:
+ response.headers.set(b"Content-type", b"text/plain")
+ response.write_status_headers()
+ time.sleep(delay)
+ for i in range(count):
+ response.writer.write_content(b"TEST_TRICKLE\n")
+ time.sleep(delay)
diff --git a/test/wpt/tests/fetch/api/resources/utils.js b/test/wpt/tests/fetch/api/resources/utils.js
new file mode 100644
index 0000000..3b20ecc
--- /dev/null
+++ b/test/wpt/tests/fetch/api/resources/utils.js
@@ -0,0 +1,105 @@
+var RESOURCES_DIR = "../resources/";
+
+function dirname(path) {
+ return path.replace(/\/[^\/]*$/, '/')
+}
+
+function checkRequest(request, ExpectedValuesDict) {
+ for (var attribute in ExpectedValuesDict) {
+ switch(attribute) {
+ case "headers":
+ for (var key in ExpectedValuesDict["headers"].keys()) {
+ assert_equals(request["headers"].get(key), ExpectedValuesDict["headers"].get(key),
+ "Check headers attribute has " + key + ":" + ExpectedValuesDict["headers"].get(key));
+ }
+ break;
+
+ case "body":
+ //for checking body's content, a dedicated asyncronous/promise test should be used
+ assert_true(request["headers"].has("Content-Type") , "Check request has body using Content-Type header")
+ break;
+
+ case "method":
+ case "referrer":
+ case "referrerPolicy":
+ case "credentials":
+ case "cache":
+ case "redirect":
+ case "integrity":
+ case "url":
+ case "destination":
+ assert_equals(request[attribute], ExpectedValuesDict[attribute], "Check " + attribute + " attribute")
+ break;
+
+ default:
+ break;
+ }
+ }
+}
+
+function stringToArray(str) {
+ var array = new Uint8Array(str.length);
+ for (var i=0, strLen = str.length; i < strLen; i++)
+ array[i] = str.charCodeAt(i);
+ return array;
+}
+
+function encode_utf8(str)
+{
+ if (self.TextEncoder)
+ return (new TextEncoder).encode(str);
+ return stringToArray(unescape(encodeURIComponent(str)));
+}
+
+function validateBufferFromString(buffer, expectedValue, message)
+{
+ return assert_array_equals(new Uint8Array(buffer !== undefined ? buffer : []), stringToArray(expectedValue), message);
+}
+
+function validateStreamFromString(reader, expectedValue, retrievedArrayBuffer) {
+ // Passing Uint8Array for byte streams; non-byte streams will simply ignore it
+ return reader.read(new Uint8Array(64)).then(function(data) {
+ if (!data.done) {
+ assert_true(data.value instanceof Uint8Array, "Fetch ReadableStream chunks should be Uint8Array");
+ var newBuffer;
+ if (retrievedArrayBuffer) {
+ newBuffer = new Uint8Array(data.value.length + retrievedArrayBuffer.length);
+ newBuffer.set(retrievedArrayBuffer, 0);
+ newBuffer.set(data.value, retrievedArrayBuffer.length);
+ } else {
+ newBuffer = data.value;
+ }
+ return validateStreamFromString(reader, expectedValue, newBuffer);
+ }
+ validateBufferFromString(retrievedArrayBuffer, expectedValue, "Retrieve and verify stream");
+ });
+}
+
+function validateStreamFromPartialString(reader, expectedValue, retrievedArrayBuffer) {
+ // Passing Uint8Array for byte streams; non-byte streams will simply ignore it
+ return reader.read(new Uint8Array(64)).then(function(data) {
+ if (!data.done) {
+ assert_true(data.value instanceof Uint8Array, "Fetch ReadableStream chunks should be Uint8Array");
+ var newBuffer;
+ if (retrievedArrayBuffer) {
+ newBuffer = new Uint8Array(data.value.length + retrievedArrayBuffer.length);
+ newBuffer.set(retrievedArrayBuffer, 0);
+ newBuffer.set(data.value, retrievedArrayBuffer.length);
+ } else {
+ newBuffer = data.value;
+ }
+ return validateStreamFromPartialString(reader, expectedValue, newBuffer);
+ }
+
+ var string = new TextDecoder("utf-8").decode(retrievedArrayBuffer);
+ return assert_true(string.search(expectedValue) != -1, "Retrieve and verify stream");
+ });
+}
+
+// From streams tests
+function delay(milliseconds)
+{
+ return new Promise(function(resolve) {
+ step_timeout(resolve, milliseconds);
+ });
+}
diff --git a/test/wpt/tests/fetch/api/response/json.any.js b/test/wpt/tests/fetch/api/response/json.any.js
new file mode 100644
index 0000000..15f050e
--- /dev/null
+++ b/test/wpt/tests/fetch/api/response/json.any.js
@@ -0,0 +1,14 @@
+// See also /xhr/json.any.js
+
+promise_test(async t => {
+ const response = await fetch(`data:,\uFEFF{ "b": 1, "a": 2, "b": 3 }`);
+ const json = await response.json();
+ assert_array_equals(Object.keys(json), ["b", "a"]);
+ assert_equals(json.a, 2);
+ assert_equals(json.b, 3);
+}, "Ensure the correct JSON parser is used");
+
+promise_test(async t => {
+ const response = await fetch("/xhr/resources/utf16-bom.json");
+ return promise_rejects_js(t, SyntaxError, response.json());
+}, "Ensure UTF-16 results in an error");
diff --git a/test/wpt/tests/fetch/api/response/many-empty-chunks-crash.html b/test/wpt/tests/fetch/api/response/many-empty-chunks-crash.html
new file mode 100644
index 0000000..fe5e7d4
--- /dev/null
+++ b/test/wpt/tests/fetch/api/response/many-empty-chunks-crash.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<script>
+ new Response(new ReadableStream({
+ start(c) {
+
+ for (const i of new Array(40000).fill()) {
+ c.enqueue(new Uint8Array(0));
+ }
+ c.close();
+
+ }
+ })).text();
+</script>
diff --git a/test/wpt/tests/fetch/api/response/multi-globals/current/current.html b/test/wpt/tests/fetch/api/response/multi-globals/current/current.html
new file mode 100644
index 0000000..9bb6e0b
--- /dev/null
+++ b/test/wpt/tests/fetch/api/response/multi-globals/current/current.html
@@ -0,0 +1,3 @@
+<!DOCTYPE html>
+<title>Current page used as a test helper</title>
+<base href="success/">
diff --git a/test/wpt/tests/fetch/api/response/multi-globals/incumbent/incumbent.html b/test/wpt/tests/fetch/api/response/multi-globals/incumbent/incumbent.html
new file mode 100644
index 0000000..f63372e
--- /dev/null
+++ b/test/wpt/tests/fetch/api/response/multi-globals/incumbent/incumbent.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<title>Incumbent page used as a test helper</title>
+
+<iframe src="../current/current.html" id="c"></iframe>
+<iframe src="../relevant/relevant.html" id="r"></iframe>
+
+<script>
+'use strict';
+
+window.createRedirectResponse = (...args) => {
+ const current = document.querySelector('#c').contentWindow;
+ const relevant = document.querySelector('#r').contentWindow;
+ return current.Response.redirect.call(relevant.Response, ...args);
+};
+
+</script>
diff --git a/test/wpt/tests/fetch/api/response/multi-globals/relevant/relevant.html b/test/wpt/tests/fetch/api/response/multi-globals/relevant/relevant.html
new file mode 100644
index 0000000..44f42ed
--- /dev/null
+++ b/test/wpt/tests/fetch/api/response/multi-globals/relevant/relevant.html
@@ -0,0 +1,2 @@
+<!DOCTYPE html>
+<title>Relevant page used as a test helper</title>
diff --git a/test/wpt/tests/fetch/api/response/multi-globals/url-parsing.html b/test/wpt/tests/fetch/api/response/multi-globals/url-parsing.html
new file mode 100644
index 0000000..5f2f42a
--- /dev/null
+++ b/test/wpt/tests/fetch/api/response/multi-globals/url-parsing.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<title>Response.redirect URL parsing, with multiple globals in play</title>
+<link rel="help" href="https://fetch.spec.whatwg.org/#dom-response-redirect">
+<link rel="author" title="Domenic Denicola" href="mailto:d@domenic.me">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<!-- This is the entry global -->
+
+<iframe src="incumbent/incumbent.html"></iframe>
+
+<script>
+'use strict';
+
+const loadPromise = new Promise(resolve => {
+ window.addEventListener("load", () => resolve());
+});
+
+promise_test(() => {
+ return loadPromise.then(() => {
+ const res = document.querySelector('iframe').contentWindow.createRedirectResponse("url");
+
+ assert_equals(res.headers.get("Location"), new URL("current/success/url", location.href).href);
+ });
+}, "should parse the redirect Location URL relative to the current settings object");
+
+</script>
diff --git a/test/wpt/tests/fetch/api/response/response-body-read-task-handling.html b/test/wpt/tests/fetch/api/response/response-body-read-task-handling.html
new file mode 100644
index 0000000..64b0755
--- /dev/null
+++ b/test/wpt/tests/fetch/api/response/response-body-read-task-handling.html
@@ -0,0 +1,86 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="UTF-8">
+ <title></title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+</head>
+<body>
+ <script>
+function performMicrotaskCheckpoint() {
+ document.createNodeIterator(document, -1, {
+ acceptNode() {
+ return NodeFilter.FILTER_ACCEPT;
+ }
+ }).nextNode();
+}
+
+promise_test(function() {
+ return fetch("../resources/data.json").then(function(response) {
+ // Add a getter for "then" that will incidentally be invoked
+ // during promise resolution.
+ Object.prototype.__defineGetter__('then', () => {
+ // Clean up behind ourselves.
+ delete Object.prototype.then;
+
+ // This promise should (like all promises) be resolved
+ // asynchronously.
+ var executed = false;
+ Promise.resolve().then(_ => { executed = true; });
+
+ // This shouldn't run microtasks! They should only run
+ // after the fetch is resolved.
+ performMicrotaskCheckpoint();
+
+ // The fulfill handler above shouldn't have run yet. If it has run,
+ // throw to reject this promise and fail the test.
+ assert_false(executed, "shouldn't have run microtasks yet");
+
+ // Otherwise act as if there's no "then" property so the promise
+ // fulfills and the test passes.
+ return undefined;
+ });
+
+ // Create a read request, incidentally resolving a promise with an
+ // object value, thereby invoking the getter installed above.
+ return response.body.getReader().read();
+ });
+}, "reading from a body stream should occur in a microtask scope");
+
+promise_test(function() {
+ return fetch("../resources/data.json").then(function(response) {
+ // Add a getter for "then" that will incidentally be invoked
+ // during promise resolution.
+ Object.prototype.__defineGetter__('then', () => {
+ // Clean up behind ourselves.
+ delete Object.prototype.then;
+
+ // This promise should (like all promises) be resolved
+ // asynchronously.
+ var executed = false;
+ Promise.resolve().then(_ => { executed = true; });
+
+ // This shouldn't run microtasks! They should only run
+ // after the fetch is resolved.
+ performMicrotaskCheckpoint();
+
+ // The fulfill handler above shouldn't have run yet. If it has run,
+ // throw to reject this promise and fail the test.
+ assert_false(executed, "shouldn't have run microtasks yet");
+
+ // Otherwise act as if there's no "then" property so the promise
+ // fulfills and the test passes.
+ return undefined;
+ });
+
+ // Create a read request, incidentally resolving a promise with an
+ // object value, thereby invoking the getter installed above.
+ return response.body.pipeTo(new WritableStream({
+ write(chunk) {}
+ }))
+ });
+}, "piping from a body stream to a JS-written WritableStream should occur in a microtask scope");
+ </script>
+</body>
+</html>
diff --git a/test/wpt/tests/fetch/api/response/response-cancel-stream.any.js b/test/wpt/tests/fetch/api/response/response-cancel-stream.any.js
new file mode 100644
index 0000000..91140d1
--- /dev/null
+++ b/test/wpt/tests/fetch/api/response/response-cancel-stream.any.js
@@ -0,0 +1,64 @@
+// META: global=window,worker
+// META: title=Response consume blob and http bodies
+// META: script=../resources/utils.js
+
+promise_test(function(test) {
+ return new Response(new Blob([], { "type" : "text/plain" })).body.cancel();
+}, "Cancelling a starting blob Response stream");
+
+promise_test(function(test) {
+ var response = new Response(new Blob(["This is data"], { "type" : "text/plain" }));
+ var reader = response.body.getReader();
+ reader.read();
+ return reader.cancel();
+}, "Cancelling a loading blob Response stream");
+
+promise_test(function(test) {
+ var response = new Response(new Blob(["T"], { "type" : "text/plain" }));
+ var reader = response.body.getReader();
+
+ var closedPromise = reader.closed.then(function() {
+ return reader.cancel();
+ });
+ reader.read().then(function readMore({done, value}) {
+ if (!done) return reader.read().then(readMore);
+ });
+ return closedPromise;
+}, "Cancelling a closed blob Response stream");
+
+promise_test(function(test) {
+ return fetch(RESOURCES_DIR + "trickle.py?ms=30&count=100").then(function(response) {
+ return response.body.cancel();
+ });
+}, "Cancelling a starting Response stream");
+
+promise_test(function() {
+ return fetch(RESOURCES_DIR + "trickle.py?ms=30&count=100").then(function(response) {
+ var reader = response.body.getReader();
+ return reader.read().then(function() {
+ return reader.cancel();
+ });
+ });
+}, "Cancelling a loading Response stream");
+
+promise_test(function() {
+ async function readAll(reader) {
+ while (true) {
+ const {value, done} = await reader.read();
+ if (done)
+ return;
+ }
+ }
+
+ return fetch(RESOURCES_DIR + "top.txt").then(function(response) {
+ var reader = response.body.getReader();
+ return readAll(reader).then(() => reader.cancel());
+ });
+}, "Cancelling a closed Response stream");
+
+promise_test(async () => {
+ const response = await fetch(RESOURCES_DIR + "top.txt");
+ const { body } = response;
+ await body.cancel();
+ assert_equals(body, response.body, ".body should not change after cancellation");
+}, "Accessing .body after canceling it");
diff --git a/test/wpt/tests/fetch/api/response/response-clone-iframe.window.js b/test/wpt/tests/fetch/api/response/response-clone-iframe.window.js
new file mode 100644
index 0000000..da54616
--- /dev/null
+++ b/test/wpt/tests/fetch/api/response/response-clone-iframe.window.js
@@ -0,0 +1,32 @@
+// Verify that calling Response clone() in a detached iframe doesn't crash.
+// Regression test for https://crbug.com/1082688.
+
+'use strict';
+
+promise_test(async () => {
+ // Wait for the document body to be available.
+ await new Promise(resolve => {
+ onload = resolve;
+ });
+
+ window.iframe = document.createElement('iframe');
+ document.body.appendChild(iframe);
+ iframe.srcdoc = `<!doctype html>
+<script>
+const response = new Response('body');
+window.parent.postMessage('okay', '*');
+window.parent.iframe.remove();
+response.clone();
+</script>
+`;
+
+ await new Promise(resolve => {
+ onmessage = evt => {
+ if (evt.data === 'okay') {
+ resolve();
+ }
+ };
+ });
+
+ // If it got here without crashing, the test passed.
+}, 'clone within removed iframe should not crash');
diff --git a/test/wpt/tests/fetch/api/response/response-clone.any.js b/test/wpt/tests/fetch/api/response/response-clone.any.js
new file mode 100644
index 0000000..f5cda75
--- /dev/null
+++ b/test/wpt/tests/fetch/api/response/response-clone.any.js
@@ -0,0 +1,140 @@
+// META: global=window,worker
+// META: title=Response clone
+// META: script=../resources/utils.js
+
+var defaultValues = { "type" : "default",
+ "url" : "",
+ "ok" : true,
+ "status" : 200,
+ "statusText" : ""
+};
+
+var response = new Response();
+var clonedResponse = response.clone();
+test(function() {
+ for (var attributeName in defaultValues) {
+ var expectedValue = defaultValues[attributeName];
+ assert_equals(clonedResponse[attributeName], expectedValue,
+ "Expect default response." + attributeName + " is " + expectedValue);
+ }
+}, "Check Response's clone with default values, without body");
+
+var body = "This is response body";
+var headersInit = { "name" : "value" };
+var responseInit = { "status" : 200,
+ "statusText" : "GOOD",
+ "headers" : headersInit
+};
+var response = new Response(body, responseInit);
+var clonedResponse = response.clone();
+test(function() {
+ assert_equals(clonedResponse.status, responseInit["status"],
+ "Expect response.status is " + responseInit["status"]);
+ assert_equals(clonedResponse.statusText, responseInit["statusText"],
+ "Expect response.statusText is " + responseInit["statusText"]);
+ assert_equals(clonedResponse.headers.get("name"), "value",
+ "Expect response.headers has name:value header");
+}, "Check Response's clone has the expected attribute values");
+
+promise_test(function(test) {
+ return validateStreamFromString(response.body.getReader(), body);
+}, "Check orginal response's body after cloning");
+
+promise_test(function(test) {
+ return validateStreamFromString(clonedResponse.body.getReader(), body);
+}, "Check cloned response's body");
+
+promise_test(function(test) {
+ var disturbedResponse = new Response("data");
+ return disturbedResponse.text().then(function() {
+ assert_true(disturbedResponse.bodyUsed, "response is disturbed");
+ assert_throws_js(TypeError, function() { disturbedResponse.clone(); },
+ "Expect TypeError exception");
+ });
+}, "Cannot clone a disturbed response");
+
+promise_test(function(t) {
+ var clone;
+ var result;
+ var response;
+ return fetch('../resources/trickle.py?count=2&delay=100').then(function(res) {
+ clone = res.clone();
+ response = res;
+ return clone.text();
+ }).then(function(r) {
+ assert_equals(r.length, 26);
+ result = r;
+ return response.text();
+ }).then(function(r) {
+ assert_equals(r, result, "cloned responses should provide the same data");
+ });
+ }, 'Cloned responses should provide the same data');
+
+promise_test(function(t) {
+ var clone;
+ return fetch('../resources/trickle.py?count=2&delay=100').then(function(res) {
+ clone = res.clone();
+ res.body.cancel();
+ assert_true(res.bodyUsed);
+ assert_false(clone.bodyUsed);
+ return clone.arrayBuffer();
+ }).then(function(r) {
+ assert_equals(r.byteLength, 26);
+ assert_true(clone.bodyUsed);
+ });
+}, 'Cancelling stream should not affect cloned one');
+
+function testReadableStreamClone(initialBuffer, bufferType)
+{
+ promise_test(function(test) {
+ var response = new Response(new ReadableStream({start : function(controller) {
+ controller.enqueue(initialBuffer);
+ controller.close();
+ }}));
+
+ var clone = response.clone();
+ var stream1 = response.body;
+ var stream2 = clone.body;
+
+ var buffer;
+ return stream1.getReader().read().then(function(data) {
+ assert_false(data.done);
+ assert_equals(data.value, initialBuffer, "Buffer of being-cloned response stream is the same as the original buffer");
+ return stream2.getReader().read();
+ }).then(function(data) {
+ assert_false(data.done);
+ if (initialBuffer instanceof ArrayBuffer) {
+ assert_true(data.value instanceof ArrayBuffer, "Cloned buffer is ArrayBufer");
+ assert_equals(initialBuffer.byteLength, data.value.byteLength, "Length equal");
+ assert_array_equals(new Uint8Array(data.value), new Uint8Array(initialBuffer), "Cloned buffer chunks have the same content");
+ } else if (initialBuffer instanceof DataView) {
+ assert_true(data.value instanceof DataView, "Cloned buffer is DataView");
+ assert_equals(initialBuffer.byteLength, data.value.byteLength, "Lengths equal");
+ assert_equals(initialBuffer.byteOffset, data.value.byteOffset, "Offsets equal");
+ for (let i = 0; i < initialBuffer.byteLength; ++i) {
+ assert_equals(
+ data.value.getUint8(i), initialBuffer.getUint8(i), "Mismatch at byte ${i}");
+ }
+ } else {
+ assert_array_equals(data.value, initialBuffer, "Cloned buffer chunks have the same content");
+ }
+ assert_equals(Object.getPrototypeOf(data.value), Object.getPrototypeOf(initialBuffer), "Cloned buffers have the same type");
+ assert_not_equals(data.value, initialBuffer, "Buffer of cloned response stream is a clone of the original buffer");
+ });
+ }, "Check response clone use structureClone for teed ReadableStreams (" + bufferType + "chunk)");
+}
+
+var arrayBuffer = new ArrayBuffer(16);
+testReadableStreamClone(new Int8Array(arrayBuffer, 1), "Int8Array");
+testReadableStreamClone(new Int16Array(arrayBuffer, 2, 2), "Int16Array");
+testReadableStreamClone(new Int32Array(arrayBuffer), "Int32Array");
+testReadableStreamClone(arrayBuffer, "ArrayBuffer");
+testReadableStreamClone(new Uint8Array(arrayBuffer), "Uint8Array");
+testReadableStreamClone(new Uint8ClampedArray(arrayBuffer), "Uint8ClampedArray");
+testReadableStreamClone(new Uint16Array(arrayBuffer, 2), "Uint16Array");
+testReadableStreamClone(new Uint32Array(arrayBuffer), "Uint32Array");
+testReadableStreamClone(typeof BigInt64Array === "function" ? new BigInt64Array(arrayBuffer) : undefined, "BigInt64Array");
+testReadableStreamClone(typeof BigUint64Array === "function" ? new BigUint64Array(arrayBuffer) : undefined, "BigUint64Array");
+testReadableStreamClone(new Float32Array(arrayBuffer), "Float32Array");
+testReadableStreamClone(new Float64Array(arrayBuffer), "Float64Array");
+testReadableStreamClone(new DataView(arrayBuffer, 2, 8), "DataView");
diff --git a/test/wpt/tests/fetch/api/response/response-consume-empty.any.js b/test/wpt/tests/fetch/api/response/response-consume-empty.any.js
new file mode 100644
index 0000000..0fa85ec
--- /dev/null
+++ b/test/wpt/tests/fetch/api/response/response-consume-empty.any.js
@@ -0,0 +1,99 @@
+// META: global=window,worker
+// META: title=Response consume empty bodies
+
+function checkBodyText(test, response) {
+ return response.text().then(function(bodyAsText) {
+ assert_equals(bodyAsText, "", "Resolved value should be empty");
+ assert_false(response.bodyUsed);
+ });
+}
+
+function checkBodyBlob(test, response) {
+ return response.blob().then(function(bodyAsBlob) {
+ var promise = new Promise(function(resolve, reject) {
+ var reader = new FileReader();
+ reader.onload = function(evt) {
+ resolve(reader.result)
+ };
+ reader.onerror = function() {
+ reject("Blob's reader failed");
+ };
+ reader.readAsText(bodyAsBlob);
+ });
+ return promise.then(function(body) {
+ assert_equals(body, "", "Resolved value should be empty");
+ assert_false(response.bodyUsed);
+ });
+ });
+}
+
+function checkBodyArrayBuffer(test, response) {
+ return response.arrayBuffer().then(function(bodyAsArrayBuffer) {
+ assert_equals(bodyAsArrayBuffer.byteLength, 0, "Resolved value should be empty");
+ assert_false(response.bodyUsed);
+ });
+}
+
+function checkBodyJSON(test, response) {
+ return response.json().then(
+ function(bodyAsJSON) {
+ assert_unreached("JSON parsing should fail");
+ },
+ function() {
+ assert_false(response.bodyUsed);
+ });
+}
+
+function checkBodyFormData(test, response) {
+ return response.formData().then(function(bodyAsFormData) {
+ assert_true(bodyAsFormData instanceof FormData, "Should receive a FormData");
+ assert_false(response.bodyUsed);
+ });
+}
+
+function checkBodyFormDataError(test, response) {
+ return promise_rejects_js(test, TypeError, response.formData()).then(function() {
+ assert_false(response.bodyUsed);
+ });
+}
+
+function checkResponseWithNoBody(bodyType, checkFunction, headers = []) {
+ promise_test(function(test) {
+ var response = new Response(undefined, { "headers": headers });
+ assert_false(response.bodyUsed);
+ return checkFunction(test, response);
+ }, "Consume response's body as " + bodyType);
+}
+
+checkResponseWithNoBody("text", checkBodyText);
+checkResponseWithNoBody("blob", checkBodyBlob);
+checkResponseWithNoBody("arrayBuffer", checkBodyArrayBuffer);
+checkResponseWithNoBody("json (error case)", checkBodyJSON);
+checkResponseWithNoBody("formData with correct multipart type (error case)", checkBodyFormDataError, [["Content-Type", 'multipart/form-data; boundary="boundary"']]);
+checkResponseWithNoBody("formData with correct urlencoded type", checkBodyFormData, [["Content-Type", "application/x-www-form-urlencoded;charset=UTF-8"]]);
+checkResponseWithNoBody("formData without correct type (error case)", checkBodyFormDataError);
+
+function checkResponseWithEmptyBody(bodyType, body, asText) {
+ promise_test(function(test) {
+ var response = new Response(body);
+ assert_false(response.bodyUsed, "bodyUsed is false at init");
+ if (asText) {
+ return response.text().then(function(bodyAsString) {
+ assert_equals(bodyAsString.length, 0, "Resolved value should be empty");
+ assert_true(response.bodyUsed, "bodyUsed is true after being consumed");
+ });
+ }
+ return response.arrayBuffer().then(function(bodyAsArrayBuffer) {
+ assert_equals(bodyAsArrayBuffer.byteLength, 0, "Resolved value should be empty");
+ assert_true(response.bodyUsed, "bodyUsed is true after being consumed");
+ });
+ }, "Consume empty " + bodyType + " response body as " + (asText ? "text" : "arrayBuffer"));
+}
+
+checkResponseWithEmptyBody("blob", new Blob([], { "type" : "text/plain" }), false);
+checkResponseWithEmptyBody("text", "", false);
+checkResponseWithEmptyBody("blob", new Blob([], { "type" : "text/plain" }), true);
+checkResponseWithEmptyBody("text", "", true);
+checkResponseWithEmptyBody("URLSearchParams", new URLSearchParams(""), true);
+checkResponseWithEmptyBody("FormData", new FormData(), true);
+checkResponseWithEmptyBody("ArrayBuffer", new ArrayBuffer(), true);
diff --git a/test/wpt/tests/fetch/api/response/response-consume-stream.any.js b/test/wpt/tests/fetch/api/response/response-consume-stream.any.js
new file mode 100644
index 0000000..befce62
--- /dev/null
+++ b/test/wpt/tests/fetch/api/response/response-consume-stream.any.js
@@ -0,0 +1,61 @@
+// META: global=window,worker
+// META: title=Response consume
+// META: script=../resources/utils.js
+
+promise_test(function(test) {
+ var body = "";
+ var response = new Response("");
+ return validateStreamFromString(response.body.getReader(), "");
+}, "Read empty text response's body as readableStream");
+
+promise_test(function(test) {
+ var response = new Response(new Blob([], { "type" : "text/plain" }));
+ return validateStreamFromString(response.body.getReader(), "");
+}, "Read empty blob response's body as readableStream");
+
+var formData = new FormData();
+formData.append("name", "value");
+var textData = JSON.stringify("This is response's body");
+var blob = new Blob([textData], { "type" : "text/plain" });
+var urlSearchParamsData = "name=value";
+var urlSearchParams = new URLSearchParams(urlSearchParamsData);
+
+for (const mode of [undefined, "byob"]) {
+ promise_test(function(test) {
+ var response = new Response(blob);
+ return validateStreamFromString(response.body.getReader({ mode }), textData);
+ }, `Read blob response's body as readableStream with mode=${mode}`);
+
+ promise_test(function(test) {
+ var response = new Response(textData);
+ return validateStreamFromString(response.body.getReader({ mode }), textData);
+ }, `Read text response's body as readableStream with mode=${mode}`);
+
+ promise_test(function(test) {
+ var response = new Response(urlSearchParams);
+ return validateStreamFromString(response.body.getReader({ mode }), urlSearchParamsData);
+ }, `Read URLSearchParams response's body as readableStream with mode=${mode}`);
+
+ promise_test(function(test) {
+ var arrayBuffer = new ArrayBuffer(textData.length);
+ var int8Array = new Int8Array(arrayBuffer);
+ for (var cptr = 0; cptr < textData.length; cptr++)
+ int8Array[cptr] = textData.charCodeAt(cptr);
+
+ return validateStreamFromString(new Response(arrayBuffer).body.getReader({ mode }), textData);
+ }, `Read array buffer response's body as readableStream with mode=${mode}`);
+
+ promise_test(function(test) {
+ var response = new Response(formData);
+ return validateStreamFromPartialString(response.body.getReader({ mode }),
+ "Content-Disposition: form-data; name=\"name\"\r\n\r\nvalue");
+ }, `Read form data response's body as readableStream with mode=${mode}`);
+}
+
+test(function() {
+ assert_equals(Response.error().body, null);
+}, "Getting an error Response stream");
+
+test(function() {
+ assert_equals(Response.redirect("/").body, null);
+}, "Getting a redirect Response stream");
diff --git a/test/wpt/tests/fetch/api/response/response-consume.html b/test/wpt/tests/fetch/api/response/response-consume.html
new file mode 100644
index 0000000..89fc49f
--- /dev/null
+++ b/test/wpt/tests/fetch/api/response/response-consume.html
@@ -0,0 +1,317 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>Response consume</title>
+ <meta name="help" href="https://fetch.spec.whatwg.org/#response">
+ <meta name="help" href="https://fetch.spec.whatwg.org/#body-mixin">
+ <meta name="author" title="Canon Research France" href="https://www.crf.canon.fr">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="../resources/utils.js"></script>
+ </head>
+ <body>
+ <script>
+ function blobToFormDataResponse(name, blob) {
+ var formData = new FormData();
+ formData.append(name, blob);
+ return new Response(formData);
+ }
+
+ function readBlobAsArrayBuffer(blob) {
+ return new Promise(function(resolve, reject) {
+ var reader = new FileReader();
+ reader.onload = function(evt) {
+ resolve(reader.result);
+ };
+ reader.onerror = function(evt) {
+ reject("Blob's reader failed");
+ };
+ reader.readAsArrayBuffer(blob);
+ });
+ }
+
+ function blobToTypeViaFetch(blob) {
+ var url = URL.createObjectURL(blob);
+ return fetch(url).then(function(response) {
+ return response.headers.get('Content-Type');
+ });
+ }
+
+ function responsePromise(body, responseInit) {
+ return new Promise(function(resolve, reject) {
+ resolve(new Response(body, responseInit));
+ });
+ }
+
+ function responseStringToMultipartFormTextData(response, name, value) {
+ assert_true(response.headers.has("Content-Type"), "Response contains Content-Type header");
+ var boundaryMatches = response.headers.get("Content-Type").match(/;\s*boundary=("?)([^";\s]*)\1/);
+ assert_true(!!boundaryMatches, "Response contains boundary parameter");
+ return stringToMultipartFormTextData(boundaryMatches[2], name, value);
+ }
+
+ function streamResponsePromise(streamData, responseInit) {
+ return new Promise(function(resolve, reject) {
+ var stream = new ReadableStream({
+ start: function(controller) {
+ controller.enqueue(stringToArray(streamData));
+ controller.close();
+ }
+ });
+ resolve(new Response(stream, responseInit));
+ });
+ }
+
+ function stringToMultipartFormTextData(multipartBoundary, name, value) {
+ return ('--' + multipartBoundary + '\r\n' +
+ 'Content-Disposition: form-data;name="' + name + '"\r\n' +
+ '\r\n' +
+ value + '\r\n' +
+ '--' + multipartBoundary + '--\r\n');
+ }
+
+ function checkBodyText(test, response, expectedBody) {
+ return response.text().then( function(bodyAsText) {
+ assert_equals(bodyAsText, expectedBody, "Retrieve and verify response's body");
+ assert_true(response.bodyUsed, "body as text: bodyUsed turned true");
+ });
+ }
+
+ function checkBodyBlob(test, response, expectedBody, expectedType) {
+ return response.blob().then(function(bodyAsBlob) {
+ assert_equals(bodyAsBlob.type, expectedType || "text/plain", "Blob body type should be computed from the response Content-Type");
+
+ var promise = blobToTypeViaFetch(bodyAsBlob).then(function(type) {
+ assert_equals(type, expectedType || "text/plain", 'Type via blob URL');
+ return new Promise( function (resolve, reject) {
+ var reader = new FileReader();
+ reader.onload = function(evt) {
+ resolve(reader.result)
+ };
+ reader.onerror = function () {
+ reject("Blob's reader failed");
+ };
+ reader.readAsText(bodyAsBlob);
+ });
+ });
+ return promise.then(function(body) {
+ assert_equals(body, expectedBody, "Retrieve and verify response's body");
+ assert_true(response.bodyUsed, "body as blob: bodyUsed turned true");
+ });
+ });
+ }
+
+ function checkBodyArrayBuffer(test, response, expectedBody) {
+ return response.arrayBuffer().then( function(bodyAsArrayBuffer) {
+ validateBufferFromString(bodyAsArrayBuffer, expectedBody, "Retrieve and verify response's body");
+ assert_true(response.bodyUsed, "body as arrayBuffer: bodyUsed turned true");
+ });
+ }
+
+ function checkBodyJSON(test, response, expectedBody) {
+ return response.json().then(function(bodyAsJSON) {
+ var strBody = JSON.stringify(bodyAsJSON)
+ assert_equals(strBody, expectedBody, "Retrieve and verify response's body");
+ assert_true(response.bodyUsed, "body as json: bodyUsed turned true");
+ });
+ }
+
+ function checkBodyFormDataMultipart(test, response, expectedBody) {
+ return response.formData().then(function(bodyAsFormData) {
+ assert_true(bodyAsFormData instanceof FormData, "Should receive a FormData");
+ var entryName = "name";
+ var strBody = responseStringToMultipartFormTextData(response, entryName, bodyAsFormData.get(entryName));
+ assert_equals(strBody, expectedBody, "Retrieve and verify response's body");
+ assert_true(response.bodyUsed, "body as formData: bodyUsed turned true");
+ });
+ }
+
+ function checkBodyFormDataUrlencoded(test, response, expectedBody) {
+ return response.formData().then(function(bodyAsFormData) {
+ assert_true(bodyAsFormData instanceof FormData, "Should receive a FormData");
+ var entryName = "name";
+ var strBody = entryName + "=" + bodyAsFormData.get(entryName);
+ assert_equals(strBody, expectedBody, "Retrieve and verify response's body");
+ assert_true(response.bodyUsed, "body as formData: bodyUsed turned true");
+ });
+ }
+
+ function checkBodyFormDataError(test, response, expectedBody) {
+ return promise_rejects_js(test, TypeError, response.formData()).then(function() {
+ assert_true(response.bodyUsed, "body as formData: bodyUsed turned true");
+ });
+ }
+
+ function checkResponseBody(responsePromise, expectedBody, checkFunction, bodyTypes) {
+ promise_test(function(test) {
+ return responsePromise.then(function(response) {
+ assert_false(response.bodyUsed, "bodyUsed is false at init");
+ return checkFunction(test, response, expectedBody);
+ });
+ }, "Consume response's body: " + bodyTypes);
+ }
+
+ var textData = JSON.stringify("This is response's body");
+ var textResponseInit = { "headers": [["Content-Type", "text/PLAIN"]] };
+ var blob = new Blob([textData], { "type": "application/octet-stream" });
+ var multipartBoundary = "boundary-" + Math.random();
+ var formData = new FormData();
+ var formTextResponseInit = { "headers": [["Content-Type", 'multipart/FORM-data; boundary="' + multipartBoundary + '"']] };
+ var formTextData = stringToMultipartFormTextData(multipartBoundary, "name", textData);
+ var formBlob = new Blob([formTextData]);
+ var urlSearchParamsData = "name=value";
+ var urlSearchParams = new URLSearchParams(urlSearchParamsData);
+ var urlSearchParamsType = "application/x-www-form-urlencoded;charset=UTF-8";
+ var urlSearchParamsResponseInit = { "headers": [["Content-Type", urlSearchParamsType]] };
+ var urlSearchParamsBlob = new Blob([urlSearchParamsData], { "type": urlSearchParamsType });
+ formData.append("name", textData);
+
+ // https://fetch.spec.whatwg.org/#concept-body-package-data
+ // "UTF-8 decoded without BOM" is used for formData(), either in
+ // "multipart/form-data" and "application/x-www-form-urlencoded" cases,
+ // so BOMs in the values should be kept.
+ // (The "application/x-www-form-urlencoded" cases are tested in
+ // url/urlencoded-parser.any.js)
+ var textDataWithBom = "\uFEFFquick\uFEFFfox\uFEFF";
+ var formTextDataWithBom = stringToMultipartFormTextData(multipartBoundary, "name", textDataWithBom);
+ var formTextDataWithBomExpectedForMultipartFormData = stringToMultipartFormTextData(multipartBoundary, "name", textDataWithBom);
+
+ checkResponseBody(responsePromise(textData, textResponseInit), textData, checkBodyText, "from text to text");
+ checkResponseBody(responsePromise(textData, textResponseInit), textData, checkBodyBlob, "from text to blob");
+ checkResponseBody(responsePromise(textData, textResponseInit), textData, checkBodyArrayBuffer, "from text to arrayBuffer");
+ checkResponseBody(responsePromise(textData, textResponseInit), textData, checkBodyJSON, "from text to json");
+ checkResponseBody(responsePromise(formTextData, formTextResponseInit), formTextData, checkBodyFormDataMultipart, "from text with correct multipart type to formData");
+ checkResponseBody(responsePromise(formTextDataWithBom, formTextResponseInit), formTextDataWithBomExpectedForMultipartFormData, checkBodyFormDataMultipart, "from text with correct multipart type to formData with BOM");
+ checkResponseBody(responsePromise(formTextData, textResponseInit), undefined, checkBodyFormDataError, "from text without correct multipart type to formData (error case)");
+ checkResponseBody(responsePromise(urlSearchParamsData, urlSearchParamsResponseInit), urlSearchParamsData, checkBodyFormDataUrlencoded, "from text with correct urlencoded type to formData");
+ checkResponseBody(responsePromise(urlSearchParamsData, textResponseInit), undefined, checkBodyFormDataError, "from text without correct urlencoded type to formData (error case)");
+
+ checkResponseBody(responsePromise(blob, textResponseInit), textData, checkBodyBlob, "from blob to blob");
+ checkResponseBody(responsePromise(blob), textData, checkBodyText, "from blob to text");
+ checkResponseBody(responsePromise(blob), textData, checkBodyArrayBuffer, "from blob to arrayBuffer");
+ checkResponseBody(responsePromise(blob), textData, checkBodyJSON, "from blob to json");
+ checkResponseBody(responsePromise(formBlob, formTextResponseInit), formTextData, checkBodyFormDataMultipart, "from blob with correct multipart type to formData");
+ checkResponseBody(responsePromise(formBlob, textResponseInit), undefined, checkBodyFormDataError, "from blob without correct multipart type to formData (error case)");
+ checkResponseBody(responsePromise(urlSearchParamsBlob, urlSearchParamsResponseInit), urlSearchParamsData, checkBodyFormDataUrlencoded, "from blob with correct urlencoded type to formData");
+ checkResponseBody(responsePromise(urlSearchParamsBlob, textResponseInit), undefined, checkBodyFormDataError, "from blob without correct urlencoded type to formData (error case)");
+
+ function checkFormDataResponseBody(responsePromise, expectedName, expectedValue, checkFunction, bodyTypes) {
+ promise_test(function(test) {
+ return responsePromise.then(function(response) {
+ assert_false(response.bodyUsed, "bodyUsed is false at init");
+ var expectedBody = responseStringToMultipartFormTextData(response, expectedName, expectedValue);
+ return Promise.resolve().then(function() {
+ if (checkFunction === checkBodyFormDataMultipart)
+ return expectedBody;
+ // Modify expectedBody to use the same spacing for
+ // Content-Disposition parameters as Response and FormData does.
+ var response2 = new Response(formData);
+ return response2.text().then(function(formDataAsText) {
+ var reName = /[ \t]*;[ \t]*name=/;
+ var nameMatches = formDataAsText.match(reName);
+ return expectedBody.replace(reName, nameMatches[0]);
+ });
+ }).then(function(expectedBody) {
+ return checkFunction(test, response, expectedBody);
+ });
+ });
+ }, "Consume response's body: " + bodyTypes);
+ }
+
+ checkFormDataResponseBody(responsePromise(formData), "name", textData, checkBodyFormDataMultipart, "from FormData to formData");
+ checkResponseBody(responsePromise(formData, textResponseInit), undefined, checkBodyFormDataError, "from FormData without correct type to formData (error case)");
+ checkFormDataResponseBody(responsePromise(formData), "name", textData, function(test, response, expectedBody) { return checkBodyBlob(test, response, expectedBody, response.headers.get('Content-Type').toLowerCase()); }, "from FormData to blob");
+ checkFormDataResponseBody(responsePromise(formData), "name", textData, checkBodyText, "from FormData to text");
+ checkFormDataResponseBody(responsePromise(formData), "name", textData, checkBodyArrayBuffer, "from FormData to arrayBuffer");
+
+ checkResponseBody(responsePromise(urlSearchParams), urlSearchParamsData, checkBodyFormDataUrlencoded, "from URLSearchParams to formData");
+ checkResponseBody(responsePromise(urlSearchParams, textResponseInit), urlSearchParamsData, checkBodyFormDataError, "from URLSearchParams without correct type to formData (error case)");
+ checkResponseBody(responsePromise(urlSearchParams), urlSearchParamsData, function(test, response, expectedBody) { return checkBodyBlob(test, response, expectedBody, "application/x-www-form-urlencoded;charset=utf-8"); }, "from URLSearchParams to blob");
+ checkResponseBody(responsePromise(urlSearchParams), urlSearchParamsData, checkBodyText, "from URLSearchParams to text");
+ checkResponseBody(responsePromise(urlSearchParams), urlSearchParamsData, checkBodyArrayBuffer, "from URLSearchParams to arrayBuffer");
+
+ checkResponseBody(streamResponsePromise(textData, textResponseInit), textData, checkBodyBlob, "from stream to blob");
+ checkResponseBody(streamResponsePromise(textData), textData, checkBodyText, "from stream to text");
+ checkResponseBody(streamResponsePromise(textData), textData, checkBodyArrayBuffer, "from stream to arrayBuffer");
+ checkResponseBody(streamResponsePromise(textData), textData, checkBodyJSON, "from stream to json");
+ checkResponseBody(streamResponsePromise(formTextData, formTextResponseInit), formTextData, checkBodyFormDataMultipart, "from stream with correct multipart type to formData");
+ checkResponseBody(streamResponsePromise(formTextData), formTextData, checkBodyFormDataError, "from stream without correct multipart type to formData (error case)");
+ checkResponseBody(streamResponsePromise(urlSearchParamsData, urlSearchParamsResponseInit), urlSearchParamsData, checkBodyFormDataUrlencoded, "from stream with correct urlencoded type to formData");
+ checkResponseBody(streamResponsePromise(urlSearchParamsData), urlSearchParamsData, checkBodyFormDataError, "from stream without correct urlencoded type to formData (error case)");
+
+ checkResponseBody(fetch("../resources/top.txt"), "top", checkBodyBlob, "from fetch to blob");
+ checkResponseBody(fetch("../resources/top.txt"), "top", checkBodyText, "from fetch to text");
+ checkResponseBody(fetch("../resources/top.txt"), "top", checkBodyArrayBuffer, "from fetch to arrayBuffer");
+ checkResponseBody(fetch("../resources/top.txt"), "top", checkBodyFormDataError, "from fetch without correct type to formData (error case)");
+
+ promise_test(function(test) {
+ var response = new Response(new Blob([
+ "--boundary\r\n",
+ "Content-Disposition: form-data; name=string\r\n",
+ "\r\nvalue", new Uint8Array([0xC2, 0xA0]), "1\r\n",
+ "--boundary\r\n",
+ "Content-Disposition: form-data; name=string-with-default-charset\r\n",
+ "Content-Type: text/plain; charset=utf-8\r\n",
+ "\r\nvalue", new Uint8Array([0xC2, 0xA0]), "2\r\n",
+ "--boundary\r\n",
+ "Content-Disposition: form-data; name=string-with-non-default-charset\r\n",
+ "Content-Type: text/plain; charset=iso-8859-1\r\n",
+ "\r\nvalue", new Uint8Array([0xC2, 0xA0]), "3\r\n",
+ "--boundary\r\n",
+ "Content-Disposition: form-data; name=string-with-non-default-type\r\n",
+ "Content-Type: application/octet-stream\r\n",
+ "\r\nvalue", new Uint8Array([0xC2, 0xA0]), "4\r\n",
+ "--boundary\r\n",
+ "Content-Disposition: form-data; name=file; filename=file1\r\n",
+ "Content-Type: application/octet-stream; x-param=x-value\r\n",
+ "\r\n", new Uint8Array([5, 0x0, 0xFF]), "\r\n",
+ "--boundary\r\n",
+ "Content-Disposition: form-data; name=\"file-without-type\"; filename=\"file2\"\r\n",
+ "\r\n", new Uint8Array([6, 0x0, 0x7F, 0xFF]), "\r\n",
+ "--boundary--\r\n"
+ ]), { "headers": [["Content-Type", 'multipart/form-data; boundary="boundary"']] });
+ return response.formData().then(function(bodyAsFormData) {
+ // Non-file parts must always be decoded using utf-8 encoding.
+ assert_equals(bodyAsFormData.get("string"), "value\u00A01", "Retrieve and verify response's 1st entry value");
+ assert_equals(bodyAsFormData.get("string-with-default-charset"), "value\u00A02", "Retrieve and verify response's 2nd entry value");
+ assert_equals(bodyAsFormData.get("string-with-non-default-charset"), "value\u00A03", "Retrieve and verify response's 3rd entry value");
+ assert_equals(bodyAsFormData.get("string-with-non-default-type"), "value\u00A04", "Retrieve and verify response's 4th entry value");
+ // The name of a File must be taken from the filename parameter in
+ // the Content-Disposition header field.
+ assert_equals(bodyAsFormData.get("file").name, "file1", "Retrieve and verify response's 5th entry name property");
+ assert_equals(bodyAsFormData.get("file-without-type").name, "file2", "Retrieve and verify response's 6th entry name property");
+ // The type of a File must be taken from the Content-Type header field
+ // which defaults to "text/plain".
+ assert_equals(bodyAsFormData.get("file").type, "application/octet-stream; x-param=x-value", "Retrieve and verify response's 5th entry type property");
+ assert_equals(bodyAsFormData.get("file-without-type").type, "text/plain", "Retrieve and verify response's 6th entry type property");
+
+ return Promise.resolve().then(function() {
+ return blobToFormDataResponse("file", bodyAsFormData.get("file")).text().then(function(bodyAsText) {
+ // Verify that filename, name and type are preserved.
+ assert_regexp_match(bodyAsText, /\r\nContent-Disposition: *form-data;([^\r\n]*;)* *filename=("?)file1\2[;\r]/i, "Retrieve and verify response's 5th entry filename parameter");
+ assert_regexp_match(bodyAsText, /\r\nContent-Disposition: *form-data;([^\r\n]*;)* *name=("?)file\2[;\r]/i, "Retrieve and verify response's 5th entry name parameter");
+ assert_regexp_match(bodyAsText, /\r\nContent-Type: *application\/octet-stream; x-param=x-value\r\n/i, "Retrieve and verify response's 5th entry type field");
+ // Verify that the content is preserved.
+ return readBlobAsArrayBuffer(bodyAsFormData.get("file")).then(function(arrayBuffer) {
+ assert_array_equals(new Uint8Array(arrayBuffer), new Uint8Array([5, 0x0, 0xFF]), "Retrieve and verify response's 5th entry content");
+ });
+ });
+ }).then(function() {
+ return blobToFormDataResponse("file-without-type", bodyAsFormData.get("file-without-type")).text().then(function(bodyAsText) {
+ // Verify that filename, name and type are preserved.
+ assert_regexp_match(bodyAsText, /\r\nContent-Disposition: *form-data;([^\r\n]*;)* *filename=("?)file2\2[;\r]/i, "Retrieve and verify response's 6th entry filename parameter");
+ assert_regexp_match(bodyAsText, /\r\nContent-Disposition: *form-data;([^\r\n]*;)* *name=("?)file-without-type\2[;\r]/i, "Retrieve and verify response's 6th entry name parameter");
+ assert_regexp_match(bodyAsText, /\r\nContent-Type: *text\/plain\r\n/i, "Retrieve and verify response's 6th entry type field");
+ // Verify that the content is preserved.
+ return readBlobAsArrayBuffer(bodyAsFormData.get("file-without-type")).then(function(arrayBuffer) {
+ assert_array_equals(new Uint8Array(arrayBuffer), new Uint8Array([6, 0x0, 0x7F, 0xFF]), "Retrieve and verify response's 6th entry content");
+ });
+ });
+ });
+ });
+ }, "Consume response's body: from multipart form data blob to formData");
+
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/fetch/api/response/response-error-from-stream.any.js b/test/wpt/tests/fetch/api/response/response-error-from-stream.any.js
new file mode 100644
index 0000000..118eb7d
--- /dev/null
+++ b/test/wpt/tests/fetch/api/response/response-error-from-stream.any.js
@@ -0,0 +1,59 @@
+// META: global=window,worker
+// META: title=Response Receives Propagated Error from ReadableStream
+
+function newStreamWithStartError() {
+ var err = new Error("Start error");
+ return [new ReadableStream({
+ start(controller) {
+ controller.error(err);
+ }
+ }),
+ err]
+}
+
+function newStreamWithPullError() {
+ var err = new Error("Pull error");
+ return [new ReadableStream({
+ pull(controller) {
+ controller.error(err);
+ }
+ }),
+ err]
+}
+
+function runRequestPromiseTest([stream, err], responseReaderMethod, testDescription) {
+ promise_test(test => {
+ return promise_rejects_exactly(
+ test,
+ err,
+ new Response(stream)[responseReaderMethod](),
+ 'CustomTestError should propagate'
+ )
+ }, testDescription)
+}
+
+
+promise_test(test => {
+ var [stream, err] = newStreamWithStartError();
+ return promise_rejects_exactly(test, err, stream.getReader().read(), 'CustomTestError should propagate')
+}, "ReadableStreamDefaultReader Promise receives ReadableStream start() Error")
+
+promise_test(test => {
+ var [stream, err] = newStreamWithPullError();
+ return promise_rejects_exactly(test, err, stream.getReader().read(), 'CustomTestError should propagate')
+}, "ReadableStreamDefaultReader Promise receives ReadableStream pull() Error")
+
+
+// test start() errors for all Body reader methods
+runRequestPromiseTest(newStreamWithStartError(), 'arrayBuffer', 'ReadableStream start() Error propagates to Response.arrayBuffer() Promise');
+runRequestPromiseTest(newStreamWithStartError(), 'blob', 'ReadableStream start() Error propagates to Response.blob() Promise');
+runRequestPromiseTest(newStreamWithStartError(), 'formData', 'ReadableStream start() Error propagates to Response.formData() Promise');
+runRequestPromiseTest(newStreamWithStartError(), 'json', 'ReadableStream start() Error propagates to Response.json() Promise');
+runRequestPromiseTest(newStreamWithStartError(), 'text', 'ReadableStream start() Error propagates to Response.text() Promise');
+
+// test pull() errors for all Body reader methods
+runRequestPromiseTest(newStreamWithPullError(), 'arrayBuffer', 'ReadableStream pull() Error propagates to Response.arrayBuffer() Promise');
+runRequestPromiseTest(newStreamWithPullError(), 'blob', 'ReadableStream pull() Error propagates to Response.blob() Promise');
+runRequestPromiseTest(newStreamWithPullError(), 'formData', 'ReadableStream pull() Error propagates to Response.formData() Promise');
+runRequestPromiseTest(newStreamWithPullError(), 'json', 'ReadableStream pull() Error propagates to Response.json() Promise');
+runRequestPromiseTest(newStreamWithPullError(), 'text', 'ReadableStream pull() Error propagates to Response.text() Promise');
diff --git a/test/wpt/tests/fetch/api/response/response-error.any.js b/test/wpt/tests/fetch/api/response/response-error.any.js
new file mode 100644
index 0000000..a76bc43
--- /dev/null
+++ b/test/wpt/tests/fetch/api/response/response-error.any.js
@@ -0,0 +1,27 @@
+// META: global=window,worker
+// META: title=Response error
+
+var invalidStatus = [0, 100, 199, 600, 1000];
+invalidStatus.forEach(function(status) {
+ test(function() {
+ assert_throws_js(RangeError, function() { new Response("", { "status" : status }); },
+ "Expect RangeError exception when status is " + status);
+ },"Throws RangeError when responseInit's status is " + status);
+});
+
+var invalidStatusText = ["\n", "Ä€"];
+invalidStatusText.forEach(function(statusText) {
+ test(function() {
+ assert_throws_js(TypeError, function() { new Response("", { "statusText" : statusText }); },
+ "Expect TypeError exception " + statusText);
+ },"Throws TypeError when responseInit's statusText is " + statusText);
+});
+
+var nullBodyStatus = [204, 205, 304];
+nullBodyStatus.forEach(function(status) {
+ test(function() {
+ assert_throws_js(TypeError,
+ function() { new Response("body", {"status" : status }); },
+ "Expect TypeError exception ");
+ },"Throws TypeError when building a response with body and a body status of " + status);
+});
diff --git a/test/wpt/tests/fetch/api/response/response-from-stream.any.js b/test/wpt/tests/fetch/api/response/response-from-stream.any.js
new file mode 100644
index 0000000..ea5192b
--- /dev/null
+++ b/test/wpt/tests/fetch/api/response/response-from-stream.any.js
@@ -0,0 +1,23 @@
+// META: global=window,worker
+
+"use strict";
+
+test(() => {
+ const stream = new ReadableStream();
+ stream.getReader();
+ assert_throws_js(TypeError, () => new Response(stream));
+}, "Constructing a Response with a stream on which getReader() is called");
+
+test(() => {
+ const stream = new ReadableStream();
+ stream.getReader().read();
+ assert_throws_js(TypeError, () => new Response(stream));
+}, "Constructing a Response with a stream on which read() is called");
+
+promise_test(async () => {
+ const stream = new ReadableStream({ pull: c => c.enqueue(new Uint8Array()) }),
+ reader = stream.getReader();
+ await reader.read();
+ reader.releaseLock();
+ assert_throws_js(TypeError, () => new Response(stream));
+}, "Constructing a Response with a stream on which read() and releaseLock() are called");
diff --git a/test/wpt/tests/fetch/api/response/response-init-001.any.js b/test/wpt/tests/fetch/api/response/response-init-001.any.js
new file mode 100644
index 0000000..559e49a
--- /dev/null
+++ b/test/wpt/tests/fetch/api/response/response-init-001.any.js
@@ -0,0 +1,64 @@
+// META: global=window,worker
+// META: title=Response init: simple cases
+
+var defaultValues = { "type" : "default",
+ "url" : "",
+ "ok" : true,
+ "status" : 200,
+ "statusText" : "",
+ "body" : null
+};
+
+var statusCodes = { "givenValues" : [200, 300, 400, 500, 599],
+ "expectedValues" : [200, 300, 400, 500, 599]
+};
+var statusTexts = { "givenValues" : ["", "OK", "with space", String.fromCharCode(0x80)],
+ "expectedValues" : ["", "OK", "with space", String.fromCharCode(0x80)]
+};
+var initValuesDict = { "status" : statusCodes,
+ "statusText" : statusTexts
+};
+
+function isOkStatus(status) {
+ return 200 <= status && 299 >= status;
+}
+
+var response = new Response();
+for (var attributeName in defaultValues) {
+ test(function() {
+ var expectedValue = defaultValues[attributeName];
+ assert_equals(response[attributeName], expectedValue,
+ "Expect default response." + attributeName + " is " + expectedValue);
+ }, "Check default value for " + attributeName + " attribute");
+}
+
+for (var attributeName in initValuesDict) {
+ test(function() {
+ var valuesToTest = initValuesDict[attributeName];
+ for (var valueIdx in valuesToTest["givenValues"]) {
+ var givenValue = valuesToTest["givenValues"][valueIdx];
+ var expectedValue = valuesToTest["expectedValues"][valueIdx];
+ var responseInit = {};
+ responseInit[attributeName] = givenValue;
+ var response = new Response("", responseInit);
+ assert_equals(response[attributeName], expectedValue,
+ "Expect response." + attributeName + " is " + expectedValue +
+ " when initialized with " + givenValue);
+ assert_equals(response.ok, isOkStatus(response.status),
+ "Expect response.ok is " + isOkStatus(response.status));
+ }
+ }, "Check " + attributeName + " init values and associated getter");
+}
+
+test(function() {
+ const response1 = new Response("");
+ assert_equals(response1.headers, response1.headers);
+
+ const response2 = new Response("", {"headers": {"X-Foo": "bar"}});
+ assert_equals(response2.headers, response2.headers);
+ const headers = response2.headers;
+ response2.headers.set("X-Foo", "quux");
+ assert_equals(headers, response2.headers);
+ headers.set("X-Other-Header", "baz");
+ assert_equals(headers, response2.headers);
+}, "Test that Response.headers has the [SameObject] extended attribute");
diff --git a/test/wpt/tests/fetch/api/response/response-init-002.any.js b/test/wpt/tests/fetch/api/response/response-init-002.any.js
new file mode 100644
index 0000000..6c0a46e
--- /dev/null
+++ b/test/wpt/tests/fetch/api/response/response-init-002.any.js
@@ -0,0 +1,61 @@
+// META: global=window,worker
+// META: title=Response init: body and headers
+// META: script=../resources/utils.js
+
+test(function() {
+ var headerDict = {"name1": "value1",
+ "name2": "value2",
+ "name3": "value3"
+ };
+ var headers = new Headers(headerDict);
+ var response = new Response("", { "headers" : headers })
+ for (var name in headerDict) {
+ assert_equals(response.headers.get(name), headerDict[name],
+ "response's headers has " + name + " : " + headerDict[name]);
+ }
+}, "Initialize Response with headers values");
+
+function checkResponseInit(body, bodyType, expectedTextBody) {
+ promise_test(function(test) {
+ var response = new Response(body);
+ var resHeaders = response.headers;
+ var mime = resHeaders.get("Content-Type");
+ assert_true(mime && mime.search(bodyType) > -1, "Content-Type header should be \"" + bodyType + "\" ");
+ return response.text().then(function(bodyAsText) {
+ //not equals: cannot guess formData exact value
+ assert_true(bodyAsText.search(expectedTextBody) > -1, "Retrieve and verify response body");
+ });
+ }, "Initialize Response's body with " + bodyType);
+}
+
+var blob = new Blob(["This is a blob"], {type: "application/octet-binary"});
+var formaData = new FormData();
+formaData.append("name", "value");
+var urlSearchParams = "URLSearchParams are not supported";
+//avoid test timeout if not implemented
+if (self.URLSearchParams)
+ urlSearchParams = new URLSearchParams("name=value");
+var usvString = "This is a USVString"
+
+checkResponseInit(blob, "application/octet-binary", "This is a blob");
+checkResponseInit(formaData, "multipart/form-data", "name=\"name\"\r\n\r\nvalue");
+checkResponseInit(urlSearchParams, "application/x-www-form-urlencoded;charset=UTF-8", "name=value");
+checkResponseInit(usvString, "text/plain;charset=UTF-8", "This is a USVString");
+
+promise_test(function(test) {
+ var body = "This is response body";
+ var response = new Response(body);
+ return validateStreamFromString(response.body.getReader(), body);
+}, "Read Response's body as readableStream");
+
+promise_test(function(test) {
+ var response = new Response("This is my fork", {"headers" : [["Content-Type", ""]]});
+ return response.blob().then(function(blob) {
+ assert_equals(blob.type, "", "Blob type should be the empty string");
+ });
+}, "Testing empty Response Content-Type header");
+
+test(function() {
+ var response = new Response(null, {status: 204});
+ assert_equals(response.body, null);
+}, "Testing null Response body");
diff --git a/test/wpt/tests/fetch/api/response/response-init-contenttype.any.js b/test/wpt/tests/fetch/api/response/response-init-contenttype.any.js
new file mode 100644
index 0000000..3a7744c
--- /dev/null
+++ b/test/wpt/tests/fetch/api/response/response-init-contenttype.any.js
@@ -0,0 +1,125 @@
+test(() => {
+ const response = new Response();
+ assert_equals(response.headers.get("Content-Type"), null);
+}, "Default Content-Type for Response with empty body");
+
+test(() => {
+ const blob = new Blob([]);
+ const response = new Response(blob);
+ assert_equals(response.headers.get("Content-Type"), null);
+}, "Default Content-Type for Response with Blob body (no type set)");
+
+test(() => {
+ const blob = new Blob([], { type: "" });
+ const response = new Response(blob);
+ assert_equals(response.headers.get("Content-Type"), null);
+}, "Default Content-Type for Response with Blob body (empty type)");
+
+test(() => {
+ const blob = new Blob([], { type: "a/b; c=d" });
+ const response = new Response(blob);
+ assert_equals(response.headers.get("Content-Type"), "a/b; c=d");
+}, "Default Content-Type for Response with Blob body (set type)");
+
+test(() => {
+ const buffer = new Uint8Array();
+ const response = new Response(buffer);
+ assert_equals(response.headers.get("Content-Type"), null);
+}, "Default Content-Type for Response with buffer source body");
+
+promise_test(async () => {
+ const formData = new FormData();
+ formData.append("a", "b");
+ const response = new Response(formData);
+ const boundary = (await response.text()).split("\r\n")[0].slice(2);
+ assert_equals(
+ response.headers.get("Content-Type"),
+ `multipart/form-data; boundary=${boundary}`,
+ );
+}, "Default Content-Type for Response with FormData body");
+
+test(() => {
+ const usp = new URLSearchParams();
+ const response = new Response(usp);
+ assert_equals(
+ response.headers.get("Content-Type"),
+ "application/x-www-form-urlencoded;charset=UTF-8",
+ );
+}, "Default Content-Type for Response with URLSearchParams body");
+
+test(() => {
+ const response = new Response("");
+ assert_equals(
+ response.headers.get("Content-Type"),
+ "text/plain;charset=UTF-8",
+ );
+}, "Default Content-Type for Response with string body");
+
+test(() => {
+ const stream = new ReadableStream();
+ const response = new Response(stream);
+ assert_equals(response.headers.get("Content-Type"), null);
+}, "Default Content-Type for Response with ReadableStream body");
+
+// -----------------------------------------------------------------------------
+
+const OVERRIDE_MIME = "test/only; mime=type";
+
+function responseWithOverrideMime(body) {
+ return new Response(
+ body,
+ { headers: { "Content-Type": OVERRIDE_MIME } },
+ );
+}
+
+test(() => {
+ const response = responseWithOverrideMime(undefined);
+ assert_equals(response.headers.get("Content-Type"), OVERRIDE_MIME);
+}, "Can override Content-Type for Response with empty body");
+
+test(() => {
+ const blob = new Blob([]);
+ const response = responseWithOverrideMime(blob);
+ assert_equals(response.headers.get("Content-Type"), OVERRIDE_MIME);
+}, "Can override Content-Type for Response with Blob body (no type set)");
+
+test(() => {
+ const blob = new Blob([], { type: "" });
+ const response = responseWithOverrideMime(blob);
+ assert_equals(response.headers.get("Content-Type"), OVERRIDE_MIME);
+}, "Can override Content-Type for Response with Blob body (empty type)");
+
+test(() => {
+ const blob = new Blob([], { type: "a/b; c=d" });
+ const response = responseWithOverrideMime(blob);
+ assert_equals(response.headers.get("Content-Type"), OVERRIDE_MIME);
+}, "Can override Content-Type for Response with Blob body (set type)");
+
+test(() => {
+ const buffer = new Uint8Array();
+ const response = responseWithOverrideMime(buffer);
+ assert_equals(response.headers.get("Content-Type"), OVERRIDE_MIME);
+}, "Can override Content-Type for Response with buffer source body");
+
+test(() => {
+ const formData = new FormData();
+ const response = responseWithOverrideMime(formData);
+ assert_equals(response.headers.get("Content-Type"), OVERRIDE_MIME);
+}, "Can override Content-Type for Response with FormData body");
+
+test(() => {
+ const usp = new URLSearchParams();
+ const response = responseWithOverrideMime(usp);
+ assert_equals(response.headers.get("Content-Type"), OVERRIDE_MIME);
+}, "Can override Content-Type for Response with URLSearchParams body");
+
+test(() => {
+ const response = responseWithOverrideMime("");
+ assert_equals(response.headers.get("Content-Type"), OVERRIDE_MIME);
+}, "Can override Content-Type for Response with string body");
+
+test(() => {
+ const stream = new ReadableStream();
+ const response = responseWithOverrideMime(stream);
+ assert_equals(response.headers.get("Content-Type"), OVERRIDE_MIME);
+}, "Can override Content-Type for Response with ReadableStream body");
diff --git a/test/wpt/tests/fetch/api/response/response-static-error.any.js b/test/wpt/tests/fetch/api/response/response-static-error.any.js
new file mode 100644
index 0000000..1f8c49a
--- /dev/null
+++ b/test/wpt/tests/fetch/api/response/response-static-error.any.js
@@ -0,0 +1,34 @@
+// META: global=window,worker
+// META: title=Response: error static method
+
+test(function() {
+ var responseError = Response.error();
+ assert_equals(responseError.type, "error", "Network error response's type is error");
+ assert_equals(responseError.status, 0, "Network error response's status is 0");
+ assert_equals(responseError.statusText, "", "Network error response's statusText is empty");
+ assert_equals(responseError.body, null, "Network error response's body is null");
+
+ assert_true(responseError.headers.entries().next().done, "Headers should be empty");
+}, "Check response returned by static method error()");
+
+promise_test (async function() {
+ let response = await fetch("../resources/data.json");
+
+ try {
+ response.headers.append('name', 'value');
+ } catch (e) {
+ assert_equals(e.constructor.name, "TypeError");
+ }
+
+ assert_not_equals(response.headers.get("name"), "value", "response headers should be immutable");
+}, "Ensure response headers are immutable");
+
+test(function() {
+ const headers = Response.error().headers;
+
+ // Avoid false positives if expected API is not available
+ assert_true(!!headers);
+ assert_equals(typeof headers.append, 'function');
+
+ assert_throws_js(TypeError, function () { headers.append('name', 'value'); });
+}, "the 'guard' of the Headers instance should be immutable");
diff --git a/test/wpt/tests/fetch/api/response/response-static-json.any.js b/test/wpt/tests/fetch/api/response/response-static-json.any.js
new file mode 100644
index 0000000..5ec79e6
--- /dev/null
+++ b/test/wpt/tests/fetch/api/response/response-static-json.any.js
@@ -0,0 +1,96 @@
+// META: global=window,worker
+// META: title=Response: json static method
+
+const APPLICATION_JSON = "application/json";
+const FOO_BAR = "foo/bar";
+
+const INIT_TESTS = [
+ [undefined, 200, "", APPLICATION_JSON, {}],
+ [{ status: 400 }, 400, "", APPLICATION_JSON, {}],
+ [{ statusText: "foo" }, 200, "foo", APPLICATION_JSON, {}],
+ [{ headers: {} }, 200, "", APPLICATION_JSON, {}],
+ [{ headers: { "content-type": FOO_BAR } }, 200, "", FOO_BAR, {}],
+ [{ headers: { "x-foo": "bar" } }, 200, "", APPLICATION_JSON, { "x-foo": "bar" }],
+];
+
+for (const [init, expectedStatus, expectedStatusText, expectedContentType, expectedHeaders] of INIT_TESTS) {
+ promise_test(async function () {
+ const response = Response.json("hello world", init);
+ assert_equals(response.type, "default", "Response's type is default");
+ assert_equals(response.status, expectedStatus, "Response's status is " + expectedStatus);
+ assert_equals(response.statusText, expectedStatusText, "Response's statusText is " + JSON.stringify(expectedStatusText));
+ assert_equals(response.headers.get("content-type"), expectedContentType, "Response's content-type is " + expectedContentType);
+ for (const key in expectedHeaders) {
+ assert_equals(response.headers.get(key), expectedHeaders[key], "Response's header " + key + " is " + JSON.stringify(expectedHeaders[key]));
+ }
+
+ const data = await response.json();
+ assert_equals(data, "hello world", "Response's body is 'hello world'");
+ }, `Check response returned by static json() with init ${JSON.stringify(init)}`);
+}
+
+const nullBodyStatus = [204, 205, 304];
+for (const status of nullBodyStatus) {
+ test(function () {
+ assert_throws_js(
+ TypeError,
+ function () {
+ Response.json("hello world", { status: status });
+ },
+ );
+ }, `Throws TypeError when calling static json() with a status of ${status}`);
+}
+
+promise_test(async function () {
+ const response = Response.json({ foo: "bar" });
+ const data = await response.json();
+ assert_equals(typeof data, "object", "Response's json body is an object");
+ assert_equals(data.foo, "bar", "Response's json body is { foo: 'bar' }");
+}, "Check static json() encodes JSON objects correctly");
+
+test(function () {
+ assert_throws_js(
+ TypeError,
+ function () {
+ Response.json(Symbol("foo"));
+ },
+ );
+}, "Check static json() throws when data is not encodable");
+
+test(function () {
+ const a = { b: 1 };
+ a.a = a;
+ assert_throws_js(
+ TypeError,
+ function () {
+ Response.json(a);
+ },
+ );
+}, "Check static json() throws when data is circular");
+
+promise_test(async function () {
+ class CustomError extends Error {
+ name = "CustomError";
+ }
+ assert_throws_js(
+ CustomError,
+ function () {
+ Response.json({ get foo() { throw new CustomError("bar") }});
+ }
+ )
+}, "Check static json() propagates JSON serializer errors");
+
+const encodingChecks = [
+ ["ðŒ†", [34, 240, 157, 140, 134, 34]],
+ ["\uDF06\uD834", [34, 92, 117, 100, 102, 48, 54, 92, 117, 100, 56, 51, 52, 34]],
+ ["\uDEAD", [34, 92, 117, 100, 101, 97, 100, 34]],
+];
+
+for (const [input, expected] of encodingChecks) {
+ promise_test(async function () {
+ const response = Response.json(input);
+ const buffer = await response.arrayBuffer();
+ const data = new Uint8Array(buffer);
+ assert_array_equals(data, expected);
+ }, `Check response returned by static json() with input ${input}`);
+}
diff --git a/test/wpt/tests/fetch/api/response/response-static-redirect.any.js b/test/wpt/tests/fetch/api/response/response-static-redirect.any.js
new file mode 100644
index 0000000..b16c56d
--- /dev/null
+++ b/test/wpt/tests/fetch/api/response/response-static-redirect.any.js
@@ -0,0 +1,40 @@
+// META: global=window,worker
+// META: title=Response: redirect static method
+
+var url = "http://test.url:1234/";
+test(function() {
+ const redirectResponse = Response.redirect(url);
+ assert_equals(redirectResponse.type, "default");
+ assert_false(redirectResponse.redirected);
+ assert_false(redirectResponse.ok);
+ assert_equals(redirectResponse.status, 302, "Default redirect status is 302");
+ assert_equals(redirectResponse.headers.get("Location"), url,
+ "redirected response has Location header with the correct url");
+ assert_equals(redirectResponse.statusText, "");
+}, "Check default redirect response");
+
+[301, 302, 303, 307, 308].forEach(function(status) {
+ test(function() {
+ const redirectResponse = Response.redirect(url, status);
+ assert_equals(redirectResponse.type, "default");
+ assert_false(redirectResponse.redirected);
+ assert_false(redirectResponse.ok);
+ assert_equals(redirectResponse.status, status, "Redirect status is " + status);
+ assert_equals(redirectResponse.headers.get("Location"), url);
+ assert_equals(redirectResponse.statusText, "");
+ }, "Check response returned by static method redirect(), status = " + status);
+});
+
+test(function() {
+ var invalidUrl = "http://:This is not an url";
+ assert_throws_js(TypeError, function() { Response.redirect(invalidUrl); },
+ "Expect TypeError exception");
+}, "Check error returned when giving invalid url to redirect()");
+
+var invalidRedirectStatus = [200, 309, 400, 500];
+invalidRedirectStatus.forEach(function(invalidStatus) {
+ test(function() {
+ assert_throws_js(RangeError, function() { Response.redirect(url, invalidStatus); },
+ "Expect RangeError exception");
+ }, "Check error returned when giving invalid status to redirect(), status = " + invalidStatus);
+});
diff --git a/test/wpt/tests/fetch/api/response/response-stream-bad-chunk.any.js b/test/wpt/tests/fetch/api/response/response-stream-bad-chunk.any.js
new file mode 100644
index 0000000..d3d92e1
--- /dev/null
+++ b/test/wpt/tests/fetch/api/response/response-stream-bad-chunk.any.js
@@ -0,0 +1,24 @@
+// META: global=window,worker
+// META: title=Response causes TypeError from bad chunk type
+
+function runChunkTest(responseReaderMethod, testDescription) {
+ promise_test(test => {
+ let stream = new ReadableStream({
+ start(controller) {
+ controller.enqueue("not Uint8Array");
+ controller.close();
+ }
+ });
+
+ return promise_rejects_js(test, TypeError,
+ new Response(stream)[responseReaderMethod](),
+ 'TypeError should propagate'
+ )
+ }, testDescription)
+}
+
+runChunkTest('arrayBuffer', 'ReadableStream with non-Uint8Array chunk passed to Response.arrayBuffer() causes TypeError');
+runChunkTest('blob', 'ReadableStream with non-Uint8Array chunk passed to Response.blob() causes TypeError');
+runChunkTest('formData', 'ReadableStream with non-Uint8Array chunk passed to Response.formData() causes TypeError');
+runChunkTest('json', 'ReadableStream with non-Uint8Array chunk passed to Response.json() causes TypeError');
+runChunkTest('text', 'ReadableStream with non-Uint8Array chunk passed to Response.text() causes TypeError');
diff --git a/test/wpt/tests/fetch/api/response/response-stream-disturbed-1.any.js b/test/wpt/tests/fetch/api/response/response-stream-disturbed-1.any.js
new file mode 100644
index 0000000..64f65f1
--- /dev/null
+++ b/test/wpt/tests/fetch/api/response/response-stream-disturbed-1.any.js
@@ -0,0 +1,44 @@
+// META: global=window,worker
+// META: title=Consuming Response body after getting a ReadableStream
+// META: script=./response-stream-disturbed-util.js
+
+async function createResponseWithReadableStream(bodySource, callback) {
+ const response = await responseFromBodySource(bodySource);
+ const reader = response.body.getReader();
+ reader.releaseLock();
+ return callback(response);
+}
+
+for (const bodySource of ["fetch", "stream", "string"]) {
+ promise_test(function() {
+ return createResponseWithReadableStream(bodySource, function(response) {
+ return response.blob().then(function(blob) {
+ assert_true(blob instanceof Blob);
+ });
+ });
+ }, `Getting blob after getting the Response body - not disturbed, not locked (body source: ${bodySource})`);
+
+ promise_test(function() {
+ return createResponseWithReadableStream(bodySource, function(response) {
+ return response.text().then(function(text) {
+ assert_true(text.length > 0);
+ });
+ });
+ }, `Getting text after getting the Response body - not disturbed, not locked (body source: ${bodySource})`);
+
+ promise_test(function() {
+ return createResponseWithReadableStream(bodySource, function(response) {
+ return response.json().then(function(json) {
+ assert_equals(typeof json, "object");
+ });
+ });
+ }, `Getting json after getting the Response body - not disturbed, not locked (body source: ${bodySource})`);
+
+ promise_test(function() {
+ return createResponseWithReadableStream(bodySource, function(response) {
+ return response.arrayBuffer().then(function(arrayBuffer) {
+ assert_true(arrayBuffer.byteLength > 0);
+ });
+ });
+ }, `Getting arrayBuffer after getting the Response body - not disturbed, not locked (body source: ${bodySource})`);
+}
diff --git a/test/wpt/tests/fetch/api/response/response-stream-disturbed-2.any.js b/test/wpt/tests/fetch/api/response/response-stream-disturbed-2.any.js
new file mode 100644
index 0000000..c46a180
--- /dev/null
+++ b/test/wpt/tests/fetch/api/response/response-stream-disturbed-2.any.js
@@ -0,0 +1,35 @@
+// META: global=window,worker
+// META: title=Consuming Response body after getting a ReadableStream
+// META: script=./response-stream-disturbed-util.js
+
+async function createResponseWithLockedReadableStream(bodySource, callback) {
+ const response = await responseFromBodySource(bodySource);
+ response.body.getReader();
+ return callback(response);
+}
+
+for (const bodySource of ["fetch", "stream", "string"]) {
+ promise_test(function(test) {
+ return createResponseWithLockedReadableStream(bodySource, function(response) {
+ return promise_rejects_js(test, TypeError, response.blob());
+ });
+ }, `Getting blob after getting a locked Response body (body source: ${bodySource})`);
+
+ promise_test(function(test) {
+ return createResponseWithLockedReadableStream(bodySource, function(response) {
+ return promise_rejects_js(test, TypeError, response.text());
+ });
+ }, `Getting text after getting a locked Response body (body source: ${bodySource})`);
+
+ promise_test(function(test) {
+ return createResponseWithLockedReadableStream(bodySource, function(response) {
+ return promise_rejects_js(test, TypeError, response.json());
+ });
+ }, `Getting json after getting a locked Response body (body source: ${bodySource})`);
+
+ promise_test(function(test) {
+ return createResponseWithLockedReadableStream(bodySource, function(response) {
+ return promise_rejects_js(test, TypeError, response.arrayBuffer());
+ });
+ }, `Getting arrayBuffer after getting a locked Response body (body source: ${bodySource})`);
+}
diff --git a/test/wpt/tests/fetch/api/response/response-stream-disturbed-3.any.js b/test/wpt/tests/fetch/api/response/response-stream-disturbed-3.any.js
new file mode 100644
index 0000000..35fb086
--- /dev/null
+++ b/test/wpt/tests/fetch/api/response/response-stream-disturbed-3.any.js
@@ -0,0 +1,36 @@
+// META: global=window,worker
+// META: title=Consuming Response body after getting a ReadableStream
+// META: script=./response-stream-disturbed-util.js
+
+async function createResponseWithDisturbedReadableStream(bodySource, callback) {
+ const response = await responseFromBodySource(bodySource);
+ const reader = response.body.getReader();
+ reader.read();
+ return callback(response);
+}
+
+for (const bodySource of ["fetch", "stream", "string"]) {
+ promise_test(function(test) {
+ return createResponseWithDisturbedReadableStream(bodySource, function(response) {
+ return promise_rejects_js(test, TypeError, response.blob());
+ });
+ }, `Getting blob after reading the Response body (body source: ${bodySource})`);
+
+ promise_test(function(test) {
+ return createResponseWithDisturbedReadableStream(bodySource, function(response) {
+ return promise_rejects_js(test, TypeError, response.text());
+ });
+ }, `Getting text after reading the Response body (body source: ${bodySource})`);
+
+ promise_test(function(test) {
+ return createResponseWithDisturbedReadableStream(bodySource, function(response) {
+ return promise_rejects_js(test, TypeError, response.json());
+ });
+ }, `Getting json after reading the Response body (body source: ${bodySource})`);
+
+ promise_test(function(test) {
+ return createResponseWithDisturbedReadableStream(bodySource, function(response) {
+ return promise_rejects_js(test, TypeError, response.arrayBuffer());
+ });
+ }, `Getting arrayBuffer after reading the Response body (body source: ${bodySource})`);
+}
diff --git a/test/wpt/tests/fetch/api/response/response-stream-disturbed-4.any.js b/test/wpt/tests/fetch/api/response/response-stream-disturbed-4.any.js
new file mode 100644
index 0000000..490672f
--- /dev/null
+++ b/test/wpt/tests/fetch/api/response/response-stream-disturbed-4.any.js
@@ -0,0 +1,35 @@
+// META: global=window,worker
+// META: title=Consuming Response body after getting a ReadableStream
+// META: script=./response-stream-disturbed-util.js
+
+async function createResponseWithCancelledReadableStream(bodySource, callback) {
+ const response = await responseFromBodySource(bodySource);
+ response.body.cancel();
+ return callback(response);
+}
+
+for (const bodySource of ["fetch", "stream", "string"]) {
+ promise_test(function(test) {
+ return createResponseWithCancelledReadableStream(bodySource, function(response) {
+ return promise_rejects_js(test, TypeError, response.blob());
+ });
+ }, `Getting blob after cancelling the Response body (body source: ${bodySource})`);
+
+ promise_test(function(test) {
+ return createResponseWithCancelledReadableStream(bodySource, function(response) {
+ return promise_rejects_js(test, TypeError, response.text());
+ });
+ }, `Getting text after cancelling the Response body (body source: ${bodySource})`);
+
+ promise_test(function(test) {
+ return createResponseWithCancelledReadableStream(bodySource, function(response) {
+ return promise_rejects_js(test, TypeError, response.json());
+ });
+ }, `Getting json after cancelling the Response body (body source: ${bodySource})`);
+
+ promise_test(function(test) {
+ return createResponseWithCancelledReadableStream(bodySource, function(response) {
+ return promise_rejects_js(test, TypeError, response.arrayBuffer());
+ });
+ }, `Getting arrayBuffer after cancelling the Response body (body source: ${bodySource})`);
+}
diff --git a/test/wpt/tests/fetch/api/response/response-stream-disturbed-5.any.js b/test/wpt/tests/fetch/api/response/response-stream-disturbed-5.any.js
new file mode 100644
index 0000000..348fc39
--- /dev/null
+++ b/test/wpt/tests/fetch/api/response/response-stream-disturbed-5.any.js
@@ -0,0 +1,19 @@
+// META: global=window,worker
+// META: title=Consuming Response body after getting a ReadableStream
+// META: script=./response-stream-disturbed-util.js
+
+for (const bodySource of ["fetch", "stream", "string"]) {
+ for (const consumeAs of ["blob", "text", "json", "arrayBuffer"]) {
+ promise_test(
+ async () => {
+ const response = await responseFromBodySource(bodySource);
+ response[consumeAs]();
+ assert_not_equals(response.body, null);
+ assert_throws_js(TypeError, function () {
+ response.body.getReader();
+ });
+ },
+ `Getting a body reader after consuming as ${consumeAs} (body source: ${bodySource})`,
+ );
+ }
+}
diff --git a/test/wpt/tests/fetch/api/response/response-stream-disturbed-6.any.js b/test/wpt/tests/fetch/api/response/response-stream-disturbed-6.any.js
new file mode 100644
index 0000000..61d8544
--- /dev/null
+++ b/test/wpt/tests/fetch/api/response/response-stream-disturbed-6.any.js
@@ -0,0 +1,76 @@
+// META: global=window,worker
+// META: title=ReadableStream disturbed tests, via Response's bodyUsed property
+
+"use strict";
+
+test(() => {
+ const stream = new ReadableStream();
+ const response = new Response(stream);
+ assert_false(response.bodyUsed, "On construction");
+
+ const reader = stream.getReader();
+ assert_false(response.bodyUsed, "After getting a reader");
+
+ reader.read();
+ assert_true(response.bodyUsed, "After calling stream.read()");
+}, "A non-closed stream on which read() has been called");
+
+test(() => {
+ const stream = new ReadableStream();
+ const response = new Response(stream);
+ assert_false(response.bodyUsed, "On construction");
+
+ const reader = stream.getReader();
+ assert_false(response.bodyUsed, "After getting a reader");
+
+ reader.cancel();
+ assert_true(response.bodyUsed, "After calling stream.cancel()");
+}, "A non-closed stream on which cancel() has been called");
+
+test(() => {
+ const stream = new ReadableStream({
+ start(c) {
+ c.close();
+ }
+ });
+ const response = new Response(stream);
+ assert_false(response.bodyUsed, "On construction");
+
+ const reader = stream.getReader();
+ assert_false(response.bodyUsed, "After getting a reader");
+
+ reader.read();
+ assert_true(response.bodyUsed, "After calling stream.read()");
+}, "A closed stream on which read() has been called");
+
+test(() => {
+ const stream = new ReadableStream({
+ start(c) {
+ c.error(new Error("some error"));
+ }
+ });
+ const response = new Response(stream);
+ assert_false(response.bodyUsed, "On construction");
+
+ const reader = stream.getReader();
+ assert_false(response.bodyUsed, "After getting a reader");
+
+ reader.read().then(() => { }, () => { });
+ assert_true(response.bodyUsed, "After calling stream.read()");
+}, "An errored stream on which read() has been called");
+
+test(() => {
+ const stream = new ReadableStream({
+ start(c) {
+ c.error(new Error("some error"));
+ }
+ });
+ const response = new Response(stream);
+ assert_false(response.bodyUsed, "On construction");
+
+ const reader = stream.getReader();
+ assert_false(response.bodyUsed, "After getting a reader");
+
+ reader.cancel().then(() => { }, () => { });
+ assert_true(response.bodyUsed, "After calling stream.cancel()");
+}, "An errored stream on which cancel() has been called");
diff --git a/test/wpt/tests/fetch/api/response/response-stream-disturbed-by-pipe.any.js b/test/wpt/tests/fetch/api/response/response-stream-disturbed-by-pipe.any.js
new file mode 100644
index 0000000..5341b75
--- /dev/null
+++ b/test/wpt/tests/fetch/api/response/response-stream-disturbed-by-pipe.any.js
@@ -0,0 +1,17 @@
+// META: global=window,worker
+
+test(() => {
+ const r = new Response(new ReadableStream());
+ // highWaterMark: 0 means that nothing will actually be read from the body.
+ r.body.pipeTo(new WritableStream({}, {highWaterMark: 0}));
+ assert_true(r.bodyUsed, 'bodyUsed should be true');
+}, 'using pipeTo on Response body should disturb it synchronously');
+
+test(() => {
+ const r = new Response(new ReadableStream());
+ r.body.pipeThrough({
+ writable: new WritableStream({}, {highWaterMark: 0}),
+ readable: new ReadableStream()
+ });
+ assert_true(r.bodyUsed, 'bodyUsed should be true');
+}, 'using pipeThrough on Response body should disturb it synchronously');
diff --git a/test/wpt/tests/fetch/api/response/response-stream-disturbed-util.js b/test/wpt/tests/fetch/api/response/response-stream-disturbed-util.js
new file mode 100644
index 0000000..50bb586
--- /dev/null
+++ b/test/wpt/tests/fetch/api/response/response-stream-disturbed-util.js
@@ -0,0 +1,17 @@
+const BODY = '{"key": "value"}';
+
+function responseFromBodySource(bodySource) {
+ if (bodySource === "fetch") {
+ return fetch("../resources/data.json");
+ } else if (bodySource === "stream") {
+ const stream = new ReadableStream({
+ start(controller) {
+ controller.enqueue(new TextEncoder().encode(BODY));
+ controller.close();
+ },
+ });
+ return new Response(stream);
+ } else {
+ return new Response(BODY);
+ }
+}
diff --git a/test/wpt/tests/fetch/api/response/response-stream-with-broken-then.any.js b/test/wpt/tests/fetch/api/response/response-stream-with-broken-then.any.js
new file mode 100644
index 0000000..8fef66c
--- /dev/null
+++ b/test/wpt/tests/fetch/api/response/response-stream-with-broken-then.any.js
@@ -0,0 +1,117 @@
+// META: global=window,worker
+// META: script=../resources/utils.js
+
+promise_test(async () => {
+ // t.add_cleanup doesn't work when Object.prototype.then is overwritten, so
+ // these tests use add_completion_callback for cleanup instead.
+ add_completion_callback(() => delete Object.prototype.then);
+ const hello = new TextEncoder().encode('hello');
+ const bye = new TextEncoder().encode('bye');
+ const rs = new ReadableStream({
+ start(controller) {
+ controller.enqueue(hello);
+ controller.close();
+ }
+ });
+ const resp = new Response(rs);
+ Object.prototype.then = (onFulfilled) => {
+ delete Object.prototype.then;
+ onFulfilled({done: false, value: bye});
+ };
+ const text = await resp.text();
+ delete Object.prototype.then;
+ assert_equals(text, 'hello', 'The value should be "hello".');
+}, 'Attempt to inject {done: false, value: bye} via Object.prototype.then.');
+
+promise_test(async (t) => {
+ add_completion_callback(() => delete Object.prototype.then);
+ const hello = new TextEncoder().encode('hello');
+ const rs = new ReadableStream({
+ start(controller) {
+ controller.enqueue(hello);
+ controller.close();
+ }
+ });
+ const resp = new Response(rs);
+ Object.prototype.then = (onFulfilled) => {
+ delete Object.prototype.then;
+ onFulfilled({done: false, value: undefined});
+ };
+ const text = await resp.text();
+ delete Object.prototype.then;
+ assert_equals(text, 'hello', 'The value should be "hello".');
+}, 'Attempt to inject value: undefined via Object.prototype.then.');
+
+promise_test(async (t) => {
+ add_completion_callback(() => delete Object.prototype.then);
+ const hello = new TextEncoder().encode('hello');
+ const rs = new ReadableStream({
+ start(controller) {
+ controller.enqueue(hello);
+ controller.close();
+ }
+ });
+ const resp = new Response(rs);
+ Object.prototype.then = (onFulfilled) => {
+ delete Object.prototype.then;
+ onFulfilled(undefined);
+ };
+ const text = await resp.text();
+ delete Object.prototype.then;
+ assert_equals(text, 'hello', 'The value should be "hello".');
+}, 'Attempt to inject undefined via Object.prototype.then.');
+
+promise_test(async (t) => {
+ add_completion_callback(() => delete Object.prototype.then);
+ const hello = new TextEncoder().encode('hello');
+ const rs = new ReadableStream({
+ start(controller) {
+ controller.enqueue(hello);
+ controller.close();
+ }
+ });
+ const resp = new Response(rs);
+ Object.prototype.then = (onFulfilled) => {
+ delete Object.prototype.then;
+ onFulfilled(8.2);
+ };
+ const text = await resp.text();
+ delete Object.prototype.then;
+ assert_equals(text, 'hello', 'The value should be "hello".');
+}, 'Attempt to inject 8.2 via Object.prototype.then.');
+
+promise_test(async () => {
+ add_completion_callback(() => delete Object.prototype.then);
+ const hello = new TextEncoder().encode('hello');
+ const bye = new TextEncoder().encode('bye');
+ const resp = new Response(hello);
+ Object.prototype.then = (onFulfilled) => {
+ delete Object.prototype.then;
+ onFulfilled({done: false, value: bye});
+ };
+ const text = await resp.text();
+ delete Object.prototype.then;
+ assert_equals(text, 'hello', 'The value should be "hello".');
+}, 'intercepting arraybuffer to text conversion via Object.prototype.then ' +
+ 'should not be possible');
+
+promise_test(async () => {
+ add_completion_callback(() => delete Object.prototype.then);
+ const u8a123 = new Uint8Array([1, 2, 3]);
+ const u8a456 = new Uint8Array([4, 5, 6]);
+ const resp = new Response(u8a123);
+ const writtenBytes = [];
+ const ws = new WritableStream({
+ write(chunk) {
+ writtenBytes.push(...Array.from(chunk));
+ }
+ });
+ Object.prototype.then = (onFulfilled) => {
+ delete Object.prototype.then;
+ onFulfilled({done: false, value: u8a456});
+ };
+ await resp.body.pipeTo(ws);
+ delete Object.prototype.then;
+ assert_array_equals(writtenBytes, u8a123, 'The value should be [1, 2, 3]');
+}, 'intercepting arraybuffer to body readable stream conversion via ' +
+ 'Object.prototype.then should not be possible');
diff --git a/test/wpt/tests/fetch/connection-pool/network-partition-key.html b/test/wpt/tests/fetch/connection-pool/network-partition-key.html
new file mode 100644
index 0000000..60a784c
--- /dev/null
+++ b/test/wpt/tests/fetch/connection-pool/network-partition-key.html
@@ -0,0 +1,264 @@
+<!doctype html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Connection partitioning by site</title>
+ <meta name="help" href="https://fetch.spec.whatwg.org/#network-partition-keys">
+ <meta name="timeout" content="long">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/common/utils.js"></script>
+ <script src="/common/get-host-info.sub.js"></script>
+</head>
+<body>
+<!-- Used to open about:blank tabs from opaque origins -->
+<iframe id="iframe0" sandbox="allow-popups allow-scripts allow-popups-to-escape-sandbox"></iframe>
+<iframe id="iframe1" sandbox="allow-popups allow-scripts allow-popups-to-escape-sandbox"></iframe>
+<script>
+const host = get_host_info();
+
+// These two origins must correspond to different sites for this test to pass.
+const POPUP_ORIGINS = [
+ host.ORIGIN,
+ host.HTTP_NOTSAMESITE_ORIGIN
+];
+
+// This origin should ideally correspond to a different site from the two above, but the
+// tests will still pass if it matches the site of one of the other two origins.
+const OTHER_ORIGIN = host.REMOTE_ORIGIN;
+
+// Except for the csp_sandbox and about:blanks, each test opens up two windows, one at
+// POPUP_ORIGINS[0], one at POPUP_ORIGINS[1], and has them request subresources from
+// subresource_origin. All requests (HTML, JS, and fetch requests) for each window go
+// through network-partition-key.py and have a partition_id parameter, which is used
+// to check if any request for one window uses the same socket as a request for the
+// other window.
+//
+// Whenever requests from the two different popup windows use the same connection, the
+// fetch requests all start returning 400 errors, but other requests will continue to
+// succeed, to make for clearer errors.
+//
+// include_credentials indicates whether the fetch requests use credentials or not,
+// which is interesting as uncredentialed sockets have separate connection pools.
+const tests = [
+ {
+ name: 'With credentials',
+ subresource_origin: POPUP_ORIGINS[0],
+ include_credentials: true,
+ popup_params: [
+ {type: 'main_frame'},
+ {type: 'main_frame'}
+ ]
+ },
+ {
+ name: 'Without credentials',
+ subresource_origin: POPUP_ORIGINS[0],
+ include_credentials: false,
+ popup_params: [
+ {type: 'main_frame'},
+ {type: 'main_frame'}
+ ]
+ },
+ {
+ name: 'Cross-site resources with credentials',
+ subresource_origin: OTHER_ORIGIN,
+ include_credentials: true,
+ popup_params: [
+ {type: 'main_frame'},
+ {type: 'main_frame'}
+ ]
+ },
+ {
+ name: 'Cross-site resources without credentials',
+ subresource_origin: OTHER_ORIGIN,
+ include_credentials: false,
+ popup_params: [
+ {type: 'main_frame'},
+ {type: 'main_frame'}
+ ]
+ },
+ {
+ name: 'Iframes',
+ subresource_origin: OTHER_ORIGIN,
+ include_credentials: true,
+ popup_params: [
+ {
+ type: 'iframe',
+ iframe_origin: OTHER_ORIGIN
+ },
+ {
+ type: 'iframe',
+ iframe_origin: OTHER_ORIGIN
+ }
+ ]
+ },
+ {
+ name: 'Workers',
+ subresource_origin: POPUP_ORIGINS[0],
+ include_credentials: true,
+ popup_params: [
+ {type: 'worker'},
+ {type: 'worker'}
+ ]
+ },
+ {
+ name: 'Workers with cross-site resources',
+ subresource_origin: OTHER_ORIGIN,
+ include_credentials: true,
+ popup_params: [
+ {type: 'worker'},
+ {type: 'worker'}
+ ]
+ },
+ {
+ name: 'CSP sandbox',
+ subresource_origin: POPUP_ORIGINS[0],
+ include_credentials: true,
+ popup_params: [
+ {type: 'csp_sandbox'},
+ {type: 'csp_sandbox'}
+ ]
+ },
+ {
+ name: 'about:blank from opaque origin iframe',
+ subresource_origin: OTHER_ORIGIN,
+ include_credentials: true,
+ popup_params: [
+ {type: 'opaque_about_blank'},
+ {type: 'opaque_about_blank'}
+ ]
+ },
+];
+
+const BASE_PATH = window.location.pathname.replace(/\/[^\/]*$/, '/');
+
+function create_script_url(origin, uuid, partition_id, dispatch) {
+ return `${origin}${BASE_PATH}resources/network-partition-key.py?uuid=${uuid}&partition_id=${partition_id}&dispatch=${dispatch}`
+}
+
+function run_test(test) {
+ var uuid = token();
+
+ // Used to track the opened popup windows, so they can be closed at the end of the test.
+ // They could be closed immediately after use, but safest to keep them open, as browsers
+ // could use closing a window as a hint to close idle sockets that the window used.
+ var popup_windows = [];
+
+ // Creates a popup window at |url| and waits for a test result. Returns a promise.
+ function create_popup_and_wait_for_result(url) {
+ return new Promise(function(resolve, reject) {
+ popup_windows.push(window.open(url));
+ // Listen for the result
+ function message_listener(event) {
+ if (event.data.result === 'success') {
+ resolve();
+ } else if (event.data.result === 'error') {
+ reject(event.data.details);
+ } else {
+ reject('Unexpected message.');
+ }
+ }
+ window.addEventListener('message', message_listener, {once: 'true'});
+ });
+ }
+
+ // Navigates iframe to url and waits for a test result. Returns a promise.
+ function navigate_iframe_and_wait_for_result(iframe, url) {
+ return new Promise(function(resolve, reject) {
+ iframe.src = url;
+ // Listen for the result
+ function message_listener(event) {
+ if (event.data.result === 'success') {
+ resolve();
+ } else if (event.data.result === 'error') {
+ reject(event.data.details);
+ } else {
+ reject('Unexpected message.');
+ }
+ }
+ window.addEventListener('message', message_listener, {once: 'true'});
+ });
+ }
+
+ function make_test_function(test, index) {
+ var popup_params = test.popup_params[index];
+ return function() {
+ var popup_path;
+ var additional_url_params = '';
+ var origin = POPUP_ORIGINS[index];
+ var partition_id = POPUP_ORIGINS[index];
+ if (popup_params.type == 'main_frame') {
+ popup_path = 'resources/network-partition-checker.html';
+ } else if (popup_params.type == 'iframe') {
+ popup_path = 'resources/network-partition-iframe-checker.html';
+ additional_url_params = `&other_origin=${popup_params.iframe_origin}`;
+ } else if (popup_params.type == 'worker') {
+ popup_path = 'resources/network-partition-worker-checker.html';
+ // The origin of the dedicated worker must mutch the page that loads it.
+ additional_url_params = `&other_origin=${POPUP_ORIGINS[index]}`;
+ } else if (popup_params.type == 'csp_sandbox') {
+ // For the Content-Security-Policy sandbox test, all requests are from the same origin, but
+ // the origin should be treated as an opaque origin, so sockets should not be reused.
+ origin = test.subresource_origin;
+ partition_id = index;
+ popup_path = 'resources/network-partition-checker.html';
+ // Don't check partition of root document, since the document isn't sandboxed until the
+ // root document is fetched.
+ additional_url_params = '&sandbox=true&nocheck_partition=true'
+ } else if (popup_params.type=='opaque_about_blank') {
+ popup_path = 'resources/network-partition-about-blank-checker.html';
+ } else if (popup_params.type == 'iframe') {
+ throw 'Unrecognized popup_params.type.';
+ }
+ var url = create_script_url(origin, uuid, partition_id, 'fetch_file');
+ url += `&subresource_origin=${test.subresource_origin}`
+ url += `&include_credentials=${test.include_credentials}`
+ url += `&path=${BASE_PATH.substring(1)}${popup_path}`;
+ url += additional_url_params;
+
+ if (popup_params.type=='opaque_about_blank') {
+ return navigate_iframe_and_wait_for_result(iframe = document.getElementById('iframe' + index), url);
+ }
+
+ return create_popup_and_wait_for_result(url);
+ }
+ }
+
+ // Takes a Promise, and cleans up state when the promise has completed, successfully or not, re-throwing
+ // any exception from the passed in Promise.
+ async function clean_up_when_done(promise) {
+ var error;
+ try {
+ await promise;
+ } catch (e) {
+ error = e;
+ }
+
+ popup_windows.map(function (win) { win.close(); });
+
+ try {
+ var cleanup_url = create_script_url(host.ORIGIN, uuid, host.ORIGIN, 'clean_up');
+ var response = await fetch(cleanup_url, {credentials: 'omit', mode: 'cors'});
+ assert_equals(await response.text(), 'cleanup complete', `Sever state cleanup failed`);
+ } catch (e) {
+ // Prefer error from the passed in Promise over errors from the fetch request to clean up server state.
+ error = error || e;
+ }
+ if (error)
+ throw error;
+ }
+
+ return clean_up_when_done(
+ make_test_function(test, 0)()
+ .then(make_test_function(test, 1)));
+}
+
+tests.forEach(function (test) {
+ promise_test(
+ function() { return run_test(test); },
+ test.name);
+})
+
+</script>
+</body>
+</html>
diff --git a/test/wpt/tests/fetch/connection-pool/resources/network-partition-about-blank-checker.html b/test/wpt/tests/fetch/connection-pool/resources/network-partition-about-blank-checker.html
new file mode 100644
index 0000000..7a8b613
--- /dev/null
+++ b/test/wpt/tests/fetch/connection-pool/resources/network-partition-about-blank-checker.html
@@ -0,0 +1,35 @@
+<!doctype html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>about:blank Network Partition Checker</title>
+ <meta name="help" href="https://fetch.spec.whatwg.org/#network-partition-keys">
+ <meta name="timeout" content="normal">
+</head>
+<body>
+<script>
+ async function fetch_and_reply() {
+ // Load about:blank in a new tab, and inject the network partition checking code into it.
+ var win;
+ try {
+ win = window.open();
+ var url = 'SUBRESOURCE_PREFIX:&dispatch=fetch_file&path=fetch/connection-pool/resources/network-partition-checker.html&sandbox=true';
+ var response = await fetch(url, {credentials: 'omit', mode: 'cors'});
+ win.document.write(await response.text());
+ } catch (e) {
+ win.close();
+ window.parent.postMessage({result: 'error', details: e.message}, '*');
+ return;
+ }
+
+ // Listen for first message from the new window and pass it back to the parent.
+ function message_listener(event) {
+ window.parent.postMessage(event.data, '*');
+ win.close();
+ }
+ window.addEventListener('message', message_listener, {once: true});
+ }
+ fetch_and_reply();
+</script>
+</body>
+</html>
diff --git a/test/wpt/tests/fetch/connection-pool/resources/network-partition-checker.html b/test/wpt/tests/fetch/connection-pool/resources/network-partition-checker.html
new file mode 100644
index 0000000..b058f61
--- /dev/null
+++ b/test/wpt/tests/fetch/connection-pool/resources/network-partition-checker.html
@@ -0,0 +1,30 @@
+<!doctype html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Network Partition Checker</title>
+ <meta name="help" href="https://fetch.spec.whatwg.org/#network-partition-keys">
+ <meta name="timeout" content="normal">
+ <script src="SUBRESOURCE_PREFIX:&dispatch=fetch_file&path=common/utils.js"></script>
+ <script src="SUBRESOURCE_PREFIX:&dispatch=fetch_file&path=resources/testharness.js"></script>
+ <script src="SUBRESOURCE_PREFIX:&dispatch=fetch_file&path=fetch/connection-pool/resources/network-partition-key.js"></script>
+</head>
+<body>
+<script>
+ async function fetch_and_reply() {
+ // If this is a top level window, report to the opener. Otherwise, this is an iframe,
+ // so report to the parent.
+ var report_to = window.opener;
+ if (!report_to)
+ report_to = window.parent;
+ try {
+ await check_partition_ids();
+ report_to.postMessage({result: 'success'}, '*');
+ } catch (e) {
+ report_to.postMessage({result: 'error', details: e.message}, '*');
+ }
+ }
+ fetch_and_reply();
+</script>
+</body>
+</html>
diff --git a/test/wpt/tests/fetch/connection-pool/resources/network-partition-iframe-checker.html b/test/wpt/tests/fetch/connection-pool/resources/network-partition-iframe-checker.html
new file mode 100644
index 0000000..f76ed18
--- /dev/null
+++ b/test/wpt/tests/fetch/connection-pool/resources/network-partition-iframe-checker.html
@@ -0,0 +1,22 @@
+<!doctype html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Iframe Network Partition Checker</title>
+ <meta name="help" href="https://fetch.spec.whatwg.org/#network-partition-keys">
+ <meta name="timeout" content="normal">
+ <script src="SUBRESOURCE_PREFIX:&dispatch=fetch_file&path=common/utils.js"></script>
+ <script src="SUBRESOURCE_PREFIX:&dispatch=fetch_file&path=resources/testharness.js"></script>
+ <script src="SUBRESOURCE_PREFIX:&dispatch=fetch_file&path=fetch/connection-pool/resources/network-partition-key.js"></script>
+</head>
+<body>
+<script>
+ // Listen for first message from the iframe, and pass it back to the opener.
+ function message_listener(event) {
+ window.opener.postMessage(event.data, '*');
+ }
+ window.addEventListener('message', message_listener, {once: 'true'});
+</script>
+<iframe src="SUBRESOURCE_PREFIX:&dispatch=fetch_file&path=fetch/connection-pool/resources/network-partition-checker.html"></iframe>
+</body>
+</html>
diff --git a/test/wpt/tests/fetch/connection-pool/resources/network-partition-key.js b/test/wpt/tests/fetch/connection-pool/resources/network-partition-key.js
new file mode 100644
index 0000000..bd66109
--- /dev/null
+++ b/test/wpt/tests/fetch/connection-pool/resources/network-partition-key.js
@@ -0,0 +1,47 @@
+// Runs multiple fetches that validate connections see only a single partition_id.
+// Requests are run in parallel so that they use multiple connections to maximize the
+// chance of exercising all matching connections in the connection pool. Only returns
+// once all requests have completed to make cleaning up server state non-racy.
+function check_partition_ids(location) {
+ const NUM_FETCHES = 20;
+
+ var base_url = 'SUBRESOURCE_PREFIX:&dispatch=check_partition';
+
+ // Not a perfect parse of the query string, but good enough for this test.
+ var include_credentials = base_url.search('include_credentials=true') != -1;
+ var exclude_credentials = base_url.search('include_credentials=false') != -1;
+ if (include_credentials != !exclude_credentials)
+ throw new Exception('Credentials mode not specified');
+
+
+ // Run NUM_FETCHES in parallel.
+ var fetches = [];
+ for (i = 0; i < NUM_FETCHES; ++i) {
+ var fetch_params = {
+ credentials: 'omit',
+ mode: 'cors',
+ headers: {
+ 'Header-To-Force-CORS': 'cors'
+ },
+ };
+
+ // Use a unique URL for each request, in case the caching layer serializes multiple
+ // requests for the same URL.
+ var url = `${base_url}&${token()}`;
+
+ fetches.push(fetch(url, fetch_params).then(
+ function (response) {
+ return response.text().then(function(text) {
+ assert_equals(text, 'ok', `Socket unexpectedly reused`);
+ });
+ }));
+ }
+
+ // Wait for all promises to complete.
+ return Promise.allSettled(fetches).then(function (results) {
+ results.forEach(function (result) {
+ if (result.status != 'fulfilled')
+ throw result.reason;
+ });
+ });
+}
diff --git a/test/wpt/tests/fetch/connection-pool/resources/network-partition-key.py b/test/wpt/tests/fetch/connection-pool/resources/network-partition-key.py
new file mode 100644
index 0000000..32fe499
--- /dev/null
+++ b/test/wpt/tests/fetch/connection-pool/resources/network-partition-key.py
@@ -0,0 +1,130 @@
+import mimetypes
+import os
+
+from wptserve.utils import isomorphic_decode, isomorphic_encode
+
+# Test server that tracks the last partition_id was used with each connection for each uuid, and
+# lets consumers query if multiple different partition_ids have been been used for any socket.
+#
+# Server assumes that ports aren't reused, so a client address and a server port uniquely identify
+# a connection. If that constraint is ever violated, the test will be flaky. No sockets being
+# closed for the duration of the test is sufficient to ensure that, though even if sockets are
+# closed, the OS should generally prefer to use new ports for new connections, if any are
+# available.
+def main(request, response):
+ response.headers.set(b"Cache-Control", b"no-store")
+ dispatch = request.GET.first(b"dispatch", None)
+ uuid = request.GET.first(b"uuid", None)
+ partition_id = request.GET.first(b"partition_id", None)
+
+ if not uuid or not dispatch or not partition_id:
+ return simple_response(request, response, 404, b"Not found", b"Invalid query parameters")
+
+ # Unless nocheck_partition is true, check partition_id against server_state, and update server_state.
+ stash = request.server.stash
+ test_failed = False
+ request_count = 0;
+ connection_count = 0;
+ if request.GET.first(b"nocheck_partition", None) != b"True":
+ # Need to grab the lock to access the Stash, since requests are made in parallel.
+ with stash.lock:
+ # Don't use server hostname here, since H2 allows multiple hosts to reuse a connection.
+ # Server IP is not currently available, unfortunately.
+ address_key = isomorphic_encode(str(request.client_address) + u"|" + str(request.url_parts.port))
+ server_state = stash.take(uuid) or {b"test_failed": False,
+ b"request_count": 0, b"connection_count": 0}
+ request_count = server_state[b"request_count"]
+ request_count += 1
+ server_state[b"request_count"] = request_count
+ if address_key in server_state:
+ if server_state[address_key] != partition_id:
+ server_state[b"test_failed"] = True
+ else:
+ connection_count = server_state[b"connection_count"]
+ connection_count += 1
+ server_state[b"connection_count"] = connection_count
+ server_state[address_key] = partition_id
+ test_failed = server_state[b"test_failed"]
+ stash.put(uuid, server_state)
+
+ origin = request.headers.get(b"Origin")
+ if origin:
+ response.headers.set(b"Access-Control-Allow-Origin", origin)
+ response.headers.set(b"Access-Control-Allow-Credentials", b"true")
+
+ if request.method == u"OPTIONS":
+ return handle_preflight(request, response)
+
+ if dispatch == b"fetch_file":
+ return handle_fetch_file(request, response, partition_id, uuid)
+
+ if dispatch == b"check_partition":
+ status = request.GET.first(b"status", 200)
+ if test_failed:
+ return simple_response(request, response, status, b"OK", b"Multiple partition IDs used on a socket")
+ body = b"ok"
+ if request.GET.first(b"addcounter", False):
+ body += (". Request was sent " + str(request_count) + " times. " +
+ str(connection_count) + " connections were created.").encode('utf-8')
+ return simple_response(request, response, status, b"OK", body)
+
+ if dispatch == b"clean_up":
+ stash.take(uuid)
+ if test_failed:
+ return simple_response(request, response, 200, b"OK", b"Test failed, but cleanup completed.")
+ return simple_response(request, response, 200, b"OK", b"cleanup complete")
+
+ return simple_response(request, response, 404, b"Not Found", b"Unrecognized dispatch parameter: " + dispatch)
+
+def handle_preflight(request, response):
+ response.status = (200, b"OK")
+ response.headers.set(b"Access-Control-Allow-Methods", b"GET")
+ response.headers.set(b"Access-Control-Allow-Headers", b"header-to-force-cors")
+ response.headers.set(b"Access-Control-Max-Age", b"86400")
+ return b"Preflight request"
+
+def simple_response(request, response, status_code, status_message, body, content_type=b"text/plain"):
+ response.status = (status_code, status_message)
+ response.headers.set(b"Content-Type", content_type)
+ return body
+
+def handle_fetch_file(request, response, partition_id, uuid):
+ subresource_origin = request.GET.first(b"subresource_origin", None)
+ rel_path = request.GET.first(b"path", None)
+
+ # This needs to be passed on to subresources so they all have access to it.
+ include_credentials = request.GET.first(b"include_credentials", None)
+ if not subresource_origin or not rel_path or not include_credentials:
+ return simple_response(request, response, 404, b"Not found", b"Invalid query parameters")
+
+ cur_path = os.path.realpath(isomorphic_decode(__file__))
+ base_path = os.path.abspath(os.path.join(os.path.dirname(cur_path), os.pardir, os.pardir, os.pardir))
+ path = os.path.abspath(os.path.join(base_path, isomorphic_decode(rel_path)))
+
+ # Basic security check.
+ if not path.startswith(base_path):
+ return simple_response(request, response, 404, b"Not found", b"Invalid path")
+
+ sandbox = request.GET.first(b"sandbox", None)
+ if sandbox == b"true":
+ response.headers.set(b"Content-Security-Policy", b"sandbox allow-scripts")
+
+ file = open(path, mode="rb")
+ body = file.read()
+ file.close()
+
+ subresource_path = b"/" + isomorphic_encode(os.path.relpath(isomorphic_decode(__file__), base_path)).replace(b'\\', b'/')
+ subresource_params = b"?partition_id=" + partition_id + b"&uuid=" + uuid + b"&subresource_origin=" + subresource_origin + b"&include_credentials=" + include_credentials
+ body = body.replace(b"SUBRESOURCE_PREFIX:", subresource_origin + subresource_path + subresource_params)
+
+ other_origin = request.GET.first(b"other_origin", None)
+ if other_origin:
+ body = body.replace(b"OTHER_PREFIX:", other_origin + subresource_path + subresource_params)
+
+ mimetypes.init()
+ mimetype_pair = mimetypes.guess_type(path)
+ mimetype = mimetype_pair[0]
+
+ if mimetype == None or mimetype_pair[1] != None:
+ return simple_response(request, response, 500, b"Server Error", b"Unknown MIME type")
+ return simple_response(request, response, 200, b"OK", body, mimetype)
diff --git a/test/wpt/tests/fetch/connection-pool/resources/network-partition-worker-checker.html b/test/wpt/tests/fetch/connection-pool/resources/network-partition-worker-checker.html
new file mode 100644
index 0000000..e6b7ea7
--- /dev/null
+++ b/test/wpt/tests/fetch/connection-pool/resources/network-partition-worker-checker.html
@@ -0,0 +1,24 @@
+<!doctype html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Worker Network Partition Checker</title>
+ <meta name="help" href="https://fetch.spec.whatwg.org/#network-partition-keys">
+ <meta name="timeout" content="normal">
+ <script src="SUBRESOURCE_PREFIX:&dispatch=fetch_file&path=common/utils.js"></script>
+ <script src="SUBRESOURCE_PREFIX:&dispatch=fetch_file&path=resources/testharness.js"></script>
+ <script src="SUBRESOURCE_PREFIX:&dispatch=fetch_file&path=fetch/connection-pool/resources/network-partition-key.js"></script>
+</head>
+<body>
+<script>
+ // Workers must be same origin as the page loading them, but it's simpler to reuse the
+ // OTHER_PREFIX mechanism in the Python code than to craft the URL in Javascript here.
+ var worker = new Worker('OTHER_PREFIX:&dispatch=fetch_file&path=fetch/connection-pool/resources/network-partition-worker.js');
+ function message_listener(event) {
+ window.opener.postMessage(event.data, '*');
+ worker.terminate();
+ }
+ worker.addEventListener('message', message_listener);
+</script>
+</body>
+</html>
diff --git a/test/wpt/tests/fetch/connection-pool/resources/network-partition-worker.js b/test/wpt/tests/fetch/connection-pool/resources/network-partition-worker.js
new file mode 100644
index 0000000..1745edf
--- /dev/null
+++ b/test/wpt/tests/fetch/connection-pool/resources/network-partition-worker.js
@@ -0,0 +1,15 @@
+// This tests the partition key of fetches to subresouce_origin made by the worker and
+// imported scripts from subresource_origin.
+importScripts('SUBRESOURCE_PREFIX:&dispatch=fetch_file&path=common/utils.js');
+importScripts('SUBRESOURCE_PREFIX:&dispatch=fetch_file&path=resources/testharness.js');
+importScripts('SUBRESOURCE_PREFIX:&dispatch=fetch_file&path=fetch/connection-pool/resources/network-partition-key.js');
+
+async function fetch_and_reply() {
+ try {
+ await check_partition_ids();
+ self.postMessage({result: 'success'});
+ } catch (e) {
+ self.postMessage({result: 'error', details: e.message});
+ }
+}
+fetch_and_reply();
diff --git a/test/wpt/tests/fetch/content-encoding/bad-gzip-body.any.js b/test/wpt/tests/fetch/content-encoding/bad-gzip-body.any.js
new file mode 100644
index 0000000..17bc126
--- /dev/null
+++ b/test/wpt/tests/fetch/content-encoding/bad-gzip-body.any.js
@@ -0,0 +1,22 @@
+// META: global=window,worker
+
+promise_test((test) => {
+ return fetch("resources/bad-gzip-body.py").then(res => {
+ assert_equals(res.status, 200);
+ });
+}, "Fetching a resource with bad gzip content should still resolve");
+
+[
+ "arrayBuffer",
+ "blob",
+ "formData",
+ "json",
+ "text"
+].forEach(method => {
+ promise_test(t => {
+ return fetch("resources/bad-gzip-body.py").then(res => {
+ assert_equals(res.status, 200);
+ return promise_rejects_js(t, TypeError, res[method]());
+ });
+ }, "Consuming the body of a resource with bad gzip content with " + method + "() should reject");
+});
diff --git a/test/wpt/tests/fetch/content-encoding/gzip-body.any.js b/test/wpt/tests/fetch/content-encoding/gzip-body.any.js
new file mode 100644
index 0000000..37758b7
--- /dev/null
+++ b/test/wpt/tests/fetch/content-encoding/gzip-body.any.js
@@ -0,0 +1,16 @@
+// META: global=window,worker
+
+const expectedDecompressedSize = 10500;
+[
+ "text",
+ "octetstream"
+].forEach(contentType => {
+ promise_test(async t => {
+ let response = await fetch(`resources/foo.${contentType}.gz`);
+ assert_true(response.ok);
+ let arrayBuffer = await response.arrayBuffer()
+ let u8 = new Uint8Array(arrayBuffer);
+ assert_equals(u8.length, expectedDecompressedSize);
+ }, `fetched gzip data with content type ${contentType} should be decompressed.`);
+});
+
diff --git a/test/wpt/tests/fetch/content-encoding/resources/bad-gzip-body.py b/test/wpt/tests/fetch/content-encoding/resources/bad-gzip-body.py
new file mode 100644
index 0000000..a79b94e
--- /dev/null
+++ b/test/wpt/tests/fetch/content-encoding/resources/bad-gzip-body.py
@@ -0,0 +1,3 @@
+def main(request, response):
+ headers = [(b"Content-Encoding", b"gzip")]
+ return headers, b"not actually gzip"
diff --git a/test/wpt/tests/fetch/content-encoding/resources/foo.octetstream.gz b/test/wpt/tests/fetch/content-encoding/resources/foo.octetstream.gz
new file mode 100644
index 0000000..f3df4cb
--- /dev/null
+++ b/test/wpt/tests/fetch/content-encoding/resources/foo.octetstream.gz
Binary files differ
diff --git a/test/wpt/tests/fetch/content-encoding/resources/foo.octetstream.gz.headers b/test/wpt/tests/fetch/content-encoding/resources/foo.octetstream.gz.headers
new file mode 100644
index 0000000..27d4f40
--- /dev/null
+++ b/test/wpt/tests/fetch/content-encoding/resources/foo.octetstream.gz.headers
@@ -0,0 +1,2 @@
+Content-type: application/octet-stream
+Content-Encoding: gzip
diff --git a/test/wpt/tests/fetch/content-encoding/resources/foo.text.gz b/test/wpt/tests/fetch/content-encoding/resources/foo.text.gz
new file mode 100644
index 0000000..05a5cce
--- /dev/null
+++ b/test/wpt/tests/fetch/content-encoding/resources/foo.text.gz
Binary files differ
diff --git a/test/wpt/tests/fetch/content-encoding/resources/foo.text.gz.headers b/test/wpt/tests/fetch/content-encoding/resources/foo.text.gz.headers
new file mode 100644
index 0000000..7def3dd
--- /dev/null
+++ b/test/wpt/tests/fetch/content-encoding/resources/foo.text.gz.headers
@@ -0,0 +1,2 @@
+Content-type: text/plain
+Content-Encoding: gzip
diff --git a/test/wpt/tests/fetch/content-length/api-and-duplicate-headers.any.js b/test/wpt/tests/fetch/content-length/api-and-duplicate-headers.any.js
new file mode 100644
index 0000000..8015289
--- /dev/null
+++ b/test/wpt/tests/fetch/content-length/api-and-duplicate-headers.any.js
@@ -0,0 +1,23 @@
+promise_test(async t => {
+ const response = await fetch("resources/identical-duplicates.asis");
+ assert_equals(response.statusText, "BLAH");
+ assert_equals(response.headers.get("test"), "x, x");
+ assert_equals(response.headers.get("content-type"), "text/plain, text/plain");
+ assert_equals(response.headers.get("content-length"), "6, 6");
+ const text = await response.text();
+ assert_equals(text, "Test.\n");
+}, "fetch() and duplicate Content-Length/Content-Type headers");
+
+async_test(t => {
+ const xhr = new XMLHttpRequest();
+ xhr.open("GET", "resources/identical-duplicates.asis");
+ xhr.send();
+ xhr.onload = t.step_func_done(() => {
+ assert_equals(xhr.statusText, "BLAH");
+ assert_equals(xhr.getResponseHeader("test"), "x, x");
+ assert_equals(xhr.getResponseHeader("content-type"), "text/plain, text/plain");
+ assert_equals(xhr.getResponseHeader("content-length"), "6, 6");
+ assert_equals(xhr.getAllResponseHeaders(), "content-length: 6, 6\r\ncontent-type: text/plain, text/plain\r\ntest: x, x\r\n");
+ assert_equals(xhr.responseText, "Test.\n");
+ });
+}, "XMLHttpRequest and duplicate Content-Length/Content-Type headers");
diff --git a/test/wpt/tests/fetch/content-length/content-length.html b/test/wpt/tests/fetch/content-length/content-length.html
new file mode 100644
index 0000000..cda9b5b
--- /dev/null
+++ b/test/wpt/tests/fetch/content-length/content-length.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<!-- CAUTION: if updating this test also update the expected content-length in the .headers file -->
+<title>Content-Length Test</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+setup({ single_test: true });
+onload = function() {
+ assert_equals(document.body.textContent, "PASS");
+ done();
+}
+</script>
+<body>PASS
+but FAIL if this is in the body. \ No newline at end of file
diff --git a/test/wpt/tests/fetch/content-length/content-length.html.headers b/test/wpt/tests/fetch/content-length/content-length.html.headers
new file mode 100644
index 0000000..25389b7
--- /dev/null
+++ b/test/wpt/tests/fetch/content-length/content-length.html.headers
@@ -0,0 +1 @@
+Content-Length: 403
diff --git a/test/wpt/tests/fetch/content-length/parsing.window.js b/test/wpt/tests/fetch/content-length/parsing.window.js
new file mode 100644
index 0000000..5028ad9
--- /dev/null
+++ b/test/wpt/tests/fetch/content-length/parsing.window.js
@@ -0,0 +1,18 @@
+promise_test(() => {
+ return fetch("resources/content-lengths.json").then(res => res.json()).then(runTests);
+}, "Loading JSON…");
+
+function runTests(testUnits) {
+ testUnits.forEach(({ input, output }) => {
+ promise_test(t => {
+ const result = fetch(`resources/content-length.py?length=${encodeURIComponent(input)}`);
+ if (output === null) {
+ return promise_rejects_js(t, TypeError, result);
+ } else {
+ return result.then(res => res.text()).then(text => {
+ assert_equals(text.length, output);
+ });
+ }
+ }, `Input: ${format_value(input)}. Expected: ${output === null ? "network error" : output}.`);
+ });
+}
diff --git a/test/wpt/tests/fetch/content-length/resources/content-length.py b/test/wpt/tests/fetch/content-length/resources/content-length.py
new file mode 100644
index 0000000..92cfade
--- /dev/null
+++ b/test/wpt/tests/fetch/content-length/resources/content-length.py
@@ -0,0 +1,10 @@
+def main(request, response):
+ response.add_required_headers = False
+ output = b"HTTP/1.1 200 OK\r\n"
+ output += b"Content-Type: text/plain;charset=UTF-8\r\n"
+ output += b"Connection: close\r\n"
+ output += request.GET.first(b"length") + b"\r\n"
+ output += b"\r\n"
+ output += b"Fact: this is really forty-two bytes long."
+ response.writer.write(output)
+ response.close_connection = True
diff --git a/test/wpt/tests/fetch/content-length/resources/content-lengths.json b/test/wpt/tests/fetch/content-length/resources/content-lengths.json
new file mode 100644
index 0000000..ac6f1a2
--- /dev/null
+++ b/test/wpt/tests/fetch/content-length/resources/content-lengths.json
@@ -0,0 +1,142 @@
+[
+ {
+ "input": "Content-Length: 42",
+ "output": 42
+ },
+ {
+ "input": "Content-Length: 42,42",
+ "output": 42
+ },
+ {
+ "input": "Content-Length: 42\r\nContent-Length: 42",
+ "output": 42
+ },
+ {
+ "input": "Content-Length: 42\r\nContent-Length: 42,42",
+ "output": 42
+ },
+ {
+ "input": "Content-Length: 30",
+ "output": 30
+ },
+ {
+ "input": "Content-Length: 30,30",
+ "output": 30
+ },
+ {
+ "input": "Content-Length: 30\r\nContent-Length: 30",
+ "output": 30
+ },
+ {
+ "input": "Content-Length: 30\r\nContent-Length: 30,30",
+ "output": 30
+ },
+ {
+ "input": "Content-Length: 30,30\r\nContent-Length: 30,30",
+ "output": 30
+ },
+ {
+ "input": "Content-Length: 30,30, 30 \r\nContent-Length: 30 ",
+ "output": 30
+ },
+ {
+ "input": "Content-Length: 30,42\r\nContent-Length: 30",
+ "output": null
+ },
+ {
+ "input": "Content-Length: 30,42\r\nContent-Length: 30,42",
+ "output": null
+ },
+ {
+ "input": "Content-Length: 42,30",
+ "output": null
+ },
+ {
+ "input": "Content-Length: 30,42",
+ "output": null
+ },
+ {
+ "input": "Content-Length: 42\r\nContent-Length: 30",
+ "output": null
+ },
+ {
+ "input": "Content-Length: 30\r\nContent-Length: 42",
+ "output": null
+ },
+ {
+ "input": "Content-Length: 30,",
+ "output": null
+ },
+ {
+ "input": "Content-Length: ,30",
+ "output": null
+ },
+ {
+ "input": "Content-Length: 30\r\nContent-Length: \t",
+ "output": null
+ },
+ {
+ "input": "Content-Length: \r\nContent-Length: 30",
+ "output": null
+ },
+ {
+ "input": "Content-Length: aaaah\r\nContent-Length: nah",
+ "output": null
+ },
+ {
+ "input": "Content-Length: aaaah, nah",
+ "output": null
+ },
+ {
+ "input": "Content-Length: aaaah\r\nContent-Length: aaaah",
+ "output": 42
+ },
+ {
+ "input": "Content-Length: aaaah, aaaah",
+ "output": 42
+ },
+ {
+ "input": "Content-Length: aaaah",
+ "output": 42
+ },
+ {
+ "input": "Content-Length: 42s",
+ "output": 42
+ },
+ {
+ "input": "Content-Length: 30s",
+ "output": 42
+ },
+ {
+ "input": "Content-Length: -1",
+ "output": 42
+ },
+ {
+ "input": "Content-Length: 0x20",
+ "output": 42
+ },
+ {
+ "input": "Content-Length: 030",
+ "output": 30
+ },
+ {
+ "input": "Content-Length: 030\r\nContent-Length: 30",
+ "output": null
+ },
+ {
+ "input": "Content-Length: 030, 30",
+ "output": null
+ },
+ {
+ "input": "Content-Length: \"30\"",
+ "output": 42
+ },
+ {
+ "input": "Content-Length:30\r\nContent-Length:,\r\nContent-Length:30",
+ "output": null
+ },
+ {
+ "input": "Content-Length: ",
+ "output": 42
+ }
+]
diff --git a/test/wpt/tests/fetch/content-length/resources/identical-duplicates.asis b/test/wpt/tests/fetch/content-length/resources/identical-duplicates.asis
new file mode 100644
index 0000000..f38c9a4
--- /dev/null
+++ b/test/wpt/tests/fetch/content-length/resources/identical-duplicates.asis
@@ -0,0 +1,9 @@
+HTTP/1.1 200 BLAH
+Test: x
+Test: x
+Content-Type: text/plain
+Content-Type: text/plain
+Content-Length: 6
+Content-Length: 6
+
+Test.
diff --git a/test/wpt/tests/fetch/content-length/too-long.window.js b/test/wpt/tests/fetch/content-length/too-long.window.js
new file mode 100644
index 0000000..f8cefaa
--- /dev/null
+++ b/test/wpt/tests/fetch/content-length/too-long.window.js
@@ -0,0 +1,4 @@
+promise_test(async t => {
+ const result = await fetch(`resources/content-length.py?length=${encodeURIComponent("Content-Length: 50")}`);
+ await promise_rejects_js(t, TypeError, result.text());
+}, "Content-Length header value of network response exceeds response body");
diff --git a/test/wpt/tests/fetch/content-type/README.md b/test/wpt/tests/fetch/content-type/README.md
new file mode 100644
index 0000000..f553b7e
--- /dev/null
+++ b/test/wpt/tests/fetch/content-type/README.md
@@ -0,0 +1,20 @@
+# `resources/content-types.json`
+
+An array of tests. Each test has these fields:
+
+* `contentType`: an array of values for the `Content-Type` header. A harness needs to run the test twice if there are multiple values. One time with the values concatenated with `,` followed by a space and one time with multiple `Content-Type` declarations, each on their own line with one of the values, in order.
+* `encoding`: the expected encoding, null for the default.
+* `mimeType`: the result of extracting a MIME type and serializing it.
+* `documentContentType`: the MIME type expected to be exposed in DOM documents.
+
+(These tests are currently somewhat geared towards browser use, but could be generalized easily enough if someone wanted to contribute tests for MIME types that would cause downloads in the browser or some such.)
+
+# `resources/script-content-types.json`
+
+An array of tests, surprise. Each test has these fields:
+
+* `contentType`: see above.
+* `executes`: whether the script is expected to execute.
+* `encoding`: how the script is expected to be decoded.
+
+These tests are expected to be loaded through `<script src>` and the server is expected to set `X-Content-Type-Options: nosniff`.
diff --git a/test/wpt/tests/fetch/content-type/multipart-malformed.any.js b/test/wpt/tests/fetch/content-type/multipart-malformed.any.js
new file mode 100644
index 0000000..9de0edc
--- /dev/null
+++ b/test/wpt/tests/fetch/content-type/multipart-malformed.any.js
@@ -0,0 +1,22 @@
+// This is a repro for Chromium issue https://crbug.com/1412007.
+promise_test(t => {
+ const form_string =
+ "--Boundary_with_capital_letters\r\n" +
+ "Content-Type: application/json\r\n" +
+ 'Content-Disposition: form-data; name="does_this_work"\r\n' +
+ "\r\n" +
+ 'YES\r\n' +
+ "--Boundary_with_capital_letters-Random junk";
+
+ const r = new Response(new Blob([form_string]), {
+ headers: [
+ [
+ "Content-Type",
+ "multipart/form-data; boundary=Boundary_with_capital_letters",
+ ],
+ ],
+ });
+
+ return promise_rejects_js(t, TypeError, r.formData(),
+ "form data should fail to parse");
+}, "Invalid form data should not crash the browser");
diff --git a/test/wpt/tests/fetch/content-type/multipart.window.js b/test/wpt/tests/fetch/content-type/multipart.window.js
new file mode 100644
index 0000000..03b037a
--- /dev/null
+++ b/test/wpt/tests/fetch/content-type/multipart.window.js
@@ -0,0 +1,33 @@
+// META: title=Ensure capital letters can be used in the boundary value.
+setup({ single_test: true });
+(async () => {
+ const form_string =
+ "--Boundary_with_capital_letters\r\n" +
+ "Content-Type: application/json\r\n" +
+ 'Content-Disposition: form-data; name="does_this_work"\r\n' +
+ "\r\n" +
+ 'YES\r\n' +
+ "--Boundary_with_capital_letters--\r\n";
+
+ const r = new Response(new Blob([form_string]), {
+ headers: [
+ [
+ "Content-Type",
+ "multipart/form-data; boundary=Boundary_with_capital_letters",
+ ],
+ ],
+ });
+
+ var s = "";
+ try {
+ const fd = await r.formData();
+ for (const [key, value] of fd.entries()) {
+ s += (`${key} = ${value}`);
+ }
+ } catch (ex) {
+ s = ex;
+ }
+
+ assert_equals(s, "does_this_work = YES");
+ done();
+})();
diff --git a/test/wpt/tests/fetch/content-type/resources/content-type.py b/test/wpt/tests/fetch/content-type/resources/content-type.py
new file mode 100644
index 0000000..1f077b6
--- /dev/null
+++ b/test/wpt/tests/fetch/content-type/resources/content-type.py
@@ -0,0 +1,18 @@
+from wptserve.utils import isomorphic_encode
+
+def main(request, response):
+ values = request.GET.get_list(b"value")
+ content = request.GET.first(b"content", b"<b>hi</b>\n")
+ output = b"HTTP/1.1 200 OK\r\n"
+ output += b"X-Content-Type-Options: nosniff\r\n"
+ if b"single_header" in request.GET:
+ output += b"Content-Type: " + b",".join(values) + b"\r\n"
+ else:
+ for value in values:
+ output += b"Content-Type: " + value + b"\r\n"
+ output += b"Content-Length: " + isomorphic_encode(str(len(content))) + b"\r\n"
+ output += b"Connection: close\r\n"
+ output += b"\r\n"
+ output += content
+ response.writer.write(output)
+ response.close_connection = True
diff --git a/test/wpt/tests/fetch/content-type/resources/content-types.json b/test/wpt/tests/fetch/content-type/resources/content-types.json
new file mode 100644
index 0000000..9578fc5
--- /dev/null
+++ b/test/wpt/tests/fetch/content-type/resources/content-types.json
@@ -0,0 +1,122 @@
+[
+ {
+ "contentType": ["", "text/plain"],
+ "encoding": null,
+ "mimeType": "text/plain",
+ "documentContentType": "text/plain"
+ },
+ {
+ "contentType": ["text/plain", ""],
+ "encoding": null,
+ "mimeType": "text/plain",
+ "documentContentType": "text/plain"
+ },
+ {
+ "contentType": ["text/html", "text/plain"],
+ "encoding": null,
+ "mimeType": "text/plain",
+ "documentContentType": "text/plain"
+ },
+ {
+ "contentType": ["text/plain;charset=gbk", "text/html"],
+ "encoding": null,
+ "mimeType": "text/html",
+ "documentContentType": "text/html"
+ },
+ {
+ "contentType": ["text/plain;charset=gbk", "text/html;charset=windows-1254"],
+ "encoding": "windows-1254",
+ "mimeType": "text/html;charset=windows-1254",
+ "documentContentType": "text/html"
+ },
+ {
+ "contentType": ["text/plain;charset=gbk", "text/plain"],
+ "encoding": "GBK",
+ "mimeType": "text/plain;charset=gbk",
+ "documentContentType": "text/plain"
+ },
+ {
+ "contentType": ["text/plain;charset=gbk", "text/plain;charset=windows-1252"],
+ "encoding": "windows-1252",
+ "mimeType": "text/plain;charset=windows-1252",
+ "documentContentType": "text/plain"
+ },
+ {
+ "contentType": ["text/html;charset=gbk", "text/html;x=\",text/plain"],
+ "encoding": "GBK",
+ "mimeType": "text/html;x=\",text/plain\";charset=gbk",
+ "documentContentType": "text/html"
+ },
+ {
+ "contentType": ["text/plain;charset=gbk;x=foo", "text/plain"],
+ "encoding": "GBK",
+ "mimeType": "text/plain;charset=gbk",
+ "documentContentType": "text/plain"
+ },
+ {
+ "contentType": ["text/html;charset=gbk", "text/plain", "text/html"],
+ "encoding": null,
+ "mimeType": "text/html",
+ "documentContentType": "text/html"
+ },
+ {
+ "contentType": ["text/plain", "*/*"],
+ "encoding": null,
+ "mimeType": "text/plain",
+ "documentContentType": "text/plain"
+ },
+ {
+ "contentType": ["text/html", "*/*"],
+ "encoding": null,
+ "mimeType": "text/html",
+ "documentContentType": "text/html"
+ },
+ {
+ "contentType": ["*/*", "text/html"],
+ "encoding": null,
+ "mimeType": "text/html",
+ "documentContentType": "text/html"
+ },
+ {
+ "contentType": ["text/plain", "*/*;charset=gbk"],
+ "encoding": null,
+ "mimeType": "text/plain",
+ "documentContentType": "text/plain"
+ },
+ {
+ "contentType": ["text/html", "*/*;charset=gbk"],
+ "encoding": null,
+ "mimeType": "text/html",
+ "documentContentType": "text/html"
+ },
+ {
+ "contentType": ["text/html;x=\"", "text/plain"],
+ "encoding": null,
+ "mimeType": "text/html;x=\", text/plain\"",
+ "documentContentType": "text/html"
+ },
+ {
+ "contentType": ["text/html;\"", "text/plain"],
+ "encoding": null,
+ "mimeType": "text/html",
+ "documentContentType": "text/html"
+ },
+ {
+ "contentType": ["text/html;\"", "\\\"", "text/plain"],
+ "encoding": null,
+ "mimeType": "text/html",
+ "documentContentType": "text/html"
+ },
+ {
+ "contentType": ["text/html;\"", "\\\"", "text/plain", "\";charset=GBK"],
+ "encoding": "GBK",
+ "mimeType": "text/html;charset=GBK",
+ "documentContentType": "text/html"
+ },
+ {
+ "contentType": ["text/html;\"", "\"", "text/plain"],
+ "encoding": null,
+ "mimeType": "text/plain",
+ "documentContentType": "text/plain"
+ }
+]
diff --git a/test/wpt/tests/fetch/content-type/resources/script-content-types.json b/test/wpt/tests/fetch/content-type/resources/script-content-types.json
new file mode 100644
index 0000000..b8a843b
--- /dev/null
+++ b/test/wpt/tests/fetch/content-type/resources/script-content-types.json
@@ -0,0 +1,92 @@
+[
+ {
+ "contentType": ["text/javascript;charset=windows-1252"],
+ "executes": true,
+ "encoding": "windows-1252"
+ },
+ {
+ "contentType": ["text/javascript;\";charset=windows-1252"],
+ "executes": true,
+ "encoding": "windows-1252"
+ },
+ {
+ "contentType": ["text/javascript\u000C"],
+ "executes": false,
+ "encoding": null
+ },
+ {
+ "contentType": ["\"text/javascript\""],
+ "executes": false,
+ "encoding": null
+ },
+ {
+ "contentType": ["text/ javascript"],
+ "executes": false,
+ "encoding": null
+ },
+ {
+ "contentType": ["text /javascript"],
+ "executes": false,
+ "encoding": null
+ },
+ {
+ "contentType": ["x/x", "text/javascript"],
+ "executes": true,
+ "encoding": null
+ },
+ {
+ "contentType": ["x/x;charset=windows-1252", "text/javascript"],
+ "executes": true,
+ "encoding": null
+ },
+ {
+ "contentType": ["text/javascript", "x/x"],
+ "executes": false,
+ "encoding": null
+ },
+ {
+ "contentType": ["text/javascript; charset=windows-1252", "text/javascript"],
+ "executes": true,
+ "encoding": "windows-1252"
+ },
+ {
+ "contentType": ["text/javascript;\"", "x/x"],
+ "executes": true,
+ "encoding": null
+ },
+ {
+ "contentType": ["text/javascript", ""],
+ "executes": true,
+ "encoding": null
+ },
+ {
+ "contentType": ["text/javascript", "error"],
+ "executes": true,
+ "encoding": null
+ },
+ {
+ "contentType": ["text/javascript;charset=windows-1252", "x/x", "text/javascript"],
+ "executes": true,
+ "encoding": null
+ },
+ {
+ "contentType": ["text/javascript;charset=windows-1252", "error", "text/javascript"],
+ "executes": true,
+ "encoding": "windows-1252"
+ },
+ {
+ "contentType": ["text/javascript;charset=windows-1252", "", "text/javascript"],
+ "executes": true,
+ "encoding": "windows-1252"
+ },
+ {
+ "contentType": ["text/javascript;charset=windows-1252;\"", "\\\"", "x/x"],
+ "executes": true,
+ "encoding": "windows-1252"
+ },
+ {
+ "contentType": ["x/x;\"", "x/y;\\\"", "text/javascript;charset=windows-1252;\"", "text/javascript"],
+ "executes": true,
+ "encoding": null
+ }
+]
diff --git a/test/wpt/tests/fetch/content-type/response.window.js b/test/wpt/tests/fetch/content-type/response.window.js
new file mode 100644
index 0000000..746f51c
--- /dev/null
+++ b/test/wpt/tests/fetch/content-type/response.window.js
@@ -0,0 +1,72 @@
+promise_test(() => {
+ return fetch("resources/content-types.json").then(res => res.json()).then(runTests);
+}, "Loading JSON…");
+
+function runTests(tests) {
+ tests.forEach(testUnit => {
+ runFrameTest(testUnit, false);
+ runFrameTest(testUnit, true);
+ runFetchTest(testUnit, false);
+ runFetchTest(testUnit, true);
+ runRequestResponseTest(testUnit, "Request");
+ runRequestResponseTest(testUnit, "Response");
+ });
+}
+
+function runFrameTest(testUnit, singleHeader) {
+ // Note: window.js is always UTF-8
+ const encoding = testUnit.encoding !== null ? testUnit.encoding : "UTF-8";
+ async_test(t => {
+ const frame = document.body.appendChild(document.createElement("iframe"));
+ t.add_cleanup(() => frame.remove());
+ frame.src = getURL(testUnit.contentType, singleHeader);
+ frame.onload = t.step_func_done(() => {
+ // Edge requires toUpperCase()
+ const doc = frame.contentDocument;
+ assert_equals(doc.characterSet.toUpperCase(), encoding.toUpperCase());
+ if (testUnit.documentContentType === "text/plain") {
+ assert_equals(doc.body.textContent, "<b>hi</b>\n");
+ } else if (testUnit.documentContentType === "text/html") {
+ assert_equals(doc.body.firstChild.localName, "b");
+ assert_equals(doc.body.firstChild.textContent, "hi");
+ }
+ assert_equals(doc.contentType, testUnit.documentContentType);
+ });
+ }, getDesc("<iframe>", testUnit.contentType, singleHeader));
+}
+
+function getDesc(type, input, singleHeader) {
+ return type + ": " + (singleHeader ? "combined" : "separate") + " response Content-Type: " + input.join(" ");
+}
+
+function getURL(input, singleHeader) {
+ // Edge does not support URLSearchParams
+ let url = "resources/content-type.py?"
+ if (singleHeader) {
+ url += "single_header&"
+ }
+ input.forEach(val => {
+ url += "value=" + encodeURIComponent(val) + "&";
+ });
+ return url;
+}
+
+function runFetchTest(testUnit, singleHeader) {
+ promise_test(async t => {
+ const blob = await (await fetch(getURL(testUnit.contentType, singleHeader))).blob();
+ assert_equals(blob.type, testUnit.mimeType);
+ }, getDesc("fetch()", testUnit.contentType, singleHeader));
+}
+
+function runRequestResponseTest(testUnit, stringConstructor) {
+ promise_test(async t => {
+ // Cannot give Response a body as that will set Content-Type, but Request needs a URL
+ const constructorArgument = stringConstructor === "Request" ? "about:blank" : undefined;
+ const r = new self[stringConstructor](constructorArgument);
+ testUnit.contentType.forEach(val => {
+ r.headers.append("Content-Type", val);
+ });
+ const blob = await r.blob();
+ assert_equals(blob.type, testUnit.mimeType);
+ }, getDesc(stringConstructor, testUnit.contentType, true));
+}
diff --git a/test/wpt/tests/fetch/content-type/script.window.js b/test/wpt/tests/fetch/content-type/script.window.js
new file mode 100644
index 0000000..3159895
--- /dev/null
+++ b/test/wpt/tests/fetch/content-type/script.window.js
@@ -0,0 +1,48 @@
+promise_test(() => {
+ return fetch("resources/script-content-types.json").then(res => res.json()).then(runTests);
+}, "Loading JSON…");
+
+self.stringFromExecutedScript = undefined;
+
+function runTests(allTestData) {
+ allTestData.forEach(testData => {
+ runScriptTest(testData, false);
+ if (testData.contentType.length > 1) {
+ runScriptTest(testData, true);
+ }
+ });
+}
+
+function runScriptTest(testData, singleHeader) {
+ async_test(t => {
+ const script = document.createElement("script");
+ t.add_cleanup(() => {
+ script.remove()
+ self.stringFromExecutedScript = undefined;
+ });
+ script.src = getURL(testData.contentType, singleHeader);
+ document.head.appendChild(script);
+ if (testData.executes) {
+ script.onload = t.step_func_done(() => {
+ assert_equals(self.stringFromExecutedScript, testData.encoding === "windows-1252" ? "€" : "€");
+ });
+ script.onerror = t.unreached_func("onerror");
+ } else {
+ script.onerror = t.step_func_done();
+ script.onload = t.unreached_func("onload");
+ }
+ }, (singleHeader ? "combined" : "separate") + " " + testData.contentType.join(" "));
+}
+
+function getURL(input, singleHeader) {
+ // Edge does not support URLSearchParams
+ let url = "resources/content-type.py?"
+ if (singleHeader) {
+ url += "single_header&"
+ }
+ input.forEach(val => {
+ url += "value=" + encodeURIComponent(val) + "&";
+ });
+ url += "&content=" + encodeURIComponent("self.stringFromExecutedScript = \"€\"");
+ return url;
+}
diff --git a/test/wpt/tests/fetch/corb/README.md b/test/wpt/tests/fetch/corb/README.md
new file mode 100644
index 0000000..f29562b
--- /dev/null
+++ b/test/wpt/tests/fetch/corb/README.md
@@ -0,0 +1,67 @@
+# Tests related to Cross-Origin Resource Blocking (CORB).
+
+### Summary
+
+This directory contains tests related to the
+[Cross-Origin Resource Blocking (CORB)](https://chromium.googlesource.com/chromium/src/+/main/services/network/cross_origin_read_blocking_explainer.md)
+algorithm.
+
+The tests in this directory interact with various, random features,
+but the tests have been grouped together into the `fetch/corb` directory,
+because all of these tests verify behavior that is important to the CORB
+algorithm.
+
+
+### CORB is not universally implemented yet
+
+CORB has been included
+in the [Fetch spec](https://fetch.spec.whatwg.org/#corb)
+since [May 2018](https://github.com/whatwg/fetch/pull/686).
+
+Some tests in this directory (e.g.
+`css-with-json-parser-breaker`) cover behavior spec-ed outside of CORB (making
+sure that CORB doesn't change the existing web behavior) and therefore are
+valuable independently from CORB's standardization efforts and should already
+be passing across all browsers.
+
+Tests that cover behavior that is changed by CORB are currently marked as
+[tentative](https://web-platform-tests.org/writing-tests/file-names.html)
+(using `.tentative` substring in their filename).
+Such tests may fail unless CORB is enabled. In practice this means that:
+* Such tests will pass in Chromium
+ (where CORB is enabled by default [since M68](https://crrev.com/553830)).
+* Such tests may fail in other browsers.
+
+
+### Limitations of WPT test coverage
+
+CORB is a defense-in-depth and in general should not cause changes in behavior
+that can be observed by web features or by end users. This makes CORB difficult
+or even impossible to test via WPT.
+
+WPT tests can cover the following:
+
+* Helping verify CORB has no observable impact in specific scenarios.
+ Examples:
+ * image rendering of (an empty response of) a html document blocked by CORB
+ should be indistinguishable from rendering such html document without CORB -
+ `img-html-correctly-labeled.sub.html`
+ * CORB shouldn't block responses that don't sniff as a CORB-protected document
+ type - `img-png-mislabeled-as-html.sub.html`
+* Helping document cases where CORB causes observable changes in behavior.
+ Examples:
+ * blocking of nosniff images labeled as non-image, CORB-protected
+ Content-Type - `img-png-mislabeled-as-html-nosniff.tentative.sub.html`
+ * blocking of CORB-protected documents can prevent triggering
+ syntax errors in scripts -
+ `script-html-via-cross-origin-blob-url.tentative.sub.html`
+* Helping verify which MIME types are protected by CORB.
+
+Examples of aspects that WPT tests cannot cover (these aspects have to be
+covered in other, browser-specific tests):
+* Verifying that CORB doesn't affect things that are only indirectly
+ observable by the web (like
+ [prefetch](https://html.spec.whatwg.org/#link-type-prefetch).
+* Verifying that CORB strips headers of blocked responses.
+* Verifying that CORB blocks responses before they reach the process hosting
+ a cross-origin execution context.
diff --git a/test/wpt/tests/fetch/corb/img-html-correctly-labeled.sub-ref.html b/test/wpt/tests/fetch/corb/img-html-correctly-labeled.sub-ref.html
new file mode 100644
index 0000000..0e75596
--- /dev/null
+++ b/test/wpt/tests/fetch/corb/img-html-correctly-labeled.sub-ref.html
@@ -0,0 +1,4 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<!-- Same-origin, so the HTTP response is not CORB-eligible -->
+<img src="resources/html-correctly-labeled.html">
diff --git a/test/wpt/tests/fetch/corb/img-html-correctly-labeled.sub.html b/test/wpt/tests/fetch/corb/img-html-correctly-labeled.sub.html
new file mode 100644
index 0000000..844cd0c
--- /dev/null
+++ b/test/wpt/tests/fetch/corb/img-html-correctly-labeled.sub.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<!-- Test verifies that html fed to an <img> tag doesn't have any observable
+ difference with and without CORB (in both cases the resource body cannot be
+ rendered as an image - html cannot be rendered as an image and the empty body
+ from a CORB-blocked response also cannot be rendered as an image).
+-->
+<meta charset="utf-8">
+<!-- Reference page uses same-origin resources, which are not CORB-eligible. -->
+<link rel="match" href="img-html-correctly-labeled.sub-ref.html">
+<!-- www1 is cross-origin, so the HTTP response is CORB-eligible -->
+<img src="http://{{domains[www1]}}:{{ports[http][0]}}/fetch/corb/resources/html-correctly-labeled.html">
diff --git a/test/wpt/tests/fetch/corb/img-mime-types-coverage.tentative.sub.html b/test/wpt/tests/fetch/corb/img-mime-types-coverage.tentative.sub.html
new file mode 100644
index 0000000..e2386de
--- /dev/null
+++ b/test/wpt/tests/fetch/corb/img-mime-types-coverage.tentative.sub.html
@@ -0,0 +1,85 @@
+<!-- Test verifies that cross-origin, nosniff images are 1) blocked when their
+ MIME type is covered by CORB and 2) allowed otherwise.
+
+ This test is very similar to fetch/nosniff/images.html, except that
+ 1) it deals with cross-origin images (CORB ignores same-origin fetches),
+ 2) it focuses on MIME types relevant to CORB.
+ There are opportunities to unify the test here with nosniff tests *if*
+ we can also start blocking same-origin (or cors-allowed) images. We
+ should try to gather data to quantify the impact of such change.
+-->
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<div id=log></div>
+<script>
+ var passes = [
+ // Empty or non-sensical MIME types
+ null, "", "x", "x/x",
+
+ // MIME-types not protected by CORB
+ "image/gif", "image/png", "image/png;blah", "image/svg+xml",
+ "application/javascript", "application/jsonp",
+ "application/dash+xml", // video format
+ "image/gif;HI=THERE",
+
+ // Non-image MIME-types that in practice get used for images on the web.
+ //
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1302539
+ "application/octet-stream",
+ // https://crbug.com/990853
+ "application/x-www-form-urlencoded",
+
+ // MIME types that may seem to be JSON or XML, but really aren't - i.e.
+ // these MIME types are not covered by:
+ // - https://mimesniff.spec.whatwg.org/#json-mime-type
+ // - https://mimesniff.spec.whatwg.org/#xml-mime-type
+ // - https://tools.ietf.org/html/rfc6839
+ // - https://tools.ietf.org/html/rfc7303
+ "text/x-json", "text/json+blah", "application/json+blah",
+ "text/xml+blah", "application/xml+blah",
+ "application/blahjson", "text/blahxml"]
+
+ var fails = [
+ // CORB-protected MIME-types - i.e. ones covered by:
+ // - https://mimesniff.spec.whatwg.org/#html-mime-type
+ // - https://mimesniff.spec.whatwg.org/#json-mime-type
+ // - https://mimesniff.spec.whatwg.org/#xml-mime-type
+ "text/html",
+ "text/json", "application/json", "text/xml", "application/xml",
+ "application/blah+json", "text/blah+json",
+ "application/blah+xml", "text/blah+xml",
+ "TEXT/HTML", "TEXT/JSON", "TEXT/BLAH+JSON", "APPLICATION/BLAH+XML",
+ "text/json;does=it;matter", "text/HTML;NO=it;does=NOT"]
+
+ const get_url = (mime) => {
+ // www1 is cross-origin, so the HTTP response is CORB-eligible -->
+ url = "http://{{domains[www1]}}:{{ports[http][0]}}"
+ url = url + "/fetch/nosniff/resources/image.py"
+ if (mime != null) {
+ url += "?type=" + encodeURIComponent(mime)
+ }
+ return url
+ }
+
+ passes.forEach(function(mime) {
+ async_test(function(t) {
+ var img = document.createElement("img")
+ img.onerror = t.unreached_func("Unexpected error event")
+ img.onload = t.step_func_done(function(){
+ assert_equals(img.width, 96)
+ })
+ img.src = get_url(mime)
+ document.body.appendChild(img)
+ }, "CORB should allow the response if Content-Type is: '" + mime + "'. ")
+ })
+
+ fails.forEach(function(mime) {
+ async_test(function(t) {
+ var img = document.createElement("img")
+ img.onerror = t.step_func_done()
+ img.onload = t.unreached_func("Unexpected load event")
+ img.src = get_url(mime)
+ document.body.appendChild(img)
+ }, "CORB should block the response if Content-Type is: '" + mime + "'. ")
+ })
+</script>
diff --git a/test/wpt/tests/fetch/corb/img-png-mislabeled-as-html-nosniff.tentative.sub-ref.html b/test/wpt/tests/fetch/corb/img-png-mislabeled-as-html-nosniff.tentative.sub-ref.html
new file mode 100644
index 0000000..a771ed6
--- /dev/null
+++ b/test/wpt/tests/fetch/corb/img-png-mislabeled-as-html-nosniff.tentative.sub-ref.html
@@ -0,0 +1,4 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<!-- Same-origin, so the HTTP response is not CORB-eligible -->
+<img src="resources/empty-labeled-as-png.png">
diff --git a/test/wpt/tests/fetch/corb/img-png-mislabeled-as-html-nosniff.tentative.sub.html b/test/wpt/tests/fetch/corb/img-png-mislabeled-as-html-nosniff.tentative.sub.html
new file mode 100644
index 0000000..82adc47
--- /dev/null
+++ b/test/wpt/tests/fetch/corb/img-png-mislabeled-as-html-nosniff.tentative.sub.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<!-- Test verifies that CORB blocks an image mislabeled as text/html if
+ sniffing is disabled via `X-Content-Type-Options: nosniff` response header.
+ This has an observable effect (the image stops rendering), compared to the
+ behavior with no CORB.
+-->
+<meta charset="utf-8">
+<!-- Reference page uses same-origin resources, which are not CORB-eligible. -->
+<link rel="match" href="img-png-mislabeled-as-html-nosniff.tentative.sub-ref.html">
+<!-- www1 is cross-origin, so the HTTP response is CORB-eligible -->
+<img src="http://{{domains[www1]}}:{{ports[http][0]}}/fetch/corb/resources/png-mislabeled-as-html-nosniff.png">
diff --git a/test/wpt/tests/fetch/corb/img-png-mislabeled-as-html.sub-ref.html b/test/wpt/tests/fetch/corb/img-png-mislabeled-as-html.sub-ref.html
new file mode 100644
index 0000000..ebb337d
--- /dev/null
+++ b/test/wpt/tests/fetch/corb/img-png-mislabeled-as-html.sub-ref.html
@@ -0,0 +1,4 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<!-- Same-origin, so the HTTP response is not CORB-eligible -->
+<img src="resources/png-correctly-labeled.png">
diff --git a/test/wpt/tests/fetch/corb/img-png-mislabeled-as-html.sub.html b/test/wpt/tests/fetch/corb/img-png-mislabeled-as-html.sub.html
new file mode 100644
index 0000000..1ae4cfc
--- /dev/null
+++ b/test/wpt/tests/fetch/corb/img-png-mislabeled-as-html.sub.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<!-- Test verifies that CORB won't block an image after sniffing determines
+ that the text/html Content-Type response header doesn't match the response
+ body.
+-->
+<meta charset="utf-8">
+<!-- Reference page uses same-origin resources, which are not CORB-eligible. -->
+<link rel="match" href="img-png-mislabeled-as-html.sub-ref.html">
+<!-- www1 is cross-origin, so the HTTP response is CORB-eligible -->
+<img src="http://{{domains[www1]}}:{{ports[http][0]}}/fetch/corb/resources/png-mislabeled-as-html.png">
diff --git a/test/wpt/tests/fetch/corb/img-svg-doctype-html-mimetype-empty.sub.html b/test/wpt/tests/fetch/corb/img-svg-doctype-html-mimetype-empty.sub.html
new file mode 100644
index 0000000..3219fed
--- /dev/null
+++ b/test/wpt/tests/fetch/corb/img-svg-doctype-html-mimetype-empty.sub.html
@@ -0,0 +1,7 @@
+<!DOCTYPE html>
+<!-- Verifies CORB/ORB SVG image blocking.
+ This image has no MIME type and an html DOCTYPE declaration and is
+ expected to be blocked-->
+<meta charset="utf-8">
+<link rel="match" href="img-svg-invalid.sub-ref.html">
+<img src="http://{{domains[www1]}}:{{ports[http][0]}}/fetch/corb/resources/svg-doctype-html-mimetype-empty.svg">
diff --git a/test/wpt/tests/fetch/corb/img-svg-doctype-html-mimetype-svg.sub.html b/test/wpt/tests/fetch/corb/img-svg-doctype-html-mimetype-svg.sub.html
new file mode 100644
index 0000000..efcfaa2
--- /dev/null
+++ b/test/wpt/tests/fetch/corb/img-svg-doctype-html-mimetype-svg.sub.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<!-- Verifies CORB/ORB SVG image blocking.
+ This image has an SVG MIME type and an html DOCTYPE declaration and is
+ expected to load.
+
+ This testcase is distilled from a bugreport and real web page. See:
+ https://crbug.com/1359788
+-->
+<meta charset="utf-8">
+<link rel="match" href="img-svg.sub-ref.html">
+<img src="http://{{domains[www1]}}:{{ports[http][0]}}/fetch/corb/resources/svg-doctype-html-mimetype-svg.svg">
diff --git a/test/wpt/tests/fetch/corb/img-svg-invalid.sub-ref.html b/test/wpt/tests/fetch/corb/img-svg-invalid.sub-ref.html
new file mode 100644
index 0000000..484cd0a
--- /dev/null
+++ b/test/wpt/tests/fetch/corb/img-svg-invalid.sub-ref.html
@@ -0,0 +1,5 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<!-- Same-origin, but invalid URL.
+ Serves as reference for tests that expect the image to be blocked. -->
+<img src="resources/invalid.svg">
diff --git a/test/wpt/tests/fetch/corb/img-svg-labeled-as-dash.sub.html b/test/wpt/tests/fetch/corb/img-svg-labeled-as-dash.sub.html
new file mode 100644
index 0000000..0578b83
--- /dev/null
+++ b/test/wpt/tests/fetch/corb/img-svg-labeled-as-dash.sub.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<!-- Verifies CORB/ORB SVG image blocking.
+ This image is served with a DASH MIME type, and is expected to be blocked. -->
+<meta charset="utf-8">
+<link rel="match" href="img-svg-invalid.sub-ref.html">
+<img src="http://{{domains[www1]}}:{{ports[http][0]}}/fetch/corb/resources/svg-labeled-as-dash.svg">
diff --git a/test/wpt/tests/fetch/corb/img-svg-labeled-as-svg-xml.sub.html b/test/wpt/tests/fetch/corb/img-svg-labeled-as-svg-xml.sub.html
new file mode 100644
index 0000000..30a2eb3
--- /dev/null
+++ b/test/wpt/tests/fetch/corb/img-svg-labeled-as-svg-xml.sub.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<!-- Verifies CORB/ORB SVG image blocking.
+ This image is served with a proper SVG MIME type and is expected to load. -->
+<meta charset="utf-8">
+<link rel="match" href="img-svg.sub-ref.html">
+<img src="http://{{domains[www1]}}:{{ports[http][0]}}/fetch/corb/resources/svg-labeled-as-svg-xml.svg">
diff --git a/test/wpt/tests/fetch/corb/img-svg-xml-decl.sub.html b/test/wpt/tests/fetch/corb/img-svg-xml-decl.sub.html
new file mode 100644
index 0000000..0d3aeaf
--- /dev/null
+++ b/test/wpt/tests/fetch/corb/img-svg-xml-decl.sub.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<!-- Verifies CORB/ORB SVG image blocking.
+ This image has an XML declaration and is expected to load. -->
+<meta charset="utf-8">
+<link rel="match" href="img-svg.sub-ref.html">
+<img src="http://{{domains[www1]}}:{{ports[http][0]}}/fetch/corb/resources/svg-xml-decl.svg">
diff --git a/test/wpt/tests/fetch/corb/img-svg.sub-ref.html b/test/wpt/tests/fetch/corb/img-svg.sub-ref.html
new file mode 100644
index 0000000..5462f68
--- /dev/null
+++ b/test/wpt/tests/fetch/corb/img-svg.sub-ref.html
@@ -0,0 +1,5 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<!-- Same-origin, so the HTTP response is not CORB-eligible.
+ Serves as reference for cases the image is expected to be loaded. -->
+<img src="resources/svg.svg">
diff --git a/test/wpt/tests/fetch/corb/preload-image-png-mislabeled-as-html-nosniff.tentative.sub.html b/test/wpt/tests/fetch/corb/preload-image-png-mislabeled-as-html-nosniff.tentative.sub.html
new file mode 100644
index 0000000..cea80f2
--- /dev/null
+++ b/test/wpt/tests/fetch/corb/preload-image-png-mislabeled-as-html-nosniff.tentative.sub.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<!-- This test verifies observable CORB impact on <link rel="preload"> elements.
+-->
+<meta charset="utf-8">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id=log></div>
+
+<script>
+async_test(function(t) {
+ // With CORB the link.onerror event will be reached
+ // (because CORB will block the cross-origin preload).
+ window.preloadErrorEvent = t.step_func_done();
+
+ // Without CORB the link.onload event will be reached.
+ window.preloadLoadEvent = t.unreached_func("link/preload onload event reached.");
+});
+</script>
+
+<!-- www1 is cross-origin, so the HTTP response is CORB-eligible -->
+<link rel="preload" as="image"
+ onerror="window.preloadErrorEvent()"
+ onload="window.preloadLoadEvent()"
+ href="http://{{domains[www1]}}:{{ports[http][0]}}/fetch/corb/resources/png-mislabeled-as-html-nosniff.png">
diff --git a/test/wpt/tests/fetch/corb/resources/css-mislabeled-as-html-nosniff.css b/test/wpt/tests/fetch/corb/resources/css-mislabeled-as-html-nosniff.css
new file mode 100644
index 0000000..afd2b92
--- /dev/null
+++ b/test/wpt/tests/fetch/corb/resources/css-mislabeled-as-html-nosniff.css
@@ -0,0 +1 @@
+#header { color: red; }
diff --git a/test/wpt/tests/fetch/corb/resources/css-mislabeled-as-html-nosniff.css.headers b/test/wpt/tests/fetch/corb/resources/css-mislabeled-as-html-nosniff.css.headers
new file mode 100644
index 0000000..0f228f9
--- /dev/null
+++ b/test/wpt/tests/fetch/corb/resources/css-mislabeled-as-html-nosniff.css.headers
@@ -0,0 +1,2 @@
+Content-Type: text/html
+X-Content-Type-Options: nosniff
diff --git a/test/wpt/tests/fetch/corb/resources/css-mislabeled-as-html.css b/test/wpt/tests/fetch/corb/resources/css-mislabeled-as-html.css
new file mode 100644
index 0000000..afd2b92
--- /dev/null
+++ b/test/wpt/tests/fetch/corb/resources/css-mislabeled-as-html.css
@@ -0,0 +1 @@
+#header { color: red; }
diff --git a/test/wpt/tests/fetch/corb/resources/css-mislabeled-as-html.css.headers b/test/wpt/tests/fetch/corb/resources/css-mislabeled-as-html.css.headers
new file mode 100644
index 0000000..156209f
--- /dev/null
+++ b/test/wpt/tests/fetch/corb/resources/css-mislabeled-as-html.css.headers
@@ -0,0 +1 @@
+Content-Type: text/html
diff --git a/test/wpt/tests/fetch/corb/resources/css-with-json-parser-breaker.css b/test/wpt/tests/fetch/corb/resources/css-with-json-parser-breaker.css
new file mode 100644
index 0000000..7db6f5c
--- /dev/null
+++ b/test/wpt/tests/fetch/corb/resources/css-with-json-parser-breaker.css
@@ -0,0 +1,3 @@
+)]}'
+{}
+#header { color: red; }
diff --git a/test/wpt/tests/fetch/corb/resources/empty-labeled-as-png.png b/test/wpt/tests/fetch/corb/resources/empty-labeled-as-png.png
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/wpt/tests/fetch/corb/resources/empty-labeled-as-png.png
diff --git a/test/wpt/tests/fetch/corb/resources/empty-labeled-as-png.png.headers b/test/wpt/tests/fetch/corb/resources/empty-labeled-as-png.png.headers
new file mode 100644
index 0000000..e7be84a
--- /dev/null
+++ b/test/wpt/tests/fetch/corb/resources/empty-labeled-as-png.png.headers
@@ -0,0 +1 @@
+Content-Type: image/png
diff --git a/test/wpt/tests/fetch/corb/resources/html-correctly-labeled.html b/test/wpt/tests/fetch/corb/resources/html-correctly-labeled.html
new file mode 100644
index 0000000..7bad71b
--- /dev/null
+++ b/test/wpt/tests/fetch/corb/resources/html-correctly-labeled.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>Page Title</title>
+ </head>
+ <body>
+ <p>Page body</p>
+ </body>
+</html>
diff --git a/test/wpt/tests/fetch/corb/resources/html-correctly-labeled.html.headers b/test/wpt/tests/fetch/corb/resources/html-correctly-labeled.html.headers
new file mode 100644
index 0000000..156209f
--- /dev/null
+++ b/test/wpt/tests/fetch/corb/resources/html-correctly-labeled.html.headers
@@ -0,0 +1 @@
+Content-Type: text/html
diff --git a/test/wpt/tests/fetch/corb/resources/html-js-polyglot.js b/test/wpt/tests/fetch/corb/resources/html-js-polyglot.js
new file mode 100644
index 0000000..db45bb4
--- /dev/null
+++ b/test/wpt/tests/fetch/corb/resources/html-js-polyglot.js
@@ -0,0 +1,9 @@
+<!--/*--><html><body><script type="text/javascript"><!--//*/
+
+// This is a regression test for https://crbug.com/839425
+// which found out that some script resources are served
+// with text/html content-type and with a body that is
+// both a valid html and a valid javascript.
+window['html-js-polyglot.js'] = true;
+
+//--></script></body></html>
diff --git a/test/wpt/tests/fetch/corb/resources/html-js-polyglot.js.headers b/test/wpt/tests/fetch/corb/resources/html-js-polyglot.js.headers
new file mode 100644
index 0000000..156209f
--- /dev/null
+++ b/test/wpt/tests/fetch/corb/resources/html-js-polyglot.js.headers
@@ -0,0 +1 @@
+Content-Type: text/html
diff --git a/test/wpt/tests/fetch/corb/resources/html-js-polyglot2.js b/test/wpt/tests/fetch/corb/resources/html-js-polyglot2.js
new file mode 100644
index 0000000..faae1b7
--- /dev/null
+++ b/test/wpt/tests/fetch/corb/resources/html-js-polyglot2.js
@@ -0,0 +1,10 @@
+<!-- comment --> <script type='text/javascript'>
+//<![CDATA[
+
+// This is a regression test for https://crbug.com/839945
+// which found out that some script resources are served
+// with text/html content-type and with a body that is
+// both a valid html and a valid javascript.
+window['html-js-polyglot2.js'] = true;
+
+//]]>--></script>
diff --git a/test/wpt/tests/fetch/corb/resources/html-js-polyglot2.js.headers b/test/wpt/tests/fetch/corb/resources/html-js-polyglot2.js.headers
new file mode 100644
index 0000000..156209f
--- /dev/null
+++ b/test/wpt/tests/fetch/corb/resources/html-js-polyglot2.js.headers
@@ -0,0 +1 @@
+Content-Type: text/html
diff --git a/test/wpt/tests/fetch/corb/resources/js-mislabeled-as-html-nosniff.js b/test/wpt/tests/fetch/corb/resources/js-mislabeled-as-html-nosniff.js
new file mode 100644
index 0000000..a880a5b
--- /dev/null
+++ b/test/wpt/tests/fetch/corb/resources/js-mislabeled-as-html-nosniff.js
@@ -0,0 +1 @@
+window.has_executed_script = true;
diff --git a/test/wpt/tests/fetch/corb/resources/js-mislabeled-as-html-nosniff.js.headers b/test/wpt/tests/fetch/corb/resources/js-mislabeled-as-html-nosniff.js.headers
new file mode 100644
index 0000000..0f228f9
--- /dev/null
+++ b/test/wpt/tests/fetch/corb/resources/js-mislabeled-as-html-nosniff.js.headers
@@ -0,0 +1,2 @@
+Content-Type: text/html
+X-Content-Type-Options: nosniff
diff --git a/test/wpt/tests/fetch/corb/resources/js-mislabeled-as-html.js b/test/wpt/tests/fetch/corb/resources/js-mislabeled-as-html.js
new file mode 100644
index 0000000..a880a5b
--- /dev/null
+++ b/test/wpt/tests/fetch/corb/resources/js-mislabeled-as-html.js
@@ -0,0 +1 @@
+window.has_executed_script = true;
diff --git a/test/wpt/tests/fetch/corb/resources/js-mislabeled-as-html.js.headers b/test/wpt/tests/fetch/corb/resources/js-mislabeled-as-html.js.headers
new file mode 100644
index 0000000..156209f
--- /dev/null
+++ b/test/wpt/tests/fetch/corb/resources/js-mislabeled-as-html.js.headers
@@ -0,0 +1 @@
+Content-Type: text/html
diff --git a/test/wpt/tests/fetch/corb/resources/png-correctly-labeled.png b/test/wpt/tests/fetch/corb/resources/png-correctly-labeled.png
new file mode 100644
index 0000000..820f8ca
--- /dev/null
+++ b/test/wpt/tests/fetch/corb/resources/png-correctly-labeled.png
Binary files differ
diff --git a/test/wpt/tests/fetch/corb/resources/png-correctly-labeled.png.headers b/test/wpt/tests/fetch/corb/resources/png-correctly-labeled.png.headers
new file mode 100644
index 0000000..e7be84a
--- /dev/null
+++ b/test/wpt/tests/fetch/corb/resources/png-correctly-labeled.png.headers
@@ -0,0 +1 @@
+Content-Type: image/png
diff --git a/test/wpt/tests/fetch/corb/resources/png-mislabeled-as-html-nosniff.png b/test/wpt/tests/fetch/corb/resources/png-mislabeled-as-html-nosniff.png
new file mode 100644
index 0000000..820f8ca
--- /dev/null
+++ b/test/wpt/tests/fetch/corb/resources/png-mislabeled-as-html-nosniff.png
Binary files differ
diff --git a/test/wpt/tests/fetch/corb/resources/png-mislabeled-as-html-nosniff.png.headers b/test/wpt/tests/fetch/corb/resources/png-mislabeled-as-html-nosniff.png.headers
new file mode 100644
index 0000000..0f228f9
--- /dev/null
+++ b/test/wpt/tests/fetch/corb/resources/png-mislabeled-as-html-nosniff.png.headers
@@ -0,0 +1,2 @@
+Content-Type: text/html
+X-Content-Type-Options: nosniff
diff --git a/test/wpt/tests/fetch/corb/resources/png-mislabeled-as-html.png b/test/wpt/tests/fetch/corb/resources/png-mislabeled-as-html.png
new file mode 100644
index 0000000..820f8ca
--- /dev/null
+++ b/test/wpt/tests/fetch/corb/resources/png-mislabeled-as-html.png
Binary files differ
diff --git a/test/wpt/tests/fetch/corb/resources/png-mislabeled-as-html.png.headers b/test/wpt/tests/fetch/corb/resources/png-mislabeled-as-html.png.headers
new file mode 100644
index 0000000..156209f
--- /dev/null
+++ b/test/wpt/tests/fetch/corb/resources/png-mislabeled-as-html.png.headers
@@ -0,0 +1 @@
+Content-Type: text/html
diff --git a/test/wpt/tests/fetch/corb/resources/response_block_probe.js b/test/wpt/tests/fetch/corb/resources/response_block_probe.js
new file mode 100644
index 0000000..9c3b87b
--- /dev/null
+++ b/test/wpt/tests/fetch/corb/resources/response_block_probe.js
@@ -0,0 +1 @@
+alert(1); // Arbitrary JavaScript. Details don't matter for the test.
diff --git a/test/wpt/tests/fetch/corb/resources/response_block_probe.js.headers b/test/wpt/tests/fetch/corb/resources/response_block_probe.js.headers
new file mode 100644
index 0000000..0d848b0
--- /dev/null
+++ b/test/wpt/tests/fetch/corb/resources/response_block_probe.js.headers
@@ -0,0 +1 @@
+Content-Type: text/csv
diff --git a/test/wpt/tests/fetch/corb/resources/sniffable-resource.py b/test/wpt/tests/fetch/corb/resources/sniffable-resource.py
new file mode 100644
index 0000000..f815093
--- /dev/null
+++ b/test/wpt/tests/fetch/corb/resources/sniffable-resource.py
@@ -0,0 +1,11 @@
+def main(request, response):
+ body = request.GET.first(b"body", None)
+ type = request.GET.first(b"type", None)
+
+ response.add_required_headers = False
+ response.writer.write_status(200)
+ response.writer.write_header(b"content-length", len(body))
+ response.writer.write_header(b"content-type", type)
+ response.writer.end_headers()
+
+ response.writer.write(body)
diff --git a/test/wpt/tests/fetch/corb/resources/subframe-that-posts-html-containing-blob-url-to-parent.html b/test/wpt/tests/fetch/corb/resources/subframe-that-posts-html-containing-blob-url-to-parent.html
new file mode 100644
index 0000000..67b3ad5
--- /dev/null
+++ b/test/wpt/tests/fetch/corb/resources/subframe-that-posts-html-containing-blob-url-to-parent.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<script>
+fetch('html-correctly-labeled.html')
+ .then(response => response.blob())
+ .then(blob => {
+ let msg = { blob_size: blob.size,
+ blob_type: blob.type,
+ blob_url: URL.createObjectURL(blob) };
+ window.parent.postMessage(msg, '*');
+ })
+ .catch(error => {
+ let msg = { error: error };
+ window.parent.postMessage(msg, '*');
+ });
+</script>
diff --git a/test/wpt/tests/fetch/corb/resources/svg-doctype-html-mimetype-empty.svg b/test/wpt/tests/fetch/corb/resources/svg-doctype-html-mimetype-empty.svg
new file mode 100644
index 0000000..fa2d29b
--- /dev/null
+++ b/test/wpt/tests/fetch/corb/resources/svg-doctype-html-mimetype-empty.svg
@@ -0,0 +1,4 @@
+<!DOCTYPE html>
+<svg width="920" height="625" xmlns="http://www.w3.org/2000/svg" role="img">
+<rect width="100" height="100" style="fill:rgb(255,0,0)"/>
+</svg>
diff --git a/test/wpt/tests/fetch/corb/resources/svg-doctype-html-mimetype-empty.svg.headers b/test/wpt/tests/fetch/corb/resources/svg-doctype-html-mimetype-empty.svg.headers
new file mode 100644
index 0000000..29515ee
--- /dev/null
+++ b/test/wpt/tests/fetch/corb/resources/svg-doctype-html-mimetype-empty.svg.headers
@@ -0,0 +1 @@
+Content-Type:
diff --git a/test/wpt/tests/fetch/corb/resources/svg-doctype-html-mimetype-svg.svg b/test/wpt/tests/fetch/corb/resources/svg-doctype-html-mimetype-svg.svg
new file mode 100644
index 0000000..fa2d29b
--- /dev/null
+++ b/test/wpt/tests/fetch/corb/resources/svg-doctype-html-mimetype-svg.svg
@@ -0,0 +1,4 @@
+<!DOCTYPE html>
+<svg width="920" height="625" xmlns="http://www.w3.org/2000/svg" role="img">
+<rect width="100" height="100" style="fill:rgb(255,0,0)"/>
+</svg>
diff --git a/test/wpt/tests/fetch/corb/resources/svg-doctype-html-mimetype-svg.svg.headers b/test/wpt/tests/fetch/corb/resources/svg-doctype-html-mimetype-svg.svg.headers
new file mode 100644
index 0000000..070de35
--- /dev/null
+++ b/test/wpt/tests/fetch/corb/resources/svg-doctype-html-mimetype-svg.svg.headers
@@ -0,0 +1 @@
+Content-Type: image/svg+xml
diff --git a/test/wpt/tests/fetch/corb/resources/svg-labeled-as-dash.svg b/test/wpt/tests/fetch/corb/resources/svg-labeled-as-dash.svg
new file mode 100644
index 0000000..2b7d101
--- /dev/null
+++ b/test/wpt/tests/fetch/corb/resources/svg-labeled-as-dash.svg
@@ -0,0 +1,3 @@
+<svg width="920" height="625" xmlns="http://www.w3.org/2000/svg" role="img">
+<rect width="100" height="100" style="fill:rgb(255,0,0)"/>
+</svg>
diff --git a/test/wpt/tests/fetch/corb/resources/svg-labeled-as-dash.svg.headers b/test/wpt/tests/fetch/corb/resources/svg-labeled-as-dash.svg.headers
new file mode 100644
index 0000000..43ce612
--- /dev/null
+++ b/test/wpt/tests/fetch/corb/resources/svg-labeled-as-dash.svg.headers
@@ -0,0 +1 @@
+Content-Type: application/dash+xml
diff --git a/test/wpt/tests/fetch/corb/resources/svg-labeled-as-svg-xml.svg b/test/wpt/tests/fetch/corb/resources/svg-labeled-as-svg-xml.svg
new file mode 100644
index 0000000..2b7d101
--- /dev/null
+++ b/test/wpt/tests/fetch/corb/resources/svg-labeled-as-svg-xml.svg
@@ -0,0 +1,3 @@
+<svg width="920" height="625" xmlns="http://www.w3.org/2000/svg" role="img">
+<rect width="100" height="100" style="fill:rgb(255,0,0)"/>
+</svg>
diff --git a/test/wpt/tests/fetch/corb/resources/svg-labeled-as-svg-xml.svg.headers b/test/wpt/tests/fetch/corb/resources/svg-labeled-as-svg-xml.svg.headers
new file mode 100644
index 0000000..070de35
--- /dev/null
+++ b/test/wpt/tests/fetch/corb/resources/svg-labeled-as-svg-xml.svg.headers
@@ -0,0 +1 @@
+Content-Type: image/svg+xml
diff --git a/test/wpt/tests/fetch/corb/resources/svg-xml-decl.svg b/test/wpt/tests/fetch/corb/resources/svg-xml-decl.svg
new file mode 100644
index 0000000..3b39aff
--- /dev/null
+++ b/test/wpt/tests/fetch/corb/resources/svg-xml-decl.svg
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<svg width="920" height="625" xmlns="http://www.w3.org/2000/svg" role="img">
+<rect width="100" height="100" style="fill:rgb(255,0,0)"/>
+</svg>
diff --git a/test/wpt/tests/fetch/corb/resources/svg.svg b/test/wpt/tests/fetch/corb/resources/svg.svg
new file mode 100644
index 0000000..2b7d101
--- /dev/null
+++ b/test/wpt/tests/fetch/corb/resources/svg.svg
@@ -0,0 +1,3 @@
+<svg width="920" height="625" xmlns="http://www.w3.org/2000/svg" role="img">
+<rect width="100" height="100" style="fill:rgb(255,0,0)"/>
+</svg>
diff --git a/test/wpt/tests/fetch/corb/resources/svg.svg.headers b/test/wpt/tests/fetch/corb/resources/svg.svg.headers
new file mode 100644
index 0000000..070de35
--- /dev/null
+++ b/test/wpt/tests/fetch/corb/resources/svg.svg.headers
@@ -0,0 +1 @@
+Content-Type: image/svg+xml
diff --git a/test/wpt/tests/fetch/corb/response_block.tentative.https.html b/test/wpt/tests/fetch/corb/response_block.tentative.https.html
new file mode 100644
index 0000000..6b11600
--- /dev/null
+++ b/test/wpt/tests/fetch/corb/response_block.tentative.https.html
@@ -0,0 +1,50 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<body>
+<script>
+// A cross-origin response containing JavaScript, labelled as text/csv.
+const probeUrl = get_host_info().HTTPS_REMOTE_ORIGIN +
+ "/fetch/corb/resources/response_block_probe.js";
+
+// Test handling of blocked responses in CORB/ORB for <script> elements.
+function probe_script() {
+ // We will cross-origin load a script resource that should get blocked by all
+ // versions of CORB/ORB. Two things may happen:
+ //
+ // 1, An empty response is injected. (What CORB does.)
+ // 2, An error is injected and script loading aborts. (What ORB does.)
+
+ // Load the probe as a script.
+ const script = document.createElement("script");
+ script.src = probeUrl;
+ document.body.appendChild(script);
+
+ // Return a promise that will return a string description corresponding to the
+ // conditions above.
+ return new Promise((resolve, reject) => {
+ script.onload = _ => resolve("Resource loaded (expected for CORB)");
+ script.onerror = _ => resolve("ORB-style network error");
+ });
+}
+
+// Test handling of blocked responses in CORB/ORB for script-initiated fetches.
+function probe_fetch() {
+ return fetch(probeUrl, {mode: "no-cors"})
+ .then(response => response.text())
+ .then(text => {
+ assert_equals(text, "");
+ return "Resource loaded (expected for CORB)";
+ })
+ .catch(_ => "ORB-style network error");
+}
+
+// These tests check for ORB behaviour.
+promise_test(t => probe_script().then(
+ value => assert_equals(value, "ORB-style network error")),
+ "ORB: Expect error response from <script> fetch.");
+promise_test(t => probe_fetch().then(
+ value => assert_equals(value, "ORB-style network error")),
+ "ORB: Expect error response from fetch().");
+</script>
diff --git a/test/wpt/tests/fetch/corb/script-html-correctly-labeled.tentative.sub.html b/test/wpt/tests/fetch/corb/script-html-correctly-labeled.tentative.sub.html
new file mode 100644
index 0000000..6d1947c
--- /dev/null
+++ b/test/wpt/tests/fetch/corb/script-html-correctly-labeled.tentative.sub.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<!-- Test verifies that html fed to a <script> tag won't report a syntax
+ error after CORB blocks the response (an empty response body injected
+ by CORB won't have any JavaScript syntax errors).
+-->
+<meta charset="utf-8">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id=log></div>
+<script>
+setup({allow_uncaught_exception : true});
+async_test(function(t) {
+ var script = document.createElement("script")
+
+ // Without CORB, the html document would cause a syntax error when parsed as
+ // JavaScript, but with CORB there should be no errors (because CORB will
+ // replace the response body with an empty body). With ORB, the script loading
+ // itself will error out.
+ script.onload = t.step_func_done();
+ script.onerror = t.step_func_done();
+ addEventListener("error",function(e) {
+ t.step(function() {
+ assert_unreached("Empty body of a CORB-blocked response shouldn't trigger syntax errors.");
+ t.done();
+ })
+ });
+
+ // www1 is cross-origin, so the HTTP response is CORB-eligible.
+ script.src = 'http://{{domains[www1]}}:{{ports[http][0]}}/fetch/corb/resources/html-correctly-labeled.html';
+ document.body.appendChild(script)
+}, "CORB-blocked script has no syntax errors");
+</script>
diff --git a/test/wpt/tests/fetch/corb/script-html-js-polyglot.sub.html b/test/wpt/tests/fetch/corb/script-html-js-polyglot.sub.html
new file mode 100644
index 0000000..9a272d6
--- /dev/null
+++ b/test/wpt/tests/fetch/corb/script-html-js-polyglot.sub.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<!-- Test verifies that CORB won't block a polyglot script that is
+ both a valid HTML document and also valid Javascript.
+-->
+<meta charset="utf-8">
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<div id=log></div>
+<script>
+["html-js-polyglot.js", "html-js-polyglot2.js"].forEach(polyglot_name => {
+ async_test(function(t) {
+ window[polyglot_name] = false;
+ var script = document.createElement("script");
+
+ script.onload = t.step_func_done(function(){
+ // Verify that the script response wasn't blocked - that script
+ // should have set window[polyglot_name] to true.
+ assert_true(window[polyglot_name]);
+ })
+ addEventListener("error",function(e) {
+ t.step(function() {
+ assert_unreached("No errors are expected with or without CORB.");
+ t.done();
+ })
+ });
+
+ // www1 is cross-origin, so the HTTP response is CORB-eligible.
+ script.src = "http://{{domains[www1]}}:{{ports[http][0]}}/fetch/corb/resources/" + polyglot_name;
+ document.body.appendChild(script);
+ }, "CORB cannot block polyglot HTML/JS: " + polyglot_name);
+});
+</script>
diff --git a/test/wpt/tests/fetch/corb/script-html-via-cross-origin-blob-url.sub.html b/test/wpt/tests/fetch/corb/script-html-via-cross-origin-blob-url.sub.html
new file mode 100644
index 0000000..c8a90c7
--- /dev/null
+++ b/test/wpt/tests/fetch/corb/script-html-via-cross-origin-blob-url.sub.html
@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<!-- Test verifies that cross-origin blob URIs are blocked both with and
+ without CORB.
+-->
+<meta charset="utf-8">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id=log></div>
+<script>
+async_test(function(t) {
+ function step1_createSubframe() {
+ addEventListener("message", function(e) {
+ t.step(function() { step2_processSubframeMsg(e.data); })
+ });
+ var subframe = document.createElement("iframe")
+ // www1 is cross-origin, to ensure that the received blob will be cross-origin.
+ subframe.src = 'http://{{domains[www1]}}:{{ports[http][0]}}/fetch/corb/resources/subframe-that-posts-html-containing-blob-url-to-parent.html';
+ document.body.appendChild(subframe);
+ }
+
+ function step2_processSubframeMsg(msg) {
+ assert_false(msg.hasOwnProperty('error'), 'unexpected property found: "error"');
+ assert_equals(msg.blob_type, 'text/html');
+ assert_equals(msg.blob_size, 147);
+
+ // With and without CORB loading of a cross-origin blob should be blocked
+ // (this is verified by expecting |script.onerror|, but not |script.onload|
+ // below).
+ var script = document.createElement("script")
+ script.src = msg.blob_url;
+ script.onerror = t.step_func_done(function(){})
+ script.onload = t.unreached_func("Unexpected load event")
+ document.body.appendChild(script)
+ }
+
+ step1_createSubframe();
+});
+</script>
diff --git a/test/wpt/tests/fetch/corb/script-js-mislabeled-as-html-nosniff.sub.html b/test/wpt/tests/fetch/corb/script-js-mislabeled-as-html-nosniff.sub.html
new file mode 100644
index 0000000..b6bc909
--- /dev/null
+++ b/test/wpt/tests/fetch/corb/script-js-mislabeled-as-html-nosniff.sub.html
@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<!-- Test verifies that script mislabeled as html won't execute with and without CORB
+ if the nosniff response header is present.
+
+ The expected behavior is covered by the Fetch spec at
+ https://fetch.spec.whatwg.org/#should-response-to-request-be-blocked-due-to-nosniff?
+
+ See also the following tests:
+ - fetch/nosniff/importscripts.html
+ - fetch/nosniff/script.html
+ - fetch/nosniff/worker.html
+-->
+<meta charset="utf-8">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id=log></div>
+
+<script>
+setup({ single_test: true });
+window.has_executed_script = false;
+</script>
+
+<!-- www1 is cross-origin, so the HTTP response is CORB-eligible -->
+<script src="http://{{domains[www1]}}:{{ports[http][0]}}/fetch/corb/resources/js-mislabeled-as-html-nosniff.js">
+</script>
+
+<script>
+// Verify what observable effects the <script> tag above had.
+// Assertion should hold with and without CORB:
+assert_false(window.has_executed_script,
+ 'The cross-origin script should not be executed');
+done();
+</script>
diff --git a/test/wpt/tests/fetch/corb/script-js-mislabeled-as-html.sub.html b/test/wpt/tests/fetch/corb/script-js-mislabeled-as-html.sub.html
new file mode 100644
index 0000000..44cb1f8
--- /dev/null
+++ b/test/wpt/tests/fetch/corb/script-js-mislabeled-as-html.sub.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<!-- Test verifies that script mislabeled as html will execute with and without
+ CORB (CORB should allow the script after sniffing).
+-->
+<meta charset="utf-8">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id=log></div>
+
+<script>
+setup({ single_test: true });
+window.has_executed_script = false;
+</script>
+
+<!-- www1 is cross-origin, so the HTTP response is CORB-eligible -->
+<script src="http://{{domains[www1]}}:{{ports[http][0]}}/fetch/corb/resources/js-mislabeled-as-html.js">
+</script>
+
+<script>
+// Verify what observable effects the <script> tag above had.
+// Assertion should hold with and without CORB:
+assert_true(window.has_executed_script,
+ 'The cross-origin script should execute');
+done();
+</script>
diff --git a/test/wpt/tests/fetch/corb/script-resource-with-json-parser-breaker.tentative.sub.html b/test/wpt/tests/fetch/corb/script-resource-with-json-parser-breaker.tentative.sub.html
new file mode 100644
index 0000000..f0eb1f0
--- /dev/null
+++ b/test/wpt/tests/fetch/corb/script-resource-with-json-parser-breaker.tentative.sub.html
@@ -0,0 +1,85 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<!-- Test verifies CORB will block responses beginning with a JSON parser
+ breaker regardless of their MIME type (excluding text/css - see below).
+
+ A JSON parser breaker is a prefix added to resources with sensitive data to
+ prevent cross-site script inclusion (XSSI) and similar attacks. For example,
+ it may be included in JSON files to prevent them from leaking data via a
+ <script> tag, making the response only useful to a fetch or XmlHttpRequest.
+ See also https://chromium.googlesource.com/chromium/src/+/main/services/network/cross_origin_read_blocking_explainer.md#Protecting-JSON
+
+ The assumption is that all images, other media, scripts, fonts and other
+ resources that may be embedded cross-origin will never begin with a JSON
+ parser breaker. For example an JPEG image should always being with FF D8 FF,
+ a PNG image with 89 50 4E 47 0D 0A 1A 0A bytes and an SVG image with "<?xml"
+ substring.
+
+ The assumption above excludes text/css which (as shown by
+ style-css-with-json-parser-breaker.sub.html) can parse as valid stylesheet
+ even in presence of a JSON parser breaker.
+-->
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id=log></div>
+<script>
+setup({allow_uncaught_exception : true});
+
+// A subset of JSON security prefixes (only ones that are parser breakers).
+json_parser_breakers = [
+ ")]}'",
+ "{}&&",
+ "{} &&",
+]
+
+// JSON parser breaker should trigger CORB blocking for any Content-Type - even
+// for resources that claim to be of a MIME type that is normally allowed to be
+// embedded in cross-origin documents (like images and/or scripts).
+mime_types = [
+ // CORB-protected MIME types
+ "text/html",
+ "text/xml",
+ "text/json",
+ "text/plain",
+
+ // MIME types that normally are allowed by CORB.
+ "application/javascript",
+ "image/png",
+ "image/svg+xml",
+
+ // Other types.
+ "application/pdf",
+ "application/zip",
+]
+
+function test(mime_type, body) {
+ // The test below depends on a global/shared event handler - we need to ensure
+ // that no tests run in parallel - this is achieved by using `promise_test`
+ // instead of `async_test`. See also
+ // https://web-platform-tests.org/writing-tests/testharness-api.html#promise-tests
+ promise_test(t => new Promise(function(resolve, reject) {
+ var script = document.createElement("script")
+
+ // Without CORB, the JSON parser breaker would cause a syntax error when
+ // parsed as JavaScript, but with CORB there should be no errors (because
+ // CORB will replace the response body with an empty body). With ORB,
+ // the script loading itself should error out.
+ script.onload = resolve;
+ script.onerror = resolve;
+ addEventListener("error", t.unreached_func(
+ "Empty body of a CORS-blocked response shouldn't trigger syntax errors."))
+
+ // www1 is cross-origin, so the HTTP response is CORB-eligible.
+ var src_prefix = "http://{{domains[www1]}}:{{ports[http][0]}}/fetch/corb/resources/sniffable-resource.py";
+ script.src = src_prefix + "?type=" + mime_type + "&body=" + encodeURIComponent(body);
+ document.body.appendChild(script)
+ }), "CORB-blocks '" + mime_type + "' that starts with the following JSON parser breaker: " + body);
+}
+
+mime_types.forEach(function(type) {
+ json_parser_breakers.forEach(function(body) {
+ test(type, body);
+ });
+});
+
+</script>
diff --git a/test/wpt/tests/fetch/corb/script-resource-with-nonsniffable-types.tentative.sub.html b/test/wpt/tests/fetch/corb/script-resource-with-nonsniffable-types.tentative.sub.html
new file mode 100644
index 0000000..6d490d5
--- /dev/null
+++ b/test/wpt/tests/fetch/corb/script-resource-with-nonsniffable-types.tentative.sub.html
@@ -0,0 +1,84 @@
+<!DOCTYPE html>
+<!-- Test verifies CORB will block responses with types that do not
+ require confirmation sniffing.
+
+ We assume that:
+ 1) it is unlikely that images, other media, scripts, etc. will be mislabelled
+ as the |protected_mime_types| below,
+ 2) the |protected_mime_types| below are likely to contain sensitive,
+ credentialled data.
+-->
+<meta charset="utf-8">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<div id=log></div>
+<script>
+setup({allow_uncaught_exception : true, single_test : true});
+
+function test(mime_type, is_blocking_expected) {
+ var action = is_blocking_expected ? "blocks" : "does not block";
+
+ async_test(function(t) {
+ var script = document.createElement("script")
+ var script_has_run_token = "script_has_run" + token();
+
+ // With and without CORB there should be no error, but without CORB the
+ // original script body will be preserved and |window.script_has_run| will
+ // be set.
+ window[script_has_run_token] = false;
+ script.onload = t.step_func_done(function(){
+ if (is_blocking_expected) {
+ assert_false(window[script_has_run_token]);
+ } else {
+ assert_true(window[script_has_run_token]);
+ }
+ });
+ addEventListener("error",function(e) {
+ t.step(function() {
+ assert_unreached("Unexpected error: " + e);
+ t.done();
+ })
+ });
+
+ // www1 is cross-origin, so the HTTP response is CORB-eligible.
+ var src_prefix = "http://{{domains[www1]}}:{{ports[http][0]}}/fetch/corb/resources/sniffable-resource.py";
+ body = `window['${script_has_run_token}'] = true;`
+ script.src = src_prefix + "?type=" + mime_type + "&body=" + encodeURIComponent(body);
+ document.body.appendChild(script)
+ }, "CORB " + action + " '" + mime_type + "'");
+}
+
+// Some mime types should be protected by CORB without any kind
+// of confirmation sniffing.
+protected_mime_types = [
+ "application/gzip",
+ "application/pdf",
+ "application/x-gzip",
+ "application/x-protobuf",
+ "application/zip",
+ "multipart/byteranges",
+ "multipart/signed",
+ "text/csv",
+ "text/event-stream",
+]
+protected_mime_types.forEach(function(type) {
+ test(type, true /* is_blocking_expected */);
+});
+
+// Other mime types.
+other_mime_types = [
+ // These content types are legitimately allowed in 'no-cors' fetches.
+ "application/javascript",
+
+ // Confirmation sniffing will fail and prevent CORB from blocking the
+ // response.
+ "text/html",
+
+ // Unrecognized content types.
+ "application/blah"
+]
+other_mime_types.forEach(function(type) {
+ test(type, false /* is_blocking_expected */);
+});
+</script>
diff --git a/test/wpt/tests/fetch/corb/style-css-mislabeled-as-html-nosniff.sub.html b/test/wpt/tests/fetch/corb/style-css-mislabeled-as-html-nosniff.sub.html
new file mode 100644
index 0000000..8fef0dc
--- /dev/null
+++ b/test/wpt/tests/fetch/corb/style-css-mislabeled-as-html-nosniff.sub.html
@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<!-- Test verifies that a stylesheet mislabeled as html won't execute with and
+ without CORB if the nosniff response header is present.
+
+ The expected behavior is covered by the Fetch spec at
+ https://fetch.spec.whatwg.org/#should-response-to-request-be-blocked-due-to-nosniff?
+
+ See also the following tests:
+ - fetch/nosniff/stylesheet.html
+-->
+<meta charset="utf-8">
+<title>CSS is not applied (because of nosniff + non-text/css headers)</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+
+<!-- Default style that will be applied if the external stylesheet resource
+ below won't load for any reason. This stylesheet will set h1's
+ color to green (see |default_color| below). -->
+<style>
+h1 { color: green; }
+</style>
+
+<!-- This stylesheet (if loaded) should set h1#header's color to red
+ (see |external_color| below). -->
+<!-- www1 is cross-origin, so the HTTP response is CORB-eligible -->
+<link rel="stylesheet" type="text/css"
+ href="http://{{domains[www1]}}:{{ports[http][0]}}/fetch/corb/resources/css-mislabeled-as-html-nosniff.css">
+
+<body>
+ <h1 id="header">Header example</h1>
+ <p>Paragraph body</p>
+</body>
+
+<script>
+test(() => {
+ let style = getComputedStyle(document.getElementById('header'));
+ const external_color = 'rgb(255, 0, 0)'; // red
+ const default_color = 'rgb(0, 128, 0)'; // green
+ assert_equals(style.getPropertyValue('color'), default_color);
+ assert_not_equals(style.getPropertyValue('color'), external_color);
+});
+</script>
diff --git a/test/wpt/tests/fetch/corb/style-css-mislabeled-as-html.sub.html b/test/wpt/tests/fetch/corb/style-css-mislabeled-as-html.sub.html
new file mode 100644
index 0000000..4f0b4c2
--- /dev/null
+++ b/test/wpt/tests/fetch/corb/style-css-mislabeled-as-html.sub.html
@@ -0,0 +1,36 @@
+<!DOCTYPE html>
+<!-- Test verifies that CORB won't impact a cross-origin stylesheet mislabeled
+ as text/html (because even without CORB mislabeled CSS will be rejected).
+-->
+<meta charset="utf-8">
+<title>CSS is not applied (because of strict content-type enforcement for cross-origin stylesheets)</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+
+<!-- Default style that will be applied if the external stylesheet resource
+ below won't load for any reason. This stylesheet will set h1's
+ color to green (see |default_color| below). -->
+<style>
+h1 { color: green; }
+</style>
+
+<!-- This stylesheet (if loaded) should set h1#header's color to red
+ (see |external_color| below). -->
+<!-- www1 is cross-origin, so the HTTP response is CORB-eligible -->
+<link rel="stylesheet" type="text/css"
+ href="http://{{domains[www1]}}:{{ports[http][0]}}/fetch/corb/resources/css-mislabeled-as-html.css">
+
+<body>
+ <h1 id="header">Header example</h1>
+ <p>Paragraph body</p>
+</body>
+
+<script>
+test(() => {
+ let style = getComputedStyle(document.getElementById('header'));
+ const external_color = 'rgb(255, 0, 0)'; // red
+ const default_color = 'rgb(0, 128, 0)'; // green
+ assert_equals(style.getPropertyValue('color'), default_color);
+ assert_not_equals(style.getPropertyValue('color'), external_color);
+});
+</script>
diff --git a/test/wpt/tests/fetch/corb/style-css-with-json-parser-breaker.sub.html b/test/wpt/tests/fetch/corb/style-css-with-json-parser-breaker.sub.html
new file mode 100644
index 0000000..29ed586
--- /dev/null
+++ b/test/wpt/tests/fetch/corb/style-css-with-json-parser-breaker.sub.html
@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<!-- Test verifies that CORB won't block a stylesheet that
+ 1) is correctly labeled with text/css Content-Type and parsing fine as text/css
+ 2) starts with a JSON parser breaker (like )]}')
+-->
+<meta charset="utf-8">
+<title>CORB doesn't block a stylesheet that has a proper Content-Type and begins with a JSON parser breaker</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+
+<!-- Default style that will be applied if the external stylesheet resource
+ below won't load for any reason. This stylesheet will set h1's
+ color to green (see |default_color| below). -->
+<style>
+h1 { color: green; }
+</style>
+
+<!-- This stylesheet (if loaded) should set h1#header's color to red
+ (see |external_color| below). -->
+<!-- www1 is cross-origin, so the HTTP response is CORB-eligible -->
+<link rel="stylesheet" type="text/css"
+ href="http://{{domains[www1]}}:{{ports[http][0]}}/fetch/corb/resources/css-with-json-parser-breaker.css">
+
+<body>
+ <h1 id="header">Header example</h1>
+ <p>Paragraph body</p>
+</body>
+
+<script>
+test(() => {
+ // Verify that CSS got applied / did not get blocked by CORB.
+ let style = getComputedStyle(document.getElementById('header'));
+ const external_color = 'rgb(255, 0, 0)'; // red
+ const default_color = 'rgb(0, 128, 0)'; // green
+ assert_equals(style.getPropertyValue('color'), external_color);
+ assert_not_equals(style.getPropertyValue('color'), default_color);
+});
+</script>
diff --git a/test/wpt/tests/fetch/corb/style-html-correctly-labeled.sub.html b/test/wpt/tests/fetch/corb/style-html-correctly-labeled.sub.html
new file mode 100644
index 0000000..cdefcd2
--- /dev/null
+++ b/test/wpt/tests/fetch/corb/style-html-correctly-labeled.sub.html
@@ -0,0 +1,41 @@
+<!DOCTYPE html>
+<!-- Test verifies that using a HTML document as a stylesheet has no observable
+ differences with and without CORB:
+ - The cross-origin stylesheet requires a correct text/css Content-Type
+ and therefore won't render even without CORB. This aspect of this test
+ is similar to the style-css-mislabeled-as-html.sub.html test.
+ - Even if the Content-Type requirements were relaxed for cross-origin stylesheets,
+ the HTML document is unlikely to parse as a stylesheet (unless a polyglot
+ HTML/CSS document is crafted as part of an attack) and therefore the
+ observable behavior should be indistinguishable from parsing the empty,
+ CORB-blocked response as a stylesheet.
+-->
+<meta charset="utf-8">
+<title>CSS is not applied (because of mismatched Content-Type header)</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+
+<!-- Default style that will be applied if the external stylesheet resource
+ below won't load for any reason. This stylesheet will set h1's
+ color to green (see |default_color| below). -->
+<style>
+h1 { color: green; }
+</style>
+
+<!-- This is not really a stylesheet... -->
+<!-- www1 is cross-origin, so the HTTP response is CORB-eligible -->
+<link rel="stylesheet" type="text/css"
+ href="http://{{domains[www1]}}:{{ports[http][0]}}/fetch/corb/resources/html-correctly-labeled.html">
+
+<body>
+ <h1 id="header">Header example</h1>
+ <p>Paragraph body</p>
+</body>
+
+<script>
+test(() => {
+ var style = getComputedStyle(document.getElementById('header'));
+ const default_color = 'rgb(0, 128, 0)'; // green
+ assert_equals(style.getPropertyValue('color'), default_color);
+});
+</script>
diff --git a/test/wpt/tests/fetch/cross-origin-resource-policy/fetch-in-iframe.html b/test/wpt/tests/fetch/cross-origin-resource-policy/fetch-in-iframe.html
new file mode 100644
index 0000000..cc6a3a8
--- /dev/null
+++ b/test/wpt/tests/fetch/cross-origin-resource-policy/fetch-in-iframe.html
@@ -0,0 +1,67 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/common/get-host-info.sub.js"></script>
+</head>
+<body>
+ <script>
+const host = get_host_info();
+const remoteBaseURL = host.HTTP_REMOTE_ORIGIN + window.location.pathname.replace(/\/[^\/]*$/, '/') ;
+const notSameSiteBaseURL = host.HTTP_NOTSAMESITE_ORIGIN + window.location.pathname.replace(/\/[^\/]*$/, '/') ;
+const localBaseURL = host.HTTP_ORIGIN + window.location.pathname.replace(/\/[^\/]*$/, '/') ;
+
+function with_iframe(url)
+{
+ return new Promise(function(resolve) {
+ var frame = document.createElement('iframe');
+ frame.src = url;
+ frame.onload = function() { resolve(frame); };
+ document.body.appendChild(frame);
+ });
+}
+
+function loadIFrameAndFetch(iframeURL, fetchURL, expectedFetchResult, title)
+{
+ promise_test(async () => {
+ const frame = await with_iframe(iframeURL);
+ let receiveMessage;
+ const promise = new Promise((resolve, reject) => {
+ receiveMessage = (event) => {
+ if (event.data !== expectedFetchResult) {
+ reject("Received unexpected message " + event.data);
+ return;
+ }
+ resolve();
+ }
+ window.addEventListener("message", receiveMessage, false);
+ });
+ frame.contentWindow.postMessage(fetchURL, "*");
+ return promise.finally(() => {
+ frame.remove();
+ window.removeEventListener("message", receiveMessage, false);
+ });
+ }, title);
+}
+
+// This above data URL should be equivalent to resources/iframeFetch.html
+var dataIFrameURL = "data:text/html;base64,PCFET0NUWVBFIGh0bWw+CjxodG1sPgo8aGVhZD4KICAgIDxzY3JpcHQ+CiAgICAgICAgZnVuY3Rpb24gcHJvY2Vzc01lc3NhZ2UoZXZlbnQpCiAgICAgICAgewogICAgICAgICAgICBmZXRjaChldmVudC5kYXRhLCB7IG1vZGU6ICJuby1jb3JzIiB9KS50aGVuKCgpID0+IHsKICAgICAgICAgICAgICAgIHBhcmVudC5wb3N0TWVzc2FnZSgib2siLCAiKiIpOwogICAgICAgICAgICB9LCAoKSA9PiB7CiAgICAgICAgICAgICAgICBwYXJlbnQucG9zdE1lc3NhZ2UoImtvIiwgIioiKTsKICAgICAgICAgICAgfSk7CiAgICAgICAgfQogICAgICAgIHdpbmRvdy5hZGRFdmVudExpc3RlbmVyKCJtZXNzYWdlIiwgcHJvY2Vzc01lc3NhZ2UsIGZhbHNlKTsKICAgIDwvc2NyaXB0Pgo8L2hlYWQ+Cjxib2R5PgogICAgPGgzPlRoZSBpZnJhbWUgbWFraW5nIGEgc2FtZSBvcmlnaW4gZmV0Y2ggY2FsbC48L2gzPgo8L2JvZHk+CjwvaHRtbD4K";
+
+loadIFrameAndFetch(dataIFrameURL, localBaseURL + "resources/hello.py?corp=same-origin", "ko",
+ "Cross-origin fetch in a data: iframe load fails if the server blocks cross-origin loads with a 'Cross-Origin-Resource-Policy: same-origin' response header.");
+
+loadIFrameAndFetch(dataIFrameURL, localBaseURL + "resources/hello.py?corp=same-site", "ko",
+ "Cross-origin fetch in a data: iframe load fails if the server blocks cross-origin loads with a 'Cross-Origin-Resource-Policy: same-site' response header.");
+
+loadIFrameAndFetch(remoteBaseURL + "resources/iframeFetch.html", localBaseURL + "resources/hello.py?corp=same-origin", "ko",
+ "Cross-origin fetch in a cross origin iframe load fails if the server blocks cross-origin loads with a 'Cross-Origin-Resource-Policy: same-origin' response header.");
+
+loadIFrameAndFetch(notSameSiteBaseURL + "resources/iframeFetch.html", localBaseURL + "resources/hello.py?corp=same-site", "ko",
+ "Cross-origin fetch in a cross origin iframe load fails if the server blocks cross-origin loads with a 'Cross-Origin-Resource-Policy: same-site' response header.");
+
+loadIFrameAndFetch(remoteBaseURL + "resources/iframeFetch.html", remoteBaseURL + "resources/hello.py?corp=same-origin", "ok",
+ "Same-origin fetch in a cross origin iframe load succeeds if the server blocks cross-origin loads with a 'Cross-Origin-Resource-Policy: same-origin' response header.");
+ </script>
+</body>
+</html>
diff --git a/test/wpt/tests/fetch/cross-origin-resource-policy/fetch.any.js b/test/wpt/tests/fetch/cross-origin-resource-policy/fetch.any.js
new file mode 100644
index 0000000..64a7bfe
--- /dev/null
+++ b/test/wpt/tests/fetch/cross-origin-resource-policy/fetch.any.js
@@ -0,0 +1,76 @@
+// META: timeout=long
+// META: global=window,dedicatedworker,sharedworker
+// META: script=/common/get-host-info.sub.js
+
+const host = get_host_info();
+const path = "/fetch/cross-origin-resource-policy/";
+const localBaseURL = host.HTTP_ORIGIN + path;
+const sameSiteBaseURL = "http://" + host.ORIGINAL_HOST + ":" + host.HTTP_PORT2 + path;
+const notSameSiteBaseURL = host.HTTP_NOTSAMESITE_ORIGIN + path;
+const httpsBaseURL = host.HTTPS_ORIGIN + path;
+
+promise_test(async () => {
+ const response = await fetch("./resources/hello.py?corp=same-origin");
+ assert_equals(await response.text(), "hello");
+}, "Same-origin fetch with a 'Cross-Origin-Resource-Policy: same-origin' response header.");
+
+promise_test(async () => {
+ const response = await fetch("./resources/hello.py?corp=same-site");
+ assert_equals(await response.text(), "hello");
+}, "Same-origin fetch with a 'Cross-Origin-Resource-Policy: same-site' response header.");
+
+promise_test(async (test) => {
+ const response = await fetch(notSameSiteBaseURL + "resources/hello.py?corp=same-origin");
+ assert_equals(await response.text(), "hello");
+}, "Cross-origin cors fetch with a 'Cross-Origin-Resource-Policy: same-origin' response header.");
+
+promise_test(async (test) => {
+ const response = await fetch(notSameSiteBaseURL + "resources/hello.py?corp=same-site");
+ assert_equals(await response.text(), "hello");
+}, "Cross-origin cors fetch with a 'Cross-Origin-Resource-Policy: same-site' response header.");
+
+promise_test((test) => {
+ const remoteURL = notSameSiteBaseURL + "resources/hello.py?corp=same-origin";
+ return promise_rejects_js(test, TypeError, fetch(remoteURL, { mode : "no-cors" }));
+}, "Cross-origin no-cors fetch with a 'Cross-Origin-Resource-Policy: same-origin' response header.");
+
+promise_test((test) => {
+ const remoteURL = notSameSiteBaseURL + "resources/hello.py?corp=same-site";
+ return promise_rejects_js(test, TypeError, fetch(remoteURL, { mode: "no-cors" }));
+}, "Cross-origin no-cors fetch with a 'Cross-Origin-Resource-Policy: same-site' response header.");
+
+promise_test((test) => {
+ const remoteURL = httpsBaseURL + "resources/hello.py?corp=same-site";
+ return promise_rejects_js(test, TypeError, fetch(remoteURL, { mode: "no-cors" }));
+}, "Cross-scheme (HTTP to HTTPS) no-cors fetch to a same-site URL with a 'Cross-Origin-Resource-Policy: same-site' response header.");
+
+promise_test((test) => {
+ const remoteURL = httpsBaseURL + "resources/hello.py?corp=same-origin";
+ return promise_rejects_js(test, TypeError, fetch(remoteURL, { mode : "no-cors" }));
+}, "Cross-origin no-cors fetch to a same-site URL with a 'Cross-Origin-Resource-Policy: same-origin' response header.");
+
+promise_test(async (test) => {
+ const remoteSameSiteURL = sameSiteBaseURL + "resources/hello.py?corp=same-site";
+
+ await fetch(remoteSameSiteURL, { mode: "no-cors" });
+
+ return promise_rejects_js(test, TypeError, fetch(sameSiteBaseURL + "resources/hello.py?corp=same-origin", { mode: "no-cors" }));
+}, "Valid cross-origin no-cors fetch with a 'Cross-Origin-Resource-Policy: same-site' response header.");
+
+promise_test((test) => {
+ const finalURL = notSameSiteBaseURL + "resources/hello.py?corp=same-origin";
+ return promise_rejects_js(test, TypeError, fetch("resources/redirect.py?redirectTo=" + encodeURIComponent(finalURL), { mode: "no-cors" }));
+}, "Cross-origin no-cors fetch with a 'Cross-Origin-Resource-Policy: same-origin' response header after a redirection.");
+
+promise_test((test) => {
+ const finalURL = localBaseURL + "resources/hello.py?corp=same-origin";
+ return fetch(notSameSiteBaseURL + "resources/redirect.py?redirectTo=" + encodeURIComponent(finalURL), { mode: "no-cors" });
+}, "Cross-origin no-cors fetch with a 'Cross-Origin-Resource-Policy: same-origin' response header after a cross-origin redirection.");
+
+promise_test(async (test) => {
+ const finalURL = localBaseURL + "resources/hello.py?corp=same-origin";
+
+ await fetch(finalURL, { mode: "no-cors" });
+
+ return promise_rejects_js(test, TypeError, fetch(notSameSiteBaseURL + "resources/redirect.py?corp=same-origin&redirectTo=" + encodeURIComponent(finalURL), { mode: "no-cors" }));
+}, "Cross-origin no-cors fetch with a 'Cross-Origin-Resource-Policy: same-origin' redirect response header.");
diff --git a/test/wpt/tests/fetch/cross-origin-resource-policy/fetch.https.any.js b/test/wpt/tests/fetch/cross-origin-resource-policy/fetch.https.any.js
new file mode 100644
index 0000000..c9b5b75
--- /dev/null
+++ b/test/wpt/tests/fetch/cross-origin-resource-policy/fetch.https.any.js
@@ -0,0 +1,56 @@
+// META: timeout=long
+// META: global=window,worker
+// META: script=/common/get-host-info.sub.js
+
+const host = get_host_info();
+const path = "/fetch/cross-origin-resource-policy/";
+const localBaseURL = host.HTTPS_ORIGIN + path;
+const notSameSiteBaseURL = host.HTTPS_NOTSAMESITE_ORIGIN + path;
+
+promise_test(async () => {
+ const response = await fetch("./resources/hello.py?corp=same-origin");
+ assert_equals(await response.text(), "hello");
+}, "Same-origin fetch with a 'Cross-Origin-Resource-Policy: same-origin' response header.");
+
+promise_test(async () => {
+ const response = await fetch("./resources/hello.py?corp=same-site");
+ assert_equals(await response.text(), "hello");
+}, "Same-origin fetch with a 'Cross-Origin-Resource-Policy: same-site' response header.");
+
+promise_test(async (test) => {
+ const response = await fetch(notSameSiteBaseURL + "resources/hello.py?corp=same-origin");
+ assert_equals(await response.text(), "hello");
+}, "Cross-origin cors fetch with a 'Cross-Origin-Resource-Policy: same-origin' response header.");
+
+promise_test(async (test) => {
+ const response = await fetch(notSameSiteBaseURL + "resources/hello.py?corp=same-site");
+ assert_equals(await response.text(), "hello");
+}, "Cross-origin cors fetch with a 'Cross-Origin-Resource-Policy: same-site' response header.");
+
+promise_test((test) => {
+ const remoteURL = notSameSiteBaseURL + "resources/hello.py?corp=same-origin";
+ return promise_rejects_js(test, TypeError, fetch(remoteURL, { mode : "no-cors" }));
+}, "Cross-origin no-cors fetch with a 'Cross-Origin-Resource-Policy: same-origin' response header.");
+
+promise_test((test) => {
+ const remoteURL = notSameSiteBaseURL + "resources/hello.py?corp=same-site";
+ return promise_rejects_js(test, TypeError, fetch(remoteURL, { mode: "no-cors" }));
+}, "Cross-origin no-cors fetch with a 'Cross-Origin-Resource-Policy: same-site' response header.");
+
+promise_test((test) => {
+ const finalURL = notSameSiteBaseURL + "resources/hello.py?corp=same-origin";
+ return promise_rejects_js(test, TypeError, fetch("resources/redirect.py?redirectTo=" + encodeURIComponent(finalURL), { mode: "no-cors" }));
+}, "Cross-origin no-cors fetch with a 'Cross-Origin-Resource-Policy: same-origin' response header after a redirection.");
+
+promise_test((test) => {
+ const finalURL = localBaseURL + "resources/hello.py?corp=same-origin";
+ return fetch(notSameSiteBaseURL + "resources/redirect.py?redirectTo=" + encodeURIComponent(finalURL), { mode: "no-cors" });
+}, "Cross-origin no-cors fetch with a 'Cross-Origin-Resource-Policy: same-origin' response header after a cross-origin redirection.");
+
+promise_test(async (test) => {
+ const finalURL = localBaseURL + "resources/hello.py?corp=same-origin";
+
+ await fetch(finalURL, { mode: "no-cors" });
+
+ return promise_rejects_js(test, TypeError, fetch(notSameSiteBaseURL + "resources/redirect.py?corp=same-origin&redirectTo=" + encodeURIComponent(finalURL), { mode: "no-cors" }));
+}, "Cross-origin no-cors fetch with a 'Cross-Origin-Resource-Policy: same-origin' redirect response header.");
diff --git a/test/wpt/tests/fetch/cross-origin-resource-policy/iframe-loads.html b/test/wpt/tests/fetch/cross-origin-resource-policy/iframe-loads.html
new file mode 100644
index 0000000..63902c3
--- /dev/null
+++ b/test/wpt/tests/fetch/cross-origin-resource-policy/iframe-loads.html
@@ -0,0 +1,46 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/common/get-host-info.sub.js"></script>
+</head>
+<body>
+ <script>
+const host = get_host_info();
+const remoteBaseURL = host.HTTP_REMOTE_ORIGIN + window.location.pathname.replace(/\/[^\/]*$/, '/') ;
+const localBaseURL = host.HTTP_ORIGIN + window.location.pathname.replace(/\/[^\/]*$/, '/') ;
+
+function with_iframe(url) {
+ return new Promise(function(resolve) {
+ var frame = document.createElement('iframe');
+ frame.src = url;
+ frame.onload = function() { resolve(frame); };
+ document.body.appendChild(frame);
+ });
+}
+
+promise_test(async() => {
+ const url = remoteBaseURL + "resources/iframe.py?corp=same-origin";
+
+ await new Promise((resolve, reject) => {
+ return fetch(url, { mode: "no-cors" }).then(reject, resolve);
+ });
+
+ const iframe = await with_iframe(url);
+ return new Promise((resolve, reject) => {
+ window.addEventListener("message", (event) => {
+ if (event.data !== "pong") {
+ reject(event.data);
+ return;
+ }
+ resolve();
+ }, false);
+ iframe.contentWindow.postMessage("ping", "*");
+ }).finally(() => {
+ iframe.remove();
+ });
+}, "Load an iframe that has Cross-Origin-Resource-Policy header");
+ </script>
+</body>
+</html>
diff --git a/test/wpt/tests/fetch/cross-origin-resource-policy/image-loads.html b/test/wpt/tests/fetch/cross-origin-resource-policy/image-loads.html
new file mode 100644
index 0000000..060b755
--- /dev/null
+++ b/test/wpt/tests/fetch/cross-origin-resource-policy/image-loads.html
@@ -0,0 +1,54 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/common/get-host-info.sub.js"></script>
+</head>
+<body>
+ <div id="testDiv"></div>
+ <script>
+const host = get_host_info();
+const notSameSiteBaseURL = host.HTTP_NOTSAMESITE_ORIGIN + window.location.pathname.replace(/\/[^\/]*$/, '/') ;
+const ok = true;
+const ko = false;
+const noCors = false;
+
+function loadImage(url, shoudLoad, corsMode, title)
+{
+ const testDiv = document.getElementById("testDiv");
+ promise_test(() => {
+ const img = new Image();
+ if (corsMode)
+ img.crossOrigin = corsMode;
+ img.src = url;
+ return new Promise((resolve, reject) => {
+ img.onload = shoudLoad ? resolve : reject;
+ img.onerror = shoudLoad ? reject : resolve;
+ testDiv.appendChild(img);
+ }).finally(() => {
+ testDiv.innerHTML = "";
+ });
+ }, title);
+}
+
+loadImage("./resources/image.py?corp=same-origin", ok, noCors,
+ "Same-origin image load with a 'Cross-Origin-Resource-Policy: same-origin' response header.");
+
+loadImage("./resources/image.py?corp=same-site", ok, noCors,
+ "Same-origin image load with a 'Cross-Origin-Resource-Policy: same-site' response header.");
+
+loadImage(notSameSiteBaseURL + "resources/image.py?corp=same-origin&acao=*", ok, "anonymous",
+ "Cross-origin cors image load with a 'Cross-Origin-Resource-Policy: same-origin' response header.");
+
+loadImage(notSameSiteBaseURL + "resources/image.py?corp=same-site&acao=*", ok, "anonymous",
+ "Cross-origin cors image load with a 'Cross-Origin-Resource-Policy: same-site' response header.");
+
+loadImage(notSameSiteBaseURL + "resources/image.py?corp=same-origin&acao=*", ko, noCors,
+ "Cross-origin no-cors image load with a 'Cross-Origin-Resource-Policy: same-origin' response header.");
+
+loadImage(notSameSiteBaseURL + "resources/image.py?corp=same-site&acao=*", ko, noCors,
+ "Cross-origin no-cors image load with a 'Cross-Origin-Resource-Policy: same-site' response header.");
+ </script>
+</body>
+</html>
diff --git a/test/wpt/tests/fetch/cross-origin-resource-policy/resources/green.png b/test/wpt/tests/fetch/cross-origin-resource-policy/resources/green.png
new file mode 100644
index 0000000..28a1faa
--- /dev/null
+++ b/test/wpt/tests/fetch/cross-origin-resource-policy/resources/green.png
Binary files differ
diff --git a/test/wpt/tests/fetch/cross-origin-resource-policy/resources/hello.py b/test/wpt/tests/fetch/cross-origin-resource-policy/resources/hello.py
new file mode 100644
index 0000000..2b1cb84
--- /dev/null
+++ b/test/wpt/tests/fetch/cross-origin-resource-policy/resources/hello.py
@@ -0,0 +1,6 @@
+def main(request, response):
+ headers = [(b"Cross-Origin-Resource-Policy", request.GET[b'corp'])]
+ if b'origin' in request.headers:
+ headers.append((b'Access-Control-Allow-Origin', request.headers[b'origin']))
+
+ return 200, headers, b"hello"
diff --git a/test/wpt/tests/fetch/cross-origin-resource-policy/resources/iframe.py b/test/wpt/tests/fetch/cross-origin-resource-policy/resources/iframe.py
new file mode 100644
index 0000000..815ecf5
--- /dev/null
+++ b/test/wpt/tests/fetch/cross-origin-resource-policy/resources/iframe.py
@@ -0,0 +1,5 @@
+def main(request, response):
+ headers = [(b"Content-Type", b"text/html"),
+ (b"Cross-Origin-Resource-Policy", request.GET[b'corp'])]
+ return 200, headers, b"<body><h3>The iframe</h3><script>window.onmessage = () => { parent.postMessage('pong', '*'); }</script></body>"
+
diff --git a/test/wpt/tests/fetch/cross-origin-resource-policy/resources/iframeFetch.html b/test/wpt/tests/fetch/cross-origin-resource-policy/resources/iframeFetch.html
new file mode 100644
index 0000000..2571858
--- /dev/null
+++ b/test/wpt/tests/fetch/cross-origin-resource-policy/resources/iframeFetch.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <script>
+ function processMessage(event)
+ {
+ fetch(event.data, { mode: "no-cors" }).then(() => {
+ parent.postMessage("ok", "*");
+ }, () => {
+ parent.postMessage("ko", "*");
+ });
+ }
+ window.addEventListener("message", processMessage, false);
+ </script>
+</head>
+<body>
+ <h3>The iframe making a same origin fetch call.</h3>
+</body>
+</html>
diff --git a/test/wpt/tests/fetch/cross-origin-resource-policy/resources/image.py b/test/wpt/tests/fetch/cross-origin-resource-policy/resources/image.py
new file mode 100644
index 0000000..2a779cf
--- /dev/null
+++ b/test/wpt/tests/fetch/cross-origin-resource-policy/resources/image.py
@@ -0,0 +1,22 @@
+import os.path
+
+from wptserve.utils import isomorphic_decode
+
+def main(request, response):
+ type = request.GET.first(b"type", None)
+
+ body = open(os.path.join(os.path.dirname(isomorphic_decode(__file__)), u"green.png"), u"rb").read()
+
+ response.add_required_headers = False
+ response.writer.write_status(200)
+
+ if b'corp' in request.GET:
+ response.writer.write_header(b"cross-origin-resource-policy", request.GET[b'corp'])
+ if b'acao' in request.GET:
+ response.writer.write_header(b"access-control-allow-origin", request.GET[b'acao'])
+ response.writer.write_header(b"content-length", len(body))
+ if(type != None):
+ response.writer.write_header(b"content-type", type)
+ response.writer.end_headers()
+
+ response.writer.write(body)
diff --git a/test/wpt/tests/fetch/cross-origin-resource-policy/resources/redirect.py b/test/wpt/tests/fetch/cross-origin-resource-policy/resources/redirect.py
new file mode 100644
index 0000000..0dad4dd
--- /dev/null
+++ b/test/wpt/tests/fetch/cross-origin-resource-policy/resources/redirect.py
@@ -0,0 +1,6 @@
+def main(request, response):
+ headers = [(b"Location", request.GET[b'redirectTo'])]
+ if b'corp' in request.GET:
+ headers.append((b'Cross-Origin-Resource-Policy', request.GET[b'corp']))
+
+ return 302, headers, b""
diff --git a/test/wpt/tests/fetch/cross-origin-resource-policy/resources/script.py b/test/wpt/tests/fetch/cross-origin-resource-policy/resources/script.py
new file mode 100644
index 0000000..58f8d34
--- /dev/null
+++ b/test/wpt/tests/fetch/cross-origin-resource-policy/resources/script.py
@@ -0,0 +1,6 @@
+def main(request, response):
+ headers = [(b"Cross-Origin-Resource-Policy", request.GET[b'corp'])]
+ if b'origin' in request.headers:
+ headers.append((b'Access-Control-Allow-Origin', request.headers[b'origin']))
+
+ return 200, headers, b""
diff --git a/test/wpt/tests/fetch/cross-origin-resource-policy/scheme-restriction.any.js b/test/wpt/tests/fetch/cross-origin-resource-policy/scheme-restriction.any.js
new file mode 100644
index 0000000..8f63381
--- /dev/null
+++ b/test/wpt/tests/fetch/cross-origin-resource-policy/scheme-restriction.any.js
@@ -0,0 +1,7 @@
+// META: script=/common/get-host-info.sub.js
+
+promise_test(t => {
+ return promise_rejects_js(t,
+ TypeError,
+ fetch(get_host_info().HTTPS_REMOTE_ORIGIN + "/fetch/cross-origin-resource-policy/resources/hello.py?corp=same-site", { mode: "no-cors" }));
+}, "Cross-Origin-Resource-Policy: same-site blocks retrieving HTTPS from HTTP");
diff --git a/test/wpt/tests/fetch/cross-origin-resource-policy/scheme-restriction.https.window.js b/test/wpt/tests/fetch/cross-origin-resource-policy/scheme-restriction.https.window.js
new file mode 100644
index 0000000..4c74571
--- /dev/null
+++ b/test/wpt/tests/fetch/cross-origin-resource-policy/scheme-restriction.https.window.js
@@ -0,0 +1,13 @@
+// META: script=/common/get-host-info.sub.js
+
+promise_test(t => {
+ const img = new Image();
+ img.src = get_host_info().HTTP_REMOTE_ORIGIN + "/fetch/cross-origin-resource-policy/resources/image.py?corp=same-site";
+ return new Promise((resolve, reject) => {
+ img.onload = resolve;
+ img.onerror = reject;
+ document.body.appendChild(img);
+ }).finally(() => {
+ img.remove();
+ });
+}, "Cross-Origin-Resource-Policy does not block Mixed Content <img>");
diff --git a/test/wpt/tests/fetch/cross-origin-resource-policy/script-loads.html b/test/wpt/tests/fetch/cross-origin-resource-policy/script-loads.html
new file mode 100644
index 0000000..a9690fc
--- /dev/null
+++ b/test/wpt/tests/fetch/cross-origin-resource-policy/script-loads.html
@@ -0,0 +1,52 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/common/get-host-info.sub.js"></script>
+</head>
+<body>
+ <div id="testDiv"></div>
+ <script>
+const host = get_host_info();
+const notSameSiteBaseURL = host.HTTP_NOTSAMESITE_ORIGIN + window.location.pathname.replace(/\/[^\/]*$/, '/') ;
+const ok = true;
+const ko = false;
+const noCors = false;
+
+function loadScript(url, shoudLoad, corsMode, title)
+{
+ const testDiv = document.getElementById("testDiv");
+ promise_test(() => {
+ const script = document.createElement("script");
+ if (corsMode)
+ script.crossOrigin = corsMode;
+ script.src = url;
+ return new Promise((resolve, reject) => {
+ script.onload = shoudLoad ? resolve : reject;
+ script.onerror = shoudLoad ? reject : resolve;
+ testDiv.appendChild(script);
+ });
+ }, title);
+}
+
+loadScript("./resources/script.py?corp=same-origin", ok, noCors,
+ "Same-origin script load with a 'Cross-Origin-Resource-Policy: same-origin' response header.");
+
+loadScript("./resources/script.py?corp=same-site", ok, noCors,
+ "Same-origin script load with a 'Cross-Origin-Resource-Policy: same-site' response header.");
+
+loadScript(notSameSiteBaseURL + "resources/script.py?corp=same-origin&acao=*", ok, "anonymous",
+ "Cross-origin cors script load with a 'Cross-Origin-Resource-Policy: same-origin' response header.");
+
+loadScript(notSameSiteBaseURL + "resources/script.py?corp=same-site&acao=*", ok, "anonymous",
+ "Cross-origin cors script load with a 'Cross-Origin-Resource-Policy: same-site' response header.");
+
+loadScript(notSameSiteBaseURL + "resources/script.py?corp=same-origin&acao=*", ko, noCors,
+ "Cross-origin no-cors script load with a 'Cross-Origin-Resource-Policy: same-origin' response header.");
+
+loadScript(notSameSiteBaseURL + "resources/script.py?corp=same-site&acao=*", ko, noCors,
+ "Cross-origin no-cors script load with a 'Cross-Origin-Resource-Policy: same-site' response header.");
+ </script>
+</body>
+</html>
diff --git a/test/wpt/tests/fetch/cross-origin-resource-policy/syntax.any.js b/test/wpt/tests/fetch/cross-origin-resource-policy/syntax.any.js
new file mode 100644
index 0000000..dc87497
--- /dev/null
+++ b/test/wpt/tests/fetch/cross-origin-resource-policy/syntax.any.js
@@ -0,0 +1,19 @@
+// META: script=/common/get-host-info.sub.js
+
+const crossOriginURL = get_host_info().HTTP_REMOTE_ORIGIN + "/fetch/cross-origin-resource-policy/resources/hello.py?corp=";
+
+[
+ "same",
+ "same, same-origin",
+ "SAME-ORIGIN",
+ "Same-Origin",
+ "same-origin, <>",
+ "same-origin, same-origin",
+ "https://www.example.com", // See https://github.com/whatwg/fetch/issues/760
+].forEach(incorrectHeaderValue => {
+ // Note: an incorrect value results in a successful load, so this test is only meaningful in
+ // implementations with support for the header.
+ promise_test(t => {
+ return fetch(crossOriginURL + encodeURIComponent(incorrectHeaderValue), { mode: "no-cors" });
+ }, "Parsing Cross-Origin-Resource-Policy: " + incorrectHeaderValue);
+});
diff --git a/test/wpt/tests/fetch/data-urls/README.md b/test/wpt/tests/fetch/data-urls/README.md
new file mode 100644
index 0000000..1ce5b18
--- /dev/null
+++ b/test/wpt/tests/fetch/data-urls/README.md
@@ -0,0 +1,11 @@
+## data: URLs
+
+`resources/data-urls.json` contains `data:` URL tests. The tests are encoded as a JSON array. Each value in the array is an array of two or three values. The first value describes the input, the second value describes the expected MIME type, null if the input is expected to fail somehow, or the empty string if the expected value is `text/plain;charset=US-ASCII`. The third value, if present, describes the expected body as an array of integers representing bytes.
+
+These tests are used for `data:` URLs in this directory (see `processing.any.js`).
+
+## Forgiving-base64 decode
+
+`resources/base64.json` contains [forgiving-base64 decode](https://infra.spec.whatwg.org/#forgiving-base64-decode) tests. The tests are encoded as a JSON array. Each value in the array is an array of two values. The first value describes the input, the second value describes the output as an array of integers representing bytes or null if the input cannot be decoded.
+
+These tests are used for `data:` URLs in this directory (see `base64.any.js`) and `window.atob()` in `../../html/webappapis/atob/base64.html`.
diff --git a/test/wpt/tests/fetch/data-urls/base64.any.js b/test/wpt/tests/fetch/data-urls/base64.any.js
new file mode 100644
index 0000000..83f34db
--- /dev/null
+++ b/test/wpt/tests/fetch/data-urls/base64.any.js
@@ -0,0 +1,18 @@
+// META: global=window,worker
+
+promise_test(() => fetch("resources/base64.json").then(res => res.json()).then(runBase64Tests), "Setup.");
+function runBase64Tests(tests) {
+ for(let i = 0; i < tests.length; i++) {
+ const input = tests[i][0],
+ output = tests[i][1],
+ dataURL = "data:;base64," + input;
+ promise_test(t => {
+ if(output === null) {
+ return promise_rejects_js(t, TypeError, fetch(dataURL));
+ }
+ return fetch(dataURL).then(res => res.arrayBuffer()).then(body => {
+ assert_array_equals(new Uint8Array(body), output);
+ });
+ }, "data: URL base64 handling: " + format_value(input));
+ }
+}
diff --git a/test/wpt/tests/fetch/data-urls/navigate.window.js b/test/wpt/tests/fetch/data-urls/navigate.window.js
new file mode 100644
index 0000000..b532a00
--- /dev/null
+++ b/test/wpt/tests/fetch/data-urls/navigate.window.js
@@ -0,0 +1,75 @@
+// META: timeout=long
+//
+// Test some edge cases around navigation to data: URLs to ensure they use the same code path
+
+[
+ {
+ input: "data:text/html,<script>parent.postMessage(1, '*')</script>",
+ result: 1,
+ name: "Nothing fancy",
+ },
+ {
+ input: "data:text/html;base64,PHNjcmlwdD5wYXJlbnQucG9zdE1lc3NhZ2UoMiwgJyonKTwvc2NyaXB0Pg==",
+ result: 2,
+ name: "base64",
+ },
+ {
+ input: "data:text/html;base64,PHNjcmlwdD5wYXJlbnQucG9zdE1lc3NhZ2UoNCwgJyonKTwvc2NyaXB0Pr+/",
+ result: 4,
+ name: "base64 with code points that differ from base64url"
+ },
+ {
+ input: "data:text/html;base64,PHNjcml%09%20%20%0A%0C%0DwdD5wYXJlbnQucG9zdE1lc3NhZ2UoNiwgJyonKTwvc2NyaXB0Pg==",
+ result: 6,
+ name: "ASCII whitespace in the input is removed"
+ }
+].forEach(({ input, result, name }) => {
+ // Use promise_test so they go sequentially
+ promise_test(async t => {
+ const event = await new Promise((resolve, reject) => {
+ self.addEventListener("message", t.step_func(resolve), { once: true });
+ const frame = document.body.appendChild(document.createElement("iframe"));
+ t.add_cleanup(() => frame.remove());
+
+ // The assumption is that postMessage() is quicker
+ t.step_timeout(reject, 500);
+ frame.src = input;
+ });
+ assert_equals(event.data, result);
+ }, name);
+});
+
+// Failure cases
+[
+ {
+ input: "data:text/html;base64,PHNjcmlwdD5wYXJlbnQucG9zdE1lc3NhZ2UoMywgJyonKTwvc2NyaXB0Pg=",
+ name: "base64 with incorrect padding",
+ },
+ {
+ input: "data:text/html;base64,PHNjcmlwdD5wYXJlbnQucG9zdE1lc3NhZ2UoNSwgJyonKTwvc2NyaXB0Pr-_",
+ name: "base64url is not supported"
+ },
+ {
+ input: "data:text/html;base64,%0BPHNjcmlwdD5wYXJlbnQucG9zdE1lc3NhZ2UoNywgJyonKTwvc2NyaXB0Pg==",
+ name: "Vertical tab in the input leads to an error"
+ }
+].forEach(({ input, name }) => {
+ // Continue to use promise_test so they go sequentially
+ promise_test(async t => {
+ const event = await new Promise((resolve, reject) => {
+ self.addEventListener("message", t.step_func(reject), { once: true });
+ const frame = document.body.appendChild(document.createElement("iframe"));
+ t.add_cleanup(() => frame.remove());
+
+ // The assumption is that postMessage() is quicker
+ t.step_timeout(resolve, 500);
+ frame.src = input;
+ });
+ }, name);
+});
+
+// I found some of the interesting code point cases above through brute force:
+//
+// for (i = 0; i < 256; i++) {
+// w(btoa("<script>parent.postMessage(5, '*')<\/script>" + String.fromCodePoint(i) + String.fromCodePoint(i)));
+// }
diff --git a/test/wpt/tests/fetch/data-urls/processing.any.js b/test/wpt/tests/fetch/data-urls/processing.any.js
new file mode 100644
index 0000000..cec97bd
--- /dev/null
+++ b/test/wpt/tests/fetch/data-urls/processing.any.js
@@ -0,0 +1,22 @@
+// META: global=window,worker
+
+promise_test(() => fetch("resources/data-urls.json").then(res => res.json()).then(runDataURLTests), "Setup.");
+function runDataURLTests(tests) {
+ for(let i = 0; i < tests.length; i++) {
+ const input = tests[i][0],
+ expectedMimeType = tests[i][1],
+ expectedBody = expectedMimeType !== null ? tests[i][2] : null;
+ promise_test(t => {
+ if(expectedMimeType === null) {
+ return promise_rejects_js(t, TypeError, fetch(input));
+ } else {
+ return fetch(input).then(res => {
+ return res.arrayBuffer().then(body => {
+ assert_array_equals(new Uint8Array(body), expectedBody);
+ assert_equals(res.headers.get("content-type"), expectedMimeType); // We could assert this earlier, but this fails often
+ });
+ });
+ }
+ }, format_value(input));
+ }
+}
diff --git a/test/wpt/tests/fetch/data-urls/resources/base64.json b/test/wpt/tests/fetch/data-urls/resources/base64.json
new file mode 100644
index 0000000..01f981a
--- /dev/null
+++ b/test/wpt/tests/fetch/data-urls/resources/base64.json
@@ -0,0 +1,82 @@
+[
+ ["", []],
+ ["abcd", [105, 183, 29]],
+ [" abcd", [105, 183, 29]],
+ ["abcd ", [105, 183, 29]],
+ [" abcd===", null],
+ ["abcd=== ", null],
+ ["abcd ===", null],
+ ["a", null],
+ ["ab", [105]],
+ ["abc", [105, 183]],
+ ["abcde", null],
+ ["ð€€", null],
+ ["=", null],
+ ["==", null],
+ ["===", null],
+ ["====", null],
+ ["=====", null],
+ ["a=", null],
+ ["a==", null],
+ ["a===", null],
+ ["a====", null],
+ ["a=====", null],
+ ["ab=", null],
+ ["ab==", [105]],
+ ["ab===", null],
+ ["ab====", null],
+ ["ab=====", null],
+ ["abc=", [105, 183]],
+ ["abc==", null],
+ ["abc===", null],
+ ["abc====", null],
+ ["abc=====", null],
+ ["abcd=", null],
+ ["abcd==", null],
+ ["abcd===", null],
+ ["abcd====", null],
+ ["abcd=====", null],
+ ["abcde=", null],
+ ["abcde==", null],
+ ["abcde===", null],
+ ["abcde====", null],
+ ["abcde=====", null],
+ ["=a", null],
+ ["=a=", null],
+ ["a=b", null],
+ ["a=b=", null],
+ ["ab=c", null],
+ ["ab=c=", null],
+ ["abc=d", null],
+ ["abc=d=", null],
+ ["ab\u000Bcd", null],
+ ["ab\u3000cd", null],
+ ["ab\u3001cd", null],
+ ["ab\tcd", [105, 183, 29]],
+ ["ab\ncd", [105, 183, 29]],
+ ["ab\fcd", [105, 183, 29]],
+ ["ab\rcd", [105, 183, 29]],
+ ["ab cd", [105, 183, 29]],
+ ["ab\u00a0cd", null],
+ ["ab\t\n\f\r cd", [105, 183, 29]],
+ [" \t\n\f\r ab\t\n\f\r cd\t\n\f\r ", [105, 183, 29]],
+ ["ab\t\n\f\r =\t\n\f\r =\t\n\f\r ", [105]],
+ ["A", null],
+ ["/A", [252]],
+ ["//A", [255, 240]],
+ ["///A", [255, 255, 192]],
+ ["////A", null],
+ ["/", null],
+ ["A/", [3]],
+ ["AA/", [0, 15]],
+ ["AAAA/", null],
+ ["AAA/", [0, 0, 63]],
+ ["\u0000nonsense", null],
+ ["abcd\u0000nonsense", null],
+ ["YQ", [97]],
+ ["YR", [97]],
+ ["~~", null],
+ ["..", null],
+ ["--", null],
+ ["__", null]
+]
diff --git a/test/wpt/tests/fetch/data-urls/resources/data-urls.json b/test/wpt/tests/fetch/data-urls/resources/data-urls.json
new file mode 100644
index 0000000..f318d1f
--- /dev/null
+++ b/test/wpt/tests/fetch/data-urls/resources/data-urls.json
@@ -0,0 +1,214 @@
+[
+ ["data://test/,X",
+ "text/plain;charset=US-ASCII",
+ [88]],
+ ["data://test:test/,X",
+ null],
+ ["data:,X",
+ "text/plain;charset=US-ASCII",
+ [88]],
+ ["data:",
+ null],
+ ["data:text/html",
+ null],
+ ["data:text/html ;charset=x ",
+ null],
+ ["data:,",
+ "text/plain;charset=US-ASCII",
+ []],
+ ["data:,X#X",
+ "text/plain;charset=US-ASCII",
+ [88]],
+ ["data:,%FF",
+ "text/plain;charset=US-ASCII",
+ [255]],
+ ["data:text/plain,X",
+ "text/plain",
+ [88]],
+ ["data:text/plain ,X",
+ "text/plain",
+ [88]],
+ ["data:text/plain%20,X",
+ "text/plain%20",
+ [88]],
+ ["data:text/plain\f,X",
+ "text/plain%0c",
+ [88]],
+ ["data:text/plain%0C,X",
+ "text/plain%0c",
+ [88]],
+ ["data:text/plain;,X",
+ "text/plain",
+ [88]],
+ ["data:;x=x;charset=x,X",
+ "text/plain;x=x;charset=x",
+ [88]],
+ ["data:;x=x,X",
+ "text/plain;x=x",
+ [88]],
+ ["data:text/plain;charset=windows-1252,%C2%B1",
+ "text/plain;charset=windows-1252",
+ [194, 177]],
+ ["data:text/plain;Charset=UTF-8,%C2%B1",
+ "text/plain;charset=UTF-8",
+ [194, 177]],
+ ["data:text/plain;charset=windows-1252,áñçə💩",
+ "text/plain;charset=windows-1252",
+ [195, 161, 195, 177, 195, 167, 201, 153, 240, 159, 146, 169]],
+ ["data:text/plain;charset=UTF-8,áñçə💩",
+ "text/plain;charset=UTF-8",
+ [195, 161, 195, 177, 195, 167, 201, 153, 240, 159, 146, 169]],
+ ["data:image/gif,%C2%B1",
+ "image/gif",
+ [194, 177]],
+ ["data:IMAGE/gif,%C2%B1",
+ "image/gif",
+ [194, 177]],
+ ["data:IMAGE/gif;hi=x,%C2%B1",
+ "image/gif;hi=x",
+ [194, 177]],
+ ["data:IMAGE/gif;CHARSET=x,%C2%B1",
+ "image/gif;charset=x",
+ [194, 177]],
+ ["data: ,%FF",
+ "text/plain;charset=US-ASCII",
+ [255]],
+ ["data:%20,%FF",
+ "text/plain;charset=US-ASCII",
+ [255]],
+ ["data:\f,%FF",
+ "text/plain;charset=US-ASCII",
+ [255]],
+ ["data:%1F,%FF",
+ "text/plain;charset=US-ASCII",
+ [255]],
+ ["data:\u0000,%FF",
+ "text/plain;charset=US-ASCII",
+ [255]],
+ ["data:%00,%FF",
+ "text/plain;charset=US-ASCII",
+ [255]],
+ ["data:text/html ,X",
+ "text/html",
+ [88]],
+ ["data:text / html,X",
+ "text/plain;charset=US-ASCII",
+ [88]],
+ ["data:†,X",
+ "text/plain;charset=US-ASCII",
+ [88]],
+ ["data:†/†,X",
+ "%e2%80%a0/%e2%80%a0",
+ [88]],
+ ["data:X,X",
+ "text/plain;charset=US-ASCII",
+ [88]],
+ ["data:image/png,X X",
+ "image/png",
+ [88, 32, 88]],
+ ["data:application/javascript,X X",
+ "application/javascript",
+ [88, 32, 88]],
+ ["data:application/xml,X X",
+ "application/xml",
+ [88, 32, 88]],
+ ["data:text/javascript,X X",
+ "text/javascript",
+ [88, 32, 88]],
+ ["data:text/plain,X X",
+ "text/plain",
+ [88, 32, 88]],
+ ["data:unknown/unknown,X X",
+ "unknown/unknown",
+ [88, 32, 88]],
+ ["data:text/plain;a=\",\",X",
+ "text/plain;a=\"\"",
+ [34, 44, 88]],
+ ["data:text/plain;a=%2C,X",
+ "text/plain;a=%2C",
+ [88]],
+ ["data:;base64;base64,WA",
+ "text/plain",
+ [88]],
+ ["data:x/x;base64;base64,WA",
+ "x/x",
+ [88]],
+ ["data:x/x;base64;charset=x,WA",
+ "x/x;charset=x",
+ [87, 65]],
+ ["data:x/x;base64;charset=x;base64,WA",
+ "x/x;charset=x",
+ [88]],
+ ["data:x/x;base64;base64x,WA",
+ "x/x",
+ [87, 65]],
+ ["data:;base64,W%20A",
+ "text/plain;charset=US-ASCII",
+ [88]],
+ ["data:;base64,W%0CA",
+ "text/plain;charset=US-ASCII",
+ [88]],
+ ["data:x;base64x,WA",
+ "text/plain;charset=US-ASCII",
+ [87, 65]],
+ ["data:x;base64;x,WA",
+ "text/plain;charset=US-ASCII",
+ [87, 65]],
+ ["data:x;base64=x,WA",
+ "text/plain;charset=US-ASCII",
+ [87, 65]],
+ ["data:; base64,WA",
+ "text/plain;charset=US-ASCII",
+ [88]],
+ ["data:; base64,WA",
+ "text/plain;charset=US-ASCII",
+ [88]],
+ ["data: ;charset=x ; base64,WA",
+ "text/plain;charset=x",
+ [88]],
+ ["data:;base64;,WA",
+ "text/plain",
+ [87, 65]],
+ ["data:;base64 ,WA",
+ "text/plain;charset=US-ASCII",
+ [88]],
+ ["data:;base64 ,WA",
+ "text/plain;charset=US-ASCII",
+ [88]],
+ ["data:;base 64,WA",
+ "text/plain",
+ [87, 65]],
+ ["data:;BASe64,WA",
+ "text/plain;charset=US-ASCII",
+ [88]],
+ ["data:;%62ase64,WA",
+ "text/plain",
+ [87, 65]],
+ ["data:%3Bbase64,WA",
+ "text/plain;charset=US-ASCII",
+ [87, 65]],
+ ["data:;charset=x,X",
+ "text/plain;charset=x",
+ [88]],
+ ["data:; charset=x,X",
+ "text/plain;charset=x",
+ [88]],
+ ["data:;charset =x,X",
+ "text/plain",
+ [88]],
+ ["data:;charset= x,X",
+ "text/plain;charset=\" x\"",
+ [88]],
+ ["data:;charset=,X",
+ "text/plain",
+ [88]],
+ ["data:;charset,X",
+ "text/plain",
+ [88]],
+ ["data:;charset=\"x\",X",
+ "text/plain;charset=x",
+ [88]],
+ ["data:;CHARSET=\"X\",X",
+ "text/plain;charset=X",
+ [88]]
+]
diff --git a/test/wpt/tests/fetch/fetch-later/META.yml b/test/wpt/tests/fetch/fetch-later/META.yml
new file mode 100644
index 0000000..f8fd46b
--- /dev/null
+++ b/test/wpt/tests/fetch/fetch-later/META.yml
@@ -0,0 +1,3 @@
+spec: https://whatpr.org/fetch/1647/094ea69...152d725.html#fetch-later-method
+suggested_reviewers:
+ - mingyc
diff --git a/test/wpt/tests/fetch/fetch-later/README.md b/test/wpt/tests/fetch/fetch-later/README.md
new file mode 100644
index 0000000..661e2b9
--- /dev/null
+++ b/test/wpt/tests/fetch/fetch-later/README.md
@@ -0,0 +1,3 @@
+# FetchLater Tests
+
+These tests cover [FetchLater method](https://whatpr.org/fetch/1647/094ea69...152d725.html#fetch-later-method) related behaviors.
diff --git a/test/wpt/tests/fetch/fetch-later/basic.tentative.https.window.js b/test/wpt/tests/fetch/fetch-later/basic.tentative.https.window.js
new file mode 100644
index 0000000..a8ca011
--- /dev/null
+++ b/test/wpt/tests/fetch/fetch-later/basic.tentative.https.window.js
@@ -0,0 +1,13 @@
+// META: script=/resources/testharness.js
+// META: script=/resources/testharnessreport.js
+
+'use strict';
+
+test(() => {
+ assert_throws_js(TypeError, () => fetchLater());
+}, `fetchLater() cannot be called without request.`);
+
+test(() => {
+ const result = fetchLater('/');
+ assert_false(result.sent);
+}, `fetchLater()'s return tells the deferred request is not yet sent.`);
diff --git a/test/wpt/tests/fetch/fetch-later/non-secure.window.js b/test/wpt/tests/fetch/fetch-later/non-secure.window.js
new file mode 100644
index 0000000..2f2c3ea
--- /dev/null
+++ b/test/wpt/tests/fetch/fetch-later/non-secure.window.js
@@ -0,0 +1,8 @@
+// META: script=/resources/testharness.js
+// META: script=/resources/testharnessreport.js
+
+'use strict';
+
+test(() => {
+ assert_false(window.hasOwnProperty('fetchLater'));
+}, `fetchLater() is not supported in non-secure context.`);
diff --git a/test/wpt/tests/fetch/fetch-later/sendondiscard.tentative.https.window.js b/test/wpt/tests/fetch/fetch-later/sendondiscard.tentative.https.window.js
new file mode 100644
index 0000000..0613d18
--- /dev/null
+++ b/test/wpt/tests/fetch/fetch-later/sendondiscard.tentative.https.window.js
@@ -0,0 +1,28 @@
+// 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);
+ 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++) {
+ let get = fetchLater(url);
+ let post = fetchLater(url, {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.');
diff --git a/test/wpt/tests/fetch/h1-parsing/README.md b/test/wpt/tests/fetch/h1-parsing/README.md
new file mode 100644
index 0000000..487a892
--- /dev/null
+++ b/test/wpt/tests/fetch/h1-parsing/README.md
@@ -0,0 +1,5 @@
+This directory tries to document "rough consensus" on where HTTP/1 parsing should end up between browsers.
+
+Any tests that browsers currently fail should have associated bug reports.
+
+[whatwg/fetch issue #1156](https://github.com/whatwg/fetch/issues/1156) provides context for this effort and pointers to the various issues, pull requests, and bug reports that are associated with it.
diff --git a/test/wpt/tests/fetch/h1-parsing/lone-cr.window.js b/test/wpt/tests/fetch/h1-parsing/lone-cr.window.js
new file mode 100644
index 0000000..6b46ed6
--- /dev/null
+++ b/test/wpt/tests/fetch/h1-parsing/lone-cr.window.js
@@ -0,0 +1,23 @@
+// These tests expect that a network error is returned if there's a CR that is not immediately
+// followed by LF before reaching message-body.
+//
+// No browser does this currently, but Firefox does treat it equivalently to a space which gives
+// hope.
+
+[
+ "HTTP/1.1\r200 OK\n\nBODY",
+ "HTTP/1.1 200\rOK\n\nBODY",
+ "HTTP/1.1 200 OK\n\rHeader: Value\n\nBODY",
+ "HTTP/1.1 200 OK\nHeader\r: Value\n\nBODY",
+ "HTTP/1.1 200 OK\nHeader:\r Value\n\nBODY",
+ "HTTP/1.1 200 OK\nHeader: Value\r\n\nBody",
+ "HTTP/1.1 200 OK\nHeader: Value\r\r\nBODY",
+ "HTTP/1.1 200 OK\nHeader: Value\rHeader2: Value2\n\nBODY",
+ "HTTP/1.1 200 OK\nHeader: Value\n\rBODY",
+ "HTTP/1.1 200 OK\nHeader: Value\n\r"
+].forEach(input => {
+ promise_test(t => {
+ const message = encodeURIComponent(input);
+ return promise_rejects_js(t, TypeError, fetch(`resources/message.py?message=${message}`));
+ }, `Parsing response with a lone CR before message-body (${input})`);
+});
diff --git a/test/wpt/tests/fetch/h1-parsing/resources-with-0x00-in-header.window.js b/test/wpt/tests/fetch/h1-parsing/resources-with-0x00-in-header.window.js
new file mode 100644
index 0000000..37a61c1
--- /dev/null
+++ b/test/wpt/tests/fetch/h1-parsing/resources-with-0x00-in-header.window.js
@@ -0,0 +1,31 @@
+async_test(t => {
+ const script = document.createElement("script");
+ t.add_cleanup(() => script.remove());
+ script.src = "resources/script-with-0x00-in-header.py";
+ script.onerror = t.step_func_done();
+ script.onload = t.unreached_func();
+ document.body.append(script);
+}, "Expect network error for script with 0x00 in a header");
+
+async_test(t => {
+ const frame = document.createElement("iframe");
+ t.add_cleanup(() => frame.remove());
+ frame.src = "resources/document-with-0x00-in-header.py";
+ // If network errors result in load events for frames per
+ // https://github.com/whatwg/html/issues/125 and https://github.com/whatwg/html/issues/1230 this
+ // should be changed to use the load event instead.
+ t.step_timeout(() => {
+ assert_equals(window.frameLoaded, undefined);
+ t.done();
+ }, 1000);
+ document.body.append(frame);
+}, "Expect network error for frame navigation to resource with 0x00 in a header");
+
+async_test(t => {
+ const img = document.createElement("img");
+ t.add_cleanup(() => img.remove());
+ img.src = "resources/blue-with-0x00-in-a-header.asis";
+ img.onerror = t.step_func_done();
+ img.onload = t.unreached_func();
+ document.body.append(img);
+}, "Expect network error for image with 0x00 in a header");
diff --git a/test/wpt/tests/fetch/h1-parsing/resources/README.md b/test/wpt/tests/fetch/h1-parsing/resources/README.md
new file mode 100644
index 0000000..2175d27
--- /dev/null
+++ b/test/wpt/tests/fetch/h1-parsing/resources/README.md
@@ -0,0 +1,6 @@
+`blue-with-0x00-in-a-header.asis` is a copy from `../../images/blue.png` with the following prepended using Control Pictures to signify actual newlines and 0x00:
+```
+HTTP/1.1 200 AN IMAGEââŠ
+Content-Type: image/pngââŠ
+Custom: â€ââŠââŠ
+```
diff --git a/test/wpt/tests/fetch/h1-parsing/resources/blue-with-0x00-in-a-header.asis b/test/wpt/tests/fetch/h1-parsing/resources/blue-with-0x00-in-a-header.asis
new file mode 100644
index 0000000..102340a
--- /dev/null
+++ b/test/wpt/tests/fetch/h1-parsing/resources/blue-with-0x00-in-a-header.asis
Binary files differ
diff --git a/test/wpt/tests/fetch/h1-parsing/resources/document-with-0x00-in-header.py b/test/wpt/tests/fetch/h1-parsing/resources/document-with-0x00-in-header.py
new file mode 100644
index 0000000..d91998b
--- /dev/null
+++ b/test/wpt/tests/fetch/h1-parsing/resources/document-with-0x00-in-header.py
@@ -0,0 +1,4 @@
+def main(request, response):
+ response.headers.set(b"Content-Type", b"text/html")
+ response.headers.set(b"Custom", b"\0")
+ return b"<!doctype html><script>top.frameLoaded=true</script><b>This is a document.</b>"
diff --git a/test/wpt/tests/fetch/h1-parsing/resources/message.py b/test/wpt/tests/fetch/h1-parsing/resources/message.py
new file mode 100644
index 0000000..640080c
--- /dev/null
+++ b/test/wpt/tests/fetch/h1-parsing/resources/message.py
@@ -0,0 +1,3 @@
+def main(request, response):
+ response.writer.write(request.GET.first(b"message"))
+ response.close_connection = True
diff --git a/test/wpt/tests/fetch/h1-parsing/resources/script-with-0x00-in-header.py b/test/wpt/tests/fetch/h1-parsing/resources/script-with-0x00-in-header.py
new file mode 100644
index 0000000..39f58d8
--- /dev/null
+++ b/test/wpt/tests/fetch/h1-parsing/resources/script-with-0x00-in-header.py
@@ -0,0 +1,4 @@
+def main(request, response):
+ response.headers.set(b"Content-Type", b"text/javascript")
+ response.headers.set(b"Custom", b"\0")
+ return b"var thisIsJavaScript = 0"
diff --git a/test/wpt/tests/fetch/h1-parsing/resources/status-code.py b/test/wpt/tests/fetch/h1-parsing/resources/status-code.py
new file mode 100644
index 0000000..5421893
--- /dev/null
+++ b/test/wpt/tests/fetch/h1-parsing/resources/status-code.py
@@ -0,0 +1,6 @@
+def main(request, response):
+ output = b"HTTP/1.1 "
+ output += request.GET.first(b"input")
+ output += b"\nheader-parsing: is sad\n"
+ response.writer.write(output)
+ response.close_connection = True
diff --git a/test/wpt/tests/fetch/h1-parsing/status-code.window.js b/test/wpt/tests/fetch/h1-parsing/status-code.window.js
new file mode 100644
index 0000000..5776cf4
--- /dev/null
+++ b/test/wpt/tests/fetch/h1-parsing/status-code.window.js
@@ -0,0 +1,98 @@
+[
+ {
+ input: "",
+ expected: null
+ },
+ {
+ input: "BLAH",
+ expected: null
+ },
+ {
+ input: "0 OK",
+ expected: {
+ status: 0,
+ statusText: "OK"
+ }
+ },
+ {
+ input: "1 OK",
+ expected: {
+ status: 1,
+ statusText: "OK"
+ }
+ },
+ {
+ input: "99 NOT OK",
+ expected: {
+ status: 99,
+ statusText: "NOT OK"
+ }
+ },
+ {
+ input: "077 77",
+ expected: {
+ status: 77,
+ statusText: "77"
+ }
+ },
+ {
+ input: "099 HELLO",
+ expected: {
+ status: 99,
+ statusText: "HELLO"
+ }
+ },
+ {
+ input: "200",
+ expected: {
+ status: 200,
+ statusText: ""
+ }
+ },
+ {
+ input: "999 DOES IT MATTER",
+ expected: {
+ status: 999,
+ statusText: "DOES IT MATTER"
+ }
+ },
+ {
+ input: "1000 BOO",
+ expected: null
+ },
+ {
+ input: "0200 BOO",
+ expected: null
+ },
+ {
+ input: "65736 NOT 200 OR SOME SUCH",
+ expected: null
+ },
+ {
+ input: "131072 HI",
+ expected: null
+ },
+ {
+ input: "-200 TEST",
+ expected: null
+ },
+ {
+ input: "0xA",
+ expected: null
+ },
+ {
+ input: "C8",
+ expected: null
+ }
+].forEach(({ description, input, expected }) => {
+ promise_test(async t => {
+ if (expected !== null) {
+ const response = await fetch("resources/status-code.py?input=" + input);
+ assert_equals(response.status, expected.status);
+ assert_equals(response.statusText, expected.statusText);
+ assert_equals(response.headers.get("header-parsing"), "is sad");
+ } else {
+ await promise_rejects_js(t, TypeError, fetch("resources/status-code.py?input=" + input));
+ }
+ }, `HTTP/1.1 ${input} ${expected === null ? "(network error)" : ""}`);
+});
diff --git a/test/wpt/tests/fetch/http-cache/304-update.any.js b/test/wpt/tests/fetch/http-cache/304-update.any.js
new file mode 100644
index 0000000..15484f0
--- /dev/null
+++ b/test/wpt/tests/fetch/http-cache/304-update.any.js
@@ -0,0 +1,146 @@
+// META: global=window,worker
+// META: title=HTTP Cache - 304 Updates
+// META: timeout=long
+// META: script=/common/utils.js
+// META: script=/common/get-host-info.sub.js
+// META: script=http-cache.js
+
+var tests = [
+ {
+ name: "HTTP cache updates returned headers from a Last-Modified 304",
+ requests: [
+ {
+ response_headers: [
+ ["Expires", -5000],
+ ["Last-Modified", -3000],
+ ["Test-Header", "A"]
+ ]
+ },
+ {
+ response_headers: [
+ ["Expires", -3000],
+ ["Last-Modified", -3000],
+ ["Test-Header", "B"]
+ ],
+ expected_type: "lm_validated",
+ expected_response_headers: [
+ ["Test-Header", "B"]
+ ]
+ }
+ ]
+ },
+ {
+ name: "HTTP cache updates stored headers from a Last-Modified 304",
+ requests: [
+ {
+ response_headers: [
+ ["Expires", -5000],
+ ["Last-Modified", -3000],
+ ["Test-Header", "A"]
+ ]
+ },
+ {
+ response_headers: [
+ ["Expires", 3000],
+ ["Last-Modified", -3000],
+ ["Test-Header", "B"]
+ ],
+ expected_type: "lm_validated",
+ expected_response_headers: [
+ ["Test-Header", "B"]
+ ],
+ pause_after: true
+ },
+ {
+ expected_type: "cached",
+ expected_response_headers: [
+ ["Test-Header", "B"]
+ ]
+ }
+ ]
+ },
+ {
+ name: "HTTP cache updates returned headers from a ETag 304",
+ requests: [
+ {
+ response_headers: [
+ ["Expires", -5000],
+ ["ETag", "ABC"],
+ ["Test-Header", "A"]
+ ]
+ },
+ {
+ response_headers: [
+ ["Expires", -3000],
+ ["ETag", "ABC"],
+ ["Test-Header", "B"]
+ ],
+ expected_type: "etag_validated",
+ expected_response_headers: [
+ ["Test-Header", "B"]
+ ]
+ }
+ ]
+ },
+ {
+ name: "HTTP cache updates stored headers from a ETag 304",
+ requests: [
+ {
+ response_headers: [
+ ["Expires", -5000],
+ ["ETag", "DEF"],
+ ["Test-Header", "A"]
+ ]
+ },
+ {
+ response_headers: [
+ ["Expires", 3000],
+ ["ETag", "DEF"],
+ ["Test-Header", "B"]
+ ],
+ expected_type: "etag_validated",
+ expected_response_headers: [
+ ["Test-Header", "B"]
+ ],
+ pause_after: true
+ },
+ {
+ expected_type: "cached",
+ expected_response_headers: [
+ ["Test-Header", "B"]
+ ]
+ }
+ ]
+ },
+ {
+ name: "Content-* header",
+ requests: [
+ {
+ response_headers: [
+ ["Expires", -5000],
+ ["ETag", "GHI"],
+ ["Content-Test-Header", "A"]
+ ]
+ },
+ {
+ response_headers: [
+ ["Expires", 3000],
+ ["ETag", "GHI"],
+ ["Content-Test-Header", "B"]
+ ],
+ expected_type: "etag_validated",
+ expected_response_headers: [
+ ["Content-Test-Header", "B"]
+ ],
+ pause_after: true
+ },
+ {
+ expected_type: "cached",
+ expected_response_headers: [
+ ["Content-Test-Header", "B"]
+ ]
+ }
+ ]
+ },
+];
+run_tests(tests);
diff --git a/test/wpt/tests/fetch/http-cache/README.md b/test/wpt/tests/fetch/http-cache/README.md
new file mode 100644
index 0000000..512c422
--- /dev/null
+++ b/test/wpt/tests/fetch/http-cache/README.md
@@ -0,0 +1,72 @@
+## HTTP Caching Tests
+
+These tests cover HTTP-specified behaviours for caches, primarily from
+[RFC9111](https://www.rfc-editor.org/rfc/rfc9111.html), but as seen through the
+lens of Fetch.
+
+A few notes:
+
+* By its nature, [caching is entirely optional](
+ https://www.rfc-editor.org/rfc/rfc9111.html#section-2-2);
+ some tests expecting a response to be
+ cached might fail because the client chose not to cache it, or chose to
+ race the cache with a network request.
+
+* Likewise, some tests might fail because there is a separate document-level
+ cache that's not well defined; see [this
+ issue](https://github.com/whatwg/fetch/issues/354).
+
+* [Partial content tests](partial.any.js) (a.k.a. Range requests) are not specified
+ in Fetch; tests are included here for interest only.
+
+* Some browser caches will behave differently when reloading /
+ shift-reloading, despite the `cache mode` staying the same.
+
+* [cache-tests.fyi](https://cache-tests.fyi/) is another test suite of HTTP caching
+ which also caters to server/CDN implementations.
+
+## Test Format
+
+Each test run gets its own URL and randomized content and operates independently.
+
+Each test is an an array of objects, with the following members:
+
+- `name` - The name of the test.
+- `requests` - a list of request objects (see below).
+
+Possible members of a request object:
+
+- template - A template object for the request, by name.
+- request_method - A string containing the HTTP method to be used.
+- request_headers - An array of `[header_name_string, header_value_string]` arrays to
+ emit in the request.
+- request_body - A string to use as the request body.
+- mode - The mode string to pass to `fetch()`.
+- credentials - The credentials string to pass to `fetch()`.
+- cache - The cache string to pass to `fetch()`.
+- pause_after - Boolean controlling a 3-second pause after the request completes.
+- response_status - A `[number, string]` array containing the HTTP status code
+ and phrase to return.
+- response_headers - An array of `[header_name_string, header_value_string]` arrays to
+ emit in the response. These values will also be checked like
+ expected_response_headers, unless there is a third value that is
+ `false`. See below for special handling considerations.
+- response_body - String to send as the response body. If not set, it will contain
+ the test identifier.
+- expected_type - One of `["cached", "not_cached", "lm_validate", "etag_validate", "error"]`
+- expected_status - A number representing a HTTP status code to check the response for.
+ If not set, the value of `response_status[0]` will be used; if that
+ is not set, 200 will be used.
+- expected_request_headers - An array of `[header_name_string, header_value_string]` representing
+ headers to check the request for.
+- expected_response_headers - An array of `[header_name_string, header_value_string]` representing
+ headers to check the response for. See also response_headers.
+- expected_response_text - A string to check the response body against. If not present, `response_body` will be checked if present and non-null; otherwise the response body will be checked for the test uuid (unless the status code disallows a body). Set to `null` to disable all response body checking.
+
+Some headers in `response_headers` are treated specially:
+
+* For date-carrying headers, if the value is a number, it will be interpreted as a delta to the time of the first request at the server.
+* For URL-carrying headers, the value will be appended as a query parameter for `target`.
+
+See the source for exact details.
+
diff --git a/test/wpt/tests/fetch/http-cache/basic-auth-cache-test-ref.html b/test/wpt/tests/fetch/http-cache/basic-auth-cache-test-ref.html
new file mode 100644
index 0000000..905facd
--- /dev/null
+++ b/test/wpt/tests/fetch/http-cache/basic-auth-cache-test-ref.html
@@ -0,0 +1,6 @@
+<!doctype html>
+<html>
+ <meta charset="utf-8">
+ <img src="/images/green.png">
+ <img src="/images/green.png">
+</html>
diff --git a/test/wpt/tests/fetch/http-cache/basic-auth-cache-test.html b/test/wpt/tests/fetch/http-cache/basic-auth-cache-test.html
new file mode 100644
index 0000000..a8979ba
--- /dev/null
+++ b/test/wpt/tests/fetch/http-cache/basic-auth-cache-test.html
@@ -0,0 +1,27 @@
+<!doctype html>
+<html id="doc" class="reftest-wait">
+ <meta charset="utf-8">
+ <link rel="match" href="basic-auth-cache-test-ref.html">
+
+ <img id="auth" onload="loadNoAuth()">
+ <img id="noauth" onload="removeWait()">
+
+
+ <script type="text/javascript">
+ function loadAuth() {
+ var authUrl = 'http://testuser:testpass@' + window.location.host + '/fetch/http-cache/resources/securedimage.py';
+ document.getElementById('auth').src = authUrl;
+ }
+
+ function loadNoAuth() {
+ var noAuthUrl = 'http://' + window.location.host + '/fetch/http-cache/resources/securedimage.py';
+ document.getElementById('noauth').src = noAuthUrl;
+ }
+
+ function removeWait() {
+ document.getElementById('doc').className = "";
+ }
+
+ window.onload = loadAuth;
+ </script>
+</html>
diff --git a/test/wpt/tests/fetch/http-cache/cache-mode.any.js b/test/wpt/tests/fetch/http-cache/cache-mode.any.js
new file mode 100644
index 0000000..8f406d5
--- /dev/null
+++ b/test/wpt/tests/fetch/http-cache/cache-mode.any.js
@@ -0,0 +1,61 @@
+// META: global=window,worker
+// META: title=Fetch - Cache Mode
+// META: timeout=long
+// META: script=/common/utils.js
+// META: script=/common/get-host-info.sub.js
+// META: script=http-cache.js
+
+var tests = [
+ {
+ name: "Fetch sends Cache-Control: max-age=0 when cache mode is no-cache",
+ requests: [
+ {
+ cache: "no-cache",
+ expected_request_headers: [['cache-control', 'max-age=0']]
+ }
+ ]
+ },
+ {
+ name: "Fetch doesn't touch Cache-Control when cache mode is no-cache and Cache-Control is already present",
+ requests: [
+ {
+ cache: "no-cache",
+ request_headers: [['cache-control', 'foo']],
+ expected_request_headers: [['cache-control', 'foo']]
+ }
+ ]
+ },
+ {
+ name: "Fetch sends Cache-Control: no-cache and Pragma: no-cache when cache mode is no-store",
+ requests: [
+ {
+ cache: "no-store",
+ expected_request_headers: [
+ ['cache-control', 'no-cache'],
+ ['pragma', 'no-cache']
+ ]
+ }
+ ]
+ },
+ {
+ name: "Fetch doesn't touch Cache-Control when cache mode is no-store and Cache-Control is already present",
+ requests: [
+ {
+ cache: "no-store",
+ request_headers: [['cache-control', 'foo']],
+ expected_request_headers: [['cache-control', 'foo']]
+ }
+ ]
+ },
+ {
+ name: "Fetch doesn't touch Pragma when cache mode is no-store and Pragma is already present",
+ requests: [
+ {
+ cache: "no-store",
+ request_headers: [['pragma', 'foo']],
+ expected_request_headers: [['pragma', 'foo']]
+ }
+ ]
+ }
+];
+run_tests(tests);
diff --git a/test/wpt/tests/fetch/http-cache/cc-request.any.js b/test/wpt/tests/fetch/http-cache/cc-request.any.js
new file mode 100644
index 0000000..d556566
--- /dev/null
+++ b/test/wpt/tests/fetch/http-cache/cc-request.any.js
@@ -0,0 +1,202 @@
+// META: global=window,worker
+// META: title=HTTP Cache - Cache-Control Request Directives
+// META: timeout=long
+// META: script=/common/utils.js
+// META: script=/common/get-host-info.sub.js
+// META: script=http-cache.js
+
+var tests = [
+ {
+ name: "HTTP cache doesn't use aged but fresh response when request contains Cache-Control: max-age=0",
+ requests: [
+ {
+ template: "fresh",
+ pause_after: true
+ },
+ {
+ request_headers: [
+ ["Cache-Control", "max-age=0"]
+ ],
+ expected_type: "not_cached"
+ }
+ ]
+ },
+ {
+ name: "HTTP cache doesn't use aged but fresh response when request contains Cache-Control: max-age=1",
+ requests: [
+ {
+ template: "fresh",
+ pause_after: true
+ },
+ {
+ request_headers: [
+ ["Cache-Control", "max-age=1"]
+ ],
+ expected_type: "not_cached"
+ }
+ ]
+ },
+ {
+ name: "HTTP cache doesn't use fresh response with Age header when request contains Cache-Control: max-age that is greater than remaining freshness",
+ requests: [
+ {
+ response_headers: [
+ ["Cache-Control", "max-age=3600"],
+ ["Age", "1800"]
+ ]
+ },
+ {
+ request_headers: [
+ ["Cache-Control", "max-age=600"]
+ ],
+ expected_type: "not_cached"
+ }
+ ]
+ },
+ {
+ name: "HTTP cache does use aged stale response when request contains Cache-Control: max-stale that permits its use",
+ requests: [
+ {
+ response_headers: [
+ ["Cache-Control", "max-age=1"]
+ ],
+ pause_after: true
+ },
+ {
+ request_headers: [
+ ["Cache-Control", "max-stale=1000"]
+ ],
+ expected_type: "cached"
+ }
+ ]
+ },
+ {
+ name: "HTTP cache does reuse stale response with Age header when request contains Cache-Control: max-stale that permits its use",
+ requests: [
+ {
+ response_headers: [
+ ["Cache-Control", "max-age=1500"],
+ ["Age", "2000"]
+ ]
+ },
+ {
+ request_headers: [
+ ["Cache-Control", "max-stale=1000"]
+ ],
+ expected_type: "cached"
+ }
+ ]
+ },
+ {
+ name: "HTTP cache doesn't reuse fresh response when request contains Cache-Control: min-fresh that wants it fresher",
+ requests: [
+ {
+ response_headers: [
+ ["Cache-Control", "max-age=1500"]
+ ]
+ },
+ {
+ request_headers: [
+ ["Cache-Control", "min-fresh=2000"]
+ ],
+ expected_type: "not_cached"
+ }
+ ]
+ },
+ {
+ name: "HTTP cache doesn't reuse fresh response with Age header when request contains Cache-Control: min-fresh that wants it fresher",
+ requests: [
+ {
+ response_headers: [
+ ["Cache-Control", "max-age=1500"],
+ ["Age", "1000"]
+ ]
+ },
+ {
+ request_headers: [
+ ["Cache-Control", "min-fresh=1000"]
+ ],
+ expected_type: "not_cached"
+ }
+ ]
+ },
+ {
+ name: "HTTP cache doesn't reuse fresh response when request contains Cache-Control: no-cache",
+ requests: [
+ {
+ response_headers: [
+ ["Cache-Control", "max-age=3600"]
+ ]
+ },
+ {
+ request_headers: [
+ ["Cache-Control", "no-cache"]
+ ],
+ expected_type: "not_cached"
+ }
+ ]
+ },
+ {
+ name: "HTTP cache validates fresh response with Last-Modified when request contains Cache-Control: no-cache",
+ requests: [
+ {
+ response_headers: [
+ ["Cache-Control", "max-age=3600"],
+ ["Last-Modified", -10000]
+ ]
+ },
+ {
+ request_headers: [
+ ["Cache-Control", "no-cache"]
+ ],
+ expected_type: "lm_validate"
+ }
+ ]
+ },
+ {
+ name: "HTTP cache validates fresh response with ETag when request contains Cache-Control: no-cache",
+ requests: [
+ {
+ response_headers: [
+ ["Cache-Control", "max-age=3600"],
+ ["ETag", http_content("abc")]
+ ]
+ },
+ {
+ request_headers: [
+ ["Cache-Control", "no-cache"]
+ ],
+ expected_type: "etag_validate"
+ }
+ ]
+ },
+ {
+ name: "HTTP cache doesn't reuse fresh response when request contains Cache-Control: no-store",
+ requests: [
+ {
+ response_headers: [
+ ["Cache-Control", "max-age=3600"]
+ ]
+ },
+ {
+ request_headers: [
+ ["Cache-Control", "no-store"]
+ ],
+ expected_type: "not_cached"
+ }
+ ]
+ },
+ {
+ name: "HTTP cache generates 504 status code when nothing is in cache and request contains Cache-Control: only-if-cached",
+ requests: [
+ {
+ request_headers: [
+ ["Cache-Control", "only-if-cached"]
+ ],
+ expected_status: 504,
+ expected_response_text: null
+ }
+ ]
+ }
+];
+run_tests(tests);
diff --git a/test/wpt/tests/fetch/http-cache/credentials.tentative.any.js b/test/wpt/tests/fetch/http-cache/credentials.tentative.any.js
new file mode 100644
index 0000000..3177092
--- /dev/null
+++ b/test/wpt/tests/fetch/http-cache/credentials.tentative.any.js
@@ -0,0 +1,62 @@
+// META: global=window,worker
+// META: title=HTTP Cache - Content
+// META: timeout=long
+// META: script=/common/utils.js
+// META: script=http-cache.js
+
+// This is a tentative test.
+// Firefox behavior is used as expectations.
+//
+// whatwg/fetch issue:
+// https://github.com/whatwg/fetch/issues/1253
+//
+// Chrome design doc:
+// https://docs.google.com/document/d/1lvbiy4n-GM5I56Ncw304sgvY5Td32R6KHitjRXvkZ6U/edit#
+
+const request_cacheable = {
+ request_headers: [],
+ response_headers: [
+ ['Cache-Control', 'max-age=3600'],
+ ],
+ // TODO(arthursonzogni): The behavior is tested only for same-origin requests.
+ // It must behave similarly for cross-site and cross-origin requests. The
+ // problems is the http-cache.js infrastructure returns the
+ // "Server-Request-Count" as HTTP response headers, which aren't readable for
+ // CORS requests.
+ base_url: location.href.replace(/\/[^\/]*$/, '/'),
+};
+
+const request_credentialled = { ...request_cacheable, credentials: 'include', };
+const request_anonymous = { ...request_cacheable, credentials: 'omit', };
+
+const responseIndex = count => {
+ return {
+ expected_response_headers: [
+ ['Server-Request-Count', count.toString()],
+ ],
+ }
+};
+
+var tests = [
+ {
+ name: 'same-origin: 2xAnonymous, 2xCredentialled, 1xAnonymous',
+ requests: [
+ { ...request_anonymous , ...responseIndex(1)} ,
+ { ...request_anonymous , ...responseIndex(1)} ,
+ { ...request_credentialled , ...responseIndex(2)} ,
+ { ...request_credentialled , ...responseIndex(2)} ,
+ { ...request_anonymous , ...responseIndex(1)} ,
+ ]
+ },
+ {
+ name: 'same-origin: 2xCredentialled, 2xAnonymous, 1xCredentialled',
+ requests: [
+ { ...request_credentialled , ...responseIndex(1)} ,
+ { ...request_credentialled , ...responseIndex(1)} ,
+ { ...request_anonymous , ...responseIndex(2)} ,
+ { ...request_anonymous , ...responseIndex(2)} ,
+ { ...request_credentialled , ...responseIndex(1)} ,
+ ]
+ },
+];
+run_tests(tests);
diff --git a/test/wpt/tests/fetch/http-cache/freshness.any.js b/test/wpt/tests/fetch/http-cache/freshness.any.js
new file mode 100644
index 0000000..6b97c82
--- /dev/null
+++ b/test/wpt/tests/fetch/http-cache/freshness.any.js
@@ -0,0 +1,215 @@
+// META: global=window,worker
+// META: title=HTTP Cache - Freshness
+// META: timeout=long
+// META: script=/common/utils.js
+// META: script=/common/get-host-info.sub.js
+// META: script=http-cache.js
+
+var tests = [
+ // response directives
+ {
+ name: "HTTP cache reuses a response with a future Expires",
+ requests: [
+ {
+ response_headers: [
+ ["Expires", (30 * 24 * 60 * 60)]
+ ]
+ },
+ {
+ expected_type: "cached"
+ }
+ ]
+ },
+ {
+ name: "HTTP cache does not reuse a response with a past Expires",
+ requests: [
+ {
+ response_headers: [
+ ["Expires", (-30 * 24 * 60 * 60)]
+ ]
+ },
+ {
+ expected_type: "not_cached"
+ }
+ ]
+ },
+ {
+ name: "HTTP cache does not reuse a response with a present Expires",
+ requests: [
+ {
+ response_headers: [
+ ["Expires", 0]
+ ]
+ },
+ {
+ expected_type: "not_cached"
+ }
+ ]
+ },
+ {
+ name: "HTTP cache does not reuse a response with an invalid Expires",
+ requests: [
+ {
+ response_headers: [
+ ["Expires", "0"]
+ ]
+ },
+ {
+ expected_type: "not_cached"
+ }
+ ]
+ },
+ {
+ name: "HTTP cache reuses a response with positive Cache-Control: max-age",
+ requests: [
+ {
+ response_headers: [
+ ["Cache-Control", "max-age=3600"]
+ ]
+ },
+ {
+ expected_type: "cached"
+ }
+ ]
+ },
+ {
+ name: "HTTP cache does not reuse a response with Cache-Control: max-age=0",
+ requests: [
+ {
+ response_headers: [
+ ["Cache-Control", "max-age=0"]
+ ]
+ },
+ {
+ expected_type: "not_cached"
+ }
+ ]
+ },
+ {
+ name: "HTTP cache reuses a response with positive Cache-Control: max-age and a past Expires",
+ requests: [
+ {
+ response_headers: [
+ ["Cache-Control", "max-age=3600"],
+ ["Expires", -10000]
+ ]
+ },
+ {
+ expected_type: "cached"
+ }
+ ]
+ },
+ {
+ name: "HTTP cache reuses a response with positive Cache-Control: max-age and an invalid Expires",
+ requests: [
+ {
+ response_headers: [
+ ["Cache-Control", "max-age=3600"],
+ ["Expires", "0"]
+ ]
+ },
+ {
+ expected_type: "cached"
+ }
+ ]
+ },
+ {
+ name: "HTTP cache does not reuse a response with Cache-Control: max-age=0 and a future Expires",
+ requests: [
+ {
+ response_headers: [
+ ["Cache-Control", "max-age=0"],
+ ["Expires", 10000]
+ ]
+ },
+ {
+ expected_type: "not_cached"
+ }
+ ]
+ },
+ {
+ name: "HTTP cache does not prefer Cache-Control: s-maxage over Cache-Control: max-age",
+ requests: [
+ {
+ response_headers: [
+ ["Cache-Control", "max-age=1, s-maxage=3600"]
+ ],
+ pause_after: true,
+ },
+ {
+ expected_type: "not_cached"
+ }
+ ]
+ },
+ {
+ name: "HTTP cache does not reuse a response when the Age header is greater than its freshness lifetime",
+ requests: [
+ {
+ response_headers: [
+ ["Cache-Control", "max-age=3600"],
+ ["Age", "12000"]
+ ],
+ },
+ {
+ expected_type: "not_cached"
+ }
+ ]
+ },
+ {
+ name: "HTTP cache does not store a response with Cache-Control: no-store",
+ requests: [
+ {
+ response_headers: [
+ ["Cache-Control", "no-store"]
+ ]
+ },
+ {
+ expected_type: "not_cached"
+ }
+ ]
+ },
+ {
+ name: "HTTP cache does not store a response with Cache-Control: no-store, even with max-age and Expires",
+ requests: [
+ {
+ response_headers: [
+ ["Cache-Control", "max-age=10000, no-store"],
+ ["Expires", 10000]
+ ]
+ },
+ {
+ expected_type: "not_cached"
+ }
+ ]
+ },
+ {
+ name: "HTTP cache stores a response with Cache-Control: no-cache, but revalidates upon use",
+ requests: [
+ {
+ response_headers: [
+ ["Cache-Control", "no-cache"],
+ ["ETag", "abcd"]
+ ]
+ },
+ {
+ expected_type: "etag_validated"
+ }
+ ]
+ },
+ {
+ name: "HTTP cache stores a response with Cache-Control: no-cache, but revalidates upon use, even with max-age and Expires",
+ requests: [
+ {
+ response_headers: [
+ ["Cache-Control", "max-age=10000, no-cache"],
+ ["Expires", 10000],
+ ["ETag", "abcd"]
+ ]
+ },
+ {
+ expected_type: "etag_validated"
+ }
+ ]
+ },
+];
+run_tests(tests);
diff --git a/test/wpt/tests/fetch/http-cache/heuristic.any.js b/test/wpt/tests/fetch/http-cache/heuristic.any.js
new file mode 100644
index 0000000..d846131
--- /dev/null
+++ b/test/wpt/tests/fetch/http-cache/heuristic.any.js
@@ -0,0 +1,93 @@
+// META: global=window,worker
+// META: title=HTTP Cache - Heuristic Freshness
+// META: timeout=long
+// META: script=/common/utils.js
+// META: script=/common/get-host-info.sub.js
+// META: script=http-cache.js
+
+var tests = [
+ {
+ name: "HTTP cache reuses an unknown response with Last-Modified based upon heuristic freshness when Cache-Control: public is present",
+ requests: [
+ {
+ response_status: [299, "Whatever"],
+ response_headers: [
+ ["Last-Modified", (-3 * 100)],
+ ["Cache-Control", "public"]
+ ],
+ },
+ {
+ expected_type: "cached",
+ response_status: [299, "Whatever"]
+ }
+ ]
+ },
+ {
+ name: "HTTP cache does not reuse an unknown response with Last-Modified based upon heuristic freshness when Cache-Control: public is not present",
+ requests: [
+ {
+ response_status: [299, "Whatever"],
+ response_headers: [
+ ["Last-Modified", (-3 * 100)]
+ ],
+ },
+ {
+ expected_type: "not_cached"
+ }
+ ]
+ }
+];
+
+function check_status(status) {
+ var succeed = status[0];
+ var code = status[1];
+ var phrase = status[2];
+ var body = status[3];
+ if (body === undefined) {
+ body = http_content(code);
+ }
+ var expected_type = "not_cached";
+ var desired = "does not use"
+ if (succeed === true) {
+ expected_type = "cached";
+ desired = "reuses";
+ }
+ tests.push(
+ {
+ name: "HTTP cache " + desired + " a " + code + " " + phrase + " response with Last-Modified based upon heuristic freshness",
+ requests: [
+ {
+ response_status: [code, phrase],
+ response_headers: [
+ ["Last-Modified", (-3 * 100)]
+ ],
+ response_body: body
+ },
+ {
+ expected_type: expected_type,
+ response_status: [code, phrase],
+ response_body: body
+ }
+ ]
+ }
+ )
+}
+[
+ [true, 200, "OK"],
+ [true, 203, "Non-Authoritative Information"],
+ [true, 204, "No Content", ""],
+ [true, 404, "Not Found"],
+ [true, 405, "Method Not Allowed"],
+ [true, 410, "Gone"],
+ [true, 414, "URI Too Long"],
+ [true, 501, "Not Implemented"]
+].forEach(check_status);
+[
+ [false, 201, "Created"],
+ [false, 202, "Accepted"],
+ [false, 403, "Forbidden"],
+ [false, 502, "Bad Gateway"],
+ [false, 503, "Service Unavailable"],
+ [false, 504, "Gateway Timeout"],
+].forEach(check_status);
+run_tests(tests);
diff --git a/test/wpt/tests/fetch/http-cache/http-cache.js b/test/wpt/tests/fetch/http-cache/http-cache.js
new file mode 100644
index 0000000..19f1ca9
--- /dev/null
+++ b/test/wpt/tests/fetch/http-cache/http-cache.js
@@ -0,0 +1,274 @@
+/* global btoa fetch token promise_test step_timeout */
+/* global assert_equals assert_true assert_own_property assert_throws_js assert_less_than */
+
+const templates = {
+ 'fresh': {
+ 'response_headers': [
+ ['Expires', 100000],
+ ['Last-Modified', 0]
+ ]
+ },
+ 'stale': {
+ 'response_headers': [
+ ['Expires', -5000],
+ ['Last-Modified', -100000]
+ ]
+ },
+ 'lcl_response': {
+ 'response_headers': [
+ ['Location', 'location_target'],
+ ['Content-Location', 'content_location_target']
+ ]
+ },
+ 'location': {
+ 'query_arg': 'location_target',
+ 'response_headers': [
+ ['Expires', 100000],
+ ['Last-Modified', 0]
+ ]
+ },
+ 'content_location': {
+ 'query_arg': 'content_location_target',
+ 'response_headers': [
+ ['Expires', 100000],
+ ['Last-Modified', 0]
+ ]
+ }
+}
+
+const noBodyStatus = new Set([204, 304])
+
+function makeTest (test) {
+ return function () {
+ var uuid = token()
+ var requests = expandTemplates(test)
+ var fetchFunctions = makeFetchFunctions(requests, uuid)
+ return runTest(fetchFunctions, requests, uuid)
+ }
+}
+
+function makeFetchFunctions(requests, uuid) {
+ var fetchFunctions = []
+ for (let i = 0; i < requests.length; ++i) {
+ fetchFunctions.push({
+ code: function (idx) {
+ var config = requests[idx]
+ var url = makeTestUrl(uuid, config)
+ var init = fetchInit(requests, config)
+ return fetch(url, init)
+ .then(makeCheckResponse(idx, config))
+ .then(makeCheckResponseBody(config, uuid), function (reason) {
+ if ('expected_type' in config && config.expected_type === 'error') {
+ assert_throws_js(TypeError, function () { throw reason })
+ } else {
+ throw reason
+ }
+ })
+ },
+ pauseAfter: 'pause_after' in requests[i]
+ })
+ }
+ return fetchFunctions
+}
+
+function runTest(fetchFunctions, requests, uuid) {
+ var idx = 0
+ function runNextStep () {
+ if (fetchFunctions.length) {
+ var nextFetchFunction = fetchFunctions.shift()
+ if (nextFetchFunction.pauseAfter === true) {
+ return nextFetchFunction.code(idx++)
+ .then(pause)
+ .then(runNextStep)
+ } else {
+ return nextFetchFunction.code(idx++)
+ .then(runNextStep)
+ }
+ } else {
+ return Promise.resolve()
+ }
+ }
+
+ return runNextStep()
+ .then(function () {
+ return getServerState(uuid)
+ }).then(function (testState) {
+ checkRequests(requests, testState)
+ return Promise.resolve()
+ })
+}
+
+function expandTemplates (test) {
+ var rawRequests = test.requests
+ var requests = []
+ for (let i = 0; i < rawRequests.length; i++) {
+ var request = rawRequests[i]
+ request.name = test.name
+ if ('template' in request) {
+ var template = templates[request['template']]
+ for (let member in template) {
+ if (!request.hasOwnProperty(member)) {
+ request[member] = template[member]
+ }
+ }
+ }
+ requests.push(request)
+ }
+ return requests
+}
+
+function fetchInit (requests, config) {
+ var init = {
+ 'headers': []
+ }
+ if ('request_method' in config) init.method = config['request_method']
+ // Note: init.headers must be a copy of config['request_headers'] array,
+ // because new elements are added later.
+ if ('request_headers' in config) init.headers = [...config['request_headers']];
+ if ('name' in config) init.headers.push(['Test-Name', config.name])
+ if ('request_body' in config) init.body = config['request_body']
+ if ('mode' in config) init.mode = config['mode']
+ if ('credentials' in config) init.credentials = config['credentials']
+ if ('cache' in config) init.cache = config['cache']
+ init.headers.push(['Test-Requests', btoa(JSON.stringify(requests))])
+ return init
+}
+
+function makeCheckResponse (idx, config) {
+ return function checkResponse (response) {
+ var reqNum = idx + 1
+ var resNum = parseInt(response.headers.get('Server-Request-Count'))
+ if ('expected_type' in config) {
+ if (config.expected_type === 'error') {
+ assert_true(false, `Request ${reqNum} doesn't throw an error`)
+ return response.text()
+ }
+ if (config.expected_type === 'cached') {
+ assert_less_than(resNum, reqNum, `Response ${reqNum} does not come from cache`)
+ }
+ if (config.expected_type === 'not_cached') {
+ assert_equals(resNum, reqNum, `Response ${reqNum} comes from cache`)
+ }
+ }
+ if ('expected_status' in config) {
+ assert_equals(response.status, config.expected_status,
+ `Response ${reqNum} status is ${response.status}, not ${config.expected_status}`)
+ } else if ('response_status' in config) {
+ assert_equals(response.status, config.response_status[0],
+ `Response ${reqNum} status is ${response.status}, not ${config.response_status[0]}`)
+ } else {
+ assert_equals(response.status, 200, `Response ${reqNum} status is ${response.status}, not 200`)
+ }
+ if ('response_headers' in config) {
+ config.response_headers.forEach(function (header) {
+ if (header.len < 3 || header[2] === true) {
+ assert_equals(response.headers.get(header[0]), header[1],
+ `Response ${reqNum} header ${header[0]} is "${response.headers.get(header[0])}", not "${header[1]}"`)
+ }
+ })
+ }
+ if ('expected_response_headers' in config) {
+ config.expected_response_headers.forEach(function (header) {
+ assert_equals(response.headers.get(header[0]), header[1],
+ `Response ${reqNum} header ${header[0]} is "${response.headers.get(header[0])}", not "${header[1]}"`)
+ })
+ }
+ return response.text()
+ }
+}
+
+function makeCheckResponseBody (config, uuid) {
+ return function checkResponseBody (resBody) {
+ var statusCode = 200
+ if ('response_status' in config) {
+ statusCode = config.response_status[0]
+ }
+ if ('expected_response_text' in config) {
+ if (config.expected_response_text !== null) {
+ assert_equals(resBody, config.expected_response_text,
+ `Response body is "${resBody}", not expected "${config.expected_response_text}"`)
+ }
+ } else if ('response_body' in config && config.response_body !== null) {
+ assert_equals(resBody, config.response_body,
+ `Response body is "${resBody}", not sent "${config.response_body}"`)
+ } else if (!noBodyStatus.has(statusCode)) {
+ assert_equals(resBody, uuid, `Response body is "${resBody}", not default "${uuid}"`)
+ }
+ }
+}
+
+function checkRequests (requests, testState) {
+ var testIdx = 0
+ for (let i = 0; i < requests.length; ++i) {
+ var expectedValidatingHeaders = []
+ var config = requests[i]
+ var serverRequest = testState[testIdx]
+ var reqNum = i + 1
+ if ('expected_type' in config) {
+ if (config.expected_type === 'cached') continue // the server will not see the request
+ if (config.expected_type === 'etag_validated') {
+ expectedValidatingHeaders.push('if-none-match')
+ }
+ if (config.expected_type === 'lm_validated') {
+ expectedValidatingHeaders.push('if-modified-since')
+ }
+ }
+ testIdx++
+ expectedValidatingHeaders.forEach(vhdr => {
+ assert_own_property(serverRequest.request_headers, vhdr,
+ `request ${reqNum} doesn't have ${vhdr} header`)
+ })
+ if ('expected_request_headers' in config) {
+ config.expected_request_headers.forEach(expectedHdr => {
+ assert_equals(serverRequest.request_headers[expectedHdr[0].toLowerCase()], expectedHdr[1],
+ `request ${reqNum} header ${expectedHdr[0]} value is "${serverRequest.request_headers[expectedHdr[0].toLowerCase()]}", not "${expectedHdr[1]}"`)
+ })
+ }
+ }
+}
+
+function pause () {
+ return new Promise(function (resolve, reject) {
+ step_timeout(function () {
+ return resolve()
+ }, 3000)
+ })
+}
+
+function makeTestUrl (uuid, config) {
+ var arg = ''
+ var base_url = ''
+ if ('base_url' in config) {
+ base_url = config.base_url
+ }
+ if ('query_arg' in config) {
+ arg = `&target=${config.query_arg}`
+ }
+ return `${base_url}resources/http-cache.py?dispatch=test&uuid=${uuid}${arg}`
+}
+
+function getServerState (uuid) {
+ return fetch(`resources/http-cache.py?dispatch=state&uuid=${uuid}`)
+ .then(function (response) {
+ return response.text()
+ }).then(function (text) {
+ return JSON.parse(text) || []
+ })
+}
+
+function run_tests (tests) {
+ tests.forEach(function (test) {
+ promise_test(makeTest(test), test.name)
+ })
+}
+
+var contentStore = {}
+function http_content (csKey) {
+ if (csKey in contentStore) {
+ return contentStore[csKey]
+ } else {
+ var content = btoa(Math.random() * Date.now())
+ contentStore[csKey] = content
+ return content
+ }
+}
diff --git a/test/wpt/tests/fetch/http-cache/invalidate.any.js b/test/wpt/tests/fetch/http-cache/invalidate.any.js
new file mode 100644
index 0000000..9f8090a
--- /dev/null
+++ b/test/wpt/tests/fetch/http-cache/invalidate.any.js
@@ -0,0 +1,235 @@
+// META: global=window,worker
+// META: title=HTTP Cache - Invalidation
+// META: timeout=long
+// META: script=/common/utils.js
+// META: script=/common/get-host-info.sub.js
+// META: script=http-cache.js
+
+var tests = [
+ {
+ name: 'HTTP cache invalidates after a successful response from a POST',
+ requests: [
+ {
+ template: "fresh"
+ }, {
+ request_method: "POST",
+ request_body: "abc"
+ }, {
+ expected_type: "not_cached"
+ }
+ ]
+ },
+ {
+ name: 'HTTP cache does not invalidate after a failed response from an unsafe request',
+ requests: [
+ {
+ template: "fresh"
+ }, {
+ request_method: "POST",
+ request_body: "abc",
+ response_status: [500, "Internal Server Error"]
+ }, {
+ expected_type: "cached"
+ }
+ ]
+ },
+ {
+ name: 'HTTP cache invalidates after a successful response from a PUT',
+ requests: [
+ {
+ template: "fresh"
+ }, {
+ template: "fresh",
+ request_method: "PUT",
+ request_body: "abc"
+ }, {
+ expected_type: "not_cached"
+ }
+ ]
+ },
+ {
+ name: 'HTTP cache invalidates after a successful response from a DELETE',
+ requests: [
+ {
+ template: "fresh"
+ }, {
+ request_method: "DELETE",
+ request_body: "abc"
+ }, {
+ expected_type: "not_cached"
+ }
+ ]
+ },
+ {
+ name: 'HTTP cache invalidates after a successful response from an unknown method',
+ requests: [
+ {
+ template: "fresh"
+ }, {
+ request_method: "FOO",
+ request_body: "abc"
+ }, {
+ expected_type: "not_cached"
+ }
+ ]
+ },
+
+
+ {
+ name: 'HTTP cache invalidates Location URL after a successful response from a POST',
+ requests: [
+ {
+ template: "location"
+ }, {
+ request_method: "POST",
+ request_body: "abc",
+ template: "lcl_response"
+ }, {
+ template: "location",
+ expected_type: "not_cached"
+ }
+ ]
+ },
+ {
+ name: 'HTTP cache does not invalidate Location URL after a failed response from an unsafe request',
+ requests: [
+ {
+ template: "location"
+ }, {
+ template: "lcl_response",
+ request_method: "POST",
+ request_body: "abc",
+ response_status: [500, "Internal Server Error"]
+ }, {
+ template: "location",
+ expected_type: "cached"
+ }
+ ]
+ },
+ {
+ name: 'HTTP cache invalidates Location URL after a successful response from a PUT',
+ requests: [
+ {
+ template: "location"
+ }, {
+ template: "lcl_response",
+ request_method: "PUT",
+ request_body: "abc"
+ }, {
+ template: "location",
+ expected_type: "not_cached"
+ }
+ ]
+ },
+ {
+ name: 'HTTP cache invalidates Location URL after a successful response from a DELETE',
+ requests: [
+ {
+ template: "location"
+ }, {
+ template: "lcl_response",
+ request_method: "DELETE",
+ request_body: "abc"
+ }, {
+ template: "location",
+ expected_type: "not_cached"
+ }
+ ]
+ },
+ {
+ name: 'HTTP cache invalidates Location URL after a successful response from an unknown method',
+ requests: [
+ {
+ template: "location"
+ }, {
+ template: "lcl_response",
+ request_method: "FOO",
+ request_body: "abc"
+ }, {
+ template: "location",
+ expected_type: "not_cached"
+ }
+ ]
+ },
+
+
+
+ {
+ name: 'HTTP cache invalidates Content-Location URL after a successful response from a POST',
+ requests: [
+ {
+ template: "content_location"
+ }, {
+ request_method: "POST",
+ request_body: "abc",
+ template: "lcl_response"
+ }, {
+ template: "content_location",
+ expected_type: "not_cached"
+ }
+ ]
+ },
+ {
+ name: 'HTTP cache does not invalidate Content-Location URL after a failed response from an unsafe request',
+ requests: [
+ {
+ template: "content_location"
+ }, {
+ template: "lcl_response",
+ request_method: "POST",
+ request_body: "abc",
+ response_status: [500, "Internal Server Error"]
+ }, {
+ template: "content_location",
+ expected_type: "cached"
+ }
+ ]
+ },
+ {
+ name: 'HTTP cache invalidates Content-Location URL after a successful response from a PUT',
+ requests: [
+ {
+ template: "content_location"
+ }, {
+ template: "lcl_response",
+ request_method: "PUT",
+ request_body: "abc"
+ }, {
+ template: "content_location",
+ expected_type: "not_cached"
+ }
+ ]
+ },
+ {
+ name: 'HTTP cache invalidates Content-Location URL after a successful response from a DELETE',
+ requests: [
+ {
+ template: "content_location"
+ }, {
+ template: "lcl_response",
+ request_method: "DELETE",
+ request_body: "abc"
+ }, {
+ template: "content_location",
+ expected_type: "not_cached"
+ }
+ ]
+ },
+ {
+ name: 'HTTP cache invalidates Content-Location URL after a successful response from an unknown method',
+ requests: [
+ {
+ template: "content_location"
+ }, {
+ template: "lcl_response",
+ request_method: "FOO",
+ request_body: "abc"
+ }, {
+ template: "content_location",
+ expected_type: "not_cached"
+ }
+ ]
+ }
+
+];
+run_tests(tests);
diff --git a/test/wpt/tests/fetch/http-cache/partial.any.js b/test/wpt/tests/fetch/http-cache/partial.any.js
new file mode 100644
index 0000000..3f23b59
--- /dev/null
+++ b/test/wpt/tests/fetch/http-cache/partial.any.js
@@ -0,0 +1,208 @@
+// META: global=window,worker
+// META: title=HTTP Cache - Partial Content
+// META: timeout=long
+// META: script=/common/utils.js
+// META: script=/common/get-host-info.sub.js
+// META: script=http-cache.js
+
+var tests = [
+ {
+ name: "HTTP cache stores partial content and reuses it",
+ requests: [
+ {
+ request_headers: [
+ ['Range', "bytes=-5"]
+ ],
+ response_status: [206, "Partial Content"],
+ response_headers: [
+ ["Cache-Control", "max-age=3600"],
+ ["Content-Range", "bytes 4-9/10"]
+ ],
+ response_body: "01234",
+ expected_request_headers: [
+ ["Range", "bytes=-5"]
+ ]
+ },
+ {
+ request_headers: [
+ ["Range", "bytes=-5"]
+ ],
+ expected_type: "cached",
+ expected_status: 206,
+ expected_response_text: "01234"
+ }
+ ]
+ },
+ {
+ name: "HTTP cache stores complete response and serves smaller ranges from it (byte-range-spec)",
+ requests: [
+ {
+ response_headers: [
+ ["Cache-Control", "max-age=3600"]
+ ],
+ response_body: "01234567890"
+ },
+ {
+ request_headers: [
+ ['Range', "bytes=0-1"]
+ ],
+ expected_type: "cached",
+ expected_status: 206,
+ expected_response_text: "01"
+ },
+ ]
+ },
+ {
+ name: "HTTP cache stores complete response and serves smaller ranges from it (absent last-byte-pos)",
+ requests: [
+ {
+ response_headers: [
+ ["Cache-Control", "max-age=3600"],
+ ],
+ response_body: "01234567890"
+ },
+ {
+ request_headers: [
+ ['Range', "bytes=1-"]
+ ],
+ expected_type: "cached",
+ expected_status: 206,
+ expected_response_text: "1234567890"
+ }
+ ]
+ },
+ {
+ name: "HTTP cache stores complete response and serves smaller ranges from it (suffix-byte-range-spec)",
+ requests: [
+ {
+ response_headers: [
+ ["Cache-Control", "max-age=3600"],
+ ],
+ response_body: "0123456789A"
+ },
+ {
+ request_headers: [
+ ['Range', "bytes=-1"]
+ ],
+ expected_type: "cached",
+ expected_status: 206,
+ expected_response_text: "A"
+ }
+ ]
+ },
+ {
+ name: "HTTP cache stores complete response and serves smaller ranges from it with only-if-cached",
+ requests: [
+ {
+ response_headers: [
+ ["Cache-Control", "max-age=3600"]
+ ],
+ response_body: "01234567890"
+ },
+ {
+ request_headers: [
+ ['Range', "bytes=0-1"]
+ ],
+ mode: "same-origin",
+ cache: "only-if-cached",
+ expected_type: "cached",
+ expected_status: 206,
+ expected_response_text: "01"
+ },
+ ]
+ },
+ {
+ name: "HTTP cache stores partial response and serves smaller ranges from it (byte-range-spec)",
+ requests: [
+ {
+ request_headers: [
+ ['Range', "bytes=-5"]
+ ],
+ response_status: [206, "Partial Content"],
+ response_headers: [
+ ["Cache-Control", "max-age=3600"],
+ ["Content-Range", "bytes 4-9/10"]
+ ],
+ response_body: "01234"
+ },
+ {
+ request_headers: [
+ ['Range', "bytes=6-8"]
+ ],
+ expected_type: "cached",
+ expected_status: 206,
+ expected_response_text: "234"
+ }
+ ]
+ },
+ {
+ name: "HTTP cache stores partial response and serves smaller ranges from it (absent last-byte-pos)",
+ requests: [
+ {
+ request_headers: [
+ ['Range', "bytes=-5"]
+ ],
+ response_status: [206, "Partial Content"],
+ response_headers: [
+ ["Cache-Control", "max-age=3600"],
+ ["Content-Range", "bytes 4-9/10"]
+ ],
+ response_body: "01234"
+ },
+ {
+ request_headers: [
+ ["Range", "bytes=6-"]
+ ],
+ expected_type: "cached",
+ expected_status: 206,
+ expected_response_text: "234"
+ }
+ ]
+ },
+ {
+ name: "HTTP cache stores partial response and serves smaller ranges from it (suffix-byte-range-spec)",
+ requests: [
+ {
+ request_headers: [
+ ['Range', "bytes=-5"]
+ ],
+ response_status: [206, "Partial Content"],
+ response_headers: [
+ ["Cache-Control", "max-age=3600"],
+ ["Content-Range", "bytes 4-9/10"]
+ ],
+ response_body: "01234"
+ },
+ {
+ request_headers: [
+ ['Range', "bytes=-1"]
+ ],
+ expected_type: "cached",
+ expected_status: 206,
+ expected_response_text: "4"
+ }
+ ]
+ },
+ {
+ name: "HTTP cache stores partial content and completes it",
+ requests: [
+ {
+ request_headers: [
+ ['Range', "bytes=-5"]
+ ],
+ response_status: [206, "Partial Content"],
+ response_headers: [
+ ["Cache-Control", "max-age=3600"],
+ ["Content-Range", "bytes 0-4/10"]
+ ],
+ response_body: "01234"
+ },
+ {
+ expected_request_headers: [
+ ["range", "bytes=5-"]
+ ]
+ }
+ ]
+ },
+];
+run_tests(tests);
diff --git a/test/wpt/tests/fetch/http-cache/post-patch.any.js b/test/wpt/tests/fetch/http-cache/post-patch.any.js
new file mode 100644
index 0000000..0a69baa
--- /dev/null
+++ b/test/wpt/tests/fetch/http-cache/post-patch.any.js
@@ -0,0 +1,46 @@
+// META: global=window,worker
+// META: title=HTTP Cache - Caching POST and PATCH responses
+// META: timeout=long
+// META: script=/common/utils.js
+// META: script=/common/get-host-info.sub.js
+// META: script=http-cache.js
+
+var tests = [
+ {
+ name: "HTTP cache uses content after PATCH request with response containing Content-Location and cache-allowing header",
+ requests: [
+ {
+ request_method: "PATCH",
+ request_body: "abc",
+ response_status: [200, "OK"],
+ response_headers: [
+ ['Cache-Control', "private, max-age=1000"],
+ ['Content-Location', ""]
+ ],
+ response_body: "abc"
+ },
+ {
+ expected_type: "cached"
+ }
+ ]
+ },
+ {
+ name: "HTTP cache uses content after POST request with response containing Content-Location and cache-allowing header",
+ requests: [
+ {
+ request_method: "POST",
+ request_body: "abc",
+ response_status: [200, "OK"],
+ response_headers: [
+ ['Cache-Control', "private, max-age=1000"],
+ ['Content-Location', ""]
+ ],
+ response_body: "abc"
+ },
+ {
+ expected_type: "cached"
+ }
+ ]
+ }
+];
+run_tests(tests);
diff --git a/test/wpt/tests/fetch/http-cache/resources/http-cache.py b/test/wpt/tests/fetch/http-cache/resources/http-cache.py
new file mode 100644
index 0000000..3ab610d
--- /dev/null
+++ b/test/wpt/tests/fetch/http-cache/resources/http-cache.py
@@ -0,0 +1,124 @@
+import datetime
+import json
+import time
+from base64 import b64decode
+
+from wptserve.utils import isomorphic_decode, isomorphic_encode
+
+NOTEHDRS = set([u'content-type', u'access-control-allow-origin', u'last-modified', u'etag'])
+NOBODYSTATUS = set([204, 304])
+LOCATIONHDRS = set([u'location', u'content-location'])
+DATEHDRS = set([u'date', u'expires', u'last-modified'])
+
+def main(request, response):
+ dispatch = request.GET.first(b"dispatch", None)
+ uuid = request.GET.first(b"uuid", None)
+ response.headers.set(b"Access-Control-Allow-Credentials", b"true")
+
+ if request.method == u"OPTIONS":
+ return handle_preflight(uuid, request, response)
+ if not uuid:
+ response.status = (404, b"Not Found")
+ response.headers.set(b"Content-Type", b"text/plain")
+ return b"UUID not found"
+ if dispatch == b'test':
+ return handle_test(uuid, request, response)
+ elif dispatch == b'state':
+ return handle_state(uuid, request, response)
+ response.status = (404, b"Not Found")
+ response.headers.set(b"Content-Type", b"text/plain")
+ return b"Fallthrough"
+
+def handle_preflight(uuid, request, response):
+ response.status = (200, b"OK")
+ response.headers.set(b"Access-Control-Allow-Origin", request.headers.get(b"origin") or '*')
+ response.headers.set(b"Access-Control-Allow-Methods", b"GET")
+ response.headers.set(b"Access-Control-Allow-Headers", request.headers.get(b"Access-Control-Request-Headers") or "*")
+ response.headers.set(b"Access-Control-Max-Age", b"86400")
+ return b"Preflight request"
+
+def handle_state(uuid, request, response):
+ response.headers.set(b"Content-Type", b"text/plain")
+ return json.dumps(request.server.stash.take(uuid))
+
+def handle_test(uuid, request, response):
+ server_state = request.server.stash.take(uuid) or []
+ try:
+ requests = json.loads(b64decode(request.headers.get(b'Test-Requests', b"")))
+ except:
+ response.status = (400, b"Bad Request")
+ response.headers.set(b"Content-Type", b"text/plain")
+ return b"No or bad Test-Requests request header"
+ config = requests[len(server_state)]
+ if not config:
+ response.status = (404, b"Not Found")
+ response.headers.set(b"Content-Type", b"text/plain")
+ return b"Config not found"
+ noted_headers = {}
+ now = time.time()
+ for header in config.get(u'response_headers', []):
+ if header[0].lower() in LOCATIONHDRS: # magic locations
+ if (len(header[1]) > 0):
+ header[1] = u"%s&target=%s" % (request.url, header[1])
+ else:
+ header[1] = request.url
+ if header[0].lower() in DATEHDRS and isinstance(header[1], int): # magic dates
+ header[1] = http_date(now, header[1])
+ response.headers.set(isomorphic_encode(header[0]), isomorphic_encode(header[1]))
+ if header[0].lower() in NOTEHDRS:
+ noted_headers[header[0].lower()] = header[1]
+ state = {
+ u'now': now,
+ u'request_method': request.method,
+ u'request_headers': dict([[isomorphic_decode(h.lower()), isomorphic_decode(request.headers[h])] for h in request.headers]),
+ u'response_headers': noted_headers
+ }
+ server_state.append(state)
+ request.server.stash.put(uuid, server_state)
+
+ if u"access-control-allow-origin" not in noted_headers:
+ response.headers.set(b"Access-Control-Allow-Origin", b"*")
+ if u"content-type" not in noted_headers:
+ response.headers.set(b"Content-Type", b"text/plain")
+ response.headers.set(b"Server-Request-Count", len(server_state))
+
+ code, phrase = config.get(u"response_status", [200, b"OK"])
+ if config.get(u"expected_type", u"").endswith(u'validated'):
+ ref_hdrs = server_state[0][u'response_headers']
+ previous_lm = ref_hdrs.get(u'last-modified', False)
+ if previous_lm and request.headers.get(b"If-Modified-Since", False) == isomorphic_encode(previous_lm):
+ code, phrase = [304, b"Not Modified"]
+ previous_etag = ref_hdrs.get(u'etag', False)
+ if previous_etag and request.headers.get(b"If-None-Match", False) == isomorphic_encode(previous_etag):
+ code, phrase = [304, b"Not Modified"]
+ if code != 304:
+ code, phrase = [999, b'304 Not Generated']
+ response.status = (code, phrase)
+
+ content = config.get(u"response_body", uuid)
+ if code in NOBODYSTATUS:
+ return b""
+ return content
+
+
+def get_header(headers, header_name):
+ result = None
+ for header in headers:
+ if header[0].lower() == header_name.lower():
+ result = header[1]
+ return result
+
+WEEKDAYS = [u'Mon', u'Tue', u'Wed', u'Thu', u'Fri', u'Sat', u'Sun']
+MONTHS = [None, u'Jan', u'Feb', u'Mar', u'Apr', u'May', u'Jun', u'Jul',
+ u'Aug', u'Sep', u'Oct', u'Nov', u'Dec']
+
+def http_date(now, delta_secs=0):
+ date = datetime.datetime.utcfromtimestamp(now + delta_secs)
+ return u"%s, %.2d %s %.4d %.2d:%.2d:%.2d GMT" % (
+ WEEKDAYS[date.weekday()],
+ date.day,
+ MONTHS[date.month],
+ date.year,
+ date.hour,
+ date.minute,
+ date.second)
diff --git a/test/wpt/tests/fetch/http-cache/resources/securedimage.py b/test/wpt/tests/fetch/http-cache/resources/securedimage.py
new file mode 100644
index 0000000..cac9cfe
--- /dev/null
+++ b/test/wpt/tests/fetch/http-cache/resources/securedimage.py
@@ -0,0 +1,19 @@
+# -*- coding: utf-8 -
+
+from wptserve.utils import isomorphic_decode, isomorphic_encode
+
+def main(request, response):
+ image_url = str.replace(request.url, u"fetch/http-cache/resources/securedimage.py", u"images/green.png")
+
+ if b"authorization" not in request.headers:
+ response.status = 401
+ response.headers.set(b"WWW-Authenticate", b"Basic")
+ return
+ else:
+ auth = request.headers.get(b"Authorization")
+ if auth != b"Basic dGVzdHVzZXI6dGVzdHBhc3M=":
+ response.set_error(403, u"Invalid username or password - " + isomorphic_decode(auth))
+ return
+
+ response.status = 301
+ response.headers.set(b"Location", isomorphic_encode(image_url))
diff --git a/test/wpt/tests/fetch/http-cache/resources/split-cache-popup-with-iframe.html b/test/wpt/tests/fetch/http-cache/resources/split-cache-popup-with-iframe.html
new file mode 100644
index 0000000..48b1618
--- /dev/null
+++ b/test/wpt/tests/fetch/http-cache/resources/split-cache-popup-with-iframe.html
@@ -0,0 +1,34 @@
+<!doctype html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>HTTP Cache - helper</title>
+ <meta name="help" href="https://fetch.spec.whatwg.org/#http-cache-partitions">
+ <meta name="timeout" content="normal">
+ <script src="/resources/testharness.js"></script>
+ <script src="/common/get-host-info.sub.js"></script>
+</head>
+<body>
+<script>
+ const host = get_host_info();
+
+ // Create iframe that is same-origin to the opener.
+ var iframe = document.createElement("iframe");
+ iframe.src = host.HTTP_ORIGIN + window.location.pathname.replace(/\/[^\/]*$/, '/') + "split-cache-popup.html";
+ document.body.appendChild(iframe);
+
+ window.addEventListener("message", function listener(event) {
+ if (event.origin !== host.HTTP_ORIGIN) {
+ // Ignore messages not from the iframe or opener
+ return;
+ } else if (typeof(event.data) === "object") {
+ // This message came from the opener, pass it on to the iframe
+ iframe.contentWindow.postMessage(event.data, host.HTTP_ORIGIN);
+ } else if (typeof(event.data) === "string") {
+ // This message came from the iframe, pass it on to the opener
+ window.opener.postMessage(event.data, host.HTTP_ORIGIN);
+ }
+ })
+</script>
+</body>
+</html>
diff --git a/test/wpt/tests/fetch/http-cache/resources/split-cache-popup.html b/test/wpt/tests/fetch/http-cache/resources/split-cache-popup.html
new file mode 100644
index 0000000..edb5794
--- /dev/null
+++ b/test/wpt/tests/fetch/http-cache/resources/split-cache-popup.html
@@ -0,0 +1,28 @@
+<!doctype html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>HTTP Cache - helper</title>
+ <meta name="help" href="https://fetch.spec.whatwg.org/#http-cache-partitions">
+ <meta name="timeout" content="normal">
+ <script src="/resources/testharness.js"></script>
+ <script src="../http-cache.js"></script>
+</head>
+<body>
+<script>
+ window.addEventListener("message", function listener(event) {
+ window.removeEventListener("message", listener)
+
+ var fetchFunction = makeFetchFunctions(event.data.requests, event.data.uuid)[event.data.index]
+ fetchFunction.code(event.data.index).then(
+ function(response) {
+ event.source.postMessage("success", event.origin)
+ },
+ function(response) {
+ event.source.postMessage("error", event.origin)
+ }
+ )
+ })
+</script>
+</body>
+</html>
diff --git a/test/wpt/tests/fetch/http-cache/split-cache.html b/test/wpt/tests/fetch/http-cache/split-cache.html
new file mode 100644
index 0000000..fe93d2e
--- /dev/null
+++ b/test/wpt/tests/fetch/http-cache/split-cache.html
@@ -0,0 +1,158 @@
+<!doctype html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>HTTP Cache - Partioning by site</title>
+ <meta name="help" href="https://fetch.spec.whatwg.org/#http-cache-partitions">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/common/utils.js"></script>
+ <script src="/common/get-host-info.sub.js"></script>
+ <script src="http-cache.js"></script>
+</head>
+<body>
+<script>
+const host = get_host_info();
+
+// We run this entire test four times, varying the following two booleans:
+// - is_cross_site_test, which controls whether the popup is cross-site.
+// - load_resource_in_iframe, which controls whether the popup loads the
+// resource in an iframe or the top-level frame. Note that the iframe is
+// always same-site to the opener.
+function performFullTest(is_cross_site_test, load_resource_in_iframe, name) {
+ const POPUP_HTTP_ORIGIN = is_cross_site_test ? host.HTTP_NOTSAMESITE_ORIGIN : host.HTTP_ORIGIN
+ const LOCAL_HTTP_ORIGIN = host.HTTP_ORIGIN
+
+ const popupBaseURL = POPUP_HTTP_ORIGIN + window.location.pathname.replace(/\/[^\/]*$/, '/') ;
+ const localBaseURL = LOCAL_HTTP_ORIGIN + window.location.pathname.replace(/\/[^\/]*$/, '/') ;
+
+ // Note: Navigation requests are requested with credentials. Make sure the
+ // fetch requests are also requested with credentials. This ensures passing
+ // this test is not simply the consequence of discriminating anonymous and
+ // credentialled request in the HTTP cache.
+ //
+ // See https://github.com/whatwg/fetch/issues/1253
+ var test = {
+ requests: [
+ {
+ response_headers: [
+ ["Expires", (30 * 24 * 60 * 60)],
+ ["Access-Control-Allow-Origin", POPUP_HTTP_ORIGIN],
+ ],
+ base_url: localBaseURL,
+ credentials: "include",
+ },
+ {
+ response_headers: [
+ ["Access-Control-Allow-Origin", POPUP_HTTP_ORIGIN],
+ ],
+ base_url: localBaseURL,
+ credentials: "include",
+ },
+ {
+ request_headers: [
+ ["Cache-Control", "no-cache"]
+ ],
+ response_headers: [
+ ["Access-Control-Allow-Origin", POPUP_HTTP_ORIGIN],
+ ],
+ // If the popup's request was a cache hit, we would only expect 2
+ // requests to the server. If it was a cache miss, we would expect 3.
+ // load_resource_in_iframe does not affect the expectation as, even
+ // though the iframe (if present) is same-site, we expect a cache miss
+ // when the popup's top-level frame is a different site.
+ expected_response_headers: [
+ ["server-request-count", is_cross_site_test ? "3" : "2"]
+ ],
+ base_url: localBaseURL,
+ credentials: "include",
+ }
+ ]
+ }
+
+ var uuid = token()
+ var local_requests = expandTemplates(test)
+ var fetchFns = makeFetchFunctions(local_requests, uuid)
+
+ var popup_requests = expandTemplates(test)
+
+ // Request the resource with a long cache expiry
+ function local_fetch() {
+ return fetchFns[0].code(0)
+ }
+
+ function popup_fetch() {
+ return new Promise(function(resolve, reject) {
+ var relativeUrl = load_resource_in_iframe
+ ? "resources/split-cache-popup-with-iframe.html"
+ : "resources/split-cache-popup.html";
+ var win = window.open(popupBaseURL + relativeUrl);
+
+ // Post a message to initiate the popup's request and give the necessary
+ // information. Posted repeatedly to account for dropped messages as the
+ // popup is loading.
+ function postMessage(event) {
+ var payload = {
+ index: 1,
+ requests: popup_requests,
+ uuid: uuid
+ }
+ win.postMessage(payload, POPUP_HTTP_ORIGIN)
+ }
+ var messagePoster = setInterval(postMessage, 100)
+
+ // Listen for the result
+ function messageListener(event) {
+ if (event.origin !== POPUP_HTTP_ORIGIN) {
+ reject("Unknown error")
+ } else if (event.data === "success") {
+ resolve()
+ } else if (event.data === "error") {
+ reject("Error in popup")
+ } else {
+ return; // Ignore testharness.js internal messages
+ }
+ window.removeEventListener("message", messageListener)
+ clearInterval(messagePoster)
+ win.close()
+ }
+ window.addEventListener("message", messageListener)
+ })
+ }
+
+ function local_fetch2() {
+ return fetchFns[2].code(2)
+ }
+
+ // Final checks.
+ function check_server_info() {
+ return getServerState(uuid)
+ .then(function (testState) {
+ checkRequests(local_requests, testState)
+ return Promise.resolve()
+ })
+ }
+
+ promise_test(() => {
+ return local_fetch()
+ .then(popup_fetch)
+ .then(local_fetch2)
+ .then(check_server_info)
+ }, name)
+}
+
+performFullTest(
+ false /* is_cross_site_test */, false /* load_resource_in_iframe */,
+ "HTTP cache is shared between same-site top-level frames");
+performFullTest(
+ true /* is_cross_site_test */, false /* load_resource_in_iframe */,
+ "HTTP cache is not shared between cross-site top-level frames");
+performFullTest(
+ false /* is_cross_site_test */, true /* load_resource_in_iframe */,
+ "HTTP cache is shared between same-site frames with same-site top-level frames");
+performFullTest(
+ true /* is_cross_site_test */, true /* load_resource_in_iframe */,
+ "HTTP cache is not shared between same-site frames with cross-site top-level frames");
+</script>
+</body>
+</html>
diff --git a/test/wpt/tests/fetch/http-cache/status.any.js b/test/wpt/tests/fetch/http-cache/status.any.js
new file mode 100644
index 0000000..10c83a2
--- /dev/null
+++ b/test/wpt/tests/fetch/http-cache/status.any.js
@@ -0,0 +1,60 @@
+// META: global=window,worker
+// META: title=HTTP Cache - Status Codes
+// META: timeout=long
+// META: script=/common/utils.js
+// META: script=/common/get-host-info.sub.js
+// META: script=http-cache.js
+
+var tests = [];
+function check_status(status) {
+ var code = status[0];
+ var phrase = status[1];
+ var body = status[2];
+ if (body === undefined) {
+ body = http_content(code);
+ }
+ tests.push({
+ name: "HTTP cache goes to the network if it has a stale " + code + " response",
+ requests: [
+ {
+ template: "stale",
+ response_status: [code, phrase],
+ response_body: body
+ }, {
+ expected_type: "not_cached",
+ response_status: [code, phrase],
+ response_body: body
+ }
+ ]
+ })
+ tests.push({
+ name: "HTTP cache avoids going to the network if it has a fresh " + code + " response",
+ requests: [
+ {
+ template: "fresh",
+ response_status: [code, phrase],
+ response_body: body
+ }, {
+ expected_type: "cached",
+ response_status: [code, phrase],
+ response_body: body
+ }
+ ]
+ })
+}
+[
+ [200, "OK"],
+ [203, "Non-Authoritative Information"],
+ [204, "No Content", null],
+ [299, "Whatever"],
+ [400, "Bad Request"],
+ [404, "Not Found"],
+ [410, "Gone"],
+ [499, "Whatever"],
+ [500, "Internal Server Error"],
+ [502, "Bad Gateway"],
+ [503, "Service Unavailable"],
+ [504, "Gateway Timeout"],
+ [599, "Whatever"]
+].forEach(check_status);
+run_tests(tests);
diff --git a/test/wpt/tests/fetch/http-cache/vary.any.js b/test/wpt/tests/fetch/http-cache/vary.any.js
new file mode 100644
index 0000000..2cfd226
--- /dev/null
+++ b/test/wpt/tests/fetch/http-cache/vary.any.js
@@ -0,0 +1,313 @@
+// META: global=window,worker
+// META: title=HTTP Cache - Vary
+// META: timeout=long
+// META: script=/common/utils.js
+// META: script=/common/get-host-info.sub.js
+// META: script=http-cache.js
+
+var tests = [
+ {
+ name: "HTTP cache reuses Vary response when request matches",
+ requests: [
+ {
+ request_headers: [
+ ["Foo", "1"]
+ ],
+ response_headers: [
+ ["Expires", 5000],
+ ["Last-Modified", -3000],
+ ["Vary", "Foo"]
+ ]
+ },
+ {
+ request_headers: [
+ ["Foo", "1"]
+ ],
+ expected_type: "cached"
+ }
+ ]
+ },
+ {
+ name: "HTTP cache doesn't use Vary response when request doesn't match",
+ requests: [
+ {
+ request_headers: [
+ ["Foo", "1"]
+ ],
+ response_headers: [
+ ["Expires", 5000],
+ ["Last-Modified", -3000],
+ ["Vary", "Foo"]
+ ]
+ },
+ {
+ request_headers: [
+ ["Foo", "2"]
+ ],
+ expected_type: "not_cached"
+ }
+ ]
+ },
+ {
+ name: "HTTP cache doesn't use Vary response when request omits variant header",
+ requests: [
+ {
+ request_headers: [
+ ["Foo", "1"]
+ ],
+ response_headers: [
+ ["Expires", 5000],
+ ["Last-Modified", -3000],
+ ["Vary", "Foo"]
+ ]
+ },
+ {
+ expected_type: "not_cached"
+ }
+ ]
+ },
+ {
+ name: "HTTP cache doesn't invalidate existing Vary response",
+ requests: [
+ {
+ request_headers: [
+ ["Foo", "1"]
+ ],
+ response_headers: [
+ ["Expires", 5000],
+ ["Last-Modified", -3000],
+ ["Vary", "Foo"]
+ ],
+ response_body: http_content('foo_1')
+ },
+ {
+ request_headers: [
+ ["Foo", "2"]
+ ],
+ response_headers: [
+ ["Expires", 5000],
+ ["Last-Modified", -3000],
+ ["Vary", "Foo"]
+ ],
+ expected_type: "not_cached",
+ response_body: http_content('foo_2'),
+ },
+ {
+ request_headers: [
+ ["Foo", "1"]
+ ],
+ response_body: http_content('foo_1'),
+ expected_type: "cached"
+ }
+ ]
+ },
+ {
+ name: "HTTP cache doesn't pay attention to headers not listed in Vary",
+ requests: [
+ {
+ request_headers: [
+ ["Foo", "1"],
+ ["Other", "2"]
+ ],
+ response_headers: [
+ ["Expires", 5000],
+ ["Last-Modified", -3000],
+ ["Vary", "Foo"]
+ ],
+ },
+ {
+ request_headers: [
+ ["Foo", "1"],
+ ["Other", "3"]
+ ],
+ expected_type: "cached"
+ }
+ ]
+ },
+ {
+ name: "HTTP cache reuses two-way Vary response when request matches",
+ requests: [
+ {
+ request_headers: [
+ ["Foo", "1"],
+ ["Bar", "abc"]
+ ],
+ response_headers: [
+ ["Expires", 5000],
+ ["Last-Modified", -3000],
+ ["Vary", "Foo, Bar"]
+ ]
+ },
+ {
+ request_headers: [
+ ["Foo", "1"],
+ ["Bar", "abc"]
+ ],
+ expected_type: "cached"
+ }
+ ]
+ },
+ {
+ name: "HTTP cache doesn't use two-way Vary response when request doesn't match",
+ requests: [
+ {
+ request_headers: [
+ ["Foo", "1"],
+ ["Bar", "abc"]
+ ],
+ response_headers: [
+ ["Expires", 5000],
+ ["Last-Modified", -3000],
+ ["Vary", "Foo, Bar"]
+ ]
+ },
+ {
+ request_headers: [
+ ["Foo", "2"],
+ ["Bar", "abc"]
+ ],
+ expected_type: "not_cached"
+ }
+ ]
+ },
+ {
+ name: "HTTP cache doesn't use two-way Vary response when request omits variant header",
+ requests: [
+ {
+ request_headers: [
+ ["Foo", "1"]
+ ],
+ response_headers: [
+ ["Expires", 5000],
+ ["Last-Modified", -3000],
+ ["Vary", "Foo, Bar"]
+ ]
+ },
+ {
+ expected_type: "not_cached"
+ }
+ ]
+ },
+ {
+ name: "HTTP cache reuses three-way Vary response when request matches",
+ requests: [
+ {
+ request_headers: [
+ ["Foo", "1"],
+ ["Bar", "abc"],
+ ["Baz", "789"]
+ ],
+ response_headers: [
+ ["Expires", 5000],
+ ["Last-Modified", -3000],
+ ["Vary", "Foo, Bar, Baz"]
+ ]
+ },
+ {
+ request_headers: [
+ ["Foo", "1"],
+ ["Bar", "abc"],
+ ["Baz", "789"]
+ ],
+ expected_type: "cached"
+ }
+ ]
+ },
+ {
+ name: "HTTP cache doesn't use three-way Vary response when request doesn't match",
+ requests: [
+ {
+ request_headers: [
+ ["Foo", "1"],
+ ["Bar", "abc"],
+ ["Baz", "789"]
+ ],
+ response_headers: [
+ ["Expires", 5000],
+ ["Last-Modified", -3000],
+ ["Vary", "Foo, Bar, Baz"]
+ ]
+ },
+ {
+ request_headers: [
+ ["Foo", "2"],
+ ["Bar", "abc"],
+ ["Baz", "789"]
+ ],
+ expected_type: "not_cached"
+ }
+ ]
+ },
+ {
+ name: "HTTP cache doesn't use three-way Vary response when request doesn't match, regardless of header order",
+ requests: [
+ {
+ request_headers: [
+ ["Foo", "1"],
+ ["Bar", "abc4"],
+ ["Baz", "789"]
+ ],
+ response_headers: [
+ ["Expires", 5000],
+ ["Last-Modified", -3000],
+ ["Vary", "Foo, Bar, Baz"]
+ ]
+ },
+ {
+ request_headers: [
+ ["Foo", "1"],
+ ["Bar", "abc"],
+ ["Baz", "789"]
+ ],
+ expected_type: "not_cached"
+ }
+ ]
+ },
+ {
+ name: "HTTP cache uses three-way Vary response when both request and the original request omited a variant header",
+ requests: [
+ {
+ request_headers: [
+ ["Foo", "1"],
+ ["Baz", "789"]
+ ],
+ response_headers: [
+ ["Expires", 5000],
+ ["Last-Modified", -3000],
+ ["Vary", "Foo, Bar, Baz"]
+ ]
+ },
+ {
+ request_headers: [
+ ["Foo", "1"],
+ ["Baz", "789"]
+ ],
+ expected_type: "cached"
+ }
+ ]
+ },
+ {
+ name: "HTTP cache doesn't use Vary response with a field value of '*'",
+ requests: [
+ {
+ request_headers: [
+ ["Foo", "1"],
+ ["Baz", "789"]
+ ],
+ response_headers: [
+ ["Expires", 5000],
+ ["Last-Modified", -3000],
+ ["Vary", "*"]
+ ]
+ },
+ {
+ request_headers: [
+ ["*", "1"],
+ ["Baz", "789"]
+ ],
+ expected_type: "not_cached"
+ }
+ ]
+ }
+];
+run_tests(tests);
diff --git a/test/wpt/tests/fetch/images/canvas-remote-read-remote-image-redirect.html b/test/wpt/tests/fetch/images/canvas-remote-read-remote-image-redirect.html
new file mode 100644
index 0000000..4a887f3
--- /dev/null
+++ b/test/wpt/tests/fetch/images/canvas-remote-read-remote-image-redirect.html
@@ -0,0 +1,28 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Load a no-cors image from a same-origin URL that redirects to a cross-origin URL that redirects to the initial origin</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script>
+setup({ single_test: true });
+var image = new Image();
+image.onload = function() {
+ const canvas = document.createElement("canvas");
+ canvas.width = 100;
+ canvas.height = 100;
+
+ const context = canvas.getContext("2d");
+ context.drawImage(image, 0, 0, 100, 100);
+
+ assert_throws_dom("SecurityError", () => {
+ context.getImageData(0, 0, 100, 100);
+ });
+ done();
+}
+
+const info = get_host_info();
+const finalURL = get_host_info().HTTP_ORIGIN + "/images/apng.png";
+const intermediateURL = get_host_info().HTTP_REMOTE_ORIGIN + "/fetch/api/resources/redirect.py?location=" + finalURL;
+image.src = "/fetch/api/resources/redirect.py?location=" + encodeURIComponent(intermediateURL);
+</script>
diff --git a/test/wpt/tests/fetch/metadata/META.yml b/test/wpt/tests/fetch/metadata/META.yml
new file mode 100644
index 0000000..85f0a7d
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/META.yml
@@ -0,0 +1,4 @@
+spec: https://w3c.github.io/webappsec-fetch-metadata/
+suggested_reviewers:
+ - mikewest
+ - iVanlIsh
diff --git a/test/wpt/tests/fetch/metadata/README.md b/test/wpt/tests/fetch/metadata/README.md
new file mode 100644
index 0000000..34864d4
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/README.md
@@ -0,0 +1,9 @@
+Fetch Metadata Tests
+====================
+
+This directory contains tests related to the Fetch Metadata proposal:
+
+: Explainer
+:: <https://github.com/w3c/webappsec-fetch-metadata>
+: "Spec"
+:: <https://w3c.github.io/webappsec-fetch-metadata/>
diff --git a/test/wpt/tests/fetch/metadata/audio-worklet.https.html b/test/wpt/tests/fetch/metadata/audio-worklet.https.html
new file mode 100644
index 0000000..3b768ef
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/audio-worklet.https.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=/fetch/metadata/resources/helper.js></script>
+<script src=/common/utils.js></script>
+<script>
+
+ promise_test(async t => {
+ const nonce = token();
+ const key = "worklet-destination" + nonce;
+ const context = new AudioContext();
+
+ await context.audioWorklet.addModule("/fetch/metadata/resources/record-header.py?file=" + key);
+ const expected = {"site": "same-origin", "user": "", "mode": "cors", "dest": "audioworklet"};
+ await validate_expectations(key, expected);
+ }, "The fetch metadata for audio worklet");
+
+</script>
+<body></body>
diff --git a/test/wpt/tests/fetch/metadata/embed.https.sub.tentative.html b/test/wpt/tests/fetch/metadata/embed.https.sub.tentative.html
new file mode 100644
index 0000000..1900dbd
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/embed.https.sub.tentative.html
@@ -0,0 +1,63 @@
+<!DOCTYPE html>
+<meta charset="utf-8"/>
+<link rel="author" href="mtrzos@google.com" title="Maciek Trzos">
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=/fetch/metadata/resources/helper.js></script>
+<script src=/common/utils.js></script>
+<body>
+
+<p>Relevant issue: <a href="https://github.com/whatwg/html/issues/513">
+&lt;embed> should support loading random HTML documents, like &lt;object>
+</a></p>
+
+<script>
+ const nonce = token();
+
+ const origins = {
+ "same-origin": "https://{{host}}:{{ports[https][0]}}",
+ "same-site": "https://{{hosts[][www]}}:{{ports[https][0]}}",
+ "cross-site": "https://{{hosts[alt][www]}}:{{ports[https][0]}}",
+ };
+
+ for (let site in origins) {
+ promise_test(t => {
+ return new Promise((resolve, reject) => {
+ let key = "embed-" + site + "-" + nonce;
+
+ let el = document.createElement('embed');
+ el.src = origins[site] + "/fetch/metadata/resources/record-header.py?file=" + key;
+ el.onload = _ => {
+ let expected = {"dest": "embed", "site": site, "user": "", "mode": "navigate"};
+ validate_expectations(key, expected, site + " embed")
+ .then(resolve)
+ .catch(reject);
+ };
+
+ document.body.appendChild(el);
+ })
+ }, "Wrapper: " + site + " embed");
+
+ promise_test(t => {
+ return new Promise((resolve, reject) => {
+ let key = "post-embed-" + site + "-" + nonce;
+
+ let el = document.createElement('embed');
+ el.src = "/common/blank.html";
+ el.addEventListener("load", _ => {
+ el.addEventListener("load", _ => {
+ let expected = {"dest": "embed", "site": site, "user":"", "mode":"navigate"};
+ validate_expectations(key, expected, "Navigate to " + site + " embed")
+ .then(resolve)
+ .catch(reject);
+ }, { once: true });
+
+ // Navigate the existing `<embed>`
+ window.frames[window.length - 1].location = origins[site] + "/fetch/metadata/resources/record-header.py?file=" + key;
+ }, { once: true });
+
+ document.body.appendChild(el);
+ })
+ }, "Wrapper: Navigate to " + site + " embed");
+ }
+</script>
diff --git a/test/wpt/tests/fetch/metadata/fetch-preflight.https.sub.any.js b/test/wpt/tests/fetch/metadata/fetch-preflight.https.sub.any.js
new file mode 100644
index 0000000..d524743
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/fetch-preflight.https.sub.any.js
@@ -0,0 +1,29 @@
+// META: global=window,worker
+// META: script=/fetch/metadata/resources/helper.js
+
+// Site
+promise_test(t => {
+ return validate_expectations_custom_url("https://{{hosts[][www]}}:{{ports[https][0]}}/fetch/metadata/resources/echo-as-json.py",
+ {
+ mode: "cors",
+ headers: { 'x-test': 'testing' }
+ }, {
+ "site": "same-site",
+ "user": "",
+ "mode": "cors",
+ "dest": "empty"
+ }, "Same-site fetch with preflight");
+}, "Same-site fetch with preflight");
+
+promise_test(t => {
+ return validate_expectations_custom_url("https://{{hosts[alt][www]}}:{{ports[https][0]}}/fetch/metadata/resources/echo-as-json.py",
+ {
+ mode: "cors",
+ headers: { 'x-test': 'testing' }
+ }, {
+ "site": "cross-site",
+ "user": "",
+ "mode": "cors",
+ "dest": "empty"
+ }, "Cross-site fetch with preflight");
+}, "Cross-site fetch with preflight");
diff --git a/test/wpt/tests/fetch/metadata/fetch.https.sub.any.js b/test/wpt/tests/fetch/metadata/fetch.https.sub.any.js
new file mode 100644
index 0000000..aeec5cd
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/fetch.https.sub.any.js
@@ -0,0 +1,58 @@
+// META: global=window,worker
+// META: script=/fetch/metadata/resources/helper.js
+
+// Site
+promise_test(t => {
+ return validate_expectations_custom_url("https://{{host}}:{{ports[https][0]}}/fetch/metadata/resources/echo-as-json.py", {}, {
+ "site": "same-origin",
+ "user": "",
+ "mode": "cors",
+ "dest": "empty"
+ }, "Same-origin fetch");
+}, "Same-origin fetch");
+
+promise_test(t => {
+ return validate_expectations_custom_url("https://{{hosts[][www]}}:{{ports[https][0]}}/fetch/metadata/resources/echo-as-json.py", {}, {
+ "site": "same-site",
+ "user": "",
+ "mode": "cors",
+ "dest": "empty"
+ }, "Same-site fetch");
+}, "Same-site fetch");
+
+promise_test(t => {
+ return validate_expectations_custom_url("https://{{hosts[alt][www]}}:{{ports[https][0]}}/fetch/metadata/resources/echo-as-json.py", {}, {
+ "site": "cross-site",
+ "user": "",
+ "mode": "cors",
+ "dest": "empty"
+ }, "Cross-site fetch");
+}, "Cross-site fetch");
+
+// Mode
+promise_test(t => {
+ return validate_expectations_custom_url("https://{{host}}:{{ports[https][0]}}/fetch/metadata/resources/echo-as-json.py", {mode: "same-origin"}, {
+ "site": "same-origin",
+ "user": "",
+ "mode": "same-origin",
+ "dest": "empty"
+ }, "Same-origin mode");
+}, "Same-origin mode");
+
+promise_test(t => {
+ return validate_expectations_custom_url("https://{{host}}:{{ports[https][0]}}/fetch/metadata/resources/echo-as-json.py", {mode: "cors"}, {
+ "site": "same-origin",
+ "user": "",
+ "mode": "cors",
+ "dest": "empty"
+ }, "CORS mode");
+}, "CORS mode");
+
+promise_test(t => {
+ return validate_expectations_custom_url("https://{{host}}:{{ports[https][0]}}/fetch/metadata/resources/echo-as-json.py", {mode: "no-cors"}, {
+ "site": "same-origin",
+ "user": "",
+ "mode": "no-cors",
+ "dest": "empty"
+ }, "no-CORS mode");
+}, "no-CORS mode");
diff --git a/test/wpt/tests/fetch/metadata/generated/appcache-manifest.https.sub.html b/test/wpt/tests/fetch/metadata/generated/appcache-manifest.https.sub.html
new file mode 100644
index 0000000..cf322fd
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/generated/appcache-manifest.https.sub.html
@@ -0,0 +1,341 @@
+<!DOCTYPE html>
+<!--
+This test was procedurally generated. Please do not modify it directly.
+Sources:
+- fetch/metadata/tools/fetch-metadata.conf.yml
+- fetch/metadata/tools/templates/appcache-manifest.sub.https.html
+-->
+<html lang="en">
+ <meta charset="utf-8">
+ <title>HTTP headers on request for Appcache manifest</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/fetch/metadata/resources/helper.sub.js"></script>
+ <body>
+ <script>
+ 'use strict';
+
+ function induceRequest(url) {
+ const iframe = document.createElement('iframe');
+ iframe.src =
+ '/fetch/metadata/resources/appcache-iframe.sub.html?manifest=' + encodeURIComponent(url);
+
+ return new Promise((resolve) => {
+ addEventListener('message', function onMessage(event) {
+ if (event.source !== iframe.contentWindow) {
+ return;
+ }
+ removeEventListener('message', onMessage);
+ resolve(event.data);
+ });
+
+ document.body.appendChild(iframe);
+ })
+ .then((message) => {
+ if (message !== 'okay') {
+ throw message;
+ }
+ })
+ .then(() => iframe.remove());
+ }
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ assert_implements_optional(
+ !!window.applicationCache, 'Application Cache supported.'
+ );
+
+ induceRequest(makeRequestURL(key, ['httpsOrigin']))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-origin']);
+ })
+ .then(() => t.done(), t.step_func((error) => { throw error; }));
+ }, 'sec-fetch-site - Same origin');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ assert_implements_optional(
+ !!window.applicationCache, 'Application Cache supported.'
+ );
+
+ induceRequest(makeRequestURL(key, ['httpsCrossSite']))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ })
+ .then(() => t.done(), t.step_func((error) => { throw error; }));
+ }, 'sec-fetch-site - Cross-site');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ assert_implements_optional(
+ !!window.applicationCache, 'Application Cache supported.'
+ );
+
+ induceRequest(makeRequestURL(key, ['httpsSameSite']))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ })
+ .then(() => t.done(), t.step_func((error) => { throw error; }));
+ }, 'sec-fetch-site - Same site');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ assert_implements_optional(
+ !!window.applicationCache, 'Application Cache supported.'
+ );
+
+ induceRequest(makeRequestURL(key, ['httpsOrigin', 'httpOrigin']))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ })
+ .then(() => t.done(), t.step_func((error) => { throw error; }));
+ }, 'sec-fetch-site - HTTPS downgrade (header not sent)');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ assert_implements_optional(
+ !!window.applicationCache, 'Application Cache supported.'
+ );
+
+ induceRequest(makeRequestURL(key, ['httpOrigin', 'httpsOrigin']))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ })
+ .then(() => t.done(), t.step_func((error) => { throw error; }));
+ }, 'sec-fetch-site - HTTPS upgrade');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ assert_implements_optional(
+ !!window.applicationCache, 'Application Cache supported.'
+ );
+
+ induceRequest(makeRequestURL(key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin']))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ })
+ .then(() => t.done(), t.step_func((error) => { throw error; }));
+ }, 'sec-fetch-site - HTTPS downgrade-upgrade');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ assert_implements_optional(
+ !!window.applicationCache, 'Application Cache supported.'
+ );
+
+ induceRequest(makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite', 'httpsOrigin']))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ })
+ .then(() => t.done(), t.step_func((error) => { throw error; }));
+ }, 'sec-fetch-site - Same-Origin -> Cross-Site -> Same-Origin redirect');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ assert_implements_optional(
+ !!window.applicationCache, 'Application Cache supported.'
+ );
+
+ induceRequest(makeRequestURL(key, ['httpsOrigin', 'httpsSameSite', 'httpsOrigin']))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ })
+ .then(() => t.done(), t.step_func((error) => { throw error; }));
+ }, 'sec-fetch-site - Same-Origin -> Same-Site -> Same-Origin redirect');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ assert_implements_optional(
+ !!window.applicationCache, 'Application Cache supported.'
+ );
+
+ induceRequest(makeRequestURL(key, ['httpsCrossSite', 'httpsOrigin']))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ })
+ .then(() => t.done(), t.step_func((error) => { throw error; }));
+ }, 'sec-fetch-site - Cross-Site -> Same Origin');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ assert_implements_optional(
+ !!window.applicationCache, 'Application Cache supported.'
+ );
+
+ induceRequest(makeRequestURL(key, ['httpsCrossSite', 'httpsSameSite']))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ })
+ .then(() => t.done(), t.step_func((error) => { throw error; }));
+ }, 'sec-fetch-site - Cross-Site -> Same-Site');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ assert_implements_optional(
+ !!window.applicationCache, 'Application Cache supported.'
+ );
+
+ induceRequest(makeRequestURL(key, ['httpsCrossSite', 'httpsCrossSite']))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ })
+ .then(() => t.done(), t.step_func((error) => { throw error; }));
+ }, 'sec-fetch-site - Cross-Site -> Cross-Site');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ assert_implements_optional(
+ !!window.applicationCache, 'Application Cache supported.'
+ );
+
+ induceRequest(makeRequestURL(key, ['httpsOrigin', 'httpsOrigin']))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-origin']);
+ })
+ .then(() => t.done(), t.step_func((error) => { throw error; }));
+ }, 'sec-fetch-site - Same-Origin -> Same Origin');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ assert_implements_optional(
+ !!window.applicationCache, 'Application Cache supported.'
+ );
+
+ induceRequest(makeRequestURL(key, ['httpsOrigin', 'httpsSameSite']))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ })
+ .then(() => t.done(), t.step_func((error) => { throw error; }));
+ }, 'sec-fetch-site - Same-Origin -> Same-Site');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ assert_implements_optional(
+ !!window.applicationCache, 'Application Cache supported.'
+ );
+
+ induceRequest(makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite']))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ })
+ .then(() => t.done(), t.step_func((error) => { throw error; }));
+ }, 'sec-fetch-site - Same-Origin -> Cross-Site');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ assert_implements_optional(
+ !!window.applicationCache, 'Application Cache supported.'
+ );
+
+ induceRequest(makeRequestURL(key, ['httpsSameSite', 'httpsOrigin']))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ })
+ .then(() => t.done(), t.step_func((error) => { throw error; }));
+ }, 'sec-fetch-site - Same-Site -> Same Origin');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ assert_implements_optional(
+ !!window.applicationCache, 'Application Cache supported.'
+ );
+
+ induceRequest(makeRequestURL(key, ['httpsSameSite', 'httpsSameSite']))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ })
+ .then(() => t.done(), t.step_func((error) => { throw error; }));
+ }, 'sec-fetch-site - Same-Site -> Same-Site');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ assert_implements_optional(
+ !!window.applicationCache, 'Application Cache supported.'
+ );
+
+ induceRequest(makeRequestURL(key, ['httpsSameSite', 'httpsCrossSite']))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ })
+ .then(() => t.done(), t.step_func((error) => { throw error; }));
+ }, 'sec-fetch-site - Same-Site -> Cross-Site');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ assert_implements_optional(
+ !!window.applicationCache, 'Application Cache supported.'
+ );
+
+ induceRequest(makeRequestURL(key, []))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-mode');
+ assert_array_equals(headers['sec-fetch-mode'], ['no-cors']);
+ })
+ .then(() => t.done(), t.step_func((error) => { throw error; }));
+ }, 'sec-fetch-mode');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ assert_implements_optional(
+ !!window.applicationCache, 'Application Cache supported.'
+ );
+
+ induceRequest(makeRequestURL(key, []))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-dest');
+ assert_array_equals(headers['sec-fetch-dest'], ['empty']);
+ })
+ .then(() => t.done(), t.step_func((error) => { throw error; }));
+ }, 'sec-fetch-dest');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ assert_implements_optional(
+ !!window.applicationCache, 'Application Cache supported.'
+ );
+
+ induceRequest(makeRequestURL(key, []))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ })
+ .then(() => t.done(), t.step_func((error) => { throw error; }));
+ }, 'sec-fetch-user');
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/fetch/metadata/generated/audioworklet.https.sub.html b/test/wpt/tests/fetch/metadata/generated/audioworklet.https.sub.html
new file mode 100644
index 0000000..64fb760
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/generated/audioworklet.https.sub.html
@@ -0,0 +1,271 @@
+<!DOCTYPE html>
+<!--
+This test was procedurally generated. Please do not modify it directly.
+Sources:
+- fetch/metadata/tools/fetch-metadata.conf.yml
+- fetch/metadata/tools/templates/audioworklet.https.sub.html
+-->
+<html lang="en">
+ <meta charset="utf-8">
+ <title>HTTP headers on request for AudioWorklet module</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/resources/testdriver.js"></script>
+ <script src="/resources/testdriver-vendor.js"></script>
+ <script src="/fetch/metadata/resources/helper.sub.js"></script>
+ <body>
+ <script>
+ 'use strict';
+
+ function induceRequest(url, test) {
+ return test_driver.bless(
+ 'Enable WebAudio playback',
+ () => {
+ const audioContext = new AudioContext();
+
+ test.add_cleanup(() => audioContext.close());
+
+ return audioContext.audioWorklet.addModule(url);
+ }
+ );
+ }
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin'], {mime: 'text/javascript'}),
+ t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-origin']);
+ });
+ }, 'sec-fetch-site - Same origin');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsCrossSite'], {mime: 'text/javascript'}),
+ t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-site');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsSameSite'], {mime: 'text/javascript'}),
+ t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same site');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite', 'httpsOrigin'], {mime: 'text/javascript'}),
+ t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Cross-Site -> Same-Origin redirect');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpsSameSite', 'httpsOrigin'], {mime: 'text/javascript'}),
+ t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Same-Site -> Same-Origin redirect');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsCrossSite', 'httpsOrigin'], {mime: 'text/javascript'}),
+ t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-Site -> Same Origin');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsCrossSite', 'httpsSameSite'], {mime: 'text/javascript'}),
+ t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-Site -> Same-Site');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsCrossSite', 'httpsCrossSite'], {mime: 'text/javascript'}),
+ t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-Site -> Cross-Site');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpsOrigin'], {mime: 'text/javascript'}),
+ t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-origin']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Same Origin');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpsSameSite'], {mime: 'text/javascript'}),
+ t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Same-Site');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite'], {mime: 'text/javascript'}),
+ t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Cross-Site');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsSameSite', 'httpsOrigin'], {mime: 'text/javascript'}),
+ t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Site -> Same Origin');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsSameSite', 'httpsSameSite'], {mime: 'text/javascript'}),
+ t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Site -> Same-Site');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsSameSite', 'httpsCrossSite'], {mime: 'text/javascript'}),
+ t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Same-Site -> Cross-Site');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, [], {mime: 'text/javascript'}),
+ t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-mode');
+ assert_array_equals(headers['sec-fetch-mode'], ['cors']);
+ });
+ }, 'sec-fetch-mode');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, [], {mime: 'text/javascript'}),
+ t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-dest');
+ assert_array_equals(headers['sec-fetch-dest'], ['audioworklet']);
+ });
+ }, 'sec-fetch-dest');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, [], {mime: 'text/javascript'}),
+ t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user');
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/fetch/metadata/generated/css-font-face.https.sub.tentative.html b/test/wpt/tests/fetch/metadata/generated/css-font-face.https.sub.tentative.html
new file mode 100644
index 0000000..332effe
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/generated/css-font-face.https.sub.tentative.html
@@ -0,0 +1,230 @@
+<!DOCTYPE html>
+<!--
+This test was procedurally generated. Please do not modify it directly.
+Sources:
+- fetch/metadata/tools/fetch-metadata.conf.yml
+- fetch/metadata/tools/templates/css-font-face.sub.html
+-->
+<html lang="en">
+ <meta charset="utf-8">
+ <title>HTTP headers on request for CSS font-face</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/fetch/metadata/resources/helper.sub.js"></script>
+ <body>
+ <script>
+ 'use strict';
+
+ let count = 0;
+
+ function induceRequest(t, url) {
+ const id = `el-${count += 1}`;
+ const style = document.createElement('style');
+ style.appendChild(document.createTextNode(`
+ @font-face {
+ font-family: wpt-font-family${id};
+ src: url(${url});
+ }
+ #el-${id} {
+ font-family: wpt-font-family${id};
+ }
+ `));
+ const div = document.createElement('div');
+ div.setAttribute('id', 'el-' + id);
+ div.appendChild(style);
+ div.appendChild(document.createTextNode('x'));
+ document.body.appendChild(div);
+
+ t.add_cleanup(() => div.remove());
+
+ return document.fonts.ready;
+ }
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(t, makeRequestURL(key, ['httpsOrigin']))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-origin']);
+ });
+ }, 'sec-fetch-site - Same origin');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(t, makeRequestURL(key, ['httpsCrossSite']))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-site');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(t, makeRequestURL(key, ['httpsSameSite']))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same site');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(t, makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite', 'httpsOrigin']))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Cross-Site -> Same-Origin redirect');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(t, makeRequestURL(key, ['httpsOrigin', 'httpsSameSite', 'httpsOrigin']))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Same-Site -> Same-Origin redirect');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(t, makeRequestURL(key, ['httpsCrossSite', 'httpsOrigin']))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-Site -> Same Origin');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(t, makeRequestURL(key, ['httpsCrossSite', 'httpsSameSite']))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-Site -> Same-Site');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(t, makeRequestURL(key, ['httpsCrossSite', 'httpsCrossSite']))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-Site -> Cross-Site');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(t, makeRequestURL(key, ['httpsOrigin', 'httpsOrigin']))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-origin']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Same Origin');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(t, makeRequestURL(key, ['httpsOrigin', 'httpsSameSite']))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Same-Site');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(t, makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite']))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Cross-Site');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(t, makeRequestURL(key, ['httpsSameSite', 'httpsOrigin']))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Site -> Same Origin');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(t, makeRequestURL(key, ['httpsSameSite', 'httpsSameSite']))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Site -> Same-Site');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(t, makeRequestURL(key, ['httpsSameSite', 'httpsCrossSite']))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Same-Site -> Cross-Site');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(t, makeRequestURL(key, []))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-mode');
+ assert_array_equals(headers['sec-fetch-mode'], ['cors']);
+ });
+ }, 'sec-fetch-mode');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(t, makeRequestURL(key, []))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-dest');
+ assert_array_equals(headers['sec-fetch-dest'], ['font']);
+ });
+ }, 'sec-fetch-dest');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(t, makeRequestURL(key, []))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user');
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/fetch/metadata/generated/css-font-face.sub.tentative.html b/test/wpt/tests/fetch/metadata/generated/css-font-face.sub.tentative.html
new file mode 100644
index 0000000..8a0b90c
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/generated/css-font-face.sub.tentative.html
@@ -0,0 +1,196 @@
+<!DOCTYPE html>
+<!--
+This test was procedurally generated. Please do not modify it directly.
+Sources:
+- fetch/metadata/tools/fetch-metadata.conf.yml
+- fetch/metadata/tools/templates/css-font-face.sub.html
+-->
+<html lang="en">
+ <meta charset="utf-8">
+ <title>HTTP headers on request for CSS font-face</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/fetch/metadata/resources/helper.sub.js"></script>
+ <body>
+ <script>
+ 'use strict';
+
+ let count = 0;
+
+ function induceRequest(t, url) {
+ const id = `el-${count += 1}`;
+ const style = document.createElement('style');
+ style.appendChild(document.createTextNode(`
+ @font-face {
+ font-family: wpt-font-family${id};
+ src: url(${url});
+ }
+ #el-${id} {
+ font-family: wpt-font-family${id};
+ }
+ `));
+ const div = document.createElement('div');
+ div.setAttribute('id', 'el-' + id);
+ div.appendChild(style);
+ div.appendChild(document.createTextNode('x'));
+ document.body.appendChild(div);
+
+ t.add_cleanup(() => div.remove());
+
+ return document.fonts.ready;
+ }
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(t, makeRequestURL(key, ['httpOrigin']))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - Not sent to non-trustworthy same-origin destination');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(t, makeRequestURL(key, ['httpSameSite']))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - Not sent to non-trustworthy same-site destination');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(t, makeRequestURL(key, ['httpCrossSite']))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - Not sent to non-trustworthy cross-site destination');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(t, makeRequestURL(key, ['httpOrigin']))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ });
+ }, 'sec-fetch-mode - Not sent to non-trustworthy same-origin destination');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(t, makeRequestURL(key, ['httpSameSite']))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ });
+ }, 'sec-fetch-mode - Not sent to non-trustworthy same-site destination');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(t, makeRequestURL(key, ['httpCrossSite']))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ });
+ }, 'sec-fetch-mode - Not sent to non-trustworthy cross-site destination');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(t, makeRequestURL(key, ['httpOrigin']))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ });
+ }, 'sec-fetch-dest - Not sent to non-trustworthy same-origin destination');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(t, makeRequestURL(key, ['httpSameSite']))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ });
+ }, 'sec-fetch-dest - Not sent to non-trustworthy same-site destination');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(t, makeRequestURL(key, ['httpCrossSite']))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ });
+ }, 'sec-fetch-dest - Not sent to non-trustworthy cross-site destination');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(t, makeRequestURL(key, ['httpOrigin']))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - Not sent to non-trustworthy same-origin destination');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(t, makeRequestURL(key, ['httpSameSite']))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - Not sent to non-trustworthy same-site destination');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(t, makeRequestURL(key, ['httpCrossSite']))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - Not sent to non-trustworthy cross-site destination');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(t, makeRequestURL(key, ['httpsOrigin', 'httpOrigin']))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - HTTPS downgrade (header not sent)');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(t, makeRequestURL(key, ['httpOrigin', 'httpsOrigin']))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - HTTPS upgrade');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(t, makeRequestURL(key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin']))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - HTTPS downgrade-upgrade');
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/fetch/metadata/generated/css-images.https.sub.tentative.html b/test/wpt/tests/fetch/metadata/generated/css-images.https.sub.tentative.html
new file mode 100644
index 0000000..3fa2401
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/generated/css-images.https.sub.tentative.html
@@ -0,0 +1,1384 @@
+<!DOCTYPE html>
+<!--
+This test was procedurally generated. Please do not modify it directly.
+Sources:
+- fetch/metadata/tools/fetch-metadata.conf.yml
+- fetch/metadata/tools/templates/css-images.sub.html
+-->
+<html lang="en">
+ <meta charset="utf-8">
+ <meta name="timeout" content="long">
+ <title>HTTP headers on request for CSS image-accepting properties</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/fetch/metadata/resources/helper.sub.js"></script>
+ <body>
+ <script>
+ 'use strict';
+
+ /**
+ * The subtests in this file use an iframe to induce requests for CSS
+ * resources because an iframe's `onload` event is the most direct and
+ * generic mechanism to detect loading of CSS resources. As an optimization,
+ * the subtests share the same iframe and document.
+ */
+ const declarations = [];
+ const iframe = document.createElement('iframe');
+ const whenIframeReady = new Promise((resolve, reject) => {
+ iframe.onload = resolve;
+ iframe.onerror = reject;
+ });
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsOrigin']);
+
+ declarations.push(`background-image: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_equals(headers['sec-fetch-site'], 'same-origin');
+ })
+ .then(t.step_func_done(), (error) => t.unreached_func());
+ }, 'background-image sec-fetch-site - Same origin');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsOrigin']);
+
+ declarations.push(`border-image: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-origin']);
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'border-image sec-fetch-site - Same origin');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsOrigin']);
+
+ declarations.push(`content: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-origin']);
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'content sec-fetch-site - Same origin');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsOrigin']);
+
+ declarations.push(`cursor: url("${url}"), auto;`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-origin']);
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'cursor sec-fetch-site - Same origin');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsOrigin']);
+
+ declarations.push(`list-style-image: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-origin']);
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'list-style-image sec-fetch-site - Same origin');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsCrossSite']);
+
+ declarations.push(`background-image: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_equals(headers['sec-fetch-site'], 'cross-site');
+ })
+ .then(t.step_func_done(), (error) => t.unreached_func());
+ }, 'background-image sec-fetch-site - Cross-site');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsCrossSite']);
+
+ declarations.push(`border-image: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'border-image sec-fetch-site - Cross-site');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsCrossSite']);
+
+ declarations.push(`content: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'content sec-fetch-site - Cross-site');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsCrossSite']);
+
+ declarations.push(`cursor: url("${url}"), auto;`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'cursor sec-fetch-site - Cross-site');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsCrossSite']);
+
+ declarations.push(`list-style-image: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'list-style-image sec-fetch-site - Cross-site');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsSameSite']);
+
+ declarations.push(`background-image: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_equals(headers['sec-fetch-site'], 'same-site');
+ })
+ .then(t.step_func_done(), (error) => t.unreached_func());
+ }, 'background-image sec-fetch-site - Same site');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsSameSite']);
+
+ declarations.push(`border-image: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'border-image sec-fetch-site - Same site');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsSameSite']);
+
+ declarations.push(`content: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'content sec-fetch-site - Same site');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsSameSite']);
+
+ declarations.push(`cursor: url("${url}"), auto;`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'cursor sec-fetch-site - Same site');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsSameSite']);
+
+ declarations.push(`list-style-image: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'list-style-image sec-fetch-site - Same site');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite', 'httpsOrigin']);
+
+ declarations.push(`background-image: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_equals(headers['sec-fetch-site'], 'cross-site');
+ })
+ .then(t.step_func_done(), (error) => t.unreached_func());
+ }, 'background-image sec-fetch-site - Same-Origin -> Cross-Site -> Same-Origin redirect');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite', 'httpsOrigin']);
+
+ declarations.push(`border-image: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'border-image sec-fetch-site - Same-Origin -> Cross-Site -> Same-Origin redirect');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite', 'httpsOrigin']);
+
+ declarations.push(`content: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'content sec-fetch-site - Same-Origin -> Cross-Site -> Same-Origin redirect');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite', 'httpsOrigin']);
+
+ declarations.push(`cursor: url("${url}"), auto;`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'cursor sec-fetch-site - Same-Origin -> Cross-Site -> Same-Origin redirect');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite', 'httpsOrigin']);
+
+ declarations.push(`list-style-image: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'list-style-image sec-fetch-site - Same-Origin -> Cross-Site -> Same-Origin redirect');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsOrigin', 'httpsSameSite', 'httpsOrigin']);
+
+ declarations.push(`background-image: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_equals(headers['sec-fetch-site'], 'same-site');
+ })
+ .then(t.step_func_done(), (error) => t.unreached_func());
+ }, 'background-image sec-fetch-site - Same-Origin -> Same-Site -> Same-Origin redirect');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsOrigin', 'httpsSameSite', 'httpsOrigin']);
+
+ declarations.push(`border-image: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'border-image sec-fetch-site - Same-Origin -> Same-Site -> Same-Origin redirect');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsOrigin', 'httpsSameSite', 'httpsOrigin']);
+
+ declarations.push(`content: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'content sec-fetch-site - Same-Origin -> Same-Site -> Same-Origin redirect');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsOrigin', 'httpsSameSite', 'httpsOrigin']);
+
+ declarations.push(`cursor: url("${url}"), auto;`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'cursor sec-fetch-site - Same-Origin -> Same-Site -> Same-Origin redirect');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsOrigin', 'httpsSameSite', 'httpsOrigin']);
+
+ declarations.push(`list-style-image: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'list-style-image sec-fetch-site - Same-Origin -> Same-Site -> Same-Origin redirect');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsCrossSite', 'httpsOrigin']);
+
+ declarations.push(`background-image: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_equals(headers['sec-fetch-site'], 'cross-site');
+ })
+ .then(t.step_func_done(), (error) => t.unreached_func());
+ }, 'background-image sec-fetch-site - Cross-Site -> Same Origin');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsCrossSite', 'httpsOrigin']);
+
+ declarations.push(`border-image: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'border-image sec-fetch-site - Cross-Site -> Same Origin');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsCrossSite', 'httpsOrigin']);
+
+ declarations.push(`content: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'content sec-fetch-site - Cross-Site -> Same Origin');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsCrossSite', 'httpsOrigin']);
+
+ declarations.push(`cursor: url("${url}"), auto;`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'cursor sec-fetch-site - Cross-Site -> Same Origin');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsCrossSite', 'httpsOrigin']);
+
+ declarations.push(`list-style-image: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'list-style-image sec-fetch-site - Cross-Site -> Same Origin');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsCrossSite', 'httpsSameSite']);
+
+ declarations.push(`background-image: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_equals(headers['sec-fetch-site'], 'cross-site');
+ })
+ .then(t.step_func_done(), (error) => t.unreached_func());
+ }, 'background-image sec-fetch-site - Cross-Site -> Same-Site');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsCrossSite', 'httpsSameSite']);
+
+ declarations.push(`border-image: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'border-image sec-fetch-site - Cross-Site -> Same-Site');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsCrossSite', 'httpsSameSite']);
+
+ declarations.push(`content: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'content sec-fetch-site - Cross-Site -> Same-Site');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsCrossSite', 'httpsSameSite']);
+
+ declarations.push(`cursor: url("${url}"), auto;`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'cursor sec-fetch-site - Cross-Site -> Same-Site');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsCrossSite', 'httpsSameSite']);
+
+ declarations.push(`list-style-image: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'list-style-image sec-fetch-site - Cross-Site -> Same-Site');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsCrossSite', 'httpsCrossSite']);
+
+ declarations.push(`background-image: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_equals(headers['sec-fetch-site'], 'cross-site');
+ })
+ .then(t.step_func_done(), (error) => t.unreached_func());
+ }, 'background-image sec-fetch-site - Cross-Site -> Cross-Site');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsCrossSite', 'httpsCrossSite']);
+
+ declarations.push(`border-image: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'border-image sec-fetch-site - Cross-Site -> Cross-Site');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsCrossSite', 'httpsCrossSite']);
+
+ declarations.push(`content: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'content sec-fetch-site - Cross-Site -> Cross-Site');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsCrossSite', 'httpsCrossSite']);
+
+ declarations.push(`cursor: url("${url}"), auto;`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'cursor sec-fetch-site - Cross-Site -> Cross-Site');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsCrossSite', 'httpsCrossSite']);
+
+ declarations.push(`list-style-image: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'list-style-image sec-fetch-site - Cross-Site -> Cross-Site');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsOrigin', 'httpsOrigin']);
+
+ declarations.push(`background-image: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_equals(headers['sec-fetch-site'], 'same-origin');
+ })
+ .then(t.step_func_done(), (error) => t.unreached_func());
+ }, 'background-image sec-fetch-site - Same-Origin -> Same Origin');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsOrigin', 'httpsOrigin']);
+
+ declarations.push(`border-image: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-origin']);
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'border-image sec-fetch-site - Same-Origin -> Same Origin');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsOrigin', 'httpsOrigin']);
+
+ declarations.push(`content: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-origin']);
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'content sec-fetch-site - Same-Origin -> Same Origin');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsOrigin', 'httpsOrigin']);
+
+ declarations.push(`cursor: url("${url}"), auto;`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-origin']);
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'cursor sec-fetch-site - Same-Origin -> Same Origin');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsOrigin', 'httpsOrigin']);
+
+ declarations.push(`list-style-image: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-origin']);
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'list-style-image sec-fetch-site - Same-Origin -> Same Origin');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsOrigin', 'httpsSameSite']);
+
+ declarations.push(`background-image: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_equals(headers['sec-fetch-site'], 'same-site');
+ })
+ .then(t.step_func_done(), (error) => t.unreached_func());
+ }, 'background-image sec-fetch-site - Same-Origin -> Same-Site');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsOrigin', 'httpsSameSite']);
+
+ declarations.push(`border-image: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'border-image sec-fetch-site - Same-Origin -> Same-Site');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsOrigin', 'httpsSameSite']);
+
+ declarations.push(`content: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'content sec-fetch-site - Same-Origin -> Same-Site');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsOrigin', 'httpsSameSite']);
+
+ declarations.push(`cursor: url("${url}"), auto;`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'cursor sec-fetch-site - Same-Origin -> Same-Site');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsOrigin', 'httpsSameSite']);
+
+ declarations.push(`list-style-image: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'list-style-image sec-fetch-site - Same-Origin -> Same-Site');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite']);
+
+ declarations.push(`background-image: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_equals(headers['sec-fetch-site'], 'cross-site');
+ })
+ .then(t.step_func_done(), (error) => t.unreached_func());
+ }, 'background-image sec-fetch-site - Same-Origin -> Cross-Site');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite']);
+
+ declarations.push(`border-image: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'border-image sec-fetch-site - Same-Origin -> Cross-Site');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite']);
+
+ declarations.push(`content: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'content sec-fetch-site - Same-Origin -> Cross-Site');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite']);
+
+ declarations.push(`cursor: url("${url}"), auto;`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'cursor sec-fetch-site - Same-Origin -> Cross-Site');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite']);
+
+ declarations.push(`list-style-image: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'list-style-image sec-fetch-site - Same-Origin -> Cross-Site');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsSameSite', 'httpsOrigin']);
+
+ declarations.push(`background-image: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_equals(headers['sec-fetch-site'], 'same-site');
+ })
+ .then(t.step_func_done(), (error) => t.unreached_func());
+ }, 'background-image sec-fetch-site - Same-Site -> Same Origin');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsSameSite', 'httpsOrigin']);
+
+ declarations.push(`border-image: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'border-image sec-fetch-site - Same-Site -> Same Origin');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsSameSite', 'httpsOrigin']);
+
+ declarations.push(`content: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'content sec-fetch-site - Same-Site -> Same Origin');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsSameSite', 'httpsOrigin']);
+
+ declarations.push(`cursor: url("${url}"), auto;`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'cursor sec-fetch-site - Same-Site -> Same Origin');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsSameSite', 'httpsOrigin']);
+
+ declarations.push(`list-style-image: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'list-style-image sec-fetch-site - Same-Site -> Same Origin');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsSameSite', 'httpsSameSite']);
+
+ declarations.push(`background-image: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_equals(headers['sec-fetch-site'], 'same-site');
+ })
+ .then(t.step_func_done(), (error) => t.unreached_func());
+ }, 'background-image sec-fetch-site - Same-Site -> Same-Site');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsSameSite', 'httpsSameSite']);
+
+ declarations.push(`border-image: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'border-image sec-fetch-site - Same-Site -> Same-Site');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsSameSite', 'httpsSameSite']);
+
+ declarations.push(`content: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'content sec-fetch-site - Same-Site -> Same-Site');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsSameSite', 'httpsSameSite']);
+
+ declarations.push(`cursor: url("${url}"), auto;`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'cursor sec-fetch-site - Same-Site -> Same-Site');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsSameSite', 'httpsSameSite']);
+
+ declarations.push(`list-style-image: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'list-style-image sec-fetch-site - Same-Site -> Same-Site');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsSameSite', 'httpsCrossSite']);
+
+ declarations.push(`background-image: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_equals(headers['sec-fetch-site'], 'cross-site');
+ })
+ .then(t.step_func_done(), (error) => t.unreached_func());
+ }, 'background-image sec-fetch-site - Same-Site -> Cross-Site');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsSameSite', 'httpsCrossSite']);
+
+ declarations.push(`border-image: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'border-image sec-fetch-site - Same-Site -> Cross-Site');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsSameSite', 'httpsCrossSite']);
+
+ declarations.push(`content: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'content sec-fetch-site - Same-Site -> Cross-Site');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsSameSite', 'httpsCrossSite']);
+
+ declarations.push(`cursor: url("${url}"), auto;`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'cursor sec-fetch-site - Same-Site -> Cross-Site');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsSameSite', 'httpsCrossSite']);
+
+ declarations.push(`list-style-image: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'list-style-image sec-fetch-site - Same-Site -> Cross-Site');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin']);
+
+ declarations.push(`background-image: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_equals(headers['sec-fetch-site'], 'cross-site');
+ })
+ .then(t.step_func_done(), (error) => t.unreached_func());
+ }, 'background-image sec-fetch-site - HTTPS downgrade-upgrade');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin']);
+
+ declarations.push(`border-image: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'border-image sec-fetch-site - HTTPS downgrade-upgrade');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin']);
+
+ declarations.push(`content: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'content sec-fetch-site - HTTPS downgrade-upgrade');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin']);
+
+ declarations.push(`cursor: url("${url}"), auto;`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'cursor sec-fetch-site - HTTPS downgrade-upgrade');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin']);
+
+ declarations.push(`list-style-image: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'list-style-image sec-fetch-site - HTTPS downgrade-upgrade');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, []);
+
+ declarations.push(`background-image: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-mode');
+ assert_equals(headers['sec-fetch-mode'], 'no-cors');
+ })
+ .then(t.step_func_done(), (error) => t.unreached_func());
+ }, 'background-image sec-fetch-mode');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, []);
+
+ declarations.push(`border-image: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-mode');
+ assert_array_equals(headers['sec-fetch-mode'], ['no-cors']);
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'border-image sec-fetch-mode');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, []);
+
+ declarations.push(`content: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-mode');
+ assert_array_equals(headers['sec-fetch-mode'], ['no-cors']);
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'content sec-fetch-mode');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, []);
+
+ declarations.push(`cursor: url("${url}"), auto;`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-mode');
+ assert_array_equals(headers['sec-fetch-mode'], ['no-cors']);
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'cursor sec-fetch-mode');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, []);
+
+ declarations.push(`list-style-image: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-mode');
+ assert_array_equals(headers['sec-fetch-mode'], ['no-cors']);
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'list-style-image sec-fetch-mode');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, []);
+
+ declarations.push(`background-image: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-dest');
+ assert_equals(headers['sec-fetch-dest'], 'image');
+ })
+ .then(t.step_func_done(), (error) => t.unreached_func());
+ }, 'background-image sec-fetch-dest');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, []);
+
+ declarations.push(`border-image: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-dest');
+ assert_array_equals(headers['sec-fetch-dest'], ['image']);
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'border-image sec-fetch-dest');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, []);
+
+ declarations.push(`content: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-dest');
+ assert_array_equals(headers['sec-fetch-dest'], ['image']);
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'content sec-fetch-dest');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, []);
+
+ declarations.push(`cursor: url("${url}"), auto;`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-dest');
+ assert_array_equals(headers['sec-fetch-dest'], ['image']);
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'cursor sec-fetch-dest');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, []);
+
+ declarations.push(`list-style-image: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-dest');
+ assert_array_equals(headers['sec-fetch-dest'], ['image']);
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'list-style-image sec-fetch-dest');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, []);
+
+ declarations.push(`background-image: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ })
+ .then(t.step_func_done(), (error) => t.unreached_func());
+ }, 'background-image sec-fetch-user');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, []);
+
+ declarations.push(`border-image: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'border-image sec-fetch-user');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, []);
+
+ declarations.push(`content: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'content sec-fetch-user');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, []);
+
+ declarations.push(`cursor: url("${url}"), auto;`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'cursor sec-fetch-user');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, []);
+
+ declarations.push(`list-style-image: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'list-style-image sec-fetch-user');
+
+ iframe.srcdoc = declarations.map((declaration, index) => `
+ <style>.el${index} { ${declaration} }</style><div class="el${index}"></div>`
+ ).join('');
+ document.body.appendChild(iframe);
+
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/fetch/metadata/generated/css-images.sub.tentative.html b/test/wpt/tests/fetch/metadata/generated/css-images.sub.tentative.html
new file mode 100644
index 0000000..f1ef27c
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/generated/css-images.sub.tentative.html
@@ -0,0 +1,1099 @@
+<!DOCTYPE html>
+<!--
+This test was procedurally generated. Please do not modify it directly.
+Sources:
+- fetch/metadata/tools/fetch-metadata.conf.yml
+- fetch/metadata/tools/templates/css-images.sub.html
+-->
+<html lang="en">
+ <meta charset="utf-8">
+ <meta name="timeout" content="long">
+ <title>HTTP headers on request for CSS image-accepting properties</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/fetch/metadata/resources/helper.sub.js"></script>
+ <body>
+ <script>
+ 'use strict';
+
+ /**
+ * The subtests in this file use an iframe to induce requests for CSS
+ * resources because an iframe's `onload` event is the most direct and
+ * generic mechanism to detect loading of CSS resources. As an optimization,
+ * the subtests share the same iframe and document.
+ */
+ const declarations = [];
+ const iframe = document.createElement('iframe');
+ const whenIframeReady = new Promise((resolve, reject) => {
+ iframe.onload = resolve;
+ iframe.onerror = reject;
+ });
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpOrigin']);
+
+ declarations.push(`background-image: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ })
+ .then(t.step_func_done(), (error) => t.unreached_func());
+ }, 'background-image sec-fetch-site - Not sent to non-trustworthy same-origin destination');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpOrigin']);
+
+ declarations.push(`border-image: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'border-image sec-fetch-site - Not sent to non-trustworthy same-origin destination');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpOrigin']);
+
+ declarations.push(`content: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'content sec-fetch-site - Not sent to non-trustworthy same-origin destination');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpOrigin']);
+
+ declarations.push(`cursor: url("${url}"), auto;`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'cursor sec-fetch-site - Not sent to non-trustworthy same-origin destination');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpOrigin']);
+
+ declarations.push(`list-style-image: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'list-style-image sec-fetch-site - Not sent to non-trustworthy same-origin destination');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpSameSite']);
+
+ declarations.push(`background-image: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ })
+ .then(t.step_func_done(), (error) => t.unreached_func());
+ }, 'background-image sec-fetch-site - Not sent to non-trustworthy same-site destination');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpSameSite']);
+
+ declarations.push(`border-image: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'border-image sec-fetch-site - Not sent to non-trustworthy same-site destination');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpSameSite']);
+
+ declarations.push(`content: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'content sec-fetch-site - Not sent to non-trustworthy same-site destination');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpSameSite']);
+
+ declarations.push(`cursor: url("${url}"), auto;`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'cursor sec-fetch-site - Not sent to non-trustworthy same-site destination');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpSameSite']);
+
+ declarations.push(`list-style-image: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'list-style-image sec-fetch-site - Not sent to non-trustworthy same-site destination');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpCrossSite']);
+
+ declarations.push(`background-image: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ })
+ .then(t.step_func_done(), (error) => t.unreached_func());
+ }, 'background-image sec-fetch-site - Not sent to non-trustworthy cross-site destination');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpCrossSite']);
+
+ declarations.push(`border-image: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'border-image sec-fetch-site - Not sent to non-trustworthy cross-site destination');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpCrossSite']);
+
+ declarations.push(`content: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'content sec-fetch-site - Not sent to non-trustworthy cross-site destination');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpCrossSite']);
+
+ declarations.push(`cursor: url("${url}"), auto;`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'cursor sec-fetch-site - Not sent to non-trustworthy cross-site destination');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpCrossSite']);
+
+ declarations.push(`list-style-image: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'list-style-image sec-fetch-site - Not sent to non-trustworthy cross-site destination');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpOrigin']);
+
+ declarations.push(`background-image: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ })
+ .then(t.step_func_done(), (error) => t.unreached_func());
+ }, 'background-image sec-fetch-mode - Not sent to non-trustworthy same-origin destination');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpOrigin']);
+
+ declarations.push(`border-image: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'border-image sec-fetch-mode - Not sent to non-trustworthy same-origin destination');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpOrigin']);
+
+ declarations.push(`content: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'content sec-fetch-mode - Not sent to non-trustworthy same-origin destination');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpOrigin']);
+
+ declarations.push(`cursor: url("${url}"), auto;`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'cursor sec-fetch-mode - Not sent to non-trustworthy same-origin destination');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpOrigin']);
+
+ declarations.push(`list-style-image: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'list-style-image sec-fetch-mode - Not sent to non-trustworthy same-origin destination');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpSameSite']);
+
+ declarations.push(`background-image: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ })
+ .then(t.step_func_done(), (error) => t.unreached_func());
+ }, 'background-image sec-fetch-mode - Not sent to non-trustworthy same-site destination');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpSameSite']);
+
+ declarations.push(`border-image: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'border-image sec-fetch-mode - Not sent to non-trustworthy same-site destination');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpSameSite']);
+
+ declarations.push(`content: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'content sec-fetch-mode - Not sent to non-trustworthy same-site destination');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpSameSite']);
+
+ declarations.push(`cursor: url("${url}"), auto;`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'cursor sec-fetch-mode - Not sent to non-trustworthy same-site destination');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpSameSite']);
+
+ declarations.push(`list-style-image: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'list-style-image sec-fetch-mode - Not sent to non-trustworthy same-site destination');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpCrossSite']);
+
+ declarations.push(`background-image: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ })
+ .then(t.step_func_done(), (error) => t.unreached_func());
+ }, 'background-image sec-fetch-mode - Not sent to non-trustworthy cross-site destination');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpCrossSite']);
+
+ declarations.push(`border-image: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'border-image sec-fetch-mode - Not sent to non-trustworthy cross-site destination');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpCrossSite']);
+
+ declarations.push(`content: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'content sec-fetch-mode - Not sent to non-trustworthy cross-site destination');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpCrossSite']);
+
+ declarations.push(`cursor: url("${url}"), auto;`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'cursor sec-fetch-mode - Not sent to non-trustworthy cross-site destination');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpCrossSite']);
+
+ declarations.push(`list-style-image: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'list-style-image sec-fetch-mode - Not sent to non-trustworthy cross-site destination');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpOrigin']);
+
+ declarations.push(`background-image: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ })
+ .then(t.step_func_done(), (error) => t.unreached_func());
+ }, 'background-image sec-fetch-dest - Not sent to non-trustworthy same-origin destination');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpOrigin']);
+
+ declarations.push(`border-image: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'border-image sec-fetch-dest - Not sent to non-trustworthy same-origin destination');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpOrigin']);
+
+ declarations.push(`content: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'content sec-fetch-dest - Not sent to non-trustworthy same-origin destination');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpOrigin']);
+
+ declarations.push(`cursor: url("${url}"), auto;`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'cursor sec-fetch-dest - Not sent to non-trustworthy same-origin destination');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpOrigin']);
+
+ declarations.push(`list-style-image: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'list-style-image sec-fetch-dest - Not sent to non-trustworthy same-origin destination');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpSameSite']);
+
+ declarations.push(`background-image: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ })
+ .then(t.step_func_done(), (error) => t.unreached_func());
+ }, 'background-image sec-fetch-dest - Not sent to non-trustworthy same-site destination');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpSameSite']);
+
+ declarations.push(`border-image: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'border-image sec-fetch-dest - Not sent to non-trustworthy same-site destination');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpSameSite']);
+
+ declarations.push(`content: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'content sec-fetch-dest - Not sent to non-trustworthy same-site destination');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpSameSite']);
+
+ declarations.push(`cursor: url("${url}"), auto;`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'cursor sec-fetch-dest - Not sent to non-trustworthy same-site destination');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpSameSite']);
+
+ declarations.push(`list-style-image: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'list-style-image sec-fetch-dest - Not sent to non-trustworthy same-site destination');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpCrossSite']);
+
+ declarations.push(`background-image: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ })
+ .then(t.step_func_done(), (error) => t.unreached_func());
+ }, 'background-image sec-fetch-dest - Not sent to non-trustworthy cross-site destination');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpCrossSite']);
+
+ declarations.push(`border-image: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'border-image sec-fetch-dest - Not sent to non-trustworthy cross-site destination');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpCrossSite']);
+
+ declarations.push(`content: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'content sec-fetch-dest - Not sent to non-trustworthy cross-site destination');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpCrossSite']);
+
+ declarations.push(`cursor: url("${url}"), auto;`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'cursor sec-fetch-dest - Not sent to non-trustworthy cross-site destination');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpCrossSite']);
+
+ declarations.push(`list-style-image: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'list-style-image sec-fetch-dest - Not sent to non-trustworthy cross-site destination');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpOrigin']);
+
+ declarations.push(`background-image: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ })
+ .then(t.step_func_done(), (error) => t.unreached_func());
+ }, 'background-image sec-fetch-user - Not sent to non-trustworthy same-origin destination');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpOrigin']);
+
+ declarations.push(`border-image: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'border-image sec-fetch-user - Not sent to non-trustworthy same-origin destination');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpOrigin']);
+
+ declarations.push(`content: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'content sec-fetch-user - Not sent to non-trustworthy same-origin destination');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpOrigin']);
+
+ declarations.push(`cursor: url("${url}"), auto;`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'cursor sec-fetch-user - Not sent to non-trustworthy same-origin destination');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpOrigin']);
+
+ declarations.push(`list-style-image: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'list-style-image sec-fetch-user - Not sent to non-trustworthy same-origin destination');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpSameSite']);
+
+ declarations.push(`background-image: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ })
+ .then(t.step_func_done(), (error) => t.unreached_func());
+ }, 'background-image sec-fetch-user - Not sent to non-trustworthy same-site destination');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpSameSite']);
+
+ declarations.push(`border-image: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'border-image sec-fetch-user - Not sent to non-trustworthy same-site destination');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpSameSite']);
+
+ declarations.push(`content: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'content sec-fetch-user - Not sent to non-trustworthy same-site destination');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpSameSite']);
+
+ declarations.push(`cursor: url("${url}"), auto;`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'cursor sec-fetch-user - Not sent to non-trustworthy same-site destination');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpSameSite']);
+
+ declarations.push(`list-style-image: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'list-style-image sec-fetch-user - Not sent to non-trustworthy same-site destination');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpCrossSite']);
+
+ declarations.push(`background-image: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ })
+ .then(t.step_func_done(), (error) => t.unreached_func());
+ }, 'background-image sec-fetch-user - Not sent to non-trustworthy cross-site destination');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpCrossSite']);
+
+ declarations.push(`border-image: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'border-image sec-fetch-user - Not sent to non-trustworthy cross-site destination');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpCrossSite']);
+
+ declarations.push(`content: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'content sec-fetch-user - Not sent to non-trustworthy cross-site destination');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpCrossSite']);
+
+ declarations.push(`cursor: url("${url}"), auto;`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'cursor sec-fetch-user - Not sent to non-trustworthy cross-site destination');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpCrossSite']);
+
+ declarations.push(`list-style-image: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'list-style-image sec-fetch-user - Not sent to non-trustworthy cross-site destination');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsOrigin', 'httpOrigin']);
+
+ declarations.push(`background-image: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ })
+ .then(t.step_func_done(), (error) => t.unreached_func());
+ }, 'background-image sec-fetch-site - HTTPS downgrade (header not sent)');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsOrigin', 'httpOrigin']);
+
+ declarations.push(`border-image: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'border-image sec-fetch-site - HTTPS downgrade (header not sent)');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsOrigin', 'httpOrigin']);
+
+ declarations.push(`content: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'content sec-fetch-site - HTTPS downgrade (header not sent)');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsOrigin', 'httpOrigin']);
+
+ declarations.push(`cursor: url("${url}"), auto;`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'cursor sec-fetch-site - HTTPS downgrade (header not sent)');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsOrigin', 'httpOrigin']);
+
+ declarations.push(`list-style-image: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'list-style-image sec-fetch-site - HTTPS downgrade (header not sent)');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpOrigin', 'httpsOrigin']);
+
+ declarations.push(`background-image: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_equals(headers['sec-fetch-site'], 'cross-site');
+ })
+ .then(t.step_func_done(), (error) => t.unreached_func());
+ }, 'background-image sec-fetch-site - HTTPS upgrade');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpOrigin', 'httpsOrigin']);
+
+ declarations.push(`border-image: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'border-image sec-fetch-site - HTTPS upgrade');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpOrigin', 'httpsOrigin']);
+
+ declarations.push(`content: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'content sec-fetch-site - HTTPS upgrade');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpOrigin', 'httpsOrigin']);
+
+ declarations.push(`cursor: url("${url}"), auto;`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'cursor sec-fetch-site - HTTPS upgrade');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpOrigin', 'httpsOrigin']);
+
+ declarations.push(`list-style-image: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'list-style-image sec-fetch-site - HTTPS upgrade');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin']);
+
+ declarations.push(`background-image: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_equals(headers['sec-fetch-site'], 'cross-site');
+ })
+ .then(t.step_func_done(), (error) => t.unreached_func());
+ }, 'background-image sec-fetch-site - HTTPS downgrade-upgrade');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin']);
+
+ declarations.push(`border-image: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'border-image sec-fetch-site - HTTPS downgrade-upgrade');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin']);
+
+ declarations.push(`content: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'content sec-fetch-site - HTTPS downgrade-upgrade');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin']);
+
+ declarations.push(`cursor: url("${url}"), auto;`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'cursor sec-fetch-site - HTTPS downgrade-upgrade');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin']);
+
+ declarations.push(`list-style-image: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'list-style-image sec-fetch-site - HTTPS downgrade-upgrade');
+
+ iframe.srcdoc = declarations.map((declaration, index) => `
+ <style>.el${index} { ${declaration} }</style><div class="el${index}"></div>`
+ ).join('');
+ document.body.appendChild(iframe);
+
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/fetch/metadata/generated/element-a.https.sub.html b/test/wpt/tests/fetch/metadata/generated/element-a.https.sub.html
new file mode 100644
index 0000000..dffd36c
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/generated/element-a.https.sub.html
@@ -0,0 +1,482 @@
+<!DOCTYPE html>
+<!--
+This test was procedurally generated. Please do not modify it directly.
+Sources:
+- fetch/metadata/tools/fetch-metadata.conf.yml
+- fetch/metadata/tools/templates/element-a.sub.html
+-->
+<html lang="en">
+ <meta charset="utf-8">
+ <meta name="timeout" content="long">
+ <title>HTTP headers on request for HTML "a" element navigation</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/resources/testdriver.js"></script>
+ <script src="/resources/testdriver-vendor.js"></script>
+ <script src="/fetch/metadata/resources/helper.sub.js"></script>
+ <body>
+ <script>
+ 'use strict';
+
+ function induceRequest(url, {test, userActivated, attributes}) {
+ const win = window.open();
+ const anchor = win.document.createElement('a');
+ anchor.setAttribute('href', url);
+
+ for (const [ name, value ] of Object.entries(attributes)) {
+ anchor.setAttribute(name, value);
+ }
+
+ win.document.body.appendChild(anchor);
+
+ test.add_cleanup(() => win.close());
+
+ if (userActivated) {
+ test_driver.bless('enable user activation', () => anchor.click());
+ } else {
+ anchor.click();
+ }
+ }
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ makeRequestURL(key, ['httpsOrigin'], {mime: 'text/html'}),
+ {
+ test: t,
+ userActivated: false,
+ attributes: {}
+ }
+ );
+
+ // `induceRequest` does not necessarily trigger a navigation, so the Python
+ // handler must be polled until it has received the initial request.
+ return retrieve(key, {poll: true})
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-origin']);
+ });
+ }, 'sec-fetch-site - Same origin - no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ makeRequestURL(key, ['httpsCrossSite'], {mime: 'text/html'}),
+ {
+ test: t,
+ userActivated: false,
+ attributes: {}
+ }
+ );
+
+ // `induceRequest` does not necessarily trigger a navigation, so the Python
+ // handler must be polled until it has received the initial request.
+ return retrieve(key, {poll: true})
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-site - no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ makeRequestURL(key, ['httpsSameSite'], {mime: 'text/html'}),
+ {
+ test: t,
+ userActivated: false,
+ attributes: {}
+ }
+ );
+
+ // `induceRequest` does not necessarily trigger a navigation, so the Python
+ // handler must be polled until it has received the initial request.
+ return retrieve(key, {poll: true})
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same site - no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite', 'httpsOrigin'], {mime: 'text/html'}),
+ {
+ test: t,
+ userActivated: false,
+ attributes: {}
+ }
+ );
+
+ // `induceRequest` does not necessarily trigger a navigation, so the Python
+ // handler must be polled until it has received the initial request.
+ return retrieve(key, {poll: true})
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Cross-Site -> Same-Origin redirect - no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpsSameSite', 'httpsOrigin'], {mime: 'text/html'}),
+ {
+ test: t,
+ userActivated: false,
+ attributes: {}
+ }
+ );
+
+ // `induceRequest` does not necessarily trigger a navigation, so the Python
+ // handler must be polled until it has received the initial request.
+ return retrieve(key, {poll: true})
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Same-Site -> Same-Origin redirect - no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ makeRequestURL(key, ['httpsCrossSite', 'httpsOrigin'], {mime: 'text/html'}),
+ {
+ test: t,
+ userActivated: false,
+ attributes: {}
+ }
+ );
+
+ // `induceRequest` does not necessarily trigger a navigation, so the Python
+ // handler must be polled until it has received the initial request.
+ return retrieve(key, {poll: true})
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-Site -> Same Origin - no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ makeRequestURL(key, ['httpsCrossSite', 'httpsSameSite'], {mime: 'text/html'}),
+ {
+ test: t,
+ userActivated: false,
+ attributes: {}
+ }
+ );
+
+ // `induceRequest` does not necessarily trigger a navigation, so the Python
+ // handler must be polled until it has received the initial request.
+ return retrieve(key, {poll: true})
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-Site -> Same-Site - no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ makeRequestURL(key, ['httpsCrossSite', 'httpsCrossSite'], {mime: 'text/html'}),
+ {
+ test: t,
+ userActivated: false,
+ attributes: {}
+ }
+ );
+
+ // `induceRequest` does not necessarily trigger a navigation, so the Python
+ // handler must be polled until it has received the initial request.
+ return retrieve(key, {poll: true})
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-Site -> Cross-Site - no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpsOrigin'], {mime: 'text/html'}),
+ {
+ test: t,
+ userActivated: false,
+ attributes: {}
+ }
+ );
+
+ // `induceRequest` does not necessarily trigger a navigation, so the Python
+ // handler must be polled until it has received the initial request.
+ return retrieve(key, {poll: true})
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-origin']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Same Origin - no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpsSameSite'], {mime: 'text/html'}),
+ {
+ test: t,
+ userActivated: false,
+ attributes: {}
+ }
+ );
+
+ // `induceRequest` does not necessarily trigger a navigation, so the Python
+ // handler must be polled until it has received the initial request.
+ return retrieve(key, {poll: true})
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Same-Site - no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite'], {mime: 'text/html'}),
+ {
+ test: t,
+ userActivated: false,
+ attributes: {}
+ }
+ );
+
+ // `induceRequest` does not necessarily trigger a navigation, so the Python
+ // handler must be polled until it has received the initial request.
+ return retrieve(key, {poll: true})
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Cross-Site - no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ makeRequestURL(key, ['httpsSameSite', 'httpsOrigin'], {mime: 'text/html'}),
+ {
+ test: t,
+ userActivated: false,
+ attributes: {}
+ }
+ );
+
+ // `induceRequest` does not necessarily trigger a navigation, so the Python
+ // handler must be polled until it has received the initial request.
+ return retrieve(key, {poll: true})
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Site -> Same Origin - no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ makeRequestURL(key, ['httpsSameSite', 'httpsSameSite'], {mime: 'text/html'}),
+ {
+ test: t,
+ userActivated: false,
+ attributes: {}
+ }
+ );
+
+ // `induceRequest` does not necessarily trigger a navigation, so the Python
+ // handler must be polled until it has received the initial request.
+ return retrieve(key, {poll: true})
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Site -> Same-Site - no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ makeRequestURL(key, ['httpsSameSite', 'httpsCrossSite'], {mime: 'text/html'}),
+ {
+ test: t,
+ userActivated: false,
+ attributes: {}
+ }
+ );
+
+ // `induceRequest` does not necessarily trigger a navigation, so the Python
+ // handler must be polled until it has received the initial request.
+ return retrieve(key, {poll: true})
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Same-Site -> Cross-Site - no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin'], {mime: 'text/html'}),
+ {
+ test: t,
+ userActivated: false,
+ attributes: {}
+ }
+ );
+
+ // `induceRequest` does not necessarily trigger a navigation, so the Python
+ // handler must be polled until it has received the initial request.
+ return retrieve(key, {poll: true})
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - HTTPS downgrade-upgrade - no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ makeRequestURL(key, [], {mime: 'text/html'}),
+ {
+ test: t,
+ userActivated: false,
+ attributes: {}
+ }
+ );
+
+ // `induceRequest` does not necessarily trigger a navigation, so the Python
+ // handler must be polled until it has received the initial request.
+ return retrieve(key, {poll: true})
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-mode');
+ assert_array_equals(headers['sec-fetch-mode'], ['navigate']);
+ });
+ }, 'sec-fetch-mode - no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ makeRequestURL(key, [], {mime: 'text/html'}),
+ {
+ test: t,
+ userActivated: false,
+ attributes: {"download": ""}
+ }
+ );
+
+ // `induceRequest` does not necessarily trigger a navigation, so the Python
+ // handler must be polled until it has received the initial request.
+ return retrieve(key, {poll: true})
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-mode');
+ assert_array_equals(headers['sec-fetch-mode'], ['no-cors']);
+ });
+ }, 'sec-fetch-mode - attributes: download');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ makeRequestURL(key, [], {mime: 'text/html'}),
+ {
+ test: t,
+ userActivated: false,
+ attributes: {}
+ }
+ );
+
+ // `induceRequest` does not necessarily trigger a navigation, so the Python
+ // handler must be polled until it has received the initial request.
+ return retrieve(key, {poll: true})
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-dest');
+ assert_array_equals(headers['sec-fetch-dest'], ['document']);
+ });
+ }, 'sec-fetch-dest - no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ makeRequestURL(key, [], {mime: 'text/html'}),
+ {
+ test: t,
+ userActivated: false,
+ attributes: {"download": ""}
+ }
+ );
+
+ // `induceRequest` does not necessarily trigger a navigation, so the Python
+ // handler must be polled until it has received the initial request.
+ return retrieve(key, {poll: true})
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-dest');
+ assert_array_equals(headers['sec-fetch-dest'], ['empty']);
+ });
+ }, 'sec-fetch-dest - attributes: download');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ makeRequestURL(key, [], {mime: 'text/html'}),
+ {
+ test: t,
+ userActivated: false,
+ attributes: {}
+ }
+ );
+
+ // `induceRequest` does not necessarily trigger a navigation, so the Python
+ // handler must be polled until it has received the initial request.
+ return retrieve(key, {poll: true})
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ makeRequestURL(key, [], {mime: 'text/html'}),
+ {
+ test: t,
+ userActivated: true,
+ attributes: {}
+ }
+ );
+
+ // `induceRequest` does not necessarily trigger a navigation, so the Python
+ // handler must be polled until it has received the initial request.
+ return retrieve(key, {poll: true})
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-user');
+ assert_array_equals(headers['sec-fetch-user'], ['?1']);
+ });
+ }, 'sec-fetch-user - no attributes with user activation');
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/fetch/metadata/generated/element-a.sub.html b/test/wpt/tests/fetch/metadata/generated/element-a.sub.html
new file mode 100644
index 0000000..0661de3
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/generated/element-a.sub.html
@@ -0,0 +1,342 @@
+<!DOCTYPE html>
+<!--
+This test was procedurally generated. Please do not modify it directly.
+Sources:
+- fetch/metadata/tools/fetch-metadata.conf.yml
+- fetch/metadata/tools/templates/element-a.sub.html
+-->
+<html lang="en">
+ <meta charset="utf-8">
+ <meta name="timeout" content="long">
+ <title>HTTP headers on request for HTML "a" element navigation</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/fetch/metadata/resources/helper.sub.js"></script>
+ <body>
+ <script>
+ 'use strict';
+
+ function induceRequest(url, {test, userActivated, attributes}) {
+ const win = window.open();
+ const anchor = win.document.createElement('a');
+ anchor.setAttribute('href', url);
+
+ for (const [ name, value ] of Object.entries(attributes)) {
+ anchor.setAttribute(name, value);
+ }
+
+ win.document.body.appendChild(anchor);
+
+ test.add_cleanup(() => win.close());
+
+ if (userActivated) {
+ test_driver.bless('enable user activation', () => anchor.click());
+ } else {
+ anchor.click();
+ }
+ }
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ makeRequestURL(key, ['httpOrigin'], {mime: 'text/html'}),
+ {
+ test: t,
+ userActivated: false,
+ attributes: {}
+ }
+ );
+
+ // `induceRequest` does not necessarily trigger a navigation, so the Python
+ // handler must be polled until it has received the initial request.
+ return retrieve(key, {poll: true})
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - Not sent to non-trustworthy same-origin destination - no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ makeRequestURL(key, ['httpSameSite'], {mime: 'text/html'}),
+ {
+ test: t,
+ userActivated: false,
+ attributes: {}
+ }
+ );
+
+ // `induceRequest` does not necessarily trigger a navigation, so the Python
+ // handler must be polled until it has received the initial request.
+ return retrieve(key, {poll: true})
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - Not sent to non-trustworthy same-site destination - no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ makeRequestURL(key, ['httpCrossSite'], {mime: 'text/html'}),
+ {
+ test: t,
+ userActivated: false,
+ attributes: {}
+ }
+ );
+
+ // `induceRequest` does not necessarily trigger a navigation, so the Python
+ // handler must be polled until it has received the initial request.
+ return retrieve(key, {poll: true})
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - Not sent to non-trustworthy cross-site destination - no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ makeRequestURL(key, ['httpOrigin'], {mime: 'text/html'}),
+ {
+ test: t,
+ userActivated: false,
+ attributes: {}
+ }
+ );
+
+ // `induceRequest` does not necessarily trigger a navigation, so the Python
+ // handler must be polled until it has received the initial request.
+ return retrieve(key, {poll: true})
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ });
+ }, 'sec-fetch-mode - Not sent to non-trustworthy same-origin destination - no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ makeRequestURL(key, ['httpSameSite'], {mime: 'text/html'}),
+ {
+ test: t,
+ userActivated: false,
+ attributes: {}
+ }
+ );
+
+ // `induceRequest` does not necessarily trigger a navigation, so the Python
+ // handler must be polled until it has received the initial request.
+ return retrieve(key, {poll: true})
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ });
+ }, 'sec-fetch-mode - Not sent to non-trustworthy same-site destination - no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ makeRequestURL(key, ['httpCrossSite'], {mime: 'text/html'}),
+ {
+ test: t,
+ userActivated: false,
+ attributes: {}
+ }
+ );
+
+ // `induceRequest` does not necessarily trigger a navigation, so the Python
+ // handler must be polled until it has received the initial request.
+ return retrieve(key, {poll: true})
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ });
+ }, 'sec-fetch-mode - Not sent to non-trustworthy cross-site destination - no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ makeRequestURL(key, ['httpOrigin'], {mime: 'text/html'}),
+ {
+ test: t,
+ userActivated: false,
+ attributes: {}
+ }
+ );
+
+ // `induceRequest` does not necessarily trigger a navigation, so the Python
+ // handler must be polled until it has received the initial request.
+ return retrieve(key, {poll: true})
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ });
+ }, 'sec-fetch-dest - Not sent to non-trustworthy same-origin destination - no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ makeRequestURL(key, ['httpSameSite'], {mime: 'text/html'}),
+ {
+ test: t,
+ userActivated: false,
+ attributes: {}
+ }
+ );
+
+ // `induceRequest` does not necessarily trigger a navigation, so the Python
+ // handler must be polled until it has received the initial request.
+ return retrieve(key, {poll: true})
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ });
+ }, 'sec-fetch-dest - Not sent to non-trustworthy same-site destination - no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ makeRequestURL(key, ['httpCrossSite'], {mime: 'text/html'}),
+ {
+ test: t,
+ userActivated: false,
+ attributes: {}
+ }
+ );
+
+ // `induceRequest` does not necessarily trigger a navigation, so the Python
+ // handler must be polled until it has received the initial request.
+ return retrieve(key, {poll: true})
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ });
+ }, 'sec-fetch-dest - Not sent to non-trustworthy cross-site destination - no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ makeRequestURL(key, ['httpOrigin'], {mime: 'text/html'}),
+ {
+ test: t,
+ userActivated: false,
+ attributes: {}
+ }
+ );
+
+ // `induceRequest` does not necessarily trigger a navigation, so the Python
+ // handler must be polled until it has received the initial request.
+ return retrieve(key, {poll: true})
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - Not sent to non-trustworthy same-origin destination - no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ makeRequestURL(key, ['httpSameSite'], {mime: 'text/html'}),
+ {
+ test: t,
+ userActivated: false,
+ attributes: {}
+ }
+ );
+
+ // `induceRequest` does not necessarily trigger a navigation, so the Python
+ // handler must be polled until it has received the initial request.
+ return retrieve(key, {poll: true})
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - Not sent to non-trustworthy same-site destination - no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ makeRequestURL(key, ['httpCrossSite'], {mime: 'text/html'}),
+ {
+ test: t,
+ userActivated: false,
+ attributes: {}
+ }
+ );
+
+ // `induceRequest` does not necessarily trigger a navigation, so the Python
+ // handler must be polled until it has received the initial request.
+ return retrieve(key, {poll: true})
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - Not sent to non-trustworthy cross-site destination - no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpOrigin'], {mime: 'text/html'}),
+ {
+ test: t,
+ userActivated: false,
+ attributes: {}
+ }
+ );
+
+ // `induceRequest` does not necessarily trigger a navigation, so the Python
+ // handler must be polled until it has received the initial request.
+ return retrieve(key, {poll: true})
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - HTTPS downgrade (header not sent) - no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ makeRequestURL(key, ['httpOrigin', 'httpsOrigin'], {mime: 'text/html'}),
+ {
+ test: t,
+ userActivated: false,
+ attributes: {}
+ }
+ );
+
+ // `induceRequest` does not necessarily trigger a navigation, so the Python
+ // handler must be polled until it has received the initial request.
+ return retrieve(key, {poll: true})
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - HTTPS upgrade - no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin'], {mime: 'text/html'}),
+ {
+ test: t,
+ userActivated: false,
+ attributes: {}
+ }
+ );
+
+ // `induceRequest` does not necessarily trigger a navigation, so the Python
+ // handler must be polled until it has received the initial request.
+ return retrieve(key, {poll: true})
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - HTTPS downgrade-upgrade - no attributes');
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/fetch/metadata/generated/element-area.https.sub.html b/test/wpt/tests/fetch/metadata/generated/element-area.https.sub.html
new file mode 100644
index 0000000..be3f5f9
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/generated/element-area.https.sub.html
@@ -0,0 +1,482 @@
+<!DOCTYPE html>
+<!--
+This test was procedurally generated. Please do not modify it directly.
+Sources:
+- fetch/metadata/tools/fetch-metadata.conf.yml
+- fetch/metadata/tools/templates/element-area.sub.html
+-->
+<html lang="en">
+ <meta charset="utf-8">
+ <meta name="timeout" content="long">
+ <title>HTTP headers on request for HTML "area" element navigation</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/resources/testdriver.js"></script>
+ <script src="/resources/testdriver-vendor.js"></script>
+ <script src="/fetch/metadata/resources/helper.sub.js"></script>
+ <body>
+ <script>
+ 'use strict';
+
+ function induceRequest(url, {test, userActivated, attributes}) {
+ const win = window.open();
+ const area = win.document.createElement('area');
+ area.setAttribute('href', url);
+
+ for (const [ name, value ] of Object.entries(attributes)) {
+ area.setAttribute(name, value);
+ }
+
+ win.document.body.appendChild(area);
+
+ test.add_cleanup(() => win.close());
+
+ if (userActivated) {
+ test_driver.bless('enable user activation', () => area.click());
+ } else {
+ area.click();
+ }
+ }
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ makeRequestURL(key, ['httpsOrigin'], {mime: 'text/html'}),
+ {
+ test: t,
+ userActivated: false,
+ attributes: {}
+ }
+ );
+
+ // `induceRequest` does not necessarily trigger a navigation, so the Python
+ // handler must be polled until it has received the initial request.
+ return retrieve(key, {poll: true})
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-origin']);
+ });
+ }, 'sec-fetch-site - Same origin - no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ makeRequestURL(key, ['httpsCrossSite'], {mime: 'text/html'}),
+ {
+ test: t,
+ userActivated: false,
+ attributes: {}
+ }
+ );
+
+ // `induceRequest` does not necessarily trigger a navigation, so the Python
+ // handler must be polled until it has received the initial request.
+ return retrieve(key, {poll: true})
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-site - no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ makeRequestURL(key, ['httpsSameSite'], {mime: 'text/html'}),
+ {
+ test: t,
+ userActivated: false,
+ attributes: {}
+ }
+ );
+
+ // `induceRequest` does not necessarily trigger a navigation, so the Python
+ // handler must be polled until it has received the initial request.
+ return retrieve(key, {poll: true})
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same site - no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite', 'httpsOrigin'], {mime: 'text/html'}),
+ {
+ test: t,
+ userActivated: false,
+ attributes: {}
+ }
+ );
+
+ // `induceRequest` does not necessarily trigger a navigation, so the Python
+ // handler must be polled until it has received the initial request.
+ return retrieve(key, {poll: true})
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Cross-Site -> Same-Origin redirect - no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpsSameSite', 'httpsOrigin'], {mime: 'text/html'}),
+ {
+ test: t,
+ userActivated: false,
+ attributes: {}
+ }
+ );
+
+ // `induceRequest` does not necessarily trigger a navigation, so the Python
+ // handler must be polled until it has received the initial request.
+ return retrieve(key, {poll: true})
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Same-Site -> Same-Origin redirect - no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ makeRequestURL(key, ['httpsCrossSite', 'httpsOrigin'], {mime: 'text/html'}),
+ {
+ test: t,
+ userActivated: false,
+ attributes: {}
+ }
+ );
+
+ // `induceRequest` does not necessarily trigger a navigation, so the Python
+ // handler must be polled until it has received the initial request.
+ return retrieve(key, {poll: true})
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-Site -> Same Origin - no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ makeRequestURL(key, ['httpsCrossSite', 'httpsSameSite'], {mime: 'text/html'}),
+ {
+ test: t,
+ userActivated: false,
+ attributes: {}
+ }
+ );
+
+ // `induceRequest` does not necessarily trigger a navigation, so the Python
+ // handler must be polled until it has received the initial request.
+ return retrieve(key, {poll: true})
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-Site -> Same-Site - no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ makeRequestURL(key, ['httpsCrossSite', 'httpsCrossSite'], {mime: 'text/html'}),
+ {
+ test: t,
+ userActivated: false,
+ attributes: {}
+ }
+ );
+
+ // `induceRequest` does not necessarily trigger a navigation, so the Python
+ // handler must be polled until it has received the initial request.
+ return retrieve(key, {poll: true})
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-Site -> Cross-Site - no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpsOrigin'], {mime: 'text/html'}),
+ {
+ test: t,
+ userActivated: false,
+ attributes: {}
+ }
+ );
+
+ // `induceRequest` does not necessarily trigger a navigation, so the Python
+ // handler must be polled until it has received the initial request.
+ return retrieve(key, {poll: true})
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-origin']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Same Origin - no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpsSameSite'], {mime: 'text/html'}),
+ {
+ test: t,
+ userActivated: false,
+ attributes: {}
+ }
+ );
+
+ // `induceRequest` does not necessarily trigger a navigation, so the Python
+ // handler must be polled until it has received the initial request.
+ return retrieve(key, {poll: true})
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Same-Site - no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite'], {mime: 'text/html'}),
+ {
+ test: t,
+ userActivated: false,
+ attributes: {}
+ }
+ );
+
+ // `induceRequest` does not necessarily trigger a navigation, so the Python
+ // handler must be polled until it has received the initial request.
+ return retrieve(key, {poll: true})
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Cross-Site - no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ makeRequestURL(key, ['httpsSameSite', 'httpsOrigin'], {mime: 'text/html'}),
+ {
+ test: t,
+ userActivated: false,
+ attributes: {}
+ }
+ );
+
+ // `induceRequest` does not necessarily trigger a navigation, so the Python
+ // handler must be polled until it has received the initial request.
+ return retrieve(key, {poll: true})
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Site -> Same Origin - no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ makeRequestURL(key, ['httpsSameSite', 'httpsSameSite'], {mime: 'text/html'}),
+ {
+ test: t,
+ userActivated: false,
+ attributes: {}
+ }
+ );
+
+ // `induceRequest` does not necessarily trigger a navigation, so the Python
+ // handler must be polled until it has received the initial request.
+ return retrieve(key, {poll: true})
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Site -> Same-Site - no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ makeRequestURL(key, ['httpsSameSite', 'httpsCrossSite'], {mime: 'text/html'}),
+ {
+ test: t,
+ userActivated: false,
+ attributes: {}
+ }
+ );
+
+ // `induceRequest` does not necessarily trigger a navigation, so the Python
+ // handler must be polled until it has received the initial request.
+ return retrieve(key, {poll: true})
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Same-Site -> Cross-Site - no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin'], {mime: 'text/html'}),
+ {
+ test: t,
+ userActivated: false,
+ attributes: {}
+ }
+ );
+
+ // `induceRequest` does not necessarily trigger a navigation, so the Python
+ // handler must be polled until it has received the initial request.
+ return retrieve(key, {poll: true})
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - HTTPS downgrade-upgrade - no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ makeRequestURL(key, [], {mime: 'text/html'}),
+ {
+ test: t,
+ userActivated: false,
+ attributes: {}
+ }
+ );
+
+ // `induceRequest` does not necessarily trigger a navigation, so the Python
+ // handler must be polled until it has received the initial request.
+ return retrieve(key, {poll: true})
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-mode');
+ assert_array_equals(headers['sec-fetch-mode'], ['navigate']);
+ });
+ }, 'sec-fetch-mode - no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ makeRequestURL(key, [], {mime: 'text/html'}),
+ {
+ test: t,
+ userActivated: false,
+ attributes: {"download": ""}
+ }
+ );
+
+ // `induceRequest` does not necessarily trigger a navigation, so the Python
+ // handler must be polled until it has received the initial request.
+ return retrieve(key, {poll: true})
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-mode');
+ assert_array_equals(headers['sec-fetch-mode'], ['no-cors']);
+ });
+ }, 'sec-fetch-mode - attributes: download');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ makeRequestURL(key, [], {mime: 'text/html'}),
+ {
+ test: t,
+ userActivated: false,
+ attributes: {}
+ }
+ );
+
+ // `induceRequest` does not necessarily trigger a navigation, so the Python
+ // handler must be polled until it has received the initial request.
+ return retrieve(key, {poll: true})
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-dest');
+ assert_array_equals(headers['sec-fetch-dest'], ['document']);
+ });
+ }, 'sec-fetch-dest - no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ makeRequestURL(key, [], {mime: 'text/html'}),
+ {
+ test: t,
+ userActivated: false,
+ attributes: {"download": ""}
+ }
+ );
+
+ // `induceRequest` does not necessarily trigger a navigation, so the Python
+ // handler must be polled until it has received the initial request.
+ return retrieve(key, {poll: true})
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-dest');
+ assert_array_equals(headers['sec-fetch-dest'], ['empty']);
+ });
+ }, 'sec-fetch-dest - attributes: download');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ makeRequestURL(key, [], {mime: 'text/html'}),
+ {
+ test: t,
+ userActivated: false,
+ attributes: {}
+ }
+ );
+
+ // `induceRequest` does not necessarily trigger a navigation, so the Python
+ // handler must be polled until it has received the initial request.
+ return retrieve(key, {poll: true})
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ makeRequestURL(key, [], {mime: 'text/html'}),
+ {
+ test: t,
+ userActivated: true,
+ attributes: {}
+ }
+ );
+
+ // `induceRequest` does not necessarily trigger a navigation, so the Python
+ // handler must be polled until it has received the initial request.
+ return retrieve(key, {poll: true})
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-user');
+ assert_array_equals(headers['sec-fetch-user'], ['?1']);
+ });
+ }, 'sec-fetch-user - no attributes with user activation');
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/fetch/metadata/generated/element-area.sub.html b/test/wpt/tests/fetch/metadata/generated/element-area.sub.html
new file mode 100644
index 0000000..5f5c338
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/generated/element-area.sub.html
@@ -0,0 +1,342 @@
+<!DOCTYPE html>
+<!--
+This test was procedurally generated. Please do not modify it directly.
+Sources:
+- fetch/metadata/tools/fetch-metadata.conf.yml
+- fetch/metadata/tools/templates/element-area.sub.html
+-->
+<html lang="en">
+ <meta charset="utf-8">
+ <meta name="timeout" content="long">
+ <title>HTTP headers on request for HTML "area" element navigation</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/fetch/metadata/resources/helper.sub.js"></script>
+ <body>
+ <script>
+ 'use strict';
+
+ function induceRequest(url, {test, userActivated, attributes}) {
+ const win = window.open();
+ const area = win.document.createElement('area');
+ area.setAttribute('href', url);
+
+ for (const [ name, value ] of Object.entries(attributes)) {
+ area.setAttribute(name, value);
+ }
+
+ win.document.body.appendChild(area);
+
+ test.add_cleanup(() => win.close());
+
+ if (userActivated) {
+ test_driver.bless('enable user activation', () => area.click());
+ } else {
+ area.click();
+ }
+ }
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ makeRequestURL(key, ['httpOrigin'], {mime: 'text/html'}),
+ {
+ test: t,
+ userActivated: false,
+ attributes: {}
+ }
+ );
+
+ // `induceRequest` does not necessarily trigger a navigation, so the Python
+ // handler must be polled until it has received the initial request.
+ return retrieve(key, {poll: true})
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - Not sent to non-trustworthy same-origin destination - no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ makeRequestURL(key, ['httpSameSite'], {mime: 'text/html'}),
+ {
+ test: t,
+ userActivated: false,
+ attributes: {}
+ }
+ );
+
+ // `induceRequest` does not necessarily trigger a navigation, so the Python
+ // handler must be polled until it has received the initial request.
+ return retrieve(key, {poll: true})
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - Not sent to non-trustworthy same-site destination - no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ makeRequestURL(key, ['httpCrossSite'], {mime: 'text/html'}),
+ {
+ test: t,
+ userActivated: false,
+ attributes: {}
+ }
+ );
+
+ // `induceRequest` does not necessarily trigger a navigation, so the Python
+ // handler must be polled until it has received the initial request.
+ return retrieve(key, {poll: true})
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - Not sent to non-trustworthy cross-site destination - no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ makeRequestURL(key, ['httpOrigin'], {mime: 'text/html'}),
+ {
+ test: t,
+ userActivated: false,
+ attributes: {}
+ }
+ );
+
+ // `induceRequest` does not necessarily trigger a navigation, so the Python
+ // handler must be polled until it has received the initial request.
+ return retrieve(key, {poll: true})
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ });
+ }, 'sec-fetch-mode - Not sent to non-trustworthy same-origin destination - no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ makeRequestURL(key, ['httpSameSite'], {mime: 'text/html'}),
+ {
+ test: t,
+ userActivated: false,
+ attributes: {}
+ }
+ );
+
+ // `induceRequest` does not necessarily trigger a navigation, so the Python
+ // handler must be polled until it has received the initial request.
+ return retrieve(key, {poll: true})
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ });
+ }, 'sec-fetch-mode - Not sent to non-trustworthy same-site destination - no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ makeRequestURL(key, ['httpCrossSite'], {mime: 'text/html'}),
+ {
+ test: t,
+ userActivated: false,
+ attributes: {}
+ }
+ );
+
+ // `induceRequest` does not necessarily trigger a navigation, so the Python
+ // handler must be polled until it has received the initial request.
+ return retrieve(key, {poll: true})
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ });
+ }, 'sec-fetch-mode - Not sent to non-trustworthy cross-site destination - no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ makeRequestURL(key, ['httpOrigin'], {mime: 'text/html'}),
+ {
+ test: t,
+ userActivated: false,
+ attributes: {}
+ }
+ );
+
+ // `induceRequest` does not necessarily trigger a navigation, so the Python
+ // handler must be polled until it has received the initial request.
+ return retrieve(key, {poll: true})
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ });
+ }, 'sec-fetch-dest - Not sent to non-trustworthy same-origin destination - no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ makeRequestURL(key, ['httpSameSite'], {mime: 'text/html'}),
+ {
+ test: t,
+ userActivated: false,
+ attributes: {}
+ }
+ );
+
+ // `induceRequest` does not necessarily trigger a navigation, so the Python
+ // handler must be polled until it has received the initial request.
+ return retrieve(key, {poll: true})
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ });
+ }, 'sec-fetch-dest - Not sent to non-trustworthy same-site destination - no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ makeRequestURL(key, ['httpCrossSite'], {mime: 'text/html'}),
+ {
+ test: t,
+ userActivated: false,
+ attributes: {}
+ }
+ );
+
+ // `induceRequest` does not necessarily trigger a navigation, so the Python
+ // handler must be polled until it has received the initial request.
+ return retrieve(key, {poll: true})
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ });
+ }, 'sec-fetch-dest - Not sent to non-trustworthy cross-site destination - no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ makeRequestURL(key, ['httpOrigin'], {mime: 'text/html'}),
+ {
+ test: t,
+ userActivated: false,
+ attributes: {}
+ }
+ );
+
+ // `induceRequest` does not necessarily trigger a navigation, so the Python
+ // handler must be polled until it has received the initial request.
+ return retrieve(key, {poll: true})
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - Not sent to non-trustworthy same-origin destination - no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ makeRequestURL(key, ['httpSameSite'], {mime: 'text/html'}),
+ {
+ test: t,
+ userActivated: false,
+ attributes: {}
+ }
+ );
+
+ // `induceRequest` does not necessarily trigger a navigation, so the Python
+ // handler must be polled until it has received the initial request.
+ return retrieve(key, {poll: true})
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - Not sent to non-trustworthy same-site destination - no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ makeRequestURL(key, ['httpCrossSite'], {mime: 'text/html'}),
+ {
+ test: t,
+ userActivated: false,
+ attributes: {}
+ }
+ );
+
+ // `induceRequest` does not necessarily trigger a navigation, so the Python
+ // handler must be polled until it has received the initial request.
+ return retrieve(key, {poll: true})
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - Not sent to non-trustworthy cross-site destination - no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpOrigin'], {mime: 'text/html'}),
+ {
+ test: t,
+ userActivated: false,
+ attributes: {}
+ }
+ );
+
+ // `induceRequest` does not necessarily trigger a navigation, so the Python
+ // handler must be polled until it has received the initial request.
+ return retrieve(key, {poll: true})
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - HTTPS downgrade (header not sent) - no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ makeRequestURL(key, ['httpOrigin', 'httpsOrigin'], {mime: 'text/html'}),
+ {
+ test: t,
+ userActivated: false,
+ attributes: {}
+ }
+ );
+
+ // `induceRequest` does not necessarily trigger a navigation, so the Python
+ // handler must be polled until it has received the initial request.
+ return retrieve(key, {poll: true})
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - HTTPS upgrade - no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin'], {mime: 'text/html'}),
+ {
+ test: t,
+ userActivated: false,
+ attributes: {}
+ }
+ );
+
+ // `induceRequest` does not necessarily trigger a navigation, so the Python
+ // handler must be polled until it has received the initial request.
+ return retrieve(key, {poll: true})
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - HTTPS downgrade-upgrade - no attributes');
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/fetch/metadata/generated/element-audio.https.sub.html b/test/wpt/tests/fetch/metadata/generated/element-audio.https.sub.html
new file mode 100644
index 0000000..a9d9512
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/generated/element-audio.https.sub.html
@@ -0,0 +1,325 @@
+<!DOCTYPE html>
+<!--
+This test was procedurally generated. Please do not modify it directly.
+Sources:
+- fetch/metadata/tools/fetch-metadata.conf.yml
+- fetch/metadata/tools/templates/element-audio.sub.html
+-->
+<html lang="en">
+ <meta charset="utf-8">
+ <title>HTTP headers on request for HTML "audio" element source</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/fetch/metadata/resources/helper.sub.js"></script>
+ <body>
+ <script>
+ 'use strict';
+
+ function induceRequest(url, attributes) {
+ const audio = document.createElement('audio');
+
+ for (const [ name, value ] of Object.entries(attributes)) {
+ audio.setAttribute(name, value);
+ }
+
+ return new Promise((resolve) => {
+ audio.setAttribute('src', url);
+ audio.onload = audio.onerror = resolve;
+ });
+ }
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin']),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-origin']);
+ });
+ }, 'sec-fetch-site - Same origin, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsCrossSite']),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-site, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsSameSite']),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same site, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite', 'httpsOrigin']),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Cross-Site -> Same-Origin redirect, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpsSameSite', 'httpsOrigin']),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Same-Site -> Same-Origin redirect, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsCrossSite', 'httpsOrigin']),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-Site -> Same Origin, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsCrossSite', 'httpsSameSite']),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-Site -> Same-Site, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsCrossSite', 'httpsCrossSite']),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-Site -> Cross-Site, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpsOrigin']),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-origin']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Same Origin, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpsSameSite']),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Same-Site, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite']),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Cross-Site, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsSameSite', 'httpsOrigin']),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Site -> Same Origin, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsSameSite', 'httpsSameSite']),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Site -> Same-Site, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsSameSite', 'httpsCrossSite']),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Same-Site -> Cross-Site, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin']),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - HTTPS downgrade-upgrade, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, []),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-mode');
+ assert_array_equals(headers['sec-fetch-mode'], ['no-cors']);
+ });
+ }, 'sec-fetch-mode - no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, []),
+ {"crossorigin": ""}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-mode');
+ assert_array_equals(headers['sec-fetch-mode'], ['cors']);
+ });
+ }, 'sec-fetch-mode - attributes: crossorigin');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, []),
+ {"crossorigin": "anonymous"}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-mode');
+ assert_array_equals(headers['sec-fetch-mode'], ['cors']);
+ });
+ }, 'sec-fetch-mode - attributes: crossorigin=anonymous');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, []),
+ {"crossorigin": "use-credentials"}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-mode');
+ assert_array_equals(headers['sec-fetch-mode'], ['cors']);
+ });
+ }, 'sec-fetch-mode - attributes: crossorigin=use-credentials');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, []),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-dest');
+ assert_array_equals(headers['sec-fetch-dest'], ['audio']);
+ });
+ }, 'sec-fetch-dest - no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, []),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - no attributes');
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/fetch/metadata/generated/element-audio.sub.html b/test/wpt/tests/fetch/metadata/generated/element-audio.sub.html
new file mode 100644
index 0000000..2b62632
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/generated/element-audio.sub.html
@@ -0,0 +1,229 @@
+<!DOCTYPE html>
+<!--
+This test was procedurally generated. Please do not modify it directly.
+Sources:
+- fetch/metadata/tools/fetch-metadata.conf.yml
+- fetch/metadata/tools/templates/element-audio.sub.html
+-->
+<html lang="en">
+ <meta charset="utf-8">
+ <title>HTTP headers on request for HTML "audio" element source</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/fetch/metadata/resources/helper.sub.js"></script>
+ <body>
+ <script>
+ 'use strict';
+
+ function induceRequest(url, attributes) {
+ const audio = document.createElement('audio');
+
+ for (const [ name, value ] of Object.entries(attributes)) {
+ audio.setAttribute(name, value);
+ }
+
+ return new Promise((resolve) => {
+ audio.setAttribute('src', url);
+ audio.onload = audio.onerror = resolve;
+ });
+ }
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpOrigin']),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - Not sent to non-trustworthy same-origin destination, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpSameSite']),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - Not sent to non-trustworthy same-site destination, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpCrossSite']),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - Not sent to non-trustworthy cross-site destination, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpOrigin']),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ });
+ }, 'sec-fetch-mode - Not sent to non-trustworthy same-origin destination, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpSameSite']),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ });
+ }, 'sec-fetch-mode - Not sent to non-trustworthy same-site destination, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpCrossSite']),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ });
+ }, 'sec-fetch-mode - Not sent to non-trustworthy cross-site destination, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpOrigin']),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ });
+ }, 'sec-fetch-dest - Not sent to non-trustworthy same-origin destination, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpSameSite']),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ });
+ }, 'sec-fetch-dest - Not sent to non-trustworthy same-site destination, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpCrossSite']),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ });
+ }, 'sec-fetch-dest - Not sent to non-trustworthy cross-site destination, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpOrigin']),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - Not sent to non-trustworthy same-origin destination, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpSameSite']),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - Not sent to non-trustworthy same-site destination, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpCrossSite']),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - Not sent to non-trustworthy cross-site destination, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpOrigin']),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - HTTPS downgrade (header not sent), no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpOrigin', 'httpsOrigin']),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - HTTPS upgrade, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin']),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - HTTPS downgrade-upgrade, no attributes');
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/fetch/metadata/generated/element-embed.https.sub.html b/test/wpt/tests/fetch/metadata/generated/element-embed.https.sub.html
new file mode 100644
index 0000000..819bed8
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/generated/element-embed.https.sub.html
@@ -0,0 +1,224 @@
+<!DOCTYPE html>
+<!--
+This test was procedurally generated. Please do not modify it directly.
+Sources:
+- fetch/metadata/tools/fetch-metadata.conf.yml
+- fetch/metadata/tools/templates/element-embed.sub.html
+-->
+<html lang="en">
+ <meta charset="utf-8">
+ <title>HTTP headers on request for HTML "embed" element source</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/fetch/metadata/resources/helper.sub.js"></script>
+ <body>
+ <script>
+ 'use strict';
+
+ const params = {
+ body: `
+ <svg xmlns="http://www.w3.org/2000/svg" width="123" height="123">
+ <rect fill="lime" width="123" height="123"/>
+ </svg>
+ `,
+ mime: 'image/svg+xml'
+ };
+
+ function induceRequest(t, url) {
+ const embed = document.createElement('embed');
+ embed.setAttribute('src', url);
+ document.body.appendChild(embed);
+
+ t.add_cleanup(() => embed.remove());
+
+ return new Promise((resolve) => embed.addEventListener('load', resolve));
+ }
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(t, makeRequestURL(key, ['httpsOrigin'], params))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-origin']);
+ });
+ }, 'sec-fetch-site - Same origin');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(t, makeRequestURL(key, ['httpsCrossSite'], params))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-site');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(t, makeRequestURL(key, ['httpsSameSite'], params))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same site');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(t, makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite', 'httpsOrigin'], params))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Cross-Site -> Same-Origin redirect');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(t, makeRequestURL(key, ['httpsOrigin', 'httpsSameSite', 'httpsOrigin'], params))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Same-Site -> Same-Origin redirect');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(t, makeRequestURL(key, ['httpsCrossSite', 'httpsOrigin'], params))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-Site -> Same Origin');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(t, makeRequestURL(key, ['httpsCrossSite', 'httpsSameSite'], params))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-Site -> Same-Site');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(t, makeRequestURL(key, ['httpsCrossSite', 'httpsCrossSite'], params))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-Site -> Cross-Site');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(t, makeRequestURL(key, ['httpsOrigin', 'httpsOrigin'], params))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-origin']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Same Origin');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(t, makeRequestURL(key, ['httpsOrigin', 'httpsSameSite'], params))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Same-Site');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(t, makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite'], params))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Cross-Site');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(t, makeRequestURL(key, ['httpsSameSite', 'httpsOrigin'], params))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Site -> Same Origin');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(t, makeRequestURL(key, ['httpsSameSite', 'httpsSameSite'], params))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Site -> Same-Site');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(t, makeRequestURL(key, ['httpsSameSite', 'httpsCrossSite'], params))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Same-Site -> Cross-Site');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(t, makeRequestURL(key, [], params))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-mode');
+ assert_array_equals(headers['sec-fetch-mode'], ['no-cors']);
+ });
+ }, 'sec-fetch-mode');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(t, makeRequestURL(key, [], params))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-dest');
+ assert_array_equals(headers['sec-fetch-dest'], ['embed']);
+ });
+ }, 'sec-fetch-dest');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(t, makeRequestURL(key, [], params))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user');
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/fetch/metadata/generated/element-embed.sub.html b/test/wpt/tests/fetch/metadata/generated/element-embed.sub.html
new file mode 100644
index 0000000..b6e14a5
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/generated/element-embed.sub.html
@@ -0,0 +1,190 @@
+<!DOCTYPE html>
+<!--
+This test was procedurally generated. Please do not modify it directly.
+Sources:
+- fetch/metadata/tools/fetch-metadata.conf.yml
+- fetch/metadata/tools/templates/element-embed.sub.html
+-->
+<html lang="en">
+ <meta charset="utf-8">
+ <title>HTTP headers on request for HTML "embed" element source</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/fetch/metadata/resources/helper.sub.js"></script>
+ <body>
+ <script>
+ 'use strict';
+
+ const params = {
+ body: `
+ <svg xmlns="http://www.w3.org/2000/svg" width="123" height="123">
+ <rect fill="lime" width="123" height="123"/>
+ </svg>
+ `,
+ mime: 'image/svg+xml'
+ };
+
+ function induceRequest(t, url) {
+ const embed = document.createElement('embed');
+ embed.setAttribute('src', url);
+ document.body.appendChild(embed);
+
+ t.add_cleanup(() => embed.remove());
+
+ return new Promise((resolve) => embed.addEventListener('load', resolve));
+ }
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(t, makeRequestURL(key, ['httpOrigin'], params))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - Not sent to non-trustworthy same-origin destination');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(t, makeRequestURL(key, ['httpSameSite'], params))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - Not sent to non-trustworthy same-site destination');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(t, makeRequestURL(key, ['httpCrossSite'], params))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - Not sent to non-trustworthy cross-site destination');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(t, makeRequestURL(key, ['httpOrigin'], params))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ });
+ }, 'sec-fetch-mode - Not sent to non-trustworthy same-origin destination');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(t, makeRequestURL(key, ['httpSameSite'], params))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ });
+ }, 'sec-fetch-mode - Not sent to non-trustworthy same-site destination');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(t, makeRequestURL(key, ['httpCrossSite'], params))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ });
+ }, 'sec-fetch-mode - Not sent to non-trustworthy cross-site destination');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(t, makeRequestURL(key, ['httpOrigin'], params))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ });
+ }, 'sec-fetch-dest - Not sent to non-trustworthy same-origin destination');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(t, makeRequestURL(key, ['httpSameSite'], params))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ });
+ }, 'sec-fetch-dest - Not sent to non-trustworthy same-site destination');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(t, makeRequestURL(key, ['httpCrossSite'], params))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ });
+ }, 'sec-fetch-dest - Not sent to non-trustworthy cross-site destination');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(t, makeRequestURL(key, ['httpOrigin'], params))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - Not sent to non-trustworthy same-origin destination');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(t, makeRequestURL(key, ['httpSameSite'], params))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - Not sent to non-trustworthy same-site destination');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(t, makeRequestURL(key, ['httpCrossSite'], params))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - Not sent to non-trustworthy cross-site destination');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(t, makeRequestURL(key, ['httpsOrigin', 'httpOrigin'], params))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - HTTPS downgrade (header not sent)');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(t, makeRequestURL(key, ['httpOrigin', 'httpsOrigin'], params))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - HTTPS upgrade');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(t, makeRequestURL(key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin'], params))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - HTTPS downgrade-upgrade');
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/fetch/metadata/generated/element-frame.https.sub.html b/test/wpt/tests/fetch/metadata/generated/element-frame.https.sub.html
new file mode 100644
index 0000000..17504ff
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/generated/element-frame.https.sub.html
@@ -0,0 +1,309 @@
+<!DOCTYPE html>
+<!--
+This test was procedurally generated. Please do not modify it directly.
+Sources:
+- fetch/metadata/tools/fetch-metadata.conf.yml
+- fetch/metadata/tools/templates/element-frame.sub.html
+-->
+<html lang="en">
+ <meta charset="utf-8">
+ <title>HTTP headers on request for HTML "frame" element source</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/resources/testdriver.js"></script>
+ <script src="/resources/testdriver-vendor.js"></script>
+ <script src="/fetch/metadata/resources/helper.sub.js"></script>
+ <body>
+ <script>
+ 'use strict';
+
+ function induceRequest(url, test, userActivated) {
+ const frame = document.createElement('frame');
+
+ const setSrc = () => frame.setAttribute('src', url);
+
+ document.body.appendChild(frame);
+ test.add_cleanup(() => frame.remove());
+
+ return new Promise((resolve) => {
+ if (userActivated) {
+ test_driver.bless('enable user activation', setSrc);
+ } else {
+ setSrc();
+ }
+
+ frame.onload = frame.onerror = resolve;
+ });
+ }
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin'], {mime: 'text/html'}),
+ t,
+ false
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-origin']);
+ });
+ }, 'sec-fetch-site - Same origin');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsCrossSite'], {mime: 'text/html'}),
+ t,
+ false
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-site');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsSameSite'], {mime: 'text/html'}),
+ t,
+ false
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same site');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite', 'httpsOrigin'], {mime: 'text/html'}),
+ t,
+ false
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Cross-Site -> Same-Origin redirect');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpsSameSite', 'httpsOrigin'], {mime: 'text/html'}),
+ t,
+ false
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Same-Site -> Same-Origin redirect');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsCrossSite', 'httpsOrigin'], {mime: 'text/html'}),
+ t,
+ false
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-Site -> Same Origin');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsCrossSite', 'httpsSameSite'], {mime: 'text/html'}),
+ t,
+ false
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-Site -> Same-Site');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsCrossSite', 'httpsCrossSite'], {mime: 'text/html'}),
+ t,
+ false
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-Site -> Cross-Site');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpsOrigin'], {mime: 'text/html'}),
+ t,
+ false
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-origin']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Same Origin');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpsSameSite'], {mime: 'text/html'}),
+ t,
+ false
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Same-Site');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite'], {mime: 'text/html'}),
+ t,
+ false
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Cross-Site');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsSameSite', 'httpsOrigin'], {mime: 'text/html'}),
+ t,
+ false
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Site -> Same Origin');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsSameSite', 'httpsSameSite'], {mime: 'text/html'}),
+ t,
+ false
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Site -> Same-Site');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsSameSite', 'httpsCrossSite'], {mime: 'text/html'}),
+ t,
+ false
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Same-Site -> Cross-Site');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, [], {mime: 'text/html'}),
+ t,
+ false
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-mode');
+ assert_array_equals(headers['sec-fetch-mode'], ['navigate']);
+ });
+ }, 'sec-fetch-mode');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, [], {mime: 'text/html'}),
+ t,
+ false
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-dest');
+ assert_array_equals(headers['sec-fetch-dest'], ['frame']);
+ });
+ }, 'sec-fetch-dest');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, [], {mime: 'text/html'}),
+ t,
+ false
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, [], {mime: 'text/html'}),
+ t,
+ true
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-user');
+ assert_array_equals(headers['sec-fetch-user'], ['?1']);
+ });
+ }, 'sec-fetch-user with user activation');
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/fetch/metadata/generated/element-frame.sub.html b/test/wpt/tests/fetch/metadata/generated/element-frame.sub.html
new file mode 100644
index 0000000..2d9a7ec
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/generated/element-frame.sub.html
@@ -0,0 +1,250 @@
+<!DOCTYPE html>
+<!--
+This test was procedurally generated. Please do not modify it directly.
+Sources:
+- fetch/metadata/tools/fetch-metadata.conf.yml
+- fetch/metadata/tools/templates/element-frame.sub.html
+-->
+<html lang="en">
+ <meta charset="utf-8">
+ <title>HTTP headers on request for HTML "frame" element source</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/fetch/metadata/resources/helper.sub.js"></script>
+ <body>
+ <script>
+ 'use strict';
+
+ function induceRequest(url, test, userActivated) {
+ const frame = document.createElement('frame');
+
+ const setSrc = () => frame.setAttribute('src', url);
+
+ document.body.appendChild(frame);
+ test.add_cleanup(() => frame.remove());
+
+ return new Promise((resolve) => {
+ if (userActivated) {
+ test_driver.bless('enable user activation', setSrc);
+ } else {
+ setSrc();
+ }
+
+ frame.onload = frame.onerror = resolve;
+ });
+ }
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpOrigin'], {mime: 'text/html'}),
+ t,
+ false
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - Not sent to non-trustworthy same-origin destination');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpSameSite'], {mime: 'text/html'}),
+ t,
+ false
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - Not sent to non-trustworthy same-site destination');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpCrossSite'], {mime: 'text/html'}),
+ t,
+ false
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - Not sent to non-trustworthy cross-site destination');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpOrigin'], {mime: 'text/html'}),
+ t,
+ false
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ });
+ }, 'sec-fetch-mode - Not sent to non-trustworthy same-origin destination');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpSameSite'], {mime: 'text/html'}),
+ t,
+ false
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ });
+ }, 'sec-fetch-mode - Not sent to non-trustworthy same-site destination');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpCrossSite'], {mime: 'text/html'}),
+ t,
+ false
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ });
+ }, 'sec-fetch-mode - Not sent to non-trustworthy cross-site destination');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpOrigin'], {mime: 'text/html'}),
+ t,
+ false
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ });
+ }, 'sec-fetch-dest - Not sent to non-trustworthy same-origin destination');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpSameSite'], {mime: 'text/html'}),
+ t,
+ false
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ });
+ }, 'sec-fetch-dest - Not sent to non-trustworthy same-site destination');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpCrossSite'], {mime: 'text/html'}),
+ t,
+ false
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ });
+ }, 'sec-fetch-dest - Not sent to non-trustworthy cross-site destination');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpOrigin'], {mime: 'text/html'}),
+ t,
+ false
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - Not sent to non-trustworthy same-origin destination');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpSameSite'], {mime: 'text/html'}),
+ t,
+ false
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - Not sent to non-trustworthy same-site destination');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpCrossSite'], {mime: 'text/html'}),
+ t,
+ false
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - Not sent to non-trustworthy cross-site destination');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpOrigin'], {mime: 'text/html'}),
+ t,
+ false
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - HTTPS downgrade (header not sent)');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpOrigin', 'httpsOrigin'], {mime: 'text/html'}),
+ t,
+ false
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - HTTPS upgrade');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin'], {mime: 'text/html'}),
+ t,
+ false
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - HTTPS downgrade-upgrade');
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/fetch/metadata/generated/element-iframe.https.sub.html b/test/wpt/tests/fetch/metadata/generated/element-iframe.https.sub.html
new file mode 100644
index 0000000..fba1c8b
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/generated/element-iframe.https.sub.html
@@ -0,0 +1,309 @@
+<!DOCTYPE html>
+<!--
+This test was procedurally generated. Please do not modify it directly.
+Sources:
+- fetch/metadata/tools/fetch-metadata.conf.yml
+- fetch/metadata/tools/templates/element-iframe.sub.html
+-->
+<html lang="en">
+ <meta charset="utf-8">
+ <title>HTTP headers on request for HTML "frame" element source</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/resources/testdriver.js"></script>
+ <script src="/resources/testdriver-vendor.js"></script>
+ <script src="/fetch/metadata/resources/helper.sub.js"></script>
+ <body>
+ <script>
+ 'use strict';
+
+ function induceRequest(url, test, userActivated) {
+ const iframe = document.createElement('iframe');
+
+ const setSrc = () => iframe.setAttribute('src', url);
+
+ document.body.appendChild(iframe);
+ test.add_cleanup(() => iframe.remove());
+
+ return new Promise((resolve) => {
+ if (userActivated) {
+ test_driver.bless('enable user activation', setSrc);
+ } else {
+ setSrc();
+ }
+
+ iframe.onload = iframe.onerror = resolve;
+ });
+ }
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin'], {mime: 'text/html'}),
+ t,
+ false
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-origin']);
+ });
+ }, 'sec-fetch-site - Same origin');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsCrossSite'], {mime: 'text/html'}),
+ t,
+ false
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-site');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsSameSite'], {mime: 'text/html'}),
+ t,
+ false
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same site');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite', 'httpsOrigin'], {mime: 'text/html'}),
+ t,
+ false
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Cross-Site -> Same-Origin redirect');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpsSameSite', 'httpsOrigin'], {mime: 'text/html'}),
+ t,
+ false
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Same-Site -> Same-Origin redirect');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsCrossSite', 'httpsOrigin'], {mime: 'text/html'}),
+ t,
+ false
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-Site -> Same Origin');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsCrossSite', 'httpsSameSite'], {mime: 'text/html'}),
+ t,
+ false
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-Site -> Same-Site');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsCrossSite', 'httpsCrossSite'], {mime: 'text/html'}),
+ t,
+ false
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-Site -> Cross-Site');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpsOrigin'], {mime: 'text/html'}),
+ t,
+ false
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-origin']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Same Origin');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpsSameSite'], {mime: 'text/html'}),
+ t,
+ false
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Same-Site');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite'], {mime: 'text/html'}),
+ t,
+ false
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Cross-Site');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsSameSite', 'httpsOrigin'], {mime: 'text/html'}),
+ t,
+ false
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Site -> Same Origin');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsSameSite', 'httpsSameSite'], {mime: 'text/html'}),
+ t,
+ false
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Site -> Same-Site');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsSameSite', 'httpsCrossSite'], {mime: 'text/html'}),
+ t,
+ false
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Same-Site -> Cross-Site');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, [], {mime: 'text/html'}),
+ t,
+ false
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-mode');
+ assert_array_equals(headers['sec-fetch-mode'], ['navigate']);
+ });
+ }, 'sec-fetch-mode');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, [], {mime: 'text/html'}),
+ t,
+ false
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-dest');
+ assert_array_equals(headers['sec-fetch-dest'], ['iframe']);
+ });
+ }, 'sec-fetch-dest');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, [], {mime: 'text/html'}),
+ t,
+ false
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, [], {mime: 'text/html'}),
+ t,
+ true
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-user');
+ assert_array_equals(headers['sec-fetch-user'], ['?1']);
+ });
+ }, 'sec-fetch-user with user activation');
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/fetch/metadata/generated/element-iframe.sub.html b/test/wpt/tests/fetch/metadata/generated/element-iframe.sub.html
new file mode 100644
index 0000000..6f71cc0
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/generated/element-iframe.sub.html
@@ -0,0 +1,250 @@
+<!DOCTYPE html>
+<!--
+This test was procedurally generated. Please do not modify it directly.
+Sources:
+- fetch/metadata/tools/fetch-metadata.conf.yml
+- fetch/metadata/tools/templates/element-iframe.sub.html
+-->
+<html lang="en">
+ <meta charset="utf-8">
+ <title>HTTP headers on request for HTML "frame" element source</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/fetch/metadata/resources/helper.sub.js"></script>
+ <body>
+ <script>
+ 'use strict';
+
+ function induceRequest(url, test, userActivated) {
+ const iframe = document.createElement('iframe');
+
+ const setSrc = () => iframe.setAttribute('src', url);
+
+ document.body.appendChild(iframe);
+ test.add_cleanup(() => iframe.remove());
+
+ return new Promise((resolve) => {
+ if (userActivated) {
+ test_driver.bless('enable user activation', setSrc);
+ } else {
+ setSrc();
+ }
+
+ iframe.onload = iframe.onerror = resolve;
+ });
+ }
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpOrigin'], {mime: 'text/html'}),
+ t,
+ false
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - Not sent to non-trustworthy same-origin destination');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpSameSite'], {mime: 'text/html'}),
+ t,
+ false
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - Not sent to non-trustworthy same-site destination');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpCrossSite'], {mime: 'text/html'}),
+ t,
+ false
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - Not sent to non-trustworthy cross-site destination');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpOrigin'], {mime: 'text/html'}),
+ t,
+ false
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ });
+ }, 'sec-fetch-mode - Not sent to non-trustworthy same-origin destination');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpSameSite'], {mime: 'text/html'}),
+ t,
+ false
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ });
+ }, 'sec-fetch-mode - Not sent to non-trustworthy same-site destination');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpCrossSite'], {mime: 'text/html'}),
+ t,
+ false
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ });
+ }, 'sec-fetch-mode - Not sent to non-trustworthy cross-site destination');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpOrigin'], {mime: 'text/html'}),
+ t,
+ false
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ });
+ }, 'sec-fetch-dest - Not sent to non-trustworthy same-origin destination');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpSameSite'], {mime: 'text/html'}),
+ t,
+ false
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ });
+ }, 'sec-fetch-dest - Not sent to non-trustworthy same-site destination');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpCrossSite'], {mime: 'text/html'}),
+ t,
+ false
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ });
+ }, 'sec-fetch-dest - Not sent to non-trustworthy cross-site destination');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpOrigin'], {mime: 'text/html'}),
+ t,
+ false
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - Not sent to non-trustworthy same-origin destination');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpSameSite'], {mime: 'text/html'}),
+ t,
+ false
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - Not sent to non-trustworthy same-site destination');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpCrossSite'], {mime: 'text/html'}),
+ t,
+ false
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - Not sent to non-trustworthy cross-site destination');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpOrigin'], {mime: 'text/html'}),
+ t,
+ false
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - HTTPS downgrade (header not sent)');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpOrigin', 'httpsOrigin'], {mime: 'text/html'}),
+ t,
+ false
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - HTTPS upgrade');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin'], {mime: 'text/html'}),
+ t,
+ false
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - HTTPS downgrade-upgrade');
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/fetch/metadata/generated/element-img-environment-change.https.sub.html b/test/wpt/tests/fetch/metadata/generated/element-img-environment-change.https.sub.html
new file mode 100644
index 0000000..a19aa11
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/generated/element-img-environment-change.https.sub.html
@@ -0,0 +1,357 @@
+<!DOCTYPE html>
+<!--
+This test was procedurally generated. Please do not modify it directly.
+Sources:
+- fetch/metadata/tools/fetch-metadata.conf.yml
+- fetch/metadata/tools/templates/element-img-environment-change.sub.html
+-->
+<html lang="en">
+ <meta charset="utf-8">
+ <title>HTTP headers on image request triggered by change to environment</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/fetch/metadata/resources/helper.sub.js"></script>
+ <body>
+ <script>
+ 'use strict';
+
+ // The response to the request under test must describe a valid image
+ // resource in order for the `load` event to be fired.
+ const params = {
+ body: `
+ <svg xmlns="http://www.w3.org/2000/svg" width="123" height="123">
+ <rect fill="lime" width="123" height="123"/>
+ </svg>
+ `,
+ mime: 'image/svg+xml'
+ };
+
+ function induceRequest(t, url, attributes) {
+ const iframe = document.createElement('iframe');
+ iframe.style.width = '50px';
+ document.body.appendChild(iframe);
+ t.add_cleanup(() => iframe.remove());
+ iframe.contentDocument.open();
+ iframe.contentDocument.close();
+
+ const image = iframe.contentDocument.createElement('img');
+ for (const [ name, value ] of Object.entries(attributes)) {
+ image.setAttribute(name, value);
+ }
+ iframe.contentDocument.body.appendChild(image);
+
+ image.setAttribute('srcset', `${url} 100w, /media/1x1-green.png 1w`);
+ image.setAttribute('sizes', '(max-width: 100px) 1px, (min-width: 150px) 123px');
+
+ return new Promise((resolve) => {
+ image.onload = image.onerror = resolve;
+ })
+ .then(() => {
+
+ iframe.style.width = '200px';
+
+ return new Promise((resolve) => image.onload = resolve);
+ });
+ }
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ t,
+ makeRequestURL(key, ['httpsOrigin'], params),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-origin']);
+ });
+ }, 'sec-fetch-site - Same origin, no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ t,
+ makeRequestURL(key, ['httpsCrossSite'], params),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-site, no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ t,
+ makeRequestURL(key, ['httpsSameSite'], params),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same site, no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ t,
+ makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite', 'httpsOrigin'], params),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Cross-Site -> Same-Origin redirect, no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ t,
+ makeRequestURL(key, ['httpsOrigin', 'httpsSameSite', 'httpsOrigin'], params),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Same-Site -> Same-Origin redirect, no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ t,
+ makeRequestURL(key, ['httpsCrossSite', 'httpsOrigin'], params),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-Site -> Same Origin, no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ t,
+ makeRequestURL(key, ['httpsCrossSite', 'httpsSameSite'], params),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-Site -> Same-Site, no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ t,
+ makeRequestURL(key, ['httpsCrossSite', 'httpsCrossSite'], params),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-Site -> Cross-Site, no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ t,
+ makeRequestURL(key, ['httpsOrigin', 'httpsOrigin'], params),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-origin']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Same Origin, no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ t,
+ makeRequestURL(key, ['httpsOrigin', 'httpsSameSite'], params),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Same-Site, no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ t,
+ makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite'], params),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Cross-Site, no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ t,
+ makeRequestURL(key, ['httpsSameSite', 'httpsOrigin'], params),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Site -> Same Origin, no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ t,
+ makeRequestURL(key, ['httpsSameSite', 'httpsSameSite'], params),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Site -> Same-Site, no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ t,
+ makeRequestURL(key, ['httpsSameSite', 'httpsCrossSite'], params),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Same-Site -> Cross-Site, no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ t,
+ makeRequestURL(key, [], params),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-mode');
+ assert_array_equals(headers['sec-fetch-mode'], ['no-cors']);
+ });
+ }, 'sec-fetch-mode - no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ t,
+ makeRequestURL(key, [], params),
+ {"crossorigin": ""}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-mode');
+ assert_array_equals(headers['sec-fetch-mode'], ['cors']);
+ });
+ }, 'sec-fetch-mode - attributes: crossorigin');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ t,
+ makeRequestURL(key, [], params),
+ {"crossorigin": "anonymous"}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-mode');
+ assert_array_equals(headers['sec-fetch-mode'], ['cors']);
+ });
+ }, 'sec-fetch-mode - attributes: crossorigin=anonymous');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ t,
+ makeRequestURL(key, [], params),
+ {"crossorigin": "use-credentials"}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-mode');
+ assert_array_equals(headers['sec-fetch-mode'], ['cors']);
+ });
+ }, 'sec-fetch-mode - attributes: crossorigin=use-credentials');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ t,
+ makeRequestURL(key, [], params),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-dest');
+ assert_array_equals(headers['sec-fetch-dest'], ['image']);
+ });
+ }, 'sec-fetch-dest - no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ t,
+ makeRequestURL(key, [], params),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - no attributes');
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/fetch/metadata/generated/element-img-environment-change.sub.html b/test/wpt/tests/fetch/metadata/generated/element-img-environment-change.sub.html
new file mode 100644
index 0000000..9665872
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/generated/element-img-environment-change.sub.html
@@ -0,0 +1,270 @@
+<!DOCTYPE html>
+<!--
+This test was procedurally generated. Please do not modify it directly.
+Sources:
+- fetch/metadata/tools/fetch-metadata.conf.yml
+- fetch/metadata/tools/templates/element-img-environment-change.sub.html
+-->
+<html lang="en">
+ <meta charset="utf-8">
+ <title>HTTP headers on image request triggered by change to environment</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/fetch/metadata/resources/helper.sub.js"></script>
+ <body>
+ <script>
+ 'use strict';
+
+ // The response to the request under test must describe a valid image
+ // resource in order for the `load` event to be fired.
+ const params = {
+ body: `
+ <svg xmlns="http://www.w3.org/2000/svg" width="123" height="123">
+ <rect fill="lime" width="123" height="123"/>
+ </svg>
+ `,
+ mime: 'image/svg+xml'
+ };
+
+ function induceRequest(t, url, attributes) {
+ const iframe = document.createElement('iframe');
+ iframe.style.width = '50px';
+ document.body.appendChild(iframe);
+ t.add_cleanup(() => iframe.remove());
+ iframe.contentDocument.open();
+ iframe.contentDocument.close();
+
+ const image = iframe.contentDocument.createElement('img');
+ for (const [ name, value ] of Object.entries(attributes)) {
+ image.setAttribute(name, value);
+ }
+ iframe.contentDocument.body.appendChild(image);
+
+ image.setAttribute('srcset', `${url} 100w, /media/1x1-green.png 1w`);
+ image.setAttribute('sizes', '(max-width: 100px) 1px, (min-width: 150px) 123px');
+
+ return new Promise((resolve) => {
+ image.onload = image.onerror = resolve;
+ })
+ .then(() => {
+
+ iframe.style.width = '200px';
+
+ return new Promise((resolve) => image.onload = resolve);
+ });
+ }
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ t,
+ makeRequestURL(key, ['httpOrigin'], params),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - Not sent to non-trustworthy same-origin destination, no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ t,
+ makeRequestURL(key, ['httpSameSite'], params),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - Not sent to non-trustworthy same-site destination, no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ t,
+ makeRequestURL(key, ['httpCrossSite'], params),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - Not sent to non-trustworthy cross-site destination, no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ t,
+ makeRequestURL(key, ['httpOrigin'], params),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ });
+ }, 'sec-fetch-mode - Not sent to non-trustworthy same-origin destination, no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ t,
+ makeRequestURL(key, ['httpSameSite'], params),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ });
+ }, 'sec-fetch-mode - Not sent to non-trustworthy same-site destination, no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ t,
+ makeRequestURL(key, ['httpCrossSite'], params),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ });
+ }, 'sec-fetch-mode - Not sent to non-trustworthy cross-site destination, no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ t,
+ makeRequestURL(key, ['httpOrigin'], params),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ });
+ }, 'sec-fetch-dest - Not sent to non-trustworthy same-origin destination, no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ t,
+ makeRequestURL(key, ['httpSameSite'], params),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ });
+ }, 'sec-fetch-dest - Not sent to non-trustworthy same-site destination, no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ t,
+ makeRequestURL(key, ['httpCrossSite'], params),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ });
+ }, 'sec-fetch-dest - Not sent to non-trustworthy cross-site destination, no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ t,
+ makeRequestURL(key, ['httpOrigin'], params),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - Not sent to non-trustworthy same-origin destination, no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ t,
+ makeRequestURL(key, ['httpSameSite'], params),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - Not sent to non-trustworthy same-site destination, no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ t,
+ makeRequestURL(key, ['httpCrossSite'], params),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - Not sent to non-trustworthy cross-site destination, no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ t,
+ makeRequestURL(key, ['httpsOrigin', 'httpOrigin'], params),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - HTTPS downgrade (header not sent), no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ t,
+ makeRequestURL(key, ['httpOrigin', 'httpsOrigin'], params),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - HTTPS upgrade, no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ t,
+ makeRequestURL(key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin'], params),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - HTTPS downgrade-upgrade, no attributes');
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/fetch/metadata/generated/element-img.https.sub.html b/test/wpt/tests/fetch/metadata/generated/element-img.https.sub.html
new file mode 100644
index 0000000..51d6e08
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/generated/element-img.https.sub.html
@@ -0,0 +1,645 @@
+<!DOCTYPE html>
+<!--
+This test was procedurally generated. Please do not modify it directly.
+Sources:
+- fetch/metadata/tools/fetch-metadata.conf.yml
+- fetch/metadata/tools/templates/element-img.sub.html
+-->
+<html lang="en">
+ <meta charset="utf-8">
+ <title>HTTP headers on request for HTML "img" element source</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/fetch/metadata/resources/helper.sub.js"></script>
+ <body>
+ <script>
+ 'use strict';
+
+ function induceRequest(url, sourceAttr, attributes) {
+ const image = document.createElement('img');
+
+ for (const [ name, value ] of Object.entries(attributes)) {
+ image.setAttribute(name, value);
+ }
+
+ return new Promise((resolve) => {
+ image.setAttribute(sourceAttr, url);
+ image.onload = image.onerror = resolve;
+ });
+ }
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin']),
+ 'src',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-origin']);
+ });
+ }, 'sec-fetch-site - src - Same origin, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin']),
+ 'srcset',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-origin']);
+ });
+ }, 'sec-fetch-site - srcset - Same origin, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsCrossSite']),
+ 'src',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - src - Cross-site, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsCrossSite']),
+ 'srcset',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - srcset - Cross-site, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsSameSite']),
+ 'src',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - src - Same site, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsSameSite']),
+ 'srcset',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - srcset - Same site, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite', 'httpsOrigin']),
+ 'src',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - src - Same-Origin -> Cross-Site -> Same-Origin redirect, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite', 'httpsOrigin']),
+ 'srcset',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - srcset - Same-Origin -> Cross-Site -> Same-Origin redirect, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpsSameSite', 'httpsOrigin']),
+ 'src',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - src - Same-Origin -> Same-Site -> Same-Origin redirect, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpsSameSite', 'httpsOrigin']),
+ 'srcset',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - srcset - Same-Origin -> Same-Site -> Same-Origin redirect, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsCrossSite', 'httpsOrigin']),
+ 'src',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - src - Cross-Site -> Same Origin, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsCrossSite', 'httpsOrigin']),
+ 'srcset',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - srcset - Cross-Site -> Same Origin, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsCrossSite', 'httpsSameSite']),
+ 'src',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - src - Cross-Site -> Same-Site, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsCrossSite', 'httpsSameSite']),
+ 'srcset',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - srcset - Cross-Site -> Same-Site, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsCrossSite', 'httpsCrossSite']),
+ 'src',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - src - Cross-Site -> Cross-Site, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsCrossSite', 'httpsCrossSite']),
+ 'srcset',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - srcset - Cross-Site -> Cross-Site, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpsOrigin']),
+ 'src',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-origin']);
+ });
+ }, 'sec-fetch-site - src - Same-Origin -> Same Origin, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpsOrigin']),
+ 'srcset',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-origin']);
+ });
+ }, 'sec-fetch-site - srcset - Same-Origin -> Same Origin, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpsSameSite']),
+ 'src',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - src - Same-Origin -> Same-Site, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpsSameSite']),
+ 'srcset',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - srcset - Same-Origin -> Same-Site, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite']),
+ 'src',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - src - Same-Origin -> Cross-Site, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite']),
+ 'srcset',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - srcset - Same-Origin -> Cross-Site, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsSameSite', 'httpsOrigin']),
+ 'src',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - src - Same-Site -> Same Origin, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsSameSite', 'httpsOrigin']),
+ 'srcset',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - srcset - Same-Site -> Same Origin, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsSameSite', 'httpsSameSite']),
+ 'src',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - src - Same-Site -> Same-Site, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsSameSite', 'httpsSameSite']),
+ 'srcset',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - srcset - Same-Site -> Same-Site, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsSameSite', 'httpsCrossSite']),
+ 'src',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - src - Same-Site -> Cross-Site, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsSameSite', 'httpsCrossSite']),
+ 'srcset',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - srcset - Same-Site -> Cross-Site, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin']),
+ 'src',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - src - HTTPS downgrade-upgrade, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, []),
+ 'src',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-mode');
+ assert_array_equals(headers['sec-fetch-mode'], ['no-cors']);
+ });
+ }, 'sec-fetch-mode - src - no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, []),
+ 'src',
+ {"crossorigin": ""}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-mode');
+ assert_array_equals(headers['sec-fetch-mode'], ['cors']);
+ });
+ }, 'sec-fetch-mode - src - attributes: crossorigin');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, []),
+ 'src',
+ {"crossorigin": "anonymous"}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-mode');
+ assert_array_equals(headers['sec-fetch-mode'], ['cors']);
+ });
+ }, 'sec-fetch-mode - src - attributes: crossorigin=anonymous');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, []),
+ 'src',
+ {"crossorigin": "use-credentials"}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-mode');
+ assert_array_equals(headers['sec-fetch-mode'], ['cors']);
+ });
+ }, 'sec-fetch-mode - src - attributes: crossorigin=use-credentials');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, []),
+ 'srcset',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-mode');
+ assert_array_equals(headers['sec-fetch-mode'], ['no-cors']);
+ });
+ }, 'sec-fetch-mode - srcset - no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, []),
+ 'srcset',
+ {"crossorigin": ""}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-mode');
+ assert_array_equals(headers['sec-fetch-mode'], ['cors']);
+ });
+ }, 'sec-fetch-mode - srcset - attributes: crossorigin');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, []),
+ 'srcset',
+ {"crossorigin": "anonymous"}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-mode');
+ assert_array_equals(headers['sec-fetch-mode'], ['cors']);
+ });
+ }, 'sec-fetch-mode - srcset - attributes: crossorigin=anonymous');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, []),
+ 'srcset',
+ {"crossorigin": "use-credentials"}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-mode');
+ assert_array_equals(headers['sec-fetch-mode'], ['cors']);
+ });
+ }, 'sec-fetch-mode - srcset - attributes: crossorigin=use-credentials');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, []),
+ 'src',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-dest');
+ assert_array_equals(headers['sec-fetch-dest'], ['image']);
+ });
+ }, 'sec-fetch-dest - src - no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, []),
+ 'srcset',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-dest');
+ assert_array_equals(headers['sec-fetch-dest'], ['image']);
+ });
+ }, 'sec-fetch-dest - srcset - no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, []),
+ 'src',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - src - no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, []),
+ 'srcset',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - srcset - no attributes');
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/fetch/metadata/generated/element-img.sub.html b/test/wpt/tests/fetch/metadata/generated/element-img.sub.html
new file mode 100644
index 0000000..5a4b152
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/generated/element-img.sub.html
@@ -0,0 +1,456 @@
+<!DOCTYPE html>
+<!--
+This test was procedurally generated. Please do not modify it directly.
+Sources:
+- fetch/metadata/tools/fetch-metadata.conf.yml
+- fetch/metadata/tools/templates/element-img.sub.html
+-->
+<html lang="en">
+ <meta charset="utf-8">
+ <title>HTTP headers on request for HTML "img" element source</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/fetch/metadata/resources/helper.sub.js"></script>
+ <body>
+ <script>
+ 'use strict';
+
+ function induceRequest(url, sourceAttr, attributes) {
+ const image = document.createElement('img');
+
+ for (const [ name, value ] of Object.entries(attributes)) {
+ image.setAttribute(name, value);
+ }
+
+ return new Promise((resolve) => {
+ image.setAttribute(sourceAttr, url);
+ image.onload = image.onerror = resolve;
+ });
+ }
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpOrigin']),
+ 'src',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - src - Not sent to non-trustworthy same-origin destination, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpOrigin']),
+ 'srcset',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - srcset - Not sent to non-trustworthy same-origin destination, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpSameSite']),
+ 'src',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - src - Not sent to non-trustworthy same-site destination, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpSameSite']),
+ 'srcset',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - srcset - Not sent to non-trustworthy same-site destination, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpCrossSite']),
+ 'src',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - src - Not sent to non-trustworthy cross-site destination, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpCrossSite']),
+ 'srcset',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - srcset - Not sent to non-trustworthy cross-site destination, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpOrigin']),
+ 'src',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ });
+ }, 'sec-fetch-mode - src - Not sent to non-trustworthy same-origin destination, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpOrigin']),
+ 'srcset',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ });
+ }, 'sec-fetch-mode - srcset - Not sent to non-trustworthy same-origin destination, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpSameSite']),
+ 'src',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ });
+ }, 'sec-fetch-mode - src - Not sent to non-trustworthy same-site destination, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpSameSite']),
+ 'srcset',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ });
+ }, 'sec-fetch-mode - srcset - Not sent to non-trustworthy same-site destination, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpCrossSite']),
+ 'src',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ });
+ }, 'sec-fetch-mode - src - Not sent to non-trustworthy cross-site destination, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpCrossSite']),
+ 'srcset',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ });
+ }, 'sec-fetch-mode - srcset - Not sent to non-trustworthy cross-site destination, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpOrigin']),
+ 'src',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ });
+ }, 'sec-fetch-dest - src - Not sent to non-trustworthy same-origin destination, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpOrigin']),
+ 'srcset',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ });
+ }, 'sec-fetch-dest - srcset - Not sent to non-trustworthy same-origin destination, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpSameSite']),
+ 'src',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ });
+ }, 'sec-fetch-dest - src - Not sent to non-trustworthy same-site destination, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpSameSite']),
+ 'srcset',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ });
+ }, 'sec-fetch-dest - srcset - Not sent to non-trustworthy same-site destination, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpCrossSite']),
+ 'src',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ });
+ }, 'sec-fetch-dest - src - Not sent to non-trustworthy cross-site destination, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpCrossSite']),
+ 'srcset',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ });
+ }, 'sec-fetch-dest - srcset - Not sent to non-trustworthy cross-site destination, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpOrigin']),
+ 'src',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - src - Not sent to non-trustworthy same-origin destination, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpOrigin']),
+ 'srcset',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - srcset - Not sent to non-trustworthy same-origin destination, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpSameSite']),
+ 'src',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - src - Not sent to non-trustworthy same-site destination, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpSameSite']),
+ 'srcset',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - srcset - Not sent to non-trustworthy same-site destination, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpCrossSite']),
+ 'src',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - src - Not sent to non-trustworthy cross-site destination, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpCrossSite']),
+ 'srcset',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - srcset - Not sent to non-trustworthy cross-site destination, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpOrigin']),
+ 'src',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - src - HTTPS downgrade (header not sent), no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpOrigin']),
+ 'srcset',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - srcset - HTTPS downgrade (header not sent), no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpOrigin', 'httpsOrigin']),
+ 'src',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - src - HTTPS upgrade, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpOrigin', 'httpsOrigin']),
+ 'srcset',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - srcset - HTTPS upgrade, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin']),
+ 'src',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - src - HTTPS downgrade-upgrade, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin']),
+ 'srcset',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - srcset - HTTPS downgrade-upgrade, no attributes');
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/fetch/metadata/generated/element-input-image.https.sub.html b/test/wpt/tests/fetch/metadata/generated/element-input-image.https.sub.html
new file mode 100644
index 0000000..7fa6740
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/generated/element-input-image.https.sub.html
@@ -0,0 +1,229 @@
+<!DOCTYPE html>
+<!--
+This test was procedurally generated. Please do not modify it directly.
+Sources:
+- fetch/metadata/tools/fetch-metadata.conf.yml
+- fetch/metadata/tools/templates/element-input-image.sub.html
+-->
+<html lang="en">
+ <meta charset="utf-8">
+ <title>HTTP headers on request for HTML "input" element with type="button"</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/fetch/metadata/resources/helper.sub.js"></script>
+ <body>
+ <script>
+ 'use strict';
+
+ function induceRequest(url, test) {
+ const input = document.createElement('input');
+ input.setAttribute('type', 'image');
+
+ document.body.appendChild(input);
+ test.add_cleanup(() => input.remove());
+
+ return new Promise((resolve) => {
+ input.onload = input.onerror = resolve;
+ input.setAttribute('src', url);
+ });
+ }
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(makeRequestURL(key, ['httpsOrigin']), t)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-origin']);
+ });
+ }, 'sec-fetch-site - Same origin, no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(makeRequestURL(key, ['httpsCrossSite']), t)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-site, no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(makeRequestURL(key, ['httpsSameSite']), t)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same site, no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite', 'httpsOrigin']), t)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Cross-Site -> Same-Origin redirect, no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(makeRequestURL(key, ['httpsOrigin', 'httpsSameSite', 'httpsOrigin']), t)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Same-Site -> Same-Origin redirect, no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(makeRequestURL(key, ['httpsCrossSite', 'httpsOrigin']), t)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-Site -> Same Origin, no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(makeRequestURL(key, ['httpsCrossSite', 'httpsSameSite']), t)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-Site -> Same-Site, no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(makeRequestURL(key, ['httpsCrossSite', 'httpsCrossSite']), t)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-Site -> Cross-Site, no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(makeRequestURL(key, ['httpsOrigin', 'httpsOrigin']), t)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-origin']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Same Origin, no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(makeRequestURL(key, ['httpsOrigin', 'httpsSameSite']), t)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Same-Site, no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite']), t)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Cross-Site, no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(makeRequestURL(key, ['httpsSameSite', 'httpsOrigin']), t)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Site -> Same Origin, no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(makeRequestURL(key, ['httpsSameSite', 'httpsSameSite']), t)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Site -> Same-Site, no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(makeRequestURL(key, ['httpsSameSite', 'httpsCrossSite']), t)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Same-Site -> Cross-Site, no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(makeRequestURL(key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin']), t)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - HTTPS downgrade-upgrade, no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(makeRequestURL(key, []), t)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-mode');
+ assert_array_equals(headers['sec-fetch-mode'], ['no-cors']);
+ });
+ }, 'sec-fetch-mode - no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(makeRequestURL(key, []), t)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-dest');
+ assert_array_equals(headers['sec-fetch-dest'], ['image']);
+ });
+ }, 'sec-fetch-dest - no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(makeRequestURL(key, []), t)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - no attributes');
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/fetch/metadata/generated/element-input-image.sub.html b/test/wpt/tests/fetch/metadata/generated/element-input-image.sub.html
new file mode 100644
index 0000000..fb2a146
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/generated/element-input-image.sub.html
@@ -0,0 +1,184 @@
+<!DOCTYPE html>
+<!--
+This test was procedurally generated. Please do not modify it directly.
+Sources:
+- fetch/metadata/tools/fetch-metadata.conf.yml
+- fetch/metadata/tools/templates/element-input-image.sub.html
+-->
+<html lang="en">
+ <meta charset="utf-8">
+ <title>HTTP headers on request for HTML "input" element with type="button"</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/fetch/metadata/resources/helper.sub.js"></script>
+ <body>
+ <script>
+ 'use strict';
+
+ function induceRequest(url, test) {
+ const input = document.createElement('input');
+ input.setAttribute('type', 'image');
+
+ document.body.appendChild(input);
+ test.add_cleanup(() => input.remove());
+
+ return new Promise((resolve) => {
+ input.onload = input.onerror = resolve;
+ input.setAttribute('src', url);
+ });
+ }
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(makeRequestURL(key, ['httpOrigin']), t)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - Not sent to non-trustworthy same-origin destination, no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(makeRequestURL(key, ['httpSameSite']), t)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - Not sent to non-trustworthy same-site destination, no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(makeRequestURL(key, ['httpCrossSite']), t)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - Not sent to non-trustworthy cross-site destination, no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(makeRequestURL(key, ['httpOrigin']), t)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ });
+ }, 'sec-fetch-mode - Not sent to non-trustworthy same-origin destination, no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(makeRequestURL(key, ['httpSameSite']), t)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ });
+ }, 'sec-fetch-mode - Not sent to non-trustworthy same-site destination, no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(makeRequestURL(key, ['httpCrossSite']), t)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ });
+ }, 'sec-fetch-mode - Not sent to non-trustworthy cross-site destination, no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(makeRequestURL(key, ['httpOrigin']), t)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ });
+ }, 'sec-fetch-dest - Not sent to non-trustworthy same-origin destination, no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(makeRequestURL(key, ['httpSameSite']), t)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ });
+ }, 'sec-fetch-dest - Not sent to non-trustworthy same-site destination, no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(makeRequestURL(key, ['httpCrossSite']), t)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ });
+ }, 'sec-fetch-dest - Not sent to non-trustworthy cross-site destination, no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(makeRequestURL(key, ['httpOrigin']), t)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - Not sent to non-trustworthy same-origin destination, no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(makeRequestURL(key, ['httpSameSite']), t)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - Not sent to non-trustworthy same-site destination, no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(makeRequestURL(key, ['httpCrossSite']), t)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - Not sent to non-trustworthy cross-site destination, no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(makeRequestURL(key, ['httpsOrigin', 'httpOrigin']), t)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - HTTPS downgrade (header not sent), no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(makeRequestURL(key, ['httpOrigin', 'httpsOrigin']), t)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - HTTPS upgrade, no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(makeRequestURL(key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin']), t)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - HTTPS downgrade-upgrade, no attributes');
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/fetch/metadata/generated/element-link-icon.https.sub.html b/test/wpt/tests/fetch/metadata/generated/element-link-icon.https.sub.html
new file mode 100644
index 0000000..b244960
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/generated/element-link-icon.https.sub.html
@@ -0,0 +1,371 @@
+<!DOCTYPE html>
+<!--
+This test was procedurally generated. Please do not modify it directly.
+Sources:
+- fetch/metadata/tools/fetch-metadata.conf.yml
+- fetch/metadata/tools/templates/element-link-icon.sub.html
+-->
+<html lang="en">
+ <meta charset="utf-8">
+ <meta name="timeout" content="long">
+ <title>HTTP headers on request for HTML "link" element with rel="icon"</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/fetch/metadata/resources/helper.sub.js"></script>
+ <body>
+ <script>
+ 'use strict';
+
+ const params = {
+ body: `
+ <svg xmlns="http://www.w3.org/2000/svg" width="123" height="123">
+ <rect fill="lime" width="123" height="123"/>
+ </svg>
+ `,
+ mime: 'image/svg+xml'
+ };
+
+ /**
+ * The `link` element supports a `load` event. That event would reliably
+ * indicate that the browser had received the request. Multiple major
+ * browsers do not implement the event, however, so in order to promote the
+ * visibility of this test, a less efficient polling-based detection
+ * mechanism is used.
+ *
+ * https://bugzilla.mozilla.org/show_bug.cgi?id=1638188
+ * https://bugs.chromium.org/p/chromium/issues/detail?id=1083034
+ */
+ function induceRequest(t, url, attributes) {
+ const link = document.createElement('link');
+ link.setAttribute('rel', 'icon');
+ link.setAttribute('href', url);
+
+ for (const [ name, value ] of Object.entries(attributes)) {
+ link.setAttribute(name, value);
+ }
+
+ document.head.appendChild(link);
+ t.add_cleanup(() => link.remove());
+ }
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ t,
+ makeRequestURL(key, ['httpsOrigin'], params),
+ {}
+ );
+
+ return retrieve(key, {poll:true})
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-origin']);
+ });
+ }, 'sec-fetch-site - Same origin no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ t,
+ makeRequestURL(key, ['httpsCrossSite'], params),
+ {}
+ );
+
+ return retrieve(key, {poll:true})
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-site no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ t,
+ makeRequestURL(key, ['httpsSameSite'], params),
+ {}
+ );
+
+ return retrieve(key, {poll:true})
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same site no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ t,
+ makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite', 'httpsOrigin'], params),
+ {}
+ );
+
+ return retrieve(key, {poll:true})
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Cross-Site -> Same-Origin redirect no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ t,
+ makeRequestURL(key, ['httpsOrigin', 'httpsSameSite', 'httpsOrigin'], params),
+ {}
+ );
+
+ return retrieve(key, {poll:true})
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Same-Site -> Same-Origin redirect no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ t,
+ makeRequestURL(key, ['httpsCrossSite', 'httpsOrigin'], params),
+ {}
+ );
+
+ return retrieve(key, {poll:true})
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-Site -> Same Origin no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ t,
+ makeRequestURL(key, ['httpsCrossSite', 'httpsSameSite'], params),
+ {}
+ );
+
+ return retrieve(key, {poll:true})
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-Site -> Same-Site no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ t,
+ makeRequestURL(key, ['httpsCrossSite', 'httpsCrossSite'], params),
+ {}
+ );
+
+ return retrieve(key, {poll:true})
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-Site -> Cross-Site no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ t,
+ makeRequestURL(key, ['httpsOrigin', 'httpsOrigin'], params),
+ {}
+ );
+
+ return retrieve(key, {poll:true})
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-origin']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Same Origin no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ t,
+ makeRequestURL(key, ['httpsOrigin', 'httpsSameSite'], params),
+ {}
+ );
+
+ return retrieve(key, {poll:true})
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Same-Site no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ t,
+ makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite'], params),
+ {}
+ );
+
+ return retrieve(key, {poll:true})
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Cross-Site no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ t,
+ makeRequestURL(key, ['httpsSameSite', 'httpsOrigin'], params),
+ {}
+ );
+
+ return retrieve(key, {poll:true})
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Site -> Same Origin no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ t,
+ makeRequestURL(key, ['httpsSameSite', 'httpsSameSite'], params),
+ {}
+ );
+
+ return retrieve(key, {poll:true})
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Site -> Same-Site no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ t,
+ makeRequestURL(key, ['httpsSameSite', 'httpsCrossSite'], params),
+ {}
+ );
+
+ return retrieve(key, {poll:true})
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Same-Site -> Cross-Site no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ t,
+ makeRequestURL(key, [], params),
+ {}
+ );
+
+ return retrieve(key, {poll:true})
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-mode');
+ assert_array_equals(headers['sec-fetch-mode'], ['no-cors']);
+ });
+ }, 'sec-fetch-mode no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ t,
+ makeRequestURL(key, [], params),
+ {"crossorigin": ""}
+ );
+
+ return retrieve(key, {poll:true})
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-mode');
+ assert_array_equals(headers['sec-fetch-mode'], ['cors']);
+ });
+ }, 'sec-fetch-mode attributes: crossorigin');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ t,
+ makeRequestURL(key, [], params),
+ {"crossorigin": "anonymous"}
+ );
+
+ return retrieve(key, {poll:true})
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-mode');
+ assert_array_equals(headers['sec-fetch-mode'], ['cors']);
+ });
+ }, 'sec-fetch-mode attributes: crossorigin=anonymous');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ t,
+ makeRequestURL(key, [], params),
+ {"crossorigin": "use-credentials"}
+ );
+
+ return retrieve(key, {poll:true})
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-mode');
+ assert_array_equals(headers['sec-fetch-mode'], ['cors']);
+ });
+ }, 'sec-fetch-mode attributes: crossorigin=use-credentials');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ t,
+ makeRequestURL(key, [], params),
+ {}
+ );
+
+ return retrieve(key, {poll:true})
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-dest');
+ assert_array_equals(headers['sec-fetch-dest'], ['empty']);
+ });
+ }, 'sec-fetch-dest no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ t,
+ makeRequestURL(key, [], params),
+ {}
+ );
+
+ return retrieve(key, {poll:true})
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user no attributes');
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/fetch/metadata/generated/element-link-icon.sub.html b/test/wpt/tests/fetch/metadata/generated/element-link-icon.sub.html
new file mode 100644
index 0000000..e9226c1
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/generated/element-link-icon.sub.html
@@ -0,0 +1,279 @@
+<!DOCTYPE html>
+<!--
+This test was procedurally generated. Please do not modify it directly.
+Sources:
+- fetch/metadata/tools/fetch-metadata.conf.yml
+- fetch/metadata/tools/templates/element-link-icon.sub.html
+-->
+<html lang="en">
+ <meta charset="utf-8">
+ <meta name="timeout" content="long">
+ <title>HTTP headers on request for HTML "link" element with rel="icon"</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/fetch/metadata/resources/helper.sub.js"></script>
+ <body>
+ <script>
+ 'use strict';
+
+ const params = {
+ body: `
+ <svg xmlns="http://www.w3.org/2000/svg" width="123" height="123">
+ <rect fill="lime" width="123" height="123"/>
+ </svg>
+ `,
+ mime: 'image/svg+xml'
+ };
+
+ /**
+ * The `link` element supports a `load` event. That event would reliably
+ * indicate that the browser had received the request. Multiple major
+ * browsers do not implement the event, however, so in order to promote the
+ * visibility of this test, a less efficient polling-based detection
+ * mechanism is used.
+ *
+ * https://bugzilla.mozilla.org/show_bug.cgi?id=1638188
+ * https://bugs.chromium.org/p/chromium/issues/detail?id=1083034
+ */
+ function induceRequest(t, url, attributes) {
+ const link = document.createElement('link');
+ link.setAttribute('rel', 'icon');
+ link.setAttribute('href', url);
+
+ for (const [ name, value ] of Object.entries(attributes)) {
+ link.setAttribute(name, value);
+ }
+
+ document.head.appendChild(link);
+ t.add_cleanup(() => link.remove());
+ }
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ t,
+ makeRequestURL(key, ['httpOrigin'], params),
+ {}
+ );
+
+ return retrieve(key, {poll:true})
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - Not sent to non-trustworthy same-origin destination no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ t,
+ makeRequestURL(key, ['httpSameSite'], params),
+ {}
+ );
+
+ return retrieve(key, {poll:true})
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - Not sent to non-trustworthy same-site destination no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ t,
+ makeRequestURL(key, ['httpCrossSite'], params),
+ {}
+ );
+
+ return retrieve(key, {poll:true})
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - Not sent to non-trustworthy cross-site destination no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ t,
+ makeRequestURL(key, ['httpOrigin'], params),
+ {}
+ );
+
+ return retrieve(key, {poll:true})
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ });
+ }, 'sec-fetch-mode - Not sent to non-trustworthy same-origin destination no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ t,
+ makeRequestURL(key, ['httpSameSite'], params),
+ {}
+ );
+
+ return retrieve(key, {poll:true})
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ });
+ }, 'sec-fetch-mode - Not sent to non-trustworthy same-site destination no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ t,
+ makeRequestURL(key, ['httpCrossSite'], params),
+ {}
+ );
+
+ return retrieve(key, {poll:true})
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ });
+ }, 'sec-fetch-mode - Not sent to non-trustworthy cross-site destination no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ t,
+ makeRequestURL(key, ['httpOrigin'], params),
+ {}
+ );
+
+ return retrieve(key, {poll:true})
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ });
+ }, 'sec-fetch-dest - Not sent to non-trustworthy same-origin destination no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ t,
+ makeRequestURL(key, ['httpSameSite'], params),
+ {}
+ );
+
+ return retrieve(key, {poll:true})
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ });
+ }, 'sec-fetch-dest - Not sent to non-trustworthy same-site destination no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ t,
+ makeRequestURL(key, ['httpCrossSite'], params),
+ {}
+ );
+
+ return retrieve(key, {poll:true})
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ });
+ }, 'sec-fetch-dest - Not sent to non-trustworthy cross-site destination no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ t,
+ makeRequestURL(key, ['httpOrigin'], params),
+ {}
+ );
+
+ return retrieve(key, {poll:true})
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - Not sent to non-trustworthy same-origin destination no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ t,
+ makeRequestURL(key, ['httpSameSite'], params),
+ {}
+ );
+
+ return retrieve(key, {poll:true})
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - Not sent to non-trustworthy same-site destination no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ t,
+ makeRequestURL(key, ['httpCrossSite'], params),
+ {}
+ );
+
+ return retrieve(key, {poll:true})
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - Not sent to non-trustworthy cross-site destination no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ t,
+ makeRequestURL(key, ['httpsOrigin', 'httpOrigin'], params),
+ {}
+ );
+
+ return retrieve(key, {poll:true})
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - HTTPS downgrade (header not sent) no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ t,
+ makeRequestURL(key, ['httpOrigin', 'httpsOrigin'], params),
+ {}
+ );
+
+ return retrieve(key, {poll:true})
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - HTTPS upgrade no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ t,
+ makeRequestURL(key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin'], params),
+ {}
+ );
+
+ return retrieve(key, {poll:true})
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - HTTPS downgrade-upgrade no attributes');
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/fetch/metadata/generated/element-link-prefetch.https.optional.sub.html b/test/wpt/tests/fetch/metadata/generated/element-link-prefetch.https.optional.sub.html
new file mode 100644
index 0000000..bdd684a
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/generated/element-link-prefetch.https.optional.sub.html
@@ -0,0 +1,559 @@
+<!DOCTYPE html>
+<!--
+This test was procedurally generated. Please do not modify it directly.
+Sources:
+- fetch/metadata/tools/fetch-metadata.conf.yml
+- fetch/metadata/tools/templates/element-link-prefetch.optional.sub.html
+-->
+<html lang="en">
+ <meta charset="utf-8">
+ <meta name="timeout" content="long">
+ <title>HTTP headers on request for HTML "link" element with rel="prefetch"</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/fetch/metadata/resources/helper.sub.js"></script>
+ <body>
+ <script>
+ 'use strict';
+
+ /**
+ * The `link` element supports a `load` event. That event would reliably
+ * indicate that the browser had received the request. Multiple major
+ * browsers do not implement the event, however, so in order to promote the
+ * visibility of this test, a less efficient polling-based detection
+ * mechanism is used.
+ *
+ * https://bugzilla.mozilla.org/show_bug.cgi?id=1638188
+ * https://bugs.chromium.org/p/chromium/issues/detail?id=1083034
+ */
+ function induceRequest(t, url, attributes) {
+ const link = document.createElement('link');
+ link.setAttribute('rel', 'prefetch');
+ link.setAttribute('href', url);
+
+ for (const [ name, value ] of Object.entries(attributes)) {
+ link.setAttribute(name, value);
+ }
+
+ document.head.appendChild(link);
+ t.add_cleanup(() => link.remove());
+ }
+
+ setup(() => {
+ assert_implements_optional(document.createElement('link').relList.supports('prefetch'));
+ });
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ t,
+ makeRequestURL(key, ['httpsOrigin']),
+ {}
+ );
+
+ return retrieve(key, {poll:true})
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-origin']);
+ });
+ }, 'sec-fetch-site - Same origin no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ t,
+ makeRequestURL(key, ['httpsCrossSite']),
+ {}
+ );
+
+ return retrieve(key, {poll:true})
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-site no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ t,
+ makeRequestURL(key, ['httpsSameSite']),
+ {}
+ );
+
+ return retrieve(key, {poll:true})
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same site no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ t,
+ makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite', 'httpsOrigin']),
+ {}
+ );
+
+ return retrieve(key, {poll:true})
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Cross-Site -> Same-Origin redirect no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ t,
+ makeRequestURL(key, ['httpsOrigin', 'httpsSameSite', 'httpsOrigin']),
+ {}
+ );
+
+ return retrieve(key, {poll:true})
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Same-Site -> Same-Origin redirect no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ t,
+ makeRequestURL(key, ['httpsCrossSite', 'httpsOrigin']),
+ {}
+ );
+
+ return retrieve(key, {poll:true})
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-Site -> Same Origin no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ t,
+ makeRequestURL(key, ['httpsCrossSite', 'httpsSameSite']),
+ {}
+ );
+
+ return retrieve(key, {poll:true})
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-Site -> Same-Site no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ t,
+ makeRequestURL(key, ['httpsCrossSite', 'httpsCrossSite']),
+ {}
+ );
+
+ return retrieve(key, {poll:true})
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-Site -> Cross-Site no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ t,
+ makeRequestURL(key, ['httpsOrigin', 'httpsOrigin']),
+ {}
+ );
+
+ return retrieve(key, {poll:true})
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-origin']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Same Origin no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ t,
+ makeRequestURL(key, ['httpsOrigin', 'httpsSameSite']),
+ {}
+ );
+
+ return retrieve(key, {poll:true})
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Same-Site no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ t,
+ makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite']),
+ {}
+ );
+
+ return retrieve(key, {poll:true})
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Cross-Site no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ t,
+ makeRequestURL(key, ['httpsSameSite', 'httpsOrigin']),
+ {}
+ );
+
+ return retrieve(key, {poll:true})
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Site -> Same Origin no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ t,
+ makeRequestURL(key, ['httpsSameSite', 'httpsSameSite']),
+ {}
+ );
+
+ return retrieve(key, {poll:true})
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Site -> Same-Site no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ t,
+ makeRequestURL(key, ['httpsSameSite', 'httpsCrossSite']),
+ {}
+ );
+
+ return retrieve(key, {poll:true})
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Same-Site -> Cross-Site no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ t,
+ makeRequestURL(key, []),
+ {}
+ );
+
+ return retrieve(key, {poll:true})
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-mode');
+ assert_array_equals(headers['sec-fetch-mode'], ['no-cors']);
+ });
+ }, 'sec-fetch-mode no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ t,
+ makeRequestURL(key, []),
+ {"crossorigin": ""}
+ );
+
+ return retrieve(key, {poll:true})
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-mode');
+ assert_array_equals(headers['sec-fetch-mode'], ['cors']);
+ });
+ }, 'sec-fetch-mode attributes: crossorigin');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ t,
+ makeRequestURL(key, []),
+ {"crossorigin": "anonymous"}
+ );
+
+ return retrieve(key, {poll:true})
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-mode');
+ assert_array_equals(headers['sec-fetch-mode'], ['cors']);
+ });
+ }, 'sec-fetch-mode attributes: crossorigin=anonymous');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ t,
+ makeRequestURL(key, []),
+ {"crossorigin": "use-credentials"}
+ );
+
+ return retrieve(key, {poll:true})
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-mode');
+ assert_array_equals(headers['sec-fetch-mode'], ['cors']);
+ });
+ }, 'sec-fetch-mode attributes: crossorigin=use-credentials');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ t,
+ makeRequestURL(key, []),
+ {}
+ );
+
+ return retrieve(key, {poll:true})
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-dest');
+ assert_array_equals(headers['sec-fetch-dest'], ['empty']);
+ });
+ }, 'sec-fetch-dest no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ t,
+ makeRequestURL(key, []),
+ {"as": "audio"}
+ );
+
+ return retrieve(key, {poll:true})
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-dest');
+ assert_array_equals(headers['sec-fetch-dest'], ['audio']);
+ });
+ }, 'sec-fetch-dest attributes: as=audio');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ t,
+ makeRequestURL(key, []),
+ {"as": "document"}
+ );
+
+ return retrieve(key, {poll:true})
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-dest');
+ assert_array_equals(headers['sec-fetch-dest'], ['document']);
+ });
+ }, 'sec-fetch-dest attributes: as=document');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ t,
+ makeRequestURL(key, []),
+ {"as": "embed"}
+ );
+
+ return retrieve(key, {poll:true})
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-dest');
+ assert_array_equals(headers['sec-fetch-dest'], ['embed']);
+ });
+ }, 'sec-fetch-dest attributes: as=embed');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ t,
+ makeRequestURL(key, []),
+ {"as": "fetch"}
+ );
+
+ return retrieve(key, {poll:true})
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-dest');
+ assert_array_equals(headers['sec-fetch-dest'], ['fetch']);
+ });
+ }, 'sec-fetch-dest attributes: as=fetch');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ t,
+ makeRequestURL(key, []),
+ {"as": "font"}
+ );
+
+ return retrieve(key, {poll:true})
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-dest');
+ assert_array_equals(headers['sec-fetch-dest'], ['font']);
+ });
+ }, 'sec-fetch-dest attributes: as=font');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ t,
+ makeRequestURL(key, []),
+ {"as": "image"}
+ );
+
+ return retrieve(key, {poll:true})
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-dest');
+ assert_array_equals(headers['sec-fetch-dest'], ['image']);
+ });
+ }, 'sec-fetch-dest attributes: as=image');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ t,
+ makeRequestURL(key, []),
+ {"as": "object"}
+ );
+
+ return retrieve(key, {poll:true})
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-dest');
+ assert_array_equals(headers['sec-fetch-dest'], ['object']);
+ });
+ }, 'sec-fetch-dest attributes: as=object');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ t,
+ makeRequestURL(key, []),
+ {"as": "script"}
+ );
+
+ return retrieve(key, {poll:true})
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-dest');
+ assert_array_equals(headers['sec-fetch-dest'], ['script']);
+ });
+ }, 'sec-fetch-dest attributes: as=script');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ t,
+ makeRequestURL(key, []),
+ {"as": "style"}
+ );
+
+ return retrieve(key, {poll:true})
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-dest');
+ assert_array_equals(headers['sec-fetch-dest'], ['style']);
+ });
+ }, 'sec-fetch-dest attributes: as=style');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ t,
+ makeRequestURL(key, []),
+ {"as": "track"}
+ );
+
+ return retrieve(key, {poll:true})
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-dest');
+ assert_array_equals(headers['sec-fetch-dest'], ['track']);
+ });
+ }, 'sec-fetch-dest attributes: as=track');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ t,
+ makeRequestURL(key, []),
+ {"as": "video"}
+ );
+
+ return retrieve(key, {poll:true})
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-dest');
+ assert_array_equals(headers['sec-fetch-dest'], ['video']);
+ });
+ }, 'sec-fetch-dest attributes: as=video');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ t,
+ makeRequestURL(key, []),
+ {"as": "worker"}
+ );
+
+ return retrieve(key, {poll:true})
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-dest');
+ assert_array_equals(headers['sec-fetch-dest'], ['worker']);
+ });
+ }, 'sec-fetch-dest attributes: as=worker');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ t,
+ makeRequestURL(key, []),
+ {}
+ );
+
+ return retrieve(key, {poll:true})
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user no attributes');
+ </script>
+ </body>
+</html>
+
diff --git a/test/wpt/tests/fetch/metadata/generated/element-link-prefetch.optional.sub.html b/test/wpt/tests/fetch/metadata/generated/element-link-prefetch.optional.sub.html
new file mode 100644
index 0000000..c224488
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/generated/element-link-prefetch.optional.sub.html
@@ -0,0 +1,275 @@
+<!DOCTYPE html>
+<!--
+This test was procedurally generated. Please do not modify it directly.
+Sources:
+- fetch/metadata/tools/fetch-metadata.conf.yml
+- fetch/metadata/tools/templates/element-link-prefetch.optional.sub.html
+-->
+<html lang="en">
+ <meta charset="utf-8">
+ <meta name="timeout" content="long">
+ <title>HTTP headers on request for HTML "link" element with rel="prefetch"</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/fetch/metadata/resources/helper.sub.js"></script>
+ <body>
+ <script>
+ 'use strict';
+
+ /**
+ * The `link` element supports a `load` event. That event would reliably
+ * indicate that the browser had received the request. Multiple major
+ * browsers do not implement the event, however, so in order to promote the
+ * visibility of this test, a less efficient polling-based detection
+ * mechanism is used.
+ *
+ * https://bugzilla.mozilla.org/show_bug.cgi?id=1638188
+ * https://bugs.chromium.org/p/chromium/issues/detail?id=1083034
+ */
+ function induceRequest(t, url, attributes) {
+ const link = document.createElement('link');
+ link.setAttribute('rel', 'prefetch');
+ link.setAttribute('href', url);
+
+ for (const [ name, value ] of Object.entries(attributes)) {
+ link.setAttribute(name, value);
+ }
+
+ document.head.appendChild(link);
+ t.add_cleanup(() => link.remove());
+ }
+
+ setup(() => {
+ assert_implements_optional(document.createElement('link').relList.supports('prefetch'));
+ });
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ t,
+ makeRequestURL(key, ['httpOrigin']),
+ {}
+ );
+
+ return retrieve(key, {poll:true})
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - Not sent to non-trustworthy same-origin destination no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ t,
+ makeRequestURL(key, ['httpSameSite']),
+ {}
+ );
+
+ return retrieve(key, {poll:true})
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - Not sent to non-trustworthy same-site destination no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ t,
+ makeRequestURL(key, ['httpCrossSite']),
+ {}
+ );
+
+ return retrieve(key, {poll:true})
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - Not sent to non-trustworthy cross-site destination no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ t,
+ makeRequestURL(key, ['httpOrigin']),
+ {}
+ );
+
+ return retrieve(key, {poll:true})
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ });
+ }, 'sec-fetch-mode - Not sent to non-trustworthy same-origin destination no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ t,
+ makeRequestURL(key, ['httpSameSite']),
+ {}
+ );
+
+ return retrieve(key, {poll:true})
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ });
+ }, 'sec-fetch-mode - Not sent to non-trustworthy same-site destination no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ t,
+ makeRequestURL(key, ['httpCrossSite']),
+ {}
+ );
+
+ return retrieve(key, {poll:true})
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ });
+ }, 'sec-fetch-mode - Not sent to non-trustworthy cross-site destination no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ t,
+ makeRequestURL(key, ['httpOrigin']),
+ {}
+ );
+
+ return retrieve(key, {poll:true})
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ });
+ }, 'sec-fetch-dest - Not sent to non-trustworthy same-origin destination no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ t,
+ makeRequestURL(key, ['httpSameSite']),
+ {}
+ );
+
+ return retrieve(key, {poll:true})
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ });
+ }, 'sec-fetch-dest - Not sent to non-trustworthy same-site destination no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ t,
+ makeRequestURL(key, ['httpCrossSite']),
+ {}
+ );
+
+ return retrieve(key, {poll:true})
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ });
+ }, 'sec-fetch-dest - Not sent to non-trustworthy cross-site destination no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ t,
+ makeRequestURL(key, ['httpOrigin']),
+ {}
+ );
+
+ return retrieve(key, {poll:true})
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - Not sent to non-trustworthy same-origin destination no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ t,
+ makeRequestURL(key, ['httpSameSite']),
+ {}
+ );
+
+ return retrieve(key, {poll:true})
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - Not sent to non-trustworthy same-site destination no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ t,
+ makeRequestURL(key, ['httpCrossSite']),
+ {}
+ );
+
+ return retrieve(key, {poll:true})
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - Not sent to non-trustworthy cross-site destination no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ t,
+ makeRequestURL(key, ['httpsOrigin', 'httpOrigin']),
+ {}
+ );
+
+ return retrieve(key, {poll:true})
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - HTTPS downgrade (header not sent) no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ t,
+ makeRequestURL(key, ['httpOrigin', 'httpsOrigin']),
+ {}
+ );
+
+ return retrieve(key, {poll:true})
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - HTTPS upgrade no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ t,
+ makeRequestURL(key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin']),
+ {}
+ );
+
+ return retrieve(key, {poll:true})
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - HTTPS downgrade-upgrade no attributes');
+ </script>
+ </body>
+</html>
+
diff --git a/test/wpt/tests/fetch/metadata/generated/element-meta-refresh.https.optional.sub.html b/test/wpt/tests/fetch/metadata/generated/element-meta-refresh.https.optional.sub.html
new file mode 100644
index 0000000..3a1a8eb
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/generated/element-meta-refresh.https.optional.sub.html
@@ -0,0 +1,276 @@
+<!DOCTYPE html>
+<!--
+This test was procedurally generated. Please do not modify it directly.
+Sources:
+- fetch/metadata/tools/fetch-metadata.conf.yml
+- fetch/metadata/tools/templates/element-meta-refresh.optional.sub.html
+-->
+<html lang="en">
+ <meta charset="utf-8">
+ <title>HTTP headers on request for HTML "meta" element with http-equiv="refresh"</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/fetch/metadata/resources/helper.sub.js"></script>
+ <body>
+ <script>
+ 'use strict';
+
+ function induceRequest(url, test) {
+ const win = window.open();
+ test.add_cleanup(() => win.close());
+
+ win.document.open();
+ win.document.write(
+ `<meta http-equiv="Refresh" content="0; URL=${url}">`
+ );
+ win.document.close();
+
+ return new Promise((resolve) => {
+ addEventListener('message', (event) => {
+ if (event.source === win) {
+ resolve();
+ }
+ });
+ });
+ }
+
+ const responseParams = {
+ mime: 'text/html',
+ body: `<script>opener.postMessage(0, '*')</${''}script>`
+ };
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin'], responseParams), t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-origin']);
+ });
+ }, 'sec-fetch-site - Same origin');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsCrossSite'], responseParams), t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-site');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsSameSite'], responseParams), t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same site');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite', 'httpsOrigin'], responseParams), t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Cross-Site -> Same-Origin redirect');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpsSameSite', 'httpsOrigin'], responseParams), t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Same-Site -> Same-Origin redirect');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsCrossSite', 'httpsOrigin'], responseParams), t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-Site -> Same Origin');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsCrossSite', 'httpsSameSite'], responseParams), t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-Site -> Same-Site');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsCrossSite', 'httpsCrossSite'], responseParams), t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-Site -> Cross-Site');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpsOrigin'], responseParams), t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-origin']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Same Origin');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpsSameSite'], responseParams), t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Same-Site');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite'], responseParams), t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Cross-Site');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsSameSite', 'httpsOrigin'], responseParams), t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Site -> Same Origin');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsSameSite', 'httpsSameSite'], responseParams), t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Site -> Same-Site');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsSameSite', 'httpsCrossSite'], responseParams), t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Same-Site -> Cross-Site');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin'], responseParams), t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - HTTPS downgrade-upgrade');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, [], responseParams), t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-mode');
+ assert_array_equals(headers['sec-fetch-mode'], ['navigate']);
+ });
+ }, 'sec-fetch-mode');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, [], responseParams), t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-dest');
+ assert_array_equals(headers['sec-fetch-dest'], ['document']);
+ });
+ }, 'sec-fetch-dest');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, [], responseParams), t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user');
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/fetch/metadata/generated/element-meta-refresh.optional.sub.html b/test/wpt/tests/fetch/metadata/generated/element-meta-refresh.optional.sub.html
new file mode 100644
index 0000000..df3e92e
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/generated/element-meta-refresh.optional.sub.html
@@ -0,0 +1,225 @@
+<!DOCTYPE html>
+<!--
+This test was procedurally generated. Please do not modify it directly.
+Sources:
+- fetch/metadata/tools/fetch-metadata.conf.yml
+- fetch/metadata/tools/templates/element-meta-refresh.optional.sub.html
+-->
+<html lang="en">
+ <meta charset="utf-8">
+ <title>HTTP headers on request for HTML "meta" element with http-equiv="refresh"</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/fetch/metadata/resources/helper.sub.js"></script>
+ <body>
+ <script>
+ 'use strict';
+
+ function induceRequest(url, test) {
+ const win = window.open();
+ test.add_cleanup(() => win.close());
+
+ win.document.open();
+ win.document.write(
+ `<meta http-equiv="Refresh" content="0; URL=${url}">`
+ );
+ win.document.close();
+
+ return new Promise((resolve) => {
+ addEventListener('message', (event) => {
+ if (event.source === win) {
+ resolve();
+ }
+ });
+ });
+ }
+
+ const responseParams = {
+ mime: 'text/html',
+ body: `<script>opener.postMessage(0, '*')</${''}script>`
+ };
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpOrigin'], responseParams), t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - Not sent to non-trustworthy same-origin destination');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpSameSite'], responseParams), t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - Not sent to non-trustworthy same-site destination');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpCrossSite'], responseParams), t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - Not sent to non-trustworthy cross-site destination');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpOrigin'], responseParams), t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ });
+ }, 'sec-fetch-mode - Not sent to non-trustworthy same-origin destination');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpSameSite'], responseParams), t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ });
+ }, 'sec-fetch-mode - Not sent to non-trustworthy same-site destination');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpCrossSite'], responseParams), t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ });
+ }, 'sec-fetch-mode - Not sent to non-trustworthy cross-site destination');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpOrigin'], responseParams), t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ });
+ }, 'sec-fetch-dest - Not sent to non-trustworthy same-origin destination');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpSameSite'], responseParams), t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ });
+ }, 'sec-fetch-dest - Not sent to non-trustworthy same-site destination');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpCrossSite'], responseParams), t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ });
+ }, 'sec-fetch-dest - Not sent to non-trustworthy cross-site destination');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpOrigin'], responseParams), t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - Not sent to non-trustworthy same-origin destination');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpSameSite'], responseParams), t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - Not sent to non-trustworthy same-site destination');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpCrossSite'], responseParams), t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - Not sent to non-trustworthy cross-site destination');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpOrigin'], responseParams), t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - HTTPS downgrade (header not sent)');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpOrigin', 'httpsOrigin'], responseParams), t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - HTTPS upgrade');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin'], responseParams), t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - HTTPS downgrade-upgrade');
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/fetch/metadata/generated/element-picture.https.sub.html b/test/wpt/tests/fetch/metadata/generated/element-picture.https.sub.html
new file mode 100644
index 0000000..ba6636a
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/generated/element-picture.https.sub.html
@@ -0,0 +1,997 @@
+<!DOCTYPE html>
+<!--
+This test was procedurally generated. Please do not modify it directly.
+Sources:
+- fetch/metadata/tools/fetch-metadata.conf.yml
+- fetch/metadata/tools/templates/element-picture.sub.html
+-->
+<html lang="en">
+ <meta charset="utf-8">
+ <title>HTTP headers on request for HTML "picture" element source</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/fetch/metadata/resources/helper.sub.js"></script>
+ <body>
+ <script>
+ 'use strict';
+
+ function induceRequest(url, sourceEl, sourceAttr, attributes) {
+ const picture = document.createElement('picture');
+ const els = {
+ img: document.createElement('img'),
+ source: document.createElement('source')
+ };
+ picture.appendChild(els.source);
+ picture.appendChild(els.img);
+ document.body.appendChild(picture);
+
+ for (const [ name, value ] of Object.entries(attributes)) {
+ els.img.setAttribute(name, value);
+ }
+
+ return new Promise((resolve) => {
+ els[sourceEl].setAttribute(sourceAttr, url);
+ els.img.onload = els.img.onerror = resolve;
+ });
+ }
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin']),
+ 'img',
+ 'src',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-origin']);
+ });
+ }, 'sec-fetch-site - img[src] - Same origin, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin']),
+ 'img',
+ 'srcset',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-origin']);
+ });
+ }, 'sec-fetch-site - img[srcset] - Same origin, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin']),
+ 'source',
+ 'srcset',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-origin']);
+ });
+ }, 'sec-fetch-site - source[srcset] - Same origin, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsCrossSite']),
+ 'img',
+ 'src',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - img[src] - Cross-site, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsCrossSite']),
+ 'img',
+ 'srcset',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - img[srcset] - Cross-site, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsCrossSite']),
+ 'source',
+ 'srcset',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - source[srcset] - Cross-site, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsSameSite']),
+ 'img',
+ 'src',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - img[src] - Same site, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsSameSite']),
+ 'img',
+ 'srcset',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - img[srcset] - Same site, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsSameSite']),
+ 'source',
+ 'srcset',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - source[srcset] - Same site, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite', 'httpsOrigin']),
+ 'img',
+ 'src',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - img[src] - Same-Origin -> Cross-Site -> Same-Origin redirect, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite', 'httpsOrigin']),
+ 'img',
+ 'srcset',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - img[srcset] - Same-Origin -> Cross-Site -> Same-Origin redirect, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite', 'httpsOrigin']),
+ 'source',
+ 'srcset',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - source[srcset] - Same-Origin -> Cross-Site -> Same-Origin redirect, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpsSameSite', 'httpsOrigin']),
+ 'img',
+ 'src',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - img[src] - Same-Origin -> Same-Site -> Same-Origin redirect, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpsSameSite', 'httpsOrigin']),
+ 'img',
+ 'srcset',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - img[srcset] - Same-Origin -> Same-Site -> Same-Origin redirect, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpsSameSite', 'httpsOrigin']),
+ 'source',
+ 'srcset',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - source[srcset] - Same-Origin -> Same-Site -> Same-Origin redirect, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsCrossSite', 'httpsOrigin']),
+ 'img',
+ 'src',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - img[src] - Cross-Site -> Same Origin, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsCrossSite', 'httpsOrigin']),
+ 'img',
+ 'srcset',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - img[srcset] - Cross-Site -> Same Origin, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsCrossSite', 'httpsOrigin']),
+ 'source',
+ 'srcset',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - source[srcset] - Cross-Site -> Same Origin, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsCrossSite', 'httpsSameSite']),
+ 'img',
+ 'src',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - img[src] - Cross-Site -> Same-Site, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsCrossSite', 'httpsSameSite']),
+ 'img',
+ 'srcset',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - img[srcset] - Cross-Site -> Same-Site, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsCrossSite', 'httpsSameSite']),
+ 'source',
+ 'srcset',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - source[srcset] - Cross-Site -> Same-Site, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsCrossSite', 'httpsCrossSite']),
+ 'img',
+ 'src',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - img[src] - Cross-Site -> Cross-Site, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsCrossSite', 'httpsCrossSite']),
+ 'img',
+ 'srcset',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - img[srcset] - Cross-Site -> Cross-Site, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsCrossSite', 'httpsCrossSite']),
+ 'source',
+ 'srcset',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - source[srcset] - Cross-Site -> Cross-Site, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpsOrigin']),
+ 'img',
+ 'src',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-origin']);
+ });
+ }, 'sec-fetch-site - img[src] - Same-Origin -> Same Origin, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpsOrigin']),
+ 'img',
+ 'srcset',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-origin']);
+ });
+ }, 'sec-fetch-site - img[srcset] - Same-Origin -> Same Origin, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpsOrigin']),
+ 'source',
+ 'srcset',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-origin']);
+ });
+ }, 'sec-fetch-site - source[srcset] - Same-Origin -> Same Origin, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpsSameSite']),
+ 'img',
+ 'src',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - img[src] - Same-Origin -> Same-Site, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpsSameSite']),
+ 'img',
+ 'srcset',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - img[srcset] - Same-Origin -> Same-Site, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpsSameSite']),
+ 'source',
+ 'srcset',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - source[srcset] - Same-Origin -> Same-Site, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite']),
+ 'img',
+ 'src',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - img[src] - Same-Origin -> Cross-Site, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite']),
+ 'img',
+ 'srcset',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - img[srcset] - Same-Origin -> Cross-Site, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite']),
+ 'source',
+ 'srcset',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - source[srcset] - Same-Origin -> Cross-Site, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsSameSite', 'httpsOrigin']),
+ 'img',
+ 'src',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - img[src] - Same-Site -> Same Origin, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsSameSite', 'httpsOrigin']),
+ 'img',
+ 'srcset',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - img[srcset] - Same-Site -> Same Origin, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsSameSite', 'httpsOrigin']),
+ 'source',
+ 'srcset',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - source[srcset] - Same-Site -> Same Origin, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsSameSite', 'httpsSameSite']),
+ 'img',
+ 'src',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - img[src] - Same-Site -> Same-Site, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsSameSite', 'httpsSameSite']),
+ 'img',
+ 'srcset',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - img[srcset] - Same-Site -> Same-Site, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsSameSite', 'httpsSameSite']),
+ 'source',
+ 'srcset',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - source[srcset] - Same-Site -> Same-Site, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsSameSite', 'httpsCrossSite']),
+ 'img',
+ 'src',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - img[src] - Same-Site -> Cross-Site, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsSameSite', 'httpsCrossSite']),
+ 'img',
+ 'srcset',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - img[srcset] - Same-Site -> Cross-Site, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsSameSite', 'httpsCrossSite']),
+ 'source',
+ 'srcset',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - source[srcset] - Same-Site -> Cross-Site, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, []),
+ 'img',
+ 'src',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-mode');
+ assert_array_equals(headers['sec-fetch-mode'], ['no-cors']);
+ });
+ }, 'sec-fetch-mode - img[src] - no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, []),
+ 'img',
+ 'srcset',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-mode');
+ assert_array_equals(headers['sec-fetch-mode'], ['no-cors']);
+ });
+ }, 'sec-fetch-mode - img[srcset] - no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, []),
+ 'source',
+ 'srcset',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-mode');
+ assert_array_equals(headers['sec-fetch-mode'], ['no-cors']);
+ });
+ }, 'sec-fetch-mode - source[srcset] - no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, []),
+ 'img',
+ 'src',
+ {"crossorigin": ""}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-mode');
+ assert_array_equals(headers['sec-fetch-mode'], ['cors']);
+ });
+ }, 'sec-fetch-mode - img[src] - attributes: crossorigin');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, []),
+ 'img',
+ 'srcset',
+ {"crossorigin": ""}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-mode');
+ assert_array_equals(headers['sec-fetch-mode'], ['cors']);
+ });
+ }, 'sec-fetch-mode - img[srcset] - attributes: crossorigin');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, []),
+ 'source',
+ 'srcset',
+ {"crossorigin": ""}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-mode');
+ assert_array_equals(headers['sec-fetch-mode'], ['cors']);
+ });
+ }, 'sec-fetch-mode - source[srcset] - attributes: crossorigin');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, []),
+ 'img',
+ 'src',
+ {"crossorigin": "anonymous"}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-mode');
+ assert_array_equals(headers['sec-fetch-mode'], ['cors']);
+ });
+ }, 'sec-fetch-mode - img[src] - attributes: crossorigin=anonymous');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, []),
+ 'img',
+ 'srcset',
+ {"crossorigin": "anonymous"}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-mode');
+ assert_array_equals(headers['sec-fetch-mode'], ['cors']);
+ });
+ }, 'sec-fetch-mode - img[srcset] - attributes: crossorigin=anonymous');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, []),
+ 'source',
+ 'srcset',
+ {"crossorigin": "anonymous"}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-mode');
+ assert_array_equals(headers['sec-fetch-mode'], ['cors']);
+ });
+ }, 'sec-fetch-mode - source[srcset] - attributes: crossorigin=anonymous');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, []),
+ 'img',
+ 'src',
+ {"crossorigin": "use-credentials"}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-mode');
+ assert_array_equals(headers['sec-fetch-mode'], ['cors']);
+ });
+ }, 'sec-fetch-mode - img[src] - attributes: crossorigin=use-credentials');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, []),
+ 'img',
+ 'srcset',
+ {"crossorigin": "use-credentials"}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-mode');
+ assert_array_equals(headers['sec-fetch-mode'], ['cors']);
+ });
+ }, 'sec-fetch-mode - img[srcset] - attributes: crossorigin=use-credentials');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, []),
+ 'source',
+ 'srcset',
+ {"crossorigin": "use-credentials"}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-mode');
+ assert_array_equals(headers['sec-fetch-mode'], ['cors']);
+ });
+ }, 'sec-fetch-mode - source[srcset] - attributes: crossorigin=use-credentials');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, []),
+ 'img',
+ 'src',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-dest');
+ assert_array_equals(headers['sec-fetch-dest'], ['image']);
+ });
+ }, 'sec-fetch-dest - img[src] - no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, []),
+ 'img',
+ 'srcset',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-dest');
+ assert_array_equals(headers['sec-fetch-dest'], ['image']);
+ });
+ }, 'sec-fetch-dest - img[srcset] - no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, []),
+ 'source',
+ 'srcset',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-dest');
+ assert_array_equals(headers['sec-fetch-dest'], ['image']);
+ });
+ }, 'sec-fetch-dest - source[srcset] - no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, []),
+ 'img',
+ 'src',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - img[src] - no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, []),
+ 'img',
+ 'srcset',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - img[srcset] - no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, []),
+ 'source',
+ 'srcset',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - source[srcset] - no attributes');
+ </script>
+ </body>
+</html>
+
diff --git a/test/wpt/tests/fetch/metadata/generated/element-picture.sub.html b/test/wpt/tests/fetch/metadata/generated/element-picture.sub.html
new file mode 100644
index 0000000..64f851c
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/generated/element-picture.sub.html
@@ -0,0 +1,721 @@
+<!DOCTYPE html>
+<!--
+This test was procedurally generated. Please do not modify it directly.
+Sources:
+- fetch/metadata/tools/fetch-metadata.conf.yml
+- fetch/metadata/tools/templates/element-picture.sub.html
+-->
+<html lang="en">
+ <meta charset="utf-8">
+ <title>HTTP headers on request for HTML "picture" element source</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/fetch/metadata/resources/helper.sub.js"></script>
+ <body>
+ <script>
+ 'use strict';
+
+ function induceRequest(url, sourceEl, sourceAttr, attributes) {
+ const picture = document.createElement('picture');
+ const els = {
+ img: document.createElement('img'),
+ source: document.createElement('source')
+ };
+ picture.appendChild(els.source);
+ picture.appendChild(els.img);
+ document.body.appendChild(picture);
+
+ for (const [ name, value ] of Object.entries(attributes)) {
+ els.img.setAttribute(name, value);
+ }
+
+ return new Promise((resolve) => {
+ els[sourceEl].setAttribute(sourceAttr, url);
+ els.img.onload = els.img.onerror = resolve;
+ });
+ }
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpOrigin']),
+ 'img',
+ 'src',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - img[src] - Not sent to non-trustworthy same-origin destination, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpOrigin']),
+ 'img',
+ 'srcset',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - img[srcset] - Not sent to non-trustworthy same-origin destination, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpOrigin']),
+ 'source',
+ 'srcset',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - source[srcset] - Not sent to non-trustworthy same-origin destination, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpSameSite']),
+ 'img',
+ 'src',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - img[src] - Not sent to non-trustworthy same-site destination, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpSameSite']),
+ 'img',
+ 'srcset',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - img[srcset] - Not sent to non-trustworthy same-site destination, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpSameSite']),
+ 'source',
+ 'srcset',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - source[srcset] - Not sent to non-trustworthy same-site destination, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpCrossSite']),
+ 'img',
+ 'src',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - img[src] - Not sent to non-trustworthy cross-site destination, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpCrossSite']),
+ 'img',
+ 'srcset',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - img[srcset] - Not sent to non-trustworthy cross-site destination, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpCrossSite']),
+ 'source',
+ 'srcset',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - source[srcset] - Not sent to non-trustworthy cross-site destination, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpOrigin']),
+ 'img',
+ 'src',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ });
+ }, 'sec-fetch-mode - img[src] - Not sent to non-trustworthy same-origin destination, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpOrigin']),
+ 'img',
+ 'srcset',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ });
+ }, 'sec-fetch-mode - img[srcset] - Not sent to non-trustworthy same-origin destination, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpOrigin']),
+ 'source',
+ 'srcset',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ });
+ }, 'sec-fetch-mode - source[srcset] - Not sent to non-trustworthy same-origin destination, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpSameSite']),
+ 'img',
+ 'src',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ });
+ }, 'sec-fetch-mode - img[src] - Not sent to non-trustworthy same-site destination, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpSameSite']),
+ 'img',
+ 'srcset',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ });
+ }, 'sec-fetch-mode - img[srcset] - Not sent to non-trustworthy same-site destination, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpSameSite']),
+ 'source',
+ 'srcset',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ });
+ }, 'sec-fetch-mode - source[srcset] - Not sent to non-trustworthy same-site destination, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpCrossSite']),
+ 'img',
+ 'src',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ });
+ }, 'sec-fetch-mode - img[src] - Not sent to non-trustworthy cross-site destination, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpCrossSite']),
+ 'img',
+ 'srcset',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ });
+ }, 'sec-fetch-mode - img[srcset] - Not sent to non-trustworthy cross-site destination, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpCrossSite']),
+ 'source',
+ 'srcset',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ });
+ }, 'sec-fetch-mode - source[srcset] - Not sent to non-trustworthy cross-site destination, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpOrigin']),
+ 'img',
+ 'src',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ });
+ }, 'sec-fetch-dest - img[src] - Not sent to non-trustworthy same-origin destination, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpOrigin']),
+ 'img',
+ 'srcset',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ });
+ }, 'sec-fetch-dest - img[srcset] - Not sent to non-trustworthy same-origin destination, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpOrigin']),
+ 'source',
+ 'srcset',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ });
+ }, 'sec-fetch-dest - source[srcset] - Not sent to non-trustworthy same-origin destination, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpSameSite']),
+ 'img',
+ 'src',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ });
+ }, 'sec-fetch-dest - img[src] - Not sent to non-trustworthy same-site destination, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpSameSite']),
+ 'img',
+ 'srcset',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ });
+ }, 'sec-fetch-dest - img[srcset] - Not sent to non-trustworthy same-site destination, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpSameSite']),
+ 'source',
+ 'srcset',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ });
+ }, 'sec-fetch-dest - source[srcset] - Not sent to non-trustworthy same-site destination, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpCrossSite']),
+ 'img',
+ 'src',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ });
+ }, 'sec-fetch-dest - img[src] - Not sent to non-trustworthy cross-site destination, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpCrossSite']),
+ 'img',
+ 'srcset',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ });
+ }, 'sec-fetch-dest - img[srcset] - Not sent to non-trustworthy cross-site destination, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpCrossSite']),
+ 'source',
+ 'srcset',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ });
+ }, 'sec-fetch-dest - source[srcset] - Not sent to non-trustworthy cross-site destination, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpOrigin']),
+ 'img',
+ 'src',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - img[src] - Not sent to non-trustworthy same-origin destination, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpOrigin']),
+ 'img',
+ 'srcset',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - img[srcset] - Not sent to non-trustworthy same-origin destination, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpOrigin']),
+ 'source',
+ 'srcset',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - source[srcset] - Not sent to non-trustworthy same-origin destination, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpSameSite']),
+ 'img',
+ 'src',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - img[src] - Not sent to non-trustworthy same-site destination, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpSameSite']),
+ 'img',
+ 'srcset',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - img[srcset] - Not sent to non-trustworthy same-site destination, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpSameSite']),
+ 'source',
+ 'srcset',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - source[srcset] - Not sent to non-trustworthy same-site destination, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpCrossSite']),
+ 'img',
+ 'src',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - img[src] - Not sent to non-trustworthy cross-site destination, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpCrossSite']),
+ 'img',
+ 'srcset',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - img[srcset] - Not sent to non-trustworthy cross-site destination, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpCrossSite']),
+ 'source',
+ 'srcset',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - source[srcset] - Not sent to non-trustworthy cross-site destination, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpOrigin']),
+ 'img',
+ 'src',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - img[src] - HTTPS downgrade (header not sent), no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpOrigin']),
+ 'img',
+ 'srcset',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - img[srcset] - HTTPS downgrade (header not sent), no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpOrigin']),
+ 'source',
+ 'srcset',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - source[srcset] - HTTPS downgrade (header not sent), no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpOrigin', 'httpsOrigin']),
+ 'img',
+ 'src',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - img[src] - HTTPS upgrade, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpOrigin', 'httpsOrigin']),
+ 'img',
+ 'srcset',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - img[srcset] - HTTPS upgrade, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpOrigin', 'httpsOrigin']),
+ 'source',
+ 'srcset',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - source[srcset] - HTTPS upgrade, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin']),
+ 'img',
+ 'src',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - img[src] - HTTPS downgrade-upgrade, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin']),
+ 'img',
+ 'srcset',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - img[srcset] - HTTPS downgrade-upgrade, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin']),
+ 'source',
+ 'srcset',
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - source[srcset] - HTTPS downgrade-upgrade, no attributes');
+ </script>
+ </body>
+</html>
+
diff --git a/test/wpt/tests/fetch/metadata/generated/element-script.https.sub.html b/test/wpt/tests/fetch/metadata/generated/element-script.https.sub.html
new file mode 100644
index 0000000..dcdcba2
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/generated/element-script.https.sub.html
@@ -0,0 +1,593 @@
+<!DOCTYPE html>
+<!--
+This test was procedurally generated. Please do not modify it directly.
+Sources:
+- fetch/metadata/tools/fetch-metadata.conf.yml
+- fetch/metadata/tools/templates/element-script.sub.html
+-->
+<html lang="en">
+ <meta charset="utf-8">
+ <title>HTTP headers on request for HTML "script" element source</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/fetch/metadata/resources/helper.sub.js"></script>
+ <body>
+ <script>
+ 'use strict';
+
+ function induceRequest(url, attributes) {
+ const script = document.createElement('script');
+ script.setAttribute('src', url);
+
+ for (const [ name, value ] of Object.entries(attributes)) {
+ script.setAttribute(name, value);
+ }
+
+ return new Promise((resolve, reject) => {
+ script.onload = resolve;
+ script.onerror = () => reject('Failed to load script');
+ document.body.appendChild(script);
+ })
+ .then(() => script.remove());
+ }
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpsOrigin'], { mime: 'application/javascript' }
+ );
+
+ return induceRequest(url,
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-origin']);
+ });
+ }, 'sec-fetch-site - Same origin, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpsOrigin'], { mime: 'application/javascript' }
+ );
+
+ return induceRequest(url,
+ {"type": "module"}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-origin']);
+ });
+ }, 'sec-fetch-site - Same origin, attributes: type=module');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpsCrossSite'], { mime: 'application/javascript' }
+ );
+
+ return induceRequest(url,
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-site, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpsCrossSite'], { mime: 'application/javascript' }
+ );
+
+ return induceRequest(url,
+ {"type": "module"}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-site, attributes: type=module');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpsSameSite'], { mime: 'application/javascript' }
+ );
+
+ return induceRequest(url,
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same site, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpsSameSite'], { mime: 'application/javascript' }
+ );
+
+ return induceRequest(url,
+ {"type": "module"}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same site, attributes: type=module');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpsOrigin', 'httpsCrossSite', 'httpsOrigin'], { mime: 'application/javascript' }
+ );
+
+ return induceRequest(url,
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Cross-Site -> Same-Origin redirect, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpsOrigin', 'httpsCrossSite', 'httpsOrigin'], { mime: 'application/javascript' }
+ );
+
+ return induceRequest(url,
+ {"type": "module"}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Cross-Site -> Same-Origin redirect, attributes: type=module');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpsOrigin', 'httpsSameSite', 'httpsOrigin'], { mime: 'application/javascript' }
+ );
+
+ return induceRequest(url,
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Same-Site -> Same-Origin redirect, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpsOrigin', 'httpsSameSite', 'httpsOrigin'], { mime: 'application/javascript' }
+ );
+
+ return induceRequest(url,
+ {"type": "module"}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Same-Site -> Same-Origin redirect, attributes: type=module');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpsCrossSite', 'httpsOrigin'], { mime: 'application/javascript' }
+ );
+
+ return induceRequest(url,
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-Site -> Same Origin, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpsCrossSite', 'httpsOrigin'], { mime: 'application/javascript' }
+ );
+
+ return induceRequest(url,
+ {"type": "module"}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-Site -> Same Origin, attributes: type=module');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpsCrossSite', 'httpsSameSite'], { mime: 'application/javascript' }
+ );
+
+ return induceRequest(url,
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-Site -> Same-Site, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpsCrossSite', 'httpsSameSite'], { mime: 'application/javascript' }
+ );
+
+ return induceRequest(url,
+ {"type": "module"}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-Site -> Same-Site, attributes: type=module');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpsCrossSite', 'httpsCrossSite'], { mime: 'application/javascript' }
+ );
+
+ return induceRequest(url,
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-Site -> Cross-Site, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpsCrossSite', 'httpsCrossSite'], { mime: 'application/javascript' }
+ );
+
+ return induceRequest(url,
+ {"type": "module"}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-Site -> Cross-Site, attributes: type=module');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpsOrigin', 'httpsOrigin'], { mime: 'application/javascript' }
+ );
+
+ return induceRequest(url,
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-origin']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Same Origin, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpsOrigin', 'httpsOrigin'], { mime: 'application/javascript' }
+ );
+
+ return induceRequest(url,
+ {"type": "module"}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-origin']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Same Origin, attributes: type=module');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpsOrigin', 'httpsSameSite'], { mime: 'application/javascript' }
+ );
+
+ return induceRequest(url,
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Same-Site, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpsOrigin', 'httpsSameSite'], { mime: 'application/javascript' }
+ );
+
+ return induceRequest(url,
+ {"type": "module"}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Same-Site, attributes: type=module');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpsOrigin', 'httpsCrossSite'], { mime: 'application/javascript' }
+ );
+
+ return induceRequest(url,
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Cross-Site, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpsOrigin', 'httpsCrossSite'], { mime: 'application/javascript' }
+ );
+
+ return induceRequest(url,
+ {"type": "module"}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Cross-Site, attributes: type=module');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpsSameSite', 'httpsOrigin'], { mime: 'application/javascript' }
+ );
+
+ return induceRequest(url,
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Site -> Same Origin, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpsSameSite', 'httpsOrigin'], { mime: 'application/javascript' }
+ );
+
+ return induceRequest(url,
+ {"type": "module"}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Site -> Same Origin, attributes: type=module');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpsSameSite', 'httpsSameSite'], { mime: 'application/javascript' }
+ );
+
+ return induceRequest(url,
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Site -> Same-Site, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpsSameSite', 'httpsSameSite'], { mime: 'application/javascript' }
+ );
+
+ return induceRequest(url,
+ {"type": "module"}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Site -> Same-Site, attributes: type=module');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpsSameSite', 'httpsCrossSite'], { mime: 'application/javascript' }
+ );
+
+ return induceRequest(url,
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Same-Site -> Cross-Site, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpsSameSite', 'httpsCrossSite'], { mime: 'application/javascript' }
+ );
+
+ return induceRequest(url,
+ {"type": "module"}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Same-Site -> Cross-Site, attributes: type=module');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, [], { mime: 'application/javascript' }
+ );
+
+ return induceRequest(url,
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-mode');
+ assert_array_equals(headers['sec-fetch-mode'], ['no-cors']);
+ });
+ }, 'sec-fetch-mode - no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, [], { mime: 'application/javascript' }
+ );
+
+ return induceRequest(url,
+ {"type": "module"}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-mode');
+ assert_array_equals(headers['sec-fetch-mode'], ['cors']);
+ });
+ }, 'sec-fetch-mode - attributes: type=module');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, [], { mime: 'application/javascript' }
+ );
+
+ return induceRequest(url,
+ {"crossorigin": ""}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-mode');
+ assert_array_equals(headers['sec-fetch-mode'], ['cors']);
+ });
+ }, 'sec-fetch-mode - attributes: crossorigin');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, [], { mime: 'application/javascript' }
+ );
+
+ return induceRequest(url,
+ {"crossorigin": "anonymous"}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-mode');
+ assert_array_equals(headers['sec-fetch-mode'], ['cors']);
+ });
+ }, 'sec-fetch-mode - attributes: crossorigin=anonymous');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, [], { mime: 'application/javascript' }
+ );
+
+ return induceRequest(url,
+ {"crossorigin": "use-credentials"}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-mode');
+ assert_array_equals(headers['sec-fetch-mode'], ['cors']);
+ });
+ }, 'sec-fetch-mode - attributes: crossorigin=use-credentials');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, [], { mime: 'application/javascript' }
+ );
+
+ return induceRequest(url,
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-dest');
+ assert_array_equals(headers['sec-fetch-dest'], ['script']);
+ });
+ }, 'sec-fetch-dest - no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, [], { mime: 'application/javascript' }
+ );
+
+ return induceRequest(url,
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - no attributes');
+ </script>
+</html>
diff --git a/test/wpt/tests/fetch/metadata/generated/element-script.sub.html b/test/wpt/tests/fetch/metadata/generated/element-script.sub.html
new file mode 100644
index 0000000..a252669
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/generated/element-script.sub.html
@@ -0,0 +1,488 @@
+<!DOCTYPE html>
+<!--
+This test was procedurally generated. Please do not modify it directly.
+Sources:
+- fetch/metadata/tools/fetch-metadata.conf.yml
+- fetch/metadata/tools/templates/element-script.sub.html
+-->
+<html lang="en">
+ <meta charset="utf-8">
+ <title>HTTP headers on request for HTML "script" element source</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/fetch/metadata/resources/helper.sub.js"></script>
+ <body>
+ <script>
+ 'use strict';
+
+ function induceRequest(url, attributes) {
+ const script = document.createElement('script');
+ script.setAttribute('src', url);
+
+ for (const [ name, value ] of Object.entries(attributes)) {
+ script.setAttribute(name, value);
+ }
+
+ return new Promise((resolve, reject) => {
+ script.onload = resolve;
+ script.onerror = () => reject('Failed to load script');
+ document.body.appendChild(script);
+ })
+ .then(() => script.remove());
+ }
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpOrigin'], { mime: 'application/javascript' }
+ );
+
+ return induceRequest(url,
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - Not sent to non-trustworthy same-origin destination, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpOrigin'], { mime: 'application/javascript' }
+ );
+
+ return induceRequest(url,
+ {"type": "module"}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - Not sent to non-trustworthy same-origin destination, attributes: type=module');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpSameSite'], { mime: 'application/javascript' }
+ );
+
+ return induceRequest(url,
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - Not sent to non-trustworthy same-site destination, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpSameSite'], { mime: 'application/javascript' }
+ );
+
+ return induceRequest(url,
+ {"type": "module"}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - Not sent to non-trustworthy same-site destination, attributes: type=module');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpCrossSite'], { mime: 'application/javascript' }
+ );
+
+ return induceRequest(url,
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - Not sent to non-trustworthy cross-site destination, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpCrossSite'], { mime: 'application/javascript' }
+ );
+
+ return induceRequest(url,
+ {"type": "module"}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - Not sent to non-trustworthy cross-site destination, attributes: type=module');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpOrigin'], { mime: 'application/javascript' }
+ );
+
+ return induceRequest(url,
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ });
+ }, 'sec-fetch-mode - Not sent to non-trustworthy same-origin destination, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpOrigin'], { mime: 'application/javascript' }
+ );
+
+ return induceRequest(url,
+ {"type": "module"}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ });
+ }, 'sec-fetch-mode - Not sent to non-trustworthy same-origin destination, attributes: type=module');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpSameSite'], { mime: 'application/javascript' }
+ );
+
+ return induceRequest(url,
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ });
+ }, 'sec-fetch-mode - Not sent to non-trustworthy same-site destination, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpSameSite'], { mime: 'application/javascript' }
+ );
+
+ return induceRequest(url,
+ {"type": "module"}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ });
+ }, 'sec-fetch-mode - Not sent to non-trustworthy same-site destination, attributes: type=module');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpCrossSite'], { mime: 'application/javascript' }
+ );
+
+ return induceRequest(url,
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ });
+ }, 'sec-fetch-mode - Not sent to non-trustworthy cross-site destination, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpCrossSite'], { mime: 'application/javascript' }
+ );
+
+ return induceRequest(url,
+ {"type": "module"}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ });
+ }, 'sec-fetch-mode - Not sent to non-trustworthy cross-site destination, attributes: type=module');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpOrigin'], { mime: 'application/javascript' }
+ );
+
+ return induceRequest(url,
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ });
+ }, 'sec-fetch-dest - Not sent to non-trustworthy same-origin destination, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpOrigin'], { mime: 'application/javascript' }
+ );
+
+ return induceRequest(url,
+ {"type": "module"}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ });
+ }, 'sec-fetch-dest - Not sent to non-trustworthy same-origin destination, attributes: type=module');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpSameSite'], { mime: 'application/javascript' }
+ );
+
+ return induceRequest(url,
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ });
+ }, 'sec-fetch-dest - Not sent to non-trustworthy same-site destination, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpSameSite'], { mime: 'application/javascript' }
+ );
+
+ return induceRequest(url,
+ {"type": "module"}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ });
+ }, 'sec-fetch-dest - Not sent to non-trustworthy same-site destination, attributes: type=module');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpCrossSite'], { mime: 'application/javascript' }
+ );
+
+ return induceRequest(url,
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ });
+ }, 'sec-fetch-dest - Not sent to non-trustworthy cross-site destination, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpCrossSite'], { mime: 'application/javascript' }
+ );
+
+ return induceRequest(url,
+ {"type": "module"}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ });
+ }, 'sec-fetch-dest - Not sent to non-trustworthy cross-site destination, attributes: type=module');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpOrigin'], { mime: 'application/javascript' }
+ );
+
+ return induceRequest(url,
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - Not sent to non-trustworthy same-origin destination, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpOrigin'], { mime: 'application/javascript' }
+ );
+
+ return induceRequest(url,
+ {"type": "module"}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - Not sent to non-trustworthy same-origin destination, attributes: type=module');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpSameSite'], { mime: 'application/javascript' }
+ );
+
+ return induceRequest(url,
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - Not sent to non-trustworthy same-site destination, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpSameSite'], { mime: 'application/javascript' }
+ );
+
+ return induceRequest(url,
+ {"type": "module"}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - Not sent to non-trustworthy same-site destination, attributes: type=module');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpCrossSite'], { mime: 'application/javascript' }
+ );
+
+ return induceRequest(url,
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - Not sent to non-trustworthy cross-site destination, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpCrossSite'], { mime: 'application/javascript' }
+ );
+
+ return induceRequest(url,
+ {"type": "module"}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - Not sent to non-trustworthy cross-site destination, attributes: type=module');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpsOrigin', 'httpOrigin'], { mime: 'application/javascript' }
+ );
+
+ return induceRequest(url,
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - HTTPS downgrade (header not sent), no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpsOrigin', 'httpOrigin'], { mime: 'application/javascript' }
+ );
+
+ return induceRequest(url,
+ {"type": "module"}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - HTTPS downgrade (header not sent), attributes: type=module');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpOrigin', 'httpsOrigin'], { mime: 'application/javascript' }
+ );
+
+ return induceRequest(url,
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - HTTPS upgrade, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpOrigin', 'httpsOrigin'], { mime: 'application/javascript' }
+ );
+
+ return induceRequest(url,
+ {"type": "module"}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - HTTPS upgrade, attributes: type=module');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin'], { mime: 'application/javascript' }
+ );
+
+ return induceRequest(url,
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - HTTPS downgrade-upgrade, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin'], { mime: 'application/javascript' }
+ );
+
+ return induceRequest(url,
+ {"type": "module"}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - HTTPS downgrade-upgrade, attributes: type=module');
+ </script>
+</html>
diff --git a/test/wpt/tests/fetch/metadata/generated/element-video-poster.https.sub.html b/test/wpt/tests/fetch/metadata/generated/element-video-poster.https.sub.html
new file mode 100644
index 0000000..5805b46
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/generated/element-video-poster.https.sub.html
@@ -0,0 +1,243 @@
+<!DOCTYPE html>
+<!--
+This test was procedurally generated. Please do not modify it directly.
+Sources:
+- fetch/metadata/tools/fetch-metadata.conf.yml
+- fetch/metadata/tools/templates/element-video-poster.sub.html
+-->
+<html lang="en">
+ <meta charset="utf-8">
+ <title>HTTP headers on request for HTML "video" element "poster"</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/fetch/metadata/resources/helper.sub.js"></script>
+ <body>
+ <script>
+ 'use strict';
+
+ const params = {
+ body: `
+ <svg xmlns="http://www.w3.org/2000/svg" width="123" height="123">
+ <rect fill="lime" width="123" height="123"/>
+ </svg>
+ `,
+ mime: 'image/svg+xml'
+ };
+
+ function induceRequest(t, url) {
+ var video = document.createElement('video');
+ video.setAttribute('poster', url);
+ document.body.appendChild(video);
+
+ const poll = () => {
+ if (video.clientWidth === 123) {
+ return;
+ }
+
+ return new Promise((resolve) => t.step_timeout(resolve, 0))
+ .then(poll);
+ };
+ t.add_cleanup(() => video.remove());
+
+ return poll();
+ }
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(t, makeRequestURL(key, ['httpsOrigin'], params))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-origin']);
+ });
+ }, 'sec-fetch-site - Same origin');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(t, makeRequestURL(key, ['httpsCrossSite'], params))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-site');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(t, makeRequestURL(key, ['httpsSameSite'], params))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same site');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(t, makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite', 'httpsOrigin'], params))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Cross-Site -> Same-Origin redirect');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(t, makeRequestURL(key, ['httpsOrigin', 'httpsSameSite', 'httpsOrigin'], params))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Same-Site -> Same-Origin redirect');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(t, makeRequestURL(key, ['httpsCrossSite', 'httpsOrigin'], params))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-Site -> Same Origin');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(t, makeRequestURL(key, ['httpsCrossSite', 'httpsSameSite'], params))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-Site -> Same-Site');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(t, makeRequestURL(key, ['httpsCrossSite', 'httpsCrossSite'], params))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-Site -> Cross-Site');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(t, makeRequestURL(key, ['httpsOrigin', 'httpsOrigin'], params))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-origin']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Same Origin');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(t, makeRequestURL(key, ['httpsOrigin', 'httpsSameSite'], params))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Same-Site');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(t, makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite'], params))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Cross-Site');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(t, makeRequestURL(key, ['httpsSameSite', 'httpsOrigin'], params))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Site -> Same Origin');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(t, makeRequestURL(key, ['httpsSameSite', 'httpsSameSite'], params))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Site -> Same-Site');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(t, makeRequestURL(key, ['httpsSameSite', 'httpsCrossSite'], params))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Same-Site -> Cross-Site');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(t, makeRequestURL(key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin'], params))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - HTTPS downgrade-upgrade');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(t, makeRequestURL(key, [], params))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-mode');
+ assert_array_equals(headers['sec-fetch-mode'], ['no-cors']);
+ });
+ }, 'sec-fetch-mode');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(t, makeRequestURL(key, [], params))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-dest');
+ assert_array_equals(headers['sec-fetch-dest'], ['image']);
+ });
+ }, 'sec-fetch-dest');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(t, makeRequestURL(key, [], params))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user');
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/fetch/metadata/generated/element-video-poster.sub.html b/test/wpt/tests/fetch/metadata/generated/element-video-poster.sub.html
new file mode 100644
index 0000000..e6cc5ee
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/generated/element-video-poster.sub.html
@@ -0,0 +1,198 @@
+<!DOCTYPE html>
+<!--
+This test was procedurally generated. Please do not modify it directly.
+Sources:
+- fetch/metadata/tools/fetch-metadata.conf.yml
+- fetch/metadata/tools/templates/element-video-poster.sub.html
+-->
+<html lang="en">
+ <meta charset="utf-8">
+ <title>HTTP headers on request for HTML "video" element "poster"</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/fetch/metadata/resources/helper.sub.js"></script>
+ <body>
+ <script>
+ 'use strict';
+
+ const params = {
+ body: `
+ <svg xmlns="http://www.w3.org/2000/svg" width="123" height="123">
+ <rect fill="lime" width="123" height="123"/>
+ </svg>
+ `,
+ mime: 'image/svg+xml'
+ };
+
+ function induceRequest(t, url) {
+ var video = document.createElement('video');
+ video.setAttribute('poster', url);
+ document.body.appendChild(video);
+
+ const poll = () => {
+ if (video.clientWidth === 123) {
+ return;
+ }
+
+ return new Promise((resolve) => t.step_timeout(resolve, 0))
+ .then(poll);
+ };
+ t.add_cleanup(() => video.remove());
+
+ return poll();
+ }
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(t, makeRequestURL(key, ['httpOrigin'], params))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - Not sent to non-trustworthy same-origin destination');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(t, makeRequestURL(key, ['httpSameSite'], params))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - Not sent to non-trustworthy same-site destination');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(t, makeRequestURL(key, ['httpCrossSite'], params))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - Not sent to non-trustworthy cross-site destination');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(t, makeRequestURL(key, ['httpOrigin'], params))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ });
+ }, 'sec-fetch-mode - Not sent to non-trustworthy same-origin destination');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(t, makeRequestURL(key, ['httpSameSite'], params))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ });
+ }, 'sec-fetch-mode - Not sent to non-trustworthy same-site destination');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(t, makeRequestURL(key, ['httpCrossSite'], params))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ });
+ }, 'sec-fetch-mode - Not sent to non-trustworthy cross-site destination');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(t, makeRequestURL(key, ['httpOrigin'], params))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ });
+ }, 'sec-fetch-dest - Not sent to non-trustworthy same-origin destination');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(t, makeRequestURL(key, ['httpSameSite'], params))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ });
+ }, 'sec-fetch-dest - Not sent to non-trustworthy same-site destination');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(t, makeRequestURL(key, ['httpCrossSite'], params))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ });
+ }, 'sec-fetch-dest - Not sent to non-trustworthy cross-site destination');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(t, makeRequestURL(key, ['httpOrigin'], params))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - Not sent to non-trustworthy same-origin destination');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(t, makeRequestURL(key, ['httpSameSite'], params))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - Not sent to non-trustworthy same-site destination');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(t, makeRequestURL(key, ['httpCrossSite'], params))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - Not sent to non-trustworthy cross-site destination');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(t, makeRequestURL(key, ['httpsOrigin', 'httpOrigin'], params))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - HTTPS downgrade (header not sent)');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(t, makeRequestURL(key, ['httpOrigin', 'httpsOrigin'], params))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - HTTPS upgrade');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(t, makeRequestURL(key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin'], params))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - HTTPS downgrade-upgrade');
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/fetch/metadata/generated/element-video.https.sub.html b/test/wpt/tests/fetch/metadata/generated/element-video.https.sub.html
new file mode 100644
index 0000000..971360d
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/generated/element-video.https.sub.html
@@ -0,0 +1,325 @@
+<!DOCTYPE html>
+<!--
+This test was procedurally generated. Please do not modify it directly.
+Sources:
+- fetch/metadata/tools/fetch-metadata.conf.yml
+- fetch/metadata/tools/templates/element-video.sub.html
+-->
+<html lang="en">
+ <meta charset="utf-8">
+ <title>HTTP headers on request for HTML "video" element source</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/fetch/metadata/resources/helper.sub.js"></script>
+ <body>
+ <script>
+ 'use strict';
+
+ function induceRequest(url, attributes) {
+ const video = document.createElement('video');
+
+ for (const [ name, value ] of Object.entries(attributes)) {
+ video.setAttribute(name, value);
+ }
+
+ return new Promise((resolve) => {
+ video.setAttribute('src', url);
+ video.onload = video.onerror = resolve;
+ });
+ }
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin']),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-origin']);
+ });
+ }, 'sec-fetch-site - Same origin, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsCrossSite']),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-site, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsSameSite']),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same site, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite', 'httpsOrigin']),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Cross-Site -> Same-Origin redirect, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpsSameSite', 'httpsOrigin']),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Same-Site -> Same-Origin redirect, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsCrossSite', 'httpsOrigin']),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-Site -> Same Origin, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsCrossSite', 'httpsSameSite']),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-Site -> Same-Site, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsCrossSite', 'httpsCrossSite']),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-Site -> Cross-Site, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpsOrigin']),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-origin']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Same Origin, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpsSameSite']),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Same-Site, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite']),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Cross-Site, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsSameSite', 'httpsOrigin']),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Site -> Same Origin, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsSameSite', 'httpsSameSite']),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Site -> Same-Site, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsSameSite', 'httpsCrossSite']),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Same-Site -> Cross-Site, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin']),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - HTTPS downgrade-upgrade, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, []),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-mode');
+ assert_array_equals(headers['sec-fetch-mode'], ['no-cors']);
+ });
+ }, 'sec-fetch-mode - no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, []),
+ {"crossorigin": ""}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-mode');
+ assert_array_equals(headers['sec-fetch-mode'], ['cors']);
+ });
+ }, 'sec-fetch-mode - attributes: crossorigin');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, []),
+ {"crossorigin": "anonymous"}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-mode');
+ assert_array_equals(headers['sec-fetch-mode'], ['cors']);
+ });
+ }, 'sec-fetch-mode - attributes: crossorigin=anonymous');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, []),
+ {"crossorigin": "use-credentials"}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-mode');
+ assert_array_equals(headers['sec-fetch-mode'], ['cors']);
+ });
+ }, 'sec-fetch-mode - attributes: crossorigin=use-credentials');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, []),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-dest');
+ assert_array_equals(headers['sec-fetch-dest'], ['video']);
+ });
+ }, 'sec-fetch-dest - no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, []),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - no attributes');
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/fetch/metadata/generated/element-video.sub.html b/test/wpt/tests/fetch/metadata/generated/element-video.sub.html
new file mode 100644
index 0000000..9707413
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/generated/element-video.sub.html
@@ -0,0 +1,229 @@
+<!DOCTYPE html>
+<!--
+This test was procedurally generated. Please do not modify it directly.
+Sources:
+- fetch/metadata/tools/fetch-metadata.conf.yml
+- fetch/metadata/tools/templates/element-video.sub.html
+-->
+<html lang="en">
+ <meta charset="utf-8">
+ <title>HTTP headers on request for HTML "video" element source</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/fetch/metadata/resources/helper.sub.js"></script>
+ <body>
+ <script>
+ 'use strict';
+
+ function induceRequest(url, attributes) {
+ const video = document.createElement('video');
+
+ for (const [ name, value ] of Object.entries(attributes)) {
+ video.setAttribute(name, value);
+ }
+
+ return new Promise((resolve) => {
+ video.setAttribute('src', url);
+ video.onload = video.onerror = resolve;
+ });
+ }
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpOrigin']),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - Not sent to non-trustworthy same-origin destination, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpSameSite']),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - Not sent to non-trustworthy same-site destination, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpCrossSite']),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - Not sent to non-trustworthy cross-site destination, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpOrigin']),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ });
+ }, 'sec-fetch-mode - Not sent to non-trustworthy same-origin destination, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpSameSite']),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ });
+ }, 'sec-fetch-mode - Not sent to non-trustworthy same-site destination, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpCrossSite']),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ });
+ }, 'sec-fetch-mode - Not sent to non-trustworthy cross-site destination, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpOrigin']),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ });
+ }, 'sec-fetch-dest - Not sent to non-trustworthy same-origin destination, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpSameSite']),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ });
+ }, 'sec-fetch-dest - Not sent to non-trustworthy same-site destination, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpCrossSite']),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ });
+ }, 'sec-fetch-dest - Not sent to non-trustworthy cross-site destination, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpOrigin']),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - Not sent to non-trustworthy same-origin destination, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpSameSite']),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - Not sent to non-trustworthy same-site destination, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpCrossSite']),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - Not sent to non-trustworthy cross-site destination, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpOrigin']),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - HTTPS downgrade (header not sent), no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpOrigin', 'httpsOrigin']),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - HTTPS upgrade, no attributes');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin']),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - HTTPS downgrade-upgrade, no attributes');
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/fetch/metadata/generated/fetch-via-serviceworker.https.sub.html b/test/wpt/tests/fetch/metadata/generated/fetch-via-serviceworker.https.sub.html
new file mode 100644
index 0000000..22f9309
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/generated/fetch-via-serviceworker.https.sub.html
@@ -0,0 +1,683 @@
+<!DOCTYPE html>
+<!--
+This test was procedurally generated. Please do not modify it directly.
+Sources:
+- fetch/metadata/tools/fetch-metadata.conf.yml
+- fetch/metadata/tools/templates/fetch-via-serviceworker.https.sub.html
+-->
+<html lang="en">
+ <meta charset="utf-8">
+ <meta name="timeout" content="long">
+ <title>HTTP headers on request using the "fetch" API and passing through a Serive Worker</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script>
+ <script src="/fetch/metadata/resources/helper.sub.js"></script>
+ <body>
+ <script>
+ 'use strict';
+
+ const scripts = {
+ fallback: '/fetch/metadata/resources/fetch-via-serviceworker--fallback--sw.js',
+ respondWith: '/fetch/metadata/resources/fetch-via-serviceworker--respondWith--sw.js'
+ };
+
+ function induceRequest(t, url, init, script) {
+ const SCOPE = '/fetch/metadata/resources/fetch-via-serviceworker-frame.html';
+ const SCRIPT = scripts[script];
+
+ return service_worker_unregister_and_register(t, SCRIPT, SCOPE)
+ .then((registration) => {
+ t.add_cleanup(() => registration.unregister());
+
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(() => with_iframe(SCOPE))
+ .then((frame) => {
+ t.add_cleanup(() => frame.remove());
+
+ return frame.contentWindow.fetch(url, init);
+ });
+ }
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ t,
+ makeRequestURL(key, ['httpsOrigin']),
+ {"mode": "no-cors"},
+ 'respondWith'
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-origin']);
+ });
+ }, 'sec-fetch-site - Same origin, init: mode=no-cors - respondWith');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ t,
+ makeRequestURL(key, ['httpsOrigin']),
+ {"mode": "no-cors"},
+ 'fallback'
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-origin']);
+ });
+ }, 'sec-fetch-site - Same origin, init: mode=no-cors - fallback');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ t,
+ makeRequestURL(key, ['httpsCrossSite']),
+ {"mode": "no-cors"},
+ 'respondWith'
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-site, init: mode=no-cors - respondWith');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ t,
+ makeRequestURL(key, ['httpsCrossSite']),
+ {"mode": "no-cors"},
+ 'fallback'
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-site, init: mode=no-cors - fallback');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ t,
+ makeRequestURL(key, ['httpsSameSite']),
+ {"mode": "no-cors"},
+ 'respondWith'
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same site, init: mode=no-cors - respondWith');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ t,
+ makeRequestURL(key, ['httpsSameSite']),
+ {"mode": "no-cors"},
+ 'fallback'
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same site, init: mode=no-cors - fallback');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ t,
+ makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite', 'httpsOrigin']),
+ {"mode": "no-cors"},
+ 'respondWith'
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Cross-Site -> Same-Origin redirect, init: mode=no-cors - respondWith');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ t,
+ makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite', 'httpsOrigin']),
+ {"mode": "no-cors"},
+ 'fallback'
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Cross-Site -> Same-Origin redirect, init: mode=no-cors - fallback');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ t,
+ makeRequestURL(key, ['httpsOrigin', 'httpsSameSite', 'httpsOrigin']),
+ {"mode": "no-cors"},
+ 'respondWith'
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Same-Site -> Same-Origin redirect, init: mode=no-cors - respondWith');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ t,
+ makeRequestURL(key, ['httpsOrigin', 'httpsSameSite', 'httpsOrigin']),
+ {"mode": "no-cors"},
+ 'fallback'
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Same-Site -> Same-Origin redirect, init: mode=no-cors - fallback');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ t,
+ makeRequestURL(key, ['httpsCrossSite', 'httpsOrigin']),
+ {"mode": "no-cors"},
+ 'respondWith'
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-Site -> Same Origin, init: mode=no-cors - respondWith');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ t,
+ makeRequestURL(key, ['httpsCrossSite', 'httpsOrigin']),
+ {"mode": "no-cors"},
+ 'fallback'
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-Site -> Same Origin, init: mode=no-cors - fallback');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ t,
+ makeRequestURL(key, ['httpsCrossSite', 'httpsSameSite']),
+ {"mode": "no-cors"},
+ 'respondWith'
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-Site -> Same-Site, init: mode=no-cors - respondWith');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ t,
+ makeRequestURL(key, ['httpsCrossSite', 'httpsSameSite']),
+ {"mode": "no-cors"},
+ 'fallback'
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-Site -> Same-Site, init: mode=no-cors - fallback');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ t,
+ makeRequestURL(key, ['httpsCrossSite', 'httpsCrossSite']),
+ {"mode": "no-cors"},
+ 'respondWith'
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-Site -> Cross-Site, init: mode=no-cors - respondWith');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ t,
+ makeRequestURL(key, ['httpsCrossSite', 'httpsCrossSite']),
+ {"mode": "no-cors"},
+ 'fallback'
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-Site -> Cross-Site, init: mode=no-cors - fallback');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ t,
+ makeRequestURL(key, ['httpsOrigin', 'httpsOrigin']),
+ {"mode": "no-cors"},
+ 'respondWith'
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-origin']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Same Origin, init: mode=no-cors - respondWith');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ t,
+ makeRequestURL(key, ['httpsOrigin', 'httpsOrigin']),
+ {"mode": "no-cors"},
+ 'fallback'
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-origin']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Same Origin, init: mode=no-cors - fallback');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ t,
+ makeRequestURL(key, ['httpsOrigin', 'httpsSameSite']),
+ {"mode": "no-cors"},
+ 'respondWith'
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Same-Site, init: mode=no-cors - respondWith');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ t,
+ makeRequestURL(key, ['httpsOrigin', 'httpsSameSite']),
+ {"mode": "no-cors"},
+ 'fallback'
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Same-Site, init: mode=no-cors - fallback');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ t,
+ makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite']),
+ {"mode": "no-cors"},
+ 'respondWith'
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Cross-Site, init: mode=no-cors - respondWith');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ t,
+ makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite']),
+ {"mode": "no-cors"},
+ 'fallback'
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Cross-Site, init: mode=no-cors - fallback');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ t,
+ makeRequestURL(key, ['httpsSameSite', 'httpsOrigin']),
+ {"mode": "no-cors"},
+ 'respondWith'
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Site -> Same Origin, init: mode=no-cors - respondWith');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ t,
+ makeRequestURL(key, ['httpsSameSite', 'httpsOrigin']),
+ {"mode": "no-cors"},
+ 'fallback'
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Site -> Same Origin, init: mode=no-cors - fallback');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ t,
+ makeRequestURL(key, ['httpsSameSite', 'httpsSameSite']),
+ {"mode": "no-cors"},
+ 'respondWith'
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Site -> Same-Site, init: mode=no-cors - respondWith');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ t,
+ makeRequestURL(key, ['httpsSameSite', 'httpsSameSite']),
+ {"mode": "no-cors"},
+ 'fallback'
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Site -> Same-Site, init: mode=no-cors - fallback');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ t,
+ makeRequestURL(key, ['httpsSameSite', 'httpsCrossSite']),
+ {"mode": "no-cors"},
+ 'respondWith'
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Same-Site -> Cross-Site, init: mode=no-cors - respondWith');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ t,
+ makeRequestURL(key, ['httpsSameSite', 'httpsCrossSite']),
+ {"mode": "no-cors"},
+ 'fallback'
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Same-Site -> Cross-Site, init: mode=no-cors - fallback');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ t,
+ makeRequestURL(key, []),
+ {},
+ 'respondWith'
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-mode');
+ assert_array_equals(headers['sec-fetch-mode'], ['cors']);
+ });
+ }, 'sec-fetch-mode - no init - respondWith');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ t,
+ makeRequestURL(key, []),
+ {},
+ 'fallback'
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-mode');
+ assert_array_equals(headers['sec-fetch-mode'], ['cors']);
+ });
+ }, 'sec-fetch-mode - no init - fallback');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ t,
+ makeRequestURL(key, []),
+ {"mode": "cors"},
+ 'respondWith'
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-mode');
+ assert_array_equals(headers['sec-fetch-mode'], ['cors']);
+ });
+ }, 'sec-fetch-mode - init: mode=cors - respondWith');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ t,
+ makeRequestURL(key, []),
+ {"mode": "cors"},
+ 'fallback'
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-mode');
+ assert_array_equals(headers['sec-fetch-mode'], ['cors']);
+ });
+ }, 'sec-fetch-mode - init: mode=cors - fallback');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ t,
+ makeRequestURL(key, []),
+ {"mode": "no-cors"},
+ 'respondWith'
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-mode');
+ assert_array_equals(headers['sec-fetch-mode'], ['no-cors']);
+ });
+ }, 'sec-fetch-mode - init: mode=no-cors - respondWith');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ t,
+ makeRequestURL(key, []),
+ {"mode": "no-cors"},
+ 'fallback'
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-mode');
+ assert_array_equals(headers['sec-fetch-mode'], ['no-cors']);
+ });
+ }, 'sec-fetch-mode - init: mode=no-cors - fallback');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ t,
+ makeRequestURL(key, []),
+ {"mode": "same-origin"},
+ 'respondWith'
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-mode');
+ assert_array_equals(headers['sec-fetch-mode'], ['same-origin']);
+ });
+ }, 'sec-fetch-mode - init: mode=same-origin - respondWith');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ t,
+ makeRequestURL(key, []),
+ {"mode": "same-origin"},
+ 'fallback'
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-mode');
+ assert_array_equals(headers['sec-fetch-mode'], ['same-origin']);
+ });
+ }, 'sec-fetch-mode - init: mode=same-origin - fallback');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ t,
+ makeRequestURL(key, []),
+ {},
+ 'respondWith'
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-dest');
+ assert_array_equals(headers['sec-fetch-dest'], ['empty']);
+ });
+ }, 'sec-fetch-dest - no init - respondWith');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ t,
+ makeRequestURL(key, []),
+ {},
+ 'fallback'
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-dest');
+ assert_array_equals(headers['sec-fetch-dest'], ['empty']);
+ });
+ }, 'sec-fetch-dest - no init - fallback');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ t,
+ makeRequestURL(key, []),
+ {},
+ 'respondWith'
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - no init - respondWith');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ t,
+ makeRequestURL(key, []),
+ {},
+ 'fallback'
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - no init - fallback');
+
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/fetch/metadata/generated/fetch.https.sub.html b/test/wpt/tests/fetch/metadata/generated/fetch.https.sub.html
new file mode 100644
index 0000000..dde1dae
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/generated/fetch.https.sub.html
@@ -0,0 +1,302 @@
+<!DOCTYPE html>
+<!--
+This test was procedurally generated. Please do not modify it directly.
+Sources:
+- fetch/metadata/tools/fetch-metadata.conf.yml
+- fetch/metadata/tools/templates/fetch.sub.html
+-->
+<html lang="en">
+ <meta charset="utf-8">
+ <title>HTTP headers on request using the "fetch" API</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/fetch/metadata/resources/helper.sub.js"></script>
+ <body>
+ <script>
+ 'use strict';
+
+ function induceRequest(url, init) {
+ return fetch(url, init);
+ }
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin']),
+ {"mode": "no-cors"}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-origin']);
+ });
+ }, 'sec-fetch-site - Same origin, init: mode=no-cors');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsCrossSite']),
+ {"mode": "no-cors"}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-site, init: mode=no-cors');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsSameSite']),
+ {"mode": "no-cors"}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same site, init: mode=no-cors');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite', 'httpsOrigin']),
+ {"mode": "no-cors"}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Cross-Site -> Same-Origin redirect, init: mode=no-cors');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpsSameSite', 'httpsOrigin']),
+ {"mode": "no-cors"}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Same-Site -> Same-Origin redirect, init: mode=no-cors');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsCrossSite', 'httpsOrigin']),
+ {"mode": "no-cors"}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-Site -> Same Origin, init: mode=no-cors');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsCrossSite', 'httpsSameSite']),
+ {"mode": "no-cors"}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-Site -> Same-Site, init: mode=no-cors');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsCrossSite', 'httpsCrossSite']),
+ {"mode": "no-cors"}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-Site -> Cross-Site, init: mode=no-cors');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpsOrigin']),
+ {"mode": "no-cors"}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-origin']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Same Origin, init: mode=no-cors');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpsSameSite']),
+ {"mode": "no-cors"}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Same-Site, init: mode=no-cors');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite']),
+ {"mode": "no-cors"}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Cross-Site, init: mode=no-cors');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsSameSite', 'httpsOrigin']),
+ {"mode": "no-cors"}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Site -> Same Origin, init: mode=no-cors');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsSameSite', 'httpsSameSite']),
+ {"mode": "no-cors"}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Site -> Same-Site, init: mode=no-cors');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsSameSite', 'httpsCrossSite']),
+ {"mode": "no-cors"}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Same-Site -> Cross-Site, init: mode=no-cors');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, []),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-mode');
+ assert_array_equals(headers['sec-fetch-mode'], ['cors']);
+ });
+ }, 'sec-fetch-mode - no init');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, []),
+ {"mode": "cors"}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-mode');
+ assert_array_equals(headers['sec-fetch-mode'], ['cors']);
+ });
+ }, 'sec-fetch-mode - init: mode=cors');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, []),
+ {"mode": "no-cors"}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-mode');
+ assert_array_equals(headers['sec-fetch-mode'], ['no-cors']);
+ });
+ }, 'sec-fetch-mode - init: mode=no-cors');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, []),
+ {"mode": "same-origin"}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-mode');
+ assert_array_equals(headers['sec-fetch-mode'], ['same-origin']);
+ });
+ }, 'sec-fetch-mode - init: mode=same-origin');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, []),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-dest');
+ assert_array_equals(headers['sec-fetch-dest'], ['empty']);
+ });
+ }, 'sec-fetch-dest - no init');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, []),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - no init');
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/fetch/metadata/generated/fetch.sub.html b/test/wpt/tests/fetch/metadata/generated/fetch.sub.html
new file mode 100644
index 0000000..d28ea9b
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/generated/fetch.sub.html
@@ -0,0 +1,220 @@
+<!DOCTYPE html>
+<!--
+This test was procedurally generated. Please do not modify it directly.
+Sources:
+- fetch/metadata/tools/fetch-metadata.conf.yml
+- fetch/metadata/tools/templates/fetch.sub.html
+-->
+<html lang="en">
+ <meta charset="utf-8">
+ <title>HTTP headers on request using the "fetch" API</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/fetch/metadata/resources/helper.sub.js"></script>
+ <body>
+ <script>
+ 'use strict';
+
+ function induceRequest(url, init) {
+ return fetch(url, init);
+ }
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpOrigin']),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - Not sent to non-trustworthy same-origin destination, no init');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpSameSite']),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - Not sent to non-trustworthy same-site destination, no init');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpCrossSite']),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - Not sent to non-trustworthy cross-site destination, no init');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpOrigin']),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ });
+ }, 'sec-fetch-mode - Not sent to non-trustworthy same-origin destination, no init');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpSameSite']),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ });
+ }, 'sec-fetch-mode - Not sent to non-trustworthy same-site destination, no init');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpCrossSite']),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ });
+ }, 'sec-fetch-mode - Not sent to non-trustworthy cross-site destination, no init');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpOrigin']),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ });
+ }, 'sec-fetch-dest - Not sent to non-trustworthy same-origin destination, no init');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpSameSite']),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ });
+ }, 'sec-fetch-dest - Not sent to non-trustworthy same-site destination, no init');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpCrossSite']),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ });
+ }, 'sec-fetch-dest - Not sent to non-trustworthy cross-site destination, no init');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpOrigin']),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - Not sent to non-trustworthy same-origin destination, no init');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpSameSite']),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - Not sent to non-trustworthy same-site destination, no init');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpCrossSite']),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - Not sent to non-trustworthy cross-site destination, no init');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpOrigin']),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - HTTPS downgrade (header not sent), no init');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpOrigin', 'httpsOrigin']),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - HTTPS upgrade, no init');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin']),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - HTTPS downgrade-upgrade, no init');
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/fetch/metadata/generated/form-submission.https.sub.html b/test/wpt/tests/fetch/metadata/generated/form-submission.https.sub.html
new file mode 100644
index 0000000..988b07c
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/generated/form-submission.https.sub.html
@@ -0,0 +1,522 @@
+<!DOCTYPE html>
+<!--
+This test was procedurally generated. Please do not modify it directly.
+Sources:
+- fetch/metadata/tools/fetch-metadata.conf.yml
+- fetch/metadata/tools/templates/form-submission.sub.html
+-->
+<html lang="en">
+ <meta charset="utf-8">
+ <meta name="timeout" content="long">
+ <title>HTTP headers on request for HTML form navigation</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/resources/testdriver.js"></script>
+ <script src="/resources/testdriver-vendor.js"></script>
+ <script src="/fetch/metadata/resources/helper.sub.js"></script>
+ <body>
+ <script>
+ 'use strict';
+
+ function induceRequest(method, url, userActivated) {
+ const windowName = String(Math.random());
+ const form = document.createElement('form');
+ const submit = document.createElement('input');
+ submit.setAttribute('type', 'submit');
+ form.appendChild(submit);
+ const win = open('about:blank', windowName);
+ form.setAttribute('method', method);
+ form.setAttribute('action', url);
+ form.setAttribute('target', windowName);
+ document.body.appendChild(form);
+
+ // Query parameters must be expressed as form values so that they are sent
+ // with the submission of forms whose method is POST.
+ Array.from(new URL(url, location.origin).searchParams)
+ .forEach(([name, value]) => {
+ const input = document.createElement('input');
+ input.setAttribute('type', 'hidden');
+ input.setAttribute('name', name);
+ input.setAttribute('value', value);
+ form.appendChild(input);
+ });
+
+ return new Promise((resolve) => {
+ addEventListener('message', function(event) {
+ if (event.source === win) {
+ resolve();
+ }
+ });
+
+ if (userActivated) {
+ test_driver.click(submit);
+ } else {
+ submit.click();
+ }
+ })
+ .then(() => {
+ form.remove();
+ win.close();
+ });
+ }
+ const responseParams = {
+ mime: 'text/html',
+ body: `<script>opener.postMessage('done', '*')</${''}script>`
+ };
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsOrigin'], responseParams);
+ const userActivated = false;
+ return induceRequest('GET', url, userActivated)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-origin']);
+ });
+ }, 'sec-fetch-site - Same origin - GET');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsOrigin'], responseParams);
+ const userActivated = false;
+ return induceRequest('POST', url, userActivated)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-origin']);
+ });
+ }, 'sec-fetch-site - Same origin - POST');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsCrossSite'], responseParams);
+ const userActivated = false;
+ return induceRequest('GET', url, userActivated)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-site - GET');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsCrossSite'], responseParams);
+ const userActivated = false;
+ return induceRequest('POST', url, userActivated)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-site - POST');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsSameSite'], responseParams);
+ const userActivated = false;
+ return induceRequest('GET', url, userActivated)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same site - GET');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsSameSite'], responseParams);
+ const userActivated = false;
+ return induceRequest('POST', url, userActivated)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same site - POST');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite', 'httpsOrigin'], responseParams);
+ const userActivated = false;
+ return induceRequest('GET', url, userActivated)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Cross-Site -> Same-Origin redirect - GET');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite', 'httpsOrigin'], responseParams);
+ const userActivated = false;
+ return induceRequest('POST', url, userActivated)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Cross-Site -> Same-Origin redirect - POST');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsOrigin', 'httpsSameSite', 'httpsOrigin'], responseParams);
+ const userActivated = false;
+ return induceRequest('GET', url, userActivated)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Same-Site -> Same-Origin redirect - GET');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsOrigin', 'httpsSameSite', 'httpsOrigin'], responseParams);
+ const userActivated = false;
+ return induceRequest('POST', url, userActivated)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Same-Site -> Same-Origin redirect - POST');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsCrossSite', 'httpsOrigin'], responseParams);
+ const userActivated = false;
+ return induceRequest('GET', url, userActivated)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-Site -> Same Origin - GET');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsCrossSite', 'httpsOrigin'], responseParams);
+ const userActivated = false;
+ return induceRequest('POST', url, userActivated)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-Site -> Same Origin - POST');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsCrossSite', 'httpsSameSite'], responseParams);
+ const userActivated = false;
+ return induceRequest('GET', url, userActivated)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-Site -> Same-Site - GET');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsCrossSite', 'httpsSameSite'], responseParams);
+ const userActivated = false;
+ return induceRequest('POST', url, userActivated)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-Site -> Same-Site - POST');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsCrossSite', 'httpsCrossSite'], responseParams);
+ const userActivated = false;
+ return induceRequest('GET', url, userActivated)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-Site -> Cross-Site - GET');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsCrossSite', 'httpsCrossSite'], responseParams);
+ const userActivated = false;
+ return induceRequest('POST', url, userActivated)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-Site -> Cross-Site - POST');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsOrigin', 'httpsOrigin'], responseParams);
+ const userActivated = false;
+ return induceRequest('GET', url, userActivated)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-origin']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Same Origin - GET');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsOrigin', 'httpsOrigin'], responseParams);
+ const userActivated = false;
+ return induceRequest('POST', url, userActivated)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-origin']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Same Origin - POST');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsOrigin', 'httpsSameSite'], responseParams);
+ const userActivated = false;
+ return induceRequest('GET', url, userActivated)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Same-Site - GET');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsOrigin', 'httpsSameSite'], responseParams);
+ const userActivated = false;
+ return induceRequest('POST', url, userActivated)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Same-Site - POST');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite'], responseParams);
+ const userActivated = false;
+ return induceRequest('GET', url, userActivated)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Cross-Site - GET');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite'], responseParams);
+ const userActivated = false;
+ return induceRequest('POST', url, userActivated)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Cross-Site - POST');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsSameSite', 'httpsOrigin'], responseParams);
+ const userActivated = false;
+ return induceRequest('GET', url, userActivated)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Site -> Same Origin - GET');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsSameSite', 'httpsOrigin'], responseParams);
+ const userActivated = false;
+ return induceRequest('POST', url, userActivated)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Site -> Same Origin - POST');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsSameSite', 'httpsSameSite'], responseParams);
+ const userActivated = false;
+ return induceRequest('GET', url, userActivated)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Site -> Same-Site - GET');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsSameSite', 'httpsSameSite'], responseParams);
+ const userActivated = false;
+ return induceRequest('POST', url, userActivated)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Site -> Same-Site - POST');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsSameSite', 'httpsCrossSite'], responseParams);
+ const userActivated = false;
+ return induceRequest('GET', url, userActivated)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Same-Site -> Cross-Site - GET');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsSameSite', 'httpsCrossSite'], responseParams);
+ const userActivated = false;
+ return induceRequest('POST', url, userActivated)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Same-Site -> Cross-Site - POST');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin'], responseParams);
+ const userActivated = false;
+ return induceRequest('GET', url, userActivated)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - HTTPS downgrade-upgrade - GET');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin'], responseParams);
+ const userActivated = false;
+ return induceRequest('POST', url, userActivated)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - HTTPS downgrade-upgrade - POST');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, [], responseParams);
+ const userActivated = false;
+ return induceRequest('GET', url, userActivated)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-mode');
+ assert_array_equals(headers['sec-fetch-mode'], ['navigate']);
+ });
+ }, 'sec-fetch-mode - GET');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, [], responseParams);
+ const userActivated = false;
+ return induceRequest('POST', url, userActivated)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-mode');
+ assert_array_equals(headers['sec-fetch-mode'], ['navigate']);
+ });
+ }, 'sec-fetch-mode - POST');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, [], responseParams);
+ const userActivated = false;
+ return induceRequest('GET', url, userActivated)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-dest');
+ assert_array_equals(headers['sec-fetch-dest'], ['document']);
+ });
+ }, 'sec-fetch-dest - GET');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, [], responseParams);
+ const userActivated = false;
+ return induceRequest('POST', url, userActivated)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-dest');
+ assert_array_equals(headers['sec-fetch-dest'], ['document']);
+ });
+ }, 'sec-fetch-dest - POST');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, [], responseParams);
+ const userActivated = false;
+ return induceRequest('GET', url, userActivated)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - GET');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, [], responseParams);
+ const userActivated = true;
+ return induceRequest('GET', url, userActivated)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-user');
+ assert_array_equals(headers['sec-fetch-user'], ['?1']);
+ });
+ }, 'sec-fetch-user - GET with user activation');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, [], responseParams);
+ const userActivated = false;
+ return induceRequest('POST', url, userActivated)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - POST');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, [], responseParams);
+ const userActivated = true;
+ return induceRequest('POST', url, userActivated)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-user');
+ assert_array_equals(headers['sec-fetch-user'], ['?1']);
+ });
+ }, 'sec-fetch-user - POST with user activation');
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/fetch/metadata/generated/form-submission.sub.html b/test/wpt/tests/fetch/metadata/generated/form-submission.sub.html
new file mode 100644
index 0000000..f862062
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/generated/form-submission.sub.html
@@ -0,0 +1,400 @@
+<!DOCTYPE html>
+<!--
+This test was procedurally generated. Please do not modify it directly.
+Sources:
+- fetch/metadata/tools/fetch-metadata.conf.yml
+- fetch/metadata/tools/templates/form-submission.sub.html
+-->
+<html lang="en">
+ <meta charset="utf-8">
+ <meta name="timeout" content="long">
+ <title>HTTP headers on request for HTML form navigation</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/fetch/metadata/resources/helper.sub.js"></script>
+ <body>
+ <script>
+ 'use strict';
+
+ function induceRequest(method, url, userActivated) {
+ const windowName = String(Math.random());
+ const form = document.createElement('form');
+ const submit = document.createElement('input');
+ submit.setAttribute('type', 'submit');
+ form.appendChild(submit);
+ const win = open('about:blank', windowName);
+ form.setAttribute('method', method);
+ form.setAttribute('action', url);
+ form.setAttribute('target', windowName);
+ document.body.appendChild(form);
+
+ // Query parameters must be expressed as form values so that they are sent
+ // with the submission of forms whose method is POST.
+ Array.from(new URL(url, location.origin).searchParams)
+ .forEach(([name, value]) => {
+ const input = document.createElement('input');
+ input.setAttribute('type', 'hidden');
+ input.setAttribute('name', name);
+ input.setAttribute('value', value);
+ form.appendChild(input);
+ });
+
+ return new Promise((resolve) => {
+ addEventListener('message', function(event) {
+ if (event.source === win) {
+ resolve();
+ }
+ });
+
+ if (userActivated) {
+ test_driver.click(submit);
+ } else {
+ submit.click();
+ }
+ })
+ .then(() => {
+ form.remove();
+ win.close();
+ });
+ }
+ const responseParams = {
+ mime: 'text/html',
+ body: `<script>opener.postMessage('done', '*')</${''}script>`
+ };
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpOrigin'], responseParams);
+ const userActivated = false;
+ return induceRequest('GET', url, userActivated)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - Not sent to non-trustworthy same-origin destination - GET');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpOrigin'], responseParams);
+ const userActivated = false;
+ return induceRequest('POST', url, userActivated)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - Not sent to non-trustworthy same-origin destination - POST');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpSameSite'], responseParams);
+ const userActivated = false;
+ return induceRequest('GET', url, userActivated)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - Not sent to non-trustworthy same-site destination - GET');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpSameSite'], responseParams);
+ const userActivated = false;
+ return induceRequest('POST', url, userActivated)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - Not sent to non-trustworthy same-site destination - POST');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpCrossSite'], responseParams);
+ const userActivated = false;
+ return induceRequest('GET', url, userActivated)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - Not sent to non-trustworthy cross-site destination - GET');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpCrossSite'], responseParams);
+ const userActivated = false;
+ return induceRequest('POST', url, userActivated)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - Not sent to non-trustworthy cross-site destination - POST');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpOrigin'], responseParams);
+ const userActivated = false;
+ return induceRequest('GET', url, userActivated)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ });
+ }, 'sec-fetch-mode - Not sent to non-trustworthy same-origin destination - GET');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpOrigin'], responseParams);
+ const userActivated = false;
+ return induceRequest('POST', url, userActivated)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ });
+ }, 'sec-fetch-mode - Not sent to non-trustworthy same-origin destination - POST');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpSameSite'], responseParams);
+ const userActivated = false;
+ return induceRequest('GET', url, userActivated)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ });
+ }, 'sec-fetch-mode - Not sent to non-trustworthy same-site destination - GET');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpSameSite'], responseParams);
+ const userActivated = false;
+ return induceRequest('POST', url, userActivated)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ });
+ }, 'sec-fetch-mode - Not sent to non-trustworthy same-site destination - POST');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpCrossSite'], responseParams);
+ const userActivated = false;
+ return induceRequest('GET', url, userActivated)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ });
+ }, 'sec-fetch-mode - Not sent to non-trustworthy cross-site destination - GET');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpCrossSite'], responseParams);
+ const userActivated = false;
+ return induceRequest('POST', url, userActivated)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ });
+ }, 'sec-fetch-mode - Not sent to non-trustworthy cross-site destination - POST');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpOrigin'], responseParams);
+ const userActivated = false;
+ return induceRequest('GET', url, userActivated)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ });
+ }, 'sec-fetch-dest - Not sent to non-trustworthy same-origin destination - GET');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpOrigin'], responseParams);
+ const userActivated = false;
+ return induceRequest('POST', url, userActivated)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ });
+ }, 'sec-fetch-dest - Not sent to non-trustworthy same-origin destination - POST');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpSameSite'], responseParams);
+ const userActivated = false;
+ return induceRequest('GET', url, userActivated)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ });
+ }, 'sec-fetch-dest - Not sent to non-trustworthy same-site destination - GET');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpSameSite'], responseParams);
+ const userActivated = false;
+ return induceRequest('POST', url, userActivated)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ });
+ }, 'sec-fetch-dest - Not sent to non-trustworthy same-site destination - POST');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpCrossSite'], responseParams);
+ const userActivated = false;
+ return induceRequest('GET', url, userActivated)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ });
+ }, 'sec-fetch-dest - Not sent to non-trustworthy cross-site destination - GET');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpCrossSite'], responseParams);
+ const userActivated = false;
+ return induceRequest('POST', url, userActivated)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ });
+ }, 'sec-fetch-dest - Not sent to non-trustworthy cross-site destination - POST');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpOrigin'], responseParams);
+ const userActivated = false;
+ return induceRequest('GET', url, userActivated)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - Not sent to non-trustworthy same-origin destination - GET');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpOrigin'], responseParams);
+ const userActivated = false;
+ return induceRequest('POST', url, userActivated)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - Not sent to non-trustworthy same-origin destination - POST');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpSameSite'], responseParams);
+ const userActivated = false;
+ return induceRequest('GET', url, userActivated)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - Not sent to non-trustworthy same-site destination - GET');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpSameSite'], responseParams);
+ const userActivated = false;
+ return induceRequest('POST', url, userActivated)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - Not sent to non-trustworthy same-site destination - POST');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpCrossSite'], responseParams);
+ const userActivated = false;
+ return induceRequest('GET', url, userActivated)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - Not sent to non-trustworthy cross-site destination - GET');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpCrossSite'], responseParams);
+ const userActivated = false;
+ return induceRequest('POST', url, userActivated)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - Not sent to non-trustworthy cross-site destination - POST');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsOrigin', 'httpOrigin'], responseParams);
+ const userActivated = false;
+ return induceRequest('GET', url, userActivated)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - HTTPS downgrade (header not sent) - GET');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsOrigin', 'httpOrigin'], responseParams);
+ const userActivated = false;
+ return induceRequest('POST', url, userActivated)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - HTTPS downgrade (header not sent) - POST');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpOrigin', 'httpsOrigin'], responseParams);
+ const userActivated = false;
+ return induceRequest('GET', url, userActivated)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - HTTPS upgrade - GET');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpOrigin', 'httpsOrigin'], responseParams);
+ const userActivated = false;
+ return induceRequest('POST', url, userActivated)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - HTTPS upgrade - POST');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin'], responseParams);
+ const userActivated = false;
+ return induceRequest('GET', url, userActivated)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - HTTPS downgrade-upgrade - GET');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin'], responseParams);
+ const userActivated = false;
+ return induceRequest('POST', url, userActivated)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - HTTPS downgrade-upgrade - POST');
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/fetch/metadata/generated/header-link.https.sub.html b/test/wpt/tests/fetch/metadata/generated/header-link.https.sub.html
new file mode 100644
index 0000000..09f0113
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/generated/header-link.https.sub.html
@@ -0,0 +1,529 @@
+<!DOCTYPE html>
+<!--
+This test was procedurally generated. Please do not modify it directly.
+Sources:
+- fetch/metadata/tools/fetch-metadata.conf.yml
+- fetch/metadata/tools/templates/header-link.sub.html
+-->
+<html lang="en">
+ <meta charset="utf-8">
+ <title>HTTP headers on request for HTTP "Link" header</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/fetch/metadata/resources/helper.sub.js"></script>
+ <body>
+ <script>
+ 'use strict';
+
+ function induceRequest(url, rel, test) {
+ const iframe = document.createElement('iframe');
+
+ iframe.setAttribute(
+ 'src',
+ '/fetch/metadata/resources/header-link.py' +
+ `?location=${encodeURIComponent(url)}&rel=${rel}`
+ );
+
+ document.body.appendChild(iframe);
+ test.add_cleanup(() => iframe.remove());
+
+ return new Promise((resolve) => {
+ iframe.onload = iframe.onerror = resolve;
+ });
+ }
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin'], {mime: 'text/html'}),
+ 'icon',
+ t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-origin']);
+ });
+ }, 'sec-fetch-site rel=icon - Same origin');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin'], {mime: 'text/html'}),
+ 'stylesheet',
+ t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-origin']);
+ });
+ }, 'sec-fetch-site rel=stylesheet - Same origin');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsCrossSite'], {mime: 'text/html'}),
+ 'icon',
+ t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site rel=icon - Cross-site');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsCrossSite'], {mime: 'text/html'}),
+ 'stylesheet',
+ t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site rel=stylesheet - Cross-site');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsSameSite'], {mime: 'text/html'}),
+ 'icon',
+ t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site rel=icon - Same site');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsSameSite'], {mime: 'text/html'}),
+ 'stylesheet',
+ t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site rel=stylesheet - Same site');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite', 'httpsOrigin'], {mime: 'text/html'}),
+ 'icon',
+ t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site rel=icon - Same-Origin -> Cross-Site -> Same-Origin redirect');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite', 'httpsOrigin'], {mime: 'text/html'}),
+ 'stylesheet',
+ t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site rel=stylesheet - Same-Origin -> Cross-Site -> Same-Origin redirect');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpsSameSite', 'httpsOrigin'], {mime: 'text/html'}),
+ 'icon',
+ t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site rel=icon - Same-Origin -> Same-Site -> Same-Origin redirect');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpsSameSite', 'httpsOrigin'], {mime: 'text/html'}),
+ 'stylesheet',
+ t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site rel=stylesheet - Same-Origin -> Same-Site -> Same-Origin redirect');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsCrossSite', 'httpsOrigin'], {mime: 'text/html'}),
+ 'icon',
+ t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site rel=icon - Cross-Site -> Same Origin');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsCrossSite', 'httpsOrigin'], {mime: 'text/html'}),
+ 'stylesheet',
+ t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site rel=stylesheet - Cross-Site -> Same Origin');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsCrossSite', 'httpsSameSite'], {mime: 'text/html'}),
+ 'icon',
+ t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site rel=icon - Cross-Site -> Same-Site');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsCrossSite', 'httpsSameSite'], {mime: 'text/html'}),
+ 'stylesheet',
+ t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site rel=stylesheet - Cross-Site -> Same-Site');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsCrossSite', 'httpsCrossSite'], {mime: 'text/html'}),
+ 'icon',
+ t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site rel=icon - Cross-Site -> Cross-Site');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsCrossSite', 'httpsCrossSite'], {mime: 'text/html'}),
+ 'stylesheet',
+ t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site rel=stylesheet - Cross-Site -> Cross-Site');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpsOrigin'], {mime: 'text/html'}),
+ 'icon',
+ t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-origin']);
+ });
+ }, 'sec-fetch-site rel=icon - Same-Origin -> Same Origin');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpsOrigin'], {mime: 'text/html'}),
+ 'stylesheet',
+ t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-origin']);
+ });
+ }, 'sec-fetch-site rel=stylesheet - Same-Origin -> Same Origin');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpsSameSite'], {mime: 'text/html'}),
+ 'icon',
+ t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site rel=icon - Same-Origin -> Same-Site');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpsSameSite'], {mime: 'text/html'}),
+ 'stylesheet',
+ t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site rel=stylesheet - Same-Origin -> Same-Site');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite'], {mime: 'text/html'}),
+ 'icon',
+ t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site rel=icon - Same-Origin -> Cross-Site');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite'], {mime: 'text/html'}),
+ 'stylesheet',
+ t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site rel=stylesheet - Same-Origin -> Cross-Site');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsSameSite', 'httpsOrigin'], {mime: 'text/html'}),
+ 'icon',
+ t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site rel=icon - Same-Site -> Same Origin');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsSameSite', 'httpsOrigin'], {mime: 'text/html'}),
+ 'stylesheet',
+ t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site rel=stylesheet - Same-Site -> Same Origin');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsSameSite', 'httpsSameSite'], {mime: 'text/html'}),
+ 'icon',
+ t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site rel=icon - Same-Site -> Same-Site');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsSameSite', 'httpsSameSite'], {mime: 'text/html'}),
+ 'stylesheet',
+ t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site rel=stylesheet - Same-Site -> Same-Site');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsSameSite', 'httpsCrossSite'], {mime: 'text/html'}),
+ 'icon',
+ t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site rel=icon - Same-Site -> Cross-Site');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsSameSite', 'httpsCrossSite'], {mime: 'text/html'}),
+ 'stylesheet',
+ t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site rel=stylesheet - Same-Site -> Cross-Site');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, [], {mime: 'text/html'}),
+ 'icon',
+ t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-mode');
+ assert_array_equals(headers['sec-fetch-mode'], ['no-cors']);
+ });
+ }, 'sec-fetch-mode rel=icon');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, [], {mime: 'text/html'}),
+ 'stylesheet',
+ t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-mode');
+ assert_array_equals(headers['sec-fetch-mode'], ['no-cors']);
+ });
+ }, 'sec-fetch-mode rel=stylesheet');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, [], {mime: 'text/html'}),
+ 'icon',
+ t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-dest');
+ assert_array_equals(headers['sec-fetch-dest'], ['empty']);
+ });
+ }, 'sec-fetch-dest rel=icon');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, [], {mime: 'text/html'}),
+ 'icon',
+ t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user rel=icon');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, [], {mime: 'text/html'}),
+ 'stylesheet',
+ t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user rel=stylesheet');
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/fetch/metadata/generated/header-link.https.sub.tentative.html b/test/wpt/tests/fetch/metadata/generated/header-link.https.sub.tentative.html
new file mode 100644
index 0000000..307c37f
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/generated/header-link.https.sub.tentative.html
@@ -0,0 +1,51 @@
+<!DOCTYPE html>
+<!--
+This test was procedurally generated. Please do not modify it directly.
+Sources:
+- fetch/metadata/tools/fetch-metadata.conf.yml
+- fetch/metadata/tools/templates/header-link.sub.html
+-->
+<html lang="en">
+ <meta charset="utf-8">
+ <title>HTTP headers on request for HTTP "Link" header</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/fetch/metadata/resources/helper.sub.js"></script>
+ <body>
+ <script>
+ 'use strict';
+
+ function induceRequest(url, rel, test) {
+ const iframe = document.createElement('iframe');
+
+ iframe.setAttribute(
+ 'src',
+ '/fetch/metadata/resources/header-link.py' +
+ `?location=${encodeURIComponent(url)}&rel=${rel}`
+ );
+
+ document.body.appendChild(iframe);
+ test.add_cleanup(() => iframe.remove());
+
+ return new Promise((resolve) => {
+ iframe.onload = iframe.onerror = resolve;
+ });
+ }
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, [], {mime: 'text/html'}),
+ 'stylesheet',
+ t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-dest');
+ assert_array_equals(headers['sec-fetch-dest'], ['style']);
+ });
+ }, 'sec-fetch-dest rel=stylesheet');
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/fetch/metadata/generated/header-link.sub.html b/test/wpt/tests/fetch/metadata/generated/header-link.sub.html
new file mode 100644
index 0000000..8b6cdae
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/generated/header-link.sub.html
@@ -0,0 +1,460 @@
+<!DOCTYPE html>
+<!--
+This test was procedurally generated. Please do not modify it directly.
+Sources:
+- fetch/metadata/tools/fetch-metadata.conf.yml
+- fetch/metadata/tools/templates/header-link.sub.html
+-->
+<html lang="en">
+ <meta charset="utf-8">
+ <title>HTTP headers on request for HTTP "Link" header</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/fetch/metadata/resources/helper.sub.js"></script>
+ <body>
+ <script>
+ 'use strict';
+
+ function induceRequest(url, rel, test) {
+ const iframe = document.createElement('iframe');
+
+ iframe.setAttribute(
+ 'src',
+ '/fetch/metadata/resources/header-link.py' +
+ `?location=${encodeURIComponent(url)}&rel=${rel}`
+ );
+
+ document.body.appendChild(iframe);
+ test.add_cleanup(() => iframe.remove());
+
+ return new Promise((resolve) => {
+ iframe.onload = iframe.onerror = resolve;
+ });
+ }
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpOrigin'], {mime: 'text/html'}),
+ 'icon',
+ t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site rel=icon - Not sent to non-trustworthy same-origin destination');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpOrigin'], {mime: 'text/html'}),
+ 'stylesheet',
+ t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site rel=stylesheet - Not sent to non-trustworthy same-origin destination');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpSameSite'], {mime: 'text/html'}),
+ 'icon',
+ t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site rel=icon - Not sent to non-trustworthy same-site destination');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpSameSite'], {mime: 'text/html'}),
+ 'stylesheet',
+ t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site rel=stylesheet - Not sent to non-trustworthy same-site destination');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpCrossSite'], {mime: 'text/html'}),
+ 'icon',
+ t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site rel=icon - Not sent to non-trustworthy cross-site destination');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpCrossSite'], {mime: 'text/html'}),
+ 'stylesheet',
+ t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site rel=stylesheet - Not sent to non-trustworthy cross-site destination');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpOrigin'], {mime: 'text/html'}),
+ 'icon',
+ t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ });
+ }, 'sec-fetch-mode rel=icon - Not sent to non-trustworthy same-origin destination');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpOrigin'], {mime: 'text/html'}),
+ 'stylesheet',
+ t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ });
+ }, 'sec-fetch-mode rel=stylesheet - Not sent to non-trustworthy same-origin destination');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpSameSite'], {mime: 'text/html'}),
+ 'icon',
+ t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ });
+ }, 'sec-fetch-mode rel=icon - Not sent to non-trustworthy same-site destination');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpSameSite'], {mime: 'text/html'}),
+ 'stylesheet',
+ t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ });
+ }, 'sec-fetch-mode rel=stylesheet - Not sent to non-trustworthy same-site destination');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpCrossSite'], {mime: 'text/html'}),
+ 'icon',
+ t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ });
+ }, 'sec-fetch-mode rel=icon - Not sent to non-trustworthy cross-site destination');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpCrossSite'], {mime: 'text/html'}),
+ 'stylesheet',
+ t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ });
+ }, 'sec-fetch-mode rel=stylesheet - Not sent to non-trustworthy cross-site destination');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpOrigin'], {mime: 'text/html'}),
+ 'icon',
+ t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ });
+ }, 'sec-fetch-dest rel=icon - Not sent to non-trustworthy same-origin destination');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpOrigin'], {mime: 'text/html'}),
+ 'stylesheet',
+ t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ });
+ }, 'sec-fetch-dest rel=stylesheet - Not sent to non-trustworthy same-origin destination');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpSameSite'], {mime: 'text/html'}),
+ 'icon',
+ t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ });
+ }, 'sec-fetch-dest rel=icon - Not sent to non-trustworthy same-site destination');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpSameSite'], {mime: 'text/html'}),
+ 'stylesheet',
+ t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ });
+ }, 'sec-fetch-dest rel=stylesheet - Not sent to non-trustworthy same-site destination');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpCrossSite'], {mime: 'text/html'}),
+ 'icon',
+ t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ });
+ }, 'sec-fetch-dest rel=icon - Not sent to non-trustworthy cross-site destination');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpCrossSite'], {mime: 'text/html'}),
+ 'stylesheet',
+ t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ });
+ }, 'sec-fetch-dest rel=stylesheet - Not sent to non-trustworthy cross-site destination');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpOrigin'], {mime: 'text/html'}),
+ 'icon',
+ t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user rel=icon - Not sent to non-trustworthy same-origin destination');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpOrigin'], {mime: 'text/html'}),
+ 'stylesheet',
+ t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user rel=stylesheet - Not sent to non-trustworthy same-origin destination');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpSameSite'], {mime: 'text/html'}),
+ 'icon',
+ t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user rel=icon - Not sent to non-trustworthy same-site destination');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpSameSite'], {mime: 'text/html'}),
+ 'stylesheet',
+ t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user rel=stylesheet - Not sent to non-trustworthy same-site destination');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpCrossSite'], {mime: 'text/html'}),
+ 'icon',
+ t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user rel=icon - Not sent to non-trustworthy cross-site destination');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpCrossSite'], {mime: 'text/html'}),
+ 'stylesheet',
+ t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user rel=stylesheet - Not sent to non-trustworthy cross-site destination');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpOrigin'], {mime: 'text/html'}),
+ 'icon',
+ t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site rel=icon - HTTPS downgrade (header not sent)');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpOrigin'], {mime: 'text/html'}),
+ 'stylesheet',
+ t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site rel=stylesheet - HTTPS downgrade (header not sent)');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpOrigin', 'httpsOrigin'], {mime: 'text/html'}),
+ 'icon',
+ t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site rel=icon - HTTPS upgrade');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpOrigin', 'httpsOrigin'], {mime: 'text/html'}),
+ 'stylesheet',
+ t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site rel=stylesheet - HTTPS upgrade');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin'], {mime: 'text/html'}),
+ 'icon',
+ t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site rel=icon - HTTPS downgrade-upgrade');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin'], {mime: 'text/html'}),
+ 'stylesheet',
+ t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site rel=stylesheet - HTTPS downgrade-upgrade');
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/fetch/metadata/generated/header-refresh.https.optional.sub.html b/test/wpt/tests/fetch/metadata/generated/header-refresh.https.optional.sub.html
new file mode 100644
index 0000000..e63ee42
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/generated/header-refresh.https.optional.sub.html
@@ -0,0 +1,273 @@
+<!DOCTYPE html>
+<!--
+This test was procedurally generated. Please do not modify it directly.
+Sources:
+- fetch/metadata/tools/fetch-metadata.conf.yml
+- fetch/metadata/tools/templates/header-refresh.optional.sub.html
+-->
+<html lang="en">
+ <meta charset="utf-8">
+ <meta name="timeout" content="long">
+ <title>HTTP headers on request for HTTP "Refresh" header</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/fetch/metadata/resources/helper.sub.js"></script>
+ <body>
+ <script>
+ 'use strict';
+
+ function induceRequest(url, test) {
+ const win = window.open();
+ test.add_cleanup(() => win.close());
+
+ win.location = `/common/refresh.py?location=${encodeURIComponent(url)}`
+
+ return new Promise((resolve) => {
+ addEventListener('message', (event) => {
+ if (event.source === win) {
+ resolve();
+ }
+ });
+ });
+ }
+
+ const responseParams = {
+ mime: 'text/html',
+ body: `<script>opener.postMessage(0, '*')</${''}script>`
+ };
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin'], responseParams), t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-origin']);
+ });
+ }, 'sec-fetch-site - Same origin');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsCrossSite'], responseParams), t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-site');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsSameSite'], responseParams), t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same site');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite', 'httpsOrigin'], responseParams), t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Cross-Site -> Same-Origin redirect');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpsSameSite', 'httpsOrigin'], responseParams), t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Same-Site -> Same-Origin redirect');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsCrossSite', 'httpsOrigin'], responseParams), t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-Site -> Same Origin');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsCrossSite', 'httpsSameSite'], responseParams), t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-Site -> Same-Site');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsCrossSite', 'httpsCrossSite'], responseParams), t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-Site -> Cross-Site');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpsOrigin'], responseParams), t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-origin']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Same Origin');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpsSameSite'], responseParams), t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Same-Site');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite'], responseParams), t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Cross-Site');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsSameSite', 'httpsOrigin'], responseParams), t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Site -> Same Origin');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsSameSite', 'httpsSameSite'], responseParams), t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Site -> Same-Site');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsSameSite', 'httpsCrossSite'], responseParams), t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Same-Site -> Cross-Site');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin'], responseParams), t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - HTTPS downgrade-upgrade');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, [], responseParams), t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-mode');
+ assert_array_equals(headers['sec-fetch-mode'], ['navigate']);
+ });
+ }, 'sec-fetch-mode');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, [], responseParams), t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-dest');
+ assert_array_equals(headers['sec-fetch-dest'], ['document']);
+ });
+ }, 'sec-fetch-dest');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, [], responseParams), t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user');
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/fetch/metadata/generated/header-refresh.optional.sub.html b/test/wpt/tests/fetch/metadata/generated/header-refresh.optional.sub.html
new file mode 100644
index 0000000..4674ada
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/generated/header-refresh.optional.sub.html
@@ -0,0 +1,222 @@
+<!DOCTYPE html>
+<!--
+This test was procedurally generated. Please do not modify it directly.
+Sources:
+- fetch/metadata/tools/fetch-metadata.conf.yml
+- fetch/metadata/tools/templates/header-refresh.optional.sub.html
+-->
+<html lang="en">
+ <meta charset="utf-8">
+ <meta name="timeout" content="long">
+ <title>HTTP headers on request for HTTP "Refresh" header</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/fetch/metadata/resources/helper.sub.js"></script>
+ <body>
+ <script>
+ 'use strict';
+
+ function induceRequest(url, test) {
+ const win = window.open();
+ test.add_cleanup(() => win.close());
+
+ win.location = `/common/refresh.py?location=${encodeURIComponent(url)}`
+
+ return new Promise((resolve) => {
+ addEventListener('message', (event) => {
+ if (event.source === win) {
+ resolve();
+ }
+ });
+ });
+ }
+
+ const responseParams = {
+ mime: 'text/html',
+ body: `<script>opener.postMessage(0, '*')</${''}script>`
+ };
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpOrigin'], responseParams), t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - Not sent to non-trustworthy same-origin destination');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpSameSite'], responseParams), t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - Not sent to non-trustworthy same-site destination');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpCrossSite'], responseParams), t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - Not sent to non-trustworthy cross-site destination');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpOrigin'], responseParams), t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ });
+ }, 'sec-fetch-mode - Not sent to non-trustworthy same-origin destination');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpSameSite'], responseParams), t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ });
+ }, 'sec-fetch-mode - Not sent to non-trustworthy same-site destination');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpCrossSite'], responseParams), t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ });
+ }, 'sec-fetch-mode - Not sent to non-trustworthy cross-site destination');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpOrigin'], responseParams), t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ });
+ }, 'sec-fetch-dest - Not sent to non-trustworthy same-origin destination');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpSameSite'], responseParams), t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ });
+ }, 'sec-fetch-dest - Not sent to non-trustworthy same-site destination');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpCrossSite'], responseParams), t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ });
+ }, 'sec-fetch-dest - Not sent to non-trustworthy cross-site destination');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpOrigin'], responseParams), t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - Not sent to non-trustworthy same-origin destination');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpSameSite'], responseParams), t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - Not sent to non-trustworthy same-site destination');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpCrossSite'], responseParams), t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - Not sent to non-trustworthy cross-site destination');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpOrigin'], responseParams), t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - HTTPS downgrade (header not sent)');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpOrigin', 'httpsOrigin'], responseParams), t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - HTTPS upgrade');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin'], responseParams), t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - HTTPS downgrade-upgrade');
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/fetch/metadata/generated/script-module-import-dynamic.https.sub.html b/test/wpt/tests/fetch/metadata/generated/script-module-import-dynamic.https.sub.html
new file mode 100644
index 0000000..72d60fc
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/generated/script-module-import-dynamic.https.sub.html
@@ -0,0 +1,254 @@
+<!DOCTYPE html>
+<!--
+This test was procedurally generated. Please do not modify it directly.
+Sources:
+- fetch/metadata/tools/fetch-metadata.conf.yml
+- fetch/metadata/tools/templates/script-module-import-dynamic.sub.html
+-->
+<html lang="en">
+ <meta charset="utf-8">
+ <title>HTTP headers on request for dynamic ECMAScript module import</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/fetch/metadata/resources/helper.sub.js"></script>
+ <script type="module">
+ 'use strict';
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpsOrigin'], { mime: 'application/javascript' }
+ );
+
+ return import(url)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-origin']);
+ });
+ }, 'sec-fetch-site - Same origin');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpsCrossSite'], { mime: 'application/javascript' }
+ );
+
+ return import(url)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-site');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpsSameSite'], { mime: 'application/javascript' }
+ );
+
+ return import(url)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same site');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpsOrigin', 'httpsCrossSite', 'httpsOrigin'], { mime: 'application/javascript' }
+ );
+
+ return import(url)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Cross-Site -> Same-Origin redirect');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpsOrigin', 'httpsSameSite', 'httpsOrigin'], { mime: 'application/javascript' }
+ );
+
+ return import(url)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Same-Site -> Same-Origin redirect');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpsCrossSite', 'httpsOrigin'], { mime: 'application/javascript' }
+ );
+
+ return import(url)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-Site -> Same Origin');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpsCrossSite', 'httpsSameSite'], { mime: 'application/javascript' }
+ );
+
+ return import(url)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-Site -> Same-Site');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpsCrossSite', 'httpsCrossSite'], { mime: 'application/javascript' }
+ );
+
+ return import(url)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-Site -> Cross-Site');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpsOrigin', 'httpsOrigin'], { mime: 'application/javascript' }
+ );
+
+ return import(url)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-origin']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Same Origin');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpsOrigin', 'httpsSameSite'], { mime: 'application/javascript' }
+ );
+
+ return import(url)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Same-Site');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpsOrigin', 'httpsCrossSite'], { mime: 'application/javascript' }
+ );
+
+ return import(url)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Cross-Site');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpsSameSite', 'httpsOrigin'], { mime: 'application/javascript' }
+ );
+
+ return import(url)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Site -> Same Origin');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpsSameSite', 'httpsSameSite'], { mime: 'application/javascript' }
+ );
+
+ return import(url)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Site -> Same-Site');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpsSameSite', 'httpsCrossSite'], { mime: 'application/javascript' }
+ );
+
+ return import(url)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Same-Site -> Cross-Site');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, [], { mime: 'application/javascript' }
+ );
+
+ return import(url)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-mode');
+ assert_array_equals(headers['sec-fetch-mode'], ['cors']);
+ });
+ }, 'sec-fetch-mode');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, [], { mime: 'application/javascript' }
+ );
+
+ return import(url)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-dest');
+ assert_array_equals(headers['sec-fetch-dest'], ['script']);
+ });
+ }, 'sec-fetch-dest');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, [], { mime: 'application/javascript' }
+ );
+
+ return import(url)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user');
+ </script>
+</html>
diff --git a/test/wpt/tests/fetch/metadata/generated/script-module-import-dynamic.sub.html b/test/wpt/tests/fetch/metadata/generated/script-module-import-dynamic.sub.html
new file mode 100644
index 0000000..088720c
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/generated/script-module-import-dynamic.sub.html
@@ -0,0 +1,214 @@
+<!DOCTYPE html>
+<!--
+This test was procedurally generated. Please do not modify it directly.
+Sources:
+- fetch/metadata/tools/fetch-metadata.conf.yml
+- fetch/metadata/tools/templates/script-module-import-dynamic.sub.html
+-->
+<html lang="en">
+ <meta charset="utf-8">
+ <title>HTTP headers on request for dynamic ECMAScript module import</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/fetch/metadata/resources/helper.sub.js"></script>
+ <script type="module">
+ 'use strict';
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpOrigin'], { mime: 'application/javascript' }
+ );
+
+ return import(url)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - Not sent to non-trustworthy same-origin destination');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpSameSite'], { mime: 'application/javascript' }
+ );
+
+ return import(url)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - Not sent to non-trustworthy same-site destination');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpCrossSite'], { mime: 'application/javascript' }
+ );
+
+ return import(url)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - Not sent to non-trustworthy cross-site destination');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpOrigin'], { mime: 'application/javascript' }
+ );
+
+ return import(url)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ });
+ }, 'sec-fetch-mode - Not sent to non-trustworthy same-origin destination');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpSameSite'], { mime: 'application/javascript' }
+ );
+
+ return import(url)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ });
+ }, 'sec-fetch-mode - Not sent to non-trustworthy same-site destination');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpCrossSite'], { mime: 'application/javascript' }
+ );
+
+ return import(url)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ });
+ }, 'sec-fetch-mode - Not sent to non-trustworthy cross-site destination');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpOrigin'], { mime: 'application/javascript' }
+ );
+
+ return import(url)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ });
+ }, 'sec-fetch-dest - Not sent to non-trustworthy same-origin destination');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpSameSite'], { mime: 'application/javascript' }
+ );
+
+ return import(url)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ });
+ }, 'sec-fetch-dest - Not sent to non-trustworthy same-site destination');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpCrossSite'], { mime: 'application/javascript' }
+ );
+
+ return import(url)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ });
+ }, 'sec-fetch-dest - Not sent to non-trustworthy cross-site destination');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpOrigin'], { mime: 'application/javascript' }
+ );
+
+ return import(url)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - Not sent to non-trustworthy same-origin destination');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpSameSite'], { mime: 'application/javascript' }
+ );
+
+ return import(url)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - Not sent to non-trustworthy same-site destination');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpCrossSite'], { mime: 'application/javascript' }
+ );
+
+ return import(url)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - Not sent to non-trustworthy cross-site destination');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpsOrigin', 'httpOrigin'], { mime: 'application/javascript' }
+ );
+
+ return import(url)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - HTTPS downgrade (header not sent)');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpOrigin', 'httpsOrigin'], { mime: 'application/javascript' }
+ );
+
+ return import(url)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - HTTPS upgrade');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin'], { mime: 'application/javascript' }
+ );
+
+ return import(url)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - HTTPS downgrade-upgrade');
+ </script>
+</html>
diff --git a/test/wpt/tests/fetch/metadata/generated/script-module-import-static.https.sub.html b/test/wpt/tests/fetch/metadata/generated/script-module-import-static.https.sub.html
new file mode 100644
index 0000000..cea3464
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/generated/script-module-import-static.https.sub.html
@@ -0,0 +1,288 @@
+<!DOCTYPE html>
+<!--
+This test was procedurally generated. Please do not modify it directly.
+Sources:
+- fetch/metadata/tools/fetch-metadata.conf.yml
+- fetch/metadata/tools/templates/script-module-import-static.sub.html
+-->
+<html lang="en">
+ <meta charset="utf-8">
+ <title>HTTP headers on request for static ECMAScript module import</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/fetch/metadata/resources/helper.sub.js"></script>
+ <body>
+ <script>
+ 'use strict';
+
+ function induceRequest(url) {
+ const script = document.createElement('script');
+ script.setAttribute('type', 'module');
+ script.setAttribute(
+ 'src',
+ '/fetch/metadata/resources/es-module.sub.js?moduleId=' + encodeURIComponent(url)
+ );
+
+ return new Promise((resolve, reject) => {
+ script.onload = resolve;
+ script.onerror = () => reject('Failed to load script');
+ document.body.appendChild(script);
+ })
+ .then(() => script.remove());
+ }
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(
+ key, ['httpsOrigin'], { mime: 'application/javascript' }
+ )
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-origin']);
+ });
+ }, 'sec-fetch-site - Same origin');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(
+ key, ['httpsCrossSite'], { mime: 'application/javascript' }
+ )
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-site');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(
+ key, ['httpsSameSite'], { mime: 'application/javascript' }
+ )
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same site');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(
+ key, ['httpsOrigin', 'httpsCrossSite', 'httpsOrigin'], { mime: 'application/javascript' }
+ )
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Cross-Site -> Same-Origin redirect');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(
+ key, ['httpsOrigin', 'httpsSameSite', 'httpsOrigin'], { mime: 'application/javascript' }
+ )
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Same-Site -> Same-Origin redirect');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(
+ key, ['httpsCrossSite', 'httpsOrigin'], { mime: 'application/javascript' }
+ )
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-Site -> Same Origin');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(
+ key, ['httpsCrossSite', 'httpsSameSite'], { mime: 'application/javascript' }
+ )
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-Site -> Same-Site');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(
+ key, ['httpsCrossSite', 'httpsCrossSite'], { mime: 'application/javascript' }
+ )
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-Site -> Cross-Site');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(
+ key, ['httpsOrigin', 'httpsOrigin'], { mime: 'application/javascript' }
+ )
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-origin']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Same Origin');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(
+ key, ['httpsOrigin', 'httpsSameSite'], { mime: 'application/javascript' }
+ )
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Same-Site');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(
+ key, ['httpsOrigin', 'httpsCrossSite'], { mime: 'application/javascript' }
+ )
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Cross-Site');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(
+ key, ['httpsSameSite', 'httpsOrigin'], { mime: 'application/javascript' }
+ )
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Site -> Same Origin');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(
+ key, ['httpsSameSite', 'httpsSameSite'], { mime: 'application/javascript' }
+ )
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Site -> Same-Site');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(
+ key, ['httpsSameSite', 'httpsCrossSite'], { mime: 'application/javascript' }
+ )
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Same-Site -> Cross-Site');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(
+ key, [], { mime: 'application/javascript' }
+ )
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-mode');
+ assert_array_equals(headers['sec-fetch-mode'], ['cors']);
+ });
+ }, 'sec-fetch-mode');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(
+ key, [], { mime: 'application/javascript' }
+ )
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-dest');
+ assert_array_equals(headers['sec-fetch-dest'], ['script']);
+ });
+ }, 'sec-fetch-dest');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(
+ key, [], { mime: 'application/javascript' }
+ )
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user');
+ </script>
+</html>
diff --git a/test/wpt/tests/fetch/metadata/generated/script-module-import-static.sub.html b/test/wpt/tests/fetch/metadata/generated/script-module-import-static.sub.html
new file mode 100644
index 0000000..0f94f71
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/generated/script-module-import-static.sub.html
@@ -0,0 +1,246 @@
+<!DOCTYPE html>
+<!--
+This test was procedurally generated. Please do not modify it directly.
+Sources:
+- fetch/metadata/tools/fetch-metadata.conf.yml
+- fetch/metadata/tools/templates/script-module-import-static.sub.html
+-->
+<html lang="en">
+ <meta charset="utf-8">
+ <title>HTTP headers on request for static ECMAScript module import</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/fetch/metadata/resources/helper.sub.js"></script>
+ <body>
+ <script>
+ 'use strict';
+
+ function induceRequest(url) {
+ const script = document.createElement('script');
+ script.setAttribute('type', 'module');
+ script.setAttribute(
+ 'src',
+ '/fetch/metadata/resources/es-module.sub.js?moduleId=' + encodeURIComponent(url)
+ );
+
+ return new Promise((resolve, reject) => {
+ script.onload = resolve;
+ script.onerror = () => reject('Failed to load script');
+ document.body.appendChild(script);
+ })
+ .then(() => script.remove());
+ }
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(
+ key, ['httpOrigin'], { mime: 'application/javascript' }
+ )
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - Not sent to non-trustworthy same-origin destination');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(
+ key, ['httpSameSite'], { mime: 'application/javascript' }
+ )
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - Not sent to non-trustworthy same-site destination');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(
+ key, ['httpCrossSite'], { mime: 'application/javascript' }
+ )
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - Not sent to non-trustworthy cross-site destination');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(
+ key, ['httpOrigin'], { mime: 'application/javascript' }
+ )
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ });
+ }, 'sec-fetch-mode - Not sent to non-trustworthy same-origin destination');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(
+ key, ['httpSameSite'], { mime: 'application/javascript' }
+ )
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ });
+ }, 'sec-fetch-mode - Not sent to non-trustworthy same-site destination');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(
+ key, ['httpCrossSite'], { mime: 'application/javascript' }
+ )
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ });
+ }, 'sec-fetch-mode - Not sent to non-trustworthy cross-site destination');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(
+ key, ['httpOrigin'], { mime: 'application/javascript' }
+ )
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ });
+ }, 'sec-fetch-dest - Not sent to non-trustworthy same-origin destination');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(
+ key, ['httpSameSite'], { mime: 'application/javascript' }
+ )
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ });
+ }, 'sec-fetch-dest - Not sent to non-trustworthy same-site destination');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(
+ key, ['httpCrossSite'], { mime: 'application/javascript' }
+ )
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ });
+ }, 'sec-fetch-dest - Not sent to non-trustworthy cross-site destination');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(
+ key, ['httpOrigin'], { mime: 'application/javascript' }
+ )
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - Not sent to non-trustworthy same-origin destination');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(
+ key, ['httpSameSite'], { mime: 'application/javascript' }
+ )
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - Not sent to non-trustworthy same-site destination');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(
+ key, ['httpCrossSite'], { mime: 'application/javascript' }
+ )
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - Not sent to non-trustworthy cross-site destination');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(
+ key, ['httpsOrigin', 'httpOrigin'], { mime: 'application/javascript' }
+ )
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - HTTPS downgrade (header not sent)');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(
+ key, ['httpOrigin', 'httpsOrigin'], { mime: 'application/javascript' }
+ )
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - HTTPS upgrade');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(
+ key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin'], { mime: 'application/javascript' }
+ )
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - HTTPS downgrade-upgrade');
+ </script>
+</html>
diff --git a/test/wpt/tests/fetch/metadata/generated/serviceworker.https.sub.html b/test/wpt/tests/fetch/metadata/generated/serviceworker.https.sub.html
new file mode 100644
index 0000000..12e3736
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/generated/serviceworker.https.sub.html
@@ -0,0 +1,170 @@
+<!DOCTYPE html>
+<!--
+This test was procedurally generated. Please do not modify it directly.
+Sources:
+- fetch/metadata/tools/fetch-metadata.conf.yml
+- fetch/metadata/tools/templates/serviceworker.https.sub.html
+-->
+<!DOCTYPE html>
+<html lang="en">
+ <meta charset="utf-8">
+ <title>HTTP headers on request for Service Workers</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/fetch/metadata/resources/helper.sub.js"></script>
+ <body>
+ <script>
+ 'use strict';
+
+ function induceRequest(t, url, options, event, clear) {
+ // Register a service worker and check the request header.
+ return navigator.serviceWorker.register(url, options)
+ .then((registration) => {
+ t.add_cleanup(() => registration.unregister());
+ if (event === 'register') {
+ return;
+ }
+ return clear().then(() => registration.update());
+ });
+ }
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpsOrigin'], { mime: 'application/javascript' }
+ );
+
+ return induceRequest(t, url, {}, 'register')
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-origin']);
+ });
+ }, 'sec-fetch-site - Same origin, no options - registration');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpsOrigin'], { mime: 'application/javascript' }
+ );
+
+ return induceRequest(t, url, {}, 'update', () => retrieve(key))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-origin']);
+ });
+ }, 'sec-fetch-site - Same origin, no options - updating');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, [], { mime: 'application/javascript' }
+ );
+
+ return induceRequest(t, url, {"type": "classic"}, 'register')
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-mode');
+ assert_array_equals(headers['sec-fetch-mode'], ['same-origin']);
+ });
+ }, 'sec-fetch-mode - options: type=classic - registration');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, [], { mime: 'application/javascript' }
+ );
+
+ return induceRequest(t, url, {"type": "classic"}, 'update', () => retrieve(key))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-mode');
+ assert_array_equals(headers['sec-fetch-mode'], ['same-origin']);
+ });
+ }, 'sec-fetch-mode - options: type=classic - updating');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, [], { mime: 'application/javascript' }
+ );
+
+ return induceRequest(t, url, {}, 'register')
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-mode');
+ assert_array_equals(headers['sec-fetch-mode'], ['same-origin']);
+ });
+ }, 'sec-fetch-mode - no options - registration');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, [], { mime: 'application/javascript' }
+ );
+
+ return induceRequest(t, url, {}, 'update', () => retrieve(key))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-mode');
+ assert_array_equals(headers['sec-fetch-mode'], ['same-origin']);
+ });
+ }, 'sec-fetch-mode - no options - updating');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, [], { mime: 'application/javascript' }
+ );
+
+ return induceRequest(t, url, {}, 'register')
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-dest');
+ assert_array_equals(headers['sec-fetch-dest'], ['serviceworker']);
+ });
+ }, 'sec-fetch-dest - no options - registration');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, [], { mime: 'application/javascript' }
+ );
+
+ return induceRequest(t, url, {}, 'update', () => retrieve(key))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-dest');
+ assert_array_equals(headers['sec-fetch-dest'], ['serviceworker']);
+ });
+ }, 'sec-fetch-dest - no options - updating');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, [], { mime: 'application/javascript' }
+ );
+
+ return induceRequest(t, url, {}, 'register')
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - no options - registration');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, [], { mime: 'application/javascript' }
+ );
+
+ return induceRequest(t, url, {}, 'update', () => retrieve(key))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - no options - updating');
+ </script>
+</body>
+</html>
diff --git a/test/wpt/tests/fetch/metadata/generated/svg-image.https.sub.html b/test/wpt/tests/fetch/metadata/generated/svg-image.https.sub.html
new file mode 100644
index 0000000..b059eb3
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/generated/svg-image.https.sub.html
@@ -0,0 +1,367 @@
+<!DOCTYPE html>
+<!--
+This test was procedurally generated. Please do not modify it directly.
+Sources:
+- fetch/metadata/tools/fetch-metadata.conf.yml
+- fetch/metadata/tools/templates/svg-image.sub.html
+-->
+<html lang="en">
+ <meta charset="utf-8">
+ <meta name="timeout" content="long">
+ <title>HTTP headers on request for SVG "image" element source</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/fetch/metadata/resources/helper.sub.js"></script>
+ <body>
+ <script>
+ 'use strict';
+
+ const params = {
+ body: `
+ <svg xmlns="http://www.w3.org/2000/svg" width="123" height="123">
+ <rect fill="lime" width="123" height="123"/>
+ </svg>
+ `,
+ mime: 'image/svg+xml'
+ };
+
+ function induceRequest(t, url, attributes) {
+ const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
+ svg.setAttributeNS(
+ "http://www.w3.org/2000/xmlns/",
+ "xmlns:xlink",
+ "http://www.w3.org/1999/xlink"
+ );
+ const image = document.createElementNS("http://www.w3.org/2000/svg", "image");
+ image.setAttribute("href", url);
+ svg.appendChild(image);
+
+ for (const [ name, value ] of Object.entries(attributes)) {
+ image.setAttribute(name, value);
+ }
+
+ document.body.appendChild(svg);
+ t.add_cleanup(() => svg.remove());
+
+ return new Promise((resolve, reject) => {
+ image.onload = resolve;
+ image.onerror = reject;
+ });
+ }
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ t,
+ makeRequestURL(key, ['httpsOrigin'], params),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-origin']);
+ });
+ }, 'sec-fetch-site - Same origin no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ t,
+ makeRequestURL(key, ['httpsCrossSite'], params),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-site no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ t,
+ makeRequestURL(key, ['httpsSameSite'], params),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same site no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ t,
+ makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite', 'httpsOrigin'], params),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Cross-Site -> Same-Origin redirect no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ t,
+ makeRequestURL(key, ['httpsOrigin', 'httpsSameSite', 'httpsOrigin'], params),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Same-Site -> Same-Origin redirect no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ t,
+ makeRequestURL(key, ['httpsCrossSite', 'httpsOrigin'], params),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-Site -> Same Origin no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ t,
+ makeRequestURL(key, ['httpsCrossSite', 'httpsSameSite'], params),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-Site -> Same-Site no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ t,
+ makeRequestURL(key, ['httpsCrossSite', 'httpsCrossSite'], params),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-Site -> Cross-Site no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ t,
+ makeRequestURL(key, ['httpsOrigin', 'httpsOrigin'], params),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-origin']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Same Origin no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ t,
+ makeRequestURL(key, ['httpsOrigin', 'httpsSameSite'], params),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Same-Site no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ t,
+ makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite'], params),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Cross-Site no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ t,
+ makeRequestURL(key, ['httpsSameSite', 'httpsOrigin'], params),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Site -> Same Origin no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ t,
+ makeRequestURL(key, ['httpsSameSite', 'httpsSameSite'], params),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Site -> Same-Site no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ t,
+ makeRequestURL(key, ['httpsSameSite', 'httpsCrossSite'], params),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Same-Site -> Cross-Site no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ t,
+ makeRequestURL(key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin'], params),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - HTTPS downgrade-upgrade no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ t,
+ makeRequestURL(key, [], params),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-mode');
+ assert_array_equals(headers['sec-fetch-mode'], ['no-cors']);
+ });
+ }, 'sec-fetch-mode no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ t,
+ makeRequestURL(key, [], params),
+ {"crossorigin": ""}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-mode');
+ assert_array_equals(headers['sec-fetch-mode'], ['cors']);
+ });
+ }, 'sec-fetch-mode attributes: crossorigin');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ t,
+ makeRequestURL(key, [], params),
+ {"crossorigin": "anonymous"}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-mode');
+ assert_array_equals(headers['sec-fetch-mode'], ['cors']);
+ });
+ }, 'sec-fetch-mode attributes: crossorigin=anonymous');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ t,
+ makeRequestURL(key, [], params),
+ {"crossorigin": "use-credentials"}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-mode');
+ assert_array_equals(headers['sec-fetch-mode'], ['cors']);
+ });
+ }, 'sec-fetch-mode attributes: crossorigin=use-credentials');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ t,
+ makeRequestURL(key, [], params),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-dest');
+ assert_array_equals(headers['sec-fetch-dest'], ['empty']);
+ });
+ }, 'sec-fetch-dest no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ t,
+ makeRequestURL(key, [], params),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user no attributes');
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/fetch/metadata/generated/svg-image.sub.html b/test/wpt/tests/fetch/metadata/generated/svg-image.sub.html
new file mode 100644
index 0000000..a28bbb1
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/generated/svg-image.sub.html
@@ -0,0 +1,265 @@
+<!DOCTYPE html>
+<!--
+This test was procedurally generated. Please do not modify it directly.
+Sources:
+- fetch/metadata/tools/fetch-metadata.conf.yml
+- fetch/metadata/tools/templates/svg-image.sub.html
+-->
+<html lang="en">
+ <meta charset="utf-8">
+ <meta name="timeout" content="long">
+ <title>HTTP headers on request for SVG "image" element source</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/fetch/metadata/resources/helper.sub.js"></script>
+ <body>
+ <script>
+ 'use strict';
+
+ const params = {
+ body: `
+ <svg xmlns="http://www.w3.org/2000/svg" width="123" height="123">
+ <rect fill="lime" width="123" height="123"/>
+ </svg>
+ `,
+ mime: 'image/svg+xml'
+ };
+
+ function induceRequest(t, url, attributes) {
+ const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
+ svg.setAttributeNS(
+ "http://www.w3.org/2000/xmlns/",
+ "xmlns:xlink",
+ "http://www.w3.org/1999/xlink"
+ );
+ const image = document.createElementNS("http://www.w3.org/2000/svg", "image");
+ image.setAttribute("href", url);
+ svg.appendChild(image);
+
+ for (const [ name, value ] of Object.entries(attributes)) {
+ image.setAttribute(name, value);
+ }
+
+ document.body.appendChild(svg);
+ t.add_cleanup(() => svg.remove());
+
+ return new Promise((resolve, reject) => {
+ image.onload = resolve;
+ image.onerror = reject;
+ });
+ }
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ t,
+ makeRequestURL(key, ['httpOrigin'], params),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - Not sent to non-trustworthy same-origin destination no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ t,
+ makeRequestURL(key, ['httpSameSite'], params),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - Not sent to non-trustworthy same-site destination no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ t,
+ makeRequestURL(key, ['httpCrossSite'], params),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - Not sent to non-trustworthy cross-site destination no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ t,
+ makeRequestURL(key, ['httpOrigin'], params),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ });
+ }, 'sec-fetch-mode - Not sent to non-trustworthy same-origin destination no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ t,
+ makeRequestURL(key, ['httpSameSite'], params),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ });
+ }, 'sec-fetch-mode - Not sent to non-trustworthy same-site destination no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ t,
+ makeRequestURL(key, ['httpCrossSite'], params),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ });
+ }, 'sec-fetch-mode - Not sent to non-trustworthy cross-site destination no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ t,
+ makeRequestURL(key, ['httpOrigin'], params),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ });
+ }, 'sec-fetch-dest - Not sent to non-trustworthy same-origin destination no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ t,
+ makeRequestURL(key, ['httpSameSite'], params),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ });
+ }, 'sec-fetch-dest - Not sent to non-trustworthy same-site destination no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ t,
+ makeRequestURL(key, ['httpCrossSite'], params),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ });
+ }, 'sec-fetch-dest - Not sent to non-trustworthy cross-site destination no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ t,
+ makeRequestURL(key, ['httpOrigin'], params),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - Not sent to non-trustworthy same-origin destination no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ t,
+ makeRequestURL(key, ['httpSameSite'], params),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - Not sent to non-trustworthy same-site destination no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ t,
+ makeRequestURL(key, ['httpCrossSite'], params),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - Not sent to non-trustworthy cross-site destination no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ t,
+ makeRequestURL(key, ['httpsOrigin', 'httpOrigin'], params),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - HTTPS downgrade (header not sent) no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ t,
+ makeRequestURL(key, ['httpOrigin', 'httpsOrigin'], params),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - HTTPS upgrade no attributes');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ t,
+ makeRequestURL(key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin'], params),
+ {}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - HTTPS downgrade-upgrade no attributes');
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/fetch/metadata/generated/window-history.https.sub.html b/test/wpt/tests/fetch/metadata/generated/window-history.https.sub.html
new file mode 100644
index 0000000..c2b3079
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/generated/window-history.https.sub.html
@@ -0,0 +1,237 @@
+<!DOCTYPE html>
+<!--
+This test was procedurally generated. Please do not modify it directly.
+Sources:
+- fetch/metadata/tools/fetch-metadata.conf.yml
+- fetch/metadata/tools/templates/window-history.sub.html
+-->
+<html lang="en">
+ <meta charset="utf-8">
+ <title>HTTP headers on request for navigation via the HTML History API</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/fetch/metadata/resources/helper.sub.js"></script>
+ <body>
+ <script>
+ 'use strict';
+
+ const whenDone = (win) => {
+ return new Promise((resolve) => {
+ addEventListener('message', function handle(event) {
+ if (event.source === win) {
+ resolve();
+ removeEventListener('message', handle);
+ }
+ });
+ })
+ };
+
+ /**
+ * Prime the UA's session history such that the location of the request is
+ * immediately behind the current entry. Because the location may not be
+ * same-origin with the current browsing context, this must be done via a
+ * true navigation and not, e.g. the `history.pushState` API. The initial
+ * navigation will alter the WPT server's internal state; in order to avoid
+ * false positives, clear that state prior to initiating the second
+ * navigation via `history.back`.
+ */
+ function induceBackRequest(url, test, clear) {
+ const win = window.open(url);
+
+ test.add_cleanup(() => win.close());
+
+ return whenDone(win)
+ .then(clear)
+ .then(() => win.history.back())
+ .then(() => whenDone(win));
+ }
+
+ /**
+ * Prime the UA's session history such that the location of the request is
+ * immediately ahead of the current entry. Because the location may not be
+ * same-origin with the current browsing context, this must be done via a
+ * true navigation and not, e.g. the `history.pushState` API. The initial
+ * navigation will alter the WPT server's internal state; in order to avoid
+ * false positives, clear that state prior to initiating the second
+ * navigation via `history.forward`.
+ */
+ function induceForwardRequest(url, test, clear) {
+ const win = window.open(messageOpenerUrl);
+
+ test.add_cleanup(() => win.close());
+
+ return whenDone(win)
+ .then(() => win.location = url)
+ .then(() => whenDone(win))
+ .then(clear)
+ .then(() => win.history.go(-2))
+ .then(() => whenDone(win))
+ .then(() => win.history.forward())
+ .then(() => whenDone(win));
+ }
+
+ const messageOpenerUrl = new URL(
+ '/fetch/metadata/resources/message-opener.html', location
+ );
+ // For these tests to function, replacement must *not* be enabled during
+ // navigation. Assignment must therefore take place after the document has
+ // completely loaded [1]. This event is not directly observable, but it is
+ // scheduled as a task immediately following the global object's `load`
+ // event [2]. By queuing a task during the dispatch of the `load` event,
+ // navigation can be consistently triggered without replacement.
+ //
+ // [1] https://html.spec.whatwg.org/multipage/history.html#location-object-setter-navigate
+ // [2] https://html.spec.whatwg.org/multipage/parsing.html#the-end
+ const responseParams = {
+ mime: 'text/html',
+ body: `<script>
+ window.addEventListener('load', () => {
+ set`+`Timeout(() => location.assign('${messageOpenerUrl}'));
+ });
+ <`+`/script>`
+ };
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsOrigin'], responseParams);
+
+ return induceBackRequest(url, t, () => retrieve(key))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-origin']);
+ });
+ }, 'sec-fetch-site - Same origin - history.back');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsOrigin'], responseParams);
+
+ return induceForwardRequest(url, t, () => retrieve(key))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-origin']);
+ });
+ }, 'sec-fetch-site - Same origin - history.forward');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsCrossSite'], responseParams);
+
+ return induceBackRequest(url, t, () => retrieve(key))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-site - history.back');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsCrossSite'], responseParams);
+
+ return induceForwardRequest(url, t, () => retrieve(key))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-site - history.forward');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsSameSite'], responseParams);
+
+ return induceBackRequest(url, t, () => retrieve(key))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same site - history.back');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsSameSite'], responseParams);
+
+ return induceForwardRequest(url, t, () => retrieve(key))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same site - history.forward');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, [], responseParams);
+
+ return induceBackRequest(url, t, () => retrieve(key))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-mode');
+ assert_array_equals(headers['sec-fetch-mode'], ['navigate']);
+ });
+ }, 'sec-fetch-mode - history.back');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, [], responseParams);
+
+ return induceForwardRequest(url, t, () => retrieve(key))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-mode');
+ assert_array_equals(headers['sec-fetch-mode'], ['navigate']);
+ });
+ }, 'sec-fetch-mode - history.forward');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, [], responseParams);
+
+ return induceBackRequest(url, t, () => retrieve(key))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-dest');
+ assert_array_equals(headers['sec-fetch-dest'], ['document']);
+ });
+ }, 'sec-fetch-dest - history.back');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, [], responseParams);
+
+ return induceForwardRequest(url, t, () => retrieve(key))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-dest');
+ assert_array_equals(headers['sec-fetch-dest'], ['document']);
+ });
+ }, 'sec-fetch-dest - history.forward');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, [], responseParams);
+
+ return induceBackRequest(url, t, () => retrieve(key))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - history.back');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, [], responseParams);
+
+ return induceForwardRequest(url, t, () => retrieve(key))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - history.forward');
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/fetch/metadata/generated/window-history.sub.html b/test/wpt/tests/fetch/metadata/generated/window-history.sub.html
new file mode 100644
index 0000000..333d90c
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/generated/window-history.sub.html
@@ -0,0 +1,360 @@
+<!DOCTYPE html>
+<!--
+This test was procedurally generated. Please do not modify it directly.
+Sources:
+- fetch/metadata/tools/fetch-metadata.conf.yml
+- fetch/metadata/tools/templates/window-history.sub.html
+-->
+<html lang="en">
+ <meta charset="utf-8">
+ <meta name="timeout" content="long">
+ <title>HTTP headers on request for navigation via the HTML History API</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/fetch/metadata/resources/helper.sub.js"></script>
+ <body>
+ <script>
+ 'use strict';
+
+ const whenDone = (win) => {
+ return new Promise((resolve) => {
+ addEventListener('message', function handle(event) {
+ if (event.source === win) {
+ resolve();
+ removeEventListener('message', handle);
+ }
+ });
+ })
+ };
+
+ /**
+ * Prime the UA's session history such that the location of the request is
+ * immediately behind the current entry. Because the location may not be
+ * same-origin with the current browsing context, this must be done via a
+ * true navigation and not, e.g. the `history.pushState` API. The initial
+ * navigation will alter the WPT server's internal state; in order to avoid
+ * false positives, clear that state prior to initiating the second
+ * navigation via `history.back`.
+ */
+ function induceBackRequest(url, test, clear) {
+ const win = window.open(url);
+
+ test.add_cleanup(() => win.close());
+
+ return whenDone(win)
+ .then(clear)
+ .then(() => win.history.back())
+ .then(() => whenDone(win));
+ }
+
+ /**
+ * Prime the UA's session history such that the location of the request is
+ * immediately ahead of the current entry. Because the location may not be
+ * same-origin with the current browsing context, this must be done via a
+ * true navigation and not, e.g. the `history.pushState` API. The initial
+ * navigation will alter the WPT server's internal state; in order to avoid
+ * false positives, clear that state prior to initiating the second
+ * navigation via `history.forward`.
+ */
+ function induceForwardRequest(url, test, clear) {
+ const win = window.open(messageOpenerUrl);
+
+ test.add_cleanup(() => win.close());
+
+ return whenDone(win)
+ .then(() => win.location = url)
+ .then(() => whenDone(win))
+ .then(clear)
+ .then(() => win.history.go(-2))
+ .then(() => whenDone(win))
+ .then(() => win.history.forward())
+ .then(() => whenDone(win));
+ }
+
+ const messageOpenerUrl = new URL(
+ '/fetch/metadata/resources/message-opener.html', location
+ );
+ // For these tests to function, replacement must *not* be enabled during
+ // navigation. Assignment must therefore take place after the document has
+ // completely loaded [1]. This event is not directly observable, but it is
+ // scheduled as a task immediately following the global object's `load`
+ // event [2]. By queuing a task during the dispatch of the `load` event,
+ // navigation can be consistently triggered without replacement.
+ //
+ // [1] https://html.spec.whatwg.org/multipage/history.html#location-object-setter-navigate
+ // [2] https://html.spec.whatwg.org/multipage/parsing.html#the-end
+ const responseParams = {
+ mime: 'text/html',
+ body: `<script>
+ window.addEventListener('load', () => {
+ set`+`Timeout(() => location.assign('${messageOpenerUrl}'));
+ });
+ <`+`/script>`
+ };
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpOrigin'], responseParams);
+
+ return induceBackRequest(url, t, () => retrieve(key))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - Not sent to non-trustworthy same-origin destination - history.back');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpOrigin'], responseParams);
+
+ return induceForwardRequest(url, t, () => retrieve(key))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - Not sent to non-trustworthy same-origin destination - history.forward');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpSameSite'], responseParams);
+
+ return induceBackRequest(url, t, () => retrieve(key))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - Not sent to non-trustworthy same-site destination - history.back');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpSameSite'], responseParams);
+
+ return induceForwardRequest(url, t, () => retrieve(key))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - Not sent to non-trustworthy same-site destination - history.forward');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpCrossSite'], responseParams);
+
+ return induceBackRequest(url, t, () => retrieve(key))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - Not sent to non-trustworthy cross-site destination - history.back');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpCrossSite'], responseParams);
+
+ return induceForwardRequest(url, t, () => retrieve(key))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - Not sent to non-trustworthy cross-site destination - history.forward');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpOrigin'], responseParams);
+
+ return induceBackRequest(url, t, () => retrieve(key))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ });
+ }, 'sec-fetch-mode - Not sent to non-trustworthy same-origin destination - history.back');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpOrigin'], responseParams);
+
+ return induceForwardRequest(url, t, () => retrieve(key))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ });
+ }, 'sec-fetch-mode - Not sent to non-trustworthy same-origin destination - history.forward');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpSameSite'], responseParams);
+
+ return induceBackRequest(url, t, () => retrieve(key))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ });
+ }, 'sec-fetch-mode - Not sent to non-trustworthy same-site destination - history.back');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpSameSite'], responseParams);
+
+ return induceForwardRequest(url, t, () => retrieve(key))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ });
+ }, 'sec-fetch-mode - Not sent to non-trustworthy same-site destination - history.forward');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpCrossSite'], responseParams);
+
+ return induceBackRequest(url, t, () => retrieve(key))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ });
+ }, 'sec-fetch-mode - Not sent to non-trustworthy cross-site destination - history.back');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpCrossSite'], responseParams);
+
+ return induceForwardRequest(url, t, () => retrieve(key))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ });
+ }, 'sec-fetch-mode - Not sent to non-trustworthy cross-site destination - history.forward');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpOrigin'], responseParams);
+
+ return induceBackRequest(url, t, () => retrieve(key))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ });
+ }, 'sec-fetch-dest - Not sent to non-trustworthy same-origin destination - history.back');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpOrigin'], responseParams);
+
+ return induceForwardRequest(url, t, () => retrieve(key))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ });
+ }, 'sec-fetch-dest - Not sent to non-trustworthy same-origin destination - history.forward');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpSameSite'], responseParams);
+
+ return induceBackRequest(url, t, () => retrieve(key))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ });
+ }, 'sec-fetch-dest - Not sent to non-trustworthy same-site destination - history.back');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpSameSite'], responseParams);
+
+ return induceForwardRequest(url, t, () => retrieve(key))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ });
+ }, 'sec-fetch-dest - Not sent to non-trustworthy same-site destination - history.forward');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpCrossSite'], responseParams);
+
+ return induceBackRequest(url, t, () => retrieve(key))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ });
+ }, 'sec-fetch-dest - Not sent to non-trustworthy cross-site destination - history.back');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpCrossSite'], responseParams);
+
+ return induceForwardRequest(url, t, () => retrieve(key))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ });
+ }, 'sec-fetch-dest - Not sent to non-trustworthy cross-site destination - history.forward');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpOrigin'], responseParams);
+
+ return induceBackRequest(url, t, () => retrieve(key))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - Not sent to non-trustworthy same-origin destination - history.back');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpOrigin'], responseParams);
+
+ return induceForwardRequest(url, t, () => retrieve(key))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - Not sent to non-trustworthy same-origin destination - history.forward');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpSameSite'], responseParams);
+
+ return induceBackRequest(url, t, () => retrieve(key))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - Not sent to non-trustworthy same-site destination - history.back');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpSameSite'], responseParams);
+
+ return induceForwardRequest(url, t, () => retrieve(key))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - Not sent to non-trustworthy same-site destination - history.forward');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpCrossSite'], responseParams);
+
+ return induceBackRequest(url, t, () => retrieve(key))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - Not sent to non-trustworthy cross-site destination - history.back');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpCrossSite'], responseParams);
+
+ return induceForwardRequest(url, t, () => retrieve(key))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - Not sent to non-trustworthy cross-site destination - history.forward');
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/fetch/metadata/generated/window-location.https.sub.html b/test/wpt/tests/fetch/metadata/generated/window-location.https.sub.html
new file mode 100644
index 0000000..4a0d2fd
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/generated/window-location.https.sub.html
@@ -0,0 +1,1184 @@
+<!DOCTYPE html>
+<!--
+This test was procedurally generated. Please do not modify it directly.
+Sources:
+- fetch/metadata/tools/fetch-metadata.conf.yml
+- fetch/metadata/tools/templates/window-location.sub.html
+-->
+<html lang="en">
+ <meta charset="utf-8">
+ <meta name="timeout" content="long">
+ <title>HTTP headers on request for navigation via the HTML Location API</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/resources/testdriver.js"></script>
+ <script src="/resources/testdriver-vendor.js"></script>
+ <script src="/fetch/metadata/resources/helper.sub.js"></script>
+ <body>
+ <script>
+ 'use strict';
+
+ function induceRequest(url, navigate, userActivated) {
+ const win = window.open();
+
+ return new Promise((resolve) => {
+ addEventListener('message', function(event) {
+ if (event.source === win) {
+ resolve();
+ }
+ });
+
+ if (userActivated) {
+ test_driver.bless('enable user activation', () => {
+ navigate(win, url);
+ });
+ } else {
+ navigate(win, url);
+ }
+ })
+ .then(() => win.close());
+ }
+
+ const responseParams = {
+ mime: 'text/html',
+ body: `<script>opener.postMessage('done', '*')</${''}script>`
+ };
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsOrigin'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location = path;
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-origin']);
+ });
+ }, 'sec-fetch-site - Same origin - location');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsOrigin'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location.href = path;
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-origin']);
+ });
+ }, 'sec-fetch-site - Same origin - location.href');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsOrigin'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location.assign(path);
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-origin']);
+ });
+ }, 'sec-fetch-site - Same origin - location.assign');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsOrigin'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location.replace(path);
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-origin']);
+ });
+ }, 'sec-fetch-site - Same origin - location.replace');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsCrossSite'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location = path;
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-site - location');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsCrossSite'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location.href = path;
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-site - location.href');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsCrossSite'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location.assign(path);
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-site - location.assign');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsCrossSite'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location.replace(path);
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-site - location.replace');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsSameSite'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location = path;
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same site - location');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsSameSite'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location.href = path;
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same site - location.href');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsSameSite'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location.assign(path);
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same site - location.assign');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsSameSite'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location.replace(path);
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same site - location.replace');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite', 'httpsOrigin'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location = path;
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Cross-Site -> Same-Origin redirect - location');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite', 'httpsOrigin'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location.href = path;
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Cross-Site -> Same-Origin redirect - location.href');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite', 'httpsOrigin'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location.assign(path);
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Cross-Site -> Same-Origin redirect - location.assign');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite', 'httpsOrigin'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location.replace(path);
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Cross-Site -> Same-Origin redirect - location.replace');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsOrigin', 'httpsSameSite', 'httpsOrigin'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location = path;
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Same-Site -> Same-Origin redirect - location');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsOrigin', 'httpsSameSite', 'httpsOrigin'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location.href = path;
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Same-Site -> Same-Origin redirect - location.href');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsOrigin', 'httpsSameSite', 'httpsOrigin'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location.assign(path);
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Same-Site -> Same-Origin redirect - location.assign');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsOrigin', 'httpsSameSite', 'httpsOrigin'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location.replace(path);
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Same-Site -> Same-Origin redirect - location.replace');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsCrossSite', 'httpsOrigin'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location = path;
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-Site -> Same Origin - location');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsCrossSite', 'httpsOrigin'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location.href = path;
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-Site -> Same Origin - location.href');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsCrossSite', 'httpsOrigin'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location.assign(path);
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-Site -> Same Origin - location.assign');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsCrossSite', 'httpsOrigin'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location.replace(path);
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-Site -> Same Origin - location.replace');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsCrossSite', 'httpsSameSite'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location = path;
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-Site -> Same-Site - location');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsCrossSite', 'httpsSameSite'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location.href = path;
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-Site -> Same-Site - location.href');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsCrossSite', 'httpsSameSite'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location.assign(path);
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-Site -> Same-Site - location.assign');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsCrossSite', 'httpsSameSite'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location.replace(path);
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-Site -> Same-Site - location.replace');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsCrossSite', 'httpsCrossSite'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location = path;
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-Site -> Cross-Site - location');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsCrossSite', 'httpsCrossSite'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location.href = path;
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-Site -> Cross-Site - location.href');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsCrossSite', 'httpsCrossSite'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location.assign(path);
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-Site -> Cross-Site - location.assign');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsCrossSite', 'httpsCrossSite'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location.replace(path);
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-Site -> Cross-Site - location.replace');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsOrigin', 'httpsOrigin'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location = path;
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-origin']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Same Origin - location');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsOrigin', 'httpsOrigin'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location.href = path;
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-origin']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Same Origin - location.href');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsOrigin', 'httpsOrigin'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location.assign(path);
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-origin']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Same Origin - location.assign');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsOrigin', 'httpsOrigin'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location.replace(path);
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-origin']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Same Origin - location.replace');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsOrigin', 'httpsSameSite'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location = path;
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Same-Site - location');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsOrigin', 'httpsSameSite'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location.href = path;
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Same-Site - location.href');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsOrigin', 'httpsSameSite'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location.assign(path);
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Same-Site - location.assign');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsOrigin', 'httpsSameSite'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location.replace(path);
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Same-Site - location.replace');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location = path;
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Cross-Site - location');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location.href = path;
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Cross-Site - location.href');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location.assign(path);
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Cross-Site - location.assign');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location.replace(path);
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Cross-Site - location.replace');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsSameSite', 'httpsOrigin'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location = path;
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Site -> Same Origin - location');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsSameSite', 'httpsOrigin'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location.href = path;
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Site -> Same Origin - location.href');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsSameSite', 'httpsOrigin'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location.assign(path);
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Site -> Same Origin - location.assign');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsSameSite', 'httpsOrigin'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location.replace(path);
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Site -> Same Origin - location.replace');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsSameSite', 'httpsSameSite'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location = path;
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Site -> Same-Site - location');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsSameSite', 'httpsSameSite'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location.href = path;
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Site -> Same-Site - location.href');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsSameSite', 'httpsSameSite'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location.assign(path);
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Site -> Same-Site - location.assign');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsSameSite', 'httpsSameSite'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location.replace(path);
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Site -> Same-Site - location.replace');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsSameSite', 'httpsCrossSite'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location = path;
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Same-Site -> Cross-Site - location');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsSameSite', 'httpsCrossSite'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location.href = path;
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Same-Site -> Cross-Site - location.href');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsSameSite', 'httpsCrossSite'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location.assign(path);
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Same-Site -> Cross-Site - location.assign');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsSameSite', 'httpsCrossSite'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location.replace(path);
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Same-Site -> Cross-Site - location.replace');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location = path;
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - HTTPS downgrade-upgrade - location');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location.href = path;
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - HTTPS downgrade-upgrade - location.href');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location.assign(path);
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - HTTPS downgrade-upgrade - location.assign');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location.replace(path);
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - HTTPS downgrade-upgrade - location.replace');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, [], responseParams);
+
+ const navigate = (win, path) => {
+ win.location = path;
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-mode');
+ assert_array_equals(headers['sec-fetch-mode'], ['navigate']);
+ });
+ }, 'sec-fetch-mode - location');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, [], responseParams);
+
+ const navigate = (win, path) => {
+ win.location.href = path;
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-mode');
+ assert_array_equals(headers['sec-fetch-mode'], ['navigate']);
+ });
+ }, 'sec-fetch-mode - location.href');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, [], responseParams);
+
+ const navigate = (win, path) => {
+ win.location.assign(path);
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-mode');
+ assert_array_equals(headers['sec-fetch-mode'], ['navigate']);
+ });
+ }, 'sec-fetch-mode - location.assign');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, [], responseParams);
+
+ const navigate = (win, path) => {
+ win.location.replace(path);
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-mode');
+ assert_array_equals(headers['sec-fetch-mode'], ['navigate']);
+ });
+ }, 'sec-fetch-mode - location.replace');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, [], responseParams);
+
+ const navigate = (win, path) => {
+ win.location = path;
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-dest');
+ assert_array_equals(headers['sec-fetch-dest'], ['document']);
+ });
+ }, 'sec-fetch-dest - location');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, [], responseParams);
+
+ const navigate = (win, path) => {
+ win.location.href = path;
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-dest');
+ assert_array_equals(headers['sec-fetch-dest'], ['document']);
+ });
+ }, 'sec-fetch-dest - location.href');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, [], responseParams);
+
+ const navigate = (win, path) => {
+ win.location.assign(path);
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-dest');
+ assert_array_equals(headers['sec-fetch-dest'], ['document']);
+ });
+ }, 'sec-fetch-dest - location.assign');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, [], responseParams);
+
+ const navigate = (win, path) => {
+ win.location.replace(path);
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-dest');
+ assert_array_equals(headers['sec-fetch-dest'], ['document']);
+ });
+ }, 'sec-fetch-dest - location.replace');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, [], responseParams);
+
+ const navigate = (win, path) => {
+ win.location = path;
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - location');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, [], responseParams);
+
+ const navigate = (win, path) => {
+ win.location.href = path;
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - location.href');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, [], responseParams);
+
+ const navigate = (win, path) => {
+ win.location.assign(path);
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - location.assign');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, [], responseParams);
+
+ const navigate = (win, path) => {
+ win.location.replace(path);
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - location.replace');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, [], responseParams);
+
+ const navigate = (win, path) => {
+ win.location = path;
+ };
+ return induceRequest(url, navigate, true)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-user');
+ assert_array_equals(headers['sec-fetch-user'], ['?1']);
+ });
+ }, 'sec-fetch-user - location with user activation');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, [], responseParams);
+
+ const navigate = (win, path) => {
+ win.location.href = path;
+ };
+ return induceRequest(url, navigate, true)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-user');
+ assert_array_equals(headers['sec-fetch-user'], ['?1']);
+ });
+ }, 'sec-fetch-user - location.href with user activation');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, [], responseParams);
+
+ const navigate = (win, path) => {
+ win.location.assign(path);
+ };
+ return induceRequest(url, navigate, true)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-user');
+ assert_array_equals(headers['sec-fetch-user'], ['?1']);
+ });
+ }, 'sec-fetch-user - location.assign with user activation');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, [], responseParams);
+
+ const navigate = (win, path) => {
+ win.location.replace(path);
+ };
+ return induceRequest(url, navigate, true)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-user');
+ assert_array_equals(headers['sec-fetch-user'], ['?1']);
+ });
+ }, 'sec-fetch-user - location.replace with user activation');
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/fetch/metadata/generated/window-location.sub.html b/test/wpt/tests/fetch/metadata/generated/window-location.sub.html
new file mode 100644
index 0000000..bb3e680
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/generated/window-location.sub.html
@@ -0,0 +1,894 @@
+<!DOCTYPE html>
+<!--
+This test was procedurally generated. Please do not modify it directly.
+Sources:
+- fetch/metadata/tools/fetch-metadata.conf.yml
+- fetch/metadata/tools/templates/window-location.sub.html
+-->
+<html lang="en">
+ <meta charset="utf-8">
+ <meta name="timeout" content="long">
+ <title>HTTP headers on request for navigation via the HTML Location API</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/fetch/metadata/resources/helper.sub.js"></script>
+ <body>
+ <script>
+ 'use strict';
+
+ function induceRequest(url, navigate, userActivated) {
+ const win = window.open();
+
+ return new Promise((resolve) => {
+ addEventListener('message', function(event) {
+ if (event.source === win) {
+ resolve();
+ }
+ });
+
+ if (userActivated) {
+ test_driver.bless('enable user activation', () => {
+ navigate(win, url);
+ });
+ } else {
+ navigate(win, url);
+ }
+ })
+ .then(() => win.close());
+ }
+
+ const responseParams = {
+ mime: 'text/html',
+ body: `<script>opener.postMessage('done', '*')</${''}script>`
+ };
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpOrigin'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location = path;
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - Not sent to non-trustworthy same-origin destination - location');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpOrigin'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location.href = path;
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - Not sent to non-trustworthy same-origin destination - location.href');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpOrigin'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location.assign(path);
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - Not sent to non-trustworthy same-origin destination - location.assign');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpOrigin'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location.replace(path);
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - Not sent to non-trustworthy same-origin destination - location.replace');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpSameSite'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location = path;
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - Not sent to non-trustworthy same-site destination - location');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpSameSite'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location.href = path;
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - Not sent to non-trustworthy same-site destination - location.href');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpSameSite'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location.assign(path);
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - Not sent to non-trustworthy same-site destination - location.assign');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpSameSite'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location.replace(path);
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - Not sent to non-trustworthy same-site destination - location.replace');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpCrossSite'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location = path;
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - Not sent to non-trustworthy cross-site destination - location');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpCrossSite'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location.href = path;
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - Not sent to non-trustworthy cross-site destination - location.href');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpCrossSite'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location.assign(path);
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - Not sent to non-trustworthy cross-site destination - location.assign');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpCrossSite'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location.replace(path);
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - Not sent to non-trustworthy cross-site destination - location.replace');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpOrigin'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location = path;
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ });
+ }, 'sec-fetch-mode - Not sent to non-trustworthy same-origin destination - location');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpOrigin'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location.href = path;
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ });
+ }, 'sec-fetch-mode - Not sent to non-trustworthy same-origin destination - location.href');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpOrigin'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location.assign(path);
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ });
+ }, 'sec-fetch-mode - Not sent to non-trustworthy same-origin destination - location.assign');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpOrigin'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location.replace(path);
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ });
+ }, 'sec-fetch-mode - Not sent to non-trustworthy same-origin destination - location.replace');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpSameSite'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location = path;
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ });
+ }, 'sec-fetch-mode - Not sent to non-trustworthy same-site destination - location');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpSameSite'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location.href = path;
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ });
+ }, 'sec-fetch-mode - Not sent to non-trustworthy same-site destination - location.href');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpSameSite'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location.assign(path);
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ });
+ }, 'sec-fetch-mode - Not sent to non-trustworthy same-site destination - location.assign');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpSameSite'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location.replace(path);
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ });
+ }, 'sec-fetch-mode - Not sent to non-trustworthy same-site destination - location.replace');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpCrossSite'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location = path;
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ });
+ }, 'sec-fetch-mode - Not sent to non-trustworthy cross-site destination - location');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpCrossSite'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location.href = path;
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ });
+ }, 'sec-fetch-mode - Not sent to non-trustworthy cross-site destination - location.href');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpCrossSite'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location.assign(path);
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ });
+ }, 'sec-fetch-mode - Not sent to non-trustworthy cross-site destination - location.assign');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpCrossSite'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location.replace(path);
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ });
+ }, 'sec-fetch-mode - Not sent to non-trustworthy cross-site destination - location.replace');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpOrigin'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location = path;
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ });
+ }, 'sec-fetch-dest - Not sent to non-trustworthy same-origin destination - location');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpOrigin'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location.href = path;
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ });
+ }, 'sec-fetch-dest - Not sent to non-trustworthy same-origin destination - location.href');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpOrigin'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location.assign(path);
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ });
+ }, 'sec-fetch-dest - Not sent to non-trustworthy same-origin destination - location.assign');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpOrigin'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location.replace(path);
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ });
+ }, 'sec-fetch-dest - Not sent to non-trustworthy same-origin destination - location.replace');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpSameSite'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location = path;
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ });
+ }, 'sec-fetch-dest - Not sent to non-trustworthy same-site destination - location');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpSameSite'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location.href = path;
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ });
+ }, 'sec-fetch-dest - Not sent to non-trustworthy same-site destination - location.href');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpSameSite'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location.assign(path);
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ });
+ }, 'sec-fetch-dest - Not sent to non-trustworthy same-site destination - location.assign');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpSameSite'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location.replace(path);
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ });
+ }, 'sec-fetch-dest - Not sent to non-trustworthy same-site destination - location.replace');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpCrossSite'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location = path;
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ });
+ }, 'sec-fetch-dest - Not sent to non-trustworthy cross-site destination - location');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpCrossSite'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location.href = path;
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ });
+ }, 'sec-fetch-dest - Not sent to non-trustworthy cross-site destination - location.href');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpCrossSite'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location.assign(path);
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ });
+ }, 'sec-fetch-dest - Not sent to non-trustworthy cross-site destination - location.assign');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpCrossSite'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location.replace(path);
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ });
+ }, 'sec-fetch-dest - Not sent to non-trustworthy cross-site destination - location.replace');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpOrigin'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location = path;
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - Not sent to non-trustworthy same-origin destination - location');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpOrigin'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location.href = path;
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - Not sent to non-trustworthy same-origin destination - location.href');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpOrigin'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location.assign(path);
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - Not sent to non-trustworthy same-origin destination - location.assign');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpOrigin'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location.replace(path);
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - Not sent to non-trustworthy same-origin destination - location.replace');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpSameSite'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location = path;
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - Not sent to non-trustworthy same-site destination - location');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpSameSite'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location.href = path;
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - Not sent to non-trustworthy same-site destination - location.href');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpSameSite'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location.assign(path);
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - Not sent to non-trustworthy same-site destination - location.assign');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpSameSite'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location.replace(path);
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - Not sent to non-trustworthy same-site destination - location.replace');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpCrossSite'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location = path;
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - Not sent to non-trustworthy cross-site destination - location');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpCrossSite'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location.href = path;
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - Not sent to non-trustworthy cross-site destination - location.href');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpCrossSite'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location.assign(path);
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - Not sent to non-trustworthy cross-site destination - location.assign');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpCrossSite'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location.replace(path);
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - Not sent to non-trustworthy cross-site destination - location.replace');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsOrigin', 'httpOrigin'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location = path;
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - HTTPS downgrade (header not sent) - location');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsOrigin', 'httpOrigin'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location.href = path;
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - HTTPS downgrade (header not sent) - location.href');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsOrigin', 'httpOrigin'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location.assign(path);
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - HTTPS downgrade (header not sent) - location.assign');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsOrigin', 'httpOrigin'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location.replace(path);
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - HTTPS downgrade (header not sent) - location.replace');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpOrigin', 'httpsOrigin'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location = path;
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - HTTPS upgrade - location');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpOrigin', 'httpsOrigin'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location.href = path;
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - HTTPS upgrade - location.href');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpOrigin', 'httpsOrigin'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location.assign(path);
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - HTTPS upgrade - location.assign');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpOrigin', 'httpsOrigin'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location.replace(path);
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - HTTPS upgrade - location.replace');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location = path;
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - HTTPS downgrade-upgrade - location');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location.href = path;
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - HTTPS downgrade-upgrade - location.href');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location.assign(path);
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - HTTPS downgrade-upgrade - location.assign');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin'], responseParams);
+
+ const navigate = (win, path) => {
+ win.location.replace(path);
+ };
+ return induceRequest(url, navigate, false)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - HTTPS downgrade-upgrade - location.replace');
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/fetch/metadata/generated/worker-dedicated-constructor.https.sub.html b/test/wpt/tests/fetch/metadata/generated/worker-dedicated-constructor.https.sub.html
new file mode 100644
index 0000000..86f1760
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/generated/worker-dedicated-constructor.https.sub.html
@@ -0,0 +1,118 @@
+<!DOCTYPE html>
+<!--
+This test was procedurally generated. Please do not modify it directly.
+Sources:
+- fetch/metadata/tools/fetch-metadata.conf.yml
+- fetch/metadata/tools/templates/worker-dedicated-constructor.sub.html
+-->
+<html lang="en">
+ <meta charset="utf-8">
+ <title>HTTP headers on request for dedicated worker via the "Worker" constructor</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/fetch/metadata/resources/helper.sub.js"></script>
+ <script type="module">
+ 'use strict';
+ function induceRequest(url, options) {
+ return new Promise((resolve, reject) => {
+ const worker = new Worker(url, options);
+ worker.onmessage = resolve;
+ worker.onerror = reject;
+ });
+ }
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key,
+ [],
+ { mime: 'application/javascript', body: 'postMessage("")' }
+ );
+
+ return induceRequest(url)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-mode');
+ assert_array_equals(headers['sec-fetch-mode'], ['same-origin']);
+ });
+ }, 'sec-fetch-mode - no options');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key,
+ [],
+ { mime: 'application/javascript', body: 'postMessage("")' }
+ );
+
+ return induceRequest(url, {"type": "module"})
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-mode');
+ assert_array_equals(headers['sec-fetch-mode'], ['same-origin']);
+ });
+ }, 'sec-fetch-mode - options: type=module');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key,
+ [],
+ { mime: 'application/javascript', body: 'postMessage("")' }
+ );
+
+ return induceRequest(url)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-dest');
+ assert_array_equals(headers['sec-fetch-dest'], ['worker']);
+ });
+ }, 'sec-fetch-dest - no options');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key,
+ [],
+ { mime: 'application/javascript', body: 'postMessage("")' }
+ );
+
+ return induceRequest(url, {"type": "module"})
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-dest');
+ assert_array_equals(headers['sec-fetch-dest'], ['worker']);
+ });
+ }, 'sec-fetch-dest - options: type=module');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key,
+ [],
+ { mime: 'application/javascript', body: 'postMessage("")' }
+ );
+
+ return induceRequest(url)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - no options');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key,
+ [],
+ { mime: 'application/javascript', body: 'postMessage("")' }
+ );
+
+ return induceRequest(url, {"type": "module"})
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - options: type=module');
+ </script>
+</html>
diff --git a/test/wpt/tests/fetch/metadata/generated/worker-dedicated-constructor.sub.html b/test/wpt/tests/fetch/metadata/generated/worker-dedicated-constructor.sub.html
new file mode 100644
index 0000000..69ac768
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/generated/worker-dedicated-constructor.sub.html
@@ -0,0 +1,204 @@
+<!DOCTYPE html>
+<!--
+This test was procedurally generated. Please do not modify it directly.
+Sources:
+- fetch/metadata/tools/fetch-metadata.conf.yml
+- fetch/metadata/tools/templates/worker-dedicated-constructor.sub.html
+-->
+<html lang="en">
+ <meta charset="utf-8">
+ <title>HTTP headers on request for dedicated worker via the "Worker" constructor</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/fetch/metadata/resources/helper.sub.js"></script>
+ <script type="module">
+ 'use strict';
+ function induceRequest(url, options) {
+ return new Promise((resolve, reject) => {
+ const worker = new Worker(url, options);
+ worker.onmessage = resolve;
+ worker.onerror = reject;
+ });
+ }
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key,
+ ['httpOrigin'],
+ { mime: 'application/javascript', body: 'postMessage("")' }
+ );
+
+ return induceRequest(url)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - Not sent to non-trustworthy same-origin destination, no options');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key,
+ ['httpSameSite'],
+ { mime: 'application/javascript', body: 'postMessage("")' }
+ );
+
+ return induceRequest(url)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - Not sent to non-trustworthy same-site destination, no options');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key,
+ ['httpCrossSite'],
+ { mime: 'application/javascript', body: 'postMessage("")' }
+ );
+
+ return induceRequest(url)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - Not sent to non-trustworthy cross-site destination, no options');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key,
+ ['httpOrigin'],
+ { mime: 'application/javascript', body: 'postMessage("")' }
+ );
+
+ return induceRequest(url)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ });
+ }, 'sec-fetch-mode - Not sent to non-trustworthy same-origin destination, no options');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key,
+ ['httpSameSite'],
+ { mime: 'application/javascript', body: 'postMessage("")' }
+ );
+
+ return induceRequest(url)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ });
+ }, 'sec-fetch-mode - Not sent to non-trustworthy same-site destination, no options');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key,
+ ['httpCrossSite'],
+ { mime: 'application/javascript', body: 'postMessage("")' }
+ );
+
+ return induceRequest(url)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ });
+ }, 'sec-fetch-mode - Not sent to non-trustworthy cross-site destination, no options');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key,
+ ['httpOrigin'],
+ { mime: 'application/javascript', body: 'postMessage("")' }
+ );
+
+ return induceRequest(url)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ });
+ }, 'sec-fetch-dest - Not sent to non-trustworthy same-origin destination, no options');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key,
+ ['httpSameSite'],
+ { mime: 'application/javascript', body: 'postMessage("")' }
+ );
+
+ return induceRequest(url)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ });
+ }, 'sec-fetch-dest - Not sent to non-trustworthy same-site destination, no options');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key,
+ ['httpCrossSite'],
+ { mime: 'application/javascript', body: 'postMessage("")' }
+ );
+
+ return induceRequest(url)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ });
+ }, 'sec-fetch-dest - Not sent to non-trustworthy cross-site destination, no options');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key,
+ ['httpOrigin'],
+ { mime: 'application/javascript', body: 'postMessage("")' }
+ );
+
+ return induceRequest(url)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - Not sent to non-trustworthy same-origin destination, no options');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key,
+ ['httpSameSite'],
+ { mime: 'application/javascript', body: 'postMessage("")' }
+ );
+
+ return induceRequest(url)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - Not sent to non-trustworthy same-site destination, no options');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key,
+ ['httpCrossSite'],
+ { mime: 'application/javascript', body: 'postMessage("")' }
+ );
+
+ return induceRequest(url)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - Not sent to non-trustworthy cross-site destination, no options');
+ </script>
+</html>
diff --git a/test/wpt/tests/fetch/metadata/generated/worker-dedicated-importscripts.https.sub.html b/test/wpt/tests/fetch/metadata/generated/worker-dedicated-importscripts.https.sub.html
new file mode 100644
index 0000000..0cd9f35
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/generated/worker-dedicated-importscripts.https.sub.html
@@ -0,0 +1,268 @@
+<!DOCTYPE html>
+<!--
+This test was procedurally generated. Please do not modify it directly.
+Sources:
+- fetch/metadata/tools/fetch-metadata.conf.yml
+- fetch/metadata/tools/templates/worker-dedicated-importscripts.sub.html
+-->
+<html lang="en">
+ <meta charset="utf-8">
+ <title>HTTP headers on request for dedicated worker via the "importScripts" API</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/fetch/metadata/resources/helper.sub.js"></script>
+ <script type="module">
+ 'use strict';
+ function induceRequest(url, options) {
+ const src = `
+ importScripts('${url}');
+ postMessage('done');
+ `;
+ const workerUrl = URL.createObjectURL(
+ new Blob([src], { type: 'application/javascript' })
+ );
+ return new Promise((resolve, reject) => {
+ const worker = new Worker(workerUrl, options);
+ worker.onmessage = resolve;
+ worker.onerror = reject;
+ });
+ }
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpsOrigin'], { mime: 'application/javascript' }
+ );
+
+ return induceRequest(url)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-origin']);
+ });
+ }, 'sec-fetch-site - Same origin');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpsCrossSite'], { mime: 'application/javascript' }
+ );
+
+ return induceRequest(url)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-site');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpsSameSite'], { mime: 'application/javascript' }
+ );
+
+ return induceRequest(url)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same site');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpsOrigin', 'httpsCrossSite', 'httpsOrigin'], { mime: 'application/javascript' }
+ );
+
+ return induceRequest(url)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Cross-Site -> Same-Origin redirect');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpsOrigin', 'httpsSameSite', 'httpsOrigin'], { mime: 'application/javascript' }
+ );
+
+ return induceRequest(url)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Same-Site -> Same-Origin redirect');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpsCrossSite', 'httpsOrigin'], { mime: 'application/javascript' }
+ );
+
+ return induceRequest(url)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-Site -> Same Origin');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpsCrossSite', 'httpsSameSite'], { mime: 'application/javascript' }
+ );
+
+ return induceRequest(url)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-Site -> Same-Site');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpsCrossSite', 'httpsCrossSite'], { mime: 'application/javascript' }
+ );
+
+ return induceRequest(url)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Cross-Site -> Cross-Site');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpsOrigin', 'httpsOrigin'], { mime: 'application/javascript' }
+ );
+
+ return induceRequest(url)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-origin']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Same Origin');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpsOrigin', 'httpsSameSite'], { mime: 'application/javascript' }
+ );
+
+ return induceRequest(url)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Same-Site');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpsOrigin', 'httpsCrossSite'], { mime: 'application/javascript' }
+ );
+
+ return induceRequest(url)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Same-Origin -> Cross-Site');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpsSameSite', 'httpsOrigin'], { mime: 'application/javascript' }
+ );
+
+ return induceRequest(url)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Site -> Same Origin');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpsSameSite', 'httpsSameSite'], { mime: 'application/javascript' }
+ );
+
+ return induceRequest(url)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['same-site']);
+ });
+ }, 'sec-fetch-site - Same-Site -> Same-Site');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpsSameSite', 'httpsCrossSite'], { mime: 'application/javascript' }
+ );
+
+ return induceRequest(url)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - Same-Site -> Cross-Site');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, [], { mime: 'application/javascript' }
+ );
+
+ return induceRequest(url)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-mode');
+ assert_array_equals(headers['sec-fetch-mode'], ['no-cors']);
+ });
+ }, 'sec-fetch-mode');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, [], { mime: 'application/javascript' }
+ );
+
+ return induceRequest(url)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-dest');
+ assert_array_equals(headers['sec-fetch-dest'], ['script']);
+ });
+ }, 'sec-fetch-dest');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, [], { mime: 'application/javascript' }
+ );
+
+ return induceRequest(url)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user');
+ </script>
+</html>
diff --git a/test/wpt/tests/fetch/metadata/generated/worker-dedicated-importscripts.sub.html b/test/wpt/tests/fetch/metadata/generated/worker-dedicated-importscripts.sub.html
new file mode 100644
index 0000000..0555bba
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/generated/worker-dedicated-importscripts.sub.html
@@ -0,0 +1,228 @@
+<!DOCTYPE html>
+<!--
+This test was procedurally generated. Please do not modify it directly.
+Sources:
+- fetch/metadata/tools/fetch-metadata.conf.yml
+- fetch/metadata/tools/templates/worker-dedicated-importscripts.sub.html
+-->
+<html lang="en">
+ <meta charset="utf-8">
+ <title>HTTP headers on request for dedicated worker via the "importScripts" API</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/fetch/metadata/resources/helper.sub.js"></script>
+ <script type="module">
+ 'use strict';
+ function induceRequest(url, options) {
+ const src = `
+ importScripts('${url}');
+ postMessage('done');
+ `;
+ const workerUrl = URL.createObjectURL(
+ new Blob([src], { type: 'application/javascript' })
+ );
+ return new Promise((resolve, reject) => {
+ const worker = new Worker(workerUrl, options);
+ worker.onmessage = resolve;
+ worker.onerror = reject;
+ });
+ }
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpOrigin'], { mime: 'application/javascript' }
+ );
+
+ return induceRequest(url)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - Not sent to non-trustworthy same-origin destination');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpSameSite'], { mime: 'application/javascript' }
+ );
+
+ return induceRequest(url)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - Not sent to non-trustworthy same-site destination');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpCrossSite'], { mime: 'application/javascript' }
+ );
+
+ return induceRequest(url)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - Not sent to non-trustworthy cross-site destination');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpOrigin'], { mime: 'application/javascript' }
+ );
+
+ return induceRequest(url)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ });
+ }, 'sec-fetch-mode - Not sent to non-trustworthy same-origin destination');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpSameSite'], { mime: 'application/javascript' }
+ );
+
+ return induceRequest(url)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ });
+ }, 'sec-fetch-mode - Not sent to non-trustworthy same-site destination');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpCrossSite'], { mime: 'application/javascript' }
+ );
+
+ return induceRequest(url)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-mode');
+ });
+ }, 'sec-fetch-mode - Not sent to non-trustworthy cross-site destination');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpOrigin'], { mime: 'application/javascript' }
+ );
+
+ return induceRequest(url)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ });
+ }, 'sec-fetch-dest - Not sent to non-trustworthy same-origin destination');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpSameSite'], { mime: 'application/javascript' }
+ );
+
+ return induceRequest(url)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ });
+ }, 'sec-fetch-dest - Not sent to non-trustworthy same-site destination');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpCrossSite'], { mime: 'application/javascript' }
+ );
+
+ return induceRequest(url)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-dest');
+ });
+ }, 'sec-fetch-dest - Not sent to non-trustworthy cross-site destination');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpOrigin'], { mime: 'application/javascript' }
+ );
+
+ return induceRequest(url)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - Not sent to non-trustworthy same-origin destination');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpSameSite'], { mime: 'application/javascript' }
+ );
+
+ return induceRequest(url)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - Not sent to non-trustworthy same-site destination');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpCrossSite'], { mime: 'application/javascript' }
+ );
+
+ return induceRequest(url)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-user');
+ });
+ }, 'sec-fetch-user - Not sent to non-trustworthy cross-site destination');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpsOrigin', 'httpOrigin'], { mime: 'application/javascript' }
+ );
+
+ return induceRequest(url)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_not_own_property(headers, 'sec-fetch-site');
+ });
+ }, 'sec-fetch-site - HTTPS downgrade (header not sent)');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpOrigin', 'httpsOrigin'], { mime: 'application/javascript' }
+ );
+
+ return induceRequest(url)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - HTTPS upgrade');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin'], { mime: 'application/javascript' }
+ );
+
+ return induceRequest(url)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ assert_own_property(headers, 'sec-fetch-site');
+ assert_array_equals(headers['sec-fetch-site'], ['cross-site']);
+ });
+ }, 'sec-fetch-site - HTTPS downgrade-upgrade');
+ </script>
+</html>
diff --git a/test/wpt/tests/fetch/metadata/navigation.https.sub.html b/test/wpt/tests/fetch/metadata/navigation.https.sub.html
new file mode 100644
index 0000000..32c9cf7
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/navigation.https.sub.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<html>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=/fetch/metadata/resources/helper.js></script>
+<script>
+ test(t => {
+ let expected = {
+ "mode": "navigate",
+ "site": "none",
+ "dest": "document"
+ };
+
+ let actual = {
+ "mode": "{{headers[sec-fetch-mode]}}",
+ "site": "{{headers[sec-fetch-site]}}",
+ // Skipping `Sec-Fetch-User`, as the test harness isn't consistent here.
+ "dest": "{{headers[sec-fetch-dest]}}"
+ };
+
+ assert_header_equals(actual, expected);
+ }, "This page's top-level navigation.");
+</script>
diff --git a/test/wpt/tests/fetch/metadata/object.https.sub.html b/test/wpt/tests/fetch/metadata/object.https.sub.html
new file mode 100644
index 0000000..fae5b37
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/object.https.sub.html
@@ -0,0 +1,62 @@
+<!DOCTYPE html>
+<meta charset="utf-8"/>
+<link rel="author" href="mtrzos@google.com" title="Maciek Trzos">
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=/fetch/metadata/resources/helper.js></script>
+<script src=/common/utils.js></script>
+<body>
+<script>
+ let nonce = token();
+
+ promise_test(t => {
+ return new Promise((resolve, reject) => {
+ let key = "object-same-origin" + nonce;
+
+ let e = document.createElement('object');
+ e.data = "https://{{host}}:{{ports[https][0]}}/fetch/metadata/resources/record-header.py?file=" + key;
+ e.onload = e => {
+ let expected = {"site":"same-origin", "user":"", "mode":"navigate", "dest": "object"};
+ validate_expectations(key, expected, "Same-Origin object")
+ .then(_ => resolve())
+ .catch(e => reject(e));
+ };
+
+ document.body.appendChild(e);
+ })
+ }, "Same-Origin object");
+
+ promise_test(t => {
+ return new Promise((resolve, reject) => {
+ let key = "object-same-site" + nonce;
+
+ let e = document.createElement('object');
+ e.data = "https://{{hosts[][www]}}:{{ports[https][0]}}/fetch/metadata/resources/record-header.py?file=" + key;
+ e.onload = e => {
+ let expected = {"site":"same-site", "user":"", "mode":"navigate", "dest": "object"};
+ validate_expectations(key, expected, "Same-Site object")
+ .then(_ => resolve())
+ .catch(e => reject(e));
+ };
+
+ document.body.appendChild(e);
+ })
+ }, "Same-Site object");
+
+ promise_test(t => {
+ return new Promise((resolve, reject) => {
+ let key = "object-cross-site" + nonce;
+
+ let e = document.createElement('object');
+ e.data = "https://{{hosts[alt][www]}}:{{ports[https][0]}}/fetch/metadata/resources/record-header.py?file=" + key;
+ e.onload = e => {
+ let expected = {"site":"cross-site", "user":"", "mode":"navigate", "dest": "object"};
+ validate_expectations(key, expected, "Cross-Site object")
+ .then(_ => resolve())
+ .catch(e => reject(e));
+ };
+
+ document.body.appendChild(e);
+ })
+ }, "Cross-Site object");
+</script>
diff --git a/test/wpt/tests/fetch/metadata/paint-worklet.https.html b/test/wpt/tests/fetch/metadata/paint-worklet.https.html
new file mode 100644
index 0000000..49fc776
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/paint-worklet.https.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=/fetch/metadata/resources/helper.js></script>
+<script src=/common/utils.js></script>
+<script>
+
+ promise_test(async t => {
+ const nonce = token();
+ const key = "worklet-destination" + nonce;
+
+ await CSS.paintWorklet.addModule("/fetch/metadata/resources/record-header.py?file=" + key);
+ const expected = {"site": "same-origin", "user": "", "mode": "cors", "dest": "paintworklet"};
+ await validate_expectations(key, expected);
+ }, "The fetch metadata for paint worklet");
+
+</script>
+<body></body>
diff --git a/test/wpt/tests/fetch/metadata/portal.https.sub.html b/test/wpt/tests/fetch/metadata/portal.https.sub.html
new file mode 100644
index 0000000..55b555a
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/portal.https.sub.html
@@ -0,0 +1,50 @@
+<!DOCTYPE html>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=/resources/testdriver.js></script>
+<script src=/resources/testdriver-vendor.js></script>
+<script src=/fetch/metadata/resources/helper.js></script>
+<script src=/common/utils.js></script>
+<script src=/portals/resources/stash-utils.sub.js></script>
+<body>
+<script>
+ const USER = true;
+ const FORCED = false;
+
+ function create_test(host, expectations) {
+ async_test(t => {
+ assert_implements("HTMLPortalElement" in window, "Portals are not supported.");
+
+ let p = document.createElement('portal');
+ const key = token();
+ StashUtils.takeValue(key).then(t.step_func_done(value => {
+ assert_header_equals(value, expectations, `{{host}} -> ${host} portal`);
+ }));
+
+ let url = `https://${host}/fetch/metadata/resources/post-to-owner.py?key=${key}`;
+ p.src = url;
+ document.body.appendChild(p);
+ }, `{{host}} -> ${host} portal`);
+ }
+
+ create_test("{{host}}:{{ports[https][0]}}", {
+ "site": "same-origin",
+ "user": "",
+ "mode": "navigate",
+ "dest": "iframe"
+ });
+
+ create_test("{{hosts[][www]}}:{{ports[https][0]}}", {
+ "site": "same-site",
+ "user": "",
+ "mode": "navigate",
+ "dest": "iframe"
+ });
+
+ create_test("{{hosts[alt][www]}}:{{ports[https][0]}}", {
+ "site": "cross-site",
+ "user": "",
+ "mode": "navigate",
+ "dest": "iframe"
+ });
+</script>
diff --git a/test/wpt/tests/fetch/metadata/preload.https.sub.html b/test/wpt/tests/fetch/metadata/preload.https.sub.html
new file mode 100644
index 0000000..29042a8
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/preload.https.sub.html
@@ -0,0 +1,50 @@
+<!DOCTYPE html>
+<meta name="timeout" content="long">
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=/fetch/metadata/resources/helper.js></script>
+<script src=/common/utils.js></script>
+<body></body>
+<script>
+ test(t => {
+ assert_true(document.createElement('link').relList.supports('preload'));
+ }, "Browser supports preload.");
+
+ function create_test(host, as, expected) {
+ async_test(t => {
+ let nonce = token();
+ let key = as + nonce;
+
+ let e = document.createElement('link');
+ e.rel = "preload";
+ e.href = `https://${host}/fetch/metadata/resources/record-header.py?file=${key}`;
+ e.setAttribute("crossorigin", "crossorigin");
+ if (as !== undefined) {
+ e.setAttribute("as", as);
+ }
+ e.onload = e.onerror = t.step_func(e => {
+ fetch("/fetch/metadata/resources/record-header.py?retrieve=true&file=" + key)
+ .then(t.step_func(response => response.text()))
+ .then(t.step_func_done(text => assert_header_equals(text, expected, `preload ${as} ${host}`)))
+ .catch(t.unreached_func());
+ });
+
+ document.head.appendChild(e);
+ }, `<link rel='preload' as='${as}' href='https://${host}/...'>`);
+ }
+
+ let as_tests = [
+ [ "fetch", "empty" ],
+ [ "font", "font" ],
+ [ "image", "image" ],
+ [ "script", "script" ],
+ [ "style", "style" ],
+ [ "track", "track" ],
+ ];
+
+ as_tests.forEach(item => {
+ create_test("{{host}}:{{ports[https][0]}}", item[0], {"site":"same-origin", "user":"", "mode": "cors", "dest": item[1]});
+ create_test("{{hosts[][www]}}:{{ports[https][0]}}", item[0], {"site":"same-site", "user":"", "mode": "cors", "dest": item[1]});
+ create_test("{{hosts[alt][www]}}:{{ports[https][0]}}", item[0], {"site":"cross-site", "user":"", "mode": "cors", "dest": item[1]});
+ });
+</script>
diff --git a/test/wpt/tests/fetch/metadata/redirect/multiple-redirect-https-downgrade-upgrade.sub.html b/test/wpt/tests/fetch/metadata/redirect/multiple-redirect-https-downgrade-upgrade.sub.html
new file mode 100644
index 0000000..0f8f320
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/redirect/multiple-redirect-https-downgrade-upgrade.sub.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=/fetch/metadata/resources/helper.js></script>
+<script src=/fetch/metadata/resources/redirectTestHelper.sub.js></script>
+<script src=/common/security-features/resources/common.sub.js></script>
+<script src=/common/utils.js></script>
+<body>
+<script>
+ let nonce = token();
+ let expected = {"site": "cross-site", "user": "", "mode": "cors", "dest": "font"};
+
+ // Validate various scenarios handle a request that redirects from https => http => https
+ // correctly and avoids disclosure of any Sec- headers.
+ RunCommonRedirectTests("Https downgrade-upgrade", MultipleRedirectTo, expected);
+</script>
+</body>
diff --git a/test/wpt/tests/fetch/metadata/redirect/redirect-http-upgrade.sub.html b/test/wpt/tests/fetch/metadata/redirect/redirect-http-upgrade.sub.html
new file mode 100644
index 0000000..fa765b6
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/redirect/redirect-http-upgrade.sub.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=/fetch/metadata/resources/helper.js></script>
+<script src=/fetch/metadata/resources/redirectTestHelper.sub.js></script>
+<script src=/common/security-features/resources/common.sub.js></script>
+<script src=/common/utils.js></script>
+<body>
+<script>
+ let nonce = token();
+ let expected = { "site": "cross-site", "user": "", "mode": "cors", "dest": "font" };
+
+ // Validate various scenarios handle a request that redirects from http => https correctly and add the proper Sec- headers.
+ RunCommonRedirectTests("Http upgrade", upgradeRedirectTo, expected);
+</script>
+</body>
diff --git a/test/wpt/tests/fetch/metadata/redirect/redirect-https-downgrade.sub.html b/test/wpt/tests/fetch/metadata/redirect/redirect-https-downgrade.sub.html
new file mode 100644
index 0000000..4e5a48e
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/redirect/redirect-https-downgrade.sub.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=/fetch/metadata/resources/helper.js></script>
+<script src=/fetch/metadata/resources/redirectTestHelper.sub.js></script>
+<script src=/common/security-features/resources/common.sub.js></script>
+<script src=/common/utils.js></script>
+<body>
+ <script>
+ let nonce = token();
+ let expected = { "site": "", "user": "", "mode": "", "dest": "" };
+
+ // Validate various scenarios handle a request that redirects from https => http correctly and avoids disclosure of any Sec- headers.
+ RunCommonRedirectTests("Https downgrade", downgradeRedirectTo, expected);
+</script>
+</body>
diff --git a/test/wpt/tests/fetch/metadata/report.https.sub.html b/test/wpt/tests/fetch/metadata/report.https.sub.html
new file mode 100644
index 0000000..b65f7c0
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/report.https.sub.html
@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<link rel="author" href="mtrzos@google.com" title="Maciek Trzos">
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=/fetch/metadata/resources/helper.js></script>
+<script>
+ setup({ explicit_done: true });
+ function generate_test(expected, name) {
+ async_test(t => {
+ t.step_timeout(_ => {
+ return validate_expectations("report-" + name, expected, name + " report")
+ .then(_ => t.done());
+ }, 1000);
+ }, name + " report");
+ }
+
+ let counter = 0;
+ document.addEventListener("securitypolicyviolation", (e) => {
+ counter++;
+ if (counter == 3) {
+ generate_test({"site":"same-origin", "user":"", "mode": "no-cors", "dest": "report"}, "same-origin");
+ generate_test({"site":"same-site", "user":"", "mode": "no-cors", "dest": "report"}, "same-site");
+ generate_test({"site":"cross-site", "user":"", "mode": "no-cors", "dest": "report"}, "cross-site");
+
+ done();
+ }
+ });
+</script>
+
+<!-- The hostname here is unimportant, so long as it doesn't match 'self'. -->
+<link id="style" href="https://{{hosts[alt][élève]}}:{{ports[https][0]}}/css/support/a-green.css" rel="stylesheet">
+
+<body></body>
diff --git a/test/wpt/tests/fetch/metadata/report.https.sub.html.sub.headers b/test/wpt/tests/fetch/metadata/report.https.sub.html.sub.headers
new file mode 100644
index 0000000..1ec5df7
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/report.https.sub.html.sub.headers
@@ -0,0 +1,3 @@
+Content-Security-Policy: style-src 'self' 'unsafe-inline'; report-uri /fetch/metadata/resources/record-header.py?file=report-same-origin
+Content-Security-Policy: style-src 'self' 'unsafe-inline'; report-uri https://{{hosts[][www]}}:{{ports[https][0]}}/fetch/metadata/resources/record-header.py?file=report-same-site
+Content-Security-Policy: style-src 'self' 'unsafe-inline'; report-uri https://{{hosts[alt][www]}}:{{ports[https][0]}}/fetch/metadata/resources/record-header.py?file=report-cross-site
diff --git a/test/wpt/tests/fetch/metadata/resources/appcache-iframe.sub.html b/test/wpt/tests/fetch/metadata/resources/appcache-iframe.sub.html
new file mode 100644
index 0000000..cea9a4f
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/resources/appcache-iframe.sub.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<html lang="en" manifest="{{GET[manifest]}}">
+<script>
+if (!window.applicationCache) {
+ parent.postMessage('application cache not supported');
+} else {
+ applicationCache.onnoupdate =
+ applicationCache.ondownloading =
+ applicationCache.onobsolete =
+ applicationCache.onerror = function() {
+ parent.postMessage('okay');
+ };
+}
+</script>
+</html>
diff --git a/test/wpt/tests/fetch/metadata/resources/dedicatedWorker.js b/test/wpt/tests/fetch/metadata/resources/dedicatedWorker.js
new file mode 100644
index 0000000..18626d3
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/resources/dedicatedWorker.js
@@ -0,0 +1 @@
+self.postMessage("Loaded");
diff --git a/test/wpt/tests/fetch/metadata/resources/echo-as-json.py b/test/wpt/tests/fetch/metadata/resources/echo-as-json.py
new file mode 100644
index 0000000..44f68e8
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/resources/echo-as-json.py
@@ -0,0 +1,29 @@
+import json
+
+from wptserve.utils import isomorphic_decode
+
+def main(request, response):
+ headers = [(b"Content-Type", b"application/json"),
+ (b"Access-Control-Allow-Credentials", b"true")]
+
+ if b"origin" in request.headers:
+ headers.append((b"Access-Control-Allow-Origin", request.headers[b"origin"]))
+
+ body = u""
+
+ # If we're in a preflight, verify that `Sec-Fetch-Mode` is `cors`.
+ if request.method == u'OPTIONS':
+ if request.headers.get(b"sec-fetch-mode") != b"cors":
+ return (403, b"Failed"), [], body
+
+ headers.append((b"Access-Control-Allow-Methods", b"*"))
+ headers.append((b"Access-Control-Allow-Headers", b"*"))
+ else:
+ body = json.dumps({
+ u"dest": isomorphic_decode(request.headers.get(b"sec-fetch-dest", b"")),
+ u"mode": isomorphic_decode(request.headers.get(b"sec-fetch-mode", b"")),
+ u"site": isomorphic_decode(request.headers.get(b"sec-fetch-site", b"")),
+ u"user": isomorphic_decode(request.headers.get(b"sec-fetch-user", b"")),
+ })
+
+ return headers, body
diff --git a/test/wpt/tests/fetch/metadata/resources/echo-as-script.py b/test/wpt/tests/fetch/metadata/resources/echo-as-script.py
new file mode 100644
index 0000000..1e7bc91
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/resources/echo-as-script.py
@@ -0,0 +1,14 @@
+import json
+
+from wptserve.utils import isomorphic_decode
+
+def main(request, response):
+ headers = [(b"Content-Type", b"text/javascript")]
+ body = u"var header = %s;" % json.dumps({
+ u"dest": isomorphic_decode(request.headers.get(b"sec-fetch-dest", b"")),
+ u"mode": isomorphic_decode(request.headers.get(b"sec-fetch-mode", b"")),
+ u"site": isomorphic_decode(request.headers.get(b"sec-fetch-site", b"")),
+ u"user": isomorphic_decode(request.headers.get(b"sec-fetch-user", b"")),
+ })
+
+ return headers, body
diff --git a/test/wpt/tests/fetch/metadata/resources/es-module.sub.js b/test/wpt/tests/fetch/metadata/resources/es-module.sub.js
new file mode 100644
index 0000000..f9668a3
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/resources/es-module.sub.js
@@ -0,0 +1 @@
+import '{{GET[moduleId]}}';
diff --git a/test/wpt/tests/fetch/metadata/resources/fetch-via-serviceworker--fallback--sw.js b/test/wpt/tests/fetch/metadata/resources/fetch-via-serviceworker--fallback--sw.js
new file mode 100644
index 0000000..09858b2
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/resources/fetch-via-serviceworker--fallback--sw.js
@@ -0,0 +1,3 @@
+self.addEventListener('fetch', function(event) {
+ // Empty event handler - will fallback to the network.
+});
diff --git a/test/wpt/tests/fetch/metadata/resources/fetch-via-serviceworker--respondWith--sw.js b/test/wpt/tests/fetch/metadata/resources/fetch-via-serviceworker--respondWith--sw.js
new file mode 100644
index 0000000..8bf8d8f
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/resources/fetch-via-serviceworker--respondWith--sw.js
@@ -0,0 +1,3 @@
+self.addEventListener('fetch', function(event) {
+ event.respondWith(fetch(event.request));
+});
diff --git a/test/wpt/tests/fetch/metadata/resources/fetch-via-serviceworker-frame.html b/test/wpt/tests/fetch/metadata/resources/fetch-via-serviceworker-frame.html
new file mode 100644
index 0000000..9879802
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/resources/fetch-via-serviceworker-frame.html
@@ -0,0 +1,3 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Page Title</title>
diff --git a/test/wpt/tests/fetch/metadata/resources/header-link.py b/test/wpt/tests/fetch/metadata/resources/header-link.py
new file mode 100644
index 0000000..de89116
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/resources/header-link.py
@@ -0,0 +1,15 @@
+def main(request, response):
+ """
+ Respond with a blank HTML document and a `Link` header which describes
+ a link relation specified by the requests `location` and `rel` query string
+ parameters
+ """
+ headers = [
+ (b'Content-Type', b'text/html'),
+ (
+ b'Link',
+ b'<' + request.GET.first(b'location') + b'>; rel=' + request.GET.first(b'rel')
+ )
+ ]
+ return (200, headers, b'')
+
diff --git a/test/wpt/tests/fetch/metadata/resources/helper.js b/test/wpt/tests/fetch/metadata/resources/helper.js
new file mode 100644
index 0000000..725f9a7
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/resources/helper.js
@@ -0,0 +1,42 @@
+function validate_expectations(key, expected, tag) {
+ return fetch("/fetch/metadata/resources/record-header.py?retrieve=true&file=" + key)
+ .then(response => response.text())
+ .then(text => {
+ assert_not_equals(text, "No header has been recorded");
+ let value = JSON.parse(text);
+ test(t => assert_equals(value.dest, expected.dest), `${tag}: sec-fetch-dest`);
+ test(t => assert_equals(value.mode, expected.mode), `${tag}: sec-fetch-mode`);
+ test(t => assert_equals(value.site, expected.site), `${tag}: sec-fetch-site`);
+ test(t => assert_equals(value.user, expected.user), `${tag}: sec-fetch-user`);
+ });
+}
+
+function validate_expectations_custom_url(url, header, expected, tag) {
+ return fetch(url, header)
+ .then(response => response.text())
+ .then(text => {
+ assert_not_equals(text, "No header has been recorded");
+ let value = JSON.parse(text);
+ test(t => assert_equals(value.dest, expected.dest), `${tag}: sec-fetch-dest`);
+ test(t => assert_equals(value.mode, expected.mode), `${tag}: sec-fetch-mode`);
+ test(t => assert_equals(value.site, expected.site), `${tag}: sec-fetch-site`);
+ test(t => assert_equals(value.user, expected.user), `${tag}: sec-fetch-user`);
+ });
+}
+
+/**
+ * @param {object} value
+ * @param {object} expected
+ * @param {string} tag
+ **/
+function assert_header_equals(value, expected, tag) {
+ if (typeof(value) === "string"){
+ assert_not_equals(value, "No header has been recorded");
+ value = JSON.parse(value);
+ }
+
+ test(t => assert_equals(value.dest, expected.dest), `${tag}: sec-fetch-dest`);
+ test(t => assert_equals(value.mode, expected.mode), `${tag}: sec-fetch-mode`);
+ test(t => assert_equals(value.site, expected.site), `${tag}: sec-fetch-site`);
+ test(t => assert_equals(value.user, expected.user), `${tag}: sec-fetch-user`);
+}
diff --git a/test/wpt/tests/fetch/metadata/resources/helper.sub.js b/test/wpt/tests/fetch/metadata/resources/helper.sub.js
new file mode 100644
index 0000000..fd179fe
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/resources/helper.sub.js
@@ -0,0 +1,67 @@
+'use strict';
+
+/**
+ * Construct a URL which, when followed, will trigger redirection through zero
+ * or more specified origins and ultimately resolve in the Python handler
+ * `record-headers.py`.
+ *
+ * @param {string} key - the WPT server "stash" name where the request's
+ * headers should be stored
+ * @param {string[]} [origins] - zero or more origin names through which the
+ * request should pass; see the function
+ * implementation for a completel list of names
+ * and corresponding origins; If specified, the
+ * final origin will be used to access the
+ * `record-headers.py` hander.
+ * @param {object} [params] - a collection of key-value pairs to include as
+ * URL "search" parameters in the final request to
+ * `record-headers.py`
+ *
+ * @returns {string} an absolute URL
+ */
+function makeRequestURL(key, origins, params) {
+ const byName = {
+ httpOrigin: 'http://{{host}}:{{ports[http][0]}}',
+ httpSameSite: 'http://{{hosts[][www]}}:{{ports[http][0]}}',
+ httpCrossSite: 'http://{{hosts[alt][]}}:{{ports[http][0]}}',
+ httpsOrigin: 'https://{{host}}:{{ports[https][0]}}',
+ httpsSameSite: 'https://{{hosts[][www]}}:{{ports[https][0]}}',
+ httpsCrossSite: 'https://{{hosts[alt][]}}:{{ports[https][0]}}'
+ };
+ const redirectPath = '/fetch/api/resources/redirect.py?location=';
+ const path = '/fetch/metadata/resources/record-headers.py?key=' + key;
+
+ let requestUrl = path;
+ if (params) {
+ requestUrl += '&' + new URLSearchParams(params).toString();
+ }
+
+ if (origins && origins.length) {
+ requestUrl = byName[origins.pop()] + requestUrl;
+
+ while (origins.length) {
+ requestUrl = byName[origins.pop()] + redirectPath +
+ encodeURIComponent(requestUrl);
+ }
+ } else {
+ requestUrl = byName.httpsOrigin + requestUrl;
+ }
+
+ return requestUrl;
+}
+
+function retrieve(key, options) {
+ return fetch('/fetch/metadata/resources/record-headers.py?retrieve&key=' + key)
+ .then((response) => {
+ if (response.status === 204 && options && options.poll) {
+ return new Promise((resolve) => setTimeout(resolve, 300))
+ .then(() => retrieve(key, options));
+ }
+
+ if (response.status !== 200) {
+ throw new Error('Failed to query for recorded headers.');
+ }
+
+ return response.text().then((text) => JSON.parse(text));
+ });
+}
diff --git a/test/wpt/tests/fetch/metadata/resources/message-opener.html b/test/wpt/tests/fetch/metadata/resources/message-opener.html
new file mode 100644
index 0000000..eb2af7b
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/resources/message-opener.html
@@ -0,0 +1,17 @@
+<script>
+/**
+ * Send a message to the opening browsing context when the document is
+ * "completely loaded" (a condition which occurs immediately after the `load`
+ * and `pageshow` events are fired).
+ * https://html.spec.whatwg.org/multipage/parsing.html#the-end
+ */
+'use strict';
+
+// The `pageshow` event is used instead of the `load` event because this
+// document may itself be accessed via history traversal. In such cases, the
+// browser may choose to reuse a cached document and therefore fire no
+// additional `load` events.
+addEventListener('pageshow', () => {
+ setTimeout(() => opener.postMessage(null, '*'), 0);
+});
+</script>
diff --git a/test/wpt/tests/fetch/metadata/resources/post-to-owner.py b/test/wpt/tests/fetch/metadata/resources/post-to-owner.py
new file mode 100644
index 0000000..256dd6e
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/resources/post-to-owner.py
@@ -0,0 +1,36 @@
+import json
+
+from wptserve.utils import isomorphic_decode
+
+def main(request, response):
+ headers = [
+ (b"Content-Type", b"text/html"),
+ (b"Cache-Control", b"no-cache, no-store, must-revalidate")
+ ]
+ key = request.GET.first(b"key", None)
+
+ # We serialize the key into JSON, so have to decode it first.
+ if key is not None:
+ key = key.decode('utf-8')
+
+ body = u"""
+ <!DOCTYPE html>
+ <script src="/portals/resources/stash-utils.sub.js"></script>
+ <script>
+ var data = %s;
+ if (window.opener)
+ window.opener.postMessage(data, "*");
+ if (window.top != window)
+ window.top.postMessage(data, "*");
+
+ const key = %s;
+ if (key)
+ StashUtils.putValue(key, data);
+ </script>
+ """ % (json.dumps({
+ u"dest": isomorphic_decode(request.headers.get(b"sec-fetch-dest", b"")),
+ u"mode": isomorphic_decode(request.headers.get(b"sec-fetch-mode", b"")),
+ u"site": isomorphic_decode(request.headers.get(b"sec-fetch-site", b"")),
+ u"user": isomorphic_decode(request.headers.get(b"sec-fetch-user", b"")),
+ }), json.dumps(key))
+ return headers, body
diff --git a/test/wpt/tests/fetch/metadata/resources/record-header.py b/test/wpt/tests/fetch/metadata/resources/record-header.py
new file mode 100644
index 0000000..29ff2ed
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/resources/record-header.py
@@ -0,0 +1,145 @@
+import os
+import hashlib
+import json
+
+from wptserve.utils import isomorphic_decode
+
+def main(request, response):
+ ## Get the query parameter (key) from URL ##
+ ## Tests will record POST requests (CSP Report) and GET (rest) ##
+ if request.GET:
+ key = request.GET[b'file']
+ elif request.POST:
+ key = request.POST[b'file']
+
+ ## Convert the key from String to UUID valid String ##
+ testId = hashlib.md5(key).hexdigest()
+
+ ## Handle the header retrieval request ##
+ if b'retrieve' in request.GET:
+ response.writer.write_status(200)
+ response.writer.write_header(b"Connection", b"close")
+ response.writer.end_headers()
+ try:
+ header_value = request.server.stash.take(testId)
+ response.writer.write(header_value)
+ except (KeyError, ValueError) as e:
+ response.writer.write(u"No header has been recorded")
+ pass
+
+ response.close_connection = True
+
+ ## Record incoming fetch metadata header value
+ else:
+ try:
+ ## Return a serialized JSON object with one member per header. If the ##
+ ## header isn't present, the member will contain an empty string. ##
+ header = json.dumps({
+ u"dest": isomorphic_decode(request.headers.get(b"sec-fetch-dest", b"")),
+ u"mode": isomorphic_decode(request.headers.get(b"sec-fetch-mode", b"")),
+ u"site": isomorphic_decode(request.headers.get(b"sec-fetch-site", b"")),
+ u"user": isomorphic_decode(request.headers.get(b"sec-fetch-user", b"")),
+ })
+ request.server.stash.put(testId, header)
+ except KeyError:
+ ## The header is already recorded or it doesn't exist
+ pass
+
+ ## Prevent the browser from caching returned responses and allow CORS ##
+ response.headers.set(b"Access-Control-Allow-Origin", b"*")
+ response.headers.set(b"Cache-Control", b"no-cache, no-store, must-revalidate")
+ response.headers.set(b"Pragma", b"no-cache")
+ response.headers.set(b"Expires", b"0")
+
+ ## Add a valid ServiceWorker Content-Type ##
+ if key.startswith(b"serviceworker"):
+ response.headers.set(b"Content-Type", b"application/javascript")
+
+ ## Add a valid image Content-Type ##
+ if key.startswith(b"image"):
+ response.headers.set(b"Content-Type", b"image/png")
+ file = open(os.path.join(request.doc_root, u"media", u"1x1-green.png"), u"rb")
+ image = file.read()
+ file.close()
+ return image
+
+ ## Return a valid .vtt content for the <track> tag ##
+ if key.startswith(b"track"):
+ return b"WEBVTT"
+
+ ## Return a valid SharedWorker ##
+ if key.startswith(b"sharedworker"):
+ response.headers.set(b"Content-Type", b"application/javascript")
+ file = open(os.path.join(request.doc_root, u"fetch", u"metadata",
+ u"resources", u"sharedWorker.js"), u"rb")
+ shared_worker = file.read()
+ file.close()
+ return shared_worker
+
+ ## Return a valid font content and Content-Type ##
+ if key.startswith(b"font"):
+ response.headers.set(b"Content-Type", b"application/x-font-ttf")
+ file = open(os.path.join(request.doc_root, u"fonts", u"Ahem.ttf"), u"rb")
+ font = file.read()
+ file.close()
+ return font
+
+ ## Return a valid audio content and Content-Type ##
+ if key.startswith(b"audio"):
+ response.headers.set(b"Content-Type", b"audio/mpeg")
+ file = open(os.path.join(request.doc_root, u"media", u"sound_5.mp3"), u"rb")
+ audio = file.read()
+ file.close()
+ return audio
+
+ ## Return a valid video content and Content-Type ##
+ if key.startswith(b"video"):
+ response.headers.set(b"Content-Type", b"video/mp4")
+ file = open(os.path.join(request.doc_root, u"media", u"A4.mp4"), u"rb")
+ video = file.read()
+ file.close()
+ return video
+
+ ## Return valid style content and Content-Type ##
+ if key.startswith(b"style"):
+ response.headers.set(b"Content-Type", b"text/css")
+ return b"div { }"
+
+ ## Return a valid embed/object content and Content-Type ##
+ if key.startswith(b"embed") or key.startswith(b"object"):
+ response.headers.set(b"Content-Type", b"text/html")
+ return b"<html>EMBED!</html>"
+
+ ## Return a valid image content and Content-Type for redirect requests ##
+ if key.startswith(b"redirect"):
+ response.headers.set(b"Content-Type", b"image/jpeg")
+ file = open(os.path.join(request.doc_root, u"media", u"1x1-green.png"), u"rb")
+ image = file.read()
+ file.close()
+ return image
+
+ ## Return a valid dedicated worker
+ if key.startswith(b"worker"):
+ response.headers.set(b"Content-Type", b"application/javascript")
+ return b"self.postMessage('loaded');"
+
+ ## Return a valid worklet
+ if key.startswith(b"worklet"):
+ response.headers.set(b"Content-Type", b"application/javascript")
+ return b""
+
+ ## Return a valid XSLT
+ if key.startswith(b"xslt"):
+ response.headers.set(b"Content-Type", b"text/xsl")
+ return b"""<?xml version="1.0" encoding="UTF-8"?>
+<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
+ <xsl:template match="@*|node()">
+ <xsl:copy>
+ <xsl:apply-templates select="@*|node()"/>
+ </xsl:copy>
+ </xsl:template>
+</xsl:stylesheet>"""
+
+ if key.startswith(b"script"):
+ response.headers.set(b"Content-Type", b"application/javascript")
+ return b"void 0;"
diff --git a/test/wpt/tests/fetch/metadata/resources/record-headers.py b/test/wpt/tests/fetch/metadata/resources/record-headers.py
new file mode 100644
index 0000000..0362fe2
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/resources/record-headers.py
@@ -0,0 +1,73 @@
+import os
+import uuid
+import hashlib
+import time
+import json
+
+
+def bytes_to_strings(d):
+ # Recursively convert bytes to strings in `d`.
+ if not isinstance(d, dict):
+ if isinstance(d, (tuple,list,set)):
+ v = [bytes_to_strings(x) for x in d]
+ return v
+ else:
+ if isinstance(d, bytes):
+ d = d.decode()
+ return d
+
+ result = {}
+ for k,v in d.items():
+ if isinstance(k, bytes):
+ k = k.decode()
+ if isinstance(v, dict):
+ v = bytes_to_strings(v)
+ elif isinstance(v, (tuple,list,set)):
+ v = [bytes_to_strings(x) for x in v]
+ elif isinstance(v, bytes):
+ v = v.decode()
+ result[k] = v
+ return result
+
+
+def main(request, response):
+ # This condition avoids false positives from CORS preflight checks, where the
+ # request under test may be followed immediately by a request to the same URL
+ # using a different HTTP method.
+ if b'requireOPTIONS' in request.GET and request.method != b'OPTIONS':
+ return
+
+ if b'key' in request.GET:
+ key = request.GET[b'key']
+ elif b'key' in request.POST:
+ key = request.POST[b'key']
+
+ ## Convert the key from String to UUID valid String ##
+ testId = hashlib.md5(key).hexdigest()
+
+ ## Handle the header retrieval request ##
+ if b'retrieve' in request.GET:
+ recorded_headers = request.server.stash.take(testId)
+
+ if recorded_headers is None:
+ return (204, [], b'')
+
+ return (200, [], recorded_headers)
+
+ ## Record incoming fetch metadata header value
+ else:
+ try:
+ request.server.stash.put(testId, json.dumps(bytes_to_strings(request.headers)))
+ except KeyError:
+ ## The header is already recorded or it doesn't exist
+ pass
+
+ ## Prevent the browser from caching returned responses and allow CORS ##
+ response.headers.set(b"Access-Control-Allow-Origin", b"*")
+ response.headers.set(b"Cache-Control", b"no-cache, no-store, must-revalidate")
+ response.headers.set(b"Pragma", b"no-cache")
+ response.headers.set(b"Expires", b"0")
+ if b"mime" in request.GET:
+ response.headers.set(b"Content-Type", request.GET.first(b"mime"))
+
+ return request.GET.first(b"body", request.POST.first(b"body", b""))
diff --git a/test/wpt/tests/fetch/metadata/resources/redirectTestHelper.sub.js b/test/wpt/tests/fetch/metadata/resources/redirectTestHelper.sub.js
new file mode 100644
index 0000000..1bfbbae
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/resources/redirectTestHelper.sub.js
@@ -0,0 +1,167 @@
+function createVideoElement() {
+ let el = document.createElement('video');
+ el.src = '/media/movie_5.mp4';
+ el.setAttribute('controls', '');
+ el.setAttribute('crossorigin', '');
+ return el;
+}
+
+function createTrack() {
+ let el = document.createElement('track');
+ el.setAttribute('default', '');
+ el.setAttribute('kind', 'captions');
+ el.setAttribute('srclang', 'en');
+ return el;
+}
+
+let secureRedirectURL = 'https://{{host}}:{{ports[https][0]}}/fetch/api/resources/redirect.py?location=';
+let insecureRedirectURL = 'http://{{host}}:{{ports[http][0]}}/fetch/api/resources/redirect.py?location=';
+let secureTestURL = 'https://{{host}}:{{ports[https][0]}}/fetch/metadata/';
+let insecureTestURL = 'http://{{host}}:{{ports[http][0]}}/fetch/metadata/';
+
+// Helper to craft an URL that will go from HTTPS => HTTP => HTTPS to
+// simulate us downgrading then upgrading again during the same redirect chain.
+function MultipleRedirectTo(partialPath) {
+ let finalURL = insecureRedirectURL + encodeURIComponent(secureTestURL + partialPath);
+ return secureRedirectURL + encodeURIComponent(finalURL);
+}
+
+// Helper to craft an URL that will go from HTTP => HTTPS to simulate upgrading a
+// given request.
+function upgradeRedirectTo(partialPath) {
+ return insecureRedirectURL + encodeURIComponent(secureTestURL + partialPath);
+}
+
+// Helper to craft an URL that will go from HTTPS => HTTP to simulate downgrading a
+// given request.
+function downgradeRedirectTo(partialPath) {
+ return secureRedirectURL + encodeURIComponent(insecureTestURL + partialPath);
+}
+
+// Helper to run common redirect test cases that don't require special setup on
+// the test page itself.
+function RunCommonRedirectTests(testNamePrefix, urlHelperMethod, expectedResults) {
+ async_test(t => {
+ let testWindow = window.open(urlHelperMethod('resources/post-to-owner.py?top-level-navigation' + nonce));
+ t.add_cleanup(_ => testWindow.close());
+ window.addEventListener('message', t.step_func(e => {
+ if (e.source != testWindow) {
+ return;
+ }
+
+ let expectation = { ...expectedResults };
+ if (expectation['mode'] != '')
+ expectation['mode'] = 'navigate';
+ if (expectation['dest'] == 'font')
+ expectation['dest'] = 'document';
+ assert_header_equals(e.data, expectation, testNamePrefix + ' top level navigation');
+ t.done();
+ }));
+ }, testNamePrefix + ' top level navigation');
+
+ promise_test(t => {
+ return new Promise((resolve, reject) => {
+ let key = 'embed-https-redirect' + nonce;
+ let e = document.createElement('embed');
+ e.src = urlHelperMethod('resources/record-header.py?file=' + key);
+ e.onload = e => {
+ let expectation = { ...expectedResults };
+ if (expectation['mode'] != '')
+ expectation['mode'] = 'navigate';
+ if (expectation['dest'] == 'font')
+ expectation['dest'] = 'embed';
+ fetch('/fetch/metadata/resources/record-header.py?retrieve=true&file=' + key)
+ .then(response => response.text())
+ .then(t.step_func(text => assert_header_equals(text, expectation, testNamePrefix + ' embed')))
+ .then(resolve)
+ .catch(e => reject(e));
+ };
+ document.body.appendChild(e);
+ });
+ }, testNamePrefix + ' embed');
+
+ promise_test(t => {
+ return new Promise((resolve, reject) => {
+ let key = 'object-https-redirect' + nonce;
+ let e = document.createElement('object');
+ e.data = urlHelperMethod('resources/record-header.py?file=' + key);
+ e.onload = e => {
+ let expectation = { ...expectedResults };
+ if (expectation['mode'] != '')
+ expectation['mode'] = 'navigate';
+ if (expectation['dest'] == 'font')
+ expectation['dest'] = 'object';
+ fetch('/fetch/metadata/resources/record-header.py?retrieve=true&file=' + key)
+ .then(response => response.text())
+ .then(t.step_func(text => assert_header_equals(text, expectation, testNamePrefix + ' object')))
+ .then(resolve)
+ .catch(e => reject(e));
+ };
+ document.body.appendChild(e);
+ });
+ }, testNamePrefix + ' object');
+
+ if (document.createElement('link').relList.supports('preload')) {
+ async_test(t => {
+ let key = 'preload' + nonce;
+ let e = document.createElement('link');
+ e.rel = 'preload';
+ e.href = urlHelperMethod('resources/record-header.py?file=' + key);
+ e.setAttribute('as', 'track');
+ e.onload = e.onerror = t.step_func_done(e => {
+ let expectation = { ...expectedResults };
+ if (expectation['mode'] != '')
+ expectation['mode'] = 'cors';
+ fetch('/fetch/metadata/resources/record-header.py?retrieve=true&file=' + key)
+ .then(t.step_func(response => response.text()))
+ .then(t.step_func_done(text => assert_header_equals(text, expectation, testNamePrefix + ' preload')))
+ .catch(t.unreached_func());
+ });
+ document.head.appendChild(e);
+ }, testNamePrefix + ' preload');
+ }
+
+ promise_test(t => {
+ return new Promise((resolve, reject) => {
+ let key = 'style-https-redirect' + nonce;
+ let e = document.createElement('link');
+ e.rel = 'stylesheet';
+ e.href = urlHelperMethod('resources/record-header.py?file=' + key);
+ e.onload = e => {
+ let expectation = { ...expectedResults };
+ if (expectation['mode'] != '')
+ expectation['mode'] = 'no-cors';
+ if (expectation['dest'] == 'font')
+ expectation['dest'] = 'style';
+ fetch('/fetch/metadata/resources/record-header.py?retrieve=true&file=' + key)
+ .then(response => response.text())
+ .then(t.step_func(text => assert_header_equals(text, expectation, testNamePrefix + ' stylesheet')))
+ .then(resolve)
+ .catch(e => reject(e));
+ };
+ document.body.appendChild(e);
+ });
+ }, testNamePrefix + ' stylesheet');
+
+ promise_test(t => {
+ return new Promise((resolve, reject) => {
+ let key = 'track-https-redirect' + nonce;
+ let video = createVideoElement();
+ let el = createTrack();
+ el.src = urlHelperMethod('resources/record-header.py?file=' + key);
+ el.onload = t.step_func(_ => {
+ let expectation = { ...expectedResults };
+ if (expectation['mode'] != '')
+ expectation['mode'] = 'cors';
+ if (expectation['dest'] == 'font')
+ expectation['dest'] = 'track';
+ fetch('/fetch/metadata/resources/record-header.py?retrieve=true&file=' + key)
+ .then(response => response.text())
+ .then(t.step_func(text => assert_header_equals(text, expectation, testNamePrefix + ' track')))
+ .then(resolve);
+ });
+ video.appendChild(el);
+ document.body.appendChild(video);
+ });
+ }, testNamePrefix + ' track');
+}
diff --git a/test/wpt/tests/fetch/metadata/resources/serviceworker-accessors-frame.html b/test/wpt/tests/fetch/metadata/resources/serviceworker-accessors-frame.html
new file mode 100644
index 0000000..9879802
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/resources/serviceworker-accessors-frame.html
@@ -0,0 +1,3 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Page Title</title>
diff --git a/test/wpt/tests/fetch/metadata/resources/serviceworker-accessors.sw.js b/test/wpt/tests/fetch/metadata/resources/serviceworker-accessors.sw.js
new file mode 100644
index 0000000..36c55a7
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/resources/serviceworker-accessors.sw.js
@@ -0,0 +1,14 @@
+addEventListener("fetch", event => {
+ event.waitUntil(async function () {
+ if (!event.clientId) return;
+ const client = await clients.get(event.clientId);
+ if (!client) return;
+
+ client.postMessage({
+ "dest": event.request.headers.get("sec-fetch-dest"),
+ "mode": event.request.headers.get("sec-fetch-mode"),
+ "site": event.request.headers.get("sec-fetch-site"),
+ "user": event.request.headers.get("sec-fetch-user")
+ });
+ }());
+});
diff --git a/test/wpt/tests/fetch/metadata/resources/sharedWorker.js b/test/wpt/tests/fetch/metadata/resources/sharedWorker.js
new file mode 100644
index 0000000..5eb89cb
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/resources/sharedWorker.js
@@ -0,0 +1,9 @@
+onconnect = function(e) {
+ var port = e.ports[0];
+
+ port.addEventListener('message', function(e) {
+ port.postMessage("Ready");
+ });
+
+ port.start();
+}
diff --git a/test/wpt/tests/fetch/metadata/resources/unload-with-beacon.html b/test/wpt/tests/fetch/metadata/resources/unload-with-beacon.html
new file mode 100644
index 0000000..b00c9a5
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/resources/unload-with-beacon.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<script>
+ // When told, register an unload handler that will trigger a beacon to the
+ // URL given by the sender of the message.
+ window.addEventListener('message', e => {
+ var url = e.data;
+ window.addEventListener('unload', () => {
+ navigator.sendBeacon(url, 'blah');
+ });
+ window.parent.postMessage('navigate-away', '*');
+ });
+</script>
diff --git a/test/wpt/tests/fetch/metadata/resources/xslt-test.sub.xml b/test/wpt/tests/fetch/metadata/resources/xslt-test.sub.xml
new file mode 100644
index 0000000..acb478a
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/resources/xslt-test.sub.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<?xml-stylesheet href="https://{{host}}:{{ports[https][0]}}/fetch/metadata/resources/record-header.py?file=xslt-same-origin{{GET[token]}}" type="text/xsl" ?>
+<!-- Only testing same-origin XSLT because same-site and cross-site XSLT is blocked. -->
+
+<!-- postMessage parent back when the resources are loaded -->
+<script xmlns="http://www.w3.org/1999/xhtml"><![CDATA[
+ setTimeout(function(){
+ if (window.opener)
+ window.opener.postMessage("", "*");
+ if (window.top != window)
+ window.top.postMessage("", "*");}, 100);
+]]></script>
diff --git a/test/wpt/tests/fetch/metadata/serviceworker-accessors.https.sub.html b/test/wpt/tests/fetch/metadata/serviceworker-accessors.https.sub.html
new file mode 100644
index 0000000..03a8321
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/serviceworker-accessors.https.sub.html
@@ -0,0 +1,51 @@
+<!DOCTYPE html>
+<!--
+ This test verifies that Fetch Metadata headers are not exposed to Service
+ Workers via the request's `headers` accessor.
+-->
+<meta charset="utf-8"/>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=/fetch/metadata/resources/helper.js></script>
+<script src=/service-workers/service-worker/resources/test-helpers.sub.js></script>
+<script src=/common/utils.js></script>
+<script>
+ const SCOPE = 'resources/serviceworker-accessors-frame.html';
+ const SCRIPT = 'resources/serviceworker-accessors.sw.js';
+
+ function assert_headers_not_seen_in_service_worker(frame) {
+ return new Promise((resolve, reject) => {
+ frame.contentWindow.fetch(SCOPE, {mode:'no-cors'});
+ frame.contentWindow.navigator.serviceWorker.addEventListener('message', e => {
+ assert_header_equals(e.data, {
+ "dest": null,
+ "mode": null,
+ "site": null,
+ "user": null
+ });
+ resolve();
+ });
+ });
+ }
+
+ promise_test(async function(t) {
+ const reg = await service_worker_unregister_and_register(t, SCRIPT, SCOPE);
+
+ t.add_cleanup(async () => {
+ if (reg)
+ await reg.unregister();
+ });
+
+ await wait_for_state(t, reg.installing, 'activated');
+
+ const frame = await with_iframe(SCOPE);
+ t.add_cleanup(async () => {
+ if (frame)
+ frame.remove();
+ });
+
+ // Trigger a fetch that will go through the service worker, and validate
+ // the visible headers.
+ await assert_headers_not_seen_in_service_worker(frame);
+ }, 'Sec-Fetch headers in Service Worker fetch handler.');
+</script>
diff --git a/test/wpt/tests/fetch/metadata/sharedworker.https.sub.html b/test/wpt/tests/fetch/metadata/sharedworker.https.sub.html
new file mode 100644
index 0000000..4df8582
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/sharedworker.https.sub.html
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+
+<link rel="author" href="mtrzos@google.com" title="Maciek Trzos">
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=/fetch/metadata/resources/helper.js></script>
+<script src=/common/utils.js></script>
+<script>
+ let nonce = token();
+ let key = "sharedworker-same-origin" + nonce;
+
+ // TESTS //
+ if (window.Worker) {
+
+ // Same-Origin test
+ var sharedWorker = new SharedWorker('/fetch/metadata/resources/record-header.py?file=' + key);
+ sharedWorker.port.start();
+
+ sharedWorker.onerror = function(){
+ test_same_origin();
+ }
+ sharedWorker.port.onmessage = function(e) {
+ test_same_origin();
+ }
+ sharedWorker.port.postMessage("Ready");
+ }
+
+ function test_same_origin(){
+ promise_test(t => {
+ return new Promise((resolve, reject) => {
+ let expected = {"site":"same-origin", "user":"", "mode": "same-origin", "dest": "sharedworker"};
+
+ validate_expectations(key, expected)
+ .then(_ => resolve())
+ .catch(e => reject(e));
+ })
+ }, "Same-Origin sharedworker")
+ }
+</script>
+<body></body>
diff --git a/test/wpt/tests/fetch/metadata/style.https.sub.html b/test/wpt/tests/fetch/metadata/style.https.sub.html
new file mode 100644
index 0000000..a30d81d
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/style.https.sub.html
@@ -0,0 +1,86 @@
+<!DOCTYPE html>
+<html>
+<link rel="author" href="mtrzos@google.com" title="Maciek Trzos">
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=/fetch/metadata/resources/helper.js></script>
+<script src=/common/utils.js></script>
+<body></body>
+<script>
+ let nonce = token();
+
+ promise_test(t => {
+ return new Promise((resolve, reject) => {
+ let key = "style-same-origin" + nonce;
+
+ let e = document.createElement('link');
+ e.rel = "stylesheet";
+ e.href = "https://{{host}}:{{ports[https][0]}}/fetch/metadata/resources/record-header.py?file=" + key;
+ e.onload = e => {
+ let expected = {"site":"same-origin", "user":"", "mode": "no-cors", "dest": "style"};
+ validate_expectations(key, expected, "Same-Origin style")
+ .then(_ => resolve())
+ .catch(e => reject(e));
+ };
+
+ document.body.appendChild(e);
+ })
+ }, "Same-Origin style");
+
+ promise_test(t => {
+ return new Promise((resolve, reject) => {
+ let key = "style-same-site" + nonce;
+
+ let e = document.createElement('link');
+ e.rel = "stylesheet";
+ e.href = "https://{{hosts[][www]}}:{{ports[https][0]}}/fetch/metadata/resources/record-header.py?file=" + key;
+ e.onload = e => {
+ let expected = {"site":"same-site", "user":"", "mode": "no-cors", "dest": "style"};
+ validate_expectations(key, expected, "Same-Site style")
+ .then(_ => resolve())
+ .catch(e => reject(e));
+ };
+
+ document.body.appendChild(e);
+ })
+ }, "Same-Site style");
+
+ promise_test(t => {
+ return new Promise((resolve, reject) => {
+ let key = "style-cross-site" + nonce;
+
+ let e = document.createElement('link');
+ e.rel = "stylesheet";
+ e.href = "https://{{hosts[alt][www]}}:{{ports[https][0]}}/fetch/metadata/resources/record-header.py?file=" + key;
+ e.onload = e => {
+ let expected = {"site":"cross-site", "user":"", "mode": "no-cors", "dest": "style"};
+ validate_expectations(key, expected, "Cross-Site style")
+ .then(_ => resolve())
+ .catch(e => reject(e));
+ };
+
+ document.body.appendChild(e);
+ })
+ }, "Cross-Site style");
+
+ promise_test(t => {
+ return new Promise((resolve, reject) => {
+ let key = "style-same-origin-cors" + nonce;
+
+ let e = document.createElement('link');
+ e.rel = "stylesheet";
+ e.href = "https://{{host}}:{{ports[https][0]}}/fetch/metadata/resources/record-header.py?file=" + key;
+ e.crossOrigin = "anonymous";
+ e.onload = e => {
+ let expected = {"site":"same-origin", "user":"", "mode": "cors", "dest": "style"};
+ validate_expectations(key, expected, "Same-Origin, cors style")
+ .then(_ => resolve())
+ .catch(e => reject(e));
+ };
+
+ document.body.appendChild(e);
+ })
+ }, "Same-Origin, cors style");
+</script>
+</html>
+
diff --git a/test/wpt/tests/fetch/metadata/tools/README.md b/test/wpt/tests/fetch/metadata/tools/README.md
new file mode 100644
index 0000000..1c3bac2
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/tools/README.md
@@ -0,0 +1,126 @@
+# Fetch Metadata test generation framework
+
+This directory defines a command-line tool for procedurally generating WPT
+tests.
+
+## Motivation
+
+Many features of the web platform involve the browser making one or more HTTP
+requests to remote servers. Only some aspects of these requests are specified
+within the standard that defines the relevant feature. Other aspects are
+specified by external standards which span the entire platform (e.g. [Fetch
+Metadata Request Headers](https://w3c.github.io/webappsec-fetch-metadata/)).
+
+This state of affairs makes it difficult to maintain test coverage for two
+reasons:
+
+- When a new feature introduces a new kind of web request, it must be verified
+ to integrate with every cross-cutting standard.
+- When a new cross-cutting standard is introduced, it must be verified to
+ integrate with every kind of web request.
+
+The tool in this directory attempts to reduce this tension. It allows
+maintainers to express instructions for making web requests in an abstract
+sense. These generic instructions can be reused by to produce a different suite
+of tests for each cross-cutting feature.
+
+When a new kind of request is proposed, a single generic template can be
+defined here. This will provide the maintainers of all cross-cutting features
+with clear instruction on how to extend their test suite with the new feature.
+
+Similarly, when a new cross-cutting feature is proposed, the authors can use
+this tool to build a test suite which spans the entire platform.
+
+## Build script
+
+To generate the Fetch Metadata tests, run `./wpt update-built --include fetch`
+in the root of the repository.
+
+## Configuration
+
+The test generation tool requires a YAML-formatted configuration file as its
+input. The file should define a dictionary with the following keys:
+
+- `templates` - a string describing the filesystem path from which template
+ files should be loaded
+- `output_directory` - a string describing the filesystem path where the
+ generated test files should be written
+- `cases` - a list of dictionaries describing how the test templates should be
+ expanded with individual subtests; each dictionary should have the following
+ keys:
+ - `all_subtests` - properties which should be defined for every expansion
+ - `common_axis` - a list of dictionaries
+ - `template_axes` - a dictionary relating template names to properties that
+ should be used when expanding that particular template
+
+Internally, the tool creates a set of "subtests" for each template. This set is
+the Cartesian product of the `common_axis` and the given template's entry in
+the `template_axes` dictionary. It uses this set of subtests to expand the
+template, creating an output file. Refer to the next section for a concrete
+example of how the expansion is performed.
+
+In general, the tool will output a single file for each template. However, the
+`filename_flags` attribute has special semantics. It is used to separate
+subtests for the same template file. This is intended to accommodate [the
+web-platform-test's filename-based
+conventions](https://web-platform-tests.org/writing-tests/file-names.html).
+
+For instance, when `.https` is present in a test file's name, the WPT test
+harness will load that test using the HTTPS protocol. Subtests which include
+the value `https` in the `filename_flags` property will be expanded using the
+appropriate template but written to a distinct file whose name includes
+`.https`.
+
+The generation tool requires that the configuration file references every
+template in the `templates` directory. Because templates and configuration
+files may be contributed by different people, this requirement ensures that
+configuration authors are aware of all available templates. Some templates may
+not be relevant for some features; in those cases, the configuration file can
+include an empty array for the template's entry in the `template_axes`
+dictionary (as in `template3.html` in the example which follows).
+
+## Expansion example
+
+In the following example configuration file, `a`, `b`, `s`, `w`, `x`, `y`, and
+`z` all represent associative arrays.
+
+```yaml
+templates: path/to/templates
+output_directory: path/to/output
+cases:
+ - every_subtest: s
+ common_axis: [a, b]
+ template_axes:
+ template1.html: [w]
+ template2.html: [x, y, z]
+ template3.html: []
+```
+
+When run with such a configuration file, the tool would generate two files,
+expanded with data as described below (where `(a, b)` represents the union of
+`a` and `b`):
+
+ template1.html: [(a, w), (b, w)]
+ template2.html: [(a, x), (b, x), (a, y), (b, y), (a, z), (b, z)]
+ template3.html: (zero tests; not expanded)
+
+## Design Considerations
+
+**Efficiency of generated output** The tool is capable of generating a large
+number of tests given a small amount of input. Naively structured, this could
+result in test suites which take large amount of time and computational
+resources to complete. The tool has been designed to help authors structure the
+generated output to reduce these resource requirements.
+
+**Literalness of generated output** Because the generated output is how most
+people will interact with the tests, it is important that it be approachable.
+This tool avoids outputting abstractions which would frustrate attempts to read
+the source code or step through its execution environment.
+
+**Simplicity** The test generation logic itself was written to be approachable.
+This makes it easier to anticipate how the tool will behave with new input, and
+it lowers the bar for others to contribute improvements.
+
+Non-goals include conciseness of template files (verbosity makes the potential
+expansions more predictable) and conciseness of generated output (verbosity
+aids in the interpretation of results).
diff --git a/test/wpt/tests/fetch/metadata/tools/fetch-metadata.conf.yml b/test/wpt/tests/fetch/metadata/tools/fetch-metadata.conf.yml
new file mode 100644
index 0000000..b277bcb
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/tools/fetch-metadata.conf.yml
@@ -0,0 +1,806 @@
+---
+templates: templates
+output_directory: ../generated
+cases:
+ - all_subtests:
+ expected: NULL
+ filename_flags: []
+ common_axis:
+ - headerName: sec-fetch-site
+ origins: [httpOrigin]
+ description: Not sent to non-trustworthy same-origin destination
+ - headerName: sec-fetch-site
+ origins: [httpSameSite]
+ description: Not sent to non-trustworthy same-site destination
+ - headerName: sec-fetch-site
+ origins: [httpCrossSite]
+ description: Not sent to non-trustworthy cross-site destination
+ - headerName: sec-fetch-mode
+ origins: [httpOrigin]
+ description: Not sent to non-trustworthy same-origin destination
+ - headerName: sec-fetch-mode
+ origins: [httpSameSite]
+ description: Not sent to non-trustworthy same-site destination
+ - headerName: sec-fetch-mode
+ origins: [httpCrossSite]
+ description: Not sent to non-trustworthy cross-site destination
+ - headerName: sec-fetch-dest
+ origins: [httpOrigin]
+ description: Not sent to non-trustworthy same-origin destination
+ - headerName: sec-fetch-dest
+ origins: [httpSameSite]
+ description: Not sent to non-trustworthy same-site destination
+ - headerName: sec-fetch-dest
+ origins: [httpCrossSite]
+ description: Not sent to non-trustworthy cross-site destination
+ - headerName: sec-fetch-user
+ origins: [httpOrigin]
+ description: Not sent to non-trustworthy same-origin destination
+ - headerName: sec-fetch-user
+ origins: [httpSameSite]
+ description: Not sent to non-trustworthy same-site destination
+ - headerName: sec-fetch-user
+ origins: [httpCrossSite]
+ description: Not sent to non-trustworthy cross-site destination
+ template_axes:
+ # Unused
+ appcache-manifest.sub.https.html: []
+ # The `audioWorklet` interface is only available in secure contexts
+ # https://webaudio.github.io/web-audio-api/#BaseAudioContext
+ audioworklet.https.sub.html: []
+ # Service workers are only available in secure context
+ fetch-via-serviceworker.https.sub.html: []
+ # Service workers are only available in secure context
+ serviceworker.https.sub.html: []
+
+ css-images.sub.html:
+ - filename_flags: [tentative]
+ css-font-face.sub.html:
+ - filename_flags: [tentative]
+ element-a.sub.html: [{}]
+ element-area.sub.html: [{}]
+ element-audio.sub.html: [{}]
+ element-embed.sub.html: [{}]
+ element-frame.sub.html: [{}]
+ element-iframe.sub.html: [{}]
+ element-img.sub.html:
+ - sourceAttr: src
+ - sourceAttr: srcset
+ element-img-environment-change.sub.html: [{}]
+ element-input-image.sub.html: [{}]
+ element-link-icon.sub.html: [{}]
+ element-link-prefetch.optional.sub.html: [{}]
+ element-meta-refresh.optional.sub.html: [{}]
+ element-picture.sub.html: [{}]
+ element-script.sub.html:
+ - {}
+ - elementAttrs: { type: module }
+ element-video.sub.html: [{}]
+ element-video-poster.sub.html: [{}]
+ fetch.sub.html: [{}]
+ form-submission.sub.html:
+ - method: GET
+ - method: POST
+ header-link.sub.html:
+ - rel: icon
+ - rel: stylesheet
+ header-refresh.optional.sub.html: [{}]
+ window-location.sub.html: [{}]
+ script-module-import-dynamic.sub.html: [{}]
+ script-module-import-static.sub.html: [{}]
+ svg-image.sub.html: [{}]
+ window-history.sub.html: [{}]
+ worker-dedicated-importscripts.sub.html: [{}]
+ worker-dedicated-constructor.sub.html: [{}]
+
+ # Sec-Fetch-Site - direct requests
+ - all_subtests:
+ headerName: sec-fetch-site
+ filename_flags: [https]
+ common_axis:
+ - description: Same origin
+ origins: [httpsOrigin]
+ expected: same-origin
+ - description: Cross-site
+ origins: [httpsCrossSite]
+ expected: cross-site
+ - description: Same site
+ origins: [httpsSameSite]
+ expected: same-site
+ template_axes:
+ # Unused
+ # - the request mode of all "classic" worker scripts is set to
+ # "same-origin"
+ # https://html.spec.whatwg.org/#fetch-a-classic-worker-script
+ # - the request mode of all "top-level "module" worker scripts is set to
+ # "same-origin":
+ # https://html.spec.whatwg.org/#fetch-a-single-module-script
+ worker-dedicated-constructor.sub.html: []
+
+ appcache-manifest.sub.https.html: [{}]
+ audioworklet.https.sub.html: [{}]
+ css-images.sub.html:
+ - filename_flags: [tentative]
+ css-font-face.sub.html:
+ - filename_flags: [tentative]
+ element-a.sub.html: [{}]
+ element-area.sub.html: [{}]
+ element-audio.sub.html: [{}]
+ element-embed.sub.html: [{}]
+ element-frame.sub.html: [{}]
+ element-iframe.sub.html: [{}]
+ element-img.sub.html:
+ - sourceAttr: src
+ - sourceAttr: srcset
+ element-img-environment-change.sub.html: [{}]
+ element-input-image.sub.html: [{}]
+ element-link-icon.sub.html: [{}]
+ element-link-prefetch.optional.sub.html: [{}]
+ element-meta-refresh.optional.sub.html: [{}]
+ element-picture.sub.html: [{}]
+ element-script.sub.html:
+ - {}
+ - elementAttrs: { type: module }
+ element-video.sub.html: [{}]
+ element-video-poster.sub.html: [{}]
+ fetch.sub.html: [{ init: { mode: no-cors } }]
+ fetch-via-serviceworker.https.sub.html: [{ init: { mode: no-cors } }]
+ form-submission.sub.html:
+ - method: GET
+ - method: POST
+ header-link.sub.html:
+ - rel: icon
+ - rel: stylesheet
+ header-refresh.optional.sub.html: [{}]
+ window-location.sub.html: [{}]
+ script-module-import-dynamic.sub.html: [{}]
+ script-module-import-static.sub.html: [{}]
+ serviceworker.https.sub.html: [{}]
+ svg-image.sub.html: [{}]
+ window-history.sub.html: [{}]
+ worker-dedicated-importscripts.sub.html: [{}]
+
+ # Sec-Fetch-Site - redirection from HTTP
+ - all_subtests:
+ headerName: sec-fetch-site
+ filename_flags: []
+ common_axis:
+ - description: HTTPS downgrade (header not sent)
+ origins: [httpsOrigin, httpOrigin]
+ expected: NULL
+ - description: HTTPS upgrade
+ origins: [httpOrigin, httpsOrigin]
+ expected: cross-site
+ - description: HTTPS downgrade-upgrade
+ origins: [httpsOrigin, httpOrigin, httpsOrigin]
+ expected: cross-site
+ template_axes:
+ # Unused
+ # The `audioWorklet` interface is only available in secure contexts
+ # https://webaudio.github.io/web-audio-api/#BaseAudioContext
+ audioworklet.https.sub.html: []
+ # Service workers are only available in secure context
+ fetch-via-serviceworker.https.sub.html: []
+ # Service workers' redirect mode is "error"
+ serviceworker.https.sub.html: []
+ # Interstitial locations in an HTTP redirect chain are not added to the
+ # session history, so these requests cannot be initiated using the
+ # History API.
+ window-history.sub.html: []
+ # Unused
+ # - the request mode of all "classic" worker scripts is set to
+ # "same-origin"
+ # https://html.spec.whatwg.org/#fetch-a-classic-worker-script
+ # - the request mode of all "top-level "module" worker scripts is set to
+ # "same-origin":
+ # https://html.spec.whatwg.org/#fetch-a-single-module-script
+ worker-dedicated-constructor.sub.html: []
+
+ appcache-manifest.sub.https.html: [{}]
+ css-images.sub.html:
+ - filename_flags: [tentative]
+ css-font-face.sub.html:
+ - filename_flags: [tentative]
+ element-a.sub.html: [{}]
+ element-area.sub.html: [{}]
+ element-audio.sub.html: [{}]
+ element-embed.sub.html: [{}]
+ element-frame.sub.html: [{}]
+ element-iframe.sub.html: [{}]
+ element-img.sub.html:
+ - sourceAttr: src
+ - sourceAttr: srcset
+ element-img-environment-change.sub.html: [{}]
+ element-input-image.sub.html: [{}]
+ element-link-icon.sub.html: [{}]
+ element-link-prefetch.optional.sub.html: [{}]
+ element-meta-refresh.optional.sub.html: [{}]
+ element-picture.sub.html: [{}]
+ element-script.sub.html:
+ - {}
+ - elementAttrs: { type: module }
+ element-video.sub.html: [{}]
+ element-video-poster.sub.html: [{}]
+ fetch.sub.html: [{}]
+ form-submission.sub.html:
+ - method: GET
+ - method: POST
+ header-link.sub.html:
+ - rel: icon
+ - rel: stylesheet
+ header-refresh.optional.sub.html: [{}]
+ window-location.sub.html: [{}]
+ script-module-import-dynamic.sub.html: [{}]
+ script-module-import-static.sub.html: [{}]
+ svg-image.sub.html: [{}]
+ worker-dedicated-importscripts.sub.html: [{}]
+
+ # Sec-Fetch-Site - redirection from HTTPS
+ - all_subtests:
+ headerName: sec-fetch-site
+ filename_flags: [https]
+ common_axis:
+ - description: Same-Origin -> Cross-Site -> Same-Origin redirect
+ origins: [httpsOrigin, httpsCrossSite, httpsOrigin]
+ expected: cross-site
+ - description: Same-Origin -> Same-Site -> Same-Origin redirect
+ origins: [httpsOrigin, httpsSameSite, httpsOrigin]
+ expected: same-site
+ - description: Cross-Site -> Same Origin
+ origins: [httpsCrossSite, httpsOrigin]
+ expected: cross-site
+ - description: Cross-Site -> Same-Site
+ origins: [httpsCrossSite, httpsSameSite]
+ expected: cross-site
+ - description: Cross-Site -> Cross-Site
+ origins: [httpsCrossSite, httpsCrossSite]
+ expected: cross-site
+ - description: Same-Origin -> Same Origin
+ origins: [httpsOrigin, httpsOrigin]
+ expected: same-origin
+ - description: Same-Origin -> Same-Site
+ origins: [httpsOrigin, httpsSameSite]
+ expected: same-site
+ - description: Same-Origin -> Cross-Site
+ origins: [httpsOrigin, httpsCrossSite]
+ expected: cross-site
+ - description: Same-Site -> Same Origin
+ origins: [httpsSameSite, httpsOrigin]
+ expected: same-site
+ - description: Same-Site -> Same-Site
+ origins: [httpsSameSite, httpsSameSite]
+ expected: same-site
+ - description: Same-Site -> Cross-Site
+ origins: [httpsSameSite, httpsCrossSite]
+ expected: cross-site
+ template_axes:
+ # Service Workers' redirect mode is "error"
+ serviceworker.https.sub.html: []
+ # Interstitial locations in an HTTP redirect chain are not added to the
+ # session history, so these requests cannot be initiated using the
+ # History API.
+ window-history.sub.html: []
+ # Unused
+ # - the request mode of all "classic" worker scripts is set to
+ # "same-origin"
+ # https://html.spec.whatwg.org/#fetch-a-classic-worker-script
+ # - the request mode of all "top-level "module" worker scripts is set to
+ # "same-origin":
+ # https://html.spec.whatwg.org/#fetch-a-single-module-script
+ worker-dedicated-constructor.sub.html: []
+
+ appcache-manifest.sub.https.html: [{}]
+ audioworklet.https.sub.html: [{}]
+ css-images.sub.html:
+ - filename_flags: [tentative]
+ css-font-face.sub.html:
+ - filename_flags: [tentative]
+ element-a.sub.html: [{}]
+ element-area.sub.html: [{}]
+ element-audio.sub.html: [{}]
+ element-embed.sub.html: [{}]
+ element-frame.sub.html: [{}]
+ element-iframe.sub.html: [{}]
+ element-img.sub.html:
+ - sourceAttr: src
+ - sourceAttr: srcset
+ element-img-environment-change.sub.html: [{}]
+ element-input-image.sub.html: [{}]
+ element-link-icon.sub.html: [{}]
+ element-link-prefetch.optional.sub.html: [{}]
+ element-meta-refresh.optional.sub.html: [{}]
+ element-picture.sub.html: [{}]
+ element-script.sub.html:
+ - {}
+ - elementAttrs: { type: module }
+ element-video.sub.html: [{}]
+ element-video-poster.sub.html: [{}]
+ fetch.sub.html: [{ init: { mode: no-cors } }]
+ fetch-via-serviceworker.https.sub.html: [{ init: { mode: no-cors } }]
+ form-submission.sub.html:
+ - method: GET
+ - method: POST
+ header-link.sub.html:
+ - rel: icon
+ - rel: stylesheet
+ header-refresh.optional.sub.html: [{}]
+ window-location.sub.html: [{}]
+ script-module-import-dynamic.sub.html: [{}]
+ script-module-import-static.sub.html: [{}]
+ svg-image.sub.html: [{}]
+ worker-dedicated-importscripts.sub.html: [{}]
+
+ # Sec-Fetch-Site - redirection with mixed content
+ # These tests verify the effect that redirection has on the request's "site".
+ # The initial request must be made to a resource that is "same-site" with its
+ # origin. This avoids false positives because if the request were made to a
+ # cross-site resource, the value of "cross-site" would be assigned regardless
+ # of the subseqent redirection.
+ #
+ # Because these conditions necessarily warrant mixed content, only templates
+ # which can be configured to allow mixed content [1] can be used.
+ #
+ # [1] https://w3c.github.io/webappsec-mixed-content/#should-block-fetch
+
+ - common_axis:
+ - description: HTTPS downgrade-upgrade
+ headerName: sec-fetch-site
+ origins: [httpsOrigin, httpOrigin, httpsOrigin]
+ expected: cross-site
+ filename_flags: [https]
+ template_axes:
+ # Mixed Content considers only a small subset of requests as
+ # "optionally-blockable." These are the only requests that can be tested
+ # for the "downgrade-upgrade" scenario, so all other templates must be
+ # explicitly ignored.
+ audioworklet.https.sub.html: []
+ css-font-face.sub.html: []
+ element-embed.sub.html: []
+ element-frame.sub.html: []
+ element-iframe.sub.html: []
+ element-img-environment-change.sub.html: []
+ element-link-icon.sub.html: []
+ element-link-prefetch.optional.sub.html: []
+ element-picture.sub.html: []
+ element-script.sub.html: []
+ fetch.sub.html: []
+ fetch-via-serviceworker.https.sub.html: []
+ header-link.sub.html: []
+ script-module-import-static.sub.html: []
+ script-module-import-dynamic.sub.html: []
+ # Service Workers' redirect mode is "error"
+ serviceworker.https.sub.html: []
+ # Interstitial locations in an HTTP redirect chain are not added to the
+ # session history, so these requests cannot be initiated using the
+ # History API.
+ window-history.sub.html: []
+ worker-dedicated-constructor.sub.html: []
+ worker-dedicated-importscripts.sub.html: []
+ # Avoid duplicate subtest for 'sec-fetch-site - HTTPS downgrade-upgrade'
+ appcache-manifest.sub.https.html: []
+ css-images.sub.html:
+ - filename_flags: [tentative]
+ element-a.sub.html: [{}]
+ element-area.sub.html: [{}]
+ element-audio.sub.html: [{}]
+ element-img.sub.html:
+ # srcset omitted because it is not "optionally-blockable"
+ # https://w3c.github.io/webappsec-mixed-content/#category-optionally-blockable
+ - sourceAttr: src
+ element-input-image.sub.html: [{}]
+ element-meta-refresh.optional.sub.html: [{}]
+ element-video.sub.html: [{}]
+ element-video-poster.sub.html: [{}]
+ form-submission.sub.html:
+ - method: GET
+ - method: POST
+ header-refresh.optional.sub.html: [{}]
+ svg-image.sub.html: [{}]
+ window-location.sub.html: [{}]
+
+ # Sec-Fetch-Mode
+ # These tests are served over HTTPS so the induced requests will be both
+ # same-origin with the document [1] and a potentially-trustworthy URL [2].
+ #
+ # [1] https://html.spec.whatwg.org/multipage/origin.html#same-origin
+ # [2] https://w3c.github.io/webappsec-secure-contexts/#potentially-trustworthy-url
+ - common_axis:
+ - headerName: sec-fetch-mode
+ filename_flags: [https]
+ origins: []
+ template_axes:
+ appcache-manifest.sub.https.html:
+ - expected: no-cors
+ audioworklet.https.sub.html:
+ # https://html.spec.whatwg.org/multipage/webappapis.html#fetch-a-single-module-script
+ - expected: cors
+ css-images.sub.html:
+ - expected: no-cors
+ filename_flags: [tentative]
+ css-font-face.sub.html:
+ - expected: cors
+ filename_flags: [tentative]
+ element-a.sub.html:
+ - expected: navigate
+ # https://html.spec.whatwg.org/multipage/links.html#downloading-hyperlinks
+ - elementAttrs: {download: ''}
+ expected: no-cors
+ element-area.sub.html:
+ - expected: navigate
+ # https://html.spec.whatwg.org/multipage/links.html#downloading-hyperlinks
+ - elementAttrs: {download: ''}
+ expected: no-cors
+ element-audio.sub.html:
+ - expected: no-cors
+ - expected: cors
+ elementAttrs: { crossorigin: '' }
+ - expected: cors
+ elementAttrs: { crossorigin: anonymous }
+ - expected: cors
+ elementAttrs: { crossorigin: use-credentials }
+ element-embed.sub.html:
+ - expected: no-cors
+ element-frame.sub.html:
+ - expected: navigate
+ element-iframe.sub.html:
+ - expected: navigate
+ element-img.sub.html:
+ - sourceAttr: src
+ expected: no-cors
+ - sourceAttr: src
+ expected: cors
+ elementAttrs: { crossorigin: '' }
+ - sourceAttr: src
+ expected: cors
+ elementAttrs: { crossorigin: anonymous }
+ - sourceAttr: src
+ expected: cors
+ elementAttrs: { crossorigin: use-credentials }
+ - sourceAttr: srcset
+ expected: no-cors
+ - sourceAttr: srcset
+ expected: cors
+ elementAttrs: { crossorigin: '' }
+ - sourceAttr: srcset
+ expected: cors
+ elementAttrs: { crossorigin: anonymous }
+ - sourceAttr: srcset
+ expected: cors
+ elementAttrs: { crossorigin: use-credentials }
+ element-img-environment-change.sub.html:
+ - expected: no-cors
+ - expected: cors
+ elementAttrs: { crossorigin: '' }
+ - expected: cors
+ elementAttrs: { crossorigin: anonymous }
+ - expected: cors
+ elementAttrs: { crossorigin: use-credentials }
+ element-input-image.sub.html:
+ - expected: no-cors
+ element-link-icon.sub.html:
+ - expected: no-cors
+ - expected: cors
+ elementAttrs: { crossorigin: '' }
+ - expected: cors
+ elementAttrs: { crossorigin: anonymous }
+ - expected: cors
+ elementAttrs: { crossorigin: use-credentials }
+ element-link-prefetch.optional.sub.html:
+ - expected: no-cors
+ - expected: cors
+ elementAttrs: { crossorigin: '' }
+ - expected: cors
+ elementAttrs: { crossorigin: anonymous }
+ - expected: cors
+ elementAttrs: { crossorigin: use-credentials }
+ element-meta-refresh.optional.sub.html:
+ - expected: navigate
+ element-picture.sub.html:
+ - expected: no-cors
+ - expected: cors
+ elementAttrs: { crossorigin: '' }
+ - expected: cors
+ elementAttrs: { crossorigin: anonymous }
+ - expected: cors
+ elementAttrs: { crossorigin: use-credentials }
+ element-script.sub.html:
+ - expected: no-cors
+ - expected: cors
+ elementAttrs: { type: module }
+ - expected: cors
+ elementAttrs: { crossorigin: '' }
+ - expected: cors
+ elementAttrs: { crossorigin: anonymous }
+ - expected: cors
+ elementAttrs: { crossorigin: use-credentials }
+ element-video.sub.html:
+ - expected: no-cors
+ - expected: cors
+ elementAttrs: { crossorigin: '' }
+ - expected: cors
+ elementAttrs: { crossorigin: anonymous }
+ - expected: cors
+ elementAttrs: { crossorigin: use-credentials }
+ element-video-poster.sub.html:
+ - expected: no-cors
+ fetch.sub.html:
+ - expected: cors
+ - expected: cors
+ init: { mode: cors }
+ - expected: no-cors
+ init: { mode: no-cors }
+ - expected: same-origin
+ init: { mode: same-origin }
+ fetch-via-serviceworker.https.sub.html:
+ - expected: cors
+ - expected: cors
+ init: { mode: cors }
+ - expected: no-cors
+ init: { mode: no-cors }
+ - expected: same-origin
+ init: { mode: same-origin }
+ form-submission.sub.html:
+ - method: GET
+ expected: navigate
+ - method: POST
+ expected: navigate
+ header-link.sub.html:
+ - rel: icon
+ expected: no-cors
+ - rel: stylesheet
+ expected: no-cors
+ header-refresh.optional.sub.html:
+ - expected: navigate
+ window-history.sub.html:
+ - expected: navigate
+ window-location.sub.html:
+ - expected: navigate
+ script-module-import-dynamic.sub.html:
+ - expected: cors
+ script-module-import-static.sub.html:
+ - expected: cors
+ # https://svgwg.org/svg2-draft/linking.html#processingURL-fetch
+ svg-image.sub.html:
+ - expected: no-cors
+ - expected: cors
+ elementAttrs: { crossorigin: '' }
+ - expected: cors
+ elementAttrs: { crossorigin: anonymous }
+ - expected: cors
+ elementAttrs: { crossorigin: use-credentials }
+ serviceworker.https.sub.html:
+ - expected: same-origin
+ options: { type: 'classic' }
+ # https://github.com/whatwg/html/pull/5875
+ - expected: same-origin
+ worker-dedicated-constructor.sub.html:
+ - expected: same-origin
+ - options: { type: module }
+ expected: same-origin
+ worker-dedicated-importscripts.sub.html:
+ - expected: no-cors
+
+ # Sec-Fetch-Dest
+ - common_axis:
+ - headerName: sec-fetch-dest
+ filename_flags: [https]
+ origins: []
+ template_axes:
+ appcache-manifest.sub.https.html:
+ - expected: empty
+ audioworklet.https.sub.html:
+ # https://github.com/WebAudio/web-audio-api/issues/2203
+ - expected: audioworklet
+ css-images.sub.html:
+ - expected: image
+ filename_flags: [tentative]
+ css-font-face.sub.html:
+ - expected: font
+ filename_flags: [tentative]
+ element-a.sub.html:
+ - expected: document
+ # https://html.spec.whatwg.org/multipage/links.html#downloading-hyperlinks
+ - elementAttrs: {download: ''}
+ expected: empty
+ element-area.sub.html:
+ - expected: document
+ # https://html.spec.whatwg.org/multipage/links.html#downloading-hyperlinks
+ - elementAttrs: {download: ''}
+ expected: empty
+ element-audio.sub.html:
+ - expected: audio
+ element-embed.sub.html:
+ - expected: embed
+ element-frame.sub.html:
+ # https://github.com/whatwg/html/pull/4976
+ - expected: frame
+ element-iframe.sub.html:
+ # https://github.com/whatwg/html/pull/4976
+ - expected: iframe
+ element-img.sub.html:
+ - sourceAttr: src
+ expected: image
+ - sourceAttr: srcset
+ expected: image
+ element-img-environment-change.sub.html:
+ - expected: image
+ element-input-image.sub.html:
+ - expected: image
+ element-link-icon.sub.html:
+ - expected: empty
+ element-link-prefetch.optional.sub.html:
+ - expected: empty
+ - elementAttrs: { as: audio }
+ expected: audio
+ - elementAttrs: { as: document }
+ expected: document
+ - elementAttrs: { as: embed }
+ expected: embed
+ - elementAttrs: { as: fetch }
+ expected: fetch
+ - elementAttrs: { as: font }
+ expected: font
+ - elementAttrs: { as: image }
+ expected: image
+ - elementAttrs: { as: object }
+ expected: object
+ - elementAttrs: { as: script }
+ expected: script
+ - elementAttrs: { as: style }
+ expected: style
+ - elementAttrs: { as: track }
+ expected: track
+ - elementAttrs: { as: video }
+ expected: video
+ - elementAttrs: { as: worker }
+ expected: worker
+ element-meta-refresh.optional.sub.html:
+ - expected: document
+ element-picture.sub.html:
+ - expected: image
+ element-script.sub.html:
+ - expected: script
+ element-video.sub.html:
+ - expected: video
+ element-video-poster.sub.html:
+ - expected: image
+ fetch.sub.html:
+ - expected: empty
+ fetch-via-serviceworker.https.sub.html:
+ - expected: empty
+ form-submission.sub.html:
+ - method: GET
+ expected: document
+ - method: POST
+ expected: document
+ header-link.sub.html:
+ - rel: icon
+ expected: empty
+ - rel: stylesheet
+ filename_flags: [tentative]
+ expected: style
+ header-refresh.optional.sub.html:
+ - expected: document
+ window-history.sub.html:
+ - expected: document
+ window-location.sub.html:
+ - expected: document
+ script-module-import-dynamic.sub.html:
+ - expected: script
+ script-module-import-static.sub.html:
+ - expected: script
+ serviceworker.https.sub.html:
+ - expected: serviceworker
+ # Implemented as "image" in Chromium and Firefox, but specified as
+ # "empty"
+ # https://github.com/w3c/svgwg/issues/782
+ svg-image.sub.html:
+ - expected: empty
+ worker-dedicated-constructor.sub.html:
+ - expected: worker
+ - options: { type: module }
+ expected: worker
+ worker-dedicated-importscripts.sub.html:
+ - expected: script
+
+ # Sec-Fetch-User
+ - common_axis:
+ - headerName: sec-fetch-user
+ filename_flags: [https]
+ origins: []
+ template_axes:
+ appcache-manifest.sub.https.html:
+ - expected: NULL
+ audioworklet.https.sub.html:
+ - expected: NULL
+ css-images.sub.html:
+ - expected: NULL
+ filename_flags: [tentative]
+ css-font-face.sub.html:
+ - expected: NULL
+ filename_flags: [tentative]
+ element-a.sub.html:
+ - expected: NULL
+ - userActivated: TRUE
+ expected: ?1
+ element-area.sub.html:
+ - expected: NULL
+ - userActivated: TRUE
+ expected: ?1
+ element-audio.sub.html:
+ - expected: NULL
+ element-embed.sub.html:
+ - expected: NULL
+ element-frame.sub.html:
+ - expected: NULL
+ - userActivated: TRUE
+ expected: ?1
+ element-iframe.sub.html:
+ - expected: NULL
+ - userActivated: TRUE
+ expected: ?1
+ element-img.sub.html:
+ - sourceAttr: src
+ expected: NULL
+ - sourceAttr: srcset
+ expected: NULL
+ element-img-environment-change.sub.html:
+ - expected: NULL
+ element-input-image.sub.html:
+ - expected: NULL
+ element-link-icon.sub.html:
+ - expected: NULL
+ element-link-prefetch.optional.sub.html:
+ - expected: NULL
+ element-meta-refresh.optional.sub.html:
+ - expected: NULL
+ element-picture.sub.html:
+ - expected: NULL
+ element-script.sub.html:
+ - expected: NULL
+ element-video.sub.html:
+ - expected: NULL
+ element-video-poster.sub.html:
+ - expected: NULL
+ fetch.sub.html:
+ - expected: NULL
+ fetch-via-serviceworker.https.sub.html:
+ - expected: NULL
+ form-submission.sub.html:
+ - method: GET
+ expected: NULL
+ - method: GET
+ userActivated: TRUE
+ expected: ?1
+ - method: POST
+ expected: NULL
+ - method: POST
+ userActivated: TRUE
+ expected: ?1
+ header-link.sub.html:
+ - rel: icon
+ expected: NULL
+ - rel: stylesheet
+ expected: NULL
+ header-refresh.optional.sub.html:
+ - expected: NULL
+ window-history.sub.html:
+ - expected: NULL
+ window-location.sub.html:
+ - expected: NULL
+ - userActivated: TRUE
+ expected: ?1
+ script-module-import-dynamic.sub.html:
+ - expected: NULL
+ script-module-import-static.sub.html:
+ - expected: NULL
+ serviceworker.https.sub.html:
+ - expected: NULL
+ svg-image.sub.html:
+ - expected: NULL
+ worker-dedicated-constructor.sub.html:
+ - expected: NULL
+ - options: { type: module }
+ expected: NULL
+ worker-dedicated-importscripts.sub.html:
+ - expected: NULL
diff --git a/test/wpt/tests/fetch/metadata/tools/generate.py b/test/wpt/tests/fetch/metadata/tools/generate.py
new file mode 100644
index 0000000..fa850c8
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/tools/generate.py
@@ -0,0 +1,195 @@
+#!/usr/bin/env python3
+
+import itertools
+import os
+
+import jinja2
+import yaml
+
+HERE = os.path.abspath(os.path.dirname(__file__))
+PROJECT_ROOT = os.path.join(HERE, '..', '..', '..')
+
+def find_templates(starting_directory):
+ for directory, subdirectories, file_names in os.walk(starting_directory):
+ for file_name in file_names:
+ if file_name.startswith('.'):
+ continue
+ yield file_name, os.path.join(directory, file_name)
+
+def test_name(directory, template_name, subtest_flags):
+ '''
+ Create a test name based on a template and the WPT file name flags [1]
+ required for a given subtest. This name is used to determine how subtests
+ may be grouped together. In order to promote grouping, the combination uses
+ a few aspects of how file name flags are interpreted:
+
+ - repeated flags have no effect, so duplicates are removed
+ - flag sequence does not matter, so flags are consistently sorted
+
+ directory | template_name | subtest_flags | result
+ ----------|------------------|-----------------|-------
+ cors | image.html | [] | cors/image.html
+ cors | image.https.html | [] | cors/image.https.html
+ cors | image.html | [https] | cors/image.https.html
+ cors | image.https.html | [https] | cors/image.https.html
+ cors | image.https.html | [https] | cors/image.https.html
+ cors | image.sub.html | [https] | cors/image.https.sub.html
+ cors | image.https.html | [sub] | cors/image.https.sub.html
+
+ [1] docs/writing-tests/file-names.md
+ '''
+ template_name_parts = template_name.split('.')
+ flags = set(subtest_flags) | set(template_name_parts[1:-1])
+ test_name_parts = (
+ [template_name_parts[0]] +
+ sorted(flags) +
+ [template_name_parts[-1]]
+ )
+ return os.path.join(directory, '.'.join(test_name_parts))
+
+def merge(a, b):
+ if type(a) != type(b):
+ raise Exception('Cannot merge disparate types')
+ if type(a) == list:
+ return a + b
+ if type(a) == dict:
+ merged = {}
+
+ for key in a:
+ if key in b:
+ merged[key] = merge(a[key], b[key])
+ else:
+ merged[key] = a[key]
+
+ for key in b:
+ if not key in a:
+ merged[key] = b[key]
+
+ return merged
+
+ raise Exception('Cannot merge {} type'.format(type(a).__name__))
+
+def product(a, b):
+ '''
+ Given two lists of objects, compute their Cartesian product by merging the
+ elements together. For example,
+
+ product(
+ [{'a': 1}, {'b': 2}],
+ [{'c': 3}, {'d': 4}, {'e': 5}]
+ )
+
+ returns the following list:
+
+ [
+ {'a': 1, 'c': 3},
+ {'a': 1, 'd': 4},
+ {'a': 1, 'e': 5},
+ {'b': 2, 'c': 3},
+ {'b': 2, 'd': 4},
+ {'b': 2, 'e': 5}
+ ]
+ '''
+ result = []
+
+ for a_object in a:
+ for b_object in b:
+ result.append(merge(a_object, b_object))
+
+ return result
+
+def make_provenance(project_root, cases, template):
+ return '\n'.join([
+ 'This test was procedurally generated. Please do not modify it directly.',
+ 'Sources:',
+ '- {}'.format(os.path.relpath(cases, project_root)),
+ '- {}'.format(os.path.relpath(template, project_root))
+ ])
+
+def collection_filter(obj, title):
+ if not obj:
+ return 'no {}'.format(title)
+
+ members = []
+ for name, value in obj.items():
+ if value == '':
+ members.append(name)
+ else:
+ members.append('{}={}'.format(name, value))
+
+ return '{}: {}'.format(title, ', '.join(members))
+
+def pad_filter(value, side, padding):
+ if not value:
+ return ''
+ if side == 'start':
+ return padding + value
+
+ return value + padding
+
+def main(config_file):
+ with open(config_file, 'r') as handle:
+ config = yaml.safe_load(handle.read())
+
+ templates_directory = os.path.normpath(
+ os.path.join(os.path.dirname(config_file), config['templates'])
+ )
+
+ environment = jinja2.Environment(
+ variable_start_string='[%',
+ variable_end_string='%]'
+ )
+ environment.filters['collection'] = collection_filter
+ environment.filters['pad'] = pad_filter
+ templates = {}
+ subtests = {}
+
+ for template_name, path in find_templates(templates_directory):
+ subtests[template_name] = []
+ with open(path, 'r') as handle:
+ templates[template_name] = environment.from_string(handle.read())
+
+ for case in config['cases']:
+ unused_templates = set(templates) - set(case['template_axes'])
+
+ # This warning is intended to help authors avoid mistakenly omitting
+ # templates. It can be silenced by extending the`template_axes`
+ # dictionary with an empty list for templates which are intentionally
+ # unused.
+ if unused_templates:
+ print(
+ 'Warning: case does not reference the following templates:'
+ )
+ print('\n'.join('- {}'.format(name) for name in unused_templates))
+
+ common_axis = product(
+ case['common_axis'], [case.get('all_subtests', {})]
+ )
+
+ for template_name, template_axis in case['template_axes'].items():
+ subtests[template_name].extend(product(common_axis, template_axis))
+
+ for template_name, template in templates.items():
+ provenance = make_provenance(
+ PROJECT_ROOT,
+ config_file,
+ os.path.join(templates_directory, template_name)
+ )
+ get_filename = lambda subtest: test_name(
+ config['output_directory'],
+ template_name,
+ subtest['filename_flags']
+ )
+ subtests_by_filename = itertools.groupby(
+ sorted(subtests[template_name], key=get_filename),
+ key=get_filename
+ )
+ for filename, some_subtests in subtests_by_filename:
+ with open(filename, 'w') as handle:
+ handle.write(templates[template_name].render(
+ subtests=list(some_subtests),
+ provenance=provenance
+ ) + '\n')
+
+if __name__ == '__main__':
+ main('fetch-metadata.conf.yml')
diff --git a/test/wpt/tests/fetch/metadata/tools/templates/appcache-manifest.sub.https.html b/test/wpt/tests/fetch/metadata/tools/templates/appcache-manifest.sub.https.html
new file mode 100644
index 0000000..0dfc084
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/tools/templates/appcache-manifest.sub.https.html
@@ -0,0 +1,63 @@
+<!DOCTYPE html>
+<!--
+[%provenance%]
+-->
+<html lang="en">
+ <meta charset="utf-8">
+ <title>HTTP headers on request for Appcache manifest</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/fetch/metadata/resources/helper.sub.js"></script>
+ <body>
+ <script>
+ 'use strict';
+
+ function induceRequest(url) {
+ const iframe = document.createElement('iframe');
+ iframe.src =
+ '/fetch/metadata/resources/appcache-iframe.sub.html?manifest=' + encodeURIComponent(url);
+
+ return new Promise((resolve) => {
+ addEventListener('message', function onMessage(event) {
+ if (event.source !== iframe.contentWindow) {
+ return;
+ }
+ removeEventListener('message', onMessage);
+ resolve(event.data);
+ });
+
+ document.body.appendChild(iframe);
+ })
+ .then((message) => {
+ if (message !== 'okay') {
+ throw message;
+ }
+ })
+ .then(() => iframe.remove());
+ }
+
+ {%- for subtest in subtests %}
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ assert_implements_optional(
+ !!window.applicationCache, 'Application Cache supported.'
+ );
+
+ induceRequest(makeRequestURL(key, [% subtest.origins %]))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ {%- if subtest.expected == none %}
+ assert_not_own_property(headers, '[%subtest.headerName%]');
+ {%- else %}
+ assert_own_property(headers, '[%subtest.headerName%]');
+ assert_array_equals(headers['[%subtest.headerName%]'], ['[%subtest.expected%]']);
+ {%- endif %}
+ })
+ .then(() => t.done(), t.step_func((error) => { throw error; }));
+ }, '[%subtest.headerName%][%subtest.description | pad("start", " - ")%]');
+
+ {%- endfor %}
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/fetch/metadata/tools/templates/audioworklet.https.sub.html b/test/wpt/tests/fetch/metadata/tools/templates/audioworklet.https.sub.html
new file mode 100644
index 0000000..7be309c
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/tools/templates/audioworklet.https.sub.html
@@ -0,0 +1,53 @@
+<!DOCTYPE html>
+<!--
+[%provenance%]
+-->
+<html lang="en">
+ <meta charset="utf-8">
+ <title>HTTP headers on request for AudioWorklet module</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/resources/testdriver.js"></script>
+ <script src="/resources/testdriver-vendor.js"></script>
+ <script src="/fetch/metadata/resources/helper.sub.js"></script>
+ <body>
+ <script>
+ 'use strict';
+
+ function induceRequest(url, test) {
+ return test_driver.bless(
+ 'Enable WebAudio playback',
+ () => {
+ const audioContext = new AudioContext();
+
+ test.add_cleanup(() => audioContext.close());
+
+ return audioContext.audioWorklet.addModule(url);
+ }
+ );
+ }
+
+ {%- for subtest in subtests %}
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, [% subtest.origins %], {mime: 'text/javascript'}),
+ t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ {%- if subtest.expected == none %}
+ assert_not_own_property(headers, '[%subtest.headerName%]');
+ {%- else %}
+ assert_own_property(headers, '[%subtest.headerName%]');
+ assert_array_equals(headers['[%subtest.headerName%]'], ['[%subtest.expected%]']);
+ {%- endif %}
+ });
+ }, '[%subtest.headerName%][%subtest.description | pad("start", " - ")%]');
+
+ {%- endfor %}
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/fetch/metadata/tools/templates/css-font-face.sub.html b/test/wpt/tests/fetch/metadata/tools/templates/css-font-face.sub.html
new file mode 100644
index 0000000..94b33f4
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/tools/templates/css-font-face.sub.html
@@ -0,0 +1,60 @@
+<!DOCTYPE html>
+<!--
+[%provenance%]
+-->
+<html lang="en">
+ <meta charset="utf-8">
+ <title>HTTP headers on request for CSS font-face</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/fetch/metadata/resources/helper.sub.js"></script>
+ <body>
+ <script>
+ 'use strict';
+
+ let count = 0;
+
+ function induceRequest(t, url) {
+ const id = `el-${count += 1}`;
+ const style = document.createElement('style');
+ style.appendChild(document.createTextNode(`
+ @font-face {
+ font-family: wpt-font-family${id};
+ src: url(${url});
+ }
+ #el-${id} {
+ font-family: wpt-font-family${id};
+ }
+ `));
+ const div = document.createElement('div');
+ div.setAttribute('id', 'el-' + id);
+ div.appendChild(style);
+ div.appendChild(document.createTextNode('x'));
+ document.body.appendChild(div);
+
+ t.add_cleanup(() => div.remove());
+
+ return document.fonts.ready;
+ }
+
+ {%- for subtest in subtests %}
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(t, makeRequestURL(key, [% subtest.origins %]))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ {%- if subtest.expected == none %}
+ assert_not_own_property(headers, '[%subtest.headerName%]');
+ {%- else %}
+ assert_own_property(headers, '[%subtest.headerName%]');
+ assert_array_equals(headers['[%subtest.headerName%]'], ['[%subtest.expected%]']);
+ {%- endif %}
+ });
+ }, '[%subtest.headerName%][%subtest.description | pad("start", " - ")%]');
+
+ {%- endfor %}
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/fetch/metadata/tools/templates/css-images.sub.html b/test/wpt/tests/fetch/metadata/tools/templates/css-images.sub.html
new file mode 100644
index 0000000..e394f9f
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/tools/templates/css-images.sub.html
@@ -0,0 +1,137 @@
+<!DOCTYPE html>
+<!--
+[%provenance%]
+-->
+<html lang="en">
+ <meta charset="utf-8">
+ {%- if subtests|length > 10 %}
+ <meta name="timeout" content="long">
+ {%- endif %}
+ <title>HTTP headers on request for CSS image-accepting properties</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/fetch/metadata/resources/helper.sub.js"></script>
+ <body>
+ <script>
+ 'use strict';
+
+ /**
+ * The subtests in this file use an iframe to induce requests for CSS
+ * resources because an iframe's `onload` event is the most direct and
+ * generic mechanism to detect loading of CSS resources. As an optimization,
+ * the subtests share the same iframe and document.
+ */
+ const declarations = [];
+ const iframe = document.createElement('iframe');
+ const whenIframeReady = new Promise((resolve, reject) => {
+ iframe.onload = resolve;
+ iframe.onerror = reject;
+ });
+
+ {%- for subtest in subtests %}
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, [% subtest.origins %]);
+
+ declarations.push(`background-image: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ {%- if subtest.expected == none %}
+ assert_not_own_property(headers, '[%subtest.headerName%]');
+ {%- else %}
+ assert_own_property(headers, '[%subtest.headerName%]');
+ assert_equals(headers['[%subtest.headerName%]'], '[%subtest.expected%]');
+ {%- endif %}
+ })
+ .then(t.step_func_done(), (error) => t.unreached_func());
+ }, 'background-image [%subtest.headerName%][%subtest.description | pad("start", " - ")%]');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, [% subtest.origins %]);
+
+ declarations.push(`border-image: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ {%- if subtest.expected == none %}
+ assert_not_own_property(headers, '[%subtest.headerName%]');
+ {%- else %}
+ assert_own_property(headers, '[%subtest.headerName%]');
+ assert_array_equals(headers['[%subtest.headerName%]'], ['[%subtest.expected%]']);
+ {%- endif %}
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'border-image [%subtest.headerName%][%subtest.description | pad("start", " - ")%]');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, [% subtest.origins %]);
+
+ declarations.push(`content: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ {%- if subtest.expected == none %}
+ assert_not_own_property(headers, '[%subtest.headerName%]');
+ {%- else %}
+ assert_own_property(headers, '[%subtest.headerName%]');
+ assert_array_equals(headers['[%subtest.headerName%]'], ['[%subtest.expected%]']);
+ {%- endif %}
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'content [%subtest.headerName%][%subtest.description | pad("start", " - ")%]');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, [% subtest.origins %]);
+
+ declarations.push(`cursor: url("${url}"), auto;`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ {%- if subtest.expected == none %}
+ assert_not_own_property(headers, '[%subtest.headerName%]');
+ {%- else %}
+ assert_own_property(headers, '[%subtest.headerName%]');
+ assert_array_equals(headers['[%subtest.headerName%]'], ['[%subtest.expected%]']);
+ {%- endif %}
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'cursor [%subtest.headerName%][%subtest.description | pad("start", " - ")%]');
+
+ async_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, [% subtest.origins %]);
+
+ declarations.push(`list-style-image: url("${url}");`);
+
+ whenIframeReady
+ .then(() => retrieve(key))
+ .then((headers) => {
+ {%- if subtest.expected == none %}
+ assert_not_own_property(headers, '[%subtest.headerName%]');
+ {%- else %}
+ assert_own_property(headers, '[%subtest.headerName%]');
+ assert_array_equals(headers['[%subtest.headerName%]'], ['[%subtest.expected%]']);
+ {%- endif %}
+ })
+ .then(t.step_func_done(), t.unreached_func());
+ }, 'list-style-image [%subtest.headerName%][%subtest.description | pad("start", " - ")%]');
+
+ {%- endfor %}
+
+ iframe.srcdoc = declarations.map((declaration, index) => `
+ <style>.el${index} { ${declaration} }</style><div class="el${index}"></div>`
+ ).join('');
+ document.body.appendChild(iframe);
+
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/fetch/metadata/tools/templates/element-a.sub.html b/test/wpt/tests/fetch/metadata/tools/templates/element-a.sub.html
new file mode 100644
index 0000000..2bd8e8a
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/tools/templates/element-a.sub.html
@@ -0,0 +1,72 @@
+<!DOCTYPE html>
+<!--
+[%provenance%]
+-->
+<html lang="en">
+ <meta charset="utf-8">
+ {%- if subtests|length > 10 %}
+ <meta name="timeout" content="long">
+ {%- endif %}
+ <title>HTTP headers on request for HTML "a" element navigation</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ {%- if subtests|selectattr('userActivated')|list %}
+ <script src="/resources/testdriver.js"></script>
+ <script src="/resources/testdriver-vendor.js"></script>
+ {%- endif %}
+ <script src="/fetch/metadata/resources/helper.sub.js"></script>
+ <body>
+ <script>
+ 'use strict';
+
+ function induceRequest(url, {test, userActivated, attributes}) {
+ const win = window.open();
+ const anchor = win.document.createElement('a');
+ anchor.setAttribute('href', url);
+
+ for (const [ name, value ] of Object.entries(attributes)) {
+ anchor.setAttribute(name, value);
+ }
+
+ win.document.body.appendChild(anchor);
+
+ test.add_cleanup(() => win.close());
+
+ if (userActivated) {
+ test_driver.bless('enable user activation', () => anchor.click());
+ } else {
+ anchor.click();
+ }
+ }
+
+ {%- for subtest in subtests %}
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ makeRequestURL(key, [% subtest.origins %], {mime: 'text/html'}),
+ {
+ test: t,
+ userActivated: [%subtest.userActivated | default(false) | tojson%],
+ attributes: [%subtest.elementAttrs | default({}) | tojson%]
+ }
+ );
+
+ // `induceRequest` does not necessarily trigger a navigation, so the Python
+ // handler must be polled until it has received the initial request.
+ return retrieve(key, {poll: true})
+ .then((headers) => {
+ {%- if subtest.expected == none %}
+ assert_not_own_property(headers, '[%subtest.headerName%]');
+ {%- else %}
+ assert_own_property(headers, '[%subtest.headerName%]');
+ assert_array_equals(headers['[%subtest.headerName%]'], ['[%subtest.expected%]']);
+ {%- endif %}
+ });
+ }, '[%subtest.headerName%][%subtest.description | pad("start", " - ")%] - [%subtest.elementAttrs | collection("attributes")%][% " with user activation" if subtest.userActivated%]');
+
+ {%- endfor %}
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/fetch/metadata/tools/templates/element-area.sub.html b/test/wpt/tests/fetch/metadata/tools/templates/element-area.sub.html
new file mode 100644
index 0000000..0cef5b2
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/tools/templates/element-area.sub.html
@@ -0,0 +1,72 @@
+<!DOCTYPE html>
+<!--
+[%provenance%]
+-->
+<html lang="en">
+ <meta charset="utf-8">
+ {%- if subtests|length > 10 %}
+ <meta name="timeout" content="long">
+ {%- endif %}
+ <title>HTTP headers on request for HTML "area" element navigation</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ {%- if subtests|selectattr('userActivated')|list %}
+ <script src="/resources/testdriver.js"></script>
+ <script src="/resources/testdriver-vendor.js"></script>
+ {%- endif %}
+ <script src="/fetch/metadata/resources/helper.sub.js"></script>
+ <body>
+ <script>
+ 'use strict';
+
+ function induceRequest(url, {test, userActivated, attributes}) {
+ const win = window.open();
+ const area = win.document.createElement('area');
+ area.setAttribute('href', url);
+
+ for (const [ name, value ] of Object.entries(attributes)) {
+ area.setAttribute(name, value);
+ }
+
+ win.document.body.appendChild(area);
+
+ test.add_cleanup(() => win.close());
+
+ if (userActivated) {
+ test_driver.bless('enable user activation', () => area.click());
+ } else {
+ area.click();
+ }
+ }
+
+ {%- for subtest in subtests %}
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ makeRequestURL(key, [% subtest.origins %], {mime: 'text/html'}),
+ {
+ test: t,
+ userActivated: [%subtest.userActivated | default(false) | tojson%],
+ attributes: [%subtest.elementAttrs | default({}) | tojson%]
+ }
+ );
+
+ // `induceRequest` does not necessarily trigger a navigation, so the Python
+ // handler must be polled until it has received the initial request.
+ return retrieve(key, {poll: true})
+ .then((headers) => {
+ {%- if subtest.expected == none %}
+ assert_not_own_property(headers, '[%subtest.headerName%]');
+ {%- else %}
+ assert_own_property(headers, '[%subtest.headerName%]');
+ assert_array_equals(headers['[%subtest.headerName%]'], ['[%subtest.expected%]']);
+ {%- endif %}
+ });
+ }, '[%subtest.headerName%][%subtest.description | pad("start", " - ")%] - [%subtest.elementAttrs | collection("attributes")%][% " with user activation" if subtest.userActivated%]');
+
+ {%- endfor %}
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/fetch/metadata/tools/templates/element-audio.sub.html b/test/wpt/tests/fetch/metadata/tools/templates/element-audio.sub.html
new file mode 100644
index 0000000..92bc221
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/tools/templates/element-audio.sub.html
@@ -0,0 +1,51 @@
+<!DOCTYPE html>
+<!--
+[%provenance%]
+-->
+<html lang="en">
+ <meta charset="utf-8">
+ <title>HTTP headers on request for HTML "audio" element source</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/fetch/metadata/resources/helper.sub.js"></script>
+ <body>
+ <script>
+ 'use strict';
+
+ function induceRequest(url, attributes) {
+ const audio = document.createElement('audio');
+
+ for (const [ name, value ] of Object.entries(attributes)) {
+ audio.setAttribute(name, value);
+ }
+
+ return new Promise((resolve) => {
+ audio.setAttribute('src', url);
+ audio.onload = audio.onerror = resolve;
+ });
+ }
+
+ {%- for subtest in subtests %}
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, [% subtest.origins %]),
+ [%subtest.elementAttrs | default({}) | tojson%]
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ {%- if subtest.expected == none %}
+ assert_not_own_property(headers, '[%subtest.headerName%]');
+ {%- else %}
+ assert_own_property(headers, '[%subtest.headerName%]');
+ assert_array_equals(headers['[%subtest.headerName%]'], ['[%subtest.expected%]']);
+ {%- endif %}
+ });
+ }, '[%subtest.headerName%] - [%subtest.description | pad("end", ", ")%][%subtest.elementAttrs | collection("attributes")%]');
+
+ {%- endfor %}
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/fetch/metadata/tools/templates/element-embed.sub.html b/test/wpt/tests/fetch/metadata/tools/templates/element-embed.sub.html
new file mode 100644
index 0000000..18ce09e
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/tools/templates/element-embed.sub.html
@@ -0,0 +1,54 @@
+<!DOCTYPE html>
+<!--
+[%provenance%]
+-->
+<html lang="en">
+ <meta charset="utf-8">
+ <title>HTTP headers on request for HTML "embed" element source</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/fetch/metadata/resources/helper.sub.js"></script>
+ <body>
+ <script>
+ 'use strict';
+
+ const params = {
+ body: `
+ <svg xmlns="http://www.w3.org/2000/svg" width="123" height="123">
+ <rect fill="lime" width="123" height="123"/>
+ </svg>
+ `,
+ mime: 'image/svg+xml'
+ };
+
+ function induceRequest(t, url) {
+ const embed = document.createElement('embed');
+ embed.setAttribute('src', url);
+ document.body.appendChild(embed);
+
+ t.add_cleanup(() => embed.remove());
+
+ return new Promise((resolve) => embed.addEventListener('load', resolve));
+ }
+
+ {%- for subtest in subtests %}
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(t, makeRequestURL(key, [% subtest.origins %], params))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ {%- if subtest.expected == none %}
+ assert_not_own_property(headers, '[%subtest.headerName%]');
+ {%- else %}
+ assert_own_property(headers, '[%subtest.headerName%]');
+ assert_array_equals(headers['[%subtest.headerName%]'], ['[%subtest.expected%]']);
+ {%- endif %}
+ });
+ }, '[%subtest.headerName%][%subtest.description | pad("start", " - ")%]');
+
+ {%- endfor %}
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/fetch/metadata/tools/templates/element-frame.sub.html b/test/wpt/tests/fetch/metadata/tools/templates/element-frame.sub.html
new file mode 100644
index 0000000..ce90171
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/tools/templates/element-frame.sub.html
@@ -0,0 +1,62 @@
+<!DOCTYPE html>
+<!--
+[%provenance%]
+-->
+<html lang="en">
+ <meta charset="utf-8">
+ <title>HTTP headers on request for HTML "frame" element source</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ {%- if subtests|selectattr('userActivated')|list %}
+ <script src="/resources/testdriver.js"></script>
+ <script src="/resources/testdriver-vendor.js"></script>
+ {%- endif %}
+ <script src="/fetch/metadata/resources/helper.sub.js"></script>
+ <body>
+ <script>
+ 'use strict';
+
+ function induceRequest(url, test, userActivated) {
+ const frame = document.createElement('frame');
+
+ const setSrc = () => frame.setAttribute('src', url);
+
+ document.body.appendChild(frame);
+ test.add_cleanup(() => frame.remove());
+
+ return new Promise((resolve) => {
+ if (userActivated) {
+ test_driver.bless('enable user activation', setSrc);
+ } else {
+ setSrc();
+ }
+
+ frame.onload = frame.onerror = resolve;
+ });
+ }
+
+ {%- for subtest in subtests %}
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, [% subtest.origins %], {mime: 'text/html'}),
+ t,
+ [%subtest.userActivated | default(false) | tojson%]
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ {%- if subtest.expected == none %}
+ assert_not_own_property(headers, '[%subtest.headerName%]');
+ {%- else %}
+ assert_own_property(headers, '[%subtest.headerName%]');
+ assert_array_equals(headers['[%subtest.headerName%]'], ['[%subtest.expected%]']);
+ {%- endif %}
+ });
+ }, '[%subtest.headerName%][%subtest.description | pad("start", " - ")%][% " with user activation" if subtest.userActivated%]');
+
+ {%- endfor %}
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/fetch/metadata/tools/templates/element-iframe.sub.html b/test/wpt/tests/fetch/metadata/tools/templates/element-iframe.sub.html
new file mode 100644
index 0000000..43a632a
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/tools/templates/element-iframe.sub.html
@@ -0,0 +1,62 @@
+<!DOCTYPE html>
+<!--
+[%provenance%]
+-->
+<html lang="en">
+ <meta charset="utf-8">
+ <title>HTTP headers on request for HTML "frame" element source</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ {%- if subtests|selectattr('userActivated')|list %}
+ <script src="/resources/testdriver.js"></script>
+ <script src="/resources/testdriver-vendor.js"></script>
+ {%- endif %}
+ <script src="/fetch/metadata/resources/helper.sub.js"></script>
+ <body>
+ <script>
+ 'use strict';
+
+ function induceRequest(url, test, userActivated) {
+ const iframe = document.createElement('iframe');
+
+ const setSrc = () => iframe.setAttribute('src', url);
+
+ document.body.appendChild(iframe);
+ test.add_cleanup(() => iframe.remove());
+
+ return new Promise((resolve) => {
+ if (userActivated) {
+ test_driver.bless('enable user activation', setSrc);
+ } else {
+ setSrc();
+ }
+
+ iframe.onload = iframe.onerror = resolve;
+ });
+ }
+
+ {%- for subtest in subtests %}
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, [% subtest.origins %], {mime: 'text/html'}),
+ t,
+ [%subtest.userActivated | default(false) | tojson%]
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ {%- if subtest.expected == none %}
+ assert_not_own_property(headers, '[%subtest.headerName%]');
+ {%- else %}
+ assert_own_property(headers, '[%subtest.headerName%]');
+ assert_array_equals(headers['[%subtest.headerName%]'], ['[%subtest.expected%]']);
+ {%- endif %}
+ });
+ }, '[%subtest.headerName%][%subtest.description | pad("start", " - ")%][% " with user activation" if subtest.userActivated%]');
+
+ {%- endfor %}
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/fetch/metadata/tools/templates/element-img-environment-change.sub.html b/test/wpt/tests/fetch/metadata/tools/templates/element-img-environment-change.sub.html
new file mode 100644
index 0000000..5a65114
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/tools/templates/element-img-environment-change.sub.html
@@ -0,0 +1,78 @@
+<!DOCTYPE html>
+<!--
+[%provenance%]
+-->
+<html lang="en">
+ <meta charset="utf-8">
+ <title>HTTP headers on image request triggered by change to environment</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/fetch/metadata/resources/helper.sub.js"></script>
+ <body>
+ <script>
+ 'use strict';
+
+ // The response to the request under test must describe a valid image
+ // resource in order for the `load` event to be fired.
+ const params = {
+ body: `
+ <svg xmlns="http://www.w3.org/2000/svg" width="123" height="123">
+ <rect fill="lime" width="123" height="123"/>
+ </svg>
+ `,
+ mime: 'image/svg+xml'
+ };
+
+ function induceRequest(t, url, attributes) {
+ const iframe = document.createElement('iframe');
+ iframe.style.width = '50px';
+ document.body.appendChild(iframe);
+ t.add_cleanup(() => iframe.remove());
+ iframe.contentDocument.open();
+ iframe.contentDocument.close();
+
+ const image = iframe.contentDocument.createElement('img');
+ for (const [ name, value ] of Object.entries(attributes)) {
+ image.setAttribute(name, value);
+ }
+ iframe.contentDocument.body.appendChild(image);
+
+ image.setAttribute('srcset', `${url} 100w, /media/1x1-green.png 1w`);
+ image.setAttribute('sizes', '(max-width: 100px) 1px, (min-width: 150px) 123px');
+
+ return new Promise((resolve) => {
+ image.onload = image.onerror = resolve;
+ })
+ .then(() => {
+
+ iframe.style.width = '200px';
+
+ return new Promise((resolve) => image.onload = resolve);
+ });
+ }
+
+ {%- for subtest in subtests %}
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ t,
+ makeRequestURL(key, [% subtest.origins %], params),
+ [%subtest.elementAttrs | default({}) | tojson%]
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ {%- if subtest.expected == none %}
+ assert_not_own_property(headers, '[%subtest.headerName%]');
+ {%- else %}
+ assert_own_property(headers, '[%subtest.headerName%]');
+ assert_array_equals(headers['[%subtest.headerName%]'], ['[%subtest.expected%]']);
+ {%- endif %}
+ });
+ }, '[%subtest.headerName%] - [%subtest.description | pad("end", ", ")%][%subtest.elementAttrs | collection("attributes")%]');
+
+ {%- endfor %}
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/fetch/metadata/tools/templates/element-img.sub.html b/test/wpt/tests/fetch/metadata/tools/templates/element-img.sub.html
new file mode 100644
index 0000000..1dac584
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/tools/templates/element-img.sub.html
@@ -0,0 +1,52 @@
+<!DOCTYPE html>
+<!--
+[%provenance%]
+-->
+<html lang="en">
+ <meta charset="utf-8">
+ <title>HTTP headers on request for HTML "img" element source</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/fetch/metadata/resources/helper.sub.js"></script>
+ <body>
+ <script>
+ 'use strict';
+
+ function induceRequest(url, sourceAttr, attributes) {
+ const image = document.createElement('img');
+
+ for (const [ name, value ] of Object.entries(attributes)) {
+ image.setAttribute(name, value);
+ }
+
+ return new Promise((resolve) => {
+ image.setAttribute(sourceAttr, url);
+ image.onload = image.onerror = resolve;
+ });
+ }
+
+ {%- for subtest in subtests %}
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, [% subtest.origins %]),
+ '[%subtest.sourceAttr%]',
+ [%subtest.elementAttrs | default({}) | tojson%]
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ {%- if subtest.expected == none %}
+ assert_not_own_property(headers, '[%subtest.headerName%]');
+ {%- else %}
+ assert_own_property(headers, '[%subtest.headerName%]');
+ assert_array_equals(headers['[%subtest.headerName%]'], ['[%subtest.expected%]']);
+ {%- endif %}
+ });
+ }, '[%subtest.headerName%] - [%subtest.sourceAttr%] - [%subtest.description | pad("end", ", ")%][%subtest.elementAttrs | collection("attributes")%]');
+
+ {%- endfor %}
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/fetch/metadata/tools/templates/element-input-image.sub.html b/test/wpt/tests/fetch/metadata/tools/templates/element-input-image.sub.html
new file mode 100644
index 0000000..3c50008
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/tools/templates/element-input-image.sub.html
@@ -0,0 +1,48 @@
+<!DOCTYPE html>
+<!--
+[%provenance%]
+-->
+<html lang="en">
+ <meta charset="utf-8">
+ <title>HTTP headers on request for HTML "input" element with type="button"</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/fetch/metadata/resources/helper.sub.js"></script>
+ <body>
+ <script>
+ 'use strict';
+
+ function induceRequest(url, test) {
+ const input = document.createElement('input');
+ input.setAttribute('type', 'image');
+
+ document.body.appendChild(input);
+ test.add_cleanup(() => input.remove());
+
+ return new Promise((resolve) => {
+ input.onload = input.onerror = resolve;
+ input.setAttribute('src', url);
+ });
+ }
+
+ {%- for subtest in subtests %}
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(makeRequestURL(key, [% subtest.origins %]), t)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ {%- if subtest.expected == none %}
+ assert_not_own_property(headers, '[%subtest.headerName%]');
+ {%- else %}
+ assert_own_property(headers, '[%subtest.headerName%]');
+ assert_array_equals(headers['[%subtest.headerName%]'], ['[%subtest.expected%]']);
+ {%- endif %}
+ });
+ }, '[%subtest.headerName%] - [%subtest.description | pad("end", ", ")%][%subtest.elementAttrs | collection("attributes")%]');
+
+ {%- endfor %}
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/fetch/metadata/tools/templates/element-link-icon.sub.html b/test/wpt/tests/fetch/metadata/tools/templates/element-link-icon.sub.html
new file mode 100644
index 0000000..18ce12a
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/tools/templates/element-link-icon.sub.html
@@ -0,0 +1,75 @@
+<!DOCTYPE html>
+<!--
+[%provenance%]
+-->
+<html lang="en">
+ <meta charset="utf-8">
+ {%- if subtests|length > 10 %}
+ <meta name="timeout" content="long">
+ {%- endif %}
+ <title>HTTP headers on request for HTML "link" element with rel="icon"</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/fetch/metadata/resources/helper.sub.js"></script>
+ <body>
+ <script>
+ 'use strict';
+
+ const params = {
+ body: `
+ <svg xmlns="http://www.w3.org/2000/svg" width="123" height="123">
+ <rect fill="lime" width="123" height="123"/>
+ </svg>
+ `,
+ mime: 'image/svg+xml'
+ };
+
+ /**
+ * The `link` element supports a `load` event. That event would reliably
+ * indicate that the browser had received the request. Multiple major
+ * browsers do not implement the event, however, so in order to promote the
+ * visibility of this test, a less efficient polling-based detection
+ * mechanism is used.
+ *
+ * https://bugzilla.mozilla.org/show_bug.cgi?id=1638188
+ * https://bugs.chromium.org/p/chromium/issues/detail?id=1083034
+ */
+ function induceRequest(t, url, attributes) {
+ const link = document.createElement('link');
+ link.setAttribute('rel', 'icon');
+ link.setAttribute('href', url);
+
+ for (const [ name, value ] of Object.entries(attributes)) {
+ link.setAttribute(name, value);
+ }
+
+ document.head.appendChild(link);
+ t.add_cleanup(() => link.remove());
+ }
+
+ {%- for subtest in subtests %}
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ t,
+ makeRequestURL(key, [% subtest.origins %], params),
+ [%subtest.elementAttrs | default({}) | tojson%]
+ );
+
+ return retrieve(key, {poll:true})
+ .then((headers) => {
+ {%- if subtest.expected == none %}
+ assert_not_own_property(headers, '[%subtest.headerName%]');
+ {%- else %}
+ assert_own_property(headers, '[%subtest.headerName%]');
+ assert_array_equals(headers['[%subtest.headerName%]'], ['[%subtest.expected%]']);
+ {%- endif %}
+ });
+ }, '[%subtest.headerName%][%subtest.description | pad("start", " - ")%] [%subtest.elementAttrs | collection("attributes")%]');
+
+ {%- endfor %}
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/fetch/metadata/tools/templates/element-link-prefetch.optional.sub.html b/test/wpt/tests/fetch/metadata/tools/templates/element-link-prefetch.optional.sub.html
new file mode 100644
index 0000000..59d677d
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/tools/templates/element-link-prefetch.optional.sub.html
@@ -0,0 +1,71 @@
+<!DOCTYPE html>
+<!--
+[%provenance%]
+-->
+<html lang="en">
+ <meta charset="utf-8">
+ {%- if subtests|length > 10 %}
+ <meta name="timeout" content="long">
+ {%- endif %}
+ <title>HTTP headers on request for HTML "link" element with rel="prefetch"</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/fetch/metadata/resources/helper.sub.js"></script>
+ <body>
+ <script>
+ 'use strict';
+
+ /**
+ * The `link` element supports a `load` event. That event would reliably
+ * indicate that the browser had received the request. Multiple major
+ * browsers do not implement the event, however, so in order to promote the
+ * visibility of this test, a less efficient polling-based detection
+ * mechanism is used.
+ *
+ * https://bugzilla.mozilla.org/show_bug.cgi?id=1638188
+ * https://bugs.chromium.org/p/chromium/issues/detail?id=1083034
+ */
+ function induceRequest(t, url, attributes) {
+ const link = document.createElement('link');
+ link.setAttribute('rel', 'prefetch');
+ link.setAttribute('href', url);
+
+ for (const [ name, value ] of Object.entries(attributes)) {
+ link.setAttribute(name, value);
+ }
+
+ document.head.appendChild(link);
+ t.add_cleanup(() => link.remove());
+ }
+
+ setup(() => {
+ assert_implements_optional(document.createElement('link').relList.supports('prefetch'));
+ });
+
+ {%- for subtest in subtests %}
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ induceRequest(
+ t,
+ makeRequestURL(key, [% subtest.origins %]),
+ [%subtest.elementAttrs | default({}) | tojson%]
+ );
+
+ return retrieve(key, {poll:true})
+ .then((headers) => {
+ {%- if subtest.expected == none %}
+ assert_not_own_property(headers, '[%subtest.headerName%]');
+ {%- else %}
+ assert_own_property(headers, '[%subtest.headerName%]');
+ assert_array_equals(headers['[%subtest.headerName%]'], ['[%subtest.expected%]']);
+ {%- endif %}
+ });
+ }, '[%subtest.headerName%][%subtest.description | pad("start", " - ")%] [%subtest.elementAttrs | collection("attributes")%]');
+
+ {%- endfor %}
+ </script>
+ </body>
+</html>
+
diff --git a/test/wpt/tests/fetch/metadata/tools/templates/element-meta-refresh.optional.sub.html b/test/wpt/tests/fetch/metadata/tools/templates/element-meta-refresh.optional.sub.html
new file mode 100644
index 0000000..5a8d8f8
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/tools/templates/element-meta-refresh.optional.sub.html
@@ -0,0 +1,60 @@
+<!DOCTYPE html>
+<!--
+[%provenance%]
+-->
+<html lang="en">
+ <meta charset="utf-8">
+ <title>HTTP headers on request for HTML "meta" element with http-equiv="refresh"</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/fetch/metadata/resources/helper.sub.js"></script>
+ <body>
+ <script>
+ 'use strict';
+
+ function induceRequest(url, test) {
+ const win = window.open();
+ test.add_cleanup(() => win.close());
+
+ win.document.open();
+ win.document.write(
+ `<meta http-equiv="Refresh" content="0; URL=${url}">`
+ );
+ win.document.close();
+
+ return new Promise((resolve) => {
+ addEventListener('message', (event) => {
+ if (event.source === win) {
+ resolve();
+ }
+ });
+ });
+ }
+
+ const responseParams = {
+ mime: 'text/html',
+ body: `<script>opener.postMessage(0, '*')</${''}script>`
+ };
+ {%- for subtest in subtests %}
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, [% subtest.origins %], responseParams), t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ {%- if subtest.expected == none %}
+ assert_not_own_property(headers, '[%subtest.headerName%]');
+ {%- else %}
+ assert_own_property(headers, '[%subtest.headerName%]');
+ assert_array_equals(headers['[%subtest.headerName%]'], ['[%subtest.expected%]']);
+ {%- endif %}
+ });
+ }, '[%subtest.headerName%][%subtest.description | pad("start", " - ")%]');
+
+ {%- endfor %}
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/fetch/metadata/tools/templates/element-picture.sub.html b/test/wpt/tests/fetch/metadata/tools/templates/element-picture.sub.html
new file mode 100644
index 0000000..903aeed
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/tools/templates/element-picture.sub.html
@@ -0,0 +1,101 @@
+<!DOCTYPE html>
+<!--
+[%provenance%]
+-->
+<html lang="en">
+ <meta charset="utf-8">
+ <title>HTTP headers on request for HTML "picture" element source</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/fetch/metadata/resources/helper.sub.js"></script>
+ <body>
+ <script>
+ 'use strict';
+
+ function induceRequest(url, sourceEl, sourceAttr, attributes) {
+ const picture = document.createElement('picture');
+ const els = {
+ img: document.createElement('img'),
+ source: document.createElement('source')
+ };
+ picture.appendChild(els.source);
+ picture.appendChild(els.img);
+ document.body.appendChild(picture);
+
+ for (const [ name, value ] of Object.entries(attributes)) {
+ els.img.setAttribute(name, value);
+ }
+
+ return new Promise((resolve) => {
+ els[sourceEl].setAttribute(sourceAttr, url);
+ els.img.onload = els.img.onerror = resolve;
+ });
+ }
+
+ {%- for subtest in subtests %}
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, [% subtest.origins %]),
+ 'img',
+ 'src',
+ [%subtest.elementAttrs | default({}) | tojson%]
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ {%- if subtest.expected == none %}
+ assert_not_own_property(headers, '[%subtest.headerName%]');
+ {%- else %}
+ assert_own_property(headers, '[%subtest.headerName%]');
+ assert_array_equals(headers['[%subtest.headerName%]'], ['[%subtest.expected%]']);
+ {%- endif %}
+ });
+ }, '[%subtest.headerName%] - img[src] - [%subtest.description | pad("end", ", ")%][%subtest.elementAttrs | collection("attributes")%]');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, [% subtest.origins %]),
+ 'img',
+ 'srcset',
+ [%subtest.elementAttrs | default({}) | tojson%]
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ {%- if subtest.expected == none %}
+ assert_not_own_property(headers, '[%subtest.headerName%]');
+ {%- else %}
+ assert_own_property(headers, '[%subtest.headerName%]');
+ assert_array_equals(headers['[%subtest.headerName%]'], ['[%subtest.expected%]']);
+ {%- endif %}
+ });
+ }, '[%subtest.headerName%] - img[srcset] - [%subtest.description | pad("end", ", ")%][%subtest.elementAttrs | collection("attributes")%]');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, [% subtest.origins %]),
+ 'source',
+ 'srcset',
+ [%subtest.elementAttrs | default({}) | tojson%]
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ {%- if subtest.expected == none %}
+ assert_not_own_property(headers, '[%subtest.headerName%]');
+ {%- else %}
+ assert_own_property(headers, '[%subtest.headerName%]');
+ assert_array_equals(headers['[%subtest.headerName%]'], ['[%subtest.expected%]']);
+ {%- endif %}
+ });
+ }, '[%subtest.headerName%] - source[srcset] - [%subtest.description | pad("end", ", ")%][%subtest.elementAttrs | collection("attributes")%]');
+
+ {%- endfor %}
+ </script>
+ </body>
+</html>
+
diff --git a/test/wpt/tests/fetch/metadata/tools/templates/element-script.sub.html b/test/wpt/tests/fetch/metadata/tools/templates/element-script.sub.html
new file mode 100644
index 0000000..4a281ae
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/tools/templates/element-script.sub.html
@@ -0,0 +1,54 @@
+<!DOCTYPE html>
+<!--
+[%provenance%]
+-->
+<html lang="en">
+ <meta charset="utf-8">
+ <title>HTTP headers on request for HTML "script" element source</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/fetch/metadata/resources/helper.sub.js"></script>
+ <body>
+ <script>
+ 'use strict';
+
+ function induceRequest(url, attributes) {
+ const script = document.createElement('script');
+ script.setAttribute('src', url);
+
+ for (const [ name, value ] of Object.entries(attributes)) {
+ script.setAttribute(name, value);
+ }
+
+ return new Promise((resolve, reject) => {
+ script.onload = resolve;
+ script.onerror = () => reject('Failed to load script');
+ document.body.appendChild(script);
+ })
+ .then(() => script.remove());
+ }
+ {%- for subtest in subtests %}
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, [% subtest.origins %], { mime: 'application/javascript' }
+ );
+
+ return induceRequest(url,
+ [%subtest.elementAttrs | default({}) | tojson%]
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ {%- if subtest.expected == none %}
+ assert_not_own_property(headers, '[%subtest.headerName%]');
+ {%- else %}
+ assert_own_property(headers, '[%subtest.headerName%]');
+ assert_array_equals(headers['[%subtest.headerName%]'], ['[%subtest.expected%]']);
+ {%- endif %}
+ });
+ }, '[%subtest.headerName%] - [%subtest.description | pad("end", ", ")%][%-subtest.elementAttrs | collection("attributes")%]');
+
+ {%- endfor %}
+ </script>
+</html>
diff --git a/test/wpt/tests/fetch/metadata/tools/templates/element-video-poster.sub.html b/test/wpt/tests/fetch/metadata/tools/templates/element-video-poster.sub.html
new file mode 100644
index 0000000..9cdaf06
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/tools/templates/element-video-poster.sub.html
@@ -0,0 +1,62 @@
+<!DOCTYPE html>
+<!--
+[%provenance%]
+-->
+<html lang="en">
+ <meta charset="utf-8">
+ <title>HTTP headers on request for HTML "video" element "poster"</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/fetch/metadata/resources/helper.sub.js"></script>
+ <body>
+ <script>
+ 'use strict';
+
+ const params = {
+ body: `
+ <svg xmlns="http://www.w3.org/2000/svg" width="123" height="123">
+ <rect fill="lime" width="123" height="123"/>
+ </svg>
+ `,
+ mime: 'image/svg+xml'
+ };
+
+ function induceRequest(t, url) {
+ var video = document.createElement('video');
+ video.setAttribute('poster', url);
+ document.body.appendChild(video);
+
+ const poll = () => {
+ if (video.clientWidth === 123) {
+ return;
+ }
+
+ return new Promise((resolve) => t.step_timeout(resolve, 0))
+ .then(poll);
+ };
+ t.add_cleanup(() => video.remove());
+
+ return poll();
+ }
+
+ {%- for subtest in subtests %}
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(t, makeRequestURL(key, [% subtest.origins %], params))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ {%- if subtest.expected == none %}
+ assert_not_own_property(headers, '[%subtest.headerName%]');
+ {%- else %}
+ assert_own_property(headers, '[%subtest.headerName%]');
+ assert_array_equals(headers['[%subtest.headerName%]'], ['[%subtest.expected%]']);
+ {%- endif %}
+ });
+ }, '[%subtest.headerName%][%subtest.description | pad("start", " - ")%]');
+
+ {%- endfor %}
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/fetch/metadata/tools/templates/element-video.sub.html b/test/wpt/tests/fetch/metadata/tools/templates/element-video.sub.html
new file mode 100644
index 0000000..1b7b976
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/tools/templates/element-video.sub.html
@@ -0,0 +1,51 @@
+<!DOCTYPE html>
+<!--
+[%provenance%]
+-->
+<html lang="en">
+ <meta charset="utf-8">
+ <title>HTTP headers on request for HTML "video" element source</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/fetch/metadata/resources/helper.sub.js"></script>
+ <body>
+ <script>
+ 'use strict';
+
+ function induceRequest(url, attributes) {
+ const video = document.createElement('video');
+
+ for (const [ name, value ] of Object.entries(attributes)) {
+ video.setAttribute(name, value);
+ }
+
+ return new Promise((resolve) => {
+ video.setAttribute('src', url);
+ video.onload = video.onerror = resolve;
+ });
+ }
+
+ {%- for subtest in subtests %}
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, [% subtest.origins %]),
+ [%subtest.elementAttrs | default({}) | tojson%]
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ {%- if subtest.expected == none %}
+ assert_not_own_property(headers, '[%subtest.headerName%]');
+ {%- else %}
+ assert_own_property(headers, '[%subtest.headerName%]');
+ assert_array_equals(headers['[%subtest.headerName%]'], ['[%subtest.expected%]']);
+ {%- endif %}
+ });
+ }, '[%subtest.headerName%] - [%subtest.description | pad("end", ", ")%][%subtest.elementAttrs | collection("attributes")%]');
+
+ {%- endfor %}
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/fetch/metadata/tools/templates/fetch-via-serviceworker.https.sub.html b/test/wpt/tests/fetch/metadata/tools/templates/fetch-via-serviceworker.https.sub.html
new file mode 100644
index 0000000..eead710
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/tools/templates/fetch-via-serviceworker.https.sub.html
@@ -0,0 +1,88 @@
+<!DOCTYPE html>
+<!--
+[%provenance%]
+-->
+<html lang="en">
+ <meta charset="utf-8">
+ {%- if subtests|length > 10 %}
+ <meta name="timeout" content="long">
+ {%- endif %}
+ <title>HTTP headers on request using the "fetch" API and passing through a Serive Worker</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script>
+ <script src="/fetch/metadata/resources/helper.sub.js"></script>
+ <body>
+ <script>
+ 'use strict';
+
+ const scripts = {
+ fallback: '/fetch/metadata/resources/fetch-via-serviceworker--fallback--sw.js',
+ respondWith: '/fetch/metadata/resources/fetch-via-serviceworker--respondWith--sw.js'
+ };
+
+ function induceRequest(t, url, init, script) {
+ const SCOPE = '/fetch/metadata/resources/fetch-via-serviceworker-frame.html';
+ const SCRIPT = scripts[script];
+
+ return service_worker_unregister_and_register(t, SCRIPT, SCOPE)
+ .then((registration) => {
+ t.add_cleanup(() => registration.unregister());
+
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(() => with_iframe(SCOPE))
+ .then((frame) => {
+ t.add_cleanup(() => frame.remove());
+
+ return frame.contentWindow.fetch(url, init);
+ });
+ }
+
+ {%- for subtest in subtests %}
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ t,
+ makeRequestURL(key, [% subtest.origins %]),
+ [%subtest.init | default({}) | tojson%],
+ 'respondWith'
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ {%- if subtest.expected == none %}
+ assert_not_own_property(headers, '[%subtest.headerName%]');
+ {%- else %}
+ assert_own_property(headers, '[%subtest.headerName%]');
+ assert_array_equals(headers['[%subtest.headerName%]'], ['[%subtest.expected%]']);
+ {%- endif %}
+ });
+ }, '[%subtest.headerName%] - [%subtest.description | pad("end", ", ")%][%subtest.init | collection("init")%] - respondWith');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ t,
+ makeRequestURL(key, [% subtest.origins %]),
+ [%subtest.init | default({}) | tojson%],
+ 'fallback'
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ {%- if subtest.expected == none %}
+ assert_not_own_property(headers, '[%subtest.headerName%]');
+ {%- else %}
+ assert_own_property(headers, '[%subtest.headerName%]');
+ assert_array_equals(headers['[%subtest.headerName%]'], ['[%subtest.expected%]']);
+ {%- endif %}
+ });
+ }, '[%subtest.headerName%] - [%subtest.description | pad("end", ", ")%][%subtest.init | collection("init")%] - fallback');
+
+ {%- endfor %}
+
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/fetch/metadata/tools/templates/fetch.sub.html b/test/wpt/tests/fetch/metadata/tools/templates/fetch.sub.html
new file mode 100644
index 0000000..a8dc536
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/tools/templates/fetch.sub.html
@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<!--
+[%provenance%]
+-->
+<html lang="en">
+ <meta charset="utf-8">
+ <title>HTTP headers on request using the "fetch" API</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/fetch/metadata/resources/helper.sub.js"></script>
+ <body>
+ <script>
+ 'use strict';
+
+ function induceRequest(url, init) {
+ return fetch(url, init);
+ }
+
+ {%- for subtest in subtests %}
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, [% subtest.origins %]),
+ [%subtest.init | default({}) | tojson%]
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ {%- if subtest.expected == none %}
+ assert_not_own_property(headers, '[%subtest.headerName%]');
+ {%- else %}
+ assert_own_property(headers, '[%subtest.headerName%]');
+ assert_array_equals(headers['[%subtest.headerName%]'], ['[%subtest.expected%]']);
+ {%- endif %}
+ });
+ }, '[%subtest.headerName%] - [%subtest.description | pad("end", ", ")%][%subtest.init | collection("init")%]');
+
+ {%- endfor %}
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/fetch/metadata/tools/templates/form-submission.sub.html b/test/wpt/tests/fetch/metadata/tools/templates/form-submission.sub.html
new file mode 100644
index 0000000..4c9c8c5
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/tools/templates/form-submission.sub.html
@@ -0,0 +1,87 @@
+<!DOCTYPE html>
+<!--
+[%provenance%]
+-->
+<html lang="en">
+ <meta charset="utf-8">
+ <meta name="timeout" content="long">
+ <title>HTTP headers on request for HTML form navigation</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ {%- if subtests|selectattr('userActivated')|list %}
+ <script src="/resources/testdriver.js"></script>
+ <script src="/resources/testdriver-vendor.js"></script>
+ {%- endif %}
+ <script src="/fetch/metadata/resources/helper.sub.js"></script>
+ <body>
+ <script>
+ 'use strict';
+
+ function induceRequest(method, url, userActivated) {
+ const windowName = String(Math.random());
+ const form = document.createElement('form');
+ const submit = document.createElement('input');
+ submit.setAttribute('type', 'submit');
+ form.appendChild(submit);
+ const win = open('about:blank', windowName);
+ form.setAttribute('method', method);
+ form.setAttribute('action', url);
+ form.setAttribute('target', windowName);
+ document.body.appendChild(form);
+
+ // Query parameters must be expressed as form values so that they are sent
+ // with the submission of forms whose method is POST.
+ Array.from(new URL(url, location.origin).searchParams)
+ .forEach(([name, value]) => {
+ const input = document.createElement('input');
+ input.setAttribute('type', 'hidden');
+ input.setAttribute('name', name);
+ input.setAttribute('value', value);
+ form.appendChild(input);
+ });
+
+ return new Promise((resolve) => {
+ addEventListener('message', function(event) {
+ if (event.source === win) {
+ resolve();
+ }
+ });
+
+ if (userActivated) {
+ test_driver.click(submit);
+ } else {
+ submit.click();
+ }
+ })
+ .then(() => {
+ form.remove();
+ win.close();
+ });
+ }
+ const responseParams = {
+ mime: 'text/html',
+ body: `<script>opener.postMessage('done', '*')</${''}script>`
+ };
+
+ {%- for subtest in subtests %}
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, [% subtest.origins %], responseParams);
+ const userActivated = [% 'true' if subtest.userActivated else 'false' %];
+ return induceRequest('[%subtest.method | default("POST")%]', url, userActivated)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ {%- if subtest.expected == none %}
+ assert_not_own_property(headers, '[%subtest.headerName%]');
+ {%- else %}
+ assert_own_property(headers, '[%subtest.headerName%]');
+ assert_array_equals(headers['[%subtest.headerName%]'], ['[%subtest.expected%]']);
+ {%- endif %}
+ });
+ }, '[%subtest.headerName%] - [%subtest.description | pad("end", " - ")%][%subtest.method | default("POST")%][%" with user activation" if subtest.userActivated%]');
+
+ {%- endfor %}
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/fetch/metadata/tools/templates/header-link.sub.html b/test/wpt/tests/fetch/metadata/tools/templates/header-link.sub.html
new file mode 100644
index 0000000..2831f22
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/tools/templates/header-link.sub.html
@@ -0,0 +1,56 @@
+<!DOCTYPE html>
+<!--
+[%provenance%]
+-->
+<html lang="en">
+ <meta charset="utf-8">
+ <title>HTTP headers on request for HTTP "Link" header</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/fetch/metadata/resources/helper.sub.js"></script>
+ <body>
+ <script>
+ 'use strict';
+
+ function induceRequest(url, rel, test) {
+ const iframe = document.createElement('iframe');
+
+ iframe.setAttribute(
+ 'src',
+ '/fetch/metadata/resources/header-link.py' +
+ `?location=${encodeURIComponent(url)}&rel=${rel}`
+ );
+
+ document.body.appendChild(iframe);
+ test.add_cleanup(() => iframe.remove());
+
+ return new Promise((resolve) => {
+ iframe.onload = iframe.onerror = resolve;
+ });
+ }
+
+ {%- for subtest in subtests %}
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, [% subtest.origins %], {mime: 'text/html'}),
+ '[%subtest.rel%]',
+ t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ {%- if subtest.expected == none %}
+ assert_not_own_property(headers, '[%subtest.headerName%]');
+ {%- else %}
+ assert_own_property(headers, '[%subtest.headerName%]');
+ assert_array_equals(headers['[%subtest.headerName%]'], ['[%subtest.expected%]']);
+ {%- endif %}
+ });
+ }, '[%subtest.headerName%] rel=[%subtest.rel%][%subtest.description | pad("start", " - ")%]');
+
+ {%- endfor %}
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/fetch/metadata/tools/templates/header-refresh.optional.sub.html b/test/wpt/tests/fetch/metadata/tools/templates/header-refresh.optional.sub.html
new file mode 100644
index 0000000..ec963d5
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/tools/templates/header-refresh.optional.sub.html
@@ -0,0 +1,59 @@
+<!DOCTYPE html>
+<!--
+[%provenance%]
+-->
+<html lang="en">
+ <meta charset="utf-8">
+ {%- if subtests|length > 10 %}
+ <meta name="timeout" content="long">
+ {%- endif %}
+ <title>HTTP headers on request for HTTP "Refresh" header</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/fetch/metadata/resources/helper.sub.js"></script>
+ <body>
+ <script>
+ 'use strict';
+
+ function induceRequest(url, test) {
+ const win = window.open();
+ test.add_cleanup(() => win.close());
+
+ win.location = `/common/refresh.py?location=${encodeURIComponent(url)}`
+
+ return new Promise((resolve) => {
+ addEventListener('message', (event) => {
+ if (event.source === win) {
+ resolve();
+ }
+ });
+ });
+ }
+
+ const responseParams = {
+ mime: 'text/html',
+ body: `<script>opener.postMessage(0, '*')</${''}script>`
+ };
+ {%- for subtest in subtests %}
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(key, [% subtest.origins %], responseParams), t
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ {%- if subtest.expected == none %}
+ assert_not_own_property(headers, '[%subtest.headerName%]');
+ {%- else %}
+ assert_own_property(headers, '[%subtest.headerName%]');
+ assert_array_equals(headers['[%subtest.headerName%]'], ['[%subtest.expected%]']);
+ {%- endif %}
+ });
+ }, '[%subtest.headerName%][%subtest.description | pad("start", " - ")%]');
+
+ {%- endfor %}
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/fetch/metadata/tools/templates/script-module-import-dynamic.sub.html b/test/wpt/tests/fetch/metadata/tools/templates/script-module-import-dynamic.sub.html
new file mode 100644
index 0000000..653d3cd
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/tools/templates/script-module-import-dynamic.sub.html
@@ -0,0 +1,35 @@
+<!DOCTYPE html>
+<!--
+[%provenance%]
+-->
+<html lang="en">
+ <meta charset="utf-8">
+ <title>HTTP headers on request for dynamic ECMAScript module import</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/fetch/metadata/resources/helper.sub.js"></script>
+ <script type="module">
+ 'use strict';
+ {%- for subtest in subtests %}
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, [% subtest.origins %], { mime: 'application/javascript' }
+ );
+
+ return import(url)
+ .then(() => retrieve(key))
+ .then((headers) => {
+ {%- if subtest.expected == none %}
+ assert_not_own_property(headers, '[%subtest.headerName%]');
+ {%- else %}
+ assert_own_property(headers, '[%subtest.headerName%]');
+ assert_array_equals(headers['[%subtest.headerName%]'], ['[%subtest.expected%]']);
+ {%- endif %}
+ });
+ }, '[%subtest.headerName%][%subtest.description | pad("start", " - ")%]');
+
+ {%- endfor %}
+ </script>
+</html>
diff --git a/test/wpt/tests/fetch/metadata/tools/templates/script-module-import-static.sub.html b/test/wpt/tests/fetch/metadata/tools/templates/script-module-import-static.sub.html
new file mode 100644
index 0000000..c8d5f95
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/tools/templates/script-module-import-static.sub.html
@@ -0,0 +1,53 @@
+<!DOCTYPE html>
+<!--
+[%provenance%]
+-->
+<html lang="en">
+ <meta charset="utf-8">
+ <title>HTTP headers on request for static ECMAScript module import</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/fetch/metadata/resources/helper.sub.js"></script>
+ <body>
+ <script>
+ 'use strict';
+
+ function induceRequest(url) {
+ const script = document.createElement('script');
+ script.setAttribute('type', 'module');
+ script.setAttribute(
+ 'src',
+ '/fetch/metadata/resources/es-module.sub.js?moduleId=' + encodeURIComponent(url)
+ );
+
+ return new Promise((resolve, reject) => {
+ script.onload = resolve;
+ script.onerror = () => reject('Failed to load script');
+ document.body.appendChild(script);
+ })
+ .then(() => script.remove());
+ }
+ {%- for subtest in subtests %}
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ makeRequestURL(
+ key, [% subtest.origins %], { mime: 'application/javascript' }
+ )
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ {%- if subtest.expected == none %}
+ assert_not_own_property(headers, '[%subtest.headerName%]');
+ {%- else %}
+ assert_own_property(headers, '[%subtest.headerName%]');
+ assert_array_equals(headers['[%subtest.headerName%]'], ['[%subtest.expected%]']);
+ {%- endif %}
+ });
+ }, '[%subtest.headerName%][%subtest.description | pad("start", " - ")%]');
+
+ {%- endfor %}
+ </script>
+</html>
diff --git a/test/wpt/tests/fetch/metadata/tools/templates/serviceworker.https.sub.html b/test/wpt/tests/fetch/metadata/tools/templates/serviceworker.https.sub.html
new file mode 100644
index 0000000..8284325
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/tools/templates/serviceworker.https.sub.html
@@ -0,0 +1,72 @@
+<!DOCTYPE html>
+<!--
+[%provenance%]
+-->
+<!DOCTYPE html>
+<html lang="en">
+ <meta charset="utf-8">
+ <title>HTTP headers on request for Service Workers</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/fetch/metadata/resources/helper.sub.js"></script>
+ <body>
+ <script>
+ 'use strict';
+
+ function induceRequest(t, url, options, event, clear) {
+ // Register a service worker and check the request header.
+ return navigator.serviceWorker.register(url, options)
+ .then((registration) => {
+ t.add_cleanup(() => registration.unregister());
+ if (event === 'register') {
+ return;
+ }
+ return clear().then(() => registration.update());
+ });
+ }
+
+ {%- for subtest in subtests %}
+ {%- set origin = subtest.origins[0]|default('httpsOrigin') %}
+ {%- if origin == 'httpsOrigin' or not origin %}
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, [% subtest.origins %], { mime: 'application/javascript' }
+ );
+
+ return induceRequest(t, url, [%subtest.options | default({}) | tojson%], 'register')
+ .then(() => retrieve(key))
+ .then((headers) => {
+ {%- if subtest.expected == none %}
+ assert_not_own_property(headers, '[%subtest.headerName%]');
+ {%- else %}
+ assert_own_property(headers, '[%subtest.headerName%]');
+ assert_array_equals(headers['[%subtest.headerName%]'], ['[%subtest.expected%]']);
+ {%- endif %}
+ });
+ }, '[%subtest.headerName%] - [%subtest.description | pad("end", ", ")%][%subtest.options | collection("options")%] - registration');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, [% subtest.origins %], { mime: 'application/javascript' }
+ );
+
+ return induceRequest(t, url, [%subtest.options | default({}) | tojson%], 'update', () => retrieve(key))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ {%- if subtest.expected == none %}
+ assert_not_own_property(headers, '[%subtest.headerName%]');
+ {%- else %}
+ assert_own_property(headers, '[%subtest.headerName%]');
+ assert_array_equals(headers['[%subtest.headerName%]'], ['[%subtest.expected%]']);
+ {%- endif %}
+ });
+ }, '[%subtest.headerName%] - [%subtest.description | pad("end", ", ")%][%subtest.options | collection("options")%] - updating');
+
+ {%- endif %}
+ {%- endfor %}
+ </script>
+</body>
+</html>
diff --git a/test/wpt/tests/fetch/metadata/tools/templates/svg-image.sub.html b/test/wpt/tests/fetch/metadata/tools/templates/svg-image.sub.html
new file mode 100644
index 0000000..52f7806
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/tools/templates/svg-image.sub.html
@@ -0,0 +1,75 @@
+<!DOCTYPE html>
+<!--
+[%provenance%]
+-->
+<html lang="en">
+ <meta charset="utf-8">
+ {%- if subtests|length > 10 %}
+ <meta name="timeout" content="long">
+ {%- endif %}
+ <title>HTTP headers on request for SVG "image" element source</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/fetch/metadata/resources/helper.sub.js"></script>
+ <body>
+ <script>
+ 'use strict';
+
+ const params = {
+ body: `
+ <svg xmlns="http://www.w3.org/2000/svg" width="123" height="123">
+ <rect fill="lime" width="123" height="123"/>
+ </svg>
+ `,
+ mime: 'image/svg+xml'
+ };
+
+ function induceRequest(t, url, attributes) {
+ const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
+ svg.setAttributeNS(
+ "http://www.w3.org/2000/xmlns/",
+ "xmlns:xlink",
+ "http://www.w3.org/1999/xlink"
+ );
+ const image = document.createElementNS("http://www.w3.org/2000/svg", "image");
+ image.setAttribute("href", url);
+ svg.appendChild(image);
+
+ for (const [ name, value ] of Object.entries(attributes)) {
+ image.setAttribute(name, value);
+ }
+
+ document.body.appendChild(svg);
+ t.add_cleanup(() => svg.remove());
+
+ return new Promise((resolve, reject) => {
+ image.onload = resolve;
+ image.onerror = reject;
+ });
+ }
+
+ {%- for subtest in subtests %}
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+
+ return induceRequest(
+ t,
+ makeRequestURL(key, [% subtest.origins %], params),
+ [%subtest.elementAttrs | default({}) | tojson%]
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ {%- if subtest.expected == none %}
+ assert_not_own_property(headers, '[%subtest.headerName%]');
+ {%- else %}
+ assert_own_property(headers, '[%subtest.headerName%]');
+ assert_array_equals(headers['[%subtest.headerName%]'], ['[%subtest.expected%]']);
+ {%- endif %}
+ });
+ }, '[%subtest.headerName%][%subtest.description | pad("start", " - ")%] [%subtest.elementAttrs | collection("attributes")%]');
+
+ {%- endfor %}
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/fetch/metadata/tools/templates/window-history.sub.html b/test/wpt/tests/fetch/metadata/tools/templates/window-history.sub.html
new file mode 100644
index 0000000..286d019
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/tools/templates/window-history.sub.html
@@ -0,0 +1,134 @@
+<!DOCTYPE html>
+<!--
+[%provenance%]
+-->
+<html lang="en">
+ <meta charset="utf-8">
+ {%- if subtests|length > 10 %}
+ <meta name="timeout" content="long">
+ {%- endif %}
+ <title>HTTP headers on request for navigation via the HTML History API</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ {%- if subtests|selectattr('userActivated')|list %}
+ <script src="/resources/testdriver.js"></script>
+ <script src="/resources/testdriver-vendor.js"></script>
+ {%- endif %}
+ <script src="/fetch/metadata/resources/helper.sub.js"></script>
+ <body>
+ <script>
+ 'use strict';
+
+ const whenDone = (win) => {
+ return new Promise((resolve) => {
+ addEventListener('message', function handle(event) {
+ if (event.source === win) {
+ resolve();
+ removeEventListener('message', handle);
+ }
+ });
+ })
+ };
+
+ /**
+ * Prime the UA's session history such that the location of the request is
+ * immediately behind the current entry. Because the location may not be
+ * same-origin with the current browsing context, this must be done via a
+ * true navigation and not, e.g. the `history.pushState` API. The initial
+ * navigation will alter the WPT server's internal state; in order to avoid
+ * false positives, clear that state prior to initiating the second
+ * navigation via `history.back`.
+ */
+ function induceBackRequest(url, test, clear) {
+ const win = window.open(url);
+
+ test.add_cleanup(() => win.close());
+
+ return whenDone(win)
+ .then(clear)
+ .then(() => win.history.back())
+ .then(() => whenDone(win));
+ }
+
+ /**
+ * Prime the UA's session history such that the location of the request is
+ * immediately ahead of the current entry. Because the location may not be
+ * same-origin with the current browsing context, this must be done via a
+ * true navigation and not, e.g. the `history.pushState` API. The initial
+ * navigation will alter the WPT server's internal state; in order to avoid
+ * false positives, clear that state prior to initiating the second
+ * navigation via `history.forward`.
+ */
+ function induceForwardRequest(url, test, clear) {
+ const win = window.open(messageOpenerUrl);
+
+ test.add_cleanup(() => win.close());
+
+ return whenDone(win)
+ .then(() => win.location = url)
+ .then(() => whenDone(win))
+ .then(clear)
+ .then(() => win.history.go(-2))
+ .then(() => whenDone(win))
+ .then(() => win.history.forward())
+ .then(() => whenDone(win));
+ }
+
+ const messageOpenerUrl = new URL(
+ '/fetch/metadata/resources/message-opener.html', location
+ );
+ // For these tests to function, replacement must *not* be enabled during
+ // navigation. Assignment must therefore take place after the document has
+ // completely loaded [1]. This event is not directly observable, but it is
+ // scheduled as a task immediately following the global object's `load`
+ // event [2]. By queuing a task during the dispatch of the `load` event,
+ // navigation can be consistently triggered without replacement.
+ //
+ // [1] https://html.spec.whatwg.org/multipage/history.html#location-object-setter-navigate
+ // [2] https://html.spec.whatwg.org/multipage/parsing.html#the-end
+ const responseParams = {
+ mime: 'text/html',
+ body: `<script>
+ window.addEventListener('load', () => {
+ set`+`Timeout(() => location.assign('${messageOpenerUrl}'));
+ });
+ <`+`/script>`
+ };
+ {%- for subtest in subtests %}
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, [% subtest.origins %], responseParams);
+
+ return induceBackRequest(url, t, () => retrieve(key))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ {%- if subtest.expected == none %}
+ assert_not_own_property(headers, '[%subtest.headerName%]');
+ {%- else %}
+ assert_own_property(headers, '[%subtest.headerName%]');
+ assert_array_equals(headers['[%subtest.headerName%]'], ['[%subtest.expected%]']);
+ {%- endif %}
+ });
+ }, '[%subtest.headerName%] - [%subtest.description | pad("right", " - ")%]history.back[%subtest.api%][% " with user activation" if subtest.userActivated%]');
+
+ promise_test((t) => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, [% subtest.origins %], responseParams);
+
+ return induceForwardRequest(url, t, () => retrieve(key))
+ .then(() => retrieve(key))
+ .then((headers) => {
+ {%- if subtest.expected == none %}
+ assert_not_own_property(headers, '[%subtest.headerName%]');
+ {%- else %}
+ assert_own_property(headers, '[%subtest.headerName%]');
+ assert_array_equals(headers['[%subtest.headerName%]'], ['[%subtest.expected%]']);
+ {%- endif %}
+ });
+ }, '[%subtest.headerName%] - [%subtest.description | pad("right", " - ")%]history.forward[%subtest.api%][% " with user activation" if subtest.userActivated%]');
+
+ {%- endfor %}
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/fetch/metadata/tools/templates/window-location.sub.html b/test/wpt/tests/fetch/metadata/tools/templates/window-location.sub.html
new file mode 100644
index 0000000..96f3912
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/tools/templates/window-location.sub.html
@@ -0,0 +1,128 @@
+<!DOCTYPE html>
+<!--
+[%provenance%]
+-->
+<html lang="en">
+ <meta charset="utf-8">
+ {%- if subtests|length > 10 %}
+ <meta name="timeout" content="long">
+ {%- endif %}
+ <title>HTTP headers on request for navigation via the HTML Location API</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ {%- if subtests|selectattr('userActivated')|list %}
+ <script src="/resources/testdriver.js"></script>
+ <script src="/resources/testdriver-vendor.js"></script>
+ {%- endif %}
+ <script src="/fetch/metadata/resources/helper.sub.js"></script>
+ <body>
+ <script>
+ 'use strict';
+
+ function induceRequest(url, navigate, userActivated) {
+ const win = window.open();
+
+ return new Promise((resolve) => {
+ addEventListener('message', function(event) {
+ if (event.source === win) {
+ resolve();
+ }
+ });
+
+ if (userActivated) {
+ test_driver.bless('enable user activation', () => {
+ navigate(win, url);
+ });
+ } else {
+ navigate(win, url);
+ }
+ })
+ .then(() => win.close());
+ }
+
+ const responseParams = {
+ mime: 'text/html',
+ body: `<script>opener.postMessage('done', '*')</${''}script>`
+ };
+ {%- for subtest in subtests %}
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, [% subtest.origins %], responseParams);
+
+ const navigate = (win, path) => {
+ win.location = path;
+ };
+ return induceRequest(url, navigate, [% 'true' if subtest.userActivated else 'false' %])
+ .then(() => retrieve(key))
+ .then((headers) => {
+ {%- if subtest.expected == none %}
+ assert_not_own_property(headers, '[%subtest.headerName%]');
+ {%- else %}
+ assert_own_property(headers, '[%subtest.headerName%]');
+ assert_array_equals(headers['[%subtest.headerName%]'], ['[%subtest.expected%]']);
+ {%- endif %}
+ });
+ }, '[%subtest.headerName%] - [%subtest.description | pad("end", " - ")%]location[% " with user activation" if subtest.userActivated%]');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, [% subtest.origins %], responseParams);
+
+ const navigate = (win, path) => {
+ win.location.href = path;
+ };
+ return induceRequest(url, navigate, [% 'true' if subtest.userActivated else 'false' %])
+ .then(() => retrieve(key))
+ .then((headers) => {
+ {%- if subtest.expected == none %}
+ assert_not_own_property(headers, '[%subtest.headerName%]');
+ {%- else %}
+ assert_own_property(headers, '[%subtest.headerName%]');
+ assert_array_equals(headers['[%subtest.headerName%]'], ['[%subtest.expected%]']);
+ {%- endif %}
+ });
+ }, '[%subtest.headerName%] - [%subtest.description | pad("end", " - ")%]location.href[% " with user activation" if subtest.userActivated%]');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, [% subtest.origins %], responseParams);
+
+ const navigate = (win, path) => {
+ win.location.assign(path);
+ };
+ return induceRequest(url, navigate, [% 'true' if subtest.userActivated else 'false' %])
+ .then(() => retrieve(key))
+ .then((headers) => {
+ {%- if subtest.expected == none %}
+ assert_not_own_property(headers, '[%subtest.headerName%]');
+ {%- else %}
+ assert_own_property(headers, '[%subtest.headerName%]');
+ assert_array_equals(headers['[%subtest.headerName%]'], ['[%subtest.expected%]']);
+ {%- endif %}
+ });
+ }, '[%subtest.headerName%] - [%subtest.description | pad("end", " - ")%]location.assign[% " with user activation" if subtest.userActivated%]');
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(key, [% subtest.origins %], responseParams);
+
+ const navigate = (win, path) => {
+ win.location.replace(path);
+ };
+ return induceRequest(url, navigate, [% 'true' if subtest.userActivated else 'false' %])
+ .then(() => retrieve(key))
+ .then((headers) => {
+ {%- if subtest.expected == none %}
+ assert_not_own_property(headers, '[%subtest.headerName%]');
+ {%- else %}
+ assert_own_property(headers, '[%subtest.headerName%]');
+ assert_array_equals(headers['[%subtest.headerName%]'], ['[%subtest.expected%]']);
+ {%- endif %}
+ });
+ }, '[%subtest.headerName%] - [%subtest.description | pad("end", " - ")%]location.replace[% " with user activation" if subtest.userActivated%]');
+
+ {%- endfor %}
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/fetch/metadata/tools/templates/worker-dedicated-constructor.sub.html b/test/wpt/tests/fetch/metadata/tools/templates/worker-dedicated-constructor.sub.html
new file mode 100644
index 0000000..fede596
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/tools/templates/worker-dedicated-constructor.sub.html
@@ -0,0 +1,49 @@
+<!DOCTYPE html>
+<!--
+[%provenance%]
+-->
+<html lang="en">
+ <meta charset="utf-8">
+ <title>HTTP headers on request for dedicated worker via the "Worker" constructor</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/fetch/metadata/resources/helper.sub.js"></script>
+ <script type="module">
+ 'use strict';
+ function induceRequest(url, options) {
+ return new Promise((resolve, reject) => {
+ const worker = new Worker(url, options);
+ worker.onmessage = resolve;
+ worker.onerror = reject;
+ });
+ }
+
+ {%- for subtest in subtests %}
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key,
+ [% subtest.origins %],
+ { mime: 'application/javascript', body: 'postMessage("")' }
+ );
+
+ return induceRequest(url
+ {%- if subtest.options -%}
+ , [% subtest.options | tojson %]
+ {%- endif -%}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ {%- if subtest.expected == none %}
+ assert_not_own_property(headers, '[%subtest.headerName%]');
+ {%- else %}
+ assert_own_property(headers, '[%subtest.headerName%]');
+ assert_array_equals(headers['[%subtest.headerName%]'], ['[%subtest.expected%]']);
+ {%- endif %}
+ });
+ }, '[%subtest.headerName%] - [%subtest.description | pad("end", ", ")%][%subtest.options|collection("options")%]');
+
+ {%- endfor %}
+ </script>
+</html>
diff --git a/test/wpt/tests/fetch/metadata/tools/templates/worker-dedicated-importscripts.sub.html b/test/wpt/tests/fetch/metadata/tools/templates/worker-dedicated-importscripts.sub.html
new file mode 100644
index 0000000..93e6374
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/tools/templates/worker-dedicated-importscripts.sub.html
@@ -0,0 +1,54 @@
+<!DOCTYPE html>
+<!--
+[%provenance%]
+-->
+<html lang="en">
+ <meta charset="utf-8">
+ <title>HTTP headers on request for dedicated worker via the "importScripts" API</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/fetch/metadata/resources/helper.sub.js"></script>
+ <script type="module">
+ 'use strict';
+ function induceRequest(url, options) {
+ const src = `
+ importScripts('${url}');
+ postMessage('done');
+ `;
+ const workerUrl = URL.createObjectURL(
+ new Blob([src], { type: 'application/javascript' })
+ );
+ return new Promise((resolve, reject) => {
+ const worker = new Worker(workerUrl, options);
+ worker.onmessage = resolve;
+ worker.onerror = reject;
+ });
+ }
+
+ {%- for subtest in subtests %}
+
+ promise_test(() => {
+ const key = '{{uuid()}}';
+ const url = makeRequestURL(
+ key, [% subtest.origins %], { mime: 'application/javascript' }
+ );
+
+ return induceRequest(url
+ {%- if subtest.options -%}
+ , [% subtest.options | tojson %]
+ {%- endif -%}
+ )
+ .then(() => retrieve(key))
+ .then((headers) => {
+ {%- if subtest.expected == none %}
+ assert_not_own_property(headers, '[%subtest.headerName%]');
+ {%- else %}
+ assert_own_property(headers, '[%subtest.headerName%]');
+ assert_array_equals(headers['[%subtest.headerName%]'], ['[%subtest.expected%]']);
+ {%- endif %}
+ });
+ }, '[%subtest.headerName%][%subtest.description | pad("start", " - ")%]');
+
+ {%- endfor %}
+ </script>
+</html>
diff --git a/test/wpt/tests/fetch/metadata/track.https.sub.html b/test/wpt/tests/fetch/metadata/track.https.sub.html
new file mode 100644
index 0000000..346798f
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/track.https.sub.html
@@ -0,0 +1,119 @@
+<!DOCTYPE html>
+
+<link rel="author" href="mtrzos@google.com" title="Maciek Trzos">
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=/fetch/metadata/resources/helper.js></script>
+<script src=/common/utils.js></script>
+<body>
+</body>
+<script>
+ let nonce = token();
+
+ function createVideoElement() {
+ let el = document.createElement('video');
+ el.src = "/media/movie_5.mp4";
+ el.setAttribute("controls", "");
+ el.setAttribute("crossorigin", "");
+ return el;
+ }
+
+ function createTrack() {
+ let el = document.createElement("track");
+ el.setAttribute("default", "");
+ el.setAttribute("kind", "captions");
+ el.setAttribute("srclang", "en");
+ return el;
+ }
+
+ promise_test(t => {
+ return new Promise((resolve, reject) => {
+ let key = "track-same-origin" + nonce;
+ let video = createVideoElement();
+ let el = createTrack();
+ el.src = "https://{{host}}:{{ports[https][0]}}/fetch/metadata/resources/record-header.py?file=" + key;
+ el.onload = t.step_func(_ => {
+ expected = {
+ "site": "same-origin",
+ "user": "",
+ "mode": "cors", // Because the `video` element has `crossorigin`
+ "dest": "track"
+ };
+ validate_expectations(key, expected, "Same-Origin track")
+ .then(_ => resolve());
+ });
+ video.appendChild(el);
+ document.body.appendChild(video);
+ });
+ }, "Same-Origin track");
+
+ promise_test(t => {
+ return new Promise((resolve, reject) => {
+ let key = "track-same-site" + nonce;
+ let video = createVideoElement();
+ let el = createTrack();
+ el.src = "https://{{hosts[][www]}}:{{ports[https][0]}}/fetch/metadata/resources/record-header.py?file=" + key;
+ el.onload = t.step_func(_ => {
+ expected = {
+ "site": "same-site",
+ "user": "",
+ "mode": "cors", // Because the `video` element has `crossorigin`
+ "dest": "track"
+ };
+ validate_expectations(key, expected, "Same-Site track")
+ .then(resolve)
+ .catch(reject);
+
+ });
+ video.appendChild(el);
+ document.body.appendChild(video);
+ });
+ }, "Same-Site track");
+
+ promise_test(t => {
+ return new Promise((resolve, reject) => {
+ let key = "track-cross-site" + nonce;
+ let video = createVideoElement();
+ let el = createTrack();
+ el.src = "https://{{hosts[alt][www]}}:{{ports[https][0]}}/fetch/metadata/resources/record-header.py?file=" + key;
+ el.onload = t.step_func(_ => {
+ expected = {
+ "site": "cross-site",
+ "user": "",
+ "mode": "cors", // Because the `video` element has `crossorigin`
+ "dest": "track"
+ };
+ validate_expectations(key, expected,"Cross-Site track")
+ .then(resolve)
+ .catch(reject);
+ });
+ video.appendChild(el);
+ document.body.appendChild(video);
+ });
+ }, "Cross-Site track");
+
+ promise_test(t => {
+ return new Promise((resolve, reject) => {
+ let key = "track-same-origin-cors" + nonce;
+ let video = createVideoElement();
+
+ // Unset `crossorigin` to change the CORS mode:
+ video.crossOrigin = undefined;
+
+ let el = createTrack();
+ el.src = "https://{{host}}:{{ports[https][0]}}/fetch/metadata/resources/record-header.py?file=" + key;
+ el.onload = t.step_func(_ => {
+ expected = {
+ "site":"same-origin",
+ "user":"",
+ "mode": "same-origin",
+ "dest": "track"
+ };
+ validate_expectations(key, expected, "Same-Origin, CORS track")
+ .then(_ => resolve());
+ });
+ video.appendChild(el);
+ document.body.appendChild(video);
+ });
+ }, "Same-Origin, CORS track");
+</script>
diff --git a/test/wpt/tests/fetch/metadata/trailing-dot.https.sub.any.js b/test/wpt/tests/fetch/metadata/trailing-dot.https.sub.any.js
new file mode 100644
index 0000000..5e32fc4
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/trailing-dot.https.sub.any.js
@@ -0,0 +1,30 @@
+// META: global=window,worker
+// META: script=/fetch/metadata/resources/helper.js
+
+// Site
+promise_test(t => {
+ return validate_expectations_custom_url("https://{{host}}.:{{ports[https][0]}}/fetch/metadata/resources/echo-as-json.py", {}, {
+ "site": "cross-site",
+ "user": "",
+ "mode": "cors",
+ "dest": "empty"
+ }, "Fetching a resource from the same origin, but spelled with a trailing dot.");
+}, "Fetching a resource from the same origin, but spelled with a trailing dot.");
+
+promise_test(t => {
+ return validate_expectations_custom_url("https://{{hosts[][www]}}.:{{ports[https][0]}}/fetch/metadata/resources/echo-as-json.py", {}, {
+ "site": "cross-site",
+ "user": "",
+ "mode": "cors",
+ "dest": "empty"
+ }, "Fetching a resource from the same site, but spelled with a trailing dot.");
+}, "Fetching a resource from the same site, but spelled with a trailing dot.");
+
+promise_test(t => {
+ return validate_expectations_custom_url("https://{{hosts[alt][www]}}.:{{ports[https][0]}}/fetch/metadata/resources/echo-as-json.py", {}, {
+ "site": "cross-site",
+ "user": "",
+ "mode": "cors",
+ "dest": "empty"
+ }, "Fetching a resource from a cross-site host, spelled with a trailing dot.");
+}, "Fetching a resource from a cross-site host, spelled with a trailing dot.");
diff --git a/test/wpt/tests/fetch/metadata/unload.https.sub.html b/test/wpt/tests/fetch/metadata/unload.https.sub.html
new file mode 100644
index 0000000..bc26048
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/unload.https.sub.html
@@ -0,0 +1,64 @@
+<!DOCTYPE html>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=/resources/testdriver.js></script>
+<script src=/resources/testdriver-vendor.js></script>
+<script src=/fetch/metadata/resources/helper.js></script>
+<script src=/common/utils.js></script>
+<body>
+<script>
+ // The test
+ // 1. Creates a same-origin iframe
+ // 2. Adds to the iframe an unload handler that will
+ // trigger a request to <unload_request_url>/.../record-header.py...
+ // 3. Navigate the iframe to a cross-origin url (to data: url)
+ // 4. Waits until the request goes through
+ // 5. Verifies Sec-Fetch-Site request header of the request.
+ //
+ // This is a regression test for https://crbug.com/986577.
+ function create_test(unload_request_origin, expectations) {
+ async_test(t => {
+ // STEP 1: Create an iframe.
+ let nonce = token();
+ let key = "unload-test-" + nonce;
+ let url = unload_request_origin +
+ "/fetch/metadata/resources/record-header.py?file=" + key;
+ let i = document.createElement('iframe');
+ i.src = 'resources/unload-with-beacon.html';
+ i.onload = () => {
+ // STEP 2: Ask the iframe to add an unload handler.
+ i.contentWindow.postMessage(url, '*');
+ };
+ window.addEventListener('message', e => {
+ // STEP 3: Navigate the iframe away
+ i.contentWindow.location = 'data:text/html,DONE';
+ });
+ document.body.appendChild(i);
+
+ // STEPS 4 and 5: Wait for the beacon to go through and verify
+ // the request headers.
+ function wait_and_verify() {
+ t.step_timeout(() => {
+ fetch("resources/record-header.py?retrieve=true&file=" + key)
+ .then(response => response.text())
+ .then(text => t.step(() => {
+ if (text == 'No header has been recorded') {
+ wait_and_verify();
+ return;
+ }
+ assert_header_equals(text, expectations);
+ t.done();
+ }))
+ }, 200);
+ }
+ wait_and_verify();
+ }, "Fetch from an unload handler");
+ }
+
+ create_test("https://{{host}}:{{ports[https][0]}}", {
+ "site": "same-origin",
+ "user": "",
+ "mode": "no-cors",
+ "dest": "empty"
+ });
+</script>
diff --git a/test/wpt/tests/fetch/metadata/window-open.https.sub.html b/test/wpt/tests/fetch/metadata/window-open.https.sub.html
new file mode 100644
index 0000000..94ba76a
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/window-open.https.sub.html
@@ -0,0 +1,199 @@
+<!DOCTYPE html>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=/resources/testdriver.js></script>
+<script src=/resources/testdriver-vendor.js></script>
+<script src=/fetch/metadata/resources/helper.js></script>
+<body>
+<script>
+ // Forced navigations:
+ async_test(t => {
+ let w = window.open("https://{{host}}:{{ports[https][0]}}/fetch/metadata/resources/post-to-owner.py");
+ t.add_cleanup(_ => w.close());
+ window.addEventListener('message', t.step_func(e => {
+ if (e.source != w)
+ return;
+
+ assert_header_equals(e.data, {
+ "site": "same-origin",
+ "user": "",
+ "mode": "navigate",
+ "dest": "document"
+ }, "Same-origin window, forced");
+ t.done();
+ }));
+ }, "Same-origin window, forced");
+
+ async_test(t => {
+ let w = window.open("https://{{hosts[][www]}}:{{ports[https][0]}}/fetch/metadata/resources/post-to-owner.py");
+ t.add_cleanup(_ => w.close());
+ window.addEventListener('message', t.step_func(e => {
+ if (e.source != w)
+ return;
+
+ assert_header_equals(e.data, {
+ "site": "same-site",
+ "user": "",
+ "mode": "navigate",
+ "dest": "document"
+ }, "Same-site window, forced");
+ t.done();
+ }));
+ }, "Same-site window, forced");
+
+ async_test(t => {
+ let w = window.open("https://{{hosts[alt][www]}}:{{ports[https][0]}}/fetch/metadata/resources/post-to-owner.py");
+ t.add_cleanup(_ => w.close());
+ window.addEventListener('message', t.step_func(e => {
+ if (e.source != w)
+ return;
+
+ assert_header_equals(e.data, {
+ "site": "cross-site",
+ "user": "",
+ "mode": "navigate",
+ "dest": "document"
+ }, "Cross-site window, forced");
+ t.done();
+ }));
+ }, "Cross-site window, forced");
+
+ async_test(t => {
+ let w = window.open("https://{{host}}:{{ports[https][0]}}/fetch/metadata/resources/post-to-owner.py");
+ t.add_cleanup(_ => w.close());
+ let messages = 0;
+ window.addEventListener('message', t.step_func(e => {
+ messages++;
+ if (e.source != w)
+ return;
+
+ assert_header_equals(e.data, {
+ "site": "same-origin",
+ "user": "",
+ "mode": "navigate",
+ "dest": "document"
+ }, "Same-origin window, forced, reloaded");
+
+ if (messages == 1) {
+ w.location.reload();
+ } else {
+ t.done();
+ }
+ }));
+ }, "Same-origin window, forced, reloaded");
+
+ async_test(t => {
+ let w = window.open("https://{{hosts[][www]}}:{{ports[https][0]}}/fetch/metadata/resources/post-to-owner.py");
+ t.add_cleanup(_ => w.close());
+ let messages = 0;
+ window.addEventListener('message', t.step_func(e => {
+ messages++;
+ if (e.source != w)
+ return;
+
+ assert_header_equals(e.data, {
+ "site": "same-site",
+ "user": "",
+ "mode": "navigate",
+ "dest": "document"
+ }, "Same-site window, forced, reloaded");
+
+ if (messages == 1) {
+ w.location.reload();
+ } else {
+ t.done();
+ }
+ }));
+ }, "Same-site window, forced, reloaded");
+
+ async_test(t => {
+ let w = window.open("https://{{hosts[alt][www]}}:{{ports[https][0]}}/fetch/metadata/resources/post-to-owner.py");
+ t.add_cleanup(_ => w.close());
+ let messages = 0;
+ window.addEventListener('message', t.step_func(e => {
+ messages++;
+ if (e.source != w)
+ return;
+
+ assert_header_equals(e.data, {
+ "site": "cross-site",
+ "user": "",
+ "mode": "navigate",
+ "dest": "document"
+ }, "Cross-site window, forced, reloaded");
+
+ if (messages == 1) {
+ w.location.reload();
+ } else {
+ t.done();
+ }
+ }));
+ }, "Cross-site window, forced, reloaded");
+
+ // User-activated navigations:
+ async_test(t => {
+ let b = document.createElement('button');
+ b.onclick = t.step_func(_ => {
+ let w = window.open("https://{{host}}:{{ports[https][0]}}/fetch/metadata/resources/post-to-owner.py");
+ t.add_cleanup(_ => w.close());
+ window.addEventListener('message', t.step_func(e => {
+ if (e.source != w)
+ return;
+
+ assert_header_equals(e.data, {
+ "site": "same-origin",
+ "user": "?1",
+ "mode": "navigate",
+ "dest": "document"
+ }, "Same-origin window, user-activated");
+ t.done();
+ }));
+ });
+ document.body.appendChild(b);
+ test_driver.click(b);
+ }, "Same-origin window, user-activated");
+
+ async_test(t => {
+ let b = document.createElement('button');
+ b.onclick = t.step_func(_ => {
+ let w = window.open("https://{{hosts[][www]}}:{{ports[https][0]}}/fetch/metadata/resources/post-to-owner.py");
+ t.add_cleanup(_ => w.close());
+ window.addEventListener('message', t.step_func(e => {
+ if (e.source != w)
+ return;
+
+ assert_header_equals(e.data, {
+ "site": "same-site",
+ "user": "?1",
+ "mode": "navigate",
+ "dest": "document"
+ }, "Same-site window, user-activated");
+ t.done();
+ }));
+ });
+ document.body.appendChild(b);
+ test_driver.click(b);
+ }, "Same-site window, user-activated");
+
+ async_test(t => {
+ let b = document.createElement('button');
+ b.onclick = t.step_func(_ => {
+ let w = window.open("https://{{hosts[alt][www]}}:{{ports[https][0]}}/fetch/metadata/resources/post-to-owner.py");
+ t.add_cleanup(_ => w.close());
+ window.addEventListener('message', t.step_func(e => {
+ if (e.source != w)
+ return;
+
+ assert_header_equals(e.data, {
+ "site": "cross-site",
+ "user": "?1",
+ "mode": "navigate",
+ "dest": "document"
+ }, "Cross-site window, user-activated");
+ t.done();
+ }));
+ });
+ document.body.appendChild(b);
+ test_driver.click(b);
+ }, "Cross-site window, user-activated");
+</script>
diff --git a/test/wpt/tests/fetch/metadata/worker.https.sub.html b/test/wpt/tests/fetch/metadata/worker.https.sub.html
new file mode 100644
index 0000000..20a4fe5
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/worker.https.sub.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+
+<link rel="author" href="mtrzos@google.com" title="Maciek Trzos">
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=/fetch/metadata/resources/helper.js></script>
+<script src=/common/utils.js></script>
+<script>
+ let nonce = token();
+
+ promise_test(t => {
+ return new Promise((resolve, reject) => {
+ let key = "worker-same-origin" + nonce;
+ let w = new Worker("/fetch/metadata/resources/record-header.py?file=" + key);
+ w.onmessage = e => {
+ let expected = {"site":"same-origin", "user":"", "mode": "same-origin", "dest": "worker"};
+ validate_expectations(key, expected)
+ .then(_ => resolve())
+ .catch(e => reject(e));
+ };
+ });
+ }, "Same-Origin worker");
+</script>
+<body></body>
diff --git a/test/wpt/tests/fetch/metadata/xslt.https.sub.html b/test/wpt/tests/fetch/metadata/xslt.https.sub.html
new file mode 100644
index 0000000..dc72d7b
--- /dev/null
+++ b/test/wpt/tests/fetch/metadata/xslt.https.sub.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+
+<link rel="author" href="mtrzos@google.com" title="Maciek Trzos">
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=/fetch/metadata/resources/helper.js></script>
+<script src=/common/utils.js></script>
+<script>
+ // Open a window with XML document which loads resources via <?xml-stylesheet/> tag
+ let nonce = token();
+ let w = window.open("resources/xslt-test.sub.xml?token=" + nonce);
+ window.addEventListener('message', function(e) {
+ if (e.source != w)
+ return;
+
+ // Only testing same-origin XSLT because same-site and cross-site XSLT is blocked.
+ promise_test(t => {
+ let expected = {"site":"same-origin", "user":"", "mode": "same-origin", "dest": "xslt"};
+ return validate_expectations("xslt-same-origin" + nonce, expected);
+ }, "Same-Origin xslt");
+
+ w.close();
+ });
+
+</script>
diff --git a/test/wpt/tests/fetch/nosniff/image.html b/test/wpt/tests/fetch/nosniff/image.html
new file mode 100644
index 0000000..9dfdb94
--- /dev/null
+++ b/test/wpt/tests/fetch/nosniff/image.html
@@ -0,0 +1,39 @@
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<div id=log></div>
+<script>
+ // Note: images get always sniffed, nosniff doesn't do anything
+ // (but note the tentative Cross-Origin Read Blocking (CORB) tests
+ // - for example wpt/fetch/corb/img-mime-types-coverage.tentative.sub.html).
+ var passes = [
+ // Empty or non-sensical MIME types
+ null, "", "x", "x/x",
+
+ // Image MIME types
+ "image/gif", "image/png", "image/png;blah", "image/svg+xml",
+
+ // CORB-protected MIME types (but note that CORB doesn't apply here,
+ // because CORB ignores same-origin requests).
+ "text/html", "application/xml", "application/blah+xml"
+ ]
+
+ const get_url = (mime) => {
+ let url = "resources/image.py"
+ if (mime != null) {
+ url += "?type=" + encodeURIComponent(mime)
+ }
+ return url
+ }
+
+ passes.forEach(function(mime) {
+ async_test(function(t) {
+ var img = document.createElement("img")
+ img.onerror = t.unreached_func("Unexpected error event")
+ img.onload = t.step_func_done(function(){
+ assert_equals(img.width, 96)
+ })
+ img.src = get_url(mime)
+ document.body.appendChild(img)
+ }, "URL query: " + mime)
+ })
+</script>
diff --git a/test/wpt/tests/fetch/nosniff/importscripts.html b/test/wpt/tests/fetch/nosniff/importscripts.html
new file mode 100644
index 0000000..920b6bd
--- /dev/null
+++ b/test/wpt/tests/fetch/nosniff/importscripts.html
@@ -0,0 +1,14 @@
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<div id=log></div>
+<script>
+ async_test(function(t) {
+ var w = new Worker("importscripts.js")
+ w.onmessage = t.step_func(function(e) {
+ if(e.data == "END")
+ t.done()
+ else
+ assert_equals(e.data, "PASS")
+ })
+ }, "Test importScripts()")
+</script>
diff --git a/test/wpt/tests/fetch/nosniff/importscripts.js b/test/wpt/tests/fetch/nosniff/importscripts.js
new file mode 100644
index 0000000..1895280
--- /dev/null
+++ b/test/wpt/tests/fetch/nosniff/importscripts.js
@@ -0,0 +1,28 @@
+// Testing importScripts()
+function log(w) { this.postMessage(w) }
+function f() { log("FAIL") }
+function p() { log("PASS") }
+
+const get_url = (mime, outcome) => {
+ let url = "resources/js.py"
+ if (mime != null) {
+ url += "?type=" + encodeURIComponent(mime)
+ }
+ if (outcome) {
+ url += "&outcome=p"
+ }
+ return url
+}
+
+[null, "", "x", "x/x", "text/html", "text/json"].forEach(function(mime) {
+ try {
+ importScripts(get_url(mime))
+ } catch(e) {
+ (e.name == "NetworkError") ? p() : log("FAIL (no NetworkError exception): " + mime)
+ }
+
+})
+importScripts(get_url("text/javascript", true))
+importScripts(get_url("text/ecmascript", true))
+importScripts(get_url("text/ecmascript;blah", true))
+log("END")
diff --git a/test/wpt/tests/fetch/nosniff/parsing-nosniff.window.js b/test/wpt/tests/fetch/nosniff/parsing-nosniff.window.js
new file mode 100644
index 0000000..2a26486
--- /dev/null
+++ b/test/wpt/tests/fetch/nosniff/parsing-nosniff.window.js
@@ -0,0 +1,27 @@
+promise_test(() => fetch("resources/x-content-type-options.json").then(res => res.json()).then(runTests), "Loading JSON…");
+
+function runTests(allTestData) {
+ for (let i = 0; i < allTestData.length; i++) {
+ const testData = allTestData[i],
+ input = encodeURIComponent(testData.input);
+ promise_test(t => {
+ let resolve;
+ const promise = new Promise(r => resolve = r);
+ const script = document.createElement("script");
+ t.add_cleanup(() => script.remove());
+ // A <script> element loading a classic script does not care about the MIME type, unless
+ // X-Content-Type-Options: nosniff is specified, in which case a JavaScript MIME type is
+ // enforced, which x/x is not.
+ if (testData.nosniff) {
+ script.onerror = resolve;
+ script.onload = t.unreached_func("Script should not have loaded");
+ } else {
+ script.onerror = t.unreached_func("Script should have loaded");
+ script.onload = resolve;
+ }
+ script.src = "resources/nosniff.py?nosniff=" + input;
+ document.body.appendChild(script);
+ return promise;
+ }, input);
+ }
+}
diff --git a/test/wpt/tests/fetch/nosniff/resources/css.py b/test/wpt/tests/fetch/nosniff/resources/css.py
new file mode 100644
index 0000000..8afb569
--- /dev/null
+++ b/test/wpt/tests/fetch/nosniff/resources/css.py
@@ -0,0 +1,23 @@
+def main(request, response):
+ type = request.GET.first(b"type", None)
+ is_revalidation = request.headers.get(b"If-Modified-Since", None)
+
+ content = b"/* nothing to see here */"
+
+ response.add_required_headers = False
+ if is_revalidation is not None:
+ response.writer.write_status(304)
+ response.writer.write_header(b"x-content-type-options", b"nosniff")
+ response.writer.write_header(b"content-length", 0)
+ if(type != None):
+ response.writer.write_header(b"content-type", type)
+ response.writer.end_headers()
+ response.writer.write(b"")
+ else:
+ response.writer.write_status(200)
+ response.writer.write_header(b"x-content-type-options", b"nosniff")
+ response.writer.write_header(b"content-length", len(content))
+ if(type != None):
+ response.writer.write_header(b"content-type", type)
+ response.writer.end_headers()
+ response.writer.write(content)
diff --git a/test/wpt/tests/fetch/nosniff/resources/image.py b/test/wpt/tests/fetch/nosniff/resources/image.py
new file mode 100644
index 0000000..9fd367c
--- /dev/null
+++ b/test/wpt/tests/fetch/nosniff/resources/image.py
@@ -0,0 +1,24 @@
+import os.path
+
+from wptserve.utils import isomorphic_decode
+
+def main(request, response):
+ type = request.GET.first(b"type", None)
+
+ if type != None and b"svg" in type:
+ filename = u"green-96x96.svg"
+ else:
+ filename = u"blue96x96.png"
+
+ path = os.path.join(os.path.dirname(isomorphic_decode(__file__)), u"../../../images", filename)
+ body = open(path, u"rb").read()
+
+ response.add_required_headers = False
+ response.writer.write_status(200)
+ response.writer.write_header(b"x-content-type-options", b"nosniff")
+ response.writer.write_header(b"content-length", len(body))
+ if(type != None):
+ response.writer.write_header(b"content-type", type)
+ response.writer.end_headers()
+
+ response.writer.write(body)
diff --git a/test/wpt/tests/fetch/nosniff/resources/js.py b/test/wpt/tests/fetch/nosniff/resources/js.py
new file mode 100644
index 0000000..784050a
--- /dev/null
+++ b/test/wpt/tests/fetch/nosniff/resources/js.py
@@ -0,0 +1,17 @@
+def main(request, response):
+ outcome = request.GET.first(b"outcome", b"f")
+ type = request.GET.first(b"type", b"Content-Type missing")
+
+ content = b"// nothing to see here"
+ content += b"\n"
+ content += b"log('FAIL: " + type + b"')" if (outcome == b"f") else b"p()"
+
+ response.add_required_headers = False
+ response.writer.write_status(200)
+ response.writer.write_header(b"x-content-type-options", b"nosniff")
+ response.writer.write_header(b"content-length", len(content))
+ if(type != b"Content-Type missing"):
+ response.writer.write_header(b"content-type", type)
+ response.writer.end_headers()
+
+ response.writer.write(content)
diff --git a/test/wpt/tests/fetch/nosniff/resources/nosniff.py b/test/wpt/tests/fetch/nosniff/resources/nosniff.py
new file mode 100644
index 0000000..159ecfb
--- /dev/null
+++ b/test/wpt/tests/fetch/nosniff/resources/nosniff.py
@@ -0,0 +1,11 @@
+def main(request, response):
+ response.add_required_headers = False
+ output = b"HTTP/1.1 220 YOU HAVE NO POWER HERE\r\n"
+ output += b"Content-Length: 22\r\n"
+ output += b"Connection: close\r\n"
+ output += b"Content-Type: x/x\r\n"
+ output += request.GET.first(b"nosniff") + b"\r\n"
+ output += b"\r\n"
+ output += b"// nothing to see here"
+ response.writer.write(output)
+ response.close_connection = True
diff --git a/test/wpt/tests/fetch/nosniff/resources/worker.py b/test/wpt/tests/fetch/nosniff/resources/worker.py
new file mode 100644
index 0000000..2d7e3f6
--- /dev/null
+++ b/test/wpt/tests/fetch/nosniff/resources/worker.py
@@ -0,0 +1,16 @@
+def main(request, response):
+ type = request.GET.first(b"type", None)
+
+ content = b"// nothing to see here"
+ content += b"\n"
+ content += b"this.postMessage('hi')"
+
+ response.add_required_headers = False
+ response.writer.write_status(200)
+ response.writer.write_header(b"x-content-type-options", b"nosniff")
+ response.writer.write_header(b"content-length", len(content))
+ if(type != None):
+ response.writer.write_header(b"content-type", type)
+ response.writer.end_headers()
+
+ response.writer.write(content)
diff --git a/test/wpt/tests/fetch/nosniff/resources/x-content-type-options.json b/test/wpt/tests/fetch/nosniff/resources/x-content-type-options.json
new file mode 100644
index 0000000..080fc19
--- /dev/null
+++ b/test/wpt/tests/fetch/nosniff/resources/x-content-type-options.json
@@ -0,0 +1,62 @@
+[
+ {
+ "input": "X-Content-Type-Options: NOSNIFF",
+ "nosniff": true
+ },
+ {
+ "input": "x-content-type-OPTIONS: nosniff",
+ "nosniff": true
+ },
+ {
+ "input": "X-Content-Type-Options: nosniff,,@#$#%%&^&^*()()11!",
+ "nosniff": true
+ },
+ {
+ "input": "X-Content-Type-Options: @#$#%%&^&^*()()11!,nosniff",
+ "nosniff": false
+ },
+ {
+ "input": "X-Content-Type-Options: nosniff\r\nX-Content-Type-Options: no",
+ "nosniff": true
+ },
+ {
+ "input": "X-Content-Type-Options: no\r\nX-Content-Type-Options: nosniff",
+ "nosniff": false
+ },
+ {
+ "input": "X-Content-Type-Options:\r\nX-Content-Type-Options: nosniff",
+ "nosniff": false
+ },
+ {
+ "input": "X-Content-Type-Options: nosniff\r\nX-Content-Type-Options: nosniff",
+ "nosniff": true
+ },
+ {
+ "input": "X-Content-Type-Options: ,nosniff",
+ "nosniff": false
+ },
+ {
+ "input": "X-Content-Type-Options: nosniff\u000C",
+ "nosniff": false
+ },
+ {
+ "input": "X-Content-Type-Options: nosniff\u000B",
+ "nosniff": false
+ },
+ {
+ "input": "X-Content-Type-Options: nosniff\u000B,nosniff",
+ "nosniff": false
+ },
+ {
+ "input": "X-Content-Type-Options: 'NosniFF'",
+ "nosniff": false
+ },
+ {
+ "input": "X-Content-Type-Options: \"nosniFF\"",
+ "nosniff": false
+ },
+ {
+ "input": "Content-Type-Options: nosniff",
+ "nosniff": false
+ }
+]
diff --git a/test/wpt/tests/fetch/nosniff/script.html b/test/wpt/tests/fetch/nosniff/script.html
new file mode 100644
index 0000000..e0b5dac
--- /dev/null
+++ b/test/wpt/tests/fetch/nosniff/script.html
@@ -0,0 +1,43 @@
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<div id=log></div>
+<script>
+ var log = function() {}, // see comment below
+ p = function() {}, // see comment below
+ fails = [null, "", "x", "x/x", "text/html", "text/json"],
+ passes = ["text/javascript", "text/ecmascript", "text/ecmascript;blah", "text/javascript1.0"]
+
+ // Ideally we'd also check whether the scripts in fact execute, but that would involve
+ // timers and might get a bit racy without cross-browser support for the execute events.
+
+ const get_url = (mime, outcome) => {
+ let url = "resources/js.py"
+ if (mime != null) {
+ url += "?type=" + encodeURIComponent(mime)
+ }
+ if (outcome) {
+ url += "&outcome=p"
+ }
+ return url
+ }
+
+ fails.forEach(function(mime) {
+ async_test(function(t) {
+ var script = document.createElement("script")
+ script.onerror = t.step_func_done(function(){})
+ script.onload = t.unreached_func("Unexpected load event")
+ script.src = get_url(mime)
+ document.body.appendChild(script)
+ }, "URL query: " + mime)
+ })
+
+ passes.forEach(function(mime) {
+ async_test(function(t) {
+ var script = document.createElement("script")
+ script.onerror = t.unreached_func("Unexpected error event")
+ script.onload = t.step_func_done(function(){})
+ script.src = get_url(mime, true)
+ document.body.appendChild(script)
+ }, "URL query: " + mime)
+ })
+</script>
diff --git a/test/wpt/tests/fetch/nosniff/stylesheet.html b/test/wpt/tests/fetch/nosniff/stylesheet.html
new file mode 100644
index 0000000..8f2b547
--- /dev/null
+++ b/test/wpt/tests/fetch/nosniff/stylesheet.html
@@ -0,0 +1,60 @@
+<!-- quirks mode is important, text/css is already required otherwise -->
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<div id=log></div>
+<script>
+ var fails = [null, "", "x", "x/x", "text/html", "text/json"],
+ passes = ["text/css", "text/css;charset=utf-8", "text/css;blah"]
+
+ const get_url = (mime) => {
+ let url = "resources/css.py"
+ if (mime != null) {
+ url += "?type=" + encodeURIComponent(mime)
+ }
+ return url
+ }
+
+ fails.forEach(function(mime) {
+ async_test(function(t) {
+ var link = document.createElement("link")
+ link.rel = "stylesheet"
+ link.onerror = t.step_func_done()
+ link.onload = t.unreached_func("Unexpected load event")
+ link.href = get_url(mime)
+ document.body.appendChild(link)
+ }, "URL query: " + mime)
+ })
+
+ fails.forEach(function(mime) {
+ async_test(function(t) {
+ var link = document.createElement("link")
+ link.rel = "stylesheet"
+ link.onerror = t.step_func_done()
+ link.onload = t.unreached_func("Unexpected load event")
+ link.href = get_url(mime)
+ document.body.appendChild(link)
+ }, "Revalidated URL query: " + mime)
+ })
+
+ passes.forEach(function(mime) {
+ async_test(function(t) {
+ var link = document.createElement("link")
+ link.rel = "stylesheet"
+ link.onerror = t.unreached_func("Unexpected error event")
+ link.onload = t.step_func_done()
+ link.href = get_url(mime)
+ document.body.appendChild(link)
+ }, "URL query: " + mime)
+ })
+
+ passes.forEach(function(mime) {
+ async_test(function(t) {
+ var link = document.createElement("link")
+ link.rel = "stylesheet"
+ link.onerror = t.unreached_func("Unexpected error event")
+ link.onload = t.step_func_done()
+ link.href = get_url(mime)
+ document.body.appendChild(link)
+ }, "Revalidated URL query: " + mime)
+ })
+</script>
diff --git a/test/wpt/tests/fetch/nosniff/worker.html b/test/wpt/tests/fetch/nosniff/worker.html
new file mode 100644
index 0000000..c8c1076
--- /dev/null
+++ b/test/wpt/tests/fetch/nosniff/worker.html
@@ -0,0 +1,28 @@
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<div id=log></div>
+<script>
+ var workers = [],
+ fails = ["", "?type=", "?type=x", "?type=x/x", "?type=text/html", "?type=text/json"],
+ passes = ["?type=text/javascript", "?type=text/ecmascript", "?type=text/ecmascript;yay"]
+
+ fails.forEach(function(urlpart) {
+ async_test(function(t) {
+ var w = new Worker("resources/worker.py" + urlpart)
+ w.onmessage = t.unreached_func("Unexpected message event")
+ w.onerror = t.step_func_done(function(){})
+ workers.push(w) // avoid GC
+ }, "URL query: " + urlpart)
+ })
+
+ passes.forEach(function(urlpart) {
+ async_test(function(t) {
+ var w = new Worker("resources/worker.py" + urlpart)
+ w.onmessage = t.step_func_done(function(e){
+ assert_equals(e.data, "hi")
+ })
+ w.onerror = t.unreached_func("Unexpected error event")
+ workers.push(w) // avoid GC
+ }, "URL query: " + urlpart)
+ })
+</script>
diff --git a/test/wpt/tests/fetch/orb/resources/data.json b/test/wpt/tests/fetch/orb/resources/data.json
new file mode 100644
index 0000000..f2a886f
--- /dev/null
+++ b/test/wpt/tests/fetch/orb/resources/data.json
@@ -0,0 +1,3 @@
+{
+ "hello": "world"
+}
diff --git a/test/wpt/tests/fetch/orb/resources/data_non_ascii.json b/test/wpt/tests/fetch/orb/resources/data_non_ascii.json
new file mode 100644
index 0000000..64566c5
--- /dev/null
+++ b/test/wpt/tests/fetch/orb/resources/data_non_ascii.json
@@ -0,0 +1 @@
+["你好"]
diff --git a/test/wpt/tests/fetch/orb/resources/empty.json b/test/wpt/tests/fetch/orb/resources/empty.json
new file mode 100644
index 0000000..0967ef4
--- /dev/null
+++ b/test/wpt/tests/fetch/orb/resources/empty.json
@@ -0,0 +1 @@
+{}
diff --git a/test/wpt/tests/fetch/orb/resources/font.ttf b/test/wpt/tests/fetch/orb/resources/font.ttf
new file mode 100644
index 0000000..9023592
--- /dev/null
+++ b/test/wpt/tests/fetch/orb/resources/font.ttf
Binary files differ
diff --git a/test/wpt/tests/fetch/orb/resources/image.png b/test/wpt/tests/fetch/orb/resources/image.png
new file mode 100644
index 0000000..820f8ca
--- /dev/null
+++ b/test/wpt/tests/fetch/orb/resources/image.png
Binary files differ
diff --git a/test/wpt/tests/fetch/orb/resources/js-unlabeled-utf16-without-bom.json b/test/wpt/tests/fetch/orb/resources/js-unlabeled-utf16-without-bom.json
new file mode 100644
index 0000000..157a8f5
--- /dev/null
+++ b/test/wpt/tests/fetch/orb/resources/js-unlabeled-utf16-without-bom.json
Binary files differ
diff --git a/test/wpt/tests/fetch/orb/resources/js-unlabeled.js b/test/wpt/tests/fetch/orb/resources/js-unlabeled.js
new file mode 100644
index 0000000..a880a5b
--- /dev/null
+++ b/test/wpt/tests/fetch/orb/resources/js-unlabeled.js
@@ -0,0 +1 @@
+window.has_executed_script = true;
diff --git a/test/wpt/tests/fetch/orb/resources/png-mislabeled-as-html.png b/test/wpt/tests/fetch/orb/resources/png-mislabeled-as-html.png
new file mode 100644
index 0000000..820f8ca
--- /dev/null
+++ b/test/wpt/tests/fetch/orb/resources/png-mislabeled-as-html.png
Binary files differ
diff --git a/test/wpt/tests/fetch/orb/resources/png-mislabeled-as-html.png.headers b/test/wpt/tests/fetch/orb/resources/png-mislabeled-as-html.png.headers
new file mode 100644
index 0000000..156209f
--- /dev/null
+++ b/test/wpt/tests/fetch/orb/resources/png-mislabeled-as-html.png.headers
@@ -0,0 +1 @@
+Content-Type: text/html
diff --git a/test/wpt/tests/fetch/orb/resources/png-unlabeled.png b/test/wpt/tests/fetch/orb/resources/png-unlabeled.png
new file mode 100644
index 0000000..820f8ca
--- /dev/null
+++ b/test/wpt/tests/fetch/orb/resources/png-unlabeled.png
Binary files differ
diff --git a/test/wpt/tests/fetch/orb/resources/script-asm-js-invalid.js b/test/wpt/tests/fetch/orb/resources/script-asm-js-invalid.js
new file mode 100644
index 0000000..8d1bbd6
--- /dev/null
+++ b/test/wpt/tests/fetch/orb/resources/script-asm-js-invalid.js
@@ -0,0 +1,4 @@
+function f() {
+ "use asm";
+ return;
+}
diff --git a/test/wpt/tests/fetch/orb/resources/script-asm-js-valid.js b/test/wpt/tests/fetch/orb/resources/script-asm-js-valid.js
new file mode 100644
index 0000000..79b375f
--- /dev/null
+++ b/test/wpt/tests/fetch/orb/resources/script-asm-js-valid.js
@@ -0,0 +1,4 @@
+function f() {
+ "use asm";
+ return {};
+}
diff --git a/test/wpt/tests/fetch/orb/resources/script-iso-8559-1.js b/test/wpt/tests/fetch/orb/resources/script-iso-8559-1.js
new file mode 100644
index 0000000..3bccb6a
--- /dev/null
+++ b/test/wpt/tests/fetch/orb/resources/script-iso-8559-1.js
@@ -0,0 +1,4 @@
+"use strict";
+function fn() {
+ return "§A¦n";
+}
diff --git a/test/wpt/tests/fetch/orb/resources/script-utf16-bom.js b/test/wpt/tests/fetch/orb/resources/script-utf16-bom.js
new file mode 100644
index 0000000..16b76e9
--- /dev/null
+++ b/test/wpt/tests/fetch/orb/resources/script-utf16-bom.js
Binary files differ
diff --git a/test/wpt/tests/fetch/orb/resources/script-utf16-without-bom.js b/test/wpt/tests/fetch/orb/resources/script-utf16-without-bom.js
new file mode 100644
index 0000000..d983086
--- /dev/null
+++ b/test/wpt/tests/fetch/orb/resources/script-utf16-without-bom.js
Binary files differ
diff --git a/test/wpt/tests/fetch/orb/resources/script.js b/test/wpt/tests/fetch/orb/resources/script.js
new file mode 100644
index 0000000..19675d2
--- /dev/null
+++ b/test/wpt/tests/fetch/orb/resources/script.js
@@ -0,0 +1,4 @@
+"use strict";
+function fn() {
+ return 42;
+}
diff --git a/test/wpt/tests/fetch/orb/resources/sound.mp3 b/test/wpt/tests/fetch/orb/resources/sound.mp3
new file mode 100644
index 0000000..a15d1de
--- /dev/null
+++ b/test/wpt/tests/fetch/orb/resources/sound.mp3
Binary files differ
diff --git a/test/wpt/tests/fetch/orb/resources/text.txt b/test/wpt/tests/fetch/orb/resources/text.txt
new file mode 100644
index 0000000..270c611
--- /dev/null
+++ b/test/wpt/tests/fetch/orb/resources/text.txt
@@ -0,0 +1 @@
+hello, world!
diff --git a/test/wpt/tests/fetch/orb/resources/utils.js b/test/wpt/tests/fetch/orb/resources/utils.js
new file mode 100644
index 0000000..94a2177
--- /dev/null
+++ b/test/wpt/tests/fetch/orb/resources/utils.js
@@ -0,0 +1,18 @@
+function header(name, value) {
+ return `header(${name},${value})`;
+}
+
+function contentType(type) {
+ return header("Content-Type", type);
+}
+
+function contentTypeOptions(type) {
+ return header("X-Content-Type-Options", type);
+}
+
+function fetchORB(file, options, ...pipe) {
+ return fetch(`${file}${pipe.length ? `?pipe=${pipe.join("|")}` : ""}`, {
+ ...(options || {}),
+ mode: "no-cors",
+ });
+}
diff --git a/test/wpt/tests/fetch/orb/tentative/compressed-image-sniffing.sub.html b/test/wpt/tests/fetch/orb/tentative/compressed-image-sniffing.sub.html
new file mode 100644
index 0000000..38e70c6
--- /dev/null
+++ b/test/wpt/tests/fetch/orb/tentative/compressed-image-sniffing.sub.html
@@ -0,0 +1,20 @@
+<!-- Test verifies that compressed images should not be blocked
+-->
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<div id=log></div>
+<script>
+async_test(function(t) {
+ let url = "http://{{domains[www1]}}:{{ports[http][0]}}"
+ url = url + "/fetch/orb/resources/png-unlabeled.png?pipe=gzip"
+
+ const img = document.createElement("img");
+ img.src = url;
+ img.onerror = t.unreached_func("Unexpected error event")
+ img.onload = t.step_func_done(function () {
+ assert_true(true);
+ })
+ document.body.appendChild(img)
+}, "ORB shouldn't block compressed images");
+</script>
+
diff --git a/test/wpt/tests/fetch/orb/tentative/content-range.sub.any.js b/test/wpt/tests/fetch/orb/tentative/content-range.sub.any.js
new file mode 100644
index 0000000..ee97521
--- /dev/null
+++ b/test/wpt/tests/fetch/orb/tentative/content-range.sub.any.js
@@ -0,0 +1,31 @@
+// META: script=/fetch/orb/resources/utils.js
+
+const url =
+ "http://{{domains[www1]}}:{{ports[http][0]}}/fetch/orb/resources/image.png";
+
+promise_test(async () => {
+ let headers = new Headers([["Range", "bytes=0-99"]]);
+ await fetchORB(
+ url,
+ { headers },
+ header("Content-Range", "bytes 0-99/1010"),
+ "slice(null,100)",
+ "status(206)"
+ );
+}, "ORB shouldn't block opaque range of image/png starting at zero");
+
+promise_test(
+ t =>
+ promise_rejects_js(
+ t,
+ TypeError,
+ fetchORB(
+ url,
+ { headers: new Headers([["Range", "bytes 10-99"]]) },
+ header("Content-Range", "bytes 10-99/1010"),
+ "slice(10,100)",
+ "status(206)"
+ )
+ ),
+ "ORB should block opaque range of image/png not starting at zero, that isn't subsequent"
+);
diff --git a/test/wpt/tests/fetch/orb/tentative/img-mime-types-coverage.tentative.sub.html b/test/wpt/tests/fetch/orb/tentative/img-mime-types-coverage.tentative.sub.html
new file mode 100644
index 0000000..5dc6c5d
--- /dev/null
+++ b/test/wpt/tests/fetch/orb/tentative/img-mime-types-coverage.tentative.sub.html
@@ -0,0 +1,126 @@
+<!-- Test verifies that cross-origin, nosniff images are 1) blocked when their
+ MIME type is covered by ORB and 2) allowed otherwise.
+
+ This test is very similar to fetch/orb/img-mime-types-coverage.tentative.sub.html,
+ except that it focuses on MIME types relevant to ORB.
+-->
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<div id=log></div>
+<script>
+ var passes = [
+ // ORB safelisted MIME-types - i.e. ones covered by:
+ // - https://github.com/annevk/orb
+
+ "text/css",
+ "image/svg+xml",
+
+ // JavaScript MIME types
+ "application/ecmascript",
+ "application/javascript",
+ "application/x-ecmascript",
+ "application/x-javascript",
+ "text/ecmascript",
+ "text/javascript",
+ "text/javascript1.0",
+ "text/javascript1.1",
+ "text/javascript1.2",
+ "text/javascript1.3",
+ "text/javascript1.4",
+ "text/javascript1.5",
+ "text/jscript",
+ "text/livescript",
+ "text/x-ecmascript",
+ "text/x-javascript",
+ ]
+
+ var fails = [
+ // ORB blocklisted MIME-types - i.e. ones covered by:
+ // - https://github.com/annevk/orb
+
+ "text/html",
+
+ // JSON MIME type
+ "application/json",
+ "text/json",
+ "application/ld+json",
+
+ // XML MIME type
+ "text/xml",
+ "application/xml",
+ "application/xhtml+xml",
+
+ "application/dash+xml",
+ "application/gzip",
+ "application/msexcel",
+ "application/mspowerpoint",
+ "application/msword",
+ "application/msword-template",
+ "application/pdf",
+ "application/vnd.apple.mpegurl",
+ "application/vnd.ces-quickpoint",
+ "application/vnd.ces-quicksheet",
+ "application/vnd.ces-quickword",
+ "application/vnd.ms-excel",
+ "application/vnd.ms-excel.sheet.macroenabled.12",
+ "application/vnd.ms-powerpoint",
+ "application/vnd.ms-powerpoint.presentation.macroenabled.12",
+ "application/vnd.ms-word",
+ "application/vnd.ms-word.document.12",
+ "application/vnd.ms-word.document.macroenabled.12",
+ "application/vnd.msword",
+ "application/vnd.openxmlformats-officedocument.presentationml.presentation",
+ "application/vnd.openxmlformats-officedocument.presentationml.template",
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.template",
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.template",
+ "application/vnd.presentation-openxml",
+ "application/vnd.presentation-openxmlm",
+ "application/vnd.spreadsheet-openxml",
+ "application/vnd.wordprocessing-openxml",
+ "application/x-gzip",
+ "application/x-protobuf",
+ "application/x-protobuffer",
+ "application/zip",
+ "audio/mpegurl",
+ "multipart/byteranges",
+ "multipart/signed",
+ "text/event-stream",
+ "text/csv",
+ "text/vtt",
+]
+
+ const get_url = (mime) => {
+ // www1 is cross-origin, so the HTTP response is ORB-eligible -->
+ url = "http://{{domains[www1]}}:{{ports[http][0]}}"
+ url = url + "/fetch/nosniff/resources/image.py"
+ if (mime != null) {
+ url += "?type=" + encodeURIComponent(mime)
+ }
+ return url
+ }
+
+ passes.forEach(function (mime) {
+ async_test(function (t) {
+ var img = document.createElement("img")
+ img.onerror = t.unreached_func("Unexpected error event")
+ img.onload = t.step_func_done(function () {
+ assert_equals(img.width, 96)
+ })
+ img.src = get_url(mime)
+ document.body.appendChild(img)
+ }, "ORB should allow the response if Content-Type is: '" + mime + "'. ")
+ })
+
+ fails.forEach(function (mime) {
+ async_test(function (t) {
+ var img = document.createElement("img")
+ img.onerror = t.step_func_done()
+ img.onload = t.unreached_func("Unexpected load event")
+ img.src = get_url(mime)
+ document.body.appendChild(img)
+ }, "ORB should block the response if Content-Type is: '" + mime + "'. ")
+ })
+</script>
+
diff --git a/test/wpt/tests/fetch/orb/tentative/img-png-mislabeled-as-html.sub-ref.html b/test/wpt/tests/fetch/orb/tentative/img-png-mislabeled-as-html.sub-ref.html
new file mode 100644
index 0000000..66462fb
--- /dev/null
+++ b/test/wpt/tests/fetch/orb/tentative/img-png-mislabeled-as-html.sub-ref.html
@@ -0,0 +1,5 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<!-- Same-origin, so the HTTP response is not ORB-eligible. -->
+<img src="../resources/png-mislabeled-as-html.png">
+
diff --git a/test/wpt/tests/fetch/orb/tentative/img-png-mislabeled-as-html.sub.html b/test/wpt/tests/fetch/orb/tentative/img-png-mislabeled-as-html.sub.html
new file mode 100644
index 0000000..aa03f4d
--- /dev/null
+++ b/test/wpt/tests/fetch/orb/tentative/img-png-mislabeled-as-html.sub.html
@@ -0,0 +1,7 @@
+<!DOCTYPE html>
+<!-- Test verifies that ORB allows an mislabeled cross-origin image after sniffing. -->
+<meta charset="utf-8">
+<!-- Reference page uses same-origin resources, which are not ORB-eligible. -->
+<link rel="match" href="img-png-mislabeled-as-html.sub-ref.html">
+<!-- www1 is cross-origin, so the HTTP response is ORB-eligible -->
+<img src="http://{{domains[www1]}}:{{ports[http][0]}}/fetch/orb/resources/png-mislabeled-as-html.png">
diff --git a/test/wpt/tests/fetch/orb/tentative/img-png-unlabeled.sub-ref.html b/test/wpt/tests/fetch/orb/tentative/img-png-unlabeled.sub-ref.html
new file mode 100644
index 0000000..2d5e3bb
--- /dev/null
+++ b/test/wpt/tests/fetch/orb/tentative/img-png-unlabeled.sub-ref.html
@@ -0,0 +1,5 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<!-- Same-origin, so the HTTP response is not ORB-eligible. -->
+<img src="../resources/png-unlabeled.png">
+
diff --git a/test/wpt/tests/fetch/orb/tentative/img-png-unlabeled.sub.html b/test/wpt/tests/fetch/orb/tentative/img-png-unlabeled.sub.html
new file mode 100644
index 0000000..77415f6
--- /dev/null
+++ b/test/wpt/tests/fetch/orb/tentative/img-png-unlabeled.sub.html
@@ -0,0 +1,7 @@
+<!DOCTYPE html>
+<!-- Test verifies that ORB allows an unlabeled cross-origin image after sniffing. -->
+<meta charset="utf-8">
+<!-- Reference page uses same-origin resources, which are not ORB-eligible. -->
+<link rel="match" href="img-png-unlabeled.sub-ref.html">
+<!-- www1 is cross-origin, so the HTTP response is ORB-eligible -->
+<img src="http://{{domains[www1]}}:{{ports[http][0]}}/fetch/orb/resources/png-unlabeled.png">
diff --git a/test/wpt/tests/fetch/orb/tentative/known-mime-type.sub.any.js b/test/wpt/tests/fetch/orb/tentative/known-mime-type.sub.any.js
new file mode 100644
index 0000000..b0521e8
--- /dev/null
+++ b/test/wpt/tests/fetch/orb/tentative/known-mime-type.sub.any.js
@@ -0,0 +1,86 @@
+// META: script=/fetch/orb/resources/utils.js
+
+const path = "http://{{domains[www1]}}:{{ports[http][0]}}/fetch/orb/resources";
+
+promise_test(
+ t =>
+ promise_rejects_js(
+ t,
+ TypeError,
+ fetchORB(`${path}/font.ttf`, null, contentType("font/ttf"))
+ ),
+ "ORB should block opaque font/ttf"
+);
+
+promise_test(
+ t =>
+ promise_rejects_js(
+ t,
+ TypeError,
+ fetchORB(`${path}/text.txt`, null, contentType("text/plain"))
+ ),
+ "ORB should block opaque text/plain"
+);
+
+promise_test(
+ t =>
+ promise_rejects_js(
+ t,
+ TypeError,
+ fetchORB(`${path}/data.json`, null, contentType("application/json"))
+ ),
+ "ORB should block opaque application/json (non-empty)"
+);
+
+promise_test(
+ t =>
+ promise_rejects_js(
+ t,
+ TypeError,
+ fetchORB(`${path}/empty.json`, null, contentType("application/json"))
+ ),
+ "ORB should block opaque application/json (empty)"
+);
+
+promise_test(
+ t =>
+ promise_rejects_js(
+ t,
+ TypeError,
+ fetchORB(`${path}/data_non_ascii.json`, null, contentType("application/json"))
+ ),
+ "ORB should block opaque application/json which contains non ascii characters"
+);
+
+promise_test(async () => {
+ fetchORB(`${path}/image.png`, null, contentType("image/png"));
+}, "ORB shouldn't block opaque image/png");
+
+promise_test(async () => {
+ await fetchORB(`${path}/script.js`, null, contentType("text/javascript"));
+}, "ORB shouldn't block opaque text/javascript");
+
+// Test javascript validation can correctly decode the content with BOM.
+promise_test(async () => {
+ await fetchORB(`${path}/script-utf16-bom.js`, null, contentType("application/json"));
+}, "ORB shouldn't block opaque text/javascript (utf16 encoded with BOM)");
+
+// Test javascript validation can correctly decode the content with the http charset hint.
+promise_test(async () => {
+ await fetchORB(`${path}/script-utf16-without-bom.js`, null, contentType("application/json; charset=utf-16"));
+}, "ORB shouldn't block opaque text/javascript (utf16 encoded without BOM but charset is provided in content-type)");
+
+// Test javascript validation can correctly decode the content for iso-8559-1 (fallback decoder in Firefox).
+promise_test(async () => {
+ await fetchORB(`${path}/script-iso-8559-1.js`, null, contentType("application/json"));
+}, "ORB shouldn't block opaque text/javascript (iso-8559-1 encoded)");
+
+// Test javascript validation can correctly parse asm.js.
+promise_test(async () => {
+ await fetchORB(`${path}/script-asm-js-valid.js`, null, contentType("application/json"));
+}, "ORB shouldn't block text/javascript with valid asm.js");
+
+// Test javascript validation can correctly parse invalid asm.js with valid JS syntax.
+promise_test(async () => {
+ await fetchORB(`${path}/script-asm-js-invalid.js`, null, contentType("application/json"));
+}, "ORB shouldn't block text/javascript with invalid asm.js");
diff --git a/test/wpt/tests/fetch/orb/tentative/nosniff.sub.any.js b/test/wpt/tests/fetch/orb/tentative/nosniff.sub.any.js
new file mode 100644
index 0000000..3df9d22
--- /dev/null
+++ b/test/wpt/tests/fetch/orb/tentative/nosniff.sub.any.js
@@ -0,0 +1,59 @@
+// META: script=/fetch/orb/resources/utils.js
+
+const path = "http://{{domains[www1]}}:{{ports[http][0]}}/fetch/orb/resources";
+
+promise_test(
+ t =>
+ promise_rejects_js(
+ t,
+ TypeError,
+ fetchORB(
+ `${path}/text.txt`,
+ null,
+ contentType("text/plain"),
+ contentTypeOptions("nosniff")
+ )
+ ),
+ "ORB should block opaque text/plain with nosniff"
+);
+
+promise_test(
+ t =>
+ promise_rejects_js(
+ t,
+ TypeError,
+ fetchORB(
+ `${path}/data.json`,
+ null,
+ contentType("application/json"),
+ contentTypeOptions("nosniff")
+ )
+ ),
+ "ORB should block opaque-response-blocklisted MIME type with nosniff"
+);
+
+promise_test(
+ t =>
+ promise_rejects_js(
+ t,
+ TypeError,
+ fetchORB(
+ `${path}/data.json`,
+ null,
+ contentType(""),
+ contentTypeOptions("nosniff")
+ )
+ ),
+ "ORB should block opaque response with empty Content-Type and nosniff"
+);
+
+promise_test(
+ () =>
+ fetchORB(
+ `${path}/image.png`,
+ null,
+ contentType(""),
+ contentTypeOptions("nosniff")
+ ),
+ "ORB shouldn't block opaque image with empty Content-Type and nosniff"
+);
diff --git a/test/wpt/tests/fetch/orb/tentative/script-js-unlabeled-gziped.sub.html b/test/wpt/tests/fetch/orb/tentative/script-js-unlabeled-gziped.sub.html
new file mode 100644
index 0000000..fe85440
--- /dev/null
+++ b/test/wpt/tests/fetch/orb/tentative/script-js-unlabeled-gziped.sub.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<!-- Test verifies that gziped script which parses as Javascript (not JSON) without Content-Type will execute with ORB. -->
+<meta charset="utf-8">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id=log></div>
+
+<script>
+setup({ single_test: true });
+window.has_executed_script = false;
+</script>
+
+<!-- www1 is cross-origin, so the HTTP response is CORB-eligible -->
+<script src="http://{{domains[www1]}}:{{ports[http][0]}}/fetch/orb/resources/js-unlabeled.js?pipe=gzip|header(Content-Type,)">
+</script>
+
+<script>
+// Verify what observable effects the <script> tag above had.
+// Assertion should hold with and without ORB:
+assert_true(window.has_executed_script,
+ 'The cross-origin script should execute');
+done();
+</script>
+
diff --git a/test/wpt/tests/fetch/orb/tentative/script-unlabeled.sub.html b/test/wpt/tests/fetch/orb/tentative/script-unlabeled.sub.html
new file mode 100644
index 0000000..4987f13
--- /dev/null
+++ b/test/wpt/tests/fetch/orb/tentative/script-unlabeled.sub.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<!-- Test verifies that script which parses as Javascript (not JSON) without Content-Type will execute with ORB. -->
+<meta charset="utf-8">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id=log></div>
+
+<script>
+setup({ single_test: true });
+window.has_executed_script = false;
+</script>
+
+<!-- www1 is cross-origin, so the HTTP response is CORB-eligible -->
+<script src="http://{{domains[www1]}}:{{ports[http][0]}}/fetch/orb/resources/js-unlabeled.js">
+</script>
+
+<script>
+// Verify what observable effects the <script> tag above had.
+// Assertion should hold with and without ORB:
+assert_true(window.has_executed_script,
+ 'The cross-origin script should execute');
+done();
+</script>
+
diff --git a/test/wpt/tests/fetch/orb/tentative/script-utf16-without-bom-hint-charset.sub.html b/test/wpt/tests/fetch/orb/tentative/script-utf16-without-bom-hint-charset.sub.html
new file mode 100644
index 0000000..b15f976
--- /dev/null
+++ b/test/wpt/tests/fetch/orb/tentative/script-utf16-without-bom-hint-charset.sub.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<!-- Test verifies that utf-16 encoded script (without BOM) which parses as Javascript (not JSON) will execute with ORB. -->
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id=log></div>
+
+<script>
+setup({ single_test: true });
+window.has_executed_script = false;
+</script>
+
+<!-- www1 is cross-origin, so the HTTP response is CORB-eligible -->
+<script charset="utf-16" src="http://{{domains[www1]}}:{{ports[http][0]}}/fetch/orb/resources/js-unlabeled-utf16-without-bom.json">
+</script>
+
+<script>
+// Verify what observable effects the <script> tag above had.
+// Assertion should hold with and without ORB:
+assert_true(window.has_executed_script,
+ 'The cross-origin script should execute');
+done();
+</script>
diff --git a/test/wpt/tests/fetch/orb/tentative/status.sub.any.js b/test/wpt/tests/fetch/orb/tentative/status.sub.any.js
new file mode 100644
index 0000000..b94d8b7
--- /dev/null
+++ b/test/wpt/tests/fetch/orb/tentative/status.sub.any.js
@@ -0,0 +1,33 @@
+// META: script=/fetch/orb/resources/utils.js
+
+const path = "http://{{domains[www1]}}:{{ports[http][0]}}/fetch/orb/resources";
+
+promise_test(
+ t =>
+ promise_rejects_js(
+ t,
+ TypeError,
+ fetchORB(
+ `${path}/data.json`,
+ null,
+ contentType("application/json"),
+ "status(206)"
+ )
+ ),
+ "ORB should block opaque-response-blocklisted MIME type with status 206"
+);
+
+promise_test(
+ t =>
+ promise_rejects_js(
+ t,
+ TypeError,
+ fetchORB(
+ `${path}/data.json`,
+ null,
+ contentType("application/json"),
+ "status(302)"
+ )
+ ),
+ "ORB should block opaque response with non-ok status"
+);
diff --git a/test/wpt/tests/fetch/orb/tentative/status.sub.html b/test/wpt/tests/fetch/orb/tentative/status.sub.html
new file mode 100644
index 0000000..a62bdeb
--- /dev/null
+++ b/test/wpt/tests/fetch/orb/tentative/status.sub.html
@@ -0,0 +1,17 @@
+'use strict';
+
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<div id=log></div>
+<script>
+async_test(function(t) {
+ let url = "http://{{domains[www1]}}:{{ports[http][0]}}"
+ url = `${url}/fetch/orb/resources/sound.mp3?pipe=status(301)|header(Content-Type,)`
+
+ const video = document.createElement("video");
+ video.src = url;
+ video.onerror = t.step_func_done();
+ video.onload = t.unreached_func("Unexpected error event");
+ document.body.appendChild(video);
+}, "ORB should block initial media requests with status not 200 or 206");
+</script>
diff --git a/test/wpt/tests/fetch/orb/tentative/unknown-mime-type.sub.any.js b/test/wpt/tests/fetch/orb/tentative/unknown-mime-type.sub.any.js
new file mode 100644
index 0000000..f72ff92
--- /dev/null
+++ b/test/wpt/tests/fetch/orb/tentative/unknown-mime-type.sub.any.js
@@ -0,0 +1,28 @@
+// META: script=/fetch/orb/resources/utils.js
+
+const path = "http://{{domains[www1]}}:{{ports[http][0]}}/fetch/orb/resources";
+
+promise_test(
+ () => fetchORB(`${path}/font.ttf`, null, contentType("")),
+ "ORB shouldn't block opaque failed missing MIME type (font/ttf)"
+);
+
+promise_test(
+ () => fetchORB(`${path}/text.txt`, null, contentType("")),
+ "ORB shouldn't block opaque failed missing MIME type (text/plain)"
+);
+
+promise_test(
+ t => fetchORB(`${path}/data.json`, null, contentType("")),
+ "ORB shouldn't block opaque failed missing MIME type (application/json)"
+);
+
+promise_test(
+ () => fetchORB(`${path}/image.png`, null, contentType("")),
+ "ORB shouldn't block opaque failed missing MIME type (image/png)"
+);
+
+promise_test(
+ () => fetchORB(`${path}/script.js`, null, contentType("")),
+ "ORB shouldn't block opaque failed missing MIME type (text/javascript)"
+);
diff --git a/test/wpt/tests/fetch/origin/assorted.window.js b/test/wpt/tests/fetch/origin/assorted.window.js
new file mode 100644
index 0000000..033d010
--- /dev/null
+++ b/test/wpt/tests/fetch/origin/assorted.window.js
@@ -0,0 +1,211 @@
+// META: script=/common/utils.js
+// META: script=/common/get-host-info.sub.js
+
+const origins = get_host_info();
+
+promise_test(async function () {
+ const stash = token(),
+ redirectPath = "/fetch/origin/resources/redirect-and-stash.py";
+
+ // Cross-origin -> same-origin will result in setting the tainted origin flag for the second
+ // request.
+ let url = origins.HTTP_ORIGIN + redirectPath + "?stash=" + stash;
+ url = origins.HTTP_REMOTE_ORIGIN + redirectPath + "?stash=" + stash + "&location=" + encodeURIComponent(url) + "&dummyJS";
+
+ await fetch(url, { mode: "no-cors", method: "POST" });
+
+ const json = await (await fetch(redirectPath + "?dump&stash=" + stash)).json();
+
+ assert_equals(json[0], origins.HTTP_ORIGIN);
+ assert_equals(json[1], "null");
+}, "Origin header and 308 redirect");
+
+promise_test(async function () {
+ const stash = token(),
+ redirectPath = "/fetch/origin/resources/redirect-and-stash.py";
+
+ let url = origins.HTTP_ORIGIN + redirectPath + "?stash=" + stash;
+ url = origins.HTTP_REMOTE_ORIGIN + redirectPath + "?stash=" + stash + "&location=" + encodeURIComponent(url);
+
+ await new Promise(resolve => {
+ const frame = document.createElement("iframe");
+ frame.src = url;
+ frame.onload = () => {
+ resolve();
+ frame.remove();
+ }
+ document.body.appendChild(frame);
+ });
+
+ const json = await (await fetch(redirectPath + "?dump&stash=" + stash)).json();
+
+ assert_equals(json[0], "no Origin header");
+ assert_equals(json[1], "no Origin header");
+}, "Origin header and GET navigation");
+
+promise_test(async function () {
+ const stash = token(),
+ redirectPath = "/fetch/origin/resources/redirect-and-stash.py";
+
+ let url = origins.HTTP_ORIGIN + redirectPath + "?stash=" + stash;
+ url = origins.HTTP_REMOTE_ORIGIN + redirectPath + "?stash=" + stash + "&location=" + encodeURIComponent(url);
+
+ await new Promise(resolve => {
+ const frame = document.createElement("iframe");
+ self.addEventListener("message", e => {
+ if (e.data === "loaded") {
+ resolve();
+ frame.remove();
+ }
+ }, { once: true });
+ frame.onload = () => {
+ const doc = frame.contentDocument,
+ form = doc.body.appendChild(doc.createElement("form")),
+ submit = form.appendChild(doc.createElement("input"));
+ form.action = url;
+ form.method = "POST";
+ submit.type = "submit";
+ submit.click();
+ }
+ document.body.appendChild(frame);
+ });
+
+ const json = await (await fetch(redirectPath + "?dump&stash=" + stash)).json();
+
+ assert_equals(json[0], origins.HTTP_ORIGIN);
+ assert_equals(json[1], "null");
+}, "Origin header and POST navigation");
+
+function navigationReferrerPolicy(referrerPolicy, destination, expectedOrigin) {
+ return async function () {
+ const stash = token();
+ const referrerPolicyPath = "/fetch/origin/resources/referrer-policy.py";
+ const redirectPath = "/fetch/origin/resources/redirect-and-stash.py";
+
+ let postUrl =
+ (destination === "same-origin" ? origins.HTTP_ORIGIN
+ : origins.HTTP_REMOTE_ORIGIN) +
+ redirectPath + "?stash=" + stash;
+
+ await new Promise(resolve => {
+ const frame = document.createElement("iframe");
+ document.body.appendChild(frame);
+ frame.src = origins.HTTP_ORIGIN + referrerPolicyPath +
+ "?referrerPolicy=" + referrerPolicy;
+ self.addEventListener("message", function listener(e) {
+ if (e.data === "loaded") {
+ resolve();
+ frame.remove();
+ self.removeEventListener("message", listener);
+ } else if (e.data === "action") {
+ const doc = frame.contentDocument,
+ form = doc.body.appendChild(doc.createElement("form")),
+ submit = form.appendChild(doc.createElement("input"));
+ form.action = postUrl;
+ form.method = "POST";
+ submit.type = "submit";
+ submit.click();
+ }
+ });
+ });
+
+ const json = await (await fetch(redirectPath + "?dump&stash=" + stash)).json();
+
+ assert_equals(json[0], expectedOrigin);
+ };
+}
+
+function fetchReferrerPolicy(referrerPolicy, destination, fetchMode, expectedOrigin, httpMethod) {
+ return async function () {
+ const stash = token();
+ const redirectPath = "/fetch/origin/resources/redirect-and-stash.py";
+
+ let fetchUrl =
+ (destination === "same-origin" ? origins.HTTP_ORIGIN
+ : origins.HTTP_REMOTE_ORIGIN) +
+ redirectPath + "?stash=" + stash + "&dummyJS";
+
+ await fetch(fetchUrl, { mode: fetchMode, method: httpMethod , "referrerPolicy": referrerPolicy});
+
+ const json = await (await fetch(redirectPath + "?dump&stash=" + stash)).json();
+
+ assert_equals(json[0], expectedOrigin);
+ };
+}
+
+function referrerPolicyTestString(referrerPolicy, method, destination) {
+ return "Origin header and " + method + " " + destination + " with Referrer-Policy " +
+ referrerPolicy;
+}
+
+[
+ {
+ "policy": "no-referrer",
+ "expectedOriginForSameOrigin": "null",
+ "expectedOriginForCrossOrigin": "null"
+ },
+ {
+ "policy": "same-origin",
+ "expectedOriginForSameOrigin": origins.HTTP_ORIGIN,
+ "expectedOriginForCrossOrigin": "null"
+ },
+ {
+ "policy": "origin-when-cross-origin",
+ "expectedOriginForSameOrigin": origins.HTTP_ORIGIN,
+ "expectedOriginForCrossOrigin": origins.HTTP_ORIGIN
+ },
+ {
+ "policy": "no-referrer-when-downgrade",
+ "expectedOriginForSameOrigin": origins.HTTP_ORIGIN,
+ "expectedOriginForCrossOrigin": origins.HTTP_ORIGIN
+ },
+ {
+ "policy": "unsafe-url",
+ "expectedOriginForSameOrigin": origins.HTTP_ORIGIN,
+ "expectedOriginForCrossOrigin": origins.HTTP_ORIGIN
+ },
+].forEach(testObj => {
+ [
+ {
+ "name": "same-origin",
+ "expectedOrigin": testObj.expectedOriginForSameOrigin
+ },
+ {
+ "name": "cross-origin",
+ "expectedOrigin": testObj.expectedOriginForCrossOrigin
+ }
+ ].forEach(destination => {
+ // Test form POST navigation
+ promise_test(navigationReferrerPolicy(testObj.policy,
+ destination.name,
+ destination.expectedOrigin),
+ referrerPolicyTestString(testObj.policy, "POST",
+ destination.name + " navigation"));
+ // Test fetch
+ promise_test(fetchReferrerPolicy(testObj.policy,
+ destination.name,
+ "no-cors",
+ destination.expectedOrigin,
+ "POST"),
+ referrerPolicyTestString(testObj.policy, "POST",
+ destination.name + " fetch no-cors mode"));
+
+ // Test cors mode POST
+ promise_test(fetchReferrerPolicy(testObj.policy,
+ destination.name,
+ "cors",
+ origins.HTTP_ORIGIN,
+ "POST"),
+ referrerPolicyTestString(testObj.policy, "POST",
+ destination.name + " fetch cors mode"));
+
+ // Test cors mode GET
+ promise_test(fetchReferrerPolicy(testObj.policy,
+ destination.name,
+ "cors",
+ (destination.name == "same-origin") ? "no Origin header" : origins.HTTP_ORIGIN,
+ "GET"),
+ referrerPolicyTestString(testObj.policy, "GET",
+ destination.name + " fetch cors mode"));
+ });
+});
diff --git a/test/wpt/tests/fetch/origin/resources/redirect-and-stash.py b/test/wpt/tests/fetch/origin/resources/redirect-and-stash.py
new file mode 100644
index 0000000..36c584c
--- /dev/null
+++ b/test/wpt/tests/fetch/origin/resources/redirect-and-stash.py
@@ -0,0 +1,38 @@
+import json
+
+from wptserve.utils import isomorphic_decode
+
+def main(request, response):
+ key = request.GET.first(b"stash")
+ origin = request.headers.get(b"origin")
+ if origin is None:
+ origin = b"no Origin header"
+
+ origin_list = request.server.stash.take(key)
+
+ if b"dump" in request.GET:
+ response.headers.set(b"Content-Type", b"application/json")
+ response.content = json.dumps(origin_list)
+ return
+
+ if origin_list is None:
+ origin_list = [isomorphic_decode(origin)]
+ else:
+ origin_list.append(isomorphic_decode(origin))
+
+ request.server.stash.put(key, origin_list)
+
+ if b"location" in request.GET:
+ location = request.GET.first(b"location")
+ if b"dummyJS" in request.GET:
+ location += b"&dummyJS"
+ response.status = 308
+ response.headers.set(b"Location", location)
+ return
+
+ response.headers.set(b"Content-Type", b"text/html")
+ response.headers.set(b"Access-Control-Allow-Origin", b"*")
+ if b"dummyJS" in request.GET:
+ response.content = b"console.log('dummy JS')"
+ else:
+ response.content = b"<meta charset=utf-8>\n<body><script>parent.postMessage('loaded','*')</script></body>"
diff --git a/test/wpt/tests/fetch/origin/resources/referrer-policy.py b/test/wpt/tests/fetch/origin/resources/referrer-policy.py
new file mode 100644
index 0000000..15716e0
--- /dev/null
+++ b/test/wpt/tests/fetch/origin/resources/referrer-policy.py
@@ -0,0 +1,7 @@
+def main(request, response):
+ if b"referrerPolicy" in request.GET:
+ response.headers.set(b"Referrer-Policy",
+ request.GET.first(b"referrerPolicy"))
+ response.status = 200
+ response.headers.set(b"Content-Type", b"text/html")
+ response.content = b"<meta charset=utf-8>\n<body><script>parent.postMessage('action','*')</script></body>"
diff --git a/test/wpt/tests/fetch/private-network-access/META.yml b/test/wpt/tests/fetch/private-network-access/META.yml
new file mode 100644
index 0000000..944ce6f
--- /dev/null
+++ b/test/wpt/tests/fetch/private-network-access/META.yml
@@ -0,0 +1,7 @@
+spec: https://wicg.github.io/private-network-access/
+suggested_reviewers:
+ - letitz
+ - lyf
+ - hemeryar
+ - camillelamy
+ - mikewest
diff --git a/test/wpt/tests/fetch/private-network-access/README.md b/test/wpt/tests/fetch/private-network-access/README.md
new file mode 100644
index 0000000..a69aab4
--- /dev/null
+++ b/test/wpt/tests/fetch/private-network-access/README.md
@@ -0,0 +1,10 @@
+# Private Network Access tests
+
+This directory contains tests for Private Network Access' integration with
+the Fetch specification.
+
+See also:
+
+* [The specification](https://wicg.github.io/private-network-access/)
+* [The repository](https://github.com/WICG/private-network-access/)
+* [Open issues](https://github.com/WICG/private-network-access/issues/)
diff --git a/test/wpt/tests/fetch/private-network-access/fenced-frame-no-preflight-required.tentative.https.window.js b/test/wpt/tests/fetch/private-network-access/fenced-frame-no-preflight-required.tentative.https.window.js
new file mode 100644
index 0000000..21233f6
--- /dev/null
+++ b/test/wpt/tests/fetch/private-network-access/fenced-frame-no-preflight-required.tentative.https.window.js
@@ -0,0 +1,91 @@
+// META: script=/common/dispatcher/dispatcher.js
+// META: script=/common/utils.js
+// META: script=resources/support.sub.js
+// META: script=/fenced-frame/resources/utils.js
+// META: timeout=long
+//
+// Spec: https://wicg.github.io/private-network-access/#integration-fetch
+//
+// These tests verify that contexts can navigate fenced frames to more-public or
+// same address spaces without private network access preflight request header.
+
+setup(() => {
+ assert_true(window.isSecureContext);
+});
+
+// Source: secure local context.
+//
+// All fetches unaffected by Private Network Access.
+
+promise_test_parallel(
+ t => fencedFrameTest(t, {
+ source: {server: Server.HTTPS_LOCAL},
+ target: {server: Server.HTTPS_LOCAL},
+ expected: FrameTestResult.SUCCESS,
+ }),
+ 'local to local: no preflight required.');
+
+promise_test_parallel(
+ t => fencedFrameTest(t, {
+ source: {server: Server.HTTPS_LOCAL},
+ target: {server: Server.HTTPS_PRIVATE},
+ expected: FrameTestResult.SUCCESS,
+ }),
+ 'local to private: no preflight required.');
+
+promise_test_parallel(
+ t => fencedFrameTest(t, {
+ source: {server: Server.HTTPS_LOCAL},
+ target: {server: Server.HTTPS_PUBLIC},
+ expected: FrameTestResult.SUCCESS,
+ }),
+ 'local to public: no preflight required.');
+
+promise_test_parallel(
+ t => fencedFrameTest(t, {
+ source: {server: Server.HTTPS_PRIVATE},
+ target: {server: Server.HTTPS_PRIVATE},
+ expected: FrameTestResult.SUCCESS,
+ }),
+ 'private to private: no preflight required.');
+
+promise_test_parallel(
+ t => fencedFrameTest(t, {
+ source: {server: Server.HTTPS_PRIVATE},
+ target: {server: Server.HTTPS_PUBLIC},
+ expected: FrameTestResult.SUCCESS,
+ }),
+ 'private to public: no preflight required.');
+
+promise_test_parallel(
+ t => fencedFrameTest(t, {
+ source: {server: Server.HTTPS_PUBLIC},
+ target: {server: Server.HTTPS_PUBLIC},
+ expected: FrameTestResult.SUCCESS,
+ }),
+ 'public to public: no preflight required.');
+
+promise_test_parallel(
+ t => fencedFrameTest(t, {
+ source: {
+ server: Server.HTTPS_LOCAL,
+ treatAsPublic: true,
+ },
+ target: {server: Server.HTTPS_PUBLIC},
+ expected: FrameTestResult.SUCCESS,
+ }),
+ 'treat-as-public-address to public: no preflight required.');
+
+promise_test_parallel(
+ t => fencedFrameTest(t, {
+ source: {
+ server: Server.HTTPS_LOCAL,
+ treatAsPublic: true,
+ },
+ target: {
+ server: Server.HTTPS_PUBLIC,
+ behavior: {preflight: PreflightBehavior.optionalSuccess(token())}
+ },
+ expected: FrameTestResult.SUCCESS,
+ }),
+ 'treat-as-public-address to local: optional preflight');
diff --git a/test/wpt/tests/fetch/private-network-access/fenced-frame-subresource-fetch.tentative.https.window.js b/test/wpt/tests/fetch/private-network-access/fenced-frame-subresource-fetch.tentative.https.window.js
new file mode 100644
index 0000000..2dff325
--- /dev/null
+++ b/test/wpt/tests/fetch/private-network-access/fenced-frame-subresource-fetch.tentative.https.window.js
@@ -0,0 +1,330 @@
+// META: script=/common/subset-tests-by-key.js
+// META: script=/common/utils.js
+// META: script=resources/support.sub.js
+// META: script=/fenced-frame/resources/utils.js
+// META: variant=?include=baseline
+// META: variant=?include=from-local
+// META: variant=?include=from-private
+// META: variant=?include=from-public
+// META: timeout=long
+//
+// Spec: https://wicg.github.io/private-network-access/#integration-fetch
+//
+// These tests verify that secure contexts can fetch subresources in fenced
+// frames from all address spaces, provided that the target server, if more
+// private than the initiator, respond affirmatively to preflight requests.
+//
+
+setup(() => {
+ // Making sure we are in a secure context, as expected.
+ assert_true(window.isSecureContext);
+});
+
+// Source: secure local context.
+//
+// All fetches unaffected by Private Network Access.
+
+subsetTestByKey(
+ 'from-local', promise_test, t => fencedFrameFetchTest(t, {
+ source: {server: Server.HTTPS_LOCAL},
+ target: {server: Server.HTTPS_LOCAL},
+ fetchOptions: {method: 'GET', mode: 'cors'},
+ expected: FetchTestResult.SUCCESS,
+ }),
+ 'local to local: no preflight required.');
+
+subsetTestByKey(
+ 'from-local', promise_test,
+ t => fencedFrameFetchTest(t, {
+ source: {server: Server.HTTPS_LOCAL},
+ target: {
+ server: Server.HTTPS_PRIVATE,
+ behavior: {response: ResponseBehavior.allowCrossOrigin()},
+ },
+ fetchOptions: {method: 'GET', mode: 'cors'},
+ expected: FetchTestResult.SUCCESS,
+ }),
+ 'local to private: no preflight required.');
+
+
+subsetTestByKey(
+ 'from-local', promise_test,
+ t => fencedFrameFetchTest(t, {
+ source: {server: Server.HTTPS_LOCAL},
+ target: {
+ server: Server.HTTPS_PUBLIC,
+ behavior: {response: ResponseBehavior.allowCrossOrigin()},
+ },
+ fetchOptions: {method: 'GET', mode: 'cors'},
+ expected: FetchTestResult.SUCCESS,
+ }),
+ 'local to public: no preflight required.');
+
+// Strictly speaking, the following two tests do not exercise PNA-specific
+// logic, but they serve as a baseline for comparison, ensuring that non-PNA
+// preflight requests are sent and handled as expected.
+
+subsetTestByKey(
+ 'baseline', promise_test,
+ t => fencedFrameFetchTest(t, {
+ source: {server: Server.HTTPS_LOCAL},
+ target: {
+ server: Server.HTTPS_PUBLIC,
+ behavior: {
+ preflight: PreflightBehavior.failure(),
+ response: ResponseBehavior.allowCrossOrigin(),
+ },
+ },
+ fetchOptions: {method: 'PUT', mode: 'cors'},
+ expected: FetchTestResult.FAILURE,
+ }),
+ 'local to public: PUT preflight failure.');
+
+subsetTestByKey(
+ 'baseline', promise_test,
+ t => fencedFrameFetchTest(t, {
+ source: {server: Server.HTTPS_LOCAL},
+ target: {
+ server: Server.HTTPS_PUBLIC,
+ behavior: {
+ preflight: PreflightBehavior.success(token()),
+ response: ResponseBehavior.allowCrossOrigin(),
+ }
+ },
+ fetchOptions: {method: 'PUT', mode: 'cors'},
+ expected: FetchTestResult.SUCCESS,
+ }),
+ 'local to public: PUT preflight success.');
+
+// Generates tests of preflight behavior for a single (source, target) pair.
+//
+// Scenarios:
+//
+// - cors mode:
+// - preflight response has non-2xx HTTP code
+// - preflight response is missing CORS headers
+// - preflight response is missing the PNA-specific `Access-Control` header
+// - final response is missing CORS headers
+// - success
+// - success with PUT method (non-"simple" request)
+// - no-cors mode:
+// - preflight response has non-2xx HTTP code
+// - preflight response is missing CORS headers
+// - preflight response is missing the PNA-specific `Access-Control` header
+// - success
+//
+function makePreflightTests({
+ subsetKey,
+ source,
+ sourceDescription,
+ targetServer,
+ targetDescription,
+}) {
+ const prefix = `${sourceDescription} to ${targetDescription}: `;
+
+ subsetTestByKey(
+ subsetKey, promise_test,
+ t => fencedFrameFetchTest(t, {
+ source,
+ target: {
+ server: targetServer,
+ behavior: {
+ preflight: PreflightBehavior.failure(),
+ response: ResponseBehavior.allowCrossOrigin(),
+ },
+ },
+ fetchOptions: {method: 'GET', mode: 'cors'},
+ expected: FetchTestResult.FAILURE,
+ }),
+ prefix + 'failed preflight.');
+
+ subsetTestByKey(
+ subsetKey, promise_test,
+ t => fencedFrameFetchTest(t, {
+ source,
+ target: {
+ server: targetServer,
+ behavior: {
+ preflight: PreflightBehavior.noCorsHeader(token()),
+ response: ResponseBehavior.allowCrossOrigin(),
+ },
+ },
+ fetchOptions: {method: 'GET', mode: 'cors'},
+ expected: FetchTestResult.FAILURE,
+ }),
+ prefix + 'missing CORS headers on preflight response.');
+
+ subsetTestByKey(
+ subsetKey, promise_test,
+ t => fencedFrameFetchTest(t, {
+ source,
+ target: {
+ server: targetServer,
+ behavior: {
+ preflight: PreflightBehavior.noPnaHeader(token()),
+ response: ResponseBehavior.allowCrossOrigin(),
+ },
+ },
+ fetchOptions: {method: 'GET', mode: 'cors'},
+ expected: FetchTestResult.FAILURE,
+ }),
+ prefix + 'missing PNA header on preflight response.');
+
+ subsetTestByKey(
+ subsetKey, promise_test,
+ t => fencedFrameFetchTest(t, {
+ source,
+ target: {
+ server: targetServer,
+ behavior: {preflight: PreflightBehavior.success(token())},
+ },
+ fetchOptions: {method: 'GET', mode: 'cors'},
+ expected: FetchTestResult.FAILURE,
+ }),
+ prefix + 'missing CORS headers on final response.');
+
+ subsetTestByKey(
+ subsetKey, promise_test,
+ t => fencedFrameFetchTest(t, {
+ source,
+ target: {
+ server: targetServer,
+ behavior: {
+ preflight: PreflightBehavior.success(token()),
+ response: ResponseBehavior.allowCrossOrigin(),
+ },
+ },
+ fetchOptions: {method: 'GET', mode: 'cors'},
+ expected: FetchTestResult.SUCCESS,
+ }),
+ prefix + 'success.');
+
+ subsetTestByKey(
+ subsetKey, promise_test,
+ t => fencedFrameFetchTest(t, {
+ source,
+ target: {
+ server: targetServer,
+ behavior: {
+ preflight: PreflightBehavior.success(token()),
+ response: ResponseBehavior.allowCrossOrigin(),
+ },
+ },
+ fetchOptions: {method: 'PUT', mode: 'cors'},
+ expected: FetchTestResult.SUCCESS,
+ }),
+ prefix + 'PUT success.');
+
+ subsetTestByKey(
+ subsetKey, promise_test, t => fencedFrameFetchTest(t, {
+ source,
+ target: {server: targetServer},
+ fetchOptions: {method: 'GET', mode: 'no-cors'},
+ expected: FetchTestResult.FAILURE,
+ }),
+ prefix + 'no-CORS mode failed preflight.');
+
+ subsetTestByKey(
+ subsetKey, promise_test,
+ t => fencedFrameFetchTest(t, {
+ source,
+ target: {
+ server: targetServer,
+ behavior: {preflight: PreflightBehavior.noCorsHeader(token())},
+ },
+ fetchOptions: {method: 'GET', mode: 'no-cors'},
+ expected: FetchTestResult.FAILURE,
+ }),
+ prefix + 'no-CORS mode missing CORS headers on preflight response.');
+
+ subsetTestByKey(
+ subsetKey, promise_test,
+ t => fencedFrameFetchTest(t, {
+ source,
+ target: {
+ server: targetServer,
+ behavior: {preflight: PreflightBehavior.noPnaHeader(token())},
+ },
+ fetchOptions: {method: 'GET', mode: 'no-cors'},
+ expected: FetchTestResult.FAILURE,
+ }),
+ prefix + 'no-CORS mode missing PNA header on preflight response.');
+
+ subsetTestByKey(
+ subsetKey, promise_test,
+ t => fencedFrameFetchTest(t, {
+ source,
+ target: {
+ server: targetServer,
+ behavior: {preflight: PreflightBehavior.success(token())},
+ },
+ fetchOptions: {method: 'GET', mode: 'no-cors'},
+ expected: FetchTestResult.OPAQUE,
+ }),
+ prefix + 'no-CORS mode success.');
+}
+
+// Source: private secure context.
+//
+// Fetches to the local address space require a successful preflight response
+// carrying a PNA-specific header.
+
+makePreflightTests({
+ subsetKey: 'from-private',
+ source: {server: Server.HTTPS_PRIVATE},
+ sourceDescription: 'private',
+ targetServer: Server.HTTPS_LOCAL,
+ targetDescription: 'local',
+});
+
+subsetTestByKey(
+ 'from-private', promise_test, t => fencedFrameFetchTest(t, {
+ source: {server: Server.HTTPS_PRIVATE},
+ target: {server: Server.HTTPS_PRIVATE},
+ fetchOptions: {method: 'GET', mode: 'cors'},
+ expected: FetchTestResult.SUCCESS,
+ }),
+ 'private to private: no preflight required.');
+
+subsetTestByKey(
+ 'from-private', promise_test,
+ t => fencedFrameFetchTest(t, {
+ source: {server: Server.HTTPS_PRIVATE},
+ target: {
+ server: Server.HTTPS_PRIVATE,
+ behavior: {response: ResponseBehavior.allowCrossOrigin()},
+ },
+ fetchOptions: {method: 'GET', mode: 'cors'},
+ expected: FetchTestResult.SUCCESS,
+ }),
+ 'private to public: no preflight required.');
+
+// Source: public secure context.
+//
+// Fetches to the local and private address spaces require a successful
+// preflight response carrying a PNA-specific header.
+
+makePreflightTests({
+ subsetKey: 'from-public',
+ source: {server: Server.HTTPS_PUBLIC},
+ sourceDescription: 'public',
+ targetServer: Server.HTTPS_LOCAL,
+ targetDescription: 'local',
+});
+
+makePreflightTests({
+ subsetKey: 'from-public',
+ source: {server: Server.HTTPS_PUBLIC},
+ sourceDescription: 'public',
+ targetServer: Server.HTTPS_PRIVATE,
+ targetDescription: 'private',
+});
+
+subsetTestByKey(
+ 'from-public', promise_test, t => fencedFrameFetchTest(t, {
+ source: {server: Server.HTTPS_PUBLIC},
+ target: {server: Server.HTTPS_PUBLIC},
+ fetchOptions: {method: 'GET', mode: 'cors'},
+ expected: FetchTestResult.SUCCESS,
+ }),
+ 'public to public: no preflight required.');
diff --git a/test/wpt/tests/fetch/private-network-access/fenced-frame.tentative.https.window.js b/test/wpt/tests/fetch/private-network-access/fenced-frame.tentative.https.window.js
new file mode 100644
index 0000000..370cc9f
--- /dev/null
+++ b/test/wpt/tests/fetch/private-network-access/fenced-frame.tentative.https.window.js
@@ -0,0 +1,150 @@
+// META: script=/common/dispatcher/dispatcher.js
+// META: script=/common/utils.js
+// META: script=resources/support.sub.js
+// META: script=/fenced-frame/resources/utils.js
+// META: timeout=long
+//
+// Spec: https://wicg.github.io/private-network-access/#integration-fetch
+//
+// These tests verify that contexts can navigate fenced frames to less-public
+// address spaces iff the target server responds affirmatively to preflight
+// requests.
+
+setup(() => {
+ assert_true(window.isSecureContext);
+});
+
+// Generates tests of preflight behavior for a single (source, target) pair.
+//
+// Scenarios:
+//
+// - parent navigates child:
+// - preflight response has non-2xx HTTP code
+// - preflight response is missing CORS headers
+// - preflight response is missing the PNA-specific `Access-Control` header
+// - preflight response has the required PNA related headers, but still fails
+// because of the limitation of fenced frame that subjects to PNA checks.
+//
+function makePreflightTests({
+ sourceName,
+ sourceServer,
+ sourceTreatAsPublic,
+ targetName,
+ targetServer,
+}) {
+ const prefix = `${sourceName} to ${targetName}: `;
+
+ const source = {
+ server: sourceServer,
+ treatAsPublic: sourceTreatAsPublic,
+ };
+
+ promise_test_parallel(
+ t => fencedFrameTest(t, {
+ source,
+ target: {
+ server: targetServer,
+ behavior: {preflight: PreflightBehavior.failure()},
+ },
+ expected: FrameTestResult.FAILURE,
+ }),
+ prefix + 'failed preflight.');
+
+ promise_test_parallel(
+ t => fencedFrameTest(t, {
+ source,
+ target: {
+ server: targetServer,
+ behavior: {preflight: PreflightBehavior.noCorsHeader(token())},
+ },
+ expected: FrameTestResult.FAILURE,
+ }),
+ prefix + 'missing CORS headers.');
+
+ promise_test_parallel(
+ t => fencedFrameTest(t, {
+ source,
+ target: {
+ server: targetServer,
+ behavior: {preflight: PreflightBehavior.noPnaHeader(token())},
+ },
+ expected: FrameTestResult.FAILURE,
+ }),
+ prefix + 'missing PNA header.');
+
+ promise_test_parallel(
+ t => fencedFrameTest(t, {
+ source,
+ target: {
+ server: targetServer,
+ behavior: {
+ preflight: PreflightBehavior.success(token()),
+ response: ResponseBehavior.allowCrossOrigin()
+ },
+ },
+ expected: FrameTestResult.FAILURE,
+ }),
+ prefix + 'failed because fenced frames are incompatible with PNA.');
+}
+
+// Source: private secure context.
+//
+// Fetches to the local address space require a successful preflight response
+// carrying a PNA-specific header.
+
+makePreflightTests({
+ sourceServer: Server.HTTPS_PRIVATE,
+ sourceName: 'private',
+ targetServer: Server.HTTPS_LOCAL,
+ targetName: 'local',
+});
+
+// Source: public secure context.
+//
+// Fetches to the local and private address spaces require a successful
+// preflight response carrying a PNA-specific header.
+
+makePreflightTests({
+ sourceServer: Server.HTTPS_PUBLIC,
+ sourceName: 'public',
+ targetServer: Server.HTTPS_LOCAL,
+ targetName: 'local',
+});
+
+makePreflightTests({
+ sourceServer: Server.HTTPS_PUBLIC,
+ sourceName: 'public',
+ targetServer: Server.HTTPS_PRIVATE,
+ targetName: 'private',
+});
+
+// The following tests verify that `CSP: treat-as-public-address` makes
+// documents behave as if they had been served from a public IP address.
+
+makePreflightTests({
+ sourceServer: Server.HTTPS_LOCAL,
+ sourceTreatAsPublic: true,
+ sourceName: 'treat-as-public-address',
+ targetServer: Server.OTHER_HTTPS_LOCAL,
+ targetName: 'local',
+});
+
+promise_test_parallel(
+ t => fencedFrameTest(t, {
+ source: {
+ server: Server.HTTPS_LOCAL,
+ treatAsPublic: true,
+ },
+ target: {server: Server.HTTPS_LOCAL},
+ expected: FrameTestResult.FAILURE,
+ }),
+ 'treat-as-public-address to local (same-origin): fenced frame embedder ' +
+ 'initiated navigation has opaque origin.');
+
+makePreflightTests({
+ sourceServer: Server.HTTPS_LOCAL,
+ sourceTreatAsPublic: true,
+ sourceName: 'treat-as-public-address',
+ targetServer: Server.HTTPS_PRIVATE,
+ targetName: 'private',
+});
diff --git a/test/wpt/tests/fetch/private-network-access/fetch-from-treat-as-public.tentative.https.window.js b/test/wpt/tests/fetch/private-network-access/fetch-from-treat-as-public.tentative.https.window.js
new file mode 100644
index 0000000..084e032
--- /dev/null
+++ b/test/wpt/tests/fetch/private-network-access/fetch-from-treat-as-public.tentative.https.window.js
@@ -0,0 +1,80 @@
+// META: script=/common/utils.js
+// META: script=resources/support.sub.js
+//
+// Spec: https://wicg.github.io/private-network-access/#integration-fetch
+//
+// These tests verify that documents fetched from the `local` or `private`
+// address space yet carrying the `treat-as-public-address` CSP directive are
+// treated as if they had been fetched from the `public` address space.
+
+promise_test(t => fetchTest(t, {
+ source: {
+ server: Server.HTTPS_LOCAL,
+ treatAsPublic: true,
+ },
+ target: {
+ server: Server.OTHER_HTTPS_LOCAL,
+ preflight: PreflightBehavior.noPnaHeader(token()),
+ },
+ expected: FetchTestResult.FAILURE,
+}), "treat-as-public-address to local: failed preflight.");
+
+promise_test(t => fetchTest(t, {
+ source: {
+ server: Server.HTTPS_LOCAL,
+ treatAsPublic: true,
+ },
+ target: {
+ server: Server.OTHER_HTTPS_LOCAL,
+ behavior: {
+ preflight: PreflightBehavior.success(token()),
+ response: ResponseBehavior.allowCrossOrigin(),
+ },
+ },
+ expected: FetchTestResult.SUCCESS,
+}), "treat-as-public-address to local: success.");
+
+promise_test(t => fetchTest(t, {
+ source: {
+ server: Server.HTTPS_LOCAL,
+ treatAsPublic: true,
+ },
+ target: { server: Server.HTTPS_LOCAL },
+ expected: FetchTestResult.SUCCESS,
+}), "treat-as-public-address to local (same-origin): no preflight required.");
+
+promise_test(t => fetchTest(t, {
+ source: {
+ server: Server.HTTPS_LOCAL,
+ treatAsPublic: true,
+ },
+ target: { server: Server.HTTPS_PRIVATE },
+ expected: FetchTestResult.FAILURE,
+}), "treat-as-public-address to private: failed preflight.");
+
+promise_test(t => fetchTest(t, {
+ source: {
+ server: Server.HTTPS_LOCAL,
+ treatAsPublic: true,
+ },
+ target: {
+ server: Server.HTTPS_PRIVATE,
+ behavior: {
+ preflight: PreflightBehavior.optionalSuccess(token()),
+ response: ResponseBehavior.allowCrossOrigin(),
+ },
+ },
+ expected: FetchTestResult.SUCCESS,
+}), "treat-as-public-address to private: success.");
+
+promise_test(t => fetchTest(t, {
+ source: {
+ server: Server.HTTPS_LOCAL,
+ treatAsPublic: true,
+ },
+ target: {
+ server: Server.HTTPS_PUBLIC,
+ behavior: { response: ResponseBehavior.allowCrossOrigin() },
+ },
+ expected: FetchTestResult.SUCCESS,
+}), "treat-as-public-address to public: no preflight required.");
diff --git a/test/wpt/tests/fetch/private-network-access/fetch.tentative.https.window.js b/test/wpt/tests/fetch/private-network-access/fetch.tentative.https.window.js
new file mode 100644
index 0000000..dbc4f23
--- /dev/null
+++ b/test/wpt/tests/fetch/private-network-access/fetch.tentative.https.window.js
@@ -0,0 +1,271 @@
+// META: script=/common/subset-tests-by-key.js
+// META: script=/common/utils.js
+// META: script=resources/support.sub.js
+// META: variant=?include=baseline
+// META: variant=?include=from-local
+// META: variant=?include=from-private
+// META: variant=?include=from-public
+//
+// Spec: https://wicg.github.io/private-network-access/#integration-fetch
+//
+// These tests verify that secure contexts can fetch subresources from all
+// address spaces, provided that the target server, if more private than the
+// initiator, respond affirmatively to preflight requests.
+//
+// This file covers only those tests that must execute in a secure context.
+// Other tests are defined in: fetch.window.js
+
+setup(() => {
+ // Making sure we are in a secure context, as expected.
+ assert_true(window.isSecureContext);
+});
+
+// Source: secure local context.
+//
+// All fetches unaffected by Private Network Access.
+
+subsetTestByKey("from-local", promise_test, t => fetchTest(t, {
+ source: { server: Server.HTTPS_LOCAL },
+ target: { server: Server.HTTPS_LOCAL },
+ expected: FetchTestResult.SUCCESS,
+}), "local to local: no preflight required.");
+
+subsetTestByKey("from-local", promise_test, t => fetchTest(t, {
+ source: { server: Server.HTTPS_LOCAL },
+ target: {
+ server: Server.HTTPS_PRIVATE,
+ behavior: { response: ResponseBehavior.allowCrossOrigin() },
+ },
+ expected: FetchTestResult.SUCCESS,
+}), "local to private: no preflight required.");
+
+
+subsetTestByKey("from-local", promise_test, t => fetchTest(t, {
+ source: { server: Server.HTTPS_LOCAL },
+ target: {
+ server: Server.HTTPS_PUBLIC,
+ behavior: { response: ResponseBehavior.allowCrossOrigin() },
+ },
+ expected: FetchTestResult.SUCCESS,
+}), "local to public: no preflight required.");
+
+// Strictly speaking, the following two tests do not exercise PNA-specific
+// logic, but they serve as a baseline for comparison, ensuring that non-PNA
+// preflight requests are sent and handled as expected.
+
+subsetTestByKey("baseline", promise_test, t => fetchTest(t, {
+ source: { server: Server.HTTPS_LOCAL },
+ target: {
+ server: Server.HTTPS_PUBLIC,
+ behavior: {
+ preflight: PreflightBehavior.failure(),
+ response: ResponseBehavior.allowCrossOrigin(),
+ },
+ },
+ fetchOptions: { method: "PUT" },
+ expected: FetchTestResult.FAILURE,
+}), "local to public: PUT preflight failure.");
+
+subsetTestByKey("baseline", promise_test, t => fetchTest(t, {
+ source: { server: Server.HTTPS_LOCAL },
+ target: {
+ server: Server.HTTPS_PUBLIC,
+ behavior: {
+ preflight: PreflightBehavior.success(token()),
+ response: ResponseBehavior.allowCrossOrigin(),
+ }
+ },
+ fetchOptions: { method: "PUT" },
+ expected: FetchTestResult.SUCCESS,
+}), "local to public: PUT preflight success.");
+
+// Generates tests of preflight behavior for a single (source, target) pair.
+//
+// Scenarios:
+//
+// - cors mode:
+// - preflight response has non-2xx HTTP code
+// - preflight response is missing CORS headers
+// - preflight response is missing the PNA-specific `Access-Control` header
+// - final response is missing CORS headers
+// - success
+// - success with PUT method (non-"simple" request)
+// - no-cors mode:
+// - preflight response has non-2xx HTTP code
+// - preflight response is missing CORS headers
+// - preflight response is missing the PNA-specific `Access-Control` header
+// - success
+//
+function makePreflightTests({
+ subsetKey,
+ source,
+ sourceDescription,
+ targetServer,
+ targetDescription,
+}) {
+ const prefix =
+ `${sourceDescription} to ${targetDescription}: `;
+
+ subsetTestByKey(subsetKey, promise_test, t => fetchTest(t, {
+ source,
+ target: {
+ server: targetServer,
+ behavior: {
+ preflight: PreflightBehavior.failure(),
+ response: ResponseBehavior.allowCrossOrigin(),
+ },
+ },
+ expected: FetchTestResult.FAILURE,
+ }), prefix + "failed preflight.");
+
+ subsetTestByKey(subsetKey, promise_test, t => fetchTest(t, {
+ source,
+ target: {
+ server: targetServer,
+ behavior: {
+ preflight: PreflightBehavior.noCorsHeader(token()),
+ response: ResponseBehavior.allowCrossOrigin(),
+ },
+ },
+ expected: FetchTestResult.FAILURE,
+ }), prefix + "missing CORS headers on preflight response.");
+
+ subsetTestByKey(subsetKey, promise_test, t => fetchTest(t, {
+ source,
+ target: {
+ server: targetServer,
+ behavior: {
+ preflight: PreflightBehavior.noPnaHeader(token()),
+ response: ResponseBehavior.allowCrossOrigin(),
+ },
+ },
+ expected: FetchTestResult.FAILURE,
+ }), prefix + "missing PNA header on preflight response.");
+
+ subsetTestByKey(subsetKey, promise_test, t => fetchTest(t, {
+ source,
+ target: {
+ server: targetServer,
+ behavior: { preflight: PreflightBehavior.success(token()) },
+ },
+ expected: FetchTestResult.FAILURE,
+ }), prefix + "missing CORS headers on final response.");
+
+ subsetTestByKey(subsetKey, promise_test, t => fetchTest(t, {
+ source,
+ target: {
+ server: targetServer,
+ behavior: {
+ preflight: PreflightBehavior.success(token()),
+ response: ResponseBehavior.allowCrossOrigin(),
+ },
+ },
+ expected: FetchTestResult.SUCCESS,
+ }), prefix + "success.");
+
+ subsetTestByKey(subsetKey, promise_test, t => fetchTest(t, {
+ source,
+ target: {
+ server: targetServer,
+ behavior: {
+ preflight: PreflightBehavior.success(token()),
+ response: ResponseBehavior.allowCrossOrigin(),
+ },
+ },
+ fetchOptions: { method: "PUT" },
+ expected: FetchTestResult.SUCCESS,
+ }), prefix + "PUT success.");
+
+ subsetTestByKey(subsetKey, promise_test, t => fetchTest(t, {
+ source,
+ target: { server: targetServer },
+ fetchOptions: { mode: "no-cors" },
+ expected: FetchTestResult.FAILURE,
+ }), prefix + "no-CORS mode failed preflight.");
+
+ subsetTestByKey(subsetKey, promise_test, t => fetchTest(t, {
+ source,
+ target: {
+ server: targetServer,
+ behavior: { preflight: PreflightBehavior.noCorsHeader(token()) },
+ },
+ fetchOptions: { mode: "no-cors" },
+ expected: FetchTestResult.FAILURE,
+ }), prefix + "no-CORS mode missing CORS headers on preflight response.");
+
+ subsetTestByKey(subsetKey, promise_test, t => fetchTest(t, {
+ source,
+ target: {
+ server: targetServer,
+ behavior: { preflight: PreflightBehavior.noPnaHeader(token()) },
+ },
+ fetchOptions: { mode: "no-cors" },
+ expected: FetchTestResult.FAILURE,
+ }), prefix + "no-CORS mode missing PNA header on preflight response.");
+
+ subsetTestByKey(subsetKey, promise_test, t => fetchTest(t, {
+ source,
+ target: {
+ server: targetServer,
+ behavior: { preflight: PreflightBehavior.success(token()) },
+ },
+ fetchOptions: { mode: "no-cors" },
+ expected: FetchTestResult.OPAQUE,
+ }), prefix + "no-CORS mode success.");
+}
+
+// Source: private secure context.
+//
+// Fetches to the local address space require a successful preflight response
+// carrying a PNA-specific header.
+
+makePreflightTests({
+ subsetKey: "from-private",
+ source: { server: Server.HTTPS_PRIVATE },
+ sourceDescription: "private",
+ targetServer: Server.HTTPS_LOCAL,
+ targetDescription: "local",
+});
+
+subsetTestByKey("from-private", promise_test, t => fetchTest(t, {
+ source: { server: Server.HTTPS_PRIVATE },
+ target: { server: Server.HTTPS_PRIVATE },
+ expected: FetchTestResult.SUCCESS,
+}), "private to private: no preflight required.");
+
+subsetTestByKey("from-private", promise_test, t => fetchTest(t, {
+ source: { server: Server.HTTPS_PRIVATE },
+ target: {
+ server: Server.HTTPS_PRIVATE,
+ behavior: { response: ResponseBehavior.allowCrossOrigin() },
+ },
+ expected: FetchTestResult.SUCCESS,
+}), "private to public: no preflight required.");
+
+// Source: public secure context.
+//
+// Fetches to the local and private address spaces require a successful
+// preflight response carrying a PNA-specific header.
+
+makePreflightTests({
+ subsetKey: "from-public",
+ source: { server: Server.HTTPS_PUBLIC },
+ sourceDescription: "public",
+ targetServer: Server.HTTPS_LOCAL,
+ targetDescription: "local",
+});
+
+makePreflightTests({
+ subsetKey: "from-public",
+ source: { server: Server.HTTPS_PUBLIC },
+ sourceDescription: "public",
+ targetServer: Server.HTTPS_PRIVATE,
+ targetDescription: "private",
+});
+
+subsetTestByKey("from-public", promise_test, t => fetchTest(t, {
+ source: { server: Server.HTTPS_PUBLIC },
+ target: { server: Server.HTTPS_PUBLIC },
+ expected: FetchTestResult.SUCCESS,
+}), "public to public: no preflight required.");
+
diff --git a/test/wpt/tests/fetch/private-network-access/fetch.tentative.window.js b/test/wpt/tests/fetch/private-network-access/fetch.tentative.window.js
new file mode 100644
index 0000000..8ee54c9
--- /dev/null
+++ b/test/wpt/tests/fetch/private-network-access/fetch.tentative.window.js
@@ -0,0 +1,183 @@
+// META: script=/common/utils.js
+// META: script=resources/support.sub.js
+//
+// Spec: https://wicg.github.io/private-network-access/#integration-fetch
+//
+// These tests verify that non-secure contexts cannot fetch subresources from
+// less-public address spaces, and can fetch them otherwise.
+//
+// This file covers only those tests that must execute in a non secure context.
+// Other tests are defined in: fetch.https.window.js
+
+setup(() => {
+ // Making sure we are in a non secure context, as expected.
+ assert_false(window.isSecureContext);
+});
+
+promise_test(t => fetchTest(t, {
+ source: { server: Server.HTTP_LOCAL },
+ target: { server: Server.HTTP_LOCAL },
+ expected: FetchTestResult.SUCCESS,
+}), "local to local: no preflight required.");
+
+promise_test(t => fetchTest(t, {
+ source: { server: Server.HTTP_LOCAL },
+ target: {
+ server: Server.HTTP_PRIVATE,
+ behavior: { response: ResponseBehavior.allowCrossOrigin() },
+ },
+ expected: FetchTestResult.SUCCESS,
+}), "local to private: no preflight required.");
+
+promise_test(t => fetchTest(t, {
+ source: { server: Server.HTTP_LOCAL },
+ target: {
+ server: Server.HTTP_PUBLIC,
+ behavior: { response: ResponseBehavior.allowCrossOrigin() },
+ },
+ expected: FetchTestResult.SUCCESS,
+}), "local to public: no preflight required.");
+
+promise_test(t => fetchTest(t, {
+ source: { server: Server.HTTP_PRIVATE },
+ target: {
+ server: Server.HTTP_LOCAL,
+ behavior: {
+ preflight: PreflightBehavior.optionalSuccess(token()),
+ response: ResponseBehavior.allowCrossOrigin(),
+ },
+ },
+ expected: FetchTestResult.FAILURE,
+}), "private to local: failure.");
+
+promise_test(t => fetchTest(t, {
+ source: { server: Server.HTTP_PRIVATE },
+ target: { server: Server.HTTP_PRIVATE },
+ expected: FetchTestResult.SUCCESS,
+}), "private to private: no preflight required.");
+
+promise_test(t => fetchTest(t, {
+ source: { server: Server.HTTP_PRIVATE },
+ target: {
+ server: Server.HTTP_PUBLIC,
+ behavior: { response: ResponseBehavior.allowCrossOrigin() },
+ },
+ expected: FetchTestResult.SUCCESS,
+}), "private to public: no preflight required.");
+
+promise_test(t => fetchTest(t, {
+ source: { server: Server.HTTP_PUBLIC },
+ target: {
+ server: Server.HTTP_LOCAL,
+ behavior: {
+ preflight: PreflightBehavior.optionalSuccess(token()),
+ response: ResponseBehavior.allowCrossOrigin(),
+ },
+ },
+ expected: FetchTestResult.FAILURE,
+}), "public to local: failure.");
+
+promise_test(t => fetchTest(t, {
+ source: { server: Server.HTTP_PUBLIC },
+ target: {
+ server: Server.HTTP_PRIVATE,
+ behavior: {
+ preflight: PreflightBehavior.optionalSuccess(token()),
+ response: ResponseBehavior.allowCrossOrigin(),
+ },
+ },
+ expected: FetchTestResult.FAILURE,
+}), "public to private: failure.");
+
+promise_test(t => fetchTest(t, {
+ source: { server: Server.HTTP_PUBLIC },
+ target: { server: Server.HTTP_PUBLIC },
+ expected: FetchTestResult.SUCCESS,
+}), "public to public: no preflight required.");
+
+// These tests verify that documents fetched from the `local` address space yet
+// carrying the `treat-as-public-address` CSP directive are treated as if they
+// had been fetched from the `public` address space.
+
+promise_test(t => fetchTest(t, {
+ source: {
+ server: Server.HTTP_LOCAL,
+ treatAsPublic: true,
+ },
+ target: {
+ server: Server.HTTP_LOCAL,
+ behavior: {
+ preflight: PreflightBehavior.success(token()),
+ response: ResponseBehavior.allowCrossOrigin(),
+ },
+ },
+ expected: FetchTestResult.FAILURE,
+}), "treat-as-public-address to local: failure.");
+
+promise_test(t => fetchTest(t, {
+ source: {
+ server: Server.HTTP_LOCAL,
+ treatAsPublic: true,
+ },
+ target: {
+ server: Server.HTTP_PRIVATE,
+ behavior: {
+ preflight: PreflightBehavior.success(token()),
+ response: ResponseBehavior.allowCrossOrigin(),
+ },
+ },
+ expected: FetchTestResult.FAILURE,
+}), "treat-as-public-address to private: failure.");
+
+promise_test(t => fetchTest(t, {
+ source: {
+ server: Server.HTTP_LOCAL,
+ treatAsPublic: true,
+ },
+ target: {
+ server: Server.HTTP_PUBLIC,
+ behavior: { response: ResponseBehavior.allowCrossOrigin() },
+ },
+ expected: FetchTestResult.SUCCESS,
+}), "treat-as-public-address to public: no preflight required.");
+
+// These tests verify that HTTPS iframes embedded in an HTTP top-level document
+// cannot fetch subresources from less-public address spaces. Indeed, even
+// though the iframes have HTTPS origins, they are non-secure contexts because
+// their parent is a non-secure context.
+
+promise_test(t => fetchTest(t, {
+ source: { server: Server.HTTPS_PRIVATE },
+ target: {
+ server: Server.HTTPS_LOCAL,
+ behavior: {
+ preflight: PreflightBehavior.success(token()),
+ response: ResponseBehavior.allowCrossOrigin(),
+ },
+ },
+ expected: FetchTestResult.FAILURE,
+}), "private https to local: failure.");
+
+promise_test(t => fetchTest(t, {
+ source: { server: Server.HTTPS_PUBLIC },
+ target: {
+ server: Server.HTTPS_LOCAL,
+ behavior: {
+ preflight: PreflightBehavior.success(token()),
+ response: ResponseBehavior.allowCrossOrigin(),
+ },
+ },
+ expected: FetchTestResult.FAILURE,
+}), "public https to local: failure.");
+
+promise_test(t => fetchTest(t, {
+ source: { server: Server.HTTPS_PUBLIC },
+ target: {
+ server: Server.HTTPS_PRIVATE,
+ behavior: {
+ preflight: PreflightBehavior.success(token()),
+ response: ResponseBehavior.allowCrossOrigin(),
+ },
+ },
+ expected: FetchTestResult.FAILURE,
+}), "public https to private: failure.");
diff --git a/test/wpt/tests/fetch/private-network-access/iframe.tentative.https.window.js b/test/wpt/tests/fetch/private-network-access/iframe.tentative.https.window.js
new file mode 100644
index 0000000..0c12970
--- /dev/null
+++ b/test/wpt/tests/fetch/private-network-access/iframe.tentative.https.window.js
@@ -0,0 +1,266 @@
+// META: script=/common/subset-tests-by-key.js
+// META: script=/common/dispatcher/dispatcher.js
+// META: script=/common/utils.js
+// META: script=resources/support.sub.js
+// META: timeout=long
+// META: variant=?include=from-local
+// META: variant=?include=from-private
+// META: variant=?include=from-public
+// META: variant=?include=from-treat-as-public
+// META: variant=?include=grandparent
+//
+// Spec: https://wicg.github.io/private-network-access/#integration-fetch
+//
+// These tests verify that contexts can navigate iframes to less-public address
+// spaces iff the target server responds affirmatively to preflight requests.
+//
+// This file covers only those tests that must execute in a secure context.
+// Other tests are defined in: iframe.tentative.window.js
+
+setup(() => {
+ assert_true(window.isSecureContext);
+});
+
+// Source: secure local context.
+//
+// All fetches unaffected by Private Network Access.
+
+subsetTestByKey("from-local", promise_test_parallel, t => iframeTest(t, {
+ source: { server: Server.HTTPS_LOCAL },
+ target: { server: Server.HTTPS_LOCAL },
+ expected: FrameTestResult.SUCCESS,
+}), "local to local: no preflight required.");
+
+subsetTestByKey("from-local", promise_test_parallel, t => iframeTest(t, {
+ source: { server: Server.HTTPS_LOCAL },
+ target: { server: Server.HTTPS_PRIVATE },
+ expected: FrameTestResult.SUCCESS,
+}), "local to private: no preflight required.");
+
+subsetTestByKey("from-local", promise_test_parallel, t => iframeTest(t, {
+ source: { server: Server.HTTPS_LOCAL },
+ target: { server: Server.HTTPS_PUBLIC },
+ expected: FrameTestResult.SUCCESS,
+}), "local to public: no preflight required.");
+
+// Generates tests of preflight behavior for a single (source, target) pair.
+//
+// Scenarios:
+//
+// - parent navigates child:
+// - preflight response has non-2xx HTTP code
+// - preflight response is missing CORS headers
+// - preflight response is missing the PNA-specific `Access-Control` header
+// - success
+//
+function makePreflightTests({
+ key,
+ sourceName,
+ sourceServer,
+ sourceTreatAsPublic,
+ targetName,
+ targetServer,
+}) {
+ const prefix =
+ `${sourceName} to ${targetName}: `;
+
+ const source = {
+ server: sourceServer,
+ treatAsPublic: sourceTreatAsPublic,
+ };
+
+ promise_test_parallel(t => iframeTest(t, {
+ source,
+ target: {
+ server: targetServer,
+ behavior: { preflight: PreflightBehavior.failure() },
+ },
+ expected: FrameTestResult.FAILURE,
+ }), prefix + "failed preflight.");
+
+ promise_test_parallel(t => iframeTest(t, {
+ source,
+ target: {
+ server: targetServer,
+ behavior: { preflight: PreflightBehavior.noCorsHeader(token()) },
+ },
+ expected: FrameTestResult.FAILURE,
+ }), prefix + "missing CORS headers.");
+
+ promise_test_parallel(t => iframeTest(t, {
+ source,
+ target: {
+ server: targetServer,
+ behavior: { preflight: PreflightBehavior.noPnaHeader(token()) },
+ },
+ expected: FrameTestResult.FAILURE,
+ }), prefix + "missing PNA header.");
+
+ promise_test_parallel(t => iframeTest(t, {
+ source,
+ target: {
+ server: targetServer,
+ behavior: { preflight: PreflightBehavior.success(token()) },
+ },
+ expected: FrameTestResult.SUCCESS,
+ }), prefix + "success.");
+}
+
+// Source: private secure context.
+//
+// Fetches to the local address space require a successful preflight response
+// carrying a PNA-specific header.
+
+subsetTestByKey('from-private', makePreflightTests, {
+ sourceServer: Server.HTTPS_PRIVATE,
+ sourceName: 'private',
+ targetServer: Server.HTTPS_LOCAL,
+ targetName: 'local',
+});
+
+subsetTestByKey("from-private", promise_test_parallel, t => iframeTest(t, {
+ source: { server: Server.HTTPS_PRIVATE },
+ target: { server: Server.HTTPS_PRIVATE },
+ expected: FrameTestResult.SUCCESS,
+}), "private to private: no preflight required.");
+
+subsetTestByKey("from-private", promise_test_parallel, t => iframeTest(t, {
+ source: { server: Server.HTTPS_PRIVATE },
+ target: { server: Server.HTTPS_PUBLIC },
+ expected: FrameTestResult.SUCCESS,
+}), "private to public: no preflight required.");
+
+// Source: public secure context.
+//
+// Fetches to the local and private address spaces require a successful
+// preflight response carrying a PNA-specific header.
+
+subsetTestByKey('from-public', makePreflightTests, {
+ sourceServer: Server.HTTPS_PUBLIC,
+ sourceName: "public",
+ targetServer: Server.HTTPS_LOCAL,
+ targetName: "local",
+});
+
+subsetTestByKey('from-public', makePreflightTests, {
+ sourceServer: Server.HTTPS_PUBLIC,
+ sourceName: "public",
+ targetServer: Server.HTTPS_PRIVATE,
+ targetName: "private",
+});
+
+subsetTestByKey("from-public", promise_test_parallel, t => iframeTest(t, {
+ source: { server: Server.HTTPS_PUBLIC },
+ target: { server: Server.HTTPS_PUBLIC },
+ expected: FrameTestResult.SUCCESS,
+}), "public to public: no preflight required.");
+
+// The following tests verify that `CSP: treat-as-public-address` makes
+// documents behave as if they had been served from a public IP address.
+
+subsetTestByKey('from-treat-as-public', makePreflightTests, {
+ sourceServer: Server.HTTPS_LOCAL,
+ sourceTreatAsPublic: true,
+ sourceName: "treat-as-public-address",
+ targetServer: Server.OTHER_HTTPS_LOCAL,
+ targetName: "local",
+});
+
+subsetTestByKey(
+ 'from-treat-as-public', promise_test_parallel,
+ t => iframeTest(t, {
+ source: {
+ server: Server.HTTPS_LOCAL,
+ treatAsPublic: true,
+ },
+ target: {server: Server.HTTPS_LOCAL},
+ expected: FrameTestResult.SUCCESS,
+ }),
+ 'treat-as-public-address to local (same-origin): no preflight required.'
+);
+
+subsetTestByKey('from-treat-as-public', makePreflightTests, {
+ sourceServer: Server.HTTPS_LOCAL,
+ sourceTreatAsPublic: true,
+ sourceName: "treat-as-public-address",
+ targetServer: Server.HTTPS_PRIVATE,
+ targetName: "private",
+});
+
+subsetTestByKey(
+ 'from-treat-as-public', promise_test_parallel,
+ t => iframeTest(t, {
+ source: {
+ server: Server.HTTPS_LOCAL,
+ treatAsPublic: true,
+ },
+ target: {server: Server.HTTPS_PUBLIC},
+ expected: FrameTestResult.SUCCESS,
+ }),
+ 'treat-as-public-address to public: no preflight required.'
+);
+
+subsetTestByKey(
+ 'from-treat-as-public', promise_test_parallel,
+ t => iframeTest(t, {
+ source: {
+ server: Server.HTTPS_LOCAL,
+ treatAsPublic: true,
+ },
+ target: {
+ server: Server.HTTPS_PUBLIC,
+ behavior: {preflight: PreflightBehavior.optionalSuccess(token())}
+ },
+ expected: FrameTestResult.SUCCESS,
+ }),
+ 'treat-as-public-address to local: optional preflight'
+);
+
+// The following tests verify that when a grandparent frame navigates its
+// grandchild, the IP address space of the grandparent is compared against the
+// IP address space of the response. Indeed, the navigation initiator in this
+// case is the grandparent, not the parent.
+
+subsetTestByKey('grandparent', iframeGrandparentTest, {
+ name: 'local to local, grandparent navigates: no preflight required.',
+ grandparentServer: Server.HTTPS_LOCAL,
+ child: {server: Server.HTTPS_PUBLIC},
+ grandchild: {server: Server.OTHER_HTTPS_LOCAL},
+ expected: FrameTestResult.SUCCESS,
+});
+
+subsetTestByKey('grandparent', iframeGrandparentTest, {
+ name: "local to local (same-origin), grandparent navigates: no preflight required.",
+ grandparentServer: Server.HTTPS_LOCAL,
+ child: { server: Server.HTTPS_PUBLIC },
+ grandchild: { server: Server.HTTPS_LOCAL },
+ expected: FrameTestResult.SUCCESS,
+});
+
+subsetTestByKey('grandparent', iframeGrandparentTest, {
+ name: "public to local, grandparent navigates: failure.",
+ grandparentServer: Server.HTTPS_PUBLIC,
+ child: {
+ server: Server.HTTPS_LOCAL,
+ behavior: { preflight: PreflightBehavior.success(token()) },
+ },
+ grandchild: {
+ server: Server.HTTPS_LOCAL,
+ behavior: { preflight: PreflightBehavior.failure() },
+ },
+ expected: FrameTestResult.FAILURE,
+});
+
+subsetTestByKey('grandparent', iframeGrandparentTest, {
+ name: "public to local, grandparent navigates: success.",
+ grandparentServer: Server.HTTPS_PUBLIC,
+ child: {
+ server: Server.HTTPS_LOCAL,
+ behavior: { preflight: PreflightBehavior.success(token()) },
+ },
+ grandchild: {
+ server: Server.HTTPS_LOCAL,
+ behavior: { preflight: PreflightBehavior.success(token()) },
+ },
+ expected: FrameTestResult.SUCCESS,
+});
diff --git a/test/wpt/tests/fetch/private-network-access/iframe.tentative.window.js b/test/wpt/tests/fetch/private-network-access/iframe.tentative.window.js
new file mode 100644
index 0000000..c0770df
--- /dev/null
+++ b/test/wpt/tests/fetch/private-network-access/iframe.tentative.window.js
@@ -0,0 +1,110 @@
+// META: script=/common/dispatcher/dispatcher.js
+// META: script=/common/utils.js
+// META: script=resources/support.sub.js
+//
+// Spec: https://wicg.github.io/private-network-access/#integration-fetch
+//
+// These tests verify that non-secure contexts cannot navigate iframes to
+// less-public address spaces, and can navigate them otherwise.
+//
+// This file covers only those tests that must execute in a non secure context.
+// Other tests are defined in: iframe.https.window.js
+
+setup(() => {
+ // Making sure we are in a non secure context, as expected.
+ assert_false(window.isSecureContext);
+});
+
+promise_test_parallel(t => iframeTest(t, {
+ source: { server: Server.HTTP_LOCAL },
+ target: { server: Server.HTTP_LOCAL },
+ expected: FrameTestResult.SUCCESS,
+}), "local to local: no preflight required.");
+
+promise_test_parallel(t => iframeTest(t, {
+ source: { server: Server.HTTP_LOCAL },
+ target: { server: Server.HTTP_PRIVATE },
+ expected: FrameTestResult.SUCCESS,
+}), "local to private: no preflight required.");
+
+promise_test_parallel(t => iframeTest(t, {
+ source: { server: Server.HTTP_LOCAL },
+ target: { server: Server.HTTP_PUBLIC },
+ expected: FrameTestResult.SUCCESS,
+}), "local to public: no preflight required.");
+
+promise_test_parallel(t => iframeTest(t, {
+ source: { server: Server.HTTP_PRIVATE },
+ target: { server: Server.HTTP_LOCAL },
+ expected: FrameTestResult.FAILURE,
+}), "private to local: failure.");
+
+promise_test_parallel(t => iframeTest(t, {
+ source: { server: Server.HTTP_PRIVATE },
+ target: { server: Server.HTTP_PRIVATE },
+ expected: FrameTestResult.SUCCESS,
+}), "private to private: no preflight required.");
+
+promise_test_parallel(t => iframeTest(t, {
+ source: { server: Server.HTTP_PRIVATE },
+ target: { server: Server.HTTP_PUBLIC },
+ expected: FrameTestResult.SUCCESS,
+}), "private to public: no preflight required.");
+
+promise_test_parallel(t => iframeTest(t, {
+ source: { server: Server.HTTP_PUBLIC },
+ target: { server: Server.HTTP_LOCAL },
+ expected: FrameTestResult.FAILURE,
+}), "public to local: failure.");
+
+promise_test_parallel(t => iframeTest(t, {
+ source: { server: Server.HTTP_PUBLIC },
+ target: { server: Server.HTTP_PRIVATE },
+ expected: FrameTestResult.FAILURE,
+}), "public to private: failure.");
+
+promise_test_parallel(t => iframeTest(t, {
+ source: { server: Server.HTTP_PUBLIC },
+ target: { server: Server.HTTP_PUBLIC },
+ expected: FrameTestResult.SUCCESS,
+}), "public to public: no preflight required.");
+
+promise_test_parallel(t => iframeTest(t, {
+ source: {
+ server: Server.HTTP_LOCAL,
+ treatAsPublic: true,
+ },
+ target: { server: Server.HTTP_LOCAL },
+ expected: FrameTestResult.FAILURE,
+}), "treat-as-public-address to local: failure.");
+
+promise_test_parallel(t => iframeTest(t, {
+ source: {
+ server: Server.HTTP_LOCAL,
+ treatAsPublic: true,
+ },
+ target: { server: Server.HTTP_PRIVATE },
+ expected: FrameTestResult.FAILURE,
+}), "treat-as-public-address to private: failure.");
+
+promise_test_parallel(t => iframeTest(t, {
+ source: {
+ server: Server.HTTP_LOCAL,
+ treatAsPublic: true,
+ },
+ target: { server: Server.HTTP_PUBLIC },
+ expected: FrameTestResult.SUCCESS,
+}), "treat-as-public-address to public: no preflight required.");
+
+// The following test verifies that when a grandparent frame navigates its
+// grandchild, the IP address space of the grandparent is compared against the
+// IP address space of the response. Indeed, the navigation initiator in this
+// case is the grandparent, not the parent.
+
+iframeGrandparentTest({
+ name: "local to local, grandparent navigates: success.",
+ grandparentServer: Server.HTTP_LOCAL,
+ child: { server: Server.HTTP_PUBLIC },
+ grandchild: { server: Server.HTTP_LOCAL },
+ expected: FrameTestResult.SUCCESS,
+});
diff --git a/test/wpt/tests/fetch/private-network-access/mixed-content-fetch.tentative.https.window.js b/test/wpt/tests/fetch/private-network-access/mixed-content-fetch.tentative.https.window.js
new file mode 100644
index 0000000..54485dc
--- /dev/null
+++ b/test/wpt/tests/fetch/private-network-access/mixed-content-fetch.tentative.https.window.js
@@ -0,0 +1,277 @@
+// META: script=/common/utils.js
+// META: script=resources/support.sub.js
+//
+// Spec: https://wicg.github.io/private-network-access
+//
+// These tests verify that secure contexts can fetch non-secure subresources
+// from more private address spaces, avoiding mixed context checks, as long as
+// they specify a valid `targetAddressSpace` fetch option that matches the
+// target server's address space.
+
+setup(() => {
+ // Making sure we are in a secure context, as expected.
+ assert_true(window.isSecureContext);
+});
+
+// Given `addressSpace`, returns the other three possible IP address spaces.
+function otherAddressSpaces(addressSpace) {
+ switch (addressSpace) {
+ case "local": return ["unknown", "private", "public"];
+ case "private": return ["unknown", "local", "public"];
+ case "public": return ["unknown", "local", "private"];
+ }
+}
+
+// Generates tests of `targetAddressSpace` for the given (source, target)
+// address space pair, expecting fetches to succeed iff `targetAddressSpace` is
+// correct.
+//
+// Scenarios exercised:
+//
+// - cors mode:
+// - missing targetAddressSpace option
+// - incorrect targetAddressSpace option (x3, see `otherAddressSpaces()`)
+// - failed preflight
+// - success
+// - success with PUT method (non-"simple" request)
+// - no-cors mode:
+// - success
+//
+function makeTests({ source, target }) {
+ const sourceServer = Server.get("https", source);
+ const targetServer = Server.get("http", target);
+
+ const makeTest = ({
+ fetchOptions,
+ targetBehavior,
+ name,
+ expected
+ }) => {
+ promise_test_parallel(t => fetchTest(t, {
+ source: { server: sourceServer },
+ target: {
+ server: targetServer,
+ behavior: targetBehavior,
+ },
+ fetchOptions,
+ expected,
+ }), `${sourceServer.name} to ${targetServer.name}: ${name}.`);
+ };
+
+ makeTest({
+ name: "missing targetAddressSpace",
+ targetBehavior: {
+ preflight: PreflightBehavior.success(token()),
+ response: ResponseBehavior.allowCrossOrigin(),
+ },
+ expected: FetchTestResult.FAILURE,
+ });
+
+ const correctAddressSpace = targetServer.addressSpace;
+
+ for (const targetAddressSpace of otherAddressSpaces(correctAddressSpace)) {
+ makeTest({
+ name: `wrong targetAddressSpace "${targetAddressSpace}"`,
+ targetBehavior: {
+ preflight: PreflightBehavior.success(token()),
+ response: ResponseBehavior.allowCrossOrigin(),
+ },
+ fetchOptions: { targetAddressSpace },
+ expected: FetchTestResult.FAILURE,
+ });
+ }
+
+ makeTest({
+ name: "failed preflight",
+ targetBehavior: {
+ preflight: PreflightBehavior.failure(),
+ response: ResponseBehavior.allowCrossOrigin(),
+ },
+ fetchOptions: { targetAddressSpace: correctAddressSpace },
+ expected: FetchTestResult.FAILURE,
+ });
+
+ makeTest({
+ name: "success",
+ targetBehavior: {
+ preflight: PreflightBehavior.success(token()),
+ response: ResponseBehavior.allowCrossOrigin(),
+ },
+ fetchOptions: { targetAddressSpace: correctAddressSpace },
+ expected: FetchTestResult.SUCCESS,
+ });
+
+ makeTest({
+ name: "PUT success",
+ targetBehavior: {
+ preflight: PreflightBehavior.success(token()),
+ response: ResponseBehavior.allowCrossOrigin(),
+ },
+ fetchOptions: {
+ targetAddressSpace: correctAddressSpace,
+ method: "PUT",
+ },
+ expected: FetchTestResult.SUCCESS,
+ });
+
+ makeTest({
+ name: "no-cors success",
+ targetBehavior: {
+ preflight: PreflightBehavior.success(token()),
+ response: ResponseBehavior.allowCrossOrigin(),
+ },
+ fetchOptions: {
+ targetAddressSpace: correctAddressSpace,
+ mode: "no-cors",
+ },
+ expected: FetchTestResult.OPAQUE,
+ });
+}
+
+// Generates tests for the given (source, target) address space pair expecting
+// that `targetAddressSpace` cannot be used to bypass mixed content.
+//
+// Scenarios exercised:
+//
+// - wrong `targetAddressSpace` (x3, see `otherAddressSpaces()`)
+// - correct `targetAddressSpace`
+//
+function makeNoBypassTests({ source, target }) {
+ const sourceServer = Server.get("https", source);
+ const targetServer = Server.get("http", target);
+
+ const prefix = `${sourceServer.name} to ${targetServer.name}: `;
+
+ const correctAddressSpace = targetServer.addressSpace;
+ for (const targetAddressSpace of otherAddressSpaces(correctAddressSpace)) {
+ promise_test_parallel(t => fetchTest(t, {
+ source: { server: sourceServer },
+ target: {
+ server: targetServer,
+ behavior: {
+ preflight: PreflightBehavior.success(token()),
+ response: ResponseBehavior.allowCrossOrigin(),
+ },
+ },
+ fetchOptions: { targetAddressSpace },
+ expected: FetchTestResult.FAILURE,
+ }), prefix + `wrong targetAddressSpace "${targetAddressSpace}".`);
+ }
+
+ promise_test_parallel(t => fetchTest(t, {
+ source: { server: sourceServer },
+ target: {
+ server: targetServer,
+ behavior: {
+ preflight: PreflightBehavior.success(token()),
+ response: ResponseBehavior.allowCrossOrigin(),
+ },
+ },
+ fetchOptions: { targetAddressSpace: correctAddressSpace },
+ expected: FetchTestResult.FAILURE,
+ }), prefix + 'not a private network request.');
+}
+
+// Source: local secure context.
+//
+// Fetches to the local and private address spaces cannot use
+// `targetAddressSpace` to bypass mixed content, as they are not otherwise
+// blocked by Private Network Access.
+
+makeNoBypassTests({ source: "local", target: "local" });
+makeNoBypassTests({ source: "local", target: "private" });
+makeNoBypassTests({ source: "local", target: "public" });
+
+// Source: private secure context.
+//
+// Fetches to the local address space requires the right `targetAddressSpace`
+// option, as well as a successful preflight response carrying a PNA-specific
+// header.
+//
+// Fetches to the private address space cannot use `targetAddressSpace` to
+// bypass mixed content, as they are not otherwise blocked by Private Network
+// Access.
+
+makeTests({ source: "private", target: "local" });
+
+makeNoBypassTests({ source: "private", target: "private" });
+makeNoBypassTests({ source: "private", target: "public" });
+
+// Source: public secure context.
+//
+// Fetches to the local and private address spaces require the right
+// `targetAddressSpace` option, as well as a successful preflight response
+// carrying a PNA-specific header.
+
+makeTests({ source: "public", target: "local" });
+makeTests({ source: "public", target: "private" });
+
+makeNoBypassTests({ source: "public", target: "public" });
+
+// These tests verify that documents fetched from the `local` address space yet
+// carrying the `treat-as-public-address` CSP directive are treated as if they
+// had been fetched from the `public` address space.
+
+promise_test_parallel(t => fetchTest(t, {
+ source: {
+ server: Server.HTTPS_LOCAL,
+ treatAsPublic: true,
+ },
+ target: {
+ server: Server.HTTP_LOCAL,
+ behavior: {
+ preflight: PreflightBehavior.optionalSuccess(token()),
+ response: ResponseBehavior.allowCrossOrigin(),
+ },
+ },
+ fetchOptions: { targetAddressSpace: "private" },
+ expected: FetchTestResult.FAILURE,
+}), 'https-treat-as-public to http-local: wrong targetAddressSpace "private".');
+
+promise_test_parallel(t => fetchTest(t, {
+ source: {
+ server: Server.HTTPS_LOCAL,
+ treatAsPublic: true,
+ },
+ target: {
+ server: Server.HTTP_LOCAL,
+ behavior: {
+ preflight: PreflightBehavior.optionalSuccess(token()),
+ response: ResponseBehavior.allowCrossOrigin(),
+ },
+ },
+ fetchOptions: { targetAddressSpace: "local" },
+ expected: FetchTestResult.SUCCESS,
+}), "https-treat-as-public to http-local: success.");
+
+promise_test_parallel(t => fetchTest(t, {
+ source: {
+ server: Server.HTTPS_LOCAL,
+ treatAsPublic: true,
+ },
+ target: {
+ server: Server.HTTP_PRIVATE,
+ behavior: {
+ preflight: PreflightBehavior.success(token()),
+ response: ResponseBehavior.allowCrossOrigin(),
+ },
+ },
+ fetchOptions: { targetAddressSpace: "local" },
+ expected: FetchTestResult.FAILURE,
+}), 'https-treat-as-public to http-private: wrong targetAddressSpace "local".');
+
+promise_test_parallel(t => fetchTest(t, {
+ source: {
+ server: Server.HTTPS_LOCAL,
+ treatAsPublic: true,
+ },
+ target: {
+ server: Server.HTTP_PRIVATE,
+ behavior: {
+ preflight: PreflightBehavior.success(token()),
+ response: ResponseBehavior.allowCrossOrigin(),
+ },
+ },
+ fetchOptions: { targetAddressSpace: "private" },
+ expected: FetchTestResult.SUCCESS,
+}), "https-treat-as-public to http-private: success.");
diff --git a/test/wpt/tests/fetch/private-network-access/nested-worker.tentative.https.window.js b/test/wpt/tests/fetch/private-network-access/nested-worker.tentative.https.window.js
new file mode 100644
index 0000000..3eeb435
--- /dev/null
+++ b/test/wpt/tests/fetch/private-network-access/nested-worker.tentative.https.window.js
@@ -0,0 +1,36 @@
+// META: script=/common/utils.js
+// META: script=resources/support.sub.js
+//
+// Spec: https://wicg.github.io/private-network-access/#integration-fetch
+//
+// These tests check that initial `Worker` script fetches from within worker
+// scopes are subject to Private Network Access checks, just like a worker
+// script fetches from within document scopes (for non-nested workers). The
+// latter are tested in: worker.https.window.js
+//
+// This file covers only those tests that must execute in a secure context.
+// Other tests are defined in: nested-worker.window.js
+
+promise_test(t => nestedWorkerScriptTest(t, {
+ source: {
+ server: Server.HTTPS_LOCAL,
+ treatAsPublic: true,
+ },
+ target: { server: Server.HTTPS_LOCAL },
+ expected: WorkerScriptTestResult.FAILURE,
+}), "treat-as-public to local: failure.");
+
+promise_test(t => nestedWorkerScriptTest(t, {
+ source: {
+ server: Server.HTTPS_PRIVATE,
+ treatAsPublic: true,
+ },
+ target: { server: Server.HTTPS_PRIVATE },
+ expected: WorkerScriptTestResult.FAILURE,
+}), "treat-as-public to private: failure.");
+
+promise_test(t => nestedWorkerScriptTest(t, {
+ source: { server: Server.HTTPS_PUBLIC },
+ target: { server: Server.HTTPS_PUBLIC },
+ expected: WorkerScriptTestResult.SUCCESS,
+}), "public to public: success.");
diff --git a/test/wpt/tests/fetch/private-network-access/nested-worker.tentative.window.js b/test/wpt/tests/fetch/private-network-access/nested-worker.tentative.window.js
new file mode 100644
index 0000000..6d246e1
--- /dev/null
+++ b/test/wpt/tests/fetch/private-network-access/nested-worker.tentative.window.js
@@ -0,0 +1,36 @@
+// META: script=/common/utils.js
+// META: script=resources/support.sub.js
+//
+// Spec: https://wicg.github.io/private-network-access/#integration-fetch
+//
+// These tests check that initial `Worker` script fetches from within worker
+// scopes are subject to Private Network Access checks, just like a worker
+// script fetches from within document scopes (for non-nested workers). The
+// latter are tested in: worker.window.js
+//
+// This file covers only those tests that must execute in a non secure context.
+// Other tests are defined in: nested-worker.https.window.js
+
+promise_test(t => nestedWorkerScriptTest(t, {
+ source: {
+ server: Server.HTTP_LOCAL,
+ treatAsPublic: true,
+ },
+ target: { server: Server.HTTP_LOCAL },
+ expected: WorkerScriptTestResult.FAILURE,
+}), "treat-as-public to local: failure.");
+
+promise_test(t => nestedWorkerScriptTest(t, {
+ source: {
+ server: Server.HTTP_PRIVATE,
+ treatAsPublic: true,
+ },
+ target: { server: Server.HTTP_PRIVATE },
+ expected: WorkerScriptTestResult.FAILURE,
+}), "treat-as-public to private: failure.");
+
+promise_test(t => nestedWorkerScriptTest(t, {
+ source: { server: Server.HTTP_PUBLIC },
+ target: { server: Server.HTTP_PUBLIC },
+ expected: WorkerScriptTestResult.SUCCESS,
+}), "public to public: success.");
diff --git a/test/wpt/tests/fetch/private-network-access/preflight-cache.https.tentative.window.js b/test/wpt/tests/fetch/private-network-access/preflight-cache.https.tentative.window.js
new file mode 100644
index 0000000..87dbf50
--- /dev/null
+++ b/test/wpt/tests/fetch/private-network-access/preflight-cache.https.tentative.window.js
@@ -0,0 +1,88 @@
+// META: script=/common/utils.js
+// META: script=resources/support.sub.js
+//
+// Spec: https://wicg.github.io/private-network-access/#cors-preflight
+//
+// These tests verify that PNA preflight responses are cached.
+//
+// TODO(https://crbug.com/1268312): We cannot currently test that cache
+// entries are keyed by target IP address space because that requires
+// loading the same URL from different IP address spaces, and the WPT
+// framework does not allow that.
+promise_test(async t => {
+ let uuid = token();
+ await fetchTest(t, {
+ source: { server: Server.HTTPS_PRIVATE },
+ target: {
+ server: Server.HTTPS_LOCAL,
+ behavior: {
+ preflight: PreflightBehavior.singlePreflight(uuid),
+ response: ResponseBehavior.allowCrossOrigin(),
+ },
+ },
+ expected: FetchTestResult.SUCCESS,
+ });
+ await fetchTest(t, {
+ source: { server: Server.HTTPS_PRIVATE },
+ target: {
+ server: Server.HTTPS_LOCAL,
+ behavior: {
+ preflight: PreflightBehavior.singlePreflight(uuid),
+ response: ResponseBehavior.allowCrossOrigin(),
+ },
+ },
+ expected: FetchTestResult.SUCCESS,
+ });
+}, "private to local: success.");
+
+promise_test(async t => {
+ let uuid = token();
+ await fetchTest(t, {
+ source: { server: Server.HTTPS_PUBLIC },
+ target: {
+ server: Server.HTTPS_LOCAL,
+ behavior: {
+ preflight: PreflightBehavior.singlePreflight(uuid),
+ response: ResponseBehavior.allowCrossOrigin(),
+ },
+ },
+ expected: FetchTestResult.SUCCESS,
+ });
+ await fetchTest(t, {
+ source: { server: Server.HTTPS_PUBLIC },
+ target: {
+ server: Server.HTTPS_LOCAL,
+ behavior: {
+ preflight: PreflightBehavior.singlePreflight(uuid),
+ response: ResponseBehavior.allowCrossOrigin(),
+ },
+ },
+ expected: FetchTestResult.SUCCESS,
+ });
+}, "public to local: success.");
+
+promise_test(async t => {
+ let uuid = token();
+ await fetchTest(t, {
+ source: { server: Server.HTTPS_PUBLIC },
+ target: {
+ server: Server.HTTPS_PRIVATE,
+ behavior: {
+ preflight: PreflightBehavior.singlePreflight(uuid),
+ response: ResponseBehavior.allowCrossOrigin(),
+ },
+ },
+ expected: FetchTestResult.SUCCESS,
+ });
+ await fetchTest(t, {
+ source: { server: Server.HTTPS_PUBLIC },
+ target: {
+ server: Server.HTTPS_PRIVATE,
+ behavior: {
+ preflight: PreflightBehavior.singlePreflight(uuid),
+ response: ResponseBehavior.allowCrossOrigin(),
+ },
+ },
+ expected: FetchTestResult.SUCCESS,
+ });
+}, "public to private: success."); \ No newline at end of file
diff --git a/test/wpt/tests/fetch/private-network-access/redirect.tentative.https.window.js b/test/wpt/tests/fetch/private-network-access/redirect.tentative.https.window.js
new file mode 100644
index 0000000..efbd8f3
--- /dev/null
+++ b/test/wpt/tests/fetch/private-network-access/redirect.tentative.https.window.js
@@ -0,0 +1,640 @@
+// META: script=/common/utils.js
+// META: script=resources/support.sub.js
+//
+// Spec: https://wicg.github.io/private-network-access/#integration-fetch
+//
+// This test verifies that Private Network Access checks are applied to all
+// the endpoints in a redirect chain, relative to the same client context.
+
+// local -> private -> public
+//
+// Request 1 (local -> private): no preflight.
+// Request 2 (local -> public): no preflight.
+
+promise_test(t => fetchTest(t, {
+ source: { server: Server.HTTPS_LOCAL },
+ target: {
+ server: Server.HTTPS_PRIVATE,
+ behavior: {
+ response: ResponseBehavior.allowCrossOrigin(),
+ redirect: preflightUrl({
+ server: Server.HTTPS_PUBLIC,
+ behavior: { response: ResponseBehavior.allowCrossOrigin() },
+ }),
+ }
+ },
+ expected: FetchTestResult.SUCCESS,
+}), "local to private to public: success.");
+
+// local -> private -> local
+//
+// Request 1 (local -> private): no preflight.
+// Request 2 (local -> local): no preflight.
+//
+// This checks that the client for the second request is still the initial
+// context, not the redirector.
+
+promise_test(t => fetchTest(t, {
+ source: { server: Server.HTTPS_LOCAL },
+ target: {
+ server: Server.HTTPS_PRIVATE,
+ behavior: {
+ response: ResponseBehavior.allowCrossOrigin(),
+ redirect: preflightUrl({
+ server: Server.HTTPS_LOCAL,
+ behavior: { response: ResponseBehavior.allowCrossOrigin() },
+ }),
+ }
+ },
+ expected: FetchTestResult.SUCCESS,
+}), "local to private to local: success.");
+
+// private -> private -> local
+//
+// Request 1 (private -> private): no preflight.
+// Request 2 (private -> local): preflight required.
+//
+// This verifies that PNA checks are applied after redirects.
+
+promise_test(t => fetchTest(t, {
+ source: { server: Server.HTTPS_PRIVATE },
+ target: {
+ server: Server.HTTPS_PRIVATE,
+ behavior: {
+ redirect: preflightUrl({
+ server: Server.HTTPS_LOCAL,
+ behavior: { response: ResponseBehavior.allowCrossOrigin() },
+ }),
+ }
+ },
+ expected: FetchTestResult.FAILURE,
+}), "private to private to local: failed preflight.");
+
+promise_test(t => fetchTest(t, {
+ source: { server: Server.HTTPS_PRIVATE },
+ target: {
+ server: Server.HTTPS_PRIVATE,
+ behavior: {
+ redirect: preflightUrl({
+ server: Server.HTTPS_LOCAL,
+ behavior: {
+ preflight: PreflightBehavior.success(token()),
+ response: ResponseBehavior.allowCrossOrigin(),
+ },
+ }),
+ }
+ },
+ expected: FetchTestResult.SUCCESS,
+}), "private to private to local: success.");
+
+promise_test(t => fetchTest(t, {
+ source: { server: Server.HTTPS_PRIVATE },
+ target: {
+ server: Server.HTTPS_PRIVATE,
+ behavior: {
+ redirect: preflightUrl({
+ server: Server.HTTPS_LOCAL,
+ behavior: { preflight: PreflightBehavior.success(token()) },
+ }),
+ }
+ },
+ fetchOptions: { mode: "no-cors" },
+ expected: FetchTestResult.OPAQUE,
+}), "private to private to local: no-cors success.");
+
+// private -> local -> private
+//
+// Request 1 (private -> local): preflight required.
+// Request 2 (private -> private): no preflight.
+//
+// This verifies that PNA checks are applied independently to every step in a
+// redirect chain.
+
+promise_test(t => fetchTest(t, {
+ source: { server: Server.HTTPS_PRIVATE },
+ target: {
+ server: Server.HTTPS_LOCAL,
+ behavior: {
+ response: ResponseBehavior.allowCrossOrigin(),
+ redirect: preflightUrl({
+ server: Server.HTTPS_PRIVATE,
+ }),
+ }
+ },
+ expected: FetchTestResult.FAILURE,
+}), "private to local to private: failed preflight.");
+
+promise_test(t => fetchTest(t, {
+ source: { server: Server.HTTPS_PRIVATE },
+ target: {
+ server: Server.HTTPS_LOCAL,
+ behavior: {
+ preflight: PreflightBehavior.success(token()),
+ response: ResponseBehavior.allowCrossOrigin(),
+ redirect: preflightUrl({
+ server: Server.HTTPS_PRIVATE,
+ behavior: { response: ResponseBehavior.allowCrossOrigin() },
+ }),
+ }
+ },
+ expected: FetchTestResult.SUCCESS,
+}), "private to local to private: success.");
+
+promise_test(t => fetchTest(t, {
+ source: { server: Server.HTTPS_PRIVATE },
+ target: {
+ server: Server.HTTPS_LOCAL,
+ behavior: {
+ preflight: PreflightBehavior.success(token()),
+ redirect: preflightUrl({ server: Server.HTTPS_PRIVATE }),
+ }
+ },
+ fetchOptions: { mode: "no-cors" },
+ expected: FetchTestResult.OPAQUE,
+}), "private to local to private: no-cors success.");
+
+// public -> private -> local
+//
+// Request 1 (public -> private): preflight required.
+// Request 2 (public -> local): preflight required.
+//
+// This verifies that PNA checks are applied to every step in a redirect chain.
+
+promise_test(t => fetchTest(t, {
+ source: { server: Server.HTTPS_PUBLIC },
+ target: {
+ server: Server.HTTPS_PRIVATE,
+ behavior: {
+ response: ResponseBehavior.allowCrossOrigin(),
+ redirect: preflightUrl({
+ server: Server.HTTPS_LOCAL,
+ behavior: {
+ preflight: PreflightBehavior.success(token()),
+ response: ResponseBehavior.allowCrossOrigin(),
+ },
+ }),
+ }
+ },
+ expected: FetchTestResult.FAILURE,
+}), "public to private to local: failed first preflight.");
+
+promise_test(t => fetchTest(t, {
+ source: { server: Server.HTTPS_PUBLIC },
+ target: {
+ server: Server.HTTPS_PRIVATE,
+ behavior: {
+ preflight: PreflightBehavior.success(token()),
+ response: ResponseBehavior.allowCrossOrigin(),
+ redirect: preflightUrl({
+ server: Server.HTTPS_LOCAL,
+ behavior: {
+ response: ResponseBehavior.allowCrossOrigin(),
+ },
+ }),
+ }
+ },
+ expected: FetchTestResult.FAILURE,
+}), "public to private to local: failed second preflight.");
+
+promise_test(t => fetchTest(t, {
+ source: { server: Server.HTTPS_PUBLIC },
+ target: {
+ server: Server.HTTPS_PRIVATE,
+ behavior: {
+ preflight: PreflightBehavior.success(token()),
+ response: ResponseBehavior.allowCrossOrigin(),
+ redirect: preflightUrl({
+ server: Server.HTTPS_LOCAL,
+ behavior: {
+ preflight: PreflightBehavior.success(token()),
+ response: ResponseBehavior.allowCrossOrigin(),
+ },
+ }),
+ }
+ },
+ expected: FetchTestResult.SUCCESS,
+}), "public to private to local: success.");
+
+promise_test(t => fetchTest(t, {
+ source: { server: Server.HTTPS_PUBLIC },
+ target: {
+ server: Server.HTTPS_PRIVATE,
+ behavior: {
+ preflight: PreflightBehavior.success(token()),
+ redirect: preflightUrl({
+ server: Server.HTTPS_LOCAL,
+ behavior: { preflight: PreflightBehavior.success(token()) },
+ }),
+ }
+ },
+ fetchOptions: { mode: "no-cors" },
+ expected: FetchTestResult.OPAQUE,
+}), "public to private to local: no-cors success.");
+
+// treat-as-public -> local -> private
+
+// Request 1 (treat-as-public -> local): preflight required.
+// Request 2 (treat-as-public -> private): preflight required.
+
+// This verifies that PNA checks are applied to every step in a redirect chain.
+
+promise_test(t => fetchTest(t, {
+ source: {
+ server: Server.HTTPS_LOCAL,
+ treatAsPublic: true,
+ },
+ target: {
+ server: Server.OTHER_HTTPS_LOCAL,
+ behavior: {
+ redirect: preflightUrl({
+ server: Server.HTTPS_PRIVATE,
+ behavior: {
+ preflight: PreflightBehavior.success(token()),
+ response: ResponseBehavior.allowCrossOrigin(),
+ },
+ }),
+ response: ResponseBehavior.allowCrossOrigin(),
+ }
+ },
+ expected: FetchTestResult.FAILURE,
+}), "treat-as-public to local to private: failed first preflight.");
+
+promise_test(t => fetchTest(t, {
+ source: {
+ server: Server.HTTPS_LOCAL,
+ treatAsPublic: true,
+ },
+ target: {
+ server: Server.OTHER_HTTPS_LOCAL,
+ behavior: {
+ preflight: PreflightBehavior.success(token()),
+ redirect: preflightUrl({
+ server: Server.HTTPS_PRIVATE,
+ behavior: {
+ preflight: PreflightBehavior.noPnaHeader(token()),
+ response: ResponseBehavior.allowCrossOrigin(),
+ },
+ }),
+ response: ResponseBehavior.allowCrossOrigin(),
+ }
+ },
+ expected: FetchTestResult.FAILURE,
+}), "treat-as-public to local to private: failed second preflight.");
+
+promise_test(t => fetchTest(t, {
+ source: {
+ server: Server.HTTPS_LOCAL,
+ treatAsPublic: true,
+ },
+ target: {
+ server: Server.OTHER_HTTPS_LOCAL,
+ behavior: {
+ preflight: PreflightBehavior.success(token()),
+ redirect: preflightUrl({
+ server: Server.HTTPS_PRIVATE,
+ behavior: {
+ preflight: PreflightBehavior.success(token()),
+ response: ResponseBehavior.allowCrossOrigin(),
+ },
+ }),
+ response: ResponseBehavior.allowCrossOrigin(),
+ }
+ },
+ expected: FetchTestResult.SUCCESS,
+}), "treat-as-public to local to private: success.");
+
+promise_test(t => fetchTest(t, {
+ source: {
+ server: Server.HTTPS_LOCAL,
+ treatAsPublic: true,
+ },
+ target: {
+ server: Server.OTHER_HTTPS_LOCAL,
+ behavior: {
+ redirect: preflightUrl({
+ server: Server.HTTPS_PRIVATE,
+ behavior: { preflight: PreflightBehavior.success(token()) },
+ }),
+ }
+ },
+ fetchOptions: { mode: "no-cors" },
+ expected: FetchTestResult.FAILURE,
+}), "treat-as-public to local to private: no-cors failed first preflight.");
+
+promise_test(t => fetchTest(t, {
+ source: {
+ server: Server.HTTPS_LOCAL,
+ treatAsPublic: true,
+ },
+ target: {
+ server: Server.OTHER_HTTPS_LOCAL,
+ behavior: {
+ preflight: PreflightBehavior.success(token()),
+ redirect: preflightUrl({ server: Server.HTTPS_PRIVATE }),
+ }
+ },
+ fetchOptions: { mode: "no-cors" },
+ expected: FetchTestResult.FAILURE,
+}), "treat-as-public to local to private: no-cors failed second preflight.");
+
+promise_test(t => fetchTest(t, {
+ source: {
+ server: Server.HTTPS_LOCAL,
+ treatAsPublic: true,
+ },
+ target: {
+ server: Server.OTHER_HTTPS_LOCAL,
+ behavior: {
+ preflight: PreflightBehavior.success(token()),
+ redirect: preflightUrl({
+ server: Server.HTTPS_PRIVATE,
+ behavior: { preflight: PreflightBehavior.success(token()) },
+ }),
+ }
+ },
+ fetchOptions: { mode: "no-cors" },
+ expected: FetchTestResult.OPAQUE,
+}), "treat-as-public to local to private: no-cors success.");
+
+// treat-as-public -> local (same-origin) -> private
+
+// Request 1 (treat-as-public -> local (same-origin)): no preflight required.
+// Request 2 (treat-as-public -> private): preflight required.
+
+// This verifies that PNA checks are applied only to the second step in a
+// redirect chain if the first step is same-origin and the origin is potentially
+// trustworthy.
+
+promise_test(t => fetchTest(t, {
+ source: {
+ server: Server.HTTPS_LOCAL,
+ treatAsPublic: true,
+ },
+ target: {
+ server: Server.HTTPS_LOCAL,
+ behavior: {
+ redirect: preflightUrl({
+ server: Server.HTTPS_PRIVATE,
+ behavior: {
+ preflight: PreflightBehavior.noPnaHeader(token()),
+ response: ResponseBehavior.allowCrossOrigin(),
+ },
+ }),
+ }
+ },
+ expected: FetchTestResult.FAILURE,
+}), "treat-as-public to local (same-origin) to private: failed second preflight.");
+
+promise_test(t => fetchTest(t, {
+ source: {
+ server: Server.HTTPS_LOCAL,
+ treatAsPublic: true,
+ },
+ target: {
+ server: Server.HTTPS_LOCAL,
+ behavior: {
+ redirect: preflightUrl({
+ server: Server.HTTPS_PRIVATE,
+ behavior: {
+ preflight: PreflightBehavior.success(token()),
+ response: ResponseBehavior.allowCrossOrigin(),
+ },
+ }),
+ }
+ },
+ expected: FetchTestResult.SUCCESS,
+}), "treat-as-public to local (same-origin) to private: success.");
+
+promise_test(t => fetchTest(t, {
+ source: {
+ server: Server.HTTPS_LOCAL,
+ treatAsPublic: true,
+ },
+ target: {
+ server: Server.HTTPS_LOCAL,
+ behavior: {
+ redirect: preflightUrl({ server: Server.HTTPS_PRIVATE }),
+ }
+ },
+ fetchOptions: { mode: "no-cors" },
+ expected: FetchTestResult.FAILURE,
+}), "treat-as-public to local (same-origin) to private: no-cors failed second preflight.");
+
+promise_test(t => fetchTest(t, {
+ source: {
+ server: Server.HTTPS_LOCAL,
+ treatAsPublic: true,
+ },
+ target: {
+ server: Server.HTTPS_LOCAL,
+ behavior: {
+ redirect: preflightUrl({
+ server: Server.HTTPS_PRIVATE,
+ behavior: { preflight: PreflightBehavior.success(token()) },
+ }),
+ }
+ },
+ fetchOptions: { mode: "no-cors" },
+ expected: FetchTestResult.OPAQUE,
+}), "treat-as-public to local (same-origin) to private: no-cors success.");
+
+// treat-as-public -> private -> local
+
+// Request 1 (treat-as-public -> private): preflight required.
+// Request 2 (treat-as-public -> local): preflight required.
+
+// This verifies that PNA checks are applied to every step in a redirect chain.
+
+promise_test(t => fetchTest(t, {
+ source: {
+ server: Server.HTTPS_LOCAL,
+ treatAsPublic: true,
+ },
+ target: {
+ server: Server.HTTPS_PRIVATE,
+ behavior: {
+ preflight: PreflightBehavior.noPnaHeader(token()),
+ response: ResponseBehavior.allowCrossOrigin(),
+ redirect: preflightUrl({
+ server: Server.OTHER_HTTPS_LOCAL,
+ behavior: {
+ preflight: PreflightBehavior.success(token()),
+ response: ResponseBehavior.allowCrossOrigin(),
+ },
+ }),
+ }
+ },
+ expected: FetchTestResult.FAILURE,
+}), "treat-as-public to private to local: failed first preflight.");
+
+promise_test(t => fetchTest(t, {
+ source: {
+ server: Server.HTTPS_LOCAL,
+ treatAsPublic: true,
+ },
+ target: {
+ server: Server.HTTPS_PRIVATE,
+ behavior: {
+ preflight: PreflightBehavior.success(token()),
+ response: ResponseBehavior.allowCrossOrigin(),
+ redirect: preflightUrl({
+ server: Server.OTHER_HTTPS_LOCAL,
+ behavior: { response: ResponseBehavior.allowCrossOrigin() },
+ }),
+ }
+ },
+ expected: FetchTestResult.FAILURE,
+}), "treat-as-public to private to local: failed second preflight.");
+
+promise_test(t => fetchTest(t, {
+ source: {
+ server: Server.HTTPS_LOCAL,
+ treatAsPublic: true,
+ },
+ target: {
+ server: Server.HTTPS_PRIVATE,
+ behavior: {
+ preflight: PreflightBehavior.success(token()),
+ response: ResponseBehavior.allowCrossOrigin(),
+ redirect: preflightUrl({
+ server: Server.OTHER_HTTPS_LOCAL,
+ behavior: {
+ preflight: PreflightBehavior.success(token()),
+ response: ResponseBehavior.allowCrossOrigin(),
+ },
+ }),
+ }
+ },
+ expected: FetchTestResult.SUCCESS,
+}), "treat-as-public to private to local: success.");
+
+promise_test(t => fetchTest(t, {
+ source: {
+ server: Server.HTTPS_LOCAL,
+ treatAsPublic: true,
+ },
+ target: {
+ server: Server.HTTPS_PRIVATE,
+ behavior: {
+ redirect: preflightUrl({
+ server: Server.OTHER_HTTPS_LOCAL,
+ behavior: { preflight: PreflightBehavior.success(token()) },
+ }),
+ }
+ },
+ fetchOptions: { mode: "no-cors" },
+ expected: FetchTestResult.FAILURE,
+}), "treat-as-public to private to local: no-cors failed first preflight.");
+
+promise_test(t => fetchTest(t, {
+ source: {
+ server: Server.HTTPS_LOCAL,
+ treatAsPublic: true,
+ },
+ target: {
+ server: Server.HTTPS_PRIVATE,
+ behavior: {
+ preflight: PreflightBehavior.success(token()),
+ redirect: preflightUrl({ server: Server.OTHER_HTTPS_LOCAL }),
+ }
+ },
+ fetchOptions: { mode: "no-cors" },
+ expected: FetchTestResult.FAILURE,
+}), "treat-as-public to private to local: no-cors failed second preflight.");
+
+promise_test(t => fetchTest(t, {
+ source: {
+ server: Server.HTTPS_LOCAL,
+ treatAsPublic: true,
+ },
+ target: {
+ server: Server.HTTPS_PRIVATE,
+ behavior: {
+ preflight: PreflightBehavior.success(token()),
+ redirect: preflightUrl({
+ server: Server.OTHER_HTTPS_LOCAL,
+ behavior: { preflight: PreflightBehavior.success(token()) },
+ }),
+ }
+ },
+ fetchOptions: { mode: "no-cors" },
+ expected: FetchTestResult.OPAQUE,
+}), "treat-as-public to private to local: no-cors success.");
+
+// treat-as-public -> private -> local (same-origin)
+
+// Request 1 (treat-as-public -> private): preflight required.
+// Request 2 (treat-as-public -> local (same-origin)): no preflight required.
+
+// This verifies that PNA checks are only applied to the first step in a
+// redirect chain if the second step is same-origin and the origin is
+// potentially trustworthy.
+
+promise_test(t => fetchTest(t, {
+ source: {
+ server: Server.HTTPS_LOCAL,
+ treatAsPublic: true,
+ },
+ target: {
+ server: Server.HTTPS_PRIVATE,
+ behavior: {
+ preflight: PreflightBehavior.noPnaHeader(token()),
+ response: ResponseBehavior.allowCrossOrigin(),
+ redirect: preflightUrl({
+ server: Server.HTTPS_LOCAL,
+ behavior: { response: ResponseBehavior.allowCrossOrigin() },
+ }),
+ }
+ },
+ expected: FetchTestResult.FAILURE,
+}), "treat-as-public to private to local (same-origin): failed first preflight.");
+
+promise_test(t => fetchTest(t, {
+ source: {
+ server: Server.HTTPS_LOCAL,
+ treatAsPublic: true,
+ },
+ target: {
+ server: Server.HTTPS_PRIVATE,
+ behavior: {
+ preflight: PreflightBehavior.success(token()),
+ response: ResponseBehavior.allowCrossOrigin(),
+ redirect: preflightUrl({
+ server: Server.HTTPS_LOCAL,
+ behavior: { response: ResponseBehavior.allowCrossOrigin() },
+ }),
+ }
+ },
+ expected: FetchTestResult.SUCCESS,
+}), "treat-as-public to private to local (same-origin): success.");
+
+promise_test(t => fetchTest(t, {
+ source: {
+ server: Server.HTTPS_LOCAL,
+ treatAsPublic: true,
+ },
+ target: {
+ server: Server.HTTPS_PRIVATE,
+ behavior: {
+ redirect: preflightUrl({ server: Server.HTTPS_LOCAL }),
+ }
+ },
+ fetchOptions: { mode: "no-cors" },
+ expected: FetchTestResult.FAILURE,
+}), "treat-as-public to private to local (same-origin): no-cors failed first preflight.");
+
+promise_test(t => fetchTest(t, {
+ source: {
+ server: Server.HTTPS_LOCAL,
+ treatAsPublic: true,
+ },
+ target: {
+ server: Server.HTTPS_PRIVATE,
+ behavior: {
+ preflight: PreflightBehavior.success(token()),
+ redirect: preflightUrl({ server: Server.HTTPS_LOCAL }),
+ }
+ },
+ fetchOptions: { mode: "no-cors" },
+ expected: FetchTestResult.OPAQUE,
+}), "treat-as-public to private to local (same-origin): no-cors success.");
diff --git a/test/wpt/tests/fetch/private-network-access/resources/executor.html b/test/wpt/tests/fetch/private-network-access/resources/executor.html
new file mode 100644
index 0000000..d712129
--- /dev/null
+++ b/test/wpt/tests/fetch/private-network-access/resources/executor.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Executor</title>
+<body></body>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<script>
+ const uuid = new URL(window.location).searchParams.get("executor-uuid");
+ const executor = new Executor(uuid);
+</script>
diff --git a/test/wpt/tests/fetch/private-network-access/resources/fenced-frame-fetcher.https.html b/test/wpt/tests/fetch/private-network-access/resources/fenced-frame-fetcher.https.html
new file mode 100644
index 0000000..b14601d
--- /dev/null
+++ b/test/wpt/tests/fetch/private-network-access/resources/fenced-frame-fetcher.https.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<script src="../../../fenced-frame/resources/utils.js"></script>
+<title>Fetcher</title>
+<script>
+ const url = new URL(location.href).searchParams.get("url");
+ const mode = new URL(location.href).searchParams.get("mode");
+ const method = new URL(location.href).searchParams.get("method");
+ const [error_token, ok_token, body_token, type_token] = parseKeylist();
+
+ fetch(url, {mode: mode, method: method})
+ .then(async function(response) {
+ const body = await response.text();
+ writeValueToServer(ok_token, response.ok);
+ writeValueToServer(body_token, body);
+ writeValueToServer(type_token, response.type);
+ writeValueToServer(error_token, "");
+ })
+ .catch(error => {
+ writeValueToServer(ok_token, "");
+ writeValueToServer(body_token, "");
+ writeValueToServer(type_token, "");
+ writeValueToServer(error_token, error.toString());
+ });
+</script> \ No newline at end of file
diff --git a/test/wpt/tests/fetch/private-network-access/resources/fenced-frame-fetcher.https.html.headers b/test/wpt/tests/fetch/private-network-access/resources/fenced-frame-fetcher.https.html.headers
new file mode 100644
index 0000000..6247f6d
--- /dev/null
+++ b/test/wpt/tests/fetch/private-network-access/resources/fenced-frame-fetcher.https.html.headers
@@ -0,0 +1 @@
+Supports-Loading-Mode: fenced-frame \ No newline at end of file
diff --git a/test/wpt/tests/fetch/private-network-access/resources/fenced-frame-local-network-access-target.https.html b/test/wpt/tests/fetch/private-network-access/resources/fenced-frame-local-network-access-target.https.html
new file mode 100644
index 0000000..2b55e05
--- /dev/null
+++ b/test/wpt/tests/fetch/private-network-access/resources/fenced-frame-local-network-access-target.https.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<script src="../../../fenced-frame/resources/utils.js"></script>
+<title>Fenced frame target</title>
+<script>
+ const [frame_loaded_key] = parseKeylist();
+ writeValueToServer(frame_loaded_key, 'loaded');
+</script>
diff --git a/test/wpt/tests/fetch/private-network-access/resources/fenced-frame-local-network-access.https.html b/test/wpt/tests/fetch/private-network-access/resources/fenced-frame-local-network-access.https.html
new file mode 100644
index 0000000..98f1184
--- /dev/null
+++ b/test/wpt/tests/fetch/private-network-access/resources/fenced-frame-local-network-access.https.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<script src="/resources/testharness.js"></script>
+<script src="../../../fenced-frame/resources/utils.js"></script>
+<script src="/common/utils.js"></script>
+<title>Fenced frame</title>
+<body></body>
+<script>
+(async () => {
+ const target = new URL(location.href).searchParams.get("fenced_frame_url");
+ const urn = await runSelectURL(target);
+ attachFencedFrame(urn);
+})();
+</script>
diff --git a/test/wpt/tests/fetch/private-network-access/resources/fenced-frame-local-network-access.https.html.headers b/test/wpt/tests/fetch/private-network-access/resources/fenced-frame-local-network-access.https.html.headers
new file mode 100644
index 0000000..6247f6d
--- /dev/null
+++ b/test/wpt/tests/fetch/private-network-access/resources/fenced-frame-local-network-access.https.html.headers
@@ -0,0 +1 @@
+Supports-Loading-Mode: fenced-frame \ No newline at end of file
diff --git a/test/wpt/tests/fetch/private-network-access/resources/fetcher.html b/test/wpt/tests/fetch/private-network-access/resources/fetcher.html
new file mode 100644
index 0000000..000a5cc
--- /dev/null
+++ b/test/wpt/tests/fetch/private-network-access/resources/fetcher.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Fetcher</title>
+<script>
+ window.addEventListener("message", function (event) {
+ const { url, options } = event.data;
+ fetch(url, options)
+ .then(async function(response) {
+ const body = await response.text();
+ const message = {
+ ok: response.ok,
+ type: response.type,
+ body: body,
+ };
+ parent.postMessage(message, "*");
+ })
+ .catch(error => {
+ parent.postMessage({ error: error.toString() }, "*");
+ });
+ });
+</script>
diff --git a/test/wpt/tests/fetch/private-network-access/resources/fetcher.js b/test/wpt/tests/fetch/private-network-access/resources/fetcher.js
new file mode 100644
index 0000000..3a18598
--- /dev/null
+++ b/test/wpt/tests/fetch/private-network-access/resources/fetcher.js
@@ -0,0 +1,20 @@
+async function doFetch(url) {
+ const response = await fetch(url);
+ const body = await response.text();
+ return {
+ status: response.status,
+ body,
+ };
+}
+
+async function fetchAndPost(url) {
+ try {
+ const message = await doFetch(url);
+ self.postMessage(message);
+ } catch(e) {
+ self.postMessage({ error: e.name });
+ }
+}
+
+const url = new URL(self.location.href).searchParams.get("url");
+fetchAndPost(url);
diff --git a/test/wpt/tests/fetch/private-network-access/resources/iframed.html b/test/wpt/tests/fetch/private-network-access/resources/iframed.html
new file mode 100644
index 0000000..c889c28
--- /dev/null
+++ b/test/wpt/tests/fetch/private-network-access/resources/iframed.html
@@ -0,0 +1,7 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Iframed</title>
+<script>
+ const uuid = new URL(window.location).searchParams.get("iframe-uuid");
+ top.postMessage({ uuid, message: "loaded" }, "*");
+</script>
diff --git a/test/wpt/tests/fetch/private-network-access/resources/iframer.html b/test/wpt/tests/fetch/private-network-access/resources/iframer.html
new file mode 100644
index 0000000..304cc54
--- /dev/null
+++ b/test/wpt/tests/fetch/private-network-access/resources/iframer.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Iframer</title>
+<body></body>
+<script>
+ const child = document.createElement("iframe");
+ child.src = new URL(window.location).searchParams.get("url");
+ document.body.appendChild(child);
+</script>
diff --git a/test/wpt/tests/fetch/private-network-access/resources/preflight.py b/test/wpt/tests/fetch/private-network-access/resources/preflight.py
new file mode 100644
index 0000000..be3abdb
--- /dev/null
+++ b/test/wpt/tests/fetch/private-network-access/resources/preflight.py
@@ -0,0 +1,175 @@
+# This endpoint responds to both preflight requests and the subsequent requests.
+#
+# Its behavior can be configured with various search/GET parameters, all of
+# which are optional:
+#
+# - treat-as-public-once: Must be a valid UUID if set.
+# If set, then this endpoint expects to receive a non-preflight request first,
+# for which it sets the `Content-Security-Policy: treat-as-public-address`
+# response header. This allows testing "DNS rebinding", where a URL first
+# resolves to the public IP address space, then a non-public IP address space.
+# - preflight-uuid: Must be a valid UUID if set, distinct from the value of the
+# `treat-as-public-once` parameter if both are set.
+# If set, then this endpoint expects to receive a preflight request first
+# followed by a regular request, as in the regular CORS protocol. If the
+# `treat-as-public-once` header is also set, it takes precedence: this
+# endpoint expects to receive a non-preflight request first, then a preflight
+# request, then finally a regular request.
+# If unset, then this endpoint expects to receive no preflight request, only
+# a regular (non-OPTIONS) request.
+# - preflight-headers: Valid values are:
+# - cors: this endpoint responds with valid CORS headers to preflights. These
+# should be sufficient for non-PNA preflight requests to succeed, but not
+# for PNA-specific preflight requests.
+# - cors+pna: this endpoint responds with valid CORS and PNA headers to
+# preflights. These should be sufficient for both non-PNA preflight
+# requests and PNA-specific preflight requests to succeed.
+# - cors+pna+sw: this endpoint responds with valid CORS and PNA headers and
+# "Access-Control-Allow-Headers: Service-Worker" to preflights. These should
+# be sufficient for both non-PNA preflight requests and PNA-specific
+# preflight requests to succeed. This allows the main request to fetch a
+# service worker script.
+# - unspecified, or any other value: this endpoint responds with no CORS or
+# PNA headers. Preflight requests should fail.
+# - final-headers: Valid values are:
+# - cors: this endpoint responds with valid CORS headers to CORS-enabled
+# non-preflight requests. These should be sufficient for non-preflighted
+# CORS-enabled requests to succeed.
+# - unspecified: this endpoint responds with no CORS headers to non-preflight
+# requests. This should fail CORS-enabled requests, but be sufficient for
+# no-CORS requests.
+#
+# The following parameters only affect non-preflight responses:
+#
+# - redirect: If set, the response code is set to 301 and the `Location`
+# response header is set to this value.
+# - mime-type: If set, the `Content-Type` response header is set to this value.
+# - file: Specifies a path (relative to this file's directory) to a file. If
+# set, the response body is copied from this file.
+# - random-js-prefix: If set to any value, the response body is prefixed with
+# a Javascript comment line containing a random value. This is useful in
+# service worker tests, since service workers are only updated if the new
+# script is not byte-for-byte identical with the old script.
+# - body: If set and `file` is not, the response body is set to this value.
+#
+
+import os
+import random
+
+from wptserve.utils import isomorphic_encode
+
+_ACAO = ("Access-Control-Allow-Origin", "*")
+_ACAPN = ("Access-Control-Allow-Private-Network", "true")
+_ACAH = ("Access-Control-Allow-Headers", "Service-Worker")
+
+def _get_response_headers(method, mode):
+ acam = ("Access-Control-Allow-Methods", method)
+
+ if mode == b"cors":
+ return [acam, _ACAO]
+
+ if mode == b"cors+pna":
+ return [acam, _ACAO, _ACAPN]
+
+ if mode == b"cors+pna+sw":
+ return [acam, _ACAO, _ACAPN, _ACAH]
+
+ return []
+
+def _get_expect_single_preflight(request):
+ return request.GET.get(b"expect-single-preflight")
+
+def _is_preflight_optional(request):
+ return request.GET.get(b"is-preflight-optional")
+
+def _get_preflight_uuid(request):
+ return request.GET.get(b"preflight-uuid")
+
+def _is_loaded_in_fenced_frame(request):
+ return request.GET.get(b"is-loaded-in-fenced-frame")
+
+def _should_treat_as_public_once(request):
+ uuid = request.GET.get(b"treat-as-public-once")
+ if uuid is None:
+ # If the search parameter is not given, never treat as public.
+ return False
+
+ # If the parameter is given, we treat the request as public only if the UUID
+ # has never been seen and stashed.
+ result = request.server.stash.take(uuid) is None
+ request.server.stash.put(uuid, "")
+ return result
+
+def _handle_preflight_request(request, response):
+ if _should_treat_as_public_once(request):
+ return (400, [], "received preflight for first treat-as-public request")
+
+ uuid = _get_preflight_uuid(request)
+ if uuid is None:
+ return (400, [], "missing `preflight-uuid` param from preflight URL")
+
+ value = request.server.stash.take(uuid)
+ request.server.stash.put(uuid, "preflight")
+ if _get_expect_single_preflight(request) and value is not None:
+ return (400, [], "received duplicated preflight")
+
+ method = request.headers.get("Access-Control-Request-Method")
+ mode = request.GET.get(b"preflight-headers")
+ headers = _get_response_headers(method, mode)
+
+ return (headers, "preflight")
+
+def _final_response_body(request):
+ file_name = request.GET.get(b"file")
+ if file_name is None:
+ return request.GET.get(b"body") or "success"
+
+ prefix = b""
+ if request.GET.get(b"random-js-prefix"):
+ value = random.randint(0, 1000000000)
+ prefix = isomorphic_encode("// Random value: {}\n\n".format(value))
+
+ path = os.path.join(os.path.dirname(isomorphic_encode(__file__)), file_name)
+ with open(path, 'rb') as f:
+ contents = f.read()
+
+ return prefix + contents
+
+def _handle_final_request(request, response):
+ if _should_treat_as_public_once(request):
+ headers = [("Content-Security-Policy", "treat-as-public-address"),]
+ else:
+ uuid = _get_preflight_uuid(request)
+ if uuid is not None:
+ if (request.server.stash.take(uuid) is None and
+ not _is_preflight_optional(request)):
+ return (405, [], "no preflight received")
+ request.server.stash.put(uuid, "final")
+
+ mode = request.GET.get(b"final-headers")
+ headers = _get_response_headers(request.method, mode)
+
+ redirect = request.GET.get(b"redirect")
+ if redirect is not None:
+ headers.append(("Location", redirect))
+ return (301, headers, b"")
+
+ mime_type = request.GET.get(b"mime-type")
+ if mime_type is not None:
+ headers.append(("Content-Type", mime_type),)
+
+ if _is_loaded_in_fenced_frame(request):
+ headers.append(("Supports-Loading-Mode", "fenced-frame"))
+
+ body = _final_response_body(request)
+ return (headers, body)
+
+def main(request, response):
+ try:
+ if request.method == "OPTIONS":
+ return _handle_preflight_request(request, response)
+ else:
+ return _handle_final_request(request, response)
+ except BaseException as e:
+ # Surface exceptions to the client, where they show up as assertion errors.
+ return (500, [("X-exception", str(e))], "exception: {}".format(e))
diff --git a/test/wpt/tests/fetch/private-network-access/resources/service-worker-bridge.html b/test/wpt/tests/fetch/private-network-access/resources/service-worker-bridge.html
new file mode 100644
index 0000000..816de53
--- /dev/null
+++ b/test/wpt/tests/fetch/private-network-access/resources/service-worker-bridge.html
@@ -0,0 +1,155 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>ServiceWorker Bridge</title>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script>
+ // This bridge document exists to perform service worker commands on behalf
+ // of a test page. It lives within the same scope (including origin) as the
+ // service worker script, allowing it to be controlled by the service worker.
+
+ async function register({ url, options }) {
+ await navigator.serviceWorker.register(url, options);
+ return { loaded: true };
+ }
+
+ async function unregister({ scope }) {
+ const registration = await navigator.serviceWorker.getRegistration(scope);
+ if (!registration) {
+ return { unregistered: false, error: "no registration" };
+ }
+
+ const unregistered = await registration.unregister();
+ return { unregistered };
+ }
+
+ async function update({ scope }) {
+ const registration = await navigator.serviceWorker.getRegistration(scope);
+ if (!registration) {
+ return { updated: false, error: "no registration" };
+ }
+
+ const newRegistration = await registration.update();
+ return { updated: true };
+ }
+
+ // Total number of `controllerchange` events since document creation.
+ let totalNumControllerChanges = 0;
+ navigator.serviceWorker.addEventListener("controllerchange", () => {
+ totalNumControllerChanges++;
+ });
+
+ // Using `navigator.serviceWorker.ready` does not allow noticing new
+ // controllers after an update, so we count `controllerchange` events instead.
+ // This has the added benefit of ensuring that subsequent fetches are handled
+ // by the service worker, whereas `ready` does not guarantee that.
+ async function wait({ numControllerChanges }) {
+ if (totalNumControllerChanges >= numControllerChanges) {
+ return {
+ controlled: !!navigator.serviceWorker.controller,
+ numControllerChanges: totalNumControllerChanges,
+ };
+ }
+
+ let remaining = numControllerChanges - totalNumControllerChanges;
+ await new Promise((resolve) => {
+ navigator.serviceWorker.addEventListener("controllerchange", () => {
+ remaining--;
+ if (remaining == 0) {
+ resolve();
+ }
+ });
+ });
+
+ return {
+ controlled: !!navigator.serviceWorker.controller,
+ numControllerChanges,
+ };
+ }
+
+ async function doFetch({ url, options }) {
+ const response = await fetch(url, options);
+ const body = await response.text();
+ return {
+ ok: response.ok,
+ body,
+ };
+ }
+
+ async function setPermission({ name, state }) {
+ await test_driver.set_permission({ name }, state);
+
+ // Double-check, just to be sure.
+ // See the comment in `../service-worker-background-fetch.js`.
+ const permissionStatus = await navigator.permissions.query({ name });
+ return { state: permissionStatus.state };
+ }
+
+ async function backgroundFetch({ scope, url }) {
+ const registration = await navigator.serviceWorker.getRegistration(scope);
+ if (!registration) {
+ return { error: "no registration" };
+ }
+
+ const fetchRegistration =
+ await registration.backgroundFetch.fetch("test", url);
+ const resultReady = new Promise((resolve) => {
+ fetchRegistration.addEventListener("progress", () => {
+ if (fetchRegistration.result) {
+ resolve();
+ }
+ });
+ });
+
+ let ok;
+ let body;
+ const record = await fetchRegistration.match(url);
+ if (record) {
+ const response = await record.responseReady;
+ body = await response.text();
+ ok = response.ok;
+ }
+
+ // Wait for the result after getting the response. If the steps are
+ // inverted, then sometimes the response is not found due to an
+ // `UnknownError`.
+ await resultReady;
+
+ return {
+ result: fetchRegistration.result,
+ failureReason: fetchRegistration.failureReason,
+ ok,
+ body,
+ };
+ }
+
+ function getAction(action) {
+ switch (action) {
+ case "register":
+ return register;
+ case "unregister":
+ return unregister;
+ case "wait":
+ return wait;
+ case "update":
+ return update;
+ case "fetch":
+ return doFetch;
+ case "set-permission":
+ return setPermission;
+ case "background-fetch":
+ return backgroundFetch;
+ }
+ }
+
+ window.addEventListener("message", async (evt) => {
+ let message;
+ try {
+ const action = getAction(evt.data.action);
+ message = await action(evt.data);
+ } catch(e) {
+ message = { error: e.name };
+ }
+ parent.postMessage(message, "*");
+ });
+</script>
diff --git a/test/wpt/tests/fetch/private-network-access/resources/service-worker.js b/test/wpt/tests/fetch/private-network-access/resources/service-worker.js
new file mode 100644
index 0000000..bca71ad
--- /dev/null
+++ b/test/wpt/tests/fetch/private-network-access/resources/service-worker.js
@@ -0,0 +1,18 @@
+self.addEventListener("install", () => {
+ // Skip waiting before replacing the previously-active service worker, if any.
+ // This allows the bridge script to notice the controller change and query
+ // the install time via fetch.
+ self.skipWaiting();
+});
+
+self.addEventListener("activate", (event) => {
+ // Claim all clients so that the bridge script notices the activation.
+ event.waitUntil(self.clients.claim());
+});
+
+self.addEventListener("fetch", (event) => {
+ const url = new URL(event.request.url).searchParams.get("proxied-url");
+ if (url) {
+ event.respondWith(fetch(url));
+ }
+});
diff --git a/test/wpt/tests/fetch/private-network-access/resources/shared-fetcher.js b/test/wpt/tests/fetch/private-network-access/resources/shared-fetcher.js
new file mode 100644
index 0000000..30bde1e
--- /dev/null
+++ b/test/wpt/tests/fetch/private-network-access/resources/shared-fetcher.js
@@ -0,0 +1,23 @@
+async function doFetch(url) {
+ const response = await fetch(url);
+ const body = await response.text();
+ return {
+ status: response.status,
+ body,
+ };
+}
+
+async function fetchAndPost(url, port) {
+ try {
+ const message = await doFetch(url);
+ port.postMessage(message);
+ } catch(e) {
+ port.postMessage({ error: e.name });
+ }
+}
+
+const url = new URL(self.location.href).searchParams.get("url");
+
+self.addEventListener("connect", async (evt) => {
+ await fetchAndPost(url, evt.ports[0]);
+});
diff --git a/test/wpt/tests/fetch/private-network-access/resources/shared-worker-blob-fetcher.html b/test/wpt/tests/fetch/private-network-access/resources/shared-worker-blob-fetcher.html
new file mode 100644
index 0000000..a79869b
--- /dev/null
+++ b/test/wpt/tests/fetch/private-network-access/resources/shared-worker-blob-fetcher.html
@@ -0,0 +1,50 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>SharedWorker Blob Fetcher</title>
+<script>
+ window.addEventListener("message", function (evt) {
+ let { url } = evt.data;
+
+ const workerScriptContent = `
+ async function doFetch(url) {
+ const response = await fetch(url);
+ const body = await response.text();
+ return {
+ status: response.status,
+ body,
+ };
+ }
+
+ async function fetchAndPost(url, port) {
+ try {
+ const message = await doFetch(url);
+ port.postMessage(message);
+ } catch(e) {
+ port.postMessage({ error: e.name });
+ }
+ }
+
+ const url = "${url}";
+
+ self.addEventListener("connect", async (evt) => {
+ await fetchAndPost(url, evt.ports[0]);
+ });
+ `;
+ const blob =
+ new Blob([workerScriptContent], {type: 'application/javascript'});
+ const workerScriptUrl = URL.createObjectURL(blob);
+
+ const worker = new SharedWorker(workerScriptUrl);
+
+ URL.revokeObjectURL(workerScriptUrl);
+
+ worker.onerror = (evt) => {
+ parent.postMessage({ error: evt.message || "unknown error" }, "*");
+ };
+
+ worker.port.addEventListener("message", (evt) => {
+ parent.postMessage(evt.data, "*");
+ });
+ worker.port.start();
+ });
+</script>
diff --git a/test/wpt/tests/fetch/private-network-access/resources/shared-worker-fetcher.html b/test/wpt/tests/fetch/private-network-access/resources/shared-worker-fetcher.html
new file mode 100644
index 0000000..4af4b1f
--- /dev/null
+++ b/test/wpt/tests/fetch/private-network-access/resources/shared-worker-fetcher.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>SharedWorker Fetcher</title>
+<script>
+ window.addEventListener("message", function (evt) {
+ let { url } = evt.data;
+
+ const worker = new SharedWorker(url);
+
+ worker.onerror = (evt) => {
+ parent.postMessage({ error: evt.message || "unknown error" }, "*");
+ };
+
+ worker.port.addEventListener("message", (evt) => {
+ parent.postMessage(evt.data, "*");
+ });
+ worker.port.start();
+ });
+</script>
diff --git a/test/wpt/tests/fetch/private-network-access/resources/socket-opener.html b/test/wpt/tests/fetch/private-network-access/resources/socket-opener.html
new file mode 100644
index 0000000..48d2721
--- /dev/null
+++ b/test/wpt/tests/fetch/private-network-access/resources/socket-opener.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>WebSocket Opener</title>
+<script>
+ window.addEventListener("message", function (event) {
+ const socket = new WebSocket(event.data);
+
+ socket.onopen = () => {
+ parent.postMessage("open", "*");
+ };
+ socket.onclose = (evt) => {
+ parent.postMessage(`close: code ${evt.code}`, "*");
+ };
+ });
+</script>
diff --git a/test/wpt/tests/fetch/private-network-access/resources/support.sub.js b/test/wpt/tests/fetch/private-network-access/resources/support.sub.js
new file mode 100644
index 0000000..27d733d
--- /dev/null
+++ b/test/wpt/tests/fetch/private-network-access/resources/support.sub.js
@@ -0,0 +1,759 @@
+// Creates a new iframe in `doc`, calls `func` on it and appends it as a child
+// of `doc`.
+// Returns a promise that resolves to the iframe once loaded (successfully or
+// not).
+// The iframe is removed from `doc` once test `t` is done running.
+//
+// NOTE: There exists no interoperable way to check whether an iframe failed to
+// load, so this should only be used when the iframe is expected to load. It
+// also means we cannot wire the iframe's `error` event to a promise
+// rejection. See: https://github.com/whatwg/html/issues/125
+function appendIframeWith(t, doc, func) {
+ return new Promise(resolve => {
+ const child = doc.createElement("iframe");
+ t.add_cleanup(() => child.remove());
+
+ child.addEventListener("load", () => resolve(child), { once: true });
+ func(child);
+ doc.body.appendChild(child);
+ });
+}
+
+// Appends a child iframe to `doc` sourced from `src`.
+//
+// See `appendIframeWith()` for more details.
+function appendIframe(t, doc, src) {
+ return appendIframeWith(t, doc, child => { child.src = src; });
+}
+
+// Registers an event listener that will resolve this promise when this
+// window receives a message posted to it.
+//
+// `options` has the following shape:
+//
+// {
+// source: If specified, this function waits for the first message from the
+// given source only, ignoring other messages.
+//
+// filter: If specified, this function calls `filter` on each incoming
+// message, and resolves iff it returns true.
+// }
+//
+function futureMessage(options) {
+ return new Promise(resolve => {
+ window.addEventListener("message", (e) => {
+ if (options?.source && options.source !== e.source) {
+ return;
+ }
+
+ if (options?.filter && !options.filter(e.data)) {
+ return;
+ }
+
+ resolve(e.data);
+ });
+ });
+};
+
+// Like `promise_test()`, but executes tests in parallel like `async_test()`.
+//
+// Cribbed from COEP tests.
+function promise_test_parallel(promise, description) {
+ async_test(test => {
+ promise(test)
+ .then(() => test.done())
+ .catch(test.step_func(error => { throw error; }));
+ }, description);
+};
+
+async function postMessageAndAwaitReply(target, message) {
+ const reply = futureMessage({ source: target });
+ target.postMessage(message, "*");
+ return await reply;
+}
+
+// Maps protocol (without the trailing colon) and address space to port.
+const SERVER_PORTS = {
+ "http": {
+ "local": {{ports[http][0]}},
+ "private": {{ports[http-private][0]}},
+ "public": {{ports[http-public][0]}},
+ },
+ "https": {
+ "local": {{ports[https][0]}},
+ "other-local": {{ports[https][1]}},
+ "private": {{ports[https-private][0]}},
+ "public": {{ports[https-public][0]}},
+ },
+ "ws": {
+ "local": {{ports[ws][0]}},
+ },
+ "wss": {
+ "local": {{ports[wss][0]}},
+ },
+};
+
+// A `Server` is a web server accessible by tests. It has the following shape:
+//
+// {
+// addressSpace: the IP address space of the server ("local", "private" or
+// "public"),
+// name: a human-readable name for the server,
+// port: the port on which the server listens for connections,
+// protocol: the protocol (including trailing colon) spoken by the server,
+// }
+//
+// Constants below define the available servers, which can also be accessed
+// programmatically with `get()`.
+class Server {
+ // Maps the given `protocol` (without a trailing colon) and `addressSpace` to
+ // a server. Returns null if no such server exists.
+ static get(protocol, addressSpace) {
+ const ports = SERVER_PORTS[protocol];
+ if (ports === undefined) {
+ return null;
+ }
+
+ const port = ports[addressSpace];
+ if (port === undefined) {
+ return null;
+ }
+
+ return {
+ addressSpace,
+ name: `${protocol}-${addressSpace}`,
+ port,
+ protocol: protocol + ':',
+ };
+ }
+
+ static HTTP_LOCAL = Server.get("http", "local");
+ static HTTP_PRIVATE = Server.get("http", "private");
+ static HTTP_PUBLIC = Server.get("http", "public");
+ static HTTPS_LOCAL = Server.get("https", "local");
+ static OTHER_HTTPS_LOCAL = Server.get("https", "other-local");
+ static HTTPS_PRIVATE = Server.get("https", "private");
+ static HTTPS_PUBLIC = Server.get("https", "public");
+ static WS_LOCAL = Server.get("ws", "local");
+ static WSS_LOCAL = Server.get("wss", "local");
+};
+
+// Resolves a URL relative to the current location, returning an absolute URL.
+//
+// `url` specifies the relative URL, e.g. "foo.html" or "http://foo.example".
+// `options`, if defined, should have the following shape:
+//
+// {
+// // Optional. Overrides the protocol of the returned URL.
+// protocol,
+//
+// // Optional. Overrides the port of the returned URL.
+// port,
+//
+// // Extra headers.
+// headers,
+//
+// // Extra search params.
+// searchParams,
+// }
+//
+function resolveUrl(url, options) {
+ const result = new URL(url, window.location);
+ if (options === undefined) {
+ return result;
+ }
+
+ const { port, protocol, headers, searchParams } = options;
+ if (port !== undefined) {
+ result.port = port;
+ }
+ if (protocol !== undefined) {
+ result.protocol = protocol;
+ }
+ if (headers !== undefined) {
+ const pipes = [];
+ for (key in headers) {
+ pipes.push(`header(${key},${headers[key]})`);
+ }
+ result.searchParams.append("pipe", pipes.join("|"));
+ }
+ if (searchParams !== undefined) {
+ for (key in searchParams) {
+ result.searchParams.append(key, searchParams[key]);
+ }
+ }
+
+ return result;
+}
+
+// Computes options to pass to `resolveUrl()` for a source document's URL.
+//
+// `server` identifies the server from which to load the document.
+// `treatAsPublic`, if set to true, specifies that the source document should
+// be artificially placed in the `public` address space using CSP.
+function sourceResolveOptions({ server, treatAsPublic }) {
+ const options = {...server};
+ if (treatAsPublic) {
+ options.headers = { "Content-Security-Policy": "treat-as-public-address" };
+ }
+ return options;
+}
+
+// Computes the URL of a preflight handler configured with the given options.
+//
+// `server` identifies the server from which to load the resource.
+// `behavior` specifies the behavior of the target server. It may contain:
+// - `preflight`: The result of calling one of `PreflightBehavior`'s methods.
+// - `response`: The result of calling one of `ResponseBehavior`'s methods.
+// - `redirect`: A URL to which the target should redirect GET requests.
+function preflightUrl({ server, behavior }) {
+ assert_not_equals(server, undefined, 'server');
+ const options = {...server};
+ if (behavior) {
+ const { preflight, response, redirect } = behavior;
+ options.searchParams = {
+ ...preflight,
+ ...response,
+ };
+ if (redirect !== undefined) {
+ options.searchParams.redirect = redirect;
+ }
+ }
+
+ return resolveUrl("resources/preflight.py", options);
+}
+
+// Methods generate behavior specifications for how `resources/preflight.py`
+// should behave upon receiving a preflight request.
+const PreflightBehavior = {
+ // The preflight response should fail with a non-2xx code.
+ failure: () => ({}),
+
+ // The preflight response should be missing CORS headers.
+ // `uuid` should be a UUID that uniquely identifies the preflight request.
+ noCorsHeader: (uuid) => ({
+ "preflight-uuid": uuid,
+ }),
+
+ // The preflight response should be missing PNA headers.
+ // `uuid` should be a UUID that uniquely identifies the preflight request.
+ noPnaHeader: (uuid) => ({
+ "preflight-uuid": uuid,
+ "preflight-headers": "cors",
+ }),
+
+ // The preflight response should succeed.
+ // `uuid` should be a UUID that uniquely identifies the preflight request.
+ success: (uuid) => ({
+ "preflight-uuid": uuid,
+ "preflight-headers": "cors+pna",
+ }),
+
+ optionalSuccess: (uuid) => ({
+ "preflight-uuid": uuid,
+ "preflight-headers": "cors+pna",
+ "is-preflight-optional": true,
+ }),
+
+ // The preflight response should succeed and allow service-worker header.
+ // `uuid` should be a UUID that uniquely identifies the preflight request.
+ serviceWorkerSuccess: (uuid) => ({
+ "preflight-uuid": uuid,
+ "preflight-headers": "cors+pna+sw",
+ }),
+
+ // The preflight response should succeed only if it is the first preflight.
+ // `uuid` should be a UUID that uniquely identifies the preflight request.
+ singlePreflight: (uuid) => ({
+ "preflight-uuid": uuid,
+ "preflight-headers": "cors+pna",
+ "expect-single-preflight": true,
+ }),
+};
+
+// Methods generate behavior specifications for how `resources/preflight.py`
+// should behave upon receiving a regular (non-preflight) request.
+const ResponseBehavior = {
+ // The response should succeed without CORS headers.
+ default: () => ({}),
+
+ // The response should succeed with CORS headers.
+ allowCrossOrigin: () => ({ "final-headers": "cors" }),
+};
+
+const FetchTestResult = {
+ SUCCESS: {
+ ok: true,
+ body: "success",
+ },
+ OPAQUE: {
+ ok: false,
+ type: "opaque",
+ body: "",
+ },
+ FAILURE: {
+ error: "TypeError: Failed to fetch",
+ },
+};
+
+// Runs a fetch test. Tries to fetch a given subresource from a given document.
+//
+// Main argument shape:
+//
+// {
+// // Optional. Passed to `sourceResolveOptions()`.
+// source,
+//
+// // Optional. Passed to `preflightUrl()`.
+// target,
+//
+// // Optional. Passed to `fetch()`.
+// fetchOptions,
+//
+// // Required. One of the values in `FetchTestResult`.
+// expected,
+// }
+//
+async function fetchTest(t, { source, target, fetchOptions, expected }) {
+ const sourceUrl =
+ resolveUrl("resources/fetcher.html", sourceResolveOptions(source));
+
+ const targetUrl = preflightUrl(target);
+
+ const iframe = await appendIframe(t, document, sourceUrl);
+ const reply = futureMessage({ source: iframe.contentWindow });
+
+ const message = {
+ url: targetUrl.href,
+ options: fetchOptions,
+ };
+ iframe.contentWindow.postMessage(message, "*");
+
+ const { error, ok, type, body } = await reply;
+
+ assert_equals(error, expected.error, "error");
+
+ assert_equals(ok, expected.ok, "response ok");
+ assert_equals(body, expected.body, "response body");
+
+ if (expected.type !== undefined) {
+ assert_equals(type, expected.type, "response type");
+ }
+}
+
+// Similar to `fetchTest`, but replaced iframes with fenced frames.
+async function fencedFrameFetchTest(t, { source, target, fetchOptions, expected }) {
+ const fetcher_url =
+ resolveUrl("resources/fenced-frame-fetcher.https.html", sourceResolveOptions(source));
+
+ const target_url = preflightUrl(target);
+ target_url.searchParams.set("is-loaded-in-fenced-frame", true);
+
+ fetcher_url.searchParams.set("mode", fetchOptions.mode);
+ fetcher_url.searchParams.set("method", fetchOptions.method);
+ fetcher_url.searchParams.set("url", target_url);
+
+ const error_token = token();
+ const ok_token = token();
+ const body_token = token();
+ const type_token = token();
+ const source_url = generateURL(fetcher_url, [error_token, ok_token, body_token, type_token]);
+
+ const urn = await generateURNFromFledge(source_url, []);
+ attachFencedFrame(urn);
+
+ const error = await nextValueFromServer(error_token);
+ const ok = await nextValueFromServer(ok_token);
+ const body = await nextValueFromServer(body_token);
+ const type = await nextValueFromServer(type_token);
+
+ assert_equals(error, expected.error || "" , "error");
+ assert_equals(body, expected.body || "", "response body");
+ assert_equals(ok, expected.ok !== undefined ? expected.ok.toString() : "", "response ok");
+ if (expected.type !== undefined) {
+ assert_equals(type, expected.type, "response type");
+ }
+}
+
+const XhrTestResult = {
+ SUCCESS: {
+ loaded: true,
+ status: 200,
+ body: "success",
+ },
+ FAILURE: {
+ loaded: false,
+ status: 0,
+ },
+};
+
+// Runs an XHR test. Tries to fetch a given subresource from a given document.
+//
+// Main argument shape:
+//
+// {
+// // Optional. Passed to `sourceResolveOptions()`.
+// source,
+//
+// // Optional. Passed to `preflightUrl()`.
+// target,
+//
+// // Optional. Method to use when sending the request. Defaults to "GET".
+// method,
+//
+// // Required. One of the values in `XhrTestResult`.
+// expected,
+// }
+//
+async function xhrTest(t, { source, target, method, expected }) {
+ const sourceUrl =
+ resolveUrl("resources/xhr-sender.html", sourceResolveOptions(source));
+
+ const targetUrl = preflightUrl(target);
+
+ const iframe = await appendIframe(t, document, sourceUrl);
+ const reply = futureMessage();
+
+ const message = {
+ url: targetUrl.href,
+ method: method,
+ };
+ iframe.contentWindow.postMessage(message, "*");
+
+ const { loaded, status, body } = await reply;
+
+ assert_equals(loaded, expected.loaded, "response loaded");
+ assert_equals(status, expected.status, "response status");
+ assert_equals(body, expected.body, "response body");
+}
+
+const FrameTestResult = {
+ SUCCESS: "loaded",
+ FAILURE: "timeout",
+};
+
+async function iframeTest(t, { source, target, expected }) {
+ // Allows running tests in parallel.
+ const uuid = token();
+
+ const targetUrl = preflightUrl(target);
+ targetUrl.searchParams.set("file", "iframed.html");
+ targetUrl.searchParams.set("iframe-uuid", uuid);
+
+ const sourceUrl =
+ resolveUrl("resources/iframer.html", sourceResolveOptions(source));
+ sourceUrl.searchParams.set("url", targetUrl);
+
+ const messagePromise = futureMessage({
+ filter: (data) => data.uuid === uuid,
+ });
+ const iframe = await appendIframe(t, document, sourceUrl);
+
+ // The grandchild frame posts a message iff it loads successfully.
+ // There exists no interoperable way to check whether an iframe failed to
+ // load, so we use a timeout.
+ // See: https://github.com/whatwg/html/issues/125
+ const result = await Promise.race([
+ messagePromise.then((data) => data.message),
+ new Promise((resolve) => {
+ t.step_timeout(() => resolve("timeout"), 500 /* ms */);
+ }),
+ ]);
+
+ assert_equals(result, expected);
+}
+
+// Similar to `iframeTest`, but replaced iframes with fenced frames.
+async function fencedFrameTest(t, { source, target, expected }) {
+ // Allows running tests in parallel.
+ const target_url = preflightUrl(target);
+ target_url.searchParams.set("file", "fenced-frame-local-network-access-target.https.html");
+ target_url.searchParams.set("is-loaded-in-fenced-frame", true);
+
+ const frame_loaded_key = token();
+ const child_frame_target = generateURL(target_url, [frame_loaded_key]);
+
+ const source_url =
+ resolveUrl("resources/fenced-frame-local-network-access.https.html", sourceResolveOptions(source));
+ source_url.searchParams.set("fenced_frame_url", child_frame_target);
+
+ const urn = await generateURNFromFledge(source_url, []);
+ attachFencedFrame(urn);
+
+ // The grandchild fenced frame writes a value to the server iff it loads
+ // successfully.
+ const result = (expected == FrameTestResult.SUCCESS) ?
+ await nextValueFromServer(frame_loaded_key) :
+ await Promise.race([
+ nextValueFromServer(frame_loaded_key),
+ new Promise((resolve) => {
+ t.step_timeout(() => resolve("timeout"), 10000 /* ms */);
+ }),
+ ]);
+
+ assert_equals(result, expected);
+}
+
+const iframeGrandparentTest = ({
+ name,
+ grandparentServer,
+ child,
+ grandchild,
+ expected,
+}) => promise_test_parallel(async (t) => {
+ // Allows running tests in parallel.
+ const grandparentUuid = token();
+ const childUuid = token();
+ const grandchildUuid = token();
+
+ const grandparentUrl =
+ resolveUrl("resources/executor.html", grandparentServer);
+ grandparentUrl.searchParams.set("executor-uuid", grandparentUuid);
+
+ const childUrl = preflightUrl(child);
+ childUrl.searchParams.set("file", "executor.html");
+ childUrl.searchParams.set("executor-uuid", childUuid);
+
+ const grandchildUrl = preflightUrl(grandchild);
+ grandchildUrl.searchParams.set("file", "iframed.html");
+ grandchildUrl.searchParams.set("iframe-uuid", grandchildUuid);
+
+ const iframe = await appendIframe(t, document, grandparentUrl);
+
+ const addChild = (url) => new Promise((resolve) => {
+ const child = document.createElement("iframe");
+ child.src = url;
+ child.addEventListener("load", () => resolve(), { once: true });
+ document.body.appendChild(child);
+ });
+
+ const grandparentCtx = new RemoteContext(grandparentUuid);
+ await grandparentCtx.execute_script(addChild, [childUrl]);
+
+ // Add a blank grandchild frame inside the child.
+ // Apply a timeout to this step so that failures at this step do not block the
+ // execution of other tests.
+ const childCtx = new RemoteContext(childUuid);
+ await Promise.race([
+ childCtx.execute_script(addChild, ["about:blank"]),
+ new Promise((resolve, reject) => t.step_timeout(
+ () => reject("timeout adding grandchild"),
+ 2000 /* ms */
+ )),
+ ]);
+
+ const messagePromise = futureMessage({
+ filter: (data) => data.uuid === grandchildUuid,
+ });
+ await grandparentCtx.execute_script((url) => {
+ const child = window.frames[0];
+ const grandchild = child.frames[0];
+ grandchild.location = url;
+ }, [grandchildUrl]);
+
+ // The great-grandchild frame posts a message iff it loads successfully.
+ // There exists no interoperable way to check whether an iframe failed to
+ // load, so we use a timeout.
+ // See: https://github.com/whatwg/html/issues/125
+ const result = await Promise.race([
+ messagePromise.then((data) => data.message),
+ new Promise((resolve) => {
+ t.step_timeout(() => resolve("timeout"), 2000 /* ms */);
+ }),
+ ]);
+
+ assert_equals(result, expected);
+}, name);
+
+const WebsocketTestResult = {
+ SUCCESS: "open",
+
+ // The code is a best guess. It is not yet entirely specified, so it may need
+ // to be changed in the future based on implementation experience.
+ FAILURE: "close: code 1006",
+};
+
+// Runs a websocket test. Attempts to open a websocket from `source` (in an
+// iframe) to `target`, then checks that the result is as `expected`.
+//
+// Argument shape:
+//
+// {
+// // Required. Passed to `sourceResolveOptions()`.
+// source,
+//
+// // Required.
+// target: {
+// // Required. Target server.
+// server,
+// }
+//
+// // Required. Should be one of the values in `WebsocketTestResult`.
+// expected,
+// }
+//
+async function websocketTest(t, { source, target, expected }) {
+ const sourceUrl =
+ resolveUrl("resources/socket-opener.html", sourceResolveOptions(source));
+
+ const targetUrl = resolveUrl("/echo", target.server);
+
+ const iframe = await appendIframe(t, document, sourceUrl);
+
+ const reply = futureMessage();
+ iframe.contentWindow.postMessage(targetUrl.href, "*");
+
+ assert_equals(await reply, expected);
+}
+
+const WorkerScriptTestResult = {
+ SUCCESS: { loaded: true },
+ FAILURE: { error: "unknown error" },
+};
+
+function workerScriptUrl(target) {
+ const url = preflightUrl(target);
+
+ url.searchParams.append("body", "postMessage({ loaded: true })")
+ url.searchParams.append("mime-type", "application/javascript")
+
+ return url;
+}
+
+async function workerScriptTest(t, { source, target, expected }) {
+ const sourceUrl =
+ resolveUrl("resources/worker-fetcher.html", sourceResolveOptions(source));
+
+ const targetUrl = workerScriptUrl(target);
+
+ const iframe = await appendIframe(t, document, sourceUrl);
+ const reply = futureMessage();
+
+ iframe.contentWindow.postMessage({ url: targetUrl.href }, "*");
+
+ const { error, loaded } = await reply;
+
+ assert_equals(error, expected.error, "worker error");
+ assert_equals(loaded, expected.loaded, "response loaded");
+}
+
+async function nestedWorkerScriptTest(t, { source, target, expected }) {
+ const targetUrl = workerScriptUrl(target);
+
+ const sourceUrl = resolveUrl(
+ "resources/worker-fetcher.js", sourceResolveOptions(source));
+ sourceUrl.searchParams.append("url", targetUrl);
+
+ // Iframe must be same-origin with the parent worker.
+ const iframeUrl = new URL("worker-fetcher.html", sourceUrl);
+
+ const iframe = await appendIframe(t, document, iframeUrl);
+ const reply = futureMessage();
+
+ iframe.contentWindow.postMessage({ url: sourceUrl.href }, "*");
+
+ const { error, loaded } = await reply;
+
+ assert_equals(error, expected.error, "worker error");
+ assert_equals(loaded, expected.loaded, "response loaded");
+}
+
+async function sharedWorkerScriptTest(t, { source, target, expected }) {
+ const sourceUrl = resolveUrl("resources/shared-worker-fetcher.html",
+ sourceResolveOptions(source));
+ const targetUrl = preflightUrl(target);
+ targetUrl.searchParams.append(
+ "body", "onconnect = (e) => e.ports[0].postMessage({ loaded: true })")
+ targetUrl.searchParams.append("mime-type", "application/javascript")
+
+ const iframe = await appendIframe(t, document, sourceUrl);
+ const reply = futureMessage();
+
+ iframe.contentWindow.postMessage({ url: targetUrl.href }, "*");
+
+ const { error, loaded } = await reply;
+
+ assert_equals(error, expected.error, "worker error");
+ assert_equals(loaded, expected.loaded, "response loaded");
+}
+
+// Results that may be expected in tests.
+const WorkerFetchTestResult = {
+ SUCCESS: { status: 200, body: "success" },
+ FAILURE: { error: "TypeError" },
+};
+
+async function workerFetchTest(t, { source, target, expected }) {
+ const targetUrl = preflightUrl(target);
+
+ const sourceUrl =
+ resolveUrl("resources/fetcher.js", sourceResolveOptions(source));
+ sourceUrl.searchParams.append("url", targetUrl.href);
+
+ const fetcherUrl = new URL("worker-fetcher.html", sourceUrl);
+
+ const reply = futureMessage();
+ const iframe = await appendIframe(t, document, fetcherUrl);
+
+ iframe.contentWindow.postMessage({ url: sourceUrl.href }, "*");
+
+ const { error, status, body } = await reply;
+ assert_equals(error, expected.error, "fetch error");
+ assert_equals(status, expected.status, "response status");
+ assert_equals(body, expected.body, "response body");
+}
+
+async function workerBlobFetchTest(t, { source, target, expected }) {
+ const targetUrl = preflightUrl(target);
+
+ const fetcherUrl = resolveUrl(
+ 'resources/worker-blob-fetcher.html', sourceResolveOptions(source));
+
+ const reply = futureMessage();
+ const iframe = await appendIframe(t, document, fetcherUrl);
+
+ iframe.contentWindow.postMessage({ url: targetUrl.href }, "*");
+
+ const { error, status, body } = await reply;
+ assert_equals(error, expected.error, "fetch error");
+ assert_equals(status, expected.status, "response status");
+ assert_equals(body, expected.body, "response body");
+}
+
+async function sharedWorkerFetchTest(t, { source, target, expected }) {
+ const targetUrl = preflightUrl(target);
+
+ const sourceUrl =
+ resolveUrl("resources/shared-fetcher.js", sourceResolveOptions(source));
+ sourceUrl.searchParams.append("url", targetUrl.href);
+
+ const fetcherUrl = new URL("shared-worker-fetcher.html", sourceUrl);
+
+ const reply = futureMessage();
+ const iframe = await appendIframe(t, document, fetcherUrl);
+
+ iframe.contentWindow.postMessage({ url: sourceUrl.href }, "*");
+
+ const { error, status, body } = await reply;
+ assert_equals(error, expected.error, "fetch error");
+ assert_equals(status, expected.status, "response status");
+ assert_equals(body, expected.body, "response body");
+}
+
+async function sharedWorkerBlobFetchTest(t, { source, target, expected }) {
+ const targetUrl = preflightUrl(target);
+
+ const fetcherUrl = resolveUrl(
+ 'resources/shared-worker-blob-fetcher.html',
+ sourceResolveOptions(source));
+
+ const reply = futureMessage();
+ const iframe = await appendIframe(t, document, fetcherUrl);
+
+ iframe.contentWindow.postMessage({ url: targetUrl.href }, "*");
+
+ const { error, status, body } = await reply;
+ assert_equals(error, expected.error, "fetch error");
+ assert_equals(status, expected.status, "response status");
+ assert_equals(body, expected.body, "response body");
+}
diff --git a/test/wpt/tests/fetch/private-network-access/resources/worker-blob-fetcher.html b/test/wpt/tests/fetch/private-network-access/resources/worker-blob-fetcher.html
new file mode 100644
index 0000000..5a50271
--- /dev/null
+++ b/test/wpt/tests/fetch/private-network-access/resources/worker-blob-fetcher.html
@@ -0,0 +1,45 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Worker Blob Fetcher</title>
+<script>
+ window.addEventListener("message", function (evt) {
+ const { url } = evt.data;
+
+ const workerScriptContent = `
+ async function doFetch(url) {
+ const response = await fetch(url);
+ const body = await response.text();
+ return {
+ status: response.status,
+ body,
+ };
+ }
+
+ async function fetchAndPost(url) {
+ try {
+ const message = await doFetch(url);
+ self.postMessage(message);
+ } catch(e) {
+ self.postMessage({ error: e.name });
+ }
+ }
+
+ fetchAndPost("${url}");
+ `;
+ const blob =
+ new Blob([workerScriptContent], {type: 'application/javascript'});
+ const workerScriptUrl = URL.createObjectURL(blob);
+
+ const worker = new Worker(workerScriptUrl);
+
+ URL.revokeObjectURL(workerScriptUrl);
+
+ worker.addEventListener("message", (evt) => {
+ parent.postMessage(evt.data, "*");
+ });
+
+ worker.addEventListener("error", (evt) => {
+ parent.postMessage({ error: evt.message || "unknown error" }, "*");
+ });
+ });
+</script>
diff --git a/test/wpt/tests/fetch/private-network-access/resources/worker-fetcher.html b/test/wpt/tests/fetch/private-network-access/resources/worker-fetcher.html
new file mode 100644
index 0000000..bd155a5
--- /dev/null
+++ b/test/wpt/tests/fetch/private-network-access/resources/worker-fetcher.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Worker Fetcher</title>
+<script>
+ window.addEventListener("message", function (evt) {
+ let { url } = evt.data;
+
+ const worker = new Worker(url);
+
+ worker.addEventListener("message", (evt) => {
+ parent.postMessage(evt.data, "*");
+ });
+
+ worker.addEventListener("error", (evt) => {
+ parent.postMessage({ error: evt.message || "unknown error" }, "*");
+ });
+ });
+</script>
diff --git a/test/wpt/tests/fetch/private-network-access/resources/worker-fetcher.js b/test/wpt/tests/fetch/private-network-access/resources/worker-fetcher.js
new file mode 100644
index 0000000..aab49af
--- /dev/null
+++ b/test/wpt/tests/fetch/private-network-access/resources/worker-fetcher.js
@@ -0,0 +1,11 @@
+const url = new URL(self.location).searchParams.get("url");
+const worker = new Worker(url);
+
+// Relay messages from the worker to the parent frame.
+worker.addEventListener("message", (evt) => {
+ self.postMessage(evt.data);
+});
+
+worker.addEventListener("error", (evt) => {
+ self.postMessage({ error: evt.message || "unknown error" });
+});
diff --git a/test/wpt/tests/fetch/private-network-access/resources/xhr-sender.html b/test/wpt/tests/fetch/private-network-access/resources/xhr-sender.html
new file mode 100644
index 0000000..b131fa4
--- /dev/null
+++ b/test/wpt/tests/fetch/private-network-access/resources/xhr-sender.html
@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>XHR Sender</title>
+<script>
+ window.addEventListener("message", function (event) {
+ let { url, method } = event.data;
+ if (!method) {
+ method = "GET";
+ }
+
+ const xhr = new XMLHttpRequest;
+
+ xhr.addEventListener("load", (evt) => {
+ const message = {
+ loaded: true,
+ status: xhr.status,
+ body: xhr.responseText,
+ };
+ parent.postMessage(message, "*");
+ });
+
+ xhr.addEventListener("error", (evt) => {
+ const message = {
+ loaded: false,
+ status: xhr.status,
+ };
+ parent.postMessage(message, "*");
+ });
+
+ xhr.open(method, url);
+ xhr.send();
+ });
+</script>
diff --git a/test/wpt/tests/fetch/private-network-access/service-worker-background-fetch.tentative.https.window.js b/test/wpt/tests/fetch/private-network-access/service-worker-background-fetch.tentative.https.window.js
new file mode 100644
index 0000000..6369b16
--- /dev/null
+++ b/test/wpt/tests/fetch/private-network-access/service-worker-background-fetch.tentative.https.window.js
@@ -0,0 +1,142 @@
+// META: script=/common/utils.js
+// META: script=resources/support.sub.js
+//
+// Spec: https://wicg.github.io/private-network-access/#integration-fetch
+// Spec: https://wicg.github.io/background-fetch/
+//
+// These tests check that background fetches from within `ServiceWorker` scripts
+// are not subject to Private Network Access checks.
+
+// Results that may be expected in tests.
+const TestResult = {
+ SUCCESS: { ok: true, body: "success", result: "success", failureReason: "" },
+};
+
+async function makeTest(t, { source, target, expected }) {
+ const scriptUrl =
+ resolveUrl("resources/service-worker.js", sourceResolveOptions(source));
+
+ const bridgeUrl = new URL("service-worker-bridge.html", scriptUrl);
+
+ const targetUrl = preflightUrl(target);
+
+ const iframe = await appendIframe(t, document, bridgeUrl);
+
+ const request = (message) => {
+ const reply = futureMessage();
+ iframe.contentWindow.postMessage(message, "*");
+ return reply;
+ };
+
+ {
+ const { error, loaded } = await request({
+ action: "register",
+ url: scriptUrl.href,
+ });
+
+ assert_equals(error, undefined, "register error");
+ assert_true(loaded, "response loaded");
+ }
+
+ {
+ const { error, state } = await request({
+ action: "set-permission",
+ name: "background-fetch",
+ state: "granted",
+ });
+
+ assert_equals(error, undefined, "set permission error");
+ assert_equals(state, "granted", "permission state");
+ }
+
+ {
+ const { error, result, failureReason, ok, body } = await request({
+ action: "background-fetch",
+ url: targetUrl.href,
+ });
+
+ assert_equals(error, expected.error, "error");
+ assert_equals(failureReason, expected.failureReason, "fetch failure reason");
+ assert_equals(result, expected.result, "fetch result");
+ assert_equals(ok, expected.ok, "response ok");
+ assert_equals(body, expected.body, "response body");
+ }
+}
+
+promise_test(t => makeTest(t, {
+ source: { server: Server.HTTPS_LOCAL },
+ target: { server: Server.HTTPS_LOCAL },
+ expected: TestResult.SUCCESS,
+}), "local to local: success.");
+
+promise_test(t => makeTest(t, {
+ source: { server: Server.HTTPS_PRIVATE },
+ target: {
+ server: Server.HTTPS_LOCAL,
+ behavior: { response: ResponseBehavior.allowCrossOrigin() },
+ },
+ expected: TestResult.SUCCESS,
+}), "private to local: success.");
+
+promise_test(t => makeTest(t, {
+ source: { server: Server.HTTPS_PRIVATE },
+ target: { server: Server.HTTPS_PRIVATE },
+ expected: TestResult.SUCCESS,
+}), "private to private: success.");
+
+promise_test(t => makeTest(t, {
+ source: { server: Server.HTTPS_PUBLIC },
+ target: {
+ server: Server.HTTPS_LOCAL,
+ behavior: { response: ResponseBehavior.allowCrossOrigin() },
+ },
+ expected: TestResult.SUCCESS,
+}), "public to local: success.");
+
+promise_test(t => makeTest(t, {
+ source: { server: Server.HTTPS_PUBLIC },
+ target: {
+ server: Server.HTTPS_PRIVATE,
+ behavior: { response: ResponseBehavior.allowCrossOrigin() },
+ },
+ expected: TestResult.SUCCESS,
+}), "public to private: success.");
+
+promise_test(t => makeTest(t, {
+ source: { server: Server.HTTPS_PUBLIC },
+ target: { server: Server.HTTPS_PUBLIC },
+ expected: TestResult.SUCCESS,
+}), "public to public: success.");
+
+promise_test(t => makeTest(t, {
+ source: {
+ server: Server.HTTPS_LOCAL,
+ treatAsPublic: true,
+ },
+ target: { server: Server.HTTPS_LOCAL },
+ expected: TestResult.SUCCESS,
+}), "treat-as-public to local: success.");
+
+promise_test(t => makeTest(t, {
+ source: {
+ server: Server.HTTPS_LOCAL,
+ treatAsPublic: true,
+ },
+ target: {
+ server: Server.HTTPS_PRIVATE,
+ behavior: { response: ResponseBehavior.allowCrossOrigin() },
+ },
+ expected: TestResult.SUCCESS,
+}), "treat-as-public to private: success.");
+
+promise_test(t => makeTest(t, {
+ source: {
+ server: Server.HTTPS_LOCAL,
+ treatAsPublic: true,
+ },
+ target: {
+ server: Server.HTTPS_PUBLIC,
+ behavior: { response: ResponseBehavior.allowCrossOrigin() },
+ },
+ expected: TestResult.SUCCESS,
+}), "treat-as-public to public: success.");
diff --git a/test/wpt/tests/fetch/private-network-access/service-worker-fetch.tentative.https.window.js b/test/wpt/tests/fetch/private-network-access/service-worker-fetch.tentative.https.window.js
new file mode 100644
index 0000000..cb6d1f7
--- /dev/null
+++ b/test/wpt/tests/fetch/private-network-access/service-worker-fetch.tentative.https.window.js
@@ -0,0 +1,235 @@
+// META: script=/common/utils.js
+// META: script=resources/support.sub.js
+// META: script=/common/subset-tests.js
+// META: variant=?1-8
+// META: variant=?9-last
+//
+// Spec: https://wicg.github.io/private-network-access/#integration-fetch
+//
+// These tests check that fetches from within `ServiceWorker` scripts are
+// subject to Private Network Access checks, just like fetches from within
+// documents.
+
+// Results that may be expected in tests.
+const TestResult = {
+ SUCCESS: { ok: true, body: "success" },
+ FAILURE: { error: "TypeError" },
+};
+
+async function makeTest(t, { source, target, expected }) {
+ const bridgeUrl = resolveUrl(
+ "resources/service-worker-bridge.html",
+ sourceResolveOptions({ server: source.server }));
+
+ const scriptUrl =
+ resolveUrl("resources/service-worker.js", sourceResolveOptions(source));
+
+ const realTargetUrl = preflightUrl(target);
+
+ // Fetch a URL within the service worker's scope, but tell it which URL to
+ // really fetch.
+ const targetUrl = new URL("service-worker-proxy", scriptUrl);
+ targetUrl.searchParams.append("proxied-url", realTargetUrl.href);
+
+ const iframe = await appendIframe(t, document, bridgeUrl);
+
+ const request = (message) => {
+ const reply = futureMessage();
+ iframe.contentWindow.postMessage(message, "*");
+ return reply;
+ };
+
+ {
+ const { error, loaded } = await request({
+ action: "register",
+ url: scriptUrl.href,
+ });
+
+ assert_equals(error, undefined, "register error");
+ assert_true(loaded, "response loaded");
+ }
+
+ try {
+ const { controlled, numControllerChanges } = await request({
+ action: "wait",
+ numControllerChanges: 1,
+ });
+
+ assert_equals(numControllerChanges, 1, "controller change");
+ assert_true(controlled, "bridge script is controlled");
+
+ const { error, ok, body } = await request({
+ action: "fetch",
+ url: targetUrl.href,
+ });
+
+ assert_equals(error, expected.error, "fetch error");
+ assert_equals(ok, expected.ok, "response ok");
+ assert_equals(body, expected.body, "response body");
+ } finally {
+ // Always unregister the service worker.
+ const { error, unregistered } = await request({
+ action: "unregister",
+ scope: new URL("./", scriptUrl).href,
+ });
+
+ assert_equals(error, undefined, "unregister error");
+ assert_true(unregistered, "unregistered");
+ }
+}
+
+subsetTest(promise_test, t => makeTest(t, {
+ source: { server: Server.HTTPS_LOCAL },
+ target: { server: Server.HTTPS_LOCAL },
+ expected: TestResult.SUCCESS,
+}), "local to local: success.");
+
+subsetTest(promise_test, t => makeTest(t, {
+ source: { server: Server.HTTPS_PRIVATE },
+ target: {
+ server: Server.HTTPS_LOCAL,
+ behavior: { response: ResponseBehavior.allowCrossOrigin() },
+ },
+ expected: TestResult.FAILURE,
+}), "private to local: failed preflight.");
+
+subsetTest(promise_test, t => makeTest(t, {
+ source: { server: Server.HTTPS_PRIVATE },
+ target: {
+ server: Server.HTTPS_LOCAL,
+ behavior: {
+ preflight: PreflightBehavior.success(token()),
+ response: ResponseBehavior.allowCrossOrigin(),
+ },
+ },
+ expected: TestResult.SUCCESS,
+}), "private to local: success.");
+
+subsetTest(promise_test, t => makeTest(t, {
+ source: { server: Server.HTTPS_PRIVATE },
+ target: { server: Server.HTTPS_PRIVATE },
+ expected: TestResult.SUCCESS,
+}), "private to private: success.");
+
+subsetTest(promise_test, t => makeTest(t, {
+ source: { server: Server.HTTPS_PUBLIC },
+ target: {
+ server: Server.HTTPS_LOCAL,
+ behavior: { response: ResponseBehavior.allowCrossOrigin() },
+ },
+ expected: TestResult.FAILURE,
+}), "public to local: failed preflight.");
+
+subsetTest(promise_test, t => makeTest(t, {
+ source: { server: Server.HTTPS_PUBLIC },
+ target: {
+ server: Server.HTTPS_LOCAL,
+ behavior: {
+ preflight: PreflightBehavior.success(token()),
+ response: ResponseBehavior.allowCrossOrigin(),
+ },
+ },
+ expected: TestResult.SUCCESS,
+}), "public to local: success.");
+
+subsetTest(promise_test, t => makeTest(t, {
+ source: { server: Server.HTTPS_PUBLIC },
+ target: {
+ server: Server.HTTPS_PRIVATE,
+ behavior: { response: ResponseBehavior.allowCrossOrigin() },
+ },
+ expected: TestResult.FAILURE,
+}), "public to private: failed preflight.");
+
+subsetTest(promise_test, t => makeTest(t, {
+ source: { server: Server.HTTPS_PUBLIC },
+ target: {
+ server: Server.HTTPS_PRIVATE,
+ behavior: {
+ preflight: PreflightBehavior.success(token()),
+ response: ResponseBehavior.allowCrossOrigin(),
+ },
+ },
+ expected: TestResult.SUCCESS,
+}), "public to private: success.");
+
+subsetTest(promise_test, t => makeTest(t, {
+ source: { server: Server.HTTPS_PUBLIC },
+ target: { server: Server.HTTPS_PUBLIC },
+ expected: TestResult.SUCCESS,
+}), "public to public: success.");
+
+subsetTest(promise_test, t => makeTest(t, {
+ source: {
+ server: Server.HTTPS_LOCAL,
+ treatAsPublic: true,
+ },
+ target: {
+ server: Server.OTHER_HTTPS_LOCAL,
+ behavior: { response: ResponseBehavior.allowCrossOrigin() },
+ },
+ expected: TestResult.FAILURE,
+}), "treat-as-public to local: failed preflight.");
+
+subsetTest(promise_test, t => makeTest(t, {
+ source: {
+ server: Server.HTTPS_LOCAL,
+ treatAsPublic: true,
+ },
+ target: {
+ server: Server.OTHER_HTTPS_LOCAL,
+ behavior: {
+ preflight: PreflightBehavior.success(token()),
+ response: ResponseBehavior.allowCrossOrigin(),
+ },
+ },
+ expected: TestResult.SUCCESS,
+}), "treat-as-public to local: success.");
+
+subsetTest(promise_test, t => makeTest(t, {
+ source: {
+ server: Server.HTTPS_LOCAL,
+ treatAsPublic: true,
+ },
+ target: { server: Server.HTTPS_LOCAL },
+ expected: TestResult.SUCCESS,
+}), "treat-as-public to local (same-origin): no preflight required.");
+
+subsetTest(promise_test, t => makeTest(t, {
+ source: {
+ server: Server.HTTPS_LOCAL,
+ treatAsPublic: true,
+ },
+ target: {
+ server: Server.HTTPS_PRIVATE,
+ behavior: { response: ResponseBehavior.allowCrossOrigin() },
+ },
+ expected: TestResult.FAILURE,
+}), "treat-as-public to private: failed preflight.");
+
+subsetTest(promise_test, t => makeTest(t, {
+ source: {
+ server: Server.HTTPS_LOCAL,
+ treatAsPublic: true,
+ },
+ target: {
+ server: Server.HTTPS_PRIVATE,
+ behavior: {
+ preflight: PreflightBehavior.success(token()),
+ response: ResponseBehavior.allowCrossOrigin(),
+ },
+ },
+ expected: TestResult.SUCCESS,
+}), "treat-as-public to private: success.");
+
+subsetTest(promise_test, t => makeTest(t, {
+ source: {
+ server: Server.HTTPS_LOCAL,
+ treatAsPublic: true,
+ },
+ target: {
+ server: Server.HTTPS_PUBLIC,
+ behavior: { response: ResponseBehavior.allowCrossOrigin() },
+ },
+ expected: TestResult.SUCCESS,
+}), "treat-as-public to public: success.");
diff --git a/test/wpt/tests/fetch/private-network-access/service-worker-update.tentative.https.window.js b/test/wpt/tests/fetch/private-network-access/service-worker-update.tentative.https.window.js
new file mode 100644
index 0000000..4882d23
--- /dev/null
+++ b/test/wpt/tests/fetch/private-network-access/service-worker-update.tentative.https.window.js
@@ -0,0 +1,106 @@
+// META: script=/common/utils.js
+// META: script=resources/support.sub.js
+//
+// Spec: https://wicg.github.io/private-network-access/#integration-fetch
+//
+// These tests check that `ServiceWorker` script update fetches are exempt from
+// Private Network Access checks because they are always same-origin and the
+// origin is potentially trustworthy. The client of the fetch, for PNA purposes,
+// is taken to be the previous script.
+//
+// The tests is carried out by instantiating a service worker from a resource
+// that carries the `Content-Security-Policy: treat-as-public-address` header,
+// such that the registration is placed in the public IP address space. When
+// the script is fetched for an update, the client is thus considered public,
+// yet the same-origin fetch observes that the server's IP endpoint is not
+// necessarily in the public IP address space.
+//
+// See also: worker.https.window.js
+
+// Results that may be expected in tests.
+const TestResult = {
+ SUCCESS: { updated: true },
+ FAILURE: { error: "TypeError" },
+};
+
+async function makeTest(t, { target, expected }) {
+ // The bridge must be same-origin with the service worker script.
+ const bridgeUrl = resolveUrl(
+ "resources/service-worker-bridge.html",
+ sourceResolveOptions({ server: target.server }));
+
+ const scriptUrl = preflightUrl(target);
+ scriptUrl.searchParams.append("treat-as-public-once", token());
+ scriptUrl.searchParams.append("mime-type", "application/javascript");
+ scriptUrl.searchParams.append("file", "service-worker.js");
+ scriptUrl.searchParams.append("random-js-prefix", true);
+
+ const iframe = await appendIframe(t, document, bridgeUrl);
+
+ const request = (message) => {
+ const reply = futureMessage();
+ iframe.contentWindow.postMessage(message, "*");
+ return reply;
+ };
+
+ {
+ const { error, loaded } = await request({
+ action: "register",
+ url: scriptUrl.href,
+ });
+
+ assert_equals(error, undefined, "register error");
+ assert_true(loaded, "response loaded");
+ }
+
+ try {
+ let { controlled, numControllerChanges } = await request({
+ action: "wait",
+ numControllerChanges: 1,
+ });
+
+ assert_equals(numControllerChanges, 1, "controller change");
+ assert_true(controlled, "bridge script is controlled");
+
+ const { error, updated } = await request({ action: "update" });
+
+ assert_equals(error, expected.error, "update error");
+ assert_equals(updated, expected.updated, "registration updated");
+
+ // Stop here if we do not expect the update to succeed.
+ if (!expected.updated) {
+ return;
+ }
+
+ ({ controlled, numControllerChanges } = await request({
+ action: "wait",
+ numControllerChanges: 2,
+ }));
+
+ assert_equals(numControllerChanges, 2, "controller change");
+ assert_true(controlled, "bridge script still controlled");
+ } finally {
+ const { error, unregistered } = await request({
+ action: "unregister",
+ scope: new URL("./", scriptUrl).href,
+ });
+
+ assert_equals(error, undefined, "unregister error");
+ assert_true(unregistered, "unregistered");
+ }
+}
+
+promise_test(t => makeTest(t, {
+ target: { server: Server.HTTPS_LOCAL },
+ expected: TestResult.SUCCESS,
+}), "update public to local: success.");
+
+promise_test(t => makeTest(t, {
+ target: { server: Server.HTTPS_PRIVATE },
+ expected: TestResult.SUCCESS,
+}), "update public to private: success.");
+
+promise_test(t => makeTest(t, {
+ target: { server: Server.HTTPS_PUBLIC },
+ expected: TestResult.SUCCESS,
+}), "update public to public: success.");
diff --git a/test/wpt/tests/fetch/private-network-access/service-worker.tentative.https.window.js b/test/wpt/tests/fetch/private-network-access/service-worker.tentative.https.window.js
new file mode 100644
index 0000000..046f662
--- /dev/null
+++ b/test/wpt/tests/fetch/private-network-access/service-worker.tentative.https.window.js
@@ -0,0 +1,84 @@
+// META: script=/common/utils.js
+// META: script=resources/support.sub.js
+//
+// Spec: https://wicg.github.io/private-network-access/#integration-fetch
+//
+// These tests check that initial `ServiceWorker` script fetches are exempt from
+// Private Network Access checks because they are always same-origin and the
+// origin is potentially trustworthy.
+//
+// See also: worker.https.window.js
+
+// Results that may be expected in tests.
+const TestResult = {
+ SUCCESS: {
+ register: { loaded: true },
+ unregister: { unregistered: true },
+ },
+ FAILURE: {
+ register: { error: "TypeError" },
+ unregister: { unregistered: false, error: "no registration" },
+ },
+};
+
+async function makeTest(t, { source, target, expected }) {
+ const sourceUrl = resolveUrl("resources/service-worker-bridge.html",
+ sourceResolveOptions(source));
+
+ const targetUrl = preflightUrl(target);
+ targetUrl.searchParams.append("body", "undefined");
+ targetUrl.searchParams.append("mime-type", "application/javascript");
+
+ const scope = resolveUrl(`resources/${token()}`, {...target.server}).href;
+
+ const iframe = await appendIframe(t, document, sourceUrl);
+
+ {
+ const reply = futureMessage();
+ const message = {
+ action: "register",
+ url: targetUrl.href,
+ options: { scope },
+ };
+ iframe.contentWindow.postMessage(message, "*");
+
+ const { error, loaded } = await reply;
+
+ assert_equals(error, expected.register.error, "register error");
+ assert_equals(loaded, expected.register.loaded, "response loaded");
+ }
+
+ {
+ const reply = futureMessage();
+ iframe.contentWindow.postMessage({ action: "unregister", scope }, "*");
+
+ const { error, unregistered } = await reply;
+ assert_equals(error, expected.unregister.error, "unregister error");
+ assert_equals(
+ unregistered, expected.unregister.unregistered, "worker unregistered");
+ }
+}
+
+promise_test(t => makeTest(t, {
+ source: {
+ server: Server.HTTPS_LOCAL,
+ treatAsPublic: true,
+ },
+ target: { server: Server.HTTPS_LOCAL },
+ expected: TestResult.SUCCESS,
+}), "treat-as-public to local: success.");
+
+promise_test(t => makeTest(t, {
+ source: {
+ server: Server.HTTPS_PRIVATE,
+ treatAsPublic: true,
+ },
+ target: { server: Server.HTTPS_PRIVATE },
+ expected: TestResult.SUCCESS,
+}), "treat-as-public to private: success.");
+
+promise_test(t => makeTest(t, {
+ source: { server: Server.HTTPS_PUBLIC },
+ target: { server: Server.HTTPS_PUBLIC },
+ expected: TestResult.SUCCESS,
+}), "public to public: success.");
diff --git a/test/wpt/tests/fetch/private-network-access/shared-worker-blob-fetch.tentative.https.window.js b/test/wpt/tests/fetch/private-network-access/shared-worker-blob-fetch.tentative.https.window.js
new file mode 100644
index 0000000..269abb7
--- /dev/null
+++ b/test/wpt/tests/fetch/private-network-access/shared-worker-blob-fetch.tentative.https.window.js
@@ -0,0 +1,168 @@
+// META: script=/common/utils.js
+// META: script=resources/support.sub.js
+//
+// Spec: https://wicg.github.io/private-network-access/#integration-fetch
+//
+// These tests check that fetches from within `SharedWorker` scripts that are
+// loaded from blob URLs are subject to Private Network Access checks, just like
+// fetches from within documents.
+//
+// This file covers only those tests that must execute in a secure context.
+// Other tests are defined in: shared-worker-blob-fetch.window.js
+
+promise_test(t => sharedWorkerBlobFetchTest(t, {
+ source: { server: Server.HTTPS_LOCAL },
+ target: { server: Server.HTTPS_LOCAL },
+ expected: WorkerFetchTestResult.SUCCESS,
+}), "local to local: success.");
+
+promise_test(t => sharedWorkerBlobFetchTest(t, {
+ source: { server: Server.HTTPS_PRIVATE },
+ target: {
+ server: Server.HTTPS_LOCAL,
+ behavior: { response: ResponseBehavior.allowCrossOrigin() },
+ },
+ expected: WorkerFetchTestResult.FAILURE,
+}), "private to local: failed preflight.");
+
+promise_test(t => sharedWorkerBlobFetchTest(t, {
+ source: { server: Server.HTTPS_PRIVATE },
+ target: {
+ server: Server.HTTPS_LOCAL,
+ behavior: {
+ preflight: PreflightBehavior.success(token()),
+ response: ResponseBehavior.allowCrossOrigin(),
+ },
+ },
+ expected: WorkerFetchTestResult.SUCCESS,
+}), "private to local: success.");
+
+promise_test(t => sharedWorkerBlobFetchTest(t, {
+ source: { server: Server.HTTPS_PRIVATE },
+ target: { server: Server.HTTPS_PRIVATE },
+ expected: WorkerFetchTestResult.SUCCESS,
+}), "private to private: success.");
+
+promise_test(t => sharedWorkerBlobFetchTest(t, {
+ source: { server: Server.HTTPS_PUBLIC },
+ target: {
+ server: Server.HTTPS_LOCAL,
+ behavior: { response: ResponseBehavior.allowCrossOrigin() },
+ },
+ expected: WorkerFetchTestResult.FAILURE,
+}), "public to local: failed preflight.");
+
+promise_test(t => sharedWorkerBlobFetchTest(t, {
+ source: { server: Server.HTTPS_PUBLIC },
+ target: {
+ server: Server.HTTPS_LOCAL,
+ behavior: {
+ preflight: PreflightBehavior.success(token()),
+ response: ResponseBehavior.allowCrossOrigin(),
+ },
+ },
+ expected: WorkerFetchTestResult.SUCCESS,
+}), "public to local: success.");
+
+promise_test(t => sharedWorkerBlobFetchTest(t, {
+ source: { server: Server.HTTPS_PUBLIC },
+ target: {
+ server: Server.HTTPS_PRIVATE,
+ behavior: { response: ResponseBehavior.allowCrossOrigin() },
+ },
+ expected: WorkerFetchTestResult.FAILURE,
+}), "public to private: failed preflight.");
+
+promise_test(t => sharedWorkerBlobFetchTest(t, {
+ source: { server: Server.HTTPS_PUBLIC },
+ target: {
+ server: Server.HTTPS_PRIVATE,
+ behavior: {
+ preflight: PreflightBehavior.success(token()),
+ response: ResponseBehavior.allowCrossOrigin(),
+ },
+ },
+ expected: WorkerFetchTestResult.SUCCESS,
+}), "public to private: success.");
+
+promise_test(t => sharedWorkerBlobFetchTest(t, {
+ source: { server: Server.HTTPS_PUBLIC },
+ target: { server: Server.HTTPS_PUBLIC },
+ expected: WorkerFetchTestResult.SUCCESS,
+}), "public to public: success.");
+
+promise_test(t => sharedWorkerBlobFetchTest(t, {
+ source: {
+ server: Server.HTTPS_LOCAL,
+ treatAsPublic: true,
+ },
+ target: {
+ server: Server.OTHER_HTTPS_LOCAL,
+ behavior: { response: ResponseBehavior.allowCrossOrigin() },
+ },
+ expected: WorkerFetchTestResult.FAILURE,
+}), "treat-as-public to local: failed preflight.");
+
+promise_test(t => sharedWorkerBlobFetchTest(t, {
+ source: {
+ server: Server.HTTPS_LOCAL,
+ treatAsPublic: true,
+ },
+ target: {
+ server: Server.OTHER_HTTPS_LOCAL,
+ behavior: {
+ preflight: PreflightBehavior.success(token()),
+ response: ResponseBehavior.allowCrossOrigin(),
+ },
+ },
+ expected: WorkerFetchTestResult.SUCCESS,
+}), "treat-as-public to local: success.");
+
+promise_test(t => sharedWorkerBlobFetchTest(t, {
+ source: {
+ server: Server.HTTPS_LOCAL,
+ treatAsPublic: true,
+ },
+ target: { server: Server.HTTPS_LOCAL },
+ expected: WorkerFetchTestResult.SUCCESS,
+}), "treat-as-public to local (same-origin): no preflight required.");
+
+promise_test(t => sharedWorkerBlobFetchTest(t, {
+ source: {
+ server: Server.HTTPS_LOCAL,
+ treatAsPublic: true,
+ },
+ target: {
+ server: Server.HTTPS_PRIVATE,
+ behavior: { response: ResponseBehavior.allowCrossOrigin() },
+ },
+ expected: WorkerFetchTestResult.FAILURE,
+}), "treat-as-public to private: failed preflight.");
+
+promise_test(t => sharedWorkerBlobFetchTest(t, {
+ source: {
+ server: Server.HTTPS_LOCAL,
+ treatAsPublic: true,
+ },
+ target: {
+ server: Server.HTTPS_PRIVATE,
+ behavior: {
+ preflight: PreflightBehavior.success(token()),
+ response: ResponseBehavior.allowCrossOrigin(),
+ },
+ },
+ expected: WorkerFetchTestResult.SUCCESS,
+}), "treat-as-public to private: success.");
+
+promise_test(t => sharedWorkerBlobFetchTest(t, {
+ source: {
+ server: Server.HTTPS_LOCAL,
+ treatAsPublic: true,
+ },
+ target: {
+ server: Server.HTTPS_PUBLIC,
+ behavior: { response: ResponseBehavior.allowCrossOrigin() },
+ },
+ expected: WorkerFetchTestResult.SUCCESS,
+}), "treat-as-public to public: success.");
+
diff --git a/test/wpt/tests/fetch/private-network-access/shared-worker-blob-fetch.tentative.window.js b/test/wpt/tests/fetch/private-network-access/shared-worker-blob-fetch.tentative.window.js
new file mode 100644
index 0000000..d430ea7
--- /dev/null
+++ b/test/wpt/tests/fetch/private-network-access/shared-worker-blob-fetch.tentative.window.js
@@ -0,0 +1,173 @@
+// META: script=/common/utils.js
+// META: script=resources/support.sub.js
+//
+// Spec: https://wicg.github.io/private-network-access/#integration-fetch
+//
+// These tests check that fetches from within `SharedWorker` scripts that are
+// loaded from blob URLs are subject to Private Network Access checks, just like
+// fetches from within documents.
+//
+// This file covers only those tests that must execute in a non-secure context.
+// Other tests are defined in: shared-worker-blob-fetch.https.window.js
+
+promise_test(t => sharedWorkerBlobFetchTest(t, {
+ source: { server: Server.HTTP_LOCAL },
+ target: { server: Server.HTTP_LOCAL },
+ expected: WorkerFetchTestResult.SUCCESS,
+}), "local to local: success.");
+
+promise_test(t => sharedWorkerBlobFetchTest(t, {
+ source: { server: Server.HTTP_PRIVATE },
+ target: {
+ server: Server.HTTP_LOCAL,
+ behavior: {
+ preflight: PreflightBehavior.optionalSuccess(token()),
+ response: ResponseBehavior.allowCrossOrigin(),
+ },
+ },
+ expected: WorkerFetchTestResult.FAILURE,
+}), "private to local: failure.");
+
+promise_test(t => sharedWorkerBlobFetchTest(t, {
+ source: { server: Server.HTTP_PRIVATE },
+ target: { server: Server.HTTP_PRIVATE },
+ expected: WorkerFetchTestResult.SUCCESS,
+}), "private to private: success.");
+
+promise_test(t => sharedWorkerBlobFetchTest(t, {
+ source: { server: Server.HTTP_PUBLIC },
+ target: {
+ server: Server.HTTP_LOCAL,
+ behavior: {
+ preflight: PreflightBehavior.optionalSuccess(token()),
+ response: ResponseBehavior.allowCrossOrigin(),
+ },
+ },
+ expected: WorkerFetchTestResult.FAILURE,
+}), "public to local: failure.");
+
+promise_test(t => sharedWorkerBlobFetchTest(t, {
+ source: { server: Server.HTTP_PUBLIC },
+ target: {
+ server: Server.HTTP_PRIVATE,
+ behavior: {
+ preflight: PreflightBehavior.optionalSuccess(token()),
+ response: ResponseBehavior.allowCrossOrigin(),
+ },
+ },
+ expected: WorkerFetchTestResult.FAILURE,
+}), "public to private: failure.");
+
+promise_test(t => sharedWorkerBlobFetchTest(t, {
+ source: { server: Server.HTTP_PUBLIC },
+ target: { server: Server.HTTP_PUBLIC },
+ expected: WorkerFetchTestResult.SUCCESS,
+}), "public to public: success.");
+
+promise_test(t => sharedWorkerBlobFetchTest(t, {
+ source: {
+ server: Server.HTTP_LOCAL,
+ treatAsPublic: true,
+ },
+ target: {
+ server: Server.HTTP_LOCAL,
+ behavior: { preflight: PreflightBehavior.optionalSuccess(token()) },
+ },
+ expected: WorkerFetchTestResult.FAILURE,
+}), "treat-as-public to local: failure.");
+
+promise_test(t => sharedWorkerBlobFetchTest(t, {
+ source: {
+ server: Server.HTTP_LOCAL,
+ treatAsPublic: true,
+ },
+ target: {
+ server: Server.HTTP_PRIVATE,
+ behavior: {
+ preflight: PreflightBehavior.optionalSuccess(token()),
+ response: ResponseBehavior.allowCrossOrigin(),
+ },
+ },
+ expected: WorkerFetchTestResult.FAILURE,
+}), "treat-as-public to private: failure.");
+
+promise_test(t => sharedWorkerBlobFetchTest(t, {
+ source: {
+ server: Server.HTTP_LOCAL,
+ treatAsPublic: true,
+ },
+ target: {
+ server: Server.HTTP_PUBLIC,
+ behavior: { response: ResponseBehavior.allowCrossOrigin() },
+ },
+ expected: WorkerFetchTestResult.SUCCESS,
+}), "treat-as-public to public: success.");
+
+// The following tests verify that workers served over HTTPS are not allowed to
+// make private network requests because they are not secure contexts.
+
+promise_test(t => sharedWorkerBlobFetchTest(t, {
+ source: { server: Server.HTTPS_LOCAL },
+ target: {
+ server: Server.HTTP_LOCAL,
+ behavior: { response: ResponseBehavior.allowCrossOrigin() },
+ },
+ expected: WorkerFetchTestResult.SUCCESS,
+}), "local https to local: success.");
+
+promise_test(t => sharedWorkerBlobFetchTest(t, {
+ source: { server: Server.HTTPS_PUBLIC },
+ target: {
+ server: Server.HTTP_LOCAL,
+ behavior: {
+ preflight: PreflightBehavior.optionalSuccess(token()),
+ response: ResponseBehavior.allowCrossOrigin(),
+ },
+ },
+ expected: WorkerFetchTestResult.FAILURE,
+}), "private https to local: failure.");
+
+promise_test(t => sharedWorkerBlobFetchTest(t, {
+ source: { server: Server.HTTPS_PUBLIC },
+ target: {
+ server: Server.HTTP_LOCAL,
+ behavior: {
+ preflight: PreflightBehavior.optionalSuccess(token()),
+ response: ResponseBehavior.allowCrossOrigin(),
+ },
+ },
+ expected: WorkerFetchTestResult.FAILURE,
+}), "public https to local: failure.");
+
+promise_test(t => sharedWorkerBlobFetchTest(t, {
+ source: { server: Server.HTTPS_LOCAL },
+ target: {
+ server: Server.HTTPS_LOCAL,
+ behavior: { response: ResponseBehavior.allowCrossOrigin() },
+ },
+ expected: WorkerFetchTestResult.SUCCESS,
+}), "local https to local https: success.");
+
+promise_test(t => sharedWorkerBlobFetchTest(t, {
+ source: { server: Server.HTTPS_PRIVATE },
+ target: {
+ server: Server.HTTPS_LOCAL,
+ behavior: {
+ preflight: PreflightBehavior.optionalSuccess(token()),
+ response: ResponseBehavior.allowCrossOrigin(),
+ },
+ },
+ expected: WorkerFetchTestResult.FAILURE,
+}), "private https to local https: failure.");
+
+promise_test(t => sharedWorkerBlobFetchTest(t, {
+ source: { server: Server.HTTPS_PUBLIC },
+ target: {
+ server: Server.HTTPS_LOCAL,
+ behavior: {
+ preflight: PreflightBehavior.optionalSuccess(token()),
+ response: ResponseBehavior.allowCrossOrigin(),
+ },
+ },
+ expected: WorkerFetchTestResult.FAILURE,
+}), "public https to local https: failure.");
diff --git a/test/wpt/tests/fetch/private-network-access/shared-worker-fetch.tentative.https.window.js b/test/wpt/tests/fetch/private-network-access/shared-worker-fetch.tentative.https.window.js
new file mode 100644
index 0000000..e5f2b94
--- /dev/null
+++ b/test/wpt/tests/fetch/private-network-access/shared-worker-fetch.tentative.https.window.js
@@ -0,0 +1,167 @@
+// META: script=/common/utils.js
+// META: script=resources/support.sub.js
+//
+// Spec: https://wicg.github.io/private-network-access/#integration-fetch
+//
+// These tests check that fetches from within `SharedWorker` scripts are subject
+// to Private Network Access checks, just like fetches from within documents.
+//
+// This file covers only those tests that must execute in a secure context.
+// Other tests are defined in: shared-worker-fetch.window.js
+
+promise_test(t => sharedWorkerFetchTest(t, {
+ source: { server: Server.HTTPS_LOCAL },
+ target: { server: Server.HTTPS_LOCAL },
+ expected: WorkerFetchTestResult.SUCCESS,
+}), "local to local: success.");
+
+promise_test(t => sharedWorkerFetchTest(t, {
+ source: { server: Server.HTTPS_PRIVATE },
+ target: {
+ server: Server.HTTPS_LOCAL,
+ behavior: { response: ResponseBehavior.allowCrossOrigin() },
+ },
+ expected: WorkerFetchTestResult.FAILURE,
+}), "private to local: failure.");
+
+promise_test(t => sharedWorkerFetchTest(t, {
+ source: { server: Server.HTTPS_PRIVATE },
+ target: {
+ server: Server.HTTPS_LOCAL,
+ behavior: {
+ preflight: PreflightBehavior.success(token()),
+ response: ResponseBehavior.allowCrossOrigin(),
+ },
+ },
+ expected: WorkerFetchTestResult.SUCCESS,
+}), "private to local: success.");
+
+promise_test(t => sharedWorkerFetchTest(t, {
+ source: { server: Server.HTTPS_PRIVATE },
+ target: { server: Server.HTTPS_PRIVATE },
+ expected: WorkerFetchTestResult.SUCCESS,
+}), "private to private: success.");
+
+promise_test(t => sharedWorkerFetchTest(t, {
+ source: { server: Server.HTTPS_PUBLIC },
+ target: {
+ server: Server.HTTPS_LOCAL,
+ behavior: { response: ResponseBehavior.allowCrossOrigin() },
+ },
+ expected: WorkerFetchTestResult.FAILURE,
+}), "public to local: failed preflight.");
+
+promise_test(t => sharedWorkerFetchTest(t, {
+ source: { server: Server.HTTPS_PUBLIC },
+ target: {
+ server: Server.HTTPS_LOCAL,
+ behavior: {
+ preflight: PreflightBehavior.success(token()),
+ response: ResponseBehavior.allowCrossOrigin(),
+ },
+ },
+ expected: WorkerFetchTestResult.SUCCESS,
+}), "public to local: success.");
+
+promise_test(t => sharedWorkerFetchTest(t, {
+ source: { server: Server.HTTPS_PUBLIC },
+ target: {
+ server: Server.HTTPS_PRIVATE,
+ behavior: { response: ResponseBehavior.allowCrossOrigin() },
+ },
+ expected: WorkerFetchTestResult.FAILURE,
+}), "public to private: failed preflight.");
+
+promise_test(t => sharedWorkerFetchTest(t, {
+ source: { server: Server.HTTPS_PUBLIC },
+ target: {
+ server: Server.HTTPS_PRIVATE,
+ behavior: {
+ preflight: PreflightBehavior.success(token()),
+ response: ResponseBehavior.allowCrossOrigin(),
+ },
+ },
+ expected: WorkerFetchTestResult.SUCCESS,
+}), "public to private: success.");
+
+promise_test(t => sharedWorkerFetchTest(t, {
+ source: { server: Server.HTTPS_PUBLIC },
+ target: { server: Server.HTTPS_PUBLIC },
+ expected: WorkerFetchTestResult.SUCCESS,
+}), "public to public: success.");
+
+promise_test(t => sharedWorkerFetchTest(t, {
+ source: {
+ server: Server.HTTPS_LOCAL,
+ treatAsPublic: true,
+ },
+ target: {
+ server: Server.OTHER_HTTPS_LOCAL,
+ behavior: { response: ResponseBehavior.allowCrossOrigin() },
+ },
+ expected: WorkerFetchTestResult.FAILURE,
+}), "treat-as-public to local: failed preflight.");
+
+promise_test(t => sharedWorkerFetchTest(t, {
+ source: {
+ server: Server.HTTPS_LOCAL,
+ treatAsPublic: true,
+ },
+ target: {
+ server: Server.OTHER_HTTPS_LOCAL,
+ behavior: {
+ preflight: PreflightBehavior.success(token()),
+ response: ResponseBehavior.allowCrossOrigin(),
+ },
+ },
+ expected: WorkerFetchTestResult.SUCCESS,
+}), "treat-as-public to local: success.");
+
+promise_test(t => sharedWorkerFetchTest(t, {
+ source: {
+ server: Server.HTTPS_LOCAL,
+ treatAsPublic: true,
+ },
+ target: { server: Server.HTTPS_LOCAL },
+ expected: WorkerFetchTestResult.SUCCESS,
+}), "treat-as-public to local (same-origin): no preflight required.");
+
+promise_test(t => sharedWorkerFetchTest(t, {
+ source: {
+ server: Server.HTTPS_LOCAL,
+ treatAsPublic: true,
+ },
+ target: {
+ server: Server.HTTPS_PRIVATE,
+ behavior: { response: ResponseBehavior.allowCrossOrigin() },
+ },
+ expected: WorkerFetchTestResult.FAILURE,
+}), "treat-as-public to private: failed preflight.");
+
+promise_test(t => sharedWorkerFetchTest(t, {
+ source: {
+ server: Server.HTTPS_LOCAL,
+ treatAsPublic: true,
+ },
+ target: {
+ server: Server.HTTPS_PRIVATE,
+ behavior: {
+ preflight: PreflightBehavior.success(token()),
+ response: ResponseBehavior.allowCrossOrigin(),
+ },
+ },
+ expected: WorkerFetchTestResult.SUCCESS,
+}), "treat-as-public to private: success.");
+
+promise_test(t => sharedWorkerFetchTest(t, {
+ source: {
+ server: Server.HTTPS_LOCAL,
+ treatAsPublic: true,
+ },
+ target: {
+ server: Server.HTTPS_PUBLIC,
+ behavior: { response: ResponseBehavior.allowCrossOrigin() },
+ },
+ expected: WorkerFetchTestResult.SUCCESS,
+}), "treat-as-public to public: success.");
+
diff --git a/test/wpt/tests/fetch/private-network-access/shared-worker-fetch.tentative.window.js b/test/wpt/tests/fetch/private-network-access/shared-worker-fetch.tentative.window.js
new file mode 100644
index 0000000..9bc1a89
--- /dev/null
+++ b/test/wpt/tests/fetch/private-network-access/shared-worker-fetch.tentative.window.js
@@ -0,0 +1,154 @@
+// META: script=/common/utils.js
+// META: script=resources/support.sub.js
+//
+// Spec: https://wicg.github.io/private-network-access/#integration-fetch
+//
+// These tests check that fetches from within `SharedWorker` scripts are subject
+// to Private Network Access checks, just like fetches from within documents.
+//
+// This file covers only those tests that must execute in a non-secure context.
+// Other tests are defined in: shared-worker-fetch.https.window.js
+
+promise_test(t => sharedWorkerFetchTest(t, {
+ source: { server: Server.HTTP_LOCAL },
+ target: { server: Server.HTTP_LOCAL },
+ expected: WorkerFetchTestResult.SUCCESS,
+}), "local to local: success.");
+
+promise_test(t => sharedWorkerFetchTest(t, {
+ source: { server: Server.HTTP_PRIVATE },
+ target: {
+ server: Server.HTTP_LOCAL,
+ behavior: {
+ preflight: PreflightBehavior.optionalSuccess(token()),
+ response: ResponseBehavior.allowCrossOrigin(),
+ },
+ },
+ expected: WorkerFetchTestResult.FAILURE,
+}), "private to local: failure.");
+
+promise_test(t => sharedWorkerFetchTest(t, {
+ source: { server: Server.HTTP_PRIVATE },
+ target: { server: Server.HTTP_PRIVATE },
+ expected: WorkerFetchTestResult.SUCCESS,
+}), "private to private: success.");
+
+promise_test(t => sharedWorkerFetchTest(t, {
+ source: { server: Server.HTTP_PUBLIC },
+ target: {
+ server: Server.HTTP_LOCAL,
+ behavior: {
+ preflight: PreflightBehavior.optionalSuccess(token()),
+ response: ResponseBehavior.allowCrossOrigin(),
+ },
+ },
+ expected: WorkerFetchTestResult.FAILURE,
+}), "public to local: failure.");
+
+promise_test(t => sharedWorkerFetchTest(t, {
+ source: { server: Server.HTTP_PUBLIC },
+ target: {
+ server: Server.HTTP_PRIVATE,
+ behavior: {
+ preflight: PreflightBehavior.optionalSuccess(token()),
+ response: ResponseBehavior.allowCrossOrigin(),
+ },
+ },
+ expected: WorkerFetchTestResult.FAILURE,
+}), "public to private: failure.");
+
+promise_test(t => sharedWorkerFetchTest(t, {
+ source: { server: Server.HTTP_PUBLIC },
+ target: { server: Server.HTTP_PUBLIC },
+ expected: WorkerFetchTestResult.SUCCESS,
+}), "public to public: success.");
+
+promise_test(t => sharedWorkerFetchTest(t, {
+ source: {
+ server: Server.HTTP_LOCAL,
+ treatAsPublic: true,
+ },
+ target: {
+ server: Server.HTTP_LOCAL,
+ behavior: { preflight: PreflightBehavior.optionalSuccess(token()) },
+ },
+ expected: WorkerFetchTestResult.FAILURE,
+}), "treat-as-public to local: failure.");
+
+promise_test(t => sharedWorkerFetchTest(t, {
+ source: {
+ server: Server.HTTP_LOCAL,
+ treatAsPublic: true,
+ },
+ target: {
+ server: Server.HTTP_PRIVATE,
+ behavior: {
+ preflight: PreflightBehavior.optionalSuccess(token()),
+ response: ResponseBehavior.allowCrossOrigin(),
+ },
+ },
+ expected: WorkerFetchTestResult.FAILURE,
+}), "treat-as-public to private: failure.");
+
+promise_test(t => sharedWorkerFetchTest(t, {
+ source: {
+ server: Server.HTTP_LOCAL,
+ treatAsPublic: true,
+ },
+ target: {
+ server: Server.HTTP_PUBLIC,
+ behavior: { response: ResponseBehavior.allowCrossOrigin() },
+ },
+ expected: WorkerFetchTestResult.SUCCESS,
+}), "treat-as-public to public: success.");
+
+// The following tests verify that workers served over HTTPS are not allowed to
+// make private network requests because they are not secure contexts.
+
+promise_test(t => sharedWorkerFetchTest(t, {
+ source: { server: Server.HTTPS_LOCAL },
+ target: {
+ server: Server.HTTP_LOCAL,
+ behavior: {
+ preflight: PreflightBehavior.optionalSuccess(token()),
+ response: ResponseBehavior.allowCrossOrigin(),
+ },
+ },
+ expected: WorkerFetchTestResult.SUCCESS,
+}), "local https to local: success.");
+
+promise_test(t => sharedWorkerFetchTest(t, {
+ source: { server: Server.HTTPS_PRIVATE },
+ target: {
+ server: Server.HTTP_LOCAL,
+ behavior: {
+ preflight: PreflightBehavior.optionalSuccess(token()),
+ response: ResponseBehavior.allowCrossOrigin(),
+ },
+ },
+ expected: WorkerFetchTestResult.FAILURE,
+}), "private https to local: failure.");
+
+promise_test(t => sharedWorkerFetchTest(t, {
+ source: { server: Server.HTTPS_PUBLIC },
+ target: {
+ server: Server.HTTP_LOCAL,
+ behavior: {
+ preflight: PreflightBehavior.optionalSuccess(token()),
+ response: ResponseBehavior.allowCrossOrigin(),
+ },
+ },
+ expected: WorkerFetchTestResult.FAILURE,
+}), "public https to local: failure.");
+
+promise_test(t => sharedWorkerFetchTest(t, {
+ source: { server: Server.HTTPS_PUBLIC },
+ target: {
+ server: Server.HTTP_PRIVATE,
+ behavior: {
+ preflight: PreflightBehavior.optionalSuccess(token()),
+ response: ResponseBehavior.allowCrossOrigin(),
+ },
+ },
+ expected: WorkerFetchTestResult.FAILURE,
+}), "public https to private: failure.");
diff --git a/test/wpt/tests/fetch/private-network-access/shared-worker.tentative.https.window.js b/test/wpt/tests/fetch/private-network-access/shared-worker.tentative.https.window.js
new file mode 100644
index 0000000..24ae108
--- /dev/null
+++ b/test/wpt/tests/fetch/private-network-access/shared-worker.tentative.https.window.js
@@ -0,0 +1,34 @@
+// META: script=/common/utils.js
+// META: script=resources/support.sub.js
+//
+// Spec: https://wicg.github.io/private-network-access/#integration-fetch
+//
+// These tests mirror `Worker` tests, except using `SharedWorker`.
+// See also: worker.https.window.js
+//
+// This file covers only those tests that must execute in a secure context.
+// Other tests are defined in: shared-worker.window.js
+
+promise_test(t => sharedWorkerScriptTest(t, {
+ source: {
+ server: Server.HTTPS_LOCAL,
+ treatAsPublic: true,
+ },
+ target: { server: Server.HTTPS_LOCAL },
+ expected: WorkerScriptTestResult.SUCCESS,
+}), "treat-as-public to local: success.");
+
+promise_test(t => sharedWorkerScriptTest(t, {
+ source: {
+ server: Server.HTTPS_PRIVATE,
+ treatAsPublic: true,
+ },
+ target: { server: Server.HTTPS_PRIVATE },
+ expected: WorkerScriptTestResult.SUCCESS,
+}), "treat-as-public to private: success.");
+
+promise_test(t => sharedWorkerScriptTest(t, {
+ source: { server: Server.HTTPS_PUBLIC },
+ target: { server: Server.HTTPS_PUBLIC },
+ expected: WorkerScriptTestResult.SUCCESS,
+}), "public to public: success.");
diff --git a/test/wpt/tests/fetch/private-network-access/shared-worker.tentative.window.js b/test/wpt/tests/fetch/private-network-access/shared-worker.tentative.window.js
new file mode 100644
index 0000000..ffa8a36
--- /dev/null
+++ b/test/wpt/tests/fetch/private-network-access/shared-worker.tentative.window.js
@@ -0,0 +1,34 @@
+// META: script=/common/utils.js
+// META: script=resources/support.sub.js
+//
+// Spec: https://wicg.github.io/private-network-access/#integration-fetch
+//
+// These tests mirror `Worker` tests, except using `SharedWorker`.
+// See also: shared-worker.window.js
+//
+// This file covers only those tests that must execute in a non secure context.
+// Other tests are defined in: shared-worker.https.window.js
+
+promise_test(t => sharedWorkerScriptTest(t, {
+ source: {
+ server: Server.HTTP_LOCAL,
+ treatAsPublic: true,
+ },
+ target: { server: Server.HTTP_LOCAL },
+ expected: WorkerScriptTestResult.FAILURE,
+}), "treat-as-public to local: failure.");
+
+promise_test(t => sharedWorkerScriptTest(t, {
+ source: {
+ server: Server.HTTP_PRIVATE,
+ treatAsPublic: true,
+ },
+ target: { server: Server.HTTP_PRIVATE },
+ expected: WorkerScriptTestResult.FAILURE,
+}), "treat-as-public to private: failure.");
+
+promise_test(t => sharedWorkerScriptTest(t, {
+ source: { server: Server.HTTP_PUBLIC },
+ target: { server: Server.HTTP_PUBLIC },
+ expected: WorkerScriptTestResult.SUCCESS,
+}), "public to public: success.");
diff --git a/test/wpt/tests/fetch/private-network-access/websocket.tentative.https.window.js b/test/wpt/tests/fetch/private-network-access/websocket.tentative.https.window.js
new file mode 100644
index 0000000..0731896
--- /dev/null
+++ b/test/wpt/tests/fetch/private-network-access/websocket.tentative.https.window.js
@@ -0,0 +1,40 @@
+// META: script=resources/support.sub.js
+//
+// Spec: https://wicg.github.io/private-network-access/#integration-fetch
+//
+// These tests verify that websocket connections behave similarly to fetches.
+//
+// This file covers only those tests that must execute in a secure context.
+// Other tests are defined in: websocket.https.window.js
+
+setup(() => {
+ // Making sure we are in a secure context, as expected.
+ assert_true(window.isSecureContext);
+});
+
+promise_test(t => websocketTest(t, {
+ source: { server: Server.HTTPS_LOCAL },
+ target: { server: Server.WSS_LOCAL },
+ expected: WebsocketTestResult.SUCCESS,
+}), "local to local: websocket success.");
+
+promise_test(t => websocketTest(t, {
+ source: { server: Server.HTTPS_PRIVATE },
+ target: { server: Server.WSS_LOCAL },
+ expected: WebsocketTestResult.SUCCESS,
+}), "private to local: websocket success.");
+
+promise_test(t => websocketTest(t, {
+ source: { server: Server.HTTPS_PUBLIC },
+ target: { server: Server.WSS_LOCAL },
+ expected: WebsocketTestResult.SUCCESS,
+}), "public to local: websocket success.");
+
+promise_test(t => websocketTest(t, {
+ source: {
+ server: Server.HTTPS_LOCAL,
+ treatAsPublic: true,
+ },
+ target: { server: Server.WSS_LOCAL },
+ expected: WebsocketTestResult.SUCCESS,
+}), "treat-as-public to local: websocket success.");
diff --git a/test/wpt/tests/fetch/private-network-access/websocket.tentative.window.js b/test/wpt/tests/fetch/private-network-access/websocket.tentative.window.js
new file mode 100644
index 0000000..a44cfae
--- /dev/null
+++ b/test/wpt/tests/fetch/private-network-access/websocket.tentative.window.js
@@ -0,0 +1,40 @@
+// META: script=resources/support.sub.js
+//
+// Spec: https://wicg.github.io/private-network-access/#integration-fetch
+
+// These tests verify that websocket connections behave similarly to fetches.
+//
+// This file covers only those tests that must execute in a non secure context.
+// Other tests are defined in: websocket.https.window.js
+
+setup(() => {
+ // Making sure we are in a non secure context, as expected.
+ assert_false(window.isSecureContext);
+});
+
+promise_test(t => websocketTest(t, {
+ source: { server: Server.HTTP_LOCAL },
+ target: { server: Server.WS_LOCAL },
+ expected: WebsocketTestResult.SUCCESS,
+}), "local to local: websocket success.");
+
+promise_test(t => websocketTest(t, {
+ source: { server: Server.HTTP_PRIVATE },
+ target: { server: Server.WS_LOCAL },
+ expected: WebsocketTestResult.FAILURE,
+}), "private to local: websocket failure.");
+
+promise_test(t => websocketTest(t, {
+ source: { server: Server.HTTP_PUBLIC },
+ target: { server: Server.WS_LOCAL },
+ expected: WebsocketTestResult.FAILURE,
+}), "public to local: websocket failure.");
+
+promise_test(t => websocketTest(t, {
+ source: {
+ server: Server.HTTP_LOCAL,
+ treatAsPublic: true,
+ },
+ target: { server: Server.WS_LOCAL },
+ expected: WebsocketTestResult.FAILURE,
+}), "treat-as-public to local: websocket failure.");
diff --git a/test/wpt/tests/fetch/private-network-access/worker-blob-fetch.tentative.window.js b/test/wpt/tests/fetch/private-network-access/worker-blob-fetch.tentative.window.js
new file mode 100644
index 0000000..e119746
--- /dev/null
+++ b/test/wpt/tests/fetch/private-network-access/worker-blob-fetch.tentative.window.js
@@ -0,0 +1,155 @@
+// META: script=/common/utils.js
+// META: script=resources/support.sub.js
+//
+// Spec: https://wicg.github.io/private-network-access/#integration-fetch
+//
+// These tests check that fetches from within `Worker` scripts loaded from blob
+// URLs are subject to Private Network Access checks, just like fetches from
+// within documents.
+//
+// This file covers only those tests that must execute in a non-secure context.
+// Other tests are defined in: worker-blob-fetch.https.window.js
+
+promise_test(t => workerBlobFetchTest(t, {
+ source: { server: Server.HTTP_LOCAL },
+ target: { server: Server.HTTP_LOCAL },
+ expected: WorkerFetchTestResult.SUCCESS,
+}), "local to local: success.");
+
+promise_test(t => workerBlobFetchTest(t, {
+ source: { server: Server.HTTP_PRIVATE },
+ target: {
+ server: Server.HTTP_LOCAL,
+ behavior: {
+ preflight: PreflightBehavior.optionalSuccess(token()),
+ response: ResponseBehavior.allowCrossOrigin(),
+ },
+ },
+ expected: WorkerFetchTestResult.FAILURE,
+}), "private to local: failure.");
+
+promise_test(t => workerBlobFetchTest(t, {
+ source: { server: Server.HTTP_PRIVATE },
+ target: { server: Server.HTTP_PRIVATE },
+ expected: WorkerFetchTestResult.SUCCESS,
+}), "private to private: success.");
+
+promise_test(t => workerBlobFetchTest(t, {
+ source: { server: Server.HTTP_PUBLIC },
+ target: {
+ server: Server.HTTP_LOCAL,
+ behavior: {
+ preflight: PreflightBehavior.optionalSuccess(token()),
+ response: ResponseBehavior.allowCrossOrigin(),
+ },
+ },
+ expected: WorkerFetchTestResult.FAILURE,
+}), "public to local: failure.");
+
+promise_test(t => workerBlobFetchTest(t, {
+ source: { server: Server.HTTP_PUBLIC },
+ target: {
+ server: Server.HTTP_PRIVATE,
+ behavior: {
+ preflight: PreflightBehavior.optionalSuccess(token()),
+ response: ResponseBehavior.allowCrossOrigin(),
+ },
+ },
+ expected: WorkerFetchTestResult.FAILURE,
+}), "public to private: failure.");
+
+promise_test(t => workerBlobFetchTest(t, {
+ source: { server: Server.HTTP_PUBLIC },
+ target: { server: Server.HTTP_PUBLIC },
+ expected: WorkerFetchTestResult.SUCCESS,
+}), "public to public: success.");
+
+promise_test(t => workerBlobFetchTest(t, {
+ source: {
+ server: Server.HTTP_LOCAL,
+ treatAsPublic: true,
+ },
+ target: {
+ server: Server.HTTP_LOCAL,
+ behavior: { preflight: PreflightBehavior.optionalSuccess(token()) },
+ },
+ expected: WorkerFetchTestResult.FAILURE,
+}), "treat-as-public to local: failure.");
+
+promise_test(t => workerBlobFetchTest(t, {
+ source: {
+ server: Server.HTTP_LOCAL,
+ treatAsPublic: true,
+ },
+ target: {
+ server: Server.HTTP_PRIVATE,
+ behavior: {
+ preflight: PreflightBehavior.optionalSuccess(token()),
+ response: ResponseBehavior.allowCrossOrigin(),
+ },
+ },
+ expected: WorkerFetchTestResult.FAILURE,
+}), "treat-as-public to private: failure.");
+
+promise_test(t => workerBlobFetchTest(t, {
+ source: {
+ server: Server.HTTP_LOCAL,
+ treatAsPublic: true,
+ },
+ target: {
+ server: Server.HTTP_PUBLIC,
+ behavior: { response: ResponseBehavior.allowCrossOrigin() },
+ },
+ expected: WorkerFetchTestResult.SUCCESS,
+}), "treat-as-public to public: success.");
+
+// The following tests verify that workers served over HTTPS are not allowed to
+// make private network requests because they are not secure contexts.
+
+promise_test(t => workerBlobFetchTest(t, {
+ source: { server: Server.HTTPS_LOCAL },
+ target: {
+ server: Server.HTTPS_LOCAL,
+ behavior: {
+ preflight: PreflightBehavior.optionalSuccess(token()),
+ response: ResponseBehavior.allowCrossOrigin(),
+ },
+ },
+ expected: WorkerFetchTestResult.SUCCESS,
+}), "local https to local https: success.");
+
+promise_test(t => workerBlobFetchTest(t, {
+ source: { server: Server.HTTPS_PRIVATE },
+ target: {
+ server: Server.HTTPS_LOCAL,
+ behavior: {
+ preflight: PreflightBehavior.optionalSuccess(token()),
+ response: ResponseBehavior.allowCrossOrigin(),
+ },
+ },
+ expected: WorkerFetchTestResult.FAILURE,
+}), "private https to local https: failure.");
+
+promise_test(t => workerBlobFetchTest(t, {
+ source: { server: Server.HTTPS_PUBLIC },
+ target: {
+ server: Server.HTTPS_PRIVATE,
+ behavior: {
+ preflight: PreflightBehavior.optionalSuccess(token()),
+ response: ResponseBehavior.allowCrossOrigin(),
+ },
+ },
+ expected: WorkerFetchTestResult.FAILURE,
+}), "public https to private https: failure.");
+
+promise_test(t => workerBlobFetchTest(t, {
+ source: { server: Server.HTTPS_PUBLIC },
+ target: {
+ server: Server.HTTPS_LOCAL,
+ behavior: {
+ preflight: PreflightBehavior.optionalSuccess(token()),
+ response: ResponseBehavior.allowCrossOrigin(),
+ },
+ },
+ expected: WorkerFetchTestResult.FAILURE,
+}), "public https to local https: failure.");
diff --git a/test/wpt/tests/fetch/private-network-access/worker-fetch.tentative.https.window.js b/test/wpt/tests/fetch/private-network-access/worker-fetch.tentative.https.window.js
new file mode 100644
index 0000000..89e0c3c
--- /dev/null
+++ b/test/wpt/tests/fetch/private-network-access/worker-fetch.tentative.https.window.js
@@ -0,0 +1,151 @@
+// META: script=/common/utils.js
+// META: script=resources/support.sub.js
+//
+// Spec: https://wicg.github.io/private-network-access/#integration-fetch
+//
+// These tests check that fetches from within `Worker` scripts are subject to
+// Private Network Access checks, just like fetches from within documents.
+//
+// This file covers only those tests that must execute in a secure context.
+// Other tests are defined in: worker-fetch.window.js
+
+promise_test(t => workerFetchTest(t, {
+ source: { server: Server.HTTPS_LOCAL },
+ target: { server: Server.HTTPS_LOCAL },
+ expected: WorkerFetchTestResult.SUCCESS,
+}), "local to local: success.");
+
+promise_test(t => workerFetchTest(t, {
+ source: { server: Server.HTTPS_PRIVATE },
+ target: {
+ server: Server.HTTPS_LOCAL,
+ behavior: { response: ResponseBehavior.allowCrossOrigin() },
+ },
+ expected: WorkerFetchTestResult.FAILURE,
+}), "private to local: failed preflight.");
+
+promise_test(t => workerFetchTest(t, {
+ source: { server: Server.HTTPS_PRIVATE },
+ target: {
+ server: Server.HTTPS_LOCAL,
+ behavior: {
+ preflight: PreflightBehavior.success(token()),
+ response: ResponseBehavior.allowCrossOrigin(),
+ },
+ },
+ expected: WorkerFetchTestResult.SUCCESS,
+}), "private to local: success.");
+
+promise_test(t => workerFetchTest(t, {
+ source: { server: Server.HTTPS_PRIVATE },
+ target: { server: Server.HTTPS_PRIVATE },
+ expected: WorkerFetchTestResult.SUCCESS,
+}), "private to private: success.");
+
+promise_test(t => workerFetchTest(t, {
+ source: { server: Server.HTTPS_PUBLIC },
+ target: {
+ server: Server.HTTPS_LOCAL,
+ behavior: { response: ResponseBehavior.allowCrossOrigin() },
+ },
+ expected: WorkerFetchTestResult.FAILURE,
+}), "public to local: failed preflight.");
+
+promise_test(t => workerFetchTest(t, {
+ source: { server: Server.HTTPS_PUBLIC },
+ target: {
+ server: Server.HTTPS_LOCAL,
+ behavior: {
+ preflight: PreflightBehavior.success(token()),
+ response: ResponseBehavior.allowCrossOrigin(),
+ },
+ },
+ expected: WorkerFetchTestResult.SUCCESS,
+}), "public to local: success.");
+
+promise_test(t => workerFetchTest(t, {
+ source: { server: Server.HTTPS_PUBLIC },
+ target: {
+ server: Server.HTTPS_PRIVATE,
+ behavior: { response: ResponseBehavior.allowCrossOrigin() },
+ },
+ expected: WorkerFetchTestResult.FAILURE,
+}), "public to private: failed preflight.");
+
+promise_test(t => workerFetchTest(t, {
+ source: { server: Server.HTTPS_PUBLIC },
+ target: {
+ server: Server.HTTPS_PRIVATE,
+ behavior: {
+ preflight: PreflightBehavior.success(token()),
+ response: ResponseBehavior.allowCrossOrigin(),
+ },
+ },
+ expected: WorkerFetchTestResult.SUCCESS,
+}), "public to private: success.");
+
+promise_test(t => workerFetchTest(t, {
+ source: { server: Server.HTTPS_PUBLIC },
+ target: { server: Server.HTTPS_PUBLIC },
+ expected: WorkerFetchTestResult.SUCCESS,
+}), "public to public: success.");
+
+promise_test(t => workerFetchTest(t, {
+ source: {
+ server: Server.HTTPS_LOCAL,
+ treatAsPublic: true,
+ },
+ target: { server: Server.HTTPS_LOCAL },
+ expected: WorkerFetchTestResult.FAILURE,
+}), "treat-as-public to local: failed preflight.");
+
+promise_test(t => workerFetchTest(t, {
+ source: {
+ server: Server.HTTPS_LOCAL,
+ treatAsPublic: true,
+ },
+ target: {
+ server: Server.HTTPS_LOCAL,
+ behavior: { preflight: PreflightBehavior.optionalSuccess(token()) },
+ },
+ expected: WorkerFetchTestResult.SUCCESS,
+}), "treat-as-public to local: success.");
+
+promise_test(t => workerFetchTest(t, {
+ source: {
+ server: Server.HTTPS_LOCAL,
+ treatAsPublic: true,
+ },
+ target: {
+ server: Server.HTTPS_PRIVATE,
+ behavior: { response: ResponseBehavior.allowCrossOrigin() },
+ },
+ expected: WorkerFetchTestResult.FAILURE,
+}), "treat-as-public to private: failed preflight.");
+
+promise_test(t => workerFetchTest(t, {
+ source: {
+ server: Server.HTTPS_LOCAL,
+ treatAsPublic: true,
+ },
+ target: {
+ server: Server.HTTPS_PRIVATE,
+ behavior: {
+ preflight: PreflightBehavior.success(token()),
+ response: ResponseBehavior.allowCrossOrigin(),
+ },
+ },
+ expected: WorkerFetchTestResult.SUCCESS,
+}), "treat-as-public to private: success.");
+
+promise_test(t => workerFetchTest(t, {
+ source: {
+ server: Server.HTTPS_LOCAL,
+ treatAsPublic: true,
+ },
+ target: {
+ server: Server.HTTPS_PUBLIC,
+ behavior: { response: ResponseBehavior.allowCrossOrigin() },
+ },
+ expected: WorkerFetchTestResult.SUCCESS,
+}), "treat-as-public to public: success.");
diff --git a/test/wpt/tests/fetch/private-network-access/worker-fetch.tentative.window.js b/test/wpt/tests/fetch/private-network-access/worker-fetch.tentative.window.js
new file mode 100644
index 0000000..4d6b12f
--- /dev/null
+++ b/test/wpt/tests/fetch/private-network-access/worker-fetch.tentative.window.js
@@ -0,0 +1,154 @@
+// META: script=/common/utils.js
+// META: script=resources/support.sub.js
+//
+// Spec: https://wicg.github.io/private-network-access/#integration-fetch
+//
+// These tests check that fetches from within `Worker` scripts are subject to
+// Private Network Access checks, just like fetches from within documents.
+//
+// This file covers only those tests that must execute in a non-secure context.
+// Other tests are defined in: worker-fetch.https.window.js
+
+promise_test(t => workerFetchTest(t, {
+ source: { server: Server.HTTP_LOCAL },
+ target: { server: Server.HTTP_LOCAL },
+ expected: WorkerFetchTestResult.SUCCESS,
+}), "local to local: success.");
+
+promise_test(t => workerFetchTest(t, {
+ source: { server: Server.HTTP_PRIVATE },
+ target: {
+ server: Server.HTTP_LOCAL,
+ behavior: {
+ preflight: PreflightBehavior.optionalSuccess(token()),
+ response: ResponseBehavior.allowCrossOrigin(),
+ },
+ },
+ expected: WorkerFetchTestResult.FAILURE,
+}), "private to local: failure.");
+
+promise_test(t => workerFetchTest(t, {
+ source: { server: Server.HTTP_PRIVATE },
+ target: { server: Server.HTTP_PRIVATE },
+ expected: WorkerFetchTestResult.SUCCESS,
+}), "private to private: success.");
+
+promise_test(t => workerFetchTest(t, {
+ source: { server: Server.HTTP_PUBLIC },
+ target: {
+ server: Server.HTTP_LOCAL,
+ behavior: {
+ preflight: PreflightBehavior.optionalSuccess(token()),
+ response: ResponseBehavior.allowCrossOrigin(),
+ },
+ },
+ expected: WorkerFetchTestResult.FAILURE,
+}), "public to local: failure.");
+
+promise_test(t => workerFetchTest(t, {
+ source: { server: Server.HTTP_PUBLIC },
+ target: {
+ server: Server.HTTP_PRIVATE,
+ behavior: {
+ preflight: PreflightBehavior.optionalSuccess(token()),
+ response: ResponseBehavior.allowCrossOrigin(),
+ },
+ },
+ expected: WorkerFetchTestResult.FAILURE,
+}), "public to private: failure.");
+
+promise_test(t => workerFetchTest(t, {
+ source: { server: Server.HTTP_PUBLIC },
+ target: { server: Server.HTTP_PUBLIC },
+ expected: WorkerFetchTestResult.SUCCESS,
+}), "public to public: success.");
+
+promise_test(t => workerFetchTest(t, {
+ source: {
+ server: Server.HTTP_LOCAL,
+ treatAsPublic: true,
+ },
+ target: {
+ server: Server.HTTP_LOCAL,
+ behavior: { preflight: PreflightBehavior.optionalSuccess(token()) },
+ },
+ expected: WorkerFetchTestResult.FAILURE,
+}), "treat-as-public to local: failure.");
+
+promise_test(t => workerFetchTest(t, {
+ source: {
+ server: Server.HTTP_LOCAL,
+ treatAsPublic: true,
+ },
+ target: {
+ server: Server.HTTP_PRIVATE,
+ behavior: {
+ preflight: PreflightBehavior.optionalSuccess(token()),
+ response: ResponseBehavior.allowCrossOrigin(),
+ },
+ },
+ expected: WorkerFetchTestResult.FAILURE,
+}), "treat-as-public to private: failure.");
+
+promise_test(t => workerFetchTest(t, {
+ source: {
+ server: Server.HTTP_LOCAL,
+ treatAsPublic: true,
+ },
+ target: {
+ server: Server.HTTP_PUBLIC,
+ behavior: { response: ResponseBehavior.allowCrossOrigin() },
+ },
+ expected: WorkerFetchTestResult.SUCCESS,
+}), "treat-as-public to public: success.");
+
+// The following tests verify that workers served over HTTPS are not allowed to
+// make private network requests because they are not secure contexts.
+
+promise_test(t => workerFetchTest(t, {
+ source: { server: Server.HTTPS_LOCAL },
+ target: {
+ server: Server.HTTPS_LOCAL,
+ behavior: {
+ preflight: PreflightBehavior.optionalSuccess(token()),
+ response: ResponseBehavior.allowCrossOrigin(),
+ },
+ },
+ expected: WorkerFetchTestResult.SUCCESS,
+}), "local https to local https: success.");
+
+promise_test(t => workerFetchTest(t, {
+ source: { server: Server.HTTPS_PRIVATE },
+ target: {
+ server: Server.HTTPS_LOCAL,
+ behavior: {
+ preflight: PreflightBehavior.optionalSuccess(token()),
+ response: ResponseBehavior.allowCrossOrigin(),
+ },
+ },
+ expected: WorkerFetchTestResult.FAILURE,
+}), "private https to local https: failure.");
+
+promise_test(t => workerFetchTest(t, {
+ source: { server: Server.HTTPS_PUBLIC },
+ target: {
+ server: Server.HTTPS_PRIVATE,
+ behavior: {
+ preflight: PreflightBehavior.optionalSuccess(token()),
+ response: ResponseBehavior.allowCrossOrigin(),
+ },
+ },
+ expected: WorkerFetchTestResult.FAILURE,
+}), "public https to private https: failure.");
+
+promise_test(t => workerFetchTest(t, {
+ source: { server: Server.HTTPS_PUBLIC },
+ target: {
+ server: Server.HTTPS_LOCAL,
+ behavior: {
+ preflight: PreflightBehavior.optionalSuccess(token()),
+ response: ResponseBehavior.allowCrossOrigin(),
+ },
+ },
+ expected: WorkerFetchTestResult.FAILURE,
+}), "public https to local https: failure.");
diff --git a/test/wpt/tests/fetch/private-network-access/worker.tentative.https.window.js b/test/wpt/tests/fetch/private-network-access/worker.tentative.https.window.js
new file mode 100644
index 0000000..a0f1931
--- /dev/null
+++ b/test/wpt/tests/fetch/private-network-access/worker.tentative.https.window.js
@@ -0,0 +1,37 @@
+// META: script=/common/utils.js
+// META: script=resources/support.sub.js
+//
+// Spec: https://wicg.github.io/private-network-access/#integration-fetch
+//
+// These tests check that initial `Worker` script fetches in secure contexts are
+// exempt from Private Network Access checks because workers can only be fetched
+// same-origin and the origin is potentially trustworthy. The only way to test
+// this is using the `treat-as-public` CSP directive to artificially place the
+// parent document in the `public` IP address space.
+//
+// This file covers only those tests that must execute in a secure context.
+// Other tests are defined in: worker.window.js
+
+promise_test(t => workerScriptTest(t, {
+ source: {
+ server: Server.HTTPS_LOCAL,
+ treatAsPublic: true,
+ },
+ target: { server: Server.HTTPS_LOCAL },
+ expected: WorkerScriptTestResult.SUCCESS,
+}), "treat-as-public to local: success.");
+
+promise_test(t => workerScriptTest(t, {
+ source: {
+ server: Server.HTTPS_PRIVATE,
+ treatAsPublic: true,
+ },
+ target: { server: Server.HTTPS_PRIVATE },
+ expected: WorkerScriptTestResult.SUCCESS,
+}), "treat-as-public to private: success.");
+
+promise_test(t => workerScriptTest(t, {
+ source: { server: Server.HTTPS_PUBLIC },
+ target: { server: Server.HTTPS_PUBLIC },
+ expected: WorkerScriptTestResult.SUCCESS,
+}), "public to public: success.");
diff --git a/test/wpt/tests/fetch/private-network-access/worker.tentative.window.js b/test/wpt/tests/fetch/private-network-access/worker.tentative.window.js
new file mode 100644
index 0000000..118c099
--- /dev/null
+++ b/test/wpt/tests/fetch/private-network-access/worker.tentative.window.js
@@ -0,0 +1,37 @@
+// META: script=/common/utils.js
+// META: script=resources/support.sub.js
+//
+// Spec: https://wicg.github.io/private-network-access/#integration-fetch
+//
+// These tests check that initial `Worker` script fetches are subject to Private
+// Network Access checks, just like a regular `fetch()`. The main difference is
+// that workers can only be fetched same-origin, so the only way to test this
+// is using the `treat-as-public` CSP directive to artificially place the parent
+// document in the `public` IP address space.
+//
+// This file covers only those tests that must execute in a non secure context.
+// Other tests are defined in: worker.https.window.js
+
+promise_test(t => workerScriptTest(t, {
+ source: {
+ server: Server.HTTP_LOCAL,
+ treatAsPublic: true,
+ },
+ target: { server: Server.HTTP_LOCAL },
+ expected: WorkerScriptTestResult.FAILURE,
+}), "treat-as-public to local: failure.");
+
+promise_test(t => workerScriptTest(t, {
+ source: {
+ server: Server.HTTP_PRIVATE,
+ treatAsPublic: true,
+ },
+ target: { server: Server.HTTP_PRIVATE },
+ expected: WorkerScriptTestResult.FAILURE,
+}), "treat-as-public to private: failure.");
+
+promise_test(t => workerScriptTest(t, {
+ source: { server: Server.HTTP_PUBLIC },
+ target: { server: Server.HTTP_PUBLIC },
+ expected: WorkerScriptTestResult.SUCCESS,
+}), "public to public: success.");
diff --git a/test/wpt/tests/fetch/private-network-access/xhr-from-treat-as-public.tentative.https.window.js b/test/wpt/tests/fetch/private-network-access/xhr-from-treat-as-public.tentative.https.window.js
new file mode 100644
index 0000000..3aae305
--- /dev/null
+++ b/test/wpt/tests/fetch/private-network-access/xhr-from-treat-as-public.tentative.https.window.js
@@ -0,0 +1,83 @@
+// META: script=/common/utils.js
+// META: script=resources/support.sub.js
+//
+// Spec: https://wicg.github.io/private-network-access/#integration-fetch
+//
+// These tests verify that documents fetched from the `local` address space yet
+// carrying the `treat-as-public-address` CSP directive are treated as if they
+// had been fetched from the `public` address space.
+
+promise_test(t => xhrTest(t, {
+ source: {
+ server: Server.HTTPS_LOCAL,
+ treatAsPublic: true,
+ },
+ target: {
+ server: Server.OTHER_HTTPS_LOCAL,
+ behavior: { response: ResponseBehavior.allowCrossOrigin() },
+ },
+ expected: XhrTestResult.FAILURE,
+}), "treat-as-public to local: failed preflight.");
+
+promise_test(t => xhrTest(t, {
+ source: {
+ server: Server.HTTPS_LOCAL,
+ treatAsPublic: true,
+ },
+ target: {
+ server: Server.OTHER_HTTPS_LOCAL,
+ behavior: {
+ preflight: PreflightBehavior.success(token()),
+ response: ResponseBehavior.allowCrossOrigin(),
+ },
+ },
+ expected: XhrTestResult.SUCCESS,
+}), "treat-as-public to local: success.");
+
+promise_test(t => xhrTest(t, {
+ source: {
+ server: Server.HTTPS_LOCAL,
+ treatAsPublic: true,
+ },
+ target: { server: Server.HTTPS_LOCAL },
+ expected: XhrTestResult.SUCCESS,
+}), "treat-as-public to local (same-origin): no preflight required.");
+
+promise_test(t => xhrTest(t, {
+ source: {
+ server: Server.HTTPS_LOCAL,
+ treatAsPublic: true,
+ },
+ target: {
+ server: Server.HTTPS_PRIVATE,
+ behavior: { response: ResponseBehavior.allowCrossOrigin() },
+ },
+ expected: XhrTestResult.FAILURE,
+}), "treat-as-public to private: failed preflight.");
+
+promise_test(t => xhrTest(t, {
+ source: {
+ server: Server.HTTPS_LOCAL,
+ treatAsPublic: true,
+ },
+ target: {
+ server: Server.HTTPS_PRIVATE,
+ behavior: {
+ preflight: PreflightBehavior.optionalSuccess(token()),
+ response: ResponseBehavior.allowCrossOrigin(),
+ },
+ },
+ expected: XhrTestResult.SUCCESS,
+}), "treat-as-public to private: success.");
+
+promise_test(t => xhrTest(t, {
+ source: {
+ server: Server.HTTPS_LOCAL,
+ treatAsPublic: true,
+ },
+ target: {
+ server: Server.HTTPS_PUBLIC,
+ behavior: { response: ResponseBehavior.allowCrossOrigin() },
+ },
+ expected: XhrTestResult.SUCCESS,
+}), "treat-as-public to public: no preflight required.");
diff --git a/test/wpt/tests/fetch/private-network-access/xhr.https.tentative.window.js b/test/wpt/tests/fetch/private-network-access/xhr.https.tentative.window.js
new file mode 100644
index 0000000..4dc5da9
--- /dev/null
+++ b/test/wpt/tests/fetch/private-network-access/xhr.https.tentative.window.js
@@ -0,0 +1,142 @@
+// META: script=/common/subset-tests-by-key.js
+// META: script=/common/utils.js
+// META: script=resources/support.sub.js
+// META: variant=?include=from-local
+// META: variant=?include=from-private
+// META: variant=?include=from-public
+//
+// Spec: https://wicg.github.io/private-network-access/#integration-fetch
+//
+// These tests mirror fetch.https.window.js, but use `XmlHttpRequest` instead of
+// `fetch()` to perform subresource fetches. Preflights are tested less
+// extensively due to coverage being already provided by `fetch()`.
+//
+// This file covers only those tests that must execute in a secure context.
+// Other tests are defined in: xhr.window.js
+
+setup(() => {
+ // Making sure we are in a secure context, as expected.
+ assert_true(window.isSecureContext);
+});
+
+// Source: secure local context.
+//
+// All fetches unaffected by Private Network Access.
+
+subsetTestByKey("from-local", promise_test, t => xhrTest(t, {
+ source: { server: Server.HTTPS_LOCAL },
+ target: { server: Server.HTTPS_LOCAL },
+ expected: XhrTestResult.SUCCESS,
+}), "local to local: no preflight required.");
+
+subsetTestByKey("from-local", promise_test, t => xhrTest(t, {
+ source: { server: Server.HTTPS_LOCAL },
+ target: {
+ server: Server.HTTPS_PRIVATE,
+ behavior: { response: ResponseBehavior.allowCrossOrigin() },
+ },
+ expected: XhrTestResult.SUCCESS,
+}), "local to private: no preflight required.");
+
+subsetTestByKey("from-local", promise_test, t => xhrTest(t, {
+ source: { server: Server.HTTPS_LOCAL },
+ target: {
+ server: Server.HTTPS_PUBLIC,
+ behavior: { response: ResponseBehavior.allowCrossOrigin() },
+ },
+ expected: XhrTestResult.SUCCESS,
+}), "local to public: no preflight required.");
+
+// Source: private secure context.
+//
+// Fetches to the local address space require a successful preflight response
+// carrying a PNA-specific header.
+
+subsetTestByKey("from-private", promise_test, t => xhrTest(t, {
+ source: { server: Server.HTTPS_PRIVATE },
+ target: {
+ server: Server.HTTPS_LOCAL,
+ behavior: { response: ResponseBehavior.allowCrossOrigin() },
+ },
+ expected: XhrTestResult.FAILURE,
+}), "private to local: failed preflight.");
+
+subsetTestByKey("from-private", promise_test, t => xhrTest(t, {
+ source: { server: Server.HTTPS_PRIVATE },
+ target: {
+ server: Server.HTTPS_LOCAL,
+ behavior: {
+ preflight: PreflightBehavior.success(token()),
+ response: ResponseBehavior.allowCrossOrigin(),
+ },
+ },
+ expected: XhrTestResult.SUCCESS,
+}), "private to local: success.");
+
+subsetTestByKey("from-private", promise_test, t => xhrTest(t, {
+ source: { server: Server.HTTPS_PRIVATE },
+ target: { server: Server.HTTPS_PRIVATE },
+ expected: XhrTestResult.SUCCESS,
+}), "private to private: no preflight required.");
+
+subsetTestByKey("from-private", promise_test, t => xhrTest(t, {
+ source: { server: Server.HTTPS_PRIVATE },
+ target: {
+ server: Server.HTTPS_PUBLIC,
+ behavior: { response: ResponseBehavior.allowCrossOrigin() },
+ },
+ expected: XhrTestResult.SUCCESS,
+}), "private to public: no preflight required.");
+
+// Source: public secure context.
+//
+// Fetches to the local and private address spaces require a successful
+// preflight response carrying a PNA-specific header.
+
+subsetTestByKey("from-public", promise_test, t => xhrTest(t, {
+ source: { server: Server.HTTPS_PUBLIC },
+ target: {
+ server: Server.HTTPS_LOCAL,
+ behavior: { response: ResponseBehavior.allowCrossOrigin() },
+ },
+ expected: XhrTestResult.FAILURE,
+}), "public to local: failed preflight.");
+
+subsetTestByKey("from-public", promise_test, t => xhrTest(t, {
+ source: { server: Server.HTTPS_PUBLIC },
+ target: {
+ server: Server.HTTPS_LOCAL,
+ behavior: {
+ preflight: PreflightBehavior.success(token()),
+ response: ResponseBehavior.allowCrossOrigin(),
+ },
+ },
+ expected: XhrTestResult.SUCCESS,
+}), "public to local: success.");
+
+subsetTestByKey("from-public", promise_test, t => xhrTest(t, {
+ source: { server: Server.HTTPS_PUBLIC },
+ target: {
+ server: Server.HTTPS_PRIVATE,
+ behavior: { response: ResponseBehavior.allowCrossOrigin() },
+ },
+ expected: XhrTestResult.FAILURE,
+}), "public to private: failed preflight.");
+
+subsetTestByKey("from-public", promise_test, t => xhrTest(t, {
+ source: { server: Server.HTTPS_PUBLIC },
+ target: {
+ server: Server.HTTPS_PRIVATE,
+ behavior: {
+ preflight: PreflightBehavior.success(token()),
+ response: ResponseBehavior.allowCrossOrigin(),
+ },
+ },
+ expected: XhrTestResult.SUCCESS,
+}), "public to private: success.");
+
+subsetTestByKey("from-public", promise_test, t => xhrTest(t, {
+ source: { server: Server.HTTPS_PUBLIC },
+ target: { server: Server.HTTPS_PUBLIC },
+ expected: XhrTestResult.SUCCESS,
+}), "public to public: no preflight required.");
diff --git a/test/wpt/tests/fetch/private-network-access/xhr.tentative.window.js b/test/wpt/tests/fetch/private-network-access/xhr.tentative.window.js
new file mode 100644
index 0000000..fa307dc
--- /dev/null
+++ b/test/wpt/tests/fetch/private-network-access/xhr.tentative.window.js
@@ -0,0 +1,195 @@
+// META: script=/common/utils.js
+// META: script=resources/support.sub.js
+//
+// Spec: https://wicg.github.io/private-network-access/#integration-fetch
+//
+// These tests mirror fetch.window.js, but use `XmlHttpRequest` instead of
+// `fetch()` to perform subresource fetches.
+//
+// This file covers only those tests that must execute in a non secure context.
+// Other tests are defined in: xhr.https.window.js
+
+setup(() => {
+ // Making sure we are in a non secure context, as expected.
+ assert_false(window.isSecureContext);
+});
+
+promise_test(t => xhrTest(t, {
+ source: { server: Server.HTTP_LOCAL },
+ target: { server: Server.HTTP_LOCAL },
+ expected: XhrTestResult.SUCCESS,
+}), "local to local: no preflight required.");
+
+promise_test(t => xhrTest(t, {
+ source: { server: Server.HTTP_LOCAL },
+ target: {
+ server: Server.HTTP_PRIVATE,
+ behavior: { response: ResponseBehavior.allowCrossOrigin() },
+ },
+ expected: XhrTestResult.SUCCESS,
+}), "local to private: no preflight required.");
+
+promise_test(t => xhrTest(t, {
+ source: { server: Server.HTTP_LOCAL },
+ target: {
+ server: Server.HTTP_PUBLIC,
+ behavior: { response: ResponseBehavior.allowCrossOrigin() },
+ },
+ expected: XhrTestResult.SUCCESS,
+}), "local to public: no preflight required.");
+
+promise_test(t => xhrTest(t, {
+ source: { server: Server.HTTP_PRIVATE },
+ target: {
+ server: Server.HTTP_LOCAL,
+ behavior: {
+ preflight: PreflightBehavior.optionalSuccess(token()),
+ response: ResponseBehavior.allowCrossOrigin(),
+ },
+ },
+ expected: XhrTestResult.FAILURE,
+}), "private to local: failure.");
+
+promise_test(t => xhrTest(t, {
+ source: { server: Server.HTTP_PRIVATE },
+ target: { server: Server.HTTP_PRIVATE },
+ expected: XhrTestResult.SUCCESS,
+}), "private to private: no preflight required.");
+
+promise_test(t => xhrTest(t, {
+ source: { server: Server.HTTP_PRIVATE },
+ target: {
+ server: Server.HTTP_PUBLIC,
+ behavior: { response: ResponseBehavior.allowCrossOrigin() },
+ },
+ expected: XhrTestResult.SUCCESS,
+}), "private to public: no preflight required.");
+
+promise_test(t => xhrTest(t, {
+ source: { server: Server.HTTP_PUBLIC },
+ target: {
+ server: Server.HTTP_LOCAL,
+ behavior: {
+ preflight: PreflightBehavior.optionalSuccess(token()),
+ response: ResponseBehavior.allowCrossOrigin(),
+ },
+ },
+ expected: XhrTestResult.FAILURE,
+}), "public to local: failure.");
+
+promise_test(t => xhrTest(t, {
+ source: { server: Server.HTTP_PUBLIC },
+ target: {
+ server: Server.HTTP_PRIVATE,
+ behavior: {
+ preflight: PreflightBehavior.optionalSuccess(token()),
+ response: ResponseBehavior.allowCrossOrigin(),
+ },
+ },
+ expected: XhrTestResult.FAILURE,
+}), "public to private: failure.");
+
+promise_test(t => xhrTest(t, {
+ source: { server: Server.HTTP_PUBLIC },
+ target: { server: Server.HTTP_PUBLIC },
+ expected: XhrTestResult.SUCCESS,
+}), "public to public: no preflight required.");
+
+// These tests verify that documents fetched from the `local` address space yet
+// carrying the `treat-as-public-address` CSP directive are treated as if they
+// had been fetched from the `public` address space.
+
+promise_test(t => xhrTest(t, {
+ source: {
+ server: Server.HTTP_LOCAL,
+ treatAsPublic: true,
+ },
+ target: {
+ server: Server.HTTP_LOCAL,
+ behavior: {
+ preflight: PreflightBehavior.optionalSuccess(token()),
+ response: ResponseBehavior.allowCrossOrigin(),
+ },
+ },
+ expected: XhrTestResult.FAILURE,
+}), "treat-as-public-address to local: failure.");
+
+promise_test(t => xhrTest(t, {
+ source: {
+ server: Server.HTTP_LOCAL,
+ treatAsPublic: true,
+ },
+ target: {
+ server: Server.HTTP_PRIVATE,
+ behavior: {
+ preflight: PreflightBehavior.optionalSuccess(token()),
+ response: ResponseBehavior.allowCrossOrigin(),
+ },
+ },
+ expected: XhrTestResult.FAILURE,
+}), "treat-as-public-address to private: failure.");
+
+promise_test(t => xhrTest(t, {
+ source: {
+ server: Server.HTTP_LOCAL,
+ treatAsPublic: true,
+ },
+ target: {
+ server: Server.HTTP_PUBLIC,
+ behavior: { response: ResponseBehavior.allowCrossOrigin() },
+ },
+ expected: XhrTestResult.SUCCESS,
+}), "treat-as-public-address to public: no preflight required.");
+
+// These tests verify that HTTPS iframes embedded in an HTTP top-level document
+// cannot fetch subresources from less-public address spaces. Indeed, even
+// though the iframes have HTTPS origins, they are non-secure contexts because
+// their parent is a non-secure context.
+
+promise_test(t => xhrTest(t, {
+ source: { server: Server.HTTPS_LOCAL },
+ target: {
+ server: Server.HTTPS_LOCAL,
+ behavior: {
+ preflight: PreflightBehavior.optionalSuccess(token()),
+ response: ResponseBehavior.allowCrossOrigin(),
+ },
+ },
+ expected: XhrTestResult.SUCCESS,
+}), "local https to local: success.");
+
+promise_test(t => xhrTest(t, {
+ source: { server: Server.HTTPS_PRIVATE },
+ target: {
+ server: Server.HTTPS_LOCAL,
+ behavior: {
+ preflight: PreflightBehavior.optionalSuccess(token()),
+ response: ResponseBehavior.allowCrossOrigin(),
+ },
+ },
+ expected: XhrTestResult.FAILURE,
+}), "private https to local: failure.");
+
+promise_test(t => xhrTest(t, {
+ source: { server: Server.HTTPS_PUBLIC },
+ target: {
+ server: Server.HTTPS_LOCAL,
+ behavior: {
+ preflight: PreflightBehavior.optionalSuccess(token()),
+ response: ResponseBehavior.allowCrossOrigin(),
+ },
+ },
+ expected: XhrTestResult.FAILURE,
+}), "public https to local: failure.");
+
+promise_test(t => xhrTest(t, {
+ source: { server: Server.HTTPS_PUBLIC },
+ target: {
+ server: Server.HTTPS_PRIVATE,
+ behavior: {
+ preflight: PreflightBehavior.optionalSuccess(token()),
+ response: ResponseBehavior.allowCrossOrigin(),
+ },
+ },
+ expected: XhrTestResult.FAILURE,
+}), "public https to private: failure.");
diff --git a/test/wpt/tests/fetch/range/blob.any.js b/test/wpt/tests/fetch/range/blob.any.js
new file mode 100644
index 0000000..7bcd4b9
--- /dev/null
+++ b/test/wpt/tests/fetch/range/blob.any.js
@@ -0,0 +1,233 @@
+// META: script=/common/utils.js
+
+const supportedBlobRange = [
+ {
+ name: "A simple blob range request.",
+ data: ["A simple Hello, World! example"],
+ type: "text/plain",
+ range: "bytes=9-21",
+ content_length: 13,
+ content_range: "bytes 9-21/30",
+ result: "Hello, World!",
+ },
+ {
+ name: "A blob range request with no type.",
+ data: ["A simple Hello, World! example"],
+ type: undefined,
+ range: "bytes=9-21",
+ content_length: 13,
+ content_range: "bytes 9-21/30",
+ result: "Hello, World!",
+ },
+ {
+ name: "A blob range request with no end.",
+ data: ["Range with no end"],
+ type: "text/plain",
+ range: "bytes=11-",
+ content_length: 6,
+ content_range: "bytes 11-16/17",
+ result: "no end",
+ },
+ {
+ name: "A blob range request with no start.",
+ data: ["Range with no start"],
+ type: "text/plain",
+ range: "bytes=-8",
+ content_length: 8,
+ content_range: "bytes 11-18/19",
+ result: "no start",
+ },
+ {
+ name: "A simple blob range request with whitespace.",
+ data: ["A simple Hello, World! example"],
+ type: "text/plain",
+ range: "bytes= \t9-21",
+ content_length: 13,
+ content_range: "bytes 9-21/30",
+ result: "Hello, World!",
+ },
+ {
+ name: "Blob content with short content and a large range end",
+ data: ["Not much here"],
+ type: "text/plain",
+ range: "bytes=4-100000000000",
+ content_length: 9,
+ content_range: "bytes 4-12/13",
+ result: "much here",
+ },
+ {
+ name: "Blob content with short content and a range end matching content length",
+ data: ["Not much here"],
+ type: "text/plain",
+ range: "bytes=4-13",
+ content_length: 9,
+ content_range: "bytes 4-12/13",
+ result: "much here",
+ },
+ {
+ name: "Blob range with whitespace before and after hyphen",
+ data: ["Valid whitespace #1"],
+ type: "text/plain",
+ range: "bytes=5 - 10",
+ content_length: 6,
+ content_range: "bytes 5-10/19",
+ result: " white",
+ },
+ {
+ name: "Blob range with whitespace after hyphen",
+ data: ["Valid whitespace #2"],
+ type: "text/plain",
+ range: "bytes=-\t 5",
+ content_length: 5,
+ content_range: "bytes 14-18/19",
+ result: "ce #2",
+ },
+ {
+ name: "Blob range with whitespace around equals sign",
+ data: ["Valid whitespace #3"],
+ type: "text/plain",
+ range: "bytes \t =\t 6-",
+ content_length: 13,
+ content_range: "bytes 6-18/19",
+ result: "whitespace #3",
+ },
+];
+
+const unsupportedBlobRange = [
+ {
+ name: "Blob range with no value",
+ data: ["Blob range should have a value"],
+ type: "text/plain",
+ range: "",
+ },
+ {
+ name: "Blob range with incorrect range header",
+ data: ["A"],
+ type: "text/plain",
+ range: "byte=0-"
+ },
+ {
+ name: "Blob range with incorrect range header #2",
+ data: ["A"],
+ type: "text/plain",
+ range: "bytes"
+ },
+ {
+ name: "Blob range with incorrect range header #3",
+ data: ["A"],
+ type: "text/plain",
+ range: "bytes\t \t"
+ },
+ {
+ name: "Blob range request with multiple range values",
+ data: ["Multiple ranges are not currently supported"],
+ type: "text/plain",
+ range: "bytes=0-5,15-",
+ },
+ {
+ name: "Blob range request with multiple range values and whitespace",
+ data: ["Multiple ranges are not currently supported"],
+ type: "text/plain",
+ range: "bytes=0-5, 15-",
+ },
+ {
+ name: "Blob range request with trailing comma",
+ data: ["Range with invalid trailing comma"],
+ type: "text/plain",
+ range: "bytes=0-5,",
+ },
+ {
+ name: "Blob range with no start or end",
+ data: ["Range with no start or end"],
+ type: "text/plain",
+ range: "bytes=-",
+ },
+ {
+ name: "Blob range request with short range end",
+ data: ["Range end should be greater than range start"],
+ type: "text/plain",
+ range: "bytes=10-5",
+ },
+ {
+ name: "Blob range start should be an ASCII digit",
+ data: ["Range start must be an ASCII digit"],
+ type: "text/plain",
+ range: "bytes=x-5",
+ },
+ {
+ name: "Blob range should have a dash",
+ data: ["Blob range should have a dash"],
+ type: "text/plain",
+ range: "bytes=5",
+ },
+ {
+ name: "Blob range end should be an ASCII digit",
+ data: ["Range end must be an ASCII digit"],
+ type: "text/plain",
+ range: "bytes=5-x",
+ },
+ {
+ name: "Blob range should include '-'",
+ data: ["Range end must include '-'"],
+ type: "text/plain",
+ range: "bytes=x",
+ },
+ {
+ name: "Blob range should include '='",
+ data: ["Range end must include '='"],
+ type: "text/plain",
+ range: "bytes 5-",
+ },
+ {
+ name: "Blob range should include 'bytes='",
+ data: ["Range end must include 'bytes='"],
+ type: "text/plain",
+ range: "5-",
+ },
+ {
+ name: "Blob content with short content and a large range start",
+ data: ["Not much here"],
+ type: "text/plain",
+ range: "bytes=100000-",
+ },
+ {
+ name: "Blob content with short content and a range start matching the content length",
+ data: ["Not much here"],
+ type: "text/plain",
+ range: "bytes=13-",
+ },
+];
+
+supportedBlobRange.forEach(({ name, data, type, range, content_length, content_range, result }) => {
+ promise_test(async t => {
+ const blob = new Blob(data, { "type" : type });
+ const blobURL = URL.createObjectURL(blob);
+ t.add_cleanup(() => URL.revokeObjectURL(blobURL));
+ const resp = await fetch(blobURL, {
+ "headers": {
+ "Range": range
+ }
+ });
+ assert_equals(resp.status, 206, "HTTP status is 206");
+ assert_equals(resp.type, "basic", "response type is basic");
+ assert_equals(resp.headers.get("Content-Type"), type || "", "Content-Type is " + resp.headers.get("Content-Type"));
+ assert_equals(resp.headers.get("Content-Length"), content_length.toString(), "Content-Length is " + resp.headers.get("Content-Length"));
+ assert_equals(resp.headers.get("Content-Range"), content_range, "Content-Range is " + resp.headers.get("Content-Range"));
+ const text = await resp.text();
+ assert_equals(text, result, "Response's body is correct");
+ }, name);
+});
+
+unsupportedBlobRange.forEach(({ name, data, type, range }) => {
+ promise_test(t => {
+ const blob = new Blob(data, { "type" : type });
+ const blobURL = URL.createObjectURL(blob);
+ t.add_cleanup(() => URL.revokeObjectURL(blobURL));
+ const promise = fetch(blobURL, {
+ "headers": {
+ "Range": range
+ }
+ });
+ return promise_rejects_js(t, TypeError, promise);
+ }, name);
+});
diff --git a/test/wpt/tests/fetch/range/data.any.js b/test/wpt/tests/fetch/range/data.any.js
new file mode 100644
index 0000000..22ef11e
--- /dev/null
+++ b/test/wpt/tests/fetch/range/data.any.js
@@ -0,0 +1,29 @@
+// META: script=/common/utils.js
+
+promise_test(async () => {
+ return fetch("data:text/plain;charset=US-ASCII,paddingHello%2C%20World%21padding", {
+ "method": "GET",
+ "Range": "bytes=13-26"
+ }).then(function(resp) {
+ assert_equals(resp.status, 200, "HTTP status is 200");
+ assert_equals(resp.type, "basic", "response type is basic");
+ assert_equals(resp.headers.get("Content-Type"), "text/plain;charset=US-ASCII", "Content-Type is " + resp.headers.get("Content-Type"));
+ return resp.text();
+ }).then(function(text) {
+ assert_equals(text, 'paddingHello, World!padding', "Response's body ignores range");
+ });
+}, "data: URL and Range header");
+
+promise_test(async () => {
+ return fetch("data:text/plain;charset=US-ASCII,paddingHello%2C%20paddingWorld%21padding", {
+ "method": "GET",
+ "Range": "bytes=7-14,21-27"
+ }).then(function(resp) {
+ assert_equals(resp.status, 200, "HTTP status is 200");
+ assert_equals(resp.type, "basic", "response type is basic");
+ assert_equals(resp.headers.get("Content-Type"), "text/plain;charset=US-ASCII", "Content-Type is " + resp.headers.get("Content-Type"));
+ return resp.text();
+ }).then(function(text) {
+ assert_equals(text, 'paddingHello, paddingWorld!padding', "Response's body ignores range");
+ });
+}, "data: URL and Range header with multiple ranges");
diff --git a/test/wpt/tests/fetch/range/general.any.js b/test/wpt/tests/fetch/range/general.any.js
new file mode 100644
index 0000000..64b225a
--- /dev/null
+++ b/test/wpt/tests/fetch/range/general.any.js
@@ -0,0 +1,140 @@
+// META: timeout=long
+// META: global=window,worker
+// META: script=/common/get-host-info.sub.js
+// META: script=/common/utils.js
+
+// Helpers that return headers objects with a particular guard
+function headersGuardNone(fill) {
+ if (fill) return new Headers(fill);
+ return new Headers();
+}
+
+function headersGuardResponse(fill) {
+ const opts = {};
+ if (fill) opts.headers = fill;
+ return new Response('', opts).headers;
+}
+
+function headersGuardRequest(fill) {
+ const opts = {};
+ if (fill) opts.headers = fill;
+ return new Request('./', opts).headers;
+}
+
+function headersGuardRequestNoCors(fill) {
+ const opts = { mode: 'no-cors' };
+ if (fill) opts.headers = fill;
+ return new Request('./', opts).headers;
+}
+
+const headerGuardTypes = [
+ ['none', headersGuardNone],
+ ['response', headersGuardResponse],
+ ['request', headersGuardRequest]
+];
+
+for (const [guardType, createHeaders] of headerGuardTypes) {
+ test(() => {
+ // There are three ways to set headers.
+ // Filling, appending, and setting. Test each:
+ let headers = createHeaders({ Range: 'foo' });
+ assert_equals(headers.get('Range'), 'foo');
+
+ headers = createHeaders();
+ headers.append('Range', 'foo');
+ assert_equals(headers.get('Range'), 'foo');
+
+ headers = createHeaders();
+ headers.set('Range', 'foo');
+ assert_equals(headers.get('Range'), 'foo');
+ }, `Range header setting allowed for guard type: ${guardType}`);
+}
+
+test(() => {
+ let headers = headersGuardRequestNoCors({ Range: 'foo' });
+ assert_false(headers.has('Range'));
+
+ headers = headersGuardRequestNoCors();
+ headers.append('Range', 'foo');
+ assert_false(headers.has('Range'));
+
+ headers = headersGuardRequestNoCors();
+ headers.set('Range', 'foo');
+ assert_false(headers.has('Range'));
+}, `Privileged header not allowed for guard type: request-no-cors`);
+
+promise_test(async () => {
+ const wavURL = new URL('resources/long-wav.py', location);
+ const stashTakeURL = new URL('resources/stash-take.py', location);
+
+ function changeToken() {
+ const stashToken = token();
+ wavURL.searchParams.set('accept-encoding-key', stashToken);
+ stashTakeURL.searchParams.set('key', stashToken);
+ }
+
+ const rangeHeaders = [
+ 'bytes=0-10',
+ 'foo=0-10',
+ 'foo',
+ ''
+ ];
+
+ for (const rangeHeader of rangeHeaders) {
+ changeToken();
+
+ await fetch(wavURL, {
+ headers: { Range: rangeHeader }
+ });
+
+ const response = await fetch(stashTakeURL);
+
+ assert_regexp_match(await response.json(),
+ /.*\bidentity\b.*/,
+ `Expect identity accept-encoding if range header is ${JSON.stringify(rangeHeader)}`);
+ }
+}, `Fetch with range header will be sent with Accept-Encoding: identity`);
+
+promise_test(async () => {
+ const wavURL = new URL(get_host_info().HTTP_REMOTE_ORIGIN + '/fetch/range/resources/long-wav.py');
+ const stashTakeURL = new URL('resources/stash-take.py', location);
+
+ function changeToken() {
+ const stashToken = token();
+ wavURL.searchParams.set('accept-encoding-key', stashToken);
+ stashTakeURL.searchParams.set('key', stashToken);
+ }
+
+ const rangeHeaders = [
+ 'bytes=10-9',
+ 'bytes=-0',
+ 'bytes=0000000000000000000000000000000000000000000000000000000000011-0000000000000000000000000000000000000000000000000000000000111',
+ ];
+
+ for (const rangeHeader of rangeHeaders) {
+ changeToken();
+ await fetch(wavURL, { headers: { Range : rangeHeader} }).then(() => { throw "loaded with range header " + rangeHeader }, () => { });
+ }
+}, `Cross Origin Fetch with non safe range header`);
+
+promise_test(async () => {
+ const wavURL = new URL(get_host_info().HTTP_REMOTE_ORIGIN + '/fetch/range/resources/long-wav.py');
+ const stashTakeURL = new URL('resources/stash-take.py', location);
+
+ function changeToken() {
+ const stashToken = token();
+ wavURL.searchParams.set('accept-encoding-key', stashToken);
+ stashTakeURL.searchParams.set('key', stashToken);
+ }
+
+ const rangeHeaders = [
+ 'bytes=0-10',
+ 'bytes=0-',
+ 'bytes=00000000000000000000000000000000000000000000000000000000011-00000000000000000000000000000000000000000000000000000000000111',
+ ];
+
+ for (const rangeHeader of rangeHeaders) {
+ changeToken();
+ await fetch(wavURL, { headers: { Range: rangeHeader } }).then(() => { }, () => { throw "failed load with range header " + rangeHeader });
+ }
+}, `Cross Origin Fetch with safe range header`);
diff --git a/test/wpt/tests/fetch/range/general.window.js b/test/wpt/tests/fetch/range/general.window.js
new file mode 100644
index 0000000..afe80d6
--- /dev/null
+++ b/test/wpt/tests/fetch/range/general.window.js
@@ -0,0 +1,29 @@
+// META: script=resources/utils.js
+// META: script=/common/utils.js
+
+const onload = new Promise(r => window.addEventListener('load', r));
+
+// It's weird that browsers do this, but it should continue to work.
+promise_test(async t => {
+ await loadScript('resources/partial-script.py?pretend-offset=90000');
+ assert_true(self.scriptExecuted);
+}, `Script executed from partial response`);
+
+promise_test(async () => {
+ const wavURL = new URL('resources/long-wav.py', location);
+ const stashTakeURL = new URL('resources/stash-take.py', location);
+ const stashToken = token();
+ wavURL.searchParams.set('accept-encoding-key', stashToken);
+ stashTakeURL.searchParams.set('key', stashToken);
+
+ // The testing framework waits for window onload. If the audio element
+ // is appended before onload, it extends it, and the test times out.
+ await onload;
+
+ const audio = appendAudio(document, wavURL);
+ await new Promise(r => audio.addEventListener('progress', r));
+ audio.remove();
+
+ const response = await fetch(stashTakeURL);
+ assert_equals(await response.json(), 'identity', `Expect identity accept-encoding on media request`);
+}, `Fetch with range header will be sent with Accept-Encoding: identity`);
diff --git a/test/wpt/tests/fetch/range/non-matching-range-response.html b/test/wpt/tests/fetch/range/non-matching-range-response.html
new file mode 100644
index 0000000..ba76c36
--- /dev/null
+++ b/test/wpt/tests/fetch/range/non-matching-range-response.html
@@ -0,0 +1,34 @@
+<!DOCTYPE html>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<body>
+<script>
+function range_rewrite_test(rewrites, expect, label) {
+ promise_test(async t => {
+ const url = new URL('resources/video-with-range.py', location.href);
+ const params = new URLSearchParams();
+ params.set('rewrites', JSON.stringify(rewrites));
+ url.search = params.toString();
+ const video = document.createElement('video');
+ video.autoplay = true;
+ video.muted = true;
+ video.src = url.toString();
+ const timeout = new Promise(resolve => t.step_timeout(() => resolve('timeout'), 10000));
+ const ok = new Promise(resolve => video.addEventListener('play', () => resolve('ok')));
+ t.add_cleanup(() => video.remove());
+ document.body.appendChild(video);
+ const result = await Promise.any([timeout, ok]);
+ assert_equals(result, 'ok');
+ }, `${label} should ${expect === 'ok' ? 'succeed' : 'fail'}`);
+}
+
+range_rewrite_test([], 'ok', 'Range requests with no rewrites');
+range_rewrite_test(
+ [
+ {request: ['0', '*'], response: [0, 100]},
+ {request: ['100', '*'], response: [50, 2000]}
+ ], 'ok', 'Range response out of range of request');
+range_rewrite_test([{request: ['0', '*'], status: 200}], 'ok', 'Range requests ignored (200 status)');
+</script>
+</body>
diff --git a/test/wpt/tests/fetch/range/resources/basic.html b/test/wpt/tests/fetch/range/resources/basic.html
new file mode 100644
index 0000000..0e76edd
--- /dev/null
+++ b/test/wpt/tests/fetch/range/resources/basic.html
@@ -0,0 +1 @@
+<!DOCTYPE html>
diff --git a/test/wpt/tests/fetch/range/resources/long-wav.py b/test/wpt/tests/fetch/range/resources/long-wav.py
new file mode 100644
index 0000000..acfc81a
--- /dev/null
+++ b/test/wpt/tests/fetch/range/resources/long-wav.py
@@ -0,0 +1,134 @@
+"""
+This generates a 30 minute silent wav, and is capable of
+responding to Range requests.
+"""
+import time
+import re
+import struct
+
+from wptserve.utils import isomorphic_decode
+
+def create_wav_header(sample_rate, bit_depth, channels, duration):
+ bytes_per_sample = int(bit_depth / 8)
+ block_align = bytes_per_sample * channels
+ byte_rate = sample_rate * block_align
+ sub_chunk_2_size = duration * byte_rate
+
+ data = b''
+ # ChunkID
+ data += b'RIFF'
+ # ChunkSize
+ data += struct.pack('<L', 36 + sub_chunk_2_size)
+ # Format
+ data += b'WAVE'
+ # Subchunk1ID
+ data += b'fmt '
+ # Subchunk1Size
+ data += struct.pack('<L', 16)
+ # AudioFormat
+ data += struct.pack('<H', 1)
+ # NumChannels
+ data += struct.pack('<H', channels)
+ # SampleRate
+ data += struct.pack('<L', sample_rate)
+ # ByteRate
+ data += struct.pack('<L', byte_rate)
+ # BlockAlign
+ data += struct.pack('<H', block_align)
+ # BitsPerSample
+ data += struct.pack('<H', bit_depth)
+ # Subchunk2ID
+ data += b'data'
+ # Subchunk2Size
+ data += struct.pack('<L', sub_chunk_2_size)
+
+ return data
+
+
+def main(request, response):
+ if request.method == u"OPTIONS":
+ response.status = (404, b"Not Found")
+ response.headers.set(b"Content-Type", b"text/plain")
+ return b"Preflight not accepted"
+
+ response.headers.set(b"Content-Type", b"audio/wav")
+ response.headers.set(b"Accept-Ranges", b"bytes")
+ response.headers.set(b"Cache-Control", b"no-cache")
+ response.headers.set(b"Access-Control-Allow-Origin", request.headers.get(b'Origin', b''))
+
+ range_header = request.headers.get(b'Range', b'')
+ range_header_match = range_header and re.search(r'^bytes=(\d*)-(\d*)$', isomorphic_decode(range_header))
+ range_received_key = request.GET.first(b'range-received-key', b'')
+ accept_encoding_key = request.GET.first(b'accept-encoding-key', b'')
+
+ if range_received_key and range_header:
+ # Remove any current value
+ request.server.stash.take(range_received_key, b'/fetch/range/')
+ # This is later collected using stash-take.py
+ request.server.stash.put(range_received_key, u'range-header-received', b'/fetch/range/')
+
+ if accept_encoding_key:
+ # Remove any current value
+ request.server.stash.take(
+ accept_encoding_key,
+ b'/fetch/range/'
+ )
+ # This is later collected using stash-take.py
+ request.server.stash.put(
+ accept_encoding_key,
+ isomorphic_decode(request.headers.get(b'Accept-Encoding', b'')),
+ b'/fetch/range/'
+ )
+
+ # Audio details
+ sample_rate = 8000
+ bit_depth = 8
+ channels = 1
+ duration = 60 * 5
+
+ total_length = int((sample_rate * bit_depth * channels * duration) / 8)
+ bytes_remaining_to_send = total_length
+ initial_write = b''
+
+ if range_header_match:
+ response.status = 206
+ start, end = range_header_match.groups()
+
+ start = int(start)
+ end = int(end) if end else 0
+
+ if end:
+ bytes_remaining_to_send = (end + 1) - start
+ else:
+ bytes_remaining_to_send = total_length - start
+
+ wav_header = create_wav_header(sample_rate, bit_depth, channels, duration)
+
+ if start < len(wav_header):
+ initial_write = wav_header[start:]
+
+ if bytes_remaining_to_send < len(initial_write):
+ initial_write = initial_write[0:bytes_remaining_to_send]
+
+ content_range = b"bytes %d-%d/%d" % (start, end or total_length - 1, total_length)
+
+ response.headers.set(b"Content-Range", content_range)
+ else:
+ initial_write = create_wav_header(sample_rate, bit_depth, channels, duration)
+
+ response.headers.set(b"Content-Length", bytes_remaining_to_send)
+
+ response.write_status_headers()
+ response.writer.write(initial_write)
+
+ bytes_remaining_to_send -= len(initial_write)
+
+ while bytes_remaining_to_send > 0:
+ to_send = b'\x00' * min(bytes_remaining_to_send, sample_rate)
+ bytes_remaining_to_send -= len(to_send)
+
+ if not response.writer.write(to_send):
+ break
+
+ # Throttle the stream
+ time.sleep(0.5)
diff --git a/test/wpt/tests/fetch/range/resources/partial-script.py b/test/wpt/tests/fetch/range/resources/partial-script.py
new file mode 100644
index 0000000..a9570ec
--- /dev/null
+++ b/test/wpt/tests/fetch/range/resources/partial-script.py
@@ -0,0 +1,29 @@
+"""
+This generates a partial response containing valid JavaScript.
+"""
+
+def main(request, response):
+ require_range = request.GET.first(b'require-range', b'')
+ pretend_offset = int(request.GET.first(b'pretend-offset', b'0'))
+ range_header = request.headers.get(b'Range', b'')
+
+ if require_range and not range_header:
+ response.set_error(412, u"Range header required")
+ response.write()
+ return
+
+ response.headers.set(b"Content-Type", b"text/plain")
+ response.headers.set(b"Accept-Ranges", b"bytes")
+ response.headers.set(b"Cache-Control", b"no-cache")
+ response.status = 206
+
+ to_send = b'self.scriptExecuted = true;'
+ length = len(to_send)
+
+ content_range = b"bytes %d-%d/%d" % (
+ pretend_offset, pretend_offset + length - 1, pretend_offset + length)
+
+ response.headers.set(b"Content-Range", content_range)
+ response.headers.set(b"Content-Length", length)
+
+ response.content = to_send
diff --git a/test/wpt/tests/fetch/range/resources/partial-text.py b/test/wpt/tests/fetch/range/resources/partial-text.py
new file mode 100644
index 0000000..fa3d117
--- /dev/null
+++ b/test/wpt/tests/fetch/range/resources/partial-text.py
@@ -0,0 +1,53 @@
+"""
+This generates a partial response for a 100-byte text file.
+"""
+import re
+
+from wptserve.utils import isomorphic_decode
+
+def main(request, response):
+ total_length = int(request.GET.first(b'length', b'100'))
+ partial_code = int(request.GET.first(b'partial', b'206'))
+ content_type = request.GET.first(b'type', b'text/plain')
+ range_header = request.headers.get(b'Range', b'')
+
+ # Send a 200 if there is no range request
+ if not range_header:
+ to_send = ''.zfill(total_length)
+ response.headers.set(b"Content-Type", content_type)
+ response.headers.set(b"Cache-Control", b"no-cache")
+ response.headers.set(b"Content-Length", total_length)
+ response.content = to_send
+ return
+
+ # Simple range parsing, requires specifically "bytes=xxx-xxxx"
+ range_header_match = re.search(r'^bytes=(\d*)-(\d*)$', isomorphic_decode(range_header))
+ start, end = range_header_match.groups()
+ start = int(start)
+ end = int(end) if end else total_length
+ length = end - start
+
+ # Error the request if the range goes beyond the length
+ if length <= 0 or end > total_length:
+ response.set_error(416, u"Range Not Satisfiable")
+ # set_error sets the MIME type to application/json, which - for a
+ # no-cors media request - will be blocked by ORB. We'll just force
+ # the expected MIME type here, whichfixes the test, but doesn't make
+ # sense in general.
+ response.headers = [(b"Content-Type", content_type)]
+ response.write()
+ return
+
+ # Generate a partial response of the requested length
+ to_send = ''.zfill(length)
+ response.headers.set(b"Content-Type", content_type)
+ response.headers.set(b"Accept-Ranges", b"bytes")
+ response.headers.set(b"Cache-Control", b"no-cache")
+ response.status = partial_code
+
+ content_range = b"bytes %d-%d/%d" % (start, end, total_length)
+
+ response.headers.set(b"Content-Range", content_range)
+ response.headers.set(b"Content-Length", length)
+
+ response.content = to_send
diff --git a/test/wpt/tests/fetch/range/resources/range-sw.js b/test/wpt/tests/fetch/range/resources/range-sw.js
new file mode 100644
index 0000000..b47823f
--- /dev/null
+++ b/test/wpt/tests/fetch/range/resources/range-sw.js
@@ -0,0 +1,218 @@
+importScripts('/resources/testharness.js');
+
+setup({ explicit_done: true });
+
+function assert_range_request(request, expectedRangeHeader, name) {
+ assert_equals(request.headers.get('Range'), expectedRangeHeader, name);
+}
+
+async function broadcast(msg) {
+ for (const client of await clients.matchAll()) {
+ client.postMessage(msg);
+ }
+}
+
+addEventListener('fetch', async event => {
+ /** @type Request */
+ const request = event.request;
+ const url = new URL(request.url);
+ const action = url.searchParams.get('action');
+
+ switch (action) {
+ case 'range-header-filter-test':
+ rangeHeaderFilterTest(request);
+ return;
+ case 'range-header-passthrough-test':
+ rangeHeaderPassthroughTest(event);
+ return;
+ case 'store-ranged-response':
+ storeRangedResponse(event);
+ return;
+ case 'use-stored-ranged-response':
+ useStoredRangeResponse(event);
+ return;
+ case 'broadcast-accept-encoding':
+ broadcastAcceptEncoding(event);
+ return;
+ case 'record-media-range-request':
+ return recordMediaRangeRequest(event);
+ case 'use-media-range-request':
+ useMediaRangeRequest(event);
+ return;
+ }
+});
+
+/**
+ * @param {Request} request
+ */
+function rangeHeaderFilterTest(request) {
+ const rangeValue = request.headers.get('Range');
+
+ test(() => {
+ assert_range_request(new Request(request), rangeValue, `Untampered`);
+ assert_range_request(new Request(request, {}), rangeValue, `Untampered (no init props set)`);
+ assert_range_request(new Request(request, { __foo: 'bar' }), rangeValue, `Untampered (only invalid props set)`);
+ assert_range_request(new Request(request, { mode: 'cors' }), rangeValue, `More permissive mode`);
+ assert_range_request(request.clone(), rangeValue, `Clone`);
+ }, "Range headers correctly preserved");
+
+ test(() => {
+ assert_range_request(new Request(request, { headers: { Range: 'foo' } }), null, `Tampered - range header set`);
+ assert_range_request(new Request(request, { headers: {} }), null, `Tampered - empty headers set`);
+ assert_range_request(new Request(request, { mode: 'no-cors' }), null, `Tampered – mode set`);
+ assert_range_request(new Request(request, { cache: 'no-cache' }), null, `Tampered – cache mode set`);
+ }, "Range headers correctly removed");
+
+ test(() => {
+ let headers;
+
+ headers = new Request(request).headers;
+ headers.delete('does-not-exist');
+ assert_equals(headers.get('Range'), rangeValue, `Preserved if no header actually removed`);
+
+ headers = new Request(request).headers;
+ headers.append('foo', 'bar');
+ assert_equals(headers.get('Range'), rangeValue, `Preserved if silent-failure on append (due to request-no-cors guard)`);
+
+ headers = new Request(request).headers;
+ headers.set('foo', 'bar');
+ assert_equals(headers.get('Range'), rangeValue, `Preserved if silent-failure on set (due to request-no-cors guard)`);
+
+ headers = new Request(request).headers;
+ headers.append('Range', 'foo');
+ assert_equals(headers.get('Range'), rangeValue, `Preserved if silent-failure on append (due to request-no-cors guard)`);
+
+ headers = new Request(request).headers;
+ headers.set('Range', 'foo');
+ assert_equals(headers.get('Range'), rangeValue, `Preserved if silent-failure on set (due to request-no-cors guard)`);
+
+ headers = new Request(request).headers;
+ headers.append('Accept', 'whatever');
+ assert_equals(headers.get('Range'), null, `Stripped if header successfully appended`);
+
+ headers = new Request(request).headers;
+ headers.set('Accept', 'whatever');
+ assert_equals(headers.get('Range'), null, `Stripped if header successfully set`);
+
+ headers = new Request(request).headers;
+ headers.delete('Accept');
+ assert_equals(headers.get('Range'), null, `Stripped if header successfully deleted`);
+
+ headers = new Request(request).headers;
+ headers.delete('Range');
+ assert_equals(headers.get('Range'), null, `Stripped if range header successfully deleted`);
+ }, "Headers correctly filtered");
+
+ done();
+}
+
+function rangeHeaderPassthroughTest(event) {
+ /** @type Request */
+ const request = event.request;
+ const url = new URL(request.url);
+ const key = url.searchParams.get('range-received-key');
+
+ event.waitUntil(new Promise(resolve => {
+ promise_test(async () => {
+ await fetch(event.request);
+ const response = await fetch('stash-take.py?key=' + key);
+ assert_equals(await response.json(), 'range-header-received');
+ resolve();
+ }, `Include range header in network request`);
+
+ done();
+ }));
+
+ // Just send back any response, it isn't important for the test.
+ event.respondWith(new Response(''));
+}
+
+let storedRangeResponseP;
+
+function storeRangedResponse(event) {
+ /** @type Request */
+ const request = event.request;
+ const id = new URL(request.url).searchParams.get('id');
+
+ storedRangeResponseP = fetch(event.request);
+ broadcast({ id });
+
+ // Just send back any response, it isn't important for the test.
+ event.respondWith(new Response(''));
+}
+
+function useStoredRangeResponse(event) {
+ event.respondWith(async function() {
+ const response = await storedRangeResponseP;
+ if (!response) throw Error("Expected stored range response");
+ return response.clone();
+ }());
+}
+
+function broadcastAcceptEncoding(event) {
+ /** @type Request */
+ const request = event.request;
+ const id = new URL(request.url).searchParams.get('id');
+
+ broadcast({
+ id,
+ acceptEncoding: request.headers.get('Accept-Encoding')
+ });
+
+ // Just send back any response, it isn't important for the test.
+ event.respondWith(new Response(''));
+}
+
+let rangeResponse = {};
+
+async function recordMediaRangeRequest(event) {
+ /** @type Request */
+ const request = event.request;
+ const url = new URL(request.url);
+ const urlParams = new URLSearchParams(url.search);
+ const size = urlParams.get("size");
+ const id = urlParams.get('id');
+ const key = 'size' + size;
+
+ if (key in rangeResponse) {
+ // Don't re-fetch ranges we already have.
+ const clonedResponse = rangeResponse[key].clone();
+ event.respondWith(clonedResponse);
+ } else if (event.request.headers.get("range") === "bytes=0-") {
+ // Generate a bogus 206 response to trigger subsequent range requests
+ // of the desired size.
+ const length = urlParams.get("length") + 100;
+ const body = "A".repeat(Number(size));
+ event.respondWith(new Response(body, {status: 206, headers: {
+ "Content-Type": "audio/mp4",
+ "Content-Range": `bytes 0-1/${length}`
+ }}));
+ } else if (event.request.headers.get("range") === `bytes=${Number(size)}-`) {
+ // Pass through actual range requests which will attempt to fetch up to the
+ // length in the original response which is bigger than the actual resource
+ // to make sure 206 and 416 responses are treated the same.
+ rangeResponse[key] = await fetch(event.request);
+
+ // Let the client know we have the range response for the given ID
+ broadcast({id});
+ } else {
+ event.respondWith(Promise.reject(Error("Invalid Request")));
+ }
+}
+
+function useMediaRangeRequest(event) {
+ /** @type Request */
+ const request = event.request;
+ const url = new URL(request.url);
+ const urlParams = new URLSearchParams(url.search);
+ const size = urlParams.get("size");
+ const key = 'size' + size;
+
+ // Send a clone of the range response to preload.
+ if (key in rangeResponse) {
+ const clonedResponse = rangeResponse[key].clone();
+ event.respondWith(clonedResponse);
+ } else {
+ event.respondWith(Promise.reject(Error("Invalid Request")));
+ }
+}
diff --git a/test/wpt/tests/fetch/range/resources/stash-take.py b/test/wpt/tests/fetch/range/resources/stash-take.py
new file mode 100644
index 0000000..6cf6ff5
--- /dev/null
+++ b/test/wpt/tests/fetch/range/resources/stash-take.py
@@ -0,0 +1,7 @@
+from wptserve.handlers import json_handler
+
+
+@json_handler
+def main(request, response):
+ key = request.GET.first(b"key")
+ return request.server.stash.take(key, b'/fetch/range/')
diff --git a/test/wpt/tests/fetch/range/resources/utils.js b/test/wpt/tests/fetch/range/resources/utils.js
new file mode 100644
index 0000000..ad2853b
--- /dev/null
+++ b/test/wpt/tests/fetch/range/resources/utils.js
@@ -0,0 +1,36 @@
+function loadScript(url, { doc = document }={}) {
+ return new Promise((resolve, reject) => {
+ const script = doc.createElement('script');
+ script.onload = () => resolve();
+ script.onerror = () => reject(Error("Script load failed"));
+ script.src = url;
+ doc.body.appendChild(script);
+ })
+}
+
+function preloadImage(url, { doc = document }={}) {
+ return new Promise((resolve, reject) => {
+ const preload = doc.createElement('link');
+ preload.rel = 'preload';
+ preload.as = 'image';
+ preload.onload = () => resolve();
+ preload.onerror = () => resolve();
+ preload.href = url;
+ doc.body.appendChild(preload);
+ })
+}
+
+/**
+ *
+ * @param {Document} document
+ * @param {string|URL} url
+ * @returns {HTMLAudioElement}
+ */
+function appendAudio(document, url) {
+ const audio = document.createElement('audio');
+ audio.muted = true;
+ audio.src = url;
+ audio.preload = true;
+ document.body.appendChild(audio);
+ return audio;
+}
diff --git a/test/wpt/tests/fetch/range/resources/video-with-range.py b/test/wpt/tests/fetch/range/resources/video-with-range.py
new file mode 100644
index 0000000..2d15ccf
--- /dev/null
+++ b/test/wpt/tests/fetch/range/resources/video-with-range.py
@@ -0,0 +1,43 @@
+import re
+import os
+import json
+from wptserve.utils import isomorphic_decode
+
+def main(request, response):
+ path = os.path.join(request.doc_root, u"media", "sine440.mp3")
+ total_size = os.path.getsize(path)
+ rewrites = json.loads(request.GET.first(b'rewrites', '[]'))
+ range_header = request.headers.get(b'Range')
+ range_header_match = range_header and re.search(r'^bytes=(\d*)-(\d*)$', isomorphic_decode(range_header))
+ start = None
+ end = None
+ if range_header_match:
+ response.status = 206
+ start, end = range_header_match.groups()
+ if range_header:
+ status = 206
+ else:
+ status = 200
+ for rewrite in rewrites:
+ req_start, req_end = rewrite['request']
+ if start == req_start or req_start == '*':
+ if end == req_end or req_end == '*':
+ if 'response' in rewrite:
+ start, end = rewrite['response']
+ if 'status' in rewrite:
+ status = rewrite['status']
+
+ start = int(start or 0)
+ end = int(end or total_size)
+ headers = []
+ if status == 206:
+ headers.append((b"Content-Range", b"bytes %d-%d/%d" % (start, end - 1, total_size)))
+ headers.append((b"Accept-Ranges", b"bytes"))
+
+ headers.append((b"Content-Type", b"audio/mp3"))
+ headers.append((b"Content-Length", str(end - start)))
+ headers.append((b"Cache-Control", b"no-cache"))
+ video_file = open(path, "rb")
+ video_file.seek(start)
+ content = video_file.read(end)
+ return status, headers, content
diff --git a/test/wpt/tests/fetch/range/sw.https.window.js b/test/wpt/tests/fetch/range/sw.https.window.js
new file mode 100644
index 0000000..62ad894
--- /dev/null
+++ b/test/wpt/tests/fetch/range/sw.https.window.js
@@ -0,0 +1,228 @@
+// META: script=../../../service-workers/service-worker/resources/test-helpers.sub.js
+// META: script=/common/utils.js
+// META: script=/common/get-host-info.sub.js
+// META: script=resources/utils.js
+
+const { REMOTE_HOST } = get_host_info();
+const BASE_SCOPE = 'resources/basic.html?';
+
+async function cleanup() {
+ for (const iframe of document.querySelectorAll('.test-iframe')) {
+ iframe.parentNode.removeChild(iframe);
+ }
+
+ for (const reg of await navigator.serviceWorker.getRegistrations()) {
+ await reg.unregister();
+ }
+}
+
+async function setupRegistration(t, scope) {
+ await cleanup();
+ const reg = await navigator.serviceWorker.register('resources/range-sw.js', { scope });
+ await wait_for_state(t, reg.installing, 'activated');
+ return reg;
+}
+
+function awaitMessage(obj, id) {
+ return new Promise(resolve => {
+ obj.addEventListener('message', function listener(event) {
+ if (event.data.id !== id) return;
+ obj.removeEventListener('message', listener);
+ resolve(event.data);
+ });
+ });
+}
+
+promise_test(async t => {
+ const scope = BASE_SCOPE + Math.random();
+ const reg = await setupRegistration(t, scope);
+ const iframe = await with_iframe(scope);
+ const w = iframe.contentWindow;
+
+ // Trigger a cross-origin range request using media
+ const url = new URL('long-wav.py?action=range-header-filter-test', w.location);
+ url.hostname = REMOTE_HOST;
+ appendAudio(w.document, url);
+
+ // See rangeHeaderFilterTest in resources/range-sw.js
+ await fetch_tests_from_worker(reg.active);
+}, `Defer range header filter tests to service worker`);
+
+promise_test(async t => {
+ const scope = BASE_SCOPE + Math.random();
+ const reg = await setupRegistration(t, scope);
+ const iframe = await with_iframe(scope);
+ const w = iframe.contentWindow;
+
+ // Trigger a cross-origin range request using media
+ const url = new URL('long-wav.py', w.location);
+ url.searchParams.set('action', 'range-header-passthrough-test');
+ url.searchParams.set('range-received-key', token());
+ url.hostname = REMOTE_HOST;
+ appendAudio(w.document, url);
+
+ // See rangeHeaderPassthroughTest in resources/range-sw.js
+ await fetch_tests_from_worker(reg.active);
+}, `Defer range header passthrough tests to service worker`);
+
+promise_test(async t => {
+ const scope = BASE_SCOPE + Math.random();
+ await setupRegistration(t, scope);
+ const iframe = await with_iframe(scope);
+ const w = iframe.contentWindow;
+ const id = Math.random() + '';
+ const storedRangeResponse = awaitMessage(w.navigator.serviceWorker, id);
+
+ // Trigger a cross-origin range request using media
+ const url = new URL('partial-script.py', w.location);
+ url.searchParams.set('require-range', '1');
+ url.searchParams.set('action', 'store-ranged-response');
+ url.searchParams.set('id', id);
+ url.hostname = REMOTE_HOST;
+
+ appendAudio(w.document, url);
+
+ await storedRangeResponse;
+
+ // Fetching should reject
+ const fetchPromise = w.fetch('?action=use-stored-ranged-response', { mode: 'no-cors' });
+ await promise_rejects_js(t, w.TypeError, fetchPromise);
+
+ // Script loading should error too
+ const loadScriptPromise = loadScript('?action=use-stored-ranged-response', { doc: w.document });
+ await promise_rejects_js(t, Error, loadScriptPromise);
+
+ await loadScriptPromise.catch(() => {});
+
+ assert_false(!!w.scriptExecuted, `Partial response shouldn't be executed`);
+}, `Ranged response not allowed following no-cors ranged request`);
+
+promise_test(async t => {
+ const scope = BASE_SCOPE + Math.random();
+ await setupRegistration(t, scope);
+ const iframe = await with_iframe(scope);
+ const w = iframe.contentWindow;
+ const id = Math.random() + '';
+ const storedRangeResponse = awaitMessage(w.navigator.serviceWorker, id);
+
+ // Trigger a range request using media
+ const url = new URL('partial-script.py', w.location);
+ url.searchParams.set('require-range', '1');
+ url.searchParams.set('action', 'store-ranged-response');
+ url.searchParams.set('id', id);
+
+ appendAudio(w.document, url);
+
+ await storedRangeResponse;
+
+ // This should not throw
+ await w.fetch('?action=use-stored-ranged-response');
+
+ // This shouldn't throw either
+ await loadScript('?action=use-stored-ranged-response', { doc: w.document });
+
+ assert_true(w.scriptExecuted, `Partial response should be executed`);
+}, `Non-opaque ranged response executed`);
+
+promise_test(async t => {
+ const scope = BASE_SCOPE + Math.random();
+ await setupRegistration(t, scope);
+ const iframe = await with_iframe(scope);
+ const w = iframe.contentWindow;
+ const fetchId = Math.random() + '';
+ const fetchBroadcast = awaitMessage(w.navigator.serviceWorker, fetchId);
+ const audioId = Math.random() + '';
+ const audioBroadcast = awaitMessage(w.navigator.serviceWorker, audioId);
+
+ const url = new URL('long-wav.py', w.location);
+ url.searchParams.set('action', 'broadcast-accept-encoding');
+ url.searchParams.set('id', fetchId);
+
+ await w.fetch(url, {
+ headers: { Range: 'bytes=0-10' }
+ });
+
+ assert_equals((await fetchBroadcast).acceptEncoding, null, "Accept-Encoding should not be set for fetch");
+
+ url.searchParams.set('id', audioId);
+ appendAudio(w.document, url);
+
+ assert_equals((await audioBroadcast).acceptEncoding, null, "Accept-Encoding should not be set for media");
+}, `Accept-Encoding should not appear in a service worker`);
+
+promise_test(async t => {
+ const scope = BASE_SCOPE + Math.random();
+ await setupRegistration(t, scope);
+ const iframe = await with_iframe(scope);
+ const w = iframe.contentWindow;
+ const length = 100;
+ const count = 3;
+ const counts = {};
+
+ // test a single range request size
+ async function testSizedRange(size, partialResponseCode) {
+ const rangeId = Math.random() + '';
+ const rangeBroadcast = awaitMessage(w.navigator.serviceWorker, rangeId);
+
+ // Create a bogus audio element to trick the browser into sending
+ // cross-origin range requests that can be manipulated by the service worker.
+ const sound_url = new URL('partial-text.py', w.location);
+ sound_url.hostname = REMOTE_HOST;
+ sound_url.searchParams.set('action', 'record-media-range-request');
+ sound_url.searchParams.set('length', length);
+ sound_url.searchParams.set('size', size);
+ sound_url.searchParams.set('partial', partialResponseCode);
+ sound_url.searchParams.set('id', rangeId);
+ sound_url.searchParams.set('type', 'audio/mp4');
+ appendAudio(w.document, sound_url);
+
+ // wait for the range requests to happen
+ await rangeBroadcast;
+
+ // Create multiple preload requests and count the number of resource timing
+ // entries that get created to make sure 206 and 416 range responses are treated
+ // the same.
+ const url = new URL('partial-text.py', w.location);
+ url.searchParams.set('action', 'use-media-range-request');
+ url.searchParams.set('size', size);
+ url.searchParams.set('type', 'audio/mp4');
+ counts['size' + size] = 0;
+ for (let i = 0; i < count; i++) {
+ await preloadImage(url, { doc: w.document });
+ }
+ }
+
+ // Test range requests from 1 smaller than the correct size to 1 larger than
+ // the correct size to exercise the various permutations using the default 206
+ // response code for successful range requests.
+ for (let size = length - 1; size <= length + 1; size++) {
+ await testSizedRange(size, '206');
+ }
+
+ // Test a successful range request using a 200 response.
+ await testSizedRange(length - 2, '200');
+
+ // Check the resource timing entries and count the reported number of fetches of each type
+ const resources = w.performance.getEntriesByType("resource");
+ for (const entry of resources) {
+ const url = new URL(entry.name);
+ if (url.searchParams.has('action') &&
+ url.searchParams.get('action') == 'use-media-range-request' &&
+ url.searchParams.has('size')) {
+ counts['size' + url.searchParams.get('size')]++;
+ }
+ }
+
+ // Make sure there are a non-zero number of preload requests and they are all the same
+ let counts_valid = true;
+ const first = 'size' + (length - 2);
+ for (let size = length - 2; size <= length + 1; size++) {
+ let key = 'size' + size;
+ if (!(key in counts) || counts[key] <= 0 || counts[key] != counts[first]) {
+ counts_valid = false;
+ break;
+ }
+ }
+
+ assert_true(counts_valid, `Opaque range request preloads were different for error and success`);
+}, `Opaque range preload successes and failures should be indistinguishable`);
diff --git a/test/wpt/tests/fetch/redirect-navigate/302-found-post-handler.py b/test/wpt/tests/fetch/redirect-navigate/302-found-post-handler.py
new file mode 100644
index 0000000..40a224f
--- /dev/null
+++ b/test/wpt/tests/fetch/redirect-navigate/302-found-post-handler.py
@@ -0,0 +1,15 @@
+from wptserve.utils import isomorphic_encode
+
+def main(request, response):
+ if request.method == u"POST":
+ response.add_required_headers = False
+ response.writer.write_status(302)
+ response.writer.write_header(b"Location", isomorphic_encode(request.url))
+ response.writer.end_headers()
+ response.writer.write(b"")
+ elif request.method == u"GET":
+ return ([(b"Content-Type", b"text/plain")],
+ b"OK")
+ else:
+ return ([(b"Content-Type", b"text/plain")],
+ b"FAIL") \ No newline at end of file
diff --git a/test/wpt/tests/fetch/redirect-navigate/302-found-post.html b/test/wpt/tests/fetch/redirect-navigate/302-found-post.html
new file mode 100644
index 0000000..854cd32
--- /dev/null
+++ b/test/wpt/tests/fetch/redirect-navigate/302-found-post.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<!-- Step 1: send POST request to a URL which will then 302 Found redirect -->
+<title>HTTP 302 Found POST Navigation Test</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+async_test(function(t) {
+ window.addEventListener("load", function() {
+ var frame = document.getElementById("frame");
+ var link = new URL("302-found-post-handler.py", window.location.href);
+ frame.contentWindow.document.body.innerHTML = '<form action="' + link.href + '" method="POST" id="form"><input name="n"></form>';
+ frame.contentWindow.document.getElementById("form").submit();
+ frame.addEventListener("load", t.step_func_done(function() {
+ assert_equals(frame.contentWindow.document.body.textContent, "OK");
+ }));
+ });
+}, "HTTP 302 Found POST Navigation");
+</script>
+<body>
+<iframe id="frame" src="about:blank"></iframe>
diff --git a/test/wpt/tests/fetch/redirect-navigate/preserve-fragment.html b/test/wpt/tests/fetch/redirect-navigate/preserve-fragment.html
new file mode 100644
index 0000000..682539a
--- /dev/null
+++ b/test/wpt/tests/fetch/redirect-navigate/preserve-fragment.html
@@ -0,0 +1,202 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="UTF-8">
+ <title>Ensure fragment is kept across redirects</title>
+ <meta name="timeout" content="long">
+ <link rel=help href="https://www.w3.org/TR/cuap/#uri">
+ <link rel=help href="https://tools.ietf.org/html/rfc7231#section-7.1.2">
+ <link rel=help href="https://bugs.webkit.org/show_bug.cgi?id=158420">
+ <link rel=help href="https://bugs.webkit.org/show_bug.cgi?id=24175">
+ <script src="/common/get-host-info.sub.js"></script>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script>
+ let frame;
+ let message;
+
+ const HTTP_SAME_ORIGIN = "HTTP - SameOrigin";
+ const HTTPS_SAME_ORIGIN = "HTTPS - SameOrigin";
+ const HTTP_CROSS_ORIGIN = "HTTP - CrossOrigin";
+ const HTTPS_CROSS_ORIGIN = "HTTPS - CrossOrigin";
+
+ function messageReceived(f) {
+ return new Promise((resolve) => {
+ window.addEventListener("message", (e) => {
+ message = e.data;
+ resolve();
+ }, {once: true});
+ f();
+ });
+ }
+
+ function getHostname(navigation_type) {
+ switch (navigation_type) {
+ case HTTP_SAME_ORIGIN:
+ return get_host_info().HTTP_ORIGIN;
+ case HTTPS_SAME_ORIGIN:
+ return get_host_info().HTTPS_ORIGIN
+ case HTTP_CROSS_ORIGIN:
+ return get_host_info().HTTP_REMOTE_ORIGIN
+ case HTTPS_CROSS_ORIGIN:
+ return get_host_info().HTTPS_REMOTE_ORIGIN
+ }
+
+ return 'nonexistent'
+ }
+
+ // Turns |path| from a relative to this file path into a full URL, with
+ // the host being determined by one of the ORIGIN strings above.
+ function relativePathToFull(path, navigation_type) {
+ let host = getHostname(navigation_type);
+
+ const pathname = window.location.pathname;
+ const base_path = pathname.substring(0, pathname.lastIndexOf('/') + 1);
+
+ return host + base_path + path;
+ }
+
+ // Constructs a URL to redirect.py which will respond with the given
+ // redirect status |code| to the provided |to_url|. Optionally adds on a
+ // |fragment|, if provided, to use in the initial request to redirect.py
+ function buildRedirectUrl(to_url, code, fragment) {
+ to_url = encodeURIComponent(to_url);
+ let dest = `/common/redirect.py?status=${code}&location=${to_url}`;
+ if (fragment)
+ dest = dest + '#' + fragment;
+ return dest;
+ }
+
+ async function redirectTo(url, code, navigation_type, fragment) {
+ const dest = buildRedirectUrl(url, code, fragment);
+ await messageReceived( () => {
+ frame.contentWindow.location = getHostname(navigation_type) + dest;
+ });
+ }
+
+ async function doubleRedirectTo(url, code, navigation_type, fragment, intermediate_fragment) {
+ const second_redirection = buildRedirectUrl(url, code, intermediate_fragment);
+ const first_redirection = buildRedirectUrl(second_redirection, code, fragment);
+ await messageReceived( () => {
+ frame.contentWindow.location = getHostname(navigation_type) + first_redirection;
+ });
+ }
+
+ onload = () => {
+ frame = document.getElementById("frame");
+
+ // The tests in this file verify fragments are correctly propagated in
+ // a number of HTTP redirect scenarios. Each test is run for every
+ // relevant redirect status code. We also run each scenario under each
+ // combination of navigating to cross/same origin and using http/https.
+ const status_codes = [301, 302, 303, 307, 308];
+ const navigation_types = [HTTP_SAME_ORIGIN,
+ HTTPS_SAME_ORIGIN,
+ HTTP_CROSS_ORIGIN,
+ HTTPS_CROSS_ORIGIN];
+
+ for (let navigation_type of navigation_types) {
+ // Navigate to a URL with a fragment. The URL redirects to a different
+ // page. Ensure we land on the redirected page with the fragment
+ // specified in the initial navigation's URL.
+ //
+ // Redirect chain: urlA#target -> urlB
+ //
+ for (let code of status_codes) {
+ promise_test(async () => {
+ const to_url = relativePathToFull('resources/destination.html', navigation_type);
+ await redirectTo(to_url, code, navigation_type, "target");
+ assert_true(message.url.endsWith('#target'));
+ assert_equals(message.scrollY, 2000, "scrolls to fragment from initial navigation.");
+ }, `[${navigation_type}] Preserve fragment in ${code} redirect`);
+ }
+
+ // Navigate to a URL with a fragment. The URL redirects to a different
+ // URL that also contains a fragment. Ensure we land on the redirected
+ // page using the fragment specified in the redirect response and not
+ // the one in the initial navigation.
+ //
+ // Redirect chain: urlA#target -> urlB#fromRedirect
+ //
+ for (let code of status_codes) {
+ promise_test(async () => {
+ const to_url = relativePathToFull('resources/destination.html#fromRedirect', navigation_type);
+ await redirectTo(to_url, code, navigation_type, "target");
+ assert_true(message.url.endsWith('#fromRedirect'), `Unexpected fragment: ${message.url}`);
+ assert_equals(message.scrollY, 4000, "scrolls to fragment from redirect.");
+ }, `[${navigation_type}] Redirect URL fragment takes precedence in ${code} redirect`);
+ }
+
+ // Perform two redirects. The initial navigation has a fragment and
+ // will redirect to a URL that also responds with a redirect. Ensure we
+ // land on the final page with the fragment from the original
+ // navigation.
+ //
+ // Redirect chain: urlA#target -> urlB -> urlC
+ //
+ for (let code of status_codes) {
+ promise_test(async () => {
+ const to_url = relativePathToFull('resources/destination.html', navigation_type);
+ await doubleRedirectTo(to_url, code, navigation_type, "target");
+ assert_true(message.url.endsWith('#target'), `Unexpected fragment: ${message.url}`);
+ assert_equals(message.scrollY, 2000, "scrolls to fragment from initial navigation.");
+ }, `[${navigation_type}] Preserve fragment in multiple ${code} redirects`);
+ }
+
+ // Perform two redirects. The initial navigation has a fragment and
+ // will redirect to a URL that also responds with a redirect. The
+ // second redirection to the final page also has a fragment. Ensure we
+ // land on the final page with the fragment from the redirection
+ // response URL.
+ //
+ // Redirect chain: urlA#target -> urlB -> urlC#fromRedirect
+ //
+ for (let code of status_codes) {
+ promise_test(async () => {
+ const to_url = relativePathToFull('resources/destination.html#fromRedirect', navigation_type);
+ await doubleRedirectTo(to_url, code, navigation_type, "target");
+ assert_true(message.url.endsWith('#fromRedirect'), `Unexpected fragment: ${message.url}`);
+ assert_equals(message.scrollY, 4000, "scrolls to fragment from redirect.");
+ }, `[${navigation_type}] Destination URL fragment takes precedence in multiple ${code} redirects`);
+ }
+
+ // Perform two redirects. The initial navigation has a fragment and
+ // will redirect to a URL that also responds with a redirect. This
+ // time, both redirect response have a fragment. Ensure we land on the
+ // final page with the fragment from the last redirection response URL.
+ //
+ // Redirect chain: urlA#target -> urlB#intermediate -> urlC#fromRedirect
+ //
+ for (let code of status_codes) {
+ promise_test(async () => {
+ const to_url = relativePathToFull('resources/destination.html#fromRedirect', navigation_type);
+ await doubleRedirectTo(to_url, code, navigation_type, "target", "intermediate");
+ assert_true(message.url.endsWith('#fromRedirect'), `Unexpected fragment: ${message.url}`);
+ assert_equals(message.scrollY, 4000, "scrolls to fragment from redirect.");
+ }, `[${navigation_type}] Final redirect fragment takes precedence over intermediate in multiple ${code} redirects`);
+ }
+
+ // Perform two redirects. The initial navigation has a fragment and
+ // will redirect to a URL that also responds with a redirect. The first
+ // redirect response has a fragment but the second doesn't. Ensure we
+ // land on the final page with the fragment from the first redirection
+ // response URL.
+ //
+ // Redirect chain: urlA#target -> urlB#fromRedirect -> urlC
+ //
+ for (let code of status_codes) {
+ promise_test(async () => {
+ const to_url = relativePathToFull('resources/destination.html', navigation_type);
+ await doubleRedirectTo(to_url, code, navigation_type, "target", "fromRedirect");
+ assert_true(message.url.endsWith('#fromRedirect'), `Unexpected fragment: ${message.url}`);
+ assert_equals(message.scrollY, 4000, "scrolls to fragment from redirect.");
+ }, `[${navigation_type}] Preserve intermediate fragment in multiple ${code} redirects`);
+ }
+ }
+ }
+ </script>
+ </head>
+ <body>
+ <iframe id="frame" src=""></iframe>
+ </body>
+</html>
diff --git a/test/wpt/tests/fetch/redirect-navigate/resources/destination.html b/test/wpt/tests/fetch/redirect-navigate/resources/destination.html
new file mode 100644
index 0000000..f98c5a8
--- /dev/null
+++ b/test/wpt/tests/fetch/redirect-navigate/resources/destination.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="UTF-8">
+ <style>
+ body {
+ height: 10000px;
+ margin: 0;
+ }
+ p {
+ position: absolute;
+ margin: 0;
+ }
+ </style>
+ <script>
+ window.onload = () => {
+ window.parent.postMessage({
+ url: window.location.toString(),
+ scrollY: window.scrollY
+ }, "*");
+ }
+ </script>
+ </head>
+ <body>
+ <p style="top: 2000px" id="target">Target</p>
+ <p style="top: 4000px" id="fromRedirect">Target</p>
+ </body>
+</html>
diff --git a/test/wpt/tests/fetch/redirects/data.window.js b/test/wpt/tests/fetch/redirects/data.window.js
new file mode 100644
index 0000000..eeb4196
--- /dev/null
+++ b/test/wpt/tests/fetch/redirects/data.window.js
@@ -0,0 +1,25 @@
+// See ../api/redirect/redirect-to-dataurl.any.js for fetch() tests
+
+async_test(t => {
+ const img = document.createElement("img");
+ img.onload = t.unreached_func();
+ img.onerror = t.step_func_done();
+ img.src = "../api/resources/redirect.py?location=data:image/png%3Bbase64,iVBORw0KGgoAAAANSUhEUgAAAIUAAABqCAIAAAAdqgU8AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAF6SURBVHhe7dNBDQAADIPA%2Bje92eBxSQUQSLedlQzo0TLQonFWPVoGWjT%2BoUfLQIvGP/RoGWjR%2BIceLQMtGv/Qo2WgReMferQMtGj8Q4%2BWgRaNf%2BjRMtCi8Q89WgZaNP6hR8tAi8Y/9GgZaNH4hx4tAy0a/9CjZaBF4x96tAy0aPxDj5aBFo1/6NEy0KLxDz1aBlo0/qFHy0CLxj/0aBlo0fiHHi0DLRr/0KNloEXjH3q0DLRo/EOPloEWjX/o0TLQovEPPVoGWjT%2BoUfLQIvGP/RoGWjR%2BIceLQMtGv/Qo2WgReMferQMtGj8Q4%2BWgRaNf%2BjRMtCi8Q89WgZaNP6hR8tAi8Y/9GgZaNH4hx4tAy0a/9CjZaBF4x96tAy0aPxDj5aBFo1/6NEy0KLxDz1aBlo0/qFHy0CLxj/0aBlo0fiHHi0DLRr/0KNloEXjH3q0DLRo/EOPloEWjX/o0TLQovEPPVoGWjT%2BoUfLQIvGP/RoGWjR%2BIceJQMPIOzeGc0PIDEAAAAASUVORK5CYII";
+}, "<img> fetch that redirects to data: URL");
+
+globalThis.globalTest = null;
+async_test(t => {
+ globalThis.globalTest = t;
+ const script = document.createElement("script");
+ script.src = "../api/resources/redirect.py?location=data:text/javascript,(globalThis.globalTest.unreached_func())()";
+ script.onerror = t.step_func_done();
+ document.body.append(script);
+}, "<script> fetch that redirects to data: URL");
+
+async_test(t => {
+ const client = new XMLHttpRequest();
+ client.open("GET", "../api/resources/redirect.py?location=data:,");
+ client.send();
+ client.onload = t.unreached_func();
+ client.onerror = t.step_func_done();
+}, "XMLHttpRequest fetch that redirects to data: URL");
diff --git a/test/wpt/tests/fetch/redirects/subresource-fragments.html b/test/wpt/tests/fetch/redirects/subresource-fragments.html
new file mode 100644
index 0000000..0bd74d7
--- /dev/null
+++ b/test/wpt/tests/fetch/redirects/subresource-fragments.html
@@ -0,0 +1,39 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Subresources and fragment preservation</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=/html/canvas/resources/canvas-tests.js></script>
+<div id=log></div>
+<!--
+ The source image is 50h x 100w and its color depends on the fragment.
+
+ This image is then drawn on a 50h x 100w transparent black canvas.
+-->
+<img data-desc="Control"
+ src="/images/colors.svg#green">
+<img data-desc="Redirect with the original URL containing a fragment"
+ src="../api/resources/redirect.py?simple&location=/images/colors.svg#green">
+<img data-desc="Redirect with the response Location header containing a fragment"
+ src="../api/resources/redirect.py?simple&location=/images/colors.svg%23green">
+<img data-desc="Redirect with both the original URL and response Location header containing a fragment"
+ src="../api/resources/redirect.py?simple&location=/images/colors.svg%23green#red">
+<canvas width=100 height=50></canvas>
+<script>
+setup({ explicit_done:true });
+onload = () => {
+ const canvas = document.querySelector("canvas");
+ const ctx = canvas.getContext("2d");
+ document.querySelectorAll("img").forEach(img => {
+ test(t => {
+ t.add_cleanup(() => {
+ ctx.clearRect(0, 0, 100, 50);
+ });
+ ctx.drawImage(img, 0, 0);
+ // canvas, pixelX, pixelY, r, g, b, alpha, ?, ?, tolerance
+ _assertPixelApprox(canvas, 40, 40, 0, 255, 0, 255, 4);
+ }, img.dataset.desc);
+ });
+ done();
+};
+</script>
diff --git a/test/wpt/tests/fetch/security/1xx-response.any.js b/test/wpt/tests/fetch/security/1xx-response.any.js
new file mode 100644
index 0000000..df4dafc
--- /dev/null
+++ b/test/wpt/tests/fetch/security/1xx-response.any.js
@@ -0,0 +1,28 @@
+promise_test(async (t) => {
+ // The 100 response should be ignored, then the transaction ends, which
+ // should lead to an error.
+ await promise_rejects_js(
+ t, TypeError, fetch('/common/text-plain.txt?pipe=status(100)'));
+}, 'Status(100) should be ignored.');
+
+// This behavior is being discussed at https://github.com/whatwg/fetch/issues/1397.
+promise_test(async (t) => {
+ const res = await fetch('/common/text-plain.txt?pipe=status(101)');
+ assert_equals(res.status, 101);
+ const body = await res.text();
+ assert_equals(body, '');
+}, 'Status(101) should be accepted, with removing body.');
+
+promise_test(async (t) => {
+ // The 103 response should be ignored, then the transaction ends, which
+ // should lead to an error.
+ await promise_rejects_js(
+ t, TypeError, fetch('/common/text-plain.txt?pipe=status(103)'));
+}, 'Status(103) should be ignored.');
+
+promise_test(async (t) => {
+ // The 199 response should be ignored, then the transaction ends, which
+ // should lead to an error.
+ await promise_rejects_js(
+ t, TypeError, fetch('/common/text-plain.txt?pipe=status(199)'));
+}, 'Status(199) should be ignored.');
diff --git a/test/wpt/tests/fetch/security/dangling-markup-mitigation-data-url.tentative.sub.html b/test/wpt/tests/fetch/security/dangling-markup-mitigation-data-url.tentative.sub.html
new file mode 100644
index 0000000..f27735d
--- /dev/null
+++ b/test/wpt/tests/fetch/security/dangling-markup-mitigation-data-url.tentative.sub.html
@@ -0,0 +1,229 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<body>
+<script>
+ function readableURL(url) {
+ return url.replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t");
+ }
+
+ // For each of the following tests, we'll inject a frame containing the HTML we'd like to poke at
+ // as a `srcdoc` attribute. Because we're injecting markup via `srcdoc`, we need to entity-escape
+ // the content we'd like to treat as "raw" (e.g. `\n` => `&#10;`, `<` => `&lt;`), and
+ // double-escape the "escaped" content.
+ var rawBrace = "&lt;";
+ var escapedBrace = "&amp;lt;";
+ var doubleEscapedBrace = "&amp;amp;lt;";
+ var rawNewline = "&#10;";
+ var escapedNewline = "&amp;#10;";
+ // doubleEscapedNewline is used inside a data URI, and so must have its '#' escaped.
+ var doubleEscapedNewline = "&amp;amp;%2310;";
+
+ function appendFrameAndGetElement(test, frame) {
+ return new Promise((resolve, reject) => {
+ frame.onload = test.step_func(_ => {
+ frame.onload = null;
+ resolve(frame.contentDocument.querySelector('#dangling'));
+ });
+ document.body.appendChild(frame);
+ });
+ }
+
+ function assert_img_loaded(test, frame) {
+ appendFrameAndGetElement(test, frame)
+ .then(test.step_func_done(img => {
+ assert_equals(img.naturalHeight, 1, "Height");
+ frame.remove();
+ }));
+ }
+
+ function assert_img_not_loaded(test, frame) {
+ appendFrameAndGetElement(test, frame)
+ .then(test.step_func_done(img => {
+ assert_equals(img.naturalHeight, 0, "Height");
+ assert_equals(img.naturalWidth, 0, "Width");
+ }));
+ }
+
+ function assert_nested_img_not_loaded(test, frame) {
+ window.addEventListener('message', test.step_func(e => {
+ if (e.source != frame.contentWindow)
+ return;
+
+ assert_equals(e.data, 'error');
+ test.done();
+ }));
+ appendFrameAndGetElement(test, frame);
+ }
+
+ function assert_nested_img_loaded(test, frame) {
+ window.addEventListener('message', test.step_func(e => {
+ if (e.source != frame.contentWindow)
+ return;
+
+ assert_equals(e.data, 'loaded');
+ test.done();
+ }));
+ appendFrameAndGetElement(test, frame);
+ }
+
+ function createFrame(markup) {
+ var i = document.createElement('iframe');
+ i.srcdoc = `${markup}sekrit`;
+ return i;
+ }
+
+ // Subresource requests:
+ [
+ // Data URLs don't themselves trigger blocking:
+ `<img id="dangling" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=">`,
+ `<img id="dangling" src="data:image/png;base64,${rawNewline}iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=">`,
+ `<img id="dangling" src="data:image/png;base64,i${rawNewline}VBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=">`,
+
+ // Data URLs with visual structure don't trigger blocking
+ `<img id="dangling" src="data:image/svg+xml;utf8,
+ <svg width='1' height='1' xmlns='http://www.w3.org/2000/svg'>
+ <rect width='100%' height='100%' fill='rebeccapurple'/>
+ <rect x='10%' y='10%' width='80%' height='80%' fill='lightgreen'/>
+ </svg>">`
+ ].forEach(markup => {
+ async_test(t => {
+ var i = createFrame(`${markup} <element attr="" another=''>`);
+ assert_img_loaded(t, i);
+ }, readableURL(markup));
+ });
+
+ // Nested subresource requests:
+ //
+ // The following tests load a given HTML string into `<iframe srcdoc="...">`, so we'll
+ // end up with a frame with an ID of `dangling` inside the srcdoc frame. That frame's
+ // `src` is a `data:` URL that resolves to an HTML document containing an `<img>`. The
+ // error/load handlers on that image are piped back up to the top-level document to
+ // determine whether the tests' expectations were met. *phew*
+
+ // Allowed:
+ [
+ // Just a newline:
+ `<iframe id="dangling"
+ src="data:text/html,
+ <img
+ onload='window.parent.postMessage(&quot;loaded&quot;, &quot;*&quot;);'
+ onerror='window.parent.postMessage(&quot;error&quot;, &quot;*&quot;);'
+ src='http://{{host}}:{{ports[http][0]}}/images/gr${rawNewline}een-256x256.png'>
+ ">
+ </iframe>`,
+
+ // Just a brace:
+ `<iframe id="dangling"
+ src="data:text/html,
+ <img
+ onload='window.parent.postMessage(&quot;loaded&quot;, &quot;*&quot;);'
+ onerror='window.parent.postMessage(&quot;error&quot;, &quot;*&quot;);'
+ src='http://{{host}}:{{ports[http][0]}}/images/green-256x256.png?${rawBrace}'>
+ ">
+ </iframe>`,
+
+ // Newline and escaped brace.
+ `<iframe id="dangling"
+ src="data:text/html,
+ <img
+ onload='window.parent.postMessage(&quot;loaded&quot;, &quot;*&quot;);'
+ onerror='window.parent.postMessage(&quot;error&quot;, &quot;*&quot;);'
+ src='http://{{host}}:{{ports[http][0]}}/images/gr${rawNewline}een-256x256.png?${doubleEscapedBrace}'>
+ ">
+ </iframe>`,
+
+ // Brace and escaped newline:
+ `<iframe id="dangling"
+ src="data:text/html,
+ <img
+ onload='window.parent.postMessage(&quot;loaded&quot;, &quot;*&quot;);'
+ onerror='window.parent.postMessage(&quot;error&quot;, &quot;*&quot;);'
+ src='http://{{host}}:{{ports[http][0]}}/images/green-256x256.png?${doubleEscapedNewline}${rawBrace}'>
+ ">
+ </iframe>`,
+ ].forEach(markup => {
+ async_test(t => {
+ var i = createFrame(`
+ <script>
+ // Repeat the message so that the parent can track this frame as the source.
+ window.onmessage = e => window.parent.postMessage(e.data, '*');
+ </scr`+`ipt>
+ ${markup}
+ `);
+ assert_nested_img_loaded(t, i);
+ }, readableURL(markup));
+ });
+
+ // Nested requests that should fail:
+ [
+ // Newline and brace:
+ `<iframe id="dangling"
+ src="data:text/html,
+ <img
+ onload='window.parent.postMessage(&quot;loaded&quot;, &quot;*&quot;);'
+ onerror='window.parent.postMessage(&quot;error&quot;, &quot;*&quot;);'
+ src='http://{{host}}:{{ports[http][0]}}/images/gr${rawNewline}een-256x256.png?${rawBrace}'>
+ ">
+ </iframe>`,
+
+ // Leading whitespace:
+ `<iframe id="dangling"
+ src=" data:text/html,
+ <img
+ onload='window.parent.postMessage(&quot;loaded&quot;, &quot;*&quot;);'
+ onerror='window.parent.postMessage(&quot;error&quot;, &quot;*&quot;);'
+ src='http://{{host}}:{{ports[http][0]}}/images/gr${rawNewline}een-256x256.png?${rawBrace}'>
+ ">
+ </iframe>`,
+
+ // Leading newline:
+ `<iframe id="dangling"
+ src="\ndata:text/html,
+ <img
+ onload='window.parent.postMessage(&quot;loaded&quot;, &quot;*&quot;);'
+ onerror='window.parent.postMessage(&quot;error&quot;, &quot;*&quot;);'
+ src='http://{{host}}:{{ports[http][0]}}/images/gr${rawNewline}een-256x256.png?${rawBrace}'>
+ ">
+ </iframe>`,
+ `<iframe id="dangling"
+ src="${rawNewline}data:text/html,
+ <img
+ onload='window.parent.postMessage(&quot;loaded&quot;, &quot;*&quot;);'
+ onerror='window.parent.postMessage(&quot;error&quot;, &quot;*&quot;);'
+ src='http://{{host}}:{{ports[http][0]}}/images/gr${rawNewline}een-256x256.png?${rawBrace}'>
+ ">
+ </iframe>`,
+
+ // Leading tab:
+ `<iframe id="dangling"
+ src="\tdata:text/html,
+ <img
+ onload='window.parent.postMessage(&quot;loaded&quot;, &quot;*&quot;);'
+ onerror='window.parent.postMessage(&quot;error&quot;, &quot;*&quot;);'
+ src='http://{{host}}:{{ports[http][0]}}/images/gr${rawNewline}een-256x256.png?${rawBrace}'>
+ ">
+ </iframe>`,
+
+ // Leading carrige return:
+ `<iframe id="dangling"
+ src="\rdata:text/html,
+ <img
+ onload='window.parent.postMessage(&quot;loaded&quot;, &quot;*&quot;);'
+ onerror='window.parent.postMessage(&quot;error&quot;, &quot;*&quot;);'
+ src='http://{{host}}:{{ports[http][0]}}/images/gr${rawNewline}een-256x256.png?${rawBrace}'>
+ ">
+ </iframe>`,
+ ].forEach(markup => {
+ async_test(t => {
+ var i = createFrame(`
+ <script>
+ // Repeat the message so that the parent can track this frame as the source.
+ window.onmessage = e => window.parent.postMessage(e.data, '*');
+ </scr`+`ipt>
+ ${markup}
+ `);
+ assert_nested_img_not_loaded(t, i);
+ }, readableURL(markup));
+ });
+</script>
diff --git a/test/wpt/tests/fetch/security/dangling-markup-mitigation.tentative.html b/test/wpt/tests/fetch/security/dangling-markup-mitigation.tentative.html
new file mode 100644
index 0000000..61a9316
--- /dev/null
+++ b/test/wpt/tests/fetch/security/dangling-markup-mitigation.tentative.html
@@ -0,0 +1,147 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<body>
+<script>
+ function readableURL(url) {
+ return url.replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t");
+ }
+
+ var should_load = [
+ `/images/green-1x1.png`,
+ `/images/gre\nen-1x1.png`,
+ `/images/gre\ten-1x1.png`,
+ `/images/gre\ren-1x1.png`,
+ `/images/green-1x1.png?img=<`,
+ `/images/green-1x1.png?img=&lt;`,
+ `/images/green-1x1.png?img=%3C`,
+ `/images/gr\neen-1x1.png?img=%3C`,
+ `/images/gr\reen-1x1.png?img=%3C`,
+ `/images/gr\teen-1x1.png?img=%3C`,
+ `/images/green-1x1.png?img=&#10;`,
+ `/images/gr\neen-1x1.png?img=&#10;`,
+ `/images/gr\reen-1x1.png?img=&#10;`,
+ `/images/gr\teen-1x1.png?img=&#10;`,
+ ];
+ should_load.forEach(url => async_test(t => {
+ fetch(url)
+ .then(t.step_func_done(r => {
+ assert_equals(r.status, 200);
+ }))
+ .catch(t.unreached_func("Fetch should succeed."));
+ }, "Fetch: " + readableURL(url)));
+
+ var should_block = [
+ `/images/gre\nen-1x1.png?img=<`,
+ `/images/gre\ren-1x1.png?img=<`,
+ `/images/gre\ten-1x1.png?img=<`,
+ `/images/green-1x1.png?<\n=block`,
+ `/images/green-1x1.png?<\r=block`,
+ `/images/green-1x1.png?<\t=block`,
+ ];
+ should_block.forEach(url => async_test(t => {
+ fetch(url)
+ .then(t.unreached_func("Fetch should fail."))
+ .catch(t.step_func_done());
+ }, "Fetch: " + readableURL(url)));
+
+
+ // For each of the following tests, we'll inject a frame containing the HTML we'd like to poke at
+ // as a `srcdoc` attribute. Because we're injecting markup via `srcdoc`, we need to entity-escape
+ // the content we'd like to treat as "raw" (e.g. `\n` => `&#10;`, `<` => `&lt;`), and
+ // double-escape the "escaped" content.
+ var rawBrace = "&lt;";
+ var escapedBrace = "&amp;lt;";
+ var rawNewline = "&#10;";
+ var escapedNewline = "&amp;#10;";
+
+ function appendFrameAndGetElement(test, frame) {
+ return new Promise((resolve, reject) => {
+ frame.onload = test.step_func(_ => {
+ frame.onload = null;
+ resolve(frame.contentDocument.querySelector('#dangling'));
+ });
+ document.body.appendChild(frame);
+ });
+ }
+
+ function assert_img_loaded(test, frame) {
+ appendFrameAndGetElement(test, frame)
+ .then(test.step_func_done(img => {
+ assert_equals(img.naturalHeight, 1, "Height");
+ frame.remove();
+ }));
+ }
+
+ function assert_img_not_loaded(test, frame) {
+ appendFrameAndGetElement(test, frame)
+ .then(test.step_func_done(img => {
+ assert_equals(img.naturalHeight, 0, "Height");
+ assert_equals(img.naturalWidth, 0, "Width");
+ }));
+ }
+
+ function createFrame(markup) {
+ var i = document.createElement('iframe');
+ i.srcdoc = `${markup}sekrit`;
+ return i;
+ }
+
+ // The following resources should not be blocked, as their URLs do not contain both a `\n` and `<`
+ // character in the body of the URL.
+ var should_load = [
+ // Brace alone doesn't block:
+ `<img id="dangling" src="/images/green-1x1.png?img=${rawBrace}b">`,
+
+ // Newline alone doesn't block:
+ `<img id="dangling" src="/images/green-1x1.png?img=${rawNewline}b">`,
+
+ // Entity-escaped characters don't trigger blocking:
+ `<img id="dangling" src="/images/green-1x1.png?img=${escapedNewline}b">`,
+ `<img id="dangling" src="/images/green-1x1.png?img=${escapedBrace}b">`,
+ `<img id="dangling" src="/images/green-1x1.png?img=${escapedNewline}b${escapedBrace}c">`,
+
+ // Leading and trailing whitespace is stripped:
+ `
+ <img id="dangling" src="
+ /images/green-1x1.png?img=
+ ">
+ `,
+ `
+ <img id="dangling" src="
+ /images/green-1x1.png?img=${escapedBrace}
+ ">
+ `,
+ `
+ <img id="dangling" src="
+ /images/green-1x1.png?img=${escapedNewline}
+ ">
+ `,
+ ];
+
+ should_load.forEach(markup => {
+ async_test(t => {
+ var i = createFrame(`${markup} <element attr="" another=''>`);
+ assert_img_loaded(t, i);
+ }, readableURL(markup));
+ });
+
+ // The following resources should be blocked, as their URLs contain both `\n` and `<` characters:
+ var should_block = [
+ `<img id="dangling" src="/images/green-1x1.png?img=${rawNewline}${rawBrace}b">`,
+ `<img id="dangling" src="/images/green-1x1.png?img=${rawBrace}${rawNewline}b">`,
+ `
+ <img id="dangling" src="/images/green-1x1.png?img=
+ ${rawBrace}
+ ${rawNewline}b
+ ">
+ `,
+ ];
+
+ should_block.forEach(markup => {
+ async_test(t => {
+ var i = createFrame(`${markup}`);
+ assert_img_not_loaded(t, i);
+ }, readableURL(markup));
+ });
+</script>
diff --git a/test/wpt/tests/fetch/security/embedded-credentials.tentative.sub.html b/test/wpt/tests/fetch/security/embedded-credentials.tentative.sub.html
new file mode 100644
index 0000000..ca5ee1c
--- /dev/null
+++ b/test/wpt/tests/fetch/security/embedded-credentials.tentative.sub.html
@@ -0,0 +1,89 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<body>
+<script>
+ async_test(t => {
+ var i = document.createElement('img');
+ i.onerror = t.step_func_done();
+ i.onload = t.unreached_func("'onload' should not fire.");
+ i.src = "http://user:pass@{{domains[www]}}:{{ports[http][0]}}/images/red.png";
+ }, "Embedded credentials are treated as network errors.");
+
+ async_test(t => {
+ var i = document.createElement('iframe');
+ i.src = "./support/embedded-credential-window.sub.html";
+ i.onload = t.step_func(_ => {
+ var c = new MessageChannel();
+ c.port1.onmessage = t.step_func_done(e => {
+ assert_equals(e.data, "Error", "The image should not load.");
+ i.remove();
+ });
+ i.contentWindow.postMessage("Hi!", "*", [c.port2]);
+ });
+ document.body.appendChild(i);
+ }, "Embedded credentials are treated as network errors in frames.");
+
+ async_test(t => {
+ var w = window.open("./support/embedded-credential-window.sub.html");
+ window.addEventListener("message", t.step_func(message => {
+ if (message.source != w)
+ return;
+
+ var c = new MessageChannel();
+ c.port1.onmessage = t.step_func_done(e => {
+ w.close();
+ assert_equals(e.data, "Error", "The image should not load.");
+ });
+ w.postMessage("absolute", "*", [c.port2]);
+ }));
+ }, "Embedded credentials are treated as network errors in new windows.");
+
+ async_test(t => {
+ var w = window.open();
+ w.location.href = "http://user:pass@{{domains[www]}}:{{ports[http][0]}}/fetch/security/support/embedded-credential-window.sub.html";
+ window.addEventListener("message", t.step_func(message => {
+ if (message.source != w)
+ return;
+
+ var c = new MessageChannel();
+ c.port1.onmessage = t.step_func_done(e => {
+ w.close();
+ assert_equals(e.data, "Load", "The image should load.");
+ });
+ w.postMessage("relative", "*", [c.port2]);
+ }));
+ }, "Embedded credentials matching the top-level are not treated as network errors for relative URLs.");
+
+ async_test(t => {
+ var w = window.open();
+ w.location.href = "http://user:pass@{{domains[www]}}:{{ports[http][0]}}/fetch/security/support/embedded-credential-window.sub.html";
+ window.addEventListener("message", t.step_func(message => {
+ if (message.source != w)
+ return;
+
+ var c = new MessageChannel();
+ c.port1.onmessage = t.step_func_done(e => {
+ w.close();
+ assert_equals(e.data, "Load", "The image should load.");
+ });
+ w.postMessage("same-origin-matching", "*", [c.port2]);
+ }));
+ }, "Embedded credentials matching the top-level are not treated as network errors for same-origin URLs.");
+
+ async_test(t => {
+ var w = window.open();
+ w.location.href = "http://user:pass@{{domains[www]}}:{{ports[http][0]}}/fetch/security/support/embedded-credential-window.sub.html";
+ window.addEventListener("message", t.step_func(message => {
+ if (message.source != w)
+ return;
+
+ var c = new MessageChannel();
+ c.port1.onmessage = t.step_func_done(e => {
+ w.close();
+ assert_equals(e.data, "Error", "The image should load.");
+ });
+ w.postMessage("cross-origin-matching", "*", [c.port2]);
+ }));
+ }, "Embedded credentials matching the top-level are treated as network errors for cross-origin URLs.");
+</script>
diff --git a/test/wpt/tests/fetch/security/redirect-to-url-with-credentials.https.html b/test/wpt/tests/fetch/security/redirect-to-url-with-credentials.https.html
new file mode 100644
index 0000000..b064648
--- /dev/null
+++ b/test/wpt/tests/fetch/security/redirect-to-url-with-credentials.https.html
@@ -0,0 +1,68 @@
+<html>
+<header>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/common/get-host-info.sub.js"></script>
+</header>
+<body>
+<script>
+var host = get_host_info();
+
+var sameOriginImageURL = "/common/redirect.py?location=" + host.HTTPS_ORIGIN_WITH_CREDS + "/service-workers/service-worker/resources/fetch-access-control.py?ACAOrigin= " + host.HTTPS_ORIGIN + "%26PNGIMAGE%26ACACredentials=true";
+var imageURL = "/common/redirect.py?location=" + host.HTTPS_REMOTE_ORIGIN_WITH_CREDS + "/service-workers/service-worker/resources/fetch-access-control.py?ACAOrigin= " + host.HTTPS_ORIGIN + "%26PNGIMAGE%26ACACredentials=true";
+var frameURL = "/common/redirect.py?location=" + host.HTTPS_REMOTE_ORIGIN_WITH_CREDS + "/common/blank.html";
+
+promise_test((test) => {
+ return fetch(imageURL, {mode: "no-cors"});
+}, "No CORS fetch after a redirect with an URL containing credentials");
+
+promise_test((test) => {
+ return promise_rejects_js(test, TypeError, fetch(imageURL, {mode: "cors"}));
+}, "CORS fetch after a redirect with a cross origin URL containing credentials");
+
+promise_test((test) => {
+ return fetch(sameOriginImageURL, {mode: "cors"});
+}, "CORS fetch after a redirect with a same origin URL containing credentials");
+
+promise_test((test) => {
+ return new Promise((resolve, reject) => {
+ var image = new Image();
+ image.onload = resolve;
+ image.onerror = (e) => reject(e);
+ image.src = imageURL;
+ });
+}, "Image loading after a redirect with an URL containing credentials");
+
+promise_test((test) => {
+ return new Promise((resolve, reject) => {
+ var image = new Image();
+ image.crossOrigin = "use-credentials";
+ image.onerror = resolve;
+ image.onload = () => reject("Image should not load");
+ image.src = imageURL;
+ });
+}, "CORS Image loading after a redirect with a cross origin URL containing credentials");
+
+promise_test((test) => {
+ return new Promise((resolve, reject) => {
+ var image = new Image();
+ image.crossOrigin = "use-credentials";
+ image.onload = resolve;
+ image.onerror = (e) => reject(e);
+ image.src = sameOriginImageURL;
+ });
+}, "CORS Image loading after a redirect with a same origin URL containing credentials");
+
+promise_test(async (test) => {
+ var iframe = document.createElement("iframe");
+ document.body.appendChild(iframe);
+ await new Promise((resolve, reject) => {
+ iframe.src = frameURL;
+ iframe.onload = resolve;
+ iframe.onerror = (e) => reject(e);
+ });
+ document.body.removeChild(iframe);
+}, "Frame loading after a redirect with an URL containing credentials");
+</script>
+</body>
+</html>
diff --git a/test/wpt/tests/fetch/security/support/embedded-credential-window.sub.html b/test/wpt/tests/fetch/security/support/embedded-credential-window.sub.html
new file mode 100644
index 0000000..20d307e
--- /dev/null
+++ b/test/wpt/tests/fetch/security/support/embedded-credential-window.sub.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<script>
+ window.addEventListener("message", e => {
+ var i = document.createElement('img');
+ i.onload = () => { e.ports[0].postMessage("Load"); }
+ i.onerror = () => { e.ports[0].postMessage("Error"); }
+ if (e.data == "relative") {
+ i.src = "/images/green.png";
+ } else if (e.data == "same-origin-matching") {
+ i.src = "http://user:pass@{{domains[www]}}:{{ports[http][0]}}/images/green.png";
+ } else if (e.data == "cross-origin-matching") {
+ i.src = "http://user:pass@{{domains[élève]}}:{{ports[http][0]}}/images/red.png";
+ } else {
+ i.src = "http://user:pass@{{domains[www]}}:{{ports[http][0]}}/images/red.png";
+ }
+ });
+
+ (window.opener || window.parent).postMessage("Hi!", "*");
+</script>
diff --git a/test/wpt/tests/fetch/stale-while-revalidate/fetch-sw.https.html b/test/wpt/tests/fetch/stale-while-revalidate/fetch-sw.https.html
new file mode 100644
index 0000000..efcebc2
--- /dev/null
+++ b/test/wpt/tests/fetch/stale-while-revalidate/fetch-sw.https.html
@@ -0,0 +1,65 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Stale Revalidation Requests don't get sent to service worker</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="../../service-workers/service-worker/resources/test-helpers.sub.js"></script>
+ <script src="/common/utils.js"></script>
+</head>
+<body>
+<script>
+
+ // Duplicating this resource to make service worker scoping simpler.
+ async function setupRegistrationAndWaitToBeControlled(t, scope) {
+ const controlled = new Promise((resolve) => {
+ navigator.serviceWorker.oncontrollerchange = () => { resolve(); };
+ });
+ const reg = await navigator.serviceWorker.register('sw-intercept.js');
+ await wait_for_state(t, reg.installing, 'activated');
+ await controlled;
+ add_completion_callback(_ => reg.unregister());
+ return reg;
+ }
+
+ // Using 250ms polling interval to provide enough 'network calmness' to give
+ // the background low priority revalidation request a chance to kick in.
+ function wait250ms(test) {
+ return new Promise(resolve => {
+ test.step_timeout(() => {
+ resolve();
+ }, 250);
+ });
+ }
+
+ promise_test(async (test) => {
+ var request_token = token();
+ const uri = 'resources/stale-script.py?token=' + request_token;
+
+ await setupRegistrationAndWaitToBeControlled(test, 'resources/stale-script.py');
+
+ var service_worker_count = 0;
+ navigator.serviceWorker.addEventListener('message', function once(event) {
+ if (event.data.endsWith(uri)) {
+ service_worker_count++;
+ }
+ });
+
+ const response = await fetch(uri);
+ const response2 = await fetch(uri);
+ assert_equals(response.headers.get('Unique-Id'), response2.headers.get('Unique-Id'));
+ while(true) {
+ const revalidation_check = await fetch(`resources/stale-script.py?query&token=` + request_token);
+ if (revalidation_check.headers.get('Count') == '2') {
+ // The service worker should not see the revalidation request.
+ assert_equals(service_worker_count, 2);
+ break;
+ }
+ await wait250ms(test);
+ }
+ }, 'Second fetch returns same response');
+
+</script>
+</body>
+</html>
diff --git a/test/wpt/tests/fetch/stale-while-revalidate/fetch.any.js b/test/wpt/tests/fetch/stale-while-revalidate/fetch.any.js
new file mode 100644
index 0000000..3682b9d
--- /dev/null
+++ b/test/wpt/tests/fetch/stale-while-revalidate/fetch.any.js
@@ -0,0 +1,32 @@
+// META: global=window,worker
+// META: title=Tests Stale While Revalidate is executed for fetch API
+// META: script=/common/utils.js
+
+function wait25ms(test) {
+ return new Promise(resolve => {
+ test.step_timeout(() => {
+ resolve();
+ }, 25);
+ });
+}
+
+promise_test(async (test) => {
+ var request_token = token();
+
+ const response = await fetch(`resources/stale-script.py?token=` + request_token);
+ // Wait until resource is completely fetched to allow caching before next fetch.
+ const body = await response.text();
+ const response2 = await fetch(`resources/stale-script.py?token=` + request_token);
+
+ assert_equals(response.headers.get('Unique-Id'), response2.headers.get('Unique-Id'));
+ const body2 = await response2.text();
+ assert_equals(body, body2);
+
+ while(true) {
+ const revalidation_check = await fetch(`resources/stale-script.py?query&token=` + request_token);
+ if (revalidation_check.headers.get('Count') == '2') {
+ break;
+ }
+ await wait25ms(test);
+ }
+}, 'Second fetch returns same response');
diff --git a/test/wpt/tests/fetch/stale-while-revalidate/resources/stale-css.py b/test/wpt/tests/fetch/stale-while-revalidate/resources/stale-css.py
new file mode 100644
index 0000000..b876683
--- /dev/null
+++ b/test/wpt/tests/fetch/stale-while-revalidate/resources/stale-css.py
@@ -0,0 +1,28 @@
+def main(request, response):
+
+ token = request.GET.first(b"token", None)
+ is_query = request.GET.first(b"query", None) != None
+ with request.server.stash.lock:
+ value = request.server.stash.take(token)
+ count = 0
+ if value != None:
+ count = int(value)
+ if is_query:
+ if count < 2:
+ request.server.stash.put(token, count)
+ else:
+ count = count + 1
+ request.server.stash.put(token, count)
+ if is_query:
+ headers = [(b"Count", count)]
+ content = b""
+ return 200, headers, content
+ else:
+ content = b"body { background: rgb(0, 128, 0); }"
+ if count > 1:
+ content = b"body { background: rgb(255, 0, 0); }"
+
+ headers = [(b"Content-Type", b"text/css"),
+ (b"Cache-Control", b"private, max-age=0, stale-while-revalidate=60")]
+
+ return 200, headers, content
diff --git a/test/wpt/tests/fetch/stale-while-revalidate/resources/stale-image.py b/test/wpt/tests/fetch/stale-while-revalidate/resources/stale-image.py
new file mode 100644
index 0000000..36e6fc0
--- /dev/null
+++ b/test/wpt/tests/fetch/stale-while-revalidate/resources/stale-image.py
@@ -0,0 +1,40 @@
+import os.path
+
+from wptserve.utils import isomorphic_decode
+
+def main(request, response):
+
+ token = request.GET.first(b"token", None)
+ is_query = request.GET.first(b"query", None) != None
+ with request.server.stash.lock:
+ value = request.server.stash.take(token)
+ count = 0
+ if value != None:
+ count = int(value)
+ if is_query:
+ if count < 2:
+ request.server.stash.put(token, count)
+ else:
+ count = count + 1
+ request.server.stash.put(token, count)
+
+ if is_query:
+ headers = [(b"Count", count)]
+ content = b""
+ return 200, headers, content
+ else:
+ filename = u"green-16x16.png"
+ if count > 1:
+ filename = u"green-256x256.png"
+
+ path = os.path.join(os.path.dirname(isomorphic_decode(__file__)), u"../../../images", filename)
+ body = open(path, "rb").read()
+
+ response.add_required_headers = False
+ response.writer.write_status(200)
+ response.writer.write_header(b"content-length", len(body))
+ response.writer.write_header(b"Cache-Control", b"private, max-age=0, stale-while-revalidate=60")
+ response.writer.write_header(b"content-type", b"image/png")
+ response.writer.end_headers()
+
+ response.writer.write(body)
diff --git a/test/wpt/tests/fetch/stale-while-revalidate/resources/stale-script.py b/test/wpt/tests/fetch/stale-while-revalidate/resources/stale-script.py
new file mode 100644
index 0000000..731cd80
--- /dev/null
+++ b/test/wpt/tests/fetch/stale-while-revalidate/resources/stale-script.py
@@ -0,0 +1,32 @@
+import random, string
+
+def id_token():
+ letters = string.ascii_lowercase
+ return b''.join(random.choice(letters).encode("utf-8") for i in range(20))
+
+def main(request, response):
+ token = request.GET.first(b"token", None)
+ is_query = request.GET.first(b"query", None) != None
+ with request.server.stash.lock:
+ value = request.server.stash.take(token)
+ count = 0
+ if value != None:
+ count = int(value)
+ if is_query:
+ if count < 2:
+ request.server.stash.put(token, count)
+ else:
+ count = count + 1
+ request.server.stash.put(token, count)
+
+ if is_query:
+ headers = [(b"Count", count)]
+ content = u""
+ return 200, headers, content
+ else:
+ unique_id = id_token()
+ headers = [(b"Content-Type", b"text/javascript"),
+ (b"Cache-Control", b"private, max-age=0, stale-while-revalidate=60"),
+ (b"Unique-Id", unique_id)]
+ content = b"report('%s')" % unique_id
+ return 200, headers, content
diff --git a/test/wpt/tests/fetch/stale-while-revalidate/revalidate-not-blocked-by-csp.html b/test/wpt/tests/fetch/stale-while-revalidate/revalidate-not-blocked-by-csp.html
new file mode 100644
index 0000000..ea70b9a
--- /dev/null
+++ b/test/wpt/tests/fetch/stale-while-revalidate/revalidate-not-blocked-by-csp.html
@@ -0,0 +1,69 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Test revalidations requests aren't blocked by CSP.</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<body>
+<script>
+
+// Regression test for https://crbug.com/1070117.
+var request_token = token();
+let image_src = "resources/stale-image.py?token=" + request_token;
+
+let loadImage = async () => {
+ let img = document.createElement("img");
+ img.src = image_src;
+ let loaded = new Promise(r => img.onload = r);
+ document.body.appendChild(img);
+ await loaded;
+ return img;
+};
+
+promise_test(async t => {
+ await new Promise(r => window.onload = r);
+
+ // No CSP report must be sent from now.
+ //
+ // TODO(arthursonzogni): Some browser implementations do not support the
+ // ReportingObserver yet. Ideally, another way to access the reports should be
+ // used to test them.
+ const observer = new ReportingObserver(t.unreached_func(
+ "CSP reports aren't sent for revalidation requests"));
+ if (observer)
+ observer.observe();
+
+ let img1 = await loadImage(); // Load initial resource.
+ let img2 = loadImage(); // Request stale resource.
+
+ // Insert a <meta> CSP. This will block any image load starting from now.
+ const metaCSP = document.createElement("meta");
+ metaCSP.httpEquiv = "Content-Security-Policy";
+ metaCSP.content = "img-src 'none'";
+ document.getElementsByTagName("head")[0].appendChild(metaCSP)
+
+ // The images were requested before the <meta> CSP above was added. So they
+ // will load. Nevertheless, the resource will be stale. A revalidation request
+ // is going to be made after that.
+ assert_equals(img1.width, 16, "(initial version loaded)");
+ assert_equals((await img2).width, 16, "(stale version loaded)");
+
+ // At some point, the <img> resource is going to be revalidated. It must not
+ // be blocked nor trigger a CSP violation report.
+
+ // Query the server again and again. At some point it must have received the
+ // revalidation request. We poll, because we don't know when the revalidation
+ // will occur.
+ let query = false;
+ while(true) {
+ await new Promise(r => step_timeout(r, 25));
+ let response = await fetch(`${image_src}${query ? "&query" : ""}`);
+ let count = response.headers.get("Count");
+ if (count == "2")
+ break;
+ query ^= true;
+ }
+}, "Request revalidation aren't blocked by CSP");
+
+</script>
+</body>
diff --git a/test/wpt/tests/fetch/stale-while-revalidate/stale-css.html b/test/wpt/tests/fetch/stale-while-revalidate/stale-css.html
new file mode 100644
index 0000000..603a60c
--- /dev/null
+++ b/test/wpt/tests/fetch/stale-while-revalidate/stale-css.html
@@ -0,0 +1,51 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Tests Stale While Revalidate works for css</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<body>
+<script>
+
+var request_token = token();
+let link_src = "./resources/stale-css.py?token=" + request_token;
+
+let loadLink = async() => {
+ let link = document.createElement("link");
+ link.rel = "stylesheet";
+ link.type = "text/css";
+ link.href = "resources/stale-css.py?token=" + request_token;
+ let loaded = new Promise(r => link.onload = r);
+ document.body.appendChild(link);
+ await loaded;
+ return window
+ .getComputedStyle(document.body)
+ .getPropertyValue('background-color');
+};
+
+promise_test(async t => {
+ await new Promise(r => window.onload = r);
+
+ let bgColor1 = await loadLink();
+ assert_equals(bgColor1, "rgb(0, 128, 0)", "(initial version loaded)");
+
+ let bgColor2 = await loadLink();
+ assert_equals(bgColor2, "rgb(0, 128, 0)", "(stale version loaded)");
+
+ // Query the server again and again. At some point it must have received the
+ // revalidation request. We poll, because we don't know when the revalidation
+ // will occur.
+ while(true) {
+ await new Promise(r => step_timeout(r, 25));
+ let response = await fetch(link_src + "&query");
+ let count = response.headers.get("Count");
+ if (count == '2')
+ break;
+ }
+
+ let bgColor3 = await loadLink();
+ assert_equals(bgColor3, "rgb(255, 0, 0)", "(revalidated version loaded)");
+}, 'Cache returns stale resource');
+
+</script>
+</body>
diff --git a/test/wpt/tests/fetch/stale-while-revalidate/stale-image.html b/test/wpt/tests/fetch/stale-while-revalidate/stale-image.html
new file mode 100644
index 0000000..d86bdfb
--- /dev/null
+++ b/test/wpt/tests/fetch/stale-while-revalidate/stale-image.html
@@ -0,0 +1,55 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Tests Stale While Revalidate works for images</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<body>
+<!--
+Use a child document to load the second stale image into because
+an image loaded into the same document will skip cache-control headers.
+See: https://html.spec.whatwg.org/#the-list-of-available-images
+-->
+<iframe id="child1" srcdoc=""></iframe>
+<iframe id="child2" srcdoc=""></iframe>
+<script>
+
+var request_token = token();
+let image_src = "resources/stale-image.py?token=" + request_token;
+
+let loadImage = async (document) => {
+ let img = document.createElement("img");
+ img.src = image_src;
+ let loaded = new Promise(r => img.onload = r);
+ document.body.appendChild(img);
+ await loaded;
+ return img;
+};
+
+promise_test(async t => {
+ await new Promise(r => window.onload = r);
+
+ let img1 = await loadImage(document);
+ assert_equals(img1.width, 16, "(initial version loaded)");
+
+ let img2 = await loadImage(child1.contentDocument);
+ assert_equals(img2.width, 16, "(stale version loaded)");
+
+ // Query the server again and again. At some point it must have received the
+ // revalidation request. We poll, because we don't know when the revalidation
+ // will occur.
+ while(true) {
+ await new Promise(r => step_timeout(r, 25));
+ let response = await fetch(image_src + "&query");
+ let count = response.headers.get("Count");
+ if (count == '2')
+ break;
+ }
+
+ let img3 = await loadImage(child2.contentDocument);
+ assert_equals(img3.width, 256, "(revalidated version loaded)");
+
+}, 'Cache returns stale resource');
+
+</script>
+</body>
diff --git a/test/wpt/tests/fetch/stale-while-revalidate/stale-script.html b/test/wpt/tests/fetch/stale-while-revalidate/stale-script.html
new file mode 100644
index 0000000..f531748
--- /dev/null
+++ b/test/wpt/tests/fetch/stale-while-revalidate/stale-script.html
@@ -0,0 +1,59 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Tests Stale While Revalidate works for scripts</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<body>
+<script>
+
+const request_token = token();
+const script_src = "./resources/stale-script.py?token=" + request_token;
+
+// The script above will call report() via a uniquely generated ID on the
+// subresource. If it is a cache hit, the ID will be the same and
+// |last_modified_count| won't be incremented.
+let last_modified;
+let last_modified_count = 0;
+function report(mod) {
+ if (last_modified == mod)
+ return;
+ last_modified = mod;
+ last_modified_count++;
+}
+
+let loadScript = async () => {
+ let script = document.createElement("script");
+ let script_loaded = new Promise(r => script.onload = r);
+ script.src = script_src;
+ document.body.appendChild(script);
+ await script_loaded;
+};
+
+promise_test(async t => {
+ await new Promise(r => window.onload = r);
+
+ await loadScript();
+ assert_equals(last_modified_count, 1, '(initial version loaded)');
+
+ await loadScript();
+ assert_equals(last_modified_count, 1, '(stale version loaded)');
+
+ // Query the server again and again. At some point it must have received the
+ // revalidation request. We poll, because we don't know when the revalidation
+ // will occur.
+ while(true) {
+ await new Promise(r => step_timeout(r, 25));
+ let response = await fetch(script_src + "&query");
+ let count = response.headers.get("Count");
+ if (count == '2')
+ break;
+ }
+
+ await loadScript();
+ assert_equals(last_modified_count, 2, '(revalidated version loaded)');
+
+}, 'Cache returns stale resource');
+
+</script>
+</body>
diff --git a/test/wpt/tests/fetch/stale-while-revalidate/sw-intercept.js b/test/wpt/tests/fetch/stale-while-revalidate/sw-intercept.js
new file mode 100644
index 0000000..dca7de5
--- /dev/null
+++ b/test/wpt/tests/fetch/stale-while-revalidate/sw-intercept.js
@@ -0,0 +1,14 @@
+async function broadcast(msg) {
+ for (const client of await clients.matchAll()) {
+ client.postMessage(msg);
+ }
+}
+
+self.addEventListener('fetch', event => {
+ event.waitUntil(broadcast(event.request.url));
+ event.respondWith(fetch(event.request));
+});
+
+self.addEventListener('activate', event => {
+ self.clients.claim();
+});
diff --git a/test/wpt/tests/interfaces/ANGLE_instanced_arrays.idl b/test/wpt/tests/interfaces/ANGLE_instanced_arrays.idl
new file mode 100644
index 0000000..557a416
--- /dev/null
+++ b/test/wpt/tests/interfaces/ANGLE_instanced_arrays.idl
@@ -0,0 +1,12 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: WebGL ANGLE_instanced_arrays Khronos Ratified Extension Specification (https://registry.khronos.org/webgl/extensions/ANGLE_instanced_arrays/)
+
+[Exposed=(Window,Worker), LegacyNoInterfaceObject]
+interface ANGLE_instanced_arrays {
+ const GLenum VERTEX_ATTRIB_ARRAY_DIVISOR_ANGLE = 0x88FE;
+ undefined drawArraysInstancedANGLE(GLenum mode, GLint first, GLsizei count, GLsizei primcount);
+ undefined drawElementsInstancedANGLE(GLenum mode, GLsizei count, GLenum type, GLintptr offset, GLsizei primcount);
+ undefined vertexAttribDivisorANGLE(GLuint index, GLuint divisor);
+};
diff --git a/test/wpt/tests/interfaces/CSP.idl b/test/wpt/tests/interfaces/CSP.idl
new file mode 100644
index 0000000..ac0a6ff
--- /dev/null
+++ b/test/wpt/tests/interfaces/CSP.idl
@@ -0,0 +1,56 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Content Security Policy Level 3 (https://w3c.github.io/webappsec-csp/)
+
+[Exposed=Window]
+interface CSPViolationReportBody : ReportBody {
+ [Default] object toJSON();
+ readonly attribute USVString documentURL;
+ readonly attribute USVString? referrer;
+ readonly attribute USVString? blockedURL;
+ readonly attribute DOMString effectiveDirective;
+ readonly attribute DOMString originalPolicy;
+ readonly attribute USVString? sourceFile;
+ readonly attribute DOMString? sample;
+ readonly attribute SecurityPolicyViolationEventDisposition disposition;
+ readonly attribute unsigned short statusCode;
+ readonly attribute unsigned long? lineNumber;
+ readonly attribute unsigned long? columnNumber;
+};
+
+enum SecurityPolicyViolationEventDisposition {
+ "enforce", "report"
+};
+
+[Exposed=(Window,Worker)]
+interface SecurityPolicyViolationEvent : Event {
+ constructor(DOMString type, optional SecurityPolicyViolationEventInit eventInitDict = {});
+ readonly attribute USVString documentURI;
+ readonly attribute USVString referrer;
+ readonly attribute USVString blockedURI;
+ readonly attribute DOMString effectiveDirective;
+ readonly attribute DOMString violatedDirective; // historical alias of effectiveDirective
+ readonly attribute DOMString originalPolicy;
+ readonly attribute USVString sourceFile;
+ readonly attribute DOMString sample;
+ readonly attribute SecurityPolicyViolationEventDisposition disposition;
+ readonly attribute unsigned short statusCode;
+ readonly attribute unsigned long lineNumber;
+ readonly attribute unsigned long columnNumber;
+};
+
+dictionary SecurityPolicyViolationEventInit : EventInit {
+ required USVString documentURI;
+ USVString referrer = "";
+ USVString blockedURI = "";
+ required DOMString violatedDirective;
+ required DOMString effectiveDirective;
+ required DOMString originalPolicy;
+ USVString sourceFile = "";
+ DOMString sample = "";
+ required SecurityPolicyViolationEventDisposition disposition;
+ required unsigned short statusCode;
+ unsigned long lineNumber = 0;
+ unsigned long columnNumber = 0;
+};
diff --git a/test/wpt/tests/interfaces/DOM-Parsing.idl b/test/wpt/tests/interfaces/DOM-Parsing.idl
new file mode 100644
index 0000000..d0d84ab
--- /dev/null
+++ b/test/wpt/tests/interfaces/DOM-Parsing.idl
@@ -0,0 +1,26 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: DOM Parsing and Serialization (https://w3c.github.io/DOM-Parsing/)
+
+[Exposed=Window]
+interface XMLSerializer {
+ constructor();
+ DOMString serializeToString(Node root);
+};
+
+interface mixin InnerHTML {
+ [CEReactions] attribute [LegacyNullToEmptyString] DOMString innerHTML;
+};
+
+Element includes InnerHTML;
+ShadowRoot includes InnerHTML;
+
+partial interface Element {
+ [CEReactions] attribute [LegacyNullToEmptyString] DOMString outerHTML;
+ [CEReactions] undefined insertAdjacentHTML(DOMString position, DOMString text);
+};
+
+partial interface Range {
+ [CEReactions, NewObject] DocumentFragment createContextualFragment(DOMString fragment);
+};
diff --git a/test/wpt/tests/interfaces/EXT_blend_minmax.idl b/test/wpt/tests/interfaces/EXT_blend_minmax.idl
new file mode 100644
index 0000000..fd7d26e
--- /dev/null
+++ b/test/wpt/tests/interfaces/EXT_blend_minmax.idl
@@ -0,0 +1,10 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: WebGL EXT_blend_minmax Khronos Ratified Extension Specification (https://registry.khronos.org/webgl/extensions/EXT_blend_minmax/)
+
+[Exposed=(Window,Worker), LegacyNoInterfaceObject]
+interface EXT_blend_minmax {
+ const GLenum MIN_EXT = 0x8007;
+ const GLenum MAX_EXT = 0x8008;
+};
diff --git a/test/wpt/tests/interfaces/EXT_color_buffer_float.idl b/test/wpt/tests/interfaces/EXT_color_buffer_float.idl
new file mode 100644
index 0000000..09bd397
--- /dev/null
+++ b/test/wpt/tests/interfaces/EXT_color_buffer_float.idl
@@ -0,0 +1,8 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: WebGL EXT_color_buffer_float Extension Specification (https://registry.khronos.org/webgl/extensions/EXT_color_buffer_float/)
+
+[Exposed=(Window,Worker), LegacyNoInterfaceObject]
+interface EXT_color_buffer_float {
+}; // interface EXT_color_buffer_float
diff --git a/test/wpt/tests/interfaces/EXT_color_buffer_half_float.idl b/test/wpt/tests/interfaces/EXT_color_buffer_half_float.idl
new file mode 100644
index 0000000..7197e44
--- /dev/null
+++ b/test/wpt/tests/interfaces/EXT_color_buffer_half_float.idl
@@ -0,0 +1,12 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: WebGL EXT_color_buffer_half_float Extension Specification (https://registry.khronos.org/webgl/extensions/EXT_color_buffer_half_float/)
+
+[Exposed=(Window,Worker), LegacyNoInterfaceObject]
+interface EXT_color_buffer_half_float {
+ const GLenum RGBA16F_EXT = 0x881A;
+ const GLenum RGB16F_EXT = 0x881B;
+ const GLenum FRAMEBUFFER_ATTACHMENT_COMPONENT_TYPE_EXT = 0x8211;
+ const GLenum UNSIGNED_NORMALIZED_EXT = 0x8C17;
+}; // interface EXT_color_buffer_half_float
diff --git a/test/wpt/tests/interfaces/EXT_disjoint_timer_query.idl b/test/wpt/tests/interfaces/EXT_disjoint_timer_query.idl
new file mode 100644
index 0000000..cf0c8d9
--- /dev/null
+++ b/test/wpt/tests/interfaces/EXT_disjoint_timer_query.idl
@@ -0,0 +1,30 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: WebGL EXT_disjoint_timer_query Extension Specification (https://registry.khronos.org/webgl/extensions/EXT_disjoint_timer_query/)
+
+typedef unsigned long long GLuint64EXT;
+
+[Exposed=(Window,Worker), LegacyNoInterfaceObject]
+interface WebGLTimerQueryEXT : WebGLObject {
+};
+
+[Exposed=(Window,Worker), LegacyNoInterfaceObject]
+interface EXT_disjoint_timer_query {
+ const GLenum QUERY_COUNTER_BITS_EXT = 0x8864;
+ const GLenum CURRENT_QUERY_EXT = 0x8865;
+ const GLenum QUERY_RESULT_EXT = 0x8866;
+ const GLenum QUERY_RESULT_AVAILABLE_EXT = 0x8867;
+ const GLenum TIME_ELAPSED_EXT = 0x88BF;
+ const GLenum TIMESTAMP_EXT = 0x8E28;
+ const GLenum GPU_DISJOINT_EXT = 0x8FBB;
+
+ WebGLTimerQueryEXT? createQueryEXT();
+ undefined deleteQueryEXT(WebGLTimerQueryEXT? query);
+ [WebGLHandlesContextLoss] boolean isQueryEXT(WebGLTimerQueryEXT? query);
+ undefined beginQueryEXT(GLenum target, WebGLTimerQueryEXT query);
+ undefined endQueryEXT(GLenum target);
+ undefined queryCounterEXT(WebGLTimerQueryEXT query, GLenum target);
+ any getQueryEXT(GLenum target, GLenum pname);
+ any getQueryObjectEXT(WebGLTimerQueryEXT query, GLenum pname);
+};
diff --git a/test/wpt/tests/interfaces/EXT_disjoint_timer_query_webgl2.idl b/test/wpt/tests/interfaces/EXT_disjoint_timer_query_webgl2.idl
new file mode 100644
index 0000000..689203c
--- /dev/null
+++ b/test/wpt/tests/interfaces/EXT_disjoint_timer_query_webgl2.idl
@@ -0,0 +1,14 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: WebGL EXT_disjoint_timer_query_webgl2 Extension Specification (https://registry.khronos.org/webgl/extensions/EXT_disjoint_timer_query_webgl2/)
+
+[Exposed=(Window,Worker), LegacyNoInterfaceObject]
+interface EXT_disjoint_timer_query_webgl2 {
+ const GLenum QUERY_COUNTER_BITS_EXT = 0x8864;
+ const GLenum TIME_ELAPSED_EXT = 0x88BF;
+ const GLenum TIMESTAMP_EXT = 0x8E28;
+ const GLenum GPU_DISJOINT_EXT = 0x8FBB;
+
+ undefined queryCounterEXT(WebGLQuery query, GLenum target);
+};
diff --git a/test/wpt/tests/interfaces/EXT_float_blend.idl b/test/wpt/tests/interfaces/EXT_float_blend.idl
new file mode 100644
index 0000000..58ec47e
--- /dev/null
+++ b/test/wpt/tests/interfaces/EXT_float_blend.idl
@@ -0,0 +1,8 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: WebGL EXT_float_blend Extension Specification (https://registry.khronos.org/webgl/extensions/EXT_float_blend/)
+
+[Exposed=(Window,Worker), LegacyNoInterfaceObject]
+interface EXT_float_blend {
+}; // interface EXT_float_blend
diff --git a/test/wpt/tests/interfaces/EXT_frag_depth.idl b/test/wpt/tests/interfaces/EXT_frag_depth.idl
new file mode 100644
index 0000000..1ae6896
--- /dev/null
+++ b/test/wpt/tests/interfaces/EXT_frag_depth.idl
@@ -0,0 +1,8 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: WebGL EXT_frag_depth Khronos Ratified Extension Specification (https://registry.khronos.org/webgl/extensions/EXT_frag_depth/)
+
+[Exposed=(Window,Worker), LegacyNoInterfaceObject]
+interface EXT_frag_depth {
+};
diff --git a/test/wpt/tests/interfaces/EXT_sRGB.idl b/test/wpt/tests/interfaces/EXT_sRGB.idl
new file mode 100644
index 0000000..3c03c33
--- /dev/null
+++ b/test/wpt/tests/interfaces/EXT_sRGB.idl
@@ -0,0 +1,12 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: WebGL EXT_sRGB Extension Specification (https://registry.khronos.org/webgl/extensions/EXT_sRGB/)
+
+[Exposed=(Window,Worker), LegacyNoInterfaceObject]
+interface EXT_sRGB {
+ const GLenum SRGB_EXT = 0x8C40;
+ const GLenum SRGB_ALPHA_EXT = 0x8C42;
+ const GLenum SRGB8_ALPHA8_EXT = 0x8C43;
+ const GLenum FRAMEBUFFER_ATTACHMENT_COLOR_ENCODING_EXT = 0x8210;
+};
diff --git a/test/wpt/tests/interfaces/EXT_shader_texture_lod.idl b/test/wpt/tests/interfaces/EXT_shader_texture_lod.idl
new file mode 100644
index 0000000..13df26c
--- /dev/null
+++ b/test/wpt/tests/interfaces/EXT_shader_texture_lod.idl
@@ -0,0 +1,8 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: WebGL EXT_shader_texture_lod Khronos Ratified Extension Specification (https://registry.khronos.org/webgl/extensions/EXT_shader_texture_lod/)
+
+[Exposed=(Window,Worker), LegacyNoInterfaceObject]
+interface EXT_shader_texture_lod {
+};
diff --git a/test/wpt/tests/interfaces/EXT_texture_compression_bptc.idl b/test/wpt/tests/interfaces/EXT_texture_compression_bptc.idl
new file mode 100644
index 0000000..2772980
--- /dev/null
+++ b/test/wpt/tests/interfaces/EXT_texture_compression_bptc.idl
@@ -0,0 +1,12 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: WebGL EXT_texture_compression_bptc Extension Specification (https://registry.khronos.org/webgl/extensions/EXT_texture_compression_bptc/)
+
+[Exposed=(Window,Worker), LegacyNoInterfaceObject]
+interface EXT_texture_compression_bptc {
+ const GLenum COMPRESSED_RGBA_BPTC_UNORM_EXT = 0x8E8C;
+ const GLenum COMPRESSED_SRGB_ALPHA_BPTC_UNORM_EXT = 0x8E8D;
+ const GLenum COMPRESSED_RGB_BPTC_SIGNED_FLOAT_EXT = 0x8E8E;
+ const GLenum COMPRESSED_RGB_BPTC_UNSIGNED_FLOAT_EXT = 0x8E8F;
+};
diff --git a/test/wpt/tests/interfaces/EXT_texture_compression_rgtc.idl b/test/wpt/tests/interfaces/EXT_texture_compression_rgtc.idl
new file mode 100644
index 0000000..f12b962
--- /dev/null
+++ b/test/wpt/tests/interfaces/EXT_texture_compression_rgtc.idl
@@ -0,0 +1,12 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: WebGL EXT_texture_compression_rgtc Extension Specification (https://registry.khronos.org/webgl/extensions/EXT_texture_compression_rgtc/)
+
+[Exposed=(Window,Worker), LegacyNoInterfaceObject]
+interface EXT_texture_compression_rgtc {
+ const GLenum COMPRESSED_RED_RGTC1_EXT = 0x8DBB;
+ const GLenum COMPRESSED_SIGNED_RED_RGTC1_EXT = 0x8DBC;
+ const GLenum COMPRESSED_RED_GREEN_RGTC2_EXT = 0x8DBD;
+ const GLenum COMPRESSED_SIGNED_RED_GREEN_RGTC2_EXT = 0x8DBE;
+};
diff --git a/test/wpt/tests/interfaces/EXT_texture_filter_anisotropic.idl b/test/wpt/tests/interfaces/EXT_texture_filter_anisotropic.idl
new file mode 100644
index 0000000..5c78bfa
--- /dev/null
+++ b/test/wpt/tests/interfaces/EXT_texture_filter_anisotropic.idl
@@ -0,0 +1,10 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: WebGL EXT_texture_filter_anisotropic Khronos Ratified Extension Specification (https://registry.khronos.org/webgl/extensions/EXT_texture_filter_anisotropic/)
+
+[Exposed=(Window,Worker), LegacyNoInterfaceObject]
+interface EXT_texture_filter_anisotropic {
+ const GLenum TEXTURE_MAX_ANISOTROPY_EXT = 0x84FE;
+ const GLenum MAX_TEXTURE_MAX_ANISOTROPY_EXT = 0x84FF;
+};
diff --git a/test/wpt/tests/interfaces/EXT_texture_norm16.idl b/test/wpt/tests/interfaces/EXT_texture_norm16.idl
new file mode 100644
index 0000000..1fe5ed8
--- /dev/null
+++ b/test/wpt/tests/interfaces/EXT_texture_norm16.idl
@@ -0,0 +1,16 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: WebGL EXT_texture_norm16 Extension Specification (https://registry.khronos.org/webgl/extensions/EXT_texture_norm16/)
+
+[Exposed=(Window,Worker), LegacyNoInterfaceObject]
+interface EXT_texture_norm16 {
+ const GLenum R16_EXT = 0x822A;
+ const GLenum RG16_EXT = 0x822C;
+ const GLenum RGB16_EXT = 0x8054;
+ const GLenum RGBA16_EXT = 0x805B;
+ const GLenum R16_SNORM_EXT = 0x8F98;
+ const GLenum RG16_SNORM_EXT = 0x8F99;
+ const GLenum RGB16_SNORM_EXT = 0x8F9A;
+ const GLenum RGBA16_SNORM_EXT = 0x8F9B;
+};
diff --git a/test/wpt/tests/interfaces/FedCM.idl b/test/wpt/tests/interfaces/FedCM.idl
new file mode 100644
index 0000000..8de87e8
--- /dev/null
+++ b/test/wpt/tests/interfaces/FedCM.idl
@@ -0,0 +1,67 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Federated Credential Management API (https://fedidcg.github.io/FedCM/)
+
+[Exposed=Window, SecureContext]
+interface IdentityCredential : Credential {
+ readonly attribute USVString? token;
+};
+
+partial dictionary CredentialRequestOptions {
+ IdentityCredentialRequestOptions identity;
+};
+
+dictionary IdentityCredentialRequestOptions {
+ sequence<IdentityProviderConfig> providers;
+};
+
+dictionary IdentityProviderConfig {
+ required USVString configURL;
+ required USVString clientId;
+ USVString nonce;
+};
+
+dictionary IdentityProviderWellKnown {
+ required sequence<USVString> provider_urls;
+};
+
+dictionary IdentityProviderIcon {
+ required USVString url;
+ unsigned long size;
+};
+
+dictionary IdentityProviderBranding {
+ USVString background_color;
+ USVString color;
+ sequence<IdentityProviderIcon> icons;
+ USVString name;
+};
+
+dictionary IdentityProviderAPIConfig {
+ required USVString accounts_endpoint;
+ required USVString client_metadata_endpoint;
+ required USVString id_assertion_endpoint;
+ IdentityProviderBranding branding;
+};
+
+dictionary IdentityProviderAccount {
+ required USVString id;
+ required USVString name;
+ required USVString email;
+ USVString given_name;
+ USVString picture;
+ sequence<USVString> approved_clients;
+};
+dictionary IdentityProviderAccountList {
+ sequence<IdentityProviderAccount> accounts;
+};
+
+dictionary IdentityProviderToken {
+ required USVString token;
+};
+
+dictionary IdentityProviderClientMetadata {
+ USVString privacy_policy_url;
+ USVString terms_of_service_url;
+};
diff --git a/test/wpt/tests/interfaces/FileAPI.idl b/test/wpt/tests/interfaces/FileAPI.idl
new file mode 100644
index 0000000..aee0e65
--- /dev/null
+++ b/test/wpt/tests/interfaces/FileAPI.idl
@@ -0,0 +1,100 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: File API (https://w3c.github.io/FileAPI/)
+
+[Exposed=(Window,Worker), Serializable]
+interface Blob {
+ constructor(optional sequence<BlobPart> blobParts,
+ optional BlobPropertyBag options = {});
+
+ readonly attribute unsigned long long size;
+ readonly attribute DOMString type;
+
+ // slice Blob into byte-ranged chunks
+ Blob slice(optional [Clamp] long long start,
+ optional [Clamp] long long end,
+ optional DOMString contentType);
+
+ // read from the Blob.
+ [NewObject] ReadableStream stream();
+ [NewObject] Promise<USVString> text();
+ [NewObject] Promise<ArrayBuffer> arrayBuffer();
+};
+
+enum EndingType { "transparent", "native" };
+
+dictionary BlobPropertyBag {
+ DOMString type = "";
+ EndingType endings = "transparent";
+};
+
+typedef (BufferSource or Blob or USVString) BlobPart;
+
+[Exposed=(Window,Worker), Serializable]
+interface File : Blob {
+ constructor(sequence<BlobPart> fileBits,
+ USVString fileName,
+ optional FilePropertyBag options = {});
+ readonly attribute DOMString name;
+ readonly attribute long long lastModified;
+};
+
+dictionary FilePropertyBag : BlobPropertyBag {
+ long long lastModified;
+};
+
+[Exposed=(Window,Worker), Serializable]
+interface FileList {
+ getter File? item(unsigned long index);
+ readonly attribute unsigned long length;
+};
+
+[Exposed=(Window,Worker)]
+interface FileReader: EventTarget {
+ constructor();
+ // async read methods
+ undefined readAsArrayBuffer(Blob blob);
+ undefined readAsBinaryString(Blob blob);
+ undefined readAsText(Blob blob, optional DOMString encoding);
+ undefined readAsDataURL(Blob blob);
+
+ undefined abort();
+
+ // states
+ const unsigned short EMPTY = 0;
+ const unsigned short LOADING = 1;
+ const unsigned short DONE = 2;
+
+ readonly attribute unsigned short readyState;
+
+ // File or Blob data
+ readonly attribute (DOMString or ArrayBuffer)? result;
+
+ readonly attribute DOMException? error;
+
+ // event handler content attributes
+ attribute EventHandler onloadstart;
+ attribute EventHandler onprogress;
+ attribute EventHandler onload;
+ attribute EventHandler onabort;
+ attribute EventHandler onerror;
+ attribute EventHandler onloadend;
+};
+
+[Exposed=(DedicatedWorker,SharedWorker)]
+interface FileReaderSync {
+ constructor();
+ // Synchronously return strings
+
+ ArrayBuffer readAsArrayBuffer(Blob blob);
+ DOMString readAsBinaryString(Blob blob);
+ DOMString readAsText(Blob blob, optional DOMString encoding);
+ DOMString readAsDataURL(Blob blob);
+};
+
+[Exposed=(Window,DedicatedWorker,SharedWorker)]
+partial interface URL {
+ static DOMString createObjectURL((Blob or MediaSource) obj);
+ static undefined revokeObjectURL(DOMString url);
+};
diff --git a/test/wpt/tests/interfaces/IndexedDB.idl b/test/wpt/tests/interfaces/IndexedDB.idl
new file mode 100644
index 0000000..d82391d
--- /dev/null
+++ b/test/wpt/tests/interfaces/IndexedDB.idl
@@ -0,0 +1,226 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Indexed Database API 3.0 (https://w3c.github.io/IndexedDB/)
+
+[Exposed=(Window,Worker)]
+interface IDBRequest : EventTarget {
+ readonly attribute any result;
+ readonly attribute DOMException? error;
+ readonly attribute (IDBObjectStore or IDBIndex or IDBCursor)? source;
+ readonly attribute IDBTransaction? transaction;
+ readonly attribute IDBRequestReadyState readyState;
+
+ // Event handlers:
+ attribute EventHandler onsuccess;
+ attribute EventHandler onerror;
+};
+
+enum IDBRequestReadyState {
+ "pending",
+ "done"
+};
+
+[Exposed=(Window,Worker)]
+interface IDBOpenDBRequest : IDBRequest {
+ // Event handlers:
+ attribute EventHandler onblocked;
+ attribute EventHandler onupgradeneeded;
+};
+
+[Exposed=(Window,Worker)]
+interface IDBVersionChangeEvent : Event {
+ constructor(DOMString type, optional IDBVersionChangeEventInit eventInitDict = {});
+ readonly attribute unsigned long long oldVersion;
+ readonly attribute unsigned long long? newVersion;
+};
+
+dictionary IDBVersionChangeEventInit : EventInit {
+ unsigned long long oldVersion = 0;
+ unsigned long long? newVersion = null;
+};
+
+partial interface mixin WindowOrWorkerGlobalScope {
+ [SameObject] readonly attribute IDBFactory indexedDB;
+};
+
+[Exposed=(Window,Worker)]
+interface IDBFactory {
+ [NewObject] IDBOpenDBRequest open(DOMString name,
+ optional [EnforceRange] unsigned long long version);
+ [NewObject] IDBOpenDBRequest deleteDatabase(DOMString name);
+
+ Promise<sequence<IDBDatabaseInfo>> databases();
+
+ short cmp(any first, any second);
+};
+
+dictionary IDBDatabaseInfo {
+ DOMString name;
+ unsigned long long version;
+};
+
+[Exposed=(Window,Worker)]
+interface IDBDatabase : EventTarget {
+ readonly attribute DOMString name;
+ readonly attribute unsigned long long version;
+ readonly attribute DOMStringList objectStoreNames;
+
+ [NewObject] IDBTransaction transaction((DOMString or sequence<DOMString>) storeNames,
+ optional IDBTransactionMode mode = "readonly",
+ optional IDBTransactionOptions options = {});
+ undefined close();
+
+ [NewObject] IDBObjectStore createObjectStore(
+ DOMString name,
+ optional IDBObjectStoreParameters options = {});
+ undefined deleteObjectStore(DOMString name);
+
+ // Event handlers:
+ attribute EventHandler onabort;
+ attribute EventHandler onclose;
+ attribute EventHandler onerror;
+ attribute EventHandler onversionchange;
+};
+
+enum IDBTransactionDurability { "default", "strict", "relaxed" };
+
+dictionary IDBTransactionOptions {
+ IDBTransactionDurability durability = "default";
+};
+
+dictionary IDBObjectStoreParameters {
+ (DOMString or sequence<DOMString>)? keyPath = null;
+ boolean autoIncrement = false;
+};
+
+[Exposed=(Window,Worker)]
+interface IDBObjectStore {
+ attribute DOMString name;
+ readonly attribute any keyPath;
+ readonly attribute DOMStringList indexNames;
+ [SameObject] readonly attribute IDBTransaction transaction;
+ readonly attribute boolean autoIncrement;
+
+ [NewObject] IDBRequest put(any value, optional any key);
+ [NewObject] IDBRequest add(any value, optional any key);
+ [NewObject] IDBRequest delete(any query);
+ [NewObject] IDBRequest clear();
+ [NewObject] IDBRequest get(any query);
+ [NewObject] IDBRequest getKey(any query);
+ [NewObject] IDBRequest getAll(optional any query,
+ optional [EnforceRange] unsigned long count);
+ [NewObject] IDBRequest getAllKeys(optional any query,
+ optional [EnforceRange] unsigned long count);
+ [NewObject] IDBRequest count(optional any query);
+
+ [NewObject] IDBRequest openCursor(optional any query,
+ optional IDBCursorDirection direction = "next");
+ [NewObject] IDBRequest openKeyCursor(optional any query,
+ optional IDBCursorDirection direction = "next");
+
+ IDBIndex index(DOMString name);
+
+ [NewObject] IDBIndex createIndex(DOMString name,
+ (DOMString or sequence<DOMString>) keyPath,
+ optional IDBIndexParameters options = {});
+ undefined deleteIndex(DOMString name);
+};
+
+dictionary IDBIndexParameters {
+ boolean unique = false;
+ boolean multiEntry = false;
+};
+
+[Exposed=(Window,Worker)]
+interface IDBIndex {
+ attribute DOMString name;
+ [SameObject] readonly attribute IDBObjectStore objectStore;
+ readonly attribute any keyPath;
+ readonly attribute boolean multiEntry;
+ readonly attribute boolean unique;
+
+ [NewObject] IDBRequest get(any query);
+ [NewObject] IDBRequest getKey(any query);
+ [NewObject] IDBRequest getAll(optional any query,
+ optional [EnforceRange] unsigned long count);
+ [NewObject] IDBRequest getAllKeys(optional any query,
+ optional [EnforceRange] unsigned long count);
+ [NewObject] IDBRequest count(optional any query);
+
+ [NewObject] IDBRequest openCursor(optional any query,
+ optional IDBCursorDirection direction = "next");
+ [NewObject] IDBRequest openKeyCursor(optional any query,
+ optional IDBCursorDirection direction = "next");
+};
+
+[Exposed=(Window,Worker)]
+interface IDBKeyRange {
+ readonly attribute any lower;
+ readonly attribute any upper;
+ readonly attribute boolean lowerOpen;
+ readonly attribute boolean upperOpen;
+
+ // Static construction methods:
+ [NewObject] static IDBKeyRange only(any value);
+ [NewObject] static IDBKeyRange lowerBound(any lower, optional boolean open = false);
+ [NewObject] static IDBKeyRange upperBound(any upper, optional boolean open = false);
+ [NewObject] static IDBKeyRange bound(any lower,
+ any upper,
+ optional boolean lowerOpen = false,
+ optional boolean upperOpen = false);
+
+ boolean includes(any key);
+};
+
+[Exposed=(Window,Worker)]
+interface IDBCursor {
+ readonly attribute (IDBObjectStore or IDBIndex) source;
+ readonly attribute IDBCursorDirection direction;
+ readonly attribute any key;
+ readonly attribute any primaryKey;
+ [SameObject] readonly attribute IDBRequest request;
+
+ undefined advance([EnforceRange] unsigned long count);
+ undefined continue(optional any key);
+ undefined continuePrimaryKey(any key, any primaryKey);
+
+ [NewObject] IDBRequest update(any value);
+ [NewObject] IDBRequest delete();
+};
+
+enum IDBCursorDirection {
+ "next",
+ "nextunique",
+ "prev",
+ "prevunique"
+};
+
+[Exposed=(Window,Worker)]
+interface IDBCursorWithValue : IDBCursor {
+ readonly attribute any value;
+};
+
+[Exposed=(Window,Worker)]
+interface IDBTransaction : EventTarget {
+ readonly attribute DOMStringList objectStoreNames;
+ readonly attribute IDBTransactionMode mode;
+ readonly attribute IDBTransactionDurability durability;
+ [SameObject] readonly attribute IDBDatabase db;
+ readonly attribute DOMException? error;
+
+ IDBObjectStore objectStore(DOMString name);
+ undefined commit();
+ undefined abort();
+
+ // Event handlers:
+ attribute EventHandler onabort;
+ attribute EventHandler oncomplete;
+ attribute EventHandler onerror;
+};
+
+enum IDBTransactionMode {
+ "readonly",
+ "readwrite",
+ "versionchange"
+};
diff --git a/test/wpt/tests/interfaces/KHR_parallel_shader_compile.idl b/test/wpt/tests/interfaces/KHR_parallel_shader_compile.idl
new file mode 100644
index 0000000..1470965
--- /dev/null
+++ b/test/wpt/tests/interfaces/KHR_parallel_shader_compile.idl
@@ -0,0 +1,9 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: WebGL KHR_parallel_shader_compile Extension Specification (https://registry.khronos.org/webgl/extensions/KHR_parallel_shader_compile/)
+
+[Exposed=(Window,Worker), LegacyNoInterfaceObject]
+interface KHR_parallel_shader_compile {
+ const GLenum COMPLETION_STATUS_KHR = 0x91B1;
+};
diff --git a/test/wpt/tests/interfaces/META.yml b/test/wpt/tests/interfaces/META.yml
new file mode 100644
index 0000000..c1dd8dd
--- /dev/null
+++ b/test/wpt/tests/interfaces/META.yml
@@ -0,0 +1,2 @@
+suggested_reviewers:
+ - foolip
diff --git a/test/wpt/tests/interfaces/OES_draw_buffers_indexed.idl b/test/wpt/tests/interfaces/OES_draw_buffers_indexed.idl
new file mode 100644
index 0000000..ea1e217
--- /dev/null
+++ b/test/wpt/tests/interfaces/OES_draw_buffers_indexed.idl
@@ -0,0 +1,26 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: WebGL OES_draw_buffers_indexed Extension Specification (https://registry.khronos.org/webgl/extensions/OES_draw_buffers_indexed/)
+
+[Exposed=(Window,Worker), LegacyNoInterfaceObject]
+interface OES_draw_buffers_indexed {
+ undefined enableiOES(GLenum target, GLuint index);
+
+ undefined disableiOES(GLenum target, GLuint index);
+
+ undefined blendEquationiOES(GLuint buf, GLenum mode);
+
+ undefined blendEquationSeparateiOES(GLuint buf,
+ GLenum modeRGB, GLenum modeAlpha);
+
+ undefined blendFunciOES(GLuint buf,
+ GLenum src, GLenum dst);
+
+ undefined blendFuncSeparateiOES(GLuint buf,
+ GLenum srcRGB, GLenum dstRGB,
+ GLenum srcAlpha, GLenum dstAlpha);
+
+ undefined colorMaskiOES(GLuint buf,
+ GLboolean r, GLboolean g, GLboolean b, GLboolean a);
+};
diff --git a/test/wpt/tests/interfaces/OES_element_index_uint.idl b/test/wpt/tests/interfaces/OES_element_index_uint.idl
new file mode 100644
index 0000000..df43a57
--- /dev/null
+++ b/test/wpt/tests/interfaces/OES_element_index_uint.idl
@@ -0,0 +1,8 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: WebGL OES_element_index_uint Khronos Ratified Extension Specification (https://registry.khronos.org/webgl/extensions/OES_element_index_uint/)
+
+[Exposed=(Window,Worker), LegacyNoInterfaceObject]
+interface OES_element_index_uint {
+};
diff --git a/test/wpt/tests/interfaces/OES_fbo_render_mipmap.idl b/test/wpt/tests/interfaces/OES_fbo_render_mipmap.idl
new file mode 100644
index 0000000..608c392
--- /dev/null
+++ b/test/wpt/tests/interfaces/OES_fbo_render_mipmap.idl
@@ -0,0 +1,8 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: WebGL OES_fbo_render_mipmap Extension Specification (https://registry.khronos.org/webgl/extensions/OES_fbo_render_mipmap/)
+
+[Exposed=(Window,Worker), LegacyNoInterfaceObject]
+interface OES_fbo_render_mipmap {
+};
diff --git a/test/wpt/tests/interfaces/OES_standard_derivatives.idl b/test/wpt/tests/interfaces/OES_standard_derivatives.idl
new file mode 100644
index 0000000..7bf073a
--- /dev/null
+++ b/test/wpt/tests/interfaces/OES_standard_derivatives.idl
@@ -0,0 +1,9 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: WebGL OES_standard_derivatives Khronos Ratified Extension Specification (https://registry.khronos.org/webgl/extensions/OES_standard_derivatives/)
+
+[Exposed=(Window,Worker), LegacyNoInterfaceObject]
+interface OES_standard_derivatives {
+ const GLenum FRAGMENT_SHADER_DERIVATIVE_HINT_OES = 0x8B8B;
+};
diff --git a/test/wpt/tests/interfaces/OES_texture_float.idl b/test/wpt/tests/interfaces/OES_texture_float.idl
new file mode 100644
index 0000000..a1bb79c
--- /dev/null
+++ b/test/wpt/tests/interfaces/OES_texture_float.idl
@@ -0,0 +1,7 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: WebGL OES_texture_float Khronos Ratified Extension Specification (https://registry.khronos.org/webgl/extensions/OES_texture_float/)
+
+[Exposed=(Window,Worker), LegacyNoInterfaceObject]
+interface OES_texture_float { };
diff --git a/test/wpt/tests/interfaces/OES_texture_float_linear.idl b/test/wpt/tests/interfaces/OES_texture_float_linear.idl
new file mode 100644
index 0000000..4626297
--- /dev/null
+++ b/test/wpt/tests/interfaces/OES_texture_float_linear.idl
@@ -0,0 +1,7 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: WebGL OES_texture_float_linear Khronos Ratified Extension Specification (https://registry.khronos.org/webgl/extensions/OES_texture_float_linear/)
+
+[Exposed=(Window,Worker), LegacyNoInterfaceObject]
+interface OES_texture_float_linear { };
diff --git a/test/wpt/tests/interfaces/OES_texture_half_float.idl b/test/wpt/tests/interfaces/OES_texture_half_float.idl
new file mode 100644
index 0000000..be41454
--- /dev/null
+++ b/test/wpt/tests/interfaces/OES_texture_half_float.idl
@@ -0,0 +1,9 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: WebGL OES_texture_half_float Khronos Ratified Extension Specification (https://registry.khronos.org/webgl/extensions/OES_texture_half_float/)
+
+[Exposed=(Window,Worker), LegacyNoInterfaceObject]
+interface OES_texture_half_float {
+ const GLenum HALF_FLOAT_OES = 0x8D61;
+};
diff --git a/test/wpt/tests/interfaces/OES_texture_half_float_linear.idl b/test/wpt/tests/interfaces/OES_texture_half_float_linear.idl
new file mode 100644
index 0000000..2f1a999
--- /dev/null
+++ b/test/wpt/tests/interfaces/OES_texture_half_float_linear.idl
@@ -0,0 +1,7 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: WebGL OES_texture_half_float_linear Khronos Ratified Extension Specification (https://registry.khronos.org/webgl/extensions/OES_texture_half_float_linear/)
+
+[Exposed=(Window,Worker), LegacyNoInterfaceObject]
+interface OES_texture_half_float_linear { };
diff --git a/test/wpt/tests/interfaces/OES_vertex_array_object.idl b/test/wpt/tests/interfaces/OES_vertex_array_object.idl
new file mode 100644
index 0000000..8aeb745
--- /dev/null
+++ b/test/wpt/tests/interfaces/OES_vertex_array_object.idl
@@ -0,0 +1,18 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: WebGL OES_vertex_array_object Khronos Ratified Extension Specification (https://registry.khronos.org/webgl/extensions/OES_vertex_array_object/)
+
+[Exposed=(Window,Worker), LegacyNoInterfaceObject]
+interface WebGLVertexArrayObjectOES : WebGLObject {
+};
+
+[Exposed=(Window,Worker), LegacyNoInterfaceObject]
+interface OES_vertex_array_object {
+ const GLenum VERTEX_ARRAY_BINDING_OES = 0x85B5;
+
+ WebGLVertexArrayObjectOES? createVertexArrayOES();
+ undefined deleteVertexArrayOES(WebGLVertexArrayObjectOES? arrayObject);
+ [WebGLHandlesContextLoss] GLboolean isVertexArrayOES(WebGLVertexArrayObjectOES? arrayObject);
+ undefined bindVertexArrayOES(WebGLVertexArrayObjectOES? arrayObject);
+};
diff --git a/test/wpt/tests/interfaces/OVR_multiview2.idl b/test/wpt/tests/interfaces/OVR_multiview2.idl
new file mode 100644
index 0000000..9c1ecc4
--- /dev/null
+++ b/test/wpt/tests/interfaces/OVR_multiview2.idl
@@ -0,0 +1,14 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: WebGL OVR_multiview2 Extension Specification (https://registry.khronos.org/webgl/extensions/OVR_multiview2/)
+
+[Exposed=(Window,Worker), LegacyNoInterfaceObject]
+interface OVR_multiview2 {
+ const GLenum FRAMEBUFFER_ATTACHMENT_TEXTURE_NUM_VIEWS_OVR = 0x9630;
+ const GLenum FRAMEBUFFER_ATTACHMENT_TEXTURE_BASE_VIEW_INDEX_OVR = 0x9632;
+ const GLenum MAX_VIEWS_OVR = 0x9631;
+ const GLenum FRAMEBUFFER_INCOMPLETE_VIEW_TARGETS_OVR = 0x9633;
+
+ undefined framebufferTextureMultiviewOVR(GLenum target, GLenum attachment, WebGLTexture? texture, GLint level, GLint baseViewIndex, GLsizei numViews);
+};
diff --git a/test/wpt/tests/interfaces/README.md b/test/wpt/tests/interfaces/README.md
new file mode 100644
index 0000000..5e948ad
--- /dev/null
+++ b/test/wpt/tests/interfaces/README.md
@@ -0,0 +1,3 @@
+This directory contains [Web IDL](https://webidl.spec.whatwg.org/) interface definitions for use in idlharness.js tests.
+
+The `.idl` files (except `*.tentative.idl`) are copied from [@webref/idl](https://www.npmjs.com/package/@webref/idl) by a [workflow](https://github.com/web-platform-tests/wpt/blob/master/.github/workflows/interfaces.yml) that tries to sync the files daily. The resulting pull requests require manual review but can be approved/merged by anyone with write access.
diff --git a/test/wpt/tests/interfaces/SVG.idl b/test/wpt/tests/interfaces/SVG.idl
new file mode 100644
index 0000000..3a0b861
--- /dev/null
+++ b/test/wpt/tests/interfaces/SVG.idl
@@ -0,0 +1,693 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Scalable Vector Graphics (SVG) 2 (https://svgwg.org/svg2-draft/)
+
+[Exposed=Window]
+interface SVGElement : Element {
+
+ [SameObject] readonly attribute SVGAnimatedString className;
+
+ readonly attribute SVGSVGElement? ownerSVGElement;
+ readonly attribute SVGElement? viewportElement;
+};
+
+SVGElement includes GlobalEventHandlers;
+SVGElement includes SVGElementInstance;
+SVGElement includes HTMLOrSVGElement;
+
+dictionary SVGBoundingBoxOptions {
+ boolean fill = true;
+ boolean stroke = false;
+ boolean markers = false;
+ boolean clipped = false;
+};
+
+[Exposed=Window]
+interface SVGGraphicsElement : SVGElement {
+ [SameObject] readonly attribute SVGAnimatedTransformList transform;
+
+ DOMRect getBBox(optional SVGBoundingBoxOptions options = {});
+ DOMMatrix? getCTM();
+ DOMMatrix? getScreenCTM();
+};
+
+SVGGraphicsElement includes SVGTests;
+
+[Exposed=Window]
+interface SVGGeometryElement : SVGGraphicsElement {
+ [SameObject] readonly attribute SVGAnimatedNumber pathLength;
+
+ boolean isPointInFill(optional DOMPointInit point = {});
+ boolean isPointInStroke(optional DOMPointInit point = {});
+ float getTotalLength();
+ DOMPoint getPointAtLength(float distance);
+};
+
+[Exposed=Window]
+interface SVGNumber {
+ attribute float value;
+};
+
+[Exposed=Window]
+interface SVGLength {
+
+ // Length Unit Types
+ const unsigned short SVG_LENGTHTYPE_UNKNOWN = 0;
+ const unsigned short SVG_LENGTHTYPE_NUMBER = 1;
+ const unsigned short SVG_LENGTHTYPE_PERCENTAGE = 2;
+ const unsigned short SVG_LENGTHTYPE_EMS = 3;
+ const unsigned short SVG_LENGTHTYPE_EXS = 4;
+ const unsigned short SVG_LENGTHTYPE_PX = 5;
+ const unsigned short SVG_LENGTHTYPE_CM = 6;
+ const unsigned short SVG_LENGTHTYPE_MM = 7;
+ const unsigned short SVG_LENGTHTYPE_IN = 8;
+ const unsigned short SVG_LENGTHTYPE_PT = 9;
+ const unsigned short SVG_LENGTHTYPE_PC = 10;
+
+ readonly attribute unsigned short unitType;
+ attribute float value;
+ attribute float valueInSpecifiedUnits;
+ attribute DOMString valueAsString;
+
+ undefined newValueSpecifiedUnits(unsigned short unitType, float valueInSpecifiedUnits);
+ undefined convertToSpecifiedUnits(unsigned short unitType);
+};
+
+[Exposed=Window]
+interface SVGAngle {
+
+ // Angle Unit Types
+ const unsigned short SVG_ANGLETYPE_UNKNOWN = 0;
+ const unsigned short SVG_ANGLETYPE_UNSPECIFIED = 1;
+ const unsigned short SVG_ANGLETYPE_DEG = 2;
+ const unsigned short SVG_ANGLETYPE_RAD = 3;
+ const unsigned short SVG_ANGLETYPE_GRAD = 4;
+
+ readonly attribute unsigned short unitType;
+ attribute float value;
+ attribute float valueInSpecifiedUnits;
+ attribute DOMString valueAsString;
+
+ undefined newValueSpecifiedUnits(unsigned short unitType, float valueInSpecifiedUnits);
+ undefined convertToSpecifiedUnits(unsigned short unitType);
+};
+
+[Exposed=Window]
+interface SVGNumberList {
+
+ readonly attribute unsigned long length;
+ readonly attribute unsigned long numberOfItems;
+
+ undefined clear();
+ SVGNumber initialize(SVGNumber newItem);
+ getter SVGNumber getItem(unsigned long index);
+ SVGNumber insertItemBefore(SVGNumber newItem, unsigned long index);
+ SVGNumber replaceItem(SVGNumber newItem, unsigned long index);
+ SVGNumber removeItem(unsigned long index);
+ SVGNumber appendItem(SVGNumber newItem);
+ setter undefined (unsigned long index, SVGNumber newItem);
+};
+
+[Exposed=Window]
+interface SVGLengthList {
+
+ readonly attribute unsigned long length;
+ readonly attribute unsigned long numberOfItems;
+
+ undefined clear();
+ SVGLength initialize(SVGLength newItem);
+ getter SVGLength getItem(unsigned long index);
+ SVGLength insertItemBefore(SVGLength newItem, unsigned long index);
+ SVGLength replaceItem(SVGLength newItem, unsigned long index);
+ SVGLength removeItem(unsigned long index);
+ SVGLength appendItem(SVGLength newItem);
+ setter undefined (unsigned long index, SVGLength newItem);
+};
+
+[Exposed=Window]
+interface SVGStringList {
+
+ readonly attribute unsigned long length;
+ readonly attribute unsigned long numberOfItems;
+
+ undefined clear();
+ DOMString initialize(DOMString newItem);
+ getter DOMString getItem(unsigned long index);
+ DOMString insertItemBefore(DOMString newItem, unsigned long index);
+ DOMString replaceItem(DOMString newItem, unsigned long index);
+ DOMString removeItem(unsigned long index);
+ DOMString appendItem(DOMString newItem);
+ setter undefined (unsigned long index, DOMString newItem);
+};
+
+[Exposed=Window]
+interface SVGAnimatedBoolean {
+ attribute boolean baseVal;
+ readonly attribute boolean animVal;
+};
+
+[Exposed=Window]
+interface SVGAnimatedEnumeration {
+ attribute unsigned short baseVal;
+ readonly attribute unsigned short animVal;
+};
+
+[Exposed=Window]
+interface SVGAnimatedInteger {
+ attribute long baseVal;
+ readonly attribute long animVal;
+};
+
+[Exposed=Window]
+interface SVGAnimatedNumber {
+ attribute float baseVal;
+ readonly attribute float animVal;
+};
+
+[Exposed=Window]
+interface SVGAnimatedLength {
+ [SameObject] readonly attribute SVGLength baseVal;
+ [SameObject] readonly attribute SVGLength animVal;
+};
+
+[Exposed=Window]
+interface SVGAnimatedAngle {
+ [SameObject] readonly attribute SVGAngle baseVal;
+ [SameObject] readonly attribute SVGAngle animVal;
+};
+
+[Exposed=Window]
+interface SVGAnimatedString {
+ attribute DOMString baseVal;
+ readonly attribute DOMString animVal;
+};
+
+[Exposed=Window]
+interface SVGAnimatedRect {
+ [SameObject] readonly attribute DOMRect baseVal;
+ [SameObject] readonly attribute DOMRectReadOnly animVal;
+};
+
+[Exposed=Window]
+interface SVGAnimatedNumberList {
+ [SameObject] readonly attribute SVGNumberList baseVal;
+ [SameObject] readonly attribute SVGNumberList animVal;
+};
+
+[Exposed=Window]
+interface SVGAnimatedLengthList {
+ [SameObject] readonly attribute SVGLengthList baseVal;
+ [SameObject] readonly attribute SVGLengthList animVal;
+};
+
+[Exposed=Window]
+interface SVGUnitTypes {
+ // Unit Types
+ const unsigned short SVG_UNIT_TYPE_UNKNOWN = 0;
+ const unsigned short SVG_UNIT_TYPE_USERSPACEONUSE = 1;
+ const unsigned short SVG_UNIT_TYPE_OBJECTBOUNDINGBOX = 2;
+};
+
+interface mixin SVGTests {
+ [SameObject] readonly attribute SVGStringList requiredExtensions;
+ [SameObject] readonly attribute SVGStringList systemLanguage;
+};
+
+interface mixin SVGFitToViewBox {
+ [SameObject] readonly attribute SVGAnimatedRect viewBox;
+ [SameObject] readonly attribute SVGAnimatedPreserveAspectRatio preserveAspectRatio;
+};
+
+interface mixin SVGURIReference {
+ [SameObject] readonly attribute SVGAnimatedString href;
+};
+
+partial interface Document {
+ readonly attribute SVGSVGElement? rootElement;
+};
+
+[Exposed=Window]
+interface SVGSVGElement : SVGGraphicsElement {
+
+ [SameObject] readonly attribute SVGAnimatedLength x;
+ [SameObject] readonly attribute SVGAnimatedLength y;
+ [SameObject] readonly attribute SVGAnimatedLength width;
+ [SameObject] readonly attribute SVGAnimatedLength height;
+
+ attribute float currentScale;
+ [SameObject] readonly attribute DOMPointReadOnly currentTranslate;
+
+ NodeList getIntersectionList(DOMRectReadOnly rect, SVGElement? referenceElement);
+ NodeList getEnclosureList(DOMRectReadOnly rect, SVGElement? referenceElement);
+ boolean checkIntersection(SVGElement element, DOMRectReadOnly rect);
+ boolean checkEnclosure(SVGElement element, DOMRectReadOnly rect);
+
+ undefined deselectAll();
+
+ SVGNumber createSVGNumber();
+ SVGLength createSVGLength();
+ SVGAngle createSVGAngle();
+ DOMPoint createSVGPoint();
+ DOMMatrix createSVGMatrix();
+ DOMRect createSVGRect();
+ SVGTransform createSVGTransform();
+ SVGTransform createSVGTransformFromMatrix(optional DOMMatrix2DInit matrix = {});
+
+ Element getElementById(DOMString elementId);
+
+ // Deprecated methods that have no effect when called,
+ // but which are kept for compatibility reasons.
+ unsigned long suspendRedraw(unsigned long maxWaitMilliseconds);
+ undefined unsuspendRedraw(unsigned long suspendHandleID);
+ undefined unsuspendRedrawAll();
+ undefined forceRedraw();
+};
+
+SVGSVGElement includes SVGFitToViewBox;
+SVGSVGElement includes WindowEventHandlers;
+
+[Exposed=Window]
+interface SVGGElement : SVGGraphicsElement {
+};
+
+[Exposed=Window]
+interface SVGDefsElement : SVGGraphicsElement {
+};
+
+[Exposed=Window]
+interface SVGDescElement : SVGElement {
+};
+
+[Exposed=Window]
+interface SVGMetadataElement : SVGElement {
+};
+
+[Exposed=Window]
+interface SVGTitleElement : SVGElement {
+};
+
+[Exposed=Window]
+interface SVGSymbolElement : SVGGraphicsElement {
+};
+
+SVGSymbolElement includes SVGFitToViewBox;
+
+[Exposed=Window]
+interface SVGUseElement : SVGGraphicsElement {
+ [SameObject] readonly attribute SVGAnimatedLength x;
+ [SameObject] readonly attribute SVGAnimatedLength y;
+ [SameObject] readonly attribute SVGAnimatedLength width;
+ [SameObject] readonly attribute SVGAnimatedLength height;
+ [SameObject] readonly attribute SVGElement? instanceRoot;
+ [SameObject] readonly attribute SVGElement? animatedInstanceRoot;
+};
+
+SVGUseElement includes SVGURIReference;
+
+[Exposed=Window]
+interface SVGUseElementShadowRoot : ShadowRoot {
+};
+
+interface mixin SVGElementInstance {
+ [SameObject] readonly attribute SVGElement? correspondingElement;
+ [SameObject] readonly attribute SVGUseElement? correspondingUseElement;
+};
+
+[Exposed=Window]
+interface ShadowAnimation : Animation {
+ constructor(Animation source, (Element or CSSPseudoElement) newTarget);
+ [SameObject] readonly attribute Animation sourceAnimation;
+};
+
+[Exposed=Window]
+interface SVGSwitchElement : SVGGraphicsElement {
+};
+
+interface mixin GetSVGDocument {
+ Document getSVGDocument();
+};
+
+[Exposed=Window]
+interface SVGStyleElement : SVGElement {
+ attribute DOMString type;
+ attribute DOMString media;
+ attribute DOMString title;
+};
+
+SVGStyleElement includes LinkStyle;
+
+[Exposed=Window]
+interface SVGTransform {
+
+ // Transform Types
+ const unsigned short SVG_TRANSFORM_UNKNOWN = 0;
+ const unsigned short SVG_TRANSFORM_MATRIX = 1;
+ const unsigned short SVG_TRANSFORM_TRANSLATE = 2;
+ const unsigned short SVG_TRANSFORM_SCALE = 3;
+ const unsigned short SVG_TRANSFORM_ROTATE = 4;
+ const unsigned short SVG_TRANSFORM_SKEWX = 5;
+ const unsigned short SVG_TRANSFORM_SKEWY = 6;
+
+ readonly attribute unsigned short type;
+ [SameObject] readonly attribute DOMMatrix matrix;
+ readonly attribute float angle;
+
+ undefined setMatrix(optional DOMMatrix2DInit matrix = {});
+ undefined setTranslate(float tx, float ty);
+ undefined setScale(float sx, float sy);
+ undefined setRotate(float angle, float cx, float cy);
+ undefined setSkewX(float angle);
+ undefined setSkewY(float angle);
+};
+
+[Exposed=Window]
+interface SVGTransformList {
+
+ readonly attribute unsigned long length;
+ readonly attribute unsigned long numberOfItems;
+
+ undefined clear();
+ SVGTransform initialize(SVGTransform newItem);
+ getter SVGTransform getItem(unsigned long index);
+ SVGTransform insertItemBefore(SVGTransform newItem, unsigned long index);
+ SVGTransform replaceItem(SVGTransform newItem, unsigned long index);
+ SVGTransform removeItem(unsigned long index);
+ SVGTransform appendItem(SVGTransform newItem);
+ setter undefined (unsigned long index, SVGTransform newItem);
+
+ // Additional methods not common to other list interfaces.
+ SVGTransform createSVGTransformFromMatrix(optional DOMMatrix2DInit matrix = {});
+ SVGTransform? consolidate();
+};
+
+[Exposed=Window]
+interface SVGAnimatedTransformList {
+ [SameObject] readonly attribute SVGTransformList baseVal;
+ [SameObject] readonly attribute SVGTransformList animVal;
+};
+
+[Exposed=Window]
+interface SVGPreserveAspectRatio {
+
+ // Alignment Types
+ const unsigned short SVG_PRESERVEASPECTRATIO_UNKNOWN = 0;
+ const unsigned short SVG_PRESERVEASPECTRATIO_NONE = 1;
+ const unsigned short SVG_PRESERVEASPECTRATIO_XMINYMIN = 2;
+ const unsigned short SVG_PRESERVEASPECTRATIO_XMIDYMIN = 3;
+ const unsigned short SVG_PRESERVEASPECTRATIO_XMAXYMIN = 4;
+ const unsigned short SVG_PRESERVEASPECTRATIO_XMINYMID = 5;
+ const unsigned short SVG_PRESERVEASPECTRATIO_XMIDYMID = 6;
+ const unsigned short SVG_PRESERVEASPECTRATIO_XMAXYMID = 7;
+ const unsigned short SVG_PRESERVEASPECTRATIO_XMINYMAX = 8;
+ const unsigned short SVG_PRESERVEASPECTRATIO_XMIDYMAX = 9;
+ const unsigned short SVG_PRESERVEASPECTRATIO_XMAXYMAX = 10;
+
+ // Meet-or-slice Types
+ const unsigned short SVG_MEETORSLICE_UNKNOWN = 0;
+ const unsigned short SVG_MEETORSLICE_MEET = 1;
+ const unsigned short SVG_MEETORSLICE_SLICE = 2;
+
+ attribute unsigned short align;
+ attribute unsigned short meetOrSlice;
+};
+
+[Exposed=Window]
+interface SVGAnimatedPreserveAspectRatio {
+ [SameObject] readonly attribute SVGPreserveAspectRatio baseVal;
+ [SameObject] readonly attribute SVGPreserveAspectRatio animVal;
+};
+
+[Exposed=Window]
+interface SVGPathElement : SVGGeometryElement {
+};
+
+[Exposed=Window]
+interface SVGRectElement : SVGGeometryElement {
+ [SameObject] readonly attribute SVGAnimatedLength x;
+ [SameObject] readonly attribute SVGAnimatedLength y;
+ [SameObject] readonly attribute SVGAnimatedLength width;
+ [SameObject] readonly attribute SVGAnimatedLength height;
+ [SameObject] readonly attribute SVGAnimatedLength rx;
+ [SameObject] readonly attribute SVGAnimatedLength ry;
+};
+
+[Exposed=Window]
+interface SVGCircleElement : SVGGeometryElement {
+ [SameObject] readonly attribute SVGAnimatedLength cx;
+ [SameObject] readonly attribute SVGAnimatedLength cy;
+ [SameObject] readonly attribute SVGAnimatedLength r;
+};
+
+[Exposed=Window]
+interface SVGEllipseElement : SVGGeometryElement {
+ [SameObject] readonly attribute SVGAnimatedLength cx;
+ [SameObject] readonly attribute SVGAnimatedLength cy;
+ [SameObject] readonly attribute SVGAnimatedLength rx;
+ [SameObject] readonly attribute SVGAnimatedLength ry;
+};
+
+[Exposed=Window]
+interface SVGLineElement : SVGGeometryElement {
+ [SameObject] readonly attribute SVGAnimatedLength x1;
+ [SameObject] readonly attribute SVGAnimatedLength y1;
+ [SameObject] readonly attribute SVGAnimatedLength x2;
+ [SameObject] readonly attribute SVGAnimatedLength y2;
+};
+
+interface mixin SVGAnimatedPoints {
+ [SameObject] readonly attribute SVGPointList points;
+ [SameObject] readonly attribute SVGPointList animatedPoints;
+};
+
+[Exposed=Window]
+interface SVGPointList {
+
+ readonly attribute unsigned long length;
+ readonly attribute unsigned long numberOfItems;
+
+ undefined clear();
+ DOMPoint initialize(DOMPoint newItem);
+ getter DOMPoint getItem(unsigned long index);
+ DOMPoint insertItemBefore(DOMPoint newItem, unsigned long index);
+ DOMPoint replaceItem(DOMPoint newItem, unsigned long index);
+ DOMPoint removeItem(unsigned long index);
+ DOMPoint appendItem(DOMPoint newItem);
+ setter undefined (unsigned long index, DOMPoint newItem);
+};
+
+[Exposed=Window]
+interface SVGPolylineElement : SVGGeometryElement {
+};
+
+SVGPolylineElement includes SVGAnimatedPoints;
+
+[Exposed=Window]
+interface SVGPolygonElement : SVGGeometryElement {
+};
+
+SVGPolygonElement includes SVGAnimatedPoints;
+
+[Exposed=Window]
+interface SVGTextContentElement : SVGGraphicsElement {
+
+ // lengthAdjust Types
+ const unsigned short LENGTHADJUST_UNKNOWN = 0;
+ const unsigned short LENGTHADJUST_SPACING = 1;
+ const unsigned short LENGTHADJUST_SPACINGANDGLYPHS = 2;
+
+ [SameObject] readonly attribute SVGAnimatedLength textLength;
+ [SameObject] readonly attribute SVGAnimatedEnumeration lengthAdjust;
+
+ long getNumberOfChars();
+ float getComputedTextLength();
+ float getSubStringLength(unsigned long charnum, unsigned long nchars);
+ DOMPoint getStartPositionOfChar(unsigned long charnum);
+ DOMPoint getEndPositionOfChar(unsigned long charnum);
+ DOMRect getExtentOfChar(unsigned long charnum);
+ float getRotationOfChar(unsigned long charnum);
+ long getCharNumAtPosition(optional DOMPointInit point = {});
+ undefined selectSubString(unsigned long charnum, unsigned long nchars);
+};
+
+[Exposed=Window]
+interface SVGTextPositioningElement : SVGTextContentElement {
+ [SameObject] readonly attribute SVGAnimatedLengthList x;
+ [SameObject] readonly attribute SVGAnimatedLengthList y;
+ [SameObject] readonly attribute SVGAnimatedLengthList dx;
+ [SameObject] readonly attribute SVGAnimatedLengthList dy;
+ [SameObject] readonly attribute SVGAnimatedNumberList rotate;
+};
+
+[Exposed=Window]
+interface SVGTextElement : SVGTextPositioningElement {
+};
+
+[Exposed=Window]
+interface SVGTSpanElement : SVGTextPositioningElement {
+};
+
+[Exposed=Window]
+interface SVGTextPathElement : SVGTextContentElement {
+
+ // textPath Method Types
+ const unsigned short TEXTPATH_METHODTYPE_UNKNOWN = 0;
+ const unsigned short TEXTPATH_METHODTYPE_ALIGN = 1;
+ const unsigned short TEXTPATH_METHODTYPE_STRETCH = 2;
+
+ // textPath Spacing Types
+ const unsigned short TEXTPATH_SPACINGTYPE_UNKNOWN = 0;
+ const unsigned short TEXTPATH_SPACINGTYPE_AUTO = 1;
+ const unsigned short TEXTPATH_SPACINGTYPE_EXACT = 2;
+
+ [SameObject] readonly attribute SVGAnimatedLength startOffset;
+ [SameObject] readonly attribute SVGAnimatedEnumeration method;
+ [SameObject] readonly attribute SVGAnimatedEnumeration spacing;
+};
+
+SVGTextPathElement includes SVGURIReference;
+
+[Exposed=Window]
+interface SVGImageElement : SVGGraphicsElement {
+ [SameObject] readonly attribute SVGAnimatedLength x;
+ [SameObject] readonly attribute SVGAnimatedLength y;
+ [SameObject] readonly attribute SVGAnimatedLength width;
+ [SameObject] readonly attribute SVGAnimatedLength height;
+ [SameObject] readonly attribute SVGAnimatedPreserveAspectRatio preserveAspectRatio;
+ attribute DOMString? crossOrigin;
+};
+
+SVGImageElement includes SVGURIReference;
+
+[Exposed=Window]
+interface SVGForeignObjectElement : SVGGraphicsElement {
+ [SameObject] readonly attribute SVGAnimatedLength x;
+ [SameObject] readonly attribute SVGAnimatedLength y;
+ [SameObject] readonly attribute SVGAnimatedLength width;
+ [SameObject] readonly attribute SVGAnimatedLength height;
+};
+
+[Exposed=Window]
+interface SVGMarkerElement : SVGElement {
+
+ // Marker Unit Types
+ const unsigned short SVG_MARKERUNITS_UNKNOWN = 0;
+ const unsigned short SVG_MARKERUNITS_USERSPACEONUSE = 1;
+ const unsigned short SVG_MARKERUNITS_STROKEWIDTH = 2;
+
+ // Marker Orientation Types
+ const unsigned short SVG_MARKER_ORIENT_UNKNOWN = 0;
+ const unsigned short SVG_MARKER_ORIENT_AUTO = 1;
+ const unsigned short SVG_MARKER_ORIENT_ANGLE = 2;
+
+ [SameObject] readonly attribute SVGAnimatedLength refX;
+ [SameObject] readonly attribute SVGAnimatedLength refY;
+ [SameObject] readonly attribute SVGAnimatedEnumeration markerUnits;
+ [SameObject] readonly attribute SVGAnimatedLength markerWidth;
+ [SameObject] readonly attribute SVGAnimatedLength markerHeight;
+ [SameObject] readonly attribute SVGAnimatedEnumeration orientType;
+ [SameObject] readonly attribute SVGAnimatedAngle orientAngle;
+ attribute DOMString orient;
+
+ undefined setOrientToAuto();
+ undefined setOrientToAngle(SVGAngle angle);
+};
+
+SVGMarkerElement includes SVGFitToViewBox;
+
+[Exposed=Window]
+interface SVGGradientElement : SVGElement {
+
+ // Spread Method Types
+ const unsigned short SVG_SPREADMETHOD_UNKNOWN = 0;
+ const unsigned short SVG_SPREADMETHOD_PAD = 1;
+ const unsigned short SVG_SPREADMETHOD_REFLECT = 2;
+ const unsigned short SVG_SPREADMETHOD_REPEAT = 3;
+
+ [SameObject] readonly attribute SVGAnimatedEnumeration gradientUnits;
+ [SameObject] readonly attribute SVGAnimatedTransformList gradientTransform;
+ [SameObject] readonly attribute SVGAnimatedEnumeration spreadMethod;
+};
+
+SVGGradientElement includes SVGURIReference;
+
+[Exposed=Window]
+interface SVGLinearGradientElement : SVGGradientElement {
+ [SameObject] readonly attribute SVGAnimatedLength x1;
+ [SameObject] readonly attribute SVGAnimatedLength y1;
+ [SameObject] readonly attribute SVGAnimatedLength x2;
+ [SameObject] readonly attribute SVGAnimatedLength y2;
+};
+
+[Exposed=Window]
+interface SVGRadialGradientElement : SVGGradientElement {
+ [SameObject] readonly attribute SVGAnimatedLength cx;
+ [SameObject] readonly attribute SVGAnimatedLength cy;
+ [SameObject] readonly attribute SVGAnimatedLength r;
+ [SameObject] readonly attribute SVGAnimatedLength fx;
+ [SameObject] readonly attribute SVGAnimatedLength fy;
+ [SameObject] readonly attribute SVGAnimatedLength fr;
+};
+
+[Exposed=Window]
+interface SVGStopElement : SVGElement {
+ [SameObject] readonly attribute SVGAnimatedNumber offset;
+};
+
+[Exposed=Window]
+interface SVGPatternElement : SVGElement {
+ [SameObject] readonly attribute SVGAnimatedEnumeration patternUnits;
+ [SameObject] readonly attribute SVGAnimatedEnumeration patternContentUnits;
+ [SameObject] readonly attribute SVGAnimatedTransformList patternTransform;
+ [SameObject] readonly attribute SVGAnimatedLength x;
+ [SameObject] readonly attribute SVGAnimatedLength y;
+ [SameObject] readonly attribute SVGAnimatedLength width;
+ [SameObject] readonly attribute SVGAnimatedLength height;
+};
+
+SVGPatternElement includes SVGFitToViewBox;
+SVGPatternElement includes SVGURIReference;
+
+[Exposed=Window]
+interface SVGScriptElement : SVGElement {
+ attribute DOMString type;
+ attribute DOMString? crossOrigin;
+};
+
+SVGScriptElement includes SVGURIReference;
+
+[Exposed=Window]
+interface SVGAElement : SVGGraphicsElement {
+ [SameObject] readonly attribute SVGAnimatedString target;
+ attribute DOMString download;
+ attribute USVString ping;
+ attribute DOMString rel;
+ [SameObject, PutForwards=value] readonly attribute DOMTokenList relList;
+ attribute DOMString hreflang;
+ attribute DOMString type;
+
+ attribute DOMString text;
+
+ attribute DOMString referrerPolicy;
+};
+
+SVGAElement includes SVGURIReference;
+
+// Inline HTMLHyperlinkElementUtils except href, which conflicts.
+partial interface SVGAElement {
+ readonly attribute USVString origin;
+ [CEReactions] attribute USVString protocol;
+ [CEReactions] attribute USVString username;
+ [CEReactions] attribute USVString password;
+ [CEReactions] attribute USVString host;
+ [CEReactions] attribute USVString hostname;
+ [CEReactions] attribute USVString port;
+ [CEReactions] attribute USVString pathname;
+ [CEReactions] attribute USVString search;
+ [CEReactions] attribute USVString hash;
+};
+
+[Exposed=Window]
+interface SVGViewElement : SVGElement {};
+
+SVGViewElement includes SVGFitToViewBox;
diff --git a/test/wpt/tests/interfaces/WEBGL_blend_equation_advanced_coherent.idl b/test/wpt/tests/interfaces/WEBGL_blend_equation_advanced_coherent.idl
new file mode 100644
index 0000000..2208329
--- /dev/null
+++ b/test/wpt/tests/interfaces/WEBGL_blend_equation_advanced_coherent.idl
@@ -0,0 +1,23 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: WebGL WEBGL_blend_equation_advanced_coherent Extension Draft Specification (https://registry.khronos.org/webgl/extensions/WEBGL_blend_equation_advanced_coherent/)
+
+[Exposed=(Window,Worker), LegacyNoInterfaceObject]
+interface WEBGL_blend_equation_advanced_coherent {
+ const GLenum MULTIPLY = 0x9294;
+ const GLenum SCREEN = 0x9295;
+ const GLenum OVERLAY = 0x9296;
+ const GLenum DARKEN = 0x9297;
+ const GLenum LIGHTEN = 0x9298;
+ const GLenum COLORDODGE = 0x9299;
+ const GLenum COLORBURN = 0x929A;
+ const GLenum HARDLIGHT = 0x929B;
+ const GLenum SOFTLIGHT = 0x929C;
+ const GLenum DIFFERENCE = 0x929E;
+ const GLenum EXCLUSION = 0x92A0;
+ const GLenum HSL_HUE = 0x92AD;
+ const GLenum HSL_SATURATION = 0x92AE;
+ const GLenum HSL_COLOR = 0x92AF;
+ const GLenum HSL_LUMINOSITY = 0x92B0;
+};
diff --git a/test/wpt/tests/interfaces/WEBGL_clip_cull_distance.idl b/test/wpt/tests/interfaces/WEBGL_clip_cull_distance.idl
new file mode 100644
index 0000000..46fa921
--- /dev/null
+++ b/test/wpt/tests/interfaces/WEBGL_clip_cull_distance.idl
@@ -0,0 +1,20 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: WebGL WEBGL_clip_cull_distance Extension Draft Specification (https://registry.khronos.org/webgl/extensions/WEBGL_clip_cull_distance/)
+
+[Exposed=(Window,Worker), LegacyNoInterfaceObject]
+interface WEBGL_clip_cull_distance {
+ const GLenum MAX_CLIP_DISTANCES_WEBGL = 0x0D32;
+ const GLenum MAX_CULL_DISTANCES_WEBGL = 0x82F9;
+ const GLenum MAX_COMBINED_CLIP_AND_CULL_DISTANCES_WEBGL = 0x82FA;
+
+ const GLenum CLIP_DISTANCE0_WEBGL = 0x3000;
+ const GLenum CLIP_DISTANCE1_WEBGL = 0x3001;
+ const GLenum CLIP_DISTANCE2_WEBGL = 0x3002;
+ const GLenum CLIP_DISTANCE3_WEBGL = 0x3003;
+ const GLenum CLIP_DISTANCE4_WEBGL = 0x3004;
+ const GLenum CLIP_DISTANCE5_WEBGL = 0x3005;
+ const GLenum CLIP_DISTANCE6_WEBGL = 0x3006;
+ const GLenum CLIP_DISTANCE7_WEBGL = 0x3007;
+};
diff --git a/test/wpt/tests/interfaces/WEBGL_color_buffer_float.idl b/test/wpt/tests/interfaces/WEBGL_color_buffer_float.idl
new file mode 100644
index 0000000..b73f631
--- /dev/null
+++ b/test/wpt/tests/interfaces/WEBGL_color_buffer_float.idl
@@ -0,0 +1,11 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: WebGL WEBGL_color_buffer_float Extension Specification (https://registry.khronos.org/webgl/extensions/WEBGL_color_buffer_float/)
+
+[Exposed=(Window,Worker), LegacyNoInterfaceObject]
+interface WEBGL_color_buffer_float {
+ const GLenum RGBA32F_EXT = 0x8814;
+ const GLenum FRAMEBUFFER_ATTACHMENT_COMPONENT_TYPE_EXT = 0x8211;
+ const GLenum UNSIGNED_NORMALIZED_EXT = 0x8C17;
+}; // interface WEBGL_color_buffer_float
diff --git a/test/wpt/tests/interfaces/WEBGL_compressed_texture_astc.idl b/test/wpt/tests/interfaces/WEBGL_compressed_texture_astc.idl
new file mode 100644
index 0000000..9e4632f
--- /dev/null
+++ b/test/wpt/tests/interfaces/WEBGL_compressed_texture_astc.idl
@@ -0,0 +1,41 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: WebGL WEBGL_compressed_texture_astc Extension Specification (https://registry.khronos.org/webgl/extensions/WEBGL_compressed_texture_astc/)
+
+[Exposed=(Window,Worker), LegacyNoInterfaceObject]
+interface WEBGL_compressed_texture_astc {
+ /* Compressed Texture Format */
+ const GLenum COMPRESSED_RGBA_ASTC_4x4_KHR = 0x93B0;
+ const GLenum COMPRESSED_RGBA_ASTC_5x4_KHR = 0x93B1;
+ const GLenum COMPRESSED_RGBA_ASTC_5x5_KHR = 0x93B2;
+ const GLenum COMPRESSED_RGBA_ASTC_6x5_KHR = 0x93B3;
+ const GLenum COMPRESSED_RGBA_ASTC_6x6_KHR = 0x93B4;
+ const GLenum COMPRESSED_RGBA_ASTC_8x5_KHR = 0x93B5;
+ const GLenum COMPRESSED_RGBA_ASTC_8x6_KHR = 0x93B6;
+ const GLenum COMPRESSED_RGBA_ASTC_8x8_KHR = 0x93B7;
+ const GLenum COMPRESSED_RGBA_ASTC_10x5_KHR = 0x93B8;
+ const GLenum COMPRESSED_RGBA_ASTC_10x6_KHR = 0x93B9;
+ const GLenum COMPRESSED_RGBA_ASTC_10x8_KHR = 0x93BA;
+ const GLenum COMPRESSED_RGBA_ASTC_10x10_KHR = 0x93BB;
+ const GLenum COMPRESSED_RGBA_ASTC_12x10_KHR = 0x93BC;
+ const GLenum COMPRESSED_RGBA_ASTC_12x12_KHR = 0x93BD;
+
+ const GLenum COMPRESSED_SRGB8_ALPHA8_ASTC_4x4_KHR = 0x93D0;
+ const GLenum COMPRESSED_SRGB8_ALPHA8_ASTC_5x4_KHR = 0x93D1;
+ const GLenum COMPRESSED_SRGB8_ALPHA8_ASTC_5x5_KHR = 0x93D2;
+ const GLenum COMPRESSED_SRGB8_ALPHA8_ASTC_6x5_KHR = 0x93D3;
+ const GLenum COMPRESSED_SRGB8_ALPHA8_ASTC_6x6_KHR = 0x93D4;
+ const GLenum COMPRESSED_SRGB8_ALPHA8_ASTC_8x5_KHR = 0x93D5;
+ const GLenum COMPRESSED_SRGB8_ALPHA8_ASTC_8x6_KHR = 0x93D6;
+ const GLenum COMPRESSED_SRGB8_ALPHA8_ASTC_8x8_KHR = 0x93D7;
+ const GLenum COMPRESSED_SRGB8_ALPHA8_ASTC_10x5_KHR = 0x93D8;
+ const GLenum COMPRESSED_SRGB8_ALPHA8_ASTC_10x6_KHR = 0x93D9;
+ const GLenum COMPRESSED_SRGB8_ALPHA8_ASTC_10x8_KHR = 0x93DA;
+ const GLenum COMPRESSED_SRGB8_ALPHA8_ASTC_10x10_KHR = 0x93DB;
+ const GLenum COMPRESSED_SRGB8_ALPHA8_ASTC_12x10_KHR = 0x93DC;
+ const GLenum COMPRESSED_SRGB8_ALPHA8_ASTC_12x12_KHR = 0x93DD;
+
+ // Profile query support.
+ sequence<DOMString> getSupportedProfiles();
+};
diff --git a/test/wpt/tests/interfaces/WEBGL_compressed_texture_etc.idl b/test/wpt/tests/interfaces/WEBGL_compressed_texture_etc.idl
new file mode 100644
index 0000000..5174a08
--- /dev/null
+++ b/test/wpt/tests/interfaces/WEBGL_compressed_texture_etc.idl
@@ -0,0 +1,19 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: WebGL WEBGL_compressed_texture_etc Extension Specification (https://registry.khronos.org/webgl/extensions/WEBGL_compressed_texture_etc/)
+
+[Exposed=(Window,Worker), LegacyNoInterfaceObject]
+interface WEBGL_compressed_texture_etc {
+ /* Compressed Texture Formats */
+ const GLenum COMPRESSED_R11_EAC = 0x9270;
+ const GLenum COMPRESSED_SIGNED_R11_EAC = 0x9271;
+ const GLenum COMPRESSED_RG11_EAC = 0x9272;
+ const GLenum COMPRESSED_SIGNED_RG11_EAC = 0x9273;
+ const GLenum COMPRESSED_RGB8_ETC2 = 0x9274;
+ const GLenum COMPRESSED_SRGB8_ETC2 = 0x9275;
+ const GLenum COMPRESSED_RGB8_PUNCHTHROUGH_ALPHA1_ETC2 = 0x9276;
+ const GLenum COMPRESSED_SRGB8_PUNCHTHROUGH_ALPHA1_ETC2 = 0x9277;
+ const GLenum COMPRESSED_RGBA8_ETC2_EAC = 0x9278;
+ const GLenum COMPRESSED_SRGB8_ALPHA8_ETC2_EAC = 0x9279;
+};
diff --git a/test/wpt/tests/interfaces/WEBGL_compressed_texture_etc1.idl b/test/wpt/tests/interfaces/WEBGL_compressed_texture_etc1.idl
new file mode 100644
index 0000000..773697e
--- /dev/null
+++ b/test/wpt/tests/interfaces/WEBGL_compressed_texture_etc1.idl
@@ -0,0 +1,10 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: WebGL WEBGL_compressed_texture_etc1 Extension Specification (https://registry.khronos.org/webgl/extensions/WEBGL_compressed_texture_etc1/)
+
+[Exposed=(Window,Worker), LegacyNoInterfaceObject]
+interface WEBGL_compressed_texture_etc1 {
+ /* Compressed Texture Format */
+ const GLenum COMPRESSED_RGB_ETC1_WEBGL = 0x8D64;
+};
diff --git a/test/wpt/tests/interfaces/WEBGL_compressed_texture_pvrtc.idl b/test/wpt/tests/interfaces/WEBGL_compressed_texture_pvrtc.idl
new file mode 100644
index 0000000..5aa004a
--- /dev/null
+++ b/test/wpt/tests/interfaces/WEBGL_compressed_texture_pvrtc.idl
@@ -0,0 +1,13 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: WebGL WEBGL_compressed_texture_pvrtc Extension Specification (https://registry.khronos.org/webgl/extensions/WEBGL_compressed_texture_pvrtc/)
+
+[Exposed=(Window,Worker), LegacyNoInterfaceObject]
+interface WEBGL_compressed_texture_pvrtc {
+ /* Compressed Texture Formats */
+ const GLenum COMPRESSED_RGB_PVRTC_4BPPV1_IMG = 0x8C00;
+ const GLenum COMPRESSED_RGB_PVRTC_2BPPV1_IMG = 0x8C01;
+ const GLenum COMPRESSED_RGBA_PVRTC_4BPPV1_IMG = 0x8C02;
+ const GLenum COMPRESSED_RGBA_PVRTC_2BPPV1_IMG = 0x8C03;
+};
diff --git a/test/wpt/tests/interfaces/WEBGL_compressed_texture_s3tc.idl b/test/wpt/tests/interfaces/WEBGL_compressed_texture_s3tc.idl
new file mode 100644
index 0000000..6e7c4bd
--- /dev/null
+++ b/test/wpt/tests/interfaces/WEBGL_compressed_texture_s3tc.idl
@@ -0,0 +1,13 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: WebGL WEBGL_compressed_texture_s3tc Khronos Ratified Extension Specification (https://registry.khronos.org/webgl/extensions/WEBGL_compressed_texture_s3tc/)
+
+[Exposed=(Window,Worker), LegacyNoInterfaceObject]
+interface WEBGL_compressed_texture_s3tc {
+ /* Compressed Texture Formats */
+ const GLenum COMPRESSED_RGB_S3TC_DXT1_EXT = 0x83F0;
+ const GLenum COMPRESSED_RGBA_S3TC_DXT1_EXT = 0x83F1;
+ const GLenum COMPRESSED_RGBA_S3TC_DXT3_EXT = 0x83F2;
+ const GLenum COMPRESSED_RGBA_S3TC_DXT5_EXT = 0x83F3;
+};
diff --git a/test/wpt/tests/interfaces/WEBGL_compressed_texture_s3tc_srgb.idl b/test/wpt/tests/interfaces/WEBGL_compressed_texture_s3tc_srgb.idl
new file mode 100644
index 0000000..809265e
--- /dev/null
+++ b/test/wpt/tests/interfaces/WEBGL_compressed_texture_s3tc_srgb.idl
@@ -0,0 +1,13 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: WebGL WEBGL_compressed_texture_s3tc_srgb Extension Specification (https://registry.khronos.org/webgl/extensions/WEBGL_compressed_texture_s3tc_srgb/)
+
+[Exposed=(Window,Worker), LegacyNoInterfaceObject]
+interface WEBGL_compressed_texture_s3tc_srgb {
+ /* Compressed Texture Formats */
+ const GLenum COMPRESSED_SRGB_S3TC_DXT1_EXT = 0x8C4C;
+ const GLenum COMPRESSED_SRGB_ALPHA_S3TC_DXT1_EXT = 0x8C4D;
+ const GLenum COMPRESSED_SRGB_ALPHA_S3TC_DXT3_EXT = 0x8C4E;
+ const GLenum COMPRESSED_SRGB_ALPHA_S3TC_DXT5_EXT = 0x8C4F;
+};
diff --git a/test/wpt/tests/interfaces/WEBGL_debug_renderer_info.idl b/test/wpt/tests/interfaces/WEBGL_debug_renderer_info.idl
new file mode 100644
index 0000000..7694061
--- /dev/null
+++ b/test/wpt/tests/interfaces/WEBGL_debug_renderer_info.idl
@@ -0,0 +1,12 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: WebGL WEBGL_debug_renderer_info Khronos Ratified Extension Specification (https://registry.khronos.org/webgl/extensions/WEBGL_debug_renderer_info/)
+
+[Exposed=(Window,Worker), LegacyNoInterfaceObject]
+interface WEBGL_debug_renderer_info {
+
+ const GLenum UNMASKED_VENDOR_WEBGL = 0x9245;
+ const GLenum UNMASKED_RENDERER_WEBGL = 0x9246;
+
+};
diff --git a/test/wpt/tests/interfaces/WEBGL_debug_shaders.idl b/test/wpt/tests/interfaces/WEBGL_debug_shaders.idl
new file mode 100644
index 0000000..ecb48d0
--- /dev/null
+++ b/test/wpt/tests/interfaces/WEBGL_debug_shaders.idl
@@ -0,0 +1,11 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: WebGL WEBGL_debug_shaders Khronos Ratified Extension Specification (https://registry.khronos.org/webgl/extensions/WEBGL_debug_shaders/)
+
+[Exposed=(Window,Worker), LegacyNoInterfaceObject]
+interface WEBGL_debug_shaders {
+
+ DOMString getTranslatedShaderSource(WebGLShader shader);
+
+};
diff --git a/test/wpt/tests/interfaces/WEBGL_depth_texture.idl b/test/wpt/tests/interfaces/WEBGL_depth_texture.idl
new file mode 100644
index 0000000..a9ec791
--- /dev/null
+++ b/test/wpt/tests/interfaces/WEBGL_depth_texture.idl
@@ -0,0 +1,9 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: WebGL WEBGL_depth_texture Khronos Ratified Extension Specification (https://registry.khronos.org/webgl/extensions/WEBGL_depth_texture/)
+
+[Exposed=(Window,Worker), LegacyNoInterfaceObject]
+interface WEBGL_depth_texture {
+ const GLenum UNSIGNED_INT_24_8_WEBGL = 0x84FA;
+};
diff --git a/test/wpt/tests/interfaces/WEBGL_draw_buffers.idl b/test/wpt/tests/interfaces/WEBGL_draw_buffers.idl
new file mode 100644
index 0000000..3310388
--- /dev/null
+++ b/test/wpt/tests/interfaces/WEBGL_draw_buffers.idl
@@ -0,0 +1,46 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: WebGL WEBGL_draw_buffers Khronos Ratified Extension Specification (https://registry.khronos.org/webgl/extensions/WEBGL_draw_buffers/)
+
+[Exposed=(Window,Worker), LegacyNoInterfaceObject]
+interface WEBGL_draw_buffers {
+ const GLenum COLOR_ATTACHMENT0_WEBGL = 0x8CE0;
+ const GLenum COLOR_ATTACHMENT1_WEBGL = 0x8CE1;
+ const GLenum COLOR_ATTACHMENT2_WEBGL = 0x8CE2;
+ const GLenum COLOR_ATTACHMENT3_WEBGL = 0x8CE3;
+ const GLenum COLOR_ATTACHMENT4_WEBGL = 0x8CE4;
+ const GLenum COLOR_ATTACHMENT5_WEBGL = 0x8CE5;
+ const GLenum COLOR_ATTACHMENT6_WEBGL = 0x8CE6;
+ const GLenum COLOR_ATTACHMENT7_WEBGL = 0x8CE7;
+ const GLenum COLOR_ATTACHMENT8_WEBGL = 0x8CE8;
+ const GLenum COLOR_ATTACHMENT9_WEBGL = 0x8CE9;
+ const GLenum COLOR_ATTACHMENT10_WEBGL = 0x8CEA;
+ const GLenum COLOR_ATTACHMENT11_WEBGL = 0x8CEB;
+ const GLenum COLOR_ATTACHMENT12_WEBGL = 0x8CEC;
+ const GLenum COLOR_ATTACHMENT13_WEBGL = 0x8CED;
+ const GLenum COLOR_ATTACHMENT14_WEBGL = 0x8CEE;
+ const GLenum COLOR_ATTACHMENT15_WEBGL = 0x8CEF;
+
+ const GLenum DRAW_BUFFER0_WEBGL = 0x8825;
+ const GLenum DRAW_BUFFER1_WEBGL = 0x8826;
+ const GLenum DRAW_BUFFER2_WEBGL = 0x8827;
+ const GLenum DRAW_BUFFER3_WEBGL = 0x8828;
+ const GLenum DRAW_BUFFER4_WEBGL = 0x8829;
+ const GLenum DRAW_BUFFER5_WEBGL = 0x882A;
+ const GLenum DRAW_BUFFER6_WEBGL = 0x882B;
+ const GLenum DRAW_BUFFER7_WEBGL = 0x882C;
+ const GLenum DRAW_BUFFER8_WEBGL = 0x882D;
+ const GLenum DRAW_BUFFER9_WEBGL = 0x882E;
+ const GLenum DRAW_BUFFER10_WEBGL = 0x882F;
+ const GLenum DRAW_BUFFER11_WEBGL = 0x8830;
+ const GLenum DRAW_BUFFER12_WEBGL = 0x8831;
+ const GLenum DRAW_BUFFER13_WEBGL = 0x8832;
+ const GLenum DRAW_BUFFER14_WEBGL = 0x8833;
+ const GLenum DRAW_BUFFER15_WEBGL = 0x8834;
+
+ const GLenum MAX_COLOR_ATTACHMENTS_WEBGL = 0x8CDF;
+ const GLenum MAX_DRAW_BUFFERS_WEBGL = 0x8824;
+
+ undefined drawBuffersWEBGL(sequence<GLenum> buffers);
+};
diff --git a/test/wpt/tests/interfaces/WEBGL_draw_instanced_base_vertex_base_instance.idl b/test/wpt/tests/interfaces/WEBGL_draw_instanced_base_vertex_base_instance.idl
new file mode 100644
index 0000000..38f7a42
--- /dev/null
+++ b/test/wpt/tests/interfaces/WEBGL_draw_instanced_base_vertex_base_instance.idl
@@ -0,0 +1,14 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: WebGL WEBGL_draw_instanced_base_vertex_base_instance Extension Draft Specification (https://registry.khronos.org/webgl/extensions/WEBGL_draw_instanced_base_vertex_base_instance/)
+
+[Exposed=(Window,Worker), LegacyNoInterfaceObject]
+interface WEBGL_draw_instanced_base_vertex_base_instance {
+ undefined drawArraysInstancedBaseInstanceWEBGL(
+ GLenum mode, GLint first, GLsizei count,
+ GLsizei instanceCount, GLuint baseInstance);
+ undefined drawElementsInstancedBaseVertexBaseInstanceWEBGL(
+ GLenum mode, GLsizei count, GLenum type, GLintptr offset,
+ GLsizei instanceCount, GLint baseVertex, GLuint baseInstance);
+};
diff --git a/test/wpt/tests/interfaces/WEBGL_lose_context.idl b/test/wpt/tests/interfaces/WEBGL_lose_context.idl
new file mode 100644
index 0000000..ee68fb5
--- /dev/null
+++ b/test/wpt/tests/interfaces/WEBGL_lose_context.idl
@@ -0,0 +1,10 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: WebGL WEBGL_lose_context Khronos Ratified Extension Specification (https://registry.khronos.org/webgl/extensions/WEBGL_lose_context/)
+
+[Exposed=(Window,Worker), LegacyNoInterfaceObject]
+interface WEBGL_lose_context {
+ undefined loseContext();
+ undefined restoreContext();
+};
diff --git a/test/wpt/tests/interfaces/WEBGL_multi_draw.idl b/test/wpt/tests/interfaces/WEBGL_multi_draw.idl
new file mode 100644
index 0000000..ee8c044
--- /dev/null
+++ b/test/wpt/tests/interfaces/WEBGL_multi_draw.idl
@@ -0,0 +1,32 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: WebGL WEBGL_multi_draw Extension Specification (https://registry.khronos.org/webgl/extensions/WEBGL_multi_draw/)
+
+[Exposed=(Window,Worker), LegacyNoInterfaceObject]
+interface WEBGL_multi_draw {
+ undefined multiDrawArraysWEBGL(
+ GLenum mode,
+ ([AllowShared] Int32Array or sequence<GLint>) firstsList, GLuint firstsOffset,
+ ([AllowShared] Int32Array or sequence<GLsizei>) countsList, GLuint countsOffset,
+ GLsizei drawcount);
+ undefined multiDrawElementsWEBGL(
+ GLenum mode,
+ ([AllowShared] Int32Array or sequence<GLsizei>) countsList, GLuint countsOffset,
+ GLenum type,
+ ([AllowShared] Int32Array or sequence<GLsizei>) offsetsList, GLuint offsetsOffset,
+ GLsizei drawcount);
+ undefined multiDrawArraysInstancedWEBGL(
+ GLenum mode,
+ ([AllowShared] Int32Array or sequence<GLint>) firstsList, GLuint firstsOffset,
+ ([AllowShared] Int32Array or sequence<GLsizei>) countsList, GLuint countsOffset,
+ ([AllowShared] Int32Array or sequence<GLsizei>) instanceCountsList, GLuint instanceCountsOffset,
+ GLsizei drawcount);
+ undefined multiDrawElementsInstancedWEBGL(
+ GLenum mode,
+ ([AllowShared] Int32Array or sequence<GLsizei>) countsList, GLuint countsOffset,
+ GLenum type,
+ ([AllowShared] Int32Array or sequence<GLsizei>) offsetsList, GLuint offsetsOffset,
+ ([AllowShared] Int32Array or sequence<GLsizei>) instanceCountsList, GLuint instanceCountsOffset,
+ GLsizei drawcount);
+};
diff --git a/test/wpt/tests/interfaces/WEBGL_multi_draw_instanced_base_vertex_base_instance.idl b/test/wpt/tests/interfaces/WEBGL_multi_draw_instanced_base_vertex_base_instance.idl
new file mode 100644
index 0000000..2258fa9
--- /dev/null
+++ b/test/wpt/tests/interfaces/WEBGL_multi_draw_instanced_base_vertex_base_instance.idl
@@ -0,0 +1,26 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: WebGL WEBGL_multi_draw_instanced_base_vertex_base_instance Extension Draft Specification (https://registry.khronos.org/webgl/extensions/WEBGL_multi_draw_instanced_base_vertex_base_instance/)
+
+[Exposed=(Window,Worker), LegacyNoInterfaceObject]
+interface WEBGL_multi_draw_instanced_base_vertex_base_instance {
+ undefined multiDrawArraysInstancedBaseInstanceWEBGL(
+ GLenum mode,
+ ([AllowShared] Int32Array or sequence<GLint>) firstsList, GLuint firstsOffset,
+ ([AllowShared] Int32Array or sequence<GLsizei>) countsList, GLuint countsOffset,
+ ([AllowShared] Int32Array or sequence<GLsizei>) instanceCountsList, GLuint instanceCountsOffset,
+ ([AllowShared] Uint32Array or sequence<GLuint>) baseInstancesList, GLuint baseInstancesOffset,
+ GLsizei drawcount
+ );
+ undefined multiDrawElementsInstancedBaseVertexBaseInstanceWEBGL(
+ GLenum mode,
+ ([AllowShared] Int32Array or sequence<GLsizei>) countsList, GLuint countsOffset,
+ GLenum type,
+ ([AllowShared] Int32Array or sequence<GLsizei>) offsetsList, GLuint offsetsOffset,
+ ([AllowShared] Int32Array or sequence<GLsizei>) instanceCountsList, GLuint instanceCountsOffset,
+ ([AllowShared] Int32Array or sequence<GLint>) baseVerticesList, GLuint baseVerticesOffset,
+ ([AllowShared] Uint32Array or sequence<GLuint>) baseInstancesList, GLuint baseInstancesOffset,
+ GLsizei drawcount
+ );
+};
diff --git a/test/wpt/tests/interfaces/WEBGL_provoking_vertex.idl b/test/wpt/tests/interfaces/WEBGL_provoking_vertex.idl
new file mode 100644
index 0000000..035e1d2
--- /dev/null
+++ b/test/wpt/tests/interfaces/WEBGL_provoking_vertex.idl
@@ -0,0 +1,13 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: WebGL WEBGL_provoking_vertex Extension Specification (https://registry.khronos.org/webgl/extensions/WEBGL_provoking_vertex/)
+
+[Exposed=(Window,Worker), LegacyNoInterfaceObject]
+interface WEBGL_provoking_vertex {
+ const GLenum FIRST_VERTEX_CONVENTION_WEBGL = 0x8E4D;
+ const GLenum LAST_VERTEX_CONVENTION_WEBGL = 0x8E4E; // default
+ const GLenum PROVOKING_VERTEX_WEBGL = 0x8E4F;
+
+ undefined provokingVertexWEBGL(GLenum provokeMode);
+};
diff --git a/test/wpt/tests/interfaces/WebCryptoAPI.idl b/test/wpt/tests/interfaces/WebCryptoAPI.idl
new file mode 100644
index 0000000..0e68ea8
--- /dev/null
+++ b/test/wpt/tests/interfaces/WebCryptoAPI.idl
@@ -0,0 +1,237 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Web Cryptography API (https://w3c.github.io/webcrypto/)
+
+partial interface mixin WindowOrWorkerGlobalScope {
+ [SameObject] readonly attribute Crypto crypto;
+};
+
+[Exposed=(Window,Worker)]
+interface Crypto {
+ [SecureContext] readonly attribute SubtleCrypto subtle;
+ ArrayBufferView getRandomValues(ArrayBufferView array);
+ [SecureContext] DOMString randomUUID();
+};
+
+typedef (object or DOMString) AlgorithmIdentifier;
+
+typedef AlgorithmIdentifier HashAlgorithmIdentifier;
+
+dictionary Algorithm {
+ required DOMString name;
+};
+
+dictionary KeyAlgorithm {
+ required DOMString name;
+};
+
+enum KeyType { "public", "private", "secret" };
+
+enum KeyUsage { "encrypt", "decrypt", "sign", "verify", "deriveKey", "deriveBits", "wrapKey", "unwrapKey" };
+
+[SecureContext,Exposed=(Window,Worker),Serializable]
+interface CryptoKey {
+ readonly attribute KeyType type;
+ readonly attribute boolean extractable;
+ readonly attribute object algorithm;
+ readonly attribute object usages;
+};
+
+enum KeyFormat { "raw", "spki", "pkcs8", "jwk" };
+
+[SecureContext,Exposed=(Window,Worker)]
+interface SubtleCrypto {
+ Promise<any> encrypt(AlgorithmIdentifier algorithm,
+ CryptoKey key,
+ BufferSource data);
+ Promise<any> decrypt(AlgorithmIdentifier algorithm,
+ CryptoKey key,
+ BufferSource data);
+ Promise<any> sign(AlgorithmIdentifier algorithm,
+ CryptoKey key,
+ BufferSource data);
+ Promise<any> verify(AlgorithmIdentifier algorithm,
+ CryptoKey key,
+ BufferSource signature,
+ BufferSource data);
+ Promise<any> digest(AlgorithmIdentifier algorithm,
+ BufferSource data);
+
+ Promise<any> generateKey(AlgorithmIdentifier algorithm,
+ boolean extractable,
+ sequence<KeyUsage> keyUsages );
+ Promise<any> deriveKey(AlgorithmIdentifier algorithm,
+ CryptoKey baseKey,
+ AlgorithmIdentifier derivedKeyType,
+ boolean extractable,
+ sequence<KeyUsage> keyUsages );
+ Promise<ArrayBuffer> deriveBits(AlgorithmIdentifier algorithm,
+ CryptoKey baseKey,
+ unsigned long length);
+
+ Promise<CryptoKey> importKey(KeyFormat format,
+ (BufferSource or JsonWebKey) keyData,
+ AlgorithmIdentifier algorithm,
+ boolean extractable,
+ sequence<KeyUsage> keyUsages );
+ Promise<any> exportKey(KeyFormat format, CryptoKey key);
+
+ Promise<any> wrapKey(KeyFormat format,
+ CryptoKey key,
+ CryptoKey wrappingKey,
+ AlgorithmIdentifier wrapAlgorithm);
+ Promise<CryptoKey> unwrapKey(KeyFormat format,
+ BufferSource wrappedKey,
+ CryptoKey unwrappingKey,
+ AlgorithmIdentifier unwrapAlgorithm,
+ AlgorithmIdentifier unwrappedKeyAlgorithm,
+ boolean extractable,
+ sequence<KeyUsage> keyUsages );
+};
+
+dictionary RsaOtherPrimesInfo {
+ // The following fields are defined in Section 6.3.2.7 of JSON Web Algorithms
+ DOMString r;
+ DOMString d;
+ DOMString t;
+};
+
+dictionary JsonWebKey {
+ // The following fields are defined in Section 3.1 of JSON Web Key
+ DOMString kty;
+ DOMString use;
+ sequence<DOMString> key_ops;
+ DOMString alg;
+
+ // The following fields are defined in JSON Web Key Parameters Registration
+ boolean ext;
+
+ // The following fields are defined in Section 6 of JSON Web Algorithms
+ DOMString crv;
+ DOMString x;
+ DOMString y;
+ DOMString d;
+ DOMString n;
+ DOMString e;
+ DOMString p;
+ DOMString q;
+ DOMString dp;
+ DOMString dq;
+ DOMString qi;
+ sequence<RsaOtherPrimesInfo> oth;
+ DOMString k;
+};
+
+typedef Uint8Array BigInteger;
+
+dictionary CryptoKeyPair {
+ CryptoKey publicKey;
+ CryptoKey privateKey;
+};
+
+dictionary RsaKeyGenParams : Algorithm {
+ required [EnforceRange] unsigned long modulusLength;
+ required BigInteger publicExponent;
+};
+
+dictionary RsaHashedKeyGenParams : RsaKeyGenParams {
+ required HashAlgorithmIdentifier hash;
+};
+
+dictionary RsaKeyAlgorithm : KeyAlgorithm {
+ required unsigned long modulusLength;
+ required BigInteger publicExponent;
+};
+
+dictionary RsaHashedKeyAlgorithm : RsaKeyAlgorithm {
+ required KeyAlgorithm hash;
+};
+
+dictionary RsaHashedImportParams : Algorithm {
+ required HashAlgorithmIdentifier hash;
+};
+
+dictionary RsaPssParams : Algorithm {
+ required [EnforceRange] unsigned long saltLength;
+};
+
+dictionary RsaOaepParams : Algorithm {
+ BufferSource label;
+};
+
+dictionary EcdsaParams : Algorithm {
+ required HashAlgorithmIdentifier hash;
+};
+
+typedef DOMString NamedCurve;
+
+dictionary EcKeyGenParams : Algorithm {
+ required NamedCurve namedCurve;
+};
+
+dictionary EcKeyAlgorithm : KeyAlgorithm {
+ required NamedCurve namedCurve;
+};
+
+dictionary EcKeyImportParams : Algorithm {
+ required NamedCurve namedCurve;
+};
+
+dictionary EcdhKeyDeriveParams : Algorithm {
+ required CryptoKey public;
+};
+
+dictionary AesCtrParams : Algorithm {
+ required BufferSource counter;
+ required [EnforceRange] octet length;
+};
+
+dictionary AesKeyAlgorithm : KeyAlgorithm {
+ required unsigned short length;
+};
+
+dictionary AesKeyGenParams : Algorithm {
+ required [EnforceRange] unsigned short length;
+};
+
+dictionary AesDerivedKeyParams : Algorithm {
+ required [EnforceRange] unsigned short length;
+};
+
+dictionary AesCbcParams : Algorithm {
+ required BufferSource iv;
+};
+
+dictionary AesGcmParams : Algorithm {
+ required BufferSource iv;
+ BufferSource additionalData;
+ [EnforceRange] octet tagLength;
+};
+
+dictionary HmacImportParams : Algorithm {
+ required HashAlgorithmIdentifier hash;
+ [EnforceRange] unsigned long length;
+};
+
+dictionary HmacKeyAlgorithm : KeyAlgorithm {
+ required KeyAlgorithm hash;
+ required unsigned long length;
+};
+
+dictionary HmacKeyGenParams : Algorithm {
+ required HashAlgorithmIdentifier hash;
+ [EnforceRange] unsigned long length;
+};
+
+dictionary HkdfParams : Algorithm {
+ required HashAlgorithmIdentifier hash;
+ required BufferSource salt;
+ required BufferSource info;
+};
+
+dictionary Pbkdf2Params : Algorithm {
+ required BufferSource salt;
+ required [EnforceRange] unsigned long iterations;
+ required HashAlgorithmIdentifier hash;
+};
diff --git a/test/wpt/tests/interfaces/accelerometer.idl b/test/wpt/tests/interfaces/accelerometer.idl
new file mode 100644
index 0000000..fc8fc07
--- /dev/null
+++ b/test/wpt/tests/interfaces/accelerometer.idl
@@ -0,0 +1,40 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Accelerometer (https://w3c.github.io/accelerometer/)
+
+[SecureContext, Exposed=Window]
+interface Accelerometer : Sensor {
+ constructor(optional AccelerometerSensorOptions options = {});
+ readonly attribute double? x;
+ readonly attribute double? y;
+ readonly attribute double? z;
+};
+
+enum AccelerometerLocalCoordinateSystem { "device", "screen" };
+
+dictionary AccelerometerSensorOptions : SensorOptions {
+ AccelerometerLocalCoordinateSystem referenceFrame = "device";
+};
+
+[SecureContext, Exposed=Window]
+interface LinearAccelerationSensor : Accelerometer {
+ constructor(optional AccelerometerSensorOptions options = {});
+};
+
+[SecureContext, Exposed=Window]
+interface GravitySensor : Accelerometer {
+ constructor(optional AccelerometerSensorOptions options = {});
+};
+
+dictionary AccelerometerReadingValues {
+ required double? x;
+ required double? y;
+ required double? z;
+};
+
+dictionary LinearAccelerationReadingValues : AccelerometerReadingValues {
+};
+
+dictionary GravityReadingValues : AccelerometerReadingValues {
+};
diff --git a/test/wpt/tests/interfaces/ambient-light.idl b/test/wpt/tests/interfaces/ambient-light.idl
new file mode 100644
index 0000000..6d9c8e0
--- /dev/null
+++ b/test/wpt/tests/interfaces/ambient-light.idl
@@ -0,0 +1,14 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Ambient Light Sensor (https://w3c.github.io/ambient-light/)
+
+[SecureContext, Exposed=Window]
+interface AmbientLightSensor : Sensor {
+ constructor(optional SensorOptions sensorOptions = {});
+ readonly attribute double? illuminance;
+};
+
+dictionary AmbientLightReadingValues {
+ required double? illuminance;
+};
diff --git a/test/wpt/tests/interfaces/anchors.idl b/test/wpt/tests/interfaces/anchors.idl
new file mode 100644
index 0000000..d8c5aa6
--- /dev/null
+++ b/test/wpt/tests/interfaces/anchors.idl
@@ -0,0 +1,37 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: WebXR Anchors Module (https://immersive-web.github.io/anchors/)
+
+[SecureContext, Exposed=Window]
+interface XRAnchor {
+ readonly attribute XRSpace anchorSpace;
+
+ Promise<DOMString> requestPersistentHandle();
+
+ undefined delete();
+};
+
+partial interface XRFrame {
+ Promise<XRAnchor> createAnchor(XRRigidTransform pose, XRSpace space);
+};
+
+partial interface XRSession {
+ readonly attribute FrozenArray<DOMString> persistentAnchors;
+
+ Promise<XRAnchor> restorePersistentAnchor(DOMString uuid);
+ Promise<undefined> deletePersistentAnchor(DOMString uuid);
+};
+
+partial interface XRHitTestResult {
+ Promise<XRAnchor> createAnchor();
+};
+
+[Exposed=Window]
+interface XRAnchorSet {
+ readonly setlike<XRAnchor>;
+};
+
+partial interface XRFrame {
+ [SameObject] readonly attribute XRAnchorSet trackedAnchors;
+};
diff --git a/test/wpt/tests/interfaces/attribution-reporting-api.idl b/test/wpt/tests/interfaces/attribution-reporting-api.idl
new file mode 100644
index 0000000..ed4497b
--- /dev/null
+++ b/test/wpt/tests/interfaces/attribution-reporting-api.idl
@@ -0,0 +1,26 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Attribution Reporting (https://wicg.github.io/attribution-reporting-api/)
+
+interface mixin HTMLAttributionSrcElementUtils {
+ [CEReactions, SecureContext] attribute USVString attributionSrc;
+};
+
+HTMLAnchorElement includes HTMLAttributionSrcElementUtils;
+HTMLImageElement includes HTMLAttributionSrcElementUtils;
+HTMLScriptElement includes HTMLAttributionSrcElementUtils;
+
+dictionary AttributionReportingRequestOptions {
+ required boolean eventSourceEligible;
+ required boolean triggerEligible;
+};
+
+partial dictionary RequestInit {
+ AttributionReportingRequestOptions attributionReporting;
+};
+
+partial interface XMLHttpRequest {
+ [SecureContext]
+ undefined setAttributionReporting(AttributionReportingRequestOptions options);
+};
diff --git a/test/wpt/tests/interfaces/audio-output.idl b/test/wpt/tests/interfaces/audio-output.idl
new file mode 100644
index 0000000..80ceb22
--- /dev/null
+++ b/test/wpt/tests/interfaces/audio-output.idl
@@ -0,0 +1,17 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Audio Output Devices API (https://w3c.github.io/mediacapture-output/)
+
+partial interface HTMLMediaElement {
+ [SecureContext] readonly attribute DOMString sinkId;
+ [SecureContext] Promise<undefined> setSinkId (DOMString sinkId);
+};
+
+partial interface MediaDevices {
+ Promise<MediaDeviceInfo> selectAudioOutput(optional AudioOutputOptions options = {});
+};
+
+dictionary AudioOutputOptions {
+ DOMString deviceId = "";
+};
diff --git a/test/wpt/tests/interfaces/autoplay-detection.idl b/test/wpt/tests/interfaces/autoplay-detection.idl
new file mode 100644
index 0000000..cd0884f
--- /dev/null
+++ b/test/wpt/tests/interfaces/autoplay-detection.idl
@@ -0,0 +1,19 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Autoplay Policy Detection (https://w3c.github.io/autoplay/)
+
+enum AutoplayPolicy {
+ "allowed",
+ "allowed-muted",
+ "disallowed"
+};
+
+enum AutoplayPolicyMediaType { "mediaelement", "audiocontext" };
+
+[Exposed=Window]
+partial interface Navigator {
+ AutoplayPolicy getAutoplayPolicy(AutoplayPolicyMediaType type);
+ AutoplayPolicy getAutoplayPolicy(HTMLMediaElement element);
+ AutoplayPolicy getAutoplayPolicy(AudioContext context);
+};
diff --git a/test/wpt/tests/interfaces/background-fetch.idl b/test/wpt/tests/interfaces/background-fetch.idl
new file mode 100644
index 0000000..993bd8b
--- /dev/null
+++ b/test/wpt/tests/interfaces/background-fetch.idl
@@ -0,0 +1,89 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Background Fetch (https://wicg.github.io/background-fetch/)
+
+partial interface ServiceWorkerGlobalScope {
+ attribute EventHandler onbackgroundfetchsuccess;
+ attribute EventHandler onbackgroundfetchfail;
+ attribute EventHandler onbackgroundfetchabort;
+ attribute EventHandler onbackgroundfetchclick;
+};
+
+partial interface ServiceWorkerRegistration {
+ readonly attribute BackgroundFetchManager backgroundFetch;
+};
+
+[Exposed=(Window,Worker)]
+interface BackgroundFetchManager {
+ Promise<BackgroundFetchRegistration> fetch(DOMString id, (RequestInfo or sequence<RequestInfo>) requests, optional BackgroundFetchOptions options = {});
+ Promise<BackgroundFetchRegistration?> get(DOMString id);
+ Promise<sequence<DOMString>> getIds();
+};
+
+dictionary BackgroundFetchUIOptions {
+ sequence<ImageResource> icons;
+ DOMString title;
+};
+
+dictionary BackgroundFetchOptions : BackgroundFetchUIOptions {
+ unsigned long long downloadTotal = 0;
+};
+
+[Exposed=(Window,Worker)]
+interface BackgroundFetchRegistration : EventTarget {
+ readonly attribute DOMString id;
+ readonly attribute unsigned long long uploadTotal;
+ readonly attribute unsigned long long uploaded;
+ readonly attribute unsigned long long downloadTotal;
+ readonly attribute unsigned long long downloaded;
+ readonly attribute BackgroundFetchResult result;
+ readonly attribute BackgroundFetchFailureReason failureReason;
+ readonly attribute boolean recordsAvailable;
+
+ attribute EventHandler onprogress;
+
+ Promise<boolean> abort();
+ Promise<BackgroundFetchRecord> match(RequestInfo request, optional CacheQueryOptions options = {});
+ Promise<sequence<BackgroundFetchRecord>> matchAll(optional RequestInfo request, optional CacheQueryOptions options = {});
+};
+
+enum BackgroundFetchResult { "", "success", "failure" };
+
+enum BackgroundFetchFailureReason {
+ // The background fetch has not completed yet, or was successful.
+ "",
+ // The operation was aborted by the user, or abort() was called.
+ "aborted",
+ // A response had a not-ok-status.
+ "bad-status",
+ // A fetch failed for other reasons, e.g. CORS, MIX, an invalid partial response,
+ // or a general network failure for a fetch that cannot be retried.
+ "fetch-error",
+ // Storage quota was reached during the operation.
+ "quota-exceeded",
+ // The provided downloadTotal was exceeded.
+ "download-total-exceeded"
+};
+
+[Exposed=(Window,Worker)]
+interface BackgroundFetchRecord {
+ readonly attribute Request request;
+ readonly attribute Promise<Response> responseReady;
+};
+
+[Exposed=ServiceWorker]
+interface BackgroundFetchEvent : ExtendableEvent {
+ constructor(DOMString type, BackgroundFetchEventInit init);
+ readonly attribute BackgroundFetchRegistration registration;
+};
+
+dictionary BackgroundFetchEventInit : ExtendableEventInit {
+ required BackgroundFetchRegistration registration;
+};
+
+[Exposed=ServiceWorker]
+interface BackgroundFetchUpdateUIEvent : BackgroundFetchEvent {
+ constructor(DOMString type, BackgroundFetchEventInit init);
+ Promise<undefined> updateUI(optional BackgroundFetchUIOptions options = {});
+};
diff --git a/test/wpt/tests/interfaces/background-sync.idl b/test/wpt/tests/interfaces/background-sync.idl
new file mode 100644
index 0000000..79a13a6
--- /dev/null
+++ b/test/wpt/tests/interfaces/background-sync.idl
@@ -0,0 +1,30 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Web Background Synchronization (https://wicg.github.io/background-sync/spec/)
+
+partial interface ServiceWorkerRegistration {
+ readonly attribute SyncManager sync;
+};
+
+[Exposed=(Window,Worker)]
+interface SyncManager {
+ Promise<undefined> register(DOMString tag);
+ Promise<sequence<DOMString>> getTags();
+};
+
+partial interface ServiceWorkerGlobalScope {
+ attribute EventHandler onsync;
+};
+
+[Exposed=ServiceWorker]
+interface SyncEvent : ExtendableEvent {
+ constructor(DOMString type, SyncEventInit init);
+ readonly attribute DOMString tag;
+ readonly attribute boolean lastChance;
+};
+
+dictionary SyncEventInit : ExtendableEventInit {
+ required DOMString tag;
+ boolean lastChance = false;
+};
diff --git a/test/wpt/tests/interfaces/badging.idl b/test/wpt/tests/interfaces/badging.idl
new file mode 100644
index 0000000..8b401e0
--- /dev/null
+++ b/test/wpt/tests/interfaces/badging.idl
@@ -0,0 +1,15 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Badging API (https://w3c.github.io/badging/)
+
+[SecureContext]
+interface mixin NavigatorBadge {
+ Promise<undefined> setAppBadge(
+ optional [EnforceRange] unsigned long long contents
+ );
+ Promise<undefined> clearAppBadge();
+};
+
+Navigator includes NavigatorBadge;
+WorkerNavigator includes NavigatorBadge;
diff --git a/test/wpt/tests/interfaces/battery-status.idl b/test/wpt/tests/interfaces/battery-status.idl
new file mode 100644
index 0000000..2d042db
--- /dev/null
+++ b/test/wpt/tests/interfaces/battery-status.idl
@@ -0,0 +1,21 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Battery Status API (https://w3c.github.io/battery/)
+
+[SecureContext]
+partial interface Navigator {
+ Promise<BatteryManager> getBattery();
+};
+
+[SecureContext, Exposed=Window]
+interface BatteryManager : EventTarget {
+ readonly attribute boolean charging;
+ readonly attribute unrestricted double chargingTime;
+ readonly attribute unrestricted double dischargingTime;
+ readonly attribute double level;
+ attribute EventHandler onchargingchange;
+ attribute EventHandler onchargingtimechange;
+ attribute EventHandler ondischargingtimechange;
+ attribute EventHandler onlevelchange;
+};
diff --git a/test/wpt/tests/interfaces/beacon.idl b/test/wpt/tests/interfaces/beacon.idl
new file mode 100644
index 0000000..103a999
--- /dev/null
+++ b/test/wpt/tests/interfaces/beacon.idl
@@ -0,0 +1,8 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Beacon (https://w3c.github.io/beacon/)
+
+partial interface Navigator {
+ boolean sendBeacon(USVString url, optional BodyInit? data = null);
+};
diff --git a/test/wpt/tests/interfaces/capture-handle-identity.idl b/test/wpt/tests/interfaces/capture-handle-identity.idl
new file mode 100644
index 0000000..37b2c61
--- /dev/null
+++ b/test/wpt/tests/interfaces/capture-handle-identity.idl
@@ -0,0 +1,27 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Capture Handle - Bootstrapping Collaboration when Screensharing (https://w3c.github.io/mediacapture-handle/identity/)
+
+dictionary CaptureHandleConfig {
+ boolean exposeOrigin = false;
+ DOMString handle = "";
+ sequence<DOMString> permittedOrigins = [];
+};
+
+partial interface MediaDevices {
+ undefined setCaptureHandleConfig(optional CaptureHandleConfig config = {});
+};
+
+dictionary CaptureHandle {
+ DOMString origin;
+ DOMString handle;
+};
+
+partial interface MediaStreamTrack {
+ CaptureHandle? getCaptureHandle();
+};
+
+partial interface MediaStreamTrack {
+ attribute EventHandler oncapturehandlechange;
+};
diff --git a/test/wpt/tests/interfaces/captured-mouse-events.tentative.idl b/test/wpt/tests/interfaces/captured-mouse-events.tentative.idl
new file mode 100644
index 0000000..7b081cd
--- /dev/null
+++ b/test/wpt/tests/interfaces/captured-mouse-events.tentative.idl
@@ -0,0 +1,25 @@
+// https://screen-share.github.io/mouse-events/
+
+enum CaptureStartFocusBehavior {
+ "focus-captured-surface",
+ "no-focus-change"
+};
+
+[Exposed=Window, SecureContext]
+interface CaptureController : EventTarget {
+ constructor();
+ undefined setFocusBehavior(CaptureStartFocusBehavior focusBehavior);
+ attribute EventHandler oncapturedmousechange;
+};
+
+[Exposed=Window]
+interface CapturedMouseEvent : Event {
+ constructor(DOMString type, optional CapturedMouseEventInit eventInitDict = {});
+ readonly attribute long surfaceX;
+ readonly attribute long surfaceY;
+};
+
+dictionary CapturedMouseEventInit : EventInit {
+ long surfaceX = -1;
+ long surfaceY = -1;
+};
diff --git a/test/wpt/tests/interfaces/clipboard-apis.idl b/test/wpt/tests/interfaces/clipboard-apis.idl
new file mode 100644
index 0000000..3f2c9ba
--- /dev/null
+++ b/test/wpt/tests/interfaces/clipboard-apis.idl
@@ -0,0 +1,51 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Clipboard API and events (https://w3c.github.io/clipboard-apis/)
+
+dictionary ClipboardEventInit : EventInit {
+ DataTransfer? clipboardData = null;
+};
+
+[Exposed=Window]
+interface ClipboardEvent : Event {
+ constructor(DOMString type, optional ClipboardEventInit eventInitDict = {});
+ readonly attribute DataTransfer? clipboardData;
+};
+
+partial interface Navigator {
+ [SecureContext, SameObject] readonly attribute Clipboard clipboard;
+};
+
+typedef Promise<(DOMString or Blob)> ClipboardItemData;
+
+[SecureContext, Exposed=Window]
+interface ClipboardItem {
+ constructor(record<DOMString, ClipboardItemData> items,
+ optional ClipboardItemOptions options = {});
+
+ readonly attribute PresentationStyle presentationStyle;
+ readonly attribute FrozenArray<DOMString> types;
+
+ Promise<Blob> getType(DOMString type);
+};
+
+enum PresentationStyle { "unspecified", "inline", "attachment" };
+
+dictionary ClipboardItemOptions {
+ PresentationStyle presentationStyle = "unspecified";
+};
+
+typedef sequence<ClipboardItem> ClipboardItems;
+
+[SecureContext, Exposed=Window]
+interface Clipboard : EventTarget {
+ Promise<ClipboardItems> read();
+ Promise<DOMString> readText();
+ Promise<undefined> write(ClipboardItems data);
+ Promise<undefined> writeText(DOMString data);
+};
+
+dictionary ClipboardPermissionDescriptor : PermissionDescriptor {
+ boolean allowWithoutGesture = false;
+};
diff --git a/test/wpt/tests/interfaces/close-watcher.idl b/test/wpt/tests/interfaces/close-watcher.idl
new file mode 100644
index 0000000..de7940c
--- /dev/null
+++ b/test/wpt/tests/interfaces/close-watcher.idl
@@ -0,0 +1,19 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Close Watcher API (https://wicg.github.io/close-watcher/)
+
+[Exposed=Window]
+interface CloseWatcher : EventTarget {
+ constructor(optional CloseWatcherOptions options = {});
+
+ undefined destroy();
+ undefined close();
+
+ attribute EventHandler oncancel;
+ attribute EventHandler onclose;
+};
+
+dictionary CloseWatcherOptions {
+ AbortSignal signal;
+};
diff --git a/test/wpt/tests/interfaces/compat.idl b/test/wpt/tests/interfaces/compat.idl
new file mode 100644
index 0000000..8106c2d
--- /dev/null
+++ b/test/wpt/tests/interfaces/compat.idl
@@ -0,0 +1,13 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Compatibility Standard (https://compat.spec.whatwg.org/)
+
+partial interface Window {
+ readonly attribute short orientation;
+ attribute EventHandler onorientationchange;
+};
+
+partial interface HTMLBodyElement {
+ attribute EventHandler onorientationchange;
+};
diff --git a/test/wpt/tests/interfaces/compression.idl b/test/wpt/tests/interfaces/compression.idl
new file mode 100644
index 0000000..7525d7c
--- /dev/null
+++ b/test/wpt/tests/interfaces/compression.idl
@@ -0,0 +1,22 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Compression Streams (https://wicg.github.io/compression/)
+
+enum CompressionFormat {
+ "deflate",
+ "deflate-raw",
+ "gzip",
+};
+
+[Exposed=*]
+interface CompressionStream {
+ constructor(CompressionFormat format);
+};
+CompressionStream includes GenericTransformStream;
+
+[Exposed=*]
+interface DecompressionStream {
+ constructor(CompressionFormat format);
+};
+DecompressionStream includes GenericTransformStream;
diff --git a/test/wpt/tests/interfaces/compute-pressure.idl b/test/wpt/tests/interfaces/compute-pressure.idl
new file mode 100644
index 0000000..3e35dc4
--- /dev/null
+++ b/test/wpt/tests/interfaces/compute-pressure.idl
@@ -0,0 +1,37 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Compute Pressure Level 1 (https://w3c.github.io/compute-pressure/)
+
+enum PressureSource { "thermals", "cpu" };
+
+enum PressureState { "nominal", "fair", "serious", "critical" };
+
+callback PressureUpdateCallback = undefined (
+ sequence<PressureRecord> changes,
+ PressureObserver observer
+);
+
+[Exposed=(DedicatedWorker,SharedWorker,Window), SecureContext]
+interface PressureObserver {
+ constructor(PressureUpdateCallback callback, optional PressureObserverOptions options = {});
+
+ Promise<undefined> observe(PressureSource source);
+ undefined unobserve(PressureSource source);
+ undefined disconnect();
+ sequence<PressureRecord> takeRecords();
+
+ [SameObject] static readonly attribute FrozenArray<PressureSource> supportedSources;
+};
+
+[Exposed=(DedicatedWorker,SharedWorker,Window), SecureContext]
+interface PressureRecord {
+ readonly attribute PressureSource source;
+ readonly attribute PressureState state;
+ readonly attribute DOMHighResTimeStamp time;
+ [Default] object toJSON();
+};
+
+dictionary PressureObserverOptions {
+ double sampleRate = 1.0;
+};
diff --git a/test/wpt/tests/interfaces/console.idl b/test/wpt/tests/interfaces/console.idl
new file mode 100644
index 0000000..fdf1d0d
--- /dev/null
+++ b/test/wpt/tests/interfaces/console.idl
@@ -0,0 +1,34 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Console Standard (https://console.spec.whatwg.org/)
+
+[Exposed=*]
+namespace console { // but see namespace object requirements below
+ // Logging
+ undefined assert(optional boolean condition = false, any... data);
+ undefined clear();
+ undefined debug(any... data);
+ undefined error(any... data);
+ undefined info(any... data);
+ undefined log(any... data);
+ undefined table(optional any tabularData, optional sequence<DOMString> properties);
+ undefined trace(any... data);
+ undefined warn(any... data);
+ undefined dir(optional any item, optional object? options);
+ undefined dirxml(any... data);
+
+ // Counting
+ undefined count(optional DOMString label = "default");
+ undefined countReset(optional DOMString label = "default");
+
+ // Grouping
+ undefined group(any... data);
+ undefined groupCollapsed(any... data);
+ undefined groupEnd();
+
+ // Timing
+ undefined time(optional DOMString label = "default");
+ undefined timeLog(optional DOMString label = "default", any... data);
+ undefined timeEnd(optional DOMString label = "default");
+};
diff --git a/test/wpt/tests/interfaces/contact-picker.idl b/test/wpt/tests/interfaces/contact-picker.idl
new file mode 100644
index 0000000..0119d0e
--- /dev/null
+++ b/test/wpt/tests/interfaces/contact-picker.idl
@@ -0,0 +1,44 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Contact Picker API (https://w3c.github.io/contact-picker/)
+
+[Exposed=Window]
+partial interface Navigator {
+ [SecureContext, SameObject] readonly attribute ContactsManager contacts;
+};
+
+enum ContactProperty { "address", "email", "icon", "name", "tel" };
+
+[Exposed=Window]
+interface ContactAddress {
+ [Default] object toJSON();
+ readonly attribute DOMString city;
+ readonly attribute DOMString country;
+ readonly attribute DOMString dependentLocality;
+ readonly attribute DOMString organization;
+ readonly attribute DOMString phone;
+ readonly attribute DOMString postalCode;
+ readonly attribute DOMString recipient;
+ readonly attribute DOMString region;
+ readonly attribute DOMString sortingCode;
+ readonly attribute FrozenArray<DOMString> addressLine;
+};
+
+dictionary ContactInfo {
+ sequence<ContactAddress> address;
+ sequence<DOMString> email;
+ sequence<Blob> icon;
+ sequence<DOMString> name;
+ sequence<DOMString> tel;
+};
+
+dictionary ContactsSelectOptions {
+ boolean multiple = false;
+};
+
+[Exposed=Window,SecureContext]
+interface ContactsManager {
+ Promise<sequence<ContactProperty>> getProperties();
+ Promise<sequence<ContactInfo>> select(sequence<ContactProperty> properties, optional ContactsSelectOptions options = {});
+};
diff --git a/test/wpt/tests/interfaces/content-index.idl b/test/wpt/tests/interfaces/content-index.idl
new file mode 100644
index 0000000..177c5b9
--- /dev/null
+++ b/test/wpt/tests/interfaces/content-index.idl
@@ -0,0 +1,46 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Content Index (https://wicg.github.io/content-index/spec/)
+
+partial interface ServiceWorkerGlobalScope {
+ attribute EventHandler oncontentdelete;
+};
+
+partial interface ServiceWorkerRegistration {
+ [SameObject] readonly attribute ContentIndex index;
+};
+
+enum ContentCategory {
+ "",
+ "homepage",
+ "article",
+ "video",
+ "audio",
+};
+
+dictionary ContentDescription {
+ required DOMString id;
+ required DOMString title;
+ required DOMString description;
+ ContentCategory category = "";
+ sequence<ImageResource> icons = [];
+ required USVString url;
+};
+
+[Exposed=(Window,Worker)]
+interface ContentIndex {
+ Promise<undefined> add(ContentDescription description);
+ Promise<undefined> delete(DOMString id);
+ Promise<sequence<ContentDescription>> getAll();
+};
+
+dictionary ContentIndexEventInit : ExtendableEventInit {
+ required DOMString id;
+};
+
+[Exposed=ServiceWorker]
+interface ContentIndexEvent : ExtendableEvent {
+ constructor(DOMString type, ContentIndexEventInit init);
+ readonly attribute DOMString id;
+};
diff --git a/test/wpt/tests/interfaces/cookie-store.idl b/test/wpt/tests/interfaces/cookie-store.idl
new file mode 100644
index 0000000..f44b4c6
--- /dev/null
+++ b/test/wpt/tests/interfaces/cookie-store.idl
@@ -0,0 +1,110 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Cookie Store API (https://wicg.github.io/cookie-store/)
+
+[Exposed=(ServiceWorker,Window),
+ SecureContext]
+interface CookieStore : EventTarget {
+ Promise<CookieListItem?> get(USVString name);
+ Promise<CookieListItem?> get(optional CookieStoreGetOptions options = {});
+
+ Promise<CookieList> getAll(USVString name);
+ Promise<CookieList> getAll(optional CookieStoreGetOptions options = {});
+
+ Promise<undefined> set(USVString name, USVString value);
+ Promise<undefined> set(CookieInit options);
+
+ Promise<undefined> delete(USVString name);
+ Promise<undefined> delete(CookieStoreDeleteOptions options);
+
+ [Exposed=Window]
+ attribute EventHandler onchange;
+};
+
+dictionary CookieStoreGetOptions {
+ USVString name;
+ USVString url;
+};
+
+enum CookieSameSite {
+ "strict",
+ "lax",
+ "none"
+};
+
+dictionary CookieInit {
+ required USVString name;
+ required USVString value;
+ DOMHighResTimeStamp? expires = null;
+ USVString? domain = null;
+ USVString path = "/";
+ CookieSameSite sameSite = "strict";
+};
+
+dictionary CookieStoreDeleteOptions {
+ required USVString name;
+ USVString? domain = null;
+ USVString path = "/";
+};
+
+dictionary CookieListItem {
+ USVString name;
+ USVString value;
+ USVString? domain;
+ USVString path;
+ DOMHighResTimeStamp? expires;
+ boolean secure;
+ CookieSameSite sameSite;
+};
+
+typedef sequence<CookieListItem> CookieList;
+
+[Exposed=(ServiceWorker,Window),
+ SecureContext]
+interface CookieStoreManager {
+ Promise<undefined> subscribe(sequence<CookieStoreGetOptions> subscriptions);
+ Promise<sequence<CookieStoreGetOptions>> getSubscriptions();
+ Promise<undefined> unsubscribe(sequence<CookieStoreGetOptions> subscriptions);
+};
+
+[Exposed=(ServiceWorker,Window)]
+partial interface ServiceWorkerRegistration {
+ [SameObject] readonly attribute CookieStoreManager cookies;
+};
+
+[Exposed=Window,
+ SecureContext]
+interface CookieChangeEvent : Event {
+ constructor(DOMString type, optional CookieChangeEventInit eventInitDict = {});
+ [SameObject] readonly attribute FrozenArray<CookieListItem> changed;
+ [SameObject] readonly attribute FrozenArray<CookieListItem> deleted;
+};
+
+dictionary CookieChangeEventInit : EventInit {
+ CookieList changed;
+ CookieList deleted;
+};
+
+[Exposed=ServiceWorker]
+interface ExtendableCookieChangeEvent : ExtendableEvent {
+ constructor(DOMString type, optional ExtendableCookieChangeEventInit eventInitDict = {});
+ [SameObject] readonly attribute FrozenArray<CookieListItem> changed;
+ [SameObject] readonly attribute FrozenArray<CookieListItem> deleted;
+};
+
+dictionary ExtendableCookieChangeEventInit : ExtendableEventInit {
+ CookieList changed;
+ CookieList deleted;
+};
+
+[SecureContext]
+partial interface Window {
+ [SameObject] readonly attribute CookieStore cookieStore;
+};
+
+partial interface ServiceWorkerGlobalScope {
+ [SameObject] readonly attribute CookieStore cookieStore;
+
+ attribute EventHandler oncookiechange;
+};
diff --git a/test/wpt/tests/interfaces/credential-management.idl b/test/wpt/tests/interfaces/credential-management.idl
new file mode 100644
index 0000000..e9fab13
--- /dev/null
+++ b/test/wpt/tests/interfaces/credential-management.idl
@@ -0,0 +1,105 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Credential Management Level 1 (https://w3c.github.io/webappsec-credential-management/)
+
+[Exposed=Window, SecureContext]
+interface Credential {
+ readonly attribute USVString id;
+ readonly attribute DOMString type;
+ static Promise<boolean> isConditionalMediationAvailable();
+};
+
+[SecureContext]
+interface mixin CredentialUserData {
+ readonly attribute USVString name;
+ readonly attribute USVString iconURL;
+};
+
+partial interface Navigator {
+ [SecureContext, SameObject] readonly attribute CredentialsContainer credentials;
+};
+
+[Exposed=Window, SecureContext]
+interface CredentialsContainer {
+ Promise<Credential?> get(optional CredentialRequestOptions options = {});
+ Promise<Credential> store(Credential credential);
+ Promise<Credential?> create(optional CredentialCreationOptions options = {});
+ Promise<undefined> preventSilentAccess();
+};
+
+dictionary CredentialData {
+ required USVString id;
+};
+
+dictionary CredentialRequestOptions {
+ CredentialMediationRequirement mediation = "optional";
+ AbortSignal signal;
+};
+
+enum CredentialMediationRequirement {
+ "silent",
+ "optional",
+ "conditional",
+ "required"
+};
+
+dictionary CredentialCreationOptions {
+ AbortSignal signal;
+};
+
+[Exposed=Window,
+ SecureContext]
+interface PasswordCredential : Credential {
+ constructor(HTMLFormElement form);
+ constructor(PasswordCredentialData data);
+ readonly attribute USVString password;
+};
+PasswordCredential includes CredentialUserData;
+
+partial dictionary CredentialRequestOptions {
+ boolean password = false;
+};
+
+dictionary PasswordCredentialData : CredentialData {
+ USVString name;
+ USVString iconURL;
+ required USVString origin;
+ required USVString password;
+};
+
+typedef (PasswordCredentialData or HTMLFormElement) PasswordCredentialInit;
+
+partial dictionary CredentialCreationOptions {
+ PasswordCredentialInit password;
+};
+
+[Exposed=Window,
+ SecureContext]
+interface FederatedCredential : Credential {
+ constructor(FederatedCredentialInit data);
+ readonly attribute USVString provider;
+ readonly attribute DOMString? protocol;
+};
+FederatedCredential includes CredentialUserData;
+
+dictionary FederatedCredentialRequestOptions {
+ sequence<USVString> providers;
+ sequence<DOMString> protocols;
+};
+
+partial dictionary CredentialRequestOptions {
+ FederatedCredentialRequestOptions federated;
+};
+
+dictionary FederatedCredentialInit : CredentialData {
+ USVString name;
+ USVString iconURL;
+ required USVString origin;
+ required USVString provider;
+ DOMString protocol;
+};
+
+partial dictionary CredentialCreationOptions {
+ FederatedCredentialInit federated;
+};
diff --git a/test/wpt/tests/interfaces/csp-embedded-enforcement.idl b/test/wpt/tests/interfaces/csp-embedded-enforcement.idl
new file mode 100644
index 0000000..a980630
--- /dev/null
+++ b/test/wpt/tests/interfaces/csp-embedded-enforcement.idl
@@ -0,0 +1,8 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Content Security Policy: Embedded Enforcement (https://w3c.github.io/webappsec-cspee/)
+
+partial interface HTMLIFrameElement {
+ [CEReactions] attribute DOMString csp;
+};
diff --git a/test/wpt/tests/interfaces/csp-next.idl b/test/wpt/tests/interfaces/csp-next.idl
new file mode 100644
index 0000000..d94b36c
--- /dev/null
+++ b/test/wpt/tests/interfaces/csp-next.idl
@@ -0,0 +1,21 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Scripting Policy (https://wicg.github.io/csp-next/scripting-policy.html)
+
+enum ScriptingPolicyViolationType {
+ "externalScript",
+ "inlineScript",
+ "inlineEventHandler",
+ "eval"
+};
+
+[Exposed=(Window,Worker), SecureContext]
+interface ScriptingPolicyReportBody : ReportBody {
+ [Default] object toJSON();
+ readonly attribute DOMString violationType;
+ readonly attribute USVString? violationURL;
+ readonly attribute USVString? violationSample;
+ readonly attribute unsigned long lineno;
+ readonly attribute unsigned long colno;
+};
diff --git a/test/wpt/tests/interfaces/css-anchor-position.idl b/test/wpt/tests/interfaces/css-anchor-position.idl
new file mode 100644
index 0000000..c5da3f4
--- /dev/null
+++ b/test/wpt/tests/interfaces/css-anchor-position.idl
@@ -0,0 +1,11 @@
+// Source: CSS Anchor Positioning (https://drafts.csswg.org/css-anchor-position-1/)
+
+[Exposed=Window]
+interface CSSPositionFallbackRule : CSSGroupingRule {
+ readonly attribute CSSOMString name;
+};
+
+[Exposed=Window]
+interface CSSTryRule : CSSRule {
+ [SameObject, PutForwards=cssText] readonly attribute CSSStyleDeclaration style;
+};
diff --git a/test/wpt/tests/interfaces/css-animation-worklet.idl b/test/wpt/tests/interfaces/css-animation-worklet.idl
new file mode 100644
index 0000000..82d34a3
--- /dev/null
+++ b/test/wpt/tests/interfaces/css-animation-worklet.idl
@@ -0,0 +1,37 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: CSS Animation Worklet API (https://drafts.css-houdini.org/css-animationworklet-1/)
+
+[Exposed=Window]
+partial namespace CSS {
+ [SameObject] readonly attribute Worklet animationWorklet;
+};
+
+[ Global=(Worklet,AnimationWorklet), Exposed=AnimationWorklet ]
+interface AnimationWorkletGlobalScope : WorkletGlobalScope {
+ undefined registerAnimator(DOMString name, AnimatorInstanceConstructor animatorCtor);
+};
+
+callback AnimatorInstanceConstructor = any (any options, optional any state);
+
+[ Exposed=AnimationWorklet ]
+interface WorkletAnimationEffect {
+ EffectTiming getTiming();
+ ComputedEffectTiming getComputedTiming();
+ attribute double? localTime;
+};
+
+[Exposed=Window]
+interface WorkletAnimation : Animation {
+ constructor(DOMString animatorName,
+ optional (AnimationEffect or sequence<AnimationEffect>)? effects = null,
+ optional AnimationTimeline? timeline,
+ optional any options);
+ readonly attribute DOMString animatorName;
+};
+
+[Exposed=AnimationWorklet]
+interface WorkletGroupEffect {
+ sequence<WorkletAnimationEffect> getChildren();
+};
diff --git a/test/wpt/tests/interfaces/css-animations-2.idl b/test/wpt/tests/interfaces/css-animations-2.idl
new file mode 100644
index 0000000..84f138e
--- /dev/null
+++ b/test/wpt/tests/interfaces/css-animations-2.idl
@@ -0,0 +1,9 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: CSS Animations Level 2 (https://drafts.csswg.org/css-animations-2/)
+
+[Exposed=Window]
+interface CSSAnimation : Animation {
+ readonly attribute CSSOMString animationName;
+};
diff --git a/test/wpt/tests/interfaces/css-animations.idl b/test/wpt/tests/interfaces/css-animations.idl
new file mode 100644
index 0000000..6620e01
--- /dev/null
+++ b/test/wpt/tests/interfaces/css-animations.idl
@@ -0,0 +1,47 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: CSS Animations Level 1 (https://drafts.csswg.org/css-animations-1/)
+
+[Exposed=Window]
+interface AnimationEvent : Event {
+ constructor(CSSOMString type, optional AnimationEventInit animationEventInitDict = {});
+ readonly attribute CSSOMString animationName;
+ readonly attribute double elapsedTime;
+ readonly attribute CSSOMString pseudoElement;
+};
+dictionary AnimationEventInit : EventInit {
+ CSSOMString animationName = "";
+ double elapsedTime = 0.0;
+ CSSOMString pseudoElement = "";
+};
+
+partial interface CSSRule {
+ const unsigned short KEYFRAMES_RULE = 7;
+ const unsigned short KEYFRAME_RULE = 8;
+};
+
+[Exposed=Window]
+interface CSSKeyframeRule : CSSRule {
+ attribute CSSOMString keyText;
+ [SameObject, PutForwards=cssText] readonly attribute CSSStyleDeclaration style;
+};
+
+[Exposed=Window]
+interface CSSKeyframesRule : CSSRule {
+ attribute CSSOMString name;
+ readonly attribute CSSRuleList cssRules;
+ readonly attribute unsigned long length;
+
+ getter CSSKeyframeRule (unsigned long index);
+ undefined appendRule(CSSOMString rule);
+ undefined deleteRule(CSSOMString select);
+ CSSKeyframeRule? findRule(CSSOMString select);
+};
+
+partial interface mixin GlobalEventHandlers {
+ attribute EventHandler onanimationstart;
+ attribute EventHandler onanimationiteration;
+ attribute EventHandler onanimationend;
+ attribute EventHandler onanimationcancel;
+};
diff --git a/test/wpt/tests/interfaces/css-cascade-6.idl b/test/wpt/tests/interfaces/css-cascade-6.idl
new file mode 100644
index 0000000..3bdf6ba
--- /dev/null
+++ b/test/wpt/tests/interfaces/css-cascade-6.idl
@@ -0,0 +1,10 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: CSS Cascading and Inheritance Level 6 (https://drafts.csswg.org/css-cascade-6/)
+
+[Exposed=Window]
+interface CSSScopeRule : CSSGroupingRule {
+ readonly attribute CSSOMString? start;
+ readonly attribute CSSOMString? end;
+};
diff --git a/test/wpt/tests/interfaces/css-cascade.idl b/test/wpt/tests/interfaces/css-cascade.idl
new file mode 100644
index 0000000..0dd9969
--- /dev/null
+++ b/test/wpt/tests/interfaces/css-cascade.idl
@@ -0,0 +1,14 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: CSS Cascading and Inheritance Level 5 (https://drafts.csswg.org/css-cascade-5/)
+
+[Exposed=Window]
+interface CSSLayerBlockRule : CSSGroupingRule {
+ readonly attribute CSSOMString name;
+};
+
+[Exposed=Window]
+interface CSSLayerStatementRule : CSSRule {
+ readonly attribute FrozenArray<CSSOMString> nameList;
+};
diff --git a/test/wpt/tests/interfaces/css-color-5.idl b/test/wpt/tests/interfaces/css-color-5.idl
new file mode 100644
index 0000000..6f5c6df
--- /dev/null
+++ b/test/wpt/tests/interfaces/css-color-5.idl
@@ -0,0 +1,12 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: CSS Color Module Level 5 (https://drafts.csswg.org/css-color-5/)
+
+[Exposed=Window]
+interface CSSColorProfileRule : CSSRule {
+ readonly attribute CSSOMString name ;
+ readonly attribute CSSOMString src ;
+ readonly attribute CSSOMString renderingIntent ;
+ readonly attribute CSSOMString components ;
+};
diff --git a/test/wpt/tests/interfaces/css-conditional.idl b/test/wpt/tests/interfaces/css-conditional.idl
new file mode 100644
index 0000000..d87f305
--- /dev/null
+++ b/test/wpt/tests/interfaces/css-conditional.idl
@@ -0,0 +1,27 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: CSS Conditional Rules Module Level 3 (https://drafts.csswg.org/css-conditional-3/)
+
+partial interface CSSRule {
+ const unsigned short SUPPORTS_RULE = 12;
+};
+
+[Exposed=Window]
+interface CSSConditionRule : CSSGroupingRule {
+ readonly attribute CSSOMString conditionText;
+};
+
+[Exposed=Window]
+interface CSSMediaRule : CSSConditionRule {
+ [SameObject, PutForwards=mediaText] readonly attribute MediaList media;
+};
+
+[Exposed=Window]
+interface CSSSupportsRule : CSSConditionRule {
+};
+
+partial namespace CSS {
+ boolean supports(CSSOMString property, CSSOMString value);
+ boolean supports(CSSOMString conditionText);
+};
diff --git a/test/wpt/tests/interfaces/css-contain-3.idl b/test/wpt/tests/interfaces/css-contain-3.idl
new file mode 100644
index 0000000..0ecf380
--- /dev/null
+++ b/test/wpt/tests/interfaces/css-contain-3.idl
@@ -0,0 +1,10 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: CSS Containment Module Level 3 (https://drafts.csswg.org/css-contain-3/)
+
+[Exposed=Window]
+interface CSSContainerRule : CSSConditionRule {
+ readonly attribute CSSOMString containerName;
+ readonly attribute CSSOMString containerQuery;
+};
diff --git a/test/wpt/tests/interfaces/css-contain.idl b/test/wpt/tests/interfaces/css-contain.idl
new file mode 100644
index 0000000..be2137a
--- /dev/null
+++ b/test/wpt/tests/interfaces/css-contain.idl
@@ -0,0 +1,13 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: CSS Containment Module Level 2 (https://drafts.csswg.org/css-contain-2/)
+
+[Exposed=Window]
+interface ContentVisibilityAutoStateChangeEvent : Event {
+ constructor(DOMString type, optional ContentVisibilityAutoStateChangeEventInit eventInitDict = {});
+ readonly attribute boolean skipped;
+};
+dictionary ContentVisibilityAutoStateChangeEventInit : EventInit {
+ boolean skipped = false;
+};
diff --git a/test/wpt/tests/interfaces/css-counter-styles.idl b/test/wpt/tests/interfaces/css-counter-styles.idl
new file mode 100644
index 0000000..f679e0f
--- /dev/null
+++ b/test/wpt/tests/interfaces/css-counter-styles.idl
@@ -0,0 +1,23 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: CSS Counter Styles Level 3 (https://drafts.csswg.org/css-counter-styles-3/)
+
+partial interface CSSRule {
+ const unsigned short COUNTER_STYLE_RULE = 11;
+};
+
+[Exposed=Window]
+interface CSSCounterStyleRule : CSSRule {
+ attribute CSSOMString name;
+ attribute CSSOMString system;
+ attribute CSSOMString symbols;
+ attribute CSSOMString additiveSymbols;
+ attribute CSSOMString negative;
+ attribute CSSOMString prefix;
+ attribute CSSOMString suffix;
+ attribute CSSOMString range;
+ attribute CSSOMString pad;
+ attribute CSSOMString speakAs;
+ attribute CSSOMString fallback;
+};
diff --git a/test/wpt/tests/interfaces/css-font-loading.idl b/test/wpt/tests/interfaces/css-font-loading.idl
new file mode 100644
index 0000000..6f2e16d
--- /dev/null
+++ b/test/wpt/tests/interfaces/css-font-loading.idl
@@ -0,0 +1,134 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: CSS Font Loading Module Level 3 (https://drafts.csswg.org/css-font-loading-3/)
+
+typedef (ArrayBuffer or ArrayBufferView) BinaryData;
+
+dictionary FontFaceDescriptors {
+ CSSOMString style = "normal";
+ CSSOMString weight = "normal";
+ CSSOMString stretch = "normal";
+ CSSOMString unicodeRange = "U+0-10FFFF";
+ CSSOMString variant = "normal";
+ CSSOMString featureSettings = "normal";
+ CSSOMString variationSettings = "normal";
+ CSSOMString display = "auto";
+ CSSOMString ascentOverride = "normal";
+ CSSOMString descentOverride = "normal";
+ CSSOMString lineGapOverride = "normal";
+};
+
+enum FontFaceLoadStatus { "unloaded", "loading", "loaded", "error" };
+
+[Exposed=(Window,Worker)]
+interface FontFace {
+ constructor(CSSOMString family, (CSSOMString or BinaryData) source,
+ optional FontFaceDescriptors descriptors = {});
+ attribute CSSOMString family;
+ attribute CSSOMString style;
+ attribute CSSOMString weight;
+ attribute CSSOMString stretch;
+ attribute CSSOMString unicodeRange;
+ attribute CSSOMString variant;
+ attribute CSSOMString featureSettings;
+ attribute CSSOMString variationSettings;
+ attribute CSSOMString display;
+ attribute CSSOMString ascentOverride;
+ attribute CSSOMString descentOverride;
+ attribute CSSOMString lineGapOverride;
+
+ readonly attribute FontFaceLoadStatus status;
+
+ Promise<FontFace> load();
+ readonly attribute Promise<FontFace> loaded;
+};
+
+[Exposed=(Window,Worker)]
+interface FontFaceFeatures {
+ /* The CSSWG is still discussing what goes in here */
+};
+
+[Exposed=(Window,Worker)]
+interface FontFaceVariationAxis {
+ readonly attribute DOMString name;
+ readonly attribute DOMString axisTag;
+ readonly attribute double minimumValue;
+ readonly attribute double maximumValue;
+ readonly attribute double defaultValue;
+};
+
+[Exposed=(Window,Worker)]
+interface FontFaceVariations {
+ readonly setlike<FontFaceVariationAxis>;
+};
+
+[Exposed=(Window,Worker)]
+interface FontFacePalette {
+ iterable<DOMString>;
+ readonly attribute unsigned long length;
+ getter DOMString (unsigned long index);
+ readonly attribute boolean usableWithLightBackground;
+ readonly attribute boolean usableWithDarkBackground;
+};
+
+[Exposed=(Window,Worker)]
+interface FontFacePalettes {
+ iterable<FontFacePalette>;
+ readonly attribute unsigned long length;
+ getter FontFacePalette (unsigned long index);
+};
+
+partial interface FontFace {
+ readonly attribute FontFaceFeatures features;
+ readonly attribute FontFaceVariations variations;
+ readonly attribute FontFacePalettes palettes;
+};
+
+dictionary FontFaceSetLoadEventInit : EventInit {
+ sequence<FontFace> fontfaces = [];
+};
+
+[Exposed=(Window,Worker)]
+interface FontFaceSetLoadEvent : Event {
+ constructor(CSSOMString type, optional FontFaceSetLoadEventInit eventInitDict = {});
+ [SameObject] readonly attribute FrozenArray<FontFace> fontfaces;
+};
+
+enum FontFaceSetLoadStatus { "loading", "loaded" };
+
+[Exposed=(Window,Worker)]
+interface FontFaceSet : EventTarget {
+ constructor(sequence<FontFace> initialFaces);
+
+ setlike<FontFace>;
+ FontFaceSet add(FontFace font);
+ boolean delete(FontFace font);
+ undefined clear();
+
+ // events for when loading state changes
+ attribute EventHandler onloading;
+ attribute EventHandler onloadingdone;
+ attribute EventHandler onloadingerror;
+
+ // check and start loads if appropriate
+ // and fulfill promise when all loads complete
+ Promise<sequence<FontFace>> load(CSSOMString font, optional CSSOMString text = " ");
+
+ // return whether all fonts in the fontlist are loaded
+ // (does not initiate load if not available)
+ boolean check(CSSOMString font, optional CSSOMString text = " ");
+
+ // async notification that font loading and layout operations are done
+ readonly attribute Promise<FontFaceSet> ready;
+
+ // loading state, "loading" while one or more fonts loading, "loaded" otherwise
+ readonly attribute FontFaceSetLoadStatus status;
+};
+
+interface mixin FontFaceSource {
+ readonly attribute FontFaceSet fonts;
+};
+
+Document includes FontFaceSource;
+WorkerGlobalScope includes FontFaceSource;
diff --git a/test/wpt/tests/interfaces/css-fonts.idl b/test/wpt/tests/interfaces/css-fonts.idl
new file mode 100644
index 0000000..eddfc02
--- /dev/null
+++ b/test/wpt/tests/interfaces/css-fonts.idl
@@ -0,0 +1,36 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: CSS Fonts Module Level 4 (https://drafts.csswg.org/css-fonts-4/)
+
+[Exposed=Window]
+interface CSSFontFaceRule : CSSRule {
+ readonly attribute CSSStyleDeclaration style;
+};
+
+partial interface CSSRule { const unsigned short FONT_FEATURE_VALUES_RULE = 14;
+};
+[Exposed=Window]
+interface CSSFontFeatureValuesRule : CSSRule {
+ attribute CSSOMString fontFamily;
+ readonly attribute CSSFontFeatureValuesMap annotation;
+ readonly attribute CSSFontFeatureValuesMap ornaments;
+ readonly attribute CSSFontFeatureValuesMap stylistic;
+ readonly attribute CSSFontFeatureValuesMap swash;
+ readonly attribute CSSFontFeatureValuesMap characterVariant;
+ readonly attribute CSSFontFeatureValuesMap styleset;
+};
+
+[Exposed=Window]
+interface CSSFontFeatureValuesMap {
+ maplike<CSSOMString, sequence<unsigned long>>;
+ undefined set(CSSOMString featureValueName,
+ (unsigned long or sequence<unsigned long>) values);
+};
+
+[Exposed=Window]interface CSSFontPaletteValuesRule : CSSRule {
+ readonly attribute CSSOMString name;
+ readonly attribute CSSOMString fontFamily;
+ readonly attribute CSSOMString basePalette;
+ readonly attribute CSSOMString overrideColors;
+};
diff --git a/test/wpt/tests/interfaces/css-highlight-api.idl b/test/wpt/tests/interfaces/css-highlight-api.idl
new file mode 100644
index 0000000..f3c6b2e
--- /dev/null
+++ b/test/wpt/tests/interfaces/css-highlight-api.idl
@@ -0,0 +1,27 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: CSS Custom Highlight API Module Level 1 (https://drafts.csswg.org/css-highlight-api-1/)
+
+enum HighlightType {
+ "highlight",
+ "spelling-error",
+ "grammar-error"
+};
+
+[Exposed=Window]
+interface Highlight {
+ constructor(AbstractRange... initialRanges);
+ setlike<AbstractRange>;
+ attribute long priority;
+ attribute HighlightType type;
+};
+
+partial namespace CSS {
+ readonly attribute HighlightRegistry highlights;
+};
+
+[Exposed=Window]
+interface HighlightRegistry {
+ maplike<DOMString, Highlight>;
+};
diff --git a/test/wpt/tests/interfaces/css-images-4.idl b/test/wpt/tests/interfaces/css-images-4.idl
new file mode 100644
index 0000000..8866b00
--- /dev/null
+++ b/test/wpt/tests/interfaces/css-images-4.idl
@@ -0,0 +1,8 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: CSS Images Module Level 4 (https://drafts.csswg.org/css-images-4/)
+
+partial namespace CSS {
+ [SameObject] readonly attribute any elementSources;
+};
diff --git a/test/wpt/tests/interfaces/css-layout-api.idl b/test/wpt/tests/interfaces/css-layout-api.idl
new file mode 100644
index 0000000..2b772d5
--- /dev/null
+++ b/test/wpt/tests/interfaces/css-layout-api.idl
@@ -0,0 +1,144 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: CSS Layout API Level 1 (https://drafts.css-houdini.org/css-layout-api-1/)
+
+partial namespace CSS {
+ [SameObject] readonly attribute Worklet layoutWorklet;
+};
+
+[Global=(Worklet,LayoutWorklet),Exposed=LayoutWorklet]
+interface LayoutWorkletGlobalScope : WorkletGlobalScope {
+ undefined registerLayout(DOMString name, VoidFunction layoutCtor);
+};
+
+dictionary LayoutOptions {
+ ChildDisplayType childDisplay = "block";
+ LayoutSizingMode sizing = "block-like";
+};
+
+enum ChildDisplayType {
+ "block", // default - "blockifies" the child boxes.
+ "normal",
+};
+
+enum LayoutSizingMode {
+ "block-like", // default - Sizing behaves like block containers.
+ "manual", // Sizing is specified by the web developer.
+};
+
+[Exposed=LayoutWorklet]
+interface LayoutChild {
+ readonly attribute StylePropertyMapReadOnly styleMap;
+
+ Promise<IntrinsicSizes> intrinsicSizes();
+ Promise<LayoutFragment> layoutNextFragment(LayoutConstraintsOptions constraints, ChildBreakToken breakToken);
+};
+
+[Exposed=LayoutWorklet]
+interface LayoutFragment {
+ readonly attribute double inlineSize;
+ readonly attribute double blockSize;
+
+ attribute double inlineOffset;
+ attribute double blockOffset;
+
+ readonly attribute any data;
+
+ readonly attribute ChildBreakToken? breakToken;
+};
+
+[Exposed=LayoutWorklet]
+interface IntrinsicSizes {
+ readonly attribute double minContentSize;
+ readonly attribute double maxContentSize;
+};
+
+[Exposed=LayoutWorklet]
+interface LayoutConstraints {
+ readonly attribute double availableInlineSize;
+ readonly attribute double availableBlockSize;
+
+ readonly attribute double? fixedInlineSize;
+ readonly attribute double? fixedBlockSize;
+
+ readonly attribute double percentageInlineSize;
+ readonly attribute double percentageBlockSize;
+
+ readonly attribute double? blockFragmentationOffset;
+ readonly attribute BlockFragmentationType blockFragmentationType;
+
+ readonly attribute any data;
+};
+
+enum BlockFragmentationType { "none", "page", "column", "region" };
+
+dictionary LayoutConstraintsOptions {
+ double availableInlineSize;
+ double availableBlockSize;
+
+ double fixedInlineSize;
+ double fixedBlockSize;
+
+ double percentageInlineSize;
+ double percentageBlockSize;
+
+ double blockFragmentationOffset;
+ BlockFragmentationType blockFragmentationType = "none";
+
+ any data;
+};
+
+[Exposed=LayoutWorklet]
+interface ChildBreakToken {
+ readonly attribute BreakType breakType;
+ readonly attribute LayoutChild child;
+};
+
+[Exposed=LayoutWorklet]
+interface BreakToken {
+ readonly attribute FrozenArray<ChildBreakToken> childBreakTokens;
+ readonly attribute any data;
+};
+
+dictionary BreakTokenOptions {
+ sequence<ChildBreakToken> childBreakTokens;
+ any data = null;
+};
+
+enum BreakType { "none", "line", "column", "page", "region" };
+
+[Exposed=LayoutWorklet]
+interface LayoutEdges {
+ readonly attribute double inlineStart;
+ readonly attribute double inlineEnd;
+
+ readonly attribute double blockStart;
+ readonly attribute double blockEnd;
+
+ // Convenience attributes for the sum in one direction.
+ readonly attribute double inline;
+ readonly attribute double block;
+};
+
+// This is the final return value from the author defined layout() method.
+dictionary FragmentResultOptions {
+ double inlineSize = 0;
+ double blockSize = 0;
+ double autoBlockSize = 0;
+ sequence<LayoutFragment> childFragments = [];
+ any data = null;
+ BreakTokenOptions breakToken = null;
+};
+
+[Exposed=LayoutWorklet]
+interface FragmentResult {
+ constructor(optional FragmentResultOptions options = {});
+ readonly attribute double inlineSize;
+ readonly attribute double blockSize;
+};
+
+dictionary IntrinsicSizesResultOptions {
+ double maxContentSize;
+ double minContentSize;
+};
diff --git a/test/wpt/tests/interfaces/css-masking.idl b/test/wpt/tests/interfaces/css-masking.idl
new file mode 100644
index 0000000..72fbd9a
--- /dev/null
+++ b/test/wpt/tests/interfaces/css-masking.idl
@@ -0,0 +1,20 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: CSS Masking Module Level 1 (https://drafts.fxtf.org/css-masking-1/)
+
+[Exposed=Window]
+interface SVGClipPathElement : SVGElement {
+ readonly attribute SVGAnimatedEnumeration clipPathUnits;
+ readonly attribute SVGAnimatedTransformList transform;
+};
+
+[Exposed=Window]
+interface SVGMaskElement : SVGElement {
+ readonly attribute SVGAnimatedEnumeration maskUnits;
+ readonly attribute SVGAnimatedEnumeration maskContentUnits;
+ readonly attribute SVGAnimatedLength x;
+ readonly attribute SVGAnimatedLength y;
+ readonly attribute SVGAnimatedLength width;
+ readonly attribute SVGAnimatedLength height;
+};
diff --git a/test/wpt/tests/interfaces/css-nav.idl b/test/wpt/tests/interfaces/css-nav.idl
new file mode 100644
index 0000000..03f039e
--- /dev/null
+++ b/test/wpt/tests/interfaces/css-nav.idl
@@ -0,0 +1,48 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: CSS Spatial Navigation Level 1 (https://drafts.csswg.org/css-nav-1/)
+
+enum SpatialNavigationDirection {
+ "up",
+ "down",
+ "left",
+ "right",
+};
+
+partial interface Window {
+ undefined navigate(SpatialNavigationDirection dir);
+};
+
+enum FocusableAreaSearchMode {
+ "visible",
+ "all"
+};
+
+dictionary FocusableAreasOption {
+ FocusableAreaSearchMode mode;
+};
+
+dictionary SpatialNavigationSearchOptions {
+ sequence<Node>? candidates;
+ Node? container;
+};
+
+partial interface Element {
+ Node getSpatialNavigationContainer();
+ sequence<Node> focusableAreas(optional FocusableAreasOption option = {});
+ Node? spatialNavigationSearch(SpatialNavigationDirection dir, optional SpatialNavigationSearchOptions options = {});
+};
+
+[Exposed=Window]
+interface NavigationEvent : UIEvent {
+ constructor(DOMString type,
+ optional NavigationEventInit eventInitDict = {});
+ readonly attribute SpatialNavigationDirection dir;
+ readonly attribute EventTarget? relatedTarget;
+};
+
+dictionary NavigationEventInit : UIEventInit {
+ SpatialNavigationDirection dir;
+ EventTarget? relatedTarget = null;
+};
diff --git a/test/wpt/tests/interfaces/css-nesting.idl b/test/wpt/tests/interfaces/css-nesting.idl
new file mode 100644
index 0000000..01f27ab
--- /dev/null
+++ b/test/wpt/tests/interfaces/css-nesting.idl
@@ -0,0 +1,10 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: CSS Nesting Module (https://drafts.csswg.org/css-nesting-1/)
+
+partial interface CSSStyleRule {
+ [SameObject] readonly attribute CSSRuleList cssRules;
+ unsigned long insertRule(CSSOMString rule, optional unsigned long index = 0);
+ undefined deleteRule(unsigned long index);
+};
diff --git a/test/wpt/tests/interfaces/css-paint-api.idl b/test/wpt/tests/interfaces/css-paint-api.idl
new file mode 100644
index 0000000..0924c53
--- /dev/null
+++ b/test/wpt/tests/interfaces/css-paint-api.idl
@@ -0,0 +1,39 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: CSS Painting API Level 1 (https://drafts.css-houdini.org/css-paint-api-1/)
+
+partial namespace CSS {
+ [SameObject] readonly attribute Worklet paintWorklet;
+};
+
+[Global=(Worklet,PaintWorklet),Exposed=PaintWorklet]
+interface PaintWorkletGlobalScope : WorkletGlobalScope {
+ undefined registerPaint(DOMString name, VoidFunction paintCtor);
+ readonly attribute unrestricted double devicePixelRatio;
+};
+
+dictionary PaintRenderingContext2DSettings {
+ boolean alpha = true;
+};
+
+[Exposed=PaintWorklet]
+interface PaintRenderingContext2D {
+};
+PaintRenderingContext2D includes CanvasState;
+PaintRenderingContext2D includes CanvasTransform;
+PaintRenderingContext2D includes CanvasCompositing;
+PaintRenderingContext2D includes CanvasImageSmoothing;
+PaintRenderingContext2D includes CanvasFillStrokeStyles;
+PaintRenderingContext2D includes CanvasShadowStyles;
+PaintRenderingContext2D includes CanvasRect;
+PaintRenderingContext2D includes CanvasDrawPath;
+PaintRenderingContext2D includes CanvasDrawImage;
+PaintRenderingContext2D includes CanvasPathDrawingStyles;
+PaintRenderingContext2D includes CanvasPath;
+
+[Exposed=PaintWorklet]
+interface PaintSize {
+ readonly attribute double width;
+ readonly attribute double height;
+};
diff --git a/test/wpt/tests/interfaces/css-parser-api.idl b/test/wpt/tests/interfaces/css-parser-api.idl
new file mode 100644
index 0000000..4e34a3f
--- /dev/null
+++ b/test/wpt/tests/interfaces/css-parser-api.idl
@@ -0,0 +1,76 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: CSS Parser API (https://wicg.github.io/css-parser-api/)
+
+typedef (DOMString or ReadableStream) CSSStringSource;
+typedef (DOMString or CSSStyleValue or CSSParserValue) CSSToken;
+
+partial namespace CSS {
+ Promise<sequence<CSSParserRule>> parseStylesheet(CSSStringSource css, optional CSSParserOptions options = {});
+ Promise<sequence<CSSParserRule>> parseRuleList(CSSStringSource css, optional CSSParserOptions options = {});
+ Promise<CSSParserRule> parseRule(CSSStringSource css, optional CSSParserOptions options = {});
+ Promise<sequence<CSSParserRule>> parseDeclarationList(CSSStringSource css, optional CSSParserOptions options = {});
+ CSSParserDeclaration parseDeclaration(DOMString css, optional CSSParserOptions options = {});
+ CSSToken parseValue(DOMString css);
+ sequence<CSSToken> parseValueList(DOMString css);
+ sequence<sequence<CSSToken>> parseCommaValueList(DOMString css);
+};
+
+dictionary CSSParserOptions {
+ object atRules;
+ /* dict of at-rule name => at-rule type
+ (contains decls or contains qualified rules) */
+};
+
+[Exposed=Window]
+interface CSSParserRule {
+ /* Just a superclass. */
+};
+
+[Exposed=Window]
+interface CSSParserAtRule : CSSParserRule {
+ constructor(DOMString name, sequence<CSSToken> prelude, optional sequence<CSSParserRule>? body);
+ readonly attribute DOMString name;
+ readonly attribute FrozenArray<CSSParserValue> prelude;
+ readonly attribute FrozenArray<CSSParserRule>? body;
+ /* nullable to handle at-statements */
+ stringifier;
+};
+
+[Exposed=Window]
+interface CSSParserQualifiedRule : CSSParserRule {
+ constructor(sequence<CSSToken> prelude, optional sequence<CSSParserRule>? body);
+ readonly attribute FrozenArray<CSSParserValue> prelude;
+ readonly attribute FrozenArray<CSSParserRule> body;
+ stringifier;
+};
+
+[Exposed=Window]
+interface CSSParserDeclaration : CSSParserRule {
+ constructor(DOMString name, optional sequence<CSSParserRule> body);
+ readonly attribute DOMString name;
+ readonly attribute FrozenArray<CSSParserValue> body;
+ stringifier;
+};
+
+[Exposed=Window]
+interface CSSParserValue {
+ /* Just a superclass. */
+};
+
+[Exposed=Window]
+interface CSSParserBlock : CSSParserValue {
+ constructor(DOMString name, sequence<CSSParserValue> body);
+ readonly attribute DOMString name; /* "[]", "{}", or "()" */
+ readonly attribute FrozenArray<CSSParserValue> body;
+ stringifier;
+};
+
+[Exposed=Window]
+interface CSSParserFunction : CSSParserValue {
+ constructor(DOMString name, sequence<sequence<CSSParserValue>> args);
+ readonly attribute DOMString name;
+ readonly attribute FrozenArray<FrozenArray<CSSParserValue>> args;
+ stringifier;
+};
diff --git a/test/wpt/tests/interfaces/css-properties-values-api.idl b/test/wpt/tests/interfaces/css-properties-values-api.idl
new file mode 100644
index 0000000..eb7d7b0
--- /dev/null
+++ b/test/wpt/tests/interfaces/css-properties-values-api.idl
@@ -0,0 +1,23 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: CSS Properties and Values API Level 1 (https://drafts.css-houdini.org/css-properties-values-api-1/)
+
+dictionary PropertyDefinition {
+ required DOMString name;
+ DOMString syntax = "*";
+ required boolean inherits;
+ DOMString initialValue;
+};
+
+partial namespace CSS {
+ undefined registerProperty(PropertyDefinition definition);
+};
+
+[Exposed=Window]
+interface CSSPropertyRule : CSSRule {
+ readonly attribute CSSOMString name;
+ readonly attribute CSSOMString syntax;
+ readonly attribute boolean inherits;
+ readonly attribute CSSOMString? initialValue;
+};
diff --git a/test/wpt/tests/interfaces/css-pseudo.idl b/test/wpt/tests/interfaces/css-pseudo.idl
new file mode 100644
index 0000000..dbe4c54
--- /dev/null
+++ b/test/wpt/tests/interfaces/css-pseudo.idl
@@ -0,0 +1,16 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: CSS Pseudo-Elements Module Level 4 (https://drafts.csswg.org/css-pseudo-4/)
+
+[Exposed=Window]
+interface CSSPseudoElement : EventTarget {
+ readonly attribute CSSOMString type;
+ readonly attribute Element element;
+ readonly attribute (Element or CSSPseudoElement) parent;
+ CSSPseudoElement? pseudo(CSSOMString type);
+};
+
+partial interface Element {
+ CSSPseudoElement? pseudo(CSSOMString type);
+};
diff --git a/test/wpt/tests/interfaces/css-regions.idl b/test/wpt/tests/interfaces/css-regions.idl
new file mode 100644
index 0000000..113438f
--- /dev/null
+++ b/test/wpt/tests/interfaces/css-regions.idl
@@ -0,0 +1,29 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: CSS Regions Module Level 1 (https://drafts.csswg.org/css-regions-1/)
+
+partial interface Document {
+ readonly attribute NamedFlowMap namedFlows;
+};
+
+[Exposed=Window] interface NamedFlowMap {
+ maplike<CSSOMString, NamedFlow>;
+};
+
+[Exposed=Window]
+interface NamedFlow : EventTarget {
+ readonly attribute CSSOMString name;
+ readonly attribute boolean overset;
+ sequence<Element> getRegions();
+ readonly attribute short firstEmptyRegionIndex;
+ sequence<Node> getContent();
+ sequence<Element> getRegionsByContent(Node node);
+};
+
+interface mixin Region {
+ readonly attribute CSSOMString regionOverset;
+ sequence<Range>? getRegionFlowRanges();
+};
+
+Element includes Region;
diff --git a/test/wpt/tests/interfaces/css-shadow-parts.idl b/test/wpt/tests/interfaces/css-shadow-parts.idl
new file mode 100644
index 0000000..3759199
--- /dev/null
+++ b/test/wpt/tests/interfaces/css-shadow-parts.idl
@@ -0,0 +1,8 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: CSS Shadow Parts (https://drafts.csswg.org/css-shadow-parts-1/)
+
+partial interface Element {
+ [SameObject, PutForwards=value] readonly attribute DOMTokenList part;
+};
diff --git a/test/wpt/tests/interfaces/css-toggle.tentative.idl b/test/wpt/tests/interfaces/css-toggle.tentative.idl
new file mode 100644
index 0000000..5587019
--- /dev/null
+++ b/test/wpt/tests/interfaces/css-toggle.tentative.idl
@@ -0,0 +1,51 @@
+partial interface Element {
+ [SameObject] readonly attribute CSSToggleMap toggles;
+};
+
+interface CSSToggleMap {
+ maplike<DOMString, CSSToggle>;
+ CSSToggleMap set(DOMString key, CSSToggle value);
+};
+
+interface CSSToggle {
+ attribute (unsigned long or DOMString) value;
+ attribute unsigned long? valueAsNumber;
+ attribute DOMString? valueAsString;
+
+ attribute (unsigned long or FrozenArray<DOMString>) states;
+ attribute boolean group;
+ attribute CSSToggleScope scope;
+ attribute CSSToggleCycle cycle;
+
+ constructor(optional CSSToggleData options);
+};
+
+dictionary CSSToggleData {
+ (unsigned long or DOMString) value = 0;
+ (unsigned long or sequence<DOMString>) states = 1;
+ boolean group = false;
+ CSSToggleScope scope = "wide";
+ CSSToggleCycle cycle = "cycle";
+};
+
+enum CSSToggleScope {
+ "narrow",
+ "wide",
+};
+
+enum CSSToggleCycle {
+ "cycle",
+ "cycle-on",
+ "sticky",
+};
+
+interface CSSToggleEvent : Event {
+ constructor(DOMString type, optional CSSToggleEventInit eventInitDict = {});
+ readonly attribute DOMString toggleName;
+ readonly attribute CSSToggle? toggle;
+};
+
+dictionary CSSToggleEventInit : EventInit {
+ DOMString toggleName = "";
+ CSSToggle? toggle = null;
+};
diff --git a/test/wpt/tests/interfaces/css-transitions-2.idl b/test/wpt/tests/interfaces/css-transitions-2.idl
new file mode 100644
index 0000000..9d06f3c
--- /dev/null
+++ b/test/wpt/tests/interfaces/css-transitions-2.idl
@@ -0,0 +1,9 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: CSS Transitions Level 2 (https://drafts.csswg.org/css-transitions-2/)
+
+[Exposed=Window]
+interface CSSTransition : Animation {
+ readonly attribute CSSOMString transitionProperty;
+};
diff --git a/test/wpt/tests/interfaces/css-transitions.idl b/test/wpt/tests/interfaces/css-transitions.idl
new file mode 100644
index 0000000..0f00b2c
--- /dev/null
+++ b/test/wpt/tests/interfaces/css-transitions.idl
@@ -0,0 +1,25 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: CSS Transitions (https://drafts.csswg.org/css-transitions-1/)
+
+[Exposed=Window]
+interface TransitionEvent : Event {
+ constructor(CSSOMString type, optional TransitionEventInit transitionEventInitDict = {});
+ readonly attribute CSSOMString propertyName;
+ readonly attribute double elapsedTime;
+ readonly attribute CSSOMString pseudoElement;
+};
+
+dictionary TransitionEventInit : EventInit {
+ CSSOMString propertyName = "";
+ double elapsedTime = 0.0;
+ CSSOMString pseudoElement = "";
+};
+
+partial interface mixin GlobalEventHandlers {
+ attribute EventHandler ontransitionrun;
+ attribute EventHandler ontransitionstart;
+ attribute EventHandler ontransitionend;
+ attribute EventHandler ontransitioncancel;
+};
diff --git a/test/wpt/tests/interfaces/css-typed-om.idl b/test/wpt/tests/interfaces/css-typed-om.idl
new file mode 100644
index 0000000..0df6a03
--- /dev/null
+++ b/test/wpt/tests/interfaces/css-typed-om.idl
@@ -0,0 +1,423 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: CSS Typed OM Level 1 (https://drafts.css-houdini.org/css-typed-om-1/)
+
+[Exposed=(Window, Worker, PaintWorklet, LayoutWorklet)]
+interface CSSStyleValue {
+ stringifier;
+ [Exposed=Window] static CSSStyleValue parse(USVString property, USVString cssText);
+ [Exposed=Window] static sequence<CSSStyleValue> parseAll(USVString property, USVString cssText);
+};
+
+[Exposed=(Window, Worker, PaintWorklet, LayoutWorklet)]
+interface StylePropertyMapReadOnly {
+ iterable<USVString, sequence<CSSStyleValue>>;
+ (undefined or CSSStyleValue) get(USVString property);
+ sequence<CSSStyleValue> getAll(USVString property);
+ boolean has(USVString property);
+ readonly attribute unsigned long size;
+};
+
+[Exposed=Window]
+interface StylePropertyMap : StylePropertyMapReadOnly {
+ undefined set(USVString property, (CSSStyleValue or USVString)... values);
+ undefined append(USVString property, (CSSStyleValue or USVString)... values);
+ undefined delete(USVString property);
+ undefined clear();
+};
+
+partial interface Element {
+ [SameObject] StylePropertyMapReadOnly computedStyleMap();
+};
+
+partial interface CSSStyleRule {
+ [SameObject] readonly attribute StylePropertyMap styleMap;
+};
+
+partial interface mixin ElementCSSInlineStyle {
+ [SameObject] readonly attribute StylePropertyMap attributeStyleMap;
+};
+
+[Exposed=(Window, Worker, PaintWorklet, LayoutWorklet)]
+interface CSSUnparsedValue : CSSStyleValue {
+ constructor(sequence<CSSUnparsedSegment> members);
+ iterable<CSSUnparsedSegment>;
+ readonly attribute unsigned long length;
+ getter CSSUnparsedSegment (unsigned long index);
+ setter CSSUnparsedSegment (unsigned long index, CSSUnparsedSegment val);
+};
+
+typedef (USVString or CSSVariableReferenceValue) CSSUnparsedSegment;
+
+[Exposed=(Window, Worker, PaintWorklet, LayoutWorklet)]
+interface CSSVariableReferenceValue {
+ constructor(USVString variable, optional CSSUnparsedValue? fallback = null);
+ attribute USVString variable;
+ readonly attribute CSSUnparsedValue? fallback;
+};
+
+[Exposed=(Window, Worker, PaintWorklet, LayoutWorklet)]
+interface CSSKeywordValue : CSSStyleValue {
+ constructor(USVString value);
+ attribute USVString value;
+};
+
+typedef (DOMString or CSSKeywordValue) CSSKeywordish;
+
+typedef (double or CSSNumericValue) CSSNumberish;
+
+enum CSSNumericBaseType {
+ "length",
+ "angle",
+ "time",
+ "frequency",
+ "resolution",
+ "flex",
+ "percent",
+};
+
+dictionary CSSNumericType {
+ long length;
+ long angle;
+ long time;
+ long frequency;
+ long resolution;
+ long flex;
+ long percent;
+ CSSNumericBaseType percentHint;
+};
+
+[Exposed=(Window, Worker, PaintWorklet, LayoutWorklet)]
+interface CSSNumericValue : CSSStyleValue {
+ CSSNumericValue add(CSSNumberish... values);
+ CSSNumericValue sub(CSSNumberish... values);
+ CSSNumericValue mul(CSSNumberish... values);
+ CSSNumericValue div(CSSNumberish... values);
+ CSSNumericValue min(CSSNumberish... values);
+ CSSNumericValue max(CSSNumberish... values);
+
+ boolean equals(CSSNumberish... value);
+
+ CSSUnitValue to(USVString unit);
+ CSSMathSum toSum(USVString... units);
+ CSSNumericType type();
+
+ [Exposed=Window] static CSSNumericValue parse(USVString cssText);
+};
+
+[Exposed=(Window, Worker, PaintWorklet, LayoutWorklet)]
+interface CSSUnitValue : CSSNumericValue {
+ constructor(double value, USVString unit);
+ attribute double value;
+ readonly attribute USVString unit;
+};
+
+[Exposed=(Window, Worker, PaintWorklet, LayoutWorklet)]
+interface CSSMathValue : CSSNumericValue {
+ readonly attribute CSSMathOperator operator;
+};
+
+[Exposed=(Window, Worker, PaintWorklet, LayoutWorklet)]
+interface CSSMathSum : CSSMathValue {
+ constructor(CSSNumberish... args);
+ readonly attribute CSSNumericArray values;
+};
+
+[Exposed=(Window, Worker, PaintWorklet, LayoutWorklet)]
+interface CSSMathProduct : CSSMathValue {
+ constructor(CSSNumberish... args);
+ readonly attribute CSSNumericArray values;
+};
+
+[Exposed=(Window, Worker, PaintWorklet, LayoutWorklet)]
+interface CSSMathNegate : CSSMathValue {
+ constructor(CSSNumberish arg);
+ readonly attribute CSSNumericValue value;
+};
+
+[Exposed=(Window, Worker, PaintWorklet, LayoutWorklet)]
+interface CSSMathInvert : CSSMathValue {
+ constructor(CSSNumberish arg);
+ readonly attribute CSSNumericValue value;
+};
+
+[Exposed=(Window, Worker, PaintWorklet, LayoutWorklet)]
+interface CSSMathMin : CSSMathValue {
+ constructor(CSSNumberish... args);
+ readonly attribute CSSNumericArray values;
+};
+
+[Exposed=(Window, Worker, PaintWorklet, LayoutWorklet)]
+interface CSSMathMax : CSSMathValue {
+ constructor(CSSNumberish... args);
+ readonly attribute CSSNumericArray values;
+};
+
+[Exposed=(Window, Worker, PaintWorklet, LayoutWorklet)]
+interface CSSMathClamp : CSSMathValue {
+ constructor(CSSNumberish lower, CSSNumberish value, CSSNumberish upper);
+ readonly attribute CSSNumericValue lower;
+ readonly attribute CSSNumericValue value;
+ readonly attribute CSSNumericValue upper;
+};
+
+[Exposed=(Window, Worker, PaintWorklet, LayoutWorklet)]
+interface CSSNumericArray {
+ iterable<CSSNumericValue>;
+ readonly attribute unsigned long length;
+ getter CSSNumericValue (unsigned long index);
+};
+
+enum CSSMathOperator {
+ "sum",
+ "product",
+ "negate",
+ "invert",
+ "min",
+ "max",
+ "clamp",
+};
+
+partial namespace CSS {
+ CSSUnitValue number(double value);
+ CSSUnitValue percent(double value);
+
+ // <length>
+ CSSUnitValue em(double value);
+ CSSUnitValue ex(double value);
+ CSSUnitValue ch(double value);
+ CSSUnitValue ic(double value);
+ CSSUnitValue rem(double value);
+ CSSUnitValue lh(double value);
+ CSSUnitValue rlh(double value);
+ CSSUnitValue vw(double value);
+ CSSUnitValue vh(double value);
+ CSSUnitValue vi(double value);
+ CSSUnitValue vb(double value);
+ CSSUnitValue vmin(double value);
+ CSSUnitValue vmax(double value);
+ CSSUnitValue svw(double value);
+ CSSUnitValue svh(double value);
+ CSSUnitValue svi(double value);
+ CSSUnitValue svb(double value);
+ CSSUnitValue svmin(double value);
+ CSSUnitValue svmax(double value);
+ CSSUnitValue lvw(double value);
+ CSSUnitValue lvh(double value);
+ CSSUnitValue lvi(double value);
+ CSSUnitValue lvb(double value);
+ CSSUnitValue lvmin(double value);
+ CSSUnitValue lvmax(double value);
+ CSSUnitValue dvw(double value);
+ CSSUnitValue dvh(double value);
+ CSSUnitValue dvi(double value);
+ CSSUnitValue dvb(double value);
+ CSSUnitValue dvmin(double value);
+ CSSUnitValue dvmax(double value);
+ CSSUnitValue cqw(double value);
+ CSSUnitValue cqh(double value);
+ CSSUnitValue cqi(double value);
+ CSSUnitValue cqb(double value);
+ CSSUnitValue cqmin(double value);
+ CSSUnitValue cqmax(double value);
+ CSSUnitValue cm(double value);
+ CSSUnitValue mm(double value);
+ CSSUnitValue Q(double value);
+ CSSUnitValue in(double value);
+ CSSUnitValue pt(double value);
+ CSSUnitValue pc(double value);
+ CSSUnitValue px(double value);
+
+ // <angle>
+ CSSUnitValue deg(double value);
+ CSSUnitValue grad(double value);
+ CSSUnitValue rad(double value);
+ CSSUnitValue turn(double value);
+
+ // <time>
+ CSSUnitValue s(double value);
+ CSSUnitValue ms(double value);
+
+ // <frequency>
+ CSSUnitValue Hz(double value);
+ CSSUnitValue kHz(double value);
+
+ // <resolution>
+ CSSUnitValue dpi(double value);
+ CSSUnitValue dpcm(double value);
+ CSSUnitValue dppx(double value);
+
+ // <flex>
+ CSSUnitValue fr(double value);
+};
+
+[Exposed=(Window, Worker, PaintWorklet, LayoutWorklet)]
+interface CSSTransformValue : CSSStyleValue {
+ constructor(sequence<CSSTransformComponent> transforms);
+ iterable<CSSTransformComponent>;
+ readonly attribute unsigned long length;
+ getter CSSTransformComponent (unsigned long index);
+ setter CSSTransformComponent (unsigned long index, CSSTransformComponent val);
+
+ readonly attribute boolean is2D;
+ DOMMatrix toMatrix();
+};
+
+typedef (CSSNumericValue or CSSKeywordish) CSSPerspectiveValue;
+
+[Exposed=(Window, Worker, PaintWorklet, LayoutWorklet)]
+interface CSSTransformComponent {
+ stringifier;
+ attribute boolean is2D;
+ DOMMatrix toMatrix();
+};
+
+[Exposed=(Window, Worker, PaintWorklet, LayoutWorklet)]
+interface CSSTranslate : CSSTransformComponent {
+ constructor(CSSNumericValue x, CSSNumericValue y, optional CSSNumericValue z);
+ attribute CSSNumericValue x;
+ attribute CSSNumericValue y;
+ attribute CSSNumericValue z;
+};
+
+[Exposed=(Window, Worker, PaintWorklet, LayoutWorklet)]
+interface CSSRotate : CSSTransformComponent {
+ constructor(CSSNumericValue angle);
+ constructor(CSSNumberish x, CSSNumberish y, CSSNumberish z, CSSNumericValue angle);
+ attribute CSSNumberish x;
+ attribute CSSNumberish y;
+ attribute CSSNumberish z;
+ attribute CSSNumericValue angle;
+};
+
+[Exposed=(Window, Worker, PaintWorklet, LayoutWorklet)]
+interface CSSScale : CSSTransformComponent {
+ constructor(CSSNumberish x, CSSNumberish y, optional CSSNumberish z);
+ attribute CSSNumberish x;
+ attribute CSSNumberish y;
+ attribute CSSNumberish z;
+};
+
+[Exposed=(Window, Worker, PaintWorklet, LayoutWorklet)]
+interface CSSSkew : CSSTransformComponent {
+ constructor(CSSNumericValue ax, CSSNumericValue ay);
+ attribute CSSNumericValue ax;
+ attribute CSSNumericValue ay;
+};
+
+[Exposed=(Window, Worker, PaintWorklet, LayoutWorklet)]
+interface CSSSkewX : CSSTransformComponent {
+ constructor(CSSNumericValue ax);
+ attribute CSSNumericValue ax;
+};
+
+[Exposed=(Window, Worker, PaintWorklet, LayoutWorklet)]
+interface CSSSkewY : CSSTransformComponent {
+ constructor(CSSNumericValue ay);
+ attribute CSSNumericValue ay;
+};
+
+/* Note that skew(x,y) is *not* the same as skewX(x) skewY(y),
+ thus the separate interfaces for all three. */
+
+[Exposed=(Window, Worker, PaintWorklet, LayoutWorklet)]
+interface CSSPerspective : CSSTransformComponent {
+ constructor(CSSPerspectiveValue length);
+ attribute CSSPerspectiveValue length;
+};
+
+[Exposed=(Window, Worker, PaintWorklet, LayoutWorklet)]
+interface CSSMatrixComponent : CSSTransformComponent {
+ constructor(DOMMatrixReadOnly matrix, optional CSSMatrixComponentOptions options = {});
+ attribute DOMMatrix matrix;
+};
+
+dictionary CSSMatrixComponentOptions {
+ boolean is2D;
+};
+
+[Exposed=(Window, Worker, PaintWorklet, LayoutWorklet)]
+interface CSSImageValue : CSSStyleValue {
+};
+
+[Exposed=(Window, Worker, PaintWorklet, LayoutWorklet)]
+interface CSSColorValue : CSSStyleValue {
+ [Exposed=Window] static (CSSColorValue or CSSStyleValue) parse(USVString cssText);
+};
+
+typedef (CSSNumberish or CSSKeywordish) CSSColorRGBComp;
+typedef (CSSNumberish or CSSKeywordish) CSSColorPercent;
+typedef (CSSNumberish or CSSKeywordish) CSSColorNumber;
+typedef (CSSNumberish or CSSKeywordish) CSSColorAngle;
+
+[Exposed=(Window, Worker, PaintWorklet, LayoutWorklet)]
+interface CSSRGB : CSSColorValue {
+ constructor(CSSColorRGBComp r, CSSColorRGBComp g, CSSColorRGBComp b, optional CSSColorPercent alpha = 1);
+ attribute CSSColorRGBComp r;
+ attribute CSSColorRGBComp g;
+ attribute CSSColorRGBComp b;
+ attribute CSSColorPercent alpha;
+};
+
+[Exposed=(Window, Worker, PaintWorklet, LayoutWorklet)]
+interface CSSHSL : CSSColorValue {
+ constructor(CSSColorAngle h, CSSColorPercent s, CSSColorPercent l, optional CSSColorPercent alpha = 1);
+ attribute CSSColorAngle h;
+ attribute CSSColorPercent s;
+ attribute CSSColorPercent l;
+ attribute CSSColorPercent alpha;
+};
+
+[Exposed=(Window, Worker, PaintWorklet, LayoutWorklet)]
+interface CSSHWB : CSSColorValue {
+ constructor(CSSNumericValue h, CSSNumberish w, CSSNumberish b, optional CSSNumberish alpha = 1);
+ attribute CSSNumericValue h;
+ attribute CSSNumberish w;
+ attribute CSSNumberish b;
+ attribute CSSNumberish alpha;
+};
+
+[Exposed=(Window, Worker, PaintWorklet, LayoutWorklet)]
+interface CSSLab : CSSColorValue {
+ constructor(CSSColorPercent l, CSSColorNumber a, CSSColorNumber b, optional CSSColorPercent alpha = 1);
+ attribute CSSColorPercent l;
+ attribute CSSColorNumber a;
+ attribute CSSColorNumber b;
+ attribute CSSColorPercent alpha;
+};
+
+[Exposed=(Window, Worker, PaintWorklet, LayoutWorklet)]
+interface CSSLCH : CSSColorValue {
+ constructor(CSSColorPercent l, CSSColorPercent c, CSSColorAngle h, optional CSSColorPercent alpha = 1);
+ attribute CSSColorPercent l;
+ attribute CSSColorPercent c;
+ attribute CSSColorAngle h;
+ attribute CSSColorPercent alpha;
+};
+
+[Exposed=(Window, Worker, PaintWorklet, LayoutWorklet)]
+interface CSSOKLab : CSSColorValue {
+ constructor(CSSColorPercent l, CSSColorNumber a, CSSColorNumber b, optional CSSColorPercent alpha = 1);
+ attribute CSSColorPercent l;
+ attribute CSSColorNumber a;
+ attribute CSSColorNumber b;
+ attribute CSSColorPercent alpha;
+};
+
+[Exposed=(Window, Worker, PaintWorklet, LayoutWorklet)]
+interface CSSOKLCH : CSSColorValue {
+ constructor(CSSColorPercent l, CSSColorPercent c, CSSColorAngle h, optional CSSColorPercent alpha = 1);
+ attribute CSSColorPercent l;
+ attribute CSSColorPercent c;
+ attribute CSSColorAngle h;
+ attribute CSSColorPercent alpha;
+};
+
+[Exposed=(Window, Worker, PaintWorklet, LayoutWorklet)]
+interface CSSColor : CSSColorValue {
+ constructor(CSSKeywordish colorSpace, sequence<CSSColorPercent> channels, optional CSSNumberish alpha = 1);
+ attribute CSSKeywordish colorSpace;
+ attribute ObservableArray<CSSColorPercent> channels;
+ attribute CSSNumberish alpha;
+};
diff --git a/test/wpt/tests/interfaces/css-view-transitions.idl b/test/wpt/tests/interfaces/css-view-transitions.idl
new file mode 100644
index 0000000..745eb1d
--- /dev/null
+++ b/test/wpt/tests/interfaces/css-view-transitions.idl
@@ -0,0 +1,18 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: CSS View Transitions Module Level 1 (https://drafts.csswg.org/css-view-transitions-1/)
+
+partial interface Document {
+ ViewTransition startViewTransition(optional UpdateCallback? updateCallback = null);
+};
+
+callback UpdateCallback = Promise<any> ();
+
+[Exposed=Window]
+interface ViewTransition {
+ readonly attribute Promise<undefined> updateCallbackDone;
+ readonly attribute Promise<undefined> ready;
+ readonly attribute Promise<undefined> finished;
+ undefined skipTransition();
+};
diff --git a/test/wpt/tests/interfaces/cssom-view.idl b/test/wpt/tests/interfaces/cssom-view.idl
new file mode 100644
index 0000000..4e531a2
--- /dev/null
+++ b/test/wpt/tests/interfaces/cssom-view.idl
@@ -0,0 +1,200 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: CSSOM View Module (https://drafts.csswg.org/cssom-view-1/)
+
+enum ScrollBehavior { "auto", "instant", "smooth" };
+
+dictionary ScrollOptions {
+ ScrollBehavior behavior = "auto";
+};
+dictionary ScrollToOptions : ScrollOptions {
+ unrestricted double left;
+ unrestricted double top;
+};
+
+partial interface Window {
+ [NewObject] MediaQueryList matchMedia(CSSOMString query);
+ [SameObject, Replaceable] readonly attribute Screen screen;
+ [SameObject, Replaceable] readonly attribute VisualViewport? visualViewport;
+
+ // browsing context
+ undefined moveTo(long x, long y);
+ undefined moveBy(long x, long y);
+ undefined resizeTo(long width, long height);
+ undefined resizeBy(long x, long y);
+
+ // viewport
+ [Replaceable] readonly attribute long innerWidth;
+ [Replaceable] readonly attribute long innerHeight;
+
+ // viewport scrolling
+ [Replaceable] readonly attribute double scrollX;
+ [Replaceable] readonly attribute double pageXOffset;
+ [Replaceable] readonly attribute double scrollY;
+ [Replaceable] readonly attribute double pageYOffset;
+ undefined scroll(optional ScrollToOptions options = {});
+ undefined scroll(unrestricted double x, unrestricted double y);
+ undefined scrollTo(optional ScrollToOptions options = {});
+ undefined scrollTo(unrestricted double x, unrestricted double y);
+ undefined scrollBy(optional ScrollToOptions options = {});
+ undefined scrollBy(unrestricted double x, unrestricted double y);
+
+ // client
+ [Replaceable] readonly attribute long screenX;
+ [Replaceable] readonly attribute long screenLeft;
+ [Replaceable] readonly attribute long screenY;
+ [Replaceable] readonly attribute long screenTop;
+ [Replaceable] readonly attribute long outerWidth;
+ [Replaceable] readonly attribute long outerHeight;
+ [Replaceable] readonly attribute double devicePixelRatio;
+};
+
+[Exposed=Window]
+interface MediaQueryList : EventTarget {
+ readonly attribute CSSOMString media;
+ readonly attribute boolean matches;
+ undefined addListener(EventListener? callback);
+ undefined removeListener(EventListener? callback);
+ attribute EventHandler onchange;
+};
+
+[Exposed=Window]
+interface MediaQueryListEvent : Event {
+ constructor(CSSOMString type, optional MediaQueryListEventInit eventInitDict = {});
+ readonly attribute CSSOMString media;
+ readonly attribute boolean matches;
+};
+
+dictionary MediaQueryListEventInit : EventInit {
+ CSSOMString media = "";
+ boolean matches = false;
+};
+
+[Exposed=Window]
+interface Screen {
+ readonly attribute long availWidth;
+ readonly attribute long availHeight;
+ readonly attribute long width;
+ readonly attribute long height;
+ readonly attribute unsigned long colorDepth;
+ readonly attribute unsigned long pixelDepth;
+};
+
+partial interface Document {
+ Element? elementFromPoint(double x, double y);
+ sequence<Element> elementsFromPoint(double x, double y);
+ CaretPosition? caretPositionFromPoint(double x, double y);
+ readonly attribute Element? scrollingElement;
+};
+
+[Exposed=Window]
+interface CaretPosition {
+ readonly attribute Node offsetNode;
+ readonly attribute unsigned long offset;
+ [NewObject] DOMRect? getClientRect();
+};
+
+enum ScrollLogicalPosition { "start", "center", "end", "nearest" };
+dictionary ScrollIntoViewOptions : ScrollOptions {
+ ScrollLogicalPosition block = "start";
+ ScrollLogicalPosition inline = "nearest";
+};
+
+dictionary CheckVisibilityOptions {
+ boolean checkOpacity = false;
+ boolean checkVisibilityCSS = false;
+};
+
+partial interface Element {
+ DOMRectList getClientRects();
+ [NewObject] DOMRect getBoundingClientRect();
+
+ boolean checkVisibility(optional CheckVisibilityOptions options = {});
+
+ undefined scrollIntoView(optional (boolean or ScrollIntoViewOptions) arg = {});
+ undefined scroll(optional ScrollToOptions options = {});
+ undefined scroll(unrestricted double x, unrestricted double y);
+ undefined scrollTo(optional ScrollToOptions options = {});
+ undefined scrollTo(unrestricted double x, unrestricted double y);
+ undefined scrollBy(optional ScrollToOptions options = {});
+ undefined scrollBy(unrestricted double x, unrestricted double y);
+ attribute unrestricted double scrollTop;
+ attribute unrestricted double scrollLeft;
+ readonly attribute long scrollWidth;
+ readonly attribute long scrollHeight;
+ readonly attribute long clientTop;
+ readonly attribute long clientLeft;
+ readonly attribute long clientWidth;
+ readonly attribute long clientHeight;
+};
+
+partial interface HTMLElement {
+ readonly attribute Element? offsetParent;
+ readonly attribute long offsetTop;
+ readonly attribute long offsetLeft;
+ readonly attribute long offsetWidth;
+ readonly attribute long offsetHeight;
+};
+
+partial interface HTMLImageElement {
+ readonly attribute long x;
+ readonly attribute long y;
+};
+
+partial interface Range {
+ DOMRectList getClientRects();
+ [NewObject] DOMRect getBoundingClientRect();
+};
+
+partial interface MouseEvent {
+ readonly attribute double pageX;
+ readonly attribute double pageY;
+ readonly attribute double x;
+ readonly attribute double y;
+ readonly attribute double offsetX;
+ readonly attribute double offsetY;
+};
+
+enum CSSBoxType { "margin", "border", "padding", "content" };
+dictionary BoxQuadOptions {
+ CSSBoxType box = "border";
+ GeometryNode relativeTo; // XXX default document (i.e. viewport)
+};
+
+dictionary ConvertCoordinateOptions {
+ CSSBoxType fromBox = "border";
+ CSSBoxType toBox = "border";
+};
+
+interface mixin GeometryUtils {
+ sequence<DOMQuad> getBoxQuads(optional BoxQuadOptions options = {});
+ DOMQuad convertQuadFromNode(DOMQuadInit quad, GeometryNode from, optional ConvertCoordinateOptions options = {});
+ DOMQuad convertRectFromNode(DOMRectReadOnly rect, GeometryNode from, optional ConvertCoordinateOptions options = {});
+ DOMPoint convertPointFromNode(DOMPointInit point, GeometryNode from, optional ConvertCoordinateOptions options = {}); // XXX z,w turns into 0
+};
+
+Text includes GeometryUtils; // like Range
+Element includes GeometryUtils;
+CSSPseudoElement includes GeometryUtils;
+Document includes GeometryUtils;
+
+typedef (Text or Element or CSSPseudoElement or Document) GeometryNode;
+
+[Exposed=Window]
+interface VisualViewport : EventTarget {
+ readonly attribute double offsetLeft;
+ readonly attribute double offsetTop;
+
+ readonly attribute double pageLeft;
+ readonly attribute double pageTop;
+
+ readonly attribute double width;
+ readonly attribute double height;
+
+ readonly attribute double scale;
+
+ attribute EventHandler onresize;
+ attribute EventHandler onscroll;
+ attribute EventHandler onscrollend;
+};
diff --git a/test/wpt/tests/interfaces/cssom.idl b/test/wpt/tests/interfaces/cssom.idl
new file mode 100644
index 0000000..0574f1a
--- /dev/null
+++ b/test/wpt/tests/interfaces/cssom.idl
@@ -0,0 +1,169 @@
+// GENERATED PREAMBLE - DO NOT EDIT
+// CSSOMString is an implementation-defined type of either DOMString or
+// USVString in CSSOM: https://drafts.csswg.org/cssom/#cssomstring-type
+// For web-platform-tests, use DOMString because USVString has additional
+// requirements in type conversion and could result in spurious failures for
+// implementations that use DOMString.
+typedef DOMString CSSOMString;
+
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: CSS Object Model (CSSOM) (https://drafts.csswg.org/cssom-1/)
+
+[Exposed=Window]
+interface MediaList {
+ stringifier attribute [LegacyNullToEmptyString] CSSOMString mediaText;
+ readonly attribute unsigned long length;
+ getter CSSOMString? item(unsigned long index);
+ undefined appendMedium(CSSOMString medium);
+ undefined deleteMedium(CSSOMString medium);
+};
+
+[Exposed=Window]
+interface StyleSheet {
+ readonly attribute CSSOMString type;
+ readonly attribute USVString? href;
+ readonly attribute (Element or ProcessingInstruction)? ownerNode;
+ readonly attribute CSSStyleSheet? parentStyleSheet;
+ readonly attribute DOMString? title;
+ [SameObject, PutForwards=mediaText] readonly attribute MediaList media;
+ attribute boolean disabled;
+};
+
+[Exposed=Window]
+interface CSSStyleSheet : StyleSheet {
+ constructor(optional CSSStyleSheetInit options = {});
+
+ readonly attribute CSSRule? ownerRule;
+ [SameObject] readonly attribute CSSRuleList cssRules;
+ unsigned long insertRule(CSSOMString rule, optional unsigned long index = 0);
+ undefined deleteRule(unsigned long index);
+
+ Promise<CSSStyleSheet> replace(USVString text);
+ undefined replaceSync(USVString text);
+};
+
+dictionary CSSStyleSheetInit {
+ DOMString baseURL = null;
+ (MediaList or DOMString) media = "";
+ boolean disabled = false;
+};
+
+partial interface CSSStyleSheet {
+ [SameObject] readonly attribute CSSRuleList rules;
+ long addRule(optional DOMString selector = "undefined", optional DOMString style = "undefined", optional unsigned long index);
+ undefined removeRule(optional unsigned long index = 0);
+};
+
+[Exposed=Window]
+interface StyleSheetList {
+ getter CSSStyleSheet? item(unsigned long index);
+ readonly attribute unsigned long length;
+};
+
+partial interface mixin DocumentOrShadowRoot {
+ [SameObject] readonly attribute StyleSheetList styleSheets;
+ attribute ObservableArray<CSSStyleSheet> adoptedStyleSheets;
+};
+
+interface mixin LinkStyle {
+ readonly attribute CSSStyleSheet? sheet;
+};
+
+ProcessingInstruction includes LinkStyle;
+[Exposed=Window]
+interface CSSRuleList {
+ getter CSSRule? item(unsigned long index);
+ readonly attribute unsigned long length;
+};
+
+[Exposed=Window]
+interface CSSRule {
+ attribute CSSOMString cssText;
+ readonly attribute CSSRule? parentRule;
+ readonly attribute CSSStyleSheet? parentStyleSheet;
+
+ // the following attribute and constants are historical
+ readonly attribute unsigned short type;
+ const unsigned short STYLE_RULE = 1;
+ const unsigned short CHARSET_RULE = 2;
+ const unsigned short IMPORT_RULE = 3;
+ const unsigned short MEDIA_RULE = 4;
+ const unsigned short FONT_FACE_RULE = 5;
+ const unsigned short PAGE_RULE = 6;
+ const unsigned short MARGIN_RULE = 9;
+ const unsigned short NAMESPACE_RULE = 10;
+};
+
+[Exposed=Window]
+interface CSSImportRule : CSSRule {
+ readonly attribute USVString href;
+ [SameObject, PutForwards=mediaText] readonly attribute MediaList media;
+ [SameObject] readonly attribute CSSStyleSheet? styleSheet;
+ readonly attribute CSSOMString? layerName;
+ readonly attribute CSSOMString? supportsText;
+};
+
+[Exposed=Window]
+interface CSSGroupingRule : CSSRule {
+ [SameObject] readonly attribute CSSRuleList cssRules;
+ unsigned long insertRule(CSSOMString rule, optional unsigned long index = 0);
+ undefined deleteRule(unsigned long index);
+};
+
+[Exposed=Window]
+interface CSSStyleRule : CSSGroupingRule {
+ attribute CSSOMString selectorText;
+ [SameObject, PutForwards=cssText] readonly attribute CSSStyleDeclaration style;
+};
+
+[Exposed=Window]
+interface CSSPageRule : CSSGroupingRule {
+ attribute CSSOMString selectorText;
+ [SameObject, PutForwards=cssText] readonly attribute CSSStyleDeclaration style;
+};
+
+[Exposed=Window]
+interface CSSMarginRule : CSSRule {
+ readonly attribute CSSOMString name;
+ [SameObject, PutForwards=cssText] readonly attribute CSSStyleDeclaration style;
+};
+
+[Exposed=Window]
+interface CSSNamespaceRule : CSSRule {
+ readonly attribute CSSOMString namespaceURI;
+ readonly attribute CSSOMString prefix;
+};
+
+[Exposed=Window]
+interface CSSStyleDeclaration {
+ [CEReactions] attribute CSSOMString cssText;
+ readonly attribute unsigned long length;
+ getter CSSOMString item(unsigned long index);
+ CSSOMString getPropertyValue(CSSOMString property);
+ CSSOMString getPropertyPriority(CSSOMString property);
+ [CEReactions] undefined setProperty(CSSOMString property, [LegacyNullToEmptyString] CSSOMString value, optional [LegacyNullToEmptyString] CSSOMString priority = "");
+ [CEReactions] CSSOMString removeProperty(CSSOMString property);
+ readonly attribute CSSRule? parentRule;
+ [CEReactions] attribute [LegacyNullToEmptyString] CSSOMString cssFloat;
+};
+
+interface mixin ElementCSSInlineStyle {
+ [SameObject, PutForwards=cssText] readonly attribute CSSStyleDeclaration style;
+};
+
+HTMLElement includes ElementCSSInlineStyle;
+
+SVGElement includes ElementCSSInlineStyle;
+
+MathMLElement includes ElementCSSInlineStyle;
+
+partial interface Window {
+ [NewObject] CSSStyleDeclaration getComputedStyle(Element elt, optional CSSOMString? pseudoElt);
+};
+
+[Exposed=Window]
+namespace CSS {
+ CSSOMString escape(CSSOMString ident);
+};
diff --git a/test/wpt/tests/interfaces/custom-state-pseudo-class.idl b/test/wpt/tests/interfaces/custom-state-pseudo-class.idl
new file mode 100644
index 0000000..342f1ed
--- /dev/null
+++ b/test/wpt/tests/interfaces/custom-state-pseudo-class.idl
@@ -0,0 +1,14 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Custom State Pseudo Class (https://wicg.github.io/custom-state-pseudo-class/)
+
+partial interface ElementInternals {
+ readonly attribute CustomStateSet states;
+};
+
+[Exposed=Window]
+interface CustomStateSet {
+ setlike<DOMString>;
+ undefined add(DOMString value);
+};
diff --git a/test/wpt/tests/interfaces/datacue.idl b/test/wpt/tests/interfaces/datacue.idl
new file mode 100644
index 0000000..f84d6e9
--- /dev/null
+++ b/test/wpt/tests/interfaces/datacue.idl
@@ -0,0 +1,12 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: DataCue API (https://wicg.github.io/datacue/)
+
+[Exposed=Window]
+interface DataCue : TextTrackCue {
+ constructor(double startTime, unrestricted double endTime,
+ any value, optional DOMString type);
+ attribute any value;
+ readonly attribute DOMString type;
+};
diff --git a/test/wpt/tests/interfaces/deprecation-reporting.idl b/test/wpt/tests/interfaces/deprecation-reporting.idl
new file mode 100644
index 0000000..4cf76ba
--- /dev/null
+++ b/test/wpt/tests/interfaces/deprecation-reporting.idl
@@ -0,0 +1,15 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Deprecation Reporting (https://wicg.github.io/deprecation-reporting/)
+
+[Exposed=(Window,Worker)]
+interface DeprecationReportBody : ReportBody {
+ [Default] object toJSON();
+ readonly attribute DOMString id;
+ readonly attribute object? anticipatedRemoval;
+ readonly attribute DOMString message;
+ readonly attribute DOMString? sourceFile;
+ readonly attribute unsigned long? lineNumber;
+ readonly attribute unsigned long? columnNumber;
+};
diff --git a/test/wpt/tests/interfaces/device-memory.idl b/test/wpt/tests/interfaces/device-memory.idl
new file mode 100644
index 0000000..e8197e8
--- /dev/null
+++ b/test/wpt/tests/interfaces/device-memory.idl
@@ -0,0 +1,14 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Device Memory (https://www.w3.org/TR/device-memory/)
+
+[
+ SecureContext,
+ Exposed=(Window,Worker)
+] interface mixin NavigatorDeviceMemory {
+ readonly attribute double deviceMemory;
+};
+
+Navigator includes NavigatorDeviceMemory;
+WorkerNavigator includes NavigatorDeviceMemory;
diff --git a/test/wpt/tests/interfaces/device-posture.idl b/test/wpt/tests/interfaces/device-posture.idl
new file mode 100644
index 0000000..0f1dded
--- /dev/null
+++ b/test/wpt/tests/interfaces/device-posture.idl
@@ -0,0 +1,20 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Device Posture API (https://w3c.github.io/device-posture/)
+
+[SecureContext, Exposed=(Window)]
+partial interface Navigator {
+ [SameObject] readonly attribute DevicePosture devicePosture;
+};
+
+[SecureContext, Exposed=(Window)]
+interface DevicePosture : EventTarget {
+ readonly attribute DevicePostureType type;
+ attribute EventHandler onchange;
+};
+
+enum DevicePostureType {
+ "continuous",
+ "folded"
+};
diff --git a/test/wpt/tests/interfaces/digital-goods.idl b/test/wpt/tests/interfaces/digital-goods.idl
new file mode 100644
index 0000000..38cedac
--- /dev/null
+++ b/test/wpt/tests/interfaces/digital-goods.idl
@@ -0,0 +1,44 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Digital Goods API (https://wicg.github.io/digital-goods/)
+
+partial interface Window {
+ [SecureContext] Promise<DigitalGoodsService> getDigitalGoodsService(
+ DOMString serviceProvider);
+};
+
+[Exposed=Window, SecureContext] interface DigitalGoodsService {
+
+ Promise<sequence<ItemDetails>> getDetails(sequence<DOMString> itemIds);
+
+ Promise<sequence<PurchaseDetails>> listPurchases();
+
+ Promise<sequence<PurchaseDetails>> listPurchaseHistory();
+
+ Promise<undefined> consume(DOMString purchaseToken);
+};
+
+dictionary ItemDetails {
+ required DOMString itemId;
+ required DOMString title;
+ required PaymentCurrencyAmount price;
+ ItemType type;
+ DOMString description;
+ sequence<DOMString> iconURLs;
+ DOMString subscriptionPeriod;
+ DOMString freeTrialPeriod;
+ PaymentCurrencyAmount introductoryPrice;
+ DOMString introductoryPricePeriod;
+ [EnforceRange] unsigned long long introductoryPriceCycles;
+};
+
+enum ItemType {
+ "product",
+ "subscription",
+};
+
+dictionary PurchaseDetails {
+ required DOMString itemId;
+ required DOMString purchaseToken;
+};
diff --git a/test/wpt/tests/interfaces/document-picture-in-picture.idl b/test/wpt/tests/interfaces/document-picture-in-picture.idl
new file mode 100644
index 0000000..742f65e
--- /dev/null
+++ b/test/wpt/tests/interfaces/document-picture-in-picture.idl
@@ -0,0 +1,34 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Document Picture-in-Picture Specification (https://wicg.github.io/document-picture-in-picture/)
+
+[Exposed=Window]
+partial interface Window {
+ [SameObject, SecureContext] readonly attribute DocumentPictureInPicture
+ documentPictureInPicture;
+};
+
+[Exposed=Window, SecureContext]
+interface DocumentPictureInPicture : EventTarget {
+ [NewObject] Promise<Window> requestWindow(
+ optional DocumentPictureInPictureOptions options = {});
+ readonly attribute Window window;
+ attribute EventHandler onenter;
+};
+
+dictionary DocumentPictureInPictureOptions {
+ [EnforceRange] unsigned long long width = 0;
+ [EnforceRange] unsigned long long height = 0;
+ boolean copyStyleSheets = false;
+};
+
+[Exposed=Window]
+interface DocumentPictureInPictureEvent : Event {
+ constructor(DOMString type, DocumentPictureInPictureEventInit eventInitDict);
+ [SameObject] readonly attribute Window window;
+};
+
+dictionary DocumentPictureInPictureEventInit : EventInit {
+ required Window window;
+};
diff --git a/test/wpt/tests/interfaces/dom.idl b/test/wpt/tests/interfaces/dom.idl
new file mode 100644
index 0000000..c2def87
--- /dev/null
+++ b/test/wpt/tests/interfaces/dom.idl
@@ -0,0 +1,646 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: DOM Standard (https://dom.spec.whatwg.org/)
+
+[Exposed=*]
+interface Event {
+ constructor(DOMString type, optional EventInit eventInitDict = {});
+
+ readonly attribute DOMString type;
+ readonly attribute EventTarget? target;
+ readonly attribute EventTarget? srcElement; // legacy
+ readonly attribute EventTarget? currentTarget;
+ sequence<EventTarget> composedPath();
+
+ const unsigned short NONE = 0;
+ const unsigned short CAPTURING_PHASE = 1;
+ const unsigned short AT_TARGET = 2;
+ const unsigned short BUBBLING_PHASE = 3;
+ readonly attribute unsigned short eventPhase;
+
+ undefined stopPropagation();
+ attribute boolean cancelBubble; // legacy alias of .stopPropagation()
+ undefined stopImmediatePropagation();
+
+ readonly attribute boolean bubbles;
+ readonly attribute boolean cancelable;
+ attribute boolean returnValue; // legacy
+ undefined preventDefault();
+ readonly attribute boolean defaultPrevented;
+ readonly attribute boolean composed;
+
+ [LegacyUnforgeable] readonly attribute boolean isTrusted;
+ readonly attribute DOMHighResTimeStamp timeStamp;
+
+ undefined initEvent(DOMString type, optional boolean bubbles = false, optional boolean cancelable = false); // legacy
+};
+
+dictionary EventInit {
+ boolean bubbles = false;
+ boolean cancelable = false;
+ boolean composed = false;
+};
+
+partial interface Window {
+ [Replaceable] readonly attribute (Event or undefined) event; // legacy
+};
+
+[Exposed=*]
+interface CustomEvent : Event {
+ constructor(DOMString type, optional CustomEventInit eventInitDict = {});
+
+ readonly attribute any detail;
+
+ undefined initCustomEvent(DOMString type, optional boolean bubbles = false, optional boolean cancelable = false, optional any detail = null); // legacy
+};
+
+dictionary CustomEventInit : EventInit {
+ any detail = null;
+};
+
+[Exposed=*]
+interface EventTarget {
+ constructor();
+
+ undefined addEventListener(DOMString type, EventListener? callback, optional (AddEventListenerOptions or boolean) options = {});
+ undefined removeEventListener(DOMString type, EventListener? callback, optional (EventListenerOptions or boolean) options = {});
+ boolean dispatchEvent(Event event);
+};
+
+callback interface EventListener {
+ undefined handleEvent(Event event);
+};
+
+dictionary EventListenerOptions {
+ boolean capture = false;
+};
+
+dictionary AddEventListenerOptions : EventListenerOptions {
+ boolean passive;
+ boolean once = false;
+ AbortSignal signal;
+};
+
+[Exposed=*]
+interface AbortController {
+ constructor();
+
+ [SameObject] readonly attribute AbortSignal signal;
+
+ undefined abort(optional any reason);
+};
+
+[Exposed=*]
+interface AbortSignal : EventTarget {
+ [NewObject] static AbortSignal abort(optional any reason);
+ [Exposed=(Window,Worker), NewObject] static AbortSignal timeout([EnforceRange] unsigned long long milliseconds);
+ [NewObject] static AbortSignal _any(sequence<AbortSignal> signals);
+
+ readonly attribute boolean aborted;
+ readonly attribute any reason;
+ undefined throwIfAborted();
+
+ attribute EventHandler onabort;
+};
+interface mixin NonElementParentNode {
+ Element? getElementById(DOMString elementId);
+};
+Document includes NonElementParentNode;
+DocumentFragment includes NonElementParentNode;
+
+interface mixin DocumentOrShadowRoot {
+};
+Document includes DocumentOrShadowRoot;
+ShadowRoot includes DocumentOrShadowRoot;
+
+interface mixin ParentNode {
+ [SameObject] readonly attribute HTMLCollection children;
+ readonly attribute Element? firstElementChild;
+ readonly attribute Element? lastElementChild;
+ readonly attribute unsigned long childElementCount;
+
+ [CEReactions, Unscopable] undefined prepend((Node or DOMString)... nodes);
+ [CEReactions, Unscopable] undefined append((Node or DOMString)... nodes);
+ [CEReactions, Unscopable] undefined replaceChildren((Node or DOMString)... nodes);
+
+ Element? querySelector(DOMString selectors);
+ [NewObject] NodeList querySelectorAll(DOMString selectors);
+};
+Document includes ParentNode;
+DocumentFragment includes ParentNode;
+Element includes ParentNode;
+
+interface mixin NonDocumentTypeChildNode {
+ readonly attribute Element? previousElementSibling;
+ readonly attribute Element? nextElementSibling;
+};
+Element includes NonDocumentTypeChildNode;
+CharacterData includes NonDocumentTypeChildNode;
+
+interface mixin ChildNode {
+ [CEReactions, Unscopable] undefined before((Node or DOMString)... nodes);
+ [CEReactions, Unscopable] undefined after((Node or DOMString)... nodes);
+ [CEReactions, Unscopable] undefined replaceWith((Node or DOMString)... nodes);
+ [CEReactions, Unscopable] undefined remove();
+};
+DocumentType includes ChildNode;
+Element includes ChildNode;
+CharacterData includes ChildNode;
+
+interface mixin Slottable {
+ readonly attribute HTMLSlotElement? assignedSlot;
+};
+Element includes Slottable;
+Text includes Slottable;
+
+[Exposed=Window]
+interface NodeList {
+ getter Node? item(unsigned long index);
+ readonly attribute unsigned long length;
+ iterable<Node>;
+};
+
+[Exposed=Window, LegacyUnenumerableNamedProperties]
+interface HTMLCollection {
+ readonly attribute unsigned long length;
+ getter Element? item(unsigned long index);
+ getter Element? namedItem(DOMString name);
+};
+
+[Exposed=Window]
+interface MutationObserver {
+ constructor(MutationCallback callback);
+
+ undefined observe(Node target, optional MutationObserverInit options = {});
+ undefined disconnect();
+ sequence<MutationRecord> takeRecords();
+};
+
+callback MutationCallback = undefined (sequence<MutationRecord> mutations, MutationObserver observer);
+
+dictionary MutationObserverInit {
+ boolean childList = false;
+ boolean attributes;
+ boolean characterData;
+ boolean subtree = false;
+ boolean attributeOldValue;
+ boolean characterDataOldValue;
+ sequence<DOMString> attributeFilter;
+};
+
+[Exposed=Window]
+interface MutationRecord {
+ readonly attribute DOMString type;
+ [SameObject] readonly attribute Node target;
+ [SameObject] readonly attribute NodeList addedNodes;
+ [SameObject] readonly attribute NodeList removedNodes;
+ readonly attribute Node? previousSibling;
+ readonly attribute Node? nextSibling;
+ readonly attribute DOMString? attributeName;
+ readonly attribute DOMString? attributeNamespace;
+ readonly attribute DOMString? oldValue;
+};
+
+[Exposed=Window]
+interface Node : EventTarget {
+ const unsigned short ELEMENT_NODE = 1;
+ const unsigned short ATTRIBUTE_NODE = 2;
+ const unsigned short TEXT_NODE = 3;
+ const unsigned short CDATA_SECTION_NODE = 4;
+ const unsigned short ENTITY_REFERENCE_NODE = 5; // legacy
+ const unsigned short ENTITY_NODE = 6; // legacy
+ const unsigned short PROCESSING_INSTRUCTION_NODE = 7;
+ const unsigned short COMMENT_NODE = 8;
+ const unsigned short DOCUMENT_NODE = 9;
+ const unsigned short DOCUMENT_TYPE_NODE = 10;
+ const unsigned short DOCUMENT_FRAGMENT_NODE = 11;
+ const unsigned short NOTATION_NODE = 12; // legacy
+ readonly attribute unsigned short nodeType;
+ readonly attribute DOMString nodeName;
+
+ readonly attribute USVString baseURI;
+
+ readonly attribute boolean isConnected;
+ readonly attribute Document? ownerDocument;
+ Node getRootNode(optional GetRootNodeOptions options = {});
+ readonly attribute Node? parentNode;
+ readonly attribute Element? parentElement;
+ boolean hasChildNodes();
+ [SameObject] readonly attribute NodeList childNodes;
+ readonly attribute Node? firstChild;
+ readonly attribute Node? lastChild;
+ readonly attribute Node? previousSibling;
+ readonly attribute Node? nextSibling;
+
+ [CEReactions] attribute DOMString? nodeValue;
+ [CEReactions] attribute DOMString? textContent;
+ [CEReactions] undefined normalize();
+
+ [CEReactions, NewObject] Node cloneNode(optional boolean deep = false);
+ boolean isEqualNode(Node? otherNode);
+ boolean isSameNode(Node? otherNode); // legacy alias of ===
+
+ const unsigned short DOCUMENT_POSITION_DISCONNECTED = 0x01;
+ const unsigned short DOCUMENT_POSITION_PRECEDING = 0x02;
+ const unsigned short DOCUMENT_POSITION_FOLLOWING = 0x04;
+ const unsigned short DOCUMENT_POSITION_CONTAINS = 0x08;
+ const unsigned short DOCUMENT_POSITION_CONTAINED_BY = 0x10;
+ const unsigned short DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC = 0x20;
+ unsigned short compareDocumentPosition(Node other);
+ boolean contains(Node? other);
+
+ DOMString? lookupPrefix(DOMString? namespace);
+ DOMString? lookupNamespaceURI(DOMString? prefix);
+ boolean isDefaultNamespace(DOMString? namespace);
+
+ [CEReactions] Node insertBefore(Node node, Node? child);
+ [CEReactions] Node appendChild(Node node);
+ [CEReactions] Node replaceChild(Node node, Node child);
+ [CEReactions] Node removeChild(Node child);
+};
+
+dictionary GetRootNodeOptions {
+ boolean composed = false;
+};
+
+[Exposed=Window]
+interface Document : Node {
+ constructor();
+
+ [SameObject] readonly attribute DOMImplementation implementation;
+ readonly attribute USVString URL;
+ readonly attribute USVString documentURI;
+ readonly attribute DOMString compatMode;
+ readonly attribute DOMString characterSet;
+ readonly attribute DOMString charset; // legacy alias of .characterSet
+ readonly attribute DOMString inputEncoding; // legacy alias of .characterSet
+ readonly attribute DOMString contentType;
+
+ readonly attribute DocumentType? doctype;
+ readonly attribute Element? documentElement;
+ HTMLCollection getElementsByTagName(DOMString qualifiedName);
+ HTMLCollection getElementsByTagNameNS(DOMString? namespace, DOMString localName);
+ HTMLCollection getElementsByClassName(DOMString classNames);
+
+ [CEReactions, NewObject] Element createElement(DOMString localName, optional (DOMString or ElementCreationOptions) options = {});
+ [CEReactions, NewObject] Element createElementNS(DOMString? namespace, DOMString qualifiedName, optional (DOMString or ElementCreationOptions) options = {});
+ [NewObject] DocumentFragment createDocumentFragment();
+ [NewObject] Text createTextNode(DOMString data);
+ [NewObject] CDATASection createCDATASection(DOMString data);
+ [NewObject] Comment createComment(DOMString data);
+ [NewObject] ProcessingInstruction createProcessingInstruction(DOMString target, DOMString data);
+
+ [CEReactions, NewObject] Node importNode(Node node, optional boolean deep = false);
+ [CEReactions] Node adoptNode(Node node);
+
+ [NewObject] Attr createAttribute(DOMString localName);
+ [NewObject] Attr createAttributeNS(DOMString? namespace, DOMString qualifiedName);
+
+ [NewObject] Event createEvent(DOMString interface); // legacy
+
+ [NewObject] Range createRange();
+
+ // NodeFilter.SHOW_ALL = 0xFFFFFFFF
+ [NewObject] NodeIterator createNodeIterator(Node root, optional unsigned long whatToShow = 0xFFFFFFFF, optional NodeFilter? filter = null);
+ [NewObject] TreeWalker createTreeWalker(Node root, optional unsigned long whatToShow = 0xFFFFFFFF, optional NodeFilter? filter = null);
+};
+
+[Exposed=Window]
+interface XMLDocument : Document {};
+
+dictionary ElementCreationOptions {
+ DOMString is;
+};
+
+[Exposed=Window]
+interface DOMImplementation {
+ [NewObject] DocumentType createDocumentType(DOMString qualifiedName, DOMString publicId, DOMString systemId);
+ [NewObject] XMLDocument createDocument(DOMString? namespace, [LegacyNullToEmptyString] DOMString qualifiedName, optional DocumentType? doctype = null);
+ [NewObject] Document createHTMLDocument(optional DOMString title);
+
+ boolean hasFeature(); // useless; always returns true
+};
+
+[Exposed=Window]
+interface DocumentType : Node {
+ readonly attribute DOMString name;
+ readonly attribute DOMString publicId;
+ readonly attribute DOMString systemId;
+};
+
+[Exposed=Window]
+interface DocumentFragment : Node {
+ constructor();
+};
+
+[Exposed=Window]
+interface ShadowRoot : DocumentFragment {
+ readonly attribute ShadowRootMode mode;
+ readonly attribute boolean delegatesFocus;
+ readonly attribute SlotAssignmentMode slotAssignment;
+ readonly attribute Element host;
+ attribute EventHandler onslotchange;
+};
+
+enum ShadowRootMode { "open", "closed" };
+enum SlotAssignmentMode { "manual", "named" };
+
+[Exposed=Window]
+interface Element : Node {
+ readonly attribute DOMString? namespaceURI;
+ readonly attribute DOMString? prefix;
+ readonly attribute DOMString localName;
+ readonly attribute DOMString tagName;
+
+ [CEReactions] attribute DOMString id;
+ [CEReactions] attribute DOMString className;
+ [SameObject, PutForwards=value] readonly attribute DOMTokenList classList;
+ [CEReactions, Unscopable] attribute DOMString slot;
+
+ boolean hasAttributes();
+ [SameObject] readonly attribute NamedNodeMap attributes;
+ sequence<DOMString> getAttributeNames();
+ DOMString? getAttribute(DOMString qualifiedName);
+ DOMString? getAttributeNS(DOMString? namespace, DOMString localName);
+ [CEReactions] undefined setAttribute(DOMString qualifiedName, DOMString value);
+ [CEReactions] undefined setAttributeNS(DOMString? namespace, DOMString qualifiedName, DOMString value);
+ [CEReactions] undefined removeAttribute(DOMString qualifiedName);
+ [CEReactions] undefined removeAttributeNS(DOMString? namespace, DOMString localName);
+ [CEReactions] boolean toggleAttribute(DOMString qualifiedName, optional boolean force);
+ boolean hasAttribute(DOMString qualifiedName);
+ boolean hasAttributeNS(DOMString? namespace, DOMString localName);
+
+ Attr? getAttributeNode(DOMString qualifiedName);
+ Attr? getAttributeNodeNS(DOMString? namespace, DOMString localName);
+ [CEReactions] Attr? setAttributeNode(Attr attr);
+ [CEReactions] Attr? setAttributeNodeNS(Attr attr);
+ [CEReactions] Attr removeAttributeNode(Attr attr);
+
+ ShadowRoot attachShadow(ShadowRootInit init);
+ readonly attribute ShadowRoot? shadowRoot;
+
+ Element? closest(DOMString selectors);
+ boolean matches(DOMString selectors);
+ boolean webkitMatchesSelector(DOMString selectors); // legacy alias of .matches
+
+ HTMLCollection getElementsByTagName(DOMString qualifiedName);
+ HTMLCollection getElementsByTagNameNS(DOMString? namespace, DOMString localName);
+ HTMLCollection getElementsByClassName(DOMString classNames);
+
+ [CEReactions] Element? insertAdjacentElement(DOMString where, Element element); // legacy
+ undefined insertAdjacentText(DOMString where, DOMString data); // legacy
+};
+
+dictionary ShadowRootInit {
+ required ShadowRootMode mode;
+ boolean delegatesFocus = false;
+ SlotAssignmentMode slotAssignment = "named";
+};
+
+[Exposed=Window,
+ LegacyUnenumerableNamedProperties]
+interface NamedNodeMap {
+ readonly attribute unsigned long length;
+ getter Attr? item(unsigned long index);
+ getter Attr? getNamedItem(DOMString qualifiedName);
+ Attr? getNamedItemNS(DOMString? namespace, DOMString localName);
+ [CEReactions] Attr? setNamedItem(Attr attr);
+ [CEReactions] Attr? setNamedItemNS(Attr attr);
+ [CEReactions] Attr removeNamedItem(DOMString qualifiedName);
+ [CEReactions] Attr removeNamedItemNS(DOMString? namespace, DOMString localName);
+};
+
+[Exposed=Window]
+interface Attr : Node {
+ readonly attribute DOMString? namespaceURI;
+ readonly attribute DOMString? prefix;
+ readonly attribute DOMString localName;
+ readonly attribute DOMString name;
+ [CEReactions] attribute DOMString value;
+
+ readonly attribute Element? ownerElement;
+
+ readonly attribute boolean specified; // useless; always returns true
+};
+[Exposed=Window]
+interface CharacterData : Node {
+ attribute [LegacyNullToEmptyString] DOMString data;
+ readonly attribute unsigned long length;
+ DOMString substringData(unsigned long offset, unsigned long count);
+ undefined appendData(DOMString data);
+ undefined insertData(unsigned long offset, DOMString data);
+ undefined deleteData(unsigned long offset, unsigned long count);
+ undefined replaceData(unsigned long offset, unsigned long count, DOMString data);
+};
+
+[Exposed=Window]
+interface Text : CharacterData {
+ constructor(optional DOMString data = "");
+
+ [NewObject] Text splitText(unsigned long offset);
+ readonly attribute DOMString wholeText;
+};
+
+[Exposed=Window]
+interface CDATASection : Text {
+};
+[Exposed=Window]
+interface ProcessingInstruction : CharacterData {
+ readonly attribute DOMString target;
+};
+[Exposed=Window]
+interface Comment : CharacterData {
+ constructor(optional DOMString data = "");
+};
+
+[Exposed=Window]
+interface AbstractRange {
+ readonly attribute Node startContainer;
+ readonly attribute unsigned long startOffset;
+ readonly attribute Node endContainer;
+ readonly attribute unsigned long endOffset;
+ readonly attribute boolean collapsed;
+};
+
+dictionary StaticRangeInit {
+ required Node startContainer;
+ required unsigned long startOffset;
+ required Node endContainer;
+ required unsigned long endOffset;
+};
+
+[Exposed=Window]
+interface StaticRange : AbstractRange {
+ constructor(StaticRangeInit init);
+};
+
+[Exposed=Window]
+interface Range : AbstractRange {
+ constructor();
+
+ readonly attribute Node commonAncestorContainer;
+
+ undefined setStart(Node node, unsigned long offset);
+ undefined setEnd(Node node, unsigned long offset);
+ undefined setStartBefore(Node node);
+ undefined setStartAfter(Node node);
+ undefined setEndBefore(Node node);
+ undefined setEndAfter(Node node);
+ undefined collapse(optional boolean toStart = false);
+ undefined selectNode(Node node);
+ undefined selectNodeContents(Node node);
+
+ const unsigned short START_TO_START = 0;
+ const unsigned short START_TO_END = 1;
+ const unsigned short END_TO_END = 2;
+ const unsigned short END_TO_START = 3;
+ short compareBoundaryPoints(unsigned short how, Range sourceRange);
+
+ [CEReactions] undefined deleteContents();
+ [CEReactions, NewObject] DocumentFragment extractContents();
+ [CEReactions, NewObject] DocumentFragment cloneContents();
+ [CEReactions] undefined insertNode(Node node);
+ [CEReactions] undefined surroundContents(Node newParent);
+
+ [NewObject] Range cloneRange();
+ undefined detach();
+
+ boolean isPointInRange(Node node, unsigned long offset);
+ short comparePoint(Node node, unsigned long offset);
+
+ boolean intersectsNode(Node node);
+
+ stringifier;
+};
+
+[Exposed=Window]
+interface NodeIterator {
+ [SameObject] readonly attribute Node root;
+ readonly attribute Node referenceNode;
+ readonly attribute boolean pointerBeforeReferenceNode;
+ readonly attribute unsigned long whatToShow;
+ readonly attribute NodeFilter? filter;
+
+ Node? nextNode();
+ Node? previousNode();
+
+ undefined detach();
+};
+
+[Exposed=Window]
+interface TreeWalker {
+ [SameObject] readonly attribute Node root;
+ readonly attribute unsigned long whatToShow;
+ readonly attribute NodeFilter? filter;
+ attribute Node currentNode;
+
+ Node? parentNode();
+ Node? firstChild();
+ Node? lastChild();
+ Node? previousSibling();
+ Node? nextSibling();
+ Node? previousNode();
+ Node? nextNode();
+};
+[Exposed=Window]
+callback interface NodeFilter {
+ // Constants for acceptNode()
+ const unsigned short FILTER_ACCEPT = 1;
+ const unsigned short FILTER_REJECT = 2;
+ const unsigned short FILTER_SKIP = 3;
+
+ // Constants for whatToShow
+ const unsigned long SHOW_ALL = 0xFFFFFFFF;
+ const unsigned long SHOW_ELEMENT = 0x1;
+ const unsigned long SHOW_ATTRIBUTE = 0x2;
+ const unsigned long SHOW_TEXT = 0x4;
+ const unsigned long SHOW_CDATA_SECTION = 0x8;
+ const unsigned long SHOW_ENTITY_REFERENCE = 0x10; // legacy
+ const unsigned long SHOW_ENTITY = 0x20; // legacy
+ const unsigned long SHOW_PROCESSING_INSTRUCTION = 0x40;
+ const unsigned long SHOW_COMMENT = 0x80;
+ const unsigned long SHOW_DOCUMENT = 0x100;
+ const unsigned long SHOW_DOCUMENT_TYPE = 0x200;
+ const unsigned long SHOW_DOCUMENT_FRAGMENT = 0x400;
+ const unsigned long SHOW_NOTATION = 0x800; // legacy
+
+ unsigned short acceptNode(Node node);
+};
+
+[Exposed=Window]
+interface DOMTokenList {
+ readonly attribute unsigned long length;
+ getter DOMString? item(unsigned long index);
+ boolean contains(DOMString token);
+ [CEReactions] undefined add(DOMString... tokens);
+ [CEReactions] undefined remove(DOMString... tokens);
+ [CEReactions] boolean toggle(DOMString token, optional boolean force);
+ [CEReactions] boolean replace(DOMString token, DOMString newToken);
+ boolean supports(DOMString token);
+ [CEReactions] stringifier attribute DOMString value;
+ iterable<DOMString>;
+};
+
+[Exposed=Window]
+interface XPathResult {
+ const unsigned short ANY_TYPE = 0;
+ const unsigned short NUMBER_TYPE = 1;
+ const unsigned short STRING_TYPE = 2;
+ const unsigned short BOOLEAN_TYPE = 3;
+ const unsigned short UNORDERED_NODE_ITERATOR_TYPE = 4;
+ const unsigned short ORDERED_NODE_ITERATOR_TYPE = 5;
+ const unsigned short UNORDERED_NODE_SNAPSHOT_TYPE = 6;
+ const unsigned short ORDERED_NODE_SNAPSHOT_TYPE = 7;
+ const unsigned short ANY_UNORDERED_NODE_TYPE = 8;
+ const unsigned short FIRST_ORDERED_NODE_TYPE = 9;
+
+ readonly attribute unsigned short resultType;
+ readonly attribute unrestricted double numberValue;
+ readonly attribute DOMString stringValue;
+ readonly attribute boolean booleanValue;
+ readonly attribute Node? singleNodeValue;
+ readonly attribute boolean invalidIteratorState;
+ readonly attribute unsigned long snapshotLength;
+
+ Node? iterateNext();
+ Node? snapshotItem(unsigned long index);
+};
+
+[Exposed=Window]
+interface XPathExpression {
+ // XPathResult.ANY_TYPE = 0
+ XPathResult evaluate(Node contextNode, optional unsigned short type = 0, optional XPathResult? result = null);
+};
+
+callback interface XPathNSResolver {
+ DOMString? lookupNamespaceURI(DOMString? prefix);
+};
+
+interface mixin XPathEvaluatorBase {
+ [NewObject] XPathExpression createExpression(DOMString expression, optional XPathNSResolver? resolver = null);
+ Node createNSResolver(Node nodeResolver); // legacy
+ // XPathResult.ANY_TYPE = 0
+ XPathResult evaluate(DOMString expression, Node contextNode, optional XPathNSResolver? resolver = null, optional unsigned short type = 0, optional XPathResult? result = null);
+};
+Document includes XPathEvaluatorBase;
+
+[Exposed=Window]
+interface XPathEvaluator {
+ constructor();
+};
+
+XPathEvaluator includes XPathEvaluatorBase;
+
+[Exposed=Window]
+interface XSLTProcessor {
+ constructor();
+ undefined importStylesheet(Node style);
+ [CEReactions] DocumentFragment transformToFragment(Node source, Document output);
+ [CEReactions] Document transformToDocument(Node source);
+ undefined setParameter([LegacyNullToEmptyString] DOMString namespaceURI, DOMString localName, any value);
+ any getParameter([LegacyNullToEmptyString] DOMString namespaceURI, DOMString localName);
+ undefined removeParameter([LegacyNullToEmptyString] DOMString namespaceURI, DOMString localName);
+ undefined clearParameters();
+ undefined reset();
+};
diff --git a/test/wpt/tests/interfaces/edit-context.idl b/test/wpt/tests/interfaces/edit-context.idl
new file mode 100644
index 0000000..91d8af2
--- /dev/null
+++ b/test/wpt/tests/interfaces/edit-context.idl
@@ -0,0 +1,111 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: EditContext API (https://w3c.github.io/edit-context/)
+
+partial interface HTMLElement {
+ attribute EditContext? editContext;
+};
+
+dictionary EditContextInit {
+ DOMString text;
+ unsigned long selectionStart;
+ unsigned long selectionEnd;
+};
+
+[Exposed=Window]
+interface EditContext : EventTarget {
+ constructor(optional EditContextInit options = {});
+
+ undefined updateText(unsigned long rangeStart, unsigned long rangeEnd,
+ DOMString text);
+ undefined updateSelection(unsigned long start, unsigned long end);
+ undefined updateControlBounds(DOMRect controlBounds);
+ undefined updateSelectionBounds(DOMRect selectionBounds);
+ undefined updateCharacterBounds(unsigned long rangeStart, sequence<DOMRect> characterBounds);
+
+ sequence<Element> attachedElements();
+
+ readonly attribute DOMString text;
+ readonly attribute unsigned long selectionStart;
+ readonly attribute unsigned long selectionEnd;
+ readonly attribute unsigned long compositionRangeStart;
+ readonly attribute unsigned long compositionRangeEnd;
+ readonly attribute boolean isComposing;
+ readonly attribute DOMRect controlBounds;
+ readonly attribute DOMRect selectionBounds;
+ readonly attribute unsigned long characterBoundsRangeStart;
+ sequence<DOMRect> characterBounds();
+
+ attribute EventHandler ontextupdate;
+ attribute EventHandler ontextformatupdate;
+ attribute EventHandler oncharacterboundsupdate;
+ attribute EventHandler oncompositionstart;
+ attribute EventHandler oncompositionend;
+};
+
+dictionary TextUpdateEventInit : EventInit {
+ unsigned long updateRangeStart;
+ unsigned long updateRangeEnd;
+ DOMString text;
+ unsigned long selectionStart;
+ unsigned long selectionEnd;
+ unsigned long compositionStart;
+ unsigned long compositionEnd;
+};
+
+[Exposed=Window]
+interface TextUpdateEvent : Event {
+ constructor(DOMString type, optional TextUpdateEventInit options = {});
+ readonly attribute unsigned long updateRangeStart;
+ readonly attribute unsigned long updateRangeEnd;
+ readonly attribute DOMString text;
+ readonly attribute unsigned long selectionStart;
+ readonly attribute unsigned long selectionEnd;
+ readonly attribute unsigned long compositionStart;
+ readonly attribute unsigned long compositionEnd;
+};
+
+dictionary TextFormatInit {
+ unsigned long rangeStart;
+ unsigned long rangeEnd;
+ DOMString textColor;
+ DOMString backgroundColor;
+ DOMString underlineStyle;
+ DOMString underlineThickness;
+ DOMString underlineColor;
+};
+
+[Exposed=Window]
+interface TextFormat {
+ constructor(optional TextFormatInit options = {});
+ readonly attribute unsigned long rangeStart;
+ readonly attribute unsigned long rangeEnd;
+ readonly attribute DOMString textColor;
+ readonly attribute DOMString backgroundColor;
+ readonly attribute DOMString underlineStyle;
+ readonly attribute DOMString underlineThickness;
+ readonly attribute DOMString underlineColor;
+};
+
+dictionary TextFormatUpdateEventInit : EventInit {
+ sequence<TextFormat> textFormats;
+};
+
+[Exposed=Window]
+interface TextFormatUpdateEvent : Event {
+ constructor(DOMString type, optional TextFormatUpdateEventInit options = {});
+ sequence<TextFormat> getTextFormats();
+};
+
+dictionary CharacterBoundsUpdateEventInit : EventInit {
+ unsigned long rangeStart;
+ unsigned long rangeEnd;
+};
+
+[Exposed=Window]
+interface CharacterBoundsUpdateEvent : Event {
+ constructor(DOMString type, optional CharacterBoundsUpdateEventInit options = {});
+ readonly attribute unsigned long rangeStart;
+ readonly attribute unsigned long rangeEnd;
+};
diff --git a/test/wpt/tests/interfaces/element-timing.idl b/test/wpt/tests/interfaces/element-timing.idl
new file mode 100644
index 0000000..70ca384
--- /dev/null
+++ b/test/wpt/tests/interfaces/element-timing.idl
@@ -0,0 +1,22 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Element Timing API (https://wicg.github.io/element-timing/)
+
+[Exposed=Window]
+interface PerformanceElementTiming : PerformanceEntry {
+ readonly attribute DOMHighResTimeStamp renderTime;
+ readonly attribute DOMHighResTimeStamp loadTime;
+ readonly attribute DOMRectReadOnly intersectionRect;
+ readonly attribute DOMString identifier;
+ readonly attribute unsigned long naturalWidth;
+ readonly attribute unsigned long naturalHeight;
+ readonly attribute DOMString id;
+ readonly attribute Element? element;
+ readonly attribute DOMString url;
+ [Default] object toJSON();
+};
+
+partial interface Element {
+ [CEReactions] attribute DOMString elementTiming;
+};
diff --git a/test/wpt/tests/interfaces/encoding.idl b/test/wpt/tests/interfaces/encoding.idl
new file mode 100644
index 0000000..a8cbe44
--- /dev/null
+++ b/test/wpt/tests/interfaces/encoding.idl
@@ -0,0 +1,59 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Encoding Standard (https://encoding.spec.whatwg.org/)
+
+interface mixin TextDecoderCommon {
+ readonly attribute DOMString encoding;
+ readonly attribute boolean fatal;
+ readonly attribute boolean ignoreBOM;
+};
+
+dictionary TextDecoderOptions {
+ boolean fatal = false;
+ boolean ignoreBOM = false;
+};
+
+dictionary TextDecodeOptions {
+ boolean stream = false;
+};
+
+[Exposed=*]
+interface TextDecoder {
+ constructor(optional DOMString label = "utf-8", optional TextDecoderOptions options = {});
+
+ USVString decode(optional [AllowShared] BufferSource input, optional TextDecodeOptions options = {});
+};
+TextDecoder includes TextDecoderCommon;
+
+interface mixin TextEncoderCommon {
+ readonly attribute DOMString encoding;
+};
+
+dictionary TextEncoderEncodeIntoResult {
+ unsigned long long read;
+ unsigned long long written;
+};
+
+[Exposed=*]
+interface TextEncoder {
+ constructor();
+
+ [NewObject] Uint8Array encode(optional USVString input = "");
+ TextEncoderEncodeIntoResult encodeInto(USVString source, [AllowShared] Uint8Array destination);
+};
+TextEncoder includes TextEncoderCommon;
+
+[Exposed=*]
+interface TextDecoderStream {
+ constructor(optional DOMString label = "utf-8", optional TextDecoderOptions options = {});
+};
+TextDecoderStream includes TextDecoderCommon;
+TextDecoderStream includes GenericTransformStream;
+
+[Exposed=*]
+interface TextEncoderStream {
+ constructor();
+};
+TextEncoderStream includes TextEncoderCommon;
+TextEncoderStream includes GenericTransformStream;
diff --git a/test/wpt/tests/interfaces/encrypted-media.idl b/test/wpt/tests/interfaces/encrypted-media.idl
new file mode 100644
index 0000000..24db48e
--- /dev/null
+++ b/test/wpt/tests/interfaces/encrypted-media.idl
@@ -0,0 +1,125 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Encrypted Media Extensions (https://w3c.github.io/encrypted-media/)
+
+[Exposed=Window]
+partial interface Navigator {
+ [SecureContext] Promise<MediaKeySystemAccess> requestMediaKeySystemAccess (DOMString keySystem, sequence<MediaKeySystemConfiguration> supportedConfigurations);
+};
+
+enum MediaKeysRequirement {
+ "required",
+ "optional",
+ "not-allowed"
+};
+
+dictionary MediaKeySystemConfiguration {
+ DOMString label = "";
+ sequence<DOMString> initDataTypes = [];
+ sequence<MediaKeySystemMediaCapability> audioCapabilities = [];
+ sequence<MediaKeySystemMediaCapability> videoCapabilities = [];
+ MediaKeysRequirement distinctiveIdentifier = "optional";
+ MediaKeysRequirement persistentState = "optional";
+ sequence<DOMString> sessionTypes;
+};
+
+dictionary MediaKeySystemMediaCapability {
+ DOMString contentType = "";
+ DOMString? encryptionScheme = null;
+ DOMString robustness = "";
+};
+
+[Exposed=Window, SecureContext] interface MediaKeySystemAccess {
+ readonly attribute DOMString keySystem;
+ MediaKeySystemConfiguration getConfiguration ();
+ Promise<MediaKeys> createMediaKeys ();
+};
+
+enum MediaKeySessionType {
+ "temporary",
+ "persistent-license"
+};
+
+[Exposed=Window, SecureContext] interface MediaKeys {
+ MediaKeySession createSession (optional MediaKeySessionType sessionType = "temporary");
+ Promise<boolean> setServerCertificate (BufferSource serverCertificate);
+};
+
+enum MediaKeySessionClosedReason {
+ "internal-error",
+ "closed-by-application",
+ "release-acknowledged",
+ "hardware-context-reset",
+ "resource-evicted"
+};
+
+[Exposed=Window, SecureContext] interface MediaKeySession : EventTarget {
+ readonly attribute DOMString sessionId;
+ readonly attribute unrestricted double expiration;
+ readonly attribute Promise<MediaKeySessionClosedReason> closed;
+ readonly attribute MediaKeyStatusMap keyStatuses;
+ attribute EventHandler onkeystatuseschange;
+ attribute EventHandler onmessage;
+ Promise<undefined> generateRequest (DOMString initDataType, BufferSource initData);
+ Promise<boolean> load (DOMString sessionId);
+ Promise<undefined> update (BufferSource response);
+ Promise<undefined> close ();
+ Promise<undefined> remove ();
+};
+
+[Exposed=Window, SecureContext] interface MediaKeyStatusMap {
+ iterable<BufferSource,MediaKeyStatus>;
+ readonly attribute unsigned long size;
+ boolean has (BufferSource keyId);
+ (MediaKeyStatus or undefined) get (BufferSource keyId);
+};
+
+enum MediaKeyStatus {
+ "usable",
+ "expired",
+ "released",
+ "output-restricted",
+ "output-downscaled",
+ "usable-in-future",
+ "status-pending",
+ "internal-error"
+};
+
+enum MediaKeyMessageType {
+ "license-request",
+ "license-renewal",
+ "license-release",
+ "individualization-request"
+};
+
+[Exposed=Window, SecureContext]
+interface MediaKeyMessageEvent : Event {
+ constructor(DOMString type, MediaKeyMessageEventInit eventInitDict);
+ readonly attribute MediaKeyMessageType messageType;
+ readonly attribute ArrayBuffer message;
+};
+
+dictionary MediaKeyMessageEventInit : EventInit {
+ required MediaKeyMessageType messageType;
+ required ArrayBuffer message;
+};
+
+[Exposed=Window] partial interface HTMLMediaElement {
+ [SecureContext] readonly attribute MediaKeys? mediaKeys;
+ attribute EventHandler onencrypted;
+ attribute EventHandler onwaitingforkey;
+ [SecureContext] Promise<undefined> setMediaKeys (MediaKeys? mediaKeys);
+};
+
+[Exposed=Window]
+interface MediaEncryptedEvent : Event {
+ constructor(DOMString type, optional MediaEncryptedEventInit eventInitDict = {});
+ readonly attribute DOMString initDataType;
+ readonly attribute ArrayBuffer? initData;
+};
+
+dictionary MediaEncryptedEventInit : EventInit {
+ DOMString initDataType = "";
+ ArrayBuffer? initData = null;
+};
diff --git a/test/wpt/tests/interfaces/entries-api.idl b/test/wpt/tests/interfaces/entries-api.idl
new file mode 100644
index 0000000..cd536bc
--- /dev/null
+++ b/test/wpt/tests/interfaces/entries-api.idl
@@ -0,0 +1,71 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: File and Directory Entries API (https://wicg.github.io/entries-api/)
+
+partial interface File {
+ readonly attribute USVString webkitRelativePath;
+};
+
+partial interface HTMLInputElement {
+ attribute boolean webkitdirectory;
+ readonly attribute FrozenArray<FileSystemEntry> webkitEntries;
+};
+
+partial interface DataTransferItem {
+ FileSystemEntry? webkitGetAsEntry();
+};
+
+callback ErrorCallback = undefined (DOMException err);
+
+[Exposed=Window]
+interface FileSystemEntry {
+ readonly attribute boolean isFile;
+ readonly attribute boolean isDirectory;
+ readonly attribute USVString name;
+ readonly attribute USVString fullPath;
+ readonly attribute FileSystem filesystem;
+
+ undefined getParent(optional FileSystemEntryCallback successCallback,
+ optional ErrorCallback errorCallback);
+};
+
+[Exposed=Window]
+interface FileSystemDirectoryEntry : FileSystemEntry {
+ FileSystemDirectoryReader createReader();
+ undefined getFile(optional USVString? path,
+ optional FileSystemFlags options = {},
+ optional FileSystemEntryCallback successCallback,
+ optional ErrorCallback errorCallback);
+ undefined getDirectory(optional USVString? path,
+ optional FileSystemFlags options = {},
+ optional FileSystemEntryCallback successCallback,
+ optional ErrorCallback errorCallback);
+};
+
+dictionary FileSystemFlags {
+ boolean create = false;
+ boolean exclusive = false;
+};
+
+callback FileSystemEntryCallback = undefined (FileSystemEntry entry);
+
+[Exposed=Window]
+interface FileSystemDirectoryReader {
+ undefined readEntries(FileSystemEntriesCallback successCallback,
+ optional ErrorCallback errorCallback);
+};
+callback FileSystemEntriesCallback = undefined (sequence<FileSystemEntry> entries);
+
+[Exposed=Window]
+interface FileSystemFileEntry : FileSystemEntry {
+ undefined file(FileCallback successCallback,
+ optional ErrorCallback errorCallback);
+};
+callback FileCallback = undefined (File file);
+
+[Exposed=Window]
+interface FileSystem {
+ readonly attribute USVString name;
+ readonly attribute FileSystemDirectoryEntry root;
+};
diff --git a/test/wpt/tests/interfaces/event-timing.idl b/test/wpt/tests/interfaces/event-timing.idl
new file mode 100644
index 0000000..741a05d
--- /dev/null
+++ b/test/wpt/tests/interfaces/event-timing.idl
@@ -0,0 +1,29 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Event Timing API (https://w3c.github.io/event-timing)
+
+[Exposed=Window]
+interface PerformanceEventTiming : PerformanceEntry {
+ readonly attribute DOMHighResTimeStamp processingStart;
+ readonly attribute DOMHighResTimeStamp processingEnd;
+ readonly attribute boolean cancelable;
+ readonly attribute Node? target;
+ readonly attribute unsigned long long interactionId;
+ [Default] object toJSON();
+};
+
+[Exposed=Window]
+interface EventCounts {
+ readonly maplike<DOMString, unsigned long long>;
+};
+
+[Exposed=Window]
+partial interface Performance {
+ [SameObject] readonly attribute EventCounts eventCounts;
+ readonly attribute unsigned long long interactionCount;
+};
+
+partial dictionary PerformanceObserverInit {
+ DOMHighResTimeStamp durationThreshold;
+};
diff --git a/test/wpt/tests/interfaces/eyedropper-api.idl b/test/wpt/tests/interfaces/eyedropper-api.idl
new file mode 100644
index 0000000..62c8c4a
--- /dev/null
+++ b/test/wpt/tests/interfaces/eyedropper-api.idl
@@ -0,0 +1,18 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: EyeDropper API (https://wicg.github.io/eyedropper-api/)
+
+dictionary ColorSelectionResult {
+ DOMString sRGBHex;
+};
+
+dictionary ColorSelectionOptions {
+ AbortSignal signal;
+};
+
+[Exposed=Window, SecureContext]
+interface EyeDropper {
+ constructor();
+ Promise<ColorSelectionResult> open(optional ColorSelectionOptions options = {});
+};
diff --git a/test/wpt/tests/interfaces/fenced-frame.idl b/test/wpt/tests/interfaces/fenced-frame.idl
new file mode 100644
index 0000000..440ec2b
--- /dev/null
+++ b/test/wpt/tests/interfaces/fenced-frame.idl
@@ -0,0 +1,57 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Fenced Frame (https://wicg.github.io/fenced-frame/)
+
+[Exposed=Window]
+interface HTMLFencedFrameElement : HTMLElement {
+ [HTMLConstructor] constructor();
+
+ [CEReactions] attribute FencedFrameConfig? config;
+ [CEReactions] attribute DOMString width;
+ [CEReactions] attribute DOMString height;
+ [CEReactions] attribute DOMString allow;
+};
+
+enum OpaqueProperty {"opaque"};
+
+typedef (unsigned long or OpaqueProperty) FencedFrameConfigSize;
+typedef USVString FencedFrameConfigURL;
+
+[Exposed=Window]
+interface FencedFrameConfig {
+ readonly attribute FencedFrameConfigSize? containerWidth;
+ readonly attribute FencedFrameConfigSize? containerHeight;
+ readonly attribute FencedFrameConfigSize? contentWidth;
+ readonly attribute FencedFrameConfigSize? contentHeight;
+
+ undefined setSharedStorageContext(DOMString contextString);
+};
+
+enum FenceReportingDestination {
+ "buyer",
+ "seller",
+ "component-seller",
+ "direct-seller",
+ "shared-storage-select-url",
+};
+
+dictionary FenceEvent {
+ required DOMString eventType;
+ required DOMString eventData;
+ required sequence<FenceReportingDestination> destination;
+};
+
+typedef (FenceEvent or DOMString) ReportEventType;
+
+[Exposed=Window]
+interface Fence {
+ undefined reportEvent(ReportEventType event);
+ undefined setReportEventDataForAutomaticBeacons(FenceEvent event);
+ sequence<FencedFrameConfig> getNestedConfigs();
+};
+
+partial interface Window {
+ // Collection of fenced frame APIs
+ readonly attribute Fence? fence;
+};
diff --git a/test/wpt/tests/interfaces/fetch.idl b/test/wpt/tests/interfaces/fetch.idl
new file mode 100644
index 0000000..81a5e69
--- /dev/null
+++ b/test/wpt/tests/interfaces/fetch.idl
@@ -0,0 +1,117 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Fetch Standard (https://fetch.spec.whatwg.org/)
+
+typedef (sequence<sequence<ByteString>> or record<ByteString, ByteString>) HeadersInit;
+
+[Exposed=(Window,Worker)]
+interface Headers {
+ constructor(optional HeadersInit init);
+
+ undefined append(ByteString name, ByteString value);
+ undefined delete(ByteString name);
+ ByteString? get(ByteString name);
+ sequence<ByteString> getSetCookie();
+ boolean has(ByteString name);
+ undefined set(ByteString name, ByteString value);
+ iterable<ByteString, ByteString>;
+};
+
+typedef (Blob or BufferSource or FormData or URLSearchParams or USVString) XMLHttpRequestBodyInit;
+
+typedef (ReadableStream or XMLHttpRequestBodyInit) BodyInit;
+interface mixin Body {
+ readonly attribute ReadableStream? body;
+ readonly attribute boolean bodyUsed;
+ [NewObject] Promise<ArrayBuffer> arrayBuffer();
+ [NewObject] Promise<Blob> blob();
+ [NewObject] Promise<FormData> formData();
+ [NewObject] Promise<any> json();
+ [NewObject] Promise<USVString> text();
+};
+typedef (Request or USVString) RequestInfo;
+
+[Exposed=(Window,Worker)]
+interface Request {
+ constructor(RequestInfo input, optional RequestInit init = {});
+
+ readonly attribute ByteString method;
+ readonly attribute USVString url;
+ [SameObject] readonly attribute Headers headers;
+
+ readonly attribute RequestDestination destination;
+ readonly attribute USVString referrer;
+ readonly attribute ReferrerPolicy referrerPolicy;
+ readonly attribute RequestMode mode;
+ readonly attribute RequestCredentials credentials;
+ readonly attribute RequestCache cache;
+ readonly attribute RequestRedirect redirect;
+ readonly attribute DOMString integrity;
+ readonly attribute boolean keepalive;
+ readonly attribute boolean isReloadNavigation;
+ readonly attribute boolean isHistoryNavigation;
+ readonly attribute AbortSignal signal;
+ readonly attribute RequestDuplex duplex;
+
+ [NewObject] Request clone();
+};
+Request includes Body;
+
+dictionary RequestInit {
+ ByteString method;
+ HeadersInit headers;
+ BodyInit? body;
+ USVString referrer;
+ ReferrerPolicy referrerPolicy;
+ RequestMode mode;
+ RequestCredentials credentials;
+ RequestCache cache;
+ RequestRedirect redirect;
+ DOMString integrity;
+ boolean keepalive;
+ AbortSignal? signal;
+ RequestDuplex duplex;
+ RequestPriority priority;
+ any window; // can only be set to null
+};
+
+enum RequestDestination { "", "audio", "audioworklet", "document", "embed", "font", "frame", "iframe", "image", "manifest", "object", "paintworklet", "report", "script", "sharedworker", "style", "track", "video", "worker", "xslt" };
+enum RequestMode { "navigate", "same-origin", "no-cors", "cors" };
+enum RequestCredentials { "omit", "same-origin", "include" };
+enum RequestCache { "default", "no-store", "reload", "no-cache", "force-cache", "only-if-cached" };
+enum RequestRedirect { "follow", "error", "manual" };
+enum RequestDuplex { "half" };
+enum RequestPriority { "high", "low", "auto" };
+
+[Exposed=(Window,Worker)]interface Response {
+ constructor(optional BodyInit? body = null, optional ResponseInit init = {});
+
+ [NewObject] static Response error();
+ [NewObject] static Response redirect(USVString url, optional unsigned short status = 302);
+ [NewObject] static Response json(any data, optional ResponseInit init = {});
+
+ readonly attribute ResponseType type;
+
+ readonly attribute USVString url;
+ readonly attribute boolean redirected;
+ readonly attribute unsigned short status;
+ readonly attribute boolean ok;
+ readonly attribute ByteString statusText;
+ [SameObject] readonly attribute Headers headers;
+
+ [NewObject] Response clone();
+};
+Response includes Body;
+
+dictionary ResponseInit {
+ unsigned short status = 200;
+ ByteString statusText = "";
+ HeadersInit headers;
+};
+
+enum ResponseType { "basic", "cors", "default", "error", "opaque", "opaqueredirect" };
+
+partial interface mixin WindowOrWorkerGlobalScope {
+ [NewObject] Promise<Response> fetch(RequestInfo input, optional RequestInit init = {});
+};
diff --git a/test/wpt/tests/interfaces/fido.idl b/test/wpt/tests/interfaces/fido.idl
new file mode 100644
index 0000000..32b6c75
--- /dev/null
+++ b/test/wpt/tests/interfaces/fido.idl
@@ -0,0 +1,47 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Client to Authenticator Protocol (CTAP) (https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-errata-20220621.html)
+
+partial dictionary AuthenticationExtensionsClientInputs {
+ USVString credentialProtectionPolicy;
+ boolean enforceCredentialProtectionPolicy = false;
+};
+
+partial dictionary AuthenticationExtensionsClientInputs {
+ ArrayBuffer credBlob;
+};
+
+partial dictionary AuthenticationExtensionsClientInputs {
+ boolean getCredBlob;
+};
+
+partial dictionary AuthenticationExtensionsClientInputs {
+ boolean minPinLength;
+};
+
+partial dictionary AuthenticationExtensionsClientInputs {
+ boolean hmacCreateSecret;
+};
+
+dictionary HMACGetSecretInput {
+ required ArrayBuffer salt1; // 32-byte random data
+ ArrayBuffer salt2; // Optional additional 32-byte random data
+};
+
+partial dictionary AuthenticationExtensionsClientInputs {
+ HMACGetSecretInput hmacGetSecret;
+};
+
+partial dictionary AuthenticationExtensionsClientOutputs {
+ boolean hmacCreateSecret;
+};
+
+dictionary HMACGetSecretOutput {
+ required ArrayBuffer output1;
+ ArrayBuffer output2;
+};
+
+partial dictionary AuthenticationExtensionsClientOutputs {
+ HMACGetSecretOutput hmacGetSecret;
+};
diff --git a/test/wpt/tests/interfaces/file-system-access.idl b/test/wpt/tests/interfaces/file-system-access.idl
new file mode 100644
index 0000000..572f934
--- /dev/null
+++ b/test/wpt/tests/interfaces/file-system-access.idl
@@ -0,0 +1,72 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: File System Access (https://wicg.github.io/file-system-access/)
+
+enum FileSystemPermissionMode {
+ "read",
+ "readwrite"
+};
+
+dictionary FileSystemPermissionDescriptor : PermissionDescriptor {
+ required FileSystemHandle handle;
+ FileSystemPermissionMode mode = "read";
+};
+
+dictionary FileSystemHandlePermissionDescriptor {
+ FileSystemPermissionMode mode = "read";
+};
+
+[Exposed=(Window,Worker), SecureContext, Serializable]
+partial interface FileSystemHandle {
+ Promise<PermissionState> queryPermission(optional FileSystemHandlePermissionDescriptor descriptor = {});
+ Promise<PermissionState> requestPermission(optional FileSystemHandlePermissionDescriptor descriptor = {});
+};
+
+enum WellKnownDirectory {
+ "desktop",
+ "documents",
+ "downloads",
+ "music",
+ "pictures",
+ "videos",
+};
+
+typedef (WellKnownDirectory or FileSystemHandle) StartInDirectory;
+
+dictionary FilePickerAcceptType {
+ USVString description;
+ record<USVString, (USVString or sequence<USVString>)> accept;
+};
+
+dictionary FilePickerOptions {
+ sequence<FilePickerAcceptType> types;
+ boolean excludeAcceptAllOption = false;
+ DOMString id;
+ StartInDirectory startIn;
+};
+
+dictionary OpenFilePickerOptions : FilePickerOptions {
+ boolean multiple = false;
+};
+
+dictionary SaveFilePickerOptions : FilePickerOptions {
+ USVString? suggestedName;
+};
+
+dictionary DirectoryPickerOptions {
+ DOMString id;
+ StartInDirectory startIn;
+ FileSystemPermissionMode mode = "read";
+};
+
+[SecureContext]
+partial interface Window {
+ Promise<sequence<FileSystemFileHandle>> showOpenFilePicker(optional OpenFilePickerOptions options = {});
+ Promise<FileSystemFileHandle> showSaveFilePicker(optional SaveFilePickerOptions options = {});
+ Promise<FileSystemDirectoryHandle> showDirectoryPicker(optional DirectoryPickerOptions options = {});
+};
+
+partial interface DataTransferItem {
+ Promise<FileSystemHandle?> getAsFileSystemHandle();
+};
diff --git a/test/wpt/tests/interfaces/filter-effects.idl b/test/wpt/tests/interfaces/filter-effects.idl
new file mode 100644
index 0000000..ecbb6a9
--- /dev/null
+++ b/test/wpt/tests/interfaces/filter-effects.idl
@@ -0,0 +1,341 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Filter Effects Module Level 1 (https://drafts.fxtf.org/filter-effects-1/)
+
+[Exposed=Window]
+interface SVGFilterElement : SVGElement {
+ readonly attribute SVGAnimatedEnumeration filterUnits;
+ readonly attribute SVGAnimatedEnumeration primitiveUnits;
+ readonly attribute SVGAnimatedLength x;
+ readonly attribute SVGAnimatedLength y;
+ readonly attribute SVGAnimatedLength width;
+ readonly attribute SVGAnimatedLength height;
+};
+
+SVGFilterElement includes SVGURIReference;
+
+interface mixin SVGFilterPrimitiveStandardAttributes {
+ readonly attribute SVGAnimatedLength x;
+ readonly attribute SVGAnimatedLength y;
+ readonly attribute SVGAnimatedLength width;
+ readonly attribute SVGAnimatedLength height;
+ readonly attribute SVGAnimatedString result;
+};
+
+[Exposed=Window]
+interface SVGFEBlendElement : SVGElement {
+
+ // Blend Mode Types
+ const unsigned short SVG_FEBLEND_MODE_UNKNOWN = 0;
+ const unsigned short SVG_FEBLEND_MODE_NORMAL = 1;
+ const unsigned short SVG_FEBLEND_MODE_MULTIPLY = 2;
+ const unsigned short SVG_FEBLEND_MODE_SCREEN = 3;
+ const unsigned short SVG_FEBLEND_MODE_DARKEN = 4;
+ const unsigned short SVG_FEBLEND_MODE_LIGHTEN = 5;
+ const unsigned short SVG_FEBLEND_MODE_OVERLAY = 6;
+ const unsigned short SVG_FEBLEND_MODE_COLOR_DODGE = 7;
+ const unsigned short SVG_FEBLEND_MODE_COLOR_BURN = 8;
+ const unsigned short SVG_FEBLEND_MODE_HARD_LIGHT = 9;
+ const unsigned short SVG_FEBLEND_MODE_SOFT_LIGHT = 10;
+ const unsigned short SVG_FEBLEND_MODE_DIFFERENCE = 11;
+ const unsigned short SVG_FEBLEND_MODE_EXCLUSION = 12;
+ const unsigned short SVG_FEBLEND_MODE_HUE = 13;
+ const unsigned short SVG_FEBLEND_MODE_SATURATION = 14;
+ const unsigned short SVG_FEBLEND_MODE_COLOR = 15;
+ const unsigned short SVG_FEBLEND_MODE_LUMINOSITY = 16;
+
+ readonly attribute SVGAnimatedString in1;
+ readonly attribute SVGAnimatedString in2;
+ readonly attribute SVGAnimatedEnumeration mode;
+};
+
+SVGFEBlendElement includes SVGFilterPrimitiveStandardAttributes;
+
+[Exposed=Window]
+interface SVGFEColorMatrixElement : SVGElement {
+
+ // Color Matrix Types
+ const unsigned short SVG_FECOLORMATRIX_TYPE_UNKNOWN = 0;
+ const unsigned short SVG_FECOLORMATRIX_TYPE_MATRIX = 1;
+ const unsigned short SVG_FECOLORMATRIX_TYPE_SATURATE = 2;
+ const unsigned short SVG_FECOLORMATRIX_TYPE_HUEROTATE = 3;
+ const unsigned short SVG_FECOLORMATRIX_TYPE_LUMINANCETOALPHA = 4;
+
+ readonly attribute SVGAnimatedString in1;
+ readonly attribute SVGAnimatedEnumeration type;
+ readonly attribute SVGAnimatedNumberList values;
+};
+
+SVGFEColorMatrixElement includes SVGFilterPrimitiveStandardAttributes;
+
+[Exposed=Window]
+interface SVGFEComponentTransferElement : SVGElement {
+ readonly attribute SVGAnimatedString in1;
+};
+
+SVGFEComponentTransferElement includes SVGFilterPrimitiveStandardAttributes;
+
+[Exposed=Window]
+interface SVGComponentTransferFunctionElement : SVGElement {
+
+ // Component Transfer Types
+ const unsigned short SVG_FECOMPONENTTRANSFER_TYPE_UNKNOWN = 0;
+ const unsigned short SVG_FECOMPONENTTRANSFER_TYPE_IDENTITY = 1;
+ const unsigned short SVG_FECOMPONENTTRANSFER_TYPE_TABLE = 2;
+ const unsigned short SVG_FECOMPONENTTRANSFER_TYPE_DISCRETE = 3;
+ const unsigned short SVG_FECOMPONENTTRANSFER_TYPE_LINEAR = 4;
+ const unsigned short SVG_FECOMPONENTTRANSFER_TYPE_GAMMA = 5;
+
+ readonly attribute SVGAnimatedEnumeration type;
+ readonly attribute SVGAnimatedNumberList tableValues;
+ readonly attribute SVGAnimatedNumber slope;
+ readonly attribute SVGAnimatedNumber intercept;
+ readonly attribute SVGAnimatedNumber amplitude;
+ readonly attribute SVGAnimatedNumber exponent;
+ readonly attribute SVGAnimatedNumber offset;
+};
+
+[Exposed=Window]
+interface SVGFEFuncRElement : SVGComponentTransferFunctionElement {
+};
+
+[Exposed=Window]
+interface SVGFEFuncGElement : SVGComponentTransferFunctionElement {
+};
+
+[Exposed=Window]
+interface SVGFEFuncBElement : SVGComponentTransferFunctionElement {
+};
+
+[Exposed=Window]
+interface SVGFEFuncAElement : SVGComponentTransferFunctionElement {
+};
+
+[Exposed=Window]
+interface SVGFECompositeElement : SVGElement {
+
+ // Composite Operators
+ const unsigned short SVG_FECOMPOSITE_OPERATOR_UNKNOWN = 0;
+ const unsigned short SVG_FECOMPOSITE_OPERATOR_OVER = 1;
+ const unsigned short SVG_FECOMPOSITE_OPERATOR_IN = 2;
+ const unsigned short SVG_FECOMPOSITE_OPERATOR_OUT = 3;
+ const unsigned short SVG_FECOMPOSITE_OPERATOR_ATOP = 4;
+ const unsigned short SVG_FECOMPOSITE_OPERATOR_XOR = 5;
+ const unsigned short SVG_FECOMPOSITE_OPERATOR_ARITHMETIC = 6;
+
+ readonly attribute SVGAnimatedString in1;
+ readonly attribute SVGAnimatedString in2;
+ readonly attribute SVGAnimatedEnumeration operator;
+ readonly attribute SVGAnimatedNumber k1;
+ readonly attribute SVGAnimatedNumber k2;
+ readonly attribute SVGAnimatedNumber k3;
+ readonly attribute SVGAnimatedNumber k4;
+};
+
+SVGFECompositeElement includes SVGFilterPrimitiveStandardAttributes;
+
+[Exposed=Window]
+interface SVGFEConvolveMatrixElement : SVGElement {
+
+ // Edge Mode Values
+ const unsigned short SVG_EDGEMODE_UNKNOWN = 0;
+ const unsigned short SVG_EDGEMODE_DUPLICATE = 1;
+ const unsigned short SVG_EDGEMODE_WRAP = 2;
+ const unsigned short SVG_EDGEMODE_NONE = 3;
+
+ readonly attribute SVGAnimatedString in1;
+ readonly attribute SVGAnimatedInteger orderX;
+ readonly attribute SVGAnimatedInteger orderY;
+ readonly attribute SVGAnimatedNumberList kernelMatrix;
+ readonly attribute SVGAnimatedNumber divisor;
+ readonly attribute SVGAnimatedNumber bias;
+ readonly attribute SVGAnimatedInteger targetX;
+ readonly attribute SVGAnimatedInteger targetY;
+ readonly attribute SVGAnimatedEnumeration edgeMode;
+ readonly attribute SVGAnimatedNumber kernelUnitLengthX;
+ readonly attribute SVGAnimatedNumber kernelUnitLengthY;
+ readonly attribute SVGAnimatedBoolean preserveAlpha;
+};
+
+SVGFEConvolveMatrixElement includes SVGFilterPrimitiveStandardAttributes;
+
+[Exposed=Window]
+interface SVGFEDiffuseLightingElement : SVGElement {
+ readonly attribute SVGAnimatedString in1;
+ readonly attribute SVGAnimatedNumber surfaceScale;
+ readonly attribute SVGAnimatedNumber diffuseConstant;
+ readonly attribute SVGAnimatedNumber kernelUnitLengthX;
+ readonly attribute SVGAnimatedNumber kernelUnitLengthY;
+};
+
+SVGFEDiffuseLightingElement includes SVGFilterPrimitiveStandardAttributes;
+
+[Exposed=Window]
+interface SVGFEDistantLightElement : SVGElement {
+ readonly attribute SVGAnimatedNumber azimuth;
+ readonly attribute SVGAnimatedNumber elevation;
+};
+
+[Exposed=Window]
+interface SVGFEPointLightElement : SVGElement {
+ readonly attribute SVGAnimatedNumber x;
+ readonly attribute SVGAnimatedNumber y;
+ readonly attribute SVGAnimatedNumber z;
+};
+
+[Exposed=Window]
+interface SVGFESpotLightElement : SVGElement {
+ readonly attribute SVGAnimatedNumber x;
+ readonly attribute SVGAnimatedNumber y;
+ readonly attribute SVGAnimatedNumber z;
+ readonly attribute SVGAnimatedNumber pointsAtX;
+ readonly attribute SVGAnimatedNumber pointsAtY;
+ readonly attribute SVGAnimatedNumber pointsAtZ;
+ readonly attribute SVGAnimatedNumber specularExponent;
+ readonly attribute SVGAnimatedNumber limitingConeAngle;
+};
+
+[Exposed=Window]
+interface SVGFEDisplacementMapElement : SVGElement {
+
+ // Channel Selectors
+ const unsigned short SVG_CHANNEL_UNKNOWN = 0;
+ const unsigned short SVG_CHANNEL_R = 1;
+ const unsigned short SVG_CHANNEL_G = 2;
+ const unsigned short SVG_CHANNEL_B = 3;
+ const unsigned short SVG_CHANNEL_A = 4;
+
+ readonly attribute SVGAnimatedString in1;
+ readonly attribute SVGAnimatedString in2;
+ readonly attribute SVGAnimatedNumber scale;
+ readonly attribute SVGAnimatedEnumeration xChannelSelector;
+ readonly attribute SVGAnimatedEnumeration yChannelSelector;
+};
+
+SVGFEDisplacementMapElement includes SVGFilterPrimitiveStandardAttributes;
+
+[Exposed=Window]
+interface SVGFEDropShadowElement : SVGElement {
+ readonly attribute SVGAnimatedString in1;
+ readonly attribute SVGAnimatedNumber dx;
+ readonly attribute SVGAnimatedNumber dy;
+ readonly attribute SVGAnimatedNumber stdDeviationX;
+ readonly attribute SVGAnimatedNumber stdDeviationY;
+
+ undefined setStdDeviation(float stdDeviationX, float stdDeviationY);
+};
+
+SVGFEDropShadowElement includes SVGFilterPrimitiveStandardAttributes;
+
+[Exposed=Window]
+interface SVGFEFloodElement : SVGElement {
+};
+
+SVGFEFloodElement includes SVGFilterPrimitiveStandardAttributes;
+
+[Exposed=Window]
+interface SVGFEGaussianBlurElement : SVGElement {
+
+ // Edge Mode Values
+ const unsigned short SVG_EDGEMODE_UNKNOWN = 0;
+ const unsigned short SVG_EDGEMODE_DUPLICATE = 1;
+ const unsigned short SVG_EDGEMODE_WRAP = 2;
+ const unsigned short SVG_EDGEMODE_NONE = 3;
+
+ readonly attribute SVGAnimatedString in1;
+ readonly attribute SVGAnimatedNumber stdDeviationX;
+ readonly attribute SVGAnimatedNumber stdDeviationY;
+ readonly attribute SVGAnimatedEnumeration edgeMode;
+
+ undefined setStdDeviation(float stdDeviationX, float stdDeviationY);
+};
+
+SVGFEGaussianBlurElement includes SVGFilterPrimitiveStandardAttributes;
+
+[Exposed=Window]
+interface SVGFEImageElement : SVGElement {
+ readonly attribute SVGAnimatedPreserveAspectRatio preserveAspectRatio;
+ readonly attribute SVGAnimatedString crossOrigin;
+};
+
+SVGFEImageElement includes SVGFilterPrimitiveStandardAttributes;
+SVGFEImageElement includes SVGURIReference;
+
+[Exposed=Window]
+interface SVGFEMergeElement : SVGElement {
+};
+
+SVGFEMergeElement includes SVGFilterPrimitiveStandardAttributes;
+
+[Exposed=Window]
+interface SVGFEMergeNodeElement : SVGElement {
+ readonly attribute SVGAnimatedString in1;
+};
+
+[Exposed=Window]
+interface SVGFEMorphologyElement : SVGElement {
+
+ // Morphology Operators
+ const unsigned short SVG_MORPHOLOGY_OPERATOR_UNKNOWN = 0;
+ const unsigned short SVG_MORPHOLOGY_OPERATOR_ERODE = 1;
+ const unsigned short SVG_MORPHOLOGY_OPERATOR_DILATE = 2;
+
+ readonly attribute SVGAnimatedString in1;
+ readonly attribute SVGAnimatedEnumeration operator;
+ readonly attribute SVGAnimatedNumber radiusX;
+ readonly attribute SVGAnimatedNumber radiusY;
+};
+
+SVGFEMorphologyElement includes SVGFilterPrimitiveStandardAttributes;
+
+[Exposed=Window]
+interface SVGFEOffsetElement : SVGElement {
+ readonly attribute SVGAnimatedString in1;
+ readonly attribute SVGAnimatedNumber dx;
+ readonly attribute SVGAnimatedNumber dy;
+};
+
+SVGFEOffsetElement includes SVGFilterPrimitiveStandardAttributes;
+
+[Exposed=Window]
+interface SVGFESpecularLightingElement : SVGElement {
+ readonly attribute SVGAnimatedString in1;
+ readonly attribute SVGAnimatedNumber surfaceScale;
+ readonly attribute SVGAnimatedNumber specularConstant;
+ readonly attribute SVGAnimatedNumber specularExponent;
+ readonly attribute SVGAnimatedNumber kernelUnitLengthX;
+ readonly attribute SVGAnimatedNumber kernelUnitLengthY;
+};
+
+SVGFESpecularLightingElement includes SVGFilterPrimitiveStandardAttributes;
+
+[Exposed=Window]
+interface SVGFETileElement : SVGElement {
+ readonly attribute SVGAnimatedString in1;
+};
+
+SVGFETileElement includes SVGFilterPrimitiveStandardAttributes;
+
+[Exposed=Window]
+interface SVGFETurbulenceElement : SVGElement {
+
+ // Turbulence Types
+ const unsigned short SVG_TURBULENCE_TYPE_UNKNOWN = 0;
+ const unsigned short SVG_TURBULENCE_TYPE_FRACTALNOISE = 1;
+ const unsigned short SVG_TURBULENCE_TYPE_TURBULENCE = 2;
+
+ // Stitch Options
+ const unsigned short SVG_STITCHTYPE_UNKNOWN = 0;
+ const unsigned short SVG_STITCHTYPE_STITCH = 1;
+ const unsigned short SVG_STITCHTYPE_NOSTITCH = 2;
+
+ readonly attribute SVGAnimatedNumber baseFrequencyX;
+ readonly attribute SVGAnimatedNumber baseFrequencyY;
+ readonly attribute SVGAnimatedInteger numOctaves;
+ readonly attribute SVGAnimatedNumber seed;
+ readonly attribute SVGAnimatedEnumeration stitchTiles;
+ readonly attribute SVGAnimatedEnumeration type;
+};
+
+SVGFETurbulenceElement includes SVGFilterPrimitiveStandardAttributes;
diff --git a/test/wpt/tests/interfaces/font-metrics-api.idl b/test/wpt/tests/interfaces/font-metrics-api.idl
new file mode 100644
index 0000000..9bb94bc
--- /dev/null
+++ b/test/wpt/tests/interfaces/font-metrics-api.idl
@@ -0,0 +1,42 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Font Metrics API Level 1 (https://drafts.css-houdini.org/font-metrics-api-1/)
+
+partial interface Document {
+ FontMetrics measureElement(Element element);
+ FontMetrics measureText(DOMString text, StylePropertyMapReadOnly styleMap);
+};
+
+[Exposed=Window]
+interface FontMetrics {
+ readonly attribute double width;
+ readonly attribute FrozenArray<double> advances;
+
+ readonly attribute double boundingBoxLeft;
+ readonly attribute double boundingBoxRight;
+
+ readonly attribute double height;
+ readonly attribute double emHeightAscent;
+ readonly attribute double emHeightDescent;
+ readonly attribute double boundingBoxAscent;
+ readonly attribute double boundingBoxDescent;
+ readonly attribute double fontBoundingBoxAscent;
+ readonly attribute double fontBoundingBoxDescent;
+
+ readonly attribute Baseline dominantBaseline;
+ readonly attribute FrozenArray<Baseline> baselines;
+ readonly attribute FrozenArray<Font> fonts;
+};
+
+[Exposed=Window]
+interface Baseline {
+ readonly attribute DOMString name;
+ readonly attribute double value;
+};
+
+[Exposed=Window]
+interface Font {
+ readonly attribute DOMString name;
+ readonly attribute unsigned long glyphsRendered;
+};
diff --git a/test/wpt/tests/interfaces/fs.idl b/test/wpt/tests/interfaces/fs.idl
new file mode 100644
index 0000000..e341ab3
--- /dev/null
+++ b/test/wpt/tests/interfaces/fs.idl
@@ -0,0 +1,97 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: File System Standard (https://fs.spec.whatwg.org/)
+
+enum FileSystemHandleKind {
+ "file",
+ "directory",
+};
+
+[Exposed=(Window,Worker), SecureContext, Serializable]
+interface FileSystemHandle {
+ readonly attribute FileSystemHandleKind kind;
+ readonly attribute USVString name;
+
+ Promise<boolean> isSameEntry(FileSystemHandle other);
+};
+
+dictionary FileSystemCreateWritableOptions {
+ boolean keepExistingData = false;
+};
+
+[Exposed=(Window,Worker), SecureContext, Serializable]
+interface FileSystemFileHandle : FileSystemHandle {
+ Promise<File> getFile();
+ Promise<FileSystemWritableFileStream> createWritable(optional FileSystemCreateWritableOptions options = {});
+ [Exposed=DedicatedWorker]
+ Promise<FileSystemSyncAccessHandle> createSyncAccessHandle();
+};
+
+dictionary FileSystemGetFileOptions {
+ boolean create = false;
+};
+
+dictionary FileSystemGetDirectoryOptions {
+ boolean create = false;
+};
+
+dictionary FileSystemRemoveOptions {
+ boolean recursive = false;
+};
+
+[Exposed=(Window,Worker), SecureContext, Serializable]
+interface FileSystemDirectoryHandle : FileSystemHandle {
+ async iterable<USVString, FileSystemHandle>;
+
+ Promise<FileSystemFileHandle> getFileHandle(USVString name, optional FileSystemGetFileOptions options = {});
+ Promise<FileSystemDirectoryHandle> getDirectoryHandle(USVString name, optional FileSystemGetDirectoryOptions options = {});
+
+ Promise<undefined> removeEntry(USVString name, optional FileSystemRemoveOptions options = {});
+
+ Promise<sequence<USVString>?> resolve(FileSystemHandle possibleDescendant);
+};
+
+enum WriteCommandType {
+ "write",
+ "seek",
+ "truncate",
+};
+
+dictionary WriteParams {
+ required WriteCommandType type;
+ unsigned long long? size;
+ unsigned long long? position;
+ (BufferSource or Blob or USVString)? data;
+};
+
+typedef (BufferSource or Blob or USVString or WriteParams) FileSystemWriteChunkType;
+
+[Exposed=(Window,Worker), SecureContext]
+interface FileSystemWritableFileStream : WritableStream {
+ Promise<undefined> write(FileSystemWriteChunkType data);
+ Promise<undefined> seek(unsigned long long position);
+ Promise<undefined> truncate(unsigned long long size);
+};
+
+dictionary FileSystemReadWriteOptions {
+ [EnforceRange] unsigned long long at;
+};
+
+[Exposed=DedicatedWorker, SecureContext]
+interface FileSystemSyncAccessHandle {
+ unsigned long long read([AllowShared] BufferSource buffer,
+ optional FileSystemReadWriteOptions options = {});
+ unsigned long long write([AllowShared] BufferSource buffer,
+ optional FileSystemReadWriteOptions options = {});
+
+ undefined truncate([EnforceRange] unsigned long long newSize);
+ unsigned long long getSize();
+ undefined flush();
+ undefined close();
+};
+
+[SecureContext]
+partial interface StorageManager {
+ Promise<FileSystemDirectoryHandle> getDirectory();
+};
diff --git a/test/wpt/tests/interfaces/fullscreen.idl b/test/wpt/tests/interfaces/fullscreen.idl
new file mode 100644
index 0000000..2f67f09
--- /dev/null
+++ b/test/wpt/tests/interfaces/fullscreen.idl
@@ -0,0 +1,35 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Fullscreen API Standard (https://fullscreen.spec.whatwg.org/)
+
+enum FullscreenNavigationUI {
+ "auto",
+ "show",
+ "hide"
+};
+
+dictionary FullscreenOptions {
+ FullscreenNavigationUI navigationUI = "auto";
+};
+
+partial interface Element {
+ Promise<undefined> requestFullscreen(optional FullscreenOptions options = {});
+
+ attribute EventHandler onfullscreenchange;
+ attribute EventHandler onfullscreenerror;
+};
+
+partial interface Document {
+ [LegacyLenientSetter] readonly attribute boolean fullscreenEnabled;
+ [LegacyLenientSetter, Unscopable] readonly attribute boolean fullscreen; // historical
+
+ Promise<undefined> exitFullscreen();
+
+ attribute EventHandler onfullscreenchange;
+ attribute EventHandler onfullscreenerror;
+};
+
+partial interface mixin DocumentOrShadowRoot {
+ [LegacyLenientSetter] readonly attribute Element? fullscreenElement;
+};
diff --git a/test/wpt/tests/interfaces/gamepad-extensions.idl b/test/wpt/tests/interfaces/gamepad-extensions.idl
new file mode 100644
index 0000000..d7d7506
--- /dev/null
+++ b/test/wpt/tests/interfaces/gamepad-extensions.idl
@@ -0,0 +1,71 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Gamepad Extensions (https://w3c.github.io/gamepad/extensions.html)
+
+enum GamepadHand {
+ "", /* unknown, both hands, or not applicable */
+ "left",
+ "right"
+};
+
+[Exposed=Window]
+interface GamepadHapticActuator {
+ readonly attribute GamepadHapticActuatorType type;
+ boolean canPlayEffectType(GamepadHapticEffectType type);
+ Promise<GamepadHapticsResult> playEffect(
+ GamepadHapticEffectType type,
+ optional GamepadEffectParameters params = {});
+ Promise<boolean> pulse(double value, double duration);
+ Promise<GamepadHapticsResult> reset();
+};
+
+enum GamepadHapticsResult {
+ "complete",
+ "preempted"
+};
+
+enum GamepadHapticActuatorType {
+ "vibration",
+ "dual-rumble"
+};
+
+enum GamepadHapticEffectType {
+ "dual-rumble"
+};
+
+dictionary GamepadEffectParameters {
+ double duration = 0.0;
+ double startDelay = 0.0;
+ double strongMagnitude = 0.0;
+ double weakMagnitude = 0.0;
+};
+
+[Exposed=Window]
+interface GamepadPose {
+ readonly attribute boolean hasOrientation;
+ readonly attribute boolean hasPosition;
+
+ readonly attribute Float32Array? position;
+ readonly attribute Float32Array? linearVelocity;
+ readonly attribute Float32Array? linearAcceleration;
+ readonly attribute Float32Array? orientation;
+ readonly attribute Float32Array? angularVelocity;
+ readonly attribute Float32Array? angularAcceleration;
+};
+
+[Exposed=Window, SecureContext]
+interface GamepadTouch {
+ readonly attribute unsigned long touchId;
+ readonly attribute octet surfaceId;
+ readonly attribute Float32Array position;
+ readonly attribute Uint32Array? surfaceDimensions;
+};
+
+partial interface Gamepad {
+ readonly attribute GamepadHand hand;
+ readonly attribute FrozenArray<GamepadHapticActuator> hapticActuators;
+ readonly attribute GamepadPose? pose;
+ readonly attribute FrozenArray<GamepadTouch>? touchEvents;
+ [SameObject] readonly attribute GamepadHapticActuator? vibrationActuator;
+};
diff --git a/test/wpt/tests/interfaces/gamepad.idl b/test/wpt/tests/interfaces/gamepad.idl
new file mode 100644
index 0000000..bbc62da
--- /dev/null
+++ b/test/wpt/tests/interfaces/gamepad.idl
@@ -0,0 +1,49 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Gamepad (https://w3c.github.io/gamepad/)
+
+[Exposed=Window, SecureContext]
+interface Gamepad {
+ readonly attribute DOMString id;
+ readonly attribute long index;
+ readonly attribute boolean connected;
+ readonly attribute DOMHighResTimeStamp timestamp;
+ readonly attribute GamepadMappingType mapping;
+ readonly attribute FrozenArray<double> axes;
+ readonly attribute FrozenArray<GamepadButton> buttons;
+};
+
+[Exposed=Window, SecureContext]
+interface GamepadButton {
+ readonly attribute boolean pressed;
+ readonly attribute boolean touched;
+ readonly attribute double value;
+};
+
+enum GamepadMappingType {
+ "",
+ "standard",
+ "xr-standard",
+};
+
+[Exposed=Window]
+partial interface Navigator {
+ sequence<Gamepad?> getGamepads();
+};
+
+[Exposed=Window, SecureContext]
+
+interface GamepadEvent: Event {
+ constructor(DOMString type, GamepadEventInit eventInitDict);
+ [SameObject] readonly attribute Gamepad gamepad;
+};
+
+dictionary GamepadEventInit : EventInit {
+ required Gamepad gamepad;
+};
+
+partial interface mixin WindowEventHandlers {
+ attribute EventHandler ongamepadconnected;
+ attribute EventHandler ongamepaddisconnected;
+};
diff --git a/test/wpt/tests/interfaces/generic-sensor.idl b/test/wpt/tests/interfaces/generic-sensor.idl
new file mode 100644
index 0000000..157072f
--- /dev/null
+++ b/test/wpt/tests/interfaces/generic-sensor.idl
@@ -0,0 +1,60 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Generic Sensor API (https://w3c.github.io/sensors/)
+
+[SecureContext, Exposed=(DedicatedWorker, Window)]
+interface Sensor : EventTarget {
+ readonly attribute boolean activated;
+ readonly attribute boolean hasReading;
+ readonly attribute DOMHighResTimeStamp? timestamp;
+ undefined start();
+ undefined stop();
+ attribute EventHandler onreading;
+ attribute EventHandler onactivate;
+ attribute EventHandler onerror;
+};
+
+dictionary SensorOptions {
+ double frequency;
+};
+
+[SecureContext, Exposed=(DedicatedWorker, Window)]
+interface SensorErrorEvent : Event {
+ constructor(DOMString type, SensorErrorEventInit errorEventInitDict);
+ readonly attribute DOMException error;
+};
+
+dictionary SensorErrorEventInit : EventInit {
+ required DOMException error;
+};
+
+dictionary MockSensorConfiguration {
+ required MockSensorType mockSensorType;
+ boolean connected = true;
+ double? maxSamplingFrequency;
+ double? minSamplingFrequency;
+};
+
+dictionary MockSensor {
+ double maxSamplingFrequency;
+ double minSamplingFrequency;
+ double requestedSamplingFrequency;
+};
+
+enum MockSensorType {
+ "ambient-light",
+ "accelerometer",
+ "linear-acceleration",
+ "gravity",
+ "gyroscope",
+ "magnetometer",
+ "uncalibrated-magnetometer",
+ "absolute-orientation",
+ "relative-orientation",
+ "geolocation",
+ "proximity",
+};
+
+dictionary MockSensorReadingValues {
+};
diff --git a/test/wpt/tests/interfaces/geolocation-sensor.idl b/test/wpt/tests/interfaces/geolocation-sensor.idl
new file mode 100644
index 0000000..e1d6762
--- /dev/null
+++ b/test/wpt/tests/interfaces/geolocation-sensor.idl
@@ -0,0 +1,47 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Geolocation Sensor (https://w3c.github.io/geolocation-sensor/)
+
+[SecureContext,
+ Exposed=(DedicatedWorker, Window)]
+interface GeolocationSensor : Sensor {
+ constructor(optional GeolocationSensorOptions options = {});
+ static Promise<GeolocationSensorReading> read(optional ReadOptions readOptions = {});
+ readonly attribute unrestricted double? latitude;
+ readonly attribute unrestricted double? longitude;
+ readonly attribute unrestricted double? altitude;
+ readonly attribute unrestricted double? accuracy;
+ readonly attribute unrestricted double? altitudeAccuracy;
+ readonly attribute unrestricted double? heading;
+ readonly attribute unrestricted double? speed;
+};
+
+dictionary GeolocationSensorOptions : SensorOptions {
+ // placeholder for GeolocationSensor-specific options
+};
+
+dictionary ReadOptions : GeolocationSensorOptions {
+ AbortSignal? signal;
+};
+
+dictionary GeolocationSensorReading {
+ DOMHighResTimeStamp? timestamp;
+ double? latitude;
+ double? longitude;
+ double? altitude;
+ double? accuracy;
+ double? altitudeAccuracy;
+ double? heading;
+ double? speed;
+};
+
+dictionary GeolocationReadingValues {
+ required double? latitude;
+ required double? longitude;
+ required double? altitude;
+ required double? accuracy;
+ required double? altitudeAccuracy;
+ required double? heading;
+ required double? speed;
+};
diff --git a/test/wpt/tests/interfaces/geolocation.idl b/test/wpt/tests/interfaces/geolocation.idl
new file mode 100644
index 0000000..4b971f0
--- /dev/null
+++ b/test/wpt/tests/interfaces/geolocation.idl
@@ -0,0 +1,65 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Geolocation API (https://w3c.github.io/geolocation-api/)
+
+partial interface Navigator {
+ [SameObject] readonly attribute Geolocation geolocation;
+};
+
+[Exposed=Window]
+interface Geolocation {
+ undefined getCurrentPosition (
+ PositionCallback successCallback,
+ optional PositionErrorCallback? errorCallback = null,
+ optional PositionOptions options = {}
+ );
+
+ long watchPosition (
+ PositionCallback successCallback,
+ optional PositionErrorCallback? errorCallback = null,
+ optional PositionOptions options = {}
+ );
+
+ undefined clearWatch (long watchId);
+};
+
+callback PositionCallback = undefined (
+ GeolocationPosition position
+);
+
+callback PositionErrorCallback = undefined (
+ GeolocationPositionError positionError
+);
+
+dictionary PositionOptions {
+ boolean enableHighAccuracy = false;
+ [Clamp] unsigned long timeout = 0xFFFFFFFF;
+ [Clamp] unsigned long maximumAge = 0;
+};
+
+[Exposed=Window, SecureContext]
+interface GeolocationPosition {
+ readonly attribute GeolocationCoordinates coords;
+ readonly attribute EpochTimeStamp timestamp;
+};
+
+[Exposed=Window, SecureContext]
+interface GeolocationCoordinates {
+ readonly attribute double accuracy;
+ readonly attribute double latitude;
+ readonly attribute double longitude;
+ readonly attribute double? altitude;
+ readonly attribute double? altitudeAccuracy;
+ readonly attribute double? heading;
+ readonly attribute double? speed;
+};
+
+[Exposed=Window]
+interface GeolocationPositionError {
+ const unsigned short PERMISSION_DENIED = 1;
+ const unsigned short POSITION_UNAVAILABLE = 2;
+ const unsigned short TIMEOUT = 3;
+ readonly attribute unsigned short code;
+ readonly attribute DOMString message;
+};
diff --git a/test/wpt/tests/interfaces/geometry.idl b/test/wpt/tests/interfaces/geometry.idl
new file mode 100644
index 0000000..f7df449
--- /dev/null
+++ b/test/wpt/tests/interfaces/geometry.idl
@@ -0,0 +1,290 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Geometry Interfaces Module Level 1 (https://drafts.fxtf.org/geometry-1/)
+
+[Exposed=(Window,Worker),
+ Serializable]
+interface DOMPointReadOnly {
+ constructor(optional unrestricted double x = 0, optional unrestricted double y = 0,
+ optional unrestricted double z = 0, optional unrestricted double w = 1);
+
+ [NewObject] static DOMPointReadOnly fromPoint(optional DOMPointInit other = {});
+
+ readonly attribute unrestricted double x;
+ readonly attribute unrestricted double y;
+ readonly attribute unrestricted double z;
+ readonly attribute unrestricted double w;
+
+ [NewObject] DOMPoint matrixTransform(optional DOMMatrixInit matrix = {});
+
+ [Default] object toJSON();
+};
+
+[Exposed=(Window,Worker),
+ Serializable,
+ LegacyWindowAlias=SVGPoint]
+interface DOMPoint : DOMPointReadOnly {
+ constructor(optional unrestricted double x = 0, optional unrestricted double y = 0,
+ optional unrestricted double z = 0, optional unrestricted double w = 1);
+
+ [NewObject] static DOMPoint fromPoint(optional DOMPointInit other = {});
+
+ inherit attribute unrestricted double x;
+ inherit attribute unrestricted double y;
+ inherit attribute unrestricted double z;
+ inherit attribute unrestricted double w;
+};
+
+dictionary DOMPointInit {
+ unrestricted double x = 0;
+ unrestricted double y = 0;
+ unrestricted double z = 0;
+ unrestricted double w = 1;
+};
+
+[Exposed=(Window,Worker),
+ Serializable]
+interface DOMRectReadOnly {
+ constructor(optional unrestricted double x = 0, optional unrestricted double y = 0,
+ optional unrestricted double width = 0, optional unrestricted double height = 0);
+
+ [NewObject] static DOMRectReadOnly fromRect(optional DOMRectInit other = {});
+
+ readonly attribute unrestricted double x;
+ readonly attribute unrestricted double y;
+ readonly attribute unrestricted double width;
+ readonly attribute unrestricted double height;
+ readonly attribute unrestricted double top;
+ readonly attribute unrestricted double right;
+ readonly attribute unrestricted double bottom;
+ readonly attribute unrestricted double left;
+
+ [Default] object toJSON();
+};
+
+[Exposed=(Window,Worker),
+ Serializable,
+ LegacyWindowAlias=SVGRect]
+interface DOMRect : DOMRectReadOnly {
+ constructor(optional unrestricted double x = 0, optional unrestricted double y = 0,
+ optional unrestricted double width = 0, optional unrestricted double height = 0);
+
+ [NewObject] static DOMRect fromRect(optional DOMRectInit other = {});
+
+ inherit attribute unrestricted double x;
+ inherit attribute unrestricted double y;
+ inherit attribute unrestricted double width;
+ inherit attribute unrestricted double height;
+};
+
+dictionary DOMRectInit {
+ unrestricted double x = 0;
+ unrestricted double y = 0;
+ unrestricted double width = 0;
+ unrestricted double height = 0;
+};
+
+[Exposed=Window]
+interface DOMRectList {
+ readonly attribute unsigned long length;
+ getter DOMRect? item(unsigned long index);
+};
+
+[Exposed=(Window,Worker),
+ Serializable]
+interface DOMQuad {
+ constructor(optional DOMPointInit p1 = {}, optional DOMPointInit p2 = {},
+ optional DOMPointInit p3 = {}, optional DOMPointInit p4 = {});
+
+ [NewObject] static DOMQuad fromRect(optional DOMRectInit other = {});
+ [NewObject] static DOMQuad fromQuad(optional DOMQuadInit other = {});
+
+ [SameObject] readonly attribute DOMPoint p1;
+ [SameObject] readonly attribute DOMPoint p2;
+ [SameObject] readonly attribute DOMPoint p3;
+ [SameObject] readonly attribute DOMPoint p4;
+ [NewObject] DOMRect getBounds();
+
+ [Default] object toJSON();
+};
+
+dictionary DOMQuadInit {
+ DOMPointInit p1;
+ DOMPointInit p2;
+ DOMPointInit p3;
+ DOMPointInit p4;
+};
+
+[Exposed=(Window,Worker),
+ Serializable]
+interface DOMMatrixReadOnly {
+ constructor(optional (DOMString or sequence<unrestricted double>) init);
+
+ [NewObject] static DOMMatrixReadOnly fromMatrix(optional DOMMatrixInit other = {});
+ [NewObject] static DOMMatrixReadOnly fromFloat32Array(Float32Array array32);
+ [NewObject] static DOMMatrixReadOnly fromFloat64Array(Float64Array array64);
+
+ // These attributes are simple aliases for certain elements of the 4x4 matrix
+ readonly attribute unrestricted double a;
+ readonly attribute unrestricted double b;
+ readonly attribute unrestricted double c;
+ readonly attribute unrestricted double d;
+ readonly attribute unrestricted double e;
+ readonly attribute unrestricted double f;
+
+ readonly attribute unrestricted double m11;
+ readonly attribute unrestricted double m12;
+ readonly attribute unrestricted double m13;
+ readonly attribute unrestricted double m14;
+ readonly attribute unrestricted double m21;
+ readonly attribute unrestricted double m22;
+ readonly attribute unrestricted double m23;
+ readonly attribute unrestricted double m24;
+ readonly attribute unrestricted double m31;
+ readonly attribute unrestricted double m32;
+ readonly attribute unrestricted double m33;
+ readonly attribute unrestricted double m34;
+ readonly attribute unrestricted double m41;
+ readonly attribute unrestricted double m42;
+ readonly attribute unrestricted double m43;
+ readonly attribute unrestricted double m44;
+
+ readonly attribute boolean is2D;
+ readonly attribute boolean isIdentity;
+
+ // Immutable transform methods
+ [NewObject] DOMMatrix translate(optional unrestricted double tx = 0,
+ optional unrestricted double ty = 0,
+ optional unrestricted double tz = 0);
+ [NewObject] DOMMatrix scale(optional unrestricted double scaleX = 1,
+ optional unrestricted double scaleY,
+ optional unrestricted double scaleZ = 1,
+ optional unrestricted double originX = 0,
+ optional unrestricted double originY = 0,
+ optional unrestricted double originZ = 0);
+ [NewObject] DOMMatrix scaleNonUniform(optional unrestricted double scaleX = 1,
+ optional unrestricted double scaleY = 1);
+ [NewObject] DOMMatrix scale3d(optional unrestricted double scale = 1,
+ optional unrestricted double originX = 0,
+ optional unrestricted double originY = 0,
+ optional unrestricted double originZ = 0);
+ [NewObject] DOMMatrix rotate(optional unrestricted double rotX = 0,
+ optional unrestricted double rotY,
+ optional unrestricted double rotZ);
+ [NewObject] DOMMatrix rotateFromVector(optional unrestricted double x = 0,
+ optional unrestricted double y = 0);
+ [NewObject] DOMMatrix rotateAxisAngle(optional unrestricted double x = 0,
+ optional unrestricted double y = 0,
+ optional unrestricted double z = 0,
+ optional unrestricted double angle = 0);
+ [NewObject] DOMMatrix skewX(optional unrestricted double sx = 0);
+ [NewObject] DOMMatrix skewY(optional unrestricted double sy = 0);
+ [NewObject] DOMMatrix multiply(optional DOMMatrixInit other = {});
+ [NewObject] DOMMatrix flipX();
+ [NewObject] DOMMatrix flipY();
+ [NewObject] DOMMatrix inverse();
+
+ [NewObject] DOMPoint transformPoint(optional DOMPointInit point = {});
+ [NewObject] Float32Array toFloat32Array();
+ [NewObject] Float64Array toFloat64Array();
+
+ [Exposed=Window] stringifier;
+ [Default] object toJSON();
+};
+
+[Exposed=(Window,Worker),
+ Serializable,
+ LegacyWindowAlias=(SVGMatrix,WebKitCSSMatrix)]
+interface DOMMatrix : DOMMatrixReadOnly {
+ constructor(optional (DOMString or sequence<unrestricted double>) init);
+
+ [NewObject] static DOMMatrix fromMatrix(optional DOMMatrixInit other = {});
+ [NewObject] static DOMMatrix fromFloat32Array(Float32Array array32);
+ [NewObject] static DOMMatrix fromFloat64Array(Float64Array array64);
+
+ // These attributes are simple aliases for certain elements of the 4x4 matrix
+ inherit attribute unrestricted double a;
+ inherit attribute unrestricted double b;
+ inherit attribute unrestricted double c;
+ inherit attribute unrestricted double d;
+ inherit attribute unrestricted double e;
+ inherit attribute unrestricted double f;
+
+ inherit attribute unrestricted double m11;
+ inherit attribute unrestricted double m12;
+ inherit attribute unrestricted double m13;
+ inherit attribute unrestricted double m14;
+ inherit attribute unrestricted double m21;
+ inherit attribute unrestricted double m22;
+ inherit attribute unrestricted double m23;
+ inherit attribute unrestricted double m24;
+ inherit attribute unrestricted double m31;
+ inherit attribute unrestricted double m32;
+ inherit attribute unrestricted double m33;
+ inherit attribute unrestricted double m34;
+ inherit attribute unrestricted double m41;
+ inherit attribute unrestricted double m42;
+ inherit attribute unrestricted double m43;
+ inherit attribute unrestricted double m44;
+
+ // Mutable transform methods
+ DOMMatrix multiplySelf(optional DOMMatrixInit other = {});
+ DOMMatrix preMultiplySelf(optional DOMMatrixInit other = {});
+ DOMMatrix translateSelf(optional unrestricted double tx = 0,
+ optional unrestricted double ty = 0,
+ optional unrestricted double tz = 0);
+ DOMMatrix scaleSelf(optional unrestricted double scaleX = 1,
+ optional unrestricted double scaleY,
+ optional unrestricted double scaleZ = 1,
+ optional unrestricted double originX = 0,
+ optional unrestricted double originY = 0,
+ optional unrestricted double originZ = 0);
+ DOMMatrix scale3dSelf(optional unrestricted double scale = 1,
+ optional unrestricted double originX = 0,
+ optional unrestricted double originY = 0,
+ optional unrestricted double originZ = 0);
+ DOMMatrix rotateSelf(optional unrestricted double rotX = 0,
+ optional unrestricted double rotY,
+ optional unrestricted double rotZ);
+ DOMMatrix rotateFromVectorSelf(optional unrestricted double x = 0,
+ optional unrestricted double y = 0);
+ DOMMatrix rotateAxisAngleSelf(optional unrestricted double x = 0,
+ optional unrestricted double y = 0,
+ optional unrestricted double z = 0,
+ optional unrestricted double angle = 0);
+ DOMMatrix skewXSelf(optional unrestricted double sx = 0);
+ DOMMatrix skewYSelf(optional unrestricted double sy = 0);
+ DOMMatrix invertSelf();
+
+ [Exposed=Window] DOMMatrix setMatrixValue(DOMString transformList);
+};
+
+dictionary DOMMatrix2DInit {
+ unrestricted double a;
+ unrestricted double b;
+ unrestricted double c;
+ unrestricted double d;
+ unrestricted double e;
+ unrestricted double f;
+ unrestricted double m11;
+ unrestricted double m12;
+ unrestricted double m21;
+ unrestricted double m22;
+ unrestricted double m41;
+ unrestricted double m42;
+};
+
+dictionary DOMMatrixInit : DOMMatrix2DInit {
+ unrestricted double m13 = 0;
+ unrestricted double m14 = 0;
+ unrestricted double m23 = 0;
+ unrestricted double m24 = 0;
+ unrestricted double m31 = 0;
+ unrestricted double m32 = 0;
+ unrestricted double m33 = 1;
+ unrestricted double m34 = 0;
+ unrestricted double m43 = 0;
+ unrestricted double m44 = 1;
+ boolean is2D;
+};
diff --git a/test/wpt/tests/interfaces/get-installed-related-apps.idl b/test/wpt/tests/interfaces/get-installed-related-apps.idl
new file mode 100644
index 0000000..e096044
--- /dev/null
+++ b/test/wpt/tests/interfaces/get-installed-related-apps.idl
@@ -0,0 +1,16 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Get Installed Related Apps API (https://wicg.github.io/get-installed-related-apps/spec/)
+
+dictionary RelatedApplication {
+ required USVString platform;
+ USVString url;
+ DOMString id;
+ USVString version;
+};
+
+[Exposed=Window]
+partial interface Navigator {
+ [SecureContext] Promise<sequence<RelatedApplication>> getInstalledRelatedApps();
+};
diff --git a/test/wpt/tests/interfaces/gpc-spec.idl b/test/wpt/tests/interfaces/gpc-spec.idl
new file mode 100644
index 0000000..0e9a063
--- /dev/null
+++ b/test/wpt/tests/interfaces/gpc-spec.idl
@@ -0,0 +1,10 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Global Privacy Control (GPC) (https://privacycg.github.io/gpc-spec/)
+
+interface mixin GlobalPrivacyControl {
+ readonly attribute boolean globalPrivacyControl;
+};
+Navigator includes GlobalPrivacyControl;
+WorkerNavigator includes GlobalPrivacyControl;
diff --git a/test/wpt/tests/interfaces/gyroscope.idl b/test/wpt/tests/interfaces/gyroscope.idl
new file mode 100644
index 0000000..00fb0ef
--- /dev/null
+++ b/test/wpt/tests/interfaces/gyroscope.idl
@@ -0,0 +1,24 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Gyroscope (https://w3c.github.io/gyroscope/)
+
+[SecureContext, Exposed=Window]
+interface Gyroscope : Sensor {
+ constructor(optional GyroscopeSensorOptions sensorOptions = {});
+ readonly attribute double? x;
+ readonly attribute double? y;
+ readonly attribute double? z;
+};
+
+enum GyroscopeLocalCoordinateSystem { "device", "screen" };
+
+dictionary GyroscopeSensorOptions : SensorOptions {
+ GyroscopeLocalCoordinateSystem referenceFrame = "device";
+};
+
+dictionary GyroscopeReadingValues {
+ required double? x;
+ required double? y;
+ required double? z;
+};
diff --git a/test/wpt/tests/interfaces/hr-time.idl b/test/wpt/tests/interfaces/hr-time.idl
new file mode 100644
index 0000000..835ee8a
--- /dev/null
+++ b/test/wpt/tests/interfaces/hr-time.idl
@@ -0,0 +1,19 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: High Resolution Time (https://w3c.github.io/hr-time/)
+
+typedef double DOMHighResTimeStamp;
+
+typedef unsigned long long EpochTimeStamp;
+
+[Exposed=(Window,Worker)]
+interface Performance : EventTarget {
+ DOMHighResTimeStamp now();
+ readonly attribute DOMHighResTimeStamp timeOrigin;
+ [Default] object toJSON();
+};
+
+partial interface mixin WindowOrWorkerGlobalScope {
+ [Replaceable] readonly attribute Performance performance;
+};
diff --git a/test/wpt/tests/interfaces/html-media-capture.idl b/test/wpt/tests/interfaces/html-media-capture.idl
new file mode 100644
index 0000000..696dce6
--- /dev/null
+++ b/test/wpt/tests/interfaces/html-media-capture.idl
@@ -0,0 +1,8 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: HTML Media Capture (https://w3c.github.io/html-media-capture/)
+
+partial interface HTMLInputElement {
+ [CEReactions] attribute DOMString capture;
+};
diff --git a/test/wpt/tests/interfaces/html.idl b/test/wpt/tests/interfaces/html.idl
new file mode 100644
index 0000000..99b3370
--- /dev/null
+++ b/test/wpt/tests/interfaces/html.idl
@@ -0,0 +1,2725 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: HTML Standard (https://html.spec.whatwg.org/multipage/)
+
+[Exposed=Window,
+ LegacyUnenumerableNamedProperties]
+interface HTMLAllCollection {
+ readonly attribute unsigned long length;
+ getter Element (unsigned long index);
+ getter (HTMLCollection or Element)? namedItem(DOMString name);
+ (HTMLCollection or Element)? item(optional DOMString nameOrIndex);
+
+ // Note: HTMLAllCollection objects have a custom [[Call]] internal method and an [[IsHTMLDDA]] internal slot.
+};
+
+[Exposed=Window]
+interface HTMLFormControlsCollection : HTMLCollection {
+ // inherits length and item()
+ getter (RadioNodeList or Element)? namedItem(DOMString name); // shadows inherited namedItem()
+};
+
+[Exposed=Window]
+interface RadioNodeList : NodeList {
+ attribute DOMString value;
+};
+
+[Exposed=Window]
+interface HTMLOptionsCollection : HTMLCollection {
+ // inherits item(), namedItem()
+ [CEReactions] attribute unsigned long length; // shadows inherited length
+ [CEReactions] setter undefined (unsigned long index, HTMLOptionElement? option);
+ [CEReactions] undefined add((HTMLOptionElement or HTMLOptGroupElement) element, optional (HTMLElement or long)? before = null);
+ [CEReactions] undefined remove(long index);
+ attribute long selectedIndex;
+};
+
+[Exposed=(Window,Worker)]
+interface DOMStringList {
+ readonly attribute unsigned long length;
+ getter DOMString? item(unsigned long index);
+ boolean contains(DOMString string);
+};
+
+enum DocumentReadyState { "loading", "interactive", "complete" };
+enum DocumentVisibilityState { "visible", "hidden" };
+typedef (HTMLScriptElement or SVGScriptElement) HTMLOrSVGScriptElement;
+
+[LegacyOverrideBuiltIns]
+partial interface Document {
+ // resource metadata management
+ [PutForwards=href, LegacyUnforgeable] readonly attribute Location? location;
+ attribute USVString domain;
+ readonly attribute USVString referrer;
+ attribute USVString cookie;
+ readonly attribute DOMString lastModified;
+ readonly attribute DocumentReadyState readyState;
+
+ // DOM tree accessors
+ getter object (DOMString name);
+ [CEReactions] attribute DOMString title;
+ [CEReactions] attribute DOMString dir;
+ [CEReactions] attribute HTMLElement? body;
+ readonly attribute HTMLHeadElement? head;
+ [SameObject] readonly attribute HTMLCollection images;
+ [SameObject] readonly attribute HTMLCollection embeds;
+ [SameObject] readonly attribute HTMLCollection plugins;
+ [SameObject] readonly attribute HTMLCollection links;
+ [SameObject] readonly attribute HTMLCollection forms;
+ [SameObject] readonly attribute HTMLCollection scripts;
+ NodeList getElementsByName(DOMString elementName);
+ readonly attribute HTMLOrSVGScriptElement? currentScript; // classic scripts in a document tree only
+
+ // dynamic markup insertion
+ [CEReactions] Document open(optional DOMString unused1, optional DOMString unused2); // both arguments are ignored
+ WindowProxy? open(USVString url, DOMString name, DOMString features);
+ [CEReactions] undefined close();
+ [CEReactions] undefined write(DOMString... text);
+ [CEReactions] undefined writeln(DOMString... text);
+
+ // user interaction
+ readonly attribute WindowProxy? defaultView;
+ boolean hasFocus();
+ [CEReactions] attribute DOMString designMode;
+ [CEReactions] boolean execCommand(DOMString commandId, optional boolean showUI = false, optional DOMString value = "");
+ boolean queryCommandEnabled(DOMString commandId);
+ boolean queryCommandIndeterm(DOMString commandId);
+ boolean queryCommandState(DOMString commandId);
+ boolean queryCommandSupported(DOMString commandId);
+ DOMString queryCommandValue(DOMString commandId);
+ readonly attribute boolean hidden;
+ readonly attribute DocumentVisibilityState visibilityState;
+
+ // special event handler IDL attributes that only apply to Document objects
+ [LegacyLenientThis] attribute EventHandler onreadystatechange;
+ attribute EventHandler onvisibilitychange;
+
+ // also has obsolete members
+};
+Document includes GlobalEventHandlers;
+
+partial interface mixin DocumentOrShadowRoot {
+ readonly attribute Element? activeElement;
+};
+
+[Exposed=Window]
+interface HTMLElement : Element {
+ [HTMLConstructor] constructor();
+
+ // metadata attributes
+ [CEReactions] attribute DOMString title;
+ [CEReactions] attribute DOMString lang;
+ [CEReactions] attribute boolean translate;
+ [CEReactions] attribute DOMString dir;
+
+ // user interaction
+ [CEReactions] attribute (boolean or unrestricted double or DOMString)? hidden;
+ [CEReactions] attribute boolean inert;
+ undefined click();
+ [CEReactions] attribute DOMString accessKey;
+ readonly attribute DOMString accessKeyLabel;
+ [CEReactions] attribute boolean draggable;
+ [CEReactions] attribute boolean spellcheck;
+ [CEReactions] attribute DOMString autocapitalize;
+
+ [CEReactions] attribute [LegacyNullToEmptyString] DOMString innerText;
+ [CEReactions] attribute [LegacyNullToEmptyString] DOMString outerText;
+
+ ElementInternals attachInternals();
+
+ // The popover API
+ undefined showPopover();
+ undefined hidePopover();
+ undefined togglePopover(optional boolean force);
+ [CEReactions] attribute DOMString? popover;
+};
+
+HTMLElement includes GlobalEventHandlers;
+HTMLElement includes ElementContentEditable;
+HTMLElement includes HTMLOrSVGElement;
+
+[Exposed=Window]
+interface HTMLUnknownElement : HTMLElement {
+ // Note: intentionally no [HTMLConstructor]
+};
+
+interface mixin HTMLOrSVGElement {
+ [SameObject] readonly attribute DOMStringMap dataset;
+ attribute DOMString nonce; // intentionally no [CEReactions]
+
+ [CEReactions] attribute boolean autofocus;
+ [CEReactions] attribute long tabIndex;
+ undefined focus(optional FocusOptions options = {});
+ undefined blur();
+};
+
+[Exposed=Window,
+ LegacyOverrideBuiltIns]
+interface DOMStringMap {
+ getter DOMString (DOMString name);
+ [CEReactions] setter undefined (DOMString name, DOMString value);
+ [CEReactions] deleter undefined (DOMString name);
+};
+
+[Exposed=Window]
+interface HTMLHtmlElement : HTMLElement {
+ [HTMLConstructor] constructor();
+
+ // also has obsolete members
+};
+
+[Exposed=Window]
+interface HTMLHeadElement : HTMLElement {
+ [HTMLConstructor] constructor();
+};
+
+[Exposed=Window]
+interface HTMLTitleElement : HTMLElement {
+ [HTMLConstructor] constructor();
+
+ [CEReactions] attribute DOMString text;
+};
+
+[Exposed=Window]
+interface HTMLBaseElement : HTMLElement {
+ [HTMLConstructor] constructor();
+
+ [CEReactions] attribute USVString href;
+ [CEReactions] attribute DOMString target;
+};
+
+[Exposed=Window]
+interface HTMLLinkElement : HTMLElement {
+ [HTMLConstructor] constructor();
+
+ [CEReactions] attribute USVString href;
+ [CEReactions] attribute DOMString? crossOrigin;
+ [CEReactions] attribute DOMString rel;
+ [CEReactions] attribute DOMString as;
+ [SameObject, PutForwards=value] readonly attribute DOMTokenList relList;
+ [CEReactions] attribute DOMString media;
+ [CEReactions] attribute DOMString integrity;
+ [CEReactions] attribute DOMString hreflang;
+ [CEReactions] attribute DOMString type;
+ [SameObject, PutForwards=value] readonly attribute DOMTokenList sizes;
+ [CEReactions] attribute USVString imageSrcset;
+ [CEReactions] attribute DOMString imageSizes;
+ [CEReactions] attribute DOMString referrerPolicy;
+ [SameObject, PutForwards=value] readonly attribute DOMTokenList blocking;
+ [CEReactions] attribute boolean disabled;
+ [CEReactions] attribute DOMString fetchPriority;
+
+ // also has obsolete members
+};
+HTMLLinkElement includes LinkStyle;
+
+[Exposed=Window]
+interface HTMLMetaElement : HTMLElement {
+ [HTMLConstructor] constructor();
+
+ [CEReactions] attribute DOMString name;
+ [CEReactions] attribute DOMString httpEquiv;
+ [CEReactions] attribute DOMString content;
+ [CEReactions] attribute DOMString media;
+
+ // also has obsolete members
+};
+
+[Exposed=Window]
+interface HTMLStyleElement : HTMLElement {
+ [HTMLConstructor] constructor();
+
+ attribute boolean disabled;
+ [CEReactions] attribute DOMString media;
+ [SameObject, PutForwards=value] readonly attribute DOMTokenList blocking;
+
+ // also has obsolete members
+};
+HTMLStyleElement includes LinkStyle;
+
+[Exposed=Window]
+interface HTMLBodyElement : HTMLElement {
+ [HTMLConstructor] constructor();
+
+ // also has obsolete members
+};
+
+HTMLBodyElement includes WindowEventHandlers;
+
+[Exposed=Window]
+interface HTMLHeadingElement : HTMLElement {
+ [HTMLConstructor] constructor();
+
+ // also has obsolete members
+};
+
+[Exposed=Window]
+interface HTMLParagraphElement : HTMLElement {
+ [HTMLConstructor] constructor();
+
+ // also has obsolete members
+};
+
+[Exposed=Window]
+interface HTMLHRElement : HTMLElement {
+ [HTMLConstructor] constructor();
+
+ // also has obsolete members
+};
+
+[Exposed=Window]
+interface HTMLPreElement : HTMLElement {
+ [HTMLConstructor] constructor();
+
+ // also has obsolete members
+};
+
+[Exposed=Window]
+interface HTMLQuoteElement : HTMLElement {
+ [HTMLConstructor] constructor();
+
+ [CEReactions] attribute USVString cite;
+};
+
+[Exposed=Window]
+interface HTMLOListElement : HTMLElement {
+ [HTMLConstructor] constructor();
+
+ [CEReactions] attribute boolean reversed;
+ [CEReactions] attribute long start;
+ [CEReactions] attribute DOMString type;
+
+ // also has obsolete members
+};
+
+[Exposed=Window]
+interface HTMLUListElement : HTMLElement {
+ [HTMLConstructor] constructor();
+
+ // also has obsolete members
+};
+
+[Exposed=Window]
+interface HTMLMenuElement : HTMLElement {
+ [HTMLConstructor] constructor();
+
+ // also has obsolete members
+};
+
+[Exposed=Window]
+interface HTMLLIElement : HTMLElement {
+ [HTMLConstructor] constructor();
+
+ [CEReactions] attribute long value;
+
+ // also has obsolete members
+};
+
+[Exposed=Window]
+interface HTMLDListElement : HTMLElement {
+ [HTMLConstructor] constructor();
+
+ // also has obsolete members
+};
+
+[Exposed=Window]
+interface HTMLDivElement : HTMLElement {
+ [HTMLConstructor] constructor();
+
+ // also has obsolete members
+};
+
+[Exposed=Window]
+interface HTMLAnchorElement : HTMLElement {
+ [HTMLConstructor] constructor();
+
+ [CEReactions] attribute DOMString target;
+ [CEReactions] attribute DOMString download;
+ [CEReactions] attribute USVString ping;
+ [CEReactions] attribute DOMString rel;
+ [SameObject, PutForwards=value] readonly attribute DOMTokenList relList;
+ [CEReactions] attribute DOMString hreflang;
+ [CEReactions] attribute DOMString type;
+
+ [CEReactions] attribute DOMString text;
+
+ [CEReactions] attribute DOMString referrerPolicy;
+
+ // also has obsolete members
+};
+HTMLAnchorElement includes HTMLHyperlinkElementUtils;
+
+[Exposed=Window]
+interface HTMLDataElement : HTMLElement {
+ [HTMLConstructor] constructor();
+
+ [CEReactions] attribute DOMString value;
+};
+
+[Exposed=Window]
+interface HTMLTimeElement : HTMLElement {
+ [HTMLConstructor] constructor();
+
+ [CEReactions] attribute DOMString dateTime;
+};
+
+[Exposed=Window]
+interface HTMLSpanElement : HTMLElement {
+ [HTMLConstructor] constructor();
+};
+
+[Exposed=Window]
+interface HTMLBRElement : HTMLElement {
+ [HTMLConstructor] constructor();
+
+ // also has obsolete members
+};
+
+interface mixin HTMLHyperlinkElementUtils {
+ [CEReactions] stringifier attribute USVString href;
+ readonly attribute USVString origin;
+ [CEReactions] attribute USVString protocol;
+ [CEReactions] attribute USVString username;
+ [CEReactions] attribute USVString password;
+ [CEReactions] attribute USVString host;
+ [CEReactions] attribute USVString hostname;
+ [CEReactions] attribute USVString port;
+ [CEReactions] attribute USVString pathname;
+ [CEReactions] attribute USVString search;
+ [CEReactions] attribute USVString hash;
+};
+
+[Exposed=Window]
+interface HTMLModElement : HTMLElement {
+ [HTMLConstructor] constructor();
+
+ [CEReactions] attribute USVString cite;
+ [CEReactions] attribute DOMString dateTime;
+};
+
+[Exposed=Window]
+interface HTMLPictureElement : HTMLElement {
+ [HTMLConstructor] constructor();
+};
+
+[Exposed=Window]
+interface HTMLSourceElement : HTMLElement {
+ [HTMLConstructor] constructor();
+
+ [CEReactions] attribute USVString src;
+ [CEReactions] attribute DOMString type;
+ [CEReactions] attribute USVString srcset;
+ [CEReactions] attribute DOMString sizes;
+ [CEReactions] attribute DOMString media;
+ [CEReactions] attribute unsigned long width;
+ [CEReactions] attribute unsigned long height;
+};
+
+[Exposed=Window,
+ LegacyFactoryFunction=Image(optional unsigned long width, optional unsigned long height)]
+interface HTMLImageElement : HTMLElement {
+ [HTMLConstructor] constructor();
+
+ [CEReactions] attribute DOMString alt;
+ [CEReactions] attribute USVString src;
+ [CEReactions] attribute USVString srcset;
+ [CEReactions] attribute DOMString sizes;
+ [CEReactions] attribute DOMString? crossOrigin;
+ [CEReactions] attribute DOMString useMap;
+ [CEReactions] attribute boolean isMap;
+ [CEReactions] attribute unsigned long width;
+ [CEReactions] attribute unsigned long height;
+ readonly attribute unsigned long naturalWidth;
+ readonly attribute unsigned long naturalHeight;
+ readonly attribute boolean complete;
+ readonly attribute USVString currentSrc;
+ [CEReactions] attribute DOMString referrerPolicy;
+ [CEReactions] attribute DOMString decoding;
+ [CEReactions] attribute DOMString loading;
+ [CEReactions] attribute DOMString fetchPriority;
+
+ Promise<undefined> decode();
+
+ // also has obsolete members
+};
+
+[Exposed=Window]
+interface HTMLIFrameElement : HTMLElement {
+ [HTMLConstructor] constructor();
+
+ [CEReactions] attribute USVString src;
+ [CEReactions] attribute DOMString srcdoc;
+ [CEReactions] attribute DOMString name;
+ [SameObject, PutForwards=value] readonly attribute DOMTokenList sandbox;
+ [CEReactions] attribute DOMString allow;
+ [CEReactions] attribute boolean allowFullscreen;
+ [CEReactions] attribute DOMString width;
+ [CEReactions] attribute DOMString height;
+ [CEReactions] attribute DOMString referrerPolicy;
+ [CEReactions] attribute DOMString loading;
+ readonly attribute Document? contentDocument;
+ readonly attribute WindowProxy? contentWindow;
+ Document? getSVGDocument();
+
+ // also has obsolete members
+};
+
+[Exposed=Window]
+interface HTMLEmbedElement : HTMLElement {
+ [HTMLConstructor] constructor();
+
+ [CEReactions] attribute USVString src;
+ [CEReactions] attribute DOMString type;
+ [CEReactions] attribute DOMString width;
+ [CEReactions] attribute DOMString height;
+ Document? getSVGDocument();
+
+ // also has obsolete members
+};
+
+[Exposed=Window]
+interface HTMLObjectElement : HTMLElement {
+ [HTMLConstructor] constructor();
+
+ [CEReactions] attribute USVString data;
+ [CEReactions] attribute DOMString type;
+ [CEReactions] attribute DOMString name;
+ readonly attribute HTMLFormElement? form;
+ [CEReactions] attribute DOMString width;
+ [CEReactions] attribute DOMString height;
+ readonly attribute Document? contentDocument;
+ readonly attribute WindowProxy? contentWindow;
+ Document? getSVGDocument();
+
+ readonly attribute boolean willValidate;
+ readonly attribute ValidityState validity;
+ readonly attribute DOMString validationMessage;
+ boolean checkValidity();
+ boolean reportValidity();
+ undefined setCustomValidity(DOMString error);
+
+ // also has obsolete members
+};
+
+[Exposed=Window]
+interface HTMLVideoElement : HTMLMediaElement {
+ [HTMLConstructor] constructor();
+
+ [CEReactions] attribute unsigned long width;
+ [CEReactions] attribute unsigned long height;
+ readonly attribute unsigned long videoWidth;
+ readonly attribute unsigned long videoHeight;
+ [CEReactions] attribute USVString poster;
+ [CEReactions] attribute boolean playsInline;
+};
+
+[Exposed=Window,
+ LegacyFactoryFunction=Audio(optional DOMString src)]
+interface HTMLAudioElement : HTMLMediaElement {
+ [HTMLConstructor] constructor();
+};
+
+[Exposed=Window]
+interface HTMLTrackElement : HTMLElement {
+ [HTMLConstructor] constructor();
+
+ [CEReactions] attribute DOMString kind;
+ [CEReactions] attribute USVString src;
+ [CEReactions] attribute DOMString srclang;
+ [CEReactions] attribute DOMString label;
+ [CEReactions] attribute boolean default;
+
+ const unsigned short NONE = 0;
+ const unsigned short LOADING = 1;
+ const unsigned short LOADED = 2;
+ const unsigned short ERROR = 3;
+ readonly attribute unsigned short readyState;
+
+ readonly attribute TextTrack track;
+};
+
+enum CanPlayTypeResult { "" /* empty string */, "maybe", "probably" };
+typedef (MediaStream or MediaSource or Blob) MediaProvider;
+
+[Exposed=Window]
+interface HTMLMediaElement : HTMLElement {
+
+ // error state
+ readonly attribute MediaError? error;
+
+ // network state
+ [CEReactions] attribute USVString src;
+ attribute MediaProvider? srcObject;
+ readonly attribute USVString currentSrc;
+ [CEReactions] attribute DOMString? crossOrigin;
+ const unsigned short NETWORK_EMPTY = 0;
+ const unsigned short NETWORK_IDLE = 1;
+ const unsigned short NETWORK_LOADING = 2;
+ const unsigned short NETWORK_NO_SOURCE = 3;
+ readonly attribute unsigned short networkState;
+ [CEReactions] attribute DOMString preload;
+ readonly attribute TimeRanges buffered;
+ undefined load();
+ CanPlayTypeResult canPlayType(DOMString type);
+
+ // ready state
+ const unsigned short HAVE_NOTHING = 0;
+ const unsigned short HAVE_METADATA = 1;
+ const unsigned short HAVE_CURRENT_DATA = 2;
+ const unsigned short HAVE_FUTURE_DATA = 3;
+ const unsigned short HAVE_ENOUGH_DATA = 4;
+ readonly attribute unsigned short readyState;
+ readonly attribute boolean seeking;
+
+ // playback state
+ attribute double currentTime;
+ undefined fastSeek(double time);
+ readonly attribute unrestricted double duration;
+ object getStartDate();
+ readonly attribute boolean paused;
+ attribute double defaultPlaybackRate;
+ attribute double playbackRate;
+ attribute boolean preservesPitch;
+ readonly attribute TimeRanges played;
+ readonly attribute TimeRanges seekable;
+ readonly attribute boolean ended;
+ [CEReactions] attribute boolean autoplay;
+ [CEReactions] attribute boolean loop;
+ Promise<undefined> play();
+ undefined pause();
+
+ // controls
+ [CEReactions] attribute boolean controls;
+ attribute double volume;
+ attribute boolean muted;
+ [CEReactions] attribute boolean defaultMuted;
+
+ // tracks
+ [SameObject] readonly attribute AudioTrackList audioTracks;
+ [SameObject] readonly attribute VideoTrackList videoTracks;
+ [SameObject] readonly attribute TextTrackList textTracks;
+ TextTrack addTextTrack(TextTrackKind kind, optional DOMString label = "", optional DOMString language = "");
+};
+
+[Exposed=Window]
+interface MediaError {
+ const unsigned short MEDIA_ERR_ABORTED = 1;
+ const unsigned short MEDIA_ERR_NETWORK = 2;
+ const unsigned short MEDIA_ERR_DECODE = 3;
+ const unsigned short MEDIA_ERR_SRC_NOT_SUPPORTED = 4;
+
+ readonly attribute unsigned short code;
+ readonly attribute DOMString message;
+};
+
+[Exposed=Window]
+interface AudioTrackList : EventTarget {
+ readonly attribute unsigned long length;
+ getter AudioTrack (unsigned long index);
+ AudioTrack? getTrackById(DOMString id);
+
+ attribute EventHandler onchange;
+ attribute EventHandler onaddtrack;
+ attribute EventHandler onremovetrack;
+};
+
+[Exposed=Window]
+interface AudioTrack {
+ readonly attribute DOMString id;
+ readonly attribute DOMString kind;
+ readonly attribute DOMString label;
+ readonly attribute DOMString language;
+ attribute boolean enabled;
+};
+
+[Exposed=Window]
+interface VideoTrackList : EventTarget {
+ readonly attribute unsigned long length;
+ getter VideoTrack (unsigned long index);
+ VideoTrack? getTrackById(DOMString id);
+ readonly attribute long selectedIndex;
+
+ attribute EventHandler onchange;
+ attribute EventHandler onaddtrack;
+ attribute EventHandler onremovetrack;
+};
+
+[Exposed=Window]
+interface VideoTrack {
+ readonly attribute DOMString id;
+ readonly attribute DOMString kind;
+ readonly attribute DOMString label;
+ readonly attribute DOMString language;
+ attribute boolean selected;
+};
+
+[Exposed=Window]
+interface TextTrackList : EventTarget {
+ readonly attribute unsigned long length;
+ getter TextTrack (unsigned long index);
+ TextTrack? getTrackById(DOMString id);
+
+ attribute EventHandler onchange;
+ attribute EventHandler onaddtrack;
+ attribute EventHandler onremovetrack;
+};
+
+enum TextTrackMode { "disabled", "hidden", "showing" };
+enum TextTrackKind { "subtitles", "captions", "descriptions", "chapters", "metadata" };
+
+[Exposed=Window]
+interface TextTrack : EventTarget {
+ readonly attribute TextTrackKind kind;
+ readonly attribute DOMString label;
+ readonly attribute DOMString language;
+
+ readonly attribute DOMString id;
+ readonly attribute DOMString inBandMetadataTrackDispatchType;
+
+ attribute TextTrackMode mode;
+
+ readonly attribute TextTrackCueList? cues;
+ readonly attribute TextTrackCueList? activeCues;
+
+ undefined addCue(TextTrackCue cue);
+ undefined removeCue(TextTrackCue cue);
+
+ attribute EventHandler oncuechange;
+};
+
+[Exposed=Window]
+interface TextTrackCueList {
+ readonly attribute unsigned long length;
+ getter TextTrackCue (unsigned long index);
+ TextTrackCue? getCueById(DOMString id);
+};
+
+[Exposed=Window]
+interface TextTrackCue : EventTarget {
+ readonly attribute TextTrack? track;
+
+ attribute DOMString id;
+ attribute double startTime;
+ attribute unrestricted double endTime;
+ attribute boolean pauseOnExit;
+
+ attribute EventHandler onenter;
+ attribute EventHandler onexit;
+};
+
+[Exposed=Window]
+interface TimeRanges {
+ readonly attribute unsigned long length;
+ double start(unsigned long index);
+ double end(unsigned long index);
+};
+
+[Exposed=Window]
+interface TrackEvent : Event {
+ constructor(DOMString type, optional TrackEventInit eventInitDict = {});
+
+ readonly attribute (VideoTrack or AudioTrack or TextTrack)? track;
+};
+
+dictionary TrackEventInit : EventInit {
+ (VideoTrack or AudioTrack or TextTrack)? track = null;
+};
+
+[Exposed=Window]
+interface HTMLMapElement : HTMLElement {
+ [HTMLConstructor] constructor();
+
+ [CEReactions] attribute DOMString name;
+ [SameObject] readonly attribute HTMLCollection areas;
+};
+
+[Exposed=Window]
+interface HTMLAreaElement : HTMLElement {
+ [HTMLConstructor] constructor();
+
+ [CEReactions] attribute DOMString alt;
+ [CEReactions] attribute DOMString coords;
+ [CEReactions] attribute DOMString shape;
+ [CEReactions] attribute DOMString target;
+ [CEReactions] attribute DOMString download;
+ [CEReactions] attribute USVString ping;
+ [CEReactions] attribute DOMString rel;
+ [SameObject, PutForwards=value] readonly attribute DOMTokenList relList;
+ [CEReactions] attribute DOMString referrerPolicy;
+
+ // also has obsolete members
+};
+HTMLAreaElement includes HTMLHyperlinkElementUtils;
+
+[Exposed=Window]
+interface HTMLTableElement : HTMLElement {
+ [HTMLConstructor] constructor();
+
+ [CEReactions] attribute HTMLTableCaptionElement? caption;
+ HTMLTableCaptionElement createCaption();
+ [CEReactions] undefined deleteCaption();
+
+ [CEReactions] attribute HTMLTableSectionElement? tHead;
+ HTMLTableSectionElement createTHead();
+ [CEReactions] undefined deleteTHead();
+
+ [CEReactions] attribute HTMLTableSectionElement? tFoot;
+ HTMLTableSectionElement createTFoot();
+ [CEReactions] undefined deleteTFoot();
+
+ [SameObject] readonly attribute HTMLCollection tBodies;
+ HTMLTableSectionElement createTBody();
+
+ [SameObject] readonly attribute HTMLCollection rows;
+ HTMLTableRowElement insertRow(optional long index = -1);
+ [CEReactions] undefined deleteRow(long index);
+
+ // also has obsolete members
+};
+
+[Exposed=Window]
+interface HTMLTableCaptionElement : HTMLElement {
+ [HTMLConstructor] constructor();
+
+ // also has obsolete members
+};
+
+[Exposed=Window]
+interface HTMLTableColElement : HTMLElement {
+ [HTMLConstructor] constructor();
+
+ [CEReactions] attribute unsigned long span;
+
+ // also has obsolete members
+};
+
+[Exposed=Window]
+interface HTMLTableSectionElement : HTMLElement {
+ [HTMLConstructor] constructor();
+
+ [SameObject] readonly attribute HTMLCollection rows;
+ HTMLTableRowElement insertRow(optional long index = -1);
+ [CEReactions] undefined deleteRow(long index);
+
+ // also has obsolete members
+};
+
+[Exposed=Window]
+interface HTMLTableRowElement : HTMLElement {
+ [HTMLConstructor] constructor();
+
+ readonly attribute long rowIndex;
+ readonly attribute long sectionRowIndex;
+ [SameObject] readonly attribute HTMLCollection cells;
+ HTMLTableCellElement insertCell(optional long index = -1);
+ [CEReactions] undefined deleteCell(long index);
+
+ // also has obsolete members
+};
+
+[Exposed=Window]
+interface HTMLTableCellElement : HTMLElement {
+ [HTMLConstructor] constructor();
+
+ [CEReactions] attribute unsigned long colSpan;
+ [CEReactions] attribute unsigned long rowSpan;
+ [CEReactions] attribute DOMString headers;
+ readonly attribute long cellIndex;
+
+ [CEReactions] attribute DOMString scope; // only conforming for th elements
+ [CEReactions] attribute DOMString abbr; // only conforming for th elements
+
+ // also has obsolete members
+};
+
+[Exposed=Window,
+ LegacyOverrideBuiltIns,
+ LegacyUnenumerableNamedProperties]
+interface HTMLFormElement : HTMLElement {
+ [HTMLConstructor] constructor();
+
+ [CEReactions] attribute DOMString acceptCharset;
+ [CEReactions] attribute USVString action;
+ [CEReactions] attribute DOMString autocomplete;
+ [CEReactions] attribute DOMString enctype;
+ [CEReactions] attribute DOMString encoding;
+ [CEReactions] attribute DOMString method;
+ [CEReactions] attribute DOMString name;
+ [CEReactions] attribute boolean noValidate;
+ [CEReactions] attribute DOMString target;
+ [CEReactions] attribute DOMString rel;
+ [SameObject, PutForwards=value] readonly attribute DOMTokenList relList;
+
+ [SameObject] readonly attribute HTMLFormControlsCollection elements;
+ readonly attribute unsigned long length;
+ getter Element (unsigned long index);
+ getter (RadioNodeList or Element) (DOMString name);
+
+ undefined submit();
+ undefined requestSubmit(optional HTMLElement? submitter = null);
+ [CEReactions] undefined reset();
+ boolean checkValidity();
+ boolean reportValidity();
+};
+
+[Exposed=Window]
+interface HTMLLabelElement : HTMLElement {
+ [HTMLConstructor] constructor();
+
+ readonly attribute HTMLFormElement? form;
+ [CEReactions] attribute DOMString htmlFor;
+ readonly attribute HTMLElement? control;
+};
+
+[Exposed=Window]
+interface HTMLInputElement : HTMLElement {
+ [HTMLConstructor] constructor();
+
+ [CEReactions] attribute DOMString accept;
+ [CEReactions] attribute DOMString alt;
+ [CEReactions] attribute DOMString autocomplete;
+ [CEReactions] attribute boolean defaultChecked;
+ attribute boolean checked;
+ [CEReactions] attribute DOMString dirName;
+ [CEReactions] attribute boolean disabled;
+ readonly attribute HTMLFormElement? form;
+ attribute FileList? files;
+ [CEReactions] attribute USVString formAction;
+ [CEReactions] attribute DOMString formEnctype;
+ [CEReactions] attribute DOMString formMethod;
+ [CEReactions] attribute boolean formNoValidate;
+ [CEReactions] attribute DOMString formTarget;
+ [CEReactions] attribute unsigned long height;
+ attribute boolean indeterminate;
+ readonly attribute HTMLDataListElement? list;
+ [CEReactions] attribute DOMString max;
+ [CEReactions] attribute long maxLength;
+ [CEReactions] attribute DOMString min;
+ [CEReactions] attribute long minLength;
+ [CEReactions] attribute boolean multiple;
+ [CEReactions] attribute DOMString name;
+ [CEReactions] attribute DOMString pattern;
+ [CEReactions] attribute DOMString placeholder;
+ [CEReactions] attribute boolean readOnly;
+ [CEReactions] attribute boolean required;
+ [CEReactions] attribute unsigned long size;
+ [CEReactions] attribute USVString src;
+ [CEReactions] attribute DOMString step;
+ [CEReactions] attribute DOMString type;
+ [CEReactions] attribute DOMString defaultValue;
+ [CEReactions] attribute [LegacyNullToEmptyString] DOMString value;
+ attribute object? valueAsDate;
+ attribute unrestricted double valueAsNumber;
+ [CEReactions] attribute unsigned long width;
+
+ undefined stepUp(optional long n = 1);
+ undefined stepDown(optional long n = 1);
+
+ readonly attribute boolean willValidate;
+ readonly attribute ValidityState validity;
+ readonly attribute DOMString validationMessage;
+ boolean checkValidity();
+ boolean reportValidity();
+ undefined setCustomValidity(DOMString error);
+
+ readonly attribute NodeList? labels;
+
+ undefined select();
+ attribute unsigned long? selectionStart;
+ attribute unsigned long? selectionEnd;
+ attribute DOMString? selectionDirection;
+ undefined setRangeText(DOMString replacement);
+ undefined setRangeText(DOMString replacement, unsigned long start, unsigned long end, optional SelectionMode selectionMode = "preserve");
+ undefined setSelectionRange(unsigned long start, unsigned long end, optional DOMString direction);
+
+ undefined showPicker();
+
+ // also has obsolete members
+};
+HTMLInputElement includes PopoverInvokerElement;
+
+[Exposed=Window]
+interface HTMLButtonElement : HTMLElement {
+ [HTMLConstructor] constructor();
+
+ [CEReactions] attribute boolean disabled;
+ readonly attribute HTMLFormElement? form;
+ [CEReactions] attribute USVString formAction;
+ [CEReactions] attribute DOMString formEnctype;
+ [CEReactions] attribute DOMString formMethod;
+ [CEReactions] attribute boolean formNoValidate;
+ [CEReactions] attribute DOMString formTarget;
+ [CEReactions] attribute DOMString name;
+ [CEReactions] attribute DOMString type;
+ [CEReactions] attribute DOMString value;
+
+ readonly attribute boolean willValidate;
+ readonly attribute ValidityState validity;
+ readonly attribute DOMString validationMessage;
+ boolean checkValidity();
+ boolean reportValidity();
+ undefined setCustomValidity(DOMString error);
+
+ readonly attribute NodeList labels;
+};
+HTMLButtonElement includes PopoverInvokerElement;
+
+[Exposed=Window]
+interface HTMLSelectElement : HTMLElement {
+ [HTMLConstructor] constructor();
+
+ [CEReactions] attribute DOMString autocomplete;
+ [CEReactions] attribute boolean disabled;
+ readonly attribute HTMLFormElement? form;
+ [CEReactions] attribute boolean multiple;
+ [CEReactions] attribute DOMString name;
+ [CEReactions] attribute boolean required;
+ [CEReactions] attribute unsigned long size;
+
+ readonly attribute DOMString type;
+
+ [SameObject] readonly attribute HTMLOptionsCollection options;
+ [CEReactions] attribute unsigned long length;
+ getter HTMLOptionElement? item(unsigned long index);
+ HTMLOptionElement? namedItem(DOMString name);
+ [CEReactions] undefined add((HTMLOptionElement or HTMLOptGroupElement) element, optional (HTMLElement or long)? before = null);
+ [CEReactions] undefined remove(); // ChildNode overload
+ [CEReactions] undefined remove(long index);
+ [CEReactions] setter undefined (unsigned long index, HTMLOptionElement? option);
+
+ [SameObject] readonly attribute HTMLCollection selectedOptions;
+ attribute long selectedIndex;
+ attribute DOMString value;
+
+ readonly attribute boolean willValidate;
+ readonly attribute ValidityState validity;
+ readonly attribute DOMString validationMessage;
+ boolean checkValidity();
+ boolean reportValidity();
+ undefined setCustomValidity(DOMString error);
+
+ readonly attribute NodeList labels;
+};
+
+[Exposed=Window]
+interface HTMLDataListElement : HTMLElement {
+ [HTMLConstructor] constructor();
+
+ [SameObject] readonly attribute HTMLCollection options;
+};
+
+[Exposed=Window]
+interface HTMLOptGroupElement : HTMLElement {
+ [HTMLConstructor] constructor();
+
+ [CEReactions] attribute boolean disabled;
+ [CEReactions] attribute DOMString label;
+};
+
+[Exposed=Window,
+ LegacyFactoryFunction=Option(optional DOMString text = "", optional DOMString value, optional boolean defaultSelected = false, optional boolean selected = false)]
+interface HTMLOptionElement : HTMLElement {
+ [HTMLConstructor] constructor();
+
+ [CEReactions] attribute boolean disabled;
+ readonly attribute HTMLFormElement? form;
+ [CEReactions] attribute DOMString label;
+ [CEReactions] attribute boolean defaultSelected;
+ attribute boolean selected;
+ [CEReactions] attribute DOMString value;
+
+ [CEReactions] attribute DOMString text;
+ readonly attribute long index;
+};
+
+[Exposed=Window]
+interface HTMLTextAreaElement : HTMLElement {
+ [HTMLConstructor] constructor();
+
+ [CEReactions] attribute DOMString autocomplete;
+ [CEReactions] attribute unsigned long cols;
+ [CEReactions] attribute DOMString dirName;
+ [CEReactions] attribute boolean disabled;
+ readonly attribute HTMLFormElement? form;
+ [CEReactions] attribute long maxLength;
+ [CEReactions] attribute long minLength;
+ [CEReactions] attribute DOMString name;
+ [CEReactions] attribute DOMString placeholder;
+ [CEReactions] attribute boolean readOnly;
+ [CEReactions] attribute boolean required;
+ [CEReactions] attribute unsigned long rows;
+ [CEReactions] attribute DOMString wrap;
+
+ readonly attribute DOMString type;
+ [CEReactions] attribute DOMString defaultValue;
+ attribute [LegacyNullToEmptyString] DOMString value;
+ readonly attribute unsigned long textLength;
+
+ readonly attribute boolean willValidate;
+ readonly attribute ValidityState validity;
+ readonly attribute DOMString validationMessage;
+ boolean checkValidity();
+ boolean reportValidity();
+ undefined setCustomValidity(DOMString error);
+
+ readonly attribute NodeList labels;
+
+ undefined select();
+ attribute unsigned long selectionStart;
+ attribute unsigned long selectionEnd;
+ attribute DOMString selectionDirection;
+ undefined setRangeText(DOMString replacement);
+ undefined setRangeText(DOMString replacement, unsigned long start, unsigned long end, optional SelectionMode selectionMode = "preserve");
+ undefined setSelectionRange(unsigned long start, unsigned long end, optional DOMString direction);
+};
+
+[Exposed=Window]
+interface HTMLOutputElement : HTMLElement {
+ [HTMLConstructor] constructor();
+
+ [SameObject, PutForwards=value] readonly attribute DOMTokenList htmlFor;
+ readonly attribute HTMLFormElement? form;
+ [CEReactions] attribute DOMString name;
+
+ readonly attribute DOMString type;
+ [CEReactions] attribute DOMString defaultValue;
+ [CEReactions] attribute DOMString value;
+
+ readonly attribute boolean willValidate;
+ readonly attribute ValidityState validity;
+ readonly attribute DOMString validationMessage;
+ boolean checkValidity();
+ boolean reportValidity();
+ undefined setCustomValidity(DOMString error);
+
+ readonly attribute NodeList labels;
+};
+
+[Exposed=Window]
+interface HTMLProgressElement : HTMLElement {
+ [HTMLConstructor] constructor();
+
+ [CEReactions] attribute double value;
+ [CEReactions] attribute double max;
+ readonly attribute double position;
+ readonly attribute NodeList labels;
+};
+
+[Exposed=Window]
+interface HTMLMeterElement : HTMLElement {
+ [HTMLConstructor] constructor();
+
+ [CEReactions] attribute double value;
+ [CEReactions] attribute double min;
+ [CEReactions] attribute double max;
+ [CEReactions] attribute double low;
+ [CEReactions] attribute double high;
+ [CEReactions] attribute double optimum;
+ readonly attribute NodeList labels;
+};
+
+[Exposed=Window]
+interface HTMLFieldSetElement : HTMLElement {
+ [HTMLConstructor] constructor();
+
+ [CEReactions] attribute boolean disabled;
+ readonly attribute HTMLFormElement? form;
+ [CEReactions] attribute DOMString name;
+
+ readonly attribute DOMString type;
+
+ [SameObject] readonly attribute HTMLCollection elements;
+
+ readonly attribute boolean willValidate;
+ [SameObject] readonly attribute ValidityState validity;
+ readonly attribute DOMString validationMessage;
+ boolean checkValidity();
+ boolean reportValidity();
+ undefined setCustomValidity(DOMString error);
+};
+
+[Exposed=Window]
+interface HTMLLegendElement : HTMLElement {
+ [HTMLConstructor] constructor();
+
+ readonly attribute HTMLFormElement? form;
+
+ // also has obsolete members
+};
+
+enum SelectionMode {
+ "select",
+ "start",
+ "end",
+ "preserve" // default
+};
+
+[Exposed=Window]
+interface ValidityState {
+ readonly attribute boolean valueMissing;
+ readonly attribute boolean typeMismatch;
+ readonly attribute boolean patternMismatch;
+ readonly attribute boolean tooLong;
+ readonly attribute boolean tooShort;
+ readonly attribute boolean rangeUnderflow;
+ readonly attribute boolean rangeOverflow;
+ readonly attribute boolean stepMismatch;
+ readonly attribute boolean badInput;
+ readonly attribute boolean customError;
+ readonly attribute boolean valid;
+};
+
+[Exposed=Window]
+interface SubmitEvent : Event {
+ constructor(DOMString type, optional SubmitEventInit eventInitDict = {});
+
+ readonly attribute HTMLElement? submitter;
+};
+
+dictionary SubmitEventInit : EventInit {
+ HTMLElement? submitter = null;
+};
+
+[Exposed=Window]
+interface FormDataEvent : Event {
+ constructor(DOMString type, FormDataEventInit eventInitDict);
+
+ readonly attribute FormData formData;
+};
+
+dictionary FormDataEventInit : EventInit {
+ required FormData formData;
+};
+
+[Exposed=Window]
+interface HTMLDetailsElement : HTMLElement {
+ [HTMLConstructor] constructor();
+
+ [CEReactions] attribute boolean open;
+};
+
+[Exposed=Window]
+interface HTMLDialogElement : HTMLElement {
+ [HTMLConstructor] constructor();
+
+ [CEReactions] attribute boolean open;
+ attribute DOMString returnValue;
+ [CEReactions] undefined show();
+ [CEReactions] undefined showModal();
+ [CEReactions] undefined close(optional DOMString returnValue);
+};
+
+[Exposed=Window]
+interface HTMLScriptElement : HTMLElement {
+ [HTMLConstructor] constructor();
+
+ [CEReactions] attribute USVString src;
+ [CEReactions] attribute DOMString type;
+ [CEReactions] attribute boolean noModule;
+ [CEReactions] attribute boolean async;
+ [CEReactions] attribute boolean defer;
+ [CEReactions] attribute DOMString? crossOrigin;
+ [CEReactions] attribute DOMString text;
+ [CEReactions] attribute DOMString integrity;
+ [CEReactions] attribute DOMString referrerPolicy;
+ [SameObject, PutForwards=value] readonly attribute DOMTokenList blocking;
+ [CEReactions] attribute DOMString fetchPriority;
+
+ static boolean supports(DOMString type);
+
+ // also has obsolete members
+};
+
+[Exposed=Window]
+interface HTMLTemplateElement : HTMLElement {
+ [HTMLConstructor] constructor();
+
+ readonly attribute DocumentFragment content;
+};
+
+[Exposed=Window]
+interface HTMLSlotElement : HTMLElement {
+ [HTMLConstructor] constructor();
+
+ [CEReactions] attribute DOMString name;
+ sequence<Node> assignedNodes(optional AssignedNodesOptions options = {});
+ sequence<Element> assignedElements(optional AssignedNodesOptions options = {});
+ undefined assign((Element or Text)... nodes);
+};
+
+dictionary AssignedNodesOptions {
+ boolean flatten = false;
+};
+
+typedef (CanvasRenderingContext2D or ImageBitmapRenderingContext or WebGLRenderingContext or WebGL2RenderingContext or GPUCanvasContext) RenderingContext;
+
+[Exposed=Window]
+interface HTMLCanvasElement : HTMLElement {
+ [HTMLConstructor] constructor();
+
+ [CEReactions] attribute unsigned long width;
+ [CEReactions] attribute unsigned long height;
+
+ RenderingContext? getContext(DOMString contextId, optional any options = null);
+
+ USVString toDataURL(optional DOMString type = "image/png", optional any quality);
+ undefined toBlob(BlobCallback _callback, optional DOMString type = "image/png", optional any quality);
+ OffscreenCanvas transferControlToOffscreen();
+};
+
+callback BlobCallback = undefined (Blob? blob);
+
+typedef (HTMLImageElement or
+ SVGImageElement) HTMLOrSVGImageElement;
+
+typedef (HTMLOrSVGImageElement or
+ HTMLVideoElement or
+ HTMLCanvasElement or
+ ImageBitmap or
+ OffscreenCanvas or
+ VideoFrame) CanvasImageSource;
+
+enum PredefinedColorSpace { "srgb", "display-p3" };
+
+enum CanvasFillRule { "nonzero", "evenodd" };
+
+dictionary CanvasRenderingContext2DSettings {
+ boolean alpha = true;
+ boolean desynchronized = false;
+ PredefinedColorSpace colorSpace = "srgb";
+ boolean willReadFrequently = false;
+};
+
+enum ImageSmoothingQuality { "low", "medium", "high" };
+
+[Exposed=Window]
+interface CanvasRenderingContext2D {
+ // back-reference to the canvas
+ readonly attribute HTMLCanvasElement canvas;
+
+ CanvasRenderingContext2DSettings getContextAttributes();
+};
+CanvasRenderingContext2D includes CanvasState;
+CanvasRenderingContext2D includes CanvasTransform;
+CanvasRenderingContext2D includes CanvasCompositing;
+CanvasRenderingContext2D includes CanvasImageSmoothing;
+CanvasRenderingContext2D includes CanvasFillStrokeStyles;
+CanvasRenderingContext2D includes CanvasShadowStyles;
+CanvasRenderingContext2D includes CanvasFilters;
+CanvasRenderingContext2D includes CanvasRect;
+CanvasRenderingContext2D includes CanvasDrawPath;
+CanvasRenderingContext2D includes CanvasUserInterface;
+CanvasRenderingContext2D includes CanvasText;
+CanvasRenderingContext2D includes CanvasDrawImage;
+CanvasRenderingContext2D includes CanvasImageData;
+CanvasRenderingContext2D includes CanvasPathDrawingStyles;
+CanvasRenderingContext2D includes CanvasTextDrawingStyles;
+CanvasRenderingContext2D includes CanvasPath;
+
+interface mixin CanvasState {
+ // state
+ undefined save(); // push state on state stack
+ undefined restore(); // pop state stack and restore state
+ undefined reset(); // reset the rendering context to its default state
+ boolean isContextLost(); // return whether context is lost
+};
+
+interface mixin CanvasTransform {
+ // transformations (default transform is the identity matrix)
+ undefined scale(unrestricted double x, unrestricted double y);
+ undefined rotate(unrestricted double angle);
+ undefined translate(unrestricted double x, unrestricted double y);
+ undefined transform(unrestricted double a, unrestricted double b, unrestricted double c, unrestricted double d, unrestricted double e, unrestricted double f);
+
+ [NewObject] DOMMatrix getTransform();
+ undefined setTransform(unrestricted double a, unrestricted double b, unrestricted double c, unrestricted double d, unrestricted double e, unrestricted double f);
+ undefined setTransform(optional DOMMatrix2DInit transform = {});
+ undefined resetTransform();
+
+};
+
+interface mixin CanvasCompositing {
+ // compositing
+ attribute unrestricted double globalAlpha; // (default 1.0)
+ attribute DOMString globalCompositeOperation; // (default "source-over")
+};
+
+interface mixin CanvasImageSmoothing {
+ // image smoothing
+ attribute boolean imageSmoothingEnabled; // (default true)
+ attribute ImageSmoothingQuality imageSmoothingQuality; // (default low)
+
+};
+
+interface mixin CanvasFillStrokeStyles {
+ // colors and styles (see also the CanvasPathDrawingStyles and CanvasTextDrawingStyles interfaces)
+ attribute (DOMString or CanvasGradient or CanvasPattern) strokeStyle; // (default black)
+ attribute (DOMString or CanvasGradient or CanvasPattern) fillStyle; // (default black)
+ CanvasGradient createLinearGradient(double x0, double y0, double x1, double y1);
+ CanvasGradient createRadialGradient(double x0, double y0, double r0, double x1, double y1, double r1);
+ CanvasGradient createConicGradient(double startAngle, double x, double y);
+ CanvasPattern? createPattern(CanvasImageSource image, [LegacyNullToEmptyString] DOMString repetition);
+
+};
+
+interface mixin CanvasShadowStyles {
+ // shadows
+ attribute unrestricted double shadowOffsetX; // (default 0)
+ attribute unrestricted double shadowOffsetY; // (default 0)
+ attribute unrestricted double shadowBlur; // (default 0)
+ attribute DOMString shadowColor; // (default transparent black)
+};
+
+interface mixin CanvasFilters {
+ // filters
+ attribute DOMString filter; // (default "none")
+};
+
+interface mixin CanvasRect {
+ // rects
+ undefined clearRect(unrestricted double x, unrestricted double y, unrestricted double w, unrestricted double h);
+ undefined fillRect(unrestricted double x, unrestricted double y, unrestricted double w, unrestricted double h);
+ undefined strokeRect(unrestricted double x, unrestricted double y, unrestricted double w, unrestricted double h);
+};
+
+interface mixin CanvasDrawPath {
+ // path API (see also CanvasPath)
+ undefined beginPath();
+ undefined fill(optional CanvasFillRule fillRule = "nonzero");
+ undefined fill(Path2D path, optional CanvasFillRule fillRule = "nonzero");
+ undefined stroke();
+ undefined stroke(Path2D path);
+ undefined clip(optional CanvasFillRule fillRule = "nonzero");
+ undefined clip(Path2D path, optional CanvasFillRule fillRule = "nonzero");
+ boolean isPointInPath(unrestricted double x, unrestricted double y, optional CanvasFillRule fillRule = "nonzero");
+ boolean isPointInPath(Path2D path, unrestricted double x, unrestricted double y, optional CanvasFillRule fillRule = "nonzero");
+ boolean isPointInStroke(unrestricted double x, unrestricted double y);
+ boolean isPointInStroke(Path2D path, unrestricted double x, unrestricted double y);
+};
+
+interface mixin CanvasUserInterface {
+ undefined drawFocusIfNeeded(Element element);
+ undefined drawFocusIfNeeded(Path2D path, Element element);
+ undefined scrollPathIntoView();
+ undefined scrollPathIntoView(Path2D path);
+};
+
+interface mixin CanvasText {
+ // text (see also the CanvasPathDrawingStyles and CanvasTextDrawingStyles interfaces)
+ undefined fillText(DOMString text, unrestricted double x, unrestricted double y, optional unrestricted double maxWidth);
+ undefined strokeText(DOMString text, unrestricted double x, unrestricted double y, optional unrestricted double maxWidth);
+ TextMetrics measureText(DOMString text);
+};
+
+interface mixin CanvasDrawImage {
+ // drawing images
+ undefined drawImage(CanvasImageSource image, unrestricted double dx, unrestricted double dy);
+ undefined drawImage(CanvasImageSource image, unrestricted double dx, unrestricted double dy, unrestricted double dw, unrestricted double dh);
+ undefined drawImage(CanvasImageSource image, unrestricted double sx, unrestricted double sy, unrestricted double sw, unrestricted double sh, unrestricted double dx, unrestricted double dy, unrestricted double dw, unrestricted double dh);
+};
+
+interface mixin CanvasImageData {
+ // pixel manipulation
+ ImageData createImageData([EnforceRange] long sw, [EnforceRange] long sh, optional ImageDataSettings settings = {});
+ ImageData createImageData(ImageData imagedata);
+ ImageData getImageData([EnforceRange] long sx, [EnforceRange] long sy, [EnforceRange] long sw, [EnforceRange] long sh, optional ImageDataSettings settings = {});
+ undefined putImageData(ImageData imagedata, [EnforceRange] long dx, [EnforceRange] long dy);
+ undefined putImageData(ImageData imagedata, [EnforceRange] long dx, [EnforceRange] long dy, [EnforceRange] long dirtyX, [EnforceRange] long dirtyY, [EnforceRange] long dirtyWidth, [EnforceRange] long dirtyHeight);
+};
+
+enum CanvasLineCap { "butt", "round", "square" };
+enum CanvasLineJoin { "round", "bevel", "miter" };
+enum CanvasTextAlign { "start", "end", "left", "right", "center" };
+enum CanvasTextBaseline { "top", "hanging", "middle", "alphabetic", "ideographic", "bottom" };
+enum CanvasDirection { "ltr", "rtl", "inherit" };
+enum CanvasFontKerning { "auto", "normal", "none" };
+enum CanvasFontStretch { "ultra-condensed", "extra-condensed", "condensed", "semi-condensed", "normal", "semi-expanded", "expanded", "extra-expanded", "ultra-expanded" };
+enum CanvasFontVariantCaps { "normal", "small-caps", "all-small-caps", "petite-caps", "all-petite-caps", "unicase", "titling-caps" };
+enum CanvasTextRendering { "auto", "optimizeSpeed", "optimizeLegibility", "geometricPrecision" };
+
+interface mixin CanvasPathDrawingStyles {
+ // line caps/joins
+ attribute unrestricted double lineWidth; // (default 1)
+ attribute CanvasLineCap lineCap; // (default "butt")
+ attribute CanvasLineJoin lineJoin; // (default "miter")
+ attribute unrestricted double miterLimit; // (default 10)
+
+ // dashed lines
+ undefined setLineDash(sequence<unrestricted double> segments); // default empty
+ sequence<unrestricted double> getLineDash();
+ attribute unrestricted double lineDashOffset;
+};
+
+interface mixin CanvasTextDrawingStyles {
+ // text
+ attribute DOMString font; // (default 10px sans-serif)
+ attribute CanvasTextAlign textAlign; // (default: "start")
+ attribute CanvasTextBaseline textBaseline; // (default: "alphabetic")
+ attribute CanvasDirection direction; // (default: "inherit")
+ attribute DOMString letterSpacing; // (default: "0px")
+ attribute CanvasFontKerning fontKerning; // (default: "auto")
+ attribute CanvasFontStretch fontStretch; // (default: "normal")
+ attribute CanvasFontVariantCaps fontVariantCaps; // (default: "normal")
+ attribute CanvasTextRendering textRendering; // (default: "auto")
+ attribute DOMString wordSpacing; // (default: "0px")
+};
+
+interface mixin CanvasPath {
+ // shared path API methods
+ undefined closePath();
+ undefined moveTo(unrestricted double x, unrestricted double y);
+ undefined lineTo(unrestricted double x, unrestricted double y);
+ undefined quadraticCurveTo(unrestricted double cpx, unrestricted double cpy, unrestricted double x, unrestricted double y);
+ undefined bezierCurveTo(unrestricted double cp1x, unrestricted double cp1y, unrestricted double cp2x, unrestricted double cp2y, unrestricted double x, unrestricted double y);
+ undefined arcTo(unrestricted double x1, unrestricted double y1, unrestricted double x2, unrestricted double y2, unrestricted double radius);
+ undefined rect(unrestricted double x, unrestricted double y, unrestricted double w, unrestricted double h);
+ undefined roundRect(unrestricted double x, unrestricted double y, unrestricted double w, unrestricted double h, optional (unrestricted double or DOMPointInit or sequence<(unrestricted double or DOMPointInit)>) radii = 0);
+ undefined arc(unrestricted double x, unrestricted double y, unrestricted double radius, unrestricted double startAngle, unrestricted double endAngle, optional boolean counterclockwise = false);
+ undefined ellipse(unrestricted double x, unrestricted double y, unrestricted double radiusX, unrestricted double radiusY, unrestricted double rotation, unrestricted double startAngle, unrestricted double endAngle, optional boolean counterclockwise = false);
+};
+
+[Exposed=(Window,Worker)]
+interface CanvasGradient {
+ // opaque object
+ undefined addColorStop(double offset, DOMString color);
+};
+
+[Exposed=(Window,Worker)]
+interface CanvasPattern {
+ // opaque object
+ undefined setTransform(optional DOMMatrix2DInit transform = {});
+};
+
+[Exposed=(Window,Worker)]
+interface TextMetrics {
+ // x-direction
+ readonly attribute double width; // advance width
+ readonly attribute double actualBoundingBoxLeft;
+ readonly attribute double actualBoundingBoxRight;
+
+ // y-direction
+ readonly attribute double fontBoundingBoxAscent;
+ readonly attribute double fontBoundingBoxDescent;
+ readonly attribute double actualBoundingBoxAscent;
+ readonly attribute double actualBoundingBoxDescent;
+ readonly attribute double emHeightAscent;
+ readonly attribute double emHeightDescent;
+ readonly attribute double hangingBaseline;
+ readonly attribute double alphabeticBaseline;
+ readonly attribute double ideographicBaseline;
+};
+
+dictionary ImageDataSettings {
+ PredefinedColorSpace colorSpace;
+};
+
+[Exposed=(Window,Worker),
+ Serializable]
+interface ImageData {
+ constructor(unsigned long sw, unsigned long sh, optional ImageDataSettings settings = {});
+ constructor(Uint8ClampedArray data, unsigned long sw, optional unsigned long sh, optional ImageDataSettings settings = {});
+
+ readonly attribute unsigned long width;
+ readonly attribute unsigned long height;
+ readonly attribute Uint8ClampedArray data;
+ readonly attribute PredefinedColorSpace colorSpace;
+};
+
+[Exposed=(Window,Worker)]
+interface Path2D {
+ constructor(optional (Path2D or DOMString) path);
+
+ undefined addPath(Path2D path, optional DOMMatrix2DInit transform = {});
+};
+Path2D includes CanvasPath;
+
+[Exposed=(Window,Worker)]
+interface ImageBitmapRenderingContext {
+ readonly attribute (HTMLCanvasElement or OffscreenCanvas) canvas;
+ undefined transferFromImageBitmap(ImageBitmap? bitmap);
+};
+
+dictionary ImageBitmapRenderingContextSettings {
+ boolean alpha = true;
+};
+
+typedef (OffscreenCanvasRenderingContext2D or ImageBitmapRenderingContext or WebGLRenderingContext or WebGL2RenderingContext or GPUCanvasContext) OffscreenRenderingContext;
+
+dictionary ImageEncodeOptions {
+ DOMString type = "image/png";
+ unrestricted double quality;
+};
+
+enum OffscreenRenderingContextId { "2d", "bitmaprenderer", "webgl", "webgl2", "webgpu" };
+
+[Exposed=(Window,Worker), Transferable]
+interface OffscreenCanvas : EventTarget {
+ constructor([EnforceRange] unsigned long long width, [EnforceRange] unsigned long long height);
+
+ attribute [EnforceRange] unsigned long long width;
+ attribute [EnforceRange] unsigned long long height;
+
+ OffscreenRenderingContext? getContext(OffscreenRenderingContextId contextId, optional any options = null);
+ ImageBitmap transferToImageBitmap();
+ Promise<Blob> convertToBlob(optional ImageEncodeOptions options = {});
+
+ attribute EventHandler oncontextlost;
+ attribute EventHandler oncontextrestored;
+};
+
+[Exposed=(Window,Worker)]
+interface OffscreenCanvasRenderingContext2D {
+ undefined commit();
+ readonly attribute OffscreenCanvas canvas;
+};
+
+OffscreenCanvasRenderingContext2D includes CanvasState;
+OffscreenCanvasRenderingContext2D includes CanvasTransform;
+OffscreenCanvasRenderingContext2D includes CanvasCompositing;
+OffscreenCanvasRenderingContext2D includes CanvasImageSmoothing;
+OffscreenCanvasRenderingContext2D includes CanvasFillStrokeStyles;
+OffscreenCanvasRenderingContext2D includes CanvasShadowStyles;
+OffscreenCanvasRenderingContext2D includes CanvasFilters;
+OffscreenCanvasRenderingContext2D includes CanvasRect;
+OffscreenCanvasRenderingContext2D includes CanvasDrawPath;
+OffscreenCanvasRenderingContext2D includes CanvasText;
+OffscreenCanvasRenderingContext2D includes CanvasDrawImage;
+OffscreenCanvasRenderingContext2D includes CanvasImageData;
+OffscreenCanvasRenderingContext2D includes CanvasPathDrawingStyles;
+OffscreenCanvasRenderingContext2D includes CanvasTextDrawingStyles;
+OffscreenCanvasRenderingContext2D includes CanvasPath;
+
+[Exposed=Window]
+interface CustomElementRegistry {
+ [CEReactions] undefined define(DOMString name, CustomElementConstructor constructor, optional ElementDefinitionOptions options = {});
+ (CustomElementConstructor or undefined) get(DOMString name);
+ Promise<CustomElementConstructor> whenDefined(DOMString name);
+ [CEReactions] undefined upgrade(Node root);
+};
+
+callback CustomElementConstructor = HTMLElement ();
+
+dictionary ElementDefinitionOptions {
+ DOMString extends;
+};
+
+[Exposed=Window]
+interface ElementInternals {
+ // Shadow root access
+ readonly attribute ShadowRoot? shadowRoot;
+
+ // Form-associated custom elements
+ undefined setFormValue((File or USVString or FormData)? value,
+ optional (File or USVString or FormData)? state);
+
+ readonly attribute HTMLFormElement? form;
+
+ undefined setValidity(optional ValidityStateFlags flags = {},
+ optional DOMString message,
+ optional HTMLElement anchor);
+ readonly attribute boolean willValidate;
+ readonly attribute ValidityState validity;
+ readonly attribute DOMString validationMessage;
+ boolean checkValidity();
+ boolean reportValidity();
+
+ readonly attribute NodeList labels;
+};
+
+// Accessibility semantics
+ElementInternals includes ARIAMixin;
+
+dictionary ValidityStateFlags {
+ boolean valueMissing = false;
+ boolean typeMismatch = false;
+ boolean patternMismatch = false;
+ boolean tooLong = false;
+ boolean tooShort = false;
+ boolean rangeUnderflow = false;
+ boolean rangeOverflow = false;
+ boolean stepMismatch = false;
+ boolean badInput = false;
+ boolean customError = false;
+};
+
+[Exposed=(Window)]
+interface VisibilityStateEntry : PerformanceEntry {
+ readonly attribute DOMString name; // shadows inherited name
+ readonly attribute DOMString entryType; // shadows inherited entryType
+ readonly attribute DOMHighResTimeStamp startTime; // shadows inherited startTime
+ readonly attribute unsigned long duration; // shadows inherited duration
+};
+
+[Exposed=Window]
+interface UserActivation {
+ readonly attribute boolean hasBeenActive;
+ readonly attribute boolean isActive;
+};
+
+partial interface Navigator {
+ [SameObject] readonly attribute UserActivation userActivation;
+};
+
+dictionary FocusOptions {
+ boolean preventScroll = false;
+ boolean focusVisible;
+};
+
+interface mixin ElementContentEditable {
+ [CEReactions] attribute DOMString contentEditable;
+ [CEReactions] attribute DOMString enterKeyHint;
+ readonly attribute boolean isContentEditable;
+ [CEReactions] attribute DOMString inputMode;
+};
+
+[Exposed=Window]
+interface DataTransfer {
+ constructor();
+
+ attribute DOMString dropEffect;
+ attribute DOMString effectAllowed;
+
+ [SameObject] readonly attribute DataTransferItemList items;
+
+ undefined setDragImage(Element image, long x, long y);
+
+ /* old interface */
+ readonly attribute FrozenArray<DOMString> types;
+ DOMString getData(DOMString format);
+ undefined setData(DOMString format, DOMString data);
+ undefined clearData(optional DOMString format);
+ [SameObject] readonly attribute FileList files;
+};
+
+[Exposed=Window]
+interface DataTransferItemList {
+ readonly attribute unsigned long length;
+ getter DataTransferItem (unsigned long index);
+ DataTransferItem? add(DOMString data, DOMString type);
+ DataTransferItem? add(File data);
+ undefined remove(unsigned long index);
+ undefined clear();
+};
+
+[Exposed=Window]
+interface DataTransferItem {
+ readonly attribute DOMString kind;
+ readonly attribute DOMString type;
+ undefined getAsString(FunctionStringCallback? _callback);
+ File? getAsFile();
+};
+
+callback FunctionStringCallback = undefined (DOMString data);
+
+[Exposed=Window]
+interface DragEvent : MouseEvent {
+ constructor(DOMString type, optional DragEventInit eventInitDict = {});
+
+ readonly attribute DataTransfer? dataTransfer;
+};
+
+dictionary DragEventInit : MouseEventInit {
+ DataTransfer? dataTransfer = null;
+};
+
+interface mixin PopoverInvokerElement {
+ [CEReactions] attribute Element? popoverTargetElement;
+ [CEReactions] attribute DOMString popoverTargetAction;
+};
+
+[Exposed=Window]
+interface ToggleEvent : Event {
+ constructor(DOMString type, optional ToggleEventInit eventInitDict = {});
+ readonly attribute DOMString oldState;
+ readonly attribute DOMString newState;
+};
+
+dictionary ToggleEventInit : EventInit {
+ DOMString oldState = "";
+ DOMString newState = "";
+};
+
+[Global=Window,
+ Exposed=Window,
+ LegacyUnenumerableNamedProperties]
+interface Window : EventTarget {
+ // the current browsing context
+ [LegacyUnforgeable] readonly attribute WindowProxy window;
+ [Replaceable] readonly attribute WindowProxy self;
+ [LegacyUnforgeable] readonly attribute Document document;
+ attribute DOMString name;
+ [PutForwards=href, LegacyUnforgeable] readonly attribute Location location;
+ readonly attribute History history;
+ readonly attribute CustomElementRegistry customElements;
+ [Replaceable] readonly attribute BarProp locationbar;
+ [Replaceable] readonly attribute BarProp menubar;
+ [Replaceable] readonly attribute BarProp personalbar;
+ [Replaceable] readonly attribute BarProp scrollbars;
+ [Replaceable] readonly attribute BarProp statusbar;
+ [Replaceable] readonly attribute BarProp toolbar;
+ attribute DOMString status;
+ undefined close();
+ readonly attribute boolean closed;
+ undefined stop();
+ undefined focus();
+ undefined blur();
+
+ // other browsing contexts
+ [Replaceable] readonly attribute WindowProxy frames;
+ [Replaceable] readonly attribute unsigned long length;
+ [LegacyUnforgeable] readonly attribute WindowProxy? top;
+ attribute any opener;
+ [Replaceable] readonly attribute WindowProxy? parent;
+ readonly attribute Element? frameElement;
+ WindowProxy? open(optional USVString url = "", optional DOMString target = "_blank", optional [LegacyNullToEmptyString] DOMString features = "");
+
+ // Since this is the global object, the IDL named getter adds a NamedPropertiesObject exotic
+ // object on the prototype chain. Indeed, this does not make the global object an exotic object.
+ // Indexed access is taken care of by the WindowProxy exotic object.
+ getter object (DOMString name);
+
+ // the user agent
+ readonly attribute Navigator navigator;
+ readonly attribute Navigator clientInformation; // legacy alias of .navigator
+ readonly attribute boolean originAgentCluster;
+
+ // user prompts
+ undefined alert();
+ undefined alert(DOMString message);
+ boolean confirm(optional DOMString message = "");
+ DOMString? prompt(optional DOMString message = "", optional DOMString default = "");
+ undefined print();
+
+ undefined postMessage(any message, USVString targetOrigin, optional sequence<object> transfer = []);
+ undefined postMessage(any message, optional WindowPostMessageOptions options = {});
+
+ // also has obsolete members
+};
+Window includes GlobalEventHandlers;
+Window includes WindowEventHandlers;
+
+dictionary WindowPostMessageOptions : StructuredSerializeOptions {
+ USVString targetOrigin = "/";
+};
+
+[Exposed=Window]
+interface BarProp {
+ readonly attribute boolean visible;
+};
+
+[Exposed=Window]
+interface Location { // but see also additional creation steps and overridden internal methods
+ [LegacyUnforgeable] stringifier attribute USVString href;
+ [LegacyUnforgeable] readonly attribute USVString origin;
+ [LegacyUnforgeable] attribute USVString protocol;
+ [LegacyUnforgeable] attribute USVString host;
+ [LegacyUnforgeable] attribute USVString hostname;
+ [LegacyUnforgeable] attribute USVString port;
+ [LegacyUnforgeable] attribute USVString pathname;
+ [LegacyUnforgeable] attribute USVString search;
+ [LegacyUnforgeable] attribute USVString hash;
+
+ [LegacyUnforgeable] undefined assign(USVString url);
+ [LegacyUnforgeable] undefined replace(USVString url);
+ [LegacyUnforgeable] undefined reload();
+
+ [LegacyUnforgeable, SameObject] readonly attribute DOMStringList ancestorOrigins;
+};
+
+enum ScrollRestoration { "auto", "manual" };
+
+[Exposed=Window]
+interface History {
+ readonly attribute unsigned long length;
+ attribute ScrollRestoration scrollRestoration;
+ readonly attribute any state;
+ undefined go(optional long delta = 0);
+ undefined back();
+ undefined forward();
+ undefined pushState(any data, DOMString unused, optional USVString? url = null);
+ undefined replaceState(any data, DOMString unused, optional USVString? url = null);
+};
+
+[Exposed=Window]
+interface PopStateEvent : Event {
+ constructor(DOMString type, optional PopStateEventInit eventInitDict = {});
+
+ readonly attribute any state;
+};
+
+dictionary PopStateEventInit : EventInit {
+ any state = null;
+};
+
+[Exposed=Window]
+interface HashChangeEvent : Event {
+ constructor(DOMString type, optional HashChangeEventInit eventInitDict = {});
+
+ readonly attribute USVString oldURL;
+ readonly attribute USVString newURL;
+};
+
+dictionary HashChangeEventInit : EventInit {
+ USVString oldURL = "";
+ USVString newURL = "";
+};
+
+[Exposed=Window]
+interface PageTransitionEvent : Event {
+ constructor(DOMString type, optional PageTransitionEventInit eventInitDict = {});
+
+ readonly attribute boolean persisted;
+};
+
+dictionary PageTransitionEventInit : EventInit {
+ boolean persisted = false;
+};
+
+[Exposed=Window]
+interface BeforeUnloadEvent : Event {
+ attribute DOMString returnValue;
+};
+
+[Exposed=*]
+interface ErrorEvent : Event {
+ constructor(DOMString type, optional ErrorEventInit eventInitDict = {});
+
+ readonly attribute DOMString message;
+ readonly attribute USVString filename;
+ readonly attribute unsigned long lineno;
+ readonly attribute unsigned long colno;
+ readonly attribute any error;
+};
+
+dictionary ErrorEventInit : EventInit {
+ DOMString message = "";
+ USVString filename = "";
+ unsigned long lineno = 0;
+ unsigned long colno = 0;
+ any error;
+};
+
+[Exposed=*]
+interface PromiseRejectionEvent : Event {
+ constructor(DOMString type, PromiseRejectionEventInit eventInitDict);
+
+ readonly attribute Promise<any> promise;
+ readonly attribute any reason;
+};
+
+dictionary PromiseRejectionEventInit : EventInit {
+ required Promise<any> promise;
+ any reason;
+};
+
+[LegacyTreatNonObjectAsNull]
+callback EventHandlerNonNull = any (Event event);
+typedef EventHandlerNonNull? EventHandler;
+
+[LegacyTreatNonObjectAsNull]
+callback OnErrorEventHandlerNonNull = any ((Event or DOMString) event, optional DOMString source, optional unsigned long lineno, optional unsigned long colno, optional any error);
+typedef OnErrorEventHandlerNonNull? OnErrorEventHandler;
+
+[LegacyTreatNonObjectAsNull]
+callback OnBeforeUnloadEventHandlerNonNull = DOMString? (Event event);
+typedef OnBeforeUnloadEventHandlerNonNull? OnBeforeUnloadEventHandler;
+
+interface mixin GlobalEventHandlers {
+ attribute EventHandler onabort;
+ attribute EventHandler onauxclick;
+ attribute EventHandler onbeforeinput;
+ attribute EventHandler onbeforematch;
+ attribute EventHandler onbeforetoggle;
+ attribute EventHandler onblur;
+ attribute EventHandler oncancel;
+ attribute EventHandler oncanplay;
+ attribute EventHandler oncanplaythrough;
+ attribute EventHandler onchange;
+ attribute EventHandler onclick;
+ attribute EventHandler onclose;
+ attribute EventHandler oncontextlost;
+ attribute EventHandler oncontextmenu;
+ attribute EventHandler oncontextrestored;
+ attribute EventHandler oncopy;
+ attribute EventHandler oncuechange;
+ attribute EventHandler oncut;
+ attribute EventHandler ondblclick;
+ attribute EventHandler ondrag;
+ attribute EventHandler ondragend;
+ attribute EventHandler ondragenter;
+ attribute EventHandler ondragleave;
+ attribute EventHandler ondragover;
+ attribute EventHandler ondragstart;
+ attribute EventHandler ondrop;
+ attribute EventHandler ondurationchange;
+ attribute EventHandler onemptied;
+ attribute EventHandler onended;
+ attribute OnErrorEventHandler onerror;
+ attribute EventHandler onfocus;
+ attribute EventHandler onformdata;
+ attribute EventHandler oninput;
+ attribute EventHandler oninvalid;
+ attribute EventHandler onkeydown;
+ attribute EventHandler onkeypress;
+ attribute EventHandler onkeyup;
+ attribute EventHandler onload;
+ attribute EventHandler onloadeddata;
+ attribute EventHandler onloadedmetadata;
+ attribute EventHandler onloadstart;
+ attribute EventHandler onmousedown;
+ [LegacyLenientThis] attribute EventHandler onmouseenter;
+ [LegacyLenientThis] attribute EventHandler onmouseleave;
+ attribute EventHandler onmousemove;
+ attribute EventHandler onmouseout;
+ attribute EventHandler onmouseover;
+ attribute EventHandler onmouseup;
+ attribute EventHandler onpaste;
+ attribute EventHandler onpause;
+ attribute EventHandler onplay;
+ attribute EventHandler onplaying;
+ attribute EventHandler onprogress;
+ attribute EventHandler onratechange;
+ attribute EventHandler onreset;
+ attribute EventHandler onresize;
+ attribute EventHandler onscroll;
+ attribute EventHandler onscrollend;
+ attribute EventHandler onsecuritypolicyviolation;
+ attribute EventHandler onseeked;
+ attribute EventHandler onseeking;
+ attribute EventHandler onselect;
+ attribute EventHandler onslotchange;
+ attribute EventHandler onstalled;
+ attribute EventHandler onsubmit;
+ attribute EventHandler onsuspend;
+ attribute EventHandler ontimeupdate;
+ attribute EventHandler ontoggle;
+ attribute EventHandler onvolumechange;
+ attribute EventHandler onwaiting;
+ attribute EventHandler onwebkitanimationend;
+ attribute EventHandler onwebkitanimationiteration;
+ attribute EventHandler onwebkitanimationstart;
+ attribute EventHandler onwebkittransitionend;
+ attribute EventHandler onwheel;
+};
+
+interface mixin WindowEventHandlers {
+ attribute EventHandler onafterprint;
+ attribute EventHandler onbeforeprint;
+ attribute OnBeforeUnloadEventHandler onbeforeunload;
+ attribute EventHandler onhashchange;
+ attribute EventHandler onlanguagechange;
+ attribute EventHandler onmessage;
+ attribute EventHandler onmessageerror;
+ attribute EventHandler onoffline;
+ attribute EventHandler ononline;
+ attribute EventHandler onpagehide;
+ attribute EventHandler onpageshow;
+ attribute EventHandler onpopstate;
+ attribute EventHandler onrejectionhandled;
+ attribute EventHandler onstorage;
+ attribute EventHandler onunhandledrejection;
+ attribute EventHandler onunload;
+};
+
+typedef (DOMString or Function) TimerHandler;
+
+interface mixin WindowOrWorkerGlobalScope {
+ [Replaceable] readonly attribute USVString origin;
+ readonly attribute boolean isSecureContext;
+ readonly attribute boolean crossOriginIsolated;
+
+ undefined reportError(any e);
+
+ // base64 utility methods
+ DOMString btoa(DOMString data);
+ ByteString atob(DOMString data);
+
+ // timers
+ long setTimeout(TimerHandler handler, optional long timeout = 0, any... arguments);
+ undefined clearTimeout(optional long id = 0);
+ long setInterval(TimerHandler handler, optional long timeout = 0, any... arguments);
+ undefined clearInterval(optional long id = 0);
+
+ // microtask queuing
+ undefined queueMicrotask(VoidFunction callback);
+
+ // ImageBitmap
+ Promise<ImageBitmap> createImageBitmap(ImageBitmapSource image, optional ImageBitmapOptions options = {});
+ Promise<ImageBitmap> createImageBitmap(ImageBitmapSource image, long sx, long sy, long sw, long sh, optional ImageBitmapOptions options = {});
+
+ // structured cloning
+ any structuredClone(any value, optional StructuredSerializeOptions options = {});
+};
+Window includes WindowOrWorkerGlobalScope;
+WorkerGlobalScope includes WindowOrWorkerGlobalScope;
+
+[Exposed=Window]
+interface DOMParser {
+ constructor();
+
+ [NewObject] Document parseFromString(DOMString string, DOMParserSupportedType type);
+};
+
+enum DOMParserSupportedType {
+ "text/html",
+ "text/xml",
+ "application/xml",
+ "application/xhtml+xml",
+ "image/svg+xml"
+};
+
+[Exposed=Window]
+interface Navigator {
+ // objects implementing this interface also implement the interfaces given below
+};
+Navigator includes NavigatorID;
+Navigator includes NavigatorLanguage;
+Navigator includes NavigatorOnLine;
+Navigator includes NavigatorContentUtils;
+Navigator includes NavigatorCookies;
+Navigator includes NavigatorPlugins;
+Navigator includes NavigatorConcurrentHardware;
+
+interface mixin NavigatorID {
+ readonly attribute DOMString appCodeName; // constant "Mozilla"
+ readonly attribute DOMString appName; // constant "Netscape"
+ readonly attribute DOMString appVersion;
+ readonly attribute DOMString platform;
+ readonly attribute DOMString product; // constant "Gecko"
+ [Exposed=Window] readonly attribute DOMString productSub;
+ readonly attribute DOMString userAgent;
+ [Exposed=Window] readonly attribute DOMString vendor;
+ [Exposed=Window] readonly attribute DOMString vendorSub; // constant ""
+};
+
+partial interface mixin NavigatorID {
+ [Exposed=Window] boolean taintEnabled(); // constant false
+ [Exposed=Window] readonly attribute DOMString oscpu;
+};
+
+interface mixin NavigatorLanguage {
+ readonly attribute DOMString language;
+ readonly attribute FrozenArray<DOMString> languages;
+};
+
+interface mixin NavigatorOnLine {
+ readonly attribute boolean onLine;
+};
+
+interface mixin NavigatorContentUtils {
+ [SecureContext] undefined registerProtocolHandler(DOMString scheme, USVString url);
+ [SecureContext] undefined unregisterProtocolHandler(DOMString scheme, USVString url);
+};
+
+interface mixin NavigatorCookies {
+ readonly attribute boolean cookieEnabled;
+};
+
+interface mixin NavigatorPlugins {
+ [SameObject] readonly attribute PluginArray plugins;
+ [SameObject] readonly attribute MimeTypeArray mimeTypes;
+ boolean javaEnabled();
+ readonly attribute boolean pdfViewerEnabled;
+};
+
+[Exposed=Window,
+ LegacyUnenumerableNamedProperties]
+interface PluginArray {
+ undefined refresh();
+ readonly attribute unsigned long length;
+ getter Plugin? item(unsigned long index);
+ getter Plugin? namedItem(DOMString name);
+};
+
+[Exposed=Window,
+ LegacyUnenumerableNamedProperties]
+interface MimeTypeArray {
+ readonly attribute unsigned long length;
+ getter MimeType? item(unsigned long index);
+ getter MimeType? namedItem(DOMString name);
+};
+
+[Exposed=Window,
+ LegacyUnenumerableNamedProperties]
+interface Plugin {
+ readonly attribute DOMString name;
+ readonly attribute DOMString description;
+ readonly attribute DOMString filename;
+ readonly attribute unsigned long length;
+ getter MimeType? item(unsigned long index);
+ getter MimeType? namedItem(DOMString name);
+};
+
+[Exposed=Window]
+interface MimeType {
+ readonly attribute DOMString type;
+ readonly attribute DOMString description;
+ readonly attribute DOMString suffixes;
+ readonly attribute Plugin enabledPlugin;
+};
+
+[Exposed=(Window,Worker), Serializable, Transferable]
+interface ImageBitmap {
+ readonly attribute unsigned long width;
+ readonly attribute unsigned long height;
+ undefined close();
+};
+
+typedef (CanvasImageSource or
+ Blob or
+ ImageData) ImageBitmapSource;
+
+enum ImageOrientation { "from-image", "flipY" };
+enum PremultiplyAlpha { "none", "premultiply", "default" };
+enum ColorSpaceConversion { "none", "default" };
+enum ResizeQuality { "pixelated", "low", "medium", "high" };
+
+dictionary ImageBitmapOptions {
+ ImageOrientation imageOrientation = "from-image";
+ PremultiplyAlpha premultiplyAlpha = "default";
+ ColorSpaceConversion colorSpaceConversion = "default";
+ [EnforceRange] unsigned long resizeWidth;
+ [EnforceRange] unsigned long resizeHeight;
+ ResizeQuality resizeQuality = "low";
+};
+
+callback FrameRequestCallback = undefined (DOMHighResTimeStamp time);
+
+interface mixin AnimationFrameProvider {
+ unsigned long requestAnimationFrame(FrameRequestCallback callback);
+ undefined cancelAnimationFrame(unsigned long handle);
+};
+Window includes AnimationFrameProvider;
+DedicatedWorkerGlobalScope includes AnimationFrameProvider;
+
+[Exposed=(Window,Worker,AudioWorklet)]
+interface MessageEvent : Event {
+ constructor(DOMString type, optional MessageEventInit eventInitDict = {});
+
+ readonly attribute any data;
+ readonly attribute USVString origin;
+ readonly attribute DOMString lastEventId;
+ readonly attribute MessageEventSource? source;
+ readonly attribute FrozenArray<MessagePort> ports;
+
+ undefined initMessageEvent(DOMString type, optional boolean bubbles = false, optional boolean cancelable = false, optional any data = null, optional USVString origin = "", optional DOMString lastEventId = "", optional MessageEventSource? source = null, optional sequence<MessagePort> ports = []);
+};
+
+dictionary MessageEventInit : EventInit {
+ any data = null;
+ USVString origin = "";
+ DOMString lastEventId = "";
+ MessageEventSource? source = null;
+ sequence<MessagePort> ports = [];
+};
+
+typedef (WindowProxy or MessagePort or ServiceWorker) MessageEventSource;
+
+[Exposed=(Window,Worker)]
+interface EventSource : EventTarget {
+ constructor(USVString url, optional EventSourceInit eventSourceInitDict = {});
+
+ readonly attribute USVString url;
+ readonly attribute boolean withCredentials;
+
+ // ready state
+ const unsigned short CONNECTING = 0;
+ const unsigned short OPEN = 1;
+ const unsigned short CLOSED = 2;
+ readonly attribute unsigned short readyState;
+
+ // networking
+ attribute EventHandler onopen;
+ attribute EventHandler onmessage;
+ attribute EventHandler onerror;
+ undefined close();
+};
+
+dictionary EventSourceInit {
+ boolean withCredentials = false;
+};
+
+[Exposed=(Window,Worker)]
+interface MessageChannel {
+ constructor();
+
+ readonly attribute MessagePort port1;
+ readonly attribute MessagePort port2;
+};
+
+[Exposed=(Window,Worker,AudioWorklet), Transferable]
+interface MessagePort : EventTarget {
+ undefined postMessage(any message, sequence<object> transfer);
+ undefined postMessage(any message, optional StructuredSerializeOptions options = {});
+ undefined start();
+ undefined close();
+
+ // event handlers
+ attribute EventHandler onmessage;
+ attribute EventHandler onmessageerror;
+};
+
+dictionary StructuredSerializeOptions {
+ sequence<object> transfer = [];
+};
+
+[Exposed=(Window,Worker)]
+interface BroadcastChannel : EventTarget {
+ constructor(DOMString name);
+
+ readonly attribute DOMString name;
+ undefined postMessage(any message);
+ undefined close();
+ attribute EventHandler onmessage;
+ attribute EventHandler onmessageerror;
+};
+
+[Exposed=Worker]
+interface WorkerGlobalScope : EventTarget {
+ readonly attribute WorkerGlobalScope self;
+ readonly attribute WorkerLocation location;
+ readonly attribute WorkerNavigator navigator;
+ undefined importScripts(USVString... urls);
+
+ attribute OnErrorEventHandler onerror;
+ attribute EventHandler onlanguagechange;
+ attribute EventHandler onoffline;
+ attribute EventHandler ononline;
+ attribute EventHandler onrejectionhandled;
+ attribute EventHandler onunhandledrejection;
+};
+
+[Global=(Worker,DedicatedWorker),Exposed=DedicatedWorker]
+interface DedicatedWorkerGlobalScope : WorkerGlobalScope {
+ [Replaceable] readonly attribute DOMString name;
+
+ undefined postMessage(any message, sequence<object> transfer);
+ undefined postMessage(any message, optional StructuredSerializeOptions options = {});
+
+ undefined close();
+
+ attribute EventHandler onmessage;
+ attribute EventHandler onmessageerror;
+};
+
+[Global=(Worker,SharedWorker),Exposed=SharedWorker]
+interface SharedWorkerGlobalScope : WorkerGlobalScope {
+ [Replaceable] readonly attribute DOMString name;
+
+ undefined close();
+
+ attribute EventHandler onconnect;
+};
+
+interface mixin AbstractWorker {
+ attribute EventHandler onerror;
+};
+
+[Exposed=(Window,DedicatedWorker,SharedWorker)]
+interface Worker : EventTarget {
+ constructor(USVString scriptURL, optional WorkerOptions options = {});
+
+ undefined terminate();
+
+ undefined postMessage(any message, sequence<object> transfer);
+ undefined postMessage(any message, optional StructuredSerializeOptions options = {});
+ attribute EventHandler onmessage;
+ attribute EventHandler onmessageerror;
+};
+
+dictionary WorkerOptions {
+ WorkerType type = "classic";
+ RequestCredentials credentials = "same-origin"; // credentials is only used if type is "module"
+ DOMString name = "";
+};
+
+enum WorkerType { "classic", "module" };
+
+Worker includes AbstractWorker;
+
+[Exposed=Window]
+interface SharedWorker : EventTarget {
+ constructor(USVString scriptURL, optional (DOMString or WorkerOptions) options = {});
+
+ readonly attribute MessagePort port;
+};
+SharedWorker includes AbstractWorker;
+
+interface mixin NavigatorConcurrentHardware {
+ readonly attribute unsigned long long hardwareConcurrency;
+};
+
+[Exposed=Worker]
+interface WorkerNavigator {};
+WorkerNavigator includes NavigatorID;
+WorkerNavigator includes NavigatorLanguage;
+WorkerNavigator includes NavigatorOnLine;
+WorkerNavigator includes NavigatorConcurrentHardware;
+
+[Exposed=Worker]
+interface WorkerLocation {
+ stringifier readonly attribute USVString href;
+ readonly attribute USVString origin;
+ readonly attribute USVString protocol;
+ readonly attribute USVString host;
+ readonly attribute USVString hostname;
+ readonly attribute USVString port;
+ readonly attribute USVString pathname;
+ readonly attribute USVString search;
+ readonly attribute USVString hash;
+};
+
+[Exposed=Worklet, SecureContext]
+interface WorkletGlobalScope {};
+
+[Exposed=Window, SecureContext]
+interface Worklet {
+ [NewObject] Promise<undefined> addModule(USVString moduleURL, optional WorkletOptions options = {});
+};
+
+dictionary WorkletOptions {
+ RequestCredentials credentials = "same-origin";
+};
+
+[Exposed=Window]
+interface Storage {
+ readonly attribute unsigned long length;
+ DOMString? key(unsigned long index);
+ getter DOMString? getItem(DOMString key);
+ setter undefined setItem(DOMString key, DOMString value);
+ deleter undefined removeItem(DOMString key);
+ undefined clear();
+};
+
+interface mixin WindowSessionStorage {
+ readonly attribute Storage sessionStorage;
+};
+Window includes WindowSessionStorage;
+
+interface mixin WindowLocalStorage {
+ readonly attribute Storage localStorage;
+};
+Window includes WindowLocalStorage;
+
+[Exposed=Window]
+interface StorageEvent : Event {
+ constructor(DOMString type, optional StorageEventInit eventInitDict = {});
+
+ readonly attribute DOMString? key;
+ readonly attribute DOMString? oldValue;
+ readonly attribute DOMString? newValue;
+ readonly attribute USVString url;
+ readonly attribute Storage? storageArea;
+
+ undefined initStorageEvent(DOMString type, optional boolean bubbles = false, optional boolean cancelable = false, optional DOMString? key = null, optional DOMString? oldValue = null, optional DOMString? newValue = null, optional USVString url = "", optional Storage? storageArea = null);
+};
+
+dictionary StorageEventInit : EventInit {
+ DOMString? key = null;
+ DOMString? oldValue = null;
+ DOMString? newValue = null;
+ USVString url = "";
+ Storage? storageArea = null;
+};
+
+[Exposed=Window]
+interface HTMLMarqueeElement : HTMLElement {
+ [HTMLConstructor] constructor();
+
+ [CEReactions] attribute DOMString behavior;
+ [CEReactions] attribute DOMString bgColor;
+ [CEReactions] attribute DOMString direction;
+ [CEReactions] attribute DOMString height;
+ [CEReactions] attribute unsigned long hspace;
+ [CEReactions] attribute long loop;
+ [CEReactions] attribute unsigned long scrollAmount;
+ [CEReactions] attribute unsigned long scrollDelay;
+ [CEReactions] attribute boolean trueSpeed;
+ [CEReactions] attribute unsigned long vspace;
+ [CEReactions] attribute DOMString width;
+
+ undefined start();
+ undefined stop();
+};
+
+[Exposed=Window]
+interface HTMLFrameSetElement : HTMLElement {
+ [HTMLConstructor] constructor();
+
+ [CEReactions] attribute DOMString cols;
+ [CEReactions] attribute DOMString rows;
+};
+HTMLFrameSetElement includes WindowEventHandlers;
+
+[Exposed=Window]
+interface HTMLFrameElement : HTMLElement {
+ [HTMLConstructor] constructor();
+
+ [CEReactions] attribute DOMString name;
+ [CEReactions] attribute DOMString scrolling;
+ [CEReactions] attribute USVString src;
+ [CEReactions] attribute DOMString frameBorder;
+ [CEReactions] attribute USVString longDesc;
+ [CEReactions] attribute boolean noResize;
+ readonly attribute Document? contentDocument;
+ readonly attribute WindowProxy? contentWindow;
+
+ [CEReactions] attribute [LegacyNullToEmptyString] DOMString marginHeight;
+ [CEReactions] attribute [LegacyNullToEmptyString] DOMString marginWidth;
+};
+
+partial interface HTMLAnchorElement {
+ [CEReactions] attribute DOMString coords;
+ [CEReactions] attribute DOMString charset;
+ [CEReactions] attribute DOMString name;
+ [CEReactions] attribute DOMString rev;
+ [CEReactions] attribute DOMString shape;
+};
+
+partial interface HTMLAreaElement {
+ [CEReactions] attribute boolean noHref;
+};
+
+partial interface HTMLBodyElement {
+ [CEReactions] attribute [LegacyNullToEmptyString] DOMString text;
+ [CEReactions] attribute [LegacyNullToEmptyString] DOMString link;
+ [CEReactions] attribute [LegacyNullToEmptyString] DOMString vLink;
+ [CEReactions] attribute [LegacyNullToEmptyString] DOMString aLink;
+ [CEReactions] attribute [LegacyNullToEmptyString] DOMString bgColor;
+ [CEReactions] attribute DOMString background;
+};
+
+partial interface HTMLBRElement {
+ [CEReactions] attribute DOMString clear;
+};
+
+partial interface HTMLTableCaptionElement {
+ [CEReactions] attribute DOMString align;
+};
+
+partial interface HTMLTableColElement {
+ [CEReactions] attribute DOMString align;
+ [CEReactions] attribute DOMString ch;
+ [CEReactions] attribute DOMString chOff;
+ [CEReactions] attribute DOMString vAlign;
+ [CEReactions] attribute DOMString width;
+};
+
+[Exposed=Window]
+interface HTMLDirectoryElement : HTMLElement {
+ [HTMLConstructor] constructor();
+
+ [CEReactions] attribute boolean compact;
+};
+
+partial interface HTMLDivElement {
+ [CEReactions] attribute DOMString align;
+};
+
+partial interface HTMLDListElement {
+ [CEReactions] attribute boolean compact;
+};
+
+partial interface HTMLEmbedElement {
+ [CEReactions] attribute DOMString align;
+ [CEReactions] attribute DOMString name;
+};
+
+[Exposed=Window]
+interface HTMLFontElement : HTMLElement {
+ [HTMLConstructor] constructor();
+
+ [CEReactions] attribute [LegacyNullToEmptyString] DOMString color;
+ [CEReactions] attribute DOMString face;
+ [CEReactions] attribute DOMString size;
+};
+
+partial interface HTMLHeadingElement {
+ [CEReactions] attribute DOMString align;
+};
+
+partial interface HTMLHRElement {
+ [CEReactions] attribute DOMString align;
+ [CEReactions] attribute DOMString color;
+ [CEReactions] attribute boolean noShade;
+ [CEReactions] attribute DOMString size;
+ [CEReactions] attribute DOMString width;
+};
+
+partial interface HTMLHtmlElement {
+ [CEReactions] attribute DOMString version;
+};
+
+partial interface HTMLIFrameElement {
+ [CEReactions] attribute DOMString align;
+ [CEReactions] attribute DOMString scrolling;
+ [CEReactions] attribute DOMString frameBorder;
+ [CEReactions] attribute USVString longDesc;
+
+ [CEReactions] attribute [LegacyNullToEmptyString] DOMString marginHeight;
+ [CEReactions] attribute [LegacyNullToEmptyString] DOMString marginWidth;
+};
+
+partial interface HTMLImageElement {
+ [CEReactions] attribute DOMString name;
+ [CEReactions] attribute USVString lowsrc;
+ [CEReactions] attribute DOMString align;
+ [CEReactions] attribute unsigned long hspace;
+ [CEReactions] attribute unsigned long vspace;
+ [CEReactions] attribute USVString longDesc;
+
+ [CEReactions] attribute [LegacyNullToEmptyString] DOMString border;
+};
+
+partial interface HTMLInputElement {
+ [CEReactions] attribute DOMString align;
+ [CEReactions] attribute DOMString useMap;
+};
+
+partial interface HTMLLegendElement {
+ [CEReactions] attribute DOMString align;
+};
+
+partial interface HTMLLIElement {
+ [CEReactions] attribute DOMString type;
+};
+
+partial interface HTMLLinkElement {
+ [CEReactions] attribute DOMString charset;
+ [CEReactions] attribute DOMString rev;
+ [CEReactions] attribute DOMString target;
+};
+
+partial interface HTMLMenuElement {
+ [CEReactions] attribute boolean compact;
+};
+
+partial interface HTMLMetaElement {
+ [CEReactions] attribute DOMString scheme;
+};
+
+partial interface HTMLObjectElement {
+ [CEReactions] attribute DOMString align;
+ [CEReactions] attribute DOMString archive;
+ [CEReactions] attribute DOMString code;
+ [CEReactions] attribute boolean declare;
+ [CEReactions] attribute unsigned long hspace;
+ [CEReactions] attribute DOMString standby;
+ [CEReactions] attribute unsigned long vspace;
+ [CEReactions] attribute DOMString codeBase;
+ [CEReactions] attribute DOMString codeType;
+ [CEReactions] attribute DOMString useMap;
+
+ [CEReactions] attribute [LegacyNullToEmptyString] DOMString border;
+};
+
+partial interface HTMLOListElement {
+ [CEReactions] attribute boolean compact;
+};
+
+partial interface HTMLParagraphElement {
+ [CEReactions] attribute DOMString align;
+};
+
+[Exposed=Window]
+interface HTMLParamElement : HTMLElement {
+ [HTMLConstructor] constructor();
+
+ [CEReactions] attribute DOMString name;
+ [CEReactions] attribute DOMString value;
+ [CEReactions] attribute DOMString type;
+ [CEReactions] attribute DOMString valueType;
+};
+
+partial interface HTMLPreElement {
+ [CEReactions] attribute long width;
+};
+
+partial interface HTMLStyleElement {
+ [CEReactions] attribute DOMString type;
+};
+
+partial interface HTMLScriptElement {
+ [CEReactions] attribute DOMString charset;
+ [CEReactions] attribute DOMString event;
+ [CEReactions] attribute DOMString htmlFor;
+};
+
+partial interface HTMLTableElement {
+ [CEReactions] attribute DOMString align;
+ [CEReactions] attribute DOMString border;
+ [CEReactions] attribute DOMString frame;
+ [CEReactions] attribute DOMString rules;
+ [CEReactions] attribute DOMString summary;
+ [CEReactions] attribute DOMString width;
+
+ [CEReactions] attribute [LegacyNullToEmptyString] DOMString bgColor;
+ [CEReactions] attribute [LegacyNullToEmptyString] DOMString cellPadding;
+ [CEReactions] attribute [LegacyNullToEmptyString] DOMString cellSpacing;
+};
+
+partial interface HTMLTableSectionElement {
+ [CEReactions] attribute DOMString align;
+ [CEReactions] attribute DOMString ch;
+ [CEReactions] attribute DOMString chOff;
+ [CEReactions] attribute DOMString vAlign;
+};
+
+partial interface HTMLTableCellElement {
+ [CEReactions] attribute DOMString align;
+ [CEReactions] attribute DOMString axis;
+ [CEReactions] attribute DOMString height;
+ [CEReactions] attribute DOMString width;
+
+ [CEReactions] attribute DOMString ch;
+ [CEReactions] attribute DOMString chOff;
+ [CEReactions] attribute boolean noWrap;
+ [CEReactions] attribute DOMString vAlign;
+
+ [CEReactions] attribute [LegacyNullToEmptyString] DOMString bgColor;
+};
+
+partial interface HTMLTableRowElement {
+ [CEReactions] attribute DOMString align;
+ [CEReactions] attribute DOMString ch;
+ [CEReactions] attribute DOMString chOff;
+ [CEReactions] attribute DOMString vAlign;
+
+ [CEReactions] attribute [LegacyNullToEmptyString] DOMString bgColor;
+};
+
+partial interface HTMLUListElement {
+ [CEReactions] attribute boolean compact;
+ [CEReactions] attribute DOMString type;
+};
+
+partial interface Document {
+ [CEReactions] attribute [LegacyNullToEmptyString] DOMString fgColor;
+ [CEReactions] attribute [LegacyNullToEmptyString] DOMString linkColor;
+ [CEReactions] attribute [LegacyNullToEmptyString] DOMString vlinkColor;
+ [CEReactions] attribute [LegacyNullToEmptyString] DOMString alinkColor;
+ [CEReactions] attribute [LegacyNullToEmptyString] DOMString bgColor;
+
+ [SameObject] readonly attribute HTMLCollection anchors;
+ [SameObject] readonly attribute HTMLCollection applets;
+
+ undefined clear();
+ undefined captureEvents();
+ undefined releaseEvents();
+
+ [SameObject] readonly attribute HTMLAllCollection all;
+};
+
+partial interface Window {
+ undefined captureEvents();
+ undefined releaseEvents();
+
+ [Replaceable, SameObject] readonly attribute External external;
+};
+
+[Exposed=Window]
+interface External {
+ undefined AddSearchProvider();
+ undefined IsSearchProviderInstalled();
+};
diff --git a/test/wpt/tests/interfaces/idle-detection.idl b/test/wpt/tests/interfaces/idle-detection.idl
new file mode 100644
index 0000000..54d42f3
--- /dev/null
+++ b/test/wpt/tests/interfaces/idle-detection.idl
@@ -0,0 +1,31 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Idle Detection API (https://wicg.github.io/idle-detection/)
+
+enum UserIdleState {
+ "active",
+ "idle"
+};
+
+enum ScreenIdleState {
+ "locked",
+ "unlocked"
+};
+
+dictionary IdleOptions {
+ [EnforceRange] unsigned long long threshold;
+ AbortSignal signal;
+};
+
+[
+ SecureContext,
+ Exposed=(Window,DedicatedWorker)
+] interface IdleDetector : EventTarget {
+ constructor();
+ readonly attribute UserIdleState? userState;
+ readonly attribute ScreenIdleState? screenState;
+ attribute EventHandler onchange;
+ [Exposed=Window] static Promise<PermissionState> requestPermission();
+ Promise<undefined> start(optional IdleOptions options = {});
+};
diff --git a/test/wpt/tests/interfaces/image-capture.idl b/test/wpt/tests/interfaces/image-capture.idl
new file mode 100644
index 0000000..f98912c
--- /dev/null
+++ b/test/wpt/tests/interfaces/image-capture.idl
@@ -0,0 +1,160 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: MediaStream Image Capture (https://w3c.github.io/mediacapture-image/)
+
+[Exposed=Window]
+interface ImageCapture {
+ constructor(MediaStreamTrack videoTrack);
+ Promise<Blob> takePhoto(optional PhotoSettings photoSettings = {});
+ Promise<PhotoCapabilities> getPhotoCapabilities();
+ Promise<PhotoSettings> getPhotoSettings();
+
+ Promise<ImageBitmap> grabFrame();
+
+ readonly attribute MediaStreamTrack track;
+};
+
+dictionary PhotoCapabilities {
+ RedEyeReduction redEyeReduction;
+ MediaSettingsRange imageHeight;
+ MediaSettingsRange imageWidth;
+ sequence<FillLightMode> fillLightMode;
+};
+
+dictionary PhotoSettings {
+ FillLightMode fillLightMode;
+ double imageHeight;
+ double imageWidth;
+ boolean redEyeReduction;
+};
+
+dictionary MediaSettingsRange {
+ double max;
+ double min;
+ double step;
+};
+
+enum RedEyeReduction {
+ "never",
+ "always",
+ "controllable"
+};
+
+enum FillLightMode {
+ "auto",
+ "off",
+ "flash"
+};
+
+partial dictionary MediaTrackSupportedConstraints {
+ boolean whiteBalanceMode = true;
+ boolean exposureMode = true;
+ boolean focusMode = true;
+ boolean pointsOfInterest = true;
+
+ boolean exposureCompensation = true;
+ boolean exposureTime = true;
+ boolean colorTemperature = true;
+ boolean iso = true;
+
+ boolean brightness = true;
+ boolean contrast = true;
+ boolean pan = true;
+ boolean saturation = true;
+ boolean sharpness = true;
+ boolean focusDistance = true;
+ boolean tilt = true;
+ boolean zoom = true;
+ boolean torch = true;
+};
+
+partial dictionary MediaTrackCapabilities {
+ sequence<DOMString> whiteBalanceMode;
+ sequence<DOMString> exposureMode;
+ sequence<DOMString> focusMode;
+
+ MediaSettingsRange exposureCompensation;
+ MediaSettingsRange exposureTime;
+ MediaSettingsRange colorTemperature;
+ MediaSettingsRange iso;
+
+ MediaSettingsRange brightness;
+ MediaSettingsRange contrast;
+ MediaSettingsRange saturation;
+ MediaSettingsRange sharpness;
+
+ MediaSettingsRange focusDistance;
+ MediaSettingsRange pan;
+ MediaSettingsRange tilt;
+ MediaSettingsRange zoom;
+
+ boolean torch;
+};
+
+partial dictionary MediaTrackConstraintSet {
+ ConstrainDOMString whiteBalanceMode;
+ ConstrainDOMString exposureMode;
+ ConstrainDOMString focusMode;
+ ConstrainPoint2D pointsOfInterest;
+
+ ConstrainDouble exposureCompensation;
+ ConstrainDouble exposureTime;
+ ConstrainDouble colorTemperature;
+ ConstrainDouble iso;
+
+ ConstrainDouble brightness;
+ ConstrainDouble contrast;
+ ConstrainDouble saturation;
+ ConstrainDouble sharpness;
+
+ ConstrainDouble focusDistance;
+ (boolean or ConstrainDouble) pan;
+ (boolean or ConstrainDouble) tilt;
+ (boolean or ConstrainDouble) zoom;
+
+ ConstrainBoolean torch;
+};
+
+partial dictionary MediaTrackSettings {
+ DOMString whiteBalanceMode;
+ DOMString exposureMode;
+ DOMString focusMode;
+ sequence<Point2D> pointsOfInterest;
+
+ double exposureCompensation;
+ double exposureTime;
+ double colorTemperature;
+ double iso;
+
+ double brightness;
+ double contrast;
+ double saturation;
+ double sharpness;
+
+ double focusDistance;
+ double pan;
+ double tilt;
+ double zoom;
+
+ boolean torch;
+};
+
+dictionary ConstrainPoint2DParameters {
+ sequence<Point2D> exact;
+ sequence<Point2D> ideal;
+};
+
+typedef (sequence<Point2D> or ConstrainPoint2DParameters) ConstrainPoint2D;
+
+enum MeteringMode {
+ "none",
+ "manual",
+ "single-shot",
+ "continuous"
+};
+
+dictionary Point2D {
+ double x = 0.0;
+ double y = 0.0;
+};
diff --git a/test/wpt/tests/interfaces/image-resource.idl b/test/wpt/tests/interfaces/image-resource.idl
new file mode 100644
index 0000000..d7f653e
--- /dev/null
+++ b/test/wpt/tests/interfaces/image-resource.idl
@@ -0,0 +1,11 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Image Resource (https://w3c.github.io/image-resource/)
+
+dictionary ImageResource {
+ required USVString src;
+ DOMString sizes;
+ DOMString type;
+ DOMString label;
+};
diff --git a/test/wpt/tests/interfaces/ink-enhancement.idl b/test/wpt/tests/interfaces/ink-enhancement.idl
new file mode 100644
index 0000000..660e225
--- /dev/null
+++ b/test/wpt/tests/interfaces/ink-enhancement.idl
@@ -0,0 +1,32 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Ink API (https://wicg.github.io/ink-enhancement/)
+
+[Exposed=Window]
+interface Ink {
+ Promise<InkPresenter> requestPresenter(
+ optional InkPresenterParam param = {});
+};
+
+dictionary InkPresenterParam {
+ Element? presentationArea = null;
+};
+
+[Exposed=Window]
+interface InkPresenter {
+ readonly attribute Element? presentationArea;
+ readonly attribute unsigned long expectedImprovement;
+
+ undefined updateInkTrailStartPoint(PointerEvent event, InkTrailStyle style);
+};
+
+dictionary InkTrailStyle {
+ required DOMString color;
+ required unrestricted double diameter;
+};
+
+[Exposed=Window]
+partial interface Navigator {
+ [SameObject] readonly attribute Ink ink;
+};
diff --git a/test/wpt/tests/interfaces/input-device-capabilities.idl b/test/wpt/tests/interfaces/input-device-capabilities.idl
new file mode 100644
index 0000000..72d91de
--- /dev/null
+++ b/test/wpt/tests/interfaces/input-device-capabilities.idl
@@ -0,0 +1,24 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Input Device Capabilities (https://wicg.github.io/input-device-capabilities/)
+
+[Exposed=Window]
+interface InputDeviceCapabilities {
+ constructor(optional InputDeviceCapabilitiesInit deviceInitDict = {});
+ readonly attribute boolean firesTouchEvents;
+ readonly attribute boolean pointerMovementScrolls;
+};
+
+dictionary InputDeviceCapabilitiesInit {
+ boolean firesTouchEvents = false;
+ boolean pointerMovementScrolls = false;
+};
+
+partial interface UIEvent {
+ readonly attribute InputDeviceCapabilities? sourceCapabilities;
+};
+
+partial dictionary UIEventInit {
+ InputDeviceCapabilities? sourceCapabilities = null;
+};
diff --git a/test/wpt/tests/interfaces/input-events.idl b/test/wpt/tests/interfaces/input-events.idl
new file mode 100644
index 0000000..6a2147b
--- /dev/null
+++ b/test/wpt/tests/interfaces/input-events.idl
@@ -0,0 +1,14 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Input Events Level 2 (https://w3c.github.io/input-events/)
+
+partial interface InputEvent {
+ readonly attribute DataTransfer? dataTransfer;
+ sequence<StaticRange> getTargetRanges();
+};
+
+partial dictionary InputEventInit {
+ DataTransfer? dataTransfer = null;
+ sequence<StaticRange> targetRanges = [];
+};
diff --git a/test/wpt/tests/interfaces/intersection-observer.idl b/test/wpt/tests/interfaces/intersection-observer.idl
new file mode 100644
index 0000000..52db1c4
--- /dev/null
+++ b/test/wpt/tests/interfaces/intersection-observer.idl
@@ -0,0 +1,46 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Intersection Observer (https://w3c.github.io/IntersectionObserver/)
+
+callback IntersectionObserverCallback = undefined (sequence<IntersectionObserverEntry> entries, IntersectionObserver observer);
+
+[Exposed=Window]
+interface IntersectionObserver {
+ constructor(IntersectionObserverCallback callback, optional IntersectionObserverInit options = {});
+ readonly attribute (Element or Document)? root;
+ readonly attribute DOMString rootMargin;
+ readonly attribute FrozenArray<double> thresholds;
+ undefined observe(Element target);
+ undefined unobserve(Element target);
+ undefined disconnect();
+ sequence<IntersectionObserverEntry> takeRecords();
+};
+
+[Exposed=Window]
+interface IntersectionObserverEntry {
+ constructor(IntersectionObserverEntryInit intersectionObserverEntryInit);
+ readonly attribute DOMHighResTimeStamp time;
+ readonly attribute DOMRectReadOnly? rootBounds;
+ readonly attribute DOMRectReadOnly boundingClientRect;
+ readonly attribute DOMRectReadOnly intersectionRect;
+ readonly attribute boolean isIntersecting;
+ readonly attribute double intersectionRatio;
+ readonly attribute Element target;
+};
+
+dictionary IntersectionObserverEntryInit {
+ required DOMHighResTimeStamp time;
+ required DOMRectInit? rootBounds;
+ required DOMRectInit boundingClientRect;
+ required DOMRectInit intersectionRect;
+ required boolean isIntersecting;
+ required double intersectionRatio;
+ required Element target;
+};
+
+dictionary IntersectionObserverInit {
+ (Element or Document)? root = null;
+ DOMString rootMargin = "0px";
+ (double or sequence<double>) threshold = 0;
+};
diff --git a/test/wpt/tests/interfaces/intervention-reporting.idl b/test/wpt/tests/interfaces/intervention-reporting.idl
new file mode 100644
index 0000000..3c3b800
--- /dev/null
+++ b/test/wpt/tests/interfaces/intervention-reporting.idl
@@ -0,0 +1,14 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Intervention Reporting (https://wicg.github.io/intervention-reporting/)
+
+[Exposed=(Window,Worker)]
+interface InterventionReportBody : ReportBody {
+ [Default] object toJSON();
+ readonly attribute DOMString id;
+ readonly attribute DOMString message;
+ readonly attribute DOMString? sourceFile;
+ readonly attribute unsigned long? lineNumber;
+ readonly attribute unsigned long? columnNumber;
+};
diff --git a/test/wpt/tests/interfaces/is-input-pending.idl b/test/wpt/tests/interfaces/is-input-pending.idl
new file mode 100644
index 0000000..735bdf0
--- /dev/null
+++ b/test/wpt/tests/interfaces/is-input-pending.idl
@@ -0,0 +1,16 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Early detection of input events (https://wicg.github.io/is-input-pending/)
+
+dictionary IsInputPendingOptions {
+ boolean includeContinuous = false;
+};
+
+[Exposed=Window] interface Scheduling {
+ boolean isInputPending(optional IsInputPendingOptions isInputPendingOptions = {});
+};
+
+partial interface Navigator {
+ readonly attribute Scheduling scheduling;
+};
diff --git a/test/wpt/tests/interfaces/js-self-profiling.idl b/test/wpt/tests/interfaces/js-self-profiling.idl
new file mode 100644
index 0000000..04cd1af
--- /dev/null
+++ b/test/wpt/tests/interfaces/js-self-profiling.idl
@@ -0,0 +1,44 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: JS Self-Profiling API (https://wicg.github.io/js-self-profiling/)
+
+[Exposed=Window]
+interface Profiler : EventTarget {
+ readonly attribute DOMHighResTimeStamp sampleInterval;
+ readonly attribute boolean stopped;
+
+ constructor(ProfilerInitOptions options);
+ Promise<ProfilerTrace> stop();
+};
+
+typedef DOMString ProfilerResource;
+
+dictionary ProfilerTrace {
+ required sequence<ProfilerResource> resources;
+ required sequence<ProfilerFrame> frames;
+ required sequence<ProfilerStack> stacks;
+ required sequence<ProfilerSample> samples;
+};
+
+dictionary ProfilerSample {
+ required DOMHighResTimeStamp timestamp;
+ unsigned long long stackId;
+};
+
+dictionary ProfilerStack {
+ unsigned long long parentId;
+ required unsigned long long frameId;
+};
+
+dictionary ProfilerFrame {
+ required DOMString name;
+ unsigned long long resourceId;
+ unsigned long long line;
+ unsigned long long column;
+};
+
+dictionary ProfilerInitOptions {
+ required DOMHighResTimeStamp sampleInterval;
+ required unsigned long maxBufferSize;
+};
diff --git a/test/wpt/tests/interfaces/keyboard-lock.idl b/test/wpt/tests/interfaces/keyboard-lock.idl
new file mode 100644
index 0000000..d81e992
--- /dev/null
+++ b/test/wpt/tests/interfaces/keyboard-lock.idl
@@ -0,0 +1,14 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Keyboard Lock (https://wicg.github.io/keyboard-lock/)
+
+partial interface Navigator {
+ [SecureContext, SameObject] readonly attribute Keyboard keyboard;
+};
+
+[SecureContext, Exposed=Window]
+interface Keyboard : EventTarget {
+ Promise<undefined> lock(optional sequence<DOMString> keyCodes = []);
+ undefined unlock();
+};
diff --git a/test/wpt/tests/interfaces/keyboard-map.idl b/test/wpt/tests/interfaces/keyboard-map.idl
new file mode 100644
index 0000000..5103734
--- /dev/null
+++ b/test/wpt/tests/interfaces/keyboard-map.idl
@@ -0,0 +1,15 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Keyboard Map (https://wicg.github.io/keyboard-map/)
+
+[Exposed=Window]
+interface KeyboardLayoutMap {
+ readonly maplike<DOMString, DOMString>;
+};
+
+partial interface Keyboard {
+ Promise<KeyboardLayoutMap> getLayoutMap();
+
+ attribute EventHandler onlayoutchange;
+};
diff --git a/test/wpt/tests/interfaces/largest-contentful-paint.idl b/test/wpt/tests/interfaces/largest-contentful-paint.idl
new file mode 100644
index 0000000..872ba55
--- /dev/null
+++ b/test/wpt/tests/interfaces/largest-contentful-paint.idl
@@ -0,0 +1,15 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Largest Contentful Paint (https://w3c.github.io/largest-contentful-paint/)
+
+[Exposed=Window]
+interface LargestContentfulPaint : PerformanceEntry {
+ readonly attribute DOMHighResTimeStamp renderTime;
+ readonly attribute DOMHighResTimeStamp loadTime;
+ readonly attribute unsigned long size;
+ readonly attribute DOMString id;
+ readonly attribute DOMString url;
+ readonly attribute Element? element;
+ [Default] object toJSON();
+};
diff --git a/test/wpt/tests/interfaces/layout-instability.idl b/test/wpt/tests/interfaces/layout-instability.idl
new file mode 100644
index 0000000..4fb1b70
--- /dev/null
+++ b/test/wpt/tests/interfaces/layout-instability.idl
@@ -0,0 +1,20 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Layout Instability API (https://wicg.github.io/layout-instability/)
+
+[Exposed=Window]
+interface LayoutShift : PerformanceEntry {
+ readonly attribute double value;
+ readonly attribute boolean hadRecentInput;
+ readonly attribute DOMHighResTimeStamp lastInputTime;
+ readonly attribute FrozenArray<LayoutShiftAttribution> sources;
+ [Default] object toJSON();
+};
+
+[Exposed=Window]
+interface LayoutShiftAttribution {
+ readonly attribute Node? node;
+ readonly attribute DOMRectReadOnly previousRect;
+ readonly attribute DOMRectReadOnly currentRect;
+};
diff --git a/test/wpt/tests/interfaces/local-font-access.idl b/test/wpt/tests/interfaces/local-font-access.idl
new file mode 100644
index 0000000..10e2e1f
--- /dev/null
+++ b/test/wpt/tests/interfaces/local-font-access.idl
@@ -0,0 +1,24 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Local Font Access API (https://wicg.github.io/local-font-access/)
+
+[SecureContext]
+partial interface Window {
+ Promise<sequence<FontData>> queryLocalFonts(optional QueryOptions options = {});
+};
+
+dictionary QueryOptions {
+ sequence<DOMString> postscriptNames;
+};
+
+[Exposed=Window]
+interface FontData {
+ Promise<Blob> blob();
+
+ // Names
+ readonly attribute USVString postscriptName;
+ readonly attribute USVString fullName;
+ readonly attribute USVString family;
+ readonly attribute USVString style;
+};
diff --git a/test/wpt/tests/interfaces/longtasks.idl b/test/wpt/tests/interfaces/longtasks.idl
new file mode 100644
index 0000000..c0aaa7a
--- /dev/null
+++ b/test/wpt/tests/interfaces/longtasks.idl
@@ -0,0 +1,19 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Long Tasks API (https://w3c.github.io/longtasks/)
+
+[Exposed=Window]
+interface PerformanceLongTaskTiming : PerformanceEntry {
+ readonly attribute FrozenArray<TaskAttributionTiming> attribution;
+ [Default] object toJSON();
+};
+
+[Exposed=Window]
+interface TaskAttributionTiming : PerformanceEntry {
+ readonly attribute DOMString containerType;
+ readonly attribute DOMString containerSrc;
+ readonly attribute DOMString containerId;
+ readonly attribute DOMString containerName;
+ [Default] object toJSON();
+};
diff --git a/test/wpt/tests/interfaces/magnetometer.idl b/test/wpt/tests/interfaces/magnetometer.idl
new file mode 100644
index 0000000..45ba9ed
--- /dev/null
+++ b/test/wpt/tests/interfaces/magnetometer.idl
@@ -0,0 +1,46 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Magnetometer (https://w3c.github.io/magnetometer/)
+
+[SecureContext,
+ Exposed=Window]
+interface Magnetometer : Sensor {
+ constructor(optional MagnetometerSensorOptions sensorOptions = {});
+ readonly attribute double? x;
+ readonly attribute double? y;
+ readonly attribute double? z;
+};
+
+enum MagnetometerLocalCoordinateSystem { "device", "screen" };
+
+dictionary MagnetometerSensorOptions : SensorOptions {
+ MagnetometerLocalCoordinateSystem referenceFrame = "device";
+};
+
+[SecureContext,
+ Exposed=Window]
+interface UncalibratedMagnetometer : Sensor {
+ constructor(optional MagnetometerSensorOptions sensorOptions = {});
+ readonly attribute double? x;
+ readonly attribute double? y;
+ readonly attribute double? z;
+ readonly attribute double? xBias;
+ readonly attribute double? yBias;
+ readonly attribute double? zBias;
+};
+
+dictionary MagnetometerReadingValues {
+ required double? x;
+ required double? y;
+ required double? z;
+};
+
+dictionary UncalibratedMagnetometerReadingValues {
+ required double? x;
+ required double? y;
+ required double? z;
+ required double? xBias;
+ required double? yBias;
+ required double? zBias;
+};
diff --git a/test/wpt/tests/interfaces/manifest-incubations.idl b/test/wpt/tests/interfaces/manifest-incubations.idl
new file mode 100644
index 0000000..bab3998
--- /dev/null
+++ b/test/wpt/tests/interfaces/manifest-incubations.idl
@@ -0,0 +1,24 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Manifest Incubations (https://wicg.github.io/manifest-incubations/)
+
+[Exposed=Window]
+interface BeforeInstallPromptEvent : Event {
+ constructor(DOMString type, optional EventInit eventInitDict = {});
+ Promise<PromptResponseObject> prompt();
+};
+
+dictionary PromptResponseObject {
+ AppBannerPromptOutcome userChoice;
+};
+
+enum AppBannerPromptOutcome {
+ "accepted",
+ "dismissed"
+};
+
+partial interface Window {
+ attribute EventHandler onappinstalled;
+ attribute EventHandler onbeforeinstallprompt;
+};
diff --git a/test/wpt/tests/interfaces/mathml-core.idl b/test/wpt/tests/interfaces/mathml-core.idl
new file mode 100644
index 0000000..fb5539e
--- /dev/null
+++ b/test/wpt/tests/interfaces/mathml-core.idl
@@ -0,0 +1,9 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: MathML Core (https://w3c.github.io/mathml-core/)
+
+[Exposed=Window]
+interface MathMLElement : Element { };
+MathMLElement includes GlobalEventHandlers;
+MathMLElement includes HTMLOrSVGElement;
diff --git a/test/wpt/tests/interfaces/media-capabilities.idl b/test/wpt/tests/interfaces/media-capabilities.idl
new file mode 100644
index 0000000..94339b6
--- /dev/null
+++ b/test/wpt/tests/interfaces/media-capabilities.idl
@@ -0,0 +1,115 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Media Capabilities (https://w3c.github.io/media-capabilities/)
+
+dictionary MediaConfiguration {
+ VideoConfiguration video;
+ AudioConfiguration audio;
+};
+
+dictionary MediaDecodingConfiguration : MediaConfiguration {
+ required MediaDecodingType type;
+ MediaCapabilitiesKeySystemConfiguration keySystemConfiguration;
+};
+
+dictionary MediaEncodingConfiguration : MediaConfiguration {
+ required MediaEncodingType type;
+};
+
+enum MediaDecodingType {
+ "file",
+ "media-source",
+ "webrtc"
+};
+
+enum MediaEncodingType {
+ "record",
+ "webrtc"
+};
+
+dictionary VideoConfiguration {
+ required DOMString contentType;
+ required unsigned long width;
+ required unsigned long height;
+ required unsigned long long bitrate;
+ required double framerate;
+ boolean hasAlphaChannel;
+ HdrMetadataType hdrMetadataType;
+ ColorGamut colorGamut;
+ TransferFunction transferFunction;
+ DOMString scalabilityMode;
+ boolean spatialScalability;
+};
+
+enum HdrMetadataType {
+ "smpteSt2086",
+ "smpteSt2094-10",
+ "smpteSt2094-40"
+};
+
+enum ColorGamut {
+ "srgb",
+ "p3",
+ "rec2020"
+};
+
+enum TransferFunction {
+ "srgb",
+ "pq",
+ "hlg"
+};
+
+dictionary AudioConfiguration {
+ required DOMString contentType;
+ DOMString channels;
+ unsigned long long bitrate;
+ unsigned long samplerate;
+ boolean spatialRendering;
+};
+
+dictionary MediaCapabilitiesKeySystemConfiguration {
+ required DOMString keySystem;
+ DOMString initDataType = "";
+ MediaKeysRequirement distinctiveIdentifier = "optional";
+ MediaKeysRequirement persistentState = "optional";
+ sequence<DOMString> sessionTypes;
+ KeySystemTrackConfiguration audio;
+ KeySystemTrackConfiguration video;
+};
+
+dictionary KeySystemTrackConfiguration {
+ DOMString robustness = "";
+ DOMString? encryptionScheme = null;
+};
+
+dictionary MediaCapabilitiesInfo {
+ required boolean supported;
+ required boolean smooth;
+ required boolean powerEfficient;
+};
+
+dictionary MediaCapabilitiesDecodingInfo : MediaCapabilitiesInfo {
+ required MediaKeySystemAccess keySystemAccess;
+ MediaDecodingConfiguration configuration;
+};
+
+dictionary MediaCapabilitiesEncodingInfo : MediaCapabilitiesInfo {
+ MediaEncodingConfiguration configuration;
+};
+
+[Exposed=Window]
+partial interface Navigator {
+ [SameObject] readonly attribute MediaCapabilities mediaCapabilities;
+};
+
+[Exposed=Worker]
+partial interface WorkerNavigator {
+ [SameObject] readonly attribute MediaCapabilities mediaCapabilities;
+};
+
+[Exposed=(Window, Worker)]
+interface MediaCapabilities {
+ [NewObject] Promise<MediaCapabilitiesDecodingInfo> decodingInfo(MediaDecodingConfiguration configuration);
+ [NewObject] Promise<MediaCapabilitiesEncodingInfo> encodingInfo(MediaEncodingConfiguration configuration);
+};
diff --git a/test/wpt/tests/interfaces/media-playback-quality.idl b/test/wpt/tests/interfaces/media-playback-quality.idl
new file mode 100644
index 0000000..f73d8db
--- /dev/null
+++ b/test/wpt/tests/interfaces/media-playback-quality.idl
@@ -0,0 +1,18 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Media Playback Quality (https://w3c.github.io/media-playback-quality/)
+
+partial interface HTMLVideoElement {
+ VideoPlaybackQuality getVideoPlaybackQuality();
+};
+
+[Exposed=Window]
+interface VideoPlaybackQuality {
+ readonly attribute DOMHighResTimeStamp creationTime;
+ readonly attribute unsigned long droppedVideoFrames;
+ readonly attribute unsigned long totalVideoFrames;
+
+ // Deprecated!
+ readonly attribute unsigned long corruptedVideoFrames;
+};
diff --git a/test/wpt/tests/interfaces/media-source.idl b/test/wpt/tests/interfaces/media-source.idl
new file mode 100644
index 0000000..1105943
--- /dev/null
+++ b/test/wpt/tests/interfaces/media-source.idl
@@ -0,0 +1,91 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Media Source Extensionsâ„¢ (https://w3c.github.io/media-source/)
+
+enum ReadyState {
+ "closed",
+ "open",
+ "ended"
+};
+
+enum EndOfStreamError {
+ "network",
+ "decode"
+};
+
+[Exposed=(Window,DedicatedWorker)]
+interface MediaSource : EventTarget {
+ constructor();
+
+ [ SameObject, Exposed=DedicatedWorker ]
+ readonly attribute MediaSourceHandle handle;
+
+ readonly attribute SourceBufferList sourceBuffers;
+ readonly attribute SourceBufferList activeSourceBuffers;
+ readonly attribute ReadyState readyState;
+ attribute unrestricted double duration;
+ attribute EventHandler onsourceopen;
+ attribute EventHandler onsourceended;
+ attribute EventHandler onsourceclose;
+ static readonly attribute boolean canConstructInDedicatedWorker;
+ SourceBuffer addSourceBuffer (DOMString type);
+ undefined removeSourceBuffer (SourceBuffer sourceBuffer);
+ undefined endOfStream (optional EndOfStreamError error);
+ undefined setLiveSeekableRange (double start, double end);
+ undefined clearLiveSeekableRange ();
+ static boolean isTypeSupported (DOMString type);
+};
+
+[Transferable, Exposed=(Window,DedicatedWorker)]
+interface MediaSourceHandle {};
+
+enum AppendMode {
+ "segments",
+ "sequence"
+};
+
+[Exposed=(Window,DedicatedWorker)]
+interface SourceBuffer : EventTarget {
+ attribute AppendMode mode;
+ readonly attribute boolean updating;
+ readonly attribute TimeRanges buffered;
+ attribute double timestampOffset;
+ readonly attribute AudioTrackList audioTracks;
+ readonly attribute VideoTrackList videoTracks;
+ readonly attribute TextTrackList textTracks;
+ attribute double appendWindowStart;
+ attribute unrestricted double appendWindowEnd;
+ attribute EventHandler onupdatestart;
+ attribute EventHandler onupdate;
+ attribute EventHandler onupdateend;
+ attribute EventHandler onerror;
+ attribute EventHandler onabort;
+ undefined appendBuffer (BufferSource data);
+ undefined abort ();
+ undefined changeType (DOMString type);
+ undefined remove (double start, unrestricted double end);
+};
+
+[Exposed=(Window,DedicatedWorker)]
+interface SourceBufferList : EventTarget {
+ readonly attribute unsigned long length;
+ attribute EventHandler onaddsourcebuffer;
+ attribute EventHandler onremovesourcebuffer;
+ getter SourceBuffer (unsigned long index);
+};
+
+[Exposed=(Window,DedicatedWorker)]
+partial interface AudioTrack {
+ readonly attribute SourceBuffer? sourceBuffer;
+};
+
+[Exposed=(Window,DedicatedWorker)]
+partial interface VideoTrack {
+ readonly attribute SourceBuffer? sourceBuffer;
+};
+
+[Exposed=(Window,DedicatedWorker)]
+partial interface TextTrack {
+ readonly attribute SourceBuffer? sourceBuffer;
+};
diff --git a/test/wpt/tests/interfaces/mediacapture-automation.idl b/test/wpt/tests/interfaces/mediacapture-automation.idl
new file mode 100644
index 0000000..9fe2623
--- /dev/null
+++ b/test/wpt/tests/interfaces/mediacapture-automation.idl
@@ -0,0 +1,36 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Media Capture Automation (https://w3c.github.io/mediacapture-automation/)
+
+enum MockCapturePromptResult {
+ "granted",
+ "denied"
+};
+
+dictionary MockCapturePromptResultConfiguration {
+ MockCapturePromptResult getUserMedia;
+ MockCapturePromptResult getDisplayMedia;
+};
+
+dictionary MockCaptureDeviceConfiguration {
+ DOMString label;
+ DOMString deviceId;
+ DOMString groupId;
+};
+
+dictionary MockCameraConfiguration : MockCaptureDeviceConfiguration {
+ double defaultFrameRate = 30;
+ DOMString facingMode = "user";
+ // TODO: Add more capabilities parameters like:
+ // ULongRange width;
+ // ULongRange height;
+ // DoubleRange frameRate;
+};
+
+dictionary MockMicrophoneConfiguration : MockCaptureDeviceConfiguration {
+ unsigned long defaultSampleRate = 44100;
+ // TODO: Add more capabilities parameters like:
+ // ULongRange sampleRate;
+ // sequence echoCancellation;
+};
diff --git a/test/wpt/tests/interfaces/mediacapture-fromelement.idl b/test/wpt/tests/interfaces/mediacapture-fromelement.idl
new file mode 100644
index 0000000..b25f870
--- /dev/null
+++ b/test/wpt/tests/interfaces/mediacapture-fromelement.idl
@@ -0,0 +1,17 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Media Capture from DOM Elements (https://w3c.github.io/mediacapture-fromelement/)
+
+partial interface HTMLMediaElement {
+ MediaStream captureStream ();
+};
+
+partial interface HTMLCanvasElement {
+ MediaStream captureStream (optional double frameRequestRate);
+};
+
+[Exposed=Window] interface CanvasCaptureMediaStreamTrack : MediaStreamTrack {
+ readonly attribute HTMLCanvasElement canvas;
+ undefined requestFrame ();
+};
diff --git a/test/wpt/tests/interfaces/mediacapture-handle-actions.idl b/test/wpt/tests/interfaces/mediacapture-handle-actions.idl
new file mode 100644
index 0000000..408f8e8
--- /dev/null
+++ b/test/wpt/tests/interfaces/mediacapture-handle-actions.idl
@@ -0,0 +1,31 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: The Capture-Handle Actions Mechanism (https://w3c.github.io/mediacapture-handle/actions/)
+
+partial interface MediaDevices {
+ undefined setSupportedCaptureActions(sequence<DOMString> actions);
+ attribute EventHandler oncaptureaction;
+};
+
+enum CaptureAction {
+ "next",
+ "previous",
+ "first",
+ "last"
+};
+
+[Exposed=Window]
+interface CaptureActionEvent : Event {
+ constructor(optional CaptureActionEventInit init = {});
+ readonly attribute CaptureAction action;
+};
+
+dictionary CaptureActionEventInit : EventInit {
+ DOMString action;
+};
+
+partial interface MediaStreamTrack {
+ sequence<DOMString> getSupportedCaptureActions();
+ Promise<undefined> sendCaptureAction(CaptureAction action);
+};
diff --git a/test/wpt/tests/interfaces/mediacapture-region.idl b/test/wpt/tests/interfaces/mediacapture-region.idl
new file mode 100644
index 0000000..7a5fb7f
--- /dev/null
+++ b/test/wpt/tests/interfaces/mediacapture-region.idl
@@ -0,0 +1,15 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Region Capture (https://w3c.github.io/mediacapture-region/)
+
+[Exposed=(Window,Worker), Serializable]
+interface CropTarget {
+ [Exposed=Window, SecureContext] static Promise<CropTarget> fromElement(Element element);
+};
+
+[Exposed = Window]
+interface BrowserCaptureMediaStreamTrack : MediaStreamTrack {
+ Promise<undefined> cropTo(CropTarget? cropTarget);
+ BrowserCaptureMediaStreamTrack clone();
+};
diff --git a/test/wpt/tests/interfaces/mediacapture-streams.idl b/test/wpt/tests/interfaces/mediacapture-streams.idl
new file mode 100644
index 0000000..3197ff7
--- /dev/null
+++ b/test/wpt/tests/interfaces/mediacapture-streams.idl
@@ -0,0 +1,248 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Media Capture and Streams (https://w3c.github.io/mediacapture-main/)
+
+[Exposed=Window]
+interface MediaStream : EventTarget {
+ constructor();
+ constructor(MediaStream stream);
+ constructor(sequence<MediaStreamTrack> tracks);
+ readonly attribute DOMString id;
+ sequence<MediaStreamTrack> getAudioTracks();
+ sequence<MediaStreamTrack> getVideoTracks();
+ sequence<MediaStreamTrack> getTracks();
+ MediaStreamTrack? getTrackById(DOMString trackId);
+ undefined addTrack(MediaStreamTrack track);
+ undefined removeTrack(MediaStreamTrack track);
+ MediaStream clone();
+ readonly attribute boolean active;
+ attribute EventHandler onaddtrack;
+ attribute EventHandler onremovetrack;
+};
+
+[Exposed=Window]
+interface MediaStreamTrack : EventTarget {
+ readonly attribute DOMString kind;
+ readonly attribute DOMString id;
+ readonly attribute DOMString label;
+ attribute boolean enabled;
+ readonly attribute boolean muted;
+ attribute EventHandler onmute;
+ attribute EventHandler onunmute;
+ readonly attribute MediaStreamTrackState readyState;
+ attribute EventHandler onended;
+ MediaStreamTrack clone();
+ undefined stop();
+ MediaTrackCapabilities getCapabilities();
+ MediaTrackConstraints getConstraints();
+ MediaTrackSettings getSettings();
+ Promise<undefined> applyConstraints(optional MediaTrackConstraints constraints = {});
+};
+
+enum MediaStreamTrackState {
+ "live",
+ "ended"
+};
+
+dictionary MediaTrackSupportedConstraints {
+ boolean width = true;
+ boolean height = true;
+ boolean aspectRatio = true;
+ boolean frameRate = true;
+ boolean facingMode = true;
+ boolean resizeMode = true;
+ boolean sampleRate = true;
+ boolean sampleSize = true;
+ boolean echoCancellation = true;
+ boolean autoGainControl = true;
+ boolean noiseSuppression = true;
+ boolean latency = true;
+ boolean channelCount = true;
+ boolean deviceId = true;
+ boolean groupId = true;
+};
+
+dictionary MediaTrackCapabilities {
+ ULongRange width;
+ ULongRange height;
+ DoubleRange aspectRatio;
+ DoubleRange frameRate;
+ sequence<DOMString> facingMode;
+ sequence<DOMString> resizeMode;
+ ULongRange sampleRate;
+ ULongRange sampleSize;
+ sequence<boolean> echoCancellation;
+ sequence<boolean> autoGainControl;
+ sequence<boolean> noiseSuppression;
+ DoubleRange latency;
+ ULongRange channelCount;
+ DOMString deviceId;
+ DOMString groupId;
+};
+
+dictionary MediaTrackConstraints : MediaTrackConstraintSet {
+ sequence<MediaTrackConstraintSet> advanced;
+};
+
+dictionary MediaTrackConstraintSet {
+ ConstrainULong width;
+ ConstrainULong height;
+ ConstrainDouble aspectRatio;
+ ConstrainDouble frameRate;
+ ConstrainDOMString facingMode;
+ ConstrainDOMString resizeMode;
+ ConstrainULong sampleRate;
+ ConstrainULong sampleSize;
+ ConstrainBoolean echoCancellation;
+ ConstrainBoolean autoGainControl;
+ ConstrainBoolean noiseSuppression;
+ ConstrainDouble latency;
+ ConstrainULong channelCount;
+ ConstrainDOMString deviceId;
+ ConstrainDOMString groupId;
+};
+
+dictionary MediaTrackSettings {
+ unsigned long width;
+ unsigned long height;
+ double aspectRatio;
+ double frameRate;
+ DOMString facingMode;
+ DOMString resizeMode;
+ unsigned long sampleRate;
+ unsigned long sampleSize;
+ boolean echoCancellation;
+ boolean autoGainControl;
+ boolean noiseSuppression;
+ double latency;
+ unsigned long channelCount;
+ DOMString deviceId;
+ DOMString groupId;
+};
+
+enum VideoFacingModeEnum {
+ "user",
+ "environment",
+ "left",
+ "right"
+};
+
+enum VideoResizeModeEnum {
+ "none",
+ "crop-and-scale"
+};
+
+[Exposed=Window]
+interface MediaStreamTrackEvent : Event {
+ constructor(DOMString type, MediaStreamTrackEventInit eventInitDict);
+ [SameObject] readonly attribute MediaStreamTrack track;
+};
+
+dictionary MediaStreamTrackEventInit : EventInit {
+ required MediaStreamTrack track;
+};
+
+[Exposed=Window]
+interface OverconstrainedError : DOMException {
+ constructor(DOMString constraint, optional DOMString message = "");
+ readonly attribute DOMString constraint;
+};
+
+partial interface Navigator {
+ [SameObject, SecureContext] readonly attribute MediaDevices mediaDevices;
+};
+
+[Exposed=Window, SecureContext]
+interface MediaDevices : EventTarget {
+ attribute EventHandler ondevicechange;
+ Promise<sequence<MediaDeviceInfo>> enumerateDevices();
+};
+
+[Exposed=Window, SecureContext]
+interface MediaDeviceInfo {
+ readonly attribute DOMString deviceId;
+ readonly attribute MediaDeviceKind kind;
+ readonly attribute DOMString label;
+ readonly attribute DOMString groupId;
+ [Default] object toJSON();
+};
+
+enum MediaDeviceKind {
+ "audioinput",
+ "audiooutput",
+ "videoinput"
+};
+
+[Exposed=Window, SecureContext]
+interface InputDeviceInfo : MediaDeviceInfo {
+ MediaTrackCapabilities getCapabilities();
+};
+
+partial interface MediaDevices {
+ MediaTrackSupportedConstraints getSupportedConstraints();
+ Promise<MediaStream> getUserMedia(optional MediaStreamConstraints constraints = {});
+};
+
+dictionary MediaStreamConstraints {
+ (boolean or MediaTrackConstraints) video = false;
+ (boolean or MediaTrackConstraints) audio = false;
+};
+
+partial interface Navigator {
+ [SecureContext] undefined getUserMedia(MediaStreamConstraints constraints,
+ NavigatorUserMediaSuccessCallback successCallback,
+ NavigatorUserMediaErrorCallback errorCallback);
+};
+
+callback NavigatorUserMediaSuccessCallback = undefined (MediaStream stream);
+
+callback NavigatorUserMediaErrorCallback = undefined (DOMException error);
+
+dictionary DoubleRange {
+ double max;
+ double min;
+};
+
+dictionary ConstrainDoubleRange : DoubleRange {
+ double exact;
+ double ideal;
+};
+
+dictionary ULongRange {
+ [Clamp] unsigned long max;
+ [Clamp] unsigned long min;
+};
+
+dictionary ConstrainULongRange : ULongRange {
+ [Clamp] unsigned long exact;
+ [Clamp] unsigned long ideal;
+};
+
+dictionary ConstrainBooleanParameters {
+ boolean exact;
+ boolean ideal;
+};
+
+dictionary ConstrainDOMStringParameters {
+ (DOMString or sequence<DOMString>) exact;
+ (DOMString or sequence<DOMString>) ideal;
+};
+
+typedef ([Clamp] unsigned long or ConstrainULongRange) ConstrainULong;
+
+typedef (double or ConstrainDoubleRange) ConstrainDouble;
+
+typedef (boolean or ConstrainBooleanParameters) ConstrainBoolean;
+
+typedef (DOMString or
+sequence<DOMString> or
+ConstrainDOMStringParameters) ConstrainDOMString;
+
+dictionary DevicePermissionDescriptor : PermissionDescriptor {
+ DOMString deviceId;
+};
+
+dictionary CameraDevicePermissionDescriptor : DevicePermissionDescriptor {
+ boolean panTiltZoom = false;
+};
diff --git a/test/wpt/tests/interfaces/mediacapture-transform.idl b/test/wpt/tests/interfaces/mediacapture-transform.idl
new file mode 100644
index 0000000..5b2c8fa
--- /dev/null
+++ b/test/wpt/tests/interfaces/mediacapture-transform.idl
@@ -0,0 +1,23 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: MediaStreamTrack Insertable Media Processing using Streams (https://w3c.github.io/mediacapture-transform/)
+
+[Exposed=DedicatedWorker]
+interface MediaStreamTrackProcessor {
+ constructor(MediaStreamTrackProcessorInit init);
+ attribute ReadableStream readable;
+};
+
+dictionary MediaStreamTrackProcessorInit {
+ required MediaStreamTrack track;
+ [EnforceRange] unsigned short maxBufferSize;
+};
+
+[Exposed=DedicatedWorker]
+interface VideoTrackGenerator {
+ constructor();
+ readonly attribute WritableStream writable;
+ attribute boolean muted;
+ readonly attribute MediaStreamTrack track;
+};
diff --git a/test/wpt/tests/interfaces/mediacapture-viewport.idl b/test/wpt/tests/interfaces/mediacapture-viewport.idl
new file mode 100644
index 0000000..a9dcf74
--- /dev/null
+++ b/test/wpt/tests/interfaces/mediacapture-viewport.idl
@@ -0,0 +1,14 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Viewport Capture (https://w3c.github.io/mediacapture-viewport/)
+
+partial interface MediaDevices {
+ Promise<MediaStream> getViewportMedia(
+ optional ViewportMediaStreamConstraints constraints = {});
+};
+
+dictionary ViewportMediaStreamConstraints {
+ (boolean or MediaTrackConstraints) video = true;
+ (boolean or MediaTrackConstraints) audio = false;
+};
diff --git a/test/wpt/tests/interfaces/mediasession.idl b/test/wpt/tests/interfaces/mediasession.idl
new file mode 100644
index 0000000..cca46ac
--- /dev/null
+++ b/test/wpt/tests/interfaces/mediasession.idl
@@ -0,0 +1,84 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Media Session Standard (https://w3c.github.io/mediasession/)
+
+[Exposed=Window]
+partial interface Navigator {
+ [SameObject] readonly attribute MediaSession mediaSession;
+};
+
+enum MediaSessionPlaybackState {
+ "none",
+ "paused",
+ "playing"
+};
+
+enum MediaSessionAction {
+ "play",
+ "pause",
+ "seekbackward",
+ "seekforward",
+ "previoustrack",
+ "nexttrack",
+ "skipad",
+ "stop",
+ "seekto",
+ "togglemicrophone",
+ "togglecamera",
+ "hangup",
+ "previousslide",
+ "nextslide"
+};
+
+callback MediaSessionActionHandler = undefined(MediaSessionActionDetails details);
+
+[Exposed=Window]
+interface MediaSession {
+ attribute MediaMetadata? metadata;
+
+ attribute MediaSessionPlaybackState playbackState;
+
+ undefined setActionHandler(MediaSessionAction action, MediaSessionActionHandler? handler);
+
+ undefined setPositionState(optional MediaPositionState state = {});
+
+ undefined setMicrophoneActive(boolean active);
+
+ undefined setCameraActive(boolean active);
+};
+
+[Exposed=Window]
+interface MediaMetadata {
+ constructor(optional MediaMetadataInit init = {});
+ attribute DOMString title;
+ attribute DOMString artist;
+ attribute DOMString album;
+ attribute FrozenArray<MediaImage> artwork;
+};
+
+dictionary MediaMetadataInit {
+ DOMString title = "";
+ DOMString artist = "";
+ DOMString album = "";
+ sequence<MediaImage> artwork = [];
+};
+
+dictionary MediaImage {
+ required USVString src;
+ DOMString sizes = "";
+ DOMString type = "";
+};
+
+dictionary MediaPositionState {
+ double duration;
+ double playbackRate;
+ double position;
+};
+
+dictionary MediaSessionActionDetails {
+ required MediaSessionAction action;
+ double seekOffset;
+ double seekTime;
+ boolean fastSeek;
+};
diff --git a/test/wpt/tests/interfaces/mediastream-recording.idl b/test/wpt/tests/interfaces/mediastream-recording.idl
new file mode 100644
index 0000000..496bfcf
--- /dev/null
+++ b/test/wpt/tests/interfaces/mediastream-recording.idl
@@ -0,0 +1,62 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: MediaStream Recording (https://w3c.github.io/mediacapture-record/)
+
+[Exposed=Window]
+interface MediaRecorder : EventTarget {
+ constructor(MediaStream stream, optional MediaRecorderOptions options = {});
+ readonly attribute MediaStream stream;
+ readonly attribute DOMString mimeType;
+ readonly attribute RecordingState state;
+ attribute EventHandler onstart;
+ attribute EventHandler onstop;
+ attribute EventHandler ondataavailable;
+ attribute EventHandler onpause;
+ attribute EventHandler onresume;
+ attribute EventHandler onerror;
+ readonly attribute unsigned long videoBitsPerSecond;
+ readonly attribute unsigned long audioBitsPerSecond;
+ readonly attribute BitrateMode audioBitrateMode;
+
+ undefined start(optional unsigned long timeslice);
+ undefined stop();
+ undefined pause();
+ undefined resume();
+ undefined requestData();
+
+ static boolean isTypeSupported(DOMString type);
+};
+
+dictionary MediaRecorderOptions {
+ DOMString mimeType = "";
+ unsigned long audioBitsPerSecond;
+ unsigned long videoBitsPerSecond;
+ unsigned long bitsPerSecond;
+ BitrateMode audioBitrateMode = "variable";
+ DOMHighResTimeStamp videoKeyFrameIntervalDuration;
+ unsigned long videoKeyFrameIntervalCount;
+};
+
+enum BitrateMode {
+ "constant",
+ "variable"
+};
+
+enum RecordingState {
+ "inactive",
+ "recording",
+ "paused"
+};
+
+[Exposed=Window]
+interface BlobEvent : Event {
+ constructor(DOMString type, BlobEventInit eventInitDict);
+ [SameObject] readonly attribute Blob data;
+ readonly attribute DOMHighResTimeStamp timecode;
+};
+
+dictionary BlobEventInit {
+ required Blob data;
+ DOMHighResTimeStamp timecode;
+};
diff --git a/test/wpt/tests/interfaces/model-element.idl b/test/wpt/tests/interfaces/model-element.idl
new file mode 100644
index 0000000..aa031a1
--- /dev/null
+++ b/test/wpt/tests/interfaces/model-element.idl
@@ -0,0 +1,9 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: The <model> element (https://immersive-web.github.io/model-element/)
+
+[Exposed=Window]
+interface HTMLModelElement : HTMLElement {
+
+};
diff --git a/test/wpt/tests/interfaces/mst-content-hint.idl b/test/wpt/tests/interfaces/mst-content-hint.idl
new file mode 100644
index 0000000..a41abb5
--- /dev/null
+++ b/test/wpt/tests/interfaces/mst-content-hint.idl
@@ -0,0 +1,18 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: MediaStreamTrack Content Hints (https://w3c.github.io/mst-content-hint/)
+
+partial interface MediaStreamTrack {
+ attribute DOMString contentHint;
+};
+
+enum RTCDegradationPreference {
+ "maintain-framerate",
+ "maintain-resolution",
+ "balanced"
+};
+
+partial dictionary RTCRtpSendParameters {
+ RTCDegradationPreference degradationPreference;
+};
diff --git a/test/wpt/tests/interfaces/navigation-timing.idl b/test/wpt/tests/interfaces/navigation-timing.idl
new file mode 100644
index 0000000..5a33964
--- /dev/null
+++ b/test/wpt/tests/interfaces/navigation-timing.idl
@@ -0,0 +1,71 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Navigation Timing Level 2 (https://w3c.github.io/navigation-timing/)
+
+[Exposed=Window]
+interface PerformanceNavigationTiming : PerformanceResourceTiming {
+ readonly attribute DOMHighResTimeStamp unloadEventStart;
+ readonly attribute DOMHighResTimeStamp unloadEventEnd;
+ readonly attribute DOMHighResTimeStamp domInteractive;
+ readonly attribute DOMHighResTimeStamp domContentLoadedEventStart;
+ readonly attribute DOMHighResTimeStamp domContentLoadedEventEnd;
+ readonly attribute DOMHighResTimeStamp domComplete;
+ readonly attribute DOMHighResTimeStamp loadEventStart;
+ readonly attribute DOMHighResTimeStamp loadEventEnd;
+ readonly attribute NavigationTimingType type;
+ readonly attribute unsigned short redirectCount;
+ [Default] object toJSON();
+};
+
+enum NavigationTimingType {
+ "navigate",
+ "reload",
+ "back_forward",
+ "prerender"
+};
+
+[Exposed=Window]
+interface PerformanceTiming {
+ readonly attribute unsigned long long navigationStart;
+ readonly attribute unsigned long long unloadEventStart;
+ readonly attribute unsigned long long unloadEventEnd;
+ readonly attribute unsigned long long redirectStart;
+ readonly attribute unsigned long long redirectEnd;
+ readonly attribute unsigned long long fetchStart;
+ readonly attribute unsigned long long domainLookupStart;
+ readonly attribute unsigned long long domainLookupEnd;
+ readonly attribute unsigned long long connectStart;
+ readonly attribute unsigned long long connectEnd;
+ readonly attribute unsigned long long secureConnectionStart;
+ readonly attribute unsigned long long requestStart;
+ readonly attribute unsigned long long responseStart;
+ readonly attribute unsigned long long responseEnd;
+ readonly attribute unsigned long long domLoading;
+ readonly attribute unsigned long long domInteractive;
+ readonly attribute unsigned long long domContentLoadedEventStart;
+ readonly attribute unsigned long long domContentLoadedEventEnd;
+ readonly attribute unsigned long long domComplete;
+ readonly attribute unsigned long long loadEventStart;
+ readonly attribute unsigned long long loadEventEnd;
+ [Default] object toJSON();
+};
+
+[Exposed=Window]
+interface PerformanceNavigation {
+ const unsigned short TYPE_NAVIGATE = 0;
+ const unsigned short TYPE_RELOAD = 1;
+ const unsigned short TYPE_BACK_FORWARD = 2;
+ const unsigned short TYPE_RESERVED = 255;
+ readonly attribute unsigned short type;
+ readonly attribute unsigned short redirectCount;
+ [Default] object toJSON();
+};
+
+[Exposed=Window]
+partial interface Performance {
+ [SameObject]
+ readonly attribute PerformanceTiming timing;
+ [SameObject]
+ readonly attribute PerformanceNavigation navigation;
+};
diff --git a/test/wpt/tests/interfaces/netinfo.idl b/test/wpt/tests/interfaces/netinfo.idl
new file mode 100644
index 0000000..ac5265d
--- /dev/null
+++ b/test/wpt/tests/interfaces/netinfo.idl
@@ -0,0 +1,43 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Network Information API (https://wicg.github.io/netinfo/)
+
+enum ConnectionType {
+ "bluetooth",
+ "cellular",
+ "ethernet",
+ "mixed",
+ "none",
+ "other",
+ "unknown",
+ "wifi",
+ "wimax"
+};
+
+enum EffectiveConnectionType {
+ "2g",
+ "3g",
+ "4g",
+ "slow-2g"
+};
+
+interface mixin NavigatorNetworkInformation {
+ [SameObject] readonly attribute NetworkInformation connection;
+};
+
+Navigator includes NavigatorNetworkInformation;
+WorkerNavigator includes NavigatorNetworkInformation;
+
+[Exposed=(Window,Worker)]
+interface NetworkInformation : EventTarget {
+ readonly attribute ConnectionType type;
+ readonly attribute EffectiveConnectionType effectiveType;
+ readonly attribute Megabit downlinkMax;
+ readonly attribute Megabit downlink;
+ readonly attribute Millisecond rtt;
+ attribute EventHandler onchange;
+};
+
+typedef unrestricted double Megabit;
+typedef unsigned long long Millisecond;
diff --git a/test/wpt/tests/interfaces/notifications.idl b/test/wpt/tests/interfaces/notifications.idl
new file mode 100644
index 0000000..4300b17
--- /dev/null
+++ b/test/wpt/tests/interfaces/notifications.idl
@@ -0,0 +1,101 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Notifications API Standard (https://notifications.spec.whatwg.org/)
+
+[Exposed=(Window,Worker)]
+interface Notification : EventTarget {
+ constructor(DOMString title, optional NotificationOptions options = {});
+
+ static readonly attribute NotificationPermission permission;
+ [Exposed=Window] static Promise<NotificationPermission> requestPermission(optional NotificationPermissionCallback deprecatedCallback);
+
+ static readonly attribute unsigned long maxActions;
+
+ attribute EventHandler onclick;
+ attribute EventHandler onshow;
+ attribute EventHandler onerror;
+ attribute EventHandler onclose;
+
+ readonly attribute DOMString title;
+ readonly attribute NotificationDirection dir;
+ readonly attribute DOMString lang;
+ readonly attribute DOMString body;
+ readonly attribute DOMString tag;
+ readonly attribute USVString image;
+ readonly attribute USVString icon;
+ readonly attribute USVString badge;
+ [SameObject] readonly attribute FrozenArray<unsigned long> vibrate;
+ readonly attribute EpochTimeStamp timestamp;
+ readonly attribute boolean renotify;
+ readonly attribute boolean? silent;
+ readonly attribute boolean requireInteraction;
+ [SameObject] readonly attribute any data;
+ [SameObject] readonly attribute FrozenArray<NotificationAction> actions;
+
+ undefined close();
+};
+
+dictionary NotificationOptions {
+ NotificationDirection dir = "auto";
+ DOMString lang = "";
+ DOMString body = "";
+ DOMString tag = "";
+ USVString image;
+ USVString icon;
+ USVString badge;
+ VibratePattern vibrate;
+ EpochTimeStamp timestamp;
+ boolean renotify = false;
+ boolean? silent = null;
+ boolean requireInteraction = false;
+ any data = null;
+ sequence<NotificationAction> actions = [];
+};
+
+enum NotificationPermission {
+ "default",
+ "denied",
+ "granted"
+};
+
+enum NotificationDirection {
+ "auto",
+ "ltr",
+ "rtl"
+};
+
+dictionary NotificationAction {
+ required DOMString action;
+ required DOMString title;
+ USVString icon;
+};
+
+callback NotificationPermissionCallback = undefined (NotificationPermission permission);
+
+dictionary GetNotificationOptions {
+ DOMString tag = "";
+};
+
+partial interface ServiceWorkerRegistration {
+ Promise<undefined> showNotification(DOMString title, optional NotificationOptions options = {});
+ Promise<sequence<Notification>> getNotifications(optional GetNotificationOptions filter = {});
+};
+
+[Exposed=ServiceWorker]
+interface NotificationEvent : ExtendableEvent {
+ constructor(DOMString type, NotificationEventInit eventInitDict);
+
+ readonly attribute Notification notification;
+ readonly attribute DOMString action;
+};
+
+dictionary NotificationEventInit : ExtendableEventInit {
+ required Notification notification;
+ DOMString action = "";
+};
+
+partial interface ServiceWorkerGlobalScope {
+ attribute EventHandler onnotificationclick;
+ attribute EventHandler onnotificationclose;
+};
diff --git a/test/wpt/tests/interfaces/orientation-event.idl b/test/wpt/tests/interfaces/orientation-event.idl
new file mode 100644
index 0000000..a93d465
--- /dev/null
+++ b/test/wpt/tests/interfaces/orientation-event.idl
@@ -0,0 +1,78 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: DeviceOrientation Event Specification (https://w3c.github.io/deviceorientation/)
+
+partial interface Window {
+ [SecureContext] attribute EventHandler ondeviceorientation;
+};
+
+[Exposed=Window, SecureContext]
+interface DeviceOrientationEvent : Event {
+ constructor(DOMString type, optional DeviceOrientationEventInit eventInitDict = {});
+ readonly attribute double? alpha;
+ readonly attribute double? beta;
+ readonly attribute double? gamma;
+ readonly attribute boolean absolute;
+
+ static Promise<PermissionState> requestPermission();
+};
+
+dictionary DeviceOrientationEventInit : EventInit {
+ double? alpha = null;
+ double? beta = null;
+ double? gamma = null;
+ boolean absolute = false;
+};
+
+partial interface Window {
+ [SecureContext] attribute EventHandler ondeviceorientationabsolute;
+};
+
+partial interface Window {
+ [SecureContext] attribute EventHandler ondevicemotion;
+};
+
+[Exposed=Window, SecureContext]
+interface DeviceMotionEventAcceleration {
+ readonly attribute double? x;
+ readonly attribute double? y;
+ readonly attribute double? z;
+};
+
+[Exposed=Window, SecureContext]
+interface DeviceMotionEventRotationRate {
+ readonly attribute double? alpha;
+ readonly attribute double? beta;
+ readonly attribute double? gamma;
+};
+
+[Exposed=Window, SecureContext]
+interface DeviceMotionEvent : Event {
+ constructor(DOMString type, optional DeviceMotionEventInit eventInitDict = {});
+ readonly attribute DeviceMotionEventAcceleration? acceleration;
+ readonly attribute DeviceMotionEventAcceleration? accelerationIncludingGravity;
+ readonly attribute DeviceMotionEventRotationRate? rotationRate;
+ readonly attribute double interval;
+
+ static Promise<PermissionState> requestPermission();
+};
+
+dictionary DeviceMotionEventAccelerationInit {
+ double? x = null;
+ double? y = null;
+ double? z = null;
+};
+
+dictionary DeviceMotionEventRotationRateInit {
+ double? alpha = null;
+ double? beta = null;
+ double? gamma = null;
+};
+
+dictionary DeviceMotionEventInit : EventInit {
+ DeviceMotionEventAccelerationInit acceleration;
+ DeviceMotionEventAccelerationInit accelerationIncludingGravity;
+ DeviceMotionEventRotationRateInit rotationRate;
+ double interval = 0;
+};
diff --git a/test/wpt/tests/interfaces/orientation-sensor.idl b/test/wpt/tests/interfaces/orientation-sensor.idl
new file mode 100644
index 0000000..5172c87
--- /dev/null
+++ b/test/wpt/tests/interfaces/orientation-sensor.idl
@@ -0,0 +1,35 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Orientation Sensor (https://w3c.github.io/orientation-sensor/)
+
+typedef (Float32Array or Float64Array or DOMMatrix) RotationMatrixType;
+
+[SecureContext, Exposed=Window]
+interface OrientationSensor : Sensor {
+ readonly attribute FrozenArray<double>? quaternion;
+ undefined populateMatrix(RotationMatrixType targetMatrix);
+};
+
+enum OrientationSensorLocalCoordinateSystem { "device", "screen" };
+
+dictionary OrientationSensorOptions : SensorOptions {
+ OrientationSensorLocalCoordinateSystem referenceFrame = "device";
+};
+
+[SecureContext, Exposed=Window]
+interface AbsoluteOrientationSensor : OrientationSensor {
+ constructor(optional OrientationSensorOptions sensorOptions = {});
+};
+
+[SecureContext, Exposed=Window]
+interface RelativeOrientationSensor : OrientationSensor {
+ constructor(optional OrientationSensorOptions sensorOptions = {});
+};
+
+dictionary AbsoluteOrientationReadingValues {
+ required FrozenArray<double>? quaternion;
+};
+
+dictionary RelativeOrientationReadingValues : AbsoluteOrientationReadingValues {
+};
diff --git a/test/wpt/tests/interfaces/page-lifecycle.idl b/test/wpt/tests/interfaces/page-lifecycle.idl
new file mode 100644
index 0000000..26de11c
--- /dev/null
+++ b/test/wpt/tests/interfaces/page-lifecycle.idl
@@ -0,0 +1,19 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Page Lifecycle (https://wicg.github.io/page-lifecycle/)
+
+partial interface Document {
+ attribute EventHandler onfreeze;
+ attribute EventHandler onresume;
+ readonly attribute boolean wasDiscarded;
+};
+
+partial interface Client {
+ readonly attribute ClientLifecycleState lifecycleState;
+};
+
+enum ClientLifecycleState {
+ "active",
+ "frozen"
+};
diff --git a/test/wpt/tests/interfaces/paint-timing.idl b/test/wpt/tests/interfaces/paint-timing.idl
new file mode 100644
index 0000000..052b74e
--- /dev/null
+++ b/test/wpt/tests/interfaces/paint-timing.idl
@@ -0,0 +1,7 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Paint Timing 1 (https://w3c.github.io/paint-timing/)
+
+[Exposed=Window]
+interface PerformancePaintTiming : PerformanceEntry {};
diff --git a/test/wpt/tests/interfaces/parakeet.tentative.idl b/test/wpt/tests/interfaces/parakeet.tentative.idl
new file mode 100644
index 0000000..a65cb19
--- /dev/null
+++ b/test/wpt/tests/interfaces/parakeet.tentative.idl
@@ -0,0 +1,32 @@
+enum AdSignals {
+ "coarse-geolocation",
+ "coarse-ua",
+ "targeting",
+ "user-ad-interests"
+};
+dictionary AdProperties{
+ DOMString width;
+ DOMString height;
+ DOMString slot;
+ DOMString lang;
+ DOMString adtype;
+ double bidFloor;
+};
+dictionary AdTargeting{
+ sequence<DOMString> interests;
+ GeolocationCoordinates geolocation;
+};
+
+dictionary AdRequestConfig{
+ required USVString adRequestUrl;
+ required(AdProperties or sequence<AdProperties>) adProperties;
+ DOMString publisherCode;
+ AdTargeting targeting;
+ sequence<AdSignals> anonymizedProxiedSignals;
+ USVString fallbackSource;
+};
+
+partial interface Navigator {
+ Promise<Ads> createAdRequest(AdRequestConfig config);
+ Promise<URL> finalizeAd(Ads ads, AuctionAdConfig config);
+};
diff --git a/test/wpt/tests/interfaces/payment-handler.idl b/test/wpt/tests/interfaces/payment-handler.idl
new file mode 100644
index 0000000..91c0129
--- /dev/null
+++ b/test/wpt/tests/interfaces/payment-handler.idl
@@ -0,0 +1,131 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Payment Handler API (https://w3c.github.io/payment-handler/)
+
+partial interface ServiceWorkerRegistration {
+ [SameObject] readonly attribute PaymentManager paymentManager;
+};
+
+[SecureContext, Exposed=(Window)]
+interface PaymentManager {
+ attribute DOMString userHint;
+ Promise<undefined> enableDelegations(sequence<PaymentDelegation> delegations);
+};
+
+enum PaymentDelegation {
+ "shippingAddress",
+ "payerName",
+ "payerPhone",
+ "payerEmail"
+};
+
+partial interface ServiceWorkerGlobalScope {
+ attribute EventHandler oncanmakepayment;
+};
+
+[Exposed=ServiceWorker]
+interface CanMakePaymentEvent : ExtendableEvent {
+ constructor(DOMString type);
+ undefined respondWith(Promise<boolean> canMakePaymentResponse);
+};
+
+partial interface ServiceWorkerGlobalScope {
+ attribute EventHandler onpaymentrequest;
+};
+
+dictionary PaymentRequestDetailsUpdate {
+ DOMString error;
+ PaymentCurrencyAmount total;
+ sequence<PaymentDetailsModifier> modifiers;
+ sequence<PaymentShippingOption> shippingOptions;
+ object paymentMethodErrors;
+ AddressErrors shippingAddressErrors;
+};
+
+[Exposed=ServiceWorker]
+interface PaymentRequestEvent : ExtendableEvent {
+ constructor(DOMString type, optional PaymentRequestEventInit eventInitDict = {});
+ readonly attribute USVString topOrigin;
+ readonly attribute USVString paymentRequestOrigin;
+ readonly attribute DOMString paymentRequestId;
+ readonly attribute FrozenArray<PaymentMethodData> methodData;
+ readonly attribute object total;
+ readonly attribute FrozenArray<PaymentDetailsModifier> modifiers;
+ readonly attribute object? paymentOptions;
+ readonly attribute FrozenArray<PaymentShippingOption>? shippingOptions;
+ Promise<WindowClient?> openWindow(USVString url);
+ Promise<PaymentRequestDetailsUpdate?> changePaymentMethod(DOMString methodName, optional object? methodDetails = null);
+ Promise<PaymentRequestDetailsUpdate?> changeShippingAddress(optional AddressInit shippingAddress = {});
+ Promise<PaymentRequestDetailsUpdate?> changeShippingOption(DOMString shippingOption);
+ undefined respondWith(Promise<PaymentHandlerResponse> handlerResponsePromise);
+};
+
+dictionary PaymentRequestEventInit : ExtendableEventInit {
+ USVString topOrigin;
+ USVString paymentRequestOrigin;
+ DOMString paymentRequestId;
+ sequence<PaymentMethodData> methodData;
+ PaymentCurrencyAmount total;
+ sequence<PaymentDetailsModifier> modifiers;
+ PaymentOptions paymentOptions;
+ sequence<PaymentShippingOption> shippingOptions;
+};
+
+dictionary PaymentHandlerResponse {
+DOMString methodName;
+object details;
+DOMString? payerName;
+DOMString? payerEmail;
+DOMString? payerPhone;
+AddressInit shippingAddress;
+DOMString? shippingOption;
+};
+
+dictionary AddressInit {
+ DOMString country = "";
+ sequence<DOMString> addressLine = [];
+ DOMString region = "";
+ DOMString city = "";
+ DOMString dependentLocality = "";
+ DOMString postalCode = "";
+ DOMString sortingCode = "";
+ DOMString organization = "";
+ DOMString recipient = "";
+ DOMString phone = "";
+};
+
+dictionary PaymentOptions {
+ boolean requestPayerName = false;
+ boolean requestBillingAddress = false;
+ boolean requestPayerEmail = false;
+ boolean requestPayerPhone = false;
+ boolean requestShipping = false;
+ PaymentShippingType shippingType = "shipping";
+};
+
+dictionary PaymentShippingOption {
+ required DOMString id;
+ required DOMString label;
+ required PaymentCurrencyAmount amount;
+ boolean selected = false;
+};
+
+enum PaymentShippingType {
+ "shipping",
+ "delivery",
+ "pickup"
+};
+
+dictionary AddressErrors {
+ DOMString addressLine;
+ DOMString city;
+ DOMString country;
+ DOMString dependentLocality;
+ DOMString organization;
+ DOMString phone;
+ DOMString postalCode;
+ DOMString recipient;
+ DOMString region;
+ DOMString sortingCode;
+};
diff --git a/test/wpt/tests/interfaces/payment-request.idl b/test/wpt/tests/interfaces/payment-request.idl
new file mode 100644
index 0000000..0a97d4d
--- /dev/null
+++ b/test/wpt/tests/interfaces/payment-request.idl
@@ -0,0 +1,112 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Payment Request API 1.1 (https://w3c.github.io/payment-request/)
+
+[SecureContext, Exposed=Window]
+interface PaymentRequest : EventTarget {
+ constructor(
+ sequence<PaymentMethodData> methodData,
+ PaymentDetailsInit details
+ );
+ [NewObject]
+ Promise<PaymentResponse> show(optional Promise<PaymentDetailsUpdate> detailsPromise);
+ [NewObject]
+ Promise<undefined> abort();
+ [NewObject]
+ Promise<boolean> canMakePayment();
+
+ readonly attribute DOMString id;
+
+ attribute EventHandler onpaymentmethodchange;
+};
+
+dictionary PaymentMethodData {
+ required DOMString supportedMethods;
+ object data;
+};
+
+dictionary PaymentCurrencyAmount {
+ required DOMString currency;
+ required DOMString value;
+};
+
+dictionary PaymentDetailsBase {
+ sequence<PaymentItem> displayItems;
+ sequence<PaymentDetailsModifier> modifiers;
+};
+
+dictionary PaymentDetailsInit : PaymentDetailsBase {
+ DOMString id;
+ required PaymentItem total;
+};
+
+dictionary PaymentDetailsUpdate : PaymentDetailsBase {
+ PaymentItem total;
+ object paymentMethodErrors;
+};
+
+dictionary PaymentDetailsModifier {
+ required DOMString supportedMethods;
+ PaymentItem total;
+ sequence<PaymentItem> additionalDisplayItems;
+ object data;
+};
+
+dictionary PaymentItem {
+ required DOMString label;
+ required PaymentCurrencyAmount amount;
+ boolean pending = false;
+};
+
+dictionary PaymentCompleteDetails {
+ object? data = null;
+};
+
+enum PaymentComplete {
+ "fail",
+ "success",
+ "unknown"
+};
+
+[SecureContext, Exposed=Window]
+interface PaymentResponse : EventTarget {
+ [Default] object toJSON();
+
+ readonly attribute DOMString requestId;
+ readonly attribute DOMString methodName;
+ readonly attribute object details;
+
+ [NewObject]
+ Promise<undefined> complete(
+ optional PaymentComplete result = "unknown",
+ optional PaymentCompleteDetails details = {}
+ );
+ [NewObject]
+ Promise<undefined> retry(optional PaymentValidationErrors errorFields = {});
+};
+
+dictionary PaymentValidationErrors {
+ DOMString error;
+ object paymentMethod;
+};
+
+[SecureContext, Exposed=Window]
+interface PaymentMethodChangeEvent : PaymentRequestUpdateEvent {
+ constructor(DOMString type, optional PaymentMethodChangeEventInit eventInitDict = {});
+ readonly attribute DOMString methodName;
+ readonly attribute object? methodDetails;
+};
+
+dictionary PaymentMethodChangeEventInit : PaymentRequestUpdateEventInit {
+ DOMString methodName = "";
+ object? methodDetails = null;
+};
+
+[SecureContext, Exposed=Window]
+interface PaymentRequestUpdateEvent : Event {
+ constructor(DOMString type, optional PaymentRequestUpdateEventInit eventInitDict = {});
+ undefined updateWith(Promise<PaymentDetailsUpdate> detailsPromise);
+};
+
+dictionary PaymentRequestUpdateEventInit : EventInit {};
diff --git a/test/wpt/tests/interfaces/performance-measure-memory.idl b/test/wpt/tests/interfaces/performance-measure-memory.idl
new file mode 100644
index 0000000..b60f2b6
--- /dev/null
+++ b/test/wpt/tests/interfaces/performance-measure-memory.idl
@@ -0,0 +1,30 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Measure Memory API (https://wicg.github.io/performance-measure-memory/)
+
+dictionary MemoryMeasurement {
+ unsigned long long bytes;
+ sequence<MemoryBreakdownEntry> breakdown;
+};
+
+dictionary MemoryBreakdownEntry {
+ unsigned long long bytes;
+ sequence<MemoryAttribution> attribution;
+ sequence<DOMString> types;
+};
+
+dictionary MemoryAttribution {
+ USVString url;
+ MemoryAttributionContainer container;
+ DOMString scope;
+};
+
+dictionary MemoryAttributionContainer {
+ DOMString id;
+ USVString src;
+};
+
+partial interface Performance {
+ [Exposed=(Window,ServiceWorker,SharedWorker), CrossOriginIsolated] Promise<MemoryMeasurement> measureUserAgentSpecificMemory();
+};
diff --git a/test/wpt/tests/interfaces/performance-timeline.idl b/test/wpt/tests/interfaces/performance-timeline.idl
new file mode 100644
index 0000000..cdd8faf
--- /dev/null
+++ b/test/wpt/tests/interfaces/performance-timeline.idl
@@ -0,0 +1,49 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Performance Timeline (https://w3c.github.io/performance-timeline/)
+
+partial interface Performance {
+ PerformanceEntryList getEntries ();
+ PerformanceEntryList getEntriesByType (DOMString type);
+ PerformanceEntryList getEntriesByName (DOMString name, optional DOMString type);
+};
+typedef sequence<PerformanceEntry> PerformanceEntryList;
+
+[Exposed=(Window,Worker)]
+interface PerformanceEntry {
+ readonly attribute DOMString name;
+ readonly attribute DOMString entryType;
+ readonly attribute DOMHighResTimeStamp startTime;
+ readonly attribute DOMHighResTimeStamp duration;
+ [Default] object toJSON();
+};
+
+callback PerformanceObserverCallback = undefined (PerformanceObserverEntryList entries,
+ PerformanceObserver observer,
+ optional PerformanceObserverCallbackOptions options = {});
+[Exposed=(Window,Worker)]
+interface PerformanceObserver {
+ constructor(PerformanceObserverCallback callback);
+ undefined observe (optional PerformanceObserverInit options = {});
+ undefined disconnect ();
+ PerformanceEntryList takeRecords();
+ [SameObject] static readonly attribute FrozenArray<DOMString> supportedEntryTypes;
+};
+
+dictionary PerformanceObserverCallbackOptions {
+ unsigned long long droppedEntriesCount;
+};
+
+dictionary PerformanceObserverInit {
+ sequence<DOMString> entryTypes;
+ DOMString type;
+ boolean buffered;
+};
+
+[Exposed=(Window,Worker)]
+interface PerformanceObserverEntryList {
+ PerformanceEntryList getEntries();
+ PerformanceEntryList getEntriesByType (DOMString type);
+ PerformanceEntryList getEntriesByName (DOMString name, optional DOMString type);
+};
diff --git a/test/wpt/tests/interfaces/periodic-background-sync.idl b/test/wpt/tests/interfaces/periodic-background-sync.idl
new file mode 100644
index 0000000..d61ebe9
--- /dev/null
+++ b/test/wpt/tests/interfaces/periodic-background-sync.idl
@@ -0,0 +1,34 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Web Periodic Background Synchronization (https://wicg.github.io/periodic-background-sync/)
+
+partial interface ServiceWorkerGlobalScope {
+ attribute EventHandler onperiodicsync;
+};
+
+[Exposed=(Window,Worker)]
+partial interface ServiceWorkerRegistration {
+ readonly attribute PeriodicSyncManager periodicSync;
+};
+
+[Exposed=(Window,Worker)]
+interface PeriodicSyncManager {
+ Promise<undefined> register(DOMString tag, optional BackgroundSyncOptions options = {});
+ Promise<sequence<DOMString>> getTags();
+ Promise<undefined> unregister(DOMString tag);
+};
+
+dictionary BackgroundSyncOptions {
+ [EnforceRange] unsigned long long minInterval = 0;
+};
+
+dictionary PeriodicSyncEventInit : ExtendableEventInit {
+ required DOMString tag;
+};
+
+[Exposed=ServiceWorker]
+interface PeriodicSyncEvent : ExtendableEvent {
+ constructor(DOMString type, PeriodicSyncEventInit init);
+ readonly attribute DOMString tag;
+};
diff --git a/test/wpt/tests/interfaces/permissions-policy.idl b/test/wpt/tests/interfaces/permissions-policy.idl
new file mode 100644
index 0000000..16945e3
--- /dev/null
+++ b/test/wpt/tests/interfaces/permissions-policy.idl
@@ -0,0 +1,29 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Permissions Policy (https://w3c.github.io/webappsec-permissions-policy/)
+
+[Exposed=Window]
+interface PermissionsPolicy {
+ boolean allowsFeature(DOMString feature, optional DOMString origin);
+ sequence<DOMString> features();
+ sequence<DOMString> allowedFeatures();
+ sequence<DOMString> getAllowlistForFeature(DOMString feature);
+};
+
+partial interface Document {
+ [SameObject] readonly attribute PermissionsPolicy permissionsPolicy;
+};
+
+partial interface HTMLIFrameElement {
+ [SameObject] readonly attribute PermissionsPolicy permissionsPolicy;
+};
+
+[Exposed=Window]
+interface PermissionsPolicyViolationReportBody : ReportBody {
+ readonly attribute DOMString featureId;
+ readonly attribute DOMString? sourceFile;
+ readonly attribute long? lineNumber;
+ readonly attribute long? columnNumber;
+ readonly attribute DOMString disposition;
+};
diff --git a/test/wpt/tests/interfaces/permissions-request.idl b/test/wpt/tests/interfaces/permissions-request.idl
new file mode 100644
index 0000000..e189194
--- /dev/null
+++ b/test/wpt/tests/interfaces/permissions-request.idl
@@ -0,0 +1,8 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Requesting Permissions (https://wicg.github.io/permissions-request/)
+
+partial interface Permissions {
+ Promise<PermissionStatus> request(object permissionDesc);
+};
diff --git a/test/wpt/tests/interfaces/permissions-revoke.idl b/test/wpt/tests/interfaces/permissions-revoke.idl
new file mode 100644
index 0000000..5e8f386
--- /dev/null
+++ b/test/wpt/tests/interfaces/permissions-revoke.idl
@@ -0,0 +1,8 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Relinquishing Permissions (https://wicg.github.io/permissions-revoke/)
+
+partial interface Permissions {
+ Promise<PermissionStatus> revoke(object permissionDesc);
+};
diff --git a/test/wpt/tests/interfaces/permissions.idl b/test/wpt/tests/interfaces/permissions.idl
new file mode 100644
index 0000000..fbcb674
--- /dev/null
+++ b/test/wpt/tests/interfaces/permissions.idl
@@ -0,0 +1,41 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Permissions (https://w3c.github.io/permissions/)
+
+[Exposed=(Window)]
+partial interface Navigator {
+ [SameObject] readonly attribute Permissions permissions;
+};
+
+[Exposed=(Worker)]
+partial interface WorkerNavigator {
+ [SameObject] readonly attribute Permissions permissions;
+};
+
+[Exposed=(Window,Worker)]
+interface Permissions {
+ Promise<PermissionStatus> query(object permissionDesc);
+};
+
+dictionary PermissionDescriptor {
+ required DOMString name;
+};
+
+[Exposed=(Window,Worker)]
+interface PermissionStatus : EventTarget {
+ readonly attribute PermissionState state;
+ readonly attribute DOMString name;
+ attribute EventHandler onchange;
+};
+
+enum PermissionState {
+ "granted",
+ "denied",
+ "prompt",
+};
+
+dictionary PermissionSetParameters {
+ required PermissionDescriptor descriptor;
+ required PermissionState state;
+};
diff --git a/test/wpt/tests/interfaces/picture-in-picture.idl b/test/wpt/tests/interfaces/picture-in-picture.idl
new file mode 100644
index 0000000..516fb59
--- /dev/null
+++ b/test/wpt/tests/interfaces/picture-in-picture.idl
@@ -0,0 +1,41 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Picture-in-Picture (https://w3c.github.io/picture-in-picture/)
+
+partial interface HTMLVideoElement {
+ [NewObject] Promise<PictureInPictureWindow> requestPictureInPicture();
+
+ attribute EventHandler onenterpictureinpicture;
+ attribute EventHandler onleavepictureinpicture;
+
+ [CEReactions] attribute boolean disablePictureInPicture;
+};
+
+partial interface Document {
+ readonly attribute boolean pictureInPictureEnabled;
+
+ [NewObject] Promise<undefined> exitPictureInPicture();
+};
+
+partial interface mixin DocumentOrShadowRoot {
+ readonly attribute Element? pictureInPictureElement;
+};
+
+[Exposed=Window]
+interface PictureInPictureWindow : EventTarget {
+ readonly attribute long width;
+ readonly attribute long height;
+
+ attribute EventHandler onresize;
+};
+
+[Exposed=Window]
+interface PictureInPictureEvent : Event {
+ constructor(DOMString type, PictureInPictureEventInit eventInitDict);
+ [SameObject] readonly attribute PictureInPictureWindow pictureInPictureWindow;
+};
+
+dictionary PictureInPictureEventInit : EventInit {
+ required PictureInPictureWindow pictureInPictureWindow;
+};
diff --git a/test/wpt/tests/interfaces/pointerevents.idl b/test/wpt/tests/interfaces/pointerevents.idl
new file mode 100644
index 0000000..4ecb290
--- /dev/null
+++ b/test/wpt/tests/interfaces/pointerevents.idl
@@ -0,0 +1,64 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Pointer Events (https://w3c.github.io/pointerevents/)
+
+dictionary PointerEventInit : MouseEventInit {
+ long pointerId = 0;
+ double width = 1;
+ double height = 1;
+ float pressure = 0;
+ float tangentialPressure = 0;
+ long tiltX;
+ long tiltY;
+ long twist = 0;
+ double altitudeAngle;
+ double azimuthAngle;
+ DOMString pointerType = "";
+ boolean isPrimary = false;
+ sequence<PointerEvent> coalescedEvents = [];
+ sequence<PointerEvent> predictedEvents = [];
+};
+
+[Exposed=Window]
+interface PointerEvent : MouseEvent {
+ constructor(DOMString type, optional PointerEventInit eventInitDict = {});
+ readonly attribute long pointerId;
+ readonly attribute double width;
+ readonly attribute double height;
+ readonly attribute float pressure;
+ readonly attribute float tangentialPressure;
+ readonly attribute long tiltX;
+ readonly attribute long tiltY;
+ readonly attribute long twist;
+ readonly attribute double altitudeAngle;
+ readonly attribute double azimuthAngle;
+ readonly attribute DOMString pointerType;
+ readonly attribute boolean isPrimary;
+ [SecureContext] sequence<PointerEvent> getCoalescedEvents();
+ sequence<PointerEvent> getPredictedEvents();
+};
+
+partial interface Element {
+ undefined setPointerCapture (long pointerId);
+ undefined releasePointerCapture (long pointerId);
+ boolean hasPointerCapture (long pointerId);
+};
+
+partial interface mixin GlobalEventHandlers {
+ attribute EventHandler onpointerover;
+ attribute EventHandler onpointerenter;
+ attribute EventHandler onpointerdown;
+ attribute EventHandler onpointermove;
+ [SecureContext] attribute EventHandler onpointerrawupdate;
+ attribute EventHandler onpointerup;
+ attribute EventHandler onpointercancel;
+ attribute EventHandler onpointerout;
+ attribute EventHandler onpointerleave;
+ attribute EventHandler ongotpointercapture;
+ attribute EventHandler onlostpointercapture;
+};
+
+partial interface Navigator {
+ readonly attribute long maxTouchPoints;
+};
diff --git a/test/wpt/tests/interfaces/pointerlock.idl b/test/wpt/tests/interfaces/pointerlock.idl
new file mode 100644
index 0000000..0204bf5
--- /dev/null
+++ b/test/wpt/tests/interfaces/pointerlock.idl
@@ -0,0 +1,28 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Pointer Lock 2.0 (https://w3c.github.io/pointerlock/)
+
+partial interface Element {
+ undefined requestPointerLock();
+};
+
+partial interface Document {
+ attribute EventHandler onpointerlockchange;
+ attribute EventHandler onpointerlockerror;
+ undefined exitPointerLock();
+};
+
+partial interface mixin DocumentOrShadowRoot {
+ readonly attribute Element ? pointerLockElement;
+};
+
+partial interface MouseEvent {
+ readonly attribute double movementX;
+ readonly attribute double movementY;
+};
+
+partial dictionary MouseEventInit {
+ double movementX = 0;
+ double movementY = 0;
+};
diff --git a/test/wpt/tests/interfaces/portals.idl b/test/wpt/tests/interfaces/portals.idl
new file mode 100644
index 0000000..5d85cce
--- /dev/null
+++ b/test/wpt/tests/interfaces/portals.idl
@@ -0,0 +1,50 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Portals (https://wicg.github.io/portals/)
+
+[Exposed=Window]
+interface HTMLPortalElement : HTMLElement {
+ [HTMLConstructor] constructor();
+
+ [CEReactions] attribute USVString src;
+ [CEReactions] attribute DOMString referrerPolicy;
+
+ [NewObject] Promise<undefined> activate(optional PortalActivateOptions options = {});
+ undefined postMessage(any message, optional StructuredSerializeOptions options = {});
+
+ attribute EventHandler onmessage;
+ attribute EventHandler onmessageerror;
+};
+
+dictionary PortalActivateOptions : StructuredSerializeOptions {
+ any data;
+};
+
+partial interface Window {
+ readonly attribute PortalHost? portalHost;
+};
+
+[Exposed=Window]
+interface PortalHost : EventTarget {
+ undefined postMessage(any message, optional StructuredSerializeOptions options = {});
+
+ attribute EventHandler onmessage;
+ attribute EventHandler onmessageerror;
+};
+
+[Exposed=Window]
+interface PortalActivateEvent : Event {
+ constructor(DOMString type, optional PortalActivateEventInit eventInitDict = {});
+
+ readonly attribute any data;
+ HTMLPortalElement adoptPredecessor();
+};
+
+dictionary PortalActivateEventInit : EventInit {
+ any data = null;
+};
+
+partial interface mixin WindowEventHandlers {
+ attribute EventHandler onportalactivate;
+};
diff --git a/test/wpt/tests/interfaces/prefer-current-tab.idl b/test/wpt/tests/interfaces/prefer-current-tab.idl
new file mode 100644
index 0000000..86445e5
--- /dev/null
+++ b/test/wpt/tests/interfaces/prefer-current-tab.idl
@@ -0,0 +1,8 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: preferCurrentTab (https://wicg.github.io/prefer-current-tab/)
+
+partial dictionary MediaStreamConstraints {
+ boolean preferCurrentTab = false;
+};
diff --git a/test/wpt/tests/interfaces/prerendering-revamped.idl b/test/wpt/tests/interfaces/prerendering-revamped.idl
new file mode 100644
index 0000000..8f01432
--- /dev/null
+++ b/test/wpt/tests/interfaces/prerendering-revamped.idl
@@ -0,0 +1,15 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Prerendering Revamped (https://wicg.github.io/nav-speculation/prerendering.html)
+
+partial interface Document {
+ readonly attribute boolean prerendering;
+
+ // Under "special event handler IDL attributes that only apply to Document objects"
+ attribute EventHandler onprerenderingchange;
+};
+
+partial interface PerformanceNavigationTiming {
+ readonly attribute DOMHighResTimeStamp activationStart;
+};
diff --git a/test/wpt/tests/interfaces/presentation-api.idl b/test/wpt/tests/interfaces/presentation-api.idl
new file mode 100644
index 0000000..4f1e4be
--- /dev/null
+++ b/test/wpt/tests/interfaces/presentation-api.idl
@@ -0,0 +1,95 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Presentation API (https://w3c.github.io/presentation-api/)
+
+partial interface Navigator {
+ [SecureContext, SameObject] readonly attribute Presentation presentation;
+};
+
+[SecureContext, Exposed=Window]
+interface Presentation {
+};
+
+partial interface Presentation {
+ attribute PresentationRequest? defaultRequest;
+};
+
+partial interface Presentation {
+ readonly attribute PresentationReceiver? receiver;
+};
+
+[SecureContext, Exposed=Window]
+interface PresentationRequest : EventTarget {
+ constructor(USVString url);
+ constructor(sequence<USVString> urls);
+ Promise<PresentationConnection> start();
+ Promise<PresentationConnection> reconnect(USVString presentationId);
+ Promise<PresentationAvailability> getAvailability();
+
+ attribute EventHandler onconnectionavailable;
+};
+
+[SecureContext, Exposed=Window]
+interface PresentationAvailability : EventTarget {
+ readonly attribute boolean value;
+
+ attribute EventHandler onchange;
+};
+
+[SecureContext, Exposed=Window]
+interface PresentationConnectionAvailableEvent : Event {
+ constructor(DOMString type, PresentationConnectionAvailableEventInit eventInitDict);
+ [SameObject] readonly attribute PresentationConnection connection;
+};
+
+dictionary PresentationConnectionAvailableEventInit : EventInit {
+ required PresentationConnection connection;
+};
+
+enum PresentationConnectionState { "connecting", "connected", "closed", "terminated" };
+
+[SecureContext, Exposed=Window]
+interface PresentationConnection : EventTarget {
+ readonly attribute USVString id;
+ readonly attribute USVString url;
+ readonly attribute PresentationConnectionState state;
+ undefined close();
+ undefined terminate();
+ attribute EventHandler onconnect;
+ attribute EventHandler onclose;
+ attribute EventHandler onterminate;
+
+ // Communication
+ attribute BinaryType binaryType;
+ attribute EventHandler onmessage;
+ undefined send (DOMString message);
+ undefined send (Blob data);
+ undefined send (ArrayBuffer data);
+ undefined send (ArrayBufferView data);
+};
+
+enum PresentationConnectionCloseReason { "error", "closed", "wentaway" };
+
+[SecureContext, Exposed=Window]
+interface PresentationConnectionCloseEvent : Event {
+ constructor(DOMString type, PresentationConnectionCloseEventInit eventInitDict);
+ readonly attribute PresentationConnectionCloseReason reason;
+ readonly attribute DOMString message;
+};
+
+dictionary PresentationConnectionCloseEventInit : EventInit {
+ required PresentationConnectionCloseReason reason;
+ DOMString message = "";
+};
+
+[SecureContext, Exposed=Window]
+interface PresentationReceiver {
+ readonly attribute Promise<PresentationConnectionList> connectionList;
+};
+
+[SecureContext, Exposed=Window]
+interface PresentationConnectionList : EventTarget {
+ readonly attribute FrozenArray<PresentationConnection> connections;
+ attribute EventHandler onconnectionavailable;
+};
diff --git a/test/wpt/tests/interfaces/private-click-measurement.idl b/test/wpt/tests/interfaces/private-click-measurement.idl
new file mode 100644
index 0000000..3bed7cc
--- /dev/null
+++ b/test/wpt/tests/interfaces/private-click-measurement.idl
@@ -0,0 +1,8 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Private Click Measurement (https://privacycg.github.io/private-click-measurement/)
+
+partial interface HTMLAnchorElement {
+ [CEReactions] attribute unsigned long attributionSourceId;
+};
diff --git a/test/wpt/tests/interfaces/proximity.idl b/test/wpt/tests/interfaces/proximity.idl
new file mode 100644
index 0000000..3cbfbd5
--- /dev/null
+++ b/test/wpt/tests/interfaces/proximity.idl
@@ -0,0 +1,18 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Proximity Sensor (https://w3c.github.io/proximity/)
+
+[SecureContext, Exposed=Window]
+interface ProximitySensor : Sensor {
+ constructor(optional SensorOptions sensorOptions = {});
+ readonly attribute double? distance;
+ readonly attribute double? max;
+ readonly attribute boolean? near;
+};
+
+dictionary ProximityReadingValues {
+ required double? distance;
+ required double? max;
+ required boolean? near;
+};
diff --git a/test/wpt/tests/interfaces/push-api.idl b/test/wpt/tests/interfaces/push-api.idl
new file mode 100644
index 0000000..f582788
--- /dev/null
+++ b/test/wpt/tests/interfaces/push-api.idl
@@ -0,0 +1,93 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Push API (https://w3c.github.io/push-api/)
+
+dictionary PushPermissionDescriptor : PermissionDescriptor {
+ boolean userVisibleOnly = false;
+};
+
+[SecureContext]
+partial interface ServiceWorkerRegistration {
+ readonly attribute PushManager pushManager;
+};
+
+[Exposed=(Window,Worker), SecureContext]
+interface PushManager {
+ [SameObject] static readonly attribute FrozenArray<DOMString> supportedContentEncodings;
+
+ Promise<PushSubscription> subscribe(optional PushSubscriptionOptionsInit options = {});
+ Promise<PushSubscription?> getSubscription();
+ Promise<PermissionState> permissionState(optional PushSubscriptionOptionsInit options = {});
+};
+
+[Exposed=(Window,Worker), SecureContext]
+interface PushSubscriptionOptions {
+ readonly attribute boolean userVisibleOnly;
+ [SameObject] readonly attribute ArrayBuffer? applicationServerKey;
+};
+
+dictionary PushSubscriptionOptionsInit {
+ boolean userVisibleOnly = false;
+ (BufferSource or DOMString)? applicationServerKey = null;
+};
+
+[Exposed=(Window,Worker), SecureContext]
+interface PushSubscription {
+ readonly attribute USVString endpoint;
+ readonly attribute EpochTimeStamp? expirationTime;
+ [SameObject] readonly attribute PushSubscriptionOptions options;
+ ArrayBuffer? getKey(PushEncryptionKeyName name);
+ Promise<boolean> unsubscribe();
+
+ PushSubscriptionJSON toJSON();
+};
+
+dictionary PushSubscriptionJSON {
+ USVString endpoint;
+ EpochTimeStamp? expirationTime = null;
+ record<DOMString, USVString> keys;
+};
+
+enum PushEncryptionKeyName {
+ "p256dh",
+ "auth"
+};
+
+[Exposed=ServiceWorker, SecureContext]
+interface PushMessageData {
+ ArrayBuffer arrayBuffer();
+ Blob blob();
+ any json();
+ USVString text();
+};
+
+[Exposed=ServiceWorker, SecureContext]
+partial interface ServiceWorkerGlobalScope {
+ attribute EventHandler onpush;
+ attribute EventHandler onpushsubscriptionchange;
+};
+
+[Exposed=ServiceWorker, SecureContext]
+interface PushEvent : ExtendableEvent {
+ constructor(DOMString type, optional PushEventInit eventInitDict = {});
+ readonly attribute PushMessageData? data;
+};
+
+typedef (BufferSource or USVString) PushMessageDataInit;
+
+dictionary PushEventInit : ExtendableEventInit {
+ PushMessageDataInit data;
+};
+
+[Exposed=ServiceWorker, SecureContext]
+interface PushSubscriptionChangeEvent : ExtendableEvent {
+ constructor(DOMString type, optional PushSubscriptionChangeEventInit eventInitDict = {});
+ readonly attribute PushSubscription? newSubscription;
+ readonly attribute PushSubscription? oldSubscription;
+};
+
+dictionary PushSubscriptionChangeEventInit : ExtendableEventInit {
+ PushSubscription newSubscription = null;
+ PushSubscription oldSubscription = null;
+};
diff --git a/test/wpt/tests/interfaces/raw-camera-access.idl b/test/wpt/tests/interfaces/raw-camera-access.idl
new file mode 100644
index 0000000..d8ee0bb
--- /dev/null
+++ b/test/wpt/tests/interfaces/raw-camera-access.idl
@@ -0,0 +1,18 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: WebXR Raw Camera Access Module (https://immersive-web.github.io/raw-camera-access/)
+
+partial interface XRView {
+ [SameObject] readonly attribute XRCamera? camera;
+};
+
+[SecureContext, Exposed=Window]
+interface XRCamera {
+ readonly attribute unsigned long width;
+ readonly attribute unsigned long height;
+};
+
+partial interface XRWebGLBinding {
+ WebGLTexture? getCameraImage(XRCamera camera);
+};
diff --git a/test/wpt/tests/interfaces/real-world-meshing.idl b/test/wpt/tests/interfaces/real-world-meshing.idl
new file mode 100644
index 0000000..38fe71f
--- /dev/null
+++ b/test/wpt/tests/interfaces/real-world-meshing.idl
@@ -0,0 +1,21 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: WebXR Mesh Detection Module (https://immersive-web.github.io/real-world-meshing/)
+
+[Exposed=Window] interface XRMesh {
+ [SameObject] readonly attribute XRSpace meshSpace;
+
+ readonly attribute FrozenArray<Float32Array> vertices;
+ readonly attribute Uint32Array indices;
+ readonly attribute DOMHighResTimeStamp lastChangedTime;
+ readonly attribute DOMString? semanticLabel;
+};
+
+[Exposed=Window] interface XRMeshSet {
+ readonly setlike<XRMesh>;
+};
+
+partial interface XRFrame {
+ readonly attribute XRMeshSet detectedMeshs;
+};
diff --git a/test/wpt/tests/interfaces/referrer-policy.idl b/test/wpt/tests/interfaces/referrer-policy.idl
new file mode 100644
index 0000000..0ef9a1f
--- /dev/null
+++ b/test/wpt/tests/interfaces/referrer-policy.idl
@@ -0,0 +1,16 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Referrer Policy (https://w3c.github.io/webappsec-referrer-policy/)
+
+enum ReferrerPolicy {
+ "",
+ "no-referrer",
+ "no-referrer-when-downgrade",
+ "same-origin",
+ "origin",
+ "strict-origin",
+ "origin-when-cross-origin",
+ "strict-origin-when-cross-origin",
+ "unsafe-url"
+};
diff --git a/test/wpt/tests/interfaces/remote-playback.idl b/test/wpt/tests/interfaces/remote-playback.idl
new file mode 100644
index 0000000..2522410
--- /dev/null
+++ b/test/wpt/tests/interfaces/remote-playback.idl
@@ -0,0 +1,32 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Remote Playback API (https://w3c.github.io/remote-playback/)
+
+[Exposed=Window]
+interface RemotePlayback : EventTarget {
+ Promise<long> watchAvailability(RemotePlaybackAvailabilityCallback callback);
+ Promise<undefined> cancelWatchAvailability(optional long id);
+
+ readonly attribute RemotePlaybackState state;
+
+ attribute EventHandler onconnecting;
+ attribute EventHandler onconnect;
+ attribute EventHandler ondisconnect;
+
+ Promise<undefined> prompt();
+};
+
+enum RemotePlaybackState {
+ "connecting",
+ "connected",
+ "disconnected"
+};
+
+callback RemotePlaybackAvailabilityCallback = undefined(boolean available);
+
+partial interface HTMLMediaElement {
+ [SameObject] readonly attribute RemotePlayback remote;
+
+ [CEReactions] attribute boolean disableRemotePlayback;
+};
diff --git a/test/wpt/tests/interfaces/reporting.idl b/test/wpt/tests/interfaces/reporting.idl
new file mode 100644
index 0000000..c0a400a
--- /dev/null
+++ b/test/wpt/tests/interfaces/reporting.idl
@@ -0,0 +1,39 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Reporting API (https://w3c.github.io/reporting/)
+
+[Exposed=(Window,Worker)]
+interface ReportBody {
+ [Default] object toJSON();
+};
+
+[Exposed=(Window,Worker)]
+interface Report {
+ [Default] object toJSON();
+ readonly attribute DOMString type;
+ readonly attribute DOMString url;
+ readonly attribute ReportBody? body;
+};
+
+[Exposed=(Window,Worker)]
+interface ReportingObserver {
+ constructor(ReportingObserverCallback callback, optional ReportingObserverOptions options = {});
+ undefined observe();
+ undefined disconnect();
+ ReportList takeRecords();
+};
+
+callback ReportingObserverCallback = undefined (sequence<Report> reports, ReportingObserver observer);
+
+dictionary ReportingObserverOptions {
+ sequence<DOMString> types;
+ boolean buffered = false;
+};
+
+typedef sequence<Report> ReportList;
+
+dictionary GenerateTestReportParameters {
+ required DOMString message;
+ DOMString group = "default";
+};
diff --git a/test/wpt/tests/interfaces/requestStorageAccessFor.idl b/test/wpt/tests/interfaces/requestStorageAccessFor.idl
new file mode 100644
index 0000000..adca77a
--- /dev/null
+++ b/test/wpt/tests/interfaces/requestStorageAccessFor.idl
@@ -0,0 +1,12 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: requestStorageAccessFor API (https://privacycg.github.io/requestStorageAccessFor/)
+
+partial interface Document {
+ Promise<undefined> requestStorageAccessFor(USVString requestedOrigin);
+};
+
+dictionary TopLevelStorageAccessPermissionDescriptor : PermissionDescriptor {
+ USVString requestedOrigin = "";
+};
diff --git a/test/wpt/tests/interfaces/requestidlecallback.idl b/test/wpt/tests/interfaces/requestidlecallback.idl
new file mode 100644
index 0000000..9c49aa1
--- /dev/null
+++ b/test/wpt/tests/interfaces/requestidlecallback.idl
@@ -0,0 +1,20 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: requestIdleCallback() (https://w3c.github.io/requestidlecallback/)
+
+partial interface Window {
+ unsigned long requestIdleCallback(IdleRequestCallback callback, optional IdleRequestOptions options = {});
+ undefined cancelIdleCallback(unsigned long handle);
+};
+
+dictionary IdleRequestOptions {
+ unsigned long timeout;
+};
+
+[Exposed=Window] interface IdleDeadline {
+ DOMHighResTimeStamp timeRemaining();
+ readonly attribute boolean didTimeout;
+};
+
+callback IdleRequestCallback = undefined (IdleDeadline deadline);
diff --git a/test/wpt/tests/interfaces/resize-observer.idl b/test/wpt/tests/interfaces/resize-observer.idl
new file mode 100644
index 0000000..07f9703
--- /dev/null
+++ b/test/wpt/tests/interfaces/resize-observer.idl
@@ -0,0 +1,37 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Resize Observer (https://drafts.csswg.org/resize-observer-1/)
+
+enum ResizeObserverBoxOptions {
+ "border-box", "content-box", "device-pixel-content-box"
+};
+
+dictionary ResizeObserverOptions {
+ ResizeObserverBoxOptions box = "content-box";
+};
+
+[Exposed=(Window)]
+interface ResizeObserver {
+ constructor(ResizeObserverCallback callback);
+ undefined observe(Element target, optional ResizeObserverOptions options = {});
+ undefined unobserve(Element target);
+ undefined disconnect();
+};
+
+callback ResizeObserverCallback = undefined (sequence<ResizeObserverEntry> entries, ResizeObserver observer);
+
+[Exposed=Window]
+interface ResizeObserverEntry {
+ readonly attribute Element target;
+ readonly attribute DOMRectReadOnly contentRect;
+ readonly attribute FrozenArray<ResizeObserverSize> borderBoxSize;
+ readonly attribute FrozenArray<ResizeObserverSize> contentBoxSize;
+ readonly attribute FrozenArray<ResizeObserverSize> devicePixelContentBoxSize;
+};
+
+[Exposed=Window]
+interface ResizeObserverSize {
+ readonly attribute unrestricted double inlineSize;
+ readonly attribute unrestricted double blockSize;
+};
diff --git a/test/wpt/tests/interfaces/resource-timing.idl b/test/wpt/tests/interfaces/resource-timing.idl
new file mode 100644
index 0000000..33fed05
--- /dev/null
+++ b/test/wpt/tests/interfaces/resource-timing.idl
@@ -0,0 +1,42 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Resource Timing (https://w3c.github.io/resource-timing/)
+
+[Exposed=(Window,Worker)]
+interface PerformanceResourceTiming : PerformanceEntry {
+ readonly attribute DOMString initiatorType;
+ readonly attribute DOMString deliveryType;
+ readonly attribute ByteString nextHopProtocol;
+ readonly attribute DOMHighResTimeStamp workerStart;
+ readonly attribute DOMHighResTimeStamp redirectStart;
+ readonly attribute DOMHighResTimeStamp redirectEnd;
+ readonly attribute DOMHighResTimeStamp fetchStart;
+ readonly attribute DOMHighResTimeStamp domainLookupStart;
+ readonly attribute DOMHighResTimeStamp domainLookupEnd;
+ readonly attribute DOMHighResTimeStamp connectStart;
+ readonly attribute DOMHighResTimeStamp connectEnd;
+ readonly attribute DOMHighResTimeStamp secureConnectionStart;
+ readonly attribute DOMHighResTimeStamp requestStart;
+ readonly attribute DOMHighResTimeStamp firstInterimResponseStart;
+ readonly attribute DOMHighResTimeStamp responseStart;
+ readonly attribute DOMHighResTimeStamp responseEnd;
+ readonly attribute unsigned long long transferSize;
+ readonly attribute unsigned long long encodedBodySize;
+ readonly attribute unsigned long long decodedBodySize;
+ readonly attribute unsigned short responseStatus;
+ readonly attribute RenderBlockingStatusType renderBlockingStatus;
+ readonly attribute DOMString contentType;
+ [Default] object toJSON();
+};
+
+enum RenderBlockingStatusType {
+ "blocking",
+ "non-blocking"
+};
+
+partial interface Performance {
+ undefined clearResourceTimings ();
+ undefined setResourceTimingBufferSize (unsigned long maxSize);
+ attribute EventHandler onresourcetimingbufferfull;
+};
diff --git a/test/wpt/tests/interfaces/sanitizer-api.idl b/test/wpt/tests/interfaces/sanitizer-api.idl
new file mode 100644
index 0000000..117a55f
--- /dev/null
+++ b/test/wpt/tests/interfaces/sanitizer-api.idl
@@ -0,0 +1,38 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: HTML Sanitizer API (https://wicg.github.io/sanitizer-api/)
+
+[
+ Exposed=(Window),
+ SecureContext
+] interface Sanitizer {
+ constructor(optional SanitizerConfig config = {});
+
+ DocumentFragment sanitize((Document or DocumentFragment) input);
+ Element? sanitizeFor(DOMString element, DOMString input);
+
+ SanitizerConfig getConfiguration();
+ static SanitizerConfig getDefaultConfiguration();
+};
+
+dictionary SetHTMLOptions {
+ Sanitizer sanitizer;
+};
+[SecureContext]
+partial interface Element {
+ undefined setHTML(DOMString input, optional SetHTMLOptions options = {});
+};
+
+dictionary SanitizerConfig {
+ sequence<DOMString> allowElements;
+ sequence<DOMString> blockElements;
+ sequence<DOMString> dropElements;
+ AttributeMatchList allowAttributes;
+ AttributeMatchList dropAttributes;
+ boolean allowCustomElements;
+ boolean allowUnknownMarkup;
+ boolean allowComments;
+};
+
+typedef record<DOMString, sequence<DOMString>> AttributeMatchList;
diff --git a/test/wpt/tests/interfaces/sanitizer-api.tentative.idl b/test/wpt/tests/interfaces/sanitizer-api.tentative.idl
new file mode 100644
index 0000000..3e843d8
--- /dev/null
+++ b/test/wpt/tests/interfaces/sanitizer-api.tentative.idl
@@ -0,0 +1,17 @@
+// https://wicg.github.io/sanitizer-api/
+
+[
+ Exposed=Window,
+ SecureContext
+] interface Sanitizer {
+ constructor(optional SanitizerConfig sanitizerConfig = {});
+ DocumentFragment sanitize((DocumentFragment or Document) input);
+};
+
+dictionary SanitizerConfig {
+ sequence<DOMString> allowElements;
+ sequence<DOMString> blockElements;
+ sequence<DOMString> dropElements;
+ sequence<DOMString> allowAttributes;
+ sequence<DOMString> dropAttributes;
+};
diff --git a/test/wpt/tests/interfaces/savedata.idl b/test/wpt/tests/interfaces/savedata.idl
new file mode 100644
index 0000000..f1274b8
--- /dev/null
+++ b/test/wpt/tests/interfaces/savedata.idl
@@ -0,0 +1,10 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Save Data API (https://wicg.github.io/savedata/)
+
+interface mixin NetworkInformationSaveData {
+ [SameObject] readonly attribute boolean saveData;
+};
+
+NetworkInformation includes NetworkInformationSaveData;
diff --git a/test/wpt/tests/interfaces/scheduling-apis.idl b/test/wpt/tests/interfaces/scheduling-apis.idl
new file mode 100644
index 0000000..1e84e79
--- /dev/null
+++ b/test/wpt/tests/interfaces/scheduling-apis.idl
@@ -0,0 +1,63 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Prioritized Task Scheduling (https://wicg.github.io/scheduling-apis/)
+
+enum TaskPriority {
+ "user-blocking",
+ "user-visible",
+ "background"
+};
+
+dictionary SchedulerPostTaskOptions {
+ AbortSignal signal;
+ TaskPriority priority;
+ [EnforceRange] unsigned long long delay = 0;
+};
+
+callback SchedulerPostTaskCallback = any ();
+
+[Exposed=(Window, Worker)]
+interface Scheduler {
+ Promise<any> postTask(SchedulerPostTaskCallback callback,
+ optional SchedulerPostTaskOptions options = {});
+};
+
+[Exposed=(Window, Worker)]
+interface TaskPriorityChangeEvent : Event {
+ constructor(DOMString type, TaskPriorityChangeEventInit priorityChangeEventInitDict);
+
+ readonly attribute TaskPriority previousPriority;
+};
+
+dictionary TaskPriorityChangeEventInit : EventInit {
+ required TaskPriority previousPriority;
+};
+
+dictionary TaskControllerInit {
+ TaskPriority priority = "user-visible";
+};
+
+[Exposed=(Window,Worker)]
+interface TaskController : AbortController {
+ constructor(optional TaskControllerInit init = {});
+
+ undefined setPriority(TaskPriority priority);
+};
+
+dictionary TaskSignalAnyInit {
+ (TaskPriority or TaskSignal) priority = "user-visible";
+};
+
+[Exposed=(Window, Worker)]
+interface TaskSignal : AbortSignal {
+ [NewObject] static TaskSignal _any(sequence<AbortSignal> signals, optional TaskSignalAnyInit init = {});
+
+ readonly attribute TaskPriority priority;
+
+ attribute EventHandler onprioritychange;
+};
+
+partial interface mixin WindowOrWorkerGlobalScope {
+ [Replaceable] readonly attribute Scheduler scheduler;
+};
diff --git a/test/wpt/tests/interfaces/screen-capture.idl b/test/wpt/tests/interfaces/screen-capture.idl
new file mode 100644
index 0000000..830b96d
--- /dev/null
+++ b/test/wpt/tests/interfaces/screen-capture.idl
@@ -0,0 +1,85 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Screen Capture (https://w3c.github.io/mediacapture-screen-share/)
+
+partial interface MediaDevices {
+ Promise<MediaStream> getDisplayMedia(optional DisplayMediaStreamOptions options = {});
+};
+
+enum CaptureStartFocusBehavior {
+ "focus-captured-surface",
+ "no-focus-change"
+};
+
+[Exposed=Window, SecureContext]
+interface CaptureController : EventTarget {
+ constructor();
+ undefined setFocusBehavior(CaptureStartFocusBehavior focusBehavior);
+};
+
+enum SelfCapturePreferenceEnum {
+ "include",
+ "exclude"
+};
+
+enum SystemAudioPreferenceEnum {
+ "include",
+ "exclude"
+};
+
+enum SurfaceSwitchingPreferenceEnum {
+ "include",
+ "exclude"
+};
+
+dictionary DisplayMediaStreamOptions {
+ (boolean or MediaTrackConstraints) video = true;
+ (boolean or MediaTrackConstraints) audio = false;
+ CaptureController controller;
+ SelfCapturePreferenceEnum selfBrowserSurface;
+ SystemAudioPreferenceEnum systemAudio;
+ SurfaceSwitchingPreferenceEnum surfaceSwitching;
+};
+
+partial dictionary MediaTrackSupportedConstraints {
+ boolean displaySurface = true;
+ boolean logicalSurface = true;
+ boolean cursor = true;
+ boolean restrictOwnAudio = true;
+ boolean suppressLocalAudioPlayback = true;
+};
+
+partial dictionary MediaTrackConstraintSet {
+ ConstrainDOMString displaySurface;
+ ConstrainBoolean logicalSurface;
+ ConstrainDOMString cursor;
+ ConstrainBoolean restrictOwnAudio;
+ ConstrainBoolean suppressLocalAudioPlayback;
+};
+
+partial dictionary MediaTrackSettings {
+ DOMString displaySurface;
+ boolean logicalSurface;
+ DOMString cursor;
+ boolean restrictOwnAudio;
+ boolean suppressLocalAudioPlayback;
+};
+
+partial dictionary MediaTrackCapabilities {
+ DOMString displaySurface;
+ boolean logicalSurface;
+ sequence<DOMString> cursor;
+};
+
+enum DisplayCaptureSurfaceType {
+ "monitor",
+ "window",
+ "browser"
+};
+
+enum CursorCaptureConstraint {
+ "never",
+ "always",
+ "motion"
+};
diff --git a/test/wpt/tests/interfaces/screen-orientation.idl b/test/wpt/tests/interfaces/screen-orientation.idl
new file mode 100644
index 0000000..df8a1db
--- /dev/null
+++ b/test/wpt/tests/interfaces/screen-orientation.idl
@@ -0,0 +1,35 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Screen Orientation (https://w3c.github.io/screen-orientation/)
+
+partial interface Screen {
+ [SameObject] readonly attribute ScreenOrientation orientation;
+};
+
+[Exposed=Window]
+interface ScreenOrientation : EventTarget {
+ Promise<undefined> lock(OrientationLockType orientation);
+ undefined unlock();
+ readonly attribute OrientationType type;
+ readonly attribute unsigned short angle;
+ attribute EventHandler onchange;
+};
+
+enum OrientationLockType {
+ "any",
+ "natural",
+ "landscape",
+ "portrait",
+ "portrait-primary",
+ "portrait-secondary",
+ "landscape-primary",
+ "landscape-secondary"
+};
+
+enum OrientationType {
+ "portrait-primary",
+ "portrait-secondary",
+ "landscape-primary",
+ "landscape-secondary"
+};
diff --git a/test/wpt/tests/interfaces/screen-wake-lock.idl b/test/wpt/tests/interfaces/screen-wake-lock.idl
new file mode 100644
index 0000000..c9d259e
--- /dev/null
+++ b/test/wpt/tests/interfaces/screen-wake-lock.idl
@@ -0,0 +1,24 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Screen Wake Lock API (https://w3c.github.io/screen-wake-lock/)
+
+[SecureContext]
+partial interface Navigator {
+ [SameObject] readonly attribute WakeLock wakeLock;
+};
+
+[SecureContext, Exposed=(Window)]
+interface WakeLock {
+ Promise<WakeLockSentinel> request(optional WakeLockType type = "screen");
+};
+
+[SecureContext, Exposed=(Window)]
+interface WakeLockSentinel : EventTarget {
+ readonly attribute boolean released;
+ readonly attribute WakeLockType type;
+ Promise<undefined> release();
+ attribute EventHandler onrelease;
+};
+
+enum WakeLockType { "screen" };
diff --git a/test/wpt/tests/interfaces/scroll-animations.idl b/test/wpt/tests/interfaces/scroll-animations.idl
new file mode 100644
index 0000000..31b3746
--- /dev/null
+++ b/test/wpt/tests/interfaces/scroll-animations.idl
@@ -0,0 +1,46 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Scroll-driven Animations (https://drafts.csswg.org/scroll-animations-1/)
+
+enum ScrollAxis {
+ "block",
+ "inline",
+ "horizontal",
+ "vertical"
+};
+
+dictionary ScrollTimelineOptions {
+ Element? source;
+ ScrollAxis axis = "block";
+};
+
+[Exposed=Window]
+interface ScrollTimeline : AnimationTimeline {
+ constructor(optional ScrollTimelineOptions options = {});
+ readonly attribute Element? source;
+ readonly attribute ScrollAxis axis;
+};
+
+dictionary ViewTimelineOptions {
+ Element subject;
+ ScrollAxis axis = "block";
+ (DOMString or sequence<(CSSNumericValue or CSSKeywordValue)>) inset = "auto";
+};
+
+[Exposed=Window]
+interface ViewTimeline : ScrollTimeline {
+ constructor(optional ViewTimelineOptions options = {});
+ readonly attribute Element subject;
+ readonly attribute CSSNumericValue startOffset;
+ readonly attribute CSSNumericValue endOffset;
+};
+
+dictionary AnimationTimeOptions {
+ DOMString? range;
+};
+
+[Exposed=Window]
+partial interface AnimationTimeline {
+ CSSNumericValue? getCurrentTime(optional AnimationTimeOptions options = {});
+};
diff --git a/test/wpt/tests/interfaces/scroll-to-text-fragment.idl b/test/wpt/tests/interfaces/scroll-to-text-fragment.idl
new file mode 100644
index 0000000..be7bf73
--- /dev/null
+++ b/test/wpt/tests/interfaces/scroll-to-text-fragment.idl
@@ -0,0 +1,12 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: URL Fragment Text Directives (https://wicg.github.io/scroll-to-text-fragment/)
+
+[Exposed=Window]
+interface FragmentDirective {
+};
+
+partial interface Document {
+ [SameObject] readonly attribute FragmentDirective fragmentDirective;
+};
diff --git a/test/wpt/tests/interfaces/secure-payment-confirmation.idl b/test/wpt/tests/interfaces/secure-payment-confirmation.idl
new file mode 100644
index 0000000..08ec806
--- /dev/null
+++ b/test/wpt/tests/interfaces/secure-payment-confirmation.idl
@@ -0,0 +1,52 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Secure Payment Confirmation (https://w3c.github.io/secure-payment-confirmation/)
+
+dictionary SecurePaymentConfirmationRequest {
+ required BufferSource challenge;
+ required USVString rpId;
+ required sequence<BufferSource> credentialIds;
+ required PaymentCredentialInstrument instrument;
+ unsigned long timeout;
+ USVString payeeName;
+ USVString payeeOrigin;
+ AuthenticationExtensionsClientInputs extensions;
+ sequence<USVString> locale;
+ boolean showOptOut;
+};
+
+partial dictionary AuthenticationExtensionsClientInputs {
+ AuthenticationExtensionsPaymentInputs payment;
+};
+
+dictionary AuthenticationExtensionsPaymentInputs {
+ boolean isPayment;
+
+ // Only used for authentication.
+ USVString rpId;
+ USVString topOrigin;
+ USVString payeeName;
+ USVString payeeOrigin;
+ PaymentCurrencyAmount total;
+ PaymentCredentialInstrument instrument;
+};
+
+dictionary CollectedClientPaymentData : CollectedClientData {
+ required CollectedClientAdditionalPaymentData payment;
+};
+
+dictionary CollectedClientAdditionalPaymentData {
+ required USVString rpId;
+ required USVString topOrigin;
+ USVString payeeName;
+ USVString payeeOrigin;
+ required PaymentCurrencyAmount total;
+ required PaymentCredentialInstrument instrument;
+};
+
+dictionary PaymentCredentialInstrument {
+ required USVString displayName;
+ required USVString icon;
+ boolean iconMustBeShown = true;
+};
diff --git a/test/wpt/tests/interfaces/selection-api.idl b/test/wpt/tests/interfaces/selection-api.idl
new file mode 100644
index 0000000..a84536a
--- /dev/null
+++ b/test/wpt/tests/interfaces/selection-api.idl
@@ -0,0 +1,46 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Selection API (https://w3c.github.io/selection-api/)
+
+[Exposed=Window]
+interface Selection {
+ readonly attribute Node? anchorNode;
+ readonly attribute unsigned long anchorOffset;
+ readonly attribute Node? focusNode;
+ readonly attribute unsigned long focusOffset;
+ readonly attribute boolean isCollapsed;
+ readonly attribute unsigned long rangeCount;
+ readonly attribute DOMString type;
+ readonly attribute DOMString direction;
+ Range getRangeAt(unsigned long index);
+ undefined addRange(Range range);
+ undefined removeRange(Range range);
+ undefined removeAllRanges();
+ undefined empty();
+ sequence<StaticRange> getComposedRanges(ShadowRoot... shadowRoots);
+ undefined collapse(Node? node, optional unsigned long offset = 0);
+ undefined setPosition(Node? node, optional unsigned long offset = 0);
+ undefined collapseToStart();
+ undefined collapseToEnd();
+ undefined extend(Node node, optional unsigned long offset = 0);
+ undefined setBaseAndExtent(Node anchorNode, unsigned long anchorOffset, Node focusNode, unsigned long focusOffset);
+ undefined selectAllChildren(Node node);
+ undefined modify(optional DOMString alter, optional DOMString direction, optional DOMString granularity);
+ [CEReactions] undefined deleteFromDocument();
+ boolean containsNode(Node node, optional boolean allowPartialContainment = false);
+ stringifier;
+};
+
+partial interface Document {
+ Selection? getSelection();
+};
+
+partial interface Window {
+ Selection? getSelection();
+};
+
+partial interface mixin GlobalEventHandlers {
+ attribute EventHandler onselectstart;
+ attribute EventHandler onselectionchange;
+};
diff --git a/test/wpt/tests/interfaces/serial.idl b/test/wpt/tests/interfaces/serial.idl
new file mode 100644
index 0000000..ee46a85
--- /dev/null
+++ b/test/wpt/tests/interfaces/serial.idl
@@ -0,0 +1,85 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Web Serial API (https://wicg.github.io/serial/)
+
+[Exposed=Window, SecureContext]
+partial interface Navigator {
+ [SameObject] readonly attribute Serial serial;
+};
+
+[Exposed=DedicatedWorker, SecureContext]
+partial interface WorkerNavigator {
+ [SameObject] readonly attribute Serial serial;
+};
+
+[Exposed=(DedicatedWorker, Window), SecureContext]
+interface Serial : EventTarget {
+ attribute EventHandler onconnect;
+ attribute EventHandler ondisconnect;
+ Promise<sequence<SerialPort>> getPorts();
+ [Exposed=Window] Promise<SerialPort> requestPort(optional SerialPortRequestOptions options = {});
+};
+
+dictionary SerialPortRequestOptions {
+ sequence<SerialPortFilter> filters;
+};
+
+dictionary SerialPortFilter {
+ unsigned short usbVendorId;
+ unsigned short usbProductId;
+};
+
+[Exposed=(DedicatedWorker,Window), SecureContext]
+interface SerialPort : EventTarget {
+ attribute EventHandler onconnect;
+ attribute EventHandler ondisconnect;
+ readonly attribute ReadableStream readable;
+ readonly attribute WritableStream writable;
+
+ SerialPortInfo getInfo();
+
+ Promise<undefined> open(SerialOptions options);
+ Promise<undefined> setSignals(optional SerialOutputSignals signals = {});
+ Promise<SerialInputSignals> getSignals();
+ Promise<undefined> close();
+ Promise<undefined> forget();
+};
+
+dictionary SerialPortInfo {
+ unsigned short usbVendorId;
+ unsigned short usbProductId;
+};
+
+dictionary SerialOptions {
+ [EnforceRange] required unsigned long baudRate;
+ [EnforceRange] octet dataBits = 8;
+ [EnforceRange] octet stopBits = 1;
+ ParityType parity = "none";
+ [EnforceRange] unsigned long bufferSize = 255;
+ FlowControlType flowControl = "none";
+};
+
+enum ParityType {
+ "none",
+ "even",
+ "odd"
+};
+
+enum FlowControlType {
+ "none",
+ "hardware"
+};
+
+dictionary SerialOutputSignals {
+ boolean dataTerminalReady;
+ boolean requestToSend;
+ boolean break;
+};
+
+dictionary SerialInputSignals {
+ required boolean dataCarrierDetect;
+ required boolean clearToSend;
+ required boolean ringIndicator;
+ required boolean dataSetReady;
+};
diff --git a/test/wpt/tests/interfaces/server-timing.idl b/test/wpt/tests/interfaces/server-timing.idl
new file mode 100644
index 0000000..ef2a761
--- /dev/null
+++ b/test/wpt/tests/interfaces/server-timing.idl
@@ -0,0 +1,17 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Server Timing (https://w3c.github.io/server-timing/)
+
+[Exposed=(Window,Worker)]
+interface PerformanceServerTiming {
+ readonly attribute DOMString name;
+ readonly attribute DOMHighResTimeStamp duration;
+ readonly attribute DOMString description;
+ [Default] object toJSON();
+};
+
+[Exposed=(Window,Worker)]
+partial interface PerformanceResourceTiming {
+ readonly attribute FrozenArray<PerformanceServerTiming> serverTiming;
+};
diff --git a/test/wpt/tests/interfaces/service-workers.idl b/test/wpt/tests/interfaces/service-workers.idl
new file mode 100644
index 0000000..6d44d61
--- /dev/null
+++ b/test/wpt/tests/interfaces/service-workers.idl
@@ -0,0 +1,240 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Service Workers Nightly (https://w3c.github.io/ServiceWorker/)
+
+[SecureContext, Exposed=(Window,Worker)]
+interface ServiceWorker : EventTarget {
+ readonly attribute USVString scriptURL;
+ readonly attribute ServiceWorkerState state;
+ undefined postMessage(any message, sequence<object> transfer);
+ undefined postMessage(any message, optional StructuredSerializeOptions options = {});
+
+ // event
+ attribute EventHandler onstatechange;
+};
+ServiceWorker includes AbstractWorker;
+
+enum ServiceWorkerState {
+ "parsed",
+ "installing",
+ "installed",
+ "activating",
+ "activated",
+ "redundant"
+};
+
+[SecureContext, Exposed=(Window,Worker)]
+interface ServiceWorkerRegistration : EventTarget {
+ readonly attribute ServiceWorker? installing;
+ readonly attribute ServiceWorker? waiting;
+ readonly attribute ServiceWorker? active;
+ [SameObject] readonly attribute NavigationPreloadManager navigationPreload;
+
+ readonly attribute USVString scope;
+ readonly attribute ServiceWorkerUpdateViaCache updateViaCache;
+
+ [NewObject] Promise<undefined> update();
+ [NewObject] Promise<boolean> unregister();
+
+ // event
+ attribute EventHandler onupdatefound;
+};
+
+enum ServiceWorkerUpdateViaCache {
+ "imports",
+ "all",
+ "none"
+};
+
+partial interface Navigator {
+ [SecureContext, SameObject] readonly attribute ServiceWorkerContainer serviceWorker;
+};
+
+partial interface WorkerNavigator {
+ [SecureContext, SameObject] readonly attribute ServiceWorkerContainer serviceWorker;
+};
+
+[SecureContext, Exposed=(Window,Worker)]
+interface ServiceWorkerContainer : EventTarget {
+ readonly attribute ServiceWorker? controller;
+ readonly attribute Promise<ServiceWorkerRegistration> ready;
+
+ [NewObject] Promise<ServiceWorkerRegistration> register(USVString scriptURL, optional RegistrationOptions options = {});
+
+ [NewObject] Promise<(ServiceWorkerRegistration or undefined)> getRegistration(optional USVString clientURL = "");
+ [NewObject] Promise<FrozenArray<ServiceWorkerRegistration>> getRegistrations();
+
+ undefined startMessages();
+
+ // events
+ attribute EventHandler oncontrollerchange;
+ attribute EventHandler onmessage; // event.source of message events is ServiceWorker object
+ attribute EventHandler onmessageerror;
+};
+
+dictionary RegistrationOptions {
+ USVString scope;
+ WorkerType type = "classic";
+ ServiceWorkerUpdateViaCache updateViaCache = "imports";
+};
+
+[SecureContext, Exposed=(Window,Worker)]
+interface NavigationPreloadManager {
+ Promise<undefined> enable();
+ Promise<undefined> disable();
+ Promise<undefined> setHeaderValue(ByteString value);
+ Promise<NavigationPreloadState> getState();
+};
+
+dictionary NavigationPreloadState {
+ boolean enabled = false;
+ ByteString headerValue;
+};
+
+[Global=(Worker,ServiceWorker), Exposed=ServiceWorker]
+interface ServiceWorkerGlobalScope : WorkerGlobalScope {
+ [SameObject] readonly attribute Clients clients;
+ [SameObject] readonly attribute ServiceWorkerRegistration registration;
+ [SameObject] readonly attribute ServiceWorker serviceWorker;
+
+ [NewObject] Promise<undefined> skipWaiting();
+
+ attribute EventHandler oninstall;
+ attribute EventHandler onactivate;
+ attribute EventHandler onfetch;
+
+ attribute EventHandler onmessage;
+ attribute EventHandler onmessageerror;
+};
+
+[Exposed=ServiceWorker]
+interface Client {
+ readonly attribute USVString url;
+ readonly attribute FrameType frameType;
+ readonly attribute DOMString id;
+ readonly attribute ClientType type;
+ undefined postMessage(any message, sequence<object> transfer);
+ undefined postMessage(any message, optional StructuredSerializeOptions options = {});
+};
+
+[Exposed=ServiceWorker]
+interface WindowClient : Client {
+ readonly attribute DocumentVisibilityState visibilityState;
+ readonly attribute boolean focused;
+ [SameObject] readonly attribute FrozenArray<USVString> ancestorOrigins;
+ [NewObject] Promise<WindowClient> focus();
+ [NewObject] Promise<WindowClient?> navigate(USVString url);
+};
+
+enum FrameType {
+ "auxiliary",
+ "top-level",
+ "nested",
+ "none"
+};
+
+[Exposed=ServiceWorker]
+interface Clients {
+ // The objects returned will be new instances every time
+ [NewObject] Promise<(Client or undefined)> get(DOMString id);
+ [NewObject] Promise<FrozenArray<Client>> matchAll(optional ClientQueryOptions options = {});
+ [NewObject] Promise<WindowClient?> openWindow(USVString url);
+ [NewObject] Promise<undefined> claim();
+};
+
+dictionary ClientQueryOptions {
+ boolean includeUncontrolled = false;
+ ClientType type = "window";
+};
+
+enum ClientType {
+ "window",
+ "worker",
+ "sharedworker",
+ "all"
+};
+
+[Exposed=ServiceWorker]
+interface ExtendableEvent : Event {
+ constructor(DOMString type, optional ExtendableEventInit eventInitDict = {});
+ undefined waitUntil(Promise<any> f);
+};
+
+dictionary ExtendableEventInit : EventInit {
+ // Defined for the forward compatibility across the derived events
+};
+
+[Exposed=ServiceWorker]
+interface FetchEvent : ExtendableEvent {
+ constructor(DOMString type, FetchEventInit eventInitDict);
+ [SameObject] readonly attribute Request request;
+ readonly attribute Promise<any> preloadResponse;
+ readonly attribute DOMString clientId;
+ readonly attribute DOMString resultingClientId;
+ readonly attribute DOMString replacesClientId;
+ readonly attribute Promise<undefined> handled;
+
+ undefined respondWith(Promise<Response> r);
+};
+
+dictionary FetchEventInit : ExtendableEventInit {
+ required Request request;
+ Promise<any> preloadResponse;
+ DOMString clientId = "";
+ DOMString resultingClientId = "";
+ DOMString replacesClientId = "";
+ Promise<undefined> handled;
+};
+
+[Exposed=ServiceWorker]
+interface ExtendableMessageEvent : ExtendableEvent {
+ constructor(DOMString type, optional ExtendableMessageEventInit eventInitDict = {});
+ readonly attribute any data;
+ readonly attribute USVString origin;
+ readonly attribute DOMString lastEventId;
+ [SameObject] readonly attribute (Client or ServiceWorker or MessagePort)? source;
+ readonly attribute FrozenArray<MessagePort> ports;
+};
+
+dictionary ExtendableMessageEventInit : ExtendableEventInit {
+ any data = null;
+ USVString origin = "";
+ DOMString lastEventId = "";
+ (Client or ServiceWorker or MessagePort)? source = null;
+ sequence<MessagePort> ports = [];
+};
+
+partial interface mixin WindowOrWorkerGlobalScope {
+ [SecureContext, SameObject] readonly attribute CacheStorage caches;
+};
+
+[SecureContext, Exposed=(Window,Worker)]
+interface Cache {
+ [NewObject] Promise<(Response or undefined)> match(RequestInfo request, optional CacheQueryOptions options = {});
+ [NewObject] Promise<FrozenArray<Response>> matchAll(optional RequestInfo request, optional CacheQueryOptions options = {});
+ [NewObject] Promise<undefined> add(RequestInfo request);
+ [NewObject] Promise<undefined> addAll(sequence<RequestInfo> requests);
+ [NewObject] Promise<undefined> put(RequestInfo request, Response response);
+ [NewObject] Promise<boolean> delete(RequestInfo request, optional CacheQueryOptions options = {});
+ [NewObject] Promise<FrozenArray<Request>> keys(optional RequestInfo request, optional CacheQueryOptions options = {});
+};
+
+dictionary CacheQueryOptions {
+ boolean ignoreSearch = false;
+ boolean ignoreMethod = false;
+ boolean ignoreVary = false;
+};
+
+[SecureContext, Exposed=(Window,Worker)]
+interface CacheStorage {
+ [NewObject] Promise<(Response or undefined)> match(RequestInfo request, optional MultiCacheQueryOptions options = {});
+ [NewObject] Promise<boolean> has(DOMString cacheName);
+ [NewObject] Promise<Cache> open(DOMString cacheName);
+ [NewObject] Promise<boolean> delete(DOMString cacheName);
+ [NewObject] Promise<sequence<DOMString>> keys();
+};
+
+dictionary MultiCacheQueryOptions : CacheQueryOptions {
+ DOMString cacheName;
+};
diff --git a/test/wpt/tests/interfaces/shape-detection-api.idl b/test/wpt/tests/interfaces/shape-detection-api.idl
new file mode 100644
index 0000000..4fc1f08
--- /dev/null
+++ b/test/wpt/tests/interfaces/shape-detection-api.idl
@@ -0,0 +1,69 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Accelerated Shape Detection in Images (https://wicg.github.io/shape-detection-api/)
+
+[Exposed=(Window,Worker),
+ SecureContext]
+interface FaceDetector {
+ constructor(optional FaceDetectorOptions faceDetectorOptions = {});
+ Promise<sequence<DetectedFace>> detect(ImageBitmapSource image);
+};
+
+dictionary FaceDetectorOptions {
+ unsigned short maxDetectedFaces;
+ boolean fastMode;
+};
+
+dictionary DetectedFace {
+ required DOMRectReadOnly boundingBox;
+ required FrozenArray<Landmark>? landmarks;
+};
+
+dictionary Landmark {
+ required FrozenArray<Point2D> locations;
+ LandmarkType type;
+};
+
+enum LandmarkType {
+ "mouth",
+ "eye",
+ "nose"
+};
+
+[Exposed=(Window,Worker),
+ SecureContext]
+interface BarcodeDetector {
+ constructor(optional BarcodeDetectorOptions barcodeDetectorOptions = {});
+ static Promise<sequence<BarcodeFormat>> getSupportedFormats();
+
+ Promise<sequence<DetectedBarcode>> detect(ImageBitmapSource image);
+};
+
+dictionary BarcodeDetectorOptions {
+ sequence<BarcodeFormat> formats;
+};
+
+dictionary DetectedBarcode {
+ required DOMRectReadOnly boundingBox;
+ required DOMString rawValue;
+ required BarcodeFormat format;
+ required FrozenArray<Point2D> cornerPoints;
+};
+
+enum BarcodeFormat {
+ "aztec",
+ "code_128",
+ "code_39",
+ "code_93",
+ "codabar",
+ "data_matrix",
+ "ean_13",
+ "ean_8",
+ "itf",
+ "pdf417",
+ "qr_code",
+ "unknown",
+ "upc_a",
+ "upc_e"
+};
diff --git a/test/wpt/tests/interfaces/shared-storage.idl b/test/wpt/tests/interfaces/shared-storage.idl
new file mode 100644
index 0000000..eb5806f
--- /dev/null
+++ b/test/wpt/tests/interfaces/shared-storage.idl
@@ -0,0 +1,80 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Shared Storage API (https://wicg.github.io/shared-storage/)
+
+[Exposed=(Window)]
+interface SharedStorageWorklet : Worklet {
+};
+
+[Exposed=SharedStorageWorklet, Global=SharedStorageWorklet]
+interface SharedStorageWorkletGlobalScope : WorkletGlobalScope {
+ undefined register(DOMString name,
+ SharedStorageOperationConstructor operationCtor);
+};
+
+callback SharedStorageOperationConstructor =
+ SharedStorageOperation(optional SharedStorageRunOperationMethodOptions options);
+
+[Exposed=SharedStorageWorklet]
+interface SharedStorageOperation {
+};
+
+dictionary SharedStorageRunOperationMethodOptions {
+ object data;
+ boolean resolveToConfig = false;
+ boolean keepAlive = false;
+};
+
+[Exposed=SharedStorageWorklet]
+interface SharedStorageRunOperation : SharedStorageOperation {
+ Promise<undefined> run(object data);
+};
+
+[Exposed=SharedStorageWorklet]
+interface SharedStorageSelectURLOperation : SharedStorageOperation {
+ Promise<long> run(object data,
+ FrozenArray<SharedStorageUrlWithMetadata> urls);
+};
+
+[Exposed=(Window,SharedStorageWorklet)]
+interface SharedStorage {
+ Promise<any> set(DOMString key,
+ DOMString value,
+ optional SharedStorageSetMethodOptions options = {});
+ Promise<any> append(DOMString key,
+ DOMString value);
+ Promise<any> delete(DOMString key);
+ Promise<any> clear();
+};
+
+dictionary SharedStorageSetMethodOptions {
+ boolean ignoreIfPresent = false;
+};
+
+typedef (USVString or FencedFrameConfig) SharedStorageResponse;
+
+[Exposed=(Window)]
+interface WindowSharedStorage : SharedStorage {
+ Promise<any> run(DOMString name,
+ optional SharedStorageRunOperationMethodOptions options = {});
+ Promise<SharedStorageResponse> selectURL(DOMString name,
+ FrozenArray<SharedStorageUrlWithMetadata> urls,
+ optional SharedStorageRunOperationMethodOptions options = {});
+
+ readonly attribute SharedStorageWorklet worklet;
+};
+
+dictionary SharedStorageUrlWithMetadata {
+ required USVString url;
+ object reportingMetadata;
+};
+
+[Exposed=(SharedStorageWorklet)]
+interface WorkletSharedStorage : SharedStorage {
+ Promise<DOMString> get(DOMString key);
+ Promise<unsigned long> length();
+ Promise<double> remainingBudget();
+
+ async iterable<DOMString, DOMString>;
+};
diff --git a/test/wpt/tests/interfaces/speech-api.idl b/test/wpt/tests/interfaces/speech-api.idl
new file mode 100644
index 0000000..7408548
--- /dev/null
+++ b/test/wpt/tests/interfaces/speech-api.idl
@@ -0,0 +1,202 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Web Speech API (https://wicg.github.io/speech-api/)
+
+[Exposed=Window]
+interface SpeechRecognition : EventTarget {
+ constructor();
+
+ // recognition parameters
+ attribute SpeechGrammarList grammars;
+ attribute DOMString lang;
+ attribute boolean continuous;
+ attribute boolean interimResults;
+ attribute unsigned long maxAlternatives;
+
+ // methods to drive the speech interaction
+ undefined start();
+ undefined stop();
+ undefined abort();
+
+ // event methods
+ attribute EventHandler onaudiostart;
+ attribute EventHandler onsoundstart;
+ attribute EventHandler onspeechstart;
+ attribute EventHandler onspeechend;
+ attribute EventHandler onsoundend;
+ attribute EventHandler onaudioend;
+ attribute EventHandler onresult;
+ attribute EventHandler onnomatch;
+ attribute EventHandler onerror;
+ attribute EventHandler onstart;
+ attribute EventHandler onend;
+};
+
+enum SpeechRecognitionErrorCode {
+ "no-speech",
+ "aborted",
+ "audio-capture",
+ "network",
+ "not-allowed",
+ "service-not-allowed",
+ "bad-grammar",
+ "language-not-supported"
+};
+
+[Exposed=Window]
+interface SpeechRecognitionErrorEvent : Event {
+ constructor(DOMString type, SpeechRecognitionErrorEventInit eventInitDict);
+ readonly attribute SpeechRecognitionErrorCode error;
+ readonly attribute DOMString message;
+};
+
+dictionary SpeechRecognitionErrorEventInit : EventInit {
+ required SpeechRecognitionErrorCode error;
+ DOMString message = "";
+};
+
+// Item in N-best list
+[Exposed=Window]
+interface SpeechRecognitionAlternative {
+ readonly attribute DOMString transcript;
+ readonly attribute float confidence;
+};
+
+// A complete one-shot simple response
+[Exposed=Window]
+interface SpeechRecognitionResult {
+ readonly attribute unsigned long length;
+ getter SpeechRecognitionAlternative item(unsigned long index);
+ readonly attribute boolean isFinal;
+};
+
+// A collection of responses (used in continuous mode)
+[Exposed=Window]
+interface SpeechRecognitionResultList {
+ readonly attribute unsigned long length;
+ getter SpeechRecognitionResult item(unsigned long index);
+};
+
+// A full response, which could be interim or final, part of a continuous response or not
+[Exposed=Window]
+interface SpeechRecognitionEvent : Event {
+ constructor(DOMString type, SpeechRecognitionEventInit eventInitDict);
+ readonly attribute unsigned long resultIndex;
+ readonly attribute SpeechRecognitionResultList results;
+};
+
+dictionary SpeechRecognitionEventInit : EventInit {
+ unsigned long resultIndex = 0;
+ required SpeechRecognitionResultList results;
+};
+
+// The object representing a speech grammar
+[Exposed=Window]
+interface SpeechGrammar {
+ attribute DOMString src;
+ attribute float weight;
+};
+
+// The object representing a speech grammar collection
+[Exposed=Window]
+interface SpeechGrammarList {
+ constructor();
+ readonly attribute unsigned long length;
+ getter SpeechGrammar item(unsigned long index);
+ undefined addFromURI(DOMString src,
+ optional float weight = 1.0);
+ undefined addFromString(DOMString string,
+ optional float weight = 1.0);
+};
+
+[Exposed=Window]
+interface SpeechSynthesis : EventTarget {
+ readonly attribute boolean pending;
+ readonly attribute boolean speaking;
+ readonly attribute boolean paused;
+
+ attribute EventHandler onvoiceschanged;
+
+ undefined speak(SpeechSynthesisUtterance utterance);
+ undefined cancel();
+ undefined pause();
+ undefined resume();
+ sequence<SpeechSynthesisVoice> getVoices();
+};
+
+partial interface Window {
+ [SameObject] readonly attribute SpeechSynthesis speechSynthesis;
+};
+
+[Exposed=Window]
+interface SpeechSynthesisUtterance : EventTarget {
+ constructor(optional DOMString text);
+
+ attribute DOMString text;
+ attribute DOMString lang;
+ attribute SpeechSynthesisVoice? voice;
+ attribute float volume;
+ attribute float rate;
+ attribute float pitch;
+
+ attribute EventHandler onstart;
+ attribute EventHandler onend;
+ attribute EventHandler onerror;
+ attribute EventHandler onpause;
+ attribute EventHandler onresume;
+ attribute EventHandler onmark;
+ attribute EventHandler onboundary;
+};
+
+[Exposed=Window]
+interface SpeechSynthesisEvent : Event {
+ constructor(DOMString type, SpeechSynthesisEventInit eventInitDict);
+ readonly attribute SpeechSynthesisUtterance utterance;
+ readonly attribute unsigned long charIndex;
+ readonly attribute unsigned long charLength;
+ readonly attribute float elapsedTime;
+ readonly attribute DOMString name;
+};
+
+dictionary SpeechSynthesisEventInit : EventInit {
+ required SpeechSynthesisUtterance utterance;
+ unsigned long charIndex = 0;
+ unsigned long charLength = 0;
+ float elapsedTime = 0;
+ DOMString name = "";
+};
+
+enum SpeechSynthesisErrorCode {
+ "canceled",
+ "interrupted",
+ "audio-busy",
+ "audio-hardware",
+ "network",
+ "synthesis-unavailable",
+ "synthesis-failed",
+ "language-unavailable",
+ "voice-unavailable",
+ "text-too-long",
+ "invalid-argument",
+ "not-allowed",
+};
+
+[Exposed=Window]
+interface SpeechSynthesisErrorEvent : SpeechSynthesisEvent {
+ constructor(DOMString type, SpeechSynthesisErrorEventInit eventInitDict);
+ readonly attribute SpeechSynthesisErrorCode error;
+};
+
+dictionary SpeechSynthesisErrorEventInit : SpeechSynthesisEventInit {
+ required SpeechSynthesisErrorCode error;
+};
+
+[Exposed=Window]
+interface SpeechSynthesisVoice {
+ readonly attribute DOMString voiceURI;
+ readonly attribute DOMString name;
+ readonly attribute DOMString lang;
+ readonly attribute boolean localService;
+ readonly attribute boolean default;
+};
diff --git a/test/wpt/tests/interfaces/storage-access.idl b/test/wpt/tests/interfaces/storage-access.idl
new file mode 100644
index 0000000..fff583e
--- /dev/null
+++ b/test/wpt/tests/interfaces/storage-access.idl
@@ -0,0 +1,9 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: The Storage Access API (https://privacycg.github.io/storage-access/)
+
+partial interface Document {
+ Promise<boolean> hasStorageAccess();
+ Promise<undefined> requestStorageAccess();
+};
diff --git a/test/wpt/tests/interfaces/storage-buckets.idl b/test/wpt/tests/interfaces/storage-buckets.idl
new file mode 100644
index 0000000..f3d500a
--- /dev/null
+++ b/test/wpt/tests/interfaces/storage-buckets.idl
@@ -0,0 +1,53 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Storage Buckets API (https://wicg.github.io/storage-buckets/)
+
+[SecureContext]
+interface mixin NavigatorStorageBuckets {
+ [SameObject] readonly attribute StorageBucketManager storageBuckets;
+};
+Navigator includes NavigatorStorageBuckets;
+WorkerNavigator includes NavigatorStorageBuckets;
+
+[Exposed=(Window,Worker),
+ SecureContext]
+interface StorageBucketManager {
+ Promise<StorageBucket> open(DOMString name, optional StorageBucketOptions options = {});
+ Promise<sequence<DOMString>> keys();
+ Promise<undefined> delete(DOMString name);
+};
+
+enum StorageBucketDurability {
+ "strict",
+ "relaxed"
+};
+
+dictionary StorageBucketOptions {
+ boolean? persisted = null;
+ StorageBucketDurability? durability = null;
+ unsigned long long? quota = null;
+ DOMHighResTimeStamp? expires = null;
+};
+
+[Exposed=(Window,Worker),
+ SecureContext]
+interface StorageBucket {
+ readonly attribute DOMString name;
+
+ [Exposed=Window] Promise<boolean> persist();
+ Promise<boolean> persisted();
+
+ Promise<StorageEstimate> estimate();
+
+ Promise<StorageBucketDurability> durability();
+
+ Promise<undefined> setExpires(DOMHighResTimeStamp expires);
+ Promise<DOMHighResTimeStamp?> expires();
+
+ [SameObject] readonly attribute IDBFactory indexedDB;
+
+ [SameObject] readonly attribute CacheStorage caches;
+
+ Promise<FileSystemDirectoryHandle> getDirectory();
+};
diff --git a/test/wpt/tests/interfaces/storage-buckets.tentative.idl b/test/wpt/tests/interfaces/storage-buckets.tentative.idl
new file mode 100644
index 0000000..73d72ce
--- /dev/null
+++ b/test/wpt/tests/interfaces/storage-buckets.tentative.idl
@@ -0,0 +1,36 @@
+[
+ Exposed=(Window,Worker),
+ SecureContext
+] interface StorageBucketManager {
+ Promise<StorageBucket> open(DOMString name,
+ optional StorageBucketOptions options = {});
+ Promise<sequence<DOMString>> keys();
+ Promise<undefined> delete(DOMString name);
+};
+
+dictionary StorageBucketOptions {
+ boolean persisted = false;
+ StorageBucketDurability durability = "relaxed";
+ unsigned long long? quota = null;
+ DOMTimeStamp? expires = null;
+};
+
+enum StorageBucketDurability {
+ "strict",
+ "relaxed"
+};
+
+[
+ Exposed=(Window,Worker),
+ SecureContext
+] interface StorageBucket {
+ [Exposed=Window] Promise<boolean> persist();
+ Promise<boolean> persisted();
+
+ Promise<StorageEstimate> estimate();
+
+ Promise<StorageBucketDurability> durability();
+
+ Promise<undefined> setExpires(DOMTimeStamp expires);
+ Promise<DOMTimeStamp> expires();
+};
diff --git a/test/wpt/tests/interfaces/storage.idl b/test/wpt/tests/interfaces/storage.idl
new file mode 100644
index 0000000..d47e37c
--- /dev/null
+++ b/test/wpt/tests/interfaces/storage.idl
@@ -0,0 +1,25 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Storage Standard (https://storage.spec.whatwg.org/)
+
+[SecureContext]
+interface mixin NavigatorStorage {
+ [SameObject] readonly attribute StorageManager storage;
+};
+Navigator includes NavigatorStorage;
+WorkerNavigator includes NavigatorStorage;
+
+[SecureContext,
+ Exposed=(Window,Worker)]
+interface StorageManager {
+ Promise<boolean> persisted();
+ [Exposed=Window] Promise<boolean> persist();
+
+ Promise<StorageEstimate> estimate();
+};
+
+dictionary StorageEstimate {
+ unsigned long long usage;
+ unsigned long long quota;
+};
diff --git a/test/wpt/tests/interfaces/streams.idl b/test/wpt/tests/interfaces/streams.idl
new file mode 100644
index 0000000..fd5420f
--- /dev/null
+++ b/test/wpt/tests/interfaces/streams.idl
@@ -0,0 +1,222 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Streams Standard (https://streams.spec.whatwg.org/)
+
+[Exposed=*, Transferable]
+interface ReadableStream {
+ constructor(optional object underlyingSource, optional QueuingStrategy strategy = {});
+
+ readonly attribute boolean locked;
+
+ Promise<undefined> cancel(optional any reason);
+ ReadableStreamReader getReader(optional ReadableStreamGetReaderOptions options = {});
+ ReadableStream pipeThrough(ReadableWritablePair transform, optional StreamPipeOptions options = {});
+ Promise<undefined> pipeTo(WritableStream destination, optional StreamPipeOptions options = {});
+ sequence<ReadableStream> tee();
+
+ async iterable<any>(optional ReadableStreamIteratorOptions options = {});
+};
+
+typedef (ReadableStreamDefaultReader or ReadableStreamBYOBReader) ReadableStreamReader;
+
+enum ReadableStreamReaderMode { "byob" };
+
+dictionary ReadableStreamGetReaderOptions {
+ ReadableStreamReaderMode mode;
+};
+
+dictionary ReadableStreamIteratorOptions {
+ boolean preventCancel = false;
+};
+
+dictionary ReadableWritablePair {
+ required ReadableStream readable;
+ required WritableStream writable;
+};
+
+dictionary StreamPipeOptions {
+ boolean preventClose = false;
+ boolean preventAbort = false;
+ boolean preventCancel = false;
+ AbortSignal signal;
+};
+
+dictionary UnderlyingSource {
+ UnderlyingSourceStartCallback start;
+ UnderlyingSourcePullCallback pull;
+ UnderlyingSourceCancelCallback cancel;
+ ReadableStreamType type;
+ [EnforceRange] unsigned long long autoAllocateChunkSize;
+};
+
+typedef (ReadableStreamDefaultController or ReadableByteStreamController) ReadableStreamController;
+
+callback UnderlyingSourceStartCallback = any (ReadableStreamController controller);
+callback UnderlyingSourcePullCallback = Promise<undefined> (ReadableStreamController controller);
+callback UnderlyingSourceCancelCallback = Promise<undefined> (optional any reason);
+
+enum ReadableStreamType { "bytes" };
+
+interface mixin ReadableStreamGenericReader {
+ readonly attribute Promise<undefined> closed;
+
+ Promise<undefined> cancel(optional any reason);
+};
+
+[Exposed=*]
+interface ReadableStreamDefaultReader {
+ constructor(ReadableStream stream);
+
+ Promise<ReadableStreamReadResult> read();
+ undefined releaseLock();
+};
+ReadableStreamDefaultReader includes ReadableStreamGenericReader;
+
+dictionary ReadableStreamReadResult {
+ any value;
+ boolean done;
+};
+
+[Exposed=*]
+interface ReadableStreamBYOBReader {
+ constructor(ReadableStream stream);
+
+ Promise<ReadableStreamReadResult> read(ArrayBufferView view);
+ undefined releaseLock();
+};
+ReadableStreamBYOBReader includes ReadableStreamGenericReader;
+
+[Exposed=*]
+interface ReadableStreamDefaultController {
+ readonly attribute unrestricted double? desiredSize;
+
+ undefined close();
+ undefined enqueue(optional any chunk);
+ undefined error(optional any e);
+};
+
+[Exposed=*]
+interface ReadableByteStreamController {
+ readonly attribute ReadableStreamBYOBRequest? byobRequest;
+ readonly attribute unrestricted double? desiredSize;
+
+ undefined close();
+ undefined enqueue(ArrayBufferView chunk);
+ undefined error(optional any e);
+};
+
+[Exposed=*]
+interface ReadableStreamBYOBRequest {
+ readonly attribute ArrayBufferView? view;
+
+ undefined respond([EnforceRange] unsigned long long bytesWritten);
+ undefined respondWithNewView(ArrayBufferView view);
+};
+
+[Exposed=*, Transferable]
+interface WritableStream {
+ constructor(optional object underlyingSink, optional QueuingStrategy strategy = {});
+
+ readonly attribute boolean locked;
+
+ Promise<undefined> abort(optional any reason);
+ Promise<undefined> close();
+ WritableStreamDefaultWriter getWriter();
+};
+
+dictionary UnderlyingSink {
+ UnderlyingSinkStartCallback start;
+ UnderlyingSinkWriteCallback write;
+ UnderlyingSinkCloseCallback close;
+ UnderlyingSinkAbortCallback abort;
+ any type;
+};
+
+callback UnderlyingSinkStartCallback = any (WritableStreamDefaultController controller);
+callback UnderlyingSinkWriteCallback = Promise<undefined> (any chunk, WritableStreamDefaultController controller);
+callback UnderlyingSinkCloseCallback = Promise<undefined> ();
+callback UnderlyingSinkAbortCallback = Promise<undefined> (optional any reason);
+
+[Exposed=*]
+interface WritableStreamDefaultWriter {
+ constructor(WritableStream stream);
+
+ readonly attribute Promise<undefined> closed;
+ readonly attribute unrestricted double? desiredSize;
+ readonly attribute Promise<undefined> ready;
+
+ Promise<undefined> abort(optional any reason);
+ Promise<undefined> close();
+ undefined releaseLock();
+ Promise<undefined> write(optional any chunk);
+};
+
+[Exposed=*]
+interface WritableStreamDefaultController {
+ readonly attribute AbortSignal signal;
+ undefined error(optional any e);
+};
+
+[Exposed=*, Transferable]
+interface TransformStream {
+ constructor(optional object transformer,
+ optional QueuingStrategy writableStrategy = {},
+ optional QueuingStrategy readableStrategy = {});
+
+ readonly attribute ReadableStream readable;
+ readonly attribute WritableStream writable;
+};
+
+dictionary Transformer {
+ TransformerStartCallback start;
+ TransformerTransformCallback transform;
+ TransformerFlushCallback flush;
+ any readableType;
+ any writableType;
+};
+
+callback TransformerStartCallback = any (TransformStreamDefaultController controller);
+callback TransformerFlushCallback = Promise<undefined> (TransformStreamDefaultController controller);
+callback TransformerTransformCallback = Promise<undefined> (any chunk, TransformStreamDefaultController controller);
+
+[Exposed=*]
+interface TransformStreamDefaultController {
+ readonly attribute unrestricted double? desiredSize;
+
+ undefined enqueue(optional any chunk);
+ undefined error(optional any reason);
+ undefined terminate();
+};
+
+dictionary QueuingStrategy {
+ unrestricted double highWaterMark;
+ QueuingStrategySize size;
+};
+
+callback QueuingStrategySize = unrestricted double (any chunk);
+
+dictionary QueuingStrategyInit {
+ required unrestricted double highWaterMark;
+};
+
+[Exposed=*]
+interface ByteLengthQueuingStrategy {
+ constructor(QueuingStrategyInit init);
+
+ readonly attribute unrestricted double highWaterMark;
+ readonly attribute Function size;
+};
+
+[Exposed=*]
+interface CountQueuingStrategy {
+ constructor(QueuingStrategyInit init);
+
+ readonly attribute unrestricted double highWaterMark;
+ readonly attribute Function size;
+};
+
+interface mixin GenericTransformStream {
+ readonly attribute ReadableStream readable;
+ readonly attribute WritableStream writable;
+};
diff --git a/test/wpt/tests/interfaces/sub-apps.tentative.idl b/test/wpt/tests/interfaces/sub-apps.tentative.idl
new file mode 100644
index 0000000..39dcd97
--- /dev/null
+++ b/test/wpt/tests/interfaces/sub-apps.tentative.idl
@@ -0,0 +1,17 @@
+[
+ Exposed=Window,
+ SecureContext,
+ ImplementedAs=SubApps
+] partial interface Navigator {
+ [SameObject, RuntimeEnabled=DesktopPWAsSubApps] readonly attribute SubApps subApps;
+};
+
+[
+ Exposed=Window,
+ SecureContext,
+ RuntimeEnabled=DesktopPWAsSubApps
+] interface SubApps {
+ [CallWith=ScriptState, RaisesException] Promise<undefined> add(DOMString install_url);
+ [CallWith=ScriptState, RaisesException] Promise<FrozenArray<DOMString>> list();
+ [CallWith=ScriptState, RaisesException] Promise<undefined> remove(DOMString app_id);
+};
diff --git a/test/wpt/tests/interfaces/svg-animations.idl b/test/wpt/tests/interfaces/svg-animations.idl
new file mode 100644
index 0000000..b57e1b9
--- /dev/null
+++ b/test/wpt/tests/interfaces/svg-animations.idl
@@ -0,0 +1,68 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: SVG Animations (https://svgwg.org/specs/animations/)
+
+[Exposed=Window]
+interface TimeEvent : Event {
+
+ readonly attribute WindowProxy? view;
+ readonly attribute long detail;
+
+ undefined initTimeEvent(DOMString typeArg, Window? viewArg, long detailArg);
+};
+
+[Exposed=Window]
+interface SVGAnimationElement : SVGElement {
+
+ readonly attribute SVGElement? targetElement;
+
+ attribute EventHandler onbegin;
+ attribute EventHandler onend;
+ attribute EventHandler onrepeat;
+
+ float getStartTime();
+ float getCurrentTime();
+ float getSimpleDuration();
+
+ undefined beginElement();
+ undefined beginElementAt(float offset);
+ undefined endElement();
+ undefined endElementAt(float offset);
+};
+
+SVGAnimationElement includes SVGTests;
+
+[Exposed=Window]
+interface SVGAnimateElement : SVGAnimationElement {
+};
+
+[Exposed=Window]
+interface SVGSetElement : SVGAnimationElement {
+};
+
+[Exposed=Window]
+interface SVGAnimateMotionElement : SVGAnimationElement {
+};
+
+[Exposed=Window]
+interface SVGMPathElement : SVGElement {
+};
+
+SVGMPathElement includes SVGURIReference;
+
+[Exposed=Window]
+interface SVGAnimateTransformElement : SVGAnimationElement {
+};
+
+[Exposed=Window]
+interface SVGDiscardElement : SVGAnimationElement {
+};
+
+partial interface SVGSVGElement {
+ undefined pauseAnimations();
+ undefined unpauseAnimations();
+ boolean animationsPaused();
+ float getCurrentTime();
+ undefined setCurrentTime(float seconds);
+};
diff --git a/test/wpt/tests/interfaces/testutils.idl b/test/wpt/tests/interfaces/testutils.idl
new file mode 100644
index 0000000..c5b7efd
--- /dev/null
+++ b/test/wpt/tests/interfaces/testutils.idl
@@ -0,0 +1,9 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Test Utils Standard (https://testutils.spec.whatwg.org/)
+
+[Exposed=(Window,Worker)]
+namespace TestUtils {
+ [NewObject] Promise<undefined> gc();
+};
diff --git a/test/wpt/tests/interfaces/text-detection-api.idl b/test/wpt/tests/interfaces/text-detection-api.idl
new file mode 100644
index 0000000..95b6427
--- /dev/null
+++ b/test/wpt/tests/interfaces/text-detection-api.idl
@@ -0,0 +1,18 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Accelerated Text Detection in Images (https://wicg.github.io/shape-detection-api/text.html)
+
+[
+ Exposed=(Window,Worker),
+ SecureContext
+] interface TextDetector {
+ constructor();
+ Promise<sequence<DetectedText>> detect(ImageBitmapSource image);
+};
+
+dictionary DetectedText {
+ required DOMRectReadOnly boundingBox;
+ required DOMString rawValue;
+ required FrozenArray<Point2D> cornerPoints;
+};
diff --git a/test/wpt/tests/interfaces/touch-events.idl b/test/wpt/tests/interfaces/touch-events.idl
new file mode 100644
index 0000000..9844f08
--- /dev/null
+++ b/test/wpt/tests/interfaces/touch-events.idl
@@ -0,0 +1,79 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Touch Events - Level 2 (https://w3c.github.io/touch-events/)
+
+enum TouchType {
+ "direct",
+ "stylus"
+};
+
+dictionary TouchInit {
+ required long identifier;
+ required EventTarget target;
+ double clientX = 0;
+ double clientY = 0;
+ double screenX = 0;
+ double screenY = 0;
+ double pageX = 0;
+ double pageY = 0;
+ float radiusX = 0;
+ float radiusY = 0;
+ float rotationAngle = 0;
+ float force = 0;
+ double altitudeAngle = 0;
+ double azimuthAngle = 0;
+ TouchType touchType = "direct";
+};
+
+[Exposed=Window]
+interface Touch {
+ constructor(TouchInit touchInitDict);
+ readonly attribute long identifier;
+ readonly attribute EventTarget target;
+ readonly attribute double screenX;
+ readonly attribute double screenY;
+ readonly attribute double clientX;
+ readonly attribute double clientY;
+ readonly attribute double pageX;
+ readonly attribute double pageY;
+ readonly attribute float radiusX;
+ readonly attribute float radiusY;
+ readonly attribute float rotationAngle;
+ readonly attribute float force;
+ readonly attribute float altitudeAngle;
+ readonly attribute float azimuthAngle;
+ readonly attribute TouchType touchType;
+};
+
+[Exposed=Window]
+interface TouchList {
+ readonly attribute unsigned long length;
+ getter Touch? item (unsigned long index);
+};
+
+dictionary TouchEventInit : EventModifierInit {
+ sequence<Touch> touches = [];
+ sequence<Touch> targetTouches = [];
+ sequence<Touch> changedTouches = [];
+};
+
+[Exposed=Window]
+interface TouchEvent : UIEvent {
+ constructor(DOMString type, optional TouchEventInit eventInitDict = {});
+ readonly attribute TouchList touches;
+ readonly attribute TouchList targetTouches;
+ readonly attribute TouchList changedTouches;
+ readonly attribute boolean altKey;
+ readonly attribute boolean metaKey;
+ readonly attribute boolean ctrlKey;
+ readonly attribute boolean shiftKey;
+ getter boolean getModifierState (DOMString keyArg);
+};
+
+partial interface mixin GlobalEventHandlers {
+ attribute EventHandler ontouchstart;
+ attribute EventHandler ontouchend;
+ attribute EventHandler ontouchmove;
+ attribute EventHandler ontouchcancel;
+};
diff --git a/test/wpt/tests/interfaces/trust-token-api.idl b/test/wpt/tests/interfaces/trust-token-api.idl
new file mode 100644
index 0000000..f521ace
--- /dev/null
+++ b/test/wpt/tests/interfaces/trust-token-api.idl
@@ -0,0 +1,26 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Private State Token API (https://wicg.github.io/trust-token-api/)
+
+enum RefreshPolicy { "none", "refresh" };
+
+enum TokenVersion { "1" };
+
+enum OperationType { "token-request", "send-redemption-record", "token-redemption" };
+
+dictionary PrivateToken {
+ required TokenVersion version;
+ required OperationType operation;
+ RefreshPolicy refreshPolicy = "none";
+ sequence<USVString> issuers;
+};
+
+partial dictionary RequestInit {
+ PrivateToken privateToken;
+};
+
+partial interface Document {
+ Promise<boolean> hasPrivateTokens(USVString issuer);
+ Promise<boolean> hasRedemptionRecord(USVString issuer);
+};
diff --git a/test/wpt/tests/interfaces/trusted-types.idl b/test/wpt/tests/interfaces/trusted-types.idl
new file mode 100644
index 0000000..2356238
--- /dev/null
+++ b/test/wpt/tests/interfaces/trusted-types.idl
@@ -0,0 +1,71 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Trusted Types (https://w3c.github.io/trusted-types/dist/spec/)
+
+[Exposed=(Window,Worker)]
+interface TrustedHTML {
+ stringifier;
+ DOMString toJSON();
+ static TrustedHTML fromLiteral(object templateStringsArray);
+};
+
+[Exposed=(Window,Worker)]
+interface TrustedScript {
+ stringifier;
+ DOMString toJSON();
+ static TrustedScript fromLiteral(object templateStringsArray);
+};
+
+[Exposed=(Window,Worker)]
+interface TrustedScriptURL {
+ stringifier;
+ USVString toJSON();
+ static TrustedScriptURL fromLiteral(object templateStringsArray);
+};
+
+[Exposed=(Window,Worker)] interface TrustedTypePolicyFactory {
+ TrustedTypePolicy createPolicy(
+ DOMString policyName, optional TrustedTypePolicyOptions policyOptions = {});
+ boolean isHTML(any value);
+ boolean isScript(any value);
+ boolean isScriptURL(any value);
+ readonly attribute TrustedHTML emptyHTML;
+ readonly attribute TrustedScript emptyScript;
+ DOMString? getAttributeType(
+ DOMString tagName,
+ DOMString attribute,
+ optional DOMString elementNs = "",
+ optional DOMString attrNs = "");
+ DOMString? getPropertyType(
+ DOMString tagName,
+ DOMString property,
+ optional DOMString elementNs = "");
+ readonly attribute TrustedTypePolicy? defaultPolicy;
+};
+
+[Exposed=(Window,Worker)]
+interface TrustedTypePolicy {
+ readonly attribute DOMString name;
+ TrustedHTML createHTML(DOMString input, any... arguments);
+ TrustedScript createScript(DOMString input, any... arguments);
+ TrustedScriptURL createScriptURL(DOMString input, any... arguments);
+};
+
+dictionary TrustedTypePolicyOptions {
+ CreateHTMLCallback? createHTML;
+ CreateScriptCallback? createScript;
+ CreateScriptURLCallback? createScriptURL;
+};
+callback CreateHTMLCallback = DOMString (DOMString input, any... arguments);
+callback CreateScriptCallback = DOMString (DOMString input, any... arguments);
+callback CreateScriptURLCallback = USVString (DOMString input, any... arguments);
+
+typedef [StringContext=TrustedHTML] DOMString HTMLString;
+typedef [StringContext=TrustedScript] DOMString ScriptString;
+typedef [StringContext=TrustedScriptURL] USVString ScriptURLString;
+typedef (TrustedHTML or TrustedScript or TrustedScriptURL) TrustedType;
+
+partial interface mixin WindowOrWorkerGlobalScope {
+ readonly attribute TrustedTypePolicyFactory trustedTypes;
+};
diff --git a/test/wpt/tests/interfaces/turtledove.idl b/test/wpt/tests/interfaces/turtledove.idl
new file mode 100644
index 0000000..f5867e9
--- /dev/null
+++ b/test/wpt/tests/interfaces/turtledove.idl
@@ -0,0 +1,120 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Protected Audience (formerly FLEDGE) (https://wicg.github.io/turtledove/)
+
+[SecureContext]
+partial interface Navigator {
+ Promise<undefined> joinAdInterestGroup(AuctionAdInterestGroup group, double durationSeconds);
+};
+
+dictionary AuctionAd {
+ required USVString renderURL;
+ any metadata;
+};
+
+dictionary AuctionAdInterestGroup {
+ required USVString owner;
+ required USVString name;
+
+ double priority = 0.0;
+ boolean enableBiddingSignalsPrioritization = false;
+ record<DOMString, double> priorityVector;
+ record<DOMString, double> prioritySignalsOverrides;
+
+ DOMString executionMode = "compatibility";
+ USVString biddingLogicURL;
+ USVString biddingWasmHelperURL;
+ USVString updateURL;
+ USVString trustedBiddingSignalsURL;
+ sequence<USVString> trustedBiddingSignalsKeys;
+ any userBiddingSignals;
+ sequence<AuctionAd> ads;
+ sequence<AuctionAd> adComponents;
+};
+
+[SecureContext]
+partial interface Navigator {
+ Promise<undefined> leaveAdInterestGroup(AuctionAdInterestGroupKey group);
+};
+
+dictionary AuctionAdInterestGroupKey {
+ required USVString owner;
+ required USVString name;
+};
+
+[SecureContext]
+partial interface Navigator {
+ Promise<USVString?> runAdAuction(AuctionAdConfig config);
+};
+
+dictionary AuctionAdConfig {
+ required USVString seller;
+ required USVString decisionLogicURL;
+ USVString trustedScoringSignalsURL;
+ sequence<USVString> interestGroupBuyers;
+ any auctionSignals;
+ any sellerSignals;
+ USVString directFromSellerSignals;
+ unsigned long long sellerTimeout;
+ unsigned short sellerExperimentGroupId;
+ record<USVString, any> perBuyerSignals;
+ record<USVString, unsigned long long> perBuyerTimeouts;
+ record<USVString, unsigned short> perBuyerGroupLimits;
+ record<USVString, unsigned short> perBuyerExperimentGroupIds;
+ record<USVString, record<USVString, double>> perBuyerPrioritySignals;
+ sequence<AuctionAdConfig> componentAuctions = [];
+ AbortSignal? signal;
+};
+
+[Exposed=InterestGroupScriptRunnerGlobalScope]
+interface InterestGroupScriptRunnerGlobalScope {
+};
+
+[Exposed=InterestGroupBiddingScriptRunnerGlobalScope,
+ Global=(InterestGroupScriptRunnerGlobalScope,
+ InterestGroupBiddingScriptRunnerGlobalScope)]
+interface InterestGroupBiddingScriptRunnerGlobalScope
+ : InterestGroupScriptRunnerGlobalScope {
+ boolean setBid();
+ boolean setBid(GenerateBidOutput generateBidOutput);
+ undefined setPriority(double priority);
+ undefined setPrioritySignalsOverride(DOMString key, double priority);
+};
+
+[Exposed=InterestGroupScoringScriptRunnerGlobalScope,
+ Global=(InterestGroupScriptRunnerGlobalScope,
+ InterestGroupScoringScriptRunnerGlobalScope)]
+interface InterestGroupScoringScriptRunnerGlobalScope
+ : InterestGroupScriptRunnerGlobalScope {
+};
+
+[Exposed=InterestGroupReportingScriptRunnerGlobalScope,
+ Global=(InterestGroupScriptRunnerGlobalScope,
+ InterestGroupReportingScriptRunnerGlobalScope)]
+interface InterestGroupReportingScriptRunnerGlobalScope
+ : InterestGroupScriptRunnerGlobalScope {
+ undefined sendReportTo(DOMString url);
+ undefined registerAdBeacon(record<DOMString, USVString> map);
+};
+
+dictionary AdRender {
+ required DOMString url;
+ required DOMString width;
+ required DOMString height;
+};
+
+dictionary GenerateBidOutput {
+ required double bid;
+ required (DOMString or AdRender) adRender;
+ any ad;
+ sequence<(DOMString or AdRender)> adComponents;
+ double adCost;
+ double modelingSignals;
+ boolean allowComponentAuction = false;
+};
+
+[SecureContext]
+partial interface Navigator {
+ undefined updateAdInterestGroups();
+};
diff --git a/test/wpt/tests/interfaces/ua-client-hints.idl b/test/wpt/tests/interfaces/ua-client-hints.idl
new file mode 100644
index 0000000..c69714b
--- /dev/null
+++ b/test/wpt/tests/interfaces/ua-client-hints.idl
@@ -0,0 +1,45 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: User-Agent Client Hints (https://wicg.github.io/ua-client-hints/)
+
+dictionary NavigatorUABrandVersion {
+ DOMString brand;
+ DOMString version;
+};
+
+dictionary UADataValues {
+ DOMString architecture;
+ DOMString bitness;
+ sequence<NavigatorUABrandVersion> brands;
+ DOMString formFactor;
+ sequence<NavigatorUABrandVersion> fullVersionList;
+ DOMString model;
+ boolean mobile;
+ DOMString platform;
+ DOMString platformVersion;
+ DOMString uaFullVersion; // deprecated in favor of fullVersionList
+ boolean wow64;
+};
+
+dictionary UALowEntropyJSON {
+ sequence<NavigatorUABrandVersion> brands;
+ boolean mobile;
+ DOMString platform;
+};
+
+[Exposed=(Window,Worker)]
+interface NavigatorUAData {
+ readonly attribute FrozenArray<NavigatorUABrandVersion> brands;
+ readonly attribute boolean mobile;
+ readonly attribute DOMString platform;
+ Promise<UADataValues> getHighEntropyValues(sequence<DOMString> hints);
+ UALowEntropyJSON toJSON();
+};
+
+interface mixin NavigatorUA {
+ [SecureContext] readonly attribute NavigatorUAData userAgentData;
+};
+
+Navigator includes NavigatorUA;
+WorkerNavigator includes NavigatorUA;
diff --git a/test/wpt/tests/interfaces/uievents.idl b/test/wpt/tests/interfaces/uievents.idl
new file mode 100644
index 0000000..5fdc812
--- /dev/null
+++ b/test/wpt/tests/interfaces/uievents.idl
@@ -0,0 +1,248 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: UI Events (https://w3c.github.io/uievents/)
+
+[Exposed=Window]
+interface UIEvent : Event {
+ constructor(DOMString type, optional UIEventInit eventInitDict = {});
+ readonly attribute Window? view;
+ readonly attribute long detail;
+};
+
+dictionary UIEventInit : EventInit {
+ Window? view = null;
+ long detail = 0;
+};
+
+[Exposed=Window]
+interface FocusEvent : UIEvent {
+ constructor(DOMString type, optional FocusEventInit eventInitDict = {});
+ readonly attribute EventTarget? relatedTarget;
+};
+
+dictionary FocusEventInit : UIEventInit {
+ EventTarget? relatedTarget = null;
+};
+
+[Exposed=Window]
+interface MouseEvent : UIEvent {
+ constructor(DOMString type, optional MouseEventInit eventInitDict = {});
+ readonly attribute long screenX;
+ readonly attribute long screenY;
+ readonly attribute long clientX;
+ readonly attribute long clientY;
+
+ readonly attribute boolean ctrlKey;
+ readonly attribute boolean shiftKey;
+ readonly attribute boolean altKey;
+ readonly attribute boolean metaKey;
+
+ readonly attribute short button;
+ readonly attribute unsigned short buttons;
+
+ readonly attribute EventTarget? relatedTarget;
+
+ boolean getModifierState(DOMString keyArg);
+};
+
+dictionary MouseEventInit : EventModifierInit {
+ long screenX = 0;
+ long screenY = 0;
+ long clientX = 0;
+ long clientY = 0;
+
+ short button = 0;
+ unsigned short buttons = 0;
+ EventTarget? relatedTarget = null;
+};
+
+dictionary EventModifierInit : UIEventInit {
+ boolean ctrlKey = false;
+ boolean shiftKey = false;
+ boolean altKey = false;
+ boolean metaKey = false;
+
+ boolean modifierAltGraph = false;
+ boolean modifierCapsLock = false;
+ boolean modifierFn = false;
+ boolean modifierFnLock = false;
+ boolean modifierHyper = false;
+ boolean modifierNumLock = false;
+ boolean modifierScrollLock = false;
+ boolean modifierSuper = false;
+ boolean modifierSymbol = false;
+ boolean modifierSymbolLock = false;
+};
+
+[Exposed=Window]
+interface WheelEvent : MouseEvent {
+ constructor(DOMString type, optional WheelEventInit eventInitDict = {});
+ // DeltaModeCode
+ const unsigned long DOM_DELTA_PIXEL = 0x00;
+ const unsigned long DOM_DELTA_LINE = 0x01;
+ const unsigned long DOM_DELTA_PAGE = 0x02;
+
+ readonly attribute double deltaX;
+ readonly attribute double deltaY;
+ readonly attribute double deltaZ;
+ readonly attribute unsigned long deltaMode;
+};
+
+dictionary WheelEventInit : MouseEventInit {
+ double deltaX = 0.0;
+ double deltaY = 0.0;
+ double deltaZ = 0.0;
+ unsigned long deltaMode = 0;
+};
+
+[Exposed=Window]
+interface InputEvent : UIEvent {
+ constructor(DOMString type, optional InputEventInit eventInitDict = {});
+ readonly attribute DOMString? data;
+ readonly attribute boolean isComposing;
+ readonly attribute DOMString inputType;
+};
+
+dictionary InputEventInit : UIEventInit {
+ DOMString? data = null;
+ boolean isComposing = false;
+ DOMString inputType = "";
+};
+
+[Exposed=Window]
+interface KeyboardEvent : UIEvent {
+ constructor(DOMString type, optional KeyboardEventInit eventInitDict = {});
+ // KeyLocationCode
+ const unsigned long DOM_KEY_LOCATION_STANDARD = 0x00;
+ const unsigned long DOM_KEY_LOCATION_LEFT = 0x01;
+ const unsigned long DOM_KEY_LOCATION_RIGHT = 0x02;
+ const unsigned long DOM_KEY_LOCATION_NUMPAD = 0x03;
+
+ readonly attribute DOMString key;
+ readonly attribute DOMString code;
+ readonly attribute unsigned long location;
+
+ readonly attribute boolean ctrlKey;
+ readonly attribute boolean shiftKey;
+ readonly attribute boolean altKey;
+ readonly attribute boolean metaKey;
+
+ readonly attribute boolean repeat;
+ readonly attribute boolean isComposing;
+
+ boolean getModifierState(DOMString keyArg);
+};
+
+dictionary KeyboardEventInit : EventModifierInit {
+ DOMString key = "";
+ DOMString code = "";
+ unsigned long location = 0;
+ boolean repeat = false;
+ boolean isComposing = false;
+};
+
+[Exposed=Window]
+interface CompositionEvent : UIEvent {
+ constructor(DOMString type, optional CompositionEventInit eventInitDict = {});
+ readonly attribute DOMString data;
+};
+
+dictionary CompositionEventInit : UIEventInit {
+ DOMString data = "";
+};
+
+partial interface UIEvent {
+ // Deprecated in this specification
+ undefined initUIEvent(DOMString typeArg,
+ optional boolean bubblesArg = false,
+ optional boolean cancelableArg = false,
+ optional Window? viewArg = null,
+ optional long detailArg = 0);
+};
+
+partial interface MouseEvent {
+ // Deprecated in this specification
+ undefined initMouseEvent(DOMString typeArg,
+ optional boolean bubblesArg = false,
+ optional boolean cancelableArg = false,
+ optional Window? viewArg = null,
+ optional long detailArg = 0,
+ optional long screenXArg = 0,
+ optional long screenYArg = 0,
+ optional long clientXArg = 0,
+ optional long clientYArg = 0,
+ optional boolean ctrlKeyArg = false,
+ optional boolean altKeyArg = false,
+ optional boolean shiftKeyArg = false,
+ optional boolean metaKeyArg = false,
+ optional short buttonArg = 0,
+ optional EventTarget? relatedTargetArg = null);
+};
+
+partial interface KeyboardEvent {
+ // Originally introduced (and deprecated) in this specification
+ undefined initKeyboardEvent(DOMString typeArg,
+ optional boolean bubblesArg = false,
+ optional boolean cancelableArg = false,
+ optional Window? viewArg = null,
+ optional DOMString keyArg = "",
+ optional unsigned long locationArg = 0,
+ optional boolean ctrlKey = false,
+ optional boolean altKey = false,
+ optional boolean shiftKey = false,
+ optional boolean metaKey = false);
+};
+
+partial interface CompositionEvent {
+ // Originally introduced (and deprecated) in this specification
+ undefined initCompositionEvent(DOMString typeArg,
+ optional boolean bubblesArg = false,
+ optional boolean cancelableArg = false,
+ optional WindowProxy? viewArg = null,
+ optional DOMString dataArg = "");
+};
+
+partial interface UIEvent {
+ // The following support legacy user agents
+ readonly attribute unsigned long which;
+};
+
+partial dictionary UIEventInit {
+ unsigned long which = 0;
+};
+
+partial interface KeyboardEvent {
+ // The following support legacy user agents
+ readonly attribute unsigned long charCode;
+ readonly attribute unsigned long keyCode;
+};
+
+partial dictionary KeyboardEventInit {
+ // The following support legacy user agents
+ unsigned long charCode = 0;
+ unsigned long keyCode = 0;
+};
+
+[Exposed=Window]
+interface MutationEvent : Event {
+ // attrChangeType
+ const unsigned short MODIFICATION = 1;
+ const unsigned short ADDITION = 2;
+ const unsigned short REMOVAL = 3;
+
+ readonly attribute Node? relatedNode;
+ readonly attribute DOMString prevValue;
+ readonly attribute DOMString newValue;
+ readonly attribute DOMString attrName;
+ readonly attribute unsigned short attrChange;
+
+ undefined initMutationEvent(DOMString typeArg,
+ optional boolean bubblesArg = false,
+ optional boolean cancelableArg = false,
+ optional Node? relatedNodeArg = null,
+ optional DOMString prevValueArg = "",
+ optional DOMString newValueArg = "",
+ optional DOMString attrNameArg = "",
+ optional unsigned short attrChangeArg = 0);
+};
diff --git a/test/wpt/tests/interfaces/url.idl b/test/wpt/tests/interfaces/url.idl
new file mode 100644
index 0000000..a5e4d1e
--- /dev/null
+++ b/test/wpt/tests/interfaces/url.idl
@@ -0,0 +1,46 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: URL Standard (https://url.spec.whatwg.org/)
+
+[Exposed=*,
+ LegacyWindowAlias=webkitURL]
+interface URL {
+ constructor(USVString url, optional USVString base);
+
+ static boolean canParse(USVString url, optional USVString base);
+
+ stringifier attribute USVString href;
+ readonly attribute USVString origin;
+ attribute USVString protocol;
+ attribute USVString username;
+ attribute USVString password;
+ attribute USVString host;
+ attribute USVString hostname;
+ attribute USVString port;
+ attribute USVString pathname;
+ attribute USVString search;
+ [SameObject] readonly attribute URLSearchParams searchParams;
+ attribute USVString hash;
+
+ USVString toJSON();
+};
+
+[Exposed=*]
+interface URLSearchParams {
+ constructor(optional (sequence<sequence<USVString>> or record<USVString, USVString> or USVString) init = "");
+
+ readonly attribute unsigned long size;
+
+ undefined append(USVString name, USVString value);
+ undefined delete(USVString name, optional USVString value);
+ USVString? get(USVString name);
+ sequence<USVString> getAll(USVString name);
+ boolean has(USVString name, optional USVString value);
+ undefined set(USVString name, USVString value);
+
+ undefined sort();
+
+ iterable<USVString, USVString>;
+ stringifier;
+};
diff --git a/test/wpt/tests/interfaces/urlpattern.idl b/test/wpt/tests/interfaces/urlpattern.idl
new file mode 100644
index 0000000..e342eb5
--- /dev/null
+++ b/test/wpt/tests/interfaces/urlpattern.idl
@@ -0,0 +1,59 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: URLPattern API (https://wicg.github.io/urlpattern/)
+
+typedef (USVString or URLPatternInit) URLPatternInput;
+
+[Exposed=(Window,Worker)]
+interface URLPattern {
+ constructor(URLPatternInput input, USVString baseURL, optional URLPatternOptions options = {});
+ constructor(optional URLPatternInput input = {}, optional URLPatternOptions options = {});
+
+ boolean test(optional URLPatternInput input = {}, optional USVString baseURL);
+
+ URLPatternResult? exec(optional URLPatternInput input = {}, optional USVString baseURL);
+
+ readonly attribute USVString protocol;
+ readonly attribute USVString username;
+ readonly attribute USVString password;
+ readonly attribute USVString hostname;
+ readonly attribute USVString port;
+ readonly attribute USVString pathname;
+ readonly attribute USVString search;
+ readonly attribute USVString hash;
+};
+
+dictionary URLPatternInit {
+ USVString protocol;
+ USVString username;
+ USVString password;
+ USVString hostname;
+ USVString port;
+ USVString pathname;
+ USVString search;
+ USVString hash;
+ USVString baseURL;
+};
+
+dictionary URLPatternOptions {
+ boolean ignoreCase = false;
+};
+
+dictionary URLPatternResult {
+ sequence<URLPatternInput> inputs;
+
+ URLPatternComponentResult protocol;
+ URLPatternComponentResult username;
+ URLPatternComponentResult password;
+ URLPatternComponentResult hostname;
+ URLPatternComponentResult port;
+ URLPatternComponentResult pathname;
+ URLPatternComponentResult search;
+ URLPatternComponentResult hash;
+};
+
+dictionary URLPatternComponentResult {
+ USVString input;
+ record<USVString, (USVString or undefined)> groups;
+};
diff --git a/test/wpt/tests/interfaces/user-timing.idl b/test/wpt/tests/interfaces/user-timing.idl
new file mode 100644
index 0000000..28ee8aa
--- /dev/null
+++ b/test/wpt/tests/interfaces/user-timing.idl
@@ -0,0 +1,34 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: User Timing Level 3 (https://w3c.github.io/user-timing/)
+
+dictionary PerformanceMarkOptions {
+ any detail;
+ DOMHighResTimeStamp startTime;
+};
+
+dictionary PerformanceMeasureOptions {
+ any detail;
+ (DOMString or DOMHighResTimeStamp) start;
+ DOMHighResTimeStamp duration;
+ (DOMString or DOMHighResTimeStamp) end;
+};
+
+partial interface Performance {
+ PerformanceMark mark(DOMString markName, optional PerformanceMarkOptions markOptions = {});
+ undefined clearMarks(optional DOMString markName);
+ PerformanceMeasure measure(DOMString measureName, optional (DOMString or PerformanceMeasureOptions) startOrMeasureOptions = {}, optional DOMString endMark);
+ undefined clearMeasures(optional DOMString measureName);
+};
+
+[Exposed=(Window,Worker)]
+interface PerformanceMark : PerformanceEntry {
+ constructor(DOMString markName, optional PerformanceMarkOptions markOptions = {});
+ readonly attribute any detail;
+};
+
+[Exposed=(Window,Worker)]
+interface PerformanceMeasure : PerformanceEntry {
+ readonly attribute any detail;
+};
diff --git a/test/wpt/tests/interfaces/vibration.idl b/test/wpt/tests/interfaces/vibration.idl
new file mode 100644
index 0000000..22ab1c4
--- /dev/null
+++ b/test/wpt/tests/interfaces/vibration.idl
@@ -0,0 +1,10 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Vibration API (Second Edition) (https://w3c.github.io/vibration/)
+
+typedef (unsigned long or sequence<unsigned long>) VibratePattern;
+
+partial interface Navigator {
+ boolean vibrate (VibratePattern pattern);
+};
diff --git a/test/wpt/tests/interfaces/video-rvfc.idl b/test/wpt/tests/interfaces/video-rvfc.idl
new file mode 100644
index 0000000..adb4ef2
--- /dev/null
+++ b/test/wpt/tests/interfaces/video-rvfc.idl
@@ -0,0 +1,27 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: HTMLVideoElement.requestVideoFrameCallback() (https://wicg.github.io/video-rvfc/)
+
+dictionary VideoFrameCallbackMetadata {
+ required DOMHighResTimeStamp presentationTime;
+ required DOMHighResTimeStamp expectedDisplayTime;
+
+ required unsigned long width;
+ required unsigned long height;
+ required double mediaTime;
+
+ required unsigned long presentedFrames;
+ double processingDuration;
+
+ DOMHighResTimeStamp captureTime;
+ DOMHighResTimeStamp receiveTime;
+ unsigned long rtpTimestamp;
+};
+
+callback VideoFrameRequestCallback = undefined(DOMHighResTimeStamp now, VideoFrameCallbackMetadata metadata);
+
+partial interface HTMLVideoElement {
+ unsigned long requestVideoFrameCallback(VideoFrameRequestCallback callback);
+ undefined cancelVideoFrameCallback(unsigned long handle);
+};
diff --git a/test/wpt/tests/interfaces/virtual-keyboard.idl b/test/wpt/tests/interfaces/virtual-keyboard.idl
new file mode 100644
index 0000000..74dafc5
--- /dev/null
+++ b/test/wpt/tests/interfaces/virtual-keyboard.idl
@@ -0,0 +1,21 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: VirtualKeyboard API (https://w3c.github.io/virtual-keyboard/)
+
+partial interface Navigator {
+ [SecureContext, SameObject] readonly attribute VirtualKeyboard virtualKeyboard;
+};
+
+[Exposed=Window, SecureContext]
+interface VirtualKeyboard : EventTarget {
+ undefined show();
+ undefined hide();
+ readonly attribute DOMRect boundingRect;
+ attribute boolean overlaysContent;
+ attribute EventHandler ongeometrychange;
+};
+
+partial interface mixin ElementContentEditable {
+ [CEReactions] attribute DOMString virtualKeyboardPolicy;
+};
diff --git a/test/wpt/tests/interfaces/virtual-keyboard.tentative.idl b/test/wpt/tests/interfaces/virtual-keyboard.tentative.idl
new file mode 100644
index 0000000..2991d24
--- /dev/null
+++ b/test/wpt/tests/interfaces/virtual-keyboard.tentative.idl
@@ -0,0 +1,15 @@
+// Explainers:
+// https://github.com/MicrosoftEdge/MSEdgeExplainers/blob/main/VirtualKeyboardPolicy/explainer.md
+// https://github.com/MicrosoftEdge/MSEdgeExplainers/blob/main/VirtualKeyboardAPI/explainer.md
+
+partial interface Navigator {
+ [SecureContext, SameObject] readonly attribute VirtualKeyboard virtualKeyboard;
+};
+
+[SecureContext, Exposed=Window] interface VirtualKeyboard : EventTarget {
+ undefined show();
+ undefined hide();
+ readonly attribute DOMRect boundingRect;
+ attribute boolean overlaysContent;
+ attribute EventHandler ongeometrychange;
+};
diff --git a/test/wpt/tests/interfaces/wai-aria.idl b/test/wpt/tests/interfaces/wai-aria.idl
new file mode 100644
index 0000000..3434bf7
--- /dev/null
+++ b/test/wpt/tests/interfaces/wai-aria.idl
@@ -0,0 +1,59 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Accessible Rich Internet Applications (WAI-ARIA) 1.3 (https://w3c.github.io/aria/)
+
+interface mixin ARIAMixin {
+ [CEReactions] attribute DOMString? role;
+ [CEReactions] attribute Element? ariaActiveDescendantElement;
+ [CEReactions] attribute DOMString? ariaAtomic;
+ [CEReactions] attribute DOMString? ariaAutoComplete;
+ [CEReactions] attribute DOMString? ariaBusy;
+ [CEReactions] attribute DOMString? ariaChecked;
+ [CEReactions] attribute DOMString? ariaColCount;
+ [CEReactions] attribute DOMString? ariaColIndex;
+ [CEReactions] attribute DOMString? ariaColIndexText;
+ [CEReactions] attribute DOMString? ariaColSpan;
+ [CEReactions] attribute FrozenArray<Element>? ariaControlsElements;
+ [CEReactions] attribute DOMString? ariaCurrent;
+ [CEReactions] attribute FrozenArray<Element>? ariaDescribedByElements;
+ [CEReactions] attribute DOMString? ariaDescription;
+ [CEReactions] attribute FrozenArray<Element>? ariaDetailsElements;
+ [CEReactions] attribute DOMString? ariaDisabled;
+ [CEReactions] attribute FrozenArray<Element>? ariaErrorMessageElements;
+ [CEReactions] attribute DOMString? ariaExpanded;
+ [CEReactions] attribute FrozenArray<Element>? ariaFlowToElements;
+ [CEReactions] attribute DOMString? ariaHasPopup;
+ [CEReactions] attribute DOMString? ariaHidden;
+ [CEReactions] attribute DOMString? ariaInvalid;
+ [CEReactions] attribute DOMString? ariaKeyShortcuts;
+ [CEReactions] attribute DOMString? ariaLabel;
+ [CEReactions] attribute FrozenArray<Element>? ariaLabelledByElements;
+ [CEReactions] attribute DOMString? ariaLevel;
+ [CEReactions] attribute DOMString? ariaLive;
+ [CEReactions] attribute DOMString? ariaModal;
+ [CEReactions] attribute DOMString? ariaMultiLine;
+ [CEReactions] attribute DOMString? ariaMultiSelectable;
+ [CEReactions] attribute DOMString? ariaOrientation;
+ [CEReactions] attribute FrozenArray<Element>? ariaOwnsElements;
+ [CEReactions] attribute DOMString? ariaPlaceholder;
+ [CEReactions] attribute DOMString? ariaPosInSet;
+ [CEReactions] attribute DOMString? ariaPressed;
+ [CEReactions] attribute DOMString? ariaReadOnly;
+
+ [CEReactions] attribute DOMString? ariaRequired;
+ [CEReactions] attribute DOMString? ariaRoleDescription;
+ [CEReactions] attribute DOMString? ariaRowCount;
+ [CEReactions] attribute DOMString? ariaRowIndex;
+ [CEReactions] attribute DOMString? ariaRowIndexText;
+ [CEReactions] attribute DOMString? ariaRowSpan;
+ [CEReactions] attribute DOMString? ariaSelected;
+ [CEReactions] attribute DOMString? ariaSetSize;
+ [CEReactions] attribute DOMString? ariaSort;
+ [CEReactions] attribute DOMString? ariaValueMax;
+ [CEReactions] attribute DOMString? ariaValueMin;
+ [CEReactions] attribute DOMString? ariaValueNow;
+ [CEReactions] attribute DOMString? ariaValueText;
+};
+
+Element includes ARIAMixin;
diff --git a/test/wpt/tests/interfaces/wasm-js-api.idl b/test/wpt/tests/interfaces/wasm-js-api.idl
new file mode 100644
index 0000000..0d43842
--- /dev/null
+++ b/test/wpt/tests/interfaces/wasm-js-api.idl
@@ -0,0 +1,110 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: WebAssembly JavaScript Interface (https://webassembly.github.io/spec/js-api/)
+
+dictionary WebAssemblyInstantiatedSource {
+ required Module module;
+ required Instance instance;
+};
+
+[Exposed=*]
+namespace WebAssembly {
+ boolean validate(BufferSource bytes);
+ Promise<Module> compile(BufferSource bytes);
+
+ Promise<WebAssemblyInstantiatedSource> instantiate(
+ BufferSource bytes, optional object importObject);
+
+ Promise<Instance> instantiate(
+ Module moduleObject, optional object importObject);
+};
+
+enum ImportExportKind {
+ "function",
+ "table",
+ "memory",
+ "global"
+};
+
+dictionary ModuleExportDescriptor {
+ required USVString name;
+ required ImportExportKind kind;
+ // Note: Other fields such as signature may be added in the future.
+};
+
+dictionary ModuleImportDescriptor {
+ required USVString module;
+ required USVString name;
+ required ImportExportKind kind;
+};
+
+[LegacyNamespace=WebAssembly, Exposed=*]
+interface Module {
+ constructor(BufferSource bytes);
+ static sequence<ModuleExportDescriptor> exports(Module moduleObject);
+ static sequence<ModuleImportDescriptor> imports(Module moduleObject);
+ static sequence<ArrayBuffer> customSections(Module moduleObject, DOMString sectionName);
+};
+
+[LegacyNamespace=WebAssembly, Exposed=*]
+interface Instance {
+ constructor(Module module, optional object importObject);
+ readonly attribute object exports;
+};
+
+dictionary MemoryDescriptor {
+ required [EnforceRange] unsigned long initial;
+ [EnforceRange] unsigned long maximum;
+};
+
+[LegacyNamespace=WebAssembly, Exposed=*]
+interface Memory {
+ constructor(MemoryDescriptor descriptor);
+ unsigned long grow([EnforceRange] unsigned long delta);
+ readonly attribute ArrayBuffer buffer;
+};
+
+enum TableKind {
+ "externref",
+ "anyfunc",
+ // Note: More values may be added in future iterations,
+ // e.g., typed function references, typed GC references
+};
+
+dictionary TableDescriptor {
+ required TableKind element;
+ required [EnforceRange] unsigned long initial;
+ [EnforceRange] unsigned long maximum;
+};
+
+[LegacyNamespace=WebAssembly, Exposed=*]
+interface Table {
+ constructor(TableDescriptor descriptor, optional any value);
+ unsigned long grow([EnforceRange] unsigned long delta, optional any value);
+ any get([EnforceRange] unsigned long index);
+ undefined set([EnforceRange] unsigned long index, optional any value);
+ readonly attribute unsigned long length;
+};
+
+enum ValueType {
+ "i32",
+ "i64",
+ "f32",
+ "f64",
+ "v128",
+ "externref",
+ "anyfunc",
+};
+
+dictionary GlobalDescriptor {
+ required ValueType value;
+ boolean mutable = false;
+};
+
+[LegacyNamespace=WebAssembly, Exposed=*]
+interface Global {
+ constructor(GlobalDescriptor descriptor, optional any v);
+ any valueOf();
+ attribute any value;
+};
diff --git a/test/wpt/tests/interfaces/wasm-web-api.idl b/test/wpt/tests/interfaces/wasm-web-api.idl
new file mode 100644
index 0000000..088c8ee
--- /dev/null
+++ b/test/wpt/tests/interfaces/wasm-web-api.idl
@@ -0,0 +1,11 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: WebAssembly Web API (https://webassembly.github.io/spec/web-api/)
+
+[Exposed=(Window,Worker)]
+partial namespace WebAssembly {
+ Promise<Module> compileStreaming(Promise<Response> source);
+ Promise<WebAssemblyInstantiatedSource> instantiateStreaming(
+ Promise<Response> source, optional object importObject);
+};
diff --git a/test/wpt/tests/interfaces/web-animations-2.idl b/test/wpt/tests/interfaces/web-animations-2.idl
new file mode 100644
index 0000000..f9f68a0
--- /dev/null
+++ b/test/wpt/tests/interfaces/web-animations-2.idl
@@ -0,0 +1,112 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Web Animations Level 2 (https://drafts.csswg.org/web-animations-2/)
+
+[Exposed=Window]
+partial interface AnimationTimeline {
+ readonly attribute CSSNumberish? currentTime;
+ readonly attribute CSSNumberish? duration;
+ Animation play (optional AnimationEffect? effect = null);
+};
+
+[Exposed=Window]
+partial interface Animation {
+ attribute CSSNumberish? startTime;
+ attribute CSSNumberish? currentTime;
+};
+
+[Exposed=Window]
+partial interface AnimationEffect {
+ // Timing hierarchy
+ readonly attribute GroupEffect? parent;
+ readonly attribute AnimationEffect? previousSibling;
+ readonly attribute AnimationEffect? nextSibling;
+
+ undefined before (AnimationEffect... effects);
+ undefined after (AnimationEffect... effects);
+ undefined replace (AnimationEffect... effects);
+ undefined remove ();
+};
+
+partial dictionary EffectTiming {
+ double delay;
+ double endDelay;
+ double playbackRate = 1.0;
+ (unrestricted double or CSSNumericValue or DOMString) duration = "auto";
+};
+
+partial dictionary OptionalEffectTiming {
+ double playbackRate;
+};
+
+partial dictionary ComputedEffectTiming {
+ CSSNumberish startTime;
+ CSSNumberish endTime;
+ CSSNumberish activeDuration;
+ CSSNumberish? localTime;
+};
+
+[Exposed=Window]
+interface GroupEffect {
+ constructor(sequence<AnimationEffect>? children,
+ optional (unrestricted double or EffectTiming) timing = {});
+
+ readonly attribute AnimationNodeList children;
+ readonly attribute AnimationEffect? firstChild;
+ readonly attribute AnimationEffect? lastChild;
+ GroupEffect clone ();
+
+ undefined prepend (AnimationEffect... effects);
+ undefined append (AnimationEffect... effects);
+};
+
+[Exposed=Window]
+interface AnimationNodeList {
+ readonly attribute unsigned long length;
+ getter AnimationEffect? item (unsigned long index);
+};
+
+[Exposed=Window]
+interface SequenceEffect : GroupEffect {
+ constructor(sequence<AnimationEffect>? children,
+ optional (unrestricted double or EffectTiming) timing = {});
+
+ SequenceEffect clone ();
+};
+
+partial interface KeyframeEffect {
+ attribute IterationCompositeOperation iterationComposite;
+};
+
+partial dictionary KeyframeEffectOptions {
+ IterationCompositeOperation iterationComposite = "replace";
+};
+
+enum IterationCompositeOperation { "replace", "accumulate" };
+
+callback EffectCallback = undefined (double? progress,
+ (Element or CSSPseudoElement) currentTarget,
+ Animation animation);
+
+dictionary TimelineRangeOffset {
+ CSSOMString? rangeName;
+ CSSNumericValue offset;
+};
+
+partial dictionary KeyframeAnimationOptions {
+ (TimelineRangeOffset or CSSNumericValue or CSSKeywordValue or DOMString) rangeStart = "normal";
+ (TimelineRangeOffset or CSSNumericValue or CSSKeywordValue or DOMString) rangeEnd = "normal";
+};
+
+[Exposed=Window]
+interface AnimationPlaybackEvent : Event {
+ constructor(DOMString type, optional AnimationPlaybackEventInit
+ eventInitDict = {});
+ readonly attribute CSSNumberish? currentTime;
+ readonly attribute CSSNumberish? timelineTime;
+};
+dictionary AnimationPlaybackEventInit : EventInit {
+ CSSNumberish? currentTime = null;
+ CSSNumberish? timelineTime = null;
+};
diff --git a/test/wpt/tests/interfaces/web-animations.idl b/test/wpt/tests/interfaces/web-animations.idl
new file mode 100644
index 0000000..956d700
--- /dev/null
+++ b/test/wpt/tests/interfaces/web-animations.idl
@@ -0,0 +1,149 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Web Animations (https://drafts.csswg.org/web-animations-1/)
+
+[Exposed=Window]
+interface AnimationTimeline {
+};
+
+dictionary DocumentTimelineOptions {
+ DOMHighResTimeStamp originTime = 0;
+};
+
+[Exposed=Window]
+interface DocumentTimeline : AnimationTimeline {
+ constructor(optional DocumentTimelineOptions options = {});
+};
+
+[Exposed=Window]
+interface Animation : EventTarget {
+ constructor(optional AnimationEffect? effect = null,
+ optional AnimationTimeline? timeline);
+ attribute DOMString id;
+ attribute AnimationEffect? effect;
+ attribute AnimationTimeline? timeline;
+ attribute double playbackRate;
+ readonly attribute AnimationPlayState playState;
+ readonly attribute AnimationReplaceState replaceState;
+ readonly attribute boolean pending;
+ readonly attribute Promise<Animation> ready;
+ readonly attribute Promise<Animation> finished;
+ attribute EventHandler onfinish;
+ attribute EventHandler oncancel;
+ attribute EventHandler onremove;
+ undefined cancel();
+ undefined finish();
+ undefined play();
+ undefined pause();
+ undefined updatePlaybackRate(double playbackRate);
+ undefined reverse();
+ undefined persist();
+ [CEReactions]
+ undefined commitStyles();
+};
+
+enum AnimationPlayState { "idle", "running", "paused", "finished" };
+
+enum AnimationReplaceState { "active", "removed", "persisted" };
+
+[Exposed=Window]
+interface AnimationEffect {
+ EffectTiming getTiming();
+ ComputedEffectTiming getComputedTiming();
+ undefined updateTiming(optional OptionalEffectTiming timing = {});
+};
+
+dictionary EffectTiming {
+ FillMode fill = "auto";
+ double iterationStart = 0.0;
+ unrestricted double iterations = 1.0;
+ PlaybackDirection direction = "normal";
+ DOMString easing = "linear";
+};
+
+dictionary OptionalEffectTiming {
+ double delay;
+ double endDelay;
+ FillMode fill;
+ double iterationStart;
+ unrestricted double iterations;
+ (unrestricted double or DOMString) duration;
+ PlaybackDirection direction;
+ DOMString easing;
+};
+
+enum FillMode { "none", "forwards", "backwards", "both", "auto" };
+
+enum PlaybackDirection { "normal", "reverse", "alternate", "alternate-reverse" };
+
+dictionary ComputedEffectTiming : EffectTiming {
+ double? progress;
+ unrestricted double? currentIteration;
+};
+
+[Exposed=Window]
+interface KeyframeEffect : AnimationEffect {
+ constructor(Element? target,
+ object? keyframes,
+ optional (unrestricted double or KeyframeEffectOptions) options = {});
+ constructor(KeyframeEffect source);
+ attribute Element? target;
+ attribute CSSOMString? pseudoElement;
+ attribute CompositeOperation composite;
+ sequence<object> getKeyframes();
+ undefined setKeyframes(object? keyframes);
+};
+
+dictionary BaseComputedKeyframe {
+ double? offset = null;
+ double computedOffset;
+ DOMString easing = "linear";
+ CompositeOperationOrAuto composite = "auto";
+};
+
+dictionary BasePropertyIndexedKeyframe {
+ (double? or sequence<double?>) offset = [];
+ (DOMString or sequence<DOMString>) easing = [];
+ (CompositeOperationOrAuto or sequence<CompositeOperationOrAuto>) composite = [];
+};
+
+dictionary BaseKeyframe {
+ double? offset = null;
+ DOMString easing = "linear";
+ CompositeOperationOrAuto composite = "auto";
+};
+
+dictionary KeyframeEffectOptions : EffectTiming {
+ CompositeOperation composite = "replace";
+ CSSOMString? pseudoElement = null;
+};
+
+enum CompositeOperation { "replace", "add", "accumulate" };
+
+enum CompositeOperationOrAuto { "replace", "add", "accumulate", "auto" };
+
+interface mixin Animatable {
+ Animation animate(object? keyframes,
+ optional (unrestricted double or KeyframeAnimationOptions) options = {});
+ sequence<Animation> getAnimations(optional GetAnimationsOptions options = {});
+};
+
+dictionary KeyframeAnimationOptions : KeyframeEffectOptions {
+ DOMString id = "";
+ AnimationTimeline? timeline;
+};
+
+dictionary GetAnimationsOptions {
+ boolean subtree = false;
+};
+
+partial interface Document {
+ readonly attribute DocumentTimeline timeline;
+};
+
+partial interface mixin DocumentOrShadowRoot {
+ sequence<Animation> getAnimations();
+};
+
+Element includes Animatable;
diff --git a/test/wpt/tests/interfaces/web-app-launch.idl b/test/wpt/tests/interfaces/web-app-launch.idl
new file mode 100644
index 0000000..c3b6e39
--- /dev/null
+++ b/test/wpt/tests/interfaces/web-app-launch.idl
@@ -0,0 +1,19 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Web App Launch Handler API (https://wicg.github.io/web-app-launch/)
+
+[Exposed=Window] interface LaunchParams {
+ readonly attribute DOMString? targetURL;
+ readonly attribute FrozenArray<FileSystemHandle> files;
+};
+
+callback LaunchConsumer = any (LaunchParams params);
+
+partial interface Window {
+ readonly attribute LaunchQueue launchQueue;
+};
+
+[Exposed=Window] interface LaunchQueue {
+ undefined setConsumer(LaunchConsumer consumer);
+};
diff --git a/test/wpt/tests/interfaces/web-bluetooth.idl b/test/wpt/tests/interfaces/web-bluetooth.idl
new file mode 100644
index 0000000..5d46119
--- /dev/null
+++ b/test/wpt/tests/interfaces/web-bluetooth.idl
@@ -0,0 +1,252 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Web Bluetooth (https://webbluetoothcg.github.io/web-bluetooth/)
+
+dictionary BluetoothDataFilterInit {
+ BufferSource dataPrefix;
+ BufferSource mask;
+};
+
+dictionary BluetoothManufacturerDataFilterInit : BluetoothDataFilterInit {
+ required [EnforceRange] unsigned short companyIdentifier;
+};
+
+dictionary BluetoothServiceDataFilterInit : BluetoothDataFilterInit {
+ required BluetoothServiceUUID service;
+};
+
+dictionary BluetoothLEScanFilterInit {
+ sequence<BluetoothServiceUUID> services;
+ DOMString name;
+ DOMString namePrefix;
+ sequence<BluetoothManufacturerDataFilterInit> manufacturerData;
+ sequence<BluetoothServiceDataFilterInit> serviceData;
+};
+
+dictionary RequestDeviceOptions {
+ sequence<BluetoothLEScanFilterInit> filters;
+ sequence<BluetoothLEScanFilterInit> exclusionFilters;
+ sequence<BluetoothServiceUUID> optionalServices = [];
+ sequence<unsigned short> optionalManufacturerData = [];
+ boolean acceptAllDevices = false;
+};
+
+[Exposed=Window, SecureContext]
+interface Bluetooth : EventTarget {
+ Promise<boolean> getAvailability();
+ attribute EventHandler onavailabilitychanged;
+ [SameObject]
+ readonly attribute BluetoothDevice? referringDevice;
+ Promise<sequence<BluetoothDevice>> getDevices();
+ Promise<BluetoothDevice> requestDevice(optional RequestDeviceOptions options = {});
+};
+
+Bluetooth includes BluetoothDeviceEventHandlers;
+Bluetooth includes CharacteristicEventHandlers;
+Bluetooth includes ServiceEventHandlers;
+
+dictionary BluetoothPermissionDescriptor : PermissionDescriptor {
+ DOMString deviceId;
+ // These match RequestDeviceOptions.
+ sequence<BluetoothLEScanFilterInit> filters;
+ sequence<BluetoothServiceUUID> optionalServices = [];
+ sequence<unsigned short> optionalManufacturerData = [];
+ boolean acceptAllDevices = false;
+};
+
+dictionary AllowedBluetoothDevice {
+ required DOMString deviceId;
+ required boolean mayUseGATT;
+ // An allowedServices of "all" means all services are allowed.
+ required (DOMString or sequence<UUID>) allowedServices;
+ required sequence<unsigned short> allowedManufacturerData;
+};
+dictionary BluetoothPermissionStorage {
+ required sequence<AllowedBluetoothDevice> allowedDevices;
+};
+
+[Exposed=Window]
+interface BluetoothPermissionResult : PermissionStatus {
+ attribute FrozenArray<BluetoothDevice> devices;
+};
+
+[
+ Exposed=Window,
+ SecureContext
+]
+interface ValueEvent : Event {
+ constructor(DOMString type, optional ValueEventInit initDict = {});
+ readonly attribute any value;
+};
+
+dictionary ValueEventInit : EventInit {
+ any value = null;
+};
+
+[Exposed=Window, SecureContext]
+interface BluetoothDevice : EventTarget {
+ readonly attribute DOMString id;
+ readonly attribute DOMString? name;
+ readonly attribute BluetoothRemoteGATTServer? gatt;
+
+ Promise<undefined> forget();
+ Promise<undefined> watchAdvertisements(
+ optional WatchAdvertisementsOptions options = {});
+ readonly attribute boolean watchingAdvertisements;
+};
+BluetoothDevice includes BluetoothDeviceEventHandlers;
+BluetoothDevice includes CharacteristicEventHandlers;
+BluetoothDevice includes ServiceEventHandlers;
+
+dictionary WatchAdvertisementsOptions {
+ AbortSignal signal;
+};
+
+[Exposed=Window, SecureContext]
+interface BluetoothManufacturerDataMap {
+ readonly maplike<unsigned short, DataView>;
+};
+[Exposed=Window, SecureContext]
+interface BluetoothServiceDataMap {
+ readonly maplike<UUID, DataView>;
+};
+[
+ Exposed=Window,
+ SecureContext
+]
+interface BluetoothAdvertisingEvent : Event {
+ constructor(DOMString type, BluetoothAdvertisingEventInit init);
+ [SameObject]
+ readonly attribute BluetoothDevice device;
+ readonly attribute FrozenArray<UUID> uuids;
+ readonly attribute DOMString? name;
+ readonly attribute unsigned short? appearance;
+ readonly attribute byte? txPower;
+ readonly attribute byte? rssi;
+ [SameObject]
+ readonly attribute BluetoothManufacturerDataMap manufacturerData;
+ [SameObject]
+ readonly attribute BluetoothServiceDataMap serviceData;
+};
+dictionary BluetoothAdvertisingEventInit : EventInit {
+ required BluetoothDevice device;
+ sequence<(DOMString or unsigned long)> uuids;
+ DOMString name;
+ unsigned short appearance;
+ byte txPower;
+ byte rssi;
+ BluetoothManufacturerDataMap manufacturerData;
+ BluetoothServiceDataMap serviceData;
+};
+
+[Exposed=Window, SecureContext]
+interface BluetoothRemoteGATTServer {
+ [SameObject]
+ readonly attribute BluetoothDevice device;
+ readonly attribute boolean connected;
+ Promise<BluetoothRemoteGATTServer> connect();
+ undefined disconnect();
+ Promise<BluetoothRemoteGATTService> getPrimaryService(BluetoothServiceUUID service);
+ Promise<sequence<BluetoothRemoteGATTService>>
+ getPrimaryServices(optional BluetoothServiceUUID service);
+};
+
+[Exposed=Window, SecureContext]
+interface BluetoothRemoteGATTService : EventTarget {
+ [SameObject]
+ readonly attribute BluetoothDevice device;
+ readonly attribute UUID uuid;
+ readonly attribute boolean isPrimary;
+ Promise<BluetoothRemoteGATTCharacteristic>
+ getCharacteristic(BluetoothCharacteristicUUID characteristic);
+ Promise<sequence<BluetoothRemoteGATTCharacteristic>>
+ getCharacteristics(optional BluetoothCharacteristicUUID characteristic);
+ Promise<BluetoothRemoteGATTService>
+ getIncludedService(BluetoothServiceUUID service);
+ Promise<sequence<BluetoothRemoteGATTService>>
+ getIncludedServices(optional BluetoothServiceUUID service);
+};
+BluetoothRemoteGATTService includes CharacteristicEventHandlers;
+BluetoothRemoteGATTService includes ServiceEventHandlers;
+
+[Exposed=Window, SecureContext]
+interface BluetoothRemoteGATTCharacteristic : EventTarget {
+ [SameObject]
+ readonly attribute BluetoothRemoteGATTService service;
+ readonly attribute UUID uuid;
+ readonly attribute BluetoothCharacteristicProperties properties;
+ readonly attribute DataView? value;
+ Promise<BluetoothRemoteGATTDescriptor> getDescriptor(BluetoothDescriptorUUID descriptor);
+ Promise<sequence<BluetoothRemoteGATTDescriptor>>
+ getDescriptors(optional BluetoothDescriptorUUID descriptor);
+ Promise<DataView> readValue();
+ Promise<undefined> writeValue(BufferSource value);
+ Promise<undefined> writeValueWithResponse(BufferSource value);
+ Promise<undefined> writeValueWithoutResponse(BufferSource value);
+ Promise<BluetoothRemoteGATTCharacteristic> startNotifications();
+ Promise<BluetoothRemoteGATTCharacteristic> stopNotifications();
+};
+BluetoothRemoteGATTCharacteristic includes CharacteristicEventHandlers;
+
+[Exposed=Window, SecureContext]
+interface BluetoothCharacteristicProperties {
+ readonly attribute boolean broadcast;
+ readonly attribute boolean read;
+ readonly attribute boolean writeWithoutResponse;
+ readonly attribute boolean write;
+ readonly attribute boolean notify;
+ readonly attribute boolean indicate;
+ readonly attribute boolean authenticatedSignedWrites;
+ readonly attribute boolean reliableWrite;
+ readonly attribute boolean writableAuxiliaries;
+};
+
+[Exposed=Window, SecureContext]
+interface BluetoothRemoteGATTDescriptor {
+ [SameObject]
+ readonly attribute BluetoothRemoteGATTCharacteristic characteristic;
+ readonly attribute UUID uuid;
+ readonly attribute DataView? value;
+ Promise<DataView> readValue();
+ Promise<undefined> writeValue(BufferSource value);
+};
+
+[SecureContext]
+interface mixin CharacteristicEventHandlers {
+ attribute EventHandler oncharacteristicvaluechanged;
+};
+
+[SecureContext]
+interface mixin BluetoothDeviceEventHandlers {
+ attribute EventHandler onadvertisementreceived;
+ attribute EventHandler ongattserverdisconnected;
+};
+
+[SecureContext]
+interface mixin ServiceEventHandlers {
+ attribute EventHandler onserviceadded;
+ attribute EventHandler onservicechanged;
+ attribute EventHandler onserviceremoved;
+};
+
+typedef DOMString UUID;
+
+[Exposed=Window]
+interface BluetoothUUID {
+ static UUID getService((DOMString or unsigned long) name);
+ static UUID getCharacteristic((DOMString or unsigned long) name);
+ static UUID getDescriptor((DOMString or unsigned long) name);
+
+ static UUID canonicalUUID([EnforceRange] unsigned long alias);
+};
+
+typedef (DOMString or unsigned long) BluetoothServiceUUID;
+typedef (DOMString or unsigned long) BluetoothCharacteristicUUID;
+typedef (DOMString or unsigned long) BluetoothDescriptorUUID;
+
+[SecureContext]
+partial interface Navigator {
+ [SameObject]
+ readonly attribute Bluetooth bluetooth;
+};
diff --git a/test/wpt/tests/interfaces/web-locks.idl b/test/wpt/tests/interfaces/web-locks.idl
new file mode 100644
index 0000000..d79e404
--- /dev/null
+++ b/test/wpt/tests/interfaces/web-locks.idl
@@ -0,0 +1,50 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Web Locks API (https://w3c.github.io/web-locks/)
+
+[SecureContext]
+interface mixin NavigatorLocks {
+ readonly attribute LockManager locks;
+};
+Navigator includes NavigatorLocks;
+WorkerNavigator includes NavigatorLocks;
+
+[SecureContext, Exposed=(Window,Worker)]
+interface LockManager {
+ Promise<any> request(DOMString name,
+ LockGrantedCallback callback);
+ Promise<any> request(DOMString name,
+ LockOptions options,
+ LockGrantedCallback callback);
+
+ Promise<LockManagerSnapshot> query();
+};
+
+callback LockGrantedCallback = Promise<any> (Lock? lock);
+
+enum LockMode { "shared", "exclusive" };
+
+dictionary LockOptions {
+ LockMode mode = "exclusive";
+ boolean ifAvailable = false;
+ boolean steal = false;
+ AbortSignal signal;
+};
+
+dictionary LockManagerSnapshot {
+ sequence<LockInfo> held;
+ sequence<LockInfo> pending;
+};
+
+dictionary LockInfo {
+ DOMString name;
+ LockMode mode;
+ DOMString clientId;
+};
+
+[SecureContext, Exposed=(Window,Worker)]
+interface Lock {
+ readonly attribute DOMString name;
+ readonly attribute LockMode mode;
+};
diff --git a/test/wpt/tests/interfaces/web-nfc.idl b/test/wpt/tests/interfaces/web-nfc.idl
new file mode 100644
index 0000000..ff042b0
--- /dev/null
+++ b/test/wpt/tests/interfaces/web-nfc.idl
@@ -0,0 +1,81 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Web NFC (https://w3c.github.io/web-nfc/)
+
+[SecureContext, Exposed=Window]
+interface NDEFMessage {
+ constructor(NDEFMessageInit messageInit);
+ readonly attribute FrozenArray<NDEFRecord> records;
+};
+
+dictionary NDEFMessageInit {
+ required sequence<NDEFRecordInit> records;
+};
+
+[SecureContext, Exposed=Window]
+interface NDEFRecord {
+ constructor(NDEFRecordInit recordInit);
+
+ readonly attribute USVString recordType;
+ readonly attribute USVString? mediaType;
+ readonly attribute USVString? id;
+ readonly attribute DataView? data;
+
+ readonly attribute USVString? encoding;
+ readonly attribute USVString? lang;
+
+ sequence<NDEFRecord>? toRecords();
+};
+
+dictionary NDEFRecordInit {
+ required USVString recordType;
+ USVString mediaType;
+ USVString id;
+
+ USVString encoding;
+ USVString lang;
+
+ any data; // DOMString or BufferSource or NDEFMessageInit
+};
+
+typedef (DOMString or BufferSource or NDEFMessageInit) NDEFMessageSource;
+
+[SecureContext, Exposed=Window]
+interface NDEFReader : EventTarget {
+ constructor();
+
+ attribute EventHandler onreading;
+ attribute EventHandler onreadingerror;
+
+ Promise<undefined> scan(optional NDEFScanOptions options={});
+ Promise<undefined> write(NDEFMessageSource message,
+ optional NDEFWriteOptions options={});
+ Promise<undefined> makeReadOnly(optional NDEFMakeReadOnlyOptions options={});
+};
+
+[SecureContext, Exposed=Window]
+interface NDEFReadingEvent : Event {
+ constructor(DOMString type, NDEFReadingEventInit readingEventInitDict);
+
+ readonly attribute DOMString serialNumber;
+ [SameObject] readonly attribute NDEFMessage message;
+};
+
+dictionary NDEFReadingEventInit : EventInit {
+ DOMString? serialNumber = "";
+ required NDEFMessageInit message;
+};
+
+dictionary NDEFWriteOptions {
+ boolean overwrite = true;
+ AbortSignal? signal;
+};
+
+dictionary NDEFMakeReadOnlyOptions {
+ AbortSignal? signal;
+};
+
+dictionary NDEFScanOptions {
+ AbortSignal signal;
+};
diff --git a/test/wpt/tests/interfaces/web-otp.idl b/test/wpt/tests/interfaces/web-otp.idl
new file mode 100644
index 0000000..591979e
--- /dev/null
+++ b/test/wpt/tests/interfaces/web-otp.idl
@@ -0,0 +1,21 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: WebOTP API (https://wicg.github.io/web-otp/)
+
+[Exposed=Window, SecureContext]
+interface OTPCredential : Credential {
+ readonly attribute DOMString code;
+};
+
+partial dictionary CredentialRequestOptions {
+ OTPCredentialRequestOptions otp;
+};
+
+dictionary OTPCredentialRequestOptions {
+ sequence<OTPCredentialTransportType> transport = [];
+};
+
+enum OTPCredentialTransportType {
+ "sms",
+};
diff --git a/test/wpt/tests/interfaces/web-share.idl b/test/wpt/tests/interfaces/web-share.idl
new file mode 100644
index 0000000..12a2dbb
--- /dev/null
+++ b/test/wpt/tests/interfaces/web-share.idl
@@ -0,0 +1,16 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Web Share API (https://w3c.github.io/web-share/)
+
+partial interface Navigator {
+ [SecureContext] Promise<undefined> share(optional ShareData data = {});
+ [SecureContext] boolean canShare(optional ShareData data = {});
+};
+
+dictionary ShareData {
+ sequence<File> files;
+ USVString title;
+ USVString text;
+ USVString url;
+};
diff --git a/test/wpt/tests/interfaces/webaudio.idl b/test/wpt/tests/interfaces/webaudio.idl
new file mode 100644
index 0000000..1569de2
--- /dev/null
+++ b/test/wpt/tests/interfaces/webaudio.idl
@@ -0,0 +1,674 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Web Audio API (https://webaudio.github.io/web-audio-api/)
+
+enum AudioContextState {
+ "suspended",
+ "running",
+ "closed"
+};
+
+callback DecodeErrorCallback = undefined (DOMException error);
+
+callback DecodeSuccessCallback = undefined (AudioBuffer decodedData);
+
+[Exposed=Window]
+interface BaseAudioContext : EventTarget {
+ readonly attribute AudioDestinationNode destination;
+ readonly attribute float sampleRate;
+ readonly attribute double currentTime;
+ readonly attribute AudioListener listener;
+ readonly attribute AudioContextState state;
+ [SameObject, SecureContext]
+ readonly attribute AudioWorklet audioWorklet;
+ attribute EventHandler onstatechange;
+
+ AnalyserNode createAnalyser ();
+ BiquadFilterNode createBiquadFilter ();
+ AudioBuffer createBuffer (unsigned long numberOfChannels,
+ unsigned long length,
+ float sampleRate);
+ AudioBufferSourceNode createBufferSource ();
+ ChannelMergerNode createChannelMerger (optional unsigned long numberOfInputs = 6);
+ ChannelSplitterNode createChannelSplitter (
+ optional unsigned long numberOfOutputs = 6);
+ ConstantSourceNode createConstantSource ();
+ ConvolverNode createConvolver ();
+ DelayNode createDelay (optional double maxDelayTime = 1.0);
+ DynamicsCompressorNode createDynamicsCompressor ();
+ GainNode createGain ();
+ IIRFilterNode createIIRFilter (sequence<double> feedforward,
+ sequence<double> feedback);
+ OscillatorNode createOscillator ();
+ PannerNode createPanner ();
+ PeriodicWave createPeriodicWave (sequence<float> real,
+ sequence<float> imag,
+ optional PeriodicWaveConstraints constraints = {});
+ ScriptProcessorNode createScriptProcessor(
+ optional unsigned long bufferSize = 0,
+ optional unsigned long numberOfInputChannels = 2,
+ optional unsigned long numberOfOutputChannels = 2);
+ StereoPannerNode createStereoPanner ();
+ WaveShaperNode createWaveShaper ();
+
+ Promise<AudioBuffer> decodeAudioData (
+ ArrayBuffer audioData,
+ optional DecodeSuccessCallback? successCallback,
+ optional DecodeErrorCallback? errorCallback);
+};
+
+enum AudioContextLatencyCategory {
+ "balanced",
+ "interactive",
+ "playback"
+};
+
+enum AudioSinkType {
+ "none"
+};
+
+[Exposed=Window]
+interface AudioContext : BaseAudioContext {
+ constructor (optional AudioContextOptions contextOptions = {});
+ readonly attribute double baseLatency;
+ readonly attribute double outputLatency;
+ [SecureContext] readonly attribute (DOMString or AudioSinkInfo) sinkId;
+ [SecureContext] readonly attribute AudioRenderCapacity renderCapacity;
+ attribute EventHandler onsinkchange;
+ AudioTimestamp getOutputTimestamp ();
+ Promise<undefined> resume ();
+ Promise<undefined> suspend ();
+ Promise<undefined> close ();
+ [SecureContext] Promise<undefined> setSinkId ((DOMString or AudioSinkOptions) sinkId);
+ MediaElementAudioSourceNode createMediaElementSource (HTMLMediaElement mediaElement);
+ MediaStreamAudioSourceNode createMediaStreamSource (MediaStream mediaStream);
+ MediaStreamTrackAudioSourceNode createMediaStreamTrackSource (
+ MediaStreamTrack mediaStreamTrack);
+ MediaStreamAudioDestinationNode createMediaStreamDestination ();
+};
+
+dictionary AudioContextOptions {
+ (AudioContextLatencyCategory or double) latencyHint = "interactive";
+ float sampleRate;
+ (DOMString or AudioSinkOptions) sinkId;
+};
+
+dictionary AudioSinkOptions {
+ required AudioSinkType type;
+};
+
+[Exposed=Window]
+interface AudioSinkInfo {
+ readonly attribute AudioSinkType type;
+};
+
+dictionary AudioTimestamp {
+ double contextTime;
+ DOMHighResTimeStamp performanceTime;
+};
+
+[Exposed=Window]
+interface AudioRenderCapacity : EventTarget {
+ undefined start(optional AudioRenderCapacityOptions options = {});
+ undefined stop();
+ attribute EventHandler onupdate;
+};
+
+dictionary AudioRenderCapacityOptions {
+ double updateInterval = 1;
+};
+
+[Exposed=Window]
+interface AudioRenderCapacityEvent : Event {
+ constructor (DOMString type, optional AudioRenderCapacityEventInit eventInitDict = {});
+ readonly attribute double timestamp;
+ readonly attribute double averageLoad;
+ readonly attribute double peakLoad;
+ readonly attribute double underrunRatio;
+};
+
+dictionary AudioRenderCapacityEventInit : EventInit {
+ double timestamp = 0;
+ double averageLoad = 0;
+ double peakLoad = 0;
+ double underrunRatio = 0;
+};
+
+[Exposed=Window]
+interface OfflineAudioContext : BaseAudioContext {
+ constructor(OfflineAudioContextOptions contextOptions);
+ constructor(unsigned long numberOfChannels, unsigned long length, float sampleRate);
+ Promise<AudioBuffer> startRendering();
+ Promise<undefined> resume();
+ Promise<undefined> suspend(double suspendTime);
+ readonly attribute unsigned long length;
+ attribute EventHandler oncomplete;
+};
+
+dictionary OfflineAudioContextOptions {
+ unsigned long numberOfChannels = 1;
+ required unsigned long length;
+ required float sampleRate;
+};
+
+[Exposed=Window]
+interface OfflineAudioCompletionEvent : Event {
+ constructor (DOMString type, OfflineAudioCompletionEventInit eventInitDict);
+ readonly attribute AudioBuffer renderedBuffer;
+};
+
+dictionary OfflineAudioCompletionEventInit : EventInit {
+ required AudioBuffer renderedBuffer;
+};
+
+[Exposed=Window]
+interface AudioBuffer {
+ constructor (AudioBufferOptions options);
+ readonly attribute float sampleRate;
+ readonly attribute unsigned long length;
+ readonly attribute double duration;
+ readonly attribute unsigned long numberOfChannels;
+ Float32Array getChannelData (unsigned long channel);
+ undefined copyFromChannel (Float32Array destination,
+ unsigned long channelNumber,
+ optional unsigned long bufferOffset = 0);
+ undefined copyToChannel (Float32Array source,
+ unsigned long channelNumber,
+ optional unsigned long bufferOffset = 0);
+};
+
+dictionary AudioBufferOptions {
+ unsigned long numberOfChannels = 1;
+ required unsigned long length;
+ required float sampleRate;
+};
+
+[Exposed=Window]
+interface AudioNode : EventTarget {
+ AudioNode connect (AudioNode destinationNode,
+ optional unsigned long output = 0,
+ optional unsigned long input = 0);
+ undefined connect (AudioParam destinationParam, optional unsigned long output = 0);
+ undefined disconnect ();
+ undefined disconnect (unsigned long output);
+ undefined disconnect (AudioNode destinationNode);
+ undefined disconnect (AudioNode destinationNode, unsigned long output);
+ undefined disconnect (AudioNode destinationNode,
+ unsigned long output,
+ unsigned long input);
+ undefined disconnect (AudioParam destinationParam);
+ undefined disconnect (AudioParam destinationParam, unsigned long output);
+ readonly attribute BaseAudioContext context;
+ readonly attribute unsigned long numberOfInputs;
+ readonly attribute unsigned long numberOfOutputs;
+ attribute unsigned long channelCount;
+ attribute ChannelCountMode channelCountMode;
+ attribute ChannelInterpretation channelInterpretation;
+};
+
+enum ChannelCountMode {
+ "max",
+ "clamped-max",
+ "explicit"
+};
+
+enum ChannelInterpretation {
+ "speakers",
+ "discrete"
+};
+
+dictionary AudioNodeOptions {
+ unsigned long channelCount;
+ ChannelCountMode channelCountMode;
+ ChannelInterpretation channelInterpretation;
+};
+
+enum AutomationRate {
+ "a-rate",
+ "k-rate"
+};
+
+[Exposed=Window]
+interface AudioParam {
+ attribute float value;
+ attribute AutomationRate automationRate;
+ readonly attribute float defaultValue;
+ readonly attribute float minValue;
+ readonly attribute float maxValue;
+ AudioParam setValueAtTime (float value, double startTime);
+ AudioParam linearRampToValueAtTime (float value, double endTime);
+ AudioParam exponentialRampToValueAtTime (float value, double endTime);
+ AudioParam setTargetAtTime (float target, double startTime, float timeConstant);
+ AudioParam setValueCurveAtTime (sequence<float> values,
+ double startTime,
+ double duration);
+ AudioParam cancelScheduledValues (double cancelTime);
+ AudioParam cancelAndHoldAtTime (double cancelTime);
+};
+
+[Exposed=Window]
+interface AudioScheduledSourceNode : AudioNode {
+ attribute EventHandler onended;
+ undefined start(optional double when = 0);
+ undefined stop(optional double when = 0);
+};
+
+[Exposed=Window]
+interface AnalyserNode : AudioNode {
+ constructor (BaseAudioContext context, optional AnalyserOptions options = {});
+ undefined getFloatFrequencyData (Float32Array array);
+ undefined getByteFrequencyData (Uint8Array array);
+ undefined getFloatTimeDomainData (Float32Array array);
+ undefined getByteTimeDomainData (Uint8Array array);
+ attribute unsigned long fftSize;
+ readonly attribute unsigned long frequencyBinCount;
+ attribute double minDecibels;
+ attribute double maxDecibels;
+ attribute double smoothingTimeConstant;
+};
+
+dictionary AnalyserOptions : AudioNodeOptions {
+ unsigned long fftSize = 2048;
+ double maxDecibels = -30;
+ double minDecibels = -100;
+ double smoothingTimeConstant = 0.8;
+};
+
+[Exposed=Window]
+interface AudioBufferSourceNode : AudioScheduledSourceNode {
+ constructor (BaseAudioContext context,
+ optional AudioBufferSourceOptions options = {});
+ attribute AudioBuffer? buffer;
+ readonly attribute AudioParam playbackRate;
+ readonly attribute AudioParam detune;
+ attribute boolean loop;
+ attribute double loopStart;
+ attribute double loopEnd;
+ undefined start (optional double when = 0,
+ optional double offset,
+ optional double duration);
+};
+
+dictionary AudioBufferSourceOptions {
+ AudioBuffer? buffer;
+ float detune = 0;
+ boolean loop = false;
+ double loopEnd = 0;
+ double loopStart = 0;
+ float playbackRate = 1;
+};
+
+[Exposed=Window]
+interface AudioDestinationNode : AudioNode {
+ readonly attribute unsigned long maxChannelCount;
+};
+
+[Exposed=Window]
+interface AudioListener {
+ readonly attribute AudioParam positionX;
+ readonly attribute AudioParam positionY;
+ readonly attribute AudioParam positionZ;
+ readonly attribute AudioParam forwardX;
+ readonly attribute AudioParam forwardY;
+ readonly attribute AudioParam forwardZ;
+ readonly attribute AudioParam upX;
+ readonly attribute AudioParam upY;
+ readonly attribute AudioParam upZ;
+ undefined setPosition (float x, float y, float z);
+ undefined setOrientation (float x, float y, float z, float xUp, float yUp, float zUp);
+};
+
+[Exposed=Window]
+interface AudioProcessingEvent : Event {
+ constructor (DOMString type, AudioProcessingEventInit eventInitDict);
+ readonly attribute double playbackTime;
+ readonly attribute AudioBuffer inputBuffer;
+ readonly attribute AudioBuffer outputBuffer;
+};
+
+dictionary AudioProcessingEventInit : EventInit {
+ required double playbackTime;
+ required AudioBuffer inputBuffer;
+ required AudioBuffer outputBuffer;
+};
+
+enum BiquadFilterType {
+ "lowpass",
+ "highpass",
+ "bandpass",
+ "lowshelf",
+ "highshelf",
+ "peaking",
+ "notch",
+ "allpass"
+};
+
+[Exposed=Window]
+interface BiquadFilterNode : AudioNode {
+ constructor (BaseAudioContext context, optional BiquadFilterOptions options = {});
+ attribute BiquadFilterType type;
+ readonly attribute AudioParam frequency;
+ readonly attribute AudioParam detune;
+ readonly attribute AudioParam Q;
+ readonly attribute AudioParam gain;
+ undefined getFrequencyResponse (Float32Array frequencyHz,
+ Float32Array magResponse,
+ Float32Array phaseResponse);
+};
+
+dictionary BiquadFilterOptions : AudioNodeOptions {
+ BiquadFilterType type = "lowpass";
+ float Q = 1;
+ float detune = 0;
+ float frequency = 350;
+ float gain = 0;
+};
+
+[Exposed=Window]
+interface ChannelMergerNode : AudioNode {
+ constructor (BaseAudioContext context, optional ChannelMergerOptions options = {});
+};
+
+dictionary ChannelMergerOptions : AudioNodeOptions {
+ unsigned long numberOfInputs = 6;
+};
+
+[Exposed=Window]
+interface ChannelSplitterNode : AudioNode {
+ constructor (BaseAudioContext context, optional ChannelSplitterOptions options = {});
+};
+
+dictionary ChannelSplitterOptions : AudioNodeOptions {
+ unsigned long numberOfOutputs = 6;
+};
+
+[Exposed=Window]
+interface ConstantSourceNode : AudioScheduledSourceNode {
+ constructor (BaseAudioContext context, optional ConstantSourceOptions options = {});
+ readonly attribute AudioParam offset;
+};
+
+dictionary ConstantSourceOptions {
+ float offset = 1;
+};
+
+[Exposed=Window]
+interface ConvolverNode : AudioNode {
+ constructor (BaseAudioContext context, optional ConvolverOptions options = {});
+ attribute AudioBuffer? buffer;
+ attribute boolean normalize;
+};
+
+dictionary ConvolverOptions : AudioNodeOptions {
+ AudioBuffer? buffer;
+ boolean disableNormalization = false;
+};
+
+[Exposed=Window]
+interface DelayNode : AudioNode {
+ constructor (BaseAudioContext context, optional DelayOptions options = {});
+ readonly attribute AudioParam delayTime;
+};
+
+dictionary DelayOptions : AudioNodeOptions {
+ double maxDelayTime = 1;
+ double delayTime = 0;
+};
+
+[Exposed=Window]
+interface DynamicsCompressorNode : AudioNode {
+ constructor (BaseAudioContext context,
+ optional DynamicsCompressorOptions options = {});
+ readonly attribute AudioParam threshold;
+ readonly attribute AudioParam knee;
+ readonly attribute AudioParam ratio;
+ readonly attribute float reduction;
+ readonly attribute AudioParam attack;
+ readonly attribute AudioParam release;
+};
+
+dictionary DynamicsCompressorOptions : AudioNodeOptions {
+ float attack = 0.003;
+ float knee = 30;
+ float ratio = 12;
+ float release = 0.25;
+ float threshold = -24;
+};
+
+[Exposed=Window]
+interface GainNode : AudioNode {
+ constructor (BaseAudioContext context, optional GainOptions options = {});
+ readonly attribute AudioParam gain;
+};
+
+dictionary GainOptions : AudioNodeOptions {
+ float gain = 1.0;
+};
+
+[Exposed=Window]
+interface IIRFilterNode : AudioNode {
+ constructor (BaseAudioContext context, IIRFilterOptions options);
+ undefined getFrequencyResponse (Float32Array frequencyHz,
+ Float32Array magResponse,
+ Float32Array phaseResponse);
+};
+
+dictionary IIRFilterOptions : AudioNodeOptions {
+ required sequence<double> feedforward;
+ required sequence<double> feedback;
+};
+
+[Exposed=Window]
+interface MediaElementAudioSourceNode : AudioNode {
+ constructor (AudioContext context, MediaElementAudioSourceOptions options);
+ [SameObject] readonly attribute HTMLMediaElement mediaElement;
+};
+
+dictionary MediaElementAudioSourceOptions {
+ required HTMLMediaElement mediaElement;
+};
+
+[Exposed=Window]
+interface MediaStreamAudioDestinationNode : AudioNode {
+ constructor (AudioContext context, optional AudioNodeOptions options = {});
+ readonly attribute MediaStream stream;
+};
+
+[Exposed=Window]
+interface MediaStreamAudioSourceNode : AudioNode {
+ constructor (AudioContext context, MediaStreamAudioSourceOptions options);
+ [SameObject] readonly attribute MediaStream mediaStream;
+};
+
+dictionary MediaStreamAudioSourceOptions {
+ required MediaStream mediaStream;
+};
+
+[Exposed=Window]
+interface MediaStreamTrackAudioSourceNode : AudioNode {
+ constructor (AudioContext context, MediaStreamTrackAudioSourceOptions options);
+};
+
+dictionary MediaStreamTrackAudioSourceOptions {
+ required MediaStreamTrack mediaStreamTrack;
+};
+
+enum OscillatorType {
+ "sine",
+ "square",
+ "sawtooth",
+ "triangle",
+ "custom"
+};
+
+[Exposed=Window]
+interface OscillatorNode : AudioScheduledSourceNode {
+ constructor (BaseAudioContext context, optional OscillatorOptions options = {});
+ attribute OscillatorType type;
+ readonly attribute AudioParam frequency;
+ readonly attribute AudioParam detune;
+ undefined setPeriodicWave (PeriodicWave periodicWave);
+};
+
+dictionary OscillatorOptions : AudioNodeOptions {
+ OscillatorType type = "sine";
+ float frequency = 440;
+ float detune = 0;
+ PeriodicWave periodicWave;
+};
+
+enum PanningModelType {
+ "equalpower",
+ "HRTF"
+};
+
+enum DistanceModelType {
+ "linear",
+ "inverse",
+ "exponential"
+};
+
+[Exposed=Window]
+interface PannerNode : AudioNode {
+ constructor (BaseAudioContext context, optional PannerOptions options = {});
+ attribute PanningModelType panningModel;
+ readonly attribute AudioParam positionX;
+ readonly attribute AudioParam positionY;
+ readonly attribute AudioParam positionZ;
+ readonly attribute AudioParam orientationX;
+ readonly attribute AudioParam orientationY;
+ readonly attribute AudioParam orientationZ;
+ attribute DistanceModelType distanceModel;
+ attribute double refDistance;
+ attribute double maxDistance;
+ attribute double rolloffFactor;
+ attribute double coneInnerAngle;
+ attribute double coneOuterAngle;
+ attribute double coneOuterGain;
+ undefined setPosition (float x, float y, float z);
+ undefined setOrientation (float x, float y, float z);
+};
+
+dictionary PannerOptions : AudioNodeOptions {
+ PanningModelType panningModel = "equalpower";
+ DistanceModelType distanceModel = "inverse";
+ float positionX = 0;
+ float positionY = 0;
+ float positionZ = 0;
+ float orientationX = 1;
+ float orientationY = 0;
+ float orientationZ = 0;
+ double refDistance = 1;
+ double maxDistance = 10000;
+ double rolloffFactor = 1;
+ double coneInnerAngle = 360;
+ double coneOuterAngle = 360;
+ double coneOuterGain = 0;
+};
+
+[Exposed=Window]
+interface PeriodicWave {
+ constructor (BaseAudioContext context, optional PeriodicWaveOptions options = {});
+};
+
+dictionary PeriodicWaveConstraints {
+ boolean disableNormalization = false;
+};
+
+dictionary PeriodicWaveOptions : PeriodicWaveConstraints {
+ sequence<float> real;
+ sequence<float> imag;
+};
+
+[Exposed=Window]
+interface ScriptProcessorNode : AudioNode {
+ attribute EventHandler onaudioprocess;
+ readonly attribute long bufferSize;
+};
+
+[Exposed=Window]
+interface StereoPannerNode : AudioNode {
+ constructor (BaseAudioContext context, optional StereoPannerOptions options = {});
+ readonly attribute AudioParam pan;
+};
+
+dictionary StereoPannerOptions : AudioNodeOptions {
+ float pan = 0;
+};
+
+enum OverSampleType {
+ "none",
+ "2x",
+ "4x"
+};
+
+[Exposed=Window]
+interface WaveShaperNode : AudioNode {
+ constructor (BaseAudioContext context, optional WaveShaperOptions options = {});
+ attribute Float32Array? curve;
+ attribute OverSampleType oversample;
+};
+
+dictionary WaveShaperOptions : AudioNodeOptions {
+ sequence<float> curve;
+ OverSampleType oversample = "none";
+};
+
+[Exposed=Window, SecureContext]
+interface AudioWorklet : Worklet {
+ readonly attribute MessagePort port;
+};
+
+callback AudioWorkletProcessorConstructor = AudioWorkletProcessor (object options);
+
+[Global=(Worklet, AudioWorklet), Exposed=AudioWorklet]
+interface AudioWorkletGlobalScope : WorkletGlobalScope {
+ undefined registerProcessor (DOMString name,
+ AudioWorkletProcessorConstructor processorCtor);
+ readonly attribute unsigned long long currentFrame;
+ readonly attribute double currentTime;
+ readonly attribute float sampleRate;
+ readonly attribute MessagePort port;
+};
+
+[Exposed=Window]
+interface AudioParamMap {
+ readonly maplike<DOMString, AudioParam>;
+};
+
+[Exposed=Window, SecureContext]
+interface AudioWorkletNode : AudioNode {
+ constructor (BaseAudioContext context, DOMString name,
+ optional AudioWorkletNodeOptions options = {});
+ readonly attribute AudioParamMap parameters;
+ readonly attribute MessagePort port;
+ attribute EventHandler onprocessorerror;
+};
+
+dictionary AudioWorkletNodeOptions : AudioNodeOptions {
+ unsigned long numberOfInputs = 1;
+ unsigned long numberOfOutputs = 1;
+ sequence<unsigned long> outputChannelCount;
+ record<DOMString, double> parameterData;
+ object processorOptions;
+};
+
+[Exposed=AudioWorklet]
+interface AudioWorkletProcessor {
+ constructor ();
+ readonly attribute MessagePort port;
+};
+
+callback AudioWorkletProcessCallback =
+ boolean (FrozenArray<FrozenArray<Float32Array>> inputs,
+ FrozenArray<FrozenArray<Float32Array>> outputs,
+ object parameters);
+
+dictionary AudioParamDescriptor {
+ required DOMString name;
+ float defaultValue = 0;
+ float minValue = -3.4028235e38;
+ float maxValue = 3.4028235e38;
+ AutomationRate automationRate = "a-rate";
+};
diff --git a/test/wpt/tests/interfaces/webauthn.idl b/test/wpt/tests/interfaces/webauthn.idl
new file mode 100644
index 0000000..9a37207
--- /dev/null
+++ b/test/wpt/tests/interfaces/webauthn.idl
@@ -0,0 +1,350 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Web Authentication: An API for accessing Public Key Credentials - Level (https://w3c.github.io/webauthn/)
+
+[SecureContext, Exposed=Window]
+interface PublicKeyCredential : Credential {
+ [SameObject] readonly attribute ArrayBuffer rawId;
+ [SameObject] readonly attribute AuthenticatorResponse response;
+ [SameObject] readonly attribute DOMString? authenticatorAttachment;
+ AuthenticationExtensionsClientOutputs getClientExtensionResults();
+ static Promise<boolean> isConditionalMediationAvailable();
+ PublicKeyCredentialJSON toJSON();
+};
+
+typedef DOMString Base64URLString;
+typedef (RegistrationResponseJSON or AuthenticationResponseJSON) PublicKeyCredentialJSON;
+
+dictionary RegistrationResponseJSON {
+ required Base64URLString id;
+ required Base64URLString rawId;
+ required AuthenticatorAttestationResponseJSON response;
+ DOMString authenticatorAttachment;
+ required AuthenticationExtensionsClientOutputsJSON clientExtensionResults;
+ required DOMString type;
+};
+
+dictionary AuthenticatorAttestationResponseJSON {
+ required Base64URLString clientDataJSON;
+ required Base64URLString attestationObject;
+ required sequence<DOMString> transports;
+};
+
+dictionary AuthenticationResponseJSON {
+ required Base64URLString id;
+ required Base64URLString rawId;
+ required AuthenticatorAssertionResponseJSON response;
+ DOMString authenticatorAttachment;
+ required AuthenticationExtensionsClientOutputsJSON clientExtensionResults;
+ required DOMString type;
+};
+
+dictionary AuthenticatorAssertionResponseJSON {
+ required Base64URLString clientDataJSON;
+ required Base64URLString authenticatorData;
+ required Base64URLString signature;
+ Base64URLString userHandle;
+};
+
+dictionary AuthenticationExtensionsClientOutputsJSON {
+};
+
+partial dictionary CredentialCreationOptions {
+ PublicKeyCredentialCreationOptions publicKey;
+};
+
+partial dictionary CredentialRequestOptions {
+ PublicKeyCredentialRequestOptions publicKey;
+};
+
+partial interface PublicKeyCredential {
+ static Promise<boolean> isUserVerifyingPlatformAuthenticatorAvailable();
+};
+
+partial interface PublicKeyCredential {
+ static PublicKeyCredentialCreationOptions parseCreationOptionsFromJSON(PublicKeyCredentialCreationOptionsJSON options);
+};
+
+dictionary PublicKeyCredentialCreationOptionsJSON {
+ required PublicKeyCredentialRpEntity rp;
+ required PublicKeyCredentialUserEntityJSON user;
+ required Base64URLString challenge;
+ required sequence<PublicKeyCredentialParameters> pubKeyCredParams;
+ unsigned long timeout;
+ sequence<PublicKeyCredentialDescriptorJSON> excludeCredentials = [];
+ AuthenticatorSelectionCriteria authenticatorSelection;
+ DOMString attestation = "none";
+ AuthenticationExtensionsClientInputsJSON extensions;
+};
+
+dictionary PublicKeyCredentialUserEntityJSON {
+ required Base64URLString id;
+ required DOMString name;
+ required DOMString displayName;
+};
+
+dictionary PublicKeyCredentialDescriptorJSON {
+ required Base64URLString id;
+ required DOMString type;
+ sequence<DOMString> transports;
+};
+
+dictionary AuthenticationExtensionsClientInputsJSON {
+};
+
+partial interface PublicKeyCredential {
+ static PublicKeyCredentialRequestOptions parseRequestOptionsFromJSON(PublicKeyCredentialRequestOptionsJSON options);
+};
+
+dictionary PublicKeyCredentialRequestOptionsJSON {
+ required Base64URLString challenge;
+ unsigned long timeout;
+ DOMString rpId;
+ sequence<PublicKeyCredentialDescriptorJSON> allowCredentials = [];
+ DOMString userVerification = "preferred";
+ AuthenticationExtensionsClientInputsJSON extensions;
+};
+
+[SecureContext, Exposed=Window]
+interface AuthenticatorResponse {
+ [SameObject] readonly attribute ArrayBuffer clientDataJSON;
+};
+
+[SecureContext, Exposed=Window]
+interface AuthenticatorAttestationResponse : AuthenticatorResponse {
+ [SameObject] readonly attribute ArrayBuffer attestationObject;
+ sequence<DOMString> getTransports();
+ ArrayBuffer getAuthenticatorData();
+ ArrayBuffer? getPublicKey();
+ COSEAlgorithmIdentifier getPublicKeyAlgorithm();
+};
+
+[SecureContext, Exposed=Window]
+interface AuthenticatorAssertionResponse : AuthenticatorResponse {
+ [SameObject] readonly attribute ArrayBuffer authenticatorData;
+ [SameObject] readonly attribute ArrayBuffer signature;
+ [SameObject] readonly attribute ArrayBuffer? userHandle;
+ [SameObject] readonly attribute ArrayBuffer? attestationObject;
+};
+
+dictionary PublicKeyCredentialParameters {
+ required DOMString type;
+ required COSEAlgorithmIdentifier alg;
+};
+
+dictionary PublicKeyCredentialCreationOptions {
+ required PublicKeyCredentialRpEntity rp;
+ required PublicKeyCredentialUserEntity user;
+
+ required BufferSource challenge;
+ required sequence<PublicKeyCredentialParameters> pubKeyCredParams;
+
+ unsigned long timeout;
+ sequence<PublicKeyCredentialDescriptor> excludeCredentials = [];
+ AuthenticatorSelectionCriteria authenticatorSelection;
+ DOMString attestation = "none";
+ sequence<DOMString> attestationFormats = [];
+ AuthenticationExtensionsClientInputs extensions;
+};
+
+dictionary PublicKeyCredentialEntity {
+ required DOMString name;
+};
+
+dictionary PublicKeyCredentialRpEntity : PublicKeyCredentialEntity {
+ DOMString id;
+};
+
+dictionary PublicKeyCredentialUserEntity : PublicKeyCredentialEntity {
+ required BufferSource id;
+ required DOMString displayName;
+};
+
+dictionary AuthenticatorSelectionCriteria {
+ DOMString authenticatorAttachment;
+ DOMString residentKey;
+ boolean requireResidentKey = false;
+ DOMString userVerification = "preferred";
+};
+
+enum AuthenticatorAttachment {
+ "platform",
+ "cross-platform"
+};
+
+enum ResidentKeyRequirement {
+ "discouraged",
+ "preferred",
+ "required"
+};
+
+enum AttestationConveyancePreference {
+ "none",
+ "indirect",
+ "direct",
+ "enterprise"
+};
+
+dictionary PublicKeyCredentialRequestOptions {
+ required BufferSource challenge;
+ unsigned long timeout;
+ USVString rpId;
+ sequence<PublicKeyCredentialDescriptor> allowCredentials = [];
+ DOMString userVerification = "preferred";
+ DOMString attestation = "none";
+ sequence<DOMString> attestationFormats = [];
+ AuthenticationExtensionsClientInputs extensions;
+};
+
+dictionary AuthenticationExtensionsClientInputs {
+};
+
+dictionary AuthenticationExtensionsClientOutputs {
+};
+
+dictionary CollectedClientData {
+ required DOMString type;
+ required DOMString challenge;
+ required DOMString origin;
+ DOMString topOrigin;
+ boolean crossOrigin;
+};
+
+dictionary TokenBinding {
+ required DOMString status;
+ DOMString id;
+};
+
+enum TokenBindingStatus { "present", "supported" };
+
+enum PublicKeyCredentialType {
+ "public-key"
+};
+
+dictionary PublicKeyCredentialDescriptor {
+ required DOMString type;
+ required BufferSource id;
+ sequence<DOMString> transports;
+};
+
+enum AuthenticatorTransport {
+ "usb",
+ "nfc",
+ "ble",
+ "smart-card",
+ "hybrid",
+ "internal"
+};
+
+typedef long COSEAlgorithmIdentifier;
+
+enum UserVerificationRequirement {
+ "required",
+ "preferred",
+ "discouraged"
+};
+
+partial dictionary AuthenticationExtensionsClientInputs {
+ USVString appid;
+};
+
+partial dictionary AuthenticationExtensionsClientOutputs {
+ boolean appid;
+};
+
+partial dictionary AuthenticationExtensionsClientInputs {
+ USVString appidExclude;
+};
+
+partial dictionary AuthenticationExtensionsClientOutputs {
+ boolean appidExclude;
+};
+
+partial dictionary AuthenticationExtensionsClientInputs {
+ boolean credProps;
+};
+
+dictionary CredentialPropertiesOutput {
+ boolean rk;
+};
+
+partial dictionary AuthenticationExtensionsClientOutputs {
+ CredentialPropertiesOutput credProps;
+};
+
+dictionary AuthenticationExtensionsPRFValues {
+ required BufferSource first;
+ BufferSource second;
+};
+
+dictionary AuthenticationExtensionsPRFInputs {
+ AuthenticationExtensionsPRFValues eval;
+ record<USVString, AuthenticationExtensionsPRFValues> evalByCredential;
+};
+
+partial dictionary AuthenticationExtensionsClientInputs {
+ AuthenticationExtensionsPRFInputs prf;
+};
+
+dictionary AuthenticationExtensionsPRFOutputs {
+ boolean enabled;
+ AuthenticationExtensionsPRFValues results;
+};
+
+partial dictionary AuthenticationExtensionsClientOutputs {
+ AuthenticationExtensionsPRFOutputs prf;
+};
+
+partial dictionary AuthenticationExtensionsClientInputs {
+ AuthenticationExtensionsLargeBlobInputs largeBlob;
+};
+
+enum LargeBlobSupport {
+ "required",
+ "preferred",
+};
+
+dictionary AuthenticationExtensionsLargeBlobInputs {
+ DOMString support;
+ boolean read;
+ BufferSource write;
+};
+
+partial dictionary AuthenticationExtensionsClientOutputs {
+ AuthenticationExtensionsLargeBlobOutputs largeBlob;
+};
+
+dictionary AuthenticationExtensionsLargeBlobOutputs {
+ boolean supported;
+ ArrayBuffer blob;
+ boolean written;
+};
+
+partial dictionary AuthenticationExtensionsClientInputs {
+ boolean uvm;
+};
+
+typedef sequence<unsigned long> UvmEntry;
+typedef sequence<UvmEntry> UvmEntries;
+
+partial dictionary AuthenticationExtensionsClientOutputs {
+ UvmEntries uvm;
+};
+
+dictionary AuthenticationExtensionsDevicePublicKeyInputs {
+ DOMString attestation = "none";
+ sequence<DOMString> attestationFormats = [];
+};
+
+partial dictionary AuthenticationExtensionsClientInputs {
+ AuthenticationExtensionsDevicePublicKeyInputs devicePubKey;
+};
+
+dictionary AuthenticationExtensionsDevicePublicKeyOutputs {
+ ArrayBuffer signature;
+};
+
+partial dictionary AuthenticationExtensionsClientOutputs {
+ AuthenticationExtensionsDevicePublicKeyOutputs devicePubKey;
+};
diff --git a/test/wpt/tests/interfaces/webcodecs-aac-codec-registration.idl b/test/wpt/tests/interfaces/webcodecs-aac-codec-registration.idl
new file mode 100644
index 0000000..124a0b0
--- /dev/null
+++ b/test/wpt/tests/interfaces/webcodecs-aac-codec-registration.idl
@@ -0,0 +1,17 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: AAC WebCodecs Registration (https://w3c.github.io/webcodecs/aac_codec_registration.html)
+
+partial dictionary AudioEncoderConfig {
+ AacEncoderConfig aac;
+};
+
+dictionary AacEncoderConfig {
+ AacBitstreamFormat format = "aac";
+};
+
+enum AacBitstreamFormat {
+ "aac",
+ "adts",
+};
diff --git a/test/wpt/tests/interfaces/webcodecs-av1-codec-registration.idl b/test/wpt/tests/interfaces/webcodecs-av1-codec-registration.idl
new file mode 100644
index 0000000..ab20879
--- /dev/null
+++ b/test/wpt/tests/interfaces/webcodecs-av1-codec-registration.idl
@@ -0,0 +1,20 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: AV1 WebCodecs Registration (https://w3c.github.io/webcodecs/av1_codec_registration.html)
+
+partial dictionary VideoEncoderConfig {
+ AV1EncoderConfig av1;
+};
+
+dictionary AV1EncoderConfig {
+ boolean forceScreenContentTools = false;
+};
+
+partial dictionary VideoEncoderEncodeOptions {
+ VideoEncoderEncodeOptionsForAv1 av1;
+};
+
+dictionary VideoEncoderEncodeOptionsForAv1 {
+ unsigned short? quantizer;
+};
diff --git a/test/wpt/tests/interfaces/webcodecs-avc-codec-registration.idl b/test/wpt/tests/interfaces/webcodecs-avc-codec-registration.idl
new file mode 100644
index 0000000..2b952c2
--- /dev/null
+++ b/test/wpt/tests/interfaces/webcodecs-avc-codec-registration.idl
@@ -0,0 +1,25 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: AVC (H.264) WebCodecs Registration (https://w3c.github.io/webcodecs/avc_codec_registration.html)
+
+partial dictionary VideoEncoderConfig {
+ AvcEncoderConfig avc;
+};
+
+dictionary AvcEncoderConfig {
+ AvcBitstreamFormat format = "avc";
+};
+
+enum AvcBitstreamFormat {
+ "annexb",
+ "avc",
+};
+
+partial dictionary VideoEncoderEncodeOptions {
+ VideoEncoderEncodeOptionsForAvc avc;
+};
+
+dictionary VideoEncoderEncodeOptionsForAvc {
+ unsigned short? quantizer;
+};
diff --git a/test/wpt/tests/interfaces/webcodecs-flac-codec-registration.idl b/test/wpt/tests/interfaces/webcodecs-flac-codec-registration.idl
new file mode 100644
index 0000000..0f7e13a
--- /dev/null
+++ b/test/wpt/tests/interfaces/webcodecs-flac-codec-registration.idl
@@ -0,0 +1,13 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: FLAC WebCodecs Registration (https://w3c.github.io/webcodecs/flac_codec_registration.html)
+
+partial dictionary AudioEncoderConfig {
+ FlacEncoderConfig flac;
+};
+
+dictionary FlacEncoderConfig {
+ [EnforceRange] unsigned long blockSize = 0;
+ [EnforceRange] unsigned long compressLevel = 5;
+};
diff --git a/test/wpt/tests/interfaces/webcodecs-hevc-codec-registration.idl b/test/wpt/tests/interfaces/webcodecs-hevc-codec-registration.idl
new file mode 100644
index 0000000..b767db8
--- /dev/null
+++ b/test/wpt/tests/interfaces/webcodecs-hevc-codec-registration.idl
@@ -0,0 +1,17 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: HEVC (H.265) WebCodecs Registration (https://w3c.github.io/webcodecs/hevc_codec_registration.html)
+
+partial dictionary VideoEncoderConfig {
+ HevcEncoderConfig hevc;
+};
+
+dictionary HevcEncoderConfig {
+ HevcBitstreamFormat format = "hevc";
+};
+
+enum HevcBitstreamFormat {
+ "annexb",
+ "hevc",
+};
diff --git a/test/wpt/tests/interfaces/webcodecs-opus-codec-registration.idl b/test/wpt/tests/interfaces/webcodecs-opus-codec-registration.idl
new file mode 100644
index 0000000..0d198a6
--- /dev/null
+++ b/test/wpt/tests/interfaces/webcodecs-opus-codec-registration.idl
@@ -0,0 +1,22 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Opus WebCodecs Registration (https://w3c.github.io/webcodecs/opus_codec_registration.html)
+
+partial dictionary AudioEncoderConfig {
+ OpusEncoderConfig opus;
+};
+
+dictionary OpusEncoderConfig {
+ OpusBitstreamFormat format = "opus";
+ [EnforceRange] unsigned long long frameDuration = 20000;
+ [EnforceRange] unsigned long complexity;
+ [EnforceRange] unsigned long packetlossperc = 0;
+ boolean useinbandfec = false;
+ boolean usedtx = false;
+};
+
+enum OpusBitstreamFormat {
+ "opus",
+ "ogg",
+};
diff --git a/test/wpt/tests/interfaces/webcodecs-vp9-codec-registration.idl b/test/wpt/tests/interfaces/webcodecs-vp9-codec-registration.idl
new file mode 100644
index 0000000..aca64a7
--- /dev/null
+++ b/test/wpt/tests/interfaces/webcodecs-vp9-codec-registration.idl
@@ -0,0 +1,12 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: VP9 WebCodecs Registration (https://w3c.github.io/webcodecs/vp9_codec_registration.html)
+
+partial dictionary VideoEncoderEncodeOptions {
+ VideoEncoderEncodeOptionsForVp9 vp9;
+};
+
+dictionary VideoEncoderEncodeOptionsForVp9 {
+ unsigned short? quantizer;
+};
diff --git a/test/wpt/tests/interfaces/webcodecs.idl b/test/wpt/tests/interfaces/webcodecs.idl
new file mode 100644
index 0000000..0b95dc8
--- /dev/null
+++ b/test/wpt/tests/interfaces/webcodecs.idl
@@ -0,0 +1,501 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: WebCodecs (https://w3c.github.io/webcodecs/)
+
+[Exposed=(Window,DedicatedWorker), SecureContext]
+interface AudioDecoder : EventTarget {
+ constructor(AudioDecoderInit init);
+
+ readonly attribute CodecState state;
+ readonly attribute unsigned long decodeQueueSize;
+ attribute EventHandler ondequeue;
+
+ undefined configure(AudioDecoderConfig config);
+ undefined decode(EncodedAudioChunk chunk);
+ Promise<undefined> flush();
+ undefined reset();
+ undefined close();
+
+ static Promise<AudioDecoderSupport> isConfigSupported(AudioDecoderConfig config);
+};
+
+dictionary AudioDecoderInit {
+ required AudioDataOutputCallback output;
+ required WebCodecsErrorCallback error;
+};
+
+callback AudioDataOutputCallback = undefined(AudioData output);
+
+[Exposed=(Window,DedicatedWorker), SecureContext]
+interface VideoDecoder : EventTarget {
+ constructor(VideoDecoderInit init);
+
+ readonly attribute CodecState state;
+ readonly attribute unsigned long decodeQueueSize;
+ attribute EventHandler ondequeue;
+
+ undefined configure(VideoDecoderConfig config);
+ undefined decode(EncodedVideoChunk chunk);
+ Promise<undefined> flush();
+ undefined reset();
+ undefined close();
+
+ static Promise<VideoDecoderSupport> isConfigSupported(VideoDecoderConfig config);
+};
+
+dictionary VideoDecoderInit {
+ required VideoFrameOutputCallback output;
+ required WebCodecsErrorCallback error;
+};
+
+callback VideoFrameOutputCallback = undefined(VideoFrame output);
+
+[Exposed=(Window,DedicatedWorker), SecureContext]
+interface AudioEncoder : EventTarget {
+ constructor(AudioEncoderInit init);
+
+ readonly attribute CodecState state;
+ readonly attribute unsigned long encodeQueueSize;
+ attribute EventHandler ondequeue;
+
+ undefined configure(AudioEncoderConfig config);
+ undefined encode(AudioData data);
+ Promise<undefined> flush();
+ undefined reset();
+ undefined close();
+
+ static Promise<AudioEncoderSupport> isConfigSupported(AudioEncoderConfig config);
+};
+
+dictionary AudioEncoderInit {
+ required EncodedAudioChunkOutputCallback output;
+ required WebCodecsErrorCallback error;
+};
+
+callback EncodedAudioChunkOutputCallback =
+ undefined (EncodedAudioChunk output,
+ optional EncodedAudioChunkMetadata metadata = {});
+
+dictionary EncodedAudioChunkMetadata {
+ AudioDecoderConfig decoderConfig;
+};
+
+[Exposed=(Window,DedicatedWorker), SecureContext]
+interface VideoEncoder : EventTarget {
+ constructor(VideoEncoderInit init);
+
+ readonly attribute CodecState state;
+ readonly attribute unsigned long encodeQueueSize;
+ attribute EventHandler ondequeue;
+
+ undefined configure(VideoEncoderConfig config);
+ undefined encode(VideoFrame frame, optional VideoEncoderEncodeOptions options = {});
+ Promise<undefined> flush();
+ undefined reset();
+ undefined close();
+
+ static Promise<VideoEncoderSupport> isConfigSupported(VideoEncoderConfig config);
+};
+
+dictionary VideoEncoderInit {
+ required EncodedVideoChunkOutputCallback output;
+ required WebCodecsErrorCallback error;
+};
+
+callback EncodedVideoChunkOutputCallback =
+ undefined (EncodedVideoChunk chunk,
+ optional EncodedVideoChunkMetadata metadata = {});
+
+dictionary EncodedVideoChunkMetadata {
+ VideoDecoderConfig decoderConfig;
+ SvcOutputMetadata svc;
+ BufferSource alphaSideData;
+};
+
+dictionary SvcOutputMetadata {
+ unsigned long temporalLayerId;
+};
+
+dictionary AudioDecoderSupport {
+ boolean supported;
+ AudioDecoderConfig config;
+};
+
+dictionary VideoDecoderSupport {
+ boolean supported;
+ VideoDecoderConfig config;
+};
+
+dictionary AudioEncoderSupport {
+ boolean supported;
+ AudioEncoderConfig config;
+};
+
+dictionary VideoEncoderSupport {
+ boolean supported;
+ VideoEncoderConfig config;
+};
+
+dictionary AudioDecoderConfig {
+ required DOMString codec;
+ [EnforceRange] required unsigned long sampleRate;
+ [EnforceRange] required unsigned long numberOfChannels;
+ BufferSource description;
+};
+
+dictionary VideoDecoderConfig {
+ required DOMString codec;
+ [AllowShared] BufferSource description;
+ [EnforceRange] unsigned long codedWidth;
+ [EnforceRange] unsigned long codedHeight;
+ [EnforceRange] unsigned long displayAspectWidth;
+ [EnforceRange] unsigned long displayAspectHeight;
+ VideoColorSpaceInit colorSpace;
+ HardwareAcceleration hardwareAcceleration = "no-preference";
+ boolean optimizeForLatency;
+};
+
+dictionary AudioEncoderConfig {
+ required DOMString codec;
+ [EnforceRange] unsigned long sampleRate;
+ [EnforceRange] unsigned long numberOfChannels;
+ [EnforceRange] unsigned long long bitrate;
+ BitrateMode bitrateMode;
+};
+
+dictionary VideoEncoderConfig {
+ required DOMString codec;
+ [EnforceRange] required unsigned long width;
+ [EnforceRange] required unsigned long height;
+ [EnforceRange] unsigned long displayWidth;
+ [EnforceRange] unsigned long displayHeight;
+ [EnforceRange] unsigned long long bitrate;
+ double framerate;
+ HardwareAcceleration hardwareAcceleration = "no-preference";
+ AlphaOption alpha = "discard";
+ DOMString scalabilityMode;
+ VideoEncoderBitrateMode bitrateMode = "variable";
+ LatencyMode latencyMode = "quality";
+};
+
+enum HardwareAcceleration {
+ "no-preference",
+ "prefer-hardware",
+ "prefer-software",
+};
+
+enum AlphaOption {
+ "keep",
+ "discard",
+};
+
+enum LatencyMode {
+ "quality",
+ "realtime"
+};
+
+dictionary VideoEncoderEncodeOptions {
+ boolean keyFrame = false;
+};
+
+enum VideoEncoderBitrateMode {
+ "constant",
+ "variable",
+ "quantizer"
+};
+
+enum CodecState {
+ "unconfigured",
+ "configured",
+ "closed"
+};
+
+callback WebCodecsErrorCallback = undefined(DOMException error);
+
+[Exposed=(Window,DedicatedWorker), Serializable]
+interface EncodedAudioChunk {
+ constructor(EncodedAudioChunkInit init);
+ readonly attribute EncodedAudioChunkType type;
+ readonly attribute long long timestamp; // microseconds
+ readonly attribute unsigned long long? duration; // microseconds
+ readonly attribute unsigned long byteLength;
+
+ undefined copyTo([AllowShared] BufferSource destination);
+};
+
+dictionary EncodedAudioChunkInit {
+ required EncodedAudioChunkType type;
+ [EnforceRange] required long long timestamp; // microseconds
+ [EnforceRange] unsigned long long duration; // microseconds
+ required BufferSource data;
+};
+
+enum EncodedAudioChunkType {
+ "key",
+ "delta",
+};
+
+[Exposed=(Window,DedicatedWorker), Serializable]
+interface EncodedVideoChunk {
+ constructor(EncodedVideoChunkInit init);
+ readonly attribute EncodedVideoChunkType type;
+ readonly attribute long long timestamp; // microseconds
+ readonly attribute unsigned long long? duration; // microseconds
+ readonly attribute unsigned long byteLength;
+
+ undefined copyTo([AllowShared] BufferSource destination);
+};
+
+dictionary EncodedVideoChunkInit {
+ required EncodedVideoChunkType type;
+ [EnforceRange] required long long timestamp; // microseconds
+ [EnforceRange] unsigned long long duration; // microseconds
+ required [AllowShared] BufferSource data;
+};
+
+enum EncodedVideoChunkType {
+ "key",
+ "delta",
+};
+
+[Exposed=(Window,DedicatedWorker), Serializable, Transferable]
+interface AudioData {
+ constructor(AudioDataInit init);
+
+ readonly attribute AudioSampleFormat? format;
+ readonly attribute float sampleRate;
+ readonly attribute unsigned long numberOfFrames;
+ readonly attribute unsigned long numberOfChannels;
+ readonly attribute unsigned long long duration; // microseconds
+ readonly attribute long long timestamp; // microseconds
+
+ unsigned long allocationSize(AudioDataCopyToOptions options);
+ undefined copyTo([AllowShared] BufferSource destination, AudioDataCopyToOptions options);
+ AudioData clone();
+ undefined close();
+};
+
+dictionary AudioDataInit {
+ required AudioSampleFormat format;
+ required float sampleRate;
+ [EnforceRange] required unsigned long numberOfFrames;
+ [EnforceRange] required unsigned long numberOfChannels;
+ [EnforceRange] required long long timestamp; // microseconds
+ required BufferSource data;
+};
+
+dictionary AudioDataCopyToOptions {
+ [EnforceRange] required unsigned long planeIndex;
+ [EnforceRange] unsigned long frameOffset = 0;
+ [EnforceRange] unsigned long frameCount;
+ AudioSampleFormat format;
+};
+
+enum AudioSampleFormat {
+ "u8",
+ "s16",
+ "s32",
+ "f32",
+ "u8-planar",
+ "s16-planar",
+ "s32-planar",
+ "f32-planar",
+};
+
+[Exposed=(Window,DedicatedWorker), Serializable, Transferable]
+interface VideoFrame {
+ constructor(CanvasImageSource image, optional VideoFrameInit init = {});
+ constructor([AllowShared] BufferSource data, VideoFrameBufferInit init);
+
+ readonly attribute VideoPixelFormat? format;
+ readonly attribute unsigned long codedWidth;
+ readonly attribute unsigned long codedHeight;
+ readonly attribute DOMRectReadOnly? codedRect;
+ readonly attribute DOMRectReadOnly? visibleRect;
+ readonly attribute unsigned long displayWidth;
+ readonly attribute unsigned long displayHeight;
+ readonly attribute unsigned long long? duration; // microseconds
+ readonly attribute long long timestamp; // microseconds
+ readonly attribute VideoColorSpace colorSpace;
+
+ VideoFrameMetadata metadata();
+
+ unsigned long allocationSize(
+ optional VideoFrameCopyToOptions options = {});
+ Promise<sequence<PlaneLayout>> copyTo(
+ [AllowShared] BufferSource destination,
+ optional VideoFrameCopyToOptions options = {});
+ VideoFrame clone();
+ undefined close();
+};
+
+dictionary VideoFrameInit {
+ unsigned long long duration; // microseconds
+ long long timestamp; // microseconds
+ AlphaOption alpha = "keep";
+
+ // Default matches image. May be used to efficiently crop. Will trigger
+ // new computation of displayWidth and displayHeight using image’s pixel
+ // aspect ratio unless an explicit displayWidth and displayHeight are given.
+ DOMRectInit visibleRect;
+
+ // Default matches image unless visibleRect is provided.
+ [EnforceRange] unsigned long displayWidth;
+ [EnforceRange] unsigned long displayHeight;
+
+ VideoFrameMetadata metadata;
+};
+
+dictionary VideoFrameBufferInit {
+ required VideoPixelFormat format;
+ required [EnforceRange] unsigned long codedWidth;
+ required [EnforceRange] unsigned long codedHeight;
+ required [EnforceRange] long long timestamp; // microseconds
+ [EnforceRange] unsigned long long duration; // microseconds
+
+ // Default layout is tightly-packed.
+ sequence<PlaneLayout> layout;
+
+ // Default visible rect is coded size positioned at (0,0)
+ DOMRectInit visibleRect;
+
+ // Default display dimensions match visibleRect.
+ [EnforceRange] unsigned long displayWidth;
+ [EnforceRange] unsigned long displayHeight;
+
+ VideoColorSpaceInit colorSpace;
+};
+
+dictionary VideoFrameMetadata {
+ // Possible members are recorded in the VideoFrame Metadata Registry.
+};
+
+dictionary VideoFrameCopyToOptions {
+ DOMRectInit rect;
+ sequence<PlaneLayout> layout;
+};
+
+dictionary PlaneLayout {
+ [EnforceRange] required unsigned long offset;
+ [EnforceRange] required unsigned long stride;
+};
+
+enum VideoPixelFormat {
+ // 4:2:0 Y, U, V
+ "I420",
+ // 4:2:0 Y, U, V, A
+ "I420A",
+ // 4:2:2 Y, U, V
+ "I422",
+ // 4:4:4 Y, U, V
+ "I444",
+ // 4:2:0 Y, UV
+ "NV12",
+ // 32bpp RGBA
+ "RGBA",
+ // 32bpp RGBX (opaque)
+ "RGBX",
+ // 32bpp BGRA
+ "BGRA",
+ // 32bpp BGRX (opaque)
+ "BGRX",
+};
+
+[Exposed=(Window,DedicatedWorker)]
+interface VideoColorSpace {
+ constructor(optional VideoColorSpaceInit init = {});
+
+ readonly attribute VideoColorPrimaries? primaries;
+ readonly attribute VideoTransferCharacteristics? transfer;
+ readonly attribute VideoMatrixCoefficients? matrix;
+ readonly attribute boolean? fullRange;
+
+ [Default] VideoColorSpaceInit toJSON();
+};
+
+dictionary VideoColorSpaceInit {
+ VideoColorPrimaries? primaries = null;
+ VideoTransferCharacteristics? transfer = null;
+ VideoMatrixCoefficients? matrix = null;
+ boolean? fullRange = null;
+};
+
+enum VideoColorPrimaries {
+ "bt709",
+ "bt470bg",
+ "smpte170m",
+ "bt2020",
+ "smpte432",
+};
+
+enum VideoTransferCharacteristics {
+ "bt709",
+ "smpte170m",
+ "iec61966-2-1",
+ "linear",
+ "pq",
+ "hlg",
+};
+
+enum VideoMatrixCoefficients {
+ "rgb",
+ "bt709",
+ "bt470bg",
+ "smpte170m",
+ "bt2020-ncl",
+};
+
+[Exposed=(Window,DedicatedWorker), SecureContext]
+interface ImageDecoder {
+ constructor(ImageDecoderInit init);
+
+ readonly attribute DOMString type;
+ readonly attribute boolean complete;
+ readonly attribute Promise<undefined> completed;
+ readonly attribute ImageTrackList tracks;
+
+ Promise<ImageDecodeResult> decode(optional ImageDecodeOptions options = {});
+ undefined reset();
+ undefined close();
+
+ static Promise<boolean> isTypeSupported(DOMString type);
+};
+
+typedef (BufferSource or ReadableStream) ImageBufferSource;
+dictionary ImageDecoderInit {
+ required DOMString type;
+ required ImageBufferSource data;
+ ColorSpaceConversion colorSpaceConversion = "default";
+ [EnforceRange] unsigned long desiredWidth;
+ [EnforceRange] unsigned long desiredHeight;
+ boolean preferAnimation;
+};
+
+dictionary ImageDecodeOptions {
+ [EnforceRange] unsigned long frameIndex = 0;
+ boolean completeFramesOnly = true;
+};
+
+dictionary ImageDecodeResult {
+ required VideoFrame image;
+ required boolean complete;
+};
+
+[Exposed=(Window,DedicatedWorker)]
+interface ImageTrackList {
+ getter ImageTrack (unsigned long index);
+
+ readonly attribute Promise<undefined> ready;
+ readonly attribute unsigned long length;
+ readonly attribute long selectedIndex;
+ readonly attribute ImageTrack? selectedTrack;
+};
+
+[Exposed=(Window,DedicatedWorker)]
+interface ImageTrack {
+ readonly attribute boolean animated;
+ readonly attribute unsigned long frameCount;
+ readonly attribute unrestricted float repetitionCount;
+ attribute boolean selected;
+};
diff --git a/test/wpt/tests/interfaces/webcrypto-secure-curves.idl b/test/wpt/tests/interfaces/webcrypto-secure-curves.idl
new file mode 100644
index 0000000..01bb290
--- /dev/null
+++ b/test/wpt/tests/interfaces/webcrypto-secure-curves.idl
@@ -0,0 +1,8 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Secure Curves in the Web Cryptography API (https://wicg.github.io/webcrypto-secure-curves/)
+
+dictionary Ed448Params : Algorithm {
+ BufferSource context;
+};
diff --git a/test/wpt/tests/interfaces/webdriver.idl b/test/wpt/tests/interfaces/webdriver.idl
new file mode 100644
index 0000000..194e2d8
--- /dev/null
+++ b/test/wpt/tests/interfaces/webdriver.idl
@@ -0,0 +1,9 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: WebDriver (https://w3c.github.io/webdriver/)
+
+interface mixin NavigatorAutomationInformation {
+ readonly attribute boolean webdriver;
+};
+Navigator includes NavigatorAutomationInformation;
diff --git a/test/wpt/tests/interfaces/webgl1.idl b/test/wpt/tests/interfaces/webgl1.idl
new file mode 100644
index 0000000..3912e9a
--- /dev/null
+++ b/test/wpt/tests/interfaces/webgl1.idl
@@ -0,0 +1,745 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: WebGL Specification (https://registry.khronos.org/webgl/specs/latest/1.0/)
+
+typedef unsigned long GLenum;
+typedef boolean GLboolean;
+typedef unsigned long GLbitfield;
+typedef byte GLbyte; /* 'byte' should be a signed 8 bit type. */
+typedef short GLshort;
+typedef long GLint;
+typedef long GLsizei;
+typedef long long GLintptr;
+typedef long long GLsizeiptr;
+// Ideally the typedef below would use 'unsigned byte', but that doesn't currently exist in Web IDL.
+typedef octet GLubyte; /* 'octet' should be an unsigned 8 bit type. */
+typedef unsigned short GLushort;
+typedef unsigned long GLuint;
+typedef unrestricted float GLfloat;
+typedef unrestricted float GLclampf;
+
+// The power preference settings are documented in the WebGLContextAttributes
+// section of the specification.
+enum WebGLPowerPreference { "default", "low-power", "high-performance" };
+
+dictionary WebGLContextAttributes {
+ boolean alpha = true;
+ boolean depth = true;
+ boolean stencil = false;
+ boolean antialias = true;
+ boolean premultipliedAlpha = true;
+ boolean preserveDrawingBuffer = false;
+ WebGLPowerPreference powerPreference = "default";
+ boolean failIfMajorPerformanceCaveat = false;
+ boolean desynchronized = false;
+};
+
+[Exposed=(Window,Worker)]
+interface WebGLObject {
+};
+
+[Exposed=(Window,Worker)]
+interface WebGLBuffer : WebGLObject {
+};
+
+[Exposed=(Window,Worker)]
+interface WebGLFramebuffer : WebGLObject {
+};
+
+[Exposed=(Window,Worker)]
+interface WebGLProgram : WebGLObject {
+};
+
+[Exposed=(Window,Worker)]
+interface WebGLRenderbuffer : WebGLObject {
+};
+
+[Exposed=(Window,Worker)]
+interface WebGLShader : WebGLObject {
+};
+
+[Exposed=(Window,Worker)]
+interface WebGLTexture : WebGLObject {
+};
+
+[Exposed=(Window,Worker)]
+interface WebGLUniformLocation {
+};
+
+[Exposed=(Window,Worker)]
+interface WebGLActiveInfo {
+ readonly attribute GLint size;
+ readonly attribute GLenum type;
+ readonly attribute DOMString name;
+};
+
+[Exposed=(Window,Worker)]
+interface WebGLShaderPrecisionFormat {
+ readonly attribute GLint rangeMin;
+ readonly attribute GLint rangeMax;
+ readonly attribute GLint precision;
+};
+
+typedef (ImageBitmap or
+ ImageData or
+ HTMLImageElement or
+ HTMLCanvasElement or
+ HTMLVideoElement or
+ OffscreenCanvas or
+ VideoFrame) TexImageSource;
+
+typedef ([AllowShared] Float32Array or sequence<GLfloat>) Float32List;
+typedef ([AllowShared] Int32Array or sequence<GLint>) Int32List;
+
+interface mixin WebGLRenderingContextBase
+{
+
+ /* ClearBufferMask */
+ const GLenum DEPTH_BUFFER_BIT = 0x00000100;
+ const GLenum STENCIL_BUFFER_BIT = 0x00000400;
+ const GLenum COLOR_BUFFER_BIT = 0x00004000;
+
+ /* BeginMode */
+ const GLenum POINTS = 0x0000;
+ const GLenum LINES = 0x0001;
+ const GLenum LINE_LOOP = 0x0002;
+ const GLenum LINE_STRIP = 0x0003;
+ const GLenum TRIANGLES = 0x0004;
+ const GLenum TRIANGLE_STRIP = 0x0005;
+ const GLenum TRIANGLE_FAN = 0x0006;
+
+ /* AlphaFunction (not supported in ES20) */
+ /* NEVER */
+ /* LESS */
+ /* EQUAL */
+ /* LEQUAL */
+ /* GREATER */
+ /* NOTEQUAL */
+ /* GEQUAL */
+ /* ALWAYS */
+
+ /* BlendingFactorDest */
+ const GLenum ZERO = 0;
+ const GLenum ONE = 1;
+ const GLenum SRC_COLOR = 0x0300;
+ const GLenum ONE_MINUS_SRC_COLOR = 0x0301;
+ const GLenum SRC_ALPHA = 0x0302;
+ const GLenum ONE_MINUS_SRC_ALPHA = 0x0303;
+ const GLenum DST_ALPHA = 0x0304;
+ const GLenum ONE_MINUS_DST_ALPHA = 0x0305;
+
+ /* BlendingFactorSrc */
+ /* ZERO */
+ /* ONE */
+ const GLenum DST_COLOR = 0x0306;
+ const GLenum ONE_MINUS_DST_COLOR = 0x0307;
+ const GLenum SRC_ALPHA_SATURATE = 0x0308;
+ /* SRC_ALPHA */
+ /* ONE_MINUS_SRC_ALPHA */
+ /* DST_ALPHA */
+ /* ONE_MINUS_DST_ALPHA */
+
+ /* BlendEquationSeparate */
+ const GLenum FUNC_ADD = 0x8006;
+ const GLenum BLEND_EQUATION = 0x8009;
+ const GLenum BLEND_EQUATION_RGB = 0x8009; /* same as BLEND_EQUATION */
+ const GLenum BLEND_EQUATION_ALPHA = 0x883D;
+
+ /* BlendSubtract */
+ const GLenum FUNC_SUBTRACT = 0x800A;
+ const GLenum FUNC_REVERSE_SUBTRACT = 0x800B;
+
+ /* Separate Blend Functions */
+ const GLenum BLEND_DST_RGB = 0x80C8;
+ const GLenum BLEND_SRC_RGB = 0x80C9;
+ const GLenum BLEND_DST_ALPHA = 0x80CA;
+ const GLenum BLEND_SRC_ALPHA = 0x80CB;
+ const GLenum CONSTANT_COLOR = 0x8001;
+ const GLenum ONE_MINUS_CONSTANT_COLOR = 0x8002;
+ const GLenum CONSTANT_ALPHA = 0x8003;
+ const GLenum ONE_MINUS_CONSTANT_ALPHA = 0x8004;
+ const GLenum BLEND_COLOR = 0x8005;
+
+ /* Buffer Objects */
+ const GLenum ARRAY_BUFFER = 0x8892;
+ const GLenum ELEMENT_ARRAY_BUFFER = 0x8893;
+ const GLenum ARRAY_BUFFER_BINDING = 0x8894;
+ const GLenum ELEMENT_ARRAY_BUFFER_BINDING = 0x8895;
+
+ const GLenum STREAM_DRAW = 0x88E0;
+ const GLenum STATIC_DRAW = 0x88E4;
+ const GLenum DYNAMIC_DRAW = 0x88E8;
+
+ const GLenum BUFFER_SIZE = 0x8764;
+ const GLenum BUFFER_USAGE = 0x8765;
+
+ const GLenum CURRENT_VERTEX_ATTRIB = 0x8626;
+
+ /* CullFaceMode */
+ const GLenum FRONT = 0x0404;
+ const GLenum BACK = 0x0405;
+ const GLenum FRONT_AND_BACK = 0x0408;
+
+ /* DepthFunction */
+ /* NEVER */
+ /* LESS */
+ /* EQUAL */
+ /* LEQUAL */
+ /* GREATER */
+ /* NOTEQUAL */
+ /* GEQUAL */
+ /* ALWAYS */
+
+ /* EnableCap */
+ /* TEXTURE_2D */
+ const GLenum CULL_FACE = 0x0B44;
+ const GLenum BLEND = 0x0BE2;
+ const GLenum DITHER = 0x0BD0;
+ const GLenum STENCIL_TEST = 0x0B90;
+ const GLenum DEPTH_TEST = 0x0B71;
+ const GLenum SCISSOR_TEST = 0x0C11;
+ const GLenum POLYGON_OFFSET_FILL = 0x8037;
+ const GLenum SAMPLE_ALPHA_TO_COVERAGE = 0x809E;
+ const GLenum SAMPLE_COVERAGE = 0x80A0;
+
+ /* ErrorCode */
+ const GLenum NO_ERROR = 0;
+ const GLenum INVALID_ENUM = 0x0500;
+ const GLenum INVALID_VALUE = 0x0501;
+ const GLenum INVALID_OPERATION = 0x0502;
+ const GLenum OUT_OF_MEMORY = 0x0505;
+
+ /* FrontFaceDirection */
+ const GLenum CW = 0x0900;
+ const GLenum CCW = 0x0901;
+
+ /* GetPName */
+ const GLenum LINE_WIDTH = 0x0B21;
+ const GLenum ALIASED_POINT_SIZE_RANGE = 0x846D;
+ const GLenum ALIASED_LINE_WIDTH_RANGE = 0x846E;
+ const GLenum CULL_FACE_MODE = 0x0B45;
+ const GLenum FRONT_FACE = 0x0B46;
+ const GLenum DEPTH_RANGE = 0x0B70;
+ const GLenum DEPTH_WRITEMASK = 0x0B72;
+ const GLenum DEPTH_CLEAR_VALUE = 0x0B73;
+ const GLenum DEPTH_FUNC = 0x0B74;
+ const GLenum STENCIL_CLEAR_VALUE = 0x0B91;
+ const GLenum STENCIL_FUNC = 0x0B92;
+ const GLenum STENCIL_FAIL = 0x0B94;
+ const GLenum STENCIL_PASS_DEPTH_FAIL = 0x0B95;
+ const GLenum STENCIL_PASS_DEPTH_PASS = 0x0B96;
+ const GLenum STENCIL_REF = 0x0B97;
+ const GLenum STENCIL_VALUE_MASK = 0x0B93;
+ const GLenum STENCIL_WRITEMASK = 0x0B98;
+ const GLenum STENCIL_BACK_FUNC = 0x8800;
+ const GLenum STENCIL_BACK_FAIL = 0x8801;
+ const GLenum STENCIL_BACK_PASS_DEPTH_FAIL = 0x8802;
+ const GLenum STENCIL_BACK_PASS_DEPTH_PASS = 0x8803;
+ const GLenum STENCIL_BACK_REF = 0x8CA3;
+ const GLenum STENCIL_BACK_VALUE_MASK = 0x8CA4;
+ const GLenum STENCIL_BACK_WRITEMASK = 0x8CA5;
+ const GLenum VIEWPORT = 0x0BA2;
+ const GLenum SCISSOR_BOX = 0x0C10;
+ /* SCISSOR_TEST */
+ const GLenum COLOR_CLEAR_VALUE = 0x0C22;
+ const GLenum COLOR_WRITEMASK = 0x0C23;
+ const GLenum UNPACK_ALIGNMENT = 0x0CF5;
+ const GLenum PACK_ALIGNMENT = 0x0D05;
+ const GLenum MAX_TEXTURE_SIZE = 0x0D33;
+ const GLenum MAX_VIEWPORT_DIMS = 0x0D3A;
+ const GLenum SUBPIXEL_BITS = 0x0D50;
+ const GLenum RED_BITS = 0x0D52;
+ const GLenum GREEN_BITS = 0x0D53;
+ const GLenum BLUE_BITS = 0x0D54;
+ const GLenum ALPHA_BITS = 0x0D55;
+ const GLenum DEPTH_BITS = 0x0D56;
+ const GLenum STENCIL_BITS = 0x0D57;
+ const GLenum POLYGON_OFFSET_UNITS = 0x2A00;
+ /* POLYGON_OFFSET_FILL */
+ const GLenum POLYGON_OFFSET_FACTOR = 0x8038;
+ const GLenum TEXTURE_BINDING_2D = 0x8069;
+ const GLenum SAMPLE_BUFFERS = 0x80A8;
+ const GLenum SAMPLES = 0x80A9;
+ const GLenum SAMPLE_COVERAGE_VALUE = 0x80AA;
+ const GLenum SAMPLE_COVERAGE_INVERT = 0x80AB;
+
+ /* GetTextureParameter */
+ /* TEXTURE_MAG_FILTER */
+ /* TEXTURE_MIN_FILTER */
+ /* TEXTURE_WRAP_S */
+ /* TEXTURE_WRAP_T */
+
+ const GLenum COMPRESSED_TEXTURE_FORMATS = 0x86A3;
+
+ /* HintMode */
+ const GLenum DONT_CARE = 0x1100;
+ const GLenum FASTEST = 0x1101;
+ const GLenum NICEST = 0x1102;
+
+ /* HintTarget */
+ const GLenum GENERATE_MIPMAP_HINT = 0x8192;
+
+ /* DataType */
+ const GLenum BYTE = 0x1400;
+ const GLenum UNSIGNED_BYTE = 0x1401;
+ const GLenum SHORT = 0x1402;
+ const GLenum UNSIGNED_SHORT = 0x1403;
+ const GLenum INT = 0x1404;
+ const GLenum UNSIGNED_INT = 0x1405;
+ const GLenum FLOAT = 0x1406;
+
+ /* PixelFormat */
+ const GLenum DEPTH_COMPONENT = 0x1902;
+ const GLenum ALPHA = 0x1906;
+ const GLenum RGB = 0x1907;
+ const GLenum RGBA = 0x1908;
+ const GLenum LUMINANCE = 0x1909;
+ const GLenum LUMINANCE_ALPHA = 0x190A;
+
+ /* PixelType */
+ /* UNSIGNED_BYTE */
+ const GLenum UNSIGNED_SHORT_4_4_4_4 = 0x8033;
+ const GLenum UNSIGNED_SHORT_5_5_5_1 = 0x8034;
+ const GLenum UNSIGNED_SHORT_5_6_5 = 0x8363;
+
+ /* Shaders */
+ const GLenum FRAGMENT_SHADER = 0x8B30;
+ const GLenum VERTEX_SHADER = 0x8B31;
+ const GLenum MAX_VERTEX_ATTRIBS = 0x8869;
+ const GLenum MAX_VERTEX_UNIFORM_VECTORS = 0x8DFB;
+ const GLenum MAX_VARYING_VECTORS = 0x8DFC;
+ const GLenum MAX_COMBINED_TEXTURE_IMAGE_UNITS = 0x8B4D;
+ const GLenum MAX_VERTEX_TEXTURE_IMAGE_UNITS = 0x8B4C;
+ const GLenum MAX_TEXTURE_IMAGE_UNITS = 0x8872;
+ const GLenum MAX_FRAGMENT_UNIFORM_VECTORS = 0x8DFD;
+ const GLenum SHADER_TYPE = 0x8B4F;
+ const GLenum DELETE_STATUS = 0x8B80;
+ const GLenum LINK_STATUS = 0x8B82;
+ const GLenum VALIDATE_STATUS = 0x8B83;
+ const GLenum ATTACHED_SHADERS = 0x8B85;
+ const GLenum ACTIVE_UNIFORMS = 0x8B86;
+ const GLenum ACTIVE_ATTRIBUTES = 0x8B89;
+ const GLenum SHADING_LANGUAGE_VERSION = 0x8B8C;
+ const GLenum CURRENT_PROGRAM = 0x8B8D;
+
+ /* StencilFunction */
+ const GLenum NEVER = 0x0200;
+ const GLenum LESS = 0x0201;
+ const GLenum EQUAL = 0x0202;
+ const GLenum LEQUAL = 0x0203;
+ const GLenum GREATER = 0x0204;
+ const GLenum NOTEQUAL = 0x0205;
+ const GLenum GEQUAL = 0x0206;
+ const GLenum ALWAYS = 0x0207;
+
+ /* StencilOp */
+ /* ZERO */
+ const GLenum KEEP = 0x1E00;
+ const GLenum REPLACE = 0x1E01;
+ const GLenum INCR = 0x1E02;
+ const GLenum DECR = 0x1E03;
+ const GLenum INVERT = 0x150A;
+ const GLenum INCR_WRAP = 0x8507;
+ const GLenum DECR_WRAP = 0x8508;
+
+ /* StringName */
+ const GLenum VENDOR = 0x1F00;
+ const GLenum RENDERER = 0x1F01;
+ const GLenum VERSION = 0x1F02;
+
+ /* TextureMagFilter */
+ const GLenum NEAREST = 0x2600;
+ const GLenum LINEAR = 0x2601;
+
+ /* TextureMinFilter */
+ /* NEAREST */
+ /* LINEAR */
+ const GLenum NEAREST_MIPMAP_NEAREST = 0x2700;
+ const GLenum LINEAR_MIPMAP_NEAREST = 0x2701;
+ const GLenum NEAREST_MIPMAP_LINEAR = 0x2702;
+ const GLenum LINEAR_MIPMAP_LINEAR = 0x2703;
+
+ /* TextureParameterName */
+ const GLenum TEXTURE_MAG_FILTER = 0x2800;
+ const GLenum TEXTURE_MIN_FILTER = 0x2801;
+ const GLenum TEXTURE_WRAP_S = 0x2802;
+ const GLenum TEXTURE_WRAP_T = 0x2803;
+
+ /* TextureTarget */
+ const GLenum TEXTURE_2D = 0x0DE1;
+ const GLenum TEXTURE = 0x1702;
+
+ const GLenum TEXTURE_CUBE_MAP = 0x8513;
+ const GLenum TEXTURE_BINDING_CUBE_MAP = 0x8514;
+ const GLenum TEXTURE_CUBE_MAP_POSITIVE_X = 0x8515;
+ const GLenum TEXTURE_CUBE_MAP_NEGATIVE_X = 0x8516;
+ const GLenum TEXTURE_CUBE_MAP_POSITIVE_Y = 0x8517;
+ const GLenum TEXTURE_CUBE_MAP_NEGATIVE_Y = 0x8518;
+ const GLenum TEXTURE_CUBE_MAP_POSITIVE_Z = 0x8519;
+ const GLenum TEXTURE_CUBE_MAP_NEGATIVE_Z = 0x851A;
+ const GLenum MAX_CUBE_MAP_TEXTURE_SIZE = 0x851C;
+
+ /* TextureUnit */
+ const GLenum TEXTURE0 = 0x84C0;
+ const GLenum TEXTURE1 = 0x84C1;
+ const GLenum TEXTURE2 = 0x84C2;
+ const GLenum TEXTURE3 = 0x84C3;
+ const GLenum TEXTURE4 = 0x84C4;
+ const GLenum TEXTURE5 = 0x84C5;
+ const GLenum TEXTURE6 = 0x84C6;
+ const GLenum TEXTURE7 = 0x84C7;
+ const GLenum TEXTURE8 = 0x84C8;
+ const GLenum TEXTURE9 = 0x84C9;
+ const GLenum TEXTURE10 = 0x84CA;
+ const GLenum TEXTURE11 = 0x84CB;
+ const GLenum TEXTURE12 = 0x84CC;
+ const GLenum TEXTURE13 = 0x84CD;
+ const GLenum TEXTURE14 = 0x84CE;
+ const GLenum TEXTURE15 = 0x84CF;
+ const GLenum TEXTURE16 = 0x84D0;
+ const GLenum TEXTURE17 = 0x84D1;
+ const GLenum TEXTURE18 = 0x84D2;
+ const GLenum TEXTURE19 = 0x84D3;
+ const GLenum TEXTURE20 = 0x84D4;
+ const GLenum TEXTURE21 = 0x84D5;
+ const GLenum TEXTURE22 = 0x84D6;
+ const GLenum TEXTURE23 = 0x84D7;
+ const GLenum TEXTURE24 = 0x84D8;
+ const GLenum TEXTURE25 = 0x84D9;
+ const GLenum TEXTURE26 = 0x84DA;
+ const GLenum TEXTURE27 = 0x84DB;
+ const GLenum TEXTURE28 = 0x84DC;
+ const GLenum TEXTURE29 = 0x84DD;
+ const GLenum TEXTURE30 = 0x84DE;
+ const GLenum TEXTURE31 = 0x84DF;
+ const GLenum ACTIVE_TEXTURE = 0x84E0;
+
+ /* TextureWrapMode */
+ const GLenum REPEAT = 0x2901;
+ const GLenum CLAMP_TO_EDGE = 0x812F;
+ const GLenum MIRRORED_REPEAT = 0x8370;
+
+ /* Uniform Types */
+ const GLenum FLOAT_VEC2 = 0x8B50;
+ const GLenum FLOAT_VEC3 = 0x8B51;
+ const GLenum FLOAT_VEC4 = 0x8B52;
+ const GLenum INT_VEC2 = 0x8B53;
+ const GLenum INT_VEC3 = 0x8B54;
+ const GLenum INT_VEC4 = 0x8B55;
+ const GLenum BOOL = 0x8B56;
+ const GLenum BOOL_VEC2 = 0x8B57;
+ const GLenum BOOL_VEC3 = 0x8B58;
+ const GLenum BOOL_VEC4 = 0x8B59;
+ const GLenum FLOAT_MAT2 = 0x8B5A;
+ const GLenum FLOAT_MAT3 = 0x8B5B;
+ const GLenum FLOAT_MAT4 = 0x8B5C;
+ const GLenum SAMPLER_2D = 0x8B5E;
+ const GLenum SAMPLER_CUBE = 0x8B60;
+
+ /* Vertex Arrays */
+ const GLenum VERTEX_ATTRIB_ARRAY_ENABLED = 0x8622;
+ const GLenum VERTEX_ATTRIB_ARRAY_SIZE = 0x8623;
+ const GLenum VERTEX_ATTRIB_ARRAY_STRIDE = 0x8624;
+ const GLenum VERTEX_ATTRIB_ARRAY_TYPE = 0x8625;
+ const GLenum VERTEX_ATTRIB_ARRAY_NORMALIZED = 0x886A;
+ const GLenum VERTEX_ATTRIB_ARRAY_POINTER = 0x8645;
+ const GLenum VERTEX_ATTRIB_ARRAY_BUFFER_BINDING = 0x889F;
+
+ /* Read Format */
+ const GLenum IMPLEMENTATION_COLOR_READ_TYPE = 0x8B9A;
+ const GLenum IMPLEMENTATION_COLOR_READ_FORMAT = 0x8B9B;
+
+ /* Shader Source */
+ const GLenum COMPILE_STATUS = 0x8B81;
+
+ /* Shader Precision-Specified Types */
+ const GLenum LOW_FLOAT = 0x8DF0;
+ const GLenum MEDIUM_FLOAT = 0x8DF1;
+ const GLenum HIGH_FLOAT = 0x8DF2;
+ const GLenum LOW_INT = 0x8DF3;
+ const GLenum MEDIUM_INT = 0x8DF4;
+ const GLenum HIGH_INT = 0x8DF5;
+
+ /* Framebuffer Object. */
+ const GLenum FRAMEBUFFER = 0x8D40;
+ const GLenum RENDERBUFFER = 0x8D41;
+
+ const GLenum RGBA4 = 0x8056;
+ const GLenum RGB5_A1 = 0x8057;
+ const GLenum RGB565 = 0x8D62;
+ const GLenum DEPTH_COMPONENT16 = 0x81A5;
+ const GLenum STENCIL_INDEX8 = 0x8D48;
+ const GLenum DEPTH_STENCIL = 0x84F9;
+
+ const GLenum RENDERBUFFER_WIDTH = 0x8D42;
+ const GLenum RENDERBUFFER_HEIGHT = 0x8D43;
+ const GLenum RENDERBUFFER_INTERNAL_FORMAT = 0x8D44;
+ const GLenum RENDERBUFFER_RED_SIZE = 0x8D50;
+ const GLenum RENDERBUFFER_GREEN_SIZE = 0x8D51;
+ const GLenum RENDERBUFFER_BLUE_SIZE = 0x8D52;
+ const GLenum RENDERBUFFER_ALPHA_SIZE = 0x8D53;
+ const GLenum RENDERBUFFER_DEPTH_SIZE = 0x8D54;
+ const GLenum RENDERBUFFER_STENCIL_SIZE = 0x8D55;
+
+ const GLenum FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE = 0x8CD0;
+ const GLenum FRAMEBUFFER_ATTACHMENT_OBJECT_NAME = 0x8CD1;
+ const GLenum FRAMEBUFFER_ATTACHMENT_TEXTURE_LEVEL = 0x8CD2;
+ const GLenum FRAMEBUFFER_ATTACHMENT_TEXTURE_CUBE_MAP_FACE = 0x8CD3;
+
+ const GLenum COLOR_ATTACHMENT0 = 0x8CE0;
+ const GLenum DEPTH_ATTACHMENT = 0x8D00;
+ const GLenum STENCIL_ATTACHMENT = 0x8D20;
+ const GLenum DEPTH_STENCIL_ATTACHMENT = 0x821A;
+
+ const GLenum NONE = 0;
+
+ const GLenum FRAMEBUFFER_COMPLETE = 0x8CD5;
+ const GLenum FRAMEBUFFER_INCOMPLETE_ATTACHMENT = 0x8CD6;
+ const GLenum FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT = 0x8CD7;
+ const GLenum FRAMEBUFFER_INCOMPLETE_DIMENSIONS = 0x8CD9;
+ const GLenum FRAMEBUFFER_UNSUPPORTED = 0x8CDD;
+
+ const GLenum FRAMEBUFFER_BINDING = 0x8CA6;
+ const GLenum RENDERBUFFER_BINDING = 0x8CA7;
+ const GLenum MAX_RENDERBUFFER_SIZE = 0x84E8;
+
+ const GLenum INVALID_FRAMEBUFFER_OPERATION = 0x0506;
+
+ /* WebGL-specific enums */
+ const GLenum UNPACK_FLIP_Y_WEBGL = 0x9240;
+ const GLenum UNPACK_PREMULTIPLY_ALPHA_WEBGL = 0x9241;
+ const GLenum CONTEXT_LOST_WEBGL = 0x9242;
+ const GLenum UNPACK_COLORSPACE_CONVERSION_WEBGL = 0x9243;
+ const GLenum BROWSER_DEFAULT_WEBGL = 0x9244;
+
+ readonly attribute (HTMLCanvasElement or OffscreenCanvas) canvas;
+ readonly attribute GLsizei drawingBufferWidth;
+ readonly attribute GLsizei drawingBufferHeight;
+ attribute PredefinedColorSpace drawingBufferColorSpace;
+ attribute PredefinedColorSpace unpackColorSpace;
+
+ [WebGLHandlesContextLoss] WebGLContextAttributes? getContextAttributes();
+ [WebGLHandlesContextLoss] boolean isContextLost();
+
+ sequence<DOMString>? getSupportedExtensions();
+ object? getExtension(DOMString name);
+
+ undefined activeTexture(GLenum texture);
+ undefined attachShader(WebGLProgram program, WebGLShader shader);
+ undefined bindAttribLocation(WebGLProgram program, GLuint index, DOMString name);
+ undefined bindBuffer(GLenum target, WebGLBuffer? buffer);
+ undefined bindFramebuffer(GLenum target, WebGLFramebuffer? framebuffer);
+ undefined bindRenderbuffer(GLenum target, WebGLRenderbuffer? renderbuffer);
+ undefined bindTexture(GLenum target, WebGLTexture? texture);
+ undefined blendColor(GLclampf red, GLclampf green, GLclampf blue, GLclampf alpha);
+ undefined blendEquation(GLenum mode);
+ undefined blendEquationSeparate(GLenum modeRGB, GLenum modeAlpha);
+ undefined blendFunc(GLenum sfactor, GLenum dfactor);
+ undefined blendFuncSeparate(GLenum srcRGB, GLenum dstRGB,
+ GLenum srcAlpha, GLenum dstAlpha);
+
+ [WebGLHandlesContextLoss] GLenum checkFramebufferStatus(GLenum target);
+ undefined clear(GLbitfield mask);
+ undefined clearColor(GLclampf red, GLclampf green, GLclampf blue, GLclampf alpha);
+ undefined clearDepth(GLclampf depth);
+ undefined clearStencil(GLint s);
+ undefined colorMask(GLboolean red, GLboolean green, GLboolean blue, GLboolean alpha);
+ undefined compileShader(WebGLShader shader);
+
+ undefined copyTexImage2D(GLenum target, GLint level, GLenum internalformat,
+ GLint x, GLint y, GLsizei width, GLsizei height,
+ GLint border);
+ undefined copyTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset,
+ GLint x, GLint y, GLsizei width, GLsizei height);
+
+ WebGLBuffer? createBuffer();
+ WebGLFramebuffer? createFramebuffer();
+ WebGLProgram? createProgram();
+ WebGLRenderbuffer? createRenderbuffer();
+ WebGLShader? createShader(GLenum type);
+ WebGLTexture? createTexture();
+
+ undefined cullFace(GLenum mode);
+
+ undefined deleteBuffer(WebGLBuffer? buffer);
+ undefined deleteFramebuffer(WebGLFramebuffer? framebuffer);
+ undefined deleteProgram(WebGLProgram? program);
+ undefined deleteRenderbuffer(WebGLRenderbuffer? renderbuffer);
+ undefined deleteShader(WebGLShader? shader);
+ undefined deleteTexture(WebGLTexture? texture);
+
+ undefined depthFunc(GLenum func);
+ undefined depthMask(GLboolean flag);
+ undefined depthRange(GLclampf zNear, GLclampf zFar);
+ undefined detachShader(WebGLProgram program, WebGLShader shader);
+ undefined disable(GLenum cap);
+ undefined disableVertexAttribArray(GLuint index);
+ undefined drawArrays(GLenum mode, GLint first, GLsizei count);
+ undefined drawElements(GLenum mode, GLsizei count, GLenum type, GLintptr offset);
+
+ undefined enable(GLenum cap);
+ undefined enableVertexAttribArray(GLuint index);
+ undefined finish();
+ undefined flush();
+ undefined framebufferRenderbuffer(GLenum target, GLenum attachment,
+ GLenum renderbuffertarget,
+ WebGLRenderbuffer? renderbuffer);
+ undefined framebufferTexture2D(GLenum target, GLenum attachment, GLenum textarget,
+ WebGLTexture? texture, GLint level);
+ undefined frontFace(GLenum mode);
+
+ undefined generateMipmap(GLenum target);
+
+ WebGLActiveInfo? getActiveAttrib(WebGLProgram program, GLuint index);
+ WebGLActiveInfo? getActiveUniform(WebGLProgram program, GLuint index);
+ sequence<WebGLShader>? getAttachedShaders(WebGLProgram program);
+
+ [WebGLHandlesContextLoss] GLint getAttribLocation(WebGLProgram program, DOMString name);
+
+ any getBufferParameter(GLenum target, GLenum pname);
+ any getParameter(GLenum pname);
+
+ [WebGLHandlesContextLoss] GLenum getError();
+
+ any getFramebufferAttachmentParameter(GLenum target, GLenum attachment,
+ GLenum pname);
+ any getProgramParameter(WebGLProgram program, GLenum pname);
+ DOMString? getProgramInfoLog(WebGLProgram program);
+ any getRenderbufferParameter(GLenum target, GLenum pname);
+ any getShaderParameter(WebGLShader shader, GLenum pname);
+ WebGLShaderPrecisionFormat? getShaderPrecisionFormat(GLenum shadertype, GLenum precisiontype);
+ DOMString? getShaderInfoLog(WebGLShader shader);
+
+ DOMString? getShaderSource(WebGLShader shader);
+
+ any getTexParameter(GLenum target, GLenum pname);
+
+ any getUniform(WebGLProgram program, WebGLUniformLocation location);
+
+ WebGLUniformLocation? getUniformLocation(WebGLProgram program, DOMString name);
+
+ any getVertexAttrib(GLuint index, GLenum pname);
+
+ [WebGLHandlesContextLoss] GLintptr getVertexAttribOffset(GLuint index, GLenum pname);
+
+ undefined hint(GLenum target, GLenum mode);
+ [WebGLHandlesContextLoss] GLboolean isBuffer(WebGLBuffer? buffer);
+ [WebGLHandlesContextLoss] GLboolean isEnabled(GLenum cap);
+ [WebGLHandlesContextLoss] GLboolean isFramebuffer(WebGLFramebuffer? framebuffer);
+ [WebGLHandlesContextLoss] GLboolean isProgram(WebGLProgram? program);
+ [WebGLHandlesContextLoss] GLboolean isRenderbuffer(WebGLRenderbuffer? renderbuffer);
+ [WebGLHandlesContextLoss] GLboolean isShader(WebGLShader? shader);
+ [WebGLHandlesContextLoss] GLboolean isTexture(WebGLTexture? texture);
+ undefined lineWidth(GLfloat width);
+ undefined linkProgram(WebGLProgram program);
+ undefined pixelStorei(GLenum pname, GLint param);
+ undefined polygonOffset(GLfloat factor, GLfloat units);
+
+ undefined renderbufferStorage(GLenum target, GLenum internalformat,
+ GLsizei width, GLsizei height);
+ undefined sampleCoverage(GLclampf value, GLboolean invert);
+ undefined scissor(GLint x, GLint y, GLsizei width, GLsizei height);
+
+ undefined shaderSource(WebGLShader shader, DOMString source);
+
+ undefined stencilFunc(GLenum func, GLint ref, GLuint mask);
+ undefined stencilFuncSeparate(GLenum face, GLenum func, GLint ref, GLuint mask);
+ undefined stencilMask(GLuint mask);
+ undefined stencilMaskSeparate(GLenum face, GLuint mask);
+ undefined stencilOp(GLenum fail, GLenum zfail, GLenum zpass);
+ undefined stencilOpSeparate(GLenum face, GLenum fail, GLenum zfail, GLenum zpass);
+
+ undefined texParameterf(GLenum target, GLenum pname, GLfloat param);
+ undefined texParameteri(GLenum target, GLenum pname, GLint param);
+
+ undefined uniform1f(WebGLUniformLocation? location, GLfloat x);
+ undefined uniform2f(WebGLUniformLocation? location, GLfloat x, GLfloat y);
+ undefined uniform3f(WebGLUniformLocation? location, GLfloat x, GLfloat y, GLfloat z);
+ undefined uniform4f(WebGLUniformLocation? location, GLfloat x, GLfloat y, GLfloat z, GLfloat w);
+
+ undefined uniform1i(WebGLUniformLocation? location, GLint x);
+ undefined uniform2i(WebGLUniformLocation? location, GLint x, GLint y);
+ undefined uniform3i(WebGLUniformLocation? location, GLint x, GLint y, GLint z);
+ undefined uniform4i(WebGLUniformLocation? location, GLint x, GLint y, GLint z, GLint w);
+
+ undefined useProgram(WebGLProgram? program);
+ undefined validateProgram(WebGLProgram program);
+
+ undefined vertexAttrib1f(GLuint index, GLfloat x);
+ undefined vertexAttrib2f(GLuint index, GLfloat x, GLfloat y);
+ undefined vertexAttrib3f(GLuint index, GLfloat x, GLfloat y, GLfloat z);
+ undefined vertexAttrib4f(GLuint index, GLfloat x, GLfloat y, GLfloat z, GLfloat w);
+
+ undefined vertexAttrib1fv(GLuint index, Float32List values);
+ undefined vertexAttrib2fv(GLuint index, Float32List values);
+ undefined vertexAttrib3fv(GLuint index, Float32List values);
+ undefined vertexAttrib4fv(GLuint index, Float32List values);
+
+ undefined vertexAttribPointer(GLuint index, GLint size, GLenum type,
+ GLboolean normalized, GLsizei stride, GLintptr offset);
+
+ undefined viewport(GLint x, GLint y, GLsizei width, GLsizei height);
+};
+
+interface mixin WebGLRenderingContextOverloads
+{
+ undefined bufferData(GLenum target, GLsizeiptr size, GLenum usage);
+ undefined bufferData(GLenum target, [AllowShared] BufferSource? data, GLenum usage);
+ undefined bufferSubData(GLenum target, GLintptr offset, [AllowShared] BufferSource data);
+
+ undefined compressedTexImage2D(GLenum target, GLint level, GLenum internalformat,
+ GLsizei width, GLsizei height, GLint border,
+ [AllowShared] ArrayBufferView data);
+ undefined compressedTexSubImage2D(GLenum target, GLint level,
+ GLint xoffset, GLint yoffset,
+ GLsizei width, GLsizei height, GLenum format,
+ [AllowShared] ArrayBufferView data);
+
+ undefined readPixels(GLint x, GLint y, GLsizei width, GLsizei height,
+ GLenum format, GLenum type, [AllowShared] ArrayBufferView? pixels);
+
+ undefined texImage2D(GLenum target, GLint level, GLint internalformat,
+ GLsizei width, GLsizei height, GLint border, GLenum format,
+ GLenum type, [AllowShared] ArrayBufferView? pixels);
+ undefined texImage2D(GLenum target, GLint level, GLint internalformat,
+ GLenum format, GLenum type, TexImageSource source); // May throw DOMException
+
+ undefined texSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset,
+ GLsizei width, GLsizei height,
+ GLenum format, GLenum type, [AllowShared] ArrayBufferView? pixels);
+ undefined texSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset,
+ GLenum format, GLenum type, TexImageSource source); // May throw DOMException
+
+ undefined uniform1fv(WebGLUniformLocation? location, Float32List v);
+ undefined uniform2fv(WebGLUniformLocation? location, Float32List v);
+ undefined uniform3fv(WebGLUniformLocation? location, Float32List v);
+ undefined uniform4fv(WebGLUniformLocation? location, Float32List v);
+
+ undefined uniform1iv(WebGLUniformLocation? location, Int32List v);
+ undefined uniform2iv(WebGLUniformLocation? location, Int32List v);
+ undefined uniform3iv(WebGLUniformLocation? location, Int32List v);
+ undefined uniform4iv(WebGLUniformLocation? location, Int32List v);
+
+ undefined uniformMatrix2fv(WebGLUniformLocation? location, GLboolean transpose, Float32List value);
+ undefined uniformMatrix3fv(WebGLUniformLocation? location, GLboolean transpose, Float32List value);
+ undefined uniformMatrix4fv(WebGLUniformLocation? location, GLboolean transpose, Float32List value);
+};
+
+[Exposed=(Window,Worker)]
+interface WebGLRenderingContext
+{
+};
+WebGLRenderingContext includes WebGLRenderingContextBase;
+WebGLRenderingContext includes WebGLRenderingContextOverloads;
+
+[Exposed=(Window,Worker)]
+interface WebGLContextEvent : Event {
+ constructor(DOMString type, optional WebGLContextEventInit eventInit = {});
+ readonly attribute DOMString statusMessage;
+};
+
+// EventInit is defined in the DOM4 specification.
+dictionary WebGLContextEventInit : EventInit {
+ DOMString statusMessage = "";
+};
diff --git a/test/wpt/tests/interfaces/webgl2.idl b/test/wpt/tests/interfaces/webgl2.idl
new file mode 100644
index 0000000..22e2d6f
--- /dev/null
+++ b/test/wpt/tests/interfaces/webgl2.idl
@@ -0,0 +1,582 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: WebGL 2.0 Specification (https://registry.khronos.org/webgl/specs/latest/2.0/)
+
+typedef long long GLint64;
+typedef unsigned long long GLuint64;
+
+[Exposed=(Window,Worker)]
+interface WebGLQuery : WebGLObject {
+};
+
+[Exposed=(Window,Worker)]
+interface WebGLSampler : WebGLObject {
+};
+
+[Exposed=(Window,Worker)]
+interface WebGLSync : WebGLObject {
+};
+
+[Exposed=(Window,Worker)]
+interface WebGLTransformFeedback : WebGLObject {
+};
+
+[Exposed=(Window,Worker)]
+interface WebGLVertexArrayObject : WebGLObject {
+};
+
+typedef ([AllowShared] Uint32Array or sequence<GLuint>) Uint32List;
+
+interface mixin WebGL2RenderingContextBase
+{
+ const GLenum READ_BUFFER = 0x0C02;
+ const GLenum UNPACK_ROW_LENGTH = 0x0CF2;
+ const GLenum UNPACK_SKIP_ROWS = 0x0CF3;
+ const GLenum UNPACK_SKIP_PIXELS = 0x0CF4;
+ const GLenum PACK_ROW_LENGTH = 0x0D02;
+ const GLenum PACK_SKIP_ROWS = 0x0D03;
+ const GLenum PACK_SKIP_PIXELS = 0x0D04;
+ const GLenum COLOR = 0x1800;
+ const GLenum DEPTH = 0x1801;
+ const GLenum STENCIL = 0x1802;
+ const GLenum RED = 0x1903;
+ const GLenum RGB8 = 0x8051;
+ const GLenum RGBA8 = 0x8058;
+ const GLenum RGB10_A2 = 0x8059;
+ const GLenum TEXTURE_BINDING_3D = 0x806A;
+ const GLenum UNPACK_SKIP_IMAGES = 0x806D;
+ const GLenum UNPACK_IMAGE_HEIGHT = 0x806E;
+ const GLenum TEXTURE_3D = 0x806F;
+ const GLenum TEXTURE_WRAP_R = 0x8072;
+ const GLenum MAX_3D_TEXTURE_SIZE = 0x8073;
+ const GLenum UNSIGNED_INT_2_10_10_10_REV = 0x8368;
+ const GLenum MAX_ELEMENTS_VERTICES = 0x80E8;
+ const GLenum MAX_ELEMENTS_INDICES = 0x80E9;
+ const GLenum TEXTURE_MIN_LOD = 0x813A;
+ const GLenum TEXTURE_MAX_LOD = 0x813B;
+ const GLenum TEXTURE_BASE_LEVEL = 0x813C;
+ const GLenum TEXTURE_MAX_LEVEL = 0x813D;
+ const GLenum MIN = 0x8007;
+ const GLenum MAX = 0x8008;
+ const GLenum DEPTH_COMPONENT24 = 0x81A6;
+ const GLenum MAX_TEXTURE_LOD_BIAS = 0x84FD;
+ const GLenum TEXTURE_COMPARE_MODE = 0x884C;
+ const GLenum TEXTURE_COMPARE_FUNC = 0x884D;
+ const GLenum CURRENT_QUERY = 0x8865;
+ const GLenum QUERY_RESULT = 0x8866;
+ const GLenum QUERY_RESULT_AVAILABLE = 0x8867;
+ const GLenum STREAM_READ = 0x88E1;
+ const GLenum STREAM_COPY = 0x88E2;
+ const GLenum STATIC_READ = 0x88E5;
+ const GLenum STATIC_COPY = 0x88E6;
+ const GLenum DYNAMIC_READ = 0x88E9;
+ const GLenum DYNAMIC_COPY = 0x88EA;
+ const GLenum MAX_DRAW_BUFFERS = 0x8824;
+ const GLenum DRAW_BUFFER0 = 0x8825;
+ const GLenum DRAW_BUFFER1 = 0x8826;
+ const GLenum DRAW_BUFFER2 = 0x8827;
+ const GLenum DRAW_BUFFER3 = 0x8828;
+ const GLenum DRAW_BUFFER4 = 0x8829;
+ const GLenum DRAW_BUFFER5 = 0x882A;
+ const GLenum DRAW_BUFFER6 = 0x882B;
+ const GLenum DRAW_BUFFER7 = 0x882C;
+ const GLenum DRAW_BUFFER8 = 0x882D;
+ const GLenum DRAW_BUFFER9 = 0x882E;
+ const GLenum DRAW_BUFFER10 = 0x882F;
+ const GLenum DRAW_BUFFER11 = 0x8830;
+ const GLenum DRAW_BUFFER12 = 0x8831;
+ const GLenum DRAW_BUFFER13 = 0x8832;
+ const GLenum DRAW_BUFFER14 = 0x8833;
+ const GLenum DRAW_BUFFER15 = 0x8834;
+ const GLenum MAX_FRAGMENT_UNIFORM_COMPONENTS = 0x8B49;
+ const GLenum MAX_VERTEX_UNIFORM_COMPONENTS = 0x8B4A;
+ const GLenum SAMPLER_3D = 0x8B5F;
+ const GLenum SAMPLER_2D_SHADOW = 0x8B62;
+ const GLenum FRAGMENT_SHADER_DERIVATIVE_HINT = 0x8B8B;
+ const GLenum PIXEL_PACK_BUFFER = 0x88EB;
+ const GLenum PIXEL_UNPACK_BUFFER = 0x88EC;
+ const GLenum PIXEL_PACK_BUFFER_BINDING = 0x88ED;
+ const GLenum PIXEL_UNPACK_BUFFER_BINDING = 0x88EF;
+ const GLenum FLOAT_MAT2x3 = 0x8B65;
+ const GLenum FLOAT_MAT2x4 = 0x8B66;
+ const GLenum FLOAT_MAT3x2 = 0x8B67;
+ const GLenum FLOAT_MAT3x4 = 0x8B68;
+ const GLenum FLOAT_MAT4x2 = 0x8B69;
+ const GLenum FLOAT_MAT4x3 = 0x8B6A;
+ const GLenum SRGB = 0x8C40;
+ const GLenum SRGB8 = 0x8C41;
+ const GLenum SRGB8_ALPHA8 = 0x8C43;
+ const GLenum COMPARE_REF_TO_TEXTURE = 0x884E;
+ const GLenum RGBA32F = 0x8814;
+ const GLenum RGB32F = 0x8815;
+ const GLenum RGBA16F = 0x881A;
+ const GLenum RGB16F = 0x881B;
+ const GLenum VERTEX_ATTRIB_ARRAY_INTEGER = 0x88FD;
+ const GLenum MAX_ARRAY_TEXTURE_LAYERS = 0x88FF;
+ const GLenum MIN_PROGRAM_TEXEL_OFFSET = 0x8904;
+ const GLenum MAX_PROGRAM_TEXEL_OFFSET = 0x8905;
+ const GLenum MAX_VARYING_COMPONENTS = 0x8B4B;
+ const GLenum TEXTURE_2D_ARRAY = 0x8C1A;
+ const GLenum TEXTURE_BINDING_2D_ARRAY = 0x8C1D;
+ const GLenum R11F_G11F_B10F = 0x8C3A;
+ const GLenum UNSIGNED_INT_10F_11F_11F_REV = 0x8C3B;
+ const GLenum RGB9_E5 = 0x8C3D;
+ const GLenum UNSIGNED_INT_5_9_9_9_REV = 0x8C3E;
+ const GLenum TRANSFORM_FEEDBACK_BUFFER_MODE = 0x8C7F;
+ const GLenum MAX_TRANSFORM_FEEDBACK_SEPARATE_COMPONENTS = 0x8C80;
+ const GLenum TRANSFORM_FEEDBACK_VARYINGS = 0x8C83;
+ const GLenum TRANSFORM_FEEDBACK_BUFFER_START = 0x8C84;
+ const GLenum TRANSFORM_FEEDBACK_BUFFER_SIZE = 0x8C85;
+ const GLenum TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN = 0x8C88;
+ const GLenum RASTERIZER_DISCARD = 0x8C89;
+ const GLenum MAX_TRANSFORM_FEEDBACK_INTERLEAVED_COMPONENTS = 0x8C8A;
+ const GLenum MAX_TRANSFORM_FEEDBACK_SEPARATE_ATTRIBS = 0x8C8B;
+ const GLenum INTERLEAVED_ATTRIBS = 0x8C8C;
+ const GLenum SEPARATE_ATTRIBS = 0x8C8D;
+ const GLenum TRANSFORM_FEEDBACK_BUFFER = 0x8C8E;
+ const GLenum TRANSFORM_FEEDBACK_BUFFER_BINDING = 0x8C8F;
+ const GLenum RGBA32UI = 0x8D70;
+ const GLenum RGB32UI = 0x8D71;
+ const GLenum RGBA16UI = 0x8D76;
+ const GLenum RGB16UI = 0x8D77;
+ const GLenum RGBA8UI = 0x8D7C;
+ const GLenum RGB8UI = 0x8D7D;
+ const GLenum RGBA32I = 0x8D82;
+ const GLenum RGB32I = 0x8D83;
+ const GLenum RGBA16I = 0x8D88;
+ const GLenum RGB16I = 0x8D89;
+ const GLenum RGBA8I = 0x8D8E;
+ const GLenum RGB8I = 0x8D8F;
+ const GLenum RED_INTEGER = 0x8D94;
+ const GLenum RGB_INTEGER = 0x8D98;
+ const GLenum RGBA_INTEGER = 0x8D99;
+ const GLenum SAMPLER_2D_ARRAY = 0x8DC1;
+ const GLenum SAMPLER_2D_ARRAY_SHADOW = 0x8DC4;
+ const GLenum SAMPLER_CUBE_SHADOW = 0x8DC5;
+ const GLenum UNSIGNED_INT_VEC2 = 0x8DC6;
+ const GLenum UNSIGNED_INT_VEC3 = 0x8DC7;
+ const GLenum UNSIGNED_INT_VEC4 = 0x8DC8;
+ const GLenum INT_SAMPLER_2D = 0x8DCA;
+ const GLenum INT_SAMPLER_3D = 0x8DCB;
+ const GLenum INT_SAMPLER_CUBE = 0x8DCC;
+ const GLenum INT_SAMPLER_2D_ARRAY = 0x8DCF;
+ const GLenum UNSIGNED_INT_SAMPLER_2D = 0x8DD2;
+ const GLenum UNSIGNED_INT_SAMPLER_3D = 0x8DD3;
+ const GLenum UNSIGNED_INT_SAMPLER_CUBE = 0x8DD4;
+ const GLenum UNSIGNED_INT_SAMPLER_2D_ARRAY = 0x8DD7;
+ const GLenum DEPTH_COMPONENT32F = 0x8CAC;
+ const GLenum DEPTH32F_STENCIL8 = 0x8CAD;
+ const GLenum FLOAT_32_UNSIGNED_INT_24_8_REV = 0x8DAD;
+ const GLenum FRAMEBUFFER_ATTACHMENT_COLOR_ENCODING = 0x8210;
+ const GLenum FRAMEBUFFER_ATTACHMENT_COMPONENT_TYPE = 0x8211;
+ const GLenum FRAMEBUFFER_ATTACHMENT_RED_SIZE = 0x8212;
+ const GLenum FRAMEBUFFER_ATTACHMENT_GREEN_SIZE = 0x8213;
+ const GLenum FRAMEBUFFER_ATTACHMENT_BLUE_SIZE = 0x8214;
+ const GLenum FRAMEBUFFER_ATTACHMENT_ALPHA_SIZE = 0x8215;
+ const GLenum FRAMEBUFFER_ATTACHMENT_DEPTH_SIZE = 0x8216;
+ const GLenum FRAMEBUFFER_ATTACHMENT_STENCIL_SIZE = 0x8217;
+ const GLenum FRAMEBUFFER_DEFAULT = 0x8218;
+ const GLenum UNSIGNED_INT_24_8 = 0x84FA;
+ const GLenum DEPTH24_STENCIL8 = 0x88F0;
+ const GLenum UNSIGNED_NORMALIZED = 0x8C17;
+ const GLenum DRAW_FRAMEBUFFER_BINDING = 0x8CA6; /* Same as FRAMEBUFFER_BINDING */
+ const GLenum READ_FRAMEBUFFER = 0x8CA8;
+ const GLenum DRAW_FRAMEBUFFER = 0x8CA9;
+ const GLenum READ_FRAMEBUFFER_BINDING = 0x8CAA;
+ const GLenum RENDERBUFFER_SAMPLES = 0x8CAB;
+ const GLenum FRAMEBUFFER_ATTACHMENT_TEXTURE_LAYER = 0x8CD4;
+ const GLenum MAX_COLOR_ATTACHMENTS = 0x8CDF;
+ const GLenum COLOR_ATTACHMENT1 = 0x8CE1;
+ const GLenum COLOR_ATTACHMENT2 = 0x8CE2;
+ const GLenum COLOR_ATTACHMENT3 = 0x8CE3;
+ const GLenum COLOR_ATTACHMENT4 = 0x8CE4;
+ const GLenum COLOR_ATTACHMENT5 = 0x8CE5;
+ const GLenum COLOR_ATTACHMENT6 = 0x8CE6;
+ const GLenum COLOR_ATTACHMENT7 = 0x8CE7;
+ const GLenum COLOR_ATTACHMENT8 = 0x8CE8;
+ const GLenum COLOR_ATTACHMENT9 = 0x8CE9;
+ const GLenum COLOR_ATTACHMENT10 = 0x8CEA;
+ const GLenum COLOR_ATTACHMENT11 = 0x8CEB;
+ const GLenum COLOR_ATTACHMENT12 = 0x8CEC;
+ const GLenum COLOR_ATTACHMENT13 = 0x8CED;
+ const GLenum COLOR_ATTACHMENT14 = 0x8CEE;
+ const GLenum COLOR_ATTACHMENT15 = 0x8CEF;
+ const GLenum FRAMEBUFFER_INCOMPLETE_MULTISAMPLE = 0x8D56;
+ const GLenum MAX_SAMPLES = 0x8D57;
+ const GLenum HALF_FLOAT = 0x140B;
+ const GLenum RG = 0x8227;
+ const GLenum RG_INTEGER = 0x8228;
+ const GLenum R8 = 0x8229;
+ const GLenum RG8 = 0x822B;
+ const GLenum R16F = 0x822D;
+ const GLenum R32F = 0x822E;
+ const GLenum RG16F = 0x822F;
+ const GLenum RG32F = 0x8230;
+ const GLenum R8I = 0x8231;
+ const GLenum R8UI = 0x8232;
+ const GLenum R16I = 0x8233;
+ const GLenum R16UI = 0x8234;
+ const GLenum R32I = 0x8235;
+ const GLenum R32UI = 0x8236;
+ const GLenum RG8I = 0x8237;
+ const GLenum RG8UI = 0x8238;
+ const GLenum RG16I = 0x8239;
+ const GLenum RG16UI = 0x823A;
+ const GLenum RG32I = 0x823B;
+ const GLenum RG32UI = 0x823C;
+ const GLenum VERTEX_ARRAY_BINDING = 0x85B5;
+ const GLenum R8_SNORM = 0x8F94;
+ const GLenum RG8_SNORM = 0x8F95;
+ const GLenum RGB8_SNORM = 0x8F96;
+ const GLenum RGBA8_SNORM = 0x8F97;
+ const GLenum SIGNED_NORMALIZED = 0x8F9C;
+ const GLenum COPY_READ_BUFFER = 0x8F36;
+ const GLenum COPY_WRITE_BUFFER = 0x8F37;
+ const GLenum COPY_READ_BUFFER_BINDING = 0x8F36; /* Same as COPY_READ_BUFFER */
+ const GLenum COPY_WRITE_BUFFER_BINDING = 0x8F37; /* Same as COPY_WRITE_BUFFER */
+ const GLenum UNIFORM_BUFFER = 0x8A11;
+ const GLenum UNIFORM_BUFFER_BINDING = 0x8A28;
+ const GLenum UNIFORM_BUFFER_START = 0x8A29;
+ const GLenum UNIFORM_BUFFER_SIZE = 0x8A2A;
+ const GLenum MAX_VERTEX_UNIFORM_BLOCKS = 0x8A2B;
+ const GLenum MAX_FRAGMENT_UNIFORM_BLOCKS = 0x8A2D;
+ const GLenum MAX_COMBINED_UNIFORM_BLOCKS = 0x8A2E;
+ const GLenum MAX_UNIFORM_BUFFER_BINDINGS = 0x8A2F;
+ const GLenum MAX_UNIFORM_BLOCK_SIZE = 0x8A30;
+ const GLenum MAX_COMBINED_VERTEX_UNIFORM_COMPONENTS = 0x8A31;
+ const GLenum MAX_COMBINED_FRAGMENT_UNIFORM_COMPONENTS = 0x8A33;
+ const GLenum UNIFORM_BUFFER_OFFSET_ALIGNMENT = 0x8A34;
+ const GLenum ACTIVE_UNIFORM_BLOCKS = 0x8A36;
+ const GLenum UNIFORM_TYPE = 0x8A37;
+ const GLenum UNIFORM_SIZE = 0x8A38;
+ const GLenum UNIFORM_BLOCK_INDEX = 0x8A3A;
+ const GLenum UNIFORM_OFFSET = 0x8A3B;
+ const GLenum UNIFORM_ARRAY_STRIDE = 0x8A3C;
+ const GLenum UNIFORM_MATRIX_STRIDE = 0x8A3D;
+ const GLenum UNIFORM_IS_ROW_MAJOR = 0x8A3E;
+ const GLenum UNIFORM_BLOCK_BINDING = 0x8A3F;
+ const GLenum UNIFORM_BLOCK_DATA_SIZE = 0x8A40;
+ const GLenum UNIFORM_BLOCK_ACTIVE_UNIFORMS = 0x8A42;
+ const GLenum UNIFORM_BLOCK_ACTIVE_UNIFORM_INDICES = 0x8A43;
+ const GLenum UNIFORM_BLOCK_REFERENCED_BY_VERTEX_SHADER = 0x8A44;
+ const GLenum UNIFORM_BLOCK_REFERENCED_BY_FRAGMENT_SHADER = 0x8A46;
+ const GLenum INVALID_INDEX = 0xFFFFFFFF;
+ const GLenum MAX_VERTEX_OUTPUT_COMPONENTS = 0x9122;
+ const GLenum MAX_FRAGMENT_INPUT_COMPONENTS = 0x9125;
+ const GLenum MAX_SERVER_WAIT_TIMEOUT = 0x9111;
+ const GLenum OBJECT_TYPE = 0x9112;
+ const GLenum SYNC_CONDITION = 0x9113;
+ const GLenum SYNC_STATUS = 0x9114;
+ const GLenum SYNC_FLAGS = 0x9115;
+ const GLenum SYNC_FENCE = 0x9116;
+ const GLenum SYNC_GPU_COMMANDS_COMPLETE = 0x9117;
+ const GLenum UNSIGNALED = 0x9118;
+ const GLenum SIGNALED = 0x9119;
+ const GLenum ALREADY_SIGNALED = 0x911A;
+ const GLenum TIMEOUT_EXPIRED = 0x911B;
+ const GLenum CONDITION_SATISFIED = 0x911C;
+ const GLenum WAIT_FAILED = 0x911D;
+ const GLenum SYNC_FLUSH_COMMANDS_BIT = 0x00000001;
+ const GLenum VERTEX_ATTRIB_ARRAY_DIVISOR = 0x88FE;
+ const GLenum ANY_SAMPLES_PASSED = 0x8C2F;
+ const GLenum ANY_SAMPLES_PASSED_CONSERVATIVE = 0x8D6A;
+ const GLenum SAMPLER_BINDING = 0x8919;
+ const GLenum RGB10_A2UI = 0x906F;
+ const GLenum INT_2_10_10_10_REV = 0x8D9F;
+ const GLenum TRANSFORM_FEEDBACK = 0x8E22;
+ const GLenum TRANSFORM_FEEDBACK_PAUSED = 0x8E23;
+ const GLenum TRANSFORM_FEEDBACK_ACTIVE = 0x8E24;
+ const GLenum TRANSFORM_FEEDBACK_BINDING = 0x8E25;
+ const GLenum TEXTURE_IMMUTABLE_FORMAT = 0x912F;
+ const GLenum MAX_ELEMENT_INDEX = 0x8D6B;
+ const GLenum TEXTURE_IMMUTABLE_LEVELS = 0x82DF;
+
+ const GLint64 TIMEOUT_IGNORED = -1;
+
+ /* WebGL-specific enums */
+ const GLenum MAX_CLIENT_WAIT_TIMEOUT_WEBGL = 0x9247;
+
+ /* Buffer objects */
+ undefined copyBufferSubData(GLenum readTarget, GLenum writeTarget, GLintptr readOffset,
+ GLintptr writeOffset, GLsizeiptr size);
+ // MapBufferRange, in particular its read-only and write-only modes,
+ // can not be exposed safely to JavaScript. GetBufferSubData
+ // replaces it for the purpose of fetching data back from the GPU.
+ undefined getBufferSubData(GLenum target, GLintptr srcByteOffset, [AllowShared] ArrayBufferView dstBuffer,
+ optional GLuint dstOffset = 0, optional GLuint length = 0);
+
+ /* Framebuffer objects */
+ undefined blitFramebuffer(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0,
+ GLint dstX1, GLint dstY1, GLbitfield mask, GLenum filter);
+ undefined framebufferTextureLayer(GLenum target, GLenum attachment, WebGLTexture? texture, GLint level,
+ GLint layer);
+ undefined invalidateFramebuffer(GLenum target, sequence<GLenum> attachments);
+ undefined invalidateSubFramebuffer(GLenum target, sequence<GLenum> attachments,
+ GLint x, GLint y, GLsizei width, GLsizei height);
+ undefined readBuffer(GLenum src);
+
+ /* Renderbuffer objects */
+ any getInternalformatParameter(GLenum target, GLenum internalformat, GLenum pname);
+ undefined renderbufferStorageMultisample(GLenum target, GLsizei samples, GLenum internalformat,
+ GLsizei width, GLsizei height);
+
+ /* Texture objects */
+ undefined texStorage2D(GLenum target, GLsizei levels, GLenum internalformat, GLsizei width,
+ GLsizei height);
+ undefined texStorage3D(GLenum target, GLsizei levels, GLenum internalformat, GLsizei width,
+ GLsizei height, GLsizei depth);
+
+ undefined texImage3D(GLenum target, GLint level, GLint internalformat, GLsizei width, GLsizei height,
+ GLsizei depth, GLint border, GLenum format, GLenum type, GLintptr pboOffset);
+ undefined texImage3D(GLenum target, GLint level, GLint internalformat, GLsizei width, GLsizei height,
+ GLsizei depth, GLint border, GLenum format, GLenum type,
+ TexImageSource source); // May throw DOMException
+ undefined texImage3D(GLenum target, GLint level, GLint internalformat, GLsizei width, GLsizei height,
+ GLsizei depth, GLint border, GLenum format, GLenum type, [AllowShared] ArrayBufferView? srcData);
+ undefined texImage3D(GLenum target, GLint level, GLint internalformat, GLsizei width, GLsizei height,
+ GLsizei depth, GLint border, GLenum format, GLenum type, [AllowShared] ArrayBufferView srcData,
+ GLuint srcOffset);
+
+ undefined texSubImage3D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint zoffset,
+ GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLenum type,
+ GLintptr pboOffset);
+ undefined texSubImage3D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint zoffset,
+ GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLenum type,
+ TexImageSource source); // May throw DOMException
+ undefined texSubImage3D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint zoffset,
+ GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLenum type,
+ [AllowShared] ArrayBufferView? srcData, optional GLuint srcOffset = 0);
+
+ undefined copyTexSubImage3D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint zoffset,
+ GLint x, GLint y, GLsizei width, GLsizei height);
+
+ undefined compressedTexImage3D(GLenum target, GLint level, GLenum internalformat, GLsizei width,
+ GLsizei height, GLsizei depth, GLint border, GLsizei imageSize, GLintptr offset);
+ undefined compressedTexImage3D(GLenum target, GLint level, GLenum internalformat, GLsizei width,
+ GLsizei height, GLsizei depth, GLint border, [AllowShared] ArrayBufferView srcData,
+ optional GLuint srcOffset = 0, optional GLuint srcLengthOverride = 0);
+
+ undefined compressedTexSubImage3D(GLenum target, GLint level, GLint xoffset, GLint yoffset,
+ GLint zoffset, GLsizei width, GLsizei height, GLsizei depth,
+ GLenum format, GLsizei imageSize, GLintptr offset);
+ undefined compressedTexSubImage3D(GLenum target, GLint level, GLint xoffset, GLint yoffset,
+ GLint zoffset, GLsizei width, GLsizei height, GLsizei depth,
+ GLenum format, [AllowShared] ArrayBufferView srcData,
+ optional GLuint srcOffset = 0,
+ optional GLuint srcLengthOverride = 0);
+
+ /* Programs and shaders */
+ [WebGLHandlesContextLoss] GLint getFragDataLocation(WebGLProgram program, DOMString name);
+
+ /* Uniforms */
+ undefined uniform1ui(WebGLUniformLocation? location, GLuint v0);
+ undefined uniform2ui(WebGLUniformLocation? location, GLuint v0, GLuint v1);
+ undefined uniform3ui(WebGLUniformLocation? location, GLuint v0, GLuint v1, GLuint v2);
+ undefined uniform4ui(WebGLUniformLocation? location, GLuint v0, GLuint v1, GLuint v2, GLuint v3);
+
+ undefined uniform1uiv(WebGLUniformLocation? location, Uint32List data, optional GLuint srcOffset = 0,
+ optional GLuint srcLength = 0);
+ undefined uniform2uiv(WebGLUniformLocation? location, Uint32List data, optional GLuint srcOffset = 0,
+ optional GLuint srcLength = 0);
+ undefined uniform3uiv(WebGLUniformLocation? location, Uint32List data, optional GLuint srcOffset = 0,
+ optional GLuint srcLength = 0);
+ undefined uniform4uiv(WebGLUniformLocation? location, Uint32List data, optional GLuint srcOffset = 0,
+ optional GLuint srcLength = 0);
+ undefined uniformMatrix3x2fv(WebGLUniformLocation? location, GLboolean transpose, Float32List data,
+ optional GLuint srcOffset = 0, optional GLuint srcLength = 0);
+ undefined uniformMatrix4x2fv(WebGLUniformLocation? location, GLboolean transpose, Float32List data,
+ optional GLuint srcOffset = 0, optional GLuint srcLength = 0);
+
+ undefined uniformMatrix2x3fv(WebGLUniformLocation? location, GLboolean transpose, Float32List data,
+ optional GLuint srcOffset = 0, optional GLuint srcLength = 0);
+ undefined uniformMatrix4x3fv(WebGLUniformLocation? location, GLboolean transpose, Float32List data,
+ optional GLuint srcOffset = 0, optional GLuint srcLength = 0);
+
+ undefined uniformMatrix2x4fv(WebGLUniformLocation? location, GLboolean transpose, Float32List data,
+ optional GLuint srcOffset = 0, optional GLuint srcLength = 0);
+ undefined uniformMatrix3x4fv(WebGLUniformLocation? location, GLboolean transpose, Float32List data,
+ optional GLuint srcOffset = 0, optional GLuint srcLength = 0);
+
+ /* Vertex attribs */
+ undefined vertexAttribI4i(GLuint index, GLint x, GLint y, GLint z, GLint w);
+ undefined vertexAttribI4iv(GLuint index, Int32List values);
+ undefined vertexAttribI4ui(GLuint index, GLuint x, GLuint y, GLuint z, GLuint w);
+ undefined vertexAttribI4uiv(GLuint index, Uint32List values);
+ undefined vertexAttribIPointer(GLuint index, GLint size, GLenum type, GLsizei stride, GLintptr offset);
+
+ /* Writing to the drawing buffer */
+ undefined vertexAttribDivisor(GLuint index, GLuint divisor);
+ undefined drawArraysInstanced(GLenum mode, GLint first, GLsizei count, GLsizei instanceCount);
+ undefined drawElementsInstanced(GLenum mode, GLsizei count, GLenum type, GLintptr offset, GLsizei instanceCount);
+ undefined drawRangeElements(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type, GLintptr offset);
+
+ /* Multiple Render Targets */
+ undefined drawBuffers(sequence<GLenum> buffers);
+
+ undefined clearBufferfv(GLenum buffer, GLint drawbuffer, Float32List values,
+ optional GLuint srcOffset = 0);
+ undefined clearBufferiv(GLenum buffer, GLint drawbuffer, Int32List values,
+ optional GLuint srcOffset = 0);
+ undefined clearBufferuiv(GLenum buffer, GLint drawbuffer, Uint32List values,
+ optional GLuint srcOffset = 0);
+
+ undefined clearBufferfi(GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil);
+
+ /* Query Objects */
+ WebGLQuery? createQuery();
+ undefined deleteQuery(WebGLQuery? query);
+ [WebGLHandlesContextLoss] GLboolean isQuery(WebGLQuery? query);
+ undefined beginQuery(GLenum target, WebGLQuery query);
+ undefined endQuery(GLenum target);
+ WebGLQuery? getQuery(GLenum target, GLenum pname);
+ any getQueryParameter(WebGLQuery query, GLenum pname);
+
+ /* Sampler Objects */
+ WebGLSampler? createSampler();
+ undefined deleteSampler(WebGLSampler? sampler);
+ [WebGLHandlesContextLoss] GLboolean isSampler(WebGLSampler? sampler);
+ undefined bindSampler(GLuint unit, WebGLSampler? sampler);
+ undefined samplerParameteri(WebGLSampler sampler, GLenum pname, GLint param);
+ undefined samplerParameterf(WebGLSampler sampler, GLenum pname, GLfloat param);
+ any getSamplerParameter(WebGLSampler sampler, GLenum pname);
+
+ /* Sync objects */
+ WebGLSync? fenceSync(GLenum condition, GLbitfield flags);
+ [WebGLHandlesContextLoss] GLboolean isSync(WebGLSync? sync);
+ undefined deleteSync(WebGLSync? sync);
+ GLenum clientWaitSync(WebGLSync sync, GLbitfield flags, GLuint64 timeout);
+ undefined waitSync(WebGLSync sync, GLbitfield flags, GLint64 timeout);
+ any getSyncParameter(WebGLSync sync, GLenum pname);
+
+ /* Transform Feedback */
+ WebGLTransformFeedback? createTransformFeedback();
+ undefined deleteTransformFeedback(WebGLTransformFeedback? tf);
+ [WebGLHandlesContextLoss] GLboolean isTransformFeedback(WebGLTransformFeedback? tf);
+ undefined bindTransformFeedback (GLenum target, WebGLTransformFeedback? tf);
+ undefined beginTransformFeedback(GLenum primitiveMode);
+ undefined endTransformFeedback();
+ undefined transformFeedbackVaryings(WebGLProgram program, sequence<DOMString> varyings, GLenum bufferMode);
+ WebGLActiveInfo? getTransformFeedbackVarying(WebGLProgram program, GLuint index);
+ undefined pauseTransformFeedback();
+ undefined resumeTransformFeedback();
+
+ /* Uniform Buffer Objects and Transform Feedback Buffers */
+ undefined bindBufferBase(GLenum target, GLuint index, WebGLBuffer? buffer);
+ undefined bindBufferRange(GLenum target, GLuint index, WebGLBuffer? buffer, GLintptr offset, GLsizeiptr size);
+ any getIndexedParameter(GLenum target, GLuint index);
+ sequence<GLuint>? getUniformIndices(WebGLProgram program, sequence<DOMString> uniformNames);
+ any getActiveUniforms(WebGLProgram program, sequence<GLuint> uniformIndices, GLenum pname);
+ GLuint getUniformBlockIndex(WebGLProgram program, DOMString uniformBlockName);
+ any getActiveUniformBlockParameter(WebGLProgram program, GLuint uniformBlockIndex, GLenum pname);
+ DOMString? getActiveUniformBlockName(WebGLProgram program, GLuint uniformBlockIndex);
+ undefined uniformBlockBinding(WebGLProgram program, GLuint uniformBlockIndex, GLuint uniformBlockBinding);
+
+ /* Vertex Array Objects */
+ WebGLVertexArrayObject? createVertexArray();
+ undefined deleteVertexArray(WebGLVertexArrayObject? vertexArray);
+ [WebGLHandlesContextLoss] GLboolean isVertexArray(WebGLVertexArrayObject? vertexArray);
+ undefined bindVertexArray(WebGLVertexArrayObject? array);
+};
+
+interface mixin WebGL2RenderingContextOverloads
+{
+ // WebGL1:
+ undefined bufferData(GLenum target, GLsizeiptr size, GLenum usage);
+ undefined bufferData(GLenum target, [AllowShared] BufferSource? srcData, GLenum usage);
+ undefined bufferSubData(GLenum target, GLintptr dstByteOffset, [AllowShared] BufferSource srcData);
+ // WebGL2:
+ undefined bufferData(GLenum target, [AllowShared] ArrayBufferView srcData, GLenum usage, GLuint srcOffset,
+ optional GLuint length = 0);
+ undefined bufferSubData(GLenum target, GLintptr dstByteOffset, [AllowShared] ArrayBufferView srcData,
+ GLuint srcOffset, optional GLuint length = 0);
+
+ // WebGL1 legacy entrypoints:
+ undefined texImage2D(GLenum target, GLint level, GLint internalformat,
+ GLsizei width, GLsizei height, GLint border, GLenum format,
+ GLenum type, [AllowShared] ArrayBufferView? pixels);
+ undefined texImage2D(GLenum target, GLint level, GLint internalformat,
+ GLenum format, GLenum type, TexImageSource source); // May throw DOMException
+
+ undefined texSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset,
+ GLsizei width, GLsizei height,
+ GLenum format, GLenum type, [AllowShared] ArrayBufferView? pixels);
+ undefined texSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset,
+ GLenum format, GLenum type, TexImageSource source); // May throw DOMException
+
+ // WebGL2 entrypoints:
+ undefined texImage2D(GLenum target, GLint level, GLint internalformat, GLsizei width, GLsizei height,
+ GLint border, GLenum format, GLenum type, GLintptr pboOffset);
+ undefined texImage2D(GLenum target, GLint level, GLint internalformat, GLsizei width, GLsizei height,
+ GLint border, GLenum format, GLenum type,
+ TexImageSource source); // May throw DOMException
+ undefined texImage2D(GLenum target, GLint level, GLint internalformat, GLsizei width, GLsizei height,
+ GLint border, GLenum format, GLenum type, [AllowShared] ArrayBufferView srcData,
+ GLuint srcOffset);
+
+ undefined texSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLsizei width,
+ GLsizei height, GLenum format, GLenum type, GLintptr pboOffset);
+ undefined texSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLsizei width,
+ GLsizei height, GLenum format, GLenum type,
+ TexImageSource source); // May throw DOMException
+ undefined texSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLsizei width,
+ GLsizei height, GLenum format, GLenum type, [AllowShared] ArrayBufferView srcData,
+ GLuint srcOffset);
+
+ undefined compressedTexImage2D(GLenum target, GLint level, GLenum internalformat, GLsizei width,
+ GLsizei height, GLint border, GLsizei imageSize, GLintptr offset);
+ undefined compressedTexImage2D(GLenum target, GLint level, GLenum internalformat, GLsizei width,
+ GLsizei height, GLint border, [AllowShared] ArrayBufferView srcData,
+ optional GLuint srcOffset = 0, optional GLuint srcLengthOverride = 0);
+
+ undefined compressedTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset,
+ GLsizei width, GLsizei height, GLenum format, GLsizei imageSize, GLintptr offset);
+ undefined compressedTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset,
+ GLsizei width, GLsizei height, GLenum format,
+ [AllowShared] ArrayBufferView srcData,
+ optional GLuint srcOffset = 0,
+ optional GLuint srcLengthOverride = 0);
+
+ undefined uniform1fv(WebGLUniformLocation? location, Float32List data, optional GLuint srcOffset = 0,
+ optional GLuint srcLength = 0);
+ undefined uniform2fv(WebGLUniformLocation? location, Float32List data, optional GLuint srcOffset = 0,
+ optional GLuint srcLength = 0);
+ undefined uniform3fv(WebGLUniformLocation? location, Float32List data, optional GLuint srcOffset = 0,
+ optional GLuint srcLength = 0);
+ undefined uniform4fv(WebGLUniformLocation? location, Float32List data, optional GLuint srcOffset = 0,
+ optional GLuint srcLength = 0);
+
+ undefined uniform1iv(WebGLUniformLocation? location, Int32List data, optional GLuint srcOffset = 0,
+ optional GLuint srcLength = 0);
+ undefined uniform2iv(WebGLUniformLocation? location, Int32List data, optional GLuint srcOffset = 0,
+ optional GLuint srcLength = 0);
+ undefined uniform3iv(WebGLUniformLocation? location, Int32List data, optional GLuint srcOffset = 0,
+ optional GLuint srcLength = 0);
+ undefined uniform4iv(WebGLUniformLocation? location, Int32List data, optional GLuint srcOffset = 0,
+ optional GLuint srcLength = 0);
+
+ undefined uniformMatrix2fv(WebGLUniformLocation? location, GLboolean transpose, Float32List data,
+ optional GLuint srcOffset = 0, optional GLuint srcLength = 0);
+ undefined uniformMatrix3fv(WebGLUniformLocation? location, GLboolean transpose, Float32List data,
+ optional GLuint srcOffset = 0, optional GLuint srcLength = 0);
+ undefined uniformMatrix4fv(WebGLUniformLocation? location, GLboolean transpose, Float32List data,
+ optional GLuint srcOffset = 0, optional GLuint srcLength = 0);
+
+ /* Reading back pixels */
+ // WebGL1:
+ undefined readPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type,
+ [AllowShared] ArrayBufferView? dstData);
+ // WebGL2:
+ undefined readPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type,
+ GLintptr offset);
+ undefined readPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type,
+ [AllowShared] ArrayBufferView dstData, GLuint dstOffset);
+};
+
+[Exposed=(Window,Worker)]
+interface WebGL2RenderingContext
+{
+};
+WebGL2RenderingContext includes WebGLRenderingContextBase;
+WebGL2RenderingContext includes WebGL2RenderingContextBase;
+WebGL2RenderingContext includes WebGL2RenderingContextOverloads;
diff --git a/test/wpt/tests/interfaces/webgpu.idl b/test/wpt/tests/interfaces/webgpu.idl
new file mode 100644
index 0000000..25943d9
--- /dev/null
+++ b/test/wpt/tests/interfaces/webgpu.idl
@@ -0,0 +1,1293 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: WebGPU (https://gpuweb.github.io/gpuweb/)
+
+interface mixin GPUObjectBase {
+ attribute USVString label;
+};
+
+dictionary GPUObjectDescriptorBase {
+ USVString label;
+};
+
+[Exposed=(Window, DedicatedWorker), SecureContext]
+interface GPUSupportedLimits {
+ readonly attribute unsigned long maxTextureDimension1D;
+ readonly attribute unsigned long maxTextureDimension2D;
+ readonly attribute unsigned long maxTextureDimension3D;
+ readonly attribute unsigned long maxTextureArrayLayers;
+ readonly attribute unsigned long maxBindGroups;
+ readonly attribute unsigned long maxBindGroupsPlusVertexBuffers;
+ readonly attribute unsigned long maxBindingsPerBindGroup;
+ readonly attribute unsigned long maxDynamicUniformBuffersPerPipelineLayout;
+ readonly attribute unsigned long maxDynamicStorageBuffersPerPipelineLayout;
+ readonly attribute unsigned long maxSampledTexturesPerShaderStage;
+ readonly attribute unsigned long maxSamplersPerShaderStage;
+ readonly attribute unsigned long maxStorageBuffersPerShaderStage;
+ readonly attribute unsigned long maxStorageTexturesPerShaderStage;
+ readonly attribute unsigned long maxUniformBuffersPerShaderStage;
+ readonly attribute unsigned long long maxUniformBufferBindingSize;
+ readonly attribute unsigned long long maxStorageBufferBindingSize;
+ readonly attribute unsigned long minUniformBufferOffsetAlignment;
+ readonly attribute unsigned long minStorageBufferOffsetAlignment;
+ readonly attribute unsigned long maxVertexBuffers;
+ readonly attribute unsigned long long maxBufferSize;
+ readonly attribute unsigned long maxVertexAttributes;
+ readonly attribute unsigned long maxVertexBufferArrayStride;
+ readonly attribute unsigned long maxInterStageShaderComponents;
+ readonly attribute unsigned long maxInterStageShaderVariables;
+ readonly attribute unsigned long maxColorAttachments;
+ readonly attribute unsigned long maxColorAttachmentBytesPerSample;
+ readonly attribute unsigned long maxComputeWorkgroupStorageSize;
+ readonly attribute unsigned long maxComputeInvocationsPerWorkgroup;
+ readonly attribute unsigned long maxComputeWorkgroupSizeX;
+ readonly attribute unsigned long maxComputeWorkgroupSizeY;
+ readonly attribute unsigned long maxComputeWorkgroupSizeZ;
+ readonly attribute unsigned long maxComputeWorkgroupsPerDimension;
+};
+
+[Exposed=(Window, DedicatedWorker), SecureContext]
+interface GPUSupportedFeatures {
+ readonly setlike<DOMString>;
+};
+
+[Exposed=(Window, DedicatedWorker), SecureContext]
+interface WGSLLanguageFeatures {
+ readonly setlike<DOMString>;
+};
+
+[Exposed=(Window, DedicatedWorker), SecureContext]
+interface GPUAdapterInfo {
+ readonly attribute DOMString vendor;
+ readonly attribute DOMString architecture;
+ readonly attribute DOMString device;
+ readonly attribute DOMString description;
+};
+
+interface mixin NavigatorGPU {
+ [SameObject, SecureContext] readonly attribute GPU gpu;
+};
+Navigator includes NavigatorGPU;
+WorkerNavigator includes NavigatorGPU;
+
+[Exposed=(Window, DedicatedWorker), SecureContext]
+interface GPU {
+ Promise<GPUAdapter?> requestAdapter(optional GPURequestAdapterOptions options = {});
+ GPUTextureFormat getPreferredCanvasFormat();
+ [SameObject] readonly attribute WGSLLanguageFeatures wgslLanguageFeatures;
+};
+
+dictionary GPURequestAdapterOptions {
+ GPUPowerPreference powerPreference;
+ boolean forceFallbackAdapter = false;
+};
+
+enum GPUPowerPreference {
+ "low-power",
+ "high-performance",
+};
+
+[Exposed=(Window, DedicatedWorker), SecureContext]
+interface GPUAdapter {
+ [SameObject] readonly attribute GPUSupportedFeatures features;
+ [SameObject] readonly attribute GPUSupportedLimits limits;
+ readonly attribute boolean isFallbackAdapter;
+
+ Promise<GPUDevice> requestDevice(optional GPUDeviceDescriptor descriptor = {});
+ Promise<GPUAdapterInfo> requestAdapterInfo(optional sequence<DOMString> unmaskHints = []);
+};
+
+dictionary GPUDeviceDescriptor
+ : GPUObjectDescriptorBase {
+ sequence<GPUFeatureName> requiredFeatures = [];
+ record<DOMString, GPUSize64> requiredLimits = {};
+ GPUQueueDescriptor defaultQueue = {};
+};
+
+enum GPUFeatureName {
+ "depth-clip-control",
+ "depth32float-stencil8",
+ "texture-compression-bc",
+ "texture-compression-etc2",
+ "texture-compression-astc",
+ "timestamp-query",
+ "indirect-first-instance",
+ "shader-f16",
+ "rg11b10ufloat-renderable",
+ "bgra8unorm-storage",
+ "float32-filterable",
+};
+
+[Exposed=(Window, DedicatedWorker), SecureContext]
+interface GPUDevice : EventTarget {
+ [SameObject] readonly attribute GPUSupportedFeatures features;
+ [SameObject] readonly attribute GPUSupportedLimits limits;
+
+ [SameObject] readonly attribute GPUQueue queue;
+
+ undefined destroy();
+
+ GPUBuffer createBuffer(GPUBufferDescriptor descriptor);
+ GPUTexture createTexture(GPUTextureDescriptor descriptor);
+ GPUSampler createSampler(optional GPUSamplerDescriptor descriptor = {});
+ GPUExternalTexture importExternalTexture(GPUExternalTextureDescriptor descriptor);
+
+ GPUBindGroupLayout createBindGroupLayout(GPUBindGroupLayoutDescriptor descriptor);
+ GPUPipelineLayout createPipelineLayout(GPUPipelineLayoutDescriptor descriptor);
+ GPUBindGroup createBindGroup(GPUBindGroupDescriptor descriptor);
+
+ GPUShaderModule createShaderModule(GPUShaderModuleDescriptor descriptor);
+ GPUComputePipeline createComputePipeline(GPUComputePipelineDescriptor descriptor);
+ GPURenderPipeline createRenderPipeline(GPURenderPipelineDescriptor descriptor);
+ Promise<GPUComputePipeline> createComputePipelineAsync(GPUComputePipelineDescriptor descriptor);
+ Promise<GPURenderPipeline> createRenderPipelineAsync(GPURenderPipelineDescriptor descriptor);
+
+ GPUCommandEncoder createCommandEncoder(optional GPUCommandEncoderDescriptor descriptor = {});
+ GPURenderBundleEncoder createRenderBundleEncoder(GPURenderBundleEncoderDescriptor descriptor);
+
+ GPUQuerySet createQuerySet(GPUQuerySetDescriptor descriptor);
+};
+GPUDevice includes GPUObjectBase;
+
+[Exposed=(Window, DedicatedWorker), SecureContext]
+interface GPUBuffer {
+ readonly attribute GPUSize64Out size;
+ readonly attribute GPUFlagsConstant usage;
+
+ readonly attribute GPUBufferMapState mapState;
+
+ Promise<undefined> mapAsync(GPUMapModeFlags mode, optional GPUSize64 offset = 0, optional GPUSize64 size);
+ ArrayBuffer getMappedRange(optional GPUSize64 offset = 0, optional GPUSize64 size);
+ undefined unmap();
+
+ undefined destroy();
+};
+GPUBuffer includes GPUObjectBase;
+
+enum GPUBufferMapState {
+ "unmapped",
+ "pending",
+ "mapped",
+};
+
+dictionary GPUBufferDescriptor
+ : GPUObjectDescriptorBase {
+ required GPUSize64 size;
+ required GPUBufferUsageFlags usage;
+ boolean mappedAtCreation = false;
+};
+
+typedef [EnforceRange] unsigned long GPUBufferUsageFlags;
+[Exposed=(Window, DedicatedWorker), SecureContext]
+namespace GPUBufferUsage {
+ const GPUFlagsConstant MAP_READ = 0x0001;
+ const GPUFlagsConstant MAP_WRITE = 0x0002;
+ const GPUFlagsConstant COPY_SRC = 0x0004;
+ const GPUFlagsConstant COPY_DST = 0x0008;
+ const GPUFlagsConstant INDEX = 0x0010;
+ const GPUFlagsConstant VERTEX = 0x0020;
+ const GPUFlagsConstant UNIFORM = 0x0040;
+ const GPUFlagsConstant STORAGE = 0x0080;
+ const GPUFlagsConstant INDIRECT = 0x0100;
+ const GPUFlagsConstant QUERY_RESOLVE = 0x0200;
+};
+
+typedef [EnforceRange] unsigned long GPUMapModeFlags;
+[Exposed=(Window, DedicatedWorker), SecureContext]
+namespace GPUMapMode {
+ const GPUFlagsConstant READ = 0x0001;
+ const GPUFlagsConstant WRITE = 0x0002;
+};
+
+[Exposed=(Window, DedicatedWorker), SecureContext]
+interface GPUTexture {
+ GPUTextureView createView(optional GPUTextureViewDescriptor descriptor = {});
+
+ undefined destroy();
+
+ readonly attribute GPUIntegerCoordinateOut width;
+ readonly attribute GPUIntegerCoordinateOut height;
+ readonly attribute GPUIntegerCoordinateOut depthOrArrayLayers;
+ readonly attribute GPUIntegerCoordinateOut mipLevelCount;
+ readonly attribute GPUSize32Out sampleCount;
+ readonly attribute GPUTextureDimension dimension;
+ readonly attribute GPUTextureFormat format;
+ readonly attribute GPUFlagsConstant usage;
+};
+GPUTexture includes GPUObjectBase;
+
+dictionary GPUTextureDescriptor
+ : GPUObjectDescriptorBase {
+ required GPUExtent3D size;
+ GPUIntegerCoordinate mipLevelCount = 1;
+ GPUSize32 sampleCount = 1;
+ GPUTextureDimension dimension = "2d";
+ required GPUTextureFormat format;
+ required GPUTextureUsageFlags usage;
+ sequence<GPUTextureFormat> viewFormats = [];
+};
+
+enum GPUTextureDimension {
+ "1d",
+ "2d",
+ "3d",
+};
+
+typedef [EnforceRange] unsigned long GPUTextureUsageFlags;
+[Exposed=(Window, DedicatedWorker), SecureContext]
+namespace GPUTextureUsage {
+ const GPUFlagsConstant COPY_SRC = 0x01;
+ const GPUFlagsConstant COPY_DST = 0x02;
+ const GPUFlagsConstant TEXTURE_BINDING = 0x04;
+ const GPUFlagsConstant STORAGE_BINDING = 0x08;
+ const GPUFlagsConstant RENDER_ATTACHMENT = 0x10;
+};
+
+[Exposed=(Window, DedicatedWorker), SecureContext]
+interface GPUTextureView {
+};
+GPUTextureView includes GPUObjectBase;
+
+dictionary GPUTextureViewDescriptor
+ : GPUObjectDescriptorBase {
+ GPUTextureFormat format;
+ GPUTextureViewDimension dimension;
+ GPUTextureAspect aspect = "all";
+ GPUIntegerCoordinate baseMipLevel = 0;
+ GPUIntegerCoordinate mipLevelCount;
+ GPUIntegerCoordinate baseArrayLayer = 0;
+ GPUIntegerCoordinate arrayLayerCount;
+};
+
+enum GPUTextureViewDimension {
+ "1d",
+ "2d",
+ "2d-array",
+ "cube",
+ "cube-array",
+ "3d",
+};
+
+enum GPUTextureAspect {
+ "all",
+ "stencil-only",
+ "depth-only",
+};
+
+enum GPUTextureFormat {
+ // 8-bit formats
+ "r8unorm",
+ "r8snorm",
+ "r8uint",
+ "r8sint",
+
+ // 16-bit formats
+ "r16uint",
+ "r16sint",
+ "r16float",
+ "rg8unorm",
+ "rg8snorm",
+ "rg8uint",
+ "rg8sint",
+
+ // 32-bit formats
+ "r32uint",
+ "r32sint",
+ "r32float",
+ "rg16uint",
+ "rg16sint",
+ "rg16float",
+ "rgba8unorm",
+ "rgba8unorm-srgb",
+ "rgba8snorm",
+ "rgba8uint",
+ "rgba8sint",
+ "bgra8unorm",
+ "bgra8unorm-srgb",
+ // Packed 32-bit formats
+ "rgb9e5ufloat",
+ "rgb10a2unorm",
+ "rg11b10ufloat",
+
+ // 64-bit formats
+ "rg32uint",
+ "rg32sint",
+ "rg32float",
+ "rgba16uint",
+ "rgba16sint",
+ "rgba16float",
+
+ // 128-bit formats
+ "rgba32uint",
+ "rgba32sint",
+ "rgba32float",
+
+ // Depth/stencil formats
+ "stencil8",
+ "depth16unorm",
+ "depth24plus",
+ "depth24plus-stencil8",
+ "depth32float",
+
+ // "depth32float-stencil8" feature
+ "depth32float-stencil8",
+
+ // BC compressed formats usable if "texture-compression-bc" is both
+ // supported by the device/user agent and enabled in requestDevice.
+ "bc1-rgba-unorm",
+ "bc1-rgba-unorm-srgb",
+ "bc2-rgba-unorm",
+ "bc2-rgba-unorm-srgb",
+ "bc3-rgba-unorm",
+ "bc3-rgba-unorm-srgb",
+ "bc4-r-unorm",
+ "bc4-r-snorm",
+ "bc5-rg-unorm",
+ "bc5-rg-snorm",
+ "bc6h-rgb-ufloat",
+ "bc6h-rgb-float",
+ "bc7-rgba-unorm",
+ "bc7-rgba-unorm-srgb",
+
+ // ETC2 compressed formats usable if "texture-compression-etc2" is both
+ // supported by the device/user agent and enabled in requestDevice.
+ "etc2-rgb8unorm",
+ "etc2-rgb8unorm-srgb",
+ "etc2-rgb8a1unorm",
+ "etc2-rgb8a1unorm-srgb",
+ "etc2-rgba8unorm",
+ "etc2-rgba8unorm-srgb",
+ "eac-r11unorm",
+ "eac-r11snorm",
+ "eac-rg11unorm",
+ "eac-rg11snorm",
+
+ // ASTC compressed formats usable if "texture-compression-astc" is both
+ // supported by the device/user agent and enabled in requestDevice.
+ "astc-4x4-unorm",
+ "astc-4x4-unorm-srgb",
+ "astc-5x4-unorm",
+ "astc-5x4-unorm-srgb",
+ "astc-5x5-unorm",
+ "astc-5x5-unorm-srgb",
+ "astc-6x5-unorm",
+ "astc-6x5-unorm-srgb",
+ "astc-6x6-unorm",
+ "astc-6x6-unorm-srgb",
+ "astc-8x5-unorm",
+ "astc-8x5-unorm-srgb",
+ "astc-8x6-unorm",
+ "astc-8x6-unorm-srgb",
+ "astc-8x8-unorm",
+ "astc-8x8-unorm-srgb",
+ "astc-10x5-unorm",
+ "astc-10x5-unorm-srgb",
+ "astc-10x6-unorm",
+ "astc-10x6-unorm-srgb",
+ "astc-10x8-unorm",
+ "astc-10x8-unorm-srgb",
+ "astc-10x10-unorm",
+ "astc-10x10-unorm-srgb",
+ "astc-12x10-unorm",
+ "astc-12x10-unorm-srgb",
+ "astc-12x12-unorm",
+ "astc-12x12-unorm-srgb",
+};
+
+[Exposed=(Window, DedicatedWorker), SecureContext]
+interface GPUExternalTexture {
+};
+GPUExternalTexture includes GPUObjectBase;
+
+dictionary GPUExternalTextureDescriptor
+ : GPUObjectDescriptorBase {
+ required (HTMLVideoElement or VideoFrame) source;
+ PredefinedColorSpace colorSpace = "srgb";
+};
+
+[Exposed=(Window, DedicatedWorker), SecureContext]
+interface GPUSampler {
+};
+GPUSampler includes GPUObjectBase;
+
+dictionary GPUSamplerDescriptor
+ : GPUObjectDescriptorBase {
+ GPUAddressMode addressModeU = "clamp-to-edge";
+ GPUAddressMode addressModeV = "clamp-to-edge";
+ GPUAddressMode addressModeW = "clamp-to-edge";
+ GPUFilterMode magFilter = "nearest";
+ GPUFilterMode minFilter = "nearest";
+ GPUMipmapFilterMode mipmapFilter = "nearest";
+ float lodMinClamp = 0;
+ float lodMaxClamp = 32;
+ GPUCompareFunction compare;
+ [Clamp] unsigned short maxAnisotropy = 1;
+};
+
+enum GPUAddressMode {
+ "clamp-to-edge",
+ "repeat",
+ "mirror-repeat",
+};
+
+enum GPUFilterMode {
+ "nearest",
+ "linear",
+};
+
+enum GPUMipmapFilterMode {
+ "nearest",
+ "linear",
+};
+
+enum GPUCompareFunction {
+ "never",
+ "less",
+ "equal",
+ "less-equal",
+ "greater",
+ "not-equal",
+ "greater-equal",
+ "always",
+};
+
+[Exposed=(Window, DedicatedWorker), SecureContext]
+interface GPUBindGroupLayout {
+};
+GPUBindGroupLayout includes GPUObjectBase;
+
+dictionary GPUBindGroupLayoutDescriptor
+ : GPUObjectDescriptorBase {
+ required sequence<GPUBindGroupLayoutEntry> entries;
+};
+
+dictionary GPUBindGroupLayoutEntry {
+ required GPUIndex32 binding;
+ required GPUShaderStageFlags visibility;
+
+ GPUBufferBindingLayout buffer;
+ GPUSamplerBindingLayout sampler;
+ GPUTextureBindingLayout texture;
+ GPUStorageTextureBindingLayout storageTexture;
+ GPUExternalTextureBindingLayout externalTexture;
+};
+
+typedef [EnforceRange] unsigned long GPUShaderStageFlags;
+[Exposed=(Window, DedicatedWorker), SecureContext]
+namespace GPUShaderStage {
+ const GPUFlagsConstant VERTEX = 0x1;
+ const GPUFlagsConstant FRAGMENT = 0x2;
+ const GPUFlagsConstant COMPUTE = 0x4;
+};
+
+enum GPUBufferBindingType {
+ "uniform",
+ "storage",
+ "read-only-storage",
+};
+
+dictionary GPUBufferBindingLayout {
+ GPUBufferBindingType type = "uniform";
+ boolean hasDynamicOffset = false;
+ GPUSize64 minBindingSize = 0;
+};
+
+enum GPUSamplerBindingType {
+ "filtering",
+ "non-filtering",
+ "comparison",
+};
+
+dictionary GPUSamplerBindingLayout {
+ GPUSamplerBindingType type = "filtering";
+};
+
+enum GPUTextureSampleType {
+ "float",
+ "unfilterable-float",
+ "depth",
+ "sint",
+ "uint",
+};
+
+dictionary GPUTextureBindingLayout {
+ GPUTextureSampleType sampleType = "float";
+ GPUTextureViewDimension viewDimension = "2d";
+ boolean multisampled = false;
+};
+
+enum GPUStorageTextureAccess {
+ "write-only",
+};
+
+dictionary GPUStorageTextureBindingLayout {
+ GPUStorageTextureAccess access = "write-only";
+ required GPUTextureFormat format;
+ GPUTextureViewDimension viewDimension = "2d";
+};
+
+dictionary GPUExternalTextureBindingLayout {
+};
+
+[Exposed=(Window, DedicatedWorker), SecureContext]
+interface GPUBindGroup {
+};
+GPUBindGroup includes GPUObjectBase;
+
+dictionary GPUBindGroupDescriptor
+ : GPUObjectDescriptorBase {
+ required GPUBindGroupLayout layout;
+ required sequence<GPUBindGroupEntry> entries;
+};
+
+typedef (GPUSampler or GPUTextureView or GPUBufferBinding or GPUExternalTexture) GPUBindingResource;
+
+dictionary GPUBindGroupEntry {
+ required GPUIndex32 binding;
+ required GPUBindingResource resource;
+};
+
+dictionary GPUBufferBinding {
+ required GPUBuffer buffer;
+ GPUSize64 offset = 0;
+ GPUSize64 size;
+};
+
+[Exposed=(Window, DedicatedWorker), SecureContext]
+interface GPUPipelineLayout {
+};
+GPUPipelineLayout includes GPUObjectBase;
+
+dictionary GPUPipelineLayoutDescriptor
+ : GPUObjectDescriptorBase {
+ required sequence<GPUBindGroupLayout> bindGroupLayouts;
+};
+
+[Exposed=(Window, DedicatedWorker), SecureContext]
+interface GPUShaderModule {
+ Promise<GPUCompilationInfo> getCompilationInfo();
+};
+GPUShaderModule includes GPUObjectBase;
+
+dictionary GPUShaderModuleDescriptor
+ : GPUObjectDescriptorBase {
+ required USVString code;
+ object sourceMap;
+ record<USVString, GPUShaderModuleCompilationHint> hints;
+};
+
+dictionary GPUShaderModuleCompilationHint {
+ (GPUPipelineLayout or GPUAutoLayoutMode) layout;
+};
+
+enum GPUCompilationMessageType {
+ "error",
+ "warning",
+ "info",
+};
+
+[Exposed=(Window, DedicatedWorker), Serializable, SecureContext]
+interface GPUCompilationMessage {
+ readonly attribute DOMString message;
+ readonly attribute GPUCompilationMessageType type;
+ readonly attribute unsigned long long lineNum;
+ readonly attribute unsigned long long linePos;
+ readonly attribute unsigned long long offset;
+ readonly attribute unsigned long long length;
+};
+
+[Exposed=(Window, DedicatedWorker), Serializable, SecureContext]
+interface GPUCompilationInfo {
+ readonly attribute FrozenArray<GPUCompilationMessage> messages;
+};
+
+[Exposed=(Window, DedicatedWorker), SecureContext, Serializable]
+interface GPUPipelineError : DOMException {
+ constructor((DOMString or undefined) message, GPUPipelineErrorInit options);
+ readonly attribute GPUPipelineErrorReason reason;
+};
+
+dictionary GPUPipelineErrorInit {
+ required GPUPipelineErrorReason reason;
+};
+
+enum GPUPipelineErrorReason {
+ "validation",
+ "internal",
+};
+
+enum GPUAutoLayoutMode {
+ "auto",
+};
+
+dictionary GPUPipelineDescriptorBase
+ : GPUObjectDescriptorBase {
+ required (GPUPipelineLayout or GPUAutoLayoutMode) layout;
+};
+
+interface mixin GPUPipelineBase {
+ [NewObject] GPUBindGroupLayout getBindGroupLayout(unsigned long index);
+};
+
+dictionary GPUProgrammableStage {
+ required GPUShaderModule module;
+ required USVString entryPoint;
+ record<USVString, GPUPipelineConstantValue> constants;
+};
+
+typedef double GPUPipelineConstantValue; // May represent WGSL’s bool, f32, i32, u32, and f16 if enabled.
+
+[Exposed=(Window, DedicatedWorker), SecureContext]
+interface GPUComputePipeline {
+};
+GPUComputePipeline includes GPUObjectBase;
+GPUComputePipeline includes GPUPipelineBase;
+
+dictionary GPUComputePipelineDescriptor
+ : GPUPipelineDescriptorBase {
+ required GPUProgrammableStage compute;
+};
+
+[Exposed=(Window, DedicatedWorker), SecureContext]
+interface GPURenderPipeline {
+};
+GPURenderPipeline includes GPUObjectBase;
+GPURenderPipeline includes GPUPipelineBase;
+
+dictionary GPURenderPipelineDescriptor
+ : GPUPipelineDescriptorBase {
+ required GPUVertexState vertex;
+ GPUPrimitiveState primitive = {};
+ GPUDepthStencilState depthStencil;
+ GPUMultisampleState multisample = {};
+ GPUFragmentState fragment;
+};
+
+dictionary GPUPrimitiveState {
+ GPUPrimitiveTopology topology = "triangle-list";
+ GPUIndexFormat stripIndexFormat;
+ GPUFrontFace frontFace = "ccw";
+ GPUCullMode cullMode = "none";
+
+ // Requires "depth-clip-control" feature.
+ boolean unclippedDepth = false;
+};
+
+enum GPUPrimitiveTopology {
+ "point-list",
+ "line-list",
+ "line-strip",
+ "triangle-list",
+ "triangle-strip",
+};
+
+enum GPUFrontFace {
+ "ccw",
+ "cw",
+};
+
+enum GPUCullMode {
+ "none",
+ "front",
+ "back",
+};
+
+dictionary GPUMultisampleState {
+ GPUSize32 count = 1;
+ GPUSampleMask mask = 0xFFFFFFFF;
+ boolean alphaToCoverageEnabled = false;
+};
+
+dictionary GPUFragmentState
+ : GPUProgrammableStage {
+ required sequence<GPUColorTargetState?> targets;
+};
+
+dictionary GPUColorTargetState {
+ required GPUTextureFormat format;
+
+ GPUBlendState blend;
+ GPUColorWriteFlags writeMask = 0xF; // GPUColorWrite.ALL
+};
+
+dictionary GPUBlendState {
+ required GPUBlendComponent color;
+ required GPUBlendComponent alpha;
+};
+
+typedef [EnforceRange] unsigned long GPUColorWriteFlags;
+[Exposed=(Window, DedicatedWorker), SecureContext]
+namespace GPUColorWrite {
+ const GPUFlagsConstant RED = 0x1;
+ const GPUFlagsConstant GREEN = 0x2;
+ const GPUFlagsConstant BLUE = 0x4;
+ const GPUFlagsConstant ALPHA = 0x8;
+ const GPUFlagsConstant ALL = 0xF;
+};
+
+dictionary GPUBlendComponent {
+ GPUBlendOperation operation = "add";
+ GPUBlendFactor srcFactor = "one";
+ GPUBlendFactor dstFactor = "zero";
+};
+
+enum GPUBlendFactor {
+ "zero",
+ "one",
+ "src",
+ "one-minus-src",
+ "src-alpha",
+ "one-minus-src-alpha",
+ "dst",
+ "one-minus-dst",
+ "dst-alpha",
+ "one-minus-dst-alpha",
+ "src-alpha-saturated",
+ "constant",
+ "one-minus-constant",
+};
+
+enum GPUBlendOperation {
+ "add",
+ "subtract",
+ "reverse-subtract",
+ "min",
+ "max",
+};
+
+dictionary GPUDepthStencilState {
+ required GPUTextureFormat format;
+
+ required boolean depthWriteEnabled;
+ required GPUCompareFunction depthCompare;
+
+ GPUStencilFaceState stencilFront = {};
+ GPUStencilFaceState stencilBack = {};
+
+ GPUStencilValue stencilReadMask = 0xFFFFFFFF;
+ GPUStencilValue stencilWriteMask = 0xFFFFFFFF;
+
+ GPUDepthBias depthBias = 0;
+ float depthBiasSlopeScale = 0;
+ float depthBiasClamp = 0;
+};
+
+dictionary GPUStencilFaceState {
+ GPUCompareFunction compare = "always";
+ GPUStencilOperation failOp = "keep";
+ GPUStencilOperation depthFailOp = "keep";
+ GPUStencilOperation passOp = "keep";
+};
+
+enum GPUStencilOperation {
+ "keep",
+ "zero",
+ "replace",
+ "invert",
+ "increment-clamp",
+ "decrement-clamp",
+ "increment-wrap",
+ "decrement-wrap",
+};
+
+enum GPUIndexFormat {
+ "uint16",
+ "uint32",
+};
+
+enum GPUVertexFormat {
+ "uint8x2",
+ "uint8x4",
+ "sint8x2",
+ "sint8x4",
+ "unorm8x2",
+ "unorm8x4",
+ "snorm8x2",
+ "snorm8x4",
+ "uint16x2",
+ "uint16x4",
+ "sint16x2",
+ "sint16x4",
+ "unorm16x2",
+ "unorm16x4",
+ "snorm16x2",
+ "snorm16x4",
+ "float16x2",
+ "float16x4",
+ "float32",
+ "float32x2",
+ "float32x3",
+ "float32x4",
+ "uint32",
+ "uint32x2",
+ "uint32x3",
+ "uint32x4",
+ "sint32",
+ "sint32x2",
+ "sint32x3",
+ "sint32x4",
+};
+
+enum GPUVertexStepMode {
+ "vertex",
+ "instance",
+};
+
+dictionary GPUVertexState
+ : GPUProgrammableStage {
+ sequence<GPUVertexBufferLayout?> buffers = [];
+};
+
+dictionary GPUVertexBufferLayout {
+ required GPUSize64 arrayStride;
+ GPUVertexStepMode stepMode = "vertex";
+ required sequence<GPUVertexAttribute> attributes;
+};
+
+dictionary GPUVertexAttribute {
+ required GPUVertexFormat format;
+ required GPUSize64 offset;
+
+ required GPUIndex32 shaderLocation;
+};
+
+dictionary GPUImageDataLayout {
+ GPUSize64 offset = 0;
+ GPUSize32 bytesPerRow;
+ GPUSize32 rowsPerImage;
+};
+
+dictionary GPUImageCopyBuffer
+ : GPUImageDataLayout {
+ required GPUBuffer buffer;
+};
+
+dictionary GPUImageCopyTexture {
+ required GPUTexture texture;
+ GPUIntegerCoordinate mipLevel = 0;
+ GPUOrigin3D origin = {};
+ GPUTextureAspect aspect = "all";
+};
+
+dictionary GPUImageCopyTextureTagged
+ : GPUImageCopyTexture {
+ PredefinedColorSpace colorSpace = "srgb";
+ boolean premultipliedAlpha = false;
+};
+
+dictionary GPUImageCopyExternalImage {
+ required (ImageBitmap or HTMLVideoElement or HTMLCanvasElement or OffscreenCanvas) source;
+ GPUOrigin2D origin = {};
+ boolean flipY = false;
+};
+
+[Exposed=(Window, DedicatedWorker), SecureContext]
+interface GPUCommandBuffer {
+};
+GPUCommandBuffer includes GPUObjectBase;
+
+dictionary GPUCommandBufferDescriptor
+ : GPUObjectDescriptorBase {
+};
+
+interface mixin GPUCommandsMixin {
+};
+
+[Exposed=(Window, DedicatedWorker), SecureContext]
+interface GPUCommandEncoder {
+ GPURenderPassEncoder beginRenderPass(GPURenderPassDescriptor descriptor);
+ GPUComputePassEncoder beginComputePass(optional GPUComputePassDescriptor descriptor = {});
+
+ undefined copyBufferToBuffer(
+ GPUBuffer source,
+ GPUSize64 sourceOffset,
+ GPUBuffer destination,
+ GPUSize64 destinationOffset,
+ GPUSize64 size);
+
+ undefined copyBufferToTexture(
+ GPUImageCopyBuffer source,
+ GPUImageCopyTexture destination,
+ GPUExtent3D copySize);
+
+ undefined copyTextureToBuffer(
+ GPUImageCopyTexture source,
+ GPUImageCopyBuffer destination,
+ GPUExtent3D copySize);
+
+ undefined copyTextureToTexture(
+ GPUImageCopyTexture source,
+ GPUImageCopyTexture destination,
+ GPUExtent3D copySize);
+
+ undefined clearBuffer(
+ GPUBuffer buffer,
+ optional GPUSize64 offset = 0,
+ optional GPUSize64 size);
+
+ undefined writeTimestamp(GPUQuerySet querySet, GPUSize32 queryIndex);
+
+ undefined resolveQuerySet(
+ GPUQuerySet querySet,
+ GPUSize32 firstQuery,
+ GPUSize32 queryCount,
+ GPUBuffer destination,
+ GPUSize64 destinationOffset);
+
+ GPUCommandBuffer finish(optional GPUCommandBufferDescriptor descriptor = {});
+};
+GPUCommandEncoder includes GPUObjectBase;
+GPUCommandEncoder includes GPUCommandsMixin;
+GPUCommandEncoder includes GPUDebugCommandsMixin;
+
+dictionary GPUCommandEncoderDescriptor
+ : GPUObjectDescriptorBase {
+};
+
+interface mixin GPUBindingCommandsMixin {
+ undefined setBindGroup(GPUIndex32 index, GPUBindGroup? bindGroup,
+ optional sequence<GPUBufferDynamicOffset> dynamicOffsets = []);
+
+ undefined setBindGroup(GPUIndex32 index, GPUBindGroup? bindGroup,
+ Uint32Array dynamicOffsetsData,
+ GPUSize64 dynamicOffsetsDataStart,
+ GPUSize32 dynamicOffsetsDataLength);
+};
+
+interface mixin GPUDebugCommandsMixin {
+ undefined pushDebugGroup(USVString groupLabel);
+ undefined popDebugGroup();
+ undefined insertDebugMarker(USVString markerLabel);
+};
+
+[Exposed=(Window, DedicatedWorker), SecureContext]
+interface GPUComputePassEncoder {
+ undefined setPipeline(GPUComputePipeline pipeline);
+ undefined dispatchWorkgroups(GPUSize32 workgroupCountX, optional GPUSize32 workgroupCountY = 1, optional GPUSize32 workgroupCountZ = 1);
+ undefined dispatchWorkgroupsIndirect(GPUBuffer indirectBuffer, GPUSize64 indirectOffset);
+
+ undefined end();
+};
+GPUComputePassEncoder includes GPUObjectBase;
+GPUComputePassEncoder includes GPUCommandsMixin;
+GPUComputePassEncoder includes GPUDebugCommandsMixin;
+GPUComputePassEncoder includes GPUBindingCommandsMixin;
+
+dictionary GPUComputePassTimestampWrites {
+ required GPUQuerySet querySet;
+ GPUSize32 beginningOfPassWriteIndex;
+ GPUSize32 endOfPassWriteIndex;
+};
+
+dictionary GPUComputePassDescriptor
+ : GPUObjectDescriptorBase {
+ GPUComputePassTimestampWrites timestampWrites;
+};
+
+[Exposed=(Window, DedicatedWorker), SecureContext]
+interface GPURenderPassEncoder {
+ undefined setViewport(float x, float y,
+ float width, float height,
+ float minDepth, float maxDepth);
+
+ undefined setScissorRect(GPUIntegerCoordinate x, GPUIntegerCoordinate y,
+ GPUIntegerCoordinate width, GPUIntegerCoordinate height);
+
+ undefined setBlendConstant(GPUColor color);
+ undefined setStencilReference(GPUStencilValue reference);
+
+ undefined beginOcclusionQuery(GPUSize32 queryIndex);
+ undefined endOcclusionQuery();
+
+ undefined executeBundles(sequence<GPURenderBundle> bundles);
+ undefined end();
+};
+GPURenderPassEncoder includes GPUObjectBase;
+GPURenderPassEncoder includes GPUCommandsMixin;
+GPURenderPassEncoder includes GPUDebugCommandsMixin;
+GPURenderPassEncoder includes GPUBindingCommandsMixin;
+GPURenderPassEncoder includes GPURenderCommandsMixin;
+
+dictionary GPURenderPassTimestampWrites {
+ required GPUQuerySet querySet;
+ GPUSize32 beginningOfPassWriteIndex;
+ GPUSize32 endOfPassWriteIndex;
+};
+
+dictionary GPURenderPassDescriptor
+ : GPUObjectDescriptorBase {
+ required sequence<GPURenderPassColorAttachment?> colorAttachments;
+ GPURenderPassDepthStencilAttachment depthStencilAttachment;
+ GPUQuerySet occlusionQuerySet;
+ GPURenderPassTimestampWrites timestampWrites;
+ GPUSize64 maxDrawCount = 50000000;
+};
+
+dictionary GPURenderPassColorAttachment {
+ required GPUTextureView view;
+ GPUTextureView resolveTarget;
+
+ GPUColor clearValue;
+ required GPULoadOp loadOp;
+ required GPUStoreOp storeOp;
+};
+
+dictionary GPURenderPassDepthStencilAttachment {
+ required GPUTextureView view;
+
+ float depthClearValue;
+ GPULoadOp depthLoadOp;
+ GPUStoreOp depthStoreOp;
+ boolean depthReadOnly = false;
+
+ GPUStencilValue stencilClearValue = 0;
+ GPULoadOp stencilLoadOp;
+ GPUStoreOp stencilStoreOp;
+ boolean stencilReadOnly = false;
+};
+
+enum GPULoadOp {
+ "load",
+ "clear",
+};
+
+enum GPUStoreOp {
+ "store",
+ "discard",
+};
+
+dictionary GPURenderPassLayout
+ : GPUObjectDescriptorBase {
+ required sequence<GPUTextureFormat?> colorFormats;
+ GPUTextureFormat depthStencilFormat;
+ GPUSize32 sampleCount = 1;
+};
+
+interface mixin GPURenderCommandsMixin {
+ undefined setPipeline(GPURenderPipeline pipeline);
+
+ undefined setIndexBuffer(GPUBuffer buffer, GPUIndexFormat indexFormat, optional GPUSize64 offset = 0, optional GPUSize64 size);
+ undefined setVertexBuffer(GPUIndex32 slot, GPUBuffer? buffer, optional GPUSize64 offset = 0, optional GPUSize64 size);
+
+ undefined draw(GPUSize32 vertexCount, optional GPUSize32 instanceCount = 1,
+ optional GPUSize32 firstVertex = 0, optional GPUSize32 firstInstance = 0);
+ undefined drawIndexed(GPUSize32 indexCount, optional GPUSize32 instanceCount = 1,
+ optional GPUSize32 firstIndex = 0,
+ optional GPUSignedOffset32 baseVertex = 0,
+ optional GPUSize32 firstInstance = 0);
+
+ undefined drawIndirect(GPUBuffer indirectBuffer, GPUSize64 indirectOffset);
+ undefined drawIndexedIndirect(GPUBuffer indirectBuffer, GPUSize64 indirectOffset);
+};
+
+[Exposed=(Window, DedicatedWorker), SecureContext]
+interface GPURenderBundle {
+};
+GPURenderBundle includes GPUObjectBase;
+
+dictionary GPURenderBundleDescriptor
+ : GPUObjectDescriptorBase {
+};
+
+[Exposed=(Window, DedicatedWorker), SecureContext]
+interface GPURenderBundleEncoder {
+ GPURenderBundle finish(optional GPURenderBundleDescriptor descriptor = {});
+};
+GPURenderBundleEncoder includes GPUObjectBase;
+GPURenderBundleEncoder includes GPUCommandsMixin;
+GPURenderBundleEncoder includes GPUDebugCommandsMixin;
+GPURenderBundleEncoder includes GPUBindingCommandsMixin;
+GPURenderBundleEncoder includes GPURenderCommandsMixin;
+
+dictionary GPURenderBundleEncoderDescriptor
+ : GPURenderPassLayout {
+ boolean depthReadOnly = false;
+ boolean stencilReadOnly = false;
+};
+
+dictionary GPUQueueDescriptor
+ : GPUObjectDescriptorBase {
+};
+
+[Exposed=(Window, DedicatedWorker), SecureContext]
+interface GPUQueue {
+ undefined submit(sequence<GPUCommandBuffer> commandBuffers);
+
+ Promise<undefined> onSubmittedWorkDone();
+
+ undefined writeBuffer(
+ GPUBuffer buffer,
+ GPUSize64 bufferOffset,
+ [AllowShared] BufferSource data,
+ optional GPUSize64 dataOffset = 0,
+ optional GPUSize64 size);
+
+ undefined writeTexture(
+ GPUImageCopyTexture destination,
+ [AllowShared] BufferSource data,
+ GPUImageDataLayout dataLayout,
+ GPUExtent3D size);
+
+ undefined copyExternalImageToTexture(
+ GPUImageCopyExternalImage source,
+ GPUImageCopyTextureTagged destination,
+ GPUExtent3D copySize);
+};
+GPUQueue includes GPUObjectBase;
+
+[Exposed=(Window, DedicatedWorker), SecureContext]
+interface GPUQuerySet {
+ undefined destroy();
+
+ readonly attribute GPUQueryType type;
+ readonly attribute GPUSize32Out count;
+};
+GPUQuerySet includes GPUObjectBase;
+
+dictionary GPUQuerySetDescriptor
+ : GPUObjectDescriptorBase {
+ required GPUQueryType type;
+ required GPUSize32 count;
+};
+
+enum GPUQueryType {
+ "occlusion",
+ "timestamp",
+};
+
+[Exposed=(Window, DedicatedWorker), SecureContext]
+interface GPUCanvasContext {
+ readonly attribute (HTMLCanvasElement or OffscreenCanvas) canvas;
+
+ undefined configure(GPUCanvasConfiguration configuration);
+ undefined unconfigure();
+
+ GPUTexture getCurrentTexture();
+};
+
+enum GPUCanvasAlphaMode {
+ "opaque",
+ "premultiplied",
+};
+
+dictionary GPUCanvasConfiguration {
+ required GPUDevice device;
+ required GPUTextureFormat format;
+ GPUTextureUsageFlags usage = 0x10; // GPUTextureUsage.RENDER_ATTACHMENT
+ sequence<GPUTextureFormat> viewFormats = [];
+ PredefinedColorSpace colorSpace = "srgb";
+ GPUCanvasAlphaMode alphaMode = "opaque";
+};
+
+enum GPUDeviceLostReason {
+ "unknown",
+ "destroyed",
+};
+
+[Exposed=(Window, DedicatedWorker), SecureContext]
+interface GPUDeviceLostInfo {
+ readonly attribute GPUDeviceLostReason reason;
+ readonly attribute DOMString message;
+};
+
+partial interface GPUDevice {
+ readonly attribute Promise<GPUDeviceLostInfo> lost;
+};
+
+[Exposed=(Window, DedicatedWorker), SecureContext]
+interface GPUError {
+ readonly attribute DOMString message;
+};
+
+[Exposed=(Window, DedicatedWorker), SecureContext]
+interface GPUValidationError
+ : GPUError {
+ constructor(DOMString message);
+};
+
+[Exposed=(Window, DedicatedWorker), SecureContext]
+interface GPUOutOfMemoryError
+ : GPUError {
+ constructor(DOMString message);
+};
+
+[Exposed=(Window, DedicatedWorker), SecureContext]
+interface GPUInternalError
+ : GPUError {
+ constructor(DOMString message);
+};
+
+enum GPUErrorFilter {
+ "validation",
+ "out-of-memory",
+ "internal",
+};
+
+partial interface GPUDevice {
+ undefined pushErrorScope(GPUErrorFilter filter);
+ Promise<GPUError?> popErrorScope();
+};
+
+[Exposed=(Window, DedicatedWorker), SecureContext]
+interface GPUUncapturedErrorEvent : Event {
+ constructor(
+ DOMString type,
+ GPUUncapturedErrorEventInit gpuUncapturedErrorEventInitDict
+ );
+ [SameObject] readonly attribute GPUError error;
+};
+
+dictionary GPUUncapturedErrorEventInit : EventInit {
+ required GPUError error;
+};
+
+partial interface GPUDevice {
+ [Exposed=(Window, DedicatedWorker)]
+ attribute EventHandler onuncapturederror;
+};
+
+typedef [EnforceRange] unsigned long GPUBufferDynamicOffset;
+typedef [EnforceRange] unsigned long GPUStencilValue;
+typedef [EnforceRange] unsigned long GPUSampleMask;
+typedef [EnforceRange] long GPUDepthBias;
+
+typedef [EnforceRange] unsigned long long GPUSize64;
+typedef [EnforceRange] unsigned long GPUIntegerCoordinate;
+typedef [EnforceRange] unsigned long GPUIndex32;
+typedef [EnforceRange] unsigned long GPUSize32;
+typedef [EnforceRange] long GPUSignedOffset32;
+
+typedef unsigned long long GPUSize64Out;
+typedef unsigned long GPUIntegerCoordinateOut;
+typedef unsigned long GPUSize32Out;
+
+typedef unsigned long GPUFlagsConstant;
+
+dictionary GPUColorDict {
+ required double r;
+ required double g;
+ required double b;
+ required double a;
+};
+typedef (sequence<double> or GPUColorDict) GPUColor;
+
+dictionary GPUOrigin2DDict {
+ GPUIntegerCoordinate x = 0;
+ GPUIntegerCoordinate y = 0;
+};
+typedef (sequence<GPUIntegerCoordinate> or GPUOrigin2DDict) GPUOrigin2D;
+
+dictionary GPUOrigin3DDict {
+ GPUIntegerCoordinate x = 0;
+ GPUIntegerCoordinate y = 0;
+ GPUIntegerCoordinate z = 0;
+};
+typedef (sequence<GPUIntegerCoordinate> or GPUOrigin3DDict) GPUOrigin3D;
+
+dictionary GPUExtent3DDict {
+ required GPUIntegerCoordinate width;
+ GPUIntegerCoordinate height = 1;
+ GPUIntegerCoordinate depthOrArrayLayers = 1;
+};
+typedef (sequence<GPUIntegerCoordinate> or GPUExtent3DDict) GPUExtent3D;
diff --git a/test/wpt/tests/interfaces/webhid.idl b/test/wpt/tests/interfaces/webhid.idl
new file mode 100644
index 0000000..a1a02d3
--- /dev/null
+++ b/test/wpt/tests/interfaces/webhid.idl
@@ -0,0 +1,127 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: WebHID API (https://wicg.github.io/webhid/)
+
+[SecureContext] partial interface Navigator {
+ [SameObject] readonly attribute HID hid;
+};
+
+[Exposed=ServiceWorker, SecureContext]
+partial interface WorkerNavigator {
+ [SameObject] readonly attribute HID hid;
+};
+
+[Exposed=(Window,ServiceWorker), SecureContext]
+interface HID : EventTarget {
+ attribute EventHandler onconnect;
+ attribute EventHandler ondisconnect;
+ Promise<sequence<HIDDevice>> getDevices();
+ Promise<sequence<HIDDevice>> requestDevice(
+ HIDDeviceRequestOptions options);
+};
+
+dictionary HIDDeviceRequestOptions {
+ required sequence<HIDDeviceFilter> filters;
+ sequence<HIDDeviceFilter> exclusionFilters;
+};
+
+dictionary HIDDeviceFilter {
+ unsigned long vendorId;
+ unsigned short productId;
+ unsigned short usagePage;
+ unsigned short usage;
+};
+
+[Exposed=Window, SecureContext]
+interface HIDDevice : EventTarget {
+ attribute EventHandler oninputreport;
+ readonly attribute boolean opened;
+ readonly attribute unsigned short vendorId;
+ readonly attribute unsigned short productId;
+ readonly attribute DOMString productName;
+ readonly attribute FrozenArray<HIDCollectionInfo> collections;
+ Promise<undefined> open();
+ Promise<undefined> close();
+ Promise<undefined> forget();
+ Promise<undefined> sendReport([EnforceRange] octet reportId, BufferSource data);
+ Promise<undefined> sendFeatureReport(
+ [EnforceRange] octet reportId,
+ BufferSource data);
+ Promise<DataView> receiveFeatureReport([EnforceRange] octet reportId);
+};
+
+[Exposed=Window, SecureContext]
+interface HIDConnectionEvent : Event {
+ constructor(DOMString type, HIDConnectionEventInit eventInitDict);
+ [SameObject] readonly attribute HIDDevice device;
+};
+
+dictionary HIDConnectionEventInit : EventInit {
+ required HIDDevice device;
+};
+
+[Exposed=Window, SecureContext]
+interface HIDInputReportEvent : Event {
+ constructor(DOMString type, HIDInputReportEventInit eventInitDict);
+ [SameObject] readonly attribute HIDDevice device;
+ readonly attribute octet reportId;
+ readonly attribute DataView data;
+};
+
+dictionary HIDInputReportEventInit : EventInit {
+ required HIDDevice device;
+ required octet reportId;
+ required DataView data;
+};
+
+dictionary HIDCollectionInfo {
+ unsigned short usagePage;
+ unsigned short usage;
+ octet type;
+ sequence<HIDCollectionInfo> children;
+ sequence<HIDReportInfo> inputReports;
+ sequence<HIDReportInfo> outputReports;
+ sequence<HIDReportInfo> featureReports;
+};
+
+dictionary HIDReportInfo {
+ octet reportId;
+ sequence<HIDReportItem> items;
+};
+
+dictionary HIDReportItem {
+ boolean isAbsolute;
+ boolean isArray;
+ boolean isBufferedBytes;
+ boolean isConstant;
+ boolean isLinear;
+ boolean isRange;
+ boolean isVolatile;
+ boolean hasNull;
+ boolean hasPreferredState;
+ boolean wrap;
+ sequence<unsigned long> usages;
+ unsigned long usageMinimum;
+ unsigned long usageMaximum;
+ unsigned short reportSize;
+ unsigned short reportCount;
+ byte unitExponent;
+ HIDUnitSystem unitSystem;
+ byte unitFactorLengthExponent;
+ byte unitFactorMassExponent;
+ byte unitFactorTimeExponent;
+ byte unitFactorTemperatureExponent;
+ byte unitFactorCurrentExponent;
+ byte unitFactorLuminousIntensityExponent;
+ long logicalMinimum;
+ long logicalMaximum;
+ long physicalMinimum;
+ long physicalMaximum;
+ sequence<DOMString> strings;
+};
+
+enum HIDUnitSystem {
+ "none", "si-linear", "si-rotation", "english-linear",
+ "english-rotation", "vendor-defined", "reserved"
+};
diff --git a/test/wpt/tests/interfaces/webidl.idl b/test/wpt/tests/interfaces/webidl.idl
new file mode 100644
index 0000000..7271f19
--- /dev/null
+++ b/test/wpt/tests/interfaces/webidl.idl
@@ -0,0 +1,48 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Web IDL Standard (https://webidl.spec.whatwg.org/)
+
+typedef (Int8Array or Int16Array or Int32Array or
+ Uint8Array or Uint16Array or Uint32Array or Uint8ClampedArray or
+ BigInt64Array or BigUint64Array or
+ Float32Array or Float64Array or DataView) ArrayBufferView;
+
+typedef (ArrayBufferView or ArrayBuffer) BufferSource;
+[Exposed=*,
+ Serializable]
+interface DOMException { // but see below note about ECMAScript binding
+ constructor(optional DOMString message = "", optional DOMString name = "Error");
+ readonly attribute DOMString name;
+ readonly attribute DOMString message;
+ readonly attribute unsigned short code;
+
+ const unsigned short INDEX_SIZE_ERR = 1;
+ const unsigned short DOMSTRING_SIZE_ERR = 2;
+ const unsigned short HIERARCHY_REQUEST_ERR = 3;
+ const unsigned short WRONG_DOCUMENT_ERR = 4;
+ const unsigned short INVALID_CHARACTER_ERR = 5;
+ const unsigned short NO_DATA_ALLOWED_ERR = 6;
+ const unsigned short NO_MODIFICATION_ALLOWED_ERR = 7;
+ const unsigned short NOT_FOUND_ERR = 8;
+ const unsigned short NOT_SUPPORTED_ERR = 9;
+ const unsigned short INUSE_ATTRIBUTE_ERR = 10;
+ const unsigned short INVALID_STATE_ERR = 11;
+ const unsigned short SYNTAX_ERR = 12;
+ const unsigned short INVALID_MODIFICATION_ERR = 13;
+ const unsigned short NAMESPACE_ERR = 14;
+ const unsigned short INVALID_ACCESS_ERR = 15;
+ const unsigned short VALIDATION_ERR = 16;
+ const unsigned short TYPE_MISMATCH_ERR = 17;
+ const unsigned short SECURITY_ERR = 18;
+ const unsigned short NETWORK_ERR = 19;
+ const unsigned short ABORT_ERR = 20;
+ const unsigned short URL_MISMATCH_ERR = 21;
+ const unsigned short QUOTA_EXCEEDED_ERR = 22;
+ const unsigned short TIMEOUT_ERR = 23;
+ const unsigned short INVALID_NODE_TYPE_ERR = 24;
+ const unsigned short DATA_CLONE_ERR = 25;
+};
+
+callback Function = any (any... arguments);
+callback VoidFunction = undefined ();
diff --git a/test/wpt/tests/interfaces/webmidi.idl b/test/wpt/tests/interfaces/webmidi.idl
new file mode 100644
index 0000000..1acf1ac
--- /dev/null
+++ b/test/wpt/tests/interfaces/webmidi.idl
@@ -0,0 +1,91 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Web MIDI API (https://webaudio.github.io/web-midi-api/)
+
+dictionary MidiPermissionDescriptor : PermissionDescriptor {
+ boolean sysex = false;
+};
+
+partial interface Navigator {
+ [SecureContext]
+ Promise <MIDIAccess> requestMIDIAccess(optional MIDIOptions options = {});
+};
+
+dictionary MIDIOptions {
+ boolean sysex;
+ boolean software;
+};
+
+[SecureContext, Exposed=Window] interface MIDIInputMap {
+ readonly maplike <DOMString, MIDIInput>;
+};
+
+[SecureContext, Exposed=Window] interface MIDIOutputMap {
+ readonly maplike <DOMString, MIDIOutput>;
+};
+
+[SecureContext, Exposed=Window] interface MIDIAccess: EventTarget {
+ readonly attribute MIDIInputMap inputs;
+ readonly attribute MIDIOutputMap outputs;
+ attribute EventHandler onstatechange;
+ readonly attribute boolean sysexEnabled;
+};
+
+[SecureContext, Exposed=Window] interface MIDIPort: EventTarget {
+ readonly attribute DOMString id;
+ readonly attribute DOMString? manufacturer;
+ readonly attribute DOMString? name;
+ readonly attribute MIDIPortType type;
+ readonly attribute DOMString? version;
+ readonly attribute MIDIPortDeviceState state;
+ readonly attribute MIDIPortConnectionState connection;
+ attribute EventHandler onstatechange;
+ Promise <MIDIPort> open();
+ Promise <MIDIPort> close();
+};
+
+[SecureContext, Exposed=Window] interface MIDIInput: MIDIPort {
+ attribute EventHandler onmidimessage;
+};
+
+[SecureContext, Exposed=Window] interface MIDIOutput : MIDIPort {
+ undefined send(sequence<octet> data, optional DOMHighResTimeStamp timestamp = 0);
+ undefined clear();
+};
+
+enum MIDIPortType {
+ "input",
+ "output",
+};
+
+enum MIDIPortDeviceState {
+ "disconnected",
+ "connected",
+};
+
+enum MIDIPortConnectionState {
+ "open",
+ "closed",
+ "pending",
+};
+
+[SecureContext, Exposed=Window]
+interface MIDIMessageEvent : Event {
+ constructor(DOMString type, optional MIDIMessageEventInit eventInitDict = {});
+ readonly attribute Uint8Array data;
+};
+
+dictionary MIDIMessageEventInit: EventInit {
+ Uint8Array data;
+};
+
+[SecureContext, Exposed=Window]
+interface MIDIConnectionEvent : Event {
+ constructor(DOMString type, optional MIDIConnectionEventInit eventInitDict = {});
+ readonly attribute MIDIPort port;
+};
+
+dictionary MIDIConnectionEventInit: EventInit {
+ MIDIPort port;
+};
diff --git a/test/wpt/tests/interfaces/webnn.idl b/test/wpt/tests/interfaces/webnn.idl
new file mode 100644
index 0000000..17e3080
--- /dev/null
+++ b/test/wpt/tests/interfaces/webnn.idl
@@ -0,0 +1,544 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Web Neural Network API (https://webmachinelearning.github.io/webnn/)
+
+interface mixin NavigatorML {
+ [SecureContext, SameObject] readonly attribute ML ml;
+};
+Navigator includes NavigatorML;
+WorkerNavigator includes NavigatorML;
+
+enum MLDeviceType {
+ "cpu",
+ "gpu"
+};
+
+enum MLPowerPreference {
+ "default",
+ "high-performance",
+ "low-power"
+};
+
+dictionary MLContextOptions {
+ MLDeviceType deviceType = "cpu";
+ MLPowerPreference powerPreference = "default";
+};
+
+[SecureContext, Exposed=(Window, DedicatedWorker)]
+interface ML {
+ Promise<MLContext> createContext(optional MLContextOptions options = {});
+ Promise<MLContext> createContext(GPUDevice gpuDevice);
+
+ [Exposed=(DedicatedWorker)]
+ MLContext createContextSync(optional MLContextOptions options = {});
+ [Exposed=(DedicatedWorker)]
+ MLContext createContextSync(GPUDevice gpuDevice);
+};
+
+[SecureContext, Exposed=(Window, DedicatedWorker)]
+interface MLGraph {};
+
+enum MLInputOperandLayout {
+ "nchw",
+ "nhwc"
+};
+
+enum MLOperandType {
+ "float32",
+ "float16",
+ "int32",
+ "uint32",
+ "int8",
+ "uint8"
+};
+
+dictionary MLOperandDescriptor {
+ // The operand type.
+ required MLOperandType type;
+
+ // The dimensions field is only required for tensor operands.
+ sequence<unsigned long> dimensions;
+};
+
+[SecureContext, Exposed=(Window, DedicatedWorker)]
+interface MLOperand {};
+
+[SecureContext, Exposed=(Window, DedicatedWorker)]
+interface MLActivation {};
+
+typedef record<DOMString, ArrayBufferView> MLNamedArrayBufferViews;
+
+[SecureContext, Exposed=(Window, DedicatedWorker)]
+interface MLContext {};
+
+partial interface MLContext {
+ [Exposed=(DedicatedWorker)]
+ undefined computeSync(
+ MLGraph graph, MLNamedArrayBufferViews inputs, MLNamedArrayBufferViews outputs);
+};
+
+dictionary MLComputeResult {
+ MLNamedArrayBufferViews inputs;
+ MLNamedArrayBufferViews outputs;
+};
+
+partial interface MLContext {
+ Promise<MLComputeResult> compute(
+ MLGraph graph, MLNamedArrayBufferViews inputs, MLNamedArrayBufferViews outputs);
+};
+
+partial interface MLContext {
+ MLCommandEncoder createCommandEncoder();
+};
+
+typedef (GPUBuffer or GPUTexture) MLGPUResource;
+
+typedef record<DOMString, MLGPUResource> MLNamedGPUResources;
+
+[SecureContext, Exposed=(Window, DedicatedWorker)]
+interface MLCommandEncoder {};
+
+partial interface MLCommandEncoder {
+ undefined initializeGraph(MLGraph graph);
+};
+
+partial interface MLCommandEncoder {
+ undefined dispatch(MLGraph graph, MLNamedGPUResources inputs, MLNamedGPUResources outputs);
+};
+
+partial interface MLCommandEncoder {
+ GPUCommandBuffer finish(optional GPUCommandBufferDescriptor descriptor = {});
+};
+
+typedef record<DOMString, MLOperand> MLNamedOperands;
+
+dictionary MLBufferResourceView {
+ required GPUBuffer resource;
+ unsigned long long offset = 0;
+ unsigned long long size;
+};
+
+typedef (ArrayBufferView or MLBufferResourceView) MLBufferView;
+
+[SecureContext, Exposed=(Window, DedicatedWorker)]
+interface MLGraphBuilder {
+ // Construct the graph builder from the context.
+ constructor(MLContext context);
+
+ // Create an operand for a graph input.
+ MLOperand input(DOMString name, MLOperandDescriptor descriptor);
+
+ // Create an operand for a graph constant.
+ MLOperand constant(MLOperandDescriptor descriptor, MLBufferView bufferView);
+
+ // Create a single-value operand from the specified number of the specified type.
+ MLOperand constant(double value, optional MLOperandType type = "float32");
+
+ // Compile the graph up to the specified output operands asynchronously.
+ Promise<MLGraph> build(MLNamedOperands outputs);
+
+ // Compile the graph up to the specified output operands synchronously.
+ [Exposed=(DedicatedWorker)]
+ MLGraph buildSync(MLNamedOperands outputs);
+};
+
+dictionary MLBatchNormalizationOptions {
+ MLOperand scale;
+ MLOperand bias;
+ unsigned long axis = 1;
+ float epsilon = 1e-5;
+ MLActivation activation;
+};
+
+partial interface MLGraphBuilder {
+ MLOperand batchNormalization(MLOperand input, MLOperand mean, MLOperand variance,
+ optional MLBatchNormalizationOptions options = {});
+};
+
+dictionary MLClampOptions {
+ float minValue;
+ float maxValue;
+};
+
+partial interface MLGraphBuilder {
+ MLOperand clamp(MLOperand x, optional MLClampOptions options = {});
+ MLActivation clamp(optional MLClampOptions options = {});
+};
+
+partial interface MLGraphBuilder {
+ MLOperand concat(sequence<MLOperand> inputs, unsigned long axis);
+};
+
+enum MLConv2dFilterOperandLayout {
+ "oihw",
+ "hwio",
+ "ohwi",
+ "ihwo"
+};
+
+enum MLAutoPad {
+ "explicit",
+ "same-upper",
+ "same-lower"
+};
+
+dictionary MLConv2dOptions {
+ sequence<unsigned long> padding;
+ sequence<unsigned long> strides;
+ sequence<unsigned long> dilations;
+ MLAutoPad autoPad = "explicit";
+ unsigned long groups = 1;
+ MLInputOperandLayout inputLayout = "nchw";
+ MLConv2dFilterOperandLayout filterLayout = "oihw";
+ MLOperand bias;
+ MLActivation activation;
+};
+
+partial interface MLGraphBuilder {
+ MLOperand conv2d(MLOperand input, MLOperand filter, optional MLConv2dOptions options = {});
+};
+
+enum MLConvTranspose2dFilterOperandLayout {
+ "iohw",
+ "hwoi",
+ "ohwi"
+};
+
+dictionary MLConvTranspose2dOptions {
+ sequence<unsigned long> padding;
+ sequence<unsigned long> strides;
+ sequence<unsigned long> dilations;
+ sequence<unsigned long> outputPadding;
+ sequence<unsigned long> outputSizes;
+ MLAutoPad autoPad = "explicit";
+ unsigned long groups = 1;
+ MLInputOperandLayout inputLayout = "nchw";
+ MLConvTranspose2dFilterOperandLayout filterLayout = "iohw";
+ MLOperand bias;
+ MLActivation activation;
+};
+
+partial interface MLGraphBuilder {
+ MLOperand convTranspose2d(MLOperand input, MLOperand filter,
+ optional MLConvTranspose2dOptions options = {});
+};
+
+partial interface MLGraphBuilder {
+ MLOperand add(MLOperand a, MLOperand b);
+ MLOperand sub(MLOperand a, MLOperand b);
+ MLOperand mul(MLOperand a, MLOperand b);
+ MLOperand div(MLOperand a, MLOperand b);
+ MLOperand max(MLOperand a, MLOperand b);
+ MLOperand min(MLOperand a, MLOperand b);
+ MLOperand pow(MLOperand a, MLOperand b);
+};
+
+partial interface MLGraphBuilder {
+ MLOperand abs(MLOperand x);
+ MLOperand ceil(MLOperand x);
+ MLOperand cos(MLOperand x);
+ MLOperand exp(MLOperand x);
+ MLOperand floor(MLOperand x);
+ MLOperand log(MLOperand x);
+ MLOperand neg(MLOperand x);
+ MLOperand sin(MLOperand x);
+ MLOperand tan(MLOperand x);
+};
+
+dictionary MLEluOptions {
+ float alpha = 1;
+};
+
+partial interface MLGraphBuilder {
+ MLOperand elu(MLOperand x, optional MLEluOptions options = {});
+ MLActivation elu(optional MLEluOptions options = {});
+};
+
+dictionary MLGemmOptions {
+ MLOperand c;
+ float alpha = 1.0;
+ float beta = 1.0;
+ boolean aTranspose = false;
+ boolean bTranspose = false;
+};
+
+partial interface MLGraphBuilder {
+ MLOperand gemm(MLOperand a, MLOperand b, optional MLGemmOptions options = {});
+};
+
+enum MLGruWeightLayout {
+ "zrn", // update-reset-new gate ordering
+ "rzn" // reset-update-new gate ordering
+};
+
+enum MLRecurrentNetworkDirection {
+ "forward",
+ "backward",
+ "both"
+};
+
+dictionary MLGruOptions {
+ MLOperand bias;
+ MLOperand recurrentBias;
+ MLOperand initialHiddenState;
+ boolean resetAfter = true;
+ boolean returnSequence = false;
+ MLRecurrentNetworkDirection direction = "forward";
+ MLGruWeightLayout layout = "zrn";
+ sequence<MLActivation> activations;
+};
+
+partial interface MLGraphBuilder {
+ sequence<MLOperand> gru(MLOperand input, MLOperand weight, MLOperand recurrentWeight,
+ unsigned long steps, unsigned long hiddenSize,
+ optional MLGruOptions options = {});
+};
+
+dictionary MLGruCellOptions {
+ MLOperand bias;
+ MLOperand recurrentBias;
+ boolean resetAfter = true;
+ MLGruWeightLayout layout = "zrn";
+ sequence<MLActivation> activations;
+};
+
+partial interface MLGraphBuilder {
+ MLOperand gruCell(MLOperand input, MLOperand weight, MLOperand recurrentWeight,
+ MLOperand hiddenState, unsigned long hiddenSize,
+ optional MLGruCellOptions options = {});
+};
+
+dictionary MLHardSigmoidOptions {
+ float alpha = 0.2;
+ float beta = 0.5;
+};
+
+partial interface MLGraphBuilder {
+ MLOperand hardSigmoid(MLOperand x, optional MLHardSigmoidOptions options = {});
+ MLActivation hardSigmoid(optional MLHardSigmoidOptions options = {});
+};
+
+partial interface MLGraphBuilder {
+ MLOperand hardSwish(MLOperand x);
+ MLActivation hardSwish();
+};
+
+dictionary MLInstanceNormalizationOptions {
+ MLOperand scale;
+ MLOperand bias;
+ float epsilon = 1e-5;
+ MLInputOperandLayout layout = "nchw";
+};
+
+partial interface MLGraphBuilder {
+ MLOperand instanceNormalization(MLOperand input,
+ optional MLInstanceNormalizationOptions options = {});
+};
+
+dictionary MLLeakyReluOptions {
+ float alpha = 0.01;
+};
+
+partial interface MLGraphBuilder {
+ MLOperand leakyRelu(MLOperand x, optional MLLeakyReluOptions options = {});
+ MLActivation leakyRelu(optional MLLeakyReluOptions options = {});
+};
+
+dictionary MLLinearOptions {
+ float alpha = 1;
+ float beta = 0;
+};
+
+partial interface MLGraphBuilder {
+ MLOperand linear(MLOperand x, optional MLLinearOptions options = {});
+ MLActivation linear(optional MLLinearOptions options = {});
+};
+
+enum MLLstmWeightLayout {
+ "iofg", // input-output-forget-cell gate ordering
+ "ifgo" // input-forget-cell-output gate ordering
+};
+
+dictionary MLLstmOptions {
+ MLOperand bias;
+ MLOperand recurrentBias;
+ MLOperand peepholeWeight;
+ MLOperand initialHiddenState;
+ MLOperand initialCellState;
+ boolean returnSequence = false;
+ MLRecurrentNetworkDirection direction = "forward";
+ MLLstmWeightLayout layout = "iofg";
+ sequence<MLActivation> activations;
+};
+
+partial interface MLGraphBuilder {
+ sequence<MLOperand> lstm(MLOperand input, MLOperand weight, MLOperand recurrentWeight,
+ unsigned long steps, unsigned long hiddenSize,
+ optional MLLstmOptions options = {});
+};
+
+dictionary MLLstmCellOptions {
+ MLOperand bias;
+ MLOperand recurrentBias;
+ MLOperand peepholeWeight;
+ MLLstmWeightLayout layout = "iofg";
+ sequence<MLActivation> activations;
+};
+
+partial interface MLGraphBuilder {
+ sequence<MLOperand> lstmCell(MLOperand input, MLOperand weight, MLOperand recurrentWeight,
+ MLOperand hiddenState, MLOperand cellState, unsigned long hiddenSize,
+ optional MLLstmCellOptions options = {});
+};
+
+partial interface MLGraphBuilder {
+ MLOperand matmul(MLOperand a, MLOperand b);
+};
+
+enum MLPaddingMode {
+ "constant",
+ "edge",
+ "reflection",
+ "symmetric"
+};
+
+dictionary MLPadOptions {
+ MLPaddingMode mode = "constant";
+ float value = 0;
+};
+
+partial interface MLGraphBuilder {
+ MLOperand pad(MLOperand input,
+ sequence<unsigned long> beginningPadding,
+ sequence<unsigned long> endingPadding,
+ optional MLPadOptions options = {});
+};
+
+enum MLRoundingType {
+ "floor",
+ "ceil"
+};
+
+dictionary MLPool2dOptions {
+ sequence<unsigned long> windowDimensions;
+ sequence<unsigned long> padding;
+ sequence<unsigned long> strides;
+ sequence<unsigned long> dilations;
+ MLAutoPad autoPad = "explicit";
+ MLInputOperandLayout layout = "nchw";
+ MLRoundingType roundingType = "floor";
+ sequence<unsigned long> outputSizes;
+};
+
+partial interface MLGraphBuilder {
+ MLOperand averagePool2d(MLOperand input, optional MLPool2dOptions options = {});
+ MLOperand l2Pool2d(MLOperand input, optional MLPool2dOptions options = {});
+ MLOperand maxPool2d(MLOperand input, optional MLPool2dOptions options = {});
+};
+
+partial interface MLGraphBuilder {
+ MLOperand prelu(MLOperand x, MLOperand slope);
+};
+
+dictionary MLReduceOptions {
+ sequence<unsigned long> axes = null;
+ boolean keepDimensions = false;
+};
+
+partial interface MLGraphBuilder {
+ MLOperand reduceL1(MLOperand input, optional MLReduceOptions options = {});
+ MLOperand reduceL2(MLOperand input, optional MLReduceOptions options = {});
+ MLOperand reduceLogSum(MLOperand input, optional MLReduceOptions options = {});
+ MLOperand reduceLogSumExp(MLOperand input, optional MLReduceOptions options = {});
+ MLOperand reduceMax(MLOperand input, optional MLReduceOptions options = {});
+ MLOperand reduceMean(MLOperand input, optional MLReduceOptions options = {});
+ MLOperand reduceMin(MLOperand input, optional MLReduceOptions options = {});
+ MLOperand reduceProduct(MLOperand input, optional MLReduceOptions options = {});
+ MLOperand reduceSum(MLOperand input, optional MLReduceOptions options = {});
+ MLOperand reduceSumSquare(MLOperand input, optional MLReduceOptions options = {});
+};
+
+partial interface MLGraphBuilder {
+ MLOperand relu(MLOperand x);
+ MLActivation relu();
+};
+
+enum MLInterpolationMode {
+ "nearest-neighbor",
+ "linear"
+};
+
+dictionary MLResample2dOptions {
+ MLInterpolationMode mode = "nearest-neighbor";
+ sequence<float> scales;
+ sequence<unsigned long> sizes;
+ sequence<unsigned long> axes;
+};
+
+partial interface MLGraphBuilder {
+ MLOperand resample2d(MLOperand input, optional MLResample2dOptions options = {});
+};
+
+partial interface MLGraphBuilder {
+ MLOperand reshape(MLOperand input, sequence<unsigned long?> newShape);
+};
+
+partial interface MLGraphBuilder {
+ MLOperand sigmoid(MLOperand x);
+ MLActivation sigmoid();
+};
+
+partial interface MLGraphBuilder {
+ MLOperand slice(MLOperand input, sequence<unsigned long> starts, sequence<unsigned long> sizes);
+};
+
+partial interface MLGraphBuilder {
+ MLOperand softmax(MLOperand x);
+ MLActivation softmax();
+};
+
+dictionary MLSoftplusOptions {
+ float steepness = 1;
+};
+
+partial interface MLGraphBuilder {
+ MLOperand softplus(MLOperand x, optional MLSoftplusOptions options = {});
+ MLActivation softplus(optional MLSoftplusOptions options = {});
+};
+
+partial interface MLGraphBuilder {
+ MLOperand softsign(MLOperand x);
+ MLActivation softsign();
+};
+
+dictionary MLSplitOptions {
+ unsigned long axis = 0;
+};
+
+partial interface MLGraphBuilder {
+ sequence<MLOperand> split(MLOperand input,
+ (unsigned long or sequence<unsigned long>) splits,
+ optional MLSplitOptions options = {});
+};
+
+dictionary MLSqueezeOptions {
+ sequence<unsigned long> axes;
+};
+
+partial interface MLGraphBuilder {
+ MLOperand squeeze(MLOperand input, optional MLSqueezeOptions options = {});
+};
+
+partial interface MLGraphBuilder {
+ MLOperand tanh(MLOperand x);
+ MLActivation tanh();
+};
+
+dictionary MLTransposeOptions {
+ sequence<unsigned long> permutation;
+};
+
+partial interface MLGraphBuilder {
+ MLOperand transpose(MLOperand input, optional MLTransposeOptions options = {});
+};
diff --git a/test/wpt/tests/interfaces/webrtc-encoded-transform.idl b/test/wpt/tests/interfaces/webrtc-encoded-transform.idl
new file mode 100644
index 0000000..6dd2ba3
--- /dev/null
+++ b/test/wpt/tests/interfaces/webrtc-encoded-transform.idl
@@ -0,0 +1,128 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: WebRTC Encoded Transform (https://w3c.github.io/webrtc-encoded-transform/)
+
+typedef (SFrameTransform or RTCRtpScriptTransform) RTCRtpTransform;
+
+// New methods for RTCRtpSender and RTCRtpReceiver
+partial interface RTCRtpSender {
+ attribute RTCRtpTransform? transform;
+};
+
+partial interface RTCRtpReceiver {
+ attribute RTCRtpTransform? transform;
+};
+
+enum SFrameTransformRole {
+ "encrypt",
+ "decrypt"
+};
+
+dictionary SFrameTransformOptions {
+ SFrameTransformRole role = "encrypt";
+};
+
+typedef [EnforceRange] unsigned long long SmallCryptoKeyID;
+typedef (SmallCryptoKeyID or bigint) CryptoKeyID;
+
+[Exposed=(Window,DedicatedWorker)]
+interface SFrameTransform : EventTarget {
+ constructor(optional SFrameTransformOptions options = {});
+ Promise<undefined> setEncryptionKey(CryptoKey key, optional CryptoKeyID keyID);
+ attribute EventHandler onerror;
+};
+SFrameTransform includes GenericTransformStream;
+
+enum SFrameTransformErrorEventType {
+ "authentication",
+ "keyID",
+ "syntax"
+};
+
+[Exposed=(Window,DedicatedWorker)]
+interface SFrameTransformErrorEvent : Event {
+ constructor(DOMString type, SFrameTransformErrorEventInit eventInitDict);
+
+ readonly attribute SFrameTransformErrorEventType errorType;
+ readonly attribute CryptoKeyID? keyID;
+ readonly attribute any frame;
+};
+
+dictionary SFrameTransformErrorEventInit : EventInit {
+ required SFrameTransformErrorEventType errorType;
+ required any frame;
+ CryptoKeyID? keyID;
+};
+
+// New enum for video frame types. Will eventually re-use the equivalent defined
+// by WebCodecs.
+enum RTCEncodedVideoFrameType {
+ "empty",
+ "key",
+ "delta",
+};
+
+dictionary RTCEncodedVideoFrameMetadata {
+ unsigned long long frameId;
+ sequence<unsigned long long> dependencies;
+ unsigned short width;
+ unsigned short height;
+ unsigned long spatialIndex;
+ unsigned long temporalIndex;
+ unsigned long synchronizationSource;
+ octet payloadType;
+ sequence<unsigned long> contributingSources;
+ long long timestamp; // microseconds
+};
+
+// New interfaces to define encoded video and audio frames. Will eventually
+// re-use or extend the equivalent defined in WebCodecs.
+[Exposed=(Window,DedicatedWorker)]
+interface RTCEncodedVideoFrame {
+ readonly attribute RTCEncodedVideoFrameType type;
+ readonly attribute unsigned long timestamp;
+ attribute ArrayBuffer data;
+ RTCEncodedVideoFrameMetadata getMetadata();
+};
+
+dictionary RTCEncodedAudioFrameMetadata {
+ unsigned long synchronizationSource;
+ octet payloadType;
+ sequence<unsigned long> contributingSources;
+ short sequenceNumber;
+};
+
+[Exposed=(Window,DedicatedWorker)]
+interface RTCEncodedAudioFrame {
+ readonly attribute unsigned long timestamp;
+ attribute ArrayBuffer data;
+ RTCEncodedAudioFrameMetadata getMetadata();
+};
+
+[Exposed=DedicatedWorker]
+interface RTCTransformEvent : Event {
+ readonly attribute RTCRtpScriptTransformer transformer;
+};
+
+partial interface DedicatedWorkerGlobalScope {
+ attribute EventHandler onrtctransform;
+};
+
+[Exposed=DedicatedWorker]
+interface RTCRtpScriptTransformer {
+ readonly attribute ReadableStream readable;
+ readonly attribute WritableStream writable;
+ readonly attribute any options;
+ Promise<unsigned long long> generateKeyFrame(optional DOMString rid);
+ Promise<undefined> sendKeyFrameRequest();
+};
+
+[Exposed=Window]
+interface RTCRtpScriptTransform {
+ constructor(Worker worker, optional any options, optional sequence<object> transfer);
+};
+
+partial interface RTCRtpSender {
+ Promise<undefined> generateKeyFrame(optional sequence <DOMString> rids);
+};
diff --git a/test/wpt/tests/interfaces/webrtc-ice.idl b/test/wpt/tests/interfaces/webrtc-ice.idl
new file mode 100644
index 0000000..58f88ba
--- /dev/null
+++ b/test/wpt/tests/interfaces/webrtc-ice.idl
@@ -0,0 +1,24 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: IceTransport Extensions for WebRTC (https://w3c.github.io/webrtc-ice/)
+
+partial dictionary RTCIceParameters {
+ boolean iceLite;
+};
+
+dictionary RTCIceGatherOptions {
+ RTCIceTransportPolicy gatherPolicy = "all";
+ sequence<RTCIceServer> iceServers;
+};
+
+[Exposed=Window]
+partial interface RTCIceTransport {
+ constructor();
+ undefined gather (optional RTCIceGatherOptions options = {});
+ undefined start (optional RTCIceParameters remoteParameters = {}, optional RTCIceRole role = "controlled");
+ undefined stop ();
+ undefined addRemoteCandidate (optional RTCIceCandidateInit remoteCandidate = {});
+ attribute EventHandler onerror;
+ attribute EventHandler onicecandidate;
+};
diff --git a/test/wpt/tests/interfaces/webrtc-identity.idl b/test/wpt/tests/interfaces/webrtc-identity.idl
new file mode 100644
index 0000000..108c3ad
--- /dev/null
+++ b/test/wpt/tests/interfaces/webrtc-identity.idl
@@ -0,0 +1,97 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Identity for WebRTC 1.0 (https://w3c.github.io/webrtc-identity/)
+
+[Global=(Worker,RTCIdentityProvider), Exposed=RTCIdentityProvider]
+interface RTCIdentityProviderGlobalScope : WorkerGlobalScope {
+ readonly attribute RTCIdentityProviderRegistrar rtcIdentityProvider;
+};
+
+[Exposed=RTCIdentityProvider]
+interface RTCIdentityProviderRegistrar {
+ undefined register (RTCIdentityProvider idp);
+};
+
+dictionary RTCIdentityProvider {
+ required GenerateAssertionCallback generateAssertion;
+ required ValidateAssertionCallback validateAssertion;
+};
+
+callback GenerateAssertionCallback = Promise<RTCIdentityAssertionResult>
+(DOMString contents, DOMString origin, RTCIdentityProviderOptions options);
+
+callback ValidateAssertionCallback = Promise<RTCIdentityValidationResult>
+(DOMString assertion, DOMString origin);
+
+dictionary RTCIdentityAssertionResult {
+ required RTCIdentityProviderDetails idp;
+ required DOMString assertion;
+};
+
+dictionary RTCIdentityProviderDetails {
+ required DOMString domain;
+ DOMString protocol = "default";
+};
+
+dictionary RTCIdentityValidationResult {
+ required DOMString identity;
+ required DOMString contents;
+};
+
+partial interface RTCPeerConnection {
+ undefined setIdentityProvider (DOMString provider, optional RTCIdentityProviderOptions options = {});
+ Promise<DOMString> getIdentityAssertion ();
+ readonly attribute Promise<RTCIdentityAssertion> peerIdentity;
+ readonly attribute DOMString? idpLoginUrl;
+ readonly attribute DOMString? idpErrorInfo;
+};
+
+partial dictionary RTCConfiguration {
+ DOMString peerIdentity;
+};
+
+dictionary RTCIdentityProviderOptions {
+ DOMString protocol = "default";
+ DOMString usernameHint;
+ DOMString peerIdentity;
+};
+
+[Exposed=Window]
+interface RTCIdentityAssertion {
+ constructor(DOMString idp, DOMString name);
+ attribute DOMString idp;
+ attribute DOMString name;
+};
+
+partial interface RTCError {
+ readonly attribute long? httpRequestStatusCode;
+};
+
+partial dictionary RTCErrorInit {
+ long httpRequestStatusCode;
+};
+
+// This is an extension of RTCErrorDetailType from [[WEBRTC-PC]]
+// Unfortunately, WebIDL does not support partial enums (yet).
+//
+// partial enum RTCErrorDetailType {
+enum RTCErrorDetailTypeIdp {
+ "idp-bad-script-failure",
+ "idp-execution-failure",
+ "idp-load-failure",
+ "idp-need-login",
+ "idp-timeout",
+ "idp-tls-failure",
+ "idp-token-expired",
+ "idp-token-invalid",
+};
+
+partial dictionary MediaStreamConstraints {
+ DOMString peerIdentity;
+};
+
+partial interface MediaStreamTrack {
+ readonly attribute boolean isolated;
+ attribute EventHandler onisolationchange;
+};
diff --git a/test/wpt/tests/interfaces/webrtc-priority.idl b/test/wpt/tests/interfaces/webrtc-priority.idl
new file mode 100644
index 0000000..a76952b
--- /dev/null
+++ b/test/wpt/tests/interfaces/webrtc-priority.idl
@@ -0,0 +1,24 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: WebRTC Priority Control API (https://w3c.github.io/webrtc-priority/)
+
+enum RTCPriorityType {
+ "very-low",
+ "low",
+ "medium",
+ "high"
+};
+
+partial dictionary RTCRtpEncodingParameters {
+ RTCPriorityType priority = "low";
+ RTCPriorityType networkPriority;
+};
+
+partial interface RTCDataChannel {
+ readonly attribute RTCPriorityType priority;
+};
+
+partial dictionary RTCDataChannelInit {
+ RTCPriorityType priority = "low";
+};
diff --git a/test/wpt/tests/interfaces/webrtc-stats.idl b/test/wpt/tests/interfaces/webrtc-stats.idl
new file mode 100644
index 0000000..b398c73
--- /dev/null
+++ b/test/wpt/tests/interfaces/webrtc-stats.idl
@@ -0,0 +1,288 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Identifiers for WebRTC's Statistics API (https://w3c.github.io/webrtc-stats/)
+
+enum RTCStatsType {
+"codec",
+"inbound-rtp",
+"outbound-rtp",
+"remote-inbound-rtp",
+"remote-outbound-rtp",
+"media-source",
+"media-playout",
+"peer-connection",
+"data-channel",
+"transport",
+"candidate-pair",
+"local-candidate",
+"remote-candidate",
+"certificate"
+};
+
+dictionary RTCRtpStreamStats : RTCStats {
+ required unsigned long ssrc;
+ required DOMString kind;
+ DOMString transportId;
+ DOMString codecId;
+};
+
+dictionary RTCCodecStats : RTCStats {
+ required unsigned long payloadType;
+ required DOMString transportId;
+ required DOMString mimeType;
+ unsigned long clockRate;
+ unsigned long channels;
+ DOMString sdpFmtpLine;
+};
+
+dictionary RTCReceivedRtpStreamStats : RTCRtpStreamStats {
+ unsigned long long packetsReceived;
+ long long packetsLost;
+ double jitter;
+};
+
+dictionary RTCInboundRtpStreamStats : RTCReceivedRtpStreamStats {
+ required DOMString trackIdentifier;
+ required DOMString kind;
+ DOMString mid;
+ DOMString remoteId;
+ unsigned long framesDecoded;
+ unsigned long keyFramesDecoded;
+ unsigned long framesRendered;
+ unsigned long framesDropped;
+ unsigned long frameWidth;
+ unsigned long frameHeight;
+ double framesPerSecond;
+ unsigned long long qpSum;
+ double totalDecodeTime;
+ double totalInterFrameDelay;
+ double totalSquaredInterFrameDelay;
+ unsigned long pauseCount;
+ double totalPausesDuration;
+ unsigned long freezeCount;
+ double totalFreezesDuration;
+ DOMHighResTimeStamp lastPacketReceivedTimestamp;
+ unsigned long long headerBytesReceived;
+ unsigned long long packetsDiscarded;
+ unsigned long long fecPacketsReceived;
+ unsigned long long fecPacketsDiscarded;
+ unsigned long long bytesReceived;
+ unsigned long nackCount;
+ unsigned long firCount;
+ unsigned long pliCount;
+ double totalProcessingDelay;
+ DOMHighResTimeStamp estimatedPlayoutTimestamp;
+ double jitterBufferDelay;
+ double jitterBufferTargetDelay;
+ unsigned long long jitterBufferEmittedCount;
+ double jitterBufferMinimumDelay;
+ unsigned long long totalSamplesReceived;
+ unsigned long long concealedSamples;
+ unsigned long long silentConcealedSamples;
+ unsigned long long concealmentEvents;
+ unsigned long long insertedSamplesForDeceleration;
+ unsigned long long removedSamplesForAcceleration;
+ double audioLevel;
+ double totalAudioEnergy;
+ double totalSamplesDuration;
+ unsigned long framesReceived;
+ DOMString decoderImplementation;
+ DOMString playoutId;
+ boolean powerEfficientDecoder;
+ unsigned long framesAssembledFromMultiplePackets;
+ double totalAssemblyTime;
+ unsigned long long retransmittedPacketsReceived;
+ unsigned long long retransmittedBytesReceived;
+};
+
+dictionary RTCRemoteInboundRtpStreamStats : RTCReceivedRtpStreamStats {
+ DOMString localId;
+ double roundTripTime;
+ double totalRoundTripTime;
+ double fractionLost;
+ unsigned long long roundTripTimeMeasurements;
+};
+
+dictionary RTCSentRtpStreamStats : RTCRtpStreamStats {
+ unsigned long long packetsSent;
+ unsigned long long bytesSent;
+};
+
+dictionary RTCOutboundRtpStreamStats : RTCSentRtpStreamStats {
+ DOMString mid;
+ DOMString mediaSourceId;
+ DOMString remoteId;
+ DOMString rid;
+ unsigned long long headerBytesSent;
+ unsigned long long retransmittedPacketsSent;
+ unsigned long long retransmittedBytesSent;
+ double targetBitrate;
+ unsigned long long totalEncodedBytesTarget;
+ unsigned long frameWidth;
+ unsigned long frameHeight;
+ double framesPerSecond;
+ unsigned long framesSent;
+ unsigned long hugeFramesSent;
+ unsigned long framesEncoded;
+ unsigned long keyFramesEncoded;
+ unsigned long long qpSum;
+ double totalEncodeTime;
+ double totalPacketSendDelay;
+ RTCQualityLimitationReason qualityLimitationReason;
+ record<DOMString, double> qualityLimitationDurations;
+ unsigned long qualityLimitationResolutionChanges;
+ unsigned long nackCount;
+ unsigned long firCount;
+ unsigned long pliCount;
+ DOMString encoderImplementation;
+ boolean powerEfficientEncoder;
+ boolean active;
+ DOMString scalabilityMode;
+};
+
+enum RTCQualityLimitationReason {
+ "none",
+ "cpu",
+ "bandwidth",
+ "other",
+};
+
+dictionary RTCRemoteOutboundRtpStreamStats : RTCSentRtpStreamStats {
+ DOMString localId;
+ DOMHighResTimeStamp remoteTimestamp;
+ unsigned long long reportsSent;
+ double roundTripTime;
+ double totalRoundTripTime;
+ unsigned long long roundTripTimeMeasurements;
+};
+
+dictionary RTCMediaSourceStats : RTCStats {
+ required DOMString trackIdentifier;
+ required DOMString kind;
+};
+
+dictionary RTCAudioSourceStats : RTCMediaSourceStats {
+ double audioLevel;
+ double totalAudioEnergy;
+ double totalSamplesDuration;
+ double echoReturnLoss;
+ double echoReturnLossEnhancement;
+ double droppedSamplesDuration;
+ unsigned long droppedSamplesEvents;
+ double totalCaptureDelay;
+ unsigned long long totalSamplesCaptured;
+};
+
+dictionary RTCVideoSourceStats : RTCMediaSourceStats {
+ unsigned long width;
+ unsigned long height;
+ unsigned long frames;
+ double framesPerSecond;
+};
+
+dictionary RTCAudioPlayoutStats : RTCStats {
+ required DOMString kind;
+ double synthesizedSamplesDuration;
+ unsigned long synthesizedSamplesEvents;
+ double totalSamplesDuration;
+ double totalPlayoutDelay;
+ unsigned long long totalSamplesCount;
+};
+
+dictionary RTCPeerConnectionStats : RTCStats {
+ unsigned long dataChannelsOpened;
+ unsigned long dataChannelsClosed;
+};
+
+dictionary RTCDataChannelStats : RTCStats {
+ DOMString label;
+ DOMString protocol;
+ unsigned short dataChannelIdentifier;
+ required RTCDataChannelState state;
+ unsigned long messagesSent;
+ unsigned long long bytesSent;
+ unsigned long messagesReceived;
+ unsigned long long bytesReceived;
+};
+
+dictionary RTCTransportStats : RTCStats {
+ unsigned long long packetsSent;
+ unsigned long long packetsReceived;
+ unsigned long long bytesSent;
+ unsigned long long bytesReceived;
+ RTCIceRole iceRole;
+ DOMString iceLocalUsernameFragment;
+ required RTCDtlsTransportState dtlsState;
+ RTCIceTransportState iceState;
+ DOMString selectedCandidatePairId;
+ DOMString localCertificateId;
+ DOMString remoteCertificateId;
+ DOMString tlsVersion;
+ DOMString dtlsCipher;
+ RTCDtlsRole dtlsRole;
+ DOMString srtpCipher;
+ unsigned long selectedCandidatePairChanges;
+};
+
+enum RTCDtlsRole {
+ "client",
+ "server",
+ "unknown",
+};
+
+dictionary RTCIceCandidateStats : RTCStats {
+ required DOMString transportId;
+ DOMString? address;
+ long port;
+ DOMString protocol;
+ required RTCIceCandidateType candidateType;
+ long priority;
+ DOMString url;
+ RTCIceServerTransportProtocol relayProtocol;
+ DOMString foundation;
+ DOMString relatedAddress;
+ long relatedPort;
+ DOMString usernameFragment;
+ RTCIceTcpCandidateType tcpType;
+};
+
+dictionary RTCIceCandidatePairStats : RTCStats {
+ required DOMString transportId;
+ required DOMString localCandidateId;
+ required DOMString remoteCandidateId;
+ required RTCStatsIceCandidatePairState state;
+ boolean nominated;
+ unsigned long long packetsSent;
+ unsigned long long packetsReceived;
+ unsigned long long bytesSent;
+ unsigned long long bytesReceived;
+ DOMHighResTimeStamp lastPacketSentTimestamp;
+ DOMHighResTimeStamp lastPacketReceivedTimestamp;
+ double totalRoundTripTime;
+ double currentRoundTripTime;
+ double availableOutgoingBitrate;
+ double availableIncomingBitrate;
+ unsigned long long requestsReceived;
+ unsigned long long requestsSent;
+ unsigned long long responsesReceived;
+ unsigned long long responsesSent;
+ unsigned long long consentRequestsSent;
+ unsigned long packetsDiscardedOnSend;
+ unsigned long long bytesDiscardedOnSend;
+};
+
+enum RTCStatsIceCandidatePairState {
+ "frozen",
+ "waiting",
+ "in-progress",
+ "failed",
+ "succeeded"
+};
+
+dictionary RTCCertificateStats : RTCStats {
+ required DOMString fingerprint;
+ required DOMString fingerprintAlgorithm;
+ required DOMString base64Certificate;
+ DOMString issuerCertificateId;
+};
diff --git a/test/wpt/tests/interfaces/webrtc-svc.idl b/test/wpt/tests/interfaces/webrtc-svc.idl
new file mode 100644
index 0000000..e67ed70
--- /dev/null
+++ b/test/wpt/tests/interfaces/webrtc-svc.idl
@@ -0,0 +1,8 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Scalable Video Coding (SVC) Extension for WebRTC (https://w3c.github.io/webrtc-svc/)
+
+partial dictionary RTCRtpEncodingParameters {
+ DOMString scalabilityMode;
+};
diff --git a/test/wpt/tests/interfaces/webrtc.idl b/test/wpt/tests/interfaces/webrtc.idl
new file mode 100644
index 0000000..4c31d3b
--- /dev/null
+++ b/test/wpt/tests/interfaces/webrtc.idl
@@ -0,0 +1,627 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: WebRTC: Real-Time Communication in Browsers (https://w3c.github.io/webrtc-pc/)
+
+dictionary RTCConfiguration {
+ sequence<RTCIceServer> iceServers = [];
+ RTCIceTransportPolicy iceTransportPolicy = "all";
+ RTCBundlePolicy bundlePolicy = "balanced";
+ RTCRtcpMuxPolicy rtcpMuxPolicy = "require";
+ sequence<RTCCertificate> certificates = [];
+ [EnforceRange] octet iceCandidatePoolSize = 0;
+};
+
+dictionary RTCIceServer {
+ required (DOMString or sequence<DOMString>) urls;
+ DOMString username;
+ DOMString credential;
+};
+
+enum RTCIceTransportPolicy {
+ "relay",
+ "all"
+};
+
+enum RTCBundlePolicy {
+ "balanced",
+ "max-compat",
+ "max-bundle"
+};
+
+enum RTCRtcpMuxPolicy {
+ "require"
+};
+
+dictionary RTCOfferAnswerOptions {};
+
+dictionary RTCOfferOptions : RTCOfferAnswerOptions {
+ boolean iceRestart = false;
+};
+
+dictionary RTCAnswerOptions : RTCOfferAnswerOptions {};
+
+enum RTCSignalingState {
+ "stable",
+ "have-local-offer",
+ "have-remote-offer",
+ "have-local-pranswer",
+ "have-remote-pranswer",
+ "closed"
+};
+
+enum RTCIceGatheringState {
+ "new",
+ "gathering",
+ "complete"
+};
+
+enum RTCPeerConnectionState {
+ "closed",
+ "failed",
+ "disconnected",
+ "new",
+ "connecting",
+ "connected"
+};
+
+enum RTCIceConnectionState {
+ "closed",
+ "failed",
+ "disconnected",
+ "new",
+ "checking",
+ "completed",
+ "connected"
+};
+
+[Exposed=Window]
+interface RTCPeerConnection : EventTarget {
+ constructor(optional RTCConfiguration configuration = {});
+ Promise<RTCSessionDescriptionInit> createOffer(optional RTCOfferOptions options = {});
+ Promise<RTCSessionDescriptionInit> createAnswer(optional RTCAnswerOptions options = {});
+ Promise<undefined> setLocalDescription(optional RTCLocalSessionDescriptionInit description = {});
+ readonly attribute RTCSessionDescription? localDescription;
+ readonly attribute RTCSessionDescription? currentLocalDescription;
+ readonly attribute RTCSessionDescription? pendingLocalDescription;
+ Promise<undefined> setRemoteDescription(RTCSessionDescriptionInit description);
+ readonly attribute RTCSessionDescription? remoteDescription;
+ readonly attribute RTCSessionDescription? currentRemoteDescription;
+ readonly attribute RTCSessionDescription? pendingRemoteDescription;
+ Promise<undefined> addIceCandidate(optional RTCIceCandidateInit candidate = {});
+ readonly attribute RTCSignalingState signalingState;
+ readonly attribute RTCIceGatheringState iceGatheringState;
+ readonly attribute RTCIceConnectionState iceConnectionState;
+ readonly attribute RTCPeerConnectionState connectionState;
+ readonly attribute boolean? canTrickleIceCandidates;
+ undefined restartIce();
+ RTCConfiguration getConfiguration();
+ undefined setConfiguration(optional RTCConfiguration configuration = {});
+ undefined close();
+ attribute EventHandler onnegotiationneeded;
+ attribute EventHandler onicecandidate;
+ attribute EventHandler onicecandidateerror;
+ attribute EventHandler onsignalingstatechange;
+ attribute EventHandler oniceconnectionstatechange;
+ attribute EventHandler onicegatheringstatechange;
+ attribute EventHandler onconnectionstatechange;
+
+ // Legacy Interface Extensions
+ // Supporting the methods in this section is optional.
+ // If these methods are supported
+ // they must be implemented as defined
+ // in section "Legacy Interface Extensions"
+ Promise<undefined> createOffer(RTCSessionDescriptionCallback successCallback,
+ RTCPeerConnectionErrorCallback failureCallback,
+ optional RTCOfferOptions options = {});
+ Promise<undefined> setLocalDescription(RTCLocalSessionDescriptionInit description,
+ VoidFunction successCallback,
+ RTCPeerConnectionErrorCallback failureCallback);
+ Promise<undefined> createAnswer(RTCSessionDescriptionCallback successCallback,
+ RTCPeerConnectionErrorCallback failureCallback);
+ Promise<undefined> setRemoteDescription(RTCSessionDescriptionInit description,
+ VoidFunction successCallback,
+ RTCPeerConnectionErrorCallback failureCallback);
+ Promise<undefined> addIceCandidate(RTCIceCandidateInit candidate,
+ VoidFunction successCallback,
+ RTCPeerConnectionErrorCallback failureCallback);
+};
+
+callback RTCPeerConnectionErrorCallback = undefined (DOMException error);
+
+callback RTCSessionDescriptionCallback = undefined (RTCSessionDescriptionInit description);
+
+partial dictionary RTCOfferOptions {
+ boolean offerToReceiveAudio;
+ boolean offerToReceiveVideo;
+};
+
+enum RTCSdpType {
+ "offer",
+ "pranswer",
+ "answer",
+ "rollback"
+};
+
+[Exposed=Window]
+interface RTCSessionDescription {
+ constructor(RTCSessionDescriptionInit descriptionInitDict);
+ readonly attribute RTCSdpType type;
+ readonly attribute DOMString sdp;
+ [Default] object toJSON();
+};
+
+dictionary RTCSessionDescriptionInit {
+ required RTCSdpType type;
+ DOMString sdp = "";
+};
+
+dictionary RTCLocalSessionDescriptionInit {
+ RTCSdpType type;
+ DOMString sdp = "";
+};
+
+[Exposed=Window]
+interface RTCIceCandidate {
+ constructor(optional RTCIceCandidateInit candidateInitDict = {});
+ readonly attribute DOMString candidate;
+ readonly attribute DOMString? sdpMid;
+ readonly attribute unsigned short? sdpMLineIndex;
+ readonly attribute DOMString? foundation;
+ readonly attribute RTCIceComponent? component;
+ readonly attribute unsigned long? priority;
+ readonly attribute DOMString? address;
+ readonly attribute RTCIceProtocol? protocol;
+ readonly attribute unsigned short? port;
+ readonly attribute RTCIceCandidateType? type;
+ readonly attribute RTCIceTcpCandidateType? tcpType;
+ readonly attribute DOMString? relatedAddress;
+ readonly attribute unsigned short? relatedPort;
+ readonly attribute DOMString? usernameFragment;
+ readonly attribute RTCIceServerTransportProtocol? relayProtocol;
+ readonly attribute DOMString? url;
+ RTCIceCandidateInit toJSON();
+};
+
+dictionary RTCIceCandidateInit {
+ DOMString candidate = "";
+ DOMString? sdpMid = null;
+ unsigned short? sdpMLineIndex = null;
+ DOMString? usernameFragment = null;
+};
+
+enum RTCIceProtocol {
+ "udp",
+ "tcp"
+};
+
+enum RTCIceTcpCandidateType {
+ "active",
+ "passive",
+ "so"
+};
+
+enum RTCIceCandidateType {
+ "host",
+ "srflx",
+ "prflx",
+ "relay"
+};
+
+enum RTCIceServerTransportProtocol {
+ "udp",
+ "tcp",
+ "tls",
+};
+
+[Exposed=Window]
+interface RTCPeerConnectionIceEvent : Event {
+ constructor(DOMString type, optional RTCPeerConnectionIceEventInit eventInitDict = {});
+ readonly attribute RTCIceCandidate? candidate;
+ readonly attribute DOMString? url;
+};
+
+dictionary RTCPeerConnectionIceEventInit : EventInit {
+ RTCIceCandidate? candidate;
+ DOMString? url;
+};
+
+[Exposed=Window]
+interface RTCPeerConnectionIceErrorEvent : Event {
+ constructor(DOMString type, RTCPeerConnectionIceErrorEventInit eventInitDict);
+ readonly attribute DOMString? address;
+ readonly attribute unsigned short? port;
+ readonly attribute DOMString url;
+ readonly attribute unsigned short errorCode;
+ readonly attribute USVString errorText;
+};
+
+dictionary RTCPeerConnectionIceErrorEventInit : EventInit {
+ DOMString? address;
+ unsigned short? port;
+ DOMString url;
+ required unsigned short errorCode;
+ USVString errorText;
+};
+
+partial interface RTCPeerConnection {
+ static Promise<RTCCertificate>
+ generateCertificate(AlgorithmIdentifier keygenAlgorithm);
+};
+
+dictionary RTCCertificateExpiration {
+ [EnforceRange] unsigned long long expires;
+};
+
+[Exposed=Window, Serializable]
+interface RTCCertificate {
+ readonly attribute EpochTimeStamp expires;
+ sequence<RTCDtlsFingerprint> getFingerprints();
+};
+
+partial interface RTCPeerConnection {
+ sequence<RTCRtpSender> getSenders();
+ sequence<RTCRtpReceiver> getReceivers();
+ sequence<RTCRtpTransceiver> getTransceivers();
+ RTCRtpSender addTrack(MediaStreamTrack track, MediaStream... streams);
+ undefined removeTrack(RTCRtpSender sender);
+ RTCRtpTransceiver addTransceiver((MediaStreamTrack or DOMString) trackOrKind,
+ optional RTCRtpTransceiverInit init = {});
+ attribute EventHandler ontrack;
+};
+
+dictionary RTCRtpTransceiverInit {
+ RTCRtpTransceiverDirection direction = "sendrecv";
+ sequence<MediaStream> streams = [];
+ sequence<RTCRtpEncodingParameters> sendEncodings = [];
+};
+
+enum RTCRtpTransceiverDirection {
+ "sendrecv",
+ "sendonly",
+ "recvonly",
+ "inactive",
+ "stopped"
+};
+
+[Exposed=Window]
+interface RTCRtpSender {
+ readonly attribute MediaStreamTrack? track;
+ readonly attribute RTCDtlsTransport? transport;
+ static RTCRtpCapabilities? getCapabilities(DOMString kind);
+ Promise<undefined> setParameters(RTCRtpSendParameters parameters);
+ RTCRtpSendParameters getParameters();
+ Promise<undefined> replaceTrack(MediaStreamTrack? withTrack);
+ undefined setStreams(MediaStream... streams);
+ Promise<RTCStatsReport> getStats();
+};
+
+dictionary RTCRtpParameters {
+ required sequence<RTCRtpHeaderExtensionParameters> headerExtensions;
+ required RTCRtcpParameters rtcp;
+ required sequence<RTCRtpCodecParameters> codecs;
+};
+
+dictionary RTCRtpSendParameters : RTCRtpParameters {
+ required DOMString transactionId;
+ required sequence<RTCRtpEncodingParameters> encodings;
+};
+
+dictionary RTCRtpReceiveParameters : RTCRtpParameters {
+};
+
+dictionary RTCRtpCodingParameters {
+ DOMString rid;
+};
+
+dictionary RTCRtpEncodingParameters : RTCRtpCodingParameters {
+ boolean active = true;
+ unsigned long maxBitrate;
+ double maxFramerate;
+ double scaleResolutionDownBy;
+};
+
+dictionary RTCRtcpParameters {
+ DOMString cname;
+ boolean reducedSize;
+};
+
+dictionary RTCRtpHeaderExtensionParameters {
+ required DOMString uri;
+ required unsigned short id;
+ boolean encrypted = false;
+};
+
+dictionary RTCRtpCodec {
+ required DOMString mimeType;
+ required unsigned long clockRate;
+ unsigned short channels;
+ DOMString sdpFmtpLine;
+};
+
+dictionary RTCRtpCodecParameters : RTCRtpCodec {
+ required octet payloadType;
+};
+
+dictionary RTCRtpCapabilities {
+ required sequence<RTCRtpCodecCapability> codecs;
+ required sequence<RTCRtpHeaderExtensionCapability> headerExtensions;
+};
+
+dictionary RTCRtpCodecCapability : RTCRtpCodec {
+};
+
+dictionary RTCRtpHeaderExtensionCapability {
+ required DOMString uri;
+};
+
+[Exposed=Window]
+interface RTCRtpReceiver {
+ readonly attribute MediaStreamTrack track;
+ readonly attribute RTCDtlsTransport? transport;
+ static RTCRtpCapabilities? getCapabilities(DOMString kind);
+ RTCRtpReceiveParameters getParameters();
+ sequence<RTCRtpContributingSource> getContributingSources();
+ sequence<RTCRtpSynchronizationSource> getSynchronizationSources();
+ Promise<RTCStatsReport> getStats();
+};
+
+dictionary RTCRtpContributingSource {
+ required DOMHighResTimeStamp timestamp;
+ required unsigned long source;
+ double audioLevel;
+ required unsigned long rtpTimestamp;
+};
+
+dictionary RTCRtpSynchronizationSource : RTCRtpContributingSource {};
+
+[Exposed=Window]
+interface RTCRtpTransceiver {
+ readonly attribute DOMString? mid;
+ [SameObject] readonly attribute RTCRtpSender sender;
+ [SameObject] readonly attribute RTCRtpReceiver receiver;
+ attribute RTCRtpTransceiverDirection direction;
+ readonly attribute RTCRtpTransceiverDirection? currentDirection;
+ undefined stop();
+ undefined setCodecPreferences(sequence<RTCRtpCodecCapability> codecs);
+};
+
+[Exposed=Window]
+interface RTCDtlsTransport : EventTarget {
+ [SameObject] readonly attribute RTCIceTransport iceTransport;
+ readonly attribute RTCDtlsTransportState state;
+ sequence<ArrayBuffer> getRemoteCertificates();
+ attribute EventHandler onstatechange;
+ attribute EventHandler onerror;
+};
+
+enum RTCDtlsTransportState {
+ "new",
+ "connecting",
+ "connected",
+ "closed",
+ "failed"
+};
+
+dictionary RTCDtlsFingerprint {
+ DOMString algorithm;
+ DOMString value;
+};
+
+[Exposed=Window]
+interface RTCIceTransport : EventTarget {
+ readonly attribute RTCIceRole role;
+ readonly attribute RTCIceComponent component;
+ readonly attribute RTCIceTransportState state;
+ readonly attribute RTCIceGathererState gatheringState;
+ sequence<RTCIceCandidate> getLocalCandidates();
+ sequence<RTCIceCandidate> getRemoteCandidates();
+ RTCIceCandidatePair? getSelectedCandidatePair();
+ RTCIceParameters? getLocalParameters();
+ RTCIceParameters? getRemoteParameters();
+ attribute EventHandler onstatechange;
+ attribute EventHandler ongatheringstatechange;
+ attribute EventHandler onselectedcandidatepairchange;
+};
+
+dictionary RTCIceParameters {
+ DOMString usernameFragment;
+ DOMString password;
+};
+
+dictionary RTCIceCandidatePair {
+ RTCIceCandidate local;
+ RTCIceCandidate remote;
+};
+
+enum RTCIceGathererState {
+ "new",
+ "gathering",
+ "complete"
+};
+
+enum RTCIceTransportState {
+ "closed",
+ "failed",
+ "disconnected",
+ "new",
+ "checking",
+ "completed",
+ "connected"
+};
+
+enum RTCIceRole {
+ "unknown",
+ "controlling",
+ "controlled"
+};
+
+enum RTCIceComponent {
+ "rtp",
+ "rtcp"
+};
+
+[Exposed=Window]
+interface RTCTrackEvent : Event {
+ constructor(DOMString type, RTCTrackEventInit eventInitDict);
+ readonly attribute RTCRtpReceiver receiver;
+ readonly attribute MediaStreamTrack track;
+ [SameObject] readonly attribute FrozenArray<MediaStream> streams;
+ readonly attribute RTCRtpTransceiver transceiver;
+};
+
+dictionary RTCTrackEventInit : EventInit {
+ required RTCRtpReceiver receiver;
+ required MediaStreamTrack track;
+ sequence<MediaStream> streams = [];
+ required RTCRtpTransceiver transceiver;
+};
+
+partial interface RTCPeerConnection {
+ readonly attribute RTCSctpTransport? sctp;
+ RTCDataChannel createDataChannel(USVString label,
+ optional RTCDataChannelInit dataChannelDict = {});
+ attribute EventHandler ondatachannel;
+};
+
+[Exposed=Window]
+interface RTCSctpTransport : EventTarget {
+ readonly attribute RTCDtlsTransport transport;
+ readonly attribute RTCSctpTransportState state;
+ readonly attribute unrestricted double maxMessageSize;
+ readonly attribute unsigned short? maxChannels;
+ attribute EventHandler onstatechange;
+};
+
+enum RTCSctpTransportState {
+ "connecting",
+ "connected",
+ "closed"
+};
+
+[Exposed=Window]
+interface RTCDataChannel : EventTarget {
+ readonly attribute USVString label;
+ readonly attribute boolean ordered;
+ readonly attribute unsigned short? maxPacketLifeTime;
+ readonly attribute unsigned short? maxRetransmits;
+ readonly attribute USVString protocol;
+ readonly attribute boolean negotiated;
+ readonly attribute unsigned short? id;
+ readonly attribute RTCDataChannelState readyState;
+ readonly attribute unsigned long bufferedAmount;
+ [EnforceRange] attribute unsigned long bufferedAmountLowThreshold;
+ attribute EventHandler onopen;
+ attribute EventHandler onbufferedamountlow;
+ attribute EventHandler onerror;
+ attribute EventHandler onclosing;
+ attribute EventHandler onclose;
+ undefined close();
+ attribute EventHandler onmessage;
+ attribute BinaryType binaryType;
+ undefined send(USVString data);
+ undefined send(Blob data);
+ undefined send(ArrayBuffer data);
+ undefined send(ArrayBufferView data);
+};
+
+dictionary RTCDataChannelInit {
+ boolean ordered = true;
+ [EnforceRange] unsigned short maxPacketLifeTime;
+ [EnforceRange] unsigned short maxRetransmits;
+ USVString protocol = "";
+ boolean negotiated = false;
+ [EnforceRange] unsigned short id;
+};
+
+enum RTCDataChannelState {
+ "connecting",
+ "open",
+ "closing",
+ "closed"
+};
+
+[Exposed=Window]
+interface RTCDataChannelEvent : Event {
+ constructor(DOMString type, RTCDataChannelEventInit eventInitDict);
+ readonly attribute RTCDataChannel channel;
+};
+
+dictionary RTCDataChannelEventInit : EventInit {
+ required RTCDataChannel channel;
+};
+
+partial interface RTCRtpSender {
+ readonly attribute RTCDTMFSender? dtmf;
+};
+
+[Exposed=Window]
+interface RTCDTMFSender : EventTarget {
+ undefined insertDTMF(DOMString tones, optional unsigned long duration = 100, optional unsigned long interToneGap = 70);
+ attribute EventHandler ontonechange;
+ readonly attribute boolean canInsertDTMF;
+ readonly attribute DOMString toneBuffer;
+};
+
+[Exposed=Window]
+interface RTCDTMFToneChangeEvent : Event {
+ constructor(DOMString type, optional RTCDTMFToneChangeEventInit eventInitDict = {});
+ readonly attribute DOMString tone;
+};
+
+dictionary RTCDTMFToneChangeEventInit : EventInit {
+ DOMString tone = "";
+};
+
+partial interface RTCPeerConnection {
+ Promise<RTCStatsReport> getStats(optional MediaStreamTrack? selector = null);
+};
+
+[Exposed=Window]
+interface RTCStatsReport {
+ readonly maplike<DOMString, object>;
+};
+
+dictionary RTCStats {
+ required DOMHighResTimeStamp timestamp;
+ required RTCStatsType type;
+ required DOMString id;
+};
+
+[Exposed=Window]
+interface RTCError : DOMException {
+ constructor(RTCErrorInit init, optional DOMString message = "");
+ readonly attribute RTCErrorDetailType errorDetail;
+ readonly attribute long? sdpLineNumber;
+ readonly attribute long? sctpCauseCode;
+ readonly attribute unsigned long? receivedAlert;
+ readonly attribute unsigned long? sentAlert;
+};
+
+dictionary RTCErrorInit {
+ required RTCErrorDetailType errorDetail;
+ long sdpLineNumber;
+ long sctpCauseCode;
+ unsigned long receivedAlert;
+ unsigned long sentAlert;
+};
+
+enum RTCErrorDetailType {
+ "data-channel-failure",
+ "dtls-failure",
+ "fingerprint-failure",
+ "sctp-failure",
+ "sdp-syntax-error",
+ "hardware-encoder-not-available",
+ "hardware-encoder-error"
+};
+
+[Exposed=Window]
+interface RTCErrorEvent : Event {
+ constructor(DOMString type, RTCErrorEventInit eventInitDict);
+ [SameObject] readonly attribute RTCError error;
+};
+
+dictionary RTCErrorEventInit : EventInit {
+ required RTCError error;
+};
diff --git a/test/wpt/tests/interfaces/websockets.idl b/test/wpt/tests/interfaces/websockets.idl
new file mode 100644
index 0000000..6ff1679
--- /dev/null
+++ b/test/wpt/tests/interfaces/websockets.idl
@@ -0,0 +1,48 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: WebSockets Standard (https://websockets.spec.whatwg.org/)
+
+enum BinaryType { "blob", "arraybuffer" };
+
+[Exposed=(Window,Worker)]
+interface WebSocket : EventTarget {
+ constructor(USVString url, optional (DOMString or sequence<DOMString>) protocols = []);
+ readonly attribute USVString url;
+
+ // ready state
+ const unsigned short CONNECTING = 0;
+ const unsigned short OPEN = 1;
+ const unsigned short CLOSING = 2;
+ const unsigned short CLOSED = 3;
+ readonly attribute unsigned short readyState;
+ readonly attribute unsigned long long bufferedAmount;
+
+ // networking
+ attribute EventHandler onopen;
+ attribute EventHandler onerror;
+ attribute EventHandler onclose;
+ readonly attribute DOMString extensions;
+ readonly attribute DOMString protocol;
+ undefined close(optional [Clamp] unsigned short code, optional USVString reason);
+
+ // messaging
+ attribute EventHandler onmessage;
+ attribute BinaryType binaryType;
+ undefined send((BufferSource or Blob or USVString) data);
+};
+
+[Exposed=(Window,Worker)]
+interface CloseEvent : Event {
+ constructor(DOMString type, optional CloseEventInit eventInitDict = {});
+
+ readonly attribute boolean wasClean;
+ readonly attribute unsigned short code;
+ readonly attribute USVString reason;
+};
+
+dictionary CloseEventInit : EventInit {
+ boolean wasClean = false;
+ unsigned short code = 0;
+ USVString reason = "";
+};
diff --git a/test/wpt/tests/interfaces/webtransport.idl b/test/wpt/tests/interfaces/webtransport.idl
new file mode 100644
index 0000000..a9f514e
--- /dev/null
+++ b/test/wpt/tests/interfaces/webtransport.idl
@@ -0,0 +1,145 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: WebTransport (https://w3c.github.io/webtransport/)
+
+[Exposed=(Window,Worker), SecureContext]
+interface WebTransportDatagramDuplexStream {
+ readonly attribute ReadableStream readable;
+ readonly attribute WritableStream writable;
+
+ readonly attribute unsigned long maxDatagramSize;
+ attribute unrestricted double incomingMaxAge;
+ attribute unrestricted double outgoingMaxAge;
+ attribute unrestricted double incomingHighWaterMark;
+ attribute unrestricted double outgoingHighWaterMark;
+};
+
+[Exposed=(Window,Worker), SecureContext]
+interface WebTransport {
+ constructor(USVString url, optional WebTransportOptions options = {});
+
+ Promise<WebTransportStats> getStats();
+ readonly attribute Promise<undefined> ready;
+ readonly attribute WebTransportReliabilityMode reliability;
+ readonly attribute WebTransportCongestionControl congestionControl;
+ readonly attribute Promise<WebTransportCloseInfo> closed;
+ readonly attribute Promise<undefined> draining;
+ undefined close(optional WebTransportCloseInfo closeInfo = {});
+
+ readonly attribute WebTransportDatagramDuplexStream datagrams;
+
+ Promise<WebTransportBidirectionalStream> createBidirectionalStream(
+ optional WebTransportSendStreamOptions options = {});
+ /* a ReadableStream of WebTransportBidirectionalStream objects */
+ readonly attribute ReadableStream incomingBidirectionalStreams;
+
+ Promise<WebTransportSendStream> createUnidirectionalStream(
+ optional WebTransportSendStreamOptions options = {});
+ /* a ReadableStream of WebTransportReceiveStream objects */
+ readonly attribute ReadableStream incomingUnidirectionalStreams;
+};
+
+enum WebTransportReliabilityMode {
+ "pending",
+ "reliable-only",
+ "supports-unreliable",
+};
+
+dictionary WebTransportHash {
+ DOMString algorithm;
+ BufferSource value;
+};
+
+dictionary WebTransportOptions {
+ boolean allowPooling = false;
+ boolean requireUnreliable = false;
+ sequence<WebTransportHash> serverCertificateHashes;
+ WebTransportCongestionControl congestionControl = "default";
+};
+
+enum WebTransportCongestionControl {
+ "default",
+ "throughput",
+ "low-latency",
+};
+
+dictionary WebTransportCloseInfo {
+ unsigned long closeCode = 0;
+ USVString reason = "";
+};
+
+dictionary WebTransportSendStreamOptions {
+ long long? sendOrder = null;
+};
+
+dictionary WebTransportStats {
+ DOMHighResTimeStamp timestamp;
+ unsigned long long bytesSent;
+ unsigned long long packetsSent;
+ unsigned long long packetsLost;
+ unsigned long numOutgoingStreamsCreated;
+ unsigned long numIncomingStreamsCreated;
+ unsigned long long bytesReceived;
+ unsigned long long packetsReceived;
+ DOMHighResTimeStamp smoothedRtt;
+ DOMHighResTimeStamp rttVariation;
+ DOMHighResTimeStamp minRtt;
+ WebTransportDatagramStats datagrams;
+ unsigned long long? estimatedSendRate;
+};
+
+dictionary WebTransportDatagramStats {
+ DOMHighResTimeStamp timestamp;
+ unsigned long long expiredOutgoing;
+ unsigned long long droppedIncoming;
+ unsigned long long lostOutgoing;
+};
+
+[Exposed=(Window,Worker), SecureContext, Transferable]
+interface WebTransportSendStream : WritableStream {
+ attribute long long? sendOrder;
+ Promise<WebTransportSendStreamStats> getStats();
+};
+
+dictionary WebTransportSendStreamStats {
+ DOMHighResTimeStamp timestamp;
+ unsigned long long bytesWritten;
+ unsigned long long bytesSent;
+ unsigned long long bytesAcknowledged;
+};
+
+[Exposed=(Window,Worker), SecureContext, Transferable]
+interface WebTransportReceiveStream : ReadableStream {
+ Promise<WebTransportReceiveStreamStats> getStats();
+};
+
+dictionary WebTransportReceiveStreamStats {
+ DOMHighResTimeStamp timestamp;
+ unsigned long long bytesReceived;
+ unsigned long long bytesRead;
+};
+
+[Exposed=(Window,Worker), SecureContext]
+interface WebTransportBidirectionalStream {
+ readonly attribute WebTransportReceiveStream readable;
+ readonly attribute WebTransportSendStream writable;
+};
+
+[Exposed=(Window,Worker), Serializable, SecureContext]
+interface WebTransportError : DOMException {
+ constructor(optional DOMString message = "", optional WebTransportErrorOptions options = {});
+
+ readonly attribute WebTransportErrorSource source;
+ readonly attribute unsigned long? streamErrorCode;
+};
+
+dictionary WebTransportErrorOptions {
+ WebTransportErrorSource source = "stream";
+ [Clamp] unsigned long? streamErrorCode = null;
+};
+
+enum WebTransportErrorSource {
+ "stream",
+ "session",
+};
diff --git a/test/wpt/tests/interfaces/webusb.idl b/test/wpt/tests/interfaces/webusb.idl
new file mode 100644
index 0000000..fb0a71f
--- /dev/null
+++ b/test/wpt/tests/interfaces/webusb.idl
@@ -0,0 +1,249 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: WebUSB API (https://wicg.github.io/webusb/)
+
+dictionary USBDeviceFilter {
+ unsigned short vendorId;
+ unsigned short productId;
+ octet classCode;
+ octet subclassCode;
+ octet protocolCode;
+ DOMString serialNumber;
+};
+
+dictionary USBDeviceRequestOptions {
+ required sequence<USBDeviceFilter> filters;
+};
+
+[Exposed=(Worker,Window), SecureContext]
+interface USB : EventTarget {
+ attribute EventHandler onconnect;
+ attribute EventHandler ondisconnect;
+ Promise<sequence<USBDevice>> getDevices();
+ [Exposed=Window] Promise<USBDevice> requestDevice(USBDeviceRequestOptions options);
+};
+
+[Exposed=Window, SecureContext]
+partial interface Navigator {
+ [SameObject] readonly attribute USB usb;
+};
+
+[Exposed=Worker, SecureContext]
+partial interface WorkerNavigator {
+ [SameObject] readonly attribute USB usb;
+};
+
+dictionary USBConnectionEventInit : EventInit {
+ required USBDevice device;
+};
+
+[
+ Exposed=(DedicatedWorker,SharedWorker,Window),
+ SecureContext
+]
+interface USBConnectionEvent : Event {
+ constructor(DOMString type, USBConnectionEventInit eventInitDict);
+ [SameObject] readonly attribute USBDevice device;
+};
+
+enum USBTransferStatus {
+ "ok",
+ "stall",
+ "babble"
+};
+
+[
+ Exposed=(DedicatedWorker,SharedWorker,Window),
+ SecureContext
+]
+interface USBInTransferResult {
+ constructor(USBTransferStatus status, optional DataView? data);
+ readonly attribute DataView? data;
+ readonly attribute USBTransferStatus status;
+};
+
+[
+ Exposed=(DedicatedWorker,SharedWorker,Window),
+ SecureContext
+]
+interface USBOutTransferResult {
+ constructor(USBTransferStatus status, optional unsigned long bytesWritten = 0);
+ readonly attribute unsigned long bytesWritten;
+ readonly attribute USBTransferStatus status;
+};
+
+[
+ Exposed=(DedicatedWorker,SharedWorker,Window),
+ SecureContext
+]
+interface USBIsochronousInTransferPacket {
+ constructor(USBTransferStatus status, optional DataView? data);
+ readonly attribute DataView? data;
+ readonly attribute USBTransferStatus status;
+};
+
+[
+ Exposed=(DedicatedWorker,SharedWorker,Window),
+ SecureContext
+]
+interface USBIsochronousInTransferResult {
+ constructor(sequence<USBIsochronousInTransferPacket> packets, optional DataView? data);
+ readonly attribute DataView? data;
+ readonly attribute FrozenArray<USBIsochronousInTransferPacket> packets;
+};
+
+[
+ Exposed=(DedicatedWorker,SharedWorker,Window),
+ SecureContext
+]
+interface USBIsochronousOutTransferPacket {
+ constructor(USBTransferStatus status, optional unsigned long bytesWritten = 0);
+ readonly attribute unsigned long bytesWritten;
+ readonly attribute USBTransferStatus status;
+};
+
+[
+ Exposed=(DedicatedWorker,SharedWorker,Window),
+ SecureContext
+]
+interface USBIsochronousOutTransferResult {
+ constructor(sequence<USBIsochronousOutTransferPacket> packets);
+ readonly attribute FrozenArray<USBIsochronousOutTransferPacket> packets;
+};
+
+[Exposed=(DedicatedWorker,SharedWorker,Window), SecureContext]
+interface USBDevice {
+ readonly attribute octet usbVersionMajor;
+ readonly attribute octet usbVersionMinor;
+ readonly attribute octet usbVersionSubminor;
+ readonly attribute octet deviceClass;
+ readonly attribute octet deviceSubclass;
+ readonly attribute octet deviceProtocol;
+ readonly attribute unsigned short vendorId;
+ readonly attribute unsigned short productId;
+ readonly attribute octet deviceVersionMajor;
+ readonly attribute octet deviceVersionMinor;
+ readonly attribute octet deviceVersionSubminor;
+ readonly attribute DOMString? manufacturerName;
+ readonly attribute DOMString? productName;
+ readonly attribute DOMString? serialNumber;
+ readonly attribute USBConfiguration? configuration;
+ readonly attribute FrozenArray<USBConfiguration> configurations;
+ readonly attribute boolean opened;
+ Promise<undefined> open();
+ Promise<undefined> close();
+ Promise<undefined> forget();
+ Promise<undefined> selectConfiguration(octet configurationValue);
+ Promise<undefined> claimInterface(octet interfaceNumber);
+ Promise<undefined> releaseInterface(octet interfaceNumber);
+ Promise<undefined> selectAlternateInterface(octet interfaceNumber, octet alternateSetting);
+ Promise<USBInTransferResult> controlTransferIn(USBControlTransferParameters setup, unsigned short length);
+ Promise<USBOutTransferResult> controlTransferOut(USBControlTransferParameters setup, optional BufferSource data);
+ Promise<undefined> clearHalt(USBDirection direction, octet endpointNumber);
+ Promise<USBInTransferResult> transferIn(octet endpointNumber, unsigned long length);
+ Promise<USBOutTransferResult> transferOut(octet endpointNumber, BufferSource data);
+ Promise<USBIsochronousInTransferResult> isochronousTransferIn(octet endpointNumber, sequence<unsigned long> packetLengths);
+ Promise<USBIsochronousOutTransferResult> isochronousTransferOut(octet endpointNumber, BufferSource data, sequence<unsigned long> packetLengths);
+ Promise<undefined> reset();
+};
+
+enum USBRequestType {
+ "standard",
+ "class",
+ "vendor"
+};
+
+enum USBRecipient {
+ "device",
+ "interface",
+ "endpoint",
+ "other"
+};
+
+dictionary USBControlTransferParameters {
+ required USBRequestType requestType;
+ required USBRecipient recipient;
+ required octet request;
+ required unsigned short value;
+ required unsigned short index;
+};
+
+[
+ Exposed=(DedicatedWorker,SharedWorker,Window),
+ SecureContext
+]
+interface USBConfiguration {
+ constructor(USBDevice device, octet configurationValue);
+ readonly attribute octet configurationValue;
+ readonly attribute DOMString? configurationName;
+ readonly attribute FrozenArray<USBInterface> interfaces;
+};
+
+[
+ Exposed=(DedicatedWorker,SharedWorker,Window),
+ SecureContext
+]
+interface USBInterface {
+ constructor(USBConfiguration configuration, octet interfaceNumber);
+ readonly attribute octet interfaceNumber;
+ readonly attribute USBAlternateInterface alternate;
+ readonly attribute FrozenArray<USBAlternateInterface> alternates;
+ readonly attribute boolean claimed;
+};
+
+[
+ Exposed=(DedicatedWorker,SharedWorker,Window),
+ SecureContext
+]
+interface USBAlternateInterface {
+ constructor(USBInterface deviceInterface, octet alternateSetting);
+ readonly attribute octet alternateSetting;
+ readonly attribute octet interfaceClass;
+ readonly attribute octet interfaceSubclass;
+ readonly attribute octet interfaceProtocol;
+ readonly attribute DOMString? interfaceName;
+ readonly attribute FrozenArray<USBEndpoint> endpoints;
+};
+
+enum USBDirection {
+ "in",
+ "out"
+};
+
+enum USBEndpointType {
+ "bulk",
+ "interrupt",
+ "isochronous"
+};
+
+[
+ Exposed=(DedicatedWorker,SharedWorker,Window),
+ SecureContext
+]
+interface USBEndpoint {
+ constructor(USBAlternateInterface alternate, octet endpointNumber, USBDirection direction);
+ readonly attribute octet endpointNumber;
+ readonly attribute USBDirection direction;
+ readonly attribute USBEndpointType type;
+ readonly attribute unsigned long packetSize;
+};
+
+dictionary USBPermissionDescriptor : PermissionDescriptor {
+ sequence<USBDeviceFilter> filters;
+};
+
+dictionary AllowedUSBDevice {
+ required octet vendorId;
+ required octet productId;
+ DOMString serialNumber;
+};
+
+dictionary USBPermissionStorage {
+ sequence<AllowedUSBDevice> allowedDevices = [];
+};
+
+[Exposed=(DedicatedWorker,SharedWorker,Window)]
+interface USBPermissionResult : PermissionStatus {
+ attribute FrozenArray<USBDevice> devices;
+};
diff --git a/test/wpt/tests/interfaces/webvr.tentative.idl b/test/wpt/tests/interfaces/webvr.tentative.idl
new file mode 100644
index 0000000..2fc5f4e
--- /dev/null
+++ b/test/wpt/tests/interfaces/webvr.tentative.idl
@@ -0,0 +1,204 @@
+// Archived version of the WebVR spec from
+// https://w3c.github.io/webvr/archive/prerelease/1.1/index.html
+
+[Exposed=Window]
+interface VRDisplay : EventTarget {
+ readonly attribute boolean isPresenting;
+
+ /**
+ * Dictionary of capabilities describing the VRDisplay.
+ */
+ [SameObject] readonly attribute VRDisplayCapabilities capabilities;
+
+ /**
+ * If this VRDisplay supports room-scale experiences, the optional
+ * stage attribute contains details on the room-scale parameters.
+ * The stageParameters attribute can not change between null
+ * and non-null once the VRDisplay is enumerated; however,
+ * the values within VRStageParameters may change after
+ * any call to VRDisplay.submitFrame as the user may re-configure
+ * their environment at any time.
+ */
+ readonly attribute VRStageParameters? stageParameters;
+
+ /**
+ * Return the current VREyeParameters for the given eye.
+ */
+ VREyeParameters getEyeParameters(VREye whichEye);
+
+ /**
+ * An identifier for this distinct VRDisplay. Used as an
+ * association point in the Gamepad API.
+ */
+ readonly attribute unsigned long displayId;
+
+ /**
+ * A display name, a user-readable name identifying it.
+ */
+ readonly attribute DOMString displayName;
+
+ /**
+ * Populates the passed VRFrameData with the information required to render
+ * the current frame.
+ */
+ boolean getFrameData(VRFrameData frameData);
+
+ /**
+ * z-depth defining the near plane of the eye view frustum
+ * enables mapping of values in the render target depth
+ * attachment to scene coordinates. Initially set to 0.01.
+ */
+ attribute double depthNear;
+
+ /**
+ * z-depth defining the far plane of the eye view frustum
+ * enables mapping of values in the render target depth
+ * attachment to scene coordinates. Initially set to 10000.0.
+ */
+ attribute double depthFar;
+
+ /**
+ * The callback passed to `requestAnimationFrame` will be called
+ * any time a new frame should be rendered. When the VRDisplay is
+ * presenting the callback will be called at the native refresh
+ * rate of the HMD. When not presenting this function acts
+ * identically to how window.requestAnimationFrame acts. Content should
+ * make no assumptions of frame rate or vsync behavior as the HMD runs
+ * asynchronously from other displays and at differing refresh rates.
+ */
+ long requestAnimationFrame(FrameRequestCallback callback);
+
+ /**
+ * Passing the value returned by `requestAnimationFrame` to
+ * `cancelAnimationFrame` will unregister the callback.
+ */
+ undefined cancelAnimationFrame(long handle);
+
+ /**
+ * Begin presenting to the VRDisplay. Must be called in response to a user gesture.
+ * Repeat calls while already presenting will update the layers being displayed.
+ * If the number of values in the leftBounds/rightBounds arrays is not 0 or 4 for any of the passed layers the promise is rejected
+ * If the source of any of the layers is not present (null), the promise is rejected.
+ */
+ Promise<undefined> requestPresent(sequence<VRLayerInit> layers);
+
+ /**
+ * Stops presenting to the VRDisplay.
+ */
+ Promise<undefined> exitPresent();
+
+ /**
+ * Get the layers currently being presented.
+ */
+ sequence<VRLayerInit> getLayers();
+
+ /**
+ * The layer provided to the VRDisplay will be captured and presented
+ * in the HMD. Calling this function has the same effect on the source
+ * canvas as any other operation that uses its source image, and canvases
+ * created without preserveDrawingBuffer set to true will be cleared.
+ */
+ undefined submitFrame();
+};
+
+typedef (HTMLCanvasElement or
+ OffscreenCanvas) VRSource;
+
+dictionary VRLayerInit {
+ VRSource? source = null;
+
+ sequence<float> leftBounds = [];
+ sequence<float> rightBounds = [];
+};
+
+[Exposed=Window]
+interface VRDisplayCapabilities {
+ readonly attribute boolean hasPosition;
+ readonly attribute boolean hasExternalDisplay;
+ readonly attribute boolean canPresent;
+ readonly attribute unsigned long maxLayers;
+};
+
+enum VREye {
+ "left",
+ "right"
+};
+
+[Exposed=Window]
+interface VRPose {
+ readonly attribute Float32Array? position;
+ readonly attribute Float32Array? linearVelocity;
+ readonly attribute Float32Array? linearAcceleration;
+
+ readonly attribute Float32Array? orientation;
+ readonly attribute Float32Array? angularVelocity;
+ readonly attribute Float32Array? angularAcceleration;
+};
+
+[Exposed=Window]
+interface VRFrameData {
+ constructor();
+
+ readonly attribute Float32Array leftProjectionMatrix;
+ readonly attribute Float32Array leftViewMatrix;
+
+ readonly attribute Float32Array rightProjectionMatrix;
+ readonly attribute Float32Array rightViewMatrix;
+
+ readonly attribute VRPose pose;
+};
+
+[Exposed=Window]
+interface VREyeParameters {
+ readonly attribute Float32Array offset;
+
+ readonly attribute unsigned long renderWidth;
+ readonly attribute unsigned long renderHeight;
+};
+
+[Exposed=Window]
+interface VRStageParameters {
+ readonly attribute Float32Array sittingToStandingTransform;
+
+ readonly attribute float sizeX;
+ readonly attribute float sizeZ;
+};
+
+partial interface Navigator {
+ Promise<sequence<VRDisplay>> getVRDisplays();
+ readonly attribute FrozenArray<VRDisplay> activeVRDisplays;
+ readonly attribute boolean vrEnabled;
+};
+
+enum VRDisplayEventReason {
+ "mounted",
+ "navigation",
+ "requested",
+ "unmounted"
+};
+
+[Exposed=Window]
+interface VRDisplayEvent : Event {
+ constructor(DOMString type, VRDisplayEventInit eventInitDict);
+ readonly attribute VRDisplay display;
+ readonly attribute VRDisplayEventReason? reason;
+};
+
+dictionary VRDisplayEventInit : EventInit {
+ required VRDisplay display;
+ VRDisplayEventReason reason;
+};
+
+partial interface Window {
+ attribute EventHandler onvrdisplayconnect;
+ attribute EventHandler onvrdisplaydisconnect;
+ attribute EventHandler onvrdisplayactivate;
+ attribute EventHandler onvrdisplaydeactivate;
+ attribute EventHandler onvrdisplayblur;
+ attribute EventHandler onvrdisplayfocus;
+ attribute EventHandler onvrdisplaypresentchange;
+};
+
+partial interface Gamepad {
+ readonly attribute unsigned long displayId;
+};
diff --git a/test/wpt/tests/interfaces/webvtt.idl b/test/wpt/tests/interfaces/webvtt.idl
new file mode 100644
index 0000000..730e893
--- /dev/null
+++ b/test/wpt/tests/interfaces/webvtt.idl
@@ -0,0 +1,40 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: WebVTT: The Web Video Text Tracks Format (https://w3c.github.io/webvtt/)
+
+enum AutoKeyword { "auto" };
+typedef (double or AutoKeyword) LineAndPositionSetting;
+enum DirectionSetting { "" /* horizontal */, "rl", "lr" };
+enum LineAlignSetting { "start", "center", "end" };
+enum PositionAlignSetting { "line-left", "center", "line-right", "auto" };
+enum AlignSetting { "start", "center", "end", "left", "right" };
+[Exposed=Window]
+interface VTTCue : TextTrackCue {
+ constructor(double startTime, unrestricted double endTime, DOMString text);
+ attribute VTTRegion? region;
+ attribute DirectionSetting vertical;
+ attribute boolean snapToLines;
+ attribute LineAndPositionSetting line;
+ attribute LineAlignSetting lineAlign;
+ attribute LineAndPositionSetting position;
+ attribute PositionAlignSetting positionAlign;
+ attribute double size;
+ attribute AlignSetting align;
+ attribute DOMString text;
+ DocumentFragment getCueAsHTML();
+};
+
+enum ScrollSetting { "" /* none */, "up" };
+[Exposed=Window]
+interface VTTRegion {
+ constructor();
+ attribute DOMString id;
+ attribute double width;
+ attribute unsigned long lines;
+ attribute double regionAnchorX;
+ attribute double regionAnchorY;
+ attribute double viewportAnchorX;
+ attribute double viewportAnchorY;
+ attribute ScrollSetting scroll;
+};
diff --git a/test/wpt/tests/interfaces/webxr-ar-module.idl b/test/wpt/tests/interfaces/webxr-ar-module.idl
new file mode 100644
index 0000000..c3677fe
--- /dev/null
+++ b/test/wpt/tests/interfaces/webxr-ar-module.idl
@@ -0,0 +1,29 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: WebXR Augmented Reality Module - Level 1 (https://immersive-web.github.io/webxr-ar-module/)
+
+enum XREnvironmentBlendMode {
+ "opaque",
+ "alpha-blend",
+ "additive"
+};
+
+partial interface XRSession {
+ // Attributes
+ readonly attribute XREnvironmentBlendMode environmentBlendMode;
+};
+
+enum XRInteractionMode {
+ "screen-space",
+ "world-space",
+};
+
+partial interface XRSession {
+ // Attributes
+ readonly attribute XRInteractionMode interactionMode;
+};
+
+partial interface XRView {
+ readonly attribute boolean isFirstPersonObserver;
+};
diff --git a/test/wpt/tests/interfaces/webxr-depth-sensing.idl b/test/wpt/tests/interfaces/webxr-depth-sensing.idl
new file mode 100644
index 0000000..c44f029
--- /dev/null
+++ b/test/wpt/tests/interfaces/webxr-depth-sensing.idl
@@ -0,0 +1,57 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: WebXR Depth Sensing Module (https://immersive-web.github.io/depth-sensing/)
+
+enum XRDepthUsage {
+ "cpu-optimized",
+ "gpu-optimized",
+};
+
+enum XRDepthDataFormat {
+ "luminance-alpha",
+ "float32"
+};
+
+dictionary XRDepthStateInit {
+ required sequence<XRDepthUsage> usagePreference;
+ required sequence<XRDepthDataFormat> dataFormatPreference;
+};
+
+partial dictionary XRSessionInit {
+ XRDepthStateInit depthSensing;
+};
+
+partial interface XRSession {
+ readonly attribute XRDepthUsage depthUsage;
+ readonly attribute XRDepthDataFormat depthDataFormat;
+};
+
+[SecureContext, Exposed=Window]
+interface XRDepthInformation {
+ readonly attribute unsigned long width;
+ readonly attribute unsigned long height;
+
+ [SameObject] readonly attribute XRRigidTransform normDepthBufferFromNormView;
+ readonly attribute float rawValueToMeters;
+};
+
+[Exposed=Window]
+interface XRCPUDepthInformation : XRDepthInformation {
+ [SameObject] readonly attribute ArrayBuffer data;
+
+ float getDepthInMeters(float x, float y);
+};
+
+partial interface XRFrame {
+ XRCPUDepthInformation? getDepthInformation(XRView view);
+};
+
+[Exposed=Window]
+interface XRWebGLDepthInformation : XRDepthInformation {
+ [SameObject] readonly attribute WebGLTexture texture;
+};
+
+partial interface XRWebGLBinding {
+ XRWebGLDepthInformation? getDepthInformation(XRView view);
+};
diff --git a/test/wpt/tests/interfaces/webxr-dom-overlays.idl b/test/wpt/tests/interfaces/webxr-dom-overlays.idl
new file mode 100644
index 0000000..5e358c2
--- /dev/null
+++ b/test/wpt/tests/interfaces/webxr-dom-overlays.idl
@@ -0,0 +1,31 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: WebXR DOM Overlays Module (https://immersive-web.github.io/dom-overlays/)
+
+partial interface mixin GlobalEventHandlers {
+ attribute EventHandler onbeforexrselect;
+};
+
+partial dictionary XRSessionInit {
+ XRDOMOverlayInit? domOverlay;
+};
+
+partial interface XRSession {
+ readonly attribute XRDOMOverlayState? domOverlayState;
+};
+
+dictionary XRDOMOverlayInit {
+ required Element root;
+};
+
+enum XRDOMOverlayType {
+ "screen",
+ "floating",
+ "head-locked"
+};
+
+dictionary XRDOMOverlayState {
+ XRDOMOverlayType type;
+
+};
diff --git a/test/wpt/tests/interfaces/webxr-gamepads-module.idl b/test/wpt/tests/interfaces/webxr-gamepads-module.idl
new file mode 100644
index 0000000..f63921c
--- /dev/null
+++ b/test/wpt/tests/interfaces/webxr-gamepads-module.idl
@@ -0,0 +1,8 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: WebXR Gamepads Module - Level 1 (https://immersive-web.github.io/webxr-gamepads-module/)
+
+partial interface XRInputSource {
+ [SameObject] readonly attribute Gamepad? gamepad;
+};
diff --git a/test/wpt/tests/interfaces/webxr-hand-input.idl b/test/wpt/tests/interfaces/webxr-hand-input.idl
new file mode 100644
index 0000000..9a11277
--- /dev/null
+++ b/test/wpt/tests/interfaces/webxr-hand-input.idl
@@ -0,0 +1,66 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: WebXR Hand Input Module - Level 1 (https://immersive-web.github.io/webxr-hand-input/)
+
+partial interface XRInputSource {
+ [SameObject] readonly attribute XRHand? hand;
+};
+
+enum XRHandJoint {
+ "wrist",
+
+ "thumb-metacarpal",
+ "thumb-phalanx-proximal",
+ "thumb-phalanx-distal",
+ "thumb-tip",
+
+ "index-finger-metacarpal",
+ "index-finger-phalanx-proximal",
+ "index-finger-phalanx-intermediate",
+ "index-finger-phalanx-distal",
+ "index-finger-tip",
+
+ "middle-finger-metacarpal",
+ "middle-finger-phalanx-proximal",
+ "middle-finger-phalanx-intermediate",
+ "middle-finger-phalanx-distal",
+ "middle-finger-tip",
+
+ "ring-finger-metacarpal",
+ "ring-finger-phalanx-proximal",
+ "ring-finger-phalanx-intermediate",
+ "ring-finger-phalanx-distal",
+ "ring-finger-tip",
+
+ "pinky-finger-metacarpal",
+ "pinky-finger-phalanx-proximal",
+ "pinky-finger-phalanx-intermediate",
+ "pinky-finger-phalanx-distal",
+ "pinky-finger-tip"
+};
+
+[Exposed=Window]
+interface XRHand {
+ iterable<XRHandJoint, XRJointSpace>;
+
+ readonly attribute unsigned long size;
+ XRJointSpace get(XRHandJoint key);
+};
+
+[Exposed=Window]
+interface XRJointSpace: XRSpace {
+ readonly attribute XRHandJoint jointName;
+};
+
+partial interface XRFrame {
+ XRJointPose? getJointPose(XRJointSpace joint, XRSpace baseSpace);
+ boolean fillJointRadii(sequence<XRJointSpace> jointSpaces, Float32Array radii);
+
+ boolean fillPoses(sequence<XRSpace> spaces, XRSpace baseSpace, Float32Array transforms);
+};
+
+[Exposed=Window]
+interface XRJointPose: XRPose {
+ readonly attribute float radius;
+};
diff --git a/test/wpt/tests/interfaces/webxr-hit-test.idl b/test/wpt/tests/interfaces/webxr-hit-test.idl
new file mode 100644
index 0000000..fa4fb71
--- /dev/null
+++ b/test/wpt/tests/interfaces/webxr-hit-test.idl
@@ -0,0 +1,69 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: WebXR Hit Test Module (https://immersive-web.github.io/hit-test/)
+
+enum XRHitTestTrackableType {
+ "point",
+ "plane",
+ "mesh"
+};
+
+dictionary XRHitTestOptionsInit {
+ required XRSpace space;
+ FrozenArray<XRHitTestTrackableType> entityTypes;
+ XRRay offsetRay;
+};
+
+dictionary XRTransientInputHitTestOptionsInit {
+ required DOMString profile;
+ FrozenArray<XRHitTestTrackableType> entityTypes;
+ XRRay offsetRay;
+};
+
+[SecureContext, Exposed=Window]
+interface XRHitTestSource {
+ undefined cancel();
+};
+
+[SecureContext, Exposed=Window]
+interface XRTransientInputHitTestSource {
+ undefined cancel();
+};
+
+[SecureContext, Exposed=Window]
+interface XRHitTestResult {
+ XRPose? getPose(XRSpace baseSpace);
+};
+
+[SecureContext, Exposed=Window]
+interface XRTransientInputHitTestResult {
+ [SameObject] readonly attribute XRInputSource inputSource;
+ readonly attribute FrozenArray<XRHitTestResult> results;
+};
+
+partial interface XRSession {
+ Promise<XRHitTestSource> requestHitTestSource(XRHitTestOptionsInit options);
+ Promise<XRTransientInputHitTestSource> requestHitTestSourceForTransientInput(XRTransientInputHitTestOptionsInit options);
+};
+
+partial interface XRFrame {
+ FrozenArray<XRHitTestResult> getHitTestResults(XRHitTestSource hitTestSource);
+ FrozenArray<XRTransientInputHitTestResult> getHitTestResultsForTransientInput(XRTransientInputHitTestSource hitTestSource);
+};
+
+dictionary XRRayDirectionInit {
+ double x = 0;
+ double y = 0;
+ double z = -1;
+ double w = 0;
+};
+
+[SecureContext, Exposed=Window]
+interface XRRay {
+ constructor(optional DOMPointInit origin = {}, optional XRRayDirectionInit direction = {});
+ constructor(XRRigidTransform transform);
+ [SameObject] readonly attribute DOMPointReadOnly origin;
+ [SameObject] readonly attribute DOMPointReadOnly direction;
+ [SameObject] readonly attribute Float32Array matrix;
+};
diff --git a/test/wpt/tests/interfaces/webxr-lighting-estimation.idl b/test/wpt/tests/interfaces/webxr-lighting-estimation.idl
new file mode 100644
index 0000000..35aa1d7
--- /dev/null
+++ b/test/wpt/tests/interfaces/webxr-lighting-estimation.idl
@@ -0,0 +1,39 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: WebXR Lighting Estimation API Level 1 (https://immersive-web.github.io/lighting-estimation/)
+
+[SecureContext, Exposed=Window]
+interface XRLightProbe : EventTarget {
+ readonly attribute XRSpace probeSpace;
+ attribute EventHandler onreflectionchange;
+};
+
+enum XRReflectionFormat {
+ "srgba8",
+ "rgba16f",
+};
+
+[SecureContext, Exposed=Window]
+interface XRLightEstimate {
+ readonly attribute Float32Array sphericalHarmonicsCoefficients;
+ readonly attribute DOMPointReadOnly primaryLightDirection;
+ readonly attribute DOMPointReadOnly primaryLightIntensity;
+};
+
+dictionary XRLightProbeInit {
+ XRReflectionFormat reflectionFormat = "srgba8";
+};
+
+partial interface XRSession {
+ Promise<XRLightProbe> requestLightProbe(optional XRLightProbeInit options = {});
+ readonly attribute XRReflectionFormat preferredReflectionFormat;
+};
+
+partial interface XRFrame {
+ XRLightEstimate? getLightEstimate(XRLightProbe lightProbe);
+};
+
+partial interface XRWebGLBinding {
+ WebGLTexture? getReflectionCubeMap(XRLightProbe lightProbe);
+};
diff --git a/test/wpt/tests/interfaces/webxr.idl b/test/wpt/tests/interfaces/webxr.idl
new file mode 100644
index 0000000..de2b046
--- /dev/null
+++ b/test/wpt/tests/interfaces/webxr.idl
@@ -0,0 +1,295 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: WebXR Device API (https://immersive-web.github.io/webxr/)
+
+partial interface Navigator {
+ [SecureContext, SameObject] readonly attribute XRSystem xr;
+};
+
+[SecureContext, Exposed=Window] interface XRSystem : EventTarget {
+ // Methods
+ Promise<boolean> isSessionSupported(XRSessionMode mode);
+ [NewObject] Promise<XRSession> requestSession(XRSessionMode mode, optional XRSessionInit options = {});
+
+ // Events
+ attribute EventHandler ondevicechange;
+};
+
+enum XRSessionMode {
+ "inline",
+ "immersive-vr",
+ "immersive-ar"
+};
+
+dictionary XRSessionInit {
+ sequence<DOMString> requiredFeatures;
+ sequence<DOMString> optionalFeatures;
+};
+
+enum XRVisibilityState {
+ "visible",
+ "visible-blurred",
+ "hidden",
+};
+
+[SecureContext, Exposed=Window] interface XRSession : EventTarget {
+ // Attributes
+ readonly attribute XRVisibilityState visibilityState;
+ readonly attribute float? frameRate;
+ readonly attribute Float32Array? supportedFrameRates;
+ [SameObject] readonly attribute XRRenderState renderState;
+ [SameObject] readonly attribute XRInputSourceArray inputSources;
+ readonly attribute FrozenArray<DOMString> enabledFeatures;
+ readonly attribute boolean isSystemKeyboardSupported;
+
+ // Methods
+ undefined updateRenderState(optional XRRenderStateInit state = {});
+ Promise<undefined> updateTargetFrameRate(float rate);
+ [NewObject] Promise<XRReferenceSpace> requestReferenceSpace(XRReferenceSpaceType type);
+
+ unsigned long requestAnimationFrame(XRFrameRequestCallback callback);
+ undefined cancelAnimationFrame(unsigned long handle);
+
+ Promise<undefined> end();
+
+ // Events
+ attribute EventHandler onend;
+ attribute EventHandler oninputsourceschange;
+ attribute EventHandler onselect;
+ attribute EventHandler onselectstart;
+ attribute EventHandler onselectend;
+ attribute EventHandler onsqueeze;
+ attribute EventHandler onsqueezestart;
+ attribute EventHandler onsqueezeend;
+ attribute EventHandler onvisibilitychange;
+ attribute EventHandler onframeratechange;
+};
+
+dictionary XRRenderStateInit {
+ double depthNear;
+ double depthFar;
+ double inlineVerticalFieldOfView;
+ XRWebGLLayer? baseLayer;
+ sequence<XRLayer>? layers;
+};
+
+[SecureContext, Exposed=Window] interface XRRenderState {
+ readonly attribute double depthNear;
+ readonly attribute double depthFar;
+ readonly attribute double? inlineVerticalFieldOfView;
+ readonly attribute XRWebGLLayer? baseLayer;
+};
+
+callback XRFrameRequestCallback = undefined (DOMHighResTimeStamp time, XRFrame frame);
+
+[SecureContext, Exposed=Window] interface XRFrame {
+ [SameObject] readonly attribute XRSession session;
+ readonly attribute DOMHighResTimeStamp predictedDisplayTime;
+
+ XRViewerPose? getViewerPose(XRReferenceSpace referenceSpace);
+ XRPose? getPose(XRSpace space, XRSpace baseSpace);
+};
+
+[SecureContext, Exposed=Window] interface XRSpace : EventTarget {
+
+};
+
+enum XRReferenceSpaceType {
+ "viewer",
+ "local",
+ "local-floor",
+ "bounded-floor",
+ "unbounded"
+};
+
+[SecureContext, Exposed=Window]
+interface XRReferenceSpace : XRSpace {
+ [NewObject] XRReferenceSpace getOffsetReferenceSpace(XRRigidTransform originOffset);
+
+ attribute EventHandler onreset;
+};
+
+[SecureContext, Exposed=Window]
+interface XRBoundedReferenceSpace : XRReferenceSpace {
+ readonly attribute FrozenArray<DOMPointReadOnly> boundsGeometry;
+};
+
+enum XREye {
+ "none",
+ "left",
+ "right"
+};
+
+[SecureContext, Exposed=Window] interface XRView {
+ readonly attribute XREye eye;
+ readonly attribute Float32Array projectionMatrix;
+ [SameObject] readonly attribute XRRigidTransform transform;
+ readonly attribute double? recommendedViewportScale;
+
+ undefined requestViewportScale(double? scale);
+};
+
+[SecureContext, Exposed=Window] interface XRViewport {
+ readonly attribute long x;
+ readonly attribute long y;
+ readonly attribute long width;
+ readonly attribute long height;
+};
+
+[SecureContext, Exposed=Window]
+interface XRRigidTransform {
+ constructor(optional DOMPointInit position = {}, optional DOMPointInit orientation = {});
+ [SameObject] readonly attribute DOMPointReadOnly position;
+ [SameObject] readonly attribute DOMPointReadOnly orientation;
+ readonly attribute Float32Array matrix;
+ [SameObject] readonly attribute XRRigidTransform inverse;
+};
+
+[SecureContext, Exposed=Window] interface XRPose {
+ [SameObject] readonly attribute XRRigidTransform transform;
+ [SameObject] readonly attribute DOMPointReadOnly? linearVelocity;
+ [SameObject] readonly attribute DOMPointReadOnly? angularVelocity;
+
+ readonly attribute boolean emulatedPosition;
+};
+
+[SecureContext, Exposed=Window] interface XRViewerPose : XRPose {
+ [SameObject] readonly attribute FrozenArray<XRView> views;
+};
+
+enum XRHandedness {
+ "none",
+ "left",
+ "right"
+};
+
+enum XRTargetRayMode {
+ "gaze",
+ "tracked-pointer",
+ "screen"
+};
+
+[SecureContext, Exposed=Window]
+interface XRInputSource {
+ readonly attribute XRHandedness handedness;
+ readonly attribute XRTargetRayMode targetRayMode;
+ [SameObject] readonly attribute XRSpace targetRaySpace;
+ [SameObject] readonly attribute XRSpace? gripSpace;
+ [SameObject] readonly attribute FrozenArray<DOMString> profiles;
+};
+
+[SecureContext, Exposed=Window]
+interface XRInputSourceArray {
+ iterable<XRInputSource>;
+ readonly attribute unsigned long length;
+ getter XRInputSource(unsigned long index);
+};
+
+[SecureContext, Exposed=Window]
+interface XRLayer : EventTarget {};
+
+typedef (WebGLRenderingContext or
+ WebGL2RenderingContext) XRWebGLRenderingContext;
+
+dictionary XRWebGLLayerInit {
+ boolean antialias = true;
+ boolean depth = true;
+ boolean stencil = false;
+ boolean alpha = true;
+ boolean ignoreDepthValues = false;
+ double framebufferScaleFactor = 1.0;
+};
+
+[SecureContext, Exposed=Window]
+interface XRWebGLLayer: XRLayer {
+ constructor(XRSession session,
+ XRWebGLRenderingContext context,
+ optional XRWebGLLayerInit layerInit = {});
+ // Attributes
+ readonly attribute boolean antialias;
+ readonly attribute boolean ignoreDepthValues;
+ attribute float? fixedFoveation;
+
+ [SameObject] readonly attribute WebGLFramebuffer? framebuffer;
+ readonly attribute unsigned long framebufferWidth;
+ readonly attribute unsigned long framebufferHeight;
+
+ // Methods
+ XRViewport? getViewport(XRView view);
+
+ // Static Methods
+ static double getNativeFramebufferScaleFactor(XRSession session);
+};
+
+partial dictionary WebGLContextAttributes {
+ boolean xrCompatible = false;
+};
+
+partial interface mixin WebGLRenderingContextBase {
+ [NewObject] Promise<undefined> makeXRCompatible();
+};
+
+[SecureContext, Exposed=Window]
+interface XRSessionEvent : Event {
+ constructor(DOMString type, XRSessionEventInit eventInitDict);
+ [SameObject] readonly attribute XRSession session;
+};
+
+dictionary XRSessionEventInit : EventInit {
+ required XRSession session;
+};
+
+[SecureContext, Exposed=Window]
+interface XRInputSourceEvent : Event {
+ constructor(DOMString type, XRInputSourceEventInit eventInitDict);
+ [SameObject] readonly attribute XRFrame frame;
+ [SameObject] readonly attribute XRInputSource inputSource;
+};
+
+dictionary XRInputSourceEventInit : EventInit {
+ required XRFrame frame;
+ required XRInputSource inputSource;
+};
+
+[SecureContext, Exposed=Window]
+interface XRInputSourcesChangeEvent : Event {
+ constructor(DOMString type, XRInputSourcesChangeEventInit eventInitDict);
+ [SameObject] readonly attribute XRSession session;
+ [SameObject] readonly attribute FrozenArray<XRInputSource> added;
+ [SameObject] readonly attribute FrozenArray<XRInputSource> removed;
+};
+
+dictionary XRInputSourcesChangeEventInit : EventInit {
+ required XRSession session;
+ required FrozenArray<XRInputSource> added;
+ required FrozenArray<XRInputSource> removed;
+
+};
+
+[SecureContext, Exposed=Window]
+interface XRReferenceSpaceEvent : Event {
+ constructor(DOMString type, XRReferenceSpaceEventInit eventInitDict);
+ [SameObject] readonly attribute XRReferenceSpace referenceSpace;
+ [SameObject] readonly attribute XRRigidTransform? transform;
+};
+
+dictionary XRReferenceSpaceEventInit : EventInit {
+ required XRReferenceSpace referenceSpace;
+ XRRigidTransform? transform = null;
+};
+
+dictionary XRSessionSupportedPermissionDescriptor: PermissionDescriptor {
+ XRSessionMode mode;
+};
+
+dictionary XRPermissionDescriptor: PermissionDescriptor {
+ XRSessionMode mode;
+ sequence<DOMString> requiredFeatures;
+ sequence<DOMString> optionalFeatures;
+};
+
+[Exposed=Window]
+interface XRPermissionStatus: PermissionStatus {
+ attribute FrozenArray<DOMString> granted;
+};
diff --git a/test/wpt/tests/interfaces/webxrlayers.idl b/test/wpt/tests/interfaces/webxrlayers.idl
new file mode 100644
index 0000000..c8b3a71
--- /dev/null
+++ b/test/wpt/tests/interfaces/webxrlayers.idl
@@ -0,0 +1,221 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: WebXR Layers API Level 1 (https://immersive-web.github.io/layers/)
+
+enum XRLayerLayout {
+ "default",
+ "mono",
+ "stereo",
+ "stereo-left-right",
+ "stereo-top-bottom"
+};
+
+enum XRLayerQuality {
+ "default",
+ "text-optimized",
+ "graphics-optimized"
+};
+
+[Exposed=Window] interface XRCompositionLayer : XRLayer {
+ readonly attribute XRLayerLayout layout;
+
+ attribute boolean blendTextureSourceAlpha;
+ attribute boolean forceMonoPresentation;
+ attribute float opacity;
+ readonly attribute unsigned long mipLevels;
+ attribute XRLayerQuality quality;
+
+ readonly attribute boolean needsRedraw;
+
+ undefined destroy();
+};
+
+[Exposed=Window] interface XRProjectionLayer : XRCompositionLayer {
+ readonly attribute unsigned long textureWidth;
+ readonly attribute unsigned long textureHeight;
+ readonly attribute unsigned long textureArrayLength;
+
+ readonly attribute boolean ignoreDepthValues;
+ attribute float? fixedFoveation;
+ attribute XRRigidTransform? deltaPose;
+};
+
+[Exposed=Window] interface XRQuadLayer : XRCompositionLayer {
+ attribute XRSpace space;
+ attribute XRRigidTransform transform;
+
+ attribute float width;
+ attribute float height;
+
+ // Events
+ attribute EventHandler onredraw;
+};
+
+[Exposed=Window] interface XRCylinderLayer : XRCompositionLayer {
+ attribute XRSpace space;
+ attribute XRRigidTransform transform;
+
+ attribute float radius;
+ attribute float centralAngle;
+ attribute float aspectRatio;
+
+ // Events
+ attribute EventHandler onredraw;
+};
+
+[Exposed=Window] interface XREquirectLayer : XRCompositionLayer {
+ attribute XRSpace space;
+ attribute XRRigidTransform transform;
+
+ attribute float radius;
+ attribute float centralHorizontalAngle;
+ attribute float upperVerticalAngle;
+ attribute float lowerVerticalAngle;
+
+ // Events
+ attribute EventHandler onredraw;
+};
+
+[Exposed=Window] interface XRCubeLayer : XRCompositionLayer {
+ attribute XRSpace space;
+ attribute DOMPointReadOnly orientation;
+
+ // Events
+ attribute EventHandler onredraw;
+};
+
+[Exposed=Window] interface XRSubImage {
+ [SameObject] readonly attribute XRViewport viewport;
+};
+
+[Exposed=Window] interface XRWebGLSubImage : XRSubImage {
+ [SameObject] readonly attribute WebGLTexture colorTexture;
+ [SameObject] readonly attribute WebGLTexture? depthStencilTexture;
+ [SameObject] readonly attribute WebGLTexture? motionVectorTexture;
+
+ readonly attribute unsigned long? imageIndex;
+ readonly attribute unsigned long colorTextureWidth;
+ readonly attribute unsigned long colorTextureHeight;
+ readonly attribute unsigned long? depthStencilTextureWidth;
+ readonly attribute unsigned long? depthStencilTextureHeight;
+ readonly attribute unsigned long? motionVectorTextureWidth;
+ readonly attribute unsigned long? motionVectorTextureHeight;
+};
+
+enum XRTextureType {
+ "texture",
+ "texture-array"
+};
+
+dictionary XRProjectionLayerInit {
+ XRTextureType textureType = "texture";
+ GLenum colorFormat = 0x1908; // RGBA
+ GLenum depthFormat = 0x1902; // DEPTH_COMPONENT
+ double scaleFactor = 1.0;
+ boolean clearOnAccess = true;
+};
+
+dictionary XRLayerInit {
+ required XRSpace space;
+ GLenum colorFormat = 0x1908; // RGBA
+ GLenum? depthFormat;
+ unsigned long mipLevels = 1;
+ required unsigned long viewPixelWidth;
+ required unsigned long viewPixelHeight;
+ XRLayerLayout layout = "mono";
+ boolean isStatic = false;
+ boolean clearOnAccess = true;
+};
+
+dictionary XRQuadLayerInit : XRLayerInit {
+ XRTextureType textureType = "texture";
+ XRRigidTransform? transform;
+ float width = 1.0;
+ float height = 1.0;
+};
+
+dictionary XRCylinderLayerInit : XRLayerInit {
+ XRTextureType textureType = "texture";
+ XRRigidTransform? transform;
+ float radius = 2.0;
+ float centralAngle = 0.78539;
+ float aspectRatio = 2.0;
+};
+
+dictionary XREquirectLayerInit : XRLayerInit {
+ XRTextureType textureType = "texture";
+ XRRigidTransform? transform;
+ float radius = 0;
+ float centralHorizontalAngle = 6.28318;
+ float upperVerticalAngle = 1.570795;
+ float lowerVerticalAngle = -1.570795;
+};
+
+dictionary XRCubeLayerInit : XRLayerInit {
+ DOMPointReadOnly? orientation;
+};
+
+[Exposed=Window] interface XRWebGLBinding {
+ constructor(XRSession session, XRWebGLRenderingContext context);
+
+ readonly attribute double nativeProjectionScaleFactor;
+ readonly attribute boolean usesDepthValues;
+
+ XRProjectionLayer createProjectionLayer(optional XRProjectionLayerInit init = {});
+ XRQuadLayer createQuadLayer(optional XRQuadLayerInit init = {});
+ XRCylinderLayer createCylinderLayer(optional XRCylinderLayerInit init = {});
+ XREquirectLayer createEquirectLayer(optional XREquirectLayerInit init = {});
+ XRCubeLayer createCubeLayer(optional XRCubeLayerInit init = {});
+
+ XRWebGLSubImage getSubImage(XRCompositionLayer layer, XRFrame frame, optional XREye eye = "none");
+ XRWebGLSubImage getViewSubImage(XRProjectionLayer layer, XRView view);
+};
+
+dictionary XRMediaLayerInit {
+ required XRSpace space;
+ XRLayerLayout layout = "mono";
+ boolean invertStereo = false;
+};
+
+dictionary XRMediaQuadLayerInit : XRMediaLayerInit {
+ XRRigidTransform? transform;
+ float? width;
+ float? height;
+};
+
+dictionary XRMediaCylinderLayerInit : XRMediaLayerInit {
+ XRRigidTransform? transform;
+ float radius = 2.0;
+ float centralAngle = 0.78539;
+ float? aspectRatio;
+};
+
+dictionary XRMediaEquirectLayerInit : XRMediaLayerInit {
+ XRRigidTransform? transform;
+ float radius = 0.0;
+ float centralHorizontalAngle = 6.28318;
+ float upperVerticalAngle = 1.570795;
+ float lowerVerticalAngle = -1.570795;
+};
+
+[Exposed=Window] interface XRMediaBinding {
+ constructor(XRSession session);
+
+ XRQuadLayer createQuadLayer(HTMLVideoElement video, optional XRMediaQuadLayerInit init = {});
+ XRCylinderLayer createCylinderLayer(HTMLVideoElement video, optional XRMediaCylinderLayerInit init = {});
+ XREquirectLayer createEquirectLayer(HTMLVideoElement video, optional XRMediaEquirectLayerInit init = {});
+};
+
+[SecureContext, Exposed=Window] interface XRLayerEvent : Event {
+ constructor(DOMString type, XRLayerEventInit eventInitDict);
+ [SameObject] readonly attribute XRLayer layer;
+};
+
+dictionary XRLayerEventInit : EventInit {
+ required XRLayer layer;
+};
+
+[SecureContext, Exposed=Window] partial interface XRRenderState {
+ readonly attribute FrozenArray<XRLayer> layers;
+};
diff --git a/test/wpt/tests/interfaces/window-controls-overlay.idl b/test/wpt/tests/interfaces/window-controls-overlay.idl
new file mode 100644
index 0000000..051978d
--- /dev/null
+++ b/test/wpt/tests/interfaces/window-controls-overlay.idl
@@ -0,0 +1,28 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Window Controls Overlay (https://wicg.github.io/window-controls-overlay/)
+
+[SecureContext, Exposed=(Window)]
+partial interface Navigator {
+ [SameObject] readonly attribute WindowControlsOverlay windowControlsOverlay;
+};
+
+[Exposed=Window]
+interface WindowControlsOverlay : EventTarget {
+ readonly attribute boolean visible;
+ DOMRect getTitlebarAreaRect();
+ attribute EventHandler ongeometrychange;
+};
+
+[Exposed=Window]
+interface WindowControlsOverlayGeometryChangeEvent : Event {
+ constructor(DOMString type, WindowControlsOverlayGeometryChangeEventInit eventInitDict);
+ [SameObject] readonly attribute DOMRect titlebarAreaRect;
+ readonly attribute boolean visible;
+};
+
+dictionary WindowControlsOverlayGeometryChangeEventInit : EventInit {
+ required DOMRect titlebarAreaRect;
+ boolean visible = false;
+};
diff --git a/test/wpt/tests/interfaces/window-management.idl b/test/wpt/tests/interfaces/window-management.idl
new file mode 100644
index 0000000..527c41d
--- /dev/null
+++ b/test/wpt/tests/interfaces/window-management.idl
@@ -0,0 +1,42 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Window Management (https://w3c.github.io/window-management/)
+
+partial interface Screen /* : EventTarget */ {
+ [SecureContext]
+ readonly attribute boolean isExtended;
+
+ [SecureContext]
+ attribute EventHandler onchange;
+};
+
+partial interface Window {
+ [SecureContext]
+ Promise<ScreenDetails> getScreenDetails();
+};
+
+[Exposed=Window, SecureContext]
+interface ScreenDetails : EventTarget {
+ readonly attribute FrozenArray<ScreenDetailed> screens;
+ readonly attribute ScreenDetailed currentScreen;
+
+ attribute EventHandler onscreenschange;
+ attribute EventHandler oncurrentscreenchange;
+};
+
+[Exposed=Window, SecureContext]
+interface ScreenDetailed : Screen {
+ readonly attribute long availLeft;
+ readonly attribute long availTop;
+ readonly attribute long left;
+ readonly attribute long top;
+ readonly attribute boolean isPrimary;
+ readonly attribute boolean isInternal;
+ readonly attribute float devicePixelRatio;
+ readonly attribute DOMString label;
+};
+
+partial dictionary FullscreenOptions {
+ ScreenDetailed screen;
+};
diff --git a/test/wpt/tests/interfaces/xhr.idl b/test/wpt/tests/interfaces/xhr.idl
new file mode 100644
index 0000000..b4c27c8
--- /dev/null
+++ b/test/wpt/tests/interfaces/xhr.idl
@@ -0,0 +1,99 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: XMLHttpRequest Standard (https://xhr.spec.whatwg.org/)
+
+[Exposed=(Window,DedicatedWorker,SharedWorker)]
+interface XMLHttpRequestEventTarget : EventTarget {
+ // event handlers
+ attribute EventHandler onloadstart;
+ attribute EventHandler onprogress;
+ attribute EventHandler onabort;
+ attribute EventHandler onerror;
+ attribute EventHandler onload;
+ attribute EventHandler ontimeout;
+ attribute EventHandler onloadend;
+};
+
+[Exposed=(Window,DedicatedWorker,SharedWorker)]
+interface XMLHttpRequestUpload : XMLHttpRequestEventTarget {
+};
+
+enum XMLHttpRequestResponseType {
+ "",
+ "arraybuffer",
+ "blob",
+ "document",
+ "json",
+ "text"
+};
+
+[Exposed=(Window,DedicatedWorker,SharedWorker)]
+interface XMLHttpRequest : XMLHttpRequestEventTarget {
+ constructor();
+
+ // event handler
+ attribute EventHandler onreadystatechange;
+
+ // states
+ const unsigned short UNSENT = 0;
+ const unsigned short OPENED = 1;
+ const unsigned short HEADERS_RECEIVED = 2;
+ const unsigned short LOADING = 3;
+ const unsigned short DONE = 4;
+ readonly attribute unsigned short readyState;
+
+ // request
+ undefined open(ByteString method, USVString url);
+ undefined open(ByteString method, USVString url, boolean async, optional USVString? username = null, optional USVString? password = null);
+ undefined setRequestHeader(ByteString name, ByteString value);
+ attribute unsigned long timeout;
+ attribute boolean withCredentials;
+ [SameObject] readonly attribute XMLHttpRequestUpload upload;
+ undefined send(optional (Document or XMLHttpRequestBodyInit)? body = null);
+ undefined abort();
+
+ // response
+ readonly attribute USVString responseURL;
+ readonly attribute unsigned short status;
+ readonly attribute ByteString statusText;
+ ByteString? getResponseHeader(ByteString name);
+ ByteString getAllResponseHeaders();
+ undefined overrideMimeType(DOMString mime);
+ attribute XMLHttpRequestResponseType responseType;
+ readonly attribute any response;
+ readonly attribute USVString responseText;
+ [Exposed=Window] readonly attribute Document? responseXML;
+};
+
+typedef (File or USVString) FormDataEntryValue;
+
+[Exposed=(Window,Worker)]
+interface FormData {
+ constructor(optional HTMLFormElement form, optional HTMLElement? submitter = null);
+
+ undefined append(USVString name, USVString value);
+ undefined append(USVString name, Blob blobValue, optional USVString filename);
+ undefined delete(USVString name);
+ FormDataEntryValue? get(USVString name);
+ sequence<FormDataEntryValue> getAll(USVString name);
+ boolean has(USVString name);
+ undefined set(USVString name, USVString value);
+ undefined set(USVString name, Blob blobValue, optional USVString filename);
+ iterable<USVString, FormDataEntryValue>;
+};
+
+[Exposed=(Window,Worker)]
+interface ProgressEvent : Event {
+ constructor(DOMString type, optional ProgressEventInit eventInitDict = {});
+
+ readonly attribute boolean lengthComputable;
+ readonly attribute unsigned long long loaded;
+ readonly attribute unsigned long long total;
+};
+
+dictionary ProgressEventInit : EventInit {
+ boolean lengthComputable = false;
+ unsigned long long loaded = 0;
+ unsigned long long total = 0;
+};
diff --git a/test/wpt/tests/lint.ignore b/test/wpt/tests/lint.ignore
new file mode 100644
index 0000000..489c717
--- /dev/null
+++ b/test/wpt/tests/lint.ignore
@@ -0,0 +1,707 @@
+# File containing allowlist for lint errors
+# Format is:
+# ERROR TYPE:file/name/pattern[:line number]
+# e.g.
+# TRAILING WHITESPACE:example/file.html:128
+# to allow trailing whitespace on example/file.html line 128
+
+## Whitespace rules that we can't enforce yet ##
+
+INDENT TABS: conformance-checkers/*
+INDENT TABS: encoding/legacy*/*
+
+TRAILING WHITESPACE: html/canvas/tools/current-work-canvas.xhtml
+TRAILING WHITESPACE: conformance-checkers/*
+TRAILING WHITESPACE: html/syntax/xmldecl/support/no-version-or-space-or-trailing-question-trailing-body-single-quotes-spaces-and-line-breaks-around-equals.htm
+TRAILING WHITESPACE: html/syntax/xmldecl/support/no-version-or-space-or-trailing-question-trailing-body-single-quotes-spaces-and-line-breaks-around-equals-trail.htm
+
+## File types that should never be checked ##
+
+TRAILING WHITESPACE, INDENT TABS, CR AT EOL: *.adts
+TRAILING WHITESPACE, INDENT TABS, CR AT EOL: *.pdf
+TRAILING WHITESPACE, INDENT TABS, CR AT EOL: *.jpg
+TRAILING WHITESPACE, INDENT TABS, CR AT EOL: *.png
+TRAILING WHITESPACE, INDENT TABS, CR AT EOL: *.gif
+TRAILING WHITESPACE, INDENT TABS, CR AT EOL: *.wav
+TRAILING WHITESPACE, INDENT TABS, CR AT EOL: *.mp3
+TRAILING WHITESPACE, INDENT TABS, CR AT EOL: *.m4a
+TRAILING WHITESPACE, INDENT TABS, CR AT EOL: *.mov
+TRAILING WHITESPACE, INDENT TABS, CR AT EOL: *.oga
+TRAILING WHITESPACE, INDENT TABS, CR AT EOL: *.ogv
+TRAILING WHITESPACE, INDENT TABS, CR AT EOL: *.webm
+TRAILING WHITESPACE, INDENT TABS, CR AT EOL: *.mp4
+TRAILING WHITESPACE, INDENT TABS, CR AT EOL: *.m4v
+TRAILING WHITESPACE, INDENT TABS, CR AT EOL: *.otf
+TRAILING WHITESPACE, INDENT TABS, CR AT EOL: *.ttf
+TRAILING WHITESPACE, INDENT TABS, CR AT EOL: *.TTF
+TRAILING WHITESPACE, INDENT TABS, CR AT EOL: *.ttc
+TRAILING WHITESPACE, INDENT TABS, CR AT EOL: *.woff
+TRAILING WHITESPACE, INDENT TABS, CR AT EOL: *.woff2
+TRAILING WHITESPACE, INDENT TABS, CR AT EOL: *.eot
+TRAILING WHITESPACE, INDENT TABS, CR AT EOL: *.sfd
+TRAILING WHITESPACE, INDENT TABS, CR AT EOL: *.swf
+TRAILING WHITESPACE, INDENT TABS, CR AT EOL: *.ani
+TRAILING WHITESPACE, INDENT TABS, CR AT EOL: *.cur
+TRAILING WHITESPACE, INDENT TABS, CR AT EOL: *.ico
+TRAILING WHITESPACE, INDENT TABS, CR AT EOL: *.wasm
+TRAILING WHITESPACE, INDENT TABS, CR AT EOL: *.bmp
+TRAILING WHITESPACE, INDENT TABS, CR AT EOL: *.sxg
+TRAILING WHITESPACE, INDENT TABS, CR AT EOL: *.wbn
+TRAILING WHITESPACE, INDENT TABS, CR AT EOL: *.avif
+TRAILING WHITESPACE, INDENT TABS, CR AT EOL: *.annexb
+
+## .gitignore
+W3C-TEST.ORG: .gitignore
+
+## Documentation ##
+
+W3C-TEST.ORG: README.md
+W3C-TEST.ORG: */README.md
+W3C-TEST.ORG: docs/*
+WEB-PLATFORM.TEST:*/README.md
+WEB-PLATFORM.TEST:docs/*
+CR AT EOL, INDENT TABS:docs/make.bat
+INDENT TABS:docs/Makefile
+
+## Helper scripts ##
+
+PRINT STATEMENT: */tools/*
+
+## Deliberate copies of Ahem ##
+# The allowed copy
+AHEM COPY: fonts/Ahem.ttf
+
+# None of these are actually Ahem
+AHEM COPY: fonts/ahem-extra/AHEM_*.TTF
+
+## Test exclusions ##
+
+# Intentional use of CRLF
+CR AT EOL: cors/resources/cors-headers.asis
+CR AT EOL: fetch/h1-parsing/resources/blue-with-0x00-in-a-header.asis
+CR AT EOL: html/semantics/forms/the-textarea-element/multiline-placeholder-cr.html
+CR AT EOL: html/semantics/forms/the-textarea-element/multiline-placeholder-crlf.html
+CR AT EOL: html/semantics/forms/the-input-element/multiline-placeholder-cr.html
+CR AT EOL: html/semantics/forms/the-input-element/multiline-placeholder-crlf.html
+CR AT EOL: webvtt/parsing/file-parsing/tests/support/newlines.vtt
+CR AT EOL: css/css-text/ellisize-rtl-text-crash.html
+CR AT EOL: html/syntax/charset/after-head-after-1kb-crlf.html
+CR AT EOL: html/syntax/charset/after-head-in-1kb-crlf.html
+
+# Intentional use of tabs
+INDENT TABS: html/semantics/embedded-content/the-canvas-element/size.attributes.parse.whitespace.html
+INDENT TABS: webvtt/parsing/file-parsing/tests/support/header-tab.vtt
+INDENT TABS: webvtt/parsing/file-parsing/tests/support/whitespace-chars.vtt
+
+## Intentional use of trailing whitespace
+TRAILING WHITESPACE: server-timing/resources/parsing/*
+TRAILING WHITESPACE: webvtt/parsing/file-parsing/support/*.vtt
+TRAILING WHITESPACE: webvtt/parsing/file-parsing/tests/support/*.vtt
+TRAILING WHITESPACE: xhr/resources/headers-some-are-empty.asis
+TRAILING WHITESPACE: content-security-policy/support/manifest.json
+
+# Intentional use of print statements
+PRINT STATEMENT: dom/nodes/Document-createElement-namespace-tests/generate.py
+PRINT STATEMENT: encrypted-media/polyfill/make-polyfill-tests.py
+PRINT STATEMENT: resources/test/conftest.py
+PRINT STATEMENT: webdriver/tests/support/helpers.py
+
+# semi-legitimate use of console.*
+CONSOLE: console/*
+CONSOLE: common/gc.js
+CONSOLE: resources/check-layout-th.js
+CONSOLE: resources/chromium/*
+CONSOLE: resources/testharness.js
+CONSOLE: service-workers/service-worker/resources/navigation-redirect-other-origin.html
+CONSOLE: service-workers/service-worker/navigation-redirect.https.html
+CONSOLE: service-workers/service-worker/resources/clients-get-other-origin.html
+CONSOLE: webrtc/tools/*
+CONSOLE: webaudio/resources/audit.js:41
+
+# use of console in a public library - annotation-model ensures
+# it is not actually used
+CONSOLE: annotation-model/scripts/ajv.min.js
+CONSOLE: annotation-model/scripts/showdown.min.js
+CR AT EOL: annotation-model/scripts/showdown.min.js
+
+# Helper files that aren't valid XML
+PARSE-FAILED: acid/acid3/empty.xml
+PARSE-FAILED: dom/nodes/Document-createElement-namespace-tests/empty.svg
+PARSE-FAILED: dom/nodes/Document-createElement-namespace-tests/empty.xhtml
+PARSE-FAILED: dom/nodes/Document-createElement-namespace-tests/empty.xml
+PARSE-FAILED: dom/nodes/Document-createElement-namespace-tests/minimal_html.svg
+PARSE-FAILED: dom/nodes/Document-createElement-namespace-tests/minimal_html.xhtml
+PARSE-FAILED: dom/nodes/Document-createElement-namespace-tests/minimal_html.xml
+PARSE-FAILED: custom-elements/xhtml-crash.xhtml
+
+# setTimeout usage (should probably mostly be fixed)
+SET TIMEOUT: *-manual.*
+SET TIMEOUT: annotation-model/scripts/ajv.min.js
+SET TIMEOUT: apng/animated-png-timeout.html
+SET TIMEOUT: avif/animated-avif-timeout.html
+SET TIMEOUT: cookies/resources/testharness-helpers.js
+SET TIMEOUT: common/reftest-wait.js
+SET TIMEOUT: compute-pressure/resources/support-iframe.html
+SET TIMEOUT: conformance-checkers/*
+SET TIMEOUT: content-security-policy/*
+SET TIMEOUT: css/compositing/opacity-and-transform-animation-crash.html
+SET TIMEOUT: css/css-contain/contain-body-overflow-002.html
+SET TIMEOUT: css/css-display/display-contents-shadow-dom-1.html
+SET TIMEOUT: css/CSS2/normal-flow/crashtests/block-in-inline-ax-crash.html
+SET TIMEOUT: css/selectors/selector-placeholder-shown-type-change-001.html
+SET TIMEOUT: css/selectors/selector-placeholder-shown-type-change-002.html
+SET TIMEOUT: css/selectors/selector-placeholder-shown-type-change-003.html
+SET TIMEOUT: css/selectors/selector-read-write-type-change-002.html
+SET TIMEOUT: css/selectors/selector-required-type-change-002.html
+SET TIMEOUT: css/selectors/invalidation/dir-pseudo-class-in-has.html
+SET TIMEOUT: css/selectors/invalidation/lang-pseudo-class-in-has-document-element.html
+SET TIMEOUT: css/selectors/invalidation/lang-pseudo-class-in-has-xhtml.xhtml
+SET TIMEOUT: css/selectors/invalidation/lang-pseudo-class-in-has.html
+SET TIMEOUT: encrypted-media/polyfill/chrome-polyfill.js
+SET TIMEOUT: encrypted-media/polyfill/clearkey-polyfill.js
+SET TIMEOUT: encrypted-media/scripts/playback-temporary-events.js
+SET TIMEOUT: fetch/metadata/resources/helper.sub.js
+SET TIMEOUT: fetch/metadata/resources/message-opener.html
+SET TIMEOUT: fenced-frame/resources/history-length-fenced-navigations-replace-do-not-contribute-to-joint-inner.html
+SET TIMEOUT: fenced-frame/resources/utils.js
+SET TIMEOUT: focus/support/iframe-focus-with-different-site-intermediate-frame-outer.sub.html
+SET TIMEOUT: focus/support/iframe-focus-with-different-site-intermediate-frame-middle.sub.html
+SET TIMEOUT: focus/support/iframe-contentwindow-focus-with-different-site-intermediate-frame-outer.sub.html
+SET TIMEOUT: focus/support/iframe-contentwindow-focus-with-different-site-intermediate-frame-middle.sub.html
+SET TIMEOUT: focus/support/iframe-focuses-parent-different-site-inner.html
+SET TIMEOUT: focus/support/iframe-focuses-parent-same-site-inner.html
+SET TIMEOUT: generic-sensor/resources/iframe_sensor_handler.html
+SET TIMEOUT: html/browsers/browsing-the-web/back-forward-cache/resources/inflight-fetch-helper.js
+SET TIMEOUT: html/browsers/browsing-the-web/history-traversal/*
+SET TIMEOUT: html/browsers/browsing-the-web/navigating-across-documents/*
+SET TIMEOUT: html/browsers/browsing-the-web/scroll-to-fragid/*
+SET TIMEOUT: html/browsers/browsing-the-web/unloading-documents/*
+SET TIMEOUT: html/browsers/history/the-history-interface/*
+SET TIMEOUT: html/browsers/history/the-location-interface/*
+SET TIMEOUT: html/browsers/history/the-session-history-of-browsing-contexts/*
+SET TIMEOUT: html/browsers/offline/*
+SET TIMEOUT: html/browsers/the-window-object/*
+SET TIMEOUT: html/cross-origin-opener-policy/resources/fully-loaded.js
+SET TIMEOUT: html/editing/dnd/*
+SET TIMEOUT: html/semantics/embedded-content/media-elements/track/track-element/crashtests/*
+SET TIMEOUT: html/semantics/embedded-content/the-iframe-element/*
+SET TIMEOUT: html/semantics/embedded-content/the-img-element/*
+SET TIMEOUT: html/semantics/embedded-content/the-object-element/object-param-url.html
+SET TIMEOUT: html/semantics/embedded-content/the-object-element/object-param-url-ref.html
+SET TIMEOUT: html/semantics/forms/textfieldselection/select-event.html
+SET TIMEOUT: html/semantics/scripting-1/the-script-element/*
+SET TIMEOUT: html/webappapis/dynamic-markup-insertion/opening-the-input-stream/0*
+SET TIMEOUT: html/webappapis/dynamic-markup-insertion/opening-the-input-stream/resources/history-frame.html
+SET TIMEOUT: html/webappapis/dynamic-markup-insertion/opening-the-input-stream/resources/url-entry-document-timer-frame.html
+SET TIMEOUT: html/webappapis/dynamic-markup-insertion/opening-the-input-stream/tasks.window.js
+SET TIMEOUT: html/webappapis/scripting/event-loops/*
+SET TIMEOUT: html/webappapis/scripting/events/event-handler-processing-algorithm-error/*
+SET TIMEOUT: html/webappapis/scripting/processing-model-2/*
+SET TIMEOUT: IndexedDB/*
+SET TIMEOUT: infrastructure/*
+SET TIMEOUT: intersection-observer/resources/*
+SET TIMEOUT: intersection-observer/target-in-different-window.html
+SET TIMEOUT: js-self-profiling/resources/profiling-script.js
+SET TIMEOUT: measure-memory/*
+SET TIMEOUT: media-source/mediasource-util.js
+SET TIMEOUT: media-source/URL-createObjectURL-revoke.html
+SET TIMEOUT: mixed-content/generic/sanity-checker.js
+SET TIMEOUT: navigation-api/navigation-history-entry/entries-after-bfcache-in-iframe.html
+SET TIMEOUT: navigation-timing/*
+SET TIMEOUT: old-tests/submission/Microsoft/history/history_000.htm
+SET TIMEOUT: paint-timing/resources/subframe-painting.html
+SET TIMEOUT: performance-timeline/resources/navigation-id-detached-frame-page.html
+SET TIMEOUT: portals/resources/portals-adopt-predecessor-portal.html
+SET TIMEOUT: preload/single-download-preload.html
+SET TIMEOUT: preload/resources/slow-exec.js
+SET TIMEOUT: resize-observer/resources/iframe.html
+SET TIMEOUT: resource-timing/resources/nested-contexts.js
+SET TIMEOUT: reporting/resources/first-csp-report.https.sub.html
+SET TIMEOUT: reporting/resources/second-csp-report.https.sub.html
+SET TIMEOUT: scheduler/tentative/yield/yield-inherit-across-promises.any.js
+SET TIMEOUT: scheduler/tentative/yield/yield-priority-timers.any.js
+SET TIMEOUT: secure-contexts/basic-popup-and-iframe-tests.https.js
+SET TIMEOUT: service-workers/cache-storage/cache-abort.https.any.js
+SET TIMEOUT: service-workers/service-worker/activation.https.html
+SET TIMEOUT: service-workers/service-worker/fetch-frame-resource.https.html
+SET TIMEOUT: service-workers/service-worker/fetch-request-redirect.https.html
+SET TIMEOUT: service-workers/service-worker/fetch-waits-for-activate.https.html
+SET TIMEOUT: service-workers/service-worker/postMessage-client-worker.js
+SET TIMEOUT: service-workers/service-worker/update-recovery.https.html
+SET TIMEOUT: service-workers/service-worker/resources/controlled-frame-postMessage.html
+SET TIMEOUT: service-workers/service-worker/resources/controlled-worker-late-postMessage.js
+SET TIMEOUT: service-workers/service-worker/resources/controlled-worker-postMessage.js
+SET TIMEOUT: service-workers/service-worker/resources/extendable-event-async-waituntil.js
+SET TIMEOUT: service-workers/service-worker/resources/fetch-event-async-respond-with-worker.js
+SET TIMEOUT: service-workers/service-worker/resources/fetch-event-test-worker.js
+SET TIMEOUT: service-workers/service-worker/resources/opaque-response-being-preloaded-xhr.html
+SET TIMEOUT: service-workers/service-worker/resources/opaque-response-preloaded-xhr.html
+SET TIMEOUT: service-workers/service-worker/resources/performance-timeline-worker.js
+SET TIMEOUT: service-workers/service-worker/resources/resource-timing-worker.js
+SET TIMEOUT: shadow-dom/Document-prototype-currentScript.html
+SET TIMEOUT: shadow-dom/scroll-to-the-fragment-in-shadow-tree.html
+SET TIMEOUT: shadow-dom/slotchange-event.html
+SET TIMEOUT: trusted-types/block-string-assignment-to-DOMWindowTimers-setTimeout-setInterval.tentative.html
+SET TIMEOUT: trusted-types/DOMWindowTimers-setTimeout-setInterval.tentative.html
+SET TIMEOUT: pending-beacon/resources/pending_beacon-helper.js
+SET TIMEOUT: user-timing/*
+SET TIMEOUT: web-animations/crashtests/reparent-animating-element-002.html
+SET TIMEOUT: web-animations/timing-model/animations/*
+SET TIMEOUT: web-locks/crashtests/after-worker-termination.https.html
+SET TIMEOUT: webaudio/the-audio-api/the-mediaelementaudiosourcenode-interface/mediaElementAudioSourceToScriptProcessorTest.html
+SET TIMEOUT: webauthn/*timeout.https.html
+SET TIMEOUT: webdriver/*
+SET TIMEOUT: webmessaging/*
+SET TIMEOUT: webrtc-encoded-transform/script-metadata-transform-worker.js
+SET TIMEOUT: webrtc-encoded-transform/script-transform-generateKeyFrame.js
+SET TIMEOUT: webrtc-encoded-transform/script-transform-sendKeyFrameRequest.js
+SET TIMEOUT: webstorage/eventTestHarness.js
+SET TIMEOUT: webvtt/*
+SET TIMEOUT: workers/*
+SET TIMEOUT: xhr/resources/init.htm
+SET TIMEOUT: xhr/resources/xmlhttprequest-timeout.js
+SET TIMEOUT: fenced-frame/resolve-to-config-promise.https.html
+SET TIMEOUT: credential-management/support/fedcm-iframe.html
+
+# generate_tests implementation and sample usage
+GENERATE_TESTS: resources/test/tests/functional/generate-callback.html
+GENERATE_TESTS: resources/testharness.js
+
+# generate_tests usage (should be got rid of)
+GENERATE_TESTS: html/canvas/element/drawing-images-to-the-canvas/*
+GENERATE_TESTS: html/canvas/element/manual/drawing-images-to-the-canvas/*
+GENERATE_TESTS: css/css-shapes/shape-outside/values/*
+GENERATE_TESTS: css/css-tables/bounding-box-computation-1.html
+GENERATE_TESTS: css/css-tables/bounding-box-computation-2.html
+GENERATE_TESTS: css/css-tables/bounding-box-computation-3.html
+GENERATE_TESTS: css/css-tables/caption-side-1.html
+GENERATE_TESTS: css/css-tables/fixed-layout-1.html
+GENERATE_TESTS: css/css-tables/fixed-layout-2.html
+GENERATE_TESTS: css/css-tables/height-distribution/computing-row-measure-0.html
+GENERATE_TESTS: css/css-tables/height-distribution/computing-row-measure-1.html
+GENERATE_TESTS: css/css-tables/height-distribution/percentage-sizing-of-table-cell-children.html
+GENERATE_TESTS: css/css-tables/html-to-css-mapping-1.html
+GENERATE_TESTS: css/css-tables/html-to-css-mapping-2.html
+GENERATE_TESTS: css/css-tables/html5-table-formatting-1.html
+GENERATE_TESTS: css/css-tables/html5-table-formatting-2.html
+GENERATE_TESTS: css/css-tables/html5-table-formatting-3.html
+GENERATE_TESTS: css/css-tables/html5-table-formatting-fixed-layout-1.html
+GENERATE_TESTS: css/css-tables/table-model-fixup-2.html
+GENERATE_TESTS: css/css-tables/table-model-fixup.html
+GENERATE_TESTS: css/css-tables/visibility-collapse-col-001.html
+GENERATE_TESTS: css/css-tables/visibility-collapse-row-001.html
+GENERATE_TESTS: css/css-tables/width-distribution/computing-column-measure-0.html
+GENERATE_TESTS: css/css-tables/width-distribution/computing-column-measure-1.html
+GENERATE_TESTS: css/css-tables/width-distribution/computing-table-width-0.html
+GENERATE_TESTS: css/css-tables/width-distribution/computing-table-width-1.html
+GENERATE_TESTS: css/css-tables/width-distribution/distribution-algo-1.html
+GENERATE_TESTS: css/css-tables/width-distribution/distribution-algo-2.html
+GENERATE_TESTS: css/css-tables/width-distribution/distribution-algo-min-content-guess.html
+GENERATE_TESTS: css/css-tables/width-distribution/distribution-algo-min-content-percent-guess.html
+GENERATE_TESTS: css/css-tables/width-distribution/distribution-algo-min-content-specified-guess.1.html
+GENERATE_TESTS: css/css-tables/width-distribution/distribution-algo-min-content-specified-guess.html
+GENERATE_TESTS: dom/nodes/case.js
+GENERATE_TESTS: dom/ranges/Range-cloneRange.html
+GENERATE_TESTS: dom/ranges/Range-collapse.html
+GENERATE_TESTS: dom/ranges/Range-mutations.js
+GENERATE_TESTS: dom/ranges/Range-selectNode.html
+GENERATE_TESTS: dom/ranges/Range-set.html
+GENERATE_TESTS: dom/traversal/TreeWalker.html
+GENERATE_TESTS: domparsing/createContextualFragment.html
+GENERATE_TESTS: domxpath/001.html
+GENERATE_TESTS: domxpath/002.html
+GENERATE_TESTS: mediacapture-image/MediaStreamTrack-applyConstraints-reject.https.html
+GENERATE_TESTS: mediacapture-image/MediaStreamTrack-getCapabilities.https.html
+GENERATE_TESTS: mediacapture-image/MediaStreamTrack-getConstraints.https.html
+GENERATE_TESTS: mediacapture-image/MediaStreamTrack-getSettings.https.html
+GENERATE_TESTS: mediacapture-image/takePhoto-reject.html
+GENERATE_TESTS: html/semantics/scripting-1/the-template-element/template-element/template-as-a-descendant.html
+GENERATE_TESTS: html/syntax/parsing/Document.getElementsByTagName-foreign-01.html
+GENERATE_TESTS: html/syntax/parsing/template/clearing-the-stack-back-to-a-given-context/clearing-stack-back-to-a-table-body-context.html
+GENERATE_TESTS: html/syntax/parsing/template/clearing-the-stack-back-to-a-given-context/clearing-stack-back-to-a-table-context.html
+GENERATE_TESTS: html/syntax/parsing/template/clearing-the-stack-back-to-a-given-context/clearing-stack-back-to-a-table-row-context.html
+GENERATE_TESTS: html/syntax/parsing/template/creating-an-element-for-the-token/template-owner-document.html
+GENERATE_TESTS: html/syntax/serializing-html-fragments/serializing.html
+GENERATE_TESTS: html/webappapis/atob/base64.any.js
+GENERATE_TESTS: mediacapture-fromelement/capture.html
+GENERATE_TESTS: mediacapture-fromelement/creation.html
+GENERATE_TESTS: mediacapture-fromelement/ended.html
+GENERATE_TESTS: html/canvas/offscreen/manual/filter/offscreencanvas.filter.html
+GENERATE_TESTS: pointerevents/pointerevent_constructor.html
+GENERATE_TESTS: pointerevents/extension/pointerevent_constructor.html
+GENERATE_TESTS: selection/collapse.js
+GENERATE_TESTS: shadow-dom/leaktests/html-collection.html
+GENERATE_TESTS: shadow-dom/untriaged/shadow-trees/upper-boundary-encapsulation/dom-tree-accessors-001.html
+GENERATE_TESTS: shadow-dom/untriaged/shadow-trees/upper-boundary-encapsulation/ownerdocument-002.html
+GENERATE_TESTS: shadow-dom/untriaged/shadow-trees/upper-boundary-encapsulation/window-named-properties-002.html
+GENERATE_TESTS: shadow-dom/untriaged/shadow-trees/upper-boundary-encapsulation/window-named-properties-003.html
+
+# Intentional use of setTimeout
+SET TIMEOUT: common/security-features/resources/common.sub.js
+SET TIMEOUT: common/dispatcher/dispatcher.js
+SET TIMEOUT: css/css-fonts/font-display/font-display.html
+SET TIMEOUT: css/css-fonts/font-display/font-display-change.html
+SET TIMEOUT: css/css-fonts/font-display/font-display-change-ref.html
+SET TIMEOUT: css/css-fonts/font-display/font-display-feature-policy-01.tentative.html
+SET TIMEOUT: css/css-fonts/font-display/font-display-feature-policy-02.tentative.html
+SET TIMEOUT: css/css-fonts/font-display/font-display-preload.html
+SET TIMEOUT: document-policy/font-display/override-to-optional.tentative.html
+SET TIMEOUT: feature-policy/experimental-features/resources/focus-without-user-activation-iframe-tentative.html
+SET TIMEOUT: permissions-policy/experimental-features/resources/focus-without-user-activation-iframe-tentative.html
+SET TIMEOUT: html/browsers/windows/auxiliary-browsing-contexts/resources/close-opener.html
+SET TIMEOUT: html/cross-origin-embedder-policy/resources/reporting-worker.js
+SET TIMEOUT: html/cross-origin-opener-policy/navigate-to-aboutblank.https.html
+SET TIMEOUT: html/cross-origin-opener-policy/navigate-top-to-aboutblank.https.html
+SET TIMEOUT: html/dom/documents/dom-tree-accessors/Document.currentScript.html
+SET TIMEOUT: html/webappapis/dynamic-markup-insertion/opening-the-input-stream/crbug-583445-regression.window.js
+SET TIMEOUT: html/webappapis/timers/*
+SET TIMEOUT: orientation-event/resources/orientation-event-helpers.js
+SET TIMEOUT: portals/history/resources/portal-harness.js
+SET TIMEOUT: requestidlecallback/deadline-after-expired-timer.html
+SET TIMEOUT: resources/*
+SET TIMEOUT: scheduler/tentative/current-task-signal-async-abort.any.js
+SET TIMEOUT: scheduler/tentative/current-task-signal-async-priority.any.js
+SET TIMEOUT: speculation-rules/prerender/resources/activation-start.html
+SET TIMEOUT: speculation-rules/prerender/resources/prerender-response-code.html
+SET TIMEOUT: speculation-rules/prerender/resources/deferred-promise-utils.js
+SET TIMEOUT: speculation-rules/prerender/resources/session-history-harness.js
+SET TIMEOUT: speculation-rules/prerender/resources/utils.js
+SET TIMEOUT: speculation-rules/prerender/resources/request-picture-in-picture.html
+SET TIMEOUT: speculation-rules/prerender/resources/media-autoplay-attribute.html
+SET TIMEOUT: speculation-rules/prerender/resources/media-play.html
+SET TIMEOUT: html/browsers/browsing-the-web/back-forward-cache/timers.html
+SET TIMEOUT: dom/abort/crashtests/timeout-close.html
+
+# setTimeout use in reftests
+SET TIMEOUT: acid/acid3/test.html
+
+# Third party code
+*: resources/webidl2/*
+*: tools/*
+*: */third_party/*
+
+# .gitignore files in child directories
+*: cors/resources/.gitignore
+*: css/.gitignore
+*: css/css-writing-modes/tools/generators/.gitignore
+*: resources/.gitignore
+*: webaudio/.gitignore
+
+## Third party data files
+TRAILING WHITESPACE: resources/chromium/*
+
+## Test plans and implementation reports
+*: css/*/test-plan/*
+
+## Things we don't have enabled yet
+OPEN-NO-MODE: css/*
+PRINT STATEMENT: css/*
+CONTENT-VISUAL: css/*
+CONTENT-MANUAL: css/*
+
+## Whitespace rules that we can't enforce yet
+INDENT TABS: css/compositing/*
+INDENT TABS: css/CSS2/*
+INDENT TABS: css/css-backgrounds/*
+INDENT TABS: css/css-color/*
+INDENT TABS: css/css-conditional/*
+INDENT TABS: css/css-flexbox/*
+INDENT TABS: css/css-fonts/*
+INDENT TABS: css/css-images/support/1x1-green.svg
+INDENT TABS: css/css-masking/*
+INDENT TABS: css/css-multicol/*
+INDENT TABS: css/css-page/*
+INDENT TABS: css/css-round-display/*
+INDENT TABS: css/css-text/*
+INDENT TABS: css/css-text-decor/*
+INDENT TABS: css/css-transforms/*
+INDENT TABS: css/css-ui/*
+INDENT TABS: css/css-values/*
+INDENT TABS: css/css-writing-modes/*
+INDENT TABS: css/filter-effects/*
+INDENT TABS: css/mediaqueries/*
+INDENT TABS: css/selectors/*
+INDENT TABS: css/WOFF2/*
+
+
+## Things we're stopping from getting worse
+CONSOLE: css/css-shapes/shape-outside/supported-shapes/support/test-utils.js
+CONSOLE: css/css-values/viewport-units-css2-001.html
+CONSOLE: css/css-writing-modes/orthogonal-parent-shrink-to-fit-001*.html
+CONSOLE: css/css-writing-modes/tools/generators/template.html
+CONSOLE: bluetooth/characteristic/readValue/characteristic-is-removed.https.window.js
+CONSOLE: bluetooth/resources/bluetooth-test.js
+
+TRAILING WHITESPACE: css/css-fonts/support/fonts/gsubtest-lookup3.ufo/features.fea
+
+SET TIMEOUT: css/compositing/mix-blend-mode/mix-blend-mode-parent-with-3D-transform-and-transition.html
+SET TIMEOUT: css/compositing/mix-blend-mode/mix-blend-mode-sibling-with-3D-transform-and-transition.html
+SET TIMEOUT: css/css-backgrounds/color-mix-currentcolor-background-repaint-parent.html
+SET TIMEOUT: css/css-backgrounds/color-mix-currentcolor-background-repaint.html
+SET TIMEOUT: css/css-backgrounds/color-mix-currentcolor-border-repaint-parent.html
+SET TIMEOUT: css/css-backgrounds/color-mix-currentcolor-border-repaint.html
+SET TIMEOUT: css/css-backgrounds/color-mix-currentcolor-outline-repaint-parent.html
+SET TIMEOUT: css/css-backgrounds/color-mix-currentcolor-outline-repaint.html
+SET TIMEOUT: css/css-backgrounds/currentcolor-border-repaint-parent.html
+SET TIMEOUT: css/css-transitions/events-007.html
+SET TIMEOUT: css/css-transitions/support/generalParallelTest.js
+SET TIMEOUT: css/css-transitions/support/runParallelAsyncHarness.js
+SET TIMEOUT: css/css-transitions/transitioncancel-001.html
+SET TIMEOUT: css/css-values/reference/vh_not_refreshing_on_chrome-ref.html
+SET TIMEOUT: css/css-values/reference/vh_not_refreshing_on_chrome_iframe-ref.html
+SET TIMEOUT: css/css-values/vh_not_refreshing_on_chrome.html
+SET TIMEOUT: css/css-values/support/vh_not_refreshing_on_chrome_iframe.html
+SET TIMEOUT: css/css-writing-modes/orthogonal-parent-shrink-to-fit-001.html
+SET TIMEOUT: css/css-writing-modes/orthogonal-parent-shrink-to-fit-001a.html
+SET TIMEOUT: css/css-writing-modes/orthogonal-parent-shrink-to-fit-001b.html
+SET TIMEOUT: css/css-writing-modes/orthogonal-parent-shrink-to-fit-001c.html
+SET TIMEOUT: css/css-writing-modes/orthogonal-parent-shrink-to-fit-001d.html
+SET TIMEOUT: css/css-writing-modes/orthogonal-parent-shrink-to-fit-001e.html
+SET TIMEOUT: css/css-writing-modes/orthogonal-parent-shrink-to-fit-001f.html
+SET TIMEOUT: css/css-writing-modes/orthogonal-parent-shrink-to-fit-001g.html
+SET TIMEOUT: css/css-writing-modes/orthogonal-parent-shrink-to-fit-001h.html
+SET TIMEOUT: css/css-writing-modes/orthogonal-parent-shrink-to-fit-001i.html
+SET TIMEOUT: css/css-writing-modes/orthogonal-parent-shrink-to-fit-001j.html
+SET TIMEOUT: css/css-writing-modes/orthogonal-parent-shrink-to-fit-001k.html
+SET TIMEOUT: css/css-writing-modes/orthogonal-parent-shrink-to-fit-001l.html
+SET TIMEOUT: css/css-writing-modes/orthogonal-parent-shrink-to-fit-001m.html
+SET TIMEOUT: css/css-writing-modes/orthogonal-parent-shrink-to-fit-001n.html
+SET TIMEOUT: css/css-writing-modes/orthogonal-parent-shrink-to-fit-001o.html
+SET TIMEOUT: css/css-writing-modes/orthogonal-parent-shrink-to-fit-001p.html
+SET TIMEOUT: css/css-writing-modes/orthogonal-parent-shrink-to-fit-001q.html
+SET TIMEOUT: css/css-writing-modes/orthogonal-parent-shrink-to-fit-001r.html
+SET TIMEOUT: css/css-writing-modes/orthogonal-parent-shrink-to-fit-001s.html
+SET TIMEOUT: css/css-writing-modes/orthogonal-parent-shrink-to-fit-001t.html
+SET TIMEOUT: css/css-writing-modes/orthogonal-parent-shrink-to-fit-001u.html
+SET TIMEOUT: css/css-writing-modes/orthogonal-parent-shrink-to-fit-001v.html
+SET TIMEOUT: css/css-writing-modes/orthogonal-parent-shrink-to-fit-001w.html
+SET TIMEOUT: css/css-writing-modes/orthogonal-parent-shrink-to-fit-001x.html
+SET TIMEOUT: css/css-writing-modes/support/text-orientation.js
+SET TIMEOUT: css/css-writing-modes/tools/generators/template.html
+SET TIMEOUT: css/CSS2/backgrounds/background-root-101.xht
+SET TIMEOUT: css/CSS2/backgrounds/background-root-102.xht
+SET TIMEOUT: css/CSS2/backgrounds/background-root-103.xht
+SET TIMEOUT: css/CSS2/floats-clear/floats-137.xht
+SET TIMEOUT: css/CSS2/generated-content/counter-increment-000.xht
+SET TIMEOUT: css/CSS2/generated-content/counter-increment-001.xht
+SET TIMEOUT: css/CSS2/generated-content/counter-increment-002.xht
+SET TIMEOUT: css/CSS2/generated-content/counter-reset-000.xht
+SET TIMEOUT: css/CSS2/generated-content/counter-reset-001.xht
+SET TIMEOUT: css/CSS2/generated-content/counter-reset-002.xht
+SET TIMEOUT: css/CSS2/selectors/dom-hover-001.xht
+SET TIMEOUT: css/CSS2/selectors/dom-hover-002.xht
+SET TIMEOUT: css/CSS2/tables/tables-102.xht
+SET TIMEOUT: css/mediaqueries/min-width-tables-001.html
+SET TIMEOUT: css/css-text/crashtests/rendering-rtl-bidi-override-crash.html
+SET TIMEOUT: css/css-backgrounds/color-mix-currentcolor-border-repaint-parent.html
+SET TIMEOUT: svg/painting/color-mix-currentcolor-fill-stroke-repaint.html
+SET TIMEOUT: svg/painting/currentcolor-fill-stroke-repaint.html
+SET TIMEOUT: resource-timing/resources/run-async-tasks-promise.js
+
+# CSS tests that used to be at the top level and weren't subject to lints
+MISSING-LINK: css/css-fonts/matching/fixed-stretch-style-over-weight.html
+MISSING-LINK: css/css-fonts/matching/stretch-distance-over-weight-distance.html
+MISSING-LINK: css/css-fonts/matching/style-ranges-over-weight-direction.html
+MISSING-LINK: css/css-fonts/variations/font-parse-numeric-stretch-style-weight.html
+MISSING-LINK: css/css-fonts/variations/variable-box-font.html
+MISSING-LINK: css/css-fonts/variations/variable-gpos-m2b.html
+MISSING-LINK: css/css-fonts/variations/variable-gsub.html
+MISSING-LINK: css/css-scroll-anchoring/abspos-containing-block-outside-scroller.html
+MISSING-LINK: css/css-scroll-anchoring/abspos-contributes-to-static-parent-bounds.html
+MISSING-LINK: css/css-scroll-anchoring/ancestor-change-heuristic.html
+MISSING-LINK: css/css-scroll-anchoring/anchor-updates-after-explicit-scroll.html
+MISSING-LINK: css/css-scroll-anchoring/anchoring-with-bounds-clamping-div.html
+MISSING-LINK: css/css-scroll-anchoring/anchoring-with-bounds-clamping.html
+MISSING-LINK: css/css-scroll-anchoring/anonymous-block-box.html
+MISSING-LINK: css/css-scroll-anchoring/basic.html
+MISSING-LINK: css/css-scroll-anchoring/clipped-scrollers-skipped.html
+MISSING-LINK: css/css-scroll-anchoring/descend-into-container-with-float.html
+MISSING-LINK: css/css-scroll-anchoring/descend-into-container-with-overflow.html
+MISSING-LINK: css/css-scroll-anchoring/exclude-fixed-position.html
+MISSING-LINK: css/css-scroll-anchoring/inline-block.html
+MISSING-LINK: css/css-scroll-anchoring/negative-layout-overflow.html
+MISSING-LINK: css/css-scroll-anchoring/opt-out.html
+MISSING-LINK: css/css-scroll-anchoring/position-change-heuristic.html
+MISSING-LINK: css/css-scroll-anchoring/start-edge-in-block-layout-direction.html
+MISSING-LINK: css/css-scroll-anchoring/subtree-exclusion.html
+MISSING-LINK: css/css-scroll-anchoring/wrapped-text.html
+MISSING-LINK: css/css-typed-om/CSSMatrixComponent-DOMMatrix-mutable.html
+MISSING-LINK: css/css-typed-om/declared-styleMap-accepts-inherit.html
+MISSING-LINK: css/cssom-view/DOMRectList.html
+MISSING-LINK: css/cssom-view/elementFromPoint-002.html
+MISSING-LINK: css/cssom-view/elementFromPoint-003.html
+MISSING-LINK: css/cssom-view/elementFromPoint.html
+MISSING-LINK: css/cssom-view/elementScroll.html
+MISSING-LINK: css/cssom-view/elementsFromPoint-iframes.html
+MISSING-LINK: css/cssom-view/elementsFromPoint-invalid-cases.html
+MISSING-LINK: css/cssom-view/elementsFromPoint-shadowroot.html
+MISSING-LINK: css/cssom-view/elementsFromPoint-simple.html
+MISSING-LINK: css/cssom-view/elementsFromPoint-svg.html
+MISSING-LINK: css/cssom-view/elementsFromPoint-table.html
+MISSING-LINK: css/cssom-view/elementsFromPoint.html
+MISSING-LINK: css/cssom-view/historical.html
+MISSING-LINK: css/cssom-view/HTMLBody-ScrollArea_quirksmode.html
+MISSING-LINK: css/cssom-view/mouseEvent.html
+MISSING-LINK: css/cssom-view/negativeMargins.html
+MISSING-LINK: css/cssom-view/offsetTopLeftInScrollableParent.html
+MISSING-LINK: css/cssom-view/scrolling-no-browsing-context.html
+MISSING-LINK: css/cssom-view/scrolling-quirks-vs-nonquirks.html
+MISSING-LINK: css/cssom-view/scrollingElement.html
+MISSING-LINK: css/cssom-view/scrollIntoView-shadow.html
+MISSING-LINK: css/cssom-view/scrollIntoView-smooth.html
+MISSING-LINK: css/cssom-view/scrollTop-display-change.html
+
+# TODO https://github.com/web-platform-tests/wpt/issues/5770
+MISSING-LINK: css/css-highlight-api/idlharness.window.js
+MISSING-LINK: css/geometry/*.worker.js
+MISSING-LINK: css/geometry/*.any.js
+MISSING-LINK: css/filter-effects/*.any.js
+
+# Tests that use WebKit/Blink testing APIs
+LAYOUTTESTS APIS: import-maps/data-driven/resources/test-helper-iframe.js
+LAYOUTTESTS APIS: resources/chromium/enable-hyperlink-auditing.js
+LAYOUTTESTS APIS: resources/chromium/generic_sensor_mocks.js
+LAYOUTTESTS APIS: resources/chromium/webxr-test.js
+LAYOUTTESTS APIS: webxr/resources/webxr_util.js
+
+# Signed Exchange files have hard-coded URLs in the certUrl field
+WEB-PLATFORM.TEST:signed-exchange/resources/*.sxg
+WEB-PLATFORM.TEST:signed-exchange/resources/generate-test-sxgs.sh
+
+# Web Bundle files have hard-coded URLs
+WEB-PLATFORM.TEST:web-bundle/resources/*.har
+WEB-PLATFORM.TEST:web-bundle/resources/generate-test-wbns.sh
+WEB-PLATFORM.TEST:web-bundle/resources/nested/*.wbn
+WEB-PLATFORM.TEST:web-bundle/resources/wbn/*.wbn
+WEB-PLATFORM.TEST:web-bundle/subresource-loading/*.html
+WEB-PLATFORM.TEST:web-bundle/subresource-loading/resources/*.js
+
+# Tests that depend on resources in /gen/ in Chromium:
+# https://github.com/web-platform-tests/wpt/issues/16455
+# Please consult with ecosystem-infra@chromium.org before adding more.
+MISSING DEPENDENCY: credential-management/support/otpcredential-helper.js
+MISSING DEPENDENCY: credential-management/support/fedcm-mock.js
+MISSING DEPENDENCY: resources/chromium/content-index-helpers.js
+MISSING DEPENDENCY: resources/chromium/contacts_manager_mock.js
+MISSING DEPENDENCY: resources/chromium/web-bluetooth-test.js
+MISSING DEPENDENCY: resources/chromium/webusb-test.js
+MISSING DEPENDENCY: resources/chromium/fake-serial.js
+MISSING DEPENDENCY: resources/chromium/fake-hid.js
+MISSING DEPENDENCY: resources/chromium/generic_sensor_mocks.js
+MISSING DEPENDENCY: resources/chromium/mock-battery-monitor.js
+MISSING DEPENDENCY: resources/chromium/mock-barcodedetection.js
+MISSING DEPENDENCY: resources/chromium/mock-direct-sockets.js
+MISSING DEPENDENCY: resources/chromium/mock-facedetection.js
+MISSING DEPENDENCY: resources/chromium/mock-idle-detection.js
+MISSING DEPENDENCY: resources/chromium/mock-imagecapture.js
+MISSING DEPENDENCY: resources/chromium/mock-managed-config.js
+MISSING DEPENDENCY: resources/chromium/mock-pressure-service.js
+MISSING DEPENDENCY: resources/chromium/mock-subapps.js
+MISSING DEPENDENCY: resources/chromium/mock-textdetection.js
+MISSING DEPENDENCY: resources/chromium/nfc-mock.js
+MISSING DEPENDENCY: resources/chromium/webxr-test.js
+
+# Tests that are false positives for using Ahem as a system font
+AHEM SYSTEM FONT: acid/acid3/test.html
+AHEM SYSTEM FONT: resource-timing/font-timestamps.html
+AHEM SYSTEM FONT: resource-timing/initiator-type/style.html
+AHEM SYSTEM FONT: resource-timing/resources/iframe-reload-TAO.sub.html
+AHEM SYSTEM FONT: html/canvas/element/text/2d.text.measure.fontBoundingBox.ahem.html
+AHEM SYSTEM FONT: css/css-font-loading/fontface-override-descriptors.html
+AHEM SYSTEM FONT: css/css-font-loading/fontface-size-adjust-descriptor.html
+AHEM SYSTEM FONT: css/css-font-loading/fontface-size-adjust-descriptor-ref.html
+AHEM SYSTEM FONT: css/css-fonts/ascent-descent-override.html
+AHEM SYSTEM FONT: css/css-fonts/font-size-adjust-012.html
+AHEM SYSTEM FONT: css/css-fonts/font-size-adjust-012-ref.html
+AHEM SYSTEM FONT: css/css-fonts/font-size-adjust-013.html
+AHEM SYSTEM FONT: css/css-fonts/font-size-adjust-013-ref.html
+AHEM SYSTEM FONT: css/css-fonts/font-size-adjust-014.html
+AHEM SYSTEM FONT: css/css-fonts/font-size-adjust-014-ref.html
+AHEM SYSTEM FONT: css/css-fonts/font-size-adjust-metrics-override.html
+AHEM SYSTEM FONT: css/css-fonts/font-size-adjust-metrics-override-ref.html
+AHEM SYSTEM FONT: css/css-fonts/line-gap-override.html
+AHEM SYSTEM FONT: css/css-fonts/parsing/font-size-adjust-computed.html
+AHEM SYSTEM FONT: html/dom/render-blocking/remove-attr-unblocks-rendering.optional.html
+AHEM SYSTEM FONT: html/dom/render-blocking/remove-element-unblocks-rendering.optional.html
+
+# TODO: The following should be deleted along with the Ahem web font cleanup
+# PR (https://github.com/web-platform-tests/wpt/pull/18702)
+AHEM SYSTEM FONT: infrastructure/assumptions/ahem-ref.html
+AHEM SYSTEM FONT: infrastructure/assumptions/ahem.html
+
+# Existing crashtests using testharness
+TESTHARNESS-IN-OTHER-TYPE: accessibility/crashtests/computed-node-checked.html
+TESTHARNESS-IN-OTHER-TYPE: css/CSS2/floats-clear/adjoining-float-new-fc-crash.html
+TESTHARNESS-IN-OTHER-TYPE: css/CSS2/floats/floats-saturated-position-crash.html
+TESTHARNESS-IN-OTHER-TYPE: css/CSS2/linebox/video-needs-layout-crash.html
+TESTHARNESS-IN-OTHER-TYPE: css/css-break/break-before-with-no-fragmentation-crash.html
+TESTHARNESS-IN-OTHER-TYPE: css/css-multicol/abspos-in-multicol-with-spanner-crash.html
+TESTHARNESS-IN-OTHER-TYPE: css/css-multicol/extremely-tall-multicol-with-extremely-tall-child-crash.html
+TESTHARNESS-IN-OTHER-TYPE: css/css-multicol/with-custom-layout-on-same-element-crash.https.html
+TESTHARNESS-IN-OTHER-TYPE: css/css-overflow/outline-with-opacity-crash.html
+TESTHARNESS-IN-OTHER-TYPE: css/css-overflow/shrink-to-fit-auto-overflow-relayout-crash.html
+TESTHARNESS-IN-OTHER-TYPE: css/css-position/position-absolute-in-inline-crash.html
+TESTHARNESS-IN-OTHER-TYPE: css/css-pseudo/first-letter-crash.html
+TESTHARNESS-IN-OTHER-TYPE: css/css-pseudo/first-line-first-letter-insert-crash.html
+TESTHARNESS-IN-OTHER-TYPE: css/css-shapes/shape-outside/supported-shapes/polygon/shape-outside-polygon-crash.html
+TESTHARNESS-IN-OTHER-TYPE: css/css-tables/visibility-collapse-colspan-crash.html
+TESTHARNESS-IN-OTHER-TYPE: css/css-tables/visibility-collapse-rowspan-crash.html
+TESTHARNESS-IN-OTHER-TYPE: css/css-text/overflow-wrap/overflow-wrap-break-word-long-crash.html
+TESTHARNESS-IN-OTHER-TYPE: css/css-text/overflow-wrap/overflow-wrap-break-word-white-space-crash.html
+TESTHARNESS-IN-OTHER-TYPE: css/css-text/text-indent/text-indent-long-line-crash.html
+TESTHARNESS-IN-OTHER-TYPE: css/css-text/white-space/nowrap-wbr-and-space-crash.html
+TESTHARNESS-IN-OTHER-TYPE: css/css-text/white-space/pre-line-br-with-whitespace-child-crash.html
+TESTHARNESS-IN-OTHER-TYPE: css/css-text/white-space/pre-with-whitespace-crash.html
+TESTHARNESS-IN-OTHER-TYPE: css/css-writing-modes/bidi-inline-fragment-crash.html
+TESTHARNESS-IN-OTHER-TYPE: dom/svg-insert-crash.html
+TESTHARNESS-IN-OTHER-TYPE: editing/run/first-letter-crossing-engine-boundary-crash.html
+TESTHARNESS-IN-OTHER-TYPE: html/canvas/element/manual/wide-gamut-canvas/imagedata-no-color-settings-crash.html
+TESTHARNESS-IN-OTHER-TYPE: html/semantics/embedded-content/the-object-element/block-object-with-ruby-crash.html
+TESTHARNESS-IN-OTHER-TYPE: html/semantics/forms/the-input-element/time-datalist-crash.html
+TESTHARNESS-IN-OTHER-TYPE: html/semantics/forms/the-input-element/type-change-file-to-text-crash.html
+TESTHARNESS-IN-OTHER-TYPE: html/semantics/interactive-elements/the-details-element/display-table-with-rt-crash.html
+TESTHARNESS-IN-OTHER-TYPE: html/semantics/interactive-elements/the-summary-element/display-table-with-rt-crash.html
+TESTHARNESS-IN-OTHER-TYPE: html/semantics/text-level-semantics/the-ruby-element/rt-without-ruby-crash.html
+TESTHARNESS-IN-OTHER-TYPE: portals/portals-no-frame-crash.html
+TESTHARNESS-IN-OTHER-TYPE: quirks/table-replaced-descendant-percentage-height-crash.html
+TESTHARNESS-IN-OTHER-TYPE: scroll-animations/scroll-timelines/null-scroll-source-crash.html
+TESTHARNESS-IN-OTHER-TYPE: svg/extensibility/foreignObject/foreign-object-circular-filter-reference-crash.html
+TESTHARNESS-IN-OTHER-TYPE: svg/extensibility/foreignObject/foreign-object-under-clip-path-crash.html
+TESTHARNESS-IN-OTHER-TYPE: svg/extensibility/foreignObject/foreign-object-under-defs-crash.html
+TESTHARNESS-IN-OTHER-TYPE: svg/svg-in-svg/svg-in-svg-circular-filter-reference-crash.html
+
+# Adding the testharnessreport.js script causes the test to never complete.
+MISSING-TESTHARNESSREPORT: accessibility/crashtests/computed-node-checked.html
+
+PRINT STATEMENT: webdriver/tests/bidi/browsing_context/print/*
+PRINT STATEMENT: webdriver/tests/classic/print/*
+PRINT STATEMENT: webdriver/tests/support/fixtures_bidi.py
+
+DUPLICATE-BASENAME-PATH: acid/acid3/empty.html
+DUPLICATE-BASENAME-PATH: acid/acid3/empty.xml
+DUPLICATE-BASENAME-PATH: dom/nodes/Document-createElement-namespace-tests/*
+DUPLICATE-BASENAME-PATH: dom/nodes/ParentNode-querySelector-All-content.html
+DUPLICATE-BASENAME-PATH: dom/nodes/ParentNode-querySelector-All-content.xht
+DUPLICATE-BASENAME-PATH: svg/struct/reftests/reference/green-100x100.html
+DUPLICATE-BASENAME-PATH: svg/struct/reftests/reference/green-100x100.svg
+
+SET TIMEOUT: mediacapture-insertable-streams/MediaStreamTrackProcessor-video.https.html
+
+# This is a subresource which cannot use step_timeout without becoming a test
+# itself. See https://github.com/web-platform-tests/wpt/issues/16933
+SET TIMEOUT: scroll-to-text-fragment/iframe-target.html
+
+# Ported crashtests from Mozilla
+SET TIMEOUT: editing/crashtests/backcolor-in-nested-editing-host-td-from-DOMAttrModified.html
+SET TIMEOUT: editing/crashtests/contenteditable-will-be-blurred-by-focus-event-listener.html
+SET TIMEOUT: editing/crashtests/delete-content-in-no-data-object.html
+SET TIMEOUT: editing/crashtests/designMode-document-will-be-blurred-by-focus-event-listener.html
+SET TIMEOUT: editing/crashtests/indent-outdent-after-closing-editable-dialog-element.html
+SET TIMEOUT: editing/crashtests/inserthtml-after-temporarily-removing-document-element.html
+SET TIMEOUT: editing/crashtests/inserthtml-in-text-adopted-to-other-document.html
+SET TIMEOUT: editing/crashtests/insertorderedlist-in-text-adopted-to-other-document.html
+SET TIMEOUT: editing/crashtests/make-editable-div-inline-and-set-contenteditable-of-input-to-false.html
+SET TIMEOUT: editing/crashtests/outdent-across-svg-boundary.html
+SET TIMEOUT: editing/crashtests/textarea-will-be-blurred-by-focus-event-listener.html
+SET TIMEOUT: mathml/crashtests/mozilla/*
+PARSE-FAILED: mathml/crashtests/mozilla/289180-1.xml
diff --git a/test/wpt/tests/mimesniff/META.yml b/test/wpt/tests/mimesniff/META.yml
new file mode 100644
index 0000000..fd41c87
--- /dev/null
+++ b/test/wpt/tests/mimesniff/META.yml
@@ -0,0 +1,3 @@
+spec: https://mimesniff.spec.whatwg.org/
+suggested_reviewers:
+ - annevk
diff --git a/test/wpt/tests/mimesniff/README.md b/test/wpt/tests/mimesniff/README.md
new file mode 100644
index 0000000..4687cce
--- /dev/null
+++ b/test/wpt/tests/mimesniff/README.md
@@ -0,0 +1,4 @@
+Tests for the [MIME Sniffing Standard](https://mimesniff.spec.whatwg.org/).
+
+Some tests are generated from data files. To update the generated
+tests, run `wpt update-built --include mimesniff`
diff --git a/test/wpt/tests/mimesniff/media/media-sniff.window.js b/test/wpt/tests/mimesniff/media/media-sniff.window.js
new file mode 100644
index 0000000..8e9bf9a
--- /dev/null
+++ b/test/wpt/tests/mimesniff/media/media-sniff.window.js
@@ -0,0 +1,32 @@
+const tests = {
+ "vectors": [
+ "mp3-raw.mp3",
+ "mp3-with-id3.mp3",
+ "flac.flac",
+ "ogg.ogg",
+ "mp4.mp4",
+ "wav.wav",
+ "webm.webm"
+ ],
+ "contentTypes": [
+ "",
+ "bogus/mime",
+ "application/octet-stream",
+ "text/html",
+ "audio/ogg; codec=vorbis",
+ "application/pdf"
+ ]
+};
+
+tests.vectors.forEach(vector => {
+ tests.contentTypes.forEach(type => {
+ async_test(t => {
+ const element = document.createElement("audio");
+ element.src = "resources/" + vector + "?pipe=header(Content-Type,"+type+")"
+
+ element.addEventListener("error", t.unreached_func("No error expected frorm the HTMLMediaElement"));
+ element.addEventListener("loadedmetadata", t.step_func_done());
+ element.load();
+ }, vector + " loads when served with Content-Type " + type);
+ });
+});
diff --git a/test/wpt/tests/mimesniff/media/resources/flac.flac b/test/wpt/tests/mimesniff/media/resources/flac.flac
new file mode 100644
index 0000000..747ed38
--- /dev/null
+++ b/test/wpt/tests/mimesniff/media/resources/flac.flac
Binary files differ
diff --git a/test/wpt/tests/mimesniff/media/resources/make-vectors.sh b/test/wpt/tests/mimesniff/media/resources/make-vectors.sh
new file mode 100644
index 0000000..2cc0d46
--- /dev/null
+++ b/test/wpt/tests/mimesniff/media/resources/make-vectors.sh
@@ -0,0 +1,10 @@
+#!/bin/sh
+
+INPUT=wav.wav
+sox -V -r 44100 -n -b 8 -c 1 wav.wav synth 0.01 sin 330 vol -6db
+ffmpeg -i $INPUT -write_xing 0 -id3v2_version 0 mp3-raw.mp3
+ffmpeg -i $INPUT mp3-with-id3.mp3
+ffmpeg -i $INPUT flac.flac
+ffmpeg -i $INPUT ogg.ogg
+ffmpeg -i $INPUT mp4.mp4
+ffmpeg -i $INPUT webm.webm
diff --git a/test/wpt/tests/mimesniff/media/resources/mp3-raw.mp3 b/test/wpt/tests/mimesniff/media/resources/mp3-raw.mp3
new file mode 100644
index 0000000..dcc5240
--- /dev/null
+++ b/test/wpt/tests/mimesniff/media/resources/mp3-raw.mp3
Binary files differ
diff --git a/test/wpt/tests/mimesniff/media/resources/mp3-with-id3.mp3 b/test/wpt/tests/mimesniff/media/resources/mp3-with-id3.mp3
new file mode 100644
index 0000000..a6a2451
--- /dev/null
+++ b/test/wpt/tests/mimesniff/media/resources/mp3-with-id3.mp3
Binary files differ
diff --git a/test/wpt/tests/mimesniff/media/resources/mp4.mp4 b/test/wpt/tests/mimesniff/media/resources/mp4.mp4
new file mode 100644
index 0000000..abefb4f
--- /dev/null
+++ b/test/wpt/tests/mimesniff/media/resources/mp4.mp4
Binary files differ
diff --git a/test/wpt/tests/mimesniff/media/resources/ogg.ogg b/test/wpt/tests/mimesniff/media/resources/ogg.ogg
new file mode 100644
index 0000000..ba0b182
--- /dev/null
+++ b/test/wpt/tests/mimesniff/media/resources/ogg.ogg
Binary files differ
diff --git a/test/wpt/tests/mimesniff/media/resources/wav.wav b/test/wpt/tests/mimesniff/media/resources/wav.wav
new file mode 100644
index 0000000..5229388
--- /dev/null
+++ b/test/wpt/tests/mimesniff/media/resources/wav.wav
Binary files differ
diff --git a/test/wpt/tests/mimesniff/media/resources/webm.webm b/test/wpt/tests/mimesniff/media/resources/webm.webm
new file mode 100644
index 0000000..32a4af8
--- /dev/null
+++ b/test/wpt/tests/mimesniff/media/resources/webm.webm
Binary files differ
diff --git a/test/wpt/tests/mimesniff/mime-types/README.md b/test/wpt/tests/mimesniff/mime-types/README.md
new file mode 100644
index 0000000..eb5fba0
--- /dev/null
+++ b/test/wpt/tests/mimesniff/mime-types/README.md
@@ -0,0 +1,47 @@
+## MIME types
+
+`resources/mime-types.json` and `resources/generated-mime-types.json` contain MIME type tests. The tests are encoded as a JSON array. String values in the array serve as documentation. All other values are objects with the following fields:
+
+* `input`: The string to be parsed.
+* `output`: Null if parsing resulted in failure and the MIME type record serialized as string otherwise.
+* `navigable`: True if the MIME type can be used for a document to be loaded in a browsing context (i.e., does not result in a download) and omitted otherwise.
+* `encoding`: The encoding that can be extracted from the MIME type or null if no encoding can be extracted, and omitted otherwise.
+
+Note: the object description implies that there tests without `navigable` or `encoding` set.
+
+A wrapper for these JSON MIME type tests needs to take care that not all `input` values can be tested in all entrypoints. Some entrypoints only accept bytes and some have further restrictions. A function such as the one below can be used to differentiate:
+
+```js
+function isByteCompatible(str) {
+ // see https://fetch.spec.whatwg.org/#concept-header-value-normalize
+ if(/^[\u0009\u0020\u000A\u000D]+|[\u0009\u0020\u000A\u000D]+$/.test(str)) {
+ return "header-value-incompatible";
+ }
+
+ for(let i = 0; i < str.length; i++) {
+ const charCode = str.charCodeAt(i);
+ // See https://fetch.spec.whatwg.org/#concept-header-value
+ if(charCode > 0xFF) {
+ return "incompatible";
+ } else if(charCode === 0x00 || charCode === 0x0A || charCode === 0x0D) {
+ return "header-value-incompatible";
+ }
+ }
+ return "compatible";
+}
+```
+
+`resources/generated-mime-types.json` is generated by running `resources/generated-mime-types.py`. Modify the latter to correct the former.
+
+These tests are used by resources in this directory to test various aspects of MIME types.
+
+## MIME type groups
+
+`resources/mime-groups.json` contains MIME type group-membership tests. The tests are encoded as a JSON array. String values in the array serve as documentation. All other values are objects with the following fields:
+
+* `input`: The MIME type to test.
+* `groups`: An array of zero or more groups to which the MIME type belongs.
+
+In order to pass the tests an implementation must treat each MIME type as belonging to the exact set of groups listed, with no additions or omissions.
+
+Note: As MIME type groups are used only while determining the computed MIME type of a resource and are not exposed in any API, no browser-based test harness is available.
diff --git a/test/wpt/tests/mimesniff/mime-types/charset-parameter.window.js b/test/wpt/tests/mimesniff/mime-types/charset-parameter.window.js
new file mode 100644
index 0000000..923c3a3
--- /dev/null
+++ b/test/wpt/tests/mimesniff/mime-types/charset-parameter.window.js
@@ -0,0 +1,61 @@
+// META: timeout=long
+
+promise_test(() => {
+ // Don't load generated-mime-types.json as none of them are navigable
+ return fetch("resources/mime-types.json").then(res => res.json().then(runTests));
+}, "Loading data…");
+
+function isByteCompatible(str) {
+ // see https://fetch.spec.whatwg.org/#concept-header-value-normalize
+ if(/^[\u0009\u0020\u000A\u000D]+|[\u0009\u0020\u000A\u000D]+$/.test(str)) {
+ return "header-value-incompatible";
+ }
+
+ for(let i = 0; i < str.length; i++) {
+ const charCode = str.charCodeAt(i);
+ // See https://fetch.spec.whatwg.org/#concept-header-value
+ if(charCode > 0xFF) {
+ return "incompatible";
+ } else if(charCode === 0x00 || charCode === 0x0A || charCode === 0x0D) {
+ return "header-value-error";
+ }
+ }
+ return "compatible";
+}
+
+function encodeForURL(str) {
+ let output = "";
+ for(let i = 0; i < str.length; i++) {
+ const char = str.charCodeAt(i);
+ if(char > 0xFF) {
+ throw new Error("We cannot deal with input that is not latin1");
+ } else {
+ output += "%" + char.toString(16).padStart(2, "0");
+ }
+ }
+ return output;
+}
+
+function runTests(tests) {
+ tests.forEach(val => {
+ if(typeof val === "string" || val.navigable === undefined || val.encoding === undefined || isByteCompatible(val.input) !== "compatible") {
+ return;
+ }
+ const mime = val.input;
+ async_test(t => {
+ const frame = document.createElement("iframe"),
+ expectedEncoding = val.encoding === null ? "UTF-8" : val.encoding;
+ t.add_cleanup(() => frame.remove());
+ frame.onload = t.step_func(() => {
+ if(frame.contentWindow.location.href === "about:blank") {
+ return;
+ }
+ // Edge fails all these tests due to not using the correct encoding label.
+ assert_equals(frame.contentDocument.characterSet, expectedEncoding);
+ t.done();
+ });
+ frame.src = "resources/mime-charset.py?type=" + encodeForURL(mime);
+ document.body.appendChild(frame);
+ }, mime);
+ });
+}
diff --git a/test/wpt/tests/mimesniff/mime-types/parsing.any.js b/test/wpt/tests/mimesniff/mime-types/parsing.any.js
new file mode 100644
index 0000000..93be6bf
--- /dev/null
+++ b/test/wpt/tests/mimesniff/mime-types/parsing.any.js
@@ -0,0 +1,57 @@
+// META: timeout=long
+
+promise_test(() => {
+ return Promise.all([
+ fetch("resources/mime-types.json"),
+ fetch("resources/generated-mime-types.json")
+ ]).then(([res, res2]) => res.json().then(runTests).then(() => res2.json().then(runTests)));
+}, "Loading data…");
+
+function isByteCompatible(str) {
+ // see https://fetch.spec.whatwg.org/#concept-header-value-normalize
+ if(/^[\u0009\u0020\u000A\u000D]+|[\u0009\u0020\u000A\u000D]+$/.test(str)) {
+ return "header-value-incompatible";
+ }
+
+ for(let i = 0; i < str.length; i++) {
+ const charCode = str.charCodeAt(i);
+ // See https://fetch.spec.whatwg.org/#concept-header-value
+ if(charCode > 0xFF) {
+ return "incompatible";
+ } else if(charCode === 0x00 || charCode === 0x0A || charCode === 0x0D) {
+ return "header-value-error";
+ }
+ }
+ return "compatible";
+}
+
+function runTests(tests) {
+ tests.forEach(val => {
+ if(typeof val === "string") {
+ return;
+ }
+ const output = val.output === null ? "" : val.output
+ test(() => {
+ assert_equals(new Blob([], { type: val.input}).type, output, "Blob");
+ assert_equals(new File([], "noname", { type: val.input}).type, output, "File");
+ }, val.input + " (Blob/File)");
+
+ const compatibleNess = isByteCompatible(val.input);
+ if(compatibleNess === "header-value-incompatible") {
+ return;
+ }
+
+ promise_test(() => {
+ if(compatibleNess === "incompatible" || compatibleNess === "header-value-error") {
+ assert_throws_js(TypeError, () => new Request("about:blank", { headers: [["Content-Type", val.input]] }));
+ assert_throws_js(TypeError, () => new Response(null, { headers: [["Content-Type", val.input]] }));
+ return Promise.resolve();
+ } else {
+ return Promise.all([
+ new Request("about:blank", { headers: [["Content-Type", val.input]] }).blob().then(blob => assert_equals(blob.type, output)),
+ new Response(null, { headers: [["Content-Type", val.input]] }).blob().then(blob => assert_equals(blob.type, output))
+ ]);
+ }
+ }, val.input + " (Request/Response)");
+ });
+}
diff --git a/test/wpt/tests/mimesniff/mime-types/resources/generated-mime-types.json b/test/wpt/tests/mimesniff/mime-types/resources/generated-mime-types.json
new file mode 100644
index 0000000..f8934da
--- /dev/null
+++ b/test/wpt/tests/mimesniff/mime-types/resources/generated-mime-types.json
@@ -0,0 +1,3526 @@
+[
+ {
+ "input": "\u0000/x",
+ "output": null
+ },
+ {
+ "input": "x/\u0000",
+ "output": null
+ },
+ {
+ "input": "x/x;\u0000=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u0000;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u0000\";bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "\u0001/x",
+ "output": null
+ },
+ {
+ "input": "x/\u0001",
+ "output": null
+ },
+ {
+ "input": "x/x;\u0001=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u0001;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u0001\";bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "\u0002/x",
+ "output": null
+ },
+ {
+ "input": "x/\u0002",
+ "output": null
+ },
+ {
+ "input": "x/x;\u0002=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u0002;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u0002\";bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "\u0003/x",
+ "output": null
+ },
+ {
+ "input": "x/\u0003",
+ "output": null
+ },
+ {
+ "input": "x/x;\u0003=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u0003;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u0003\";bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "\u0004/x",
+ "output": null
+ },
+ {
+ "input": "x/\u0004",
+ "output": null
+ },
+ {
+ "input": "x/x;\u0004=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u0004;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u0004\";bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "\u0005/x",
+ "output": null
+ },
+ {
+ "input": "x/\u0005",
+ "output": null
+ },
+ {
+ "input": "x/x;\u0005=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u0005;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u0005\";bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "\u0006/x",
+ "output": null
+ },
+ {
+ "input": "x/\u0006",
+ "output": null
+ },
+ {
+ "input": "x/x;\u0006=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u0006;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u0006\";bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "\u0007/x",
+ "output": null
+ },
+ {
+ "input": "x/\u0007",
+ "output": null
+ },
+ {
+ "input": "x/x;\u0007=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u0007;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u0007\";bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "\b/x",
+ "output": null
+ },
+ {
+ "input": "x/\b",
+ "output": null
+ },
+ {
+ "input": "x/x;\b=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\b;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\b\";bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "\t/x",
+ "output": null
+ },
+ {
+ "input": "x/\t",
+ "output": null
+ },
+ {
+ "input": "x/x;\t=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "\n/x",
+ "output": null
+ },
+ {
+ "input": "x/\n",
+ "output": null
+ },
+ {
+ "input": "x/x;\n=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\n;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\n\";bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "\u000b/x",
+ "output": null
+ },
+ {
+ "input": "x/\u000b",
+ "output": null
+ },
+ {
+ "input": "x/x;\u000b=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u000b;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u000b\";bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "\f/x",
+ "output": null
+ },
+ {
+ "input": "x/\f",
+ "output": null
+ },
+ {
+ "input": "x/x;\f=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\f;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\f\";bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "\r/x",
+ "output": null
+ },
+ {
+ "input": "x/\r",
+ "output": null
+ },
+ {
+ "input": "x/x;\r=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\r;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\r\";bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "\u000e/x",
+ "output": null
+ },
+ {
+ "input": "x/\u000e",
+ "output": null
+ },
+ {
+ "input": "x/x;\u000e=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u000e;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u000e\";bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "\u000f/x",
+ "output": null
+ },
+ {
+ "input": "x/\u000f",
+ "output": null
+ },
+ {
+ "input": "x/x;\u000f=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u000f;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u000f\";bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "\u0010/x",
+ "output": null
+ },
+ {
+ "input": "x/\u0010",
+ "output": null
+ },
+ {
+ "input": "x/x;\u0010=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u0010;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u0010\";bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "\u0011/x",
+ "output": null
+ },
+ {
+ "input": "x/\u0011",
+ "output": null
+ },
+ {
+ "input": "x/x;\u0011=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u0011;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u0011\";bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "\u0012/x",
+ "output": null
+ },
+ {
+ "input": "x/\u0012",
+ "output": null
+ },
+ {
+ "input": "x/x;\u0012=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u0012;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u0012\";bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "\u0013/x",
+ "output": null
+ },
+ {
+ "input": "x/\u0013",
+ "output": null
+ },
+ {
+ "input": "x/x;\u0013=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u0013;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u0013\";bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "\u0014/x",
+ "output": null
+ },
+ {
+ "input": "x/\u0014",
+ "output": null
+ },
+ {
+ "input": "x/x;\u0014=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u0014;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u0014\";bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "\u0015/x",
+ "output": null
+ },
+ {
+ "input": "x/\u0015",
+ "output": null
+ },
+ {
+ "input": "x/x;\u0015=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u0015;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u0015\";bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "\u0016/x",
+ "output": null
+ },
+ {
+ "input": "x/\u0016",
+ "output": null
+ },
+ {
+ "input": "x/x;\u0016=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u0016;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u0016\";bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "\u0017/x",
+ "output": null
+ },
+ {
+ "input": "x/\u0017",
+ "output": null
+ },
+ {
+ "input": "x/x;\u0017=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u0017;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u0017\";bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "\u0018/x",
+ "output": null
+ },
+ {
+ "input": "x/\u0018",
+ "output": null
+ },
+ {
+ "input": "x/x;\u0018=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u0018;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u0018\";bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "\u0019/x",
+ "output": null
+ },
+ {
+ "input": "x/\u0019",
+ "output": null
+ },
+ {
+ "input": "x/x;\u0019=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u0019;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u0019\";bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "\u001a/x",
+ "output": null
+ },
+ {
+ "input": "x/\u001a",
+ "output": null
+ },
+ {
+ "input": "x/x;\u001a=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u001a;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u001a\";bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "\u001b/x",
+ "output": null
+ },
+ {
+ "input": "x/\u001b",
+ "output": null
+ },
+ {
+ "input": "x/x;\u001b=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u001b;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u001b\";bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "\u001c/x",
+ "output": null
+ },
+ {
+ "input": "x/\u001c",
+ "output": null
+ },
+ {
+ "input": "x/x;\u001c=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u001c;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u001c\";bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "\u001d/x",
+ "output": null
+ },
+ {
+ "input": "x/\u001d",
+ "output": null
+ },
+ {
+ "input": "x/x;\u001d=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u001d;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u001d\";bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "\u001e/x",
+ "output": null
+ },
+ {
+ "input": "x/\u001e",
+ "output": null
+ },
+ {
+ "input": "x/x;\u001e=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u001e;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u001e\";bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "\u001f/x",
+ "output": null
+ },
+ {
+ "input": "x/\u001f",
+ "output": null
+ },
+ {
+ "input": "x/x;\u001f=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u001f;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u001f\";bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": " /x",
+ "output": null
+ },
+ {
+ "input": "x/ ",
+ "output": null
+ },
+ {
+ "input": "x/x; =x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "\"/x",
+ "output": null
+ },
+ {
+ "input": "x/\"",
+ "output": null
+ },
+ {
+ "input": "x/x;\"=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "(/x",
+ "output": null
+ },
+ {
+ "input": "x/(",
+ "output": null
+ },
+ {
+ "input": "x/x;(=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=(;bonus=x",
+ "output": "x/x;x=\"(\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"(\";bonus=x",
+ "output": "x/x;x=\"(\";bonus=x"
+ },
+ {
+ "input": ")/x",
+ "output": null
+ },
+ {
+ "input": "x/)",
+ "output": null
+ },
+ {
+ "input": "x/x;)=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=);bonus=x",
+ "output": "x/x;x=\")\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\")\";bonus=x",
+ "output": "x/x;x=\")\";bonus=x"
+ },
+ {
+ "input": ",/x",
+ "output": null
+ },
+ {
+ "input": "x/,",
+ "output": null
+ },
+ {
+ "input": "x/x;,=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=,;bonus=x",
+ "output": "x/x;x=\",\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\",\";bonus=x",
+ "output": "x/x;x=\",\";bonus=x"
+ },
+ {
+ "input": "x/x;/=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=/;bonus=x",
+ "output": "x/x;x=\"/\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"/\";bonus=x",
+ "output": "x/x;x=\"/\";bonus=x"
+ },
+ {
+ "input": ":/x",
+ "output": null
+ },
+ {
+ "input": "x/:",
+ "output": null
+ },
+ {
+ "input": "x/x;:=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=:;bonus=x",
+ "output": "x/x;x=\":\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\":\";bonus=x",
+ "output": "x/x;x=\":\";bonus=x"
+ },
+ {
+ "input": ";/x",
+ "output": null
+ },
+ {
+ "input": "x/;",
+ "output": null
+ },
+ {
+ "input": "</x",
+ "output": null
+ },
+ {
+ "input": "x/<",
+ "output": null
+ },
+ {
+ "input": "x/x;<=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=<;bonus=x",
+ "output": "x/x;x=\"<\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"<\";bonus=x",
+ "output": "x/x;x=\"<\";bonus=x"
+ },
+ {
+ "input": "=/x",
+ "output": null
+ },
+ {
+ "input": "x/=",
+ "output": null
+ },
+ {
+ "input": "x/x;x==;bonus=x",
+ "output": "x/x;x=\"=\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"=\";bonus=x",
+ "output": "x/x;x=\"=\";bonus=x"
+ },
+ {
+ "input": ">/x",
+ "output": null
+ },
+ {
+ "input": "x/>",
+ "output": null
+ },
+ {
+ "input": "x/x;>=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=>;bonus=x",
+ "output": "x/x;x=\">\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\">\";bonus=x",
+ "output": "x/x;x=\">\";bonus=x"
+ },
+ {
+ "input": "?/x",
+ "output": null
+ },
+ {
+ "input": "x/?",
+ "output": null
+ },
+ {
+ "input": "x/x;?=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=?;bonus=x",
+ "output": "x/x;x=\"?\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"?\";bonus=x",
+ "output": "x/x;x=\"?\";bonus=x"
+ },
+ {
+ "input": "@/x",
+ "output": null
+ },
+ {
+ "input": "x/@",
+ "output": null
+ },
+ {
+ "input": "x/x;@=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=@;bonus=x",
+ "output": "x/x;x=\"@\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"@\";bonus=x",
+ "output": "x/x;x=\"@\";bonus=x"
+ },
+ {
+ "input": "[/x",
+ "output": null
+ },
+ {
+ "input": "x/[",
+ "output": null
+ },
+ {
+ "input": "x/x;[=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=[;bonus=x",
+ "output": "x/x;x=\"[\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"[\";bonus=x",
+ "output": "x/x;x=\"[\";bonus=x"
+ },
+ {
+ "input": "\\/x",
+ "output": null
+ },
+ {
+ "input": "x/\\",
+ "output": null
+ },
+ {
+ "input": "x/x;\\=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "]/x",
+ "output": null
+ },
+ {
+ "input": "x/]",
+ "output": null
+ },
+ {
+ "input": "x/x;]=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=];bonus=x",
+ "output": "x/x;x=\"]\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"]\";bonus=x",
+ "output": "x/x;x=\"]\";bonus=x"
+ },
+ {
+ "input": "{/x",
+ "output": null
+ },
+ {
+ "input": "x/{",
+ "output": null
+ },
+ {
+ "input": "x/x;{=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x={;bonus=x",
+ "output": "x/x;x=\"{\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"{\";bonus=x",
+ "output": "x/x;x=\"{\";bonus=x"
+ },
+ {
+ "input": "}/x",
+ "output": null
+ },
+ {
+ "input": "x/}",
+ "output": null
+ },
+ {
+ "input": "x/x;}=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=};bonus=x",
+ "output": "x/x;x=\"}\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"}\";bonus=x",
+ "output": "x/x;x=\"}\";bonus=x"
+ },
+ {
+ "input": "\u007f/x",
+ "output": null
+ },
+ {
+ "input": "x/\u007f",
+ "output": null
+ },
+ {
+ "input": "x/x;\u007f=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u007f;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u007f\";bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "\u0080/x",
+ "output": null
+ },
+ {
+ "input": "x/\u0080",
+ "output": null
+ },
+ {
+ "input": "x/x;\u0080=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u0080;bonus=x",
+ "output": "x/x;x=\"\u0080\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u0080\";bonus=x",
+ "output": "x/x;x=\"\u0080\";bonus=x"
+ },
+ {
+ "input": "\u0081/x",
+ "output": null
+ },
+ {
+ "input": "x/\u0081",
+ "output": null
+ },
+ {
+ "input": "x/x;\u0081=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u0081;bonus=x",
+ "output": "x/x;x=\"\u0081\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u0081\";bonus=x",
+ "output": "x/x;x=\"\u0081\";bonus=x"
+ },
+ {
+ "input": "\u0082/x",
+ "output": null
+ },
+ {
+ "input": "x/\u0082",
+ "output": null
+ },
+ {
+ "input": "x/x;\u0082=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u0082;bonus=x",
+ "output": "x/x;x=\"\u0082\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u0082\";bonus=x",
+ "output": "x/x;x=\"\u0082\";bonus=x"
+ },
+ {
+ "input": "\u0083/x",
+ "output": null
+ },
+ {
+ "input": "x/\u0083",
+ "output": null
+ },
+ {
+ "input": "x/x;\u0083=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u0083;bonus=x",
+ "output": "x/x;x=\"\u0083\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u0083\";bonus=x",
+ "output": "x/x;x=\"\u0083\";bonus=x"
+ },
+ {
+ "input": "\u0084/x",
+ "output": null
+ },
+ {
+ "input": "x/\u0084",
+ "output": null
+ },
+ {
+ "input": "x/x;\u0084=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u0084;bonus=x",
+ "output": "x/x;x=\"\u0084\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u0084\";bonus=x",
+ "output": "x/x;x=\"\u0084\";bonus=x"
+ },
+ {
+ "input": "\u0085/x",
+ "output": null
+ },
+ {
+ "input": "x/\u0085",
+ "output": null
+ },
+ {
+ "input": "x/x;\u0085=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u0085;bonus=x",
+ "output": "x/x;x=\"\u0085\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u0085\";bonus=x",
+ "output": "x/x;x=\"\u0085\";bonus=x"
+ },
+ {
+ "input": "\u0086/x",
+ "output": null
+ },
+ {
+ "input": "x/\u0086",
+ "output": null
+ },
+ {
+ "input": "x/x;\u0086=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u0086;bonus=x",
+ "output": "x/x;x=\"\u0086\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u0086\";bonus=x",
+ "output": "x/x;x=\"\u0086\";bonus=x"
+ },
+ {
+ "input": "\u0087/x",
+ "output": null
+ },
+ {
+ "input": "x/\u0087",
+ "output": null
+ },
+ {
+ "input": "x/x;\u0087=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u0087;bonus=x",
+ "output": "x/x;x=\"\u0087\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u0087\";bonus=x",
+ "output": "x/x;x=\"\u0087\";bonus=x"
+ },
+ {
+ "input": "\u0088/x",
+ "output": null
+ },
+ {
+ "input": "x/\u0088",
+ "output": null
+ },
+ {
+ "input": "x/x;\u0088=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u0088;bonus=x",
+ "output": "x/x;x=\"\u0088\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u0088\";bonus=x",
+ "output": "x/x;x=\"\u0088\";bonus=x"
+ },
+ {
+ "input": "\u0089/x",
+ "output": null
+ },
+ {
+ "input": "x/\u0089",
+ "output": null
+ },
+ {
+ "input": "x/x;\u0089=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u0089;bonus=x",
+ "output": "x/x;x=\"\u0089\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u0089\";bonus=x",
+ "output": "x/x;x=\"\u0089\";bonus=x"
+ },
+ {
+ "input": "\u008a/x",
+ "output": null
+ },
+ {
+ "input": "x/\u008a",
+ "output": null
+ },
+ {
+ "input": "x/x;\u008a=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u008a;bonus=x",
+ "output": "x/x;x=\"\u008a\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u008a\";bonus=x",
+ "output": "x/x;x=\"\u008a\";bonus=x"
+ },
+ {
+ "input": "\u008b/x",
+ "output": null
+ },
+ {
+ "input": "x/\u008b",
+ "output": null
+ },
+ {
+ "input": "x/x;\u008b=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u008b;bonus=x",
+ "output": "x/x;x=\"\u008b\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u008b\";bonus=x",
+ "output": "x/x;x=\"\u008b\";bonus=x"
+ },
+ {
+ "input": "\u008c/x",
+ "output": null
+ },
+ {
+ "input": "x/\u008c",
+ "output": null
+ },
+ {
+ "input": "x/x;\u008c=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u008c;bonus=x",
+ "output": "x/x;x=\"\u008c\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u008c\";bonus=x",
+ "output": "x/x;x=\"\u008c\";bonus=x"
+ },
+ {
+ "input": "\u008d/x",
+ "output": null
+ },
+ {
+ "input": "x/\u008d",
+ "output": null
+ },
+ {
+ "input": "x/x;\u008d=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u008d;bonus=x",
+ "output": "x/x;x=\"\u008d\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u008d\";bonus=x",
+ "output": "x/x;x=\"\u008d\";bonus=x"
+ },
+ {
+ "input": "\u008e/x",
+ "output": null
+ },
+ {
+ "input": "x/\u008e",
+ "output": null
+ },
+ {
+ "input": "x/x;\u008e=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u008e;bonus=x",
+ "output": "x/x;x=\"\u008e\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u008e\";bonus=x",
+ "output": "x/x;x=\"\u008e\";bonus=x"
+ },
+ {
+ "input": "\u008f/x",
+ "output": null
+ },
+ {
+ "input": "x/\u008f",
+ "output": null
+ },
+ {
+ "input": "x/x;\u008f=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u008f;bonus=x",
+ "output": "x/x;x=\"\u008f\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u008f\";bonus=x",
+ "output": "x/x;x=\"\u008f\";bonus=x"
+ },
+ {
+ "input": "\u0090/x",
+ "output": null
+ },
+ {
+ "input": "x/\u0090",
+ "output": null
+ },
+ {
+ "input": "x/x;\u0090=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u0090;bonus=x",
+ "output": "x/x;x=\"\u0090\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u0090\";bonus=x",
+ "output": "x/x;x=\"\u0090\";bonus=x"
+ },
+ {
+ "input": "\u0091/x",
+ "output": null
+ },
+ {
+ "input": "x/\u0091",
+ "output": null
+ },
+ {
+ "input": "x/x;\u0091=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u0091;bonus=x",
+ "output": "x/x;x=\"\u0091\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u0091\";bonus=x",
+ "output": "x/x;x=\"\u0091\";bonus=x"
+ },
+ {
+ "input": "\u0092/x",
+ "output": null
+ },
+ {
+ "input": "x/\u0092",
+ "output": null
+ },
+ {
+ "input": "x/x;\u0092=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u0092;bonus=x",
+ "output": "x/x;x=\"\u0092\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u0092\";bonus=x",
+ "output": "x/x;x=\"\u0092\";bonus=x"
+ },
+ {
+ "input": "\u0093/x",
+ "output": null
+ },
+ {
+ "input": "x/\u0093",
+ "output": null
+ },
+ {
+ "input": "x/x;\u0093=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u0093;bonus=x",
+ "output": "x/x;x=\"\u0093\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u0093\";bonus=x",
+ "output": "x/x;x=\"\u0093\";bonus=x"
+ },
+ {
+ "input": "\u0094/x",
+ "output": null
+ },
+ {
+ "input": "x/\u0094",
+ "output": null
+ },
+ {
+ "input": "x/x;\u0094=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u0094;bonus=x",
+ "output": "x/x;x=\"\u0094\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u0094\";bonus=x",
+ "output": "x/x;x=\"\u0094\";bonus=x"
+ },
+ {
+ "input": "\u0095/x",
+ "output": null
+ },
+ {
+ "input": "x/\u0095",
+ "output": null
+ },
+ {
+ "input": "x/x;\u0095=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u0095;bonus=x",
+ "output": "x/x;x=\"\u0095\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u0095\";bonus=x",
+ "output": "x/x;x=\"\u0095\";bonus=x"
+ },
+ {
+ "input": "\u0096/x",
+ "output": null
+ },
+ {
+ "input": "x/\u0096",
+ "output": null
+ },
+ {
+ "input": "x/x;\u0096=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u0096;bonus=x",
+ "output": "x/x;x=\"\u0096\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u0096\";bonus=x",
+ "output": "x/x;x=\"\u0096\";bonus=x"
+ },
+ {
+ "input": "\u0097/x",
+ "output": null
+ },
+ {
+ "input": "x/\u0097",
+ "output": null
+ },
+ {
+ "input": "x/x;\u0097=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u0097;bonus=x",
+ "output": "x/x;x=\"\u0097\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u0097\";bonus=x",
+ "output": "x/x;x=\"\u0097\";bonus=x"
+ },
+ {
+ "input": "\u0098/x",
+ "output": null
+ },
+ {
+ "input": "x/\u0098",
+ "output": null
+ },
+ {
+ "input": "x/x;\u0098=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u0098;bonus=x",
+ "output": "x/x;x=\"\u0098\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u0098\";bonus=x",
+ "output": "x/x;x=\"\u0098\";bonus=x"
+ },
+ {
+ "input": "\u0099/x",
+ "output": null
+ },
+ {
+ "input": "x/\u0099",
+ "output": null
+ },
+ {
+ "input": "x/x;\u0099=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u0099;bonus=x",
+ "output": "x/x;x=\"\u0099\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u0099\";bonus=x",
+ "output": "x/x;x=\"\u0099\";bonus=x"
+ },
+ {
+ "input": "\u009a/x",
+ "output": null
+ },
+ {
+ "input": "x/\u009a",
+ "output": null
+ },
+ {
+ "input": "x/x;\u009a=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u009a;bonus=x",
+ "output": "x/x;x=\"\u009a\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u009a\";bonus=x",
+ "output": "x/x;x=\"\u009a\";bonus=x"
+ },
+ {
+ "input": "\u009b/x",
+ "output": null
+ },
+ {
+ "input": "x/\u009b",
+ "output": null
+ },
+ {
+ "input": "x/x;\u009b=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u009b;bonus=x",
+ "output": "x/x;x=\"\u009b\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u009b\";bonus=x",
+ "output": "x/x;x=\"\u009b\";bonus=x"
+ },
+ {
+ "input": "\u009c/x",
+ "output": null
+ },
+ {
+ "input": "x/\u009c",
+ "output": null
+ },
+ {
+ "input": "x/x;\u009c=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u009c;bonus=x",
+ "output": "x/x;x=\"\u009c\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u009c\";bonus=x",
+ "output": "x/x;x=\"\u009c\";bonus=x"
+ },
+ {
+ "input": "\u009d/x",
+ "output": null
+ },
+ {
+ "input": "x/\u009d",
+ "output": null
+ },
+ {
+ "input": "x/x;\u009d=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u009d;bonus=x",
+ "output": "x/x;x=\"\u009d\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u009d\";bonus=x",
+ "output": "x/x;x=\"\u009d\";bonus=x"
+ },
+ {
+ "input": "\u009e/x",
+ "output": null
+ },
+ {
+ "input": "x/\u009e",
+ "output": null
+ },
+ {
+ "input": "x/x;\u009e=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u009e;bonus=x",
+ "output": "x/x;x=\"\u009e\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u009e\";bonus=x",
+ "output": "x/x;x=\"\u009e\";bonus=x"
+ },
+ {
+ "input": "\u009f/x",
+ "output": null
+ },
+ {
+ "input": "x/\u009f",
+ "output": null
+ },
+ {
+ "input": "x/x;\u009f=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u009f;bonus=x",
+ "output": "x/x;x=\"\u009f\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u009f\";bonus=x",
+ "output": "x/x;x=\"\u009f\";bonus=x"
+ },
+ {
+ "input": "\u00a0/x",
+ "output": null
+ },
+ {
+ "input": "x/\u00a0",
+ "output": null
+ },
+ {
+ "input": "x/x;\u00a0=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u00a0;bonus=x",
+ "output": "x/x;x=\"\u00a0\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u00a0\";bonus=x",
+ "output": "x/x;x=\"\u00a0\";bonus=x"
+ },
+ {
+ "input": "\u00a1/x",
+ "output": null
+ },
+ {
+ "input": "x/\u00a1",
+ "output": null
+ },
+ {
+ "input": "x/x;\u00a1=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u00a1;bonus=x",
+ "output": "x/x;x=\"\u00a1\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u00a1\";bonus=x",
+ "output": "x/x;x=\"\u00a1\";bonus=x"
+ },
+ {
+ "input": "\u00a2/x",
+ "output": null
+ },
+ {
+ "input": "x/\u00a2",
+ "output": null
+ },
+ {
+ "input": "x/x;\u00a2=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u00a2;bonus=x",
+ "output": "x/x;x=\"\u00a2\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u00a2\";bonus=x",
+ "output": "x/x;x=\"\u00a2\";bonus=x"
+ },
+ {
+ "input": "\u00a3/x",
+ "output": null
+ },
+ {
+ "input": "x/\u00a3",
+ "output": null
+ },
+ {
+ "input": "x/x;\u00a3=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u00a3;bonus=x",
+ "output": "x/x;x=\"\u00a3\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u00a3\";bonus=x",
+ "output": "x/x;x=\"\u00a3\";bonus=x"
+ },
+ {
+ "input": "\u00a4/x",
+ "output": null
+ },
+ {
+ "input": "x/\u00a4",
+ "output": null
+ },
+ {
+ "input": "x/x;\u00a4=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u00a4;bonus=x",
+ "output": "x/x;x=\"\u00a4\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u00a4\";bonus=x",
+ "output": "x/x;x=\"\u00a4\";bonus=x"
+ },
+ {
+ "input": "\u00a5/x",
+ "output": null
+ },
+ {
+ "input": "x/\u00a5",
+ "output": null
+ },
+ {
+ "input": "x/x;\u00a5=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u00a5;bonus=x",
+ "output": "x/x;x=\"\u00a5\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u00a5\";bonus=x",
+ "output": "x/x;x=\"\u00a5\";bonus=x"
+ },
+ {
+ "input": "\u00a6/x",
+ "output": null
+ },
+ {
+ "input": "x/\u00a6",
+ "output": null
+ },
+ {
+ "input": "x/x;\u00a6=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u00a6;bonus=x",
+ "output": "x/x;x=\"\u00a6\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u00a6\";bonus=x",
+ "output": "x/x;x=\"\u00a6\";bonus=x"
+ },
+ {
+ "input": "\u00a7/x",
+ "output": null
+ },
+ {
+ "input": "x/\u00a7",
+ "output": null
+ },
+ {
+ "input": "x/x;\u00a7=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u00a7;bonus=x",
+ "output": "x/x;x=\"\u00a7\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u00a7\";bonus=x",
+ "output": "x/x;x=\"\u00a7\";bonus=x"
+ },
+ {
+ "input": "\u00a8/x",
+ "output": null
+ },
+ {
+ "input": "x/\u00a8",
+ "output": null
+ },
+ {
+ "input": "x/x;\u00a8=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u00a8;bonus=x",
+ "output": "x/x;x=\"\u00a8\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u00a8\";bonus=x",
+ "output": "x/x;x=\"\u00a8\";bonus=x"
+ },
+ {
+ "input": "\u00a9/x",
+ "output": null
+ },
+ {
+ "input": "x/\u00a9",
+ "output": null
+ },
+ {
+ "input": "x/x;\u00a9=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u00a9;bonus=x",
+ "output": "x/x;x=\"\u00a9\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u00a9\";bonus=x",
+ "output": "x/x;x=\"\u00a9\";bonus=x"
+ },
+ {
+ "input": "\u00aa/x",
+ "output": null
+ },
+ {
+ "input": "x/\u00aa",
+ "output": null
+ },
+ {
+ "input": "x/x;\u00aa=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u00aa;bonus=x",
+ "output": "x/x;x=\"\u00aa\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u00aa\";bonus=x",
+ "output": "x/x;x=\"\u00aa\";bonus=x"
+ },
+ {
+ "input": "\u00ab/x",
+ "output": null
+ },
+ {
+ "input": "x/\u00ab",
+ "output": null
+ },
+ {
+ "input": "x/x;\u00ab=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u00ab;bonus=x",
+ "output": "x/x;x=\"\u00ab\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u00ab\";bonus=x",
+ "output": "x/x;x=\"\u00ab\";bonus=x"
+ },
+ {
+ "input": "\u00ac/x",
+ "output": null
+ },
+ {
+ "input": "x/\u00ac",
+ "output": null
+ },
+ {
+ "input": "x/x;\u00ac=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u00ac;bonus=x",
+ "output": "x/x;x=\"\u00ac\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u00ac\";bonus=x",
+ "output": "x/x;x=\"\u00ac\";bonus=x"
+ },
+ {
+ "input": "\u00ad/x",
+ "output": null
+ },
+ {
+ "input": "x/\u00ad",
+ "output": null
+ },
+ {
+ "input": "x/x;\u00ad=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u00ad;bonus=x",
+ "output": "x/x;x=\"\u00ad\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u00ad\";bonus=x",
+ "output": "x/x;x=\"\u00ad\";bonus=x"
+ },
+ {
+ "input": "\u00ae/x",
+ "output": null
+ },
+ {
+ "input": "x/\u00ae",
+ "output": null
+ },
+ {
+ "input": "x/x;\u00ae=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u00ae;bonus=x",
+ "output": "x/x;x=\"\u00ae\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u00ae\";bonus=x",
+ "output": "x/x;x=\"\u00ae\";bonus=x"
+ },
+ {
+ "input": "\u00af/x",
+ "output": null
+ },
+ {
+ "input": "x/\u00af",
+ "output": null
+ },
+ {
+ "input": "x/x;\u00af=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u00af;bonus=x",
+ "output": "x/x;x=\"\u00af\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u00af\";bonus=x",
+ "output": "x/x;x=\"\u00af\";bonus=x"
+ },
+ {
+ "input": "\u00b0/x",
+ "output": null
+ },
+ {
+ "input": "x/\u00b0",
+ "output": null
+ },
+ {
+ "input": "x/x;\u00b0=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u00b0;bonus=x",
+ "output": "x/x;x=\"\u00b0\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u00b0\";bonus=x",
+ "output": "x/x;x=\"\u00b0\";bonus=x"
+ },
+ {
+ "input": "\u00b1/x",
+ "output": null
+ },
+ {
+ "input": "x/\u00b1",
+ "output": null
+ },
+ {
+ "input": "x/x;\u00b1=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u00b1;bonus=x",
+ "output": "x/x;x=\"\u00b1\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u00b1\";bonus=x",
+ "output": "x/x;x=\"\u00b1\";bonus=x"
+ },
+ {
+ "input": "\u00b2/x",
+ "output": null
+ },
+ {
+ "input": "x/\u00b2",
+ "output": null
+ },
+ {
+ "input": "x/x;\u00b2=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u00b2;bonus=x",
+ "output": "x/x;x=\"\u00b2\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u00b2\";bonus=x",
+ "output": "x/x;x=\"\u00b2\";bonus=x"
+ },
+ {
+ "input": "\u00b3/x",
+ "output": null
+ },
+ {
+ "input": "x/\u00b3",
+ "output": null
+ },
+ {
+ "input": "x/x;\u00b3=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u00b3;bonus=x",
+ "output": "x/x;x=\"\u00b3\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u00b3\";bonus=x",
+ "output": "x/x;x=\"\u00b3\";bonus=x"
+ },
+ {
+ "input": "\u00b4/x",
+ "output": null
+ },
+ {
+ "input": "x/\u00b4",
+ "output": null
+ },
+ {
+ "input": "x/x;\u00b4=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u00b4;bonus=x",
+ "output": "x/x;x=\"\u00b4\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u00b4\";bonus=x",
+ "output": "x/x;x=\"\u00b4\";bonus=x"
+ },
+ {
+ "input": "\u00b5/x",
+ "output": null
+ },
+ {
+ "input": "x/\u00b5",
+ "output": null
+ },
+ {
+ "input": "x/x;\u00b5=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u00b5;bonus=x",
+ "output": "x/x;x=\"\u00b5\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u00b5\";bonus=x",
+ "output": "x/x;x=\"\u00b5\";bonus=x"
+ },
+ {
+ "input": "\u00b6/x",
+ "output": null
+ },
+ {
+ "input": "x/\u00b6",
+ "output": null
+ },
+ {
+ "input": "x/x;\u00b6=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u00b6;bonus=x",
+ "output": "x/x;x=\"\u00b6\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u00b6\";bonus=x",
+ "output": "x/x;x=\"\u00b6\";bonus=x"
+ },
+ {
+ "input": "\u00b7/x",
+ "output": null
+ },
+ {
+ "input": "x/\u00b7",
+ "output": null
+ },
+ {
+ "input": "x/x;\u00b7=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u00b7;bonus=x",
+ "output": "x/x;x=\"\u00b7\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u00b7\";bonus=x",
+ "output": "x/x;x=\"\u00b7\";bonus=x"
+ },
+ {
+ "input": "\u00b8/x",
+ "output": null
+ },
+ {
+ "input": "x/\u00b8",
+ "output": null
+ },
+ {
+ "input": "x/x;\u00b8=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u00b8;bonus=x",
+ "output": "x/x;x=\"\u00b8\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u00b8\";bonus=x",
+ "output": "x/x;x=\"\u00b8\";bonus=x"
+ },
+ {
+ "input": "\u00b9/x",
+ "output": null
+ },
+ {
+ "input": "x/\u00b9",
+ "output": null
+ },
+ {
+ "input": "x/x;\u00b9=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u00b9;bonus=x",
+ "output": "x/x;x=\"\u00b9\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u00b9\";bonus=x",
+ "output": "x/x;x=\"\u00b9\";bonus=x"
+ },
+ {
+ "input": "\u00ba/x",
+ "output": null
+ },
+ {
+ "input": "x/\u00ba",
+ "output": null
+ },
+ {
+ "input": "x/x;\u00ba=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u00ba;bonus=x",
+ "output": "x/x;x=\"\u00ba\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u00ba\";bonus=x",
+ "output": "x/x;x=\"\u00ba\";bonus=x"
+ },
+ {
+ "input": "\u00bb/x",
+ "output": null
+ },
+ {
+ "input": "x/\u00bb",
+ "output": null
+ },
+ {
+ "input": "x/x;\u00bb=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u00bb;bonus=x",
+ "output": "x/x;x=\"\u00bb\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u00bb\";bonus=x",
+ "output": "x/x;x=\"\u00bb\";bonus=x"
+ },
+ {
+ "input": "\u00bc/x",
+ "output": null
+ },
+ {
+ "input": "x/\u00bc",
+ "output": null
+ },
+ {
+ "input": "x/x;\u00bc=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u00bc;bonus=x",
+ "output": "x/x;x=\"\u00bc\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u00bc\";bonus=x",
+ "output": "x/x;x=\"\u00bc\";bonus=x"
+ },
+ {
+ "input": "\u00bd/x",
+ "output": null
+ },
+ {
+ "input": "x/\u00bd",
+ "output": null
+ },
+ {
+ "input": "x/x;\u00bd=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u00bd;bonus=x",
+ "output": "x/x;x=\"\u00bd\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u00bd\";bonus=x",
+ "output": "x/x;x=\"\u00bd\";bonus=x"
+ },
+ {
+ "input": "\u00be/x",
+ "output": null
+ },
+ {
+ "input": "x/\u00be",
+ "output": null
+ },
+ {
+ "input": "x/x;\u00be=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u00be;bonus=x",
+ "output": "x/x;x=\"\u00be\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u00be\";bonus=x",
+ "output": "x/x;x=\"\u00be\";bonus=x"
+ },
+ {
+ "input": "\u00bf/x",
+ "output": null
+ },
+ {
+ "input": "x/\u00bf",
+ "output": null
+ },
+ {
+ "input": "x/x;\u00bf=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u00bf;bonus=x",
+ "output": "x/x;x=\"\u00bf\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u00bf\";bonus=x",
+ "output": "x/x;x=\"\u00bf\";bonus=x"
+ },
+ {
+ "input": "\u00c0/x",
+ "output": null
+ },
+ {
+ "input": "x/\u00c0",
+ "output": null
+ },
+ {
+ "input": "x/x;\u00c0=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u00c0;bonus=x",
+ "output": "x/x;x=\"\u00c0\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u00c0\";bonus=x",
+ "output": "x/x;x=\"\u00c0\";bonus=x"
+ },
+ {
+ "input": "\u00c1/x",
+ "output": null
+ },
+ {
+ "input": "x/\u00c1",
+ "output": null
+ },
+ {
+ "input": "x/x;\u00c1=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u00c1;bonus=x",
+ "output": "x/x;x=\"\u00c1\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u00c1\";bonus=x",
+ "output": "x/x;x=\"\u00c1\";bonus=x"
+ },
+ {
+ "input": "\u00c2/x",
+ "output": null
+ },
+ {
+ "input": "x/\u00c2",
+ "output": null
+ },
+ {
+ "input": "x/x;\u00c2=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u00c2;bonus=x",
+ "output": "x/x;x=\"\u00c2\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u00c2\";bonus=x",
+ "output": "x/x;x=\"\u00c2\";bonus=x"
+ },
+ {
+ "input": "\u00c3/x",
+ "output": null
+ },
+ {
+ "input": "x/\u00c3",
+ "output": null
+ },
+ {
+ "input": "x/x;\u00c3=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u00c3;bonus=x",
+ "output": "x/x;x=\"\u00c3\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u00c3\";bonus=x",
+ "output": "x/x;x=\"\u00c3\";bonus=x"
+ },
+ {
+ "input": "\u00c4/x",
+ "output": null
+ },
+ {
+ "input": "x/\u00c4",
+ "output": null
+ },
+ {
+ "input": "x/x;\u00c4=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u00c4;bonus=x",
+ "output": "x/x;x=\"\u00c4\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u00c4\";bonus=x",
+ "output": "x/x;x=\"\u00c4\";bonus=x"
+ },
+ {
+ "input": "\u00c5/x",
+ "output": null
+ },
+ {
+ "input": "x/\u00c5",
+ "output": null
+ },
+ {
+ "input": "x/x;\u00c5=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u00c5;bonus=x",
+ "output": "x/x;x=\"\u00c5\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u00c5\";bonus=x",
+ "output": "x/x;x=\"\u00c5\";bonus=x"
+ },
+ {
+ "input": "\u00c6/x",
+ "output": null
+ },
+ {
+ "input": "x/\u00c6",
+ "output": null
+ },
+ {
+ "input": "x/x;\u00c6=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u00c6;bonus=x",
+ "output": "x/x;x=\"\u00c6\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u00c6\";bonus=x",
+ "output": "x/x;x=\"\u00c6\";bonus=x"
+ },
+ {
+ "input": "\u00c7/x",
+ "output": null
+ },
+ {
+ "input": "x/\u00c7",
+ "output": null
+ },
+ {
+ "input": "x/x;\u00c7=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u00c7;bonus=x",
+ "output": "x/x;x=\"\u00c7\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u00c7\";bonus=x",
+ "output": "x/x;x=\"\u00c7\";bonus=x"
+ },
+ {
+ "input": "\u00c8/x",
+ "output": null
+ },
+ {
+ "input": "x/\u00c8",
+ "output": null
+ },
+ {
+ "input": "x/x;\u00c8=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u00c8;bonus=x",
+ "output": "x/x;x=\"\u00c8\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u00c8\";bonus=x",
+ "output": "x/x;x=\"\u00c8\";bonus=x"
+ },
+ {
+ "input": "\u00c9/x",
+ "output": null
+ },
+ {
+ "input": "x/\u00c9",
+ "output": null
+ },
+ {
+ "input": "x/x;\u00c9=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u00c9;bonus=x",
+ "output": "x/x;x=\"\u00c9\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u00c9\";bonus=x",
+ "output": "x/x;x=\"\u00c9\";bonus=x"
+ },
+ {
+ "input": "\u00ca/x",
+ "output": null
+ },
+ {
+ "input": "x/\u00ca",
+ "output": null
+ },
+ {
+ "input": "x/x;\u00ca=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u00ca;bonus=x",
+ "output": "x/x;x=\"\u00ca\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u00ca\";bonus=x",
+ "output": "x/x;x=\"\u00ca\";bonus=x"
+ },
+ {
+ "input": "\u00cb/x",
+ "output": null
+ },
+ {
+ "input": "x/\u00cb",
+ "output": null
+ },
+ {
+ "input": "x/x;\u00cb=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u00cb;bonus=x",
+ "output": "x/x;x=\"\u00cb\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u00cb\";bonus=x",
+ "output": "x/x;x=\"\u00cb\";bonus=x"
+ },
+ {
+ "input": "\u00cc/x",
+ "output": null
+ },
+ {
+ "input": "x/\u00cc",
+ "output": null
+ },
+ {
+ "input": "x/x;\u00cc=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u00cc;bonus=x",
+ "output": "x/x;x=\"\u00cc\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u00cc\";bonus=x",
+ "output": "x/x;x=\"\u00cc\";bonus=x"
+ },
+ {
+ "input": "\u00cd/x",
+ "output": null
+ },
+ {
+ "input": "x/\u00cd",
+ "output": null
+ },
+ {
+ "input": "x/x;\u00cd=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u00cd;bonus=x",
+ "output": "x/x;x=\"\u00cd\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u00cd\";bonus=x",
+ "output": "x/x;x=\"\u00cd\";bonus=x"
+ },
+ {
+ "input": "\u00ce/x",
+ "output": null
+ },
+ {
+ "input": "x/\u00ce",
+ "output": null
+ },
+ {
+ "input": "x/x;\u00ce=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u00ce;bonus=x",
+ "output": "x/x;x=\"\u00ce\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u00ce\";bonus=x",
+ "output": "x/x;x=\"\u00ce\";bonus=x"
+ },
+ {
+ "input": "\u00cf/x",
+ "output": null
+ },
+ {
+ "input": "x/\u00cf",
+ "output": null
+ },
+ {
+ "input": "x/x;\u00cf=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u00cf;bonus=x",
+ "output": "x/x;x=\"\u00cf\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u00cf\";bonus=x",
+ "output": "x/x;x=\"\u00cf\";bonus=x"
+ },
+ {
+ "input": "\u00d0/x",
+ "output": null
+ },
+ {
+ "input": "x/\u00d0",
+ "output": null
+ },
+ {
+ "input": "x/x;\u00d0=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u00d0;bonus=x",
+ "output": "x/x;x=\"\u00d0\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u00d0\";bonus=x",
+ "output": "x/x;x=\"\u00d0\";bonus=x"
+ },
+ {
+ "input": "\u00d1/x",
+ "output": null
+ },
+ {
+ "input": "x/\u00d1",
+ "output": null
+ },
+ {
+ "input": "x/x;\u00d1=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u00d1;bonus=x",
+ "output": "x/x;x=\"\u00d1\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u00d1\";bonus=x",
+ "output": "x/x;x=\"\u00d1\";bonus=x"
+ },
+ {
+ "input": "\u00d2/x",
+ "output": null
+ },
+ {
+ "input": "x/\u00d2",
+ "output": null
+ },
+ {
+ "input": "x/x;\u00d2=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u00d2;bonus=x",
+ "output": "x/x;x=\"\u00d2\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u00d2\";bonus=x",
+ "output": "x/x;x=\"\u00d2\";bonus=x"
+ },
+ {
+ "input": "\u00d3/x",
+ "output": null
+ },
+ {
+ "input": "x/\u00d3",
+ "output": null
+ },
+ {
+ "input": "x/x;\u00d3=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u00d3;bonus=x",
+ "output": "x/x;x=\"\u00d3\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u00d3\";bonus=x",
+ "output": "x/x;x=\"\u00d3\";bonus=x"
+ },
+ {
+ "input": "\u00d4/x",
+ "output": null
+ },
+ {
+ "input": "x/\u00d4",
+ "output": null
+ },
+ {
+ "input": "x/x;\u00d4=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u00d4;bonus=x",
+ "output": "x/x;x=\"\u00d4\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u00d4\";bonus=x",
+ "output": "x/x;x=\"\u00d4\";bonus=x"
+ },
+ {
+ "input": "\u00d5/x",
+ "output": null
+ },
+ {
+ "input": "x/\u00d5",
+ "output": null
+ },
+ {
+ "input": "x/x;\u00d5=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u00d5;bonus=x",
+ "output": "x/x;x=\"\u00d5\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u00d5\";bonus=x",
+ "output": "x/x;x=\"\u00d5\";bonus=x"
+ },
+ {
+ "input": "\u00d6/x",
+ "output": null
+ },
+ {
+ "input": "x/\u00d6",
+ "output": null
+ },
+ {
+ "input": "x/x;\u00d6=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u00d6;bonus=x",
+ "output": "x/x;x=\"\u00d6\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u00d6\";bonus=x",
+ "output": "x/x;x=\"\u00d6\";bonus=x"
+ },
+ {
+ "input": "\u00d7/x",
+ "output": null
+ },
+ {
+ "input": "x/\u00d7",
+ "output": null
+ },
+ {
+ "input": "x/x;\u00d7=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u00d7;bonus=x",
+ "output": "x/x;x=\"\u00d7\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u00d7\";bonus=x",
+ "output": "x/x;x=\"\u00d7\";bonus=x"
+ },
+ {
+ "input": "\u00d8/x",
+ "output": null
+ },
+ {
+ "input": "x/\u00d8",
+ "output": null
+ },
+ {
+ "input": "x/x;\u00d8=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u00d8;bonus=x",
+ "output": "x/x;x=\"\u00d8\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u00d8\";bonus=x",
+ "output": "x/x;x=\"\u00d8\";bonus=x"
+ },
+ {
+ "input": "\u00d9/x",
+ "output": null
+ },
+ {
+ "input": "x/\u00d9",
+ "output": null
+ },
+ {
+ "input": "x/x;\u00d9=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u00d9;bonus=x",
+ "output": "x/x;x=\"\u00d9\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u00d9\";bonus=x",
+ "output": "x/x;x=\"\u00d9\";bonus=x"
+ },
+ {
+ "input": "\u00da/x",
+ "output": null
+ },
+ {
+ "input": "x/\u00da",
+ "output": null
+ },
+ {
+ "input": "x/x;\u00da=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u00da;bonus=x",
+ "output": "x/x;x=\"\u00da\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u00da\";bonus=x",
+ "output": "x/x;x=\"\u00da\";bonus=x"
+ },
+ {
+ "input": "\u00db/x",
+ "output": null
+ },
+ {
+ "input": "x/\u00db",
+ "output": null
+ },
+ {
+ "input": "x/x;\u00db=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u00db;bonus=x",
+ "output": "x/x;x=\"\u00db\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u00db\";bonus=x",
+ "output": "x/x;x=\"\u00db\";bonus=x"
+ },
+ {
+ "input": "\u00dc/x",
+ "output": null
+ },
+ {
+ "input": "x/\u00dc",
+ "output": null
+ },
+ {
+ "input": "x/x;\u00dc=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u00dc;bonus=x",
+ "output": "x/x;x=\"\u00dc\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u00dc\";bonus=x",
+ "output": "x/x;x=\"\u00dc\";bonus=x"
+ },
+ {
+ "input": "\u00dd/x",
+ "output": null
+ },
+ {
+ "input": "x/\u00dd",
+ "output": null
+ },
+ {
+ "input": "x/x;\u00dd=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u00dd;bonus=x",
+ "output": "x/x;x=\"\u00dd\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u00dd\";bonus=x",
+ "output": "x/x;x=\"\u00dd\";bonus=x"
+ },
+ {
+ "input": "\u00de/x",
+ "output": null
+ },
+ {
+ "input": "x/\u00de",
+ "output": null
+ },
+ {
+ "input": "x/x;\u00de=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u00de;bonus=x",
+ "output": "x/x;x=\"\u00de\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u00de\";bonus=x",
+ "output": "x/x;x=\"\u00de\";bonus=x"
+ },
+ {
+ "input": "\u00df/x",
+ "output": null
+ },
+ {
+ "input": "x/\u00df",
+ "output": null
+ },
+ {
+ "input": "x/x;\u00df=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u00df;bonus=x",
+ "output": "x/x;x=\"\u00df\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u00df\";bonus=x",
+ "output": "x/x;x=\"\u00df\";bonus=x"
+ },
+ {
+ "input": "\u00e0/x",
+ "output": null
+ },
+ {
+ "input": "x/\u00e0",
+ "output": null
+ },
+ {
+ "input": "x/x;\u00e0=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u00e0;bonus=x",
+ "output": "x/x;x=\"\u00e0\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u00e0\";bonus=x",
+ "output": "x/x;x=\"\u00e0\";bonus=x"
+ },
+ {
+ "input": "\u00e1/x",
+ "output": null
+ },
+ {
+ "input": "x/\u00e1",
+ "output": null
+ },
+ {
+ "input": "x/x;\u00e1=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u00e1;bonus=x",
+ "output": "x/x;x=\"\u00e1\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u00e1\";bonus=x",
+ "output": "x/x;x=\"\u00e1\";bonus=x"
+ },
+ {
+ "input": "\u00e2/x",
+ "output": null
+ },
+ {
+ "input": "x/\u00e2",
+ "output": null
+ },
+ {
+ "input": "x/x;\u00e2=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u00e2;bonus=x",
+ "output": "x/x;x=\"\u00e2\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u00e2\";bonus=x",
+ "output": "x/x;x=\"\u00e2\";bonus=x"
+ },
+ {
+ "input": "\u00e3/x",
+ "output": null
+ },
+ {
+ "input": "x/\u00e3",
+ "output": null
+ },
+ {
+ "input": "x/x;\u00e3=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u00e3;bonus=x",
+ "output": "x/x;x=\"\u00e3\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u00e3\";bonus=x",
+ "output": "x/x;x=\"\u00e3\";bonus=x"
+ },
+ {
+ "input": "\u00e4/x",
+ "output": null
+ },
+ {
+ "input": "x/\u00e4",
+ "output": null
+ },
+ {
+ "input": "x/x;\u00e4=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u00e4;bonus=x",
+ "output": "x/x;x=\"\u00e4\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u00e4\";bonus=x",
+ "output": "x/x;x=\"\u00e4\";bonus=x"
+ },
+ {
+ "input": "\u00e5/x",
+ "output": null
+ },
+ {
+ "input": "x/\u00e5",
+ "output": null
+ },
+ {
+ "input": "x/x;\u00e5=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u00e5;bonus=x",
+ "output": "x/x;x=\"\u00e5\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u00e5\";bonus=x",
+ "output": "x/x;x=\"\u00e5\";bonus=x"
+ },
+ {
+ "input": "\u00e6/x",
+ "output": null
+ },
+ {
+ "input": "x/\u00e6",
+ "output": null
+ },
+ {
+ "input": "x/x;\u00e6=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u00e6;bonus=x",
+ "output": "x/x;x=\"\u00e6\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u00e6\";bonus=x",
+ "output": "x/x;x=\"\u00e6\";bonus=x"
+ },
+ {
+ "input": "\u00e7/x",
+ "output": null
+ },
+ {
+ "input": "x/\u00e7",
+ "output": null
+ },
+ {
+ "input": "x/x;\u00e7=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u00e7;bonus=x",
+ "output": "x/x;x=\"\u00e7\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u00e7\";bonus=x",
+ "output": "x/x;x=\"\u00e7\";bonus=x"
+ },
+ {
+ "input": "\u00e8/x",
+ "output": null
+ },
+ {
+ "input": "x/\u00e8",
+ "output": null
+ },
+ {
+ "input": "x/x;\u00e8=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u00e8;bonus=x",
+ "output": "x/x;x=\"\u00e8\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u00e8\";bonus=x",
+ "output": "x/x;x=\"\u00e8\";bonus=x"
+ },
+ {
+ "input": "\u00e9/x",
+ "output": null
+ },
+ {
+ "input": "x/\u00e9",
+ "output": null
+ },
+ {
+ "input": "x/x;\u00e9=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u00e9;bonus=x",
+ "output": "x/x;x=\"\u00e9\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u00e9\";bonus=x",
+ "output": "x/x;x=\"\u00e9\";bonus=x"
+ },
+ {
+ "input": "\u00ea/x",
+ "output": null
+ },
+ {
+ "input": "x/\u00ea",
+ "output": null
+ },
+ {
+ "input": "x/x;\u00ea=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u00ea;bonus=x",
+ "output": "x/x;x=\"\u00ea\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u00ea\";bonus=x",
+ "output": "x/x;x=\"\u00ea\";bonus=x"
+ },
+ {
+ "input": "\u00eb/x",
+ "output": null
+ },
+ {
+ "input": "x/\u00eb",
+ "output": null
+ },
+ {
+ "input": "x/x;\u00eb=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u00eb;bonus=x",
+ "output": "x/x;x=\"\u00eb\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u00eb\";bonus=x",
+ "output": "x/x;x=\"\u00eb\";bonus=x"
+ },
+ {
+ "input": "\u00ec/x",
+ "output": null
+ },
+ {
+ "input": "x/\u00ec",
+ "output": null
+ },
+ {
+ "input": "x/x;\u00ec=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u00ec;bonus=x",
+ "output": "x/x;x=\"\u00ec\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u00ec\";bonus=x",
+ "output": "x/x;x=\"\u00ec\";bonus=x"
+ },
+ {
+ "input": "\u00ed/x",
+ "output": null
+ },
+ {
+ "input": "x/\u00ed",
+ "output": null
+ },
+ {
+ "input": "x/x;\u00ed=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u00ed;bonus=x",
+ "output": "x/x;x=\"\u00ed\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u00ed\";bonus=x",
+ "output": "x/x;x=\"\u00ed\";bonus=x"
+ },
+ {
+ "input": "\u00ee/x",
+ "output": null
+ },
+ {
+ "input": "x/\u00ee",
+ "output": null
+ },
+ {
+ "input": "x/x;\u00ee=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u00ee;bonus=x",
+ "output": "x/x;x=\"\u00ee\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u00ee\";bonus=x",
+ "output": "x/x;x=\"\u00ee\";bonus=x"
+ },
+ {
+ "input": "\u00ef/x",
+ "output": null
+ },
+ {
+ "input": "x/\u00ef",
+ "output": null
+ },
+ {
+ "input": "x/x;\u00ef=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u00ef;bonus=x",
+ "output": "x/x;x=\"\u00ef\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u00ef\";bonus=x",
+ "output": "x/x;x=\"\u00ef\";bonus=x"
+ },
+ {
+ "input": "\u00f0/x",
+ "output": null
+ },
+ {
+ "input": "x/\u00f0",
+ "output": null
+ },
+ {
+ "input": "x/x;\u00f0=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u00f0;bonus=x",
+ "output": "x/x;x=\"\u00f0\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u00f0\";bonus=x",
+ "output": "x/x;x=\"\u00f0\";bonus=x"
+ },
+ {
+ "input": "\u00f1/x",
+ "output": null
+ },
+ {
+ "input": "x/\u00f1",
+ "output": null
+ },
+ {
+ "input": "x/x;\u00f1=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u00f1;bonus=x",
+ "output": "x/x;x=\"\u00f1\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u00f1\";bonus=x",
+ "output": "x/x;x=\"\u00f1\";bonus=x"
+ },
+ {
+ "input": "\u00f2/x",
+ "output": null
+ },
+ {
+ "input": "x/\u00f2",
+ "output": null
+ },
+ {
+ "input": "x/x;\u00f2=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u00f2;bonus=x",
+ "output": "x/x;x=\"\u00f2\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u00f2\";bonus=x",
+ "output": "x/x;x=\"\u00f2\";bonus=x"
+ },
+ {
+ "input": "\u00f3/x",
+ "output": null
+ },
+ {
+ "input": "x/\u00f3",
+ "output": null
+ },
+ {
+ "input": "x/x;\u00f3=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u00f3;bonus=x",
+ "output": "x/x;x=\"\u00f3\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u00f3\";bonus=x",
+ "output": "x/x;x=\"\u00f3\";bonus=x"
+ },
+ {
+ "input": "\u00f4/x",
+ "output": null
+ },
+ {
+ "input": "x/\u00f4",
+ "output": null
+ },
+ {
+ "input": "x/x;\u00f4=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u00f4;bonus=x",
+ "output": "x/x;x=\"\u00f4\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u00f4\";bonus=x",
+ "output": "x/x;x=\"\u00f4\";bonus=x"
+ },
+ {
+ "input": "\u00f5/x",
+ "output": null
+ },
+ {
+ "input": "x/\u00f5",
+ "output": null
+ },
+ {
+ "input": "x/x;\u00f5=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u00f5;bonus=x",
+ "output": "x/x;x=\"\u00f5\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u00f5\";bonus=x",
+ "output": "x/x;x=\"\u00f5\";bonus=x"
+ },
+ {
+ "input": "\u00f6/x",
+ "output": null
+ },
+ {
+ "input": "x/\u00f6",
+ "output": null
+ },
+ {
+ "input": "x/x;\u00f6=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u00f6;bonus=x",
+ "output": "x/x;x=\"\u00f6\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u00f6\";bonus=x",
+ "output": "x/x;x=\"\u00f6\";bonus=x"
+ },
+ {
+ "input": "\u00f7/x",
+ "output": null
+ },
+ {
+ "input": "x/\u00f7",
+ "output": null
+ },
+ {
+ "input": "x/x;\u00f7=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u00f7;bonus=x",
+ "output": "x/x;x=\"\u00f7\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u00f7\";bonus=x",
+ "output": "x/x;x=\"\u00f7\";bonus=x"
+ },
+ {
+ "input": "\u00f8/x",
+ "output": null
+ },
+ {
+ "input": "x/\u00f8",
+ "output": null
+ },
+ {
+ "input": "x/x;\u00f8=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u00f8;bonus=x",
+ "output": "x/x;x=\"\u00f8\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u00f8\";bonus=x",
+ "output": "x/x;x=\"\u00f8\";bonus=x"
+ },
+ {
+ "input": "\u00f9/x",
+ "output": null
+ },
+ {
+ "input": "x/\u00f9",
+ "output": null
+ },
+ {
+ "input": "x/x;\u00f9=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u00f9;bonus=x",
+ "output": "x/x;x=\"\u00f9\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u00f9\";bonus=x",
+ "output": "x/x;x=\"\u00f9\";bonus=x"
+ },
+ {
+ "input": "\u00fa/x",
+ "output": null
+ },
+ {
+ "input": "x/\u00fa",
+ "output": null
+ },
+ {
+ "input": "x/x;\u00fa=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u00fa;bonus=x",
+ "output": "x/x;x=\"\u00fa\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u00fa\";bonus=x",
+ "output": "x/x;x=\"\u00fa\";bonus=x"
+ },
+ {
+ "input": "\u00fb/x",
+ "output": null
+ },
+ {
+ "input": "x/\u00fb",
+ "output": null
+ },
+ {
+ "input": "x/x;\u00fb=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u00fb;bonus=x",
+ "output": "x/x;x=\"\u00fb\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u00fb\";bonus=x",
+ "output": "x/x;x=\"\u00fb\";bonus=x"
+ },
+ {
+ "input": "\u00fc/x",
+ "output": null
+ },
+ {
+ "input": "x/\u00fc",
+ "output": null
+ },
+ {
+ "input": "x/x;\u00fc=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u00fc;bonus=x",
+ "output": "x/x;x=\"\u00fc\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u00fc\";bonus=x",
+ "output": "x/x;x=\"\u00fc\";bonus=x"
+ },
+ {
+ "input": "\u00fd/x",
+ "output": null
+ },
+ {
+ "input": "x/\u00fd",
+ "output": null
+ },
+ {
+ "input": "x/x;\u00fd=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u00fd;bonus=x",
+ "output": "x/x;x=\"\u00fd\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u00fd\";bonus=x",
+ "output": "x/x;x=\"\u00fd\";bonus=x"
+ },
+ {
+ "input": "\u00fe/x",
+ "output": null
+ },
+ {
+ "input": "x/\u00fe",
+ "output": null
+ },
+ {
+ "input": "x/x;\u00fe=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u00fe;bonus=x",
+ "output": "x/x;x=\"\u00fe\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u00fe\";bonus=x",
+ "output": "x/x;x=\"\u00fe\";bonus=x"
+ },
+ {
+ "input": "\u00ff/x",
+ "output": null
+ },
+ {
+ "input": "x/\u00ff",
+ "output": null
+ },
+ {
+ "input": "x/x;\u00ff=x;bonus=x",
+ "output": "x/x;bonus=x"
+ },
+ {
+ "input": "x/x;x=\u00ff;bonus=x",
+ "output": "x/x;x=\"\u00ff\";bonus=x"
+ },
+ {
+ "input": "x/x;x=\"\u00ff\";bonus=x",
+ "output": "x/x;x=\"\u00ff\";bonus=x"
+ }
+]
diff --git a/test/wpt/tests/mimesniff/mime-types/resources/generated-mime-types.py b/test/wpt/tests/mimesniff/mime-types/resources/generated-mime-types.py
new file mode 100644
index 0000000..5d0e26c
--- /dev/null
+++ b/test/wpt/tests/mimesniff/mime-types/resources/generated-mime-types.py
@@ -0,0 +1,48 @@
+import json
+import os
+
+here = os.path.dirname(__file__)
+
+def isHTTPTokenCodePoint(cp):
+ if cp in (0x21, 0x23, 0x24, 0x25, 0x26, 0x27, 0x2A, 0x2B, 0x2D, 0x2E, 0x5E, 0x5F, 0x60, 0x7C, 0x7E) or (cp >= 0x30 and cp <= 0x39) or (cp >= 0x41 and cp <= 0x5A) or (cp >= 0x61 and cp <= 0x7A):
+ return True
+ else:
+ return False
+
+def isHTTPQuotedStringTokenCodePoint(cp):
+ if cp == 0x09 or (cp >= 0x20 and cp <= 0x7E) or (cp >= 0x80 and cp <= 0xFF):
+ return True
+ else:
+ return False
+
+tests = []
+
+for cp in range(0x00, 0x100):
+ if isHTTPTokenCodePoint(cp):
+ continue
+ for scenario in ("type", "subtype", "name", "value"):
+ if scenario == "type" or scenario == "subtype":
+ if cp == 0x2F: # /
+ continue
+ if scenario == "type":
+ test = chr(cp) + "/x"
+ else:
+ test = "x/" + chr(cp)
+ tests.append({"input": test, "output": None})
+ elif scenario == "name":
+ if cp == 0x3B or cp == 0x3D: # ; =
+ continue
+ tests.append({"input": "x/x;" + chr(cp) + "=x;bonus=x", "output": "x/x;bonus=x"})
+ elif scenario == "value":
+ if cp == 0x09 or cp == 0x20 or cp == 0x22 or cp == 0x3B or cp == 0x5C: # TAB SP " ; \
+ continue
+ if isHTTPQuotedStringTokenCodePoint(cp):
+ testOutput = "x/x;x=\"" + chr(cp) + "\";bonus=x"
+ else:
+ testOutput = "x/x;bonus=x"
+ tests.append({"input": "x/x;x=" + chr(cp) + ";bonus=x", "output": testOutput})
+ tests.append({"input": "x/x;x=\"" + chr(cp) + "\";bonus=x", "output": testOutput})
+
+handle = open(os.path.join(here, "generated-mime-types.json"), "w")
+handle.write(json.dumps(tests, indent=2, separators=(',', ': ')))
+handle.write("\n")
diff --git a/test/wpt/tests/mimesniff/mime-types/resources/mime-charset.py b/test/wpt/tests/mimesniff/mime-types/resources/mime-charset.py
new file mode 100644
index 0000000..bcc3dc3
--- /dev/null
+++ b/test/wpt/tests/mimesniff/mime-types/resources/mime-charset.py
@@ -0,0 +1,19 @@
+from wptserve.utils import isomorphic_encode
+
+def main(request, response):
+ content = b"<meta charset=utf-8>\n<script>document.write(document.characterSet)</script>"
+
+ # This uses the following rather than
+ # response.headers.set("Content-Type", request.GET.first("type"));
+ # response.content = content
+ # to work around https://github.com/web-platform-tests/wpt/issues/8372.
+
+ response.add_required_headers = False
+ output = b"HTTP/1.1 200 OK\r\n"
+ output += b"Content-Length: " + isomorphic_encode(str(len(content))) + b"\r\n"
+ output += b"Content-Type: " + request.GET.first(b"type") + b"\r\n"
+ output += b"Connection: close\r\n"
+ output += b"\r\n"
+ output += content
+ response.writer.write(output)
+ response.close_connection = True
diff --git a/test/wpt/tests/mimesniff/mime-types/resources/mime-groups.json b/test/wpt/tests/mimesniff/mime-types/resources/mime-groups.json
new file mode 100644
index 0000000..1b5d7d5
--- /dev/null
+++ b/test/wpt/tests/mimesniff/mime-types/resources/mime-groups.json
@@ -0,0 +1,159 @@
+[
+ {"input": "x/x", "groups": []},
+ {"input": "image/x", "groups": ["image"]},
+ {"input": "audio/x", "groups": ["audio or video"]},
+ {"input": "video/x", "groups": ["audio or video"]},
+ {"input": "application/ogg", "groups": ["audio or video"]},
+ {"input": "application/ogg;x=x", "groups": ["audio or video"]},
+ {"input": "font/x", "groups": ["font"]},
+ {"input": "application/font-cff", "groups": ["font"]},
+ {"input": "application/font-cff;x=x", "groups": ["font"]},
+ {"input": "application/font-off", "groups": ["font"]},
+ {"input": "application/font-off;x=x", "groups": ["font"]},
+ {"input": "application/font-sfnt", "groups": ["font"]},
+ {"input": "application/font-sfnt;x=x", "groups": ["font"]},
+ {"input": "application/font-ttf", "groups": ["font"]},
+ {"input": "application/font-ttf;x=x", "groups": ["font"]},
+ {"input": "application/font-woff", "groups": ["font"]},
+ {"input": "application/font-woff;x=x", "groups": ["font"]},
+ {"input": "application/vnd.ms-fontobject", "groups": ["font"]},
+ {"input": "application/vnd.ms-fontobject;x=x", "groups": ["font"]},
+ {"input": "application/vnd.ms-opentype", "groups": ["font"]},
+ {"input": "application/vnd.ms-opentype;x=x", "groups": ["font"]},
+ {"input": "x/x+zip", "groups": ["ZIP-based"]},
+ {"input": "x/x+zip;x=x", "groups": ["ZIP-based"]},
+ {"input": "x/+zip", "groups": ["ZIP-based"]},
+ {"input": "x/+zip;x=x", "groups": ["ZIP-based"]},
+ "application/zip also matches the archive group",
+ {"input": "application/zip", "groups": ["ZIP-based", "archive"]},
+ {"input": "application/zip;x=x", "groups": ["ZIP-based", "archive"]},
+ {"input": "application/x-rar-compressed", "groups": ["archive"]},
+ {"input": "application/x-rar-compressed;x=x", "groups": ["archive"]},
+ {"input": "application/x-gzip", "groups": ["archive"]},
+ {"input": "application/x-gzip;x=x", "groups": ["archive"]},
+ "XML is also scriptable",
+ {"input": "x/x+xml", "groups": ["XML", "scriptable"]},
+ {"input": "x/x+xml;x=x", "groups": ["XML", "scriptable"]},
+ {"input": "x/+xml", "groups": ["XML", "scriptable"]},
+ {"input": "x/+xml;x=x", "groups": ["XML", "scriptable"]},
+ {"input": "application/xml", "groups": ["XML", "scriptable"]},
+ {"input": "application/xml;x=x", "groups": ["XML", "scriptable"]},
+ {"input": "text/xml", "groups": ["XML", "scriptable"]},
+ {"input": "text/xml;x=x", "groups": ["XML", "scriptable"]},
+ "HTML is scriptable",
+ {"input": "text/html", "groups": ["HTML", "scriptable"]},
+ {"input": "text/html;x=x", "groups": ["HTML", "scriptable"]},
+ "PDF is scriptable",
+ {"input": "application/pdf", "groups": ["scriptable"]},
+ {"input": "application/pdf;x=x", "groups": ["scriptable"]},
+ {"input": "application/ecmascript", "groups": ["JavaScript"]},
+ {"input": "application/ecmascript;x=x", "groups": ["JavaScript"]},
+ {"input": "application/javascript", "groups": ["JavaScript"]},
+ {"input": "application/javascript;x=x", "groups": ["JavaScript"]},
+ {"input": "application/x-ecmascript", "groups": ["JavaScript"]},
+ {"input": "application/x-ecmascript;x=x", "groups": ["JavaScript"]},
+ {"input": "application/x-javascript", "groups": ["JavaScript"]},
+ {"input": "application/x-javascript;x=x", "groups": ["JavaScript"]},
+ {"input": "text/ecmascript", "groups": ["JavaScript"]},
+ {"input": "text/ecmascript;x=x", "groups": ["JavaScript"]},
+ {"input": "text/javascript", "groups": ["JavaScript"]},
+ {"input": "text/javascript;x=x", "groups": ["JavaScript"]},
+ {"input": "text/javascript1.0", "groups": ["JavaScript"]},
+ {"input": "text/javascript1.0;x=x", "groups": ["JavaScript"]},
+ {"input": "text/javascript1.1", "groups": ["JavaScript"]},
+ {"input": "text/javascript1.1;x=x", "groups": ["JavaScript"]},
+ {"input": "text/javascript1.2", "groups": ["JavaScript"]},
+ {"input": "text/javascript1.2;x=x", "groups": ["JavaScript"]},
+ {"input": "text/javascript1.3", "groups": ["JavaScript"]},
+ {"input": "text/javascript1.3;x=x", "groups": ["JavaScript"]},
+ {"input": "text/javascript1.4", "groups": ["JavaScript"]},
+ {"input": "text/javascript1.4;x=x", "groups": ["JavaScript"]},
+ {"input": "text/javascript1.5", "groups": ["JavaScript"]},
+ {"input": "text/javascript1.5;x=x", "groups": ["JavaScript"]},
+ {"input": "text/jscript", "groups": ["JavaScript"]},
+ {"input": "text/jscript;x=x", "groups": ["JavaScript"]},
+ {"input": "text/livescript", "groups": ["JavaScript"]},
+ {"input": "text/livescript;x=x", "groups": ["JavaScript"]},
+ {"input": "text/x-ecmascript", "groups": ["JavaScript"]},
+ {"input": "text/x-ecmascript;x=x", "groups": ["JavaScript"]},
+ {"input": "text/x-javascript", "groups": ["JavaScript"]},
+ {"input": "text/x-javascript;x=x", "groups": ["JavaScript"]},
+ {"input": "x/x+json", "groups": ["JSON"]},
+ {"input": "x/x+json;x=x", "groups": ["JSON"]},
+ {"input": "x/+json", "groups": ["JSON"]},
+ {"input": "x/+json;x=x", "groups": ["JSON"]},
+ {"input": "application/json", "groups": ["JSON"]},
+ {"input": "application/json;x=x", "groups": ["JSON"]},
+ {"input": "text/json", "groups": ["JSON"]},
+ {"input": "text/json;x=x", "groups": ["JSON"]},
+ {"input": "x/x;type=image", "groups": []},
+ {"input": "x/x;type=audio", "groups": []},
+ {"input": "x/x;type=video", "groups": []},
+ {"input": "x/x;type=font", "groups": []},
+ {"input": "ximage/x", "groups": []},
+ {"input": "xaudio/x", "groups": []},
+ {"input": "xvideo/x", "groups": []},
+ {"input": "xfont/x", "groups": []},
+ {"input": "imagex/x", "groups": []},
+ {"input": "audiox/x", "groups": []},
+ {"input": "videox/x", "groups": []},
+ {"input": "fontx/x", "groups": []},
+ {"input": "x/image", "groups": []},
+ {"input": "x/png", "groups": []},
+ {"input": "x/audio", "groups": []},
+ {"input": "x/video", "groups": []},
+ {"input": "x/ogg", "groups": []},
+ {"input": "x/font", "groups": []},
+ {"input": "x/woff", "groups": []},
+ {"input": "x/font-ttf", "groups": []},
+ {"input": "x/x;x=\"application/font-ttf\"", "groups": []},
+ {"input": "x/zip", "groups": []},
+ {"input": "x/x-gzip", "groups": []},
+ {"input": "x/x-rar-compressed", "groups": []},
+ {"input": "x/x;x=\"application/x-gip\"", "groups": []},
+ {"input": "x+zip/x", "groups": []},
+ {"input": "x/x;x=x+zip", "groups": []},
+ {"input": "x/x;subtype=x+zip", "groups": []},
+ {"input": "x/xml", "groups": []},
+ {"input": "x/x;x=\"application/xml\"", "groups": []},
+ {"input": "x+xml/x", "groups": []},
+ {"input": "x/x;x=x+xml", "groups": []},
+ {"input": "x/x;subtype=x+xml", "groups": []},
+ {"input": "x/pdf", "groups": []},
+ {"input": "x/html", "groups": []},
+ {"input": "x/ecmascript", "groups": []},
+ {"input": "x/javascript", "groups": []},
+ {"input": "x/x-ecmascript", "groups": []},
+ {"input": "x/x-javascript", "groups": []},
+ {"input": "x/jscript", "groups": []},
+ {"input": "x/livescript", "groups": []},
+ {"input": "x/javascript1.0", "groups": []},
+ {"input": "x/javascript1.1", "groups": []},
+ {"input": "x/javascript1.2", "groups": []},
+ {"input": "x/javascript1.3", "groups": []},
+ {"input": "x/javascript1.4", "groups": []},
+ {"input": "x/javascript1.5", "groups": []},
+ {"input": "x/x;x=\"application/javascript\"", "groups": []},
+ {"input": "x/json", "groups": []},
+ {"input": "x/x;x=\"application/json\"", "groups": []},
+ {"input": "x+json/x", "groups": []},
+ {"input": "x/x;x=x+json", "groups": []},
+ {"input": "x/x;subtype=x+json", "groups": []},
+ {"input": "image/png", "groups": ["image"]},
+ {"input": "audio/mp4", "groups": ["audio or video"]},
+ {"input": "video/mpeg", "groups": ["audio or video"]},
+ {"input": "font/woff", "groups": ["font"]},
+ "SVG is both image and XML, thus also scriptable",
+ {"input": "image/svg+xml", "groups": ["image", "XML", "scriptable"]},
+ "Hypothetical SVG font type",
+ {"input": "font/svg+xml", "groups": ["font", "XML", "scriptable"]},
+ "XHTML is not considered HTML",
+ {"input": "application/xhtml+xml", "groups": ["XML", "scriptable"]},
+ "Subtype longer than 255",
+ {"input": "x/01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789+zip",
+ "groups": ["ZIP-based"]},
+ {"input": "x/01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789+xml",
+ "groups": ["XML", "scriptable"]},
+ {"input": "x/01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789+json",
+ "groups": ["JSON"]}
+]
diff --git a/test/wpt/tests/mimesniff/mime-types/resources/mime-types.json b/test/wpt/tests/mimesniff/mime-types/resources/mime-types.json
new file mode 100644
index 0000000..1d0ec64
--- /dev/null
+++ b/test/wpt/tests/mimesniff/mime-types/resources/mime-types.json
@@ -0,0 +1,397 @@
+[
+ "Basics",
+ {
+ "input": "text/html;charset=gbk",
+ "output": "text/html;charset=gbk",
+ "navigable": true,
+ "encoding": "GBK"
+ },
+ {
+ "input": "TEXT/HTML;CHARSET=GBK",
+ "output": "text/html;charset=GBK",
+ "navigable": true,
+ "encoding": "GBK"
+ },
+ "Legacy comment syntax",
+ {
+ "input": "text/html;charset=gbk(",
+ "output": "text/html;charset=\"gbk(\"",
+ "navigable": true,
+ "encoding": null
+ },
+ {
+ "input": "text/html;x=(;charset=gbk",
+ "output": "text/html;x=\"(\";charset=gbk",
+ "navigable": true,
+ "encoding": "GBK"
+ },
+ "Duplicate parameter",
+ {
+ "input": "text/html;charset=gbk;charset=windows-1255",
+ "output": "text/html;charset=gbk",
+ "navigable": true,
+ "encoding": "GBK"
+ },
+ {
+ "input": "text/html;charset=();charset=GBK",
+ "output": "text/html;charset=\"()\"",
+ "navigable": true,
+ "encoding": null
+ },
+ "Spaces",
+ {
+ "input": "text/html;charset =gbk",
+ "output": "text/html",
+ "navigable": true,
+ "encoding": null
+ },
+ {
+ "input": "text/html ;charset=gbk",
+ "output": "text/html;charset=gbk",
+ "navigable": true,
+ "encoding": "GBK"
+ },
+ {
+ "input": "text/html; charset=gbk",
+ "output": "text/html;charset=gbk",
+ "navigable": true,
+ "encoding": "GBK"
+ },
+ {
+ "input": "text/html;charset= gbk",
+ "output": "text/html;charset=\" gbk\"",
+ "navigable": true,
+ "encoding": "GBK"
+ },
+ {
+ "input": "text/html;charset= \"gbk\"",
+ "output": "text/html;charset=\" \\\"gbk\\\"\"",
+ "navigable": true,
+ "encoding": null
+ },
+ "0x0B and 0x0C",
+ {
+ "input": "text/html;charset=\u000Bgbk",
+ "output": "text/html",
+ "navigable": true,
+ "encoding": null
+ },
+ {
+ "input": "text/html;charset=\u000Cgbk",
+ "output": "text/html",
+ "navigable": true,
+ "encoding": null
+ },
+ {
+ "input": "text/html;\u000Bcharset=gbk",
+ "output": "text/html",
+ "navigable": true,
+ "encoding": null
+ },
+ {
+ "input": "text/html;\u000Ccharset=gbk",
+ "output": "text/html",
+ "navigable": true,
+ "encoding": null
+ },
+ "Single quotes are a token, not a delimiter",
+ {
+ "input": "text/html;charset='gbk'",
+ "output": "text/html;charset='gbk'",
+ "navigable": true,
+ "encoding": null
+ },
+ {
+ "input": "text/html;charset='gbk",
+ "output": "text/html;charset='gbk",
+ "navigable": true,
+ "encoding": null
+ },
+ {
+ "input": "text/html;charset=gbk'",
+ "output": "text/html;charset=gbk'",
+ "navigable": true,
+ "encoding": null
+ },
+ {
+ "input": "text/html;charset=';charset=GBK",
+ "output": "text/html;charset='",
+ "navigable": true,
+ "encoding": null
+ },
+ "Invalid parameters",
+ {
+ "input": "text/html;test;charset=gbk",
+ "output": "text/html;charset=gbk",
+ "navigable": true,
+ "encoding": "GBK"
+ },
+ {
+ "input": "text/html;test=;charset=gbk",
+ "output": "text/html;charset=gbk",
+ "navigable": true,
+ "encoding": "GBK"
+ },
+ {
+ "input": "text/html;';charset=gbk",
+ "output": "text/html;charset=gbk",
+ "navigable": true,
+ "encoding": "GBK"
+ },
+ {
+ "input": "text/html;\";charset=gbk",
+ "output": "text/html;charset=gbk",
+ "navigable": true,
+ "encoding": "GBK"
+ },
+ {
+ "input": "text/html ; ; charset=gbk",
+ "output": "text/html;charset=gbk",
+ "navigable": true,
+ "encoding": "GBK"
+ },
+ {
+ "input": "text/html;;;;charset=gbk",
+ "output": "text/html;charset=gbk",
+ "navigable": true,
+ "encoding": "GBK"
+ },
+ {
+ "input": "text/html;charset= \"\u007F;charset=GBK",
+ "output": "text/html;charset=GBK",
+ "navigable": true,
+ "encoding": "GBK"
+ },
+ {
+ "input": "text/html;charset=\"\u007F;charset=foo\";charset=GBK",
+ "output": "text/html;charset=GBK",
+ "navigable": true,
+ "encoding": "GBK"
+ },
+ "Double quotes",
+ {
+ "input": "text/html;charset=\"gbk\"",
+ "output": "text/html;charset=gbk",
+ "navigable": true,
+ "encoding": "GBK"
+ },
+ {
+ "input": "text/html;charset=\"gbk",
+ "output": "text/html;charset=gbk",
+ "navigable": true,
+ "encoding": "GBK"
+ },
+ {
+ "input": "text/html;charset=gbk\"",
+ "output": "text/html;charset=\"gbk\\\"\"",
+ "navigable": true,
+ "encoding": null
+ },
+ {
+ "input": "text/html;charset=\" gbk\"",
+ "output": "text/html;charset=\" gbk\"",
+ "navigable": true,
+ "encoding": "GBK"
+ },
+ {
+ "input": "text/html;charset=\"gbk \"",
+ "output": "text/html;charset=\"gbk \"",
+ "navigable": true,
+ "encoding": "GBK"
+ },
+ {
+ "input": "text/html;charset=\"\\ gbk\"",
+ "output": "text/html;charset=\" gbk\"",
+ "navigable": true,
+ "encoding": "GBK"
+ },
+ {
+ "input": "text/html;charset=\"\\g\\b\\k\"",
+ "output": "text/html;charset=gbk",
+ "navigable": true,
+ "encoding": "GBK"
+ },
+ {
+ "input": "text/html;charset=\"gbk\"x",
+ "output": "text/html;charset=gbk",
+ "navigable": true,
+ "encoding": "GBK"
+ },
+ {
+ "input": "text/html;charset=\"\";charset=GBK",
+ "output": "text/html;charset=\"\"",
+ "navigable": true,
+ "encoding": null
+ },
+ {
+ "input": "text/html;charset=\";charset=GBK",
+ "output": "text/html;charset=\";charset=GBK\"",
+ "navigable": true,
+ "encoding": null
+ },
+ "Unexpected code points",
+ {
+ "input": "text/html;charset={gbk}",
+ "output": "text/html;charset=\"{gbk}\"",
+ "navigable": true,
+ "encoding": null
+ },
+ "Parameter name longer than 127",
+ {
+ "input": "text/html;0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789=x;charset=gbk",
+ "output": "text/html;0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789=x;charset=gbk",
+ "navigable": true,
+ "encoding": "GBK"
+ },
+ "type/subtype longer than 127",
+ {
+ "input": "0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789/0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789",
+ "output": "0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789/0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789"
+ },
+ "Invalid names",
+ {
+ "input": "text/html;a]=bar;b[=bar;c=bar",
+ "output": "text/html;c=bar"
+ },
+ "Semicolons in value",
+ {
+ "input": "text/html;valid=\";\";foo=bar",
+ "output": "text/html;valid=\";\";foo=bar"
+ },
+ {
+ "input": "text/html;in]valid=\";asd=foo\";foo=bar",
+ "output": "text/html;foo=bar"
+ },
+ "Valid",
+ {
+ "input": "!#$%&'*+-.^_`|~0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz/!#$%&'*+-.^_`|~0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz;!#$%&'*+-.^_`|~0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz=!#$%&'*+-.^_`|~0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz",
+ "output": "!#$%&'*+-.^_`|~0123456789abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz/!#$%&'*+-.^_`|~0123456789abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz;!#$%&'*+-.^_`|~0123456789abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz=!#$%&'*+-.^_`|~0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
+ },
+ {
+ "input": "x/x;x=\"\t !\\\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\\\]^_`abcdefghijklmnopqrstuvwxyz{|}~\u0080\u0081\u0082\u0083\u0084\u0085\u0086\u0087\u0088\u0089\u008A\u008B\u008C\u008D\u008E\u008F\u0090\u0091\u0092\u0093\u0094\u0095\u0096\u0097\u0098\u0099\u009A\u009B\u009C\u009D\u009E\u009F\u00A0\u00A1\u00A2\u00A3\u00A4\u00A5\u00A6\u00A7\u00A8\u00A9\u00AA\u00AB\u00AC\u00AD\u00AE\u00AF\u00B0\u00B1\u00B2\u00B3\u00B4\u00B5\u00B6\u00B7\u00B8\u00B9\u00BA\u00BB\u00BC\u00BD\u00BE\u00BF\u00C0\u00C1\u00C2\u00C3\u00C4\u00C5\u00C6\u00C7\u00C8\u00C9\u00CA\u00CB\u00CC\u00CD\u00CE\u00CF\u00D0\u00D1\u00D2\u00D3\u00D4\u00D5\u00D6\u00D7\u00D8\u00D9\u00DA\u00DB\u00DC\u00DD\u00DE\u00DF\u00E0\u00E1\u00E2\u00E3\u00E4\u00E5\u00E6\u00E7\u00E8\u00E9\u00EA\u00EB\u00EC\u00ED\u00EE\u00EF\u00F0\u00F1\u00F2\u00F3\u00F4\u00F5\u00F6\u00F7\u00F8\u00F9\u00FA\u00FB\u00FC\u00FD\u00FE\u00FF\"",
+ "output": "x/x;x=\"\t !\\\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\\\]^_`abcdefghijklmnopqrstuvwxyz{|}~\u0080\u0081\u0082\u0083\u0084\u0085\u0086\u0087\u0088\u0089\u008A\u008B\u008C\u008D\u008E\u008F\u0090\u0091\u0092\u0093\u0094\u0095\u0096\u0097\u0098\u0099\u009A\u009B\u009C\u009D\u009E\u009F\u00A0\u00A1\u00A2\u00A3\u00A4\u00A5\u00A6\u00A7\u00A8\u00A9\u00AA\u00AB\u00AC\u00AD\u00AE\u00AF\u00B0\u00B1\u00B2\u00B3\u00B4\u00B5\u00B6\u00B7\u00B8\u00B9\u00BA\u00BB\u00BC\u00BD\u00BE\u00BF\u00C0\u00C1\u00C2\u00C3\u00C4\u00C5\u00C6\u00C7\u00C8\u00C9\u00CA\u00CB\u00CC\u00CD\u00CE\u00CF\u00D0\u00D1\u00D2\u00D3\u00D4\u00D5\u00D6\u00D7\u00D8\u00D9\u00DA\u00DB\u00DC\u00DD\u00DE\u00DF\u00E0\u00E1\u00E2\u00E3\u00E4\u00E5\u00E6\u00E7\u00E8\u00E9\u00EA\u00EB\u00EC\u00ED\u00EE\u00EF\u00F0\u00F1\u00F2\u00F3\u00F4\u00F5\u00F6\u00F7\u00F8\u00F9\u00FA\u00FB\u00FC\u00FD\u00FE\u00FF\""
+ },
+ "End-of-file handling",
+ {
+ "input": "x/x;test",
+ "output": "x/x"
+ },
+ {
+ "input": "x/x;test=\"\\",
+ "output": "x/x;test=\"\\\\\""
+ },
+ "Whitespace (not handled by generated-mime-types.json or above)",
+ {
+ "input": "x/x;x= ",
+ "output": "x/x"
+ },
+ {
+ "input": "x/x;x=\t",
+ "output": "x/x"
+ },
+ {
+ "input": "x/x\n\r\t ;x=x",
+ "output": "x/x;x=x"
+ },
+ {
+ "input": "\n\r\t x/x;x=x\n\r\t ",
+ "output": "x/x;x=x"
+ },
+ {
+ "input": "x/x;\n\r\t x=x\n\r\t ;x=y",
+ "output": "x/x;x=x"
+ },
+ "Latin1",
+ {
+ "input": "text/html;test=\u00FF;charset=gbk",
+ "output": "text/html;test=\"\u00FF\";charset=gbk",
+ "navigable": true,
+ "encoding": "GBK"
+ },
+ ">Latin1",
+ {
+ "input": "x/x;test=\uFFFD;x=x",
+ "output": "x/x;x=x"
+ },
+ "Failure",
+ {
+ "input": "\u000Bx/x",
+ "output": null
+ },
+ {
+ "input": "\u000Cx/x",
+ "output": null
+ },
+ {
+ "input": "x/x\u000B",
+ "output": null
+ },
+ {
+ "input": "x/x\u000C",
+ "output": null
+ },
+ {
+ "input": "",
+ "output": null
+ },
+ {
+ "input": "\t",
+ "output": null
+ },
+ {
+ "input": "/",
+ "output": null
+ },
+ {
+ "input": "bogus",
+ "output": null
+ },
+ {
+ "input": "bogus/",
+ "output": null
+ },
+ {
+ "input": "bogus/ ",
+ "output": null
+ },
+ {
+ "input": "bogus/bogus/;",
+ "output": null
+ },
+ {
+ "input": "</>",
+ "output": null
+ },
+ {
+ "input": "(/)",
+ "output": null
+ },
+ {
+ "input": "ÿ/ÿ",
+ "output": null
+ },
+ {
+ "input": "text/html(;doesnot=matter",
+ "output": null
+ },
+ {
+ "input": "{/}",
+ "output": null
+ },
+ {
+ "input": "\u0100/\u0100",
+ "output": null
+ },
+ {
+ "input": "text /html",
+ "output": null
+ },
+ {
+ "input": "text/ html",
+ "output": null
+ },
+ {
+ "input": "\"text/html\"",
+ "output": null
+ }
+]
diff --git a/test/wpt/tests/resources/.htaccess b/test/wpt/tests/resources/.htaccess
new file mode 100644
index 0000000..fd46101
--- /dev/null
+++ b/test/wpt/tests/resources/.htaccess
@@ -0,0 +1,2 @@
+# make tests that use utf-16 not inherit the encoding for testharness.js et. al.
+AddCharset utf-8 .css .js
diff --git a/test/wpt/tests/resources/META.yml b/test/wpt/tests/resources/META.yml
new file mode 100644
index 0000000..64a240c
--- /dev/null
+++ b/test/wpt/tests/resources/META.yml
@@ -0,0 +1,2 @@
+suggested_reviewers:
+ - jgraham
diff --git a/test/wpt/tests/resources/SVGAnimationTestCase-testharness.js b/test/wpt/tests/resources/SVGAnimationTestCase-testharness.js
new file mode 100644
index 0000000..9ebaf68
--- /dev/null
+++ b/test/wpt/tests/resources/SVGAnimationTestCase-testharness.js
@@ -0,0 +1,102 @@
+// NOTE(edvardt):
+// This file is a slimmed down wrapper for the old SVGAnimationTestCase.js,
+// it has some convenience functions and should not be used for new tests.
+// New tests should not build on this API as it's just meant to keep things
+// working.
+
+// Helper functions
+const xlinkNS = "http://www.w3.org/1999/xlink"
+
+function expectFillColor(element, red, green, blue, message) {
+ let color = window.getComputedStyle(element, null).fill;
+ var re = new RegExp("rgba?\\(([^, ]*), ([^, ]*), ([^, ]*)(?:, )?([^, ]*)\\)");
+ rgb = re.exec(color);
+
+ assert_approx_equals(Number(rgb[1]), red, 2.0, message);
+ assert_approx_equals(Number(rgb[2]), green, 2.0, message);
+ assert_approx_equals(Number(rgb[3]), blue, 2.0, message);
+}
+
+function expectColor(element, red, green, blue, property) {
+ if (typeof property != "string")
+ color = getComputedStyle(element).getPropertyValue("color");
+ else
+ color = getComputedStyle(element).getPropertyValue(property);
+ var re = new RegExp("rgba?\\(([^, ]*), ([^, ]*), ([^, ]*)(?:, )?([^, ]*)\\)");
+ rgb = re.exec(color);
+ assert_approx_equals(Number(rgb[1]), red, 2.0);
+ assert_approx_equals(Number(rgb[2]), green, 2.0);
+ assert_approx_equals(Number(rgb[3]), blue, 2.0);
+}
+
+function createSVGElement(type) {
+ return document.createElementNS("http://www.w3.org/2000/svg", type);
+}
+
+// Inspired by Layoutests/animations/animation-test-helpers.js
+function moveAnimationTimelineAndSample(index) {
+ var animationId = expectedResults[index][0];
+ var time = expectedResults[index][1];
+ var sampleCallback = expectedResults[index][2];
+ var animation = rootSVGElement.ownerDocument.getElementById(animationId);
+
+ // If we want to sample the animation end, add a small delta, to reliable point past the end of the animation.
+ newTime = time;
+
+ // The sample time is relative to the start time of the animation, take that into account.
+ rootSVGElement.setCurrentTime(newTime);
+
+ // NOTE(edvardt):
+ // This is a dumb hack, some of the old tests sampled before the animation start, this
+ // isn't technically part of the animation tests and is "impossible" to translate since
+ // tests start automatically. Thus I solved it by skipping it.
+ if (time != 0.0)
+ sampleCallback();
+}
+
+var currentTest = 0;
+var expectedResults;
+
+function sampleAnimation(t) {
+ if (currentTest == expectedResults.length) {
+ t.done();
+ return;
+ }
+
+ moveAnimationTimelineAndSample(currentTest);
+ ++currentTest;
+
+ step_timeout(t.step_func(function () { sampleAnimation(t); }), 0);
+}
+
+function runAnimationTest(t, expected) {
+ if (!expected)
+ throw("Expected results are missing!");
+ if (currentTest > 0)
+ throw("Not allowed to call runAnimationTest() twice");
+
+ expectedResults = expected;
+ testCount = expectedResults.length;
+ currentTest = 0;
+
+ step_timeout(t.step_func(function () { sampleAnimation(this); }), 50);
+}
+
+function smil_async_test(func) {
+ async_test(t => {
+ window.onload = t.step_func(function () {
+ // Pause animations, we'll drive them manually.
+ // This also ensures that the timeline is paused before
+ // it starts. This should make the instance time of the below
+ // 'click' (for instance) 0, and hence minimize rounding
+ // errors for the addition in moveAnimationTimelineAndSample.
+ rootSVGElement.pauseAnimations();
+
+ // If eg. an animation is running with begin="0s", and
+ // we want to sample the first time, before the animation
+ // starts, then we can't delay the testing by using an
+ // onclick event, as the animation would be past start time.
+ func(t);
+ });
+ });
+}
diff --git a/test/wpt/tests/resources/accesskey.js b/test/wpt/tests/resources/accesskey.js
new file mode 100644
index 0000000..e95c9d2
--- /dev/null
+++ b/test/wpt/tests/resources/accesskey.js
@@ -0,0 +1,34 @@
+/*
+ * Function that sends an accesskey using the proper key combination depending on the browser and OS.
+ *
+ * This needs that the test imports the following scripts:
+ * <script src="/resources/testdriver.js"></script>
+ * <script src="/resources/testdriver-actions.js"></script>
+ * <script src="/resources/testdriver-vendor.js"></script>
+*/
+function pressAccessKey(accessKey){
+ let controlKey = '\uE009'; // left Control key
+ let altKey = '\uE00A'; // left Alt key
+ let optionKey = altKey; // left Option key
+ let shiftKey = '\uE008'; // left Shift key
+ // There are differences in using accesskey across browsers and OS's.
+ // See: // https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/accesskey
+ let isMacOSX = navigator.userAgent.indexOf("Mac") != -1;
+ let osAccessKey = isMacOSX ? [controlKey, optionKey] : [shiftKey, altKey];
+ let actions = new test_driver.Actions();
+ // Press keys.
+ for (let key of osAccessKey) {
+ actions = actions.keyDown(key);
+ }
+ actions = actions
+ .keyDown(accessKey)
+ .addTick()
+ .keyUp(accessKey);
+ osAccessKey.reverse();
+ for (let key of osAccessKey) {
+ actions = actions.keyUp(key);
+ }
+ return actions.send();
+}
+
+
diff --git a/test/wpt/tests/resources/blank.html b/test/wpt/tests/resources/blank.html
new file mode 100644
index 0000000..edeaa45
--- /dev/null
+++ b/test/wpt/tests/resources/blank.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8" />
+ <title>Blank Page</title>
+ <script>
+ window.onload = function(event) {
+ // This is needed to ensure the onload event fires when this page is
+ // opened as a popup.
+ // See https://github.com/web-platform-tests/wpt/pull/18157
+ };
+ </script>
+ </head>
+ <body>
+ </body>
+</html>
diff --git a/test/wpt/tests/resources/channel.sub.js b/test/wpt/tests/resources/channel.sub.js
new file mode 100644
index 0000000..370d4f5
--- /dev/null
+++ b/test/wpt/tests/resources/channel.sub.js
@@ -0,0 +1,1097 @@
+(function() {
+ function randInt(bits) {
+ if (bits < 1 || bits > 53) {
+ throw new TypeError();
+ } else {
+ if (bits >= 1 && bits <= 30) {
+ return 0 | ((1 << bits) * Math.random());
+ } else {
+ var high = (0 | ((1 << (bits - 30)) * Math.random())) * (1 << 30);
+ var low = 0 | ((1 << 30) * Math.random());
+ return high + low;
+ }
+ }
+ }
+
+
+ function toHex(x, length) {
+ var rv = x.toString(16);
+ while (rv.length < length) {
+ rv = "0" + rv;
+ }
+ return rv;
+ }
+
+ function createUuid() {
+ return [toHex(randInt(32), 8),
+ toHex(randInt(16), 4),
+ toHex(0x4000 | randInt(12), 4),
+ toHex(0x8000 | randInt(14), 4),
+ toHex(randInt(48), 12)].join("-");
+ }
+
+
+ /**
+ * Cache of WebSocket instances per channel
+ *
+ * For reading there can only be one channel with each UUID, so we
+ * just have a simple map of {uuid: WebSocket}. The socket can be
+ * closed when the channel is closed.
+ *
+ * For writing there can be many channels for each uuid. Those can
+ * share a websocket (within a specific global), so we have a map
+ * of {uuid: [WebSocket, count]}. Count is incremented when a
+ * channel is opened with a given uuid, and decremented when its
+ * closed. When the count reaches zero we can close the underlying
+ * socket.
+ */
+ class SocketCache {
+ constructor() {
+ this.readSockets = new Map();
+ this.writeSockets = new Map();
+ };
+
+ async getOrCreate(type, uuid, onmessage=null) {
+ function createSocket() {
+ let protocol = self.isSecureContext ? "wss" : "ws";
+ let port = self.isSecureContext? "{{ports[wss][0]}}" : "{{ports[ws][0]}}";
+ let url = `${protocol}://{{host}}:${port}/msg_channel?uuid=${uuid}&direction=${type}`;
+ let socket = new WebSocket(url);
+ if (onmessage !== null) {
+ socket.onmessage = onmessage;
+ };
+ return new Promise(resolve => socket.addEventListener("open", () => resolve(socket)));
+ }
+
+ let socket;
+ if (type === "read") {
+ if (this.readSockets.has(uuid)) {
+ throw new Error("Can't create multiple read sockets with same UUID");
+ }
+ socket = await createSocket();
+ // If the socket is closed by the server, ensure it's removed from the cache
+ socket.addEventListener("close", () => this.readSockets.delete(uuid));
+ this.readSockets.set(uuid, socket);
+ } else if (type === "write") {
+ let count;
+ if (onmessage !== null) {
+ throw new Error("Can't set message handler for write sockets");
+ }
+ if (this.writeSockets.has(uuid)) {
+ [socket, count] = this.writeSockets.get(uuid);
+ } else {
+ socket = await createSocket();
+ count = 0;
+ }
+ count += 1;
+ // If the socket is closed by the server, ensure it's removed from the cache
+ socket.addEventListener("close", () => this.writeSockets.delete(uuid));
+ this.writeSockets.set(uuid, [socket, count]);
+ } else {
+ throw new Error(`Unknown type ${type}`);
+ }
+ return socket;
+ };
+
+ async close(type, uuid) {
+ let target = type === "read" ? this.readSockets : this.writeSockets;
+ const data = target.get(uuid);
+ if (!data) {
+ return;
+ }
+ let count, socket;
+ if (type == "read") {
+ socket = data;
+ count = 0;
+ } else if (type === "write") {
+ [socket, count] = data;
+ count -= 1;
+ if (count > 0) {
+ target.set(uuid, [socket, count]);
+ }
+ };
+ if (count <= 0 && socket) {
+ target.delete(uuid);
+ socket.close(1000);
+ await new Promise(resolve => socket.addEventListener("close", resolve));
+ }
+ };
+
+ async closeAll() {
+ let sockets = [];
+ this.readSockets.forEach(value => sockets.push(value));
+ this.writeSockets.forEach(value => sockets.push(value[0]));
+ let closePromises = sockets.map(socket =>
+ new Promise(resolve => socket.addEventListener("close", resolve)));
+ sockets.forEach(socket => socket.close(1000));
+ this.readSockets.clear();
+ this.writeSockets.clear();
+ await Promise.all(closePromises);
+ }
+ }
+
+ const socketCache = new SocketCache();
+
+ /**
+ * Abstract base class for objects that allow sending / receiving
+ * messages over a channel.
+ */
+ class Channel {
+ type = null;
+
+ constructor(uuid) {
+ /** UUID for the channel */
+ this.uuid = uuid;
+ this.socket = null;
+ this.eventListeners = {
+ connect: new Set(),
+ close: new Set()
+ };
+ }
+
+ hasConnection() {
+ return this.socket !== null && this.socket.readyState <= WebSocket.OPEN;
+ }
+
+ /**
+ * Connect to the channel.
+ *
+ * @param {Function} onmessage - Event handler function for
+ * the underlying websocket message.
+ */
+ async connect(onmessage) {
+ if (this.hasConnection()) {
+ return;
+ }
+ this.socket = await socketCache.getOrCreate(this.type, this.uuid, onmessage);
+ this._dispatch("connect");
+ }
+
+ /**
+ * Close the channel and underlying websocket connection
+ */
+ async close() {
+ this.socket = null;
+ await socketCache.close(this.type, this.uuid);
+ this._dispatch("close");
+ }
+
+ /**
+ * Add an event callback function. Supported message types are
+ * "connect", "close", and "message" (for ``RecvChannel``).
+ *
+ * @param {string} type - Message type.
+ * @param {Function} fn - Callback function. This is called
+ * with an event-like object, with ``type`` and ``data``
+ * properties.
+ */
+ addEventListener(type, fn) {
+ if (typeof type !== "string") {
+ throw new TypeError(`Expected string, got ${typeof type}`);
+ }
+ if (typeof fn !== "function") {
+ throw new TypeError(`Expected function, got ${typeof fn}`);
+ }
+ if (!this.eventListeners.hasOwnProperty(type)) {
+ throw new Error(`Unrecognised event type ${type}`);
+ }
+ this.eventListeners[type].add(fn);
+ };
+
+ /**
+ * Remove an event callback function.
+ *
+ * @param {string} type - Event type.
+ * @param {Function} fn - Callback function to remove.
+ */
+ removeEventListener(type, fn) {
+ if (!typeof type === "string") {
+ throw new TypeError(`Expected string, got ${typeof type}`);
+ }
+ if (typeof fn !== "function") {
+ throw new TypeError(`Expected function, got ${typeof fn}`);
+ }
+ let listeners = this.eventListeners[type];
+ if (listeners) {
+ listeners.delete(fn);
+ }
+ };
+
+ _dispatch(type, data) {
+ let listeners = this.eventListeners[type];
+ if (listeners) {
+ // If any listener throws we end up not calling the other
+ // listeners. This hopefully makes debugging easier, but
+ // is different to DOM event listeners.
+ listeners.forEach(fn => fn({type, data}));
+ }
+ };
+
+ }
+
+ /**
+ * Send messages over a channel
+ */
+ class SendChannel extends Channel {
+ type = "write";
+
+ /**
+ * Connect to the channel. Automatically called when sending the
+ * first message.
+ */
+ async connect() {
+ return super.connect(null);
+ }
+
+ async _send(cmd, body=null) {
+ if (!this.hasConnection()) {
+ await this.connect();
+ }
+ this.socket.send(JSON.stringify([cmd, body]));
+ }
+
+ /**
+ * Send a message. The message object must be JSON-serializable.
+ *
+ * @param {Object} msg - The message object to send.
+ */
+ async send(msg) {
+ await this._send("message", msg);
+ }
+
+ /**
+ * Disconnect the associated `RecvChannel <#RecvChannel>`_, if
+ * any, on the server side.
+ */
+ async disconnectReader() {
+ await this._send("disconnectReader");
+ }
+
+ /**
+ * Disconnect this channel on the server side.
+ */
+ async delete() {
+ await this._send("delete");
+ }
+ };
+ self.SendChannel = SendChannel;
+
+ const recvChannelsCreated = new Set();
+
+ /**
+ * Receive messages over a channel
+ */
+ class RecvChannel extends Channel {
+ type = "read";
+
+ constructor(uuid) {
+ if (recvChannelsCreated.has(uuid)) {
+ throw new Error(`Already created RecvChannel with id ${uuid}`);
+ }
+ super(uuid);
+ this.eventListeners.message = new Set();
+ }
+
+ async connect() {
+ if (this.hasConnection()) {
+ return;
+ }
+ await super.connect(event => this.readMessage(event.data));
+ }
+
+ readMessage(data) {
+ let msg = JSON.parse(data);
+ this._dispatch("message", msg);
+ }
+
+ /**
+ * Wait for the next message and return it (after passing it to
+ * existing handlers)
+ *
+ * @returns {Promise} - Promise that resolves to the message data.
+ */
+ nextMessage() {
+ return new Promise(resolve => {
+ let fn = ({data}) => {
+ this.removeEventListener("message", fn);
+ resolve(data);
+ };
+ this.addEventListener("message", fn);
+ });
+ }
+ }
+
+ /**
+ * Create a new channel pair
+ *
+ * @returns {Array} - Array of [RecvChannel, SendChannel] for the same channel.
+ */
+ self.channel = function() {
+ let uuid = createUuid();
+ let recvChannel = new RecvChannel(uuid);
+ let sendChannel = new SendChannel(uuid);
+ return [recvChannel, sendChannel];
+ };
+
+ /**
+ * Create an unconnected channel defined by a `uuid` in
+ * ``location.href`` for listening for `RemoteGlobal
+ * <#RemoteGlobal>`_ messages.
+ *
+ * @returns {RemoteGlobalCommandRecvChannel} - Disconnected channel
+ */
+ self.global_channel = function() {
+ let uuid = new URLSearchParams(location.search).get("uuid");
+ if (!uuid) {
+ throw new Error("URL must have a uuid parameter to use as a RemoteGlobal");
+ }
+ return new RemoteGlobalCommandRecvChannel(new RecvChannel(uuid));
+ };
+
+ /**
+ * Start listening for `RemoteGlobal <#RemoteGlobal>`_ messages on
+ * a channel defined by a `uuid` in `location.href`
+ *
+ * @returns {RemoteGlobalCommandRecvChannel} - Connected channel
+ */
+ self.start_global_channel = async function() {
+ let channel = self.global_channel();
+ await channel.connect();
+ return channel;
+ };
+
+ /**
+ * Close all WebSockets used by channels in the current realm.
+ *
+ */
+ self.close_all_channel_sockets = async function() {
+ await socketCache.closeAll();
+ // Spinning the event loop after the close events is necessary to
+ // ensure that the channels really are closed and don't affect
+ // bfcache behaviour in at least some implementations.
+ await new Promise(resolve => setTimeout(resolve, 0));
+ };
+
+ /**
+ * Handler for `RemoteGlobal <#RemoteGlobal>`_ commands.
+ *
+ * This can't be constructed directly but must be obtained from
+ * `global_channel() <#global_channel>`_ or
+ * `start_global_channel() <#start_global_channel>`_.
+ */
+ class RemoteGlobalCommandRecvChannel {
+ constructor(recvChannel) {
+ this.channel = recvChannel;
+ this.uuid = recvChannel.uuid;
+ this.channel.addEventListener("message", ({data}) => this.handleMessage(data));
+ this.messageHandlers = new Set();
+ };
+
+ /**
+ * Connect to the channel and start handling messages.
+ */
+ async connect() {
+ await this.channel.connect();
+ }
+
+ /**
+ * Close the channel and underlying websocket connection
+ */
+ async close() {
+ await this.channel.close();
+ }
+
+ async handleMessage(msg) {
+ const {id, command, params, respChannel} = msg;
+ let result = {};
+ let resp = {id, result};
+ if (command === "call") {
+ const fn = deserialize(params.fn);
+ const args = params.args.map(deserialize);
+ try {
+ let resultValue = await fn(...args);
+ result.result = serialize(resultValue);
+ } catch(e) {
+ let exception = serialize(e);
+ const getAsInt = (obj, prop) => {
+ let value = prop in obj ? parseInt(obj[prop]) : 0;
+ return Number.isNaN(value) ? 0 : value;
+ };
+ result.exceptionDetails = {
+ text: e.toString(),
+ lineNumber: getAsInt(e, "lineNumber"),
+ columnNumber: getAsInt(e, "columnNumber"),
+ exception
+ };
+ }
+ } else if (command === "postMessage") {
+ this.messageHandlers.forEach(fn => fn(deserialize(params.msg)));
+ }
+ if (respChannel) {
+ let chan = deserialize(respChannel);
+ await chan.connect();
+ await chan.send(resp);
+ }
+ }
+
+ /**
+ * Add a handler for ``postMessage`` messages
+ *
+ * @param {Function} fn - Callback function that receives the
+ * message.
+ */
+ addMessageHandler(fn) {
+ this.messageHandlers.add(fn);
+ }
+
+ /**
+ * Remove a handler for ``postMessage`` messages
+ *
+ * @param {Function} fn - Callback function to remove
+ */
+ removeMessageHandler(fn) {
+ this.messageHandlers.delete(fn);
+ }
+
+ /**
+ * Wait for the next ``postMessage`` message and return it
+ * (after passing it to existing handlers)
+ *
+ * @returns {Promise} - Promise that resolves to the message.
+ */
+ nextMessage() {
+ return new Promise(resolve => {
+ let fn = (msg) => {
+ this.removeMessageHandler(fn);
+ resolve(msg);
+ };
+ this.addMessageHandler(fn);
+ });
+ }
+ }
+
+ class RemoteGlobalResponseRecvChannel {
+ constructor(recvChannel) {
+ this.channel = recvChannel;
+ this.channel.addEventListener("message", ({data}) => this.handleMessage(data));
+ this.responseHandlers = new Map();
+ }
+
+ setResponseHandler(commandId, fn) {
+ this.responseHandlers.set(commandId, fn);
+ }
+
+ handleMessage(msg) {
+ let {id, result} = msg;
+ let handler = this.responseHandlers.get(id);
+ if (handler) {
+ this.responseHandlers.delete(id);
+ handler(result);
+ }
+ }
+
+ close() {
+ return this.channel.close();
+ }
+ }
+
+ /**
+ * Object representing a remote global that has a
+ * `RemoteGlobalCommandRecvChannel
+ * <#RemoteGlobalCommandRecvChannel>`_
+ */
+ class RemoteGlobal {
+ /**
+ * Create a new RemoteGlobal object.
+ *
+ * This doesn't actually construct the global itself; that
+ * must be done elsewhere, with a ``uuid`` query parameter in
+ * its URL set to the same as the ``uuid`` property of this
+ * object.
+ *
+ * @param {SendChannel|string} [dest] - Either a SendChannel
+ * to the destination, or the UUID of the destination. If
+ * ommitted, a new UUID is generated, which can be used when
+ * constructing the URL for the global.
+ *
+ */
+ constructor(dest) {
+ if (dest === undefined || dest === null) {
+ dest = createUuid();
+ }
+ if (typeof dest == "string") {
+ /** UUID for the global */
+ this.uuid = dest;
+ this.sendChannel = new SendChannel(dest);
+ } else if (dest instanceof SendChannel) {
+ this.sendChannel = dest;
+ this.uuid = dest.uuid;
+ } else {
+ throw new TypeError("Unrecognised type, expected string or SendChannel");
+ }
+ this.recvChannel = null;
+ this.respChannel = null;
+ this.connected = false;
+ this.commandId = 0;
+ }
+
+ /**
+ * Connect to the channel. Automatically called when sending the
+ * first message
+ */
+ async connect() {
+ if (this.connected) {
+ return;
+ }
+ let [recvChannel, respChannel] = self.channel();
+ await Promise.all([this.sendChannel.connect(), recvChannel.connect()]);
+ this.recvChannel = new RemoteGlobalResponseRecvChannel(recvChannel);
+ this.respChannel = respChannel;
+ this.connected = true;
+ }
+
+ async sendMessage(command, params, hasResp=true) {
+ if (!this.connected) {
+ await this.connect();
+ }
+ let msg = {id: this.commandId++, command, params};
+ if (hasResp) {
+ msg.respChannel = serialize(this.respChannel);
+ }
+ let response;
+ if (hasResp) {
+ response = new Promise(resolve =>
+ this.recvChannel.setResponseHandler(msg.id, resolve));
+ } else {
+ response = null;
+ }
+ this.sendChannel.send(msg);
+ return await response;
+ }
+
+ /**
+ * Run the function ``fn`` in the remote global, passing arguments
+ * ``args``, and return the result after awaiting any returned
+ * promise.
+ *
+ * @param {Function} fn - Function to run in the remote global.
+ * @param {...Any} args - Arguments to pass to the function
+ * @returns {Promise} - Promise resolving to the return value
+ * of the function.
+ */
+ async call(fn, ...args) {
+ let result = await this.sendMessage("call", {fn: serialize(fn), args: args.map(x => serialize(x))}, true);
+ if (result.exceptionDetails) {
+ throw deserialize(result.exceptionDetails.exception);
+ }
+ return deserialize(result.result);
+ }
+
+ /**
+ * Post a message to the remote
+ *
+ * @param {Any} msg - The message to send.
+ */
+ async postMessage(msg) {
+ await this.sendMessage("postMessage", {msg: serialize(msg)}, false);
+ }
+
+ /**
+ * Disconnect the associated `RemoteGlobalCommandRecvChannel
+ * <#RemoteGlobalCommandRecvChannel>`_, if any, on the server
+ * side.
+ *
+ * @returns {Promise} - Resolved once the channel is disconnected.
+ */
+ disconnectReader() {
+ // This causes any readers to disconnect until they are explicitly reconnected
+ return this.sendChannel.disconnectReader();
+ }
+
+ /**
+ * Close the channel and underlying websocket connections
+ */
+ close() {
+ let closers = [this.sendChannel.close()];
+ if (this.recvChannel !== null) {
+ closers.push(this.recvChannel.close());
+ }
+ if (this.respChannel !== null) {
+ closers.push(this.respChannel.close());
+ }
+ return Promise.all(closers);
+ }
+ }
+
+ self.RemoteGlobal = RemoteGlobal;
+
+ function typeName(value) {
+ let type = typeof value;
+ if (type === "undefined" ||
+ type === "string" ||
+ type === "boolean" ||
+ type === "number" ||
+ type === "bigint" ||
+ type === "symbol" ||
+ type === "function") {
+ return type;
+ }
+
+ if (value === null) {
+ return "null";
+ }
+ // The handling of cross-global objects here is broken
+ if (value instanceof RemoteObject) {
+ return "remoteobject";
+ }
+ if (value instanceof SendChannel) {
+ return "sendchannel";
+ }
+ if (value instanceof RecvChannel) {
+ return "recvchannel";
+ }
+ if (value instanceof Error) {
+ return "error";
+ }
+ if (Array.isArray(value)) {
+ return "array";
+ }
+ let constructor = value.constructor && value.constructor.name;
+ if (constructor === "RegExp" ||
+ constructor === "Date" ||
+ constructor === "Map" ||
+ constructor === "Set" ||
+ constructor == "WeakMap" ||
+ constructor == "WeakSet") {
+ return constructor.toLowerCase();
+ }
+ // The handling of cross-global objects here is broken
+ if (typeof window == "object" && window === self) {
+ if (value instanceof Element) {
+ return "element";
+ }
+ if (value instanceof Document) {
+ return "document";
+ }
+ if (value instanceof Node) {
+ return "node";
+ }
+ if (value instanceof Window) {
+ return "window";
+ }
+ }
+ if (Promise.resolve(value) === value) {
+ return "promise";
+ }
+ return "object";
+ }
+
+ let remoteObjectsById = new Map();
+
+ function remoteId(obj) {
+ let rv;
+ rv = createUuid();
+ remoteObjectsById.set(rv, obj);
+ return rv;
+ }
+
+ /**
+ * Representation of a non-primitive type passed through a channel
+ */
+ class RemoteObject {
+ constructor(type, objectId) {
+ this.type = type;
+ this.objectId = objectId;
+ }
+
+ /**
+ * Create a RemoteObject containing a handle to reference obj
+ *
+ * @param {Any} obj - The object to reference.
+ */
+ static from(obj) {
+ let type = typeName(obj);
+ let id = remoteId(obj);
+ return new RemoteObject(type, id);
+ }
+
+ /**
+ * Return the local object referenced by the ``objectId`` of
+ * this ``RemoteObject``, or ``null`` if there isn't a such an
+ * object in this realm.
+ */
+ toLocal() {
+ if (remoteObjectsById.has(this.objectId)) {
+ return remoteObjectsById.get(this.objectId);
+ }
+ return null;
+ }
+
+ /**
+ * Remove the object from the local cache. This means that future
+ * calls to ``toLocal`` with the same objectId will always return
+ * ``null``.
+ */
+ delete() {
+ remoteObjectsById.delete(this.objectId);
+ }
+ }
+
+ self.RemoteObject = RemoteObject;
+
+ /**
+ * Serialize an object as a JSON-compatible representation.
+ *
+ * The format used is similar (but not identical to)
+ * `WebDriver-BiDi
+ * <https://w3c.github.io/webdriver-bidi/#data-types-protocolValue>`_.
+ *
+ * Each item to be serialized can have the following fields:
+ *
+ * type - The name of the type being represented e.g. "string", or
+ * "map". For primitives this matches ``typeof``, but for
+ * ``object`` types that have particular support in the protocol
+ * e.g. arrays and maps, it is a custom value.
+ *
+ * value - A serialized representation of the object value. For
+ * container types this is a JSON container (i.e. an object or an
+ * array) containing a serialized representation of the child
+ * values.
+ *
+ * objectId - An integer used to handle object graphs. Where
+ * an object is present more than once in the serialization, the
+ * first instance has both ``value`` and ``objectId`` fields, but
+ * when encountered again, only ``objectId`` is present, with the
+ * same value as the first instance of the object.
+ *
+ * @param {Any} inValue - The value to be serialized.
+ * @returns {Object} - The serialized object value.
+ */
+ function serialize(inValue) {
+ const queue = [{item: inValue}];
+ let outValue = null;
+
+ // Map from container object input to output value
+ let objectsSeen = new Map();
+ let lastObjectId = 0;
+
+ /* Instead of making this recursive, use a queue holding the objects to be
+ * serialized. Each item in the queue can have the following properties:
+ *
+ * item (required) - the input item to be serialized
+ *
+ * target - For collections, the output serialized object to
+ * which the serialization of the current item will be added.
+ *
+ * targetName - For serializing object members, the name of
+ * the property. For serializing maps either "key" or "value",
+ * depending on whether the item represents a key or a value
+ * in the map.
+ */
+ while (queue.length > 0) {
+ const {item, target, targetName} = queue.shift();
+ let type = typeName(item);
+
+ let serialized = {type};
+
+ if (objectsSeen.has(item)) {
+ let outputValue = objectsSeen.get(item);
+ if (!outputValue.hasOwnProperty("objectId")) {
+ outputValue.objectId = lastObjectId++;
+ }
+ serialized.objectId = outputValue.objectId;
+ } else {
+ switch (type) {
+ case "undefined":
+ case "null":
+ break;
+ case "string":
+ case "boolean":
+ serialized.value = item;
+ break;
+ case "number":
+ if (item !== item) {
+ serialized.value = "NaN";
+ } else if (item === 0 && 1/item == Number.NEGATIVE_INFINITY) {
+ serialized.value = "-0";
+ } else if (item === Number.POSITIVE_INFINITY) {
+ serialized.value = "+Infinity";
+ } else if (item === Number.NEGATIVE_INFINITY) {
+ serialized.value = "-Infinity";
+ } else {
+ serialized.value = item;
+ }
+ break;
+ case "bigint":
+ case "function":
+ serialized.value = item.toString();
+ break;
+ case "remoteobject":
+ serialized.value = {
+ type: item.type,
+ objectId: item.objectId
+ };
+ break;
+ case "sendchannel":
+ serialized.value = item.uuid;
+ break;
+ case "regexp":
+ serialized.value = {
+ pattern: item.source,
+ flags: item.flags
+ };
+ break;
+ case "date":
+ serialized.value = Date.prototype.toJSON.call(item);
+ break;
+ case "error":
+ serialized.value = {
+ type: item.constructor.name,
+ name: item.name,
+ message: item.message,
+ lineNumber: item.lineNumber,
+ columnNumber: item.columnNumber,
+ fileName: item.fileName,
+ stack: item.stack,
+ };
+ break;
+ case "array":
+ case "set":
+ serialized.value = [];
+ for (let child of item) {
+ queue.push({item: child, target: serialized});
+ }
+ break;
+ case "object":
+ serialized.value = {};
+ for (let [targetName, child] of Object.entries(item)) {
+ queue.push({item: child, target: serialized, targetName});
+ }
+ break;
+ case "map":
+ serialized.value = [];
+ for (let [childKey, childValue] of item.entries()) {
+ queue.push({item: childKey, target: serialized, targetName: "key"});
+ queue.push({item: childValue, target: serialized, targetName: "value"});
+ }
+ break;
+ default:
+ throw new TypeError(`Can't serialize value of type ${type}; consider using RemoteObject.from() to wrap the object`);
+ };
+ }
+ if (serialized.objectId === undefined) {
+ objectsSeen.set(item, serialized);
+ }
+
+ if (target === undefined) {
+ if (outValue !== null) {
+ throw new Error("Tried to create multiple output values");
+ }
+ outValue = serialized;
+ } else {
+ switch (target.type) {
+ case "array":
+ case "set":
+ target.value.push(serialized);
+ break;
+ case "object":
+ target.value[targetName] = serialized;
+ break;
+ case "map":
+ // We always serialize key and value as adjacent items in the queue,
+ // so when we get the key push a new output array and then the value will
+ // be added on the next iteration.
+ if (targetName === "key") {
+ target.value.push([]);
+ }
+ target.value[target.value.length - 1].push(serialized);
+ break;
+ default:
+ throw new Error(`Unknown collection target type ${target.type}`);
+ }
+ }
+ }
+ return outValue;
+ }
+
+ /**
+ * Deserialize an object from a JSON-compatible representation.
+ *
+ * For details on the serialized representation see serialize().
+ *
+ * @param {Object} obj - The value to be deserialized.
+ * @returns {Any} - The deserialized value.
+ */
+ function deserialize(obj) {
+ let deserialized = null;
+ let queue = [{item: obj, target: null}];
+ let objectMap = new Map();
+
+ /* Instead of making this recursive, use a queue holding the objects to be
+ * deserialized. Each item in the queue has the following properties:
+ *
+ * item - The input item to be deserialised.
+ *
+ * target - For members of a collection, a wrapper around the
+ * output collection. This has a ``type`` field which is the
+ * name of the collection type, and a ``value`` field which is
+ * the actual output collection. For primitives, this is null.
+ *
+ * targetName - For object members, the property name on the
+ * output object. For maps, "key" if the item is a key in the output map,
+ * or "value" if it's a value in the output map.
+ */
+ while (queue.length > 0) {
+ const {item, target, targetName} = queue.shift();
+ const {type, value, objectId} = item;
+ let result;
+ let newTarget;
+ if (objectId !== undefined && value === undefined) {
+ result = objectMap.get(objectId);
+ } else {
+ switch(type) {
+ case "undefined":
+ result = undefined;
+ break;
+ case "null":
+ result = null;
+ break;
+ case "string":
+ case "boolean":
+ result = value;
+ break;
+ case "number":
+ if (typeof value === "string") {
+ switch(value) {
+ case "NaN":
+ result = NaN;
+ break;
+ case "-0":
+ result = -0;
+ break;
+ case "+Infinity":
+ result = Number.POSITIVE_INFINITY;
+ break;
+ case "-Infinity":
+ result = Number.NEGATIVE_INFINITY;
+ break;
+ default:
+ throw new Error(`Unexpected number value "${value}"`);
+ }
+ } else {
+ result = value;
+ }
+ break;
+ case "bigint":
+ result = BigInt(value);
+ break;
+ case "function":
+ result = new Function("...args", `return (${value}).apply(null, args)`);
+ break;
+ case "remoteobject":
+ let remote = new RemoteObject(value.type, value.objectId);
+ let local = remote.toLocal();
+ if (local !== null) {
+ result = local;
+ } else {
+ result = remote;
+ }
+ break;
+ case "sendchannel":
+ result = new SendChannel(value);
+ break;
+ case "regexp":
+ result = new RegExp(value.pattern, value.flags);
+ break;
+ case "date":
+ result = new Date(value);
+ break;
+ case "error":
+ // The item.value.type property is the name of the error constructor.
+ // If we have a constructor with the same name in the current realm,
+ // construct an instance of that type, otherwise use a generic Error
+ // type.
+ if (item.value.type in self &&
+ typeof self[item.value.type] === "function") {
+ result = new self[item.value.type](item.value.message);
+ } else {
+ result = new Error(item.value.message);
+ }
+ result.name = item.value.name;
+ result.lineNumber = item.value.lineNumber;
+ result.columnNumber = item.value.columnNumber;
+ result.fileName = item.value.fileName;
+ result.stack = item.value.stack;
+ break;
+ case "array":
+ result = [];
+ newTarget = {type, value: result};
+ for (let child of value) {
+ queue.push({item: child, target: newTarget});
+ }
+ break;
+ case "set":
+ result = new Set();
+ newTarget = {type, value: result};
+ for (let child of value) {
+ queue.push({item: child, target: newTarget});
+ }
+ break;
+ case "object":
+ result = {};
+ newTarget = {type, value: result};
+ for (let [targetName, child] of Object.entries(value)) {
+ queue.push({item: child, target: newTarget, targetName});
+ }
+ break;
+ case "map":
+ result = new Map();
+ newTarget = {type, value: result};
+ for (let [key, child] of value) {
+ queue.push({item: key, target: newTarget, targetName: "key"});
+ queue.push({item: child, target: newTarget, targetName: "value"});
+ }
+ break;
+ default:
+ throw new TypeError(`Can't deserialize object of type ${type}`);
+ }
+ if (objectId !== undefined) {
+ objectMap.set(objectId, result);
+ }
+ }
+
+ if (target === null) {
+ if (deserialized !== null) {
+ throw new Error(`Tried to deserialized a non-root output value without a target`
+ ` container object.`);
+ }
+ deserialized = result;
+ } else {
+ switch(target.type) {
+ case "array":
+ target.value.push(result);
+ break;
+ case "set":
+ target.value.add(result);
+ break;
+ case "object":
+ target.value[targetName] = result;
+ break;
+ case "map":
+ // For maps the same target wrapper is shared between key and value.
+ // After deserializing the key, set the `key` property on the target
+ // until we come to the value.
+ if (targetName === "key") {
+ target.key = result;
+ } else {
+ target.value.set(target.key, result);
+ }
+ break;
+ default:
+ throw new Error(`Unknown target type ${target.type}`);
+ }
+ }
+ }
+ return deserialized;
+ }
+})();
diff --git a/test/wpt/tests/resources/check-layout-th.js b/test/wpt/tests/resources/check-layout-th.js
new file mode 100644
index 0000000..f14ca32
--- /dev/null
+++ b/test/wpt/tests/resources/check-layout-th.js
@@ -0,0 +1,252 @@
+(function() {
+// Test is initiated from body.onload, so explicit done() call is required.
+setup({ explicit_done: true });
+
+function checkSubtreeExpectedValues(t, parent, prefix)
+{
+ var checkedLayout = checkExpectedValues(t, parent, prefix);
+ Array.prototype.forEach.call(parent.childNodes, function(node) {
+ checkedLayout |= checkSubtreeExpectedValues(t, node, prefix);
+ });
+ return checkedLayout;
+}
+
+function checkAttribute(output, node, attribute)
+{
+ var result = node.getAttribute && node.getAttribute(attribute);
+ output.checked |= !!result;
+ return result;
+}
+
+function assert_tolerance(actual, expected, message)
+{
+ if (isNaN(expected) || isNaN(actual) || Math.abs(actual - expected) >= 1) {
+ assert_equals(actual, Number(expected), message);
+ }
+}
+
+function checkDataKeys(node) {
+ // The purpose of this list of data-* attributes is simply to ensure typos
+ // in tests are caught. It is therefore "ok" to add to this list for
+ // specific tests.
+ var validData = new Set([
+ "data-anchor-polyfill",
+ "data-expected-width",
+ "data-expected-height",
+ "data-offset-x",
+ "data-offset-y",
+ "data-expected-client-width",
+ "data-expected-client-height",
+ "data-expected-scroll-width",
+ "data-expected-scroll-height",
+ "data-expected-bounding-client-rect-width",
+ "data-expected-bounding-client-rect-height",
+ "data-total-x",
+ "data-total-y",
+ "data-expected-display",
+ "data-expected-padding-top",
+ "data-expected-padding-bottom",
+ "data-expected-padding-left",
+ "data-expected-padding-right",
+ "data-expected-margin-top",
+ "data-expected-margin-bottom",
+ "data-expected-margin-left",
+ "data-expected-margin-right"
+ ]);
+ if (!node || !node.getAttributeNames)
+ return;
+ // Use "data-test" prefix if you need custom-named data elements.
+ for (let name of node.getAttributeNames()) {
+ if (name.startsWith("data-") && !name.startsWith("data-test"))
+ assert_true(validData.has(name), name + " is a valid data attribute");
+ }
+}
+
+function checkExpectedValues(t, node, prefix)
+{
+ checkDataKeys(node);
+ var output = { checked: false };
+
+ var expectedWidth = checkAttribute(output, node, "data-expected-width");
+ if (expectedWidth) {
+ assert_tolerance(node.offsetWidth, expectedWidth, prefix + "width");
+ }
+
+ var expectedHeight = checkAttribute(output, node, "data-expected-height");
+ if (expectedHeight) {
+ assert_tolerance(node.offsetHeight, expectedHeight, prefix + "height");
+ }
+
+ var expectedOffset = checkAttribute(output, node, "data-offset-x");
+ if (expectedOffset) {
+ assert_tolerance(node.offsetLeft, expectedOffset, prefix + "offsetLeft");
+ }
+
+ var expectedOffset = checkAttribute(output, node, "data-offset-y");
+ if (expectedOffset) {
+ assert_tolerance(node.offsetTop, expectedOffset, prefix + "offsetTop");
+ }
+
+ var expectedWidth = checkAttribute(output, node, "data-expected-client-width");
+ if (expectedWidth) {
+ assert_tolerance(node.clientWidth, expectedWidth, prefix + "clientWidth");
+ }
+
+ var expectedHeight = checkAttribute(output, node, "data-expected-client-height");
+ if (expectedHeight) {
+ assert_tolerance(node.clientHeight, expectedHeight, prefix + "clientHeight");
+ }
+
+ var expectedWidth = checkAttribute(output, node, "data-expected-scroll-width");
+ if (expectedWidth) {
+ assert_tolerance(node.scrollWidth, expectedWidth, prefix + "scrollWidth");
+ }
+
+ var expectedHeight = checkAttribute(output, node, "data-expected-scroll-height");
+ if (expectedHeight) {
+ assert_tolerance(node.scrollHeight, expectedHeight, prefix + "scrollHeight");
+ }
+
+ var expectedWidth = checkAttribute(output, node, "data-expected-bounding-client-rect-width");
+ if (expectedWidth) {
+ assert_tolerance(node.getBoundingClientRect().width, expectedWidth, prefix + "getBoundingClientRect().width");
+ }
+
+ var expectedHeight = checkAttribute(output, node, "data-expected-bounding-client-rect-height");
+ if (expectedHeight) {
+ assert_tolerance(node.getBoundingClientRect().height, expectedHeight, prefix + "getBoundingClientRect().height");
+ }
+
+ var expectedOffset = checkAttribute(output, node, "data-total-x");
+ if (expectedOffset) {
+ var totalLeft = node.clientLeft + node.offsetLeft;
+ assert_tolerance(totalLeft, expectedOffset, prefix +
+ "clientLeft+offsetLeft (" + node.clientLeft + " + " + node.offsetLeft + ")");
+ }
+
+ var expectedOffset = checkAttribute(output, node, "data-total-y");
+ if (expectedOffset) {
+ var totalTop = node.clientTop + node.offsetTop;
+ assert_tolerance(totalTop, expectedOffset, prefix +
+ "clientTop+offsetTop (" + node.clientTop + " + " + node.offsetTop + ")");
+ }
+
+ var expectedDisplay = checkAttribute(output, node, "data-expected-display");
+ if (expectedDisplay) {
+ var actualDisplay = getComputedStyle(node).display;
+ assert_equals(actualDisplay, expectedDisplay, prefix + "display");
+ }
+
+ var expectedPaddingTop = checkAttribute(output, node, "data-expected-padding-top");
+ if (expectedPaddingTop) {
+ var actualPaddingTop = getComputedStyle(node).paddingTop;
+ // Trim the unit "px" from the output.
+ actualPaddingTop = actualPaddingTop.slice(0, -2);
+ assert_equals(actualPaddingTop, expectedPaddingTop, prefix + "padding-top");
+ }
+
+ var expectedPaddingBottom = checkAttribute(output, node, "data-expected-padding-bottom");
+ if (expectedPaddingBottom) {
+ var actualPaddingBottom = getComputedStyle(node).paddingBottom;
+ // Trim the unit "px" from the output.
+ actualPaddingBottom = actualPaddingBottom.slice(0, -2);
+ assert_equals(actualPaddingBottom, expectedPaddingBottom, prefix + "padding-bottom");
+ }
+
+ var expectedPaddingLeft = checkAttribute(output, node, "data-expected-padding-left");
+ if (expectedPaddingLeft) {
+ var actualPaddingLeft = getComputedStyle(node).paddingLeft;
+ // Trim the unit "px" from the output.
+ actualPaddingLeft = actualPaddingLeft.slice(0, -2);
+ assert_equals(actualPaddingLeft, expectedPaddingLeft, prefix + "padding-left");
+ }
+
+ var expectedPaddingRight = checkAttribute(output, node, "data-expected-padding-right");
+ if (expectedPaddingRight) {
+ var actualPaddingRight = getComputedStyle(node).paddingRight;
+ // Trim the unit "px" from the output.
+ actualPaddingRight = actualPaddingRight.slice(0, -2);
+ assert_equals(actualPaddingRight, expectedPaddingRight, prefix + "padding-right");
+ }
+
+ var expectedMarginTop = checkAttribute(output, node, "data-expected-margin-top");
+ if (expectedMarginTop) {
+ var actualMarginTop = getComputedStyle(node).marginTop;
+ // Trim the unit "px" from the output.
+ actualMarginTop = actualMarginTop.slice(0, -2);
+ assert_equals(actualMarginTop, expectedMarginTop, prefix + "margin-top");
+ }
+
+ var expectedMarginBottom = checkAttribute(output, node, "data-expected-margin-bottom");
+ if (expectedMarginBottom) {
+ var actualMarginBottom = getComputedStyle(node).marginBottom;
+ // Trim the unit "px" from the output.
+ actualMarginBottom = actualMarginBottom.slice(0, -2);
+ assert_equals(actualMarginBottom, expectedMarginBottom, prefix + "margin-bottom");
+ }
+
+ var expectedMarginLeft = checkAttribute(output, node, "data-expected-margin-left");
+ if (expectedMarginLeft) {
+ var actualMarginLeft = getComputedStyle(node).marginLeft;
+ // Trim the unit "px" from the output.
+ actualMarginLeft = actualMarginLeft.slice(0, -2);
+ assert_equals(actualMarginLeft, expectedMarginLeft, prefix + "margin-left");
+ }
+
+ var expectedMarginRight = checkAttribute(output, node, "data-expected-margin-right");
+ if (expectedMarginRight) {
+ var actualMarginRight = getComputedStyle(node).marginRight;
+ // Trim the unit "px" from the output.
+ actualMarginRight = actualMarginRight.slice(0, -2);
+ assert_equals(actualMarginRight, expectedMarginRight, prefix + "margin-right");
+ }
+
+ return output.checked;
+}
+
+var testNumber = 0;
+var highlightError = false; // displays outline around failed test element.
+var printDomOnError = true; // prints dom when test fails.
+
+window.checkLayout = function(selectorList, callDone = true)
+{
+ if (!selectorList) {
+ console.error("You must provide a CSS selector of nodes to check.");
+ return;
+ }
+ var nodes = document.querySelectorAll(selectorList);
+ nodes = Array.prototype.slice.call(nodes);
+ var checkedLayout = false;
+ Array.prototype.forEach.call(nodes, function(node) {
+ test(function(t) {
+ var container = node.parentNode.className == 'container' ? node.parentNode : node;
+ var prefix =
+ printDomOnError ? '\n' + container.outerHTML + '\n' : '';
+ var passed = false;
+ try {
+ checkedLayout |= checkExpectedValues(t, node.parentNode, prefix);
+ checkedLayout |= checkSubtreeExpectedValues(t, node, prefix);
+ passed = true;
+ } finally {
+ if (!passed && highlightError) {
+ if (!document.getElementById('testharness_error_css')) {
+ var style = document.createElement('style');
+ style.id = 'testharness_error_css';
+ style.textContent = '.testharness_error { outline: red dotted 2px !important; }';
+ document.body.appendChild(style);
+ }
+ if (node)
+ node.classList.add('testharness_error');
+ }
+ checkedLayout |= !passed;
+ }
+ }, selectorList + ' ' + String(++testNumber));
+ });
+ if (!checkedLayout) {
+ console.error("No valid data-* attributes found in selector list : " + selectorList);
+ }
+ if (callDone)
+ done();
+};
+
+})();
diff --git a/test/wpt/tests/resources/check-layout.js b/test/wpt/tests/resources/check-layout.js
new file mode 100644
index 0000000..8634481
--- /dev/null
+++ b/test/wpt/tests/resources/check-layout.js
@@ -0,0 +1,245 @@
+(function() {
+
+function insertAfter(nodeToAdd, referenceNode)
+{
+ if (referenceNode == document.body) {
+ document.body.appendChild(nodeToAdd);
+ return;
+ }
+
+ if (referenceNode.nextSibling)
+ referenceNode.parentNode.insertBefore(nodeToAdd, referenceNode.nextSibling);
+ else
+ referenceNode.parentNode.appendChild(nodeToAdd);
+}
+
+function positionedAncestor(node)
+{
+ var ancestor = node.parentNode;
+ while (getComputedStyle(ancestor).position == 'static')
+ ancestor = ancestor.parentNode;
+ return ancestor;
+}
+
+function checkSubtreeExpectedValues(parent, failures)
+{
+ var checkedLayout = checkExpectedValues(parent, failures);
+ Array.prototype.forEach.call(parent.childNodes, function(node) {
+ checkedLayout |= checkSubtreeExpectedValues(node, failures);
+ });
+ return checkedLayout;
+}
+
+function checkAttribute(output, node, attribute)
+{
+ var result = node.getAttribute && node.getAttribute(attribute);
+ output.checked |= !!result;
+ return result;
+}
+
+function checkExpectedValues(node, failures)
+{
+ var output = { checked: false };
+ var expectedWidth = checkAttribute(output, node, "data-expected-width");
+ if (expectedWidth) {
+ if (isNaN(expectedWidth) || Math.abs(node.offsetWidth - expectedWidth) >= 1)
+ failures.push("Expected " + expectedWidth + " for width, but got " + node.offsetWidth + ". ");
+ }
+
+ var expectedHeight = checkAttribute(output, node, "data-expected-height");
+ if (expectedHeight) {
+ if (isNaN(expectedHeight) || Math.abs(node.offsetHeight - expectedHeight) >= 1)
+ failures.push("Expected " + expectedHeight + " for height, but got " + node.offsetHeight + ". ");
+ }
+
+ var expectedOffset = checkAttribute(output, node, "data-offset-x");
+ if (expectedOffset) {
+ if (isNaN(expectedOffset) || Math.abs(node.offsetLeft - expectedOffset) >= 1)
+ failures.push("Expected " + expectedOffset + " for offsetLeft, but got " + node.offsetLeft + ". ");
+ }
+
+ var expectedOffset = checkAttribute(output, node, "data-offset-y");
+ if (expectedOffset) {
+ if (isNaN(expectedOffset) || Math.abs(node.offsetTop - expectedOffset) >= 1)
+ failures.push("Expected " + expectedOffset + " for offsetTop, but got " + node.offsetTop + ". ");
+ }
+
+ var expectedOffset = checkAttribute(output, node, "data-positioned-offset-x");
+ if (expectedOffset) {
+ var actualOffset = node.getBoundingClientRect().left - positionedAncestor(node).getBoundingClientRect().left;
+ if (isNaN(expectedOffset) || Math.abs(actualOffset - expectedOffset) >= 1)
+ failures.push("Expected " + expectedOffset + " for getBoundingClientRect().left offset, but got " + actualOffset + ". ");
+ }
+
+ var expectedOffset = checkAttribute(output, node, "data-positioned-offset-y");
+ if (expectedOffset) {
+ var actualOffset = node.getBoundingClientRect().top - positionedAncestor(node).getBoundingClientRect().top;
+ if (isNaN(expectedOffset) || Math.abs(actualOffset - expectedOffset) >= 1)
+ failures.push("Expected " + expectedOffset + " for getBoundingClientRect().top offset, but got " + actualOffset + ". ");
+ }
+
+ var expectedWidth = checkAttribute(output, node, "data-expected-client-width");
+ if (expectedWidth) {
+ if (isNaN(expectedWidth) || Math.abs(node.clientWidth - expectedWidth) >= 1)
+ failures.push("Expected " + expectedWidth + " for clientWidth, but got " + node.clientWidth + ". ");
+ }
+
+ var expectedHeight = checkAttribute(output, node, "data-expected-client-height");
+ if (expectedHeight) {
+ if (isNaN(expectedHeight) || Math.abs(node.clientHeight - expectedHeight) >= 1)
+ failures.push("Expected " + expectedHeight + " for clientHeight, but got " + node.clientHeight + ". ");
+ }
+
+ var expectedWidth = checkAttribute(output, node, "data-expected-scroll-width");
+ if (expectedWidth) {
+ if (isNaN(expectedWidth) || Math.abs(node.scrollWidth - expectedWidth) >= 1)
+ failures.push("Expected " + expectedWidth + " for scrollWidth, but got " + node.scrollWidth + ". ");
+ }
+
+ var expectedHeight = checkAttribute(output, node, "data-expected-scroll-height");
+ if (expectedHeight) {
+ if (isNaN(expectedHeight) || Math.abs(node.scrollHeight - expectedHeight) >= 1)
+ failures.push("Expected " + expectedHeight + " for scrollHeight, but got " + node.scrollHeight + ". ");
+ }
+
+ var expectedOffset = checkAttribute(output, node, "data-total-x");
+ if (expectedOffset) {
+ var totalLeft = node.clientLeft + node.offsetLeft;
+ if (isNaN(expectedOffset) || Math.abs(totalLeft - expectedOffset) >= 1)
+ failures.push("Expected " + expectedOffset + " for clientLeft+offsetLeft, but got " + totalLeft + ", clientLeft: " + node.clientLeft + ", offsetLeft: " + node.offsetLeft + ". ");
+ }
+
+ var expectedOffset = checkAttribute(output, node, "data-total-y");
+ if (expectedOffset) {
+ var totalTop = node.clientTop + node.offsetTop;
+ if (isNaN(expectedOffset) || Math.abs(totalTop - expectedOffset) >= 1)
+ failures.push("Expected " + expectedOffset + " for clientTop+offsetTop, but got " + totalTop + ", clientTop: " + node.clientTop + ", + offsetTop: " + node.offsetTop + ". ");
+ }
+
+ var expectedDisplay = checkAttribute(output, node, "data-expected-display");
+ if (expectedDisplay) {
+ var actualDisplay = getComputedStyle(node).display;
+ if (actualDisplay != expectedDisplay)
+ failures.push("Expected " + expectedDisplay + " for display, but got " + actualDisplay + ". ");
+ }
+
+ var expectedPaddingTop = checkAttribute(output, node, "data-expected-padding-top");
+ if (expectedPaddingTop) {
+ var actualPaddingTop = getComputedStyle(node).paddingTop;
+ // Trim the unit "px" from the output.
+ actualPaddingTop = actualPaddingTop.substring(0, actualPaddingTop.length - 2);
+ if (actualPaddingTop != expectedPaddingTop)
+ failures.push("Expected " + expectedPaddingTop + " for padding-top, but got " + actualPaddingTop + ". ");
+ }
+
+ var expectedPaddingBottom = checkAttribute(output, node, "data-expected-padding-bottom");
+ if (expectedPaddingBottom) {
+ var actualPaddingBottom = getComputedStyle(node).paddingBottom;
+ // Trim the unit "px" from the output.
+ actualPaddingBottom = actualPaddingBottom.substring(0, actualPaddingBottom.length - 2);
+ if (actualPaddingBottom != expectedPaddingBottom)
+ failures.push("Expected " + expectedPaddingBottom + " for padding-bottom, but got " + actualPaddingBottom + ". ");
+ }
+
+ var expectedPaddingLeft = checkAttribute(output, node, "data-expected-padding-left");
+ if (expectedPaddingLeft) {
+ var actualPaddingLeft = getComputedStyle(node).paddingLeft;
+ // Trim the unit "px" from the output.
+ actualPaddingLeft = actualPaddingLeft.substring(0, actualPaddingLeft.length - 2);
+ if (actualPaddingLeft != expectedPaddingLeft)
+ failures.push("Expected " + expectedPaddingLeft + " for padding-left, but got " + actualPaddingLeft + ". ");
+ }
+
+ var expectedPaddingRight = checkAttribute(output, node, "data-expected-padding-right");
+ if (expectedPaddingRight) {
+ var actualPaddingRight = getComputedStyle(node).paddingRight;
+ // Trim the unit "px" from the output.
+ actualPaddingRight = actualPaddingRight.substring(0, actualPaddingRight.length - 2);
+ if (actualPaddingRight != expectedPaddingRight)
+ failures.push("Expected " + expectedPaddingRight + " for padding-right, but got " + actualPaddingRight + ". ");
+ }
+
+ var expectedMarginTop = checkAttribute(output, node, "data-expected-margin-top");
+ if (expectedMarginTop) {
+ var actualMarginTop = getComputedStyle(node).marginTop;
+ // Trim the unit "px" from the output.
+ actualMarginTop = actualMarginTop.substring(0, actualMarginTop.length - 2);
+ if (actualMarginTop != expectedMarginTop)
+ failures.push("Expected " + expectedMarginTop + " for margin-top, but got " + actualMarginTop + ". ");
+ }
+
+ var expectedMarginBottom = checkAttribute(output, node, "data-expected-margin-bottom");
+ if (expectedMarginBottom) {
+ var actualMarginBottom = getComputedStyle(node).marginBottom;
+ // Trim the unit "px" from the output.
+ actualMarginBottom = actualMarginBottom.substring(0, actualMarginBottom.length - 2);
+ if (actualMarginBottom != expectedMarginBottom)
+ failures.push("Expected " + expectedMarginBottom + " for margin-bottom, but got " + actualMarginBottom + ". ");
+ }
+
+ var expectedMarginLeft = checkAttribute(output, node, "data-expected-margin-left");
+ if (expectedMarginLeft) {
+ var actualMarginLeft = getComputedStyle(node).marginLeft;
+ // Trim the unit "px" from the output.
+ actualMarginLeft = actualMarginLeft.substring(0, actualMarginLeft.length - 2);
+ if (actualMarginLeft != expectedMarginLeft)
+ failures.push("Expected " + expectedMarginLeft + " for margin-left, but got " + actualMarginLeft + ". ");
+ }
+
+ var expectedMarginRight = checkAttribute(output, node, "data-expected-margin-right");
+ if (expectedMarginRight) {
+ var actualMarginRight = getComputedStyle(node).marginRight;
+ // Trim the unit "px" from the output.
+ actualMarginRight = actualMarginRight.substring(0, actualMarginRight.length - 2);
+ if (actualMarginRight != expectedMarginRight)
+ failures.push("Expected " + expectedMarginRight + " for margin-right, but got " + actualMarginRight + ". ");
+ }
+
+ return output.checked;
+}
+
+window.checkLayout = function(selectorList, outputContainer)
+{
+ var result = true;
+ if (!selectorList) {
+ document.body.appendChild(document.createTextNode("You must provide a CSS selector of nodes to check."));
+ return;
+ }
+ var nodes = document.querySelectorAll(selectorList);
+ nodes = Array.prototype.slice.call(nodes);
+ nodes.reverse();
+ var checkedLayout = false;
+ Array.prototype.forEach.call(nodes, function(node) {
+ var failures = [];
+ checkedLayout |= checkExpectedValues(node.parentNode, failures);
+ checkedLayout |= checkSubtreeExpectedValues(node, failures);
+
+ var container = node.parentNode.className == 'container' ? node.parentNode : node;
+
+ var pre = document.createElement('pre');
+ if (failures.length) {
+ pre.className = 'FAIL';
+ result = false;
+ }
+ pre.appendChild(document.createTextNode(failures.length ? "FAIL:\n" + failures.join('\n') + '\n\n' + container.outerHTML : "PASS"));
+
+ var referenceNode = container;
+ if (outputContainer) {
+ if (!outputContainer.lastChild) {
+ // Inserting a text node so we have something to insertAfter.
+ outputContainer.textContent = " ";
+ }
+ referenceNode = outputContainer.lastChild;
+ }
+ insertAfter(pre, referenceNode);
+ });
+
+ if (!checkedLayout) {
+ document.body.appendChild(document.createTextNode("FAIL: No valid data-* attributes found in selector list : " + selectorList));
+ return false;
+ }
+
+ return result;
+}
+
+})();
diff --git a/test/wpt/tests/resources/chromium/README.md b/test/wpt/tests/resources/chromium/README.md
new file mode 100644
index 0000000..be090b3
--- /dev/null
+++ b/test/wpt/tests/resources/chromium/README.md
@@ -0,0 +1,7 @@
+This directory contains Chromium-specific test resources, including mocks for
+test-only APIs implemented with
+[MojoJS](https://chromium.googlesource.com/chromium/src/+/main/mojo/public/js/README.md).
+
+Please do **not** copy `*.mojom.m.js` into this directory. Follow this doc if you
+want to add new MojoJS-backed mocks:
+https://chromium.googlesource.com/chromium/src/+/main/docs/testing/web_platform_tests.md#mojojs
diff --git a/test/wpt/tests/resources/chromium/contacts_manager_mock.js b/test/wpt/tests/resources/chromium/contacts_manager_mock.js
new file mode 100644
index 0000000..0496852
--- /dev/null
+++ b/test/wpt/tests/resources/chromium/contacts_manager_mock.js
@@ -0,0 +1,90 @@
+// Copyright 2018 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import {ContactsManager, ContactsManagerReceiver} from '/gen/third_party/blink/public/mojom/contacts/contacts_manager.mojom.m.js';
+
+self.WebContactsTest = (() => {
+ class MockContacts {
+ constructor() {
+ this.receiver_ = new ContactsManagerReceiver(this);
+
+ this.interceptor_ =
+ new MojoInterfaceInterceptor(ContactsManager.$interfaceName);
+ this.interceptor_.oninterfacerequest =
+ e => this.receiver_.$.bindHandle(e.handle);
+ this.interceptor_.start();
+
+ this.selectedContacts_ = [];
+ }
+
+ formatAddress_(address) {
+ // These are all required fields in the mojo definition.
+ return {
+ country: address.country || '',
+ addressLine: address.addressLine || [],
+ region: address.region || '',
+ city: address.city || '',
+ dependentLocality: address.dependentLocality || '',
+ postalCode: address.postCode || '',
+ sortingCode: address.sortingCode || '',
+ organization: address.organization || '',
+ recipient: address.recipient || '',
+ phone: address.phone || '',
+ };
+ }
+
+ async select(multiple, includeNames, includeEmails, includeTel, includeAddresses, includeIcons) {
+ if (this.selectedContacts_ === null)
+ return {contacts: null};
+
+ const contactInfos = await Promise.all(this.selectedContacts_.map(async contact => {
+ const contactInfo = {};
+ if (includeNames)
+ contactInfo.name = contact.name || [];
+ if (includeEmails)
+ contactInfo.email = contact.email || [];
+ if (includeTel)
+ contactInfo.tel = contact.tel || [];
+ if (includeAddresses) {
+ contactInfo.address = (contact.address || []).map(address => this.formatAddress_(address));
+ }
+ if (includeIcons) {
+ contactInfo.icon = await Promise.all(
+ (contact.icon || []).map(async blob => ({
+ mimeType: blob.type,
+ data: (await blob.text()).split('').map(s => s.charCodeAt(0)),
+ })));
+ }
+ return contactInfo;
+ }));
+
+ if (!contactInfos.length) return {contacts: []};
+ if (!multiple) return {contacts: [contactInfos[0]]};
+ return {contacts: contactInfos};
+ }
+
+ setSelectedContacts(contacts) {
+ this.selectedContacts_ = contacts;
+ }
+
+ reset() {
+ this.receiver_.$.close();
+ this.interceptor_.stop();
+ }
+ }
+
+ const mockContacts = new MockContacts();
+
+ class ContactsTestChromium {
+ constructor() {
+ Object.freeze(this); // Make it immutable.
+ }
+
+ setSelectedContacts(contacts) {
+ mockContacts.setSelectedContacts(contacts);
+ }
+ }
+
+ return ContactsTestChromium;
+})();
diff --git a/test/wpt/tests/resources/chromium/content-index-helpers.js b/test/wpt/tests/resources/chromium/content-index-helpers.js
new file mode 100644
index 0000000..936fe84
--- /dev/null
+++ b/test/wpt/tests/resources/chromium/content-index-helpers.js
@@ -0,0 +1,9 @@
+import {ContentIndexService} from '/gen/third_party/blink/public/mojom/content_index/content_index.mojom.m.js';
+
+// Returns a promise if the chromium based browser fetches icons for
+// content-index.
+export async function fetchesIcons() {
+ const remote = ContentIndexService.getRemote();
+ const {iconSizes} = await remote.getIconSizes();
+ return iconSizes.length > 0;
+};
diff --git a/test/wpt/tests/resources/chromium/enable-hyperlink-auditing.js b/test/wpt/tests/resources/chromium/enable-hyperlink-auditing.js
new file mode 100644
index 0000000..263f651
--- /dev/null
+++ b/test/wpt/tests/resources/chromium/enable-hyperlink-auditing.js
@@ -0,0 +1,2 @@
+if (window.testRunner)
+ testRunner.overridePreference("WebKitHyperlinkAuditingEnabled", 1);
diff --git a/test/wpt/tests/resources/chromium/fake-hid.js b/test/wpt/tests/resources/chromium/fake-hid.js
new file mode 100644
index 0000000..70a0149
--- /dev/null
+++ b/test/wpt/tests/resources/chromium/fake-hid.js
@@ -0,0 +1,297 @@
+import {HidConnectionReceiver, HidDeviceInfo} from '/gen/services/device/public/mojom/hid.mojom.m.js';
+import {HidService, HidServiceReceiver} from '/gen/third_party/blink/public/mojom/hid/hid.mojom.m.js';
+
+// Fake implementation of device.mojom.HidConnection. HidConnection represents
+// an open connection to a HID device and can be used to send and receive
+// reports.
+class FakeHidConnection {
+ constructor(client) {
+ this.client_ = client;
+ this.receiver_ = new HidConnectionReceiver(this);
+ this.expectedWrites_ = [];
+ this.expectedGetFeatureReports_ = [];
+ this.expectedSendFeatureReports_ = [];
+ }
+
+ bindNewPipeAndPassRemote() {
+ return this.receiver_.$.bindNewPipeAndPassRemote();
+ }
+
+ // Simulate an input report sent from the device to the host. The connection
+ // client's onInputReport method will be called with the provided |reportId|
+ // and |buffer|.
+ simulateInputReport(reportId, reportData) {
+ if (this.client_) {
+ this.client_.onInputReport(reportId, reportData);
+ }
+ }
+
+ // Specify the result for an expected call to write. If |success| is true the
+ // write will be successful, otherwise it will simulate a failure. The
+ // parameters of the next write call must match |reportId| and |buffer|.
+ queueExpectedWrite(success, reportId, reportData) {
+ this.expectedWrites_.push({
+ params: {reportId, data: reportData},
+ result: {success},
+ });
+ }
+
+ // Specify the result for an expected call to getFeatureReport. If |success|
+ // is true the operation is successful, otherwise it will simulate a failure.
+ // The parameter of the next getFeatureReport call must match |reportId|.
+ queueExpectedGetFeatureReport(success, reportId, reportData) {
+ this.expectedGetFeatureReports_.push({
+ params: {reportId},
+ result: {success, buffer: reportData},
+ });
+ }
+
+ // Specify the result for an expected call to sendFeatureReport. If |success|
+ // is true the operation is successful, otherwise it will simulate a failure.
+ // The parameters of the next sendFeatureReport call must match |reportId| and
+ // |buffer|.
+ queueExpectedSendFeatureReport(success, reportId, reportData) {
+ this.expectedSendFeatureReports_.push({
+ params: {reportId, data: reportData},
+ result: {success},
+ });
+ }
+
+ // Asserts that there are no more expected operations.
+ assertExpectationsMet() {
+ assert_equals(this.expectedWrites_.length, 0);
+ assert_equals(this.expectedGetFeatureReports_.length, 0);
+ assert_equals(this.expectedSendFeatureReports_.length, 0);
+ }
+
+ read() {}
+
+ // Implementation of HidConnection::Write. Causes an assertion failure if
+ // there are no expected write operations, or if the parameters do not match
+ // the expected call.
+ async write(reportId, buffer) {
+ let expectedWrite = this.expectedWrites_.shift();
+ assert_not_equals(expectedWrite, undefined);
+ assert_equals(reportId, expectedWrite.params.reportId);
+ let actual = new Uint8Array(buffer);
+ compareDataViews(
+ new DataView(actual.buffer, actual.byteOffset),
+ new DataView(
+ expectedWrite.params.data.buffer,
+ expectedWrite.params.data.byteOffset));
+ return expectedWrite.result;
+ }
+
+ // Implementation of HidConnection::GetFeatureReport. Causes an assertion
+ // failure if there are no expected write operations, or if the parameters do
+ // not match the expected call.
+ async getFeatureReport(reportId) {
+ let expectedGetFeatureReport = this.expectedGetFeatureReports_.shift();
+ assert_not_equals(expectedGetFeatureReport, undefined);
+ assert_equals(reportId, expectedGetFeatureReport.params.reportId);
+ return expectedGetFeatureReport.result;
+ }
+
+ // Implementation of HidConnection::SendFeatureReport. Causes an assertion
+ // failure if there are no expected write operations, or if the parameters do
+ // not match the expected call.
+ async sendFeatureReport(reportId, buffer) {
+ let expectedSendFeatureReport = this.expectedSendFeatureReports_.shift();
+ assert_not_equals(expectedSendFeatureReport, undefined);
+ assert_equals(reportId, expectedSendFeatureReport.params.reportId);
+ let actual = new Uint8Array(buffer);
+ compareDataViews(
+ new DataView(actual.buffer, actual.byteOffset),
+ new DataView(
+ expectedSendFeatureReport.params.data.buffer,
+ expectedSendFeatureReport.params.data.byteOffset));
+ return expectedSendFeatureReport.result;
+ }
+}
+
+
+// A fake implementation of the HidService mojo interface. HidService manages
+// HID device access for clients in the render process. Typically, when a client
+// requests access to a HID device a chooser dialog is shown with a list of
+// available HID devices. Selecting a device from the chooser also grants
+// permission for the client to access that device.
+//
+// The fake implementation allows tests to simulate connected devices. It also
+// skips the chooser dialog and instead allows tests to specify which device
+// should be selected. All devices are treated as if the user had already
+// granted permission. It is possible to revoke permission with forget() later.
+class FakeHidService {
+ constructor() {
+ this.interceptor_ = new MojoInterfaceInterceptor(HidService.$interfaceName);
+ this.interceptor_.oninterfacerequest = e => this.bind(e.handle);
+ this.receiver_ = new HidServiceReceiver(this);
+ this.nextGuidValue_ = 0;
+ this.simulateConnectFailure_ = false;
+ this.reset();
+ }
+
+ start() {
+ this.interceptor_.start();
+ }
+
+ stop() {
+ this.interceptor_.stop();
+ }
+
+ reset() {
+ this.devices_ = new Map();
+ this.allowedDevices_ = new Map();
+ this.fakeConnections_ = new Map();
+ this.selectedDevices_ = [];
+ }
+
+ // Creates and returns a HidDeviceInfo with the specified device IDs.
+ makeDevice(vendorId, productId) {
+ let guidValue = ++this.nextGuidValue_;
+ let info = new HidDeviceInfo();
+ info.guid = 'guid-' + guidValue.toString();
+ info.physicalDeviceId = 'physical-device-id-' + guidValue.toString();
+ info.vendorId = vendorId;
+ info.productId = productId;
+ info.productName = 'product name';
+ info.serialNumber = '0';
+ info.reportDescriptor = new Uint8Array();
+ info.collections = [];
+ info.deviceNode = 'device node';
+ return info;
+ }
+
+ // Simulates a connected device the client has already been granted permission
+ // to. Returns the key used to store the device in the map. The key is either
+ // the physical device ID, or the device GUID if it has no physical device ID.
+ addDevice(deviceInfo, grantPermission = true) {
+ let key = deviceInfo.physicalDeviceId;
+ if (key.length === 0)
+ key = deviceInfo.guid;
+
+ let devices = this.devices_.get(key) || [];
+ devices.push(deviceInfo);
+ this.devices_.set(key, devices);
+
+ if (grantPermission) {
+ let allowedDevices = this.allowedDevices_.get(key) || [];
+ allowedDevices.push(deviceInfo);
+ this.allowedDevices_.set(key, allowedDevices);
+ }
+
+ if (this.client_)
+ this.client_.deviceAdded(deviceInfo);
+ return key;
+ }
+
+ // Simulates disconnecting a connected device.
+ removeDevice(key) {
+ let devices = this.devices_.get(key);
+ this.devices_.delete(key);
+ if (this.client_ && devices) {
+ devices.forEach(deviceInfo => {
+ this.client_.deviceRemoved(deviceInfo);
+ });
+ }
+ }
+
+ // Simulates updating the device information for a connected device.
+ changeDevice(deviceInfo) {
+ let key = deviceInfo.physicalDeviceId;
+ if (key.length === 0)
+ key = deviceInfo.guid;
+
+ let devices = this.devices_.get(key) || [];
+ let i = devices.length;
+ while (i--) {
+ if (devices[i].guid == deviceInfo.guid)
+ devices.splice(i, 1);
+ }
+ devices.push(deviceInfo);
+ this.devices_.set(key, devices);
+
+ let allowedDevices = this.allowedDevices_.get(key) || [];
+ let j = allowedDevices.length;
+ while (j--) {
+ if (allowedDevices[j].guid == deviceInfo.guid)
+ allowedDevices.splice(j, 1);
+ }
+ allowedDevices.push(deviceInfo);
+ this.allowedDevices_.set(key, allowedDevices);
+
+ if (this.client_)
+ this.client_.deviceChanged(deviceInfo);
+ return key;
+ }
+
+ // Sets a flag that causes the next call to connect() to fail.
+ simulateConnectFailure() {
+ this.simulateConnectFailure_ = true;
+ }
+
+ // Sets the key of the device that will be returned as the selected item the
+ // next time requestDevice is called. The device with this key must have been
+ // previously added with addDevice.
+ setSelectedDevice(key) {
+ this.selectedDevices_ = this.devices_.get(key);
+ }
+
+ // Returns the fake HidConnection object for this device, if there is one. A
+ // connection is created once the device is opened.
+ getFakeConnection(guid) {
+ return this.fakeConnections_.get(guid);
+ }
+
+ bind(handle) {
+ this.receiver_.$.bindHandle(handle);
+ }
+
+ registerClient(client) {
+ this.client_ = client;
+ }
+
+ // Returns an array of connected devices the client has already been granted
+ // permission to access.
+ async getDevices() {
+ let devices = [];
+ this.allowedDevices_.forEach((value) => {
+ devices = devices.concat(value);
+ });
+ return {devices};
+ }
+
+ // Simulates a device chooser prompt, returning |selectedDevices_| as the
+ // simulated selection. |options| is ignored.
+ async requestDevice(options) {
+ return {devices: this.selectedDevices_};
+ }
+
+ // Returns a fake connection to the device with the specified GUID. If
+ // |connectionClient| is not null, its onInputReport method will be called
+ // when input reports are received. If simulateConnectFailure() was called
+ // then a null connection is returned instead, indicating failure.
+ async connect(guid, connectionClient) {
+ if (this.simulateConnectFailure_) {
+ this.simulateConnectFailure_ = false;
+ return {connection: null};
+ }
+ const fakeConnection = new FakeHidConnection(connectionClient);
+ this.fakeConnections_.set(guid, fakeConnection);
+ return {connection: fakeConnection.bindNewPipeAndPassRemote()};
+ }
+
+ // Removes the allowed device.
+ async forget(deviceInfo) {
+ for (const [key, value] of this.allowedDevices_) {
+ for (const device of value) {
+ if (device.guid == deviceInfo.guid) {
+ this.allowedDevices_.delete(key);
+ break;
+ }
+ }
+ }
+ return {success: true};
+ }
+}
+
+export const fakeHidService = new FakeHidService();
diff --git a/test/wpt/tests/resources/chromium/fake-serial.js b/test/wpt/tests/resources/chromium/fake-serial.js
new file mode 100644
index 0000000..e1e4d57
--- /dev/null
+++ b/test/wpt/tests/resources/chromium/fake-serial.js
@@ -0,0 +1,443 @@
+import {SerialPortFlushMode, SerialPortRemote, SerialReceiveError, SerialPortReceiver, SerialSendError} from '/gen/services/device/public/mojom/serial.mojom.m.js';
+import {SerialService, SerialServiceReceiver} from '/gen/third_party/blink/public/mojom/serial/serial.mojom.m.js';
+
+// Implementation of an UnderlyingSource to create a ReadableStream from a Mojo
+// data pipe consumer handle.
+class DataPipeSource {
+ constructor(consumer) {
+ this.consumer_ = consumer;
+ }
+
+ async pull(controller) {
+ let chunk = new ArrayBuffer(64);
+ let {result, numBytes} = this.consumer_.readData(chunk);
+ if (result == Mojo.RESULT_OK) {
+ controller.enqueue(new Uint8Array(chunk, 0, numBytes));
+ return;
+ } else if (result == Mojo.RESULT_FAILED_PRECONDITION) {
+ controller.close();
+ return;
+ } else if (result == Mojo.RESULT_SHOULD_WAIT) {
+ await this.readable();
+ return this.pull(controller);
+ }
+ }
+
+ cancel() {
+ if (this.watcher_)
+ this.watcher_.cancel();
+ this.consumer_.close();
+ }
+
+ readable() {
+ return new Promise((resolve) => {
+ this.watcher_ =
+ this.consumer_.watch({ readable: true, peerClosed: true }, () => {
+ this.watcher_.cancel();
+ this.watcher_ = undefined;
+ resolve();
+ });
+ });
+ }
+}
+
+// Implementation of an UnderlyingSink to create a WritableStream from a Mojo
+// data pipe producer handle.
+class DataPipeSink {
+ constructor(producer) {
+ this._producer = producer;
+ }
+
+ async write(chunk, controller) {
+ while (true) {
+ let {result, numBytes} = this._producer.writeData(chunk);
+ if (result == Mojo.RESULT_OK) {
+ if (numBytes == chunk.byteLength) {
+ return;
+ }
+ chunk = chunk.slice(numBytes);
+ } else if (result == Mojo.RESULT_FAILED_PRECONDITION) {
+ throw new DOMException('The pipe is closed.', 'InvalidStateError');
+ } else if (result == Mojo.RESULT_SHOULD_WAIT) {
+ await this.writable();
+ }
+ }
+ }
+
+ close() {
+ assert_equals(undefined, this._watcher);
+ this._producer.close();
+ }
+
+ abort(reason) {
+ if (this._watcher)
+ this._watcher.cancel();
+ this._producer.close();
+ }
+
+ writable() {
+ return new Promise((resolve) => {
+ this._watcher =
+ this._producer.watch({ writable: true, peerClosed: true }, () => {
+ this._watcher.cancel();
+ this._watcher = undefined;
+ resolve();
+ });
+ });
+ }
+}
+
+// Implementation of device.mojom.SerialPort.
+class FakeSerialPort {
+ constructor() {
+ this.inputSignals_ = {
+ dataCarrierDetect: false,
+ clearToSend: false,
+ ringIndicator: false,
+ dataSetReady: false
+ };
+ this.inputSignalFailure_ = false;
+ this.outputSignals_ = {
+ dataTerminalReady: false,
+ requestToSend: false,
+ break: false
+ };
+ this.outputSignalFailure_ = false;
+ }
+
+ open(options, client) {
+ if (this.receiver_ !== undefined) {
+ // Port already open.
+ return null;
+ }
+
+ let port = new SerialPortRemote();
+ this.receiver_ = new SerialPortReceiver(this);
+ this.receiver_.$.bindHandle(port.$.bindNewPipeAndPassReceiver().handle);
+
+ this.options_ = options;
+ this.client_ = client;
+ // OS typically sets DTR on open.
+ this.outputSignals_.dataTerminalReady = true;
+
+ return port;
+ }
+
+ write(data) {
+ return this.writer_.write(data);
+ }
+
+ read() {
+ return this.reader_.read();
+ }
+
+ // Reads from the port until at least |targetLength| is read or the stream is
+ // closed. The data is returned as a combined Uint8Array.
+ readWithLength(targetLength) {
+ return readWithLength(this.reader_, targetLength);
+ }
+
+ simulateReadError(error) {
+ this.writer_.close();
+ this.writer_.releaseLock();
+ this.writer_ = undefined;
+ this.writable_ = undefined;
+ this.client_.onReadError(error);
+ }
+
+ simulateParityError() {
+ this.simulateReadError(SerialReceiveError.PARITY_ERROR);
+ }
+
+ simulateDisconnectOnRead() {
+ this.simulateReadError(SerialReceiveError.DISCONNECTED);
+ }
+
+ simulateWriteError(error) {
+ this.reader_.cancel();
+ this.reader_ = undefined;
+ this.readable_ = undefined;
+ this.client_.onSendError(error);
+ }
+
+ simulateSystemErrorOnWrite() {
+ this.simulateWriteError(SerialSendError.SYSTEM_ERROR);
+ }
+
+ simulateDisconnectOnWrite() {
+ this.simulateWriteError(SerialSendError.DISCONNECTED);
+ }
+
+ simulateInputSignals(signals) {
+ this.inputSignals_ = signals;
+ }
+
+ simulateInputSignalFailure(fail) {
+ this.inputSignalFailure_ = fail;
+ }
+
+ get outputSignals() {
+ return this.outputSignals_;
+ }
+
+ simulateOutputSignalFailure(fail) {
+ this.outputSignalFailure_ = fail;
+ }
+
+ writable() {
+ if (this.writable_)
+ return Promise.resolve();
+
+ if (!this.writablePromise_) {
+ this.writablePromise_ = new Promise((resolve) => {
+ this.writableResolver_ = resolve;
+ });
+ }
+
+ return this.writablePromise_;
+ }
+
+ readable() {
+ if (this.readable_)
+ return Promise.resolve();
+
+ if (!this.readablePromise_) {
+ this.readablePromise_ = new Promise((resolve) => {
+ this.readableResolver_ = resolve;
+ });
+ }
+
+ return this.readablePromise_;
+ }
+
+ async startWriting(in_stream) {
+ this.readable_ = new ReadableStream(new DataPipeSource(in_stream));
+ this.reader_ = this.readable_.getReader();
+ if (this.readableResolver_) {
+ this.readableResolver_();
+ this.readableResolver_ = undefined;
+ this.readablePromise_ = undefined;
+ }
+ }
+
+ async startReading(out_stream) {
+ this.writable_ = new WritableStream(new DataPipeSink(out_stream));
+ this.writer_ = this.writable_.getWriter();
+ if (this.writableResolver_) {
+ this.writableResolver_();
+ this.writableResolver_ = undefined;
+ this.writablePromise_ = undefined;
+ }
+ }
+
+ async flush(mode) {
+ switch (mode) {
+ case SerialPortFlushMode.kReceive:
+ this.writer_.abort();
+ this.writer_.releaseLock();
+ this.writer_ = undefined;
+ this.writable_ = undefined;
+ break;
+ case SerialPortFlushMode.kTransmit:
+ this.reader_.cancel();
+ this.reader_ = undefined;
+ this.readable_ = undefined;
+ break;
+ }
+ }
+
+ async drain() {
+ await this.reader_.closed;
+ }
+
+ async getControlSignals() {
+ if (this.inputSignalFailure_) {
+ return {signals: null};
+ }
+
+ const signals = {
+ dcd: this.inputSignals_.dataCarrierDetect,
+ cts: this.inputSignals_.clearToSend,
+ ri: this.inputSignals_.ringIndicator,
+ dsr: this.inputSignals_.dataSetReady
+ };
+ return {signals};
+ }
+
+ async setControlSignals(signals) {
+ if (this.outputSignalFailure_) {
+ return {success: false};
+ }
+
+ if (signals.hasDtr) {
+ this.outputSignals_.dataTerminalReady = signals.dtr;
+ }
+ if (signals.hasRts) {
+ this.outputSignals_.requestToSend = signals.rts;
+ }
+ if (signals.hasBrk) {
+ this.outputSignals_.break = signals.brk;
+ }
+ return { success: true };
+ }
+
+ async configurePort(options) {
+ this.options_ = options;
+ return { success: true };
+ }
+
+ async getPortInfo() {
+ return {
+ bitrate: this.options_.bitrate,
+ dataBits: this.options_.datBits,
+ parityBit: this.options_.parityBit,
+ stopBits: this.options_.stopBits,
+ ctsFlowControl:
+ this.options_.hasCtsFlowControl && this.options_.ctsFlowControl,
+ };
+ }
+
+ async close() {
+ // OS typically clears DTR on close.
+ this.outputSignals_.dataTerminalReady = false;
+ if (this.writer_) {
+ this.writer_.close();
+ this.writer_.releaseLock();
+ this.writer_ = undefined;
+ }
+ this.writable_ = undefined;
+
+ // Close the receiver asynchronously so the reply to this message can be
+ // sent first.
+ const receiver = this.receiver_;
+ this.receiver_ = undefined;
+ setTimeout(() => {
+ receiver.$.close();
+ }, 0);
+
+ return {};
+ }
+}
+
+// Implementation of blink.mojom.SerialService.
+class FakeSerialService {
+ constructor() {
+ this.interceptor_ =
+ new MojoInterfaceInterceptor(SerialService.$interfaceName);
+ this.interceptor_.oninterfacerequest = e => this.bind(e.handle);
+ this.receiver_ = new SerialServiceReceiver(this);
+ this.clients_ = [];
+ this.nextToken_ = 0;
+ this.reset();
+ }
+
+ start() {
+ this.interceptor_.start();
+ }
+
+ stop() {
+ this.interceptor_.stop();
+ }
+
+ reset() {
+ this.ports_ = new Map();
+ this.selectedPort_ = null;
+ }
+
+ addPort(info) {
+ let portInfo = {};
+ if (info?.usbVendorId !== undefined) {
+ portInfo.hasUsbVendorId = true;
+ portInfo.usbVendorId = info.usbVendorId;
+ }
+ if (info?.usbProductId !== undefined) {
+ portInfo.hasUsbProductId = true;
+ portInfo.usbProductId = info.usbProductId;
+ }
+
+ let token = ++this.nextToken_;
+ portInfo.token = {high: 0n, low: BigInt(token)};
+
+ let record = {
+ portInfo: portInfo,
+ fakePort: new FakeSerialPort(),
+ };
+ this.ports_.set(token, record);
+
+ for (let client of this.clients_) {
+ client.onPortAdded(portInfo);
+ }
+
+ return token;
+ }
+
+ removePort(token) {
+ let record = this.ports_.get(token);
+ if (record === undefined) {
+ return;
+ }
+
+ this.ports_.delete(token);
+
+ for (let client of this.clients_) {
+ client.onPortRemoved(record.portInfo);
+ }
+ }
+
+ setSelectedPort(token) {
+ this.selectedPort_ = this.ports_.get(token);
+ }
+
+ getFakePort(token) {
+ let record = this.ports_.get(token);
+ if (record === undefined)
+ return undefined;
+ return record.fakePort;
+ }
+
+ bind(handle) {
+ this.receiver_.$.bindHandle(handle);
+ }
+
+ async setClient(client_remote) {
+ this.clients_.push(client_remote);
+ }
+
+ async getPorts() {
+ return {
+ ports: Array.from(this.ports_, ([token, record]) => record.portInfo)
+ };
+ }
+
+ async requestPort(filters) {
+ if (this.selectedPort_)
+ return { port: this.selectedPort_.portInfo };
+ else
+ return { port: null };
+ }
+
+ async openPort(token, options, client) {
+ let record = this.ports_.get(Number(token.low));
+ if (record !== undefined) {
+ return {port: record.fakePort.open(options, client)};
+ } else {
+ return {port: null};
+ }
+ }
+
+ async forgetPort(token) {
+ let record = this.ports_.get(Number(token.low));
+ if (record === undefined) {
+ return {success: false};
+ }
+
+ this.ports_.delete(Number(token.low));
+ if (record.fakePort.receiver_) {
+ record.fakePort.receiver_.$.close();
+ record.fakePort.receiver_ = undefined;
+ }
+ return {success: true};
+ }
+}
+
+export const fakeSerialService = new FakeSerialService();
diff --git a/test/wpt/tests/resources/chromium/generic_sensor_mocks.js b/test/wpt/tests/resources/chromium/generic_sensor_mocks.js
new file mode 100644
index 0000000..98a29c2
--- /dev/null
+++ b/test/wpt/tests/resources/chromium/generic_sensor_mocks.js
@@ -0,0 +1,519 @@
+import {ReportingMode, Sensor, SensorClientRemote, SensorReceiver, SensorRemote, SensorType} from '/gen/services/device/public/mojom/sensor.mojom.m.js';
+import {SensorCreationResult, SensorInitParams_READ_BUFFER_SIZE_FOR_TESTS, SensorProvider, SensorProviderReceiver} from '/gen/services/device/public/mojom/sensor_provider.mojom.m.js';
+
+// A "sliding window" that iterates over |data| and returns one item at a
+// time, advancing and wrapping around as needed. |data| must be an array of
+// arrays.
+self.RingBuffer = class {
+ constructor(data) {
+ this.bufferPosition_ = 0;
+ // Validate |data|'s format and deep-copy every element.
+ this.data_ = Array.from(data, element => {
+ if (!Array.isArray(element)) {
+ throw new TypeError('Every |data| element must be an array.');
+ }
+ return Array.from(element);
+ })
+ }
+
+ next() {
+ const value = this.data_[this.bufferPosition_];
+ this.bufferPosition_ = (this.bufferPosition_ + 1) % this.data_.length;
+ return { done: false, value: value };
+ }
+
+ value() {
+ return this.data_[this.bufferPosition_];
+ }
+
+ [Symbol.iterator]() {
+ return this;
+ }
+};
+
+class DefaultSensorTraits {
+ // https://w3c.github.io/sensors/#threshold-check-algorithm
+ static isSignificantlyDifferent(reading1, reading2) {
+ return true;
+ }
+
+ // https://w3c.github.io/sensors/#reading-quantization-algorithm
+ static roundToMultiple(reading) {
+ return reading;
+ }
+
+ // https://w3c.github.io/ambient-light/#ambient-light-threshold-check-algorithm
+ static areReadingsEqual(reading1, reading2) {
+ return false;
+ }
+}
+
+class AmbientLightSensorTraits extends DefaultSensorTraits {
+ // https://w3c.github.io/ambient-light/#reduce-sensor-accuracy
+ static #ROUNDING_MULTIPLE = 50;
+ static #SIGNIFICANCE_THRESHOLD = 25;
+
+ // https://w3c.github.io/ambient-light/#ambient-light-threshold-check-algorithm
+ static isSignificantlyDifferent([illuminance1], [illuminance2]) {
+ return Math.abs(illuminance1 - illuminance2) >=
+ this.#SIGNIFICANCE_THRESHOLD;
+ }
+
+ // https://w3c.github.io/ambient-light/#ambient-light-reading-quantization-algorithm
+ static roundToMultiple(reading) {
+ const illuminance = reading[0];
+ const scaledValue =
+ illuminance / AmbientLightSensorTraits.#ROUNDING_MULTIPLE;
+ let roundedReading = reading.splice();
+
+ if (illuminance < 0.0) {
+ roundedReading[0] = -AmbientLightSensorTraits.#ROUNDING_MULTIPLE *
+ Math.floor(-scaledValue + 0.5);
+ } else {
+ roundedReading[0] = AmbientLightSensorTraits.#ROUNDING_MULTIPLE *
+ Math.floor(scaledValue + 0.5);
+ }
+
+ return roundedReading;
+ }
+
+ // https://w3c.github.io/ambient-light/#ambient-light-threshold-check-algorithm
+ static areReadingsEqual([illuminance1], [illuminance2]) {
+ return illuminance1 === illuminance2;
+ }
+}
+
+self.GenericSensorTest = (() => {
+ // Default sensor frequency in default configurations.
+ const DEFAULT_FREQUENCY = 5;
+
+ // Class that mocks Sensor interface defined in
+ // https://cs.chromium.org/chromium/src/services/device/public/mojom/sensor.mojom
+ class MockSensor {
+ static #BUFFER_OFFSET_TIMESTAMP = 1;
+ static #BUFFER_OFFSET_READINGS = 2;
+
+ constructor(sensorRequest, buffer, reportingMode, sensorType) {
+ this.client_ = null;
+ this.startShouldFail_ = false;
+ this.notifyOnReadingChange_ = true;
+ this.reportingMode_ = reportingMode;
+ this.sensorType_ = sensorType;
+ this.sensorReadingTimerId_ = null;
+ this.readingData_ = null;
+ this.requestedFrequencies_ = [];
+ // The Blink implementation (third_party/blink/renderer/modules/sensor/sensor.cc)
+ // sets a timestamp by creating a DOMHighResTimeStamp from a given platform timestamp.
+ // In this mock implementation we use a starting value
+ // and an increment step value that resemble a platform timestamp reasonably enough.
+ this.timestamp_ = window.performance.timeOrigin;
+ // |buffer| represents a SensorReadingSharedBuffer on the C++ side in
+ // Chromium. It consists, in this order, of a
+ // SensorReadingField<OneWriterSeqLock> (an 8-byte union that includes
+ // 32-bit integer used by the lock class), and a SensorReading consisting
+ // of an 8-byte timestamp and 4 8-byte reading fields.
+ //
+ // |this.buffer_[0]| is zeroed by default, which allows OneWriterSeqLock
+ // to work with our custom memory buffer that did not actually create a
+ // OneWriterSeqLock instance. It is never changed manually here.
+ //
+ // Use MockSensor.#BUFFER_OFFSET_TIMESTAMP and
+ // MockSensor.#BUFFER_OFFSET_READINGS to access the other positions in
+ // |this.buffer_| without having to hardcode magic numbers in the code.
+ this.buffer_ = buffer;
+ this.buffer_.fill(0);
+ this.receiver_ = new SensorReceiver(this);
+ this.receiver_.$.bindHandle(sensorRequest.handle);
+ this.lastRawReading_ = null;
+ this.lastRoundedReading_ = null;
+
+ if (sensorType == SensorType.AMBIENT_LIGHT) {
+ this.sensorTraits = AmbientLightSensorTraits;
+ } else {
+ this.sensorTraits = DefaultSensorTraits;
+ }
+ }
+
+ // Returns default configuration.
+ async getDefaultConfiguration() {
+ return { frequency: DEFAULT_FREQUENCY };
+ }
+
+ // Adds configuration for the sensor and starts reporting fake data
+ // through setSensorReading function.
+ async addConfiguration(configuration) {
+ this.requestedFrequencies_.push(configuration.frequency);
+ // Sort using descending order.
+ this.requestedFrequencies_.sort(
+ (first, second) => { return second - first });
+
+ if (!this.startShouldFail_ )
+ this.startReading();
+
+ return { success: !this.startShouldFail_ };
+ }
+
+ // Removes sensor configuration from the list of active configurations and
+ // stops notification about sensor reading changes if
+ // requestedFrequencies_ is empty.
+ removeConfiguration(configuration) {
+ const index = this.requestedFrequencies_.indexOf(configuration.frequency);
+ if (index == -1)
+ return;
+
+ this.requestedFrequencies_.splice(index, 1);
+ if (this.requestedFrequencies_.length === 0)
+ this.stopReading();
+ }
+
+ // ConfigureReadingChangeNotifications(bool enabled)
+ // Configures whether to report a reading change when in ON_CHANGE
+ // reporting mode.
+ configureReadingChangeNotifications(notifyOnReadingChange) {
+ this.notifyOnReadingChange_ = notifyOnReadingChange;
+ }
+
+ resume() {
+ this.startReading();
+ }
+
+ suspend() {
+ this.stopReading();
+ }
+
+ // Mock functions
+
+ // Resets mock Sensor state.
+ reset() {
+ this.stopReading();
+ this.startShouldFail_ = false;
+ this.requestedFrequencies_ = [];
+ this.notifyOnReadingChange_ = true;
+ this.readingData_ = null;
+ this.buffer_.fill(0);
+ this.receiver_.$.close();
+ this.lastRawReading_ = null;
+ this.lastRoundedReading_ = null;
+ }
+
+ // Sets fake data that is used to deliver sensor reading updates.
+ setSensorReading(readingData) {
+ this.readingData_ = new RingBuffer(readingData);
+ }
+
+ // This is a workaround to accommodate Blink's Device Orientation
+ // implementation. In general, all tests should use setSensorReading()
+ // instead.
+ setSensorReadingImmediately(readingData) {
+ this.setSensorReading(readingData);
+
+ const reading = this.readingData_.value();
+ this.buffer_.set(reading, MockSensor.#BUFFER_OFFSET_READINGS);
+ this.buffer_[MockSensor.#BUFFER_OFFSET_TIMESTAMP] = this.timestamp_++;
+ }
+
+ // Sets flag that forces sensor to fail when addConfiguration is invoked.
+ setStartShouldFail(shouldFail) {
+ this.startShouldFail_ = shouldFail;
+ }
+
+ startReading() {
+ if (this.readingData_ != null) {
+ this.stopReading();
+ }
+ let maxFrequencyUsed = this.requestedFrequencies_[0];
+ let timeout = (1 / maxFrequencyUsed) * 1000;
+ this.sensorReadingTimerId_ = window.setInterval(() => {
+ if (this.readingData_) {
+ // |buffer_| is a TypedArray, so we need to make sure pass an
+ // array to set().
+ const reading = this.readingData_.next().value;
+ if (!Array.isArray(reading)) {
+ throw new TypeError("startReading(): The readings passed to " +
+ "setSensorReading() must be arrays");
+ }
+
+ if (this.reportingMode_ == ReportingMode.ON_CHANGE &&
+ this.lastRawReading_ !== null &&
+ !this.sensorTraits.isSignificantlyDifferent(
+ this.lastRawReading_, reading)) {
+ // In case new value is not significantly different compared to
+ // old value, new value is not sent.
+ return;
+ }
+
+ this.lastRawReading_ = reading.slice();
+ const roundedReading = this.sensorTraits.roundToMultiple(reading);
+
+ if (this.reportingMode_ == ReportingMode.ON_CHANGE &&
+ this.lastRoundedReading_ !== null &&
+ this.sensorTraits.areReadingsEqual(
+ roundedReading, this.lastRoundedReading_)) {
+ // In case new rounded value is not different compared to old
+ // value, new value is not sent.
+ return;
+ }
+ this.buffer_.set(roundedReading, MockSensor.#BUFFER_OFFSET_READINGS);
+ this.lastRoundedReading_ = roundedReading;
+ }
+
+ // For all tests sensor reading should have monotonically
+ // increasing timestamp.
+ this.buffer_[MockSensor.#BUFFER_OFFSET_TIMESTAMP] = this.timestamp_++;
+
+ if (this.reportingMode_ === ReportingMode.ON_CHANGE &&
+ this.notifyOnReadingChange_) {
+ this.client_.sensorReadingChanged();
+ }
+ }, timeout);
+ }
+
+ stopReading() {
+ if (this.sensorReadingTimerId_ != null) {
+ window.clearInterval(this.sensorReadingTimerId_);
+ this.sensorReadingTimerId_ = null;
+ }
+ }
+
+ getSamplingFrequency() {
+ if (this.requestedFrequencies_.length == 0) {
+ throw new Error("getSamplingFrequency(): No configured frequency");
+ }
+ return this.requestedFrequencies_[0];
+ }
+
+ isReadingData() {
+ return this.sensorReadingTimerId_ != null;
+ }
+ }
+
+ // Class that mocks SensorProvider interface defined in
+ // https://cs.chromium.org/chromium/src/services/device/public/mojom/sensor_provider.mojom
+ class MockSensorProvider {
+ constructor() {
+ this.readingSizeInBytes_ =
+ Number(SensorInitParams_READ_BUFFER_SIZE_FOR_TESTS);
+ this.sharedBufferSizeInBytes_ =
+ this.readingSizeInBytes_ * (SensorType.MAX_VALUE + 1);
+ let rv = Mojo.createSharedBuffer(this.sharedBufferSizeInBytes_);
+ if (rv.result != Mojo.RESULT_OK) {
+ throw new Error('MockSensorProvider: Failed to create shared buffer');
+ }
+ const handle = rv.handle;
+ rv = handle.mapBuffer(0, this.sharedBufferSizeInBytes_);
+ if (rv.result != Mojo.RESULT_OK) {
+ throw new Error("MockSensorProvider: Failed to map shared buffer");
+ }
+ this.shmemArrayBuffer_ = rv.buffer;
+ rv = handle.duplicateBufferHandle({readOnly: true});
+ if (rv.result != Mojo.RESULT_OK) {
+ throw new Error(
+ 'MockSensorProvider: failed to duplicate shared buffer');
+ }
+ this.readOnlySharedBufferHandle_ = rv.handle;
+ this.activeSensors_ = new Map();
+ this.resolveFuncs_ = new Map();
+ this.getSensorShouldFail_ = new Map();
+ this.permissionsDenied_ = new Map();
+ this.maxFrequency_ = 60;
+ this.minFrequency_ = 1;
+ this.mojomSensorType_ = new Map([
+ ['Accelerometer', SensorType.ACCELEROMETER],
+ ['LinearAccelerationSensor', SensorType.LINEAR_ACCELERATION],
+ ['GravitySensor', SensorType.GRAVITY],
+ ['AmbientLightSensor', SensorType.AMBIENT_LIGHT],
+ ['Gyroscope', SensorType.GYROSCOPE],
+ ['Magnetometer', SensorType.MAGNETOMETER],
+ ['AbsoluteOrientationSensor',
+ SensorType.ABSOLUTE_ORIENTATION_QUATERNION],
+ ['AbsoluteOrientationEulerAngles',
+ SensorType.ABSOLUTE_ORIENTATION_EULER_ANGLES],
+ ['RelativeOrientationSensor',
+ SensorType.RELATIVE_ORIENTATION_QUATERNION],
+ ['RelativeOrientationEulerAngles',
+ SensorType.RELATIVE_ORIENTATION_EULER_ANGLES],
+ ['ProximitySensor', SensorType.PROXIMITY]
+ ]);
+ this.receiver_ = new SensorProviderReceiver(this);
+
+ this.interceptor_ =
+ new MojoInterfaceInterceptor(SensorProvider.$interfaceName);
+ this.interceptor_.oninterfacerequest = e => {
+ this.bindToPipe(e.handle);
+ };
+ this.interceptor_.start();
+ }
+
+ // Returns initialized Sensor proxy to the client.
+ async getSensor(type) {
+ if (this.getSensorShouldFail_.get(type)) {
+ return {result: SensorCreationResult.ERROR_NOT_AVAILABLE,
+ initParams: null};
+ }
+ if (this.permissionsDenied_.get(type)) {
+ return {result: SensorCreationResult.ERROR_NOT_ALLOWED,
+ initParams: null};
+ }
+
+ const offset = type * this.readingSizeInBytes_;
+ const reportingMode = ReportingMode.ON_CHANGE;
+
+ const sensor = new SensorRemote();
+ if (!this.activeSensors_.has(type)) {
+ const shmemView = new Float64Array(
+ this.shmemArrayBuffer_, offset,
+ this.readingSizeInBytes_ / Float64Array.BYTES_PER_ELEMENT);
+ const mockSensor = new MockSensor(
+ sensor.$.bindNewPipeAndPassReceiver(), shmemView, reportingMode,
+ type);
+ this.activeSensors_.set(type, mockSensor);
+ this.activeSensors_.get(type).client_ = new SensorClientRemote();
+ }
+
+ const rv = this.readOnlySharedBufferHandle_.duplicateBufferHandle(
+ {readOnly: true});
+ if (rv.result != Mojo.RESULT_OK) {
+ throw new Error('getSensor(): failed to duplicate shared buffer');
+ }
+
+ const defaultConfig = { frequency: DEFAULT_FREQUENCY };
+ // Consider sensor traits to meet assertions in C++ code (see
+ // services/device/public/cpp/generic_sensor/sensor_traits.h)
+ if (type == SensorType.AMBIENT_LIGHT || type == SensorType.MAGNETOMETER) {
+ this.maxFrequency_ = Math.min(10, this.maxFrequency_);
+ }
+
+ const client = this.activeSensors_.get(type).client_;
+ const initParams = {
+ sensor,
+ clientReceiver: client.$.bindNewPipeAndPassReceiver(),
+ memory: {buffer: rv.handle},
+ bufferOffset: BigInt(offset),
+ mode: reportingMode,
+ defaultConfiguration: defaultConfig,
+ minimumFrequency: this.minFrequency_,
+ maximumFrequency: this.maxFrequency_
+ };
+
+ if (this.resolveFuncs_.has(type)) {
+ for (let resolveFunc of this.resolveFuncs_.get(type)) {
+ resolveFunc(this.activeSensors_.get(type));
+ }
+ this.resolveFuncs_.delete(type);
+ }
+
+ return {result: SensorCreationResult.SUCCESS, initParams};
+ }
+
+ // Binds object to mojo message pipe
+ bindToPipe(pipe) {
+ this.receiver_.$.bindHandle(pipe);
+ }
+
+ // Mock functions
+
+ // Resets state of mock SensorProvider between test runs.
+ reset() {
+ for (const sensor of this.activeSensors_.values()) {
+ sensor.reset();
+ }
+ this.activeSensors_.clear();
+ this.resolveFuncs_.clear();
+ this.getSensorShouldFail_.clear();
+ this.permissionsDenied_.clear();
+ this.maxFrequency_ = 60;
+ this.minFrequency_ = 1;
+ this.receiver_.$.close();
+ this.interceptor_.stop();
+ }
+
+ // Sets flag that forces mock SensorProvider to fail when getSensor() is
+ // invoked.
+ setGetSensorShouldFail(sensorType, shouldFail) {
+ this.getSensorShouldFail_.set(this.mojomSensorType_.get(sensorType),
+ shouldFail);
+ }
+
+ setPermissionsDenied(sensorType, permissionsDenied) {
+ this.permissionsDenied_.set(this.mojomSensorType_.get(sensorType),
+ permissionsDenied);
+ }
+
+ // Returns mock sensor that was created in getSensor to the layout test.
+ getCreatedSensor(sensorType) {
+ const type = this.mojomSensorType_.get(sensorType);
+ if (typeof type != "number") {
+ throw new TypeError(`getCreatedSensor(): Invalid sensor type ${sensorType}`);
+ }
+
+ if (this.activeSensors_.has(type)) {
+ return Promise.resolve(this.activeSensors_.get(type));
+ }
+
+ return new Promise(resolve => {
+ if (!this.resolveFuncs_.has(type)) {
+ this.resolveFuncs_.set(type, []);
+ }
+ this.resolveFuncs_.get(type).push(resolve);
+ });
+ }
+
+ // Sets the maximum frequency for a concrete sensor.
+ setMaximumSupportedFrequency(frequency) {
+ this.maxFrequency_ = frequency;
+ }
+
+ // Sets the minimum frequency for a concrete sensor.
+ setMinimumSupportedFrequency(frequency) {
+ this.minFrequency_ = frequency;
+ }
+ }
+
+ let testInternal = {
+ initialized: false,
+ sensorProvider: null
+ }
+
+ class GenericSensorTestChromium {
+ constructor() {
+ Object.freeze(this); // Make it immutable.
+ }
+
+ async initialize() {
+ if (testInternal.initialized)
+ throw new Error('Call reset() before initialize().');
+
+ // Grant sensor permissions for Chromium testdriver.
+ // testdriver.js only works in the top-level browsing context, so do
+ // nothing if we're in e.g. an iframe.
+ if (window.parent === window) {
+ for (const entry
+ of ['accelerometer', 'gyroscope', 'magnetometer',
+ 'ambient-light-sensor']) {
+ await test_driver.set_permission({name: entry}, 'granted');
+ }
+ }
+
+ testInternal.sensorProvider = new MockSensorProvider;
+ testInternal.initialized = true;
+ }
+ // Resets state of sensor mocks between test runs.
+ async reset() {
+ if (!testInternal.initialized)
+ throw new Error('Call initialize() before reset().');
+ testInternal.sensorProvider.reset();
+ testInternal.sensorProvider = null;
+ testInternal.initialized = false;
+
+ // Wait for an event loop iteration to let any pending mojo commands in
+ // the sensor provider finish.
+ await new Promise(resolve => setTimeout(resolve, 0));
+ }
+
+ getSensorProvider() {
+ return testInternal.sensorProvider;
+ }
+ }
+
+ return GenericSensorTestChromium;
+})();
diff --git a/test/wpt/tests/resources/chromium/generic_sensor_mocks.js.headers b/test/wpt/tests/resources/chromium/generic_sensor_mocks.js.headers
new file mode 100644
index 0000000..6805c32
--- /dev/null
+++ b/test/wpt/tests/resources/chromium/generic_sensor_mocks.js.headers
@@ -0,0 +1 @@
+Content-Type: text/javascript; charset=utf-8
diff --git a/test/wpt/tests/resources/chromium/mock-barcodedetection.js b/test/wpt/tests/resources/chromium/mock-barcodedetection.js
new file mode 100644
index 0000000..b0d2e0a
--- /dev/null
+++ b/test/wpt/tests/resources/chromium/mock-barcodedetection.js
@@ -0,0 +1,136 @@
+import {BarcodeDetectionReceiver, BarcodeFormat} from '/gen/services/shape_detection/public/mojom/barcodedetection.mojom.m.js';
+import {BarcodeDetectionProvider, BarcodeDetectionProviderReceiver} from '/gen/services/shape_detection/public/mojom/barcodedetection_provider.mojom.m.js';
+
+self.BarcodeDetectionTest = (() => {
+ // Class that mocks BarcodeDetectionProvider interface defined in
+ // https://cs.chromium.org/chromium/src/services/shape_detection/public/mojom/barcodedetection_provider.mojom
+ class MockBarcodeDetectionProvider {
+ constructor() {
+ this.receiver_ = new BarcodeDetectionProviderReceiver(this);
+
+ this.interceptor_ = new MojoInterfaceInterceptor(
+ BarcodeDetectionProvider.$interfaceName);
+ this.interceptor_.oninterfacerequest = e => {
+ if (this.should_close_pipe_on_request_)
+ e.handle.close();
+ else
+ this.receiver_.$.bindHandle(e.handle);
+ }
+ this.interceptor_.start();
+ this.should_close_pipe_on_request_ = false;
+ }
+
+ createBarcodeDetection(request, options) {
+ this.mockService_ = new MockBarcodeDetection(request, options);
+ }
+
+ enumerateSupportedFormats() {
+ return {
+ supportedFormats: [
+ BarcodeFormat.AZTEC,
+ BarcodeFormat.DATA_MATRIX,
+ BarcodeFormat.QR_CODE,
+ ]
+ };
+ }
+
+ getFrameData() {
+ return this.mockService_.bufferData_;
+ }
+
+ getFormats() {
+ return this.mockService_.options_.formats;
+ }
+
+ reset() {
+ this.mockService_ = null;
+ this.should_close_pipe_on_request_ = false;
+ this.receiver_.$.close();
+ this.interceptor_.stop();
+ }
+
+ // simulate a 'no implementation available' case
+ simulateNoImplementation() {
+ this.should_close_pipe_on_request_ = true;
+ }
+ }
+
+ // Class that mocks BarcodeDetection interface defined in
+ // https://cs.chromium.org/chromium/src/services/shape_detection/public/mojom/barcodedetection.mojom
+ class MockBarcodeDetection {
+ constructor(request, options) {
+ this.options_ = options;
+ this.receiver_ = new BarcodeDetectionReceiver(this);
+ this.receiver_.$.bindHandle(request.handle);
+ }
+
+ detect(bitmapData) {
+ this.bufferData_ =
+ new Uint32Array(getArrayBufferFromBigBuffer(bitmapData.pixelData));
+ return {
+ results: [
+ {
+ rawValue : "cats",
+ boundingBox: { x: 1.0, y: 1.0, width: 100.0, height: 100.0 },
+ format: BarcodeFormat.QR_CODE,
+ cornerPoints: [
+ { x: 1.0, y: 1.0 },
+ { x: 101.0, y: 1.0 },
+ { x: 101.0, y: 101.0 },
+ { x: 1.0, y: 101.0 }
+ ],
+ },
+ {
+ rawValue : "dogs",
+ boundingBox: { x: 2.0, y: 2.0, width: 50.0, height: 50.0 },
+ format: BarcodeFormat.CODE_128,
+ cornerPoints: [
+ { x: 2.0, y: 2.0 },
+ { x: 52.0, y: 2.0 },
+ { x: 52.0, y: 52.0 },
+ { x: 2.0, y: 52.0 }
+ ],
+ },
+ ],
+ };
+ }
+ }
+
+ let testInternal = {
+ initialized: false,
+ MockBarcodeDetectionProvider: null
+ }
+
+ class BarcodeDetectionTestChromium {
+ constructor() {
+ Object.freeze(this); // Make it immutable.
+ }
+
+ initialize() {
+ if (testInternal.initialized)
+ throw new Error('Call reset() before initialize().');
+
+ testInternal.MockBarcodeDetectionProvider = new MockBarcodeDetectionProvider;
+ testInternal.initialized = true;
+ }
+
+ // Resets state of barcode detection mocks between test runs.
+ async reset() {
+ if (!testInternal.initialized)
+ throw new Error('Call initialize() before reset().');
+ testInternal.MockBarcodeDetectionProvider.reset();
+ testInternal.MockBarcodeDetectionProvider = null;
+ testInternal.initialized = false;
+
+ await new Promise(resolve => setTimeout(resolve, 0));
+ }
+
+ MockBarcodeDetectionProvider() {
+ return testInternal.MockBarcodeDetectionProvider;
+ }
+ }
+
+ return BarcodeDetectionTestChromium;
+})();
+
+self.BarcodeFormat = BarcodeFormat;
diff --git a/test/wpt/tests/resources/chromium/mock-barcodedetection.js.headers b/test/wpt/tests/resources/chromium/mock-barcodedetection.js.headers
new file mode 100644
index 0000000..6c61a34
--- /dev/null
+++ b/test/wpt/tests/resources/chromium/mock-barcodedetection.js.headers
@@ -0,0 +1 @@
+Content-Type: text/javascript; charset=utf-8 \ No newline at end of file
diff --git a/test/wpt/tests/resources/chromium/mock-battery-monitor.headers b/test/wpt/tests/resources/chromium/mock-battery-monitor.headers
new file mode 100644
index 0000000..6805c32
--- /dev/null
+++ b/test/wpt/tests/resources/chromium/mock-battery-monitor.headers
@@ -0,0 +1 @@
+Content-Type: text/javascript; charset=utf-8
diff --git a/test/wpt/tests/resources/chromium/mock-battery-monitor.js b/test/wpt/tests/resources/chromium/mock-battery-monitor.js
new file mode 100644
index 0000000..8fa27bc
--- /dev/null
+++ b/test/wpt/tests/resources/chromium/mock-battery-monitor.js
@@ -0,0 +1,61 @@
+import {BatteryMonitor, BatteryMonitorReceiver} from '/gen/services/device/public/mojom/battery_monitor.mojom.m.js';
+
+class MockBatteryMonitor {
+ constructor() {
+ this.receiver_ = new BatteryMonitorReceiver(this);
+ this.interceptor_ =
+ new MojoInterfaceInterceptor(BatteryMonitor.$interfaceName);
+ this.interceptor_.oninterfacerequest = e =>
+ this.receiver_.$.bindHandle(e.handle);
+ this.reset();
+ }
+
+ start() {
+ this.interceptor_.start();
+ }
+
+ stop() {
+ this.interceptor_.stop();
+ }
+
+ reset() {
+ this.pendingRequests_ = [];
+ this.status_ = null;
+ this.lastKnownStatus_ = null;
+ }
+
+ queryNextStatus() {
+ const result = new Promise(resolve => this.pendingRequests_.push(resolve));
+ this.runCallbacks_();
+ return result;
+ }
+
+ setBatteryStatus(charging, chargingTime, dischargingTime, level) {
+ this.status_ = {charging, chargingTime, dischargingTime, level};
+ this.lastKnownStatus_ = this.status_;
+ this.runCallbacks_();
+ }
+
+ verifyBatteryStatus(manager) {
+ assert_not_equals(manager, undefined);
+ assert_not_equals(this.lastKnownStatus_, null);
+ assert_equals(manager.charging, this.lastKnownStatus_.charging);
+ assert_equals(manager.chargingTime, this.lastKnownStatus_.chargingTime);
+ assert_equals(
+ manager.dischargingTime, this.lastKnownStatus_.dischargingTime);
+ assert_equals(manager.level, this.lastKnownStatus_.level);
+ }
+
+ runCallbacks_() {
+ if (!this.status_ || !this.pendingRequests_.length)
+ return;
+
+ let result = {status: this.status_};
+ while (this.pendingRequests_.length) {
+ this.pendingRequests_.pop()(result);
+ }
+ this.status_ = null;
+ }
+}
+
+export const mockBatteryMonitor = new MockBatteryMonitor();
diff --git a/test/wpt/tests/resources/chromium/mock-direct-sockets.js b/test/wpt/tests/resources/chromium/mock-direct-sockets.js
new file mode 100644
index 0000000..6d557f7
--- /dev/null
+++ b/test/wpt/tests/resources/chromium/mock-direct-sockets.js
@@ -0,0 +1,94 @@
+'use strict';
+
+import {DirectSocketsService, DirectSocketsServiceReceiver} from '/gen/third_party/blink/public/mojom/direct_sockets/direct_sockets.mojom.m.js';
+
+self.DirectSocketsServiceTest = (() => {
+ // Class that mocks DirectSocketsService interface defined in
+ // https://source.chromium.org/chromium/chromium/src/third_party/blink/public/mojom/direct_sockets/direct_sockets.mojom
+ class MockDirectSocketsService {
+ constructor() {
+ this.interceptor_ = new MojoInterfaceInterceptor(DirectSocketsService.$interfaceName);
+ this.receiver_ = new DirectSocketsServiceReceiver(this);
+ this.interceptor_.oninterfacerequest = e =>
+ this.receiver_.$.bindHandle(e.handle);
+ this.interceptor_.start();
+ }
+
+ reset() {
+ this.receiver_.$.close();
+ this.interceptor_.stop();
+ }
+
+ openTCPSocket(
+ options,
+ receiver,
+ observer) {
+ return Promise.resolve({
+ // return result = net:Error::NOT_IMPLEMENTED (code -11)
+ result: -11
+ });
+ }
+
+ openConnectedUDPSocket(
+ options,
+ receiver,
+ listener) {
+ return Promise.resolve({
+ // return result = net:Error::NOT_IMPLEMENTED (code -11)
+ result: -11
+ });
+ }
+
+ openBoundUDPSocket(
+ options,
+ receiver,
+ listener) {
+ return Promise.resolve({
+ // return result = net:Error::NOT_IMPLEMENTED (code -11)
+ result: -11
+ });
+ }
+
+ openTCPServerSocket(
+ options,
+ receiver) {
+ return Promise.resolve({
+ // return result = net:Error::NOT_IMPLEMENTED (code -11)
+ result: -11
+ });
+ }
+ }
+
+ let testInternal = {
+ initialized: false,
+ mockDirectSocketsService: null
+ }
+
+ class DirectSocketsServiceTestChromium {
+ constructor() {
+ Object.freeze(this); // Make it immutable.
+ }
+
+ initialize() {
+ if (!testInternal.initialized) {
+ testInternal = {
+ mockDirectSocketsService: new MockDirectSocketsService(),
+ initialized: true
+ };
+ }
+ }
+
+ async reset() {
+ if (testInternal.initialized) {
+ testInternal.mockDirectSocketsService.reset();
+ testInternal = {
+ mockDirectSocketsService: null,
+ initialized: false
+ };
+ await new Promise(resolve => setTimeout(resolve, 0));
+ }
+ }
+ }
+
+ return DirectSocketsServiceTestChromium;
+})();
diff --git a/test/wpt/tests/resources/chromium/mock-facedetection.js b/test/wpt/tests/resources/chromium/mock-facedetection.js
new file mode 100644
index 0000000..7ae6586
--- /dev/null
+++ b/test/wpt/tests/resources/chromium/mock-facedetection.js
@@ -0,0 +1,130 @@
+import {FaceDetectionReceiver, LandmarkType} from '/gen/services/shape_detection/public/mojom/facedetection.mojom.m.js';
+import {FaceDetectionProvider, FaceDetectionProviderReceiver} from '/gen/services/shape_detection/public/mojom/facedetection_provider.mojom.m.js';
+
+self.FaceDetectionTest = (() => {
+ // Class that mocks FaceDetectionProvider interface defined in
+ // https://cs.chromium.org/chromium/src/services/shape_detection/public/mojom/facedetection_provider.mojom
+ class MockFaceDetectionProvider {
+ constructor() {
+ this.receiver_ = new FaceDetectionProviderReceiver(this);
+
+ this.interceptor_ = new MojoInterfaceInterceptor(
+ FaceDetectionProvider.$interfaceName);
+ this.interceptor_.oninterfacerequest =
+ e => this.receiver_.$.bindHandle(e.handle);
+ this.interceptor_.start();
+ }
+
+ createFaceDetection(request, options) {
+ this.mockService_ = new MockFaceDetection(request, options);
+ }
+
+ getFrameData() {
+ return this.mockService_.bufferData_;
+ }
+
+ getMaxDetectedFaces() {
+ return this.mockService_.maxDetectedFaces_;
+ }
+
+ getFastMode () {
+ return this.mockService_.fastMode_;
+ }
+
+ reset() {
+ this.mockService_ = null;
+ this.receiver_.$.close();
+ this.interceptor_.stop();
+ }
+ }
+
+ // Class that mocks FaceDetection interface defined in
+ // https://cs.chromium.org/chromium/src/services/shape_detection/public/mojom/facedetection.mojom
+ class MockFaceDetection {
+ constructor(request, options) {
+ this.maxDetectedFaces_ = options.maxDetectedFaces;
+ this.fastMode_ = options.fastMode;
+ this.receiver_ = new FaceDetectionReceiver(this);
+ this.receiver_.$.bindHandle(request.handle);
+ }
+
+ detect(bitmapData) {
+ this.bufferData_ =
+ new Uint32Array(getArrayBufferFromBigBuffer(bitmapData.pixelData));
+ return Promise.resolve({
+ results: [
+ {
+ boundingBox: {x: 1.0, y: 1.0, width: 100.0, height: 100.0},
+ landmarks: [{
+ type: LandmarkType.EYE,
+ locations: [{x: 4.0, y: 5.0}]
+ },
+ {
+ type: LandmarkType.EYE,
+ locations: [
+ {x: 4.0, y: 5.0}, {x: 5.0, y: 4.0}, {x: 6.0, y: 3.0},
+ {x: 7.0, y: 4.0}, {x: 8.0, y: 5.0}, {x: 7.0, y: 6.0},
+ {x: 6.0, y: 7.0}, {x: 5.0, y: 6.0}
+ ]
+ }]
+ },
+ {
+ boundingBox: {x: 2.0, y: 2.0, width: 200.0, height: 200.0},
+ landmarks: [{
+ type: LandmarkType.NOSE,
+ locations: [{x: 100.0, y: 50.0}]
+ },
+ {
+ type: LandmarkType.NOSE,
+ locations: [
+ {x: 80.0, y: 50.0}, {x: 70.0, y: 60.0}, {x: 60.0, y: 70.0},
+ {x: 70.0, y: 60.0}, {x: 80.0, y: 70.0}, {x: 90.0, y: 80.0},
+ {x: 100.0, y: 70.0}, {x: 90.0, y: 60.0}, {x: 80.0, y: 50.0}
+ ]
+ }]
+ },
+ {
+ boundingBox: {x: 3.0, y: 3.0, width: 300.0, height: 300.0},
+ landmarks: []
+ },
+ ]
+ });
+ }
+ }
+
+ let testInternal = {
+ initialized: false,
+ MockFaceDetectionProvider: null
+ }
+
+ class FaceDetectionTestChromium {
+ constructor() {
+ Object.freeze(this); // Make it immutable.
+ }
+
+ initialize() {
+ if (testInternal.initialized)
+ throw new Error('Call reset() before initialize().');
+
+ testInternal.MockFaceDetectionProvider = new MockFaceDetectionProvider;
+ testInternal.initialized = true;
+ }
+
+ // Resets state of face detection mocks between test runs.
+ async reset() {
+ if (!testInternal.initialized)
+ throw new Error('Call initialize() before reset().');
+ testInternal.MockFaceDetectionProvider.reset();
+ testInternal.MockFaceDetectionProvider = null;
+ testInternal.initialized = false;
+
+ await new Promise(resolve => setTimeout(resolve, 0));
+ }
+
+ MockFaceDetectionProvider() {
+ return testInternal.MockFaceDetectionProvider;
+ }
+ }
+
+ return FaceDetectionTestChromium;
+})();
diff --git a/test/wpt/tests/resources/chromium/mock-facedetection.js.headers b/test/wpt/tests/resources/chromium/mock-facedetection.js.headers
new file mode 100644
index 0000000..6c61a34
--- /dev/null
+++ b/test/wpt/tests/resources/chromium/mock-facedetection.js.headers
@@ -0,0 +1 @@
+Content-Type: text/javascript; charset=utf-8 \ No newline at end of file
diff --git a/test/wpt/tests/resources/chromium/mock-idle-detection.js b/test/wpt/tests/resources/chromium/mock-idle-detection.js
new file mode 100644
index 0000000..54fe5dd
--- /dev/null
+++ b/test/wpt/tests/resources/chromium/mock-idle-detection.js
@@ -0,0 +1,80 @@
+import {IdleManager, IdleManagerError, IdleManagerReceiver} from '/gen/third_party/blink/public/mojom/idle/idle_manager.mojom.m.js';
+
+/**
+ * This is a testing framework that enables us to test the user idle detection
+ * by intercepting the connection between the renderer and the browser and
+ * exposing a mocking API for tests.
+ *
+ * Usage:
+ *
+ * 1) Include <script src="mock.js"></script> in your file.
+ * 2) Set expectations
+ * expect(addMonitor).andReturn((threshold, monitorPtr, callback) => {
+ * // mock behavior
+ * })
+ * 3) Call navigator.idle.query()
+ *
+ * The mocking API is blink agnostic and is designed such that other engines
+ * could implement it too. Here are the symbols that are exposed to tests:
+ *
+ * - function addMonitor(): the main/only function that can be mocked.
+ * - function expect(): the main/only function that enables us to mock it.
+ * - function close(): disconnects the interceptor.
+ * - enum UserIdleState {IDLE, ACTIVE}: blink agnostic constants.
+ * - enum ScreenIdleState {LOCKED, UNLOCKED}: blink agnostic constants.
+ */
+
+class FakeIdleMonitor {
+ addMonitor(threshold, monitorPtr, callback) {
+ return this.handler.addMonitor(threshold, monitorPtr);
+ }
+ setHandler(handler) {
+ this.handler = handler;
+ return this;
+ }
+ setBinding(binding) {
+ this.binding = binding;
+ return this;
+ }
+ close() {
+ this.binding.$.close();
+ }
+}
+
+self.IdleDetectorError = {};
+
+self.addMonitor = function addMonitor(threshold, monitorPtr, callback) {
+ throw new Error("expected to be overriden by tests");
+}
+
+async function close() {
+ interceptor.close();
+}
+
+self.expect = function(call) {
+ return {
+ andReturn(callback) {
+ let handler = {};
+ handler[call.name] = callback;
+ interceptor.setHandler(handler);
+ }
+ };
+};
+
+function intercept() {
+ let result = new FakeIdleMonitor();
+
+ let binding = new IdleManagerReceiver(result);
+ let interceptor = new MojoInterfaceInterceptor(IdleManager.$interfaceName);
+ interceptor.oninterfacerequest = e => binding.$.bindHandle(e.handle);
+ interceptor.start();
+
+ self.IdleDetectorError.SUCCESS = IdleManagerError.kSuccess;
+ self.IdleDetectorError.PERMISSION_DISABLED =
+ IdleManagerError.kPermissionDisabled;
+
+ result.setBinding(binding);
+ return result;
+}
+
+const interceptor = intercept();
diff --git a/test/wpt/tests/resources/chromium/mock-imagecapture.js b/test/wpt/tests/resources/chromium/mock-imagecapture.js
new file mode 100644
index 0000000..8424e1e
--- /dev/null
+++ b/test/wpt/tests/resources/chromium/mock-imagecapture.js
@@ -0,0 +1,309 @@
+import {BackgroundBlurMode, FillLightMode, ImageCapture, ImageCaptureReceiver, MeteringMode, RedEyeReduction} from '/gen/media/capture/mojom/image_capture.mojom.m.js';
+
+self.ImageCaptureTest = (() => {
+ // Class that mocks ImageCapture interface defined in
+ // https://cs.chromium.org/chromium/src/media/capture/mojom/image_capture.mojom
+ class MockImageCapture {
+ constructor() {
+ this.interceptor_ =
+ new MojoInterfaceInterceptor(ImageCapture.$interfaceName);
+ this.interceptor_.oninterfacerequest =
+ e => this.receiver_.$.bindHandle(e.handle);
+ this.interceptor_.start();
+
+ this.state_ = {
+ state: {
+ supportedWhiteBalanceModes: [
+ MeteringMode.SINGLE_SHOT,
+ MeteringMode.CONTINUOUS
+ ],
+ currentWhiteBalanceMode: MeteringMode.CONTINUOUS,
+ supportedExposureModes: [
+ MeteringMode.MANUAL,
+ MeteringMode.SINGLE_SHOT,
+ MeteringMode.CONTINUOUS
+ ],
+ currentExposureMode: MeteringMode.MANUAL,
+ supportedFocusModes: [
+ MeteringMode.MANUAL,
+ MeteringMode.SINGLE_SHOT
+ ],
+ currentFocusMode: MeteringMode.MANUAL,
+ pointsOfInterest: [{
+ x: 0.4,
+ y: 0.6
+ }],
+
+ exposureCompensation: {
+ min: -200.0,
+ max: 200.0,
+ current: 33.0,
+ step: 33.0
+ },
+ exposureTime: {
+ min: 100.0,
+ max: 100000.0,
+ current: 1000.0,
+ step: 100.0
+ },
+ colorTemperature: {
+ min: 2500.0,
+ max: 6500.0,
+ current: 6000.0,
+ step: 1000.0
+ },
+ iso: {
+ min: 100.0,
+ max: 12000.0,
+ current: 400.0,
+ step: 1.0
+ },
+
+ brightness: {
+ min: 1.0,
+ max: 10.0,
+ current: 5.0,
+ step: 1.0
+ },
+ contrast: {
+ min: 2.0,
+ max: 9.0,
+ current: 5.0,
+ step: 1.0
+ },
+ saturation: {
+ min: 3.0,
+ max: 8.0,
+ current: 6.0,
+ step: 1.0
+ },
+ sharpness: {
+ min: 4.0,
+ max: 7.0,
+ current: 7.0,
+ step: 1.0
+ },
+
+ focusDistance: {
+ min: 1.0,
+ max: 10.0,
+ current: 3.0,
+ step: 1.0
+ },
+
+ pan: {
+ min: 0.0,
+ max: 10.0,
+ current: 5.0,
+ step: 2.0
+ },
+
+ tilt: {
+ min: 0.0,
+ max: 10.0,
+ current: 5.0,
+ step: 2.0
+ },
+
+ zoom: {
+ min: 0.0,
+ max: 10.0,
+ current: 5.0,
+ step: 5.0
+ },
+
+ supportsTorch: true,
+ torch: false,
+
+ redEyeReduction: RedEyeReduction.CONTROLLABLE,
+ height: {
+ min: 240.0,
+ max: 2448.0,
+ current: 240.0,
+ step: 2.0
+ },
+ width: {
+ min: 320.0,
+ max: 3264.0,
+ current: 320.0,
+ step: 3.0
+ },
+ fillLightMode: [FillLightMode.AUTO, FillLightMode.FLASH],
+
+ supportedBackgroundBlurModes: [
+ BackgroundBlurMode.OFF,
+ BackgroundBlurMode.BLUR
+ ],
+ backgroundBlurMode: BackgroundBlurMode.OFF,
+ }
+ };
+ this.panTiltZoomPermissionStatus_ = null;
+ this.settings_ = null;
+ this.receiver_ = new ImageCaptureReceiver(this);
+ }
+
+ reset() {
+ this.receiver_.$.close();
+ this.interceptor_.stop();
+ }
+
+ async getPhotoState(source_id) {
+ const shouldKeepPanTiltZoom = await this.isPanTiltZoomPermissionGranted();
+ if (shouldKeepPanTiltZoom)
+ return Promise.resolve(this.state_);
+
+ const newState = {...this.state_};
+ newState.state.pan = {};
+ newState.state.tilt = {};
+ newState.state.zoom = {};
+ return Promise.resolve(newState);
+ }
+
+ async setPhotoOptions(source_id, settings) {
+ const isAllowedToControlPanTiltZoom = await this.isPanTiltZoomPermissionGranted();
+ if (!isAllowedToControlPanTiltZoom &&
+ (settings.hasPan || settings.hasTilt || settings.hasZoom)) {
+ return Promise.resolve({ success: false });
+ }
+ this.settings_ = settings;
+ if (settings.hasIso)
+ this.state_.state.iso.current = settings.iso;
+ if (settings.hasHeight)
+ this.state_.state.height.current = settings.height;
+ if (settings.hasWidth)
+ this.state_.state.width.current = settings.width;
+ if (settings.hasPan)
+ this.state_.state.pan.current = settings.pan;
+ if (settings.hasTilt)
+ this.state_.state.tilt.current = settings.tilt;
+ if (settings.hasZoom)
+ this.state_.state.zoom.current = settings.zoom;
+ if (settings.hasFocusMode)
+ this.state_.state.currentFocusMode = settings.focusMode;
+ if (settings.hasFocusDistance)
+ this.state_.state.focusDistance.current = settings.focusDistance;
+
+ if (settings.pointsOfInterest.length > 0) {
+ this.state_.state.pointsOfInterest =
+ settings.pointsOfInterest;
+ }
+
+ if (settings.hasExposureMode)
+ this.state_.state.currentExposureMode = settings.exposureMode;
+
+ if (settings.hasExposureCompensation) {
+ this.state_.state.exposureCompensation.current =
+ settings.exposureCompensation;
+ }
+ if (settings.hasExposureTime) {
+ this.state_.state.exposureTime.current =
+ settings.exposureTime;
+ }
+ if (settings.hasWhiteBalanceMode) {
+ this.state_.state.currentWhiteBalanceMode =
+ settings.whiteBalanceMode;
+ }
+ if (settings.hasFillLightMode)
+ this.state_.state.fillLightMode = [settings.fillLightMode];
+ if (settings.hasRedEyeReduction)
+ this.state_.state.redEyeReduction = settings.redEyeReduction;
+ if (settings.hasColorTemperature) {
+ this.state_.state.colorTemperature.current =
+ settings.colorTemperature;
+ }
+ if (settings.hasBrightness)
+ this.state_.state.brightness.current = settings.brightness;
+ if (settings.hasContrast)
+ this.state_.state.contrast.current = settings.contrast;
+ if (settings.hasSaturation)
+ this.state_.state.saturation.current = settings.saturation;
+ if (settings.hasSharpness)
+ this.state_.state.sharpness.current = settings.sharpness;
+
+ if (settings.hasTorch)
+ this.state_.state.torch = settings.torch;
+
+ if (settings.hasBackgroundBlurMode)
+ this.state_.state.backgroundBlurMode = [settings.backgroundBlurMode];
+
+ return Promise.resolve({
+ success: true
+ });
+ }
+
+ takePhoto(source_id) {
+ return Promise.resolve({
+ blob: {
+ mimeType: 'image/cat',
+ data: new Array(2)
+ }
+ });
+ }
+
+ async isPanTiltZoomPermissionGranted() {
+ if (!this.panTiltZoomPermissionStatus_) {
+ this.panTiltZoomPermissionStatus_ = await navigator.permissions.query({
+ name: "camera",
+ panTiltZoom: true
+ });
+ }
+ return this.panTiltZoomPermissionStatus_.state == "granted";
+ }
+
+ state() {
+ return this.state_.state;
+ }
+
+ turnOffBackgroundBlurMode() {
+ this.state_.state.backgroundBlurMode = BackgroundBlurMode.OFF;
+ }
+ turnOnBackgroundBlurMode() {
+ this.state_.state.backgroundBlurMode = BackgroundBlurMode.BLUR;
+ }
+ turnOffSupportedBackgroundBlurModes() {
+ this.state_.state.supportedBackgroundBlurModes = [BackgroundBlurMode.OFF];
+ }
+ turnOnSupportedBackgroundBlurModes() {
+ this.state_.state.supportedBackgroundBlurModes = [BackgroundBlurMode.BLUR];
+ }
+
+ options() {
+ return this.settings_;
+ }
+ }
+
+ let testInternal = {
+ initialized: false,
+ mockImageCapture: null
+ }
+
+ class ImageCaptureTestChromium {
+
+ constructor() {
+ Object.freeze(this); // Make it immutable.
+ }
+
+ initialize() {
+ if (testInternal.initialized)
+ throw new Error('Call reset() before initialize().');
+
+ testInternal.mockImageCapture = new MockImageCapture;
+ testInternal.initialized = true;
+ }
+ // Resets state of image capture mocks between test runs.
+ async reset() {
+ if (!testInternal.initialized)
+ throw new Error('Call initialize() before reset().');
+ testInternal.mockImageCapture.reset();
+ testInternal.mockImageCapture = null;
+ testInternal.initialized = false;
+
+ await new Promise(resolve => setTimeout(resolve, 0));
+ }
+ mockImageCapture() {
+ return testInternal.mockImageCapture;
+ }
+ }
+
+ return ImageCaptureTestChromium;
+})();
diff --git a/test/wpt/tests/resources/chromium/mock-managed-config.js b/test/wpt/tests/resources/chromium/mock-managed-config.js
new file mode 100644
index 0000000..c9980e1
--- /dev/null
+++ b/test/wpt/tests/resources/chromium/mock-managed-config.js
@@ -0,0 +1,91 @@
+'use strict'
+
+import{ManagedConfigurationObserverRemote, ManagedConfigurationService, ManagedConfigurationServiceReceiver} from '/gen/third_party/blink/public/mojom/device/device.mojom.m.js';
+
+
+self.ManagedConfigTest = (() => {
+ // Class that mocks ManagedConfigurationService interface defined in
+ // https://source.chromium.org/chromium/chromium/src/third_party/blink/public/mojom/device/device.mojom
+ class MockManagedConfig {
+ constructor() {
+ this.receiver_ = new ManagedConfigurationServiceReceiver(this);
+ this.interceptor_ = new MojoInterfaceInterceptor(
+ ManagedConfigurationService.$interfaceName);
+ this.interceptor_.oninterfacerequest = e =>
+ this.receiver_.$.bindHandle(e.handle);
+ this.interceptor_.start();
+ this.subscription_ = null;
+ this.reset();
+ }
+
+ reset() {
+ this.configuration_ = null;
+ this.onObserverAdd_ = null;
+ }
+
+ async getManagedConfiguration(keys) {
+ if (this.configuration_ === null) {
+ return {};
+ }
+
+ return {
+ configurations: Object.keys(this.configuration_)
+ .filter(key => keys.includes(key))
+ .reduce(
+ (obj, key) => {
+ obj[key] =
+ JSON.stringify(this.configuration_[key]);
+ return obj;
+ },
+ {})
+ };
+ }
+
+ subscribeToManagedConfiguration(remote) {
+ this.subscription_ = remote;
+ if (this.onObserverAdd_ !== null) {
+ this.onObserverAdd_();
+ }
+ }
+
+ setManagedConfig(value) {
+ this.configuration_ = value;
+ if (this.subscription_ !== null) {
+ this.subscription_.onConfigurationChanged();
+ }
+ }
+ }
+
+ let testInternal = {
+ initialized: false,
+ mockManagedConfig: null
+ }
+
+ class ManagedConfigTestChromium {
+ constructor() {
+ Object.freeze(this); // Make it immutable.
+ }
+
+ initialize() {
+ if (testInternal.mockManagedConfig !== null) {
+ testInternal.mockManagedConfig.reset();
+ return;
+ }
+
+ testInternal.mockManagedConfig = new MockManagedConfig;
+ testInternal.initialized = true;
+ }
+
+ setManagedConfig(config) {
+ testInternal.mockManagedConfig.setManagedConfig(config);
+ }
+
+ async nextObserverAdded() {
+ await new Promise(resolve => {
+ testInternal.mockManagedConfig.onObserverAdd_ = resolve;
+ });
+ }
+ }
+
+ return ManagedConfigTestChromium;
+})();
diff --git a/test/wpt/tests/resources/chromium/mock-pressure-service.js b/test/wpt/tests/resources/chromium/mock-pressure-service.js
new file mode 100644
index 0000000..02d10f8
--- /dev/null
+++ b/test/wpt/tests/resources/chromium/mock-pressure-service.js
@@ -0,0 +1,134 @@
+import {PressureManager, PressureManagerReceiver, PressureStatus} from '/gen/services/device/public/mojom/pressure_manager.mojom.m.js'
+import {PressureSource, PressureState} from '/gen/services/device/public/mojom/pressure_update.mojom.m.js'
+
+class MockPressureService {
+ constructor() {
+ this.receiver_ = new PressureManagerReceiver(this);
+ this.interceptor_ =
+ new MojoInterfaceInterceptor(PressureManager.$interfaceName);
+ this.interceptor_.oninterfacerequest = e => {
+ this.receiver_.$.bindHandle(e.handle);
+ };
+ this.reset();
+ this.mojomSourceType_ = new Map([['cpu', PressureSource.kCpu]]);
+ this.mojomStateType_ = new Map([
+ ['nominal', PressureState.kNominal], ['fair', PressureState.kFair],
+ ['serious', PressureState.kSerious], ['critical', PressureState.kCritical]
+ ]);
+ this.pressureServiceReadingTimerId_ = null;
+ }
+
+ start() {
+ this.interceptor_.start();
+ }
+
+ stop() {
+ this.stopPlatformCollector();
+ this.receiver_.$.close();
+ this.interceptor_.stop();
+
+ // Wait for an event loop iteration to let any pending mojo commands in
+ // the pressure service finish.
+ return new Promise(resolve => setTimeout(resolve, 0));
+ }
+
+ reset() {
+ this.observers_ = [];
+ this.pressureUpdate_ = null;
+ this.pressureServiceReadingTimerId_ = null;
+ this.pressureStatus_ = PressureStatus.kOk;
+ this.updatesDelivered_ = 0;
+ }
+
+ async addClient(observer, source) {
+ if (this.observers_.indexOf(observer) >= 0)
+ throw new Error('addClient() has already been called');
+
+ // TODO(crbug.com/1342184): Consider other sources.
+ // For now, "cpu" is the only source.
+ if (source !== PressureSource.kCpu)
+ throw new Error('Call addClient() with a wrong PressureSource');
+
+ observer.onConnectionError.addListener(() => {
+ // Remove this observer from observer array.
+ this.observers_.splice(this.observers_.indexOf(observer), 1);
+ });
+ this.observers_.push(observer);
+
+ return {status: this.pressureStatus_};
+ }
+
+ startPlatformCollector(sampleRate) {
+ if (sampleRate === 0)
+ return;
+
+ if (this.pressureServiceReadingTimerId_ != null)
+ this.stopPlatformCollector();
+
+ // The following code for calculating the timestamp was taken from
+ // https://source.chromium.org/chromium/chromium/src/+/main:third_party/
+ // blink/web_tests/http/tests/resources/
+ // geolocation-mock.js;l=131;drc=37a9b6c03b9bda9fcd62fc0e5e8016c278abd31f
+
+ // The new Date().getTime() returns the number of milliseconds since the
+ // UNIX epoch (1970-01-01 00::00:00 UTC), while |internalValue| of the
+ // device.mojom.PressureUpdate represents the value of microseconds since
+ // the Windows FILETIME epoch (1601-01-01 00:00:00 UTC). So add the delta
+ // when sets the |internalValue|. See more info in //base/time/time.h.
+ const windowsEpoch = Date.UTC(1601, 0, 1, 0, 0, 0, 0);
+ const unixEpoch = Date.UTC(1970, 0, 1, 0, 0, 0, 0);
+ // |epochDeltaInMs| equals to base::Time::kTimeTToMicrosecondsOffset.
+ const epochDeltaInMs = unixEpoch - windowsEpoch;
+
+ const timeout = (1 / sampleRate) * 1000;
+ this.pressureServiceReadingTimerId_ = self.setInterval(() => {
+ if (this.pressureUpdate_ === null || this.observers_.length === 0)
+ return;
+ this.pressureUpdate_.timestamp = {
+ internalValue: BigInt((new Date().getTime() + epochDeltaInMs) * 1000)
+ };
+ for (let observer of this.observers_)
+ observer.onPressureUpdated(this.pressureUpdate_);
+ this.updatesDelivered_++;
+ }, timeout);
+ }
+
+ stopPlatformCollector() {
+ if (this.pressureServiceReadingTimerId_ != null) {
+ self.clearInterval(this.pressureServiceReadingTimerId_);
+ this.pressureServiceReadingTimerId_ = null;
+ }
+ this.updatesDelivered_ = 0;
+ }
+
+ updatesDelivered() {
+ return this.updatesDelivered_;
+ }
+
+ setPressureUpdate(source, state) {
+ if (!this.mojomSourceType_.has(source))
+ throw new Error(`PressureSource '${source}' is invalid`);
+
+ if (!this.mojomStateType_.has(state))
+ throw new Error(`PressureState '${state}' is invalid`);
+
+ this.pressureUpdate_ = {
+ source: this.mojomSourceType_.get(source),
+ state: this.mojomStateType_.get(state),
+ };
+ }
+
+ setExpectedFailure(expectedException) {
+ assert_true(
+ expectedException instanceof DOMException,
+ 'setExpectedFailure() expects a DOMException instance');
+ if (expectedException.name === 'NotSupportedError') {
+ this.pressureStatus_ = PressureStatus.kNotSupported;
+ } else {
+ throw new TypeError(
+ `Unexpected DOMException '${expectedException.name}'`);
+ }
+ }
+}
+
+export const mockPressureService = new MockPressureService();
diff --git a/test/wpt/tests/resources/chromium/mock-pressure-service.js.headers b/test/wpt/tests/resources/chromium/mock-pressure-service.js.headers
new file mode 100644
index 0000000..6805c32
--- /dev/null
+++ b/test/wpt/tests/resources/chromium/mock-pressure-service.js.headers
@@ -0,0 +1 @@
+Content-Type: text/javascript; charset=utf-8
diff --git a/test/wpt/tests/resources/chromium/mock-subapps.js b/test/wpt/tests/resources/chromium/mock-subapps.js
new file mode 100644
index 0000000..b819367
--- /dev/null
+++ b/test/wpt/tests/resources/chromium/mock-subapps.js
@@ -0,0 +1,89 @@
+'use strict';
+
+import {SubAppsService, SubAppsServiceReceiver, SubAppsServiceResultCode} from '/gen/third_party/blink/public/mojom/subapps/sub_apps_service.mojom.m.js';
+
+self.SubAppsServiceTest = (() => {
+ // Class that mocks SubAppsService interface defined in /third_party/blink/public/mojom/subapps/sub_apps_service.mojom
+
+ class MockSubAppsService {
+ constructor() {
+ this.interceptor_ =
+ new MojoInterfaceInterceptor(SubAppsService.$interfaceName);
+ this.receiver_ = new SubAppsServiceReceiver(this);
+ this.interceptor_.oninterfacerequest =
+ e => this.receiver_.$.bindHandle(e.handle);
+ this.interceptor_.start();
+ }
+
+ reset() {
+ this.interceptor_.stop();
+ this.receiver_.$.close();
+ }
+
+ add(sub_apps) {
+ return Promise.resolve({
+ result: testInternal.addCallReturnValue,
+ });
+ }
+
+ list() {
+ return Promise.resolve({
+ result: {
+ resultCode: testInternal.serviceResultCode,
+ subAppsList: testInternal.listCallReturnValue,
+ }
+ });
+ }
+
+ remove() {
+ return Promise.resolve({
+ result: testInternal.removeCallReturnValue,
+ });
+ }
+ }
+
+ let testInternal = {
+ initialized: false,
+ mockSubAppsService: null,
+ serviceResultCode: 0,
+ addCallReturnValue: [],
+ listCallReturnValue: [],
+ removeCallReturnValue: [],
+ }
+
+ class SubAppsServiceTestChromium {
+ constructor() {
+ Object.freeze(this); // Make it immutable.
+ }
+
+ initialize(service_result_code, add_call_return_value, list_call_return_value, remove_call_return_value) {
+ if (!testInternal.initialized) {
+ testInternal = {
+ mockSubAppsService: new MockSubAppsService(),
+ initialized: true,
+ serviceResultCode: service_result_code,
+ addCallReturnValue: add_call_return_value,
+ listCallReturnValue: list_call_return_value,
+ removeCallReturnValue: remove_call_return_value,
+ };
+ };
+ }
+
+ async reset() {
+ if (testInternal.initialized) {
+ testInternal.mockSubAppsService.reset();
+ testInternal = {
+ mockSubAppsService: null,
+ initialized: false,
+ serviceResultCode: 0,
+ addCallReturnValue: [],
+ listCallReturnValue: [],
+ removeCallReturnValue: [],
+ };
+ await new Promise(resolve => setTimeout(resolve, 0));
+ }
+ }
+ }
+
+ return SubAppsServiceTestChromium;
+})();
diff --git a/test/wpt/tests/resources/chromium/mock-textdetection.js b/test/wpt/tests/resources/chromium/mock-textdetection.js
new file mode 100644
index 0000000..52ca987
--- /dev/null
+++ b/test/wpt/tests/resources/chromium/mock-textdetection.js
@@ -0,0 +1,92 @@
+import {TextDetection, TextDetectionReceiver} from '/gen/services/shape_detection/public/mojom/textdetection.mojom.m.js';
+
+self.TextDetectionTest = (() => {
+ // Class that mocks TextDetection interface defined in
+ // https://cs.chromium.org/chromium/src/services/shape_detection/public/mojom/textdetection.mojom
+ class MockTextDetection {
+ constructor() {
+ this.receiver_ = new TextDetectionReceiver(this);
+ this.interceptor_ =
+ new MojoInterfaceInterceptor(TextDetection.$interfaceName);
+ this.interceptor_.oninterfacerequest =
+ e => this.receiver_.$.bindHandle(e.handle);
+ this.interceptor_.start();
+ }
+
+ detect(bitmapData) {
+ this.bufferData_ =
+ new Uint32Array(getArrayBufferFromBigBuffer(bitmapData.pixelData));
+ return Promise.resolve({
+ results: [
+ {
+ rawValue : "cats",
+ boundingBox: { x: 1.0, y: 1.0, width: 100.0, height: 100.0 },
+ cornerPoints: [
+ { x: 1.0, y: 1.0 },
+ { x: 101.0, y: 1.0 },
+ { x: 101.0, y: 101.0 },
+ { x: 1.0, y: 101.0 }
+ ]
+ },
+ {
+ rawValue : "dogs",
+ boundingBox: { x: 2.0, y: 2.0, width: 50.0, height: 50.0 },
+ cornerPoints: [
+ { x: 2.0, y: 2.0 },
+ { x: 52.0, y: 2.0 },
+ { x: 52.0, y: 52.0 },
+ { x: 2.0, y: 52.0 }
+ ]
+ },
+ ],
+ });
+ }
+
+ getFrameData() {
+ return this.bufferData_;
+ }
+
+ reset() {
+ this.receiver_.$.close();
+ this.interceptor_.stop();
+ }
+
+ }
+
+ let testInternal = {
+ initialized: false,
+ MockTextDetection: null
+ }
+
+ class TextDetectionTestChromium {
+ constructor() {
+ Object.freeze(this); // Make it immutable.
+ }
+
+ initialize() {
+ if (testInternal.initialized)
+ throw new Error('Call reset() before initialize().');
+
+ testInternal.MockTextDetection = new MockTextDetection;
+ testInternal.initialized = true;
+ }
+
+ // Resets state of text detection mocks between test runs.
+ async reset() {
+ if (!testInternal.initialized)
+ throw new Error('Call initialize() before reset().');
+ testInternal.MockTextDetection.reset();
+ testInternal.MockTextDetection = null;
+ testInternal.initialized = false;
+
+ await new Promise(resolve => setTimeout(resolve, 0));
+ }
+
+ MockTextDetection() {
+ return testInternal.MockTextDetection;
+ }
+ }
+
+ return TextDetectionTestChromium;
+
+})();
diff --git a/test/wpt/tests/resources/chromium/mock-textdetection.js.headers b/test/wpt/tests/resources/chromium/mock-textdetection.js.headers
new file mode 100644
index 0000000..6c61a34
--- /dev/null
+++ b/test/wpt/tests/resources/chromium/mock-textdetection.js.headers
@@ -0,0 +1 @@
+Content-Type: text/javascript; charset=utf-8 \ No newline at end of file
diff --git a/test/wpt/tests/resources/chromium/nfc-mock.js b/test/wpt/tests/resources/chromium/nfc-mock.js
new file mode 100644
index 0000000..31a71b9
--- /dev/null
+++ b/test/wpt/tests/resources/chromium/nfc-mock.js
@@ -0,0 +1,437 @@
+import {NDEFErrorType, NDEFRecordTypeCategory, NFC, NFCReceiver} from '/gen/services/device/public/mojom/nfc.mojom.m.js';
+
+// Converts between NDEFMessageInit https://w3c.github.io/web-nfc/#dom-ndefmessage
+// and mojom.NDEFMessage structure, so that watch function can be tested.
+function toMojoNDEFMessage(message) {
+ let ndefMessage = {data: []};
+ for (let record of message.records) {
+ ndefMessage.data.push(toMojoNDEFRecord(record));
+ }
+ return ndefMessage;
+}
+
+function toMojoNDEFRecord(record) {
+ let nfcRecord = {};
+ // Simply checks the existence of ':' to decide whether it's an external
+ // type or a local type. As a mock, no need to really implement the validation
+ // algorithms for them.
+ if (record.recordType.startsWith(':')) {
+ nfcRecord.category = NDEFRecordTypeCategory.kLocal;
+ } else if (record.recordType.search(':') != -1) {
+ nfcRecord.category = NDEFRecordTypeCategory.kExternal;
+ } else {
+ nfcRecord.category = NDEFRecordTypeCategory.kStandardized;
+ }
+ nfcRecord.recordType = record.recordType;
+ nfcRecord.mediaType = record.mediaType;
+ nfcRecord.id = record.id;
+ if (record.recordType == 'text') {
+ nfcRecord.encoding = record.encoding == null? 'utf-8': record.encoding;
+ nfcRecord.lang = record.lang == null? 'en': record.lang;
+ }
+ nfcRecord.data = toByteArray(record.data);
+ if (record.data != null && record.data.records !== undefined) {
+ // |record.data| may be an NDEFMessageInit, i.e. the payload is a message.
+ nfcRecord.payloadMessage = toMojoNDEFMessage(record.data);
+ }
+ return nfcRecord;
+}
+
+// Converts JS objects to byte array.
+function toByteArray(data) {
+ if (data instanceof ArrayBuffer)
+ return new Uint8Array(data);
+ else if (ArrayBuffer.isView(data))
+ return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
+
+ let byteArray = new Uint8Array(0);
+ let tmpData = data;
+ if (typeof tmpData === 'object' || typeof tmpData === 'number')
+ tmpData = JSON.stringify(tmpData);
+
+ if (typeof tmpData === 'string')
+ byteArray = new TextEncoder().encode(tmpData);
+
+ return byteArray;
+}
+
+// Compares NDEFRecords that were provided / received by the mock service.
+// TODO: Use different getters to get received record data,
+// see spec changes at https://github.com/w3c/web-nfc/pull/243.
+self.compareNDEFRecords = function(providedRecord, receivedRecord) {
+ assert_equals(providedRecord.recordType, receivedRecord.recordType);
+
+ if (providedRecord.id === undefined) {
+ assert_equals(null, receivedRecord.id);
+ } else {
+ assert_equals(providedRecord.id, receivedRecord.id);
+ }
+
+ if (providedRecord.mediaType === undefined) {
+ assert_equals(null, receivedRecord.mediaType);
+ } else {
+ assert_equals(providedRecord.mediaType, receivedRecord.mediaType);
+ }
+
+ assert_not_equals(providedRecord.recordType, 'empty');
+
+ if (providedRecord.recordType == 'text') {
+ assert_equals(
+ providedRecord.encoding == null? 'utf-8': providedRecord.encoding,
+ receivedRecord.encoding);
+ assert_equals(providedRecord.lang == null? 'en': providedRecord.lang,
+ receivedRecord.lang);
+ } else {
+ assert_equals(null, receivedRecord.encoding);
+ assert_equals(null, receivedRecord.lang);
+ }
+
+ assert_array_equals(toByteArray(providedRecord.data),
+ new Uint8Array(receivedRecord.data));
+}
+
+// Compares NDEFWriteOptions structures that were provided to API and
+// received by the mock mojo service.
+self.assertNDEFWriteOptionsEqual = function(provided, received) {
+ if (provided.overwrite !== undefined)
+ assert_equals(provided.overwrite, !!received.overwrite);
+ else
+ assert_equals(!!received.overwrite, true);
+}
+
+// Compares NDEFReaderOptions structures that were provided to API and
+// received by the mock mojo service.
+self.assertNDEFReaderOptionsEqual = function(provided, received) {
+ if (provided.url !== undefined)
+ assert_equals(provided.url, received.url);
+ else
+ assert_equals(received.url, '');
+
+ if (provided.mediaType !== undefined)
+ assert_equals(provided.mediaType, received.mediaType);
+ else
+ assert_equals(received.mediaType, '');
+
+ if (provided.recordType !== undefined) {
+ assert_equals(provided.recordType, received.recordType);
+ }
+}
+
+function createNDEFError(type) {
+ return {error: (type != null ? {errorType: type, errorMessage: ''} : null)};
+}
+
+self.WebNFCTest = (() => {
+ class MockNFC {
+ constructor() {
+ this.receiver_ = new NFCReceiver(this);
+
+ this.interceptor_ = new MojoInterfaceInterceptor(NFC.$interfaceName);
+ this.interceptor_.oninterfacerequest = e => {
+ if (this.should_close_pipe_on_request_)
+ e.handle.close();
+ else
+ this.receiver_.$.bindHandle(e.handle);
+ }
+
+ this.interceptor_.start();
+
+ this.hw_status_ = NFCHWStatus.ENABLED;
+ this.pushed_message_ = null;
+ this.pending_write_options_ = null;
+ this.pending_push_promise_func_ = null;
+ this.push_completed_ = true;
+ this.pending_make_read_only_promise_func_ = null;
+ this.make_read_only_completed_ = true;
+ this.client_ = null;
+ this.watchers_ = [];
+ this.reading_messages_ = [];
+ this.operations_suspended_ = false;
+ this.is_formatted_tag_ = false;
+ this.data_transfer_failed_ = false;
+ this.should_close_pipe_on_request_ = false;
+ }
+
+ // NFC delegate functions.
+ async push(message, options) {
+ const error = this.getHWError();
+ if (error)
+ return error;
+ // Cancels previous pending push operation.
+ if (this.pending_push_promise_func_) {
+ this.cancelPendingPushOperation();
+ }
+
+ this.pushed_message_ = message;
+ this.pending_write_options_ = options;
+ return new Promise(resolve => {
+ if (this.operations_suspended_ || !this.push_completed_) {
+ // Leaves the push pending.
+ this.pending_push_promise_func_ = resolve;
+ } else if (this.is_formatted_tag_ && !options.overwrite) {
+ // Resolves with NotAllowedError if there are NDEF records on the device
+ // and overwrite is false.
+ resolve(createNDEFError(NDEFErrorType.NOT_ALLOWED));
+ } else if (this.data_transfer_failed_) {
+ // Resolves with NetworkError if data transfer fails.
+ resolve(createNDEFError(NDEFErrorType.IO_ERROR));
+ } else {
+ resolve(createNDEFError(null));
+ }
+ });
+ }
+
+ async cancelPush() {
+ this.cancelPendingPushOperation();
+ return createNDEFError(null);
+ }
+
+ async makeReadOnly(options) {
+ const error = this.getHWError();
+ if (error)
+ return error;
+ // Cancels previous pending makeReadOnly operation.
+ if (this.pending_make_read_only_promise_func_) {
+ this.cancelPendingMakeReadOnlyOperation();
+ }
+
+ if (this.operations_suspended_ || !this.make_read_only_completed_) {
+ // Leaves the makeReadOnly pending.
+ return new Promise(resolve => {
+ this.pending_make_read_only_promise_func_ = resolve;
+ });
+ } else if (this.data_transfer_failed_) {
+ // Resolves with NetworkError if data transfer fails.
+ return createNDEFError(NDEFErrorType.IO_ERROR);
+ } else {
+ return createNDEFError(null);
+ }
+ }
+
+ async cancelMakeReadOnly() {
+ this.cancelPendingMakeReadOnlyOperation();
+ return createNDEFError(null);
+ }
+
+ setClient(client) {
+ this.client_ = client;
+ }
+
+ async watch(id) {
+ assert_true(id > 0);
+ const error = this.getHWError();
+ if (error) {
+ return error;
+ }
+
+ this.watchers_.push({id: id});
+ // Ignores reading if NFC operation is suspended
+ // or the NFC tag does not expose NDEF technology.
+ if (!this.operations_suspended_) {
+ // Triggers onWatch if the new watcher matches existing messages.
+ for (let message of this.reading_messages_) {
+ this.client_.onWatch(
+ [id], fake_tag_serial_number, toMojoNDEFMessage(message));
+ }
+ }
+
+ return createNDEFError(null);
+ }
+
+ cancelWatch(id) {
+ let index = this.watchers_.findIndex(value => value.id === id);
+ if (index !== -1) {
+ this.watchers_.splice(index, 1);
+ }
+ }
+
+ getHWError() {
+ if (this.hw_status_ === NFCHWStatus.DISABLED)
+ return createNDEFError(NDEFErrorType.NOT_READABLE);
+ if (this.hw_status_ === NFCHWStatus.NOT_SUPPORTED)
+ return createNDEFError(NDEFErrorType.NOT_SUPPORTED);
+ return null;
+ }
+
+ setHWStatus(status) {
+ this.hw_status_ = status;
+ }
+
+ pushedMessage() {
+ return this.pushed_message_;
+ }
+
+ writeOptions() {
+ return this.pending_write_options_;
+ }
+
+ watchOptions() {
+ assert_not_equals(this.watchers_.length, 0);
+ return this.watchers_[this.watchers_.length - 1].options;
+ }
+
+ setPendingPushCompleted(result) {
+ this.push_completed_ = result;
+ }
+
+ setPendingMakeReadOnlyCompleted(result) {
+ this.make_read_only_completed_ = result;
+ }
+
+ reset() {
+ this.hw_status_ = NFCHWStatus.ENABLED;
+ this.watchers_ = [];
+ this.reading_messages_ = [];
+ this.operations_suspended_ = false;
+ this.cancelPendingPushOperation();
+ this.cancelPendingMakeReadOnlyOperation();
+ this.is_formatted_tag_ = false;
+ this.data_transfer_failed_ = false;
+ this.should_close_pipe_on_request_ = false;
+ }
+
+ cancelPendingPushOperation() {
+ if (this.pending_push_promise_func_) {
+ this.pending_push_promise_func_(
+ createNDEFError(NDEFErrorType.OPERATION_CANCELLED));
+ this.pending_push_promise_func_ = null;
+ }
+
+ this.pushed_message_ = null;
+ this.pending_write_options_ = null;
+ this.push_completed_ = true;
+ }
+
+ cancelPendingMakeReadOnlyOperation() {
+ if (this.pending_make_read_only_promise_func_) {
+ this.pending_make_read_only_promise_func_(
+ createNDEFError(NDEFErrorType.OPERATION_CANCELLED));
+ this.pending_make_read_only_promise_func_ = null;
+ }
+
+ this.make_read_only_completed_ = true;
+ }
+
+ // Sets message that is used to deliver NFC reading updates.
+ setReadingMessage(message) {
+ this.reading_messages_.push(message);
+ // Ignores reading if NFC operation is suspended.
+ if(this.operations_suspended_) return;
+ // when overwrite is false, the write algorithm will read the NFC tag
+ // to determine if it has NDEF records on it.
+ if (this.pending_write_options_ && this.pending_write_options_.overwrite)
+ return;
+ // Triggers onWatch if the new message matches existing watchers.
+ for (let watcher of this.watchers_) {
+ this.client_.onWatch(
+ [watcher.id], fake_tag_serial_number,
+ toMojoNDEFMessage(message));
+ }
+ }
+
+ // Suspends all pending NFC operations. Could be used when web page
+ // visibility is lost.
+ suspendNFCOperations() {
+ this.operations_suspended_ = true;
+ }
+
+ // Resumes all suspended NFC operations.
+ resumeNFCOperations() {
+ this.operations_suspended_ = false;
+ // Resumes pending NFC reading.
+ for (let watcher of this.watchers_) {
+ for (let message of this.reading_messages_) {
+ this.client_.onWatch(
+ [watcher.id], fake_tag_serial_number,
+ toMojoNDEFMessage(message));
+ }
+ }
+ // Resumes pending push operation.
+ if (this.pending_push_promise_func_ && this.push_completed_) {
+ this.pending_push_promise_func_(createNDEFError(null));
+ this.pending_push_promise_func_ = null;
+ }
+ // Resumes pending makeReadOnly operation.
+ if (this.pending_make_read_only_promise_func_ &&
+ this.make_read_only_completed_) {
+ this.pending_make_read_only_promise_func_(createNDEFError(null));
+ this.pending_make_read_only_promise_func_ = null;
+ }
+ }
+
+ // Simulates the device coming in proximity does not expose NDEF technology.
+ simulateNonNDEFTagDiscovered() {
+ // Notify NotSupportedError to all active readers.
+ if (this.watchers_.length != 0) {
+ this.client_.onError({
+ errorType: NDEFErrorType.NOT_SUPPORTED,
+ errorMessage: ''
+ });
+ }
+ // Reject the pending push with NotSupportedError.
+ if (this.pending_push_promise_func_) {
+ this.pending_push_promise_func_(
+ createNDEFError(NDEFErrorType.NOT_SUPPORTED));
+ this.pending_push_promise_func_ = null;
+ }
+ // Reject the pending makeReadOnly with NotSupportedError.
+ if (this.pending_make_read_only_promise_func_) {
+ this.pending_make_read_only_promise_func_(
+ createNDEFError(NDEFErrorType.NOT_SUPPORTED));
+ this.pending_make_read_only_promise_func_ = null;
+ }
+ }
+
+ setIsFormattedTag(isFormatted) {
+ this.is_formatted_tag_ = isFormatted;
+ }
+
+ simulateDataTransferFails() {
+ this.data_transfer_failed_ = true;
+ }
+
+ simulateClosedPipe() {
+ this.should_close_pipe_on_request_ = true;
+ }
+ }
+
+ let testInternal = {
+ initialized: false,
+ mockNFC: null
+ }
+
+ class NFCTestChromium {
+ constructor() {
+ Object.freeze(this); // Makes it immutable.
+ }
+
+ async initialize() {
+ if (testInternal.initialized)
+ throw new Error('Call reset() before initialize().');
+
+ // Grant nfc permissions for Chromium testdriver.
+ await test_driver.set_permission({ name: 'nfc' }, 'granted');
+
+ if (testInternal.mockNFC == null) {
+ testInternal.mockNFC = new MockNFC();
+ }
+ testInternal.initialized = true;
+ }
+
+ // Reuses the nfc mock but resets its state between test runs.
+ async reset() {
+ if (!testInternal.initialized)
+ throw new Error('Call initialize() before reset().');
+ testInternal.mockNFC.reset();
+ testInternal.initialized = false;
+
+ await new Promise(resolve => setTimeout(resolve, 0));
+ }
+
+ getMockNFC() {
+ return testInternal.mockNFC;
+ }
+ }
+
+ return NFCTestChromium;
+})();
diff --git a/test/wpt/tests/resources/chromium/web-bluetooth-test.js b/test/wpt/tests/resources/chromium/web-bluetooth-test.js
new file mode 100644
index 0000000..ecea5e7
--- /dev/null
+++ b/test/wpt/tests/resources/chromium/web-bluetooth-test.js
@@ -0,0 +1,629 @@
+'use strict';
+
+const content = {};
+const bluetooth = {};
+const MOJO_CHOOSER_EVENT_TYPE_MAP = {};
+
+function toMojoCentralState(state) {
+ switch (state) {
+ case 'absent':
+ return bluetooth.mojom.CentralState.ABSENT;
+ case 'powered-off':
+ return bluetooth.mojom.CentralState.POWERED_OFF;
+ case 'powered-on':
+ return bluetooth.mojom.CentralState.POWERED_ON;
+ default:
+ throw `Unsupported value ${state} for state.`;
+ }
+}
+
+// Converts bluetooth.mojom.WriteType to a string. If |writeType| is
+// invalid, this method will throw.
+function writeTypeToString(writeType) {
+ switch (writeType) {
+ case bluetooth.mojom.WriteType.kNone:
+ return 'none';
+ case bluetooth.mojom.WriteType.kWriteDefaultDeprecated:
+ return 'default-deprecated';
+ case bluetooth.mojom.WriteType.kWriteWithResponse:
+ return 'with-response';
+ case bluetooth.mojom.WriteType.kWriteWithoutResponse:
+ return 'without-response';
+ default:
+ throw `Unknown bluetooth.mojom.WriteType: ${writeType}`;
+ }
+}
+
+// Canonicalizes UUIDs and converts them to Mojo UUIDs.
+function canonicalizeAndConvertToMojoUUID(uuids) {
+ let canonicalUUIDs = uuids.map(val => ({uuid: BluetoothUUID.getService(val)}));
+ return canonicalUUIDs;
+}
+
+// Converts WebIDL a record<DOMString, BufferSource> to a map<K, array<uint8>> to
+// use for Mojo, where the value for K is calculated using keyFn.
+function convertToMojoMap(record, keyFn, isNumberKey = false) {
+ let map = new Map();
+ for (const [key, value] of Object.entries(record)) {
+ let buffer = ArrayBuffer.isView(value) ? value.buffer : value;
+ if (isNumberKey) {
+ let numberKey = parseInt(key);
+ if (Number.isNaN(numberKey))
+ throw `Map key ${key} is not a number`;
+ map.set(keyFn(numberKey), Array.from(new Uint8Array(buffer)));
+ continue;
+ }
+ map.set(keyFn(key), Array.from(new Uint8Array(buffer)));
+ }
+ return map;
+}
+
+function ArrayToMojoCharacteristicProperties(arr) {
+ const struct = {};
+ arr.forEach(property => { struct[property] = true; });
+ return struct;
+}
+
+class FakeBluetooth {
+ constructor() {
+ this.fake_bluetooth_ptr_ = new bluetooth.mojom.FakeBluetoothRemote();
+ this.fake_bluetooth_ptr_.$.bindNewPipeAndPassReceiver().bindInBrowser('process');
+ this.fake_central_ = null;
+ }
+
+ // Set it to indicate whether the platform supports BLE. For example,
+ // Windows 7 is a platform that doesn't support Low Energy. On the other
+ // hand Windows 10 is a platform that does support LE, even if there is no
+ // Bluetooth radio present.
+ async setLESupported(supported) {
+ if (typeof supported !== 'boolean') throw 'Type Not Supported';
+ await this.fake_bluetooth_ptr_.setLESupported(supported);
+ }
+
+ // Returns a promise that resolves with a FakeCentral that clients can use
+ // to simulate events that a device in the Central/Observer role would
+ // receive as well as monitor the operations performed by the device in the
+ // Central/Observer role.
+ // Calls sets LE as supported.
+ //
+ // A "Central" object would allow its clients to receive advertising events
+ // and initiate connections to peripherals i.e. operations of two roles
+ // defined by the Bluetooth Spec: Observer and Central.
+ // See Bluetooth 4.2 Vol 3 Part C 2.2.2 "Roles when Operating over an
+ // LE Physical Transport".
+ async simulateCentral({state}) {
+ if (this.fake_central_)
+ throw 'simulateCentral() should only be called once';
+
+ await this.setLESupported(true);
+
+ let {fakeCentral: fake_central_ptr} =
+ await this.fake_bluetooth_ptr_.simulateCentral(
+ toMojoCentralState(state));
+ this.fake_central_ = new FakeCentral(fake_central_ptr);
+ return this.fake_central_;
+ }
+
+ // Returns true if there are no pending responses.
+ async allResponsesConsumed() {
+ let {consumed} = await this.fake_bluetooth_ptr_.allResponsesConsumed();
+ return consumed;
+ }
+
+ // Returns a promise that resolves with a FakeChooser that clients can use to
+ // simulate chooser events.
+ async getManualChooser() {
+ if (typeof this.fake_chooser_ === 'undefined') {
+ this.fake_chooser_ = new FakeChooser();
+ }
+ return this.fake_chooser_;
+ }
+}
+
+// FakeCentral allows clients to simulate events that a device in the
+// Central/Observer role would receive as well as monitor the operations
+// performed by the device in the Central/Observer role.
+class FakeCentral {
+ constructor(fake_central_ptr) {
+ this.fake_central_ptr_ = fake_central_ptr;
+ this.peripherals_ = new Map();
+ }
+
+ // Simulates a peripheral with |address|, |name|, |manufacturerData| and
+ // |known_service_uuids| that has already been connected to the system. If the
+ // peripheral existed already it updates its name, manufacturer data, and
+ // known UUIDs. |known_service_uuids| should be an array of
+ // BluetoothServiceUUIDs
+ // https://webbluetoothcg.github.io/web-bluetooth/#typedefdef-bluetoothserviceuuid
+ //
+ // Platforms offer methods to retrieve devices that have already been
+ // connected to the system or weren't connected through the UA e.g. a user
+ // connected a peripheral through the system's settings. This method is
+ // intended to simulate peripherals that those methods would return.
+ async simulatePreconnectedPeripheral(
+ {address, name, manufacturerData = {}, knownServiceUUIDs = []}) {
+ await this.fake_central_ptr_.simulatePreconnectedPeripheral(
+ address, name,
+ convertToMojoMap(manufacturerData, Number, true /* isNumberKey */),
+ canonicalizeAndConvertToMojoUUID(knownServiceUUIDs));
+
+ return this.fetchOrCreatePeripheral_(address);
+ }
+
+ // Simulates an advertisement packet described by |scanResult| being received
+ // from a device. If central is currently scanning, the device will appear on
+ // the list of discovered devices.
+ async simulateAdvertisementReceived(scanResult) {
+ // Create a deep-copy to prevent the original |scanResult| from being
+ // modified when the UUIDs, manufacturer, and service data are converted.
+ let clonedScanResult = JSON.parse(JSON.stringify(scanResult));
+
+ if ('uuids' in scanResult.scanRecord) {
+ clonedScanResult.scanRecord.uuids =
+ canonicalizeAndConvertToMojoUUID(scanResult.scanRecord.uuids);
+ }
+
+ // Convert the optional appearance and txPower fields to the corresponding
+ // Mojo structures, since Mojo does not support optional interger values. If
+ // the fields are undefined, set the hasValue field as false and value as 0.
+ // Otherwise, set the hasValue field as true and value with the field value.
+ const has_appearance = 'appearance' in scanResult.scanRecord;
+ clonedScanResult.scanRecord.appearance = {
+ hasValue: has_appearance,
+ value: (has_appearance ? scanResult.scanRecord.appearance : 0)
+ }
+
+ const has_tx_power = 'txPower' in scanResult.scanRecord;
+ clonedScanResult.scanRecord.txPower = {
+ hasValue: has_tx_power,
+ value: (has_tx_power ? scanResult.scanRecord.txPower : 0)
+ }
+
+ // Convert manufacturerData from a record<DOMString, BufferSource> into a
+ // map<uint8, array<uint8>> for Mojo.
+ if ('manufacturerData' in scanResult.scanRecord) {
+ clonedScanResult.scanRecord.manufacturerData = convertToMojoMap(
+ scanResult.scanRecord.manufacturerData, Number,
+ true /* isNumberKey */);
+ }
+
+ // Convert serviceData from a record<DOMString, BufferSource> into a
+ // map<string, array<uint8>> for Mojo.
+ if ('serviceData' in scanResult.scanRecord) {
+ clonedScanResult.scanRecord.serviceData.serviceData = convertToMojoMap(
+ scanResult.scanRecord.serviceData, BluetoothUUID.getService,
+ false /* isNumberKey */);
+ }
+
+ await this.fake_central_ptr_.simulateAdvertisementReceived(
+ clonedScanResult);
+
+ return this.fetchOrCreatePeripheral_(clonedScanResult.deviceAddress);
+ }
+
+ // Simulates a change in the central device described by |state|. For example,
+ // setState('powered-off') can be used to simulate the central device powering
+ // off.
+ //
+ // This method should be used for any central state changes after
+ // simulateCentral() has been called to create a FakeCentral object.
+ async setState(state) {
+ await this.fake_central_ptr_.setState(toMojoCentralState(state));
+ }
+
+ // Create a fake_peripheral object from the given address.
+ fetchOrCreatePeripheral_(address) {
+ let peripheral = this.peripherals_.get(address);
+ if (peripheral === undefined) {
+ peripheral = new FakePeripheral(address, this.fake_central_ptr_);
+ this.peripherals_.set(address, peripheral);
+ }
+ return peripheral;
+ }
+}
+
+class FakePeripheral {
+ constructor(address, fake_central_ptr) {
+ this.address = address;
+ this.fake_central_ptr_ = fake_central_ptr;
+ }
+
+ // Adds a fake GATT Service with |uuid| to be discovered when discovering
+ // the peripheral's GATT Attributes. Returns a FakeRemoteGATTService
+ // corresponding to this service. |uuid| should be a BluetoothServiceUUIDs
+ // https://webbluetoothcg.github.io/web-bluetooth/#typedefdef-bluetoothserviceuuid
+ async addFakeService({uuid}) {
+ let {serviceId: service_id} = await this.fake_central_ptr_.addFakeService(
+ this.address, {uuid: BluetoothUUID.getService(uuid)});
+
+ if (service_id === null) throw 'addFakeService failed';
+
+ return new FakeRemoteGATTService(
+ service_id, this.address, this.fake_central_ptr_);
+ }
+
+ // Sets the next GATT Connection request response to |code|. |code| could be
+ // an HCI Error Code from BT 4.2 Vol 2 Part D 1.3 List Of Error Codes or a
+ // number outside that range returned by specific platforms e.g. Android
+ // returns 0x101 to signal a GATT failure
+ // https://developer.android.com/reference/android/bluetooth/BluetoothGatt.html#GATT_FAILURE
+ async setNextGATTConnectionResponse({code}) {
+ let {success} =
+ await this.fake_central_ptr_.setNextGATTConnectionResponse(
+ this.address, code);
+
+ if (success !== true) throw 'setNextGATTConnectionResponse failed.';
+ }
+
+ // Sets the next GATT Discovery request response for peripheral with
+ // |address| to |code|. |code| could be an HCI Error Code from
+ // BT 4.2 Vol 2 Part D 1.3 List Of Error Codes or a number outside that
+ // range returned by specific platforms e.g. Android returns 0x101 to signal
+ // a GATT failure
+ // https://developer.android.com/reference/android/bluetooth/BluetoothGatt.html#GATT_FAILURE
+ //
+ // The following procedures defined at BT 4.2 Vol 3 Part G Section 4.
+ // "GATT Feature Requirements" are used to discover attributes of the
+ // GATT Server:
+ // - Primary Service Discovery
+ // - Relationship Discovery
+ // - Characteristic Discovery
+ // - Characteristic Descriptor Discovery
+ // This method aims to simulate the response once all of these procedures
+ // have completed or if there was an error during any of them.
+ async setNextGATTDiscoveryResponse({code}) {
+ let {success} =
+ await this.fake_central_ptr_.setNextGATTDiscoveryResponse(
+ this.address, code);
+
+ if (success !== true) throw 'setNextGATTDiscoveryResponse failed.';
+ }
+
+ // Simulates a GATT disconnection from the peripheral with |address|.
+ async simulateGATTDisconnection() {
+ let {success} =
+ await this.fake_central_ptr_.simulateGATTDisconnection(this.address);
+
+ if (success !== true) throw 'simulateGATTDisconnection failed.';
+ }
+
+ // Simulates an Indication from the peripheral's GATT `Service Changed`
+ // Characteristic from BT 4.2 Vol 3 Part G 7.1. This Indication is signaled
+ // when services, characteristics, or descriptors are changed, added, or
+ // removed.
+ //
+ // The value for `Service Changed` is a range of attribute handles that have
+ // changed. However, this testing specification works at an abstracted
+ // level and does not expose setting attribute handles when adding
+ // attributes. Consequently, this simulate method should include the full
+ // range of all the peripheral's attribute handle values.
+ async simulateGATTServicesChanged() {
+ let {success} =
+ await this.fake_central_ptr_.simulateGATTServicesChanged(this.address);
+
+ if (success !== true) throw 'simulateGATTServicesChanged failed.';
+ }
+}
+
+class FakeRemoteGATTService {
+ constructor(service_id, peripheral_address, fake_central_ptr) {
+ this.service_id_ = service_id;
+ this.peripheral_address_ = peripheral_address;
+ this.fake_central_ptr_ = fake_central_ptr;
+ }
+
+ // Adds a fake GATT Characteristic with |uuid| and |properties|
+ // to this fake service. The characteristic will be found when discovering
+ // the peripheral's GATT Attributes. Returns a FakeRemoteGATTCharacteristic
+ // corresponding to the added characteristic.
+ async addFakeCharacteristic({uuid, properties}) {
+ let {characteristicId: characteristic_id} =
+ await this.fake_central_ptr_.addFakeCharacteristic(
+ {uuid: BluetoothUUID.getCharacteristic(uuid)},
+ ArrayToMojoCharacteristicProperties(properties),
+ this.service_id_,
+ this.peripheral_address_);
+
+ if (characteristic_id === null) throw 'addFakeCharacteristic failed';
+
+ return new FakeRemoteGATTCharacteristic(
+ characteristic_id, this.service_id_,
+ this.peripheral_address_, this.fake_central_ptr_);
+ }
+
+ // Removes the fake GATT service from its fake peripheral.
+ async remove() {
+ let {success} =
+ await this.fake_central_ptr_.removeFakeService(
+ this.service_id_,
+ this.peripheral_address_);
+
+ if (!success) throw 'remove failed';
+ }
+}
+
+class FakeRemoteGATTCharacteristic {
+ constructor(characteristic_id, service_id, peripheral_address,
+ fake_central_ptr) {
+ this.ids_ = [characteristic_id, service_id, peripheral_address];
+ this.descriptors_ = [];
+ this.fake_central_ptr_ = fake_central_ptr;
+ }
+
+ // Adds a fake GATT Descriptor with |uuid| to be discovered when
+ // discovering the peripheral's GATT Attributes. Returns a
+ // FakeRemoteGATTDescriptor corresponding to this descriptor. |uuid| should
+ // be a BluetoothDescriptorUUID
+ // https://webbluetoothcg.github.io/web-bluetooth/#typedefdef-bluetoothdescriptoruuid
+ async addFakeDescriptor({uuid}) {
+ let {descriptorId: descriptor_id} =
+ await this.fake_central_ptr_.addFakeDescriptor(
+ {uuid: BluetoothUUID.getDescriptor(uuid)}, ...this.ids_);
+
+ if (descriptor_id === null) throw 'addFakeDescriptor failed';
+
+ let fake_descriptor = new FakeRemoteGATTDescriptor(
+ descriptor_id, ...this.ids_, this.fake_central_ptr_);
+ this.descriptors_.push(fake_descriptor);
+
+ return fake_descriptor;
+ }
+
+ // Sets the next read response for characteristic to |code| and |value|.
+ // |code| could be a GATT Error Response from
+ // BT 4.2 Vol 3 Part F 3.4.1.1 Error Response or a number outside that range
+ // returned by specific platforms e.g. Android returns 0x101 to signal a GATT
+ // failure.
+ // https://developer.android.com/reference/android/bluetooth/BluetoothGatt.html#GATT_FAILURE
+ async setNextReadResponse(gatt_code, value=null) {
+ if (gatt_code === 0 && value === null) {
+ throw '|value| can\'t be null if read should success.';
+ }
+ if (gatt_code !== 0 && value !== null) {
+ throw '|value| must be null if read should fail.';
+ }
+
+ let {success} =
+ await this.fake_central_ptr_.setNextReadCharacteristicResponse(
+ gatt_code, value, ...this.ids_);
+
+ if (!success) throw 'setNextReadCharacteristicResponse failed';
+ }
+
+ // Sets the next write response for this characteristic to |code|. If
+ // writing to a characteristic that only supports 'write_without_response'
+ // the set response will be ignored.
+ // |code| could be a GATT Error Response from
+ // BT 4.2 Vol 3 Part F 3.4.1.1 Error Response or a number outside that range
+ // returned by specific platforms e.g. Android returns 0x101 to signal a GATT
+ // failure.
+ async setNextWriteResponse(gatt_code) {
+ let {success} =
+ await this.fake_central_ptr_.setNextWriteCharacteristicResponse(
+ gatt_code, ...this.ids_);
+
+ if (!success) throw 'setNextWriteCharacteristicResponse failed';
+ }
+
+ // Sets the next subscribe to notifications response for characteristic with
+ // |characteristic_id| in |service_id| and in |peripheral_address| to
+ // |code|. |code| could be a GATT Error Response from BT 4.2 Vol 3 Part F
+ // 3.4.1.1 Error Response or a number outside that range returned by
+ // specific platforms e.g. Android returns 0x101 to signal a GATT failure.
+ async setNextSubscribeToNotificationsResponse(gatt_code) {
+ let {success} =
+ await this.fake_central_ptr_.setNextSubscribeToNotificationsResponse(
+ gatt_code, ...this.ids_);
+
+ if (!success) throw 'setNextSubscribeToNotificationsResponse failed';
+ }
+
+ // Sets the next unsubscribe to notifications response for characteristic with
+ // |characteristic_id| in |service_id| and in |peripheral_address| to
+ // |code|. |code| could be a GATT Error Response from BT 4.2 Vol 3 Part F
+ // 3.4.1.1 Error Response or a number outside that range returned by
+ // specific platforms e.g. Android returns 0x101 to signal a GATT failure.
+ async setNextUnsubscribeFromNotificationsResponse(gatt_code) {
+ let {success} =
+ await this.fake_central_ptr_.setNextUnsubscribeFromNotificationsResponse(
+ gatt_code, ...this.ids_);
+
+ if (!success) throw 'setNextUnsubscribeToNotificationsResponse failed';
+ }
+
+ // Returns true if notifications from the characteristic have been subscribed
+ // to.
+ async isNotifying() {
+ let {success, isNotifying} =
+ await this.fake_central_ptr_.isNotifying(...this.ids_);
+
+ if (!success) throw 'isNotifying failed';
+
+ return isNotifying;
+ }
+
+ // Gets the last successfully written value to the characteristic and its
+ // write type. Write type is one of 'none', 'default-deprecated',
+ // 'with-response', 'without-response'. Returns {lastValue: null,
+ // lastWriteType: 'none'} if no value has yet been written to the
+ // characteristic.
+ async getLastWrittenValue() {
+ let {success, value, writeType} =
+ await this.fake_central_ptr_.getLastWrittenCharacteristicValue(
+ ...this.ids_);
+
+ if (!success) throw 'getLastWrittenCharacteristicValue failed';
+
+ return {lastValue: value, lastWriteType: writeTypeToString(writeType)};
+ }
+
+ // Removes the fake GATT Characteristic from its fake service.
+ async remove() {
+ let {success} =
+ await this.fake_central_ptr_.removeFakeCharacteristic(...this.ids_);
+
+ if (!success) throw 'remove failed';
+ }
+}
+
+class FakeRemoteGATTDescriptor {
+ constructor(descriptor_id,
+ characteristic_id,
+ service_id,
+ peripheral_address,
+ fake_central_ptr) {
+ this.ids_ = [
+ descriptor_id, characteristic_id, service_id, peripheral_address];
+ this.fake_central_ptr_ = fake_central_ptr;
+ }
+
+ // Sets the next read response for descriptor to |code| and |value|.
+ // |code| could be a GATT Error Response from
+ // BT 4.2 Vol 3 Part F 3.4.1.1 Error Response or a number outside that range
+ // returned by specific platforms e.g. Android returns 0x101 to signal a GATT
+ // failure.
+ // https://developer.android.com/reference/android/bluetooth/BluetoothGatt.html#GATT_FAILURE
+ async setNextReadResponse(gatt_code, value=null) {
+ if (gatt_code === 0 && value === null) {
+ throw '|value| cannot be null if read should succeed.';
+ }
+ if (gatt_code !== 0 && value !== null) {
+ throw '|value| must be null if read should fail.';
+ }
+
+ let {success} =
+ await this.fake_central_ptr_.setNextReadDescriptorResponse(
+ gatt_code, value, ...this.ids_);
+
+ if (!success) throw 'setNextReadDescriptorResponse failed';
+ }
+
+ // Sets the next write response for this descriptor to |code|.
+ // |code| could be a GATT Error Response from
+ // BT 4.2 Vol 3 Part F 3.4.1.1 Error Response or a number outside that range
+ // returned by specific platforms e.g. Android returns 0x101 to signal a GATT
+ // failure.
+ async setNextWriteResponse(gatt_code) {
+ let {success} =
+ await this.fake_central_ptr_.setNextWriteDescriptorResponse(
+ gatt_code, ...this.ids_);
+
+ if (!success) throw 'setNextWriteDescriptorResponse failed';
+ }
+
+ // Gets the last successfully written value to the descriptor.
+ // Returns null if no value has yet been written to the descriptor.
+ async getLastWrittenValue() {
+ let {success, value} =
+ await this.fake_central_ptr_.getLastWrittenDescriptorValue(
+ ...this.ids_);
+
+ if (!success) throw 'getLastWrittenDescriptorValue failed';
+
+ return value;
+ }
+
+ // Removes the fake GATT Descriptor from its fake characteristic.
+ async remove() {
+ let {success} =
+ await this.fake_central_ptr_.removeFakeDescriptor(...this.ids_);
+
+ if (!success) throw 'remove failed';
+ }
+}
+
+// FakeChooser allows clients to simulate user actions on a Bluetooth chooser,
+// and records the events produced by the Bluetooth chooser.
+class FakeChooser {
+ constructor() {
+ let fakeBluetoothChooserFactoryRemote =
+ new content.mojom.FakeBluetoothChooserFactoryRemote();
+ fakeBluetoothChooserFactoryRemote.$.bindNewPipeAndPassReceiver().bindInBrowser('process');
+
+ this.fake_bluetooth_chooser_ptr_ =
+ new content.mojom.FakeBluetoothChooserRemote();
+ this.fake_bluetooth_chooser_client_receiver_ =
+ new content.mojom.FakeBluetoothChooserClientReceiver(this);
+ fakeBluetoothChooserFactoryRemote.createFakeBluetoothChooser(
+ this.fake_bluetooth_chooser_ptr_.$.bindNewPipeAndPassReceiver(),
+ this.fake_bluetooth_chooser_client_receiver_.$.associateAndPassRemote());
+
+ this.events_ = new Array();
+ this.event_listener_ = null;
+ }
+
+ // If the chooser has received more events than |numOfEvents| this function
+ // will reject the promise, else it will wait until |numOfEvents| events are
+ // received before resolving with an array of |FakeBluetoothChooserEvent|
+ // objects.
+ async waitForEvents(numOfEvents) {
+ return new Promise(resolve => {
+ if (this.events_.length > numOfEvents) {
+ throw `Asked for ${numOfEvents} event(s), but received ` +
+ `${this.events_.length}.`;
+ }
+
+ this.event_listener_ = () => {
+ if (this.events_.length === numOfEvents) {
+ let result = Array.from(this.events_);
+ this.event_listener_ = null;
+ this.events_ = [];
+ resolve(result);
+ }
+ };
+ this.event_listener_();
+ });
+ }
+
+ async selectPeripheral(peripheral) {
+ if (!(peripheral instanceof FakePeripheral)) {
+ throw '|peripheral| must be an instance of FakePeripheral';
+ }
+ await this.fake_bluetooth_chooser_ptr_.selectPeripheral(peripheral.address);
+ }
+
+ async cancel() {
+ await this.fake_bluetooth_chooser_ptr_.cancel();
+ }
+
+ async rescan() {
+ await this.fake_bluetooth_chooser_ptr_.rescan();
+ }
+
+ onEvent(chooserEvent) {
+ chooserEvent.type = MOJO_CHOOSER_EVENT_TYPE_MAP[chooserEvent.type];
+ this.events_.push(chooserEvent);
+ if (this.event_listener_ !== null) {
+ this.event_listener_();
+ }
+ }
+}
+
+async function initializeChromiumResources() {
+ content.mojom = await import(
+ '/gen/content/web_test/common/fake_bluetooth_chooser.mojom.m.js');
+ bluetooth.mojom = await import(
+ '/gen/device/bluetooth/public/mojom/test/fake_bluetooth.mojom.m.js');
+
+ const map = MOJO_CHOOSER_EVENT_TYPE_MAP;
+ const types = content.mojom.ChooserEventType;
+ map[types.CHOOSER_OPENED] = 'chooser-opened';
+ map[types.CHOOSER_CLOSED] = 'chooser-closed';
+ map[types.ADAPTER_REMOVED] = 'adapter-removed';
+ map[types.ADAPTER_DISABLED] = 'adapter-disabled';
+ map[types.ADAPTER_ENABLED] = 'adapter-enabled';
+ map[types.DISCOVERY_FAILED_TO_START] = 'discovery-failed-to-start';
+ map[types.DISCOVERING] = 'discovering';
+ map[types.DISCOVERY_IDLE] = 'discovery-idle';
+ map[types.ADD_OR_UPDATE_DEVICE] = 'add-or-update-device';
+
+ // If this line fails, it means that current environment does not support the
+ // Web Bluetooth Test API.
+ try {
+ navigator.bluetooth.test = new FakeBluetooth();
+ } catch {
+ throw 'Web Bluetooth Test API is not implemented on this ' +
+ 'environment. See the bluetooth README at ' +
+ 'https://github.com/web-platform-tests/wpt/blob/master/bluetooth/README.md#web-bluetooth-testing';
+ }
+}
diff --git a/test/wpt/tests/resources/chromium/web-bluetooth-test.js.headers b/test/wpt/tests/resources/chromium/web-bluetooth-test.js.headers
new file mode 100644
index 0000000..6805c32
--- /dev/null
+++ b/test/wpt/tests/resources/chromium/web-bluetooth-test.js.headers
@@ -0,0 +1 @@
+Content-Type: text/javascript; charset=utf-8
diff --git a/test/wpt/tests/resources/chromium/webusb-child-test.js b/test/wpt/tests/resources/chromium/webusb-child-test.js
new file mode 100644
index 0000000..21412f6
--- /dev/null
+++ b/test/wpt/tests/resources/chromium/webusb-child-test.js
@@ -0,0 +1,47 @@
+'use strict';
+
+// This polyfill prepares a child context to be attached to a parent context.
+// The parent must call navigator.usb.test.attachToContext() to attach to the
+// child context.
+(() => {
+ if (this.constructor.name === 'DedicatedWorkerGlobalScope' ||
+ this !== window.top) {
+
+ // Run Chromium specific set up code.
+ if (typeof MojoInterfaceInterceptor !== 'undefined') {
+ let messageChannel = new MessageChannel();
+ messageChannel.port1.onmessage = async (messageEvent) => {
+ if (messageEvent.data.type === 'Attach') {
+ messageEvent.data.interfaces.forEach(interfaceName => {
+ let interfaceInterceptor =
+ new MojoInterfaceInterceptor(interfaceName);
+ interfaceInterceptor.oninterfacerequest =
+ e => messageChannel.port1.postMessage({
+ type: interfaceName,
+ handle: e.handle
+ }, [e.handle]);
+ interfaceInterceptor.start();
+ });
+
+ // Wait for a call to GetDevices() to ensure that the interface
+ // handles are forwarded to the parent context.
+ try {
+ await navigator.usb.getDevices();
+ } catch (e) {
+ // This can happen in case of, for example, testing usb disallowed
+ // iframe.
+ console.error(`getDevices() throws error: ${e.name}: ${e.message}`);
+ }
+
+ messageChannel.port1.postMessage({ type: 'Complete' });
+ }
+ };
+
+ let message = { type: 'ReadyForAttachment', port: messageChannel.port2 };
+ if (typeof Window !== 'undefined')
+ parent.postMessage(message, '*', [messageChannel.port2]);
+ else
+ postMessage(message, [messageChannel.port2]);
+ }
+ }
+})();
diff --git a/test/wpt/tests/resources/chromium/webusb-child-test.js.headers b/test/wpt/tests/resources/chromium/webusb-child-test.js.headers
new file mode 100644
index 0000000..6805c32
--- /dev/null
+++ b/test/wpt/tests/resources/chromium/webusb-child-test.js.headers
@@ -0,0 +1 @@
+Content-Type: text/javascript; charset=utf-8
diff --git a/test/wpt/tests/resources/chromium/webusb-test.js b/test/wpt/tests/resources/chromium/webusb-test.js
new file mode 100644
index 0000000..7cca63d
--- /dev/null
+++ b/test/wpt/tests/resources/chromium/webusb-test.js
@@ -0,0 +1,583 @@
+'use strict';
+
+// This polyfill library implements the WebUSB Test API as specified here:
+// https://wicg.github.io/webusb/test/
+
+(() => {
+
+// These variables are logically members of the USBTest class but are defined
+// here to hide them from being visible as fields of navigator.usb.test.
+let internal = {
+ intialized: false,
+
+ webUsbService: null,
+ webUsbServiceInterceptor: null,
+
+ messagePort: null,
+};
+
+let mojom = {};
+
+async function loadMojomDefinitions() {
+ const deviceMojom =
+ await import('/gen/services/device/public/mojom/usb_device.mojom.m.js');
+ const serviceMojom = await import(
+ '/gen/third_party/blink/public/mojom/usb/web_usb_service.mojom.m.js');
+ return {
+ ...deviceMojom,
+ ...serviceMojom,
+ };
+}
+
+function getMessagePort(target) {
+ return new Promise(resolve => {
+ target.addEventListener('message', messageEvent => {
+ if (messageEvent.data.type === 'ReadyForAttachment') {
+ if (internal.messagePort === null) {
+ internal.messagePort = messageEvent.data.port;
+ }
+ resolve();
+ }
+ }, {once: true});
+ });
+}
+
+// Converts an ECMAScript String object to an instance of
+// mojo_base.mojom.String16.
+function mojoString16ToString(string16) {
+ return String.fromCharCode.apply(null, string16.data);
+}
+
+// Converts an instance of mojo_base.mojom.String16 to an ECMAScript String.
+function stringToMojoString16(string) {
+ let array = new Array(string.length);
+ for (var i = 0; i < string.length; ++i) {
+ array[i] = string.charCodeAt(i);
+ }
+ return { data: array }
+}
+
+function fakeDeviceInitToDeviceInfo(guid, init) {
+ let deviceInfo = {
+ guid: guid + "",
+ usbVersionMajor: init.usbVersionMajor,
+ usbVersionMinor: init.usbVersionMinor,
+ usbVersionSubminor: init.usbVersionSubminor,
+ classCode: init.deviceClass,
+ subclassCode: init.deviceSubclass,
+ protocolCode: init.deviceProtocol,
+ vendorId: init.vendorId,
+ productId: init.productId,
+ deviceVersionMajor: init.deviceVersionMajor,
+ deviceVersionMinor: init.deviceVersionMinor,
+ deviceVersionSubminor: init.deviceVersionSubminor,
+ manufacturerName: stringToMojoString16(init.manufacturerName),
+ productName: stringToMojoString16(init.productName),
+ serialNumber: stringToMojoString16(init.serialNumber),
+ activeConfiguration: init.activeConfigurationValue,
+ configurations: []
+ };
+ init.configurations.forEach(config => {
+ var configInfo = {
+ configurationValue: config.configurationValue,
+ configurationName: stringToMojoString16(config.configurationName),
+ selfPowered: false,
+ remoteWakeup: false,
+ maximumPower: 0,
+ interfaces: [],
+ extraData: new Uint8Array()
+ };
+ config.interfaces.forEach(iface => {
+ var interfaceInfo = {
+ interfaceNumber: iface.interfaceNumber,
+ alternates: []
+ };
+ iface.alternates.forEach(alternate => {
+ var alternateInfo = {
+ alternateSetting: alternate.alternateSetting,
+ classCode: alternate.interfaceClass,
+ subclassCode: alternate.interfaceSubclass,
+ protocolCode: alternate.interfaceProtocol,
+ interfaceName: stringToMojoString16(alternate.interfaceName),
+ endpoints: [],
+ extraData: new Uint8Array()
+ };
+ alternate.endpoints.forEach(endpoint => {
+ var endpointInfo = {
+ endpointNumber: endpoint.endpointNumber,
+ packetSize: endpoint.packetSize,
+ synchronizationType: mojom.UsbSynchronizationType.NONE,
+ usageType: mojom.UsbUsageType.DATA,
+ pollingInterval: 0,
+ extraData: new Uint8Array()
+ };
+ switch (endpoint.direction) {
+ case "in":
+ endpointInfo.direction = mojom.UsbTransferDirection.INBOUND;
+ break;
+ case "out":
+ endpointInfo.direction = mojom.UsbTransferDirection.OUTBOUND;
+ break;
+ }
+ switch (endpoint.type) {
+ case "bulk":
+ endpointInfo.type = mojom.UsbTransferType.BULK;
+ break;
+ case "interrupt":
+ endpointInfo.type = mojom.UsbTransferType.INTERRUPT;
+ break;
+ case "isochronous":
+ endpointInfo.type = mojom.UsbTransferType.ISOCHRONOUS;
+ break;
+ }
+ alternateInfo.endpoints.push(endpointInfo);
+ });
+ interfaceInfo.alternates.push(alternateInfo);
+ });
+ configInfo.interfaces.push(interfaceInfo);
+ });
+ deviceInfo.configurations.push(configInfo);
+ });
+ return deviceInfo;
+}
+
+function convertMojoDeviceFilters(input) {
+ let output = [];
+ input.forEach(filter => {
+ output.push(convertMojoDeviceFilter(filter));
+ });
+ return output;
+}
+
+function convertMojoDeviceFilter(input) {
+ let output = {};
+ if (input.hasVendorId)
+ output.vendorId = input.vendorId;
+ if (input.hasProductId)
+ output.productId = input.productId;
+ if (input.hasClassCode)
+ output.classCode = input.classCode;
+ if (input.hasSubclassCode)
+ output.subclassCode = input.subclassCode;
+ if (input.hasProtocolCode)
+ output.protocolCode = input.protocolCode;
+ if (input.serialNumber)
+ output.serialNumber = mojoString16ToString(input.serialNumber);
+ return output;
+}
+
+class FakeDevice {
+ constructor(deviceInit) {
+ this.info_ = deviceInit;
+ this.opened_ = false;
+ this.currentConfiguration_ = null;
+ this.claimedInterfaces_ = new Map();
+ }
+
+ getConfiguration() {
+ if (this.currentConfiguration_) {
+ return Promise.resolve({
+ value: this.currentConfiguration_.configurationValue });
+ } else {
+ return Promise.resolve({ value: 0 });
+ }
+ }
+
+ open() {
+ assert_false(this.opened_);
+ this.opened_ = true;
+ return Promise.resolve({result: {success: mojom.UsbOpenDeviceSuccess.OK}});
+ }
+
+ close() {
+ assert_true(this.opened_);
+ this.opened_ = false;
+ return Promise.resolve();
+ }
+
+ setConfiguration(value) {
+ assert_true(this.opened_);
+
+ let selectedConfiguration = this.info_.configurations.find(
+ configuration => configuration.configurationValue == value);
+ // Blink should never request an invalid configuration.
+ assert_not_equals(selectedConfiguration, undefined);
+ this.currentConfiguration_ = selectedConfiguration;
+ return Promise.resolve({ success: true });
+ }
+
+ async claimInterface(interfaceNumber) {
+ assert_true(this.opened_);
+ assert_false(this.currentConfiguration_ == null, 'device configured');
+ assert_false(this.claimedInterfaces_.has(interfaceNumber),
+ 'interface already claimed');
+
+ const protectedInterfaces = new Set([
+ mojom.USB_AUDIO_CLASS,
+ mojom.USB_HID_CLASS,
+ mojom.USB_MASS_STORAGE_CLASS,
+ mojom.USB_SMART_CARD_CLASS,
+ mojom.USB_VIDEO_CLASS,
+ mojom.USB_AUDIO_VIDEO_CLASS,
+ mojom.USB_WIRELESS_CLASS,
+ ]);
+
+ let iface = this.currentConfiguration_.interfaces.find(
+ iface => iface.interfaceNumber == interfaceNumber);
+ // Blink should never request an invalid interface or alternate.
+ assert_false(iface == undefined);
+ if (iface.alternates.some(
+ alt => protectedInterfaces.has(alt.interfaceClass))) {
+ return {result: mojom.UsbClaimInterfaceResult.kProtectedClass};
+ }
+
+ this.claimedInterfaces_.set(interfaceNumber, 0);
+ return {result: mojom.UsbClaimInterfaceResult.kSuccess};
+ }
+
+ releaseInterface(interfaceNumber) {
+ assert_true(this.opened_);
+ assert_false(this.currentConfiguration_ == null, 'device configured');
+ assert_true(this.claimedInterfaces_.has(interfaceNumber));
+ this.claimedInterfaces_.delete(interfaceNumber);
+ return Promise.resolve({ success: true });
+ }
+
+ setInterfaceAlternateSetting(interfaceNumber, alternateSetting) {
+ assert_true(this.opened_);
+ assert_false(this.currentConfiguration_ == null, 'device configured');
+ assert_true(this.claimedInterfaces_.has(interfaceNumber));
+
+ let iface = this.currentConfiguration_.interfaces.find(
+ iface => iface.interfaceNumber == interfaceNumber);
+ // Blink should never request an invalid interface or alternate.
+ assert_false(iface == undefined);
+ assert_true(iface.alternates.some(
+ x => x.alternateSetting == alternateSetting));
+ this.claimedInterfaces_.set(interfaceNumber, alternateSetting);
+ return Promise.resolve({ success: true });
+ }
+
+ reset() {
+ assert_true(this.opened_);
+ return Promise.resolve({ success: true });
+ }
+
+ clearHalt(endpoint) {
+ assert_true(this.opened_);
+ assert_false(this.currentConfiguration_ == null, 'device configured');
+ // TODO(reillyg): Assert that endpoint is valid.
+ return Promise.resolve({ success: true });
+ }
+
+ async controlTransferIn(params, length, timeout) {
+ assert_true(this.opened_);
+
+ if ((params.recipient == mojom.UsbControlTransferRecipient.INTERFACE ||
+ params.recipient == mojom.UsbControlTransferRecipient.ENDPOINT) &&
+ this.currentConfiguration_ == null) {
+ return {
+ status: mojom.UsbTransferStatus.PERMISSION_DENIED,
+ };
+ }
+
+ return {
+ status: mojom.UsbTransferStatus.OK,
+ data: {
+ buffer: [
+ length >> 8, length & 0xff, params.request, params.value >> 8,
+ params.value & 0xff, params.index >> 8, params.index & 0xff
+ ]
+ }
+ };
+ }
+
+ async controlTransferOut(params, data, timeout) {
+ assert_true(this.opened_);
+
+ if ((params.recipient == mojom.UsbControlTransferRecipient.INTERFACE ||
+ params.recipient == mojom.UsbControlTransferRecipient.ENDPOINT) &&
+ this.currentConfiguration_ == null) {
+ return {
+ status: mojom.UsbTransferStatus.PERMISSION_DENIED,
+ };
+ }
+
+ return {status: mojom.UsbTransferStatus.OK, bytesWritten: data.byteLength};
+ }
+
+ genericTransferIn(endpointNumber, length, timeout) {
+ assert_true(this.opened_);
+ assert_false(this.currentConfiguration_ == null, 'device configured');
+ // TODO(reillyg): Assert that endpoint is valid.
+ let data = new Array(length);
+ for (let i = 0; i < length; ++i)
+ data[i] = i & 0xff;
+ return Promise.resolve(
+ {status: mojom.UsbTransferStatus.OK, data: {buffer: data}});
+ }
+
+ genericTransferOut(endpointNumber, data, timeout) {
+ assert_true(this.opened_);
+ assert_false(this.currentConfiguration_ == null, 'device configured');
+ // TODO(reillyg): Assert that endpoint is valid.
+ return Promise.resolve(
+ {status: mojom.UsbTransferStatus.OK, bytesWritten: data.byteLength});
+ }
+
+ isochronousTransferIn(endpointNumber, packetLengths, timeout) {
+ assert_true(this.opened_);
+ assert_false(this.currentConfiguration_ == null, 'device configured');
+ // TODO(reillyg): Assert that endpoint is valid.
+ let data = new Array(packetLengths.reduce((a, b) => a + b, 0));
+ let dataOffset = 0;
+ let packets = new Array(packetLengths.length);
+ for (let i = 0; i < packetLengths.length; ++i) {
+ for (let j = 0; j < packetLengths[i]; ++j)
+ data[dataOffset++] = j & 0xff;
+ packets[i] = {
+ length: packetLengths[i],
+ transferredLength: packetLengths[i],
+ status: mojom.UsbTransferStatus.OK
+ };
+ }
+ return Promise.resolve({data: {buffer: data}, packets: packets});
+ }
+
+ isochronousTransferOut(endpointNumber, data, packetLengths, timeout) {
+ assert_true(this.opened_);
+ assert_false(this.currentConfiguration_ == null, 'device configured');
+ // TODO(reillyg): Assert that endpoint is valid.
+ let packets = new Array(packetLengths.length);
+ for (let i = 0; i < packetLengths.length; ++i) {
+ packets[i] = {
+ length: packetLengths[i],
+ transferredLength: packetLengths[i],
+ status: mojom.UsbTransferStatus.OK
+ };
+ }
+ return Promise.resolve({ packets: packets });
+ }
+}
+
+class FakeWebUsbService {
+ constructor() {
+ this.receiver_ = new mojom.WebUsbServiceReceiver(this);
+ this.devices_ = new Map();
+ this.devicesByGuid_ = new Map();
+ this.client_ = null;
+ this.nextGuid_ = 0;
+ }
+
+ addBinding(handle) {
+ this.receiver_.$.bindHandle(handle);
+ }
+
+ addDevice(fakeDevice, info) {
+ let device = {
+ fakeDevice: fakeDevice,
+ guid: (this.nextGuid_++).toString(),
+ info: info,
+ receivers: [],
+ };
+ this.devices_.set(fakeDevice, device);
+ this.devicesByGuid_.set(device.guid, device);
+ if (this.client_)
+ this.client_.onDeviceAdded(fakeDeviceInitToDeviceInfo(device.guid, info));
+ }
+
+ async forgetDevice(guid) {
+ // Permissions are currently untestable through WPT.
+ }
+
+ removeDevice(fakeDevice) {
+ let device = this.devices_.get(fakeDevice);
+ if (!device)
+ throw new Error('Cannot remove unknown device.');
+
+ for (const receiver of device.receivers)
+ receiver.$.close();
+ this.devices_.delete(device.fakeDevice);
+ this.devicesByGuid_.delete(device.guid);
+ if (this.client_) {
+ this.client_.onDeviceRemoved(
+ fakeDeviceInitToDeviceInfo(device.guid, device.info));
+ }
+ }
+
+ removeAllDevices() {
+ this.devices_.forEach(device => {
+ for (const receiver of device.receivers)
+ receiver.$.close();
+ this.client_.onDeviceRemoved(
+ fakeDeviceInitToDeviceInfo(device.guid, device.info));
+ });
+ this.devices_.clear();
+ this.devicesByGuid_.clear();
+ }
+
+ getDevices() {
+ let devices = [];
+ this.devices_.forEach(device => {
+ devices.push(fakeDeviceInitToDeviceInfo(device.guid, device.info));
+ });
+ return Promise.resolve({ results: devices });
+ }
+
+ getDevice(guid, request) {
+ let retrievedDevice = this.devicesByGuid_.get(guid);
+ if (retrievedDevice) {
+ const receiver =
+ new mojom.UsbDeviceReceiver(new FakeDevice(retrievedDevice.info));
+ receiver.$.bindHandle(request.handle);
+ receiver.onConnectionError.addListener(() => {
+ if (retrievedDevice.fakeDevice.onclose)
+ retrievedDevice.fakeDevice.onclose();
+ });
+ retrievedDevice.receivers.push(receiver);
+ } else {
+ request.handle.close();
+ }
+ }
+
+ getPermission(options) {
+ return new Promise(resolve => {
+ if (navigator.usb.test.onrequestdevice) {
+ navigator.usb.test.onrequestdevice(
+ new USBDeviceRequestEvent(options, resolve));
+ } else {
+ resolve({ result: null });
+ }
+ });
+ }
+
+ setClient(client) {
+ this.client_ = client;
+ }
+}
+
+class USBDeviceRequestEvent {
+ constructor(options, resolve) {
+ this.filters = convertMojoDeviceFilters(options.filters);
+ this.exclusionFilters = convertMojoDeviceFilters(options.exclusionFilters);
+ this.resolveFunc_ = resolve;
+ }
+
+ respondWith(value) {
+ // Wait until |value| resolves (if it is a Promise). This function returns
+ // no value.
+ Promise.resolve(value).then(fakeDevice => {
+ let device = internal.webUsbService.devices_.get(fakeDevice);
+ let result = null;
+ if (device) {
+ result = fakeDeviceInitToDeviceInfo(device.guid, device.info);
+ }
+ this.resolveFunc_({ result: result });
+ }, () => {
+ this.resolveFunc_({ result: null });
+ });
+ }
+}
+
+// Unlike FakeDevice this class is exported to callers of USBTest.addFakeDevice.
+class FakeUSBDevice {
+ constructor() {
+ this.onclose = null;
+ }
+
+ disconnect() {
+ setTimeout(() => internal.webUsbService.removeDevice(this), 0);
+ }
+}
+
+class USBTest {
+ constructor() {
+ this.onrequestdevice = undefined;
+ }
+
+ async initialize() {
+ if (internal.initialized)
+ return;
+
+ // Be ready to handle 'ReadyForAttachment' message from child iframes.
+ if ('window' in self) {
+ getMessagePort(window);
+ }
+
+ mojom = await loadMojomDefinitions();
+ internal.webUsbService = new FakeWebUsbService();
+ internal.webUsbServiceInterceptor =
+ new MojoInterfaceInterceptor(mojom.WebUsbService.$interfaceName);
+ internal.webUsbServiceInterceptor.oninterfacerequest =
+ e => internal.webUsbService.addBinding(e.handle);
+ internal.webUsbServiceInterceptor.start();
+
+ // Wait for a call to GetDevices() to pass between the renderer and the
+ // mock in order to establish that everything is set up.
+ await navigator.usb.getDevices();
+ internal.initialized = true;
+ }
+
+ // Returns a promise that is resolved when the implementation of |usb| in the
+ // global scope for |context| is controlled by the current context.
+ attachToContext(context) {
+ if (!internal.initialized)
+ throw new Error('Call initialize() before attachToContext()');
+
+ let target = context.constructor.name === 'Worker' ? context : window;
+ return getMessagePort(target).then(() => {
+ return new Promise(resolve => {
+ internal.messagePort.onmessage = channelEvent => {
+ switch (channelEvent.data.type) {
+ case mojom.WebUsbService.$interfaceName:
+ internal.webUsbService.addBinding(channelEvent.data.handle);
+ break;
+ case 'Complete':
+ resolve();
+ break;
+ }
+ };
+ internal.messagePort.postMessage({
+ type: 'Attach',
+ interfaces: [
+ mojom.WebUsbService.$interfaceName,
+ ]
+ });
+ });
+ });
+ }
+
+ addFakeDevice(deviceInit) {
+ if (!internal.initialized)
+ throw new Error('Call initialize() before addFakeDevice().');
+
+ // |addDevice| and |removeDevice| are called in a setTimeout callback so
+ // that tests do not rely on the device being immediately available which
+ // may not be true for all implementations of this test API.
+ let fakeDevice = new FakeUSBDevice();
+ setTimeout(
+ () => internal.webUsbService.addDevice(fakeDevice, deviceInit), 0);
+ return fakeDevice;
+ }
+
+ reset() {
+ if (!internal.initialized)
+ throw new Error('Call initialize() before reset().');
+
+ // Reset the mocks in a setTimeout callback so that tests do not rely on
+ // the fact that this polyfill can do this synchronously.
+ return new Promise(resolve => {
+ setTimeout(() => {
+ if (internal.messagePort !== null)
+ internal.messagePort.close();
+ internal.messagePort = null;
+ internal.webUsbService.removeAllDevices();
+ resolve();
+ }, 0);
+ });
+ }
+}
+
+navigator.usb.test = new USBTest();
+
+})();
diff --git a/test/wpt/tests/resources/chromium/webusb-test.js.headers b/test/wpt/tests/resources/chromium/webusb-test.js.headers
new file mode 100644
index 0000000..6805c32
--- /dev/null
+++ b/test/wpt/tests/resources/chromium/webusb-test.js.headers
@@ -0,0 +1 @@
+Content-Type: text/javascript; charset=utf-8
diff --git a/test/wpt/tests/resources/chromium/webxr-test-math-helper.js b/test/wpt/tests/resources/chromium/webxr-test-math-helper.js
new file mode 100644
index 0000000..22c6c12
--- /dev/null
+++ b/test/wpt/tests/resources/chromium/webxr-test-math-helper.js
@@ -0,0 +1,298 @@
+'use strict';
+
+// Math helper - used mainly in hit test implementation done by webxr-test.js
+class XRMathHelper {
+ static toString(p) {
+ return "[" + p.x + "," + p.y + "," + p.z + "," + p.w + "]";
+ }
+
+ static transform_by_matrix(matrix, point) {
+ return {
+ x : matrix[0] * point.x + matrix[4] * point.y + matrix[8] * point.z + matrix[12] * point.w,
+ y : matrix[1] * point.x + matrix[5] * point.y + matrix[9] * point.z + matrix[13] * point.w,
+ z : matrix[2] * point.x + matrix[6] * point.y + matrix[10] * point.z + matrix[14] * point.w,
+ w : matrix[3] * point.x + matrix[7] * point.y + matrix[11] * point.z + matrix[15] * point.w,
+ };
+ }
+
+ static neg(p) {
+ return {x : -p.x, y : -p.y, z : -p.z, w : p.w};
+ }
+
+ static sub(lhs, rhs) {
+ // .w is treated here like an entity type, 1 signifies points, 0 signifies vectors.
+ // point - point, point - vector, vector - vector are ok, vector - point is not.
+ if (lhs.w != rhs.w && lhs.w == 0.0) {
+ throw new Error("vector - point not allowed: " + toString(lhs) + "-" + toString(rhs));
+ }
+
+ return {x : lhs.x - rhs.x, y : lhs.y - rhs.y, z : lhs.z - rhs.z, w : lhs.w - rhs.w};
+ }
+
+ static add(lhs, rhs) {
+ if (lhs.w == rhs.w && lhs.w == 1.0) {
+ throw new Error("point + point not allowed", p1, p2);
+ }
+
+ return {x : lhs.x + rhs.x, y : lhs.y + rhs.y, z : lhs.z + rhs.z, w : lhs.w + rhs.w};
+ }
+
+ static cross(lhs, rhs) {
+ if (lhs.w != 0.0 || rhs.w != 0.0) {
+ throw new Error("cross product not allowed: " + toString(lhs) + "x" + toString(rhs));
+ }
+
+ return {
+ x : lhs.y * rhs.z - lhs.z * rhs.y,
+ y : lhs.z * rhs.x - lhs.x * rhs.z,
+ z : lhs.x * rhs.y - lhs.y * rhs.x,
+ w : 0
+ };
+ }
+
+ static dot(lhs, rhs) {
+ if (lhs.w != 0 || rhs.w != 0) {
+ throw new Error("dot product not allowed: " + toString(lhs) + "x" + toString(rhs));
+ }
+
+ return lhs.x * rhs.x + lhs.y * rhs.y + lhs.z * rhs.z;
+ }
+
+ static mul(scalar, vector) {
+ if (vector.w != 0) {
+ throw new Error("scalar * vector not allowed", scalar, vector);
+ }
+
+ return {x : vector.x * scalar, y : vector.y * scalar, z : vector.z * scalar, w : vector.w};
+ }
+
+ static length(vector) {
+ return Math.sqrt(XRMathHelper.dot(vector, vector));
+ }
+
+ static normalize(vector) {
+ const l = XRMathHelper.length(vector);
+ return XRMathHelper.mul(1.0/l, vector);
+ }
+
+ // All |face|'s points and |point| must be co-planar.
+ static pointInFace(point, face) {
+ const normalize = XRMathHelper.normalize;
+ const sub = XRMathHelper.sub;
+ const length = XRMathHelper.length;
+ const cross = XRMathHelper.cross;
+
+ let onTheRight = null;
+ let previous_point = face[face.length - 1];
+
+ // |point| is in |face| if it's on the same side of all the edges.
+ for (let i = 0; i < face.length; ++i) {
+ const current_point = face[i];
+
+ const edge_direction = normalize(sub(current_point, previous_point));
+ const turn_direction = normalize(sub(point, current_point));
+
+ const sin_turn_angle = length(cross(edge_direction, turn_direction));
+
+ if (onTheRight == null) {
+ onTheRight = sin_turn_angle >= 0;
+ } else {
+ if (onTheRight && sin_turn_angle < 0) return false;
+ if (!onTheRight && sin_turn_angle > 0) return false;
+ }
+
+ previous_point = current_point;
+ }
+
+ return true;
+ }
+
+ static det2x2(m00, m01, m10, m11) {
+ return m00 * m11 - m01 * m10;
+ }
+
+ static det3x3(
+ m00, m01, m02,
+ m10, m11, m12,
+ m20, m21, m22
+ ){
+ const det2x2 = XRMathHelper.det2x2;
+
+ return m00 * det2x2(m11, m12, m21, m22)
+ - m01 * det2x2(m10, m12, m20, m22)
+ + m02 * det2x2(m10, m11, m20, m21);
+ }
+
+ static det4x4(
+ m00, m01, m02, m03,
+ m10, m11, m12, m13,
+ m20, m21, m22, m23,
+ m30, m31, m32, m33
+ ) {
+ const det3x3 = XRMathHelper.det3x3;
+
+ return m00 * det3x3(m11, m12, m13,
+ m21, m22, m23,
+ m31, m32, m33)
+ - m01 * det3x3(m10, m12, m13,
+ m20, m22, m23,
+ m30, m32, m33)
+ + m02 * det3x3(m10, m11, m13,
+ m20, m21, m23,
+ m30, m31, m33)
+ - m03 * det3x3(m10, m11, m12,
+ m20, m21, m22,
+ m30, m31, m32);
+ }
+
+ static inv2(m) {
+ // mij - i-th column, j-th row
+ const m00 = m[0], m01 = m[1], m02 = m[2], m03 = m[3];
+ const m10 = m[4], m11 = m[5], m12 = m[6], m13 = m[7];
+ const m20 = m[8], m21 = m[9], m22 = m[10], m23 = m[11];
+ const m30 = m[12], m31 = m[13], m32 = m[14], m33 = m[15];
+
+ const det = det4x4(
+ m00, m01, m02, m03,
+ m10, m11, m12, m13,
+ m20, m21, m22, m23,
+ m30, m31, m32, m33
+ );
+ }
+
+ static transpose(m) {
+ const result = Array(16);
+ for (let i = 0; i < 4; i++) {
+ for (let j = 0; j < 4; j++) {
+ result[i * 4 + j] = m[j * 4 + i];
+ }
+ }
+ return result;
+ }
+
+ // Inverts the matrix, ported from transformation_matrix.cc.
+ static inverse(m) {
+ const det3x3 = XRMathHelper.det3x3;
+
+ // mij - i-th column, j-th row
+ const m00 = m[0], m01 = m[1], m02 = m[2], m03 = m[3];
+ const m10 = m[4], m11 = m[5], m12 = m[6], m13 = m[7];
+ const m20 = m[8], m21 = m[9], m22 = m[10], m23 = m[11];
+ const m30 = m[12], m31 = m[13], m32 = m[14], m33 = m[15];
+
+ const det = XRMathHelper.det4x4(
+ m00, m01, m02, m03,
+ m10, m11, m12, m13,
+ m20, m21, m22, m23,
+ m30, m31, m32, m33
+ );
+
+ if (Math.abs(det) < 0.0001) {
+ return null;
+ }
+
+ const invDet = 1.0 / det;
+ // Calculate `comatrix * 1/det`:
+ const result2 = [
+ // First column (m0r):
+ invDet * det3x3(m11, m12, m13, m21, m22, m23, m32, m32, m33),
+ -invDet * det3x3(m10, m12, m13, m20, m22, m23, m30, m32, m33),
+ invDet * det3x3(m10, m11, m13, m20, m21, m23, m30, m31, m33),
+ -invDet * det3x3(m10, m11, m12, m20, m21, m22, m30, m31, m32),
+ // Second column (m1r):
+ -invDet * det3x3(m01, m02, m03, m21, m22, m23, m32, m32, m33),
+ invDet * det3x3(m00, m02, m03, m20, m22, m23, m30, m32, m33),
+ -invDet * det3x3(m00, m01, m03, m20, m21, m23, m30, m31, m33),
+ invDet * det3x3(m00, m01, m02, m20, m21, m22, m30, m31, m32),
+ // Third column (m2r):
+ invDet * det3x3(m01, m02, m03, m11, m12, m13, m31, m32, m33),
+ -invDet * det3x3(m00, m02, m03, m10, m12, m13, m30, m32, m33),
+ invDet * det3x3(m00, m01, m03, m10, m11, m13, m30, m31, m33),
+ -invDet * det3x3(m00, m01, m02, m10, m11, m12, m30, m31, m32),
+ // Fourth column (m3r):
+ -invDet * det3x3(m01, m02, m03, m11, m12, m13, m21, m22, m23),
+ invDet * det3x3(m00, m02, m03, m10, m12, m13, m20, m22, m23),
+ -invDet * det3x3(m00, m01, m03, m10, m11, m13, m20, m21, m23),
+ invDet * det3x3(m00, m01, m02, m10, m11, m12, m20, m21, m22),
+ ];
+
+ // Actual inverse is `1/det * transposed(comatrix)`:
+ return XRMathHelper.transpose(result2);
+ }
+
+ static mul4x4(m1, m2) {
+ if (m1 == null || m2 == null) {
+ return null;
+ }
+
+ const result = Array(16);
+
+ for (let row = 0; row < 4; row++) {
+ for (let col = 0; col < 4; col++) {
+ result[4 * col + row] = 0;
+ for(let i = 0; i < 4; i++) {
+ result[4 * col + row] += m1[4 * i + row] * m2[4 * col + i];
+ }
+ }
+ }
+
+ return result;
+ }
+
+ // Decomposes a matrix, with the assumption that the passed in matrix is
+ // a rigid transformation (i.e. position and rotation *only*!).
+ // The result is an object with `position` and `orientation` keys, which should
+ // be compatible with FakeXRRigidTransformInit.
+ // The implementation should match the behavior of gfx::Transform, but assumes
+ // that scale, skew & perspective are not present in the matrix so it could be
+ // simplified.
+ static decomposeRigidTransform(m) {
+ const m00 = m[0], m01 = m[1], m02 = m[2], m03 = m[3];
+ const m10 = m[4], m11 = m[5], m12 = m[6], m13 = m[7];
+ const m20 = m[8], m21 = m[9], m22 = m[10], m23 = m[11];
+ const m30 = m[12], m31 = m[13], m32 = m[14], m33 = m[15];
+
+ const position = [m30, m31, m32];
+ const orientation = [0, 0, 0, 0];
+
+ const trace = m00 + m11 + m22;
+ if (trace > 0) {
+ const S = Math.sqrt(trace + 1) * 2;
+ orientation[3] = 0.25 * S;
+ orientation[0] = (m12 - m21) / S;
+ orientation[1] = (m20 - m02) / S;
+ orientation[2] = (m01 - m10) / S;
+ } else if (m00 > m11 && m00 > m22) {
+ const S = Math.sqrt(1.0 + m00 - m11 - m22) * 2;
+ orientation[3] = (m12 - m21) / S;
+ orientation[0] = 0.25 * S;
+ orientation[1] = (m01 + m10) / S;
+ orientation[2] = (m20 + m02) / S;
+ } else if (m11 > m22) {
+ const S = Math.sqrt(1.0 + m11 - m00 - m22) * 2;
+ orientation[3] = (m20 - m02) / S;
+ orientation[0] = (m01 + m10) / S;
+ orientation[1] = 0.25 * S;
+ orientation[2] = (m12 + m21) / S;
+ } else {
+ const S = Math.sqrt(1.0 + m22 - m00 - m11) * 2;
+ orientation[3] = (m01 - m10) / S;
+ orientation[0] = (m20 + m02) / S;
+ orientation[1] = (m12 + m21) / S;
+ orientation[2] = 0.25 * S;
+ }
+
+ return { position, orientation };
+ }
+
+ static identity() {
+ return [
+ 1, 0, 0, 0,
+ 0, 1, 0, 0,
+ 0, 0, 1, 0,
+ 0, 0, 0, 1
+ ];
+ };
+}
+
+XRMathHelper.EPSILON = 0.001;
diff --git a/test/wpt/tests/resources/chromium/webxr-test-math-helper.js.headers b/test/wpt/tests/resources/chromium/webxr-test-math-helper.js.headers
new file mode 100644
index 0000000..6805c32
--- /dev/null
+++ b/test/wpt/tests/resources/chromium/webxr-test-math-helper.js.headers
@@ -0,0 +1 @@
+Content-Type: text/javascript; charset=utf-8
diff --git a/test/wpt/tests/resources/chromium/webxr-test.js b/test/wpt/tests/resources/chromium/webxr-test.js
new file mode 100644
index 0000000..c5eb1bd
--- /dev/null
+++ b/test/wpt/tests/resources/chromium/webxr-test.js
@@ -0,0 +1,2125 @@
+import * as vrMojom from '/gen/device/vr/public/mojom/vr_service.mojom.m.js';
+import * as xrSessionMojom from '/gen/device/vr/public/mojom/xr_session.mojom.m.js';
+import {GamepadHand, GamepadMapping} from '/gen/device/gamepad/public/mojom/gamepad.mojom.m.js';
+
+// This polyfill library implements the WebXR Test API as specified here:
+// https://github.com/immersive-web/webxr-test-api
+
+const defaultMojoFromFloor = {
+ matrix: [1, 0, 0, 0,
+ 0, 1, 0, 0,
+ 0, 0, 1, 0,
+ 0, -1.65, 0, 1]
+};
+const default_stage_parameters = {
+ mojoFromFloor: defaultMojoFromFloor,
+ bounds: null
+};
+
+const default_framebuffer_scale = 0.7;
+
+function getMatrixFromTransform(transform) {
+ const x = transform.orientation[0];
+ const y = transform.orientation[1];
+ const z = transform.orientation[2];
+ const w = transform.orientation[3];
+
+ const m11 = 1.0 - 2.0 * (y * y + z * z);
+ const m21 = 2.0 * (x * y + z * w);
+ const m31 = 2.0 * (x * z - y * w);
+
+ const m12 = 2.0 * (x * y - z * w);
+ const m22 = 1.0 - 2.0 * (x * x + z * z);
+ const m32 = 2.0 * (y * z + x * w);
+
+ const m13 = 2.0 * (x * z + y * w);
+ const m23 = 2.0 * (y * z - x * w);
+ const m33 = 1.0 - 2.0 * (x * x + y * y);
+
+ const m14 = transform.position[0];
+ const m24 = transform.position[1];
+ const m34 = transform.position[2];
+
+ // Column-major linearized order is expected.
+ return [m11, m21, m31, 0,
+ m12, m22, m32, 0,
+ m13, m23, m33, 0,
+ m14, m24, m34, 1];
+}
+
+function getPoseFromTransform(transform) {
+ const [px, py, pz] = transform.position;
+ const [ox, oy, oz, ow] = transform.orientation;
+ return {
+ position: {x: px, y: py, z: pz},
+ orientation: {x: ox, y: oy, z: oz, w: ow},
+ };
+}
+
+function composeGFXTransform(fakeTransformInit) {
+ return {matrix: getMatrixFromTransform(fakeTransformInit)};
+}
+
+// Value equality for camera image init objects - they must contain `width` &
+// `height` properties and may contain `pixels` property.
+function isSameCameraImageInit(rhs, lhs) {
+ return lhs.width === rhs.width && lhs.height === rhs.height && lhs.pixels === rhs.pixels;
+}
+
+class ChromeXRTest {
+ constructor() {
+ this.mockVRService_ = new MockVRService();
+ }
+
+ // WebXR Test API
+ simulateDeviceConnection(init_params) {
+ return Promise.resolve(this.mockVRService_._addRuntime(init_params));
+ }
+
+ disconnectAllDevices() {
+ this.mockVRService_._removeAllRuntimes();
+ return Promise.resolve();
+ }
+
+ simulateUserActivation(callback) {
+ if (window.top !== window) {
+ // test_driver.click only works for the toplevel frame. This alternate
+ // Chrome-specific method is sufficient for starting an XR session in an
+ // iframe, and is used in platform-specific tests.
+ //
+ // TODO(https://github.com/web-platform-tests/wpt/issues/20282): use
+ // a cross-platform method if available.
+ xr_debug('simulateUserActivation', 'use eventSender');
+ document.addEventListener('click', callback);
+ eventSender.mouseMoveTo(0, 0);
+ eventSender.mouseDown();
+ eventSender.mouseUp();
+ document.removeEventListener('click', callback);
+ return;
+ }
+ const button = document.createElement('button');
+ button.textContent = 'click to continue test';
+ button.style.display = 'block';
+ button.style.fontSize = '20px';
+ button.style.padding = '10px';
+ button.onclick = () => {
+ callback();
+ document.body.removeChild(button);
+ };
+ document.body.appendChild(button);
+ test_driver.click(button);
+ }
+
+ // Helper method leveraged by chrome-specific setups.
+ Debug(name, msg) {
+ console.log(new Date().toISOString() + ' DEBUG[' + name + '] ' + msg);
+ }
+}
+
+// Mocking class definitions
+
+// Mock service implements the VRService mojo interface.
+class MockVRService {
+ constructor() {
+ this.receiver_ = new vrMojom.VRServiceReceiver(this);
+ this.runtimes_ = [];
+
+ this.interceptor_ =
+ new MojoInterfaceInterceptor(vrMojom.VRService.$interfaceName);
+ this.interceptor_.oninterfacerequest =
+ e => this.receiver_.$.bindHandle(e.handle);
+ this.interceptor_.start();
+ }
+
+ // WebXR Test API Implementation Helpers
+ _addRuntime(fakeDeviceInit) {
+ const runtime = new MockRuntime(fakeDeviceInit, this);
+ this.runtimes_.push(runtime);
+
+ if (this.client_) {
+ this.client_.onDeviceChanged();
+ }
+
+ return runtime;
+ }
+
+ _removeAllRuntimes() {
+ if (this.client_) {
+ this.client_.onDeviceChanged();
+ }
+
+ this.runtimes_ = [];
+ }
+
+ _removeRuntime(device) {
+ const index = this.runtimes_.indexOf(device);
+ if (index >= 0) {
+ this.runtimes_.splice(index, 1);
+ if (this.client_) {
+ this.client_.onDeviceChanged();
+ }
+ }
+ }
+
+ // VRService overrides
+ setClient(client) {
+ if (this.client_) {
+ throw new Error("setClient should only be called once");
+ }
+
+ this.client_ = client;
+ }
+
+ requestSession(sessionOptions) {
+ const requests = [];
+ // Request a session from all the runtimes.
+ for (let i = 0; i < this.runtimes_.length; i++) {
+ requests[i] = this.runtimes_[i]._requestRuntimeSession(sessionOptions);
+ }
+
+ return Promise.all(requests).then((results) => {
+ // Find and return the first successful result.
+ for (let i = 0; i < results.length; i++) {
+ if (results[i].session) {
+ // Construct a dummy metrics recorder
+ const metricsRecorderPtr = new vrMojom.XRSessionMetricsRecorderRemote();
+ metricsRecorderPtr.$.bindNewPipeAndPassReceiver().handle.close();
+
+ const success = {
+ session: results[i].session,
+ metricsRecorder: metricsRecorderPtr,
+ };
+
+ return {result: {success}};
+ }
+ }
+
+ // If there were no successful results, returns a null session.
+ return {
+ result: {failureReason: vrMojom.RequestSessionError.NO_RUNTIME_FOUND}
+ };
+ });
+ }
+
+ supportsSession(sessionOptions) {
+ const requests = [];
+ // Check supports on all the runtimes.
+ for (let i = 0; i < this.runtimes_.length; i++) {
+ requests[i] = this.runtimes_[i]._runtimeSupportsSession(sessionOptions);
+ }
+
+ return Promise.all(requests).then((results) => {
+ // Find and return the first successful result.
+ for (let i = 0; i < results.length; i++) {
+ if (results[i].supportsSession) {
+ return results[i];
+ }
+ }
+
+ // If there were no successful results, returns false.
+ return {supportsSession: false};
+ });
+ }
+
+ exitPresent() {
+ return Promise.resolve();
+ }
+
+ setFramesThrottled(throttled) {
+ this.setFramesThrottledImpl(throttled);
+ }
+
+ // We cannot override the mojom interceptors via the prototype; so this method
+ // and the above indirection exist to allow overrides by internal code.
+ setFramesThrottledImpl(throttled) {}
+
+ // Only handles asynchronous calls to makeXrCompatible. Synchronous calls are
+ // not supported in Javascript.
+ makeXrCompatible() {
+ if (this.runtimes_.length == 0) {
+ return {
+ xrCompatibleResult: vrMojom.XrCompatibleResult.kNoDeviceAvailable
+ };
+ }
+ return {xrCompatibleResult: vrMojom.XrCompatibleResult.kAlreadyCompatible};
+ }
+}
+
+class FakeXRAnchorController {
+ constructor() {
+ // Private properties.
+ this.device_ = null;
+ this.id_ = null;
+ this.dirty_ = true;
+
+ // Properties backing up public attributes / methods.
+ this.deleted_ = false;
+ this.paused_ = false;
+ this.anchorOrigin_ = XRMathHelper.identity();
+ }
+
+ // WebXR Test API (Anchors Extension)
+ get deleted() {
+ return this.deleted_;
+ }
+
+ pauseTracking() {
+ if(!this.paused_) {
+ this.paused_ = true;
+ this.dirty_ = true;
+ }
+ }
+
+ resumeTracking() {
+ if(this.paused_) {
+ this.paused_ = false;
+ this.dirty_ = true;
+ }
+ }
+
+ stopTracking() {
+ if(!this.deleted_) {
+ this.device_._deleteAnchorController(this.id_);
+
+ this.deleted_ = true;
+ this.dirty_ = true;
+ }
+ }
+
+ setAnchorOrigin(anchorOrigin) {
+ this.anchorOrigin_ = getMatrixFromTransform(anchorOrigin);
+ this.dirty_ = true;
+ }
+
+ // Internal implementation:
+ set id(value) {
+ this.id_ = value;
+ }
+
+ set device(value) {
+ this.device_ = value;
+ }
+
+ get dirty() {
+ return this.dirty_;
+ }
+
+ get paused() {
+ return this.paused_;
+ }
+
+ _markProcessed() {
+ this.dirty_ = false;
+ }
+
+ _getAnchorOrigin() {
+ return this.anchorOrigin_;
+ }
+}
+
+// Implements XRFrameDataProvider and XRPresentationProvider. Maintains a mock
+// for XRPresentationProvider. Implements FakeXRDevice test API.
+class MockRuntime {
+ // Mapping from string feature names to the corresponding mojo types.
+ // This is exposed as a member for extensibility.
+ static _featureToMojoMap = {
+ 'viewer': xrSessionMojom.XRSessionFeature.REF_SPACE_VIEWER,
+ 'local': xrSessionMojom.XRSessionFeature.REF_SPACE_LOCAL,
+ 'local-floor': xrSessionMojom.XRSessionFeature.REF_SPACE_LOCAL_FLOOR,
+ 'bounded-floor': xrSessionMojom.XRSessionFeature.REF_SPACE_BOUNDED_FLOOR,
+ 'unbounded': xrSessionMojom.XRSessionFeature.REF_SPACE_UNBOUNDED,
+ 'hit-test': xrSessionMojom.XRSessionFeature.HIT_TEST,
+ 'dom-overlay': xrSessionMojom.XRSessionFeature.DOM_OVERLAY,
+ 'light-estimation': xrSessionMojom.XRSessionFeature.LIGHT_ESTIMATION,
+ 'anchors': xrSessionMojom.XRSessionFeature.ANCHORS,
+ 'depth-sensing': xrSessionMojom.XRSessionFeature.DEPTH,
+ 'secondary-views': xrSessionMojom.XRSessionFeature.SECONDARY_VIEWS,
+ 'camera-access': xrSessionMojom.XRSessionFeature.CAMERA_ACCESS,
+ 'layers': xrSessionMojom.XRSessionFeature.LAYERS,
+ };
+
+ static _sessionModeToMojoMap = {
+ "inline": xrSessionMojom.XRSessionMode.kInline,
+ "immersive-vr": xrSessionMojom.XRSessionMode.kImmersiveVr,
+ "immersive-ar": xrSessionMojom.XRSessionMode.kImmersiveAr,
+ };
+
+ static _environmentBlendModeToMojoMap = {
+ "opaque": vrMojom.XREnvironmentBlendMode.kOpaque,
+ "alpha-blend": vrMojom.XREnvironmentBlendMode.kAlphaBlend,
+ "additive": vrMojom.XREnvironmentBlendMode.kAdditive,
+ };
+
+ static _interactionModeToMojoMap = {
+ "screen-space": vrMojom.XRInteractionMode.kScreenSpace,
+ "world-space": vrMojom.XRInteractionMode.kWorldSpace,
+ };
+
+ constructor(fakeDeviceInit, service) {
+ this.sessionClient_ = null;
+ this.presentation_provider_ = new MockXRPresentationProvider();
+
+ this.pose_ = null;
+ this.next_frame_id_ = 0;
+ this.bounds_ = null;
+ this.send_mojo_space_reset_ = false;
+ this.stageParameters_ = null;
+ this.stageParametersId_ = 1;
+
+ this.service_ = service;
+
+ this.framesOfReference = {};
+
+ this.input_sources_ = new Map();
+ this.next_input_source_index_ = 1;
+
+ // Currently active hit test subscriptons.
+ this.hitTestSubscriptions_ = new Map();
+ // Currently active transient hit test subscriptions.
+ this.transientHitTestSubscriptions_ = new Map();
+ // ID of the next subscription to be assigned.
+ this.next_hit_test_id_ = 1n;
+
+ this.anchor_controllers_ = new Map();
+ // ID of the next anchor to be assigned.
+ this.next_anchor_id_ = 1n;
+ // Anchor creation callback (initially null, can be set by tests).
+ this.anchor_creation_callback_ = null;
+
+ this.depthSensingData_ = null;
+ this.depthSensingDataDirty_ = false;
+
+ let supportedModes = [];
+ if (fakeDeviceInit.supportedModes) {
+ supportedModes = fakeDeviceInit.supportedModes.slice();
+ if (fakeDeviceInit.supportedModes.length === 0) {
+ supportedModes = ["inline"];
+ }
+ } else {
+ // Back-compat mode.
+ console.warn("Please use `supportedModes` to signal which modes are supported by this device.");
+ if (fakeDeviceInit.supportsImmersive == null) {
+ throw new TypeError("'supportsImmersive' must be set");
+ }
+
+ supportedModes = ["inline"];
+ if (fakeDeviceInit.supportsImmersive) {
+ supportedModes.push("immersive-vr");
+ }
+ }
+
+ this.supportedModes_ = this._convertModesToEnum(supportedModes);
+ if (this.supportedModes_.length == 0) {
+ console.error("Device has empty supported modes array!");
+ throw new InvalidStateError();
+ }
+
+ if (fakeDeviceInit.viewerOrigin != null) {
+ this.setViewerOrigin(fakeDeviceInit.viewerOrigin);
+ }
+
+ if (fakeDeviceInit.floorOrigin != null) {
+ this.setFloorOrigin(fakeDeviceInit.floorOrigin);
+ }
+
+ if (fakeDeviceInit.world) {
+ this.setWorld(fakeDeviceInit.world);
+ }
+
+ if (fakeDeviceInit.depthSensingData) {
+ this.setDepthSensingData(fakeDeviceInit.depthSensingData);
+ }
+
+ this.defaultFramebufferScale_ = default_framebuffer_scale;
+ this.enviromentBlendMode_ = this._convertBlendModeToEnum(fakeDeviceInit.environmentBlendMode);
+ this.interactionMode_ = this._convertInteractionModeToEnum(fakeDeviceInit.interactionMode);
+
+ // This appropriately handles if the coordinates are null
+ this.setBoundsGeometry(fakeDeviceInit.boundsCoordinates);
+
+ this.setViews(fakeDeviceInit.views, fakeDeviceInit.secondaryViews);
+
+ // Need to support webVR which doesn't have a notion of features
+ this._setFeatures(fakeDeviceInit.supportedFeatures || []);
+ }
+
+ // WebXR Test API
+ setViews(primaryViews, secondaryViews) {
+ this.cameraImage_ = null;
+ this.primaryViews_ = [];
+ this.secondaryViews_ = [];
+ let xOffset = 0;
+ if (primaryViews) {
+ this.primaryViews_ = [];
+ xOffset = this._setViews(primaryViews, xOffset, this.primaryViews_);
+ const cameraImage = this._findCameraImage(primaryViews);
+
+ if (cameraImage) {
+ this.cameraImage_ = cameraImage;
+ }
+ }
+
+ if (secondaryViews) {
+ this.secondaryViews_ = [];
+ this._setViews(secondaryViews, xOffset, this.secondaryViews_);
+ const cameraImage = this._findCameraImage(secondaryViews);
+
+ if (cameraImage) {
+ if (!isSameCameraImageInit(this.cameraImage_, cameraImage)) {
+ throw new Error("If present, camera resolutions on each view must match each other!"
+ + " Secondary views' camera doesn't match primary views.");
+ }
+
+ this.cameraImage_ = cameraImage;
+ }
+ }
+ }
+
+ disconnect() {
+ this.service_._removeRuntime(this);
+ this.presentation_provider_._close();
+ if (this.sessionClient_) {
+ this.sessionClient_.$.close();
+ this.sessionClient_ = null;
+ }
+
+ return Promise.resolve();
+ }
+
+ setViewerOrigin(origin, emulatedPosition = false) {
+ const p = origin.position;
+ const q = origin.orientation;
+ this.pose_ = {
+ orientation: { x: q[0], y: q[1], z: q[2], w: q[3] },
+ position: { x: p[0], y: p[1], z: p[2] },
+ emulatedPosition: emulatedPosition,
+ angularVelocity: null,
+ linearVelocity: null,
+ angularAcceleration: null,
+ linearAcceleration: null,
+ inputState: null,
+ poseIndex: 0
+ };
+ }
+
+ clearViewerOrigin() {
+ this.pose_ = null;
+ }
+
+ setFloorOrigin(floorOrigin) {
+ if (!this.stageParameters_) {
+ this.stageParameters_ = default_stage_parameters;
+ this.stageParameters_.bounds = this.bounds_;
+ }
+
+ // floorOrigin is passed in as mojoFromFloor.
+ this.stageParameters_.mojoFromFloor =
+ {matrix: getMatrixFromTransform(floorOrigin)};
+
+ this._onStageParametersUpdated();
+ }
+
+ clearFloorOrigin() {
+ if (this.stageParameters_) {
+ this.stageParameters_ = null;
+ this._onStageParametersUpdated();
+ }
+ }
+
+ setBoundsGeometry(bounds) {
+ if (bounds == null) {
+ this.bounds_ = null;
+ } else if (bounds.length < 3) {
+ throw new Error("Bounds must have a length of at least 3");
+ } else {
+ this.bounds_ = bounds;
+ }
+
+ // We can only set bounds if we have stageParameters set; otherwise, we
+ // don't know the transform from local space to bounds space.
+ // We'll cache the bounds so that they can be set in the future if the
+ // floorLevel transform is set, but we won't update them just yet.
+ if (this.stageParameters_) {
+ this.stageParameters_.bounds = this.bounds_;
+ this._onStageParametersUpdated();
+ }
+ }
+
+ simulateResetPose() {
+ this.send_mojo_space_reset_ = true;
+ }
+
+ simulateVisibilityChange(visibilityState) {
+ let mojoState = null;
+ switch (visibilityState) {
+ case "visible":
+ mojoState = vrMojom.XRVisibilityState.VISIBLE;
+ break;
+ case "visible-blurred":
+ mojoState = vrMojom.XRVisibilityState.VISIBLE_BLURRED;
+ break;
+ case "hidden":
+ mojoState = vrMojom.XRVisibilityState.HIDDEN;
+ break;
+ }
+ if (mojoState && this.sessionClient_) {
+ this.sessionClient_.onVisibilityStateChanged(mojoState);
+ }
+ }
+
+ simulateInputSourceConnection(fakeInputSourceInit) {
+ const index = this.next_input_source_index_;
+ this.next_input_source_index_++;
+
+ const source = new MockXRInputSource(fakeInputSourceInit, index, this);
+ this.input_sources_.set(index, source);
+ return source;
+ }
+
+ // WebXR Test API Hit Test extensions
+ setWorld(world) {
+ this.world_ = world;
+ }
+
+ clearWorld() {
+ this.world_ = null;
+ }
+
+ // WebXR Test API Anchor extensions
+ setAnchorCreationCallback(callback) {
+ this.anchor_creation_callback_ = callback;
+ }
+
+ setHitTestSourceCreationCallback(callback) {
+ this.hit_test_source_creation_callback_ = callback;
+ }
+
+ // WebXR Test API Lighting estimation extensions
+ setLightEstimate(fakeXrLightEstimateInit) {
+ if (!fakeXrLightEstimateInit.sphericalHarmonicsCoefficients) {
+ throw new TypeError("sphericalHarmonicsCoefficients must be set");
+ }
+
+ if (fakeXrLightEstimateInit.sphericalHarmonicsCoefficients.length != 27) {
+ throw new TypeError("Must supply all 27 sphericalHarmonicsCoefficients");
+ }
+
+ if (fakeXrLightEstimateInit.primaryLightDirection && fakeXrLightEstimateInit.primaryLightDirection.w != 0) {
+ throw new TypeError("W component of primaryLightDirection must be 0");
+ }
+
+ if (fakeXrLightEstimateInit.primaryLightIntensity && fakeXrLightEstimateInit.primaryLightIntensity.w != 1) {
+ throw new TypeError("W component of primaryLightIntensity must be 1");
+ }
+
+ // If the primaryLightDirection or primaryLightIntensity aren't set, we need to set them
+ // to the defaults that the spec expects. ArCore will either give us everything or nothing,
+ // so these aren't nullable on the mojom.
+ if (!fakeXrLightEstimateInit.primaryLightDirection) {
+ fakeXrLightEstimateInit.primaryLightDirection = { x: 0.0, y: 1.0, z: 0.0, w: 0.0 };
+ }
+
+ if (!fakeXrLightEstimateInit.primaryLightIntensity) {
+ fakeXrLightEstimateInit.primaryLightIntensity = { x: 0.0, y: 0.0, z: 0.0, w: 1.0 };
+ }
+
+ let c = fakeXrLightEstimateInit.sphericalHarmonicsCoefficients;
+
+ this.light_estimate_ = {
+ lightProbe: {
+ // XRSphereicalHarmonics
+ sphericalHarmonics: {
+ coefficients: [
+ { red: c[0], green: c[1], blue: c[2] },
+ { red: c[3], green: c[4], blue: c[5] },
+ { red: c[6], green: c[7], blue: c[8] },
+ { red: c[9], green: c[10], blue: c[11] },
+ { red: c[12], green: c[13], blue: c[14] },
+ { red: c[15], green: c[16], blue: c[17] },
+ { red: c[18], green: c[19], blue: c[20] },
+ { red: c[21], green: c[22], blue: c[23] },
+ { red: c[24], green: c[25], blue: c[26] }
+ ]
+ },
+ // Vector3dF
+ mainLightDirection: {
+ x: fakeXrLightEstimateInit.primaryLightDirection.x,
+ y: fakeXrLightEstimateInit.primaryLightDirection.y,
+ z: fakeXrLightEstimateInit.primaryLightDirection.z
+ },
+ // RgbTupleF32
+ mainLightIntensity: {
+ red: fakeXrLightEstimateInit.primaryLightIntensity.x,
+ green: fakeXrLightEstimateInit.primaryLightIntensity.y,
+ blue: fakeXrLightEstimateInit.primaryLightIntensity.z
+ }
+ }
+ }
+ }
+
+ // WebXR Test API depth Sensing Extensions
+ setDepthSensingData(depthSensingData) {
+ for(const key of ["depthData", "normDepthBufferFromNormView", "rawValueToMeters", "width", "height"]) {
+ if(!(key in depthSensingData)) {
+ throw new TypeError("Required key not present. Key: " + key);
+ }
+ }
+
+ if(depthSensingData.depthData != null) {
+ // Create new object w/ properties based on the depthSensingData, but
+ // convert the FakeXRRigidTransformInit into a transformation matrix object.
+ this.depthSensingData_ = Object.assign({},
+ depthSensingData, {
+ normDepthBufferFromNormView: composeGFXTransform(depthSensingData.normDepthBufferFromNormView),
+ });
+ } else {
+ throw new TypeError("`depthData` is not set");
+ }
+
+ this.depthSensingDataDirty_ = true;
+ }
+
+ clearDepthSensingData() {
+ this.depthSensingData_ = null;
+ this.depthSensingDataDirty_ = true;
+ }
+
+ // Internal Implementation/Helper Methods
+ _convertModeToEnum(sessionMode) {
+ if (sessionMode in MockRuntime._sessionModeToMojoMap) {
+ return MockRuntime._sessionModeToMojoMap[sessionMode];
+ }
+
+ throw new TypeError("Unrecognized value for XRSessionMode enum: " + sessionMode);
+ }
+
+ _convertModesToEnum(sessionModes) {
+ return sessionModes.map(mode => this._convertModeToEnum(mode));
+ }
+
+ _convertBlendModeToEnum(blendMode) {
+ if (blendMode in MockRuntime._environmentBlendModeToMojoMap) {
+ return MockRuntime._environmentBlendModeToMojoMap[blendMode];
+ } else {
+ if (this.supportedModes_.includes(xrSessionMojom.XRSessionMode.kImmersiveAr)) {
+ return vrMojom.XREnvironmentBlendMode.kAdditive;
+ } else if (this.supportedModes_.includes(
+ xrSessionMojom.XRSessionMode.kImmersiveVr)) {
+ return vrMojom.XREnvironmentBlendMode.kOpaque;
+ }
+ }
+ }
+
+ _convertInteractionModeToEnum(interactionMode) {
+ if (interactionMode in MockRuntime._interactionModeToMojoMap) {
+ return MockRuntime._interactionModeToMojoMap[interactionMode];
+ } else {
+ return vrMojom.XRInteractionMode.kWorldSpace;
+ }
+ }
+
+ _setViews(deviceViews, xOffset, views) {
+ for (let i = 0; i < deviceViews.length; i++) {
+ views[i] = this._getView(deviceViews[i], xOffset);
+ xOffset += deviceViews[i].resolution.width;
+ }
+
+ return xOffset;
+ }
+
+ _findCameraImage(views) {
+ const viewWithCamera = views.find(view => view.cameraImageInit);
+ if (viewWithCamera) {
+ //If we have one view with a camera resolution, all views should have the same camera resolution.
+ const allViewsHaveSameCamera = views.every(
+ view => isSameCameraImageInit(view.cameraImageInit, viewWithCamera.cameraImageInit));
+
+ if (!allViewsHaveSameCamera) {
+ throw new Error("If present, camera resolutions on each view must match each other!");
+ }
+
+ return viewWithCamera.cameraImageInit;
+ }
+
+ return null;
+ }
+
+ _onStageParametersUpdated() {
+ // Indicate for the frame loop that the stage parameters have been updated.
+ this.stageParametersId_++;
+ }
+
+ _getDefaultViews() {
+ if (this.primaryViews_) {
+ return this.primaryViews_;
+ }
+
+ const viewport_size = 20;
+ return [{
+ eye: vrMojom.XREye.kLeft,
+ fieldOfView: {
+ upDegrees: 48.316,
+ downDegrees: 50.099,
+ leftDegrees: 50.899,
+ rightDegrees: 35.197
+ },
+ mojoFromView: this._getMojoFromViewerWithOffset(composeGFXTransform({
+ position: [-0.032, 0, 0],
+ orientation: [0, 0, 0, 1]
+ })),
+ viewport: { x: 0, y: 0, width: viewport_size, height: viewport_size }
+ },
+ {
+ eye: vrMojom.XREye.kRight,
+ fieldOfView: {
+ upDegrees: 48.316,
+ downDegrees: 50.099,
+ leftDegrees: 50.899,
+ rightDegrees: 35.197
+ },
+ mojoFromView: this._getMojoFromViewerWithOffset(composeGFXTransform({
+ position: [0.032, 0, 0],
+ orientation: [0, 0, 0, 1]
+ })),
+ viewport: { x: viewport_size, y: 0, width: viewport_size, height: viewport_size }
+ }];
+ }
+
+ // This function converts between the matrix provided by the WebXR test API
+ // and the internal data representation.
+ _getView(fakeXRViewInit, xOffset) {
+ let fov = null;
+
+ if (fakeXRViewInit.fieldOfView) {
+ fov = {
+ upDegrees: fakeXRViewInit.fieldOfView.upDegrees,
+ downDegrees: fakeXRViewInit.fieldOfView.downDegrees,
+ leftDegrees: fakeXRViewInit.fieldOfView.leftDegrees,
+ rightDegrees: fakeXRViewInit.fieldOfView.rightDegrees
+ };
+ } else {
+ const m = fakeXRViewInit.projectionMatrix;
+
+ function toDegrees(tan) {
+ return Math.atan(tan) * 180 / Math.PI;
+ }
+
+ const leftTan = (1 - m[8]) / m[0];
+ const rightTan = (1 + m[8]) / m[0];
+ const upTan = (1 + m[9]) / m[5];
+ const downTan = (1 - m[9]) / m[5];
+
+ fov = {
+ upDegrees: toDegrees(upTan),
+ downDegrees: toDegrees(downTan),
+ leftDegrees: toDegrees(leftTan),
+ rightDegrees: toDegrees(rightTan)
+ };
+ }
+
+ let viewEye = vrMojom.XREye.kNone;
+ // The eye passed in corresponds to the values in the WebXR spec, which are
+ // the strings "none", "left", and "right". They should be converted to the
+ // corresponding values of XREye in vr_service.mojom.
+ switch(fakeXRViewInit.eye) {
+ case "none":
+ viewEye = vrMojom.XREye.kNone;
+ break;
+ case "left":
+ viewEye = vrMojom.XREye.kLeft;
+ break;
+ case "right":
+ viewEye = vrMojom.XREye.kRight;
+ break;
+ }
+
+ return {
+ eye: viewEye,
+ fieldOfView: fov,
+ mojoFromView: this._getMojoFromViewerWithOffset(composeGFXTransform(fakeXRViewInit.viewOffset)),
+ viewport: {
+ x: xOffset,
+ y: 0,
+ width: fakeXRViewInit.resolution.width,
+ height: fakeXRViewInit.resolution.height
+ },
+ isFirstPersonObserver: fakeXRViewInit.isFirstPersonObserver ? true : false,
+ viewOffset: composeGFXTransform(fakeXRViewInit.viewOffset)
+ };
+ }
+
+ _setFeatures(supportedFeatures) {
+ function convertFeatureToMojom(feature) {
+ if (feature in MockRuntime._featureToMojoMap) {
+ return MockRuntime._featureToMojoMap[feature];
+ } else {
+ return xrSessionMojom.XRSessionFeature.INVALID;
+ }
+ }
+
+ this.supportedFeatures_ = [];
+
+ for (let i = 0; i < supportedFeatures.length; i++) {
+ const feature = convertFeatureToMojom(supportedFeatures[i]);
+ if (feature !== xrSessionMojom.XRSessionFeature.INVALID) {
+ this.supportedFeatures_.push(feature);
+ }
+ }
+ }
+
+ // These methods are intended to be used by MockXRInputSource only.
+ _addInputSource(source) {
+ if (!this.input_sources_.has(source.source_id_)) {
+ this.input_sources_.set(source.source_id_, source);
+ }
+ }
+
+ _removeInputSource(source) {
+ this.input_sources_.delete(source.source_id_);
+ }
+
+ // These methods are intended to be used by FakeXRAnchorController only.
+ _deleteAnchorController(controllerId) {
+ this.anchor_controllers_.delete(controllerId);
+ }
+
+ // Extension point for non-standard modules.
+ _injectAdditionalFrameData(options, frameData) {
+ }
+
+ // Mojo function implementations.
+
+ // XRFrameDataProvider implementation.
+ getFrameData(options) {
+ return new Promise((resolve) => {
+
+ const populatePose = () => {
+ const mojo_space_reset = this.send_mojo_space_reset_;
+ this.send_mojo_space_reset_ = false;
+
+ if (this.pose_) {
+ this.pose_.poseIndex++;
+ }
+
+ // Setting the input_state to null tests a slightly different path than
+ // the browser tests where if the last input source is removed, the device
+ // code always sends up an empty array, but it's also valid mojom to send
+ // up a null array.
+ let input_state = null;
+ if (this.input_sources_.size > 0) {
+ input_state = [];
+ for (const input_source of this.input_sources_.values()) {
+ input_state.push(input_source._getInputSourceState());
+ }
+ }
+
+ let frame_views = this.primaryViews_;
+ for (let i = 0; i < this.primaryViews_.length; i++) {
+ this.primaryViews_[i].mojoFromView =
+ this._getMojoFromViewerWithOffset(this.primaryViews_[i].viewOffset);
+ }
+ if (this.enabledFeatures_.includes(xrSessionMojom.XRSessionFeature.SECONDARY_VIEWS)) {
+ for (let i = 0; i < this.secondaryViews_.length; i++) {
+ this.secondaryViews_[i].mojoFromView =
+ this._getMojoFromViewerWithOffset(this.secondaryViews_[i].viewOffset);
+ }
+
+ frame_views = frame_views.concat(this.secondaryViews_);
+ }
+
+ const frameData = {
+ mojoFromViewer: this.pose_,
+ views: frame_views,
+ mojoSpaceReset: mojo_space_reset,
+ inputState: input_state,
+ timeDelta: {
+ // window.performance.now() is in milliseconds, so convert to microseconds.
+ microseconds: BigInt(Math.floor(window.performance.now() * 1000)),
+ },
+ frameId: this.next_frame_id_,
+ bufferHolder: null,
+ cameraImageSize: this.cameraImage_ ? {
+ width: this.cameraImage_.width,
+ height: this.cameraImage_.height
+ } : null,
+ renderingTimeRatio: 0,
+ stageParameters: this.stageParameters_,
+ stageParametersId: this.stageParametersId_,
+ lightEstimationData: this.light_estimate_
+ };
+
+ this.next_frame_id_++;
+
+ this._calculateHitTestResults(frameData);
+
+ this._calculateAnchorInformation(frameData);
+
+ this._calculateDepthInformation(frameData);
+
+ this._injectAdditionalFrameData(options, frameData);
+
+ resolve({frameData});
+ };
+
+ if(this.sessionOptions_.mode == xrSessionMojom.XRSessionMode.kInline) {
+ // Inline sessions should not have a delay introduced since it causes them
+ // to miss a vsync blink-side and delays propagation of changes that happened
+ // within a rAFcb by one frame (e.g. setViewerOrigin() calls would take 2 frames
+ // to propagate).
+ populatePose();
+ } else {
+ // For immerive sessions, add additional delay to allow for anchor creation
+ // promises to run.
+ setTimeout(populatePose, 3); // note: according to MDN, the timeout is not exact
+ }
+ });
+ }
+
+ getEnvironmentIntegrationProvider(environmentProviderRequest) {
+ if (this.environmentProviderReceiver_) {
+ this.environmentProviderReceiver_.$.close();
+ }
+ this.environmentProviderReceiver_ =
+ new vrMojom.XREnvironmentIntegrationProviderReceiver(this);
+ this.environmentProviderReceiver_.$.bindHandle(
+ environmentProviderRequest.handle);
+ }
+
+ // XREnvironmentIntegrationProvider implementation:
+ subscribeToHitTest(nativeOriginInformation, entityTypes, ray) {
+ if (!this.supportedModes_.includes(xrSessionMojom.XRSessionMode.kImmersiveAr)) {
+ // Reject outside of AR.
+ return Promise.resolve({
+ result : vrMojom.SubscribeToHitTestResult.FAILURE_GENERIC,
+ subscriptionId : 0n
+ });
+ }
+
+ if (!this._nativeOriginKnown(nativeOriginInformation)) {
+ return Promise.resolve({
+ result : vrMojom.SubscribeToHitTestResult.FAILURE_GENERIC,
+ subscriptionId : 0n
+ });
+ }
+
+ // Reserve the id for hit test source:
+ const id = this.next_hit_test_id_++;
+ const hitTestParameters = { isTransient: false, profileName: null };
+ const controller = new FakeXRHitTestSourceController(id);
+
+
+ return this._shouldHitTestSourceCreationSucceed(hitTestParameters, controller)
+ .then((succeeded) => {
+ if(succeeded) {
+ // Store the subscription information as-is (including controller):
+ this.hitTestSubscriptions_.set(id, { nativeOriginInformation, entityTypes, ray, controller });
+
+ return Promise.resolve({
+ result : vrMojom.SubscribeToHitTestResult.SUCCESS,
+ subscriptionId : id
+ });
+ } else {
+ return Promise.resolve({
+ result : vrMojom.SubscribeToHitTestResult.FAILURE_GENERIC,
+ subscriptionId : 0n
+ });
+ }
+ });
+ }
+
+ subscribeToHitTestForTransientInput(profileName, entityTypes, ray){
+ if (!this.supportedModes_.includes(xrSessionMojom.XRSessionMode.kImmersiveAr)) {
+ // Reject outside of AR.
+ return Promise.resolve({
+ result : vrMojom.SubscribeToHitTestResult.FAILURE_GENERIC,
+ subscriptionId : 0n
+ });
+ }
+
+ const id = this.next_hit_test_id_++;
+ const hitTestParameters = { isTransient: true, profileName: profileName };
+ const controller = new FakeXRHitTestSourceController(id);
+
+ // Check if we have hit test source creation callback.
+ // If yes, ask it if the hit test source creation should succeed.
+ // If no, for back-compat, assume the hit test source creation succeeded.
+ return this._shouldHitTestSourceCreationSucceed(hitTestParameters, controller)
+ .then((succeeded) => {
+ if(succeeded) {
+ // Store the subscription information as-is (including controller):
+ this.transientHitTestSubscriptions_.set(id, { profileName, entityTypes, ray, controller });
+
+ return Promise.resolve({
+ result : vrMojom.SubscribeToHitTestResult.SUCCESS,
+ subscriptionId : id
+ });
+ } else {
+ return Promise.resolve({
+ result : vrMojom.SubscribeToHitTestResult.FAILURE_GENERIC,
+ subscriptionId : 0n
+ });
+ }
+ });
+ }
+
+ unsubscribeFromHitTest(subscriptionId) {
+ let controller = null;
+ if(this.transientHitTestSubscriptions_.has(subscriptionId)){
+ controller = this.transientHitTestSubscriptions_.get(subscriptionId).controller;
+ this.transientHitTestSubscriptions_.delete(subscriptionId);
+ } else if(this.hitTestSubscriptions_.has(subscriptionId)){
+ controller = this.hitTestSubscriptions_.get(subscriptionId).controller;
+ this.hitTestSubscriptions_.delete(subscriptionId);
+ }
+
+ if(controller) {
+ controller.deleted = true;
+ }
+ }
+
+ createAnchor(nativeOriginInformation, nativeOriginFromAnchor) {
+ return new Promise((resolve) => {
+ if(this.anchor_creation_callback_ == null) {
+ resolve({
+ result : vrMojom.CreateAnchorResult.FAILURE,
+ anchorId : 0n
+ });
+
+ return;
+ }
+
+ const mojoFromNativeOrigin = this._getMojoFromNativeOrigin(nativeOriginInformation);
+ if(mojoFromNativeOrigin == null) {
+ resolve({
+ result : vrMojom.CreateAnchorResult.FAILURE,
+ anchorId : 0n
+ });
+
+ return;
+ }
+
+ const mojoFromAnchor = XRMathHelper.mul4x4(mojoFromNativeOrigin, nativeOriginFromAnchor);
+
+ const anchorCreationParameters = {
+ requestedAnchorOrigin: mojoFromAnchor,
+ isAttachedToEntity: false,
+ };
+
+ const anchorController = new FakeXRAnchorController();
+
+ this.anchor_creation_callback_(anchorCreationParameters, anchorController)
+ .then((result) => {
+ if(result) {
+ // If the test allowed the anchor creation,
+ // store the anchor controller & return success.
+
+ const anchor_id = this.next_anchor_id_;
+ this.next_anchor_id_++;
+
+ this.anchor_controllers_.set(anchor_id, anchorController);
+ anchorController.device = this;
+ anchorController.id = anchor_id;
+
+ resolve({
+ result : vrMojom.CreateAnchorResult.SUCCESS,
+ anchorId : anchor_id
+ });
+ } else {
+ // The test has rejected anchor creation.
+ resolve({
+ result : vrMojom.CreateAnchorResult.FAILURE,
+ anchorId : 0n
+ });
+ }
+ })
+ .catch(() => {
+ // The test threw an error, treat anchor creation as failed.
+ resolve({
+ result : vrMojom.CreateAnchorResult.FAILURE,
+ anchorId : 0n
+ });
+ });
+ });
+ }
+
+ createPlaneAnchor(planeFromAnchor, planeId) {
+ return new Promise((resolve) => {
+
+ // Not supported yet.
+
+ resolve({
+ result : vrMojom.CreateAnchorResult.FAILURE,
+ anchorId : 0n,
+ });
+ });
+ }
+
+ detachAnchor(anchorId) {}
+
+ // Utility function
+ _requestRuntimeSession(sessionOptions) {
+ return this._runtimeSupportsSession(sessionOptions).then((result) => {
+ // The JavaScript bindings convert c_style_names to camelCase names.
+ const options = {
+ transportMethod:
+ vrMojom.XRPresentationTransportMethod.SUBMIT_AS_MAILBOX_HOLDER,
+ waitForTransferNotification: true,
+ waitForRenderNotification: true,
+ waitForGpuFence: false,
+ };
+
+ let submit_frame_sink;
+ if (result.supportsSession) {
+ submit_frame_sink = {
+ clientReceiver: this.presentation_provider_._getClientReceiver(),
+ provider: this.presentation_provider_._bindProvider(sessionOptions),
+ transportOptions: options
+ };
+
+ const dataProviderPtr = new vrMojom.XRFrameDataProviderRemote();
+ this.dataProviderReceiver_ =
+ new vrMojom.XRFrameDataProviderReceiver(this);
+ this.dataProviderReceiver_.$.bindHandle(
+ dataProviderPtr.$.bindNewPipeAndPassReceiver().handle);
+ this.sessionOptions_ = sessionOptions;
+
+ this.sessionClient_ = new vrMojom.XRSessionClientRemote();
+ const clientReceiver = this.sessionClient_.$.bindNewPipeAndPassReceiver();
+
+ const enabled_features = [];
+ for (let i = 0; i < sessionOptions.requiredFeatures.length; i++) {
+ if (this.supportedFeatures_.indexOf(sessionOptions.requiredFeatures[i]) !== -1) {
+ enabled_features.push(sessionOptions.requiredFeatures[i]);
+ } else {
+ return Promise.resolve({session: null});
+ }
+ }
+
+ for (let i =0; i < sessionOptions.optionalFeatures.length; i++) {
+ if (this.supportedFeatures_.indexOf(sessionOptions.optionalFeatures[i]) !== -1) {
+ enabled_features.push(sessionOptions.optionalFeatures[i]);
+ }
+ }
+
+ this.enabledFeatures_ = enabled_features;
+
+ return Promise.resolve({
+ session: {
+ submitFrameSink: submit_frame_sink,
+ dataProvider: dataProviderPtr,
+ clientReceiver: clientReceiver,
+ enabledFeatures: enabled_features,
+ deviceConfig: {
+ defaultFramebufferScale: this.defaultFramebufferScale_,
+ supportsViewportScaling: true,
+ depthConfiguration:
+ enabled_features.includes(xrSessionMojom.XRSessionFeature.DEPTH) ? {
+ depthUsage: vrMojom.XRDepthUsage.kCPUOptimized,
+ depthDataFormat: vrMojom.XRDepthDataFormat.kLuminanceAlpha,
+ } : null,
+ views: this._getDefaultViews(),
+ },
+ enviromentBlendMode: this.enviromentBlendMode_,
+ interactionMode: this.interactionMode_
+ }
+ });
+ } else {
+ return Promise.resolve({session: null});
+ }
+ });
+ }
+
+ _runtimeSupportsSession(options) {
+ let result = this.supportedModes_.includes(options.mode);
+
+ if (options.requiredFeatures.includes(xrSessionMojom.XRSessionFeature.DEPTH)
+ || options.optionalFeatures.includes(xrSessionMojom.XRSessionFeature.DEPTH)) {
+ result &= options.depthOptions.usagePreferences.includes(vrMojom.XRDepthUsage.kCPUOptimized);
+ result &= options.depthOptions.dataFormatPreferences.includes(vrMojom.XRDepthDataFormat.kLuminanceAlpha);
+ }
+
+ return Promise.resolve({
+ supportsSession: result,
+ });
+ }
+
+ // Private functions - utilities:
+ _nativeOriginKnown(nativeOriginInformation){
+
+ if (nativeOriginInformation.inputSourceSpaceInfo !== undefined) {
+ if (!this.input_sources_.has(nativeOriginInformation.inputSourceSpaceInfo.inputSourceId)) {
+ // Unknown input source.
+ return false;
+ }
+
+ return true;
+ } else if (nativeOriginInformation.referenceSpaceType !== undefined) {
+ // Bounded_floor & unbounded ref spaces are not yet supported for AR:
+ if (nativeOriginInformation.referenceSpaceType == vrMojom.XRReferenceSpaceType.kUnbounded
+ || nativeOriginInformation.referenceSpaceType == vrMojom.XRReferenceSpaceType.kBoundedFloor) {
+ return false;
+ }
+
+ return true;
+ } else {
+ // Planes and anchors are not yet supported by the mock interface.
+ return false;
+ }
+ }
+
+ // Private functions - anchors implementation:
+
+ // Modifies passed in frameData to add anchor information.
+ _calculateAnchorInformation(frameData) {
+ if (!this.supportedModes_.includes(xrSessionMojom.XRSessionMode.kImmersiveAr)) {
+ return;
+ }
+
+ frameData.anchorsData = {allAnchorsIds: [], updatedAnchorsData: []};
+ for(const [id, controller] of this.anchor_controllers_) {
+ frameData.anchorsData.allAnchorsIds.push(id);
+
+ // Send the entire anchor data over if there was a change since last GetFrameData().
+ if(controller.dirty) {
+ const anchorData = {id};
+ if(!controller.paused) {
+ anchorData.mojoFromAnchor = getPoseFromTransform(
+ XRMathHelper.decomposeRigidTransform(
+ controller._getAnchorOrigin()));
+ }
+
+ controller._markProcessed();
+
+ frameData.anchorsData.updatedAnchorsData.push(anchorData);
+ }
+ }
+ }
+
+ // Private functions - depth sensing implementation:
+
+ // Modifies passed in frameData to add anchor information.
+ _calculateDepthInformation(frameData) {
+ if (!this.supportedModes_.includes(xrSessionMojom.XRSessionMode.kImmersiveAr)) {
+ return;
+ }
+
+ if (!this.enabledFeatures_.includes(xrSessionMojom.XRSessionFeature.DEPTH)) {
+ return;
+ }
+
+ // If we don't have a current depth data, we'll return null
+ // (i.e. no data is not a valid data, so it cannot be "StillValid").
+ if (this.depthSensingData_ == null) {
+ frameData.depthData = null;
+ return;
+ }
+
+ if(!this.depthSensingDataDirty_) {
+ frameData.depthData = { dataStillValid: {}};
+ return;
+ }
+
+ frameData.depthData = {
+ updatedDepthData: {
+ timeDelta: frameData.timeDelta,
+ normTextureFromNormView: this.depthSensingData_.normDepthBufferFromNormView,
+ rawValueToMeters: this.depthSensingData_.rawValueToMeters,
+ size: { width: this.depthSensingData_.width, height: this.depthSensingData_.height },
+ pixelData: { bytes: this.depthSensingData_.depthData }
+ }
+ };
+
+ this.depthSensingDataDirty_ = false;
+ }
+
+ // Private functions - hit test implementation:
+
+ // Returns a Promise<bool> that signifies whether hit test source creation should succeed.
+ // If we have a hit test source creation callback installed, invoke it and return its result.
+ // If it's not installed, for back-compat just return a promise that resolves to true.
+ _shouldHitTestSourceCreationSucceed(hitTestParameters, controller) {
+ if(this.hit_test_source_creation_callback_) {
+ return this.hit_test_source_creation_callback_(hitTestParameters, controller);
+ } else {
+ return Promise.resolve(true);
+ }
+ }
+
+ // Modifies passed in frameData to add hit test results.
+ _calculateHitTestResults(frameData) {
+ if (!this.supportedModes_.includes(xrSessionMojom.XRSessionMode.kImmersiveAr)) {
+ return;
+ }
+
+ frameData.hitTestSubscriptionResults = {results: [],
+ transientInputResults: []};
+ if (!this.world_) {
+ return;
+ }
+
+ // Non-transient hit test:
+ for (const [id, subscription] of this.hitTestSubscriptions_) {
+ const mojo_from_native_origin = this._getMojoFromNativeOrigin(subscription.nativeOriginInformation);
+ if (!mojo_from_native_origin) continue;
+
+ const [mojo_ray_origin, mojo_ray_direction] = this._transformRayToMojoSpace(
+ subscription.ray,
+ mojo_from_native_origin
+ );
+
+ const results = this._hitTestWorld(mojo_ray_origin, mojo_ray_direction, subscription.entityTypes);
+ frameData.hitTestSubscriptionResults.results.push(
+ {subscriptionId: id, hitTestResults: results});
+ }
+
+ // Transient hit test:
+ const mojo_from_viewer = this._getMojoFromViewer();
+
+ for (const [id, subscription] of this.transientHitTestSubscriptions_) {
+ const result = {subscriptionId: id,
+ inputSourceIdToHitTestResults: new Map()};
+
+ // Find all input sources that match the profile name:
+ const matching_input_sources = Array.from(this.input_sources_.values())
+ .filter(input_source => input_source.profiles_.includes(subscription.profileName));
+
+ for (const input_source of matching_input_sources) {
+ const mojo_from_native_origin = input_source._getMojoFromInputSource(mojo_from_viewer);
+
+ const [mojo_ray_origin, mojo_ray_direction] = this._transformRayToMojoSpace(
+ subscription.ray,
+ mojo_from_native_origin
+ );
+
+ const results = this._hitTestWorld(mojo_ray_origin, mojo_ray_direction, subscription.entityTypes);
+
+ result.inputSourceIdToHitTestResults.set(input_source.source_id_, results);
+ }
+
+ frameData.hitTestSubscriptionResults.transientInputResults.push(result);
+ }
+ }
+
+ // Returns 2-element array [origin, direction] of a ray in mojo space.
+ // |ray| is expressed relative to native origin.
+ _transformRayToMojoSpace(ray, mojo_from_native_origin) {
+ const ray_origin = {
+ x: ray.origin.x,
+ y: ray.origin.y,
+ z: ray.origin.z,
+ w: 1
+ };
+ const ray_direction = {
+ x: ray.direction.x,
+ y: ray.direction.y,
+ z: ray.direction.z,
+ w: 0
+ };
+
+ const mojo_ray_origin = XRMathHelper.transform_by_matrix(
+ mojo_from_native_origin,
+ ray_origin);
+ const mojo_ray_direction = XRMathHelper.transform_by_matrix(
+ mojo_from_native_origin,
+ ray_direction);
+
+ return [mojo_ray_origin, mojo_ray_direction];
+ }
+
+ // Hit tests the passed in ray (expressed as origin and direction) against the mocked world data.
+ _hitTestWorld(origin, direction, entityTypes) {
+ let result = [];
+
+ for (const region of this.world_.hitTestRegions) {
+ const partial_result = this._hitTestRegion(
+ region,
+ origin, direction,
+ entityTypes);
+
+ result = result.concat(partial_result);
+ }
+
+ return result.sort((lhs, rhs) => lhs.distance - rhs.distance).map((hitTest) => {
+ delete hitTest.distance;
+ return hitTest;
+ });
+ }
+
+ // Hit tests the passed in ray (expressed as origin and direction) against world region.
+ // |entityTypes| is a set of FakeXRRegionTypes.
+ // |region| is FakeXRRegion.
+ // Returns array of XRHitResults, each entry will be decorated with the distance from the ray origin (along the ray).
+ _hitTestRegion(region, origin, direction, entityTypes) {
+ const regionNameToMojoEnum = {
+ "point": vrMojom.EntityTypeForHitTest.POINT,
+ "plane": vrMojom.EntityTypeForHitTest.PLANE,
+ "mesh":null
+ };
+
+ if (!entityTypes.includes(regionNameToMojoEnum[region.type])) {
+ return [];
+ }
+
+ const result = [];
+ for (const face of region.faces) {
+ const maybe_hit = this._hitTestFace(face, origin, direction);
+ if (maybe_hit) {
+ result.push(maybe_hit);
+ }
+ }
+
+ // The results should be sorted by distance and there should be no 2 entries with
+ // the same distance from ray origin - that would mean they are the same point.
+ // This situation is possible when a ray intersects the region through an edge shared
+ // by 2 faces.
+ return result.sort((lhs, rhs) => lhs.distance - rhs.distance)
+ .filter((val, index, array) => index === 0 || val.distance !== array[index - 1].distance);
+ }
+
+ // Hit tests the passed in ray (expressed as origin and direction) against a single face.
+ // |face|, |origin|, and |direction| are specified in world (aka mojo) coordinates.
+ // |face| is an array of DOMPointInits.
+ // Returns null if the face does not intersect with the ray, otherwise the result is
+ // an XRHitResult with matrix describing the pose of the intersection point.
+ _hitTestFace(face, origin, direction) {
+ const add = XRMathHelper.add;
+ const sub = XRMathHelper.sub;
+ const mul = XRMathHelper.mul;
+ const normalize = XRMathHelper.normalize;
+ const dot = XRMathHelper.dot;
+ const cross = XRMathHelper.cross;
+ const neg = XRMathHelper.neg;
+
+ //1. Calculate plane normal in world coordinates.
+ const point_A = face.vertices[0];
+ const point_B = face.vertices[1];
+ const point_C = face.vertices[2];
+
+ const edge_AB = sub(point_B, point_A);
+ const edge_AC = sub(point_C, point_A);
+
+ const normal = normalize(cross(edge_AB, edge_AC));
+
+ const numerator = dot(sub(point_A, origin), normal);
+ const denominator = dot(direction, normal);
+
+ if (Math.abs(denominator) < XRMathHelper.EPSILON) {
+ // Planes are nearly parallel - there's either infinitely many intersection points or 0.
+ // Both cases signify a "no hit" for us.
+ return null;
+ } else {
+ // Single intersection point between the infinite plane and the line (*not* ray).
+ // Need to calculate the hit test matrix taking into account the face vertices.
+ const distance = numerator / denominator;
+ if (distance < 0) {
+ // Line - plane intersection exists, but not the half-line - plane does not.
+ return null;
+ } else {
+ const intersection_point = add(origin, mul(distance, direction));
+ // Since we are treating the face as a solid, flip the normal so that its
+ // half-space will contain the ray origin.
+ const y_axis = denominator > 0 ? neg(normal) : normal;
+
+ let z_axis = null;
+ const cos_direction_and_y_axis = dot(direction, y_axis);
+ if (Math.abs(cos_direction_and_y_axis) > (1 - XRMathHelper.EPSILON)) {
+ // Ray and the hit test normal are co-linear - try using the 'up' or 'right' vector's projection on the face plane as the Z axis.
+ // Note: this edge case is currently not covered by the spec.
+ const up = {x: 0.0, y: 1.0, z: 0.0, w: 0.0};
+ const right = {x: 1.0, y: 0.0, z: 0.0, w: 0.0};
+
+ z_axis = Math.abs(dot(up, y_axis)) > (1 - XRMathHelper.EPSILON)
+ ? sub(up, mul(dot(right, y_axis), y_axis)) // `up is also co-linear with hit test normal, use `right`
+ : sub(up, mul(dot(up, y_axis), y_axis)); // `up` is not co-linear with hit test normal, use it
+ } else {
+ // Project the ray direction onto the plane, negate it and use as a Z axis.
+ z_axis = neg(sub(direction, mul(cos_direction_and_y_axis, y_axis))); // Z should point towards the ray origin, not away.
+ }
+
+ z_axis = normalize(z_axis);
+ const x_axis = normalize(cross(y_axis, z_axis));
+
+ // Filter out the points not in polygon.
+ if (!XRMathHelper.pointInFace(intersection_point, face)) {
+ return null;
+ }
+
+ const hitResult = {planeId: 0n};
+ hitResult.distance = distance; // Extend the object with additional information used by higher layers.
+ // It will not be serialized over mojom.
+
+ const matrix = new Array(16);
+
+ matrix[0] = x_axis.x;
+ matrix[1] = x_axis.y;
+ matrix[2] = x_axis.z;
+ matrix[3] = 0;
+
+ matrix[4] = y_axis.x;
+ matrix[5] = y_axis.y;
+ matrix[6] = y_axis.z;
+ matrix[7] = 0;
+
+ matrix[8] = z_axis.x;
+ matrix[9] = z_axis.y;
+ matrix[10] = z_axis.z;
+ matrix[11] = 0;
+
+ matrix[12] = intersection_point.x;
+ matrix[13] = intersection_point.y;
+ matrix[14] = intersection_point.z;
+ matrix[15] = 1;
+
+ hitResult.mojoFromResult = getPoseFromTransform(
+ XRMathHelper.decomposeRigidTransform(matrix));
+ return hitResult;
+ }
+ }
+ }
+
+ _getMojoFromViewer() {
+ if (!this.pose_) {
+ return XRMathHelper.identity();
+ }
+ const transform = {
+ position: [
+ this.pose_.position.x,
+ this.pose_.position.y,
+ this.pose_.position.z],
+ orientation: [
+ this.pose_.orientation.x,
+ this.pose_.orientation.y,
+ this.pose_.orientation.z,
+ this.pose_.orientation.w],
+ };
+
+ return getMatrixFromTransform(transform);
+ }
+
+ _getMojoFromViewerWithOffset(viewOffset) {
+ return { matrix: XRMathHelper.mul4x4(this._getMojoFromViewer(), viewOffset.matrix) };
+ }
+
+ _getMojoFromNativeOrigin(nativeOriginInformation) {
+ const mojo_from_viewer = this._getMojoFromViewer();
+
+ if (nativeOriginInformation.inputSourceSpaceInfo !== undefined) {
+ if (!this.input_sources_.has(nativeOriginInformation.inputSourceSpaceInfo.inputSourceId)) {
+ return null;
+ } else {
+ const inputSource = this.input_sources_.get(nativeOriginInformation.inputSourceSpaceInfo.inputSourceId);
+ return inputSource._getMojoFromInputSource(mojo_from_viewer);
+ }
+ } else if (nativeOriginInformation.referenceSpaceType !== undefined) {
+ switch (nativeOriginInformation.referenceSpaceType) {
+ case vrMojom.XRReferenceSpaceType.kLocal:
+ return XRMathHelper.identity();
+ case vrMojom.XRReferenceSpaceType.kLocalFloor:
+ if (this.stageParameters_ == null || this.stageParameters_.mojoFromFloor == null) {
+ console.warn("Standing transform not available.");
+ return null;
+ }
+ return this.stageParameters_.mojoFromFloor.matrix;
+ case vrMojom.XRReferenceSpaceType.kViewer:
+ return mojo_from_viewer;
+ case vrMojom.XRReferenceSpaceType.kBoundedFloor:
+ return null;
+ case vrMojom.XRReferenceSpaceType.kUnbounded:
+ return null;
+ default:
+ throw new TypeError("Unrecognized XRReferenceSpaceType!");
+ }
+ } else {
+ // Anchors & planes are not yet supported for hit test.
+ return null;
+ }
+ }
+}
+
+class MockXRInputSource {
+ constructor(fakeInputSourceInit, id, pairedDevice) {
+ this.source_id_ = id;
+ this.pairedDevice_ = pairedDevice;
+ this.handedness_ = fakeInputSourceInit.handedness;
+ this.target_ray_mode_ = fakeInputSourceInit.targetRayMode;
+
+ if (fakeInputSourceInit.pointerOrigin == null) {
+ throw new TypeError("FakeXRInputSourceInit.pointerOrigin is required.");
+ }
+
+ this.setPointerOrigin(fakeInputSourceInit.pointerOrigin);
+ this.setProfiles(fakeInputSourceInit.profiles);
+
+ this.primary_input_pressed_ = false;
+ if (fakeInputSourceInit.selectionStarted != null) {
+ this.primary_input_pressed_ = fakeInputSourceInit.selectionStarted;
+ }
+
+ this.primary_input_clicked_ = false;
+ if (fakeInputSourceInit.selectionClicked != null) {
+ this.primary_input_clicked_ = fakeInputSourceInit.selectionClicked;
+ }
+
+ this.primary_squeeze_pressed_ = false;
+ this.primary_squeeze_clicked_ = false;
+
+ this.mojo_from_input_ = null;
+ if (fakeInputSourceInit.gripOrigin != null) {
+ this.setGripOrigin(fakeInputSourceInit.gripOrigin);
+ }
+
+ // This properly handles if supportedButtons were not specified.
+ this.setSupportedButtons(fakeInputSourceInit.supportedButtons);
+
+ this.emulated_position_ = false;
+ this.desc_dirty_ = true;
+ }
+
+ // WebXR Test API
+ setHandedness(handedness) {
+ if (this.handedness_ != handedness) {
+ this.desc_dirty_ = true;
+ this.handedness_ = handedness;
+ }
+ }
+
+ setTargetRayMode(targetRayMode) {
+ if (this.target_ray_mode_ != targetRayMode) {
+ this.desc_dirty_ = true;
+ this.target_ray_mode_ = targetRayMode;
+ }
+ }
+
+ setProfiles(profiles) {
+ this.desc_dirty_ = true;
+ this.profiles_ = profiles;
+ }
+
+ setGripOrigin(transform, emulatedPosition = false) {
+ // grip_origin was renamed to mojo_from_input in mojo
+ this.mojo_from_input_ = composeGFXTransform(transform);
+ this.emulated_position_ = emulatedPosition;
+
+ // Technically, setting the grip shouldn't make the description dirty, but
+ // the webxr-test-api sets our pointer as mojoFromPointer; however, we only
+ // support it across mojom as inputFromPointer, so we need to recalculate it
+ // whenever the grip moves.
+ this.desc_dirty_ = true;
+ }
+
+ clearGripOrigin() {
+ // grip_origin was renamed to mojo_from_input in mojo
+ if (this.mojo_from_input_ != null) {
+ this.mojo_from_input_ = null;
+ this.emulated_position_ = false;
+ this.desc_dirty_ = true;
+ }
+ }
+
+ setPointerOrigin(transform, emulatedPosition = false) {
+ // pointer_origin is mojo_from_pointer.
+ this.desc_dirty_ = true;
+ this.mojo_from_pointer_ = composeGFXTransform(transform);
+ this.emulated_position_ = emulatedPosition;
+ }
+
+ disconnect() {
+ this.pairedDevice_._removeInputSource(this);
+ }
+
+ reconnect() {
+ this.pairedDevice_._addInputSource(this);
+ }
+
+ startSelection() {
+ this.primary_input_pressed_ = true;
+ if (this.gamepad_) {
+ this.gamepad_.buttons[0].pressed = true;
+ this.gamepad_.buttons[0].touched = true;
+ }
+ }
+
+ endSelection() {
+ if (!this.primary_input_pressed_) {
+ throw new Error("Attempted to end selection which was not started");
+ }
+
+ this.primary_input_pressed_ = false;
+ this.primary_input_clicked_ = true;
+
+ if (this.gamepad_) {
+ this.gamepad_.buttons[0].pressed = false;
+ this.gamepad_.buttons[0].touched = false;
+ }
+ }
+
+ simulateSelect() {
+ this.primary_input_clicked_ = true;
+ }
+
+ setSupportedButtons(supportedButtons) {
+ this.gamepad_ = null;
+ this.supported_buttons_ = [];
+
+ // If there are no supported buttons, we can stop now.
+ if (supportedButtons == null || supportedButtons.length < 1) {
+ return;
+ }
+
+ const supported_button_map = {};
+ this.gamepad_ = this._getEmptyGamepad();
+ for (let i = 0; i < supportedButtons.length; i++) {
+ const buttonType = supportedButtons[i].buttonType;
+ this.supported_buttons_.push(buttonType);
+ supported_button_map[buttonType] = supportedButtons[i];
+ }
+
+ // Let's start by building the button state in order of priority:
+ // Primary button is index 0.
+ this.gamepad_.buttons.push({
+ pressed: this.primary_input_pressed_,
+ touched: this.primary_input_pressed_,
+ value: this.primary_input_pressed_ ? 1.0 : 0.0
+ });
+
+ // Now add the rest of our buttons
+ this._addGamepadButton(supported_button_map['grip']);
+ this._addGamepadButton(supported_button_map['touchpad']);
+ this._addGamepadButton(supported_button_map['thumbstick']);
+ this._addGamepadButton(supported_button_map['optional-button']);
+ this._addGamepadButton(supported_button_map['optional-thumbstick']);
+
+ // Finally, back-fill placeholder buttons/axes
+ for (let i = 0; i < this.gamepad_.buttons.length; i++) {
+ if (this.gamepad_.buttons[i] == null) {
+ this.gamepad_.buttons[i] = {
+ pressed: false,
+ touched: false,
+ value: 0
+ };
+ }
+ }
+
+ for (let i=0; i < this.gamepad_.axes.length; i++) {
+ if (this.gamepad_.axes[i] == null) {
+ this.gamepad_.axes[i] = 0;
+ }
+ }
+ }
+
+ updateButtonState(buttonState) {
+ if (this.supported_buttons_.indexOf(buttonState.buttonType) == -1) {
+ throw new Error("Tried to update state on an unsupported button");
+ }
+
+ const buttonIndex = this._getButtonIndex(buttonState.buttonType);
+ const axesStartIndex = this._getAxesStartIndex(buttonState.buttonType);
+
+ if (buttonIndex == -1) {
+ throw new Error("Unknown Button Type!");
+ }
+
+ // is this a 'squeeze' button?
+ if (buttonIndex === this._getButtonIndex('grip')) {
+ // squeeze
+ if (buttonState.pressed) {
+ this.primary_squeeze_pressed_ = true;
+ } else if (this.gamepad_.buttons[buttonIndex].pressed) {
+ this.primary_squeeze_clicked_ = true;
+ this.primary_squeeze_pressed_ = false;
+ } else {
+ this.primary_squeeze_clicked_ = false;
+ this.primary_squeeze_pressed_ = false;
+ }
+ }
+
+ this.gamepad_.buttons[buttonIndex].pressed = buttonState.pressed;
+ this.gamepad_.buttons[buttonIndex].touched = buttonState.touched;
+ this.gamepad_.buttons[buttonIndex].value = buttonState.pressedValue;
+
+ if (axesStartIndex != -1) {
+ this.gamepad_.axes[axesStartIndex] = buttonState.xValue == null ? 0.0 : buttonState.xValue;
+ this.gamepad_.axes[axesStartIndex + 1] = buttonState.yValue == null ? 0.0 : buttonState.yValue;
+ }
+ }
+
+ // DOM Overlay Extensions
+ setOverlayPointerPosition(x, y) {
+ this.overlay_pointer_position_ = {x: x, y: y};
+ }
+
+ // Helpers for Mojom
+ _getInputSourceState() {
+ const input_state = {};
+
+ input_state.sourceId = this.source_id_;
+ input_state.isAuxiliary = false;
+
+ input_state.primaryInputPressed = this.primary_input_pressed_;
+ input_state.primaryInputClicked = this.primary_input_clicked_;
+
+ input_state.primarySqueezePressed = this.primary_squeeze_pressed_;
+ input_state.primarySqueezeClicked = this.primary_squeeze_clicked_;
+ // Setting the input source's "clicked" state should generate one "select"
+ // event. Reset the input value to prevent it from continuously generating
+ // events.
+ this.primary_input_clicked_ = false;
+ // Setting the input source's "clicked" state should generate one "squeeze"
+ // event. Reset the input value to prevent it from continuously generating
+ // events.
+ this.primary_squeeze_clicked_ = false;
+
+ input_state.mojoFromInput = this.mojo_from_input_;
+
+ input_state.gamepad = this.gamepad_;
+
+ input_state.emulatedPosition = this.emulated_position_;
+
+ if (this.desc_dirty_) {
+ const input_desc = {};
+
+ switch (this.target_ray_mode_) {
+ case 'gaze':
+ input_desc.targetRayMode = vrMojom.XRTargetRayMode.GAZING;
+ break;
+ case 'tracked-pointer':
+ input_desc.targetRayMode = vrMojom.XRTargetRayMode.POINTING;
+ break;
+ case 'screen':
+ input_desc.targetRayMode = vrMojom.XRTargetRayMode.TAPPING;
+ break;
+ default:
+ throw new Error('Unhandled target ray mode ' + this.target_ray_mode_);
+ }
+
+ switch (this.handedness_) {
+ case 'left':
+ input_desc.handedness = vrMojom.XRHandedness.LEFT;
+ break;
+ case 'right':
+ input_desc.handedness = vrMojom.XRHandedness.RIGHT;
+ break;
+ default:
+ input_desc.handedness = vrMojom.XRHandedness.NONE;
+ break;
+ }
+
+ // Mojo requires us to send the pointerOrigin as relative to the grip
+ // space. If we don't have a grip space, we'll just assume that there
+ // is a grip at identity. This allows tests to simulate controllers that
+ // are really just a pointer with no tracked grip, though we will end up
+ // exposing that grip space.
+ let mojo_from_input = XRMathHelper.identity();
+ switch (this.target_ray_mode_) {
+ case 'gaze':
+ case 'screen':
+ // For gaze and screen space, we won't have a mojo_from_input; however
+ // the "input" position is just the viewer, so use mojo_from_viewer.
+ mojo_from_input = this.pairedDevice_._getMojoFromViewer();
+ break;
+ case 'tracked-pointer':
+ // If we have a tracked grip position (e.g. mojo_from_input), then use
+ // that. If we don't, then we'll just set the pointer offset directly,
+ // using identity as set above.
+ if (this.mojo_from_input_) {
+ mojo_from_input = this.mojo_from_input_.matrix;
+ }
+ break;
+ default:
+ throw new Error('Unhandled target ray mode ' + this.target_ray_mode_);
+ }
+
+ // To convert mojo_from_pointer to input_from_pointer, we need:
+ // input_from_pointer = input_from_mojo * mojo_from_pointer
+ // Since we store mojo_from_input, we need to invert it here before
+ // multiplying.
+ let input_from_mojo = XRMathHelper.inverse(mojo_from_input);
+ input_desc.inputFromPointer = {};
+ input_desc.inputFromPointer.matrix =
+ XRMathHelper.mul4x4(input_from_mojo, this.mojo_from_pointer_.matrix);
+
+ input_desc.profiles = this.profiles_;
+
+ input_state.description = input_desc;
+
+ this.desc_dirty_ = false;
+ }
+
+ // Pointer data for DOM Overlay, set by setOverlayPointerPosition()
+ if (this.overlay_pointer_position_) {
+ input_state.overlayPointerPosition = this.overlay_pointer_position_;
+ this.overlay_pointer_position_ = null;
+ }
+
+ return input_state;
+ }
+
+ _getEmptyGamepad() {
+ // Mojo complains if some of the properties on Gamepad are null, so set
+ // everything to reasonable defaults that tests can override.
+ const gamepad = {
+ connected: true,
+ id: [],
+ timestamp: 0n,
+ axes: [],
+ buttons: [],
+ touchEvents: [],
+ mapping: GamepadMapping.GamepadMappingStandard,
+ displayId: 0,
+ };
+
+ switch (this.handedness_) {
+ case 'left':
+ gamepad.hand = GamepadHand.GamepadHandLeft;
+ break;
+ case 'right':
+ gamepad.hand = GamepadHand.GamepadHandRight;
+ break;
+ default:
+ gamepad.hand = GamepadHand.GamepadHandNone;
+ break;
+ }
+
+ return gamepad;
+ }
+
+ _addGamepadButton(buttonState) {
+ if (buttonState == null) {
+ return;
+ }
+
+ const buttonIndex = this._getButtonIndex(buttonState.buttonType);
+ const axesStartIndex = this._getAxesStartIndex(buttonState.buttonType);
+
+ if (buttonIndex == -1) {
+ throw new Error("Unknown Button Type!");
+ }
+
+ this.gamepad_.buttons[buttonIndex] = {
+ pressed: buttonState.pressed,
+ touched: buttonState.touched,
+ value: buttonState.pressedValue
+ };
+
+ // Add x/y value if supported.
+ if (axesStartIndex != -1) {
+ this.gamepad_.axes[axesStartIndex] = (buttonState.xValue == null ? 0.0 : buttonSate.xValue);
+ this.gamepad_.axes[axesStartIndex + 1] = (buttonState.yValue == null ? 0.0 : buttonSate.yValue);
+ }
+ }
+
+ // General Helper methods
+ _getButtonIndex(buttonType) {
+ switch (buttonType) {
+ case 'grip':
+ return 1;
+ case 'touchpad':
+ return 2;
+ case 'thumbstick':
+ return 3;
+ case 'optional-button':
+ return 4;
+ case 'optional-thumbstick':
+ return 5;
+ default:
+ return -1;
+ }
+ }
+
+ _getAxesStartIndex(buttonType) {
+ switch (buttonType) {
+ case 'touchpad':
+ return 0;
+ case 'thumbstick':
+ return 2;
+ case 'optional-thumbstick':
+ return 4;
+ default:
+ return -1;
+ }
+ }
+
+ _getMojoFromInputSource(mojo_from_viewer) {
+ return this.mojo_from_pointer_.matrix;
+ }
+}
+
+// Mojo helper classes
+class FakeXRHitTestSourceController {
+ constructor(id) {
+ this.id_ = id;
+ this.deleted_ = false;
+ }
+
+ get deleted() {
+ return this.deleted_;
+ }
+
+ // Internal setter:
+ set deleted(value) {
+ this.deleted_ = value;
+ }
+}
+
+class MockXRPresentationProvider {
+ constructor() {
+ this.receiver_ = null;
+ this.submit_frame_count_ = 0;
+ this.missing_frame_count_ = 0;
+ }
+
+ _bindProvider() {
+ const provider = new vrMojom.XRPresentationProviderRemote();
+
+ if (this.receiver_) {
+ this.receiver_.$.close();
+ }
+ this.receiver_ = new vrMojom.XRPresentationProviderReceiver(this);
+ this.receiver_.$.bindHandle(provider.$.bindNewPipeAndPassReceiver().handle);
+ return provider;
+ }
+
+ _getClientReceiver() {
+ this.submitFrameClient_ = new vrMojom.XRPresentationClientRemote();
+ return this.submitFrameClient_.$.bindNewPipeAndPassReceiver();
+ }
+
+ // XRPresentationProvider mojo implementation
+ updateLayerBounds(frameId, leftBounds, rightBounds, sourceSize) {}
+
+ submitFrameMissing(frameId, mailboxHolder, timeWaited) {
+ this.missing_frame_count_++;
+ }
+
+ submitFrame(frameId, mailboxHolder, timeWaited) {
+ this.submit_frame_count_++;
+
+ // Trigger the submit completion callbacks here. WARNING: The
+ // Javascript-based mojo mocks are *not* re-entrant. It's OK to
+ // wait for these notifications on the next frame, but waiting
+ // within the current frame would never finish since the incoming
+ // calls would be queued until the current execution context finishes.
+ this.submitFrameClient_.onSubmitFrameTransferred(true);
+ this.submitFrameClient_.onSubmitFrameRendered();
+ }
+
+ submitFrameWithTextureHandle(frameId, texture, syncToken) {}
+
+ submitFrameDrawnIntoTexture(frameId, syncToken, timeWaited) {}
+
+ // Utility methods
+ _close() {
+ if (this.receiver_) {
+ this.receiver_.$.close();
+ }
+ }
+}
+
+// Export these into the global object as a side effect of importing this
+// module.
+self.ChromeXRTest = ChromeXRTest;
+self.MockRuntime = MockRuntime;
+self.MockVRService = MockVRService;
+self.SubscribeToHitTestResult = vrMojom.SubscribeToHitTestResult;
+
+navigator.xr.test = new ChromeXRTest();
diff --git a/test/wpt/tests/resources/chromium/webxr-test.js.headers b/test/wpt/tests/resources/chromium/webxr-test.js.headers
new file mode 100644
index 0000000..6805c32
--- /dev/null
+++ b/test/wpt/tests/resources/chromium/webxr-test.js.headers
@@ -0,0 +1 @@
+Content-Type: text/javascript; charset=utf-8
diff --git a/test/wpt/tests/resources/declarative-shadow-dom-polyfill.js b/test/wpt/tests/resources/declarative-shadow-dom-polyfill.js
new file mode 100644
index 0000000..99a3e91
--- /dev/null
+++ b/test/wpt/tests/resources/declarative-shadow-dom-polyfill.js
@@ -0,0 +1,25 @@
+/*
+ * Polyfill for attaching shadow trees for declarative Shadow DOM for
+ * implementations that do not support declarative Shadow DOM.
+ *
+ * Note: this polyfill will feature-detect the native feature, and do nothing
+ * if supported.
+ *
+ * See: https://github.com/whatwg/html/pull/5465
+ *
+ * root: The root of the subtree in which to upgrade shadow roots
+ *
+ */
+
+function polyfill_declarative_shadow_dom(root) {
+ if (HTMLTemplateElement.prototype.hasOwnProperty('shadowRootMode'))
+ return;
+ root.querySelectorAll("template[shadowrootmode]").forEach(template => {
+ const mode = template.getAttribute("shadowrootmode");
+ const delegatesFocus = template.hasAttribute("shadowrootdelegatesfocus");
+ const shadowRoot = template.parentNode.attachShadow({ mode, delegatesFocus });
+ shadowRoot.appendChild(template.content);
+ template.remove();
+ polyfill_declarative_shadow_dom(shadowRoot);
+ });
+}
diff --git a/test/wpt/tests/resources/idlharness-shadowrealm.js b/test/wpt/tests/resources/idlharness-shadowrealm.js
new file mode 100644
index 0000000..9484ca6
--- /dev/null
+++ b/test/wpt/tests/resources/idlharness-shadowrealm.js
@@ -0,0 +1,61 @@
+// TODO: it would be nice to support `idl_array.add_objects`
+function fetch_text(url) {
+ return fetch(url).then(function (r) {
+ if (!r.ok) {
+ throw new Error("Error fetching " + url + ".");
+ }
+ return r.text();
+ });
+}
+
+/**
+ * idl_test_shadowrealm is a promise_test wrapper that handles the fetching of the IDL, and
+ * running the code in a `ShadowRealm`, avoiding repetitive boilerplate.
+ *
+ * @see https://github.com/tc39/proposal-shadowrealm
+ * @param {String[]} srcs Spec name(s) for source idl files (fetched from
+ * /interfaces/{name}.idl).
+ * @param {String[]} deps Spec name(s) for dependency idl files (fetched
+ * from /interfaces/{name}.idl). Order is important - dependencies from
+ * each source will only be included if they're already know to be a
+ * dependency (i.e. have already been seen).
+ */
+function idl_test_shadowrealm(srcs, deps) {
+ promise_setup(async t => {
+ const realm = new ShadowRealm();
+ // https://github.com/web-platform-tests/wpt/issues/31996
+ realm.evaluate("globalThis.self = globalThis; undefined;");
+
+ realm.evaluate(`
+ globalThis.self.GLOBAL = {
+ isWindow: function() { return false; },
+ isWorker: function() { return false; },
+ isShadowRealm: function() { return true; },
+ }; undefined;
+ `);
+ const specs = await Promise.all(srcs.concat(deps).map(spec => {
+ return fetch_text("/interfaces/" + spec + ".idl");
+ }));
+ const idls = JSON.stringify(specs);
+ await new Promise(
+ realm.evaluate(`(resolve,reject) => {
+ (async () => {
+ await import("/resources/testharness.js");
+ await import("/resources/WebIDLParser.js");
+ await import("/resources/idlharness.js");
+ const idls = ${idls};
+ const idl_array = new IdlArray();
+ for (let i = 0; i < ${srcs.length}; i++) {
+ idl_array.add_idls(idls[i]);
+ }
+ for (let i = ${srcs.length}; i < ${srcs.length + deps.length}; i++) {
+ idl_array.add_dependency_idls(idls[i]);
+ }
+ idl_array.test();
+ })().then(resolve, (e) => reject(e.toString()));
+ }`)
+ );
+ await fetch_tests_from_shadow_realm(realm);
+ });
+}
+// vim: set expandtab shiftwidth=4 tabstop=4 foldmarker=@{,@} foldmethod=marker:
diff --git a/test/wpt/tests/resources/idlharness.js b/test/wpt/tests/resources/idlharness.js
new file mode 100644
index 0000000..8f741b0
--- /dev/null
+++ b/test/wpt/tests/resources/idlharness.js
@@ -0,0 +1,3554 @@
+/* For user documentation see docs/_writing-tests/idlharness.md */
+
+/**
+ * Notes for people who want to edit this file (not just use it as a library):
+ *
+ * Most of the interesting stuff happens in the derived classes of IdlObject,
+ * especially IdlInterface. The entry point for all IdlObjects is .test(),
+ * which is called by IdlArray.test(). An IdlObject is conceptually just
+ * "thing we want to run tests on", and an IdlArray is an array of IdlObjects
+ * with some additional data thrown in.
+ *
+ * The object model is based on what WebIDLParser.js produces, which is in turn
+ * based on its pegjs grammar. If you want to figure out what properties an
+ * object will have from WebIDLParser.js, the best way is to look at the
+ * grammar:
+ *
+ * https://github.com/darobin/webidl.js/blob/master/lib/grammar.peg
+ *
+ * So for instance:
+ *
+ * // interface definition
+ * interface
+ * = extAttrs:extendedAttributeList? S? "interface" S name:identifier w herit:ifInheritance? w "{" w mem:ifMember* w "}" w ";" w
+ * { return { type: "interface", name: name, inheritance: herit, members: mem, extAttrs: extAttrs }; }
+ *
+ * This means that an "interface" object will have a .type property equal to
+ * the string "interface", a .name property equal to the identifier that the
+ * parser found, an .inheritance property equal to either null or the result of
+ * the "ifInheritance" production found elsewhere in the grammar, and so on.
+ * After each grammatical production is a JavaScript function in curly braces
+ * that gets called with suitable arguments and returns some JavaScript value.
+ *
+ * (Note that the version of WebIDLParser.js we use might sometimes be
+ * out-of-date or forked.)
+ *
+ * The members and methods of the classes defined by this file are all at least
+ * briefly documented, hopefully.
+ */
+(function(){
+"use strict";
+// Support subsetTestByKey from /common/subset-tests-by-key.js, but make it optional
+if (!('subsetTestByKey' in self)) {
+ self.subsetTestByKey = function(key, callback, ...args) {
+ return callback(...args);
+ }
+ self.shouldRunSubTest = () => true;
+}
+/// Helpers ///
+function constValue (cnt)
+{
+ if (cnt.type === "null") return null;
+ if (cnt.type === "NaN") return NaN;
+ if (cnt.type === "Infinity") return cnt.negative ? -Infinity : Infinity;
+ if (cnt.type === "number") return +cnt.value;
+ return cnt.value;
+}
+
+function minOverloadLength(overloads)
+{
+ // "The value of the Function object’s “length†property is
+ // a Number determined as follows:
+ // ". . .
+ // "Return the length of the shortest argument list of the
+ // entries in S."
+ if (!overloads.length) {
+ return 0;
+ }
+
+ return overloads.map(function(attr) {
+ return attr.arguments ? attr.arguments.filter(function(arg) {
+ return !arg.optional && !arg.variadic;
+ }).length : 0;
+ })
+ .reduce(function(m, n) { return Math.min(m, n); });
+}
+
+// A helper to get the global of a Function object. This is needed to determine
+// which global exceptions the function throws will come from.
+function globalOf(func)
+{
+ try {
+ // Use the fact that .constructor for a Function object is normally the
+ // Function constructor, which can be used to mint a new function in the
+ // right global.
+ return func.constructor("return this;")();
+ } catch (e) {
+ }
+ // If the above fails, because someone gave us a non-function, or a function
+ // with a weird proto chain or weird .constructor property, just fall back
+ // to 'self'.
+ return self;
+}
+
+// https://esdiscuss.org/topic/isconstructor#content-11
+function isConstructor(o) {
+ try {
+ new (new Proxy(o, {construct: () => ({})}));
+ return true;
+ } catch(e) {
+ return false;
+ }
+}
+
+function throwOrReject(a_test, operation, fn, obj, args, message, cb)
+{
+ if (operation.idlType.generic !== "Promise") {
+ assert_throws_js(globalOf(fn).TypeError, function() {
+ fn.apply(obj, args);
+ }, message);
+ cb();
+ } else {
+ try {
+ promise_rejects_js(a_test, TypeError, fn.apply(obj, args), message).then(cb, cb);
+ } catch (e){
+ a_test.step(function() {
+ assert_unreached("Throws \"" + e + "\" instead of rejecting promise");
+ cb();
+ });
+ }
+ }
+}
+
+function awaitNCallbacks(n, cb, ctx)
+{
+ var counter = 0;
+ return function() {
+ counter++;
+ if (counter >= n) {
+ cb();
+ }
+ };
+}
+
+/// IdlHarnessError ///
+// Entry point
+self.IdlHarnessError = function(message)
+{
+ /**
+ * Message to be printed as the error's toString invocation.
+ */
+ this.message = message;
+};
+
+IdlHarnessError.prototype = Object.create(Error.prototype);
+
+IdlHarnessError.prototype.toString = function()
+{
+ return this.message;
+};
+
+
+/// IdlArray ///
+// Entry point
+self.IdlArray = function()
+{
+ /**
+ * A map from strings to the corresponding named IdlObject, such as
+ * IdlInterface or IdlException. These are the things that test() will run
+ * tests on.
+ */
+ this.members = {};
+
+ /**
+ * A map from strings to arrays of strings. The keys are interface or
+ * exception names, and are expected to also exist as keys in this.members
+ * (otherwise they'll be ignored). This is populated by add_objects() --
+ * see documentation at the start of the file. The actual tests will be
+ * run by calling this.members[name].test_object(obj) for each obj in
+ * this.objects[name]. obj is a string that will be eval'd to produce a
+ * JavaScript value, which is supposed to be an object implementing the
+ * given IdlObject (interface, exception, etc.).
+ */
+ this.objects = {};
+
+ /**
+ * When adding multiple collections of IDLs one at a time, an earlier one
+ * might contain a partial interface or includes statement that depends
+ * on a later one. Save these up and handle them right before we run
+ * tests.
+ *
+ * Both this.partials and this.includes will be the objects as parsed by
+ * WebIDLParser.js, not wrapped in IdlInterface or similar.
+ */
+ this.partials = [];
+ this.includes = [];
+
+ /**
+ * Record of skipped IDL items, in case we later realize that they are a
+ * dependency (to retroactively process them).
+ */
+ this.skipped = new Map();
+};
+
+IdlArray.prototype.add_idls = function(raw_idls, options)
+{
+ /** Entry point. See documentation at beginning of file. */
+ this.internal_add_idls(WebIDL2.parse(raw_idls), options);
+};
+
+IdlArray.prototype.add_untested_idls = function(raw_idls, options)
+{
+ /** Entry point. See documentation at beginning of file. */
+ var parsed_idls = WebIDL2.parse(raw_idls);
+ this.mark_as_untested(parsed_idls);
+ this.internal_add_idls(parsed_idls, options);
+};
+
+IdlArray.prototype.mark_as_untested = function (parsed_idls)
+{
+ for (var i = 0; i < parsed_idls.length; i++) {
+ parsed_idls[i].untested = true;
+ if ("members" in parsed_idls[i]) {
+ for (var j = 0; j < parsed_idls[i].members.length; j++) {
+ parsed_idls[i].members[j].untested = true;
+ }
+ }
+ }
+};
+
+IdlArray.prototype.is_excluded_by_options = function (name, options)
+{
+ return options &&
+ (options.except && options.except.includes(name)
+ || options.only && !options.only.includes(name));
+};
+
+IdlArray.prototype.add_dependency_idls = function(raw_idls, options)
+{
+ return this.internal_add_dependency_idls(WebIDL2.parse(raw_idls), options);
+};
+
+IdlArray.prototype.internal_add_dependency_idls = function(parsed_idls, options)
+{
+ const new_options = { only: [] }
+
+ const all_deps = new Set();
+ Object.values(this.members).forEach(v => {
+ if (v.base) {
+ all_deps.add(v.base);
+ }
+ });
+ // Add both 'A' and 'B' for each 'A includes B' entry.
+ this.includes.forEach(i => {
+ all_deps.add(i.target);
+ all_deps.add(i.includes);
+ });
+ this.partials.forEach(p => all_deps.add(p.name));
+ // Add 'TypeOfType' for each "typedef TypeOfType MyType;" entry.
+ Object.entries(this.members).forEach(([k, v]) => {
+ if (v instanceof IdlTypedef) {
+ let defs = v.idlType.union
+ ? v.idlType.idlType.map(t => t.idlType)
+ : [v.idlType.idlType];
+ defs.forEach(d => all_deps.add(d));
+ }
+ });
+
+ // Add the attribute idlTypes of all the nested members of idls.
+ const attrDeps = parsedIdls => {
+ return parsedIdls.reduce((deps, parsed) => {
+ if (parsed.members) {
+ for (const attr of Object.values(parsed.members).filter(m => m.type === 'attribute')) {
+ let attrType = attr.idlType;
+ // Check for generic members (e.g. FrozenArray<MyType>)
+ if (attrType.generic) {
+ deps.add(attrType.generic);
+ attrType = attrType.idlType;
+ }
+ deps.add(attrType.idlType);
+ }
+ }
+ if (parsed.base in this.members) {
+ attrDeps([this.members[parsed.base]]).forEach(dep => deps.add(dep));
+ }
+ return deps;
+ }, new Set());
+ };
+
+ const testedMembers = Object.values(this.members).filter(m => !m.untested && m.members);
+ attrDeps(testedMembers).forEach(dep => all_deps.add(dep));
+
+ const testedPartials = this.partials.filter(m => !m.untested && m.members);
+ attrDeps(testedPartials).forEach(dep => all_deps.add(dep));
+
+
+ if (options && options.except && options.only) {
+ throw new IdlHarnessError("The only and except options can't be used together.");
+ }
+
+ const defined_or_untested = name => {
+ // NOTE: Deps are untested, so we're lenient, and skip re-encountered definitions.
+ // e.g. for 'idl' containing A:B, B:C, C:D
+ // array.add_idls(idl, {only: ['A','B']}).
+ // array.add_dependency_idls(idl);
+ // B would be encountered as tested, and encountered as a dep, so we ignore.
+ return name in this.members
+ || this.is_excluded_by_options(name, options);
+ }
+ // Maps name -> [parsed_idl, ...]
+ const process = function(parsed) {
+ var deps = [];
+ if (parsed.name) {
+ deps.push(parsed.name);
+ } else if (parsed.type === "includes") {
+ deps.push(parsed.target);
+ deps.push(parsed.includes);
+ }
+
+ deps = deps.filter(function(name) {
+ if (!name
+ || name === parsed.name && defined_or_untested(name)
+ || !all_deps.has(name)) {
+ // Flag as skipped, if it's not already processed, so we can
+ // come back to it later if we retrospectively call it a dep.
+ if (name && !(name in this.members)) {
+ this.skipped.has(name)
+ ? this.skipped.get(name).push(parsed)
+ : this.skipped.set(name, [parsed]);
+ }
+ return false;
+ }
+ return true;
+ }.bind(this));
+
+ deps.forEach(function(name) {
+ if (!new_options.only.includes(name)) {
+ new_options.only.push(name);
+ }
+
+ const follow_up = new Set();
+ for (const dep_type of ["inheritance", "includes"]) {
+ if (parsed[dep_type]) {
+ const inheriting = parsed[dep_type];
+ const inheritor = parsed.name || parsed.target;
+ const deps = [inheriting];
+ // For A includes B, we can ignore A, unless B (or some of its
+ // members) is being tested.
+ if (dep_type !== "includes"
+ || inheriting in this.members && !this.members[inheriting].untested
+ || this.partials.some(function(p) {
+ return p.name === inheriting;
+ })) {
+ deps.push(inheritor);
+ }
+ for (const dep of deps) {
+ if (!new_options.only.includes(dep)) {
+ new_options.only.push(dep);
+ }
+ all_deps.add(dep);
+ follow_up.add(dep);
+ }
+ }
+ }
+
+ for (const deferred of follow_up) {
+ if (this.skipped.has(deferred)) {
+ const next = this.skipped.get(deferred);
+ this.skipped.delete(deferred);
+ next.forEach(process);
+ }
+ }
+ }.bind(this));
+ }.bind(this);
+
+ for (let parsed of parsed_idls) {
+ process(parsed);
+ }
+
+ this.mark_as_untested(parsed_idls);
+
+ if (new_options.only.length) {
+ this.internal_add_idls(parsed_idls, new_options);
+ }
+}
+
+IdlArray.prototype.internal_add_idls = function(parsed_idls, options)
+{
+ /**
+ * Internal helper called by add_idls() and add_untested_idls().
+ *
+ * parsed_idls is an array of objects that come from WebIDLParser.js's
+ * "definitions" production. The add_untested_idls() entry point
+ * additionally sets an .untested property on each object (and its
+ * .members) so that they'll be skipped by test() -- they'll only be
+ * used for base interfaces of tested interfaces, return types, etc.
+ *
+ * options is a dictionary that can have an only or except member which are
+ * arrays. If only is given then only members, partials and interface
+ * targets listed will be added, and if except is given only those that
+ * aren't listed will be added. Only one of only and except can be used.
+ */
+
+ if (options && options.only && options.except)
+ {
+ throw new IdlHarnessError("The only and except options can't be used together.");
+ }
+
+ var should_skip = name => {
+ return this.is_excluded_by_options(name, options);
+ }
+
+ parsed_idls.forEach(function(parsed_idl)
+ {
+ var partial_types = [
+ "interface",
+ "interface mixin",
+ "dictionary",
+ "namespace",
+ ];
+ if (parsed_idl.partial && partial_types.includes(parsed_idl.type))
+ {
+ if (should_skip(parsed_idl.name))
+ {
+ return;
+ }
+ this.partials.push(parsed_idl);
+ return;
+ }
+
+ if (parsed_idl.type == "includes")
+ {
+ if (should_skip(parsed_idl.target))
+ {
+ return;
+ }
+ this.includes.push(parsed_idl);
+ return;
+ }
+
+ parsed_idl.array = this;
+ if (should_skip(parsed_idl.name))
+ {
+ return;
+ }
+ if (parsed_idl.name in this.members)
+ {
+ throw new IdlHarnessError("Duplicate identifier " + parsed_idl.name);
+ }
+
+ switch(parsed_idl.type)
+ {
+ case "interface":
+ this.members[parsed_idl.name] =
+ new IdlInterface(parsed_idl, /* is_callback = */ false, /* is_mixin = */ false);
+ break;
+
+ case "interface mixin":
+ this.members[parsed_idl.name] =
+ new IdlInterface(parsed_idl, /* is_callback = */ false, /* is_mixin = */ true);
+ break;
+
+ case "dictionary":
+ // Nothing to test, but we need the dictionary info around for type
+ // checks
+ this.members[parsed_idl.name] = new IdlDictionary(parsed_idl);
+ break;
+
+ case "typedef":
+ this.members[parsed_idl.name] = new IdlTypedef(parsed_idl);
+ break;
+
+ case "callback":
+ this.members[parsed_idl.name] = new IdlCallback(parsed_idl);
+ break;
+
+ case "enum":
+ this.members[parsed_idl.name] = new IdlEnum(parsed_idl);
+ break;
+
+ case "callback interface":
+ this.members[parsed_idl.name] =
+ new IdlInterface(parsed_idl, /* is_callback = */ true, /* is_mixin = */ false);
+ break;
+
+ case "namespace":
+ this.members[parsed_idl.name] = new IdlNamespace(parsed_idl);
+ break;
+
+ default:
+ throw parsed_idl.name + ": " + parsed_idl.type + " not yet supported";
+ }
+ }.bind(this));
+};
+
+IdlArray.prototype.add_objects = function(dict)
+{
+ /** Entry point. See documentation at beginning of file. */
+ for (var k in dict)
+ {
+ if (k in this.objects)
+ {
+ this.objects[k] = this.objects[k].concat(dict[k]);
+ }
+ else
+ {
+ this.objects[k] = dict[k];
+ }
+ }
+};
+
+IdlArray.prototype.prevent_multiple_testing = function(name)
+{
+ /** Entry point. See documentation at beginning of file. */
+ this.members[name].prevent_multiple_testing = true;
+};
+
+IdlArray.prototype.is_json_type = function(type)
+{
+ /**
+ * Checks whether type is a JSON type as per
+ * https://webidl.spec.whatwg.org/#dfn-json-types
+ */
+
+ var idlType = type.idlType;
+
+ if (type.generic == "Promise") { return false; }
+
+ // nullable and annotated types don't need to be handled separately,
+ // as webidl2 doesn't represent them wrapped-up (as they're described
+ // in WebIDL).
+
+ // union and record types
+ if (type.union || type.generic == "record") {
+ return idlType.every(this.is_json_type, this);
+ }
+
+ // sequence types
+ if (type.generic == "sequence" || type.generic == "FrozenArray") {
+ return this.is_json_type(idlType[0]);
+ }
+
+ if (typeof idlType != "string") { throw new Error("Unexpected type " + JSON.stringify(idlType)); }
+
+ switch (idlType)
+ {
+ // Numeric types
+ case "byte":
+ case "octet":
+ case "short":
+ case "unsigned short":
+ case "long":
+ case "unsigned long":
+ case "long long":
+ case "unsigned long long":
+ case "float":
+ case "double":
+ case "unrestricted float":
+ case "unrestricted double":
+ // boolean
+ case "boolean":
+ // string types
+ case "DOMString":
+ case "ByteString":
+ case "USVString":
+ // object type
+ case "object":
+ return true;
+ case "Error":
+ case "DOMException":
+ case "Int8Array":
+ case "Int16Array":
+ case "Int32Array":
+ case "Uint8Array":
+ case "Uint16Array":
+ case "Uint32Array":
+ case "Uint8ClampedArray":
+ case "BigInt64Array":
+ case "BigUint64Array":
+ case "Float32Array":
+ case "Float64Array":
+ case "ArrayBuffer":
+ case "DataView":
+ case "any":
+ return false;
+ default:
+ var thing = this.members[idlType];
+ if (!thing) { throw new Error("Type " + idlType + " not found"); }
+ if (thing instanceof IdlEnum) { return true; }
+
+ if (thing instanceof IdlTypedef) {
+ return this.is_json_type(thing.idlType);
+ }
+
+ // dictionaries where all of their members are JSON types
+ if (thing instanceof IdlDictionary) {
+ const map = new Map();
+ for (const dict of thing.get_reverse_inheritance_stack()) {
+ for (const m of dict.members) {
+ map.set(m.name, m.idlType);
+ }
+ }
+ return Array.from(map.values()).every(this.is_json_type, this);
+ }
+
+ // interface types that have a toJSON operation declared on themselves or
+ // one of their inherited interfaces.
+ if (thing instanceof IdlInterface) {
+ var base;
+ while (thing)
+ {
+ if (thing.has_to_json_regular_operation()) { return true; }
+ var mixins = this.includes[thing.name];
+ if (mixins) {
+ mixins = mixins.map(function(id) {
+ var mixin = this.members[id];
+ if (!mixin) {
+ throw new Error("Interface " + id + " not found (implemented by " + thing.name + ")");
+ }
+ return mixin;
+ }, this);
+ if (mixins.some(function(m) { return m.has_to_json_regular_operation() } )) { return true; }
+ }
+ if (!thing.base) { return false; }
+ base = this.members[thing.base];
+ if (!base) {
+ throw new Error("Interface " + thing.base + " not found (inherited by " + thing.name + ")");
+ }
+ thing = base;
+ }
+ return false;
+ }
+ return false;
+ }
+};
+
+function exposure_set(object, default_set) {
+ var exposed = object.extAttrs && object.extAttrs.filter(a => a.name === "Exposed");
+ if (exposed && exposed.length > 1) {
+ throw new IdlHarnessError(
+ `Multiple 'Exposed' extended attributes on ${object.name}`);
+ }
+
+ let result = default_set || ["Window"];
+ if (result && !(result instanceof Set)) {
+ result = new Set(result);
+ }
+ if (exposed && exposed.length) {
+ const { rhs } = exposed[0];
+ // Could be a list or a string.
+ const set =
+ rhs.type === "*" ?
+ [ "*" ] :
+ rhs.type === "identifier-list" ?
+ rhs.value.map(id => id.value) :
+ [ rhs.value ];
+ result = new Set(set);
+ }
+ if (result && result.has("*")) {
+ return "*";
+ }
+ if (result && result.has("Worker")) {
+ result.delete("Worker");
+ result.add("DedicatedWorker");
+ result.add("ServiceWorker");
+ result.add("SharedWorker");
+ }
+ return result;
+}
+
+function exposed_in(globals) {
+ if (globals === "*") {
+ return true;
+ }
+ if ('Window' in self) {
+ return globals.has("Window");
+ }
+ if ('DedicatedWorkerGlobalScope' in self &&
+ self instanceof DedicatedWorkerGlobalScope) {
+ return globals.has("DedicatedWorker");
+ }
+ if ('SharedWorkerGlobalScope' in self &&
+ self instanceof SharedWorkerGlobalScope) {
+ return globals.has("SharedWorker");
+ }
+ if ('ServiceWorkerGlobalScope' in self &&
+ self instanceof ServiceWorkerGlobalScope) {
+ return globals.has("ServiceWorker");
+ }
+ if (Object.getPrototypeOf(self) === Object.prototype) {
+ // ShadowRealm - only exposed with `"*"`.
+ return false;
+ }
+ throw new IdlHarnessError("Unexpected global object");
+}
+
+/**
+ * Asserts that the given error message is thrown for the given function.
+ * @param {string|IdlHarnessError} error Expected Error message.
+ * @param {Function} idlArrayFunc Function operating on an IdlArray that should throw.
+ */
+IdlArray.prototype.assert_throws = function(error, idlArrayFunc)
+{
+ try {
+ idlArrayFunc.call(this, this);
+ } catch (e) {
+ if (e instanceof AssertionError) {
+ throw e;
+ }
+ // Assertions for behaviour of the idlharness.js engine.
+ if (error instanceof IdlHarnessError) {
+ error = error.message;
+ }
+ if (e.message !== error) {
+ throw new IdlHarnessError(`${idlArrayFunc} threw "${e}", not the expected IdlHarnessError "${error}"`);
+ }
+ return;
+ }
+ throw new IdlHarnessError(`${idlArrayFunc} did not throw the expected IdlHarnessError`);
+}
+
+IdlArray.prototype.test = function()
+{
+ /** Entry point. See documentation at beginning of file. */
+
+ // First merge in all partial definitions and interface mixins.
+ this.merge_partials();
+ this.merge_mixins();
+
+ // Assert B defined for A : B
+ for (const member of Object.values(this.members).filter(m => m.base)) {
+ const lhs = member.name;
+ const rhs = member.base;
+ if (!(rhs in this.members)) throw new IdlHarnessError(`${lhs} inherits ${rhs}, but ${rhs} is undefined.`);
+ const lhs_is_interface = this.members[lhs] instanceof IdlInterface;
+ const rhs_is_interface = this.members[rhs] instanceof IdlInterface;
+ if (rhs_is_interface != lhs_is_interface) {
+ if (!lhs_is_interface) throw new IdlHarnessError(`${lhs} inherits ${rhs}, but ${lhs} is not an interface.`);
+ if (!rhs_is_interface) throw new IdlHarnessError(`${lhs} inherits ${rhs}, but ${rhs} is not an interface.`);
+ }
+ // Check for circular dependencies.
+ member.get_reverse_inheritance_stack();
+ }
+
+ Object.getOwnPropertyNames(this.members).forEach(function(memberName) {
+ var member = this.members[memberName];
+ if (!(member instanceof IdlInterface)) {
+ return;
+ }
+
+ var globals = exposure_set(member);
+ member.exposed = exposed_in(globals);
+ member.exposureSet = globals;
+ }.bind(this));
+
+ // Now run test() on every member, and test_object() for every object.
+ for (var name in this.members)
+ {
+ this.members[name].test();
+ if (name in this.objects)
+ {
+ const objects = this.objects[name];
+ if (!objects || !Array.isArray(objects)) {
+ throw new IdlHarnessError(`Invalid or empty objects for member ${name}`);
+ }
+ objects.forEach(function(str)
+ {
+ if (!this.members[name] || !(this.members[name] instanceof IdlInterface)) {
+ throw new IdlHarnessError(`Invalid object member name ${name}`);
+ }
+ this.members[name].test_object(str);
+ }.bind(this));
+ }
+ }
+};
+
+IdlArray.prototype.merge_partials = function()
+{
+ const testedPartials = new Map();
+ this.partials.forEach(function(parsed_idl)
+ {
+ const originalExists = parsed_idl.name in this.members
+ && (this.members[parsed_idl.name] instanceof IdlInterface
+ || this.members[parsed_idl.name] instanceof IdlDictionary
+ || this.members[parsed_idl.name] instanceof IdlNamespace);
+
+ // Ensure unique test name in case of multiple partials.
+ let partialTestName = parsed_idl.name;
+ let partialTestCount = 1;
+ if (testedPartials.has(parsed_idl.name)) {
+ partialTestCount += testedPartials.get(parsed_idl.name);
+ partialTestName = `${partialTestName}[${partialTestCount}]`;
+ }
+ testedPartials.set(parsed_idl.name, partialTestCount);
+
+ if (!parsed_idl.untested) {
+ test(function () {
+ assert_true(originalExists, `Original ${parsed_idl.type} should be defined`);
+
+ var expected;
+ switch (parsed_idl.type) {
+ case 'dictionary': expected = IdlDictionary; break;
+ case 'namespace': expected = IdlNamespace; break;
+ case 'interface':
+ case 'interface mixin':
+ default:
+ expected = IdlInterface; break;
+ }
+ assert_true(
+ expected.prototype.isPrototypeOf(this.members[parsed_idl.name]),
+ `Original ${parsed_idl.name} definition should have type ${parsed_idl.type}`);
+ }.bind(this), `Partial ${parsed_idl.type} ${partialTestName}: original ${parsed_idl.type} defined`);
+ }
+ if (!originalExists) {
+ // Not good.. but keep calm and carry on.
+ return;
+ }
+
+ if (parsed_idl.extAttrs)
+ {
+ // Special-case "Exposed". Must be a subset of original interface's exposure.
+ // Exposed on a partial is the equivalent of having the same Exposed on all nested members.
+ // See https://github.com/heycam/webidl/issues/154 for discrepency between Exposed and
+ // other extended attributes on partial interfaces.
+ const exposureAttr = parsed_idl.extAttrs.find(a => a.name === "Exposed");
+ if (exposureAttr) {
+ if (!parsed_idl.untested) {
+ test(function () {
+ const partialExposure = exposure_set(parsed_idl);
+ const memberExposure = exposure_set(this.members[parsed_idl.name]);
+ if (memberExposure === "*") {
+ return;
+ }
+ if (partialExposure === "*") {
+ throw new IdlHarnessError(
+ `Partial ${parsed_idl.name} ${parsed_idl.type} is exposed everywhere, the original ${parsed_idl.type} is not.`);
+ }
+ partialExposure.forEach(name => {
+ if (!memberExposure || !memberExposure.has(name)) {
+ throw new IdlHarnessError(
+ `Partial ${parsed_idl.name} ${parsed_idl.type} is exposed to '${name}', the original ${parsed_idl.type} is not.`);
+ }
+ });
+ }.bind(this), `Partial ${parsed_idl.type} ${partialTestName}: valid exposure set`);
+ }
+ parsed_idl.members.forEach(function (member) {
+ member.extAttrs.push(exposureAttr);
+ }.bind(this));
+ }
+
+ parsed_idl.extAttrs.forEach(function(extAttr)
+ {
+ // "Exposed" already handled above.
+ if (extAttr.name === "Exposed") {
+ return;
+ }
+ this.members[parsed_idl.name].extAttrs.push(extAttr);
+ }.bind(this));
+ }
+ if (parsed_idl.members.length) {
+ test(function () {
+ var clash = parsed_idl.members.find(function(member) {
+ return this.members[parsed_idl.name].members.find(function(m) {
+ return this.are_duplicate_members(m, member);
+ }.bind(this));
+ }.bind(this));
+ parsed_idl.members.forEach(function(member)
+ {
+ this.members[parsed_idl.name].members.push(new IdlInterfaceMember(member));
+ }.bind(this));
+ assert_true(!clash, "member " + (clash && clash.name) + " is unique");
+ }.bind(this), `Partial ${parsed_idl.type} ${partialTestName}: member names are unique`);
+ }
+ }.bind(this));
+ this.partials = [];
+}
+
+IdlArray.prototype.merge_mixins = function()
+{
+ for (const parsed_idl of this.includes)
+ {
+ const lhs = parsed_idl.target;
+ const rhs = parsed_idl.includes;
+
+ var errStr = lhs + " includes " + rhs + ", but ";
+ if (!(lhs in this.members)) throw errStr + lhs + " is undefined.";
+ if (!(this.members[lhs] instanceof IdlInterface)) throw errStr + lhs + " is not an interface.";
+ if (!(rhs in this.members)) throw errStr + rhs + " is undefined.";
+ if (!(this.members[rhs] instanceof IdlInterface)) throw errStr + rhs + " is not an interface.";
+
+ if (this.members[rhs].members.length) {
+ test(function () {
+ var clash = this.members[rhs].members.find(function(member) {
+ return this.members[lhs].members.find(function(m) {
+ return this.are_duplicate_members(m, member);
+ }.bind(this));
+ }.bind(this));
+ this.members[rhs].members.forEach(function(member) {
+ assert_true(
+ this.members[lhs].members.every(m => !this.are_duplicate_members(m, member)),
+ "member " + member.name + " is unique");
+ this.members[lhs].members.push(new IdlInterfaceMember(member));
+ }.bind(this));
+ assert_true(!clash, "member " + (clash && clash.name) + " is unique");
+ }.bind(this), lhs + " includes " + rhs + ": member names are unique");
+ }
+ }
+ this.includes = [];
+}
+
+IdlArray.prototype.are_duplicate_members = function(m1, m2) {
+ if (m1.name !== m2.name) {
+ return false;
+ }
+ if (m1.type === 'operation' && m2.type === 'operation'
+ && m1.arguments.length !== m2.arguments.length) {
+ // Method overload. TODO: Deep comparison of arguments.
+ return false;
+ }
+ return true;
+}
+
+IdlArray.prototype.assert_type_is = function(value, type)
+{
+ if (type.idlType in this.members
+ && this.members[type.idlType] instanceof IdlTypedef) {
+ this.assert_type_is(value, this.members[type.idlType].idlType);
+ return;
+ }
+
+ if (type.nullable && value === null)
+ {
+ // This is fine
+ return;
+ }
+
+ if (type.union) {
+ for (var i = 0; i < type.idlType.length; i++) {
+ try {
+ this.assert_type_is(value, type.idlType[i]);
+ // No AssertionError, so we match one type in the union
+ return;
+ } catch(e) {
+ if (e instanceof AssertionError) {
+ // We didn't match this type, let's try some others
+ continue;
+ }
+ throw e;
+ }
+ }
+ // TODO: Is there a nice way to list the union's types in the message?
+ assert_true(false, "Attribute has value " + format_value(value)
+ + " which doesn't match any of the types in the union");
+
+ }
+
+ /**
+ * Helper function that tests that value is an instance of type according
+ * to the rules of WebIDL. value is any JavaScript value, and type is an
+ * object produced by WebIDLParser.js' "type" production. That production
+ * is fairly elaborate due to the complexity of WebIDL's types, so it's
+ * best to look at the grammar to figure out what properties it might have.
+ */
+ if (type.idlType == "any")
+ {
+ // No assertions to make
+ return;
+ }
+
+ if (type.array)
+ {
+ // TODO: not supported yet
+ return;
+ }
+
+ if (type.generic === "sequence" || type.generic == "ObservableArray")
+ {
+ assert_true(Array.isArray(value), "should be an Array");
+ if (!value.length)
+ {
+ // Nothing we can do.
+ return;
+ }
+ this.assert_type_is(value[0], type.idlType[0]);
+ return;
+ }
+
+ if (type.generic === "Promise") {
+ assert_true("then" in value, "Attribute with a Promise type should have a then property");
+ // TODO: Ideally, we would check on project fulfillment
+ // that we get the right type
+ // but that would require making the type check async
+ return;
+ }
+
+ if (type.generic === "FrozenArray") {
+ assert_true(Array.isArray(value), "Value should be array");
+ assert_true(Object.isFrozen(value), "Value should be frozen");
+ if (!value.length)
+ {
+ // Nothing we can do.
+ return;
+ }
+ this.assert_type_is(value[0], type.idlType[0]);
+ return;
+ }
+
+ type = Array.isArray(type.idlType) ? type.idlType[0] : type.idlType;
+
+ switch(type)
+ {
+ case "undefined":
+ assert_equals(value, undefined);
+ return;
+
+ case "boolean":
+ assert_equals(typeof value, "boolean");
+ return;
+
+ case "byte":
+ assert_equals(typeof value, "number");
+ assert_equals(value, Math.floor(value), "should be an integer");
+ assert_true(-128 <= value && value <= 127, "byte " + value + " should be in range [-128, 127]");
+ return;
+
+ case "octet":
+ assert_equals(typeof value, "number");
+ assert_equals(value, Math.floor(value), "should be an integer");
+ assert_true(0 <= value && value <= 255, "octet " + value + " should be in range [0, 255]");
+ return;
+
+ case "short":
+ assert_equals(typeof value, "number");
+ assert_equals(value, Math.floor(value), "should be an integer");
+ assert_true(-32768 <= value && value <= 32767, "short " + value + " should be in range [-32768, 32767]");
+ return;
+
+ case "unsigned short":
+ assert_equals(typeof value, "number");
+ assert_equals(value, Math.floor(value), "should be an integer");
+ assert_true(0 <= value && value <= 65535, "unsigned short " + value + " should be in range [0, 65535]");
+ return;
+
+ case "long":
+ assert_equals(typeof value, "number");
+ assert_equals(value, Math.floor(value), "should be an integer");
+ assert_true(-2147483648 <= value && value <= 2147483647, "long " + value + " should be in range [-2147483648, 2147483647]");
+ return;
+
+ case "unsigned long":
+ assert_equals(typeof value, "number");
+ assert_equals(value, Math.floor(value), "should be an integer");
+ assert_true(0 <= value && value <= 4294967295, "unsigned long " + value + " should be in range [0, 4294967295]");
+ return;
+
+ case "long long":
+ assert_equals(typeof value, "number");
+ return;
+
+ case "unsigned long long":
+ case "DOMTimeStamp":
+ assert_equals(typeof value, "number");
+ assert_true(0 <= value, "unsigned long long should be positive");
+ return;
+
+ case "float":
+ assert_equals(typeof value, "number");
+ assert_equals(value, Math.fround(value), "float rounded to 32-bit float should be itself");
+ assert_not_equals(value, Infinity);
+ assert_not_equals(value, -Infinity);
+ assert_not_equals(value, NaN);
+ return;
+
+ case "DOMHighResTimeStamp":
+ case "double":
+ assert_equals(typeof value, "number");
+ assert_not_equals(value, Infinity);
+ assert_not_equals(value, -Infinity);
+ assert_not_equals(value, NaN);
+ return;
+
+ case "unrestricted float":
+ assert_equals(typeof value, "number");
+ assert_equals(value, Math.fround(value), "unrestricted float rounded to 32-bit float should be itself");
+ return;
+
+ case "unrestricted double":
+ assert_equals(typeof value, "number");
+ return;
+
+ case "DOMString":
+ assert_equals(typeof value, "string");
+ return;
+
+ case "ByteString":
+ assert_equals(typeof value, "string");
+ assert_regexp_match(value, /^[\x00-\x7F]*$/);
+ return;
+
+ case "USVString":
+ assert_equals(typeof value, "string");
+ assert_regexp_match(value, /^([\x00-\ud7ff\ue000-\uffff]|[\ud800-\udbff][\udc00-\udfff])*$/);
+ return;
+
+ case "ArrayBufferView":
+ assert_true(ArrayBuffer.isView(value));
+ return;
+
+ case "object":
+ assert_in_array(typeof value, ["object", "function"], "wrong type: not object or function");
+ return;
+ }
+
+ // This is a catch-all for any IDL type name which follows JS class
+ // semantics. This includes some non-interface IDL types (e.g. Int8Array,
+ // Function, ...), as well as any interface types that are not in the IDL
+ // that is fed to the harness. If an IDL type does not follow JS class
+ // semantics then it should go in the switch statement above. If an IDL
+ // type needs full checking, then the test should include it in the IDL it
+ // feeds to the harness.
+ if (!(type in this.members))
+ {
+ assert_true(value instanceof self[type], "wrong type: not a " + type);
+ return;
+ }
+
+ if (this.members[type] instanceof IdlInterface)
+ {
+ // We don't want to run the full
+ // IdlInterface.prototype.test_instance_of, because that could result
+ // in an infinite loop. TODO: This means we don't have tests for
+ // LegacyNoInterfaceObject interfaces, and we also can't test objects
+ // that come from another self.
+ assert_in_array(typeof value, ["object", "function"], "wrong type: not object or function");
+ if (value instanceof Object
+ && !this.members[type].has_extended_attribute("LegacyNoInterfaceObject")
+ && type in self)
+ {
+ assert_true(value instanceof self[type], "instanceof " + type);
+ }
+ }
+ else if (this.members[type] instanceof IdlEnum)
+ {
+ assert_equals(typeof value, "string");
+ }
+ else if (this.members[type] instanceof IdlDictionary)
+ {
+ // TODO: Test when we actually have something to test this on
+ }
+ else if (this.members[type] instanceof IdlCallback)
+ {
+ assert_equals(typeof value, "function");
+ }
+ else
+ {
+ throw new IdlHarnessError("Type " + type + " isn't an interface, callback or dictionary");
+ }
+};
+
+/// IdlObject ///
+function IdlObject() {}
+IdlObject.prototype.test = function()
+{
+ /**
+ * By default, this does nothing, so no actual tests are run for IdlObjects
+ * that don't define any (e.g., IdlDictionary at the time of this writing).
+ */
+};
+
+IdlObject.prototype.has_extended_attribute = function(name)
+{
+ /**
+ * This is only meaningful for things that support extended attributes,
+ * such as interfaces, exceptions, and members.
+ */
+ return this.extAttrs.some(function(o)
+ {
+ return o.name == name;
+ });
+};
+
+
+/// IdlDictionary ///
+// Used for IdlArray.prototype.assert_type_is
+function IdlDictionary(obj)
+{
+ /**
+ * obj is an object produced by the WebIDLParser.js "dictionary"
+ * production.
+ */
+
+ /** Self-explanatory. */
+ this.name = obj.name;
+
+ /** A back-reference to our IdlArray. */
+ this.array = obj.array;
+
+ /** An array of objects produced by the "dictionaryMember" production. */
+ this.members = obj.members;
+
+ /**
+ * The name (as a string) of the dictionary type we inherit from, or null
+ * if there is none.
+ */
+ this.base = obj.inheritance;
+}
+
+IdlDictionary.prototype = Object.create(IdlObject.prototype);
+
+IdlDictionary.prototype.get_reverse_inheritance_stack = function() {
+ return IdlInterface.prototype.get_reverse_inheritance_stack.call(this);
+};
+
+/// IdlInterface ///
+function IdlInterface(obj, is_callback, is_mixin)
+{
+ /**
+ * obj is an object produced by the WebIDLParser.js "interface" production.
+ */
+
+ /** Self-explanatory. */
+ this.name = obj.name;
+
+ /** A back-reference to our IdlArray. */
+ this.array = obj.array;
+
+ /**
+ * An indicator of whether we should run tests on the interface object and
+ * interface prototype object. Tests on members are controlled by .untested
+ * on each member, not this.
+ */
+ this.untested = obj.untested;
+
+ /** An array of objects produced by the "ExtAttr" production. */
+ this.extAttrs = obj.extAttrs;
+
+ /** An array of IdlInterfaceMembers. */
+ this.members = obj.members.map(function(m){return new IdlInterfaceMember(m); });
+ if (this.has_extended_attribute("LegacyUnforgeable")) {
+ this.members
+ .filter(function(m) { return m.special !== "static" && (m.type == "attribute" || m.type == "operation"); })
+ .forEach(function(m) { return m.isUnforgeable = true; });
+ }
+
+ /**
+ * The name (as a string) of the type we inherit from, or null if there is
+ * none.
+ */
+ this.base = obj.inheritance;
+
+ this._is_callback = is_callback;
+ this._is_mixin = is_mixin;
+}
+IdlInterface.prototype = Object.create(IdlObject.prototype);
+IdlInterface.prototype.is_callback = function()
+{
+ return this._is_callback;
+};
+
+IdlInterface.prototype.is_mixin = function()
+{
+ return this._is_mixin;
+};
+
+IdlInterface.prototype.has_constants = function()
+{
+ return this.members.some(function(member) {
+ return member.type === "const";
+ });
+};
+
+IdlInterface.prototype.get_unscopables = function()
+{
+ return this.members.filter(function(member) {
+ return member.isUnscopable;
+ });
+};
+
+IdlInterface.prototype.is_global = function()
+{
+ return this.extAttrs.some(function(attribute) {
+ return attribute.name === "Global";
+ });
+};
+
+/**
+ * Value of the LegacyNamespace extended attribute, if any.
+ *
+ * https://webidl.spec.whatwg.org/#LegacyNamespace
+ */
+IdlInterface.prototype.get_legacy_namespace = function()
+{
+ var legacyNamespace = this.extAttrs.find(function(attribute) {
+ return attribute.name === "LegacyNamespace";
+ });
+ return legacyNamespace ? legacyNamespace.rhs.value : undefined;
+};
+
+IdlInterface.prototype.get_interface_object_owner = function()
+{
+ var legacyNamespace = this.get_legacy_namespace();
+ return legacyNamespace ? self[legacyNamespace] : self;
+};
+
+IdlInterface.prototype.should_have_interface_object = function()
+{
+ // "For every interface that is exposed in a given ECMAScript global
+ // environment and:
+ // * is a callback interface that has constants declared on it, or
+ // * is a non-callback interface that is not declared with the
+ // [LegacyNoInterfaceObject] extended attribute,
+ // a corresponding property MUST exist on the ECMAScript global object.
+
+ return this.is_callback() ? this.has_constants() : !this.has_extended_attribute("LegacyNoInterfaceObject");
+};
+
+IdlInterface.prototype.assert_interface_object_exists = function()
+{
+ var owner = this.get_legacy_namespace() || "self";
+ assert_own_property(self[owner], this.name, owner + " does not have own property " + format_value(this.name));
+};
+
+IdlInterface.prototype.get_interface_object = function() {
+ if (!this.should_have_interface_object()) {
+ var reason = this.is_callback() ? "lack of declared constants" : "declared [LegacyNoInterfaceObject] attribute";
+ throw new IdlHarnessError(this.name + " has no interface object due to " + reason);
+ }
+
+ return this.get_interface_object_owner()[this.name];
+};
+
+IdlInterface.prototype.get_qualified_name = function() {
+ // https://webidl.spec.whatwg.org/#qualified-name
+ var legacyNamespace = this.get_legacy_namespace();
+ if (legacyNamespace) {
+ return legacyNamespace + "." + this.name;
+ }
+ return this.name;
+};
+
+IdlInterface.prototype.has_to_json_regular_operation = function() {
+ return this.members.some(function(m) {
+ return m.is_to_json_regular_operation();
+ });
+};
+
+IdlInterface.prototype.has_default_to_json_regular_operation = function() {
+ return this.members.some(function(m) {
+ return m.is_to_json_regular_operation() && m.has_extended_attribute("Default");
+ });
+};
+
+/**
+ * Implementation of https://webidl.spec.whatwg.org/#create-an-inheritance-stack
+ * with the order reversed.
+ *
+ * The order is reversed so that the base class comes first in the list, because
+ * this is what all call sites need.
+ *
+ * So given:
+ *
+ * A : B {};
+ * B : C {};
+ * C {};
+ *
+ * then A.get_reverse_inheritance_stack() returns [C, B, A],
+ * and B.get_reverse_inheritance_stack() returns [C, B].
+ *
+ * Note: as dictionary inheritance is expressed identically by the AST,
+ * this works just as well for getting a stack of inherited dictionaries.
+ */
+IdlInterface.prototype.get_reverse_inheritance_stack = function() {
+ const stack = [this];
+ let idl_interface = this;
+ while (idl_interface.base) {
+ const base = this.array.members[idl_interface.base];
+ if (!base) {
+ throw new Error(idl_interface.type + " " + idl_interface.base + " not found (inherited by " + idl_interface.name + ")");
+ } else if (stack.indexOf(base) > -1) {
+ stack.unshift(base);
+ const dep_chain = stack.map(i => i.name).join(',');
+ throw new IdlHarnessError(`${this.name} has a circular dependency: ${dep_chain}`);
+ }
+ idl_interface = base;
+ stack.unshift(idl_interface);
+ }
+ return stack;
+};
+
+/**
+ * Implementation of
+ * https://webidl.spec.whatwg.org/#default-tojson-operation
+ * for testing purposes.
+ *
+ * Collects the IDL types of the attributes that meet the criteria
+ * for inclusion in the default toJSON operation for easy
+ * comparison with actual value
+ */
+IdlInterface.prototype.default_to_json_operation = function() {
+ const map = new Map()
+ let isDefault = false;
+ for (const I of this.get_reverse_inheritance_stack()) {
+ if (I.has_default_to_json_regular_operation()) {
+ isDefault = true;
+ for (const m of I.members) {
+ if (m.special !== "static" && m.type == "attribute" && I.array.is_json_type(m.idlType)) {
+ map.set(m.name, m.idlType);
+ }
+ }
+ } else if (I.has_to_json_regular_operation()) {
+ isDefault = false;
+ }
+ }
+ return isDefault ? map : null;
+};
+
+IdlInterface.prototype.test = function()
+{
+ if (this.has_extended_attribute("LegacyNoInterfaceObject") || this.is_mixin())
+ {
+ // No tests to do without an instance. TODO: We should still be able
+ // to run tests on the prototype object, if we obtain one through some
+ // other means.
+ return;
+ }
+
+ // If the interface object is not exposed, only test that. Members can't be
+ // tested either, but objects could still be tested in |test_object|.
+ if (!this.exposed)
+ {
+ if (!this.untested)
+ {
+ subsetTestByKey(this.name, test, function() {
+ assert_false(this.name in self);
+ }.bind(this), this.name + " interface: existence and properties of interface object");
+ }
+ return;
+ }
+
+ if (!this.untested)
+ {
+ // First test things to do with the exception/interface object and
+ // exception/interface prototype object.
+ this.test_self();
+ }
+ // Then test things to do with its members (constants, fields, attributes,
+ // operations, . . .). These are run even if .untested is true, because
+ // members might themselves be marked as .untested. This might happen to
+ // interfaces if the interface itself is untested but a partial interface
+ // that extends it is tested -- then the interface itself and its initial
+ // members will be marked as untested, but the members added by the partial
+ // interface are still tested.
+ this.test_members();
+};
+
+IdlInterface.prototype.constructors = function()
+{
+ return this.members
+ .filter(function(m) { return m.type == "constructor"; });
+}
+
+IdlInterface.prototype.test_self = function()
+{
+ subsetTestByKey(this.name, test, function()
+ {
+ if (!this.should_have_interface_object()) {
+ return;
+ }
+
+ // The name of the property is the identifier of the interface, and its
+ // value is an object called the interface object.
+ // The property has the attributes { [[Writable]]: true,
+ // [[Enumerable]]: false, [[Configurable]]: true }."
+ // TODO: Should we test here that the property is actually writable
+ // etc., or trust getOwnPropertyDescriptor?
+ this.assert_interface_object_exists();
+ var desc = Object.getOwnPropertyDescriptor(this.get_interface_object_owner(), this.name);
+ assert_false("get" in desc, "self's property " + format_value(this.name) + " should not have a getter");
+ assert_false("set" in desc, "self's property " + format_value(this.name) + " should not have a setter");
+ assert_true(desc.writable, "self's property " + format_value(this.name) + " should be writable");
+ assert_false(desc.enumerable, "self's property " + format_value(this.name) + " should not be enumerable");
+ assert_true(desc.configurable, "self's property " + format_value(this.name) + " should be configurable");
+
+ if (this.is_callback()) {
+ // "The internal [[Prototype]] property of an interface object for
+ // a callback interface must be the Function.prototype object."
+ assert_equals(Object.getPrototypeOf(this.get_interface_object()), Function.prototype,
+ "prototype of self's property " + format_value(this.name) + " is not Object.prototype");
+
+ return;
+ }
+
+ // "The interface object for a given non-callback interface is a
+ // function object."
+ // "If an object is defined to be a function object, then it has
+ // characteristics as follows:"
+
+ // Its [[Prototype]] internal property is otherwise specified (see
+ // below).
+
+ // "* Its [[Get]] internal property is set as described in ECMA-262
+ // section 9.1.8."
+ // Not much to test for this.
+
+ // "* Its [[Construct]] internal property is set as described in
+ // ECMA-262 section 19.2.2.3."
+
+ // "* Its @@hasInstance property is set as described in ECMA-262
+ // section 19.2.3.8, unless otherwise specified."
+ // TODO
+
+ // ES6 (rev 30) 19.1.3.6:
+ // "Else, if O has a [[Call]] internal method, then let builtinTag be
+ // "Function"."
+ assert_class_string(this.get_interface_object(), "Function", "class string of " + this.name);
+
+ // "The [[Prototype]] internal property of an interface object for a
+ // non-callback interface is determined as follows:"
+ var prototype = Object.getPrototypeOf(this.get_interface_object());
+ if (this.base) {
+ // "* If the interface inherits from some other interface, the
+ // value of [[Prototype]] is the interface object for that other
+ // interface."
+ var inherited_interface = this.array.members[this.base];
+ if (!inherited_interface.has_extended_attribute("LegacyNoInterfaceObject")) {
+ inherited_interface.assert_interface_object_exists();
+ assert_equals(prototype, inherited_interface.get_interface_object(),
+ 'prototype of ' + this.name + ' is not ' +
+ this.base);
+ }
+ } else {
+ // "If the interface doesn't inherit from any other interface, the
+ // value of [[Prototype]] is %FunctionPrototype% ([ECMA-262],
+ // section 6.1.7.4)."
+ assert_equals(prototype, Function.prototype,
+ "prototype of self's property " + format_value(this.name) + " is not Function.prototype");
+ }
+
+ // Always test for [[Construct]]:
+ // https://github.com/heycam/webidl/issues/698
+ assert_true(isConstructor(this.get_interface_object()), "interface object must pass IsConstructor check");
+
+ var interface_object = this.get_interface_object();
+ assert_throws_js(globalOf(interface_object).TypeError, function() {
+ interface_object();
+ }, "interface object didn't throw TypeError when called as a function");
+
+ if (!this.constructors().length) {
+ assert_throws_js(globalOf(interface_object).TypeError, function() {
+ new interface_object();
+ }, "interface object didn't throw TypeError when called as a constructor");
+ }
+ }.bind(this), this.name + " interface: existence and properties of interface object");
+
+ if (this.should_have_interface_object() && !this.is_callback()) {
+ subsetTestByKey(this.name, test, function() {
+ // This function tests WebIDL as of 2014-10-25.
+ // https://webidl.spec.whatwg.org/#es-interface-call
+
+ this.assert_interface_object_exists();
+
+ // "Interface objects for non-callback interfaces MUST have a
+ // property named “length†with attributes { [[Writable]]: false,
+ // [[Enumerable]]: false, [[Configurable]]: true } whose value is
+ // a Number."
+ assert_own_property(this.get_interface_object(), "length");
+ var desc = Object.getOwnPropertyDescriptor(this.get_interface_object(), "length");
+ assert_false("get" in desc, this.name + ".length should not have a getter");
+ assert_false("set" in desc, this.name + ".length should not have a setter");
+ assert_false(desc.writable, this.name + ".length should not be writable");
+ assert_false(desc.enumerable, this.name + ".length should not be enumerable");
+ assert_true(desc.configurable, this.name + ".length should be configurable");
+
+ var constructors = this.constructors();
+ var expected_length = minOverloadLength(constructors);
+ assert_equals(this.get_interface_object().length, expected_length, "wrong value for " + this.name + ".length");
+ }.bind(this), this.name + " interface object length");
+ }
+
+ if (this.should_have_interface_object()) {
+ subsetTestByKey(this.name, test, function() {
+ // This function tests WebIDL as of 2015-11-17.
+ // https://webidl.spec.whatwg.org/#interface-object
+
+ this.assert_interface_object_exists();
+
+ // "All interface objects must have a property named “name†with
+ // attributes { [[Writable]]: false, [[Enumerable]]: false,
+ // [[Configurable]]: true } whose value is the identifier of the
+ // corresponding interface."
+
+ assert_own_property(this.get_interface_object(), "name");
+ var desc = Object.getOwnPropertyDescriptor(this.get_interface_object(), "name");
+ assert_false("get" in desc, this.name + ".name should not have a getter");
+ assert_false("set" in desc, this.name + ".name should not have a setter");
+ assert_false(desc.writable, this.name + ".name should not be writable");
+ assert_false(desc.enumerable, this.name + ".name should not be enumerable");
+ assert_true(desc.configurable, this.name + ".name should be configurable");
+ assert_equals(this.get_interface_object().name, this.name, "wrong value for " + this.name + ".name");
+ }.bind(this), this.name + " interface object name");
+ }
+
+
+ if (this.has_extended_attribute("LegacyWindowAlias")) {
+ subsetTestByKey(this.name, test, function()
+ {
+ var aliasAttrs = this.extAttrs.filter(function(o) { return o.name === "LegacyWindowAlias"; });
+ if (aliasAttrs.length > 1) {
+ throw new IdlHarnessError("Invalid IDL: multiple LegacyWindowAlias extended attributes on " + this.name);
+ }
+ if (this.is_callback()) {
+ throw new IdlHarnessError("Invalid IDL: LegacyWindowAlias extended attribute on non-interface " + this.name);
+ }
+ if (!(this.exposureSet === "*" || this.exposureSet.has("Window"))) {
+ throw new IdlHarnessError("Invalid IDL: LegacyWindowAlias extended attribute on " + this.name + " which is not exposed in Window");
+ }
+ // TODO: when testing of [LegacyNoInterfaceObject] interfaces is supported,
+ // check that it's not specified together with LegacyWindowAlias.
+
+ // TODO: maybe check that [LegacyWindowAlias] is not specified on a partial interface.
+
+ var rhs = aliasAttrs[0].rhs;
+ if (!rhs) {
+ throw new IdlHarnessError("Invalid IDL: LegacyWindowAlias extended attribute on " + this.name + " without identifier");
+ }
+ var aliases;
+ if (rhs.type === "identifier-list") {
+ aliases = rhs.value.map(id => id.value);
+ } else { // rhs.type === identifier
+ aliases = [ rhs.value ];
+ }
+
+ // OK now actually check the aliases...
+ var alias;
+ if (exposed_in(exposure_set(this, this.exposureSet)) && 'document' in self) {
+ for (alias of aliases) {
+ assert_true(alias in self, alias + " should exist");
+ assert_equals(self[alias], this.get_interface_object(), "self." + alias + " should be the same value as self." + this.get_qualified_name());
+ var desc = Object.getOwnPropertyDescriptor(self, alias);
+ assert_equals(desc.value, this.get_interface_object(), "wrong value in " + alias + " property descriptor");
+ assert_true(desc.writable, alias + " should be writable");
+ assert_false(desc.enumerable, alias + " should not be enumerable");
+ assert_true(desc.configurable, alias + " should be configurable");
+ assert_false('get' in desc, alias + " should not have a getter");
+ assert_false('set' in desc, alias + " should not have a setter");
+ }
+ } else {
+ for (alias of aliases) {
+ assert_false(alias in self, alias + " should not exist");
+ }
+ }
+
+ }.bind(this), this.name + " interface: legacy window alias");
+ }
+
+ if (this.has_extended_attribute("LegacyFactoryFunction")) {
+ var constructors = this.extAttrs
+ .filter(function(attr) { return attr.name == "LegacyFactoryFunction"; });
+ if (constructors.length !== 1) {
+ throw new IdlHarnessError("Internal error: missing support for multiple LegacyFactoryFunction extended attributes");
+ }
+ var constructor = constructors[0];
+ var min_length = minOverloadLength([constructor]);
+
+ subsetTestByKey(this.name, test, function()
+ {
+ // This function tests WebIDL as of 2019-01-14.
+
+ // "for every [LegacyFactoryFunction] extended attribute on an exposed
+ // interface, a corresponding property must exist on the ECMAScript
+ // global object. The name of the property is the
+ // [LegacyFactoryFunction]'s identifier, and its value is an object
+ // called a named constructor, ... . The property has the attributes
+ // { [[Writable]]: true, [[Enumerable]]: false,
+ // [[Configurable]]: true }."
+ var name = constructor.rhs.value;
+ assert_own_property(self, name);
+ var desc = Object.getOwnPropertyDescriptor(self, name);
+ assert_equals(desc.value, self[name], "wrong value in " + name + " property descriptor");
+ assert_true(desc.writable, name + " should be writable");
+ assert_false(desc.enumerable, name + " should not be enumerable");
+ assert_true(desc.configurable, name + " should be configurable");
+ assert_false("get" in desc, name + " should not have a getter");
+ assert_false("set" in desc, name + " should not have a setter");
+ }.bind(this), this.name + " interface: named constructor");
+
+ subsetTestByKey(this.name, test, function()
+ {
+ // This function tests WebIDL as of 2019-01-14.
+
+ // "2. Let F be ! CreateBuiltinFunction(realm, steps,
+ // realm.[[Intrinsics]].[[%FunctionPrototype%]])."
+ var name = constructor.rhs.value;
+ var value = self[name];
+ assert_equals(typeof value, "function", "type of value in " + name + " property descriptor");
+ assert_not_equals(value, this.get_interface_object(), "wrong value in " + name + " property descriptor");
+ assert_equals(Object.getPrototypeOf(value), Function.prototype, "wrong value for " + name + "'s prototype");
+ }.bind(this), this.name + " interface: named constructor object");
+
+ subsetTestByKey(this.name, test, function()
+ {
+ // This function tests WebIDL as of 2019-01-14.
+
+ // "7. Let proto be the interface prototype object of interface I
+ // in realm.
+ // "8. Perform ! DefinePropertyOrThrow(F, "prototype",
+ // PropertyDescriptor{
+ // [[Value]]: proto, [[Writable]]: false,
+ // [[Enumerable]]: false, [[Configurable]]: false
+ // })."
+ var name = constructor.rhs.value;
+ var expected = this.get_interface_object().prototype;
+ var desc = Object.getOwnPropertyDescriptor(self[name], "prototype");
+ assert_equals(desc.value, expected, "wrong value for " + name + ".prototype");
+ assert_false(desc.writable, "prototype should not be writable");
+ assert_false(desc.enumerable, "prototype should not be enumerable");
+ assert_false(desc.configurable, "prototype should not be configurable");
+ assert_false("get" in desc, "prototype should not have a getter");
+ assert_false("set" in desc, "prototype should not have a setter");
+ }.bind(this), this.name + " interface: named constructor prototype property");
+
+ subsetTestByKey(this.name, test, function()
+ {
+ // This function tests WebIDL as of 2019-01-14.
+
+ // "3. Perform ! SetFunctionName(F, id)."
+ var name = constructor.rhs.value;
+ var desc = Object.getOwnPropertyDescriptor(self[name], "name");
+ assert_equals(desc.value, name, "wrong value for " + name + ".name");
+ assert_false(desc.writable, "name should not be writable");
+ assert_false(desc.enumerable, "name should not be enumerable");
+ assert_true(desc.configurable, "name should be configurable");
+ assert_false("get" in desc, "name should not have a getter");
+ assert_false("set" in desc, "name should not have a setter");
+ }.bind(this), this.name + " interface: named constructor name");
+
+ subsetTestByKey(this.name, test, function()
+ {
+ // This function tests WebIDL as of 2019-01-14.
+
+ // "4. Initialize S to the effective overload set for constructors
+ // with identifier id on interface I and with argument count 0.
+ // "5. Let length be the length of the shortest argument list of
+ // the entries in S.
+ // "6. Perform ! SetFunctionLength(F, length)."
+ var name = constructor.rhs.value;
+ var desc = Object.getOwnPropertyDescriptor(self[name], "length");
+ assert_equals(desc.value, min_length, "wrong value for " + name + ".length");
+ assert_false(desc.writable, "length should not be writable");
+ assert_false(desc.enumerable, "length should not be enumerable");
+ assert_true(desc.configurable, "length should be configurable");
+ assert_false("get" in desc, "length should not have a getter");
+ assert_false("set" in desc, "length should not have a setter");
+ }.bind(this), this.name + " interface: named constructor length");
+
+ subsetTestByKey(this.name, test, function()
+ {
+ // This function tests WebIDL as of 2019-01-14.
+
+ // "1. Let steps be the following steps:
+ // " 1. If NewTarget is undefined, then throw a TypeError."
+ var name = constructor.rhs.value;
+ var args = constructor.arguments.map(function(arg) {
+ return create_suitable_object(arg.idlType);
+ });
+ assert_throws_js(globalOf(self[name]).TypeError, function() {
+ self[name](...args);
+ }.bind(this));
+ }.bind(this), this.name + " interface: named constructor without 'new'");
+ }
+
+ subsetTestByKey(this.name, test, function()
+ {
+ // This function tests WebIDL as of 2015-01-21.
+ // https://webidl.spec.whatwg.org/#interface-object
+
+ if (!this.should_have_interface_object()) {
+ return;
+ }
+
+ this.assert_interface_object_exists();
+
+ if (this.is_callback()) {
+ assert_false("prototype" in this.get_interface_object(),
+ this.name + ' should not have a "prototype" property');
+ return;
+ }
+
+ // "An interface object for a non-callback interface must have a
+ // property named “prototype†with attributes { [[Writable]]: false,
+ // [[Enumerable]]: false, [[Configurable]]: false } whose value is an
+ // object called the interface prototype object. This object has
+ // properties that correspond to the regular attributes and regular
+ // operations defined on the interface, and is described in more detail
+ // in section 4.5.4 below."
+ assert_own_property(this.get_interface_object(), "prototype",
+ 'interface "' + this.name + '" does not have own property "prototype"');
+ var desc = Object.getOwnPropertyDescriptor(this.get_interface_object(), "prototype");
+ assert_false("get" in desc, this.name + ".prototype should not have a getter");
+ assert_false("set" in desc, this.name + ".prototype should not have a setter");
+ assert_false(desc.writable, this.name + ".prototype should not be writable");
+ assert_false(desc.enumerable, this.name + ".prototype should not be enumerable");
+ assert_false(desc.configurable, this.name + ".prototype should not be configurable");
+
+ // Next, test that the [[Prototype]] of the interface prototype object
+ // is correct. (This is made somewhat difficult by the existence of
+ // [LegacyNoInterfaceObject].)
+ // TODO: Aryeh thinks there's at least other place in this file where
+ // we try to figure out if an interface prototype object is
+ // correct. Consolidate that code.
+
+ // "The interface prototype object for a given interface A must have an
+ // internal [[Prototype]] property whose value is returned from the
+ // following steps:
+ // "If A is declared with the [Global] extended
+ // attribute, and A supports named properties, then return the named
+ // properties object for A, as defined in §3.6.4 Named properties
+ // object.
+ // "Otherwise, if A is declared to inherit from another interface, then
+ // return the interface prototype object for the inherited interface.
+ // "Otherwise, return %ObjectPrototype%.
+ //
+ // "In the ECMAScript binding, the DOMException type has some additional
+ // requirements:
+ //
+ // "Unlike normal interface types, the interface prototype object
+ // for DOMException must have as its [[Prototype]] the intrinsic
+ // object %ErrorPrototype%."
+ //
+ if (this.name === "Window") {
+ assert_class_string(Object.getPrototypeOf(this.get_interface_object().prototype),
+ 'WindowProperties',
+ 'Class name for prototype of Window' +
+ '.prototype is not "WindowProperties"');
+ } else {
+ var inherit_interface, inherit_interface_interface_object;
+ if (this.base) {
+ inherit_interface = this.base;
+ var parent = this.array.members[inherit_interface];
+ if (!parent.has_extended_attribute("LegacyNoInterfaceObject")) {
+ parent.assert_interface_object_exists();
+ inherit_interface_interface_object = parent.get_interface_object();
+ }
+ } else if (this.name === "DOMException") {
+ inherit_interface = 'Error';
+ inherit_interface_interface_object = self.Error;
+ } else {
+ inherit_interface = 'Object';
+ inherit_interface_interface_object = self.Object;
+ }
+ if (inherit_interface_interface_object) {
+ assert_not_equals(inherit_interface_interface_object, undefined,
+ 'should inherit from ' + inherit_interface + ', but there is no such property');
+ assert_own_property(inherit_interface_interface_object, 'prototype',
+ 'should inherit from ' + inherit_interface + ', but that object has no "prototype" property');
+ assert_equals(Object.getPrototypeOf(this.get_interface_object().prototype),
+ inherit_interface_interface_object.prototype,
+ 'prototype of ' + this.name + '.prototype is not ' + inherit_interface + '.prototype');
+ } else {
+ // We can't test that we get the correct object, because this is the
+ // only way to get our hands on it. We only test that its class
+ // string, at least, is correct.
+ assert_class_string(Object.getPrototypeOf(this.get_interface_object().prototype),
+ inherit_interface + 'Prototype',
+ 'Class name for prototype of ' + this.name +
+ '.prototype is not "' + inherit_interface + 'Prototype"');
+ }
+ }
+
+ // "The class string of an interface prototype object is the
+ // concatenation of the interface’s qualified identifier and the string
+ // “Prototypeâ€."
+
+ // Skip these tests for now due to a specification issue about
+ // prototype name.
+ // https://www.w3.org/Bugs/Public/show_bug.cgi?id=28244
+
+ // assert_class_string(this.get_interface_object().prototype, this.get_qualified_name() + "Prototype",
+ // "class string of " + this.name + ".prototype");
+
+ // String() should end up calling {}.toString if nothing defines a
+ // stringifier.
+ if (!this.has_stringifier()) {
+ // assert_equals(String(this.get_interface_object().prototype), "[object " + this.get_qualified_name() + "Prototype]",
+ // "String(" + this.name + ".prototype)");
+ }
+ }.bind(this), this.name + " interface: existence and properties of interface prototype object");
+
+ // "If the interface is declared with the [Global]
+ // extended attribute, or the interface is in the set of inherited
+ // interfaces for any other interface that is declared with one of these
+ // attributes, then the interface prototype object must be an immutable
+ // prototype exotic object."
+ // https://webidl.spec.whatwg.org/#interface-prototype-object
+ if (this.is_global()) {
+ this.test_immutable_prototype("interface prototype object", this.get_interface_object().prototype);
+ }
+
+ subsetTestByKey(this.name, test, function()
+ {
+ if (!this.should_have_interface_object()) {
+ return;
+ }
+
+ this.assert_interface_object_exists();
+
+ if (this.is_callback()) {
+ assert_false("prototype" in this.get_interface_object(),
+ this.name + ' should not have a "prototype" property');
+ return;
+ }
+
+ assert_own_property(this.get_interface_object(), "prototype",
+ 'interface "' + this.name + '" does not have own property "prototype"');
+
+ // "If the [LegacyNoInterfaceObject] extended attribute was not specified
+ // on the interface, then the interface prototype object must also have a
+ // property named “constructor†with attributes { [[Writable]]: true,
+ // [[Enumerable]]: false, [[Configurable]]: true } whose value is a
+ // reference to the interface object for the interface."
+ assert_own_property(this.get_interface_object().prototype, "constructor",
+ this.name + '.prototype does not have own property "constructor"');
+ var desc = Object.getOwnPropertyDescriptor(this.get_interface_object().prototype, "constructor");
+ assert_false("get" in desc, this.name + ".prototype.constructor should not have a getter");
+ assert_false("set" in desc, this.name + ".prototype.constructor should not have a setter");
+ assert_true(desc.writable, this.name + ".prototype.constructor should be writable");
+ assert_false(desc.enumerable, this.name + ".prototype.constructor should not be enumerable");
+ assert_true(desc.configurable, this.name + ".prototype.constructor should be configurable");
+ assert_equals(this.get_interface_object().prototype.constructor, this.get_interface_object(),
+ this.name + '.prototype.constructor is not the same object as ' + this.name);
+ }.bind(this), this.name + ' interface: existence and properties of interface prototype object\'s "constructor" property');
+
+
+ subsetTestByKey(this.name, test, function()
+ {
+ if (!this.should_have_interface_object()) {
+ return;
+ }
+
+ this.assert_interface_object_exists();
+
+ if (this.is_callback()) {
+ assert_false("prototype" in this.get_interface_object(),
+ this.name + ' should not have a "prototype" property');
+ return;
+ }
+
+ assert_own_property(this.get_interface_object(), "prototype",
+ 'interface "' + this.name + '" does not have own property "prototype"');
+
+ // If the interface has any member declared with the [Unscopable] extended
+ // attribute, then there must be a property on the interface prototype object
+ // whose name is the @@unscopables symbol, which has the attributes
+ // { [[Writable]]: false, [[Enumerable]]: false, [[Configurable]]: true },
+ // and whose value is an object created as follows...
+ var unscopables = this.get_unscopables().map(m => m.name);
+ var proto = this.get_interface_object().prototype;
+ if (unscopables.length != 0) {
+ assert_own_property(
+ proto, Symbol.unscopables,
+ this.name + '.prototype should have an @@unscopables property');
+ var desc = Object.getOwnPropertyDescriptor(proto, Symbol.unscopables);
+ assert_false("get" in desc,
+ this.name + ".prototype[Symbol.unscopables] should not have a getter");
+ assert_false("set" in desc, this.name + ".prototype[Symbol.unscopables] should not have a setter");
+ assert_false(desc.writable, this.name + ".prototype[Symbol.unscopables] should not be writable");
+ assert_false(desc.enumerable, this.name + ".prototype[Symbol.unscopables] should not be enumerable");
+ assert_true(desc.configurable, this.name + ".prototype[Symbol.unscopables] should be configurable");
+ assert_equals(desc.value, proto[Symbol.unscopables],
+ this.name + '.prototype[Symbol.unscopables] should be in the descriptor');
+ assert_equals(typeof desc.value, "object",
+ this.name + '.prototype[Symbol.unscopables] should be an object');
+ assert_equals(Object.getPrototypeOf(desc.value), null,
+ this.name + '.prototype[Symbol.unscopables] should have a null prototype');
+ assert_equals(Object.getOwnPropertySymbols(desc.value).length,
+ 0,
+ this.name + '.prototype[Symbol.unscopables] should have the right number of symbol-named properties');
+
+ // Check that we do not have _extra_ unscopables. Checking that we
+ // have all the ones we should will happen in the per-member tests.
+ var observed = Object.getOwnPropertyNames(desc.value);
+ for (var prop of observed) {
+ assert_not_equals(unscopables.indexOf(prop),
+ -1,
+ this.name + '.prototype[Symbol.unscopables] has unexpected property "' + prop + '"');
+ }
+ } else {
+ assert_equals(Object.getOwnPropertyDescriptor(this.get_interface_object().prototype, Symbol.unscopables),
+ undefined,
+ this.name + '.prototype should not have @@unscopables');
+ }
+ }.bind(this), this.name + ' interface: existence and properties of interface prototype object\'s @@unscopables property');
+};
+
+IdlInterface.prototype.test_immutable_prototype = function(type, obj)
+{
+ if (typeof Object.setPrototypeOf !== "function") {
+ return;
+ }
+
+ subsetTestByKey(this.name, test, function(t) {
+ var originalValue = Object.getPrototypeOf(obj);
+ var newValue = Object.create(null);
+
+ t.add_cleanup(function() {
+ try {
+ Object.setPrototypeOf(obj, originalValue);
+ } catch (err) {}
+ });
+
+ assert_throws_js(TypeError, function() {
+ Object.setPrototypeOf(obj, newValue);
+ });
+
+ assert_equals(
+ Object.getPrototypeOf(obj),
+ originalValue,
+ "original value not modified"
+ );
+ }.bind(this), this.name + " interface: internal [[SetPrototypeOf]] method " +
+ "of " + type + " - setting to a new value via Object.setPrototypeOf " +
+ "should throw a TypeError");
+
+ subsetTestByKey(this.name, test, function(t) {
+ var originalValue = Object.getPrototypeOf(obj);
+ var newValue = Object.create(null);
+
+ t.add_cleanup(function() {
+ let setter = Object.getOwnPropertyDescriptor(
+ Object.prototype, '__proto__'
+ ).set;
+
+ try {
+ setter.call(obj, originalValue);
+ } catch (err) {}
+ });
+
+ // We need to find the actual setter for the '__proto__' property, so we
+ // can determine the right global for it. Walk up the prototype chain
+ // looking for that property until we find it.
+ let setter;
+ {
+ let cur = obj;
+ while (cur) {
+ const desc = Object.getOwnPropertyDescriptor(cur, "__proto__");
+ if (desc) {
+ setter = desc.set;
+ break;
+ }
+ cur = Object.getPrototypeOf(cur);
+ }
+ }
+ assert_throws_js(globalOf(setter).TypeError, function() {
+ obj.__proto__ = newValue;
+ });
+
+ assert_equals(
+ Object.getPrototypeOf(obj),
+ originalValue,
+ "original value not modified"
+ );
+ }.bind(this), this.name + " interface: internal [[SetPrototypeOf]] method " +
+ "of " + type + " - setting to a new value via __proto__ " +
+ "should throw a TypeError");
+
+ subsetTestByKey(this.name, test, function(t) {
+ var originalValue = Object.getPrototypeOf(obj);
+ var newValue = Object.create(null);
+
+ t.add_cleanup(function() {
+ try {
+ Reflect.setPrototypeOf(obj, originalValue);
+ } catch (err) {}
+ });
+
+ assert_false(Reflect.setPrototypeOf(obj, newValue));
+
+ assert_equals(
+ Object.getPrototypeOf(obj),
+ originalValue,
+ "original value not modified"
+ );
+ }.bind(this), this.name + " interface: internal [[SetPrototypeOf]] method " +
+ "of " + type + " - setting to a new value via Reflect.setPrototypeOf " +
+ "should return false");
+
+ subsetTestByKey(this.name, test, function() {
+ var originalValue = Object.getPrototypeOf(obj);
+
+ Object.setPrototypeOf(obj, originalValue);
+ }.bind(this), this.name + " interface: internal [[SetPrototypeOf]] method " +
+ "of " + type + " - setting to its original value via Object.setPrototypeOf " +
+ "should not throw");
+
+ subsetTestByKey(this.name, test, function() {
+ var originalValue = Object.getPrototypeOf(obj);
+
+ obj.__proto__ = originalValue;
+ }.bind(this), this.name + " interface: internal [[SetPrototypeOf]] method " +
+ "of " + type + " - setting to its original value via __proto__ " +
+ "should not throw");
+
+ subsetTestByKey(this.name, test, function() {
+ var originalValue = Object.getPrototypeOf(obj);
+
+ assert_true(Reflect.setPrototypeOf(obj, originalValue));
+ }.bind(this), this.name + " interface: internal [[SetPrototypeOf]] method " +
+ "of " + type + " - setting to its original value via Reflect.setPrototypeOf " +
+ "should return true");
+};
+
+IdlInterface.prototype.test_member_const = function(member)
+{
+ if (!this.has_constants()) {
+ throw new IdlHarnessError("Internal error: test_member_const called without any constants");
+ }
+
+ subsetTestByKey(this.name, test, function()
+ {
+ this.assert_interface_object_exists();
+
+ // "For each constant defined on an interface A, there must be
+ // a corresponding property on the interface object, if it
+ // exists."
+ assert_own_property(this.get_interface_object(), member.name);
+ // "The value of the property is that which is obtained by
+ // converting the constant’s IDL value to an ECMAScript
+ // value."
+ assert_equals(this.get_interface_object()[member.name], constValue(member.value),
+ "property has wrong value");
+ // "The property has attributes { [[Writable]]: false,
+ // [[Enumerable]]: true, [[Configurable]]: false }."
+ var desc = Object.getOwnPropertyDescriptor(this.get_interface_object(), member.name);
+ assert_false("get" in desc, "property should not have a getter");
+ assert_false("set" in desc, "property should not have a setter");
+ assert_false(desc.writable, "property should not be writable");
+ assert_true(desc.enumerable, "property should be enumerable");
+ assert_false(desc.configurable, "property should not be configurable");
+ }.bind(this), this.name + " interface: constant " + member.name + " on interface object");
+
+ // "In addition, a property with the same characteristics must
+ // exist on the interface prototype object."
+ subsetTestByKey(this.name, test, function()
+ {
+ this.assert_interface_object_exists();
+
+ if (this.is_callback()) {
+ assert_false("prototype" in this.get_interface_object(),
+ this.name + ' should not have a "prototype" property');
+ return;
+ }
+
+ assert_own_property(this.get_interface_object(), "prototype",
+ 'interface "' + this.name + '" does not have own property "prototype"');
+
+ assert_own_property(this.get_interface_object().prototype, member.name);
+ assert_equals(this.get_interface_object().prototype[member.name], constValue(member.value),
+ "property has wrong value");
+ var desc = Object.getOwnPropertyDescriptor(this.get_interface_object(), member.name);
+ assert_false("get" in desc, "property should not have a getter");
+ assert_false("set" in desc, "property should not have a setter");
+ assert_false(desc.writable, "property should not be writable");
+ assert_true(desc.enumerable, "property should be enumerable");
+ assert_false(desc.configurable, "property should not be configurable");
+ }.bind(this), this.name + " interface: constant " + member.name + " on interface prototype object");
+};
+
+
+IdlInterface.prototype.test_member_attribute = function(member)
+ {
+ if (!shouldRunSubTest(this.name)) {
+ return;
+ }
+ var a_test = subsetTestByKey(this.name, async_test, this.name + " interface: attribute " + member.name);
+ a_test.step(function()
+ {
+ if (!this.should_have_interface_object()) {
+ a_test.done();
+ return;
+ }
+
+ this.assert_interface_object_exists();
+ assert_own_property(this.get_interface_object(), "prototype",
+ 'interface "' + this.name + '" does not have own property "prototype"');
+
+ if (member.special === "static") {
+ assert_own_property(this.get_interface_object(), member.name,
+ "The interface object must have a property " +
+ format_value(member.name));
+ a_test.done();
+ return;
+ }
+
+ this.do_member_unscopable_asserts(member);
+
+ if (this.is_global()) {
+ assert_own_property(self, member.name,
+ "The global object must have a property " +
+ format_value(member.name));
+ assert_false(member.name in this.get_interface_object().prototype,
+ "The prototype object should not have a property " +
+ format_value(member.name));
+
+ var getter = Object.getOwnPropertyDescriptor(self, member.name).get;
+ assert_equals(typeof(getter), "function",
+ format_value(member.name) + " must have a getter");
+
+ // Try/catch around the get here, since it can legitimately throw.
+ // If it does, we obviously can't check for equality with direct
+ // invocation of the getter.
+ var gotValue;
+ var propVal;
+ try {
+ propVal = self[member.name];
+ gotValue = true;
+ } catch (e) {
+ gotValue = false;
+ }
+ if (gotValue) {
+ assert_equals(propVal, getter.call(undefined),
+ "Gets on a global should not require an explicit this");
+ }
+
+ // do_interface_attribute_asserts must be the last thing we do,
+ // since it will call done() on a_test.
+ this.do_interface_attribute_asserts(self, member, a_test);
+ } else {
+ assert_true(member.name in this.get_interface_object().prototype,
+ "The prototype object must have a property " +
+ format_value(member.name));
+
+ if (!member.has_extended_attribute("LegacyLenientThis")) {
+ if (member.idlType.generic !== "Promise") {
+ // this.get_interface_object() returns a thing in our global
+ assert_throws_js(TypeError, function() {
+ this.get_interface_object().prototype[member.name];
+ }.bind(this), "getting property on prototype object must throw TypeError");
+ // do_interface_attribute_asserts must be the last thing we
+ // do, since it will call done() on a_test.
+ this.do_interface_attribute_asserts(this.get_interface_object().prototype, member, a_test);
+ } else {
+ promise_rejects_js(a_test, TypeError,
+ this.get_interface_object().prototype[member.name])
+ .then(a_test.step_func(function() {
+ // do_interface_attribute_asserts must be the last
+ // thing we do, since it will call done() on a_test.
+ this.do_interface_attribute_asserts(this.get_interface_object().prototype,
+ member, a_test);
+ }.bind(this)));
+ }
+ } else {
+ assert_equals(this.get_interface_object().prototype[member.name], undefined,
+ "getting property on prototype object must return undefined");
+ // do_interface_attribute_asserts must be the last thing we do,
+ // since it will call done() on a_test.
+ this.do_interface_attribute_asserts(this.get_interface_object().prototype, member, a_test);
+ }
+ }
+ }.bind(this));
+};
+
+IdlInterface.prototype.test_member_operation = function(member)
+{
+ if (!shouldRunSubTest(this.name)) {
+ return;
+ }
+ var a_test = subsetTestByKey(this.name, async_test, this.name + " interface: operation " + member);
+ a_test.step(function()
+ {
+ // This function tests WebIDL as of 2015-12-29.
+ // https://webidl.spec.whatwg.org/#es-operations
+
+ if (!this.should_have_interface_object()) {
+ a_test.done();
+ return;
+ }
+
+ this.assert_interface_object_exists();
+
+ if (this.is_callback()) {
+ assert_false("prototype" in this.get_interface_object(),
+ this.name + ' should not have a "prototype" property');
+ a_test.done();
+ return;
+ }
+
+ assert_own_property(this.get_interface_object(), "prototype",
+ 'interface "' + this.name + '" does not have own property "prototype"');
+
+ // "For each unique identifier of an exposed operation defined on the
+ // interface, there must exist a corresponding property, unless the
+ // effective overload set for that identifier and operation and with an
+ // argument count of 0 has no entries."
+
+ // TODO: Consider [Exposed].
+
+ // "The location of the property is determined as follows:"
+ var memberHolderObject;
+ // "* If the operation is static, then the property exists on the
+ // interface object."
+ if (member.special === "static") {
+ assert_own_property(this.get_interface_object(), member.name,
+ "interface object missing static operation");
+ memberHolderObject = this.get_interface_object();
+ // "* Otherwise, [...] if the interface was declared with the [Global]
+ // extended attribute, then the property exists
+ // on every object that implements the interface."
+ } else if (this.is_global()) {
+ assert_own_property(self, member.name,
+ "global object missing non-static operation");
+ memberHolderObject = self;
+ // "* Otherwise, the property exists solely on the interface’s
+ // interface prototype object."
+ } else {
+ assert_own_property(this.get_interface_object().prototype, member.name,
+ "interface prototype object missing non-static operation");
+ memberHolderObject = this.get_interface_object().prototype;
+ }
+ this.do_member_unscopable_asserts(member);
+ this.do_member_operation_asserts(memberHolderObject, member, a_test);
+ }.bind(this));
+};
+
+IdlInterface.prototype.do_member_unscopable_asserts = function(member)
+{
+ // Check that if the member is unscopable then it's in the
+ // @@unscopables object properly.
+ if (!member.isUnscopable) {
+ return;
+ }
+
+ var unscopables = this.get_interface_object().prototype[Symbol.unscopables];
+ var prop = member.name;
+ var propDesc = Object.getOwnPropertyDescriptor(unscopables, prop);
+ assert_equals(typeof propDesc, "object",
+ this.name + '.prototype[Symbol.unscopables].' + prop + ' must exist')
+ assert_false("get" in propDesc,
+ this.name + '.prototype[Symbol.unscopables].' + prop + ' must have no getter');
+ assert_false("set" in propDesc,
+ this.name + '.prototype[Symbol.unscopables].' + prop + ' must have no setter');
+ assert_true(propDesc.writable,
+ this.name + '.prototype[Symbol.unscopables].' + prop + ' must be writable');
+ assert_true(propDesc.enumerable,
+ this.name + '.prototype[Symbol.unscopables].' + prop + ' must be enumerable');
+ assert_true(propDesc.configurable,
+ this.name + '.prototype[Symbol.unscopables].' + prop + ' must be configurable');
+ assert_equals(propDesc.value, true,
+ this.name + '.prototype[Symbol.unscopables].' + prop + ' must have the value `true`');
+};
+
+IdlInterface.prototype.do_member_operation_asserts = function(memberHolderObject, member, a_test)
+{
+ var done = a_test.done.bind(a_test);
+ var operationUnforgeable = member.isUnforgeable;
+ var desc = Object.getOwnPropertyDescriptor(memberHolderObject, member.name);
+ // "The property has attributes { [[Writable]]: B,
+ // [[Enumerable]]: true, [[Configurable]]: B }, where B is false if the
+ // operation is unforgeable on the interface, and true otherwise".
+ assert_false("get" in desc, "property should not have a getter");
+ assert_false("set" in desc, "property should not have a setter");
+ assert_equals(desc.writable, !operationUnforgeable,
+ "property should be writable if and only if not unforgeable");
+ assert_true(desc.enumerable, "property should be enumerable");
+ assert_equals(desc.configurable, !operationUnforgeable,
+ "property should be configurable if and only if not unforgeable");
+ // "The value of the property is a Function object whose
+ // behavior is as follows . . ."
+ assert_equals(typeof memberHolderObject[member.name], "function",
+ "property must be a function");
+
+ const operationOverloads = this.members.filter(function(m) {
+ return m.type == "operation" && m.name == member.name &&
+ (m.special === "static") === (member.special === "static");
+ });
+ assert_equals(
+ memberHolderObject[member.name].length,
+ minOverloadLength(operationOverloads),
+ "property has wrong .length");
+ assert_equals(
+ memberHolderObject[member.name].name,
+ member.name,
+ "property has wrong .name");
+
+ // Make some suitable arguments
+ var args = member.arguments.map(function(arg) {
+ return create_suitable_object(arg.idlType);
+ });
+
+ // "Let O be a value determined as follows:
+ // ". . .
+ // "Otherwise, throw a TypeError."
+ // This should be hit if the operation is not static, there is
+ // no [ImplicitThis] attribute, and the this value is null.
+ //
+ // TODO: We currently ignore the [ImplicitThis] case. Except we manually
+ // check for globals, since otherwise we'll invoke window.close(). And we
+ // have to skip this test for anything that on the proto chain of "self",
+ // since that does in fact have implicit-this behavior.
+ if (member.special !== "static") {
+ var cb;
+ if (!this.is_global() &&
+ memberHolderObject[member.name] != self[member.name])
+ {
+ cb = awaitNCallbacks(2, done);
+ throwOrReject(a_test, member, memberHolderObject[member.name], null, args,
+ "calling operation with this = null didn't throw TypeError", cb);
+ } else {
+ cb = awaitNCallbacks(1, done);
+ }
+
+ // ". . . If O is not null and is also not a platform object
+ // that implements interface I, throw a TypeError."
+ //
+ // TODO: Test a platform object that implements some other
+ // interface. (Have to be sure to get inheritance right.)
+ throwOrReject(a_test, member, memberHolderObject[member.name], {}, args,
+ "calling operation with this = {} didn't throw TypeError", cb);
+ } else {
+ done();
+ }
+}
+
+IdlInterface.prototype.test_to_json_operation = function(desc, memberHolderObject, member) {
+ var instanceName = memberHolderObject && memberHolderObject.constructor.name
+ || member.name + " object";
+ if (member.has_extended_attribute("Default")) {
+ subsetTestByKey(this.name, test, function() {
+ var map = this.default_to_json_operation();
+ var json = memberHolderObject.toJSON();
+ map.forEach(function(type, k) {
+ assert_true(k in json, "property " + JSON.stringify(k) + " should be present in the output of " + this.name + ".prototype.toJSON()");
+ var descriptor = Object.getOwnPropertyDescriptor(json, k);
+ assert_true(descriptor.writable, "property " + k + " should be writable");
+ assert_true(descriptor.configurable, "property " + k + " should be configurable");
+ assert_true(descriptor.enumerable, "property " + k + " should be enumerable");
+ this.array.assert_type_is(json[k], type);
+ delete json[k];
+ }, this);
+ }.bind(this), this.name + " interface: default toJSON operation on " + desc);
+ } else {
+ subsetTestByKey(this.name, test, function() {
+ assert_true(this.array.is_json_type(member.idlType), JSON.stringify(member.idlType) + " is not an appropriate return value for the toJSON operation of " + instanceName);
+ this.array.assert_type_is(memberHolderObject.toJSON(), member.idlType);
+ }.bind(this), this.name + " interface: toJSON operation on " + desc);
+ }
+};
+
+IdlInterface.prototype.test_member_maplike = function(member) {
+ subsetTestByKey(this.name, test, () => {
+ const proto = this.get_interface_object().prototype;
+
+ const methods = [
+ ["entries", 0],
+ ["keys", 0],
+ ["values", 0],
+ ["forEach", 1],
+ ["get", 1],
+ ["has", 1]
+ ];
+ if (!member.readonly) {
+ methods.push(
+ ["set", 2],
+ ["delete", 1],
+ ["clear", 0]
+ );
+ }
+
+ for (const [name, length] of methods) {
+ const desc = Object.getOwnPropertyDescriptor(proto, name);
+ assert_equals(typeof desc.value, "function", `${name} should be a function`);
+ assert_equals(desc.enumerable, true, `${name} enumerable`);
+ assert_equals(desc.configurable, true, `${name} configurable`);
+ assert_equals(desc.writable, true, `${name} writable`);
+ assert_equals(desc.value.length, length, `${name} function object length should be ${length}`);
+ assert_equals(desc.value.name, name, `${name} function object should have the right name`);
+ }
+
+ const iteratorDesc = Object.getOwnPropertyDescriptor(proto, Symbol.iterator);
+ assert_equals(iteratorDesc.value, proto.entries, `@@iterator should equal entries`);
+ assert_equals(iteratorDesc.enumerable, false, `@@iterator enumerable`);
+ assert_equals(iteratorDesc.configurable, true, `@@iterator configurable`);
+ assert_equals(iteratorDesc.writable, true, `@@iterator writable`);
+
+ const sizeDesc = Object.getOwnPropertyDescriptor(proto, "size");
+ assert_equals(typeof sizeDesc.get, "function", `size getter should be a function`);
+ assert_equals(sizeDesc.set, undefined, `size should not have a setter`);
+ assert_equals(sizeDesc.enumerable, true, `size enumerable`);
+ assert_equals(sizeDesc.configurable, true, `size configurable`);
+ assert_equals(sizeDesc.get.length, 0, `size getter length`);
+ assert_equals(sizeDesc.get.name, "get size", `size getter name`);
+ }, `${this.name} interface: maplike<${member.idlType.map(t => t.idlType).join(", ")}>`);
+};
+
+IdlInterface.prototype.test_member_setlike = function(member) {
+ subsetTestByKey(this.name, test, () => {
+ const proto = this.get_interface_object().prototype;
+
+ const methods = [
+ ["entries", 0],
+ ["keys", 0],
+ ["values", 0],
+ ["forEach", 1],
+ ["has", 1]
+ ];
+ if (!member.readonly) {
+ methods.push(
+ ["add", 1],
+ ["delete", 1],
+ ["clear", 0]
+ );
+ }
+
+ for (const [name, length] of methods) {
+ const desc = Object.getOwnPropertyDescriptor(proto, name);
+ assert_equals(typeof desc.value, "function", `${name} should be a function`);
+ assert_equals(desc.enumerable, true, `${name} enumerable`);
+ assert_equals(desc.configurable, true, `${name} configurable`);
+ assert_equals(desc.writable, true, `${name} writable`);
+ assert_equals(desc.value.length, length, `${name} function object length should be ${length}`);
+ assert_equals(desc.value.name, name, `${name} function object should have the right name`);
+ }
+
+ const iteratorDesc = Object.getOwnPropertyDescriptor(proto, Symbol.iterator);
+ assert_equals(iteratorDesc.value, proto.values, `@@iterator should equal values`);
+ assert_equals(iteratorDesc.enumerable, false, `@@iterator enumerable`);
+ assert_equals(iteratorDesc.configurable, true, `@@iterator configurable`);
+ assert_equals(iteratorDesc.writable, true, `@@iterator writable`);
+
+ const sizeDesc = Object.getOwnPropertyDescriptor(proto, "size");
+ assert_equals(typeof sizeDesc.get, "function", `size getter should be a function`);
+ assert_equals(sizeDesc.set, undefined, `size should not have a setter`);
+ assert_equals(sizeDesc.enumerable, true, `size enumerable`);
+ assert_equals(sizeDesc.configurable, true, `size configurable`);
+ assert_equals(sizeDesc.get.length, 0, `size getter length`);
+ assert_equals(sizeDesc.get.name, "get size", `size getter name`);
+ }, `${this.name} interface: setlike<${member.idlType.map(t => t.idlType).join(", ")}>`);
+};
+
+IdlInterface.prototype.test_member_iterable = function(member) {
+ subsetTestByKey(this.name, test, () => {
+ const isPairIterator = member.idlType.length === 2;
+ const proto = this.get_interface_object().prototype;
+
+ const methods = [
+ ["entries", 0],
+ ["keys", 0],
+ ["values", 0],
+ ["forEach", 1]
+ ];
+
+ for (const [name, length] of methods) {
+ const desc = Object.getOwnPropertyDescriptor(proto, name);
+ assert_equals(typeof desc.value, "function", `${name} should be a function`);
+ assert_equals(desc.enumerable, true, `${name} enumerable`);
+ assert_equals(desc.configurable, true, `${name} configurable`);
+ assert_equals(desc.writable, true, `${name} writable`);
+ assert_equals(desc.value.length, length, `${name} function object length should be ${length}`);
+ assert_equals(desc.value.name, name, `${name} function object should have the right name`);
+
+ if (!isPairIterator) {
+ assert_equals(desc.value, Array.prototype[name], `${name} equality with Array.prototype version`);
+ }
+ }
+
+ const iteratorDesc = Object.getOwnPropertyDescriptor(proto, Symbol.iterator);
+ assert_equals(iteratorDesc.enumerable, false, `@@iterator enumerable`);
+ assert_equals(iteratorDesc.configurable, true, `@@iterator configurable`);
+ assert_equals(iteratorDesc.writable, true, `@@iterator writable`);
+
+ if (isPairIterator) {
+ assert_equals(iteratorDesc.value, proto.entries, `@@iterator equality with entries`);
+ } else {
+ assert_equals(iteratorDesc.value, Array.prototype[Symbol.iterator], `@@iterator equality with Array.prototype version`);
+ }
+ }, `${this.name} interface: iterable<${member.idlType.map(t => t.idlType).join(", ")}>`);
+};
+
+IdlInterface.prototype.test_member_async_iterable = function(member) {
+ subsetTestByKey(this.name, test, () => {
+ const isPairIterator = member.idlType.length === 2;
+ const proto = this.get_interface_object().prototype;
+
+ // Note that although the spec allows arguments, which will be passed to the @@asyncIterator
+ // method (which is either values or entries), those arguments must always be optional. So
+ // length of 0 is still correct for values and entries.
+ const methods = [
+ ["values", 0],
+ ];
+
+ if (isPairIterator) {
+ methods.push(
+ ["entries", 0],
+ ["keys", 0]
+ );
+ }
+
+ for (const [name, length] of methods) {
+ const desc = Object.getOwnPropertyDescriptor(proto, name);
+ assert_equals(typeof desc.value, "function", `${name} should be a function`);
+ assert_equals(desc.enumerable, true, `${name} enumerable`);
+ assert_equals(desc.configurable, true, `${name} configurable`);
+ assert_equals(desc.writable, true, `${name} writable`);
+ assert_equals(desc.value.length, length, `${name} function object length should be ${length}`);
+ assert_equals(desc.value.name, name, `${name} function object should have the right name`);
+ }
+
+ const iteratorDesc = Object.getOwnPropertyDescriptor(proto, Symbol.asyncIterator);
+ assert_equals(iteratorDesc.enumerable, false, `@@iterator enumerable`);
+ assert_equals(iteratorDesc.configurable, true, `@@iterator configurable`);
+ assert_equals(iteratorDesc.writable, true, `@@iterator writable`);
+
+ if (isPairIterator) {
+ assert_equals(iteratorDesc.value, proto.entries, `@@iterator equality with entries`);
+ } else {
+ assert_equals(iteratorDesc.value, proto.values, `@@iterator equality with values`);
+ }
+ }, `${this.name} interface: async iterable<${member.idlType.map(t => t.idlType).join(", ")}>`);
+};
+
+IdlInterface.prototype.test_member_stringifier = function(member)
+{
+ subsetTestByKey(this.name, test, function()
+ {
+ if (!this.should_have_interface_object()) {
+ return;
+ }
+
+ this.assert_interface_object_exists();
+
+ if (this.is_callback()) {
+ assert_false("prototype" in this.get_interface_object(),
+ this.name + ' should not have a "prototype" property');
+ return;
+ }
+
+ assert_own_property(this.get_interface_object(), "prototype",
+ 'interface "' + this.name + '" does not have own property "prototype"');
+
+ // ". . . the property exists on the interface prototype object."
+ var interfacePrototypeObject = this.get_interface_object().prototype;
+ assert_own_property(interfacePrototypeObject, "toString",
+ "interface prototype object missing non-static operation");
+
+ var stringifierUnforgeable = member.isUnforgeable;
+ var desc = Object.getOwnPropertyDescriptor(interfacePrototypeObject, "toString");
+ // "The property has attributes { [[Writable]]: B,
+ // [[Enumerable]]: true, [[Configurable]]: B }, where B is false if the
+ // stringifier is unforgeable on the interface, and true otherwise."
+ assert_false("get" in desc, "property should not have a getter");
+ assert_false("set" in desc, "property should not have a setter");
+ assert_equals(desc.writable, !stringifierUnforgeable,
+ "property should be writable if and only if not unforgeable");
+ assert_true(desc.enumerable, "property should be enumerable");
+ assert_equals(desc.configurable, !stringifierUnforgeable,
+ "property should be configurable if and only if not unforgeable");
+ // "The value of the property is a Function object, which behaves as
+ // follows . . ."
+ assert_equals(typeof interfacePrototypeObject.toString, "function",
+ "property must be a function");
+ // "The value of the Function object’s “length†property is the Number
+ // value 0."
+ assert_equals(interfacePrototypeObject.toString.length, 0,
+ "property has wrong .length");
+
+ // "Let O be the result of calling ToObject on the this value."
+ assert_throws_js(globalOf(interfacePrototypeObject.toString).TypeError, function() {
+ interfacePrototypeObject.toString.apply(null, []);
+ }, "calling stringifier with this = null didn't throw TypeError");
+
+ // "If O is not an object that implements the interface on which the
+ // stringifier was declared, then throw a TypeError."
+ //
+ // TODO: Test a platform object that implements some other
+ // interface. (Have to be sure to get inheritance right.)
+ assert_throws_js(globalOf(interfacePrototypeObject.toString).TypeError, function() {
+ interfacePrototypeObject.toString.apply({}, []);
+ }, "calling stringifier with this = {} didn't throw TypeError");
+ }.bind(this), this.name + " interface: stringifier");
+};
+
+IdlInterface.prototype.test_members = function()
+{
+ var unexposed_members = new Set();
+ for (var i = 0; i < this.members.length; i++)
+ {
+ var member = this.members[i];
+ if (member.untested) {
+ continue;
+ }
+
+ if (!exposed_in(exposure_set(member, this.exposureSet))) {
+ if (!unexposed_members.has(member.name)) {
+ unexposed_members.add(member.name);
+ subsetTestByKey(this.name, test, function() {
+ // It's not exposed, so we shouldn't find it anywhere.
+ assert_false(member.name in this.get_interface_object(),
+ "The interface object must not have a property " +
+ format_value(member.name));
+ assert_false(member.name in this.get_interface_object().prototype,
+ "The prototype object must not have a property " +
+ format_value(member.name));
+ }.bind(this), this.name + " interface: member " + member.name);
+ }
+ continue;
+ }
+
+ switch (member.type) {
+ case "const":
+ this.test_member_const(member);
+ break;
+
+ case "attribute":
+ // For unforgeable attributes, we do the checks in
+ // test_interface_of instead.
+ if (!member.isUnforgeable)
+ {
+ this.test_member_attribute(member);
+ }
+ if (member.special === "stringifier") {
+ this.test_member_stringifier(member);
+ }
+ break;
+
+ case "operation":
+ // TODO: Need to correctly handle multiple operations with the same
+ // identifier.
+ // For unforgeable operations, we do the checks in
+ // test_interface_of instead.
+ if (member.name) {
+ if (!member.isUnforgeable)
+ {
+ this.test_member_operation(member);
+ }
+ } else if (member.special === "stringifier") {
+ this.test_member_stringifier(member);
+ }
+ break;
+
+ case "iterable":
+ if (member.async) {
+ this.test_member_async_iterable(member);
+ } else {
+ this.test_member_iterable(member);
+ }
+ break;
+ case "maplike":
+ this.test_member_maplike(member);
+ break;
+ case "setlike":
+ this.test_member_setlike(member);
+ break;
+ default:
+ // TODO: check more member types.
+ break;
+ }
+ }
+};
+
+IdlInterface.prototype.test_object = function(desc)
+{
+ var obj, exception = null;
+ try
+ {
+ obj = eval(desc);
+ }
+ catch(e)
+ {
+ exception = e;
+ }
+
+ var expected_typeof;
+ if (this.name == "HTMLAllCollection")
+ {
+ // Result of [[IsHTMLDDA]] slot
+ expected_typeof = "undefined";
+ }
+ else
+ {
+ expected_typeof = "object";
+ }
+
+ if (this.is_callback()) {
+ assert_equals(exception, null, "Unexpected exception when evaluating object");
+ assert_equals(typeof obj, expected_typeof, "wrong typeof object");
+ } else {
+ this.test_primary_interface_of(desc, obj, exception, expected_typeof);
+
+ var current_interface = this;
+ while (current_interface)
+ {
+ if (!(current_interface.name in this.array.members))
+ {
+ throw new IdlHarnessError("Interface " + current_interface.name + " not found (inherited by " + this.name + ")");
+ }
+ if (current_interface.prevent_multiple_testing && current_interface.already_tested)
+ {
+ return;
+ }
+ current_interface.test_interface_of(desc, obj, exception, expected_typeof);
+ current_interface = this.array.members[current_interface.base];
+ }
+ }
+};
+
+IdlInterface.prototype.test_primary_interface_of = function(desc, obj, exception, expected_typeof)
+{
+ // Only the object itself, not its members, are tested here, so if the
+ // interface is untested, there is nothing to do.
+ if (this.untested)
+ {
+ return;
+ }
+
+ // "The internal [[SetPrototypeOf]] method of every platform object that
+ // implements an interface with the [Global] extended
+ // attribute must execute the same algorithm as is defined for the
+ // [[SetPrototypeOf]] internal method of an immutable prototype exotic
+ // object."
+ // https://webidl.spec.whatwg.org/#platform-object-setprototypeof
+ if (this.is_global())
+ {
+ this.test_immutable_prototype("global platform object", obj);
+ }
+
+
+ // We can't easily test that its prototype is correct if there's no
+ // interface object, or the object is from a different global environment
+ // (not instanceof Object). TODO: test in this case that its prototype at
+ // least looks correct, even if we can't test that it's actually correct.
+ if (this.should_have_interface_object()
+ && (typeof obj != expected_typeof || obj instanceof Object))
+ {
+ subsetTestByKey(this.name, test, function()
+ {
+ assert_equals(exception, null, "Unexpected exception when evaluating object");
+ assert_equals(typeof obj, expected_typeof, "wrong typeof object");
+ this.assert_interface_object_exists();
+ assert_own_property(this.get_interface_object(), "prototype",
+ 'interface "' + this.name + '" does not have own property "prototype"');
+
+ // "The value of the internal [[Prototype]] property of the
+ // platform object is the interface prototype object of the primary
+ // interface from the platform object’s associated global
+ // environment."
+ assert_equals(Object.getPrototypeOf(obj),
+ this.get_interface_object().prototype,
+ desc + "'s prototype is not " + this.name + ".prototype");
+ }.bind(this), this.name + " must be primary interface of " + desc);
+ }
+
+ // "The class string of a platform object that implements one or more
+ // interfaces must be the qualified name of the primary interface of the
+ // platform object."
+ subsetTestByKey(this.name, test, function()
+ {
+ assert_equals(exception, null, "Unexpected exception when evaluating object");
+ assert_equals(typeof obj, expected_typeof, "wrong typeof object");
+ assert_class_string(obj, this.get_qualified_name(), "class string of " + desc);
+ if (!this.has_stringifier())
+ {
+ assert_equals(String(obj), "[object " + this.get_qualified_name() + "]", "String(" + desc + ")");
+ }
+ }.bind(this), "Stringification of " + desc);
+};
+
+IdlInterface.prototype.test_interface_of = function(desc, obj, exception, expected_typeof)
+{
+ // TODO: Indexed and named properties, more checks on interface members
+ this.already_tested = true;
+ if (!shouldRunSubTest(this.name)) {
+ return;
+ }
+
+ var unexposed_properties = new Set();
+ for (var i = 0; i < this.members.length; i++)
+ {
+ var member = this.members[i];
+ if (member.untested) {
+ continue;
+ }
+ if (!exposed_in(exposure_set(member, this.exposureSet)))
+ {
+ if (!unexposed_properties.has(member.name))
+ {
+ unexposed_properties.add(member.name);
+ subsetTestByKey(this.name, test, function() {
+ assert_equals(exception, null, "Unexpected exception when evaluating object");
+ assert_false(member.name in obj);
+ }.bind(this), this.name + " interface: " + desc + ' must not have property "' + member.name + '"');
+ }
+ continue;
+ }
+ if (member.type == "attribute" && member.isUnforgeable)
+ {
+ var a_test = subsetTestByKey(this.name, async_test, this.name + " interface: " + desc + ' must have own property "' + member.name + '"');
+ a_test.step(function() {
+ assert_equals(exception, null, "Unexpected exception when evaluating object");
+ assert_equals(typeof obj, expected_typeof, "wrong typeof object");
+ // Call do_interface_attribute_asserts last, since it will call a_test.done()
+ this.do_interface_attribute_asserts(obj, member, a_test);
+ }.bind(this));
+ }
+ else if (member.type == "operation" &&
+ member.name &&
+ member.isUnforgeable)
+ {
+ var a_test = subsetTestByKey(this.name, async_test, this.name + " interface: " + desc + ' must have own property "' + member.name + '"');
+ a_test.step(function()
+ {
+ assert_equals(exception, null, "Unexpected exception when evaluating object");
+ assert_equals(typeof obj, expected_typeof, "wrong typeof object");
+ assert_own_property(obj, member.name,
+ "Doesn't have the unforgeable operation property");
+ this.do_member_operation_asserts(obj, member, a_test);
+ }.bind(this));
+ }
+ else if ((member.type == "const"
+ || member.type == "attribute"
+ || member.type == "operation")
+ && member.name)
+ {
+ subsetTestByKey(this.name, test, function()
+ {
+ assert_equals(exception, null, "Unexpected exception when evaluating object");
+ assert_equals(typeof obj, expected_typeof, "wrong typeof object");
+ if (member.special !== "static") {
+ if (!this.is_global()) {
+ assert_inherits(obj, member.name);
+ } else {
+ assert_own_property(obj, member.name);
+ }
+
+ if (member.type == "const")
+ {
+ assert_equals(obj[member.name], constValue(member.value));
+ }
+ if (member.type == "attribute")
+ {
+ // Attributes are accessor properties, so they might
+ // legitimately throw an exception rather than returning
+ // anything.
+ var property, thrown = false;
+ try
+ {
+ property = obj[member.name];
+ }
+ catch (e)
+ {
+ thrown = true;
+ }
+ if (!thrown)
+ {
+ if (this.name == "Document" && member.name == "all")
+ {
+ // Result of [[IsHTMLDDA]] slot
+ assert_equals(typeof property, "undefined");
+ }
+ else
+ {
+ this.array.assert_type_is(property, member.idlType);
+ }
+ }
+ }
+ if (member.type == "operation")
+ {
+ assert_equals(typeof obj[member.name], "function");
+ }
+ }
+ }.bind(this), this.name + " interface: " + desc + ' must inherit property "' + member + '" with the proper type');
+ }
+ // TODO: This is wrong if there are multiple operations with the same
+ // identifier.
+ // TODO: Test passing arguments of the wrong type.
+ if (member.type == "operation" && member.name && member.arguments.length)
+ {
+ var description =
+ this.name + " interface: calling " + member + " on " + desc +
+ " with too few arguments must throw TypeError";
+ var a_test = subsetTestByKey(this.name, async_test, description);
+ a_test.step(function()
+ {
+ assert_equals(exception, null, "Unexpected exception when evaluating object");
+ assert_equals(typeof obj, expected_typeof, "wrong typeof object");
+ var fn;
+ if (member.special !== "static") {
+ if (!this.is_global() && !member.isUnforgeable) {
+ assert_inherits(obj, member.name);
+ } else {
+ assert_own_property(obj, member.name);
+ }
+ fn = obj[member.name];
+ }
+ else
+ {
+ assert_own_property(obj.constructor, member.name, "interface object must have static operation as own property");
+ fn = obj.constructor[member.name];
+ }
+
+ var minLength = minOverloadLength(this.members.filter(function(m) {
+ return m.type == "operation" && m.name == member.name;
+ }));
+ var args = [];
+ var cb = awaitNCallbacks(minLength, a_test.done.bind(a_test));
+ for (var i = 0; i < minLength; i++) {
+ throwOrReject(a_test, member, fn, obj, args, "Called with " + i + " arguments", cb);
+
+ args.push(create_suitable_object(member.arguments[i].idlType));
+ }
+ if (minLength === 0) {
+ cb();
+ }
+ }.bind(this));
+ }
+
+ if (member.is_to_json_regular_operation()) {
+ this.test_to_json_operation(desc, obj, member);
+ }
+ }
+};
+
+IdlInterface.prototype.has_stringifier = function()
+{
+ if (this.name === "DOMException") {
+ // toString is inherited from Error, so don't assume we have the
+ // default stringifer
+ return true;
+ }
+ if (this.members.some(function(member) { return member.special === "stringifier"; })) {
+ return true;
+ }
+ if (this.base &&
+ this.array.members[this.base].has_stringifier()) {
+ return true;
+ }
+ return false;
+};
+
+IdlInterface.prototype.do_interface_attribute_asserts = function(obj, member, a_test)
+{
+ // This function tests WebIDL as of 2015-01-27.
+ // TODO: Consider [Exposed].
+
+ // This is called by test_member_attribute() with the prototype as obj if
+ // it is not a global, and the global otherwise, and by test_interface_of()
+ // with the object as obj.
+
+ var pendingPromises = [];
+
+ // "The name of the property is the identifier of the attribute."
+ assert_own_property(obj, member.name);
+
+ // "The property has attributes { [[Get]]: G, [[Set]]: S, [[Enumerable]]:
+ // true, [[Configurable]]: configurable }, where:
+ // "configurable is false if the attribute was declared with the
+ // [LegacyUnforgeable] extended attribute and true otherwise;
+ // "G is the attribute getter, defined below; and
+ // "S is the attribute setter, also defined below."
+ var desc = Object.getOwnPropertyDescriptor(obj, member.name);
+ assert_false("value" in desc, 'property descriptor should not have a "value" field');
+ assert_false("writable" in desc, 'property descriptor should not have a "writable" field');
+ assert_true(desc.enumerable, "property should be enumerable");
+ if (member.isUnforgeable)
+ {
+ assert_false(desc.configurable, "[LegacyUnforgeable] property must not be configurable");
+ }
+ else
+ {
+ assert_true(desc.configurable, "property must be configurable");
+ }
+
+
+ // "The attribute getter is a Function object whose behavior when invoked
+ // is as follows:"
+ assert_equals(typeof desc.get, "function", "getter must be Function");
+
+ // "If the attribute is a regular attribute, then:"
+ if (member.special !== "static") {
+ // "If O is not a platform object that implements I, then:
+ // "If the attribute was specified with the [LegacyLenientThis] extended
+ // attribute, then return undefined.
+ // "Otherwise, throw a TypeError."
+ if (!member.has_extended_attribute("LegacyLenientThis")) {
+ if (member.idlType.generic !== "Promise") {
+ assert_throws_js(globalOf(desc.get).TypeError, function() {
+ desc.get.call({});
+ }.bind(this), "calling getter on wrong object type must throw TypeError");
+ } else {
+ pendingPromises.push(
+ promise_rejects_js(a_test, TypeError, desc.get.call({}),
+ "calling getter on wrong object type must reject the return promise with TypeError"));
+ }
+ } else {
+ assert_equals(desc.get.call({}), undefined,
+ "calling getter on wrong object type must return undefined");
+ }
+ }
+
+ // "The value of the Function object’s “length†property is the Number
+ // value 0."
+ assert_equals(desc.get.length, 0, "getter length must be 0");
+
+ // "Let name be the string "get " prepended to attribute’s identifier."
+ // "Perform ! SetFunctionName(F, name)."
+ assert_equals(desc.get.name, "get " + member.name,
+ "getter must have the name 'get " + member.name + "'");
+
+
+ // TODO: Test calling setter on the interface prototype (should throw
+ // TypeError in most cases).
+ if (member.readonly
+ && !member.has_extended_attribute("LegacyLenientSetter")
+ && !member.has_extended_attribute("PutForwards")
+ && !member.has_extended_attribute("Replaceable"))
+ {
+ // "The attribute setter is undefined if the attribute is declared
+ // readonly and has neither a [PutForwards] nor a [Replaceable]
+ // extended attribute declared on it."
+ assert_equals(desc.set, undefined, "setter must be undefined for readonly attributes");
+ }
+ else
+ {
+ // "Otherwise, it is a Function object whose behavior when
+ // invoked is as follows:"
+ assert_equals(typeof desc.set, "function", "setter must be function for PutForwards, Replaceable, or non-readonly attributes");
+
+ // "If the attribute is a regular attribute, then:"
+ if (member.special !== "static") {
+ // "If /validThis/ is false and the attribute was not specified
+ // with the [LegacyLenientThis] extended attribute, then throw a
+ // TypeError."
+ // "If the attribute is declared with a [Replaceable] extended
+ // attribute, then: ..."
+ // "If validThis is false, then return."
+ if (!member.has_extended_attribute("LegacyLenientThis")) {
+ assert_throws_js(globalOf(desc.set).TypeError, function() {
+ desc.set.call({});
+ }.bind(this), "calling setter on wrong object type must throw TypeError");
+ } else {
+ assert_equals(desc.set.call({}), undefined,
+ "calling setter on wrong object type must return undefined");
+ }
+ }
+
+ // "The value of the Function object’s “length†property is the Number
+ // value 1."
+ assert_equals(desc.set.length, 1, "setter length must be 1");
+
+ // "Let name be the string "set " prepended to id."
+ // "Perform ! SetFunctionName(F, name)."
+ assert_equals(desc.set.name, "set " + member.name,
+ "The attribute setter must have the name 'set " + member.name + "'");
+ }
+
+ Promise.all(pendingPromises).then(a_test.done.bind(a_test));
+}
+
+/// IdlInterfaceMember ///
+function IdlInterfaceMember(obj)
+{
+ /**
+ * obj is an object produced by the WebIDLParser.js "ifMember" production.
+ * We just forward all properties to this object without modification,
+ * except for special extAttrs handling.
+ */
+ for (var k in obj.toJSON())
+ {
+ this[k] = obj[k];
+ }
+ if (!("extAttrs" in this))
+ {
+ this.extAttrs = [];
+ }
+
+ this.isUnforgeable = this.has_extended_attribute("LegacyUnforgeable");
+ this.isUnscopable = this.has_extended_attribute("Unscopable");
+}
+
+IdlInterfaceMember.prototype = Object.create(IdlObject.prototype);
+
+IdlInterfaceMember.prototype.toJSON = function() {
+ return this;
+};
+
+IdlInterfaceMember.prototype.is_to_json_regular_operation = function() {
+ return this.type == "operation" && this.special !== "static" && this.name == "toJSON";
+};
+
+IdlInterfaceMember.prototype.toString = function() {
+ function formatType(type) {
+ var result;
+ if (type.generic) {
+ result = type.generic + "<" + type.idlType.map(formatType).join(", ") + ">";
+ } else if (type.union) {
+ result = "(" + type.subtype.map(formatType).join(" or ") + ")";
+ } else {
+ result = type.idlType;
+ }
+ if (type.nullable) {
+ result += "?"
+ }
+ return result;
+ }
+
+ if (this.type === "operation") {
+ var args = this.arguments.map(function(m) {
+ return [
+ m.optional ? "optional " : "",
+ formatType(m.idlType),
+ m.variadic ? "..." : "",
+ ].join("");
+ }).join(", ");
+ return this.name + "(" + args + ")";
+ }
+
+ return this.name;
+}
+
+/// Internal helper functions ///
+function create_suitable_object(type)
+{
+ /**
+ * type is an object produced by the WebIDLParser.js "type" production. We
+ * return a JavaScript value that matches the type, if we can figure out
+ * how.
+ */
+ if (type.nullable)
+ {
+ return null;
+ }
+ switch (type.idlType)
+ {
+ case "any":
+ case "boolean":
+ return true;
+
+ case "byte": case "octet": case "short": case "unsigned short":
+ case "long": case "unsigned long": case "long long":
+ case "unsigned long long": case "float": case "double":
+ case "unrestricted float": case "unrestricted double":
+ return 7;
+
+ case "DOMString":
+ case "ByteString":
+ case "USVString":
+ return "foo";
+
+ case "object":
+ return {a: "b"};
+
+ case "Node":
+ return document.createTextNode("abc");
+ }
+ return null;
+}
+
+/// IdlEnum ///
+// Used for IdlArray.prototype.assert_type_is
+function IdlEnum(obj)
+{
+ /**
+ * obj is an object produced by the WebIDLParser.js "dictionary"
+ * production.
+ */
+
+ /** Self-explanatory. */
+ this.name = obj.name;
+
+ /** An array of values produced by the "enum" production. */
+ this.values = obj.values;
+
+}
+
+IdlEnum.prototype = Object.create(IdlObject.prototype);
+
+/// IdlCallback ///
+// Used for IdlArray.prototype.assert_type_is
+function IdlCallback(obj)
+{
+ /**
+ * obj is an object produced by the WebIDLParser.js "callback"
+ * production.
+ */
+
+ /** Self-explanatory. */
+ this.name = obj.name;
+
+ /** Arguments for the callback. */
+ this.arguments = obj.arguments;
+}
+
+IdlCallback.prototype = Object.create(IdlObject.prototype);
+
+/// IdlTypedef ///
+// Used for IdlArray.prototype.assert_type_is
+function IdlTypedef(obj)
+{
+ /**
+ * obj is an object produced by the WebIDLParser.js "typedef"
+ * production.
+ */
+
+ /** Self-explanatory. */
+ this.name = obj.name;
+
+ /** The idlType that we are supposed to be typedeffing to. */
+ this.idlType = obj.idlType;
+
+}
+
+IdlTypedef.prototype = Object.create(IdlObject.prototype);
+
+/// IdlNamespace ///
+function IdlNamespace(obj)
+{
+ this.name = obj.name;
+ this.extAttrs = obj.extAttrs;
+ this.untested = obj.untested;
+ /** A back-reference to our IdlArray. */
+ this.array = obj.array;
+
+ /** An array of IdlInterfaceMembers. */
+ this.members = obj.members.map(m => new IdlInterfaceMember(m));
+}
+
+IdlNamespace.prototype = Object.create(IdlObject.prototype);
+
+IdlNamespace.prototype.do_member_operation_asserts = function (memberHolderObject, member, a_test)
+{
+ var desc = Object.getOwnPropertyDescriptor(memberHolderObject, member.name);
+
+ assert_false("get" in desc, "property should not have a getter");
+ assert_false("set" in desc, "property should not have a setter");
+ assert_equals(
+ desc.writable,
+ !member.isUnforgeable,
+ "property should be writable if and only if not unforgeable");
+ assert_true(desc.enumerable, "property should be enumerable");
+ assert_equals(
+ desc.configurable,
+ !member.isUnforgeable,
+ "property should be configurable if and only if not unforgeable");
+
+ assert_equals(
+ typeof memberHolderObject[member.name],
+ "function",
+ "property must be a function");
+
+ assert_equals(
+ memberHolderObject[member.name].length,
+ minOverloadLength(this.members.filter(function(m) {
+ return m.type == "operation" && m.name == member.name;
+ })),
+ "operation has wrong .length");
+ a_test.done();
+}
+
+IdlNamespace.prototype.test_member_operation = function(member)
+{
+ if (!shouldRunSubTest(this.name)) {
+ return;
+ }
+ var a_test = subsetTestByKey(
+ this.name,
+ async_test,
+ this.name + ' namespace: operation ' + member);
+ a_test.step(function() {
+ assert_own_property(
+ self[this.name],
+ member.name,
+ 'namespace object missing operation ' + format_value(member.name));
+
+ this.do_member_operation_asserts(self[this.name], member, a_test);
+ }.bind(this));
+};
+
+IdlNamespace.prototype.test_member_attribute = function (member)
+{
+ if (!shouldRunSubTest(this.name)) {
+ return;
+ }
+ var a_test = subsetTestByKey(
+ this.name,
+ async_test,
+ this.name + ' namespace: attribute ' + member.name);
+ a_test.step(function()
+ {
+ assert_own_property(
+ self[this.name],
+ member.name,
+ this.name + ' does not have property ' + format_value(member.name));
+
+ var desc = Object.getOwnPropertyDescriptor(self[this.name], member.name);
+ assert_equals(desc.set, undefined, "setter must be undefined for namespace members");
+ a_test.done();
+ }.bind(this));
+};
+
+IdlNamespace.prototype.test_self = function ()
+{
+ /**
+ * TODO(lukebjerring): Assert:
+ * - "Note that unlike interfaces or dictionaries, namespaces do not create types."
+ */
+
+ subsetTestByKey(this.name, test, () => {
+ assert_true(this.extAttrs.every(o => o.name === "Exposed" || o.name === "SecureContext"),
+ "Only the [Exposed] and [SecureContext] extended attributes are applicable to namespaces");
+ assert_true(this.has_extended_attribute("Exposed"),
+ "Namespaces must be annotated with the [Exposed] extended attribute");
+ }, `${this.name} namespace: extended attributes`);
+
+ const namespaceObject = self[this.name];
+
+ subsetTestByKey(this.name, test, () => {
+ const desc = Object.getOwnPropertyDescriptor(self, this.name);
+ assert_equals(desc.value, namespaceObject, `wrong value for ${this.name} namespace object`);
+ assert_true(desc.writable, "namespace object should be writable");
+ assert_false(desc.enumerable, "namespace object should not be enumerable");
+ assert_true(desc.configurable, "namespace object should be configurable");
+ assert_false("get" in desc, "namespace object should not have a getter");
+ assert_false("set" in desc, "namespace object should not have a setter");
+ }, `${this.name} namespace: property descriptor`);
+
+ subsetTestByKey(this.name, test, () => {
+ assert_true(Object.isExtensible(namespaceObject));
+ }, `${this.name} namespace: [[Extensible]] is true`);
+
+ subsetTestByKey(this.name, test, () => {
+ assert_true(namespaceObject instanceof Object);
+
+ if (this.name === "console") {
+ // https://console.spec.whatwg.org/#console-namespace
+ const namespacePrototype = Object.getPrototypeOf(namespaceObject);
+ assert_equals(Reflect.ownKeys(namespacePrototype).length, 0);
+ assert_equals(Object.getPrototypeOf(namespacePrototype), Object.prototype);
+ } else {
+ assert_equals(Object.getPrototypeOf(namespaceObject), Object.prototype);
+ }
+ }, `${this.name} namespace: [[Prototype]] is Object.prototype`);
+
+ subsetTestByKey(this.name, test, () => {
+ assert_equals(typeof namespaceObject, "object");
+ }, `${this.name} namespace: typeof is "object"`);
+
+ subsetTestByKey(this.name, test, () => {
+ assert_equals(
+ Object.getOwnPropertyDescriptor(namespaceObject, "length"),
+ undefined,
+ "length property must be undefined"
+ );
+ }, `${this.name} namespace: has no length property`);
+
+ subsetTestByKey(this.name, test, () => {
+ assert_equals(
+ Object.getOwnPropertyDescriptor(namespaceObject, "name"),
+ undefined,
+ "name property must be undefined"
+ );
+ }, `${this.name} namespace: has no name property`);
+};
+
+IdlNamespace.prototype.test = function ()
+{
+ if (!this.untested) {
+ this.test_self();
+ }
+
+ for (const v of Object.values(this.members)) {
+ switch (v.type) {
+
+ case 'operation':
+ this.test_member_operation(v);
+ break;
+
+ case 'attribute':
+ this.test_member_attribute(v);
+ break;
+
+ default:
+ throw 'Invalid namespace member ' + v.name + ': ' + v.type + ' not supported';
+ }
+ };
+};
+
+}());
+
+/**
+ * idl_test is a promise_test wrapper that handles the fetching of the IDL,
+ * avoiding repetitive boilerplate.
+ *
+ * @param {String[]} srcs Spec name(s) for source idl files (fetched from
+ * /interfaces/{name}.idl).
+ * @param {String[]} deps Spec name(s) for dependency idl files (fetched
+ * from /interfaces/{name}.idl). Order is important - dependencies from
+ * each source will only be included if they're already know to be a
+ * dependency (i.e. have already been seen).
+ * @param {Function} setup_func Function for extra setup of the idl_array, such
+ * as adding objects. Do not call idl_array.test() in the setup; it is
+ * called by this function (idl_test).
+ */
+function idl_test(srcs, deps, idl_setup_func) {
+ return promise_test(function (t) {
+ var idl_array = new IdlArray();
+ var setup_error = null;
+ const validationIgnored = [
+ "constructor-member",
+ "dict-arg-default",
+ "require-exposed"
+ ];
+ return Promise.all(
+ srcs.concat(deps).map(fetch_spec))
+ .then(function(results) {
+ const astArray = results.map(result =>
+ WebIDL2.parse(result.idl, { sourceName: result.spec })
+ );
+ test(() => {
+ const validations = WebIDL2.validate(astArray)
+ .filter(v => !validationIgnored.includes(v.ruleName));
+ if (validations.length) {
+ const message = validations.map(v => v.message).join("\n\n");
+ throw new Error(message);
+ }
+ }, "idl_test validation");
+ for (var i = 0; i < srcs.length; i++) {
+ idl_array.internal_add_idls(astArray[i]);
+ }
+ for (var i = srcs.length; i < srcs.length + deps.length; i++) {
+ idl_array.internal_add_dependency_idls(astArray[i]);
+ }
+ })
+ .then(function() {
+ if (idl_setup_func) {
+ return idl_setup_func(idl_array, t);
+ }
+ })
+ .catch(function(e) { setup_error = e || 'IDL setup failed.'; })
+ .then(function () {
+ var error = setup_error;
+ try {
+ idl_array.test(); // Test what we can.
+ } catch (e) {
+ // If testing fails hard here, the original setup error
+ // is more likely to be the real cause.
+ error = error || e;
+ }
+ if (error) {
+ throw error;
+ }
+ });
+ }, 'idl_test setup');
+}
+
+/**
+ * fetch_spec is a shorthand for a Promise that fetches the spec's content.
+ */
+function fetch_spec(spec) {
+ var url = '/interfaces/' + spec + '.idl';
+ return fetch(url).then(function (r) {
+ if (!r.ok) {
+ throw new IdlHarnessError("Error fetching " + url + ".");
+ }
+ return r.text();
+ }).then(idl => ({ spec, idl }));
+}
+// vim: set expandtab shiftwidth=4 tabstop=4 foldmarker=@{,@} foldmethod=marker:
diff --git a/test/wpt/tests/resources/idlharness.js.headers b/test/wpt/tests/resources/idlharness.js.headers
new file mode 100644
index 0000000..5e8f640
--- /dev/null
+++ b/test/wpt/tests/resources/idlharness.js.headers
@@ -0,0 +1,2 @@
+Content-Type: text/javascript; charset=utf-8
+Cache-Control: max-age=3600
diff --git a/test/wpt/tests/resources/readme.md b/test/wpt/tests/resources/readme.md
new file mode 100644
index 0000000..09a62fb
--- /dev/null
+++ b/test/wpt/tests/resources/readme.md
@@ -0,0 +1,14 @@
+# Resources
+
+This directory contains utilities intended for use by tests and maintained as project infrastructure.
+It does not contain tests.
+
+## `testharness.js`
+
+`testharness.js` is a framework for writing low-level tests of
+browser functionality in javascript. It provides a convenient API for
+making assertions and is intended to work for both simple synchronous
+tests, and tests of asynchronous behaviour.
+
+Complete documentation is available in the `docs/` directory of this repository
+and on the web at https://web-platform-tests.org/writing-tests/.
diff --git a/test/wpt/tests/resources/sriharness.js b/test/wpt/tests/resources/sriharness.js
new file mode 100644
index 0000000..943d677
--- /dev/null
+++ b/test/wpt/tests/resources/sriharness.js
@@ -0,0 +1,226 @@
+// `integrityValue` indicates the 'integrity' attribute value at the time of
+// #prepare-a-script.
+//
+// `integrityValueAfterPrepare` indicates how the 'integrity' attribute value
+// is modified after #prepare-a-script:
+// - `undefined` => not modified.
+// - `null` => 'integrity' attribute is removed.
+// - others => 'integrity' attribute value is set to that value.
+//
+// TODO: Make the arguments a dictionary for readability in the test files.
+var SRIScriptTest = function(pass, name, src, integrityValue, crossoriginValue, nonce, integrityValueAfterPrepare) {
+ this.pass = pass;
+ this.name = "Script: " + name;
+ this.src = src;
+ this.integrityValue = integrityValue;
+ this.crossoriginValue = crossoriginValue;
+ this.nonce = nonce;
+ this.integrityValueAfterPrepare = integrityValueAfterPrepare;
+}
+
+SRIScriptTest.prototype.execute = function() {
+ var test = async_test(this.name);
+ var e = document.createElement("script");
+ e.src = this.src;
+ if (this.integrityValue) {
+ e.setAttribute("integrity", this.integrityValue);
+ }
+ if(this.crossoriginValue) {
+ e.setAttribute("crossorigin", this.crossoriginValue);
+ }
+ if(this.nonce) {
+ e.setAttribute("nonce", this.nonce);
+ }
+ if(this.pass) {
+ e.addEventListener("load", function() {test.done()});
+ e.addEventListener("error", function() {
+ test.step(function(){ assert_unreached("Good load fired error handler.") })
+ });
+ } else {
+ e.addEventListener("load", function() {
+ test.step(function() { assert_unreached("Bad load succeeded.") })
+ });
+ e.addEventListener("error", function() {test.done()});
+ }
+ document.body.appendChild(e);
+
+ if (this.integrityValueAfterPrepare === null) {
+ e.removeAttribute("integrity");
+ } else if (this.integrityValueAfterPrepare !== undefined) {
+ e.setAttribute("integrity", this.integrityValueAfterPrepare);
+ }
+};
+
+function set_extra_attributes(element, attrs) {
+ // Apply the rest of the attributes, if any.
+ for (const [attr_name, attr_val] of Object.entries(attrs)) {
+ element[attr_name] = attr_val;
+ }
+}
+
+function buildElementFromDestination(resource_url, destination, attrs) {
+ // Assert: |destination| is a valid destination.
+ let element;
+
+ // The below switch is responsible for:
+ // 1. Creating the correct subresource element
+ // 2. Setting said element's href, src, or fetch-instigating property
+ // appropriately.
+ switch (destination) {
+ case "script":
+ element = document.createElement(destination);
+ set_extra_attributes(element, attrs);
+ element.src = resource_url;
+ break;
+ case "style":
+ element = document.createElement('link');
+ set_extra_attributes(element, attrs);
+ element.rel = 'stylesheet';
+ element.href = resource_url;
+ break;
+ case "image":
+ element = document.createElement('img');
+ set_extra_attributes(element, attrs);
+ element.src = resource_url;
+ break;
+ default:
+ assert_unreached("INVALID DESTINATION");
+ }
+
+ return element;
+}
+
+// When using SRIPreloadTest, also include /preload/resources/preload_helper.js
+// |number_of_requests| is used to ensure that preload requests are actually
+// reused as expected.
+const SRIPreloadTest = (preload_sri_success, subresource_sri_success, name,
+ number_of_requests, destination, resource_url,
+ link_attrs, subresource_attrs) => {
+ const test = async_test(name);
+ const link = document.createElement('link');
+
+ // Early-fail in UAs that do not support `preload` links.
+ test.step_func(() => {
+ assert_true(link.relList.supports('preload'),
+ "This test is automatically failing because the browser does not" +
+ "support `preload` links.");
+ })();
+
+ // Build up the link.
+ link.rel = 'preload';
+ link.as = destination;
+ link.href = resource_url;
+ for (const [attr_name, attr_val] of Object.entries(link_attrs)) {
+ link[attr_name] = attr_val; // This may override `rel` to modulepreload.
+ }
+
+ // Preload + subresource success and failure loading functions.
+ const valid_preload_failed = test.step_func(() =>
+ { assert_unreached("Valid preload fired error handler.") });
+ const invalid_preload_succeeded = test.step_func(() =>
+ { assert_unreached("Invalid preload load succeeded.") });
+ const valid_subresource_failed = test.step_func(() =>
+ { assert_unreached("Valid subresource fired error handler.") });
+ const invalid_subresource_succeeded = test.step_func(() =>
+ { assert_unreached("Invalid subresource load succeeded.") });
+ const subresource_pass = test.step_func(() => {
+ verifyNumberOfResourceTimingEntries(resource_url, number_of_requests);
+ test.done();
+ });
+ const preload_pass = test.step_func(() => {
+ const subresource_element = buildElementFromDestination(
+ resource_url,
+ destination,
+ subresource_attrs
+ );
+
+ if (subresource_sri_success) {
+ subresource_element.onload = subresource_pass;
+ subresource_element.onerror = valid_subresource_failed;
+ } else {
+ subresource_element.onload = invalid_subresource_succeeded;
+ subresource_element.onerror = subresource_pass;
+ }
+
+ document.body.append(subresource_element);
+ });
+
+ if (preload_sri_success) {
+ link.onload = preload_pass;
+ link.onerror = valid_preload_failed;
+ } else {
+ link.onload = invalid_preload_succeeded;
+ link.onerror = preload_pass;
+ }
+
+ document.head.append(link);
+}
+
+// <link> tests
+// Style tests must be done synchronously because they rely on the presence
+// and absence of global style, which can affect later tests. Thus, instead
+// of executing them one at a time, the style tests are implemented as a
+// queue that builds up a list of tests, and then executes them one at a
+// time.
+var SRIStyleTest = function(queue, pass, name, attrs, customCallback, altPassValue) {
+ this.pass = pass;
+ this.name = "Style: " + name;
+ this.customCallback = customCallback || function () {};
+ this.attrs = attrs || {};
+ this.passValue = altPassValue || "rgb(255, 255, 0)";
+
+ this.test = async_test(this.name);
+
+ this.queue = queue;
+ this.queue.push(this);
+}
+
+SRIStyleTest.prototype.execute = function() {
+ var that = this;
+ var container = document.getElementById("container");
+ while (container.hasChildNodes()) {
+ container.removeChild(container.firstChild);
+ }
+
+ var test = this.test;
+
+ var div = document.createElement("div");
+ div.className = "testdiv";
+ var e = document.createElement("link");
+
+ // The link relation is guaranteed to not be "preload" or "modulepreload".
+ this.attrs.rel = this.attrs.rel || "stylesheet";
+ for (var key in this.attrs) {
+ if (this.attrs.hasOwnProperty(key)) {
+ e.setAttribute(key, this.attrs[key]);
+ }
+ }
+
+ if(this.pass) {
+ e.addEventListener("load", function() {
+ test.step(function() {
+ var background = window.getComputedStyle(div, null).getPropertyValue("background-color");
+ assert_equals(background, that.passValue);
+ test.done();
+ });
+ });
+ e.addEventListener("error", function() {
+ test.step(function(){ assert_unreached("Good load fired error handler.") })
+ });
+ } else {
+ e.addEventListener("load", function() {
+ test.step(function() { assert_unreached("Bad load succeeded.") })
+ });
+ e.addEventListener("error", function() {
+ test.step(function() {
+ var background = window.getComputedStyle(div, null).getPropertyValue("background-color");
+ assert_not_equals(background, that.passValue);
+ test.done();
+ });
+ });
+ }
+ container.appendChild(div);
+ container.appendChild(e);
+ this.customCallback(e, container);
+};
+
diff --git a/test/wpt/tests/resources/test-only-api.js b/test/wpt/tests/resources/test-only-api.js
new file mode 100644
index 0000000..a66eb44
--- /dev/null
+++ b/test/wpt/tests/resources/test-only-api.js
@@ -0,0 +1,31 @@
+'use strict';
+
+/* Whether the browser is Chromium-based with MojoJS enabled */
+const isChromiumBased = 'MojoInterfaceInterceptor' in self;
+/* Whether the browser is WebKit-based with internal test-only API enabled */
+const isWebKitBased = !isChromiumBased && 'internals' in self;
+
+/**
+ * Loads a script in a window or worker.
+ *
+ * @param {string} path - A script path
+ * @returns {Promise}
+ */
+function loadScript(path) {
+ if (typeof document === 'undefined') {
+ // Workers (importScripts is synchronous and may throw.)
+ importScripts(path);
+ return Promise.resolve();
+ } else {
+ // Window
+ const script = document.createElement('script');
+ script.src = path;
+ script.async = false;
+ const p = new Promise((resolve, reject) => {
+ script.onload = () => { resolve(); };
+ script.onerror = e => { reject(`Error loading ${path}`); };
+ })
+ document.head.appendChild(script);
+ return p;
+ }
+}
diff --git a/test/wpt/tests/resources/test-only-api.js.headers b/test/wpt/tests/resources/test-only-api.js.headers
new file mode 100644
index 0000000..5e8f640
--- /dev/null
+++ b/test/wpt/tests/resources/test-only-api.js.headers
@@ -0,0 +1,2 @@
+Content-Type: text/javascript; charset=utf-8
+Cache-Control: max-age=3600
diff --git a/test/wpt/tests/resources/test-only-api.m.js b/test/wpt/tests/resources/test-only-api.m.js
new file mode 100644
index 0000000..984f635
--- /dev/null
+++ b/test/wpt/tests/resources/test-only-api.m.js
@@ -0,0 +1,5 @@
+/* Whether the browser is Chromium-based with MojoJS enabled */
+export const isChromiumBased = 'MojoInterfaceInterceptor' in self;
+
+/* Whether the browser is WebKit-based with internal test-only API enabled */
+export const isWebKitBased = !isChromiumBased && 'internals' in self;
diff --git a/test/wpt/tests/resources/test-only-api.m.js.headers b/test/wpt/tests/resources/test-only-api.m.js.headers
new file mode 100644
index 0000000..5e8f640
--- /dev/null
+++ b/test/wpt/tests/resources/test-only-api.m.js.headers
@@ -0,0 +1,2 @@
+Content-Type: text/javascript; charset=utf-8
+Cache-Control: max-age=3600
diff --git a/test/wpt/tests/resources/test/README.md b/test/wpt/tests/resources/test/README.md
new file mode 100644
index 0000000..edc03ef
--- /dev/null
+++ b/test/wpt/tests/resources/test/README.md
@@ -0,0 +1,83 @@
+# `testharness.js` test suite
+
+The test suite for the `testharness.js` testing framework.
+
+## Executing Tests
+
+Install the following dependencies:
+
+- [Python 2.7.9+](https://www.python.org/)
+- [the tox Python package](https://tox.readthedocs.io/en/latest/)
+- [the Mozilla Firefox web browser](https://mozilla.org/firefox)
+- [the GeckoDriver server](https://github.com/mozilla/geckodriver)
+
+Make sure `geckodriver` can be found in your `PATH`.
+
+Currently, the tests should be run with the latest *Firefox Nightly*. In order to
+specify the path to Firefox Nightly, use the following command-line option:
+
+ tox -- --binary=/path/to/FirefoxNightly
+
+### Automated Script
+
+Alternatively, you may run `tools/ci/ci_resources_unittest.sh`, which only depends on
+Python 2. The script will install other dependencies automatically and start `tox` with
+the correct arguments.
+
+## Authoring Tests
+
+Test cases are expressed as `.html` files located within the `tests/unit/` and
+`tests/functional/` sub-directories. Each test should include the
+`testharness.js` library with the following markup:
+
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+
+This should be followed by one or more `<script>` tags that interface with the
+`testharness.js` API in some way. For example:
+
+ <script>
+ test(function() {
+ 1 = 1;
+ }, 'This test is expected to fail.');
+ </script>
+
+### Unit tests
+
+The "unit test" type allows for concisely testing the expected behavior of
+assertion methods. These tests may define any number of sub-tests; the
+acceptance criteria is simply that all tests executed pass.
+
+### Functional tests
+
+Thoroughly testing the behavior of the harness itself requires ensuring a
+number of considerations which cannot be verified with the "unit testing"
+strategy. These include:
+
+- Ensuring that some tests are not run
+- Ensuring conditions that cause test failures
+- Ensuring conditions that cause harness errors
+
+Functional tests allow for these details to be verified. Every functional test
+must include a summary of the expected results as a JSON string within a
+`<script>` tag with an `id` of `"expected"`, e.g.:
+
+ <script type="text/json" id="expected">
+ {
+ "summarized_status": {
+ "message": null,
+ "stack": null,
+ "status_string": "OK"
+ },
+ "summarized_tests": [
+ {
+ "message": "ReferenceError: invalid assignment left-hand side",
+ "name": "Sample HTML5 API Tests",
+ "properties": {},
+ "stack": "(implementation-defined)",
+ "status_string": "FAIL"
+ }
+ ],
+ "type": "complete"
+ }
+ </script>
diff --git a/test/wpt/tests/resources/test/conftest.py b/test/wpt/tests/resources/test/conftest.py
new file mode 100644
index 0000000..7253cac
--- /dev/null
+++ b/test/wpt/tests/resources/test/conftest.py
@@ -0,0 +1,269 @@
+import copy
+import json
+import os
+import ssl
+import sys
+import subprocess
+import urllib
+
+import html5lib
+import py
+import pytest
+
+from wptserver import WPTServer
+
+HERE = os.path.dirname(os.path.abspath(__file__))
+WPT_ROOT = os.path.normpath(os.path.join(HERE, '..', '..'))
+HARNESS = os.path.join(HERE, 'harness.html')
+TEST_TYPES = ('functional', 'unit')
+
+sys.path.insert(0, os.path.normpath(os.path.join(WPT_ROOT, "tools")))
+import localpaths
+
+sys.path.insert(0, os.path.normpath(os.path.join(WPT_ROOT, "tools", "webdriver")))
+import webdriver
+
+
+def pytest_addoption(parser):
+ parser.addoption("--binary", action="store", default=None, help="path to browser binary")
+ parser.addoption("--headless", action="store_true", default=False, help="run browser in headless mode")
+
+
+def pytest_collect_file(file_path, path, parent):
+ if file_path.suffix.lower() != '.html':
+ return
+
+ # Tests are organized in directories by type
+ test_type = os.path.relpath(str(file_path), HERE)
+ if os.path.sep not in test_type or ".." in test_type:
+ # HTML files in this directory are not tests
+ return
+ test_type = test_type.split(os.path.sep)[1]
+
+ # Handle the deprecation of Node construction in pytest6
+ # https://docs.pytest.org/en/stable/deprecations.html#node-construction-changed-to-node-from-parent
+ if hasattr(HTMLItem, "from_parent"):
+ return HTMLItem.from_parent(parent, filename=str(file_path), test_type=test_type)
+ return HTMLItem(parent, str(file_path), test_type)
+
+
+def pytest_configure(config):
+ config.proc = subprocess.Popen(["geckodriver"])
+ config.add_cleanup(config.proc.kill)
+
+ capabilities = {"alwaysMatch": {"acceptInsecureCerts": True, "moz:firefoxOptions": {}}}
+ if config.getoption("--binary"):
+ capabilities["alwaysMatch"]["moz:firefoxOptions"]["binary"] = config.getoption("--binary")
+ if config.getoption("--headless"):
+ capabilities["alwaysMatch"]["moz:firefoxOptions"]["args"] = ["--headless"]
+
+ config.driver = webdriver.Session("localhost", 4444,
+ capabilities=capabilities)
+ config.add_cleanup(config.driver.end)
+
+ # Although the name of the `_create_unverified_context` method suggests
+ # that it is not intended for external consumption, the standard library's
+ # documentation explicitly endorses its use:
+ #
+ # > To revert to the previous, unverified, behavior
+ # > ssl._create_unverified_context() can be passed to the context
+ # > parameter.
+ #
+ # https://docs.python.org/2/library/httplib.html#httplib.HTTPSConnection
+ config.ssl_context = ssl._create_unverified_context()
+
+ config.server = WPTServer(WPT_ROOT)
+ config.server.start(config.ssl_context)
+ config.add_cleanup(config.server.stop)
+
+
+def resolve_uri(context, uri):
+ if uri.startswith('/'):
+ base = WPT_ROOT
+ path = uri[1:]
+ else:
+ base = os.path.dirname(context)
+ path = uri
+
+ return os.path.exists(os.path.join(base, path))
+
+
+def _summarize(actual):
+ def _scrub_stack(test_obj):
+ copy = dict(test_obj)
+ del copy['stack']
+ return copy
+
+ def _expand_status(status_obj):
+ for key, value in [item for item in status_obj.items()]:
+ # In "status" and "test" objects, the "status" value enum
+ # definitions are interspersed with properties for unrelated
+ # metadata. The following condition is a best-effort attempt to
+ # ignore non-enum properties.
+ if key != key.upper() or not isinstance(value, int):
+ continue
+
+ del status_obj[key]
+
+ if status_obj['status'] == value:
+ status_obj[u'status_string'] = key
+
+ del status_obj['status']
+
+ return status_obj
+
+ def _summarize_test(test_obj):
+ del test_obj['index']
+
+ assert 'phase' in test_obj
+ assert 'phases' in test_obj
+ assert 'COMPLETE' in test_obj['phases']
+ assert test_obj['phase'] == test_obj['phases']['COMPLETE']
+ del test_obj['phases']
+ del test_obj['phase']
+
+ return _expand_status(_scrub_stack(test_obj))
+
+ def _summarize_status(status_obj):
+ return _expand_status(_scrub_stack(status_obj))
+
+
+ summarized = {}
+
+ summarized[u'summarized_status'] = _summarize_status(actual['status'])
+ summarized[u'summarized_tests'] = [
+ _summarize_test(test) for test in actual['tests']]
+ summarized[u'summarized_tests'].sort(key=lambda test_obj: test_obj.get('name'))
+ summarized[u'summarized_asserts'] = [
+ {"assert_name": assert_item["assert_name"],
+ "test": assert_item["test"]["name"] if assert_item["test"] else None,
+ "args": assert_item["args"],
+ "status": assert_item["status"]} for assert_item in actual["asserts"]]
+ summarized[u'type'] = actual['type']
+
+ return summarized
+
+
+class HTMLItem(pytest.Item, pytest.Collector):
+ def __init__(self, parent, filename, test_type):
+ self.url = parent.session.config.server.url(filename)
+ self.type = test_type
+ # Some tests are reliant on the WPT servers substitution functionality,
+ # so tests must be retrieved from the server rather than read from the
+ # file system directly.
+ handle = urllib.request.urlopen(self.url,
+ context=parent.session.config.ssl_context)
+ try:
+ markup = handle.read()
+ finally:
+ handle.close()
+
+ if test_type not in TEST_TYPES:
+ raise ValueError('Unrecognized test type: "%s"' % test_type)
+
+ parsed = html5lib.parse(markup, namespaceHTMLElements=False)
+ name = None
+ self.expected = None
+
+ for element in parsed.iter():
+ if not name and element.tag == 'title':
+ name = element.text
+ continue
+ if element.tag == 'script':
+ if element.attrib.get('id') == 'expected':
+ try:
+ self.expected = json.loads(element.text)
+ except ValueError:
+ print("Failed parsing JSON in %s" % filename)
+ raise
+
+ if not name:
+ raise ValueError('No name found in %s add a <title> element' % filename)
+ elif self.type == 'functional':
+ if not self.expected:
+ raise ValueError('Functional tests must specify expected report data')
+ elif self.type == 'unit' and self.expected:
+ raise ValueError('Unit tests must not specify expected report data')
+
+ # Ensure that distinct items have distinct fspath attributes.
+ # This is necessary because pytest has an internal cache keyed on it,
+ # and only the first test with any given fspath will be run.
+ #
+ # This cannot use super(HTMLItem, self).__init__(..) because only the
+ # Collector constructor takes the fspath argument.
+ pytest.Item.__init__(self, name, parent)
+ pytest.Collector.__init__(self, name, parent, fspath=py.path.local(filename))
+
+
+ def reportinfo(self):
+ return self.fspath, None, self.url
+
+ def repr_failure(self, excinfo):
+ return pytest.Collector.repr_failure(self, excinfo)
+
+ def runtest(self):
+ if self.type == 'unit':
+ self._run_unit_test()
+ elif self.type == 'functional':
+ self._run_functional_test()
+ else:
+ raise NotImplementedError
+
+ def _run_unit_test(self):
+ driver = self.session.config.driver
+ server = self.session.config.server
+
+ driver.url = server.url(HARNESS)
+
+ actual = driver.execute_async_script(
+ 'runTest("%s", "foo", arguments[0])' % self.url
+ )
+
+ summarized = _summarize(copy.deepcopy(actual))
+
+ print(json.dumps(summarized, indent=2))
+
+ assert summarized[u'summarized_status'][u'status_string'] == u'OK', summarized[u'summarized_status'][u'message']
+ for test in summarized[u'summarized_tests']:
+ msg = "%s\n%s" % (test[u'name'], test[u'message'])
+ assert test[u'status_string'] == u'PASS', msg
+
+ def _run_functional_test(self):
+ driver = self.session.config.driver
+ server = self.session.config.server
+
+ driver.url = server.url(HARNESS)
+
+ test_url = self.url
+ actual = driver.execute_async_script('runTest("%s", "foo", arguments[0])' % test_url)
+
+ print(json.dumps(actual, indent=2))
+
+ summarized = _summarize(copy.deepcopy(actual))
+
+ print(json.dumps(summarized, indent=2))
+
+ # Test object ordering is not guaranteed. This weak assertion verifies
+ # that the indices are unique and sequential
+ indices = [test_obj.get('index') for test_obj in actual['tests']]
+ self._assert_sequence(indices)
+
+ self.expected[u'summarized_tests'].sort(key=lambda test_obj: test_obj.get('name'))
+
+ # Make asserts opt-in for now
+ if "summarized_asserts" not in self.expected:
+ del summarized["summarized_asserts"]
+ else:
+ # We can't be sure of the order of asserts even within the same test
+ # although we could also check for the failing assert being the final
+ # one
+ for obj in [summarized, self.expected]:
+ obj["summarized_asserts"].sort(
+ key=lambda x: (x["test"] or "", x["status"], x["assert_name"], tuple(x["args"])))
+
+ assert summarized == self.expected
+
+ @staticmethod
+ def _assert_sequence(nums):
+ if nums and len(nums) > 0:
+ assert nums == list(range(1, nums[-1] + 1))
diff --git a/test/wpt/tests/resources/test/harness.html b/test/wpt/tests/resources/test/harness.html
new file mode 100644
index 0000000..5ee0f28
--- /dev/null
+++ b/test/wpt/tests/resources/test/harness.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+ </head>
+ <body>
+ <script>
+function runTest(url, id, done) {
+ var child;
+
+ function onMessage(event) {
+ if (!event.data || event.data.type !== 'complete') {
+ return;
+ }
+
+ window.removeEventListener('message', onMessage);
+ child.close();
+ done(event.data);
+ }
+ window.addEventListener('message', onMessage);
+
+ window.child = child = window.open(url, id);
+}
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/resources/test/idl-helper.js b/test/wpt/tests/resources/test/idl-helper.js
new file mode 100644
index 0000000..2b73527
--- /dev/null
+++ b/test/wpt/tests/resources/test/idl-helper.js
@@ -0,0 +1,24 @@
+"use strict";
+
+var typedefFrom = interfaceFrom;
+var dictionaryFrom = interfaceFrom;
+function interfaceFrom(i) {
+ var idl = new IdlArray();
+ idl.add_idls(i);
+ for (var prop in idl.members) {
+ return idl.members[prop];
+ }
+}
+
+function memberFrom(m) {
+ var idl = new IdlArray();
+ idl.add_idls('interface A { ' + m + '; };');
+ return idl.members["A"].members[0];
+}
+
+function typeFrom(type) {
+ var ast = WebIDL2.parse('interface Foo { ' + type + ' a(); };');
+ ast = ast[0]; // get the first fragment
+ ast = ast.members[0]; // get the first member
+ return ast.idlType; // get the type of the first field
+}
diff --git a/test/wpt/tests/resources/test/nested-testharness.js b/test/wpt/tests/resources/test/nested-testharness.js
new file mode 100644
index 0000000..d97c156
--- /dev/null
+++ b/test/wpt/tests/resources/test/nested-testharness.js
@@ -0,0 +1,80 @@
+'use strict';
+
+/**
+ * Execute testharness.js and one or more scripts in an iframe. Report the
+ * results of the execution.
+ *
+ * @param {...function|...string} bodies - a function body. If specified as a
+ * function object, it will be
+ * serialized to a string using the
+ * built-in
+ * `Function.prototype.toString` prior
+ * to inclusion in the generated
+ * iframe.
+ *
+ * @returns {Promise} eventual value describing the result of the test
+ * execution; the summary object has two properties:
+ * `harness` (a string describing the harness status) and
+ * `tests` (an object whose "own" property names are the
+ * titles of the defined sub-tests and whose associated
+ * values are the subtest statuses).
+ */
+function makeTest(...bodies) {
+ const closeScript = '<' + '/script>';
+ let src = `
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Document title</title>
+<script src="/resources/testharness.js?${Math.random()}">${closeScript}
+</head>
+
+<body>
+<div id="log"></div>`;
+ bodies.forEach((body) => {
+ src += '<script>(' + body + ')();' + closeScript;
+ });
+
+ const iframe = document.createElement('iframe');
+
+ document.body.appendChild(iframe);
+ iframe.contentDocument.write(src);
+
+ return new Promise((resolve) => {
+ window.addEventListener('message', function onMessage(e) {
+ if (e.source !== iframe.contentWindow) {
+ return;
+ }
+ if (!e.data || e.data.type !=='complete') {
+ return;
+ }
+ window.removeEventListener('message', onMessage);
+ resolve(e.data);
+ });
+
+ iframe.contentDocument.close();
+ }).then(({ tests, status }) => {
+ const summary = {
+ harness: getEnumProp(status, status.status),
+ tests: {}
+ };
+
+ tests.forEach((test) => {
+ summary.tests[test.name] = getEnumProp(test, test.status);
+ });
+
+ return summary;
+ });
+}
+
+function getEnumProp(object, value) {
+ for (let property in object) {
+ if (!/^[A-Z]+$/.test(property)) {
+ continue;
+ }
+
+ if (object[property] === value) {
+ return property;
+ }
+ }
+}
diff --git a/test/wpt/tests/resources/test/requirements.txt b/test/wpt/tests/resources/test/requirements.txt
new file mode 100644
index 0000000..95d87c3
--- /dev/null
+++ b/test/wpt/tests/resources/test/requirements.txt
@@ -0,0 +1 @@
+html5lib==1.1
diff --git a/test/wpt/tests/resources/test/tests/functional/abortsignal.html b/test/wpt/tests/resources/test/tests/functional/abortsignal.html
new file mode 100644
index 0000000..e6080e9
--- /dev/null
+++ b/test/wpt/tests/resources/test/tests/functional/abortsignal.html
@@ -0,0 +1,49 @@
+<!DOCTYPE html>
+<title>Test#get_signal</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+ "use strict";
+
+ setup(() => {
+ assert_implements_optional(window.AbortController, "No AbortController");
+ });
+
+ let signal;
+ let observed = false;
+
+ test(t => {
+ signal = t.get_signal();
+ assert_true(signal instanceof AbortSignal, "Returns an abort signal");
+ assert_false(signal.aborted, "Signal should not be aborted before test end");
+ signal.onabort = () => observed = true;
+ }, "t.signal existence");
+
+ test(t => {
+ assert_true(signal.aborted, "Signal should be aborted after test end");
+ assert_true(observed, "onabort should have been called");
+ }, "t.signal.aborted");
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "message": null,
+ "status_string": "OK"
+ },
+ "summarized_tests": [
+ {
+ "message": null,
+ "name": "t.signal existence",
+ "properties": {},
+ "status_string": "PASS"
+ },
+ {
+ "message": null,
+ "name": "t.signal.aborted",
+ "properties": {},
+ "status_string": "PASS"
+ }
+ ],
+ "type": "complete"
+}
+</script>
diff --git a/test/wpt/tests/resources/test/tests/functional/add_cleanup.html b/test/wpt/tests/resources/test/tests/functional/add_cleanup.html
new file mode 100644
index 0000000..468319f
--- /dev/null
+++ b/test/wpt/tests/resources/test/tests/functional/add_cleanup.html
@@ -0,0 +1,91 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Test#add_cleanup</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+</head>
+<body>
+<div id="log"></div>
+<script>
+"use strict";
+
+var log_sync;
+test(function(t) {
+ log_sync = "";
+ t.add_cleanup(function() { log_sync += "1"; });
+ t.add_cleanup(function() { log_sync += "2"; });
+ t.add_cleanup(function() { log_sync += "3"; });
+ t.add_cleanup(function() { log_sync += "4"; });
+ t.add_cleanup(function() { log_sync += "5"; });
+ log_sync += "0";
+}, "probe synchronous");
+
+test(function() {
+ if (log_sync !== "012345") {
+ throw new Error("Expected: '012345'. Actual: '" + log_sync + "'.");
+ }
+}, "Cleanup methods are invoked exactly once and in the expected sequence.");
+
+var complete, log_async;
+async_test(function(t) {
+ complete = t.step_func(function() {
+ if (log_async !== "012") {
+ throw new Error("Expected: '012'. Actual: '" + log_async + "'.");
+ }
+
+ t.done();
+ });
+}, "Cleanup methods are invoked following the completion of asynchronous tests");
+
+async_test(function(t) {
+ log_async = "";
+ t.add_cleanup(function() { log_async += "1"; });
+
+ setTimeout(t.step_func(function() {
+ t.add_cleanup(function() {
+ log_async += "2";
+ complete();
+ });
+ log_async += "0";
+ t.done();
+ }), 0);
+}, "probe asynchronous");
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "OK",
+ "message": null
+ },
+ "summarized_tests": [
+ {
+ "status_string": "PASS",
+ "name": "Cleanup methods are invoked exactly once and in the expected sequence.",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "Cleanup methods are invoked following the completion of asynchronous tests",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "probe asynchronous",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "probe synchronous",
+ "properties": {},
+ "message": null
+ }
+ ],
+ "type": "complete"
+}
+</script>
+</body>
+</html>
diff --git a/test/wpt/tests/resources/test/tests/functional/add_cleanup_async.html b/test/wpt/tests/resources/test/tests/functional/add_cleanup_async.html
new file mode 100644
index 0000000..07ade4b
--- /dev/null
+++ b/test/wpt/tests/resources/test/tests/functional/add_cleanup_async.html
@@ -0,0 +1,85 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Test#add_cleanup with Promise-returning functions</title>
+<script src="../../../testharness.js"></script>
+<script src="../../../testharnessreport.js"></script>
+</head>
+<body>
+<div id="log"></div>
+
+<script>
+"use strict";
+var completeCount = 0;
+var counts = {
+ afterTick: null,
+ afterFirst: null
+};
+
+add_result_callback(function(result_t) {
+ completeCount += 1;
+});
+
+promise_test(function(t) {
+ t.add_cleanup(function() {
+ return new Promise(function(resolve) {
+ setTimeout(function() {
+ counts.afterTick = completeCount;
+ resolve();
+ }, 0);
+ });
+ });
+ t.add_cleanup(function() {
+ return new Promise(function(resolve) {
+
+ setTimeout(function() {
+ counts.afterFirst = completeCount;
+ resolve();
+ }, 0);
+ });
+ });
+
+ return Promise.resolve();
+}, 'promise_test with asynchronous cleanup');
+
+promise_test(function() {
+ assert_equals(
+ counts.afterTick,
+ 0,
+ "test is not asynchronously considered 'complete'"
+ );
+ assert_equals(
+ counts.afterFirst,
+ 0,
+ "test is not considered 'complete' following fulfillment of first promise"
+ );
+ assert_equals(completeCount, 1);
+
+ return Promise.resolve();
+}, "synchronously-defined promise_test");
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "OK",
+ "message": null
+ },
+ "summarized_tests": [
+ {
+ "status_string": "PASS",
+ "name": "promise_test with asynchronous cleanup",
+ "message": null,
+ "properties": {}
+ },
+ {
+ "status_string": "PASS",
+ "name": "synchronously-defined promise_test",
+ "message": null,
+ "properties": {}
+ }
+ ],
+ "type": "complete"
+}
+</script>
+</body>
+</html>
diff --git a/test/wpt/tests/resources/test/tests/functional/add_cleanup_async_bad_return.html b/test/wpt/tests/resources/test/tests/functional/add_cleanup_async_bad_return.html
new file mode 100644
index 0000000..867bde2
--- /dev/null
+++ b/test/wpt/tests/resources/test/tests/functional/add_cleanup_async_bad_return.html
@@ -0,0 +1,50 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Test#add_cleanup with non-thenable-returning function</title>
+<script src="../../../testharness.js"></script>
+<script src="../../../testharnessreport.js"></script>
+</head>
+<body>
+<div id="log"></div>
+
+<script>
+"use strict";
+
+promise_test(function(t) {
+ t.add_cleanup(function() {});
+ t.add_cleanup(function() {
+ return { then: 9 };
+ });
+ t.add_cleanup(function() { return Promise.resolve(); });
+
+ return Promise.resolve();
+}, "promise_test that returns a non-thenable object in one \"cleanup\" callback");
+
+promise_test(function() {}, "The test runner is in an unpredictable state ('NOT RUN')");
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "ERROR",
+ "message": "Test named 'promise_test that returns a non-thenable object in one \"cleanup\" callback' specified 3 'cleanup' functions, and 1 returned a non-thenable value."
+ },
+ "summarized_tests": [
+ {
+ "status_string": "NOTRUN",
+ "name": "The test runner is in an unpredictable state ('NOT RUN')",
+ "message": null,
+ "properties": {}
+ },
+ {
+ "status_string": "PASS",
+ "name": "promise_test that returns a non-thenable object in one \"cleanup\" callback",
+ "message": null,
+ "properties": {}
+ }
+ ],
+ "type": "complete"
+}
+</script>
+</body>
+</html>
diff --git a/test/wpt/tests/resources/test/tests/functional/add_cleanup_async_rejection.html b/test/wpt/tests/resources/test/tests/functional/add_cleanup_async_rejection.html
new file mode 100644
index 0000000..e51465e
--- /dev/null
+++ b/test/wpt/tests/resources/test/tests/functional/add_cleanup_async_rejection.html
@@ -0,0 +1,94 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Test#add_cleanup with Promise-returning functions (rejection handling)</title>
+<script src="../../../testharness.js"></script>
+<script src="../../../testharnessreport.js"></script>
+</head>
+<body>
+<div id="log"></div>
+
+<script>
+"use strict";
+var resolve, reject;
+var completeCount = 0;
+add_result_callback(function(result_t) {
+ completeCount += 1;
+});
+promise_test(function(t) {
+ t.add_cleanup(function() {
+ return new Promise(function(_, _reject) { reject = _reject; });
+ });
+ t.add_cleanup(function() {
+ return new Promise(function(_resolve) { resolve = _resolve; });
+ });
+
+ // The following cleanup function defines empty tests so that the reported
+ // data demonstrates the intended run-time behavior without relying on the
+ // test harness's handling of errors during test cleanup (which is tested
+ // elsewhere).
+ t.add_cleanup(function() {
+ if (completeCount === 0) {
+ promise_test(
+ function() {},
+ "test is not asynchronously considered 'complete' ('NOT RUN')"
+ );
+ }
+
+ reject();
+
+ setTimeout(function() {
+ if (completeCount === 0) {
+ promise_test(
+ function() {},
+ "test is not considered 'complete' following rejection of first " +
+ "promise ('NOT RUN')"
+ );
+ }
+
+ resolve();
+ }, 0);
+ });
+
+ return Promise.resolve();
+}, "promise_test with asynchronous cleanup including rejection");
+
+promise_test(function() {}, "synchronously-defined test ('NOT RUN')");
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "ERROR",
+ "message": "Test named 'promise_test with asynchronous cleanup including rejection' specified 3 'cleanup' functions, and 1 failed."
+ },
+ "summarized_tests": [
+ {
+ "status_string": "PASS",
+ "name": "promise_test with asynchronous cleanup including rejection",
+ "message": null,
+ "properties": {}
+ },
+ {
+ "status_string": "NOTRUN",
+ "name": "synchronously-defined test ('NOT RUN')",
+ "message": null,
+ "properties": {}
+ },
+ {
+ "status_string": "NOTRUN",
+ "name": "test is not asynchronously considered 'complete' ('NOT RUN')",
+ "message": null,
+ "properties": {}
+ },
+ {
+ "status_string": "NOTRUN",
+ "name": "test is not considered 'complete' following rejection of first promise ('NOT RUN')",
+ "message": null,
+ "properties": {}
+ }
+ ],
+ "type": "complete"
+}
+</script>
+</body>
+</html>
diff --git a/test/wpt/tests/resources/test/tests/functional/add_cleanup_async_rejection_after_load.html b/test/wpt/tests/resources/test/tests/functional/add_cleanup_async_rejection_after_load.html
new file mode 100644
index 0000000..f9b2846
--- /dev/null
+++ b/test/wpt/tests/resources/test/tests/functional/add_cleanup_async_rejection_after_load.html
@@ -0,0 +1,52 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Test#add_cleanup with Promise-returning functions (rejection handling following "load" event)</title>
+</head>
+<body>
+<h1>Promise Tests</h1>
+<p>This test demonstrates the use of <tt>promise_test</tt>. Assumes ECMAScript 6
+Promise support. Some failures are expected.</p>
+<div id="log"></div>
+<script src="../../../testharness.js"></script>
+<script src="../../../testharnessreport.js"></script>
+<script>
+promise_test(function(t) {
+ t.add_cleanup(function() {
+ return Promise.reject(new Error("foo"));
+ });
+
+ return new Promise((resolve) => {
+ document.addEventListener("DOMContentLoaded", function() {
+ setTimeout(resolve, 0)
+ });
+ });
+}, "Test with failing cleanup that completes after DOMContentLoaded event");
+
+promise_test(function(t) {
+ return Promise.resolve();
+}, "Test that should not be run due to invalid harness state ('NOT RUN')");
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "ERROR",
+ "message": "Test named 'Test with failing cleanup that completes after DOMContentLoaded event' specified 1 'cleanup' function, and 1 failed."
+ },
+ "summarized_tests": [
+ {
+ "status_string": "NOTRUN",
+ "name": "Test that should not be run due to invalid harness state ('NOT RUN')",
+ "message": null,
+ "properties": {}
+ },
+ {
+ "status_string": "PASS",
+ "name": "Test with failing cleanup that completes after DOMContentLoaded event",
+ "message": null,
+ "properties": {}
+ }
+ ],
+ "type": "complete"
+}
+</script>
diff --git a/test/wpt/tests/resources/test/tests/functional/add_cleanup_async_timeout.html b/test/wpt/tests/resources/test/tests/functional/add_cleanup_async_timeout.html
new file mode 100644
index 0000000..429536c
--- /dev/null
+++ b/test/wpt/tests/resources/test/tests/functional/add_cleanup_async_timeout.html
@@ -0,0 +1,57 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Test#add_cleanup with Promise-returning functions (timeout handling)</title>
+<script src="../../../testharness.js"></script>
+<script src="../../../testharnessreport.js"></script>
+</head>
+<body>
+<div id="log"></div>
+<script>
+"use strict";
+
+promise_test(function(t) {
+ t.add_cleanup(function() {
+ return Promise.resolve();
+ });
+
+ t.add_cleanup(function() {
+ return new Promise(function() {});
+ });
+
+ t.add_cleanup(function() {});
+
+ t.add_cleanup(function() {
+ return new Promise(function() {});
+ });
+
+ return Promise.resolve();
+}, "promise_test with asynchronous cleanup");
+
+promise_test(function() {}, "promise_test following timed out cleanup ('NOT RUN')");
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "ERROR",
+ "message": "Timeout while running cleanup for test named \"promise_test with asynchronous cleanup\"."
+ },
+ "summarized_tests": [
+ {
+ "status_string": "NOTRUN",
+ "name": "promise_test following timed out cleanup ('NOT RUN')",
+ "message": null,
+ "properties": {}
+ },
+ {
+ "status_string": "PASS",
+ "name": "promise_test with asynchronous cleanup",
+ "message": null,
+ "properties": {}
+ }
+ ],
+ "type": "complete"
+}
+</script>
+</body>
+</html>
diff --git a/test/wpt/tests/resources/test/tests/functional/add_cleanup_bad_return.html b/test/wpt/tests/resources/test/tests/functional/add_cleanup_bad_return.html
new file mode 100644
index 0000000..3cfb28a
--- /dev/null
+++ b/test/wpt/tests/resources/test/tests/functional/add_cleanup_bad_return.html
@@ -0,0 +1,61 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Test#add_cleanup with value-returning function</title>
+<script src="../../../testharness.js"></script>
+<script src="../../../testharnessreport.js"></script>
+</head>
+<body>
+<div id="log"></div>
+<script>
+"use strict";
+
+test(function(t) {
+ t.add_cleanup(function() {});
+ t.add_cleanup(function() { return null; });
+ t.add_cleanup(function() {
+ test(
+ function() {},
+ "The test runner is in an unpredictable state #1 ('NOT RUN')"
+ );
+
+ throw new Error();
+ });
+ t.add_cleanup(function() { return 4; });
+ t.add_cleanup(function() { return { then: function() {} }; });
+ t.add_cleanup(function() {});
+}, "Test that returns a value in three \"cleanup\" functions");
+
+test(function() {}, "The test runner is in an unpredictable state #2 ('NOT RUN')");
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "ERROR",
+ "message": "Test named 'Test that returns a value in three \"cleanup\" functions' specified 6 'cleanup' functions, and 1 failed, and 3 returned a non-undefined value."
+ },
+ "summarized_tests": [
+ {
+ "status_string": "PASS",
+ "name": "Test that returns a value in three \"cleanup\" functions",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "NOTRUN",
+ "name": "The test runner is in an unpredictable state #1 ('NOT RUN')",
+ "message": null,
+ "properties": {}
+ },
+ {
+ "status_string": "NOTRUN",
+ "name": "The test runner is in an unpredictable state #2 ('NOT RUN')",
+ "message": null,
+ "properties": {}
+ }
+ ],
+ "type": "complete"
+}
+</script>
+</body>
+</html>
diff --git a/test/wpt/tests/resources/test/tests/functional/add_cleanup_count.html b/test/wpt/tests/resources/test/tests/functional/add_cleanup_count.html
new file mode 100644
index 0000000..2c9b51c
--- /dev/null
+++ b/test/wpt/tests/resources/test/tests/functional/add_cleanup_count.html
@@ -0,0 +1,39 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Test#add_cleanup reported count</title>
+<script src="../../../testharness.js"></script>
+<script src="../../../testharnessreport.js"></script>
+</head>
+<body>
+<div id="log"></div>
+
+<script>
+promise_test(function(t) {
+ t.add_cleanup(function() {});
+ t.add_cleanup(function() {});
+ t.add_cleanup(function() { throw new Error(); });
+ new EventWatcher(t, document.body, []);
+
+ return Promise.resolve();
+}, 'test with 3 user-defined cleanup functions');
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "ERROR",
+ "message": "Test named 'test with 3 user-defined cleanup functions' specified 3 'cleanup' functions, and 1 failed."
+ },
+ "summarized_tests": [
+ {
+ "status_string": "PASS",
+ "name": "test with 3 user-defined cleanup functions",
+ "message": null,
+ "properties": {}
+ }
+ ],
+ "type": "complete"
+}
+</script>
+</body>
+</html>
diff --git a/test/wpt/tests/resources/test/tests/functional/add_cleanup_err.html b/test/wpt/tests/resources/test/tests/functional/add_cleanup_err.html
new file mode 100644
index 0000000..60357c6
--- /dev/null
+++ b/test/wpt/tests/resources/test/tests/functional/add_cleanup_err.html
@@ -0,0 +1,45 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Test#add_cleanup: error</title>
+<script src="../../../testharness.js"></script>
+<script src="../../../testharnessreport.js"></script>
+</head>
+<body>
+<div id="log"></div>
+<script>
+"use strict";
+
+test(function(t) {
+ t.add_cleanup(function() {
+ throw new Error('exception in cleanup function');
+ });
+}, "Exception in cleanup function causes harness failure.");
+
+test(function() {}, "This test should not be run.");
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "ERROR",
+ "message": "Test named 'Exception in cleanup function causes harness failure.' specified 1 'cleanup' function, and 1 failed."
+ },
+ "summarized_tests": [
+ {
+ "status_string": "PASS",
+ "name": "Exception in cleanup function causes harness failure.",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "NOTRUN",
+ "name": "This test should not be run.",
+ "properties": {},
+ "message": null
+ }
+ ],
+ "type": "complete"
+}
+</script>
+</body>
+</html>
diff --git a/test/wpt/tests/resources/test/tests/functional/add_cleanup_err_multi.html b/test/wpt/tests/resources/test/tests/functional/add_cleanup_err_multi.html
new file mode 100644
index 0000000..80ba1b4
--- /dev/null
+++ b/test/wpt/tests/resources/test/tests/functional/add_cleanup_err_multi.html
@@ -0,0 +1,52 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Test#add_cleanup: multiple functions with one in error</title>
+<script src="../../../testharness.js"></script>
+<script src="../../../testharnessreport.js"></script>
+</head>
+<body>
+<div id="log"></div>
+
+<script>
+"use strict";
+
+test(function(t) {
+ t.add_cleanup(function() {
+ throw new Error("exception in cleanup function");
+ });
+
+ // The following cleanup function defines a test so that the reported
+ // data demonstrates the intended run-time behavior, i.e. that
+ // `testharness.js` invokes all cleanup functions even when one or more
+ // throw errors.
+ t.add_cleanup(function() {
+ test(function() {}, "Verification test");
+ });
+ }, "Test with multiple cleanup functions");
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "ERROR",
+ "message": "Test named 'Test with multiple cleanup functions' specified 2 'cleanup' functions, and 1 failed."
+ },
+ "summarized_tests": [
+ {
+ "status_string": "PASS",
+ "name": "Test with multiple cleanup functions",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "NOTRUN",
+ "name": "Verification test",
+ "properties": {},
+ "message": null
+ }
+ ],
+ "type": "complete"
+}
+</script>
+</body>
+</html>
diff --git a/test/wpt/tests/resources/test/tests/functional/add_cleanup_sync_queue.html b/test/wpt/tests/resources/test/tests/functional/add_cleanup_sync_queue.html
new file mode 100644
index 0000000..0a61503
--- /dev/null
+++ b/test/wpt/tests/resources/test/tests/functional/add_cleanup_sync_queue.html
@@ -0,0 +1,55 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Test#add_cleanup: queuing tests</title>
+<script src="../../../testharness.js"></script>
+<script src="../../../testharnessreport.js"></script>
+</head>
+<body>
+<div id="log"></div>
+
+<script>
+"use strict";
+var firstCleaned = false;
+
+promise_test(function(t) {
+ promise_test(function() {
+ assert_true(
+ firstCleaned, "should not execute until first test is complete"
+ );
+
+ return Promise.resolve();
+ }, "test defined when no tests are queued, but one test is executing");
+
+ t.add_cleanup(function() {
+ firstCleaned = true;
+ });
+
+ return Promise.resolve();
+}, "Test with a 'cleanup' function");
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "message": null,
+ "status_string": "OK"
+ },
+ "summarized_tests": [
+ {
+ "message": null,
+ "name": "Test with a 'cleanup' function",
+ "status_string": "PASS",
+ "properties": {}
+ },
+ {
+ "message": null,
+ "name": "test defined when no tests are queued, but one test is executing",
+ "status_string": "PASS",
+ "properties": {}
+ }
+ ],
+ "type": "complete"
+}
+</script>
+</body>
+</html>
diff --git a/test/wpt/tests/resources/test/tests/functional/api-tests-1.html b/test/wpt/tests/resources/test/tests/functional/api-tests-1.html
new file mode 100644
index 0000000..9de875b
--- /dev/null
+++ b/test/wpt/tests/resources/test/tests/functional/api-tests-1.html
@@ -0,0 +1,991 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Sample HTML5 API Tests</title>
+<meta name="timeout" content="6000">
+</head>
+<body onload="load_test_attr.done()">
+<h1>Sample HTML5 API Tests</h1>
+<div id="log"></div>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+ setup_run = false;
+ setup(function() {
+ setup_run = true;
+ });
+ test(function() {assert_true(setup_run)}, "Setup function ran");
+
+ // Two examples for testing events from handler and attributes
+ var load_test_event = async_test("window onload event fires when set from the handler");
+
+ function windowLoad()
+ {
+ load_test_event.done();
+ }
+ on_event(window, "load", windowLoad);
+
+ test(function() {
+ var sequence = [];
+ var outer = document.createElement("div");
+ var inner = document.createElement("div");
+ outer.appendChild(inner);
+ document.body.appendChild(outer);
+ inner.addEventListener("click", function() {
+ sequence.push("inner");
+ }, false);
+
+ on_event(outer, "click", function() {
+ sequence.push("outer");
+ });
+ inner.click();
+
+ assert_array_equals(sequence, ["inner", "outer"]);
+ }, "on_event does not use event capture");
+
+ // see the body onload below
+ var load_test_attr = async_test("body element fires the onload event set from the attribute");
+</script>
+<script>
+ function bodyElement()
+ {
+ assert_equals(document.body, document.getElementsByTagName("body")[0]);
+ }
+ test(bodyElement, "document.body should be the first body element in the document");
+
+ test(function() {
+ assert_equals(1,1);
+ assert_equals(NaN, NaN, "NaN case");
+ assert_equals(0, 0, "Zero case");
+ }, "assert_equals tests")
+
+ test(function() {
+ assert_equals(-0, 0, "Zero case");
+ }, "assert_equals tests expected to fail")
+
+ test(function() {
+ assert_not_equals({}, {}, "object case");
+ assert_not_equals(-0, 0, "Zero case");
+ }, "assert_not_equals tests")
+
+ function testAssertPass()
+ {
+ assert_true(true);
+ }
+ test(testAssertPass, "assert_true expected to pass");
+
+ function testAssertFalse()
+ {
+ assert_true(false, "false should not be true");
+ }
+ test(testAssertFalse, "assert_true expected to fail");
+
+ function basicAssertArrayEquals()
+ {
+ assert_array_equals([1, NaN], [1, NaN], "[1, NaN] is equal to [1, NaN]");
+ }
+ test(basicAssertArrayEquals, "basic assert_array_equals test");
+
+ function assertArrayEqualsUndefined()
+ {
+ assert_array_equals(undefined, [1], "undefined equals [1]?");
+ }
+ test(assertArrayEqualsUndefined, "assert_array_equals with first param undefined");
+
+ function assertArrayEqualsTrue()
+ {
+ assert_array_equals(true, [1], "true equals [1]?");
+ }
+ test(assertArrayEqualsTrue, "assert_array_equals with first param true");
+
+ function assertArrayEqualsFalse()
+ {
+ assert_array_equals(false, [1], "false equals [1]?");
+ }
+ test(assertArrayEqualsFalse, "assert_array_equals with first param false");
+
+ function assertArrayEqualsNull()
+ {
+ assert_array_equals(null, [1], "null equals [1]?");
+ }
+ test(assertArrayEqualsNull, "assert_array_equals with first param null");
+
+ function assertArrayEqualsNumeric()
+ {
+ assert_array_equals(1, [1], "1 equals [1]?");
+ }
+ test(assertArrayEqualsNumeric, "assert_array_equals with first param 1");
+
+ function basicAssertArrayApproxEquals()
+ {
+ assert_array_approx_equals([10, 11], [11, 10], 1, "[10, 11] is approximately (+/- 1) [11, 10]")
+ }
+ test(basicAssertArrayApproxEquals, "basic assert_array_approx_equals test");
+
+ function basicAssertApproxEquals()
+ {
+ assert_approx_equals(10, 11, 1, "10 is approximately (+/- 1) 11")
+ }
+ test(basicAssertApproxEquals, "basic assert_approx_equals test");
+
+ function basicAssertLessThan()
+ {
+ assert_less_than(10, 11, "10 is less than 11")
+ }
+ test(basicAssertApproxEquals, "basic assert_less_than test");
+
+ function basicAssertGreaterThan()
+ {
+ assert_greater_than(10, 11, "10 is not greater than 11");
+ }
+ test(basicAssertGreaterThan, "assert_greater_than expected to fail");
+
+ function basicAssertGreaterThanEqual()
+ {
+ assert_greater_than_equal(10, 10, "10 is greater than or equal to 10")
+ }
+ test(basicAssertGreaterThanEqual, "basic assert_greater_than_equal test");
+
+ function basicAssertLessThanEqual()
+ {
+ assert_greater_than_equal('10', 10, "'10' is not a number")
+ }
+ test(basicAssertLessThanEqual, "assert_less_than_equal expected to fail");
+
+ function testAssertInherits() {
+ var A = function(){this.a = "a"}
+ A.prototype = {b:"b"}
+ var a = new A();
+ assert_own_property(a, "a");
+ assert_not_own_property(a, "b", "unexpected property found: \"b\"");
+ assert_inherits(a, "b");
+ }
+ test(testAssertInherits, "test for assert[_not]_own_property and insert_inherits")
+
+ test(function()
+ {
+ var a = document.createElement("a")
+ var b = document.createElement("b")
+ assert_throws_dom("NOT_FOUND_ERR", function () {a.removeChild(b)});
+ }, "Test throw DOM exception")
+
+ test(function()
+ {
+ var a = document.createElement("a")
+ var b = document.createElement("b")
+ assert_throws_js(DOMException, function () {a.removeChild(b)});
+ }, "Test throw DOMException as JS exception expected to fail")
+
+ test(function()
+ {
+ assert_throws_js(SyntaxError, function () {document.querySelector("")});
+ }, "Test throw SyntaxError DOMException where JS SyntaxError expected; expected to fail")
+
+ test(function()
+ {
+ assert_throws_js(SyntaxError, function () {JSON.parse("{")});
+ }, "Test throw JS SyntaxError")
+
+ test(function()
+ {
+ assert_throws_dom("SyntaxError", function () {document.querySelector("")});
+ }, "Test throw DOM SyntaxError")
+
+ test(function()
+ {
+ var ifr = document.createElement("iframe");
+ document.body.appendChild(ifr);
+ this.add_cleanup(() => ifr.remove());
+ assert_throws_dom("SyntaxError", ifr.contentWindow.DOMException,
+ function () {ifr.contentDocument.querySelector("")});
+ }, "Test throw DOM SyntaxError from subframe");
+
+ test(function()
+ {
+ var ifr = document.createElement("iframe");
+ document.body.appendChild(ifr);
+ this.add_cleanup(() => ifr.remove());
+ assert_throws_dom("SyntaxError",
+ function () {ifr.contentDocument.querySelector("")});
+ }, "Test throw DOM SyntaxError from subframe with incorrect global expectation; expected to fail");
+
+ test(function()
+ {
+ var ifr = document.createElement("iframe");
+ document.body.appendChild(ifr);
+ this.add_cleanup(() => ifr.remove());
+ assert_throws_dom("SyntaxError", ifr.contentWindow.DOMException,
+ function () {document.querySelector("")});
+ }, "Test throw DOM SyntaxError with incorrect expectation; expected to fail");
+
+ test(function()
+ {
+ assert_throws_dom("SyntaxError", function () {JSON.parse("{")});
+ }, "Test throw JS SyntaxError where SyntaxError DOMException expected; expected to fail")
+
+ test(function()
+ {
+ var a = document.createTextNode("a")
+ var b = document.createElement("b")
+ assert_throws_dom("NOT_FOUND_ERR", function () {a.appendChild(b)});
+ }, "Test throw DOM exception expected to fail")
+
+ test(function()
+ {
+ var e = new DOMException("I am not known", "TEST_ERROR_NO_SUCH_THING");
+ assert_throws_dom(0, function() {throw e});
+ }, "Test assert_throws_dom with ambiguous DOM-exception expected to Fail");
+
+ test(function()
+ {
+ var e = {code:0, name:"TEST_ERR", TEST_ERR:0};
+ e.constructor = DOMException;
+ assert_throws_dom("TEST_ERR", function() {throw e});
+ }, "Test assert_throws_dom with non-DOM-exception expected to Fail");
+
+ test(function()
+ {
+ var e = {code: DOMException.SYNTAX_ERR, name:"SyntaxError"};
+ e.constructor = DOMException;
+ assert_throws_dom(DOMException.SYNTAX_ERR, function() {throw e});
+ }, "Test assert_throws_dom with number code value expected to Pass");
+
+ test(function()
+ {
+ var e = new DOMException("Some message", "SyntaxError");
+ assert_throws_dom(DOMException.SYNTAX_ERR, function() {throw e});
+ }, "Test assert_throws_dom with number code value and real DOMException expected to Pass");
+
+ var t = async_test("Test step_func")
+ setTimeout(
+ t.step_func(
+ function () {
+ assert_true(true); t.done();
+ }), 0);
+
+ async_test(function(t) {
+ setTimeout(t.step_func(function (){assert_true(true); t.done();}), 0);
+ }, "Test async test with callback");
+
+ async_test(function() {
+ setTimeout(this.step_func(function (){assert_true(true); this.done();}), 0);
+ }, "Test async test with callback and `this` obj.");
+
+ async_test("test should timeout (fail) with the default of 2 seconds").step(function(){});
+
+ async_test("async test that is never started, should have status Not Run");
+
+
+ test(function(t) {
+ window.global = 1;
+ t.add_cleanup(function() {delete window.global});
+ assert_equals(window.global, 1);
+ },
+ "Test that defines a global and cleans it up");
+
+ test(function() {assert_equals(window.global, undefined)},
+ "Test that cleanup handlers from previous test ran");
+
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "TIMEOUT",
+ "message": null
+ },
+ "summarized_tests": [
+ {
+ "status_string": "PASS",
+ "name": "Setup function ran",
+ "message": null,
+ "properties": {}
+ },
+ {
+ "status_string": "FAIL",
+ "name": "Test assert_throws_dom with ambiguous DOM-exception expected to Fail",
+ "message": "Test bug: ambiguous DOMException code 0 passed to assert_throws_dom()",
+ "properties": {}
+ },
+ {
+ "status_string": "FAIL",
+ "name": "Test assert_throws_dom with non-DOM-exception expected to Fail",
+ "message": "Test bug: unrecognized DOMException code name or name \"TEST_ERR\" passed to assert_throws_dom()",
+ "properties": {}
+ },
+ {
+ "status_string": "PASS",
+ "name": "Test assert_throws_dom with number code value expected to Pass",
+ "message": null,
+ "properties": {}
+ },
+ {
+ "status_string": "PASS",
+ "name": "Test assert_throws_dom with number code value and real DOMException expected to Pass",
+ "message": null,
+ "properties": {}
+ },
+ {
+ "status_string": "PASS",
+ "name": "Test async test with callback",
+ "message": null,
+ "properties": {}
+ },
+ {
+ "status_string": "PASS",
+ "name": "Test async test with callback and `this` obj.",
+ "message": null,
+ "properties": {}
+ },
+ {
+ "status_string": "PASS",
+ "name": "Test step_func",
+ "message": null,
+ "properties": {}
+ },
+ {
+ "status_string": "PASS",
+ "name": "Test that cleanup handlers from previous test ran",
+ "message": null,
+ "properties": {}
+ },
+ {
+ "status_string": "PASS",
+ "name": "Test that defines a global and cleans it up",
+ "message": null,
+ "properties": {}
+ },
+ {
+ "status_string": "PASS",
+ "name": "Test throw DOM exception",
+ "message": null,
+ "properties": {}
+ },
+ {
+ "status_string": "FAIL",
+ "name": "Test throw DOMException as JS exception expected to fail",
+ "message": "assert_throws_js: function \"function DOMException() {\n [native code]\n}\" is not an Error subtype",
+ "properties": {}
+ },
+ {
+ "status_string": "FAIL",
+ "name": "Test throw SyntaxError DOMException where JS SyntaxError expected; expected to fail",
+ "message": "assert_throws_js: function \"function () {document.querySelector(\"\")}\" threw object \"SyntaxError: Document.querySelector: '' is not a valid selector\" (\"SyntaxError\") expected instance of function \"function SyntaxError() {\n [native code]\n}\" (\"SyntaxError\")",
+ "properties": {}
+ },
+ {
+ "status_string": "PASS",
+ "name": "Test throw JS SyntaxError",
+ "message": null,
+ "properties": {}
+ },
+ {
+ "status_string": "PASS",
+ "name": "Test throw DOM SyntaxError",
+ "message": null,
+ "properties": {}
+ },
+ {
+ "status_string": "PASS",
+ "name": "Test throw DOM SyntaxError from subframe",
+ "message": null,
+ "properties": {}
+ },
+ {
+ "status_string": "FAIL",
+ "name": "Test throw DOM SyntaxError from subframe with incorrect global expectation; expected to fail",
+ "message": "assert_throws_dom: function \"function () {ifr.contentDocument.querySelector(\"\")}\" threw an exception from the wrong global",
+ "properties": {}
+ },
+ {
+ "status_string": "FAIL",
+ "name": "Test throw DOM SyntaxError with incorrect expectation; expected to fail",
+ "message": "assert_throws_dom: function \"function () {document.querySelector(\"\")}\" threw an exception from the wrong global",
+ "properties": {}
+ },
+ {
+ "status_string": "FAIL",
+ "name": "Test throw JS SyntaxError where SyntaxError DOMException expected; expected to fail",
+ "message": "assert_throws_dom: function \"function () {JSON.parse(\"{\")}\" threw object \"SyntaxError: JSON.parse: end of data while reading object contents at line 1 column 2 of the JSON data\" that is not a DOMException SyntaxError: property \"code\" is equal to undefined, expected 12",
+ "properties": {}
+ },
+ {
+ "status_string": "FAIL",
+ "name": "Test throw DOM exception expected to fail",
+ "message": "assert_throws_dom: function \"function () {a.appendChild(b)}\" threw object \"HierarchyRequestError: Node.appendChild: Cannot add children to a Text\" that is not a DOMException NOT_FOUND_ERR: property \"code\" is equal to 3, expected 8",
+ "properties": {}
+ },
+ {
+ "status_string": "FAIL",
+ "name": "assert_array_equals with first param 1",
+ "message": "assert_array_equals: 1 equals [1]? value is 1, expected array",
+ "properties": {}
+ },
+ {
+ "status_string": "FAIL",
+ "name": "assert_array_equals with first param false",
+ "message": "assert_array_equals: false equals [1]? value is false, expected array",
+ "properties": {}
+ },
+ {
+ "status_string": "FAIL",
+ "name": "assert_array_equals with first param null",
+ "message": "assert_array_equals: null equals [1]? value is null, expected array",
+ "properties": {}
+ },
+ {
+ "status_string": "FAIL",
+ "name": "assert_array_equals with first param true",
+ "message": "assert_array_equals: true equals [1]? value is true, expected array",
+ "properties": {}
+ },
+ {
+ "status_string": "FAIL",
+ "name": "assert_array_equals with first param undefined",
+ "message": "assert_array_equals: undefined equals [1]? value is undefined, expected array",
+ "properties": {}
+ },
+ {
+ "status_string": "PASS",
+ "name": "assert_equals tests",
+ "message": null,
+ "properties": {}
+ },
+ {
+ "status_string": "FAIL",
+ "name": "assert_equals tests expected to fail",
+ "message": "assert_equals: Zero case expected 0 but got -0",
+ "properties": {}
+ },
+ {
+ "status_string": "FAIL",
+ "name": "assert_greater_than expected to fail",
+ "message": "assert_greater_than: 10 is not greater than 11 expected a number greater than 11 but got 10",
+ "properties": {}
+ },
+ {
+ "status_string": "FAIL",
+ "name": "assert_less_than_equal expected to fail",
+ "message": "assert_greater_than_equal: '10' is not a number expected a number but got a \"string\"",
+ "properties": {}
+ },
+ {
+ "status_string": "PASS",
+ "name": "assert_not_equals tests",
+ "message": null,
+ "properties": {}
+ },
+ {
+ "status_string": "FAIL",
+ "name": "assert_true expected to fail",
+ "message": "assert_true: false should not be true expected true got false",
+ "properties": {}
+ },
+ {
+ "status_string": "PASS",
+ "name": "assert_true expected to pass",
+ "message": null,
+ "properties": {}
+ },
+ {
+ "status_string": "NOTRUN",
+ "name": "async test that is never started, should have status Not Run",
+ "message": null,
+ "properties": {}
+ },
+ {
+ "status_string": "PASS",
+ "name": "basic assert_approx_equals test",
+ "message": null,
+ "properties": {}
+ },
+ {
+ "status_string": "PASS",
+ "name": "basic assert_array_approx_equals test",
+ "message": null,
+ "properties": {}
+ },
+ {
+ "status_string": "PASS",
+ "name": "basic assert_array_equals test",
+ "message": null,
+ "properties": {}
+ },
+ {
+ "status_string": "PASS",
+ "name": "basic assert_greater_than_equal test",
+ "message": null,
+ "properties": {}
+ },
+ {
+ "status_string": "PASS",
+ "name": "basic assert_less_than test",
+ "message": null,
+ "properties": {}
+ },
+ {
+ "status_string": "PASS",
+ "name": "body element fires the onload event set from the attribute",
+ "message": null,
+ "properties": {}
+ },
+ {
+ "status_string": "PASS",
+ "name": "document.body should be the first body element in the document",
+ "message": null,
+ "properties": {}
+ },
+ {
+ "status_string": "PASS",
+ "name": "test for assert[_not]_own_property and insert_inherits",
+ "message": null,
+ "properties": {}
+ },
+ {
+ "status_string": "TIMEOUT",
+ "name": "test should timeout (fail) with the default of 2 seconds",
+ "message": "Test timed out",
+ "properties": {}
+ },
+ {
+ "status_string": "PASS",
+ "name": "window onload event fires when set from the handler",
+ "message": null,
+ "properties": {}
+ },
+ {
+ "status_string": "PASS",
+ "name": "on_event does not use event capture",
+ "message": null,
+ "properties": {}
+ }
+ ],
+ "summarized_asserts": [
+ {
+ "assert_name": "assert_true",
+ "test": "Setup function ran",
+ "args": [
+ "true"
+ ],
+ "status": 0
+ },
+ {
+ "assert_name": "assert_array_equals",
+ "test": "on_event does not use event capture",
+ "args": [
+ "[\"inner\", \"outer\"]",
+ "[\"inner\", \"outer\"]"
+ ],
+ "status": 0
+ },
+ {
+ "assert_name": "assert_equals",
+ "test": "document.body should be the first body element in the document",
+ "args": [
+ "Element node <body onload=\"load_test_attr.done()\"> <h1>Sample HTML5 AP...",
+ "Element node <body onload=\"load_test_attr.done()\"> <h1>Sample HTML5 AP..."
+ ],
+ "status": 0
+ },
+ {
+ "assert_name": "assert_equals",
+ "test": "assert_equals tests",
+ "args": [
+ "1",
+ "1"
+ ],
+ "status": 0
+ },
+ {
+ "assert_name": "assert_equals",
+ "test": "assert_equals tests",
+ "args": [
+ "NaN",
+ "NaN",
+ "\"NaN case\""
+ ],
+ "status": 0
+ },
+ {
+ "assert_name": "assert_equals",
+ "test": "assert_equals tests",
+ "args": [
+ "0",
+ "0",
+ "\"Zero case\""
+ ],
+ "status": 0
+ },
+ {
+ "assert_name": "assert_equals",
+ "test": "assert_equals tests expected to fail",
+ "args": [
+ "-0",
+ "0",
+ "\"Zero case\""
+ ],
+ "status": 1
+ },
+ {
+ "assert_name": "assert_not_equals",
+ "test": "assert_not_equals tests",
+ "args": [
+ "object \"[object Object]\"",
+ "object \"[object Object]\"",
+ "\"object case\""
+ ],
+ "status": 0
+ },
+ {
+ "assert_name": "assert_not_equals",
+ "test": "assert_not_equals tests",
+ "args": [
+ "-0",
+ "0",
+ "\"Zero case\""
+ ],
+ "status": 0
+ },
+ {
+ "assert_name": "assert_true",
+ "test": "assert_true expected to pass",
+ "args": [
+ "true"
+ ],
+ "status": 0
+ },
+ {
+ "assert_name": "assert_true",
+ "test": "assert_true expected to fail",
+ "args": [
+ "false",
+ "\"false should not be true\""
+ ],
+ "status": 1
+ },
+ {
+ "assert_name": "assert_array_equals",
+ "test": "basic assert_array_equals test",
+ "args": [
+ "[1, NaN]",
+ "[1, NaN]",
+ "\"[1, NaN] is equal to [1, NaN]\""
+ ],
+ "status": 0
+ },
+ {
+ "assert_name": "assert_array_equals",
+ "test": "assert_array_equals with first param undefined",
+ "args": [
+ "undefined",
+ "[1]",
+ "\"undefined equals [1]?\""
+ ],
+ "status": 1
+ },
+ {
+ "assert_name": "assert_array_equals",
+ "test": "assert_array_equals with first param true",
+ "args": [
+ "true",
+ "[1]",
+ "\"true equals [1]?\""
+ ],
+ "status": 1
+ },
+ {
+ "assert_name": "assert_array_equals",
+ "test": "assert_array_equals with first param false",
+ "args": [
+ "false",
+ "[1]",
+ "\"false equals [1]?\""
+ ],
+ "status": 1
+ },
+ {
+ "assert_name": "assert_array_equals",
+ "test": "assert_array_equals with first param null",
+ "args": [
+ "null",
+ "[1]",
+ "\"null equals [1]?\""
+ ],
+ "status": 1
+ },
+ {
+ "assert_name": "assert_array_equals",
+ "test": "assert_array_equals with first param 1",
+ "args": [
+ "1",
+ "[1]",
+ "\"1 equals [1]?\""
+ ],
+ "status": 1
+ },
+ {
+ "assert_name": "assert_array_approx_equals",
+ "test": "basic assert_array_approx_equals test",
+ "args": [
+ "[10, 11]",
+ "[11, 10]",
+ "1",
+ "\"[10, 11] is approximately (+/- 1) [11, 10]\""
+ ],
+ "status": 0
+ },
+ {
+ "assert_name": "assert_approx_equals",
+ "test": "basic assert_approx_equals test",
+ "args": [
+ "10",
+ "11",
+ "1",
+ "\"10 is approximately (+/- 1) 11\""
+ ],
+ "status": 0
+ },
+ {
+ "assert_name": "assert_approx_equals",
+ "test": "basic assert_less_than test",
+ "args": [
+ "10",
+ "11",
+ "1",
+ "\"10 is approximately (+/- 1) 11\""
+ ],
+ "status": 0
+ },
+ {
+ "assert_name": "assert_greater_than",
+ "test": "assert_greater_than expected to fail",
+ "args": [
+ "10",
+ "11",
+ "\"10 is not greater than 11\""
+ ],
+ "status": 1
+ },
+ {
+ "assert_name": "assert_greater_than_equal",
+ "test": "basic assert_greater_than_equal test",
+ "args": [
+ "10",
+ "10",
+ "\"10 is greater than or equal to 10\""
+ ],
+ "status": 0
+ },
+ {
+ "assert_name": "assert_greater_than_equal",
+ "test": "assert_less_than_equal expected to fail",
+ "args": [
+ "\"10\"",
+ "10",
+ "\"'10' is not a number\""
+ ],
+ "status": 1
+ },
+ {
+ "assert_name": "assert_own_property",
+ "test": "test for assert[_not]_own_property and insert_inherits",
+ "args": [
+ "object \"[object Object]\"",
+ "\"a\""
+ ],
+ "status": 0
+ },
+ {
+ "assert_name": "assert_not_own_property",
+ "test": "test for assert[_not]_own_property and insert_inherits",
+ "args": [
+ "object \"[object Object]\"",
+ "\"b\"",
+ "\"unexpected property found: \\\"b\\\"\""
+ ],
+ "status": 0
+ },
+ {
+ "assert_name": "assert_inherits",
+ "test": "test for assert[_not]_own_property and insert_inherits",
+ "args": [
+ "object \"[object Object]\"",
+ "\"b\""
+ ],
+ "status": 0
+ },
+ {
+ "assert_name": "assert_throws_dom",
+ "test": "Test throw DOM exception",
+ "args": [
+ "\"NOT_FOUND_ERR\"",
+ "function \"function () {a.removeChild(b)}\""
+ ],
+ "status": 0
+ },
+ {
+ "assert_name": "assert_throws_js",
+ "test": "Test throw DOMException as JS exception expected to fail",
+ "args": [
+ "function \"function DOMException() { [native code] }\"",
+ "function \"function () {a.removeChild(b)}\""
+ ],
+ "status": 1
+ },
+ {
+ "assert_name": "assert_throws_js",
+ "test": "Test throw SyntaxError DOMException where JS SyntaxError expected; expected to fail",
+ "args": [
+ "function \"function SyntaxError() { [native code] }\"",
+ "function \"function () {document.querySelector(\"\")}\""
+ ],
+ "status": 1
+ },
+ {
+ "assert_name": "assert_throws_js",
+ "test": "Test throw JS SyntaxError",
+ "args": [
+ "function \"function SyntaxError() { [native code] }\"",
+ "function \"function () {JSON.parse(\"{\")}\""
+ ],
+ "status": 0
+ },
+ {
+ "assert_name": "assert_throws_dom",
+ "test": "Test throw DOM SyntaxError",
+ "args": [
+ "\"SyntaxError\"",
+ "function \"function () {document.querySelector(\"\")}\""
+ ],
+ "status": 0
+ },
+ {
+ "assert_name": "assert_throws_dom",
+ "test": "Test throw DOM SyntaxError from subframe",
+ "args": [
+ "\"SyntaxError\"",
+ "function \"function DOMException() { [native code] }\"",
+ "function \"function () {ifr.contentDocument.querySelector(\"\")}\""
+ ],
+ "status": 0
+ },
+ {
+ "assert_name": "assert_throws_dom",
+ "test": "Test throw DOM SyntaxError from subframe with incorrect global expectation; expected to fail",
+ "args": [
+ "\"SyntaxError\"",
+ "function \"function () {ifr.contentDocument.querySelector(\"\")}\""
+ ],
+ "status": 1
+ },
+ {
+ "assert_name": "assert_throws_dom",
+ "test": "Test throw DOM SyntaxError with incorrect expectation; expected to fail",
+ "args": [
+ "\"SyntaxError\"",
+ "function \"function DOMException() { [native code] }\"",
+ "function \"function () {document.querySelector(\"\")}\""
+ ],
+ "status": 1
+ },
+ {
+ "assert_name": "assert_throws_dom",
+ "test": "Test throw JS SyntaxError where SyntaxError DOMException expected; expected to fail",
+ "args": [
+ "\"SyntaxError\"",
+ "function \"function () {JSON.parse(\"{\")}\""
+ ],
+ "status": 1
+ },
+ {
+ "assert_name": "assert_throws_dom",
+ "test": "Test throw DOM exception expected to fail",
+ "args": [
+ "\"NOT_FOUND_ERR\"",
+ "function \"function () {a.appendChild(b)}\""
+ ],
+ "status": 1
+ },
+ {
+ "assert_name": "assert_throws_dom",
+ "test": "Test assert_throws_dom with ambiguous DOM-exception expected to Fail",
+ "args": [
+ "0",
+ "function \"function() {throw e}\""
+ ],
+ "status": 1
+ },
+ {
+ "assert_name": "assert_throws_dom",
+ "test": "Test assert_throws_dom with non-DOM-exception expected to Fail",
+ "args": [
+ "\"TEST_ERR\"",
+ "function \"function() {throw e}\""
+ ],
+ "status": 1
+ },
+ {
+ "assert_name": "assert_throws_dom",
+ "test": "Test assert_throws_dom with number code value expected to Pass",
+ "args": [
+ "12",
+ "function \"function() {throw e}\""
+ ],
+ "status": 0
+ },
+ {
+ "assert_name": "assert_throws_dom",
+ "test": "Test assert_throws_dom with number code value and real DOMException expected to Pass",
+ "args": [
+ "12",
+ "function \"function() {throw e}\""
+ ],
+ "status": 0
+ },
+ {
+ "assert_name": "assert_equals",
+ "test": "Test that defines a global and cleans it up",
+ "args": [
+ "1",
+ "1"
+ ],
+ "status": 0
+ },
+ {
+ "assert_name": "assert_equals",
+ "test": "Test that cleanup handlers from previous test ran",
+ "args": [
+ "undefined",
+ "undefined"
+ ],
+ "status": 0
+ },
+ {
+ "assert_name": "assert_true",
+ "test": "Test step_func",
+ "args": [
+ "true"
+ ],
+ "status": 0
+ },
+ {
+ "assert_name": "assert_true",
+ "test": "Test async test with callback",
+ "args": [
+ "true"
+ ],
+ "status": 0
+ },
+ {
+ "assert_name": "assert_true",
+ "test": "Test async test with callback and `this` obj.",
+ "args": [
+ "true"
+ ],
+ "status": 0
+ }
+ ],
+ "type": "complete"
+}
+</script>
+</body>
+</html>
diff --git a/test/wpt/tests/resources/test/tests/functional/api-tests-2.html b/test/wpt/tests/resources/test/tests/functional/api-tests-2.html
new file mode 100644
index 0000000..9af94f6
--- /dev/null
+++ b/test/wpt/tests/resources/test/tests/functional/api-tests-2.html
@@ -0,0 +1,62 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Sample HTML5 API Tests</title>
+</head>
+<body>
+<h1>Sample HTML5 API Tests</h1>
+<p>There should be two results</p>
+<div id="log"></div>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+setup({explicit_done:true})
+test(function() {assert_true(true)}, "Test defined before onload");
+
+onload = function() {test(function (){assert_true(true)}, "Test defined after onload");
+done();
+}
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "OK",
+ "message": null
+ },
+ "summarized_tests": [
+ {
+ "status_string": "PASS",
+ "name": "Test defined after onload",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "Test defined before onload",
+ "properties": {},
+ "message": null
+ }
+ ],
+ "summarized_asserts": [
+ {
+ "assert_name": "assert_true",
+ "test": "Test defined before onload",
+ "args": [
+ "true"
+ ],
+ "status": 0
+ },
+ {
+ "assert_name": "assert_true",
+ "test": "Test defined after onload",
+ "args": [
+ "true"
+ ],
+ "status": 0
+ }
+ ],
+ "type": "complete"
+}
+</script>
+</body>
+</html>
diff --git a/test/wpt/tests/resources/test/tests/functional/api-tests-3.html b/test/wpt/tests/resources/test/tests/functional/api-tests-3.html
new file mode 100644
index 0000000..991fc6d
--- /dev/null
+++ b/test/wpt/tests/resources/test/tests/functional/api-tests-3.html
@@ -0,0 +1,34 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Sample HTML5 API Tests</title>
+</head>
+<script src="/resources/testharness.js"></script>
+
+<body>
+<h1>Sample HTML5 API Tests</h1>
+<div id="log"></div>
+<script>
+setup({explicit_timeout:true});
+var t = async_test("This test should give a status of 'Not Run' without a delay");
+timeout();
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "TIMEOUT",
+ "message": null
+ },
+ "summarized_tests": [
+ {
+ "status_string": "NOTRUN",
+ "name": "This test should give a status of 'Not Run' without a delay",
+ "properties": {},
+ "message": null
+ }
+ ],
+ "type": "complete"
+}
+</script>
+</body>
+</html>
diff --git a/test/wpt/tests/resources/test/tests/functional/assert-array-equals.html b/test/wpt/tests/resources/test/tests/functional/assert-array-equals.html
new file mode 100644
index 0000000..b6460a4
--- /dev/null
+++ b/test/wpt/tests/resources/test/tests/functional/assert-array-equals.html
@@ -0,0 +1,162 @@
+<!DOCTYPE HTML>
+<title>assert_array_equals</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+test(() => {
+ assert_array_equals([], []);
+}, "empty and equal");
+test(() => {
+ assert_array_equals([1], [1]);
+}, "non-empty and equal");
+test(() => {
+ assert_array_equals([], [1]);
+}, "length differs");
+test(() => {
+ assert_array_equals([1], [,]);
+}, "property is present");
+test(() => {
+ assert_array_equals([,], [1]);
+}, "property is missing");
+test(() => {
+ assert_array_equals([0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20], ["x",1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20]);
+}, "property 0 differs");
+test(() => {
+ assert_array_equals([0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20], [0,1,2,3,4,"x",6,7,8,9,10,11,12,13,14,15,16,17,18,19,20]);
+}, "property 5 differs");
+test(() => {
+ assert_array_equals([0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21], [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21]);
+}, "lengths differ and input array beyond display limit");
+test(() => {
+ assert_array_equals([1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21], [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21]);
+}, "lengths differ and expected array beyond display limit");
+test(() => {
+ assert_array_equals([0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21], [,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21]);
+}, "property 0 is present and arrays are beyond display limit");
+test(() => {
+ assert_array_equals([,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21], [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21]);
+}, "property 0 is missing and arrays are beyond display limit");
+test(() => {
+ assert_array_equals([0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21], [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,,19,20,21]);
+}, "property 18 is present and arrays are beyond display limit");
+test(() => {
+ assert_array_equals([0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,,19,20,21], [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21]);
+}, "property 18 is missing and arrays are beyond display limit");
+test(() => {
+ assert_array_equals([0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21], ["x",1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21]);
+}, "property 0 differs and arrays are beyond display limit");
+test(() => {
+ assert_array_equals([0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21], [0,1,2,3,4,"x",6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21]);
+}, "property 5 differs and arrays are beyond display limit");
+test(() => {
+ assert_array_equals([0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26], [0,1,2,3,4,5,6,7,8,9,10,11,"x",13,14,15,16,17,18,19,20,21,22,23,24,25,26]);
+}, "property 5 differs and arrays are beyond display limit on both sides");
+</script>
+<script type="text/json" id="expected">
+{
+ "type": "complete",
+ "summarized_status": {
+ "message": null,
+ "status_string": "OK"
+ },
+ "summarized_tests": [
+ {
+ "name": "empty and equal",
+ "message": null,
+ "properties": {},
+ "status_string": "PASS"
+ },
+ {
+ "name": "length differs",
+ "message": "assert_array_equals: lengths differ, expected array [1] length 1, got [] length 0",
+ "properties": {},
+ "status_string": "FAIL"
+ },
+ {
+ "name": "lengths differ and expected array beyond display limit",
+ "message": "assert_array_equals: lengths differ, expected array [\u2026, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21] length 22, got [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21] length 21",
+ "properties": {},
+ "status_string": "FAIL"
+ },
+ {
+ "name": "lengths differ and input array beyond display limit",
+ "message": "assert_array_equals: lengths differ, expected array [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21] length 21, got [\u2026, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21] length 22",
+ "properties": {},
+ "status_string": "FAIL"
+ },
+ {
+ "name": "non-empty and equal",
+ "message": null,
+ "properties": {},
+ "status_string": "PASS"
+ },
+ {
+ "name": "property 0 differs",
+ "message": "assert_array_equals: expected property 0 to be \"x\" but got 0 (expected array [\"x\", 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20] got [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20])",
+ "properties": {},
+ "status_string": "FAIL"
+ },
+ {
+ "name": "property 0 differs and arrays are beyond display limit",
+ "message": "assert_array_equals: expected property 0 to be \"x\" but got 0 (expected array [\"x\", 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, \u2026] got [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, \u2026])",
+ "properties": {},
+ "status_string": "FAIL"
+ },
+ {
+ "name": "property 0 is missing and arrays are beyond display limit",
+ "message": "assert_array_equals: expected property 0 to be \"present\" but was \"missing\" (expected array [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, \u2026] got [, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, \u2026])",
+ "properties": {},
+ "status_string": "FAIL"
+ },
+ {
+ "name": "property 0 is present and arrays are beyond display limit",
+ "message": "assert_array_equals: expected property 0 to be \"missing\" but was \"present\" (expected array [, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, \u2026] got [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, \u2026])",
+ "properties": {},
+ "status_string": "FAIL"
+ },
+ {
+ "name": "property 18 is missing and arrays are beyond display limit",
+ "message": "assert_array_equals: expected property 18 to be \"present\" but was \"missing\" (expected array [\u2026, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21] got [\u2026, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, , 19, 20, 21])",
+ "properties": {},
+ "status_string": "FAIL"
+ },
+ {
+ "name": "property 18 is present and arrays are beyond display limit",
+ "message": "assert_array_equals: expected property 18 to be \"missing\" but was \"present\" (expected array [\u2026, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, , 19, 20, 21] got [\u2026, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21])",
+ "properties": {},
+ "status_string": "FAIL"
+ },
+ {
+ "name": "property 5 differs",
+ "message": "assert_array_equals: expected property 5 to be \"x\" but got 5 (expected array [0, 1, 2, 3, 4, \"x\", 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20] got [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20])",
+ "properties": {},
+ "status_string": "FAIL"
+ },
+ {
+ "name": "property 5 differs and arrays are beyond display limit",
+ "message": "assert_array_equals: expected property 5 to be \"x\" but got 5 (expected array [0, 1, 2, 3, 4, \"x\", 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, \u2026] got [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, \u2026])",
+ "properties": {},
+ "status_string": "FAIL"
+ },
+ {
+ "name": "property 5 differs and arrays are beyond display limit on both sides",
+ "message": "assert_array_equals: expected property 12 to be \"x\" but got 12 (expected array [\u2026, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, \"x\", 13, 14, 15, 16, 17, 18, 19, 20, 21, \u2026] got [\u2026, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, \u2026])",
+ "properties": {},
+ "status_string": "FAIL"
+ },
+ {
+ "name": "property is missing",
+ "message": "assert_array_equals: expected property 0 to be \"present\" but was \"missing\" (expected array [1] got [])",
+ "properties": {},
+ "status_string": "FAIL"
+ },
+ {
+ "name": "property is present",
+ "message": "assert_array_equals: expected property 0 to be \"missing\" but was \"present\" (expected array [] got [1])",
+ "properties": {},
+ "status_string": "FAIL"
+ }
+ ]
+}
+</script>
diff --git a/test/wpt/tests/resources/test/tests/functional/assert-throws-dom.html b/test/wpt/tests/resources/test/tests/functional/assert-throws-dom.html
new file mode 100644
index 0000000..4dd66b2
--- /dev/null
+++ b/test/wpt/tests/resources/test/tests/functional/assert-throws-dom.html
@@ -0,0 +1,55 @@
+<!DOCTYPE HTML>
+<title>assert_throws_dom</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+test(() => {
+ function f() {
+ assert_true(false, "Trivial assertion.");
+
+ // Would lead to throwing a SyntaxError.
+ document.createElement("div").contentEditable = "invalid";
+ }
+
+ assert_throws_dom("SyntaxError", () => { f(); });
+}, "Violated assertion nested in `assert_throws_dom`.");
+</script>
+<script type="text/json" id="expected">
+{
+ "type": "complete",
+ "summarized_status": {
+ "message": null,
+ "status_string": "OK"
+ },
+ "summarized_tests": [
+ {
+ "message": "assert_true: Trivial assertion. expected true got false",
+ "name": "Violated assertion nested in `assert_throws_dom`.",
+ "properties": {},
+ "status_string": "FAIL"
+ }
+ ],
+ "summarized_asserts": [
+ {
+ "assert_name": "assert_throws_dom",
+ "test": "Violated assertion nested in `assert_throws_dom`.",
+ "args": [
+ "\"SyntaxError\"",
+ "function \"() => { f(); }\""
+ ],
+ "status": 1
+ },
+ {
+ "assert_name": "assert_true",
+ "test": "Violated assertion nested in `assert_throws_dom`.",
+ "args": [
+ "false",
+ "\"Trivial assertion.\""
+ ],
+ "status": 1
+ }
+ ]
+}
+</script>
+
diff --git a/test/wpt/tests/resources/test/tests/functional/force_timeout.html b/test/wpt/tests/resources/test/tests/functional/force_timeout.html
new file mode 100644
index 0000000..2058fdb
--- /dev/null
+++ b/test/wpt/tests/resources/test/tests/functional/force_timeout.html
@@ -0,0 +1,60 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Test#force_timeout</title>
+</head>
+<body>
+<h1>Test#force_timeout</h1>
+<div id="log"></div>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+setup({ explicit_timeout: true });
+
+test(function(t) {
+ t.force_timeout();
+ }, 'test (synchronous)');
+
+async_test(function(t) {
+ t.step_timeout(function() {
+ t.force_timeout();
+ }, 0);
+ }, 'async_test');
+
+promise_test(function(t) {
+ t.force_timeout();
+
+ return new Promise(function() {});
+ }, 'promise_test');
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "OK",
+ "message": null
+ },
+ "summarized_tests": [
+ {
+ "status_string": "TIMEOUT",
+ "name": "async_test",
+ "message": "Test timed out",
+ "properties": {}
+ },
+ {
+ "status_string": "TIMEOUT",
+ "name": "promise_test",
+ "message": "Test timed out",
+ "properties": {}
+ },
+ {
+ "status_string": "TIMEOUT",
+ "name": "test (synchronous)",
+ "message": "Test timed out",
+ "properties": {}
+ }
+ ],
+ "type": "complete"
+}
+</script>
+</body>
+</html>
diff --git a/test/wpt/tests/resources/test/tests/functional/generate-callback.html b/test/wpt/tests/resources/test/tests/functional/generate-callback.html
new file mode 100644
index 0000000..11d4174
--- /dev/null
+++ b/test/wpt/tests/resources/test/tests/functional/generate-callback.html
@@ -0,0 +1,153 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Sample for using generate_tests to create a series of tests that share the same callback.</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+</head>
+<body>
+<script>
+// generate_tests takes an array of arrays that define tests
+// but lets pass it an empty array and verify it does nothing.
+function null_callback() {
+ throw "null_callback should not be called.";
+}
+generate_tests(null_callback, []);
+
+// Generate 3 tests specifying the name and one parameter
+function validate_arguments(arg1) {
+ assert_equals(arg1, 1, "Ensure that we get our expected argument");
+}
+generate_tests(validate_arguments, [
+ ["first test", 1],
+ ["second test", 1],
+ ["third test", 1],
+]);
+
+// Generate a test passing in a properties object that is shared across tests.
+function validate_properties() {
+ assert_true(this.properties.sentinel, "Ensure that we got the right properties object.");
+}
+generate_tests(validate_properties, [["sentinel check 1"], ["sentinel check 2"]], {sentinel: true});
+
+// Generate a test passing in a properties object that is shared across tests.
+function validate_separate_properties() {
+ if (this.name === "sentinel check 1 unique properties") {
+ assert_true(this.properties.sentinel, "Ensure that we got the right properties object. Expect sentinel: true.");
+ }
+ else {
+ assert_false(this.properties.sentinel, "Ensure that we got the right properties object. Expect sentinel: false.");
+ }
+}
+generate_tests(validate_separate_properties, [["sentinel check 1 unique properties"], ["sentinel check 2 unique properties"]], [{sentinel: true}, {sentinel: false}]);
+
+// Finally generate a complicated set of tests from another data source
+var letters = ["a", "b", "c", "d", "e", "f"];
+var numbers = [0, 1, 2, 3, 4, 5];
+function validate_related_arguments(arg1, arg2) {
+ assert_equals(arg1.charCodeAt(0) - "a".charCodeAt(0), arg2, "Ensure that we can map letters to numbers.");
+}
+function format_as_test(letter, index, letters) {
+ return ["Test to map " + letter + " to " + numbers[index], letter, numbers[index]];
+}
+generate_tests(validate_related_arguments, letters.map(format_as_test));
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "OK",
+ "message": null
+ },
+ "summarized_tests": [
+ {
+ "status_string": "PASS",
+ "name": "Test to map a to 0",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "Test to map b to 1",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "Test to map c to 2",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "Test to map d to 3",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "Test to map e to 4",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "Test to map f to 5",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "first test",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "second test",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "sentinel check 1",
+ "properties": {
+ "sentinel": true
+ },
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "sentinel check 1 unique properties",
+ "properties": {
+ "sentinel": true
+ },
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "sentinel check 2",
+ "properties": {
+ "sentinel": true
+ },
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "sentinel check 2 unique properties",
+ "properties": {
+ "sentinel": false
+ },
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "third test",
+ "properties": {},
+ "message": null
+ }
+ ],
+ "type": "complete"
+}
+</script>
+</body>
+</html>
diff --git a/test/wpt/tests/resources/test/tests/functional/idlharness/IdlDictionary/test_partial_interface_of.html b/test/wpt/tests/resources/test/tests/functional/idlharness/IdlDictionary/test_partial_interface_of.html
new file mode 100644
index 0000000..f635768
--- /dev/null
+++ b/test/wpt/tests/resources/test/tests/functional/idlharness/IdlDictionary/test_partial_interface_of.html
@@ -0,0 +1,89 @@
+<!DOCTYPE HTML>
+<html>
+
+<head>
+ <meta charset="utf-8">
+ <title>idlharness: Partial dictionary</title>
+ <script src="/resources/test/variants.js"></script>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/resources/WebIDLParser.js"></script>
+ <script src="/resources/idlharness.js"></script>
+</head>
+
+<body>
+ <p>Verify the series of sub-tests that are executed for "partial" dictionary objects.</p>
+ <script>
+ "use strict";
+
+ // No original existence
+ (() => {
+ const idlArray = new IdlArray();
+ idlArray.add_idls('partial dictionary A {};');
+ idlArray.test();
+ })();
+
+ // Multiple partials existence
+ (() => {
+ const idlArray = new IdlArray();
+ idlArray.add_untested_idls('partial dictionary B {};');
+ idlArray.add_idls('partial dictionary B {};');
+ idlArray.add_idls('partial dictionary B {};');
+ idlArray.add_idls('partial dictionary B {};');
+ idlArray.add_idls('dictionary B {};');
+ idlArray.test();
+ })();
+
+ // Original is a namespace, not a dictionary.
+ (() => {
+ const idlArray = new IdlArray();
+ idlArray.add_idls(`
+ partial dictionary C {};
+ namespace C {};`);
+ idlArray.merge_partials();
+ })();
+ </script>
+ <script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "OK",
+ "message": null
+ },
+ "summarized_tests": [
+ {
+ "name": "Partial dictionary A: original dictionary defined",
+ "status_string": "FAIL",
+ "properties": {},
+ "message": "assert_true: Original dictionary should be defined expected true got false"
+ },
+ {
+ "name": "Partial dictionary B[2]: original dictionary defined",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Partial dictionary B[3]: original dictionary defined",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Partial dictionary B[4]: original dictionary defined",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Partial dictionary C: original dictionary defined",
+ "status_string": "FAIL",
+ "properties": {},
+ "message": "assert_true: Original C definition should have type dictionary expected true got false"
+ }
+ ],
+ "type": "complete"
+}
+</script>
+</body>
+
+</html>
diff --git a/test/wpt/tests/resources/test/tests/functional/idlharness/IdlInterface/test_exposed_wildcard.html b/test/wpt/tests/resources/test/tests/functional/idlharness/IdlInterface/test_exposed_wildcard.html
new file mode 100644
index 0000000..addc0eb
--- /dev/null
+++ b/test/wpt/tests/resources/test/tests/functional/idlharness/IdlInterface/test_exposed_wildcard.html
@@ -0,0 +1,233 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>idlharness: Exposed=*</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/resources/WebIDLParser.js"></script>
+ <script src="/resources/idlharness.js"></script>
+</head>
+<body>
+<script>
+"use strict";
+
+Object.defineProperty(window, "A", {
+ enumerable: false,
+ writable: true,
+ configurable: true,
+ value: function A() {}
+ });
+Object.defineProperty(window.A, "prototype", {
+ writable: false,
+ value: window.A.prototype
+ });
+A.prototype[Symbol.toStringTag] = "A";
+
+Object.defineProperty(window, "C", {
+ enumerable: false,
+ writable: true,
+ configurable: true,
+ value: function C() {}
+ });
+Object.defineProperty(window.C, "prototype", {
+ writable: false,
+ value: window.C.prototype
+ });
+C.prototype[Symbol.toStringTag] = "C";
+
+Object.defineProperty(window, "D", {
+ enumerable: false,
+ writable: true,
+ configurable: true,
+ value: function D() {}
+ });
+Object.defineProperty(window.D, "prototype", {
+ writable: false,
+ value: window.D.prototype
+ });
+C.prototype[Symbol.toStringTag] = "D";
+Object.defineProperty(window, "B", {
+ enumerable: false,
+ writable: true,
+ configurable: true,
+ value: window.A
+ });
+
+var idlArray = new IdlArray();
+idlArray.add_idls(`
+[Exposed=*, LegacyWindowAlias=B] interface A {};
+[Exposed=*] partial interface A {};
+[Exposed=Window] interface C {};
+[Exposed=*] partial interface C {};
+[Exposed=*] interface D {};
+[Exposed=Window] partial interface D {};
+`);
+idlArray.add_objects({
+ Window: ["window"]
+});
+idlArray.test();
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "message": null,
+ "status_string": "OK"
+ },
+ "summarized_tests": [
+ {
+ "name": "A interface object length",
+ "properties": {},
+ "message": null,
+ "status_string": "PASS"
+ },
+ {
+ "name": "A interface object name",
+ "properties": {},
+ "message": null,
+ "status_string": "PASS"
+ },
+ {
+ "name": "A interface: existence and properties of interface object",
+ "properties": {},
+ "message": "assert_throws_js: interface object didn't throw TypeError when called as a function function \"function() {\n interface_object();\n }\" did not throw",
+ "status_string": "FAIL"
+ },
+ {
+ "name": "A interface: existence and properties of interface prototype object",
+ "properties": {},
+ "message": null,
+ "status_string": "PASS"
+ },
+ {
+ "name": "A interface: existence and properties of interface prototype object's \"constructor\" property",
+ "properties": {},
+ "message": null,
+ "status_string": "PASS"
+ },
+ {
+ "name": "A interface: existence and properties of interface prototype object's @@unscopables property",
+ "properties": {},
+ "message": null,
+ "status_string": "PASS"
+ },
+ {
+ "name": "A interface: legacy window alias",
+ "properties": {},
+ "message": null,
+ "status_string": "PASS"
+ },
+ {
+ "name": "C interface object length",
+ "properties": {},
+ "message": null,
+ "status_string": "PASS"
+ },
+ {
+ "name": "C interface object name",
+ "properties": {},
+ "message": null,
+ "status_string": "PASS"
+ },
+ {
+ "name": "C interface: existence and properties of interface object",
+ "properties": {},
+ "message": "assert_throws_js: interface object didn't throw TypeError when called as a function function \"function() {\n interface_object();\n }\" did not throw",
+ "status_string": "FAIL"
+ },
+ {
+ "name": "C interface: existence and properties of interface prototype object",
+ "properties": {},
+ "message": null,
+ "status_string": "PASS"
+ },
+ {
+ "name": "C interface: existence and properties of interface prototype object's \"constructor\" property",
+ "properties": {},
+ "message": null,
+ "status_string": "PASS"
+ },
+ {
+ "name": "C interface: existence and properties of interface prototype object's @@unscopables property",
+ "properties": {},
+ "message": null,
+ "status_string": "PASS"
+ },
+ {
+ "name": "D interface object length",
+ "properties": {},
+ "message": null,
+ "status_string": "PASS"
+ },
+ {
+ "name": "D interface object name",
+ "properties": {},
+ "message": null,
+ "status_string": "PASS"
+ },
+ {
+ "name": "D interface: existence and properties of interface object",
+ "properties": {},
+ "message": "assert_throws_js: interface object didn't throw TypeError when called as a function function \"function() {\n interface_object();\n }\" did not throw",
+ "status_string": "FAIL"
+ },
+ {
+ "name": "D interface: existence and properties of interface prototype object",
+ "properties": {},
+ "message": null,
+ "status_string": "PASS"
+ },
+ {
+ "name": "D interface: existence and properties of interface prototype object's \"constructor\" property",
+ "properties": {},
+ "message": null,
+ "status_string": "PASS"
+ },
+ {
+ "name": "D interface: existence and properties of interface prototype object's @@unscopables property",
+ "properties": {},
+ "message": null,
+ "status_string": "PASS"
+ },
+ {
+ "name": "Partial interface A: original interface defined",
+ "properties": {},
+ "message": null,
+ "status_string": "PASS"
+ },
+ {
+ "name": "Partial interface A: valid exposure set",
+ "properties": {},
+ "message": null,
+ "status_string": "PASS"
+ },
+ {
+ "name": "Partial interface C: original interface defined",
+ "properties": {},
+ "message": null,
+ "status_string": "PASS"
+ },
+ {
+ "name": "Partial interface C: valid exposure set",
+ "properties": {},
+ "message": "Partial C interface is exposed everywhere, the original interface is not.",
+ "status_string": "FAIL"
+ },
+ {
+ "name": "Partial interface D: original interface defined",
+ "properties": {},
+ "message": null,
+ "status_string": "PASS"
+ },
+ {
+ "name": "Partial interface D: valid exposure set",
+ "properties": {},
+ "message": null,
+ "status_string": "PASS"
+ }
+ ],
+ "type": "complete"
+}
+</script>
+</body>
+</html>
diff --git a/test/wpt/tests/resources/test/tests/functional/idlharness/IdlInterface/test_immutable_prototype.html b/test/wpt/tests/resources/test/tests/functional/idlharness/IdlInterface/test_immutable_prototype.html
new file mode 100644
index 0000000..5fe0591
--- /dev/null
+++ b/test/wpt/tests/resources/test/tests/functional/idlharness/IdlInterface/test_immutable_prototype.html
@@ -0,0 +1,298 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>idlharness: Immutable prototypes</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/resources/WebIDLParser.js"></script>
+ <script src="/resources/idlharness.js"></script>
+</head>
+<body>
+<script>
+"use strict";
+
+Object.defineProperty(window, "Foo", {
+ enumerable: false,
+ writable: true,
+ configurable: true,
+ value: function Foo() {
+ if (!new.target) {
+ throw new TypeError('Foo() must be called with new');
+ }
+ }
+ });
+Object.defineProperty(window.Foo, "prototype", {
+ writable: false,
+ value: window.Foo.prototype
+ });
+Foo.prototype[Symbol.toStringTag] = "Foo";
+
+var idlArray = new IdlArray();
+idlArray.add_untested_idls("interface EventTarget {};");
+idlArray.add_idls(
+ "[Global=Window, Exposed=Window]\n" +
+ "interface Window : EventTarget {};\n" +
+
+ "[Global=Window, Exposed=Window]\n" +
+ "interface Foo { constructor(); };"
+ );
+idlArray.add_objects({
+ Foo: ["new Foo()"],
+ Window: ["window"]
+});
+idlArray.test();
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "message": null,
+ "status_string": "OK"
+ },
+ "summarized_tests": [
+ {
+ "name": "Foo interface object length",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Foo interface object name",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Foo interface: existence and properties of interface object",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Foo interface: existence and properties of interface prototype object",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Foo interface: existence and properties of interface prototype object's \"constructor\" property",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Foo interface: existence and properties of interface prototype object's @@unscopables property",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Foo interface: internal [[SetPrototypeOf]] method of global platform object - setting to a new value via Object.setPrototypeOf should throw a TypeError",
+ "status_string": "FAIL",
+ "properties": {},
+ "message": "assert_throws_js: function \"function() {\n Object.setPrototypeOf(obj, newValue);\n }\" did not throw"
+ },
+ {
+ "name": "Foo interface: internal [[SetPrototypeOf]] method of global platform object - setting to a new value via Reflect.setPrototypeOf should return false",
+ "status_string": "FAIL",
+ "properties": {},
+ "message": "assert_false: expected false got true"
+ },
+ {
+ "name": "Foo interface: internal [[SetPrototypeOf]] method of global platform object - setting to a new value via __proto__ should throw a TypeError",
+ "status_string": "FAIL",
+ "properties": {},
+ "message": "assert_throws_js: function \"function() {\n obj.__proto__ = newValue;\n }\" did not throw"
+ },
+ {
+ "name": "Foo interface: internal [[SetPrototypeOf]] method of global platform object - setting to its original value via Object.setPrototypeOf should not throw",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Foo interface: internal [[SetPrototypeOf]] method of global platform object - setting to its original value via Reflect.setPrototypeOf should return true",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Foo interface: internal [[SetPrototypeOf]] method of global platform object - setting to its original value via __proto__ should not throw",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Foo interface: internal [[SetPrototypeOf]] method of interface prototype object - setting to a new value via Object.setPrototypeOf should throw a TypeError",
+ "status_string": "FAIL",
+ "properties": {},
+ "message": "assert_throws_js: function \"function() {\n Object.setPrototypeOf(obj, newValue);\n }\" did not throw"
+ },
+ {
+ "name": "Foo interface: internal [[SetPrototypeOf]] method of interface prototype object - setting to a new value via Reflect.setPrototypeOf should return false",
+ "status_string": "FAIL",
+ "properties": {},
+ "message": "assert_false: expected false got true"
+ },
+ {
+ "name": "Foo interface: internal [[SetPrototypeOf]] method of interface prototype object - setting to a new value via __proto__ should throw a TypeError",
+ "status_string": "FAIL",
+ "properties": {},
+ "message": "assert_throws_js: function \"function() {\n obj.__proto__ = newValue;\n }\" did not throw"
+ },
+ {
+ "name": "Foo interface: internal [[SetPrototypeOf]] method of interface prototype object - setting to its original value via Object.setPrototypeOf should not throw",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Foo interface: internal [[SetPrototypeOf]] method of interface prototype object - setting to its original value via Reflect.setPrototypeOf should return true",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Foo interface: internal [[SetPrototypeOf]] method of interface prototype object - setting to its original value via __proto__ should not throw",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Foo must be primary interface of new Foo()",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Stringification of new Foo()",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Stringification of window",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Window interface object length",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Window interface object name",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Window interface: existence and properties of interface object",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Window interface: existence and properties of interface prototype object",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Window interface: existence and properties of interface prototype object's \"constructor\" property",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Window interface: existence and properties of interface prototype object's @@unscopables property",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Window interface: internal [[SetPrototypeOf]] method of global platform object - setting to a new value via Object.setPrototypeOf should throw a TypeError",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Window interface: internal [[SetPrototypeOf]] method of global platform object - setting to a new value via Reflect.setPrototypeOf should return false",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Window interface: internal [[SetPrototypeOf]] method of global platform object - setting to a new value via __proto__ should throw a TypeError",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Window interface: internal [[SetPrototypeOf]] method of global platform object - setting to its original value via Object.setPrototypeOf should not throw",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Window interface: internal [[SetPrototypeOf]] method of global platform object - setting to its original value via Reflect.setPrototypeOf should return true",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Window interface: internal [[SetPrototypeOf]] method of global platform object - setting to its original value via __proto__ should not throw",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Window interface: internal [[SetPrototypeOf]] method of interface prototype object - setting to a new value via Object.setPrototypeOf should throw a TypeError",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Window interface: internal [[SetPrototypeOf]] method of interface prototype object - setting to a new value via Reflect.setPrototypeOf should return false",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Window interface: internal [[SetPrototypeOf]] method of interface prototype object - setting to a new value via __proto__ should throw a TypeError",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Window interface: internal [[SetPrototypeOf]] method of interface prototype object - setting to its original value via Object.setPrototypeOf should not throw",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Window interface: internal [[SetPrototypeOf]] method of interface prototype object - setting to its original value via Reflect.setPrototypeOf should return true",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Window interface: internal [[SetPrototypeOf]] method of interface prototype object - setting to its original value via __proto__ should not throw",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Window must be primary interface of window",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ }
+ ],
+ "type": "complete"
+}
+</script>
+</body>
+</html>
diff --git a/test/wpt/tests/resources/test/tests/functional/idlharness/IdlInterface/test_interface_mixin.html b/test/wpt/tests/resources/test/tests/functional/idlharness/IdlInterface/test_interface_mixin.html
new file mode 100644
index 0000000..be2844e
--- /dev/null
+++ b/test/wpt/tests/resources/test/tests/functional/idlharness/IdlInterface/test_interface_mixin.html
@@ -0,0 +1,131 @@
+<!DOCTYPE HTML>
+<html>
+
+<head>
+ <meta charset="utf-8">
+ <title>idlharness: interface mixins</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/resources/WebIDLParser.js"></script>
+ <script src="/resources/idlharness.js"></script>
+</head>
+
+<body>
+ <p>Verify the series of sub-tests that are executed for "interface mixin" objects.</p>
+ <script>
+ "use strict";
+
+ // Simple includes statement (valid)
+ (() => {
+ const idlArray = new IdlArray();
+ idlArray.add_idls(`
+ [Exposed=Window] interface I1 {};
+ interface mixin M1 { attribute any a1; };
+ I1 includes M1;`);
+ idlArray.merge_partials();
+ idlArray.merge_mixins();
+ })();
+
+ // Partial interface mixin (valid)
+ (() => {
+ const idlArray = new IdlArray();
+ idlArray.add_idls(`
+ [Exposed=Window] interface I2 {};
+ interface mixin M2 {};
+ partial interface mixin M2 { attribute any a2; };
+ I2 includes M2;`);
+ idlArray.merge_partials();
+ idlArray.merge_mixins();
+ })();
+
+ // Partial interface mixin without original mixin
+ (() => {
+ const idlArray = new IdlArray();
+ idlArray.add_idls('partial interface mixin M3 {};');
+ idlArray.merge_partials();
+ idlArray.merge_mixins();
+ })();
+
+ // Name clash between mixin and partial mixin
+ (() => {
+ const idlArray = new IdlArray();
+ idlArray.add_idls(`
+ interface mixin M4 { attribute any a4; };
+ partial interface mixin M4 { attribute any a4; };`);
+ idlArray.merge_partials();
+ idlArray.merge_mixins();
+ })();
+
+ // Name clash between interface and mixin
+ (() => {
+ const idlArray = new IdlArray();
+ idlArray.add_untested_idls(`
+ interface mixin M5 { attribute any a5; };
+ interface I5 { attribute any a5; };
+ I5 includes M5;`);
+ idlArray.merge_partials();
+ idlArray.merge_mixins();
+ })();
+ </script>
+ <script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "OK",
+ "message": null
+ },
+ "summarized_tests": [
+ {
+ "name": "I1 includes M1: member names are unique",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "I2 includes M2: member names are unique",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "I5 includes M5: member names are unique",
+ "status_string": "FAIL",
+ "properties": {},
+ "message": "assert_true: member a5 is unique expected true got false"
+ },
+ {
+ "name": "Partial interface mixin M2: member names are unique",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Partial interface mixin M2: original interface mixin defined",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Partial interface mixin M3: original interface mixin defined",
+ "status_string": "FAIL",
+ "properties": {},
+ "message": "assert_true: Original interface mixin should be defined expected true got false"
+ },
+ {
+ "name": "Partial interface mixin M4: member names are unique",
+ "status_string": "FAIL",
+ "properties": {},
+ "message": "assert_true: member a4 is unique expected true got false"
+ },
+ {
+ "name": "Partial interface mixin M4: original interface mixin defined",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ }
+ ],
+ "type": "complete"
+}
+</script>
+</body>
+
+</html>
diff --git a/test/wpt/tests/resources/test/tests/functional/idlharness/IdlInterface/test_partial_interface_of.html b/test/wpt/tests/resources/test/tests/functional/idlharness/IdlInterface/test_partial_interface_of.html
new file mode 100644
index 0000000..7dd9e67
--- /dev/null
+++ b/test/wpt/tests/resources/test/tests/functional/idlharness/IdlInterface/test_partial_interface_of.html
@@ -0,0 +1,187 @@
+<!DOCTYPE HTML>
+<html>
+
+<head>
+ <meta charset="utf-8">
+ <title>idlharness: Partial interface</title>
+ <script src="/resources/test/variants.js"></script>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/resources/WebIDLParser.js"></script>
+ <script src="/resources/idlharness.js"></script>
+</head>
+
+<body>
+ <p>Verify the series of sub-tests that are executed for "partial" interface objects.</p>
+ <script>
+ "use strict";
+
+ // No original existence
+ (() => {
+ const idlArray = new IdlArray();
+ idlArray.add_idls('partial interface A {};');
+ idlArray.test();
+ })();
+
+ // Valid exposure (Note: Worker -> {Shared,Dedicated,Service}Worker)
+ (() => {
+ const idlArray = new IdlArray();
+ idlArray.add_untested_idls(`
+ [Exposed=(Worker)]
+ interface B {};
+
+ [Exposed=(DedicatedWorker, ServiceWorker, SharedWorker)]
+ interface C {};`);
+ idlArray.add_idls(`
+ [Exposed=(DedicatedWorker, ServiceWorker, SharedWorker)]
+ partial interface B {};
+
+ [Exposed=(Worker)]
+ partial interface C {};`);
+ idlArray.merge_partials();
+ })();
+
+ // Invalid exposure
+ (() => {
+ const idlArray = new IdlArray();
+ idlArray.add_untested_idls(`
+ [Exposed=(Window, ServiceWorker)]
+ interface D {};`);
+ idlArray.add_idls(`
+ [Exposed=(DedicatedWorker)]
+ partial interface D {};`);
+ idlArray.merge_partials();
+ })();
+
+ // Original is a namespace, not an interface.
+ (() => {
+ const idlArray = new IdlArray();
+ idlArray.add_idls(`
+ partial interface E {};
+ namespace E {};`);
+ idlArray.merge_partials();
+ })();
+
+ (() => {
+ const idlArray = new IdlArray();
+ idlArray.add_idls(`
+ partial interface F {};
+ partial interface mixin G {};
+ `);
+ idlArray.add_dependency_idls(`
+ interface F {};
+ interface mixin G {};
+ interface mixin H {};
+ F includes H;
+ I includes H;
+ J includes G;
+ interface K : J {};
+ interface L : F {};
+ `);
+ test(() => {
+ // Convert idlArray.includes into a Map from name of target interface to
+ // name of included mixin. (This assumes each interface includes at most
+ // one mixin, otherwise later includes would clobber earlier ones.)
+ const includes = new Map(idlArray.includes.map(i => [i.target, i.includes]));
+ // F is tested, so H is a dep.
+ assert_equals(includes.get('F'), 'H', 'F should be picked up');
+ // H is not tested, so I is not a dep.
+ assert_false(includes.has('I'), 'I should be ignored');
+ // G is a dep, so J is a dep.
+ assert_equals(includes.get('J'), 'G', 'J should be picked up');
+ // K isn't a dep because J isn't defined.
+ assert_false('K' in idlArray.members, 'K should be ignored');
+ // L isn't a dep because F is untested.
+ assert_false('L' in idlArray.members, 'L should be ignored');
+ }, 'partial mixin dep implications');
+ })();
+
+ // Name clash (partials)
+ (() => {
+ const idlArray = new IdlArray();
+ idlArray.add_idls(`
+ interface M { attribute any A; };
+ partial interface M { attribute any A; };`);
+ idlArray.merge_partials();
+ })();
+ </script>
+ <script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "OK",
+ "message": null
+ },
+ "summarized_tests": [
+ {
+ "name": "Partial interface A: original interface defined",
+ "status_string": "FAIL",
+ "properties": {},
+ "message": "assert_true: Original interface should be defined expected true got false"
+ },
+ {
+ "name": "Partial interface B: original interface defined",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Partial interface B: valid exposure set",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Partial interface C: original interface defined",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Partial interface C: valid exposure set",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Partial interface D: original interface defined",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Partial interface D: valid exposure set",
+ "status_string": "FAIL",
+ "properties": {},
+ "message": "Partial D interface is exposed to 'DedicatedWorker', the original interface is not."
+ },
+ {
+ "name": "Partial interface E: original interface defined",
+ "status_string": "FAIL",
+ "properties": {},
+ "message": "assert_true: Original E definition should have type interface expected true got false"
+ },
+ {
+ "name": "partial mixin dep implications",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Partial interface M: original interface defined",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Partial interface M: member names are unique",
+ "status_string": "FAIL",
+ "properties": {},
+ "message": "assert_true: member A is unique expected true got false"
+ }
+ ],
+ "type": "complete"
+}
+</script>
+</body>
+
+</html>
diff --git a/test/wpt/tests/resources/test/tests/functional/idlharness/IdlInterface/test_primary_interface_of.html b/test/wpt/tests/resources/test/tests/functional/idlharness/IdlInterface/test_primary_interface_of.html
new file mode 100644
index 0000000..309de60
--- /dev/null
+++ b/test/wpt/tests/resources/test/tests/functional/idlharness/IdlInterface/test_primary_interface_of.html
@@ -0,0 +1,116 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>idlharness: Primary interface</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/resources/WebIDLParser.js"></script>
+ <script src="/resources/idlharness.js"></script>
+</head>
+<body>
+<p>Verify the series of sub-tests that are executed for "tested" interface
+objects but skipped for "untested" interface objects.</p>
+<script>
+"use strict";
+
+function FooParent() {
+ if (!new.target) {
+ throw new TypeError('FooParent() must be called with new');
+ }
+}
+Object.defineProperty(window, "Foo", {
+ enumerable: false,
+ writable: true,
+ configurable: true,
+ value: function Foo() {
+ if (!new.target) {
+ throw new TypeError('Foo() must be called with new');
+ }
+ }
+ });
+Object.defineProperty(window.Foo, "prototype", {
+ writable: false,
+ value: new FooParent()
+ });
+Object.defineProperty(window.Foo.prototype, "constructor", {
+ enumerable: false,
+ writable: true,
+ configurable: true,
+ value: window.Foo
+ });
+Object.setPrototypeOf(Foo, FooParent);
+Foo.prototype[Symbol.toStringTag] = "Foo";
+
+var idlArray = new IdlArray();
+idlArray.add_untested_idls("interface FooParent {};");
+idlArray.add_idls(
+ "interface Foo : FooParent { constructor(); };"
+ );
+idlArray.add_objects({
+ Foo: ["new Foo()"],
+ FooParent: ["new FooParent()"]
+});
+idlArray.test();
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "OK",
+ "message": null
+ },
+ "summarized_tests": [
+ {
+ "name": "Foo interface object length",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Foo interface object name",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Foo interface: existence and properties of interface object",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Foo interface: existence and properties of interface prototype object",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Foo interface: existence and properties of interface prototype object's \"constructor\" property",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Foo interface: existence and properties of interface prototype object's @@unscopables property",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Foo must be primary interface of new Foo()",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Stringification of new Foo()",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ }
+ ],
+ "type": "complete"
+}
+</script>
+</body>
+</html>
diff --git a/test/wpt/tests/resources/test/tests/functional/idlharness/IdlInterface/test_to_json_operation.html b/test/wpt/tests/resources/test/tests/functional/idlharness/IdlInterface/test_to_json_operation.html
new file mode 100644
index 0000000..bbc502a
--- /dev/null
+++ b/test/wpt/tests/resources/test/tests/functional/idlharness/IdlInterface/test_to_json_operation.html
@@ -0,0 +1,177 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>IdlInterface.prototype.test_to_json_operation()</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/resources/WebIDLParser.js"></script>
+ <script src="/resources/idlharness.js"></script>
+ <script src="../../../../idl-helper.js"></script>
+</head>
+<body>
+<script>
+ "use strict";
+ function wrap(member, obj) {
+ function F(obj) {
+ this._obj = obj;
+ }
+
+ F.prototype.toJSON = function() {
+ return this._obj;
+ }
+ Object.defineProperty(F, 'name', { value: member.name });
+ return new F(obj);
+ }
+
+ var i, obj;
+ i = interfaceFrom("interface A { [Default] object toJSON(); attribute long foo; };");
+ i.test_to_json_operation("object", wrap(i, { foo: 123 }), i.members[0]);
+
+ // should fail (wrong type)
+ i = interfaceFrom("interface B { [Default] object toJSON(); attribute long foo; };");
+ i.test_to_json_operation("object", wrap(i, { foo: "a value" }), i.members[0]);
+
+ // should handle extraneous attributes (e.g. from an extension specification)
+ i = interfaceFrom("interface C { [Default] object toJSON(); attribute long foo; };");
+ i.test_to_json_operation("object", wrap(i, { foo: 123, bar: 456 }), i.members[0]);
+
+ // should fail (missing property)
+ i = interfaceFrom("interface D { [Default] object toJSON(); attribute long foo; };");
+ i.test_to_json_operation("object", wrap(i, { }), i.members[0]);
+
+ // should fail (should be writable)
+ obj = Object.defineProperties({}, { foo: {
+ writable: false,
+ enumerable: true,
+ configurable: true,
+ value: 123
+ }});
+ i = interfaceFrom("interface F { [Default] object toJSON(); attribute long foo; };");
+ i.test_to_json_operation("object", wrap(i, obj), i.members[0]);
+
+ // should fail (should be enumerable)
+ obj = Object.defineProperties({}, { foo: {
+ writable: true,
+ enumerable: false,
+ configurable: true,
+ value: 123
+ }});
+ i = interfaceFrom("interface G { [Default] object toJSON(); attribute long foo; };");
+ i.test_to_json_operation("object", wrap(i, obj), i.members[0]);
+
+ // should fail (should be configurable)
+ obj = Object.defineProperties({}, { foo: {
+ writable: true,
+ enumerable: true,
+ configurable: false,
+ value: 123
+ }});
+ i = interfaceFrom("interface H { [Default] object toJSON(); attribute long foo; };");
+ i.test_to_json_operation("object", wrap(i, obj), i.members[0]);
+
+ var idl = new IdlArray();
+ idl.add_idls("interface I : J { [Default] object toJSON(); attribute long foo; };");
+ idl.add_idls("interface J { [Default] object toJSON(); attribute DOMString foo;};");
+ var i = idl.members.I;
+ i.test_to_json_operation("object", wrap(i, { foo: 123 }), i.members[0]);
+
+ i = interfaceFrom("interface K { [Default] object toJSON(); };");
+ i.test_to_json_operation("object", wrap(i, {}), i.members[0]);
+
+ i = interfaceFrom("interface L { DOMString toJSON(); };");
+ i.test_to_json_operation("object", wrap(i, "a string"), i.members[0]);
+
+ // should fail (wrong output type)
+ i = interfaceFrom("interface M { DOMString toJSON(); };");
+ i.test_to_json_operation("object", wrap(i, {}), i.members[0]);
+
+ // should fail (not an IDL type)
+ i = interfaceFrom("interface N { DOMException toJSON(); };");
+ i.test_to_json_operation("object", wrap(i, {}), i.members[0]);
+</script>
+<script type="text/json" id="expected">
+ {
+ "summarized_status": {
+ "message": null,
+ "status_string": "OK"
+ },
+ "summarized_tests": [
+ {
+ "message": null,
+ "name": "A interface: default toJSON operation on object",
+ "properties": {},
+ "status_string": "PASS"
+ },
+ {
+ "message": "assert_equals: expected \"number\" but got \"string\"",
+ "name": "B interface: default toJSON operation on object",
+ "properties": {},
+ "status_string": "FAIL"
+ },
+ {
+ "message": null,
+ "name": "C interface: default toJSON operation on object",
+ "properties": {},
+ "status_string": "PASS"
+ },
+ {
+ "message": "assert_true: property \"foo\" should be present in the output of D.prototype.toJSON() expected true got false",
+ "name": "D interface: default toJSON operation on object",
+ "properties": {},
+ "status_string": "FAIL"
+ },
+ {
+ "message": "assert_true: property foo should be writable expected true got false",
+ "name": "F interface: default toJSON operation on object",
+ "properties": {},
+ "status_string": "FAIL"
+ },
+ {
+ "message": "assert_true: property foo should be enumerable expected true got false",
+ "name": "G interface: default toJSON operation on object",
+ "properties": {},
+ "status_string": "FAIL"
+ },
+ {
+ "message": "assert_true: property foo should be configurable expected true got false",
+ "name": "H interface: default toJSON operation on object",
+ "properties": {},
+ "status_string": "FAIL"
+ },
+ {
+ "message": null,
+ "name": "I interface: default toJSON operation on object",
+ "properties": {},
+ "status_string": "PASS"
+ },
+ {
+ "message": null,
+ "name": "K interface: default toJSON operation on object",
+ "properties": {},
+ "status_string": "PASS"
+ },
+ {
+ "message": null,
+ "name": "L interface: toJSON operation on object",
+ "properties": {},
+ "status_string": "PASS"
+ },
+ {
+ "message": "assert_equals: expected \"string\" but got \"object\"",
+ "name": "M interface: toJSON operation on object",
+ "properties": {},
+ "status_string": "FAIL"
+ },
+ {
+ "message": "assert_true: {\"type\":\"return-type\",\"extAttrs\":[],\"generic\":\"\",\"nullable\":false,\"union\":false,\"idlType\":\"DOMException\"} is not an appropriate return value for the toJSON operation of N expected true got false",
+ "name": "N interface: toJSON operation on object",
+ "properties": {},
+ "status_string": "FAIL"
+ }
+ ],
+ "type": "complete"
+ }
+</script>
+</body>
+</html>
diff --git a/test/wpt/tests/resources/test/tests/functional/idlharness/IdlNamespace/test_attribute.html b/test/wpt/tests/resources/test/tests/functional/idlharness/IdlNamespace/test_attribute.html
new file mode 100644
index 0000000..2c94061
--- /dev/null
+++ b/test/wpt/tests/resources/test/tests/functional/idlharness/IdlNamespace/test_attribute.html
@@ -0,0 +1,100 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>idlharness: namespace attribute</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/resources/WebIDLParser.js"></script>
+ <script src="/resources/idlharness.js"></script>
+</head>
+<body>
+<p>Verify the series of sub-tests that are executed for namespace attributes.</p>
+<script>
+"use strict";
+
+Object.defineProperty(self, "foo", {
+ value: {
+ truth: true,
+ },
+ writable: true,
+ enumerable: false,
+ configurable: true,
+});
+
+var idlArray = new IdlArray();
+idlArray.add_idls(`
+[Exposed=Window]
+namespace foo {
+ readonly attribute bool truth;
+ readonly attribute bool lies;
+};`);
+idlArray.test();
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "OK",
+ "message": null
+ },
+ "summarized_tests": [
+ {
+ "name": "foo namespace: extended attributes",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "foo namespace: property descriptor",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "foo namespace: [[Extensible]] is true",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "foo namespace: [[Prototype]] is Object.prototype",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "foo namespace: typeof is \"object\"",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "foo namespace: has no length property",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "foo namespace: has no name property",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "foo namespace: attribute truth",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "foo namespace: attribute lies",
+ "status_string": "FAIL",
+ "properties": {},
+ "message": "assert_own_property: foo does not have property \"lies\" expected property \"lies\" missing"
+ }
+ ],
+ "type": "complete"
+}
+</script>
+</body>
+</html>
diff --git a/test/wpt/tests/resources/test/tests/functional/idlharness/IdlNamespace/test_operation.html b/test/wpt/tests/resources/test/tests/functional/idlharness/IdlNamespace/test_operation.html
new file mode 100644
index 0000000..da70c8f
--- /dev/null
+++ b/test/wpt/tests/resources/test/tests/functional/idlharness/IdlNamespace/test_operation.html
@@ -0,0 +1,242 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>idlharness: namespace operation</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/resources/WebIDLParser.js"></script>
+ <script src="/resources/idlharness.js"></script>
+</head>
+<body>
+<p>Verify the series of sub-tests that are executed for namespace operations.</p>
+<script>
+"use strict";
+
+Object.defineProperty(self, "foo", {
+ value: Object.defineProperty({}, "Truth", {
+ value: function() {},
+ writable: true,
+ enumerable: true,
+ configurable: true,
+ }),
+ writable: true,
+ enumerable: false,
+ configurable: true,
+});
+
+Object.defineProperty(self, "bar", {
+ value: Object.defineProperty({}, "Truth", {
+ value: function() {},
+ writable: false,
+ enumerable: true,
+ configurable: false,
+ }),
+ writable: true,
+ enumerable: false,
+ configurable: true,
+});
+
+Object.defineProperty(self, "baz", {
+ value: {
+ LongStory: function(hero, ...details) {
+ return `${hero} went and ${details.join(', then')}`
+ },
+ ShortStory: function(...details) {
+ return `${details.join('. ')}`;
+ },
+ },
+ writable: true,
+ enumerable: false,
+ configurable: true,
+});
+
+var idlArray = new IdlArray();
+idlArray.add_idls(`
+[Exposed=Window]
+namespace foo {
+ undefined Truth();
+ undefined Lies();
+};
+[Exposed=Window]
+namespace bar {
+ [LegacyUnforgeable]
+ undefined Truth();
+};
+[Exposed=Window]
+namespace baz {
+ DOMString LongStory(any hero, DOMString... details);
+ DOMString ShortStory(DOMString... details);
+};`);
+idlArray.test();
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "OK",
+ "message": null
+ },
+ "summarized_tests": [
+ {
+ "name": "foo namespace: extended attributes",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "foo namespace: property descriptor",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "foo namespace: [[Extensible]] is true",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "foo namespace: [[Prototype]] is Object.prototype",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "foo namespace: typeof is \"object\"",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "foo namespace: has no length property",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "foo namespace: has no name property",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "foo namespace: operation Truth()",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "foo namespace: operation Lies()",
+ "status_string": "FAIL",
+ "properties": {},
+ "message": "assert_own_property: namespace object missing operation \"Lies\" expected property \"Lies\" missing"
+ },
+ {
+ "name": "bar namespace: extended attributes",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "bar namespace: property descriptor",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "bar namespace: [[Extensible]] is true",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "bar namespace: [[Prototype]] is Object.prototype",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "bar namespace: typeof is \"object\"",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "bar namespace: has no length property",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "bar namespace: has no name property",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "bar namespace: operation Truth()",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+
+ {
+ "name": "baz namespace: extended attributes",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "baz namespace: property descriptor",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "baz namespace: [[Extensible]] is true",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "baz namespace: [[Prototype]] is Object.prototype",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "baz namespace: typeof is \"object\"",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "baz namespace: has no length property",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "baz namespace: has no name property",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "baz namespace: operation LongStory(any, DOMString...)",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "baz namespace: operation ShortStory(DOMString...)",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ }
+ ],
+ "type": "complete"
+}
+</script>
+</body>
+</html>
diff --git a/test/wpt/tests/resources/test/tests/functional/idlharness/IdlNamespace/test_partial_namespace.html b/test/wpt/tests/resources/test/tests/functional/idlharness/IdlNamespace/test_partial_namespace.html
new file mode 100644
index 0000000..eabdcd1
--- /dev/null
+++ b/test/wpt/tests/resources/test/tests/functional/idlharness/IdlNamespace/test_partial_namespace.html
@@ -0,0 +1,125 @@
+<!DOCTYPE HTML>
+<html>
+
+<head>
+ <meta charset="utf-8">
+ <title>idlharness: Partial namespace</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/resources/WebIDLParser.js"></script>
+ <script src="/resources/idlharness.js"></script>
+</head>
+
+<body>
+ <p>Verify the series of sub-tests that are executed for "partial" namespace objects.</p>
+ <script>
+ "use strict";
+
+ // No original existence
+ (() => {
+ const idlArray = new IdlArray();
+ idlArray.add_idls('partial namespace A {};');
+ idlArray.test();
+ })();
+
+ // Valid exposure (Note: Worker -> {Shared,Dedicated,Service}Worker)
+ (() => {
+ const idlArray = new IdlArray();
+ idlArray.add_untested_idls(`
+ [Exposed=(Worker)]
+ namespace B {};
+
+ [Exposed=(DedicatedWorker, ServiceWorker, SharedWorker)]
+ namespace C {};`);
+ idlArray.add_idls(`
+ [Exposed=(DedicatedWorker, ServiceWorker, SharedWorker)]
+ partial namespace B {};
+
+ [Exposed=(Worker)]
+ partial namespace C {};`);
+ idlArray.merge_partials();
+ })();
+
+ // Invalid exposure
+ (() => {
+ const idlArray = new IdlArray();
+ idlArray.add_untested_idls(`
+ [Exposed=(Window, ServiceWorker)]
+ namespace D {};`);
+ idlArray.add_idls(`
+ [Exposed=(DedicatedWorker)]
+ partial namespace D {};`);
+ idlArray.merge_partials();
+ })();
+
+ // Original is an interface, not a namespace.
+ (() => {
+ const idlArray = new IdlArray();
+ idlArray.add_idls(`
+ partial namespace E {};
+ interface E {};`);
+ idlArray.merge_partials();
+ })();
+ </script>
+ <script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "OK",
+ "message": null
+ },
+ "summarized_tests": [
+ {
+ "name": "Partial namespace A: original namespace defined",
+ "status_string": "FAIL",
+ "properties": {},
+ "message": "assert_true: Original namespace should be defined expected true got false"
+ },
+ {
+ "name": "Partial namespace B: original namespace defined",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Partial namespace B: valid exposure set",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Partial namespace C: original namespace defined",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Partial namespace C: valid exposure set",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Partial namespace D: original namespace defined",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Partial namespace D: valid exposure set",
+ "status_string": "FAIL",
+ "properties": {},
+ "message": "Partial D namespace is exposed to 'DedicatedWorker', the original namespace is not."
+ },
+ {
+ "name": "Partial namespace E: original namespace defined",
+ "status_string": "FAIL",
+ "properties": {},
+ "message": "assert_true: Original E definition should have type namespace expected true got false"
+ }
+ ],
+ "type": "complete"
+}
+</script>
+</body>
+
+</html>
diff --git a/test/wpt/tests/resources/test/tests/functional/iframe-callback.html b/test/wpt/tests/resources/test/tests/functional/iframe-callback.html
new file mode 100644
index 0000000..f49d0aa
--- /dev/null
+++ b/test/wpt/tests/resources/test/tests/functional/iframe-callback.html
@@ -0,0 +1,116 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Example with iframe that notifies containing document via callbacks</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+</head>
+<body onload="start_test_in_iframe()">
+<h1>Callbacks From Tests Running In An IFRAME</h1>
+<p>A test is run inside an <tt>iframe</tt> with a same origin document. The
+containing document should receive callbacks as the tests progress inside the
+<tt>iframe</tt>. A single passing test is expected in the summary below.
+<div id="log"></div>
+
+<script>
+var callbacks = [];
+var START = 1
+var TEST_STATE = 2
+var RESULT = 3
+var COMPLETION = 4
+var test_complete = false;
+
+setup({explicit_done: true});
+
+// The following callbacks are called for tests in this document as well as the
+// tests in the IFRAME. Currently, callbacks invoked from this document and any
+// child document are indistinguishable from each other.
+
+function start_callback(properties) {
+ callbacks.push(START);
+}
+
+function test_state_callback(test) {
+ callbacks.push(TEST_STATE);
+}
+
+function result_callback(test) {
+ callbacks.push(RESULT);
+}
+
+function completion_callback(tests, status) {
+ if (test_complete) {
+ return;
+ }
+ test_complete = true;
+ callbacks.push(COMPLETION);
+ verify_received_callbacks();
+ done();
+}
+
+function verify_received_callbacks() {
+ var copy_of_callbacks = callbacks.slice(0);
+
+ // Note that you can't run test assertions directly in a callback even if
+ // this is a file test. When the callback is invoked from a same-origin child
+ // page, the callstack reaches into the calling child document. Any
+ // exception thrown in a callback will be handled by the child rather than
+ // this document.
+ test(
+ function() {
+ // callbacks list should look like:
+ // START 1*(TEST_STATE) RESULT COMPLETION
+ assert_equals(copy_of_callbacks.shift(), START,
+ "The first received callback should be 'start_callback'.");
+ assert_equals(copy_of_callbacks.shift(), TEST_STATE,
+ "'test_state_callback' should be received before any " +
+ "result or completion callbacks.");
+ while(copy_of_callbacks.length > 0) {
+ var callback = copy_of_callbacks.shift();
+ if (callback != TEST_STATE) {
+ copy_of_callbacks.unshift(callback);
+ break;
+ }
+ }
+ assert_equals(copy_of_callbacks.shift(), RESULT,
+ "'test_state_callback' should be followed by 'result_callback'.");
+ assert_equals(copy_of_callbacks.shift(), COMPLETION,
+ "Final 'result_callback' should be followed by 'completion_callback'.");
+ assert_equals(copy_of_callbacks.length, 0,
+ "'completion_callback' should be the last callback.");
+ });
+}
+
+function start_test_in_iframe() {
+ // This document is going to clear any received callbacks and maintain
+ // radio silence until the test in the iframe runs to completion. The
+ // completion_callback() will then complete the testing on this document.
+ callbacks.length = 0;
+ var iframe = document.createElement("iframe");
+ // single-page-test-pass.html has a single test.
+ iframe.src = "single-page-test-pass.html";
+ iframe.style.setProperty("display", "none");
+ document.getElementById("target").appendChild(iframe);
+}
+</script>
+
+<div id="target">
+</div>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "OK",
+ "message": null
+ },
+ "summarized_tests": [
+ {
+ "status_string": "PASS",
+ "name": "Example with iframe that notifies containing document via callbacks",
+ "properties": {},
+ "message": null
+ }
+ ],
+ "type": "complete"
+}
+</script>
+</body>
diff --git a/test/wpt/tests/resources/test/tests/functional/iframe-consolidate-errors.html b/test/wpt/tests/resources/test/tests/functional/iframe-consolidate-errors.html
new file mode 100644
index 0000000..ef9b870
--- /dev/null
+++ b/test/wpt/tests/resources/test/tests/functional/iframe-consolidate-errors.html
@@ -0,0 +1,50 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Example with iframe that consolidates errors via fetch_tests_from_window</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+var parent_test = async_test("Test executing in parent context");
+</script>
+</head>
+<body onload="parent_test.done()">
+<h1>Fetching Tests From a Child Context</h1>
+<p>This test demonstrates the use of <tt>fetch_tests_from_window</tt> to pull
+tests from an <tt>iframe</tt> into the primary document.</p>
+<p>The test suite is expected to fail due to an unhandled exception in the
+child context.</p>
+<div id="log"></div>
+
+<iframe id="childContext" src="uncaught-exception-handle.html" style="display:none"></iframe>
+<!-- apisample4.html is a failing suite due to an unhandled Error. -->
+
+<script>
+ var childContext = document.getElementById("childContext");
+ fetch_tests_from_window(childContext.contentWindow);
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "ERROR",
+ "message": "Error in remote: Error: Example Error"
+ },
+ "summarized_tests": [
+ {
+ "status_string": "PASS",
+ "name": "Test executing in parent context",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "NOTRUN",
+ "name": "This should show a harness status of 'Error' and a test status of 'Not Run'",
+ "properties": {},
+ "message": null
+ }
+ ],
+ "type": "complete"
+}
+</script>
+</body>
+</html>
diff --git a/test/wpt/tests/resources/test/tests/functional/iframe-consolidate-tests.html b/test/wpt/tests/resources/test/tests/functional/iframe-consolidate-tests.html
new file mode 100644
index 0000000..246ddde
--- /dev/null
+++ b/test/wpt/tests/resources/test/tests/functional/iframe-consolidate-tests.html
@@ -0,0 +1,85 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Example with iframe that consolidates tests via fetch_tests_from_window</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+var parent_test = async_test("Test executing in parent context");
+</script>
+</head>
+<body onload="parent_test.done()">
+<h1>Fetching Tests From a Child Context</h1>
+<p>This test demonstrates the use of <tt>fetch_tests_from_window</tt> to pull
+tests from an <tt>iframe</tt> into the primary document.</p>
+<p>The test suite will not complete until tests in the child context have finished
+executing</p>
+<div id="log"></div>
+
+<iframe id="childContext" src="promise-async.html" style="display:none"></iframe>
+
+<script>
+ var childContext = document.getElementById("childContext");
+ fetch_tests_from_window(childContext.contentWindow);
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "OK",
+ "message": null
+ },
+ "summarized_tests": [
+ {
+ "status_string": "PASS",
+ "name": "Promise rejection",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "Promise resolution",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "FAIL",
+ "name": "Promises and test assertion failures (should fail)",
+ "properties": {},
+ "message": "assert_true: This failure is expected expected true got false"
+ },
+ {
+ "status_string": "PASS",
+ "name": "Promises are supported in your browser",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "Promises resolution chaining",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "Test executing in parent context",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "Use of step_func with Promises",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "FAIL",
+ "name": "Use of unreached_func with Promises (should fail)",
+ "properties": {},
+ "message": "assert_unreached: This failure is expected Reached unreachable code"
+ }
+ ],
+ "type": "complete"
+}
+</script>
+</body>
+</html>
diff --git a/test/wpt/tests/resources/test/tests/functional/iframe-msg.html b/test/wpt/tests/resources/test/tests/functional/iframe-msg.html
new file mode 100644
index 0000000..283a5d9
--- /dev/null
+++ b/test/wpt/tests/resources/test/tests/functional/iframe-msg.html
@@ -0,0 +1,84 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Example with iframe that notifies containing document via cross document messaging</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+</head>
+<body>
+<h1>Notifications From Tests Running In An IFRAME</h1>
+<p>A test is run inside an <tt>iframe</tt> with a same origin document. The
+containing document should receive messages via <tt>postMessage</tt>/
+<tt>onmessage</tt> as the tests progress inside the <tt>iframe</tt>. A single
+passing test is expected in the summary below.
+</p>
+<div id="log"></div>
+
+<script>
+var t = async_test("Containing document receives messages");
+var start_received = false;
+var result_received = false;
+var completion_received = false;
+
+// These are the messages that are expected to be seen while running the tests
+// in the IFRAME.
+var expected_messages = [
+ t.step_func(
+ function(message) {
+ assert_equals(message.data.type, "start");
+ assert_own_property(message.data, "properties");
+ }),
+
+ t.step_func(
+ function(message) {
+ assert_equals(message.data.type, "test_state");
+ assert_equals(message.data.test.status, message.data.test.NOTRUN);
+ }),
+
+ t.step_func(
+ function(message) {
+ assert_equals(message.data.type, "result");
+ assert_equals(message.data.test.status, message.data.test.PASS);
+ }),
+
+ t.step_func(
+ function(message) {
+ assert_equals(message.data.type, "complete");
+ assert_equals(message.data.tests.length, 1);
+ assert_equals(message.data.tests[0].status,
+ message.data.tests[0].PASS);
+ assert_equals(message.data.status.status, message.data.status.OK);
+ t.done();
+ }),
+
+ t.unreached_func("Too many messages received")
+];
+
+on_event(window,
+ "message",
+ function(message) {
+ var handler = expected_messages.shift();
+ handler(message);
+ });
+</script>
+<iframe src="single-page-test-pass.html" style="display:none">
+ <!-- single-page-test-pass.html implements a file_is_test test. -->
+</iframe>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "OK",
+ "message": null
+ },
+ "summarized_tests": [
+ {
+ "status_string": "PASS",
+ "name": "Containing document receives messages",
+ "properties": {},
+ "message": null
+ }
+ ],
+ "type": "complete"
+}
+</script>
+</body>
diff --git a/test/wpt/tests/resources/test/tests/functional/log-insertion.html b/test/wpt/tests/resources/test/tests/functional/log-insertion.html
new file mode 100644
index 0000000..9a63c3d
--- /dev/null
+++ b/test/wpt/tests/resources/test/tests/functional/log-insertion.html
@@ -0,0 +1,46 @@
+<!DOCTYPE HTML>
+<title>Log insertion</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+test(function(t) {
+ assert_equals(document.body, null);
+}, "Log insertion before load");
+test(function(t) {
+ assert_equals(document.body, null);
+}, "Log insertion before load (again)");
+async_test(function(t) {
+ window.onload = t.step_func_done(function() {
+ var body = document.body;
+ assert_not_equals(body, null);
+
+ var log = document.getElementById("log");
+ assert_equals(log.parentNode, body);
+ });
+}, "Log insertion after load");
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "OK",
+ "message": null
+ },
+ "summarized_tests": [{
+ "status_string": "PASS",
+ "name": "Log insertion before load",
+ "message": null,
+ "properties": {}
+ }, {
+ "status_string": "PASS",
+ "name": "Log insertion before load (again)",
+ "message": null,
+ "properties": {}
+ }, {
+ "status_string": "PASS",
+ "name": "Log insertion after load",
+ "message": null,
+ "properties": {}
+ }],
+ "type": "complete"
+}
+</script>
diff --git a/test/wpt/tests/resources/test/tests/functional/no-title.html b/test/wpt/tests/resources/test/tests/functional/no-title.html
new file mode 100644
index 0000000..a337e4e
--- /dev/null
+++ b/test/wpt/tests/resources/test/tests/functional/no-title.html
@@ -0,0 +1,146 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Tests with no title</title>
+</head>
+<script src="/resources/testharness.js"></script>
+
+<body>
+<h1>Tests with no title</h1>
+<div id="log"></div>
+<script>
+ test(function(){assert_true(true, '1')});
+ test(()=>assert_true(true, '2'));
+ test(() => assert_true(true, '3'));
+ test(() => assert_true(true, '3')); // test duplicate behaviour
+ test(() => assert_true(true, '3')); // test duplicate behaviour
+ test(() => {
+ assert_true(true, '4');
+ });
+ test(() => { assert_true(true, '5') });
+ test(() => { assert_true(true, '6') } );
+ test(() => { assert_true(true, '7'); });
+ test(() => {});
+ test(() => { });
+ test(() => {;;;;});
+ test(() => { ; ; ; ; });
+ test(()=>{});
+ test(()=>{ });
+ test(()=>{;;;;});
+ test(()=>{ ; ; ; ; });
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "OK",
+ "message": null
+ },
+ "summarized_tests": [
+ {
+ "status_string": "PASS",
+ "name": "Tests with no title",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "assert_true(true, '2')",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "assert_true(true, '3')",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "assert_true(true, '3') 1",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "assert_true(true, '3') 2",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "Tests with no title 1",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "assert_true(true, '5')",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "assert_true(true, '6')",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "assert_true(true, '7')",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "Tests with no title 2",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "Tests with no title 3",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "Tests with no title 4",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "Tests with no title 5",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "Tests with no title 6",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "Tests with no title 7",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "Tests with no title 8",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "Tests with no title 9",
+ "properties": {},
+ "message": null
+ }
+ ],
+ "type": "complete"
+}
+</script>
+</body>
+</html>
diff --git a/test/wpt/tests/resources/test/tests/functional/order.html b/test/wpt/tests/resources/test/tests/functional/order.html
new file mode 100644
index 0000000..6863838
--- /dev/null
+++ b/test/wpt/tests/resources/test/tests/functional/order.html
@@ -0,0 +1,36 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Ordering</title>
+<meta name="timeout" content="6000">
+</head>
+<body>
+<div id="log"></div>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+test(function() {}, 'second');
+test(function() {}, 'first');
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "OK",
+ "message": null
+ },
+ "summarized_tests": [{
+ "status_string": "PASS",
+ "name": "first",
+ "message": null,
+ "properties": {}
+ }, {
+ "status_string": "PASS",
+ "name": "second",
+ "message": null,
+ "properties": {}
+ }],
+ "type": "complete"
+}
+</script>
+</body>
+</html>
diff --git a/test/wpt/tests/resources/test/tests/functional/promise-async.html b/test/wpt/tests/resources/test/tests/functional/promise-async.html
new file mode 100644
index 0000000..fa82665
--- /dev/null
+++ b/test/wpt/tests/resources/test/tests/functional/promise-async.html
@@ -0,0 +1,172 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Async Tests and Promises</title>
+</head>
+<body>
+<h1>Async Tests and Promises</h1>
+<p>This test assumes ECMAScript 6 Promise support. Some failures are expected.</p>
+<div id="log"></div>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+
+test(function() {
+ var p = new Promise(function(resolve, reject) {});
+ assert_true('then' in p);
+ assert_equals(typeof Promise.resolve, 'function');
+ assert_equals(typeof Promise.reject, 'function');
+}, "Promises are supported in your browser");
+
+(function() {
+ var t = async_test("Promise resolution");
+ t.step(function() {
+ Promise.resolve('x').then(
+ t.step_func(function(value) {
+ assert_equals(value, 'x');
+ t.done();
+ }),
+ t.unreached_func('Promise should not reject')
+ );
+ });
+}());
+
+(function() {
+ var t = async_test("Promise rejection");
+ t.step(function() {
+ Promise.reject(Error('fail')).then(
+ t.unreached_func('Promise should reject'),
+ t.step_func(function(reason) {
+ assert_true(reason instanceof Error);
+ assert_equals(reason.message, 'fail');
+ t.done();
+ })
+ );
+ });
+}());
+
+(function() {
+ var t = async_test("Promises resolution chaining");
+ t.step(function() {
+ var resolutions = [];
+ Promise.resolve('a').then(
+ t.step_func(function(value) {
+ resolutions.push(value);
+ return 'b';
+ })
+ ).then(
+ t.step_func(function(value) {
+ resolutions.push(value);
+ return 'c';
+ })
+ ).then(
+ t.step_func(function(value) {
+ resolutions.push(value);
+
+ assert_array_equals(resolutions, ['a', 'b', 'c']);
+ t.done();
+ })
+ ).catch(
+ t.unreached_func('promise should not have rejected')
+ );
+ });
+}());
+
+(function() {
+ var t = async_test("Use of step_func with Promises");
+ t.step(function() {
+ var resolutions = [];
+ Promise.resolve('x').then(
+ t.step_func_done(),
+ t.unreached_func('Promise should not have rejected')
+ );
+ });
+}());
+
+(function() {
+ var t = async_test("Promises and test assertion failures (should fail)");
+ t.step(function() {
+ var resolutions = [];
+ Promise.resolve('x').then(
+ t.step_func(function(value) {
+ assert_true(false, 'This failure is expected');
+ })
+ ).then(
+ t.unreached_func('Promise should not have resolved')
+ ).catch(
+ t.unreached_func('Promise should not have rejected')
+ );
+ });
+}());
+
+(function() {
+ var t = async_test("Use of unreached_func with Promises (should fail)");
+ t.step(function() {
+ var resolutions = [];
+ var r;
+ var p = new Promise(function(resolve, reject) {
+ // Reject instead of resolve, to demonstrate failure.
+ reject(123);
+ });
+ p.then(
+ function(value) {
+ assert_equals(value, 123, 'This should not actually happen');
+ },
+ t.unreached_func('This failure is expected')
+ );
+ });
+}());
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "OK",
+ "message": null
+ },
+ "summarized_tests": [
+ {
+ "status_string": "PASS",
+ "name": "Promise rejection",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "Promise resolution",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "FAIL",
+ "name": "Promises and test assertion failures (should fail)",
+ "properties": {},
+ "message": "assert_true: This failure is expected expected true got false"
+ },
+ {
+ "status_string": "PASS",
+ "name": "Promises are supported in your browser",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "Promises resolution chaining",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "Use of step_func with Promises",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "FAIL",
+ "name": "Use of unreached_func with Promises (should fail)",
+ "properties": {},
+ "message": "assert_unreached: This failure is expected Reached unreachable code"
+ }
+ ],
+ "type": "complete"
+}
+</script>
diff --git a/test/wpt/tests/resources/test/tests/functional/promise-with-sync.html b/test/wpt/tests/resources/test/tests/functional/promise-with-sync.html
new file mode 100644
index 0000000..e8e680a
--- /dev/null
+++ b/test/wpt/tests/resources/test/tests/functional/promise-with-sync.html
@@ -0,0 +1,79 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Promise Tests and Synchronous Tests</title>
+</head>
+<body>
+<h1>Promise Tests</h1>
+<p>This test demonstrates the use of <tt>promise_test</tt> alongside synchronous tests.</p>
+<div id="log"></div>
+<script src="../../../testharness.js"></script>
+<script src="../../../testharnessreport.js"></script>
+<script>
+"use strict";
+var sequence = [];
+
+test(function(t) {
+ assert_array_equals(sequence, []);
+ sequence.push(1);
+}, "first synchronous test");
+
+promise_test(function() {
+ assert_array_equals(sequence, [1, 2]);
+
+ return Promise.resolve()
+ .then(function() {
+ assert_array_equals(sequence, [1, 2]);
+ sequence.push(3);
+ });
+}, "first promise_test");;
+
+test(function(t) {
+ assert_array_equals(sequence, [1]);
+ sequence.push(2);
+}, "second synchronous test");
+
+promise_test(function() {
+ assert_array_equals(sequence, [1, 2, 3]);
+
+ return Promise.resolve()
+ .then(function() {
+ assert_array_equals(sequence, [1, 2, 3]);
+ });
+}, "second promise_test");;
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "message": null,
+ "status_string": "OK"
+ },
+ "summarized_tests": [
+ {
+ "message": null,
+ "properties": {},
+ "name": "first promise_test",
+ "status_string": "PASS"
+ },
+ {
+ "message": null,
+ "properties": {},
+ "name": "first synchronous test",
+ "status_string": "PASS"
+ },
+ {
+ "message": null,
+ "properties": {},
+ "name": "second promise_test",
+ "status_string": "PASS"
+ },
+ {
+ "message": null,
+ "properties": {},
+ "name": "second synchronous test",
+ "status_string": "PASS"
+ }
+ ],
+ "type": "complete"
+}
+</script>
diff --git a/test/wpt/tests/resources/test/tests/functional/promise.html b/test/wpt/tests/resources/test/tests/functional/promise.html
new file mode 100644
index 0000000..f35feb0
--- /dev/null
+++ b/test/wpt/tests/resources/test/tests/functional/promise.html
@@ -0,0 +1,219 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Promise Tests</title>
+</head>
+<body>
+<h1>Promise Tests</h1>
+<p>This test demonstrates the use of <tt>promise_test</tt>. Assumes ECMAScript 6
+Promise support. Some failures are expected.</p>
+<div id="log"></div>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+test(
+ function() {
+ var p = new Promise(function(resolve, reject){});
+ assert_true("then" in p);
+ assert_equals(typeof Promise.resolve, "function");
+ assert_equals(typeof Promise.reject, "function");
+ },
+ "Promises are supported in your browser");
+
+promise_test(
+ function() {
+ return Promise.resolve("x")
+ .then(
+ function(value) {
+ assert_equals(value,
+ "x",
+ "Fulfilled promise should pass result to " +
+ "fulfill reaction.");
+ });
+ },
+ "Promise fulfillment with result");
+
+promise_test(
+ function(t) {
+ return Promise.reject(new Error("fail"))
+ .then(t.unreached_func("Promise should reject"),
+ function(reason) {
+ assert_true(
+ reason instanceof Error,
+ "Rejected promise should pass reason to fulfill reaction.");
+ assert_equals(
+ reason.message,
+ "fail",
+ "Rejected promise should pass reason to reject reaction.");
+ });
+ },
+ "Promise rejection with result");
+
+promise_test(
+ function() {
+ var resolutions = [];
+ return Promise.resolve("a")
+ .then(
+ function(value) {
+ resolutions.push(value);
+ return "b";
+ })
+ .then(
+ function(value) {
+ resolutions.push(value);
+ return "c";
+ })
+ .then(
+ function(value) {
+ resolutions.push(value);
+ assert_array_equals(resolutions, ["a", "b", "c"]);
+ });
+ },
+ "Chain of promise resolutions");
+
+promise_test(
+ function(t) {
+ var resolutions = [];
+ return Promise.resolve("x")
+ .then(
+ function(value) {
+ assert_true(false, "Expected failure.");
+ })
+ .then(t.unreached_func("UNEXPECTED FAILURE: Promise should not have resolved."));
+ },
+ "Assertion failure in a fulfill reaction (should FAIL with an expected failure)");
+
+promise_test(
+ function(t) {
+ return new Promise(
+ function(resolve, reject) {
+ reject(123);
+ })
+ .then(t.unreached_func("UNEXPECTED FAILURE: Fulfill reaction reached after rejection."),
+ t.unreached_func("Expected failure."));
+ },
+ "unreached_func as reactor (should FAIL with an expected failure)");
+
+promise_test(
+ function() {
+ return true;
+ },
+ "promise_test with function that doesn't return a Promise (should FAIL)");
+
+promise_test(function(){},
+ "promise_test with function that doesn't return anything");
+
+promise_test(
+ function() { return { then: 23 }; },
+ "promise_test that returns a non-thenable (should FAIL)");
+
+promise_test(
+ function() {
+ return Promise.reject("Expected rejection");
+ },
+ "promise_test with unhandled rejection (should FAIL)");
+
+promise_test(
+ function() {
+ return Promise.resolve(10)
+ .then(
+ function(value) {
+ throw Error("Expected exception.");
+ });
+ },
+ "promise_test with unhandled exception in fulfill reaction (should FAIL)");
+
+promise_test(
+ function(t) {
+ return Promise.reject(10)
+ .then(
+ t.unreached_func("UNEXPECTED FAILURE: Fulfill reaction reached after rejection."),
+ function(value) {
+ throw Error("Expected exception.");
+ });
+ },
+ "promise_test with unhandled exception in reject reaction (should FAIL)");
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "OK",
+ "message": null
+ },
+ "summarized_tests": [
+ {
+ "status_string": "FAIL",
+ "name": "Assertion failure in a fulfill reaction (should FAIL with an expected failure)",
+ "message": "assert_true: Expected failure. expected true got false",
+ "properties": {}
+ },
+ {
+ "status_string": "PASS",
+ "name": "Chain of promise resolutions",
+ "message": null,
+ "properties": {}
+ },
+ {
+ "status_string": "PASS",
+ "name": "Promise fulfillment with result",
+ "message": null,
+ "properties": {}
+ },
+ {
+ "status_string": "PASS",
+ "name": "Promise rejection with result",
+ "message": null,
+ "properties": {}
+ },
+ {
+ "status_string": "PASS",
+ "name": "Promises are supported in your browser",
+ "message": null,
+ "properties": {}
+ },
+ {
+ "status_string": "FAIL",
+ "name": "promise_test with function that doesn't return a Promise (should FAIL)",
+ "message": "promise_test: test body must return a 'thenable' object (received an object with no `then` method)",
+ "properties": {}
+ },
+ {
+ "status_string": "FAIL",
+ "name": "promise_test with function that doesn't return anything",
+ "message": "promise_test: test body must return a 'thenable' object (received undefined)",
+ "properties": {}
+ },
+ {
+ "status_string": "FAIL",
+ "name": "promise_test that returns a non-thenable (should FAIL)",
+ "message": "promise_test: test body must return a 'thenable' object (received an object with no `then` method)",
+ "properties": {}
+ },
+ {
+ "status_string": "FAIL",
+ "name": "promise_test with unhandled exception in fulfill reaction (should FAIL)",
+ "message": "promise_test: Unhandled rejection with value: object \"Error: Expected exception.\"",
+ "properties": {}
+ },
+ {
+ "status_string": "FAIL",
+ "name": "promise_test with unhandled exception in reject reaction (should FAIL)",
+ "message": "promise_test: Unhandled rejection with value: object \"Error: Expected exception.\"",
+ "properties": {}
+ },
+ {
+ "status_string": "FAIL",
+ "name": "promise_test with unhandled rejection (should FAIL)",
+ "message": "promise_test: Unhandled rejection with value: \"Expected rejection\"",
+ "properties": {}
+ },
+ {
+ "status_string": "FAIL",
+ "name": "unreached_func as reactor (should FAIL with an expected failure)",
+ "message": "assert_unreached: Expected failure. Reached unreachable code",
+ "properties": {}
+ }
+ ],
+ "type": "complete"
+}
+</script>
diff --git a/test/wpt/tests/resources/test/tests/functional/queue.html b/test/wpt/tests/resources/test/tests/functional/queue.html
new file mode 100644
index 0000000..0c72128
--- /dev/null
+++ b/test/wpt/tests/resources/test/tests/functional/queue.html
@@ -0,0 +1,130 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Test queuing synchronous tests</title>
+<script src="../../../testharness.js"></script>
+<script src="../../../testharnessreport.js"></script>
+</head>
+<body>
+<div id="log"></div>
+
+<script>
+"use strict";
+var inInitialTurn = true;
+
+test(function(t) {
+ assert_true(
+ inInitialTurn, "should execute in the initial turn of the event loop"
+ );
+}, "First synchronous test");
+
+test(function(t) {
+ assert_true(
+ inInitialTurn, "should execute in the initial turn of the event loop"
+ );
+}, "Second synchronous test");
+
+async_test(function(t) {
+ assert_true(
+ inInitialTurn, "should execute in the initial turn of the event loop"
+ );
+ t.done();
+}, "First async_test (run in parallel)");
+
+async_test(function(t) {
+ assert_true(
+ inInitialTurn, "should execute in the initial turn of the event loop"
+ );
+ t.done();
+}, "Second async_test (run in parallel)");
+
+test(function(t) {
+ assert_true(
+ inInitialTurn, "should execute in the initial turn of the event loop"
+ );
+}, "Third synchronous test");
+
+promise_test(function(t) {
+ assert_false(
+ inInitialTurn, "should not execute in the initial turn of the event loop"
+ );
+
+ return Promise.resolve();
+}, "promise_test");
+
+async_test(function(t) {
+ assert_true(
+ inInitialTurn, "should execute in the initial turn of the event loop"
+ );
+ t.done();
+}, "Third async_test (run in parallel)");
+
+test(function(t) {
+ assert_true(
+ inInitialTurn, "should execute in the initial turn of the event loop"
+ );
+}, "Fourth synchronous test");
+
+inInitialTurn = false;
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "OK",
+ "message": null
+ },
+ "summarized_tests": [
+ {
+ "properties": {},
+ "name": "First async_test (run in parallel)",
+ "status_string": "PASS",
+ "message": null
+ },
+ {
+ "properties": {},
+ "name": "First synchronous test",
+ "status_string": "PASS",
+ "message": null
+ },
+ {
+ "properties": {},
+ "name": "Fourth synchronous test",
+ "status_string": "PASS",
+ "message": null
+ },
+ {
+ "properties": {},
+ "name": "Second async_test (run in parallel)",
+ "status_string": "PASS",
+ "message": null
+ },
+ {
+ "properties": {},
+ "name": "Second synchronous test",
+ "status_string": "PASS",
+ "message": null
+ },
+ {
+ "properties": {},
+ "name": "Third async_test (run in parallel)",
+ "status_string": "PASS",
+ "message": null
+ },
+ {
+ "properties": {},
+ "name": "Third synchronous test",
+ "status_string": "PASS",
+ "message": null
+ },
+ {
+ "properties": {},
+ "name": "promise_test",
+ "status_string": "PASS",
+ "message": null
+ }
+ ],
+ "type": "complete"
+}
+</script>
+</body>
+</html>
diff --git a/test/wpt/tests/resources/test/tests/functional/setup-function-worker.js b/test/wpt/tests/resources/test/tests/functional/setup-function-worker.js
new file mode 100644
index 0000000..82c1456
--- /dev/null
+++ b/test/wpt/tests/resources/test/tests/functional/setup-function-worker.js
@@ -0,0 +1,14 @@
+importScripts("/resources/testharness.js");
+
+// Regression test for https://github.com/web-platform-tests/wpt/issues/27299,
+// where we broke the ability for a setup function in a worker to contain an
+// assertion (even a passing one).
+setup(function() {
+ assert_true(true, "True is true");
+});
+
+// We must define at least one test for the harness, though it is not what we
+// are testing here.
+test(function() {
+ assert_false(false, "False is false");
+}, 'Worker test');
diff --git a/test/wpt/tests/resources/test/tests/functional/setup-worker-service.html b/test/wpt/tests/resources/test/tests/functional/setup-worker-service.html
new file mode 100644
index 0000000..9f24ada
--- /dev/null
+++ b/test/wpt/tests/resources/test/tests/functional/setup-worker-service.html
@@ -0,0 +1,86 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Setup function in a service worker</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+</head>
+<body>
+<h1>Setup function in a service worker</h1>
+<p>This test assumes that the browser supports <a href="http://www.w3.org/TR/service-workers/">ServiceWorkers</a>.
+<div id="log"></div>
+
+<script>
+test(function(t) {
+ assert_true("serviceWorker" in navigator,
+ "navigator.serviceWorker exists");
+}, "Browser supports ServiceWorker");
+
+promise_test(function() {
+ // Since the service worker registration could be in an indeterminate
+ // state (due to, for example, a previous test run failing), we start by
+ // unregstering our service worker and then registering it again.
+ var scope = "service-worker-scope";
+ var worker_url = "setup-function-worker.js";
+
+ return navigator.serviceWorker.register(worker_url, {scope: scope})
+ .then(function(registration) {
+ return registration.unregister();
+ }).then(function() {
+ return navigator.serviceWorker.register(worker_url, {scope: scope});
+ }).then(function(registration) {
+ add_completion_callback(function() {
+ registration.unregister();
+ });
+
+ return new Promise(function(resolve) {
+ registration.addEventListener("updatefound", function() {
+ resolve(registration.installing);
+ });
+ });
+ }).then(function(worker) {
+ fetch_tests_from_worker(worker);
+ });
+}, "Register ServiceWorker");
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "OK",
+ "message": null
+ },
+ "summarized_tests": [
+ {
+ "status_string": "PASS",
+ "name": "Browser supports ServiceWorker",
+ "properties": {},
+ "message": null
+ },
+ {
+ "message": null,
+ "name": "Register ServiceWorker",
+ "properties": {},
+ "status_string": "PASS"
+ },
+ {
+ "message": null,
+ "name": "Worker test",
+ "properties": {},
+ "status_string": "PASS"
+ }
+ ],
+ "summarized_asserts": [
+ {
+ "assert_name": "assert_true",
+ "test": "Browser supports ServiceWorker",
+ "args": [
+ "true",
+ "\"navigator.serviceWorker exists\""
+ ],
+ "status": 0
+ }
+ ],
+ "type": "complete"
+}
+</script>
+</body>
diff --git a/test/wpt/tests/resources/test/tests/functional/single-page-test-fail.html b/test/wpt/tests/resources/test/tests/functional/single-page-test-fail.html
new file mode 100644
index 0000000..8bbd530
--- /dev/null
+++ b/test/wpt/tests/resources/test/tests/functional/single-page-test-fail.html
@@ -0,0 +1,28 @@
+<!DOCTYPE HTML>
+<title>Example with file_is_test (should fail)</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+setup({ single_test: true });
+onload = function() {
+ assert_true(false);
+ done();
+}
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "OK",
+ "message": null
+ },
+ "summarized_tests": [
+ {
+ "status_string": "FAIL",
+ "name": "Example with file_is_test (should fail)",
+ "properties": {},
+ "message": "uncaught exception: Error: assert_true: expected true got false"
+ }
+ ],
+ "type": "complete"
+}
+</script>
diff --git a/test/wpt/tests/resources/test/tests/functional/single-page-test-no-assertions.html b/test/wpt/tests/resources/test/tests/functional/single-page-test-no-assertions.html
new file mode 100644
index 0000000..9b39d2a
--- /dev/null
+++ b/test/wpt/tests/resources/test/tests/functional/single-page-test-no-assertions.html
@@ -0,0 +1,25 @@
+<!DOCTYPE HTML>
+<title>Example single page test with no asserts</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+setup({ single_test: true });
+done();
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "OK",
+ "message": null
+ },
+ "summarized_tests": [
+ {
+ "status_string": "PASS",
+ "name": "Example single page test with no asserts",
+ "properties": {},
+ "message": null
+ }
+ ],
+ "type": "complete"
+}
+</script>
diff --git a/test/wpt/tests/resources/test/tests/functional/single-page-test-no-body.html b/test/wpt/tests/resources/test/tests/functional/single-page-test-no-body.html
new file mode 100644
index 0000000..cb018f4
--- /dev/null
+++ b/test/wpt/tests/resources/test/tests/functional/single-page-test-no-body.html
@@ -0,0 +1,26 @@
+<!DOCTYPE HTML>
+<title>Example single page test with no body</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+setup({ single_test: true });
+assert_true(true);
+done();
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "OK",
+ "message": null
+ },
+ "summarized_tests": [
+ {
+ "status_string": "PASS",
+ "name": "Example single page test with no body",
+ "properties": {},
+ "message": null
+ }
+ ],
+ "type": "complete"
+}
+</script>
diff --git a/test/wpt/tests/resources/test/tests/functional/single-page-test-pass.html b/test/wpt/tests/resources/test/tests/functional/single-page-test-pass.html
new file mode 100644
index 0000000..e143e22
--- /dev/null
+++ b/test/wpt/tests/resources/test/tests/functional/single-page-test-pass.html
@@ -0,0 +1,28 @@
+<!DOCTYPE HTML>
+<title>Example with file_is_test</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+setup({ single_test: true });
+onload = function() {
+ assert_true(true);
+ done();
+}
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "OK",
+ "message": null
+ },
+ "summarized_tests": [
+ {
+ "status_string": "PASS",
+ "name": "Example with file_is_test",
+ "properties": {},
+ "message": null
+ }
+ ],
+ "type": "complete"
+}
+</script>
diff --git a/test/wpt/tests/resources/test/tests/functional/step_wait.html b/test/wpt/tests/resources/test/tests/functional/step_wait.html
new file mode 100644
index 0000000..ae3442c
--- /dev/null
+++ b/test/wpt/tests/resources/test/tests/functional/step_wait.html
@@ -0,0 +1,57 @@
+<!doctype html>
+<title>Tests for step_wait</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<div id=log></div>
+<script>
+promise_test(async t => {
+ let x = 1;
+ Promise.resolve().then(() => ++x);
+ await t.step_wait(() => x === 1);
+ assert_equals(x, 2);
+}, "Basic step_wait() test");
+
+promise_test(async t => {
+ let cond = false;
+ let x = 0;
+ setTimeout(() => cond = true, 100);
+ await t.step_wait(() => {
+ ++x;
+ return cond;
+ });
+ assert_equals(x, 2);
+}, "step_wait() isn't invoked too often");
+
+promise_test(async t => {
+ await t.step_wait(); // Throws
+}, "step_wait() takes an argument");
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_tests": [
+ {
+ "name": "Basic step_wait() test",
+ "message": null,
+ "properties": {},
+ "status_string": "PASS"
+ },
+ {
+ "name": "step_wait() isn't invoked too often",
+ "message": null,
+ "properties": {},
+ "status_string": "PASS"
+ },
+ {
+ "name": "step_wait() takes an argument",
+ "message": "cond is not a function",
+ "properties": {},
+ "status_string": "FAIL"
+ }
+ ],
+ "type": "complete",
+ "summarized_status": {
+ "status_string": "OK",
+ "message": null
+ }
+}
+</script>
diff --git a/test/wpt/tests/resources/test/tests/functional/step_wait_func.html b/test/wpt/tests/resources/test/tests/functional/step_wait_func.html
new file mode 100644
index 0000000..9fed18a
--- /dev/null
+++ b/test/wpt/tests/resources/test/tests/functional/step_wait_func.html
@@ -0,0 +1,49 @@
+<!doctype html>
+<title>Tests for step_wait_func and step_wait_func_done</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<div id=log></div>
+<script>
+async_test(t => {
+ let x = 0;
+ let step_x = 0;
+ setTimeout(() => ++x, 100);
+ t.step_wait_func(() => {
+ ++step_x;
+ return x === 1;
+ }, () => {
+ assert_equals(step_x, 2);
+ t.done();
+ });
+}, "Basic step_wait_func() test");
+
+async_test(t => {
+ let x = 0;
+ setTimeout(() => ++x, 100);
+ t.step_wait_func_done(() => true, () => assert_equals(x, 0));
+}, "Basic step_wait_func_done() test");
+
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "message": null,
+ "status_string": "OK"
+ },
+ "summarized_tests": [
+ {
+ "properties": {},
+ "message": null,
+ "name": "Basic step_wait_func() test",
+ "status_string": "PASS"
+ },
+ {
+ "properties": {},
+ "message": null,
+ "name": "Basic step_wait_func_done() test",
+ "status_string": "PASS"
+ }
+ ],
+ "type": "complete"
+}
+</script>
diff --git a/test/wpt/tests/resources/test/tests/functional/task-scheduling-promise-test.html b/test/wpt/tests/resources/test/tests/functional/task-scheduling-promise-test.html
new file mode 100644
index 0000000..9d8e5c1
--- /dev/null
+++ b/test/wpt/tests/resources/test/tests/functional/task-scheduling-promise-test.html
@@ -0,0 +1,241 @@
+<!doctype html>
+<title>testharness.js - task scheduling</title>
+<script src="../../../testharness.js"></script>
+<script src="../../../testharnessreport.js"></script>
+<script>
+var sameTask = null;
+var sameMicrotask = null;
+var expectedError = new Error('This error is expected');
+
+promise_test(function() {
+ return Promise.resolve()
+ .then(function() {
+ sameMirotask = true;
+ Promise.resolve().then(() => sameMicrotask = false);
+ });
+}, 'promise test without cleanup #1');
+
+promise_test(function() {
+ assert_false(sameMicrotask);
+
+ return Promise.resolve();
+}, 'sub-test with 0 cleanup functions executes in distinct microtask from a passing sub-test');
+
+promise_test(function() {
+ return Promise.resolve()
+ .then(function() {
+ sameMirotask = true;
+ Promise.resolve().then(() => sameMicrotask = false);
+ throw expectedError;
+ });
+}, 'failing promise test without cleanup #1');
+
+promise_test(function() {
+ assert_false(sameMicrotask);
+
+ return Promise.resolve();
+}, 'sub-test with 0 cleanup functions executes in distinct microtask from a failing sub-test');
+
+promise_test(function(t) {
+ t.add_cleanup(function() {});
+
+ return Promise.resolve()
+ .then(function() {
+ sameMirotask = true;
+ Promise.resolve().then(() => sameMicrotask = false);
+ });
+}, 'promise test with cleanup #1');
+
+promise_test(function() {
+ assert_false(sameMicrotask);
+
+ return Promise.resolve();
+}, 'sub-test with some cleanup functions executes in distinct microtask from a passing sub-test');
+
+promise_test(function(t) {
+ t.add_cleanup(function() {});
+
+ return Promise.resolve()
+ .then(function() {
+ sameMirotask = true;
+ Promise.resolve().then(() => sameMicrotask = false);
+ throw expectedError;
+ });
+}, 'failing promise test with cleanup #1');
+
+promise_test(function() {
+ assert_false(sameMicrotask);
+
+ return Promise.resolve();
+}, 'sub-test with some cleanup functions executes in distinct microtask from a failing sub-test');
+
+promise_test(function(t) {
+ return Promise.resolve()
+ .then(function() {
+ sameTask = true;
+ t.step_timeout(() => sameTask = false, 0);
+ });
+}, 'promise test without cleanup #2');
+
+promise_test(function() {
+ assert_true(sameTask);
+
+ return Promise.resolve();
+}, 'sub-test with 0 cleanup functions executes in the same task as a passing sub-test');
+
+promise_test(function(t) {
+ return Promise.resolve()
+ .then(function() {
+ sameTask = true;
+ t.step_timeout(() => sameTask = false, 0);
+ throw expectedError;
+ });
+}, 'failing promise test without cleanup #2');
+
+promise_test(function() {
+ assert_true(sameTask);
+
+ return Promise.resolve();
+}, 'sub-test with 0 cleanup functions executes in the same task as a failing sub-test');
+
+promise_test(function(t) {
+ t.add_cleanup(function() {});
+
+ return Promise.resolve()
+ .then(function() {
+ sameTask = true;
+ t.step_timeout(() => sameTask = false, 0);
+ });
+}, 'promise test with cleanup #2');
+
+promise_test(function() {
+ assert_true(sameTask);
+
+ return Promise.resolve();
+}, 'sub-test with some cleanup functions executes in the same task as a passing sub-test');
+
+promise_test(function(t) {
+ t.add_cleanup(function() {});
+
+ return Promise.resolve()
+ .then(function() {
+ sameTask = true;
+ t.step_timeout(() => sameTask = false, 0);
+ throw expectedError;
+ });
+}, 'failing promise test with cleanup #2');
+
+promise_test(function() {
+ assert_true(sameTask);
+
+ return Promise.resolve();
+}, 'sub-test with some cleanup functions executes in the same task as a failing sub-test');
+</script>
+
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "message": null,
+ "status_string": "OK"
+ },
+ "summarized_tests": [
+ {
+ "message": "promise_test: Unhandled rejection with value: object \"Error: This error is expected\"",
+ "name": "failing promise test with cleanup #1",
+ "properties": {},
+ "status_string": "FAIL"
+ },
+ {
+ "message": "promise_test: Unhandled rejection with value: object \"Error: This error is expected\"",
+ "name": "failing promise test with cleanup #2",
+ "properties": {},
+ "status_string": "FAIL"
+ },
+ {
+ "message": "promise_test: Unhandled rejection with value: object \"Error: This error is expected\"",
+ "name": "failing promise test without cleanup #1",
+ "properties": {},
+ "status_string": "FAIL"
+ },
+ {
+ "message": "promise_test: Unhandled rejection with value: object \"Error: This error is expected\"",
+ "name": "failing promise test without cleanup #2",
+ "properties": {},
+ "status_string": "FAIL"
+ },
+ {
+ "message": null,
+ "name": "promise test with cleanup #1",
+ "properties": {},
+ "status_string": "PASS"
+ },
+ {
+ "message": null,
+ "name": "promise test with cleanup #2",
+ "properties": {},
+ "status_string": "PASS"
+ },
+ {
+ "message": null,
+ "name": "promise test without cleanup #1",
+ "properties": {},
+ "status_string": "PASS"
+ },
+ {
+ "message": null,
+ "name": "promise test without cleanup #2",
+ "properties": {},
+ "status_string": "PASS"
+ },
+ {
+ "message": null,
+ "name": "sub-test with 0 cleanup functions executes in distinct microtask from a failing sub-test",
+ "properties": {},
+ "status_string": "PASS"
+ },
+ {
+ "message": null,
+ "name": "sub-test with 0 cleanup functions executes in distinct microtask from a passing sub-test",
+ "properties": {},
+ "status_string": "PASS"
+ },
+ {
+ "message": null,
+ "name": "sub-test with 0 cleanup functions executes in the same task as a failing sub-test",
+ "properties": {},
+ "status_string": "PASS"
+ },
+ {
+ "message": null,
+ "name": "sub-test with 0 cleanup functions executes in the same task as a passing sub-test",
+ "properties": {},
+ "status_string": "PASS"
+ },
+ {
+ "message": null,
+ "name": "sub-test with some cleanup functions executes in distinct microtask from a failing sub-test",
+ "properties": {},
+ "status_string": "PASS"
+ },
+ {
+ "message": null,
+ "name": "sub-test with some cleanup functions executes in distinct microtask from a passing sub-test",
+ "properties": {},
+ "status_string": "PASS"
+ },
+ {
+ "message": null,
+ "name": "sub-test with some cleanup functions executes in the same task as a failing sub-test",
+ "properties": {},
+ "status_string": "PASS"
+ },
+ {
+ "message": null,
+ "name": "sub-test with some cleanup functions executes in the same task as a passing sub-test",
+ "properties": {},
+ "status_string": "PASS"
+ }
+ ],
+ "type": "complete"
+}
+</script>
diff --git a/test/wpt/tests/resources/test/tests/functional/task-scheduling-test.html b/test/wpt/tests/resources/test/tests/functional/task-scheduling-test.html
new file mode 100644
index 0000000..0358444
--- /dev/null
+++ b/test/wpt/tests/resources/test/tests/functional/task-scheduling-test.html
@@ -0,0 +1,141 @@
+<!doctype html>
+<title>testharness.js - task scheduling</title>
+<script src="../../../testharness.js"></script>
+<script src="../../../testharnessreport.js"></script>
+<script>
+var sameMicrotask = null;
+var expectedError = new Error('This error is expected');
+
+// Derived from `immediate`
+// https://github.com/calvinmetcalf/immediate/blob/c353bd2106648cee1d525bfda22cfc4456e69c0e/lib/mutation.js
+function microTask(callback) {
+ var observer = new MutationObserver(callback);
+ var element = document.createTextNode('');
+ observer.observe(element, {
+ characterData: true
+ });
+
+ element.data = true;
+};
+
+async_test(function(t) {
+ var microtask_ran = false;
+
+ t.step_timeout(t.step_func(function() {
+ assert_true(microtask_ran, 'function registered as a microtask was executed before task');
+ t.done();
+ }), 0);
+
+ microTask(function() {
+ microtask_ran = true;
+ });
+}, 'precondition: microtask creation logic functions as expected');
+
+test(function() {
+ sameMicrotask = true;
+ microTask(function() { sameMicrotask = false; });
+}, 'synchronous test without cleanup');
+
+test(function() {
+ assert_true(sameMicrotask);
+}, 'sub-test with 0 cleanup functions executes in the same microtask as a passing sub-test');
+
+test(function() {
+ sameMicrotask = true;
+ microTask(function() { sameMicrotask = false; });
+ throw expectedError;
+}, 'failing synchronous test without cleanup');
+
+test(function() {
+ assert_true(sameMicrotask);
+}, 'sub-test with 0 cleanup functions executes in the same microtask as a failing sub-test');
+
+test(function(t) {
+ t.add_cleanup(function() {});
+
+ sameMicrotask = true;
+ microTask(function() { sameMicrotask = false; });
+}, 'synchronous test with cleanup');
+
+test(function() {
+ assert_true(sameMicrotask);
+}, 'sub-test with some cleanup functions executes in the same microtask as a passing sub-test');
+
+test(function(t) {
+ t.add_cleanup(function() {});
+
+ sameMicrotask = true;
+ microTask(function() { sameMicrotask = false; });
+ throw expectedError;
+}, 'failing synchronous test with cleanup');
+
+test(function() {
+ assert_true(sameMicrotask);
+}, 'sub-test with some cleanup functions executes in the same microtask as a failing sub-test');
+</script>
+
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "message": null,
+ "status_string": "OK"
+ },
+ "summarized_tests": [
+ {
+ "message": "This error is expected",
+ "name": "failing synchronous test with cleanup",
+ "properties": {},
+ "status_string": "FAIL"
+ },
+ {
+ "message": "This error is expected",
+ "name": "failing synchronous test without cleanup",
+ "properties": {},
+ "status_string": "FAIL"
+ },
+ {
+ "message": null,
+ "name": "precondition: microtask creation logic functions as expected",
+ "properties": {},
+ "status_string": "PASS"
+ },
+ {
+ "message": null,
+ "name": "sub-test with 0 cleanup functions executes in the same microtask as a failing sub-test",
+ "properties": {},
+ "status_string": "PASS"
+ },
+ {
+ "message": null,
+ "name": "sub-test with 0 cleanup functions executes in the same microtask as a passing sub-test",
+ "properties": {},
+ "status_string": "PASS"
+ },
+ {
+ "message": null,
+ "name": "sub-test with some cleanup functions executes in the same microtask as a failing sub-test",
+ "properties": {},
+ "status_string": "PASS"
+ },
+ {
+ "message": null,
+ "name": "sub-test with some cleanup functions executes in the same microtask as a passing sub-test",
+ "properties": {},
+ "status_string": "PASS"
+ },
+ {
+ "message": null,
+ "name": "synchronous test with cleanup",
+ "properties": {},
+ "status_string": "PASS"
+ },
+ {
+ "message": null,
+ "name": "synchronous test without cleanup",
+ "properties": {},
+ "status_string": "PASS"
+ }
+ ],
+ "type": "complete"
+}
+</script>
diff --git a/test/wpt/tests/resources/test/tests/functional/uncaught-exception-handle.html b/test/wpt/tests/resources/test/tests/functional/uncaught-exception-handle.html
new file mode 100644
index 0000000..764b0c4
--- /dev/null
+++ b/test/wpt/tests/resources/test/tests/functional/uncaught-exception-handle.html
@@ -0,0 +1,33 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Harness Handling Uncaught Exception</title>
+</head>
+<script src="/resources/testharness.js"></script>
+
+<body>
+<h1>Harness Handling Uncaught Exception</h1>
+<div id="log"></div>
+<script>
+var t = async_test("This should show a harness status of 'Error' and a test status of 'Not Run'");
+throw new Error("Example Error");
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "ERROR",
+ "message": "Error: Example Error"
+ },
+ "summarized_tests": [
+ {
+ "status_string": "NOTRUN",
+ "name": "This should show a harness status of 'Error' and a test status of 'Not Run'",
+ "properties": {},
+ "message": null
+ }
+ ],
+ "type": "complete"
+}
+</script>
+</body>
+</html>
diff --git a/test/wpt/tests/resources/test/tests/functional/uncaught-exception-ignore.html b/test/wpt/tests/resources/test/tests/functional/uncaught-exception-ignore.html
new file mode 100644
index 0000000..6bd0ddb
--- /dev/null
+++ b/test/wpt/tests/resources/test/tests/functional/uncaught-exception-ignore.html
@@ -0,0 +1,35 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Harness Ignoring Uncaught Exception</title>
+</head>
+<script src="/resources/testharness.js"></script>
+
+<body>
+<h1>Harness Ignoring Uncaught Exception</h1>
+<div id="log"></div>
+<script>
+setup({allow_uncaught_exception:true});
+var t = async_test("setup({allow_uncaught_exception:true}) should allow tests to pass even if there is an exception");
+onerror = t.step_func(function() {t.done()});
+throw new Error("Example Error");
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "OK",
+ "message": null
+ },
+ "summarized_tests": [
+ {
+ "status_string": "PASS",
+ "name": "setup({allow_uncaught_exception:true}) should allow tests to pass even if there is an exception",
+ "properties": {},
+ "message": null
+ }
+ ],
+ "type": "complete"
+}
+</script>
+</body>
+</html>
diff --git a/test/wpt/tests/resources/test/tests/functional/worker-dedicated-uncaught-allow.html b/test/wpt/tests/resources/test/tests/functional/worker-dedicated-uncaught-allow.html
new file mode 100644
index 0000000..ba28d49
--- /dev/null
+++ b/test/wpt/tests/resources/test/tests/functional/worker-dedicated-uncaught-allow.html
@@ -0,0 +1,45 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Dedicated Worker Tests - Allowed Uncaught Exception</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+</head>
+<body>
+<h1>Dedicated Web Worker Tests - Allowed Uncaught Exception</h1>
+<p>Demonstrates running <tt>testharness</tt> based tests inside a dedicated web worker.
+<p>The test harness is expected to pass despite an uncaught exception in a worker because that worker is configured to allow uncaught exceptions.</p>
+<div id="log"></div>
+
+<script>
+test(function(t) {
+ assert_true("Worker" in self, "Browser should support Workers");
+ },
+ "Browser supports Workers");
+
+fetch_tests_from_worker(new Worker("worker-uncaught-allow.js"));
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "OK",
+ "message": null
+ },
+ "summarized_tests": [
+ {
+ "status_string": "PASS",
+ "name": "Browser supports Workers",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "onerror event is triggered",
+ "properties": {},
+ "message": null
+ }
+ ],
+ "type": "complete"
+}
+</script>
+</body>
diff --git a/test/wpt/tests/resources/test/tests/functional/worker-dedicated-uncaught-single.html b/test/wpt/tests/resources/test/tests/functional/worker-dedicated-uncaught-single.html
new file mode 100644
index 0000000..486e067
--- /dev/null
+++ b/test/wpt/tests/resources/test/tests/functional/worker-dedicated-uncaught-single.html
@@ -0,0 +1,56 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Dedicated Worker Tests - Uncaught Exception in Single-Page Test</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+</head>
+<body>
+<h1>Dedicated Web Worker Tests - Uncaught Exception in Single-Page Test</h1>
+<p>Demonstrates running <tt>testharness</tt> based tests inside a dedicated web worker.
+<p>The test harness is expected to pass despite an uncaught exception in a worker because that worker is a single-page test.</p>
+<div id="log"></div>
+
+<script>
+test(function(t) {
+ assert_true("Worker" in self, "Browser should support Workers");
+ },
+ "Browser supports Workers");
+
+fetch_tests_from_worker(new Worker("worker-uncaught-single.js"));
+
+test(function(t) {
+ assert_false(false, "False is false");
+ },
+ "Test running on main document.");
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "OK",
+ "message": null
+ },
+ "summarized_tests": [
+ {
+ "status_string": "PASS",
+ "name": "Browser supports Workers",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "Test running on main document.",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "FAIL",
+ "name": "worker-uncaught-single",
+ "properties": {},
+ "message": "Error: This failure is expected."
+ }
+ ],
+ "type": "complete"
+}
+</script>
+</body>
diff --git a/test/wpt/tests/resources/test/tests/functional/worker-dedicated.sub.html b/test/wpt/tests/resources/test/tests/functional/worker-dedicated.sub.html
new file mode 100644
index 0000000..efd703c
--- /dev/null
+++ b/test/wpt/tests/resources/test/tests/functional/worker-dedicated.sub.html
@@ -0,0 +1,88 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Dedicated Worker Tests</title>
+<script src="../../../testharness.js"></script>
+<script src="../../../testharnessreport.js"></script>
+</head>
+<body>
+<h1>Dedicated Web Worker Tests</h1>
+<p>Demonstrates running <tt>testharness</tt> based tests inside a dedicated web worker.
+<p>The test harness is expected to fail due to an uncaught exception in one worker.</p>
+<div id="log"></div>
+
+<script>
+test(function(t) {
+ assert_true("Worker" in self, "Browser should support Workers");
+ },
+ "Browser supports Workers");
+
+fetch_tests_from_worker(new Worker("worker.js"));
+
+fetch_tests_from_worker(new Worker("worker-error.js"));
+
+test(function(t) {
+ assert_false(false, "False is false");
+ },
+ "Test running on main document.");
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "ERROR",
+ "message": "Error: This failure is expected."
+ },
+ "summarized_tests": [
+ {
+ "status_string": "PASS",
+ "name": "Browser supports Workers",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "Test running on main document.",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "Worker async_test that completes successfully",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "Worker test that completes successfully",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "worker test that completes successfully before exception",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "NOTRUN",
+ "name": "Worker test that doesn't run ('NOT RUN')",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "FAIL",
+ "name": "Worker test that fails ('FAIL')",
+ "properties": {},
+ "message": "assert_true: Failing test expected true got false"
+ },
+ {
+ "status_string": "TIMEOUT",
+ "name": "Worker test that times out ('TIMEOUT')",
+ "properties": {},
+ "message": "Test timed out"
+ }
+ ],
+ "type": "complete"
+}
+</script>
+</body>
diff --git a/test/wpt/tests/resources/test/tests/functional/worker-error.js b/test/wpt/tests/resources/test/tests/functional/worker-error.js
new file mode 100644
index 0000000..7b89602
--- /dev/null
+++ b/test/wpt/tests/resources/test/tests/functional/worker-error.js
@@ -0,0 +1,8 @@
+importScripts("/resources/testharness.js");
+
+// The following sub-test ensures that the worker is not interpreted as a
+// single-page test. The subsequent uncaught exception should therefore be
+// interpreted as a harness error rather than a single-page test failure.
+test(function() {}, "worker test that completes successfully before exception");
+
+throw new Error("This failure is expected.");
diff --git a/test/wpt/tests/resources/test/tests/functional/worker-service.html b/test/wpt/tests/resources/test/tests/functional/worker-service.html
new file mode 100644
index 0000000..2e07746
--- /dev/null
+++ b/test/wpt/tests/resources/test/tests/functional/worker-service.html
@@ -0,0 +1,115 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Example with a service worker</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+</head>
+<body>
+<h1>Service Worker Tests</h1>
+<p>Demonstrates running <tt>testharness</tt> based tests inside a service worker.
+<p>The test harness should time out due to one of the tests inside the worker timing out.
+<p>This test assumes that the browser supports <a href="http://www.w3.org/TR/service-workers/">ServiceWorkers</a>.
+<div id="log"></div>
+
+<script>
+test(
+ function(t) {
+ assert_true("serviceWorker" in navigator,
+ "navigator.serviceWorker exists");
+ },
+ "Browser supports ServiceWorker");
+
+promise_test(
+ function() {
+ // Since the service worker registration could be in an indeterminate
+ // state (due to, for example, a previous test run failing), we start by
+ // unregstering our service worker and then registering it again.
+ var scope = "service-worker-scope";
+ var worker_url = "worker.js";
+
+ return navigator.serviceWorker.register(worker_url, {scope: scope})
+ .then(
+ function(registration) {
+ return registration.unregister();
+ })
+ .then(
+ function() {
+ return navigator.serviceWorker.register(worker_url, {scope: scope});
+ })
+ .then(
+ function(registration) {
+ add_completion_callback(
+ function() {
+ registration.unregister();
+ });
+
+ return new Promise(
+ function(resolve) {
+ registration.addEventListener("updatefound",
+ function() {
+ resolve(registration.installing);
+ });
+ });
+ })
+ .then(
+ function(worker) {
+ fetch_tests_from_worker(worker);
+ });
+ },
+ "Register ServiceWorker");
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "TIMEOUT",
+ "message": null
+ },
+ "summarized_tests": [
+ {
+ "status_string": "PASS",
+ "name": "Browser supports ServiceWorker",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "Register ServiceWorker",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "Worker async_test that completes successfully",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "Worker test that completes successfully",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "NOTRUN",
+ "name": "Worker test that doesn't run ('NOT RUN')",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "FAIL",
+ "name": "Worker test that fails ('FAIL')",
+ "properties": {},
+ "message": "assert_true: Failing test expected true got false"
+ },
+ {
+ "status_string": "TIMEOUT",
+ "name": "Worker test that times out ('TIMEOUT')",
+ "properties": {},
+ "message": "Test timed out"
+ }
+ ],
+ "type": "complete"
+}
+</script>
+</body>
diff --git a/test/wpt/tests/resources/test/tests/functional/worker-shared.html b/test/wpt/tests/resources/test/tests/functional/worker-shared.html
new file mode 100644
index 0000000..e26f17d
--- /dev/null
+++ b/test/wpt/tests/resources/test/tests/functional/worker-shared.html
@@ -0,0 +1,73 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Example with a shared worker</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+</head>
+<body>
+<h1>Shared Web Worker Tests</h1>
+<p>Demonstrates running <tt>testharness</tt> based tests inside a shared worker.
+<p>The test harness should time out due to one of the tests in the worker timing out.
+<p>This test assumes that the browser supports <a href="http://www.w3.org/TR/workers/#shared-workers-and-the-sharedworker-interface">shared web workers</a>.
+<div id="log"></div>
+
+<script>
+test(
+ function(t) {
+ assert_true("SharedWorker" in self,
+ "Browser should support SharedWorkers");
+ },
+ "Browser supports SharedWorkers");
+
+fetch_tests_from_worker(new SharedWorker("worker.js",
+ "My shared worker"));
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "TIMEOUT",
+ "message": null
+ },
+ "summarized_tests": [
+ {
+ "status_string": "PASS",
+ "name": "Browser supports SharedWorkers",
+ "properties": {},
+ "message": null
+ },
+ {
+ "message": null,
+ "name": "Worker async_test that completes successfully",
+ "properties": {},
+ "status_string": "PASS"
+ },
+ {
+ "message": null,
+ "name": "Worker test that completes successfully",
+ "properties": {},
+ "status_string": "PASS"
+ },
+ {
+ "message": null,
+ "name": "Worker test that doesn't run ('NOT RUN')",
+ "properties": {},
+ "status_string": "NOTRUN"
+ },
+ {
+ "message": "assert_true: Failing test expected true got false",
+ "name": "Worker test that fails ('FAIL')",
+ "properties": {},
+ "status_string": "FAIL"
+ },
+ {
+ "message": "Test timed out",
+ "name": "Worker test that times out ('TIMEOUT')",
+ "properties": {},
+ "status_string": "TIMEOUT"
+ }
+ ],
+ "type": "complete"
+}
+</script>
+</body>
diff --git a/test/wpt/tests/resources/test/tests/functional/worker-uncaught-allow.js b/test/wpt/tests/resources/test/tests/functional/worker-uncaught-allow.js
new file mode 100644
index 0000000..6925d59
--- /dev/null
+++ b/test/wpt/tests/resources/test/tests/functional/worker-uncaught-allow.js
@@ -0,0 +1,19 @@
+importScripts("/resources/testharness.js");
+
+setup({allow_uncaught_exception:true});
+
+async_test(function(t) {
+ onerror = function() {
+ // Further delay the test's completion to ensure that the worker's
+ // `onerror` handler does not influence results in the parent context.
+ setTimeout(function() {
+ t.done();
+ }, 0);
+ };
+
+ setTimeout(function() {
+ throw new Error("This error is expected.");
+ }, 0);
+}, 'onerror event is triggered');
+
+done();
diff --git a/test/wpt/tests/resources/test/tests/functional/worker-uncaught-single.js b/test/wpt/tests/resources/test/tests/functional/worker-uncaught-single.js
new file mode 100644
index 0000000..c04542b
--- /dev/null
+++ b/test/wpt/tests/resources/test/tests/functional/worker-uncaught-single.js
@@ -0,0 +1,8 @@
+importScripts("/resources/testharness.js");
+
+setup({ single_test: true });
+
+// Because this script enables the `single_test` configuration option, it
+// should be interpreted as a single-page test, and the uncaught exception
+// should be reported as a test failure (harness status: OK).
+throw new Error("This failure is expected.");
diff --git a/test/wpt/tests/resources/test/tests/functional/worker.js b/test/wpt/tests/resources/test/tests/functional/worker.js
new file mode 100644
index 0000000..a923bc2
--- /dev/null
+++ b/test/wpt/tests/resources/test/tests/functional/worker.js
@@ -0,0 +1,34 @@
+importScripts("/resources/testharness.js");
+
+test(
+ function(test) {
+ assert_true(true, "True is true");
+ },
+ "Worker test that completes successfully");
+
+test(
+ function(test) {
+ assert_true(false, "Failing test");
+ },
+ "Worker test that fails ('FAIL')");
+
+async_test(
+ function(test) {
+ assert_true(true, "True is true");
+ },
+ "Worker test that times out ('TIMEOUT')");
+
+async_test("Worker test that doesn't run ('NOT RUN')");
+
+async_test(
+ function(test) {
+ self.setTimeout(
+ function() {
+ test.done();
+ },
+ 0);
+ },
+ "Worker async_test that completes successfully");
+
+// An explicit done() is required for dedicated and shared web workers.
+done();
diff --git a/test/wpt/tests/resources/test/tests/unit/IdlArray/is_json_type.html b/test/wpt/tests/resources/test/tests/unit/IdlArray/is_json_type.html
new file mode 100644
index 0000000..18e83a8
--- /dev/null
+++ b/test/wpt/tests/resources/test/tests/unit/IdlArray/is_json_type.html
@@ -0,0 +1,192 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>IdlArray.prototype.is_json_type()</title>
+</head>
+<body>
+<div id="log"></div>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/WebIDLParser.js"></script>
+<script src="/resources/idlharness.js"></script>
+<script src="../../../idl-helper.js"></script>
+<script>
+ "use strict";
+
+ test(function() {
+ var idl = new IdlArray();
+ assert_true(idl.is_json_type(typeFrom("DOMString")));
+ assert_true(idl.is_json_type(typeFrom("ByteString")));
+ assert_true(idl.is_json_type(typeFrom("USVString")));
+ idl.add_untested_idls('enum BarEnum { "a", "b", "c" };');
+ assert_true(idl.is_json_type(typeFrom("BarEnum")));
+ }, 'should return true for all string types');
+
+ test(function() {
+ var idl = new IdlArray();
+ assert_false(idl.is_json_type(typeFrom("Error")));
+ assert_false(idl.is_json_type(typeFrom("DOMException")));
+ }, 'should return false for all exception types');
+
+ test(function() {
+ var idl = new IdlArray();
+ assert_false(idl.is_json_type(typeFrom("Int8Array")));
+ assert_false(idl.is_json_type(typeFrom("Int16Array")));
+ assert_false(idl.is_json_type(typeFrom("Int32Array")));
+ assert_false(idl.is_json_type(typeFrom("Uint8Array")));
+ assert_false(idl.is_json_type(typeFrom("Uint16Array")));
+ assert_false(idl.is_json_type(typeFrom("Uint32Array")));
+ assert_false(idl.is_json_type(typeFrom("Uint8ClampedArray")));
+ assert_false(idl.is_json_type(typeFrom("BigInt64Array")));
+ assert_false(idl.is_json_type(typeFrom("BigUint64Array")));
+ assert_false(idl.is_json_type(typeFrom("Float32Array")));
+ assert_false(idl.is_json_type(typeFrom("Float64Array")));
+ assert_false(idl.is_json_type(typeFrom("ArrayBuffer")));
+ assert_false(idl.is_json_type(typeFrom("DataView")));
+ }, 'should return false for all buffer source types');
+
+ test(function() {
+ var idl = new IdlArray();
+ assert_true(idl.is_json_type(typeFrom("boolean")));
+ }, 'should return true for boolean');
+
+ test(function() {
+ var idl = new IdlArray();
+ assert_true(idl.is_json_type(typeFrom("byte")));
+ assert_true(idl.is_json_type(typeFrom("octet")));
+ assert_true(idl.is_json_type(typeFrom("short")));
+ assert_true(idl.is_json_type(typeFrom("unsigned short")));
+ assert_true(idl.is_json_type(typeFrom("long")));
+ assert_true(idl.is_json_type(typeFrom("unsigned long")));
+ assert_true(idl.is_json_type(typeFrom("long long")));
+ assert_true(idl.is_json_type(typeFrom("unsigned long long")));
+ assert_true(idl.is_json_type(typeFrom("float")));
+ assert_true(idl.is_json_type(typeFrom("unrestricted float")));
+ assert_true(idl.is_json_type(typeFrom("double")));
+ assert_true(idl.is_json_type(typeFrom("unrestricted double")));
+ }, 'should return true for all numeric types');
+
+ test(function() {
+ var idl = new IdlArray();
+ assert_false(idl.is_json_type(typeFrom("Promise<DOMString>")));
+ }, 'should return false for promises');
+
+ test(function() {
+ var idl = new IdlArray();
+ assert_false(idl.is_json_type(typeFrom("sequence<DOMException>")));
+ assert_true(idl.is_json_type(typeFrom("sequence<DOMString>")));
+ }, 'should handle sequences according to their inner types');
+
+ test(function() {
+ var idl = new IdlArray();
+ assert_false(idl.is_json_type(typeFrom("FrozenArray<DOMException>")));
+ assert_true(idl.is_json_type(typeFrom("FrozenArray<DOMString>")));
+ }, 'should handle frozen arrays according to their inner types');
+
+ test(function() {
+ var idl = new IdlArray();
+ assert_true(idl.is_json_type(typeFrom("record<DOMString, DOMString>")));
+ assert_false(idl.is_json_type(typeFrom("record<DOMString, Error>")));
+ }, 'should handle records according to their inner types');
+
+ test(function() {
+ var idl = new IdlArray();
+ assert_true(idl.is_json_type(typeFrom("object")));
+ }, 'should return true for object type');
+
+ test(function() {
+ var idl = new IdlArray();
+ assert_false(idl.is_json_type(typeFrom("any")));
+ }, 'should return false for any type');
+
+ test(function() {
+ var idl = new IdlArray();
+ idl.add_untested_idls('dictionary Foo { DOMString foo; }; dictionary Bar : Foo { DOMString bar; };');
+ assert_true(idl.is_json_type(typeFrom("Foo")));
+ assert_true(idl.is_json_type(typeFrom("Bar")));
+ }, 'should return true for dictionaries whose members are all JSON types');
+
+ test(function() {
+ var idl = new IdlArray();
+ idl.add_untested_idls('dictionary Foo { };');
+ assert_true(idl.is_json_type(typeFrom("Foo")));
+ }, 'should return true for dictionaries which have no members');
+
+ test(function() {
+ var idl = new IdlArray();
+ idl.add_untested_idls('dictionary FooBar { DOMString a; Error b; }; dictionary Baz : FooBar {};');
+ assert_false(idl.is_json_type(typeFrom("FooBar")));
+ assert_false(idl.is_json_type(typeFrom("Baz")));
+ }, 'should return false for dictionaries whose members are not all JSON types');
+
+ test(function() {
+ var idl = new IdlArray();
+ idl.add_untested_idls('interface Foo { DOMString toJSON(); };');
+ assert_true(idl.is_json_type(typeFrom("Foo")));
+ }, 'should return true for interfaces which declare a toJSON operation');
+
+ test(function() {
+ var idl = new IdlArray();
+ idl.add_untested_idls('interface Foo { DOMString toJSON(); }; interface Bar : Foo { };');
+ assert_true(idl.is_json_type(typeFrom("Bar")));
+ }, 'should return true for interfaces which inherit from an interface which declares a toJSON operation');
+
+ test(function() {
+ var idl = new IdlArray();
+ idl.add_untested_idls('interface Foo { }; interface mixin Bar { DOMString toJSON(); }; Foo includes Bar;');
+ idl.merge_mixins();
+ assert_true(idl.is_json_type(typeFrom("Foo")));
+ }, 'should return true for interfaces which mixin an interface which declare a toJSON operation');
+
+ test(function() {
+ var idl = new IdlArray();
+ idl.add_untested_idls('interface Foo { };');
+ assert_false(idl.is_json_type(typeFrom("Foo")));
+ }, 'should return false for interfaces which do not declare a toJSON operation');
+
+ test(function() {
+ var idl = new IdlArray();
+ idl.add_untested_idls('interface Foo { object toJSON(); };');
+ assert_true(idl.is_json_type(typeFrom("(Foo or DOMString)")));
+ }, 'should return true for union types whose member types are JSON types');
+
+ test(function() {
+ var idl = new IdlArray();
+ assert_false(idl.is_json_type(typeFrom("(DataView or DOMString)")));
+ }, 'should return false for union types whose member types are not all JSON types');
+
+ test(function() {
+ var idl = new IdlArray();
+ assert_true(idl.is_json_type(typeFrom("DOMString?")));
+ assert_false(idl.is_json_type(typeFrom("DataView?")));
+ }, 'should consider the inner types of nullable types');
+
+ test(function() {
+ var idl = new IdlArray();
+ assert_true(idl.is_json_type(typeFrom("[XAttr] long")));
+ assert_false(idl.is_json_type(typeFrom("[XAttr] DataView")));
+ }, 'should consider the inner types of annotated types.');
+
+ test(function() {
+ var idl = new IdlArray();
+ assert_throws_js(Error, _ => idl.is_json_type(typeFrom("Foo")));
+ }, "should throw if it references a dictionary, enum or interface which wasn't added to the IdlArray");
+
+ test(function() {
+ var idl = new IdlArray();
+ idl.add_untested_idls('interface Foo : Bar { };');
+ assert_throws_js(Error, _ => idl.is_json_type(typeFrom("Foo")));
+ }, "should throw for interfaces which inherit from another interface which wasn't added to the IdlArray");
+
+ test(function() {
+ var idl = new IdlArray();
+ assert_true(idl.is_json_type(typedefFrom("typedef double DOMHighResTimeStamp;").idlType));
+ }, 'should return true for typedefs whose source type is a JSON type');
+
+ test(function() {
+ var idl = new IdlArray();
+ assert_false(idl.is_json_type(typedefFrom("typedef DataView DOMHighResTimeStamp;").idlType));
+ }, 'should return false for typedefs whose source type is not a JSON type');
+</script>
+</body>
+</html>
diff --git a/test/wpt/tests/resources/test/tests/unit/IdlDictionary/get_reverse_inheritance_stack.html b/test/wpt/tests/resources/test/tests/unit/IdlDictionary/get_reverse_inheritance_stack.html
new file mode 100644
index 0000000..418bcde
--- /dev/null
+++ b/test/wpt/tests/resources/test/tests/unit/IdlDictionary/get_reverse_inheritance_stack.html
@@ -0,0 +1,47 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>IdlDictionary.prototype.get_reverse_inheritance_stack()</title>
+</head>
+<body>
+<div id="log"></div>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/WebIDLParser.js"></script>
+<script src="/resources/idlharness.js"></script>
+<script src="../../../idl-helper.js"></script>
+<script>
+ "use strict";
+ test(function() {
+ var stack = dictionaryFrom('dictionary A { };').get_reverse_inheritance_stack();
+ assert_array_equals(stack.map(d => d.name), ["A"]);
+ }, 'should return an array that includes itself.');
+
+ test(function() {
+ var d = dictionaryFrom('dictionary A : B { };');
+ assert_throws_js(Error, _ => d.get_reverse_inheritance_stack());
+ }, "should throw for dictionaries which inherit from another dictionary which wasn't added to the IdlArray");
+
+ test(function() {
+ var idl = new IdlArray();
+ idl.add_idls('dictionary A : B { };');
+ idl.add_untested_idls('dictionary B : C { }; dictionary C { };');
+ var A = idl.members["A"];
+ assert_array_equals(A.get_reverse_inheritance_stack().map(d => d.name), ["C", "B", "A"]);
+ }, 'should return an array of dictionaries in order of inheritance, starting with the base dictionary');
+
+ test(function () {
+ let i = new IdlArray();
+ i.add_untested_idls('dictionary A : B {};');
+ i.assert_throws(new IdlHarnessError('A inherits B, but B is undefined.'), i => i.test());
+ }, 'A : B with B undeclared should throw IdlHarnessError');
+
+ test(function () {
+ let i = new IdlArray();
+ i.add_untested_idls('dictionary A : B {};');
+ i.add_untested_idls('interface B {};');
+ i.assert_throws(new IdlHarnessError('A inherits B, but A is not an interface.'), i => i.test());
+ }, 'dictionary A : B with B interface should throw IdlHarnessError');
+</script>
+</body>
+</html>
diff --git a/test/wpt/tests/resources/test/tests/unit/IdlDictionary/test_partial_dictionary.html b/test/wpt/tests/resources/test/tests/unit/IdlDictionary/test_partial_dictionary.html
new file mode 100644
index 0000000..d6137f6
--- /dev/null
+++ b/test/wpt/tests/resources/test/tests/unit/IdlDictionary/test_partial_dictionary.html
@@ -0,0 +1,39 @@
+<!DOCTYPE HTML>
+<html>
+
+<head>
+ <meta charset="utf-8">
+ <title>idlharness: partial dictionaries</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/resources/WebIDLParser.js"></script>
+ <script src="/resources/idlharness.js"></script>
+ <script src="../../../idl-helper.js"></script>
+</head>
+
+<body>
+<pre id='idl'>
+dictionary A {};
+partial dictionary A {
+ boolean B;
+};
+partial dictionary A {
+ boolean C;
+};
+</pre>
+
+<script>
+'use strict';
+
+test(() => {
+ let idlArray = new IdlArray();
+ idlArray.add_idls(document.getElementById('idl').textContent);
+ idlArray.test();
+
+ let members = idlArray.members["A"].members.map(m => m.name);
+ assert_array_equals(members, ["B", "C"], 'A should contain B, C');
+}, 'Partial dictionaries');
+</script>
+
+</body>
+</html>
diff --git a/test/wpt/tests/resources/test/tests/unit/IdlInterface/constructors.html b/test/wpt/tests/resources/test/tests/unit/IdlInterface/constructors.html
new file mode 100644
index 0000000..e9ee3f8
--- /dev/null
+++ b/test/wpt/tests/resources/test/tests/unit/IdlInterface/constructors.html
@@ -0,0 +1,26 @@
+<!DOCTYPE HTML>
+<title>IdlInterface.prototype.constructors()</title>
+<div id="log"></div>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/WebIDLParser.js"></script>
+<script src="/resources/idlharness.js"></script>
+<script src="../../../idl-helper.js"></script>
+<script>
+"use strict";
+// [Constructor] extended attribute should not be supported:
+test(function() {
+ var i = interfaceFrom('[Constructor] interface A { };');
+ assert_equals(i.constructors().length, 0);
+}, 'Interface with Constructor extended attribute.');
+
+test(function() {
+ var i = interfaceFrom('interface A { constructor(); };');
+ assert_equals(i.constructors().length, 1);
+}, 'Interface with constructor method');
+
+test(function() {
+ var i = interfaceFrom('interface A { constructor(); constructor(any value); };');
+ assert_equals(i.constructors().length, 2);
+}, 'Interface with constructor overloads');
+</script>
diff --git a/test/wpt/tests/resources/test/tests/unit/IdlInterface/default_to_json_operation.html b/test/wpt/tests/resources/test/tests/unit/IdlInterface/default_to_json_operation.html
new file mode 100644
index 0000000..5ade7d0
--- /dev/null
+++ b/test/wpt/tests/resources/test/tests/unit/IdlInterface/default_to_json_operation.html
@@ -0,0 +1,114 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>IdlDictionary.prototype.default_to_json_operation()</title>
+</head>
+<body>
+<div id="log"></div>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/WebIDLParser.js"></script>
+<script src="/resources/idlharness.js"></script>
+<script src="../../../idl-helper.js"></script>
+<script>
+ "use strict";
+ test(function() {
+ var map = interfaceFrom('interface A { [Default] object toJSON(); };').default_to_json_operation();
+ assert_equals(map.size, 0);
+ }, 'should return an empty map when there are no attributes');
+
+ test(function() {
+ var r = interfaceFrom('interface A { };').default_to_json_operation();
+ assert_equals(r, null);
+ }, 'should return null when there is no toJSON method');
+
+ test(function() {
+ var r = interfaceFrom('interface A { DOMString toJSON(); };').default_to_json_operation();
+ assert_equals(r, null);
+ }, 'should return null when there is a toJSON method but it does not have the [Default] extended attribute');
+
+ test(function() {
+ var context = new IdlArray();
+ context.add_idls("interface A : B { DOMString toJSON(); };");
+ context.add_idls("interface B { [Default] object toJSON(); };");
+ var r = context.members.A.default_to_json_operation();
+ assert_equals(r, null);
+ }, 'should return null when there is a toJSON method but it does not have the [Default] extended attribute even if this extended attribute exists on inherited interfaces');
+
+ test(function() {
+ var map = interfaceFrom('interface A { [Default] object toJSON(); static attribute DOMString foo; };').default_to_json_operation();
+ assert_equals(map.size, 0);
+ }, 'should not include static attributes');
+
+ test(function() {
+ var map = interfaceFrom('interface A { [Default] object toJSON(); attribute Promise<DOMString> bar; };').default_to_json_operation();
+ assert_equals(map.size, 0);
+ }, 'should not include attributes which are not JSON types');
+
+ test(function() {
+ var map = interfaceFrom('interface A { [Default] object toJSON(); DOMString bar(); };').default_to_json_operation();
+ assert_equals(map.size, 0);
+ }, 'should not include operations');
+
+ test(function() {
+ var map = interfaceFrom('interface A { [Default] object toJSON(); attribute DOMString bar; };').default_to_json_operation();
+ assert_equals(map.size, 1);
+ assert_true(map.has("bar"));
+ assert_equals(map.get("bar").idlType, "DOMString");
+ }, 'should return a map whose key/value pair represent the identifier and IDL type of valid attributes');
+
+ test(function() {
+ var context = new IdlArray();
+ context.add_idls("interface A : B { [Default] object toJSON(); attribute DOMString a; };");
+ context.add_idls("interface B { [Default] object toJSON(); attribute long b; };");
+ var map = context.members.A.default_to_json_operation();
+ assert_array_equals([...map.keys()], ["b", "a"]);
+ assert_array_equals([...map.values()].map(v => v.idlType), ["long", "DOMString"]);
+ }, 'should return a properly ordered map that contains IDL types of valid attributes for inherited interfaces');
+
+ test(function() {
+ var context = new IdlArray();
+ context.add_idls("interface A : B { attribute DOMString a; };");
+ context.add_idls("interface B { [Default] object toJSON(); attribute long b; };");
+ var map = context.members.A.default_to_json_operation();
+ assert_equals(map.size, 1);
+ assert_true(map.has("b"));
+ assert_equals(map.get("b").idlType, "long");
+ assert_array_equals([...map.keys()], ["b"]);
+ }, 'should not include attributes of the current interface when the [Default] toJSON method in inherited');
+
+ test(function() {
+ var context = new IdlArray();
+ context.add_idls("interface A : B { [Default] object toJSON(); };");
+ context.add_idls("interface B : C { [Default] object toJSON(); attribute DOMString foo; };");
+ context.add_idls("interface C { [Default] object toJSON(); attribute long foo; };");
+ var map = context.members.A.default_to_json_operation();
+ assert_equals(map.size, 1);
+ assert_true(map.has("foo"));
+ assert_equals(map.get("foo").idlType, "DOMString");
+ }, 'attributes declared further away in the inheritance hierarchy should be masked by attributes declared closer');
+
+ test(function() {
+ var context = new IdlArray();
+ context.add_idls("interface A { [Default] object toJSON(); attribute DOMString a; };");
+ context.add_idls("interface B : A { attribute any b; };");
+ context.add_idls("interface C : B { [Default] object toJSON(); attribute long c; };");
+ var map = context.members.C.default_to_json_operation();
+ assert_array_equals([...map.keys()], ["a", "c"]);
+ assert_array_equals([...map.values()].map(v => v.idlType), ["DOMString", "long"]);
+ }, 'should return an ordered map that ignores attributes of inherited interfaces which do not declare a [Default] toJSON operation.');
+
+ test(function() {
+ var context = new IdlArray();
+ context.add_idls("interface D { attribute DOMString d; };");
+ context.add_idls("interface mixin M { [Default] object toJSON(); attribute long m; };");
+ context.add_idls("D includes M;");
+ context.merge_mixins();
+ var map = context.members.D.default_to_json_operation();
+ assert_array_equals([...map.keys()], ["d", "m"]);
+ assert_array_equals([...map.values()].map(v => v.idlType), ["DOMString", "long"]);
+ }, 'should return a properly ordered map that accounts for mixed-in interfaces which declare a [Default] toJSON operation.');
+</script>
+</body>
+</html>
+
diff --git a/test/wpt/tests/resources/test/tests/unit/IdlInterface/do_member_unscopable_asserts.html b/test/wpt/tests/resources/test/tests/unit/IdlInterface/do_member_unscopable_asserts.html
new file mode 100644
index 0000000..90142ef
--- /dev/null
+++ b/test/wpt/tests/resources/test/tests/unit/IdlInterface/do_member_unscopable_asserts.html
@@ -0,0 +1,56 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>IdlDictionary.prototype.do_member_unscopable_asserts()</title>
+</head>
+<body>
+<div id="log"></div>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/WebIDLParser.js"></script>
+<script src="/resources/idlharness.js"></script>
+<script src="../../../idl-helper.js"></script>
+<script>
+ 'use strict';
+ function mock_interface_A(unscopables) {
+ self.A = function A() {};
+ A.prototype[Symbol.unscopables] = unscopables;
+ }
+
+ test(function() {
+ const i = interfaceFrom('interface A { [Unscopable] attribute any x; };');
+ const member = i.members[0];
+ assert_true(member.isUnscopable);
+ mock_interface_A({ x: true });
+ i.do_member_unscopable_asserts(member);
+ }, 'should not throw for [Unscopable] with property in @@unscopables');
+
+ test(function() {
+ const i = interfaceFrom('interface A { [Unscopable] attribute any x; };');
+ const member = i.members[0];
+ assert_true(member.isUnscopable);
+ mock_interface_A({});
+ // assert_throws_* can't be used because they rethrow AssertionErrors.
+ try {
+ i.do_member_unscopable_asserts(member);
+ } catch(e) {
+ assert_true(e.message.includes('Symbol.unscopables'));
+ return;
+ }
+ assert_unreached('did not throw');
+ }, 'should throw for [Unscopable] with property missing from @@unscopables');
+
+ // This test checks that for attributes/methods which aren't [Unscopable]
+ // in the IDL, we don't assert that @@unscopables is missing the property.
+ // This could miss implementation bugs, but [Unscopable] is so rarely used
+ // that it's fairly unlikely to ever happen.
+ test(function() {
+ const i = interfaceFrom('interface A { attribute any x; };');
+ const member = i.members[0];
+ assert_false(member.isUnscopable);
+ mock_interface_A({ x: true });
+ i.do_member_unscopable_asserts(member);
+ }, 'should not throw if [Unscopable] is used but property is in @@unscopables');
+</script>
+</body>
+</html>
diff --git a/test/wpt/tests/resources/test/tests/unit/IdlInterface/get_interface_object.html b/test/wpt/tests/resources/test/tests/unit/IdlInterface/get_interface_object.html
new file mode 100644
index 0000000..a3d901a
--- /dev/null
+++ b/test/wpt/tests/resources/test/tests/unit/IdlInterface/get_interface_object.html
@@ -0,0 +1,22 @@
+<!DOCTYPE HTML>
+<title>IdlInterface.prototype.get_interface_object()</title>
+<div id="log"></div>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/WebIDLParser.js"></script>
+<script src="/resources/idlharness.js"></script>
+<script src="../../../idl-helper.js"></script>
+<script>
+"use strict";
+test(function() {
+ window.A = {};
+ var i = interfaceFrom('interface A { };');
+ assert_equals(i.get_interface_object(), window.A);
+}, 'Interface does not have LegacyNamespace.');
+
+test(function() {
+ window.Foo = { A: {} };
+ var i = interfaceFrom('[LegacyNamespace=Foo] interface A { }; namespace Foo { };');
+ assert_equals(i.get_interface_object(), window.Foo.A);
+}, 'Interface has LegacyNamespace');
+</script>
diff --git a/test/wpt/tests/resources/test/tests/unit/IdlInterface/get_interface_object_owner.html b/test/wpt/tests/resources/test/tests/unit/IdlInterface/get_interface_object_owner.html
new file mode 100644
index 0000000..51ab206
--- /dev/null
+++ b/test/wpt/tests/resources/test/tests/unit/IdlInterface/get_interface_object_owner.html
@@ -0,0 +1,21 @@
+<!DOCTYPE HTML>
+<title>IdlInterface.prototype.get_interface_object_owner()</title>
+<div id="log"></div>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/WebIDLParser.js"></script>
+<script src="/resources/idlharness.js"></script>
+<script src="../../../idl-helper.js"></script>
+<script>
+"use strict";
+test(function() {
+ var i = interfaceFrom('interface A { };');
+ assert_equals(i.get_interface_object_owner(), window);
+}, 'Interface does not have LegacyNamespace.');
+
+test(function() {
+ window.Foo = {};
+ var i = interfaceFrom('[LegacyNamespace=Foo] interface A { }; namespace Foo { };');
+ assert_equals(i.get_interface_object_owner(), window.Foo);
+}, 'Interface has LegacyNamespace');
+</script>
diff --git a/test/wpt/tests/resources/test/tests/unit/IdlInterface/get_legacy_namespace.html b/test/wpt/tests/resources/test/tests/unit/IdlInterface/get_legacy_namespace.html
new file mode 100644
index 0000000..e2d42bb
--- /dev/null
+++ b/test/wpt/tests/resources/test/tests/unit/IdlInterface/get_legacy_namespace.html
@@ -0,0 +1,20 @@
+<!DOCTYPE HTML>
+<title>IdlInterface.prototype.get_legacy_namespace()</title>
+<div id="log"></div>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/WebIDLParser.js"></script>
+<script src="/resources/idlharness.js"></script>
+<script src="../../../idl-helper.js"></script>
+<script>
+"use strict";
+test(function() {
+ var i = interfaceFrom('interface A { };');
+ assert_equals(i.get_legacy_namespace(), undefined);
+}, 'Interface does not have LegacyNamespace.');
+
+test(function() {
+ var i = interfaceFrom('[LegacyNamespace=Foo] interface A { }; namespace Foo { };');
+ assert_equals(i.get_legacy_namespace(), "Foo");
+}, 'Interface has LegacyNamespace');
+</script>
diff --git a/test/wpt/tests/resources/test/tests/unit/IdlInterface/get_qualified_name.html b/test/wpt/tests/resources/test/tests/unit/IdlInterface/get_qualified_name.html
new file mode 100644
index 0000000..677a31b
--- /dev/null
+++ b/test/wpt/tests/resources/test/tests/unit/IdlInterface/get_qualified_name.html
@@ -0,0 +1,20 @@
+<!DOCTYPE HTML>
+<title>IdlInterface.prototype.get_qualified_name()</title>
+<div id="log"></div>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/WebIDLParser.js"></script>
+<script src="/resources/idlharness.js"></script>
+<script src="../../../idl-helper.js"></script>
+<script>
+"use strict";
+test(function() {
+ var i = interfaceFrom('interface A { };');
+ assert_equals(i.get_qualified_name(), "A");
+}, 'Interface does not have LegacyNamespace.');
+
+test(function() {
+ var i = interfaceFrom('[LegacyNamespace=Foo] interface A { }; namespace Foo { };');
+ assert_equals(i.get_qualified_name(), "Foo.A");
+}, 'Interface has LegacyNamespace');
+</script>
diff --git a/test/wpt/tests/resources/test/tests/unit/IdlInterface/get_reverse_inheritance_stack.html b/test/wpt/tests/resources/test/tests/unit/IdlInterface/get_reverse_inheritance_stack.html
new file mode 100644
index 0000000..0c066ba
--- /dev/null
+++ b/test/wpt/tests/resources/test/tests/unit/IdlInterface/get_reverse_inheritance_stack.html
@@ -0,0 +1,49 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>IdlInterface.prototype.get_reverse_inheritance_stack()</title>
+</head>
+<body>
+<div id="log"></div>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/WebIDLParser.js"></script>
+<script src="/resources/idlharness.js"></script>
+<script src="../../../idl-helper.js"></script>
+<script>
+ "use strict";
+ test(function() {
+ var stack = interfaceFrom('interface A { };').get_reverse_inheritance_stack();
+ assert_array_equals(stack.map(i => i.name), ["A"]);
+ }, 'should return an array that includes itself.');
+
+ test(function() {
+ var i = interfaceFrom('interface A : B { };');
+ assert_throws_js(Error, _ => i.get_reverse_inheritance_stack());
+ }, "should throw for interfaces which inherit from another interface which wasn't added to the IdlArray");
+
+ test(function() {
+ var idl = new IdlArray();
+ idl.add_idls('interface A : B { };');
+ idl.add_untested_idls('interface B : C { }; interface C { };');
+ var A = idl.members["A"];
+ assert_array_equals(A.get_reverse_inheritance_stack().map(i => i.name), ["C", "B", "A"]);
+ }, 'should return an array of interfaces in order of inheritance, starting with the base interface');
+
+ test(function () {
+ var idl = new IdlArray();
+ idl.add_untested_idls('interface A : B { };');
+ idl.add_untested_idls('interface B : A { };');
+ idl.assert_throws('A has a circular dependency: A,B,A', i => i.test());
+ }, 'should throw when inheritance is circular');
+
+ test(function () {
+ var idl = new IdlArray();
+ idl.add_untested_idls('interface A : B { };');
+ idl.assert_throws(
+ 'Duplicate identifier A',
+ i => i.add_untested_idls('interface A : C { };'));
+ }, 'should throw when multiple inheritances defined');
+</script>
+</body>
+</html>
diff --git a/test/wpt/tests/resources/test/tests/unit/IdlInterface/has_default_to_json_regular_operation.html b/test/wpt/tests/resources/test/tests/unit/IdlInterface/has_default_to_json_regular_operation.html
new file mode 100644
index 0000000..b47262b
--- /dev/null
+++ b/test/wpt/tests/resources/test/tests/unit/IdlInterface/has_default_to_json_regular_operation.html
@@ -0,0 +1,47 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>IdlInterface.prototype.has_default_to_json_regular_operation()</title>
+</head>
+<body>
+<div id="log"></div>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/WebIDLParser.js"></script>
+<script src="/resources/idlharness.js"></script>
+<script src="../../../idl-helper.js"></script>
+<script>
+ "use strict";
+ test(function() {
+ var i = interfaceFrom('interface A { };');
+ assert_false(i.has_default_to_json_regular_operation());
+ }, 'should return false when the interface declares no toJSON operation.');
+
+ test(function() {
+ var i = interfaceFrom('interface A { static object toJSON(); };');
+ assert_false(i.has_default_to_json_regular_operation());
+ }, 'should return false when the interface declares a static toJSON operation.');
+
+ test(function() {
+ var i = interfaceFrom('interface A { object toJSON(); };');
+ assert_false(i.has_default_to_json_regular_operation());
+ }, 'should return false when the interface declares a regular toJSON operation with no extended attribute.');
+
+ test(function() {
+ var i = interfaceFrom('interface A { [x] object toJSON(); };');
+ assert_false(i.has_default_to_json_regular_operation());
+ }, 'should return false when the interface declares a regular toJSON operation with another extented attribute.');
+
+ test(function() {
+ var i = interfaceFrom('interface A { [Default] object toJSON(); };');
+ assert_true(i.has_default_to_json_regular_operation());
+ }, 'should return true when the interface declares a regular toJSON operation with the [Default] extented attribute.');
+
+ test(function() {
+ var i = interfaceFrom('interface A { [Attr, AnotherAttr, Default] object toJSON(); };');
+ assert_true(i.has_default_to_json_regular_operation());
+ }, 'should return true when the interface declares a regular toJSON operation with multiple extended attributes, including [Default].');
+</script>
+</body>
+</html>
+
diff --git a/test/wpt/tests/resources/test/tests/unit/IdlInterface/has_to_json_regular_operation.html b/test/wpt/tests/resources/test/tests/unit/IdlInterface/has_to_json_regular_operation.html
new file mode 100644
index 0000000..a1a641b
--- /dev/null
+++ b/test/wpt/tests/resources/test/tests/unit/IdlInterface/has_to_json_regular_operation.html
@@ -0,0 +1,31 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>IdlInterface.prototype.has_to_json_regular_operation()</title>
+</head>
+<body>
+<div id="log"></div>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/WebIDLParser.js"></script>
+<script src="/resources/idlharness.js"></script>
+<script src="../../../idl-helper.js"></script>
+<script>
+ "use strict";
+ test(function() {
+ var i = interfaceFrom('interface A { };');
+ assert_false(i.has_to_json_regular_operation());
+ }, 'should return false when the interface declares no toJSON operation.');
+
+ test(function() {
+ var i = interfaceFrom('interface A { static object toJSON(); };');
+ assert_false(i.has_to_json_regular_operation());
+ }, 'should return false when the interface declares a static toJSON operation.');
+
+ test(function() {
+ var i = interfaceFrom('interface A { object toJSON(); };');
+ assert_true(i.has_to_json_regular_operation());
+ }, 'should return true when the interface declares a regular toJSON operation.');
+</script>
+</body>
+</html>
diff --git a/test/wpt/tests/resources/test/tests/unit/IdlInterface/should_have_interface_object.html b/test/wpt/tests/resources/test/tests/unit/IdlInterface/should_have_interface_object.html
new file mode 100644
index 0000000..3ce9457
--- /dev/null
+++ b/test/wpt/tests/resources/test/tests/unit/IdlInterface/should_have_interface_object.html
@@ -0,0 +1,30 @@
+<!DOCTYPE HTML>
+<title>IdlInterface.prototype.should_have_interface_object()</title>
+<div id="log"></div>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/WebIDLParser.js"></script>
+<script src="/resources/idlharness.js"></script>
+<script src="../../../idl-helper.js"></script>
+<script>
+"use strict";
+test(function() {
+ var i = interfaceFrom("callback interface A { const unsigned short B = 0; };");
+ assert_true(i.should_have_interface_object());
+}, "callback interface with a constant");
+
+test(function() {
+ var i = interfaceFrom("callback interface A { undefined b(); sequence<any> c(); };");
+ assert_false(i.should_have_interface_object());
+}, "callback interface without a constant");
+
+test(function() {
+ var i = interfaceFrom("[LegacyNoInterfaceObject] interface A { };");
+ assert_false(i.should_have_interface_object());
+}, "non-callback interface with [LegacyNoInterfaceObject]");
+
+test(function() {
+ var i = interfaceFrom("interface A { };");
+ assert_true(i.should_have_interface_object());
+}, "non-callback interface without [LegacyNoInterfaceObject]");
+</script>
diff --git a/test/wpt/tests/resources/test/tests/unit/IdlInterface/test_primary_interface_of_undefined.html b/test/wpt/tests/resources/test/tests/unit/IdlInterface/test_primary_interface_of_undefined.html
new file mode 100644
index 0000000..0031558
--- /dev/null
+++ b/test/wpt/tests/resources/test/tests/unit/IdlInterface/test_primary_interface_of_undefined.html
@@ -0,0 +1,29 @@
+<!DOCTYPE HTML>
+<html>
+
+<head>
+ <title>idlharness test_primary_interface_of_undefined</title>
+</head>
+
+<body>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/WebIDLParser.js"></script>
+ <script src="/resources/idlharness.js"></script>
+ <script>
+ 'use strict';
+ test(function () {
+ let i = new IdlArray();
+ i.add_untested_idls('interface A : B {};');
+ i.assert_throws(new IdlHarnessError('A inherits B, but B is undefined.'), i => i.test());
+ }, 'A : B with B undeclared should throw IdlHarnessError');
+
+ test(function () {
+ let i = new IdlArray();
+ i.add_untested_idls('interface A : B {};');
+ i.add_untested_idls('dictionary B {};');
+ i.assert_throws(new IdlHarnessError('A inherits B, but B is not an interface.'), i => i.test());
+ }, 'interface A : B with B dictionary should throw IdlHarnessError');
+ </script>
+</body>
+
+</html>
diff --git a/test/wpt/tests/resources/test/tests/unit/IdlInterfaceMember/is_to_json_regular_operation.html b/test/wpt/tests/resources/test/tests/unit/IdlInterfaceMember/is_to_json_regular_operation.html
new file mode 100644
index 0000000..b3f402d
--- /dev/null
+++ b/test/wpt/tests/resources/test/tests/unit/IdlInterfaceMember/is_to_json_regular_operation.html
@@ -0,0 +1,42 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>IdlInterfaceMember.prototype.is_to_json_regular_operation()</title>
+</head>
+<body>
+<div id="log"></div>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/WebIDLParser.js"></script>
+<script src="/resources/idlharness.js"></script>
+<script src="../../../idl-helper.js"></script>
+<script>
+ "use strict";
+ test(function() {
+ var m = memberFrom("readonly attribute DOMString foo");
+ assert_false(m.is_to_json_regular_operation());
+ }, 'should return false when member is an attribute.');
+
+ test(function() {
+ var m = memberFrom("static undefined foo()");
+ assert_false(m.is_to_json_regular_operation());
+ }, 'should return false when member is a static operation.');
+
+ test(function() {
+ var m = memberFrom("static object toJSON()");
+ assert_false(m.is_to_json_regular_operation());
+ }, 'should return false when member is a static toJSON operation.');
+
+ test(function() {
+ var m = memberFrom("object toJSON()");
+ assert_true(m.is_to_json_regular_operation());
+ }, 'should return true when member is a regular toJSON operation.');
+
+ test(function() {
+ var m = memberFrom("[Foo] object toJSON()");
+ assert_true(m.is_to_json_regular_operation());
+ }, 'should return true when member is a regular toJSON operation with extensible attributes.');
+</script>
+</body>
+</html>
+
diff --git a/test/wpt/tests/resources/test/tests/unit/IdlInterfaceMember/toString.html b/test/wpt/tests/resources/test/tests/unit/IdlInterfaceMember/toString.html
new file mode 100644
index 0000000..054dbb1
--- /dev/null
+++ b/test/wpt/tests/resources/test/tests/unit/IdlInterfaceMember/toString.html
@@ -0,0 +1,36 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>IdlInterfaceMember.prototype.toString()</title>
+</head>
+<body>
+<div id="log"></div>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/WebIDLParser.js"></script>
+<script src="/resources/idlharness.js"></script>
+<script src="../../../idl-helper.js"></script>
+<script>
+"use strict";
+const tests = [
+ ["long x", "long"],
+ ["long? x", "long?"],
+ ["Promise<long> x", "Promise<long>"],
+ ["Promise<long?> x", "Promise<long?>"],
+ ["sequence<long> x", "sequence<long>"],
+ ["(long or DOMString) x", "(long or DOMString)"],
+ ["long x, boolean y", "long, boolean"],
+ ["long x, optional boolean y", "long, optional boolean"],
+ ["long... args", "long..."],
+ ["sequence<long>... args", "sequence<long>..."],
+ ["(long or DOMString)... args", "(long or DOMString)..."],
+];
+for (const [input, output] of tests) {
+ test(function() {
+ var m = memberFrom(`undefined foo(${input})`);
+ assert_equals(m.toString(), `foo(${output})`);
+ }, `toString for ${input}`);
+}
+</script>
+</body>
+</html>
diff --git a/test/wpt/tests/resources/test/tests/unit/assert_implements.html b/test/wpt/tests/resources/test/tests/unit/assert_implements.html
new file mode 100644
index 0000000..6e35f38
--- /dev/null
+++ b/test/wpt/tests/resources/test/tests/unit/assert_implements.html
@@ -0,0 +1,43 @@
+<!DOCTYPE HTML>
+<html lang="en">
+<meta charset="utf-8">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/test/tests/unit/helpers.js"></script>
+<title>assert_implements unittests</title>
+<script>
+'use strict';
+
+test(() => {
+ // All values in JS that are not falsy are truthy, so we just check some
+ // common cases here.
+ assert_implements(true, 'true is a truthy value');
+ assert_implements(5, 'positive integeter is a truthy value');
+ assert_implements(-5, 'negative integeter is a truthy value');
+ assert_implements('foo', 'non-empty string is a truthy value');
+}, 'truthy values');
+
+test_failure(() => {
+ assert_implements(false);
+}, 'false is a falsy value');
+
+test_failure(() => {
+ assert_implements(0);
+}, '0 is a falsy value');
+
+test_failure(() => {
+ assert_implements('');
+}, 'empty string is a falsy value');
+
+test_failure(() => {
+ assert_implements(null);
+}, 'null is a falsy value');
+
+test_failure(() => {
+ assert_implements(undefined);
+}, 'undefined is a falsy value');
+
+test_failure(() => {
+ assert_implements(NaN);
+}, 'NaN is a falsy value');
+</script>
diff --git a/test/wpt/tests/resources/test/tests/unit/assert_implements_optional.html b/test/wpt/tests/resources/test/tests/unit/assert_implements_optional.html
new file mode 100644
index 0000000..4f23e20
--- /dev/null
+++ b/test/wpt/tests/resources/test/tests/unit/assert_implements_optional.html
@@ -0,0 +1,43 @@
+<!DOCTYPE HTML>
+<html lang="en">
+<meta charset="utf-8">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/test/tests/unit/helpers.js"></script>
+<title>assert_implements_optional unittests</title>
+<script>
+'use strict';
+
+test(() => {
+ // All values in JS that are not falsy are truthy, so we just check some
+ // common cases here.
+ assert_implements_optional(true, 'true is a truthy value');
+ assert_implements_optional(5, 'positive integeter is a truthy value');
+ assert_implements_optional(-5, 'negative integeter is a truthy value');
+ assert_implements_optional('foo', 'non-empty string is a truthy value');
+}, 'truthy values');
+
+test_failure(() => {
+ assert_implements_optional(false);
+}, 'false is a falsy value');
+
+test_failure(() => {
+ assert_implements_optional(0);
+}, '0 is a falsy value');
+
+test_failure(() => {
+ assert_implements_optional('');
+}, 'empty string is a falsy value');
+
+test_failure(() => {
+ assert_implements_optional(null);
+}, 'null is a falsy value');
+
+test_failure(() => {
+ assert_implements_optional(undefined);
+}, 'undefined is a falsy value');
+
+test_failure(() => {
+ assert_implements_optional(NaN);
+}, 'NaN is a falsy value');
+</script>
diff --git a/test/wpt/tests/resources/test/tests/unit/assert_object_equals.html b/test/wpt/tests/resources/test/tests/unit/assert_object_equals.html
new file mode 100644
index 0000000..313d77b
--- /dev/null
+++ b/test/wpt/tests/resources/test/tests/unit/assert_object_equals.html
@@ -0,0 +1,152 @@
+<!DOCTYPE HTML>
+<html lang="en">
+<meta charset="utf-8">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/test/tests/unit/helpers.js"></script>
+<title>Assertion functions</title>
+<script>
+'use strict';
+
+test(function() {
+ assert_object_equals({}, {});
+}, 'empty objects');
+
+test(function() {
+ var actual = {};
+ var expected = {};
+ actual.a = actual;
+ expected.a = actual;
+
+ assert_object_equals(actual, expected);
+}, 'tolerates cycles in actual value');
+
+test(function() {
+ var actual = {};
+ var expected = {};
+ actual.a = expected;
+ expected.a = expected;
+
+ assert_object_equals(actual, expected);
+}, 'tolerates cycles in expected value');
+
+test(function() {
+ assert_object_equals({2: 99, 0: 23, 1: 45}, [23, 45, 99]);
+}, 'recognizes equivalence of actual object and expected array');
+
+test(function() {
+ assert_object_equals([23, 45, 99], {2: 99, 0: 23, 1: 45});
+}, 'recognizes equivalence of actual array and expected object');
+
+test(function() {
+ var actual = {};
+ var expected = {};
+ Object.defineProperty(actual, 'a', { value: 1, enumerable: false });
+
+ assert_not_equals(actual.a, expected.a);
+ assert_object_equals(actual, expected);
+}, 'non-enumerable properties in actual value ignored');
+
+test(function() {
+ var actual = {};
+ var expected = {};
+ Object.defineProperty(expected, 'a', { value: 1, enumerable: false });
+
+ assert_not_equals(actual.a, expected.a);
+ assert_object_equals(actual, expected);
+}, 'non-enumerable properties in expected value ignored');
+
+test(function() {
+ assert_object_equals({c: 3, a: 1, b: 2}, {a: 1, b: 2, c: 3});
+}, 'equivalent objects - "flat" object');
+
+test(function() {
+ assert_object_equals(
+ {c: {e: 5, d: 4}, b: 2, a: 1},
+ {a: 1, b: 2, c: {d: 4, e: 5}}
+ );
+}, 'equivalent objects - nested object');
+
+test(function() {
+ assert_object_equals(
+ {c: [4, 5], b: 2, a: 1},
+ {a: 1, b: 2, c: [4, 5]}
+ );
+}, 'equivalent objects - nested array');
+
+test(function() {
+ assert_object_equals({a: NaN}, {a: NaN});
+}, 'equivalent objects - NaN value');
+
+test(function() {
+ assert_object_equals({a: -0}, {a: -0});
+}, 'equivalent objects - negative zero value');
+
+test_failure(function() {
+ assert_object_equals(undefined, {});
+}, 'invalid actual value: undefined');
+
+test_failure(function() {
+ assert_object_equals(null, {});
+}, 'invalid actual value: null');
+
+test_failure(function() {
+ assert_object_equals(34, {});
+}, 'invalid actual value: number');
+
+test_failure(function() {
+ assert_object_equals({c: 3, a: 1, b: 2}, {a: 1, b: 1, c: 3});
+}, 'unequal property value - "flat" object');
+
+test_failure(function() {
+ var actual = Object.create({a: undefined});
+ var expected = {};
+
+ assert_object_equals(actual, expected);
+}, 'non-own properties in actual value verified');
+
+test_failure(function() {
+ var actual = {};
+ var expected = Object.create({a: undefined});
+
+ assert_object_equals(actual, expected);
+}, 'non-own properties in expected value verified');
+
+test_failure(function() {
+ assert_object_equals(
+ {a: 1, b: 2, c: {d: 5, e: 5, f: 6}},
+ {a: 1, b: 2, c: {d: 5, e: 7, f: 6}}
+ );
+}, 'unequal property value - nested object');
+
+test_failure(function() {
+ assert_object_equals(
+ {a: 1, b: 2, c: [4, 5, 6]},
+ {a: 1, b: 2, c: [4, 7, 6]}
+ );
+}, 'unequal property value - nested array');
+
+test_failure(function() {
+ assert_object_equals({a: NaN}, {a: 0});
+}, 'equivalent objects - NaN actual value');
+
+test_failure(function() {
+ assert_object_equals({a: 0}, {a: NaN});
+}, 'equivalent objects - NaN expected value');
+
+test_failure(function() {
+ assert_object_equals({a: -0}, {a: 0});
+}, 'equivalent objects - negative zero actual value');
+
+test_failure(function() {
+ assert_object_equals({a: 0}, {a: -0});
+}, 'equivalent objects - negative zero expected value');
+
+test_failure(function() {
+ assert_object_equals({a: 1}, {});
+}, 'actual contains additional property');
+
+test_failure(function() {
+ assert_object_equals({}, {a: 1});
+}, 'expected contains additional property');
+</script>
diff --git a/test/wpt/tests/resources/test/tests/unit/async-test-return-restrictions.html b/test/wpt/tests/resources/test/tests/unit/async-test-return-restrictions.html
new file mode 100644
index 0000000..0fde2e2
--- /dev/null
+++ b/test/wpt/tests/resources/test/tests/unit/async-test-return-restrictions.html
@@ -0,0 +1,135 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <script src="/resources/testharness.js"></script>
+ <title>Restrictions on return value from `async_test`</title>
+</head>
+<body>
+<script>
+function makeTest(...bodies) {
+ const closeScript = '<' + '/script>';
+ let src = `
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Document title</title>
+<script src="/resources/testharness.js?${Math.random()}">${closeScript}
+</head>
+
+<body>
+<div id="log"></div>`;
+ bodies.forEach((body) => {
+ src += '<script>(' + body + ')();' + closeScript;
+ });
+
+ const iframe = document.createElement('iframe');
+
+ document.body.appendChild(iframe);
+ iframe.contentDocument.write(src);
+
+ return new Promise((resolve) => {
+ window.addEventListener('message', function onMessage(e) {
+ if (e.source !== iframe.contentWindow) {
+ return;
+ }
+ if (!e.data || e.data.type !=='complete') {
+ return;
+ }
+ window.removeEventListener('message', onMessage);
+ resolve(e.data);
+ });
+
+ iframe.contentDocument.close();
+ }).then(({ tests, status }) => {
+ const summary = {
+ harness: {
+ status: getEnumProp(status, status.status),
+ message: status.message
+ },
+ tests: {}
+ };
+
+ tests.forEach((test) => {
+ summary.tests[test.name] = getEnumProp(test, test.status);
+ });
+
+ return summary;
+ });
+}
+
+function getEnumProp(object, value) {
+ for (let property in object) {
+ if (!/^[A-Z]+$/.test(property)) {
+ continue;
+ }
+
+ if (object[property] === value) {
+ return property;
+ }
+ }
+}
+
+promise_test(() => {
+ return makeTest(
+ () => {
+ async_test((t) => {t.done(); return undefined;}, 'before');
+ async_test((t) => {t.done(); return null;}, 'null');
+ async_test((t) => {t.done(); return undefined;}, 'after');
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness.status, 'ERROR');
+ assert_equals(
+ harness.message,
+ 'Test named "null" passed a function to `async_test` that returned a value.'
+ );
+ assert_equals(tests.before, 'PASS');
+ assert_equals(tests.null, 'PASS');
+ // This test did not get the chance to start.
+ assert_equals(tests.after, undefined);
+ });
+}, 'test returning `null`');
+
+promise_test(() => {
+ return makeTest(
+ () => {
+ async_test((t) => {t.done(); return undefined;}, 'before');
+ async_test((t) => {t.done(); return {};}, 'object');
+ async_test((t) => {t.done(); return undefined;}, 'after');
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness.status, 'ERROR');
+ assert_equals(
+ harness.message,
+ 'Test named "object" passed a function to `async_test` that returned a value.'
+ );
+ assert_equals(tests.before, 'PASS');
+ assert_equals(tests.object, 'PASS');
+ // This test did not get the chance to start.
+ assert_equals(tests.after, undefined);
+ });
+}, 'test returning an ordinary object');
+
+promise_test(() => {
+ return makeTest(
+ () => {
+ async_test((t) => {t.done(); return undefined;}, 'before');
+ async_test((t) => {t.done(); return Promise.resolve(5);}, 'thenable');
+ async_test((t) => {t.done(); return undefined;}, 'after');
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness.status, 'ERROR');
+ assert_equals(
+ harness.message,
+ 'Test named "thenable" passed a function to `async_test` that returned a value. ' +
+ 'Consider using `promise_test` instead when using Promises or async/await.'
+ );
+ assert_equals(tests.before, 'PASS');
+ assert_equals(tests.thenable, 'PASS');
+ // This test did not get a chance to start.
+ assert_equals(tests.after, undefined);
+ });
+}, 'test returning a thenable object');
+</script>
+</body>
+</html>
diff --git a/test/wpt/tests/resources/test/tests/unit/basic.html b/test/wpt/tests/resources/test/tests/unit/basic.html
new file mode 100644
index 0000000..d52082f
--- /dev/null
+++ b/test/wpt/tests/resources/test/tests/unit/basic.html
@@ -0,0 +1,48 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>idlharness basic</title>
+</head>
+<body>
+<div id="log"></div>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/WebIDLParser.js"></script>
+<script src="/resources/idlharness.js"></script>
+<script>
+ "use strict";
+ test(function() {
+ assert_true("IdlArray" in window);
+ }, 'IdlArray constructor should be a global object');
+ test(function() {
+ assert_true(new IdlArray() instanceof IdlArray);
+ }, 'IdlArray constructor should be constructible');
+ test(function() {
+ assert_true("WebIDL2" in window);
+ }, 'WebIDL2 namespace should be a global object');
+ test(function() {
+ assert_equals(typeof WebIDL2.parse, "function");
+ }, 'WebIDL2 namespace should have a parse method');
+ test(function() {
+ try {
+ WebIDL2.parse("I'm a syntax error");
+ throw new Error("Web IDL didn't throw");
+ } catch (e) {
+ assert_equals(e.name, "WebIDLParseError");
+ }
+ }, 'WebIDL2 parse method should bail on incorrect WebIDL');
+ test(function() {
+ assert_equals(typeof WebIDL2.parse("interface Foo {};"), "object");
+ }, 'WebIDL2 parse method should produce an AST for correct WebIDL');
+ test(function () {
+ try {
+ let i = new IdlArray();
+ i.add_untested_idls(`interface C {};`);
+ i.assert_throws('Anything', i => i.test());
+ } catch (e) {
+ assert_true(e instanceof IdlHarnessError);
+ }
+ }, `assert_throws should throw if no IdlHarnessError thrown`);
+</script>
+</body>
+</html>
diff --git a/test/wpt/tests/resources/test/tests/unit/exceptional-cases-timeouts.html b/test/wpt/tests/resources/test/tests/unit/exceptional-cases-timeouts.html
new file mode 100644
index 0000000..760ac71
--- /dev/null
+++ b/test/wpt/tests/resources/test/tests/unit/exceptional-cases-timeouts.html
@@ -0,0 +1,120 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="timeout" content="long">
+ <script src="/resources/testharness.js"></script>
+ <title>Exceptional cases - timeouts</title>
+</head>
+<body>
+<p>
+ The tests in this file are executed in parallel to avoid exceeding the "long"
+ timeout duration.
+</p>
+<script>
+function makeTest(...bodies) {
+ const closeScript = '<' + '/script>';
+ let src = `
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Document title</title>
+<script src="/resources/testharness.js?${Math.random()}">${closeScript}
+</head>
+
+<body>
+<div id="log"></div>`;
+ bodies.forEach((body) => {
+ src += '<script>(' + body + ')();' + closeScript;
+ });
+
+ const iframe = document.createElement('iframe');
+
+ document.body.appendChild(iframe);
+ iframe.contentDocument.write(src);
+
+ return new Promise((resolve) => {
+ window.addEventListener('message', function onMessage(e) {
+ if (e.source !== iframe.contentWindow) {
+ return;
+ }
+ if (!e.data || e.data.type !=='complete') {
+ return;
+ }
+ window.removeEventListener('message', onMessage);
+ resolve(e.data);
+ });
+
+ iframe.contentDocument.close();
+ }).then(({ tests, status }) => {
+ const summary = {
+ harness: getEnumProp(status, status.status),
+ tests: {}
+ };
+
+ tests.forEach((test) => {
+ summary.tests[test.name] = getEnumProp(test, test.status);
+ });
+
+ return summary;
+ });
+}
+
+function getEnumProp(object, value) {
+ for (let property in object) {
+ if (!/^[A-Z]+$/.test(property)) {
+ continue;
+ }
+
+ if (object[property] === value) {
+ return property;
+ }
+ }
+}
+
+(() => {
+ window.asyncTestCleanupCount1 = 0;
+ const nestedTest = makeTest(
+ () => {
+ async_test((t) => {
+ t.add_cleanup(() => window.parent.asyncTestCleanupCount1 += 1);
+ setTimeout(() => {
+ throw new Error('this error is expected');
+ });
+ }, 'test');
+ }
+ );
+ promise_test(() => {
+ return nestedTest.then(({harness, tests}) => {
+ assert_equals(harness, 'ERROR');
+ assert_equals(tests.test, 'TIMEOUT');
+ assert_equals(window.asyncTestCleanupCount1, 1);
+ });
+ }, 'uncaught exception during async_test which times out');
+})();
+
+(() => {
+ window.promiseTestCleanupCount2 = 0;
+ const nestedTest = makeTest(
+ () => {
+ promise_test((t) => {
+ t.add_cleanup(() => window.parent.promiseTestCleanupCount2 += 1);
+ setTimeout(() => {
+ throw new Error('this error is expected');
+ });
+
+ return new Promise(() => {});
+ }, 'test');
+ }
+ );
+ promise_test(() => {
+ return nestedTest.then(({harness, tests}) => {
+ assert_equals(harness, 'ERROR');
+ assert_equals(tests.test, 'TIMEOUT');
+ assert_equals(window.promiseTestCleanupCount2, 1);
+ });
+ }, 'uncaught exception during promise_test which times out');
+})();
+</script>
+</body>
+</html>
diff --git a/test/wpt/tests/resources/test/tests/unit/exceptional-cases.html b/test/wpt/tests/resources/test/tests/unit/exceptional-cases.html
new file mode 100644
index 0000000..4054d03
--- /dev/null
+++ b/test/wpt/tests/resources/test/tests/unit/exceptional-cases.html
@@ -0,0 +1,392 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="timeout" content="long">
+ <script src="/resources/testharness.js"></script>
+ <title>Exceptional cases</title>
+</head>
+<body>
+<script>
+function makeTest(...bodies) {
+ const closeScript = '<' + '/script>';
+ let src = `
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Document title</title>
+<script src="/resources/testharness.js?${Math.random()}">${closeScript}
+</head>
+
+<body>
+<div id="log"></div>`;
+ bodies.forEach((body) => {
+ src += '<script>(' + body + ')();' + closeScript;
+ });
+
+ const iframe = document.createElement('iframe');
+
+ document.body.appendChild(iframe);
+ iframe.contentDocument.write(src);
+
+ return new Promise((resolve) => {
+ window.addEventListener('message', function onMessage(e) {
+ if (e.source !== iframe.contentWindow) {
+ return;
+ }
+ if (!e.data || e.data.type !=='complete') {
+ return;
+ }
+ window.removeEventListener('message', onMessage);
+ resolve(e.data);
+ });
+
+ iframe.contentDocument.close();
+ }).then(({ tests, status }) => {
+ const summary = {
+ harness: getEnumProp(status, status.status),
+ tests: {}
+ };
+
+ tests.forEach((test) => {
+ summary.tests[test.name] = getEnumProp(test, test.status);
+ });
+
+ return summary;
+ });
+}
+
+function getEnumProp(object, value) {
+ for (let property in object) {
+ if (!/^[A-Z]+$/.test(property)) {
+ continue;
+ }
+
+ if (object[property] === value) {
+ return property;
+ }
+ }
+}
+
+promise_test(() => {
+ return makeTest(
+ () => { done(); }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'ERROR');
+ assert_array_equals(Object.keys(tests), []);
+ });
+}, 'completion signaled before testing begins');
+
+promise_test(() => {
+ return makeTest(
+ () => { assert_true(true); done(); }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'ERROR');
+ assert_array_equals(Object.keys(tests), []);
+ });
+}, 'passing assertion before testing begins');
+
+promise_test(() => {
+ return makeTest(
+ () => { assert_false(true); }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'ERROR');
+ assert_array_equals(Object.keys(tests), []);
+ });
+}, 'failing assertion before testing begins');
+
+promise_test(() => {
+ return makeTest(
+ () => { throw new Error('this error is expected'); }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'ERROR');
+ assert_array_equals(Object.keys(tests), []);
+ });
+}, 'uncaught exception before testing begins');
+
+promise_test(() => {
+ return makeTest(
+ () => {
+ setup({ allow_uncaught_exception: true });
+ throw new Error('this error is expected');
+ },
+ () => {
+ test(function() {}, 'a');
+ test(function() {}, 'b');
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'OK');
+ assert_equals(tests.a, 'PASS');
+ assert_equals(tests.b, 'PASS');
+ });
+}, 'uncaught exception with subsequent subtest');
+
+promise_test(() => {
+ return makeTest(
+ () => {
+ async_test((t) => {
+ setTimeout(() => {
+ setTimeout(() => t.done(), 0);
+ async_test((t) => { setTimeout(t.done.bind(t), 0); }, 'after');
+ throw new Error('this error is expected');
+ }, 0);
+ }, 'during');
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'ERROR');
+ assert_equals(tests.during, 'PASS');
+ assert_equals(tests.after, 'PASS');
+ });
+}, 'uncaught exception during async_test');
+
+promise_test(() => {
+ return makeTest(
+ () => {
+ promise_test(() => {
+ return new Promise((resolve) => {
+ setTimeout(() => {
+ resolve();
+ promise_test(() => Promise.resolve(), 'after');
+ throw new Error('this error is expected');
+ }, 0);
+ });
+ }, 'during');
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'ERROR');
+ assert_equals(tests.during, 'PASS');
+ assert_equals(tests.after, 'PASS');
+ });
+}, 'uncaught exception during promise_test');
+
+promise_test(() => {
+ return makeTest(
+ () => { test(() => {}, 'before'); },
+ () => { throw new Error('this error is expected'); },
+ () => { test(() => {}, 'after'); }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'ERROR');
+ assert_equals(tests.before, 'PASS');
+ assert_equals(tests.after, 'PASS');
+ });
+}, 'uncaught exception between tests');
+
+promise_test(() => {
+ return makeTest(
+ () => { promise_test(() => Promise.resolve(), 'before'); },
+ () => { throw new Error('this error is expected'); },
+ () => { promise_test(() => Promise.resolve(), 'after'); }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'ERROR');
+ assert_equals(tests.before, 'PASS');
+ assert_equals(tests.after, 'PASS');
+ });
+}, 'uncaught exception between promise_tests');
+
+
+// This feature of testharness.js is only observable in browsers which
+// implement the `unhandledrejection` event.
+if ('onunhandledrejection' in window) {
+
+ promise_test(() => {
+ return makeTest(
+ () => { Promise.reject(new Error('this error is expected')); }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'ERROR');
+ assert_array_equals(Object.keys(tests), []);
+ });
+ }, 'unhandled rejection before testing begins');
+
+ promise_test(() => {
+ return makeTest(
+ () => {
+ async_test((t) => {
+ Promise.reject(new Error('this error is expected'));
+
+ window.addEventListener('unhandledrejection', () => {
+ setTimeout(() => t.done(), 0);
+ async_test((t) => { setTimeout(t.done.bind(t), 0); }, 'after');
+ t.done();
+ });
+ }, 'during');
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'ERROR');
+ assert_equals(tests.during, 'PASS');
+ assert_equals(tests.after, 'PASS');
+ });
+ }, 'unhandled rejection during async_test');
+
+ promise_test(() => {
+ return makeTest(
+ () => {
+ promise_test(() => {
+ return new Promise((resolve) => {
+ Promise.reject(new Error('this error is expected'));
+
+ window.addEventListener('unhandledrejection', () => {
+ resolve();
+ promise_test(() => Promise.resolve(), 'after');
+ throw new Error('this error is expected');
+ }, 0);
+ });
+ }, 'during');
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'ERROR');
+ assert_equals(tests.during, 'PASS');
+ assert_equals(tests.after, 'PASS');
+ });
+ }, 'unhandled rejection during promise_test');
+
+ promise_test(() => {
+ return makeTest(
+ () => {
+ setup({ explicit_done: true });
+ test(() => {}, 'before');
+ Promise.reject(new Error('this error is expected'));
+ window.addEventListener('unhandledrejection', () => {
+ test(() => {}, 'after');
+ done();
+ });
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'ERROR');
+ assert_equals(tests.before, 'PASS');
+ assert_true('after' in tests);
+ });
+ }, 'unhandled rejection between tests');
+
+ promise_test(() => {
+ return makeTest(
+ () => {
+ setup({ explicit_done: true });
+ async_test((t) => { setTimeout(t.done.bind(t), 0); }, 'before');
+ Promise.reject(new Error('this error is expected'));
+ window.addEventListener('unhandledrejection', () => {
+ async_test((t) => { setTimeout(t.done.bind(t), 0); }, 'after');
+ done();
+ });
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'ERROR');
+ assert_equals(tests.before, 'PASS');
+ assert_equals(tests.after, 'PASS');
+ });
+ }, 'unhandled rejection between async_tests');
+
+ promise_test(() => {
+ return makeTest(
+ () => {
+ setup({ explicit_done: true });
+ promise_test(() => Promise.resolve(), 'before');
+ Promise.reject(new Error('this error is expected'));
+ window.addEventListener('unhandledrejection', () => {
+ promise_test(() => Promise.resolve(), 'after');
+ done();
+ });
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'ERROR');
+ assert_equals(tests.before, 'PASS');
+ assert_true('after' in tests);
+ });
+ }, 'unhandled rejection between promise_tests');
+
+ promise_test(() => {
+ return makeTest(
+ () => {
+ test((t) => {
+ t.add_cleanup(() => { throw new Error('this error is expected'); });
+ }, 'during');
+ test((t) => {}, 'after');
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'ERROR');
+ assert_equals(tests.during, 'PASS');
+ assert_equals(tests.after, 'NOTRUN');
+ });
+ }, 'exception in `add_cleanup` of a test');
+
+}
+
+
+promise_test(() => {
+ return makeTest(
+ () => {
+ setup({explicit_done: true});
+ window.addEventListener('DOMContentLoaded', () => {
+ async_test((t) => {
+ t.add_cleanup(() => {
+ setTimeout(() => {
+ async_test((t) => t.done(), 'after');
+ done();
+ }, 0);
+ throw new Error('this error is expected');
+ });
+ setTimeout(t.done.bind(t), 0);
+ }, 'during');
+ });
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'ERROR');
+ assert_equals(tests.during, 'PASS');
+ assert_equals(tests.after, 'NOTRUN');
+ });
+}, 'exception in `add_cleanup` of an async_test');
+
+promise_test(() => {
+ return makeTest(
+ () => {
+ promise_test((t) => {
+ t.add_cleanup(() => { throw new Error('this error is expected'); });
+ return Promise.resolve();
+ }, 'test');
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'ERROR');
+ assert_equals(tests.test, 'PASS');
+ });
+}, 'exception in `add_cleanup` of a promise_test');
+
+promise_test(() => {
+ return makeTest(
+ () => {
+ promise_test((t) => {
+ t.step(() => {
+ throw new Error('this error is expected');
+ });
+ }, 'test');
+ async_test((t) => t.done(), 'after');
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'OK');
+ assert_equals(tests.test, 'FAIL');
+ assert_equals(tests.after, 'PASS');
+ });
+}, 'exception in `step` of an async_test');
+
+promise_test(() => {
+ return makeTest(
+ () => {
+ promise_test((t) => {
+ t.step(() => {
+ throw new Error('this error is expected');
+ });
+
+ return new Promise(() => {});
+ }, 'test');
+
+ // This following test should be run to completion despite the fact
+ // that the promise returned by the previous test never resolves.
+ promise_test((t) => Promise.resolve(), 'after');
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'OK');
+ assert_equals(tests.test, 'FAIL');
+ assert_equals(tests.after, 'PASS');
+ });
+}, 'exception in `step` of a promise_test');
+</script>
+</body>
+</html>
diff --git a/test/wpt/tests/resources/test/tests/unit/format-value.html b/test/wpt/tests/resources/test/tests/unit/format-value.html
new file mode 100644
index 0000000..13d01b8
--- /dev/null
+++ b/test/wpt/tests/resources/test/tests/unit/format-value.html
@@ -0,0 +1,123 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>format_value utility function</title>
+ <meta charset="utf-8">
+</head>
+<body>
+<div id="log"></div>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+"use strict";
+
+test(function() {
+ assert_equals(format_value(null), "null");
+}, "null");
+
+test(function() {
+ assert_equals(format_value(undefined), "undefined");
+}, "undefined");
+
+test(function() {
+ assert_equals(format_value(true), "true");
+ assert_equals(format_value(false), "false");
+}, "boolean values");
+
+test(function() {
+ assert_equals(format_value(0.4), "0.4");
+ assert_equals(format_value(0), "0");
+ assert_equals(format_value(-0), "-0");
+}, "number values");
+
+test(function() {
+ assert_equals(format_value("a string"), "\"a string\"");
+ assert_equals(format_value("new\nline"), "\"new\\nline\"");
+}, "string values");
+
+test(function() {
+ var node = document.createElement("span");
+ node.setAttribute("data-foo", "bar");
+ assert_true(
+ /<span\b/i.test(format_value(node)), "element includes tag name"
+ );
+ assert_true(
+ /data-foo=["']?bar["']?/i.test(format_value(node)),
+ "element includes attributes"
+ );
+}, "node value: element node");
+
+test(function() {
+ var text = document.createTextNode("wpt");
+ assert_equals(format_value(text), "Text node \"wpt\"");
+}, "node value: text node");
+
+test(function() {
+ var node = document.createProcessingInstruction("wpt1", "wpt2");
+ assert_equals(
+ format_value(node),
+ "ProcessingInstruction node with target \"wpt1\" and data \"wpt2\""
+ );
+}, "node value: ProcessingInstruction node");
+
+test(function() {
+ var node = document.createComment("wpt");
+ assert_equals(format_value(node), "Comment node <!--wpt-->");
+}, "node value: comment node");
+
+test(function() {
+ var node = document.implementation.createDocument(
+ "application/xhtml+xml", "", null
+ );
+
+ assert_equals(format_value(node), "Document node with 0 children");
+
+ node.appendChild(document.createElement('html'));
+
+ assert_equals(format_value(node), "Document node with 1 child");
+}, "node value: document node");
+
+test(function() {
+ var node = document.implementation.createDocumentType("foo", "baz", "baz");
+
+ assert_equals(format_value(node), "DocumentType node");
+}, "node value: DocumentType node");
+
+test(function() {
+ var node = document.createDocumentFragment();
+
+ assert_equals(format_value(node), "DocumentFragment node with 0 children");
+
+ node.appendChild(document.createElement("span"));
+
+ assert_equals(format_value(node), "DocumentFragment node with 1 child");
+
+ node.appendChild(document.createElement("span"));
+
+ assert_equals(format_value(node), "DocumentFragment node with 2 children");
+}, "node value: DocumentFragment node");
+
+test(function() {
+ assert_equals(format_value(Symbol("wpt")), "symbol \"Symbol(wpt)\"");
+}, "symbol value");
+
+test(function() {
+ assert_equals(format_value([]), "[]");
+ assert_equals(format_value(["one"]), "[\"one\"]");
+ assert_equals(format_value(["one", "two"]), "[\"one\", \"two\"]");
+}, "array values");
+
+test(function() {
+ var obj = {
+ toString: function() {
+ throw "wpt";
+ }
+ };
+
+ assert_equals(
+ format_value(obj), "[stringifying object threw wpt with type string]"
+ );
+}, "object value with faulty `toString`");
+</script>
+</body>
+</html>
diff --git a/test/wpt/tests/resources/test/tests/unit/helpers.js b/test/wpt/tests/resources/test/tests/unit/helpers.js
new file mode 100644
index 0000000..ca378a2
--- /dev/null
+++ b/test/wpt/tests/resources/test/tests/unit/helpers.js
@@ -0,0 +1,21 @@
+// Helper for testing assertion failure cases for a testharness.js API
+//
+// The `assert_throws_*` functions cannot be used for this purpose because they
+// always fail in response to AssertionError exceptions, even when this is
+// expressed as the expected error.
+function test_failure(fn, name) {
+ test(function() {
+ try {
+ fn();
+ } catch (err) {
+ if (err instanceof AssertionError) {
+ return;
+ }
+ throw new AssertionError('Expected an AssertionError, but' + err);
+ }
+ throw new AssertionError(
+ 'Expected an AssertionError, but no error was thrown'
+ );
+ }, name);
+}
+
diff --git a/test/wpt/tests/resources/test/tests/unit/late-test.html b/test/wpt/tests/resources/test/tests/unit/late-test.html
new file mode 100644
index 0000000..693d7e3
--- /dev/null
+++ b/test/wpt/tests/resources/test/tests/unit/late-test.html
@@ -0,0 +1,54 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Test declared after harness completion</title>
+</head>
+<body>
+<div id="log"></div>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<p>This test simulates an automated test running scenario, where the test
+results emitted by testharness.js may be interpreted after some delay. It is
+intended to demonstrate that in such cases, any additional tests which are
+executed during that delay are <em>not</em> included in the dataset.</p>
+
+<p>Although these "late" tests are likely an indication of a mistake in test
+design, they cannot be detected deterministically, so in the interest of
+stability, they should be silently tolerated.</p>
+<script>
+async_test(function(t) {
+ var source = [
+ "<div id='log'></div>",
+ "<script src='/resources/testharness.js'></" + "script>",
+ "<script src='/resources/testharnessreport.js'></" + "script>",
+ "<script>",
+ "parent.childReady(window);",
+ "setup({ explicit_done: true });",
+ "test(function() {}, 'acceptable test');",
+ "onload = function() {",
+ " done();",
+ " test(function() {}, 'this test is late and should be ignored');",
+ "};",
+ "</" + "script>"
+ ].join("\n");
+ var iframe = document.createElement("iframe");
+
+ document.body.appendChild(iframe);
+ window.childReady = t.step_func(function(childWindow) {
+ childWindow.add_completion_callback(t.step_func(function(tests, status) {
+ t.step_timeout(t.step_func(function() {
+ assert_equals(tests.length, 1);
+ assert_equals(tests[0].name, "acceptable test");
+ assert_equals(status.status, status.OK);
+ t.done();
+ }), 0);
+ }));
+ });
+
+ iframe.contentDocument.open();
+ iframe.contentDocument.write(source);
+ iframe.contentDocument.close();
+});
+</script>
+</body>
+</html>
diff --git a/test/wpt/tests/resources/test/tests/unit/promise_setup-timeout.html b/test/wpt/tests/resources/test/tests/unit/promise_setup-timeout.html
new file mode 100644
index 0000000..c4947fe
--- /dev/null
+++ b/test/wpt/tests/resources/test/tests/unit/promise_setup-timeout.html
@@ -0,0 +1,28 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="timeout" content="long">
+ <script src="/resources/testharness.js"></script>
+ <script src="../../nested-testharness.js"></script>
+ <title>promise_setup - timeout</title>
+</head>
+<body>
+<script>
+'use strict';
+promise_test(() => {
+ return makeTest(
+ () => {
+ test(() => {}, 'before');
+ promise_setup(() => new Promise(() => {}));
+ promise_test(() => Promise.resolve(), 'after');
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'TIMEOUT');
+ assert_equals(tests.before, 'PASS');
+ assert_equals(tests.after, 'NOTRUN');
+ });
+}, 'timeout when returned promise does not settle');
+</script>
+</body>
+</html>
diff --git a/test/wpt/tests/resources/test/tests/unit/promise_setup.html b/test/wpt/tests/resources/test/tests/unit/promise_setup.html
new file mode 100644
index 0000000..2abb10a
--- /dev/null
+++ b/test/wpt/tests/resources/test/tests/unit/promise_setup.html
@@ -0,0 +1,333 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <script src="/resources/testharness.js"></script>
+ <script src="../../nested-testharness.js"></script>
+ <title>promise_setup</title>
+</head>
+<body>
+<script>
+'use strict';
+promise_test(() => {
+ return makeTest(
+ () => {
+ // Ensure that the harness error is the result of explicit error
+ // handling
+ setup({ allow_uncaught_exception: true });
+
+ test(() => {}, 'before');
+ promise_setup({});
+ promise_test(() => Promise.resolve(), 'after');
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'ERROR');
+ assert_equals(tests.before, 'PASS');
+ assert_equals(tests.after, undefined);
+ });
+}, 'Error when no function provided');
+
+promise_test(() => {
+ return makeTest(
+ () => {
+ test(() => {}, 'before');
+ promise_setup(() => Promise.resolve(), {});
+ promise_test(() => Promise.resolve(), 'after');
+ throw new Error('this error is expected');
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'ERROR');
+ assert_equals(tests.before, 'PASS');
+ assert_equals(tests.after, 'PASS');
+ });
+}, 'Does not apply unspecified configuration properties');
+
+promise_test(() => {
+ return makeTest(
+ () => {
+ var properties = {
+ allow_uncaught_exception: true
+ };
+ test(() => {}, 'before');
+ promise_setup(() => Promise.resolve(), properties);
+ promise_test(() => Promise.resolve(), 'after');
+ throw new Error('this error is expected');
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'ERROR');
+ assert_equals(tests.before, 'PASS');
+ assert_equals(tests.after, 'PASS');
+ });
+}, 'Ignores configuration properties when some tests have already run');
+
+promise_test(() => {
+ return makeTest(
+ () => {
+ var properties = {
+ allow_uncaught_exception: true
+ };
+ promise_setup(() => Promise.resolve(), properties);
+ promise_test(() => {
+ return new Promise((resolve) => {
+ setTimeout(() => {
+ resolve();
+ throw new Error('this error is expected');
+ });
+ });
+ }, 'after');
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'OK');
+ assert_equals(tests.after, 'PASS');
+ });
+}, 'Honors configuration properties');
+
+promise_test(() => {
+ return makeTest(
+ () => {
+ // Ensure that the harness error is the result of explicit error
+ // handling
+ setup({ allow_uncaught_exception: true });
+
+ test(() => {}, 'before');
+ promise_setup(() => { throw new Error('this error is expected'); });
+ promise_test(() => Promise.resolve(), 'after');
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'ERROR');
+ assert_equals(tests.before, 'PASS');
+ assert_equals(tests.after, 'NOTRUN');
+ });
+}, 'Error for synchronous exceptions');
+
+promise_test(() => {
+ return makeTest(
+ () => {
+ // Ensure that the harness error is the result of explicit error
+ // handling
+ setup({ allow_uncaught_exception: true });
+
+ test(() => {}, 'before');
+ promise_setup(() => undefined);
+ promise_test(() => Promise.resolve(), 'after');
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'ERROR');
+ assert_equals(tests.before, 'PASS');
+ assert_equals(tests.after, 'NOTRUN');
+ });
+}, 'Error for missing return value');
+
+promise_test(() => {
+ return makeTest(
+ () => {
+ // Ensure that the harness error is the result of explicit error
+ // handling
+ setup({ allow_uncaught_exception: true });
+
+ test(() => {}, 'before');
+ var noThen = Promise.resolve();
+ noThen.then = undefined;
+ promise_setup(() => noThen);
+ promise_test(() => Promise.resolve(), 'after');
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'ERROR');
+ assert_equals(tests.before, 'PASS');
+ assert_equals(tests.after, 'NOTRUN');
+ });
+}, 'Error for non-thenable return value');
+
+promise_test(() => {
+ return makeTest(
+ () => {
+ // Ensure that the harness error is the result of explicit error
+ // handling
+ setup({ allow_uncaught_exception: true });
+
+ test(() => {}, 'before');
+ var poisonedThen = {
+ get then() {
+ throw new Error('this error is expected');
+ }
+ };
+ promise_setup(() => poisonedThen);
+ promise_test(() => Promise.resolve(), 'after');
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'ERROR');
+ assert_equals(tests.before, 'PASS');
+ assert_equals(tests.after, 'NOTRUN');
+ });
+}, 'Error for "poisoned" `then` property');
+
+promise_test(() => {
+ return makeTest(
+ () => {
+ // Ensure that the harness error is the result of explicit error
+ // handling
+ setup({ allow_uncaught_exception: true });
+
+ test(() => {}, 'before');
+ var badThen = {
+ then() {
+ throw new Error('this error is expected');
+ }
+ };
+ promise_setup(() => badThen);
+ promise_test(() => Promise.resolve(), 'after');
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'ERROR');
+ assert_equals(tests.before, 'PASS');
+ assert_equals(tests.after, 'NOTRUN');
+ });
+}, 'Error for synchronous error from `then` method');
+
+promise_test(() => {
+ return makeTest(
+ () => {
+ // Ensure that the harness error is the result of explicit error
+ // handling
+ setup({ allow_uncaught_exception: true });
+
+ test(() => {}, 'before');
+ promise_setup(() => Promise.resolve());
+ test(() => {}, 'after');
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'ERROR');
+ assert_equals(tests.before, 'PASS');
+ assert_equals(tests.after, undefined);
+ });
+}, 'Error for subsequent invocation of `test`');
+
+promise_test(() => {
+ return makeTest(
+ () => {
+ // Ensure that the harness error is the result of explicit error
+ // handling
+ setup({ allow_uncaught_exception: true });
+
+ test(() => {}, 'before');
+ promise_setup(() => Promise.resolve());
+ async_test((t) => t.done(), 'after');
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'ERROR');
+ assert_equals(tests.before, 'PASS');
+ assert_equals(tests.after, undefined);
+ });
+}, 'Error for subsequent invocation of `async_test`');
+
+promise_test(() => {
+ return makeTest(
+ () => {
+ // Ensure that the harness error is the result of explicit error
+ // handling
+ setup({ allow_uncaught_exception: true });
+
+ test(() => {}, 'before');
+ promise_setup(() => Promise.reject());
+ promise_test(() => Promise.resolve(), 'after');
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'ERROR');
+ assert_equals(tests.before, 'PASS');
+ assert_equals(tests.after, 'NOTRUN');
+ });
+}, 'Error for rejected promise');
+
+promise_test(() => {
+ var expected_sequence = [
+ 'test body',
+ 'promise_setup begin',
+ 'promise_setup end',
+ 'promise_test body'
+ ];
+ var actual_sequence = window.actual_sequence = [];
+
+ return makeTest(
+ () => {
+ test(() => { parent.actual_sequence.push('test body'); }, 'before');
+ promise_setup(() => {
+ parent.actual_sequence.push('promise_setup begin');
+
+ return Promise.resolve()
+ .then(() => new Promise((resolve) => setTimeout(resolve, 300)))
+ .then(() => parent.actual_sequence.push('promise_setup end'));
+ });
+ promise_test(() => {
+ parent.actual_sequence.push('promise_test body');
+ return Promise.resolve();
+ }, 'after');
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'OK');
+ assert_equals(tests.before, 'PASS');
+ assert_equals(tests.after, 'PASS');
+ assert_array_equals(actual_sequence, expected_sequence);
+ });
+}, 'Waits for promise to settle');
+
+promise_test(() => {
+ var expected_sequence = [
+ 'promise_test 1 begin',
+ 'promise_test 1 end',
+ 'promise_setup begin',
+ 'promise_setup end',
+ 'promise_test 2 body'
+ ];
+ var actual_sequence = window.actual_sequence = [];
+
+ return makeTest(
+ () => {
+ promise_test((t) => {
+ parent.actual_sequence.push('promise_test 1 begin');
+
+ return Promise.resolve()
+ .then(() => new Promise((resolve) => t.step_timeout(resolve, 300)))
+ .then(() => parent.actual_sequence.push('promise_test 1 end'));
+ }, 'before');
+ promise_setup(() => {
+ parent.actual_sequence.push('promise_setup begin');
+
+ return Promise.resolve()
+ .then(() => new Promise((resolve) => setTimeout(resolve, 300)))
+ .then(() => parent.actual_sequence.push('promise_setup end'));
+ });
+ promise_test(() => {
+ parent.actual_sequence.push('promise_test 2 body');
+ return Promise.resolve();
+ }, 'after');
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'OK');
+ assert_equals(tests.before, 'PASS');
+ assert_equals(tests.after, 'PASS');
+ assert_array_equals(actual_sequence, expected_sequence);
+ });
+}, 'Waits for existing promise_test to complete');
+
+promise_test(() => {
+ return makeTest(
+ () => {
+ var properties = { allow_uncaught_exception: true };
+ promise_test(() => {
+ return new Promise((resolve) => {
+ setTimeout(() => {
+ resolve();
+ throw new Error('this error is expected');
+ });
+ });
+ }, 'before');
+ promise_setup(() => Promise.resolve(), properties);
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'ERROR');
+ assert_equals(tests.before, 'PASS');
+ });
+}, 'Defers application of setup properties');
+</script>
+</body>
+</html>
diff --git a/test/wpt/tests/resources/test/tests/unit/single_test.html b/test/wpt/tests/resources/test/tests/unit/single_test.html
new file mode 100644
index 0000000..ff766e6
--- /dev/null
+++ b/test/wpt/tests/resources/test/tests/unit/single_test.html
@@ -0,0 +1,94 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="timeout" content="long">
+ <script src="/resources/testharness.js"></script>
+ <script src="../../nested-testharness.js"></script>
+ <title>single_test</title>
+</head>
+<body>
+<script>
+promise_test(() => {
+ return makeTest(
+ () => {
+ setup({ single_test: true });
+ done();
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'OK');
+ assert_equals(tests['Document title'], 'PASS');
+ });
+}, 'Expected usage');
+
+promise_test(() => {
+ return makeTest(
+ () => {
+ setup({ single_test: true });
+ throw new Error('this error is expected');
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'OK');
+ assert_equals(tests['Document title'], 'FAIL');
+ });
+}, 'Uncaught exception');
+
+promise_test(() => {
+ return makeTest(
+ () => {
+ setup({ single_test: true });
+ Promise.reject(new Error('this error is expected'));
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'OK');
+ assert_equals(tests['Document title'], 'FAIL');
+ });
+}, 'Unhandled rejection');
+
+promise_test(() => {
+ return makeTest(
+ () => {
+ setup({ single_test: true });
+ test(function() {}, 'sync test');
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'OK');
+ assert_equals(tests['Document title'], 'FAIL');
+ assert_equals(
+ Object.keys(tests).length, 1, 'no additional subtests created'
+ );
+ });
+}, 'Erroneous usage: subtest declaration (synchronous test)');
+
+promise_test(() => {
+ return makeTest(
+ () => {
+ setup({ single_test: true });
+ async_test(function(t) { t.done(); }, 'async test');
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'OK');
+ assert_equals(tests['Document title'], 'FAIL');
+ assert_equals(
+ Object.keys(tests).length, 1, 'no additional subtests created'
+ );
+ });
+}, 'Erroneous usage: subtest declaration (asynchronous test)');
+
+promise_test(() => {
+ return makeTest(
+ () => {
+ setup({ single_test: true });
+ promise_test(function() { return Promise.resolve(); }, 'promise test');
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'OK');
+ assert_equals(tests['Document title'], 'FAIL');
+ assert_equals(
+ Object.keys(tests).length, 1, 'no additional subtests created'
+ );
+ });
+}, 'Erroneous usage: subtest declaration (promise test)');
+</script>
+</body>
+</html>
diff --git a/test/wpt/tests/resources/test/tests/unit/test-return-restrictions.html b/test/wpt/tests/resources/test/tests/unit/test-return-restrictions.html
new file mode 100644
index 0000000..0295c52
--- /dev/null
+++ b/test/wpt/tests/resources/test/tests/unit/test-return-restrictions.html
@@ -0,0 +1,156 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <script src="/resources/testharness.js"></script>
+ <title>Restrictions on return value from `test`</title>
+</head>
+<body>
+<script>
+function makeTest(...bodies) {
+ const closeScript = '<' + '/script>';
+ let src = `
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Document title</title>
+<script src="/resources/testharness.js?${Math.random()}">${closeScript}
+</head>
+
+<body>
+<div id="log"></div>`;
+ bodies.forEach((body) => {
+ src += '<script>(' + body + ')();' + closeScript;
+ });
+
+ const iframe = document.createElement('iframe');
+
+ document.body.appendChild(iframe);
+ iframe.contentDocument.write(src);
+
+ return new Promise((resolve) => {
+ window.addEventListener('message', function onMessage(e) {
+ if (e.source !== iframe.contentWindow) {
+ return;
+ }
+ if (!e.data || e.data.type !=='complete') {
+ return;
+ }
+ window.removeEventListener('message', onMessage);
+ resolve(e.data);
+ });
+
+ iframe.contentDocument.close();
+ }).then(({ tests, status }) => {
+ const summary = {
+ harness: {
+ status: getEnumProp(status, status.status),
+ message: status.message
+ },
+ tests: {}
+ };
+
+ tests.forEach((test) => {
+ summary.tests[test.name] = getEnumProp(test, test.status);
+ });
+
+ return summary;
+ });
+}
+
+function getEnumProp(object, value) {
+ for (let property in object) {
+ if (!/^[A-Z]+$/.test(property)) {
+ continue;
+ }
+
+ if (object[property] === value) {
+ return property;
+ }
+ }
+}
+
+promise_test(() => {
+ return makeTest(
+ () => {
+ test(() => undefined, 'before');
+ test(() => null, 'null');
+ test(() => undefined, 'after');
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness.status, 'ERROR');
+ assert_equals(
+ harness.message,
+ 'Test named "null" passed a function to `test` that returned a value.'
+ );
+ assert_equals(tests.before, 'PASS');
+ assert_equals(tests.null, 'PASS');
+ assert_equals(tests.after, 'PASS');
+ });
+}, 'test returning `null`');
+
+promise_test(() => {
+ return makeTest(
+ () => {
+ test(() => undefined, 'before');
+ test(() => ({}), 'object');
+ test(() => undefined, 'after');
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness.status, 'ERROR');
+ assert_equals(
+ harness.message,
+ 'Test named "object" passed a function to `test` that returned a value.'
+ );
+ assert_equals(tests.before, 'PASS');
+ assert_equals(tests.object, 'PASS');
+ assert_equals(tests.after, 'PASS');
+ });
+}, 'test returning an ordinary object');
+
+promise_test(() => {
+ return makeTest(
+ () => {
+ test(() => undefined, 'before');
+ test(() => Promise.resolve(5), 'thenable');
+ test(() => undefined, 'after');
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness.status, 'ERROR');
+ assert_equals(
+ harness.message,
+ 'Test named "thenable" passed a function to `test` that returned a value. ' +
+ 'Consider using `promise_test` instead when using Promises or async/await.'
+ );
+ assert_equals(tests.before, 'PASS');
+ assert_equals(tests.thenable, 'PASS');
+ assert_equals(tests.after, 'PASS');
+ });
+}, 'test returning a thenable object');
+
+promise_test(() => {
+ return makeTest(
+ () => {
+ test(() => undefined, 'before');
+ test(() => {
+ const iframe = document.createElement('iframe');
+ iframe.setAttribute('sandbox', '');
+ document.body.appendChild(iframe);
+ return iframe.contentWindow;
+ }, 'restricted');
+ test(() => undefined, 'after');
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness.status, 'ERROR');
+ assert_equals(
+ harness.message,
+ 'Test named "restricted" passed a function to `test` that returned a value.'
+ );
+ assert_equals(tests.before, 'PASS');
+ assert_equals(tests.restricted, 'PASS');
+ assert_equals(tests.after, 'PASS');
+ });
+}, 'test returning a restricted object');
+</script>
+</body>
+</html>
diff --git a/test/wpt/tests/resources/test/tests/unit/throwing-assertions.html b/test/wpt/tests/resources/test/tests/unit/throwing-assertions.html
new file mode 100644
index 0000000..a36a560
--- /dev/null
+++ b/test/wpt/tests/resources/test/tests/unit/throwing-assertions.html
@@ -0,0 +1,268 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="timeout" content="long">
+ <script src="/resources/testharness.js"></script>
+ <title>Test the methods that make assertions about exceptions</title>
+</head>
+<body>
+<script>
+function makeTest(...bodies) {
+ const closeScript = '<' + '/script>';
+ let src = `
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Document title</title>
+<script src="/resources/testharness.js?${Math.random()}">${closeScript}
+</head>
+
+<body>
+<div id="log"></div>`;
+ bodies.forEach((body) => {
+ src += '<script>(' + body + ')();' + closeScript;
+ });
+
+ const iframe = document.createElement('iframe');
+
+ document.body.appendChild(iframe);
+ iframe.contentDocument.write(src);
+
+ return new Promise((resolve) => {
+ window.addEventListener('message', function onMessage(e) {
+ if (e.source !== iframe.contentWindow) {
+ return;
+ }
+ if (!e.data || e.data.type !=='complete') {
+ return;
+ }
+ window.removeEventListener('message', onMessage);
+ resolve(e.data);
+ });
+
+ iframe.contentDocument.close();
+ }).then(({ tests, status }) => {
+ const summary = {
+ harness: getEnumProp(status, status.status),
+ tests: {}
+ };
+
+ tests.forEach((test) => {
+ summary.tests[test.name] = getEnumProp(test, test.status);
+ });
+
+ return summary;
+ });
+}
+
+function getEnumProp(object, value) {
+ for (let property in object) {
+ if (!/^[A-Z]+$/.test(property)) {
+ continue;
+ }
+
+ if (object[property] === value) {
+ return property;
+ }
+ }
+}
+
+promise_test(() => {
+ return makeTest(() => {
+ test(() => {
+ assert_throws_js(TypeError, () => { throw new TypeError(); });
+ });
+ }).then(({harness, tests}) => {
+ assert_equals(harness, 'OK');
+ assert_equals(tests['Document title'], 'PASS');
+ });
+}, 'assert_throws_js on a TypeError');
+
+promise_test(() => {
+ return makeTest(() => {
+ test(() => {
+ assert_throws_js(RangeError, () => { throw new RangeError(); });
+ });
+ }).then(({harness, tests}) => {
+ assert_equals(harness, 'OK');
+ assert_equals(tests['Document title'], 'PASS');
+ });
+}, 'assert_throws_js on a RangeError');
+
+promise_test(() => {
+ return makeTest(() => {
+ test(() => {
+ assert_throws_js(TypeError, () => { throw new RangeError(); });
+ });
+ }).then(({harness, tests}) => {
+ assert_equals(harness, 'OK');
+ assert_equals(tests['Document title'], 'FAIL');
+ });
+}, 'assert_throws_js on a TypeError when RangeError is thrown');
+
+promise_test(() => {
+ return makeTest(() => {
+ test(() => {
+ assert_throws_js(Error, () => { throw new TypeError(); });
+ });
+ }).then(({harness, tests}) => {
+ assert_equals(harness, 'OK');
+ assert_equals(tests['Document title'], 'FAIL');
+ });
+}, 'assert_throws_js on an Error when TypeError is thrown');
+
+promise_test(() => {
+ return makeTest(() => {
+ test(() => {
+ assert_throws_js(Error,
+ () => { throw new DOMException("hello", "Error"); });
+ });
+ }).then(({harness, tests}) => {
+ assert_equals(harness, 'OK');
+ assert_equals(tests['Document title'], 'FAIL');
+ });
+}, 'assert_throws_js on an Error when a DOMException is thrown');
+
+promise_test(() => {
+ return makeTest(() => {
+ test(() => {
+ assert_throws_js(SyntaxError,
+ () => { throw new DOMException("hey", "SyntaxError"); });
+ });
+ }).then(({harness, tests}) => {
+ assert_equals(harness, 'OK');
+ assert_equals(tests['Document title'], 'FAIL');
+ });
+}, 'assert_throws_js on a SyntaxError when a DOMException is thrown');
+
+promise_test(() => {
+ return makeTest(() => {
+ test(() => {
+ assert_throws_dom("SyntaxError",
+ () => { throw new DOMException("x", "SyntaxError"); });
+ });
+ }).then(({harness, tests}) => {
+ assert_equals(harness, 'OK');
+ assert_equals(tests['Document title'], 'PASS');
+ });
+}, 'assert_throws_dom basic sanity');
+
+promise_test(() => {
+ return makeTest(() => {
+ test(() => {
+ assert_throws_dom(12,
+ () => { throw new DOMException("x", "SyntaxError"); });
+ });
+ }).then(({harness, tests}) => {
+ assert_equals(harness, 'OK');
+ assert_equals(tests['Document title'], 'PASS');
+ });
+}, 'assert_throws_dom with numeric code');
+
+promise_test(() => {
+ return makeTest(() => {
+ test(() => {
+ assert_throws_dom("SYNTAX_ERR",
+ () => { throw new DOMException("x", "SyntaxError"); });
+ });
+ }).then(({harness, tests}) => {
+ assert_equals(harness, 'OK');
+ assert_equals(tests['Document title'], 'PASS');
+ });
+}, 'assert_throws_dom with string name for code');
+
+promise_test(() => {
+ return makeTest(() => {
+ test(() => {
+ assert_throws_dom("DataError",
+ () => { throw new DOMException("x", "DataError"); });
+ });
+ }).then(({harness, tests}) => {
+ assert_equals(harness, 'OK');
+ assert_equals(tests['Document title'], 'PASS');
+ });
+}, 'assert_throws_dom for a code-less DOMException type');
+
+promise_test(() => {
+ return makeTest(() => {
+ test(() => {
+ assert_throws_dom("NoSuchError",
+ () => { throw new DOMException("x", "NoSuchError"); });
+ });
+ }).then(({harness, tests}) => {
+ assert_equals(harness, 'OK');
+ assert_equals(tests['Document title'], 'FAIL');
+ });
+}, 'assert_throws_dom for a nonexistent DOMException type');
+
+promise_test(() => {
+ return makeTest(() => {
+ test(() => {
+ assert_throws_dom("SyntaxError", () => { throw new SyntaxError(); });
+ });
+ }).then(({harness, tests}) => {
+ assert_equals(harness, 'OK');
+ assert_equals(tests['Document title'], 'FAIL');
+ });
+}, 'assert_throws_dom when a non-DOM exception is thrown');
+
+promise_test(() => {
+ return makeTest(() => {
+ test(() => {
+ assert_throws_exactly(5, () => { throw 5; });
+ });
+ }).then(({harness, tests}) => {
+ assert_equals(harness, 'OK');
+ assert_equals(tests['Document title'], 'PASS');
+ });
+}, 'assert_throws_exactly with number');
+
+promise_test(() => {
+ return makeTest(() => {
+ test(() => {
+ assert_throws_exactly("foo", () => { throw "foo"; });
+ });
+ }).then(({harness, tests}) => {
+ assert_equals(harness, 'OK');
+ assert_equals(tests['Document title'], 'PASS');
+ });
+}, 'assert_throws_exactly with string');
+
+promise_test(() => {
+ return makeTest(() => {
+ test(() => {
+ assert_throws_exactly({}, () => { throw {}; });
+ });
+ }).then(({harness, tests}) => {
+ assert_equals(harness, 'OK');
+ assert_equals(tests['Document title'], 'FAIL');
+ });
+}, 'assert_throws_exactly with different objects');
+
+promise_test(() => {
+ return makeTest(() => {
+ test(() => {
+ var obj = {};
+ assert_throws_exactly(obj, () => { throw obj; });
+ });
+ }).then(({harness, tests}) => {
+ assert_equals(harness, 'OK');
+ assert_equals(tests['Document title'], 'PASS');
+ });
+}, 'assert_throws_exactly with same object');
+
+promise_test(() => {
+ return makeTest(() => {
+ test(() => {
+ assert_throws_exactly(TypeError, () => { throw new TypeError; });
+ });
+ }).then(({harness, tests}) => {
+ assert_equals(harness, 'OK');
+ assert_equals(tests['Document title'], 'FAIL');
+ });
+}, 'assert_throws_exactly with bogus TypeError bits ');
+
+</script>
+</body>
+</html>
diff --git a/test/wpt/tests/resources/test/tests/unit/unpaired-surrogates.html b/test/wpt/tests/resources/test/tests/unit/unpaired-surrogates.html
new file mode 100644
index 0000000..b232111
--- /dev/null
+++ b/test/wpt/tests/resources/test/tests/unit/unpaired-surrogates.html
@@ -0,0 +1,143 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <script src="/resources/testharness.js"></script>
+ <title>Restrictions on return value from `test`</title>
+</head>
+<body>
+<script>
+function makeTest(...bodies) {
+ const closeScript = '<' + '/script>';
+ let src = `
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Document title</title>
+<script src="/resources/testharness.js?${Math.random()}">${closeScript}
+</head>
+
+<body>
+<div id="log"></div>`;
+ bodies.forEach((body) => {
+ src += '<script>(' + body + ')();' + closeScript;
+ });
+
+ const iframe = document.createElement('iframe');
+
+ document.body.appendChild(iframe);
+ iframe.contentDocument.write(src);
+
+ return new Promise((resolve) => {
+ window.addEventListener('message', function onMessage(e) {
+ if (e.source !== iframe.contentWindow) {
+ return;
+ }
+ if (!e.data || e.data.type !=='complete') {
+ return;
+ }
+ window.removeEventListener('message', onMessage);
+ resolve(e.data);
+ });
+
+ iframe.contentDocument.close();
+ }).then(({ tests, status }) => {
+ const summary = {
+ harness: {
+ status: getEnumProp(status, status.status),
+ message: status.message
+ },
+ tests: {}
+ };
+
+ tests.forEach((test) => {
+ summary.tests[test.name] = getEnumProp(test, test.status);
+ });
+
+ return summary;
+ });
+}
+
+function getEnumProp(object, value) {
+ for (let property in object) {
+ if (!/^[A-Z]+$/.test(property)) {
+ continue;
+ }
+
+ if (object[property] === value) {
+ return property;
+ }
+ }
+}
+
+promise_test(() => {
+ return makeTest(
+ () => {
+ test(() => {}, 'before');
+ test(() => {}, 'U+d7ff is not modified: \ud7ff');
+ test(() => {}, 'U+e000 is not modified: \ue000');
+ test(() => {}, 'after');
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness.status, 'OK');
+ assert_equals(harness.message, null);
+ assert_equals(tests.before, 'PASS');
+ assert_equals(tests['U+d7ff is not modified: \ud7ff'], 'PASS');
+ assert_equals(tests['U+e000 is not modified: \ue000'], 'PASS');
+ assert_equals(tests.after, 'PASS');
+ });
+}, 'sub-test names which include valid code units');
+
+promise_test(() => {
+ return makeTest(
+ () => {
+ test(() => {}, 'before');
+ test(() => {}, 'U+d800U+dfff is not modified: \ud800\udfff');
+ test(() => {}, 'U+dbffU+dc00 is not modified: \udbff\udc00');
+ test(() => {}, 'after');
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness.status, 'OK');
+ assert_equals(harness.message, null);
+ assert_equals(tests.before, 'PASS');
+ assert_equals(tests['U+d800U+dfff is not modified: \ud800\udfff'], 'PASS');
+ assert_equals(tests['U+dbffU+dc00 is not modified: \udbff\udc00'], 'PASS');
+ assert_equals(tests.after, 'PASS');
+ });
+}, 'sub-test names which include paired surrogates');
+
+promise_test(() => {
+ return makeTest(
+ () => {
+ test(() => {}, 'before');
+ test(() => {}, 'U+d800 must be sanitized: \ud800');
+ test(() => {}, 'U+d800U+d801 must be sanitized: \ud800\ud801');
+ test(() => {}, 'U+dfff must be sanitized: \udfff');
+ test(() => {}, 'U+dc00U+d800U+dc00U+d800 must be sanitized: \udc00\ud800\udc00\ud800');
+ test(() => {}, 'U+dbffU+dfffU+dfff must be sanitized: \udbff\udfff\udfff');
+ test(() => {}, 'after');
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness.status, 'OK');
+ assert_equals(harness.message, null);
+ assert_equals(tests.before, 'PASS');
+ assert_equals(tests['U+d800 must be sanitized: U+d800'], 'PASS');
+ assert_equals(tests['U+dfff must be sanitized: U+dfff'], 'PASS');
+ assert_equals(
+ tests['U+d800U+d801 must be sanitized: U+d800U+d801'],
+ 'PASS'
+ );
+ assert_equals(
+ tests['U+dc00U+d800U+dc00U+d800 must be sanitized: U+dc00\ud800\udc00U+d800'],
+ 'PASS'
+ );
+ assert_equals(
+ tests['U+dbffU+dfffU+dfff must be sanitized: \udbff\udfffU+dfff'],
+ 'PASS'
+ );
+ assert_equals(tests.after, 'PASS');
+ });
+}, 'sub-test names which include unpaired surrogates');
+</script>
+</body>
+</html>
diff --git a/test/wpt/tests/resources/test/tox.ini b/test/wpt/tests/resources/test/tox.ini
new file mode 100644
index 0000000..12013a1
--- /dev/null
+++ b/test/wpt/tests/resources/test/tox.ini
@@ -0,0 +1,13 @@
+[tox]
+envlist = py37,py38,py39,py310,py311
+skipsdist=True
+
+[testenv]
+passenv=DISPLAY # Necessary for the spawned GeckoDriver process to connect to
+ # the appropriate display.
+
+deps =
+ -r{toxinidir}/../../tools/requirements_pytest.txt
+ -r{toxinidir}/requirements.txt
+
+commands = pytest -vv {posargs}
diff --git a/test/wpt/tests/resources/test/wptserver.py b/test/wpt/tests/resources/test/wptserver.py
new file mode 100644
index 0000000..1f913dd
--- /dev/null
+++ b/test/wpt/tests/resources/test/wptserver.py
@@ -0,0 +1,58 @@
+import logging
+import os
+import subprocess
+import time
+import sys
+import urllib
+
+
+class WPTServer(object):
+ def __init__(self, wpt_root):
+ self.logger = logging.getLogger()
+ self.wpt_root = wpt_root
+
+ # This is a terrible hack to get the default config of wptserve.
+ sys.path.insert(0, os.path.join(wpt_root, "tools"))
+ from serve.serve import build_config
+ with build_config(self.logger) as config:
+ self.host = config["browser_host"]
+ self.http_port = config["ports"]["http"][0]
+ self.https_port = config["ports"]["https"][0]
+
+ self.base_url = 'http://%s:%s' % (self.host, self.http_port)
+ self.https_base_url = 'https://%s:%s' % (self.host, self.https_port)
+
+ def start(self, ssl_context):
+ self.devnull = open(os.devnull, 'w')
+ wptserve_cmd = [os.path.join(self.wpt_root, 'wpt'), 'serve']
+ if sys.executable:
+ wptserve_cmd[0:0] = [sys.executable]
+ self.logger.info('Executing %s' % ' '.join(wptserve_cmd))
+ self.proc = subprocess.Popen(
+ wptserve_cmd,
+ stderr=self.devnull,
+ cwd=self.wpt_root)
+
+ for retry in range(5):
+ # Exponential backoff.
+ time.sleep(2 ** retry)
+ exit_code = self.proc.poll()
+ if exit_code != None:
+ logging.warning('Command "%s" exited with %s', ' '.join(wptserve_cmd), exit_code)
+ break
+ try:
+ urllib.request.urlopen(self.base_url, timeout=1)
+ urllib.request.urlopen(self.https_base_url, timeout=1, context=ssl_context)
+ return
+ except urllib.error.URLError:
+ pass
+
+ raise Exception('Could not start wptserve on %s' % self.base_url)
+
+ def stop(self):
+ self.proc.terminate()
+ self.proc.wait()
+ self.devnull.close()
+
+ def url(self, abs_path):
+ return self.https_base_url + '/' + os.path.relpath(abs_path, self.wpt_root)
diff --git a/test/wpt/tests/resources/testdriver-actions.js b/test/wpt/tests/resources/testdriver-actions.js
new file mode 100644
index 0000000..3e5ba74
--- /dev/null
+++ b/test/wpt/tests/resources/testdriver-actions.js
@@ -0,0 +1,599 @@
+(function() {
+ let sourceNameIdx = 0;
+
+ /**
+ * @class
+ * Builder for creating a sequence of actions
+ *
+ *
+ * The actions are dispatched once
+ * :js:func:`test_driver.Actions.send` is called. This returns a
+ * promise which resolves once the actions are complete.
+ *
+ * The other methods on :js:class:`test_driver.Actions` object are
+ * used to build the sequence of actions that will be sent. These
+ * return the `Actions` object itself, so the actions sequence can
+ * be constructed by chaining method calls.
+ *
+ * Internally :js:func:`test_driver.Actions.send` invokes
+ * :js:func:`test_driver.action_sequence`.
+ *
+ * @example
+ * let text_box = document.getElementById("text");
+ *
+ * let actions = new test_driver.Actions()
+ * .pointerMove(0, 0, {origin: text_box})
+ * .pointerDown()
+ * .pointerUp()
+ * .addTick()
+ * .keyDown("p")
+ * .keyUp("p");
+ *
+ * await actions.send();
+ *
+ * @param {number} [defaultTickDuration] - The default duration of a
+ * tick. Be default this is set ot 16ms, which is one frame time
+ * based on 60Hz display.
+ */
+ function Actions(defaultTickDuration=16) {
+ this.sourceTypes = new Map([["key", KeySource],
+ ["pointer", PointerSource],
+ ["wheel", WheelSource],
+ ["none", GeneralSource]]);
+ this.sources = new Map();
+ this.sourceOrder = [];
+ for (let sourceType of this.sourceTypes.keys()) {
+ this.sources.set(sourceType, new Map());
+ }
+ this.currentSources = new Map();
+ for (let sourceType of this.sourceTypes.keys()) {
+ this.currentSources.set(sourceType, null);
+ }
+ this.createSource("none");
+ this.tickIdx = 0;
+ this.defaultTickDuration = defaultTickDuration;
+ this.context = null;
+ }
+
+ Actions.prototype = {
+ ButtonType: {
+ LEFT: 0,
+ MIDDLE: 1,
+ RIGHT: 2,
+ BACK: 3,
+ FORWARD: 4,
+ },
+
+ /**
+ * Generate the action sequence suitable for passing to
+ * test_driver.action_sequence
+ *
+ * @returns {Array} Array of WebDriver-compatible actions sequences
+ */
+ serialize: function() {
+ let actions = [];
+ for (let [sourceType, sourceName] of this.sourceOrder) {
+ let source = this.sources.get(sourceType).get(sourceName);
+ let serialized = source.serialize(this.tickIdx + 1, this.defaultTickDuration);
+ if (serialized) {
+ serialized.id = sourceName;
+ actions.push(serialized);
+ }
+ }
+ return actions;
+ },
+
+ /**
+ * Generate and send the action sequence
+ *
+ * @returns {Promise} fulfilled after the sequence is executed,
+ * rejected if any actions fail.
+ */
+ send: function() {
+ let actions;
+ try {
+ actions = this.serialize();
+ } catch(e) {
+ return Promise.reject(e);
+ }
+ return test_driver.action_sequence(actions, this.context);
+ },
+
+ /**
+ * Set the context for the actions
+ *
+ * @param {WindowProxy} context - Context in which to run the action sequence
+ */
+ setContext: function(context) {
+ this.context = context;
+ return this;
+ },
+
+ /**
+ * Get the action source with a particular source type and name.
+ * If no name is passed, a new source with the given type is
+ * created.
+ *
+ * @param {String} type - Source type ('none', 'key', 'pointer', or 'wheel')
+ * @param {String?} name - Name of the source
+ * @returns {Source} Source object for that source.
+ */
+ getSource: function(type, name) {
+ if (!this.sources.has(type)) {
+ throw new Error(`${type} is not a valid action type`);
+ }
+ if (name === null || name === undefined) {
+ name = this.currentSources.get(type);
+ }
+ if (name === null || name === undefined) {
+ return this.createSource(type, null);
+ }
+ return this.sources.get(type).get(name);
+ },
+
+ setSource: function(type, name) {
+ if (!this.sources.has(type)) {
+ throw new Error(`${type} is not a valid action type`);
+ }
+ if (!this.sources.get(type).has(name)) {
+ throw new Error(`${name} is not a valid source for ${type}`);
+ }
+ this.currentSources.set(type, name);
+ return this;
+ },
+
+ /**
+ * Add a new key input source with the given name
+ *
+ * @param {String} name - Name of the key source
+ * @param {Bool} set - Set source as the default key source
+ * @returns {Actions}
+ */
+ addKeyboard: function(name, set=true) {
+ this.createSource("key", name);
+ if (set) {
+ this.setKeyboard(name);
+ }
+ return this;
+ },
+
+ /**
+ * Set the current default key source
+ *
+ * @param {String} name - Name of the key source
+ * @returns {Actions}
+ */
+ setKeyboard: function(name) {
+ this.setSource("key", name);
+ return this;
+ },
+
+ /**
+ * Add a new pointer input source with the given name
+ *
+ * @param {String} type - Name of the pointer source
+ * @param {String} pointerType - Type of pointing device
+ * @param {Bool} set - Set source as the default pointer source
+ * @returns {Actions}
+ */
+ addPointer: function(name, pointerType="mouse", set=true) {
+ this.createSource("pointer", name, {pointerType: pointerType});
+ if (set) {
+ this.setPointer(name);
+ }
+ return this;
+ },
+
+ /**
+ * Set the current default pointer source
+ *
+ * @param {String} name - Name of the pointer source
+ * @returns {Actions}
+ */
+ setPointer: function(name) {
+ this.setSource("pointer", name);
+ return this;
+ },
+
+ /**
+ * Add a new wheel input source with the given name
+ *
+ * @param {String} type - Name of the wheel source
+ * @param {Bool} set - Set source as the default wheel source
+ * @returns {Actions}
+ */
+ addWheel: function(name, set=true) {
+ this.createSource("wheel", name);
+ if (set) {
+ this.setWheel(name);
+ }
+ return this;
+ },
+
+ /**
+ * Set the current default wheel source
+ *
+ * @param {String} name - Name of the wheel source
+ * @returns {Actions}
+ */
+ setWheel: function(name) {
+ this.setSource("wheel", name);
+ return this;
+ },
+
+ createSource: function(type, name, parameters={}) {
+ if (!this.sources.has(type)) {
+ throw new Error(`${type} is not a valid action type`);
+ }
+ let sourceNames = new Set();
+ for (let [_, name] of this.sourceOrder) {
+ sourceNames.add(name);
+ }
+ if (!name) {
+ do {
+ name = "" + sourceNameIdx++;
+ } while (sourceNames.has(name))
+ } else {
+ if (sourceNames.has(name)) {
+ throw new Error(`Alreay have a source of type ${type} named ${name}.`);
+ }
+ }
+ this.sources.get(type).set(name, new (this.sourceTypes.get(type))(parameters));
+ this.currentSources.set(type, name);
+ this.sourceOrder.push([type, name]);
+ return this.sources.get(type).get(name);
+ },
+
+ /**
+ * Insert a new actions tick
+ *
+ * @param {Number?} duration - Minimum length of the tick in ms.
+ * @returns {Actions}
+ */
+ addTick: function(duration) {
+ this.tickIdx += 1;
+ if (duration) {
+ this.pause(duration);
+ }
+ return this;
+ },
+
+ /**
+ * Add a pause to the current tick
+ *
+ * @param {Number?} duration - Minimum length of the tick in ms.
+ * @param {String} sourceType - source type
+ * @param {String?} sourceName - Named key, pointer or wheel source to use
+ * or null for the default key, pointer or
+ * wheel source
+ * @returns {Actions}
+ */
+ pause: function(duration=0, sourceType="none", {sourceName=null}={}) {
+ if (sourceType=="none")
+ this.getSource("none").addPause(this, duration);
+ else
+ this.getSource(sourceType, sourceName).addPause(this, duration);
+ return this;
+ },
+
+ /**
+ * Create a keyDown event for the current default key source
+ *
+ * @param {String} key - Key to press
+ * @param {String?} sourceName - Named key source to use or null for the default key source
+ * @returns {Actions}
+ */
+ keyDown: function(key, {sourceName=null}={}) {
+ let source = this.getSource("key", sourceName);
+ source.keyDown(this, key);
+ return this;
+ },
+
+ /**
+ * Create a keyDown event for the current default key source
+ *
+ * @param {String} key - Key to release
+ * @param {String?} sourceName - Named key source to use or null for the default key source
+ * @returns {Actions}
+ */
+ keyUp: function(key, {sourceName=null}={}) {
+ let source = this.getSource("key", sourceName);
+ source.keyUp(this, key);
+ return this;
+ },
+
+ /**
+ * Create a pointerDown event for the current default pointer source
+ *
+ * @param {String} button - Button to press
+ * @param {String?} sourceName - Named pointer source to use or null for the default
+ * pointer source
+ * @returns {Actions}
+ */
+ pointerDown: function({button=this.ButtonType.LEFT, sourceName=null,
+ width, height, pressure, tangentialPressure,
+ tiltX, tiltY, twist, altitudeAngle, azimuthAngle}={}) {
+ let source = this.getSource("pointer", sourceName);
+ source.pointerDown(this, button, width, height, pressure, tangentialPressure,
+ tiltX, tiltY, twist, altitudeAngle, azimuthAngle);
+ return this;
+ },
+
+ /**
+ * Create a pointerUp event for the current default pointer source
+ *
+ * @param {String} button - Button to release
+ * @param {String?} sourceName - Named pointer source to use or null for the default pointer
+ * source
+ * @returns {Actions}
+ */
+ pointerUp: function({button=this.ButtonType.LEFT, sourceName=null}={}) {
+ let source = this.getSource("pointer", sourceName);
+ source.pointerUp(this, button);
+ return this;
+ },
+
+ /**
+ * Create a move event for the current default pointer source
+ *
+ * @param {Number} x - Destination x coordinate
+ * @param {Number} y - Destination y coordinate
+ * @param {String|Element} origin - Origin of the coordinate system.
+ * Either "pointer", "viewport" or an Element
+ * @param {Number?} duration - Time in ms for the move
+ * @param {String?} sourceName - Named pointer source to use or null for the default pointer
+ * source
+ * @returns {Actions}
+ */
+ pointerMove: function(x, y,
+ {origin="viewport", duration, sourceName=null,
+ width, height, pressure, tangentialPressure,
+ tiltX, tiltY, twist, altitudeAngle, azimuthAngle}={}) {
+ let source = this.getSource("pointer", sourceName);
+ source.pointerMove(this, x, y, duration, origin, width, height, pressure,
+ tangentialPressure, tiltX, tiltY, twist, altitudeAngle,
+ azimuthAngle);
+ return this;
+ },
+
+ /**
+ * Create a scroll event for the current default wheel source
+ *
+ * @param {Number} x - mouse cursor x coordinate
+ * @param {Number} y - mouse cursor y coordinate
+ * @param {Number} deltaX - scroll delta value along the x-axis in pixels
+ * @param {Number} deltaY - scroll delta value along the y-axis in pixels
+ * @param {String|Element} origin - Origin of the coordinate system.
+ * Either "viewport" or an Element
+ * @param {Number?} duration - Time in ms for the scroll
+ * @param {String?} sourceName - Named wheel source to use or null for the
+ * default wheel source
+ * @returns {Actions}
+ */
+ scroll: function(x, y, deltaX, deltaY,
+ {origin="viewport", duration, sourceName=null}={}) {
+ let source = this.getSource("wheel", sourceName);
+ source.scroll(this, x, y, deltaX, deltaY, duration, origin);
+ return this;
+ },
+ };
+
+ function GeneralSource() {
+ this.actions = new Map();
+ }
+
+ GeneralSource.prototype = {
+ serialize: function(tickCount, defaultTickDuration) {
+ let actions = [];
+ let data = {"type": "none", "actions": actions};
+ for (let i=0; i<tickCount; i++) {
+ if (this.actions.has(i)) {
+ actions.push(this.actions.get(i));
+ } else {
+ actions.push({"type": "pause", duration: defaultTickDuration});
+ }
+ }
+ return data;
+ },
+
+ addPause: function(actions, duration) {
+ let tick = actions.tickIdx;
+ if (this.actions.has(tick)) {
+ throw new Error(`Already have a pause action for the current tick`);
+ }
+ this.actions.set(tick, {type: "pause", duration: duration});
+ },
+ };
+
+ function KeySource() {
+ this.actions = new Map();
+ }
+
+ KeySource.prototype = {
+ serialize: function(tickCount) {
+ if (!this.actions.size) {
+ return undefined;
+ }
+ let actions = [];
+ let data = {"type": "key", "actions": actions};
+ for (let i=0; i<tickCount; i++) {
+ if (this.actions.has(i)) {
+ actions.push(this.actions.get(i));
+ } else {
+ actions.push({"type": "pause"});
+ }
+ }
+ return data;
+ },
+
+ keyDown: function(actions, key) {
+ let tick = actions.tickIdx;
+ if (this.actions.has(tick)) {
+ tick = actions.addTick().tickIdx;
+ }
+ this.actions.set(tick, {type: "keyDown", value: key});
+ },
+
+ keyUp: function(actions, key) {
+ let tick = actions.tickIdx;
+ if (this.actions.has(tick)) {
+ tick = actions.addTick().tickIdx;
+ }
+ this.actions.set(tick, {type: "keyUp", value: key});
+ },
+
+ addPause: function(actions, duration) {
+ let tick = actions.tickIdx;
+ if (this.actions.has(tick)) {
+ tick = actions.addTick().tickIdx;
+ }
+ this.actions.set(tick, {type: "pause", duration: duration});
+ },
+ };
+
+ function PointerSource(parameters={pointerType: "mouse"}) {
+ let pointerType = parameters.pointerType || "mouse";
+ if (!["mouse", "pen", "touch"].includes(pointerType)) {
+ throw new Error(`Invalid pointerType ${pointerType}`);
+ }
+ this.type = pointerType;
+ this.actions = new Map();
+ }
+
+ function setPointerProperties(action, width, height, pressure, tangentialPressure,
+ tiltX, tiltY, twist, altitudeAngle, azimuthAngle) {
+ if (width) {
+ action.width = width;
+ }
+ if (height) {
+ action.height = height;
+ }
+ if (pressure) {
+ action.pressure = pressure;
+ }
+ if (tangentialPressure) {
+ action.tangentialPressure = tangentialPressure;
+ }
+ if (tiltX) {
+ action.tiltX = tiltX;
+ }
+ if (tiltY) {
+ action.tiltY = tiltY;
+ }
+ if (twist) {
+ action.twist = twist;
+ }
+ if (altitudeAngle) {
+ action.altitudeAngle = altitudeAngle;
+ }
+ if (azimuthAngle) {
+ action.azimuthAngle = azimuthAngle;
+ }
+ return action;
+ }
+
+ PointerSource.prototype = {
+ serialize: function(tickCount) {
+ if (!this.actions.size) {
+ return undefined;
+ }
+ let actions = [];
+ let data = {"type": "pointer", "actions": actions, "parameters": {"pointerType": this.type}};
+ for (let i=0; i<tickCount; i++) {
+ if (this.actions.has(i)) {
+ actions.push(this.actions.get(i));
+ } else {
+ actions.push({"type": "pause"});
+ }
+ }
+ return data;
+ },
+
+ pointerDown: function(actions, button, width, height, pressure, tangentialPressure,
+ tiltX, tiltY, twist, altitudeAngle, azimuthAngle) {
+ let tick = actions.tickIdx;
+ if (this.actions.has(tick)) {
+ tick = actions.addTick().tickIdx;
+ }
+ let actionProperties = setPointerProperties({type: "pointerDown", button}, width, height,
+ pressure, tangentialPressure, tiltX, tiltY,
+ twist, altitudeAngle, azimuthAngle);
+ this.actions.set(tick, actionProperties);
+ },
+
+ pointerUp: function(actions, button) {
+ let tick = actions.tickIdx;
+ if (this.actions.has(tick)) {
+ tick = actions.addTick().tickIdx;
+ }
+ this.actions.set(tick, {type: "pointerUp", button});
+ },
+
+ pointerMove: function(actions, x, y, duration, origin, width, height, pressure,
+ tangentialPressure, tiltX, tiltY, twist, altitudeAngle, azimuthAngle) {
+ let tick = actions.tickIdx;
+ if (this.actions.has(tick)) {
+ tick = actions.addTick().tickIdx;
+ }
+ let moveAction = {type: "pointerMove", x, y, origin};
+ if (duration) {
+ moveAction.duration = duration;
+ }
+ let actionProperties = setPointerProperties(moveAction, width, height, pressure,
+ tangentialPressure, tiltX, tiltY, twist,
+ altitudeAngle, azimuthAngle);
+ this.actions.set(tick, actionProperties);
+ },
+
+ addPause: function(actions, duration) {
+ let tick = actions.tickIdx;
+ if (this.actions.has(tick)) {
+ tick = actions.addTick().tickIdx;
+ }
+ this.actions.set(tick, {type: "pause", duration: duration});
+ },
+ };
+
+ function WheelSource() {
+ this.actions = new Map();
+ }
+
+ WheelSource.prototype = {
+ serialize: function(tickCount) {
+ if (!this.actions.size) {
+ return undefined;
+ }
+ let actions = [];
+ let data = {"type": "wheel", "actions": actions};
+ for (let i=0; i<tickCount; i++) {
+ if (this.actions.has(i)) {
+ actions.push(this.actions.get(i));
+ } else {
+ actions.push({"type": "pause"});
+ }
+ }
+ return data;
+ },
+
+ scroll: function(actions, x, y, deltaX, deltaY, duration, origin) {
+ let tick = actions.tickIdx;
+ if (this.actions.has(tick)) {
+ tick = actions.addTick().tickIdx;
+ }
+ this.actions.set(tick, {type: "scroll", x, y, deltaX, deltaY, origin});
+ if (duration) {
+ this.actions.get(tick).duration = duration;
+ }
+ },
+
+ addPause: function(actions, duration) {
+ let tick = actions.tickIdx;
+ if (this.actions.has(tick)) {
+ tick = actions.addTick().tickIdx;
+ }
+ this.actions.set(tick, {type: "pause", duration: duration});
+ },
+ };
+
+ test_driver.Actions = Actions;
+})();
diff --git a/test/wpt/tests/resources/testdriver-vendor.js b/test/wpt/tests/resources/testdriver-vendor.js
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/wpt/tests/resources/testdriver-vendor.js
diff --git a/test/wpt/tests/resources/testdriver-vendor.js.headers b/test/wpt/tests/resources/testdriver-vendor.js.headers
new file mode 100644
index 0000000..5e8f640
--- /dev/null
+++ b/test/wpt/tests/resources/testdriver-vendor.js.headers
@@ -0,0 +1,2 @@
+Content-Type: text/javascript; charset=utf-8
+Cache-Control: max-age=3600
diff --git a/test/wpt/tests/resources/testdriver.js b/test/wpt/tests/resources/testdriver.js
new file mode 100644
index 0000000..a23d6ea
--- /dev/null
+++ b/test/wpt/tests/resources/testdriver.js
@@ -0,0 +1,958 @@
+(function() {
+ "use strict";
+ var idCounter = 0;
+ let testharness_context = null;
+
+ function getInViewCenterPoint(rect) {
+ var left = Math.max(0, rect.left);
+ var right = Math.min(window.innerWidth, rect.right);
+ var top = Math.max(0, rect.top);
+ var bottom = Math.min(window.innerHeight, rect.bottom);
+
+ var x = 0.5 * (left + right);
+ var y = 0.5 * (top + bottom);
+
+ return [x, y];
+ }
+
+ function getPointerInteractablePaintTree(element) {
+ let elementDocument = element.ownerDocument;
+ if (!elementDocument.contains(element)) {
+ return [];
+ }
+
+ var rectangles = element.getClientRects();
+
+ if (rectangles.length === 0) {
+ return [];
+ }
+
+ var centerPoint = getInViewCenterPoint(rectangles[0]);
+
+ if ("elementsFromPoint" in elementDocument) {
+ return elementDocument.elementsFromPoint(centerPoint[0], centerPoint[1]);
+ } else if ("msElementsFromPoint" in elementDocument) {
+ var rv = elementDocument.msElementsFromPoint(centerPoint[0], centerPoint[1]);
+ return Array.prototype.slice.call(rv ? rv : []);
+ } else {
+ throw new Error("document.elementsFromPoint unsupported");
+ }
+ }
+
+ function inView(element) {
+ var pointerInteractablePaintTree = getPointerInteractablePaintTree(element);
+ return pointerInteractablePaintTree.indexOf(element) !== -1;
+ }
+
+
+ /**
+ * @namespace {test_driver}
+ */
+ window.test_driver = {
+ /**
+ * Set the context in which testharness.js is loaded
+ *
+ * @param {WindowProxy} context - the window containing testharness.js
+ **/
+ set_test_context: function(context) {
+ if (window.test_driver_internal.set_test_context) {
+ window.test_driver_internal.set_test_context(context);
+ }
+ testharness_context = context;
+ },
+
+ /**
+ * postMessage to the context containing testharness.js
+ *
+ * @param {Object} msg - the data to POST
+ **/
+ message_test: function(msg) {
+ let target = testharness_context;
+ if (testharness_context === null) {
+ target = window;
+ }
+ target.postMessage(msg, "*");
+ },
+
+ /**
+ * Trigger user interaction in order to grant additional privileges to
+ * a provided function.
+ *
+ * See `Tracking user activation
+ * <https://html.spec.whatwg.org/multipage/interaction.html#tracking-user-activation>`_.
+ *
+ * @example
+ * var mediaElement = document.createElement('video');
+ *
+ * test_driver.bless('initiate media playback', function () {
+ * mediaElement.play();
+ * });
+ *
+ * @param {String} intent - a description of the action which must be
+ * triggered by user interaction
+ * @param {Function} action - code requiring escalated privileges
+ * @param {WindowProxy} context - Browsing context in which
+ * to run the call, or null for the current
+ * browsing context.
+ *
+ * @returns {Promise} fulfilled following user interaction and
+ * execution of the provided `action` function;
+ * rejected if interaction fails or the provided
+ * function throws an error
+ */
+ bless: function(intent, action, context=null) {
+ let contextDocument = context ? context.document : document;
+ var button = contextDocument.createElement("button");
+ button.innerHTML = "This test requires user interaction.<br />" +
+ "Please click here to allow " + intent + ".";
+ button.id = "wpt-test-driver-bless-" + (idCounter += 1);
+ const elem = contextDocument.body || contextDocument.documentElement;
+ elem.appendChild(button);
+
+ let wait_click = new Promise(resolve => button.addEventListener("click", resolve));
+
+ return test_driver.click(button)
+ .then(wait_click)
+ .then(function() {
+ button.remove();
+
+ if (typeof action === "function") {
+ return action();
+ }
+ return null;
+ });
+ },
+
+ /**
+ * Triggers a user-initiated click
+ *
+ * If ``element`` isn't inside the
+ * viewport, it will be scrolled into view before the click
+ * occurs.
+ *
+ * If ``element`` is from a different browsing context, the
+ * command will be run in that context.
+ *
+ * Matches the behaviour of the `Element Click
+ * <https://w3c.github.io/webdriver/#element-click>`_
+ * WebDriver command.
+ *
+ * **Note:** If the element to be clicked does not have a
+ * unique ID, the document must not have any DOM mutations
+ * made between the function being called and the promise
+ * settling.
+ *
+ * @param {Element} element - element to be clicked
+ * @returns {Promise} fulfilled after click occurs, or rejected in
+ * the cases the WebDriver command errors
+ */
+ click: function(element) {
+ if (!inView(element)) {
+ element.scrollIntoView({behavior: "instant",
+ block: "end",
+ inline: "nearest"});
+ }
+
+ var pointerInteractablePaintTree = getPointerInteractablePaintTree(element);
+ if (pointerInteractablePaintTree.length === 0 ||
+ !element.contains(pointerInteractablePaintTree[0])) {
+ return Promise.reject(new Error("element click intercepted error"));
+ }
+
+ var rect = element.getClientRects()[0];
+ var centerPoint = getInViewCenterPoint(rect);
+ return window.test_driver_internal.click(element,
+ {x: centerPoint[0],
+ y: centerPoint[1]});
+ },
+
+ /**
+ * Deletes all cookies.
+ *
+ * Matches the behaviour of the `Delete All Cookies
+ * <https://w3c.github.io/webdriver/#delete-all-cookies>`_
+ * WebDriver command.
+ *
+ * @param {WindowProxy} context - Browsing context in which
+ * to run the call, or null for the current
+ * browsing context.
+ *
+ * @returns {Promise} fulfilled after cookies are deleted, or rejected in
+ * the cases the WebDriver command errors
+ */
+ delete_all_cookies: function(context=null) {
+ return window.test_driver_internal.delete_all_cookies(context);
+ },
+
+ /**
+ * Get details for all cookies in the current context.
+ * See https://w3c.github.io/webdriver/#get-all-cookies
+ *
+ * @param {WindowProxy} context - Browsing context in which
+ * to run the call, or null for the current
+ * browsing context.
+ *
+ * @returns {Promise} Returns an array of cookies objects as defined in the spec:
+ * https://w3c.github.io/webdriver/#cookies
+ */
+ get_all_cookies: function(context=null) {
+ return window.test_driver_internal.get_all_cookies(context);
+ },
+
+ /**
+ * Get details for a cookie in the current context by name if it exists.
+ * See https://w3c.github.io/webdriver/#get-named-cookie
+ *
+ * @param {String} name - The name of the cookie to get.
+ * @param {WindowProxy} context - Browsing context in which
+ * to run the call, or null for the current
+ * browsing context.
+ *
+ * @returns {Promise} Returns the matching cookie as defined in the spec:
+ * https://w3c.github.io/webdriver/#cookies
+ * Rejected if no such cookie exists.
+ */
+ get_named_cookie: async function(name, context=null) {
+ let cookie = await window.test_driver_internal.get_named_cookie(name, context);
+ if (!cookie) {
+ throw new Error("no such cookie");
+ }
+ return cookie;
+ },
+
+ /**
+ * Get Computed Label for an element.
+ *
+ * This matches the behaviour of the
+ * `Get Computed Label
+ * <https://w3c.github.io/webdriver/#dfn-get-computed-label>`_
+ * WebDriver command.
+ *
+ * @param {Element} element
+ * @returns {Promise} fulfilled after the computed label is returned, or
+ * rejected in the cases the WebDriver command errors
+ */
+ get_computed_label: async function(element) {
+ let label = await window.test_driver_internal.get_computed_label(element);
+ return label;
+ },
+
+ /**
+ * Get Computed Role for an element.
+ *
+ * This matches the behaviour of the
+ * `Get Computed Label
+ * <https://w3c.github.io/webdriver/#dfn-get-computed-role>`_
+ * WebDriver command.
+ *
+ * @param {Element} element
+ * @returns {Promise} fulfilled after the computed role is returned, or
+ * rejected in the cases the WebDriver command errors
+ */
+ get_computed_role: async function(element) {
+ let role = await window.test_driver_internal.get_computed_role(element);
+ return role;
+ },
+
+ /**
+ * Send keys to an element.
+ *
+ * If ``element`` isn't inside the
+ * viewport, it will be scrolled into view before the click
+ * occurs.
+ *
+ * If ``element`` is from a different browsing context, the
+ * command will be run in that context.
+ *
+ * To send special keys, send the respective key's codepoint,
+ * as defined by `WebDriver
+ * <https://w3c.github.io/webdriver/#keyboard-actions>`_. For
+ * example, the "tab" key is represented as "``\uE004``".
+ *
+ * **Note:** these special-key codepoints are not necessarily
+ * what you would expect. For example, <kbd>Esc</kbd> is the
+ * invalid Unicode character ``\uE00C``, not the ``\u001B`` Escape
+ * character from ASCII.
+ *
+ * This matches the behaviour of the
+ * `Send Keys
+ * <https://w3c.github.io/webdriver/#element-send-keys>`_
+ * WebDriver command.
+ *
+ * **Note:** If the element to be clicked does not have a
+ * unique ID, the document must not have any DOM mutations
+ * made between the function being called and the promise
+ * settling.
+ *
+ * @param {Element} element - element to send keys to
+ * @param {String} keys - keys to send to the element
+ * @returns {Promise} fulfilled after keys are sent, or rejected in
+ * the cases the WebDriver command errors
+ */
+ send_keys: function(element, keys) {
+ if (!inView(element)) {
+ element.scrollIntoView({behavior: "instant",
+ block: "end",
+ inline: "nearest"});
+ }
+
+ return window.test_driver_internal.send_keys(element, keys);
+ },
+
+ /**
+ * Freeze the current page
+ *
+ * The freeze function transitions the page from the HIDDEN state to
+ * the FROZEN state as described in `Lifecycle API for Web Pages
+ * <https://github.com/WICG/page-lifecycle/blob/master/README.md>`_.
+ *
+ * @param {WindowProxy} context - Browsing context in which
+ * to run the call, or null for the current
+ * browsing context.
+ *
+ * @returns {Promise} fulfilled after the freeze request is sent, or rejected
+ * in case the WebDriver command errors
+ */
+ freeze: function(context=null) {
+ return window.test_driver_internal.freeze();
+ },
+
+ /**
+ * Minimizes the browser window.
+ *
+ * Matches the the behaviour of the `Minimize
+ * <https://www.w3.org/TR/webdriver/#minimize-window>`_
+ * WebDriver command
+ *
+ * @param {WindowProxy} context - Browsing context in which
+ * to run the call, or null for the current
+ * browsing context.
+ *
+ * @returns {Promise} fulfilled with the previous `WindowRect
+ * <https://www.w3.org/TR/webdriver/#dfn-windowrect-object>`_
+ * value, after the window is minimized.
+ */
+ minimize_window: function(context=null) {
+ return window.test_driver_internal.minimize_window(context);
+ },
+
+ /**
+ * Restore the window from minimized/maximized state to a given rect.
+ *
+ * Matches the behaviour of the `Set Window Rect
+ * <https://www.w3.org/TR/webdriver/#set-window-rect>`_
+ * WebDriver command
+ *
+ * @param {Object} rect - A `WindowRect
+ * <https://www.w3.org/TR/webdriver/#dfn-windowrect-object>`_
+ * @param {WindowProxy} context - Browsing context in which
+ * to run the call, or null for the current
+ * browsing context.
+ *
+ * @returns {Promise} fulfilled after the window is restored to the given rect.
+ */
+ set_window_rect: function(rect, context=null) {
+ return window.test_driver_internal.set_window_rect(rect, context);
+ },
+
+ /**
+ * Send a sequence of actions
+ *
+ * This function sends a sequence of actions to perform.
+ *
+ * Matches the behaviour of the `Actions
+ * <https://w3c.github.io/webdriver/#actions>`_ feature in
+ * WebDriver.
+ *
+ * Authors are encouraged to use the
+ * :js:class:`test_driver.Actions` builder rather than
+ * invoking this API directly.
+ *
+ * @param {Array} actions - an array of actions. The format is
+ * the same as the actions property
+ * of the `Perform Actions
+ * <https://w3c.github.io/webdriver/#perform-actions>`_
+ * WebDriver command. Each element is
+ * an object representing an input
+ * source and each input source
+ * itself has an actions property
+ * detailing the behaviour of that
+ * source at each timestep (or
+ * tick). Authors are not expected to
+ * construct the actions sequence by
+ * hand, but to use the builder api
+ * provided in testdriver-actions.js
+ * @param {WindowProxy} context - Browsing context in which
+ * to run the call, or null for the current
+ * browsing context.
+ *
+ * @returns {Promise} fulfilled after the actions are performed, or rejected in
+ * the cases the WebDriver command errors
+ */
+ action_sequence: function(actions, context=null) {
+ return window.test_driver_internal.action_sequence(actions, context);
+ },
+
+ /**
+ * Generates a test report on the current page
+ *
+ * The generate_test_report function generates a report (to be
+ * observed by ReportingObserver) for testing purposes.
+ *
+ * Matches the `Generate Test Report
+ * <https://w3c.github.io/reporting/#generate-test-report-command>`_
+ * WebDriver command.
+ *
+ * @param {WindowProxy} context - Browsing context in which
+ * to run the call, or null for the current
+ * browsing context.
+ *
+ * @returns {Promise} fulfilled after the report is generated, or
+ * rejected if the report generation fails
+ */
+ generate_test_report: function(message, context=null) {
+ return window.test_driver_internal.generate_test_report(message, context);
+ },
+
+ /**
+ * Sets the state of a permission
+ *
+ * This function simulates a user setting a permission into a
+ * particular state.
+ *
+ * Matches the `Set Permission
+ * <https://w3c.github.io/permissions/#set-permission-command>`_
+ * WebDriver command.
+ *
+ * @example
+ * await test_driver.set_permission({ name: "background-fetch" }, "denied");
+ * await test_driver.set_permission({ name: "push", userVisibleOnly: true }, "granted");
+ *
+ * @param {PermissionDescriptor} descriptor - a `PermissionDescriptor
+ * <https://w3c.github.io/permissions/#dom-permissiondescriptor>`_
+ * dictionary.
+ * @param {String} state - the state of the permission
+ * @param {WindowProxy} context - Browsing context in which
+ * to run the call, or null for the current
+ * browsing context.
+ * @returns {Promise} fulfilled after the permission is set, or rejected if setting the
+ * permission fails
+ */
+ set_permission: function(descriptor, state, context=null) {
+ let permission_params = {
+ descriptor,
+ state,
+ };
+ return window.test_driver_internal.set_permission(permission_params, context);
+ },
+
+ /**
+ * Creates a virtual authenticator
+ *
+ * This function creates a virtual authenticator for use with
+ * the U2F and WebAuthn APIs.
+ *
+ * Matches the `Add Virtual Authenticator
+ * <https://w3c.github.io/webauthn/#sctn-automation-add-virtual-authenticator>`_
+ * WebDriver command.
+ *
+ * @param {Object} config - an `Authenticator Configuration
+ * <https://w3c.github.io/webauthn/#authenticator-configuration>`_
+ * object
+ * @param {WindowProxy} context - Browsing context in which
+ * to run the call, or null for the current
+ * browsing context.
+ *
+ * @returns {Promise} fulfilled after the authenticator is added, or
+ * rejected in the cases the WebDriver command
+ * errors. Returns the ID of the authenticator
+ */
+ add_virtual_authenticator: function(config, context=null) {
+ return window.test_driver_internal.add_virtual_authenticator(config, context);
+ },
+
+ /**
+ * Removes a virtual authenticator
+ *
+ * This function removes a virtual authenticator that has been
+ * created by :js:func:`add_virtual_authenticator`.
+ *
+ * Matches the `Remove Virtual Authenticator
+ * <https://w3c.github.io/webauthn/#sctn-automation-remove-virtual-authenticator>`_
+ * WebDriver command.
+ *
+ * @param {String} authenticator_id - the ID of the authenticator to be
+ * removed.
+ * @param {WindowProxy} context - Browsing context in which
+ * to run the call, or null for the current
+ * browsing context.
+ *
+ * @returns {Promise} fulfilled after the authenticator is removed, or
+ * rejected in the cases the WebDriver command
+ * errors
+ */
+ remove_virtual_authenticator: function(authenticator_id, context=null) {
+ return window.test_driver_internal.remove_virtual_authenticator(authenticator_id, context);
+ },
+
+ /**
+ * Adds a credential to a virtual authenticator
+ *
+ * Matches the `Add Credential
+ * <https://w3c.github.io/webauthn/#sctn-automation-add-credential>`_
+ * WebDriver command.
+ *
+ * @param {String} authenticator_id - the ID of the authenticator
+ * @param {Object} credential - A `Credential Parameters
+ * <https://w3c.github.io/webauthn/#credential-parameters>`_
+ * object
+ * @param {WindowProxy} context - Browsing context in which
+ * to run the call, or null for the current
+ * browsing context.
+ *
+ * @returns {Promise} fulfilled after the credential is added, or
+ * rejected in the cases the WebDriver command
+ * errors
+ */
+ add_credential: function(authenticator_id, credential, context=null) {
+ return window.test_driver_internal.add_credential(authenticator_id, credential, context);
+ },
+
+ /**
+ * Gets all the credentials stored in an authenticator
+ *
+ * This function retrieves all the credentials (added via the U2F API,
+ * WebAuthn, or the add_credential function) stored in a virtual
+ * authenticator
+ *
+ * Matches the `Get Credentials
+ * <https://w3c.github.io/webauthn/#sctn-automation-get-credentials>`_
+ * WebDriver command.
+ *
+ * @param {String} authenticator_id - the ID of the authenticator
+ * @param {WindowProxy} context - Browsing context in which
+ * to run the call, or null for the current
+ * browsing context.
+ *
+ * @returns {Promise} fulfilled after the credentials are
+ * returned, or rejected in the cases the
+ * WebDriver command errors. Returns an
+ * array of `Credential Parameters
+ * <https://w3c.github.io/webauthn/#credential-parameters>`_
+ */
+ get_credentials: function(authenticator_id, context=null) {
+ return window.test_driver_internal.get_credentials(authenticator_id, context=null);
+ },
+
+ /**
+ * Remove a credential stored in an authenticator
+ *
+ * Matches the `Remove Credential
+ * <https://w3c.github.io/webauthn/#sctn-automation-remove-credential>`_
+ * WebDriver command.
+ *
+ * @param {String} authenticator_id - the ID of the authenticator
+ * @param {String} credential_id - the ID of the credential
+ * @param {WindowProxy} context - Browsing context in which
+ * to run the call, or null for the current
+ * browsing context.
+ *
+ * @returns {Promise} fulfilled after the credential is removed, or
+ * rejected in the cases the WebDriver command
+ * errors.
+ */
+ remove_credential: function(authenticator_id, credential_id, context=null) {
+ return window.test_driver_internal.remove_credential(authenticator_id, credential_id, context);
+ },
+
+ /**
+ * Removes all the credentials stored in a virtual authenticator
+ *
+ * Matches the `Remove All Credentials
+ * <https://w3c.github.io/webauthn/#sctn-automation-remove-all-credentials>`_
+ * WebDriver command.
+ *
+ * @param {String} authenticator_id - the ID of the authenticator
+ * @param {WindowProxy} context - Browsing context in which
+ * to run the call, or null for the current
+ * browsing context.
+ *
+ * @returns {Promise} fulfilled after the credentials are removed, or
+ * rejected in the cases the WebDriver command
+ * errors.
+ */
+ remove_all_credentials: function(authenticator_id, context=null) {
+ return window.test_driver_internal.remove_all_credentials(authenticator_id, context);
+ },
+
+ /**
+ * Sets the User Verified flag on an authenticator
+ *
+ * Sets whether requests requiring user verification will succeed or
+ * fail on a given virtual authenticator
+ *
+ * Matches the `Set User Verified
+ * <https://w3c.github.io/webauthn/#sctn-automation-set-user-verified>`_
+ * WebDriver command.
+ *
+ * @param {String} authenticator_id - the ID of the authenticator
+ * @param {boolean} uv - the User Verified flag
+ * @param {WindowProxy} context - Browsing context in which
+ * to run the call, or null for the current
+ * browsing context.
+ */
+ set_user_verified: function(authenticator_id, uv, context=null) {
+ return window.test_driver_internal.set_user_verified(authenticator_id, uv, context);
+ },
+
+ /**
+ * Sets the storage access rule for an origin when embedded
+ * in a third-party context.
+ *
+ * Matches the `Set Storage Access
+ * <https://privacycg.github.io/storage-access/#set-storage-access-command>`_
+ * WebDriver command.
+ *
+ * @param {String} origin - A third-party origin to block or allow.
+ * May be "*" to indicate all origins.
+ * @param {String} embedding_origin - an embedding (first-party) origin
+ * on which {origin}'s access should
+ * be blocked or allowed.
+ * May be "*" to indicate all origins.
+ * @param {String} state - The storage access setting.
+ * Must be either "allowed" or "blocked".
+ * @param {WindowProxy} context - Browsing context in which
+ * to run the call, or null for the current
+ * browsing context.
+ *
+ * @returns {Promise} Fulfilled after the storage access rule has been
+ * set, or rejected if setting the rule fails.
+ */
+ set_storage_access: function(origin, embedding_origin, state, context=null) {
+ if (state !== "allowed" && state !== "blocked") {
+ throw new Error("storage access status must be 'allowed' or 'blocked'");
+ }
+ const blocked = state === "blocked";
+ return window.test_driver_internal.set_storage_access(origin, embedding_origin, blocked, context);
+ },
+
+ /**
+ * Sets the current transaction automation mode for Secure Payment
+ * Confirmation.
+ *
+ * This function places `Secure Payment
+ * Confirmation <https://w3c.github.io/secure-payment-confirmation>`_ into
+ * an automated 'autoaccept' or 'autoreject' mode, to allow testing
+ * without user interaction with the transaction UX prompt.
+ *
+ * Matches the `Set SPC Transaction Mode
+ * <https://w3c.github.io/secure-payment-confirmation/#sctn-automation-set-spc-transaction-mode>`_
+ * WebDriver command.
+ *
+ * @example
+ * await test_driver.set_spc_transaction_mode("autoaccept");
+ * test.add_cleanup(() => {
+ * return test_driver.set_spc_transaction_mode("none");
+ * });
+ *
+ * // Assumption: `request` is a PaymentRequest with a secure-payment-confirmation
+ * // payment method.
+ * const response = await request.show();
+ *
+ * @param {String} mode - The `transaction mode
+ * <https://w3c.github.io/secure-payment-confirmation/#enumdef-transactionautomationmode>`_
+ * to set. Must be one of "``none``",
+ * "``autoaccept``", or
+ * "``autoreject``".
+ * @param {WindowProxy} context - Browsing context in which
+ * to run the call, or null for the current
+ * browsing context.
+ *
+ * @returns {Promise} Fulfilled after the transaction mode has been set,
+ * or rejected if setting the mode fails.
+ */
+ set_spc_transaction_mode: function(mode, context=null) {
+ return window.test_driver_internal.set_spc_transaction_mode(mode, context);
+ },
+
+ /**
+ * Cancels the Federated Credential Management dialog
+ *
+ * Matches the `Cancel dialog
+ * <https://fedidcg.github.io/FedCM/#webdriver-canceldialog>`_
+ * WebDriver command.
+ *
+ * @param {WindowProxy} context - Browsing context in which
+ * to run the call, or null for the current
+ * browsing context.
+ *
+ * @returns {Promise} Fulfilled after the dialog is canceled, or rejected
+ * in case the WebDriver command errors
+ */
+ cancel_fedcm_dialog: function(context=null) {
+ return window.test_driver_internal.cancel_fedcm_dialog(context);
+ },
+
+ /**
+ * Selects an account from the Federated Credential Management dialog
+ *
+ * Matches the `Select account
+ * <https://fedidcg.github.io/FedCM/#webdriver-selectaccount>`_
+ * WebDriver command.
+ *
+ * @param {number} account_index - Index of the account to select.
+ * @param {WindowProxy} context - Browsing context in which
+ * to run the call, or null for the current
+ * browsing context.
+ *
+ * @returns {Promise} Fulfilled after the account is selected,
+ * or rejected in case the WebDriver command errors
+ */
+ select_fedcm_account: function(account_index, context=null) {
+ return window.test_driver_internal.select_fedcm_account(account_index, context);
+ },
+
+ /**
+ * Gets the account list from the Federated Credential Management dialog
+ *
+ * Matches the `Account list
+ * <https://fedidcg.github.io/FedCM/#webdriver-accountlist>`_
+ * WebDriver command.
+ *
+ * @param {WindowProxy} context - Browsing context in which
+ * to run the call, or null for the current
+ * browsing context.
+ *
+ * @returns {Promise} fulfilled after the account list is returned, or
+ * rejected in case the WebDriver command errors
+ */
+ get_fedcm_account_list: function(context=null) {
+ return window.test_driver_internal.get_fedcm_account_list(context);
+ },
+
+ /**
+ * Gets the title of the Federated Credential Management dialog
+ *
+ * Matches the `Get title
+ * <https://fedidcg.github.io/FedCM/#webdriver-gettitle>`_
+ * WebDriver command.
+ *
+ * @param {WindowProxy} context - Browsing context in which
+ * to run the call, or null for the current
+ * browsing context.
+ *
+ * @returns {Promise} Fulfilled after the title is returned, or rejected
+ * in case the WebDriver command errors
+ */
+ get_fedcm_dialog_title: function(context=null) {
+ return window.test_driver_internal.get_fedcm_dialog_title(context);
+ },
+
+ /**
+ * Gets the type of the Federated Credential Management dialog
+ *
+ * Matches the `Get dialog type
+ * <https://fedidcg.github.io/FedCM/#webdriver-getdialogtype>`_
+ * WebDriver command.
+ *
+ * @param {WindowProxy} context - Browsing context in which
+ * to run the call, or null for the current
+ * browsing context.
+ *
+ * @returns {Promise} Fulfilled after the dialog type is returned, or
+ * rejected in case the WebDriver command errors
+ */
+ get_fedcm_dialog_type: function(context=null) {
+ return window.test_driver_internal.get_fedcm_dialog_type(context);
+ },
+
+ /**
+ * Sets whether promise rejection delay is enabled for the Federated Credential Management dialog
+ *
+ * Matches the `Set delay enabled
+ * <https://fedidcg.github.io/FedCM/#webdriver-setdelayenabled>`_
+ * WebDriver command.
+ *
+ * @param {boolean} enabled - Whether to delay FedCM promise rejection.
+ * @param {WindowProxy} context - Browsing context in which
+ * to run the call, or null for the current
+ * browsing context.
+ *
+ * @returns {Promise} Fulfilled after the delay has been enabled or disabled,
+ * or rejected in case the WebDriver command errors
+ */
+ set_fedcm_delay_enabled: function(enabled, context=null) {
+ return window.test_driver_internal.set_fedcm_delay_enabled(enabled, context);
+ },
+
+ /**
+ * Resets the Federated Credential Management dialog's cooldown
+ *
+ * Matches the `Reset cooldown
+ * <https://fedidcg.github.io/FedCM/#webdriver-resetcooldown>`_
+ * WebDriver command.
+ *
+ * @param {WindowProxy} context - Browsing context in which
+ * to run the call, or null for the current
+ * browsing context.
+ *
+ * @returns {Promise} Fulfilled after the cooldown has been reset,
+ * or rejected in case the WebDriver command errors
+ */
+ reset_fedcm_cooldown: function(context=null) {
+ return window.test_driver_internal.reset_fedcm_cooldown(context);
+ }
+ };
+
+ window.test_driver_internal = {
+ /**
+ * This flag should be set to `true` by any code which implements the
+ * internal methods defined below for automation purposes. Doing so
+ * allows the library to signal failure immediately when an automated
+ * implementation of one of the methods is not available.
+ */
+ in_automation: false,
+
+ async click(element, coords) {
+ if (this.in_automation) {
+ throw new Error("click() is not implemented by testdriver-vendor.js");
+ }
+
+ return new Promise(function(resolve, reject) {
+ element.addEventListener("click", resolve);
+ });
+ },
+
+ async delete_all_cookies(context=null) {
+ throw new Error("delete_all_cookies() is not implemented by testdriver-vendor.js");
+ },
+
+ async get_all_cookies(context=null) {
+ throw new Error("get_all_cookies() is not implemented by testdriver-vendor.js");
+ },
+
+ async get_named_cookie(name, context=null) {
+ throw new Error("get_named_cookie() is not implemented by testdriver-vendor.js");
+ },
+
+ async send_keys(element, keys) {
+ if (this.in_automation) {
+ throw new Error("send_keys() is not implemented by testdriver-vendor.js");
+ }
+
+ return new Promise(function(resolve, reject) {
+ var seen = "";
+
+ function remove() {
+ element.removeEventListener("keydown", onKeyDown);
+ }
+
+ function onKeyDown(event) {
+ if (event.key.length > 1) {
+ return;
+ }
+
+ seen += event.key;
+
+ if (keys.indexOf(seen) !== 0) {
+ reject(new Error("Unexpected key sequence: " + seen));
+ remove();
+ } else if (seen === keys) {
+ resolve();
+ remove();
+ }
+ }
+
+ element.addEventListener("keydown", onKeyDown);
+ });
+ },
+
+ async freeze(context=null) {
+ throw new Error("freeze() is not implemented by testdriver-vendor.js");
+ },
+
+ async minimize_window(context=null) {
+ throw new Error("minimize_window() is not implemented by testdriver-vendor.js");
+ },
+
+ async set_window_rect(rect, context=null) {
+ throw new Error("set_window_rect() is not implemented by testdriver-vendor.js");
+ },
+
+ async action_sequence(actions, context=null) {
+ throw new Error("action_sequence() is not implemented by testdriver-vendor.js");
+ },
+
+ async generate_test_report(message, context=null) {
+ throw new Error("generate_test_report() is not implemented by testdriver-vendor.js");
+ },
+
+ async set_permission(permission_params, context=null) {
+ throw new Error("set_permission() is not implemented by testdriver-vendor.js");
+ },
+
+ async add_virtual_authenticator(config, context=null) {
+ throw new Error("add_virtual_authenticator() is not implemented by testdriver-vendor.js");
+ },
+
+ async remove_virtual_authenticator(authenticator_id, context=null) {
+ throw new Error("remove_virtual_authenticator() is not implemented by testdriver-vendor.js");
+ },
+
+ async add_credential(authenticator_id, credential, context=null) {
+ throw new Error("add_credential() is not implemented by testdriver-vendor.js");
+ },
+
+ async get_credentials(authenticator_id, context=null) {
+ throw new Error("get_credentials() is not implemented by testdriver-vendor.js");
+ },
+
+ async remove_credential(authenticator_id, credential_id, context=null) {
+ throw new Error("remove_credential() is not implemented by testdriver-vendor.js");
+ },
+
+ async remove_all_credentials(authenticator_id, context=null) {
+ throw new Error("remove_all_credentials() is not implemented by testdriver-vendor.js");
+ },
+
+ async set_user_verified(authenticator_id, uv, context=null) {
+ throw new Error("set_user_verified() is not implemented by testdriver-vendor.js");
+ },
+
+ async set_storage_access(origin, embedding_origin, blocked, context=null) {
+ throw new Error("set_storage_access() is not implemented by testdriver-vendor.js");
+ },
+
+ async set_spc_transaction_mode(mode, context=null) {
+ throw new Error("set_spc_transaction_mode() is not implemented by testdriver-vendor.js");
+ },
+
+ async cancel_fedcm_dialog(context=null) {
+ throw new Error("cancel_fedcm_dialog() is not implemented by testdriver-vendor.js");
+ },
+
+ async select_fedcm_account(account_index, context=null) {
+ throw new Error("select_fedcm_account() is not implemented by testdriver-vendor.js");
+ },
+
+ async get_fedcm_account_list(context=null) {
+ throw new Error("get_fedcm_account_list() is not implemented by testdriver-vendor.js");
+ },
+
+ async get_fedcm_dialog_title(context=null) {
+ throw new Error("get_fedcm_dialog_title() is not implemented by testdriver-vendor.js");
+ },
+
+ async get_fedcm_dialog_type(context=null) {
+ throw new Error("get_fedcm_dialog_type() is not implemented by testdriver-vendor.js");
+ },
+
+ async set_fedcm_delay_enabled(enabled, context=null) {
+ throw new Error("set_fedcm_delay_enabled() is not implemented by testdriver-vendor.js");
+ },
+
+ async reset_fedcm_cooldown(context=null) {
+ throw new Error("reset_fedcm_cooldown() is not implemented by testdriver-vendor.js");
+ }
+ };
+})();
diff --git a/test/wpt/tests/resources/testdriver.js.headers b/test/wpt/tests/resources/testdriver.js.headers
new file mode 100644
index 0000000..5e8f640
--- /dev/null
+++ b/test/wpt/tests/resources/testdriver.js.headers
@@ -0,0 +1,2 @@
+Content-Type: text/javascript; charset=utf-8
+Cache-Control: max-age=3600
diff --git a/test/wpt/tests/resources/testharness.js b/test/wpt/tests/resources/testharness.js
new file mode 100644
index 0000000..4139930
--- /dev/null
+++ b/test/wpt/tests/resources/testharness.js
@@ -0,0 +1,4933 @@
+/*global self*/
+/*jshint latedef: nofunc*/
+
+/* Documentation: https://web-platform-tests.org/writing-tests/testharness-api.html
+ * (../docs/_writing-tests/testharness-api.md) */
+
+(function (global_scope)
+{
+ // default timeout is 10 seconds, test can override if needed
+ var settings = {
+ output:true,
+ harness_timeout:{
+ "normal":10000,
+ "long":60000
+ },
+ test_timeout:null,
+ message_events: ["start", "test_state", "result", "completion"],
+ debug: false,
+ };
+
+ var xhtml_ns = "http://www.w3.org/1999/xhtml";
+
+ /*
+ * TestEnvironment is an abstraction for the environment in which the test
+ * harness is used. Each implementation of a test environment has to provide
+ * the following interface:
+ *
+ * interface TestEnvironment {
+ * // Invoked after the global 'tests' object has been created and it's
+ * // safe to call add_*_callback() to register event handlers.
+ * void on_tests_ready();
+ *
+ * // Invoked after setup() has been called to notify the test environment
+ * // of changes to the test harness properties.
+ * void on_new_harness_properties(object properties);
+ *
+ * // Should return a new unique default test name.
+ * DOMString next_default_test_name();
+ *
+ * // Should return the test harness timeout duration in milliseconds.
+ * float test_timeout();
+ * };
+ */
+
+ /*
+ * A test environment with a DOM. The global object is 'window'. By default
+ * test results are displayed in a table. Any parent windows receive
+ * callbacks or messages via postMessage() when test events occur. See
+ * apisample11.html and apisample12.html.
+ */
+ function WindowTestEnvironment() {
+ this.name_counter = 0;
+ this.window_cache = null;
+ this.output_handler = null;
+ this.all_loaded = false;
+ var this_obj = this;
+ this.message_events = [];
+ this.dispatched_messages = [];
+
+ this.message_functions = {
+ start: [add_start_callback, remove_start_callback,
+ function (properties) {
+ this_obj._dispatch("start_callback", [properties],
+ {type: "start", properties: properties});
+ }],
+
+ test_state: [add_test_state_callback, remove_test_state_callback,
+ function(test) {
+ this_obj._dispatch("test_state_callback", [test],
+ {type: "test_state",
+ test: test.structured_clone()});
+ }],
+ result: [add_result_callback, remove_result_callback,
+ function (test) {
+ this_obj.output_handler.show_status();
+ this_obj._dispatch("result_callback", [test],
+ {type: "result",
+ test: test.structured_clone()});
+ }],
+ completion: [add_completion_callback, remove_completion_callback,
+ function (tests, harness_status, asserts) {
+ var cloned_tests = map(tests, function(test) {
+ return test.structured_clone();
+ });
+ this_obj._dispatch("completion_callback", [tests, harness_status],
+ {type: "complete",
+ tests: cloned_tests,
+ status: harness_status.structured_clone(),
+ asserts: asserts.map(assert => assert.structured_clone())});
+ }]
+ }
+
+ on_event(window, 'load', function() {
+ this_obj.all_loaded = true;
+ });
+
+ on_event(window, 'message', function(event) {
+ if (event.data && event.data.type === "getmessages" && event.source) {
+ // A window can post "getmessages" to receive a duplicate of every
+ // message posted by this environment so far. This allows subscribers
+ // from fetch_tests_from_window to 'catch up' to the current state of
+ // this environment.
+ for (var i = 0; i < this_obj.dispatched_messages.length; ++i)
+ {
+ event.source.postMessage(this_obj.dispatched_messages[i], "*");
+ }
+ }
+ });
+ }
+
+ WindowTestEnvironment.prototype._dispatch = function(selector, callback_args, message_arg) {
+ this.dispatched_messages.push(message_arg);
+ this._forEach_windows(
+ function(w, same_origin) {
+ if (same_origin) {
+ try {
+ var has_selector = selector in w;
+ } catch(e) {
+ // If document.domain was set at some point same_origin can be
+ // wrong and the above will fail.
+ has_selector = false;
+ }
+ if (has_selector) {
+ try {
+ w[selector].apply(undefined, callback_args);
+ } catch (e) {}
+ }
+ }
+ if (w !== self) {
+ w.postMessage(message_arg, "*");
+ }
+ });
+ };
+
+ WindowTestEnvironment.prototype._forEach_windows = function(callback) {
+ // Iterate over the windows [self ... top, opener]. The callback is passed
+ // two objects, the first one is the window object itself, the second one
+ // is a boolean indicating whether or not it's on the same origin as the
+ // current window.
+ var cache = this.window_cache;
+ if (!cache) {
+ cache = [[self, true]];
+ var w = self;
+ var i = 0;
+ var so;
+ while (w != w.parent) {
+ w = w.parent;
+ so = is_same_origin(w);
+ cache.push([w, so]);
+ i++;
+ }
+ w = window.opener;
+ if (w) {
+ cache.push([w, is_same_origin(w)]);
+ }
+ this.window_cache = cache;
+ }
+
+ forEach(cache,
+ function(a) {
+ callback.apply(null, a);
+ });
+ };
+
+ WindowTestEnvironment.prototype.on_tests_ready = function() {
+ var output = new Output();
+ this.output_handler = output;
+
+ var this_obj = this;
+
+ add_start_callback(function (properties) {
+ this_obj.output_handler.init(properties);
+ });
+
+ add_test_state_callback(function(test) {
+ this_obj.output_handler.show_status();
+ });
+
+ add_result_callback(function (test) {
+ this_obj.output_handler.show_status();
+ });
+
+ add_completion_callback(function (tests, harness_status, asserts_run) {
+ this_obj.output_handler.show_results(tests, harness_status, asserts_run);
+ });
+ this.setup_messages(settings.message_events);
+ };
+
+ WindowTestEnvironment.prototype.setup_messages = function(new_events) {
+ var this_obj = this;
+ forEach(settings.message_events, function(x) {
+ var current_dispatch = this_obj.message_events.indexOf(x) !== -1;
+ var new_dispatch = new_events.indexOf(x) !== -1;
+ if (!current_dispatch && new_dispatch) {
+ this_obj.message_functions[x][0](this_obj.message_functions[x][2]);
+ } else if (current_dispatch && !new_dispatch) {
+ this_obj.message_functions[x][1](this_obj.message_functions[x][2]);
+ }
+ });
+ this.message_events = new_events;
+ }
+
+ WindowTestEnvironment.prototype.next_default_test_name = function() {
+ var suffix = this.name_counter > 0 ? " " + this.name_counter : "";
+ this.name_counter++;
+ return get_title() + suffix;
+ };
+
+ WindowTestEnvironment.prototype.on_new_harness_properties = function(properties) {
+ this.output_handler.setup(properties);
+ if (properties.hasOwnProperty("message_events")) {
+ this.setup_messages(properties.message_events);
+ }
+ };
+
+ WindowTestEnvironment.prototype.add_on_loaded_callback = function(callback) {
+ on_event(window, 'load', callback);
+ };
+
+ WindowTestEnvironment.prototype.test_timeout = function() {
+ var metas = document.getElementsByTagName("meta");
+ for (var i = 0; i < metas.length; i++) {
+ if (metas[i].name == "timeout") {
+ if (metas[i].content == "long") {
+ return settings.harness_timeout.long;
+ }
+ break;
+ }
+ }
+ return settings.harness_timeout.normal;
+ };
+
+ /*
+ * Base TestEnvironment implementation for a generic web worker.
+ *
+ * Workers accumulate test results. One or more clients can connect and
+ * retrieve results from a worker at any time.
+ *
+ * WorkerTestEnvironment supports communicating with a client via a
+ * MessagePort. The mechanism for determining the appropriate MessagePort
+ * for communicating with a client depends on the type of worker and is
+ * implemented by the various specializations of WorkerTestEnvironment
+ * below.
+ *
+ * A client document using testharness can use fetch_tests_from_worker() to
+ * retrieve results from a worker. See apisample16.html.
+ */
+ function WorkerTestEnvironment() {
+ this.name_counter = 0;
+ this.all_loaded = true;
+ this.message_list = [];
+ this.message_ports = [];
+ }
+
+ WorkerTestEnvironment.prototype._dispatch = function(message) {
+ this.message_list.push(message);
+ for (var i = 0; i < this.message_ports.length; ++i)
+ {
+ this.message_ports[i].postMessage(message);
+ }
+ };
+
+ // The only requirement is that port has a postMessage() method. It doesn't
+ // have to be an instance of a MessagePort, and often isn't.
+ WorkerTestEnvironment.prototype._add_message_port = function(port) {
+ this.message_ports.push(port);
+ for (var i = 0; i < this.message_list.length; ++i)
+ {
+ port.postMessage(this.message_list[i]);
+ }
+ };
+
+ WorkerTestEnvironment.prototype.next_default_test_name = function() {
+ var suffix = this.name_counter > 0 ? " " + this.name_counter : "";
+ this.name_counter++;
+ return get_title() + suffix;
+ };
+
+ WorkerTestEnvironment.prototype.on_new_harness_properties = function() {};
+
+ WorkerTestEnvironment.prototype.on_tests_ready = function() {
+ var this_obj = this;
+ add_start_callback(
+ function(properties) {
+ this_obj._dispatch({
+ type: "start",
+ properties: properties,
+ });
+ });
+ add_test_state_callback(
+ function(test) {
+ this_obj._dispatch({
+ type: "test_state",
+ test: test.structured_clone()
+ });
+ });
+ add_result_callback(
+ function(test) {
+ this_obj._dispatch({
+ type: "result",
+ test: test.structured_clone()
+ });
+ });
+ add_completion_callback(
+ function(tests, harness_status, asserts) {
+ this_obj._dispatch({
+ type: "complete",
+ tests: map(tests,
+ function(test) {
+ return test.structured_clone();
+ }),
+ status: harness_status.structured_clone(),
+ asserts: asserts.map(assert => assert.structured_clone()),
+ });
+ });
+ };
+
+ WorkerTestEnvironment.prototype.add_on_loaded_callback = function() {};
+
+ WorkerTestEnvironment.prototype.test_timeout = function() {
+ // Tests running in a worker don't have a default timeout. I.e. all
+ // worker tests behave as if settings.explicit_timeout is true.
+ return null;
+ };
+
+ /*
+ * Dedicated web workers.
+ * https://html.spec.whatwg.org/multipage/workers.html#dedicatedworkerglobalscope
+ *
+ * This class is used as the test_environment when testharness is running
+ * inside a dedicated worker.
+ */
+ function DedicatedWorkerTestEnvironment() {
+ WorkerTestEnvironment.call(this);
+ // self is an instance of DedicatedWorkerGlobalScope which exposes
+ // a postMessage() method for communicating via the message channel
+ // established when the worker is created.
+ this._add_message_port(self);
+ }
+ DedicatedWorkerTestEnvironment.prototype = Object.create(WorkerTestEnvironment.prototype);
+
+ DedicatedWorkerTestEnvironment.prototype.on_tests_ready = function() {
+ WorkerTestEnvironment.prototype.on_tests_ready.call(this);
+ // In the absence of an onload notification, we a require dedicated
+ // workers to explicitly signal when the tests are done.
+ tests.wait_for_finish = true;
+ };
+
+ /*
+ * Shared web workers.
+ * https://html.spec.whatwg.org/multipage/workers.html#sharedworkerglobalscope
+ *
+ * This class is used as the test_environment when testharness is running
+ * inside a shared web worker.
+ */
+ function SharedWorkerTestEnvironment() {
+ WorkerTestEnvironment.call(this);
+ var this_obj = this;
+ // Shared workers receive message ports via the 'onconnect' event for
+ // each connection.
+ self.addEventListener("connect",
+ function(message_event) {
+ this_obj._add_message_port(message_event.source);
+ }, false);
+ }
+ SharedWorkerTestEnvironment.prototype = Object.create(WorkerTestEnvironment.prototype);
+
+ SharedWorkerTestEnvironment.prototype.on_tests_ready = function() {
+ WorkerTestEnvironment.prototype.on_tests_ready.call(this);
+ // In the absence of an onload notification, we a require shared
+ // workers to explicitly signal when the tests are done.
+ tests.wait_for_finish = true;
+ };
+
+ /*
+ * Service workers.
+ * http://www.w3.org/TR/service-workers/
+ *
+ * This class is used as the test_environment when testharness is running
+ * inside a service worker.
+ */
+ function ServiceWorkerTestEnvironment() {
+ WorkerTestEnvironment.call(this);
+ this.all_loaded = false;
+ this.on_loaded_callback = null;
+ var this_obj = this;
+ self.addEventListener("message",
+ function(event) {
+ if (event.data && event.data.type && event.data.type === "connect") {
+ this_obj._add_message_port(event.source);
+ }
+ }, false);
+
+ // The oninstall event is received after the service worker script and
+ // all imported scripts have been fetched and executed. It's the
+ // equivalent of an onload event for a document. All tests should have
+ // been added by the time this event is received, thus it's not
+ // necessary to wait until the onactivate event. However, tests for
+ // installed service workers need another event which is equivalent to
+ // the onload event because oninstall is fired only on installation. The
+ // onmessage event is used for that purpose since tests using
+ // testharness.js should ask the result to its service worker by
+ // PostMessage. If the onmessage event is triggered on the service
+ // worker's context, that means the worker's script has been evaluated.
+ on_event(self, "install", on_all_loaded);
+ on_event(self, "message", on_all_loaded);
+ function on_all_loaded() {
+ if (this_obj.all_loaded)
+ return;
+ this_obj.all_loaded = true;
+ if (this_obj.on_loaded_callback) {
+ this_obj.on_loaded_callback();
+ }
+ }
+ }
+
+ ServiceWorkerTestEnvironment.prototype = Object.create(WorkerTestEnvironment.prototype);
+
+ ServiceWorkerTestEnvironment.prototype.add_on_loaded_callback = function(callback) {
+ if (this.all_loaded) {
+ callback();
+ } else {
+ this.on_loaded_callback = callback;
+ }
+ };
+
+ /*
+ * Shadow realms.
+ * https://github.com/tc39/proposal-shadowrealm
+ *
+ * This class is used as the test_environment when testharness is running
+ * inside a shadow realm.
+ */
+ function ShadowRealmTestEnvironment() {
+ WorkerTestEnvironment.call(this);
+ this.all_loaded = false;
+ this.on_loaded_callback = null;
+ }
+
+ ShadowRealmTestEnvironment.prototype = Object.create(WorkerTestEnvironment.prototype);
+
+ /**
+ * Signal to the test environment that the tests are ready and the on-loaded
+ * callback should be run.
+ *
+ * Shadow realms are not *really* a DOM context: they have no `onload` or similar
+ * event for us to use to set up the test environment; so, instead, this method
+ * is manually triggered from the incubating realm
+ *
+ * @param {Function} message_destination - a function that receives JSON-serializable
+ * data to send to the incubating realm, in the same format as used by RemoteContext
+ */
+ ShadowRealmTestEnvironment.prototype.begin = function(message_destination) {
+ if (this.all_loaded) {
+ throw new Error("Tried to start a shadow realm test environment after it has already started");
+ }
+ var fakeMessagePort = {};
+ fakeMessagePort.postMessage = message_destination;
+ this._add_message_port(fakeMessagePort);
+ this.all_loaded = true;
+ if (this.on_loaded_callback) {
+ this.on_loaded_callback();
+ }
+ };
+
+ ShadowRealmTestEnvironment.prototype.add_on_loaded_callback = function(callback) {
+ if (this.all_loaded) {
+ callback();
+ } else {
+ this.on_loaded_callback = callback;
+ }
+ };
+
+ /*
+ * JavaScript shells.
+ *
+ * This class is used as the test_environment when testharness is running
+ * inside a JavaScript shell.
+ */
+ function ShellTestEnvironment() {
+ this.name_counter = 0;
+ this.all_loaded = false;
+ this.on_loaded_callback = null;
+ Promise.resolve().then(function() {
+ this.all_loaded = true
+ if (this.on_loaded_callback) {
+ this.on_loaded_callback();
+ }
+ }.bind(this));
+ this.message_list = [];
+ this.message_ports = [];
+ }
+
+ ShellTestEnvironment.prototype.next_default_test_name = function() {
+ var suffix = this.name_counter > 0 ? " " + this.name_counter : "";
+ this.name_counter++;
+ return get_title() + suffix;
+ };
+
+ ShellTestEnvironment.prototype.on_new_harness_properties = function() {};
+
+ ShellTestEnvironment.prototype.on_tests_ready = function() {};
+
+ ShellTestEnvironment.prototype.add_on_loaded_callback = function(callback) {
+ if (this.all_loaded) {
+ callback();
+ } else {
+ this.on_loaded_callback = callback;
+ }
+ };
+
+ ShellTestEnvironment.prototype.test_timeout = function() {
+ // Tests running in a shell don't have a default timeout, so behave as
+ // if settings.explicit_timeout is true.
+ return null;
+ };
+
+ function create_test_environment() {
+ if ('document' in global_scope) {
+ return new WindowTestEnvironment();
+ }
+ if ('DedicatedWorkerGlobalScope' in global_scope &&
+ global_scope instanceof DedicatedWorkerGlobalScope) {
+ return new DedicatedWorkerTestEnvironment();
+ }
+ if ('SharedWorkerGlobalScope' in global_scope &&
+ global_scope instanceof SharedWorkerGlobalScope) {
+ return new SharedWorkerTestEnvironment();
+ }
+ if ('ServiceWorkerGlobalScope' in global_scope &&
+ global_scope instanceof ServiceWorkerGlobalScope) {
+ return new ServiceWorkerTestEnvironment();
+ }
+ if ('WorkerGlobalScope' in global_scope &&
+ global_scope instanceof WorkerGlobalScope) {
+ return new DedicatedWorkerTestEnvironment();
+ }
+ /* Shadow realm global objects are _ordinary_ objects (i.e. their prototype is
+ * Object) so we don't have a nice `instanceof` test to use; instead, we
+ * check if the there is a GLOBAL.isShadowRealm() property
+ * on the global object. that was set by the test harness when it
+ * created the ShadowRealm.
+ */
+ if (global_scope.GLOBAL && global_scope.GLOBAL.isShadowRealm()) {
+ return new ShadowRealmTestEnvironment();
+ }
+
+ return new ShellTestEnvironment();
+ }
+
+ var test_environment = create_test_environment();
+
+ function is_shared_worker(worker) {
+ return 'SharedWorker' in global_scope && worker instanceof SharedWorker;
+ }
+
+ function is_service_worker(worker) {
+ // The worker object may be from another execution context,
+ // so do not use instanceof here.
+ return 'ServiceWorker' in global_scope &&
+ Object.prototype.toString.call(worker) == '[object ServiceWorker]';
+ }
+
+ var seen_func_name = Object.create(null);
+
+ function get_test_name(func, name)
+ {
+ if (name) {
+ return name;
+ }
+
+ if (func) {
+ var func_code = func.toString();
+
+ // Try and match with brackets, but fallback to matching without
+ var arrow = func_code.match(/^\(\)\s*=>\s*(?:{(.*)}\s*|(.*))$/);
+
+ // Check for JS line separators
+ if (arrow !== null && !/[\u000A\u000D\u2028\u2029]/.test(func_code)) {
+ var trimmed = (arrow[1] !== undefined ? arrow[1] : arrow[2]).trim();
+ // drop trailing ; if there's no earlier ones
+ trimmed = trimmed.replace(/^([^;]*)(;\s*)+$/, "$1");
+
+ if (trimmed) {
+ let name = trimmed;
+ if (seen_func_name[trimmed]) {
+ // This subtest name already exists, so add a suffix.
+ name += " " + seen_func_name[trimmed];
+ } else {
+ seen_func_name[trimmed] = 0;
+ }
+ seen_func_name[trimmed] += 1;
+ return name;
+ }
+ }
+ }
+
+ return test_environment.next_default_test_name();
+ }
+
+ /**
+ * @callback TestFunction
+ * @param {Test} test - The test currnetly being run.
+ * @param {Any[]} args - Additional args to pass to function.
+ *
+ */
+
+ /**
+ * Create a synchronous test
+ *
+ * @param {TestFunction} func - Test function. This is executed
+ * immediately. If it returns without error, the test status is
+ * set to ``PASS``. If it throws an :js:class:`AssertionError`, or
+ * any other exception, the test status is set to ``FAIL``
+ * (typically from an `assert` function).
+ * @param {String} name - Test name. This must be unique in a
+ * given file and must be invariant between runs.
+ */
+ function test(func, name, properties)
+ {
+ if (tests.promise_setup_called) {
+ tests.status.status = tests.status.ERROR;
+ tests.status.message = '`test` invoked after `promise_setup`';
+ tests.complete();
+ }
+ var test_name = get_test_name(func, name);
+ var test_obj = new Test(test_name, properties);
+ var value = test_obj.step(func, test_obj, test_obj);
+
+ if (value !== undefined) {
+ var msg = 'Test named "' + test_name +
+ '" passed a function to `test` that returned a value.';
+
+ try {
+ if (value && typeof value.then === 'function') {
+ msg += ' Consider using `promise_test` instead when ' +
+ 'using Promises or async/await.';
+ }
+ } catch (err) {}
+
+ tests.status.status = tests.status.ERROR;
+ tests.status.message = msg;
+ }
+
+ if (test_obj.phase === test_obj.phases.STARTED) {
+ test_obj.done();
+ }
+ }
+
+ /**
+ * Create an asynchronous test
+ *
+ * @param {TestFunction|string} funcOrName - Initial step function
+ * to call immediately with the test name as an argument (if any),
+ * or name of the test.
+ * @param {String} name - Test name (if a test function was
+ * provided). This must be unique in a given file and must be
+ * invariant between runs.
+ * @returns {Test} An object representing the ongoing test.
+ */
+ function async_test(func, name, properties)
+ {
+ if (tests.promise_setup_called) {
+ tests.status.status = tests.status.ERROR;
+ tests.status.message = '`async_test` invoked after `promise_setup`';
+ tests.complete();
+ }
+ if (typeof func !== "function") {
+ properties = name;
+ name = func;
+ func = null;
+ }
+ var test_name = get_test_name(func, name);
+ var test_obj = new Test(test_name, properties);
+ if (func) {
+ var value = test_obj.step(func, test_obj, test_obj);
+
+ // Test authors sometimes return values to async_test, expecting us
+ // to handle the value somehow. Make doing so a harness error to be
+ // clear this is invalid, and point authors to promise_test if it
+ // may be appropriate.
+ //
+ // Note that we only perform this check on the initial function
+ // passed to async_test, not on any later steps - we haven't seen a
+ // consistent problem with those (and it's harder to check).
+ if (value !== undefined) {
+ var msg = 'Test named "' + test_name +
+ '" passed a function to `async_test` that returned a value.';
+
+ try {
+ if (value && typeof value.then === 'function') {
+ msg += ' Consider using `promise_test` instead when ' +
+ 'using Promises or async/await.';
+ }
+ } catch (err) {}
+
+ tests.set_status(tests.status.ERROR, msg);
+ tests.complete();
+ }
+ }
+ return test_obj;
+ }
+
+ /**
+ * Create a promise test.
+ *
+ * Promise tests are tests which are represented by a promise
+ * object. If the promise is fulfilled the test passes, if it's
+ * rejected the test fails, otherwise the test passes.
+ *
+ * @param {TestFunction} func - Test function. This must return a
+ * promise. The test is automatically marked as complete once the
+ * promise settles.
+ * @param {String} name - Test name. This must be unique in a
+ * given file and must be invariant between runs.
+ */
+ function promise_test(func, name, properties) {
+ if (typeof func !== "function") {
+ properties = name;
+ name = func;
+ func = null;
+ }
+ var test_name = get_test_name(func, name);
+ var test = new Test(test_name, properties);
+ test._is_promise_test = true;
+
+ // If there is no promise tests queue make one.
+ if (!tests.promise_tests) {
+ tests.promise_tests = Promise.resolve();
+ }
+ tests.promise_tests = tests.promise_tests.then(function() {
+ return new Promise(function(resolve) {
+ var promise = test.step(func, test, test);
+
+ test.step(function() {
+ assert(!!promise, "promise_test", null,
+ "test body must return a 'thenable' object (received ${value})",
+ {value:promise});
+ assert(typeof promise.then === "function", "promise_test", null,
+ "test body must return a 'thenable' object (received an object with no `then` method)",
+ null);
+ });
+
+ // Test authors may use the `step` method within a
+ // `promise_test` even though this reflects a mixture of
+ // asynchronous control flow paradigms. The "done" callback
+ // should be registered prior to the resolution of the
+ // user-provided Promise to avoid timeouts in cases where the
+ // Promise does not settle but a `step` function has thrown an
+ // error.
+ add_test_done_callback(test, resolve);
+
+ Promise.resolve(promise)
+ .catch(test.step_func(
+ function(value) {
+ if (value instanceof AssertionError) {
+ throw value;
+ }
+ assert(false, "promise_test", null,
+ "Unhandled rejection with value: ${value}", {value:value});
+ }))
+ .then(function() {
+ test.done();
+ });
+ });
+ });
+ }
+
+ /**
+ * Make a copy of a Promise in the current realm.
+ *
+ * @param {Promise} promise the given promise that may be from a different
+ * realm
+ * @returns {Promise}
+ *
+ * An arbitrary promise provided by the caller may have originated
+ * in another frame that have since navigated away, rendering the
+ * frame's document inactive. Such a promise cannot be used with
+ * `await` or Promise.resolve(), as microtasks associated with it
+ * may be prevented from being run. See `issue
+ * 5319<https://github.com/whatwg/html/issues/5319>`_ for a
+ * particular case.
+ *
+ * In functions we define here, there is an expectation from the caller
+ * that the promise is from the current realm, that can always be used with
+ * `await`, etc. We therefore create a new promise in this realm that
+ * inherit the value and status from the given promise.
+ */
+
+ function bring_promise_to_current_realm(promise) {
+ return new Promise(promise.then.bind(promise));
+ }
+
+ /**
+ * Assert that a Promise is rejected with the right ECMAScript exception.
+ *
+ * @param {Test} test - the `Test` to use for the assertion.
+ * @param {Function} constructor - The expected exception constructor.
+ * @param {Promise} promise - The promise that's expected to
+ * reject with the given exception.
+ * @param {string} [description] Error message to add to assert in case of
+ * failure.
+ */
+ function promise_rejects_js(test, constructor, promise, description) {
+ return bring_promise_to_current_realm(promise)
+ .then(test.unreached_func("Should have rejected: " + description))
+ .catch(function(e) {
+ assert_throws_js_impl(constructor, function() { throw e },
+ description, "promise_rejects_js");
+ });
+ }
+
+ /**
+ * Assert that a Promise is rejected with the right DOMException.
+ *
+ * For the remaining arguments, there are two ways of calling
+ * promise_rejects_dom:
+ *
+ * 1) If the DOMException is expected to come from the current global, the
+ * third argument should be the promise expected to reject, and a fourth,
+ * optional, argument is the assertion description.
+ *
+ * 2) If the DOMException is expected to come from some other global, the
+ * third argument should be the DOMException constructor from that global,
+ * the fourth argument the promise expected to reject, and the fifth,
+ * optional, argument the assertion description.
+ *
+ * @param {Test} test - the `Test` to use for the assertion.
+ * @param {number|string} type - See documentation for
+ * `assert_throws_dom <#assert_throws_dom>`_.
+ * @param {Function} promiseOrConstructor - Either the constructor
+ * for the expected exception (if the exception comes from another
+ * global), or the promise that's expected to reject (if the
+ * exception comes from the current global).
+ * @param {Function|string} descriptionOrPromise - Either the
+ * promise that's expected to reject (if the exception comes from
+ * another global), or the optional description of the condition
+ * being tested (if the exception comes from the current global).
+ * @param {string} [description] - Description of the condition
+ * being tested (if the exception comes from another global).
+ *
+ */
+ function promise_rejects_dom(test, type, promiseOrConstructor, descriptionOrPromise, maybeDescription) {
+ let constructor, promise, description;
+ if (typeof promiseOrConstructor === "function" &&
+ promiseOrConstructor.name === "DOMException") {
+ constructor = promiseOrConstructor;
+ promise = descriptionOrPromise;
+ description = maybeDescription;
+ } else {
+ constructor = self.DOMException;
+ promise = promiseOrConstructor;
+ description = descriptionOrPromise;
+ assert(maybeDescription === undefined,
+ "Too many args pased to no-constructor version of promise_rejects_dom");
+ }
+ return bring_promise_to_current_realm(promise)
+ .then(test.unreached_func("Should have rejected: " + description))
+ .catch(function(e) {
+ assert_throws_dom_impl(type, function() { throw e }, description,
+ "promise_rejects_dom", constructor);
+ });
+ }
+
+ /**
+ * Assert that a Promise is rejected with the provided value.
+ *
+ * @param {Test} test - the `Test` to use for the assertion.
+ * @param {Any} exception - The expected value of the rejected promise.
+ * @param {Promise} promise - The promise that's expected to
+ * reject.
+ * @param {string} [description] Error message to add to assert in case of
+ * failure.
+ */
+ function promise_rejects_exactly(test, exception, promise, description) {
+ return bring_promise_to_current_realm(promise)
+ .then(test.unreached_func("Should have rejected: " + description))
+ .catch(function(e) {
+ assert_throws_exactly_impl(exception, function() { throw e },
+ description, "promise_rejects_exactly");
+ });
+ }
+
+ /**
+ * Allow DOM events to be handled using Promises.
+ *
+ * This can make it a lot easier to test a very specific series of events,
+ * including ensuring that unexpected events are not fired at any point.
+ *
+ * `EventWatcher` will assert if an event occurs while there is no `wait_for`
+ * created Promise waiting to be fulfilled, or if the event is of a different type
+ * to the type currently expected. This ensures that only the events that are
+ * expected occur, in the correct order, and with the correct timing.
+ *
+ * @constructor
+ * @param {Test} test - The `Test` to use for the assertion.
+ * @param {EventTarget} watchedNode - The target expected to receive the events.
+ * @param {string[]} eventTypes - List of events to watch for.
+ * @param {Promise} timeoutPromise - Promise that will cause the
+ * test to be set to `TIMEOUT` once fulfilled.
+ *
+ */
+ function EventWatcher(test, watchedNode, eventTypes, timeoutPromise)
+ {
+ if (typeof eventTypes == 'string') {
+ eventTypes = [eventTypes];
+ }
+
+ var waitingFor = null;
+
+ // This is null unless we are recording all events, in which case it
+ // will be an Array object.
+ var recordedEvents = null;
+
+ var eventHandler = test.step_func(function(evt) {
+ assert_true(!!waitingFor,
+ 'Not expecting event, but got ' + evt.type + ' event');
+ assert_equals(evt.type, waitingFor.types[0],
+ 'Expected ' + waitingFor.types[0] + ' event, but got ' +
+ evt.type + ' event instead');
+
+ if (Array.isArray(recordedEvents)) {
+ recordedEvents.push(evt);
+ }
+
+ if (waitingFor.types.length > 1) {
+ // Pop first event from array
+ waitingFor.types.shift();
+ return;
+ }
+ // We need to null out waitingFor before calling the resolve function
+ // since the Promise's resolve handlers may call wait_for() which will
+ // need to set waitingFor.
+ var resolveFunc = waitingFor.resolve;
+ waitingFor = null;
+ // Likewise, we should reset the state of recordedEvents.
+ var result = recordedEvents || evt;
+ recordedEvents = null;
+ resolveFunc(result);
+ });
+
+ for (var i = 0; i < eventTypes.length; i++) {
+ watchedNode.addEventListener(eventTypes[i], eventHandler, false);
+ }
+
+ /**
+ * Returns a Promise that will resolve after the specified event or
+ * series of events has occurred.
+ *
+ * @param {Object} options An optional options object. If the 'record' property
+ * on this object has the value 'all', when the Promise
+ * returned by this function is resolved, *all* Event
+ * objects that were waited for will be returned as an
+ * array.
+ *
+ * @example
+ * const watcher = new EventWatcher(t, div, [ 'animationstart',
+ * 'animationiteration',
+ * 'animationend' ]);
+ * return watcher.wait_for([ 'animationstart', 'animationend' ],
+ * { record: 'all' }).then(evts => {
+ * assert_equals(evts[0].elapsedTime, 0.0);
+ * assert_equals(evts[1].elapsedTime, 2.0);
+ * });
+ */
+ this.wait_for = function(types, options) {
+ if (waitingFor) {
+ return Promise.reject('Already waiting for an event or events');
+ }
+ if (typeof types == 'string') {
+ types = [types];
+ }
+ if (options && options.record && options.record === 'all') {
+ recordedEvents = [];
+ }
+ return new Promise(function(resolve, reject) {
+ var timeout = test.step_func(function() {
+ // If the timeout fires after the events have been received
+ // or during a subsequent call to wait_for, ignore it.
+ if (!waitingFor || waitingFor.resolve !== resolve)
+ return;
+
+ // This should always fail, otherwise we should have
+ // resolved the promise.
+ assert_true(waitingFor.types.length == 0,
+ 'Timed out waiting for ' + waitingFor.types.join(', '));
+ var result = recordedEvents;
+ recordedEvents = null;
+ var resolveFunc = waitingFor.resolve;
+ waitingFor = null;
+ resolveFunc(result);
+ });
+
+ if (timeoutPromise) {
+ timeoutPromise().then(timeout);
+ }
+
+ waitingFor = {
+ types: types,
+ resolve: resolve,
+ reject: reject
+ };
+ });
+ };
+
+ /**
+ * Stop listening for events
+ */
+ function stop_watching() {
+ for (var i = 0; i < eventTypes.length; i++) {
+ watchedNode.removeEventListener(eventTypes[i], eventHandler, false);
+ }
+ };
+
+ test._add_cleanup(stop_watching);
+
+ return this;
+ }
+ expose(EventWatcher, 'EventWatcher');
+
+ /**
+ * @typedef {Object} SettingsObject
+ * @property {bool} single_test - Use the single-page-test
+ * mode. In this mode the Document represents a single
+ * `async_test`. Asserts may be used directly without requiring
+ * `Test.step` or similar wrappers, and any exceptions set the
+ * status of the test rather than the status of the harness.
+ * @property {bool} allow_uncaught_exception - don't treat an
+ * uncaught exception as an error; needed when e.g. testing the
+ * `window.onerror` handler.
+ * @property {boolean} explicit_done - Wait for a call to `done()`
+ * before declaring all tests complete (this is always true for
+ * single-page tests).
+ * @property hide_test_state - hide the test state output while
+ * the test is running; This is helpful when the output of the test state
+ * may interfere the test results.
+ * @property {bool} explicit_timeout - disable file timeout; only
+ * stop waiting for results when the `timeout()` function is
+ * called This should typically only be set for manual tests, or
+ * by a test runner that providees its own timeout mechanism.
+ * @property {number} timeout_multiplier - Multiplier to apply to
+ * per-test timeouts. This should only be set by a test runner.
+ * @property {Document} output_document - The document to which
+ * results should be logged. By default this is the current
+ * document but could be an ancestor document in some cases e.g. a
+ * SVG test loaded in an HTML wrapper
+ *
+ */
+
+ /**
+ * Configure the harness
+ *
+ * @param {Function|SettingsObject} funcOrProperties - Either a
+ * setup function to run, or a set of properties. If this is a
+ * function that function is run synchronously. Any exception in
+ * the function will set the overall harness status to `ERROR`.
+ * @param {SettingsObject} maybeProperties - An object containing
+ * the settings to use, if the first argument is a function.
+ *
+ */
+ function setup(func_or_properties, maybe_properties)
+ {
+ var func = null;
+ var properties = {};
+ if (arguments.length === 2) {
+ func = func_or_properties;
+ properties = maybe_properties;
+ } else if (func_or_properties instanceof Function) {
+ func = func_or_properties;
+ } else {
+ properties = func_or_properties;
+ }
+ tests.setup(func, properties);
+ test_environment.on_new_harness_properties(properties);
+ }
+
+ /**
+ * Configure the harness, waiting for a promise to resolve
+ * before running any `promise_test` tests.
+ *
+ * @param {Function} func - Function returning a promise that's
+ * run synchronously. Promise tests are not run until after this
+ * function has resolved.
+ * @param {SettingsObject} [properties] - An object containing
+ * the harness settings to use.
+ *
+ */
+ function promise_setup(func, properties={})
+ {
+ if (typeof func !== "function") {
+ tests.set_status(tests.status.ERROR,
+ "promise_test invoked without a function");
+ tests.complete();
+ return;
+ }
+ tests.promise_setup_called = true;
+
+ if (!tests.promise_tests) {
+ tests.promise_tests = Promise.resolve();
+ }
+
+ tests.promise_tests = tests.promise_tests
+ .then(function()
+ {
+ var result;
+
+ tests.setup(null, properties);
+ result = func();
+ test_environment.on_new_harness_properties(properties);
+
+ if (!result || typeof result.then !== "function") {
+ throw "Non-thenable returned by function passed to `promise_setup`";
+ }
+ return result;
+ })
+ .catch(function(e)
+ {
+ tests.set_status(tests.status.ERROR,
+ String(e),
+ e && e.stack);
+ tests.complete();
+ });
+ }
+
+ /**
+ * Mark test loading as complete.
+ *
+ * Typically this function is called implicitly on page load; it's
+ * only necessary for users to call this when either the
+ * ``explicit_done`` or ``single_page`` properties have been set
+ * via the :js:func:`setup` function.
+ *
+ * For single page tests this marks the test as complete and sets its status.
+ * For other tests, this marks test loading as complete, but doesn't affect ongoing tests.
+ */
+ function done() {
+ if (tests.tests.length === 0) {
+ // `done` is invoked after handling uncaught exceptions, so if the
+ // harness status is already set, the corresponding message is more
+ // descriptive than the generic message defined here.
+ if (tests.status.status === null) {
+ tests.status.status = tests.status.ERROR;
+ tests.status.message = "done() was called without first defining any tests";
+ }
+
+ tests.complete();
+ return;
+ }
+ if (tests.file_is_test) {
+ // file is test files never have asynchronous cleanup logic,
+ // meaning the fully-synchronous `done` function can be used here.
+ tests.tests[0].done();
+ }
+ tests.end_wait();
+ }
+
+ /**
+ * @deprecated generate a list of tests from a function and list of arguments
+ *
+ * This is deprecated because it runs all the tests outside of the test functions
+ * and as a result any test throwing an exception will result in no tests being
+ * run. In almost all cases, you should simply call test within the loop you would
+ * use to generate the parameter list array.
+ *
+ * @param {Function} func - The function that will be called for each generated tests.
+ * @param {Any[][]} args - An array of arrays. Each nested array
+ * has the structure `[testName, ...testArgs]`. For each of these nested arrays
+ * array, a test is generated with name `testName` and test function equivalent to
+ * `func(..testArgs)`.
+ */
+ function generate_tests(func, args, properties) {
+ forEach(args, function(x, i)
+ {
+ var name = x[0];
+ test(function()
+ {
+ func.apply(this, x.slice(1));
+ },
+ name,
+ Array.isArray(properties) ? properties[i] : properties);
+ });
+ }
+
+ /**
+ * @deprecated
+ *
+ * Register a function as a DOM event listener to the
+ * given object for the event bubbling phase.
+ *
+ * @param {EventTarget} object - Event target
+ * @param {string} event - Event name
+ * @param {Function} callback - Event handler.
+ */
+ function on_event(object, event, callback)
+ {
+ object.addEventListener(event, callback, false);
+ }
+
+ /**
+ * Global version of :js:func:`Test.step_timeout` for use in single page tests.
+ *
+ * @param {Function} func - Function to run after the timeout
+ * @param {number} timeout - Time in ms to wait before running the
+ * test step. The actual wait time is ``timeout`` x
+ * ``timeout_multiplier``.
+ */
+ function step_timeout(func, timeout) {
+ var outer_this = this;
+ var args = Array.prototype.slice.call(arguments, 2);
+ return setTimeout(function() {
+ func.apply(outer_this, args);
+ }, timeout * tests.timeout_multiplier);
+ }
+
+ expose(test, 'test');
+ expose(async_test, 'async_test');
+ expose(promise_test, 'promise_test');
+ expose(promise_rejects_js, 'promise_rejects_js');
+ expose(promise_rejects_dom, 'promise_rejects_dom');
+ expose(promise_rejects_exactly, 'promise_rejects_exactly');
+ expose(generate_tests, 'generate_tests');
+ expose(setup, 'setup');
+ expose(promise_setup, 'promise_setup');
+ expose(done, 'done');
+ expose(on_event, 'on_event');
+ expose(step_timeout, 'step_timeout');
+
+ /*
+ * Return a string truncated to the given length, with ... added at the end
+ * if it was longer.
+ */
+ function truncate(s, len)
+ {
+ if (s.length > len) {
+ return s.substring(0, len - 3) + "...";
+ }
+ return s;
+ }
+
+ /*
+ * Return true if object is probably a Node object.
+ */
+ function is_node(object)
+ {
+ // I use duck-typing instead of instanceof, because
+ // instanceof doesn't work if the node is from another window (like an
+ // iframe's contentWindow):
+ // http://www.w3.org/Bugs/Public/show_bug.cgi?id=12295
+ try {
+ var has_node_properties = ("nodeType" in object &&
+ "nodeName" in object &&
+ "nodeValue" in object &&
+ "childNodes" in object);
+ } catch (e) {
+ // We're probably cross-origin, which means we aren't a node
+ return false;
+ }
+
+ if (has_node_properties) {
+ try {
+ object.nodeType;
+ } catch (e) {
+ // The object is probably Node.prototype or another prototype
+ // object that inherits from it, and not a Node instance.
+ return false;
+ }
+ return true;
+ }
+ return false;
+ }
+
+ var replacements = {
+ "0": "0",
+ "1": "x01",
+ "2": "x02",
+ "3": "x03",
+ "4": "x04",
+ "5": "x05",
+ "6": "x06",
+ "7": "x07",
+ "8": "b",
+ "9": "t",
+ "10": "n",
+ "11": "v",
+ "12": "f",
+ "13": "r",
+ "14": "x0e",
+ "15": "x0f",
+ "16": "x10",
+ "17": "x11",
+ "18": "x12",
+ "19": "x13",
+ "20": "x14",
+ "21": "x15",
+ "22": "x16",
+ "23": "x17",
+ "24": "x18",
+ "25": "x19",
+ "26": "x1a",
+ "27": "x1b",
+ "28": "x1c",
+ "29": "x1d",
+ "30": "x1e",
+ "31": "x1f",
+ "0xfffd": "ufffd",
+ "0xfffe": "ufffe",
+ "0xffff": "uffff",
+ };
+
+ /**
+ * Convert a value to a nice, human-readable string
+ *
+ * When many JavaScript Object values are coerced to a String, the
+ * resulting value will be ``"[object Object]"``. This obscures
+ * helpful information, making the coerced value unsuitable for
+ * use in assertion messages, test names, and debugging
+ * statements. `format_value` produces more distinctive string
+ * representations of many kinds of objects, including arrays and
+ * the more important DOM Node types. It also translates String
+ * values containing control characters to include human-readable
+ * representations.
+ *
+ * @example
+ * // "Document node with 2 children"
+ * format_value(document);
+ * @example
+ * // "\"foo\\uffffbar\""
+ * format_value("foo\uffffbar");
+ * @example
+ * // "[-0, Infinity]"
+ * format_value([-0, Infinity]);
+ * @param {Any} val - The value to convert to a string.
+ * @returns {string} - A string representation of ``val``, optimised for human readability.
+ */
+ function format_value(val, seen)
+ {
+ if (!seen) {
+ seen = [];
+ }
+ if (typeof val === "object" && val !== null) {
+ if (seen.indexOf(val) >= 0) {
+ return "[...]";
+ }
+ seen.push(val);
+ }
+ if (Array.isArray(val)) {
+ let output = "[";
+ if (val.beginEllipsis !== undefined) {
+ output += "…, ";
+ }
+ output += val.map(function(x) {return format_value(x, seen);}).join(", ");
+ if (val.endEllipsis !== undefined) {
+ output += ", …";
+ }
+ return output + "]";
+ }
+
+ switch (typeof val) {
+ case "string":
+ val = val.replace(/\\/g, "\\\\");
+ for (var p in replacements) {
+ var replace = "\\" + replacements[p];
+ val = val.replace(RegExp(String.fromCharCode(p), "g"), replace);
+ }
+ return '"' + val.replace(/"/g, '\\"') + '"';
+ case "boolean":
+ case "undefined":
+ return String(val);
+ case "number":
+ // In JavaScript, -0 === 0 and String(-0) == "0", so we have to
+ // special-case.
+ if (val === -0 && 1/val === -Infinity) {
+ return "-0";
+ }
+ return String(val);
+ case "object":
+ if (val === null) {
+ return "null";
+ }
+
+ // Special-case Node objects, since those come up a lot in my tests. I
+ // ignore namespaces.
+ if (is_node(val)) {
+ switch (val.nodeType) {
+ case Node.ELEMENT_NODE:
+ var ret = "<" + val.localName;
+ for (var i = 0; i < val.attributes.length; i++) {
+ ret += " " + val.attributes[i].name + '="' + val.attributes[i].value + '"';
+ }
+ ret += ">" + val.innerHTML + "</" + val.localName + ">";
+ return "Element node " + truncate(ret, 60);
+ case Node.TEXT_NODE:
+ return 'Text node "' + truncate(val.data, 60) + '"';
+ case Node.PROCESSING_INSTRUCTION_NODE:
+ return "ProcessingInstruction node with target " + format_value(truncate(val.target, 60)) + " and data " + format_value(truncate(val.data, 60));
+ case Node.COMMENT_NODE:
+ return "Comment node <!--" + truncate(val.data, 60) + "-->";
+ case Node.DOCUMENT_NODE:
+ return "Document node with " + val.childNodes.length + (val.childNodes.length == 1 ? " child" : " children");
+ case Node.DOCUMENT_TYPE_NODE:
+ return "DocumentType node";
+ case Node.DOCUMENT_FRAGMENT_NODE:
+ return "DocumentFragment node with " + val.childNodes.length + (val.childNodes.length == 1 ? " child" : " children");
+ default:
+ return "Node object of unknown type";
+ }
+ }
+
+ /* falls through */
+ default:
+ try {
+ return typeof val + ' "' + truncate(String(val), 1000) + '"';
+ } catch(e) {
+ return ("[stringifying object threw " + String(e) +
+ " with type " + String(typeof e) + "]");
+ }
+ }
+ }
+ expose(format_value, "format_value");
+
+ /*
+ * Assertions
+ */
+
+ function expose_assert(f, name) {
+ function assert_wrapper(...args) {
+ let status = Test.statuses.TIMEOUT;
+ let stack = null;
+ let new_assert_index = null;
+ try {
+ if (settings.debug) {
+ console.debug("ASSERT", name, tests.current_test && tests.current_test.name, args);
+ }
+ if (tests.output) {
+ tests.set_assert(name, args);
+ // Remember the newly pushed assert's index, because `apply`
+ // below might push new asserts.
+ new_assert_index = tests.asserts_run.length - 1;
+ }
+ const rv = f.apply(undefined, args);
+ status = Test.statuses.PASS;
+ return rv;
+ } catch(e) {
+ status = Test.statuses.FAIL;
+ stack = e.stack ? e.stack : null;
+ throw e;
+ } finally {
+ if (tests.output && !stack) {
+ stack = get_stack();
+ }
+ if (tests.output) {
+ tests.set_assert_status(new_assert_index, status, stack);
+ }
+ }
+ }
+ expose(assert_wrapper, name);
+ }
+
+ /**
+ * Assert that ``actual`` is strictly true
+ *
+ * @param {Any} actual - Value that is asserted to be true
+ * @param {string} [description] - Description of the condition being tested
+ */
+ function assert_true(actual, description)
+ {
+ assert(actual === true, "assert_true", description,
+ "expected true got ${actual}", {actual:actual});
+ }
+ expose_assert(assert_true, "assert_true");
+
+ /**
+ * Assert that ``actual`` is strictly false
+ *
+ * @param {Any} actual - Value that is asserted to be false
+ * @param {string} [description] - Description of the condition being tested
+ */
+ function assert_false(actual, description)
+ {
+ assert(actual === false, "assert_false", description,
+ "expected false got ${actual}", {actual:actual});
+ }
+ expose_assert(assert_false, "assert_false");
+
+ function same_value(x, y) {
+ if (y !== y) {
+ //NaN case
+ return x !== x;
+ }
+ if (x === 0 && y === 0) {
+ //Distinguish +0 and -0
+ return 1/x === 1/y;
+ }
+ return x === y;
+ }
+
+ /**
+ * Assert that ``actual`` is the same value as ``expected``.
+ *
+ * For objects this compares by cobject identity; for primitives
+ * this distinguishes between 0 and -0, and has correct handling
+ * of NaN.
+ *
+ * @param {Any} actual - Test value.
+ * @param {Any} expected - Expected value.
+ * @param {string} [description] - Description of the condition being tested.
+ */
+ function assert_equals(actual, expected, description)
+ {
+ /*
+ * Test if two primitives are equal or two objects
+ * are the same object
+ */
+ if (typeof actual != typeof expected) {
+ assert(false, "assert_equals", description,
+ "expected (" + typeof expected + ") ${expected} but got (" + typeof actual + ") ${actual}",
+ {expected:expected, actual:actual});
+ return;
+ }
+ assert(same_value(actual, expected), "assert_equals", description,
+ "expected ${expected} but got ${actual}",
+ {expected:expected, actual:actual});
+ }
+ expose_assert(assert_equals, "assert_equals");
+
+ /**
+ * Assert that ``actual`` is not the same value as ``expected``.
+ *
+ * Comparison is as for :js:func:`assert_equals`.
+ *
+ * @param {Any} actual - Test value.
+ * @param {Any} expected - The value ``actual`` is expected to be different to.
+ * @param {string} [description] - Description of the condition being tested.
+ */
+ function assert_not_equals(actual, expected, description)
+ {
+ assert(!same_value(actual, expected), "assert_not_equals", description,
+ "got disallowed value ${actual}",
+ {actual:actual});
+ }
+ expose_assert(assert_not_equals, "assert_not_equals");
+
+ /**
+ * Assert that ``expected`` is an array and ``actual`` is one of the members.
+ * This is implemented using ``indexOf``, so doesn't handle NaN or ±0 correctly.
+ *
+ * @param {Any} actual - Test value.
+ * @param {Array} expected - An array that ``actual`` is expected to
+ * be a member of.
+ * @param {string} [description] - Description of the condition being tested.
+ */
+ function assert_in_array(actual, expected, description)
+ {
+ assert(expected.indexOf(actual) != -1, "assert_in_array", description,
+ "value ${actual} not in array ${expected}",
+ {actual:actual, expected:expected});
+ }
+ expose_assert(assert_in_array, "assert_in_array");
+
+ // This function was deprecated in July of 2015.
+ // See https://github.com/web-platform-tests/wpt/issues/2033
+ /**
+ * @deprecated
+ * Recursively compare two objects for equality.
+ *
+ * See `Issue 2033
+ * <https://github.com/web-platform-tests/wpt/issues/2033>`_ for
+ * more information.
+ *
+ * @param {Object} actual - Test value.
+ * @param {Object} expected - Expected value.
+ * @param {string} [description] - Description of the condition being tested.
+ */
+ function assert_object_equals(actual, expected, description)
+ {
+ assert(typeof actual === "object" && actual !== null, "assert_object_equals", description,
+ "value is ${actual}, expected object",
+ {actual: actual});
+ //This needs to be improved a great deal
+ function check_equal(actual, expected, stack)
+ {
+ stack.push(actual);
+
+ var p;
+ for (p in actual) {
+ assert(expected.hasOwnProperty(p), "assert_object_equals", description,
+ "unexpected property ${p}", {p:p});
+
+ if (typeof actual[p] === "object" && actual[p] !== null) {
+ if (stack.indexOf(actual[p]) === -1) {
+ check_equal(actual[p], expected[p], stack);
+ }
+ } else {
+ assert(same_value(actual[p], expected[p]), "assert_object_equals", description,
+ "property ${p} expected ${expected} got ${actual}",
+ {p:p, expected:expected[p], actual:actual[p]});
+ }
+ }
+ for (p in expected) {
+ assert(actual.hasOwnProperty(p),
+ "assert_object_equals", description,
+ "expected property ${p} missing", {p:p});
+ }
+ stack.pop();
+ }
+ check_equal(actual, expected, []);
+ }
+ expose_assert(assert_object_equals, "assert_object_equals");
+
+ /**
+ * Assert that ``actual`` and ``expected`` are both arrays, and that the array properties of
+ * ``actual`` and ``expected`` are all the same value (as for :js:func:`assert_equals`).
+ *
+ * @param {Array} actual - Test array.
+ * @param {Array} expected - Array that is expected to contain the same values as ``actual``.
+ * @param {string} [description] - Description of the condition being tested.
+ */
+ function assert_array_equals(actual, expected, description)
+ {
+ const max_array_length = 20;
+ function shorten_array(arr, offset = 0) {
+ // Make ", …" only show up when it would likely reduce the length, not accounting for
+ // fonts.
+ if (arr.length < max_array_length + 2) {
+ return arr;
+ }
+ // By default we want half the elements after the offset and half before
+ // But if that takes us past the end of the array, we have more before, and
+ // if it takes us before the start we have more after.
+ const length_after_offset = Math.floor(max_array_length / 2);
+ let upper_bound = Math.min(length_after_offset + offset, arr.length);
+ const lower_bound = Math.max(upper_bound - max_array_length, 0);
+
+ if (lower_bound === 0) {
+ upper_bound = max_array_length;
+ }
+
+ const output = arr.slice(lower_bound, upper_bound);
+ if (lower_bound > 0) {
+ output.beginEllipsis = true;
+ }
+ if (upper_bound < arr.length) {
+ output.endEllipsis = true;
+ }
+ return output;
+ }
+
+ assert(typeof actual === "object" && actual !== null && "length" in actual,
+ "assert_array_equals", description,
+ "value is ${actual}, expected array",
+ {actual:actual});
+ assert(actual.length === expected.length,
+ "assert_array_equals", description,
+ "lengths differ, expected array ${expected} length ${expectedLength}, got ${actual} length ${actualLength}",
+ {expected:shorten_array(expected, expected.length - 1), expectedLength:expected.length,
+ actual:shorten_array(actual, actual.length - 1), actualLength:actual.length
+ });
+
+ for (var i = 0; i < actual.length; i++) {
+ assert(actual.hasOwnProperty(i) === expected.hasOwnProperty(i),
+ "assert_array_equals", description,
+ "expected property ${i} to be ${expected} but was ${actual} (expected array ${arrayExpected} got ${arrayActual})",
+ {i:i, expected:expected.hasOwnProperty(i) ? "present" : "missing",
+ actual:actual.hasOwnProperty(i) ? "present" : "missing",
+ arrayExpected:shorten_array(expected, i), arrayActual:shorten_array(actual, i)});
+ assert(same_value(expected[i], actual[i]),
+ "assert_array_equals", description,
+ "expected property ${i} to be ${expected} but got ${actual} (expected array ${arrayExpected} got ${arrayActual})",
+ {i:i, expected:expected[i], actual:actual[i],
+ arrayExpected:shorten_array(expected, i), arrayActual:shorten_array(actual, i)});
+ }
+ }
+ expose_assert(assert_array_equals, "assert_array_equals");
+
+ /**
+ * Assert that each array property in ``actual`` is a number within
+ * ± `epsilon` of the corresponding property in `expected`.
+ *
+ * @param {Array} actual - Array of test values.
+ * @param {Array} expected - Array of values expected to be close to the values in ``actual``.
+ * @param {number} epsilon - Magnitude of allowed difference
+ * between each value in ``actual`` and ``expected``.
+ * @param {string} [description] - Description of the condition being tested.
+ */
+ function assert_array_approx_equals(actual, expected, epsilon, description)
+ {
+ /*
+ * Test if two primitive arrays are equal within +/- epsilon
+ */
+ assert(actual.length === expected.length,
+ "assert_array_approx_equals", description,
+ "lengths differ, expected ${expected} got ${actual}",
+ {expected:expected.length, actual:actual.length});
+
+ for (var i = 0; i < actual.length; i++) {
+ assert(actual.hasOwnProperty(i) === expected.hasOwnProperty(i),
+ "assert_array_approx_equals", description,
+ "property ${i}, property expected to be ${expected} but was ${actual}",
+ {i:i, expected:expected.hasOwnProperty(i) ? "present" : "missing",
+ actual:actual.hasOwnProperty(i) ? "present" : "missing"});
+ assert(typeof actual[i] === "number",
+ "assert_array_approx_equals", description,
+ "property ${i}, expected a number but got a ${type_actual}",
+ {i:i, type_actual:typeof actual[i]});
+ assert(Math.abs(actual[i] - expected[i]) <= epsilon,
+ "assert_array_approx_equals", description,
+ "property ${i}, expected ${expected} +/- ${epsilon}, expected ${expected} but got ${actual}",
+ {i:i, expected:expected[i], actual:actual[i], epsilon:epsilon});
+ }
+ }
+ expose_assert(assert_array_approx_equals, "assert_array_approx_equals");
+
+ /**
+ * Assert that ``actual`` is within ± ``epsilon`` of ``expected``.
+ *
+ * @param {number} actual - Test value.
+ * @param {number} expected - Value number is expected to be close to.
+ * @param {number} epsilon - Magnitude of allowed difference between ``actual`` and ``expected``.
+ * @param {string} [description] - Description of the condition being tested.
+ */
+ function assert_approx_equals(actual, expected, epsilon, description)
+ {
+ /*
+ * Test if two primitive numbers are equal within +/- epsilon
+ */
+ assert(typeof actual === "number",
+ "assert_approx_equals", description,
+ "expected a number but got a ${type_actual}",
+ {type_actual:typeof actual});
+
+ // The epsilon math below does not place nice with NaN and Infinity
+ // But in this case Infinity = Infinity and NaN = NaN
+ if (isFinite(actual) || isFinite(expected)) {
+ assert(Math.abs(actual - expected) <= epsilon,
+ "assert_approx_equals", description,
+ "expected ${expected} +/- ${epsilon} but got ${actual}",
+ {expected:expected, actual:actual, epsilon:epsilon});
+ } else {
+ assert_equals(actual, expected);
+ }
+ }
+ expose_assert(assert_approx_equals, "assert_approx_equals");
+
+ /**
+ * Assert that ``actual`` is a number less than ``expected``.
+ *
+ * @param {number} actual - Test value.
+ * @param {number} expected - Number that ``actual`` must be less than.
+ * @param {string} [description] - Description of the condition being tested.
+ */
+ function assert_less_than(actual, expected, description)
+ {
+ /*
+ * Test if a primitive number is less than another
+ */
+ assert(typeof actual === "number",
+ "assert_less_than", description,
+ "expected a number but got a ${type_actual}",
+ {type_actual:typeof actual});
+
+ assert(actual < expected,
+ "assert_less_than", description,
+ "expected a number less than ${expected} but got ${actual}",
+ {expected:expected, actual:actual});
+ }
+ expose_assert(assert_less_than, "assert_less_than");
+
+ /**
+ * Assert that ``actual`` is a number greater than ``expected``.
+ *
+ * @param {number} actual - Test value.
+ * @param {number} expected - Number that ``actual`` must be greater than.
+ * @param {string} [description] - Description of the condition being tested.
+ */
+ function assert_greater_than(actual, expected, description)
+ {
+ /*
+ * Test if a primitive number is greater than another
+ */
+ assert(typeof actual === "number",
+ "assert_greater_than", description,
+ "expected a number but got a ${type_actual}",
+ {type_actual:typeof actual});
+
+ assert(actual > expected,
+ "assert_greater_than", description,
+ "expected a number greater than ${expected} but got ${actual}",
+ {expected:expected, actual:actual});
+ }
+ expose_assert(assert_greater_than, "assert_greater_than");
+
+ /**
+ * Assert that ``actual`` is a number greater than ``lower`` and less
+ * than ``upper`` but not equal to either.
+ *
+ * @param {number} actual - Test value.
+ * @param {number} lower - Number that ``actual`` must be greater than.
+ * @param {number} upper - Number that ``actual`` must be less than.
+ * @param {string} [description] - Description of the condition being tested.
+ */
+ function assert_between_exclusive(actual, lower, upper, description)
+ {
+ /*
+ * Test if a primitive number is between two others
+ */
+ assert(typeof actual === "number",
+ "assert_between_exclusive", description,
+ "expected a number but got a ${type_actual}",
+ {type_actual:typeof actual});
+
+ assert(actual > lower && actual < upper,
+ "assert_between_exclusive", description,
+ "expected a number greater than ${lower} " +
+ "and less than ${upper} but got ${actual}",
+ {lower:lower, upper:upper, actual:actual});
+ }
+ expose_assert(assert_between_exclusive, "assert_between_exclusive");
+
+ /**
+ * Assert that ``actual`` is a number less than or equal to ``expected``.
+ *
+ * @param {number} actual - Test value.
+ * @param {number} expected - Number that ``actual`` must be less
+ * than or equal to.
+ * @param {string} [description] - Description of the condition being tested.
+ */
+ function assert_less_than_equal(actual, expected, description)
+ {
+ /*
+ * Test if a primitive number is less than or equal to another
+ */
+ assert(typeof actual === "number",
+ "assert_less_than_equal", description,
+ "expected a number but got a ${type_actual}",
+ {type_actual:typeof actual});
+
+ assert(actual <= expected,
+ "assert_less_than_equal", description,
+ "expected a number less than or equal to ${expected} but got ${actual}",
+ {expected:expected, actual:actual});
+ }
+ expose_assert(assert_less_than_equal, "assert_less_than_equal");
+
+ /**
+ * Assert that ``actual`` is a number greater than or equal to ``expected``.
+ *
+ * @param {number} actual - Test value.
+ * @param {number} expected - Number that ``actual`` must be greater
+ * than or equal to.
+ * @param {string} [description] - Description of the condition being tested.
+ */
+ function assert_greater_than_equal(actual, expected, description)
+ {
+ /*
+ * Test if a primitive number is greater than or equal to another
+ */
+ assert(typeof actual === "number",
+ "assert_greater_than_equal", description,
+ "expected a number but got a ${type_actual}",
+ {type_actual:typeof actual});
+
+ assert(actual >= expected,
+ "assert_greater_than_equal", description,
+ "expected a number greater than or equal to ${expected} but got ${actual}",
+ {expected:expected, actual:actual});
+ }
+ expose_assert(assert_greater_than_equal, "assert_greater_than_equal");
+
+ /**
+ * Assert that ``actual`` is a number greater than or equal to ``lower`` and less
+ * than or equal to ``upper``.
+ *
+ * @param {number} actual - Test value.
+ * @param {number} lower - Number that ``actual`` must be greater than or equal to.
+ * @param {number} upper - Number that ``actual`` must be less than or equal to.
+ * @param {string} [description] - Description of the condition being tested.
+ */
+ function assert_between_inclusive(actual, lower, upper, description)
+ {
+ /*
+ * Test if a primitive number is between to two others or equal to either of them
+ */
+ assert(typeof actual === "number",
+ "assert_between_inclusive", description,
+ "expected a number but got a ${type_actual}",
+ {type_actual:typeof actual});
+
+ assert(actual >= lower && actual <= upper,
+ "assert_between_inclusive", description,
+ "expected a number greater than or equal to ${lower} " +
+ "and less than or equal to ${upper} but got ${actual}",
+ {lower:lower, upper:upper, actual:actual});
+ }
+ expose_assert(assert_between_inclusive, "assert_between_inclusive");
+
+ /**
+ * Assert that ``actual`` matches the RegExp ``expected``.
+ *
+ * @param {String} actual - Test string.
+ * @param {RegExp} expected - RegExp ``actual`` must match.
+ * @param {string} [description] - Description of the condition being tested.
+ */
+ function assert_regexp_match(actual, expected, description) {
+ /*
+ * Test if a string (actual) matches a regexp (expected)
+ */
+ assert(expected.test(actual),
+ "assert_regexp_match", description,
+ "expected ${expected} but got ${actual}",
+ {expected:expected, actual:actual});
+ }
+ expose_assert(assert_regexp_match, "assert_regexp_match");
+
+ /**
+ * Assert that the class string of ``object`` as returned in
+ * ``Object.prototype.toString`` is equal to ``class_name``.
+ *
+ * @param {Object} object - Object to stringify.
+ * @param {string} class_string - Expected class string for ``object``.
+ * @param {string} [description] - Description of the condition being tested.
+ */
+ function assert_class_string(object, class_string, description) {
+ var actual = {}.toString.call(object);
+ var expected = "[object " + class_string + "]";
+ assert(same_value(actual, expected), "assert_class_string", description,
+ "expected ${expected} but got ${actual}",
+ {expected:expected, actual:actual});
+ }
+ expose_assert(assert_class_string, "assert_class_string");
+
+ /**
+ * Assert that ``object`` has an own property with name ``property_name``.
+ *
+ * @param {Object} object - Object that should have the given property.
+ * @param {string} property_name - Expected property name.
+ * @param {string} [description] - Description of the condition being tested.
+ */
+ function assert_own_property(object, property_name, description) {
+ assert(object.hasOwnProperty(property_name),
+ "assert_own_property", description,
+ "expected property ${p} missing", {p:property_name});
+ }
+ expose_assert(assert_own_property, "assert_own_property");
+
+ /**
+ * Assert that ``object`` does not have an own property with name ``property_name``.
+ *
+ * @param {Object} object - Object that should not have the given property.
+ * @param {string} property_name - Property name to test.
+ * @param {string} [description] - Description of the condition being tested.
+ */
+ function assert_not_own_property(object, property_name, description) {
+ assert(!object.hasOwnProperty(property_name),
+ "assert_not_own_property", description,
+ "unexpected property ${p} is found on object", {p:property_name});
+ }
+ expose_assert(assert_not_own_property, "assert_not_own_property");
+
+ function _assert_inherits(name) {
+ return function (object, property_name, description)
+ {
+ assert((typeof object === "object" && object !== null) ||
+ typeof object === "function" ||
+ // Or has [[IsHTMLDDA]] slot
+ String(object) === "[object HTMLAllCollection]",
+ name, description,
+ "provided value is not an object");
+
+ assert("hasOwnProperty" in object,
+ name, description,
+ "provided value is an object but has no hasOwnProperty method");
+
+ assert(!object.hasOwnProperty(property_name),
+ name, description,
+ "property ${p} found on object expected in prototype chain",
+ {p:property_name});
+
+ assert(property_name in object,
+ name, description,
+ "property ${p} not found in prototype chain",
+ {p:property_name});
+ };
+ }
+
+ /**
+ * Assert that ``object`` does not have an own property with name
+ * ``property_name``, but inherits one through the prototype chain.
+ *
+ * @param {Object} object - Object that should have the given property in its prototype chain.
+ * @param {string} property_name - Expected property name.
+ * @param {string} [description] - Description of the condition being tested.
+ */
+ function assert_inherits(object, property_name, description) {
+ return _assert_inherits("assert_inherits")(object, property_name, description);
+ }
+ expose_assert(assert_inherits, "assert_inherits");
+
+ /**
+ * Alias for :js:func:`insert_inherits`.
+ *
+ * @param {Object} object - Object that should have the given property in its prototype chain.
+ * @param {string} property_name - Expected property name.
+ * @param {string} [description] - Description of the condition being tested.
+ */
+ function assert_idl_attribute(object, property_name, description) {
+ return _assert_inherits("assert_idl_attribute")(object, property_name, description);
+ }
+ expose_assert(assert_idl_attribute, "assert_idl_attribute");
+
+
+ /**
+ * Assert that ``object`` has a property named ``property_name`` and that the property is readonly.
+ *
+ * Note: The implementation tries to update the named property, so
+ * any side effects of updating will be triggered. Users are
+ * encouraged to instead inspect the property descriptor of ``property_name`` on ``object``.
+ *
+ * @param {Object} object - Object that should have the given property in its prototype chain.
+ * @param {string} property_name - Expected property name.
+ * @param {string} [description] - Description of the condition being tested.
+ */
+ function assert_readonly(object, property_name, description)
+ {
+ var initial_value = object[property_name];
+ try {
+ //Note that this can have side effects in the case where
+ //the property has PutForwards
+ object[property_name] = initial_value + "a"; //XXX use some other value here?
+ assert(same_value(object[property_name], initial_value),
+ "assert_readonly", description,
+ "changing property ${p} succeeded",
+ {p:property_name});
+ } finally {
+ object[property_name] = initial_value;
+ }
+ }
+ expose_assert(assert_readonly, "assert_readonly");
+
+ /**
+ * Assert a JS Error with the expected constructor is thrown.
+ *
+ * @param {object} constructor The expected exception constructor.
+ * @param {Function} func Function which should throw.
+ * @param {string} [description] Error description for the case that the error is not thrown.
+ */
+ function assert_throws_js(constructor, func, description)
+ {
+ assert_throws_js_impl(constructor, func, description,
+ "assert_throws_js");
+ }
+ expose_assert(assert_throws_js, "assert_throws_js");
+
+ /**
+ * Like assert_throws_js but allows specifying the assertion type
+ * (assert_throws_js or promise_rejects_js, in practice).
+ */
+ function assert_throws_js_impl(constructor, func, description,
+ assertion_type)
+ {
+ try {
+ func.call(this);
+ assert(false, assertion_type, description,
+ "${func} did not throw", {func:func});
+ } catch (e) {
+ if (e instanceof AssertionError) {
+ throw e;
+ }
+
+ // Basic sanity-checks on the thrown exception.
+ assert(typeof e === "object",
+ assertion_type, description,
+ "${func} threw ${e} with type ${type}, not an object",
+ {func:func, e:e, type:typeof e});
+
+ assert(e !== null,
+ assertion_type, description,
+ "${func} threw null, not an object",
+ {func:func});
+
+ // Basic sanity-check on the passed-in constructor
+ assert(typeof constructor == "function",
+ assertion_type, description,
+ "${constructor} is not a constructor",
+ {constructor:constructor});
+ var obj = constructor;
+ while (obj) {
+ if (typeof obj === "function" &&
+ obj.name === "Error") {
+ break;
+ }
+ obj = Object.getPrototypeOf(obj);
+ }
+ assert(obj != null,
+ assertion_type, description,
+ "${constructor} is not an Error subtype",
+ {constructor:constructor});
+
+ // And checking that our exception is reasonable
+ assert(e.constructor === constructor &&
+ e.name === constructor.name,
+ assertion_type, description,
+ "${func} threw ${actual} (${actual_name}) expected instance of ${expected} (${expected_name})",
+ {func:func, actual:e, actual_name:e.name,
+ expected:constructor,
+ expected_name:constructor.name});
+ }
+ }
+
+ // TODO: Figure out how to document the overloads better.
+ // sphinx-js doesn't seem to handle @variation correctly,
+ // and only expects a single JSDoc entry per function.
+ /**
+ * Assert a DOMException with the expected type is thrown.
+ *
+ * There are two ways of calling assert_throws_dom:
+ *
+ * 1) If the DOMException is expected to come from the current global, the
+ * second argument should be the function expected to throw and a third,
+ * optional, argument is the assertion description.
+ *
+ * 2) If the DOMException is expected to come from some other global, the
+ * second argument should be the DOMException constructor from that global,
+ * the third argument the function expected to throw, and the fourth, optional,
+ * argument the assertion description.
+ *
+ * @param {number|string} type - The expected exception name or
+ * code. See the `table of names and codes
+ * <https://webidl.spec.whatwg.org/#dfn-error-names-table>`_. If a
+ * number is passed it should be one of the numeric code values in
+ * that table (e.g. 3, 4, etc). If a string is passed it can
+ * either be an exception name (e.g. "HierarchyRequestError",
+ * "WrongDocumentError") or the name of the corresponding error
+ * code (e.g. "``HIERARCHY_REQUEST_ERR``", "``WRONG_DOCUMENT_ERR``").
+ * @param {Function} descriptionOrFunc - The function expected to
+ * throw (if the exception comes from another global), or the
+ * optional description of the condition being tested (if the
+ * exception comes from the current global).
+ * @param {string} [description] - Description of the condition
+ * being tested (if the exception comes from another global).
+ *
+ */
+ function assert_throws_dom(type, funcOrConstructor, descriptionOrFunc, maybeDescription)
+ {
+ let constructor, func, description;
+ if (funcOrConstructor.name === "DOMException") {
+ constructor = funcOrConstructor;
+ func = descriptionOrFunc;
+ description = maybeDescription;
+ } else {
+ constructor = self.DOMException;
+ func = funcOrConstructor;
+ description = descriptionOrFunc;
+ assert(maybeDescription === undefined,
+ "Too many args pased to no-constructor version of assert_throws_dom");
+ }
+ assert_throws_dom_impl(type, func, description, "assert_throws_dom", constructor)
+ }
+ expose_assert(assert_throws_dom, "assert_throws_dom");
+
+ /**
+ * Similar to assert_throws_dom but allows specifying the assertion type
+ * (assert_throws_dom or promise_rejects_dom, in practice). The
+ * "constructor" argument must be the DOMException constructor from the
+ * global we expect the exception to come from.
+ */
+ function assert_throws_dom_impl(type, func, description, assertion_type, constructor)
+ {
+ try {
+ func.call(this);
+ assert(false, assertion_type, description,
+ "${func} did not throw", {func:func});
+ } catch (e) {
+ if (e instanceof AssertionError) {
+ throw e;
+ }
+
+ // Basic sanity-checks on the thrown exception.
+ assert(typeof e === "object",
+ assertion_type, description,
+ "${func} threw ${e} with type ${type}, not an object",
+ {func:func, e:e, type:typeof e});
+
+ assert(e !== null,
+ assertion_type, description,
+ "${func} threw null, not an object",
+ {func:func});
+
+ // Sanity-check our type
+ assert(typeof type == "number" ||
+ typeof type == "string",
+ assertion_type, description,
+ "${type} is not a number or string",
+ {type:type});
+
+ var codename_name_map = {
+ INDEX_SIZE_ERR: 'IndexSizeError',
+ HIERARCHY_REQUEST_ERR: 'HierarchyRequestError',
+ WRONG_DOCUMENT_ERR: 'WrongDocumentError',
+ INVALID_CHARACTER_ERR: 'InvalidCharacterError',
+ NO_MODIFICATION_ALLOWED_ERR: 'NoModificationAllowedError',
+ NOT_FOUND_ERR: 'NotFoundError',
+ NOT_SUPPORTED_ERR: 'NotSupportedError',
+ INUSE_ATTRIBUTE_ERR: 'InUseAttributeError',
+ INVALID_STATE_ERR: 'InvalidStateError',
+ SYNTAX_ERR: 'SyntaxError',
+ INVALID_MODIFICATION_ERR: 'InvalidModificationError',
+ NAMESPACE_ERR: 'NamespaceError',
+ INVALID_ACCESS_ERR: 'InvalidAccessError',
+ TYPE_MISMATCH_ERR: 'TypeMismatchError',
+ SECURITY_ERR: 'SecurityError',
+ NETWORK_ERR: 'NetworkError',
+ ABORT_ERR: 'AbortError',
+ URL_MISMATCH_ERR: 'URLMismatchError',
+ QUOTA_EXCEEDED_ERR: 'QuotaExceededError',
+ TIMEOUT_ERR: 'TimeoutError',
+ INVALID_NODE_TYPE_ERR: 'InvalidNodeTypeError',
+ DATA_CLONE_ERR: 'DataCloneError'
+ };
+
+ var name_code_map = {
+ IndexSizeError: 1,
+ HierarchyRequestError: 3,
+ WrongDocumentError: 4,
+ InvalidCharacterError: 5,
+ NoModificationAllowedError: 7,
+ NotFoundError: 8,
+ NotSupportedError: 9,
+ InUseAttributeError: 10,
+ InvalidStateError: 11,
+ SyntaxError: 12,
+ InvalidModificationError: 13,
+ NamespaceError: 14,
+ InvalidAccessError: 15,
+ TypeMismatchError: 17,
+ SecurityError: 18,
+ NetworkError: 19,
+ AbortError: 20,
+ URLMismatchError: 21,
+ QuotaExceededError: 22,
+ TimeoutError: 23,
+ InvalidNodeTypeError: 24,
+ DataCloneError: 25,
+
+ EncodingError: 0,
+ NotReadableError: 0,
+ UnknownError: 0,
+ ConstraintError: 0,
+ DataError: 0,
+ TransactionInactiveError: 0,
+ ReadOnlyError: 0,
+ VersionError: 0,
+ OperationError: 0,
+ NotAllowedError: 0,
+ OptOutError: 0
+ };
+
+ var code_name_map = {};
+ for (var key in name_code_map) {
+ if (name_code_map[key] > 0) {
+ code_name_map[name_code_map[key]] = key;
+ }
+ }
+
+ var required_props = {};
+ var name;
+
+ if (typeof type === "number") {
+ if (type === 0) {
+ throw new AssertionError('Test bug: ambiguous DOMException code 0 passed to assert_throws_dom()');
+ } else if (!(type in code_name_map)) {
+ throw new AssertionError('Test bug: unrecognized DOMException code "' + type + '" passed to assert_throws_dom()');
+ }
+ name = code_name_map[type];
+ required_props.code = type;
+ } else if (typeof type === "string") {
+ name = type in codename_name_map ? codename_name_map[type] : type;
+ if (!(name in name_code_map)) {
+ throw new AssertionError('Test bug: unrecognized DOMException code name or name "' + type + '" passed to assert_throws_dom()');
+ }
+
+ required_props.code = name_code_map[name];
+ }
+
+ if (required_props.code === 0 ||
+ ("name" in e &&
+ e.name !== e.name.toUpperCase() &&
+ e.name !== "DOMException")) {
+ // New style exception: also test the name property.
+ required_props.name = name;
+ }
+
+ for (var prop in required_props) {
+ assert(prop in e && e[prop] == required_props[prop],
+ assertion_type, description,
+ "${func} threw ${e} that is not a DOMException " + type + ": property ${prop} is equal to ${actual}, expected ${expected}",
+ {func:func, e:e, prop:prop, actual:e[prop], expected:required_props[prop]});
+ }
+
+ // Check that the exception is from the right global. This check is last
+ // so more specific, and more informative, checks on the properties can
+ // happen in case a totally incorrect exception is thrown.
+ assert(e.constructor === constructor,
+ assertion_type, description,
+ "${func} threw an exception from the wrong global",
+ {func});
+
+ }
+ }
+
+ /**
+ * Assert the provided value is thrown.
+ *
+ * @param {value} exception The expected exception.
+ * @param {Function} func Function which should throw.
+ * @param {string} [description] Error description for the case that the error is not thrown.
+ */
+ function assert_throws_exactly(exception, func, description)
+ {
+ assert_throws_exactly_impl(exception, func, description,
+ "assert_throws_exactly");
+ }
+ expose_assert(assert_throws_exactly, "assert_throws_exactly");
+
+ /**
+ * Like assert_throws_exactly but allows specifying the assertion type
+ * (assert_throws_exactly or promise_rejects_exactly, in practice).
+ */
+ function assert_throws_exactly_impl(exception, func, description,
+ assertion_type)
+ {
+ try {
+ func.call(this);
+ assert(false, assertion_type, description,
+ "${func} did not throw", {func:func});
+ } catch (e) {
+ if (e instanceof AssertionError) {
+ throw e;
+ }
+
+ assert(same_value(e, exception), assertion_type, description,
+ "${func} threw ${e} but we expected it to throw ${exception}",
+ {func:func, e:e, exception:exception});
+ }
+ }
+
+ /**
+ * Asserts if called. Used to ensure that a specific codepath is
+ * not taken e.g. that an error event isn't fired.
+ *
+ * @param {string} [description] - Description of the condition being tested.
+ */
+ function assert_unreached(description) {
+ assert(false, "assert_unreached", description,
+ "Reached unreachable code");
+ }
+ expose_assert(assert_unreached, "assert_unreached");
+
+ /**
+ * @callback AssertFunc
+ * @param {Any} actual
+ * @param {Any} expected
+ * @param {Any[]} args
+ */
+
+ /**
+ * Asserts that ``actual`` matches at least one value of ``expected``
+ * according to a comparison defined by ``assert_func``.
+ *
+ * Note that tests with multiple allowed pass conditions are bad
+ * practice unless the spec specifically allows multiple
+ * behaviours. Test authors should not use this method simply to
+ * hide UA bugs.
+ *
+ * @param {AssertFunc} assert_func - Function to compare actual
+ * and expected. It must throw when the comparison fails and
+ * return when the comparison passes.
+ * @param {Any} actual - Test value.
+ * @param {Array} expected_array - Array of possible expected values.
+ * @param {Any[]} args - Additional arguments to pass to ``assert_func``.
+ */
+ function assert_any(assert_func, actual, expected_array, ...args)
+ {
+ var errors = [];
+ var passed = false;
+ forEach(expected_array,
+ function(expected)
+ {
+ try {
+ assert_func.apply(this, [actual, expected].concat(args));
+ passed = true;
+ } catch (e) {
+ errors.push(e.message);
+ }
+ });
+ if (!passed) {
+ throw new AssertionError(errors.join("\n\n"));
+ }
+ }
+ // FIXME: assert_any cannot use expose_assert, because assert_wrapper does
+ // not support nested assert calls (e.g. to assert_func). We need to
+ // support bypassing assert_wrapper for the inner asserts here.
+ expose(assert_any, "assert_any");
+
+ /**
+ * Assert that a feature is implemented, based on a 'truthy' condition.
+ *
+ * This function should be used to early-exit from tests in which there is
+ * no point continuing without support for a non-optional spec or spec
+ * feature. For example:
+ *
+ * assert_implements(window.Foo, 'Foo is not supported');
+ *
+ * @param {object} condition The truthy value to test
+ * @param {string} [description] Error description for the case that the condition is not truthy.
+ */
+ function assert_implements(condition, description) {
+ assert(!!condition, "assert_implements", description);
+ }
+ expose_assert(assert_implements, "assert_implements")
+
+ /**
+ * Assert that an optional feature is implemented, based on a 'truthy' condition.
+ *
+ * This function should be used to early-exit from tests in which there is
+ * no point continuing without support for an explicitly optional spec or
+ * spec feature. For example:
+ *
+ * assert_implements_optional(video.canPlayType("video/webm"),
+ * "webm video playback not supported");
+ *
+ * @param {object} condition The truthy value to test
+ * @param {string} [description] Error description for the case that the condition is not truthy.
+ */
+ function assert_implements_optional(condition, description) {
+ if (!condition) {
+ throw new OptionalFeatureUnsupportedError(description);
+ }
+ }
+ expose_assert(assert_implements_optional, "assert_implements_optional");
+
+ /**
+ * @class
+ *
+ * A single subtest. A Test is not constructed directly but via the
+ * :js:func:`test`, :js:func:`async_test` or :js:func:`promise_test` functions.
+ *
+ * @param {string} name - This must be unique in a given file and must be
+ * invariant between runs.
+ *
+ */
+ function Test(name, properties)
+ {
+ if (tests.file_is_test && tests.tests.length) {
+ throw new Error("Tried to create a test with file_is_test");
+ }
+ /** The test name. */
+ this.name = name;
+
+ this.phase = (tests.is_aborted || tests.phase === tests.phases.COMPLETE) ?
+ this.phases.COMPLETE : this.phases.INITIAL;
+
+ /** The test status code.*/
+ this.status = this.NOTRUN;
+ this.timeout_id = null;
+ this.index = null;
+
+ this.properties = properties || {};
+ this.timeout_length = settings.test_timeout;
+ if (this.timeout_length !== null) {
+ this.timeout_length *= tests.timeout_multiplier;
+ }
+
+ /** A message indicating the reason for test failure. */
+ this.message = null;
+ /** Stack trace in case of failure. */
+ this.stack = null;
+
+ this.steps = [];
+ this._is_promise_test = false;
+
+ this.cleanup_callbacks = [];
+ this._user_defined_cleanup_count = 0;
+ this._done_callbacks = [];
+
+ if (typeof AbortController === "function") {
+ this._abortController = new AbortController();
+ }
+
+ // Tests declared following harness completion are likely an indication
+ // of a programming error, but they cannot be reported
+ // deterministically.
+ if (tests.phase === tests.phases.COMPLETE) {
+ return;
+ }
+
+ tests.push(this);
+ }
+
+ /**
+ * Enum of possible test statuses.
+ *
+ * :values:
+ * - ``PASS``
+ * - ``FAIL``
+ * - ``TIMEOUT``
+ * - ``NOTRUN``
+ * - ``PRECONDITION_FAILED``
+ */
+ Test.statuses = {
+ PASS:0,
+ FAIL:1,
+ TIMEOUT:2,
+ NOTRUN:3,
+ PRECONDITION_FAILED:4
+ };
+
+ Test.prototype = merge({}, Test.statuses);
+
+ Test.prototype.phases = {
+ INITIAL:0,
+ STARTED:1,
+ HAS_RESULT:2,
+ CLEANING:3,
+ COMPLETE:4
+ };
+
+ Test.prototype.status_formats = {
+ 0: "Pass",
+ 1: "Fail",
+ 2: "Timeout",
+ 3: "Not Run",
+ 4: "Optional Feature Unsupported",
+ }
+
+ Test.prototype.format_status = function() {
+ return this.status_formats[this.status];
+ }
+
+ Test.prototype.structured_clone = function()
+ {
+ if (!this._structured_clone) {
+ var msg = this.message;
+ msg = msg ? String(msg) : msg;
+ this._structured_clone = merge({
+ name:String(this.name),
+ properties:merge({}, this.properties),
+ phases:merge({}, this.phases)
+ }, Test.statuses);
+ }
+ this._structured_clone.status = this.status;
+ this._structured_clone.message = this.message;
+ this._structured_clone.stack = this.stack;
+ this._structured_clone.index = this.index;
+ this._structured_clone.phase = this.phase;
+ return this._structured_clone;
+ };
+
+ /**
+ * Run a single step of an ongoing test.
+ *
+ * @param {string} func - Callback function to run as a step. If
+ * this throws an :js:func:`AssertionError`, or any other
+ * exception, the :js:class:`Test` status is set to ``FAIL``.
+ * @param {Object} [this_obj] - The object to use as the this
+ * value when calling ``func``. Defaults to the :js:class:`Test` object.
+ */
+ Test.prototype.step = function(func, this_obj)
+ {
+ if (this.phase > this.phases.STARTED) {
+ return;
+ }
+
+ if (settings.debug && this.phase !== this.phases.STARTED) {
+ console.log("TEST START", this.name);
+ }
+ this.phase = this.phases.STARTED;
+ //If we don't get a result before the harness times out that will be a test timeout
+ this.set_status(this.TIMEOUT, "Test timed out");
+
+ tests.started = true;
+ tests.current_test = this;
+ tests.notify_test_state(this);
+
+ if (this.timeout_id === null) {
+ this.set_timeout();
+ }
+
+ this.steps.push(func);
+
+ if (arguments.length === 1) {
+ this_obj = this;
+ }
+
+ if (settings.debug) {
+ console.debug("TEST STEP", this.name);
+ }
+
+ try {
+ return func.apply(this_obj, Array.prototype.slice.call(arguments, 2));
+ } catch (e) {
+ if (this.phase >= this.phases.HAS_RESULT) {
+ return;
+ }
+ var status = e instanceof OptionalFeatureUnsupportedError ? this.PRECONDITION_FAILED : this.FAIL;
+ var message = String((typeof e === "object" && e !== null) ? e.message : e);
+ var stack = e.stack ? e.stack : null;
+
+ this.set_status(status, message, stack);
+ this.phase = this.phases.HAS_RESULT;
+ this.done();
+ } finally {
+ this.current_test = null;
+ }
+ };
+
+ /**
+ * Wrap a function so that it runs as a step of the current test.
+ *
+ * This allows creating a callback function that will run as a
+ * test step.
+ *
+ * @example
+ * let t = async_test("Example");
+ * onload = t.step_func(e => {
+ * assert_equals(e.name, "load");
+ * // Mark the test as complete.
+ * t.done();
+ * })
+ *
+ * @param {string} func - Function to run as a step. If this
+ * throws an :js:func:`AssertionError`, or any other exception,
+ * the :js:class:`Test` status is set to ``FAIL``.
+ * @param {Object} [this_obj] - The object to use as the this
+ * value when calling ``func``. Defaults to the :js:class:`Test` object.
+ */
+ Test.prototype.step_func = function(func, this_obj)
+ {
+ var test_this = this;
+
+ if (arguments.length === 1) {
+ this_obj = test_this;
+ }
+
+ return function()
+ {
+ return test_this.step.apply(test_this, [func, this_obj].concat(
+ Array.prototype.slice.call(arguments)));
+ };
+ };
+
+ /**
+ * Wrap a function so that it runs as a step of the current test,
+ * and automatically marks the test as complete if the function
+ * returns without error.
+ *
+ * @param {string} func - Function to run as a step. If this
+ * throws an :js:func:`AssertionError`, or any other exception,
+ * the :js:class:`Test` status is set to ``FAIL``. If it returns
+ * without error the status is set to ``PASS``.
+ * @param {Object} [this_obj] - The object to use as the this
+ * value when calling `func`. Defaults to the :js:class:`Test` object.
+ */
+ Test.prototype.step_func_done = function(func, this_obj)
+ {
+ var test_this = this;
+
+ if (arguments.length === 1) {
+ this_obj = test_this;
+ }
+
+ return function()
+ {
+ if (func) {
+ test_this.step.apply(test_this, [func, this_obj].concat(
+ Array.prototype.slice.call(arguments)));
+ }
+ test_this.done();
+ };
+ };
+
+ /**
+ * Return a function that automatically sets the current test to
+ * ``FAIL`` if it's called.
+ *
+ * @param {string} [description] - Error message to add to assert
+ * in case of failure.
+ *
+ */
+ Test.prototype.unreached_func = function(description)
+ {
+ return this.step_func(function() {
+ assert_unreached(description);
+ });
+ };
+
+ /**
+ * Run a function as a step of the test after a given timeout.
+ *
+ * This multiplies the timeout by the global timeout multiplier to
+ * account for the expected execution speed of the current test
+ * environment. For example ``test.step_timeout(f, 2000)`` with a
+ * timeout multiplier of 2 will wait for 4000ms before calling ``f``.
+ *
+ * In general it's encouraged to use :js:func:`Test.step_wait` or
+ * :js:func:`step_wait_func` in preference to this function where possible,
+ * as they provide better test performance.
+ *
+ * @param {Function} func - Function to run as a test
+ * step.
+ * @param {number} timeout - Time in ms to wait before running the
+ * test step. The actual wait time is ``timeout`` x
+ * ``timeout_multiplier``.
+ *
+ */
+ Test.prototype.step_timeout = function(func, timeout) {
+ var test_this = this;
+ var args = Array.prototype.slice.call(arguments, 2);
+ return setTimeout(this.step_func(function() {
+ return func.apply(test_this, args);
+ }), timeout * tests.timeout_multiplier);
+ };
+
+ /**
+ * Poll for a function to return true, and call a callback
+ * function once it does, or assert if a timeout is
+ * reached. This is preferred over a simple step_timeout
+ * whenever possible since it allows the timeout to be longer
+ * to reduce intermittents without compromising test execution
+ * speed when the condition is quickly met.
+ *
+ * @param {Function} cond A function taking no arguments and
+ * returning a boolean. The callback is called
+ * when this function returns true.
+ * @param {Function} func A function taking no arguments to call once
+ * the condition is met.
+ * @param {string} [description] Error message to add to assert in case of
+ * failure.
+ * @param {number} timeout Timeout in ms. This is multiplied by the global
+ * timeout_multiplier
+ * @param {number} interval Polling interval in ms
+ *
+ */
+ Test.prototype.step_wait_func = function(cond, func, description,
+ timeout=3000, interval=100) {
+ var timeout_full = timeout * tests.timeout_multiplier;
+ var remaining = Math.ceil(timeout_full / interval);
+ var test_this = this;
+
+ var wait_for_inner = test_this.step_func(() => {
+ if (cond()) {
+ func();
+ } else {
+ if(remaining === 0) {
+ assert(false, "step_wait_func", description,
+ "Timed out waiting on condition");
+ }
+ remaining--;
+ setTimeout(wait_for_inner, interval);
+ }
+ });
+
+ wait_for_inner();
+ };
+
+ /**
+ * Poll for a function to return true, and invoke a callback
+ * followed by this.done() once it does, or assert if a timeout
+ * is reached. This is preferred over a simple step_timeout
+ * whenever possible since it allows the timeout to be longer
+ * to reduce intermittents without compromising test execution speed
+ * when the condition is quickly met.
+ *
+ * @example
+ * async_test(t => {
+ * const popup = window.open("resources/coop-coep.py?coop=same-origin&coep=&navigate=about:blank");
+ * t.add_cleanup(() => popup.close());
+ * assert_equals(window, popup.opener);
+ *
+ * popup.onload = t.step_func(() => {
+ * assert_true(popup.location.href.endsWith("&navigate=about:blank"));
+ * // Use step_wait_func_done as about:blank cannot message back.
+ * t.step_wait_func_done(() => popup.location.href === "about:blank");
+ * });
+ * }, "Navigating a popup to about:blank");
+ *
+ * @param {Function} cond A function taking no arguments and
+ * returning a boolean. The callback is called
+ * when this function returns true.
+ * @param {Function} func A function taking no arguments to call once
+ * the condition is met.
+ * @param {string} [description] Error message to add to assert in case of
+ * failure.
+ * @param {number} timeout Timeout in ms. This is multiplied by the global
+ * timeout_multiplier
+ * @param {number} interval Polling interval in ms
+ *
+ */
+ Test.prototype.step_wait_func_done = function(cond, func, description,
+ timeout=3000, interval=100) {
+ this.step_wait_func(cond, () => {
+ if (func) {
+ func();
+ }
+ this.done();
+ }, description, timeout, interval);
+ };
+
+ /**
+ * Poll for a function to return true, and resolve a promise
+ * once it does, or assert if a timeout is reached. This is
+ * preferred over a simple step_timeout whenever possible
+ * since it allows the timeout to be longer to reduce
+ * intermittents without compromising test execution speed
+ * when the condition is quickly met.
+ *
+ * @example
+ * promise_test(async t => {
+ * // …
+ * await t.step_wait(() => frame.contentDocument === null, "Frame navigated to a cross-origin document");
+ * // …
+ * }, "");
+ *
+ * @param {Function} cond A function taking no arguments and
+ * returning a boolean.
+ * @param {string} [description] Error message to add to assert in case of
+ * failure.
+ * @param {number} timeout Timeout in ms. This is multiplied by the global
+ * timeout_multiplier
+ * @param {number} interval Polling interval in ms
+ * @returns {Promise} Promise resolved once cond is met.
+ *
+ */
+ Test.prototype.step_wait = function(cond, description, timeout=3000, interval=100) {
+ return new Promise(resolve => {
+ this.step_wait_func(cond, resolve, description, timeout, interval);
+ });
+ }
+
+ /*
+ * Private method for registering cleanup functions. `testharness.js`
+ * internals should use this method instead of the public `add_cleanup`
+ * method in order to hide implementation details from the harness status
+ * message in the case errors.
+ */
+ Test.prototype._add_cleanup = function(callback) {
+ this.cleanup_callbacks.push(callback);
+ };
+
+ /**
+ * Schedule a function to be run after the test result is known, regardless
+ * of passing or failing state.
+ *
+ * The behavior of this function will not
+ * influence the result of the test, but if an exception is thrown, the
+ * test harness will report an error.
+ *
+ * @param {Function} callback - The cleanup function to run. This
+ * is called with no arguments.
+ */
+ Test.prototype.add_cleanup = function(callback) {
+ this._user_defined_cleanup_count += 1;
+ this._add_cleanup(callback);
+ };
+
+ Test.prototype.set_timeout = function()
+ {
+ if (this.timeout_length !== null) {
+ var this_obj = this;
+ this.timeout_id = setTimeout(function()
+ {
+ this_obj.timeout();
+ }, this.timeout_length);
+ }
+ };
+
+ Test.prototype.set_status = function(status, message, stack)
+ {
+ this.status = status;
+ this.message = message;
+ this.stack = stack ? stack : null;
+ };
+
+ /**
+ * Manually set the test status to ``TIMEOUT``.
+ */
+ Test.prototype.timeout = function()
+ {
+ this.timeout_id = null;
+ this.set_status(this.TIMEOUT, "Test timed out");
+ this.phase = this.phases.HAS_RESULT;
+ this.done();
+ };
+
+ /**
+ * Manually set the test status to ``TIMEOUT``.
+ *
+ * Alias for `Test.timeout <#Test.timeout>`_.
+ */
+ Test.prototype.force_timeout = function() {
+ return this.timeout();
+ };
+
+ /**
+ * Mark the test as complete.
+ *
+ * This sets the test status to ``PASS`` if no other status was
+ * already recorded. Any subsequent attempts to run additional
+ * test steps will be ignored.
+ *
+ * After setting the test status any test cleanup functions will
+ * be run.
+ */
+ Test.prototype.done = function()
+ {
+ if (this.phase >= this.phases.CLEANING) {
+ return;
+ }
+
+ if (this.phase <= this.phases.STARTED) {
+ this.set_status(this.PASS, null);
+ }
+
+ if (global_scope.clearTimeout) {
+ clearTimeout(this.timeout_id);
+ }
+
+ if (settings.debug) {
+ console.log("TEST DONE",
+ this.status,
+ this.name);
+ }
+
+ this.cleanup();
+ };
+
+ function add_test_done_callback(test, callback)
+ {
+ if (test.phase === test.phases.COMPLETE) {
+ callback();
+ return;
+ }
+
+ test._done_callbacks.push(callback);
+ }
+
+ /*
+ * Invoke all specified cleanup functions. If one or more produce an error,
+ * the context is in an unpredictable state, so all further testing should
+ * be cancelled.
+ */
+ Test.prototype.cleanup = function() {
+ var errors = [];
+ var bad_value_count = 0;
+ function on_error(e) {
+ errors.push(e);
+ // Abort tests immediately so that tests declared within subsequent
+ // cleanup functions are not run.
+ tests.abort();
+ }
+ var this_obj = this;
+ var results = [];
+
+ this.phase = this.phases.CLEANING;
+
+ if (this._abortController) {
+ this._abortController.abort("Test cleanup");
+ }
+
+ forEach(this.cleanup_callbacks,
+ function(cleanup_callback) {
+ var result;
+
+ try {
+ result = cleanup_callback();
+ } catch (e) {
+ on_error(e);
+ return;
+ }
+
+ if (!is_valid_cleanup_result(this_obj, result)) {
+ bad_value_count += 1;
+ // Abort tests immediately so that tests declared
+ // within subsequent cleanup functions are not run.
+ tests.abort();
+ }
+
+ results.push(result);
+ });
+
+ if (!this._is_promise_test) {
+ cleanup_done(this_obj, errors, bad_value_count);
+ } else {
+ all_async(results,
+ function(result, done) {
+ if (result && typeof result.then === "function") {
+ result
+ .then(null, on_error)
+ .then(done);
+ } else {
+ done();
+ }
+ },
+ function() {
+ cleanup_done(this_obj, errors, bad_value_count);
+ });
+ }
+ };
+
+ /*
+ * Determine if the return value of a cleanup function is valid for a given
+ * test. Any test may return the value `undefined`. Tests created with
+ * `promise_test` may alternatively return "thenable" object values.
+ */
+ function is_valid_cleanup_result(test, result) {
+ if (result === undefined) {
+ return true;
+ }
+
+ if (test._is_promise_test) {
+ return result && typeof result.then === "function";
+ }
+
+ return false;
+ }
+
+ function cleanup_done(test, errors, bad_value_count) {
+ if (errors.length || bad_value_count) {
+ var total = test._user_defined_cleanup_count;
+
+ tests.status.status = tests.status.ERROR;
+ tests.status.stack = null;
+ tests.status.message = "Test named '" + test.name +
+ "' specified " + total +
+ " 'cleanup' function" + (total > 1 ? "s" : "");
+
+ if (errors.length) {
+ tests.status.message += ", and " + errors.length + " failed";
+ tests.status.stack = ((typeof errors[0] === "object" &&
+ errors[0].hasOwnProperty("stack")) ?
+ errors[0].stack : null);
+ }
+
+ if (bad_value_count) {
+ var type = test._is_promise_test ?
+ "non-thenable" : "non-undefined";
+ tests.status.message += ", and " + bad_value_count +
+ " returned a " + type + " value";
+ }
+
+ tests.status.message += ".";
+ }
+
+ test.phase = test.phases.COMPLETE;
+ tests.result(test);
+ forEach(test._done_callbacks,
+ function(callback) {
+ callback();
+ });
+ test._done_callbacks.length = 0;
+ }
+
+ /**
+ * Gives an AbortSignal that will be aborted when the test finishes.
+ */
+ Test.prototype.get_signal = function() {
+ if (!this._abortController) {
+ throw new Error("AbortController is not supported in this browser");
+ }
+ return this._abortController.signal;
+ }
+
+ /**
+ * A RemoteTest object mirrors a Test object on a remote worker. The
+ * associated RemoteWorker updates the RemoteTest object in response to
+ * received events. In turn, the RemoteTest object replicates these events
+ * on the local document. This allows listeners (test result reporting
+ * etc..) to transparently handle local and remote events.
+ */
+ function RemoteTest(clone) {
+ var this_obj = this;
+ Object.keys(clone).forEach(
+ function(key) {
+ this_obj[key] = clone[key];
+ });
+ this.index = null;
+ this.phase = this.phases.INITIAL;
+ this.update_state_from(clone);
+ this._done_callbacks = [];
+ tests.push(this);
+ }
+
+ RemoteTest.prototype.structured_clone = function() {
+ var clone = {};
+ Object.keys(this).forEach(
+ (function(key) {
+ var value = this[key];
+ // `RemoteTest` instances are responsible for managing
+ // their own "done" callback functions, so those functions
+ // are not relevant in other execution contexts. Because of
+ // this (and because Function values cannot be serialized
+ // for cross-realm transmittance), the property should not
+ // be considered when cloning instances.
+ if (key === '_done_callbacks' ) {
+ return;
+ }
+
+ if (typeof value === "object" && value !== null) {
+ clone[key] = merge({}, value);
+ } else {
+ clone[key] = value;
+ }
+ }).bind(this));
+ clone.phases = merge({}, this.phases);
+ return clone;
+ };
+
+ /**
+ * `RemoteTest` instances are objects which represent tests running in
+ * another realm. They do not define "cleanup" functions (if necessary,
+ * such functions are defined on the associated `Test` instance within the
+ * external realm). However, `RemoteTests` may have "done" callbacks (e.g.
+ * as attached by the `Tests` instance responsible for tracking the overall
+ * test status in the parent realm). The `cleanup` method delegates to
+ * `done` in order to ensure that such callbacks are invoked following the
+ * completion of the `RemoteTest`.
+ */
+ RemoteTest.prototype.cleanup = function() {
+ this.done();
+ };
+ RemoteTest.prototype.phases = Test.prototype.phases;
+ RemoteTest.prototype.update_state_from = function(clone) {
+ this.status = clone.status;
+ this.message = clone.message;
+ this.stack = clone.stack;
+ if (this.phase === this.phases.INITIAL) {
+ this.phase = this.phases.STARTED;
+ }
+ };
+ RemoteTest.prototype.done = function() {
+ this.phase = this.phases.COMPLETE;
+
+ forEach(this._done_callbacks,
+ function(callback) {
+ callback();
+ });
+ }
+
+ RemoteTest.prototype.format_status = function() {
+ return Test.prototype.status_formats[this.status];
+ }
+
+ /*
+ * A RemoteContext listens for test events from a remote test context, such
+ * as another window or a worker. These events are then used to construct
+ * and maintain RemoteTest objects that mirror the tests running in the
+ * remote context.
+ *
+ * An optional third parameter can be used as a predicate to filter incoming
+ * MessageEvents.
+ */
+ function RemoteContext(remote, message_target, message_filter) {
+ this.running = true;
+ this.started = false;
+ this.tests = new Array();
+ this.early_exception = null;
+
+ var this_obj = this;
+ // If remote context is cross origin assigning to onerror is not
+ // possible, so silently catch those errors.
+ try {
+ remote.onerror = function(error) { this_obj.remote_error(error); };
+ } catch (e) {
+ // Ignore.
+ }
+
+ // Keeping a reference to the remote object and the message handler until
+ // remote_done() is seen prevents the remote object and its message channel
+ // from going away before all the messages are dispatched.
+ this.remote = remote;
+ this.message_target = message_target;
+ this.message_handler = function(message) {
+ var passesFilter = !message_filter || message_filter(message);
+ // The reference to the `running` property in the following
+ // condition is unnecessary because that value is only set to
+ // `false` after the `message_handler` function has been
+ // unsubscribed.
+ // TODO: Simplify the condition by removing the reference.
+ if (this_obj.running && message.data && passesFilter &&
+ (message.data.type in this_obj.message_handlers)) {
+ this_obj.message_handlers[message.data.type].call(this_obj, message.data);
+ }
+ };
+
+ if (self.Promise) {
+ this.done = new Promise(function(resolve) {
+ this_obj.doneResolve = resolve;
+ });
+ }
+
+ this.message_target.addEventListener("message", this.message_handler);
+ }
+
+ RemoteContext.prototype.remote_error = function(error) {
+ if (error.preventDefault) {
+ error.preventDefault();
+ }
+
+ // Defer interpretation of errors until the testing protocol has
+ // started and the remote test's `allow_uncaught_exception` property
+ // is available.
+ if (!this.started) {
+ this.early_exception = error;
+ } else if (!this.allow_uncaught_exception) {
+ this.report_uncaught(error);
+ }
+ };
+
+ RemoteContext.prototype.report_uncaught = function(error) {
+ var message = error.message || String(error);
+ var filename = (error.filename ? " " + error.filename: "");
+ // FIXME: Display remote error states separately from main document
+ // error state.
+ tests.set_status(tests.status.ERROR,
+ "Error in remote" + filename + ": " + message,
+ error.stack);
+ };
+
+ RemoteContext.prototype.start = function(data) {
+ this.started = true;
+ this.allow_uncaught_exception = data.properties.allow_uncaught_exception;
+
+ if (this.early_exception && !this.allow_uncaught_exception) {
+ this.report_uncaught(this.early_exception);
+ }
+ };
+
+ RemoteContext.prototype.test_state = function(data) {
+ var remote_test = this.tests[data.test.index];
+ if (!remote_test) {
+ remote_test = new RemoteTest(data.test);
+ this.tests[data.test.index] = remote_test;
+ }
+ remote_test.update_state_from(data.test);
+ tests.notify_test_state(remote_test);
+ };
+
+ RemoteContext.prototype.test_done = function(data) {
+ var remote_test = this.tests[data.test.index];
+ remote_test.update_state_from(data.test);
+ remote_test.done();
+ tests.result(remote_test);
+ };
+
+ RemoteContext.prototype.remote_done = function(data) {
+ if (tests.status.status === null &&
+ data.status.status !== data.status.OK) {
+ tests.set_status(data.status.status, data.status.message, data.status.stack);
+ }
+
+ for (let assert of data.asserts) {
+ var record = new AssertRecord();
+ record.assert_name = assert.assert_name;
+ record.args = assert.args;
+ record.test = assert.test != null ? this.tests[assert.test.index] : null;
+ record.status = assert.status;
+ record.stack = assert.stack;
+ tests.asserts_run.push(record);
+ }
+
+ this.message_target.removeEventListener("message", this.message_handler);
+ this.running = false;
+
+ // If remote context is cross origin assigning to onerror is not
+ // possible, so silently catch those errors.
+ try {
+ this.remote.onerror = null;
+ } catch (e) {
+ // Ignore.
+ }
+
+ this.remote = null;
+ this.message_target = null;
+ if (this.doneResolve) {
+ this.doneResolve();
+ }
+
+ if (tests.all_done()) {
+ tests.complete();
+ }
+ };
+
+ RemoteContext.prototype.message_handlers = {
+ start: RemoteContext.prototype.start,
+ test_state: RemoteContext.prototype.test_state,
+ result: RemoteContext.prototype.test_done,
+ complete: RemoteContext.prototype.remote_done
+ };
+
+ /**
+ * @class
+ * Status of the overall harness
+ */
+ function TestsStatus()
+ {
+ /** The status code */
+ this.status = null;
+ /** Message in case of failure */
+ this.message = null;
+ /** Stack trace in case of an exception. */
+ this.stack = null;
+ }
+
+ /**
+ * Enum of possible harness statuses.
+ *
+ * :values:
+ * - ``OK``
+ * - ``ERROR``
+ * - ``TIMEOUT``
+ * - ``PRECONDITION_FAILED``
+ */
+ TestsStatus.statuses = {
+ OK:0,
+ ERROR:1,
+ TIMEOUT:2,
+ PRECONDITION_FAILED:3
+ };
+
+ TestsStatus.prototype = merge({}, TestsStatus.statuses);
+
+ TestsStatus.prototype.formats = {
+ 0: "OK",
+ 1: "Error",
+ 2: "Timeout",
+ 3: "Optional Feature Unsupported"
+ };
+
+ TestsStatus.prototype.structured_clone = function()
+ {
+ if (!this._structured_clone) {
+ var msg = this.message;
+ msg = msg ? String(msg) : msg;
+ this._structured_clone = merge({
+ status:this.status,
+ message:msg,
+ stack:this.stack
+ }, TestsStatus.statuses);
+ }
+ return this._structured_clone;
+ };
+
+ TestsStatus.prototype.format_status = function() {
+ return this.formats[this.status];
+ };
+
+ /**
+ * @class
+ * Record of an assert that ran.
+ *
+ * @param {Test} test - The test which ran the assert.
+ * @param {string} assert_name - The function name of the assert.
+ * @param {Any} args - The arguments passed to the assert function.
+ */
+ function AssertRecord(test, assert_name, args = []) {
+ /** Name of the assert that ran */
+ this.assert_name = assert_name;
+ /** Test that ran the assert */
+ this.test = test;
+ // Avoid keeping complex objects alive
+ /** Stringification of the arguments that were passed to the assert function */
+ this.args = args.map(x => format_value(x).replace(/\n/g, " "));
+ /** Status of the assert */
+ this.status = null;
+ }
+
+ AssertRecord.prototype.structured_clone = function() {
+ return {
+ assert_name: this.assert_name,
+ test: this.test ? this.test.structured_clone() : null,
+ args: this.args,
+ status: this.status,
+ };
+ };
+
+ function Tests()
+ {
+ this.tests = [];
+ this.num_pending = 0;
+
+ this.phases = {
+ INITIAL:0,
+ SETUP:1,
+ HAVE_TESTS:2,
+ HAVE_RESULTS:3,
+ COMPLETE:4
+ };
+ this.phase = this.phases.INITIAL;
+
+ this.properties = {};
+
+ this.wait_for_finish = false;
+ this.processing_callbacks = false;
+
+ this.allow_uncaught_exception = false;
+
+ this.file_is_test = false;
+ // This value is lazily initialized in order to avoid introducing a
+ // dependency on ECMAScript 2015 Promises to all tests.
+ this.promise_tests = null;
+ this.promise_setup_called = false;
+
+ this.timeout_multiplier = 1;
+ this.timeout_length = test_environment.test_timeout();
+ this.timeout_id = null;
+
+ this.start_callbacks = [];
+ this.test_state_callbacks = [];
+ this.test_done_callbacks = [];
+ this.all_done_callbacks = [];
+
+ this.hide_test_state = false;
+ this.pending_remotes = [];
+
+ this.current_test = null;
+ this.asserts_run = [];
+
+ // Track whether output is enabled, and thus whether or not we should
+ // track asserts.
+ //
+ // On workers we don't get properties set from testharnessreport.js, so
+ // we don't know whether or not to track asserts. To avoid the
+ // resulting performance hit, we assume we are not meant to. This means
+ // that assert tracking does not function on workers.
+ this.output = settings.output && 'document' in global_scope;
+
+ this.status = new TestsStatus();
+
+ var this_obj = this;
+
+ test_environment.add_on_loaded_callback(function() {
+ if (this_obj.all_done()) {
+ this_obj.complete();
+ }
+ });
+
+ this.set_timeout();
+ }
+
+ Tests.prototype.setup = function(func, properties)
+ {
+ if (this.phase >= this.phases.HAVE_RESULTS) {
+ return;
+ }
+
+ if (this.phase < this.phases.SETUP) {
+ this.phase = this.phases.SETUP;
+ }
+
+ this.properties = properties;
+
+ for (var p in properties) {
+ if (properties.hasOwnProperty(p)) {
+ var value = properties[p];
+ if (p == "allow_uncaught_exception") {
+ this.allow_uncaught_exception = value;
+ } else if (p == "explicit_done" && value) {
+ this.wait_for_finish = true;
+ } else if (p == "explicit_timeout" && value) {
+ this.timeout_length = null;
+ if (this.timeout_id)
+ {
+ clearTimeout(this.timeout_id);
+ }
+ } else if (p == "single_test" && value) {
+ this.set_file_is_test();
+ } else if (p == "timeout_multiplier") {
+ this.timeout_multiplier = value;
+ if (this.timeout_length) {
+ this.timeout_length *= this.timeout_multiplier;
+ }
+ } else if (p == "hide_test_state") {
+ this.hide_test_state = value;
+ } else if (p == "output") {
+ this.output = value;
+ } else if (p === "debug") {
+ settings.debug = value;
+ }
+ }
+ }
+
+ if (func) {
+ try {
+ func();
+ } catch (e) {
+ this.status.status = e instanceof OptionalFeatureUnsupportedError ? this.status.PRECONDITION_FAILED : this.status.ERROR;
+ this.status.message = String(e);
+ this.status.stack = e.stack ? e.stack : null;
+ this.complete();
+ }
+ }
+ this.set_timeout();
+ };
+
+ Tests.prototype.set_file_is_test = function() {
+ if (this.tests.length > 0) {
+ throw new Error("Tried to set file as test after creating a test");
+ }
+ this.wait_for_finish = true;
+ this.file_is_test = true;
+ // Create the test, which will add it to the list of tests
+ tests.current_test = async_test();
+ };
+
+ Tests.prototype.set_status = function(status, message, stack)
+ {
+ this.status.status = status;
+ this.status.message = message;
+ this.status.stack = stack ? stack : null;
+ };
+
+ Tests.prototype.set_timeout = function() {
+ if (global_scope.clearTimeout) {
+ var this_obj = this;
+ clearTimeout(this.timeout_id);
+ if (this.timeout_length !== null) {
+ this.timeout_id = setTimeout(function() {
+ this_obj.timeout();
+ }, this.timeout_length);
+ }
+ }
+ };
+
+ Tests.prototype.timeout = function() {
+ var test_in_cleanup = null;
+
+ if (this.status.status === null) {
+ forEach(this.tests,
+ function(test) {
+ // No more than one test is expected to be in the
+ // "CLEANUP" phase at any time
+ if (test.phase === test.phases.CLEANING) {
+ test_in_cleanup = test;
+ }
+
+ test.phase = test.phases.COMPLETE;
+ });
+
+ // Timeouts that occur while a test is in the "cleanup" phase
+ // indicate that some global state was not properly reverted. This
+ // invalidates the overall test execution, so the timeout should be
+ // reported as an error and cancel the execution of any remaining
+ // tests.
+ if (test_in_cleanup) {
+ this.status.status = this.status.ERROR;
+ this.status.message = "Timeout while running cleanup for " +
+ "test named \"" + test_in_cleanup.name + "\".";
+ tests.status.stack = null;
+ } else {
+ this.status.status = this.status.TIMEOUT;
+ }
+ }
+
+ this.complete();
+ };
+
+ Tests.prototype.end_wait = function()
+ {
+ this.wait_for_finish = false;
+ if (this.all_done()) {
+ this.complete();
+ }
+ };
+
+ Tests.prototype.push = function(test)
+ {
+ if (this.phase < this.phases.HAVE_TESTS) {
+ this.start();
+ }
+ this.num_pending++;
+ test.index = this.tests.push(test);
+ this.notify_test_state(test);
+ };
+
+ Tests.prototype.notify_test_state = function(test) {
+ var this_obj = this;
+ forEach(this.test_state_callbacks,
+ function(callback) {
+ callback(test, this_obj);
+ });
+ };
+
+ Tests.prototype.all_done = function() {
+ return (this.tests.length > 0 || this.pending_remotes.length > 0) &&
+ test_environment.all_loaded &&
+ (this.num_pending === 0 || this.is_aborted) && !this.wait_for_finish &&
+ !this.processing_callbacks &&
+ !this.pending_remotes.some(function(w) { return w.running; });
+ };
+
+ Tests.prototype.start = function() {
+ this.phase = this.phases.HAVE_TESTS;
+ this.notify_start();
+ };
+
+ Tests.prototype.notify_start = function() {
+ var this_obj = this;
+ forEach (this.start_callbacks,
+ function(callback)
+ {
+ callback(this_obj.properties);
+ });
+ };
+
+ Tests.prototype.result = function(test)
+ {
+ // If the harness has already transitioned beyond the `HAVE_RESULTS`
+ // phase, subsequent tests should not cause it to revert.
+ if (this.phase <= this.phases.HAVE_RESULTS) {
+ this.phase = this.phases.HAVE_RESULTS;
+ }
+ this.num_pending--;
+ this.notify_result(test);
+ };
+
+ Tests.prototype.notify_result = function(test) {
+ var this_obj = this;
+ this.processing_callbacks = true;
+ forEach(this.test_done_callbacks,
+ function(callback)
+ {
+ callback(test, this_obj);
+ });
+ this.processing_callbacks = false;
+ if (this_obj.all_done()) {
+ this_obj.complete();
+ }
+ };
+
+ Tests.prototype.complete = function() {
+ if (this.phase === this.phases.COMPLETE) {
+ return;
+ }
+ var this_obj = this;
+ var all_complete = function() {
+ this_obj.phase = this_obj.phases.COMPLETE;
+ this_obj.notify_complete();
+ };
+ var incomplete = filter(this.tests,
+ function(test) {
+ return test.phase < test.phases.COMPLETE;
+ });
+
+ /**
+ * To preserve legacy behavior, overall test completion must be
+ * signaled synchronously.
+ */
+ if (incomplete.length === 0) {
+ all_complete();
+ return;
+ }
+
+ all_async(incomplete,
+ function(test, testDone)
+ {
+ if (test.phase === test.phases.INITIAL) {
+ test.phase = test.phases.COMPLETE;
+ testDone();
+ } else {
+ add_test_done_callback(test, testDone);
+ test.cleanup();
+ }
+ },
+ all_complete);
+ };
+
+ Tests.prototype.set_assert = function(assert_name, args) {
+ this.asserts_run.push(new AssertRecord(this.current_test, assert_name, args))
+ }
+
+ Tests.prototype.set_assert_status = function(index, status, stack) {
+ let assert_record = this.asserts_run[index];
+ assert_record.status = status;
+ assert_record.stack = stack;
+ }
+
+ /**
+ * Update the harness status to reflect an unrecoverable harness error that
+ * should cancel all further testing. Update all previously-defined tests
+ * which have not yet started to indicate that they will not be executed.
+ */
+ Tests.prototype.abort = function() {
+ this.status.status = this.status.ERROR;
+ this.is_aborted = true;
+
+ forEach(this.tests,
+ function(test) {
+ if (test.phase === test.phases.INITIAL) {
+ test.phase = test.phases.COMPLETE;
+ }
+ });
+ };
+
+ /*
+ * Determine if any tests share the same `name` property. Return an array
+ * containing the names of any such duplicates.
+ */
+ Tests.prototype.find_duplicates = function() {
+ var names = Object.create(null);
+ var duplicates = [];
+
+ forEach (this.tests,
+ function(test)
+ {
+ if (test.name in names && duplicates.indexOf(test.name) === -1) {
+ duplicates.push(test.name);
+ }
+ names[test.name] = true;
+ });
+
+ return duplicates;
+ };
+
+ function code_unit_str(char) {
+ return 'U+' + char.charCodeAt(0).toString(16);
+ }
+
+ function sanitize_unpaired_surrogates(str) {
+ return str.replace(
+ /([\ud800-\udbff]+)(?![\udc00-\udfff])|(^|[^\ud800-\udbff])([\udc00-\udfff]+)/g,
+ function(_, low, prefix, high) {
+ var output = prefix || ""; // prefix may be undefined
+ var string = low || high; // only one of these alternates can match
+ for (var i = 0; i < string.length; i++) {
+ output += code_unit_str(string[i]);
+ }
+ return output;
+ });
+ }
+
+ function sanitize_all_unpaired_surrogates(tests) {
+ forEach (tests,
+ function (test)
+ {
+ var sanitized = sanitize_unpaired_surrogates(test.name);
+
+ if (test.name !== sanitized) {
+ test.name = sanitized;
+ delete test._structured_clone;
+ }
+ });
+ }
+
+ Tests.prototype.notify_complete = function() {
+ var this_obj = this;
+ var duplicates;
+
+ if (this.status.status === null) {
+ duplicates = this.find_duplicates();
+
+ // Some transports adhere to UTF-8's restriction on unpaired
+ // surrogates. Sanitize the titles so that the results can be
+ // consistently sent via all transports.
+ sanitize_all_unpaired_surrogates(this.tests);
+
+ // Test names are presumed to be unique within test files--this
+ // allows consumers to use them for identification purposes.
+ // Duplicated names violate this expectation and should therefore
+ // be reported as an error.
+ if (duplicates.length) {
+ this.status.status = this.status.ERROR;
+ this.status.message =
+ duplicates.length + ' duplicate test name' +
+ (duplicates.length > 1 ? 's' : '') + ': "' +
+ duplicates.join('", "') + '"';
+ } else {
+ this.status.status = this.status.OK;
+ }
+ }
+
+ forEach (this.all_done_callbacks,
+ function(callback)
+ {
+ callback(this_obj.tests, this_obj.status, this_obj.asserts_run);
+ });
+ };
+
+ /*
+ * Constructs a RemoteContext that tracks tests from a specific worker.
+ */
+ Tests.prototype.create_remote_worker = function(worker) {
+ var message_port;
+
+ if (is_service_worker(worker)) {
+ message_port = navigator.serviceWorker;
+ worker.postMessage({type: "connect"});
+ } else if (is_shared_worker(worker)) {
+ message_port = worker.port;
+ message_port.start();
+ } else {
+ message_port = worker;
+ }
+
+ return new RemoteContext(worker, message_port);
+ };
+
+ /*
+ * Constructs a RemoteContext that tracks tests from a specific window.
+ */
+ Tests.prototype.create_remote_window = function(remote) {
+ remote.postMessage({type: "getmessages"}, "*");
+ return new RemoteContext(
+ remote,
+ window,
+ function(msg) {
+ return msg.source === remote;
+ }
+ );
+ };
+
+ Tests.prototype.fetch_tests_from_worker = function(worker) {
+ if (this.phase >= this.phases.COMPLETE) {
+ return;
+ }
+
+ var remoteContext = this.create_remote_worker(worker);
+ this.pending_remotes.push(remoteContext);
+ return remoteContext.done;
+ };
+
+ /**
+ * Get test results from a worker and include them in the current test.
+ *
+ * @param {Worker|SharedWorker|ServiceWorker|MessagePort} port -
+ * Either a worker object or a port connected to a worker which is
+ * running tests..
+ * @returns {Promise} - A promise that's resolved once all the remote tests are complete.
+ */
+ function fetch_tests_from_worker(port) {
+ return tests.fetch_tests_from_worker(port);
+ }
+ expose(fetch_tests_from_worker, 'fetch_tests_from_worker');
+
+ Tests.prototype.fetch_tests_from_window = function(remote) {
+ if (this.phase >= this.phases.COMPLETE) {
+ return;
+ }
+
+ var remoteContext = this.create_remote_window(remote);
+ this.pending_remotes.push(remoteContext);
+ return remoteContext.done;
+ };
+
+ /**
+ * Aggregate tests from separate windows or iframes
+ * into the current document as if they were all part of the same test file.
+ *
+ * The document of the second window (or iframe) should include
+ * ``testharness.js``, but not ``testharnessreport.js``, and use
+ * :js:func:`test`, :js:func:`async_test`, and :js:func:`promise_test` in
+ * the usual manner.
+ *
+ * @param {Window} window - The window to fetch tests from.
+ */
+ function fetch_tests_from_window(window) {
+ return tests.fetch_tests_from_window(window);
+ }
+ expose(fetch_tests_from_window, 'fetch_tests_from_window');
+
+ /**
+ * Get test results from a shadow realm and include them in the current test.
+ *
+ * @param {ShadowRealm} realm - A shadow realm also running the test harness
+ * @returns {Promise} - A promise that's resolved once all the remote tests are complete.
+ */
+ function fetch_tests_from_shadow_realm(realm) {
+ var chan = new MessageChannel();
+ function receiveMessage(msg_json) {
+ chan.port1.postMessage(JSON.parse(msg_json));
+ }
+ var done = tests.fetch_tests_from_worker(chan.port2);
+ realm.evaluate("begin_shadow_realm_tests")(receiveMessage);
+ chan.port2.start();
+ return done;
+ }
+ expose(fetch_tests_from_shadow_realm, 'fetch_tests_from_shadow_realm');
+
+ /**
+ * Begin running tests in this shadow realm test harness.
+ *
+ * To be called after all tests have been loaded; it is an error to call
+ * this more than once or in a non-Shadow Realm environment
+ *
+ * @param {Function} postMessage - A function to send test updates to the
+ * incubating realm-- accepts JSON-encoded messages in the format used by
+ * RemoteContext
+ */
+ function begin_shadow_realm_tests(postMessage) {
+ if (!(test_environment instanceof ShadowRealmTestEnvironment)) {
+ throw new Error("begin_shadow_realm_tests called in non-Shadow Realm environment");
+ }
+
+ test_environment.begin(function (msg) {
+ postMessage(JSON.stringify(msg));
+ });
+ }
+ expose(begin_shadow_realm_tests, 'begin_shadow_realm_tests');
+
+ /**
+ * Timeout the tests.
+ *
+ * This only has an effect when ``explicit_timeout`` has been set
+ * in :js:func:`setup`. In other cases any call is a no-op.
+ *
+ */
+ function timeout() {
+ if (tests.timeout_length === null) {
+ tests.timeout();
+ }
+ }
+ expose(timeout, 'timeout');
+
+ /**
+ * Add a callback that's triggered when the first :js:class:`Test` is created.
+ *
+ * @param {Function} callback - Callback function. This is called
+ * without arguments.
+ */
+ function add_start_callback(callback) {
+ tests.start_callbacks.push(callback);
+ }
+
+ /**
+ * Add a callback that's triggered when a test state changes.
+ *
+ * @param {Function} callback - Callback function, called with the
+ * :js:class:`Test` as the only argument.
+ */
+ function add_test_state_callback(callback) {
+ tests.test_state_callbacks.push(callback);
+ }
+
+ /**
+ * Add a callback that's triggered when a test result is received.
+ *
+ * @param {Function} callback - Callback function, called with the
+ * :js:class:`Test` as the only argument.
+ */
+ function add_result_callback(callback) {
+ tests.test_done_callbacks.push(callback);
+ }
+
+ /**
+ * Add a callback that's triggered when all tests are complete.
+ *
+ * @param {Function} callback - Callback function, called with an
+ * array of :js:class:`Test` objects, a :js:class:`TestsStatus`
+ * object and an array of :js:class:`AssertRecord` objects. If the
+ * debug setting is ``false`` the final argument will be an empty
+ * array.
+ *
+ * For performance reasons asserts are only tracked when the debug
+ * setting is ``true``. In other cases the array of asserts will be
+ * empty.
+ */
+ function add_completion_callback(callback) {
+ tests.all_done_callbacks.push(callback);
+ }
+
+ expose(add_start_callback, 'add_start_callback');
+ expose(add_test_state_callback, 'add_test_state_callback');
+ expose(add_result_callback, 'add_result_callback');
+ expose(add_completion_callback, 'add_completion_callback');
+
+ function remove(array, item) {
+ var index = array.indexOf(item);
+ if (index > -1) {
+ array.splice(index, 1);
+ }
+ }
+
+ function remove_start_callback(callback) {
+ remove(tests.start_callbacks, callback);
+ }
+
+ function remove_test_state_callback(callback) {
+ remove(tests.test_state_callbacks, callback);
+ }
+
+ function remove_result_callback(callback) {
+ remove(tests.test_done_callbacks, callback);
+ }
+
+ function remove_completion_callback(callback) {
+ remove(tests.all_done_callbacks, callback);
+ }
+
+ /*
+ * Output listener
+ */
+
+ function Output() {
+ this.output_document = document;
+ this.output_node = null;
+ this.enabled = settings.output;
+ this.phase = this.INITIAL;
+ }
+
+ Output.prototype.INITIAL = 0;
+ Output.prototype.STARTED = 1;
+ Output.prototype.HAVE_RESULTS = 2;
+ Output.prototype.COMPLETE = 3;
+
+ Output.prototype.setup = function(properties) {
+ if (this.phase > this.INITIAL) {
+ return;
+ }
+
+ //If output is disabled in testharnessreport.js the test shouldn't be
+ //able to override that
+ this.enabled = this.enabled && (properties.hasOwnProperty("output") ?
+ properties.output : settings.output);
+ };
+
+ Output.prototype.init = function(properties) {
+ if (this.phase >= this.STARTED) {
+ return;
+ }
+ if (properties.output_document) {
+ this.output_document = properties.output_document;
+ } else {
+ this.output_document = document;
+ }
+ this.phase = this.STARTED;
+ };
+
+ Output.prototype.resolve_log = function() {
+ var output_document;
+ if (this.output_node) {
+ return;
+ }
+ if (typeof this.output_document === "function") {
+ output_document = this.output_document.apply(undefined);
+ } else {
+ output_document = this.output_document;
+ }
+ if (!output_document) {
+ return;
+ }
+ var node = output_document.getElementById("log");
+ if (!node) {
+ if (output_document.readyState === "loading") {
+ return;
+ }
+ node = output_document.createElementNS("http://www.w3.org/1999/xhtml", "div");
+ node.id = "log";
+ if (output_document.body) {
+ output_document.body.appendChild(node);
+ } else {
+ var root = output_document.documentElement;
+ var is_html = (root &&
+ root.namespaceURI == "http://www.w3.org/1999/xhtml" &&
+ root.localName == "html");
+ var is_svg = (output_document.defaultView &&
+ "SVGSVGElement" in output_document.defaultView &&
+ root instanceof output_document.defaultView.SVGSVGElement);
+ if (is_svg) {
+ var foreignObject = output_document.createElementNS("http://www.w3.org/2000/svg", "foreignObject");
+ foreignObject.setAttribute("width", "100%");
+ foreignObject.setAttribute("height", "100%");
+ root.appendChild(foreignObject);
+ foreignObject.appendChild(node);
+ } else if (is_html) {
+ root.appendChild(output_document.createElementNS("http://www.w3.org/1999/xhtml", "body"))
+ .appendChild(node);
+ } else {
+ root.appendChild(node);
+ }
+ }
+ }
+ this.output_document = output_document;
+ this.output_node = node;
+ };
+
+ Output.prototype.show_status = function() {
+ if (this.phase < this.STARTED) {
+ this.init({});
+ }
+ if (!this.enabled || this.phase === this.COMPLETE) {
+ return;
+ }
+ this.resolve_log();
+ if (this.phase < this.HAVE_RESULTS) {
+ this.phase = this.HAVE_RESULTS;
+ }
+ var done_count = tests.tests.length - tests.num_pending;
+ if (this.output_node && !tests.hide_test_state) {
+ if (done_count < 100 ||
+ (done_count < 1000 && done_count % 100 === 0) ||
+ done_count % 1000 === 0) {
+ this.output_node.textContent = "Running, " +
+ done_count + " complete, " +
+ tests.num_pending + " remain";
+ }
+ }
+ };
+
+ Output.prototype.show_results = function (tests, harness_status, asserts_run) {
+ if (this.phase >= this.COMPLETE) {
+ return;
+ }
+ if (!this.enabled) {
+ return;
+ }
+ if (!this.output_node) {
+ this.resolve_log();
+ }
+ this.phase = this.COMPLETE;
+
+ var log = this.output_node;
+ if (!log) {
+ return;
+ }
+ var output_document = this.output_document;
+
+ while (log.lastChild) {
+ log.removeChild(log.lastChild);
+ }
+
+ var stylesheet = output_document.createElementNS(xhtml_ns, "style");
+ stylesheet.textContent = stylesheetContent;
+ var heads = output_document.getElementsByTagName("head");
+ if (heads.length) {
+ heads[0].appendChild(stylesheet);
+ }
+
+ var status_number = {};
+ forEach(tests,
+ function(test) {
+ var status = test.format_status();
+ if (status_number.hasOwnProperty(status)) {
+ status_number[status] += 1;
+ } else {
+ status_number[status] = 1;
+ }
+ });
+
+ function status_class(status)
+ {
+ return status.replace(/\s/g, '').toLowerCase();
+ }
+
+ var summary_template = ["section", {"id":"summary"},
+ ["h2", {}, "Summary"],
+ function()
+ {
+ var status = harness_status.format_status();
+ var rv = [["section", {},
+ ["p", {},
+ "Harness status: ",
+ ["span", {"class":status_class(status)},
+ status
+ ],
+ ],
+ ["button",
+ {"onclick": "let evt = new Event('__test_restart'); " +
+ "let canceled = !window.dispatchEvent(evt);" +
+ "if (!canceled) { location.reload() }"},
+ "Rerun"]
+ ]];
+
+ if (harness_status.status === harness_status.ERROR) {
+ rv[0].push(["pre", {}, harness_status.message]);
+ if (harness_status.stack) {
+ rv[0].push(["pre", {}, harness_status.stack]);
+ }
+ }
+ return rv;
+ },
+ ["p", {}, "Found ${num_tests} tests"],
+ function() {
+ var rv = [["div", {}]];
+ var i = 0;
+ while (Test.prototype.status_formats.hasOwnProperty(i)) {
+ if (status_number.hasOwnProperty(Test.prototype.status_formats[i])) {
+ var status = Test.prototype.status_formats[i];
+ rv[0].push(["div", {},
+ ["label", {},
+ ["input", {type:"checkbox", checked:"checked"}],
+ status_number[status] + " ",
+ ["span", {"class":status_class(status)}, status]]]);
+ }
+ i++;
+ }
+ return rv;
+ },
+ ];
+
+ log.appendChild(render(summary_template, {num_tests:tests.length}, output_document));
+
+ forEach(output_document.querySelectorAll("section#summary label"),
+ function(element)
+ {
+ on_event(element, "click",
+ function(e)
+ {
+ if (output_document.getElementById("results") === null) {
+ e.preventDefault();
+ return;
+ }
+ var result_class = element.querySelector("span[class]").getAttribute("class");
+ var style_element = output_document.querySelector("style#hide-" + result_class);
+ var input_element = element.querySelector("input");
+ if (!style_element && !input_element.checked) {
+ style_element = output_document.createElementNS(xhtml_ns, "style");
+ style_element.id = "hide-" + result_class;
+ style_element.textContent = "table#results > tbody > tr.overall-"+result_class+"{display:none}";
+ output_document.body.appendChild(style_element);
+ } else if (style_element && input_element.checked) {
+ style_element.parentNode.removeChild(style_element);
+ }
+ });
+ });
+
+ // This use of innerHTML plus manual escaping is not recommended in
+ // general, but is necessary here for performance. Using textContent
+ // on each individual <td> adds tens of seconds of execution time for
+ // large test suites (tens of thousands of tests).
+ function escape_html(s)
+ {
+ return s.replace(/\&/g, "&amp;")
+ .replace(/</g, "&lt;")
+ .replace(/"/g, "&quot;")
+ .replace(/'/g, "&#39;");
+ }
+
+ function has_assertions()
+ {
+ for (var i = 0; i < tests.length; i++) {
+ if (tests[i].properties.hasOwnProperty("assert")) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ function get_assertion(test)
+ {
+ if (test.properties.hasOwnProperty("assert")) {
+ if (Array.isArray(test.properties.assert)) {
+ return test.properties.assert.join(' ');
+ }
+ return test.properties.assert;
+ }
+ return '';
+ }
+
+ var asserts_run_by_test = new Map();
+ asserts_run.forEach(assert => {
+ if (!asserts_run_by_test.has(assert.test)) {
+ asserts_run_by_test.set(assert.test, []);
+ }
+ asserts_run_by_test.get(assert.test).push(assert);
+ });
+
+ function get_asserts_output(test) {
+ var asserts = asserts_run_by_test.get(test);
+ if (!asserts) {
+ return "No asserts ran";
+ }
+ rv = "<table>";
+ rv += asserts.map(assert => {
+ var output_fn = "<strong>" + escape_html(assert.assert_name) + "</strong>(";
+ var prefix_len = output_fn.length;
+ var output_args = assert.args;
+ var output_len = output_args.reduce((prev, current) => prev+current, prefix_len);
+ if (output_len[output_len.length - 1] > 50) {
+ output_args = output_args.map((x, i) =>
+ (i > 0 ? " ".repeat(prefix_len) : "" )+ x + (i < output_args.length - 1 ? ",\n" : ""));
+ } else {
+ output_args = output_args.map((x, i) => x + (i < output_args.length - 1 ? ", " : ""));
+ }
+ output_fn += escape_html(output_args.join(""));
+ output_fn += ')';
+ var output_location;
+ if (assert.stack) {
+ output_location = assert.stack.split("\n", 1)[0].replace(/@?\w+:\/\/[^ "\/]+(?::\d+)?/g, " ");
+ }
+ return "<tr class='overall-" +
+ status_class(Test.prototype.status_formats[assert.status]) + "'>" +
+ "<td class='" +
+ status_class(Test.prototype.status_formats[assert.status]) + "'>" +
+ Test.prototype.status_formats[assert.status] + "</td>" +
+ "<td><pre>" +
+ output_fn +
+ (output_location ? "\n" + escape_html(output_location) : "") +
+ "</pre></td></tr>";
+ }
+ ).join("\n");
+ rv += "</table>";
+ return rv;
+ }
+
+ log.appendChild(document.createElementNS(xhtml_ns, "section"));
+ var assertions = has_assertions();
+ var html = "<h2>Details</h2><table id='results' " + (assertions ? "class='assertions'" : "" ) + ">" +
+ "<thead><tr><th>Result</th><th>Test Name</th>" +
+ (assertions ? "<th>Assertion</th>" : "") +
+ "<th>Message</th></tr></thead>" +
+ "<tbody>";
+ for (var i = 0; i < tests.length; i++) {
+ var test = tests[i];
+ html += '<tr class="overall-' +
+ status_class(test.format_status()) +
+ '">' +
+ '<td class="' +
+ status_class(test.format_status()) +
+ '">' +
+ test.format_status() +
+ "</td><td>" +
+ escape_html(test.name) +
+ "</td><td>" +
+ (assertions ? escape_html(get_assertion(test)) + "</td><td>" : "") +
+ escape_html(test.message ? tests[i].message : " ") +
+ (tests[i].stack ? "<pre>" +
+ escape_html(tests[i].stack) +
+ "</pre>": "");
+ if (!(test instanceof RemoteTest)) {
+ html += "<details><summary>Asserts run</summary>" + get_asserts_output(test) + "</details>"
+ }
+ html += "</td></tr>";
+ }
+ html += "</tbody></table>";
+ try {
+ log.lastChild.innerHTML = html;
+ } catch (e) {
+ log.appendChild(document.createElementNS(xhtml_ns, "p"))
+ .textContent = "Setting innerHTML for the log threw an exception.";
+ log.appendChild(document.createElementNS(xhtml_ns, "pre"))
+ .textContent = html;
+ }
+ };
+
+ /*
+ * Template code
+ *
+ * A template is just a JavaScript structure. An element is represented as:
+ *
+ * [tag_name, {attr_name:attr_value}, child1, child2]
+ *
+ * the children can either be strings (which act like text nodes), other templates or
+ * functions (see below)
+ *
+ * A text node is represented as
+ *
+ * ["{text}", value]
+ *
+ * String values have a simple substitution syntax; ${foo} represents a variable foo.
+ *
+ * It is possible to embed logic in templates by using a function in a place where a
+ * node would usually go. The function must either return part of a template or null.
+ *
+ * In cases where a set of nodes are required as output rather than a single node
+ * with children it is possible to just use a list
+ * [node1, node2, node3]
+ *
+ * Usage:
+ *
+ * render(template, substitutions) - take a template and an object mapping
+ * variable names to parameters and return either a DOM node or a list of DOM nodes
+ *
+ * substitute(template, substitutions) - take a template and variable mapping object,
+ * make the variable substitutions and return the substituted template
+ *
+ */
+
+ function is_single_node(template)
+ {
+ return typeof template[0] === "string";
+ }
+
+ function substitute(template, substitutions)
+ {
+ if (typeof template === "function") {
+ var replacement = template(substitutions);
+ if (!replacement) {
+ return null;
+ }
+
+ return substitute(replacement, substitutions);
+ }
+
+ if (is_single_node(template)) {
+ return substitute_single(template, substitutions);
+ }
+
+ return filter(map(template, function(x) {
+ return substitute(x, substitutions);
+ }), function(x) {return x !== null;});
+ }
+
+ function substitute_single(template, substitutions)
+ {
+ var substitution_re = /\$\{([^ }]*)\}/g;
+
+ function do_substitution(input) {
+ var components = input.split(substitution_re);
+ var rv = [];
+ for (var i = 0; i < components.length; i += 2) {
+ rv.push(components[i]);
+ if (components[i + 1]) {
+ rv.push(String(substitutions[components[i + 1]]));
+ }
+ }
+ return rv;
+ }
+
+ function substitute_attrs(attrs, rv)
+ {
+ rv[1] = {};
+ for (var name in template[1]) {
+ if (attrs.hasOwnProperty(name)) {
+ var new_name = do_substitution(name).join("");
+ var new_value = do_substitution(attrs[name]).join("");
+ rv[1][new_name] = new_value;
+ }
+ }
+ }
+
+ function substitute_children(children, rv)
+ {
+ for (var i = 0; i < children.length; i++) {
+ if (children[i] instanceof Object) {
+ var replacement = substitute(children[i], substitutions);
+ if (replacement !== null) {
+ if (is_single_node(replacement)) {
+ rv.push(replacement);
+ } else {
+ extend(rv, replacement);
+ }
+ }
+ } else {
+ extend(rv, do_substitution(String(children[i])));
+ }
+ }
+ return rv;
+ }
+
+ var rv = [];
+ rv.push(do_substitution(String(template[0])).join(""));
+
+ if (template[0] === "{text}") {
+ substitute_children(template.slice(1), rv);
+ } else {
+ substitute_attrs(template[1], rv);
+ substitute_children(template.slice(2), rv);
+ }
+
+ return rv;
+ }
+
+ function make_dom_single(template, doc)
+ {
+ var output_document = doc || document;
+ var element;
+ if (template[0] === "{text}") {
+ element = output_document.createTextNode("");
+ for (var i = 1; i < template.length; i++) {
+ element.data += template[i];
+ }
+ } else {
+ element = output_document.createElementNS(xhtml_ns, template[0]);
+ for (var name in template[1]) {
+ if (template[1].hasOwnProperty(name)) {
+ element.setAttribute(name, template[1][name]);
+ }
+ }
+ for (var i = 2; i < template.length; i++) {
+ if (template[i] instanceof Object) {
+ var sub_element = make_dom(template[i]);
+ element.appendChild(sub_element);
+ } else {
+ var text_node = output_document.createTextNode(template[i]);
+ element.appendChild(text_node);
+ }
+ }
+ }
+
+ return element;
+ }
+
+ function make_dom(template, substitutions, output_document)
+ {
+ if (is_single_node(template)) {
+ return make_dom_single(template, output_document);
+ }
+
+ return map(template, function(x) {
+ return make_dom_single(x, output_document);
+ });
+ }
+
+ function render(template, substitutions, output_document)
+ {
+ return make_dom(substitute(template, substitutions), output_document);
+ }
+
+ /*
+ * Utility functions
+ */
+ function assert(expected_true, function_name, description, error, substitutions)
+ {
+ if (expected_true !== true) {
+ var msg = make_message(function_name, description,
+ error, substitutions);
+ throw new AssertionError(msg);
+ }
+ }
+
+ /**
+ * @class
+ * Exception type that represents a failing assert.
+ *
+ * @param {string} message - Error message.
+ */
+ function AssertionError(message)
+ {
+ if (typeof message == "string") {
+ message = sanitize_unpaired_surrogates(message);
+ }
+ this.message = message;
+ this.stack = get_stack();
+ }
+ expose(AssertionError, "AssertionError");
+
+ AssertionError.prototype = Object.create(Error.prototype);
+
+ const get_stack = function() {
+ var stack = new Error().stack;
+
+ // 'Error.stack' is not supported in all browsers/versions
+ if (!stack) {
+ return "(Stack trace unavailable)";
+ }
+
+ var lines = stack.split("\n");
+
+ // Create a pattern to match stack frames originating within testharness.js. These include the
+ // script URL, followed by the line/col (e.g., '/resources/testharness.js:120:21').
+ // Escape the URL per http://stackoverflow.com/questions/3561493/is-there-a-regexp-escape-function-in-javascript
+ // in case it contains RegExp characters.
+ var script_url = get_script_url();
+ var re_text = script_url ? script_url.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&') : "\\btestharness.js";
+ var re = new RegExp(re_text + ":\\d+:\\d+");
+
+ // Some browsers include a preamble that specifies the type of the error object. Skip this by
+ // advancing until we find the first stack frame originating from testharness.js.
+ var i = 0;
+ while (!re.test(lines[i]) && i < lines.length) {
+ i++;
+ }
+
+ // Then skip the top frames originating from testharness.js to begin the stack at the test code.
+ while (re.test(lines[i]) && i < lines.length) {
+ i++;
+ }
+
+ // Paranoid check that we didn't skip all frames. If so, return the original stack unmodified.
+ if (i >= lines.length) {
+ return stack;
+ }
+
+ return lines.slice(i).join("\n");
+ }
+
+ function OptionalFeatureUnsupportedError(message)
+ {
+ AssertionError.call(this, message);
+ }
+ OptionalFeatureUnsupportedError.prototype = Object.create(AssertionError.prototype);
+ expose(OptionalFeatureUnsupportedError, "OptionalFeatureUnsupportedError");
+
+ function make_message(function_name, description, error, substitutions)
+ {
+ for (var p in substitutions) {
+ if (substitutions.hasOwnProperty(p)) {
+ substitutions[p] = format_value(substitutions[p]);
+ }
+ }
+ var node_form = substitute(["{text}", "${function_name}: ${description}" + error],
+ merge({function_name:function_name,
+ description:(description?description + " ":"")},
+ substitutions));
+ return node_form.slice(1).join("");
+ }
+
+ function filter(array, callable, thisObj) {
+ var rv = [];
+ for (var i = 0; i < array.length; i++) {
+ if (array.hasOwnProperty(i)) {
+ var pass = callable.call(thisObj, array[i], i, array);
+ if (pass) {
+ rv.push(array[i]);
+ }
+ }
+ }
+ return rv;
+ }
+
+ function map(array, callable, thisObj)
+ {
+ var rv = [];
+ rv.length = array.length;
+ for (var i = 0; i < array.length; i++) {
+ if (array.hasOwnProperty(i)) {
+ rv[i] = callable.call(thisObj, array[i], i, array);
+ }
+ }
+ return rv;
+ }
+
+ function extend(array, items)
+ {
+ Array.prototype.push.apply(array, items);
+ }
+
+ function forEach(array, callback, thisObj)
+ {
+ for (var i = 0; i < array.length; i++) {
+ if (array.hasOwnProperty(i)) {
+ callback.call(thisObj, array[i], i, array);
+ }
+ }
+ }
+
+ /**
+ * Immediately invoke a "iteratee" function with a series of values in
+ * parallel and invoke a final "done" function when all of the "iteratee"
+ * invocations have signaled completion.
+ *
+ * If all callbacks complete synchronously (or if no callbacks are
+ * specified), the ``done_callback`` will be invoked synchronously. It is the
+ * responsibility of the caller to ensure asynchronicity in cases where
+ * that is desired.
+ *
+ * @param {array} value Zero or more values to use in the invocation of
+ * ``iter_callback``
+ * @param {function} iter_callback A function that will be invoked
+ * once for each of the values min
+ * ``value``. Two arguments will
+ * be available in each
+ * invocation: the value from
+ * ``value`` and a function that
+ * must be invoked to signal
+ * completion
+ * @param {function} done_callback A function that will be invoked after
+ * all operations initiated by the
+ * ``iter_callback`` function have signaled
+ * completion
+ */
+ function all_async(values, iter_callback, done_callback)
+ {
+ var remaining = values.length;
+
+ if (remaining === 0) {
+ done_callback();
+ }
+
+ forEach(values,
+ function(element) {
+ var invoked = false;
+ var elDone = function() {
+ if (invoked) {
+ return;
+ }
+
+ invoked = true;
+ remaining -= 1;
+
+ if (remaining === 0) {
+ done_callback();
+ }
+ };
+
+ iter_callback(element, elDone);
+ });
+ }
+
+ function merge(a,b)
+ {
+ var rv = {};
+ var p;
+ for (p in a) {
+ rv[p] = a[p];
+ }
+ for (p in b) {
+ rv[p] = b[p];
+ }
+ return rv;
+ }
+
+ function expose(object, name)
+ {
+ var components = name.split(".");
+ var target = global_scope;
+ for (var i = 0; i < components.length - 1; i++) {
+ if (!(components[i] in target)) {
+ target[components[i]] = {};
+ }
+ target = target[components[i]];
+ }
+ target[components[components.length - 1]] = object;
+ }
+
+ function is_same_origin(w) {
+ try {
+ 'random_prop' in w;
+ return true;
+ } catch (e) {
+ return false;
+ }
+ }
+
+ /** Returns the 'src' URL of the first <script> tag in the page to include the file 'testharness.js'. */
+ function get_script_url()
+ {
+ if (!('document' in global_scope)) {
+ return undefined;
+ }
+
+ var scripts = document.getElementsByTagName("script");
+ for (var i = 0; i < scripts.length; i++) {
+ var src;
+ if (scripts[i].src) {
+ src = scripts[i].src;
+ } else if (scripts[i].href) {
+ //SVG case
+ src = scripts[i].href.baseVal;
+ }
+
+ var matches = src && src.match(/^(.*\/|)testharness\.js$/);
+ if (matches) {
+ return src;
+ }
+ }
+ return undefined;
+ }
+
+ /** Returns the <title> or filename or "Untitled" */
+ function get_title()
+ {
+ if ('document' in global_scope) {
+ //Don't use document.title to work around an Opera/Presto bug in XHTML documents
+ var title = document.getElementsByTagName("title")[0];
+ if (title && title.firstChild && title.firstChild.data) {
+ return title.firstChild.data;
+ }
+ }
+ if ('META_TITLE' in global_scope && META_TITLE) {
+ return META_TITLE;
+ }
+ if ('location' in global_scope && 'pathname' in location) {
+ return location.pathname.substring(location.pathname.lastIndexOf('/') + 1, location.pathname.indexOf('.'));
+ }
+ return "Untitled";
+ }
+
+ /**
+ * Setup globals
+ */
+
+ var tests = new Tests();
+
+ if (global_scope.addEventListener) {
+ var error_handler = function(error, message, stack) {
+ var optional_unsupported = error instanceof OptionalFeatureUnsupportedError;
+ if (tests.file_is_test) {
+ var test = tests.tests[0];
+ if (test.phase >= test.phases.HAS_RESULT) {
+ return;
+ }
+ var status = optional_unsupported ? test.PRECONDITION_FAILED : test.FAIL;
+ test.set_status(status, message, stack);
+ test.phase = test.phases.HAS_RESULT;
+ } else if (!tests.allow_uncaught_exception) {
+ var status = optional_unsupported ? tests.status.PRECONDITION_FAILED : tests.status.ERROR;
+ tests.status.status = status;
+ tests.status.message = message;
+ tests.status.stack = stack;
+ }
+
+ // Do not transition to the "complete" phase if the test has been
+ // configured to allow uncaught exceptions. This gives the test an
+ // opportunity to define subtests based on the exception reporting
+ // behavior.
+ if (!tests.allow_uncaught_exception) {
+ done();
+ }
+ };
+
+ addEventListener("error", function(e) {
+ var message = e.message;
+ var stack;
+ if (e.error && e.error.stack) {
+ stack = e.error.stack;
+ } else {
+ stack = e.filename + ":" + e.lineno + ":" + e.colno;
+ }
+ error_handler(e.error, message, stack);
+ }, false);
+
+ addEventListener("unhandledrejection", function(e) {
+ var message;
+ if (e.reason && e.reason.message) {
+ message = "Unhandled rejection: " + e.reason.message;
+ } else {
+ message = "Unhandled rejection";
+ }
+ var stack;
+ if (e.reason && e.reason.stack) {
+ stack = e.reason.stack;
+ }
+ error_handler(e.reason, message, stack);
+ }, false);
+ }
+
+ test_environment.on_tests_ready();
+
+ /**
+ * Stylesheet
+ */
+ var stylesheetContent = "\
+html {\
+ font-family:DejaVu Sans, Bitstream Vera Sans, Arial, Sans;\
+}\
+\
+#log .warning,\
+#log .warning a {\
+ color: black;\
+ background: yellow;\
+}\
+\
+#log .error,\
+#log .error a {\
+ color: white;\
+ background: red;\
+}\
+\
+section#summary {\
+ margin-bottom:1em;\
+}\
+\
+table#results {\
+ border-collapse:collapse;\
+ table-layout:fixed;\
+ width:100%;\
+}\
+\
+table#results > thead > tr > th:first-child,\
+table#results > tbody > tr > td:first-child {\
+ width:8em;\
+}\
+\
+table#results > thead > tr > th:last-child,\
+table#results > thead > tr > td:last-child {\
+ width:50%;\
+}\
+\
+table#results.assertions > thead > tr > th:last-child,\
+table#results.assertions > tbody > tr > td:last-child {\
+ width:35%;\
+}\
+\
+table#results > thead > > tr > th {\
+ padding:0;\
+ padding-bottom:0.5em;\
+ border-bottom:medium solid black;\
+}\
+\
+table#results > tbody > tr> td {\
+ padding:1em;\
+ padding-bottom:0.5em;\
+ border-bottom:thin solid black;\
+}\
+\
+.pass {\
+ color:green;\
+}\
+\
+.fail {\
+ color:red;\
+}\
+\
+tr.timeout {\
+ color:red;\
+}\
+\
+tr.notrun {\
+ color:blue;\
+}\
+\
+tr.optionalunsupported {\
+ color:blue;\
+}\
+\
+.ok {\
+ color:green;\
+}\
+\
+.error {\
+ color:red;\
+}\
+\
+.pass, .fail, .timeout, .notrun, .optionalunsupported .ok, .timeout, .error {\
+ font-variant:small-caps;\
+}\
+\
+table#results span {\
+ display:block;\
+}\
+\
+table#results span.expected {\
+ font-family:DejaVu Sans Mono, Bitstream Vera Sans Mono, Monospace;\
+ white-space:pre;\
+}\
+\
+table#results span.actual {\
+ font-family:DejaVu Sans Mono, Bitstream Vera Sans Mono, Monospace;\
+ white-space:pre;\
+}\
+";
+
+})(self);
+// vim: set expandtab shiftwidth=4 tabstop=4:
diff --git a/test/wpt/tests/resources/testharness.js.headers b/test/wpt/tests/resources/testharness.js.headers
new file mode 100644
index 0000000..5e8f640
--- /dev/null
+++ b/test/wpt/tests/resources/testharness.js.headers
@@ -0,0 +1,2 @@
+Content-Type: text/javascript; charset=utf-8
+Cache-Control: max-age=3600
diff --git a/test/wpt/tests/resources/testharnessreport.js b/test/wpt/tests/resources/testharnessreport.js
new file mode 100644
index 0000000..e5cb40f
--- /dev/null
+++ b/test/wpt/tests/resources/testharnessreport.js
@@ -0,0 +1,57 @@
+/* global add_completion_callback */
+/* global setup */
+
+/*
+ * This file is intended for vendors to implement code needed to integrate
+ * testharness.js tests with their own test systems.
+ *
+ * Typically test system integration will attach callbacks when each test has
+ * run, using add_result_callback(callback(test)), or when the whole test file
+ * has completed, using
+ * add_completion_callback(callback(tests, harness_status)).
+ *
+ * For more documentation about the callback functions and the
+ * parameters they are called with see testharness.js
+ */
+
+function dump_test_results(tests, status) {
+ var results_element = document.createElement("script");
+ results_element.type = "text/json";
+ results_element.id = "__testharness__results__";
+ var test_results = tests.map(function(x) {
+ return {name:x.name, status:x.status, message:x.message, stack:x.stack}
+ });
+ var data = {test:window.location.href,
+ tests:test_results,
+ status: status.status,
+ message: status.message,
+ stack: status.stack};
+ results_element.textContent = JSON.stringify(data);
+
+ // To avoid a HierarchyRequestError with XML documents, ensure that 'results_element'
+ // is inserted at a location that results in a valid document.
+ var parent = document.body
+ ? document.body // <body> is required in XHTML documents
+ : document.documentElement; // fallback for optional <body> in HTML5, SVG, etc.
+
+ parent.appendChild(results_element);
+}
+
+add_completion_callback(dump_test_results);
+
+/* If the parent window has a testharness_properties object,
+ * we use this to provide the test settings. This is used by the
+ * default in-browser runner to configure the timeout and the
+ * rendering of results
+ */
+try {
+ if (window.opener && "testharness_properties" in window.opener) {
+ /* If we pass the testharness_properties object as-is here without
+ * JSON stringifying and reparsing it, IE fails & emits the message
+ * "Could not complete the operation due to error 80700019".
+ */
+ setup(JSON.parse(JSON.stringify(window.opener.testharness_properties)));
+ }
+} catch (e) {
+}
+// vim: set expandtab shiftwidth=4 tabstop=4:
diff --git a/test/wpt/tests/resources/testharnessreport.js.headers b/test/wpt/tests/resources/testharnessreport.js.headers
new file mode 100644
index 0000000..5e8f640
--- /dev/null
+++ b/test/wpt/tests/resources/testharnessreport.js.headers
@@ -0,0 +1,2 @@
+Content-Type: text/javascript; charset=utf-8
+Cache-Control: max-age=3600
diff --git a/test/wpt/tests/resources/webidl2/build.sh b/test/wpt/tests/resources/webidl2/build.sh
new file mode 100644
index 0000000..a631268
--- /dev/null
+++ b/test/wpt/tests/resources/webidl2/build.sh
@@ -0,0 +1,12 @@
+set -ex
+
+if [ ! -d "webidl2.js" ]; then
+ git clone https://github.com/w3c/webidl2.js.git
+fi
+cd webidl2.js
+npm install
+npm run build-debug
+HASH=$(git rev-parse HEAD)
+cd ..
+cp webidl2.js/dist/webidl2.js lib/
+echo "Currently using webidl2.js@${HASH}." > lib/VERSION.md
diff --git a/test/wpt/tests/resources/webidl2/lib/README.md b/test/wpt/tests/resources/webidl2/lib/README.md
new file mode 100644
index 0000000..1bd5832
--- /dev/null
+++ b/test/wpt/tests/resources/webidl2/lib/README.md
@@ -0,0 +1,4 @@
+This directory contains a built version of the [webidl2.js library](https://github.com/w3c/webidl2.js).
+It is built by running `npm run build-debug` at the root of that repository.
+
+The `webidl2.js.headers` file is a local addition to ensure the script is interpreted as UTF-8.
diff --git a/test/wpt/tests/resources/webidl2/lib/VERSION.md b/test/wpt/tests/resources/webidl2/lib/VERSION.md
new file mode 100644
index 0000000..5a3726c
--- /dev/null
+++ b/test/wpt/tests/resources/webidl2/lib/VERSION.md
@@ -0,0 +1 @@
+Currently using webidl2.js@6889aee6fc7d65915ab1267825248157dbc50486.
diff --git a/test/wpt/tests/resources/webidl2/lib/webidl2.js b/test/wpt/tests/resources/webidl2/lib/webidl2.js
new file mode 100644
index 0000000..7161def
--- /dev/null
+++ b/test/wpt/tests/resources/webidl2/lib/webidl2.js
@@ -0,0 +1,3824 @@
+(function webpackUniversalModuleDefinition(root, factory) {
+ if(typeof exports === 'object' && typeof module === 'object')
+ module.exports = factory();
+ else if(typeof define === 'function' && define.amd)
+ define([], factory);
+ else if(typeof exports === 'object')
+ exports["WebIDL2"] = factory();
+ else
+ root["WebIDL2"] = factory();
+})(globalThis, () => {
+return /******/ (() => { // webpackBootstrap
+/******/ "use strict";
+/******/ var __webpack_modules__ = ([
+/* 0 */,
+/* 1 */
+/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
+
+__webpack_require__.r(__webpack_exports__);
+/* harmony export */ __webpack_require__.d(__webpack_exports__, {
+/* harmony export */ "parse": () => (/* binding */ parse)
+/* harmony export */ });
+/* harmony import */ var _tokeniser_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(2);
+/* harmony import */ var _productions_enum_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(15);
+/* harmony import */ var _productions_includes_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(16);
+/* harmony import */ var _productions_extended_attributes_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(8);
+/* harmony import */ var _productions_typedef_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(17);
+/* harmony import */ var _productions_callback_js__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(18);
+/* harmony import */ var _productions_interface_js__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(19);
+/* harmony import */ var _productions_mixin_js__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(25);
+/* harmony import */ var _productions_dictionary_js__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(26);
+/* harmony import */ var _productions_namespace_js__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(28);
+/* harmony import */ var _productions_callback_interface_js__WEBPACK_IMPORTED_MODULE_10__ = __webpack_require__(29);
+/* harmony import */ var _productions_helpers_js__WEBPACK_IMPORTED_MODULE_11__ = __webpack_require__(4);
+/* harmony import */ var _productions_token_js__WEBPACK_IMPORTED_MODULE_12__ = __webpack_require__(10);
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+/**
+ * @param {Tokeniser} tokeniser
+ * @param {object} options
+ * @param {boolean} [options.concrete]
+ * @param {Function[]} [options.productions]
+ */
+function parseByTokens(tokeniser, options) {
+ const source = tokeniser.source;
+
+ function error(str) {
+ tokeniser.error(str);
+ }
+
+ function consume(...candidates) {
+ return tokeniser.consume(...candidates);
+ }
+
+ function callback() {
+ const callback = consume("callback");
+ if (!callback) return;
+ if (tokeniser.probe("interface")) {
+ return _productions_callback_interface_js__WEBPACK_IMPORTED_MODULE_10__.CallbackInterface.parse(tokeniser, callback);
+ }
+ return _productions_callback_js__WEBPACK_IMPORTED_MODULE_5__.CallbackFunction.parse(tokeniser, callback);
+ }
+
+ function interface_(opts) {
+ const base = consume("interface");
+ if (!base) return;
+ const ret =
+ _productions_mixin_js__WEBPACK_IMPORTED_MODULE_7__.Mixin.parse(tokeniser, base, opts) ||
+ _productions_interface_js__WEBPACK_IMPORTED_MODULE_6__.Interface.parse(tokeniser, base, opts) ||
+ error("Interface has no proper body");
+ return ret;
+ }
+
+ function partial() {
+ const partial = consume("partial");
+ if (!partial) return;
+ return (
+ _productions_dictionary_js__WEBPACK_IMPORTED_MODULE_8__.Dictionary.parse(tokeniser, { partial }) ||
+ interface_({ partial }) ||
+ _productions_namespace_js__WEBPACK_IMPORTED_MODULE_9__.Namespace.parse(tokeniser, { partial }) ||
+ error("Partial doesn't apply to anything")
+ );
+ }
+
+ function definition() {
+ if (options.productions) {
+ for (const production of options.productions) {
+ const result = production(tokeniser);
+ if (result) {
+ return result;
+ }
+ }
+ }
+
+ return (
+ callback() ||
+ interface_() ||
+ partial() ||
+ _productions_dictionary_js__WEBPACK_IMPORTED_MODULE_8__.Dictionary.parse(tokeniser) ||
+ _productions_enum_js__WEBPACK_IMPORTED_MODULE_1__.Enum.parse(tokeniser) ||
+ _productions_typedef_js__WEBPACK_IMPORTED_MODULE_4__.Typedef.parse(tokeniser) ||
+ _productions_includes_js__WEBPACK_IMPORTED_MODULE_2__.Includes.parse(tokeniser) ||
+ _productions_namespace_js__WEBPACK_IMPORTED_MODULE_9__.Namespace.parse(tokeniser)
+ );
+ }
+
+ function definitions() {
+ if (!source.length) return [];
+ const defs = [];
+ while (true) {
+ const ea = _productions_extended_attributes_js__WEBPACK_IMPORTED_MODULE_3__.ExtendedAttributes.parse(tokeniser);
+ const def = definition();
+ if (!def) {
+ if (ea.length) error("Stray extended attributes");
+ break;
+ }
+ (0,_productions_helpers_js__WEBPACK_IMPORTED_MODULE_11__.autoParenter)(def).extAttrs = ea;
+ defs.push(def);
+ }
+ const eof = _productions_token_js__WEBPACK_IMPORTED_MODULE_12__.Eof.parse(tokeniser);
+ if (options.concrete) {
+ defs.push(eof);
+ }
+ return defs;
+ }
+ const res = definitions();
+ if (tokeniser.position < source.length) error("Unrecognised tokens");
+ return res;
+}
+
+/**
+ * @param {string} str
+ * @param {object} [options]
+ * @param {*} [options.sourceName]
+ * @param {boolean} [options.concrete]
+ * @param {Function[]} [options.productions]
+ * @return {import("./productions/base.js").Base[]}
+ */
+function parse(str, options = {}) {
+ const tokeniser = new _tokeniser_js__WEBPACK_IMPORTED_MODULE_0__.Tokeniser(str);
+ if (typeof options.sourceName !== "undefined") {
+ // @ts-ignore (See Tokeniser.source in supplement.d.ts)
+ tokeniser.source.name = options.sourceName;
+ }
+ return parseByTokens(tokeniser, options);
+}
+
+
+/***/ }),
+/* 2 */
+/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
+
+__webpack_require__.r(__webpack_exports__);
+/* harmony export */ __webpack_require__.d(__webpack_exports__, {
+/* harmony export */ "Tokeniser": () => (/* binding */ Tokeniser),
+/* harmony export */ "WebIDLParseError": () => (/* binding */ WebIDLParseError),
+/* harmony export */ "argumentNameKeywords": () => (/* binding */ argumentNameKeywords),
+/* harmony export */ "stringTypes": () => (/* binding */ stringTypes),
+/* harmony export */ "typeNameKeywords": () => (/* binding */ typeNameKeywords)
+/* harmony export */ });
+/* harmony import */ var _error_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(3);
+/* harmony import */ var _productions_helpers_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(4);
+
+
+
+// These regular expressions use the sticky flag so they will only match at
+// the current location (ie. the offset of lastIndex).
+const tokenRe = {
+ // This expression uses a lookahead assertion to catch false matches
+ // against integers early.
+ decimal:
+ /-?(?=[0-9]*\.|[0-9]+[eE])(([0-9]+\.[0-9]*|[0-9]*\.[0-9]+)([Ee][-+]?[0-9]+)?|[0-9]+[Ee][-+]?[0-9]+)/y,
+ integer: /-?(0([Xx][0-9A-Fa-f]+|[0-7]*)|[1-9][0-9]*)/y,
+ identifier: /[_-]?[A-Za-z][0-9A-Z_a-z-]*/y,
+ string: /"[^"]*"/y,
+ whitespace: /[\t\n\r ]+/y,
+ comment: /\/\/.*|\/\*[\s\S]*?\*\//y,
+ other: /[^\t\n\r 0-9A-Za-z]/y,
+};
+
+const typeNameKeywords = [
+ "ArrayBuffer",
+ "DataView",
+ "Int8Array",
+ "Int16Array",
+ "Int32Array",
+ "Uint8Array",
+ "Uint16Array",
+ "Uint32Array",
+ "Uint8ClampedArray",
+ "BigInt64Array",
+ "BigUint64Array",
+ "Float32Array",
+ "Float64Array",
+ "any",
+ "object",
+ "symbol",
+];
+
+const stringTypes = ["ByteString", "DOMString", "USVString"];
+
+const argumentNameKeywords = [
+ "async",
+ "attribute",
+ "callback",
+ "const",
+ "constructor",
+ "deleter",
+ "dictionary",
+ "enum",
+ "getter",
+ "includes",
+ "inherit",
+ "interface",
+ "iterable",
+ "maplike",
+ "namespace",
+ "partial",
+ "required",
+ "setlike",
+ "setter",
+ "static",
+ "stringifier",
+ "typedef",
+ "unrestricted",
+];
+
+const nonRegexTerminals = [
+ "-Infinity",
+ "FrozenArray",
+ "Infinity",
+ "NaN",
+ "ObservableArray",
+ "Promise",
+ "bigint",
+ "boolean",
+ "byte",
+ "double",
+ "false",
+ "float",
+ "long",
+ "mixin",
+ "null",
+ "octet",
+ "optional",
+ "or",
+ "readonly",
+ "record",
+ "sequence",
+ "short",
+ "true",
+ "undefined",
+ "unsigned",
+ "void",
+].concat(argumentNameKeywords, stringTypes, typeNameKeywords);
+
+const punctuations = [
+ "(",
+ ")",
+ ",",
+ "...",
+ ":",
+ ";",
+ "<",
+ "=",
+ ">",
+ "?",
+ "*",
+ "[",
+ "]",
+ "{",
+ "}",
+];
+
+const reserved = [
+ // "constructor" is now a keyword
+ "_constructor",
+ "toString",
+ "_toString",
+];
+
+/**
+ * @typedef {ArrayItemType<ReturnType<typeof tokenise>>} Token
+ * @param {string} str
+ */
+function tokenise(str) {
+ const tokens = [];
+ let lastCharIndex = 0;
+ let trivia = "";
+ let line = 1;
+ let index = 0;
+ while (lastCharIndex < str.length) {
+ const nextChar = str.charAt(lastCharIndex);
+ let result = -1;
+
+ if (/[\t\n\r ]/.test(nextChar)) {
+ result = attemptTokenMatch("whitespace", { noFlushTrivia: true });
+ } else if (nextChar === "/") {
+ result = attemptTokenMatch("comment", { noFlushTrivia: true });
+ }
+
+ if (result !== -1) {
+ const currentTrivia = tokens.pop().value;
+ line += (currentTrivia.match(/\n/g) || []).length;
+ trivia += currentTrivia;
+ index -= 1;
+ } else if (/[-0-9.A-Z_a-z]/.test(nextChar)) {
+ result = attemptTokenMatch("decimal");
+ if (result === -1) {
+ result = attemptTokenMatch("integer");
+ }
+ if (result === -1) {
+ result = attemptTokenMatch("identifier");
+ const lastIndex = tokens.length - 1;
+ const token = tokens[lastIndex];
+ if (result !== -1) {
+ if (reserved.includes(token.value)) {
+ const message = `${(0,_productions_helpers_js__WEBPACK_IMPORTED_MODULE_1__.unescape)(
+ token.value
+ )} is a reserved identifier and must not be used.`;
+ throw new WebIDLParseError(
+ (0,_error_js__WEBPACK_IMPORTED_MODULE_0__.syntaxError)(tokens, lastIndex, null, message)
+ );
+ } else if (nonRegexTerminals.includes(token.value)) {
+ token.type = "inline";
+ }
+ }
+ }
+ } else if (nextChar === '"') {
+ result = attemptTokenMatch("string");
+ }
+
+ for (const punctuation of punctuations) {
+ if (str.startsWith(punctuation, lastCharIndex)) {
+ tokens.push({
+ type: "inline",
+ value: punctuation,
+ trivia,
+ line,
+ index,
+ });
+ trivia = "";
+ lastCharIndex += punctuation.length;
+ result = lastCharIndex;
+ break;
+ }
+ }
+
+ // other as the last try
+ if (result === -1) {
+ result = attemptTokenMatch("other");
+ }
+ if (result === -1) {
+ throw new Error("Token stream not progressing");
+ }
+ lastCharIndex = result;
+ index += 1;
+ }
+
+ // remaining trivia as eof
+ tokens.push({
+ type: "eof",
+ value: "",
+ trivia,
+ line,
+ index,
+ });
+
+ return tokens;
+
+ /**
+ * @param {keyof typeof tokenRe} type
+ * @param {object} options
+ * @param {boolean} [options.noFlushTrivia]
+ */
+ function attemptTokenMatch(type, { noFlushTrivia } = {}) {
+ const re = tokenRe[type];
+ re.lastIndex = lastCharIndex;
+ const result = re.exec(str);
+ if (result) {
+ tokens.push({ type, value: result[0], trivia, line, index });
+ if (!noFlushTrivia) {
+ trivia = "";
+ }
+ return re.lastIndex;
+ }
+ return -1;
+ }
+}
+
+class Tokeniser {
+ /**
+ * @param {string} idl
+ */
+ constructor(idl) {
+ this.source = tokenise(idl);
+ this.position = 0;
+ }
+
+ /**
+ * @param {string} message
+ * @return {never}
+ */
+ error(message) {
+ throw new WebIDLParseError(
+ (0,_error_js__WEBPACK_IMPORTED_MODULE_0__.syntaxError)(this.source, this.position, this.current, message)
+ );
+ }
+
+ /**
+ * @param {string} type
+ */
+ probeKind(type) {
+ return (
+ this.source.length > this.position &&
+ this.source[this.position].type === type
+ );
+ }
+
+ /**
+ * @param {string} value
+ */
+ probe(value) {
+ return (
+ this.probeKind("inline") && this.source[this.position].value === value
+ );
+ }
+
+ /**
+ * @param {...string} candidates
+ */
+ consumeKind(...candidates) {
+ for (const type of candidates) {
+ if (!this.probeKind(type)) continue;
+ const token = this.source[this.position];
+ this.position++;
+ return token;
+ }
+ }
+
+ /**
+ * @param {...string} candidates
+ */
+ consume(...candidates) {
+ if (!this.probeKind("inline")) return;
+ const token = this.source[this.position];
+ for (const value of candidates) {
+ if (token.value !== value) continue;
+ this.position++;
+ return token;
+ }
+ }
+
+ /**
+ * @param {string} value
+ */
+ consumeIdentifier(value) {
+ if (!this.probeKind("identifier")) {
+ return;
+ }
+ if (this.source[this.position].value !== value) {
+ return;
+ }
+ return this.consumeKind("identifier");
+ }
+
+ /**
+ * @param {number} position
+ */
+ unconsume(position) {
+ this.position = position;
+ }
+}
+
+class WebIDLParseError extends Error {
+ /**
+ * @param {object} options
+ * @param {string} options.message
+ * @param {string} options.bareMessage
+ * @param {string} options.context
+ * @param {number} options.line
+ * @param {*} options.sourceName
+ * @param {string} options.input
+ * @param {*[]} options.tokens
+ */
+ constructor({
+ message,
+ bareMessage,
+ context,
+ line,
+ sourceName,
+ input,
+ tokens,
+ }) {
+ super(message);
+
+ this.name = "WebIDLParseError"; // not to be mangled
+ this.bareMessage = bareMessage;
+ this.context = context;
+ this.line = line;
+ this.sourceName = sourceName;
+ this.input = input;
+ this.tokens = tokens;
+ }
+}
+
+
+/***/ }),
+/* 3 */
+/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
+
+__webpack_require__.r(__webpack_exports__);
+/* harmony export */ __webpack_require__.d(__webpack_exports__, {
+/* harmony export */ "syntaxError": () => (/* binding */ syntaxError),
+/* harmony export */ "validationError": () => (/* binding */ validationError)
+/* harmony export */ });
+/**
+ * @param {string} text
+ */
+function lastLine(text) {
+ const splitted = text.split("\n");
+ return splitted[splitted.length - 1];
+}
+
+function appendIfExist(base, target) {
+ let result = base;
+ if (target) {
+ result += ` ${target}`;
+ }
+ return result;
+}
+
+function contextAsText(node) {
+ const hierarchy = [node];
+ while (node && node.parent) {
+ const { parent } = node;
+ hierarchy.unshift(parent);
+ node = parent;
+ }
+ return hierarchy.map((n) => appendIfExist(n.type, n.name)).join(" -> ");
+}
+
+/**
+ * @typedef {object} WebIDL2ErrorOptions
+ * @property {"error" | "warning"} [level]
+ * @property {Function} [autofix]
+ * @property {string} [ruleName]
+ *
+ * @typedef {ReturnType<typeof error>} WebIDLErrorData
+ *
+ * @param {string} message error message
+ * @param {*} position
+ * @param {*} current
+ * @param {*} message
+ * @param {"Syntax" | "Validation"} kind error type
+ * @param {WebIDL2ErrorOptions=} options
+ */
+function error(
+ source,
+ position,
+ current,
+ message,
+ kind,
+ { level = "error", autofix, ruleName } = {}
+) {
+ /**
+ * @param {number} count
+ */
+ function sliceTokens(count) {
+ return count > 0
+ ? source.slice(position, position + count)
+ : source.slice(Math.max(position + count, 0), position);
+ }
+
+ /**
+ * @param {import("./tokeniser.js").Token[]} inputs
+ * @param {object} [options]
+ * @param {boolean} [options.precedes]
+ * @returns
+ */
+ function tokensToText(inputs, { precedes } = {}) {
+ const text = inputs.map((t) => t.trivia + t.value).join("");
+ const nextToken = source[position];
+ if (nextToken.type === "eof") {
+ return text;
+ }
+ if (precedes) {
+ return text + nextToken.trivia;
+ }
+ return text.slice(nextToken.trivia.length);
+ }
+
+ const maxTokens = 5; // arbitrary but works well enough
+ const line =
+ source[position].type !== "eof"
+ ? source[position].line
+ : source.length > 1
+ ? source[position - 1].line
+ : 1;
+
+ const precedingLastLine = lastLine(
+ tokensToText(sliceTokens(-maxTokens), { precedes: true })
+ );
+
+ const subsequentTokens = sliceTokens(maxTokens);
+ const subsequentText = tokensToText(subsequentTokens);
+ const subsequentFirstLine = subsequentText.split("\n")[0];
+
+ const spaced = " ".repeat(precedingLastLine.length) + "^";
+ const sourceContext = precedingLastLine + subsequentFirstLine + "\n" + spaced;
+
+ const contextType = kind === "Syntax" ? "since" : "inside";
+ const inSourceName = source.name ? ` in ${source.name}` : "";
+ const grammaticalContext =
+ current && current.name
+ ? `, ${contextType} \`${current.partial ? "partial " : ""}${contextAsText(
+ current
+ )}\``
+ : "";
+ const context = `${kind} error at line ${line}${inSourceName}${grammaticalContext}:\n${sourceContext}`;
+ return {
+ message: `${context} ${message}`,
+ bareMessage: message,
+ context,
+ line,
+ sourceName: source.name,
+ level,
+ ruleName,
+ autofix,
+ input: subsequentText,
+ tokens: subsequentTokens,
+ };
+}
+
+/**
+ * @param {string} message error message
+ */
+function syntaxError(source, position, current, message) {
+ return error(source, position, current, message, "Syntax");
+}
+
+/**
+ * @param {string} message error message
+ * @param {WebIDL2ErrorOptions} [options]
+ */
+function validationError(
+ token,
+ current,
+ ruleName,
+ message,
+ options = {}
+) {
+ options.ruleName = ruleName;
+ return error(
+ current.source,
+ token.index,
+ current,
+ message,
+ "Validation",
+ options
+ );
+}
+
+
+/***/ }),
+/* 4 */
+/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
+
+__webpack_require__.r(__webpack_exports__);
+/* harmony export */ __webpack_require__.d(__webpack_exports__, {
+/* harmony export */ "argument_list": () => (/* binding */ argument_list),
+/* harmony export */ "autoParenter": () => (/* binding */ autoParenter),
+/* harmony export */ "autofixAddExposedWindow": () => (/* binding */ autofixAddExposedWindow),
+/* harmony export */ "const_data": () => (/* binding */ const_data),
+/* harmony export */ "const_value": () => (/* binding */ const_value),
+/* harmony export */ "findLastIndex": () => (/* binding */ findLastIndex),
+/* harmony export */ "getFirstToken": () => (/* binding */ getFirstToken),
+/* harmony export */ "getLastIndentation": () => (/* binding */ getLastIndentation),
+/* harmony export */ "getMemberIndentation": () => (/* binding */ getMemberIndentation),
+/* harmony export */ "list": () => (/* binding */ list),
+/* harmony export */ "primitive_type": () => (/* binding */ primitive_type),
+/* harmony export */ "return_type": () => (/* binding */ return_type),
+/* harmony export */ "stringifier": () => (/* binding */ stringifier),
+/* harmony export */ "type_with_extended_attributes": () => (/* binding */ type_with_extended_attributes),
+/* harmony export */ "unescape": () => (/* binding */ unescape)
+/* harmony export */ });
+/* harmony import */ var _type_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(5);
+/* harmony import */ var _argument_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(11);
+/* harmony import */ var _extended_attributes_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(8);
+/* harmony import */ var _operation_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(13);
+/* harmony import */ var _attribute_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(14);
+/* harmony import */ var _tokeniser_js__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(2);
+
+
+
+
+
+
+
+/**
+ * @param {string} identifier
+ */
+function unescape(identifier) {
+ return identifier.startsWith("_") ? identifier.slice(1) : identifier;
+}
+
+/**
+ * Parses comma-separated list
+ * @param {import("../tokeniser.js").Tokeniser} tokeniser
+ * @param {object} args
+ * @param {Function} args.parser parser function for each item
+ * @param {boolean} [args.allowDangler] whether to allow dangling comma
+ * @param {string} [args.listName] the name to be shown on error messages
+ */
+function list(tokeniser, { parser, allowDangler, listName = "list" }) {
+ const first = parser(tokeniser);
+ if (!first) {
+ return [];
+ }
+ first.tokens.separator = tokeniser.consume(",");
+ const items = [first];
+ while (first.tokens.separator) {
+ const item = parser(tokeniser);
+ if (!item) {
+ if (!allowDangler) {
+ tokeniser.error(`Trailing comma in ${listName}`);
+ }
+ break;
+ }
+ item.tokens.separator = tokeniser.consume(",");
+ items.push(item);
+ if (!item.tokens.separator) break;
+ }
+ return items;
+}
+
+/**
+ * @param {import("../tokeniser.js").Tokeniser} tokeniser
+ */
+function const_value(tokeniser) {
+ return (
+ tokeniser.consumeKind("decimal", "integer") ||
+ tokeniser.consume("true", "false", "Infinity", "-Infinity", "NaN")
+ );
+}
+
+/**
+ * @param {object} token
+ * @param {string} token.type
+ * @param {string} token.value
+ */
+function const_data({ type, value }) {
+ switch (type) {
+ case "decimal":
+ case "integer":
+ return { type: "number", value };
+ case "string":
+ return { type: "string", value: value.slice(1, -1) };
+ }
+
+ switch (value) {
+ case "true":
+ case "false":
+ return { type: "boolean", value: value === "true" };
+ case "Infinity":
+ case "-Infinity":
+ return { type: "Infinity", negative: value.startsWith("-") };
+ case "[":
+ return { type: "sequence", value: [] };
+ case "{":
+ return { type: "dictionary" };
+ default:
+ return { type: value };
+ }
+}
+
+/**
+ * @param {import("../tokeniser.js").Tokeniser} tokeniser
+ */
+function primitive_type(tokeniser) {
+ function integer_type() {
+ const prefix = tokeniser.consume("unsigned");
+ const base = tokeniser.consume("short", "long");
+ if (base) {
+ const postfix = tokeniser.consume("long");
+ return new _type_js__WEBPACK_IMPORTED_MODULE_0__.Type({ source, tokens: { prefix, base, postfix } });
+ }
+ if (prefix) tokeniser.error("Failed to parse integer type");
+ }
+
+ function decimal_type() {
+ const prefix = tokeniser.consume("unrestricted");
+ const base = tokeniser.consume("float", "double");
+ if (base) {
+ return new _type_js__WEBPACK_IMPORTED_MODULE_0__.Type({ source, tokens: { prefix, base } });
+ }
+ if (prefix) tokeniser.error("Failed to parse float type");
+ }
+
+ const { source } = tokeniser;
+ const num_type = integer_type() || decimal_type();
+ if (num_type) return num_type;
+ const base = tokeniser.consume(
+ "bigint",
+ "boolean",
+ "byte",
+ "octet",
+ "undefined"
+ );
+ if (base) {
+ return new _type_js__WEBPACK_IMPORTED_MODULE_0__.Type({ source, tokens: { base } });
+ }
+}
+
+/**
+ * @param {import("../tokeniser.js").Tokeniser} tokeniser
+ */
+function argument_list(tokeniser) {
+ return list(tokeniser, {
+ parser: _argument_js__WEBPACK_IMPORTED_MODULE_1__.Argument.parse,
+ listName: "arguments list",
+ });
+}
+
+/**
+ * @param {import("../tokeniser.js").Tokeniser} tokeniser
+ * @param {string=} typeName (TODO: See Type.type for more details)
+ */
+function type_with_extended_attributes(tokeniser, typeName) {
+ const extAttrs = _extended_attributes_js__WEBPACK_IMPORTED_MODULE_2__.ExtendedAttributes.parse(tokeniser);
+ const ret = _type_js__WEBPACK_IMPORTED_MODULE_0__.Type.parse(tokeniser, typeName);
+ if (ret) autoParenter(ret).extAttrs = extAttrs;
+ return ret;
+}
+
+/**
+ * @param {import("../tokeniser.js").Tokeniser} tokeniser
+ * @param {string=} typeName (TODO: See Type.type for more details)
+ */
+function return_type(tokeniser, typeName) {
+ const typ = _type_js__WEBPACK_IMPORTED_MODULE_0__.Type.parse(tokeniser, typeName || "return-type");
+ if (typ) {
+ return typ;
+ }
+ const voidToken = tokeniser.consume("void");
+ if (voidToken) {
+ const ret = new _type_js__WEBPACK_IMPORTED_MODULE_0__.Type({
+ source: tokeniser.source,
+ tokens: { base: voidToken },
+ });
+ ret.type = "return-type";
+ return ret;
+ }
+}
+
+/**
+ * @param {import("../tokeniser.js").Tokeniser} tokeniser
+ */
+function stringifier(tokeniser) {
+ const special = tokeniser.consume("stringifier");
+ if (!special) return;
+ const member =
+ _attribute_js__WEBPACK_IMPORTED_MODULE_4__.Attribute.parse(tokeniser, { special }) ||
+ _operation_js__WEBPACK_IMPORTED_MODULE_3__.Operation.parse(tokeniser, { special }) ||
+ tokeniser.error("Unterminated stringifier");
+ return member;
+}
+
+/**
+ * @param {string} str
+ */
+function getLastIndentation(str) {
+ const lines = str.split("\n");
+ // the first line visually binds to the preceding token
+ if (lines.length) {
+ const match = lines[lines.length - 1].match(/^\s+/);
+ if (match) {
+ return match[0];
+ }
+ }
+ return "";
+}
+
+/**
+ * @param {string} parentTrivia
+ */
+function getMemberIndentation(parentTrivia) {
+ const indentation = getLastIndentation(parentTrivia);
+ const indentCh = indentation.includes("\t") ? "\t" : " ";
+ return indentation + indentCh;
+}
+
+/**
+ * @param {import("./interface.js").Interface} def
+ */
+function autofixAddExposedWindow(def) {
+ return () => {
+ if (def.extAttrs.length) {
+ const tokeniser = new _tokeniser_js__WEBPACK_IMPORTED_MODULE_5__.Tokeniser("Exposed=Window,");
+ const exposed = _extended_attributes_js__WEBPACK_IMPORTED_MODULE_2__.SimpleExtendedAttribute.parse(tokeniser);
+ exposed.tokens.separator = tokeniser.consume(",");
+ const existing = def.extAttrs[0];
+ if (!/^\s/.test(existing.tokens.name.trivia)) {
+ existing.tokens.name.trivia = ` ${existing.tokens.name.trivia}`;
+ }
+ def.extAttrs.unshift(exposed);
+ } else {
+ autoParenter(def).extAttrs = _extended_attributes_js__WEBPACK_IMPORTED_MODULE_2__.ExtendedAttributes.parse(
+ new _tokeniser_js__WEBPACK_IMPORTED_MODULE_5__.Tokeniser("[Exposed=Window]")
+ );
+ const trivia = def.tokens.base.trivia;
+ def.extAttrs.tokens.open.trivia = trivia;
+ def.tokens.base.trivia = `\n${getLastIndentation(trivia)}`;
+ }
+ };
+}
+
+/**
+ * Get the first syntax token for the given IDL object.
+ * @param {*} data
+ */
+function getFirstToken(data) {
+ if (data.extAttrs.length) {
+ return data.extAttrs.tokens.open;
+ }
+ if (data.type === "operation" && !data.special) {
+ return getFirstToken(data.idlType);
+ }
+ const tokens = Object.values(data.tokens).sort((x, y) => x.index - y.index);
+ return tokens[0];
+}
+
+/**
+ * @template T
+ * @param {T[]} array
+ * @param {(item: T) => boolean} predicate
+ */
+function findLastIndex(array, predicate) {
+ const index = array.slice().reverse().findIndex(predicate);
+ if (index === -1) {
+ return index;
+ }
+ return array.length - index - 1;
+}
+
+/**
+ * Returns a proxy that auto-assign `parent` field.
+ * @template {Record<string | symbol, any>} T
+ * @param {T} data
+ * @param {*} [parent] The object that will be assigned to `parent`.
+ * If absent, it will be `data` by default.
+ * @return {T}
+ */
+function autoParenter(data, parent) {
+ if (!parent) {
+ // Defaults to `data` unless specified otherwise.
+ parent = data;
+ }
+ if (!data) {
+ // This allows `autoParenter(undefined)` which again allows
+ // `autoParenter(parse())` where the function may return nothing.
+ return data;
+ }
+ const proxy = new Proxy(data, {
+ get(target, p) {
+ const value = target[p];
+ if (Array.isArray(value) && p !== "source") {
+ // Wraps the array so that any added items will also automatically
+ // get their `parent` values.
+ return autoParenter(value, target);
+ }
+ return value;
+ },
+ set(target, p, value) {
+ // @ts-ignore https://github.com/microsoft/TypeScript/issues/47357
+ target[p] = value;
+ if (!value) {
+ return true;
+ } else if (Array.isArray(value)) {
+ // Assigning an array will add `parent` to its items.
+ for (const item of value) {
+ if (typeof item.parent !== "undefined") {
+ item.parent = parent;
+ }
+ }
+ } else if (typeof value.parent !== "undefined") {
+ value.parent = parent;
+ }
+ return true;
+ },
+ });
+ return proxy;
+}
+
+
+/***/ }),
+/* 5 */
+/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
+
+__webpack_require__.r(__webpack_exports__);
+/* harmony export */ __webpack_require__.d(__webpack_exports__, {
+/* harmony export */ "Type": () => (/* binding */ Type)
+/* harmony export */ });
+/* harmony import */ var _base_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(6);
+/* harmony import */ var _helpers_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(4);
+/* harmony import */ var _tokeniser_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(2);
+/* harmony import */ var _error_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(3);
+/* harmony import */ var _validators_helpers_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(7);
+/* harmony import */ var _extended_attributes_js__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(8);
+
+
+
+
+
+
+
+/**
+ * @param {import("../tokeniser.js").Tokeniser} tokeniser
+ * @param {string} typeName
+ */
+function generic_type(tokeniser, typeName) {
+ const base = tokeniser.consume(
+ "FrozenArray",
+ "ObservableArray",
+ "Promise",
+ "sequence",
+ "record"
+ );
+ if (!base) {
+ return;
+ }
+ const ret = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.autoParenter)(
+ new Type({ source: tokeniser.source, tokens: { base } })
+ );
+ ret.tokens.open =
+ tokeniser.consume("<") ||
+ tokeniser.error(`No opening bracket after ${base.value}`);
+ switch (base.value) {
+ case "Promise": {
+ if (tokeniser.probe("["))
+ tokeniser.error("Promise type cannot have extended attribute");
+ const subtype =
+ (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.return_type)(tokeniser, typeName) ||
+ tokeniser.error("Missing Promise subtype");
+ ret.subtype.push(subtype);
+ break;
+ }
+ case "sequence":
+ case "FrozenArray":
+ case "ObservableArray": {
+ const subtype =
+ (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.type_with_extended_attributes)(tokeniser, typeName) ||
+ tokeniser.error(`Missing ${base.value} subtype`);
+ ret.subtype.push(subtype);
+ break;
+ }
+ case "record": {
+ if (tokeniser.probe("["))
+ tokeniser.error("Record key cannot have extended attribute");
+ const keyType =
+ tokeniser.consume(..._tokeniser_js__WEBPACK_IMPORTED_MODULE_2__.stringTypes) ||
+ tokeniser.error(`Record key must be one of: ${_tokeniser_js__WEBPACK_IMPORTED_MODULE_2__.stringTypes.join(", ")}`);
+ const keyIdlType = new Type({
+ source: tokeniser.source,
+ tokens: { base: keyType },
+ });
+ keyIdlType.tokens.separator =
+ tokeniser.consume(",") ||
+ tokeniser.error("Missing comma after record key type");
+ keyIdlType.type = typeName;
+ const valueType =
+ (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.type_with_extended_attributes)(tokeniser, typeName) ||
+ tokeniser.error("Error parsing generic type record");
+ ret.subtype.push(keyIdlType, valueType);
+ break;
+ }
+ }
+ if (!ret.idlType) tokeniser.error(`Error parsing generic type ${base.value}`);
+ ret.tokens.close =
+ tokeniser.consume(">") ||
+ tokeniser.error(`Missing closing bracket after ${base.value}`);
+ return ret.this;
+}
+
+/**
+ * @param {import("../tokeniser.js").Tokeniser} tokeniser
+ */
+function type_suffix(tokeniser, obj) {
+ const nullable = tokeniser.consume("?");
+ if (nullable) {
+ obj.tokens.nullable = nullable;
+ }
+ if (tokeniser.probe("?")) tokeniser.error("Can't nullable more than once");
+}
+
+/**
+ * @param {import("../tokeniser.js").Tokeniser} tokeniser
+ * @param {string} typeName
+ */
+function single_type(tokeniser, typeName) {
+ let ret = generic_type(tokeniser, typeName) || (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.primitive_type)(tokeniser);
+ if (!ret) {
+ const base =
+ tokeniser.consumeKind("identifier") ||
+ tokeniser.consume(..._tokeniser_js__WEBPACK_IMPORTED_MODULE_2__.stringTypes, ..._tokeniser_js__WEBPACK_IMPORTED_MODULE_2__.typeNameKeywords);
+ if (!base) {
+ return;
+ }
+ ret = new Type({ source: tokeniser.source, tokens: { base } });
+ if (tokeniser.probe("<"))
+ tokeniser.error(`Unsupported generic type ${base.value}`);
+ }
+ if (ret.generic === "Promise" && tokeniser.probe("?")) {
+ tokeniser.error("Promise type cannot be nullable");
+ }
+ ret.type = typeName || null;
+ type_suffix(tokeniser, ret);
+ if (ret.nullable && ret.idlType === "any")
+ tokeniser.error("Type `any` cannot be made nullable");
+ return ret;
+}
+
+/**
+ * @param {import("../tokeniser.js").Tokeniser} tokeniser
+ * @param {string} type
+ */
+function union_type(tokeniser, type) {
+ const tokens = {};
+ tokens.open = tokeniser.consume("(");
+ if (!tokens.open) return;
+ const ret = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.autoParenter)(new Type({ source: tokeniser.source, tokens }));
+ ret.type = type || null;
+ while (true) {
+ const typ =
+ (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.type_with_extended_attributes)(tokeniser) ||
+ tokeniser.error("No type after open parenthesis or 'or' in union type");
+ if (typ.idlType === "any")
+ tokeniser.error("Type `any` cannot be included in a union type");
+ if (typ.generic === "Promise")
+ tokeniser.error("Type `Promise` cannot be included in a union type");
+ ret.subtype.push(typ);
+ const or = tokeniser.consume("or");
+ if (or) {
+ typ.tokens.separator = or;
+ } else break;
+ }
+ if (ret.idlType.length < 2) {
+ tokeniser.error(
+ "At least two types are expected in a union type but found less"
+ );
+ }
+ tokens.close =
+ tokeniser.consume(")") || tokeniser.error("Unterminated union type");
+ type_suffix(tokeniser, ret);
+ return ret.this;
+}
+
+class Type extends _base_js__WEBPACK_IMPORTED_MODULE_0__.Base {
+ /**
+ * @param {import("../tokeniser.js").Tokeniser} tokeniser
+ * @param {string} typeName
+ */
+ static parse(tokeniser, typeName) {
+ return single_type(tokeniser, typeName) || union_type(tokeniser, typeName);
+ }
+
+ constructor({ source, tokens }) {
+ super({ source, tokens });
+ Object.defineProperty(this, "subtype", { value: [], writable: true });
+ this.extAttrs = new _extended_attributes_js__WEBPACK_IMPORTED_MODULE_5__.ExtendedAttributes({ source, tokens: {} });
+ }
+
+ get generic() {
+ if (this.subtype.length && this.tokens.base) {
+ return this.tokens.base.value;
+ }
+ return "";
+ }
+ get nullable() {
+ return Boolean(this.tokens.nullable);
+ }
+ get union() {
+ return Boolean(this.subtype.length) && !this.tokens.base;
+ }
+ get idlType() {
+ if (this.subtype.length) {
+ return this.subtype;
+ }
+ // Adding prefixes/postfixes for "unrestricted float", etc.
+ const name = [this.tokens.prefix, this.tokens.base, this.tokens.postfix]
+ .filter((t) => t)
+ .map((t) => t.value)
+ .join(" ");
+ return (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.unescape)(name);
+ }
+
+ *validate(defs) {
+ yield* this.extAttrs.validate(defs);
+
+ if (this.idlType === "void") {
+ const message = `\`void\` is now replaced by \`undefined\`. Refer to the \
+[relevant GitHub issue](https://github.com/whatwg/webidl/issues/60) \
+for more information.`;
+ yield (0,_error_js__WEBPACK_IMPORTED_MODULE_3__.validationError)(this.tokens.base, this, "replace-void", message, {
+ autofix: replaceVoid(this),
+ });
+ }
+
+ /*
+ * If a union is nullable, its subunions cannot include a dictionary
+ * If not, subunions may include dictionaries if each union is not nullable
+ */
+ const typedef = !this.union && defs.unique.get(this.idlType);
+ const target = this.union
+ ? this
+ : typedef && typedef.type === "typedef"
+ ? typedef.idlType
+ : undefined;
+ if (target && this.nullable) {
+ // do not allow any dictionary
+ const { reference } = (0,_validators_helpers_js__WEBPACK_IMPORTED_MODULE_4__.idlTypeIncludesDictionary)(target, defs) || {};
+ if (reference) {
+ const targetToken = (this.union ? reference : this).tokens.base;
+ const message = "Nullable union cannot include a dictionary type.";
+ yield (0,_error_js__WEBPACK_IMPORTED_MODULE_3__.validationError)(
+ targetToken,
+ this,
+ "no-nullable-union-dict",
+ message
+ );
+ }
+ } else {
+ // allow some dictionary
+ for (const subtype of this.subtype) {
+ yield* subtype.validate(defs);
+ }
+ }
+ }
+
+ /** @param {import("../writer.js").Writer} w */
+ write(w) {
+ const type_body = () => {
+ if (this.union || this.generic) {
+ return w.ts.wrap([
+ w.token(this.tokens.base, w.ts.generic),
+ w.token(this.tokens.open),
+ ...this.subtype.map((t) => t.write(w)),
+ w.token(this.tokens.close),
+ ]);
+ }
+ const firstToken = this.tokens.prefix || this.tokens.base;
+ const prefix = this.tokens.prefix
+ ? [this.tokens.prefix.value, w.ts.trivia(this.tokens.base.trivia)]
+ : [];
+ const ref = w.reference(
+ w.ts.wrap([
+ ...prefix,
+ this.tokens.base.value,
+ w.token(this.tokens.postfix),
+ ]),
+ {
+ unescaped: /** @type {string} (because it's not union) */ (
+ this.idlType
+ ),
+ context: this,
+ }
+ );
+ return w.ts.wrap([w.ts.trivia(firstToken.trivia), ref]);
+ };
+ return w.ts.wrap([
+ this.extAttrs.write(w),
+ type_body(),
+ w.token(this.tokens.nullable),
+ w.token(this.tokens.separator),
+ ]);
+ }
+}
+
+/**
+ * @param {Type} type
+ */
+function replaceVoid(type) {
+ return () => {
+ type.tokens.base.value = "undefined";
+ };
+}
+
+
+/***/ }),
+/* 6 */
+/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
+
+__webpack_require__.r(__webpack_exports__);
+/* harmony export */ __webpack_require__.d(__webpack_exports__, {
+/* harmony export */ "Base": () => (/* binding */ Base)
+/* harmony export */ });
+class Base {
+ /**
+ * @param {object} initializer
+ * @param {Base["source"]} initializer.source
+ * @param {Base["tokens"]} initializer.tokens
+ */
+ constructor({ source, tokens }) {
+ Object.defineProperties(this, {
+ source: { value: source },
+ tokens: { value: tokens, writable: true },
+ parent: { value: null, writable: true },
+ this: { value: this }, // useful when escaping from proxy
+ });
+ }
+
+ toJSON() {
+ const json = { type: undefined, name: undefined, inheritance: undefined };
+ let proto = this;
+ while (proto !== Object.prototype) {
+ const descMap = Object.getOwnPropertyDescriptors(proto);
+ for (const [key, value] of Object.entries(descMap)) {
+ if (value.enumerable || value.get) {
+ // @ts-ignore - allow indexing here
+ json[key] = this[key];
+ }
+ }
+ proto = Object.getPrototypeOf(proto);
+ }
+ return json;
+ }
+}
+
+
+/***/ }),
+/* 7 */
+/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
+
+__webpack_require__.r(__webpack_exports__);
+/* harmony export */ __webpack_require__.d(__webpack_exports__, {
+/* harmony export */ "dictionaryIncludesRequiredField": () => (/* binding */ dictionaryIncludesRequiredField),
+/* harmony export */ "idlTypeIncludesDictionary": () => (/* binding */ idlTypeIncludesDictionary)
+/* harmony export */ });
+/**
+ * @typedef {import("../productions/dictionary.js").Dictionary} Dictionary
+ *
+ * @param {*} idlType
+ * @param {import("../validator.js").Definitions} defs
+ * @param {object} [options]
+ * @param {boolean} [options.useNullableInner] use when the input idlType is nullable and you want to use its inner type
+ * @return {{ reference: *, dictionary: Dictionary }} the type reference that ultimately includes dictionary.
+ */
+function idlTypeIncludesDictionary(
+ idlType,
+ defs,
+ { useNullableInner } = {}
+) {
+ if (!idlType.union) {
+ const def = defs.unique.get(idlType.idlType);
+ if (!def) {
+ return;
+ }
+ if (def.type === "typedef") {
+ const { typedefIncludesDictionary } = defs.cache;
+ if (typedefIncludesDictionary.has(def)) {
+ // Note that this also halts when it met indeterminate state
+ // to prevent infinite recursion
+ return typedefIncludesDictionary.get(def);
+ }
+ defs.cache.typedefIncludesDictionary.set(def, undefined); // indeterminate state
+ const result = idlTypeIncludesDictionary(def.idlType, defs);
+ defs.cache.typedefIncludesDictionary.set(def, result);
+ if (result) {
+ return {
+ reference: idlType,
+ dictionary: result.dictionary,
+ };
+ }
+ }
+ if (def.type === "dictionary" && (useNullableInner || !idlType.nullable)) {
+ return {
+ reference: idlType,
+ dictionary: def,
+ };
+ }
+ }
+ for (const subtype of idlType.subtype) {
+ const result = idlTypeIncludesDictionary(subtype, defs);
+ if (result) {
+ if (subtype.union) {
+ return result;
+ }
+ return {
+ reference: subtype,
+ dictionary: result.dictionary,
+ };
+ }
+ }
+}
+
+/**
+ * @param {*} dict dictionary type
+ * @param {import("../validator.js").Definitions} defs
+ * @return {boolean}
+ */
+function dictionaryIncludesRequiredField(dict, defs) {
+ if (defs.cache.dictionaryIncludesRequiredField.has(dict)) {
+ return defs.cache.dictionaryIncludesRequiredField.get(dict);
+ }
+ // Set cached result to indeterminate to short-circuit circular definitions.
+ // The final result will be updated to true or false.
+ defs.cache.dictionaryIncludesRequiredField.set(dict, undefined);
+ let result = dict.members.some((field) => field.required);
+ if (!result && dict.inheritance) {
+ const superdict = defs.unique.get(dict.inheritance);
+ if (!superdict) {
+ // Assume required members in the supertype if it is unknown.
+ result = true;
+ } else if (dictionaryIncludesRequiredField(superdict, defs)) {
+ result = true;
+ }
+ }
+ defs.cache.dictionaryIncludesRequiredField.set(dict, result);
+ return result;
+}
+
+
+/***/ }),
+/* 8 */
+/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
+
+__webpack_require__.r(__webpack_exports__);
+/* harmony export */ __webpack_require__.d(__webpack_exports__, {
+/* harmony export */ "ExtendedAttributeParameters": () => (/* binding */ ExtendedAttributeParameters),
+/* harmony export */ "ExtendedAttributes": () => (/* binding */ ExtendedAttributes),
+/* harmony export */ "SimpleExtendedAttribute": () => (/* binding */ SimpleExtendedAttribute)
+/* harmony export */ });
+/* harmony import */ var _base_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(6);
+/* harmony import */ var _array_base_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(9);
+/* harmony import */ var _token_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(10);
+/* harmony import */ var _helpers_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(4);
+/* harmony import */ var _error_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(3);
+
+
+
+
+
+
+/**
+ * @param {import("../tokeniser.js").Tokeniser} tokeniser
+ * @param {string} tokenName
+ */
+function tokens(tokeniser, tokenName) {
+ return (0,_helpers_js__WEBPACK_IMPORTED_MODULE_3__.list)(tokeniser, {
+ parser: _token_js__WEBPACK_IMPORTED_MODULE_2__.WrappedToken.parser(tokeniser, tokenName),
+ listName: tokenName + " list",
+ });
+}
+
+const extAttrValueSyntax = ["identifier", "decimal", "integer", "string"];
+
+const shouldBeLegacyPrefixed = [
+ "NoInterfaceObject",
+ "LenientSetter",
+ "LenientThis",
+ "TreatNonObjectAsNull",
+ "Unforgeable",
+];
+
+const renamedLegacies = new Map([
+ .../** @type {[string, string][]} */ (
+ shouldBeLegacyPrefixed.map((name) => [name, `Legacy${name}`])
+ ),
+ ["NamedConstructor", "LegacyFactoryFunction"],
+ ["OverrideBuiltins", "LegacyOverrideBuiltIns"],
+ ["TreatNullAs", "LegacyNullToEmptyString"],
+]);
+
+/**
+ * This will allow a set of extended attribute values to be parsed.
+ * @param {import("../tokeniser.js").Tokeniser} tokeniser
+ */
+function extAttrListItems(tokeniser) {
+ for (const syntax of extAttrValueSyntax) {
+ const toks = tokens(tokeniser, syntax);
+ if (toks.length) {
+ return toks;
+ }
+ }
+ tokeniser.error(
+ `Expected identifiers, strings, decimals, or integers but none found`
+ );
+}
+
+class ExtendedAttributeParameters extends _base_js__WEBPACK_IMPORTED_MODULE_0__.Base {
+ /**
+ * @param {import("../tokeniser.js").Tokeniser} tokeniser
+ */
+ static parse(tokeniser) {
+ const tokens = { assign: tokeniser.consume("=") };
+ const ret = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_3__.autoParenter)(
+ new ExtendedAttributeParameters({ source: tokeniser.source, tokens })
+ );
+ ret.list = [];
+ if (tokens.assign) {
+ tokens.asterisk = tokeniser.consume("*");
+ if (tokens.asterisk) {
+ return ret.this;
+ }
+ tokens.secondaryName = tokeniser.consumeKind(...extAttrValueSyntax);
+ }
+ tokens.open = tokeniser.consume("(");
+ if (tokens.open) {
+ ret.list = ret.rhsIsList
+ ? // [Exposed=(Window,Worker)]
+ extAttrListItems(tokeniser)
+ : // [LegacyFactoryFunction=Audio(DOMString src)] or [Constructor(DOMString str)]
+ (0,_helpers_js__WEBPACK_IMPORTED_MODULE_3__.argument_list)(tokeniser);
+ tokens.close =
+ tokeniser.consume(")") ||
+ tokeniser.error("Unexpected token in extended attribute argument list");
+ } else if (tokens.assign && !tokens.secondaryName) {
+ tokeniser.error("No right hand side to extended attribute assignment");
+ }
+ return ret.this;
+ }
+
+ get rhsIsList() {
+ return (
+ this.tokens.assign && !this.tokens.asterisk && !this.tokens.secondaryName
+ );
+ }
+
+ get rhsType() {
+ if (this.rhsIsList) {
+ return this.list[0].tokens.value.type + "-list";
+ }
+ if (this.tokens.asterisk) {
+ return "*";
+ }
+ if (this.tokens.secondaryName) {
+ return this.tokens.secondaryName.type;
+ }
+ return null;
+ }
+
+ /** @param {import("../writer.js").Writer} w */
+ write(w) {
+ const { rhsType } = this;
+ return w.ts.wrap([
+ w.token(this.tokens.assign),
+ w.token(this.tokens.asterisk),
+ w.reference_token(this.tokens.secondaryName, this.parent),
+ w.token(this.tokens.open),
+ ...this.list.map((p) => {
+ return rhsType === "identifier-list"
+ ? w.identifier(p, this.parent)
+ : p.write(w);
+ }),
+ w.token(this.tokens.close),
+ ]);
+ }
+}
+
+class SimpleExtendedAttribute extends _base_js__WEBPACK_IMPORTED_MODULE_0__.Base {
+ /**
+ * @param {import("../tokeniser.js").Tokeniser} tokeniser
+ */
+ static parse(tokeniser) {
+ const name = tokeniser.consumeKind("identifier");
+ if (name) {
+ return new SimpleExtendedAttribute({
+ source: tokeniser.source,
+ tokens: { name },
+ params: ExtendedAttributeParameters.parse(tokeniser),
+ });
+ }
+ }
+
+ constructor({ source, tokens, params }) {
+ super({ source, tokens });
+ params.parent = this;
+ Object.defineProperty(this, "params", { value: params });
+ }
+
+ get type() {
+ return "extended-attribute";
+ }
+ get name() {
+ return this.tokens.name.value;
+ }
+ get rhs() {
+ const { rhsType: type, tokens, list } = this.params;
+ if (!type) {
+ return null;
+ }
+ const value = this.params.rhsIsList
+ ? list
+ : this.params.tokens.secondaryName
+ ? (0,_helpers_js__WEBPACK_IMPORTED_MODULE_3__.unescape)(tokens.secondaryName.value)
+ : null;
+ return { type, value };
+ }
+ get arguments() {
+ const { rhsIsList, list } = this.params;
+ if (!list || rhsIsList) {
+ return [];
+ }
+ return list;
+ }
+
+ *validate(defs) {
+ const { name } = this;
+ if (name === "LegacyNoInterfaceObject") {
+ const message = `\`[LegacyNoInterfaceObject]\` extended attribute is an \
+undesirable feature that may be removed from Web IDL in the future. Refer to the \
+[relevant upstream PR](https://github.com/whatwg/webidl/pull/609) for more \
+information.`;
+ yield (0,_error_js__WEBPACK_IMPORTED_MODULE_4__.validationError)(
+ this.tokens.name,
+ this,
+ "no-nointerfaceobject",
+ message,
+ { level: "warning" }
+ );
+ } else if (renamedLegacies.has(name)) {
+ const message = `\`[${name}]\` extended attribute is a legacy feature \
+that is now renamed to \`[${renamedLegacies.get(name)}]\`. Refer to the \
+[relevant upstream PR](https://github.com/whatwg/webidl/pull/870) for more \
+information.`;
+ yield (0,_error_js__WEBPACK_IMPORTED_MODULE_4__.validationError)(this.tokens.name, this, "renamed-legacy", message, {
+ level: "warning",
+ autofix: renameLegacyExtendedAttribute(this),
+ });
+ }
+ for (const arg of this.arguments) {
+ yield* arg.validate(defs);
+ }
+ }
+
+ /** @param {import("../writer.js").Writer} w */
+ write(w) {
+ return w.ts.wrap([
+ w.ts.trivia(this.tokens.name.trivia),
+ w.ts.extendedAttribute(
+ w.ts.wrap([
+ w.ts.extendedAttributeReference(this.name),
+ this.params.write(w),
+ ])
+ ),
+ w.token(this.tokens.separator),
+ ]);
+ }
+}
+
+/**
+ * @param {SimpleExtendedAttribute} extAttr
+ */
+function renameLegacyExtendedAttribute(extAttr) {
+ return () => {
+ const { name } = extAttr;
+ extAttr.tokens.name.value = renamedLegacies.get(name);
+ if (name === "TreatNullAs") {
+ extAttr.params.tokens = {};
+ }
+ };
+}
+
+// Note: we parse something simpler than the official syntax. It's all that ever
+// seems to be used
+class ExtendedAttributes extends _array_base_js__WEBPACK_IMPORTED_MODULE_1__.ArrayBase {
+ /**
+ * @param {import("../tokeniser.js").Tokeniser} tokeniser
+ */
+ static parse(tokeniser) {
+ const tokens = {};
+ tokens.open = tokeniser.consume("[");
+ const ret = new ExtendedAttributes({ source: tokeniser.source, tokens });
+ if (!tokens.open) return ret;
+ ret.push(
+ ...(0,_helpers_js__WEBPACK_IMPORTED_MODULE_3__.list)(tokeniser, {
+ parser: SimpleExtendedAttribute.parse,
+ listName: "extended attribute",
+ })
+ );
+ tokens.close =
+ tokeniser.consume("]") ||
+ tokeniser.error(
+ "Expected a closing token for the extended attribute list"
+ );
+ if (!ret.length) {
+ tokeniser.unconsume(tokens.close.index);
+ tokeniser.error("An extended attribute list must not be empty");
+ }
+ if (tokeniser.probe("[")) {
+ tokeniser.error(
+ "Illegal double extended attribute lists, consider merging them"
+ );
+ }
+ return ret;
+ }
+
+ *validate(defs) {
+ for (const extAttr of this) {
+ yield* extAttr.validate(defs);
+ }
+ }
+
+ /** @param {import("../writer.js").Writer} w */
+ write(w) {
+ if (!this.length) return "";
+ return w.ts.wrap([
+ w.token(this.tokens.open),
+ ...this.map((ea) => ea.write(w)),
+ w.token(this.tokens.close),
+ ]);
+ }
+}
+
+
+/***/ }),
+/* 9 */
+/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
+
+__webpack_require__.r(__webpack_exports__);
+/* harmony export */ __webpack_require__.d(__webpack_exports__, {
+/* harmony export */ "ArrayBase": () => (/* binding */ ArrayBase)
+/* harmony export */ });
+class ArrayBase extends Array {
+ constructor({ source, tokens }) {
+ super();
+ Object.defineProperties(this, {
+ source: { value: source },
+ tokens: { value: tokens },
+ parent: { value: null, writable: true },
+ });
+ }
+}
+
+
+/***/ }),
+/* 10 */
+/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
+
+__webpack_require__.r(__webpack_exports__);
+/* harmony export */ __webpack_require__.d(__webpack_exports__, {
+/* harmony export */ "Eof": () => (/* binding */ Eof),
+/* harmony export */ "WrappedToken": () => (/* binding */ WrappedToken)
+/* harmony export */ });
+/* harmony import */ var _base_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(6);
+/* harmony import */ var _helpers_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(4);
+
+
+
+class WrappedToken extends _base_js__WEBPACK_IMPORTED_MODULE_0__.Base {
+ /**
+ * @param {import("../tokeniser.js").Tokeniser} tokeniser
+ * @param {string} type
+ */
+ static parser(tokeniser, type) {
+ return () => {
+ const value = tokeniser.consumeKind(type);
+ if (value) {
+ return new WrappedToken({
+ source: tokeniser.source,
+ tokens: { value },
+ });
+ }
+ };
+ }
+
+ get value() {
+ return (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.unescape)(this.tokens.value.value);
+ }
+
+ /** @param {import("../writer.js").Writer} w */
+ write(w) {
+ return w.ts.wrap([
+ w.token(this.tokens.value),
+ w.token(this.tokens.separator),
+ ]);
+ }
+}
+
+class Eof extends WrappedToken {
+ /**
+ * @param {import("../tokeniser.js").Tokeniser} tokeniser
+ */
+ static parse(tokeniser) {
+ const value = tokeniser.consumeKind("eof");
+ if (value) {
+ return new Eof({ source: tokeniser.source, tokens: { value } });
+ }
+ }
+
+ get type() {
+ return "eof";
+ }
+}
+
+
+/***/ }),
+/* 11 */
+/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
+
+__webpack_require__.r(__webpack_exports__);
+/* harmony export */ __webpack_require__.d(__webpack_exports__, {
+/* harmony export */ "Argument": () => (/* binding */ Argument)
+/* harmony export */ });
+/* harmony import */ var _base_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(6);
+/* harmony import */ var _default_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(12);
+/* harmony import */ var _extended_attributes_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(8);
+/* harmony import */ var _helpers_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(4);
+/* harmony import */ var _tokeniser_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(2);
+/* harmony import */ var _error_js__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(3);
+/* harmony import */ var _validators_helpers_js__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(7);
+
+
+
+
+
+
+
+
+class Argument extends _base_js__WEBPACK_IMPORTED_MODULE_0__.Base {
+ /**
+ * @param {import("../tokeniser.js").Tokeniser} tokeniser
+ */
+ static parse(tokeniser) {
+ const start_position = tokeniser.position;
+ /** @type {Base["tokens"]} */
+ const tokens = {};
+ const ret = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_3__.autoParenter)(
+ new Argument({ source: tokeniser.source, tokens })
+ );
+ ret.extAttrs = _extended_attributes_js__WEBPACK_IMPORTED_MODULE_2__.ExtendedAttributes.parse(tokeniser);
+ tokens.optional = tokeniser.consume("optional");
+ ret.idlType = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_3__.type_with_extended_attributes)(tokeniser, "argument-type");
+ if (!ret.idlType) {
+ return tokeniser.unconsume(start_position);
+ }
+ if (!tokens.optional) {
+ tokens.variadic = tokeniser.consume("...");
+ }
+ tokens.name =
+ tokeniser.consumeKind("identifier") ||
+ tokeniser.consume(..._tokeniser_js__WEBPACK_IMPORTED_MODULE_4__.argumentNameKeywords);
+ if (!tokens.name) {
+ return tokeniser.unconsume(start_position);
+ }
+ ret.default = tokens.optional ? _default_js__WEBPACK_IMPORTED_MODULE_1__.Default.parse(tokeniser) : null;
+ return ret.this;
+ }
+
+ get type() {
+ return "argument";
+ }
+ get optional() {
+ return !!this.tokens.optional;
+ }
+ get variadic() {
+ return !!this.tokens.variadic;
+ }
+ get name() {
+ return (0,_helpers_js__WEBPACK_IMPORTED_MODULE_3__.unescape)(this.tokens.name.value);
+ }
+
+ /**
+ * @param {import("../validator.js").Definitions} defs
+ */
+ *validate(defs) {
+ yield* this.extAttrs.validate(defs);
+ yield* this.idlType.validate(defs);
+ const result = (0,_validators_helpers_js__WEBPACK_IMPORTED_MODULE_6__.idlTypeIncludesDictionary)(this.idlType, defs, {
+ useNullableInner: true,
+ });
+ if (result) {
+ if (this.idlType.nullable) {
+ const message = `Dictionary arguments cannot be nullable.`;
+ yield (0,_error_js__WEBPACK_IMPORTED_MODULE_5__.validationError)(
+ this.tokens.name,
+ this,
+ "no-nullable-dict-arg",
+ message
+ );
+ } else if (!this.optional) {
+ if (
+ this.parent &&
+ !(0,_validators_helpers_js__WEBPACK_IMPORTED_MODULE_6__.dictionaryIncludesRequiredField)(result.dictionary, defs) &&
+ isLastRequiredArgument(this)
+ ) {
+ const message = `Dictionary argument must be optional if it has no required fields`;
+ yield (0,_error_js__WEBPACK_IMPORTED_MODULE_5__.validationError)(
+ this.tokens.name,
+ this,
+ "dict-arg-optional",
+ message,
+ {
+ autofix: autofixDictionaryArgumentOptionality(this),
+ }
+ );
+ }
+ } else if (!this.default) {
+ const message = `Optional dictionary arguments must have a default value of \`{}\`.`;
+ yield (0,_error_js__WEBPACK_IMPORTED_MODULE_5__.validationError)(
+ this.tokens.name,
+ this,
+ "dict-arg-default",
+ message,
+ {
+ autofix: autofixOptionalDictionaryDefaultValue(this),
+ }
+ );
+ }
+ }
+ }
+
+ /** @param {import("../writer.js").Writer} w */
+ write(w) {
+ return w.ts.wrap([
+ this.extAttrs.write(w),
+ w.token(this.tokens.optional),
+ w.ts.type(this.idlType.write(w)),
+ w.token(this.tokens.variadic),
+ w.name_token(this.tokens.name, { data: this }),
+ this.default ? this.default.write(w) : "",
+ w.token(this.tokens.separator),
+ ]);
+ }
+}
+
+/**
+ * @param {Argument} arg
+ */
+function isLastRequiredArgument(arg) {
+ const list = arg.parent.arguments || arg.parent.list;
+ const index = list.indexOf(arg);
+ const requiredExists = list.slice(index + 1).some((a) => !a.optional);
+ return !requiredExists;
+}
+
+/**
+ * @param {Argument} arg
+ */
+function autofixDictionaryArgumentOptionality(arg) {
+ return () => {
+ const firstToken = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_3__.getFirstToken)(arg.idlType);
+ arg.tokens.optional = {
+ ...firstToken,
+ type: "optional",
+ value: "optional",
+ };
+ firstToken.trivia = " ";
+ autofixOptionalDictionaryDefaultValue(arg)();
+ };
+}
+
+/**
+ * @param {Argument} arg
+ */
+function autofixOptionalDictionaryDefaultValue(arg) {
+ return () => {
+ arg.default = _default_js__WEBPACK_IMPORTED_MODULE_1__.Default.parse(new _tokeniser_js__WEBPACK_IMPORTED_MODULE_4__.Tokeniser(" = {}"));
+ };
+}
+
+
+/***/ }),
+/* 12 */
+/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
+
+__webpack_require__.r(__webpack_exports__);
+/* harmony export */ __webpack_require__.d(__webpack_exports__, {
+/* harmony export */ "Default": () => (/* binding */ Default)
+/* harmony export */ });
+/* harmony import */ var _base_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(6);
+/* harmony import */ var _helpers_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(4);
+
+
+
+class Default extends _base_js__WEBPACK_IMPORTED_MODULE_0__.Base {
+ /**
+ * @param {import("../tokeniser.js").Tokeniser} tokeniser
+ */
+ static parse(tokeniser) {
+ const assign = tokeniser.consume("=");
+ if (!assign) {
+ return null;
+ }
+ const def =
+ (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.const_value)(tokeniser) ||
+ tokeniser.consumeKind("string") ||
+ tokeniser.consume("null", "[", "{") ||
+ tokeniser.error("No value for default");
+ const expression = [def];
+ if (def.value === "[") {
+ const close =
+ tokeniser.consume("]") ||
+ tokeniser.error("Default sequence value must be empty");
+ expression.push(close);
+ } else if (def.value === "{") {
+ const close =
+ tokeniser.consume("}") ||
+ tokeniser.error("Default dictionary value must be empty");
+ expression.push(close);
+ }
+ return new Default({
+ source: tokeniser.source,
+ tokens: { assign },
+ expression,
+ });
+ }
+
+ constructor({ source, tokens, expression }) {
+ super({ source, tokens });
+ expression.parent = this;
+ Object.defineProperty(this, "expression", { value: expression });
+ }
+
+ get type() {
+ return (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.const_data)(this.expression[0]).type;
+ }
+ get value() {
+ return (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.const_data)(this.expression[0]).value;
+ }
+ get negative() {
+ return (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.const_data)(this.expression[0]).negative;
+ }
+
+ /** @param {import("../writer.js").Writer} w */
+ write(w) {
+ return w.ts.wrap([
+ w.token(this.tokens.assign),
+ ...this.expression.map((t) => w.token(t)),
+ ]);
+ }
+}
+
+
+/***/ }),
+/* 13 */
+/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
+
+__webpack_require__.r(__webpack_exports__);
+/* harmony export */ __webpack_require__.d(__webpack_exports__, {
+/* harmony export */ "Operation": () => (/* binding */ Operation)
+/* harmony export */ });
+/* harmony import */ var _base_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(6);
+/* harmony import */ var _helpers_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(4);
+/* harmony import */ var _error_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(3);
+
+
+
+
+class Operation extends _base_js__WEBPACK_IMPORTED_MODULE_0__.Base {
+ /**
+ * @typedef {import("../tokeniser.js").Token} Token
+ *
+ * @param {import("../tokeniser.js").Tokeniser} tokeniser
+ * @param {object} [options]
+ * @param {Token} [options.special]
+ * @param {Token} [options.regular]
+ */
+ static parse(tokeniser, { special, regular } = {}) {
+ const tokens = { special };
+ const ret = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.autoParenter)(
+ new Operation({ source: tokeniser.source, tokens })
+ );
+ if (special && special.value === "stringifier") {
+ tokens.termination = tokeniser.consume(";");
+ if (tokens.termination) {
+ ret.arguments = [];
+ return ret;
+ }
+ }
+ if (!special && !regular) {
+ tokens.special = tokeniser.consume("getter", "setter", "deleter");
+ }
+ ret.idlType =
+ (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.return_type)(tokeniser) || tokeniser.error("Missing return type");
+ tokens.name =
+ tokeniser.consumeKind("identifier") || tokeniser.consume("includes");
+ tokens.open =
+ tokeniser.consume("(") || tokeniser.error("Invalid operation");
+ ret.arguments = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.argument_list)(tokeniser);
+ tokens.close =
+ tokeniser.consume(")") || tokeniser.error("Unterminated operation");
+ tokens.termination =
+ tokeniser.consume(";") ||
+ tokeniser.error("Unterminated operation, expected `;`");
+ return ret.this;
+ }
+
+ get type() {
+ return "operation";
+ }
+ get name() {
+ const { name } = this.tokens;
+ if (!name) {
+ return "";
+ }
+ return (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.unescape)(name.value);
+ }
+ get special() {
+ if (!this.tokens.special) {
+ return "";
+ }
+ return this.tokens.special.value;
+ }
+
+ *validate(defs) {
+ yield* this.extAttrs.validate(defs);
+ if (!this.name && ["", "static"].includes(this.special)) {
+ const message = `Regular or static operations must have both a return type and an identifier.`;
+ yield (0,_error_js__WEBPACK_IMPORTED_MODULE_2__.validationError)(this.tokens.open, this, "incomplete-op", message);
+ }
+ if (this.idlType) {
+ yield* this.idlType.validate(defs);
+ }
+ for (const argument of this.arguments) {
+ yield* argument.validate(defs);
+ }
+ }
+
+ /** @param {import("../writer.js").Writer} w */
+ write(w) {
+ const { parent } = this;
+ const body = this.idlType
+ ? [
+ w.ts.type(this.idlType.write(w)),
+ w.name_token(this.tokens.name, { data: this, parent }),
+ w.token(this.tokens.open),
+ w.ts.wrap(this.arguments.map((arg) => arg.write(w))),
+ w.token(this.tokens.close),
+ ]
+ : [];
+ return w.ts.definition(
+ w.ts.wrap([
+ this.extAttrs.write(w),
+ this.tokens.name
+ ? w.token(this.tokens.special)
+ : w.token(this.tokens.special, w.ts.nameless, { data: this, parent }),
+ ...body,
+ w.token(this.tokens.termination),
+ ]),
+ { data: this, parent }
+ );
+ }
+}
+
+
+/***/ }),
+/* 14 */
+/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
+
+__webpack_require__.r(__webpack_exports__);
+/* harmony export */ __webpack_require__.d(__webpack_exports__, {
+/* harmony export */ "Attribute": () => (/* binding */ Attribute)
+/* harmony export */ });
+/* harmony import */ var _error_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(3);
+/* harmony import */ var _validators_helpers_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(7);
+/* harmony import */ var _base_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(6);
+/* harmony import */ var _helpers_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(4);
+
+
+
+
+
+class Attribute extends _base_js__WEBPACK_IMPORTED_MODULE_2__.Base {
+ /**
+ * @param {import("../tokeniser.js").Tokeniser} tokeniser
+ * @param {object} [options]
+ * @param {import("../tokeniser.js").Token} [options.special]
+ * @param {boolean} [options.noInherit]
+ * @param {boolean} [options.readonly]
+ */
+ static parse(
+ tokeniser,
+ { special, noInherit = false, readonly = false } = {}
+ ) {
+ const start_position = tokeniser.position;
+ const tokens = { special };
+ const ret = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_3__.autoParenter)(
+ new Attribute({ source: tokeniser.source, tokens })
+ );
+ if (!special && !noInherit) {
+ tokens.special = tokeniser.consume("inherit");
+ }
+ if (ret.special === "inherit" && tokeniser.probe("readonly")) {
+ tokeniser.error("Inherited attributes cannot be read-only");
+ }
+ tokens.readonly = tokeniser.consume("readonly");
+ if (readonly && !tokens.readonly && tokeniser.probe("attribute")) {
+ tokeniser.error("Attributes must be readonly in this context");
+ }
+ tokens.base = tokeniser.consume("attribute");
+ if (!tokens.base) {
+ tokeniser.unconsume(start_position);
+ return;
+ }
+ ret.idlType =
+ (0,_helpers_js__WEBPACK_IMPORTED_MODULE_3__.type_with_extended_attributes)(tokeniser, "attribute-type") ||
+ tokeniser.error("Attribute lacks a type");
+ tokens.name =
+ tokeniser.consumeKind("identifier") ||
+ tokeniser.consume("async", "required") ||
+ tokeniser.error("Attribute lacks a name");
+ tokens.termination =
+ tokeniser.consume(";") ||
+ tokeniser.error("Unterminated attribute, expected `;`");
+ return ret.this;
+ }
+
+ get type() {
+ return "attribute";
+ }
+ get special() {
+ if (!this.tokens.special) {
+ return "";
+ }
+ return this.tokens.special.value;
+ }
+ get readonly() {
+ return !!this.tokens.readonly;
+ }
+ get name() {
+ return (0,_helpers_js__WEBPACK_IMPORTED_MODULE_3__.unescape)(this.tokens.name.value);
+ }
+
+ *validate(defs) {
+ yield* this.extAttrs.validate(defs);
+ yield* this.idlType.validate(defs);
+
+ switch (this.idlType.generic) {
+ case "sequence":
+ case "record": {
+ const message = `Attributes cannot accept ${this.idlType.generic} types.`;
+ yield (0,_error_js__WEBPACK_IMPORTED_MODULE_0__.validationError)(
+ this.tokens.name,
+ this,
+ "attr-invalid-type",
+ message
+ );
+ break;
+ }
+ default: {
+ const { reference } =
+ (0,_validators_helpers_js__WEBPACK_IMPORTED_MODULE_1__.idlTypeIncludesDictionary)(this.idlType, defs) || {};
+ if (reference) {
+ const targetToken = (this.idlType.union ? reference : this.idlType)
+ .tokens.base;
+ const message = "Attributes cannot accept dictionary types.";
+ yield (0,_error_js__WEBPACK_IMPORTED_MODULE_0__.validationError)(
+ targetToken,
+ this,
+ "attr-invalid-type",
+ message
+ );
+ }
+ }
+ }
+ }
+
+ /** @param {import("../writer.js").Writer} w */
+ write(w) {
+ const { parent } = this;
+ return w.ts.definition(
+ w.ts.wrap([
+ this.extAttrs.write(w),
+ w.token(this.tokens.special),
+ w.token(this.tokens.readonly),
+ w.token(this.tokens.base),
+ w.ts.type(this.idlType.write(w)),
+ w.name_token(this.tokens.name, { data: this, parent }),
+ w.token(this.tokens.termination),
+ ]),
+ { data: this, parent }
+ );
+ }
+}
+
+
+/***/ }),
+/* 15 */
+/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
+
+__webpack_require__.r(__webpack_exports__);
+/* harmony export */ __webpack_require__.d(__webpack_exports__, {
+/* harmony export */ "Enum": () => (/* binding */ Enum),
+/* harmony export */ "EnumValue": () => (/* binding */ EnumValue)
+/* harmony export */ });
+/* harmony import */ var _helpers_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(4);
+/* harmony import */ var _token_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(10);
+/* harmony import */ var _base_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(6);
+
+
+
+
+class EnumValue extends _token_js__WEBPACK_IMPORTED_MODULE_1__.WrappedToken {
+ /**
+ * @param {import("../tokeniser.js").Tokeniser} tokeniser
+ */
+ static parse(tokeniser) {
+ const value = tokeniser.consumeKind("string");
+ if (value) {
+ return new EnumValue({ source: tokeniser.source, tokens: { value } });
+ }
+ }
+
+ get type() {
+ return "enum-value";
+ }
+ get value() {
+ return super.value.slice(1, -1);
+ }
+
+ /** @param {import("../writer.js").Writer} w */
+ write(w) {
+ const { parent } = this;
+ return w.ts.wrap([
+ w.ts.trivia(this.tokens.value.trivia),
+ w.ts.definition(
+ w.ts.wrap(['"', w.ts.name(this.value, { data: this, parent }), '"']),
+ { data: this, parent }
+ ),
+ w.token(this.tokens.separator),
+ ]);
+ }
+}
+
+class Enum extends _base_js__WEBPACK_IMPORTED_MODULE_2__.Base {
+ /**
+ * @param {import("../tokeniser.js").Tokeniser} tokeniser
+ */
+ static parse(tokeniser) {
+ /** @type {Base["tokens"]} */
+ const tokens = {};
+ tokens.base = tokeniser.consume("enum");
+ if (!tokens.base) {
+ return;
+ }
+ tokens.name =
+ tokeniser.consumeKind("identifier") ||
+ tokeniser.error("No name for enum");
+ const ret = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_0__.autoParenter)(new Enum({ source: tokeniser.source, tokens }));
+ tokeniser.current = ret.this;
+ tokens.open = tokeniser.consume("{") || tokeniser.error("Bodyless enum");
+ ret.values = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_0__.list)(tokeniser, {
+ parser: EnumValue.parse,
+ allowDangler: true,
+ listName: "enumeration",
+ });
+ if (tokeniser.probeKind("string")) {
+ tokeniser.error("No comma between enum values");
+ }
+ tokens.close =
+ tokeniser.consume("}") || tokeniser.error("Unexpected value in enum");
+ if (!ret.values.length) {
+ tokeniser.error("No value in enum");
+ }
+ tokens.termination =
+ tokeniser.consume(";") || tokeniser.error("No semicolon after enum");
+ return ret.this;
+ }
+
+ get type() {
+ return "enum";
+ }
+ get name() {
+ return (0,_helpers_js__WEBPACK_IMPORTED_MODULE_0__.unescape)(this.tokens.name.value);
+ }
+
+ /** @param {import("../writer.js").Writer} w */
+ write(w) {
+ return w.ts.definition(
+ w.ts.wrap([
+ this.extAttrs.write(w),
+ w.token(this.tokens.base),
+ w.name_token(this.tokens.name, { data: this }),
+ w.token(this.tokens.open),
+ w.ts.wrap(this.values.map((v) => v.write(w))),
+ w.token(this.tokens.close),
+ w.token(this.tokens.termination),
+ ]),
+ { data: this }
+ );
+ }
+}
+
+
+/***/ }),
+/* 16 */
+/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
+
+__webpack_require__.r(__webpack_exports__);
+/* harmony export */ __webpack_require__.d(__webpack_exports__, {
+/* harmony export */ "Includes": () => (/* binding */ Includes)
+/* harmony export */ });
+/* harmony import */ var _base_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(6);
+/* harmony import */ var _helpers_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(4);
+
+
+
+class Includes extends _base_js__WEBPACK_IMPORTED_MODULE_0__.Base {
+ /**
+ * @param {import("../tokeniser.js").Tokeniser} tokeniser
+ */
+ static parse(tokeniser) {
+ const target = tokeniser.consumeKind("identifier");
+ if (!target) {
+ return;
+ }
+ const tokens = { target };
+ tokens.includes = tokeniser.consume("includes");
+ if (!tokens.includes) {
+ tokeniser.unconsume(target.index);
+ return;
+ }
+ tokens.mixin =
+ tokeniser.consumeKind("identifier") ||
+ tokeniser.error("Incomplete includes statement");
+ tokens.termination =
+ tokeniser.consume(";") ||
+ tokeniser.error("No terminating ; for includes statement");
+ return new Includes({ source: tokeniser.source, tokens });
+ }
+
+ get type() {
+ return "includes";
+ }
+ get target() {
+ return (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.unescape)(this.tokens.target.value);
+ }
+ get includes() {
+ return (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.unescape)(this.tokens.mixin.value);
+ }
+
+ /** @param {import("../writer.js").Writer} w */
+ write(w) {
+ return w.ts.definition(
+ w.ts.wrap([
+ this.extAttrs.write(w),
+ w.reference_token(this.tokens.target, this),
+ w.token(this.tokens.includes),
+ w.reference_token(this.tokens.mixin, this),
+ w.token(this.tokens.termination),
+ ]),
+ { data: this }
+ );
+ }
+}
+
+
+/***/ }),
+/* 17 */
+/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
+
+__webpack_require__.r(__webpack_exports__);
+/* harmony export */ __webpack_require__.d(__webpack_exports__, {
+/* harmony export */ "Typedef": () => (/* binding */ Typedef)
+/* harmony export */ });
+/* harmony import */ var _base_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(6);
+/* harmony import */ var _helpers_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(4);
+
+
+
+class Typedef extends _base_js__WEBPACK_IMPORTED_MODULE_0__.Base {
+ /**
+ * @param {import("../tokeniser.js").Tokeniser} tokeniser
+ */
+ static parse(tokeniser) {
+ /** @type {Base["tokens"]} */
+ const tokens = {};
+ const ret = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.autoParenter)(new Typedef({ source: tokeniser.source, tokens }));
+ tokens.base = tokeniser.consume("typedef");
+ if (!tokens.base) {
+ return;
+ }
+ ret.idlType =
+ (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.type_with_extended_attributes)(tokeniser, "typedef-type") ||
+ tokeniser.error("Typedef lacks a type");
+ tokens.name =
+ tokeniser.consumeKind("identifier") ||
+ tokeniser.error("Typedef lacks a name");
+ tokeniser.current = ret.this;
+ tokens.termination =
+ tokeniser.consume(";") ||
+ tokeniser.error("Unterminated typedef, expected `;`");
+ return ret.this;
+ }
+
+ get type() {
+ return "typedef";
+ }
+ get name() {
+ return (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.unescape)(this.tokens.name.value);
+ }
+
+ *validate(defs) {
+ yield* this.idlType.validate(defs);
+ }
+
+ /** @param {import("../writer.js").Writer} w */
+ write(w) {
+ return w.ts.definition(
+ w.ts.wrap([
+ this.extAttrs.write(w),
+ w.token(this.tokens.base),
+ w.ts.type(this.idlType.write(w)),
+ w.name_token(this.tokens.name, { data: this }),
+ w.token(this.tokens.termination),
+ ]),
+ { data: this }
+ );
+ }
+}
+
+
+/***/ }),
+/* 18 */
+/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
+
+__webpack_require__.r(__webpack_exports__);
+/* harmony export */ __webpack_require__.d(__webpack_exports__, {
+/* harmony export */ "CallbackFunction": () => (/* binding */ CallbackFunction)
+/* harmony export */ });
+/* harmony import */ var _base_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(6);
+/* harmony import */ var _helpers_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(4);
+
+
+
+class CallbackFunction extends _base_js__WEBPACK_IMPORTED_MODULE_0__.Base {
+ /**
+ * @param {import("../tokeniser.js").Tokeniser} tokeniser
+ */
+ static parse(tokeniser, base) {
+ const tokens = { base };
+ const ret = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.autoParenter)(
+ new CallbackFunction({ source: tokeniser.source, tokens })
+ );
+ tokens.name =
+ tokeniser.consumeKind("identifier") ||
+ tokeniser.error("Callback lacks a name");
+ tokeniser.current = ret.this;
+ tokens.assign =
+ tokeniser.consume("=") || tokeniser.error("Callback lacks an assignment");
+ ret.idlType =
+ (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.return_type)(tokeniser) || tokeniser.error("Callback lacks a return type");
+ tokens.open =
+ tokeniser.consume("(") ||
+ tokeniser.error("Callback lacks parentheses for arguments");
+ ret.arguments = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.argument_list)(tokeniser);
+ tokens.close =
+ tokeniser.consume(")") || tokeniser.error("Unterminated callback");
+ tokens.termination =
+ tokeniser.consume(";") ||
+ tokeniser.error("Unterminated callback, expected `;`");
+ return ret.this;
+ }
+
+ get type() {
+ return "callback";
+ }
+ get name() {
+ return (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.unescape)(this.tokens.name.value);
+ }
+
+ *validate(defs) {
+ yield* this.extAttrs.validate(defs);
+ yield* this.idlType.validate(defs);
+ }
+
+ /** @param {import("../writer.js").Writer} w */
+ write(w) {
+ return w.ts.definition(
+ w.ts.wrap([
+ this.extAttrs.write(w),
+ w.token(this.tokens.base),
+ w.name_token(this.tokens.name, { data: this }),
+ w.token(this.tokens.assign),
+ w.ts.type(this.idlType.write(w)),
+ w.token(this.tokens.open),
+ ...this.arguments.map((arg) => arg.write(w)),
+ w.token(this.tokens.close),
+ w.token(this.tokens.termination),
+ ]),
+ { data: this }
+ );
+ }
+}
+
+
+/***/ }),
+/* 19 */
+/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
+
+__webpack_require__.r(__webpack_exports__);
+/* harmony export */ __webpack_require__.d(__webpack_exports__, {
+/* harmony export */ "Interface": () => (/* binding */ Interface)
+/* harmony export */ });
+/* harmony import */ var _container_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(20);
+/* harmony import */ var _attribute_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(14);
+/* harmony import */ var _operation_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(13);
+/* harmony import */ var _constant_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(21);
+/* harmony import */ var _iterable_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(22);
+/* harmony import */ var _helpers_js__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(4);
+/* harmony import */ var _error_js__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(3);
+/* harmony import */ var _validators_interface_js__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(23);
+/* harmony import */ var _constructor_js__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(24);
+/* harmony import */ var _tokeniser_js__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(2);
+/* harmony import */ var _extended_attributes_js__WEBPACK_IMPORTED_MODULE_10__ = __webpack_require__(8);
+
+
+
+
+
+
+
+
+
+
+
+
+/**
+ * @param {import("../tokeniser.js").Tokeniser} tokeniser
+ */
+function static_member(tokeniser) {
+ const special = tokeniser.consume("static");
+ if (!special) return;
+ const member =
+ _attribute_js__WEBPACK_IMPORTED_MODULE_1__.Attribute.parse(tokeniser, { special }) ||
+ _operation_js__WEBPACK_IMPORTED_MODULE_2__.Operation.parse(tokeniser, { special }) ||
+ tokeniser.error("No body in static member");
+ return member;
+}
+
+class Interface extends _container_js__WEBPACK_IMPORTED_MODULE_0__.Container {
+ /**
+ * @param {import("../tokeniser.js").Tokeniser} tokeniser
+ */
+ static parse(tokeniser, base, { partial = null } = {}) {
+ const tokens = { partial, base };
+ return _container_js__WEBPACK_IMPORTED_MODULE_0__.Container.parse(
+ tokeniser,
+ new Interface({ source: tokeniser.source, tokens }),
+ {
+ inheritable: !partial,
+ allowedMembers: [
+ [_constant_js__WEBPACK_IMPORTED_MODULE_3__.Constant.parse],
+ [_constructor_js__WEBPACK_IMPORTED_MODULE_8__.Constructor.parse],
+ [static_member],
+ [_helpers_js__WEBPACK_IMPORTED_MODULE_5__.stringifier],
+ [_iterable_js__WEBPACK_IMPORTED_MODULE_4__.IterableLike.parse],
+ [_attribute_js__WEBPACK_IMPORTED_MODULE_1__.Attribute.parse],
+ [_operation_js__WEBPACK_IMPORTED_MODULE_2__.Operation.parse],
+ ],
+ }
+ );
+ }
+
+ get type() {
+ return "interface";
+ }
+
+ *validate(defs) {
+ yield* this.extAttrs.validate(defs);
+ if (
+ !this.partial &&
+ this.extAttrs.every((extAttr) => extAttr.name !== "Exposed")
+ ) {
+ const message = `Interfaces must have \`[Exposed]\` extended attribute. \
+To fix, add, for example, \`[Exposed=Window]\`. Please also consider carefully \
+if your interface should also be exposed in a Worker scope. Refer to the \
+[WebIDL spec section on Exposed](https://heycam.github.io/webidl/#Exposed) \
+for more information.`;
+ yield (0,_error_js__WEBPACK_IMPORTED_MODULE_6__.validationError)(
+ this.tokens.name,
+ this,
+ "require-exposed",
+ message,
+ {
+ autofix: (0,_helpers_js__WEBPACK_IMPORTED_MODULE_5__.autofixAddExposedWindow)(this),
+ }
+ );
+ }
+ const oldConstructors = this.extAttrs.filter(
+ (extAttr) => extAttr.name === "Constructor"
+ );
+ for (const constructor of oldConstructors) {
+ const message = `Constructors should now be represented as a \`constructor()\` operation on the interface \
+instead of \`[Constructor]\` extended attribute. Refer to the \
+[WebIDL spec section on constructor operations](https://heycam.github.io/webidl/#idl-constructors) \
+for more information.`;
+ yield (0,_error_js__WEBPACK_IMPORTED_MODULE_6__.validationError)(
+ constructor.tokens.name,
+ this,
+ "constructor-member",
+ message,
+ {
+ autofix: autofixConstructor(this, constructor),
+ }
+ );
+ }
+
+ const isGlobal = this.extAttrs.some((extAttr) => extAttr.name === "Global");
+ if (isGlobal) {
+ const factoryFunctions = this.extAttrs.filter(
+ (extAttr) => extAttr.name === "LegacyFactoryFunction"
+ );
+ for (const named of factoryFunctions) {
+ const message = `Interfaces marked as \`[Global]\` cannot have factory functions.`;
+ yield (0,_error_js__WEBPACK_IMPORTED_MODULE_6__.validationError)(
+ named.tokens.name,
+ this,
+ "no-constructible-global",
+ message
+ );
+ }
+
+ const constructors = this.members.filter(
+ (member) => member.type === "constructor"
+ );
+ for (const named of constructors) {
+ const message = `Interfaces marked as \`[Global]\` cannot have constructors.`;
+ yield (0,_error_js__WEBPACK_IMPORTED_MODULE_6__.validationError)(
+ named.tokens.base,
+ this,
+ "no-constructible-global",
+ message
+ );
+ }
+ }
+
+ yield* super.validate(defs);
+ if (!this.partial) {
+ yield* (0,_validators_interface_js__WEBPACK_IMPORTED_MODULE_7__.checkInterfaceMemberDuplication)(defs, this);
+ }
+ }
+}
+
+function autofixConstructor(interfaceDef, constructorExtAttr) {
+ interfaceDef = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_5__.autoParenter)(interfaceDef);
+ return () => {
+ const indentation = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_5__.getLastIndentation)(
+ interfaceDef.extAttrs.tokens.open.trivia
+ );
+ const memberIndent = interfaceDef.members.length
+ ? (0,_helpers_js__WEBPACK_IMPORTED_MODULE_5__.getLastIndentation)((0,_helpers_js__WEBPACK_IMPORTED_MODULE_5__.getFirstToken)(interfaceDef.members[0]).trivia)
+ : (0,_helpers_js__WEBPACK_IMPORTED_MODULE_5__.getMemberIndentation)(indentation);
+ const constructorOp = _constructor_js__WEBPACK_IMPORTED_MODULE_8__.Constructor.parse(
+ new _tokeniser_js__WEBPACK_IMPORTED_MODULE_9__.Tokeniser(`\n${memberIndent}constructor();`)
+ );
+ constructorOp.extAttrs = new _extended_attributes_js__WEBPACK_IMPORTED_MODULE_10__.ExtendedAttributes({
+ source: interfaceDef.source,
+ tokens: {},
+ });
+ (0,_helpers_js__WEBPACK_IMPORTED_MODULE_5__.autoParenter)(constructorOp).arguments = constructorExtAttr.arguments;
+
+ const existingIndex = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_5__.findLastIndex)(
+ interfaceDef.members,
+ (m) => m.type === "constructor"
+ );
+ interfaceDef.members.splice(existingIndex + 1, 0, constructorOp);
+
+ const { close } = interfaceDef.tokens;
+ if (!close.trivia.includes("\n")) {
+ close.trivia += `\n${indentation}`;
+ }
+
+ const { extAttrs } = interfaceDef;
+ const index = extAttrs.indexOf(constructorExtAttr);
+ const removed = extAttrs.splice(index, 1);
+ if (!extAttrs.length) {
+ extAttrs.tokens.open = extAttrs.tokens.close = undefined;
+ } else if (extAttrs.length === index) {
+ extAttrs[index - 1].tokens.separator = undefined;
+ } else if (!extAttrs[index].tokens.name.trivia.trim()) {
+ extAttrs[index].tokens.name.trivia = removed[0].tokens.name.trivia;
+ }
+ };
+}
+
+
+/***/ }),
+/* 20 */
+/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
+
+__webpack_require__.r(__webpack_exports__);
+/* harmony export */ __webpack_require__.d(__webpack_exports__, {
+/* harmony export */ "Container": () => (/* binding */ Container)
+/* harmony export */ });
+/* harmony import */ var _base_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(6);
+/* harmony import */ var _extended_attributes_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(8);
+/* harmony import */ var _helpers_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(4);
+
+
+
+
+/**
+ * @param {import("../tokeniser.js").Tokeniser} tokeniser
+ */
+function inheritance(tokeniser) {
+ const colon = tokeniser.consume(":");
+ if (!colon) {
+ return {};
+ }
+ const inheritance =
+ tokeniser.consumeKind("identifier") ||
+ tokeniser.error("Inheritance lacks a type");
+ return { colon, inheritance };
+}
+
+class Container extends _base_js__WEBPACK_IMPORTED_MODULE_0__.Base {
+ /**
+ * @param {import("../tokeniser.js").Tokeniser} tokeniser
+ * @param {*} instance TODO: This should be {T extends Container}, but see https://github.com/microsoft/TypeScript/issues/4628
+ * @param {*} args
+ */
+ static parse(tokeniser, instance, { inheritable, allowedMembers }) {
+ const { tokens, type } = instance;
+ tokens.name =
+ tokeniser.consumeKind("identifier") ||
+ tokeniser.error(`Missing name in ${type}`);
+ tokeniser.current = instance;
+ instance = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_2__.autoParenter)(instance);
+ if (inheritable) {
+ Object.assign(tokens, inheritance(tokeniser));
+ }
+ tokens.open = tokeniser.consume("{") || tokeniser.error(`Bodyless ${type}`);
+ instance.members = [];
+ while (true) {
+ tokens.close = tokeniser.consume("}");
+ if (tokens.close) {
+ tokens.termination =
+ tokeniser.consume(";") ||
+ tokeniser.error(`Missing semicolon after ${type}`);
+ return instance.this;
+ }
+ const ea = _extended_attributes_js__WEBPACK_IMPORTED_MODULE_1__.ExtendedAttributes.parse(tokeniser);
+ let mem;
+ for (const [parser, ...args] of allowedMembers) {
+ mem = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_2__.autoParenter)(parser(tokeniser, ...args));
+ if (mem) {
+ break;
+ }
+ }
+ if (!mem) {
+ tokeniser.error("Unknown member");
+ }
+ mem.extAttrs = ea;
+ instance.members.push(mem.this);
+ }
+ }
+
+ get partial() {
+ return !!this.tokens.partial;
+ }
+ get name() {
+ return (0,_helpers_js__WEBPACK_IMPORTED_MODULE_2__.unescape)(this.tokens.name.value);
+ }
+ get inheritance() {
+ if (!this.tokens.inheritance) {
+ return null;
+ }
+ return (0,_helpers_js__WEBPACK_IMPORTED_MODULE_2__.unescape)(this.tokens.inheritance.value);
+ }
+
+ *validate(defs) {
+ for (const member of this.members) {
+ if (member.validate) {
+ yield* member.validate(defs);
+ }
+ }
+ }
+
+ /** @param {import("../writer.js").Writer} w */
+ write(w) {
+ const inheritance = () => {
+ if (!this.tokens.inheritance) {
+ return "";
+ }
+ return w.ts.wrap([
+ w.token(this.tokens.colon),
+ w.ts.trivia(this.tokens.inheritance.trivia),
+ w.ts.inheritance(
+ w.reference(this.tokens.inheritance.value, { context: this })
+ ),
+ ]);
+ };
+
+ return w.ts.definition(
+ w.ts.wrap([
+ this.extAttrs.write(w),
+ w.token(this.tokens.callback),
+ w.token(this.tokens.partial),
+ w.token(this.tokens.base),
+ w.token(this.tokens.mixin),
+ w.name_token(this.tokens.name, { data: this }),
+ inheritance(),
+ w.token(this.tokens.open),
+ w.ts.wrap(this.members.map((m) => m.write(w))),
+ w.token(this.tokens.close),
+ w.token(this.tokens.termination),
+ ]),
+ { data: this }
+ );
+ }
+}
+
+
+/***/ }),
+/* 21 */
+/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
+
+__webpack_require__.r(__webpack_exports__);
+/* harmony export */ __webpack_require__.d(__webpack_exports__, {
+/* harmony export */ "Constant": () => (/* binding */ Constant)
+/* harmony export */ });
+/* harmony import */ var _base_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(6);
+/* harmony import */ var _type_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(5);
+/* harmony import */ var _helpers_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(4);
+
+
+
+
+class Constant extends _base_js__WEBPACK_IMPORTED_MODULE_0__.Base {
+ /**
+ * @param {import("../tokeniser.js").Tokeniser} tokeniser
+ */
+ static parse(tokeniser) {
+ /** @type {Base["tokens"]} */
+ const tokens = {};
+ tokens.base = tokeniser.consume("const");
+ if (!tokens.base) {
+ return;
+ }
+ let idlType = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_2__.primitive_type)(tokeniser);
+ if (!idlType) {
+ const base =
+ tokeniser.consumeKind("identifier") ||
+ tokeniser.error("Const lacks a type");
+ idlType = new _type_js__WEBPACK_IMPORTED_MODULE_1__.Type({ source: tokeniser.source, tokens: { base } });
+ }
+ if (tokeniser.probe("?")) {
+ tokeniser.error("Unexpected nullable constant type");
+ }
+ idlType.type = "const-type";
+ tokens.name =
+ tokeniser.consumeKind("identifier") ||
+ tokeniser.error("Const lacks a name");
+ tokens.assign =
+ tokeniser.consume("=") || tokeniser.error("Const lacks value assignment");
+ tokens.value =
+ (0,_helpers_js__WEBPACK_IMPORTED_MODULE_2__.const_value)(tokeniser) || tokeniser.error("Const lacks a value");
+ tokens.termination =
+ tokeniser.consume(";") ||
+ tokeniser.error("Unterminated const, expected `;`");
+ const ret = new Constant({ source: tokeniser.source, tokens });
+ (0,_helpers_js__WEBPACK_IMPORTED_MODULE_2__.autoParenter)(ret).idlType = idlType;
+ return ret;
+ }
+
+ get type() {
+ return "const";
+ }
+ get name() {
+ return (0,_helpers_js__WEBPACK_IMPORTED_MODULE_2__.unescape)(this.tokens.name.value);
+ }
+ get value() {
+ return (0,_helpers_js__WEBPACK_IMPORTED_MODULE_2__.const_data)(this.tokens.value);
+ }
+
+ /** @param {import("../writer.js").Writer} w */
+ write(w) {
+ const { parent } = this;
+ return w.ts.definition(
+ w.ts.wrap([
+ this.extAttrs.write(w),
+ w.token(this.tokens.base),
+ w.ts.type(this.idlType.write(w)),
+ w.name_token(this.tokens.name, { data: this, parent }),
+ w.token(this.tokens.assign),
+ w.token(this.tokens.value),
+ w.token(this.tokens.termination),
+ ]),
+ { data: this, parent }
+ );
+ }
+}
+
+
+/***/ }),
+/* 22 */
+/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
+
+__webpack_require__.r(__webpack_exports__);
+/* harmony export */ __webpack_require__.d(__webpack_exports__, {
+/* harmony export */ "IterableLike": () => (/* binding */ IterableLike)
+/* harmony export */ });
+/* harmony import */ var _base_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(6);
+/* harmony import */ var _helpers_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(4);
+
+
+
+class IterableLike extends _base_js__WEBPACK_IMPORTED_MODULE_0__.Base {
+ /**
+ * @param {import("../tokeniser.js").Tokeniser} tokeniser
+ */
+ static parse(tokeniser) {
+ const start_position = tokeniser.position;
+ const ret = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.autoParenter)(
+ new IterableLike({ source: tokeniser.source, tokens: {} })
+ );
+ const { tokens } = ret;
+ tokens.readonly = tokeniser.consume("readonly");
+ if (!tokens.readonly) {
+ tokens.async = tokeniser.consume("async");
+ }
+ tokens.base = tokens.readonly
+ ? tokeniser.consume("maplike", "setlike")
+ : tokens.async
+ ? tokeniser.consume("iterable")
+ : tokeniser.consume("iterable", "maplike", "setlike");
+ if (!tokens.base) {
+ tokeniser.unconsume(start_position);
+ return;
+ }
+
+ const { type } = ret;
+ const secondTypeRequired = type === "maplike";
+ const secondTypeAllowed = secondTypeRequired || type === "iterable";
+ const argumentAllowed = ret.async && type === "iterable";
+
+ tokens.open =
+ tokeniser.consume("<") ||
+ tokeniser.error(`Missing less-than sign \`<\` in ${type} declaration`);
+ const first =
+ (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.type_with_extended_attributes)(tokeniser) ||
+ tokeniser.error(`Missing a type argument in ${type} declaration`);
+ ret.idlType = [first];
+ ret.arguments = [];
+
+ if (secondTypeAllowed) {
+ first.tokens.separator = tokeniser.consume(",");
+ if (first.tokens.separator) {
+ ret.idlType.push((0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.type_with_extended_attributes)(tokeniser));
+ } else if (secondTypeRequired) {
+ tokeniser.error(`Missing second type argument in ${type} declaration`);
+ }
+ }
+
+ tokens.close =
+ tokeniser.consume(">") ||
+ tokeniser.error(`Missing greater-than sign \`>\` in ${type} declaration`);
+
+ if (tokeniser.probe("(")) {
+ if (argumentAllowed) {
+ tokens.argsOpen = tokeniser.consume("(");
+ ret.arguments.push(...(0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.argument_list)(tokeniser));
+ tokens.argsClose =
+ tokeniser.consume(")") ||
+ tokeniser.error("Unterminated async iterable argument list");
+ } else {
+ tokeniser.error(`Arguments are only allowed for \`async iterable\``);
+ }
+ }
+
+ tokens.termination =
+ tokeniser.consume(";") ||
+ tokeniser.error(`Missing semicolon after ${type} declaration`);
+
+ return ret.this;
+ }
+
+ get type() {
+ return this.tokens.base.value;
+ }
+ get readonly() {
+ return !!this.tokens.readonly;
+ }
+ get async() {
+ return !!this.tokens.async;
+ }
+
+ *validate(defs) {
+ for (const type of this.idlType) {
+ yield* type.validate(defs);
+ }
+ for (const argument of this.arguments) {
+ yield* argument.validate(defs);
+ }
+ }
+
+ /** @param {import("../writer.js").Writer} w */
+ write(w) {
+ return w.ts.definition(
+ w.ts.wrap([
+ this.extAttrs.write(w),
+ w.token(this.tokens.readonly),
+ w.token(this.tokens.async),
+ w.token(this.tokens.base, w.ts.generic),
+ w.token(this.tokens.open),
+ w.ts.wrap(this.idlType.map((t) => t.write(w))),
+ w.token(this.tokens.close),
+ w.token(this.tokens.argsOpen),
+ w.ts.wrap(this.arguments.map((arg) => arg.write(w))),
+ w.token(this.tokens.argsClose),
+ w.token(this.tokens.termination),
+ ]),
+ { data: this, parent: this.parent }
+ );
+ }
+}
+
+
+/***/ }),
+/* 23 */
+/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
+
+__webpack_require__.r(__webpack_exports__);
+/* harmony export */ __webpack_require__.d(__webpack_exports__, {
+/* harmony export */ "checkInterfaceMemberDuplication": () => (/* binding */ checkInterfaceMemberDuplication)
+/* harmony export */ });
+/* harmony import */ var _error_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(3);
+
+
+/**
+ * @param {import("../validator.js").Definitions} defs
+ * @param {import("../productions/container.js").Container} i
+ */
+function* checkInterfaceMemberDuplication(defs, i) {
+ const opNames = groupOperationNames(i);
+ const partials = defs.partials.get(i.name) || [];
+ const mixins = defs.mixinMap.get(i.name) || [];
+ for (const ext of [...partials, ...mixins]) {
+ const additions = getOperations(ext);
+ const statics = additions.filter((a) => a.special === "static");
+ const nonstatics = additions.filter((a) => a.special !== "static");
+ yield* checkAdditions(statics, opNames.statics, ext, i);
+ yield* checkAdditions(nonstatics, opNames.nonstatics, ext, i);
+ statics.forEach((op) => opNames.statics.add(op.name));
+ nonstatics.forEach((op) => opNames.nonstatics.add(op.name));
+ }
+
+ /**
+ * @param {import("../productions/operation.js").Operation[]} additions
+ * @param {Set<string>} existings
+ * @param {import("../productions/container.js").Container} ext
+ * @param {import("../productions/container.js").Container} base
+ */
+ function* checkAdditions(additions, existings, ext, base) {
+ for (const addition of additions) {
+ const { name } = addition;
+ if (name && existings.has(name)) {
+ const isStatic = addition.special === "static" ? "static " : "";
+ const message = `The ${isStatic}operation "${name}" has already been defined for the base interface "${base.name}" either in itself or in a mixin`;
+ yield (0,_error_js__WEBPACK_IMPORTED_MODULE_0__.validationError)(
+ addition.tokens.name,
+ ext,
+ "no-cross-overload",
+ message
+ );
+ }
+ }
+ }
+
+ /**
+ * @param {import("../productions/container.js").Container} i
+ * @returns {import("../productions/operation.js").Operation[]}
+ */
+ function getOperations(i) {
+ return i.members.filter(({ type }) => type === "operation");
+ }
+
+ /**
+ * @param {import("../productions/container.js").Container} i
+ */
+ function groupOperationNames(i) {
+ const ops = getOperations(i);
+ return {
+ statics: new Set(
+ ops.filter((op) => op.special === "static").map((op) => op.name)
+ ),
+ nonstatics: new Set(
+ ops.filter((op) => op.special !== "static").map((op) => op.name)
+ ),
+ };
+ }
+}
+
+
+/***/ }),
+/* 24 */
+/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
+
+__webpack_require__.r(__webpack_exports__);
+/* harmony export */ __webpack_require__.d(__webpack_exports__, {
+/* harmony export */ "Constructor": () => (/* binding */ Constructor)
+/* harmony export */ });
+/* harmony import */ var _base_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(6);
+/* harmony import */ var _helpers_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(4);
+
+
+
+class Constructor extends _base_js__WEBPACK_IMPORTED_MODULE_0__.Base {
+ /**
+ * @param {import("../tokeniser.js").Tokeniser} tokeniser
+ */
+ static parse(tokeniser) {
+ const base = tokeniser.consume("constructor");
+ if (!base) {
+ return;
+ }
+ /** @type {Base["tokens"]} */
+ const tokens = { base };
+ tokens.open =
+ tokeniser.consume("(") ||
+ tokeniser.error("No argument list in constructor");
+ const args = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.argument_list)(tokeniser);
+ tokens.close =
+ tokeniser.consume(")") || tokeniser.error("Unterminated constructor");
+ tokens.termination =
+ tokeniser.consume(";") ||
+ tokeniser.error("No semicolon after constructor");
+ const ret = new Constructor({ source: tokeniser.source, tokens });
+ (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.autoParenter)(ret).arguments = args;
+ return ret;
+ }
+
+ get type() {
+ return "constructor";
+ }
+
+ *validate(defs) {
+ for (const argument of this.arguments) {
+ yield* argument.validate(defs);
+ }
+ }
+
+ /** @param {import("../writer.js").Writer} w */
+ write(w) {
+ const { parent } = this;
+ return w.ts.definition(
+ w.ts.wrap([
+ this.extAttrs.write(w),
+ w.token(this.tokens.base, w.ts.nameless, { data: this, parent }),
+ w.token(this.tokens.open),
+ w.ts.wrap(this.arguments.map((arg) => arg.write(w))),
+ w.token(this.tokens.close),
+ w.token(this.tokens.termination),
+ ]),
+ { data: this, parent }
+ );
+ }
+}
+
+
+/***/ }),
+/* 25 */
+/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
+
+__webpack_require__.r(__webpack_exports__);
+/* harmony export */ __webpack_require__.d(__webpack_exports__, {
+/* harmony export */ "Mixin": () => (/* binding */ Mixin)
+/* harmony export */ });
+/* harmony import */ var _container_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(20);
+/* harmony import */ var _constant_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(21);
+/* harmony import */ var _attribute_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(14);
+/* harmony import */ var _operation_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(13);
+/* harmony import */ var _helpers_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(4);
+
+
+
+
+
+
+class Mixin extends _container_js__WEBPACK_IMPORTED_MODULE_0__.Container {
+ /**
+ * @typedef {import("../tokeniser.js").Token} Token
+ *
+ * @param {import("../tokeniser.js").Tokeniser} tokeniser
+ * @param {Token} base
+ * @param {object} [options]
+ * @param {Token} [options.partial]
+ */
+ static parse(tokeniser, base, { partial } = {}) {
+ const tokens = { partial, base };
+ tokens.mixin = tokeniser.consume("mixin");
+ if (!tokens.mixin) {
+ return;
+ }
+ return _container_js__WEBPACK_IMPORTED_MODULE_0__.Container.parse(
+ tokeniser,
+ new Mixin({ source: tokeniser.source, tokens }),
+ {
+ allowedMembers: [
+ [_constant_js__WEBPACK_IMPORTED_MODULE_1__.Constant.parse],
+ [_helpers_js__WEBPACK_IMPORTED_MODULE_4__.stringifier],
+ [_attribute_js__WEBPACK_IMPORTED_MODULE_2__.Attribute.parse, { noInherit: true }],
+ [_operation_js__WEBPACK_IMPORTED_MODULE_3__.Operation.parse, { regular: true }],
+ ],
+ }
+ );
+ }
+
+ get type() {
+ return "interface mixin";
+ }
+}
+
+
+/***/ }),
+/* 26 */
+/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
+
+__webpack_require__.r(__webpack_exports__);
+/* harmony export */ __webpack_require__.d(__webpack_exports__, {
+/* harmony export */ "Dictionary": () => (/* binding */ Dictionary)
+/* harmony export */ });
+/* harmony import */ var _container_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(20);
+/* harmony import */ var _field_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(27);
+
+
+
+class Dictionary extends _container_js__WEBPACK_IMPORTED_MODULE_0__.Container {
+ /**
+ * @param {import("../tokeniser.js").Tokeniser} tokeniser
+ * @param {object} [options]
+ * @param {import("../tokeniser.js").Token} [options.partial]
+ */
+ static parse(tokeniser, { partial } = {}) {
+ const tokens = { partial };
+ tokens.base = tokeniser.consume("dictionary");
+ if (!tokens.base) {
+ return;
+ }
+ return _container_js__WEBPACK_IMPORTED_MODULE_0__.Container.parse(
+ tokeniser,
+ new Dictionary({ source: tokeniser.source, tokens }),
+ {
+ inheritable: !partial,
+ allowedMembers: [[_field_js__WEBPACK_IMPORTED_MODULE_1__.Field.parse]],
+ }
+ );
+ }
+
+ get type() {
+ return "dictionary";
+ }
+}
+
+
+/***/ }),
+/* 27 */
+/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
+
+__webpack_require__.r(__webpack_exports__);
+/* harmony export */ __webpack_require__.d(__webpack_exports__, {
+/* harmony export */ "Field": () => (/* binding */ Field)
+/* harmony export */ });
+/* harmony import */ var _base_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(6);
+/* harmony import */ var _helpers_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(4);
+/* harmony import */ var _extended_attributes_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(8);
+/* harmony import */ var _default_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(12);
+
+
+
+
+
+class Field extends _base_js__WEBPACK_IMPORTED_MODULE_0__.Base {
+ /**
+ * @param {import("../tokeniser.js").Tokeniser} tokeniser
+ */
+ static parse(tokeniser) {
+ /** @type {Base["tokens"]} */
+ const tokens = {};
+ const ret = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.autoParenter)(new Field({ source: tokeniser.source, tokens }));
+ ret.extAttrs = _extended_attributes_js__WEBPACK_IMPORTED_MODULE_2__.ExtendedAttributes.parse(tokeniser);
+ tokens.required = tokeniser.consume("required");
+ ret.idlType =
+ (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.type_with_extended_attributes)(tokeniser, "dictionary-type") ||
+ tokeniser.error("Dictionary member lacks a type");
+ tokens.name =
+ tokeniser.consumeKind("identifier") ||
+ tokeniser.error("Dictionary member lacks a name");
+ ret.default = _default_js__WEBPACK_IMPORTED_MODULE_3__.Default.parse(tokeniser);
+ if (tokens.required && ret.default)
+ tokeniser.error("Required member must not have a default");
+ tokens.termination =
+ tokeniser.consume(";") ||
+ tokeniser.error("Unterminated dictionary member, expected `;`");
+ return ret.this;
+ }
+
+ get type() {
+ return "field";
+ }
+ get name() {
+ return (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.unescape)(this.tokens.name.value);
+ }
+ get required() {
+ return !!this.tokens.required;
+ }
+
+ *validate(defs) {
+ yield* this.idlType.validate(defs);
+ }
+
+ /** @param {import("../writer.js").Writer} w */
+ write(w) {
+ const { parent } = this;
+ return w.ts.definition(
+ w.ts.wrap([
+ this.extAttrs.write(w),
+ w.token(this.tokens.required),
+ w.ts.type(this.idlType.write(w)),
+ w.name_token(this.tokens.name, { data: this, parent }),
+ this.default ? this.default.write(w) : "",
+ w.token(this.tokens.termination),
+ ]),
+ { data: this, parent }
+ );
+ }
+}
+
+
+/***/ }),
+/* 28 */
+/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
+
+__webpack_require__.r(__webpack_exports__);
+/* harmony export */ __webpack_require__.d(__webpack_exports__, {
+/* harmony export */ "Namespace": () => (/* binding */ Namespace)
+/* harmony export */ });
+/* harmony import */ var _container_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(20);
+/* harmony import */ var _attribute_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(14);
+/* harmony import */ var _operation_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(13);
+/* harmony import */ var _error_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(3);
+/* harmony import */ var _helpers_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(4);
+/* harmony import */ var _constant_js__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(21);
+
+
+
+
+
+
+
+class Namespace extends _container_js__WEBPACK_IMPORTED_MODULE_0__.Container {
+ /**
+ * @param {import("../tokeniser.js").Tokeniser} tokeniser
+ * @param {object} [options]
+ * @param {import("../tokeniser.js").Token} [options.partial]
+ */
+ static parse(tokeniser, { partial } = {}) {
+ const tokens = { partial };
+ tokens.base = tokeniser.consume("namespace");
+ if (!tokens.base) {
+ return;
+ }
+ return _container_js__WEBPACK_IMPORTED_MODULE_0__.Container.parse(
+ tokeniser,
+ new Namespace({ source: tokeniser.source, tokens }),
+ {
+ allowedMembers: [
+ [_attribute_js__WEBPACK_IMPORTED_MODULE_1__.Attribute.parse, { noInherit: true, readonly: true }],
+ [_constant_js__WEBPACK_IMPORTED_MODULE_5__.Constant.parse],
+ [_operation_js__WEBPACK_IMPORTED_MODULE_2__.Operation.parse, { regular: true }],
+ ],
+ }
+ );
+ }
+
+ get type() {
+ return "namespace";
+ }
+
+ *validate(defs) {
+ if (
+ !this.partial &&
+ this.extAttrs.every((extAttr) => extAttr.name !== "Exposed")
+ ) {
+ const message = `Namespaces must have [Exposed] extended attribute. \
+To fix, add, for example, [Exposed=Window]. Please also consider carefully \
+if your namespace should also be exposed in a Worker scope. Refer to the \
+[WebIDL spec section on Exposed](https://heycam.github.io/webidl/#Exposed) \
+for more information.`;
+ yield (0,_error_js__WEBPACK_IMPORTED_MODULE_3__.validationError)(
+ this.tokens.name,
+ this,
+ "require-exposed",
+ message,
+ {
+ autofix: (0,_helpers_js__WEBPACK_IMPORTED_MODULE_4__.autofixAddExposedWindow)(this),
+ }
+ );
+ }
+ yield* super.validate(defs);
+ }
+}
+
+
+/***/ }),
+/* 29 */
+/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
+
+__webpack_require__.r(__webpack_exports__);
+/* harmony export */ __webpack_require__.d(__webpack_exports__, {
+/* harmony export */ "CallbackInterface": () => (/* binding */ CallbackInterface)
+/* harmony export */ });
+/* harmony import */ var _container_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(20);
+/* harmony import */ var _operation_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(13);
+/* harmony import */ var _constant_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(21);
+
+
+
+
+class CallbackInterface extends _container_js__WEBPACK_IMPORTED_MODULE_0__.Container {
+ /**
+ * @param {import("../tokeniser.js").Tokeniser} tokeniser
+ */
+ static parse(tokeniser, callback, { partial = null } = {}) {
+ const tokens = { callback };
+ tokens.base = tokeniser.consume("interface");
+ if (!tokens.base) {
+ return;
+ }
+ return _container_js__WEBPACK_IMPORTED_MODULE_0__.Container.parse(
+ tokeniser,
+ new CallbackInterface({ source: tokeniser.source, tokens }),
+ {
+ inheritable: !partial,
+ allowedMembers: [
+ [_constant_js__WEBPACK_IMPORTED_MODULE_2__.Constant.parse],
+ [_operation_js__WEBPACK_IMPORTED_MODULE_1__.Operation.parse, { regular: true }],
+ ],
+ }
+ );
+ }
+
+ get type() {
+ return "callback interface";
+ }
+}
+
+
+/***/ }),
+/* 30 */
+/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
+
+__webpack_require__.r(__webpack_exports__);
+/* harmony export */ __webpack_require__.d(__webpack_exports__, {
+/* harmony export */ "Writer": () => (/* binding */ Writer),
+/* harmony export */ "write": () => (/* binding */ write)
+/* harmony export */ });
+function noop(arg) {
+ return arg;
+}
+
+const templates = {
+ wrap: (items) => items.join(""),
+ trivia: noop,
+ name: noop,
+ reference: noop,
+ type: noop,
+ generic: noop,
+ nameless: noop,
+ inheritance: noop,
+ definition: noop,
+ extendedAttribute: noop,
+ extendedAttributeReference: noop,
+};
+
+class Writer {
+ constructor(ts) {
+ this.ts = Object.assign({}, templates, ts);
+ }
+
+ /**
+ * @param {string} raw
+ * @param {object} options
+ * @param {string} [options.unescaped]
+ * @param {import("./productions/base.js").Base} [options.context]
+ * @returns
+ */
+ reference(raw, { unescaped, context }) {
+ if (!unescaped) {
+ unescaped = raw.startsWith("_") ? raw.slice(1) : raw;
+ }
+ return this.ts.reference(raw, unescaped, context);
+ }
+
+ /**
+ * @param {import("./tokeniser.js").Token} t
+ * @param {Function} wrapper
+ * @param {...any} args
+ * @returns
+ */
+ token(t, wrapper = noop, ...args) {
+ if (!t) {
+ return "";
+ }
+ const value = wrapper(t.value, ...args);
+ return this.ts.wrap([this.ts.trivia(t.trivia), value]);
+ }
+
+ reference_token(t, context) {
+ return this.token(t, this.reference.bind(this), { context });
+ }
+
+ name_token(t, arg) {
+ return this.token(t, this.ts.name, arg);
+ }
+
+ identifier(id, context) {
+ return this.ts.wrap([
+ this.reference_token(id.tokens.value, context),
+ this.token(id.tokens.separator),
+ ]);
+ }
+}
+
+function write(ast, { templates: ts = templates } = {}) {
+ ts = Object.assign({}, templates, ts);
+
+ const w = new Writer(ts);
+
+ return ts.wrap(ast.map((it) => it.write(w)));
+}
+
+
+/***/ }),
+/* 31 */
+/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
+
+__webpack_require__.r(__webpack_exports__);
+/* harmony export */ __webpack_require__.d(__webpack_exports__, {
+/* harmony export */ "validate": () => (/* binding */ validate)
+/* harmony export */ });
+/* harmony import */ var _error_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(3);
+
+
+function getMixinMap(all, unique) {
+ const map = new Map();
+ const includes = all.filter((def) => def.type === "includes");
+ for (const include of includes) {
+ const mixin = unique.get(include.includes);
+ if (!mixin) {
+ continue;
+ }
+ const array = map.get(include.target);
+ if (array) {
+ array.push(mixin);
+ } else {
+ map.set(include.target, [mixin]);
+ }
+ }
+ return map;
+}
+
+/**
+ * @typedef {ReturnType<typeof groupDefinitions>} Definitions
+ */
+function groupDefinitions(all) {
+ const unique = new Map();
+ const duplicates = new Set();
+ const partials = new Map();
+ for (const def of all) {
+ if (def.partial) {
+ const array = partials.get(def.name);
+ if (array) {
+ array.push(def);
+ } else {
+ partials.set(def.name, [def]);
+ }
+ continue;
+ }
+ if (!def.name) {
+ continue;
+ }
+ if (!unique.has(def.name)) {
+ unique.set(def.name, def);
+ } else {
+ duplicates.add(def);
+ }
+ }
+ return {
+ all,
+ unique,
+ partials,
+ duplicates,
+ mixinMap: getMixinMap(all, unique),
+ cache: {
+ typedefIncludesDictionary: new WeakMap(),
+ dictionaryIncludesRequiredField: new WeakMap(),
+ },
+ };
+}
+
+function* checkDuplicatedNames({ unique, duplicates }) {
+ for (const dup of duplicates) {
+ const { name } = dup;
+ const message = `The name "${name}" of type "${
+ unique.get(name).type
+ }" was already seen`;
+ yield (0,_error_js__WEBPACK_IMPORTED_MODULE_0__.validationError)(dup.tokens.name, dup, "no-duplicate", message);
+ }
+}
+
+function* validateIterable(ast) {
+ const defs = groupDefinitions(ast);
+ for (const def of defs.all) {
+ if (def.validate) {
+ yield* def.validate(defs);
+ }
+ }
+ yield* checkDuplicatedNames(defs);
+}
+
+// Remove this once all of our support targets expose `.flat()` by default
+function flatten(array) {
+ if (array.flat) {
+ return array.flat();
+ }
+ return [].concat(...array);
+}
+
+/**
+ * @param {import("./productions/base.js").Base[]} ast
+ * @return {import("./error.js").WebIDLErrorData[]} validation errors
+ */
+function validate(ast) {
+ return [...validateIterable(flatten(ast))];
+}
+
+
+/***/ })
+/******/ ]);
+/************************************************************************/
+/******/ // The module cache
+/******/ var __webpack_module_cache__ = {};
+/******/
+/******/ // The require function
+/******/ function __webpack_require__(moduleId) {
+/******/ // Check if module is in cache
+/******/ var cachedModule = __webpack_module_cache__[moduleId];
+/******/ if (cachedModule !== undefined) {
+/******/ return cachedModule.exports;
+/******/ }
+/******/ // Create a new module (and put it into the cache)
+/******/ var module = __webpack_module_cache__[moduleId] = {
+/******/ // no module.id needed
+/******/ // no module.loaded needed
+/******/ exports: {}
+/******/ };
+/******/
+/******/ // Execute the module function
+/******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
+/******/
+/******/ // Return the exports of the module
+/******/ return module.exports;
+/******/ }
+/******/
+/************************************************************************/
+/******/ /* webpack/runtime/define property getters */
+/******/ (() => {
+/******/ // define getter functions for harmony exports
+/******/ __webpack_require__.d = (exports, definition) => {
+/******/ for(var key in definition) {
+/******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
+/******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
+/******/ }
+/******/ }
+/******/ };
+/******/ })();
+/******/
+/******/ /* webpack/runtime/hasOwnProperty shorthand */
+/******/ (() => {
+/******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
+/******/ })();
+/******/
+/******/ /* webpack/runtime/make namespace object */
+/******/ (() => {
+/******/ // define __esModule on exports
+/******/ __webpack_require__.r = (exports) => {
+/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
+/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
+/******/ }
+/******/ Object.defineProperty(exports, '__esModule', { value: true });
+/******/ };
+/******/ })();
+/******/
+/************************************************************************/
+var __webpack_exports__ = {};
+// This entry need to be wrapped in an IIFE because it need to be isolated against other modules in the chunk.
+(() => {
+__webpack_require__.r(__webpack_exports__);
+/* harmony export */ __webpack_require__.d(__webpack_exports__, {
+/* harmony export */ "WebIDLParseError": () => (/* reexport safe */ _lib_tokeniser_js__WEBPACK_IMPORTED_MODULE_3__.WebIDLParseError),
+/* harmony export */ "parse": () => (/* reexport safe */ _lib_webidl2_js__WEBPACK_IMPORTED_MODULE_0__.parse),
+/* harmony export */ "validate": () => (/* reexport safe */ _lib_validator_js__WEBPACK_IMPORTED_MODULE_2__.validate),
+/* harmony export */ "write": () => (/* reexport safe */ _lib_writer_js__WEBPACK_IMPORTED_MODULE_1__.write)
+/* harmony export */ });
+/* harmony import */ var _lib_webidl2_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1);
+/* harmony import */ var _lib_writer_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(30);
+/* harmony import */ var _lib_validator_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(31);
+/* harmony import */ var _lib_tokeniser_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(2);
+
+
+
+
+
+})();
+
+/******/ return __webpack_exports__;
+/******/ })()
+;
+});
+//# sourceMappingURL=webidl2.js.map \ No newline at end of file
diff --git a/test/wpt/tests/resources/webidl2/lib/webidl2.js.headers b/test/wpt/tests/resources/webidl2/lib/webidl2.js.headers
new file mode 100644
index 0000000..6805c32
--- /dev/null
+++ b/test/wpt/tests/resources/webidl2/lib/webidl2.js.headers
@@ -0,0 +1 @@
+Content-Type: text/javascript; charset=utf-8
diff --git a/test/wpt/tests/service-workers/META.yml b/test/wpt/tests/service-workers/META.yml
new file mode 100644
index 0000000..03a0dd0
--- /dev/null
+++ b/test/wpt/tests/service-workers/META.yml
@@ -0,0 +1,6 @@
+spec: https://w3c.github.io/ServiceWorker/
+suggested_reviewers:
+ - asutherland
+ - mkruisselbrink
+ - mattto
+ - wanderview
diff --git a/test/wpt/tests/service-workers/cache-storage/META.yml b/test/wpt/tests/service-workers/cache-storage/META.yml
new file mode 100644
index 0000000..bf34474
--- /dev/null
+++ b/test/wpt/tests/service-workers/cache-storage/META.yml
@@ -0,0 +1,3 @@
+suggested_reviewers:
+ - inexorabletash
+ - wanderview
diff --git a/test/wpt/tests/service-workers/cache-storage/cache-abort.https.any.js b/test/wpt/tests/service-workers/cache-storage/cache-abort.https.any.js
new file mode 100644
index 0000000..960d1bb
--- /dev/null
+++ b/test/wpt/tests/service-workers/cache-storage/cache-abort.https.any.js
@@ -0,0 +1,81 @@
+// META: title=Cache Storage: Abort
+// META: global=window,worker
+// META: script=./resources/test-helpers.js
+// META: script=/common/utils.js
+// META: timeout=long
+
+// We perform the same tests on put, add, addAll. Parameterise the tests to
+// reduce repetition.
+const methodsToTest = {
+ put: async (cache, request) => {
+ const response = await fetch(request);
+ return cache.put(request, response);
+ },
+ add: async (cache, request) => cache.add(request),
+ addAll: async (cache, request) => cache.addAll([request]),
+};
+
+for (const method in methodsToTest) {
+ const perform = methodsToTest[method];
+
+ cache_test(async (cache, test) => {
+ const controller = new AbortController();
+ const signal = controller.signal;
+ controller.abort();
+ const request = new Request('../resources/simple.txt', { signal });
+ return promise_rejects_dom(test, 'AbortError', perform(cache, request),
+ `${method} should reject`);
+ }, `${method}() on an already-aborted request should reject with AbortError`);
+
+ cache_test(async (cache, test) => {
+ const controller = new AbortController();
+ const signal = controller.signal;
+ const request = new Request('../resources/simple.txt', { signal });
+ const promise = perform(cache, request);
+ controller.abort();
+ return promise_rejects_dom(test, 'AbortError', promise,
+ `${method} should reject`);
+ }, `${method}() synchronously followed by abort should reject with ` +
+ `AbortError`);
+
+ cache_test(async (cache, test) => {
+ const controller = new AbortController();
+ const signal = controller.signal;
+ const stateKey = token();
+ const abortKey = token();
+ const request = new Request(
+ `../../../fetch/api/resources/infinite-slow-response.py?stateKey=${stateKey}&abortKey=${abortKey}`,
+ { signal });
+
+ const promise = perform(cache, request);
+
+ // Wait for the server to start sending the response body.
+ let opened = false;
+ do {
+ // Normally only one fetch to 'stash-take' is needed, but the fetches
+ // will be served in reverse order sometimes
+ // (i.e., 'stash-take' gets served before 'infinite-slow-response').
+
+ const response =
+ await fetch(`../../../fetch/api/resources/stash-take.py?key=${stateKey}`);
+ const body = await response.json();
+ if (body === 'open') opened = true;
+ } while (!opened);
+
+ // Sadly the above loop cannot guarantee that the browser has started
+ // processing the response body. This delay is needed to make the test
+ // failures non-flaky in Chrome version 66. My deepest apologies.
+ await new Promise(resolve => setTimeout(resolve, 250));
+
+ controller.abort();
+
+ await promise_rejects_dom(test, 'AbortError', promise,
+ `${method} should reject`);
+
+ // infinite-slow-response.py doesn't know when to stop.
+ return fetch(`../../../fetch/api/resources/stash-put.py?key=${abortKey}`);
+ }, `${method}() followed by abort after headers received should reject ` +
+ `with AbortError`);
+}
+
+done();
diff --git a/test/wpt/tests/service-workers/cache-storage/cache-add.https.any.js b/test/wpt/tests/service-workers/cache-storage/cache-add.https.any.js
new file mode 100644
index 0000000..eca516a
--- /dev/null
+++ b/test/wpt/tests/service-workers/cache-storage/cache-add.https.any.js
@@ -0,0 +1,368 @@
+// META: title=Cache.add and Cache.addAll
+// META: global=window,worker
+// META: script=/common/get-host-info.sub.js
+// META: script=./resources/test-helpers.js
+// META: timeout=long
+
+const { REMOTE_HOST } = get_host_info();
+
+cache_test(function(cache, test) {
+ return promise_rejects_js(
+ test,
+ TypeError,
+ cache.add(),
+ 'Cache.add should throw a TypeError when no arguments are given.');
+ }, 'Cache.add called with no arguments');
+
+cache_test(function(cache) {
+ return cache.add('./resources/simple.txt')
+ .then(function(result) {
+ assert_equals(result, undefined,
+ 'Cache.add should resolve with undefined on success.');
+ return cache.match('./resources/simple.txt');
+ })
+ .then(function(response) {
+ assert_class_string(response, 'Response',
+ 'Cache.add should put a resource in the cache.');
+ return response.text();
+ })
+ .then(function(body) {
+ assert_equals(body, 'a simple text file\n',
+ 'Cache.add should retrieve the correct body.');
+ });
+ }, 'Cache.add called with relative URL specified as a string');
+
+cache_test(function(cache, test) {
+ return promise_rejects_js(
+ test,
+ TypeError,
+ cache.add('javascript://this-is-not-http-mmkay'),
+ 'Cache.add should throw a TypeError for non-HTTP/HTTPS URLs.');
+ }, 'Cache.add called with non-HTTP/HTTPS URL');
+
+cache_test(function(cache) {
+ var request = new Request('./resources/simple.txt');
+ return cache.add(request)
+ .then(function(result) {
+ assert_equals(result, undefined,
+ 'Cache.add should resolve with undefined on success.');
+ });
+ }, 'Cache.add called with Request object');
+
+cache_test(function(cache, test) {
+ var request = new Request('./resources/simple.txt',
+ {method: 'POST', body: 'This is a body.'});
+ return promise_rejects_js(
+ test,
+ TypeError,
+ cache.add(request),
+ 'Cache.add should throw a TypeError for non-GET requests.');
+ }, 'Cache.add called with POST request');
+
+cache_test(function(cache) {
+ var request = new Request('./resources/simple.txt');
+ return cache.add(request)
+ .then(function(result) {
+ assert_equals(result, undefined,
+ 'Cache.add should resolve with undefined on success.');
+ })
+ .then(function() {
+ return cache.add(request);
+ })
+ .then(function(result) {
+ assert_equals(result, undefined,
+ 'Cache.add should resolve with undefined on success.');
+ });
+ }, 'Cache.add called twice with the same Request object');
+
+cache_test(function(cache) {
+ var request = new Request('./resources/simple.txt');
+ return request.text()
+ .then(function() {
+ assert_false(request.bodyUsed);
+ })
+ .then(function() {
+ return cache.add(request);
+ });
+ }, 'Cache.add with request with null body (not consumed)');
+
+cache_test(function(cache, test) {
+ return promise_rejects_js(
+ test,
+ TypeError,
+ cache.add('./resources/fetch-status.py?status=206'),
+ 'Cache.add should reject on partial response');
+ }, 'Cache.add with 206 response');
+
+cache_test(function(cache, test) {
+ var urls = ['./resources/fetch-status.py?status=206',
+ './resources/fetch-status.py?status=200'];
+ var requests = urls.map(function(url) {
+ return new Request(url);
+ });
+ return promise_rejects_js(
+ test,
+ TypeError,
+ cache.addAll(requests),
+ 'Cache.addAll should reject with TypeError if any request fails');
+ }, 'Cache.addAll with 206 response');
+
+cache_test(function(cache, test) {
+ var urls = ['./resources/fetch-status.py?status=206',
+ './resources/fetch-status.py?status=200'];
+ var requests = urls.map(function(url) {
+ var cross_origin_url = new URL(url, location.href);
+ cross_origin_url.hostname = REMOTE_HOST;
+ return new Request(cross_origin_url.href, { mode: 'no-cors' });
+ });
+ return promise_rejects_js(
+ test,
+ TypeError,
+ cache.addAll(requests),
+ 'Cache.addAll should reject with TypeError if any request fails');
+ }, 'Cache.addAll with opaque-filtered 206 response');
+
+cache_test(function(cache, test) {
+ return promise_rejects_js(
+ test,
+ TypeError,
+ cache.add('this-does-not-exist-please-dont-create-it'),
+ 'Cache.add should reject if response is !ok');
+ }, 'Cache.add with request that results in a status of 404');
+
+
+cache_test(function(cache, test) {
+ return promise_rejects_js(
+ test,
+ TypeError,
+ cache.add('./resources/fetch-status.py?status=500'),
+ 'Cache.add should reject if response is !ok');
+ }, 'Cache.add with request that results in a status of 500');
+
+cache_test(function(cache, test) {
+ return promise_rejects_js(
+ test,
+ TypeError,
+ cache.addAll(),
+ 'Cache.addAll with no arguments should throw TypeError.');
+ }, 'Cache.addAll with no arguments');
+
+cache_test(function(cache, test) {
+ // Assumes the existence of ../resources/simple.txt and ../resources/blank.html
+ var urls = ['./resources/simple.txt', undefined, './resources/blank.html'];
+ return promise_rejects_js(
+ test,
+ TypeError,
+ cache.addAll(urls),
+ 'Cache.addAll should throw TypeError for an undefined argument.');
+ }, 'Cache.addAll with a mix of valid and undefined arguments');
+
+cache_test(function(cache) {
+ return cache.addAll([])
+ .then(function(result) {
+ assert_equals(result, undefined,
+ 'Cache.addAll should resolve with undefined on ' +
+ 'success.');
+ return cache.keys();
+ })
+ .then(function(result) {
+ assert_equals(result.length, 0,
+ 'There should be no entry in the cache.');
+ });
+ }, 'Cache.addAll with an empty array');
+
+cache_test(function(cache) {
+ // Assumes the existence of ../resources/simple.txt and
+ // ../resources/blank.html
+ var urls = ['./resources/simple.txt',
+ self.location.href,
+ './resources/blank.html'];
+ return cache.addAll(urls)
+ .then(function(result) {
+ assert_equals(result, undefined,
+ 'Cache.addAll should resolve with undefined on ' +
+ 'success.');
+ return Promise.all(
+ urls.map(function(url) { return cache.match(url); }));
+ })
+ .then(function(responses) {
+ assert_class_string(
+ responses[0], 'Response',
+ 'Cache.addAll should put a resource in the cache.');
+ assert_class_string(
+ responses[1], 'Response',
+ 'Cache.addAll should put a resource in the cache.');
+ assert_class_string(
+ responses[2], 'Response',
+ 'Cache.addAll should put a resource in the cache.');
+ return Promise.all(
+ responses.map(function(response) { return response.text(); }));
+ })
+ .then(function(bodies) {
+ assert_equals(
+ bodies[0], 'a simple text file\n',
+ 'Cache.add should retrieve the correct body.');
+ assert_equals(
+ bodies[2], '<!DOCTYPE html>\n<title>Empty doc</title>\n',
+ 'Cache.add should retrieve the correct body.');
+ });
+ }, 'Cache.addAll with string URL arguments');
+
+cache_test(function(cache) {
+ // Assumes the existence of ../resources/simple.txt and
+ // ../resources/blank.html
+ var urls = ['./resources/simple.txt',
+ self.location.href,
+ './resources/blank.html'];
+ var requests = urls.map(function(url) {
+ return new Request(url);
+ });
+ return cache.addAll(requests)
+ .then(function(result) {
+ assert_equals(result, undefined,
+ 'Cache.addAll should resolve with undefined on ' +
+ 'success.');
+ return Promise.all(
+ urls.map(function(url) { return cache.match(url); }));
+ })
+ .then(function(responses) {
+ assert_class_string(
+ responses[0], 'Response',
+ 'Cache.addAll should put a resource in the cache.');
+ assert_class_string(
+ responses[1], 'Response',
+ 'Cache.addAll should put a resource in the cache.');
+ assert_class_string(
+ responses[2], 'Response',
+ 'Cache.addAll should put a resource in the cache.');
+ return Promise.all(
+ responses.map(function(response) { return response.text(); }));
+ })
+ .then(function(bodies) {
+ assert_equals(
+ bodies[0], 'a simple text file\n',
+ 'Cache.add should retrieve the correct body.');
+ assert_equals(
+ bodies[2], '<!DOCTYPE html>\n<title>Empty doc</title>\n',
+ 'Cache.add should retrieve the correct body.');
+ });
+ }, 'Cache.addAll with Request arguments');
+
+cache_test(function(cache, test) {
+ // Assumes that ../resources/simple.txt and ../resources/blank.html exist.
+ // The second resource does not.
+ var urls = ['./resources/simple.txt',
+ 'this-resource-should-not-exist',
+ './resources/blank.html'];
+ var requests = urls.map(function(url) {
+ return new Request(url);
+ });
+ return promise_rejects_js(
+ test,
+ TypeError,
+ cache.addAll(requests),
+ 'Cache.addAll should reject with TypeError if any request fails')
+ .then(function() {
+ return Promise.all(urls.map(function(url) {
+ return cache.match(url);
+ }));
+ })
+ .then(function(matches) {
+ assert_array_equals(
+ matches,
+ [undefined, undefined, undefined],
+ 'If any response fails, no response should be added to cache');
+ });
+ }, 'Cache.addAll with a mix of succeeding and failing requests');
+
+cache_test(function(cache, test) {
+ var request = new Request('../resources/simple.txt');
+ return promise_rejects_dom(
+ test,
+ 'InvalidStateError',
+ cache.addAll([request, request]),
+ 'Cache.addAll should throw InvalidStateError if the same request is added ' +
+ 'twice.');
+ }, 'Cache.addAll called with the same Request object specified twice');
+
+cache_test(async function(cache, test) {
+ const url = './resources/vary.py?vary=x-shape';
+ let requests = [
+ new Request(url, { headers: { 'x-shape': 'circle' }}),
+ new Request(url, { headers: { 'x-shape': 'square' }}),
+ ];
+ let result = await cache.addAll(requests);
+ assert_equals(result, undefined, 'Cache.addAll() should succeed');
+ }, 'Cache.addAll should succeed when entries differ by vary header');
+
+cache_test(async function(cache, test) {
+ const url = './resources/vary.py?vary=x-shape';
+ let requests = [
+ new Request(url, { headers: { 'x-shape': 'circle' }}),
+ new Request(url, { headers: { 'x-shape': 'circle' }}),
+ ];
+ await promise_rejects_dom(
+ test,
+ 'InvalidStateError',
+ cache.addAll(requests),
+ 'Cache.addAll() should reject when entries are duplicate by vary header');
+ }, 'Cache.addAll should reject when entries are duplicate by vary header');
+
+// VARY header matching is asymmetric. Determining if two entries are duplicate
+// depends on which entry's response is used in the comparison. The target
+// response's VARY header determines what request headers are examined. This
+// test verifies that Cache.addAll() duplicate checking handles this asymmetric
+// behavior correctly.
+cache_test(async function(cache, test) {
+ const base_url = './resources/vary.py';
+
+ // Define a request URL that sets a VARY header in the
+ // query string to be echoed back by the server.
+ const url = base_url + '?vary=x-size';
+
+ // Set a cookie to override the VARY header of the response
+ // when the request is made with credentials. This will
+ // take precedence over the query string vary param. This
+ // is a bit confusing, but it's necessary to construct a test
+ // where the URL is the same, but the VARY headers differ.
+ //
+ // Note, the test could also pass this information in additional
+ // request headers. If the cookie approach becomes too unwieldy
+ // this test could be rewritten to use that technique.
+ await fetch(base_url + '?set-vary-value-override-cookie=x-shape');
+ test.add_cleanup(_ => fetch(base_url + '?clear-vary-value-override-cookie'));
+
+ let requests = [
+ // This request will result in a Response with a "Vary: x-shape"
+ // header. This *will not* result in a duplicate match with the
+ // other entry.
+ new Request(url, { headers: { 'x-shape': 'circle',
+ 'x-size': 'big' },
+ credentials: 'same-origin' }),
+
+ // This request will result in a Response with a "Vary: x-size"
+ // header. This *will* result in a duplicate match with the other
+ // entry.
+ new Request(url, { headers: { 'x-shape': 'square',
+ 'x-size': 'big' },
+ credentials: 'omit' }),
+ ];
+ await promise_rejects_dom(
+ test,
+ 'InvalidStateError',
+ cache.addAll(requests),
+ 'Cache.addAll() should reject when one entry has a vary header ' +
+ 'matching an earlier entry.');
+
+ // Test the reverse order now.
+ await promise_rejects_dom(
+ test,
+ 'InvalidStateError',
+ cache.addAll(requests.reverse()),
+ 'Cache.addAll() should reject when one entry has a vary header ' +
+ 'matching a later entry.');
+
+ }, 'Cache.addAll should reject when one entry has a vary header ' +
+ 'matching another entry');
+
+done();
diff --git a/test/wpt/tests/service-workers/cache-storage/cache-delete.https.any.js b/test/wpt/tests/service-workers/cache-storage/cache-delete.https.any.js
new file mode 100644
index 0000000..3eae2b6
--- /dev/null
+++ b/test/wpt/tests/service-workers/cache-storage/cache-delete.https.any.js
@@ -0,0 +1,164 @@
+// META: title=Cache.delete
+// META: global=window,worker
+// META: script=./resources/test-helpers.js
+// META: timeout=long
+
+var test_url = 'https://example.com/foo';
+
+// Construct a generic Request object. The URL is |test_url|. All other fields
+// are defaults.
+function new_test_request() {
+ return new Request(test_url);
+}
+
+// Construct a generic Response object.
+function new_test_response() {
+ return new Response('Hello world!', { status: 200 });
+}
+
+cache_test(function(cache, test) {
+ return promise_rejects_js(
+ test,
+ TypeError,
+ cache.delete(),
+ 'Cache.delete should reject with a TypeError when called with no ' +
+ 'arguments.');
+ }, 'Cache.delete with no arguments');
+
+cache_test(function(cache) {
+ return cache.put(new_test_request(), new_test_response())
+ .then(function() {
+ return cache.delete(test_url);
+ })
+ .then(function(result) {
+ assert_true(result,
+ 'Cache.delete should resolve with "true" if an entry ' +
+ 'was successfully deleted.');
+ return cache.match(test_url);
+ })
+ .then(function(result) {
+ assert_equals(result, undefined,
+ 'Cache.delete should remove matching entries from cache.');
+ });
+ }, 'Cache.delete called with a string URL');
+
+cache_test(function(cache) {
+ var request = new Request(test_url);
+ return cache.put(request, new_test_response())
+ .then(function() {
+ return cache.delete(request);
+ })
+ .then(function(result) {
+ assert_true(result,
+ 'Cache.delete should resolve with "true" if an entry ' +
+ 'was successfully deleted.');
+ });
+ }, 'Cache.delete called with a Request object');
+
+cache_test(function(cache) {
+ var request = new Request(test_url);
+ var response = new_test_response();
+ return cache.put(request, response)
+ .then(function() {
+ return cache.delete(new Request(test_url, {method: 'HEAD'}));
+ })
+ .then(function(result) {
+ assert_false(result,
+ 'Cache.delete should not match a non-GET request ' +
+ 'unless ignoreMethod option is set.');
+ return cache.match(test_url);
+ })
+ .then(function(result) {
+ assert_response_equals(result, response,
+ 'Cache.delete should leave non-matching response in the cache.');
+ return cache.delete(new Request(test_url, {method: 'HEAD'}),
+ {ignoreMethod: true});
+ })
+ .then(function(result) {
+ assert_true(result,
+ 'Cache.delete should match a non-GET request ' +
+ ' if ignoreMethod is true.');
+ });
+ }, 'Cache.delete called with a HEAD request');
+
+cache_test(function(cache) {
+ var vary_request = new Request('http://example.com/c',
+ {headers: {'Cookies': 'is-for-cookie'}});
+ var vary_response = new Response('', {headers: {'Vary': 'Cookies'}});
+ var mismatched_vary_request = new Request('http://example.com/c');
+
+ return cache.put(vary_request.clone(), vary_response.clone())
+ .then(function() {
+ return cache.delete(mismatched_vary_request.clone());
+ })
+ .then(function(result) {
+ assert_false(result,
+ 'Cache.delete should not delete if vary does not ' +
+ 'match unless ignoreVary is true');
+ return cache.delete(mismatched_vary_request.clone(),
+ {ignoreVary: true});
+ })
+ .then(function(result) {
+ assert_true(result,
+ 'Cache.delete should ignore vary if ignoreVary is true');
+ });
+ }, 'Cache.delete supports ignoreVary');
+
+cache_test(function(cache) {
+ return cache.delete(test_url)
+ .then(function(result) {
+ assert_false(result,
+ 'Cache.delete should resolve with "false" if there ' +
+ 'are no matching entries.');
+ });
+ }, 'Cache.delete with a non-existent entry');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.matchAll(entries.a_with_query.request,
+ { ignoreSearch: true })
+ .then(function(result) {
+ assert_response_array_equals(
+ result,
+ [
+ entries.a.response,
+ entries.a_with_query.response
+ ]);
+ return cache.delete(entries.a_with_query.request,
+ { ignoreSearch: true });
+ })
+ .then(function(result) {
+ return cache.matchAll(entries.a_with_query.request,
+ { ignoreSearch: true });
+ })
+ .then(function(result) {
+ assert_response_array_equals(result, []);
+ });
+ },
+ 'Cache.delete with ignoreSearch option (request with search parameters)');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.matchAll(entries.a_with_query.request,
+ { ignoreSearch: true })
+ .then(function(result) {
+ assert_response_array_equals(
+ result,
+ [
+ entries.a.response,
+ entries.a_with_query.response
+ ]);
+ // cache.delete()'s behavior should be the same if ignoreSearch is
+ // not provided or if ignoreSearch is false.
+ return cache.delete(entries.a_with_query.request,
+ { ignoreSearch: false });
+ })
+ .then(function(result) {
+ return cache.matchAll(entries.a_with_query.request,
+ { ignoreSearch: true });
+ })
+ .then(function(result) {
+ assert_response_array_equals(result, [ entries.a.response ]);
+ });
+ },
+ 'Cache.delete with ignoreSearch option (when it is specified as false)');
+
+done();
diff --git a/test/wpt/tests/service-workers/cache-storage/cache-keys-attributes-for-service-worker.https.html b/test/wpt/tests/service-workers/cache-storage/cache-keys-attributes-for-service-worker.https.html
new file mode 100644
index 0000000..3c96348
--- /dev/null
+++ b/test/wpt/tests/service-workers/cache-storage/cache-keys-attributes-for-service-worker.https.html
@@ -0,0 +1,75 @@
+<!DOCTYPE html>
+<title>Cache.keys (checking request attributes that can be set only on service workers)</title>
+<link rel="help" href="https://w3c.github.io/ServiceWorker/#cache-keys">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="./../service-worker/resources/test-helpers.sub.js"></script>
+<script>
+const worker = './resources/cache-keys-attributes-for-service-worker.js';
+
+function wait(ms) {
+ return new Promise(resolve => step_timeout(resolve, ms));
+}
+
+promise_test(async (t) => {
+ const scope = './resources/blank.html?name=isReloadNavigation';
+ let frame;
+ let reg;
+
+ try {
+ reg = await service_worker_unregister_and_register(t, worker, scope);
+ await wait_for_state(t, reg.installing, 'activated');
+ frame = await with_iframe(scope);
+ assert_equals(frame.contentDocument.body.textContent,
+ 'original: false, stored: false');
+ await new Promise((resolve) => {
+ frame.onload = resolve;
+ frame.contentWindow.location.reload();
+ });
+ assert_equals(frame.contentDocument.body.textContent,
+ 'original: true, stored: true');
+ } finally {
+ if (frame) {
+ frame.remove();
+ }
+ if (reg) {
+ await reg.unregister();
+ }
+ }
+}, 'Request.IsReloadNavigation should persist.');
+
+promise_test(async (t) => {
+ const scope = './resources/blank.html?name=isHistoryNavigation';
+ let frame;
+ let reg;
+
+ try {
+ reg = await service_worker_unregister_and_register(t, worker, scope);
+ await wait_for_state(t, reg.installing, 'activated');
+ frame = await with_iframe(scope);
+ assert_equals(frame.contentDocument.body.textContent,
+ 'original: false, stored: false');
+ // Use step_timeout(0) to ensure the history entry is created for Blink
+ // and WebKit. See https://bugs.webkit.org/show_bug.cgi?id=42861.
+ await wait(0);
+ await new Promise((resolve) => {
+ frame.onload = resolve;
+ frame.src = '../resources/blank.html?ignore';
+ });
+ await wait(0);
+ await new Promise((resolve) => {
+ frame.onload = resolve;
+ frame.contentWindow.history.go(-1);
+ });
+ assert_equals(frame.contentDocument.body.textContent,
+ 'original: true, stored: true');
+ } finally {
+ if (frame) {
+ frame.remove();
+ }
+ if (reg) {
+ await reg.unregister();
+ }
+ }
+}, 'Request.IsHistoryNavigation should persist.');
+</script>
diff --git a/test/wpt/tests/service-workers/cache-storage/cache-keys.https.any.js b/test/wpt/tests/service-workers/cache-storage/cache-keys.https.any.js
new file mode 100644
index 0000000..232fb76
--- /dev/null
+++ b/test/wpt/tests/service-workers/cache-storage/cache-keys.https.any.js
@@ -0,0 +1,212 @@
+// META: title=Cache.keys
+// META: global=window,worker
+// META: script=./resources/test-helpers.js
+// META: timeout=long
+
+cache_test(cache => {
+ return cache.keys()
+ .then(requests => {
+ assert_equals(
+ requests.length, 0,
+ 'Cache.keys should resolve to an empty array for an empty cache');
+ });
+ }, 'Cache.keys() called on an empty cache');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.keys('not-present-in-the-cache')
+ .then(function(result) {
+ assert_request_array_equals(
+ result, [],
+ 'Cache.keys should resolve with an empty array on failure.');
+ });
+ }, 'Cache.keys with no matching entries');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.keys(entries.a.request.url)
+ .then(function(result) {
+ assert_request_array_equals(result, [entries.a.request],
+ 'Cache.keys should match by URL.');
+ });
+ }, 'Cache.keys with URL');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.keys(entries.a.request)
+ .then(function(result) {
+ assert_request_array_equals(
+ result, [entries.a.request],
+ 'Cache.keys should match by Request.');
+ });
+ }, 'Cache.keys with Request');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.keys(new Request(entries.a.request.url))
+ .then(function(result) {
+ assert_request_array_equals(
+ result, [entries.a.request],
+ 'Cache.keys should match by Request.');
+ });
+ }, 'Cache.keys with new Request');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.keys(entries.a.request, {ignoreSearch: true})
+ .then(function(result) {
+ assert_request_array_equals(
+ result,
+ [
+ entries.a.request,
+ entries.a_with_query.request
+ ],
+ 'Cache.keys with ignoreSearch should ignore the ' +
+ 'search parameters of cached request.');
+ });
+ },
+ 'Cache.keys with ignoreSearch option (request with no search ' +
+ 'parameters)');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.keys(entries.a_with_query.request, {ignoreSearch: true})
+ .then(function(result) {
+ assert_request_array_equals(
+ result,
+ [
+ entries.a.request,
+ entries.a_with_query.request
+ ],
+ 'Cache.keys with ignoreSearch should ignore the ' +
+ 'search parameters of request.');
+ });
+ },
+ 'Cache.keys with ignoreSearch option (request with search parameters)');
+
+cache_test(function(cache) {
+ var request = new Request('http://example.com/');
+ var head_request = new Request('http://example.com/', {method: 'HEAD'});
+ var response = new Response('foo');
+ return cache.put(request.clone(), response.clone())
+ .then(function() {
+ return cache.keys(head_request.clone());
+ })
+ .then(function(result) {
+ assert_request_array_equals(
+ result, [],
+ 'Cache.keys should resolve with an empty array with a ' +
+ 'mismatched method.');
+ return cache.keys(head_request.clone(),
+ {ignoreMethod: true});
+ })
+ .then(function(result) {
+ assert_request_array_equals(
+ result,
+ [
+ request,
+ ],
+ 'Cache.keys with ignoreMethod should ignore the ' +
+ 'method of request.');
+ });
+ }, 'Cache.keys supports ignoreMethod');
+
+cache_test(function(cache) {
+ var vary_request = new Request('http://example.com/c',
+ {headers: {'Cookies': 'is-for-cookie'}});
+ var vary_response = new Response('', {headers: {'Vary': 'Cookies'}});
+ var mismatched_vary_request = new Request('http://example.com/c');
+
+ return cache.put(vary_request.clone(), vary_response.clone())
+ .then(function() {
+ return cache.keys(mismatched_vary_request.clone());
+ })
+ .then(function(result) {
+ assert_request_array_equals(
+ result, [],
+ 'Cache.keys should resolve with an empty array with a ' +
+ 'mismatched vary.');
+ return cache.keys(mismatched_vary_request.clone(),
+ {ignoreVary: true});
+ })
+ .then(function(result) {
+ assert_request_array_equals(
+ result,
+ [
+ vary_request,
+ ],
+ 'Cache.keys with ignoreVary should ignore the ' +
+ 'vary of request.');
+ });
+ }, 'Cache.keys supports ignoreVary');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.keys(entries.cat.request.url + '#mouse')
+ .then(function(result) {
+ assert_request_array_equals(
+ result,
+ [
+ entries.cat.request,
+ ],
+ 'Cache.keys should ignore URL fragment.');
+ });
+ }, 'Cache.keys with URL containing fragment');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.keys('http')
+ .then(function(result) {
+ assert_request_array_equals(
+ result, [],
+ 'Cache.keys should treat query as a URL and not ' +
+ 'just a string fragment.');
+ });
+ }, 'Cache.keys with string fragment "http" as query');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.keys()
+ .then(function(result) {
+ assert_request_array_equals(
+ result,
+ simple_entries.map(entry => entry.request),
+ 'Cache.keys without parameters should match all entries.');
+ });
+ }, 'Cache.keys without parameters');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.keys(undefined)
+ .then(function(result) {
+ assert_request_array_equals(
+ result,
+ simple_entries.map(entry => entry.request),
+ 'Cache.keys with undefined request should match all entries.');
+ });
+ }, 'Cache.keys with explicitly undefined request');
+
+cache_test(cache => {
+ return cache.keys(undefined, {})
+ .then(requests => {
+ assert_equals(
+ requests.length, 0,
+ 'Cache.keys should resolve to an empty array for an empty cache');
+ });
+ }, 'Cache.keys with explicitly undefined request and empty options');
+
+prepopulated_cache_test(vary_entries, function(cache, entries) {
+ return cache.keys()
+ .then(function(result) {
+ assert_request_array_equals(
+ result,
+ [
+ entries.vary_cookie_is_cookie.request,
+ entries.vary_cookie_is_good.request,
+ entries.vary_cookie_absent.request,
+ ],
+ 'Cache.keys without parameters should match all entries.');
+ });
+ }, 'Cache.keys without parameters and VARY entries');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.keys(new Request(entries.cat.request.url, {method: 'HEAD'}))
+ .then(function(result) {
+ assert_request_array_equals(
+ result, [],
+ 'Cache.keys should not match HEAD request unless ignoreMethod ' +
+ 'option is set.');
+ });
+ }, 'Cache.keys with a HEAD Request');
+
+done();
diff --git a/test/wpt/tests/service-workers/cache-storage/cache-match.https.any.js b/test/wpt/tests/service-workers/cache-storage/cache-match.https.any.js
new file mode 100644
index 0000000..9ca4590
--- /dev/null
+++ b/test/wpt/tests/service-workers/cache-storage/cache-match.https.any.js
@@ -0,0 +1,437 @@
+// META: title=Cache.match
+// META: global=window,worker
+// META: script=./resources/test-helpers.js
+// META: script=/common/get-host-info.sub.js
+// META: timeout=long
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.match('not-present-in-the-cache')
+ .then(function(result) {
+ assert_equals(result, undefined,
+ 'Cache.match failures should resolve with undefined.');
+ });
+ }, 'Cache.match with no matching entries');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.match(entries.a.request.url)
+ .then(function(result) {
+ assert_response_equals(result, entries.a.response,
+ 'Cache.match should match by URL.');
+ });
+ }, 'Cache.match with URL');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.match(entries.a.request)
+ .then(function(result) {
+ assert_response_equals(result, entries.a.response,
+ 'Cache.match should match by Request.');
+ });
+ }, 'Cache.match with Request');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ var alt_response = new Response('', {status: 201});
+
+ return self.caches.open('second_matching_cache')
+ .then(function(cache) {
+ return cache.put(entries.a.request, alt_response.clone());
+ })
+ .then(function() {
+ return cache.match(entries.a.request);
+ })
+ .then(function(result) {
+ assert_response_equals(
+ result, entries.a.response,
+ 'Cache.match should match the first cache.');
+ });
+ }, 'Cache.match with multiple cache hits');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.match(new Request(entries.a.request.url))
+ .then(function(result) {
+ assert_response_equals(result, entries.a.response,
+ 'Cache.match should match by Request.');
+ });
+ }, 'Cache.match with new Request');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.match(new Request(entries.a.request.url, {method: 'HEAD'}))
+ .then(function(result) {
+ assert_equals(result, undefined,
+ 'Cache.match should not match HEAD Request.');
+ });
+ }, 'Cache.match with HEAD');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.match(entries.a.request,
+ {ignoreSearch: true})
+ .then(function(result) {
+ assert_response_in_array(
+ result,
+ [
+ entries.a.response,
+ entries.a_with_query.response
+ ],
+ 'Cache.match with ignoreSearch should ignore the ' +
+ 'search parameters of cached request.');
+ });
+ },
+ 'Cache.match with ignoreSearch option (request with no search ' +
+ 'parameters)');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.match(entries.a_with_query.request,
+ {ignoreSearch: true})
+ .then(function(result) {
+ assert_response_in_array(
+ result,
+ [
+ entries.a.response,
+ entries.a_with_query.response
+ ],
+ 'Cache.match with ignoreSearch should ignore the ' +
+ 'search parameters of request.');
+ });
+ },
+ 'Cache.match with ignoreSearch option (request with search parameter)');
+
+cache_test(function(cache) {
+ var request = new Request('http://example.com/');
+ var head_request = new Request('http://example.com/', {method: 'HEAD'});
+ var response = new Response('foo');
+ return cache.put(request.clone(), response.clone())
+ .then(function() {
+ return cache.match(head_request.clone());
+ })
+ .then(function(result) {
+ assert_equals(
+ result, undefined,
+ 'Cache.match should resolve as undefined with a ' +
+ 'mismatched method.');
+ return cache.match(head_request.clone(),
+ {ignoreMethod: true});
+ })
+ .then(function(result) {
+ assert_response_equals(
+ result, response,
+ 'Cache.match with ignoreMethod should ignore the ' +
+ 'method of request.');
+ });
+ }, 'Cache.match supports ignoreMethod');
+
+cache_test(function(cache) {
+ var vary_request = new Request('http://example.com/c',
+ {headers: {'Cookies': 'is-for-cookie'}});
+ var vary_response = new Response('', {headers: {'Vary': 'Cookies'}});
+ var mismatched_vary_request = new Request('http://example.com/c');
+
+ return cache.put(vary_request.clone(), vary_response.clone())
+ .then(function() {
+ return cache.match(mismatched_vary_request.clone());
+ })
+ .then(function(result) {
+ assert_equals(
+ result, undefined,
+ 'Cache.match should resolve as undefined with a ' +
+ 'mismatched vary.');
+ return cache.match(mismatched_vary_request.clone(),
+ {ignoreVary: true});
+ })
+ .then(function(result) {
+ assert_response_equals(
+ result, vary_response,
+ 'Cache.match with ignoreVary should ignore the ' +
+ 'vary of request.');
+ });
+ }, 'Cache.match supports ignoreVary');
+
+cache_test(function(cache) {
+ let has_cache_name = false;
+ const opts = {
+ get cacheName() {
+ has_cache_name = true;
+ return undefined;
+ }
+ };
+ return self.caches.open('foo')
+ .then(function() {
+ return cache.match('bar', opts);
+ })
+ .then(function() {
+ assert_false(has_cache_name,
+ 'Cache.match does not support cacheName option ' +
+ 'which was removed in CacheQueryOptions.');
+ });
+ }, 'Cache.match does not support cacheName option');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.match(entries.cat.request.url + '#mouse')
+ .then(function(result) {
+ assert_response_equals(result, entries.cat.response,
+ 'Cache.match should ignore URL fragment.');
+ });
+ }, 'Cache.match with URL containing fragment');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.match('http')
+ .then(function(result) {
+ assert_equals(
+ result, undefined,
+ 'Cache.match should treat query as a URL and not ' +
+ 'just a string fragment.');
+ });
+ }, 'Cache.match with string fragment "http" as query');
+
+prepopulated_cache_test(vary_entries, function(cache, entries) {
+ return cache.match('http://example.com/c')
+ .then(function(result) {
+ assert_response_in_array(
+ result,
+ [
+ entries.vary_cookie_absent.response
+ ],
+ 'Cache.match should honor "Vary" header.');
+ });
+ }, 'Cache.match with responses containing "Vary" header');
+
+cache_test(function(cache) {
+ var request = new Request('http://example.com');
+ var response;
+ var request_url = new URL('./resources/simple.txt', location.href).href;
+ return fetch(request_url)
+ .then(function(fetch_result) {
+ response = fetch_result;
+ assert_equals(
+ response.url, request_url,
+ '[https://fetch.spec.whatwg.org/#dom-response-url] ' +
+ 'Reponse.url should return the URL of the response.');
+ return cache.put(request, response.clone());
+ })
+ .then(function() {
+ return cache.match(request.url);
+ })
+ .then(function(result) {
+ assert_response_equals(
+ result, response,
+ 'Cache.match should return a Response object that has the same ' +
+ 'properties as the stored response.');
+ return cache.match(response.url);
+ })
+ .then(function(result) {
+ assert_equals(
+ result, undefined,
+ 'Cache.match should not match cache entry based on response URL.');
+ });
+ }, 'Cache.match with Request and Response objects with different URLs');
+
+cache_test(function(cache) {
+ var request_url = new URL('./resources/simple.txt', location.href).href;
+ return fetch(request_url)
+ .then(function(fetch_result) {
+ return cache.put(new Request(request_url), fetch_result);
+ })
+ .then(function() {
+ return cache.match(request_url);
+ })
+ .then(function(result) {
+ return result.text();
+ })
+ .then(function(body_text) {
+ assert_equals(body_text, 'a simple text file\n',
+ 'Cache.match should return a Response object with a ' +
+ 'valid body.');
+ })
+ .then(function() {
+ return cache.match(request_url);
+ })
+ .then(function(result) {
+ return result.text();
+ })
+ .then(function(body_text) {
+ assert_equals(body_text, 'a simple text file\n',
+ 'Cache.match should return a Response object with a ' +
+ 'valid body each time it is called.');
+ });
+ }, 'Cache.match invoked multiple times for the same Request/Response');
+
+cache_test(function(cache) {
+ var request_url = new URL('./resources/simple.txt', location.href).href;
+ return fetch(request_url)
+ .then(function(fetch_result) {
+ return cache.put(new Request(request_url), fetch_result);
+ })
+ .then(function() {
+ return cache.match(request_url);
+ })
+ .then(function(result) {
+ return result.blob();
+ })
+ .then(function(blob) {
+ var sliced = blob.slice(2,8);
+
+ return new Promise(function (resolve, reject) {
+ var reader = new FileReader();
+ reader.onloadend = function(event) {
+ resolve(event.target.result);
+ };
+ reader.readAsText(sliced);
+ });
+ })
+ .then(function(text) {
+ assert_equals(text, 'simple',
+ 'A Response blob returned by Cache.match should be ' +
+ 'sliceable.' );
+ });
+ }, 'Cache.match blob should be sliceable');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ var request = new Request(entries.a.request.clone(), {method: 'POST'});
+ return cache.match(request)
+ .then(function(result) {
+ assert_equals(result, undefined,
+ 'Cache.match should not find a match');
+ });
+ }, 'Cache.match with POST Request');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ var response = entries.non_2xx_response.response;
+ return cache.match(entries.non_2xx_response.request.url)
+ .then(function(result) {
+ assert_response_equals(
+ result, entries.non_2xx_response.response,
+ 'Cache.match should return a Response object that has the ' +
+ 'same properties as a stored non-2xx response.');
+ });
+ }, 'Cache.match with a non-2xx Response');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ var response = entries.error_response.response;
+ return cache.match(entries.error_response.request.url)
+ .then(function(result) {
+ assert_response_equals(
+ result, entries.error_response.response,
+ 'Cache.match should return a Response object that has the ' +
+ 'same properties as a stored network error response.');
+ });
+ }, 'Cache.match with a network error Response');
+
+cache_test(function(cache) {
+ // This test validates that we can get a Response from the Cache API,
+ // clone it, and read just one side of the clone. This was previously
+ // bugged in FF for Responses with large bodies.
+ var data = [];
+ data.length = 80 * 1024;
+ data.fill('F');
+ var response;
+ return cache.put('/', new Response(data.toString()))
+ .then(function(result) {
+ return cache.match('/');
+ })
+ .then(function(r) {
+ // Make sure the original response is not GC'd.
+ response = r;
+ // Return only the clone. We purposefully test that the other
+ // half of the clone does not need to be read here.
+ return response.clone().text();
+ })
+ .then(function(text) {
+ assert_equals(text, data.toString(), 'cloned body text can be read correctly');
+ });
+ }, 'Cache produces large Responses that can be cloned and read correctly.');
+
+cache_test(async (cache) => {
+ const url = get_host_info().HTTPS_REMOTE_ORIGIN +
+ '/service-workers/cache-storage/resources/simple.txt?pipe=' +
+ 'header(access-control-allow-origin,*)|' +
+ 'header(access-control-expose-headers,*)|' +
+ 'header(foo,bar)|' +
+ 'header(set-cookie,X)';
+
+ const response = await fetch(url);
+ await cache.put(new Request(url), response);
+ const cached_response = await cache.match(url);
+
+ const headers = cached_response.headers;
+ assert_equals(headers.get('access-control-expose-headers'), '*');
+ assert_equals(headers.get('foo'), 'bar');
+ assert_equals(headers.get('set-cookie'), null);
+ }, 'cors-exposed header should be stored correctly.');
+
+cache_test(async (cache) => {
+ // A URL that should load a resource with a known mime type.
+ const url = '/service-workers/cache-storage/resources/blank.html';
+ const expected_mime_type = 'text/html';
+
+ // Verify we get the expected mime type from the network. Note,
+ // we cannot use an exact match here since some browsers append
+ // character encoding information to the blob.type value.
+ const net_response = await fetch(url);
+ const net_mime_type = (await net_response.blob()).type;
+ assert_true(net_mime_type.includes(expected_mime_type),
+ 'network response should include the expected mime type');
+
+ // Verify we get the exact same mime type when reading the same
+ // URL resource back out of the cache.
+ await cache.add(url);
+ const cache_response = await cache.match(url);
+ const cache_mime_type = (await cache_response.blob()).type;
+ assert_equals(cache_mime_type, net_mime_type,
+ 'network and cache response mime types should match');
+ }, 'MIME type should be set from content-header correctly.');
+
+cache_test(async (cache) => {
+ const url = '/dummy';
+ const original_type = 'text/html';
+ const override_type = 'text/plain';
+ const init_with_headers = {
+ headers: {
+ 'content-type': original_type
+ }
+ }
+
+ // Verify constructing a synthetic response with a content-type header
+ // gets the correct mime type.
+ const response = new Response('hello world', init_with_headers);
+ const original_response_type = (await response.blob()).type;
+ assert_true(original_response_type.includes(original_type),
+ 'original response should include the expected mime type');
+
+ // Verify overwriting the content-type header changes the mime type.
+ const overwritten_response = new Response('hello world', init_with_headers);
+ overwritten_response.headers.set('content-type', override_type);
+ const overwritten_response_type = (await overwritten_response.blob()).type;
+ assert_equals(overwritten_response_type, override_type,
+ 'mime type can be overridden');
+
+ // Verify the Response read from Cache uses the original mime type
+ // computed when it was first constructed.
+ const tmp = new Response('hello world', init_with_headers);
+ tmp.headers.set('content-type', override_type);
+ await cache.put(url, tmp);
+ const cache_response = await cache.match(url);
+ const cache_mime_type = (await cache_response.blob()).type;
+ assert_equals(cache_mime_type, override_type,
+ 'overwritten and cached response mime types should match');
+ }, 'MIME type should reflect Content-Type headers of response.');
+
+cache_test(async (cache) => {
+ const url = new URL('./resources/vary.py?vary=foo',
+ get_host_info().HTTPS_REMOTE_ORIGIN + self.location.pathname);
+ const original_request = new Request(url, { mode: 'no-cors',
+ headers: { 'foo': 'bar' } });
+ const fetch_response = await fetch(original_request);
+ assert_equals(fetch_response.type, 'opaque');
+
+ await cache.put(original_request, fetch_response);
+
+ const match_response_1 = await cache.match(original_request);
+ assert_not_equals(match_response_1, undefined);
+
+ // Verify that cache.match() finds the entry even if queried with a varied
+ // header that does not match the cache key. Vary headers should be ignored
+ // for opaque responses.
+ const different_request = new Request(url, { headers: { 'foo': 'CHANGED' } });
+ const match_response_2 = await cache.match(different_request);
+ assert_not_equals(match_response_2, undefined);
+}, 'Cache.match ignores vary headers on opaque response.');
+
+done();
diff --git a/test/wpt/tests/service-workers/cache-storage/cache-matchAll.https.any.js b/test/wpt/tests/service-workers/cache-storage/cache-matchAll.https.any.js
new file mode 100644
index 0000000..93c5517
--- /dev/null
+++ b/test/wpt/tests/service-workers/cache-storage/cache-matchAll.https.any.js
@@ -0,0 +1,244 @@
+// META: title=Cache.matchAll
+// META: global=window,worker
+// META: script=./resources/test-helpers.js
+// META: timeout=long
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.matchAll('not-present-in-the-cache')
+ .then(function(result) {
+ assert_response_array_equals(
+ result, [],
+ 'Cache.matchAll should resolve with an empty array on failure.');
+ });
+ }, 'Cache.matchAll with no matching entries');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.matchAll(entries.a.request.url)
+ .then(function(result) {
+ assert_response_array_equals(result, [entries.a.response],
+ 'Cache.matchAll should match by URL.');
+ });
+ }, 'Cache.matchAll with URL');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.matchAll(entries.a.request)
+ .then(function(result) {
+ assert_response_array_equals(
+ result, [entries.a.response],
+ 'Cache.matchAll should match by Request.');
+ });
+ }, 'Cache.matchAll with Request');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.matchAll(new Request(entries.a.request.url))
+ .then(function(result) {
+ assert_response_array_equals(
+ result, [entries.a.response],
+ 'Cache.matchAll should match by Request.');
+ });
+ }, 'Cache.matchAll with new Request');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.matchAll(new Request(entries.a.request.url, {method: 'HEAD'}),
+ {ignoreSearch: true})
+ .then(function(result) {
+ assert_response_array_equals(
+ result, [],
+ 'Cache.matchAll should not match HEAD Request.');
+ });
+ }, 'Cache.matchAll with HEAD');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.matchAll(entries.a.request,
+ {ignoreSearch: true})
+ .then(function(result) {
+ assert_response_array_equals(
+ result,
+ [
+ entries.a.response,
+ entries.a_with_query.response
+ ],
+ 'Cache.matchAll with ignoreSearch should ignore the ' +
+ 'search parameters of cached request.');
+ });
+ },
+ 'Cache.matchAll with ignoreSearch option (request with no search ' +
+ 'parameters)');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.matchAll(entries.a_with_query.request,
+ {ignoreSearch: true})
+ .then(function(result) {
+ assert_response_array_equals(
+ result,
+ [
+ entries.a.response,
+ entries.a_with_query.response
+ ],
+ 'Cache.matchAll with ignoreSearch should ignore the ' +
+ 'search parameters of request.');
+ });
+ },
+ 'Cache.matchAll with ignoreSearch option (request with search parameters)');
+
+cache_test(function(cache) {
+ var request = new Request('http://example.com/');
+ var head_request = new Request('http://example.com/', {method: 'HEAD'});
+ var response = new Response('foo');
+ return cache.put(request.clone(), response.clone())
+ .then(function() {
+ return cache.matchAll(head_request.clone());
+ })
+ .then(function(result) {
+ assert_response_array_equals(
+ result, [],
+ 'Cache.matchAll should resolve with empty array for a ' +
+ 'mismatched method.');
+ return cache.matchAll(head_request.clone(),
+ {ignoreMethod: true});
+ })
+ .then(function(result) {
+ assert_response_array_equals(
+ result, [response],
+ 'Cache.matchAll with ignoreMethod should ignore the ' +
+ 'method of request.');
+ });
+ }, 'Cache.matchAll supports ignoreMethod');
+
+cache_test(function(cache) {
+ var vary_request = new Request('http://example.com/c',
+ {headers: {'Cookies': 'is-for-cookie'}});
+ var vary_response = new Response('', {headers: {'Vary': 'Cookies'}});
+ var mismatched_vary_request = new Request('http://example.com/c');
+
+ return cache.put(vary_request.clone(), vary_response.clone())
+ .then(function() {
+ return cache.matchAll(mismatched_vary_request.clone());
+ })
+ .then(function(result) {
+ assert_response_array_equals(
+ result, [],
+ 'Cache.matchAll should resolve as undefined with a ' +
+ 'mismatched vary.');
+ return cache.matchAll(mismatched_vary_request.clone(),
+ {ignoreVary: true});
+ })
+ .then(function(result) {
+ assert_response_array_equals(
+ result, [vary_response],
+ 'Cache.matchAll with ignoreVary should ignore the ' +
+ 'vary of request.');
+ });
+ }, 'Cache.matchAll supports ignoreVary');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.matchAll(entries.cat.request.url + '#mouse')
+ .then(function(result) {
+ assert_response_array_equals(
+ result,
+ [
+ entries.cat.response,
+ ],
+ 'Cache.matchAll should ignore URL fragment.');
+ });
+ }, 'Cache.matchAll with URL containing fragment');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.matchAll('http')
+ .then(function(result) {
+ assert_response_array_equals(
+ result, [],
+ 'Cache.matchAll should treat query as a URL and not ' +
+ 'just a string fragment.');
+ });
+ }, 'Cache.matchAll with string fragment "http" as query');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.matchAll()
+ .then(function(result) {
+ assert_response_array_equals(
+ result,
+ simple_entries.map(entry => entry.response),
+ 'Cache.matchAll without parameters should match all entries.');
+ });
+ }, 'Cache.matchAll without parameters');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.matchAll(undefined)
+ .then(result => {
+ assert_response_array_equals(
+ result,
+ simple_entries.map(entry => entry.response),
+ 'Cache.matchAll with undefined request should match all entries.');
+ });
+ }, 'Cache.matchAll with explicitly undefined request');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.matchAll(undefined, {})
+ .then(result => {
+ assert_response_array_equals(
+ result,
+ simple_entries.map(entry => entry.response),
+ 'Cache.matchAll with undefined request should match all entries.');
+ });
+ }, 'Cache.matchAll with explicitly undefined request and empty options');
+
+prepopulated_cache_test(vary_entries, function(cache, entries) {
+ return cache.matchAll('http://example.com/c')
+ .then(function(result) {
+ assert_response_array_equals(
+ result,
+ [
+ entries.vary_cookie_absent.response
+ ],
+ 'Cache.matchAll should exclude matches if a vary header is ' +
+ 'missing in the query request, but is present in the cached ' +
+ 'request.');
+ })
+
+ .then(function() {
+ return cache.matchAll(
+ new Request('http://example.com/c',
+ {headers: {'Cookies': 'none-of-the-above'}}));
+ })
+ .then(function(result) {
+ assert_response_array_equals(
+ result,
+ [
+ ],
+ 'Cache.matchAll should exclude matches if a vary header is ' +
+ 'missing in the cached request, but is present in the query ' +
+ 'request.');
+ })
+
+ .then(function() {
+ return cache.matchAll(
+ new Request('http://example.com/c',
+ {headers: {'Cookies': 'is-for-cookie'}}));
+ })
+ .then(function(result) {
+ assert_response_array_equals(
+ result,
+ [entries.vary_cookie_is_cookie.response],
+ 'Cache.matchAll should match the entire header if a vary header ' +
+ 'is present in both the query and cached requests.');
+ });
+ }, 'Cache.matchAll with responses containing "Vary" header');
+
+prepopulated_cache_test(vary_entries, function(cache, entries) {
+ return cache.matchAll('http://example.com/c',
+ {ignoreVary: true})
+ .then(function(result) {
+ assert_response_array_equals(
+ result,
+ [
+ entries.vary_cookie_is_cookie.response,
+ entries.vary_cookie_is_good.response,
+ entries.vary_cookie_absent.response
+ ],
+ 'Cache.matchAll should support multiple vary request/response ' +
+ 'pairs.');
+ });
+ }, 'Cache.matchAll with multiple vary pairs');
+
+done();
diff --git a/test/wpt/tests/service-workers/cache-storage/cache-put.https.any.js b/test/wpt/tests/service-workers/cache-storage/cache-put.https.any.js
new file mode 100644
index 0000000..dbf2650
--- /dev/null
+++ b/test/wpt/tests/service-workers/cache-storage/cache-put.https.any.js
@@ -0,0 +1,411 @@
+// META: title=Cache.put
+// META: global=window,worker
+// META: script=/common/get-host-info.sub.js
+// META: script=./resources/test-helpers.js
+// META: timeout=long
+
+var test_url = 'https://example.com/foo';
+var test_body = 'Hello world!';
+const { REMOTE_HOST } = get_host_info();
+
+cache_test(function(cache) {
+ var request = new Request(test_url);
+ var response = new Response(test_body);
+ return cache.put(request, response)
+ .then(function(result) {
+ assert_equals(result, undefined,
+ 'Cache.put should resolve with undefined on success.');
+ });
+ }, 'Cache.put called with simple Request and Response');
+
+cache_test(function(cache) {
+ var test_url = new URL('./resources/simple.txt', location.href).href;
+ var request = new Request(test_url);
+ var response;
+ return fetch(test_url)
+ .then(function(fetch_result) {
+ response = fetch_result.clone();
+ return cache.put(request, fetch_result);
+ })
+ .then(function() {
+ return cache.match(test_url);
+ })
+ .then(function(result) {
+ assert_response_equals(result, response,
+ 'Cache.put should update the cache with ' +
+ 'new request and response.');
+ return result.text();
+ })
+ .then(function(body) {
+ assert_equals(body, 'a simple text file\n',
+ 'Cache.put should store response body.');
+ });
+ }, 'Cache.put called with Request and Response from fetch()');
+
+cache_test(function(cache) {
+ var request = new Request(test_url);
+ var response = new Response(test_body);
+ assert_false(request.bodyUsed,
+ '[https://fetch.spec.whatwg.org/#dom-body-bodyused] ' +
+ 'Request.bodyUsed should be initially false.');
+ return cache.put(request, response)
+ .then(function() {
+ assert_false(request.bodyUsed,
+ 'Cache.put should not mark empty request\'s body used');
+ });
+ }, 'Cache.put with Request without a body');
+
+cache_test(function(cache) {
+ var request = new Request(test_url);
+ var response = new Response();
+ assert_false(response.bodyUsed,
+ '[https://fetch.spec.whatwg.org/#dom-body-bodyused] ' +
+ 'Response.bodyUsed should be initially false.');
+ return cache.put(request, response)
+ .then(function() {
+ assert_false(response.bodyUsed,
+ 'Cache.put should not mark empty response\'s body used');
+ });
+ }, 'Cache.put with Response without a body');
+
+cache_test(function(cache) {
+ var request = new Request(test_url);
+ var response = new Response(test_body);
+ return cache.put(request, response.clone())
+ .then(function() {
+ return cache.match(test_url);
+ })
+ .then(function(result) {
+ assert_response_equals(result, response,
+ 'Cache.put should update the cache with ' +
+ 'new Request and Response.');
+ });
+ }, 'Cache.put with a Response containing an empty URL');
+
+cache_test(function(cache) {
+ var request = new Request(test_url);
+ var response = new Response('', {
+ status: 200,
+ headers: [['Content-Type', 'text/plain']]
+ });
+ return cache.put(request, response)
+ .then(function() {
+ return cache.match(test_url);
+ })
+ .then(function(result) {
+ assert_equals(result.status, 200, 'Cache.put should store status.');
+ assert_equals(result.headers.get('Content-Type'), 'text/plain',
+ 'Cache.put should store headers.');
+ return result.text();
+ })
+ .then(function(body) {
+ assert_equals(body, '',
+ 'Cache.put should store response body.');
+ });
+ }, 'Cache.put with an empty response body');
+
+cache_test(function(cache, test) {
+ var request = new Request(test_url);
+ var response = new Response('', {
+ status: 206,
+ headers: [['Content-Type', 'text/plain']]
+ });
+
+ return promise_rejects_js(
+ test,
+ TypeError,
+ cache.put(request, response),
+ 'Cache.put should reject 206 Responses with a TypeError.');
+ }, 'Cache.put with synthetic 206 response');
+
+cache_test(function(cache, test) {
+ var test_url = new URL('./resources/fetch-status.py?status=206', location.href).href;
+ var request = new Request(test_url);
+ var response;
+ return fetch(test_url)
+ .then(function(fetch_result) {
+ assert_equals(fetch_result.status, 206,
+ 'Test framework error: The status code should be 206.');
+ response = fetch_result.clone();
+ return promise_rejects_js(test, TypeError, cache.put(request, fetch_result));
+ });
+ }, 'Cache.put with HTTP 206 response');
+
+cache_test(function(cache, test) {
+ // We need to jump through some hoops to allow the test to perform opaque
+ // response filtering, but bypass the ORB safelist check. This is
+ // done, by forcing the MIME type retrieval to fail and the
+ // validation of partial first response to succeed.
+ var pipe = "status(206)|header(Content-Type,)|header(Content-Range, bytes 0-1/41)|slice(null, 1)";
+ var test_url = new URL(`./resources/blank.html?pipe=${pipe}`, location.href);
+ test_url.hostname = REMOTE_HOST;
+ var request = new Request(test_url.href, { mode: 'no-cors' });
+ var response;
+ return fetch(request)
+ .then(function(fetch_result) {
+ assert_equals(fetch_result.type, 'opaque',
+ 'Test framework error: The response type should be opaque.');
+ assert_equals(fetch_result.status, 0,
+ 'Test framework error: The status code should be 0 for an ' +
+ ' opaque-filtered response. This is actually HTTP 206.');
+ response = fetch_result.clone();
+ return cache.put(request, fetch_result);
+ })
+ .then(function() {
+ return cache.match(test_url);
+ })
+ .then(function(result) {
+ assert_not_equals(result, undefined,
+ 'Cache.put should store an entry for the opaque response');
+ });
+ }, 'Cache.put with opaque-filtered HTTP 206 response');
+
+cache_test(function(cache) {
+ var test_url = new URL('./resources/fetch-status.py?status=500', location.href).href;
+ var request = new Request(test_url);
+ var response;
+ return fetch(test_url)
+ .then(function(fetch_result) {
+ assert_equals(fetch_result.status, 500,
+ 'Test framework error: The status code should be 500.');
+ response = fetch_result.clone();
+ return cache.put(request, fetch_result);
+ })
+ .then(function() {
+ return cache.match(test_url);
+ })
+ .then(function(result) {
+ assert_response_equals(result, response,
+ 'Cache.put should update the cache with ' +
+ 'new request and response.');
+ return result.text();
+ })
+ .then(function(body) {
+ assert_equals(body, '',
+ 'Cache.put should store response body.');
+ });
+ }, 'Cache.put with HTTP 500 response');
+
+cache_test(function(cache) {
+ var alternate_response_body = 'New body';
+ var alternate_response = new Response(alternate_response_body,
+ { statusText: 'New status' });
+ return cache.put(new Request(test_url),
+ new Response('Old body', { statusText: 'Old status' }))
+ .then(function() {
+ return cache.put(new Request(test_url), alternate_response.clone());
+ })
+ .then(function() {
+ return cache.match(test_url);
+ })
+ .then(function(result) {
+ assert_response_equals(result, alternate_response,
+ 'Cache.put should replace existing ' +
+ 'response with new response.');
+ return result.text();
+ })
+ .then(function(body) {
+ assert_equals(body, alternate_response_body,
+ 'Cache put should store new response body.');
+ });
+ }, 'Cache.put called twice with matching Requests and different Responses');
+
+cache_test(function(cache) {
+ var first_url = test_url;
+ var second_url = first_url + '#(O_o)';
+ var third_url = first_url + '#fragment';
+ var alternate_response_body = 'New body';
+ var alternate_response = new Response(alternate_response_body,
+ { statusText: 'New status' });
+ return cache.put(new Request(first_url),
+ new Response('Old body', { statusText: 'Old status' }))
+ .then(function() {
+ return cache.put(new Request(second_url), alternate_response.clone());
+ })
+ .then(function() {
+ return cache.match(test_url);
+ })
+ .then(function(result) {
+ assert_response_equals(result, alternate_response,
+ 'Cache.put should replace existing ' +
+ 'response with new response.');
+ return result.text();
+ })
+ .then(function(body) {
+ assert_equals(body, alternate_response_body,
+ 'Cache put should store new response body.');
+ })
+ .then(function() {
+ return cache.put(new Request(third_url), alternate_response.clone());
+ })
+ .then(function() {
+ return cache.keys();
+ })
+ .then(function(results) {
+ // Should match urls (without fragments or with different ones) to the
+ // same cache key. However, result.url should be the latest url used.
+ assert_equals(results[0].url, third_url);
+ return;
+ });
+}, 'Cache.put called multiple times with request URLs that differ only by a fragment');
+
+cache_test(function(cache) {
+ var url = 'http://example.com/foo';
+ return cache.put(url, new Response('some body'))
+ .then(function() { return cache.match(url); })
+ .then(function(response) { return response.text(); })
+ .then(function(body) {
+ assert_equals(body, 'some body',
+ 'Cache.put should accept a string as request.');
+ });
+ }, 'Cache.put with a string request');
+
+cache_test(function(cache, test) {
+ return promise_rejects_js(
+ test,
+ TypeError,
+ cache.put(new Request(test_url), 'Hello world!'),
+ 'Cache.put should only accept a Response object as the response.');
+ }, 'Cache.put with an invalid response');
+
+cache_test(function(cache, test) {
+ return promise_rejects_js(
+ test,
+ TypeError,
+ cache.put(new Request('file:///etc/passwd'),
+ new Response(test_body)),
+ 'Cache.put should reject non-HTTP/HTTPS requests with a TypeError.');
+ }, 'Cache.put with a non-HTTP/HTTPS request');
+
+cache_test(function(cache) {
+ var response = new Response(test_body);
+ return cache.put(new Request('relative-url'), response.clone())
+ .then(function() {
+ return cache.match(new URL('relative-url', location.href).href);
+ })
+ .then(function(result) {
+ assert_response_equals(result, response,
+ 'Cache.put should accept a relative URL ' +
+ 'as the request.');
+ });
+ }, 'Cache.put with a relative URL');
+
+cache_test(function(cache, test) {
+ var request = new Request('http://example.com/foo', { method: 'HEAD' });
+ return promise_rejects_js(
+ test,
+ TypeError,
+ cache.put(request, new Response(test_body)),
+ 'Cache.put should throw a TypeError for non-GET requests.');
+ }, 'Cache.put with a non-GET request');
+
+cache_test(function(cache, test) {
+ return promise_rejects_js(
+ test,
+ TypeError,
+ cache.put(new Request(test_url), null),
+ 'Cache.put should throw a TypeError for a null response.');
+ }, 'Cache.put with a null response');
+
+cache_test(function(cache, test) {
+ var request = new Request(test_url, {method: 'POST', body: test_body});
+ return promise_rejects_js(
+ test,
+ TypeError,
+ cache.put(request, new Response(test_body)),
+ 'Cache.put should throw a TypeError for a POST request.');
+ }, 'Cache.put with a POST request');
+
+cache_test(function(cache) {
+ var response = new Response(test_body);
+ assert_false(response.bodyUsed,
+ '[https://fetch.spec.whatwg.org/#dom-body-bodyused] ' +
+ 'Response.bodyUsed should be initially false.');
+ return response.text().then(function() {
+ assert_true(
+ response.bodyUsed,
+ '[https://fetch.spec.whatwg.org/#concept-body-consume-body] ' +
+ 'The text() method should make the body disturbed.');
+ var request = new Request(test_url);
+ return cache.put(request, response).then(() => {
+ assert_unreached('cache.put should be rejected');
+ }, () => {});
+ });
+ }, 'Cache.put with a used response body');
+
+cache_test(function(cache) {
+ var response = new Response(test_body);
+ return cache.put(new Request(test_url), response)
+ .then(function() {
+ assert_throws_js(TypeError, () => response.body.getReader());
+ });
+ }, 'getReader() after Cache.put');
+
+cache_test(function(cache, test) {
+ return promise_rejects_js(
+ test,
+ TypeError,
+ cache.put(new Request(test_url),
+ new Response(test_body, { headers: { VARY: '*' }})),
+ 'Cache.put should reject VARY:* Responses with a TypeError.');
+ }, 'Cache.put with a VARY:* Response');
+
+cache_test(function(cache, test) {
+ return promise_rejects_js(
+ test,
+ TypeError,
+ cache.put(new Request(test_url),
+ new Response(test_body,
+ { headers: { VARY: 'Accept-Language,*' }})),
+ 'Cache.put should reject Responses with an embedded VARY:* with a ' +
+ 'TypeError.');
+ }, 'Cache.put with an embedded VARY:* Response');
+
+cache_test(async function(cache, test) {
+ const url = new URL('./resources/vary.py?vary=*',
+ get_host_info().HTTPS_REMOTE_ORIGIN + self.location.pathname);
+ const request = new Request(url, { mode: 'no-cors' });
+ const response = await fetch(request);
+ assert_equals(response.type, 'opaque');
+ await cache.put(request, response);
+ }, 'Cache.put with a VARY:* opaque response should not reject');
+
+cache_test(function(cache) {
+ var url = 'foo.html';
+ var redirectURL = 'http://example.com/foo-bar.html';
+ var redirectResponse = Response.redirect(redirectURL);
+ assert_equals(redirectResponse.headers.get('Location'), redirectURL,
+ 'Response.redirect() should set Location header.');
+ return cache.put(url, redirectResponse.clone())
+ .then(function() {
+ return cache.match(url);
+ })
+ .then(function(response) {
+ assert_response_equals(response, redirectResponse,
+ 'Redirect response is reproduced by the Cache API');
+ assert_equals(response.headers.get('Location'), redirectURL,
+ 'Location header is preserved by Cache API.');
+ });
+ }, 'Cache.put should store Response.redirect() correctly');
+
+cache_test(async (cache) => {
+ var request = new Request(test_url);
+ var response = new Response(new Blob([test_body]));
+ await cache.put(request, response);
+ var cachedResponse = await cache.match(request);
+ assert_equals(await cachedResponse.text(), test_body);
+ }, 'Cache.put called with simple Request and blob Response');
+
+cache_test(async (cache) => {
+ var formData = new FormData();
+ formData.append("name", "value");
+
+ var request = new Request(test_url);
+ var response = new Response(formData);
+ await cache.put(request, response);
+ var cachedResponse = await cache.match(request);
+ var cachedResponseText = await cachedResponse.text();
+ assert_true(cachedResponseText.indexOf("name=\"name\"\r\n\r\nvalue") !== -1);
+}, 'Cache.put called with simple Request and form data Response');
+
+done();
diff --git a/test/wpt/tests/service-workers/cache-storage/cache-storage-buckets.https.any.js b/test/wpt/tests/service-workers/cache-storage/cache-storage-buckets.https.any.js
new file mode 100644
index 0000000..fd59ba4
--- /dev/null
+++ b/test/wpt/tests/service-workers/cache-storage/cache-storage-buckets.https.any.js
@@ -0,0 +1,64 @@
+// META: title=Cache.put
+// META: global=window,worker
+// META: script=/common/get-host-info.sub.js
+// META: script=resources/test-helpers.js
+// META: script=/storage/buckets/resources/util.js
+// META: timeout=long
+
+var test_url = 'https://example.com/foo';
+var test_body = 'Hello world!';
+const { REMOTE_HOST } = get_host_info();
+
+promise_test(async function(test) {
+ await prepareForBucketTest(test);
+ var inboxBucket = await navigator.storageBuckets.open('inbox');
+ var draftsBucket = await navigator.storageBuckets.open('drafts');
+
+ const cacheName = 'attachments';
+ const cacheKey = 'receipt1.txt';
+
+ var inboxCache = await inboxBucket.caches.open(cacheName);
+ var draftsCache = await draftsBucket.caches.open(cacheName);
+
+ await inboxCache.put(cacheKey, new Response('bread x 2'))
+ await draftsCache.put(cacheKey, new Response('eggs x 1'));
+
+ return inboxCache.match(cacheKey)
+ .then(function(result) {
+ return result.text();
+ })
+ .then(function(body) {
+ assert_equals(body, 'bread x 2', 'Wrong cache contents');
+ return draftsCache.match(cacheKey);
+ })
+ .then(function(result) {
+ return result.text();
+ })
+ .then(function(body) {
+ assert_equals(body, 'eggs x 1', 'Wrong cache contents');
+ });
+}, 'caches from different buckets have different contents');
+
+promise_test(async function(test) {
+ await prepareForBucketTest(test);
+ var inboxBucket = await navigator.storageBuckets.open('inbox');
+ var draftBucket = await navigator.storageBuckets.open('drafts');
+
+ var caches = inboxBucket.caches;
+ var attachments = await caches.open('attachments');
+ await attachments.put('receipt1.txt', new Response('bread x 2'));
+ var result = await attachments.match('receipt1.txt');
+ assert_equals(await result.text(), 'bread x 2');
+
+ await navigator.storageBuckets.delete('inbox');
+
+ await promise_rejects_dom(
+ test, 'UnknownError', caches.open('attachments'));
+
+ // Also test when `caches` is first accessed after the deletion.
+ await navigator.storageBuckets.delete('drafts');
+ return promise_rejects_dom(
+ test, 'UnknownError', draftBucket.caches.open('attachments'));
+}, 'cache.open promise is rejected when bucket is gone');
+
+done();
diff --git a/test/wpt/tests/service-workers/cache-storage/cache-storage-keys.https.any.js b/test/wpt/tests/service-workers/cache-storage/cache-storage-keys.https.any.js
new file mode 100644
index 0000000..f19522b
--- /dev/null
+++ b/test/wpt/tests/service-workers/cache-storage/cache-storage-keys.https.any.js
@@ -0,0 +1,35 @@
+// META: title=CacheStorage.keys
+// META: global=window,worker
+// META: script=./resources/test-helpers.js
+// META: timeout=long
+
+var test_cache_list =
+ ['', 'example', 'Another cache name', 'A', 'a', 'ex ample'];
+
+promise_test(function(test) {
+ return self.caches.keys()
+ .then(function(keys) {
+ assert_true(Array.isArray(keys),
+ 'CacheStorage.keys should return an Array.');
+ return Promise.all(keys.map(function(key) {
+ return self.caches.delete(key);
+ }));
+ })
+ .then(function() {
+ return Promise.all(test_cache_list.map(function(key) {
+ return self.caches.open(key);
+ }));
+ })
+
+ .then(function() { return self.caches.keys(); })
+ .then(function(keys) {
+ assert_true(Array.isArray(keys),
+ 'CacheStorage.keys should return an Array.');
+ assert_array_equals(keys,
+ test_cache_list,
+ 'CacheStorage.keys should only return ' +
+ 'existing caches.');
+ });
+ }, 'CacheStorage keys');
+
+done();
diff --git a/test/wpt/tests/service-workers/cache-storage/cache-storage-match.https.any.js b/test/wpt/tests/service-workers/cache-storage/cache-storage-match.https.any.js
new file mode 100644
index 0000000..0c31b72
--- /dev/null
+++ b/test/wpt/tests/service-workers/cache-storage/cache-storage-match.https.any.js
@@ -0,0 +1,245 @@
+// META: title=CacheStorage.match
+// META: global=window,worker
+// META: script=./resources/test-helpers.js
+// META: timeout=long
+
+(function() {
+ var next_index = 1;
+
+ // Returns a transaction (request, response, and url) for a unique URL.
+ function create_unique_transaction(test) {
+ var uniquifier = String(next_index++);
+ var url = 'http://example.com/' + uniquifier;
+
+ return {
+ request: new Request(url),
+ response: new Response('hello'),
+ url: url
+ };
+ }
+
+ self.create_unique_transaction = create_unique_transaction;
+})();
+
+cache_test(function(cache) {
+ var transaction = create_unique_transaction();
+
+ return cache.put(transaction.request.clone(), transaction.response.clone())
+ .then(function() {
+ return self.caches.match(transaction.request);
+ })
+ .then(function(response) {
+ assert_response_equals(response, transaction.response,
+ 'The response should not have changed.');
+ });
+}, 'CacheStorageMatch with no cache name provided');
+
+cache_test(function(cache) {
+ var transaction = create_unique_transaction();
+
+ var test_cache_list = ['a', 'b', 'c'];
+ return cache.put(transaction.request.clone(), transaction.response.clone())
+ .then(function() {
+ return Promise.all(test_cache_list.map(function(key) {
+ return self.caches.open(key);
+ }));
+ })
+ .then(function() {
+ return self.caches.match(transaction.request);
+ })
+ .then(function(response) {
+ assert_response_equals(response, transaction.response,
+ 'The response should not have changed.');
+ });
+}, 'CacheStorageMatch from one of many caches');
+
+promise_test(function(test) {
+ var transaction = create_unique_transaction();
+
+ var test_cache_list = ['x', 'y', 'z'];
+ return Promise.all(test_cache_list.map(function(key) {
+ return self.caches.open(key);
+ }))
+ .then(function() { return self.caches.open('x'); })
+ .then(function(cache) {
+ return cache.put(transaction.request.clone(),
+ transaction.response.clone());
+ })
+ .then(function() {
+ return self.caches.match(transaction.request, {cacheName: 'x'});
+ })
+ .then(function(response) {
+ assert_response_equals(response, transaction.response,
+ 'The response should not have changed.');
+ })
+ .then(function() {
+ return self.caches.match(transaction.request, {cacheName: 'y'});
+ })
+ .then(function(response) {
+ assert_equals(response, undefined,
+ 'Cache y should not have a response for the request.');
+ });
+}, 'CacheStorageMatch from one of many caches by name');
+
+cache_test(function(cache) {
+ var transaction = create_unique_transaction();
+ return cache.put(transaction.url, transaction.response.clone())
+ .then(function() {
+ return self.caches.match(transaction.request);
+ })
+ .then(function(response) {
+ assert_response_equals(response, transaction.response,
+ 'The response should not have changed.');
+ });
+}, 'CacheStorageMatch a string request');
+
+cache_test(function(cache) {
+ var transaction = create_unique_transaction();
+ return cache.put(transaction.request.clone(), transaction.response.clone())
+ .then(function() {
+ return self.caches.match(new Request(transaction.request.url,
+ {method: 'HEAD'}));
+ })
+ .then(function(response) {
+ assert_equals(response, undefined,
+ 'A HEAD request should not be matched');
+ });
+}, 'CacheStorageMatch a HEAD request');
+
+promise_test(function(test) {
+ var transaction = create_unique_transaction();
+ return self.caches.match(transaction.request)
+ .then(function(response) {
+ assert_equals(response, undefined,
+ 'The response should not be found.');
+ });
+}, 'CacheStorageMatch with no cached entry');
+
+promise_test(function(test) {
+ var transaction = create_unique_transaction();
+ return self.caches.delete('foo')
+ .then(function() {
+ return self.caches.has('foo');
+ })
+ .then(function(has_foo) {
+ assert_false(has_foo, "The cache should not exist.");
+ return self.caches.match(transaction.request, {cacheName: 'foo'});
+ })
+ .then(function(response) {
+ assert_equals(response, undefined,
+ 'The match with bad cache name should resolve to ' +
+ 'undefined.');
+ return self.caches.has('foo');
+ })
+ .then(function(has_foo) {
+ assert_false(has_foo, "The cache should still not exist.");
+ });
+}, 'CacheStorageMatch with no caches available but name provided');
+
+cache_test(function(cache) {
+ var transaction = create_unique_transaction();
+
+ return self.caches.delete('')
+ .then(function() {
+ return self.caches.has('');
+ })
+ .then(function(has_cache) {
+ assert_false(has_cache, "The cache should not exist.");
+ return cache.put(transaction.request, transaction.response.clone());
+ })
+ .then(function() {
+ return self.caches.match(transaction.request, {cacheName: ''});
+ })
+ .then(function(response) {
+ assert_equals(response, undefined,
+ 'The response should not be found.');
+ return self.caches.open('');
+ })
+ .then(function(cache) {
+ return cache.put(transaction.request, transaction.response);
+ })
+ .then(function() {
+ return self.caches.match(transaction.request, {cacheName: ''});
+ })
+ .then(function(response) {
+ assert_response_equals(response, transaction.response,
+ 'The response should be matched.');
+ return self.caches.delete('');
+ });
+}, 'CacheStorageMatch with empty cache name provided');
+
+cache_test(function(cache) {
+ var request = new Request('http://example.com/?foo');
+ var no_query_request = new Request('http://example.com/');
+ var response = new Response('foo');
+ return cache.put(request.clone(), response.clone())
+ .then(function() {
+ return self.caches.match(no_query_request.clone());
+ })
+ .then(function(result) {
+ assert_equals(
+ result, undefined,
+ 'CacheStorageMatch should resolve as undefined with a ' +
+ 'mismatched query.');
+ return self.caches.match(no_query_request.clone(),
+ {ignoreSearch: true});
+ })
+ .then(function(result) {
+ assert_response_equals(
+ result, response,
+ 'CacheStorageMatch with ignoreSearch should ignore the ' +
+ 'query of the request.');
+ });
+ }, 'CacheStorageMatch supports ignoreSearch');
+
+cache_test(function(cache) {
+ var request = new Request('http://example.com/');
+ var head_request = new Request('http://example.com/', {method: 'HEAD'});
+ var response = new Response('foo');
+ return cache.put(request.clone(), response.clone())
+ .then(function() {
+ return self.caches.match(head_request.clone());
+ })
+ .then(function(result) {
+ assert_equals(
+ result, undefined,
+ 'CacheStorageMatch should resolve as undefined with a ' +
+ 'mismatched method.');
+ return self.caches.match(head_request.clone(),
+ {ignoreMethod: true});
+ })
+ .then(function(result) {
+ assert_response_equals(
+ result, response,
+ 'CacheStorageMatch with ignoreMethod should ignore the ' +
+ 'method of request.');
+ });
+ }, 'Cache.match supports ignoreMethod');
+
+cache_test(function(cache) {
+ var vary_request = new Request('http://example.com/c',
+ {headers: {'Cookies': 'is-for-cookie'}});
+ var vary_response = new Response('', {headers: {'Vary': 'Cookies'}});
+ var mismatched_vary_request = new Request('http://example.com/c');
+
+ return cache.put(vary_request.clone(), vary_response.clone())
+ .then(function() {
+ return self.caches.match(mismatched_vary_request.clone());
+ })
+ .then(function(result) {
+ assert_equals(
+ result, undefined,
+ 'CacheStorageMatch should resolve as undefined with a ' +
+ ' mismatched vary.');
+ return self.caches.match(mismatched_vary_request.clone(),
+ {ignoreVary: true});
+ })
+ .then(function(result) {
+ assert_response_equals(
+ result, vary_response,
+ 'CacheStorageMatch with ignoreVary should ignore the ' +
+ 'vary of request.');
+ });
+ }, 'CacheStorageMatch supports ignoreVary');
+
+done();
diff --git a/test/wpt/tests/service-workers/cache-storage/cache-storage.https.any.js b/test/wpt/tests/service-workers/cache-storage/cache-storage.https.any.js
new file mode 100644
index 0000000..b7d5af7
--- /dev/null
+++ b/test/wpt/tests/service-workers/cache-storage/cache-storage.https.any.js
@@ -0,0 +1,239 @@
+// META: title=CacheStorage
+// META: global=window,worker
+// META: script=./resources/test-helpers.js
+// META: timeout=long
+
+promise_test(function(t) {
+ var cache_name = 'cache-storage/foo';
+ return self.caches.delete(cache_name)
+ .then(function() {
+ return self.caches.open(cache_name);
+ })
+ .then(function(cache) {
+ assert_true(cache instanceof Cache,
+ 'CacheStorage.open should return a Cache.');
+ });
+ }, 'CacheStorage.open');
+
+promise_test(function(t) {
+ var cache_name = 'cache-storage/bar';
+ var first_cache = null;
+ var second_cache = null;
+ return self.caches.open(cache_name)
+ .then(function(cache) {
+ first_cache = cache;
+ return self.caches.delete(cache_name);
+ })
+ .then(function() {
+ return first_cache.add('./resources/simple.txt');
+ })
+ .then(function() {
+ return self.caches.keys();
+ })
+ .then(function(cache_names) {
+ assert_equals(cache_names.indexOf(cache_name), -1);
+ return self.caches.open(cache_name);
+ })
+ .then(function(cache) {
+ second_cache = cache;
+ return second_cache.keys();
+ })
+ .then(function(keys) {
+ assert_equals(keys.length, 0);
+ return first_cache.keys();
+ })
+ .then(function(keys) {
+ assert_equals(keys.length, 1);
+ // Clean up
+ return self.caches.delete(cache_name);
+ });
+ }, 'CacheStorage.delete dooms, but does not delete immediately');
+
+promise_test(function(t) {
+ // Note that this test may collide with other tests running in the same
+ // origin that also uses an empty cache name.
+ var cache_name = '';
+ return self.caches.delete(cache_name)
+ .then(function() {
+ return self.caches.open(cache_name);
+ })
+ .then(function(cache) {
+ assert_true(cache instanceof Cache,
+ 'CacheStorage.open should accept an empty name.');
+ });
+ }, 'CacheStorage.open with an empty name');
+
+promise_test(function(t) {
+ return promise_rejects_js(
+ t,
+ TypeError,
+ self.caches.open(),
+ 'CacheStorage.open should throw TypeError if called with no arguments.');
+ }, 'CacheStorage.open with no arguments');
+
+promise_test(function(t) {
+ var test_cases = [
+ {
+ name: 'cache-storage/lowercase',
+ should_not_match:
+ [
+ 'cache-storage/Lowercase',
+ ' cache-storage/lowercase',
+ 'cache-storage/lowercase '
+ ]
+ },
+ {
+ name: 'cache-storage/has a space',
+ should_not_match:
+ [
+ 'cache-storage/has'
+ ]
+ },
+ {
+ name: 'cache-storage/has\000_in_the_name',
+ should_not_match:
+ [
+ 'cache-storage/has',
+ 'cache-storage/has_in_the_name'
+ ]
+ }
+ ];
+ return Promise.all(test_cases.map(function(testcase) {
+ var cache_name = testcase.name;
+ return self.caches.delete(cache_name)
+ .then(function() {
+ return self.caches.open(cache_name);
+ })
+ .then(function() {
+ return self.caches.has(cache_name);
+ })
+ .then(function(result) {
+ assert_true(result,
+ 'CacheStorage.has should return true for existing ' +
+ 'cache.');
+ })
+ .then(function() {
+ return Promise.all(
+ testcase.should_not_match.map(function(cache_name) {
+ return self.caches.has(cache_name)
+ .then(function(result) {
+ assert_false(result,
+ 'CacheStorage.has should only perform ' +
+ 'exact matches on cache names.');
+ });
+ }));
+ })
+ .then(function() {
+ return self.caches.delete(cache_name);
+ });
+ }));
+ }, 'CacheStorage.has with existing cache');
+
+promise_test(function(t) {
+ return self.caches.has('cheezburger')
+ .then(function(result) {
+ assert_false(result,
+ 'CacheStorage.has should return false for ' +
+ 'nonexistent cache.');
+ });
+ }, 'CacheStorage.has with nonexistent cache');
+
+promise_test(function(t) {
+ var cache_name = 'cache-storage/open';
+ var cache;
+ return self.caches.delete(cache_name)
+ .then(function() {
+ return self.caches.open(cache_name);
+ })
+ .then(function(result) {
+ cache = result;
+ })
+ .then(function() {
+ return cache.add('./resources/simple.txt');
+ })
+ .then(function() {
+ return self.caches.open(cache_name);
+ })
+ .then(function(result) {
+ assert_true(result instanceof Cache,
+ 'CacheStorage.open should return a Cache object');
+ assert_not_equals(result, cache,
+ 'CacheStorage.open should return a new Cache ' +
+ 'object each time its called.');
+ return Promise.all([cache.keys(), result.keys()]);
+ })
+ .then(function(results) {
+ var expected_urls = results[0].map(function(r) { return r.url });
+ var actual_urls = results[1].map(function(r) { return r.url });
+ assert_array_equals(actual_urls, expected_urls,
+ 'CacheStorage.open should return a new Cache ' +
+ 'object for the same backing store.');
+ });
+ }, 'CacheStorage.open with existing cache');
+
+promise_test(function(t) {
+ var cache_name = 'cache-storage/delete';
+
+ return self.caches.delete(cache_name)
+ .then(function() {
+ return self.caches.open(cache_name);
+ })
+ .then(function() { return self.caches.delete(cache_name); })
+ .then(function(result) {
+ assert_true(result,
+ 'CacheStorage.delete should return true after ' +
+ 'deleting an existing cache.');
+ })
+
+ .then(function() { return self.caches.has(cache_name); })
+ .then(function(cache_exists) {
+ assert_false(cache_exists,
+ 'CacheStorage.has should return false after ' +
+ 'fulfillment of CacheStorage.delete promise.');
+ });
+ }, 'CacheStorage.delete with existing cache');
+
+promise_test(function(t) {
+ return self.caches.delete('cheezburger')
+ .then(function(result) {
+ assert_false(result,
+ 'CacheStorage.delete should return false for a ' +
+ 'nonexistent cache.');
+ });
+ }, 'CacheStorage.delete with nonexistent cache');
+
+promise_test(function(t) {
+ var unpaired_name = 'unpaired\uD800';
+ var converted_name = 'unpaired\uFFFD';
+
+ // The test assumes that a cache with converted_name does not
+ // exist, but if the implementation fails the test then such
+ // a cache will be created. Start off in a fresh state by
+ // deleting all caches.
+ return delete_all_caches()
+ .then(function() {
+ return self.caches.has(converted_name);
+ })
+ .then(function(cache_exists) {
+ assert_false(cache_exists,
+ 'Test setup failure: cache should not exist');
+ })
+ .then(function() { return self.caches.open(unpaired_name); })
+ .then(function() { return self.caches.keys(); })
+ .then(function(keys) {
+ assert_true(keys.indexOf(unpaired_name) !== -1,
+ 'keys should include cache with bad name');
+ })
+ .then(function() { return self.caches.has(unpaired_name); })
+ .then(function(cache_exists) {
+ assert_true(cache_exists,
+ 'CacheStorage names should be not be converted.');
+ })
+ .then(function() { return self.caches.has(converted_name); })
+ .then(function(cache_exists) {
+ assert_false(cache_exists,
+ 'CacheStorage names should be not be converted.');
+ });
+ }, 'CacheStorage names are DOMStrings not USVStrings');
+
+done();
diff --git a/test/wpt/tests/service-workers/cache-storage/common.https.window.js b/test/wpt/tests/service-workers/cache-storage/common.https.window.js
new file mode 100644
index 0000000..eba312c
--- /dev/null
+++ b/test/wpt/tests/service-workers/cache-storage/common.https.window.js
@@ -0,0 +1,44 @@
+// META: title=Cache Storage: Verify that Window and Workers see same storage
+// META: timeout=long
+
+function wait_for_message(worker) {
+ return new Promise(function(resolve) {
+ worker.addEventListener('message', function listener(e) {
+ resolve(e.data);
+ worker.removeEventListener('message', listener);
+ });
+ });
+}
+
+promise_test(function(t) {
+ var cache_name = 'common-test';
+ return self.caches.delete(cache_name)
+ .then(function() {
+ var worker = new Worker('resources/common-worker.js');
+ worker.postMessage({name: cache_name});
+ return wait_for_message(worker);
+ })
+ .then(function(message) {
+ return self.caches.open(cache_name);
+ })
+ .then(function(cache) {
+ return Promise.all([
+ cache.match('https://example.com/a'),
+ cache.match('https://example.com/b'),
+ cache.match('https://example.com/c')
+ ]);
+ })
+ .then(function(responses) {
+ return Promise.all(responses.map(
+ function(response) { return response.text(); }
+ ));
+ })
+ .then(function(bodies) {
+ assert_equals(bodies[0], 'a',
+ 'Body should match response put by worker');
+ assert_equals(bodies[1], 'b',
+ 'Body should match response put by worker');
+ assert_equals(bodies[2], 'c',
+ 'Body should match response put by worker');
+ });
+}, 'Window sees cache puts by Worker');
diff --git a/test/wpt/tests/service-workers/cache-storage/crashtests/cache-response-clone.https.html b/test/wpt/tests/service-workers/cache-storage/crashtests/cache-response-clone.https.html
new file mode 100644
index 0000000..ec930a8
--- /dev/null
+++ b/test/wpt/tests/service-workers/cache-storage/crashtests/cache-response-clone.https.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html class="test-wait">
+<meta charset="utf-8">
+<script type="module">
+ const cache = await window.caches.open('cache_name_0')
+ await cache.add("")
+ const resp1 = await cache.match("")
+ const readStream = resp1.body
+ // Cloning will open the stream via NS_AsyncCopy in Gecko
+ resp1.clone()
+ // Give a little bit of time
+ await new Promise(setTimeout)
+ // At this point the previous open operation is about to finish but not yet.
+ // It will finish after the second open operation is made, potentially causing incorrect state.
+ await readStream.getReader().read();
+ document.documentElement.classList.remove('test-wait')
+</script>
diff --git a/test/wpt/tests/service-workers/cache-storage/credentials.https.html b/test/wpt/tests/service-workers/cache-storage/credentials.https.html
new file mode 100644
index 0000000..0fe4a0a
--- /dev/null
+++ b/test/wpt/tests/service-workers/cache-storage/credentials.https.html
@@ -0,0 +1,46 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Cache Storage: Verify credentials are respected by Cache operations</title>
+<link rel="help" href="https://w3c.github.io/ServiceWorker/#cache-storage">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="./../service-worker/resources/test-helpers.sub.js"></script>
+<style>iframe { display: none; }</style>
+<script>
+
+var worker = "./resources/credentials-worker.js";
+var scope = "./resources/credentials-iframe.html";
+promise_test(function(t) {
+ return self.caches.delete('credentials')
+ .then(function() {
+ return service_worker_unregister_and_register(t, worker, scope)
+ })
+ .then(function(reg) {
+ return wait_for_state(t, reg.installing, 'activated');
+ })
+ .then(function() {
+ return with_iframe(scope);
+ })
+ .then(function(frame) {
+ frame.contentWindow.postMessage([
+ {name: 'file.txt', username: 'aa', password: 'bb'},
+ {name: 'file.txt', username: 'cc', password: 'dd'},
+ {name: 'file.txt'}
+ ], '*');
+ return new Promise(function(resolve, reject) {
+ window.onmessage = t.step_func(function(e) {
+ resolve(e.data);
+ });
+ });
+ })
+ .then(function(data) {
+ assert_equals(data.length, 3, 'three entries should be present');
+ assert_equals(data.filter(function(url) { return /@/.test(url); }).length, 2,
+ 'two entries should contain credentials');
+ assert_true(data.some(function(url) { return /aa:bb@/.test(url); }),
+ 'entry with credentials aa:bb should be present');
+ assert_true(data.some(function(url) { return /cc:dd@/.test(url); }),
+ 'entry with credentials cc:dd should be present');
+ });
+}, "Cache API matching includes credentials");
+</script>
diff --git a/test/wpt/tests/service-workers/cache-storage/cross-partition.https.tentative.html b/test/wpt/tests/service-workers/cache-storage/cross-partition.https.tentative.html
new file mode 100644
index 0000000..1cfc256
--- /dev/null
+++ b/test/wpt/tests/service-workers/cache-storage/cross-partition.https.tentative.html
@@ -0,0 +1,269 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<!-- Pull in executor_path needed by newPopup / newIframe -->
+<script src="/html/cross-origin-embedder-policy/credentialless/resources/common.js"></script>
+<!-- Pull in importScript / newPopup / newIframe -->
+<script src="/html/anonymous-iframe/resources/common.js"></script>
+<body>
+<script>
+
+const cache_exists_js = (cache_name, response_queue_name) => `
+ try {
+ const exists = await self.caches.has("${cache_name}");
+ if (exists) {
+ await send("${response_queue_name}", "true");
+ } else {
+ await send("${response_queue_name}", "false");
+ }
+ } catch {
+ await send("${response_queue_name}", "exception");
+ }
+`;
+
+const add_iframe_js = (iframe_origin, response_queue_uuid) => `
+ const importScript = ${importScript};
+ await importScript("/html/cross-origin-embedder-policy/credentialless" +
+ "/resources/common.js");
+ await importScript("/html/anonymous-iframe/resources/common.js");
+ await importScript("/common/utils.js");
+ await send("${response_queue_uuid}", newIframe("${iframe_origin}"));
+`;
+
+const same_site_origin = get_host_info().HTTPS_ORIGIN;
+const cross_site_origin = get_host_info().HTTPS_NOTSAMESITE_ORIGIN;
+
+async function create_test_iframes(t, response_queue_uuid) {
+
+ // Create a same-origin iframe in a cross-site popup.
+ const not_same_site_popup_uuid = newPopup(t, cross_site_origin);
+ await send(not_same_site_popup_uuid,
+ add_iframe_js(same_site_origin, response_queue_uuid));
+ const iframe_1_uuid = await receive(response_queue_uuid);
+
+ // Create a same-origin iframe in a same-site popup.
+ const same_origin_popup_uuid = newPopup(t, same_site_origin);
+ await send(same_origin_popup_uuid,
+ add_iframe_js(same_site_origin, response_queue_uuid));
+ const iframe_2_uuid = await receive(response_queue_uuid);
+
+ return [iframe_1_uuid, iframe_2_uuid];
+}
+
+promise_test(t => {
+ return new Promise(async (resolve, reject) => {
+ try {
+ const response_queue_uuid = token();
+
+ const [iframe_1_uuid, iframe_2_uuid] =
+ await create_test_iframes(t, response_queue_uuid);
+
+ const cache_name = token();
+ await self.caches.open(cache_name);
+ t.add_cleanup(() => self.caches.delete(cache_name));
+
+ await send(iframe_2_uuid,
+ cache_exists_js(cache_name, response_queue_uuid));
+ if (await receive(response_queue_uuid) !== "true") {
+ reject("Cache not visible in same-top-level-site iframe");
+ }
+
+ await send(iframe_1_uuid,
+ cache_exists_js(cache_name, response_queue_uuid));
+ if (await receive(response_queue_uuid) !== "false") {
+ reject("Cache visible in not-same-top-level-site iframe");
+ }
+
+ resolve();
+ } catch (e) {
+ reject(e);
+ }
+ });
+}, "CacheStorage caches shouldn't be shared with a cross-partition iframe");
+
+const newWorker = (origin) => {
+ const worker_token = token();
+ const worker_url = origin + executor_worker_path + `&uuid=${worker_token}`;
+ const worker = new Worker(worker_url);
+ return worker_token;
+}
+
+promise_test(t => {
+ return new Promise(async (resolve, reject) => {
+ try {
+ const response_queue_uuid = token();
+
+ const create_worker_js = (origin) => `
+ const importScript = ${importScript};
+ await importScript("/html/cross-origin-embedder-policy/credentialless" +
+ "/resources/common.js");
+ await importScript("/html/anonymous-iframe/resources/common.js");
+ await importScript("/common/utils.js");
+ const newWorker = ${newWorker};
+ await send("${response_queue_uuid}", newWorker("${origin}"));
+ `;
+
+ const [iframe_1_uuid, iframe_2_uuid] =
+ await create_test_iframes(t, response_queue_uuid);
+
+ // Create a dedicated worker in the cross-top-level-site iframe.
+ await send(iframe_1_uuid, create_worker_js(same_site_origin));
+ const worker_1_uuid = await receive(response_queue_uuid);
+
+ // Create a dedicated worker in the same-top-level-site iframe.
+ await send(iframe_2_uuid, create_worker_js(same_site_origin));
+ const worker_2_uuid = await receive(response_queue_uuid);
+
+ const cache_name = token();
+ await self.caches.open(cache_name);
+ t.add_cleanup(() => self.caches.delete(cache_name));
+
+ await send(worker_2_uuid,
+ cache_exists_js(cache_name, response_queue_uuid));
+ if (await receive(response_queue_uuid) !== "true") {
+ reject("Cache not visible in same-top-level-site worker");
+ }
+
+ await send(worker_1_uuid,
+ cache_exists_js(cache_name, response_queue_uuid));
+ if (await receive(response_queue_uuid) !== "false") {
+ reject("Cache visible in not-same-top-level-site worker");
+ }
+ resolve();
+ } catch (e) {
+ reject(e);
+ }
+ });
+}, "CacheStorage caches shouldn't be shared with a cross-partition dedicated worker");
+
+const newSharedWorker = (origin) => {
+ const worker_token = token();
+ const worker_url = origin + executor_worker_path + `&uuid=${worker_token}`;
+ const worker = new SharedWorker(worker_url, worker_token);
+ return worker_token;
+}
+
+promise_test(t => {
+ return new Promise(async (resolve, reject) => {
+ try {
+ const response_queue_uuid = token();
+
+ const create_worker_js = (origin) => `
+ const importScript = ${importScript};
+ await importScript("/html/cross-origin-embedder-policy/credentialless" +
+ "/resources/common.js");
+ await importScript("/html/anonymous-iframe/resources/common.js");
+ await importScript("/common/utils.js");
+ const newSharedWorker = ${newSharedWorker};
+ await send("${response_queue_uuid}", newSharedWorker("${origin}"));
+ `;
+
+ const [iframe_1_uuid, iframe_2_uuid] =
+ await create_test_iframes(t, response_queue_uuid);
+
+ // Create a shared worker in the cross-top-level-site iframe.
+ await send(iframe_1_uuid, create_worker_js(same_site_origin));
+ const worker_1_uuid = await receive(response_queue_uuid);
+
+ // Create a shared worker in the same-top-level-site iframe.
+ await send(iframe_2_uuid, create_worker_js(same_site_origin));
+ const worker_2_uuid = await receive(response_queue_uuid);
+
+ const cache_name = token();
+ await self.caches.open(cache_name);
+ t.add_cleanup(() => self.caches.delete(cache_name));
+
+ await send(worker_2_uuid,
+ cache_exists_js(cache_name, response_queue_uuid));
+ if (await receive(response_queue_uuid) !== "true") {
+ reject("Cache not visible in same-top-level-site worker");
+ }
+
+ await send(worker_1_uuid,
+ cache_exists_js(cache_name, response_queue_uuid));
+ if (await receive(response_queue_uuid) !== "false") {
+ reject("Cache visible in not-same-top-level-site worker");
+ }
+ resolve();
+ } catch (e) {
+ reject(e);
+ }
+ });
+}, "CacheStorage caches shouldn't be shared with a cross-partition shared worker");
+
+const newServiceWorker = async (origin) => {
+ const worker_token = token();
+ const worker_url = origin + executor_service_worker_path +
+ `&uuid=${worker_token}`;
+ const worker_url_path = executor_service_worker_path.substring(0,
+ executor_service_worker_path.lastIndexOf('/'));
+ const scope = worker_url_path + "/not-used/";
+ const reg = await navigator.serviceWorker.register(worker_url,
+ {'scope': scope});
+ return worker_token;
+}
+
+promise_test(t => {
+ return new Promise(async (resolve, reject) => {
+ try {
+ const response_queue_uuid = token();
+
+ const create_worker_js = (origin) => `
+ const importScript = ${importScript};
+ await importScript("/html/cross-origin-embedder-policy/credentialless" +
+ "/resources/common.js");
+ await importScript("/html/anonymous-iframe/resources/common.js");
+ await importScript("/common/utils.js");
+ const newServiceWorker = ${newServiceWorker};
+ await send("${response_queue_uuid}", await newServiceWorker("${origin}"));
+ `;
+
+ const [iframe_1_uuid, iframe_2_uuid] =
+ await create_test_iframes(t, response_queue_uuid);
+
+ // Create a service worker in the same-top-level-site iframe.
+ await send(iframe_2_uuid, create_worker_js(same_site_origin));
+ const worker_2_uuid = await receive(response_queue_uuid);
+
+ t.add_cleanup(() =>
+ send(worker_2_uuid, "self.registration.unregister();"));
+
+ const cache_name = token();
+ await self.caches.open(cache_name);
+ t.add_cleanup(() => self.caches.delete(cache_name));
+
+ await send(worker_2_uuid,
+ cache_exists_js(cache_name, response_queue_uuid));
+ if (await receive(response_queue_uuid) !== "true") {
+ reject("Cache not visible in same-top-level-site worker");
+ }
+
+ // Create a service worker in the cross-top-level-site iframe. Note that
+ // if service workers are unpartitioned then this new service worker would
+ // replace the one created above. This is why we wait to create the second
+ // service worker until after we are done with the first one.
+ await send(iframe_1_uuid, create_worker_js(same_site_origin));
+ const worker_1_uuid = await receive(response_queue_uuid);
+
+ t.add_cleanup(() =>
+ send(worker_1_uuid, "self.registration.unregister();"));
+
+ await send(worker_1_uuid,
+ cache_exists_js(cache_name, response_queue_uuid));
+ if (await receive(response_queue_uuid) !== "false") {
+ reject("Cache visible in not-same-top-level-site worker");
+ }
+
+ resolve();
+ } catch (e) {
+ reject(e);
+ }
+ });
+}, "CacheStorage caches shouldn't be shared with a cross-partition service worker");
+</script>
+</body>
diff --git a/test/wpt/tests/service-workers/cache-storage/resources/blank.html b/test/wpt/tests/service-workers/cache-storage/resources/blank.html
new file mode 100644
index 0000000..a3c3a46
--- /dev/null
+++ b/test/wpt/tests/service-workers/cache-storage/resources/blank.html
@@ -0,0 +1,2 @@
+<!DOCTYPE html>
+<title>Empty doc</title>
diff --git a/test/wpt/tests/service-workers/cache-storage/resources/cache-keys-attributes-for-service-worker.js b/test/wpt/tests/service-workers/cache-storage/resources/cache-keys-attributes-for-service-worker.js
new file mode 100644
index 0000000..ee574d2
--- /dev/null
+++ b/test/wpt/tests/service-workers/cache-storage/resources/cache-keys-attributes-for-service-worker.js
@@ -0,0 +1,22 @@
+self.addEventListener('fetch', (event) => {
+ const params = new URL(event.request.url).searchParams;
+ if (params.has('ignore')) {
+ return;
+ }
+ if (!params.has('name')) {
+ event.respondWith(Promise.reject(TypeError('No name is provided.')));
+ return;
+ }
+
+ event.respondWith(Promise.resolve().then(async () => {
+ const name = params.get('name');
+ await caches.delete('foo');
+ const cache = await caches.open('foo');
+ await cache.put(event.request, new Response('hello'));
+ const keys = await cache.keys();
+
+ const original = event.request[name];
+ const stored = keys[0][name];
+ return new Response(`original: ${original}, stored: ${stored}`);
+ }));
+ });
diff --git a/test/wpt/tests/service-workers/cache-storage/resources/common-worker.js b/test/wpt/tests/service-workers/cache-storage/resources/common-worker.js
new file mode 100644
index 0000000..d0e8544
--- /dev/null
+++ b/test/wpt/tests/service-workers/cache-storage/resources/common-worker.js
@@ -0,0 +1,15 @@
+self.onmessage = function(e) {
+ var cache_name = e.data.name;
+
+ self.caches.open(cache_name)
+ .then(function(cache) {
+ return Promise.all([
+ cache.put('https://example.com/a', new Response('a')),
+ cache.put('https://example.com/b', new Response('b')),
+ cache.put('https://example.com/c', new Response('c'))
+ ]);
+ })
+ .then(function() {
+ self.postMessage('ok');
+ });
+};
diff --git a/test/wpt/tests/service-workers/cache-storage/resources/credentials-iframe.html b/test/wpt/tests/service-workers/cache-storage/resources/credentials-iframe.html
new file mode 100644
index 0000000..00702df
--- /dev/null
+++ b/test/wpt/tests/service-workers/cache-storage/resources/credentials-iframe.html
@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Controlled frame for Cache API test with credentials</title>
+<script>
+
+function xhr(url, username, password) {
+ return new Promise(function(resolve, reject) {
+ var xhr = new XMLHttpRequest(), async = true;
+ xhr.open('GET', url, async, username, password);
+ xhr.send();
+ xhr.onreadystatechange = function() {
+ if (xhr.readyState !== XMLHttpRequest.DONE)
+ return;
+ if (xhr.status === 200) {
+ resolve(xhr.responseText);
+ } else {
+ reject(new Error(xhr.statusText));
+ }
+ };
+ });
+}
+
+window.onmessage = function(e) {
+ Promise.all(e.data.map(function(item) {
+ return xhr(item.name, item.username, item.password);
+ }))
+ .then(function() {
+ navigator.serviceWorker.controller.postMessage('keys');
+ navigator.serviceWorker.onmessage = function(e) {
+ window.parent.postMessage(e.data, '*');
+ };
+ });
+};
+
+</script>
+<body>
+Hello? Yes, this is iframe.
+</body>
diff --git a/test/wpt/tests/service-workers/cache-storage/resources/credentials-worker.js b/test/wpt/tests/service-workers/cache-storage/resources/credentials-worker.js
new file mode 100644
index 0000000..43965b5
--- /dev/null
+++ b/test/wpt/tests/service-workers/cache-storage/resources/credentials-worker.js
@@ -0,0 +1,59 @@
+var cache_name = 'credentials';
+
+function assert_equals(actual, expected, message) {
+ if (!Object.is(actual, expected))
+ throw Error(message + ': expected: ' + expected + ', actual: ' + actual);
+}
+
+self.onfetch = function(e) {
+ if (!/\.txt$/.test(e.request.url)) return;
+ var content = e.request.url;
+ var cache;
+ e.respondWith(
+ self.caches.open(cache_name)
+ .then(function(result) {
+ cache = result;
+ return cache.put(e.request, new Response(content));
+ })
+
+ .then(function() { return cache.match(e.request); })
+ .then(function(result) { return result.text(); })
+ .then(function(text) {
+ assert_equals(text, content, 'Cache.match() body should match');
+ })
+
+ .then(function() { return cache.matchAll(e.request); })
+ .then(function(results) {
+ assert_equals(results.length, 1, 'Should have one response');
+ return results[0].text();
+ })
+ .then(function(text) {
+ assert_equals(text, content, 'Cache.matchAll() body should match');
+ })
+
+ .then(function() { return self.caches.match(e.request); })
+ .then(function(result) { return result.text(); })
+ .then(function(text) {
+ assert_equals(text, content, 'CacheStorage.match() body should match');
+ })
+
+ .then(function() {
+ return new Response('dummy');
+ })
+ );
+};
+
+self.onmessage = function(e) {
+ if (e.data === 'keys') {
+ self.caches.open(cache_name)
+ .then(function(cache) { return cache.keys(); })
+ .then(function(requests) {
+ var urls = requests.map(function(request) { return request.url; });
+ self.clients.matchAll().then(function(clients) {
+ clients.forEach(function(client) {
+ client.postMessage(urls);
+ });
+ });
+ });
+ }
+};
diff --git a/test/wpt/tests/service-workers/cache-storage/resources/fetch-status.py b/test/wpt/tests/service-workers/cache-storage/resources/fetch-status.py
new file mode 100644
index 0000000..b7109f4
--- /dev/null
+++ b/test/wpt/tests/service-workers/cache-storage/resources/fetch-status.py
@@ -0,0 +1,2 @@
+def main(request, response):
+ return int(request.GET[b"status"]), [], b""
diff --git a/test/wpt/tests/service-workers/cache-storage/resources/iframe.html b/test/wpt/tests/service-workers/cache-storage/resources/iframe.html
new file mode 100644
index 0000000..a2f1e50
--- /dev/null
+++ b/test/wpt/tests/service-workers/cache-storage/resources/iframe.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<title>ok</title>
+<script>
+window.onmessage = function(e) {
+ var id = e.data.id;
+ try {
+ var name = 'checkallowed';
+ self.caches.open(name).then(function (cache) {
+ self.caches.delete(name);
+ window.parent.postMessage({id: id, result: 'allowed'}, '*');
+ }).catch(function(e) {
+ window.parent.postMessage({id: id, result: 'denied', name: e.name, message: e.message}, '*');
+ });
+ } catch (e) {
+ window.parent.postMessage({id: id, result: 'unexpecteddenied', name: e.name, message: e.message}, '*');
+ }
+};
+</script>
diff --git a/test/wpt/tests/service-workers/cache-storage/resources/simple.txt b/test/wpt/tests/service-workers/cache-storage/resources/simple.txt
new file mode 100644
index 0000000..9e3cb91
--- /dev/null
+++ b/test/wpt/tests/service-workers/cache-storage/resources/simple.txt
@@ -0,0 +1 @@
+a simple text file
diff --git a/test/wpt/tests/service-workers/cache-storage/resources/test-helpers.js b/test/wpt/tests/service-workers/cache-storage/resources/test-helpers.js
new file mode 100644
index 0000000..050ac0b
--- /dev/null
+++ b/test/wpt/tests/service-workers/cache-storage/resources/test-helpers.js
@@ -0,0 +1,272 @@
+(function() {
+ var next_cache_index = 1;
+
+ // Returns a promise that resolves to a newly created Cache object. The
+ // returned Cache will be destroyed when |test| completes.
+ function create_temporary_cache(test) {
+ var uniquifier = String(++next_cache_index);
+ var cache_name = self.location.pathname + '/' + uniquifier;
+
+ test.add_cleanup(function() {
+ self.caches.delete(cache_name);
+ });
+
+ return self.caches.delete(cache_name)
+ .then(function() {
+ return self.caches.open(cache_name);
+ });
+ }
+
+ self.create_temporary_cache = create_temporary_cache;
+})();
+
+// Runs |test_function| with a temporary unique Cache passed in as the only
+// argument. The function is run as a part of Promise chain owned by
+// promise_test(). As such, it is expected to behave in a manner identical (with
+// the exception of the argument) to a function passed into promise_test().
+//
+// E.g.:
+// cache_test(function(cache) {
+// // Do something with |cache|, which is a Cache object.
+// }, "Some Cache test");
+function cache_test(test_function, description) {
+ promise_test(function(test) {
+ return create_temporary_cache(test)
+ .then(function(cache) { return test_function(cache, test); });
+ }, description);
+}
+
+// A set of Request/Response pairs to be used with prepopulated_cache_test().
+var simple_entries = [
+ {
+ name: 'a',
+ request: new Request('http://example.com/a'),
+ response: new Response('')
+ },
+
+ {
+ name: 'b',
+ request: new Request('http://example.com/b'),
+ response: new Response('')
+ },
+
+ {
+ name: 'a_with_query',
+ request: new Request('http://example.com/a?q=r'),
+ response: new Response('')
+ },
+
+ {
+ name: 'A',
+ request: new Request('http://example.com/A'),
+ response: new Response('')
+ },
+
+ {
+ name: 'a_https',
+ request: new Request('https://example.com/a'),
+ response: new Response('')
+ },
+
+ {
+ name: 'a_org',
+ request: new Request('http://example.org/a'),
+ response: new Response('')
+ },
+
+ {
+ name: 'cat',
+ request: new Request('http://example.com/cat'),
+ response: new Response('')
+ },
+
+ {
+ name: 'catmandu',
+ request: new Request('http://example.com/catmandu'),
+ response: new Response('')
+ },
+
+ {
+ name: 'cat_num_lives',
+ request: new Request('http://example.com/cat?lives=9'),
+ response: new Response('')
+ },
+
+ {
+ name: 'cat_in_the_hat',
+ request: new Request('http://example.com/cat/in/the/hat'),
+ response: new Response('')
+ },
+
+ {
+ name: 'non_2xx_response',
+ request: new Request('http://example.com/non2xx'),
+ response: new Response('', {status: 404, statusText: 'nope'})
+ },
+
+ {
+ name: 'error_response',
+ request: new Request('http://example.com/error'),
+ response: Response.error()
+ },
+];
+
+// A set of Request/Response pairs to be used with prepopulated_cache_test().
+// These contain a mix of test cases that use Vary headers.
+var vary_entries = [
+ {
+ name: 'vary_cookie_is_cookie',
+ request: new Request('http://example.com/c',
+ {headers: {'Cookies': 'is-for-cookie'}}),
+ response: new Response('',
+ {headers: {'Vary': 'Cookies'}})
+ },
+
+ {
+ name: 'vary_cookie_is_good',
+ request: new Request('http://example.com/c',
+ {headers: {'Cookies': 'is-good-enough-for-me'}}),
+ response: new Response('',
+ {headers: {'Vary': 'Cookies'}})
+ },
+
+ {
+ name: 'vary_cookie_absent',
+ request: new Request('http://example.com/c'),
+ response: new Response('',
+ {headers: {'Vary': 'Cookies'}})
+ }
+];
+
+// Run |test_function| with a Cache object and a map of entries. Prior to the
+// call, the Cache is populated by cache entries from |entries|. The latter is
+// expected to be an Object mapping arbitrary keys to objects of the form
+// {request: <Request object>, response: <Response object>}. Entries are
+// serially added to the cache in the order specified.
+//
+// |test_function| should return a Promise that can be used with promise_test.
+function prepopulated_cache_test(entries, test_function, description) {
+ cache_test(function(cache) {
+ var p = Promise.resolve();
+ var hash = {};
+ entries.forEach(function(entry) {
+ hash[entry.name] = entry;
+ p = p.then(function() {
+ return cache.put(entry.request.clone(), entry.response.clone())
+ .catch(function(e) {
+ assert_unreached(
+ 'Test setup failed for entry ' + entry.name + ': ' + e
+ );
+ });
+ });
+ });
+ return p
+ .then(function() {
+ assert_equals(Object.keys(hash).length, entries.length);
+ })
+ .then(function() {
+ return test_function(cache, hash);
+ });
+ }, description);
+}
+
+// Helper for testing with Headers objects. Compares Headers instances
+// by serializing |expected| and |actual| to arrays and comparing.
+function assert_header_equals(actual, expected, description) {
+ assert_class_string(actual, "Headers", description);
+ var header;
+ var actual_headers = [];
+ var expected_headers = [];
+ for (header of actual)
+ actual_headers.push(header[0] + ": " + header[1]);
+ for (header of expected)
+ expected_headers.push(header[0] + ": " + header[1]);
+ assert_array_equals(actual_headers, expected_headers,
+ description + " Headers differ.");
+}
+
+// Helper for testing with Response objects. Compares simple
+// attributes defined on the interfaces, as well as the headers. It
+// does not compare the response bodies.
+function assert_response_equals(actual, expected, description) {
+ assert_class_string(actual, "Response", description);
+ ["type", "url", "status", "ok", "statusText"].forEach(function(attribute) {
+ assert_equals(actual[attribute], expected[attribute],
+ description + " Attributes differ: " + attribute + ".");
+ });
+ assert_header_equals(actual.headers, expected.headers, description);
+}
+
+// Assert that the two arrays |actual| and |expected| contain the same
+// set of Responses as determined by assert_response_equals. The order
+// is not significant.
+//
+// |expected| is assumed to not contain any duplicates.
+function assert_response_array_equivalent(actual, expected, description) {
+ assert_true(Array.isArray(actual), description);
+ assert_equals(actual.length, expected.length, description);
+ expected.forEach(function(expected_element) {
+ // assert_response_in_array treats the first argument as being
+ // 'actual', and the second as being 'expected array'. We are
+ // switching them around because we want to be resilient
+ // against the |actual| array containing duplicates.
+ assert_response_in_array(expected_element, actual, description);
+ });
+}
+
+// Asserts that two arrays |actual| and |expected| contain the same
+// set of Responses as determined by assert_response_equals(). The
+// corresponding elements must occupy corresponding indices in their
+// respective arrays.
+function assert_response_array_equals(actual, expected, description) {
+ assert_true(Array.isArray(actual), description);
+ assert_equals(actual.length, expected.length, description);
+ actual.forEach(function(value, index) {
+ assert_response_equals(value, expected[index],
+ description + " : object[" + index + "]");
+ });
+}
+
+// Equivalent to assert_in_array, but uses assert_response_equals.
+function assert_response_in_array(actual, expected_array, description) {
+ assert_true(expected_array.some(function(element) {
+ try {
+ assert_response_equals(actual, element);
+ return true;
+ } catch (e) {
+ return false;
+ }
+ }), description);
+}
+
+// Helper for testing with Request objects. Compares simple
+// attributes defined on the interfaces, as well as the headers.
+function assert_request_equals(actual, expected, description) {
+ assert_class_string(actual, "Request", description);
+ ["url"].forEach(function(attribute) {
+ assert_equals(actual[attribute], expected[attribute],
+ description + " Attributes differ: " + attribute + ".");
+ });
+ assert_header_equals(actual.headers, expected.headers, description);
+}
+
+// Asserts that two arrays |actual| and |expected| contain the same
+// set of Requests as determined by assert_request_equals(). The
+// corresponding elements must occupy corresponding indices in their
+// respective arrays.
+function assert_request_array_equals(actual, expected, description) {
+ assert_true(Array.isArray(actual), description);
+ assert_equals(actual.length, expected.length, description);
+ actual.forEach(function(value, index) {
+ assert_request_equals(value, expected[index],
+ description + " : object[" + index + "]");
+ });
+}
+
+// Deletes all caches, returning a promise indicating success.
+function delete_all_caches() {
+ return self.caches.keys()
+ .then(function(keys) {
+ return Promise.all(keys.map(self.caches.delete.bind(self.caches)));
+ });
+}
diff --git a/test/wpt/tests/service-workers/cache-storage/resources/vary.py b/test/wpt/tests/service-workers/cache-storage/resources/vary.py
new file mode 100644
index 0000000..7fde1b1
--- /dev/null
+++ b/test/wpt/tests/service-workers/cache-storage/resources/vary.py
@@ -0,0 +1,25 @@
+def main(request, response):
+ if b"clear-vary-value-override-cookie" in request.GET:
+ response.unset_cookie(b"vary-value-override")
+ return b"vary cookie cleared"
+
+ set_cookie_vary = request.GET.first(b"set-vary-value-override-cookie",
+ default=b"")
+ if set_cookie_vary:
+ response.set_cookie(b"vary-value-override", set_cookie_vary)
+ return b"vary cookie set"
+
+ # If there is a vary-value-override cookie set, then use its value
+ # for the VARY header no matter what the query string is set to. This
+ # override is necessary to test the case when two URLs are identical
+ # (including query), but differ by VARY header.
+ cookie_vary = request.cookies.get(b"vary-value-override")
+ if cookie_vary:
+ response.headers.set(b"vary", str(cookie_vary))
+ else:
+ # If there is no cookie, then use the query string value, if present.
+ query_vary = request.GET.first(b"vary", default=b"")
+ if query_vary:
+ response.headers.set(b"vary", query_vary)
+
+ return b"vary response"
diff --git a/test/wpt/tests/service-workers/cache-storage/sandboxed-iframes.https.html b/test/wpt/tests/service-workers/cache-storage/sandboxed-iframes.https.html
new file mode 100644
index 0000000..098fa89
--- /dev/null
+++ b/test/wpt/tests/service-workers/cache-storage/sandboxed-iframes.https.html
@@ -0,0 +1,66 @@
+<!DOCTYPE html>
+<title>Cache Storage: Verify access in sandboxed iframes</title>
+<link rel="help" href="https://w3c.github.io/ServiceWorker/#cache-storage">
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+
+function load_iframe(src, sandbox) {
+ return new Promise(function(resolve, reject) {
+ var iframe = document.createElement('iframe');
+ iframe.onload = function() { resolve(iframe); };
+
+ iframe.sandbox = sandbox;
+ iframe.src = src;
+
+ document.documentElement.appendChild(iframe);
+ });
+}
+
+function wait_for_message(id) {
+ return new Promise(function(resolve) {
+ self.addEventListener('message', function listener(e) {
+ if (e.data.id === id) {
+ resolve(e.data);
+ self.removeEventListener('message', listener);
+ }
+ });
+ });
+}
+
+var counter = 0;
+
+promise_test(function(t) {
+ return load_iframe('./resources/iframe.html',
+ 'allow-scripts allow-same-origin')
+ .then(function(iframe) {
+ var id = ++counter;
+ iframe.contentWindow.postMessage({id: id}, '*');
+ return wait_for_message(id);
+ })
+ .then(function(message) {
+ assert_equals(
+ message.result, 'allowed',
+ 'Access should be allowed if sandbox has allow-same-origin');
+ });
+}, 'Sandboxed iframe with allow-same-origin is allowed access');
+
+promise_test(function(t) {
+ return load_iframe('./resources/iframe.html',
+ 'allow-scripts')
+ .then(function(iframe) {
+ var id = ++counter;
+ iframe.contentWindow.postMessage({id: id}, '*');
+ return wait_for_message(id);
+ })
+ .then(function(message) {
+ assert_equals(
+ message.result, 'denied',
+ 'Access should be denied if sandbox lacks allow-same-origin');
+ assert_equals(message.name, 'SecurityError',
+ 'Failure should be a SecurityError');
+ });
+}, 'Sandboxed iframe without allow-same-origin is denied access');
+
+</script>
diff --git a/test/wpt/tests/service-workers/idlharness.https.any.js b/test/wpt/tests/service-workers/idlharness.https.any.js
new file mode 100644
index 0000000..8db5d4d
--- /dev/null
+++ b/test/wpt/tests/service-workers/idlharness.https.any.js
@@ -0,0 +1,53 @@
+// META: global=window,worker
+// META: script=/resources/WebIDLParser.js
+// META: script=/resources/idlharness.js
+// META: script=cache-storage/resources/test-helpers.js
+// META: script=service-worker/resources/test-helpers.sub.js
+// META: timeout=long
+
+// https://w3c.github.io/ServiceWorker
+
+idl_test(
+ ['service-workers'],
+ ['dom', 'html'],
+ async (idl_array, t) => {
+ self.cacheInstance = await create_temporary_cache(t);
+
+ idl_array.add_objects({
+ CacheStorage: ['caches'],
+ Cache: ['self.cacheInstance'],
+ ServiceWorkerContainer: ['navigator.serviceWorker']
+ });
+
+ // TODO: Add ServiceWorker and ServiceWorkerRegistration instances for the
+ // other worker scopes.
+ if (self.GLOBAL.isWindow()) {
+ idl_array.add_objects({
+ ServiceWorkerRegistration: ['registrationInstance'],
+ ServiceWorker: ['registrationInstance.installing']
+ });
+
+ const scope = 'service-worker/resources/scope/idlharness';
+ const registration = await service_worker_unregister_and_register(
+ t, 'service-worker/resources/empty-worker.js', scope);
+ t.add_cleanup(() => registration.unregister());
+
+ self.registrationInstance = registration;
+ } else if (self.ServiceWorkerGlobalScope) {
+ // self.ServiceWorkerGlobalScope should only be defined for the
+ // ServiceWorker scope, which allows us to detect and test the interfaces
+ // exposed only for ServiceWorker.
+ idl_array.add_objects({
+ Clients: ['clients'],
+ ExtendableEvent: ['new ExtendableEvent("type")'],
+ FetchEvent: ['new FetchEvent("type", { request: new Request("") })'],
+ ServiceWorkerGlobalScope: ['self'],
+ ServiceWorkerRegistration: ['registration'],
+ ServiceWorker: ['serviceWorker'],
+ // TODO: Test instances of Client and WindowClient, e.g.
+ // Client: ['self.clientInstance'],
+ // WindowClient: ['self.windowClientInstance']
+ });
+ }
+ }
+);
diff --git a/test/wpt/tests/service-workers/service-worker/Service-Worker-Allowed-header.https.html b/test/wpt/tests/service-workers/service-worker/Service-Worker-Allowed-header.https.html
new file mode 100644
index 0000000..6f44bb1
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/Service-Worker-Allowed-header.https.html
@@ -0,0 +1,88 @@
+<!DOCTYPE html>
+<title>Service Worker: Service-Worker-Allowed header</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+
+const host_info = get_host_info();
+
+// Returns a URL for a service worker script whose Service-Worker-Allowed
+// header value is set to |allowed_path|. If |origin| is specified, that origin
+// is used.
+function build_script_url(allowed_path, origin) {
+ const script = 'resources/empty-worker.js';
+ const url = origin ? `${origin}${base_path()}${script}` : script;
+ return `${url}?pipe=header(Service-Worker-Allowed,${allowed_path})`;
+}
+
+// register_test is a promise_test that registers a service worker.
+function register_test(script, scope, description) {
+ promise_test(async t => {
+ t.add_cleanup(() => {
+ return service_worker_unregister(t, scope);
+ });
+
+ const registration = await service_worker_unregister_and_register(
+ t, script, scope);
+ assert_true(registration instanceof ServiceWorkerRegistration, 'registered');
+ assert_equals(registration.scope, normalizeURL(scope));
+ }, description);
+}
+
+// register_fail_test is like register_test but expects a SecurityError.
+function register_fail_test(script, scope, description) {
+ promise_test(async t => {
+ t.add_cleanup(() => {
+ return service_worker_unregister(t, scope);
+ });
+
+ await service_worker_unregister(t, scope);
+ await promise_rejects_dom(t,
+ 'SecurityError',
+ navigator.serviceWorker.register(script, {scope}));
+ }, description);
+}
+
+register_test(
+ build_script_url('/allowed-path'),
+ '/allowed-path',
+ 'Registering within Service-Worker-Allowed path');
+
+register_test(
+ build_script_url(new URL('/allowed-path', document.location)),
+ '/allowed-path',
+ 'Registering within Service-Worker-Allowed path (absolute URL)');
+
+register_test(
+ build_script_url('../allowed-path-with-parent'),
+ 'allowed-path-with-parent',
+ 'Registering within Service-Worker-Allowed path with parent reference');
+
+register_fail_test(
+ build_script_url('../allowed-path'),
+ '/disallowed-path',
+ 'Registering outside Service-Worker-Allowed path'),
+
+register_fail_test(
+ build_script_url('../allowed-path-with-parent'),
+ '/allowed-path-with-parent',
+ 'Registering outside Service-Worker-Allowed path with parent reference');
+
+register_fail_test(
+ build_script_url(host_info.HTTPS_REMOTE_ORIGIN + '/'),
+ 'resources/this-scope-is-normally-allowed',
+ 'Service-Worker-Allowed is cross-origin to script, registering on a normally allowed scope');
+
+register_fail_test(
+ build_script_url(host_info.HTTPS_REMOTE_ORIGIN + '/'),
+ '/this-scope-is-normally-disallowed',
+ 'Service-Worker-Allowed is cross-origin to script, registering on a normally disallowed scope');
+
+register_fail_test(
+ build_script_url(host_info.HTTPS_REMOTE_ORIGIN + '/cross-origin/',
+ host_info.HTTPS_REMOTE_ORIGIN),
+ '/cross-origin/',
+ 'Service-Worker-Allowed is cross-origin to page, same-origin to script');
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/close.https.html b/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/close.https.html
new file mode 100644
index 0000000..3e3cc8b
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/close.https.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<title>ServiceWorkerGlobalScope: close operation</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/test-helpers.sub.js"></script>
+<script>
+
+service_worker_test(
+ 'resources/close-worker.js', 'ServiceWorkerGlobalScope: close operation');
+
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/extendable-message-event-constructor.https.html b/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/extendable-message-event-constructor.https.html
new file mode 100644
index 0000000..525245f
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/extendable-message-event-constructor.https.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<title>ServiceWorkerGlobalScope: ExtendableMessageEvent Constructor</title>
+<script src='/resources/testharness.js'></script>
+<script src='/resources/testharnessreport.js'></script>
+<script src='../resources/test-helpers.sub.js'></script>
+<script>
+service_worker_test(
+ 'resources/extendable-message-event-constructor-worker.js', document.title
+ );
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/extendable-message-event.https.html b/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/extendable-message-event.https.html
new file mode 100644
index 0000000..89efd7a
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/extendable-message-event.https.html
@@ -0,0 +1,226 @@
+<!DOCTYPE html>
+<title>ServiceWorkerGlobalScope: ExtendableMessageEvent</title>
+<script src='/resources/testharness.js'></script>
+<script src='/resources/testharnessreport.js'></script>
+<script src='../resources/test-helpers.sub.js'></script>
+<script src='./resources/extendable-message-event-utils.js'></script>
+<script>
+promise_test(function(t) {
+ var script = 'resources/extendable-message-event-worker.js';
+ var scope = 'resources/scope/extendable-message-event-from-toplevel';
+ var registration;
+
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(function(r) {
+ registration = r;
+ add_completion_callback(function() { registration.unregister(); });
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() {
+ var saw_message = new Promise(function(resolve) {
+ navigator.serviceWorker.onmessage =
+ function(event) { resolve(event.data); }
+ });
+ var channel = new MessageChannel;
+ registration.active.postMessage('', [channel.port1]);
+ return saw_message;
+ })
+ .then(function(results) {
+ var expected = {
+ constructor: { name: 'ExtendableMessageEvent' },
+ origin: location.origin,
+ lastEventId: '',
+ source: {
+ constructor: { name: 'WindowClient' },
+ frameType: 'top-level',
+ url: location.href,
+ visibilityState: 'visible',
+ focused: true
+ },
+ ports: [ { constructor: { name: 'MessagePort' } } ]
+ };
+ ExtendableMessageEventUtils.assert_equals(results, expected);
+ });
+ }, 'Post an extendable message from a top-level client');
+
+promise_test(function(t) {
+ var script = 'resources/extendable-message-event-worker.js';
+ var scope = 'resources/scope/extendable-message-event-from-nested';
+ var frame;
+
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(function(registration) {
+ add_completion_callback(function() { registration.unregister(); });
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() { return with_iframe(scope); })
+ .then(function(f) {
+ frame = f;
+ add_completion_callback(function() { frame.remove(); });
+ var saw_message = new Promise(function(resolve) {
+ frame.contentWindow.navigator.serviceWorker.onmessage =
+ function(event) { resolve(event.data); }
+ });
+ f.contentWindow.navigator.serviceWorker.controller.postMessage('');
+ return saw_message;
+ })
+ .then(function(results) {
+ var expected = {
+ constructor: { name: 'ExtendableMessageEvent' },
+ origin: location.origin,
+ lastEventId: '',
+ source: {
+ constructor: { name: 'WindowClient' },
+ url: frame.contentWindow.location.href,
+ frameType: 'nested',
+ visibilityState: 'visible',
+ focused: false
+ },
+ ports: []
+ };
+ ExtendableMessageEventUtils.assert_equals(results, expected);
+ });
+ }, 'Post an extendable message from a nested client');
+
+promise_test(function(t) {
+ var script = 'resources/extendable-message-event-loopback-worker.js';
+ var scope = 'resources/scope/extendable-message-event-loopback';
+ var registration;
+
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(function(r) {
+ registration = r;
+ add_completion_callback(function() { registration.unregister(); });
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() {
+ var results = [];
+ var saw_message = new Promise(function(resolve) {
+ navigator.serviceWorker.onmessage = function(event) {
+ switch (event.data.type) {
+ case 'record':
+ results.push(event.data.results);
+ break;
+ case 'finish':
+ resolve(results);
+ break;
+ }
+ };
+ });
+ registration.active.postMessage({type: 'start'});
+ return saw_message;
+ })
+ .then(function(results) {
+ assert_equals(results.length, 2);
+
+ var expected_trial_1 = {
+ constructor: { name: 'ExtendableMessageEvent' },
+ origin: location.origin,
+ lastEventId: '',
+ source: {
+ constructor: { name: 'ServiceWorker' },
+ scriptURL: normalizeURL(script),
+ state: 'activated'
+ },
+ ports: []
+ };
+ assert_equals(results[0].trial, 1);
+ ExtendableMessageEventUtils.assert_equals(
+ results[0].event, expected_trial_1
+ );
+
+ var expected_trial_2 = {
+ constructor: { name: 'ExtendableMessageEvent' },
+ origin: location.origin,
+ lastEventId: '',
+ source: {
+ constructor: { name: 'ServiceWorker' },
+ scriptURL: normalizeURL(script),
+ state: 'activated'
+ },
+ ports: [],
+ };
+ assert_equals(results[1].trial, 2);
+ ExtendableMessageEventUtils.assert_equals(
+ results[1].event, expected_trial_2
+ );
+ });
+ }, 'Post loopback extendable messages');
+
+promise_test(function(t) {
+ var script1 = 'resources/extendable-message-event-ping-worker.js';
+ var script2 = 'resources/extendable-message-event-pong-worker.js';
+ var scope = 'resources/scope/extendable-message-event-pingpong';
+ var registration;
+
+ return service_worker_unregister_and_register(t, script1, scope)
+ .then(function(r) {
+ registration = r;
+ add_completion_callback(function() { registration.unregister(); });
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() {
+ // A controlled frame is necessary for keeping a waiting worker.
+ return with_iframe(scope);
+ })
+ .then(function(frame) {
+ add_completion_callback(function() { frame.remove(); });
+ return navigator.serviceWorker.register(script2, {scope: scope});
+ })
+ .then(function(r) {
+ return wait_for_state(t, r.installing, 'installed');
+ })
+ .then(function() {
+ var results = [];
+ var saw_message = new Promise(function(resolve) {
+ navigator.serviceWorker.onmessage = function(event) {
+ switch (event.data.type) {
+ case 'record':
+ results.push(event.data.results);
+ break;
+ case 'finish':
+ resolve(results);
+ break;
+ }
+ };
+ });
+ registration.active.postMessage({type: 'start'});
+ return saw_message;
+ })
+ .then(function(results) {
+ assert_equals(results.length, 2);
+
+ var expected_ping = {
+ constructor: { name: 'ExtendableMessageEvent' },
+ origin: location.origin,
+ lastEventId: '',
+ source: {
+ constructor: { name: 'ServiceWorker' },
+ scriptURL: normalizeURL(script1),
+ state: 'activated'
+ },
+ ports: []
+ };
+ assert_equals(results[0].pingOrPong, 'ping');
+ ExtendableMessageEventUtils.assert_equals(
+ results[0].event, expected_ping
+ );
+
+ var expected_pong = {
+ constructor: { name: 'ExtendableMessageEvent' },
+ origin: location.origin,
+ lastEventId: '',
+ source: {
+ constructor: { name: 'ServiceWorker' },
+ scriptURL: normalizeURL(script2),
+ state: 'installed'
+ },
+ ports: []
+ };
+ assert_equals(results[1].pingOrPong, 'pong');
+ ExtendableMessageEventUtils.assert_equals(
+ results[1].event, expected_pong
+ );
+ });
+ }, 'Post extendable messages among service workers');
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/fetch-on-the-right-interface.https.any.js b/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/fetch-on-the-right-interface.https.any.js
new file mode 100644
index 0000000..5ca5f65
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/fetch-on-the-right-interface.https.any.js
@@ -0,0 +1,14 @@
+// META: title=fetch method on the right interface
+// META: global=serviceworker
+
+test(function() {
+ assert_false(self.hasOwnProperty('fetch'), 'ServiceWorkerGlobalScope ' +
+ 'instance should not have "fetch" method as its property.');
+ assert_inherits(self, 'fetch', 'ServiceWorkerGlobalScope should ' +
+ 'inherit "fetch" method.');
+ assert_own_property(Object.getPrototypeOf(Object.getPrototypeOf(self)), 'fetch',
+ 'WorkerGlobalScope should have "fetch" propery in its prototype.');
+ assert_equals(self.fetch, Object.getPrototypeOf(Object.getPrototypeOf(self)).fetch,
+ 'ServiceWorkerGlobalScope.fetch should be the same as ' +
+ 'WorkerGlobalScope.fetch.');
+}, 'Fetch method on the right interface');
diff --git a/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/isSecureContext.https.html b/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/isSecureContext.https.html
new file mode 100644
index 0000000..399820d
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/isSecureContext.https.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Service Worker: isSecureContext</title>
+</head>
+<body>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/test-helpers.sub.js"></script>
+<script>
+'use strict';
+
+promise_test(async (t) => {
+ var url = 'isSecureContext.serviceworker.js';
+ var scope = 'empty.html';
+ var frame_sw, sw_registration;
+
+ await service_worker_unregister(t, scope);
+ var f = await with_iframe(scope);
+ t.add_cleanup(function() {
+ f.remove();
+ });
+ frame_sw = f.contentWindow.navigator.serviceWorker;
+ var registration = await navigator.serviceWorker.register(url, {scope: scope});
+ sw_registration = registration;
+ await wait_for_state(t, registration.installing, 'activated');
+ fetch_tests_from_worker(sw_registration.active);
+}, 'Setting up tests');
+
+</script>
+</body>
+</html>
diff --git a/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/isSecureContext.serviceworker.js b/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/isSecureContext.serviceworker.js
new file mode 100644
index 0000000..5033594
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/isSecureContext.serviceworker.js
@@ -0,0 +1,5 @@
+importScripts("/resources/testharness.js");
+
+test(() => {
+ assert_true(self.isSecureContext, true);
+}, "isSecureContext");
diff --git a/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/postmessage.https.html b/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/postmessage.https.html
new file mode 100644
index 0000000..99dedeb
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/postmessage.https.html
@@ -0,0 +1,83 @@
+<!DOCTYPE html>
+<title>ServiceWorkerGlobalScope: postMessage</title>
+<script src='/resources/testharness.js'></script>
+<script src='/resources/testharnessreport.js'></script>
+<script src='../resources/test-helpers.sub.js'></script>
+<script>
+
+promise_test(function(t) {
+ var script = 'resources/postmessage-loopback-worker.js';
+ var scope = 'resources/scope/postmessage-loopback';
+ var registration;
+
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(function(r) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ registration = r;
+
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() {
+ var channel = new MessageChannel();
+ var saw_message = new Promise(function(resolve) {
+ channel.port1.onmessage = function(event) {
+ resolve(event.data);
+ };
+ });
+ registration.active.postMessage({port: channel.port2},
+ [channel.port2]);
+ return saw_message;
+ })
+ .then(function(result) {
+ assert_equals(result, 'OK');
+ });
+ }, 'Post loopback messages');
+
+promise_test(function(t) {
+ var script1 = 'resources/postmessage-ping-worker.js';
+ var script2 = 'resources/postmessage-pong-worker.js';
+ var scope = 'resources/scope/postmessage-pingpong';
+ var registration;
+ var frame;
+
+ return service_worker_unregister_and_register(t, script1, scope)
+ .then(function(r) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ registration = r;
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() {
+ // A controlled frame is necessary for keeping a waiting worker.
+ return with_iframe(scope);
+ })
+ .then(function(f) {
+ frame = f;
+ return navigator.serviceWorker.register(script2, {scope: scope});
+ })
+ .then(function(r) {
+ return wait_for_state(t, r.installing, 'installed');
+ })
+ .then(function() {
+ var channel = new MessageChannel();
+ var saw_message = new Promise(function(resolve) {
+ channel.port1.onmessage = function(event) {
+ resolve(event.data);
+ };
+ });
+ registration.active.postMessage({port: channel.port2},
+ [channel.port2]);
+ return saw_message;
+ })
+ .then(function(result) {
+ assert_equals(result, 'OK');
+ frame.remove();
+ });
+ }, 'Post messages among service workers');
+
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/registration-attribute.https.html b/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/registration-attribute.https.html
new file mode 100644
index 0000000..aa3c74a
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/registration-attribute.https.html
@@ -0,0 +1,107 @@
+<!DOCTYPE html>
+<title>ServiceWorkerGlobalScope: registration</title>
+<script src='/resources/testharness.js'></script>
+<script src='/resources/testharnessreport.js'></script>
+<script src='../resources/test-helpers.sub.js'></script>
+<script>
+
+promise_test(function(t) {
+ var script = 'resources/registration-attribute-worker.js';
+ var scope = 'resources/scope/registration-attribute';
+ var registration;
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(function(reg) {
+ registration = reg;
+ add_result_callback(function() { registration.unregister(); });
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() { return with_iframe(scope); })
+ .then(function(frame) {
+ var expected_events_seen = [
+ 'updatefound',
+ 'install',
+ 'statechange(installed)',
+ 'statechange(activating)',
+ 'activate',
+ 'statechange(activated)',
+ 'fetch',
+ ];
+
+ assert_equals(
+ frame.contentDocument.body.textContent,
+ expected_events_seen.toString(),
+ 'Service Worker should respond to fetch');
+ frame.remove();
+ return registration.unregister();
+ });
+ }, 'Verify registration attributes on ServiceWorkerGlobalScope');
+
+promise_test(function(t) {
+ var script = 'resources/registration-attribute-worker.js';
+ var newer_script = 'resources/registration-attribute-newer-worker.js';
+ var scope = 'resources/scope/registration-attribute';
+ var newer_worker;
+ var registration;
+
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(function(reg) {
+ registration = reg;
+ add_result_callback(function() { registration.unregister(); });
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() {
+ return navigator.serviceWorker.register(newer_script, {scope: scope});
+ })
+ .then(function(reg) {
+ assert_equals(reg, registration);
+ newer_worker = registration.installing;
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() {
+ var channel = new MessageChannel;
+ var saw_message = new Promise(function(resolve) {
+ channel.port1.onmessage = function(e) { resolve(e.data); };
+ });
+ newer_worker.postMessage({port: channel.port2}, [channel.port2]);
+ return saw_message;
+ })
+ .then(function(results) {
+ var script_url = normalizeURL(script);
+ var newer_script_url = normalizeURL(newer_script);
+ var expectations = [
+ 'evaluate',
+ ' installing: empty',
+ ' waiting: empty',
+ ' active: ' + script_url,
+ 'updatefound',
+ ' installing: ' + newer_script_url,
+ ' waiting: empty',
+ ' active: ' + script_url,
+ 'install',
+ ' installing: ' + newer_script_url,
+ ' waiting: empty',
+ ' active: ' + script_url,
+ 'statechange(installed)',
+ ' installing: empty',
+ ' waiting: ' + newer_script_url,
+ ' active: ' + script_url,
+ 'statechange(activating)',
+ ' installing: empty',
+ ' waiting: empty',
+ ' active: ' + newer_script_url,
+ 'activate',
+ ' installing: empty',
+ ' waiting: empty',
+ ' active: ' + newer_script_url,
+ 'statechange(activated)',
+ ' installing: empty',
+ ' waiting: empty',
+ ' active: ' + newer_script_url,
+ ];
+ assert_array_equals(results, expectations);
+ return registration.unregister();
+ });
+ }, 'Verify registration attributes on ServiceWorkerGlobalScope of the ' +
+ 'newer worker');
+
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/close-worker.js b/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/close-worker.js
new file mode 100644
index 0000000..41a8bc0
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/close-worker.js
@@ -0,0 +1,5 @@
+importScripts('../../resources/worker-testharness.js');
+
+test(function() {
+ assert_false('close' in self);
+}, 'ServiceWorkerGlobalScope close operation');
diff --git a/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/error-worker.js b/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/error-worker.js
new file mode 100644
index 0000000..f6838ff
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/error-worker.js
@@ -0,0 +1,12 @@
+var source;
+
+self.addEventListener('message', function(e) {
+ source = e.source;
+ throw 'testError';
+});
+
+self.addEventListener('error', function(e) {
+ source.postMessage({
+ error: e.error, filename: e.filename, message: e.message, lineno: e.lineno,
+ colno: e.colno});
+});
diff --git a/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-constructor-worker.js b/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-constructor-worker.js
new file mode 100644
index 0000000..42da582
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-constructor-worker.js
@@ -0,0 +1,197 @@
+importScripts('/resources/testharness.js');
+
+const TEST_OBJECT = { wanwan: 123 };
+const CHANNEL1 = new MessageChannel();
+const CHANNEL2 = new MessageChannel();
+const PORTS = [CHANNEL1.port1, CHANNEL1.port2, CHANNEL2.port1];
+function createEvent(initializer) {
+ if (initializer === undefined)
+ return new ExtendableMessageEvent('type');
+ return new ExtendableMessageEvent('type', initializer);
+}
+
+// These test cases are mostly copied from the following file in the Chromium
+// project (as of commit 848ad70823991e0f12b437d789943a4ab24d65bb):
+// third_party/WebKit/LayoutTests/fast/events/constructors/message-event-constructor.html
+
+test(function() {
+ assert_false(createEvent().bubbles);
+ assert_false(createEvent().cancelable);
+ assert_equals(createEvent().data, null);
+ assert_equals(createEvent().origin, '');
+ assert_equals(createEvent().lastEventId, '');
+ assert_equals(createEvent().source, null);
+ assert_array_equals(createEvent().ports, []);
+}, 'no initializer specified');
+
+test(function() {
+ assert_false(createEvent({ bubbles: false }).bubbles);
+ assert_true(createEvent({ bubbles: true }).bubbles);
+}, '`bubbles` is specified');
+
+test(function() {
+ assert_false(createEvent({ cancelable: false }).cancelable);
+ assert_true(createEvent({ cancelable: true }).cancelable);
+}, '`cancelable` is specified');
+
+test(function() {
+ assert_equals(createEvent({ data: TEST_OBJECT }).data, TEST_OBJECT);
+ assert_equals(createEvent({ data: undefined }).data, null);
+ assert_equals(createEvent({ data: null }).data, null);
+ assert_equals(createEvent({ data: false }).data, false);
+ assert_equals(createEvent({ data: true }).data, true);
+ assert_equals(createEvent({ data: '' }).data, '');
+ assert_equals(createEvent({ data: 'chocolate' }).data, 'chocolate');
+ assert_equals(createEvent({ data: 12345 }).data, 12345);
+ assert_equals(createEvent({ data: 18446744073709551615 }).data,
+ 18446744073709552000);
+ assert_equals(createEvent({ data: NaN }).data, NaN);
+ // Note that valueOf() is not called, when the left hand side is
+ // evaluated.
+ assert_false(
+ createEvent({ data: {
+ valueOf: function() { return TEST_OBJECT; } } }).data ==
+ TEST_OBJECT);
+ assert_equals(createEvent({ get data(){ return 123; } }).data, 123);
+ let thrown = { name: 'Error' };
+ assert_throws_exactly(thrown, function() {
+ createEvent({ get data() { throw thrown; } }); });
+}, '`data` is specified');
+
+test(function() {
+ assert_equals(createEvent({ origin: 'melancholy' }).origin, 'melancholy');
+ assert_equals(createEvent({ origin: '' }).origin, '');
+ assert_equals(createEvent({ origin: null }).origin, 'null');
+ assert_equals(createEvent({ origin: false }).origin, 'false');
+ assert_equals(createEvent({ origin: true }).origin, 'true');
+ assert_equals(createEvent({ origin: 12345 }).origin, '12345');
+ assert_equals(
+ createEvent({ origin: 18446744073709551615 }).origin,
+ '18446744073709552000');
+ assert_equals(createEvent({ origin: NaN }).origin, 'NaN');
+ assert_equals(createEvent({ origin: [] }).origin, '');
+ assert_equals(createEvent({ origin: [1, 2, 3] }).origin, '1,2,3');
+ assert_equals(
+ createEvent({ origin: { melancholy: 12345 } }).origin,
+ '[object Object]');
+ // Note that valueOf() is not called, when the left hand side is
+ // evaluated.
+ assert_equals(
+ createEvent({ origin: {
+ valueOf: function() { return 'melancholy'; } } }).origin,
+ '[object Object]');
+ assert_equals(
+ createEvent({ get origin() { return 123; } }).origin, '123');
+ let thrown = { name: 'Error' };
+ assert_throws_exactly(thrown, function() {
+ createEvent({ get origin() { throw thrown; } }); });
+}, '`origin` is specified');
+
+test(function() {
+ assert_equals(
+ createEvent({ lastEventId: 'melancholy' }).lastEventId, 'melancholy');
+ assert_equals(createEvent({ lastEventId: '' }).lastEventId, '');
+ assert_equals(createEvent({ lastEventId: null }).lastEventId, 'null');
+ assert_equals(createEvent({ lastEventId: false }).lastEventId, 'false');
+ assert_equals(createEvent({ lastEventId: true }).lastEventId, 'true');
+ assert_equals(createEvent({ lastEventId: 12345 }).lastEventId, '12345');
+ assert_equals(
+ createEvent({ lastEventId: 18446744073709551615 }).lastEventId,
+ '18446744073709552000');
+ assert_equals(createEvent({ lastEventId: NaN }).lastEventId, 'NaN');
+ assert_equals(createEvent({ lastEventId: [] }).lastEventId, '');
+ assert_equals(
+ createEvent({ lastEventId: [1, 2, 3] }).lastEventId, '1,2,3');
+ assert_equals(
+ createEvent({ lastEventId: { melancholy: 12345 } }).lastEventId,
+ '[object Object]');
+ // Note that valueOf() is not called, when the left hand side is
+ // evaluated.
+ assert_equals(
+ createEvent({ lastEventId: {
+ valueOf: function() { return 'melancholy'; } } }).lastEventId,
+ '[object Object]');
+ assert_equals(
+ createEvent({ get lastEventId() { return 123; } }).lastEventId,
+ '123');
+ let thrown = { name: 'Error' };
+ assert_throws_exactly(thrown, function() {
+ createEvent({ get lastEventId() { throw thrown; } }); });
+}, '`lastEventId` is specified');
+
+test(function() {
+ assert_equals(createEvent({ source: CHANNEL1.port1 }).source, CHANNEL1.port1);
+ assert_equals(
+ createEvent({ source: self.registration.active }).source,
+ self.registration.active);
+ assert_equals(
+ createEvent({ source: CHANNEL1.port1 }).source, CHANNEL1.port1);
+ assert_throws_js(
+ TypeError, function() { createEvent({ source: this }); },
+ 'source should be Client or ServiceWorker or MessagePort');
+}, '`source` is specified');
+
+test(function() {
+ // Valid message ports.
+ var passed_ports = createEvent({ ports: PORTS}).ports;
+ assert_equals(passed_ports[0], CHANNEL1.port1);
+ assert_equals(passed_ports[1], CHANNEL1.port2);
+ assert_equals(passed_ports[2], CHANNEL2.port1);
+ assert_array_equals(createEvent({ ports: [] }).ports, []);
+ assert_array_equals(createEvent({ ports: undefined }).ports, []);
+
+ // Invalid message ports.
+ assert_throws_js(TypeError,
+ function() { createEvent({ ports: [1, 2, 3] }); });
+ assert_throws_js(TypeError,
+ function() { createEvent({ ports: TEST_OBJECT }); });
+ assert_throws_js(TypeError,
+ function() { createEvent({ ports: null }); });
+ assert_throws_js(TypeError,
+ function() { createEvent({ ports: this }); });
+ assert_throws_js(TypeError,
+ function() { createEvent({ ports: false }); });
+ assert_throws_js(TypeError,
+ function() { createEvent({ ports: true }); });
+ assert_throws_js(TypeError,
+ function() { createEvent({ ports: '' }); });
+ assert_throws_js(TypeError,
+ function() { createEvent({ ports: 'chocolate' }); });
+ assert_throws_js(TypeError,
+ function() { createEvent({ ports: 12345 }); });
+ assert_throws_js(TypeError,
+ function() { createEvent({ ports: 18446744073709551615 }); });
+ assert_throws_js(TypeError,
+ function() { createEvent({ ports: NaN }); });
+ assert_throws_js(TypeError,
+ function() { createEvent({ get ports() { return 123; } }); });
+ let thrown = { name: 'Error' };
+ assert_throws_exactly(thrown, function() {
+ createEvent({ get ports() { throw thrown; } }); });
+ // Note that valueOf() is not called, when the left hand side is
+ // evaluated.
+ var valueOf = function() { return PORTS; };
+ assert_throws_js(TypeError, function() {
+ createEvent({ ports: { valueOf: valueOf } }); });
+}, '`ports` is specified');
+
+test(function() {
+ var initializers = {
+ bubbles: true,
+ cancelable: true,
+ data: TEST_OBJECT,
+ origin: 'wonderful',
+ lastEventId: 'excellent',
+ source: CHANNEL1.port1,
+ ports: PORTS
+ };
+ assert_equals(createEvent(initializers).bubbles, true);
+ assert_equals(createEvent(initializers).cancelable, true);
+ assert_equals(createEvent(initializers).data, TEST_OBJECT);
+ assert_equals(createEvent(initializers).origin, 'wonderful');
+ assert_equals(createEvent(initializers).lastEventId, 'excellent');
+ assert_equals(createEvent(initializers).source, CHANNEL1.port1);
+ assert_equals(createEvent(initializers).ports[0], PORTS[0]);
+ assert_equals(createEvent(initializers).ports[1], PORTS[1]);
+ assert_equals(createEvent(initializers).ports[2], PORTS[2]);
+}, 'all initial values are specified');
diff --git a/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-loopback-worker.js b/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-loopback-worker.js
new file mode 100644
index 0000000..13cae88
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-loopback-worker.js
@@ -0,0 +1,36 @@
+importScripts('./extendable-message-event-utils.js');
+
+self.addEventListener('message', function(event) {
+ switch (event.data.type) {
+ case 'start':
+ self.registration.active.postMessage(
+ {type: '1st', client_id: event.source.id});
+ break;
+ case '1st':
+ // 1st loopback message via ServiceWorkerRegistration.active.
+ var results = {
+ trial: 1,
+ event: ExtendableMessageEventUtils.serialize(event)
+ };
+ var client_id = event.data.client_id;
+ event.source.postMessage({type: '2nd', client_id: client_id});
+ event.waitUntil(clients.get(client_id)
+ .then(function(client) {
+ client.postMessage({type: 'record', results: results});
+ }));
+ break;
+ case '2nd':
+ // 2nd loopback message via ExtendableMessageEvent.source.
+ var results = {
+ trial: 2,
+ event: ExtendableMessageEventUtils.serialize(event)
+ };
+ var client_id = event.data.client_id;
+ event.waitUntil(clients.get(client_id)
+ .then(function(client) {
+ client.postMessage({type: 'record', results: results});
+ client.postMessage({type: 'finish'});
+ }));
+ break;
+ }
+ });
diff --git a/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-ping-worker.js b/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-ping-worker.js
new file mode 100644
index 0000000..d07b229
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-ping-worker.js
@@ -0,0 +1,23 @@
+importScripts('./extendable-message-event-utils.js');
+
+self.addEventListener('message', function(event) {
+ switch (event.data.type) {
+ case 'start':
+ // Send a ping message to another service worker.
+ self.registration.waiting.postMessage(
+ {type: 'ping', client_id: event.source.id});
+ break;
+ case 'pong':
+ var results = {
+ pingOrPong: 'pong',
+ event: ExtendableMessageEventUtils.serialize(event)
+ };
+ var client_id = event.data.client_id;
+ event.waitUntil(clients.get(client_id)
+ .then(function(client) {
+ client.postMessage({type: 'record', results: results});
+ client.postMessage({type: 'finish'});
+ }));
+ break;
+ }
+ });
diff --git a/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-pong-worker.js b/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-pong-worker.js
new file mode 100644
index 0000000..5e9669e
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-pong-worker.js
@@ -0,0 +1,18 @@
+importScripts('./extendable-message-event-utils.js');
+
+self.addEventListener('message', function(event) {
+ switch (event.data.type) {
+ case 'ping':
+ var results = {
+ pingOrPong: 'ping',
+ event: ExtendableMessageEventUtils.serialize(event)
+ };
+ var client_id = event.data.client_id;
+ event.waitUntil(clients.get(client_id)
+ .then(function(client) {
+ client.postMessage({type: 'record', results: results});
+ event.source.postMessage({type: 'pong', client_id: client_id});
+ }));
+ break;
+ }
+ });
diff --git a/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-utils.js b/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-utils.js
new file mode 100644
index 0000000..d6a3b48
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-utils.js
@@ -0,0 +1,78 @@
+var ExtendableMessageEventUtils = {};
+
+// Create a representation of a given ExtendableMessageEvent that is suitable
+// for transmission via the `postMessage` API.
+ExtendableMessageEventUtils.serialize = function(event) {
+ var ports = event.ports.map(function(port) {
+ return { constructor: { name: port.constructor.name } };
+ });
+ return {
+ constructor: {
+ name: event.constructor.name
+ },
+ origin: event.origin,
+ lastEventId: event.lastEventId,
+ source: {
+ constructor: {
+ name: event.source.constructor.name
+ },
+ url: event.source.url,
+ frameType: event.source.frameType,
+ visibilityState: event.source.visibilityState,
+ focused: event.source.focused
+ },
+ ports: ports
+ };
+};
+
+// Compare the actual and expected values of an ExtendableMessageEvent that has
+// been transformed using the `serialize` function defined in this file.
+ExtendableMessageEventUtils.assert_equals = function(actual, expected) {
+ assert_equals(
+ actual.constructor.name, expected.constructor.name, 'event constructor'
+ );
+ assert_equals(actual.origin, expected.origin, 'event `origin` property');
+ assert_equals(
+ actual.lastEventId,
+ expected.lastEventId,
+ 'event `lastEventId` property'
+ );
+
+ assert_equals(
+ actual.source.constructor.name,
+ expected.source.constructor.name,
+ 'event `source` property constructor'
+ );
+ assert_equals(
+ actual.source.url, expected.source.url, 'event `source` property `url`'
+ );
+ assert_equals(
+ actual.source.frameType,
+ expected.source.frameType,
+ 'event `source` property `frameType`'
+ );
+ assert_equals(
+ actual.source.visibilityState,
+ expected.source.visibilityState,
+ 'event `source` property `visibilityState`'
+ );
+ assert_equals(
+ actual.source.focused,
+ expected.source.focused,
+ 'event `source` property `focused`'
+ );
+
+ assert_equals(
+ actual.ports.length,
+ expected.ports.length,
+ 'event `ports` property length'
+ );
+
+ for (var idx = 0; idx < expected.ports.length; ++idx) {
+ assert_equals(
+ actual.ports[idx].constructor.name,
+ expected.ports[idx].constructor.name,
+ 'MessagePort #' + idx + ' constructor'
+ );
+ }
+};
diff --git a/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-worker.js b/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-worker.js
new file mode 100644
index 0000000..f5e7647
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-worker.js
@@ -0,0 +1,5 @@
+importScripts('./extendable-message-event-utils.js');
+
+self.addEventListener('message', function(event) {
+ event.source.postMessage(ExtendableMessageEventUtils.serialize(event));
+ });
diff --git a/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/postmessage-loopback-worker.js b/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/postmessage-loopback-worker.js
new file mode 100644
index 0000000..083e9aa
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/postmessage-loopback-worker.js
@@ -0,0 +1,15 @@
+self.addEventListener('message', function(event) {
+ if ('port' in event.data) {
+ var port = event.data.port;
+
+ var channel = new MessageChannel();
+ channel.port1.onmessage = function(event) {
+ if ('pong' in event.data)
+ port.postMessage(event.data.pong);
+ };
+ self.registration.active.postMessage({ping: channel.port2},
+ [channel.port2]);
+ } else if ('ping' in event.data) {
+ event.data.ping.postMessage({pong: 'OK'});
+ }
+ });
diff --git a/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/postmessage-ping-worker.js b/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/postmessage-ping-worker.js
new file mode 100644
index 0000000..ebb1ecc
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/postmessage-ping-worker.js
@@ -0,0 +1,15 @@
+self.addEventListener('message', function(event) {
+ if ('port' in event.data) {
+ var port = event.data.port;
+
+ var channel = new MessageChannel();
+ channel.port1.onmessage = function(event) {
+ if ('pong' in event.data)
+ port.postMessage(event.data.pong);
+ };
+
+ // Send a ping message to another service worker.
+ self.registration.waiting.postMessage({ping: channel.port2},
+ [channel.port2]);
+ }
+ });
diff --git a/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/postmessage-pong-worker.js b/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/postmessage-pong-worker.js
new file mode 100644
index 0000000..4a0d90b
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/postmessage-pong-worker.js
@@ -0,0 +1,4 @@
+self.addEventListener('message', function(event) {
+ if ('ping' in event.data)
+ event.data.ping.postMessage({pong: 'OK'});
+ });
diff --git a/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/registration-attribute-newer-worker.js b/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/registration-attribute-newer-worker.js
new file mode 100644
index 0000000..44f3e2e
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/registration-attribute-newer-worker.js
@@ -0,0 +1,33 @@
+// TODO(nhiroki): stop using global states because service workers can be killed
+// at any point. Instead, we could post a message to the page on each event via
+// Client object (http://crbug.com/558244).
+var results = [];
+
+function stringify(worker) {
+ return worker ? worker.scriptURL : 'empty';
+}
+
+function record(event_name) {
+ results.push(event_name);
+ results.push(' installing: ' + stringify(self.registration.installing));
+ results.push(' waiting: ' + stringify(self.registration.waiting));
+ results.push(' active: ' + stringify(self.registration.active));
+}
+
+record('evaluate');
+
+self.registration.addEventListener('updatefound', function() {
+ record('updatefound');
+ var worker = self.registration.installing;
+ self.registration.installing.addEventListener('statechange', function() {
+ record('statechange(' + worker.state + ')');
+ });
+ });
+
+self.addEventListener('install', function(e) { record('install'); });
+
+self.addEventListener('activate', function(e) { record('activate'); });
+
+self.addEventListener('message', function(e) {
+ e.data.port.postMessage(results);
+ });
diff --git a/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/registration-attribute-worker.js b/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/registration-attribute-worker.js
new file mode 100644
index 0000000..315f437
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/registration-attribute-worker.js
@@ -0,0 +1,139 @@
+importScripts('../../resources/test-helpers.sub.js');
+importScripts('../../resources/worker-testharness.js');
+
+// TODO(nhiroki): stop using global states because service workers can be killed
+// at any point. Instead, we could post a message to the page on each event via
+// Client object (http://crbug.com/558244).
+var events_seen = [];
+
+// TODO(nhiroki): Move these assertions to registration-attribute.html because
+// an assertion failure on the worker is not shown on the result page and
+// handled as timeout. See registration-attribute-newer-worker.js for example.
+
+assert_equals(
+ self.registration.scope,
+ normalizeURL('scope/registration-attribute'),
+ 'On worker script evaluation, registration attribute should be set');
+assert_equals(
+ self.registration.installing,
+ null,
+ 'On worker script evaluation, installing worker should be null');
+assert_equals(
+ self.registration.waiting,
+ null,
+ 'On worker script evaluation, waiting worker should be null');
+assert_equals(
+ self.registration.active,
+ null,
+ 'On worker script evaluation, active worker should be null');
+
+self.registration.addEventListener('updatefound', function() {
+ events_seen.push('updatefound');
+
+ assert_equals(
+ self.registration.scope,
+ normalizeURL('scope/registration-attribute'),
+ 'On updatefound event, registration attribute should be set');
+ assert_equals(
+ self.registration.installing.scriptURL,
+ normalizeURL('registration-attribute-worker.js'),
+ 'On updatefound event, installing worker should be set');
+ assert_equals(
+ self.registration.waiting,
+ null,
+ 'On updatefound event, waiting worker should be null');
+ assert_equals(
+ self.registration.active,
+ null,
+ 'On updatefound event, active worker should be null');
+
+ assert_equals(
+ self.registration.installing.state,
+ 'installing',
+ 'On updatefound event, worker should be in the installing state');
+
+ var worker = self.registration.installing;
+ self.registration.installing.addEventListener('statechange', function() {
+ events_seen.push('statechange(' + worker.state + ')');
+ });
+ });
+
+self.addEventListener('install', function(e) {
+ events_seen.push('install');
+
+ assert_equals(
+ self.registration.scope,
+ normalizeURL('scope/registration-attribute'),
+ 'On install event, registration attribute should be set');
+ assert_equals(
+ self.registration.installing.scriptURL,
+ normalizeURL('registration-attribute-worker.js'),
+ 'On install event, installing worker should be set');
+ assert_equals(
+ self.registration.waiting,
+ null,
+ 'On install event, waiting worker should be null');
+ assert_equals(
+ self.registration.active,
+ null,
+ 'On install event, active worker should be null');
+
+ assert_equals(
+ self.registration.installing.state,
+ 'installing',
+ 'On install event, worker should be in the installing state');
+ });
+
+self.addEventListener('activate', function(e) {
+ events_seen.push('activate');
+
+ assert_equals(
+ self.registration.scope,
+ normalizeURL('scope/registration-attribute'),
+ 'On activate event, registration attribute should be set');
+ assert_equals(
+ self.registration.installing,
+ null,
+ 'On activate event, installing worker should be null');
+ assert_equals(
+ self.registration.waiting,
+ null,
+ 'On activate event, waiting worker should be null');
+ assert_equals(
+ self.registration.active.scriptURL,
+ normalizeURL('registration-attribute-worker.js'),
+ 'On activate event, active worker should be set');
+
+ assert_equals(
+ self.registration.active.state,
+ 'activating',
+ 'On activate event, worker should be in the activating state');
+ });
+
+self.addEventListener('fetch', function(e) {
+ events_seen.push('fetch');
+
+ assert_equals(
+ self.registration.scope,
+ normalizeURL('scope/registration-attribute'),
+ 'On fetch event, registration attribute should be set');
+ assert_equals(
+ self.registration.installing,
+ null,
+ 'On fetch event, installing worker should be null');
+ assert_equals(
+ self.registration.waiting,
+ null,
+ 'On fetch event, waiting worker should be null');
+ assert_equals(
+ self.registration.active.scriptURL,
+ normalizeURL('registration-attribute-worker.js'),
+ 'On fetch event, active worker should be set');
+
+ assert_equals(
+ self.registration.active.state,
+ 'activated',
+ 'On fetch event, worker should be in the activated state');
+
+ e.respondWith(new Response(events_seen));
+ });
diff --git a/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/unregister-controlling-worker.html b/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/unregister-controlling-worker.html
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/unregister-controlling-worker.html
diff --git a/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/unregister-worker.js b/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/unregister-worker.js
new file mode 100644
index 0000000..6da397d
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/unregister-worker.js
@@ -0,0 +1,25 @@
+function matchQuery(query) {
+ return self.location.href.indexOf(query) != -1;
+}
+
+if (matchQuery('?evaluation'))
+ self.registration.unregister();
+
+self.addEventListener('install', function(e) {
+ if (matchQuery('?install')) {
+ // Don't do waitUntil(unregister()) as that would deadlock as specified.
+ self.registration.unregister();
+ }
+ });
+
+self.addEventListener('activate', function(e) {
+ if (matchQuery('?activate'))
+ e.waitUntil(self.registration.unregister());
+ });
+
+self.addEventListener('message', function(e) {
+ e.waitUntil(self.registration.unregister()
+ .then(function(result) {
+ e.data.port.postMessage({result: result});
+ }));
+ });
diff --git a/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/update-worker.js b/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/update-worker.js
new file mode 100644
index 0000000..8be8a1f
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/update-worker.js
@@ -0,0 +1,22 @@
+var events_seen = [];
+
+self.registration.addEventListener('updatefound', function() {
+ events_seen.push('updatefound');
+ });
+
+self.addEventListener('activate', function(e) {
+ events_seen.push('activate');
+ });
+
+self.addEventListener('fetch', function(e) {
+ events_seen.push('fetch');
+ e.respondWith(new Response(events_seen));
+ });
+
+self.addEventListener('message', function(e) {
+ events_seen.push('message');
+ self.registration.update();
+ });
+
+// update() during the script evaluation should be ignored.
+self.registration.update();
diff --git a/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/update-worker.py b/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/update-worker.py
new file mode 100644
index 0000000..8a87e1b
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/update-worker.py
@@ -0,0 +1,16 @@
+import os
+import time
+
+from wptserve.utils import isomorphic_decode
+
+def main(request, response):
+ # update() does not bypass cache so set the max-age to 0 such that update()
+ # can find a new version in the network.
+ headers = [(b'Cache-Control', b'max-age: 0'),
+ (b'Content-Type', b'application/javascript')]
+ with open(os.path.join(os.path.dirname(isomorphic_decode(__file__)),
+ u'update-worker.js'), u'r') as file:
+ script = file.read()
+ # Return a different script for each access.
+ return headers, u'// %s\n%s' % (time.time(), script)
+
diff --git a/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/service-worker-error-event.https.html b/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/service-worker-error-event.https.html
new file mode 100644
index 0000000..988f546
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/service-worker-error-event.https.html
@@ -0,0 +1,31 @@
+<!DOCTYPE html>
+<title>ServiceWorkerGlobalScope: Error event error message</title>
+<script src='/resources/testharness.js'></script>
+<script src='/resources/testharnessreport.js'></script>
+<script src='../resources/test-helpers.sub.js'></script>
+<script>
+promise_test(t => {
+ var script = 'resources/error-worker.js';
+ var scope = 'resources/scope/service-worker-error-event';
+ var error_name = 'testError'
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(registration => {
+ var worker = registration.installing;
+ add_completion_callback(() => { registration.unregister(); });
+ return new Promise(function(resolve) {
+ navigator.serviceWorker.onmessage = resolve;
+ worker.postMessage('');
+ });
+ })
+ .then(e => {
+ assert_equals(e.data.error, error_name, 'error type');
+ assert_greater_than(
+ e.data.message.indexOf(error_name), -1, 'error message');
+ assert_greater_than(
+ e.data.filename.indexOf(script), -1, 'filename');
+ assert_equals(e.data.lineno, 5, 'error line number');
+ assert_equals(e.data.colno, 3, 'error column number');
+ });
+ }, 'Error handlers inside serviceworker should see the attributes of ' +
+ 'ErrorEvent');
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/unregister.https.html b/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/unregister.https.html
new file mode 100644
index 0000000..1a124d7
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/unregister.https.html
@@ -0,0 +1,139 @@
+<!DOCTYPE html>
+<title>ServiceWorkerGlobalScope: unregister</title>
+<script src='/resources/testharness.js'></script>
+<script src='/resources/testharnessreport.js'></script>
+<script src='../resources/test-helpers.sub.js'></script>
+<script>
+
+promise_test(function(t) {
+ var script = 'resources/unregister-worker.js?evaluation';
+ var scope = 'resources/scope/unregister-on-script-evaluation';
+
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(function(registration) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ return wait_for_state(t, registration.installing, 'redundant');
+ })
+ .then(function() {
+ return navigator.serviceWorker.getRegistration(scope);
+ })
+ .then(function(result) {
+ assert_equals(
+ result,
+ undefined,
+ 'After unregister(), the registration should not found');
+ });
+ }, 'Unregister on script evaluation');
+
+promise_test(function(t) {
+ var script = 'resources/unregister-worker.js?install';
+ var scope = 'resources/scope/unregister-on-install-event';
+
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(function(registration) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ return wait_for_state(t, registration.installing, 'redundant');
+ })
+ .then(function() {
+ return navigator.serviceWorker.getRegistration(scope);
+ })
+ .then(function(result) {
+ assert_equals(
+ result,
+ undefined,
+ 'After unregister(), the registration should not found');
+ });
+ }, 'Unregister on installing event');
+
+promise_test(function(t) {
+ var script = 'resources/unregister-worker.js?activate';
+ var scope = 'resources/scope/unregister-on-activate-event';
+
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(function(registration) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ return wait_for_state(t, registration.installing, 'redundant');
+ })
+ .then(function() {
+ return navigator.serviceWorker.getRegistration(scope);
+ })
+ .then(function(result) {
+ assert_equals(
+ result,
+ undefined,
+ 'After unregister(), the registration should not found');
+ });
+ }, 'Unregister on activate event');
+
+promise_test(function(t) {
+ var script = 'resources/unregister-worker.js';
+ var scope = 'resources/unregister-controlling-worker.html';
+
+ var controller;
+ var frame;
+
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(function(registration) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() { return with_iframe(scope); })
+ .then(function(f) {
+ frame = f;
+ controller = frame.contentWindow.navigator.serviceWorker.controller;
+
+ assert_equals(
+ controller.scriptURL,
+ normalizeURL(script),
+ 'Service worker should control a new document')
+
+ // Wait for the completion of unregister() on the worker.
+ var channel = new MessageChannel();
+ var promise = new Promise(function(resolve) {
+ channel.port1.onmessage = t.step_func(function(e) {
+ assert_true(e.data.result,
+ 'unregister() should successfully finish');
+ resolve();
+ });
+ });
+ controller.postMessage({port: channel.port2}, [channel.port2]);
+ return promise;
+ })
+ .then(function() {
+ return navigator.serviceWorker.getRegistration(scope);
+ })
+ .then(function(result) {
+ assert_equals(
+ result,
+ undefined,
+ 'After unregister(), the registration should not found');
+ assert_equals(
+ frame.contentWindow.navigator.serviceWorker.controller,
+ controller,
+ 'After unregister(), the worker should still control the document');
+ return with_iframe(scope);
+ })
+ .then(function(new_frame) {
+ assert_equals(
+ new_frame.contentWindow.navigator.serviceWorker.controller,
+ null,
+ 'After unregister(), the worker should not control a new document');
+
+ frame.remove();
+ new_frame.remove();
+ })
+ }, 'Unregister controlling service worker');
+
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/update.https.html b/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/update.https.html
new file mode 100644
index 0000000..a7dde22
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/update.https.html
@@ -0,0 +1,48 @@
+<!DOCTYPE html>
+<title>ServiceWorkerGlobalScope: update</title>
+<script src='/resources/testharness.js'></script>
+<script src='/resources/testharnessreport.js'></script>
+<script src='../resources/test-helpers.sub.js'></script>
+<script>
+
+promise_test(function(t) {
+ var script = 'resources/update-worker.py';
+ var scope = 'resources/scope/update';
+ var registration;
+ var frame1;
+
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(function(r) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ registration = r;
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() { return with_iframe(scope); })
+ .then(function(f) {
+ frame1 = f;
+ registration.active.postMessage('update');
+ return wait_for_update(t, registration);
+ })
+ .then(function() { return with_iframe(scope); })
+ .then(function(frame2) {
+ var expected_events_seen = [
+ 'updatefound', // by register().
+ 'activate',
+ 'fetch',
+ 'message',
+ 'updatefound', // by update() in the message handler.
+ 'fetch',
+ ];
+ assert_equals(
+ frame2.contentDocument.body.textContent,
+ expected_events_seen.toString(),
+ 'events seen by the worker');
+ frame1.remove();
+ frame2.remove();
+ });
+ }, 'Update a registration on ServiceWorkerGlobalScope');
+
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/about-blank-replacement.https.html b/test/wpt/tests/service-workers/service-worker/about-blank-replacement.https.html
new file mode 100644
index 0000000..b6efe3e
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/about-blank-replacement.https.html
@@ -0,0 +1,181 @@
+<!DOCTYPE html>
+<title>Service Worker: about:blank replacement handling</title>
+<meta name=timeout content=long>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+// This test attempts to verify various initial about:blank document
+// creation is accurately reflected via the Clients API. The goal is
+// for Clients API to reflect what the browser actually does and not
+// to make special cases for the API.
+//
+// If your browser does not create an about:blank document in certain
+// cases then please just mark the test expected fail for now. The
+// reuse of globals from about:blank documents to the final load document
+// has particularly bad interop at the moment. Hopefully we can evolve
+// tests like this to eventually align browsers.
+
+const worker = 'resources/about-blank-replacement-worker.js';
+
+// Helper routine that creates an iframe that internally has some kind
+// of nested window. The nested window could be another iframe or
+// it could be a popup window.
+function createFrameWithNestedWindow(url) {
+ return new Promise((resolve, reject) => {
+ let frame = document.createElement('iframe');
+ frame.src = url;
+ document.body.appendChild(frame);
+
+ window.addEventListener('message', function onMsg(evt) {
+ if (evt.data.type !== 'NESTED_LOADED') {
+ return;
+ }
+ window.removeEventListener('message', onMsg);
+ if (evt.data.result && evt.data.result.startsWith('failure:')) {
+ reject(evt.data.result);
+ return;
+ }
+ resolve(frame);
+ });
+ });
+}
+
+// Helper routine to request the given worker find the client with
+// the specified URL using the clients.matchAll() API.
+function getClientIdByURL(worker, url) {
+ return new Promise(resolve => {
+ navigator.serviceWorker.addEventListener('message', function onMsg(evt) {
+ if (evt.data.type !== 'GET_CLIENT_ID') {
+ return;
+ }
+ navigator.serviceWorker.removeEventListener('message', onMsg);
+ resolve(evt.data.result);
+ });
+ worker.postMessage({ type: 'GET_CLIENT_ID', url: url.toString() });
+ });
+}
+
+async function doAsyncTest(t, scope) {
+ let reg = await service_worker_unregister_and_register(t, worker, scope);
+
+ t.add_cleanup(() => service_worker_unregister(t, scope));
+
+ await wait_for_state(t, reg.installing, 'activated');
+
+ // Load the scope as a frame. We expect this in turn to have a nested
+ // iframe. The service worker will intercept the load of the nested
+ // iframe and populate its body with the client ID of the initial
+ // about:blank document it sees via clients.matchAll().
+ let frame = await createFrameWithNestedWindow(scope);
+ let initialResult = frame.contentWindow.nested().document.body.textContent;
+ assert_false(initialResult.startsWith('failure:'), `result: ${initialResult}`);
+
+ assert_equals(frame.contentWindow.navigator.serviceWorker.controller.scriptURL,
+ frame.contentWindow.nested().navigator.serviceWorker.controller.scriptURL,
+ 'nested about:blank should have same controlling service worker');
+
+ // Next, ask the service worker to find the final client ID for the fully
+ // loaded nested frame.
+ let nestedURL = new URL(frame.contentWindow.nested().location);
+ let finalResult = await getClientIdByURL(reg.active, nestedURL);
+ assert_false(finalResult.startsWith('failure:'), `result: ${finalResult}`);
+
+ // If the nested frame doesn't have a URL to load, then there is no fetch
+ // event and the body should be empty. We can't verify the final client ID
+ // against anything.
+ if (nestedURL.href === 'about:blank' ||
+ nestedURL.href === 'about:srcdoc') {
+ assert_equals('', initialResult, 'about:blank text content should be blank');
+ }
+
+ // If the nested URL is not about:blank, though, then the fetch event handler
+ // should have populated the body with the client id of the initial about:blank.
+ // Verify the final client id matches.
+ else {
+ assert_equals(initialResult, finalResult, 'client ID values should match');
+ }
+
+ frame.remove();
+}
+
+promise_test(async function(t) {
+ // Execute a test where the nested frame is simply loaded normally.
+ await doAsyncTest(t, 'resources/about-blank-replacement-frame.py');
+}, 'Initial about:blank is controlled, exposed to clients.matchAll(), and ' +
+ 'matches final Client.');
+
+promise_test(async function(t) {
+ // Execute a test where the nested frame is modified immediately by
+ // its parent. In this case we add a message listener so the service
+ // worker can ping the client to verify its existence. This ping-pong
+ // check is performed during the initial load and when verifying the
+ // final loaded client.
+ await doAsyncTest(t, 'resources/about-blank-replacement-ping-frame.py');
+}, 'Initial about:blank modified by parent is controlled, exposed to ' +
+ 'clients.matchAll(), and matches final Client.');
+
+promise_test(async function(t) {
+ // Execute a test where the nested window is a popup window instead of
+ // an iframe. This should behave the same as the simple iframe case.
+ await doAsyncTest(t, 'resources/about-blank-replacement-popup-frame.py');
+}, 'Popup initial about:blank is controlled, exposed to clients.matchAll(), and ' +
+ 'matches final Client.');
+
+promise_test(async function(t) {
+ const scope = 'resources/about-blank-replacement-uncontrolled-nested-frame.html';
+
+ let reg = await service_worker_unregister_and_register(t, worker, scope);
+
+ t.add_cleanup(() => service_worker_unregister(t, scope));
+
+ await wait_for_state(t, reg.installing, 'activated');
+
+ // Load the scope as a frame. We expect this in turn to have a nested
+ // iframe. Unlike the other tests in this file the nested iframe URL
+ // is not covered by a service worker scope. It should end up as
+ // uncontrolled even though its initial about:blank is controlled.
+ let frame = await createFrameWithNestedWindow(scope);
+ let nested = frame.contentWindow.nested();
+ let initialResult = nested.document.body.textContent;
+
+ // The nested iframe should not have been intercepted by the service
+ // worker. The empty.html nested frame has "hello world" for its body.
+ assert_equals(initialResult.trim(), 'hello world', `result: ${initialResult}`);
+
+ assert_not_equals(frame.contentWindow.navigator.serviceWorker.controller, null,
+ 'outer frame should be controlled');
+
+ assert_equals(nested.navigator.serviceWorker.controller, null,
+ 'nested frame should not be controlled');
+
+ frame.remove();
+}, 'Initial about:blank is controlled, exposed to clients.matchAll(), and ' +
+ 'final Client is not controlled by a service worker.');
+
+promise_test(async function(t) {
+ // Execute a test where the nested frame is an iframe without a src
+ // attribute. This simple nested about:blank should still inherit the
+ // controller and be visible to clients.matchAll().
+ await doAsyncTest(t, 'resources/about-blank-replacement-blank-nested-frame.html');
+}, 'Simple about:blank is controlled and is exposed to clients.matchAll().');
+
+promise_test(async function(t) {
+ // Execute a test where the nested frame is an iframe using a non-empty
+ // srcdoc containing only a tag pair so its textContent is still empty.
+ // This nested iframe should still inherit the controller and be visible
+ // to clients.matchAll().
+ await doAsyncTest(t, 'resources/about-blank-replacement-srcdoc-nested-frame.html');
+}, 'Nested about:srcdoc is controlled and is exposed to clients.matchAll().');
+
+promise_test(async function(t) {
+ // Execute a test where the nested frame is dynamically added without a src
+ // attribute. This simple nested about:blank should still inherit the
+ // controller and be visible to clients.matchAll().
+ await doAsyncTest(t, 'resources/about-blank-replacement-blank-dynamic-nested-frame.html');
+}, 'Dynamic about:blank is controlled and is exposed to clients.matchAll().');
+
+</script>
+</body>
diff --git a/test/wpt/tests/service-workers/service-worker/activate-event-after-install-state-change.https.html b/test/wpt/tests/service-workers/service-worker/activate-event-after-install-state-change.https.html
new file mode 100644
index 0000000..016a52c
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/activate-event-after-install-state-change.https.html
@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<title>Service Worker: registration events</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+promise_test(function(t) {
+ var script = 'resources/empty-worker.js';
+ var scope = 'resources/blank.html';
+ var registration;
+
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(function(registration) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ var sw = registration.installing;
+
+ return new Promise(t.step_func(function(resolve) {
+ sw.onstatechange = t.step_func(function() {
+ if (sw.state === 'installed') {
+ assert_equals(registration.active, null,
+ 'installed event should be fired before activating service worker');
+ resolve();
+ }
+ });
+ }));
+ })
+ .catch(unreached_rejection(t));
+ }, 'installed event should be fired before activating service worker');
+
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/activation-after-registration.https.html b/test/wpt/tests/service-workers/service-worker/activation-after-registration.https.html
new file mode 100644
index 0000000..29f97e3
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/activation-after-registration.https.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<title>Service Worker: Activation occurs after registration</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+promise_test(function(t) {
+ var scope = 'resources/blank.html';
+ var registration;
+
+ return service_worker_unregister_and_register(
+ t, 'resources/empty-worker.js', scope)
+ .then(function(r) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ registration = r;
+ assert_equals(
+ r.installing.state,
+ 'installing',
+ 'worker should be in the "installing" state upon registration');
+ return wait_for_state(t, r.installing, 'activated');
+ });
+}, 'activation occurs after registration');
+</script>
+</body>
diff --git a/test/wpt/tests/service-workers/service-worker/activation.https.html b/test/wpt/tests/service-workers/service-worker/activation.https.html
new file mode 100644
index 0000000..278454d
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/activation.https.html
@@ -0,0 +1,168 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<meta name="timeout" content="long">
+<title>service worker: activation</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+// Returns {registration, iframe}, where |registration| has an active and
+// waiting worker. The active worker controls |iframe| and has an inflight
+// message event that can be finished by calling
+// |registration.active.postMessage('go')|.
+function setup_activation_test(t, scope, worker_url) {
+ var registration;
+ var iframe;
+ return navigator.serviceWorker.getRegistration(scope)
+ .then(r => {
+ if (r)
+ return r.unregister();
+ })
+ .then(() => {
+ // Create an in-scope iframe. Do this prior to registration to avoid
+ // racing between an update triggered by navigation and the update()
+ // call below.
+ return with_iframe(scope);
+ })
+ .then(f => {
+ iframe = f;
+ // Create an active worker.
+ return navigator.serviceWorker.register(worker_url, { scope: scope });
+ })
+ .then(r => {
+ registration = r;
+ add_result_callback(() => registration.unregister());
+ return wait_for_state(t, r.installing, 'activated');
+ })
+ .then(() => {
+ // Check that the frame was claimed.
+ assert_not_equals(
+ iframe.contentWindow.navigator.serviceWorker.controller, null);
+ // Create an in-flight request.
+ registration.active.postMessage('wait');
+ // Now there is both a controllee and an in-flight request.
+ // Initiate an update.
+ return registration.update();
+ })
+ .then(() => {
+ // Wait for a waiting worker.
+ return wait_for_state(t, registration.installing, 'installed');
+ })
+ .then(() => {
+ return wait_for_activation_on_sample_scope(t, self);
+ })
+ .then(() => {
+ assert_not_equals(registration.waiting, null);
+ assert_not_equals(registration.active, null);
+ return Promise.resolve({registration: registration, iframe: iframe});
+ });
+}
+promise_test(t => {
+ var scope = 'resources/no-controllee';
+ var worker_url = 'resources/mint-new-worker.py';
+ var registration;
+ var iframe;
+ var new_worker;
+ return setup_activation_test(t, scope, worker_url)
+ .then(result => {
+ registration = result.registration;
+ iframe = result.iframe;
+ // Finish the in-flight request.
+ registration.active.postMessage('go');
+ return wait_for_activation_on_sample_scope(t, self);
+ })
+ .then(() => {
+ // The new worker is still waiting. Remove the frame and it should
+ // activate.
+ new_worker = registration.waiting;
+ assert_equals(new_worker.state, 'installed');
+ var reached_active = wait_for_state(t, new_worker, 'activating');
+ iframe.remove();
+ return reached_active;
+ })
+ .then(() => {
+ assert_equals(new_worker, registration.active);
+ });
+ }, 'loss of controllees triggers activation');
+promise_test(t => {
+ var scope = 'resources/no-request';
+ var worker_url = 'resources/mint-new-worker.py';
+ var registration;
+ var iframe;
+ var new_worker;
+ return setup_activation_test(t, scope, worker_url)
+ .then(result => {
+ registration = result.registration;
+ iframe = result.iframe;
+ // Remove the iframe.
+ iframe.remove();
+ return new Promise(resolve => setTimeout(resolve, 0));
+ })
+ .then(() => {
+ // Finish the request.
+ new_worker = registration.waiting;
+ var reached_active = wait_for_state(t, new_worker, 'activating');
+ registration.active.postMessage('go');
+ return reached_active;
+ })
+ .then(() => {
+ assert_equals(registration.active, new_worker);
+ });
+ }, 'finishing a request triggers activation');
+promise_test(t => {
+ var scope = 'resources/skip-waiting';
+ var worker_url = 'resources/mint-new-worker.py?skip-waiting';
+ var registration;
+ var iframe;
+ var new_worker;
+ return setup_activation_test(t, scope, worker_url)
+ .then(result => {
+ registration = result.registration;
+ iframe = result.iframe;
+ // Finish the request. The iframe does not need to be removed because
+ // skipWaiting() was called.
+ new_worker = registration.waiting;
+ var reached_active = wait_for_state(t, new_worker, 'activating');
+ registration.active.postMessage('go');
+ return reached_active;
+ })
+ .then(() => {
+ assert_equals(registration.active, new_worker);
+ // Remove the iframe.
+ iframe.remove();
+ });
+ }, 'skipWaiting bypasses no controllee requirement');
+
+// This test is not really about activation, but otherwise is very
+// similar to the other tests here.
+promise_test(t => {
+ var scope = 'resources/unregister';
+ var worker_url = 'resources/mint-new-worker.py';
+ var registration;
+ var iframe;
+ var new_worker;
+ return setup_activation_test(t, scope, worker_url)
+ .then(result => {
+ registration = result.registration;
+ iframe = result.iframe;
+ // Remove the iframe.
+ iframe.remove();
+ return registration.unregister();
+ })
+ .then(() => {
+ // The unregister operation should wait for the active worker to
+ // finish processing its events before clearing the registration.
+ new_worker = registration.waiting;
+ var reached_redundant = wait_for_state(t, new_worker, 'redundant');
+ registration.active.postMessage('go');
+ return reached_redundant;
+ })
+ .then(() => {
+ assert_equals(registration.installing, null);
+ assert_equals(registration.waiting, null);
+ assert_equals(registration.active, null);
+ });
+ }, 'finishing a request triggers unregister');
+</script>
+</body>
diff --git a/test/wpt/tests/service-workers/service-worker/active.https.html b/test/wpt/tests/service-workers/service-worker/active.https.html
new file mode 100644
index 0000000..350a34b
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/active.https.html
@@ -0,0 +1,50 @@
+<!DOCTYPE html>
+<title>ServiceWorker: navigator.serviceWorker.active</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+
+const SCRIPT = 'resources/empty-worker.js';
+const SCOPE = 'resources/blank.html';
+
+// "active" is set
+promise_test(async t => {
+
+ t.add_cleanup(async() => {
+ if (frame)
+ frame.remove();
+ if (registration)
+ await registration.unregister();
+ });
+
+ await service_worker_unregister(t, SCOPE);
+ const frame = await with_iframe(SCOPE);
+ const registration =
+ await navigator.serviceWorker.register(SCRIPT, {scope: SCOPE});
+ await wait_for_state(t, registration.installing, 'activating');
+ const container = frame.contentWindow.navigator.serviceWorker;
+ assert_equals(container.controller, null, 'controller');
+ assert_equals(registration.active.state, 'activating',
+ 'registration.active');
+ assert_equals(registration.waiting, null, 'registration.waiting');
+ assert_equals(registration.installing, null, 'registration.installing');
+
+ // FIXME: Add a test for a frame created after installation.
+ // Should the existing frame ("frame") block activation?
+}, 'active is set');
+
+// Tests that the ServiceWorker objects returned from active attribute getter
+// that represent the same service worker are the same objects.
+promise_test(async t => {
+ const registration1 =
+ await service_worker_unregister_and_register(t, SCRIPT, SCOPE);
+ const registration2 = await navigator.serviceWorker.getRegistration(SCOPE);
+ assert_equals(registration1.active, registration2.active,
+ 'ServiceWorkerRegistration.active should return the same ' +
+ 'object');
+ await registration1.unregister();
+}, 'The ServiceWorker objects returned from active attribute getter that ' +
+ 'represent the same service worker are the same objects');
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/claim-affect-other-registration.https.html b/test/wpt/tests/service-workers/service-worker/claim-affect-other-registration.https.html
new file mode 100644
index 0000000..52555ac
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/claim-affect-other-registration.https.html
@@ -0,0 +1,136 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Service Worker: claim() should affect other registration</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+
+promise_test(function(t) {
+ var frame;
+
+ var init_scope = 'resources/blank.html?affect-other';
+ var init_worker_url = 'resources/empty.js';
+ var init_registration;
+ var init_workers = [undefined, undefined];
+
+ var claim_scope = 'resources/blank.html?affect-other-registration';
+ var claim_worker_url = 'resources/claim-worker.js';
+ var claim_registration;
+ var claim_worker;
+
+ return Promise.resolve()
+ // Register the first service worker to init_scope.
+ .then(() => navigator.serviceWorker.register(init_worker_url + '?v1',
+ {scope: init_scope}))
+ .then(r => {
+ init_registration = r;
+ init_workers[0] = r.installing;
+ return Promise.resolve()
+ .then(() => wait_for_state(t, init_workers[0], 'activated'))
+ .then(() => assert_array_equals([init_registration.active,
+ init_registration.waiting,
+ init_registration.installing],
+ [init_workers[0],
+ null,
+ null],
+ 'Wrong workers.'));
+ })
+
+ // Create an iframe as the client of the first service worker of init_scope.
+ .then(() => with_iframe(claim_scope))
+ .then(f => frame = f)
+
+ // Check the controller.
+ .then(() => frame.contentWindow.navigator.serviceWorker.getRegistration(
+ normalizeURL(init_scope)))
+ .then(r => assert_equals(frame.contentWindow.navigator.serviceWorker.controller,
+ r.active,
+ '.controller should belong to init_scope.'))
+
+ // Register the second service worker to init_scope.
+ .then(() => navigator.serviceWorker.register(init_worker_url + '?v2',
+ {scope: init_scope}))
+ .then(r => {
+ assert_equals(r, init_registration, 'Should be the same registration');
+ init_workers[1] = r.installing;
+ return Promise.resolve()
+ .then(() => wait_for_state(t, init_workers[1], 'installed'))
+ .then(() => assert_array_equals([init_registration.active,
+ init_registration.waiting,
+ init_registration.installing],
+ [init_workers[0],
+ init_workers[1],
+ null],
+ 'Wrong workers.'));
+ })
+
+ // Register a service worker to claim_scope.
+ .then(() => navigator.serviceWorker.register(claim_worker_url,
+ {scope: claim_scope}))
+ .then(r => {
+ claim_registration = r;
+ claim_worker = r.installing;
+ return wait_for_state(t, claim_worker, 'activated')
+ })
+
+ // Let claim_worker claim the created iframe.
+ .then(function() {
+ var channel = new MessageChannel();
+ var saw_message = new Promise(function(resolve) {
+ channel.port1.onmessage = t.step_func(function(e) {
+ assert_equals(e.data, 'PASS',
+ 'Worker call to claim() should fulfill.');
+ resolve();
+ });
+ });
+
+ claim_worker.postMessage({port: channel.port2}, [channel.port2]);
+ return saw_message;
+ })
+
+ // Check the controller.
+ .then(() => frame.contentWindow.navigator.serviceWorker.getRegistration(
+ normalizeURL(claim_scope)))
+ .then(r => assert_equals(frame.contentWindow.navigator.serviceWorker.controller,
+ r.active,
+ '.controller should belong to claim_scope.'))
+
+ // Check the status of created registrations and service workers.
+ .then(() => wait_for_state(t, init_workers[1], 'activated'))
+ .then(() => {
+ assert_array_equals([claim_registration.active,
+ claim_registration.waiting,
+ claim_registration.installing],
+ [claim_worker,
+ null,
+ null],
+ 'claim_worker should be the only worker.')
+
+ assert_array_equals([init_registration.active,
+ init_registration.waiting,
+ init_registration.installing],
+ [init_workers[1],
+ null,
+ null],
+ 'The waiting sw should become the active worker.')
+
+ assert_array_equals([init_workers[0].state,
+ init_workers[1].state,
+ claim_worker.state],
+ ['redundant',
+ 'activated',
+ 'activated'],
+ 'Wrong worker states.');
+ })
+
+ // Cleanup and finish testing.
+ .then(() => frame.remove())
+ .then(() => Promise.all([
+ init_registration.unregister(),
+ claim_registration.unregister()
+ ]))
+ .then(() => t.done());
+}, 'claim() should affect the originally controlling registration.');
+
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/claim-fetch.https.html b/test/wpt/tests/service-workers/service-worker/claim-fetch.https.html
new file mode 100644
index 0000000..ae0082d
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/claim-fetch.https.html
@@ -0,0 +1,90 @@
+<!doctype html>
+<meta charset=utf-8>
+<title></title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+
+async function tryFetch(fetchFunc, path) {
+ let response;
+ try {
+ response = await fetchFunc(path);
+ } catch (err) {
+ throw (`fetch() threw: ${err}`);
+ }
+
+ let responseText;
+ try {
+ responseText = await response.text();
+ } catch (err) {
+ throw (`text() threw: ${err}`);
+ }
+
+ return responseText;
+}
+
+promise_test(async function(t) {
+ const scope = 'resources/';
+ const script = 'resources/claim-worker.js';
+ const resource = 'simple.txt';
+
+ // Create the test frame.
+ const frame = await with_iframe('resources/blank.html');
+ t.add_cleanup(() => frame.remove());
+
+ // Check the controller and test with fetch.
+ assert_equals(frame.contentWindow.navigator.controller, undefined,
+ 'Should have no controller.');
+ let response;
+ try {
+ response = await tryFetch(frame.contentWindow.fetch, resource);
+ } catch (err) {
+ assert_unreached(`uncontrolled fetch failed: ${err}`);
+ }
+ assert_equals(response, 'a simple text file\n',
+ 'fetch() should not be intercepted.');
+
+ // Register a service worker.
+ const registration =
+ await service_worker_unregister_and_register(t, script, scope);
+ t.add_cleanup(() => registration.unregister());
+ const worker = registration.installing;
+ await wait_for_state(t, worker, 'activated');
+
+ // Register a controllerchange event to wait until the controller is updated
+ // and check if the frame is controlled by a service worker.
+ const controllerChanged = new Promise((resolve) => {
+ frame.contentWindow.navigator.serviceWorker.oncontrollerchange = () => {
+ resolve(frame.contentWindow.navigator.serviceWorker.controller);
+ };
+ });
+
+ // Tell the service worker to claim the iframe.
+ const sawMessage = new Promise((resolve) => {
+ const channel = new MessageChannel();
+ channel.port1.onmessage = t.step_func((event) => {
+ resolve(event.data);
+ });
+ worker.postMessage({port: channel.port2}, [channel.port2]);
+ });
+ const data = await sawMessage;
+ assert_equals(data, 'PASS', 'Worker call to claim() should fulfill.');
+
+ // Check if the controller is updated after claim() and test with fetch.
+ const controller = await controllerChanged;
+ assert_true(controller instanceof frame.contentWindow.ServiceWorker,
+ 'iframe should be controlled.');
+ try {
+ response = await tryFetch(frame.contentWindow.fetch, resource);
+ } catch (err) {
+ assert_unreached(`controlled fetch failed: ${err}`);
+ }
+ assert_equals(response, 'Intercepted!',
+ 'fetch() should be intercepted.');
+}, 'fetch() should be intercepted after the client is claimed.');
+
+</script>
+</body>
diff --git a/test/wpt/tests/service-workers/service-worker/claim-not-using-registration.https.html b/test/wpt/tests/service-workers/service-worker/claim-not-using-registration.https.html
new file mode 100644
index 0000000..fd61d05
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/claim-not-using-registration.https.html
@@ -0,0 +1,131 @@
+<!DOCTYPE html>
+<title>Service Worker: claim client not using registration</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+
+promise_test(function(t) {
+ var init_scope = 'resources/blank.html?not-using-init';
+ var claim_scope = 'resources/blank.html?not-using';
+ var init_worker_url = 'resources/empty.js';
+ var claim_worker_url = 'resources/claim-worker.js';
+ var claim_worker, claim_registration, frame1, frame2;
+ return service_worker_unregister_and_register(
+ t, init_worker_url, init_scope)
+ .then(function(registration) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, init_scope);
+ });
+
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() {
+ return Promise.all(
+ [with_iframe(init_scope), with_iframe(claim_scope)]);
+ })
+ .then(function(frames) {
+ frame1 = frames[0];
+ frame2 = frames[1];
+ assert_equals(
+ frame1.contentWindow.navigator.serviceWorker.controller.scriptURL,
+ normalizeURL(init_worker_url),
+ 'Frame1 controller should not be null');
+ assert_equals(
+ frame2.contentWindow.navigator.serviceWorker.controller, null,
+ 'Frame2 controller should be null');
+ return navigator.serviceWorker.register(claim_worker_url,
+ {scope: claim_scope});
+ })
+ .then(function(registration) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, claim_scope);
+ });
+
+ claim_worker = registration.installing;
+ claim_registration = registration;
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() {
+ var saw_controllerchanged = new Promise(function(resolve) {
+ frame2.contentWindow.navigator.serviceWorker.oncontrollerchange =
+ function() { resolve(); }
+ });
+ var channel = new MessageChannel();
+ var saw_message = new Promise(function(resolve) {
+ channel.port1.onmessage = t.step_func(function(e) {
+ assert_equals(e.data, 'PASS',
+ 'Worker call to claim() should fulfill.');
+ resolve();
+ });
+ });
+ claim_worker.postMessage({port: channel.port2}, [channel.port2]);
+ return Promise.all([saw_controllerchanged, saw_message]);
+ })
+ .then(function() {
+ assert_equals(
+ frame1.contentWindow.navigator.serviceWorker.controller.scriptURL,
+ normalizeURL(init_worker_url),
+ 'Frame1 should not be influenced');
+ assert_equals(
+ frame2.contentWindow.navigator.serviceWorker.controller.scriptURL,
+ normalizeURL(claim_worker_url),
+ 'Frame2 should be controlled by the new registration');
+ frame1.remove();
+ frame2.remove();
+ return claim_registration.unregister();
+ });
+ }, 'Test claim client which is not using registration');
+
+promise_test(function(t) {
+ var scope = 'resources/blank.html?longer-matched';
+ var claim_scope = 'resources/blank.html?longer';
+ var claim_worker_url = 'resources/claim-worker.js';
+ var installing_worker_url = 'resources/empty-worker.js';
+ var frame, claim_worker;
+ return with_iframe(scope)
+ .then(function(f) {
+ frame = f;
+ return navigator.serviceWorker.register(
+ claim_worker_url, {scope: claim_scope});
+ })
+ .then(function(registration) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, claim_scope);
+ });
+
+ claim_worker = registration.installing;
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() {
+ return navigator.serviceWorker.register(
+ installing_worker_url, {scope: scope});
+ })
+ .then(function() {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ var channel = new MessageChannel();
+ var saw_message = new Promise(function(resolve) {
+ channel.port1.onmessage = t.step_func(function(e) {
+ assert_equals(e.data, 'PASS',
+ 'Worker call to claim() should fulfill.');
+ resolve();
+ });
+ });
+ claim_worker.postMessage({port: channel.port2}, [channel.port2]);
+ return saw_message;
+ })
+ .then(function() {
+ assert_equals(
+ frame.contentWindow.navigator.serviceWorker.controller, null,
+ 'Frame should not be claimed when a longer-matched ' +
+ 'registration exists');
+ frame.remove();
+ });
+ }, 'Test claim client when there\'s a longer-matched registration not ' +
+ 'already used by the page');
+
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/claim-shared-worker-fetch.https.html b/test/wpt/tests/service-workers/service-worker/claim-shared-worker-fetch.https.html
new file mode 100644
index 0000000..f5f4488
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/claim-shared-worker-fetch.https.html
@@ -0,0 +1,71 @@
+<!doctype html>
+<meta charset=utf-8>
+<title></title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+
+promise_test(function(t) {
+ var frame;
+ var resource = 'simple.txt';
+
+ var worker;
+ var scope = 'resources/';
+ var script = 'resources/claim-worker.js';
+
+ return Promise.resolve()
+ // Create the test iframe with a shared worker.
+ .then(() => with_iframe('resources/claim-shared-worker-fetch-iframe.html'))
+ .then(f => frame = f)
+
+ // Check the controller and test with fetch in the shared worker.
+ .then(() => assert_equals(frame.contentWindow.navigator.controller,
+ undefined,
+ 'Should have no controller.'))
+ .then(() => frame.contentWindow.fetch_in_shared_worker(resource))
+ .then(response_text => assert_equals(response_text,
+ 'a simple text file\n',
+ 'fetch() should not be intercepted.'))
+ // Register a service worker.
+ .then(() => service_worker_unregister_and_register(t, script, scope))
+ .then(r => {
+ t.add_cleanup(() => service_worker_unregister(t, scope));
+
+ worker = r.installing;
+
+ return wait_for_state(t, worker, 'activated')
+ })
+ // Let the service worker claim the iframe and the shared worker.
+ .then(() => {
+ var channel = new MessageChannel();
+ var saw_message = new Promise(function(resolve) {
+ channel.port1.onmessage = t.step_func(function(e) {
+ assert_equals(e.data, 'PASS',
+ 'Worker call to claim() should fulfill.');
+ resolve();
+ });
+ });
+ worker.postMessage({port: channel.port2}, [channel.port2]);
+ return saw_message;
+ })
+
+ // Check the controller and test with fetch in the shared worker.
+ .then(() => frame.contentWindow.navigator.serviceWorker.getRegistration(scope))
+ .then(r => assert_equals(frame.contentWindow.navigator.serviceWorker.controller,
+ r.active,
+ 'Test iframe should be claimed.'))
+ // TODO(horo): Check the SharedWorker's navigator.seviceWorker.controller.
+ .then(() => frame.contentWindow.fetch_in_shared_worker(resource))
+ .then(response_text =>
+ assert_equals(response_text,
+ 'Intercepted!',
+ 'fetch() in the shared worker should be intercepted.'))
+
+ // Cleanup this testcase.
+ .then(() => frame.remove());
+}, 'fetch() in SharedWorker should be intercepted after the client is claimed.')
+
+</script>
+</body>
diff --git a/test/wpt/tests/service-workers/service-worker/claim-using-registration.https.html b/test/wpt/tests/service-workers/service-worker/claim-using-registration.https.html
new file mode 100644
index 0000000..8a2a6ff
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/claim-using-registration.https.html
@@ -0,0 +1,103 @@
+<!DOCTYPE html>
+<title>Service Worker: claim client using registration</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+
+promise_test(function(t) {
+ var scope = 'resources/';
+ var frame_url = 'resources/blank.html?using-different-registration';
+ var url1 = 'resources/empty.js';
+ var url2 = 'resources/claim-worker.js';
+ var worker, sw_registration, frame;
+ return service_worker_unregister_and_register(t, url1, scope)
+ .then(function(registration) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() {
+ return with_iframe(frame_url);
+ })
+ .then(function(f) {
+ frame = f;
+ return navigator.serviceWorker.register(url2, {scope: frame_url});
+ })
+ .then(function(registration) {
+ worker = registration.installing;
+ sw_registration = registration;
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() {
+ var saw_controllerchanged = new Promise(function(resolve) {
+ frame.contentWindow.navigator.serviceWorker.oncontrollerchange =
+ function() { resolve(); }
+ });
+ var channel = new MessageChannel();
+ var saw_message = new Promise(function(resolve) {
+ channel.port1.onmessage = t.step_func(function(e) {
+ assert_equals(e.data, 'PASS',
+ 'Worker call to claim() should fulfill.');
+ resolve();
+ });
+ });
+ worker.postMessage({port: channel.port2}, [channel.port2]);
+ return Promise.all([saw_controllerchanged, saw_message]);
+ })
+ .then(function() {
+ assert_equals(
+ frame.contentWindow.navigator.serviceWorker.controller.scriptURL,
+ normalizeURL(url2),
+ 'Frame1 controller scriptURL should be changed to url2');
+ frame.remove();
+ return sw_registration.unregister();
+ });
+ }, 'Test worker claims client which is using another registration');
+
+promise_test(function(t) {
+ var scope = 'resources/blank.html?using-same-registration';
+ var url1 = 'resources/empty.js';
+ var url2 = 'resources/claim-worker.js';
+ var frame, worker;
+ return service_worker_unregister_and_register(t, url1, scope)
+ .then(function(registration) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() {
+ return with_iframe(scope);
+ })
+ .then(function(f) {
+ frame = f;
+ return navigator.serviceWorker.register(url2, {scope: scope});
+ })
+ .then(function(registration) {
+ worker = registration.installing;
+ return wait_for_state(t, registration.installing, 'installed');
+ })
+ .then(function() {
+ var channel = new MessageChannel();
+ var saw_message = new Promise(function(resolve) {
+ channel.port1.onmessage = t.step_func(function(e) {
+ assert_equals(e.data, 'FAIL: exception: InvalidStateError',
+ 'Worker call to claim() should reject with ' +
+ 'InvalidStateError');
+ resolve();
+ });
+ });
+ worker.postMessage({port: channel.port2}, [channel.port2]);
+ return saw_message;
+ })
+ .then(function() {
+ frame.remove();
+ });
+ }, 'Test for the waiting worker claims a client which is using the the ' +
+ 'same registration');
+
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/claim-with-redirect.https.html b/test/wpt/tests/service-workers/service-worker/claim-with-redirect.https.html
new file mode 100644
index 0000000..fd89cb9
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/claim-with-redirect.https.html
@@ -0,0 +1,59 @@
+<!DOCTYPE html>
+<title>Service Worker: Claim() when update happens after redirect</title>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+var host_info = get_host_info();
+var BASE_URL = host_info['HTTPS_ORIGIN'] + base_path();
+var OTHER_BASE_URL = host_info['HTTPS_REMOTE_ORIGIN'] + base_path();
+
+var WORKER_URL = OTHER_BASE_URL + 'resources/update-claim-worker.py'
+var SCOPE_URL = OTHER_BASE_URL + 'resources/redirect.py'
+var OTHER_IFRAME_URL = OTHER_BASE_URL +
+ 'resources/claim-with-redirect-iframe.html';
+
+// Different origin from the registration
+var REDIRECT_TO_URL = BASE_URL +
+ 'resources/claim-with-redirect-iframe.html?redirected';
+
+var REGISTER_IFRAME_URL = OTHER_IFRAME_URL + '?register=' +
+ encodeURIComponent(WORKER_URL) + '&' +
+ 'scope=' + encodeURIComponent(SCOPE_URL);
+var REDIRECT_IFRAME_URL = SCOPE_URL + '?Redirect=' +
+ encodeURIComponent(REDIRECT_TO_URL);
+var UPDATE_IFRAME_URL = OTHER_IFRAME_URL + '?update=' +
+ encodeURIComponent(SCOPE_URL);
+var UNREGISTER_IFRAME_URL = OTHER_IFRAME_URL + '?unregister=' +
+ encodeURIComponent(SCOPE_URL);
+
+var waiting_resolver = undefined;
+
+addEventListener('message', e => {
+ if (waiting_resolver !== undefined) {
+ waiting_resolver(e.data);
+ }
+ });
+
+function assert_with_iframe(url, expected_message) {
+ return new Promise(resolve => {
+ waiting_resolver = resolve;
+ with_iframe(url);
+ })
+ .then(data => assert_equals(data.message, expected_message));
+}
+
+// This test checks behavior when browser got a redirect header from in-scope
+// page and navigated to out-of-scope page which has a different origin from any
+// registrations.
+promise_test(t => {
+ return assert_with_iframe(REGISTER_IFRAME_URL, 'registered')
+ .then(() => assert_with_iframe(REDIRECT_IFRAME_URL, 'redirected'))
+ .then(() => assert_with_iframe(UPDATE_IFRAME_URL, 'updated'))
+ .then(() => assert_with_iframe(UNREGISTER_IFRAME_URL, 'unregistered'));
+ }, 'Claim works after redirection to another origin');
+
+</script>
+</body>
diff --git a/test/wpt/tests/service-workers/service-worker/claim-worker-fetch.https.html b/test/wpt/tests/service-workers/service-worker/claim-worker-fetch.https.html
new file mode 100644
index 0000000..7cb26c7
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/claim-worker-fetch.https.html
@@ -0,0 +1,83 @@
+<!doctype html>
+<meta charset=utf-8>
+<title></title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+
+promise_test((t) => {
+ return runTest(t, 'resources/claim-worker-fetch-iframe.html');
+}, 'fetch() in Worker should be intercepted after the client is claimed.');
+
+promise_test((t) => {
+ return runTest(t, 'resources/claim-nested-worker-fetch-iframe.html');
+}, 'fetch() in nested Worker should be intercepted after the client is claimed.');
+
+promise_test((t) => {
+ return runTest(t, 'resources/claim-blob-url-worker-fetch-iframe.html');
+}, 'fetch() in blob URL Worker should be intercepted after the client is claimed.');
+
+promise_test((t) => {
+ return runTest(t, 'resources/nested-blob-url-workers.html');
+}, 'fetch() in nested blob URL Worker created from a blob URL Worker should be intercepted after the client is claimed.');
+
+promise_test((t) => {
+ return runTest(t, 'resources/nested-worker-created-from-blob-url-worker.html');
+}, 'fetch() in nested Worker created from a blob URL Worker should be intercepted after the client is claimed.');
+
+promise_test((t) => {
+ return runTest(t, 'resources/nested-blob-url-worker-created-from-worker.html');
+}, 'fetch() in nested blob URL Worker created from a Worker should be intercepted after the client is claimed.');
+
+async function runTest(t, iframe_url) {
+ const resource = 'simple.txt';
+ const scope = 'resources/';
+ const script = 'resources/claim-worker.js';
+
+ // Create the test iframe with a dedicated worker.
+ const frame = await with_iframe(iframe_url);
+ t.add_cleanup(_ => frame.remove());
+
+ // Check the controller and test with fetch in the worker.
+ assert_equals(frame.contentWindow.navigator.controller,
+ undefined, 'Should have no controller.');
+ {
+ const response_text = await frame.contentWindow.fetch_in_worker(resource);
+ assert_equals(response_text, 'a simple text file\n',
+ 'fetch() should not be intercepted.');
+ }
+
+ // Register a service worker.
+ const reg = await service_worker_unregister_and_register(t, script, scope);
+ t.add_cleanup(_ => reg.unregister());
+ await wait_for_state(t, reg.installing, 'activated');
+
+ // Let the service worker claim the iframe and the worker.
+ const channel = new MessageChannel();
+ const saw_message = new Promise(function(resolve) {
+ channel.port1.onmessage = t.step_func(function(e) {
+ assert_equals(e.data, 'PASS', 'Worker call to claim() should fulfill.');
+ resolve();
+ });
+ });
+ reg.active.postMessage({port: channel.port2}, [channel.port2]);
+ await saw_message;
+
+ // Check the controller and test with fetch in the worker.
+ const reg2 =
+ await frame.contentWindow.navigator.serviceWorker.getRegistration(scope);
+ assert_equals(frame.contentWindow.navigator.serviceWorker.controller,
+ reg2.active, 'Test iframe should be claimed.');
+
+ {
+ // TODO(horo): Check the worker's navigator.seviceWorker.controller.
+ const response_text = await frame.contentWindow.fetch_in_worker(resource);
+ assert_equals(response_text, 'Intercepted!',
+ 'fetch() in the worker should be intercepted.');
+ }
+}
+
+</script>
+</body>
diff --git a/test/wpt/tests/service-workers/service-worker/client-id.https.html b/test/wpt/tests/service-workers/service-worker/client-id.https.html
new file mode 100644
index 0000000..b93b341
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/client-id.https.html
@@ -0,0 +1,60 @@
+<!DOCTYPE html>
+<title>Service Worker: Client.id</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+var scope = 'resources/blank.html?client-id';
+var frame1, frame2;
+
+promise_test(function(t) {
+ return service_worker_unregister_and_register(
+ t, 'resources/client-id-worker.js', scope)
+ .then(function(registration) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() { return with_iframe(scope + '#1'); })
+ .then(function(f) {
+ frame1 = f;
+ // To be sure Clients.matchAll() iterates in the same order.
+ f.focus();
+ return with_iframe(scope + '#2');
+ })
+ .then(function(f) {
+ frame2 = f;
+ var channel = new MessageChannel();
+
+ return new Promise(function(resolve, reject) {
+ channel.port1.onmessage = resolve;
+ channel.port1.onmessageerror = reject;
+ f.contentWindow.navigator.serviceWorker.controller.postMessage(
+ {port:channel.port2}, [channel.port2]);
+ });
+ })
+ .then(on_message);
+ }, 'Client.id returns the client\'s ID.');
+
+function on_message(e) {
+ // The result of two sequential clients.matchAll() calls in the SW.
+ // 1st matchAll() results in e.data[0], e.data[1].
+ // 2nd matchAll() results in e.data[2], e.data[3].
+ assert_equals(e.data.length, 4);
+ // All should be string values.
+ assert_equals(typeof e.data[0], 'string');
+ assert_equals(typeof e.data[1], 'string');
+ assert_equals(typeof e.data[2], 'string');
+ assert_equals(typeof e.data[3], 'string');
+ // Different clients should have different ids.
+ assert_not_equals(e.data[0], e.data[1]);
+ assert_not_equals(e.data[2], e.data[3]);
+ // Same clients should have an identical id.
+ assert_equals(e.data[0], e.data[2]);
+ assert_equals(e.data[1], e.data[3]);
+ frame1.remove();
+ frame2.remove();
+}
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/client-navigate.https.html b/test/wpt/tests/service-workers/service-worker/client-navigate.https.html
new file mode 100644
index 0000000..f40a086
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/client-navigate.https.html
@@ -0,0 +1,107 @@
+<!doctype html>
+<meta charset=utf-8>
+<meta name="timeout" content="long">
+<title>Service Worker: WindowClient.navigate</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+ function wait_for_message(msg) {
+ return new Promise(function(resolve, reject) {
+ var get_message_data = function get_message_data(e) {
+ window.removeEventListener("message", get_message_data);
+ resolve(e.data);
+ }
+ window.addEventListener("message", get_message_data, false);
+ });
+ }
+
+ function run_test(controller, clientId, test) {
+ return new Promise(function(resolve, reject) {
+ var channel = new MessageChannel();
+ channel.port1.onmessage = function(e) {
+ resolve(e.data);
+ };
+ var message = {
+ port: channel.port2,
+ test: test,
+ clientId: clientId,
+ };
+ controller.postMessage(
+ message, [channel.port2]);
+ });
+ }
+
+ async function with_controlled_iframe_and_url(t, name, f) {
+ const SCRIPT = "resources/client-navigate-worker.js";
+ const SCOPE = "resources/client-navigate-frame.html";
+
+ // Register service worker and wait for it to become activated
+ const registration = await service_worker_unregister_and_register(t, SCRIPT, SCOPE);
+ t.add_cleanup(() => registration.unregister());
+ const worker = registration.installing;
+ await wait_for_state(t, worker, 'activated');
+
+ // Create child iframe and make sure we register a listener for the message
+ // it sends before it's created
+ const client_id_promise = wait_for_message();
+ const iframe = await with_iframe(SCOPE);
+ t.add_cleanup(() => iframe.remove());
+ const { id } = await client_id_promise;
+
+ // Run the test in the service worker and fetch it
+ const { result, url } = await run_test(worker, id, name);
+ fetch_tests_from_worker(worker);
+ assert_equals(result, name);
+
+ // Hand over the iframe and URL from the service worker to the callback
+ await f(iframe, url);
+ }
+
+ promise_test(function(t) {
+ return with_controlled_iframe_and_url(t, 'test_client_navigate_success', async (iframe, url) => {
+ assert_equals(
+ url, new URL("resources/client-navigated-frame.html",
+ location).toString());
+ assert_equals(
+ iframe.contentWindow.location.href,
+ new URL("resources/client-navigated-frame.html",
+ location).toString());
+ });
+ }, "Frame location should update on successful navigation");
+
+ promise_test(function(t) {
+ return with_controlled_iframe_and_url(t, 'test_client_navigate_redirect', async (iframe, url) => {
+ assert_equals(url, "");
+ assert_throws_dom("SecurityError", function() { return iframe.contentWindow.location.href });
+ });
+ }, "Frame location should not be accessible after redirect");
+
+ promise_test(function(t) {
+ return with_controlled_iframe_and_url(t, 'test_client_navigate_cross_origin', async (iframe, url) => {
+ assert_equals(url, "");
+ assert_throws_dom("SecurityError", function() { return iframe.contentWindow.location.href });
+ });
+ }, "Frame location should not be accessible after cross-origin navigation");
+
+ promise_test(function(t) {
+ return with_controlled_iframe_and_url(t, 'test_client_navigate_about_blank', async (iframe, url) => {
+ assert_equals(
+ iframe.contentWindow.location.href,
+ new URL("resources/client-navigate-frame.html",
+ location).toString());
+ iframe.contentWindow.document.body.style = "background-color: green"
+ });
+ }, "Frame location should not update on failed about:blank navigation");
+
+ promise_test(function(t) {
+ return with_controlled_iframe_and_url(t, 'test_client_navigate_mixed_content', async (iframe, url) => {
+ assert_equals(
+ iframe.contentWindow.location.href,
+ new URL("resources/client-navigate-frame.html",
+ location).toString());
+ iframe.contentWindow.document.body.style = "background-color: green"
+ });
+ }, "Frame location should not update on failed mixed-content navigation");
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/client-url-of-blob-url-worker.https.html b/test/wpt/tests/service-workers/service-worker/client-url-of-blob-url-worker.https.html
new file mode 100644
index 0000000..97a2fcf
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/client-url-of-blob-url-worker.https.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<title>Service Worker: client.url of a worker created from a blob URL</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+
+const SCRIPT = 'resources/client-url-of-blob-url-worker.js';
+const SCOPE = 'resources/client-url-of-blob-url-worker.html';
+
+promise_test(async (t) => {
+ const reg = await service_worker_unregister_and_register(t, SCRIPT, SCOPE);
+ t.add_cleanup(_ => reg.unregister());
+ await wait_for_state(t, reg.installing, 'activated');
+
+ const frame = await with_iframe(SCOPE);
+ t.add_cleanup(_ => frame.remove());
+ assert_not_equals(frame.contentWindow.navigator.serviceWorker.controller,
+ null, 'frame should be controlled');
+
+ const response = await frame.contentWindow.createAndFetchFromBlobWorker();
+
+ assert_not_equals(response.result, 'one worker client should exist',
+ 'worker client should exist');
+ assert_equals(response.result, response.expected,
+ 'client.url and worker location href should be the same');
+
+}, 'Client.url of a blob URL worker should be a blob URL.');
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/clients-get-client-types.https.html b/test/wpt/tests/service-workers/service-worker/clients-get-client-types.https.html
new file mode 100644
index 0000000..63e3e51
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/clients-get-client-types.https.html
@@ -0,0 +1,108 @@
+<!DOCTYPE html>
+<title>Service Worker: Clients.get with window and worker clients</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+var scope = 'resources/clients-get-client-types';
+var frame_url = scope + '-frame.html';
+var shared_worker_url = scope + '-shared-worker.js';
+var worker_url = scope + '-worker.js';
+var client_ids = [];
+var registration;
+var frame;
+promise_test(function(t) {
+ return service_worker_unregister_and_register(
+ t, 'resources/clients-get-worker.js', scope)
+ .then(function(r) {
+ registration = r;
+ add_completion_callback(function() { registration.unregister(); });
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() {
+ return with_iframe(frame_url);
+ })
+ .then(function(f) {
+ frame = f;
+ add_completion_callback(function() { frame.remove(); });
+ frame.focus();
+ return wait_for_clientId();
+ })
+ .then(function(client_id) {
+ client_ids.push(client_id);
+ return new Promise(function(resolve) {
+ var w = new SharedWorker(shared_worker_url);
+ w.port.onmessage = function(e) {
+ resolve(e.data.clientId);
+ };
+ });
+ })
+ .then(function(client_id) {
+ client_ids.push(client_id);
+ var channel = new MessageChannel();
+ var w = new Worker(worker_url);
+ w.postMessage({cmd:'GetClientId', port:channel.port2},
+ [channel.port2]);
+ return new Promise(function(resolve) {
+ channel.port1.onmessage = function(e) {
+ resolve(e.data.clientId);
+ };
+ });
+ })
+ .then(function(client_id) {
+ client_ids.push(client_id);
+ var channel = new MessageChannel();
+ frame.contentWindow.postMessage('StartWorker', '*', [channel.port2]);
+ return new Promise(function(resolve) {
+ channel.port1.onmessage = function(e) {
+ resolve(e.data.clientId);
+ };
+ });
+ })
+ .then(function(client_id) {
+ client_ids.push(client_id);
+ var saw_message = new Promise(function(resolve) {
+ navigator.serviceWorker.onmessage = resolve;
+ });
+ registration.active.postMessage({clientIds: client_ids});
+ return saw_message;
+ })
+ .then(function(e) {
+ assert_equals(e.data.length, expected.length);
+ // We use these assert_not_equals because assert_array_equals doesn't
+ // print the error description when passed an undefined value.
+ assert_not_equals(e.data[0], undefined,
+ 'Window client should not be undefined');
+ assert_array_equals(e.data[0], expected[0], 'Window client');
+ assert_not_equals(e.data[1], undefined,
+ 'Shared worker client should not be undefined');
+ assert_array_equals(e.data[1], expected[1], 'Shared worker client');
+ assert_not_equals(e.data[2], undefined,
+ 'Worker(Started by main frame) client should not be undefined');
+ assert_array_equals(e.data[2], expected[2],
+ 'Worker(Started by main frame) client');
+ assert_not_equals(e.data[3], undefined,
+ 'Worker(Started by sub frame) client should not be undefined');
+ assert_array_equals(e.data[3], expected[3],
+ 'Worker(Started by sub frame) client');
+ });
+ }, 'Test Clients.get() with window and worker clients');
+
+function wait_for_clientId() {
+ return new Promise(function(resolve) {
+ function get_client_id(e) {
+ window.removeEventListener('message', get_client_id);
+ resolve(e.data.clientId);
+ }
+ window.addEventListener('message', get_client_id, false);
+ });
+}
+
+var expected = [
+ // visibilityState, focused, url, type, frameType
+ ['visible', true, normalizeURL(scope) + '-frame.html', 'window', 'nested'],
+ [undefined, undefined, normalizeURL(scope) + '-shared-worker.js', 'sharedworker', 'none'],
+ [undefined, undefined, normalizeURL(scope) + '-worker.js', 'worker', 'none'],
+ [undefined, undefined, normalizeURL(scope) + '-frame-worker.js', 'worker', 'none']
+];
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/clients-get-cross-origin.https.html b/test/wpt/tests/service-workers/service-worker/clients-get-cross-origin.https.html
new file mode 100644
index 0000000..1e4acfb
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/clients-get-cross-origin.https.html
@@ -0,0 +1,69 @@
+<!DOCTYPE html>
+<title>Service Worker: Clients.get across origins</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+var host_info = get_host_info();
+
+var scope = 'resources/clients-get-frame.html';
+var other_origin_iframe = host_info['HTTPS_REMOTE_ORIGIN'] + base_path() +
+ 'resources/clients-get-cross-origin-frame.html';
+// The ID of a client from the same origin as us.
+var my_origin_client_id;
+// This test asserts the behavior of the Client API in cases where the client
+// belongs to a foreign origin. It does this by creating an iframe with a
+// foreign origin which connects to a server worker in the current origin.
+promise_test(function(t) {
+ return service_worker_unregister_and_register(
+ t, 'resources/clients-get-worker.js', scope)
+ .then(function(registration) {
+ add_completion_callback(function() { registration.unregister(); });
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() {
+ return with_iframe(scope);
+ })
+ // Create a same-origin client and use it to populate |my_origin_client_id|.
+ .then(function(frame1) {
+ add_completion_callback(function() { frame1.remove(); });
+ return new Promise(function(resolve, reject) {
+ function get_client_id(e) {
+ window.removeEventListener('message', get_client_id);
+ resolve(e.data.clientId);
+ }
+ window.addEventListener('message', get_client_id, false);
+ });
+ })
+ // Create a cross-origin client. We'll communicate with this client to
+ // test the cross-origin service worker's behavior.
+ .then(function(client_id) {
+ my_origin_client_id = client_id;
+ return with_iframe(other_origin_iframe);
+ })
+ // Post the 'getClientId' message to the cross-origin client. The client
+ // will then ask its service worker to look up |my_origin_client_id| via
+ // Clients#get. Since this client ID is for a different origin, we expect
+ // the client to not be found.
+ .then(function(frame2) {
+ add_completion_callback(function() { frame2.remove(); });
+
+ frame2.contentWindow.postMessage(
+ {clientId: my_origin_client_id, type: 'getClientId'},
+ host_info['HTTPS_REMOTE_ORIGIN']
+ );
+
+ return new Promise(function(resolve) {
+ window.addEventListener('message', function(e) {
+ if (e.data && e.data.type === 'clientId') {
+ resolve(e.data.value);
+ }
+ });
+ });
+ })
+ .then(function(client_id) {
+ assert_equals(client_id, undefined, 'iframe client ID');
+ });
+ }, 'Test Clients.get() cross origin');
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/clients-get-resultingClientId.https.html b/test/wpt/tests/service-workers/service-worker/clients-get-resultingClientId.https.html
new file mode 100644
index 0000000..3419cf1
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/clients-get-resultingClientId.https.html
@@ -0,0 +1,177 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Test clients.get(resultingClientId)</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+const scope = "resources/";
+let worker;
+
+// Setup. Keep this as the first promise_test.
+promise_test(async (t) => {
+ const registration = await service_worker_unregister_and_register(
+ t, 'resources/get-resultingClientId-worker.js',
+ scope);
+ worker = registration.installing;
+ await wait_for_state(t, worker, 'activated');
+}, 'global setup');
+
+// Sends |command| to the worker and returns a promise that resolves to its
+// response. There should only be one inflight command at a time.
+async function sendCommand(command) {
+ const saw_message = new Promise((resolve) => {
+ navigator.serviceWorker.onmessage = (event) => {
+ resolve(event.data);
+ };
+ });
+ worker.postMessage(command);
+ return saw_message;
+}
+
+// Wrapper for 'startTest' command. Tells the worker a test is starting,
+// so it resets state and keeps itself alive until 'finishTest'.
+async function startTest(t) {
+ const result = await sendCommand({command: 'startTest'});
+ assert_equals(result, 'ok', 'startTest');
+
+ t.add_cleanup(async () => {
+ return finishTest();
+ });
+}
+
+// Wrapper for 'finishTest' command.
+async function finishTest() {
+ const result = await sendCommand({command: 'finishTest'});
+ assert_equals(result, 'ok', 'finishTest');
+}
+
+// Wrapper for 'getResultingClient' command. Tells the worker to return
+// clients.get(event.resultingClientId) for the navigation that occurs
+// during this test.
+//
+// The return value describes how clients.get() settled. It also includes
+// |queriedId| which is the id passed to clients.get() (the resultingClientId
+// in this case).
+//
+// Example value:
+// {
+// queriedId: 'abc',
+// promiseState: fulfilled,
+// promiseValue: client,
+// client: {
+// id: 'abc',
+// url: '//example.com/client'
+// }
+// }
+async function getResultingClient() {
+ return sendCommand({command: 'getResultingClient'});
+}
+
+// Wrapper for 'getClient' command. Tells the worker to return
+// clients.get(|id|). The return value is as in the getResultingClient()
+// documentation.
+async function getClient(id) {
+ return sendCommand({command: 'getClient', id: id});
+}
+
+// Navigates to |url|. Returns the result of clients.get() on the
+// resultingClientId.
+async function navigateAndGetResultingClient(t, url) {
+ const resultPromise = getResultingClient();
+ const frame = await with_iframe(url);
+ t.add_cleanup(() => {
+ frame.remove();
+ });
+ const result = await resultPromise;
+ const resultingClientId = result.queriedId;
+
+ // First test clients.get(event.resultingClientId) inside the fetch event. The
+ // behavior of this is subtle due to the use of iframes and about:blank
+ // replacement. The spec probably requires that it resolve to the original
+ // about:blank client, and that later that client should be discarded after
+ // load if the load was to another origin. Implementations might differ. For
+ // now, this test just asserts that the promise resolves. See
+ // https://github.com/w3c/ServiceWorker/issues/1385.
+ assert_equals(result.promiseState, 'fulfilled',
+ 'get(event.resultingClientId) in the fetch event should fulfill');
+
+ // Test clients.get() on the previous resultingClientId again. By this
+ // time the load finished, so it's more straightforward how this promise
+ // should settle. Return the result of this promise.
+ return await getClient(resultingClientId);
+}
+
+// Test get(resultingClientId) in the basic same-origin case.
+promise_test(async (t) => {
+ await startTest(t);
+
+ const url = new URL('resources/empty.html', window.location);
+ const result = await navigateAndGetResultingClient(t, url);
+ assert_equals(result.promiseState, 'fulfilled', 'promiseState');
+ assert_equals(result.promiseValue, 'client', 'promiseValue');
+ assert_equals(result.client.url, url.href, 'client.url',);
+ assert_equals(result.client.id, result.queriedId, 'client.id');
+}, 'get(resultingClientId) for same-origin document');
+
+// Test get(resultingClientId) when the response redirects to another origin.
+promise_test(async (t) => {
+ await startTest(t);
+
+ // Navigate to a URL that redirects to another origin.
+ const base_url = new URL('.', window.location);
+ const host_info = get_host_info();
+ const other_origin_url = new URL(base_url.pathname + 'resources/empty.html',
+ host_info['HTTPS_REMOTE_ORIGIN']);
+ const url = new URL('resources/empty.html', window.location);
+ const pipe = `status(302)|header(Location, ${other_origin_url})`;
+ url.searchParams.set('pipe', pipe);
+
+ // The original reserved client should have been discarded on cross-origin
+ // redirect.
+ const result = await navigateAndGetResultingClient(t, url);
+ assert_equals(result.promiseState, 'fulfilled', 'promiseState');
+ assert_equals(result.promiseValue, 'undefinedValue', 'promiseValue');
+}, 'get(resultingClientId) on cross-origin redirect');
+
+// Test get(resultingClientId) when the document is sandboxed to a unique
+// origin using a CSP HTTP response header.
+promise_test(async (t) => {
+ await startTest(t);
+
+ // Navigate to a URL that has CSP sandboxing set in the HTTP response header.
+ const url = new URL('resources/empty.html', window.location);
+ const pipe = 'header(Content-Security-Policy, sandbox)';
+ url.searchParams.set('pipe', pipe);
+
+ // The original reserved client should have been discarded upon loading
+ // the sandboxed document.
+ const result = await navigateAndGetResultingClient(t, url);
+ assert_equals(result.promiseState, 'fulfilled', 'promiseState');
+ assert_equals(result.promiseValue, 'undefinedValue', 'promiseValue');
+}, 'get(resultingClientId) for document sandboxed by CSP header');
+
+// Test get(resultingClientId) when the document is sandboxed with
+// allow-same-origin.
+promise_test(async (t) => {
+ await startTest(t);
+
+ // Navigate to a URL that has CSP sandboxing set in the HTTP response header.
+ const url = new URL('resources/empty.html', window.location);
+ const pipe = 'header(Content-Security-Policy, sandbox allow-same-origin)';
+ url.searchParams.set('pipe', pipe);
+
+ // The client should be the original reserved client, as it's same-origin.
+ const result = await navigateAndGetResultingClient(t, url);
+ assert_equals(result.promiseState, 'fulfilled', 'promiseState');
+ assert_equals(result.promiseValue, 'client', 'promiseValue');
+ assert_equals(result.client.url, url.href, 'client.url',);
+ assert_equals(result.client.id, result.queriedId, 'client.id');
+}, 'get(resultingClientId) for document sandboxed by CSP header with allow-same-origin');
+
+// Cleanup. Keep this as the last promise_test.
+promise_test(async (t) => {
+ return service_worker_unregister(t, scope);
+}, 'global cleanup');
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/clients-get.https.html b/test/wpt/tests/service-workers/service-worker/clients-get.https.html
new file mode 100644
index 0000000..4cfbf59
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/clients-get.https.html
@@ -0,0 +1,154 @@
+<!DOCTYPE html>
+<title>Service Worker: Clients.get</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+function wait_for_clientId() {
+ return new Promise(function(resolve, reject) {
+ window.onmessage = e => {
+ resolve(e.data.clientId);
+ };
+ });
+}
+
+promise_test(async t => {
+ // Register service worker.
+ const scope = 'resources/clients-get-frame.html';
+ const client_ids = [];
+ const registration = await service_worker_unregister_and_register(
+ t, 'resources/clients-get-worker.js', scope);
+ t.add_cleanup(() => registration.unregister());
+ await wait_for_state(t, registration.installing, 'activated');
+
+ // Prepare for test cases.
+ // Case 1: frame1 which is focused.
+ const frame1 = await with_iframe(scope + '#1');
+ t.add_cleanup(() => frame1.remove());
+ frame1.focus();
+ client_ids.push(await wait_for_clientId());
+ // Case 2: frame2 which is not focused.
+ const frame2 = await with_iframe(scope + '#2');
+ t.add_cleanup(() => frame2.remove());
+ client_ids.push(await wait_for_clientId());
+ // Case 3: invalid id.
+ client_ids.push('invalid-id');
+
+ // Call clients.get() for each id on the service worker.
+ const message_event = await new Promise(resolve => {
+ navigator.serviceWorker.onmessage = resolve;
+ registration.active.postMessage({clientIds: client_ids});
+ });
+
+ const expected = [
+ // visibilityState, focused, url, type, frameType
+ ['visible', true, normalizeURL(scope) + '#1', 'window', 'nested'],
+ ['visible', false, normalizeURL(scope) + '#2', 'window', 'nested'],
+ undefined
+ ];
+ assert_equals(message_event.data.length, 3);
+ assert_array_equals(message_event.data[0], expected[0]);
+ assert_array_equals(message_event.data[1], expected[1]);
+ assert_equals(message_event.data[2], expected[2]);
+}, 'Test Clients.get()');
+
+promise_test(async t => {
+ // Register service worker.
+ const scope = 'resources/simple.html';
+ const registration = await service_worker_unregister_and_register(
+ t, 'resources/clients-get-resultingClientId-worker.js', scope)
+ t.add_cleanup(() => registration.unregister());
+ await wait_for_state(t, registration.installing, 'activated');
+ const worker = registration.active;
+
+ // Load frame within the scope.
+ const frame = await with_iframe(scope);
+ t.add_cleanup(() => frame.remove());
+ frame.focus();
+
+ // Get resulting client id.
+ const resultingClientId = await new Promise(resolve => {
+ navigator.serviceWorker.onmessage = e => {
+ if (e.data.msg == 'getResultingClientId') {
+ resolve(e.data.resultingClientId);
+ }
+ };
+ worker.postMessage({msg: 'getResultingClientId'});
+ });
+
+ // Query service worker for clients.get(resultingClientId).
+ const isResultingClientUndefined = await new Promise(resolve => {
+ navigator.serviceWorker.onmessage = e => {
+ if (e.data.msg == 'getIsResultingClientUndefined') {
+ resolve(e.data.isResultingClientUndefined);
+ }
+ };
+ worker.postMessage({msg: 'getIsResultingClientUndefined',
+ resultingClientId});
+ });
+
+ assert_false(
+ isResultingClientUndefined,
+ 'Clients.get(FetchEvent.resultingClientId) resolved with a Client');
+}, 'Test successful Clients.get(FetchEvent.resultingClientId)');
+
+promise_test(async t => {
+ // Register service worker.
+ const scope = 'resources/simple.html?fail';
+ const registration = await service_worker_unregister_and_register(
+ t, 'resources/clients-get-resultingClientId-worker.js', scope);
+ t.add_cleanup(() => registration.unregister());
+ await wait_for_state(t, registration.installing, 'activated');
+
+ // Load frame, and destroy it while loading.
+ const worker = registration.active;
+ let frame = document.createElement('iframe');
+ frame.src = scope;
+ t.add_cleanup(() => {
+ if (frame) {
+ frame.remove();
+ }
+ });
+
+ await new Promise(resolve => {
+ navigator.serviceWorker.onmessage = e => {
+ // The service worker posts a message to remove the iframe during fetch
+ // event.
+ if (e.data.msg == 'destroyResultingClient') {
+ frame.remove();
+ frame = null;
+ worker.postMessage({msg: 'resultingClientDestroyed'});
+ resolve();
+ }
+ };
+ document.body.appendChild(frame);
+ });
+
+ resultingDestroyedClientId = await new Promise(resolve => {
+ navigator.serviceWorker.onmessage = e => {
+ // The worker sends a message back when it receives the message
+ // 'resultingClientDestroyed' with the resultingClientId.
+ if (e.data.msg == 'resultingClientDestroyedAck') {
+ assert_equals(frame, null, 'Frame should be destroyed at this point.');
+ resolve(e.data.resultingDestroyedClientId);
+ }
+ };
+ });
+
+ // Query service worker for clients.get(resultingDestroyedClientId).
+ const isResultingClientUndefined = await new Promise(resolve => {
+ navigator.serviceWorker.onmessage = e => {
+ if (e.data.msg == 'getIsResultingClientUndefined') {
+ resolve(e.data.isResultingClientUndefined);
+ }
+ };
+ worker.postMessage({msg: 'getIsResultingClientUndefined',
+ resultingClientId: resultingDestroyedClientId });
+ });
+
+ assert_true(
+ isResultingClientUndefined,
+ 'Clients.get(FetchEvent.resultingClientId) resolved with `undefined`');
+}, 'Test unsuccessful Clients.get(FetchEvent.resultingClientId)');
+
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/clients-matchall-blob-url-worker.https.html b/test/wpt/tests/service-workers/service-worker/clients-matchall-blob-url-worker.https.html
new file mode 100644
index 0000000..c29bac8
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/clients-matchall-blob-url-worker.https.html
@@ -0,0 +1,85 @@
+<!DOCTYPE html>
+<title>Service Worker: Clients.matchAll with a blob URL worker client</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+const SCRIPT = 'resources/clients-matchall-worker.js';
+
+promise_test(async (t) => {
+ const scope = 'resources/clients-matchall-blob-url-worker.html';
+
+ const reg = await service_worker_unregister_and_register(t, SCRIPT, scope);
+ t.add_cleanup(_ => reg.unregister());
+ await wait_for_state(t, reg.installing, 'activated');
+
+ const frame = await with_iframe(scope);
+ t.add_cleanup(_ => frame.remove());
+
+ {
+ const message = await frame.contentWindow.waitForWorker();
+ assert_equals(message.data, 'Worker is ready.',
+ 'Worker should reply to the message.');
+ }
+
+ const channel = new MessageChannel();
+ const message = await new Promise(resolve => {
+ channel.port1.onmessage = resolve;
+ frame.contentWindow.navigator.serviceWorker.controller.postMessage(
+ {port: channel.port2, options: {type: 'worker'}}, [channel.port2]);
+ });
+
+ checkMessageEvent(message);
+
+}, 'Test Clients.matchAll() with a blob URL worker client.');
+
+promise_test(async (t) => {
+ const scope = 'resources/blank.html';
+
+ const reg = await service_worker_unregister_and_register(t, SCRIPT, scope);
+ t.add_cleanup(_ => reg.unregister());
+ await wait_for_state(t, reg.installing, 'activated');
+
+ const workerScript = `
+ self.onmessage = (e) => {
+ self.postMessage("Worker is ready.");
+ };
+ `;
+ const blob = new Blob([workerScript], { type: 'text/javascript' });
+ const blobUrl = URL.createObjectURL(blob);
+ const worker = new Worker(blobUrl);
+
+ {
+ const message = await new Promise(resolve => {
+ worker.onmessage = resolve;
+ worker.postMessage("Ping to worker.");
+ });
+ assert_equals(message.data, 'Worker is ready.',
+ 'Worker should reply to the message.');
+ }
+
+ const channel = new MessageChannel();
+ const message = await new Promise(resolve => {
+ channel.port1.onmessage = resolve;
+ reg.active.postMessage(
+ {port: channel.port2,
+ options: {includeUncontrolled: true, type: 'worker'}},
+ [channel.port2]
+ );
+ });
+
+ checkMessageEvent(message);
+
+}, 'Test Clients.matchAll() with an uncontrolled blob URL worker client.');
+
+function checkMessageEvent(e) {
+ assert_equals(e.data.length, 1);
+
+ const workerClient = e.data[0];
+ assert_equals(workerClient[0], undefined); // visibilityState
+ assert_equals(workerClient[1], undefined); // focused
+ assert_true(workerClient[2].includes('blob:')); // url
+ assert_equals(workerClient[3], 'worker'); // type
+ assert_equals(workerClient[4], 'none'); // frameType
+}
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/clients-matchall-client-types.https.html b/test/wpt/tests/service-workers/service-worker/clients-matchall-client-types.https.html
new file mode 100644
index 0000000..54f182b
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/clients-matchall-client-types.https.html
@@ -0,0 +1,92 @@
+<!DOCTYPE html>
+<title>Service Worker: Clients.matchAll with various clientTypes</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+const scope = 'resources/clients-matchall-client-types';
+const iframe_url = scope + '-iframe.html';
+const shared_worker_url = scope + '-shared-worker.js';
+const dedicated_worker_url = scope + '-dedicated-worker.js';
+
+/* visibilityState, focused, url, type, frameType */
+const expected_only_window = [
+ ['visible', true, new URL(iframe_url, location).href, 'window', 'nested']
+];
+const expected_only_shared_worker = [
+ [undefined, undefined, new URL(shared_worker_url, location).href, 'sharedworker', 'none']
+];
+const expected_only_dedicated_worker = [
+ [undefined, undefined, new URL(dedicated_worker_url, location).href, 'worker', 'none']
+];
+
+// These are explicitly sorted by URL in the service worker script.
+const expected_all_clients = [
+ expected_only_dedicated_worker[0],
+ expected_only_window[0],
+ expected_only_shared_worker[0],
+];
+
+async function test_matchall(frame, expected, query_options) {
+ // Make sure the frame gets focus.
+ frame.focus();
+ const data = await new Promise(resolve => {
+ const channel = new MessageChannel();
+ channel.port1.onmessage = e => resolve(e.data);
+ frame.contentWindow.navigator.serviceWorker.controller.postMessage(
+ {port:channel.port2, options:query_options},
+ [channel.port2]);
+ });
+
+ if (typeof data === 'string') {
+ throw new Error(data);
+ }
+
+ assert_equals(data.length, expected.length, 'result count');
+
+ for (let i = 0; i < data.length; ++i) {
+ assert_array_equals(data[i], expected[i]);
+ }
+}
+
+promise_test(async t => {
+ const registration = await service_worker_unregister_and_register(
+ t, 'resources/clients-matchall-worker.js', scope);
+ t.add_cleanup(_ => registration.unregister());
+ await wait_for_state(t, registration.installing, 'activated');
+ const frame = await with_iframe(iframe_url);
+ t.add_cleanup(_ => frame.remove());
+ await test_matchall(frame, expected_only_window, {});
+ await test_matchall(frame, expected_only_window, {type:'window'});
+}, 'Verify matchAll() with window client type');
+
+promise_test(async t => {
+ const registration = await service_worker_unregister_and_register(
+ t, 'resources/clients-matchall-worker.js', scope);
+ t.add_cleanup(_ => registration.unregister());
+ await wait_for_state(t, registration.installing, 'activated');
+ const frame = await with_iframe(iframe_url);
+ t.add_cleanup(_ => frame.remove());
+
+ // Set up worker clients.
+ const shared_worker = await new Promise((resolve, reject) => {
+ const w = new SharedWorker(shared_worker_url);
+ w.onerror = e => reject(e.message);
+ w.port.onmessage = _ => resolve(w);
+ });
+ const dedicated_worker = await new Promise((resolve, reject) => {
+ const w = new Worker(dedicated_worker_url);
+ w.onerror = e => reject(e.message);
+ w.onmessage = _ => resolve(w);
+ w.postMessage('Start');
+ });
+
+ await test_matchall(frame, expected_only_window, {});
+ await test_matchall(frame, expected_only_window, {type:'window'});
+ await test_matchall(frame, expected_only_shared_worker,
+ {type:'sharedworker'});
+ await test_matchall(frame, expected_only_dedicated_worker, {type:'worker'});
+ await test_matchall(frame, expected_all_clients, {type:'all'});
+}, 'Verify matchAll() with {window, sharedworker, worker} client types');
+
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/clients-matchall-exact-controller.https.html b/test/wpt/tests/service-workers/service-worker/clients-matchall-exact-controller.https.html
new file mode 100644
index 0000000..a61c8af
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/clients-matchall-exact-controller.https.html
@@ -0,0 +1,67 @@
+<!DOCTYPE html>
+<title>Service Worker: Clients.matchAll with exact controller</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+const scope = 'resources/blank.html?clients-matchAll';
+let frames = [];
+
+function checkWorkerClients(worker, expected) {
+ return new Promise((resolve, reject) => {
+ let channel = new MessageChannel();
+ channel.port1.onmessage = evt => {
+ try {
+ assert_equals(evt.data.length, expected.length);
+ for (let i = 0; i < expected.length; ++i) {
+ assert_array_equals(evt.data[i], expected[i]);
+ }
+ resolve();
+ } catch (e) {
+ reject(e);
+ }
+ };
+
+ worker.postMessage({port:channel.port2}, [channel.port2]);
+ });
+}
+
+let expected = [
+ // visibilityState, focused, url, type, frameType
+ ['visible', true, new URL(scope + '#1', location).toString(), 'window', 'nested'],
+ ['visible', false, new URL(scope + '#2', location).toString(), 'window', 'nested']
+];
+
+promise_test(t => {
+ let script = 'resources/clients-matchall-worker.js';
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(registration => {
+ t.add_cleanup(() => service_worker_unregister(t, scope));
+
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(_ => with_iframe(scope + '#1') )
+ .then(frame1 => {
+ frames.push(frame1);
+ frame1.focus();
+ return with_iframe(scope + '#2');
+ })
+ .then(frame2 => {
+ frames.push(frame2);
+ return navigator.serviceWorker.register(script + '?updated', { scope: scope });
+ })
+ .then(registration => {
+ return wait_for_state(t, registration.installing, 'installed')
+ .then(_ => registration);
+ })
+ .then(registration => {
+ return Promise.all([
+ checkWorkerClients(registration.waiting, []),
+ checkWorkerClients(registration.active, expected),
+ ]);
+ })
+ .then(_ => {
+ frames.forEach(f => f.remove() );
+ });
+}, 'Test Clients.matchAll() with exact controller');
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/clients-matchall-frozen.https.html b/test/wpt/tests/service-workers/service-worker/clients-matchall-frozen.https.html
new file mode 100644
index 0000000..479c28a
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/clients-matchall-frozen.https.html
@@ -0,0 +1,64 @@
+<!DOCTYPE html>
+<title>Service Worker: Clients.matchAll</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+var scope = 'resources/clients-frame-freeze.html';
+var windows = [];
+var expected_window_1 =
+ {visibilityState: 'visible', focused: false, lifecycleState: "frozen", url: new URL(scope + '#1', location).toString(), type: 'window', frameType: 'top-level'};
+var expected_window_2 =
+ {visibilityState: 'visible', focused: false, lifecycleState: "active", url: new URL(scope + '#2', location).toString(), type: 'window', frameType: 'top-level'};
+function with_window(url, name) {
+ return new Promise(function(resolve) {
+ var child = window.open(url, name);
+ window.onmessage = () => {resolve(child)};
+ });
+}
+
+promise_test(function(t) {
+ return service_worker_unregister_and_register(
+ t, 'resources/clients-matchall-worker.js', scope)
+ .then(function(registration) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() { return with_window(scope + '#1', 'Child 1'); })
+ .then(function(window1) {
+ windows.push(window1);
+ return with_window(scope + '#2', 'Child 2');
+ })
+ .then(function(window2) {
+ windows.push(window2);
+ return new Promise(function(resolve) {
+ window.onmessage = resolve;
+ windows[0].postMessage('freeze');
+ });
+ })
+ .then(function() {
+ var channel = new MessageChannel();
+
+ return new Promise(function(resolve) {
+ channel.port1.onmessage = resolve;
+ windows[1].navigator.serviceWorker.controller.postMessage(
+ {port:channel.port2, includeLifecycleState: true}, [channel.port2]);
+ });
+ })
+ .then(function(e) {
+ assert_equals(e.data.length, 2);
+ // No specific order is required, so support inversion.
+ if (e.data[0][3] == new URL(scope + '#2', location)) {
+ assert_object_equals(e.data[0], expected_window_2);
+ assert_object_equals(e.data[1], expected_window_1);
+ } else {
+ assert_object_equals(e.data[0], expected_window_1);
+ assert_object_equals(e.data[1], expected_window_2);
+ }
+ });
+}, 'Test Clients.matchAll()');
+
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/clients-matchall-include-uncontrolled.https.html b/test/wpt/tests/service-workers/service-worker/clients-matchall-include-uncontrolled.https.html
new file mode 100644
index 0000000..9f34e57
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/clients-matchall-include-uncontrolled.https.html
@@ -0,0 +1,117 @@
+<!DOCTYPE html>
+<title>Service Worker: Clients.matchAll with includeUncontrolled</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+function test_matchall(service_worker, expected, query_options) {
+ expected.sort((a, b) => a[2] > b[2] ? 1 : -1);
+ return new Promise((resolve, reject) => {
+ const channel = new MessageChannel();
+ channel.port1.onmessage = e => {
+ const data = e.data.filter(info => {
+ return info[2].indexOf('clients-matchall') > -1;
+ });
+ data.sort((a, b) => a[2] > b[2] ? 1 : -1);
+ assert_equals(data.length, expected.length);
+ for (let i = 0; i < data.length; i++)
+ assert_array_equals(data[i], expected[i]);
+ resolve();
+ };
+ service_worker.postMessage({port:channel.port2, options:query_options},
+ [channel.port2]);
+ });
+}
+
+// Run clients.matchAll without and with includeUncontrolled=true.
+// (We want to run the two tests sequentially in the same promise_test
+// so that we can use the same set of iframes without intefering each other.
+promise_test(async t => {
+ // |base_url| is out-of-scope.
+ const base_url = 'resources/blank.html?clients-matchall';
+ const scope = base_url + '-includeUncontrolled';
+
+ const registration =
+ await service_worker_unregister_and_register(
+ t, 'resources/clients-matchall-worker.js', scope);
+ t.add_cleanup(() => service_worker_unregister(t, scope));
+ const service_worker = registration.installing;
+ await wait_for_state(t, service_worker, 'activated');
+
+ // Creates 3 iframes, 2 for in-scope and 1 for out-of-scope.
+ let frames = [];
+ frames.push(await with_iframe(base_url));
+ frames.push(await with_iframe(scope + '#1'));
+ frames.push(await with_iframe(scope + '#2'));
+
+ // Make sure we have focus for '#2' frame and its parent window.
+ frames[2].focus();
+ frames[2].contentWindow.focus();
+
+ const expected_without_include_uncontrolled = [
+ // visibilityState, focused, url, type, frameType
+ ['visible', false, new URL(scope + '#1', location).toString(), 'window', 'nested'],
+ ['visible', true, new URL(scope + '#2', location).toString(), 'window', 'nested']
+ ];
+ const expected_with_include_uncontrolled = [
+ // visibilityState, focused, url, type, frameType
+ ['visible', true, location.href, 'window', 'top-level'],
+ ['visible', false, new URL(scope + '#1', location).toString(), 'window', 'nested'],
+ ['visible', true, new URL(scope + '#2', location).toString(), 'window', 'nested'],
+ ['visible', false, new URL(base_url, location).toString(), 'window', 'nested']
+ ];
+
+ await test_matchall(service_worker, expected_without_include_uncontrolled);
+ await test_matchall(service_worker, expected_with_include_uncontrolled,
+ { includeUncontrolled: true });
+}, 'Verify matchAll() with windows respect includeUncontrolled');
+
+// TODO: Add tests for clients.matchAll for dedicated workers.
+
+async function create_shared_worker(script_url) {
+ const shared_worker = new SharedWorker(script_url);
+ const msgEvent = await new Promise(r => shared_worker.port.onmessage = r);
+ assert_equals(msgEvent.data, 'started');
+ return shared_worker;
+}
+
+// Run clients.matchAll for shared workers without and with
+// includeUncontrolled=true.
+promise_test(async t => {
+ const script_url = 'resources/clients-matchall-client-types-shared-worker.js';
+ const uncontrolled_script_url =
+ new URL(script_url + '?uncontrolled', location).toString();
+ const controlled_script_url =
+ new URL(script_url + '?controlled', location).toString();
+
+ // Start a shared worker that is not controlled by a service worker.
+ const uncontrolled_shared_worker =
+ await create_shared_worker(uncontrolled_script_url);
+
+ // Register a service worker.
+ const registration =
+ await service_worker_unregister_and_register(
+ t, 'resources/clients-matchall-worker.js', script_url);
+ t.add_cleanup(() => service_worker_unregister(t, script_url));
+ const service_worker = registration.installing;
+ await wait_for_state(t, service_worker, 'activated');
+
+ // Start another shared worker controlled by the service worker.
+ await create_shared_worker(controlled_script_url);
+
+ const expected_without_include_uncontrolled = [
+ // visibilityState, focused, url, type, frameType
+ [undefined, undefined, controlled_script_url, 'sharedworker', 'none'],
+ ];
+ const expected_with_include_uncontrolled = [
+ // visibilityState, focused, url, type, frameType
+ [undefined, undefined, controlled_script_url, 'sharedworker', 'none'],
+ [undefined, undefined, uncontrolled_script_url, 'sharedworker', 'none'],
+ ];
+
+ await test_matchall(service_worker, expected_without_include_uncontrolled,
+ { type: 'sharedworker' });
+ await test_matchall(service_worker, expected_with_include_uncontrolled,
+ { includeUncontrolled: true, type: 'sharedworker' });
+}, 'Verify matchAll() with shared workers respect includeUncontrolled');
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/clients-matchall-on-evaluation.https.html b/test/wpt/tests/service-workers/service-worker/clients-matchall-on-evaluation.https.html
new file mode 100644
index 0000000..8705f85
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/clients-matchall-on-evaluation.https.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<title>Service Worker: Clients.matchAll on script evaluation</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+promise_test(function(t) {
+ var script = 'resources/clients-matchall-on-evaluation-worker.js';
+ var scope = 'resources/blank.html?clients-matchAll-on-evaluation';
+
+ var saw_message = new Promise(function(resolve) {
+ navigator.serviceWorker.onmessage = function(e) {
+ assert_equals(e.data, 'matched');
+ resolve();
+ };
+ });
+
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(function(registration) {
+ add_completion_callback(function() { registration.unregister(); });
+ return saw_message;
+ });
+ }, 'Test Clients.matchAll() on script evaluation');
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/clients-matchall-order.https.html b/test/wpt/tests/service-workers/service-worker/clients-matchall-order.https.html
new file mode 100644
index 0000000..ec650f2
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/clients-matchall-order.https.html
@@ -0,0 +1,427 @@
+<!DOCTYPE html>
+<title>Service Worker: Clients.matchAll ordering</title>
+<meta name=timeout content=long>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+
+// Utility function for URLs this test will open.
+function makeURL(name, num, type) {
+ let u = new URL('resources/empty.html', location);
+ u.searchParams.set('name', name);
+ if (num !== undefined) {
+ u.searchParams.set('q', num);
+ }
+ if (type === 'nested') {
+ u.searchParams.set('nested', true);
+ }
+ return u.href;
+}
+
+// Non-test URLs that will be open during each test. The harness URLs
+// are from the WPT harness. The "extra" URL is a final window opened
+// by the test.
+const EXTRA_URL = makeURL('extra');
+const TEST_HARNESS_URL = location.href;
+const TOP_HARNESS_URL = new URL('/testharness_runner.html', location).href;
+
+// Utility function to open an iframe in the target parent window. We
+// can't just use with_iframe() because it does not support a configurable
+// parent window.
+function openFrame(parentWindow, url) {
+ return new Promise(resolve => {
+ let frame = parentWindow.document.createElement('iframe');
+ frame.src = url;
+ parentWindow.document.body.appendChild(frame);
+
+ frame.contentWindow.addEventListener('load', evt => {
+ resolve(frame);
+ }, { once: true });
+ });
+}
+
+// Utility function to open a window and wait for it to load. The
+// window may optionally have a nested iframe as well. Returns
+// a result like `{ top: <frame ref> nested: <nested frame ref if present> }`.
+function openFrameConfig(opts) {
+ let url = new URL(opts.url, location.href);
+ return openFrame(window, url.href).then(top => {
+ if (!opts.withNested) {
+ return { top: top };
+ }
+
+ url.searchParams.set('nested', true);
+ return openFrame(top.contentWindow, url.href).then(nested => {
+ return { top: top, nested: nested };
+ });
+ });
+}
+
+// Utility function that takes a list of configurations and opens the
+// corresponding windows in sequence. An array of results is returned.
+function openFrameConfigList(optList) {
+ let resultList = [];
+ function openNextWindow(optList, nextWindow) {
+ if (nextWindow >= optList.length) {
+ return resultList;
+ }
+ return openFrameConfig(optList[nextWindow]).then(result => {
+ resultList.push(result);
+ return openNextWindow(optList, nextWindow + 1);
+ });
+ }
+ return openNextWindow(optList, 0);
+}
+
+// Utility function that focuses the given entry in window result list.
+function executeFocus(frameResultList, opts) {
+ return new Promise(resolve => {
+ let w = frameResultList[opts.index][opts.type];
+ let target = w.contentWindow ? w.contentWindow : w;
+ target.addEventListener('focus', evt => {
+ resolve();
+ }, { once: true });
+ target.focus();
+ });
+}
+
+// Utility function that performs a list of focus commands in sequence
+// based on the window result list.
+function executeFocusList(frameResultList, optList) {
+ function executeNextCommand(frameResultList, optList, nextCommand) {
+ if (nextCommand >= optList.length) {
+ return;
+ }
+ return executeFocus(frameResultList, optList[nextCommand]).then(_ => {
+ return executeNextCommand(frameResultList, optList, nextCommand + 1);
+ });
+ }
+ return executeNextCommand(frameResultList, optList, 0);
+}
+
+// Perform a `clients.matchAll()` in the service worker with the given
+// options dictionary.
+function doMatchAll(worker, options) {
+ return new Promise(resolve => {
+ let channel = new MessageChannel();
+ channel.port1.onmessage = evt => {
+ resolve(evt.data);
+ };
+ worker.postMessage({ port: channel.port2, options: options, disableSort: true },
+ [channel.port2]);
+ });
+}
+
+// Function that performs a single test case. It takes a configuration object
+// describing the windows to open, how to focus them, the matchAll options,
+// and the resulting expectations. See the test cases for examples of how to
+// use this.
+function matchAllOrderTest(t, opts) {
+ let script = 'resources/clients-matchall-worker.js';
+ let worker;
+ let frameResultList;
+ let extraWindowResult;
+ return service_worker_unregister_and_register(t, script, opts.scope).then(swr => {
+ t.add_cleanup(() => service_worker_unregister(t, opts.scope));
+
+ worker = swr.installing;
+ return wait_for_state(t, worker, 'activated');
+ }).then(_ => {
+ return openFrameConfigList(opts.frameConfigList);
+ }).then(results => {
+ frameResultList = results;
+ return openFrameConfig({ url: EXTRA_URL });
+ }).then(result => {
+ extraWindowResult = result;
+ return executeFocusList(frameResultList, opts.focusConfigList);
+ }).then(_ => {
+ return doMatchAll(worker, opts.matchAllOptions);
+ }).then(data => {
+ assert_equals(data.length, opts.expected.length);
+ for (let i = 0; i < data.length; ++i) {
+ assert_equals(data[i][2], opts.expected[i], 'expected URL index ' + i);
+ }
+ }).then(_ => {
+ frameResultList.forEach(result => result.top.remove());
+ extraWindowResult.top.remove();
+ }).catch(e => {
+ if (frameResultList) {
+ frameResultList.forEach(result => result.top.remove());
+ }
+ if (extraWindowResult) {
+ extraWindowResult.top.remove();
+ }
+ throw(e);
+ });
+}
+
+// ----------
+// Test cases
+// ----------
+
+promise_test(t => {
+ let name = 'no-focus-controlled-windows';
+ let opts = {
+ scope: makeURL(name),
+
+ frameConfigList: [
+ { url: makeURL(name, 0), withNested: false },
+ { url: makeURL(name, 1), withNested: false },
+ { url: makeURL(name, 2), withNested: false },
+ ],
+
+ focusConfigList: [
+ // no focus commands
+ ],
+
+ matchAllOptions: {
+ includeUncontrolled: false
+ },
+
+ expected: [
+ makeURL(name, 0),
+ makeURL(name, 1),
+ makeURL(name, 2),
+ ],
+ };
+
+ return matchAllOrderTest(t, opts);
+}, 'Clients.matchAll() returns non-focused controlled windows in creation order.');
+
+promise_test(t => {
+ let name = 'focus-controlled-windows-1';
+ let opts = {
+ scope: makeURL(name),
+
+ frameConfigList: [
+ { url: makeURL(name, 0), withNested: false },
+ { url: makeURL(name, 1), withNested: false },
+ { url: makeURL(name, 2), withNested: false },
+ ],
+
+ focusConfigList: [
+ { index: 0, type: 'top' },
+ { index: 1, type: 'top' },
+ { index: 2, type: 'top' },
+ ],
+
+ matchAllOptions: {
+ includeUncontrolled: false
+ },
+
+ expected: [
+ makeURL(name, 2),
+ makeURL(name, 1),
+ makeURL(name, 0),
+ ],
+ };
+
+ return matchAllOrderTest(t, opts);
+}, 'Clients.matchAll() returns controlled windows in focus order. Case 1.');
+
+promise_test(t => {
+ let name = 'focus-controlled-windows-2';
+ let opts = {
+ scope: makeURL(name),
+
+ frameConfigList: [
+ { url: makeURL(name, 0), withNested: false },
+ { url: makeURL(name, 1), withNested: false },
+ { url: makeURL(name, 2), withNested: false },
+ ],
+
+ focusConfigList: [
+ { index: 2, type: 'top' },
+ { index: 1, type: 'top' },
+ { index: 0, type: 'top' },
+ ],
+
+ matchAllOptions: {
+ includeUncontrolled: false
+ },
+
+ expected: [
+ makeURL(name, 0),
+ makeURL(name, 1),
+ makeURL(name, 2),
+ ],
+ };
+
+ return matchAllOrderTest(t, opts);
+}, 'Clients.matchAll() returns controlled windows in focus order. Case 2.');
+
+promise_test(t => {
+ let name = 'no-focus-uncontrolled-windows';
+ let opts = {
+ scope: makeURL(name + '-outofscope'),
+
+ frameConfigList: [
+ { url: makeURL(name, 0), withNested: false },
+ { url: makeURL(name, 1), withNested: false },
+ { url: makeURL(name, 2), withNested: false },
+ ],
+
+ focusConfigList: [
+ // no focus commands
+ ],
+
+ matchAllOptions: {
+ includeUncontrolled: true
+ },
+
+ expected: [
+ // The harness windows have been focused, so appear first
+ TEST_HARNESS_URL,
+ TOP_HARNESS_URL,
+
+ // Test frames have not been focused, so appear in creation order
+ makeURL(name, 0),
+ makeURL(name, 1),
+ makeURL(name, 2),
+ EXTRA_URL,
+ ],
+ };
+
+ return matchAllOrderTest(t, opts);
+}, 'Clients.matchAll() returns non-focused uncontrolled windows in creation order.');
+
+promise_test(t => {
+ let name = 'focus-uncontrolled-windows-1';
+ let opts = {
+ scope: makeURL(name + '-outofscope'),
+
+ frameConfigList: [
+ { url: makeURL(name, 0), withNested: false },
+ { url: makeURL(name, 1), withNested: false },
+ { url: makeURL(name, 2), withNested: false },
+ ],
+
+ focusConfigList: [
+ { index: 0, type: 'top' },
+ { index: 1, type: 'top' },
+ { index: 2, type: 'top' },
+ ],
+
+ matchAllOptions: {
+ includeUncontrolled: true
+ },
+
+ expected: [
+ // The test harness window is a parent of all test frames. It will
+ // always have the same focus time or later as its frames. So it
+ // appears first.
+ TEST_HARNESS_URL,
+
+ makeURL(name, 2),
+ makeURL(name, 1),
+ makeURL(name, 0),
+
+ // The overall harness has been focused
+ TOP_HARNESS_URL,
+
+ // The extra frame was never focused
+ EXTRA_URL,
+ ],
+ };
+
+ return matchAllOrderTest(t, opts);
+}, 'Clients.matchAll() returns uncontrolled windows in focus order. Case 1.');
+
+promise_test(t => {
+ let name = 'focus-uncontrolled-windows-2';
+ let opts = {
+ scope: makeURL(name + '-outofscope'),
+
+ frameConfigList: [
+ { url: makeURL(name, 0), withNested: false },
+ { url: makeURL(name, 1), withNested: false },
+ { url: makeURL(name, 2), withNested: false },
+ ],
+
+ focusConfigList: [
+ { index: 2, type: 'top' },
+ { index: 1, type: 'top' },
+ { index: 0, type: 'top' },
+ ],
+
+ matchAllOptions: {
+ includeUncontrolled: true
+ },
+
+ expected: [
+ // The test harness window is a parent of all test frames. It will
+ // always have the same focus time or later as its frames. So it
+ // appears first.
+ TEST_HARNESS_URL,
+
+ makeURL(name, 0),
+ makeURL(name, 1),
+ makeURL(name, 2),
+
+ // The overall harness has been focused
+ TOP_HARNESS_URL,
+
+ // The extra frame was never focused
+ EXTRA_URL,
+ ],
+ };
+
+ return matchAllOrderTest(t, opts);
+}, 'Clients.matchAll() returns uncontrolled windows in focus order. Case 2.');
+
+promise_test(t => {
+ let name = 'focus-controlled-nested-windows';
+ let opts = {
+ scope: makeURL(name),
+
+ frameConfigList: [
+ { url: makeURL(name, 0), withNested: true },
+ { url: makeURL(name, 1), withNested: true },
+ { url: makeURL(name, 2), withNested: true },
+ ],
+
+ focusConfigList: [
+ { index: 0, type: 'top' },
+
+ // Note, some browsers don't let programmatic focus of a frame unless
+ // an ancestor window is already focused. So focus the window and
+ // then the frame.
+ { index: 1, type: 'top' },
+ { index: 1, type: 'nested' },
+
+ { index: 2, type: 'top' },
+ ],
+
+ matchAllOptions: {
+ includeUncontrolled: false
+ },
+
+ expected: [
+ // Focus order for window 2, but not its frame. We only focused
+ // the window.
+ makeURL(name, 2),
+
+ // Window 1 is next via focus order, but the window is always
+ // shown first here. The window gets its last focus time updated
+ // when the frame is focused. Since the times match between the
+ // two it falls back to creation order. The window was created
+ // before the frame. This behavior is being discussed in:
+ // https://github.com/w3c/ServiceWorker/issues/1080
+ makeURL(name, 1),
+ makeURL(name, 1, 'nested'),
+
+ // Focus order for window 0, but not its frame. We only focused
+ // the window.
+ makeURL(name, 0),
+
+ // Creation order of the frames since they are not focused by
+ // default when they are created.
+ makeURL(name, 0, 'nested'),
+ makeURL(name, 2, 'nested'),
+ ],
+ };
+
+ return matchAllOrderTest(t, opts);
+}, 'Clients.matchAll() returns controlled windows and frames in focus order.');
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/clients-matchall.https.html b/test/wpt/tests/service-workers/service-worker/clients-matchall.https.html
new file mode 100644
index 0000000..ce44f19
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/clients-matchall.https.html
@@ -0,0 +1,50 @@
+<!DOCTYPE html>
+<title>Service Worker: Clients.matchAll</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+var scope = 'resources/blank.html?clients-matchAll';
+var frames = [];
+promise_test(function(t) {
+ return service_worker_unregister_and_register(
+ t, 'resources/clients-matchall-worker.js', scope)
+ .then(function(registration) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() { return with_iframe(scope + '#1'); })
+ .then(function(frame1) {
+ frames.push(frame1);
+ frame1.focus();
+ return with_iframe(scope + '#2');
+ })
+ .then(function(frame2) {
+ frames.push(frame2);
+ var channel = new MessageChannel();
+
+ return new Promise(function(resolve) {
+ channel.port1.onmessage = resolve;
+ frame2.contentWindow.navigator.serviceWorker.controller.postMessage(
+ {port:channel.port2}, [channel.port2]);
+ });
+ })
+ .then(onMessage);
+}, 'Test Clients.matchAll()');
+
+var expected = [
+ // visibilityState, focused, url, type, frameType
+ ['visible', true, new URL(scope + '#1', location).toString(), 'window', 'nested'],
+ ['visible', false, new URL(scope + '#2', location).toString(), 'window', 'nested']
+];
+
+function onMessage(e) {
+ assert_equals(e.data.length, 2);
+ assert_array_equals(e.data[0], expected[0]);
+ assert_array_equals(e.data[1], expected[1]);
+ frames.forEach(function(f) { f.remove(); });
+}
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/controlled-dedicatedworker-postMessage.https.html b/test/wpt/tests/service-workers/service-worker/controlled-dedicatedworker-postMessage.https.html
new file mode 100644
index 0000000..7e2a604
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/controlled-dedicatedworker-postMessage.https.html
@@ -0,0 +1,44 @@
+<html>
+<head>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+</head>
+<body>
+<script>
+promise_test(async (test) => {
+ const registration = await navigator.serviceWorker.register("postMessage-client-worker.js", { scope : 'resources' });
+ activeWorker = registration.active;
+ if (activeWorker)
+ return;
+
+ activeWorker = registration.installing;
+ await new Promise(resolve => {
+ activeWorker.addEventListener('statechange', () => {
+ if (activeWorker.state === "activated")
+ resolve();
+ });
+ });
+}, "Register service worker");
+
+promise_test(async (test) => {
+ const worker = new Worker('resources/controlled-worker-postMessage.js');
+ const event = await new Promise((resolve, reject) => {
+ test.step_timeout(() => reject("test timed out"), 3000);
+ worker.onmessage = resolve;
+ });
+ assert_equals(event.data, 0);
+}, "Verify dedicated worker gets messages if setting event listener early");
+
+promise_test(async (test) => {
+ const worker = new Worker('resources/controlled-worker-late-postMessage.js?repeatMessages');
+ const event = await new Promise((resolve, reject) => {
+ test.step_timeout(() => reject("test timed out"), 3000);
+ worker.onmessage = resolve;
+ });
+ assert_not_equals(event.data, "No message received");
+ assert_true(event.data > 0);
+}, "Verify dedicated worker does not get all messages if not setting event listener early");
+</script>
+</body>
+</html>
diff --git a/test/wpt/tests/service-workers/service-worker/controlled-iframe-postMessage.https.html b/test/wpt/tests/service-workers/service-worker/controlled-iframe-postMessage.https.html
new file mode 100644
index 0000000..8f39b7f
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/controlled-iframe-postMessage.https.html
@@ -0,0 +1,67 @@
+<html>
+<head>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+</head>
+<body>
+<script>
+promise_test(async (test) => {
+ const registration = await navigator.serviceWorker.register("postMessage-client-worker.js", { scope : 'resources' });
+ activeWorker = registration.active;
+ if (activeWorker)
+ return;
+
+ activeWorker = registration.installing;
+ await new Promise(resolve => {
+ activeWorker.addEventListener('statechange', () => {
+ if (activeWorker.state === "activated")
+ resolve();
+ });
+ });
+}, "Register service worker");
+
+function with_iframe(test, url) {
+ return new Promise(function(resolve, reject) {
+ test.step_timeout(() => reject("with_iframe timed out"), 2000);
+ var frame = document.createElement('iframe');
+ frame.className = 'test-iframe';
+ frame.src = url;
+ frame.onload = function() { resolve(frame); };
+ document.body.appendChild(frame);
+ });
+}
+
+promise_test(async (test) => {
+ const frame = await with_iframe(test, 'resources/controlled-frame-postMessage.html');
+ let counter = 0;
+ while (counter++ < 100 && frame.contentWindow.messageData == undefined)
+ await new Promise(resolve => test.step_timeout(resolve, 50));
+ assert_equals(frame.contentWindow.messageData, 0);
+ frame.remove();
+}, "Verify frame gets early messages if setting synchronously message event listener");
+
+promise_test(async (test) => {
+ const frame = await with_iframe(test, 'resources/controlled-frame-postMessage.html?repeatMessages');
+ let counter = 0;
+ while (counter++ < 100 && frame.contentWindow.messageData == undefined)
+ await new Promise(resolve => test.step_timeout(resolve, 50));
+ assert_not_equals(frame.contentWindow.messageData, 0);
+ frame.remove();
+}, "Verify frame does not get all messages if not setting event listener early");
+
+promise_test(async (test) => {
+ const frame = await with_iframe(test, 'resources/controlled-frame-postMessage.html?repeatMessages&listener');
+ let counter = 0;
+ while (counter++ < 100 && frame.contentWindow.messageData.length < 5)
+ await new Promise(resolve => test.step_timeout(resolve, 50));
+
+ assert_less_than(counter, 100);
+ data = frame.contentWindow.messageData;
+ for (let cptr = 1; cptr < data.length; cptr++)
+ assert_true(data[cptr - 1] < data[cptr]);
+ frame.remove();
+}, "Verify frame does get messages in order");
+</script>
+</body>
+</html>
diff --git a/test/wpt/tests/service-workers/service-worker/controller-on-disconnect.https.html b/test/wpt/tests/service-workers/service-worker/controller-on-disconnect.https.html
new file mode 100644
index 0000000..f23dfe7
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/controller-on-disconnect.https.html
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<title>Service Worker: Controller on load</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+promise_test(function(t) {
+ var url = 'resources/empty-worker.js';
+ var scope = 'resources/blank.html';
+ var registration;
+ var controller;
+ var frame;
+ return service_worker_unregister_and_register(t, url, scope)
+ .then(function(swr) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ registration = swr;
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() {
+ return with_iframe(scope)
+ })
+ .then(function(f) {
+ frame = f;
+ var w = frame.contentWindow;
+ var swc = w.navigator.serviceWorker;
+ assert_true(swc.controller instanceof w.ServiceWorker,
+ 'controller should be a ServiceWorker object');
+
+ frame.remove();
+
+ assert_equals(swc.controller, null,
+ 'disconnected frame should not be controlled');
+ });
+}, 'controller is cleared on disconnected window');
+</script>
+</body>
diff --git a/test/wpt/tests/service-workers/service-worker/controller-on-load.https.html b/test/wpt/tests/service-workers/service-worker/controller-on-load.https.html
new file mode 100644
index 0000000..e4c5e5f
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/controller-on-load.https.html
@@ -0,0 +1,46 @@
+<!DOCTYPE html>
+<title>Service Worker: Controller on load</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+promise_test(function(t) {
+ var url = 'resources/empty-worker.js';
+ var scope = 'resources/blank.html';
+ var registration;
+ var controller;
+ var frame;
+ return service_worker_unregister_and_register(t, url, scope)
+ .then(function(swr) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ registration = swr;
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() {
+ return with_iframe(scope);
+ })
+ .then(function(f) {
+ frame = f;
+ var w = frame.contentWindow;
+ controller = w.navigator.serviceWorker.controller;
+ assert_true(controller instanceof w.ServiceWorker,
+ 'controller should be a ServiceWorker object');
+ assert_equals(controller.scriptURL, normalizeURL(url));
+
+ // objects from different windows should not be equal
+ assert_not_equals(controller, registration.active);
+
+ return w.navigator.serviceWorker.getRegistration();
+ })
+ .then(function(frameRegistration) {
+ // SW objects from same window should be equal
+ assert_equals(frameRegistration.active, controller);
+ frame.remove();
+ });
+}, 'controller is set for a controlled document');
+</script>
+</body>
diff --git a/test/wpt/tests/service-workers/service-worker/controller-on-reload.https.html b/test/wpt/tests/service-workers/service-worker/controller-on-reload.https.html
new file mode 100644
index 0000000..2e966d4
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/controller-on-reload.https.html
@@ -0,0 +1,58 @@
+<!DOCTYPE html>
+<title>Service Worker: Controller on reload</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+promise_test(function(t) {
+ const iframe_scope = 'blank.html';
+ const scope = 'resources/' + iframe_scope;
+ var frame;
+ var registration;
+ var controller;
+ return service_worker_unregister(t, scope)
+ .then(function() {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ return with_iframe(scope);
+ })
+ .then(function(f) {
+ frame = f;
+ return frame.contentWindow.navigator.serviceWorker.register(
+ 'empty-worker.js', {scope: iframe_scope});
+ })
+ .then(function(swr) {
+ registration = swr;
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() {
+ var w = frame.contentWindow;
+ assert_equals(w.navigator.serviceWorker.controller, null,
+ 'controller should be null until the document is ' +
+ 'reloaded');
+ return new Promise(function(resolve) {
+ frame.onload = function() { resolve(); }
+ w.location.reload();
+ });
+ })
+ .then(function() {
+ var w = frame.contentWindow;
+ controller = w.navigator.serviceWorker.controller;
+ assert_true(controller instanceof w.ServiceWorker,
+ 'controller should be a ServiceWorker object upon reload');
+
+ // objects from separate windows should not be equal
+ assert_not_equals(controller, registration.active);
+
+ return w.navigator.serviceWorker.getRegistration(iframe_scope);
+ })
+ .then(function(frameRegistration) {
+ assert_equals(frameRegistration.active, controller);
+ frame.remove();
+ });
+ }, 'controller is set upon reload after registration');
+</script>
+</body>
diff --git a/test/wpt/tests/service-workers/service-worker/controller-with-no-fetch-event-handler.https.html b/test/wpt/tests/service-workers/service-worker/controller-with-no-fetch-event-handler.https.html
new file mode 100644
index 0000000..d947139
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/controller-with-no-fetch-event-handler.https.html
@@ -0,0 +1,56 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Service Worker: controller without a fetch event handler</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js?pipe=sub"></script>
+<body>
+<script>
+let registration;
+let frame;
+const host_info = get_host_info();
+const remote_base_url =
+ new URL(`${host_info.HTTPS_REMOTE_ORIGIN}${base_path()}resources/`);
+
+promise_test(async t => {
+ const script = 'resources/empty.js'
+ const scope = 'resources/';
+
+ promise_test(async t => {
+ if (frame)
+ frame.remove();
+
+ if (registration)
+ await registration.unregister();
+ }, 'cleanup global state');
+
+ registration = await
+ service_worker_unregister_and_register(t, script, scope);
+ await wait_for_state(t, registration.installing, 'activated');
+ frame = await with_iframe(scope + 'blank.html');
+}, 'global setup');
+
+promise_test(async t => {
+ const url = new URL('cors-approved.txt', remote_base_url);
+ const response = await frame.contentWindow.fetch(url, {mode:'no-cors'});
+ const text = await response.text();
+ assert_equals(text, '');
+}, 'cross-origin request, no-cors mode');
+
+
+promise_test(async t => {
+ const url = new URL('cors-denied.txt', remote_base_url);
+ const response = frame.contentWindow.fetch(url);
+ await promise_rejects_js(t, frame.contentWindow.TypeError, response);
+}, 'cross-origin request, cors denied');
+
+promise_test(async t => {
+ const url = new URL('cors-approved.txt', remote_base_url);
+ response = await frame.contentWindow.fetch(url);
+ let text = await response.text();
+ text = text.trim();
+ assert_equals(text, 'plaintext');
+}, 'cross-origin request, cors approved');
+</script>
+</body>
diff --git a/test/wpt/tests/service-workers/service-worker/credentials.https.html b/test/wpt/tests/service-workers/service-worker/credentials.https.html
new file mode 100644
index 0000000..0a90dc2
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/credentials.https.html
@@ -0,0 +1,100 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Credentials for service worker scripts</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/cookies/resources/cookie-helper.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+// Check if the service worker's script has appropriate credentials for a new
+// worker and byte-for-byte checking.
+
+const SCOPE = 'resources/in-scope';
+const COOKIE_NAME = `service-worker-credentials-${Math.random()}`;
+
+promise_test(async t => {
+ // Set-Cookies for path=/.
+ await fetch(
+ `/cookies/resources/set-cookie.py?name=${COOKIE_NAME}&path=%2F`);
+}, 'Set cookies as initialization');
+
+async function get_cookies(worker) {
+ worker.postMessage('get cookie');
+ const message = await new Promise(resolve =>
+ navigator.serviceWorker.addEventListener('message', resolve));
+ return message.data;
+}
+
+promise_test(async t => {
+ const key = token();
+ const registration = await service_worker_unregister_and_register(
+ t, `resources/echo-cookie-worker.py?key=${key}`, SCOPE);
+ t.add_cleanup(() => registration.unregister());
+ const worker = registration.installing;
+
+ const cookies = await get_cookies(worker);
+ assert_equals(cookies[COOKIE_NAME], '1', 'new worker has credentials');
+
+ await registration.update();
+ const updated_worker = registration.installing;
+ const updated_cookies = await get_cookies(updated_worker);
+ assert_equals(updated_cookies[COOKIE_NAME], '1',
+ 'updated worker has credentials');
+}, 'Main script should have credentials');
+
+promise_test(async t => {
+ const key = token();
+ const registration = await service_worker_unregister_and_register(
+ t, `resources/import-echo-cookie-worker.js?key=${key}`, SCOPE);
+ t.add_cleanup(() => registration.unregister());
+ const worker = registration.installing;
+
+ const cookies = await get_cookies(worker);
+ assert_equals(cookies[COOKIE_NAME], '1', 'new worker has credentials');
+
+ await registration.update();
+ const updated_worker = registration.installing;
+ const updated_cookies = await get_cookies(updated_worker);
+ assert_equals(updated_cookies[COOKIE_NAME], '1',
+ 'updated worker has credentials');
+}, 'Imported script should have credentials');
+
+promise_test(async t => {
+ const key = token();
+ const registration = await service_worker_unregister_and_register(
+ t, `resources/import-echo-cookie-worker-module.py?key=${key}`, SCOPE, {type: 'module'});
+ t.add_cleanup(() => registration.unregister());
+ const worker = registration.installing;
+
+ const cookies = await get_cookies(worker);
+ assert_equals(cookies[COOKIE_NAME], undefined, 'new module worker should not have credentials');
+
+ await registration.update();
+ const updated_worker = registration.installing;
+ const updated_cookies = await get_cookies(updated_worker);
+ assert_equals(updated_cookies[COOKIE_NAME], undefined,
+ 'updated worker should not have credentials');
+}, 'Module with an imported statement should not have credentials');
+
+promise_test(async t => {
+ const key = token();
+ const registration = await service_worker_unregister_and_register(
+t, `resources/echo-cookie-worker.py?key=${key}`, SCOPE, {type: 'module'});
+ t.add_cleanup(() => registration.unregister());
+ const worker = registration.installing;
+
+ const cookies = await get_cookies(worker);
+ assert_equals(cookies[COOKIE_NAME], undefined, 'new module worker should not have credentials');
+
+ await registration.update();
+ const updated_worker = registration.installing;
+ const updated_cookies = await get_cookies(updated_worker);
+ assert_equals(updated_cookies[COOKIE_NAME], undefined,
+ 'updated worker should not have credentials');
+}, 'Script with service worker served as modules should not have credentials');
+
+</script>
+</body>
diff --git a/test/wpt/tests/service-workers/service-worker/data-iframe.html b/test/wpt/tests/service-workers/service-worker/data-iframe.html
new file mode 100644
index 0000000..d767d57
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/data-iframe.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<title>Service Workers in data iframes</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body></body>
+<script>
+'use strict';
+
+promise_test(t => {
+ const url = encodeURI(`data:text/html,<!DOCTYPE html>
+ <script>
+ parent.postMessage({ isDefined: 'serviceWorker' in navigator }, '*');
+ </` + `script>`);
+ var p = new Promise((resolve, reject) => {
+ window.addEventListener('message', event => {
+ resolve(event.data.isDefined);
+ });
+ });
+ with_iframe(url);
+ return p.then(isDefined => {
+ assert_false(isDefined, 'navigator.serviceWorker should not be defined in iframe');
+ });
+}, 'navigator.serviceWorker is not available in a data: iframe');
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/data-transfer-files.https.html b/test/wpt/tests/service-workers/service-worker/data-transfer-files.https.html
new file mode 100644
index 0000000..c503a28
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/data-transfer-files.https.html
@@ -0,0 +1,41 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Post a file in a navigation controlled by a service worker</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<iframe id=testframe name=testframe></iframe>
+<form id=testform method=post action="/html/semantics/forms/form-submission-0/resources/file-submission.py" target=testframe enctype="multipart/form-data">
+<input name=testinput id=testinput type=file>
+</form>
+<script>
+// Test that DataTransfer with a File entry works when posted to a
+// service worker that falls back to network. Regression test for
+// https://crbug.com/944145.
+promise_test(async (t) => {
+ const scope = '/html/semantics/forms/form-submission-0/resources/';
+ const header = `pipe=header(Service-Worker-Allowed,${scope})`;
+ const script = `resources/fetch-event-network-fallback-worker.js?${header}`;
+
+ const registration = await service_worker_unregister_and_register(
+ t, script, scope);
+ await wait_for_state(t, registration.installing, 'activated');
+
+ const dataTransfer = new DataTransfer();
+ dataTransfer.items.add(new File(['foobar'], 'name'));
+ assert_equals(1, dataTransfer.files.length);
+
+ testinput.files = dataTransfer.files;
+ testform.submit();
+
+ const data = await new Promise(resolve => {
+ onmessage = e => {
+ if (e.source !== testframe) return;
+ resolve(e.data);
+ };
+ });
+ assert_equals(data, "foobar");
+}, 'Posting a File in a navigation handled by a service worker');
+</script>
+</body>
diff --git a/test/wpt/tests/service-workers/service-worker/dedicated-worker-service-worker-interception.https.html b/test/wpt/tests/service-workers/service-worker/dedicated-worker-service-worker-interception.https.html
new file mode 100644
index 0000000..2144f48
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/dedicated-worker-service-worker-interception.https.html
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<title>DedicatedWorker: ServiceWorker interception</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+
+// Note that Chrome cannot pass these tests because of https://crbug.com/731599.
+
+function service_worker_interception_test(url, description) {
+ promise_test(async t => {
+ // Register a service worker whose scope includes |url|.
+ const kServiceWorkerScriptURL =
+ 'resources/service-worker-interception-service-worker.js';
+ const registration = await service_worker_unregister_and_register(
+ t, kServiceWorkerScriptURL, url);
+ add_result_callback(() => registration.unregister());
+ await wait_for_state(t, registration.installing, 'activated');
+
+ // Start a dedicated worker for |url|. The top-level script request and any
+ // module imports should be intercepted by the service worker.
+ const worker = new Worker(url, { type: 'module' });
+ const msg_event = await new Promise(resolve => worker.onmessage = resolve);
+ assert_equals(msg_event.data, 'LOADED_FROM_SERVICE_WORKER');
+ }, description);
+}
+
+service_worker_interception_test(
+ 'resources/service-worker-interception-network-worker.js',
+ 'Top-level module loading should be intercepted by a service worker.');
+
+service_worker_interception_test(
+ 'resources/service-worker-interception-static-import-worker.js',
+ 'Static import should be intercepted by a service worker.');
+
+service_worker_interception_test(
+ 'resources/service-worker-interception-dynamic-import-worker.js',
+ 'Dynamic import should be intercepted by a service worker.');
+
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/detached-context.https.html b/test/wpt/tests/service-workers/service-worker/detached-context.https.html
new file mode 100644
index 0000000..ce8e4cc
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/detached-context.https.html
@@ -0,0 +1,124 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Service WorkerRegistration from a removed iframe</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+</body>
+<script>
+// NOTE: This file tests corner case behavior that might not be defined in the
+// spec. See https://github.com/w3c/ServiceWorker/issues/1221
+
+promise_test(t => {
+ const url = 'resources/blank.html';
+ const scope_for_iframe = 'removed-registration'
+ const scope_for_main = 'resources/' + scope_for_iframe;
+ const script = 'resources/empty-worker.js';
+ let frame;
+ let resolvedCount = 0;
+
+ return service_worker_unregister(t, scope_for_main)
+ .then(() => {
+ return with_iframe(url);
+ })
+ .then(f => {
+ frame = f;
+ return navigator.serviceWorker.register(script,
+ {scope: scope_for_main});
+ })
+ .then(r => {
+ add_completion_callback(() => { r.unregister(); });
+ return wait_for_state(t, r.installing, 'activated');
+ })
+ .then(() => {
+ return frame.contentWindow.navigator.serviceWorker.getRegistration(
+ scope_for_iframe);
+ })
+ .then(r => {
+ frame.remove();
+ assert_equals(r.installing, null);
+ assert_equals(r.waiting, null);
+ assert_equals(r.active.state, 'activated');
+ assert_equals(r.scope, normalizeURL(scope_for_main));
+ r.onupdatefound = () => { /* empty */ };
+
+ // We want to verify that unregister() and update() do not
+ // resolve on a detached registration. We can't check for
+ // an explicit rejection, though, because not all browsers
+ // fire rejection callbacks on detached promises. Instead
+ // we wait for a sample scope to install, activate, and
+ // unregister before declaring that the promises did not
+ // resolve.
+ r.unregister().then(() => resolvedCount += 1,
+ () => {});
+ r.update().then(() => resolvedCount += 1,
+ () => {});
+ return wait_for_activation_on_sample_scope(t, window);
+ })
+ .then(() => {
+ assert_equals(resolvedCount, 0,
+ 'methods called on a detached registration should not resolve');
+ frame.remove();
+ })
+ }, 'accessing a ServiceWorkerRegistration from a removed iframe');
+
+promise_test(t => {
+ const script = 'resources/empty-worker.js';
+ const scope = 'resources/scope/serviceworker-from-detached';
+
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(registration => {
+ add_completion_callback(() => { registration.unregister(); });
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(() => { return with_iframe(scope); })
+ .then(frame => {
+ const worker = frame.contentWindow.navigator.serviceWorker.controller;
+ const ctor = frame.contentWindow.DOMException;
+ frame.remove();
+ assert_equals(worker.scriptURL, normalizeURL(script));
+ assert_equals(worker.state, 'activated');
+ worker.onstatechange = () => { /* empty */ };
+ assert_throws_dom(
+ 'InvalidStateError',
+ ctor,
+ () => { worker.postMessage(''); },
+ 'postMessage on a detached client should throw an exception.');
+ });
+ }, 'accessing a ServiceWorker object from a removed iframe');
+
+promise_test(t => {
+ const iframe = document.createElement('iframe');
+ iframe.src = 'resources/blank.html';
+ document.body.appendChild(iframe);
+ const f = iframe.contentWindow.Function;
+ function get_navigator() {
+ return f('return navigator')();
+ }
+ return new Promise(resolve => {
+ assert_equals(iframe.contentWindow.navigator, get_navigator());
+ iframe.src = 'resources/blank.html?navigate-to-new-url';
+ iframe.onload = resolve;
+ }).then(function() {
+ assert_not_equals(get_navigator().serviceWorker, null);
+ assert_equals(
+ get_navigator().serviceWorker,
+ iframe.contentWindow.navigator.serviceWorker);
+ iframe.remove();
+ });
+ }, 'accessing navigator.serviceWorker on a detached iframe');
+
+test(t => {
+ const iframe = document.createElement('iframe');
+ iframe.src = 'resources/blank.html';
+ document.body.appendChild(iframe);
+ const f = iframe.contentWindow.Function;
+ function get_navigator() {
+ return f('return navigator')();
+ }
+ assert_not_equals(get_navigator().serviceWorker, null);
+ iframe.remove();
+ assert_not_equals(get_navigator().serviceWorker, null);
+ }, 'accessing navigator on a removed frame');
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/embed-and-object-are-not-intercepted.https.html b/test/wpt/tests/service-workers/service-worker/embed-and-object-are-not-intercepted.https.html
new file mode 100644
index 0000000..581dbec
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/embed-and-object-are-not-intercepted.https.html
@@ -0,0 +1,104 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>embed and object are not intercepted</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js?pipe=sub"></script>
+<body>
+<script>
+let registration;
+
+const kScript = 'resources/embed-and-object-are-not-intercepted-worker.js';
+const kScope = 'resources/';
+
+promise_test(t => {
+ return service_worker_unregister_and_register(t, kScript, kScope)
+ .then(registration => {
+ promise_test(() => {
+ return registration.unregister();
+ }, 'restore global state');
+
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ }, 'initialize global state');
+
+promise_test(t => {
+ let frame;
+ return with_iframe('resources/embed-is-not-intercepted-iframe.html')
+ .then(f => {
+ frame = f;
+ t.add_cleanup(() => { frame.remove(); });
+ return frame.contentWindow.test_promise;
+ })
+ .then(result => {
+ assert_equals(result, 'request for embedded content was not intercepted');
+ });
+ }, 'requests for EMBED elements of embedded HTML content should not be intercepted by service workers');
+
+promise_test(t => {
+ let frame;
+ return with_iframe('resources/object-is-not-intercepted-iframe.html')
+ .then(f => {
+ frame = f;
+ t.add_cleanup(() => { frame.remove(); });
+ return frame.contentWindow.test_promise;
+ })
+ .then(result => {
+ assert_equals(result, 'request for embedded content was not intercepted');
+ });
+ }, 'requests for OBJECT elements of embedded HTML content should not be intercepted by service workers');
+
+promise_test(t => {
+ let frame;
+ return with_iframe('resources/embed-image-is-not-intercepted-iframe.html')
+ .then(f => {
+ frame = f;
+ t.add_cleanup(() => { frame.remove(); });
+ return frame.contentWindow.test_promise;
+ })
+ .then(result => {
+ assert_equals(result, 'request was not intercepted');
+ });
+ }, 'requests for EMBED elements of an image should not be intercepted by service workers');
+
+promise_test(t => {
+ let frame;
+ return with_iframe('resources/object-image-is-not-intercepted-iframe.html')
+ .then(f => {
+ frame = f;
+ t.add_cleanup(() => { frame.remove(); });
+ return frame.contentWindow.test_promise;
+ })
+ .then(result => {
+ assert_equals(result, 'request was not intercepted');
+ });
+ }, 'requests for OBJECT elements of an image should not be intercepted by service workers');
+
+promise_test(t => {
+ let frame;
+ return with_iframe('resources/object-navigation-is-not-intercepted-iframe.html')
+ .then(f => {
+ frame = f;
+ t.add_cleanup(() => { frame.remove(); });
+ return frame.contentWindow.test_promise;
+ })
+ .then(result => {
+ assert_equals(result, 'request for embedded content was not intercepted');
+ });
+ }, 'post-load navigation of OBJECT elements should not be intercepted by service workers');
+
+
+promise_test(t => {
+ let frame;
+ return with_iframe('resources/embed-navigation-is-not-intercepted-iframe.html')
+ .then(f => {
+ frame = f;
+ t.add_cleanup(() => { frame.remove(); });
+ return frame.contentWindow.test_promise;
+ })
+ .then(result => {
+ assert_equals(result, 'request for embedded content was not intercepted');
+ });
+ }, 'post-load navigation of EMBED elements should not be intercepted by service workers');
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/extendable-event-async-waituntil.https.html b/test/wpt/tests/service-workers/service-worker/extendable-event-async-waituntil.https.html
new file mode 100644
index 0000000..04e9826
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/extendable-event-async-waituntil.https.html
@@ -0,0 +1,120 @@
+<!DOCTYPE html>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="resources/testharness-helpers.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+
+function sync_message(worker, message, transfer) {
+ let wait = new Promise((res, rej) => {
+ navigator.serviceWorker.addEventListener('message', function(e) {
+ if (e.data === 'ACK') {
+ res();
+ } else {
+ rej();
+ }
+ });
+ });
+ worker.postMessage(message, transfer);
+ return wait;
+}
+
+function runTest(test, step, testBody) {
+ var scope = './resources/' + step;
+ var script = 'resources/extendable-event-async-waituntil.js?' + scope;
+ return service_worker_unregister_and_register(test, script, scope)
+ .then(function(registration) {
+ test.add_cleanup(function() {
+ return service_worker_unregister(test, scope);
+ });
+
+ let worker = registration.installing;
+ var channel = new MessageChannel();
+ var saw_message = new Promise(function(resolve) {
+ channel.port1.onmessage = function(e) { resolve(e.data); }
+ });
+
+ return wait_for_state(test, worker, 'activated')
+ .then(function() {
+ return sync_message(worker, { step: 'init', port: channel.port2 },
+ [channel.port2]);
+ })
+ .then(function() { return testBody(worker); })
+ .then(function() { return saw_message; })
+ .then(function(output) {
+ assert_equals(output.result, output.expected);
+ })
+ .then(function() { return sync_message(worker, { step: 'done' }); });
+ });
+}
+
+function msg_event_test(scope, test) {
+ var testBody = function(worker) {
+ return sync_message(worker, { step: scope });
+ };
+ return runTest(test, scope, testBody);
+}
+
+promise_test(msg_event_test.bind(this, 'no-current-extension-different-task'),
+ 'Test calling waitUntil in a task at the end of the event handler without an existing extension throws');
+
+promise_test(msg_event_test.bind(this, 'no-current-extension-different-microtask'),
+ 'Test calling waitUntil in a microtask at the end of the event handler without an existing extension suceeds');
+
+promise_test(msg_event_test.bind(this, 'current-extension-different-task'),
+ 'Test calling waitUntil in a different task an existing extension succeeds');
+
+promise_test(msg_event_test.bind(this, 'during-event-dispatch-current-extension-expired-same-microtask-turn'),
+ 'Test calling waitUntil at the end of an existing extension promise handler succeeds (event is still being dispatched)');
+
+promise_test(msg_event_test.bind(this, 'during-event-dispatch-current-extension-expired-same-microtask-turn-extra'),
+ 'Test calling waitUntil in a microtask at the end of an existing extension promise handler succeeds (event is still being dispatched)');
+
+promise_test(msg_event_test.bind(this, 'after-event-dispatch-current-extension-expired-same-microtask-turn'),
+ 'Test calling waitUntil in an existing extension promise handler succeeds (event is not being dispatched)');
+
+promise_test(msg_event_test.bind(this, 'after-event-dispatch-current-extension-expired-same-microtask-turn-extra'),
+ 'Test calling waitUntil in a microtask at the end of an existing extension promise handler throws (event is not being dispatched)');
+
+promise_test(msg_event_test.bind(this, 'current-extension-expired-different-task'),
+ 'Test calling waitUntil after the current extension expired in a different task fails');
+
+promise_test(msg_event_test.bind(this, 'script-extendable-event'),
+ 'Test calling waitUntil on a script constructed ExtendableEvent throws exception');
+
+promise_test(function(t) {
+ var testBody = function(worker) {
+ return with_iframe('./resources/pending-respondwith-async-waituntil');
+ }
+ return runTest(t, 'pending-respondwith-async-waituntil', testBody);
+ }, 'Test calling waitUntil asynchronously with pending respondWith promise.');
+
+promise_test(function(t) {
+ var testBody = function(worker) {
+ return with_iframe('./resources/during-event-dispatch-respondwith-microtask-sync-waituntil');
+ }
+ return runTest(t, 'during-event-dispatch-respondwith-microtask-sync-waituntil', testBody);
+ }, 'Test calling waitUntil synchronously inside microtask of respondWith promise (event is being dispatched).');
+
+promise_test(function(t) {
+ var testBody = function(worker) {
+ return with_iframe('./resources/during-event-dispatch-respondwith-microtask-async-waituntil');
+ }
+ return runTest(t, 'during-event-dispatch-respondwith-microtask-async-waituntil', testBody);
+ }, 'Test calling waitUntil asynchronously inside microtask of respondWith promise (event is being dispatched).');
+
+promise_test(function(t) {
+ var testBody = function(worker) {
+ return with_iframe('./resources/after-event-dispatch-respondwith-microtask-sync-waituntil');
+ }
+ return runTest(t, 'after-event-dispatch-respondwith-microtask-sync-waituntil', testBody);
+ }, 'Test calling waitUntil synchronously inside microtask of respondWith promise (event is not being dispatched).');
+
+promise_test(function(t) {
+ var testBody = function(worker) {
+ return with_iframe('./resources/after-event-dispatch-respondwith-microtask-async-waituntil');
+ }
+ return runTest(t, 'after-event-dispatch-respondwith-microtask-async-waituntil', testBody);
+ }, 'Test calling waitUntil asynchronously inside microtask of respondWith promise (event is not being dispatched).');
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/extendable-event-waituntil.https.html b/test/wpt/tests/service-workers/service-worker/extendable-event-waituntil.https.html
new file mode 100644
index 0000000..33b4eac
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/extendable-event-waituntil.https.html
@@ -0,0 +1,140 @@
+<!DOCTYPE html>
+<title>ExtendableEvent: waitUntil</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+function runTest(test, scope, onRegister) {
+ var script = 'resources/extendable-event-waituntil.js?' + scope;
+ return service_worker_unregister_and_register(test, script, scope)
+ .then(function(registration) {
+ test.add_cleanup(function() {
+ return service_worker_unregister(test, scope);
+ });
+
+ return onRegister(registration.installing);
+ });
+}
+
+// Sends a SYN to the worker and asynchronously listens for an ACK; sets
+// |obj.synced| to true once ack'd.
+function syncWorker(worker, obj) {
+ var channel = new MessageChannel();
+ worker.postMessage({port: channel.port2}, [channel.port2]);
+ return new Promise(function(resolve) {
+ channel.port1.onmessage = resolve;
+ }).then(function(e) {
+ var message = e.data;
+ assert_equals(message, 'SYNC',
+ 'Should receive sync message from worker.');
+ obj.synced = true;
+ channel.port1.postMessage('ACK');
+ });
+}
+
+promise_test(function(t) {
+ // Passing scope as the test switch for worker script.
+ var scope = 'resources/install-fulfilled';
+ var onRegister = function(worker) {
+ var obj = {};
+
+ return Promise.all([
+ syncWorker(worker, obj),
+ wait_for_state(t, worker, 'installed')
+ ]).then(function() {
+ assert_true(
+ obj.synced,
+ 'state should be "installed" after the waitUntil promise ' +
+ 'for "oninstall" is fulfilled.');
+ service_worker_unregister_and_done(t, scope);
+ });
+ };
+ return runTest(t, scope, onRegister);
+ }, 'Test install event waitUntil fulfilled');
+
+promise_test(function(t) {
+ var scope = 'resources/install-multiple-fulfilled';
+ var onRegister = function(worker) {
+ var obj1 = {};
+ var obj2 = {};
+
+ return Promise.all([
+ syncWorker(worker, obj1),
+ syncWorker(worker, obj2),
+ wait_for_state(t, worker, 'installed')
+ ]).then(function() {
+ assert_true(
+ obj1.synced && obj2.synced,
+ 'state should be "installed" after all waitUntil promises ' +
+ 'for "oninstall" are fulfilled.');
+ });
+ };
+ return runTest(t, scope, onRegister);
+ }, 'Test ExtendableEvent multiple waitUntil fulfilled.');
+
+promise_test(function(t) {
+ var scope = 'resources/install-reject-precedence';
+ var onRegister = function(worker) {
+ var obj1 = {};
+ var obj2 = {};
+
+ return Promise.all([
+ syncWorker(worker, obj1)
+ .then(function() {
+ syncWorker(worker, obj2);
+ }),
+ wait_for_state(t, worker, 'redundant')
+ ]).then(function() {
+ assert_true(
+ obj1.synced,
+ 'The "redundant" state was entered after the first "extend ' +
+ 'lifetime promise" resolved.'
+ );
+ assert_true(
+ obj2.synced,
+ 'The "redundant" state was entered after the third "extend ' +
+ 'lifetime promise" resolved.'
+ );
+ });
+ };
+ return runTest(t, scope, onRegister);
+ }, 'Test ExtendableEvent waitUntil reject precedence.');
+
+promise_test(function(t) {
+ var scope = 'resources/activate-fulfilled';
+ var onRegister = function(worker) {
+ var obj = {};
+ return wait_for_state(t, worker, 'activating')
+ .then(function() {
+ return Promise.all([
+ syncWorker(worker, obj),
+ wait_for_state(t, worker, 'activated')
+ ]);
+ })
+ .then(function() {
+ assert_true(
+ obj.synced,
+ 'state should be "activated" after the waitUntil promise ' +
+ 'for "onactivate" is fulfilled.');
+ });
+ };
+ return runTest(t, scope, onRegister);
+ }, 'Test activate event waitUntil fulfilled');
+
+promise_test(function(t) {
+ var scope = 'resources/install-rejected';
+ var onRegister = function(worker) {
+ return wait_for_state(t, worker, 'redundant');
+ };
+ return runTest(t, scope, onRegister);
+ }, 'Test install event waitUntil rejected');
+
+promise_test(function(t) {
+ var scope = 'resources/activate-rejected';
+ var onRegister = function(worker) {
+ return wait_for_state(t, worker, 'activated');
+ };
+ return runTest(t, scope, onRegister);
+ }, 'Test activate event waitUntil rejected.');
+
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/fetch-audio-tainting.https.html b/test/wpt/tests/service-workers/service-worker/fetch-audio-tainting.https.html
new file mode 100644
index 0000000..9821759
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/fetch-audio-tainting.https.html
@@ -0,0 +1,47 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js?pipe=sub"></script>
+<script>
+promise_test(async (t) => {
+ const SCOPE = 'resources/empty.html';
+ const SCRIPT = 'resources/fetch-rewrite-worker.js';
+ const host_info = get_host_info();
+ const REMOTE_ORIGIN = host_info.HTTPS_REMOTE_ORIGIN;
+
+ const reg = await service_worker_unregister_and_register(t, SCRIPT, SCOPE);
+ await wait_for_state(t, reg.installing, 'activated');
+ const frame = await with_iframe(SCOPE);
+
+ const doc = frame.contentDocument;
+ const win = frame.contentWindow;
+
+ const context = new win.AudioContext();
+ try {
+ context.suspend();
+ const audio = doc.createElement('audio');
+ audio.autoplay = true;
+ const source = context.createMediaElementSource(audio);
+ const spn = context.createScriptProcessor(16384, 1, 1);
+ source.connect(spn).connect(context.destination);
+ const url = `${REMOTE_ORIGIN}/webaudio/resources/sin_440Hz_-6dBFS_1s.wav`;
+ audio.src = '/test?url=' + encodeURIComponent(url);
+ doc.body.appendChild(audio);
+
+ await new Promise((resolve) => {
+ audio.addEventListener('playing', resolve);
+ });
+ await context.resume();
+ const event = await new Promise((resolve) => {
+ spn.addEventListener('audioprocess', resolve);
+ });
+ const data = event.inputBuffer.getChannelData(0);
+ for (const e of data) {
+ assert_equals(e, 0);
+ }
+ } finally {
+ context.close();
+ }
+ }, 'Verify CORS XHR of fetch() in a Service Worker');
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/fetch-canvas-tainting-double-write.https.html b/test/wpt/tests/service-workers/service-worker/fetch-canvas-tainting-double-write.https.html
new file mode 100644
index 0000000..dab2153
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/fetch-canvas-tainting-double-write.https.html
@@ -0,0 +1,57 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<meta charset="utf-8">
+<title>canvas tainting when written twice</title>
+<script>
+function loadImage(doc, url) {
+ return new Promise((resolve, reject) => {
+ const image = doc.createElement('img');
+ image.onload = () => { resolve(image); }
+ image.onerror = () => { reject('failed to load: ' + url); };
+ image.src = url;
+ });
+}
+
+// Tests that a canvas is tainted after it's written to with both a clear image
+// and opaque image from the same URL. A bad implementation might cache the
+// info of the clear image and assume the opaque image is also clear because
+// it's from the same URL. See https://crbug.com/907047 for details.
+promise_test(async (t) => {
+ // Set up a service worker and a controlled iframe.
+ const script = 'resources/fetch-canvas-tainting-double-write-worker.js';
+ const scope = 'resources/fetch-canvas-tainting-double-write-iframe.html';
+ const registration = await service_worker_unregister_and_register(
+ t, script, scope);
+ t.add_cleanup(() => registration.unregister());
+ await wait_for_state(t, registration.installing, 'activated');
+ const iframe = await with_iframe(scope);
+ t.add_cleanup(() => iframe.remove());
+
+ // Load the same cross-origin image URL through the controlled iframe and
+ // this uncontrolled frame. The service worker responds with a same-origin
+ // image for the controlled iframe, so it is cleartext.
+ const imagePath = base_path() + 'resources/fetch-access-control.py?PNGIMAGE';
+ const imageUrl = get_host_info()['HTTPS_REMOTE_ORIGIN'] + imagePath;
+ const clearImage = await loadImage(iframe.contentDocument, imageUrl);
+ const opaqueImage = await loadImage(document, imageUrl);
+
+ // Set up a canvas for testing tainting.
+ const canvas = document.createElement('canvas');
+ const context = canvas.getContext('2d');
+ canvas.width = clearImage.width;
+ canvas.height = clearImage.height;
+
+ // The clear image and the opaque image have the same src URL. But...
+
+ // ... the clear image doesn't taint the canvas.
+ context.drawImage(clearImage, 0, 0);
+ assert_true(canvas.toDataURL().length > 0);
+
+ // ... the opaque image taints the canvas.
+ context.drawImage(opaqueImage, 0, 0);
+ assert_throws_dom('SecurityError', () => { canvas.toDataURL(); });
+}, 'canvas is tainted after writing both a non-opaque image and an opaque image from the same URL');
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/fetch-canvas-tainting-image-cache.https.html b/test/wpt/tests/service-workers/service-worker/fetch-canvas-tainting-image-cache.https.html
new file mode 100644
index 0000000..2132381
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/fetch-canvas-tainting-image-cache.https.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Service Worker: canvas tainting of the fetched image using cached responses</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js?pipe=sub"></script>
+<script src="resources/fetch-canvas-tainting-tests.js"></script>
+<body>
+<script>
+do_canvas_tainting_tests({
+ resource_path: base_path() + 'resources/fetch-access-control.py?PNGIMAGE',
+ cache: true
+});
+</script>
+</body>
diff --git a/test/wpt/tests/service-workers/service-worker/fetch-canvas-tainting-image.https.html b/test/wpt/tests/service-workers/service-worker/fetch-canvas-tainting-image.https.html
new file mode 100644
index 0000000..57dc7d9
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/fetch-canvas-tainting-image.https.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Service Worker: canvas tainting of the fetched image</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js?pipe=sub"></script>
+<script src="resources/fetch-canvas-tainting-tests.js"></script>
+<body>
+<script>
+do_canvas_tainting_tests({
+ resource_path: base_path() + 'resources/fetch-access-control.py?PNGIMAGE',
+ cache: false
+});
+</script>
+</body>
diff --git a/test/wpt/tests/service-workers/service-worker/fetch-canvas-tainting-video-cache.https.html b/test/wpt/tests/service-workers/service-worker/fetch-canvas-tainting-video-cache.https.html
new file mode 100644
index 0000000..c37e8e5
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/fetch-canvas-tainting-video-cache.https.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Service Worker: canvas tainting of the fetched video using cache responses</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js?pipe=sub"></script>
+<script src="resources/fetch-canvas-tainting-tests.js"></script>
+<body>
+<script>
+do_canvas_tainting_tests({
+ resource_path: base_path() + 'resources/fetch-access-control.py?VIDEO',
+ cache: true
+});
+</script>
+</body>
diff --git a/test/wpt/tests/service-workers/service-worker/fetch-canvas-tainting-video-with-range-request.https.html b/test/wpt/tests/service-workers/service-worker/fetch-canvas-tainting-video-with-range-request.https.html
new file mode 100644
index 0000000..28c3071
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/fetch-canvas-tainting-video-with-range-request.https.html
@@ -0,0 +1,92 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Canvas tainting due to video whose responses are fetched via a service worker including range requests</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js?pipe=sub"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<body>
+<script>
+// These tests try to test canvas tainting due to a <video> element. The video
+// src URL is same-origin as the page, but the response is fetched via a service
+// worker that does tricky things like returning opaque responses from another
+// origin. Furthermore, this tests range requests so there are multiple
+// responses.
+//
+// We test range requests by having the server return 206 Partial Content to the
+// first request (which doesn't necessarily have a "Range" header or one with a
+// byte range). Then the <video> element automatically makes ranged requests
+// (the "Range" HTTP request header specifies a byte range). The server responds
+// to these with 206 Partial Content for the given range.
+function range_request_test(script, expected, description) {
+ promise_test(t => {
+ let frame;
+ let registration;
+ add_result_callback(() => {
+ if (frame) frame.remove();
+ if (registration) registration.unregister();
+ });
+
+ const scope = 'resources/fetch-canvas-tainting-iframe.html';
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(r => {
+ registration = r;
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(() => {
+ return with_iframe(scope);
+ })
+ .then(f => {
+ frame = f;
+ // Add "?VIDEO&PartialContent" to get a video resource from the
+ // server using range requests.
+ const video_url = 'fetch-access-control.py?VIDEO&PartialContent';
+ return frame.contentWindow.create_test_case_promise(video_url);
+ })
+ .then(result => {
+ assert_equals(result, expected);
+ });
+ }, description);
+}
+
+// We want to consider a number of scenarios:
+// (1) Range responses come from a single origin, the same-origin as the page.
+// The canvas should not be tainted.
+range_request_test(
+ 'resources/fetch-event-network-fallback-worker.js',
+ 'NOT_TAINTED',
+ 'range responses from single origin (same-origin)');
+
+// (2) Range responses come from a single origin, cross-origin from the page
+// (and without CORS sharing). This is not possible to test, since service
+// worker can't make a request with a "Range" HTTP header in no-cors mode.
+
+// (3) Range responses come from multiple origins. The first response comes from
+// cross-origin (and without CORS sharing, so is opaque). Subsequent
+// responses come from same-origin. This should result in a load error, as regardless of canvas
+// loading range requests from multiple opaque origins can reveal information across those origins.
+range_request_test(
+ 'resources/range-request-to-different-origins-worker.js',
+ 'LOAD_ERROR',
+ 'range responses from multiple origins (cross-origin first)');
+
+// (4) Range responses come from multiple origins. The first response comes from
+// same-origin. Subsequent responses come from cross-origin (and without
+// CORS sharing). Like (2) this is not possible since the service worker
+// cannot make range requests cross-origin.
+
+// (5) Range responses come from a single origin, with a mix of opaque and
+// non-opaque responses. The first request uses 'no-cors' mode to
+// receive an opaque response, and subsequent range requests use 'cors'
+// to receive non-opaque responses. The canvas should be tainted.
+range_request_test(
+ 'resources/range-request-with-different-cors-modes-worker.js',
+ 'TAINTED',
+ 'range responses from single origin with both opaque and non-opaque responses');
+
+// (6) Range responses come from a single origin, with a mix of opaque and
+// non-opaque responses. The first request uses 'cors' mode to
+// receive an non-opaque response, and subsequent range requests use
+// 'no-cors' to receive non-opaque responses. Like (2) this is not possible.
+</script>
+</body>
diff --git a/test/wpt/tests/service-workers/service-worker/fetch-canvas-tainting-video.https.html b/test/wpt/tests/service-workers/service-worker/fetch-canvas-tainting-video.https.html
new file mode 100644
index 0000000..e8c23a2
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/fetch-canvas-tainting-video.https.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Service Worker: canvas tainting of the fetched video</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js?pipe=sub"></script>
+<script src="resources/fetch-canvas-tainting-tests.js"></script>
+<body>
+<script>
+do_canvas_tainting_tests({
+ resource_path: base_path() + 'resources/fetch-access-control.py?VIDEO',
+ cache: false
+});
+</script>
+</body>
diff --git a/test/wpt/tests/service-workers/service-worker/fetch-cors-exposed-header-names.https.html b/test/wpt/tests/service-workers/service-worker/fetch-cors-exposed-header-names.https.html
new file mode 100644
index 0000000..317b021
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/fetch-cors-exposed-header-names.https.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<title>Service Worker: CORS-exposed header names should be transferred correctly</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+promise_test(async function(t) {
+ const SCOPE = 'resources/simple.html';
+ const SCRIPT = 'resources/fetch-cors-exposed-header-names-worker.js';
+ const host_info = get_host_info();
+
+ const URL = get_host_info().HTTPS_REMOTE_ORIGIN +
+ '/service-workers/service-worker/resources/simple.txt?pipe=' +
+ 'header(access-control-allow-origin,*)|' +
+ 'header(access-control-expose-headers,*)|' +
+ 'header(foo,bar)|' +
+ 'header(set-cookie,X)';
+
+ const reg = await service_worker_unregister_and_register(t, SCRIPT, SCOPE);
+ await wait_for_state(t, reg.installing, 'activated');
+ const frame = await with_iframe(SCOPE);
+
+ const response = await frame.contentWindow.fetch(URL);
+ const headers = response.headers;
+ assert_equals(headers.get('foo'), 'bar');
+ assert_equals(headers.get('set-cookie'), null);
+ assert_equals(headers.get('access-control-expose-headers'), '*');
+ }, 'CORS-exposed header names for a response from sw');
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/fetch-cors-xhr.https.html b/test/wpt/tests/service-workers/service-worker/fetch-cors-xhr.https.html
new file mode 100644
index 0000000..f8ff445
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/fetch-cors-xhr.https.html
@@ -0,0 +1,49 @@
+<!DOCTYPE html>
+<title>Service Worker: CORS XHR of fetch()</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js?pipe=sub"></script>
+<body>
+<script>
+promise_test(function(t) {
+ var SCOPE = 'resources/fetch-cors-xhr-iframe.html';
+ var SCRIPT = 'resources/fetch-rewrite-worker.js';
+ var host_info = get_host_info();
+
+ return login_https(t)
+ .then(function() {
+ return service_worker_unregister_and_register(t, SCRIPT, SCOPE);
+ })
+ .then(function(registration) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, SCOPE);
+ });
+
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() { return with_iframe(SCOPE); })
+ .then(function(frame) {
+ t.add_cleanup(function() {
+ frame.remove();
+ });
+
+ return new Promise(function(resolve, reject) {
+ var channel = new MessageChannel();
+ channel.port1.onmessage = (event) => {
+ if (event.data === 'done') {
+ resolve();
+ return;
+ }
+ test(() => {
+ assert_true(event.data.result);
+ }, event.data.testName);
+ };
+ frame.contentWindow.postMessage({},
+ host_info['HTTPS_ORIGIN'],
+ [channel.port2]);
+ });
+ });
+ }, 'Verify CORS XHR of fetch() in a Service Worker');
+</script>
+</body>
diff --git a/test/wpt/tests/service-workers/service-worker/fetch-csp.https.html b/test/wpt/tests/service-workers/service-worker/fetch-csp.https.html
new file mode 100644
index 0000000..9e7b242
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/fetch-csp.https.html
@@ -0,0 +1,138 @@
+<!DOCTYPE html>
+<title>Service Worker: CSP control of fetch()</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js?pipe=sub"></script>
+<script>
+
+function assert_resolves(promise, description) {
+ return promise.catch(function(reason) {
+ throw new Error(description + ' - ' + reason.message);
+ });
+}
+
+function assert_rejects(promise, description) {
+ return promise.then(
+ function() { throw new Error(description); },
+ function() {});
+}
+
+promise_test(function(t) {
+ var SCOPE = 'resources/fetch-csp-iframe.html';
+ var SCRIPT = 'resources/fetch-rewrite-worker.js';
+ var host_info = get_host_info();
+ var IMAGE_PATH =
+ base_path() + 'resources/fetch-access-control.py?PNGIMAGE';
+ var IMAGE_URL = host_info['HTTPS_ORIGIN'] + IMAGE_PATH;
+ var REMOTE_IMAGE_URL = host_info['HTTPS_REMOTE_ORIGIN'] + IMAGE_PATH;
+ var REDIRECT_URL =
+ host_info['HTTPS_ORIGIN'] + base_path() + 'resources/redirect.py';
+ var frame;
+
+ return service_worker_unregister_and_register(t, SCRIPT, SCOPE)
+ .then(function(registration) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, SCOPE);
+ });
+
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() {
+ return with_iframe(
+ SCOPE + '?' +
+ encodeURIComponent('img-src ' + host_info['HTTPS_ORIGIN'] +
+ '; script-src \'unsafe-inline\''));
+ })
+ .then(function(f) {
+ frame = f;
+ return assert_resolves(
+ frame.contentWindow.load_image(IMAGE_URL),
+ 'Allowed scope image resource should be loaded.');
+ })
+ .then(function() {
+ return assert_rejects(
+ frame.contentWindow.load_image(REMOTE_IMAGE_URL),
+ 'Disallowed scope image resource should not be loaded.');
+ })
+ .then(function() {
+ return assert_resolves(
+ frame.contentWindow.load_image(
+ // The request for IMAGE_URL will be fetched in SW.
+ './sample?url=' + encodeURIComponent(IMAGE_URL)),
+ 'Allowed scope image resource which was fetched via SW should ' +
+ 'be loaded.');
+ })
+ .then(function() {
+ return assert_rejects(
+ frame.contentWindow.load_image(
+ // The request for REMOTE_IMAGE_URL will be fetched in SW.
+ './sample?mode=no-cors&url=' +
+ encodeURIComponent(REMOTE_IMAGE_URL)),
+ 'Disallowed scope image resource which was fetched via SW ' +
+ 'should not be loaded.');
+ })
+ .then(function() {
+ frame.remove();
+ return with_iframe(
+ SCOPE + '?' +
+ encodeURIComponent(
+ 'img-src ' + REDIRECT_URL +
+ '; script-src \'unsafe-inline\''));
+ })
+ .then(function(f) {
+ frame = f;
+ return assert_resolves(
+ frame.contentWindow.load_image(
+ // Set 'ignore' not to call respondWith() in the SW.
+ REDIRECT_URL + '?ignore&Redirect=' +
+ encodeURIComponent(IMAGE_URL)),
+ 'When the request was redirected, CSP match algorithm should ' +
+ 'ignore the path component of the URL.');
+ })
+ .then(function() {
+ return assert_resolves(
+ frame.contentWindow.load_image(
+ // This request will be fetched via SW and redirected by
+ // redirect.php.
+ REDIRECT_URL + '?Redirect=' + encodeURIComponent(IMAGE_URL)),
+ 'When the request was redirected via SW, CSP match algorithm ' +
+ 'should ignore the path component of the URL.');
+ })
+ .then(function() {
+ return assert_resolves(
+ frame.contentWindow.load_image(
+ // The request for IMAGE_URL will be fetched in SW.
+ REDIRECT_URL + '?url=' + encodeURIComponent(IMAGE_URL)),
+ 'When the request was fetched via SW, CSP match algorithm ' +
+ 'should ignore the path component of the URL.');
+ })
+ .then(function() {
+ return assert_resolves(
+ frame.contentWindow.fetch(IMAGE_URL + "&fetch1", { mode: 'no-cors'}),
+ 'Allowed scope fetch resource should be loaded.');
+ })
+ .then(function() {
+ return assert_resolves(
+ frame.contentWindow.fetch(
+ // The request for IMAGE_URL will be fetched in SW.
+ './sample?url=' + encodeURIComponent(IMAGE_URL + '&fetch2'), { mode: 'no-cors'}),
+ 'Allowed scope fetch resource which was fetched via SW should be loaded.');
+ })
+ .then(function() {
+ return assert_rejects(
+ frame.contentWindow.fetch(REMOTE_IMAGE_URL + "&fetch3", { mode: 'no-cors'}),
+ 'Disallowed scope fetch resource should not be loaded.');
+ })
+ .then(function() {
+ return assert_rejects(
+ frame.contentWindow.fetch(
+ // The request for REMOTE_IMAGE_URL will be fetched in SW.
+ './sample?url=' + encodeURIComponent(REMOTE_IMAGE_URL + '&fetch4'), { mode: 'no-cors'}),
+ 'Disallowed scope fetch resource which was fetched via SW should not be loaded.');
+ })
+ .then(function() {
+ frame.remove();
+ });
+ }, 'Verify CSP control of fetch() in a Service Worker');
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/fetch-error.https.html b/test/wpt/tests/service-workers/service-worker/fetch-error.https.html
new file mode 100644
index 0000000..e9fdf57
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/fetch-error.https.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script>
+</head>
+<body>
+<script>
+const scope = "./resources/in-scope";
+
+promise_test(async (test) => {
+ const registration = await service_worker_unregister_and_register(
+ test, "./resources/fetch-error-worker.js", scope);
+ promise_test(async () => registration.unregister(),
+ "Unregister service worker");
+ await wait_for_state(test, registration.installing, 'activated');
+}, "Setup service worker");
+
+promise_test(async (test) => {
+ const iframe = await with_iframe(scope);
+ test.add_cleanup(() => iframe.remove());
+ const response = await iframe.contentWindow.fetch("fetch-error-test");
+ return promise_rejects_js(test, iframe.contentWindow.TypeError,
+ response.text(), 'text() should reject');
+}, "Make sure a load that makes progress does not time out");
+</script>
+</body>
+</html>
diff --git a/test/wpt/tests/service-workers/service-worker/fetch-event-add-async.https.html b/test/wpt/tests/service-workers/service-worker/fetch-event-add-async.https.html
new file mode 100644
index 0000000..ac13e4f
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/fetch-event-add-async.https.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<title>Service Worker: Fetch event added asynchronously doesn't throw</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+'use strict';
+
+service_worker_test(
+ 'resources/fetch-event-add-async-worker.js');
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/fetch-event-after-navigation-within-page.https.html b/test/wpt/tests/service-workers/service-worker/fetch-event-after-navigation-within-page.https.html
new file mode 100644
index 0000000..4812d8a
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/fetch-event-after-navigation-within-page.https.html
@@ -0,0 +1,71 @@
+<!DOCTYPE html>
+<title>ServiceWorker: navigator.serviceWorker.waiting</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+
+promise_test(function(t) {
+ var scope =
+ 'resources/fetch-event-after-navigation-within-page-iframe.html' +
+ '?hashchange';
+ var worker = 'resources/simple-intercept-worker.js';
+ var frame;
+
+ return service_worker_unregister_and_register(t, worker, scope)
+ .then(function(reg) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ return wait_for_state(t, reg.installing, 'activated');
+ })
+ .then(function() { return with_iframe(scope); })
+ .then(function(f) {
+ frame = f;
+ return frame.contentWindow.fetch_url('simple.txt');
+ })
+ .then(function(response) {
+ assert_equals(response, 'intercepted by service worker');
+ frame.contentWindow.location.hash = 'foo';
+ return frame.contentWindow.fetch_url('simple.txt');
+ })
+ .then(function(response) {
+ assert_equals(response, 'intercepted by service worker');
+ frame.remove();
+ })
+ }, 'Service Worker should respond to fetch event after the hash changes');
+
+promise_test(function(t) {
+ var scope =
+ 'resources/fetch-event-after-navigation-within-page-iframe.html' +
+ '?pushState';
+ var worker = 'resources/simple-intercept-worker.js';
+ var frame;
+
+ return service_worker_unregister_and_register(t, worker, scope)
+ .then(function(reg) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ return wait_for_state(t, reg.installing, 'activated');
+ })
+ .then(function() { return with_iframe(scope); })
+ .then(function(f) {
+ frame = f;
+ return frame.contentWindow.fetch_url('simple.txt');
+ })
+ .then(function(response) {
+ assert_equals(response, 'intercepted by service worker');
+ frame.contentWindow.history.pushState('', '', 'bar');
+ return frame.contentWindow.fetch_url('simple.txt');
+ })
+ .then(function(response) {
+ assert_equals(response, 'intercepted by service worker');
+ frame.remove();
+ })
+ }, 'Service Worker should respond to fetch event after the pushState');
+
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/fetch-event-async-respond-with.https.html b/test/wpt/tests/service-workers/service-worker/fetch-event-async-respond-with.https.html
new file mode 100644
index 0000000..d9147f8
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/fetch-event-async-respond-with.https.html
@@ -0,0 +1,73 @@
+<!DOCTYPE html>
+<html>
+<title>respondWith cannot be called asynchronously</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+// This file has tests that call respondWith() asynchronously.
+
+let frame;
+let worker;
+const script = 'resources/fetch-event-async-respond-with-worker.js';
+const scope = 'resources/simple.html';
+
+// Global setup: this must be the first promise_test.
+promise_test(async (t) => {
+ const registration =
+ await service_worker_unregister_and_register(t, script, scope);
+ worker = registration.installing;
+ await wait_for_state(t, worker, 'activated');
+ frame = await with_iframe(scope);
+}, 'global setup');
+
+// Waits for a single message from the service worker and then removes the
+// message handler. Not safe for concurrent use.
+function wait_for_message() {
+ return new Promise((resolve) => {
+ const handler = (event) => {
+ navigator.serviceWorker.removeEventListener('message', handler);
+ resolve(event.data);
+ };
+ navigator.serviceWorker.addEventListener('message', handler);
+ });
+}
+
+// Does one test case. It fetches |url|. The service worker gets a fetch event
+// for |url| and attempts to call respondWith() asynchronously. It reports back
+// to the test whether an exception was thrown.
+async function do_test(url) {
+ // Send a message to tell the worker a new test case is starting.
+ const message = wait_for_message();
+ worker.postMessage('initializeMessageHandler');
+ const response = await message;
+ assert_equals(response, 'messageHandlerInitialized');
+
+ // Start a fetch.
+ const fetchPromise = frame.contentWindow.fetch(url);
+
+ // Receive the test result from the service worker.
+ const result = wait_for_message();
+ await fetchPromise.then(()=> {}, () => {});
+ return result;
+};
+
+promise_test(async (t) => {
+ const result = await do_test('respondWith-in-task');
+ assert_true(result.didThrow, 'should throw');
+ assert_equals(result.error, 'InvalidStateError');
+}, 'respondWith in a task throws InvalidStateError');
+
+promise_test(async (t) => {
+ const result = await do_test('respondWith-in-microtask');
+ assert_equals(result.didThrow, false, 'should not throw');
+}, 'respondWith in a microtask does not throw');
+
+// Global cleanup: the final promise_test.
+promise_test(async (t) => {
+ if (frame)
+ frame.remove();
+ await service_worker_unregister(t, scope);
+}, 'global cleanup');
+</script>
+</html>
diff --git a/test/wpt/tests/service-workers/service-worker/fetch-event-handled.https.html b/test/wpt/tests/service-workers/service-worker/fetch-event-handled.https.html
new file mode 100644
index 0000000..08b88ce
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/fetch-event-handled.https.html
@@ -0,0 +1,86 @@
+<!DOCTYPE html>
+<html>
+<title>Service Worker: FetchEvent.handled</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+
+let frame = null;
+let worker = null;
+const script = 'resources/fetch-event-handled-worker.js';
+const scope = 'resources/simple.html';
+const channel = new MessageChannel();
+
+// Wait for a message from the service worker and removes the message handler.
+function wait_for_message_from_worker() {
+ return new Promise((resolve) => channel.port2.onmessage = (event) => resolve(event.data));
+}
+
+// Global setup: this must be the first promise_test.
+promise_test(async (t) => {
+ const registration =
+ await service_worker_unregister_and_register(t, script, scope);
+ worker = registration.installing;
+ if (!worker)
+ worker = registration.active;
+ worker.postMessage({port:channel.port1}, [channel.port1]);
+ await wait_for_state(t, worker, 'activated');
+}, 'global setup');
+
+promise_test(async (t) => {
+ const promise = with_iframe(scope);
+ const message = await wait_for_message_from_worker();
+ frame = await promise;
+ assert_equals(message, 'RESOLVED');
+}, 'FetchEvent.handled should resolve when respondWith() is not called for a' +
+ ' navigation request');
+
+promise_test(async (t) => {
+ frame.contentWindow.fetch('sample.txt?respondWith-not-called');
+ const message = await wait_for_message_from_worker();
+ assert_equals(message, 'RESOLVED');
+}, 'FetchEvent.handled should resolve when respondWith() is not called for a' +
+ ' sub-resource request');
+
+promise_test(async (t) => {
+ frame.contentWindow.fetch(
+ 'sample.txt?respondWith-not-called-and-event-canceled').catch((e) => {});
+ const message = await wait_for_message_from_worker();
+ assert_equals(message, 'REJECTED');
+}, 'FetchEvent.handled should reject when respondWith() is not called and the' +
+ ' event is canceled');
+
+promise_test(async (t) => {
+ frame.contentWindow.fetch(
+ 'sample.txt?respondWith-called-and-promise-resolved');
+ const message = await wait_for_message_from_worker();
+ assert_equals(message, 'RESOLVED');
+}, 'FetchEvent.handled should resolve when the promise provided' +
+ ' to respondWith() is resolved');
+
+promise_test(async (t) => {
+ frame.contentWindow.fetch(
+ 'sample.txt?respondWith-called-and-promise-resolved-to-invalid-response')
+ .catch((e) => {});
+ const message = await wait_for_message_from_worker();
+ assert_equals(message, 'REJECTED');
+}, 'FetchEvent.handled should reject when the promise provided' +
+ ' to respondWith() is resolved to an invalid response');
+
+promise_test(async (t) => {
+ frame.contentWindow.fetch(
+ 'sample.txt?respondWith-called-and-promise-rejected').catch((e) => {});
+ const message = await wait_for_message_from_worker();
+ assert_equals(message, 'REJECTED');
+}, 'FetchEvent.handled should reject when the promise provided to' +
+ ' respondWith() is rejected');
+
+// Global cleanup: the final promise_test.
+promise_test(async (t) => {
+ if (frame)
+ frame.remove();
+ await service_worker_unregister(t, scope);
+}, 'global cleanup');
+</script>
+</html>
diff --git a/test/wpt/tests/service-workers/service-worker/fetch-event-is-history-backward-navigation-manual.https.html b/test/wpt/tests/service-workers/service-worker/fetch-event-is-history-backward-navigation-manual.https.html
new file mode 100644
index 0000000..3cf5922
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/fetch-event-is-history-backward-navigation-manual.https.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<body>
+<p>Click <a href="resources/install-worker.html?isHistoryNavigation&amp;script=fetch-event-test-worker.js">this link</a>.
+ Once you see &quot;method = GET,...&quot; in the page, go to another page, and then go back to the page using the Backward button.
+ You should see &quot;method = GET, isHistoryNavigation = true&quot;.
+</p>
+</body>
+</html>
diff --git a/test/wpt/tests/service-workers/service-worker/fetch-event-is-history-forward-navigation-manual.https.html b/test/wpt/tests/service-workers/service-worker/fetch-event-is-history-forward-navigation-manual.https.html
new file mode 100644
index 0000000..401939b
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/fetch-event-is-history-forward-navigation-manual.https.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<body>
+<p>Click <a href="resources/install-worker.html?isHistoryNavigation&amp;script=fetch-event-test-worker.js">this link</a>.
+ Once you see &quot;method = GET,...&quot; in the page, go back to this page using the Backward button, and then go to the second page using the Forward button.
+ You should see &quot;method = GET, isHistoryNavigation = true&quot;.
+</p>
+</body>
+</html>
diff --git a/test/wpt/tests/service-workers/service-worker/fetch-event-is-reload-iframe-navigation-manual.https.html b/test/wpt/tests/service-workers/service-worker/fetch-event-is-reload-iframe-navigation-manual.https.html
new file mode 100644
index 0000000..cf1fecc
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/fetch-event-is-reload-iframe-navigation-manual.https.html
@@ -0,0 +1,31 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+const worker = 'resources/fetch-event-test-worker.js';
+
+promise_test(async (t) => {
+ const scope = 'resources/simple.html?isReloadNavigation';
+
+ const reg = await service_worker_unregister_and_register(t, worker, scope);
+ await wait_for_state(t, reg.installing, 'activated');
+ const frame = await with_iframe(scope);
+ assert_equals(frame.contentDocument.body.textContent,
+ 'method = GET, isReloadNavigation = false');
+ await new Promise((resolve) => {
+ frame.addEventListener('load', resolve);
+ frame.contentDocument.body.innerText =
+ 'Reload this frame manually!';
+ });
+ assert_equals(frame.contentDocument.body.textContent,
+ 'method = GET, isReloadNavigation = true');
+ frame.remove();
+ await reg.unregister();
+}, 'FetchEvent#request.isReloadNavigation is true for manual reload.');
+
+</script>
+</body>
+</html>
diff --git a/test/wpt/tests/service-workers/service-worker/fetch-event-is-reload-navigation-manual.https.html b/test/wpt/tests/service-workers/service-worker/fetch-event-is-reload-navigation-manual.https.html
new file mode 100644
index 0000000..a349f07
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/fetch-event-is-reload-navigation-manual.https.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<body>
+<p>Click <a href="resources/install-worker.html?isReloadNavigation&script=fetch-event-test-worker.js">this link</a>.
+ Once you see &quot;method = GET,...&quot; in the page, reload the page.
+ You will see &quot;method = GET, isReloadNavigation = true&quot;.
+</p>
+</body>
+</html>
diff --git a/test/wpt/tests/service-workers/service-worker/fetch-event-network-error.https.html b/test/wpt/tests/service-workers/service-worker/fetch-event-network-error.https.html
new file mode 100644
index 0000000..fea2ad1
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/fetch-event-network-error.https.html
@@ -0,0 +1,44 @@
+<!DOCTYPE html>
+<title>Service Worker: Fetch event network error</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+var resolve_test_done;
+
+var test_done_promise = new Promise(function(resolve) {
+ resolve_test_done = resolve;
+ });
+
+// Called by the child frame.
+function notify_test_done(result) {
+ resolve_test_done(result);
+}
+
+promise_test(function(t) {
+ var scope = 'resources/fetch-event-network-error-controllee-iframe.html';
+ var script = 'resources/fetch-event-network-error-worker.js';
+ var frame;
+
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(function(registration) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() {
+ return with_iframe(scope);
+ })
+ .then(function(f) {
+ frame = f;
+ return test_done_promise;
+ })
+ .then(function(result) {
+ frame.remove();
+ assert_equals(result, 'PASS');
+ });
+ }, 'Rejecting the fetch event or using preventDefault() causes a network ' +
+ 'error');
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/fetch-event-redirect.https.html b/test/wpt/tests/service-workers/service-worker/fetch-event-redirect.https.html
new file mode 100644
index 0000000..5229284
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/fetch-event-redirect.https.html
@@ -0,0 +1,1038 @@
+<!DOCTYPE html>
+<title>Service Worker: Fetch Event Redirect Handling</title>
+<meta name=timeout content=long>
+<script src="/resources/testharness.js"></script>
+<script src="resources/testharness-helpers.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+
+// ------------------------
+// Utilities for testing non-navigation requests that are intercepted with
+// a redirect.
+
+const host_info = get_host_info();
+const kScript = 'resources/fetch-rewrite-worker.js';
+const kScope = host_info['HTTPS_ORIGIN'] + base_path() +
+ 'resources/blank.html?fetch-event-redirect';
+let frame;
+
+function redirect_fetch_test(t, test) {
+ const hostKeySuffix = test['url_credentials'] ? '_WITH_CREDS' : '';
+ const successPath = base_path() + 'resources/success.py';
+
+ let acaOrigin = '';
+ let host = host_info['HTTPS_ORIGIN' + hostKeySuffix];
+ if (test['redirect_dest'] === 'no-cors') {
+ host = host_info['HTTPS_REMOTE_ORIGIN' + hostKeySuffix]
+ } else if (test['redirect_dest'] === 'cors') {
+ acaOrigin = '?ACAOrigin=' + encodeURIComponent(host_info['HTTPS_ORIGIN']);
+ host = host_info['HTTPS_REMOTE_ORIGIN' + hostKeySuffix]
+ }
+
+ const dest = '?Redirect=' + encodeURIComponent(host + successPath + acaOrigin);
+ const expectedTypeParam =
+ test['expected_type']
+ ? '&expected_type=' + test['expected_type']
+ : '';
+ const expectedRedirectedParam =
+ test['expected_redirected']
+ ? '&expected_redirected=' + test['expected_redirected']
+ : '';
+ const url = '/' + test.name +
+ '?url=' + encodeURIComponent('redirect.py' + dest) +
+ expectedTypeParam + expectedRedirectedParam
+ const request = new Request(url, test.request_init);
+
+ if (test.should_reject) {
+ return promise_rejects_js(
+ t,
+ frame.contentWindow.TypeError,
+ frame.contentWindow.fetch(request),
+ 'Must fail to fetch: url=' + url);
+ }
+ return frame.contentWindow.fetch(request).then((response) => {
+ assert_equals(response.type, test.expected_type,
+ 'response.type');
+ assert_equals(response.redirected, test.expected_redirected,
+ 'response.redirected');
+ if (response.type === 'opaque' || response.type === 'opaqueredirect') {
+ return;
+ }
+ return response.json().then((json) => {
+ assert_equals(json.result, 'success', 'JSON result must be "success".');
+ });
+ });
+}
+
+// Set up the service worker and the frame.
+promise_test(t => {
+ return service_worker_unregister_and_register(t, kScript, kScope)
+ .then(registration => {
+ promise_test(() => {
+ return registration.unregister();
+ }, 'restore global state');
+
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(() => {
+ return with_iframe(kScope);
+ })
+ .then(f => {
+ frame = f;
+ add_completion_callback(() => { f.remove(); });
+ });
+ }, 'initialize global state');
+
+// ------------------------
+// Test every combination of:
+// - RequestMode (same-origin, cors, no-cors)
+// - RequestRedirect (manual, follow, error)
+// - redirect destination origin (same-origin, cors, no-cors)
+// - redirect destination credentials (no user/pass, user/pass)
+//
+// TODO: add navigation requests
+// TODO: add redirects to data URI and verify same-origin data-URL flag behavior
+// TODO: add test where original redirect URI is cross-origin
+// TODO: verify final method is correct for 301, 302, and 303
+// TODO: verify CORS redirect results in all further redirects being
+// considered cross origin
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-manual-cors-redirects-to-sameorigin-nocreds',
+ redirect_dest: 'same-origin',
+ url_credentials: false,
+ expected_type: 'opaqueredirect',
+ expected_redirected: false,
+ request_init: {
+ redirect: 'manual',
+ mode: 'cors'
+ },
+ should_reject: false
+ });
+}, 'Non-navigation, manual redirect, cors mode Request redirected to ' +
+ 'same-origin without credentials should succeed opaqueredirect ' +
+ 'interception and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-manual-cors-redirects-to-nocors-nocreds',
+ redirect_dest: 'no-cors',
+ url_credentials: false,
+ expected_type: 'opaqueredirect',
+ expected_redirected: false,
+ request_init: {
+ redirect: 'manual',
+ mode: 'cors'
+ },
+ should_reject: false
+ });
+}, 'Non-navigation, manual redirect, cors mode Request redirected to ' +
+ 'no-cors without credentials should succeed opaqueredirect interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-manual-cors-redirects-to-cors-nocreds',
+ redirect_dest: 'cors',
+ url_credentials: false,
+ expected_type: 'opaqueredirect',
+ expected_redirected: false,
+ request_init: {
+ redirect: 'manual',
+ mode: 'cors'
+ },
+ should_reject: false
+ });
+}, 'Non-navigation, manual redirect, cors mode Request redirected to ' +
+ 'cors without credentials should succeed opaqueredirect interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-manual-sameorigin-redirects-to-sameorigin-nocreds',
+ redirect_dest: 'same-origin',
+ url_credentials: false,
+ expected_type: 'opaqueredirect',
+ expected_redirected: false,
+ request_init: {
+ redirect: 'manual',
+ mode: 'same-origin'
+ },
+ should_reject: false
+ });
+}, 'Non-navigation, manual redirect, same-origin mode Request redirected to ' +
+ 'same-origin without credentials should succeed opaqueredirect ' +
+ 'interception and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-manual-sameorigin-redirects-to-nocors-nocreds',
+ redirect_dest: 'no-cors',
+ url_credentials: false,
+ expected_type: 'opaqueredirect',
+ expected_redirected: false,
+ request_init: {
+ redirect: 'manual',
+ mode: 'same-origin'
+ },
+ should_reject: false
+ });
+}, 'Non-navigation, manual redirect, same-origin mode Request redirected to ' +
+ 'no-cors without credentials should succeed opaqueredirect interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-manual-sameorigin-redirects-to-cors-nocreds',
+ redirect_dest: 'cors',
+ url_credentials: false,
+ expected_type: 'opaqueredirect',
+ expected_redirected: false,
+ request_init: {
+ redirect: 'manual',
+ mode: 'same-origin'
+ },
+ should_reject: false
+ });
+}, 'Non-navigation, manual redirect, same-origin mode Request redirected to ' +
+ 'cors without credentials should succeed opaqueredirect interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-manual-nocors-redirects-to-sameorigin-nocreds',
+ redirect_dest: 'same-origin',
+ url_credentials: false,
+ expected_type: 'opaqueredirect',
+ expected_redirected: false,
+ request_init: {
+ redirect: 'manual',
+ mode: 'no-cors'
+ },
+ should_reject: false
+ });
+}, 'Non-navigation, manual redirect, no-cors mode Request redirected to ' +
+ 'same-origin without credentials should succeed opaqueredirect interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-manual-nocors-redirects-to-nocors-nocreds',
+ redirect_dest: 'no-cors',
+ url_credentials: false,
+ expected_type: 'opaqueredirect',
+ expected_redirected: false,
+ request_init: {
+ redirect: 'manual',
+ mode: 'no-cors'
+ },
+ // This should succeed because its redirecting from same-origin to
+ // cross-origin. Since the same-origin URL provides the location
+ // header the manual redirect mode should result in an opaqueredirect
+ // response.
+ should_reject: false
+ });
+}, 'Non-navigation, manual redirect, no-cors mode Request redirected to ' +
+ 'no-cors without credentials should succeed opaqueredirect interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-manual-nocors-redirects-to-cors-nocreds',
+ redirect_dest: 'cors',
+ url_credentials: false,
+ expected_type: 'opaqueredirect',
+ expected_redirected: false,
+ request_init: {
+ redirect: 'manual',
+ mode: 'no-cors'
+ },
+ // This should succeed because its redirecting from same-origin to
+ // cross-origin. Since the same-origin URL provides the location
+ // header the manual redirect mode should result in an opaqueredirect
+ // response.
+ should_reject: false
+ });
+}, 'Non-navigation, manual redirect, no-cors mode Request redirected to ' +
+ 'cors without credentials should succeed opaqueredirect interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-manual-cors-redirects-to-sameorigin-creds',
+ redirect_dest: 'same-origin',
+ url_credentials: true,
+ expected_type: 'opaqueredirect',
+ expected_redirected: false,
+ request_init: {
+ redirect: 'manual',
+ mode: 'cors'
+ },
+ should_reject: false
+ });
+}, 'Non-navigation, manual redirect, cors mode Request redirected to ' +
+ 'same-origin with credentials should succeed opaqueredirect interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-manual-cors-redirects-to-nocors-creds',
+ redirect_dest: 'no-cors',
+ url_credentials: true,
+ expected_type: 'opaqueredirect',
+ expected_redirected: false,
+ request_init: {
+ redirect: 'manual',
+ mode: 'cors'
+ },
+ should_reject: false
+ });
+}, 'Non-navigation, manual redirect, cors mode Request redirected to ' +
+ 'no-cors with credentials should succeed opaqueredirect interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-manual-cors-redirects-to-cors-creds',
+ redirect_dest: 'cors',
+ url_credentials: true,
+ expected_type: 'opaqueredirect',
+ expected_redirected: false,
+ request_init: {
+ redirect: 'manual',
+ mode: 'cors'
+ },
+ should_reject: false
+ });
+}, 'Non-navigation, manual redirect, cors mode Request redirected to ' +
+ 'cors with credentials should succeed opaqueredirect interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-manual-sameorigin-redirects-to-sameorigin-creds',
+ redirect_dest: 'same-origin',
+ url_credentials: true,
+ expected_type: 'opaqueredirect',
+ expected_redirected: false,
+ request_init: {
+ redirect: 'manual',
+ mode: 'same-origin'
+ },
+ should_reject: false
+ });
+}, 'Non-navigation, manual redirect, same-origin mode Request redirected to ' +
+ 'same-origin with credentials should succeed opaqueredirect interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-manual-sameorigin-redirects-to-nocors-creds',
+ redirect_dest: 'no-cors',
+ url_credentials: true,
+ expected_type: 'opaqueredirect',
+ expected_redirected: false,
+ request_init: {
+ redirect: 'manual',
+ mode: 'same-origin'
+ },
+ should_reject: false
+ });
+}, 'Non-navigation, manual redirect, same-origin mode Request redirected to ' +
+ 'no-cors with credentials should succeed opaqueredirect interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-manual-sameorigin-redirects-to-cors-creds',
+ redirect_dest: 'cors',
+ url_credentials: true,
+ expected_type: 'opaqueredirect',
+ expected_redirected: false,
+ request_init: {
+ redirect: 'manual',
+ mode: 'same-origin'
+ },
+ should_reject: false
+ });
+}, 'Non-navigation, manual redirect, same-origin mode Request redirected to ' +
+ 'cors with credentials should succeed opaqueredirect interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-manual-nocors-redirects-to-sameorigin-creds',
+ redirect_dest: 'same-origin',
+ url_credentials: true,
+ expected_type: 'opaqueredirect',
+ expected_redirected: false,
+ request_init: {
+ redirect: 'manual',
+ mode: 'no-cors'
+ },
+ should_reject: false
+ });
+}, 'Non-navigation, manual redirect, no-cors mode Request redirected to ' +
+ 'same-origin with credentials should succeed opaqueredirect interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-manual-nocors-redirects-to-nocors-creds',
+ redirect_dest: 'no-cors',
+ url_credentials: true,
+ expected_type: 'opaqueredirect',
+ expected_redirected: false,
+ request_init: {
+ redirect: 'manual',
+ mode: 'no-cors'
+ },
+ // This should succeed because its redirecting from same-origin to
+ // cross-origin. Since the same-origin URL provides the location
+ // header the manual redirect mode should result in an opaqueredirect
+ // response.
+ should_reject: false
+ });
+}, 'Non-navigation, manual redirect, no-cors mode Request redirected to ' +
+ 'no-cors with credentials should succeed opaqueredirect interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-manual-nocors-redirects-to-cors-creds',
+ redirect_dest: 'cors',
+ url_credentials: true,
+ expected_type: 'opaqueredirect',
+ expected_redirected: false,
+ request_init: {
+ redirect: 'manual',
+ mode: 'no-cors'
+ },
+ // This should succeed because its redirecting from same-origin to
+ // cross-origin. Since the same-origin URL provides the location
+ // header the manual redirect mode should result in an opaqueredirect
+ // response.
+ should_reject: false
+ });
+}, 'Non-navigation, manual redirect, no-cors mode Request redirected to ' +
+ 'cors with credentials should succeed opaqueredirect interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-follow-cors-redirects-to-sameorigin-nocreds',
+ redirect_dest: 'same-origin',
+ url_credentials: false,
+ expected_type: 'basic',
+ expected_redirected: true,
+ request_init: {
+ redirect: 'follow',
+ mode: 'cors'
+ },
+ should_reject: false
+ });
+}, 'Non-navigation, follow redirect, cors mode Request redirected to ' +
+ 'same-origin without credentials should succeed interception ' +
+ 'and response should be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-follow-cors-redirects-to-nocors-nocreds',
+ redirect_dest: 'no-cors',
+ url_credentials: false,
+ request_init: {
+ redirect: 'follow',
+ mode: 'cors'
+ },
+ // should reject because CORS requests require CORS headers on cross-origin
+ // resources
+ should_reject: true
+ });
+}, 'Non-navigation, follow redirect, cors mode Request redirected to ' +
+ 'no-cors without credentials should fail interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-follow-cors-redirects-to-cors-nocreds',
+ redirect_dest: 'cors',
+ url_credentials: false,
+ expected_type: 'cors',
+ expected_redirected: true,
+ request_init: {
+ redirect: 'follow',
+ mode: 'cors'
+ },
+ should_reject: false
+ });
+}, 'Non-navigation, follow redirect, cors mode Request redirected to ' +
+ 'cors without credentials should succeed interception ' +
+ 'and response should be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-follow-sameorigin-redirects-to-sameorigin-nocreds',
+ redirect_dest: 'same-origin',
+ url_credentials: false,
+ expected_type: 'basic',
+ expected_redirected: true,
+ request_init: {
+ redirect: 'follow',
+ mode: 'same-origin'
+ },
+ should_reject: false
+ });
+}, 'Non-navigation, follow redirect, same-origin mode Request redirected to ' +
+ 'same-origin without credentials should succeed interception ' +
+ 'and response should be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-follow-sameorigin-redirects-to-nocors-nocreds',
+ redirect_dest: 'no-cors',
+ url_credentials: false,
+ request_init: {
+ redirect: 'follow',
+ mode: 'same-origin'
+ },
+ // should reject because same-origin requests cannot load cross-origin
+ // resources
+ should_reject: true
+ });
+}, 'Non-navigation, follow redirect, same-origin mode Request redirected to ' +
+ 'no-cors without credentials should fail interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-follow-sameorigin-redirects-to-cors-nocreds',
+ redirect_dest: 'cors',
+ url_credentials: false,
+ request_init: {
+ redirect: 'follow',
+ mode: 'same-origin'
+ },
+ // should reject because same-origin requests cannot load cross-origin
+ // resources
+ should_reject: true
+ });
+}, 'Non-navigation, follow redirect, same-origin mode Request redirected to ' +
+ 'cors without credentials should fail interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-follow-nocors-redirects-to-sameorigin-nocreds',
+ redirect_dest: 'same-origin',
+ url_credentials: false,
+ expected_type: 'basic',
+ expected_redirected: true,
+ request_init: {
+ redirect: 'follow',
+ mode: 'no-cors'
+ },
+ should_reject: false
+ });
+}, 'Non-navigation, follow redirect, no-cors mode Request redirected to ' +
+ 'same-origin without credentials should succeed interception ' +
+ 'and response should be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-follow-nocors-redirects-to-nocors-nocreds',
+ redirect_dest: 'no-cors',
+ url_credentials: false,
+ expected_type: 'opaque',
+ expected_redirected: false,
+ request_init: {
+ redirect: 'follow',
+ mode: 'no-cors'
+ },
+ should_reject: false
+ });
+}, 'Non-navigation, follow redirect, no-cors mode Request redirected to ' +
+ 'no-cors without credentials should succeed interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-follow-nocors-redirects-to-cors-nocreds',
+ redirect_dest: 'cors',
+ url_credentials: false,
+ expected_type: 'opaque',
+ expected_redirected: false,
+ request_init: {
+ redirect: 'follow',
+ mode: 'no-cors'
+ },
+ should_reject: false
+ });
+}, 'Non-navigation, follow redirect, no-cors mode Request redirected to ' +
+ 'cors without credentials should succeed interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-follow-cors-redirects-to-sameorigin-creds',
+ redirect_dest: 'same-origin',
+ url_credentials: true,
+ expected_type: 'basic',
+ expected_redirected: true,
+ request_init: {
+ redirect: 'follow',
+ mode: 'cors'
+ },
+ should_reject: false
+ });
+}, 'Non-navigation, follow redirect, cors mode Request redirected to ' +
+ 'same-origin with credentials should succeed interception ' +
+ 'and response should be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-follow-cors-redirects-to-nocors-creds',
+ redirect_dest: 'no-cors',
+ url_credentials: true,
+ request_init: {
+ redirect: 'follow',
+ mode: 'cors'
+ },
+ // should reject because CORS requests require CORS headers on cross-origin
+ // resources
+ should_reject: true
+ });
+}, 'Non-navigation, follow redirect, cors mode Request redirected to ' +
+ 'no-cors with credentials should fail interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-follow-cors-redirects-to-cors-creds',
+ redirect_dest: 'cors',
+ url_credentials: true,
+ request_init: {
+ redirect: 'follow',
+ mode: 'cors'
+ },
+ // should reject because CORS requests do not allow user/pass entries in
+ // cross-origin URLs
+ // NOTE: https://github.com/whatwg/fetch/issues/112
+ should_reject: true
+ });
+}, 'Non-navigation, follow redirect, cors mode Request redirected to ' +
+ 'cors with credentials should fail interception ' +
+ 'and response should be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-follow-sameorigin-redirects-to-sameorigin-creds',
+ redirect_dest: 'same-origin',
+ url_credentials: true,
+ expected_type: 'basic',
+ expected_redirected: true,
+ request_init: {
+ redirect: 'follow',
+ mode: 'same-origin'
+ },
+ should_reject: false
+ });
+}, 'Non-navigation, follow redirect, same-origin mode Request redirected to ' +
+ 'same-origin with credentials should succeed interception ' +
+ 'and response should be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-follow-sameorigin-redirects-to-nocors-creds',
+ redirect_dest: 'no-cors',
+ url_credentials: true,
+ request_init: {
+ redirect: 'follow',
+ mode: 'same-origin'
+ },
+ // should reject because same-origin requests cannot load cross-origin
+ // resources
+ should_reject: true
+ });
+}, 'Non-navigation, follow redirect, same-origin mode Request redirected to ' +
+ 'no-cors with credentials should fail interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-follow-sameorigin-redirects-to-cors-creds',
+ redirect_dest: 'cors',
+ url_credentials: true,
+ request_init: {
+ redirect: 'follow',
+ mode: 'same-origin'
+ },
+ // should reject because same-origin requests cannot load cross-origin
+ // resources
+ should_reject: true
+ });
+}, 'Non-navigation, follow redirect, same-origin mode Request redirected to ' +
+ 'cors with credentials should fail interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-follow-nocors-redirects-to-sameorigin-creds',
+ redirect_dest: 'same-origin',
+ url_credentials: true,
+ expected_type: 'basic',
+ expected_redirected: true,
+ request_init: {
+ redirect: 'follow',
+ mode: 'no-cors'
+ },
+ should_reject: false
+ });
+}, 'Non-navigation, follow redirect, no-cors mode Request redirected to ' +
+ 'same-origin with credentials should succeed interception ' +
+ 'and response should be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-follow-nocors-redirects-to-nocors-creds',
+ redirect_dest: 'no-cors',
+ url_credentials: true,
+ expected_type: 'opaque',
+ expected_redirected: false,
+ request_init: {
+ redirect: 'follow',
+ mode: 'no-cors'
+ },
+ should_reject: false
+ });
+}, 'Non-navigation, follow redirect, no-cors mode Request redirected to ' +
+ 'no-cors with credentials should succeed interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-follow-nocors-redirects-to-cors-creds',
+ redirect_dest: 'cors',
+ url_credentials: true,
+ expected_type: 'opaque',
+ expected_redirected: false,
+ request_init: {
+ redirect: 'follow',
+ mode: 'no-cors'
+ },
+ should_reject: false
+ });
+}, 'Non-navigation, follow redirect, no-cors mode Request redirected to ' +
+ 'cors with credentials should succeed interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-error-cors-redirects-to-sameorigin-nocreds',
+ redirect_dest: 'same-origin',
+ url_credentials: false,
+ request_init: {
+ redirect: 'error',
+ mode: 'cors'
+ },
+ // should reject because requests with 'error' RequestRedirect cannot be
+ // redirected.
+ should_reject: true
+ });
+}, 'Non-navigation, error redirect, cors mode Request redirected to ' +
+ 'same-origin without credentials should fail interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-error-cors-redirects-to-nocors-nocreds',
+ redirect_dest: 'no-cors',
+ url_credentials: false,
+ request_init: {
+ redirect: 'error',
+ mode: 'cors'
+ },
+ // should reject because requests with 'error' RequestRedirect cannot be
+ // redirected.
+ should_reject: true
+ });
+}, 'Non-navigation, error redirect, cors mode Request redirected to ' +
+ 'no-cors without credentials should fail interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-error-cors-redirects-to-cors-nocreds',
+ redirect_dest: 'cors',
+ url_credentials: false,
+ request_init: {
+ redirect: 'error',
+ mode: 'cors'
+ },
+ // should reject because requests with 'error' RequestRedirect cannot be
+ // redirected.
+ should_reject: true
+ });
+}, 'Non-navigation, error redirect, cors mode Request redirected to ' +
+ 'cors without credentials should fail interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-error-sameorigin-redirects-to-sameorigin-nocreds',
+ redirect_dest: 'same-origin',
+ url_credentials: false,
+ request_init: {
+ redirect: 'error',
+ mode: 'same-origin'
+ },
+ // should reject because requests with 'error' RequestRedirect cannot be
+ // redirected.
+ should_reject: true
+ });
+}, 'Non-navigation, error redirect, same-origin mode Request redirected to ' +
+ 'same-origin without credentials should fail interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-error-sameorigin-redirects-to-nocors-nocreds',
+ redirect_dest: 'no-cors',
+ url_credentials: false,
+ request_init: {
+ redirect: 'error',
+ mode: 'same-origin'
+ },
+ // should reject because requests with 'error' RequestRedirect cannot be
+ // redirected.
+ should_reject: true
+ });
+}, 'Non-navigation, error redirect, same-origin mode Request redirected to ' +
+ 'no-cors without credentials should fail interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-error-sameorigin-redirects-to-cors-nocreds',
+ redirect_dest: 'cors',
+ url_credentials: false,
+ request_init: {
+ redirect: 'error',
+ mode: 'same-origin'
+ },
+ // should reject because requests with 'error' RequestRedirect cannot be
+ // redirected.
+ should_reject: true
+ });
+}, 'Non-navigation, error redirect, same-origin mode Request redirected to ' +
+ 'cors without credentials should fail interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-error-nocors-redirects-to-sameorigin-nocreds',
+ redirect_dest: 'same-origin',
+ url_credentials: false,
+ request_init: {
+ redirect: 'error',
+ mode: 'no-cors'
+ },
+ // should reject because requests with 'error' RequestRedirect cannot be
+ // redirected.
+ should_reject: true
+ });
+}, 'Non-navigation, error redirect, no-cors mode Request redirected to ' +
+ 'same-origin without credentials should fail interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-error-nocors-redirects-to-nocors-nocreds',
+ redirect_dest: 'no-cors',
+ url_credentials: false,
+ request_init: {
+ redirect: 'error',
+ mode: 'no-cors'
+ },
+ // should reject because requests with 'error' RequestRedirect cannot be
+ // redirected.
+ should_reject: true
+ });
+}, 'Non-navigation, error redirect, no-cors mode Request redirected to ' +
+ 'no-cors without credentials should fail interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-error-nocors-redirects-to-cors-nocreds',
+ redirect_dest: 'cors',
+ url_credentials: false,
+ request_init: {
+ redirect: 'error',
+ mode: 'no-cors'
+ },
+ // should reject because requests with 'error' RequestRedirect cannot be
+ // redirected.
+ should_reject: true
+ });
+}, 'Non-navigation, error redirect, no-cors mode Request redirected to ' +
+ 'cors without credentials should fail interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-error-cors-redirects-to-sameorigin-creds',
+ redirect_dest: 'same-origin',
+ url_credentials: true,
+ request_init: {
+ redirect: 'error',
+ mode: 'cors'
+ },
+ // should reject because requests with 'error' RequestRedirect cannot be
+ // redirected.
+ should_reject: true
+ });
+}, 'Non-navigation, error redirect, cors mode Request redirected to ' +
+ 'same-origin with credentials should fail interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-error-cors-redirects-to-nocors-creds',
+ redirect_dest: 'no-cors',
+ url_credentials: true,
+ request_init: {
+ redirect: 'error',
+ mode: 'cors'
+ },
+ // should reject because requests with 'error' RequestRedirect cannot be
+ // redirected.
+ should_reject: true
+ });
+}, 'Non-navigation, error redirect, cors mode Request redirected to ' +
+ 'no-cors with credentials should fail interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-error-cors-redirects-to-cors-creds',
+ redirect_dest: 'cors',
+ url_credentials: true,
+ request_init: {
+ redirect: 'error',
+ mode: 'cors'
+ },
+ // should reject because requests with 'error' RequestRedirect cannot be
+ // redirected.
+ should_reject: true
+ });
+}, 'Non-navigation, error redirect, cors mode Request redirected to ' +
+ 'cors with credentials should fail interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-error-sameorigin-redirects-to-sameorigin-creds',
+ redirect_dest: 'same-origin',
+ url_credentials: true,
+ request_init: {
+ redirect: 'error',
+ mode: 'same-origin'
+ },
+ // should reject because requests with 'error' RequestRedirect cannot be
+ // redirected.
+ should_reject: true
+ });
+}, 'Non-navigation, error redirect, same-origin mode Request redirected to ' +
+ 'same-origin with credentials should fail interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-error-sameorigin-redirects-to-nocors-creds',
+ redirect_dest: 'no-cors',
+ url_credentials: true,
+ request_init: {
+ redirect: 'error',
+ mode: 'same-origin'
+ },
+ // should reject because requests with 'error' RequestRedirect cannot be
+ // redirected.
+ should_reject: true
+ });
+}, 'Non-navigation, error redirect, same-origin mode Request redirected to ' +
+ 'no-cors with credentials should fail interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-error-sameorigin-redirects-to-cors-creds',
+ redirect_dest: 'cors',
+ url_credentials: true,
+ request_init: {
+ redirect: 'error',
+ mode: 'same-origin'
+ },
+ // should reject because requests with 'error' RequestRedirect cannot be
+ // redirected.
+ should_reject: true
+ });
+}, 'Non-navigation, error redirect, same-origin mode Request redirected to ' +
+ 'cors with credentials should fail interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-error-nocors-redirects-to-sameorigin-creds',
+ redirect_dest: 'same-origin',
+ url_credentials: true,
+ request_init: {
+ redirect: 'error',
+ mode: 'no-cors'
+ },
+ // should reject because requests with 'error' RequestRedirect cannot be
+ // redirected.
+ should_reject: true
+ });
+}, 'Non-navigation, error redirect, no-cors mode Request redirected to ' +
+ 'same-origin with credentials should fail interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-error-nocors-redirects-to-nocors-creds',
+ redirect_dest: 'no-cors',
+ url_credentials: true,
+ request_init: {
+ redirect: 'error',
+ mode: 'no-cors'
+ },
+ // should reject because requests with 'error' RequestRedirect cannot be
+ // redirected.
+ should_reject: true
+ });
+}, 'Non-navigation, error redirect, no-cors mode Request redirected to ' +
+ 'no-cors with credentials should fail interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-error-nocors-redirects-to-cors-creds',
+ redirect_dest: 'cors',
+ url_credentials: true,
+ request_init: {
+ redirect: 'error',
+ mode: 'no-cors'
+ },
+ // should reject because requests with 'error' RequestRedirect cannot be
+ // redirected.
+ should_reject: true
+ });
+}, 'Non-navigation, error redirect, no-cors mode Request redirected to ' +
+ 'cors with credentials should fail interception and response should not ' +
+ 'be redirected');
+</script>
+</body>
diff --git a/test/wpt/tests/service-workers/service-worker/fetch-event-referrer-policy.https.html b/test/wpt/tests/service-workers/service-worker/fetch-event-referrer-policy.https.html
new file mode 100644
index 0000000..af4b20a
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/fetch-event-referrer-policy.https.html
@@ -0,0 +1,274 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+var worker = 'resources/fetch-event-test-worker.js';
+
+function do_test(referrer, value, expected, name)
+{
+ test(() => {
+ assert_equals(value, expected);
+ }, name + (referrer ? " - Custom Referrer" : " - Default Referrer"));
+}
+
+function run_referrer_policy_tests(frame, referrer, href, origin) {
+ return frame.contentWindow.fetch('resources/simple.html?referrerFull',
+ {method: "GET", referrer: referrer})
+ .then(function(response) { return response.text(); })
+ .then(function(response_text) {
+ do_test(referrer,
+ response_text,
+ 'Referrer: ' + href + '\n' +
+ 'ReferrerPolicy: strict-origin-when-cross-origin',
+ 'Service Worker should respond to fetch with the referrer URL when a member of RequestInit is present');
+ var http_url = get_host_info()['HTTP_ORIGIN'] + base_path() +
+ '/resources/simple.html?referrerFull';
+ return frame.contentWindow.fetch(http_url,
+ {method: "GET", referrer: referrer});
+ })
+ .then(function(response) { return response.text(); })
+ .then(function(response_text) {
+ do_test(referrer,
+ response_text,
+ 'Referrer: \n' +
+ 'ReferrerPolicy: strict-origin-when-cross-origin',
+ 'Service Worker should respond to fetch with no referrer when a member of RequestInit is present with an HTTP request');
+ return frame.contentWindow.fetch('resources/simple.html?referrerFull',
+ {referrerPolicy: "", referrer: referrer});
+ })
+ .then(function(response) { return response.text(); })
+ .then(function(response_text) {
+ do_test(referrer,
+ response_text,
+ 'Referrer: ' + href + '\n' +
+ 'ReferrerPolicy: strict-origin-when-cross-origin',
+ 'Service Worker should respond to fetch with the referrer with ""');
+ var http_url = get_host_info()['HTTP_ORIGIN'] + base_path() +
+ '/resources/simple.html?referrerFull';
+ return frame.contentWindow.fetch(http_url,
+ {referrerPolicy: "", referrer: referrer});
+ })
+ .then(function(response) { return response.text(); })
+ .then(function(response_text) {
+ do_test(referrer,
+ response_text,
+ 'Referrer: \n' +
+ 'ReferrerPolicy: strict-origin-when-cross-origin',
+ 'Service Worker should respond to fetch with no referrer with ""');
+ return frame.contentWindow.fetch('resources/simple.html?referrerFull',
+ {referrerPolicy: "origin", referrer: referrer});
+ })
+ .then(function(response) { return response.text(); })
+ .then(function(response_text) {
+ do_test(referrer,
+ response_text,
+ 'Referrer: ' + origin + '/' + '\n' +
+ 'ReferrerPolicy: origin',
+ 'Service Worker should respond to fetch with the referrer origin with "origin" and a same origin request');
+ var http_url = get_host_info()['HTTP_ORIGIN'] + base_path() +
+ '/resources/simple.html?referrerFull';
+ return frame.contentWindow.fetch(http_url,
+ {referrerPolicy: "origin", referrer: referrer});
+ })
+ .then(function(response) { return response.text(); })
+ .then(function(response_text) {
+ do_test(referrer,
+ response_text,
+ 'Referrer: ' + origin + '/' + '\n' +
+ 'ReferrerPolicy: origin',
+ 'Service Worker should respond to fetch with the referrer origin with "origin" and a cross origin request');
+ return frame.contentWindow.fetch('resources/simple.html?referrerFull',
+ {referrerPolicy: "origin-when-cross-origin", referrer: referrer});
+ })
+ .then(function(response) { return response.text(); })
+ .then(function(response_text) {
+ do_test(referrer,
+ response_text,
+ 'Referrer: ' + href + '\n' +
+ 'ReferrerPolicy: origin-when-cross-origin',
+ 'Service Worker should respond to fetch with the referrer URL with "origin-when-cross-origin" and a same origin request');
+ var http_url = get_host_info()['HTTP_ORIGIN'] + base_path() +
+ '/resources/simple.html?referrerFull';
+ return frame.contentWindow.fetch(http_url,
+ {referrerPolicy: "origin-when-cross-origin", referrer: referrer});
+ })
+ .then(function(response) { return response.text(); })
+ .then(function(response_text) {
+ do_test(referrer,
+ response_text,
+ 'Referrer: ' + origin + '/' + '\n' +
+ 'ReferrerPolicy: origin-when-cross-origin',
+ 'Service Worker should respond to fetch with the referrer origin with "origin-when-cross-origin" and a cross origin request');
+ return frame.contentWindow.fetch('resources/simple.html?referrerFull',
+ {referrerPolicy: "no-referrer-when-downgrade", referrer: referrer});
+ })
+ .then(function(response) { return response.text(); })
+ .then(function(response_text) {
+ do_test(referrer,
+ response_text,
+ 'Referrer: ' + href + '\n' +
+ 'ReferrerPolicy: no-referrer-when-downgrade',
+ 'Service Worker should respond to fetch with no referrer with "no-referrer-when-downgrade" and a same origin request');
+ var http_url = get_host_info()['HTTP_ORIGIN'] + base_path() +
+ '/resources/simple.html?referrerFull';
+ return frame.contentWindow.fetch(http_url,
+ {referrerPolicy: "no-referrer-when-downgrade", referrer: referrer});
+ })
+ .then(function(response) { return response.text(); })
+ .then(function(response_text) {
+ do_test(referrer,
+ response_text,
+ 'Referrer: \n' +
+ 'ReferrerPolicy: no-referrer-when-downgrade',
+ 'Service Worker should respond to fetch with no referrer with "no-referrer-when-downgrade" and an HTTP request');
+ var http_url = get_host_info()['HTTP_ORIGIN'] + base_path() +
+ '/resources/simple.html?referrerFull';
+ return frame.contentWindow.fetch(http_url, {referrerPolicy: "unsafe-url", referrer: referrer});
+ })
+ .then(function(response) { return response.text(); })
+ .then(function(response_text) {
+ do_test(referrer,
+ response_text,
+ 'Referrer: ' + href + '\n' +
+ 'ReferrerPolicy: unsafe-url',
+ 'Service Worker should respond to fetch with no referrer with "unsafe-url"');
+ return frame.contentWindow.fetch('resources/simple.html?referrerFull',
+ {referrerPolicy: "no-referrer", referrer: referrer});
+ })
+ .then(function(response) { return response.text(); })
+ .then(function(response_text) {
+ do_test(referrer,
+ response_text,
+ 'Referrer: \n' +
+ 'ReferrerPolicy: no-referrer',
+ 'Service Worker should respond to fetch with no referrer URL with "no-referrer"');
+ return frame.contentWindow.fetch('resources/simple.html?referrerFull',
+ {referrerPolicy: "same-origin", referrer: referrer});
+ })
+ .then(function(response) { return response.text(); })
+ .then(function(response_text) {
+ do_test(referrer,
+ response_text,
+ 'Referrer: ' + href + '\n' +
+ 'ReferrerPolicy: same-origin',
+ 'Service Worker should respond to fetch with referrer URL with "same-origin" and a same origin request');
+ var http_url = get_host_info()['HTTPS_REMOTE_ORIGIN'] + base_path() +
+ '/resources/simple.html?referrerFull';
+ return frame.contentWindow.fetch(http_url,
+ {referrerPolicy: "same-origin", referrer: referrer});
+ })
+ .then(function(response) { return response.text(); })
+ .then(function(response_text) {
+ do_test(referrer,
+ response_text,
+ 'Referrer: \n' +
+ 'ReferrerPolicy: same-origin',
+ 'Service Worker should respond to fetch with no referrer with "same-origin" and cross origin request');
+ var http_url = get_host_info()['HTTPS_REMOTE_ORIGIN'] + base_path() +
+ '/resources/simple.html?referrerFull';
+ return frame.contentWindow.fetch(http_url,
+ {referrerPolicy: "strict-origin", referrer: referrer});
+ })
+ .then(function(response) { return response.text(); })
+ .then(function(response_text) {
+ do_test(referrer,
+ response_text,
+ 'Referrer: ' + origin + '/' + '\n' +
+ 'ReferrerPolicy: strict-origin',
+ 'Service Worker should respond to fetch with the referrer origin with "strict-origin" and a HTTPS cross origin request');
+ return frame.contentWindow.fetch('resources/simple.html?referrerFull',
+ {referrerPolicy: "strict-origin", referrer: referrer});
+ })
+ .then(function(response) { return response.text(); })
+ .then(function(response_text) {
+ do_test(referrer,
+ response_text,
+ 'Referrer: ' + origin + '/' + '\n' +
+ 'ReferrerPolicy: strict-origin',
+ 'Service Worker should respond to fetch with the referrer origin with "strict-origin" and a same origin request');
+ var http_url = get_host_info()['HTTP_ORIGIN'] + base_path() +
+ '/resources/simple.html?referrerFull';
+ return frame.contentWindow.fetch(http_url,
+ {referrerPolicy: "strict-origin", referrer: referrer});
+ })
+ .then(function(response) { return response.text(); })
+ .then(function(response_text) {
+ do_test(referrer,
+ response_text,
+ 'Referrer: \n' +
+ 'ReferrerPolicy: strict-origin',
+ 'Service Worker should respond to fetch with no referrer with "strict-origin" and a HTTP request');
+ return frame.contentWindow.fetch('resources/simple.html?referrerFull',
+ {referrerPolicy: "strict-origin-when-cross-origin", referrer: referrer});
+ })
+ .then(function(response) { return response.text(); })
+ .then(function(response_text) {
+ do_test(referrer,
+ response_text,
+ 'Referrer: ' + href + '\n' +
+ 'ReferrerPolicy: strict-origin-when-cross-origin',
+ 'Service Worker should respond to fetch with the referrer URL with "strict-origin-when-cross-origin" and a same origin request');
+ var http_url = get_host_info()['HTTPS_REMOTE_ORIGIN'] + base_path() +
+ '/resources/simple.html?referrerFull';
+ return frame.contentWindow.fetch(http_url,
+ {referrerPolicy: "strict-origin-when-cross-origin", referrer: referrer});
+ })
+ .then(function(response) { return response.text(); })
+ .then(function(response_text) {
+ do_test(referrer,
+ response_text,
+ 'Referrer: ' + origin + '/' + '\n' +
+ 'ReferrerPolicy: strict-origin-when-cross-origin',
+ 'Service Worker should respond to fetch with the referrer origin with "strict-origin-when-cross-origin" and a HTTPS cross origin request');
+ var http_url = get_host_info()['HTTP_ORIGIN'] + base_path() +
+ '/resources/simple.html?referrerFull';
+ return frame.contentWindow.fetch(http_url,
+ {referrerPolicy: "strict-origin-when-cross-origin", referrer: referrer});
+ })
+ .then(function(response) { return response.text(); })
+ .then(function(response_text) {
+ do_test(referrer,
+ response_text,
+ 'Referrer: \n' +
+ 'ReferrerPolicy: strict-origin-when-cross-origin',
+ 'Service Worker should respond to fetch with no referrer with "strict-origin-when-cross-origin" and a HTTP request');
+ });
+}
+
+promise_test(function(t) {
+ var scope = 'resources/simple.html?referrerPolicy';
+ var frame;
+ return service_worker_unregister_and_register(t, worker, scope)
+ .then(function(reg) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ return wait_for_state(t, reg.installing, 'activated');
+ })
+ .then(function() { return with_iframe(scope); })
+ .then(function(f) {
+ frame = f;
+ test(() => {
+ assert_equals(frame.contentDocument.body.textContent, 'ReferrerPolicy: strict-origin-when-cross-origin');
+ }, 'Service Worker should respond to fetch with the default referrer policy');
+ // First, run the referrer policy tests without passing a referrer in RequestInit.
+ return run_referrer_policy_tests(frame, undefined, frame.contentDocument.location.href,
+ frame.contentDocument.location.origin);
+ })
+ .then(function() {
+ // Now, run the referrer policy tests while passing a referrer in RequestInit.
+ var referrer = get_host_info()['HTTPS_ORIGIN'] + base_path() + 'resources/fake-referrer';
+ return run_referrer_policy_tests(frame, 'fake-referrer', referrer,
+ frame.contentDocument.location.origin);
+ })
+ .then(function() {
+ frame.remove();
+ });
+ }, 'Service Worker responds to fetch event with the referrer policy');
+
+</script>
+</body>
diff --git a/test/wpt/tests/service-workers/service-worker/fetch-event-respond-with-argument.https.html b/test/wpt/tests/service-workers/service-worker/fetch-event-respond-with-argument.https.html
new file mode 100644
index 0000000..05e2210
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/fetch-event-respond-with-argument.https.html
@@ -0,0 +1,44 @@
+<!DOCTYPE html>
+<title>Service Worker: FetchEvent.respondWith() argument type test.</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+var resolve_test_done;
+
+var test_done_promise = new Promise(function(resolve) {
+ resolve_test_done = resolve;
+ });
+
+// Called by the child frame.
+function notify_test_done(result) {
+ resolve_test_done(result);
+}
+
+promise_test(function(t) {
+ var scope = 'resources/fetch-event-respond-with-argument-iframe.html';
+ var script = 'resources/fetch-event-respond-with-argument-worker.js';
+ var frame;
+
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(function(registration) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() {
+ return with_iframe(scope);
+ })
+ .then(function(f) {
+ frame = f;
+ return test_done_promise;
+ })
+ .then(function(result) {
+ frame.remove();
+ assert_equals(result, 'PASS');
+ });
+ }, 'respondWith() takes either a Response or a promise that resolves ' +
+ 'with a Response. Other values should raise a network error.');
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/fetch-event-respond-with-body-loaded-in-chunk.https.html b/test/wpt/tests/service-workers/service-worker/fetch-event-respond-with-body-loaded-in-chunk.https.html
new file mode 100644
index 0000000..932f903
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/fetch-event-respond-with-body-loaded-in-chunk.https.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>respondWith with a response whose body is being loaded from the network by chunks</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+'use strict';
+
+const WORKER = 'resources/fetch-event-respond-with-body-loaded-in-chunk-worker.js';
+const SCOPE = 'resources/fetch-event-respond-with-body-loaded-in-chunk-iframe.html';
+
+promise_test(async t => {
+ var reg = await service_worker_unregister_and_register(t, WORKER, SCOPE);
+ add_completion_callback(() => reg.unregister());
+ await wait_for_state(t, reg.installing, 'activated');
+ let iframe = await with_iframe(SCOPE);
+ t.add_cleanup(() => iframe.remove());
+
+ let response = await iframe.contentWindow.fetch('body-in-chunk');
+ assert_equals(await response.text(), 'TEST_TRICKLE\nTEST_TRICKLE\nTEST_TRICKLE\nTEST_TRICKLE\n');
+}, 'Respond by chunks with a Response being loaded');
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/fetch-event-respond-with-custom-response.https.html b/test/wpt/tests/service-workers/service-worker/fetch-event-respond-with-custom-response.https.html
new file mode 100644
index 0000000..645a29c
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/fetch-event-respond-with-custom-response.https.html
@@ -0,0 +1,82 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>respondWith with a new Response</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+'use strict';
+
+const WORKER =
+ 'resources/fetch-event-respond-with-custom-response-worker.js';
+const SCOPE =
+ 'resources/blank.html';
+
+// Register a service worker, then create an iframe at url.
+function iframeTest(url, callback, name) {
+ return promise_test(async t => {
+ const reg = await service_worker_unregister_and_register(t, WORKER, SCOPE);
+ add_completion_callback(() => reg.unregister());
+ await wait_for_state(t, reg.installing, 'activated');
+ const iframe = await with_iframe(url);
+ const iwin = iframe.contentWindow;
+ t.add_cleanup(() => iframe.remove());
+ await callback(t, iwin);
+ }, name);
+}
+
+iframeTest(SCOPE, async (t, iwin) => {
+ const response = await iwin.fetch('?type=string');
+ assert_equals(await response.text(), 'PASS');
+}, 'Subresource built from a string');
+
+iframeTest(SCOPE, async (t, iwin) => {
+ const response = await iwin.fetch('?type=blob');
+ assert_equals(await response.text(), 'PASS');
+}, 'Subresource built from a blob');
+
+iframeTest(SCOPE, async (t, iwin) => {
+ const response = await iwin.fetch('?type=buffer');
+ assert_equals(await response.text(), 'PASS');
+}, 'Subresource built from a buffer');
+
+iframeTest(SCOPE, async (t, iwin) => {
+ const response = await iwin.fetch('?type=buffer-view');
+ assert_equals(await response.text(), 'PASS');
+}, 'Subresource built from a buffer-view');
+
+iframeTest(SCOPE, async (t, iwin) => {
+ const response = await iwin.fetch('?type=form-data');
+ const data = await response.formData();
+ assert_equals(data.get('result'), 'PASS');
+}, 'Subresource built from form-data');
+
+iframeTest(SCOPE, async (t, iwin) => {
+ const response = await iwin.fetch('?type=search-params');
+ assert_equals(await response.text(), 'result=PASS');
+}, 'Subresource built from search-params');
+
+// As above, but navigations
+
+iframeTest(SCOPE + '?type=string', (t, iwin) => {
+ assert_equals(iwin.document.body.textContent, 'PASS');
+}, 'Navigation resource built from a string');
+
+iframeTest(SCOPE + '?type=blob', (t, iwin) => {
+ assert_equals(iwin.document.body.textContent, 'PASS');
+}, 'Navigation resource built from a blob');
+
+iframeTest(SCOPE + '?type=buffer', (t, iwin) => {
+ assert_equals(iwin.document.body.textContent, 'PASS');
+}, 'Navigation resource built from a buffer');
+
+iframeTest(SCOPE + '?type=buffer-view', (t, iwin) => {
+ assert_equals(iwin.document.body.textContent, 'PASS');
+}, 'Navigation resource built from a buffer-view');
+
+// Note: not testing form data for a navigation as the boundary header is lost.
+
+iframeTest(SCOPE + '?type=search-params', (t, iwin) => {
+ assert_equals(iwin.document.body.textContent, 'result=PASS');
+}, 'Navigation resource built from search-params');
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/fetch-event-respond-with-partial-stream.https.html b/test/wpt/tests/service-workers/service-worker/fetch-event-respond-with-partial-stream.https.html
new file mode 100644
index 0000000..505cef2
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/fetch-event-respond-with-partial-stream.https.html
@@ -0,0 +1,62 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>respondWith streams data to an intercepted fetch()</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+'use strict';
+
+const WORKER =
+ 'resources/fetch-event-respond-with-partial-stream-worker.js';
+const SCOPE =
+ 'resources/fetch-event-respond-with-partial-stream-iframe.html';
+
+promise_test(async t => {
+ let reg = await service_worker_unregister_and_register(t, WORKER, SCOPE)
+ add_completion_callback(() => reg.unregister());
+
+ await wait_for_state(t, reg.installing, 'activated');
+
+ let frame = await with_iframe(SCOPE);
+ t.add_cleanup(_ => frame.remove());
+
+ let response = await frame.contentWindow.fetch('partial-stream.txt');
+
+ let reader = response.body.getReader();
+
+ let encoder = new TextEncoder();
+ let decoder = new TextDecoder();
+
+ let expected = 'partial-stream-content';
+ let encodedExpected = encoder.encode(expected);
+ let received = '';
+ let encodedReceivedLength = 0;
+
+ // Accumulate response data from the service worker. We do this as a loop
+ // to allow the browser the flexibility of rebuffering if it chooses. We
+ // do expect to get the partial data within the test timeout period, though.
+ // The spec is a bit vague at the moment about this, but it seems reasonable
+ // that the browser should not stall the response stream when the service
+ // worker has only written a partial result, but not closed the stream.
+ while (encodedReceivedLength < encodedExpected.length) {
+ let chunk = await reader.read();
+ assert_false(chunk.done, 'partial body stream should not be closed yet');
+
+ encodedReceivedLength += chunk.value.length;
+ received += decoder.decode(chunk.value);
+ }
+
+ // Note, the spec may allow some re-buffering between the service worker
+ // and the outer intercepted fetch. We could relax this exact chunk value
+ // match if necessary. The goal, though, is to ensure the outer fetch is
+ // not completely blocked until the service worker body is closed.
+ assert_equals(received, expected,
+ 'should receive partial content through service worker interception');
+
+ reg.active.postMessage('done');
+
+ await reader.closed;
+
+ }, 'respondWith() streams data to an intercepted fetch()');
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/fetch-event-respond-with-readable-stream-chunk.https.html b/test/wpt/tests/service-workers/service-worker/fetch-event-respond-with-readable-stream-chunk.https.html
new file mode 100644
index 0000000..4544a9e
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/fetch-event-respond-with-readable-stream-chunk.https.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>respondWith with a response built from a ReadableStream</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+'use strict';
+
+const WORKER = 'resources/fetch-event-respond-with-readable-stream-chunk-worker.js';
+const SCOPE = 'resources/fetch-event-respond-with-readable-stream-chunk-iframe.html';
+
+promise_test(async t => {
+ var reg = await service_worker_unregister_and_register(t, WORKER, SCOPE);
+ add_completion_callback(() => reg.unregister());
+ await wait_for_state(t, reg.installing, 'activated');
+ let iframe = await with_iframe(SCOPE);
+ t.add_cleanup(() => iframe.remove());
+
+ let response = await iframe.contentWindow.fetch('body-stream');
+ assert_equals(await response.text(), 'chunk #1 chunk #2 chunk #3 chunk #4');
+}, 'Respond by chunks with a Response built from a ReadableStream');
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/fetch-event-respond-with-readable-stream.https.html b/test/wpt/tests/service-workers/service-worker/fetch-event-respond-with-readable-stream.https.html
new file mode 100644
index 0000000..4651258
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/fetch-event-respond-with-readable-stream.https.html
@@ -0,0 +1,88 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>respondWith with a response built from a ReadableStream</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script src="/common/utils.js"></script>
+<script>
+'use strict';
+
+const WORKER =
+ 'resources/fetch-event-respond-with-readable-stream-worker.js';
+const SCOPE =
+ 'resources/blank.html';
+
+// Register a service worker, then create an iframe at url.
+function iframeTest(url, callback, name) {
+ return promise_test(async t => {
+ const reg = await service_worker_unregister_and_register(t, WORKER, SCOPE);
+ add_completion_callback(() => reg.unregister());
+ await wait_for_state(t, reg.installing, 'activated');
+ const iframe = await with_iframe(url);
+ const iwin = iframe.contentWindow;
+ t.add_cleanup(() => iframe.remove());
+ await callback(t, iwin);
+ }, name);
+}
+
+iframeTest(SCOPE, async (t, iwin) => {
+ const response = await iwin.fetch('?stream');
+ assert_equals(await response.text(), 'PASS');
+}, 'Subresource built from a ReadableStream');
+
+iframeTest(SCOPE + '?stream', (t, iwin) => {
+ assert_equals(iwin.document.body.textContent, 'PASS');
+}, 'Main resource built from a ReadableStream');
+
+iframeTest(SCOPE, async (t, iwin) => {
+ const response = await iwin.fetch('?stream&delay');
+ assert_equals(await response.text(), 'PASS');
+}, 'Subresource built from a ReadableStream - delayed');
+
+iframeTest(SCOPE + '?stream&delay', (t, iwin) => {
+ assert_equals(iwin.document.body.textContent, 'PASS');
+}, 'Main resource built from a ReadableStream - delayed');
+
+iframeTest(SCOPE, async (t, iwin) => {
+ const response = await iwin.fetch('?stream&use-fetch-stream');
+ assert_equals(await response.text(), 'PASS\n');
+}, 'Subresource built from a ReadableStream - fetch stream');
+
+iframeTest(SCOPE + '?stream&use-fetch-stream', (t, iwin) => {
+ assert_equals(iwin.document.body.textContent, 'PASS\n');
+}, 'Main resource built from a ReadableStream - fetch stream');
+
+for (const enqueue of [true, false]) {
+ const withStream = enqueue ? 'with nonempty stream' : 'with empty stream';
+ iframeTest(SCOPE, async (t, iwin) => {
+ const id = token();
+ let response = await iwin.fetch(`?stream&observe-cancel&id=${id}&enqueue=${enqueue}`);
+ response.body.cancel();
+
+ // Wait for a while to avoid a race between the cancel handling and the
+ // second fetch request.
+ await new Promise(r => step_timeout(r, 10));
+
+ response = await iwin.fetch(`?stream&query-cancel&id=${id}`);
+ assert_equals(await response.text(), 'cancelled');
+ }, `Cancellation in the page should be observable in the service worker ${withStream}`);
+
+ iframeTest(SCOPE, async (t, iwin) => {
+ const id = token();
+ const controller = new AbortController();
+ let response = await iwin.fetch(`?stream&observe-cancel&id=${id}&enqueue=${enqueue}`, {
+ signal: controller.signal
+ });
+ controller.abort();
+
+ // Wait for a while to avoid a race between the cancel handling and the
+ // second fetch request.
+ await new Promise(r => step_timeout(r, 10));
+
+ response = await iwin.fetch(`?stream&query-cancel&id=${id}`);
+ assert_equals(await response.text(), 'cancelled');
+ }, `Abort in the page should be observable in the service worker ${withStream}`);
+}
+
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/fetch-event-respond-with-response-body-with-invalid-chunk.https.html b/test/wpt/tests/service-workers/service-worker/fetch-event-respond-with-response-body-with-invalid-chunk.https.html
new file mode 100644
index 0000000..2a44811
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/fetch-event-respond-with-response-body-with-invalid-chunk.https.html
@@ -0,0 +1,46 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>respondWith with response body having invalid chunks</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+'use strict';
+
+const WORKER =
+ 'resources/fetch-event-respond-with-response-body-with-invalid-chunk-worker.js';
+const SCOPE =
+ 'resources/fetch-event-respond-with-response-body-with-invalid-chunk-iframe.html';
+
+// Called by the iframe when it has the reader promise we should watch.
+var set_reader_promise;
+let reader_promise = new Promise(resolve => set_reader_promise = resolve);
+
+var set_fetch_promise;
+let fetch_promise = new Promise(resolve => set_fetch_promise = resolve);
+
+// This test creates an controlled iframe that makes a fetch request. The
+// service worker returns a response with a body stream containing an invalid
+// chunk.
+promise_test(async t => {
+ // Start off the process.
+ let errorConstructor;
+ await service_worker_unregister_and_register(t, WORKER, SCOPE)
+ .then(reg => {
+ add_completion_callback(() => reg.unregister());
+ return wait_for_state(t, reg.installing, 'activated');
+ })
+ .then(() => with_iframe(SCOPE))
+ .then(frame => {
+ t.add_cleanup(() => frame.remove())
+ errorConstructor = frame.contentWindow.TypeError;
+ });
+
+ await promise_rejects_js(t, errorConstructor, reader_promise,
+ "read() should be rejected");
+ // Fetch should complete properly, because the reader error is caught in
+ // the subframe. That is, there should be no errors _other_ than the
+ // reader!
+ return fetch_promise;
+ }, 'Response with a ReadableStream having non-Uint8Array chunks should be transferred as errored');
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/fetch-event-respond-with-stops-propagation.https.html b/test/wpt/tests/service-workers/service-worker/fetch-event-respond-with-stops-propagation.https.html
new file mode 100644
index 0000000..31fd616
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/fetch-event-respond-with-stops-propagation.https.html
@@ -0,0 +1,37 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+promise_test(function(t) {
+ var script =
+ 'resources/fetch-event-respond-with-stops-propagation-worker.js';
+ var scope = 'resources/simple.html';
+
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(function(registration) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() {
+ return with_iframe(scope);
+ })
+ .then(function(frame) {
+ t.add_cleanup(function() { frame.remove(); });
+ var channel = new MessageChannel();
+ var saw_message = new Promise(function(resolve) {
+ channel.port1.onmessage = function(e) { resolve(e.data); }
+ });
+ var worker = frame.contentWindow.navigator.serviceWorker.controller;
+
+ worker.postMessage({port: channel.port2}, [channel.port2]);
+ return saw_message;
+ })
+ .then(function(message) {
+ assert_equals(message, 'PASS');
+ })
+ }, 'respondWith() invokes stopImmediatePropagation()');
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/fetch-event-throws-after-respond-with.https.html b/test/wpt/tests/service-workers/service-worker/fetch-event-throws-after-respond-with.https.html
new file mode 100644
index 0000000..d98fb22
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/fetch-event-throws-after-respond-with.https.html
@@ -0,0 +1,37 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title></title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+
+promise_test(function(t) {
+ var scope = 'resources/fetch-event-throws-after-respond-with-iframe.html';
+ var workerscript = 'resources/respond-then-throw-worker.js';
+ var iframe;
+ return service_worker_unregister_and_register(t, workerscript, scope)
+ .then(function(reg) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ return wait_for_state(t, reg.installing, 'activated')
+ .then(() => reg.active);
+ })
+ .then(function(worker) {
+ var channel = new MessageChannel();
+ channel.port1.onmessage = function(e) {
+ assert_equals(e.data, 'SYNC', ' Should receive sync message.');
+ channel.port1.postMessage('ACK');
+ }
+ worker.postMessage({port: channel.port2}, [channel.port2]);
+ // The iframe will only be loaded after the sync is completed.
+ return with_iframe(scope);
+ })
+ .then(function(frame) {
+ assert_true(frame.contentDocument.body.innerHTML.includes("intercepted"));
+ })
+ }, 'Fetch event handler throws after a successful respondWith()');
+
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/fetch-event-within-sw-manual.https.html b/test/wpt/tests/service-workers/service-worker/fetch-event-within-sw-manual.https.html
new file mode 100644
index 0000000..15a2e95
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/fetch-event-within-sw-manual.https.html
@@ -0,0 +1,122 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+const worker = 'resources/fetch-event-within-sw-worker.js';
+
+function wait(ms) {
+ return new Promise(r => setTimeout(r, ms));
+}
+
+function reset() {
+ for (const iframe of [...document.querySelectorAll('.test-iframe')]) {
+ iframe.remove();
+ }
+ return navigator.serviceWorker.getRegistrations().then(registrations => {
+ return Promise.all(registrations.map(r => r.unregister()));
+ }).then(() => caches.keys()).then(cacheKeys => {
+ return Promise.all(cacheKeys.map(c => caches.delete(c)));
+ });
+}
+
+add_completion_callback(reset);
+
+function regReady(reg) {
+ return new Promise((resolve, reject) => {
+ if (reg.active) {
+ resolve();
+ return;
+ }
+ const nextWorker = reg.waiting || reg.installing;
+
+ nextWorker.addEventListener('statechange', () => {
+ if (nextWorker.state == 'redundant') {
+ reject(Error(`Service worker failed to install`));
+ return;
+ }
+ if (nextWorker.state == 'activated') {
+ resolve();
+ }
+ });
+ });
+}
+
+function getCookies() {
+ return new Map(
+ document.cookie
+ .split(/;/g)
+ .map(c => c.trim().split('=').map(s => s.trim()))
+ );
+}
+
+function registerSwAndOpenFrame() {
+ return reset().then(() => navigator.serviceWorker.register(worker, {scope: 'resources/'}))
+ .then(reg => regReady(reg))
+ .then(() => with_iframe('resources/simple.html'));
+}
+
+function raceBroadcastAndCookie(channel, cookie) {
+ const initialCookie = getCookies().get(cookie);
+ let done = false;
+
+ return Promise.race([
+ new Promise(resolve => {
+ const bc = new BroadcastChannel(channel);
+ bc.onmessage = () => {
+ bc.close();
+ resolve('broadcast');
+ };
+ }),
+ (function checkCookie() {
+ // Stop polling if the broadcast channel won
+ if (done == true) return;
+ if (getCookies().get(cookie) != initialCookie) return 'cookie';
+
+ return wait(200).then(checkCookie);
+ }())
+ ]).then(val => {
+ done = true;
+ return val;
+ });
+}
+
+promise_test(() => {
+ return Notification.requestPermission().then(permission => {
+ if (permission != "granted") {
+ throw Error('You must allow notifications for this origin before running this test.');
+ }
+ return registerSwAndOpenFrame();
+ }).then(iframe => {
+ return Promise.resolve().then(() => {
+ // In this test, the service worker will ping the 'icon-request' channel
+ // if it intercepts a request for 'notification_icon.py'. If the request
+ // reaches the server it sets the 'notification' cookie to the value given
+ // in the URL. "raceBroadcastAndCookie" monitors both and returns which
+ // happens first.
+ const race = raceBroadcastAndCookie('icon-request', 'notification');
+ const notification = new iframe.contentWindow.Notification('test', {
+ icon: `notification_icon.py?set-cookie-notification=${Math.random()}`
+ });
+ notification.close();
+
+ return race.then(winner => {
+ assert_equals(winner, 'broadcast', 'The service worker intercepted the from-window notification icon request');
+ });
+ }).then(() => {
+ // Similar race to above, but this time the service worker requests the
+ // notification.
+ const race = raceBroadcastAndCookie('icon-request', 'notification');
+ iframe.contentWindow.fetch(`show-notification?set-cookie-notification=${Math.random()}`);
+
+ return race.then(winner => {
+ assert_equals(winner, 'broadcast', 'The service worker intercepted the from-service-worker notification icon request');
+ });
+ })
+ });
+}, `Notification requests intercepted both from window and SW`);
+
+</script>
+</body>
diff --git a/test/wpt/tests/service-workers/service-worker/fetch-event-within-sw.https.html b/test/wpt/tests/service-workers/service-worker/fetch-event-within-sw.https.html
new file mode 100644
index 0000000..0b52b18
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/fetch-event-within-sw.https.html
@@ -0,0 +1,53 @@
+<!DOCTYPE html>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+
+<script>
+const worker = 'resources/fetch-event-within-sw-worker.js';
+
+async function registerSwAndOpenFrame(t) {
+ const registration = await navigator.serviceWorker.register(
+ worker, { scope: 'resources/' });
+ t.add_cleanup(() => registration.unregister());
+ await wait_for_state(t, registration.installing, 'activated');
+
+ const frame = await with_iframe('resources/simple.html');
+ t.add_cleanup(() => frame.remove());
+ return frame;
+}
+
+async function deleteCaches() {
+ const cacheKeys = await caches.keys();
+ await Promise.all(cacheKeys.map(c => caches.delete(c)));
+}
+
+promise_test(async t => {
+ t.add_cleanup(deleteCaches);
+
+ const iframe = await registerSwAndOpenFrame(t);
+ const fetchText =
+ await iframe.contentWindow.fetch('sample.txt').then(r => r.text());
+
+ const cache = await iframe.contentWindow.caches.open('test');
+ await cache.add('sample.txt');
+
+ const response = await cache.match('sample.txt');
+ const cacheText = await (response ? response.text() : 'cache match failed');
+ assert_equals(fetchText, 'intercepted', 'fetch intercepted');
+ assert_equals(cacheText, 'intercepted', 'cache.add intercepted');
+}, 'Service worker intercepts requests from window');
+
+promise_test(async t => {
+ const iframe = await registerSwAndOpenFrame(t);
+ const [fetchText, cacheText] = await Promise.all([
+ iframe.contentWindow.fetch('sample.txt-inner-fetch').then(r => r.text()),
+ iframe.contentWindow.fetch('sample.txt-inner-cache').then(r => r.text())
+ ]);
+ assert_equals(fetchText, 'Hello world\n', 'fetch within SW not intercepted');
+ assert_equals(cacheText, 'Hello world\n',
+ 'cache.add within SW not intercepted');
+}, 'Service worker does not intercept fetch/cache requests within service ' +
+ 'worker');
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/fetch-event.https.h2.html b/test/wpt/tests/service-workers/service-worker/fetch-event.https.h2.html
new file mode 100644
index 0000000..5cd381e
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/fetch-event.https.h2.html
@@ -0,0 +1,112 @@
+<!DOCTYPE html>
+<meta name=timeout content=long>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="/common/utils.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+const worker = 'resources/fetch-event-test-worker.js';
+
+const method = 'POST';
+const duplex = 'half';
+
+function createBody(t) {
+ const rs = new ReadableStream({start(c) {
+ c.enqueue('i a');
+ c.enqueue('m the request');
+ step_timeout(t.step_func(() => {
+ c.enqueue(' body');
+ c.close();
+ }, 10));
+ }});
+ return rs.pipeThrough(new TextEncoderStream());
+}
+
+promise_test(async t => {
+ const scope = 'resources/';
+ const registration =
+ await service_worker_unregister_and_register(t, worker, scope);
+ await wait_for_state(t, registration.installing, 'activated');
+
+ // This will happen after all other tests
+ promise_test(t => {
+ return registration.unregister();
+ }, 'restore global state');
+}, 'global setup');
+
+// Test that the service worker can read FetchEvent#body when it is made from
+// a ReadableStream. It responds with request body it read.
+promise_test(async t => {
+ const body = createBody(t);
+ // Set page_url to "?ignore" so the service worker falls back to network
+ // for the main resource request, and add a suffix to avoid colliding
+ // with other tests.
+ const page_url = `resources/simple.html?ignore&id=${token()}`;
+ const frame = await with_iframe(page_url);
+ t.add_cleanup(() => { frame.remove(); });
+ const response = await frame.contentWindow.fetch('simple.html?request-body', {
+ method, body, duplex});
+ assert_equals(response.status, 200, 'status');
+ const text = await response.text();
+ assert_equals(text, 'i am the request body', 'body');
+}, 'The streaming request body is readable in the service worker.');
+
+// Network fallback
+promise_test(async t => {
+ const body = createBody(t);
+ // Set page_url to "?ignore" so the service worker falls back to network
+ // for the main resource request, and add a suffix to avoid colliding
+ // with other tests.
+ const page_url = `resources/simple.html?ignore&id=${token()}`;
+ const frame = await with_iframe(page_url);
+ t.add_cleanup(() => { frame.remove(); });
+ // Add "?ignore" so that the service worker falls back to
+ // echo-content.h2.py.
+ const echo_url = '/fetch/api/resources/echo-content.h2.py?ignore';
+ const response =
+ await frame.contentWindow.fetch(echo_url, { method, body, duplex});
+ assert_equals(response.status, 200, 'status');
+ const text = await response.text();
+ assert_equals(text, 'i am the request body', 'body');
+}, 'Network fallback for streaming upload.');
+
+// When the streaming body is used in the service worker, network fallback
+// fails.
+promise_test(async t => {
+ const body = createBody(t);
+ // Set page_url to "?ignore" so the service worker falls back to network
+ // for the main resource request, and add a suffix to avoid colliding
+ // with other tests.
+ const page_url = `resources/simple.html?ignore&id=${token()}`;
+ const frame = await with_iframe(page_url);
+ t.add_cleanup(() => { frame.remove(); });
+ const echo_url = '/fetch/api/resources/echo-content.h2.py?use-and-ignore';
+ const w = frame.contentWindow;
+ await promise_rejects_js(t, w.TypeError, w.fetch(echo_url, {
+ method, body, duplex}));
+}, 'When the streaming request body is used, network fallback fails.');
+
+// When the streaming body is used by clone() in the service worker, network
+// fallback succeeds.
+promise_test(async t => {
+ const body = createBody(t);
+ // Set page_url to "?ignore" so the service worker falls back to network
+ // for the main resource request, and add a suffix to avoid colliding
+ // with other tests.
+ const page_url = `resources/simple.html?ignore&id=${token()}`;
+ const frame = await with_iframe(page_url);
+ t.add_cleanup(() => { frame.remove(); });
+ // Add "?clone-and-ignore" so that the service worker falls back to
+ // echo-content.h2.py.
+ const echo_url = '/fetch/api/resources/echo-content.h2.py?clone-and-ignore';
+ const response = await frame.contentWindow.fetch(echo_url, {
+ method, body, duplex});
+ assert_equals(response.status, 200, 'status');
+ const text = await response.text();
+ assert_equals(text, 'i am the request body', 'body');
+}, 'Running clone() in the service worker does not prevent network fallback.');
+
+</script>
+</body>
diff --git a/test/wpt/tests/service-workers/service-worker/fetch-event.https.html b/test/wpt/tests/service-workers/service-worker/fetch-event.https.html
new file mode 100644
index 0000000..ce53f3c
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/fetch-event.https.html
@@ -0,0 +1,1000 @@
+<!DOCTYPE html>
+<meta name=timeout content=long>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="/common/utils.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+var worker = 'resources/fetch-event-test-worker.js';
+function wait(ms) {
+ return new Promise(resolve => step_timeout(resolve, ms));
+}
+
+promise_test(async t => {
+ const scope = 'resources/';
+ const registration =
+ await service_worker_unregister_and_register(t, worker, scope);
+ await wait_for_state(t, registration.installing, 'activated');
+
+ // This will happen after all other tests
+ promise_test(t => {
+ return registration.unregister();
+ }, 'restore global state');
+ }, 'global setup');
+
+promise_test(t => {
+ const page_url = 'resources/simple.html?headers';
+ return with_iframe(page_url)
+ .then(function(frame) {
+ t.add_cleanup(() => { frame.remove(); });
+ const headers = JSON.parse(frame.contentDocument.body.textContent);
+ const header_names = {};
+ for (const [name, value] of headers) {
+ header_names[name] = true;
+ }
+
+ assert_true(
+ header_names.hasOwnProperty('accept'),
+ 'request includes "Accept" header as inserted by Fetch'
+ );
+ });
+ }, 'Service Worker headers in the request of a fetch event');
+
+promise_test(t => {
+ const page_url = 'resources/simple.html?string';
+ return with_iframe(page_url)
+ .then(function(frame) {
+ t.add_cleanup(() => { frame.remove(); });
+ assert_equals(
+ frame.contentDocument.body.textContent,
+ 'Test string',
+ 'Service Worker should respond to fetch with a test string');
+ assert_equals(
+ frame.contentDocument.contentType,
+ 'text/plain',
+ 'The content type of the response created with a string should be text/plain');
+ assert_equals(
+ frame.contentDocument.characterSet,
+ 'UTF-8',
+ 'The character set of the response created with a string should be UTF-8');
+ });
+ }, 'Service Worker responds to fetch event with string');
+
+promise_test(t => {
+ const page_url = 'resources/simple.html?string';
+ var frame;
+ return with_iframe(page_url)
+ .then(function(f) {
+ frame = f;
+ t.add_cleanup(() => { frame.remove(); });
+ return frame.contentWindow.fetch(page_url + "#foo")
+ })
+ .then(function(response) { return response.text() })
+ .then(function(text) {
+ assert_equals(
+ text,
+ 'Test string',
+ 'Service Worker should respond to fetch with a test string');
+ });
+ }, 'Service Worker responds to fetch event using request fragment with string');
+
+promise_test(t => {
+ const page_url = 'resources/simple.html?blob';
+ return with_iframe(page_url)
+ .then(frame => {
+ t.add_cleanup(() => { frame.remove(); });
+ assert_equals(
+ frame.contentDocument.body.textContent,
+ 'Test blob',
+ 'Service Worker should respond to fetch with a test string');
+ });
+ }, 'Service Worker responds to fetch event with blob body');
+
+promise_test(t => {
+ const page_url = 'resources/simple.html?referrer';
+ return with_iframe(page_url)
+ .then(frame => {
+ t.add_cleanup(() => { frame.remove(); });
+ assert_equals(
+ frame.contentDocument.body.textContent,
+ 'Referrer: ' + document.location.href,
+ 'Service Worker should respond to fetch with the referrer URL');
+ });
+ }, 'Service Worker responds to fetch event with the referrer URL');
+
+promise_test(t => {
+ const page_url = 'resources/simple.html?clientId';
+ var frame;
+ return with_iframe(page_url)
+ .then(function(f) {
+ frame = f;
+ t.add_cleanup(() => { frame.remove(); });
+ assert_equals(
+ frame.contentDocument.body.textContent,
+ 'Client ID Not Found',
+ 'Service Worker should respond to fetch with a client id');
+ return frame.contentWindow.fetch('resources/other.html?clientId');
+ })
+ .then(function(response) { return response.text(); })
+ .then(function(response_text) {
+ assert_equals(
+ response_text.substr(0, 15),
+ 'Client ID Found',
+ 'Service Worker should respond to fetch with an existing client id');
+ });
+ }, 'Service Worker responds to fetch event with an existing client id');
+
+promise_test(t => {
+ const page_url = 'resources/simple.html?resultingClientId';
+ const expected_found = 'Resulting Client ID Found';
+ const expected_not_found = 'Resulting Client ID Not Found';
+ return with_iframe(page_url)
+ .then(function(frame) {
+ t.add_cleanup(() => { frame.remove(); });
+ assert_equals(
+ frame.contentDocument.body.textContent.substr(0, expected_found.length),
+ expected_found,
+ 'Service Worker should respond with an existing resulting client id for non-subresource requests');
+ return frame.contentWindow.fetch('resources/other.html?resultingClientId');
+ })
+ .then(function(response) { return response.text(); })
+ .then(function(response_text) {
+ assert_equals(
+ response_text.substr(0),
+ expected_not_found,
+ 'Service Worker should respond with an empty resulting client id for subresource requests');
+ });
+ }, 'Service Worker responds to fetch event with the correct resulting client id');
+
+promise_test(t => {
+ const page_url = 'resources/simple.html?ignore';
+ return with_iframe(page_url)
+ .then(function(frame) {
+ t.add_cleanup(() => { frame.remove(); });
+ assert_equals(frame.contentDocument.body.textContent,
+ 'Here\'s a simple html file.\n',
+ 'Response should come from fallback to native fetch');
+ });
+ }, 'Service Worker does not respond to fetch event');
+
+promise_test(t => {
+ const page_url = 'resources/simple.html?null';
+ return with_iframe(page_url)
+ .then(function(frame) {
+ t.add_cleanup(() => { frame.remove(); });
+ assert_equals(frame.contentDocument.body.textContent,
+ '',
+ 'Response should be the empty string');
+ });
+ }, 'Service Worker responds to fetch event with null response body');
+
+promise_test(t => {
+ const page_url = 'resources/simple.html?fetch';
+ return with_iframe(page_url)
+ .then(function(frame) {
+ t.add_cleanup(() => { frame.remove(); });
+ assert_equals(frame.contentDocument.body.textContent,
+ 'Here\'s an other html file.\n',
+ 'Response should come from fetched other file');
+ });
+ }, 'Service Worker fetches other file in fetch event');
+
+// Creates a form and an iframe and does a form submission that navigates the
+// frame to |action_url|. Returns the frame after navigation.
+function submit_form(action_url) {
+ return new Promise(resolve => {
+ const frame = document.createElement('iframe');
+ frame.name = 'post-frame';
+ document.body.appendChild(frame);
+ const form = document.createElement('form');
+ form.target = frame.name;
+ form.action = action_url;
+ form.method = 'post';
+ const input1 = document.createElement('input');
+ input1.type = 'text';
+ input1.value = 'testValue1';
+ input1.name = 'testName1'
+ form.appendChild(input1);
+ const input2 = document.createElement('input');
+ input2.type = 'text';
+ input2.value = 'testValue2';
+ input2.name = 'testName2'
+ form.appendChild(input2);
+ document.body.appendChild(form);
+ frame.onload = function() {
+ form.remove();
+ resolve(frame);
+ };
+ form.submit();
+ });
+}
+
+promise_test(t => {
+ const page_url = 'resources/simple.html?form-post';
+ return submit_form(page_url)
+ .then(frame => {
+ t.add_cleanup(() => { frame.remove(); });
+ assert_equals(frame.contentDocument.body.textContent,
+ 'POST:application/x-www-form-urlencoded:' +
+ 'testName1=testValue1&testName2=testValue2');
+ });
+ }, 'Service Worker responds to fetch event with POST form');
+
+promise_test(t => {
+ // Add '?ignore' so the service worker falls back to network.
+ const page_url = 'resources/echo-content.py?ignore';
+ return submit_form(page_url)
+ .then(frame => {
+ t.add_cleanup(() => { frame.remove(); });
+ assert_equals(frame.contentDocument.body.textContent,
+ 'testName1=testValue1&testName2=testValue2');
+ });
+ }, 'Service Worker falls back to network in fetch event with POST form');
+
+promise_test(t => {
+ const page_url = 'resources/simple.html?multiple-respond-with';
+ return with_iframe(page_url)
+ .then(frame => {
+ t.add_cleanup(() => { frame.remove(); });
+ assert_equals(
+ frame.contentDocument.body.textContent,
+ '(0)(1)[InvalidStateError](2)[InvalidStateError]',
+ 'Multiple calls of respondWith must throw InvalidStateErrors.');
+ });
+ }, 'Multiple calls of respondWith must throw InvalidStateErrors');
+
+promise_test(t => {
+ const page_url = 'resources/simple.html?used-check';
+ var first_frame;
+ return with_iframe(page_url)
+ .then(function(frame) {
+ assert_equals(frame.contentDocument.body.textContent,
+ 'Here\'s an other html file.\n',
+ 'Response should come from fetched other file');
+ first_frame = frame;
+ t.add_cleanup(() => { first_frame.remove(); });
+ return with_iframe(page_url);
+ })
+ .then(function(frame) {
+ t.add_cleanup(() => { frame.remove(); });
+ // When we access to the page_url in the second time, the content of the
+ // response is generated inside the ServiceWorker. The body contains
+ // the value of bodyUsed of the first response which is already
+ // consumed by FetchEvent.respondWith method.
+ assert_equals(
+ frame.contentDocument.body.textContent,
+ 'bodyUsed: true',
+ 'event.respondWith must set the used flag.');
+ });
+ }, 'Service Worker event.respondWith must set the used flag');
+
+promise_test(t => {
+ const page_url = 'resources/simple.html?fragment-check';
+ var fragment = '#/some/fragment';
+ var first_frame;
+ return with_iframe(page_url + fragment)
+ .then(function(frame) {
+ t.add_cleanup(() => { frame.remove(); });
+ assert_equals(
+ frame.contentDocument.body.textContent,
+ 'Fragment Found :' + fragment,
+ 'Service worker should expose URL fragments in request.');
+ });
+ }, 'Service Worker should expose FetchEvent URL fragments.');
+
+promise_test(t => {
+ const page_url = 'resources/simple.html?cache';
+ var frame;
+ var cacheTypes = [
+ undefined, 'default', 'no-store', 'reload', 'no-cache', 'force-cache', 'only-if-cached'
+ ];
+ return with_iframe(page_url)
+ .then(function(f) {
+ frame = f;
+ t.add_cleanup(() => { frame.remove(); });
+ assert_equals(frame.contentWindow.document.body.textContent, 'default');
+ var tests = cacheTypes.map(function(type) {
+ return new Promise(function(resolve, reject) {
+ var init = {cache: type};
+ if (type === 'only-if-cached') {
+ // For privacy reasons, for the time being, only-if-cached
+ // requires the mode to be same-origin.
+ init.mode = 'same-origin';
+ }
+ return frame.contentWindow.fetch(page_url + '=' + type, init)
+ .then(function(response) { return response.text(); })
+ .then(function(response_text) {
+ var expected = (type === undefined) ? 'default' : type;
+ assert_equals(response_text, expected,
+ 'Service Worker should respond to fetch with the correct type');
+ })
+ .then(resolve)
+ .catch(reject);
+ });
+ });
+ return Promise.all(tests);
+ })
+ .then(function() {
+ return new Promise(function(resolve, reject) {
+ frame.addEventListener('load', function onLoad() {
+ frame.removeEventListener('load', onLoad);
+ try {
+ assert_equals(frame.contentWindow.document.body.textContent,
+ 'no-cache');
+ resolve();
+ } catch (e) {
+ reject(e);
+ }
+ });
+ frame.contentWindow.location.reload();
+ });
+ });
+ }, 'Service Worker responds to fetch event with the correct cache types');
+
+promise_test(t => {
+ const page_url = 'resources/simple.html?eventsource';
+ var frame;
+
+ function test_eventsource(opts) {
+ return new Promise(function(resolve, reject) {
+ var eventSource = new frame.contentWindow.EventSource(page_url, opts);
+ eventSource.addEventListener('message', function(msg) {
+ eventSource.close();
+ try {
+ var data = JSON.parse(msg.data);
+ assert_equals(data.mode, 'cors',
+ 'EventSource should make CORS requests.');
+ assert_equals(data.cache, 'no-store',
+ 'EventSource should bypass the http cache.');
+ var expectedCredentials = opts.withCredentials ? 'include'
+ : 'same-origin';
+ assert_equals(data.credentials, expectedCredentials,
+ 'EventSource should pass correct credentials mode.');
+ resolve();
+ } catch (e) {
+ reject(e);
+ }
+ });
+ eventSource.addEventListener('error', function(e) {
+ eventSource.close();
+ reject('The EventSource fired an error event.');
+ });
+ });
+ }
+
+ return with_iframe(page_url)
+ .then(function(f) {
+ frame = f;
+ t.add_cleanup(() => { frame.remove(); });
+ return test_eventsource({ withCredentials: false });
+ })
+ .then(function() {
+ return test_eventsource({ withCredentials: true });
+ });
+ }, 'Service Worker should intercept EventSource');
+
+promise_test(t => {
+ const page_url = 'resources/simple.html?integrity';
+ var frame;
+ var integrity_metadata = 'gs0nqru8KbsrIt5YToQqS9fYao4GQJXtcId610g7cCU=';
+
+ return with_iframe(page_url)
+ .then(function(f) {
+ frame = f;
+ t.add_cleanup(() => { frame.remove(); });
+ // A request has associated integrity metadata (a string).
+ // Unless stated otherwise, it is the empty string.
+ assert_equals(
+ frame.contentDocument.body.textContent, '');
+
+ return frame.contentWindow.fetch(page_url, {'integrity': integrity_metadata});
+ })
+ .then(response => {
+ return response.text();
+ })
+ .then(response_text => {
+ assert_equals(response_text, integrity_metadata, 'integrity');
+ });
+ }, 'Service Worker responds to fetch event with the correct integrity_metadata');
+
+// Test that the service worker can read FetchEvent#body when it is a string.
+// It responds with request body it read.
+promise_test(t => {
+ // Set page_url to "?ignore" so the service worker falls back to network
+ // for the main resource request, and add a suffix to avoid colliding
+ // with other tests.
+ const page_url = 'resources/simple.html?ignore-for-request-body-string';
+ let frame;
+
+ return with_iframe(page_url)
+ .then(f => {
+ frame = f;
+ t.add_cleanup(() => { frame.remove(); });
+ return frame.contentWindow.fetch('simple.html?request-body', {
+ method: 'POST',
+ body: 'i am the request body'
+ });
+ })
+ .then(response => {
+ return response.text();
+ })
+ .then(response_text => {
+ assert_equals(response_text, 'i am the request body');
+ });
+ }, 'FetchEvent#body is a string');
+
+// Test that the service worker can read FetchEvent#body when it is made from
+// a ReadableStream. It responds with request body it read.
+promise_test(async t => {
+ const rs = new ReadableStream({start(c) {
+ c.enqueue('i a');
+ c.enqueue('m the request');
+ step_timeout(t.step_func(() => {
+ c.enqueue(' body');
+ c.close();
+ }, 10));
+ }});
+
+ // Set page_url to "?ignore" so the service worker falls back to network
+ // for the main resource request, and add a suffix to avoid colliding
+ // with other tests.
+ const page_url = `resources/simple.html?ignore&id=${token()}`;
+
+ const frame = await with_iframe(page_url);
+ t.add_cleanup(() => { frame.remove(); });
+ const res = await frame.contentWindow.fetch('simple.html?request-body', {
+ method: 'POST',
+ body: rs.pipeThrough(new TextEncoderStream()),
+ duplex: 'half',
+ });
+ assert_equals(await res.text(), 'i am the request body');
+ }, 'FetchEvent#body is a ReadableStream');
+
+// Test that the request body is sent to network upon network fallback,
+// for a string body.
+promise_test(t => {
+ // Set page_url to "?ignore" so the service worker falls back to network
+ // for the main resource request, and add a suffix to avoid colliding
+ // with other tests.
+ const page_url = 'resources/?ignore-for-request-body-fallback-string';
+ let frame;
+
+ return with_iframe(page_url)
+ .then(f => {
+ frame = f;
+ t.add_cleanup(() => { frame.remove(); });
+ // Add "?ignore" so the service worker falls back to echo-content.py.
+ const echo_url = '/fetch/api/resources/echo-content.py?ignore';
+ return frame.contentWindow.fetch(echo_url, {
+ method: 'POST',
+ body: 'i am the request body'
+ });
+ })
+ .then(response => {
+ return response.text();
+ })
+ .then(response_text => {
+ assert_equals(
+ response_text,
+ 'i am the request body',
+ 'the network fallback request should include the request body');
+ });
+ }, 'FetchEvent#body is a string and is passed to network fallback');
+
+// Test that the request body is sent to network upon network fallback,
+// for a ReadableStream body.
+promise_test(async t => {
+ const rs = new ReadableStream({start(c) {
+ c.enqueue('i a');
+ c.enqueue('m the request');
+ t.step_timeout(t.step_func(() => {
+ c.enqueue(' body');
+ c.close();
+ }, 10));
+ }});
+ // Set page_url to "?ignore" so the service worker falls back to network
+ // for the main resource request, and add a suffix to avoid colliding
+ // with other tests.
+ const page_url = 'resources/?ignore-for-request-body-fallback-string';
+ const frame = await with_iframe(page_url);
+ t.add_cleanup(() => { frame.remove(); });
+ // Add "?ignore" so the service worker falls back to echo-content.py.
+ const echo_url = '/fetch/api/resources/echo-content.py?ignore';
+ const w = frame.contentWindow;
+ await promise_rejects_js(t, w.TypeError, w.fetch(echo_url, {
+ method: 'POST',
+ body: rs
+ }));
+ }, 'FetchEvent#body is a none Uint8Array ReadableStream and is passed to a service worker');
+
+// Test that the request body is sent to network upon network fallback even when
+// the request body is used in the service worker, for a string body.
+promise_test(async t => {
+ // Set page_url to "?ignore" so the service worker falls back to network
+ // for the main resource request, and add a suffix to avoid colliding
+ // with other tests.
+ const page_url = 'resources/?ignore-for-request-body-fallback-string';
+
+ const frame = await with_iframe(page_url);
+ t.add_cleanup(() => { frame.remove(); });
+ // Add "?use-and-ignore" so the service worker falls back to echo-content.py.
+ const echo_url = '/fetch/api/resources/echo-content.py?use-and-ignore';
+ const response = await frame.contentWindow.fetch(echo_url, {
+ method: 'POST',
+ body: 'i am the request body'
+ });
+ const text = await response.text();
+ assert_equals(
+ text,
+ 'i am the request body',
+ 'the network fallback request should include the request body');
+ }, 'FetchEvent#body is a string, used and passed to network fallback');
+
+// Test that the request body is sent to network upon network fallback even when
+// the request body is used by clone() in the service worker, for a string body.
+promise_test(async t => {
+ // Set page_url to "?ignore" so the service worker falls back to network
+ // for the main resource request, and add a suffix to avoid colliding
+ // with other tests.
+ const page_url = 'resources/?ignore-for-request-body-fallback-string';
+
+ const frame = await with_iframe(page_url);
+ t.add_cleanup(() => { frame.remove(); });
+ // Add "?clone-and-ignore" so the service worker falls back to
+ // echo-content.py.
+ const echo_url = '/fetch/api/resources/echo-content.py?clone-and-ignore';
+ const response = await frame.contentWindow.fetch(echo_url, {
+ method: 'POST',
+ body: 'i am the request body'
+ });
+ const text = await response.text();
+ assert_equals(
+ text,
+ 'i am the request body',
+ 'the network fallback request should include the request body');
+ }, 'FetchEvent#body is a string, cloned and passed to network fallback');
+
+// Test that the service worker can read FetchEvent#body when it is a blob.
+// It responds with request body it read.
+promise_test(t => {
+ // Set page_url to "?ignore" so the service worker falls back to network
+ // for the main resource request, and add a suffix to avoid colliding
+ // with other tests.
+ const page_url = 'resources/simple.html?ignore-for-request-body-blob';
+ let frame;
+
+ return with_iframe(page_url)
+ .then(f => {
+ frame = f;
+ t.add_cleanup(() => { frame.remove(); });
+ const blob = new Blob(['it\'s me the blob', ' ', 'and more blob!']);
+ return frame.contentWindow.fetch('simple.html?request-body', {
+ method: 'POST',
+ body: blob
+ });
+ })
+ .then(response => {
+ return response.text();
+ })
+ .then(response_text => {
+ assert_equals(response_text, 'it\'s me the blob and more blob!');
+ });
+ }, 'FetchEvent#body is a blob');
+
+// Test that the request body is sent to network upon network fallback,
+// for a blob body.
+promise_test(t => {
+ // Set page_url to "?ignore" so the service worker falls back to network
+ // for the main resource request, and add a suffix to avoid colliding
+ // with other tests.
+ const page_url = 'resources/simple.html?ignore-for-request-body-fallback-blob';
+ let frame;
+
+ return with_iframe(page_url)
+ .then(f => {
+ frame = f;
+ t.add_cleanup(() => { frame.remove(); });
+ const blob = new Blob(['it\'s me the blob', ' ', 'and more blob!']);
+ // Add "?ignore" so the service worker falls back to echo-content.py.
+ const echo_url = '/fetch/api/resources/echo-content.py?ignore';
+ return frame.contentWindow.fetch(echo_url, {
+ method: 'POST',
+ body: blob
+ });
+ })
+ .then(response => {
+ return response.text();
+ })
+ .then(response_text => {
+ assert_equals(
+ response_text,
+ 'it\'s me the blob and more blob!',
+ 'the network fallback request should include the request body');
+ });
+ }, 'FetchEvent#body is a blob and is passed to network fallback');
+
+promise_test(async (t) => {
+ const page_url = 'resources/simple.html?keepalive';
+ const frame = await with_iframe(page_url);
+ t.add_cleanup(() => { frame.remove(); });
+ assert_equals(frame.contentDocument.body.textContent, 'false');
+ const response = await frame.contentWindow.fetch(page_url, {keepalive: true});
+ const text = await response.text();
+ assert_equals(text, 'true');
+ }, 'Service Worker responds to fetch event with the correct keepalive value');
+
+promise_test(async (t) => {
+ const page_url = 'resources/simple.html?isReloadNavigation';
+ const frame = await with_iframe(page_url);
+ t.add_cleanup(() => { frame.remove(); });
+ assert_equals(frame.contentDocument.body.textContent,
+ 'method = GET, isReloadNavigation = false');
+ await new Promise((resolve) => {
+ frame.addEventListener('load', resolve);
+ frame.contentWindow.location.reload();
+ });
+ assert_equals(frame.contentDocument.body.textContent,
+ 'method = GET, isReloadNavigation = true');
+ }, 'FetchEvent#request.isReloadNavigation is true (location.reload())');
+
+promise_test(async (t) => {
+ const page_url = 'resources/simple.html?isReloadNavigation';
+ const frame = await with_iframe(page_url);
+ t.add_cleanup(() => { frame.remove(); });
+ assert_equals(frame.contentDocument.body.textContent,
+ 'method = GET, isReloadNavigation = false');
+ await new Promise((resolve) => {
+ frame.addEventListener('load', resolve);
+ frame.contentWindow.history.go(0);
+ });
+ assert_equals(frame.contentDocument.body.textContent,
+ 'method = GET, isReloadNavigation = true');
+ }, 'FetchEvent#request.isReloadNavigation is true (history.go(0))');
+
+promise_test(async (t) => {
+ const page_url = 'resources/simple.html?isReloadNavigation';
+ const frame = await with_iframe(page_url);
+ t.add_cleanup(() => { frame.remove(); });
+ assert_equals(frame.contentDocument.body.textContent,
+ 'method = GET, isReloadNavigation = false');
+ await new Promise((resolve) => {
+ frame.addEventListener('load', resolve);
+ const form = frame.contentDocument.createElement('form');
+ form.method = 'POST';
+ form.name = 'form';
+ form.action = new Request(page_url).url;
+ frame.contentDocument.body.appendChild(form);
+ form.submit();
+ });
+ assert_equals(frame.contentDocument.body.textContent,
+ 'method = POST, isReloadNavigation = false');
+ await new Promise((resolve) => {
+ frame.addEventListener('load', resolve);
+ frame.contentWindow.location.reload();
+ });
+ assert_equals(frame.contentDocument.body.textContent,
+ 'method = POST, isReloadNavigation = true');
+ }, 'FetchEvent#request.isReloadNavigation is true (POST + location.reload())');
+
+promise_test(async (t) => {
+ const page_url = 'resources/simple.html?isReloadNavigation';
+ const anotherUrl = new Request('resources/simple.html').url;
+ let frame = await with_iframe(page_url);
+ t.add_cleanup(() => { frame.remove(); });
+ assert_equals(frame.contentDocument.body.textContent,
+ 'method = GET, isReloadNavigation = false');
+ // Use step_timeout(0) to ensure the history entry is created for Blink
+ // and WebKit. See https://bugs.webkit.org/show_bug.cgi?id=42861.
+ await wait(0);
+ await new Promise((resolve) => {
+ frame.addEventListener('load', resolve);
+ frame.src = anotherUrl;
+ });
+ assert_equals(frame.contentDocument.body.textContent, "Here's a simple html file.\n");
+ await new Promise((resolve) => {
+ frame.addEventListener('load', resolve);
+ frame.contentWindow.history.go(-1);
+ });
+ assert_equals(frame.contentDocument.body.textContent,
+ 'method = GET, isReloadNavigation = false');
+ await new Promise((resolve) => {
+ frame.addEventListener('load', resolve);
+ frame.contentWindow.history.go(0);
+ });
+ assert_equals(frame.contentDocument.body.textContent,
+ 'method = GET, isReloadNavigation = true');
+ await new Promise((resolve) => {
+ frame.addEventListener('load', resolve);
+ frame.contentWindow.history.go(1);
+ });
+ assert_equals(frame.contentDocument.body.textContent, "Here's a simple html file.\n");
+ }, 'FetchEvent#request.isReloadNavigation is true (with history traversal)');
+
+promise_test(async (t) => {
+ const page_url = 'resources/simple.html?isHistoryNavigation';
+ const anotherUrl = new Request('resources/simple.html?ignore').url;
+ const frame = await with_iframe(page_url);
+ t.add_cleanup(() => { frame.remove(); });
+ assert_equals(frame.contentDocument.body.textContent,
+ 'method = GET, isHistoryNavigation = false');
+ // Use step_timeout(0) to ensure the history entry is created for Blink
+ // and WebKit. See https://bugs.webkit.org/show_bug.cgi?id=42861.
+ await wait(0);
+ await new Promise((resolve) => {
+ frame.addEventListener('load', resolve);
+ frame.src = anotherUrl;
+ });
+ assert_equals(frame.contentDocument.body.textContent, "Here's a simple html file.\n");
+ await new Promise((resolve) => {
+ frame.addEventListener('load', resolve);
+ frame.contentWindow.history.go(-1);
+ });
+ assert_equals(frame.contentDocument.body.textContent,
+ 'method = GET, isHistoryNavigation = true');
+ }, 'FetchEvent#request.isHistoryNavigation is true (with history.go(-1))');
+
+promise_test(async (t) => {
+ const page_url = 'resources/simple.html?isHistoryNavigation';
+ const anotherUrl = new Request('resources/simple.html?ignore').url;
+ const frame = await with_iframe(anotherUrl);
+ t.add_cleanup(() => { frame.remove(); });
+ assert_equals(frame.contentDocument.body.textContent, "Here's a simple html file.\n");
+ // Use step_timeout(0) to ensure the history entry is created for Blink
+ // and WebKit. See https://bugs.webkit.org/show_bug.cgi?id=42861.
+ await wait(0);
+ await new Promise((resolve) => {
+ frame.addEventListener('load', resolve);
+ frame.src = page_url;
+ });
+ assert_equals(frame.contentDocument.body.textContent,
+ 'method = GET, isHistoryNavigation = false');
+ await new Promise((resolve) => {
+ frame.addEventListener('load', resolve);
+ frame.contentWindow.history.go(-1);
+ });
+ await new Promise((resolve) => {
+ frame.addEventListener('load', resolve);
+ frame.contentWindow.history.go(1);
+ });
+ assert_equals(frame.contentDocument.body.textContent,
+ 'method = GET, isHistoryNavigation = true');
+ }, 'FetchEvent#request.isHistoryNavigation is true (with history.go(1))');
+
+promise_test(async (t) => {
+ const page_url = 'resources/simple.html?isHistoryNavigation';
+ const anotherUrl = new Request('resources/simple.html?ignore').url;
+ const frame = await with_iframe(anotherUrl);
+ t.add_cleanup(() => { frame.remove(); });
+ assert_equals(frame.contentDocument.body.textContent, "Here's a simple html file.\n");
+ // Use step_timeout(0) to ensure the history entry is created for Blink
+ // and WebKit. See https://bugs.webkit.org/show_bug.cgi?id=42861.
+ await wait(0);
+ await new Promise((resolve) => {
+ frame.addEventListener('load', resolve);
+ frame.src = page_url;
+ });
+ assert_equals(frame.contentDocument.body.textContent,
+ 'method = GET, isHistoryNavigation = false');
+ await new Promise((resolve) => {
+ frame.addEventListener('load', resolve);
+ frame.contentWindow.history.go(-1);
+ });
+ await new Promise((resolve) => {
+ frame.addEventListener('load', resolve);
+ frame.contentWindow.history.go(1);
+ });
+ assert_equals(frame.contentDocument.body.textContent,
+ 'method = GET, isHistoryNavigation = true');
+ await new Promise((resolve) => {
+ frame.addEventListener('load', resolve);
+ frame.contentWindow.history.go(0);
+ });
+ assert_equals(frame.contentDocument.body.textContent,
+ 'method = GET, isHistoryNavigation = false');
+ }, 'FetchEvent#request.isHistoryNavigation is false (with history.go(0))');
+
+promise_test(async (t) => {
+ const page_url = 'resources/simple.html?isHistoryNavigation';
+ const anotherUrl = new Request('resources/simple.html?ignore').url;
+ const frame = await with_iframe(anotherUrl);
+ t.add_cleanup(() => { frame.remove(); });
+ assert_equals(frame.contentDocument.body.textContent, "Here's a simple html file.\n");
+ // Use step_timeout(0) to ensure the history entry is created for Blink
+ // and WebKit. See https://bugs.webkit.org/show_bug.cgi?id=42861.
+ await wait(0);
+ await new Promise((resolve) => {
+ frame.addEventListener('load', resolve);
+ frame.src = page_url;
+ });
+ assert_equals(frame.contentDocument.body.textContent,
+ 'method = GET, isHistoryNavigation = false');
+ await new Promise((resolve) => {
+ frame.addEventListener('load', resolve);
+ frame.contentWindow.history.go(-1);
+ });
+ await new Promise((resolve) => {
+ frame.addEventListener('load', resolve);
+ frame.contentWindow.history.go(1);
+ });
+ assert_equals(frame.contentDocument.body.textContent,
+ 'method = GET, isHistoryNavigation = true');
+ await new Promise((resolve) => {
+ frame.addEventListener('load', resolve);
+ frame.contentWindow.location.reload();
+ });
+ assert_equals(frame.contentDocument.body.textContent,
+ 'method = GET, isHistoryNavigation = false');
+ }, 'FetchEvent#request.isHistoryNavigation is false (with location.reload)');
+
+promise_test(async (t) => {
+ const page_url = 'resources/simple.html?isHistoryNavigation';
+ const anotherUrl = new Request('resources/simple.html?ignore').url;
+ const oneAnotherUrl = new Request('resources/simple.html?ignore2').url;
+
+ const frame = await with_iframe(page_url);
+ t.add_cleanup(() => { frame.remove(); });
+ assert_equals(frame.contentDocument.body.textContent,
+ 'method = GET, isHistoryNavigation = false');
+ // Use step_timeout(0) to ensure the history entry is created for Blink
+ // and WebKit. See https://bugs.webkit.org/show_bug.cgi?id=42861.
+ await wait(0);
+ await new Promise((resolve) => {
+ frame.addEventListener('load', resolve);
+ frame.src = anotherUrl;
+ });
+ assert_equals(frame.contentDocument.body.textContent, "Here's a simple html file.\n");
+ await wait(0);
+ await new Promise((resolve) => {
+ frame.addEventListener('load', resolve);
+ frame.src = oneAnotherUrl;
+ });
+ assert_equals(frame.contentDocument.body.textContent, "Here's a simple html file.\n");
+ await new Promise((resolve) => {
+ frame.addEventListener('load', resolve);
+ frame.contentWindow.history.go(-2);
+ });
+ assert_equals(frame.contentDocument.body.textContent,
+ 'method = GET, isHistoryNavigation = true');
+ }, 'FetchEvent#request.isHistoryNavigation is true (with history.go(-2))');
+
+promise_test(async (t) => {
+ const page_url = 'resources/simple.html?isHistoryNavigation';
+ const anotherUrl = new Request('resources/simple.html?ignore').url;
+ const oneAnotherUrl = new Request('resources/simple.html?ignore2').url;
+ const frame = await with_iframe(anotherUrl);
+ t.add_cleanup(() => { frame.remove(); });
+ assert_equals(frame.contentDocument.body.textContent, "Here's a simple html file.\n");
+ // Use step_timeout(0) to ensure the history entry is created for Blink
+ // and WebKit. See https://bugs.webkit.org/show_bug.cgi?id=42861.
+ await wait(0);
+ await new Promise((resolve) => {
+ frame.addEventListener('load', resolve);
+ frame.src = oneAnotherUrl;
+ });
+ assert_equals(frame.contentDocument.body.textContent, "Here's a simple html file.\n");
+ await wait(0);
+ await new Promise((resolve) => {
+ frame.addEventListener('load', resolve);
+ frame.src = page_url;
+ });
+ assert_equals(frame.contentDocument.body.textContent,
+ 'method = GET, isHistoryNavigation = false');
+ await new Promise((resolve) => {
+ frame.addEventListener('load', resolve);
+ frame.contentWindow.history.go(-2);
+ });
+ await new Promise((resolve) => {
+ frame.addEventListener('load', resolve);
+ frame.contentWindow.history.go(2);
+ });
+ assert_equals(frame.contentDocument.body.textContent,
+ 'method = GET, isHistoryNavigation = true');
+ }, 'FetchEvent#request.isHistoryNavigation is true (with history.go(2))');
+
+promise_test(async (t) => {
+ const page_url = 'resources/simple.html?isHistoryNavigation';
+ const anotherUrl = new Request('resources/simple.html?ignore').url;
+ const frame = await with_iframe(page_url);
+ t.add_cleanup(() => { frame.remove(); });
+ assert_equals(frame.contentDocument.body.textContent,
+ 'method = GET, isHistoryNavigation = false');
+ await new Promise((resolve) => {
+ frame.addEventListener('load', resolve);
+ const form = frame.contentDocument.createElement('form');
+ form.method = 'POST';
+ form.name = 'form';
+ form.action = new Request(page_url).url;
+ frame.contentDocument.body.appendChild(form);
+ form.submit();
+ });
+ assert_equals(frame.contentDocument.body.textContent,
+ 'method = POST, isHistoryNavigation = false');
+ // Use step_timeout(0) to ensure the history entry is created for Blink
+ // and WebKit. See https://bugs.webkit.org/show_bug.cgi?id=42861.
+ await wait(0);
+ await new Promise((resolve) => {
+ frame.addEventListener('load', resolve);
+ frame.src = anotherUrl;
+ });
+ assert_equals(frame.contentDocument.body.textContent, "Here's a simple html file.\n");
+ await wait(0);
+ await new Promise((resolve) => {
+ frame.addEventListener('load', resolve);
+ frame.contentWindow.history.go(-1);
+ });
+ assert_equals(frame.contentDocument.body.textContent,
+ 'method = POST, isHistoryNavigation = true');
+ }, 'FetchEvent#request.isHistoryNavigation is true (POST + history.go(-1))');
+
+// When service worker responds with a Response, no XHR upload progress
+// events are delivered.
+promise_test(async t => {
+ const page_url = 'resources/simple.html?ignore-for-request-body-string';
+ const frame = await with_iframe(page_url);
+ t.add_cleanup(() => { frame.remove(); });
+
+ const xhr = new frame.contentWindow.XMLHttpRequest();
+ xhr.open('POST', 'simple.html?request-body');
+ xhr.upload.addEventListener('progress', t.unreached_func('progress'));
+ xhr.upload.addEventListener('error', t.unreached_func('error'));
+ xhr.upload.addEventListener('abort', t.unreached_func('abort'));
+ xhr.upload.addEventListener('timeout', t.unreached_func('timeout'));
+ xhr.upload.addEventListener('load', t.unreached_func('load'));
+ xhr.upload.addEventListener('loadend', t.unreached_func('loadend'));
+ xhr.send('i am the request body');
+
+ await new Promise((resolve) => xhr.addEventListener('load', resolve));
+ }, 'XHR upload progress events for response coming from SW');
+
+// Upload progress events should be delivered for the network fallback case.
+promise_test(async t => {
+ const page_url = 'resources/simple.html?ignore-for-request-body-string';
+ const frame = await with_iframe(page_url);
+ t.add_cleanup(() => { frame.remove(); });
+
+ let progress = false;
+ let load = false;
+ let loadend = false;
+
+ const xhr = new frame.contentWindow.XMLHttpRequest();
+ xhr.open('POST', '/fetch/api/resources/echo-content.py?ignore');
+ xhr.upload.addEventListener('progress', () => progress = true);
+ xhr.upload.addEventListener('error', t.unreached_func('error'));
+ xhr.upload.addEventListener('abort', t.unreached_func('abort'));
+ xhr.upload.addEventListener('timeout', t.unreached_func('timeout'));
+ xhr.upload.addEventListener('load', () => load = true);
+ xhr.upload.addEventListener('loadend', () => loadend = true);
+ xhr.send('i am the request body');
+
+ await new Promise((resolve) => xhr.addEventListener('load', resolve));
+ assert_true(progress, 'progress');
+ assert_true(load, 'load');
+ assert_true(loadend, 'loadend');
+ }, 'XHR upload progress events for network fallback');
+
+promise_test(async t => {
+ // Set page_url to "?ignore" so the service worker falls back to network
+ // for the main resource request, and add a suffix to avoid colliding
+ // with other tests.
+ const page_url = 'resources/?ignore-for-request-body-fallback-string';
+
+ const frame = await with_iframe(page_url);
+ t.add_cleanup(() => { frame.remove(); });
+ // Add "?clone-and-ignore" so the service worker falls back to
+ // echo-content.py.
+ const echo_url = '/fetch/api/resources/echo-content.py?status=421';
+ const response = await frame.contentWindow.fetch(echo_url, {
+ method: 'POST',
+ body: 'text body'
+ });
+ assert_equals(response.status, 421);
+ const text = await response.text();
+ assert_equals(
+ text,
+ 'text body. Request was sent 1 times.',
+ 'the network fallback request should include the request body');
+ }, 'Fetch with POST with text on sw 421 response should not be retried.');
+</script>
+</body>
diff --git a/test/wpt/tests/service-workers/service-worker/fetch-frame-resource.https.html b/test/wpt/tests/service-workers/service-worker/fetch-frame-resource.https.html
new file mode 100644
index 0000000..a33309f
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/fetch-frame-resource.https.html
@@ -0,0 +1,236 @@
+<!DOCTYPE html>
+<title>Service Worker: Fetch for the frame loading.</title>
+<meta name=timeout content=long>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+var worker = 'resources/fetch-rewrite-worker.js';
+var path = base_path() + 'resources/fetch-access-control.py';
+var host_info = get_host_info();
+
+function getLoadedObject(win, contentFunc, closeFunc) {
+ return new Promise(function(resolve) {
+ function done(contentString) {
+ var result = null;
+ // fetch-access-control.py returns a string like "report( <json> )".
+ // Eval the returned string with a report functionto get the json
+ // object.
+ try {
+ function report(obj) { result = obj };
+ eval(contentString);
+ } catch(e) {
+ // just resolve null if we get unexpected page content
+ }
+ closeFunc(win);
+ resolve(result);
+ }
+
+ // We can't catch the network error on window. So we use the timer.
+ var timeout = setTimeout(function() {
+ // Failure pages are considered cross-origin in some browsers. This
+ // means you cannot even .resolve() the window because the check for
+ // the .then property will throw. Instead, treat cross-origin
+ // failure pages as the empty string which will fail to parse as the
+ // expected json result.
+ var content = '';
+ try {
+ content = contentFunc(win);
+ } catch(e) {
+ // use default empty string for cross-domain window
+ }
+ done(content);
+ }, 10000);
+
+ win.onload = function() {
+ clearTimeout(timeout);
+ let content = '';
+ try {
+ content = contentFunc(win);
+ } catch(e) {
+ // use default empty string for cross-domain window (see above)
+ }
+ done(content);
+ };
+ });
+}
+
+function getLoadedFrameAsObject(frame) {
+ return getLoadedObject(frame, function(f) {
+ return f.contentDocument.body.textContent;
+ }, function(f) {
+ f.parentNode.removeChild(f);
+ });
+}
+
+function getLoadedWindowAsObject(win) {
+ return getLoadedObject(win, function(w) {
+ return w.document.body.textContent
+ }, function(w) {
+ w.close();
+ });
+}
+
+promise_test(function(t) {
+ var scope = 'resources/fetch-frame-resource/frame-basic';
+ var frame;
+ return service_worker_unregister_and_register(t, worker, scope)
+ .then(function(reg) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ return wait_for_state(t, reg.installing, 'activated');
+ })
+ .then(function() {
+ frame = document.createElement('iframe');
+ frame.src =
+ scope + '?url=' +
+ encodeURIComponent(host_info['HTTPS_ORIGIN'] + path);
+ document.body.appendChild(frame);
+ return getLoadedFrameAsObject(frame);
+ })
+ .then(function(result) {
+ assert_equals(
+ result.jsonpResult,
+ 'success',
+ 'Basic type response could be loaded in the iframe.');
+ frame.remove();
+ });
+ }, 'Basic type response could be loaded in the iframe.');
+
+promise_test(function(t) {
+ var scope = 'resources/fetch-frame-resource/frame-cors';
+ var frame;
+ return service_worker_unregister_and_register(t, worker, scope)
+ .then(function(reg) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ return wait_for_state(t, reg.installing, 'activated');
+ })
+ .then(function() {
+ frame = document.createElement('iframe');
+ frame.src =
+ scope + '?mode=cors&url=' +
+ encodeURIComponent(host_info['HTTPS_REMOTE_ORIGIN'] + path +
+ '?ACAOrigin=' + host_info['HTTPS_ORIGIN'] +
+ '&ACACredentials=true');
+ document.body.appendChild(frame);
+ return getLoadedFrameAsObject(frame);
+ })
+ .then(function(result) {
+ assert_equals(
+ result.jsonpResult,
+ 'success',
+ 'CORS type response could be loaded in the iframe.');
+ frame.remove();
+ });
+ }, 'CORS type response could be loaded in the iframe.');
+
+promise_test(function(t) {
+ var scope = 'resources/fetch-frame-resource/frame-opaque';
+ var frame;
+ return service_worker_unregister_and_register(t, worker, scope)
+ .then(function(reg) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ return wait_for_state(t, reg.installing, 'activated');
+ })
+ .then(function() {
+ frame = document.createElement('iframe');
+ frame.src =
+ scope + '?mode=no-cors&url=' +
+ encodeURIComponent(host_info['HTTPS_REMOTE_ORIGIN'] + path);
+ document.body.appendChild(frame);
+ return getLoadedFrameAsObject(frame);
+ })
+ .then(function(result) {
+ assert_equals(
+ result,
+ null,
+ 'Opaque type response could not be loaded in the iframe.');
+ frame.remove();
+ });
+ }, 'Opaque type response could not be loaded in the iframe.');
+
+promise_test(function(t) {
+ var scope = 'resources/fetch-frame-resource/window-basic';
+ return service_worker_unregister_and_register(t, worker, scope)
+ .then(function(reg) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ return wait_for_state(t, reg.installing, 'activated');
+ })
+ .then(function() {
+ var win = window.open(
+ scope + '?url=' +
+ encodeURIComponent(host_info['HTTPS_ORIGIN'] + path));
+ return getLoadedWindowAsObject(win);
+ })
+ .then(function(result) {
+ assert_equals(
+ result.jsonpResult,
+ 'success',
+ 'Basic type response could be loaded in the new window.');
+ });
+ }, 'Basic type response could be loaded in the new window.');
+
+promise_test(function(t) {
+ var scope = 'resources/fetch-frame-resource/window-cors';
+ return service_worker_unregister_and_register(t, worker, scope)
+ .then(function(reg) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ return wait_for_state(t, reg.installing, 'activated');
+ })
+ .then(function() {
+ var win = window.open(
+ scope + '?mode=cors&url=' +
+ encodeURIComponent(host_info['HTTPS_REMOTE_ORIGIN'] + path +
+ '?ACAOrigin=' + host_info['HTTPS_ORIGIN'] +
+ '&ACACredentials=true'));
+ return getLoadedWindowAsObject(win);
+ })
+ .then(function(result) {
+ assert_equals(
+ result.jsonpResult,
+ 'success',
+ 'CORS type response could be loaded in the new window.');
+ });
+ }, 'CORS type response could be loaded in the new window.');
+
+promise_test(function(t) {
+ var scope = 'resources/fetch-frame-resource/window-opaque';
+ return service_worker_unregister_and_register(t, worker, scope)
+ .then(function(reg) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ return wait_for_state(t, reg.installing, 'activated');
+ })
+ .then(function() {
+ var win = window.open(
+ scope + '?mode=no-cors&url=' +
+ encodeURIComponent(host_info['HTTPS_REMOTE_ORIGIN'] + path));
+ return getLoadedWindowAsObject(win);
+ })
+ .then(function(result) {
+ assert_equals(
+ result,
+ null,
+ 'Opaque type response could not be loaded in the new window.');
+ });
+ }, 'Opaque type response could not be loaded in the new window.');
+</script>
+</body>
diff --git a/test/wpt/tests/service-workers/service-worker/fetch-header-visibility.https.html b/test/wpt/tests/service-workers/service-worker/fetch-header-visibility.https.html
new file mode 100644
index 0000000..1f4813c
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/fetch-header-visibility.https.html
@@ -0,0 +1,54 @@
+<!DOCTYPE html>
+<title>Service Worker: Visibility of headers during fetch.</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+ var worker = 'resources/fetch-rewrite-worker.js';
+ var path = base_path() + 'resources/fetch-access-control.py';
+ var host_info = get_host_info();
+ var frame;
+
+ promise_test(function(t) {
+ var scope = 'resources/fetch-header-visibility-iframe.html';
+ return service_worker_unregister_and_register(t, worker, scope)
+ .then(function(reg) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ return wait_for_state(t, reg.installing, 'activated');
+ })
+ .then(function() {
+ frame = document.createElement('iframe');
+ frame.src = scope;
+ document.body.appendChild(frame);
+
+ // Resolve a promise when we recieve 2 success messages
+ return new Promise(function(resolve, reject) {
+ var remaining = 4;
+ function onMessage(e) {
+ if (e.data == 'PASS') {
+ remaining--;
+ if (remaining == 0) {
+ resolve();
+ } else {
+ return;
+ }
+ } else {
+ reject(e.data);
+ }
+
+ window.removeEventListener('message', onMessage);
+ }
+ window.addEventListener('message', onMessage);
+ });
+ })
+ .then(function(result) {
+ frame.remove();
+ });
+ }, 'Visibility of defaulted headers during interception');
+</script>
+</body>
diff --git a/test/wpt/tests/service-workers/service-worker/fetch-mixed-content-to-inscope.https.html b/test/wpt/tests/service-workers/service-worker/fetch-mixed-content-to-inscope.https.html
new file mode 100644
index 0000000..0e8fa93
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/fetch-mixed-content-to-inscope.https.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<title>Service Worker: Mixed content of fetch()</title>
+<meta name=timeout content=long>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js?pipe=sub"></script>
+<body></body>
+<script>
+async_test(function(t) {
+ var host_info = get_host_info();
+ window.addEventListener('message', t.step_func(on_message), false);
+ with_iframe(
+ host_info['HTTPS_ORIGIN'] + base_path() +
+ 'resources/fetch-mixed-content-iframe.html?target=inscope');
+ function on_message(e) {
+ assert_equals(e.data.results, 'finish');
+ t.done();
+ }
+ }, 'Verify Mixed content of fetch() in a Service Worker');
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/fetch-mixed-content-to-outscope.https.html b/test/wpt/tests/service-workers/service-worker/fetch-mixed-content-to-outscope.https.html
new file mode 100644
index 0000000..391dc5d
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/fetch-mixed-content-to-outscope.https.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<title>Service Worker: Mixed content of fetch()</title>
+<meta name=timeout content=long>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js?pipe=sub"></script>
+<body></body>
+<script>
+async_test(function(t) {
+ var host_info = get_host_info();
+ window.addEventListener('message', t.step_func(on_message), false);
+ with_iframe(
+ host_info['HTTPS_ORIGIN'] + base_path() +
+ 'resources/fetch-mixed-content-iframe.html?target=outscope');
+ function on_message(e) {
+ assert_equals(e.data.results, 'finish');
+ t.done();
+ }
+ }, 'Verify Mixed content of fetch() in a Service Worker');
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/fetch-request-css-base-url.https.html b/test/wpt/tests/service-workers/service-worker/fetch-request-css-base-url.https.html
new file mode 100644
index 0000000..467a66c
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/fetch-request-css-base-url.https.html
@@ -0,0 +1,87 @@
+<!DOCTYPE html>
+<title>Service Worker: CSS's base URL must be the response URL</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js?pipe=sub"></script>
+<script>
+const SCOPE = 'resources/fetch-request-css-base-url-iframe.html';
+const SCRIPT = 'resources/fetch-request-css-base-url-worker.js';
+let worker;
+
+var signalMessage;
+function getNextMessage() {
+ return new Promise(resolve => { signalMessage = resolve; });
+}
+
+promise_test(async (t) => {
+ const registration = await service_worker_unregister_and_register(
+ t, SCRIPT, SCOPE);
+ worker = registration.installing;
+ await wait_for_state(t, worker, 'activated');
+}, 'global setup');
+
+// Creates a test concerning the base URL of a stylesheet. It loads a
+// stylesheet from a controlled page. The stylesheet makes a subresource
+// request for an image. The service worker messages back the details of the
+// image request in order to test the base URL.
+//
+// The request URL for the stylesheet is under "resources/request-url-path/".
+// The service worker may respond in a way such that the response URL is
+// different to the request URL.
+function base_url_test(params) {
+ promise_test(async (t) => {
+ let frame;
+ t.add_cleanup(() => {
+ if (frame)
+ frame.remove();
+ });
+
+ // Ask the service worker to message this page once it gets the request
+ // for the image.
+ let channel = new MessageChannel();
+ const sawPong = getNextMessage();
+ channel.port1.onmessage = (event) => {
+ signalMessage(event.data);
+ };
+ worker.postMessage({port:channel.port2},[channel.port2]);
+
+ // It sends a pong back immediately. This ping/pong protocol helps deflake
+ // the test for browsers where message/fetch ordering isn't guaranteed.
+ assert_equals('pong', await sawPong);
+
+ // Load the frame which will load the stylesheet that makes the image
+ // request.
+ const sawResult = getNextMessage();
+ frame = await with_iframe(params.framePath);
+ const result = await sawResult;
+
+ // Test the image request.
+ const base = new URL('.', document.location).href;
+ assert_equals(result.url,
+ base + params.expectImageRequestPath,
+ 'request');
+ assert_equals(result.referrer,
+ base + params.expectImageRequestReferrer,
+ 'referrer');
+ }, params.description);
+}
+
+const cssFile = 'fetch-request-css-base-url-style.css';
+
+base_url_test({
+ framePath: SCOPE + '?fetch',
+ expectImageRequestPath: 'resources/sample.png',
+ expectImageRequestReferrer: `resources/${cssFile}?fetch`,
+ description: 'base URL when service worker does respondWith(fetch(responseUrl)).'});
+
+base_url_test({
+ framePath: SCOPE + '?newResponse',
+ expectImageRequestPath: 'resources/request-url-path/sample.png',
+ expectImageRequestReferrer: `resources/request-url-path/${cssFile}?newResponse`,
+ description: 'base URL when service worker does respondWith(new Response()).'});
+
+// Cleanup step: this must be the last promise_test.
+promise_test(async (t) => {
+ return service_worker_unregister(t, SCOPE);
+}, 'cleanup global state');
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/fetch-request-css-cross-origin.https.html b/test/wpt/tests/service-workers/service-worker/fetch-request-css-cross-origin.https.html
new file mode 100644
index 0000000..d9c1c7f
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/fetch-request-css-cross-origin.https.html
@@ -0,0 +1,81 @@
+<!DOCTYPE html>
+<title>Service Worker: Cross-origin CSS files fetched via SW.</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+
+function getElementColorInFrame(frame, id) {
+ var element = frame.contentDocument.getElementById(id);
+ var style = frame.contentWindow.getComputedStyle(element, '');
+ return style['color'];
+}
+
+promise_test(async t => {
+ var SCOPE =
+ 'resources/fetch-request-css-cross-origin';
+ var SCRIPT =
+ 'resources/fetch-request-css-cross-origin-worker.js';
+ let registration = await service_worker_unregister_and_register(
+ t, SCRIPT, SCOPE);
+ promise_test(async t => {
+ await registration.unregister();
+ }, 'cleanup global state');
+
+ await wait_for_state(t, registration.installing, 'activated');
+}, 'setup global state');
+
+promise_test(async t => {
+ const EXPECTED_COLOR = 'rgb(0, 0, 255)';
+ const PAGE =
+ 'resources/fetch-request-css-cross-origin-mime-check-iframe.html';
+
+ const f = await with_iframe(PAGE);
+ t.add_cleanup(() => {f.remove(); });
+ assert_equals(
+ getElementColorInFrame(f, 'crossOriginCss'),
+ EXPECTED_COLOR,
+ 'The color must be overridden by cross origin CSS.');
+ assert_equals(
+ getElementColorInFrame(f, 'crossOriginHtml'),
+ EXPECTED_COLOR,
+ 'The color must not be overridden by cross origin non CSS file.');
+ assert_equals(
+ getElementColorInFrame(f, 'sameOriginCss'),
+ EXPECTED_COLOR,
+ 'The color must be overridden by same origin CSS.');
+ assert_equals(
+ getElementColorInFrame(f, 'sameOriginHtml'),
+ EXPECTED_COLOR,
+ 'The color must be overridden by same origin non CSS file.');
+ assert_equals(
+ getElementColorInFrame(f, 'synthetic'),
+ EXPECTED_COLOR,
+ 'The color must be overridden by synthetic CSS.');
+}, 'MIME checking of CSS resources fetched via service worker when Content-Type is not set.');
+
+promise_test(async t => {
+ const PAGE =
+ 'resources/fetch-request-css-cross-origin-read-contents.html';
+
+ const f = await with_iframe(PAGE);
+ t.add_cleanup(() => {f.remove(); });
+ assert_throws_dom('SecurityError', f.contentWindow.DOMException, () => {
+ f.contentDocument.styleSheets[0].cssRules[0].cssText;
+ });
+ assert_equals(
+ f.contentDocument.styleSheets[1].cssRules[0].cssText,
+ '#crossOriginCss { color: blue; }',
+ 'cross-origin CORS approved response');
+ assert_equals(
+ f.contentDocument.styleSheets[2].cssRules[0].cssText,
+ '#sameOriginCss { color: blue; }',
+ 'same-origin response');
+ assert_equals(
+ f.contentDocument.styleSheets[3].cssRules[0].cssText,
+ '#synthetic { color: blue; }',
+ 'service worker generated response');
+ }, 'Same-origin policy for access to CSS resources fetched via service worker');
+
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/fetch-request-css-images.https.html b/test/wpt/tests/service-workers/service-worker/fetch-request-css-images.https.html
new file mode 100644
index 0000000..586dea2
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/fetch-request-css-images.https.html
@@ -0,0 +1,214 @@
+<!DOCTYPE html>
+<title>Service Worker: FetchEvent for css image</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js?pipe=sub"></script>
+<script>
+var SCOPE = 'resources/fetch-request-resources-iframe.https.html';
+var SCRIPT = 'resources/fetch-request-resources-worker.js';
+var host_info = get_host_info();
+var LOCAL_URL =
+ host_info['HTTPS_ORIGIN'] + base_path() + 'resources/sample?test';
+var REMOTE_URL =
+ host_info['HTTPS_REMOTE_ORIGIN'] + base_path() + 'resources/sample?test';
+
+function css_image_test(expected_results, frame, url, type,
+ expected_mode, expected_credentials) {
+ expected_results[url] = {
+ url: url,
+ mode: expected_mode,
+ credentials: expected_credentials,
+ message: 'CSSImage load (url:' + url + ' type:' + type + ')'
+ };
+ return frame.contentWindow.load_css_image(url, type);
+}
+
+function css_image_set_test(expected_results, frame, url, type,
+ expected_mode, expected_credentials) {
+ expected_results[url] = {
+ url: url,
+ mode: expected_mode,
+ credentials: expected_credentials,
+ message: 'CSSImageSet load (url:' + url + ' type:' + type + ')'
+ };
+ return frame.contentWindow.load_css_image_set(url, type);
+}
+
+function waitForWorker(worker) {
+ return new Promise(function(resolve) {
+ var channel = new MessageChannel();
+ channel.port1.addEventListener('message', function(msg) {
+ if (msg.data.ready) {
+ resolve(channel);
+ }
+ });
+ channel.port1.start();
+ worker.postMessage({port: channel.port2}, [channel.port2]);
+ });
+}
+
+function create_message_promise(channel, expected_results, worker, scope) {
+ return new Promise(function(resolve) {
+ channel.port1.addEventListener('message', function(msg) {
+ var result = msg.data;
+ if (!expected_results[result.url]) {
+ return;
+ }
+ resolve(result);
+ });
+ }).then(function(result) {
+ var expected = expected_results[result.url];
+ assert_equals(
+ result.mode, expected.mode,
+ 'mode of ' + expected.message + ' must be ' +
+ expected.mode + '.');
+ assert_equals(
+ result.credentials, expected.credentials,
+ 'credentials of ' + expected.message + ' must be ' +
+ expected.credentials + '.');
+ delete expected_results[result.url];
+ });
+}
+
+promise_test(function(t) {
+ var scope = SCOPE + "?img=backgroundImage";
+ var expected_results = {};
+ var worker;
+ var frame;
+ return service_worker_unregister_and_register(t, SCRIPT, scope)
+ .then(function(registration) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ worker = registration.installing;
+ return wait_for_state(t, worker, 'activated');
+ })
+ .then(function() { return with_iframe(scope); })
+ .then(function(f) {
+ t.add_cleanup(function() {
+ f.remove();
+ });
+ frame = f;
+ return waitForWorker(worker);
+ })
+ .then(function(channel) {
+ css_image_test(expected_results, frame, LOCAL_URL + Date.now(),
+ 'backgroundImage', 'no-cors', 'include');
+ css_image_test(expected_results, frame, REMOTE_URL + Date.now(),
+ 'backgroundImage', 'no-cors', 'include');
+
+ return Promise.all([
+ create_message_promise(channel, expected_results, worker, scope),
+ create_message_promise(channel, expected_results, worker, scope)
+ ]);
+ });
+ }, 'Verify FetchEvent for css image (backgroundImage).');
+
+promise_test(function(t) {
+ var scope = SCOPE + "?img=shapeOutside";
+ var expected_results = {};
+ var worker;
+ var frame;
+ return service_worker_unregister_and_register(t, SCRIPT, scope)
+ .then(function(registration) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ worker = registration.installing;
+ return wait_for_state(t, worker, 'activated');
+ })
+ .then(function() { return with_iframe(scope); })
+ .then(function(f) {
+ t.add_cleanup(function() {
+ f.remove();
+ });
+ frame = f;
+ return waitForWorker(worker);
+ })
+ .then(function(channel) {
+ css_image_test(expected_results, frame, LOCAL_URL + Date.now(),
+ 'shapeOutside', 'cors', 'same-origin');
+ css_image_test(expected_results, frame, REMOTE_URL + Date.now(),
+ 'shapeOutside', 'cors', 'same-origin');
+
+ return Promise.all([
+ create_message_promise(channel, expected_results, worker, scope),
+ create_message_promise(channel, expected_results, worker, scope)
+ ]);
+ });
+ }, 'Verify FetchEvent for css image (shapeOutside).');
+
+promise_test(function(t) {
+ var scope = SCOPE + "?img_set=backgroundImage";
+ var expected_results = {};
+ var worker;
+ var frame;
+ return service_worker_unregister_and_register(t, SCRIPT, scope)
+ .then(function(registration) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ worker = registration.installing;
+ return wait_for_state(t, worker, 'activated');
+ })
+ .then(function() { return with_iframe(scope); })
+ .then(function(f) {
+ t.add_cleanup(function() {
+ f.remove();;
+ });
+ frame = f;
+ return waitForWorker(worker);
+ })
+ .then(function(channel) {
+ css_image_set_test(expected_results, frame, LOCAL_URL + Date.now(),
+ 'backgroundImage', 'no-cors', 'include');
+ css_image_set_test(expected_results, frame, REMOTE_URL + Date.now(),
+ 'backgroundImage', 'no-cors', 'include');
+
+ return Promise.all([
+ create_message_promise(channel, expected_results, worker, scope),
+ create_message_promise(channel, expected_results, worker, scope)
+ ]);
+ });
+ }, 'Verify FetchEvent for css image-set (backgroundImage).');
+
+promise_test(function(t) {
+ var scope = SCOPE + "?img_set=shapeOutside";
+ var expected_results = {};
+ var worker;
+ var frame;
+ return service_worker_unregister_and_register(t, SCRIPT, scope)
+ .then(function(registration) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ worker = registration.installing;
+ return wait_for_state(t, worker, 'activated');
+ })
+ .then(function() { return with_iframe(scope); })
+ .then(function(f) {
+ t.add_cleanup(function() {
+ f.remove();
+ });
+ frame = f;
+ return waitForWorker(worker);
+ })
+ .then(function(channel) {
+ css_image_set_test(expected_results, frame, LOCAL_URL + Date.now(),
+ 'shapeOutside', 'cors', 'same-origin');
+ css_image_set_test(expected_results, frame, REMOTE_URL + Date.now(),
+ 'shapeOutside', 'cors', 'same-origin');
+
+ return Promise.all([
+ create_message_promise(channel, expected_results, worker, scope),
+ create_message_promise(channel, expected_results, worker, scope)
+ ]);
+ });
+ }, 'Verify FetchEvent for css image-set (shapeOutside).');
+
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/fetch-request-fallback.https.html b/test/wpt/tests/service-workers/service-worker/fetch-request-fallback.https.html
new file mode 100644
index 0000000..a29f31d
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/fetch-request-fallback.https.html
@@ -0,0 +1,282 @@
+<!DOCTYPE html>
+<title>Service Worker: the fallback behavior of FetchEvent</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+function get_fetched_urls(worker) {
+ return new Promise(function(resolve) {
+ var channel = new MessageChannel();
+ channel.port1.onmessage = function(msg) { resolve(msg); };
+ worker.postMessage({port: channel.port2}, [channel.port2]);
+ });
+}
+
+function check_urls(worker, expected_requests) {
+ return get_fetched_urls(worker)
+ .then(function(msg) {
+ var requests = msg.data.requests;
+ assert_object_equals(requests, expected_requests);
+ });
+}
+
+var path = new URL(".", window.location).pathname;
+var SCOPE = 'resources/fetch-request-fallback-iframe.html';
+var SCRIPT = 'resources/fetch-request-fallback-worker.js';
+var host_info = get_host_info();
+var BASE_URL = host_info['HTTPS_ORIGIN'] +
+ path + 'resources/fetch-access-control.py?';
+var BASE_PNG_URL = BASE_URL + 'PNGIMAGE&';
+var OTHER_BASE_URL = host_info['HTTPS_REMOTE_ORIGIN'] +
+ path + 'resources/fetch-access-control.py?';
+var OTHER_BASE_PNG_URL = OTHER_BASE_URL + 'PNGIMAGE&';
+var REDIRECT_URL = host_info['HTTPS_ORIGIN'] +
+ path + 'resources/redirect.py?Redirect=';
+var register;
+
+promise_test(function(t) {
+ var registration;
+ var worker;
+
+ register = service_worker_unregister_and_register(t, SCRIPT, SCOPE)
+ .then(function(r) {
+ registration = r;
+ worker = registration.installing;
+ return wait_for_state(t, worker, 'activated');
+ })
+ .then(function() { return with_iframe(SCOPE); })
+ .then(function(frame) {
+ // This test should not be considered complete until after the service
+ // worker has been unregistered. Currently, `testharness.js` does not
+ // support asynchronous global "tear down" logic, so this must be
+ // expressed using a dedicated `promise_test`. Because the other
+ // sub-tests in this file are declared synchronously, this test will be
+ // the final test executed.
+ promise_test(function(t) {
+ t.add_cleanup(function() {
+ frame.remove();
+ });
+ return registration.unregister();
+ }, 'restore global state');
+
+ return {frame: frame, worker: worker};
+ });
+
+ return register;
+ }, 'initialize global state');
+
+function promise_frame_test(body, desc) {
+ promise_test(function(test) {
+ return register.then(function(result) {
+ return body(test, result.frame, result.worker);
+ });
+ }, desc);
+}
+
+promise_frame_test(function(t, frame, worker) {
+ return check_urls(
+ worker,
+ [{
+ url: host_info['HTTPS_ORIGIN'] + path + SCOPE,
+ mode: 'navigate'
+ }]);
+ }, 'The SW must intercept the request for a main resource.');
+
+promise_frame_test(function(t, frame, worker) {
+ return frame.contentWindow.xhr(BASE_URL)
+ .then(function() {
+ return check_urls(
+ worker,
+ [{ url: BASE_URL, mode: 'cors' }]);
+ });
+ }, 'The SW must intercept the request of same origin XHR.');
+
+promise_frame_test(function(t, frame, worker) {
+ return promise_rejects_js(
+ t,
+ frame.contentWindow.Error,
+ frame.contentWindow.xhr(OTHER_BASE_URL),
+ 'SW fallbacked CORS-unsupported other origin XHR should fail.')
+ .then(function() {
+ return check_urls(
+ worker,
+ [{ url: OTHER_BASE_URL, mode: 'cors' }]);
+ });
+ }, 'The SW must intercept the request of CORS-unsupported other origin XHR.');
+
+promise_frame_test(function(t, frame, worker) {
+ return frame.contentWindow.xhr(OTHER_BASE_URL + 'ACAOrigin=*')
+ .then(function() {
+ return check_urls(
+ worker,
+ [{ url: OTHER_BASE_URL + 'ACAOrigin=*', mode: 'cors' }]);
+ })
+ }, 'The SW must intercept the request of CORS-supported other origin XHR.');
+
+promise_frame_test(function(t, frame, worker) {
+ return frame.contentWindow.xhr(
+ REDIRECT_URL + encodeURIComponent(BASE_URL))
+ .then(function() {
+ return check_urls(
+ worker,
+ [{
+ url: REDIRECT_URL + encodeURIComponent(BASE_URL),
+ mode: 'cors'
+ }]);
+ });
+ }, 'The SW must intercept only the first request of redirected XHR.');
+
+promise_frame_test(function(t, frame, worker) {
+ return promise_rejects_js(
+ t,
+ frame.contentWindow.Error,
+ frame.contentWindow.xhr(
+ REDIRECT_URL + encodeURIComponent(OTHER_BASE_URL)),
+ 'SW fallbacked XHR which is redirected to CORS-unsupported ' +
+ 'other origin should fail.')
+ .then(function() {
+ return check_urls(
+ worker,
+ [{
+ url: REDIRECT_URL + encodeURIComponent(OTHER_BASE_URL),
+ mode: 'cors'
+ }]);
+ });
+ }, 'The SW must intercept only the first request for XHR which is' +
+ ' redirected to CORS-unsupported other origin.');
+
+promise_frame_test(function(t, frame, worker) {
+ return frame.contentWindow.xhr(
+ REDIRECT_URL +
+ encodeURIComponent(OTHER_BASE_URL + 'ACAOrigin=*'))
+ .then(function() {
+ return check_urls(
+ worker,
+ [{
+ url: REDIRECT_URL +
+ encodeURIComponent(OTHER_BASE_URL + 'ACAOrigin=*'),
+ mode: 'cors'
+ }]);
+ });
+ }, 'The SW must intercept only the first request for XHR which is ' +
+ 'redirected to CORS-supported other origin.');
+
+promise_frame_test(function(t, frame, worker) {
+ return frame.contentWindow.load_image(BASE_PNG_URL, '')
+ .then(function() {
+ return check_urls(
+ worker,
+ [{ url: BASE_PNG_URL, mode: 'no-cors' }]);
+ });
+ }, 'The SW must intercept the request for image.');
+
+promise_frame_test(function(t, frame, worker) {
+ return frame.contentWindow.load_image(OTHER_BASE_PNG_URL, '')
+ .then(function() {
+ return check_urls(
+ worker,
+ [{ url: OTHER_BASE_PNG_URL, mode: 'no-cors' }]);
+ });
+ }, 'The SW must intercept the request for other origin image.');
+
+promise_frame_test(function(t, frame, worker) {
+ return promise_rejects_js(
+ t,
+ frame.contentWindow.Error,
+ frame.contentWindow.load_image(OTHER_BASE_PNG_URL, 'anonymous'),
+ 'SW fallbacked CORS-unsupported other origin image request ' +
+ 'should fail.')
+ .then(function() {
+ return check_urls(
+ worker,
+ [{ url: OTHER_BASE_PNG_URL, mode: 'cors' }]);
+ })
+ }, 'The SW must intercept the request for CORS-unsupported other ' +
+ 'origin image.');
+
+promise_frame_test(function(t, frame, worker) {
+ return frame.contentWindow.load_image(
+ OTHER_BASE_PNG_URL + 'ACAOrigin=*', 'anonymous')
+ .then(function() {
+ return check_urls(
+ worker,
+ [{ url: OTHER_BASE_PNG_URL + 'ACAOrigin=*', mode: 'cors' }]);
+ });
+ }, 'The SW must intercept the request for CORS-supported other ' +
+ 'origin image.');
+
+promise_frame_test(function(t, frame, worker) {
+ return frame.contentWindow.load_image(
+ REDIRECT_URL + encodeURIComponent(BASE_PNG_URL), '')
+ .catch(function() {
+ assert_unreached(
+ 'SW fallbacked redirected image request should succeed.');
+ })
+ .then(function() {
+ return check_urls(
+ worker,
+ [{
+ url: REDIRECT_URL + encodeURIComponent(BASE_PNG_URL),
+ mode: 'no-cors'
+ }]);
+ });
+ }, 'The SW must intercept only the first request for redirected ' +
+ 'image resource.');
+
+promise_frame_test(function(t, frame, worker) {
+ return frame.contentWindow.load_image(
+ REDIRECT_URL + encodeURIComponent(OTHER_BASE_PNG_URL), '')
+ .catch(function() {
+ assert_unreached(
+ 'SW fallbacked image request which is redirected to ' +
+ 'other origin should succeed.');
+ })
+ .then(function() {
+ return check_urls(
+ worker,
+ [{
+ url: REDIRECT_URL + encodeURIComponent(OTHER_BASE_PNG_URL),
+ mode: 'no-cors'
+ }]);
+ })
+ }, 'The SW must intercept only the first request for image ' +
+ 'resource which is redirected to other origin.');
+
+promise_frame_test(function(t, frame, worker) {
+ return promise_rejects_js(
+ t,
+ frame.contentWindow.Error,
+ frame.contentWindow.load_image(
+ REDIRECT_URL + encodeURIComponent(OTHER_BASE_PNG_URL),
+ 'anonymous'),
+ 'SW fallbacked image request which is redirected to ' +
+ 'CORS-unsupported other origin should fail.')
+ .then(function() {
+ return check_urls(
+ worker,
+ [{
+ url: REDIRECT_URL + encodeURIComponent(OTHER_BASE_PNG_URL),
+ mode: 'cors'
+ }]);
+ });
+ }, 'The SW must intercept only the first request for image ' +
+ 'resource which is redirected to CORS-unsupported other origin.');
+
+promise_frame_test(function(t, frame, worker) {
+ return frame.contentWindow.load_image(
+ REDIRECT_URL +
+ encodeURIComponent(OTHER_BASE_PNG_URL + 'ACAOrigin=*'),
+ 'anonymous')
+ .then(function() {
+ return check_urls(
+ worker,
+ [{
+ url: REDIRECT_URL +
+ encodeURIComponent(OTHER_BASE_PNG_URL + 'ACAOrigin=*'),
+ mode: 'cors'
+ }]);
+ });
+ }, 'The SW must intercept only the first request for image ' +
+ 'resource which is redirected to CORS-supported other origin.');
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/fetch-request-no-freshness-headers.https.html b/test/wpt/tests/service-workers/service-worker/fetch-request-no-freshness-headers.https.html
new file mode 100644
index 0000000..03b7d35
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/fetch-request-no-freshness-headers.https.html
@@ -0,0 +1,55 @@
+<!DOCTYPE html>
+<title>Service Worker: the headers of FetchEvent shouldn't contain freshness headers</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js?pipe=sub"></script>
+<script>
+promise_test(function(t) {
+ var SCOPE = 'resources/fetch-request-no-freshness-headers-iframe.html';
+ var SCRIPT = 'resources/fetch-request-no-freshness-headers-worker.js';
+ var worker;
+ return service_worker_unregister_and_register(t, SCRIPT, SCOPE)
+ .then(function(registration) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, SCOPE);
+ });
+
+ worker = registration.installing;
+ return wait_for_state(t, worker, 'activated');
+ })
+ .then(function() { return with_iframe(SCOPE); })
+ .then(function(frame) {
+ return new Promise(function(resolve) {
+ frame.onload = function() {
+ resolve(frame);
+ };
+ frame.contentWindow.location.reload();
+ });
+ })
+ .then(function(frame) {
+ return new Promise(function(resolve) {
+ var channel = new MessageChannel();
+ channel.port1.onmessage = t.step_func(function(msg) {
+ frame.remove();
+ resolve(msg);
+ });
+ worker.postMessage(
+ {port: channel.port2}, [channel.port2]);
+ });
+ })
+ .then(function(msg) {
+ var freshness_headers = {
+ 'if-none-match': true,
+ 'if-modified-since': true
+ };
+ msg.data.requests.forEach(function(request) {
+ request.headers.forEach(function(header) {
+ assert_false(
+ !!freshness_headers[header[0]],
+ header[0] + ' header must not be set in the ' +
+ 'FetchEvent\'s request. (url = ' + request.url + ')');
+ });
+ })
+ });
+ }, 'The headers of FetchEvent shouldn\'t contain freshness headers.');
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/fetch-request-redirect.https.html b/test/wpt/tests/service-workers/service-worker/fetch-request-redirect.https.html
new file mode 100644
index 0000000..5ce015b
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/fetch-request-redirect.https.html
@@ -0,0 +1,385 @@
+<!DOCTYPE html>
+<title>Service Worker: FetchEvent for resources</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="/common/media.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+var test_scope = ""
+function assert_resolves(promise, description) {
+ return promise.then(
+ () => test(() => {}, description + " - " + test_scope),
+ (e) => test(() => { throw e; }, description + " - " + test_scope)
+ );
+}
+
+function assert_rejects(promise, description) {
+ return promise.then(
+ () => test(() => { assert_unreached(); }, description + " - " + test_scope),
+ () => test(() => {}, description + " - " + test_scope)
+ );
+}
+
+function iframe_test(url, timeout_enabled) {
+ return new Promise(function(resolve, reject) {
+ var frame = document.createElement('iframe');
+ frame.src = url;
+ if (timeout_enabled) {
+ // We can't catch the network error on iframe. So we use the timer for
+ // failure detection.
+ var timer = setTimeout(function() {
+ reject(new Error('iframe load timeout'));
+ frame.remove();
+ }, 10000);
+ }
+ frame.onload = function() {
+ if (timeout_enabled)
+ clearTimeout(timer);
+ try {
+ if (frame.contentDocument.body.textContent == 'Hello world\n')
+ resolve();
+ else
+ reject(new Error('content mismatch'));
+ } catch (e) {
+ // Chrome treats iframes that failed to load due to a network error as
+ // having a different origin, so accessing contentDocument throws an
+ // error. Other browsers might have different behavior.
+ reject(new Error(e));
+ }
+ frame.remove();
+ };
+ document.body.appendChild(frame);
+ });
+}
+
+promise_test(function(t) {
+ test_scope = "default";
+
+ var SCOPE = 'resources/fetch-request-redirect-iframe.html';
+ var SCRIPT = 'resources/fetch-rewrite-worker.js';
+ var REDIRECT_URL = base_path() + 'resources/redirect.py?Redirect=';
+ var IMAGE_URL = base_path() + 'resources/square.png';
+ var AUDIO_URL = getAudioURI("/media/sound_5");
+ var XHR_URL = base_path() + 'resources/simple.txt';
+ var HTML_URL = base_path() + 'resources/sample.html';
+
+ var REDIRECT_TO_IMAGE_URL = REDIRECT_URL + encodeURIComponent(IMAGE_URL);
+ var REDIRECT_TO_AUDIO_URL = REDIRECT_URL + encodeURIComponent(AUDIO_URL);
+ var REDIRECT_TO_XHR_URL = REDIRECT_URL + encodeURIComponent(XHR_URL);
+ var REDIRECT_TO_HTML_URL = REDIRECT_URL + encodeURIComponent(HTML_URL);
+
+ var worker;
+ var frame;
+ return service_worker_unregister_and_register(t, SCRIPT, SCOPE)
+ .then(function(registration) {
+ t.add_cleanup(() => service_worker_unregister(t, SCOPE));
+
+ worker = registration.installing;
+ return wait_for_state(t, worker, 'activated');
+ })
+ .then(function() { return with_iframe(SCOPE); })
+ .then(async function(f) {
+ frame = f;
+ // XMLHttpRequest tests.
+ await assert_resolves(frame.contentWindow.xhr(XHR_URL),
+ 'Normal XHR should succeed.');
+ await assert_resolves(frame.contentWindow.xhr(REDIRECT_TO_XHR_URL),
+ 'Redirected XHR should succeed.');
+ await assert_resolves(
+ frame.contentWindow.xhr(
+ './?url=' + encodeURIComponent(REDIRECT_TO_XHR_URL) +
+ '&redirect-mode=follow'),
+ 'Redirected XHR with Request.redirect=follow should succeed.');
+ await assert_rejects(
+ frame.contentWindow.xhr(
+ './?url=' + encodeURIComponent(REDIRECT_TO_XHR_URL) +
+ '&redirect-mode=error'),
+ 'Redirected XHR with Request.redirect=error should fail.');
+ await assert_rejects(
+ frame.contentWindow.xhr(
+ './?url=' + encodeURIComponent(REDIRECT_TO_XHR_URL) +
+ '&redirect-mode=manual'),
+ 'Redirected XHR with Request.redirect=manual should fail.');
+
+ // Image loading tests.
+ await assert_resolves(frame.contentWindow.load_image(IMAGE_URL),
+ 'Normal image resource should be loaded.');
+ await assert_resolves(
+ frame.contentWindow.load_image(REDIRECT_TO_IMAGE_URL),
+ 'Redirected image resource should be loaded.');
+ await assert_resolves(
+ frame.contentWindow.load_image(
+ './?url=' + encodeURIComponent(REDIRECT_TO_IMAGE_URL) +
+ '&redirect-mode=follow'),
+ 'Loading redirected image with Request.redirect=follow should' +
+ ' succeed.');
+ await assert_rejects(
+ frame.contentWindow.load_image(
+ './?url=' + encodeURIComponent(REDIRECT_TO_IMAGE_URL) +
+ '&redirect-mode=error'),
+ 'Loading redirected image with Request.redirect=error should ' +
+ 'fail.');
+ await assert_rejects(
+ frame.contentWindow.load_image(
+ './?url=' + encodeURIComponent(REDIRECT_TO_IMAGE_URL) +
+ '&redirect-mode=manual'),
+ 'Loading redirected image with Request.redirect=manual should' +
+ ' fail.');
+
+ // Audio loading tests.
+ await assert_resolves(frame.contentWindow.load_audio(AUDIO_URL),
+ 'Normal audio resource should be loaded.');
+ await assert_resolves(
+ frame.contentWindow.load_audio(REDIRECT_TO_AUDIO_URL),
+ 'Redirected audio resource should be loaded.');
+ await assert_resolves(
+ frame.contentWindow.load_audio(
+ './?url=' + encodeURIComponent(REDIRECT_TO_AUDIO_URL) +
+ '&redirect-mode=follow'),
+ 'Loading redirected audio with Request.redirect=follow should' +
+ ' succeed.');
+ await assert_rejects(
+ frame.contentWindow.load_audio(
+ './?url=' + encodeURIComponent(REDIRECT_TO_AUDIO_URL) +
+ '&redirect-mode=error'),
+ 'Loading redirected audio with Request.redirect=error should ' +
+ 'fail.');
+ await assert_rejects(
+ frame.contentWindow.load_audio(
+ './?url=' + encodeURIComponent(REDIRECT_TO_AUDIO_URL) +
+ '&redirect-mode=manual'),
+ 'Loading redirected audio with Request.redirect=manual should' +
+ ' fail.');
+
+ // Iframe tests.
+ await assert_resolves(iframe_test(HTML_URL),
+ 'Normal iframe loading should succeed.');
+ await assert_resolves(
+ iframe_test(REDIRECT_TO_HTML_URL),
+ 'Normal redirected iframe loading should succeed.');
+ await assert_rejects(
+ iframe_test(SCOPE + '?url=' +
+ encodeURIComponent(REDIRECT_TO_HTML_URL) +
+ '&redirect-mode=follow',
+ true /* timeout_enabled */),
+ 'Redirected iframe loading with Request.redirect=follow should'+
+ ' fail.');
+ await assert_rejects(
+ iframe_test(SCOPE + '?url=' +
+ encodeURIComponent(REDIRECT_TO_HTML_URL) +
+ '&redirect-mode=error',
+ true /* timeout_enabled */),
+ 'Redirected iframe loading with Request.redirect=error should '+
+ 'fail.');
+ await assert_resolves(
+ iframe_test(SCOPE + '?url=' +
+ encodeURIComponent(REDIRECT_TO_HTML_URL) +
+ '&redirect-mode=manual',
+ true /* timeout_enabled */),
+ 'Redirected iframe loading with Request.redirect=manual should'+
+ ' succeed.');
+ })
+ .then(function() {
+ frame.remove();
+ });
+ }, 'Verify redirect mode of Fetch API and ServiceWorker FetchEvent.');
+
+// test for reponse.redirected
+promise_test(function(t) {
+ test_scope = "redirected";
+
+ var SCOPE = 'resources/fetch-request-redirect-iframe.html';
+ var SCRIPT = 'resources/fetch-rewrite-worker.js';
+ var REDIRECT_URL = base_path() + 'resources/redirect.py?Redirect=';
+ var XHR_URL = base_path() + 'resources/simple.txt';
+ var IMAGE_URL = base_path() + 'resources/square.png';
+
+ var REDIRECT_TO_XHR_URL = REDIRECT_URL + encodeURIComponent(XHR_URL);
+
+ var host_info = get_host_info();
+
+ var CROSS_ORIGIN_URL = host_info['HTTPS_REMOTE_ORIGIN'] + IMAGE_URL;
+
+ var REDIRECT_TO_CROSS_ORIGIN = REDIRECT_URL +
+ encodeURIComponent(CROSS_ORIGIN_URL + '?ACAOrigin=*');
+
+ var worker;
+ var frame;
+ return service_worker_unregister_and_register(t, SCRIPT, SCOPE)
+ .then(function(registration) {
+ t.add_cleanup(() => service_worker_unregister(t, SCOPE));
+
+ worker = registration.installing;
+ return wait_for_state(t, worker, 'activated');
+ })
+ .then(function() { return with_iframe(SCOPE); })
+ .then(async function(f) {
+ frame = f;
+ // XMLHttpRequest tests.
+ await assert_resolves(
+ frame.contentWindow.xhr(
+ './?url=' + encodeURIComponent(XHR_URL) +
+ '&expected_redirected=false' +
+ '&expected_resolves=true'),
+ 'Normal XHR should be resolved and response should not be ' +
+ 'redirected.');
+ await assert_resolves(
+ frame.contentWindow.xhr(
+ './?url=' + encodeURIComponent(REDIRECT_TO_XHR_URL) +
+ '&expected_redirected=true' +
+ '&expected_resolves=true'),
+ 'Redirected XHR should be resolved and response should be ' +
+ 'redirected.');
+
+ // tests for request's mode = cors
+ await assert_resolves(
+ frame.contentWindow.xhr(
+ './?url=' + encodeURIComponent(XHR_URL) +
+ '&mode=cors' +
+ '&expected_redirected=false' +
+ '&expected_resolves=true'),
+ 'Normal XHR should be resolved and response should not be ' +
+ 'redirected even with CORS mode.');
+ await assert_resolves(
+ frame.contentWindow.xhr(
+ './?url=' + encodeURIComponent(REDIRECT_TO_XHR_URL) +
+ '&mode=cors' +
+ '&redirect-mode=follow' +
+ '&expected_redirected=true' +
+ '&expected_resolves=true'),
+ 'Redirected XHR should be resolved and response.redirected ' +
+ 'should be redirected with CORS mode.');
+
+ // tests for request's mode = no-cors
+ // The response.redirect should be false since we will not add
+ // redirected url list when redirect-mode is not follow.
+ await assert_rejects(
+ frame.contentWindow.xhr(
+ './?url=' + encodeURIComponent(REDIRECT_TO_XHR_URL) +
+ '&mode=no-cors' +
+ '&redirect-mode=manual' +
+ '&expected_redirected=false' +
+ '&expected_resolves=false'),
+ 'Redirected XHR should be reject and response should be ' +
+ 'redirected with NO-CORS mode and redirect-mode=manual.');
+
+ // tests for redirecting to a cors
+ await assert_resolves(
+ frame.contentWindow.load_image(
+ './?url=' + encodeURIComponent(REDIRECT_TO_CROSS_ORIGIN) +
+ '&mode=no-cors' +
+ '&redirect-mode=follow' +
+ '&expected_redirected=false' +
+ '&expected_resolves=true'),
+ 'Redirected CORS image should be reject and response should ' +
+ 'not be redirected with NO-CORS mode.');
+ })
+ .then(function() {
+ frame.remove();
+ });
+ }, 'Verify redirected of Response(Fetch API) and ServiceWorker FetchEvent.');
+
+// test for reponse.redirected after cached
+promise_test(function(t) {
+ test_scope = "cache";
+
+ var SCOPE = 'resources/fetch-request-redirect-iframe.html';
+ var SCRIPT = 'resources/fetch-rewrite-worker.js';
+ var REDIRECT_URL = base_path() + 'resources/redirect.py?Redirect=';
+ var XHR_URL = base_path() + 'resources/simple.txt';
+ var IMAGE_URL = base_path() + 'resources/square.png';
+
+ var REDIRECT_TO_XHR_URL = REDIRECT_URL + encodeURIComponent(XHR_URL);
+
+ var host_info = get_host_info();
+
+ var CROSS_ORIGIN_URL = host_info['HTTPS_REMOTE_ORIGIN'] + IMAGE_URL;
+
+ var REDIRECT_TO_CROSS_ORIGIN = REDIRECT_URL +
+ encodeURIComponent(CROSS_ORIGIN_URL + '?ACAOrigin=*');
+
+ var worker;
+ var frame;
+ return service_worker_unregister_and_register(t, SCRIPT, SCOPE)
+ .then(function(registration) {
+ t.add_cleanup(() => service_worker_unregister(t, SCOPE));
+
+ worker = registration.installing;
+ return wait_for_state(t, worker, 'activated');
+ })
+ .then(function() { return with_iframe(SCOPE); })
+ .then(async function(f) {
+ frame = f;
+ // XMLHttpRequest tests.
+ await assert_resolves(
+ frame.contentWindow.xhr(
+ './?url=' + encodeURIComponent(XHR_URL) +
+ '&expected_redirected=false' +
+ '&expected_resolves=true' +
+ '&cache'),
+ 'Normal XHR should be resolved and response should not be ' +
+ 'redirected.');
+ await assert_resolves(
+ frame.contentWindow.xhr(
+ './?url=' + encodeURIComponent(REDIRECT_TO_XHR_URL) +
+ '&expected_redirected=true' +
+ '&expected_resolves=true' +
+ '&cache'),
+ 'Redirected XHR should be resolved and response should be ' +
+ 'redirected.');
+
+ // tests for request's mode = cors
+ await assert_resolves(
+ frame.contentWindow.xhr(
+ './?url=' + encodeURIComponent(XHR_URL) +
+ '&mode=cors' +
+ '&expected_redirected=false' +
+ '&expected_resolves=true' +
+ '&cache'),
+ 'Normal XHR should be resolved and response should not be ' +
+ 'redirected even with CORS mode.');
+ await assert_resolves(
+ frame.contentWindow.xhr(
+ './?url=' + encodeURIComponent(REDIRECT_TO_XHR_URL) +
+ '&mode=cors' +
+ '&redirect-mode=follow' +
+ '&expected_redirected=true' +
+ '&expected_resolves=true' +
+ '&cache'),
+ 'Redirected XHR should be resolved and response.redirected ' +
+ 'should be redirected with CORS mode.');
+
+ // tests for request's mode = no-cors
+ // The response.redirect should be false since we will not add
+ // redirected url list when redirect-mode is not follow.
+ await assert_rejects(
+ frame.contentWindow.xhr(
+ './?url=' + encodeURIComponent(REDIRECT_TO_XHR_URL) +
+ '&mode=no-cors' +
+ '&redirect-mode=manual' +
+ '&expected_redirected=false' +
+ '&expected_resolves=false' +
+ '&cache'),
+ 'Redirected XHR should be reject and response should be ' +
+ 'redirected with NO-CORS mode and redirect-mode=manual.');
+
+ // tests for redirecting to a cors
+ await assert_resolves(
+ frame.contentWindow.load_image(
+ './?url=' + encodeURIComponent(REDIRECT_TO_CROSS_ORIGIN) +
+ '&mode=no-cors' +
+ '&redirect-mode=follow' +
+ '&expected_redirected=false' +
+ '&expected_resolves=true' +
+ '&cache'),
+ 'Redirected CORS image should be reject and response should ' +
+ 'not be redirected with NO-CORS mode.');
+ })
+ .then(function() {
+ frame.remove();
+ });
+ }, 'Verify redirected of Response(Fetch API), Cache API and ServiceWorker ' +
+ 'FetchEvent.');
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/fetch-request-resources.https.html b/test/wpt/tests/service-workers/service-worker/fetch-request-resources.https.html
new file mode 100644
index 0000000..b4680c3
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/fetch-request-resources.https.html
@@ -0,0 +1,302 @@
+<!DOCTYPE html>
+<title>Service Worker: FetchEvent for resources</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+let url_count = 0;
+const expected_results = {};
+
+function add_promise_to_test(url)
+{
+ const expected = expected_results[url];
+ return new Promise((resolve) => {
+ expected.resolve = resolve;
+ });
+}
+
+function image_test(frame, url, cross_origin, expected_mode,
+ expected_credentials) {
+ const actual_url = url + (++url_count);
+ expected_results[actual_url] = {
+ cross_origin: cross_origin,
+ mode: expected_mode,
+ credentials: expected_credentials,
+ redirect: 'follow',
+ integrity: '',
+ destination: 'image',
+ message: `Image load (url:${actual_url} cross_origin:${cross_origin})`
+ };
+ frame.contentWindow.load_image(actual_url, cross_origin);
+ return add_promise_to_test(actual_url);
+}
+
+function script_test(frame, url, cross_origin, expected_mode,
+ expected_credentials) {
+ const actual_url = url + (++url_count);
+ expected_results[actual_url] = {
+ cross_origin: cross_origin,
+ mode: expected_mode,
+ credentials: expected_credentials,
+ redirect: 'follow',
+ integrity: '',
+ destination: 'script',
+ message: `Script load (url:${actual_url} cross_origin:${cross_origin})`
+ };
+ frame.contentWindow.load_script(actual_url, cross_origin);
+ return add_promise_to_test(actual_url);
+}
+
+function css_test(frame, url, cross_origin, expected_mode,
+ expected_credentials) {
+ const actual_url = url + (++url_count);
+ expected_results[actual_url] = {
+ cross_origin: cross_origin,
+ mode: expected_mode,
+ credentials: expected_credentials,
+ redirect: 'follow',
+ integrity: '',
+ destination: 'style',
+ message: `CSS load (url:${actual_url} cross_origin:${cross_origin})`
+ };
+ frame.contentWindow.load_css(actual_url, cross_origin);
+ return add_promise_to_test(actual_url);
+}
+
+function font_face_test(frame, url, expected_mode, expected_credentials) {
+ const actual_url = url + (++url_count);
+ expected_results[actual_url] = {
+ url: actual_url,
+ mode: expected_mode,
+ credentials: expected_credentials,
+ redirect: 'follow',
+ integrity: '',
+ destination: 'font',
+ message: `FontFace load (url: ${actual_url})`
+ };
+ frame.contentWindow.load_font(actual_url);
+ return add_promise_to_test(actual_url);
+}
+
+function script_integrity_test(frame, url, integrity, expected_integrity) {
+ const actual_url = url + (++url_count);
+ expected_results[actual_url] = {
+ url: actual_url,
+ mode: 'no-cors',
+ credentials: 'include',
+ redirect: 'follow',
+ integrity: expected_integrity,
+ destination: 'script',
+ message: `Script load (url:${actual_url})`
+ };
+ frame.contentWindow.load_script_with_integrity(actual_url, integrity);
+ return add_promise_to_test(actual_url);
+}
+
+function css_integrity_test(frame, url, integrity, expected_integrity) {
+ const actual_url = url + (++url_count);
+ expected_results[actual_url] = {
+ url: actual_url,
+ mode: 'no-cors',
+ credentials: 'include',
+ redirect: 'follow',
+ integrity: expected_integrity,
+ destination: 'style',
+ message: `CSS load (url:${actual_url})`
+ };
+ frame.contentWindow.load_css_with_integrity(actual_url, integrity);
+ return add_promise_to_test(actual_url);
+}
+
+function fetch_test(frame, url, mode, credentials,
+ expected_mode, expected_credentials) {
+ const actual_url = url + (++url_count);
+ expected_results[actual_url] = {
+ mode: expected_mode,
+ credentials: expected_credentials,
+ redirect: 'follow',
+ integrity: '',
+ destination: '',
+ message: `fetch (url:${actual_url} mode:${mode} ` +
+ `credentials:${credentials})`
+ };
+ frame.contentWindow.fetch(
+ new Request(actual_url, {mode: mode, credentials: credentials}));
+ return add_promise_to_test(actual_url);
+}
+
+function audio_test(frame, url, cross_origin,
+ expected_mode, expected_credentials) {
+ const actual_url = url + (++url_count);
+ expected_results[actual_url] = {
+ mode: expected_mode,
+ credentials: expected_credentials,
+ redirect: 'follow',
+ integrity: '',
+ destination: 'audio',
+ message: `Audio load (url:${actual_url} cross_origin:${cross_origin})`
+ };
+ frame.contentWindow.load_audio(actual_url, cross_origin);
+ return add_promise_to_test(actual_url);
+}
+
+
+function video_test(frame, url, cross_origin,
+ expected_mode, expected_credentials) {
+ const actual_url = url + (++url_count);
+ expected_results[actual_url] = {
+ mode: expected_mode,
+ credentials: expected_credentials,
+ redirect: 'follow',
+ integrity: '',
+ destination: 'video',
+ message: `Video load (url:${actual_url} cross_origin:${cross_origin})`
+ };
+ frame.contentWindow.load_video(actual_url, cross_origin);
+ return add_promise_to_test(actual_url);
+}
+
+promise_test(async t => {
+ const SCOPE = 'resources/fetch-request-resources-iframe.https.html';
+ const SCRIPT = 'resources/fetch-request-resources-worker.js';
+ const host_info = get_host_info();
+ const LOCAL_URL =
+ host_info['HTTPS_ORIGIN'] + base_path() + 'resources/sample?test';
+ const REMOTE_URL =
+ host_info['HTTPS_REMOTE_ORIGIN'] + base_path() + 'resources/sample?test';
+
+ const registration =
+ await service_worker_unregister_and_register(t, SCRIPT, SCOPE);
+ t.add_cleanup(() => registration.unregister());
+ const worker = registration.installing;
+ await wait_for_state(t, worker, 'activated');
+
+ await new Promise((resolve, reject) => {
+ const channel = new MessageChannel();
+ channel.port1.onmessage = t.step_func(msg => {
+ if (msg.data.ready) {
+ resolve();
+ return;
+ }
+ const result = msg.data;
+ const expected = expected_results[result.url];
+ if (!expected) {
+ return;
+ }
+ test(() => {
+ assert_equals(
+ result.mode, expected.mode,
+ `mode of must be ${expected.mode}.`);
+ assert_equals(
+ result.credentials, expected.credentials,
+ `credentials of ${expected.message} must be ` +
+ `${expected.credentials}.`);
+ assert_equals(
+ result.redirect, expected.redirect,
+ `redirect mode of ${expected.message} must be ` +
+ `${expected.redirect}.`);
+ assert_equals(
+ result.integrity, expected.integrity,
+ `integrity of ${expected.message} must be ` +
+ `${expected.integrity}.`);
+ assert_equals(
+ result.destination, expected.destination,
+ `destination of ${expected.message} must be ` +
+ `${expected.destination}.`);
+ }, expected.message);
+ expected.resolve();
+ delete expected_results[result.url];
+ });
+ worker.postMessage({port: channel.port2}, [channel.port2]);
+ });
+
+ const f = await with_iframe(SCOPE);
+ t.add_cleanup(() => f.remove());
+
+ await image_test(f, LOCAL_URL, '', 'no-cors', 'include');
+ await image_test(f, REMOTE_URL, '', 'no-cors', 'include');
+ await css_test(f, LOCAL_URL, '', 'no-cors', 'include');
+ await css_test(f, REMOTE_URL, '', 'no-cors', 'include');
+
+ await image_test(f, LOCAL_URL, 'anonymous', 'cors', 'same-origin');
+ await image_test(f, LOCAL_URL, 'use-credentials', 'cors', 'include');
+ await image_test(f, REMOTE_URL, '', 'no-cors', 'include');
+ await image_test(f, REMOTE_URL, 'anonymous', 'cors', 'same-origin');
+ await image_test(f, REMOTE_URL, 'use-credentials', 'cors', 'include');
+
+ await script_test(f, LOCAL_URL, '', 'no-cors', 'include');
+ await script_test(f, LOCAL_URL, 'anonymous', 'cors', 'same-origin');
+ await script_test(f, LOCAL_URL, 'use-credentials', 'cors', 'include');
+ await script_test(f, REMOTE_URL, '', 'no-cors', 'include');
+ await script_test(f, REMOTE_URL, 'anonymous', 'cors', 'same-origin');
+ await script_test(f, REMOTE_URL, 'use-credentials', 'cors', 'include');
+
+ await css_test(f, LOCAL_URL, '', 'no-cors', 'include');
+ await css_test(f, LOCAL_URL, 'anonymous', 'cors', 'same-origin');
+ await css_test(f, LOCAL_URL, 'use-credentials', 'cors', 'include');
+ await css_test(f, REMOTE_URL, '', 'no-cors', 'include');
+ await css_test(f, REMOTE_URL, 'anonymous', 'cors', 'same-origin');
+ await css_test(f, REMOTE_URL, 'use-credentials', 'cors', 'include');
+
+ await font_face_test(f, LOCAL_URL, 'cors', 'same-origin');
+ await font_face_test(f, REMOTE_URL, 'cors', 'same-origin');
+
+ await script_integrity_test(f, LOCAL_URL, ' ', ' ');
+ await script_integrity_test(
+ f, LOCAL_URL,
+ 'This is not a valid integrity because it has no dashes',
+ 'This is not a valid integrity because it has no dashes');
+ await script_integrity_test(f, LOCAL_URL, 'sha256-', 'sha256-');
+ await script_integrity_test(f, LOCAL_URL, 'sha256-foo?123', 'sha256-foo?123');
+ await script_integrity_test(f, LOCAL_URL, 'sha256-foo sha384-abc ',
+ 'sha256-foo sha384-abc ');
+ await script_integrity_test(f, LOCAL_URL, 'sha256-foo sha256-abc',
+ 'sha256-foo sha256-abc');
+
+ await css_integrity_test(f, LOCAL_URL, ' ', ' ');
+ await css_integrity_test(
+ f, LOCAL_URL,
+ 'This is not a valid integrity because it has no dashes',
+ 'This is not a valid integrity because it has no dashes');
+ await css_integrity_test(f, LOCAL_URL, 'sha256-', 'sha256-');
+ await css_integrity_test(f, LOCAL_URL, 'sha256-foo?123', 'sha256-foo?123');
+ await css_integrity_test(f, LOCAL_URL, 'sha256-foo sha384-abc ',
+ 'sha256-foo sha384-abc ');
+ await css_integrity_test(f, LOCAL_URL, 'sha256-foo sha256-abc',
+ 'sha256-foo sha256-abc');
+
+ await fetch_test(f, LOCAL_URL, 'same-origin', 'omit', 'same-origin', 'omit');
+ await fetch_test(f, LOCAL_URL, 'same-origin', 'same-origin',
+ 'same-origin', 'same-origin');
+ await fetch_test(f, LOCAL_URL, 'same-origin', 'include',
+ 'same-origin', 'include');
+ await fetch_test(f, LOCAL_URL, 'no-cors', 'omit', 'no-cors', 'omit');
+ await fetch_test(f, LOCAL_URL, 'no-cors', 'same-origin',
+ 'no-cors', 'same-origin');
+ await fetch_test(f, LOCAL_URL, 'no-cors', 'include', 'no-cors', 'include');
+ await fetch_test(f, LOCAL_URL, 'cors', 'omit', 'cors', 'omit');
+ await fetch_test(f, LOCAL_URL, 'cors', 'same-origin', 'cors', 'same-origin');
+ await fetch_test(f, LOCAL_URL, 'cors', 'include', 'cors', 'include');
+ await fetch_test(f, REMOTE_URL, 'no-cors', 'omit', 'no-cors', 'omit');
+ await fetch_test(f, REMOTE_URL, 'no-cors', 'same-origin', 'no-cors', 'same-origin');
+ await fetch_test(f, REMOTE_URL, 'no-cors', 'include', 'no-cors', 'include');
+ await fetch_test(f, REMOTE_URL, 'cors', 'omit', 'cors', 'omit');
+ await fetch_test(f, REMOTE_URL, 'cors', 'same-origin', 'cors', 'same-origin');
+ await fetch_test(f, REMOTE_URL, 'cors', 'include', 'cors', 'include');
+
+ await audio_test(f, LOCAL_URL, '', 'no-cors', 'include');
+ await audio_test(f, LOCAL_URL, 'anonymous', 'cors', 'same-origin');
+ await audio_test(f, LOCAL_URL, 'use-credentials', 'cors', 'include');
+ await audio_test(f, REMOTE_URL, '', 'no-cors', 'include');
+ await audio_test(f, REMOTE_URL, 'anonymous', 'cors', 'same-origin');
+ await audio_test(f, REMOTE_URL, 'use-credentials', 'cors', 'include');
+
+ await video_test(f, LOCAL_URL, '', 'no-cors', 'include');
+ await video_test(f, LOCAL_URL, 'anonymous', 'cors', 'same-origin');
+ await video_test(f, LOCAL_URL, 'use-credentials', 'cors', 'include');
+ await video_test(f, REMOTE_URL, '', 'no-cors', 'include');
+ await video_test(f, REMOTE_URL, 'anonymous', 'cors', 'same-origin');
+ await video_test(f, REMOTE_URL, 'use-credentials', 'cors', 'include');
+}, 'Verify FetchEvent for resources.');
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/fetch-request-xhr-sync-error.https.window.js b/test/wpt/tests/service-workers/service-worker/fetch-request-xhr-sync-error.https.window.js
new file mode 100644
index 0000000..e6c0213
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/fetch-request-xhr-sync-error.https.window.js
@@ -0,0 +1,19 @@
+// META: script=resources/test-helpers.sub.js
+
+"use strict";
+
+promise_test(async t => {
+ const url = "resources/fetch-request-xhr-sync-error-worker.js";
+ const scope = "resources/fetch-request-xhr-sync-iframe.html";
+
+ const registration = await service_worker_unregister_and_register(t, url, scope);
+ t.add_cleanup(() => registration.unregister());
+
+ await wait_for_state(t, registration.installing, 'activated');
+ const frame = await with_iframe(scope);
+ t.add_cleanup(() => frame.remove());
+
+ assert_throws_dom("NetworkError", frame.contentWindow.DOMException, () => frame.contentWindow.performSyncXHR("non-existent-stream-1.txt"));
+ assert_throws_dom("NetworkError", frame.contentWindow.DOMException, () => frame.contentWindow.performSyncXHR("non-existent-stream-2.txt"));
+ assert_throws_dom("NetworkError", frame.contentWindow.DOMException, () => frame.contentWindow.performSyncXHR("non-existent-stream-3.txt"));
+}, "Verify synchronous XMLHttpRequest always throws a NetworkError for ReadableStream errors");
diff --git a/test/wpt/tests/service-workers/service-worker/fetch-request-xhr-sync-on-worker.https.html b/test/wpt/tests/service-workers/service-worker/fetch-request-xhr-sync-on-worker.https.html
new file mode 100644
index 0000000..9f18096
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/fetch-request-xhr-sync-on-worker.https.html
@@ -0,0 +1,41 @@
+<!DOCTYPE html>
+<title>Service Worker: Synchronous XHR on Worker is intercepted</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+'use strict';
+
+promise_test((t) => {
+ const url = 'resources/fetch-request-xhr-sync-on-worker-worker.js';
+ const scope = 'resources/fetch-request-xhr-sync-on-worker-scope/';
+ const non_existent_file = 'non-existent-file.txt';
+
+ // In Chromium, the service worker scope matching for workers is based on
+ // the URL of the parent HTML. So this test creates an iframe which is
+ // controlled by the service worker first, and creates a worker from the
+ // iframe.
+ return service_worker_unregister_and_register(t, url, scope)
+ .then((registration) => {
+ t.add_cleanup(() => registration.unregister());
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(() => { return with_iframe(scope + 'iframe_page'); })
+ .then((frame) => {
+ t.add_cleanup(() => frame.remove());
+ return frame.contentWindow.performSyncXHROnWorker(non_existent_file);
+ })
+ .then((result) => {
+ assert_equals(
+ result.status,
+ 200,
+ 'HTTP response status code for intercepted request'
+ );
+ assert_equals(
+ result.responseText,
+ 'Response from service worker',
+ 'HTTP response text for intercepted request'
+ );
+ });
+ }, 'Verify SyncXHR on Worker is intercepted');
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/fetch-request-xhr-sync.https.html b/test/wpt/tests/service-workers/service-worker/fetch-request-xhr-sync.https.html
new file mode 100644
index 0000000..ec27fb8
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/fetch-request-xhr-sync.https.html
@@ -0,0 +1,53 @@
+<!DOCTYPE html>
+<title>Service Worker: Synchronous XHR is intercepted</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+'use strict';
+
+promise_test(function(t) {
+ var url = 'resources/fetch-request-xhr-sync-worker.js';
+ var scope = 'resources/fetch-request-xhr-sync-iframe.html';
+ var non_existent_file = 'non-existent-file.txt';
+
+ return service_worker_unregister_and_register(t, url, scope)
+ .then(function(registration) {
+ t.add_cleanup(function() {
+ return registration.unregister();
+ });
+
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() { return with_iframe(scope); })
+ .then(function(frame) {
+ t.add_cleanup(function() {
+ frame.remove();
+ });
+
+ return new Promise(function(resolve, reject) {
+ t.step_timeout(function() {
+ var xhr;
+ try {
+ xhr = frame.contentWindow.performSyncXHR(non_existent_file);
+ resolve(xhr);
+ } catch (err) {
+ reject(err);
+ }
+ }, 0);
+ })
+ })
+ .then(function(xhr) {
+ assert_equals(
+ xhr.status,
+ 200,
+ 'HTTP response status code for intercepted request'
+ );
+ assert_equals(
+ xhr.responseText,
+ 'Response from service worker',
+ 'HTTP response text for intercepted request'
+ );
+ });
+ }, 'Verify SyncXHR is intercepted');
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/fetch-request-xhr.https.html b/test/wpt/tests/service-workers/service-worker/fetch-request-xhr.https.html
new file mode 100644
index 0000000..37a4573
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/fetch-request-xhr.https.html
@@ -0,0 +1,75 @@
+<!DOCTYPE html>
+<title>Service Worker: the body of FetchEvent using XMLHttpRequest</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js?pipe-sub"></script>
+<script>
+let frame;
+
+// Set up the service worker and the frame.
+promise_test(t => {
+ const kScope = 'resources/fetch-request-xhr-iframe.https.html';
+ const kScript = 'resources/fetch-request-xhr-worker.js';
+ return service_worker_unregister_and_register(t, kScript, kScope)
+ .then(registration => {
+ promise_test(() => {
+ return registration.unregister();
+ }, 'restore global state');
+
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(() => {
+ return with_iframe(kScope);
+ })
+ .then(f => {
+ frame = f;
+ add_completion_callback(() => { f.remove(); });
+ });
+ }, 'initialize global state');
+
+// Run the tests.
+promise_test(t => {
+ return frame.contentWindow.get_header_test();
+ }, 'event.request has the expected headers for same-origin GET.');
+
+promise_test(t => {
+ return frame.contentWindow.post_header_test();
+ }, 'event.request has the expected headers for same-origin POST.');
+
+promise_test(t => {
+ return frame.contentWindow.cross_origin_get_header_test();
+ }, 'event.request has the expected headers for cross-origin GET.');
+
+promise_test(t => {
+ return frame.contentWindow.cross_origin_post_header_test();
+ }, 'event.request has the expected headers for cross-origin POST.');
+
+promise_test(t => {
+ return frame.contentWindow.string_test();
+ }, 'FetchEvent#request.body contains XHR request data (string)');
+
+promise_test(t => {
+ return frame.contentWindow.blob_test();
+ }, 'FetchEvent#request.body contains XHR request data (blob)');
+
+promise_test(t => {
+ return frame.contentWindow.custom_method_test();
+ }, 'FetchEvent#request.method is set to XHR method');
+
+promise_test(t => {
+ return frame.contentWindow.options_method_test();
+ }, 'XHR using OPTIONS method');
+
+promise_test(t => {
+ return frame.contentWindow.form_data_test();
+ }, 'XHR with form data');
+
+promise_test(t => {
+ return frame.contentWindow.mode_credentials_test();
+ }, 'XHR with mode/credentials set');
+
+promise_test(t => {
+ return frame.contentWindow.data_url_test();
+ }, 'XHR to data URL');
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/fetch-response-taint.https.html b/test/wpt/tests/service-workers/service-worker/fetch-response-taint.https.html
new file mode 100644
index 0000000..8e190f4
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/fetch-response-taint.https.html
@@ -0,0 +1,223 @@
+<!DOCTYPE html>
+<title>Service Worker: Tainting of responses fetched via SW.</title>
+<!-- This test makes a large number of requests sequentially. -->
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+var host_info = get_host_info();
+var BASE_ORIGIN = host_info.HTTPS_ORIGIN;
+var OTHER_ORIGIN = host_info.HTTPS_REMOTE_ORIGIN;
+var BASE_URL = BASE_ORIGIN + base_path() +
+ 'resources/fetch-access-control.py?';
+var OTHER_BASE_URL = OTHER_ORIGIN + base_path() +
+ 'resources/fetch-access-control.py?';
+
+function frame_fetch(frame, url, mode, credentials) {
+ var foreignPromise = frame.contentWindow.fetch(
+ new Request(url, {mode: mode, credentials: credentials}))
+
+ // Event loops should be shared between contexts of similar origin, not all
+ // browsers adhere to this expectation at the time of this writing. Incorrect
+ // behavior in this regard can interfere with test execution when the
+ // provided iframe is removed from the document.
+ //
+ // WPT maintains a test dedicated the expected treatment of event loops, so
+ // the following workaround is acceptable in this context.
+ return Promise.resolve(foreignPromise);
+}
+
+var login_and_register;
+promise_test(function(t) {
+ var SCOPE = 'resources/fetch-response-taint-iframe.html';
+ var SCRIPT = 'resources/fetch-rewrite-worker.js';
+ var registration;
+
+ login_and_register = login_https(t, host_info.HTTPS_ORIGIN, host_info.HTTPS_REMOTE_ORIGIN)
+ .then(function() {
+ return service_worker_unregister_and_register(t, SCRIPT, SCOPE);
+ })
+ .then(function(r) {
+ registration = r;
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() { return with_iframe(SCOPE); })
+ .then(function(f) {
+ // This test should not be considered complete until after the
+ // service worker has been unregistered. Currently, `testharness.js`
+ // does not support asynchronous global "tear down" logic, so this
+ // must be expressed using a dedicated `promise_test`. Because the
+ // other sub-tests in this file are declared synchronously, this
+ // test will be the final test executed.
+ promise_test(function(t) {
+ f.remove();
+ return registration.unregister();
+ }, 'restore global state');
+
+ return f;
+ });
+ return login_and_register;
+ }, 'initialize global state');
+
+function ng_test(url, mode, credentials) {
+ promise_test(function(t) {
+ return login_and_register
+ .then(function(frame) {
+ var fetchRequest = frame_fetch(frame, url, mode, credentials);
+ return promise_rejects_js(t, frame.contentWindow.TypeError, fetchRequest);
+ });
+ }, 'url:\"' + url + '\" mode:\"' + mode +
+ '\" credentials:\"' + credentials + '\" should fail.');
+}
+
+function ok_test(url, mode, credentials, expected_type, expected_username) {
+ promise_test(function() {
+ return login_and_register.then(function(frame) {
+ return frame_fetch(frame, url, mode, credentials)
+ })
+ .then(function(res) {
+ assert_equals(res.type, expected_type, 'response type');
+ return res.text();
+ })
+ .then(function(text) {
+ if (expected_type == 'opaque') {
+ assert_equals(text, '');
+ } else {
+ return new Promise(function(resolve) {
+ var report = resolve;
+ // text must contain report() call.
+ eval(text);
+ })
+ .then(function(result) {
+ assert_equals(result.username, expected_username);
+ });
+ }
+ });
+ }, 'fetching url:\"' + url + '\" mode:\"' + mode +
+ '\" credentials:\"' + credentials + '\" should ' +
+ 'succeed.');
+}
+
+function build_rewrite_url(origin, url, mode, credentials) {
+ return origin + '/?url=' + encodeURIComponent(url) + '&mode=' + mode +
+ '&credentials=' + credentials + '&';
+}
+
+function for_each_origin_mode_credentials(callback) {
+ [BASE_ORIGIN, OTHER_ORIGIN].forEach(function(origin) {
+ ['same-origin', 'no-cors', 'cors'].forEach(function(mode) {
+ ['omit', 'same-origin', 'include'].forEach(function(credentials) {
+ callback(origin, mode, credentials);
+ });
+ });
+ });
+}
+
+ok_test(BASE_URL, 'same-origin', 'omit', 'basic', 'undefined');
+ok_test(BASE_URL, 'same-origin', 'same-origin', 'basic', 'username2s');
+ok_test(BASE_URL, 'same-origin', 'include', 'basic', 'username2s');
+ok_test(BASE_URL, 'no-cors', 'omit', 'basic', 'undefined');
+ok_test(BASE_URL, 'no-cors', 'same-origin', 'basic', 'username2s');
+ok_test(BASE_URL, 'no-cors', 'include', 'basic', 'username2s');
+ok_test(BASE_URL, 'cors', 'omit', 'basic', 'undefined');
+ok_test(BASE_URL, 'cors', 'same-origin', 'basic', 'username2s');
+ok_test(BASE_URL, 'cors', 'include', 'basic', 'username2s');
+ng_test(OTHER_BASE_URL, 'same-origin', 'omit');
+ng_test(OTHER_BASE_URL, 'same-origin', 'same-origin');
+ng_test(OTHER_BASE_URL, 'same-origin', 'include');
+ok_test(OTHER_BASE_URL, 'no-cors', 'omit', 'opaque');
+ok_test(OTHER_BASE_URL, 'no-cors', 'same-origin', 'opaque');
+ok_test(OTHER_BASE_URL, 'no-cors', 'include', 'opaque');
+ng_test(OTHER_BASE_URL, 'cors', 'omit');
+ng_test(OTHER_BASE_URL, 'cors', 'same-origin');
+ng_test(OTHER_BASE_URL, 'cors', 'include');
+ok_test(OTHER_BASE_URL + 'ACAOrigin=*', 'cors', 'omit', 'cors', 'undefined');
+ok_test(OTHER_BASE_URL + 'ACAOrigin=*', 'cors', 'same-origin', 'cors',
+ 'undefined');
+ng_test(OTHER_BASE_URL + 'ACAOrigin=*', 'cors', 'include');
+ok_test(OTHER_BASE_URL + 'ACAOrigin=' + BASE_ORIGIN + '&ACACredentials=true',
+ 'cors', 'include', 'cors', 'username1s')
+
+for_each_origin_mode_credentials(function(origin, mode, credentials) {
+ var url = build_rewrite_url(
+ origin, BASE_URL, 'same-origin', 'omit');
+ // Fetch to the other origin with same-origin mode should fail.
+ if (origin == OTHER_ORIGIN && mode == 'same-origin') {
+ ng_test(url, mode, credentials);
+ } else {
+ // The response type from the SW should be basic
+ ok_test(url, mode, credentials, 'basic', 'undefined');
+ }
+});
+
+for_each_origin_mode_credentials(function(origin, mode, credentials) {
+ var url = build_rewrite_url(
+ origin, BASE_URL, 'same-origin', 'same-origin');
+
+ // Fetch to the other origin with same-origin mode should fail.
+ if (origin == OTHER_ORIGIN && mode == 'same-origin') {
+ ng_test(url, mode, credentials);
+ } else {
+ // The response type from the SW should be basic.
+ ok_test(url, mode, credentials, 'basic', 'username2s');
+ }
+});
+
+for_each_origin_mode_credentials(function(origin, mode, credentials) {
+ var url = build_rewrite_url(
+ origin, OTHER_BASE_URL, 'same-origin', 'omit');
+ // The response from the SW should be an error.
+ ng_test(url, mode, credentials);
+});
+
+for_each_origin_mode_credentials(function(origin, mode, credentials) {
+ var url = build_rewrite_url(
+ origin, OTHER_BASE_URL, 'no-cors', 'omit');
+
+ // SW can respond only to no-cors requests.
+ if (mode != 'no-cors') {
+ ng_test(url, mode, credentials);
+ } else {
+ // The response type from the SW should be opaque.
+ ok_test(url, mode, credentials, 'opaque');
+ }
+});
+
+for_each_origin_mode_credentials(function(origin, mode, credentials) {
+ var url = build_rewrite_url(
+ origin, OTHER_BASE_URL + 'ACAOrigin=*', 'cors', 'omit');
+
+ // Fetch to the other origin with same-origin mode should fail.
+ if (origin == OTHER_ORIGIN && mode == 'same-origin') {
+ ng_test(url, mode, credentials);
+ } else if (origin == BASE_ORIGIN && mode == 'same-origin') {
+ // Cors type response to a same-origin mode request should fail
+ ng_test(url, mode, credentials);
+ } else {
+ // The response from the SW should be cors.
+ ok_test(url, mode, credentials, 'cors', 'undefined');
+ }
+});
+
+for_each_origin_mode_credentials(function(origin, mode, credentials) {
+ var url = build_rewrite_url(
+ origin,
+ OTHER_BASE_URL + 'ACAOrigin=' + BASE_ORIGIN +
+ '&ACACredentials=true',
+ 'cors', 'include');
+ // Fetch to the other origin with same-origin mode should fail.
+ if (origin == OTHER_ORIGIN && mode == 'same-origin') {
+ ng_test(url, mode, credentials);
+ } else if (origin == BASE_ORIGIN && mode == 'same-origin') {
+ // Cors type response to a same-origin mode request should fail
+ ng_test(url, mode, credentials);
+ } else {
+ // The response from the SW should be cors.
+ ok_test(url, mode, credentials, 'cors', 'username1s');
+ }
+});
+</script>
+</body>
diff --git a/test/wpt/tests/service-workers/service-worker/fetch-response-xhr.https.html b/test/wpt/tests/service-workers/service-worker/fetch-response-xhr.https.html
new file mode 100644
index 0000000..891eb02
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/fetch-response-xhr.https.html
@@ -0,0 +1,50 @@
+<!DOCTYPE html>
+<title>Service Worker: the response of FetchEvent using XMLHttpRequest</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+promise_test(function(t) {
+ var SCOPE = 'resources/fetch-response-xhr-iframe.https.html';
+ var SCRIPT = 'resources/fetch-response-xhr-worker.js';
+ var host_info = get_host_info();
+
+ window.addEventListener('message', t.step_func(on_message), false);
+ function on_message(e) {
+ assert_equals(e.data.results, 'foo, bar');
+ e.source.postMessage('ACK', host_info['HTTPS_ORIGIN']);
+ }
+
+ return service_worker_unregister_and_register(t, SCRIPT, SCOPE)
+ .then(function(registration) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, SCOPE);
+ });
+
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() { return with_iframe(SCOPE); })
+ .then(function(frame) {
+ var channel;
+
+ t.add_cleanup(function() {
+ frame.remove();
+ });
+
+ channel = new MessageChannel();
+ var onPortMsg = new Promise(function(resolve) {
+ channel.port1.onmessage = resolve;
+ });
+
+ frame.contentWindow.postMessage('START',
+ host_info['HTTPS_ORIGIN'],
+ [channel.port2]);
+
+ return onPortMsg;
+ })
+ .then(function(e) {
+ assert_equals(e.data.results, 'finish');
+ });
+ }, 'Verify the response of FetchEvent using XMLHttpRequest');
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/fetch-waits-for-activate.https.html b/test/wpt/tests/service-workers/service-worker/fetch-waits-for-activate.https.html
new file mode 100644
index 0000000..7c88845
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/fetch-waits-for-activate.https.html
@@ -0,0 +1,128 @@
+<!DOCTYPE html>
+<title>Service Worker: Fetch Event Waits for Activate Event</title>
+<meta name=timeout content=long>
+<script src="/resources/testharness.js"></script>
+<script src="resources/testharness-helpers.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+
+const worker_url = 'resources/fetch-waits-for-activate-worker.js';
+const normalized_worker_url = normalizeURL(worker_url);
+const worker_scope = 'resources/fetch-waits-for-activate/';
+
+// Resolves with the Service Worker's registration once it's reached the
+// "activating" state. (The Service Worker should remain "activating" until
+// explicitly told advance to the "activated" state).
+async function registerAndWaitForActivating(t) {
+ const registration = await service_worker_unregister_and_register(
+ t, worker_url, worker_scope);
+ t.add_cleanup(() => service_worker_unregister(t, worker_scope));
+
+ await wait_for_state(t, registration.installing, 'activating');
+
+ return registration;
+}
+
+// Attempts to ensure that the "Handle Fetch" algorithm has reached the step
+//
+// "If activeWorker’s state is "activating", wait for activeWorker’s state to
+// become "activated"."
+//
+// by waiting for some time to pass.
+//
+// WARNING: whether the algorithm has reached that step isn't directly
+// observable, so this is best effort and can race. Note that this can only
+// result in false positives (where the algorithm hasn't reached that step yet
+// and any functional events haven't actually been handled by the Service
+// Worker).
+async function ensureFunctionalEventsAreWaiting(registration) {
+ await (new Promise(resolve => { setTimeout(resolve, 1000); }));
+
+ assert_equals(registration.active.scriptURL, normalized_worker_url,
+ 'active worker should be present');
+ assert_equals(registration.active.state, 'activating',
+ 'active worker should be in activating state');
+}
+
+promise_test(async t => {
+ const registration = await registerAndWaitForActivating(t);
+
+ let frame = null;
+ t.add_cleanup(() => {
+ if (frame) {
+ frame.remove();
+ }
+ });
+
+ // This should block until we message the worker to tell it to complete
+ // the activate event.
+ const frameLoadPromise = with_iframe(worker_scope).then(function(f) {
+ frame = f;
+ });
+
+ await ensureFunctionalEventsAreWaiting(registration);
+ assert_equals(frame, null, 'frame should not be loaded');
+
+ registration.active.postMessage('ACTIVATE');
+
+ await frameLoadPromise;
+ assert_equals(frame.contentWindow.navigator.serviceWorker.controller.scriptURL,
+ normalized_worker_url,
+ 'frame should now be loaded and controlled');
+ assert_equals(registration.active.state, 'activated',
+ 'active worker should be in activated state');
+}, 'Navigation fetch events should wait for the activate event to complete.');
+
+promise_test(async t => {
+ const frame = await with_iframe(worker_scope);
+ t.add_cleanup(() => { frame.remove(); });
+
+ const registration = await registerAndWaitForActivating(t);
+
+ // Make the Service Worker control the frame so the frame can perform an
+ // intercepted fetch.
+ await (new Promise(resolve => {
+ navigator.serviceWorker.onmessage = e => {
+ assert_equals(
+ frame.contentWindow.navigator.serviceWorker.controller.scriptURL,
+ normalized_worker_url, 'frame should be controlled');
+ resolve();
+ };
+
+ registration.active.postMessage('CLAIM');
+ }));
+
+ const fetch_url = `${worker_scope}non/existent/path`;
+ const expected_fetch_result = 'Hello world';
+ let fetch_promise_settled = false;
+
+ // This should block until we message the worker to tell it to complete
+ // the activate event.
+ const fetchPromise = frame.contentWindow.fetch(fetch_url, {
+ method: 'POST',
+ body: expected_fetch_result,
+ }).then(response => {
+ fetch_promise_settled = true;
+ return response;
+ });
+
+ await ensureFunctionalEventsAreWaiting(registration);
+ assert_false(fetch_promise_settled,
+ "fetch()-ing a Service Worker-controlled scope shouldn't have " +
+ "settled yet");
+
+ registration.active.postMessage('ACTIVATE');
+
+ const response = await fetchPromise;
+ assert_equals(await response.text(), expected_fetch_result,
+ "Service Worker should have responded to request to" +
+ fetch_url)
+ assert_equals(registration.active.state, 'activated',
+ 'active worker should be in activated state');
+}, 'Subresource fetch events should wait for the activate event to complete.');
+
+</script>
+</body>
diff --git a/test/wpt/tests/service-workers/service-worker/getregistration.https.html b/test/wpt/tests/service-workers/service-worker/getregistration.https.html
new file mode 100644
index 0000000..634c2ef
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/getregistration.https.html
@@ -0,0 +1,108 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+async_test(function(t) {
+ var documentURL = 'no-such-worker';
+ navigator.serviceWorker.getRegistration(documentURL)
+ .then(function(value) {
+ assert_equals(value, undefined,
+ 'getRegistration should resolve with undefined');
+ t.done();
+ })
+ .catch(unreached_rejection(t));
+ }, 'getRegistration');
+
+promise_test(function(t) {
+ var scope = 'resources/scope/getregistration/normal';
+ var registration;
+ return service_worker_unregister_and_register(t, 'resources/empty-worker.js',
+ scope)
+ .then(function(r) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ registration = r;
+ return navigator.serviceWorker.getRegistration(scope);
+ })
+ .then(function(value) {
+ assert_equals(
+ value, registration,
+ 'getRegistration should resolve to the same registration object');
+ });
+ }, 'Register then getRegistration');
+
+promise_test(function(t) {
+ var scope = 'resources/scope/getregistration/url-with-fragment';
+ var documentURL = scope + '#ref';
+ var registration;
+ return service_worker_unregister_and_register(t, 'resources/empty-worker.js',
+ scope)
+ .then(function(r) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ registration = r;
+ return navigator.serviceWorker.getRegistration(documentURL);
+ })
+ .then(function(value) {
+ assert_equals(
+ value, registration,
+ 'getRegistration should resolve to the same registration object');
+ });
+ }, 'Register then getRegistration with a URL having a fragment');
+
+async_test(function(t) {
+ var documentURL = 'http://example.com/';
+ navigator.serviceWorker.getRegistration(documentURL)
+ .then(function() {
+ assert_unreached(
+ 'getRegistration with an out of origin URL should fail');
+ }, function(reason) {
+ assert_equals(
+ reason.name, 'SecurityError',
+ 'getRegistration with an out of origin URL should fail');
+ t.done();
+ })
+ .catch(unreached_rejection(t));
+ }, 'getRegistration with a cross origin URL');
+
+async_test(function(t) {
+ var scope = 'resources/scope/getregistration/register-unregister';
+ service_worker_unregister_and_register(t, 'resources/empty-worker.js',
+ scope)
+ .then(function(registration) {
+ return registration.unregister();
+ })
+ .then(function() {
+ return navigator.serviceWorker.getRegistration(scope);
+ })
+ .then(function(value) {
+ assert_equals(value, undefined,
+ 'getRegistration should resolve with undefined');
+ t.done();
+ })
+ .catch(unreached_rejection(t));
+ }, 'Register then Unregister then getRegistration');
+
+
+promise_test(async function(t) {
+ const scope = 'resources/scope/getregistration/register-unregister';
+ const registration = await service_worker_unregister_and_register(
+ t, 'resources/empty-worker.js', scope
+ );
+
+ const frame = await with_iframe(scope);
+ t.add_cleanup(() => frame.remove());
+
+ const frameNav = frame.contentWindow.navigator;
+ await registration.unregister();
+ const value = await frameNav.serviceWorker.getRegistration(scope);
+
+ assert_equals(value, undefined, 'getRegistration should resolve with undefined');
+}, 'Register then Unregister then getRegistration in controlled iframe');
+
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/getregistrations.https.html b/test/wpt/tests/service-workers/service-worker/getregistrations.https.html
new file mode 100644
index 0000000..3a9b9a2
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/getregistrations.https.html
@@ -0,0 +1,134 @@
+<!DOCTYPE html>
+<title>Service Worker: getRegistrations()</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script>
+// Purge the existing registrations for the origin.
+// getRegistrations() is used in order to avoid adding additional complexity
+// e.g. adding an internal function.
+promise_test(async () => {
+ const registrations = await navigator.serviceWorker.getRegistrations();
+ await Promise.all(registrations.map(r => r.unregister()));
+ const value = await navigator.serviceWorker.getRegistrations();
+ assert_array_equals(
+ value, [],
+ 'getRegistrations should resolve with an empty array.');
+}, 'registrations are not returned following unregister');
+
+promise_test(async t => {
+ const scope = 'resources/scope/getregistrations/normal';
+ const script = 'resources/empty-worker.js';
+ const registrations = [
+ await service_worker_unregister_and_register(t, script, scope)];
+ t.add_cleanup(() => registrations[0].unregister());
+ const value = await navigator.serviceWorker.getRegistrations();
+ assert_array_equals(value, registrations,
+ 'getRegistrations should resolve with an array of registrations');
+}, 'Register then getRegistrations');
+
+promise_test(async t => {
+ const scope1 = 'resources/scope/getregistrations/scope1';
+ const scope2 = 'resources/scope/getregistrations/scope2';
+ const scope3 = 'resources/scope/getregistrations/scope12';
+
+ const script = 'resources/empty-worker.js';
+ t.add_cleanup(() => service_worker_unregister(t, scope1));
+ t.add_cleanup(() => service_worker_unregister(t, scope2));
+ t.add_cleanup(() => service_worker_unregister(t, scope3));
+
+ const registrations = [
+ await service_worker_unregister_and_register(t, script, scope1),
+ await service_worker_unregister_and_register(t, script, scope2),
+ await service_worker_unregister_and_register(t, script, scope3),
+ ];
+
+ const value = await navigator.serviceWorker.getRegistrations();
+ assert_array_equals(value, registrations);
+}, 'Register multiple times then getRegistrations');
+
+promise_test(async t => {
+ const scope = 'resources/scope/getregistrations/register-unregister';
+ const script = 'resources/empty-worker.js';
+ const registration = await service_worker_unregister_and_register(t, script, scope);
+ await registration.unregister();
+ const value = await navigator.serviceWorker.getRegistrations();
+ assert_array_equals(
+ value, [], 'getRegistrations should resolve with an empty array.');
+}, 'Register then Unregister then getRegistrations');
+
+promise_test(async t => {
+ const scope = 'resources/scope/getregistrations/register-unregister-controlled';
+ const script = 'resources/empty-worker.js';
+ const registration = await service_worker_unregister_and_register(t, script, scope);
+ await wait_for_state(t, registration.installing, 'activated');
+
+ // Create a frame controlled by the service worker and unregister the
+ // worker.
+ const frame = await with_iframe(scope);
+ t.add_cleanup(() => frame.remove());
+ await registration.unregister();
+
+ const value = await navigator.serviceWorker.getRegistrations();
+ assert_array_equals(
+ value, [],
+ 'getRegistrations should resolve with an empty array.');
+ assert_equals(registration.installing, null);
+ assert_equals(registration.waiting, null);
+ assert_equals(registration.active.state, 'activated');
+}, 'Register then Unregister with controlled frame then getRegistrations');
+
+promise_test(async t => {
+ const host_info = get_host_info();
+ // Rewrite the url to point to remote origin.
+ const frame_same_origin_url = new URL("resources/frame-for-getregistrations.html", window.location);
+ const frame_url = host_info['HTTPS_REMOTE_ORIGIN'] + frame_same_origin_url.pathname;
+ const scope = 'resources/scope-for-getregistrations';
+ const script = 'resources/empty-worker.js';
+
+ // Loads an iframe and waits for 'ready' message from it to resolve promise.
+ // Caller is responsible for removing frame.
+ function with_iframe_ready(url) {
+ return new Promise(resolve => {
+ const frame = document.createElement('iframe');
+ frame.src = url;
+ window.addEventListener('message', function onMessage(e) {
+ window.removeEventListener('message', onMessage);
+ if (e.data == 'ready') {
+ resolve(frame);
+ }
+ });
+ document.body.appendChild(frame);
+ });
+ }
+
+ // We need this special frame loading function because the frame is going
+ // to register it's own service worker and there is the possibility that that
+ // register() finishes after the register() for the same domain later in the
+ // test. So we have to wait until the cross origin register() is done, and not
+ // just until the frame loads.
+ const frame = await with_iframe_ready(frame_url);
+ t.add_cleanup(async () => {
+ // Wait until the cross-origin worker is unregistered.
+ let resolve;
+ const channel = new MessageChannel();
+ channel.port1.onmessage = e => {
+ if (e.data == 'unregistered')
+ resolve();
+ };
+ frame.contentWindow.postMessage('unregister', '*', [channel.port2]);
+ await new Promise(r => { resolve = r; });
+
+ frame.remove();
+ });
+
+ const registrations = [
+ await service_worker_unregister_and_register(t, script, scope)];
+ t.add_cleanup(() => registrations[0].unregister());
+ const value = await navigator.serviceWorker.getRegistrations();
+ assert_array_equals(
+ value, registrations,
+ 'getRegistrations should only return same origin registrations.');
+}, 'getRegistrations promise resolves only with same origin registrations.');
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/global-serviceworker.https.any.js b/test/wpt/tests/service-workers/service-worker/global-serviceworker.https.any.js
new file mode 100644
index 0000000..19d7784
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/global-serviceworker.https.any.js
@@ -0,0 +1,53 @@
+// META: title=serviceWorker on service worker global
+// META: global=serviceworker
+
+test(() => {
+ assert_equals(registration.installing, null, 'registration.installing');
+ assert_equals(registration.waiting, null, 'registration.waiting');
+ assert_equals(registration.active, null, 'registration.active');
+ assert_true('serviceWorker' in self, 'self.serviceWorker exists');
+ assert_equals(serviceWorker.state, 'parsed', 'serviceWorker.state');
+ assert_readonly(self, 'serviceWorker', `self.serviceWorker is read only`);
+}, 'First run');
+
+// Cache this for later tests.
+const initialServiceWorker = self.serviceWorker;
+
+async_test((t) => {
+ assert_true('serviceWorker' in self, 'self.serviceWorker exists');
+ serviceWorker.postMessage({ messageTest: true });
+
+ // The rest of the test runs once this receives the above message.
+ addEventListener('message', t.step_func((event) => {
+ // Ignore unrelated messages.
+ if (!event.data.messageTest) return;
+ assert_equals(event.source, serviceWorker, 'event.source');
+ t.done();
+ }));
+}, 'Can post message to self during startup');
+
+// The test is registered now so there isn't a race condition when collecting tests, but the asserts
+// don't happen until the 'install' event fires.
+async_test((t) => {
+ addEventListener('install', t.step_func_done(() => {
+ assert_true('serviceWorker' in self, 'self.serviceWorker exists');
+ assert_equals(serviceWorker, initialServiceWorker, `self.serviceWorker hasn't changed`);
+ assert_equals(registration.installing, serviceWorker, 'registration.installing');
+ assert_equals(registration.waiting, null, 'registration.waiting');
+ assert_equals(registration.active, null, 'registration.active');
+ assert_equals(serviceWorker.state, 'installing', 'serviceWorker.state');
+ }));
+}, 'During install');
+
+// The test is registered now so there isn't a race condition when collecting tests, but the asserts
+// don't happen until the 'activate' event fires.
+async_test((t) => {
+ addEventListener('activate', t.step_func_done(() => {
+ assert_true('serviceWorker' in self, 'self.serviceWorker exists');
+ assert_equals(serviceWorker, initialServiceWorker, `self.serviceWorker hasn't changed`);
+ assert_equals(registration.installing, null, 'registration.installing');
+ assert_equals(registration.waiting, null, 'registration.waiting');
+ assert_equals(registration.active, serviceWorker, 'registration.active');
+ assert_equals(serviceWorker.state, 'activating', 'serviceWorker.state');
+ }));
+}, 'During activate');
diff --git a/test/wpt/tests/service-workers/service-worker/historical.https.any.js b/test/wpt/tests/service-workers/service-worker/historical.https.any.js
new file mode 100644
index 0000000..20b3ddf
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/historical.https.any.js
@@ -0,0 +1,5 @@
+// META: global=serviceworker
+
+test((t) => {
+ assert_false('targetClientId' in FetchEvent.prototype)
+}, 'targetClientId should not be on FetchEvent');
diff --git a/test/wpt/tests/service-workers/service-worker/http-to-https-redirect-and-register.https.html b/test/wpt/tests/service-workers/service-worker/http-to-https-redirect-and-register.https.html
new file mode 100644
index 0000000..5626237
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/http-to-https-redirect-and-register.https.html
@@ -0,0 +1,49 @@
+<!DOCTYPE html>
+<title>register on a secure page after redirect from an non-secure url</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+'use strict';
+
+var host_info = get_host_info();
+
+// Loads a non-secure url in a new window, which redirects to |target_url|.
+// That page then registers a service worker, and messages back with the result.
+// Returns a promise that resolves with the result.
+function redirect_and_register(target_url) {
+ var redirect_url = host_info.HTTP_REMOTE_ORIGIN + base_path() +
+ 'resources/redirect.py?Redirect=';
+ var child = window.open(redirect_url + encodeURIComponent(target_url));
+ return new Promise(resolve => {
+ window.addEventListener('message', e => resolve(e.data));
+ })
+ .then(function(result) {
+ child.close();
+ return result;
+ });
+}
+
+promise_test(function(t) {
+ var target_url = window.location.origin + base_path() +
+ 'resources/http-to-https-redirect-and-register-iframe.html';
+
+ return redirect_and_register(target_url)
+ .then(result => {
+ assert_equals(result, 'OK');
+ });
+ }, 'register on a secure page after redirect from an non-secure url');
+
+promise_test(function(t) {
+ var target_url = host_info.HTTP_REMOTE_ORIGIN + base_path() +
+ 'resources/http-to-https-redirect-and-register-iframe.html';
+
+ return redirect_and_register(target_url)
+ .then(result => {
+ assert_equals(result, 'FAIL: navigator.serviceWorker is undefined');
+ });
+ }, 'register on a non-secure page after redirect from an non-secure url');
+</script>
+</body>
diff --git a/test/wpt/tests/service-workers/service-worker/immutable-prototype-serviceworker.https.html b/test/wpt/tests/service-workers/service-worker/immutable-prototype-serviceworker.https.html
new file mode 100644
index 0000000..e63f6b3
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/immutable-prototype-serviceworker.https.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+'use strict';
+
+let expected = ['immutable', 'immutable', 'immutable', 'immutable', 'immutable'];
+
+promise_test(t =>
+ navigator.serviceWorker.register('resources/immutable-prototype-serviceworker.js', {scope: './resources/'})
+ .then(registration => {
+ let worker = registration.installing || registration.waiting || registration.active;
+ let channel = new MessageChannel()
+ worker.postMessage(channel.port2, [channel.port2]);
+ let resolve;
+ let promise = new Promise(r => resolve = r);
+ channel.port1.onmessage = resolve;
+ return promise.then(result => assert_array_equals(expected, result.data));
+ }),
+'worker prototype chain should be immutable');
+</script>
+</html>
diff --git a/test/wpt/tests/service-workers/service-worker/import-scripts-cross-origin.https.html b/test/wpt/tests/service-workers/service-worker/import-scripts-cross-origin.https.html
new file mode 100644
index 0000000..773708a
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/import-scripts-cross-origin.https.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Tests for importScripts: cross-origin</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+promise_test(async t => {
+ const scope = 'resources/import-scripts-cross-origin';
+ await service_worker_unregister(t, scope);
+ let reg = await navigator.serviceWorker.register(
+ 'resources/import-scripts-cross-origin-worker.sub.js', { scope: scope });
+ t.add_cleanup(_ => reg.unregister());
+ assert_not_equals(reg.installing, null, 'worker is installing');
+ }, 'importScripts() supports cross-origin requests');
+</script>
+</body>
diff --git a/test/wpt/tests/service-workers/service-worker/import-scripts-data-url.https.html b/test/wpt/tests/service-workers/service-worker/import-scripts-data-url.https.html
new file mode 100644
index 0000000..f092219
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/import-scripts-data-url.https.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Tests for importScripts: data: URL</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+promise_test(async t => {
+ const scope = 'resources/import-scripts-data-url';
+ await service_worker_unregister(t, scope);
+ let reg = await navigator.serviceWorker.register(
+ 'resources/import-scripts-data-url-worker.js', { scope: scope });
+ t.add_cleanup(_ => reg.unregister());
+ assert_not_equals(reg.installing, null, 'worker is installing');
+ }, 'importScripts() supports data URLs');
+</script>
+</body>
diff --git a/test/wpt/tests/service-workers/service-worker/import-scripts-mime-types.https.html b/test/wpt/tests/service-workers/service-worker/import-scripts-mime-types.https.html
new file mode 100644
index 0000000..1679831
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/import-scripts-mime-types.https.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Tests for importScripts: MIME types</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+/**
+ * Test that a Service Worker's importScript() only accepts valid MIME types.
+ */
+let serviceWorker = null;
+
+promise_test(async t => {
+ const scope = 'resources/import-scripts-mime-types';
+ const registration = await service_worker_unregister_and_register(t,
+ 'resources/import-scripts-mime-types-worker.js', scope);
+
+ add_completion_callback(() => { registration.unregister(); });
+
+ await wait_for_state(t, registration.installing, 'activated');
+
+ serviceWorker = registration.active;
+}, 'Global setup');
+
+promise_test(async t => {
+ await fetch_tests_from_worker(serviceWorker);
+}, 'Fetch importScripts tests from service worker')
+</script>
+</body>
diff --git a/test/wpt/tests/service-workers/service-worker/import-scripts-redirect.https.html b/test/wpt/tests/service-workers/service-worker/import-scripts-redirect.https.html
new file mode 100644
index 0000000..07ea494
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/import-scripts-redirect.https.html
@@ -0,0 +1,55 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Tests for importScripts: redirect</title>
+<script src="/common/utils.js"></script>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+promise_test(async t => {
+ const scope = 'resources/import-scripts-redirect';
+ await service_worker_unregister(t, scope);
+ let reg = await navigator.serviceWorker.register(
+ 'resources/import-scripts-redirect-worker.js', { scope: scope });
+ assert_not_equals(reg.installing, null, 'worker is installing');
+ await reg.unregister();
+ }, 'importScripts() supports redirects');
+
+promise_test(async t => {
+ const scope = 'resources/import-scripts-redirect';
+ await service_worker_unregister(t, scope);
+ let reg = await navigator.serviceWorker.register(
+ 'resources/import-scripts-redirect-worker.js', { scope: scope });
+ assert_not_equals(reg.installing, null, 'before update');
+ await wait_for_state(t, reg.installing, 'activated');
+ await Promise.all([
+ wait_for_update(t, reg),
+ reg.update()
+ ]);
+ assert_not_equals(reg.installing, null, 'after update');
+ await reg.unregister();
+ },
+ "an imported script redirects, and the body changes during the update check");
+
+promise_test(async t => {
+ const key = token();
+ const scope = 'resources/import-scripts-redirect';
+ await service_worker_unregister(t, scope);
+ let reg = await navigator.serviceWorker.register(
+ `resources/import-scripts-redirect-on-second-time-worker.js?Key=${key}`,
+ { scope });
+ t.add_cleanup(() => reg.unregister());
+
+ assert_not_equals(reg.installing, null, 'before update');
+ await wait_for_state(t, reg.installing, 'activated');
+ await Promise.all([
+ wait_for_update(t, reg),
+ reg.update()
+ ]);
+ assert_not_equals(reg.installing, null, 'after update');
+ },
+ "an imported script doesn't redirect initially, then redirects during " +
+ "the update check and the body changes");
+</script>
+</body>
diff --git a/test/wpt/tests/service-workers/service-worker/import-scripts-resource-map.https.html b/test/wpt/tests/service-workers/service-worker/import-scripts-resource-map.https.html
new file mode 100644
index 0000000..4742bd0
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/import-scripts-resource-map.https.html
@@ -0,0 +1,34 @@
+<!DOCTYPE html>
+<meta charset="utf-8" />
+<title>Tests for importScripts: script resource map</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+ <script>
+ // This test registers a worker that imports a script multiple times. The
+ // script should be stored on the first import and thereafter that stored
+ // script should be loaded. The worker asserts that the stored script was
+ // loaded; if the assert fails then registration fails.
+
+ promise_test(async t => {
+ const SCOPE = "resources/import-scripts-resource-map";
+ const SCRIPT = "resources/import-scripts-resource-map-worker.js";
+ await service_worker_unregister(t, SCOPE);
+ const registration = await navigator.serviceWorker.register(SCRIPT, {
+ scope: SCOPE
+ });
+ await registration.unregister();
+ }, "import the same script URL multiple times");
+
+ promise_test(async t => {
+ const SCOPE = "resources/import-scripts-diff-resource-map";
+ const SCRIPT = "resources/import-scripts-diff-resource-map-worker.js";
+ await service_worker_unregister(t, SCOPE);
+ const registration = await navigator.serviceWorker.register(SCRIPT, {
+ scope: SCOPE
+ });
+ await registration.unregister();
+ }, "call importScripts() with multiple arguments");
+ </script>
+</body>
diff --git a/test/wpt/tests/service-workers/service-worker/import-scripts-updated-flag.https.html b/test/wpt/tests/service-workers/service-worker/import-scripts-updated-flag.https.html
new file mode 100644
index 0000000..09b4496
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/import-scripts-updated-flag.https.html
@@ -0,0 +1,83 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Tests for importScripts: import scripts updated flag</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+// This test registers a worker that calls importScripts at various stages of
+// service worker lifetime. The sub-tests trigger subsequent `importScript`
+// invocations via the `message` event.
+
+var register;
+
+function post_and_wait_for_reply(worker, message) {
+ return new Promise(resolve => {
+ navigator.serviceWorker.onmessage = e => { resolve(e.data); };
+ worker.postMessage(message);
+ });
+}
+
+promise_test(function(t) {
+ const scope = 'resources/import-scripts-updated-flag';
+ let registration;
+
+ register = service_worker_unregister_and_register(
+ t, 'resources/import-scripts-updated-flag-worker.js', scope)
+ .then(r => {
+ registration = r;
+ add_completion_callback(() => { registration.unregister(); });
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(() => {
+ // This test should not be considered complete until after the
+ // service worker has been unregistered. Currently, `testharness.js`
+ // does not support asynchronous global "tear down" logic, so this
+ // must be expressed using a dedicated `promise_test`. Because the
+ // other sub-tests in this file are declared synchronously, this test
+ // will be the final test executed.
+ promise_test(function(t) {
+ return registration.unregister();
+ });
+
+ return registration.active;
+ });
+
+ return register;
+ }, 'initialize global state');
+
+promise_test(t => {
+ return register
+ .then(function(worker) {
+ return post_and_wait_for_reply(worker, 'root-and-message');
+ })
+ .then(result => {
+ assert_equals(result.error, null);
+ assert_equals(result.value, 'root-and-message');
+ });
+ }, 'import script previously imported at worker evaluation time');
+
+promise_test(t => {
+ return register
+ .then(function(worker) {
+ return post_and_wait_for_reply(worker, 'install-and-message');
+ })
+ .then(result => {
+ assert_equals(result.error, null);
+ assert_equals(result.value, 'install-and-message');
+ });
+ }, 'import script previously imported at worker install time');
+
+promise_test(t => {
+ return register
+ .then(function(worker) {
+ return post_and_wait_for_reply(worker, 'message');
+ })
+ .then(result => {
+ assert_equals(result.error, 'NetworkError');
+ assert_equals(result.value, null);
+ });
+ }, 'import script not previously imported');
+</script>
+</body>
diff --git a/test/wpt/tests/service-workers/service-worker/indexeddb.https.html b/test/wpt/tests/service-workers/service-worker/indexeddb.https.html
new file mode 100644
index 0000000..be9be49
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/indexeddb.https.html
@@ -0,0 +1,78 @@
+<!DOCTYPE html>
+<title>Service Worker: Indexed DB</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+function readDB() {
+ return new Promise(function(resolve, reject) {
+ var openRequest = indexedDB.open('db');
+
+ openRequest.onerror = reject;
+ openRequest.onsuccess = function() {
+ var db = openRequest.result;
+ var tx = db.transaction('store');
+ var store = tx.objectStore('store');
+ var getRequest = store.get('key');
+
+ getRequest.onerror = function() {
+ db.close();
+ reject(getRequest.error);
+ };
+ getRequest.onsuccess = function() {
+ db.close();
+ resolve(getRequest.result);
+ };
+ };
+ });
+}
+
+function send(worker, action) {
+ return new Promise(function(resolve, reject) {
+ var messageChannel = new MessageChannel();
+ messageChannel.port1.onmessage = function(event) {
+ if (event.data.type === 'error') {
+ reject(event.data.reason);
+ }
+
+ resolve();
+ };
+
+ worker.postMessage(
+ {action: action, port: messageChannel.port2},
+ [messageChannel.port2]);
+ });
+}
+
+promise_test(function(t) {
+ var scope = 'resources/blank.html';
+
+ return service_worker_unregister_and_register(
+ t, 'resources/indexeddb-worker.js', scope)
+ .then(function(registration) {
+ var worker = registration.installing;
+
+ promise_test(function() {
+ return registration.unregister();
+ }, 'clean up: registration');
+
+ return send(worker, 'create')
+ .then(function() {
+ promise_test(function() {
+ return new Promise(function(resolve, reject) {
+ var delete_request = indexedDB.deleteDatabase('db');
+
+ delete_request.onsuccess = resolve;
+ delete_request.onerror = reject;
+ });
+ }, 'clean up: database');
+ })
+ .then(readDB)
+ .then(function(value) {
+ assert_equals(
+ value, 'value',
+ 'The get() result should match what the worker put().');
+ });
+ });
+ }, 'Verify Indexed DB operation in a Service Worker');
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/install-event-type.https.html b/test/wpt/tests/service-workers/service-worker/install-event-type.https.html
new file mode 100644
index 0000000..7e74af8
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/install-event-type.https.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="resources/testharness-helpers.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+function wait_for_install_event(worker) {
+ return new Promise(function(resolve) {
+ worker.addEventListener('statechange', function(event) {
+ if (worker.state == 'installed')
+ resolve(true);
+ else if (worker.state == 'redundant')
+ resolve(false);
+ });
+ });
+}
+
+promise_test(function(t) {
+ var script = 'resources/install-event-type-worker.js';
+ var scope = 'resources/install-event-type';
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(function(registration) {
+ return wait_for_install_event(registration.installing);
+ })
+ .then(function(did_install) {
+ assert_true(did_install, 'The worker was installed');
+ })
+ }, 'install event type');
+
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/installing.https.html b/test/wpt/tests/service-workers/service-worker/installing.https.html
new file mode 100644
index 0000000..0f257b6
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/installing.https.html
@@ -0,0 +1,48 @@
+<!DOCTYPE html>
+<title>ServiceWorker: navigator.serviceWorker.installing</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+
+const SCRIPT = 'resources/empty-worker.js';
+const SCOPE = 'resources/blank.html';
+
+// "installing" is set
+promise_test(async t => {
+
+ t.add_cleanup(async() => {
+ if (frame)
+ frame.remove();
+ if (registration)
+ await registration.unregister();
+ });
+
+ await service_worker_unregister(t, SCOPE);
+ const frame = await with_iframe(SCOPE);
+ const registration =
+ await navigator.serviceWorker.register(SCRIPT, {scope: SCOPE});
+ const container = frame.contentWindow.navigator.serviceWorker;
+ assert_equals(container.controller, null, 'controller');
+ assert_equals(registration.active, null, 'registration.active');
+ assert_equals(registration.waiting, null, 'registration.waiting');
+ assert_equals(registration.installing.scriptURL, normalizeURL(SCRIPT),
+ 'registration.installing.scriptURL');
+ // FIXME: Add a test for a frame created after installation.
+ // Should the existing frame ("frame") block activation?
+}, 'installing is set');
+
+// Tests that The ServiceWorker objects returned from installing attribute getter
+// that represent the same service worker are the same objects.
+promise_test(async t => {
+ const registration1 =
+ await service_worker_unregister_and_register(t, SCRIPT, SCOPE);
+ const registration2 = await navigator.serviceWorker.getRegistration(SCOPE);
+ assert_equals(registration1.installing, registration2.installing,
+ 'ServiceWorkerRegistration.installing should return the ' +
+ 'same object');
+ await registration1.unregister();
+}, 'The ServiceWorker objects returned from installing attribute getter that ' +
+ 'represent the same service worker are the same objects');
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/interface-requirements-sw.https.html b/test/wpt/tests/service-workers/service-worker/interface-requirements-sw.https.html
new file mode 100644
index 0000000..eef868c
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/interface-requirements-sw.https.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<title>Service Worker Global Scope Interfaces</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+'use strict';
+
+// interface-requirements-worker.sub.js checks additional interface
+// requirements, on top of the basic IDL that is validated in
+// service-workers/idlharness.any.js
+service_worker_test(
+ 'resources/interface-requirements-worker.sub.js',
+ 'Interfaces and attributes in ServiceWorkerGlobalScope');
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/invalid-blobtype.https.html b/test/wpt/tests/service-workers/service-worker/invalid-blobtype.https.html
new file mode 100644
index 0000000..1c5920f
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/invalid-blobtype.https.html
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<title>Service Worker: respondWith with header value containing a null byte</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+promise_test(function(t) {
+ var SCOPE = 'resources/invalid-blobtype-iframe.https.html';
+ var SCRIPT = 'resources/invalid-blobtype-worker.js';
+ var host_info = get_host_info();
+ return service_worker_unregister_and_register(t, SCRIPT, SCOPE)
+ .then(function(registration) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, SCOPE);
+ });
+
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() { return with_iframe(SCOPE); })
+ .then(function(frame) {
+ t.add_cleanup(function() {
+ frame.remove();
+ });
+
+ var channel = new MessageChannel();
+ var onMsg = new Promise(function(resolve) {
+ channel.port1.onmessage = resolve;
+ });
+
+ frame.contentWindow.postMessage({},
+ host_info['HTTPS_ORIGIN'],
+ [channel.port2]);
+ return onMsg;
+ })
+ .then(function(e) {
+ assert_equals(e.data.results, 'finish');
+ });
+ }, 'Verify the response of FetchEvent using XMLHttpRequest');
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/invalid-header.https.html b/test/wpt/tests/service-workers/service-worker/invalid-header.https.html
new file mode 100644
index 0000000..1bc9769
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/invalid-header.https.html
@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<title>Service Worker: respondWith with header value containing a null byte</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+promise_test(function(t) {
+ var SCOPE = 'resources/invalid-header-iframe.https.html';
+ var SCRIPT = 'resources/invalid-header-worker.js';
+ var host_info = get_host_info();
+ return service_worker_unregister_and_register(t, SCRIPT, SCOPE)
+ .then(function(registration) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, SCOPE);
+ });
+
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() { return with_iframe(SCOPE); })
+ .then(function(frame) {
+ t.add_cleanup(function() {
+ frame.remove();
+ });
+
+ var channel = new MessageChannel();
+ var onMsg = new Promise(function(resolve) {
+ channel.port1.onmessage = resolve;
+ });
+ frame.contentWindow.postMessage({},
+ host_info['HTTPS_ORIGIN'],
+ [channel.port2]);
+ return onMsg;
+ })
+ .then(function(e) {
+ assert_equals(e.data.results, 'finish');
+ });
+ }, 'Verify the response of FetchEvent using XMLHttpRequest');
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/iso-latin1-header.https.html b/test/wpt/tests/service-workers/service-worker/iso-latin1-header.https.html
new file mode 100644
index 0000000..c27a5f4
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/iso-latin1-header.https.html
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<title>Service Worker: respondWith with header value containing an ISO Latin 1 (ISO-8859-1 Character Set) string</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+promise_test(function(t) {
+ var SCOPE = 'resources/iso-latin1-header-iframe.html';
+ var SCRIPT = 'resources/iso-latin1-header-worker.js';
+ var host_info = get_host_info();
+ return service_worker_unregister_and_register(t, SCRIPT, SCOPE)
+ .then(function(registration) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, SCOPE);
+ });
+
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() { return with_iframe(SCOPE); })
+ .then(function(frame) {
+ var channel = new MessageChannel();
+ t.add_cleanup(function() {
+ frame.remove();
+ });
+
+ var onMsg = new Promise(function(resolve) {
+ channel.port1.onmessage = resolve;
+ });
+
+ frame.contentWindow.postMessage({},
+ host_info['HTTPS_ORIGIN'],
+ [channel.port2]);
+ return onMsg;
+ })
+ .then(function(e) {
+ assert_equals(e.data.results, 'finish');
+ });
+ }, 'Verify the response of FetchEvent using XMLHttpRequest');
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/local-url-inherit-controller.https.html b/test/wpt/tests/service-workers/service-worker/local-url-inherit-controller.https.html
new file mode 100644
index 0000000..6702abc
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/local-url-inherit-controller.https.html
@@ -0,0 +1,115 @@
+<!DOCTYPE html>
+<title>Service Worker: local URL windows and workers inherit controller</title>
+<meta name=timeout content=long>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+
+const SCRIPT = 'resources/local-url-inherit-controller-worker.js';
+const SCOPE = 'resources/local-url-inherit-controller-frame.html';
+
+async function doAsyncTest(t, opts) {
+ let name = `${opts.scheme}-${opts.child}-${opts.check}`;
+ let scope = SCOPE + '?name=' + name;
+ let reg = await service_worker_unregister_and_register(t, SCRIPT, scope);
+ add_completion_callback(_ => reg.unregister());
+ await wait_for_state(t, reg.installing, 'activated');
+
+ let frame = await with_iframe(scope);
+ add_completion_callback(_ => frame.remove());
+ assert_not_equals(frame.contentWindow.navigator.serviceWorker.controller, null,
+ 'frame should be controlled');
+
+ let result = await frame.contentWindow.checkChildController(opts);
+ result = result.data;
+
+ let expect = 'unexpected';
+ if (opts.check === 'controller') {
+ expect = opts.expect === 'inherit'
+ ? frame.contentWindow.navigator.serviceWorker.controller.scriptURL
+ : null;
+ } else if (opts.check === 'fetch') {
+ // The service worker FetchEvent handler will provide an "intercepted"
+ // body. If the local URL ends up with an opaque origin and is not
+ // intercepted then it will get an opaque Response. In that case it
+ // should see an empty string body.
+ expect = opts.expect === 'intercept' ? 'intercepted' : '';
+ }
+
+ assert_equals(result, expect,
+ `${opts.scheme} URL ${opts.child} should ${opts.expect} ${opts.check}`);
+}
+
+promise_test(function(t) {
+ return doAsyncTest(t, {
+ scheme: 'blob',
+ child: 'iframe',
+ check: 'controller',
+ expect: 'inherit',
+ });
+}, 'Same-origin blob URL iframe should inherit service worker controller.');
+
+promise_test(function(t) {
+ return doAsyncTest(t, {
+ scheme: 'blob',
+ child: 'iframe',
+ check: 'fetch',
+ expect: 'intercept',
+ });
+}, 'Same-origin blob URL iframe should intercept fetch().');
+
+promise_test(function(t) {
+ return doAsyncTest(t, {
+ scheme: 'blob',
+ child: 'worker',
+ check: 'controller',
+ expect: 'inherit',
+ });
+}, 'Same-origin blob URL worker should inherit service worker controller.');
+
+promise_test(function(t) {
+ return doAsyncTest(t, {
+ scheme: 'blob',
+ child: 'worker',
+ check: 'fetch',
+ expect: 'intercept',
+ });
+}, 'Same-origin blob URL worker should intercept fetch().');
+
+promise_test(function(t) {
+ return doAsyncTest(t, {
+ scheme: 'data',
+ child: 'iframe',
+ check: 'fetch',
+ expect: 'not intercept',
+ });
+}, 'Data URL iframe should not intercept fetch().');
+
+promise_test(function(t) {
+ // Data URLs should result in an opaque origin and should probably not
+ // have access to a cross-origin service worker. See:
+ //
+ // https://github.com/w3c/ServiceWorker/issues/1262
+ //
+ return doAsyncTest(t, {
+ scheme: 'data',
+ child: 'worker',
+ check: 'controller',
+ expect: 'not inherit',
+ });
+}, 'Data URL worker should not inherit service worker controller.');
+
+promise_test(function(t) {
+ return doAsyncTest(t, {
+ scheme: 'data',
+ child: 'worker',
+ check: 'fetch',
+ expect: 'not intercept',
+ });
+}, 'Data URL worker should not intercept fetch().');
+
+</script>
+</body>
diff --git a/test/wpt/tests/service-workers/service-worker/mime-sniffing.https.html b/test/wpt/tests/service-workers/service-worker/mime-sniffing.https.html
new file mode 100644
index 0000000..8175bcd
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/mime-sniffing.https.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<title>Service Worker: MIME sniffing</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+promise_test(t => {
+ const SCOPE = 'resources/blank.html?mime-sniffing';
+ const SCRIPT = 'resources/mime-sniffing-worker.js';
+ return service_worker_unregister_and_register(t, SCRIPT, SCOPE)
+ .then(registration => {
+ add_completion_callback(() => registration.unregister());
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(_ => with_iframe(SCOPE))
+ .then(frame => {
+ add_completion_callback(() => frame.remove());
+ assert_equals(frame.contentWindow.document.body.innerText, 'test');
+ const h1 = frame.contentWindow.document.getElementById('testid');
+ assert_equals(h1.innerText,'test');
+ });
+ }, 'The response from service worker should be correctly MIME siniffed.');
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/multi-globals/current/current.https.html b/test/wpt/tests/service-workers/service-worker/multi-globals/current/current.https.html
new file mode 100644
index 0000000..82a48d4
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/multi-globals/current/current.https.html
@@ -0,0 +1,2 @@
+<!DOCTYPE html>
+<title>Current page used as a test helper</title>
diff --git a/test/wpt/tests/service-workers/service-worker/multi-globals/current/test-sw.js b/test/wpt/tests/service-workers/service-worker/multi-globals/current/test-sw.js
new file mode 100644
index 0000000..e673292
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/multi-globals/current/test-sw.js
@@ -0,0 +1 @@
+// Service worker for current/ \ No newline at end of file
diff --git a/test/wpt/tests/service-workers/service-worker/multi-globals/incumbent/incumbent.https.html b/test/wpt/tests/service-workers/service-worker/multi-globals/incumbent/incumbent.https.html
new file mode 100644
index 0000000..4585f15
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/multi-globals/incumbent/incumbent.https.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<title>Incumbent page used as a test helper</title>
+
+<iframe src="../current/current.https.html" id="c"></iframe>
+<iframe src="../relevant/relevant.https.html" id="r"></iframe>
+
+<script>
+'use strict';
+
+const current = document.querySelector('#c').contentWindow;
+const relevant = document.querySelector('#r').contentWindow;
+
+window.testRegister = options => {
+ return current.navigator.serviceWorker.register.call(relevant.navigator.serviceWorker, 'test-sw.js', options);
+};
+
+window.testGetRegistration = () => {
+ return current.navigator.serviceWorker.getRegistration.call(relevant.navigator.serviceWorker, 'test-sw.js');
+};
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/multi-globals/incumbent/test-sw.js b/test/wpt/tests/service-workers/service-worker/multi-globals/incumbent/test-sw.js
new file mode 100644
index 0000000..e2a0e93
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/multi-globals/incumbent/test-sw.js
@@ -0,0 +1 @@
+// Service worker for incumbent/ \ No newline at end of file
diff --git a/test/wpt/tests/service-workers/service-worker/multi-globals/relevant/relevant.https.html b/test/wpt/tests/service-workers/service-worker/multi-globals/relevant/relevant.https.html
new file mode 100644
index 0000000..44f42ed
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/multi-globals/relevant/relevant.https.html
@@ -0,0 +1,2 @@
+<!DOCTYPE html>
+<title>Relevant page used as a test helper</title>
diff --git a/test/wpt/tests/service-workers/service-worker/multi-globals/relevant/test-sw.js b/test/wpt/tests/service-workers/service-worker/multi-globals/relevant/test-sw.js
new file mode 100644
index 0000000..ff44cdf
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/multi-globals/relevant/test-sw.js
@@ -0,0 +1 @@
+// Service worker for relevant/ \ No newline at end of file
diff --git a/test/wpt/tests/service-workers/service-worker/multi-globals/test-sw.js b/test/wpt/tests/service-workers/service-worker/multi-globals/test-sw.js
new file mode 100644
index 0000000..ce3c940
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/multi-globals/test-sw.js
@@ -0,0 +1 @@
+// Service worker for / \ No newline at end of file
diff --git a/test/wpt/tests/service-workers/service-worker/multi-globals/url-parsing.https.html b/test/wpt/tests/service-workers/service-worker/multi-globals/url-parsing.https.html
new file mode 100644
index 0000000..b9dfe36
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/multi-globals/url-parsing.https.html
@@ -0,0 +1,73 @@
+<!DOCTYPE html>
+<title>register()/getRegistration() URL parsing, with multiple globals in play</title>
+<link rel="help" href="https://w3c.github.io/ServiceWorker/spec/service_worker/index.html#service-worker-container-register-method">
+<link rel="help" href="https://w3c.github.io/ServiceWorker/spec/service_worker/index.html#service-worker-container-getregistration-method">
+<link rel="author" title="Domenic Denicola" href="mailto:d@domenic.me">
+<script src="/resources/testharness.js"></script>
+<script src="../resources/testharness-helpers.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/test-helpers.sub.js"></script>
+
+<!-- This is the entry global -->
+
+<iframe src="incumbent/incumbent.https.html"></iframe>
+
+<script>
+'use strict';
+
+const loadPromise = new Promise(resolve => {
+ window.addEventListener('load', () => resolve());
+});
+
+promise_test(t => {
+ let registration;
+
+ return loadPromise.then(() => {
+ return frames[0].testRegister();
+ }).then(r => {
+ registration = r;
+ return wait_for_state(t, registration.installing, 'activated');
+ }).then(_ => {
+ assert_equals(registration.active.scriptURL, normalizeURL('relevant/test-sw.js'), 'the script URL should be parsed against the relevant global');
+ assert_equals(registration.scope, normalizeURL('relevant/'), 'the default scope URL should be parsed against the parsed script URL');
+
+ return registration.unregister();
+ });
+}, 'register should use the relevant global of the object it was called on to resolve the script URL and the default scope URL');
+
+promise_test(t => {
+ let registration;
+
+ return loadPromise.then(() => {
+ return frames[0].testRegister({ scope: 'scope' });
+ }).then(r => {
+ registration = r;
+ return wait_for_state(t, registration.installing, 'activated');
+ }).then(_ => {
+ assert_equals(registration.active.scriptURL, normalizeURL('relevant/test-sw.js'), 'the script URL should be parsed against the relevant global');
+ assert_equals(registration.scope, normalizeURL('relevant/scope'), 'the given scope URL should be parsed against the relevant global');
+
+ return registration.unregister();
+ });
+}, 'register should use the relevant global of the object it was called on to resolve the script URL and the given scope URL');
+
+promise_test(t => {
+ let registration;
+
+ return loadPromise.then(() => {
+ return navigator.serviceWorker.register(normalizeURL('relevant/test-sw.js'));
+ }).then(r => {
+ registration = r;
+ return frames[0].testGetRegistration();
+ })
+ .then(gottenRegistration => {
+ assert_not_equals(registration, null, 'the registration should not be null');
+ assert_not_equals(gottenRegistration, null, 'the registration from the other frame should not be null');
+ assert_equals(gottenRegistration.scope, registration.scope,
+ 'the retrieved registration\'s scope should be equal to the original\'s scope');
+
+ return registration.unregister();
+ });
+}, 'getRegistration should use the relevant global of the object it was called on to resolve the script URL');
+
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/multipart-image.https.html b/test/wpt/tests/service-workers/service-worker/multipart-image.https.html
new file mode 100644
index 0000000..00c20d2
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/multipart-image.https.html
@@ -0,0 +1,68 @@
+<!DOCTYPE html>
+<title>Tests for cross-origin multipart image returned by service worker</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+
+<script>
+// This tests loading a multipart image via service worker. The service worker responds with
+// an opaque or a non-opaque response. The content of opaque response should not be readable.
+
+const script = 'resources/multipart-image-worker.js';
+const scope = 'resources/multipart-image-iframe.html';
+let frame;
+
+function check_image_data(data) {
+ assert_equals(data[0], 255);
+ assert_equals(data[1], 0);
+ assert_equals(data[2], 0);
+ assert_equals(data[3], 255);
+}
+
+promise_test(t => {
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(registration => {
+ promise_test(() => {
+ if (frame) {
+ frame.remove();
+ }
+ return registration.unregister();
+ }, 'restore global state');
+
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(() => with_iframe(scope))
+ .then(f => {
+ frame = f;
+ });
+ }, 'initialize global state');
+
+promise_test(t => {
+ return frame.contentWindow.load_multipart_image('same-origin-multipart-image')
+ .then(img => frame.contentWindow.get_image_data(img))
+ .then(img_data => {
+ check_image_data(img_data.data);
+ });
+ }, 'same-origin multipart image via SW should be readable');
+
+promise_test(t => {
+ return frame.contentWindow.load_multipart_image('cross-origin-multipart-image-with-cors-approved')
+ .then(img => frame.contentWindow.get_image_data(img))
+ .then(img_data => {
+ check_image_data(img_data.data);
+ });
+ }, 'cross-origin multipart image via SW with approved CORS should be readable');
+
+promise_test(t => {
+ return frame.contentWindow.load_multipart_image('cross-origin-multipart-image-with-no-cors')
+ .then(img => {
+ assert_throws_dom('SecurityError', frame.contentWindow.DOMException,
+ () => frame.contentWindow.get_image_data(img));
+ });
+ }, 'cross-origin multipart image with no-cors via SW should not be readable');
+
+promise_test(t => {
+ const promise = frame.contentWindow.load_multipart_image('cross-origin-multipart-image-with-cors-rejected');
+ return promise_rejects_dom(t, 'NetworkError', frame.contentWindow.DOMException, promise);
+ }, 'cross-origin multipart image via SW with rejected CORS should fail to load');
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/multiple-register.https.html b/test/wpt/tests/service-workers/service-worker/multiple-register.https.html
new file mode 100644
index 0000000..752e132
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/multiple-register.https.html
@@ -0,0 +1,117 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+var worker_url = 'resources/empty-worker.js';
+
+async_test(function(t) {
+ var scope = 'resources/scope/subsequent-register-from-same-window';
+ var registration;
+
+ service_worker_unregister_and_register(t, worker_url, scope)
+ .then(function(r) {
+ registration = r;
+ return wait_for_state(t, r.installing, 'activated');
+ })
+ .then(function() {
+ return navigator.serviceWorker.register(worker_url, { scope: scope });
+ })
+ .then(function(new_registration) {
+ assert_equals(new_registration, registration,
+ 'register should resolve to the same registration');
+ assert_equals(new_registration.active, registration.active,
+ 'register should resolve to the same worker');
+ assert_equals(new_registration.active.state, 'activated',
+ 'the worker should be in state "activated"');
+ return registration.unregister();
+ })
+ .then(function() { t.done(); })
+ .catch(unreached_rejection(t));
+}, 'Subsequent registrations resolve to the same registration object');
+
+async_test(function(t) {
+ var scope = 'resources/scope/subsequent-register-from-different-iframe';
+ var frame;
+ var registration;
+
+ service_worker_unregister_and_register(t, worker_url, scope)
+ .then(function(r) {
+ registration = r;
+ return wait_for_state(t, r.installing, 'activated');
+ })
+ .then(function() { return with_iframe('resources/404.py'); })
+ .then(function(f) {
+ frame = f;
+ return frame.contentWindow.navigator.serviceWorker.register(
+ 'empty-worker.js',
+ { scope: 'scope/subsequent-register-from-different-iframe' });
+ })
+ .then(function(new_registration) {
+ assert_not_equals(
+ registration, new_registration,
+ 'register should resolve to a different registration');
+ assert_equals(
+ registration.scope, new_registration.scope,
+ 'registrations should have the same scope');
+
+ assert_equals(
+ registration.installing, null,
+ 'installing worker should be null');
+ assert_equals(
+ new_registration.installing, null,
+ 'installing worker should be null');
+ assert_equals(
+ registration.waiting, null,
+ 'waiting worker should be null')
+ assert_equals(
+ new_registration.waiting, null,
+ 'waiting worker should be null')
+
+ assert_not_equals(
+ registration.active, new_registration.active,
+ 'registration should have a different active worker');
+ assert_equals(
+ registration.active.scriptURL,
+ new_registration.active.scriptURL,
+ 'active workers should have the same script URL');
+ assert_equals(
+ registration.active.state,
+ new_registration.active.state,
+ 'active workers should be in the same state');
+
+ frame.remove();
+ return registration.unregister();
+ })
+ .then(function() { t.done(); })
+ .catch(unreached_rejection(t));
+}, 'Subsequent registrations from a different iframe resolve to the ' +
+ 'different registration object but they refer to the same ' +
+ 'registration and workers');
+
+async_test(function(t) {
+ var scope = 'resources/scope/concurrent-register';
+
+ service_worker_unregister(t, scope)
+ .then(function() {
+ var promises = [];
+ for (var i = 0; i < 10; ++i) {
+ promises.push(navigator.serviceWorker.register(worker_url,
+ { scope: scope }));
+ }
+ return Promise.all(promises);
+ })
+ .then(function(registrations) {
+ registrations.forEach(function(registration) {
+ assert_equals(registration, registrations[0],
+ 'register should resolve to the same registration');
+ });
+ return registrations[0].unregister();
+ })
+ .then(function() { t.done(); })
+ .catch(unreached_rejection(t));
+}, 'Concurrent registrations resolve to the same registration object');
+
+</script>
+</body>
diff --git a/test/wpt/tests/service-workers/service-worker/multiple-update.https.html b/test/wpt/tests/service-workers/service-worker/multiple-update.https.html
new file mode 100644
index 0000000..6a83f73
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/multiple-update.https.html
@@ -0,0 +1,94 @@
+<!DOCTYPE html>
+<!-- In Bug 1217367, we will try to merge update events for same registration
+ if possible. This testcase is used to make sure the optimization algorithm
+ doesn't go wrong. -->
+<title>Service Worker: Trigger multiple updates</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+promise_test(function(t) {
+ var script = 'resources/update-nocookie-worker.py';
+ var scope = 'resources/scope/update';
+ var expected_url = normalizeURL(script);
+ var registration;
+
+ return service_worker_unregister_and_register(t, expected_url, scope)
+ .then(function(r) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ registration = r;
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() {
+ // Test single update works before triggering multiple update events
+ return Promise.all([registration.update(),
+ wait_for_update(t, registration)]);
+ })
+ .then(function() {
+ assert_equals(registration.installing.scriptURL, expected_url,
+ 'new installing should be set after update resolves.');
+ assert_equals(registration.waiting, null,
+ 'waiting should still be null after update resolves.');
+ assert_equals(registration.active.scriptURL, expected_url,
+ 'active should still exist after update found.');
+ return wait_for_state(t, registration.installing, 'installed');
+ })
+ .then(function() {
+ assert_equals(registration.installing, null,
+ 'installing should be null after installing.');
+ if (registration.waiting) {
+ assert_equals(registration.waiting.scriptURL, expected_url,
+ 'waiting should be set after installing.');
+ assert_equals(registration.active.scriptURL, expected_url,
+ 'active should still exist after installing.');
+ return wait_for_state(t, registration.waiting, 'activated');
+ }
+ })
+ .then(function() {
+ // Test triggering multiple update events at the same time.
+ var promiseList = [];
+ const burstUpdateCount = 10;
+ for (var i = 0; i < burstUpdateCount; i++) {
+ promiseList.push(registration.update());
+ }
+ promiseList.push(wait_for_update(t, registration));
+ return Promise.all(promiseList);
+ })
+ .then(function() {
+ assert_equals(registration.installing.scriptURL, expected_url,
+ 'new installing should be set after update resolves.');
+ assert_equals(registration.waiting, null,
+ 'waiting should still be null after update resolves.');
+ assert_equals(registration.active.scriptURL, expected_url,
+ 'active should still exist after update found.');
+ return wait_for_state(t, registration.installing, 'installed');
+ })
+ .then(function() {
+ assert_equals(registration.installing, null,
+ 'installing should be null after installing.');
+ if (registration.waiting) {
+ assert_equals(registration.waiting.scriptURL, expected_url,
+ 'waiting should be set after installing.');
+ assert_equals(registration.active.scriptURL, expected_url,
+ 'active should still exist after installing.');
+ return wait_for_state(t, registration.waiting, 'activated');
+ }
+ })
+ .then(function() {
+ // Test update still works after handling update event burst.
+ return Promise.all([registration.update(),
+ wait_for_update(t, registration)]);
+ })
+ .then(function() {
+ assert_equals(registration.installing.scriptURL, expected_url,
+ 'new installing should be set after update resolves.');
+ assert_equals(registration.waiting, null,
+ 'waiting should be null after activated.');
+ assert_equals(registration.active.scriptURL, expected_url,
+ 'active should still exist after update found.');
+ });
+ }, 'Trigger multiple updates.');
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/navigate-window.https.html b/test/wpt/tests/service-workers/service-worker/navigate-window.https.html
new file mode 100644
index 0000000..46d32a4
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/navigate-window.https.html
@@ -0,0 +1,151 @@
+<!DOCTYPE html>
+<title>Service Worker: Navigate a Window</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+var host_info = get_host_info();
+var BASE_URL = host_info['HTTPS_ORIGIN'] + base_path();
+
+function wait_for_message(msg) {
+ return new Promise(function(resolve, reject) {
+ window.addEventListener('message', function onMsg(evt) {
+ if (evt.data.type === msg) {
+ resolve();
+ }
+ });
+ });
+}
+
+function with_window(url) {
+ var win = window.open(url);
+ return wait_for_message('LOADED').then(_ => win);
+}
+
+function navigate_window(win, url) {
+ win.location = url;
+ return wait_for_message('LOADED').then(_ => win);
+}
+
+function reload_window(win) {
+ win.location.reload();
+ return wait_for_message('LOADED').then(_ => win);
+}
+
+function go_back(win) {
+ win.history.back();
+ return wait_for_message('PAGESHOW').then(_ => win);
+}
+
+function go_forward(win) {
+ win.history.forward();
+ return wait_for_message('PAGESHOW').then(_ => win);
+}
+
+function get_clients(win, sw, opts) {
+ return new Promise((resolve, reject) => {
+ win.navigator.serviceWorker.addEventListener('message', function onMsg(evt) {
+ win.navigator.serviceWorker.removeEventListener('message', onMsg);
+ if (evt.data.type === 'success') {
+ resolve(evt.data.detail);
+ } else {
+ reject(evt.data.detail);
+ }
+ });
+ sw.postMessage({ type: 'GET_CLIENTS', opts: (opts || {}) });
+ });
+}
+
+function compare_urls(a, b) {
+ return a.url < b.url ? -1 : b.url < a.url ? 1 : 0;
+}
+
+function validate_window(win, url, opts) {
+ return win.navigator.serviceWorker.getRegistration(url)
+ .then(reg => {
+ // In order to compare service worker instances we need to
+ // make sure the DOM object is owned by the same global; the
+ // opened window in this case.
+ assert_equals(win.navigator.serviceWorker.controller, reg.active,
+ 'window should be controlled by service worker');
+ return get_clients(win, reg.active, opts);
+ })
+ .then(resultList => {
+ // We should always see our controlled window.
+ var expected = [
+ { url: url, frameType: 'auxiliary' }
+ ];
+ // If we are including uncontrolled windows, then we might see the
+ // test window itself and the test harness.
+ if (opts.includeUncontrolled) {
+ expected.push({ url: BASE_URL + 'navigate-window.https.html',
+ frameType: 'auxiliary' });
+ expected.push({
+ url: host_info['HTTPS_ORIGIN'] + '/testharness_runner.html',
+ frameType: 'top-level' });
+ }
+
+ assert_equals(resultList.length, expected.length,
+ 'expected number of clients');
+
+ expected.sort(compare_urls);
+ resultList.sort(compare_urls);
+
+ for (var i = 0; i < resultList.length; ++i) {
+ assert_equals(resultList[i].url, expected[i].url,
+ 'client should have expected url');
+ assert_equals(resultList[i].frameType, expected[i].frameType,
+ 'client should have expected frame type');
+ }
+ return win;
+ })
+}
+
+promise_test(function(t) {
+ var worker = BASE_URL + 'resources/navigate-window-worker.js';
+ var scope = BASE_URL + 'resources/loaded.html?navigate-window-controlled';
+ var url1 = scope + '&q=1';
+ var url2 = scope + '&q=2';
+ return service_worker_unregister_and_register(t, worker, scope)
+ .then(reg => wait_for_state(t, reg.installing, 'activated') )
+ .then(___ => with_window(url1))
+ .then(win => validate_window(win, url1, { includeUncontrolled: false }))
+ .then(win => navigate_window(win, url2))
+ .then(win => validate_window(win, url2, { includeUncontrolled: false }))
+ .then(win => go_back(win))
+ .then(win => validate_window(win, url1, { includeUncontrolled: false }))
+ .then(win => go_forward(win))
+ .then(win => validate_window(win, url2, { includeUncontrolled: false }))
+ .then(win => reload_window(win))
+ .then(win => validate_window(win, url2, { includeUncontrolled: false }))
+ .then(win => win.close())
+ .catch(unreached_rejection(t))
+ .then(___ => service_worker_unregister(t, scope))
+ }, 'Clients.matchAll() should not show an old window as controlled after ' +
+ 'it navigates.');
+
+promise_test(function(t) {
+ var worker = BASE_URL + 'resources/navigate-window-worker.js';
+ var scope = BASE_URL + 'resources/loaded.html?navigate-window-uncontrolled';
+ var url1 = scope + '&q=1';
+ var url2 = scope + '&q=2';
+ return service_worker_unregister_and_register(t, worker, scope)
+ .then(reg => wait_for_state(t, reg.installing, 'activated') )
+ .then(___ => with_window(url1))
+ .then(win => validate_window(win, url1, { includeUncontrolled: true }))
+ .then(win => navigate_window(win, url2))
+ .then(win => validate_window(win, url2, { includeUncontrolled: true }))
+ .then(win => go_back(win))
+ .then(win => validate_window(win, url1, { includeUncontrolled: true }))
+ .then(win => go_forward(win))
+ .then(win => validate_window(win, url2, { includeUncontrolled: true }))
+ .then(win => reload_window(win))
+ .then(win => validate_window(win, url2, { includeUncontrolled: true }))
+ .then(win => win.close())
+ .catch(unreached_rejection(t))
+ .then(___ => service_worker_unregister(t, scope))
+ }, 'Clients.matchAll() should not show an old window after it navigates.');
+</script>
+</body>
diff --git a/test/wpt/tests/service-workers/service-worker/navigation-headers.https.html b/test/wpt/tests/service-workers/service-worker/navigation-headers.https.html
new file mode 100644
index 0000000..a4b5203
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/navigation-headers.https.html
@@ -0,0 +1,819 @@
+<!DOCTYPE html>
+<meta charset="utf-8"/>
+<meta name="timeout" content="long">
+<title>Service Worker: Navigation Post Request Origin Header</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script>
+<body>
+<script>
+'use strict';
+
+const script = new URL('./resources/fetch-rewrite-worker.js', self.location);
+const base = './resources/navigation-headers-server.py';
+const scope = base + '?with-sw';
+let registration;
+
+async function post_and_get_headers(t, form_host, method, swaction,
+ redirect_hosts=[]) {
+ if (swaction === 'navpreload') {
+ assert_true('navigationPreload' in registration,
+ 'navigation preload must be supported');
+ }
+ let target_string;
+ if (swaction === 'no-sw') {
+ target_string = base + '?no-sw';
+ } else if (swaction === 'fallback') {
+ target_string = `${scope}&ignore`;
+ } else {
+ target_string = `${scope}&${swaction}`;
+ }
+ let target = new URL(target_string, self.location);
+
+ for (let i = redirect_hosts.length - 1; i >= 0; --i) {
+ const redirect_url = new URL('./resources/redirect.py', self.location);
+ redirect_url.hostname = redirect_hosts[i];
+ redirect_url.search = `?Status=307&Redirect=${encodeURIComponent(target)}`;
+ target = redirect_url;
+ }
+
+ let popup_url_path;
+ if (method === 'GET') {
+ popup_url_path = './resources/location-setter.html';
+ } else if (method === 'POST') {
+ popup_url_path = './resources/form-poster.html';
+ }
+
+ const popup_url = new URL(popup_url_path, self.location);
+ popup_url.hostname = form_host;
+ popup_url.search = `?target=${encodeURIComponent(target.href)}`;
+
+ const message_promise = new Promise(resolve => {
+ self.addEventListener('message', evt => {
+ resolve(evt.data);
+ });
+ });
+
+ const frame = await with_iframe(popup_url);
+ t.add_cleanup(() => frame.remove());
+
+ return await message_promise;
+}
+
+const SAME_ORIGIN = new URL(self.location.origin);
+const SAME_SITE = new URL(get_host_info().HTTPS_REMOTE_ORIGIN);
+const CROSS_SITE = new URL(get_host_info().HTTPS_NOTSAMESITE_ORIGIN);
+
+promise_test(async t => {
+ registration = await service_worker_unregister_and_register(t, script, scope);
+ await wait_for_state(t, registration.installing, 'activated');
+ if (registration.navigationPreload)
+ await registration.navigationPreload.enable();
+}, 'Setup service worker');
+
+//
+// Origin and referer headers
+//
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET',
+ 'no-sw');
+ assert_equals(result.origin, 'not set', 'origin header');
+ assert_equals(result.referer, SAME_ORIGIN.href, 'referer header');
+}, 'GET Navigation, same-origin with no service worker sets correct ' +
+ 'origin and referer headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'POST',
+ 'no-sw');
+ assert_equals(result.origin, SAME_ORIGIN.origin, 'origin header');
+ assert_equals(result.referer, SAME_ORIGIN.href, 'referer header');
+}, 'POST Navigation, same-origin with no service worker sets correct ' +
+ 'origin and referer headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET',
+ 'passthrough');
+ assert_equals(result.origin, 'not set', 'origin header');
+ assert_equals(result.referer, SAME_ORIGIN.href, 'referer header');
+}, 'GET Navigation, same-origin with passthrough service worker sets correct ' +
+ 'origin and referer headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'POST',
+ 'passthrough');
+ assert_equals(result.origin, SAME_ORIGIN.origin, 'origin header');
+ assert_equals(result.referer, SAME_ORIGIN.href, 'referer header');
+}, 'POST Navigation, same-origin with passthrough service worker sets correct ' +
+ 'origin and referer headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET',
+ 'fallback');
+ assert_equals(result.origin, 'not set', 'origin header');
+ assert_equals(result.referer, SAME_ORIGIN.href, 'referer header');
+}, 'GET Navigation, same-origin with fallback service worker sets correct ' +
+ 'origin and referer headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'POST',
+ 'fallback');
+ assert_equals(result.origin, SAME_ORIGIN.origin, 'origin header');
+ assert_equals(result.referer, SAME_ORIGIN.href, 'referer header');
+}, 'POST Navigation, same-origin with fallback service worker sets correct ' +
+ 'origin and referer headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET',
+ 'navpreload');
+ assert_equals(result.origin, 'not set', 'origin header');
+ assert_equals(result.referer, SAME_ORIGIN.href, 'referer header');
+}, 'GET Navigation, same-origin with navpreload service worker sets correct ' +
+ 'origin and referer headers.');
+
+// There is no POST test for navpreload since the feature only supports GET
+// requests.
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET',
+ 'change-request');
+ assert_equals(result.origin, 'not set', 'origin header');
+ assert_equals(result.referer, script.href, 'referer header');
+}, 'GET Navigation, same-origin with service worker that changes the ' +
+ 'request sets correct origin and referer headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'POST',
+ 'change-request');
+ assert_equals(result.origin, SAME_ORIGIN.origin, 'origin header');
+ assert_equals(result.referer, script.href, 'referer header');
+}, 'POST Navigation, same-origin with service worker that changes the ' +
+ 'request sets correct origin and referer headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_SITE.hostname, 'GET',
+ 'no-sw');
+ assert_equals(result.origin, 'not set', 'origin header');
+ assert_equals(result.referer, SAME_SITE.href, 'referer header');
+}, 'GET Navigation, same-site with no service worker sets correct ' +
+ 'origin and referer headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_SITE.hostname, 'POST',
+ 'no-sw');
+ assert_equals(result.origin, SAME_SITE.origin, 'origin header');
+ assert_equals(result.referer, SAME_SITE.href, 'referer header');
+}, 'POST Navigation, same-site with no service worker sets correct ' +
+ 'origin and referer headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_SITE.hostname, 'GET',
+ 'passthrough');
+ assert_equals(result.origin, 'not set', 'origin header');
+ assert_equals(result.referer, SAME_SITE.href, 'referer header');
+}, 'GET Navigation, same-site with passthrough service worker sets correct ' +
+ 'origin and referer headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_SITE.hostname, 'POST',
+ 'passthrough');
+ assert_equals(result.origin, SAME_SITE.origin, 'origin header');
+ assert_equals(result.referer, SAME_SITE.href, 'referer header');
+}, 'POST Navigation, same-site with passthrough service worker sets correct ' +
+ 'origin and referer headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_SITE.hostname, 'GET',
+ 'fallback');
+ assert_equals(result.origin, 'not set', 'origin header');
+ assert_equals(result.referer, SAME_SITE.href, 'referer header');
+}, 'GET Navigation, same-site with fallback service worker sets correct ' +
+ 'origin and referer headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_SITE.hostname, 'POST',
+ 'fallback');
+ assert_equals(result.origin, SAME_SITE.origin, 'origin header');
+ assert_equals(result.referer, SAME_SITE.href, 'referer header');
+}, 'POST Navigation, same-site with fallback service worker sets correct ' +
+ 'origin and referer headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_SITE.hostname, 'GET',
+ 'navpreload');
+ assert_equals(result.origin, 'not set', 'origin header');
+ assert_equals(result.referer, SAME_SITE.href, 'referer header');
+}, 'GET Navigation, same-site with navpreload service worker sets correct ' +
+ 'origin and referer headers.');
+
+// There is no POST test for navpreload since the feature only supports GET
+// requests.
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_SITE.hostname, 'GET',
+ 'change-request');
+ assert_equals(result.origin, 'not set', 'origin header');
+ assert_equals(result.referer, script.href, 'referer header');
+}, 'GET Navigation, same-site with service worker that changes the ' +
+ 'request sets correct origin and referer headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_SITE.hostname, 'POST',
+ 'change-request');
+ assert_equals(result.origin, SAME_ORIGIN.origin, 'origin header');
+ assert_equals(result.referer, script.href, 'referer header');
+}, 'POST Navigation, same-site with service worker that changes the ' +
+ 'request sets correct origin and referer headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, CROSS_SITE.hostname, 'GET',
+ 'no-sw');
+ assert_equals(result.origin, 'not set', 'origin header');
+ assert_equals(result.referer, CROSS_SITE.href, 'referer header');
+}, 'GET Navigation, cross-site with no service worker sets correct ' +
+ 'origin and referer headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, CROSS_SITE.hostname, 'POST',
+ 'no-sw');
+ assert_equals(result.origin, CROSS_SITE.origin, 'origin header');
+ assert_equals(result.referer, CROSS_SITE.href, 'referer header');
+}, 'POST Navigation, cross-site with no service worker sets correct ' +
+ 'origin and referer headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, CROSS_SITE.hostname, 'GET',
+ 'passthrough');
+ assert_equals(result.origin, 'not set', 'origin header');
+ assert_equals(result.referer, CROSS_SITE.href, 'referer header');
+}, 'GET Navigation, cross-site with passthrough service worker sets correct ' +
+ 'origin and referer headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, CROSS_SITE.hostname, 'POST',
+ 'passthrough');
+ assert_equals(result.origin, CROSS_SITE.origin, 'origin header');
+ assert_equals(result.referer, CROSS_SITE.href, 'referer header');
+}, 'POST Navigation, cross-site with passthrough service worker sets correct ' +
+ 'origin and referer headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, CROSS_SITE.hostname, 'GET',
+ 'fallback');
+ assert_equals(result.origin, 'not set', 'origin header');
+ assert_equals(result.referer, CROSS_SITE.href, 'referer header');
+}, 'GET Navigation, cross-site with fallback service worker sets correct ' +
+ 'origin and referer headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, CROSS_SITE.hostname, 'POST',
+ 'fallback');
+ assert_equals(result.origin, CROSS_SITE.origin, 'origin header');
+ assert_equals(result.referer, CROSS_SITE.href, 'referer header');
+}, 'POST Navigation, cross-site with fallback service worker sets correct ' +
+ 'origin and referer headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, CROSS_SITE.hostname, 'GET',
+ 'navpreload');
+ assert_equals(result.origin, 'not set', 'origin header');
+ assert_equals(result.referer, CROSS_SITE.href, 'referer header');
+}, 'GET Navigation, cross-site with navpreload service worker sets correct ' +
+ 'origin and referer headers.');
+
+// There is no POST test for navpreload since the feature only supports GET
+// requests.
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, CROSS_SITE.hostname, 'GET',
+ 'change-request');
+ assert_equals(result.origin, 'not set', 'origin header');
+ assert_equals(result.referer, script.href, 'referer header');
+}, 'GET Navigation, cross-site with service worker that changes the ' +
+ 'request sets correct origin and referer headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, CROSS_SITE.hostname, 'POST',
+ 'change-request');
+ assert_equals(result.origin, SAME_ORIGIN.origin, 'origin header');
+ assert_equals(result.referer, script.href, 'referer header');
+}, 'POST Navigation, cross-site with service worker that changes the ' +
+ 'request sets correct origin and referer headers.');
+
+//
+// Origin and referer header tests using redirects
+//
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'POST',
+ 'no-sw', [SAME_SITE.hostname]);
+ assert_equals(result.origin, 'null', 'origin header');
+ assert_equals(result.referer, SAME_ORIGIN.href, 'referer header');
+}, 'POST Navigation, same-origin with same-site redirect and no service worker ' +
+ 'sets correct origin and referer headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'POST',
+ 'passthrough', [SAME_SITE.hostname]);
+ assert_equals(result.origin, 'null', 'origin header');
+ assert_equals(result.referer, SAME_ORIGIN.href, 'referer header');
+}, 'POST Navigation, same-origin with same-site redirect and passthrough service ' +
+ 'worker sets correct origin and referer headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'POST',
+ 'fallback', [SAME_SITE.hostname]);
+ assert_equals(result.origin, 'null', 'origin header');
+ assert_equals(result.referer, SAME_ORIGIN.href, 'referer header');
+}, 'POST Navigation, same-origin with same-site redirect and fallback service ' +
+ 'worker sets correct origin and referer headers.');
+
+// There is no navpreload case because it does not work with POST requests.
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'POST',
+ 'change-request', [SAME_SITE.hostname]);
+ assert_equals(result.origin, SAME_ORIGIN.origin, 'origin header');
+ assert_equals(result.referer, script.href, 'referer header');
+}, 'POST Navigation, same-origin with same-site redirect and change-request service ' +
+ 'worker sets correct origin and referer headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'POST',
+ 'no-sw', [CROSS_SITE.hostname]);
+ assert_equals(result.origin, 'null', 'origin header');
+ assert_equals(result.referer, SAME_ORIGIN.href, 'referer header');
+}, 'POST Navigation, same-origin with cross-site redirect and no service worker ' +
+ 'sets correct origin and referer headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'POST',
+ 'passthrough', [CROSS_SITE.hostname]);
+ assert_equals(result.origin, 'null', 'origin header');
+ assert_equals(result.referer, SAME_ORIGIN.href, 'referer header');
+}, 'POST Navigation, same-origin with cross-site redirect and passthrough service ' +
+ 'worker sets correct origin and referer headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'POST',
+ 'fallback', [CROSS_SITE.hostname]);
+ assert_equals(result.origin, 'null', 'origin header');
+ assert_equals(result.referer, SAME_ORIGIN.href, 'referer header');
+}, 'POST Navigation, same-origin with cross-site redirect and fallback service ' +
+ 'worker sets correct origin and referer headers.');
+
+// There is no navpreload case because it does not work with POST requests.
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'POST',
+ 'change-request', [CROSS_SITE.hostname]);
+ assert_equals(result.origin, SAME_ORIGIN.origin, 'origin header');
+ assert_equals(result.referer, script.href, 'referer header');
+}, 'POST Navigation, same-origin with cross-site redirect and change-request service ' +
+ 'worker sets correct origin and referer headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'POST',
+ 'no-sw', [CROSS_SITE.hostname,
+ SAME_ORIGIN.hostname]);
+ assert_equals(result.origin, 'null', 'origin header');
+ assert_equals(result.referer, SAME_ORIGIN.href, 'referer header');
+}, 'POST Navigation, same-origin with cross-site redirect, same-origin redirect, ' +
+ 'and no service worker sets correct origin and referer headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'POST',
+ 'passthrough', [CROSS_SITE.hostname,
+ SAME_ORIGIN.hostname]);
+ assert_equals(result.origin, 'null', 'origin header');
+ assert_equals(result.referer, SAME_ORIGIN.href, 'referer header');
+}, 'POST Navigation, same-origin with cross-site redirect, same-origin redirect, ' +
+ 'and passthrough service worker sets correct origin and referer headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'POST',
+ 'fallback', [CROSS_SITE.hostname,
+ SAME_ORIGIN.hostname]);
+ assert_equals(result.origin, 'null', 'origin header');
+ assert_equals(result.referer, SAME_ORIGIN.href, 'referer header');
+}, 'POST Navigation, same-origin with cross-site redirect, same-origin redirect, ' +
+ 'and fallback service worker sets correct origin and referer headers.');
+
+// There is no navpreload case because it does not work with POST requests.
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'POST',
+ 'change-request', [CROSS_SITE.hostname,
+ SAME_ORIGIN.hostname]);
+ assert_equals(result.origin, SAME_ORIGIN.origin, 'origin header');
+ assert_equals(result.referer, script.href, 'referer header');
+}, 'POST Navigation, same-origin with cross-site redirect, same-origin redirect, ' +
+ 'and change-request service worker sets correct origin and referer headers.');
+
+//
+// Sec-Fetch-* Headers (separated since not all browsers implement them)
+//
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET',
+ 'no-sw');
+ assert_equals(result['sec-fetch-site'], 'same-origin', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header');
+}, 'GET Navigation, same-origin with no service worker sets correct ' +
+ 'sec-fetch headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'POST',
+ 'no-sw');
+ assert_equals(result['sec-fetch-site'], 'same-origin', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header');
+}, 'POST Navigation, same-origin with no service worker sets correct ' +
+ 'sec-fetch headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET',
+ 'passthrough');
+ assert_equals(result['sec-fetch-site'], 'same-origin', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'empty', 'sec-fetch-dest header');
+}, 'GET Navigation, same-origin with passthrough service worker sets correct ' +
+ 'sec-fetch headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'POST',
+ 'passthrough');
+ assert_equals(result['sec-fetch-site'], 'same-origin', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'empty', 'sec-fetch-dest header');
+}, 'POST Navigation, same-origin with passthrough service worker sets correct ' +
+ 'sec-fetch headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET',
+ 'fallback');
+ assert_equals(result['sec-fetch-site'], 'same-origin', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header');
+}, 'GET Navigation, same-origin with fallback service worker sets correct ' +
+ 'sec-fetch headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'POST',
+ 'fallback');
+ assert_equals(result['sec-fetch-site'], 'same-origin', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header');
+}, 'POST Navigation, same-origin with fallback service worker sets correct ' +
+ 'sec-fetch headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET',
+ 'navpreload');
+ assert_equals(result['sec-fetch-site'], 'same-origin', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header');
+}, 'GET Navigation, same-origin with navpreload service worker sets correct ' +
+ 'sec-fetch headers.');
+
+// There is no POST test for navpreload since the feature only supports GET
+// requests.
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET',
+ 'change-request');
+ assert_equals(result['sec-fetch-site'], 'same-origin', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'same-origin', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'empty', 'sec-fetch-dest header');
+}, 'GET Navigation, same-origin with service worker that changes the ' +
+ 'request sets correct sec-fetch headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'POST',
+ 'change-request');
+ assert_equals(result['sec-fetch-site'], 'same-origin', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'same-origin', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'empty', 'sec-fetch-dest header');
+}, 'POST Navigation, same-origin with service worker that changes the ' +
+ 'request sets correct sec-fetch headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_SITE.hostname, 'GET',
+ 'no-sw');
+ assert_equals(result['sec-fetch-site'], 'same-site', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header');
+}, 'GET Navigation, same-site with no service worker sets correct ' +
+ 'sec-fetch headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_SITE.hostname, 'POST',
+ 'no-sw');
+ assert_equals(result['sec-fetch-site'], 'same-site', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header');
+}, 'POST Navigation, same-site with no service worker sets correct ' +
+ 'sec-fetch headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_SITE.hostname, 'GET',
+ 'passthrough');
+ assert_equals(result['sec-fetch-site'], 'same-site', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'empty', 'sec-fetch-dest header');
+}, 'GET Navigation, same-site with passthrough service worker sets correct ' +
+ 'sec-fetch headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_SITE.hostname, 'POST',
+ 'passthrough');
+ assert_equals(result['sec-fetch-site'], 'same-site', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'empty', 'sec-fetch-dest header');
+}, 'POST Navigation, same-site with passthrough service worker sets correct ' +
+ 'sec-fetch headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_SITE.hostname, 'GET',
+ 'fallback');
+ assert_equals(result['sec-fetch-site'], 'same-site', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header');
+}, 'GET Navigation, same-site with fallback service worker sets correct ' +
+ 'sec-fetch headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_SITE.hostname, 'POST',
+ 'fallback');
+ assert_equals(result['sec-fetch-site'], 'same-site', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header');
+}, 'POST Navigation, same-site with fallback service worker sets correct ' +
+ 'sec-fetch headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_SITE.hostname, 'GET',
+ 'navpreload');
+ assert_equals(result['sec-fetch-site'], 'same-site', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header');
+}, 'GET Navigation, same-site with navpreload service worker sets correct ' +
+ 'sec-fetch headers.');
+
+// There is no POST test for navpreload since the feature only supports GET
+// requests.
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_SITE.hostname, 'GET',
+ 'change-request');
+ assert_equals(result['sec-fetch-site'], 'same-origin', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'same-origin', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'empty', 'sec-fetch-dest header');
+}, 'GET Navigation, same-site with service worker that changes the ' +
+ 'request sets correct sec-fetch headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_SITE.hostname, 'POST',
+ 'change-request');
+ assert_equals(result['sec-fetch-site'], 'same-origin', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'same-origin', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'empty', 'sec-fetch-dest header');
+}, 'POST Navigation, same-site with service worker that changes the ' +
+ 'request sets correct sec-fetch headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, CROSS_SITE.hostname, 'GET',
+ 'no-sw');
+ assert_equals(result['sec-fetch-site'], 'cross-site', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header');
+}, 'GET Navigation, cross-site with no service worker sets correct ' +
+ 'sec-fetch headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, CROSS_SITE.hostname, 'POST',
+ 'no-sw');
+ assert_equals(result['sec-fetch-site'], 'cross-site', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header');
+}, 'POST Navigation, cross-site with no service worker sets correct ' +
+ 'sec-fetch headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, CROSS_SITE.hostname, 'GET',
+ 'passthrough');
+ assert_equals(result['sec-fetch-site'], 'cross-site', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'empty', 'sec-fetch-dest header');
+}, 'GET Navigation, cross-site with passthrough service worker sets correct ' +
+ 'sec-fetch headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, CROSS_SITE.hostname, 'POST',
+ 'passthrough');
+ assert_equals(result['sec-fetch-site'], 'cross-site', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'empty', 'sec-fetch-dest header');
+}, 'POST Navigation, cross-site with passthrough service worker sets correct ' +
+ 'sec-fetch headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, CROSS_SITE.hostname, 'GET',
+ 'fallback');
+ assert_equals(result['sec-fetch-site'], 'cross-site', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header');
+}, 'GET Navigation, cross-site with fallback service worker sets correct ' +
+ 'sec-fetch headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, CROSS_SITE.hostname, 'POST',
+ 'fallback');
+ assert_equals(result['sec-fetch-site'], 'cross-site', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header');
+}, 'POST Navigation, cross-site with fallback service worker sets correct ' +
+ 'sec-fetch headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, CROSS_SITE.hostname, 'GET',
+ 'navpreload');
+ assert_equals(result['sec-fetch-site'], 'cross-site', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header');
+}, 'GET Navigation, cross-site with navpreload service worker sets correct ' +
+ 'sec-fetch headers.');
+
+// There is no POST test for navpreload since the feature only supports GET
+// requests.
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, CROSS_SITE.hostname, 'GET',
+ 'change-request');
+ assert_equals(result['sec-fetch-site'], 'same-origin', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'same-origin', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'empty', 'sec-fetch-dest header');
+}, 'GET Navigation, cross-site with service worker that changes the ' +
+ 'request sets correct sec-fetch headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, CROSS_SITE.hostname, 'POST',
+ 'change-request');
+ assert_equals(result['sec-fetch-site'], 'same-origin', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'same-origin', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'empty', 'sec-fetch-dest header');
+}, 'POST Navigation, cross-site with service worker that changes the ' +
+ 'request sets correct sec-fetch headers.');
+
+//
+// Sec-Fetch-* header tests using redirects
+//
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET',
+ 'no-sw', [SAME_SITE.hostname]);
+ assert_equals(result['sec-fetch-site'], 'same-site', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header');
+}, 'GET Navigation, same-origin with same-site redirect and no service worker ' +
+ 'sets correct sec-fetch headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET',
+ 'passthrough', [SAME_SITE.hostname]);
+ assert_equals(result['sec-fetch-site'], 'same-site', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'empty', 'sec-fetch-dest header');
+}, 'GET Navigation, same-origin with same-site redirect and passthrough service ' +
+ 'worker sets correct sec-fetch headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET',
+ 'fallback', [SAME_SITE.hostname]);
+ assert_equals(result['sec-fetch-site'], 'same-site', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header');
+}, 'GET Navigation, same-origin with same-site redirect and fallback service ' +
+ 'worker sets correct sec-fetch headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET',
+ 'navpreload', [SAME_SITE.hostname]);
+ assert_equals(result['sec-fetch-site'], 'same-site', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header');
+}, 'GET Navigation, same-origin with same-site redirect and navpreload service ' +
+ 'worker sets correct sec-fetch headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET',
+ 'change-request', [SAME_SITE.hostname]);
+ assert_equals(result['sec-fetch-site'], 'same-origin', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'same-origin', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'empty', 'sec-fetch-dest header');
+}, 'GET Navigation, same-origin with same-site redirect and change-request service ' +
+ 'worker sets correct sec-fetch headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET',
+ 'no-sw', [CROSS_SITE.hostname]);
+ assert_equals(result['sec-fetch-site'], 'cross-site', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header');
+}, 'GET Navigation, same-origin with cross-site redirect and no service worker ' +
+ 'sets correct sec-fetch headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET',
+ 'passthrough', [CROSS_SITE.hostname]);
+ assert_equals(result['sec-fetch-site'], 'cross-site', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'empty', 'sec-fetch-dest header');
+}, 'GET Navigation, same-origin with cross-site redirect and passthrough service ' +
+ 'worker sets correct sec-fetch headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET',
+ 'fallback', [CROSS_SITE.hostname]);
+ assert_equals(result['sec-fetch-site'], 'cross-site', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header');
+}, 'GET Navigation, same-origin with cross-site redirect and fallback service ' +
+ 'worker sets correct sec-fetch headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET',
+ 'navpreload', [CROSS_SITE.hostname]);
+ assert_equals(result['sec-fetch-site'], 'cross-site', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header');
+}, 'GET Navigation, same-origin with cross-site redirect and navpreload service ' +
+ 'worker sets correct sec-fetch headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET',
+ 'change-request', [CROSS_SITE.hostname]);
+ assert_equals(result['sec-fetch-site'], 'same-origin', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'same-origin', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'empty', 'sec-fetch-dest header');
+}, 'GET Navigation, same-origin with cross-site redirect and change-request service ' +
+ 'worker sets correct sec-fetch headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET',
+ 'no-sw', [CROSS_SITE.hostname,
+ SAME_ORIGIN.hostname]);
+ assert_equals(result['sec-fetch-site'], 'cross-site', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header');
+}, 'GET Navigation, same-origin with cross-site redirect, same-origin redirect, ' +
+ 'and no service worker sets correct sec-fetch headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET',
+ 'passthrough', [CROSS_SITE.hostname,
+ SAME_ORIGIN.hostname]);
+ assert_equals(result['sec-fetch-site'], 'cross-site', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'empty', 'sec-fetch-dest header');
+}, 'GET Navigation, same-origin with cross-site redirect, same-origin redirect, ' +
+ 'and passthrough service worker sets correct sec-fetch headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET',
+ 'fallback', [CROSS_SITE.hostname,
+ SAME_ORIGIN.hostname]);
+ assert_equals(result['sec-fetch-site'], 'cross-site', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header');
+}, 'GET Navigation, same-origin with cross-site redirect, same-origin redirect, ' +
+ 'and fallback service worker sets correct sec-fetch headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET',
+ 'navpreload', [CROSS_SITE.hostname,
+ SAME_ORIGIN.hostname]);
+ assert_equals(result['sec-fetch-site'], 'cross-site', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header');
+}, 'GET Navigation, same-origin with cross-site redirect, same-origin redirect, ' +
+ 'and navpreload service worker sets correct sec-fetch headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET',
+ 'change-request', [CROSS_SITE.hostname,
+ SAME_ORIGIN.hostname]);
+ assert_equals(result['sec-fetch-site'], 'same-origin', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'same-origin', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'empty', 'sec-fetch-dest header');
+}, 'GET Navigation, same-origin with cross-site redirect, same-origin redirect, ' +
+ 'and change-request service worker sets correct sec-fetch headers.');
+
+promise_test(async t => {
+ await registration.unregister();
+}, 'Cleanup service worker');
+
+</script>
+</body>
diff --git a/test/wpt/tests/service-workers/service-worker/navigation-preload/broken-chunked-encoding.https.html b/test/wpt/tests/service-workers/service-worker/navigation-preload/broken-chunked-encoding.https.html
new file mode 100644
index 0000000..ec74282
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/navigation-preload/broken-chunked-encoding.https.html
@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Navigation Preload with chunked encoding</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/test-helpers.sub.js"></script>
+<script>
+promise_test(t => {
+ var script = 'resources/broken-chunked-encoding-worker.js';
+ var scope = 'resources/broken-chunked-encoding-scope.asis';
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(registration => {
+ add_completion_callback(_ => registration.unregister());
+ var worker = registration.installing;
+ return wait_for_state(t, worker, 'activated');
+ })
+ .then(_ => with_iframe(scope))
+ .then(frame => {
+ assert_equals(
+ frame.contentDocument.body.textContent,
+ 'PASS: preloadResponse resolved');
+ });
+ }, 'FetchEvent#preloadResponse resolves even if the body is sent with broken chunked encoding.');
+
+promise_test(t => {
+ var script = 'resources/broken-chunked-encoding-worker.js';
+ var scope = 'resources/chunked-encoding-scope.py?use_broken_body';
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(registration => {
+ add_completion_callback(_ => registration.unregister());
+ var worker = registration.installing;
+ return wait_for_state(t, worker, 'activated');
+ })
+ .then(_ => with_iframe(scope))
+ .then(frame => {
+ assert_equals(
+ frame.contentDocument.body.textContent,
+ 'PASS: preloadResponse resolved');
+ });
+ }, 'FetchEvent#preloadResponse resolves even if the body is sent with broken chunked encoding with some delays');
+
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/navigation-preload/chunked-encoding.https.html b/test/wpt/tests/service-workers/service-worker/navigation-preload/chunked-encoding.https.html
new file mode 100644
index 0000000..830ce32
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/navigation-preload/chunked-encoding.https.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Navigation Preload with chunked encoding</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/test-helpers.sub.js"></script>
+<script>
+promise_test(t => {
+ var script = 'resources/chunked-encoding-worker.js';
+ var scope = 'resources/chunked-encoding-scope.py';
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(registration => {
+ add_completion_callback(_ => registration.unregister());
+ var worker = registration.installing;
+ return wait_for_state(t, worker, 'activated');
+ })
+ .then(_ => with_iframe(scope))
+ .then(frame => {
+ assert_equals(
+ frame.contentDocument.body.textContent,
+ '0123456789');
+ });
+ }, 'Navigation Preload must work with chunked encoding.');
+
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/navigation-preload/empty-preload-response-body.https.html b/test/wpt/tests/service-workers/service-worker/navigation-preload/empty-preload-response-body.https.html
new file mode 100644
index 0000000..7e8aacd
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/navigation-preload/empty-preload-response-body.https.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Navigation Preload empty response body</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/test-helpers.sub.js"></script>
+<script>
+promise_test(t => {
+ var script = 'resources/empty-preload-response-body-worker.js';
+ var scope = 'resources/empty-preload-response-body-scope.html';
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(registration => {
+ add_completion_callback(_ => registration.unregister());
+ var worker = registration.installing;
+ return wait_for_state(t, worker, 'activated');
+ })
+ .then(_ => with_iframe(scope))
+ .then(frame => {
+ assert_equals(
+ frame.contentDocument.body.textContent,
+ '[]');
+ });
+ }, 'Navigation Preload empty response body.');
+
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/navigation-preload/get-state.https.html b/test/wpt/tests/service-workers/service-worker/navigation-preload/get-state.https.html
new file mode 100644
index 0000000..08e2f49
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/navigation-preload/get-state.https.html
@@ -0,0 +1,217 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>NavigationPreloadManager.getState</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/test-helpers.sub.js"></script>
+<script src="resources/helpers.js"></script>
+<body>
+<script>
+function post_and_wait_for_reply(worker, message) {
+ return new Promise(resolve => {
+ navigator.serviceWorker.onmessage = e => { resolve(e.data); };
+ worker.postMessage(message);
+ });
+}
+
+promise_test(t => {
+ const scope = '../resources/get-state';
+ const script = '../resources/empty-worker.js';
+ var np;
+
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(r => {
+ np = r.navigationPreload;
+ add_completion_callback(() => r.unregister());
+ return wait_for_state(t, r.installing, 'activated');
+ })
+ .then(() => np.getState())
+ .then(state => {
+ expect_navigation_preload_state(state, false, 'true', 'default state');
+ return np.enable();
+ })
+ .then(result => {
+ assert_equals(result, undefined,
+ 'enable() should resolve to undefined');
+ return np.getState();
+ })
+ .then(state => {
+ expect_navigation_preload_state(state, true, 'true',
+ 'state after enable()');
+ return np.disable();
+ })
+ .then(result => {
+ assert_equals(result, undefined,
+ 'disable() should resolve to undefined');
+ return np.getState();
+ })
+ .then(state => {
+ expect_navigation_preload_state(state, false, 'true',
+ 'state after disable()');
+ return np.setHeaderValue('dreams that cannot be');
+ })
+ .then(result => {
+ assert_equals(result, undefined,
+ 'setHeaderValue() should resolve to undefined');
+ return np.getState();
+ })
+ .then(state => {
+ expect_navigation_preload_state(state, false, 'dreams that cannot be',
+ 'state after setHeaderValue()');
+ return np.setHeaderValue('').then(() => np.getState());
+ })
+ .then(state => {
+ expect_navigation_preload_state(state, false, '',
+ 'after setHeaderValue to empty string');
+ return np.setHeaderValue(null).then(() => np.getState());
+ })
+ .then(state => {
+ expect_navigation_preload_state(state, false, 'null',
+ 'after setHeaderValue to null');
+ return promise_rejects_js(t,
+ TypeError,
+ np.setHeaderValue('what\uDC00\uD800this'),
+ 'setHeaderValue() should throw if passed surrogates');
+ })
+ .then(() => {
+ return promise_rejects_js(t,
+ TypeError,
+ np.setHeaderValue('zer\0o'),
+ 'setHeaderValue() should throw if passed \\0');
+ })
+ .then(() => {
+ return promise_rejects_js(t,
+ TypeError,
+ np.setHeaderValue('\rcarriage'),
+ 'setHeaderValue() should throw if passed \\r');
+ })
+ .then(() => {
+ return promise_rejects_js(t,
+ TypeError,
+ np.setHeaderValue('newline\n'),
+ 'setHeaderValue() should throw if passed \\n');
+ })
+ .then(() => {
+ return promise_rejects_js(t,
+ TypeError,
+ np.setHeaderValue(),
+ 'setHeaderValue() should throw if passed undefined');
+ })
+ .then(() => np.enable().then(() => np.getState()))
+ .then(state => {
+ expect_navigation_preload_state(state, true, 'null',
+ 'enable() should not change header');
+ });
+ }, 'getState');
+
+// This test sends commands to a worker to call enable()/disable()/getState().
+// It checks the results from the worker and verifies that they match the
+// navigation preload state accessible from the page.
+promise_test(t => {
+ const scope = 'resources/get-state-worker';
+ const script = 'resources/get-state-worker.js';
+ var worker;
+ var registration;
+
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(r => {
+ registration = r;
+ add_completion_callback(() => registration.unregister());
+ worker = registration.installing;
+ return wait_for_state(t, worker, 'activated');
+ })
+ .then(() => {
+ // Call getState().
+ return post_and_wait_for_reply(worker, 'getState');
+ })
+ .then(data => {
+ return Promise.all([data, registration.navigationPreload.getState()]);
+ })
+ .then(states => {
+ expect_navigation_preload_state(states[0], false, 'true',
+ 'default state (from worker)');
+ expect_navigation_preload_state(states[1], false, 'true',
+ 'default state (from page)');
+ // Call enable() and then getState().
+ return post_and_wait_for_reply(worker, 'enable');
+ })
+ .then(data => {
+ assert_equals(data, undefined, 'enable() should resolve to undefined');
+ return Promise.all([
+ post_and_wait_for_reply(worker, 'getState'),
+ registration.navigationPreload.getState()
+ ]);
+ })
+ .then(states => {
+ expect_navigation_preload_state(states[0], true, 'true',
+ 'state after enable() (from worker)');
+ expect_navigation_preload_state(states[1], true, 'true',
+ 'state after enable() (from page)');
+ // Call disable() and then getState().
+ return post_and_wait_for_reply(worker, 'disable');
+ })
+ .then(data => {
+ assert_equals(data, undefined,
+ '.disable() should resolve to undefined');
+ return Promise.all([
+ post_and_wait_for_reply(worker, 'getState'),
+ registration.navigationPreload.getState()
+ ]);
+ })
+ .then(states => {
+ expect_navigation_preload_state(states[0], false, 'true',
+ 'state after disable() (from worker)');
+ expect_navigation_preload_state(states[1], false, 'true',
+ 'state after disable() (from page)');
+ return post_and_wait_for_reply(worker, 'setHeaderValue');
+ })
+ .then(data => {
+ assert_equals(data, undefined,
+ '.setHeaderValue() should resolve to undefined');
+ return Promise.all([
+ post_and_wait_for_reply(worker, 'getState'),
+ registration.navigationPreload.getState()]);
+ })
+ .then(states => {
+ expect_navigation_preload_state(
+ states[0], false, 'insightful',
+ 'state after setHeaderValue() (from worker)');
+ expect_navigation_preload_state(
+ states[1], false, 'insightful',
+ 'state after setHeaderValue() (from page)');
+ });
+ }, 'getState from a worker');
+
+// This tests navigation preload API when there is no active worker. It calls
+// the API from the main page and then from the worker itself.
+promise_test(t => {
+ const scope = 'resources/wait-for-activate-worker';
+ const script = 'resources/wait-for-activate-worker.js';
+ var registration;
+ var np;
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(r => {
+ registration = r;
+ np = registration.navigationPreload;
+ add_completion_callback(() => registration.unregister());
+ return Promise.all([
+ promise_rejects_dom(
+ t, 'InvalidStateError', np.enable(),
+ 'enable should reject if there is no active worker'),
+ promise_rejects_dom(
+ t, 'InvalidStateError', np.disable(),
+ 'disable should reject if there is no active worker'),
+ promise_rejects_dom(
+ t, 'InvalidStateError', np.setHeaderValue('umm'),
+ 'setHeaderValue should reject if there is no active worker')]);
+ })
+ .then(() => np.getState())
+ .then(state => {
+ expect_navigation_preload_state(state, false, 'true',
+ 'state before activation');
+ return post_and_wait_for_reply(registration.installing, 'ping');
+ })
+ .then(result => assert_equals(result, 'PASS'));
+ }, 'no active worker');
+</script>
+</body>
diff --git a/test/wpt/tests/service-workers/service-worker/navigation-preload/navigationPreload.https.html b/test/wpt/tests/service-workers/service-worker/navigation-preload/navigationPreload.https.html
new file mode 100644
index 0000000..392e5c1
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/navigation-preload/navigationPreload.https.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>ServiceWorker: navigator.serviceWorker.navigationPreload</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/test-helpers.sub.js"></script>
+<script src="resources/helpers.js"></script>
+<script>
+promise_test(async t => {
+ const SCRIPT = '../resources/empty-worker.js';
+ const SCOPE = '../resources/navigationpreload';
+ const registration =
+ await service_worker_unregister_and_register(t, SCRIPT, SCOPE);
+ const navigationPreload = registration.navigationPreload;
+ assert_true(navigationPreload instanceof NavigationPreloadManager,
+ 'ServiceWorkerRegistration.navigationPreload');
+ await registration.unregister();
+}, "The navigationPreload attribute must return service worker " +
+ "registration's NavigationPreloadManager object.");
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/navigation-preload/redirect.https.html b/test/wpt/tests/service-workers/service-worker/navigation-preload/redirect.https.html
new file mode 100644
index 0000000..5970f05
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/navigation-preload/redirect.https.html
@@ -0,0 +1,93 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Navigation Preload redirect response</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/test-helpers.sub.js"></script>
+<script>
+
+function check_opaqueredirect(response_info, scope) {
+ assert_equals(response_info.type, 'opaqueredirect');
+ assert_equals(response_info.url, '' + new URL(scope, location));
+ assert_equals(response_info.status, 0);
+ assert_equals(response_info.ok, false);
+ assert_equals(response_info.statusText, '');
+ assert_equals(response_info.headers.length, 0);
+}
+
+function redirect_response_test(t, scope, expected_body, expected_urls) {
+ var script = 'resources/redirect-worker.js';
+ var registration;
+ var message_resolvers = [];
+ function wait_for_message(count) {
+ var promises = [];
+ message_resolvers = [];
+ for (var i = 0; i < count; ++i) {
+ promises.push(new Promise(resolve => message_resolvers.push(resolve)));
+ }
+ return promises;
+ }
+ function on_message(e) {
+ var resolve = message_resolvers.shift();
+ if (resolve)
+ resolve(e.data);
+ }
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(reg => {
+ registration = reg;
+ add_completion_callback(_ => registration.unregister());
+ var worker = registration.installing;
+ return wait_for_state(t, worker, 'activated');
+ })
+ .then(_ => with_iframe(scope + '&base'))
+ .then(frame => {
+ assert_equals(frame.contentDocument.body.textContent, 'OK');
+ frame.contentWindow.navigator.serviceWorker.onmessage = on_message;
+ return Promise.all(wait_for_message(expected_urls.length)
+ .concat(with_iframe(scope)));
+ })
+ .then(results => {
+ var frame = results[expected_urls.length];
+ assert_equals(frame.contentDocument.body.textContent, expected_body);
+ for (var i = 0; i < expected_urls.length; ++i) {
+ check_opaqueredirect(results[i], expected_urls[i]);
+ }
+ frame.remove();
+ return registration.unregister();
+ });
+}
+
+promise_test(t => {
+ return redirect_response_test(
+ t,
+ 'resources/redirect-scope.py?type=normal',
+ 'redirected\n',
+ ['resources/redirect-scope.py?type=normal']);
+ }, 'Navigation Preload redirect response.');
+
+promise_test(t => {
+ return redirect_response_test(
+ t,
+ 'resources/redirect-scope.py?type=no-location',
+ '',
+ ['resources/redirect-scope.py?type=no-location']);
+ }, 'Navigation Preload no-location redirect response.');
+
+promise_test(t => {
+ return redirect_response_test(
+ t,
+ 'resources/redirect-scope.py?type=no-location-with-body',
+ 'BODY',
+ ['resources/redirect-scope.py?type=no-location-with-body']);
+ }, 'Navigation Preload no-location redirect response with body.');
+
+promise_test(t => {
+ return redirect_response_test(
+ t,
+ 'resources/redirect-scope.py?type=redirect-to-scope',
+ 'redirected\n',
+ ['resources/redirect-scope.py?type=redirect-to-scope',
+ 'resources/redirect-scope.py?type=redirect-to-scope2',
+ 'resources/redirect-scope.py?type=redirect-to-scope3',]);
+ }, 'Navigation Preload redirect to the same scope.');
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/navigation-preload/request-headers.https.html b/test/wpt/tests/service-workers/service-worker/navigation-preload/request-headers.https.html
new file mode 100644
index 0000000..0964201
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/navigation-preload/request-headers.https.html
@@ -0,0 +1,41 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Navigation Preload request headers</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/test-helpers.sub.js"></script>
+<script>
+promise_test(t => {
+ var script = 'resources/request-headers-worker.js';
+ var scope = 'resources/request-headers-scope.py';
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(registration => {
+ add_completion_callback(_ => registration.unregister());
+ var worker = registration.installing;
+ return wait_for_state(t, worker, 'activated');
+ })
+ .then(_ => with_iframe(scope))
+ .then(frame => {
+ var headers = JSON.parse(frame.contentDocument.body.textContent);
+ assert_true(
+ 'SERVICE-WORKER-NAVIGATION-PRELOAD' in headers,
+ 'The Navigation Preload request must specify a ' +
+ '"Service-Worker-Navigation-Preload" header.');
+ assert_array_equals(
+ headers['SERVICE-WORKER-NAVIGATION-PRELOAD'],
+ ['hello'],
+ 'The Navigation Preload request must specify the correct value ' +
+ 'for the "Service-Worker-Navigation-Preload" header.');
+ assert_true(
+ 'UPGRADE-INSECURE-REQUESTS' in headers,
+ 'The Navigation Preload request must specify an ' +
+ '"Upgrade-Insecure-Requests" header.');
+ assert_array_equals(
+ headers['UPGRADE-INSECURE-REQUESTS'],
+ ['1'],
+ 'The Navigation Preload request must specify the correct value ' +
+ 'for the "Upgrade-Insecure-Requests" header.');
+ });
+ }, 'Navigation Preload request headers.');
+
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/navigation-preload/resource-timing.https.html b/test/wpt/tests/service-workers/service-worker/navigation-preload/resource-timing.https.html
new file mode 100644
index 0000000..468a1f5
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/navigation-preload/resource-timing.https.html
@@ -0,0 +1,92 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Navigation Preload Resource Timing</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/test-helpers.sub.js"></script>
+<script>
+
+function check_timing_entry(entry, url, decodedBodySize, encodedBodySize) {
+ assert_equals(entry.name, url, 'The entry name of '+ url);
+
+ assert_equals(
+ entry.entryType, 'resource',
+ 'The entryType of preload response timing entry must be "resource' +
+ '" :' + url);
+ assert_equals(
+ entry.initiatorType, 'navigation',
+ 'The initiatorType of preload response timing entry must be ' +
+ '"navigation":' + url);
+
+ // If the server returns the redirect response, |decodedBodySize| is null and
+ // |entry.decodedBodySize| should be 0. Otherwise |entry.decodedBodySize| must
+ // same as |decodedBodySize|
+ assert_equals(
+ entry.decodedBodySize, Number(decodedBodySize),
+ 'decodedBodySize must same as the decoded size in the server:' + url);
+
+ // If the server returns the redirect response, |encodedBodySize| is null and
+ // |entry.encodedBodySize| should be 0. Otherwise |entry.encodedBodySize| must
+ // same as |encodedBodySize|
+ assert_equals(
+ entry.encodedBodySize, Number(encodedBodySize),
+ 'encodedBodySize must same as the encoded size in the server:' + url);
+
+ assert_greater_than(
+ entry.transferSize, entry.decodedBodySize,
+ 'transferSize must greater then encodedBodySize.');
+
+ assert_greater_than(entry.startTime, 0, 'startTime of ' + url);
+ assert_greater_than_equal(entry.fetchStart, entry.startTime,
+ 'fetchStart >= startTime of ' + url);
+ assert_greater_than_equal(entry.domainLookupStart, entry.fetchStart,
+ 'domainLookupStart >= fetchStart of ' + url);
+ assert_greater_than_equal(entry.domainLookupEnd, entry.domainLookupStart,
+ 'domainLookupEnd >= domainLookupStart of ' + url);
+ assert_greater_than_equal(entry.connectStart, entry.domainLookupEnd,
+ 'connectStart >= domainLookupEnd of ' + url);
+ assert_greater_than_equal(entry.connectEnd, entry.connectStart,
+ 'connectEnd >= connectStart of ' + url);
+ assert_greater_than_equal(entry.requestStart, entry.connectEnd,
+ 'requestStart >= connectEnd of ' + url);
+ assert_greater_than_equal(entry.responseStart, entry.requestStart,
+ 'domainLookupStart >= requestStart of ' + url);
+ assert_greater_than_equal(entry.responseEnd, entry.responseStart,
+ 'responseEnd >= responseStart of ' + url);
+ assert_greater_than(entry.duration, 0, 'duration of ' + url);
+}
+
+promise_test(t => {
+ var script = 'resources/resource-timing-worker.js';
+ var scope = 'resources/resource-timing-scope.py';
+ var registration;
+ var frames = [];
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(reg => {
+ registration = reg;
+ add_completion_callback(_ => registration.unregister());
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(_ => with_iframe(scope + '?type=normal'))
+ .then(frame => {
+ frames.push(frame);
+ return with_iframe(scope + '?type=redirect');
+ })
+ .then(frame => {
+ frames.push(frame);
+ frames.forEach(frame => {
+ var result = JSON.parse(frame.contentDocument.body.textContent);
+ assert_equals(
+ result.timingEntries.length, 1,
+ 'performance.getEntriesByName() must returns one ' +
+ 'PerformanceResourceTiming entry for the navigation preload.');
+ var entry = result.timingEntries[0];
+ check_timing_entry(entry, frame.src, result.decodedBodySize,
+ result.encodedBodySize);
+ frame.remove();
+ });
+ return registration.unregister();
+ });
+ }, 'Navigation Preload Resource Timing.');
+
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/broken-chunked-encoding-scope.asis b/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/broken-chunked-encoding-scope.asis
new file mode 100644
index 0000000..2a71953
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/broken-chunked-encoding-scope.asis
@@ -0,0 +1,6 @@
+HTTP/1.1 200 OK
+Content-type: text/html; charset=UTF-8
+Transfer-encoding: chunked
+
+hello
+world
diff --git a/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/broken-chunked-encoding-worker.js b/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/broken-chunked-encoding-worker.js
new file mode 100644
index 0000000..7a453e4
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/broken-chunked-encoding-worker.js
@@ -0,0 +1,11 @@
+self.addEventListener('activate', event => {
+ event.waitUntil(
+ self.registration.navigationPreload.enable());
+ });
+
+self.addEventListener('fetch', event => {
+ event.respondWith(event.preloadResponse
+ .then(
+ _ => new Response('PASS: preloadResponse resolved'),
+ _ => new Response('FAIL: preloadResponse rejected')));
+ });
diff --git a/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/chunked-encoding-scope.py b/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/chunked-encoding-scope.py
new file mode 100644
index 0000000..659c4d8
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/chunked-encoding-scope.py
@@ -0,0 +1,19 @@
+import time
+
+def main(request, response):
+ use_broken_body = b'use_broken_body' in request.GET
+
+ response.add_required_headers = False
+ response.writer.write_status(200)
+ response.writer.write_header(b"Content-type", b"text/html; charset=UTF-8")
+ response.writer.write_header(b"Transfer-encoding", b"chunked")
+ response.writer.end_headers()
+
+ for idx in range(10):
+ if use_broken_body:
+ response.writer.write(u"%s\n%s\n" % (len(str(idx)), idx))
+ else:
+ response.writer.write(u"%s\r\n%s\r\n" % (len(str(idx)), idx))
+ time.sleep(0.001)
+
+ response.writer.write(u"0\r\n\r\n")
diff --git a/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/chunked-encoding-worker.js b/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/chunked-encoding-worker.js
new file mode 100644
index 0000000..f30e5ed
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/chunked-encoding-worker.js
@@ -0,0 +1,8 @@
+self.addEventListener('activate', event => {
+ event.waitUntil(
+ self.registration.navigationPreload.enable());
+ });
+
+self.addEventListener('fetch', event => {
+ event.respondWith(event.preloadResponse);
+ });
diff --git a/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/cookie.py b/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/cookie.py
new file mode 100644
index 0000000..30a1dd4
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/cookie.py
@@ -0,0 +1,20 @@
+def main(request, response):
+ """
+ Returns a response with a Set-Cookie header based on the query params.
+ The body will be "1" if the cookie is present in the request and `drop` parameter is "0",
+ otherwise the body will be "0".
+ """
+ same_site = request.GET.first(b"same-site")
+ cookie_name = request.GET.first(b"cookie-name")
+ drop = request.GET.first(b"drop")
+ cookie_in_request = b"0"
+ cookie = b"%s=1; Secure; SameSite=%s" % (cookie_name, same_site)
+
+ if drop == b"1":
+ cookie += b"; Max-Age=0"
+
+ if request.cookies.get(cookie_name):
+ cookie_in_request = request.cookies[cookie_name].value
+
+ headers = [(b'Content-Type', b'text/html'), (b'Set-Cookie', cookie)]
+ return (200, headers, cookie_in_request)
diff --git a/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/empty-preload-response-body-scope.html b/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/empty-preload-response-body-scope.html
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/empty-preload-response-body-scope.html
diff --git a/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/empty-preload-response-body-worker.js b/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/empty-preload-response-body-worker.js
new file mode 100644
index 0000000..48c14b7
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/empty-preload-response-body-worker.js
@@ -0,0 +1,15 @@
+self.addEventListener('activate', event => {
+ event.waitUntil(
+ self.registration.navigationPreload.enable());
+ });
+
+self.addEventListener('fetch', event => {
+ event.respondWith(
+ event.preloadResponse
+ .then(res => res.text())
+ .then(text => {
+ return new Response(
+ '<body>[' + text + ']</body>',
+ {headers: [['content-type', 'text/html']]});
+ }));
+ });
diff --git a/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/get-state-worker.js b/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/get-state-worker.js
new file mode 100644
index 0000000..a14ffb4
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/get-state-worker.js
@@ -0,0 +1,21 @@
+// This worker listens for commands from the page and messages back
+// the result.
+
+function handle(message) {
+ const np = self.registration.navigationPreload;
+ switch (message) {
+ case 'getState':
+ return np.getState();
+ case 'enable':
+ return np.enable();
+ case 'disable':
+ return np.disable();
+ case 'setHeaderValue':
+ return np.setHeaderValue('insightful');
+ }
+ return Promise.reject('bad message');
+}
+
+self.addEventListener('message', e => {
+ e.waitUntil(handle(e.data).then(result => e.source.postMessage(result)));
+ });
diff --git a/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/helpers.js b/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/helpers.js
new file mode 100644
index 0000000..86f0c09
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/helpers.js
@@ -0,0 +1,5 @@
+function expect_navigation_preload_state(state, enabled, header, desc) {
+ assert_equals(Object.keys(state).length, 2, desc + ': # of keys');
+ assert_equals(state.enabled, enabled, desc + ': enabled');
+ assert_equals(state.headerValue, header, desc + ': header');
+}
diff --git a/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/navigation-preload-worker.js b/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/navigation-preload-worker.js
new file mode 100644
index 0000000..6e1ab23
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/navigation-preload-worker.js
@@ -0,0 +1,3 @@
+self.addEventListener('fetch', event => {
+ event.respondWith(event.preloadResponse);
+});
diff --git a/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/redirect-redirected.html b/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/redirect-redirected.html
new file mode 100644
index 0000000..f9bfce5
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/redirect-redirected.html
@@ -0,0 +1,3 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<body>redirected</body>
diff --git a/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/redirect-scope.py b/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/redirect-scope.py
new file mode 100644
index 0000000..84a97e5
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/redirect-scope.py
@@ -0,0 +1,38 @@
+def main(request, response):
+ if b"base" in request.GET:
+ return [(b"Content-Type", b"text/html")], b"OK"
+ type = request.GET.first(b"type")
+
+ if type == b"normal":
+ response.status = 302
+ response.headers.append(b"Location", b"redirect-redirected.html")
+ response.headers.append(b"Custom-Header", b"hello")
+ return b""
+
+ if type == b"no-location":
+ response.status = 302
+ response.headers.append(b"Content-Type", b"text/html")
+ response.headers.append(b"Custom-Header", b"hello")
+ return b""
+
+ if type == b"no-location-with-body":
+ response.status = 302
+ response.headers.append(b"Content-Type", b"text/html")
+ response.headers.append(b"Custom-Header", b"hello")
+ return b"<body>BODY</body>"
+
+ if type == b"redirect-to-scope":
+ response.status = 302
+ response.headers.append(b"Location",
+ b"redirect-scope.py?type=redirect-to-scope2")
+ return b""
+ if type == b"redirect-to-scope2":
+ response.status = 302
+ response.headers.append(b"Location",
+ b"redirect-scope.py?type=redirect-to-scope3")
+ return b""
+ if type == b"redirect-to-scope3":
+ response.status = 302
+ response.headers.append(b"Location", b"redirect-redirected.html")
+ response.headers.append(b"Custom-Header", b"hello")
+ return b""
diff --git a/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/redirect-worker.js b/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/redirect-worker.js
new file mode 100644
index 0000000..1b55f2e
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/redirect-worker.js
@@ -0,0 +1,35 @@
+self.addEventListener('activate', event => {
+ event.waitUntil(
+ self.registration.navigationPreload.enable());
+ });
+
+function get_response_info(r) {
+ var info = {
+ type: r.type,
+ url: r.url,
+ status: r.status,
+ ok: r.ok,
+ statusText: r.statusText,
+ headers: []
+ };
+ r.headers.forEach((value, name) => { info.headers.push([value, name]); });
+ return info;
+}
+
+function post_to_page(data) {
+ return self.clients.matchAll()
+ .then(clients => clients.forEach(client => client.postMessage(data)));
+}
+
+self.addEventListener('fetch', event => {
+ event.respondWith(
+ event.preloadResponse
+ .then(
+ res => {
+ if (res.url.includes("base")) {
+ return res;
+ }
+ return post_to_page(get_response_info(res)).then(_ => res);
+ },
+ err => new Response(err.toString())));
+ });
diff --git a/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/request-headers-scope.py b/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/request-headers-scope.py
new file mode 100644
index 0000000..5bab5b0
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/request-headers-scope.py
@@ -0,0 +1,14 @@
+import json
+
+from wptserve.utils import isomorphic_decode
+
+def main(request, response):
+ normalized = dict()
+
+ for key, values in dict(request.headers).items():
+ values = [isomorphic_decode(value) for value in values]
+ normalized[isomorphic_decode(key.upper())] = values
+
+ response.headers.append(b"Content-Type", b"text/html")
+
+ return json.dumps(normalized)
diff --git a/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/request-headers-worker.js b/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/request-headers-worker.js
new file mode 100644
index 0000000..1006cf2
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/request-headers-worker.js
@@ -0,0 +1,10 @@
+self.addEventListener('activate', event => {
+ event.waitUntil(
+ Promise.all[
+ self.registration.navigationPreload.enable(),
+ self.registration.navigationPreload.setHeaderValue('hello')]);
+ });
+
+self.addEventListener('fetch', event => {
+ event.respondWith(event.preloadResponse);
+ });
diff --git a/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/resource-timing-scope.py b/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/resource-timing-scope.py
new file mode 100644
index 0000000..856f9db
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/resource-timing-scope.py
@@ -0,0 +1,19 @@
+import zlib
+
+def main(request, response):
+ type = request.GET.first(b"type")
+
+ if type == "normal":
+ content = b"This is Navigation Preload Resource Timing test."
+ output = zlib.compress(content, 9)
+ headers = [(b"Content-type", b"text/plain"),
+ (b"Content-Encoding", b"deflate"),
+ (b"X-Decoded-Body-Size", len(content)),
+ (b"X-Encoded-Body-Size", len(output)),
+ (b"Content-Length", len(output))]
+ return headers, output
+
+ if type == b"redirect":
+ response.status = 302
+ response.headers.append(b"Location", b"redirect-redirected.html")
+ return b""
diff --git a/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/resource-timing-worker.js b/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/resource-timing-worker.js
new file mode 100644
index 0000000..fac0d8d
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/resource-timing-worker.js
@@ -0,0 +1,37 @@
+async function wait_for_performance_entries(url) {
+ let entries = performance.getEntriesByName(url);
+ if (entries.length > 0) {
+ return entries;
+ }
+ return new Promise((resolve) => {
+ new PerformanceObserver((list) => {
+ const entries = list.getEntriesByName(url);
+ if (entries.length > 0) {
+ resolve(entries);
+ }
+ }).observe({ entryTypes: ['resource'] });
+ });
+}
+
+self.addEventListener('activate', event => {
+ event.waitUntil(self.registration.navigationPreload.enable());
+ });
+
+self.addEventListener('fetch', event => {
+ let headers;
+ event.respondWith(
+ event.preloadResponse
+ .then(response => {
+ headers = response.headers;
+ return response.text()
+ })
+ .then(_ => wait_for_performance_entries(event.request.url))
+ .then(entries =>
+ new Response(
+ JSON.stringify({
+ decodedBodySize: headers.get('X-Decoded-Body-Size'),
+ encodedBodySize: headers.get('X-Encoded-Body-Size'),
+ timingEntries: entries
+ }),
+ {headers: {'Content-Type': 'text/html'}})));
+ });
diff --git a/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/samesite-iframe.html b/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/samesite-iframe.html
new file mode 100644
index 0000000..a28b612
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/samesite-iframe.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<body>samesite</body>
+<script>
+onmessage = (e) => {
+ if (e.data === "GetBody") {
+ parent.postMessage("samesite", '*');
+ }
+}
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/samesite-sw-helper.html b/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/samesite-sw-helper.html
new file mode 100644
index 0000000..51fdc9e
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/samesite-sw-helper.html
@@ -0,0 +1,34 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Navigation Preload Same Site SW registrator</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../resources/test-helpers.sub.js"></script>
+<script>
+
+/**
+ * This is a helper file to register/unregister service worker in a same-site
+ * iframe.
+ **/
+
+async function messageToParent(msg) {
+ parent.postMessage(msg, '*');
+}
+
+onmessage = async (e) => {
+ // t is a , but the helper function needs a test object.
+ let t = {
+ step_func: (func) => func,
+ };
+ if (e.data === "Register") {
+ let reg = await service_worker_unregister_and_register(t, "samesite-worker.js", ".");
+ let worker = reg.installing;
+ await wait_for_state(t, worker, 'activated');
+ await messageToParent("SW Registered");
+ } else if (e.data == "Unregister") {
+ await service_worker_unregister(t, ".");
+ await messageToParent("SW Unregistered");
+ }
+}
+
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/samesite-worker.js b/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/samesite-worker.js
new file mode 100644
index 0000000..f30e5ed
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/samesite-worker.js
@@ -0,0 +1,8 @@
+self.addEventListener('activate', event => {
+ event.waitUntil(
+ self.registration.navigationPreload.enable());
+ });
+
+self.addEventListener('fetch', event => {
+ event.respondWith(event.preloadResponse);
+ });
diff --git a/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/wait-for-activate-worker.js b/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/wait-for-activate-worker.js
new file mode 100644
index 0000000..87791d2
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/wait-for-activate-worker.js
@@ -0,0 +1,40 @@
+// This worker remains in the installing phase so that the
+// navigation preload API can be tested when there is no
+// active worker.
+importScripts('/resources/testharness.js');
+importScripts('helpers.js');
+
+function expect_rejection(promise) {
+ return promise.then(
+ () => { return Promise.reject('unexpected fulfillment'); },
+ err => { assert_equals('InvalidStateError', err.name); });
+}
+
+function test_before_activation() {
+ const np = self.registration.navigationPreload;
+ return expect_rejection(np.enable())
+ .then(() => expect_rejection(np.disable()))
+ .then(() => expect_rejection(np.setHeaderValue('hi')))
+ .then(() => np.getState())
+ .then(state => expect_navigation_preload_state(
+ state, false, 'true', 'state should be the default'))
+ .then(() => 'PASS')
+ .catch(err => 'FAIL: ' + err);
+}
+
+var resolve_done_promise;
+var done_promise = new Promise(resolve => { resolve_done_promise = resolve; });
+
+// Run the test once the page messages this worker.
+self.addEventListener('message', e => {
+ e.waitUntil(test_before_activation()
+ .then(result => {
+ e.source.postMessage(result);
+ resolve_done_promise();
+ }));
+ });
+
+// Don't become the active worker until the test is done.
+self.addEventListener('install', e => {
+ e.waitUntil(done_promise);
+ });
diff --git a/test/wpt/tests/service-workers/service-worker/navigation-preload/samesite-cookies.https.html b/test/wpt/tests/service-workers/service-worker/navigation-preload/samesite-cookies.https.html
new file mode 100644
index 0000000..a860d95
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/navigation-preload/samesite-cookies.https.html
@@ -0,0 +1,61 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<meta name="timeout" content="long">
+<title>Navigation Preload: SameSite cookies</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/test-helpers.sub.js"></script>
+<body>
+<script>
+const scope = 'resources/cookie.py';
+const script = 'resources/navigation-preload-worker.js';
+
+async function drop_cookie(t, same_site, cookie) {
+ const frame = await with_iframe(scope + '?same-site=' + same_site + '&cookie-name=' + cookie + '&drop=1');
+ t.add_cleanup(() => frame.remove());
+}
+
+async function same_site_cookies_test(t, same_site, cookie) {
+ // Remove the cookie before the first visit.
+ await drop_cookie(t, same_site, cookie);
+
+ {
+ const frame = await with_iframe(scope + '?same-site=' + same_site + '&cookie-name=' + cookie + '&drop=0');
+ t.add_cleanup(() => frame.remove());
+ // The body will be 0 because this is the first visit.
+ assert_equals(frame.contentDocument.body.textContent, '0', 'first visit');
+ }
+
+ {
+ const frame = await with_iframe(scope + '?same-site=' + same_site + '&cookie-name=' + cookie + '&drop=0');
+ t.add_cleanup(() => frame.remove());
+ // The body will be 1 because this is the second visit.
+ assert_equals(frame.contentDocument.body.textContent, '1', 'second visit');
+ }
+
+ // Remove the cookie after the test.
+ t.add_cleanup(() => drop_cookie(t, same_site, cookie));
+}
+
+promise_test(async t => {
+ const registration =
+ await service_worker_unregister_and_register(t, script, scope);
+ promise_test(t => registration.unregister(), 'Unregister a service worker.');
+
+ await wait_for_state(t, registration.installing, 'activated');
+ await registration.navigationPreload.enable();
+}, 'Set up a service worker for navigation preload tests.');
+
+promise_test(async t => {
+ await same_site_cookies_test(t, 'None', 'cookie-key-none');
+}, 'Navigation Preload for same site cookies (None).');
+
+promise_test(async t => {
+ await same_site_cookies_test(t, 'Strict', 'cookie-key-strict');
+}, 'Navigation Preload for same site cookies (Strict).');
+
+promise_test(async t => {
+ await same_site_cookies_test(t, 'Lax', 'cookie-key-lax');
+}, 'Navigation Preload for same site cookies (Lax).');
+</script>
+</body>
diff --git a/test/wpt/tests/service-workers/service-worker/navigation-preload/samesite-iframe.https.html b/test/wpt/tests/service-workers/service-worker/navigation-preload/samesite-iframe.https.html
new file mode 100644
index 0000000..633da99
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/navigation-preload/samesite-iframe.https.html
@@ -0,0 +1,67 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Navigation Preload for same site iframe</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="../resources/test-helpers.sub.js"></script>
+<body></body>
+<script>
+
+const SAME_SITE = get_host_info().HTTPS_REMOTE_ORIGIN;
+const RESOURCES_DIR = "/service-workers/service-worker/navigation-preload/resources/";
+
+/**
+ * This test is used for testing the NavigationPreload works in a same site iframe.
+ * The test scenario is
+ * 1. Create a same site iframe to register service worker and wait for it be activated
+ * 2. Create a same site iframe which be intercepted by the service worker.
+ * 3. Once the iframe is loaded, service worker should set the page through the preload response.
+ * And checking if the iframe's body content is expected.
+ * 4. Unregister the service worker.
+ * 5. remove created iframes.
+ */
+
+promise_test(async (t) => {
+ let resolver;
+ let checkValue = false;
+ window.onmessage = (e) => {
+ if (checkValue) {
+ assert_equals(e.data, "samesite");
+ checkValue = false;
+ }
+ resolver();
+ };
+
+ let helperIframe = document.createElement("iframe");
+ helperIframe.src = SAME_SITE + RESOURCES_DIR + "samesite-sw-helper.html";
+ document.body.appendChild(helperIframe);
+
+ await new Promise(resolve => {
+ resolver = resolve;
+ helperIframe.onload = async () => {
+ helperIframe.contentWindow.postMessage("Register", '*');
+ }
+ });
+
+ let sameSiteIframe = document.createElement("iframe");
+ sameSiteIframe.src = SAME_SITE + RESOURCES_DIR + "samesite-iframe.html";
+ document.body.appendChild(sameSiteIframe);
+ await new Promise(resolve => {
+ resolver = resolve;
+ sameSiteIframe.onload = async() => {
+ checkValue = true;
+ sameSiteIframe.contentWindow.postMessage("GetBody", '*')
+ }
+ });
+
+ await new Promise(resolve => {
+ resolver = resolve;
+ helperIframe.contentWindow.postMessage("Unregister", '*')
+ });
+
+ helperIframe.remove();
+ sameSiteIframe.remove();
+ });
+
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/navigation-redirect-body.https.html b/test/wpt/tests/service-workers/service-worker/navigation-redirect-body.https.html
new file mode 100644
index 0000000..0441c61
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/navigation-redirect-body.https.html
@@ -0,0 +1,53 @@
+<!DOCTYPE html>
+<title>Service Worker: Navigation redirection must clear body</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<meta charset="utf-8">
+<body>
+<form id="test-form" method="POST" style="display: none;">
+ <input type="submit" id="submit-button" />
+</form>
+<script>
+promise_test(function(t) {
+ var scope = 'resources/navigation-redirect-body.py';
+ var script = 'resources/navigation-redirect-body-worker.js';
+ var registration;
+ var frame = document.createElement('frame');
+ var form = document.getElementById('test-form');
+ var submit_button = document.getElementById('submit-button');
+
+ frame.src = 'about:blank';
+ frame.name = 'target_frame';
+ frame.id = 'frame';
+ document.body.appendChild(frame);
+ t.add_cleanup(function() { document.body.removeChild(frame); });
+
+ form.action = scope;
+ form.target = 'target_frame';
+
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(function(r) {
+ registration = r;
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() {
+ var frame_load_promise = new Promise(function(resolve) {
+ frame.addEventListener('load', function() {
+ resolve(frame.contentWindow.document.body.innerText);
+ }, false);
+ });
+ submit_button.click();
+ return frame_load_promise;
+ })
+ .then(function(text) {
+ var request_uri = decodeURIComponent(text);
+ assert_equals(
+ request_uri,
+ '/service-workers/service-worker/resources/navigation-redirect-body.py?redirect');
+ return registration.unregister();
+ });
+ }, 'Navigation redirection must clear body');
+</script>
+</body>
diff --git a/test/wpt/tests/service-workers/service-worker/navigation-redirect-resolution.https.html b/test/wpt/tests/service-workers/service-worker/navigation-redirect-resolution.https.html
new file mode 100644
index 0000000..59e1caf
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/navigation-redirect-resolution.https.html
@@ -0,0 +1,58 @@
+<!DOCTYPE html>
+<title>Service Worker: Navigation Redirect Resolution</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+
+function make_absolute(url) {
+ return new URL(url, location).toString();
+}
+
+const script = 'resources/fetch-rewrite-worker.js';
+
+function redirect_result_test(scope, expected_url, description) {
+ promise_test(async t => {
+ const registration = await service_worker_unregister_and_register(
+ t, script, scope);
+ t.add_cleanup(() => {
+ return service_worker_unregister(t, scope);
+ })
+ await wait_for_state(t, registration.installing, 'activated');
+
+ // The navigation to |scope| will be resolved by a fetch to |redirect_url|
+ // which returns a relative Location header. If it is resolved relative to
+ // |scope|, the result will be navigate-redirect-resolution/blank.html. If
+ // relative to |redirect_url|, it will be resources/blank.html. The latter
+ // is correct.
+ const iframe = await with_iframe(scope);
+ t.add_cleanup(() => { iframe.remove(); });
+ assert_equals(iframe.contentWindow.location.href,
+ make_absolute(expected_url));
+ }, description);
+}
+
+// |redirect_url| serves a relative redirect to resources/blank.html.
+const redirect_url = 'resources/redirect.py?Redirect=blank.html';
+
+// |scope_base| does not exist but will be replaced with a fetch of
+// |redirect_url| by fetch-rewrite-worker.js.
+const scope_base = 'resources/subdir/navigation-redirect-resolution?' +
+ 'redirect-mode=manual&url=' +
+ encodeURIComponent(make_absolute(redirect_url));
+
+// When the Service Worker forwards the result of |redirect_url| as an
+// opaqueredirect response, the redirect uses the response's URL list as the
+// base URL, not the request.
+redirect_result_test(scope_base, 'resources/blank.html',
+ 'test relative opaqueredirect');
+
+// The response's base URL should be preserved across CacheStorage and clone.
+redirect_result_test(scope_base + '&cache=1', 'resources/blank.html',
+ 'test relative opaqueredirect with CacheStorage');
+redirect_result_test(scope_base + '&clone=1', 'resources/blank.html',
+ 'test relative opaqueredirect with clone');
+
+</script>
+</body>
diff --git a/test/wpt/tests/service-workers/service-worker/navigation-redirect-to-http.https.html b/test/wpt/tests/service-workers/service-worker/navigation-redirect-to-http.https.html
new file mode 100644
index 0000000..d4d2788
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/navigation-redirect-to-http.https.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<title>Service Worker: Service Worker can receive HTTP opaqueredirect response.</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<meta charset="utf-8">
+<body></body>
+<script>
+async_test(function(t) {
+ var frame_src = get_host_info()['HTTPS_ORIGIN'] + base_path() +
+ 'resources/navigation-redirect-to-http-iframe.html';
+ function on_message(e) {
+ assert_equals(e.data.results, 'OK');
+ t.done();
+ }
+
+ window.addEventListener('message', t.step_func(on_message), false);
+
+ with_iframe(frame_src)
+ .then(function(frame) {
+ t.add_cleanup(function() { frame.remove(); });
+ });
+ }, 'Verify Service Worker can receive HTTP opaqueredirect response.');
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/navigation-redirect.https.html b/test/wpt/tests/service-workers/service-worker/navigation-redirect.https.html
new file mode 100644
index 0000000..a87de56
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/navigation-redirect.https.html
@@ -0,0 +1,846 @@
+<!DOCTYPE html>
+<title>Service Worker: Navigation redirection</title>
+<meta name="timeout" content="long">
+<!-- default variant tests document.location and intercepted URLs -->
+<meta name="variant" content="?default">
+<!-- client variant tests the Clients API (resultingClientId and Client.url) -->
+<meta name="variant" content="?client">
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+const host_info = get_host_info();
+
+// This test registers three Service Workers at SCOPE1, SCOPE2 and
+// OTHER_ORIGIN_SCOPE. And checks the redirected page's URL and the requests
+// which are intercepted by Service Worker while loading redirect page.
+const BASE_URL = host_info['HTTPS_ORIGIN'] + base_path();
+const OTHER_BASE_URL = host_info['HTTPS_REMOTE_ORIGIN'] + base_path();
+
+const SCOPE1 = BASE_URL + 'resources/navigation-redirect-scope1.py?';
+const SCOPE2 = BASE_URL + 'resources/navigation-redirect-scope2.py?';
+const OUT_SCOPE = BASE_URL + 'resources/navigation-redirect-out-scope.py?';
+const SCRIPT = 'resources/redirect-worker.js';
+
+const OTHER_ORIGIN_IFRAME_URL =
+ OTHER_BASE_URL + 'resources/navigation-redirect-other-origin.html';
+const OTHER_ORIGIN_SCOPE =
+ OTHER_BASE_URL + 'resources/navigation-redirect-scope1.py?';
+const OTHER_ORIGIN_OUT_SCOPE =
+ OTHER_BASE_URL + 'resources/navigation-redirect-out-scope.py?';
+
+let registrations;
+let workers;
+let other_origin_frame;
+let message_resolvers = {};
+let next_message_id = 0;
+
+promise_test(async t => {
+ // In this frame we register a service worker at OTHER_ORIGIN_SCOPE.
+ // And will use this frame to communicate with the worker.
+ other_origin_frame = await with_iframe(OTHER_ORIGIN_IFRAME_URL);
+
+ // Register same-origin service workers.
+ registrations = await Promise.all([
+ service_worker_unregister_and_register(t, SCRIPT, SCOPE1),
+ service_worker_unregister_and_register(t, SCRIPT, SCOPE2)]);
+
+ // Wait for all workers to activate.
+ workers = registrations.map(get_effective_worker);
+ return Promise.all([
+ wait_for_state(t, workers[0], 'activated'),
+ wait_for_state(t, workers[1], 'activated'),
+ // This promise will resolve when |wait_for_worker_promise|
+ // in OTHER_ORIGIN_IFRAME_URL resolves.
+ send_to_iframe(other_origin_frame, {command: 'wait_for_worker'})]);
+}, 'initialize global state');
+
+function get_effective_worker(registration) {
+ if (registration.active)
+ return registration.active;
+ if (registration.waiting)
+ return registration.waiting;
+ if (registration.installing)
+ return registration.installing;
+}
+
+async function check_all_intercepted_urls(expected_urls) {
+ const urls = [];
+ urls.push(await get_intercepted_urls(workers[0]));
+ urls.push(await get_intercepted_urls(workers[1]));
+ // Gets the request URLs which are intercepted by OTHER_ORIGIN_SCOPE's
+ // SW. This promise will resolve when get_request_infos() in
+ // OTHER_ORIGIN_IFRAME_URL resolves.
+ const request_infos = await send_to_iframe(other_origin_frame,
+ {command: 'get_request_infos'});
+ urls.push(request_infos.map(info => { return info.url; }));
+
+ assert_object_equals(urls, expected_urls, 'Intercepted URLs should match.');
+}
+
+// Checks |clients| returned from a worker. Only the client matching
+// |expected_final_client_tag| should be found. Returns true if a client was
+// found. Note that the final client is not necessarily found by this worker,
+// if the client is cross-origin.
+//
+// |clients| is an object like:
+// {x: {found: true, id: id1, url: url1}, b: {found: false}}
+function check_clients(clients,
+ expected_id,
+ expected_url,
+ expected_final_client_tag,
+ worker_name) {
+ let found = false;
+ Object.keys(clients).forEach(key => {
+ const info = clients[key];
+ if (info.found) {
+ assert_true(!!expected_final_client_tag,
+ `${worker_name} client tag exists`);
+ assert_equals(key, expected_final_client_tag,
+ `${worker_name} client tag matches`);
+ assert_equals(info.id, expected_id, `${worker_name} client id`);
+ assert_equals(info.url, expected_url, `${worker_name} client url`);
+ found = true;
+ }
+ });
+ return found;
+}
+
+function check_resulting_client_ids(infos, expected_infos, actual_ids, worker) {
+ assert_equals(infos.length, expected_infos.length,
+ `request length for ${worker}`);
+ for (var i = 0; i < infos.length; i++) {
+ const tag = expected_infos[i].resultingClientIdTag;
+ const url = expected_infos[i].url;
+ const actual_id = infos[i].resultingClientId;
+ const expected_id = actual_ids[tag];
+ assert_equals(typeof(actual_id), 'string',
+ `resultingClientId for ${url} request to ${worker}`);
+ if (expected_id) {
+ assert_equals(actual_id, expected_id,
+ `resultingClientId for ${url} request to ${worker}`);
+ } else {
+ actual_ids[tag] = actual_id;
+ }
+ }
+}
+
+// Creates an iframe and navigates to |url|, which is expected to start a chain
+// of redirects.
+// - |expected_last_url| is the expected window.location after the
+// navigation.
+//
+// - |expected_request_infos| is the expected requests that the service workers
+// were dispatched fetch events for. The format is:
+// [
+// [
+// // Requests received by workers[0].
+// {url: url1, resultingClientIdTag: 'a'},
+// {url: url2, resultingClientIdTag: 'a'}
+// ],
+// [
+// // Requests received by workers[1].
+// {url: url3, resultingClientIdTag: 'a'}
+// ],
+// [
+// // Requests received by the cross-origin worker.
+// {url: url4, resultingClientIdTag: 'x'}
+// {url: url5, resultingClientIdTag: 'x'}
+// ]
+// ]
+// Here, |url| is |event.request.url| and |resultingClientIdTag| represents
+// |event.resultingClientId|. Since the actual client ids are not known
+// beforehand, the expectation isn't the literal expected value, but all equal
+// tags must map to the same actual id.
+//
+// - |expected_final_client_tag| is the resultingClientIdTag that is
+// expected to map to the created client's id. This is null if there
+// is no such tag, which can happen when the final request was a cross-origin
+// redirect to out-scope, so no worker received a fetch event whose
+// resultingClientId is the id of the resulting client.
+//
+// In the example above:
+// - workers[0] receives two requests with the same resultingClientId.
+// - workers[1] receives one request also with that resultingClientId.
+// - The cross-origin worker receives two requests with the same
+// resultingClientId which differs from the previous one.
+// - Assuming |expected_final_client_tag| is 'x', then the created
+// client has the id seen by the cross-origin worker above.
+function redirect_test(url,
+ expected_last_url,
+ expected_request_infos,
+ expected_final_client_tag,
+ test_name) {
+ promise_test(async t => {
+ const frame = await with_iframe(url);
+ t.add_cleanup(() => { frame.remove(); });
+
+ // Switch on variant.
+ if (document.location.search == '?client') {
+ return client_variant_test(url, expected_last_url, expected_request_infos,
+ expected_final_client_tag, test_name);
+ }
+
+ return default_variant_test(url, expected_last_url, expected_request_infos,
+ frame, test_name);
+ }, test_name);
+}
+
+// The default variant tests the request interception chain and
+// resulting document.location.
+async function default_variant_test(url,
+ expected_last_url,
+ expected_request_infos,
+ frame,
+ test_name) {
+ const expected_intercepted_urls = expected_request_infos.map(
+ requests_for_worker => {
+ return requests_for_worker.map(info => {
+ return info.url;
+ });
+ });
+ await check_all_intercepted_urls(expected_intercepted_urls);
+ const last_url = await send_to_iframe(frame, 'getLocation');
+ assert_equals(last_url, expected_last_url, 'Last URL should match.');
+}
+
+// The "client" variant tests the Clients API using resultingClientId.
+async function client_variant_test(url,
+ expected_last_url,
+ expected_request_infos,
+ expected_final_client_tag,
+ test_name) {
+ // Request infos is an array like:
+ // [
+ // [{url: url1, resultingClientIdTag: tag1}],
+ // [{url: url2, resultingClientIdTag: tag2}],
+ // [{url: url3: resultingClientIdTag: tag3}]
+ // ]
+ const requestInfos = await get_all_request_infos();
+
+ // We check the actual infos against the expected ones, and learn the
+ // actual ids as we go.
+ const actual_ids = {};
+ check_resulting_client_ids(requestInfos[0],
+ expected_request_infos[0],
+ actual_ids,
+ 'worker0');
+ check_resulting_client_ids(requestInfos[1],
+ expected_request_infos[1],
+ actual_ids,
+ 'worker1');
+ check_resulting_client_ids(requestInfos[2],
+ expected_request_infos[2],
+ actual_ids,
+ 'crossOriginWorker');
+
+ // Now |actual_ids| maps tag to actual id:
+ // {x: id1, b: id2, c: id3}
+ // Ask each worker to try to resolve the actual ids to clients.
+ // Only |expected_final_client_tag| should resolve to a client.
+ const client_infos = await get_all_clients(actual_ids);
+
+ // Client infos is an object like:
+ // {
+ // worker0: {x: {found: true, id: id1, url: url1}, b: {found: false}},
+ // worker1: {x: {found: true, id: id1, url: url1}},
+ // crossOriginWorker: {x: {found: false}}, {b: {found: false}}
+ // }
+ //
+ // Now check each client info. check_clients() verifies each info: only
+ // |expected_final_client_tag| should ever be found and the found client
+ // should have the expected url and id. A wrinkle is that not all workers
+ // will find the client, if they are cross-origin to the client. This
+ // means check_clients() trivially passes if no clients are found. So
+ // additionally check that at least one worker found the client (|found|),
+ // if that was expected (|expect_found|).
+ let found = false;
+ const expect_found = !!expected_final_client_tag;
+ const expected_id = actual_ids[expected_final_client_tag];
+ found = check_clients(client_infos.worker0,
+ expected_id,
+ expected_last_url,
+ expected_final_client_tag,
+ 'worker0');
+ found = check_clients(client_infos.worker1,
+ expected_id,
+ expected_last_url,
+ expected_final_client_tag,
+ 'worker1') || found;
+ found = check_clients(client_infos.crossOriginWorker,
+ expected_id,
+ expected_last_url,
+ expected_final_client_tag,
+ 'crossOriginWorker') || found;
+ assert_equals(found, expect_found, 'client found');
+
+ if (!expect_found) {
+ // TODO(falken): Ask the other origin frame if it has a client of the
+ // expected URL.
+ }
+}
+
+window.addEventListener('message', on_message, false);
+
+function on_message(e) {
+ if (e.origin != host_info['HTTPS_REMOTE_ORIGIN'] &&
+ e.origin != host_info['HTTPS_ORIGIN'] ) {
+ console.error('invalid origin: ' + e.origin);
+ return;
+ }
+ var resolve = message_resolvers[e.data.id];
+ delete message_resolvers[e.data.id];
+ resolve(e.data.result);
+}
+
+function send_to_iframe(frame, message) {
+ var message_id = next_message_id++;
+ return new Promise(resolve => {
+ message_resolvers[message_id] = resolve;
+ frame.contentWindow.postMessage(
+ {id: message_id, message},
+ '*');
+ });
+}
+
+async function get_all_clients(actual_ids) {
+ const client_infos = {};
+ client_infos['worker0'] = await get_clients(workers[0], actual_ids);
+ client_infos['worker1'] = await get_clients(workers[1], actual_ids);
+ client_infos['crossOriginWorker'] =
+ await send_to_iframe(other_origin_frame,
+ {command: 'get_clients', actual_ids});
+ return client_infos;
+}
+
+function get_clients(worker, actual_ids) {
+ return new Promise(resolve => {
+ var channel = new MessageChannel();
+ channel.port1.onmessage = (msg) => {
+ resolve(msg.data.clients);
+ };
+ worker.postMessage({command: 'getClients', actual_ids, port: channel.port2},
+ [channel.port2]);
+ });
+}
+
+// Returns an array of the URLs that |worker| received fetch events for:
+// [url1, url2]
+async function get_intercepted_urls(worker) {
+ const infos = await get_request_infos(worker);
+ return infos.map(info => { return info.url; });
+}
+
+// Returns the requests that |worker| received fetch events for. The return
+// value is an array of format:
+// [
+// {url: url1, resultingClientId: id},
+// {url: url2, resultingClientId: id}
+// ]
+function get_request_infos(worker) {
+ return new Promise(resolve => {
+ var channel = new MessageChannel();
+ channel.port1.onmessage = (msg) => {
+ resolve(msg.data.requestInfos);
+ };
+ worker.postMessage({command: 'getRequestInfos', port: channel.port2},
+ [channel.port2]);
+ });
+}
+
+// Returns an array of the requests the workers received fetch events for:
+// [
+// // Requests from workers[0].
+// [
+// {url: url1, resultingClientIdTag: tag1},
+// {url: url2, resultingClientIdTag: tag1}
+// ],
+//
+// // Requests from workers[1].
+// [{url: url3, resultingClientIdTag: tag2}],
+//
+// // Requests from the cross-origin worker.
+// []
+// ]
+async function get_all_request_infos() {
+ const request_infos = [];
+ request_infos.push(await get_request_infos(workers[0]));
+ request_infos.push(await get_request_infos(workers[1]));
+ request_infos.push(await send_to_iframe(other_origin_frame,
+ {command: 'get_request_infos'}));
+ return request_infos;
+}
+
+let url;
+let url1;
+let url2;
+
+// Normal redirect (from out-scope to in-scope).
+url = SCOPE1;
+redirect_test(
+ OUT_SCOPE + 'url=' + encodeURIComponent(url),
+ url,
+ [[{url, resultingClientIdTag: 'x'}], [], []],
+ 'x',
+ 'Normal redirect to same-origin scope.');
+
+
+url = SCOPE1 + '#ref';
+redirect_test(
+ OUT_SCOPE + 'url=' + encodeURIComponent(SCOPE1) + '#ref',
+ url,
+ [[{url, resultingClientIdTag: 'x'}], [], []],
+ 'x',
+ 'Normal redirect to same-origin scope with a hash fragment.');
+
+url = SCOPE1 + '#ref2';
+redirect_test(
+ OUT_SCOPE + 'url=' + encodeURIComponent(url) + '#ref',
+ url,
+ [[{url, resultingClientIdTag: 'x'}], [], []],
+ 'x',
+ 'Normal redirect to same-origin scope with different hash fragments.');
+
+url = OTHER_ORIGIN_SCOPE;
+redirect_test(
+ OUT_SCOPE + 'url=' + encodeURIComponent(url),
+ url,
+ [[], [], [{url, resultingClientIdTag: 'x'}]],
+ 'x',
+ 'Normal redirect to other-origin scope.');
+
+// SW fallbacked redirect. SW doesn't handle the fetch request.
+url = SCOPE1 + 'url=' + encodeURIComponent(OUT_SCOPE);
+redirect_test(
+ url,
+ OUT_SCOPE,
+ [[{url, resultingClientIdTag: 'x'}], [], []],
+ 'x',
+ 'SW-fallbacked redirect to same-origin out-scope.');
+
+url1 = SCOPE1 + 'url=' + encodeURIComponent(SCOPE1);
+url2 = SCOPE1;
+redirect_test(
+ url1,
+ url2,
+ [
+ [
+ {url: url1, resultingClientIdTag: 'x'},
+ {url: url2, resultingClientIdTag: 'x'}
+ ],
+ [],
+ []
+ ],
+ 'x',
+ 'SW-fallbacked redirect to same-origin same-scope.');
+
+url1 = SCOPE1 + 'url=' + encodeURIComponent(SCOPE1) + '#ref';
+url2 = SCOPE1 + '#ref';
+redirect_test(
+ url1,
+ url2,
+ [
+ [
+ {url: url1, resultingClientIdTag: 'x'},
+ {url: url2, resultingClientIdTag: 'x'}
+ ],
+ [],
+ []
+ ],
+ 'x',
+ 'SW-fallbacked redirect to same-origin same-scope with a hash fragment.');
+
+url1 = SCOPE1 + 'url=' + encodeURIComponent(SCOPE1 + '#ref2') + '#ref';
+url2 = SCOPE1 + '#ref2';
+redirect_test(
+ url1,
+ url2,
+ [
+ [
+ {url: url1, resultingClientIdTag: 'x'},
+ {url: url2, resultingClientIdTag: 'x'}
+ ],
+ [],
+ []
+ ],
+ 'x',
+ 'SW-fallbacked redirect to same-origin same-scope with different hash ' +
+ 'fragments.');
+
+url1 = SCOPE1 + 'url=' + encodeURIComponent(SCOPE2);
+url2 = SCOPE2;
+redirect_test(
+ url1,
+ url2,
+ [
+ [{url: url1, resultingClientIdTag: 'x'}],
+ [{url: url2, resultingClientIdTag: 'x'}],
+ []
+ ],
+ 'x',
+ 'SW-fallbacked redirect to same-origin other-scope.');
+
+url1 = SCOPE1 + 'url=' + encodeURIComponent(OTHER_ORIGIN_OUT_SCOPE);
+url2 = OTHER_ORIGIN_OUT_SCOPE;
+redirect_test(
+ url1,
+ url2,
+ [[{url: url1, resultingClientIdTag: 'a'}], [], []],
+ null,
+ 'SW-fallbacked redirect to other-origin out-scope.');
+
+url1 = SCOPE1 + 'url=' + encodeURIComponent(OTHER_ORIGIN_SCOPE);
+url2 = OTHER_ORIGIN_SCOPE;
+redirect_test(
+ url1,
+ url2,
+ [
+ [{url: url1, resultingClientIdTag: 'a'}],
+ [],
+ [{url: url2, resultingClientIdTag: 'x'}]
+ ],
+ 'x',
+ 'SW-fallbacked redirect to other-origin in-scope.');
+
+
+url3 = SCOPE1;
+url2 = OTHER_ORIGIN_SCOPE + 'url=' + encodeURIComponent(url3);
+url1 = SCOPE1 + 'url=' + encodeURIComponent(url2);
+redirect_test(
+ url1,
+ url3,
+ [
+ [
+ {url: url1, resultingClientIdTag: 'a'},
+ {url: url3, resultingClientIdTag: 'x'}
+ ],
+ [],
+ [{url: url2, resultingClientIdTag: 'b'}]
+ ],
+ 'x',
+ 'SW-fallbacked redirect to other-origin and back to same-origin.');
+
+// SW generated redirect.
+// SW: event.respondWith(Response.redirect(params['url']));
+url1 = SCOPE1 + 'sw=gen&url=' + encodeURIComponent(OUT_SCOPE);
+url2 = OUT_SCOPE;
+redirect_test(
+ url1,
+ url2,
+ [[{url: url1, resultingClientIdTag: 'x'}], [], []],
+ 'x',
+ 'SW-generated redirect to same-origin out-scope.');
+
+url1 = SCOPE1 + 'sw=gen&url=' + encodeURIComponent(OUT_SCOPE) + '#ref';
+url2 = OUT_SCOPE + '#ref';
+redirect_test(
+ url1,
+ url2,
+ [[{url: url1, resultingClientIdTag: 'x'}], [], []],
+ 'x',
+ 'SW-generated redirect to same-origin out-scope with a hash fragment.');
+
+url1 = SCOPE1 + 'sw=gen&url=' + encodeURIComponent(OUT_SCOPE + '#ref2') + '#ref';
+url2 = OUT_SCOPE + '#ref2';
+redirect_test(
+ url1,
+ url2,
+ [[{url: url1, resultingClientIdTag: 'x'}], [], []],
+ 'x',
+ 'SW-generated redirect to same-origin out-scope with different hash ' +
+ 'fragments.');
+
+url1 = SCOPE1 + 'sw=gen&url=' + encodeURIComponent(SCOPE1);
+url2 = SCOPE1;
+redirect_test(
+ url1,
+ url2,
+ [
+ [
+ {url: url1, resultingClientIdTag: 'x'},
+ {url: url2, resultingClientIdTag: 'x'}
+ ],
+ [],
+ []
+ ],
+ 'x',
+ 'SW-generated redirect to same-origin same-scope.');
+
+url1 = SCOPE1 + 'sw=gen&url=' + encodeURIComponent(SCOPE2);
+url2 = SCOPE2;
+redirect_test(
+ url1,
+ url2,
+ [
+ [{url: url1, resultingClientIdTag: 'x'}],
+ [{url: url2, resultingClientIdTag: 'x'}],
+ []
+ ],
+ 'x',
+ 'SW-generated redirect to same-origin other-scope.');
+
+url1 = SCOPE1 + 'sw=gen&url=' + encodeURIComponent(OTHER_ORIGIN_OUT_SCOPE);
+url2 = OTHER_ORIGIN_OUT_SCOPE;
+redirect_test(
+ url1,
+ url2,
+ [[{url: url1, resultingClientIdTag: 'a'}], [], []],
+ null,
+ 'SW-generated redirect to other-origin out-scope.');
+
+url1 = SCOPE1 + 'sw=gen&url=' + encodeURIComponent(OTHER_ORIGIN_SCOPE);
+url2 = OTHER_ORIGIN_SCOPE;
+redirect_test(
+ url1,
+ url2,
+ [
+ [{url: url1, resultingClientIdTag: 'a'}],
+ [],
+ [{url: url2, resultingClientIdTag: 'x'}]
+ ],
+ 'x',
+ 'SW-generated redirect to other-origin in-scope.');
+
+
+// SW fetched redirect.
+// SW: event.respondWith(fetch(event.request));
+url1 = SCOPE1 + 'sw=fetch&url=' + encodeURIComponent(OUT_SCOPE)
+url2 = OUT_SCOPE;
+redirect_test(
+ url1,
+ url2,
+ [[{url: url1, resultingClientIdTag: 'x'}], [], []],
+ 'x',
+ 'SW-fetched redirect to same-origin out-scope.');
+
+url1 = SCOPE1 + 'sw=fetch&url=' + encodeURIComponent(SCOPE1);
+url2 = SCOPE1;
+redirect_test(
+ url1,
+ url2,
+ [
+ [
+ {url: url1, resultingClientIdTag: 'x'},
+ {url: url2, resultingClientIdTag: 'x'}
+ ],
+ [],
+ []
+ ],
+ 'x',
+ 'SW-fetched redirect to same-origin same-scope.');
+
+url1 = SCOPE1 + 'sw=fetch&url=' + encodeURIComponent(SCOPE2);
+url2 = SCOPE2;
+redirect_test(
+ url1,
+ url2,
+ [
+ [{url: url1, resultingClientIdTag: 'x'}],
+ [{url: url2, resultingClientIdTag: 'x'}],
+ []
+ ],
+ 'x',
+ 'SW-fetched redirect to same-origin other-scope.');
+
+url1 = SCOPE1 + 'sw=fetch&url=' + encodeURIComponent(OTHER_ORIGIN_OUT_SCOPE);
+url2 = OTHER_ORIGIN_OUT_SCOPE;
+redirect_test(
+ url1,
+ url2,
+ [[{url: url1, resultingClientIdTag: 'a'}], [], []],
+ null,
+ 'SW-fetched redirect to other-origin out-scope.');
+
+url1 = SCOPE1 + 'sw=fetch&url=' + encodeURIComponent(OTHER_ORIGIN_SCOPE);
+url2 = OTHER_ORIGIN_SCOPE;
+redirect_test(
+ url1,
+ url2,
+ [
+ [{url: url1, resultingClientIdTag: 'a'}],
+ [],
+ [{url: url2, resultingClientIdTag: 'x'}]
+ ],
+ 'x',
+ 'SW-fetched redirect to other-origin in-scope.');
+
+
+// SW responds with a fetch from a different url.
+// SW: event.respondWith(fetch(params['url']));
+url2 = SCOPE1;
+url1 = SCOPE1 + 'sw=fetch-url&url=' + encodeURIComponent(url2);
+redirect_test(
+ url1,
+ url1,
+ [
+ [
+ {url: url1, resultingClientIdTag: 'x'}
+ ],
+ [],
+ []
+ ],
+ 'x',
+ 'SW-fetched response from different URL, same-origin same-scope.');
+
+
+// Opaque redirect.
+// SW: event.respondWith(fetch(
+// new Request(event.request.url, {redirect: 'manual'})));
+url1 = SCOPE1 + 'sw=manual&url=' + encodeURIComponent(OUT_SCOPE);
+url2 = OUT_SCOPE;
+redirect_test(
+ url1,
+ url2,
+ [[{url: url1, resultingClientIdTag: 'x'}], [], []],
+ 'x',
+ 'Redirect to same-origin out-scope with opaque redirect response.');
+
+url1 = SCOPE1 + 'sw=manual&url=' + encodeURIComponent(SCOPE1);
+url2 = SCOPE1;
+redirect_test(
+ url1,
+ url2,
+ [
+ [
+ {url: url1, resultingClientIdTag: 'x'},
+ {url: url2, resultingClientIdTag: 'x'}
+ ],
+ [],
+ []
+ ],
+ 'x',
+ 'Redirect to same-origin same-scope with opaque redirect response.');
+
+url1 = SCOPE1 + 'sw=manual&url=' + encodeURIComponent(SCOPE2);
+url2 = SCOPE2;
+redirect_test(
+ url1,
+ url2,
+ [
+ [{url: url1, resultingClientIdTag: 'x'}],
+ [{url: url2, resultingClientIdTag: 'x'}],
+ []
+ ],
+ 'x',
+ 'Redirect to same-origin other-scope with opaque redirect response.');
+
+url1 = SCOPE1 + 'sw=manual&url=' + encodeURIComponent(OTHER_ORIGIN_OUT_SCOPE);
+url2 = OTHER_ORIGIN_OUT_SCOPE;
+redirect_test(
+ url1,
+ url2,
+ [[{url: url1, resultingClientIdTag: 'a'}], [], []],
+ null,
+ 'Redirect to other-origin out-scope with opaque redirect response.');
+
+url1 = SCOPE1 + 'sw=manual&url=' + encodeURIComponent(OTHER_ORIGIN_SCOPE);
+url2 = OTHER_ORIGIN_SCOPE;
+redirect_test(
+ url1,
+ url2,
+ [
+ [{url: url1, resultingClientIdTag: 'a'}],
+ [],
+ [{url: url2, resultingClientIdTag: 'x'}]
+ ],
+ 'x',
+ 'Redirect to other-origin in-scope with opaque redirect response.');
+
+url= SCOPE1 + 'sw=manual&noLocationRedirect';
+redirect_test(
+ url, url, [[{url, resultingClientIdTag: 'x'}], [], []],
+ 'x',
+ 'No location redirect response.');
+
+
+// Opaque redirect passed through Cache.
+// SW responds with an opaque redirectresponse from the Cache API.
+url1 = SCOPE1 + 'sw=manualThroughCache&url=' + encodeURIComponent(OUT_SCOPE);
+url2 = OUT_SCOPE;
+redirect_test(
+ url1,
+ url2,
+ [[{url: url1, resultingClientIdTag: 'x'}], [], []],
+ 'x',
+ 'Redirect to same-origin out-scope with opaque redirect response which ' +
+ 'is passed through Cache.');
+
+url1 = SCOPE1 + 'sw=manualThroughCache&url=' + encodeURIComponent(SCOPE1);
+url2 = SCOPE1;
+redirect_test(
+ url1,
+ url2,
+ [
+ [
+ {url: url1, resultingClientIdTag: 'x'},
+ {url: url2, resultingClientIdTag: 'x'}
+ ],
+ [],
+ []
+ ],
+ 'x',
+ 'Redirect to same-origin same-scope with opaque redirect response which ' +
+ 'is passed through Cache.');
+
+url1 = SCOPE1 + 'sw=manualThroughCache&url=' + encodeURIComponent(SCOPE2);
+url2 = SCOPE2;
+redirect_test(
+ url1,
+ url2,
+ [
+ [{url: url1, resultingClientIdTag: 'x'}],
+ [{url: url2, resultingClientIdTag: 'x'}],
+ []
+ ],
+ 'x',
+ 'Redirect to same-origin other-scope with opaque redirect response which ' +
+ 'is passed through Cache.');
+
+url1 = SCOPE1 + 'sw=manualThroughCache&url=' +
+ encodeURIComponent(OTHER_ORIGIN_OUT_SCOPE);
+url2 = OTHER_ORIGIN_OUT_SCOPE;
+redirect_test(
+ url1,
+ url2,
+ [[{url: url1, resultingClientIdTag: 'a'}], [], []],
+ null,
+ 'Redirect to other-origin out-scope with opaque redirect response which ' +
+ 'is passed through Cache.');
+
+url1 = SCOPE1 + 'sw=manualThroughCache&url=' +
+ encodeURIComponent(OTHER_ORIGIN_SCOPE);
+url2 = OTHER_ORIGIN_SCOPE;
+redirect_test(
+ url1,
+ url2,
+ [
+ [{url: url1, resultingClientIdTag: 'a'}],
+ [],
+ [{url: url2, resultingClientIdTag: 'x'}],
+ ],
+ 'x',
+ 'Redirect to other-origin in-scope with opaque redirect response which ' +
+ 'is passed through Cache.');
+
+url = SCOPE1 + 'sw=manualThroughCache&noLocationRedirect';
+redirect_test(
+ url,
+ url,
+ [[{url, resultingClientIdTag: 'x'}], [], []],
+ 'x',
+ 'No location redirect response via Cache.');
+
+// Clean up the test environment. This promise_test() needs to be the last one.
+promise_test(async t => {
+ registrations.forEach(async registration => {
+ if (registration)
+ await registration.unregister();
+ });
+ await send_to_iframe(other_origin_frame, {command: 'unregister'});
+ other_origin_frame.remove();
+}, 'clean up global state');
+</script>
+</body>
diff --git a/test/wpt/tests/service-workers/service-worker/navigation-sets-cookie.https.html b/test/wpt/tests/service-workers/service-worker/navigation-sets-cookie.https.html
new file mode 100644
index 0000000..7f6c756
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/navigation-sets-cookie.https.html
@@ -0,0 +1,133 @@
+<!DOCTYPE html>
+<meta charset="utf-8"/>
+<meta name="timeout" content="long">
+<title>Service Worker: Navigation setting cookies</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script>
+<script src="/cookies/resources/cookie-helper.sub.js"></script>
+<body>
+<script>
+'use strict';
+
+const scopepath = '/cookies/resources/setSameSite.py?with-sw';
+
+async function unregister_service_worker(origin) {
+ let target_url = origin +
+ '/service-workers/service-worker/resources/unregister-rewrite-worker.html' +
+ '?scopepath=' + encodeURIComponent(scopepath);
+ const w = window.open(target_url);
+ try {
+ await wait_for_message('SW-UNREGISTERED');
+ } finally {
+ w.close();
+ }
+}
+
+async function register_service_worker(origin) {
+ let target_url = origin +
+ '/service-workers/service-worker/resources/register-rewrite-worker.html' +
+ '?scopepath=' + encodeURIComponent(scopepath);
+ const w = window.open(target_url);
+ try {
+ await wait_for_message('SW-REGISTERED');
+ } finally {
+ w.close();
+ }
+}
+
+async function clear_cookies(origin) {
+ let target_url = origin + '/cookies/samesite/resources/puppet.html';
+ const w = window.open(target_url);
+ try {
+ await wait_for_message('READY');
+ w.postMessage({ type: 'drop' }, '*');
+ await wait_for_message('drop-complete');
+ } finally {
+ w.close();
+ }
+}
+
+// The following tests are adapted from /cookies/samesite/setcookie-navigation.https.html
+
+// Asserts that cookies are present or not present (according to `expectation`)
+// in the cookie string `cookies` with the correct names and value.
+function assert_cookies_present(cookies, value, expected_cookie_names, expectation) {
+ for (name of expected_cookie_names) {
+ let re = new RegExp("(?:^|; )" + name + "=" + value + "(?:$|;)");
+ let assertion = expectation ? assert_true : assert_false;
+ assertion(re.test(cookies), "`" + name + "=" + value + "` in cookies");
+ }
+}
+
+// Navigate from ORIGIN to |origin_to|, expecting the navigation to set SameSite
+// cookies on |origin_to|.
+function navigate_test(method, origin_to, query, title) {
+ promise_test(async function(t) {
+ // The cookies don't need to be cleared on each run because |value| is
+ // a new random value on each run, so on each run we are overwriting and
+ // checking for a cookie with a different random value.
+ let value = query + "&" + Math.random();
+ let url_from = SECURE_ORIGIN + "/cookies/samesite/resources/navigate.html"
+ let url_to = origin_to + "/cookies/resources/setSameSite.py?" + value;
+ var w = window.open(url_from);
+ await wait_for_message('READY', SECURE_ORIGIN);
+ assert_equals(SECURE_ORIGIN, window.origin);
+ assert_equals(SECURE_ORIGIN, w.origin);
+ let command = (method === "POST") ? "post-form" : "navigate";
+ w.postMessage({ type: command, url: url_to }, "*");
+ let message = await wait_for_message('COOKIES_SET', origin_to);
+ let samesite_cookie_names = ['samesite_strict', 'samesite_lax', 'samesite_none', 'samesite_unspecified'];
+ assert_cookies_present(message.data.cookies, value, samesite_cookie_names, true);
+ w.close();
+ }, title);
+}
+
+promise_test(async t => {
+ await register_service_worker(SECURE_ORIGIN);
+ await register_service_worker(SECURE_CROSS_SITE_ORIGIN);
+}, 'Setup service workers');
+
+navigate_test("GET", SECURE_ORIGIN, "with-sw&ignore",
+ "Same-site top-level navigation with fallback service worker should be able to set SameSite=* cookies.");
+navigate_test("GET", SECURE_CROSS_SITE_ORIGIN, "with-sw&ignore",
+ "Cross-site top-level navigation with fallback service worker should be able to set SameSite=* cookies.");
+navigate_test("POST", SECURE_ORIGIN, "with-sw&ignore",
+ "Same-site top-level POST with fallback service worker should be able to set SameSite=* cookies.");
+navigate_test("POST", SECURE_CROSS_SITE_ORIGIN, "with-sw&ignore",
+ "Cross-site top-level with fallback service worker POST should be able to set SameSite=* cookies.");
+
+navigate_test("GET", SECURE_ORIGIN, "with-sw",
+ "Same-site top-level navigation with passthrough service worker should be able to set SameSite=* cookies.");
+navigate_test("GET", SECURE_CROSS_SITE_ORIGIN, "with-sw",
+ "Cross-site top-level navigation with passthrough service worker should be able to set SameSite=* cookies.");
+navigate_test("POST", SECURE_ORIGIN, "with-sw",
+ "Same-site top-level POST with passthrough service worker should be able to set SameSite=* cookies.");
+navigate_test("POST", SECURE_CROSS_SITE_ORIGIN, "with-sw",
+ "Cross-site top-level with passthrough service worker POST should be able to set SameSite=* cookies.");
+
+navigate_test("GET", SECURE_ORIGIN, "with-sw&navpreload",
+ "Same-site top-level navigation with navpreload service worker should be able to set SameSite=* cookies.");
+navigate_test("GET", SECURE_CROSS_SITE_ORIGIN, "with-sw&navpreload",
+ "Cross-site top-level navigation with navpreload service worker should be able to set SameSite=* cookies.");
+// navpreload not supported with POST method
+
+navigate_test("GET", SECURE_ORIGIN, "with-sw&change-request",
+ "Same-site top-level navigation with change-request service worker should be able to set SameSite=* cookies.");
+navigate_test("GET", SECURE_CROSS_SITE_ORIGIN, "with-sw&change-request",
+ "Cross-site top-level navigation with change-request service worker should be able to set SameSite=* cookies.");
+navigate_test("POST", SECURE_ORIGIN, "with-sw&change-request",
+ "Same-site top-level POST with change-request service worker should be able to set SameSite=* cookies.");
+navigate_test("POST", SECURE_CROSS_SITE_ORIGIN, "with-sw&change-request",
+ "Cross-site top-level with change-request service worker POST should be able to set SameSite=* cookies.");
+
+promise_test(async t => {
+ await unregister_service_worker(SECURE_ORIGIN);
+ await unregister_service_worker(SECURE_CROSS_SITE_ORIGIN);
+ await clear_cookies(SECURE_ORIGIN);
+ await clear_cookies(SECURE_CROSS_SITE_ORIGIN);
+}, 'Cleanup service workers');
+
+</script>
+</body>
diff --git a/test/wpt/tests/service-workers/service-worker/navigation-timing-extended.https.html b/test/wpt/tests/service-workers/service-worker/navigation-timing-extended.https.html
new file mode 100644
index 0000000..acb02c6
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/navigation-timing-extended.https.html
@@ -0,0 +1,55 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+
+<script>
+const timingEventOrder = [
+ 'startTime',
+ 'workerStart',
+ 'fetchStart',
+ 'requestStart',
+ 'responseStart',
+ 'responseEnd',
+];
+
+function navigate_in_frame(frame, url) {
+ frame.contentWindow.location = url;
+ return new Promise((resolve) => {
+ frame.addEventListener('load', () => {
+ const timing = frame.contentWindow.performance.getEntriesByType('navigation')[0];
+ const {timeOrigin} = frame.contentWindow.performance;
+ resolve({
+ workerStart: timing.workerStart + timeOrigin,
+ fetchStart: timing.fetchStart + timeOrigin
+ })
+ });
+ });
+}
+
+const worker_url = 'resources/navigation-timing-worker-extended.js';
+
+promise_test(async (t) => {
+ const scope = 'resources/timings/dummy.html';
+ const registration = await service_worker_unregister_and_register(t, worker_url, scope);
+ t.add_cleanup(() => registration.unregister());
+ await wait_for_state(t, registration.installing, 'activating');
+ const frame = await with_iframe('resources/empty.html');
+ t.add_cleanup(() => frame.remove());
+
+ const [timingFromEntry, timingFromWorker] = await Promise.all([
+ navigate_in_frame(frame, scope),
+ new Promise(resolve => {
+ window.addEventListener('message', m => {
+ resolve(m.data)
+ })
+ })])
+
+ assert_greater_than(timingFromWorker.activateWorkerEnd, timingFromEntry.workerStart,
+ 'workerStart marking should not wait for worker activation to finish');
+ assert_greater_than(timingFromEntry.fetchStart, timingFromWorker.activateWorkerEnd,
+ 'fetchStart should be marked once the worker is activated');
+ assert_greater_than(timingFromWorker.handleFetchEvent, timingFromEntry.fetchStart,
+ 'fetchStart should be marked before the Fetch event handler is called');
+}, 'Service worker controlled navigation timing');
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/navigation-timing.https.html b/test/wpt/tests/service-workers/service-worker/navigation-timing.https.html
new file mode 100644
index 0000000..75cab40
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/navigation-timing.https.html
@@ -0,0 +1,77 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+
+<script>
+const timingEventOrder = [
+ 'startTime',
+ 'workerStart',
+ 'fetchStart',
+ 'requestStart',
+ 'responseStart',
+ 'responseEnd',
+];
+
+function verify(timing) {
+ for (let i = 0; i < timingEventOrder.length - 1; i++) {
+ assert_true(timing[timingEventOrder[i]] <= timing[timingEventOrder[i + 1]],
+ `Expected ${timingEventOrder[i]} <= ${timingEventOrder[i + 1]}`);
+ }
+}
+
+function navigate_in_frame(frame, url) {
+ frame.contentWindow.location = url;
+ return new Promise((resolve) => {
+ frame.addEventListener('load', () => {
+ const timing = frame.contentWindow.performance.getEntriesByType('navigation')[0];
+ resolve(timing);
+ });
+ });
+}
+
+const worker_url = 'resources/navigation-timing-worker.js';
+
+promise_test(async (t) => {
+ const scope = 'resources/empty.html';
+ const registration = await service_worker_unregister_and_register(t, worker_url, scope);
+ t.add_cleanup(() => registration.unregister());
+ await wait_for_state(t, registration.installing, 'activated');
+ const frame = await with_iframe(scope);
+ t.add_cleanup(() => frame.remove());
+
+ const timing = await navigate_in_frame(frame, scope);
+ assert_greater_than(timing.workerStart, 0);
+ verify(timing);
+}, 'Service worker controlled navigation timing');
+
+promise_test(async (t) => {
+ const scope = 'resources/empty.html?network-fallback';
+ const registration = await service_worker_unregister_and_register(t, worker_url, scope);
+ t.add_cleanup(() => registration.unregister());
+ await wait_for_state(t, registration.installing, 'activated');
+ const frame = await with_iframe(scope);
+ t.add_cleanup(() => frame.remove());
+
+ const timing = await navigate_in_frame(frame, scope);
+ assert_greater_than(timing.workerStart, 0);
+ verify(timing);
+}, 'Service worker controlled navigation timing network fallback');
+
+promise_test(async (t) => {
+ const scope = 'resources/redirect.py?Redirect=empty.html';
+ const registration = await service_worker_unregister_and_register(t, worker_url, scope);
+ t.add_cleanup(() => registration.unregister());
+ await wait_for_state(t, registration.installing, 'activated');
+ const frame = await with_iframe(scope);
+ t.add_cleanup(() => frame.remove());
+
+ const timing = await navigate_in_frame(frame, scope);
+ verify(timing);
+ // Additional checks for redirected navigation.
+ assert_true(timing.redirectStart <= timing.redirectEnd,
+ 'Expected redirectStart <= redirectEnd');
+ assert_true(timing.redirectEnd <= timing.fetchStart,
+ 'Expected redirectEnd <= fetchStart');
+}, 'Service worker controlled navigation timing redirect');
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/nested-blob-url-workers.https.html b/test/wpt/tests/service-workers/service-worker/nested-blob-url-workers.https.html
new file mode 100644
index 0000000..7269cbb
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/nested-blob-url-workers.https.html
@@ -0,0 +1,42 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Service Worker: nested blob URL worker clients</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+
+const SCRIPT = 'resources/simple-intercept-worker.js';
+const SCOPE = 'resources/';
+const RESOURCE = 'resources/simple.txt';
+
+promise_test((t) => {
+ return runTest(t, 'resources/nested-blob-url-workers.html');
+}, 'Nested blob URL workers should be intercepted by a service worker.');
+
+promise_test((t) => {
+ return runTest(t, 'resources/nested-worker-created-from-blob-url-worker.html');
+}, 'Nested worker created from a blob URL worker should be intercepted by a service worker.');
+
+promise_test((t) => {
+ return runTest(t, 'resources/nested-blob-url-worker-created-from-worker.html');
+}, 'Nested blob URL worker created from a worker should be intercepted by a service worker.');
+
+async function runTest(t, iframe_url) {
+ const reg = await service_worker_unregister_and_register(t, SCRIPT, SCOPE);
+ t.add_cleanup(_ => reg.unregister());
+ await wait_for_state(t, reg.installing, 'activated');
+
+ const frame = await with_iframe(iframe_url);
+ t.add_cleanup(_ => frame.remove());
+ assert_not_equals(frame.contentWindow.navigator.serviceWorker.controller,
+ null, 'frame should be controlled');
+
+ const response_text = await frame.contentWindow.fetch_in_worker(RESOURCE);
+ assert_equals(response_text, 'intercepted by service worker',
+ 'fetch() should be intercepted.');
+}
+
+</script>
+</body>
diff --git a/test/wpt/tests/service-workers/service-worker/next-hop-protocol.https.html b/test/wpt/tests/service-workers/service-worker/next-hop-protocol.https.html
new file mode 100644
index 0000000..7a90743
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/next-hop-protocol.https.html
@@ -0,0 +1,49 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Service Worker: Verify nextHopProtocol is set correctly</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+
+async function getNextHopProtocol(frame, url) {
+ let final_url = new URL(url, self.location).href;
+ await frame.contentWindow.fetch(final_url).then(r => r.text());
+ let entryList = frame.contentWindow.performance.getEntriesByName(final_url);
+ let entry = entryList[entryList.length - 1];
+ return entry.nextHopProtocol;
+}
+
+async function runTest(t, base_url, expected_protocol) {
+ const scope = 'resources/empty.html?next-hop-protocol';
+ const script = 'resources/fetch-rewrite-worker.js';
+ let frame;
+
+ const registration =
+ await service_worker_unregister_and_register(t, script, scope);
+ t.add_cleanup(async _ => registration.unregister());
+ await wait_for_state(t, registration.installing, 'activated');
+ frame = await with_iframe(scope);
+ t.add_cleanup(_ => frame.remove());
+
+ assert_equals(await getNextHopProtocol(frame, `${base_url}?generate-png`),
+ '', 'nextHopProtocol is not set on synthetic response');
+ assert_equals(await getNextHopProtocol(frame, `${base_url}?ignore`),
+ expected_protocol, 'nextHopProtocol is set on fallback');
+ assert_equals(await getNextHopProtocol(frame, `${base_url}`),
+ expected_protocol, 'nextHopProtocol is set on pass-through');
+ assert_equals(await getNextHopProtocol(frame, `${base_url}?cache`),
+ expected_protocol, 'nextHopProtocol is set on cached response');
+}
+
+promise_test(async (t) => {
+ return runTest(t, 'resources/empty.js', 'http/1.1');
+}, 'nextHopProtocol reports H1 correctly when routed via a service worker.');
+
+// This may be expected to fail if the WPT infrastructure does not fully
+// support H2 protocol testing yet.
+promise_test(async (t) => {
+ return runTest(t, 'resources/empty.h2.js', 'h2');
+}, 'nextHopProtocol reports H2 correctly when routed via a service worker.');
+
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/no-dynamic-import-in-module.any.js b/test/wpt/tests/service-workers/service-worker/no-dynamic-import-in-module.any.js
new file mode 100644
index 0000000..f7c2ef3
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/no-dynamic-import-in-module.any.js
@@ -0,0 +1,7 @@
+// META: global=serviceworker-module
+
+// This is imported to ensure import('./basic-module-2.js') fails even if
+// it has been previously statically imported.
+import './resources/basic-module-2.js';
+
+import './resources/no-dynamic-import.js';
diff --git a/test/wpt/tests/service-workers/service-worker/no-dynamic-import.any.js b/test/wpt/tests/service-workers/service-worker/no-dynamic-import.any.js
new file mode 100644
index 0000000..25b370b
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/no-dynamic-import.any.js
@@ -0,0 +1,3 @@
+// META: global=serviceworker
+
+importScripts('resources/no-dynamic-import.js');
diff --git a/test/wpt/tests/service-workers/service-worker/onactivate-script-error.https.html b/test/wpt/tests/service-workers/service-worker/onactivate-script-error.https.html
new file mode 100644
index 0000000..f5e80bb
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/onactivate-script-error.https.html
@@ -0,0 +1,74 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="resources/testharness-helpers.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+function wait_for_install(worker) {
+ return new Promise(function(resolve, reject) {
+ worker.addEventListener('statechange', function(event) {
+ if (worker.state == 'installed')
+ resolve();
+ else if (worker.state == 'redundant')
+ reject();
+ });
+ });
+}
+
+function wait_for_activate(worker) {
+ return new Promise(function(resolve, reject) {
+ worker.addEventListener('statechange', function(event) {
+ if (worker.state == 'activated')
+ resolve();
+ else if (worker.state == 'redundant')
+ reject();
+ });
+ });
+}
+
+function make_test(name, script) {
+ promise_test(function(t) {
+ var scope = script;
+ var registration;
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(function(r) {
+ registration = r;
+
+ t.add_cleanup(function() {
+ return r.unregister();
+ });
+
+ return wait_for_install(registration.installing);
+ })
+ .then(function() {
+ // Activate should succeed regardless of script errors.
+ return wait_for_activate(registration.waiting);
+ });
+ }, name);
+}
+
+[
+ {
+ name: 'activate handler throws an error',
+ script: 'resources/onactivate-throw-error-worker.js',
+ },
+ {
+ name: 'activate handler throws an error, error handler does not cancel',
+ script: 'resources/onactivate-throw-error-with-empty-onerror-worker.js',
+ },
+ {
+ name: 'activate handler dispatches an event that throws an error',
+ script: 'resources/onactivate-throw-error-from-nested-event-worker.js',
+ },
+ {
+ name: 'activate handler throws an error that is cancelled',
+ script: 'resources/onactivate-throw-error-then-cancel-worker.js',
+ },
+ {
+ name: 'activate handler throws an error and prevents default',
+ script: 'resources/onactivate-throw-error-then-prevent-default-worker.js',
+ }
+].forEach(function(test_case) {
+ make_test(test_case.name, test_case.script);
+ });
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/oninstall-script-error.https.html b/test/wpt/tests/service-workers/service-worker/oninstall-script-error.https.html
new file mode 100644
index 0000000..fe7f6e9
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/oninstall-script-error.https.html
@@ -0,0 +1,72 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="resources/testharness-helpers.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+function wait_for_install_event(worker) {
+ return new Promise(function(resolve) {
+ worker.addEventListener('statechange', function(event) {
+ if (worker.state == 'installed')
+ resolve(true);
+ else if (worker.state == 'redundant')
+ resolve(false);
+ });
+ });
+}
+
+function make_test(name, script, expect_install) {
+ promise_test(function(t) {
+ var scope = script;
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(function(registration) {
+ return wait_for_install_event(registration.installing);
+ })
+ .then(function(did_install) {
+ assert_equals(did_install, expect_install,
+ 'The worker was installed');
+ })
+ }, name);
+}
+
+[
+ {
+ name: 'install handler throws an error',
+ script: 'resources/oninstall-throw-error-worker.js',
+ expect_install: true
+ },
+ {
+ name: 'install handler throws an error, error handler does not cancel',
+ script: 'resources/oninstall-throw-error-with-empty-onerror-worker.js',
+ expect_install: true
+ },
+ {
+ name: 'install handler dispatches an event that throws an error',
+ script: 'resources/oninstall-throw-error-from-nested-event-worker.js',
+ expect_install: true
+ },
+ {
+ name: 'install handler throws an error in the waitUntil',
+ script: 'resources/oninstall-waituntil-throw-error-worker.js',
+ expect_install: false
+ },
+
+ // The following two cases test what happens when the ServiceWorkerGlobalScope
+ // 'error' event handler cancels the resulting error event. Since the
+ // original 'install' event handler through, the installation should still
+ // be stopped in this case. See:
+ // https://github.com/slightlyoff/ServiceWorker/issues/778
+ {
+ name: 'install handler throws an error that is cancelled',
+ script: 'resources/oninstall-throw-error-then-cancel-worker.js',
+ expect_install: true
+ },
+ {
+ name: 'install handler throws an error and prevents default',
+ script: 'resources/oninstall-throw-error-then-prevent-default-worker.js',
+ expect_install: true
+ }
+].forEach(function(test_case) {
+ make_test(test_case.name, test_case.script, test_case.expect_install);
+ });
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/opaque-response-preloaded.https.html b/test/wpt/tests/service-workers/service-worker/opaque-response-preloaded.https.html
new file mode 100644
index 0000000..417aa4e
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/opaque-response-preloaded.https.html
@@ -0,0 +1,50 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Opaque responses should not be reused for XHRs</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+const WORKER =
+ 'resources/opaque-response-preloaded-worker.js';
+
+var done;
+
+// These test that the browser does not inappropriately use a cached opaque
+// response for a request that is not no-cors. The test opens a controlled
+// iframe that uses link rel=preload to issue a same-origin no-cors request.
+// The service worker responds to the request with an opaque response. Then the
+// iframe does an XHR (not no-cors) to that URL again. The request should fail.
+promise_test(t => {
+ const SCOPE =
+ 'resources/opaque-response-being-preloaded-xhr.html';
+ const promise = new Promise(resolve => done = resolve);
+
+ return service_worker_unregister_and_register(t, WORKER, SCOPE)
+ .then(reg => {
+ add_completion_callback(() => reg.unregister());
+ return wait_for_state(t, reg.installing, 'activated');
+ })
+ .then(() => with_iframe(SCOPE))
+ .then(frame => t.add_cleanup(() => frame.remove() ))
+ .then(() => promise)
+ .then(result => assert_equals(result, 'PASS'));
+ }, 'Opaque responses should not be reused for XHRs, loading case');
+
+promise_test(t => {
+ const SCOPE =
+ 'resources/opaque-response-preloaded-xhr.html';
+ const promise = new Promise(resolve => done = resolve);
+
+ return service_worker_unregister_and_register(t, WORKER, SCOPE)
+ .then(reg => {
+ add_completion_callback(() => reg.unregister());
+ return wait_for_state(t, reg.installing, 'activated');
+ })
+ .then(() => with_iframe(SCOPE))
+ .then(frame => t.add_cleanup(() => frame.remove() ))
+ .then(() => promise)
+ .then(result => assert_equals(result, 'PASS'));
+ }, 'Opaque responses should not be reused for XHRs, done case');
+
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/opaque-script.https.html b/test/wpt/tests/service-workers/service-worker/opaque-script.https.html
new file mode 100644
index 0000000..7d21218
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/opaque-script.https.html
@@ -0,0 +1,71 @@
+<!doctype html>
+<title>Cache Storage: verify scripts loaded from cache_storage are marked opaque</title>
+<link rel="help" href="https://w3c.github.io/ServiceWorker/#cache-interface">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/testharness-helpers.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script>
+'use strict';
+
+const SW_URL = 'resources/opaque-script-sw.js';
+const BASE_SCOPE = './resources/opaque-script-frame.html';
+const SAME_ORIGIN_BASE = new URL('./resources/', self.location.href).href;
+const CROSS_ORIGIN_BASE = new URL('./resources/',
+ get_host_info().HTTPS_REMOTE_ORIGIN + base_path()).href;
+
+function wait_for_error() {
+ return new Promise(resolve => {
+ self.addEventListener('message', function messageHandler(evt) {
+ if (evt.data.type !== 'ErrorEvent')
+ return;
+ self.removeEventListener('message', messageHandler);
+ resolve(evt.data.msg);
+ });
+ });
+}
+
+// Load an iframe that dynamically adds a script tag that is
+// same/cross origin and large/small. It then calls a function
+// defined in that loaded script that throws an unhandled error.
+// The resulting message exposed in the global onerror handler
+// is reported back from this function. Opaque cross origin
+// scripts should not expose the details of the uncaught exception.
+async function get_error_message(t, mode, size) {
+ const script_base = mode === 'same-origin' ? SAME_ORIGIN_BASE
+ : CROSS_ORIGIN_BASE;
+ const script = script_base + `opaque-script-${size}.js`;
+ const scope = BASE_SCOPE + `?script=${script}`;
+ const reg = await service_worker_unregister_and_register(t, SW_URL, scope);
+ t.add_cleanup(_ => reg.unregister());
+ assert_true(!!reg.installing);
+ await wait_for_state(t, reg.installing, 'activated');
+ const error_promise = wait_for_error();
+ const f = await with_iframe(scope);
+ t.add_cleanup(_ => f.remove());
+ const error = await error_promise;
+ return error;
+}
+
+promise_test(async t => {
+ const error = await get_error_message(t, 'same-origin', 'small');
+ assert_true(error.includes('Intentional error'));
+}, 'Verify small same-origin cache_storage scripts are not opaque.');
+
+promise_test(async t => {
+ const error = await get_error_message(t, 'same-origin', 'large');
+ assert_true(error.includes('Intentional error'));
+}, 'Verify large same-origin cache_storage scripts are not opaque.');
+
+promise_test(async t => {
+ const error = await get_error_message(t, 'cross-origin', 'small');
+ assert_false(error.includes('Intentional error'));
+}, 'Verify small cross-origin cache_storage scripts are opaque.');
+
+promise_test(async t => {
+ const error = await get_error_message(t, 'cross-origin', 'large');
+ assert_false(error.includes('Intentional error'));
+}, 'Verify large cross-origin cache_storage scripts are opaque.');
+
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/partitioned-claim.tentative.https.html b/test/wpt/tests/service-workers/service-worker/partitioned-claim.tentative.https.html
new file mode 100644
index 0000000..1f42c52
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/partitioned-claim.tentative.https.html
@@ -0,0 +1,74 @@
+<!DOCTYPE html>
+<meta charset="utf-8"/>
+<title>Service Worker: Partitioned Service Workers</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/partitioned-utils.js"></script>
+
+<body>
+This test creates a iframe in a first-party context and then registers a
+service worker (such that the iframe client is unclaimed).
+A third-party iframe is then created which has its SW call clients.claim()
+and then the test checks that the 1p iframe was not claimed int he process.
+Finally the test has its SW call clients.claim() and confirms the 1p iframe is
+claimed.
+
+<script>
+promise_test(async t => {
+ const script = './resources/partitioned-storage-sw.js';
+ const scope = './resources/partitioned-';
+
+ // Add a 1p iframe.
+ const wait_frame_url = new URL(
+ './resources/partitioned-service-worker-iframe-claim.html?1p-mode',
+ self.location);
+
+ const frame = await with_iframe(wait_frame_url, false);
+ t.add_cleanup(async () => {
+ frame.remove();
+ });
+
+ // Add service worker to this 1P context.
+ const reg = await service_worker_unregister_and_register(t, script, scope);
+ t.add_cleanup(() => reg.unregister());
+ await wait_for_state(t, reg.installing, 'activated');
+
+ // Register the message listener.
+ self.addEventListener('message', messageEventHandler);
+
+ // Now we need to create a third-party iframe whose SW will claim it and then
+ // the iframe will postMessage that its serviceWorker.controller state has
+ // changed.
+ const third_party_iframe_url = new URL(
+ './resources/partitioned-service-worker-iframe-claim.html?3p-mode',
+ get_host_info().HTTPS_ORIGIN + self.location.pathname);
+
+ // Create the 3p window (which will in turn create the iframe with the SW)
+ // and await on its data.
+ const frame_3p_data = await loadAndReturnSwData(t, third_party_iframe_url,
+ 'window');
+ assert_equals(frame_3p_data.status, "success",
+ "3p iframe was successfully claimed");
+
+ // Confirm that the 1p iframe wasn't claimed at the same time.
+ const controller_1p_iframe = makeMessagePromise();
+ frame.contentWindow.postMessage({type: "get-controller"});
+ const controller_1p_iframe_data = await controller_1p_iframe;
+ assert_equals(controller_1p_iframe_data.controller, null,
+ "Test iframe client isn't claimed yet.");
+
+
+ // Tell the SW to claim.
+ const claimed_1p_iframe = makeMessagePromise();
+ reg.active.postMessage({type: "claim"});
+ const claimed_1p_iframe_data = await claimed_1p_iframe;
+
+ assert_equals(claimed_1p_iframe_data.status, "success",
+ "iframe client was successfully claimed.");
+
+}, "ServiceWorker's clients.claim() is partitioned");
+</script>
+
+</body> \ No newline at end of file
diff --git a/test/wpt/tests/service-workers/service-worker/partitioned-cookies.tentative.https.html b/test/wpt/tests/service-workers/service-worker/partitioned-cookies.tentative.https.html
new file mode 100644
index 0000000..5f6371c
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/partitioned-cookies.tentative.https.html
@@ -0,0 +1,119 @@
+<!DOCTYPE html>
+<head>
+<meta charset="utf-8"/>
+<meta name="timeout" content="long">
+<title>Service Worker: Partitioned Cookies</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+</head>
+
+<!--
+ This test exercises partitioned service workers' interaction with partitioned cookies.
+ Partitioned service workers should only be able to interact with partitioned cookies whose
+ partition key matches the worker's partition.
+-->
+
+<body>
+<script>
+
+promise_test(async t => {
+ const script = './resources/partitioned-cookies-sw.js'
+ const scope = './resources/partitioned-cookies-'
+ const absolute_scope = new URL(scope, window.location).href;
+
+ // Set a Partitioned cookie.
+ document.cookie = '__Host-partitioned=123; Secure; Path=/; SameSite=None; Partitioned;';
+ assert_true(document.cookie.includes('__Host-partitioned=123'));
+
+ // Set an unpartitioned cookie.
+ document.cookie = 'unpartitioned=456; Secure; Path=/; SameSite=None;';
+ assert_true(document.cookie.includes('unpartitioned=456'));
+
+ const reg = await service_worker_unregister_and_register(t, script, scope);
+ await wait_for_state(t, reg.installing, 'activated');
+ t.add_cleanup(() => reg.unregister());
+
+ // on_message will be reassigned below based on the expected reply from the service worker.
+ let on_message;
+ self.addEventListener('message', ev => on_message(ev));
+ navigator.serviceWorker.addEventListener('message', evt => {
+ self.postMessage(evt.data, '*');
+ });
+
+ const retrieved_registrations =
+ await navigator.serviceWorker.getRegistrations();
+ // It's possible that other tests have left behind other service workers.
+ // This steps filters those other SWs out.
+ const filtered_registrations =
+ retrieved_registrations.filter(reg => reg.scope == absolute_scope);
+
+ // First test that the worker script started correctly and message passing is enabed.
+ let resolve_wait_promise;
+ let wait_promise = new Promise(resolve => {
+ resolve_wait_promise = resolve;
+ });
+ let got;
+ on_message = ev => {
+ got = ev.data;
+ resolve_wait_promise();
+ };
+ filtered_registrations[0].active.postMessage({type: 'test_message'});
+ await wait_promise;
+ assert_true(got.ok, 'Message passing');
+
+ // Test that the partitioned cookie is available to this worker via HTTP.
+ wait_promise = new Promise(resolve => {
+ resolve_wait_promise = resolve;
+ });
+ on_message = ev => {
+ got = ev.data;
+ resolve_wait_promise();
+ };
+ filtered_registrations[0].active.postMessage({type: 'echo_cookies_http'});
+ await wait_promise;
+ assert_true(got.ok, 'Get cookies');
+ assert_true(got.cookies.includes('__Host-partitioned'), 'Can access partitioned cookie via HTTP');
+ assert_true(got.cookies.includes('unpartitioned'), 'Can access unpartitioned cookie via HTTP');
+
+ // Test that the partitioned cookie is available to this worker via CookieStore API.
+ wait_promise = new Promise(resolve => {
+ resolve_wait_promise = resolve;
+ });
+ on_message = ev => {
+ got = ev.data;
+ resolve_wait_promise();
+ };
+ filtered_registrations[0].active.postMessage({type: 'echo_cookies_js'});
+ await wait_promise;
+ assert_true(got.ok, 'Get cookies');
+ assert_true(got.cookies.includes('__Host-partitioned'), 'Can access partitioned cookie via JS');
+ assert_true(got.cookies.includes('unpartitioned'), 'Can access unpartitioned cookie via JS');
+
+ // Test that the partitioned cookie is not available to this worker in HTTP
+ // requests from importScripts.
+ wait_promise = new Promise(resolve => {
+ resolve_wait_promise = resolve;
+ });
+ on_message = ev => {
+ got = ev.data;
+ resolve_wait_promise();
+ };
+ filtered_registrations[0].active.postMessage({type: 'echo_cookies_import'});
+ await wait_promise;
+ assert_true(got.ok, 'Get cookies');
+ assert_true(got.cookies.includes('__Host-partitioned'), 'Can access partitioned cookie via importScripts');
+ assert_true(got.cookies.includes('unpartitioned'), 'Can access unpartitioned cookie via importScripts');
+
+ const popup = window.open(
+ new URL(
+ `./resources/partitioned-cookies-3p-window.html?origin=${
+ encodeURIComponent(self.location.origin)}`,
+ get_host_info().HTTPS_NOTSAMESITE_ORIGIN + self.location.pathname));
+ await fetch_tests_from_window(popup);
+});
+
+</script>
+</body>
+</html>
diff --git a/test/wpt/tests/service-workers/service-worker/partitioned-getRegistrations.tentative.https.html b/test/wpt/tests/service-workers/service-worker/partitioned-getRegistrations.tentative.https.html
new file mode 100644
index 0000000..7c4d4f1
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/partitioned-getRegistrations.tentative.https.html
@@ -0,0 +1,99 @@
+<!DOCTYPE html>
+<meta charset="utf-8"/>
+<title>Service Worker: Partitioned Service Workers</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/partitioned-utils.js"></script>
+
+<body>
+This test loads a SW in a first-party context and gets the SW's (randomly)
+generated ID. It does the same thing for the SW but in a third-party context
+and then confirms that the IDs are different.
+
+<script>
+promise_test(async t => {
+ const script = './resources/partitioned-storage-sw.js'
+ const scope = './resources/partitioned-'
+ const absoluteScope = new URL(scope, window.location).href;
+
+ // Add service worker to this 1P context.
+ const reg = await service_worker_unregister_and_register(t, script, scope);
+ t.add_cleanup(() => reg.unregister());
+ await wait_for_state(t, reg.installing, 'activated');
+
+ // Register the message listener.
+ self.addEventListener('message', messageEventHandler);
+
+ // Open an iframe that will create a promise within the SW.
+ // The query param is there to track which request the service worker is
+ // handling.
+ //
+ // This promise is necessary to prevent the service worker from being
+ // shutdown during the test which would cause a new ID to be generated
+ // and thus invalidate the test.
+ const wait_frame_url = new URL(
+ './resources/partitioned-waitUntilResolved.fakehtml?From1pFrame',
+ self.location);
+
+ // We don't really need the data the SW sent us from this request
+ // but we can use the ID to confirm the SW wasn't shut down during the
+ // test.
+ const wait_frame_1p_data = await loadAndReturnSwData(t, wait_frame_url,
+ 'iframe');
+
+ // Now we need to create a third-party iframe that will send us its SW's
+ // ID.
+ const third_party_iframe_url = new URL(
+ './resources/partitioned-service-worker-third-party-iframe-getRegistrations.html',
+ get_host_info().HTTPS_ORIGIN + self.location.pathname);
+
+ // Create the 3p window (which will in turn create the iframe with the SW)
+ // and await on its data.
+ const frame_3p_ID = await loadAndReturnSwData(t, third_party_iframe_url,
+ 'window');
+
+ // Now get this frame's SW's ID.
+ const frame_1p_ID_promise = makeMessagePromise();
+
+ const retrieved_registrations =
+ await navigator.serviceWorker.getRegistrations();
+ // It's possible that other tests have left behind other service workers.
+ // This steps filters those other SWs out.
+ const filtered_registrations =
+ retrieved_registrations.filter(reg => reg.scope == absoluteScope);
+
+ // Register a listener on the service worker container and then forward to
+ // the self event listener so we can reuse the existing message promise
+ // function.
+ navigator.serviceWorker.addEventListener('message', evt => {
+ self.postMessage(evt.data, '*');
+ });
+
+ filtered_registrations[0].active.postMessage({type: "get-id"});
+
+ const frame_1p_ID = await frame_1p_ID_promise;
+
+ // First check that the SW didn't shutdown during the run of the test.
+ // (Note: We're not using assert_equals because random values make it
+ // difficult to use a test expectations file.)
+ assert_true(wait_frame_1p_data.ID === frame_1p_ID.ID,
+ "1p SW didn't shutdown");
+ // Now check that the 1p and 3p IDs differ.
+ assert_false(frame_1p_ID.ID === frame_3p_ID.ID,
+ "1p SW ID matches 3p SW ID");
+
+ // Finally, for clean up, resolve the SW's promise so it stops waiting.
+ const resolve_frame_url = new URL(
+ './resources/partitioned-resolve.fakehtml?From1pFrame', self.location);
+
+ // We don't care about the data.
+ await loadAndReturnSwData(t, resolve_frame_url, 'iframe');
+
+}, "ServiceWorker's getRegistrations() is partitioned");
+
+
+</script>
+
+</body> \ No newline at end of file
diff --git a/test/wpt/tests/service-workers/service-worker/partitioned-matchAll.tentative.https.html b/test/wpt/tests/service-workers/service-worker/partitioned-matchAll.tentative.https.html
new file mode 100644
index 0000000..46beec8
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/partitioned-matchAll.tentative.https.html
@@ -0,0 +1,65 @@
+<!DOCTYPE html>
+<meta charset="utf-8"/>
+<title>Service Worker: Partitioned Service Workers</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/partitioned-utils.js"></script>
+
+<body>
+This test loads a SW in a first-party context and gets has the SW send
+its list of clients from client.matchAll(). It does the same thing for the
+SW in a third-party context as well and confirms that each SW see's the correct
+clients and that they don't see eachother's clients.
+
+<script>
+promise_test(async t => {
+
+ const script = './resources/partitioned-storage-sw.js'
+ const scope = './resources/partitioned-'
+
+ // Add service worker to this 1P context.
+ const reg = await service_worker_unregister_and_register(t, script, scope);
+ t.add_cleanup(() => reg.unregister());
+ await wait_for_state(t, reg.installing, 'activated');
+
+ // Register the message listener.
+ self.addEventListener('message', messageEventHandler);
+
+ // Create a third-party iframe that will send us its SW's clients.
+ const third_party_iframe_url = new URL(
+ './resources/partitioned-service-worker-third-party-iframe-matchAll.html',
+ get_host_info().HTTPS_ORIGIN + self.location.pathname);
+
+ const {urls_list: frame_3p_urls_list} = await loadAndReturnSwData(t,
+ third_party_iframe_url, 'window');
+
+ // Register a listener on the service worker container and then forward to
+ // the self event listener so we can reuse the existing message promise
+ // function.
+ navigator.serviceWorker.addEventListener('message', evt => {
+ self.postMessage(evt.data, '*');
+ });
+
+ const frame_1p_data_promise = makeMessagePromise();
+
+ reg.active.postMessage({type: "get-match-all"});
+
+ const {urls_list: frame_1p_urls_list} = await frame_1p_data_promise;
+
+ // If partitioning is working, the 1p and 3p SWs should only see a single
+ // client.
+ assert_equals(frame_3p_urls_list.length, 1);
+ assert_equals(frame_1p_urls_list.length, 1);
+ // Confirm that the expected URL was seen by each.
+ assert_equals(frame_3p_urls_list[0], third_party_iframe_url.toString(),
+ "3p SW has the correct client url.");
+ assert_equals(frame_1p_urls_list[0], window.location.href,
+ "1P SW has the correct client url.");
+}, "ServiceWorker's matchAll() is partitioned");
+
+
+</script>
+
+</body> \ No newline at end of file
diff --git a/test/wpt/tests/service-workers/service-worker/partitioned.tentative.https.html b/test/wpt/tests/service-workers/service-worker/partitioned.tentative.https.html
new file mode 100644
index 0000000..17a375f
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/partitioned.tentative.https.html
@@ -0,0 +1,188 @@
+<!DOCTYPE html>
+<meta charset="utf-8"/>
+<title>Service Worker: Partitioned Service Workers</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/partitioned-utils.js"></script>
+
+<body>
+ <!-- Debugging text for both test cases -->
+ The 3p iframe's postMessage:
+ <p id="iframe_response">No message received</p>
+
+ The nested iframe's postMessage:
+ <p id="nested_iframe_response">No message received</p>
+
+<script>
+promise_test(async t => {
+ const script = './resources/partitioned-storage-sw.js'
+ const scope = './resources/partitioned-'
+
+ // Add service worker to this 1P context. wait_for_state() and
+ // service_worker_unregister_and_register() are helper functions
+ // for creating test ServiceWorkers defined in:
+ // service-workers/service-worker/resources/test-helpers.sub.js
+ const reg = await service_worker_unregister_and_register(t, script, scope);
+ t.add_cleanup(() => reg.unregister());
+ await wait_for_state(t, reg.installing, 'activated');
+
+ // Registers the message listener with messageEventHandler(), defined in:
+ // service-workers/service-worker/resources/partitioned-utils.js
+ self.addEventListener('message', messageEventHandler);
+
+ // Open an iframe that will create a promise within the SW.
+ // Defined in service-workers/service-worker/resources/partitioned-storage-sw.js:
+ // `waitUntilResolved.fakehtml`: URL scope that creates the promise.
+ // `?From1pFrame`: query param that tracks which request the service worker is
+ // handling.
+ const wait_frame_url = new URL(
+ './resources/partitioned-waitUntilResolved.fakehtml?From1pFrame',
+ self.location);
+
+ // Loads a child iframe with wait_frame_url as the content and returns
+ // a promise for the data messaged from the loaded iframe.
+ // loadAndReturnSwData() defined in:
+ // service-workers/service-worker/resources/partitioned-utils.js:
+ const wait_frame_1p_data = await loadAndReturnSwData(t, wait_frame_url,
+ 'iframe');
+ assert_equals(wait_frame_1p_data.source, 'From1pFrame',
+ 'The data for the 1p frame came from the wrong source');
+
+ // Now create a 3p iframe that will try to resolve the SW in a 3p context.
+ const third_party_iframe_url = new URL(
+ './resources/partitioned-service-worker-third-party-iframe.html',
+ get_host_info().HTTPS_ORIGIN + self.location.pathname);
+
+ // loadAndReturnSwData() creates a HTTPS_NOTSAMESITE_ORIGIN or 3p `window`
+ // element which embeds an iframe with the ServiceWorker and returns
+ // a promise of the data messaged from that frame.
+ const frame_3p_data = await loadAndReturnSwData(t, third_party_iframe_url, 'window');
+ assert_equals(frame_3p_data.source, 'From3pFrame',
+ 'The data for the 3p frame came from the wrong source');
+
+ // Print some debug info to the main frame.
+ document.getElementById("iframe_response").innerHTML =
+ "3p iframe's has_pending: " + frame_3p_data.has_pending + " source: " +
+ frame_3p_data.source + ". ";
+
+ // Now do the same for the 1p iframe.
+ // Defined in service-workers/service-worker/resources/partitioned-storage-sw.js:
+ // `resolve.fakehtml`: URL scope that resolves the promise.
+ const resolve_frame_url = new URL(
+ './resources/partitioned-resolve.fakehtml?From1pFrame', self.location);
+
+ const frame_1p_data = await loadAndReturnSwData(t, resolve_frame_url,
+ 'iframe');
+ assert_equals(frame_1p_data.source, 'From1pFrame',
+ 'The data for the 1p frame came from the wrong source');
+ // Both the 1p frames should have been serviced by the same service worker ID.
+ // If this isn't the case then that means the SW could have been deactivated
+ // which invalidates the test.
+ assert_equals(frame_1p_data.ID, wait_frame_1p_data.ID,
+ 'The 1p frames were serviced by different service workers.');
+
+ document.getElementById("iframe_response").innerHTML +=
+ "1p iframe's has_pending: " + frame_1p_data.has_pending + " source: " +
+ frame_1p_data.source;
+
+ // If partitioning is working correctly then only the 1p iframe should see
+ // (and resolve) its SW's promise. Additionally the two frames should see
+ // different IDs.
+ assert_true(frame_1p_data.has_pending,
+ 'The 1p iframe saw a pending promise in the service worker.');
+ assert_false(frame_3p_data.has_pending,
+ 'The 3p iframe saw a pending promise in the service worker.');
+ assert_not_equals(frame_1p_data.ID, frame_3p_data.ID,
+ 'The frames were serviced by the same service worker thread.');
+}, 'Services workers under different top-level sites are partitioned.');
+
+// Optional Test: Checking for partitioned ServiceWorkers in an A->B->A
+// (nested-iframes with cross-site ancestor) scenario.
+promise_test(async t => {
+ const script = './resources/partitioned-storage-sw.js'
+ const scope = './resources/partitioned-'
+
+ // Add service worker to this 1P context. wait_for_state() and
+ // service_worker_unregister_and_register() are helper functions
+ // for creating test ServiceWorkers defined in:
+ // service-workers/service-worker/resources/test-helpers.sub.js
+ const reg = await service_worker_unregister_and_register(t, script, scope);
+ t.add_cleanup(() => reg.unregister());
+ await wait_for_state(t, reg.installing, 'activated');
+
+ // Registers the message listener with messageEventHandler(), defined in:
+ // service-workers/service-worker/resources/partitioned-utils.js
+ self.addEventListener('message', messageEventHandler);
+
+ // Open an iframe that will create a promise within the SW.
+ // Defined in service-workers/service-worker/resources/partitioned-storage-sw.js:
+ // `waitUntilResolved.fakehtml`: URL scope that creates the promise.
+ // `?From1pFrame`: query param that tracks which request the service worker is
+ // handling.
+ const wait_frame_url = new URL(
+ './resources/partitioned-waitUntilResolved.fakehtml?From1pFrame',
+ self.location);
+
+ // Load a child iframe with wait_frame_url as the content.
+ // loadAndReturnSwData() defined in:
+ // service-workers/service-worker/resources/partitioned-utils.js:
+ const wait_frame_1p_data = await loadAndReturnSwData(t, wait_frame_url,
+ 'iframe');
+ assert_equals(wait_frame_1p_data.source, 'From1pFrame',
+ 'The data for the 1p frame came from the wrong source');
+
+ // Now create a set of nested iframes in the configuration A1->B->A2
+ // where B is cross-site and A2 is same-site to this top-level
+ // site (A1). The innermost iframe of the nested iframes (A2) will
+ // create an additional iframe to finally resolve the ServiceWorker.
+ const nested_iframe_url = new URL(
+ './resources/partitioned-service-worker-nested-iframe-parent.html',
+ get_host_info().HTTPS_NOTSAMESITE_ORIGIN + self.location.pathname);
+
+ // Create the nested iframes (which will in turn create the iframe
+ // with the ServiceWorker) and await on receiving its data.
+ const nested_iframe_data = await loadAndReturnSwData(t, nested_iframe_url, 'iframe');
+ assert_equals(nested_iframe_data.source, 'FromNestedFrame',
+ 'The data for the nested iframe frame came from the wrong source');
+
+ // Print some debug info to the main frame.
+ document.getElementById("nested_iframe_response").innerHTML =
+ "Nested iframe's has_pending: " + nested_iframe_data.has_pending + " source: " +
+ nested_iframe_data.source + ". ";
+
+ // Now do the same for the 1p iframe.
+ // Defined in service-workers/service-worker/resources/partitioned-storage-sw.js:
+ // `resolve.fakehtml`: URL scope that resolves the promise.
+ const resolve_frame_url = new URL(
+ './resources/partitioned-resolve.fakehtml?From1pFrame', self.location);
+
+ const frame_1p_data = await loadAndReturnSwData(t, resolve_frame_url,
+ 'iframe');
+ assert_equals(frame_1p_data.source, 'From1pFrame',
+ 'The data for the 1p frame came from the wrong source');
+ // Both the 1p frames should have been serviced by the same service worker ID.
+ // If this isn't the case then that means the SW could have been deactivated
+ // which invalidates the test.
+ assert_equals(frame_1p_data.ID, wait_frame_1p_data.ID,
+ 'The 1p frames were serviced by different service workers.');
+
+ document.getElementById("nested_iframe_response").innerHTML +=
+ "1p iframe's has_pending: " + frame_1p_data.has_pending + " source: " +
+ frame_1p_data.source;
+
+ // If partitioning is working correctly then only the 1p iframe should see
+ // (and resolve) its SW's promise. Additionally, the innermost iframe of
+ // the nested iframes (A2 in the configuration A1->B->A2) should have a
+ // different service worker ID than the 1p (A1) frame.
+ assert_true(frame_1p_data.has_pending,
+ 'The 1p iframe saw a pending promise in the service worker.');
+ assert_false(nested_iframe_data.has_pending,
+ 'The 3p iframe saw a pending promise in the service worker.');
+ assert_not_equals(frame_1p_data.ID, nested_iframe_data.ID,
+ 'The frames were serviced by the same service worker thread.');
+}, 'Services workers with cross-site ancestors are partitioned.');
+
+</script>
+</body> \ No newline at end of file
diff --git a/test/wpt/tests/service-workers/service-worker/performance-timeline.https.html b/test/wpt/tests/service-workers/service-worker/performance-timeline.https.html
new file mode 100644
index 0000000..e56e6fe
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/performance-timeline.https.html
@@ -0,0 +1,49 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+
+service_worker_test(
+ 'resources/performance-timeline-worker.js',
+ 'Test Performance Timeline API in Service Worker');
+
+// The purpose of this test is to verify that service worker overhead
+// is included in the Performance API's timing information.
+promise_test(t => {
+ let script = 'resources/empty-but-slow-worker.js';
+ let scope = 'resources/sample.txt?slow-sw-timing';
+ let url = new URL(scope, window.location).href;
+ let slowURL = url + '&slow';
+ let frame;
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(reg => {
+ t.add_cleanup(() => service_worker_unregister(t, scope));
+
+ return wait_for_state(t, reg.installing, 'activated');
+ })
+ .then(_ => with_iframe(scope))
+ .then(f => {
+ frame = f;
+ return frame.contentWindow.fetch(url).then(r => r && r.text());
+ })
+ .then(_ => {
+ return frame.contentWindow.fetch(slowURL).then(r => r && r.text());
+ })
+ .then(_ => {
+ function elapsed(u) {
+ let entry = frame.contentWindow.performance.getEntriesByName(u);
+ return entry[0] ? entry[0].duration : undefined;
+ }
+ let urlTime = elapsed(url);
+ let slowURLTime = elapsed(slowURL);
+ // Verify the request slowed by the service worker is indeed measured
+ // to be slower. Note, we compare to smaller delay instead of the exact
+ // delay amount to avoid making the test racy under automation.
+ assert_greater_than(slowURLTime, urlTime + 1000,
+ 'Slow service worker request should measure increased delay.');
+ frame.remove();
+ })
+}, 'empty service worker fetch event included in performance timings');
+
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/postMessage-client-worker.js b/test/wpt/tests/service-workers/service-worker/postMessage-client-worker.js
new file mode 100644
index 0000000..64d944d
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/postMessage-client-worker.js
@@ -0,0 +1,23 @@
+async function doTest(e)
+{
+ if (e.resultingClientId) {
+ const promise = new Promise(async resolve => {
+ let counter = 0;
+ const client = await self.clients.get(e.resultingClientId);
+ if (client)
+ client.postMessage(counter++);
+ if (e.request.url.includes("repeatMessage")) {
+ setInterval(() => {
+ if (client)
+ client.postMessage(counter++);
+ }, 100);
+ }
+ setTimeout(() => {
+ resolve(fetch(e.request));
+ }, 1000);
+ });
+ e.respondWith(promise);
+ }
+}
+
+self.addEventListener("fetch", e => e.waitUntil(doTest(e)));
diff --git a/test/wpt/tests/service-workers/service-worker/postmessage-blob-url.https.html b/test/wpt/tests/service-workers/service-worker/postmessage-blob-url.https.html
new file mode 100644
index 0000000..16fddd5
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/postmessage-blob-url.https.html
@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<title>Service Worker: postMessage Blob URL</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+promise_test(t => {
+ let script = 'resources/postmessage-blob-url.js';
+ let scope = 'resources/blank.html';
+ let registration;
+ let blobText = 'Blob text';
+ let blob;
+ let blobUrl;
+
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(r => {
+ add_completion_callback(() => r.unregister());
+ registration = r;
+ let worker = registration.installing;
+ blob = new Blob([blobText]);
+ blobUrl = URL.createObjectURL(blob);
+ return new Promise(resolve => {
+ navigator.serviceWorker.onmessage = e => { resolve(e.data); }
+ worker.postMessage(blobUrl);
+ });
+ })
+ .then(response => {
+ assert_equals(response, 'Worker reply:' + blobText);
+ URL.revokeObjectURL(blobUrl);
+ return registration.unregister();
+ });
+ }, 'postMessage Blob URL to a ServiceWorker');
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/postmessage-from-waiting-serviceworker.https.html b/test/wpt/tests/service-workers/service-worker/postmessage-from-waiting-serviceworker.https.html
new file mode 100644
index 0000000..117def9
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/postmessage-from-waiting-serviceworker.https.html
@@ -0,0 +1,50 @@
+<!DOCTYPE html>
+<title>Service Worker: postMessage from waiting serviceworker</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+
+function echo(worker, data) {
+ return new Promise(resolve => {
+ navigator.serviceWorker.addEventListener('message', function onMsg(evt) {
+ navigator.serviceWorker.removeEventListener('message', onMsg);
+ resolve(evt);
+ });
+ worker.postMessage(data);
+ });
+}
+
+promise_test(t => {
+ let script = 'resources/echo-message-to-source-worker.js';
+ let scope = 'resources/client-postmessage-from-wait-serviceworker';
+ let registration;
+ let frame;
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(swr => {
+ t.add_cleanup(() => service_worker_unregister(t, scope));
+
+ registration = swr;
+ return wait_for_state(t, registration.installing, 'activated');
+ }).then(_ => {
+ return with_iframe(scope);
+ }).then(f => {
+ frame = f;
+ return navigator.serviceWorker.register(script + '?update', { scope: scope })
+ }).then(swr => {
+ assert_equals(swr, registration, 'should be same registration');
+ return wait_for_state(t, registration.installing, 'installed');
+ }).then(_ => {
+ return echo(registration.waiting, 'waiting');
+ }).then(evt => {
+ assert_equals(evt.source, registration.waiting,
+ 'message event source should be correct');
+ return echo(registration.active, 'active');
+ }).then(evt => {
+ assert_equals(evt.source, registration.active,
+ 'message event source should be correct');
+ frame.remove();
+ });
+}, 'Client.postMessage() from waiting serviceworker.');
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/postmessage-msgport-to-client.https.html b/test/wpt/tests/service-workers/service-worker/postmessage-msgport-to-client.https.html
new file mode 100644
index 0000000..29c0560
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/postmessage-msgport-to-client.https.html
@@ -0,0 +1,43 @@
+<!DOCTYPE html>
+<title>Service Worker: postMessage via MessagePort to Client</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+promise_test(t => {
+ var script = 'resources/postmessage-msgport-to-client-worker.js';
+ var scope = 'resources/blank.html';
+ var port;
+
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(registration => {
+ add_completion_callback(() => registration.unregister());
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(() => with_iframe(scope))
+ .then(frame => {
+ t.add_cleanup(() => frame.remove());
+ return new Promise(resolve => {
+ var w = frame.contentWindow;
+ w.navigator.serviceWorker.onmessage = resolve;
+ w.navigator.serviceWorker.controller.postMessage('ping');
+ });
+ })
+ .then(e => {
+ port = e.ports[0];
+ port.postMessage({value: 1});
+ port.postMessage({value: 2});
+ port.postMessage({done: true});
+ return new Promise(resolve => { port.onmessage = resolve; });
+ })
+ .then(e => {
+ assert_equals(e.data.ack, 'Acking value: 1');
+ return new Promise(resolve => { port.onmessage = resolve; });
+ })
+ .then(e => {
+ assert_equals(e.data.ack, 'Acking value: 2');
+ return new Promise(resolve => { port.onmessage = resolve; });
+ })
+ .then(e => { assert_true(e.data.done, 'done'); });
+ }, 'postMessage MessagePorts from ServiceWorker to Client');
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/postmessage-to-client-message-queue.https.html b/test/wpt/tests/service-workers/service-worker/postmessage-to-client-message-queue.https.html
new file mode 100644
index 0000000..83e5f45
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/postmessage-to-client-message-queue.https.html
@@ -0,0 +1,212 @@
+<!DOCTYPE html>
+<title>Service Worker: postMessage to Client (message queue)</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+// This function creates a message listener that captures all messages
+// sent to this window and matches them with corresponding requests.
+// This frees test code from having to use clunky constructs just to
+// avoid race conditions, since the relative order of message and
+// request arrival doesn't matter.
+function create_message_listener(t) {
+ const listener = {
+ messages: new Set(),
+ requests: new Set(),
+ waitFor: function(predicate) {
+ for (const event of this.messages) {
+ // If a message satisfying the predicate has already
+ // arrived, it gets matched to this request.
+ if (predicate(event)) {
+ this.messages.delete(event);
+ return Promise.resolve(event);
+ }
+ }
+
+ // If no match was found, the request is stored and a
+ // promise is returned.
+ const request = { predicate };
+ const promise = new Promise(resolve => request.resolve = resolve);
+ this.requests.add(request);
+ return promise;
+ }
+ };
+ window.onmessage = t.step_func(event => {
+ for (const request of listener.requests) {
+ // If the new message matches a stored request's
+ // predicate, the request's promise is resolved with this
+ // message.
+ if (request.predicate(event)) {
+ listener.requests.delete(request);
+ request.resolve(event);
+ return;
+ }
+ };
+
+ // No outstanding request for this message, store it in case
+ // it's requested later.
+ listener.messages.add(event);
+ });
+ return listener;
+}
+
+async function service_worker_register_and_activate(t, script, scope) {
+ const registration = await service_worker_unregister_and_register(t, script, scope);
+ t.add_cleanup(() => registration.unregister());
+ const worker = registration.installing;
+ await wait_for_state(t, worker, 'activated');
+ return worker;
+}
+
+// Add an iframe (parent) whose document contains a nested iframe
+// (child), then set the child's src attribute to child_url and return
+// its Window (without waiting for it to finish loading).
+async function with_nested_iframes(t, child_url) {
+ const parent = await with_iframe('resources/nested-iframe-parent.html?role=parent');
+ t.add_cleanup(() => parent.remove());
+ const child = parent.contentWindow.document.getElementById('child');
+ child.setAttribute('src', child_url);
+ return child.contentWindow;
+}
+
+// Returns a predicate matching a fetch message with the specified
+// key.
+function fetch_message(key) {
+ return event => event.data.type === 'fetch' && event.data.key === key;
+}
+
+// Returns a predicate matching a ping message with the specified
+// payload.
+function ping_message(data) {
+ return event => event.data.type === 'ping' && event.data.data === data;
+}
+
+// A client message queue test is a testharness.js test with some
+// additional setup:
+// 1. A listener (see create_message_listener)
+// 2. An active service worker
+// 3. Two nested iframes
+// 4. A state transition function that controls the order of events
+// during the test
+function client_message_queue_test(url, test_function, description) {
+ promise_test(async t => {
+ t.listener = create_message_listener(t);
+
+ const script = 'resources/stalling-service-worker.js';
+ const scope = 'resources/';
+ t.service_worker = await service_worker_register_and_activate(t, script, scope);
+
+ // We create two nested iframes such that both are controlled by
+ // the newly installed service worker.
+ const child_url = url + '?role=child';
+ t.frame = await with_nested_iframes(t, child_url);
+
+ t.state_transition = async function(from, to, scripts) {
+ // A state transition begins with the child's parser
+ // fetching a script due to a <script> tag. The request
+ // arrives at the service worker, which notifies the
+ // parent, which in turn notifies the test. Note that the
+ // event loop keeps spinning while the parser is waiting.
+ const request = await this.listener.waitFor(fetch_message(to));
+
+ // The test instructs the service worker to send two ping
+ // messages through the Client interface: first to the
+ // child, then to the parent.
+ this.service_worker.postMessage(from);
+
+ // When the parent receives the ping message, it forwards
+ // it to the test. Assuming that messages to both child
+ // and parent are mapped to the same task queue (this is
+ // not [yet] required by the spec), receiving this message
+ // guarantees that the child has already dispatched its
+ // message if it was allowed to do so.
+ await this.listener.waitFor(ping_message(from));
+
+ // Finally, reply to the service worker's fetch
+ // notification with the script it should use as the fetch
+ // request's response. This is a defensive mechanism that
+ // ensures the child's parser really is blocked until the
+ // test is ready to continue.
+ request.ports[0].postMessage([`state = '${to}';`].concat(scripts));
+ };
+
+ await test_function(t);
+ }, description);
+}
+
+function client_message_queue_enable_test(
+ install_script,
+ start_script,
+ earliest_dispatch,
+ description)
+{
+ function assert_state_less_than_equal(state1, state2, explanation) {
+ const states = ['init', 'install', 'start', 'finish', 'loaded'];
+ const index1 = states.indexOf(state1);
+ const index2 = states.indexOf(state2);
+ if (index1 > index2)
+ assert_unreached(explanation);
+ }
+
+ client_message_queue_test('enable-client-message-queue.html', async t => {
+ // While parsing the child's document, the child transitions
+ // from the 'init' state all the way to the 'finish' state.
+ // Once parsing is finished it would enter the final 'loaded'
+ // state. All but the last transition require assitance from
+ // the test.
+ await t.state_transition('init', 'install', [install_script]);
+ await t.state_transition('install', 'start', [start_script]);
+ await t.state_transition('start', 'finish', []);
+
+ // Wait for all messages to get dispatched on the child's
+ // ServiceWorkerContainer and then verify that each message
+ // was dispatched after |earliest_dispatch|.
+ const report = await t.frame.report;
+ ['init', 'install', 'start'].forEach(state => {
+ const explanation = `Message sent in state '${state}' was dispatched in '${report[state]}', should be dispatched no earlier than '${earliest_dispatch}'`;
+ assert_state_less_than_equal(earliest_dispatch,
+ report[state],
+ explanation);
+ });
+ }, description);
+}
+
+const empty_script = ``;
+
+const add_event_listener =
+ `navigator.serviceWorker.addEventListener('message', handle_message);`;
+
+const set_onmessage = `navigator.serviceWorker.onmessage = handle_message;`;
+
+const start_messages = `navigator.serviceWorker.startMessages();`;
+
+client_message_queue_enable_test(add_event_listener, empty_script, 'loaded',
+ 'Messages from ServiceWorker to Client only received after DOMContentLoaded event.');
+
+client_message_queue_enable_test(add_event_listener, start_messages, 'start',
+ 'Messages from ServiceWorker to Client only received after calling startMessages().');
+
+client_message_queue_enable_test(set_onmessage, empty_script, 'install',
+ 'Messages from ServiceWorker to Client only received after setting onmessage.');
+
+const resolve_manual_promise = `resolve_manual_promise();`
+
+async function test_microtasks_when_client_message_queue_enabled(t, scripts) {
+ await t.state_transition('init', 'start', scripts.concat([resolve_manual_promise]));
+ let result = await t.frame.result;
+ assert_equals(result[0], 'microtask', 'The microtask was executed first.');
+ assert_equals(result[1], 'message', 'The message was dispatched.');
+}
+
+client_message_queue_test('message-vs-microtask.html', t => {
+ return test_microtasks_when_client_message_queue_enabled(t, [
+ add_event_listener,
+ start_messages,
+ ]);
+}, 'Microtasks run before dispatching messages after calling startMessages().');
+
+client_message_queue_test('message-vs-microtask.html', t => {
+ return test_microtasks_when_client_message_queue_enabled(t, [set_onmessage]);
+}, 'Microtasks run before dispatching messages after setting onmessage.');
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/postmessage-to-client.https.html b/test/wpt/tests/service-workers/service-worker/postmessage-to-client.https.html
new file mode 100644
index 0000000..f834a4b
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/postmessage-to-client.https.html
@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<title>Service Worker: postMessage to Client</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+promise_test(async t => {
+ const script = 'resources/postmessage-to-client-worker.js';
+ const scope = 'resources/blank.html';
+
+ const registration =
+ await service_worker_unregister_and_register(t, script, scope);
+ t.add_cleanup(() => registration.unregister());
+ await wait_for_state(t, registration.installing, 'activated');
+ const frame = await with_iframe(scope);
+ t.add_cleanup(() => frame.remove());
+ const w = frame.contentWindow;
+
+ w.navigator.serviceWorker.controller.postMessage('ping');
+ let e = await new Promise(r => w.navigator.serviceWorker.onmessage = r);
+
+ assert_equals(e.constructor, w.MessageEvent,
+ 'message events should use MessageEvent interface.');
+ assert_equals(e.type, 'message', 'type should be "message".');
+ assert_false(e.bubbles, 'message events should not bubble.');
+ assert_false(e.cancelable, 'message events should not be cancelable.');
+ assert_equals(e.origin, location.origin,
+ 'origin of message should be origin of Service Worker.');
+ assert_equals(e.lastEventId, '',
+ 'lastEventId should be an empty string.');
+ assert_equals(e.source.constructor, w.ServiceWorker,
+ 'source should use ServiceWorker interface.');
+ assert_equals(e.source, w.navigator.serviceWorker.controller,
+ 'source should be the service worker that sent the message.');
+ assert_equals(e.ports.length, 0, 'ports should be an empty array.');
+ assert_equals(e.data, 'Sending message via clients');
+
+ e = await new Promise(r => w.navigator.serviceWorker.onmessage = r);
+ assert_equals(e.data, 'quit');
+}, 'postMessage from ServiceWorker to Client.');
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/postmessage.https.html b/test/wpt/tests/service-workers/service-worker/postmessage.https.html
new file mode 100644
index 0000000..7abb302
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/postmessage.https.html
@@ -0,0 +1,202 @@
+<!DOCTYPE html>
+<title>Service Worker: postMessage</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+promise_test(t => {
+ var script = 'resources/postmessage-worker.js';
+ var scope = 'resources/blank.html';
+ var registration;
+ var worker;
+ var port;
+
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(r => {
+ t.add_cleanup(() => r.unregister());
+ registration = r;
+ worker = registration.installing;
+
+ var messageChannel = new MessageChannel();
+ port = messageChannel.port1;
+ return new Promise(resolve => {
+ port.onmessage = resolve;
+ worker.postMessage({port: messageChannel.port2},
+ [messageChannel.port2]);
+ worker.postMessage({value: 1});
+ worker.postMessage({value: 2});
+ worker.postMessage({done: true});
+ });
+ })
+ .then(e => {
+ assert_equals(e.data, 'Acking value: 1');
+ return new Promise(resolve => { port.onmessage = resolve; });
+ })
+ .then(e => {
+ assert_equals(e.data, 'Acking value: 2');
+ return new Promise(resolve => { port.onmessage = resolve; });
+ })
+ .then(e => {
+ assert_equals(e.data, 'quit');
+ return registration.unregister(scope);
+ });
+ }, 'postMessage to a ServiceWorker (and back via MessagePort)');
+
+promise_test(t => {
+ var script = 'resources/postmessage-transferables-worker.js';
+ var scope = 'resources/blank.html';
+ var sw = navigator.serviceWorker;
+
+ var message = 'Hello, world!';
+ var text_encoder = new TextEncoder;
+ var text_decoder = new TextDecoder;
+
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(r => {
+ t.add_cleanup(() => r.unregister());
+
+ var ab = text_encoder.encode(message);
+ assert_equals(ab.byteLength, message.length);
+ r.installing.postMessage(ab, [ab.buffer]);
+ assert_equals(text_decoder.decode(ab), '');
+ assert_equals(ab.byteLength, 0);
+
+ return new Promise(resolve => { sw.onmessage = resolve; });
+ })
+ .then(e => {
+ // Verify the integrity of the transferred array buffer.
+ assert_equals(e.data.content, message);
+ assert_equals(e.data.byteLength, message.length);
+ return new Promise(resolve => { sw.onmessage = resolve; });
+ })
+ .then(e => {
+ // Verify the integrity of the array buffer sent back from
+ // ServiceWorker via Client.postMessage.
+ assert_equals(text_decoder.decode(e.data), message);
+ assert_equals(e.data.byteLength, message.length);
+ return new Promise(resolve => { sw.onmessage = resolve; });
+ })
+ .then(e => {
+ // Verify that the array buffer on ServiceWorker is neutered.
+ assert_equals(e.data.content, '');
+ assert_equals(e.data.byteLength, 0);
+ });
+ }, 'postMessage a transferable ArrayBuffer between ServiceWorker and Client');
+
+promise_test(t => {
+ var script = 'resources/postmessage-transferables-worker.js';
+ var scope = 'resources/blank.html';
+ var message = 'Hello, world!';
+ var text_encoder = new TextEncoder;
+ var text_decoder = new TextDecoder;
+ var port;
+
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(r => {
+ t.add_cleanup(() => r.unregister());
+
+ var channel = new MessageChannel;
+ port = channel.port1;
+ r.installing.postMessage(undefined, [channel.port2]);
+
+ var ab = text_encoder.encode(message);
+ assert_equals(ab.byteLength, message.length);
+ port.postMessage(ab, [ab.buffer]);
+ assert_equals(text_decoder.decode(ab), '');
+ assert_equals(ab.byteLength, 0);
+
+ return new Promise(resolve => { port.onmessage = resolve; });
+ })
+ .then(e => {
+ // Verify the integrity of the transferred array buffer.
+ assert_equals(e.data.content, message);
+ assert_equals(e.data.byteLength, message.length);
+ return new Promise(resolve => { port.onmessage = resolve; });
+ })
+ .then(e => {
+ // Verify the integrity of the array buffer sent back from
+ // ServiceWorker via Client.postMessage.
+ assert_equals(text_decoder.decode(e.data), message);
+ assert_equals(e.data.byteLength, message.length);
+ return new Promise(resolve => { port.onmessage = resolve; });
+ })
+ .then(e => {
+ // Verify that the array buffer on ServiceWorker is neutered.
+ assert_equals(e.data.content, '');
+ assert_equals(e.data.byteLength, 0);
+ });
+ }, 'postMessage a transferable ArrayBuffer between ServiceWorker and Client' +
+ ' over MessagePort');
+
+ promise_test(t => {
+ var script = 'resources/postmessage-dictionary-transferables-worker.js';
+ var scope = 'resources/blank.html';
+ var sw = navigator.serviceWorker;
+
+ var message = 'Hello, world!';
+ var text_encoder = new TextEncoder;
+ var text_decoder = new TextDecoder;
+
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(r => {
+ t.add_cleanup(() => r.unregister());
+
+ var ab = text_encoder.encode(message);
+ assert_equals(ab.byteLength, message.length);
+ r.installing.postMessage(ab, {transfer: [ab.buffer]});
+ assert_equals(text_decoder.decode(ab), '');
+ assert_equals(ab.byteLength, 0);
+
+ return new Promise(resolve => { sw.onmessage = resolve; });
+ })
+ .then(e => {
+ // Verify the integrity of the transferred array buffer.
+ assert_equals(e.data.content, message);
+ assert_equals(e.data.byteLength, message.length);
+ return new Promise(resolve => { sw.onmessage = resolve; });
+ })
+ .then(e => {
+ // Verify the integrity of the array buffer sent back from
+ // ServiceWorker via Client.postMessage.
+ assert_equals(text_decoder.decode(e.data), message);
+ assert_equals(e.data.byteLength, message.length);
+ return new Promise(resolve => { sw.onmessage = resolve; });
+ })
+ .then(e => {
+ // Verify that the array buffer on ServiceWorker is neutered.
+ assert_equals(e.data.content, '');
+ assert_equals(e.data.byteLength, 0);
+ });
+ }, 'postMessage with dictionary a transferable ArrayBuffer between ServiceWorker and Client');
+
+ promise_test(async t => {
+ const firstScript = 'resources/postmessage-echo-worker.js?one';
+ const secondScript = 'resources/postmessage-echo-worker.js?two';
+ const scope = 'resources/';
+
+ const registration = await service_worker_unregister_and_register(t, firstScript, scope);
+ t.add_cleanup(() => registration.unregister());
+ const firstWorker = registration.installing;
+
+ const messagePromise = new Promise(resolve => {
+ navigator.serviceWorker.addEventListener('message', (event) => {
+ resolve(event.data);
+ }, {once: true});
+ });
+
+ await wait_for_state(t, firstWorker, 'activated');
+ await navigator.serviceWorker.register(secondScript, {scope});
+ const secondWorker = registration.installing;
+ await wait_for_state(t, firstWorker, 'redundant');
+
+ // postMessage() to a redundant worker should be dropped silently.
+ // Historically, this threw an exception.
+ firstWorker.postMessage('firstWorker');
+
+ // To test somewhat that it was not received, send a message to another
+ // worker and check that we get a reply for that one.
+ secondWorker.postMessage('secondWorker');
+ const data = await messagePromise;
+ assert_equals(data, 'secondWorker');
+ }, 'postMessage to a redundant worker');
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/ready.https.window.js b/test/wpt/tests/service-workers/service-worker/ready.https.window.js
new file mode 100644
index 0000000..6c4e270
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/ready.https.window.js
@@ -0,0 +1,223 @@
+// META: title=Service Worker: navigator.serviceWorker.ready
+// META: script=resources/test-helpers.sub.js
+
+test(() => {
+ assert_equals(
+ navigator.serviceWorker.ready,
+ navigator.serviceWorker.ready,
+ 'repeated access to ready without intervening registrations should return the same Promise object'
+ );
+}, 'ready returns the same Promise object');
+
+promise_test(async t => {
+ const frame = await with_iframe('resources/blank.html?uncontrolled');
+ t.add_cleanup(() => frame.remove());
+
+ const promise = frame.contentWindow.navigator.serviceWorker.ready;
+
+ assert_equals(
+ Object.getPrototypeOf(promise),
+ frame.contentWindow.Promise.prototype,
+ 'the Promise should be in the context of the related document'
+ );
+}, 'ready returns a Promise object in the context of the related document');
+
+promise_test(async t => {
+ const url = 'resources/empty-worker.js';
+ const scope = 'resources/blank.html?ready-controlled';
+ const expectedURL = normalizeURL(url);
+ const registration = await service_worker_unregister_and_register(t, url, scope);
+ t.add_cleanup(() => registration.unregister());
+
+ await wait_for_state(t, registration.installing, 'activated');
+
+ const frame = await with_iframe(scope);
+ t.add_cleanup(() => frame.remove());
+
+ const readyReg = await frame.contentWindow.navigator.serviceWorker.ready;
+
+ assert_equals(readyReg.installing, null, 'installing should be null');
+ assert_equals(readyReg.waiting, null, 'waiting should be null');
+ assert_equals(readyReg.active.scriptURL, expectedURL, 'active after ready should not be null');
+ assert_equals(
+ frame.contentWindow.navigator.serviceWorker.controller,
+ readyReg.active,
+ 'the controller should be the active worker'
+ );
+ assert_in_array(
+ readyReg.active.state,
+ ['activating', 'activated'],
+ '.ready should be resolved when the registration has an active worker'
+ );
+}, 'ready on a controlled document');
+
+promise_test(async t => {
+ const url = 'resources/empty-worker.js';
+ const scope = 'resources/blank.html?ready-potential-controlled';
+ const expected_url = normalizeURL(url);
+ const frame = await with_iframe(scope);
+ t.add_cleanup(() => frame.remove());
+
+ const registration = await navigator.serviceWorker.register(url, { scope });
+ t.add_cleanup(() => registration.unregister());
+
+ const readyReg = await frame.contentWindow.navigator.serviceWorker.ready;
+
+ assert_equals(readyReg.installing, null, 'installing should be null');
+ assert_equals(readyReg.waiting, null, 'waiting should be null.')
+ assert_equals(readyReg.active.scriptURL, expected_url, 'active after ready should not be null');
+ assert_in_array(
+ readyReg.active.state,
+ ['activating', 'activated'],
+ '.ready should be resolved when the registration has an active worker'
+ );
+ assert_equals(
+ frame.contentWindow.navigator.serviceWorker.controller,
+ null,
+ 'uncontrolled document should not have a controller'
+ );
+}, 'ready on a potential controlled document');
+
+promise_test(async t => {
+ const url = 'resources/empty-worker.js';
+ const scope = 'resources/blank.html?ready-installing';
+
+ await service_worker_unregister(t, scope);
+
+ const frame = await with_iframe(scope);
+ const promise = frame.contentWindow.navigator.serviceWorker.ready;
+ navigator.serviceWorker.register(url, { scope });
+ const registration = await promise;
+
+ t.add_cleanup(async () => {
+ await registration.unregister();
+ frame.remove();
+ });
+
+ assert_equals(registration.installing, null, 'installing should be null');
+ assert_equals(registration.waiting, null, 'waiting should be null');
+ assert_not_equals(registration.active, null, 'active after ready should not be null');
+ assert_in_array(
+ registration.active.state,
+ ['activating', 'activated'],
+ '.ready should be resolved when the registration has an active worker'
+ );
+}, 'ready on an iframe whose parent registers a new service worker');
+
+promise_test(async t => {
+ const scope = 'resources/register-iframe.html';
+ const frame = await with_iframe(scope);
+
+ const registration = await frame.contentWindow.navigator.serviceWorker.ready;
+
+ t.add_cleanup(async () => {
+ await registration.unregister();
+ frame.remove();
+ });
+
+ assert_equals(registration.installing, null, 'installing should be null');
+ assert_equals(registration.waiting, null, 'waiting should be null');
+ assert_not_equals(registration.active, null, 'active after ready should not be null');
+ assert_in_array(
+ registration.active.state,
+ ['activating', 'activated'],
+ '.ready should be resolved with "active worker"'
+ );
+ }, 'ready on an iframe that installs a new service worker');
+
+promise_test(async t => {
+ const url = 'resources/empty-worker.js';
+ const matchedScope = 'resources/blank.html?ready-after-match';
+ const longerMatchedScope = 'resources/blank.html?ready-after-match-longer';
+
+ await service_worker_unregister(t, matchedScope);
+ await service_worker_unregister(t, longerMatchedScope);
+
+ const frame = await with_iframe(longerMatchedScope);
+ const registration = await navigator.serviceWorker.register(url, { scope: matchedScope });
+
+ t.add_cleanup(async () => {
+ await registration.unregister();
+ frame.remove();
+ });
+
+ await wait_for_state(t, registration.installing, 'activated');
+
+ const longerRegistration = await navigator.serviceWorker.register(url, { scope: longerMatchedScope });
+
+ t.add_cleanup(() => longerRegistration.unregister());
+
+ const readyReg = await frame.contentWindow.navigator.serviceWorker.ready;
+
+ assert_equals(
+ readyReg.scope,
+ normalizeURL(longerMatchedScope),
+ 'longer matched registration should be returned'
+ );
+ assert_equals(
+ frame.contentWindow.navigator.serviceWorker.controller,
+ null,
+ 'controller should be null'
+ );
+}, 'ready after a longer matched registration registered');
+
+promise_test(async t => {
+ const url = 'resources/empty-worker.js';
+ const matchedScope = 'resources/blank.html?ready-after-resolve';
+ const longerMatchedScope = 'resources/blank.html?ready-after-resolve-longer';
+ const registration = await service_worker_unregister_and_register(t, url, matchedScope);
+ t.add_cleanup(() => registration.unregister());
+
+ await wait_for_state(t, registration.installing, 'activated');
+
+ const frame = await with_iframe(longerMatchedScope);
+ t.add_cleanup(() => frame.remove());
+
+ const readyReg1 = await frame.contentWindow.navigator.serviceWorker.ready;
+
+ assert_equals(
+ readyReg1.scope,
+ normalizeURL(matchedScope),
+ 'matched registration should be returned'
+ );
+
+ const longerReg = await navigator.serviceWorker.register(url, { scope: longerMatchedScope });
+ t.add_cleanup(() => longerReg.unregister());
+
+ const readyReg2 = await frame.contentWindow.navigator.serviceWorker.ready;
+
+ assert_equals(
+ readyReg2.scope,
+ normalizeURL(matchedScope),
+ 'ready should only be resolved once'
+ );
+}, 'access ready after it has been resolved');
+
+promise_test(async t => {
+ const url1 = 'resources/empty-worker.js';
+ const url2 = url1 + '?2';
+ const matchedScope = 'resources/blank.html?ready-after-unregister';
+ const reg1 = await service_worker_unregister_and_register(t, url1, matchedScope);
+ t.add_cleanup(() => reg1.unregister());
+
+ await wait_for_state(t, reg1.installing, 'activating');
+
+ const frame = await with_iframe(matchedScope);
+ t.add_cleanup(() => frame.remove());
+
+ await reg1.unregister();
+
+ // Ready promise should be pending, waiting for a new registration to arrive
+ const readyPromise = frame.contentWindow.navigator.serviceWorker.ready;
+
+ const reg2 = await navigator.serviceWorker.register(url2, { scope: matchedScope });
+ t.add_cleanup(() => reg2.unregister());
+
+ const readyReg = await readyPromise;
+
+ // Wait for registration update, since it comes from another global, the states are racy.
+ await wait_for_state(t, reg2.installing || reg2.waiting || reg2.active, 'activated');
+
+ assert_equals(readyReg.active.scriptURL, reg2.active.scriptURL, 'Resolves with the second registration');
+ assert_not_equals(reg1, reg2, 'Registrations should be different');
+}, 'resolve ready after unregistering');
diff --git a/test/wpt/tests/service-workers/service-worker/redirected-response.https.html b/test/wpt/tests/service-workers/service-worker/redirected-response.https.html
new file mode 100644
index 0000000..71b35d0
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/redirected-response.https.html
@@ -0,0 +1,471 @@
+<!DOCTYPE html>
+<title>Service Worker: Redirected response</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+// Tests redirect behavior. It calls fetch_method(url, fetch_option) and tests
+// the resulting response against the expected values. It also adds the
+// response to |cache| and checks the cached response matches the expected
+// values.
+//
+// |options|: a dictionary of parameters for the test
+// |options.url|: the URL to fetch
+// |options.fetch_option|: the options passed to |fetch_method|
+// |options.fetch_method|: the method used to fetch. Useful for testing an
+// iframe's fetch() vs. this page's fetch().
+// |options.expected_type|: The value of response.type
+// |options.expected_redirected|: The value of response.redirected
+// |options.expected_intercepted_urls|: The list of intercepted request URLs.
+function redirected_test(options) {
+ return options.fetch_method.call(null, options.url, options.fetch_option).then(response => {
+ let cloned_response = response.clone();
+ assert_equals(
+ response.type, options.expected_type,
+ 'The response type of response must match. URL: ' + options.url);
+ assert_equals(
+ cloned_response.type, options.expected_type,
+ 'The response type of cloned response must match. URL: ' + options.url);
+ assert_equals(
+ response.redirected, options.expected_redirected,
+ 'The redirected flag of response must match. URL: ' + options.url);
+ assert_equals(
+ cloned_response.redirected, options.expected_redirected,
+ 'The redirected flag of cloned response must match. URL: ' + options.url);
+ if (options.expected_response_url) {
+ assert_equals(
+ cloned_response.url, options.expected_response_url,
+ 'The URL does not meet expectation. URL: ' + options.url);
+ }
+ return cache.put(options.url, response);
+ })
+ .then(_ => cache.match(options.url))
+ .then(response => {
+ assert_equals(
+ response.type, options.expected_type,
+ 'The response type of response in CacheStorage must match. ' +
+ 'URL: ' + options.url);
+ assert_equals(
+ response.redirected, options.expected_redirected,
+ 'The redirected flag of response in CacheStorage must match. ' +
+ 'URL: ' + options.url);
+ return check_intercepted_urls(options.expected_intercepted_urls);
+ });
+}
+
+async function take_intercepted_urls() {
+ const message = new Promise((resolve) => {
+ let channel = new MessageChannel();
+ channel.port1.onmessage = msg => { resolve(msg.data.requestInfos); };
+ worker.postMessage({command: 'getRequestInfos', port: channel.port2},
+ [channel.port2]);
+ });
+ const request_infos = await message;
+ return request_infos.map(info => { return info.url; });
+}
+
+function check_intercepted_urls(expected_urls) {
+ return take_intercepted_urls().then((urls) => {
+ assert_object_equals(urls, expected_urls, 'Intercepted URLs matching.');
+ });
+}
+
+function setup_and_clean() {
+ // To prevent interference from previous tests, take the intercepted URLs from
+ // the service worker.
+ return setup.then(() => take_intercepted_urls());
+}
+
+
+let host_info = get_host_info();
+const REDIRECT_URL = host_info['HTTPS_ORIGIN'] + base_path() +
+ 'resources/redirect.py?Redirect=';
+const TARGET_URL = host_info['HTTPS_ORIGIN'] + base_path() +
+ 'resources/simple.txt?';
+const REDIRECT_TO_TARGET_URL = REDIRECT_URL + encodeURIComponent(TARGET_URL);
+let frame;
+let cache;
+let setup;
+let worker;
+
+promise_test(t => {
+ const SCOPE = 'resources/blank.html?redirected-response';
+ const SCRIPT = 'resources/redirect-worker.js';
+ const CACHE_NAME = 'service-workers/service-worker/redirected-response';
+ setup = service_worker_unregister_and_register(t, SCRIPT, SCOPE)
+ .then(registration => {
+ promise_test(
+ () => registration.unregister(),
+ 'restore global state (service worker registration)');
+ worker = registration.installing;
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(_ => self.caches.open(CACHE_NAME))
+ .then(c => {
+ cache = c;
+ promise_test(
+ () => self.caches.delete(CACHE_NAME),
+ 'restore global state (caches)');
+ return with_iframe(SCOPE);
+ })
+ .then(f => {
+ frame = f;
+ add_completion_callback(() => f.remove());
+ return check_intercepted_urls(
+ [host_info['HTTPS_ORIGIN'] + base_path() + SCOPE]);
+ });
+ return setup;
+ }, 'initialize global state (service worker registration and caches)');
+
+// ===============================================================
+// Tests for requests that are out-of-scope of the service worker.
+// ===============================================================
+promise_test(t => setup_and_clean()
+ .then(() => redirected_test({url: TARGET_URL,
+ fetch_option: {},
+ fetch_method: self.fetch,
+ expected_type: 'basic',
+ expected_redirected: false,
+ expected_intercepted_urls: []})),
+ 'mode: "follow", non-intercepted request, no server redirect');
+
+promise_test(t => setup_and_clean()
+ .then(() => redirected_test({url: REDIRECT_TO_TARGET_URL,
+ fetch_option: {},
+ fetch_method: self.fetch,
+ expected_type: 'basic',
+ expected_redirected: true,
+ expected_intercepted_urls: []})),
+ 'mode: "follow", non-intercepted request');
+
+promise_test(t => setup_and_clean()
+ .then(() => redirected_test({url: REDIRECT_TO_TARGET_URL + '&manual',
+ fetch_option: {redirect: 'manual'},
+ fetch_method: self.fetch,
+ expected_type: 'opaqueredirect',
+ expected_redirected: false,
+ expected_intercepted_urls: []})),
+ 'mode: "manual", non-intercepted request');
+
+promise_test(t => setup_and_clean()
+ .then(() => promise_rejects_js(
+ t, TypeError,
+ self.fetch(REDIRECT_TO_TARGET_URL + '&error',
+ {redirect:'error'}),
+ 'The redirect response from the server should be treated as' +
+ ' an error when the redirect flag of request was \'error\'.'))
+ .then(() => check_intercepted_urls([])),
+ 'mode: "error", non-intercepted request');
+
+promise_test(t => setup_and_clean()
+ .then(() => {
+ const url = TARGET_URL + '&sw=fetch';
+ return redirected_test({url: url,
+ fetch_option: {},
+ fetch_method: frame.contentWindow.fetch,
+ expected_type: 'basic',
+ expected_redirected: false,
+ expected_intercepted_urls: [url]})
+ }),
+ 'mode: "follow", no mode change, no server redirect');
+
+// =======================================================
+// Tests for requests that are in-scope of the service worker. The service
+// worker returns a redirected response.
+// =======================================================
+promise_test(t => setup_and_clean()
+ .then(() => {
+ const url = REDIRECT_TO_TARGET_URL +
+ '&original-redirect-mode=follow&sw=fetch';
+ return redirected_test({url: url,
+ fetch_option: {redirect: 'follow'},
+ fetch_method: frame.contentWindow.fetch,
+ expected_type: 'basic',
+ expected_redirected: true,
+ expected_intercepted_urls: [url]})
+ }),
+ 'mode: "follow", no mode change');
+
+promise_test(t => setup_and_clean()
+ .then(() => {
+ const url = REDIRECT_TO_TARGET_URL +
+ '&original-redirect-mode=error&sw=follow';
+ return promise_rejects_js(
+ t, frame.contentWindow.TypeError,
+ frame.contentWindow.fetch(url, {redirect: 'error'}),
+ 'The redirected response from the service worker should be ' +
+ 'treated as an error when the redirect flag of request was ' +
+ '\'error\'.')
+ .then(() => check_intercepted_urls([url]));
+ }),
+ 'mode: "error", mode change: "follow"');
+
+promise_test(t => setup_and_clean()
+ .then(() => {
+ const url = REDIRECT_TO_TARGET_URL +
+ '&original-redirect-mode=manual&sw=follow';
+ return promise_rejects_js(
+ t, frame.contentWindow.TypeError,
+ frame.contentWindow.fetch(url, {redirect: 'manual'}),
+ 'The redirected response from the service worker should be ' +
+ 'treated as an error when the redirect flag of request was ' +
+ '\'manual\'.')
+ .then(() => check_intercepted_urls([url]));
+ }),
+ 'mode: "manual", mode change: "follow"');
+
+// =======================================================
+// Tests for requests that are in-scope of the service worker. The service
+// worker returns an opaqueredirect response.
+// =======================================================
+promise_test(t => setup_and_clean()
+ .then(() => {
+ const url = REDIRECT_TO_TARGET_URL +
+ '&original-redirect-mode=follow&sw=manual';
+ return promise_rejects_js(
+ t, frame.contentWindow.TypeError,
+ frame.contentWindow.fetch(url, {redirect: 'follow'}),
+ 'The opaqueredirect response from the service worker should ' +
+ 'be treated as an error when the redirect flag of request was' +
+ ' \'follow\'.')
+ .then(() => check_intercepted_urls([url]));
+ }),
+ 'mode: "follow", mode change: "manual"');
+
+promise_test(t => setup_and_clean()
+ .then(() => {
+ const url = REDIRECT_TO_TARGET_URL +
+ '&original-redirect-mode=error&sw=manual';
+ return promise_rejects_js(
+ t, frame.contentWindow.TypeError,
+ frame.contentWindow.fetch(url, {redirect: 'error'}),
+ 'The opaqueredirect response from the service worker should ' +
+ 'be treated as an error when the redirect flag of request was' +
+ ' \'error\'.')
+ .then(() => check_intercepted_urls([url]));
+ }),
+ 'mode: "error", mode change: "manual"');
+
+promise_test(t => setup_and_clean()
+ .then(() => {
+ const url = REDIRECT_TO_TARGET_URL +
+ '&original-redirect-mode=manual&sw=manual';
+ return redirected_test({url: url,
+ fetch_option: {redirect: 'manual'},
+ fetch_method: frame.contentWindow.fetch,
+ expected_type: 'opaqueredirect',
+ expected_redirected: false,
+ expected_intercepted_urls: [url]});
+ }),
+ 'mode: "manual", no mode change');
+
+// =======================================================
+// Tests for requests that are in-scope of the service worker. The service
+// worker returns a generated redirect response.
+// =======================================================
+promise_test(t => setup_and_clean()
+ .then(() => {
+ const url = host_info['HTTPS_ORIGIN'] + base_path() +
+ 'sample?url=' + encodeURIComponent(TARGET_URL) +
+ '&original-redirect-mode=follow&sw=gen';
+ return redirected_test({url: url,
+ fetch_option: {redirect: 'follow'},
+ fetch_method: frame.contentWindow.fetch,
+ expected_type: 'basic',
+ expected_redirected: true,
+ expected_intercepted_urls: [url, TARGET_URL]})
+ }),
+ 'mode: "follow", generated redirect response');
+
+promise_test(t => setup_and_clean()
+ .then(() => {
+ const url = host_info['HTTPS_ORIGIN'] + base_path() +
+ 'sample?url=' + encodeURIComponent(TARGET_URL) +
+ '&original-redirect-mode=error&sw=gen';
+ return promise_rejects_js(
+ t, frame.contentWindow.TypeError,
+ frame.contentWindow.fetch(url, {redirect: 'error'}),
+ 'The generated redirect response from the service worker should ' +
+ 'be treated as an error when the redirect flag of request was' +
+ ' \'error\'.')
+ .then(() => check_intercepted_urls([url]));
+ }),
+ 'mode: "error", generated redirect response');
+
+promise_test(t => setup_and_clean()
+ .then(() => {
+ const url = host_info['HTTPS_ORIGIN'] + base_path() +
+ 'sample?url=' + encodeURIComponent(TARGET_URL) +
+ '&original-redirect-mode=manual&sw=gen';
+ return redirected_test({url: url,
+ fetch_option: {redirect: 'manual'},
+ fetch_method: frame.contentWindow.fetch,
+ expected_type: 'opaqueredirect',
+ expected_redirected: false,
+ expected_intercepted_urls: [url]})
+ }),
+ 'mode: "manual", generated redirect response');
+
+// =======================================================
+// Tests for requests that are in-scope of the service worker. The service
+// worker returns a generated redirect response manually with the Response
+// constructor.
+// =======================================================
+promise_test(t => setup_and_clean()
+ .then(() => {
+ const url = host_info['HTTPS_ORIGIN'] + base_path() +
+ 'sample?url=' + encodeURIComponent(TARGET_URL) +
+ '&original-redirect-mode=follow&sw=gen-manual';
+ return redirected_test({url: url,
+ fetch_option: {redirect: 'follow'},
+ fetch_method: frame.contentWindow.fetch,
+ expected_type: 'basic',
+ expected_redirected: true,
+ expected_intercepted_urls: [url, TARGET_URL]})
+ }),
+ 'mode: "follow", manually-generated redirect response');
+
+promise_test(t => setup_and_clean()
+ .then(() => {
+ const url = host_info['HTTPS_ORIGIN'] + base_path() +
+ 'sample?url=' + encodeURIComponent(TARGET_URL) +
+ '&original-redirect-mode=error&sw=gen-manual';
+ return promise_rejects_js(
+ t, frame.contentWindow.TypeError,
+ frame.contentWindow.fetch(url, {redirect: 'error'}),
+ 'The generated redirect response from the service worker should ' +
+ 'be treated as an error when the redirect flag of request was' +
+ ' \'error\'.')
+ .then(() => check_intercepted_urls([url]));
+ }),
+ 'mode: "error", manually-generated redirect response');
+
+promise_test(t => setup_and_clean()
+ .then(() => {
+ const url = host_info['HTTPS_ORIGIN'] + base_path() +
+ 'sample?url=' + encodeURIComponent(TARGET_URL) +
+ '&original-redirect-mode=manual&sw=gen-manual';
+ return redirected_test({url: url,
+ fetch_option: {redirect: 'manual'},
+ fetch_method: frame.contentWindow.fetch,
+ expected_type: 'opaqueredirect',
+ expected_redirected: false,
+ expected_intercepted_urls: [url]})
+ }),
+ 'mode: "manual", manually-generated redirect response');
+
+// =======================================================
+// Tests for requests that are in-scope of the service worker. The service
+// worker returns a generated redirect response with a relative location header.
+// Generated responses do not have URLs, so this should fail to resolve.
+// =======================================================
+promise_test(t => setup_and_clean()
+ .then(() => {
+ const url = host_info['HTTPS_ORIGIN'] + base_path() +
+ 'sample?url=blank.html' +
+ '&original-redirect-mode=follow&sw=gen-manual';
+ return promise_rejects_js(
+ t, frame.contentWindow.TypeError,
+ frame.contentWindow.fetch(url, {redirect: 'follow'}),
+ 'Following the generated redirect response from the service worker '+
+ 'should result fail.')
+ .then(() => check_intercepted_urls([url]));
+ }),
+ 'mode: "follow", generated relative redirect response');
+
+promise_test(t => setup_and_clean()
+ .then(() => {
+ const url = host_info['HTTPS_ORIGIN'] + base_path() +
+ 'sample?url=blank.html' +
+ '&original-redirect-mode=error&sw=gen-manual';
+ return promise_rejects_js(
+ t, frame.contentWindow.TypeError,
+ frame.contentWindow.fetch(url, {redirect: 'error'}),
+ 'The generated redirect response from the service worker should ' +
+ 'be treated as an error when the redirect flag of request was' +
+ ' \'error\'.')
+ .then(() => check_intercepted_urls([url]));
+ }),
+ 'mode: "error", generated relative redirect response');
+
+promise_test(t => setup_and_clean()
+ .then(() => {
+ const url = host_info['HTTPS_ORIGIN'] + base_path() +
+ 'sample?url=blank.html' +
+ '&original-redirect-mode=manual&sw=gen-manual';
+ return redirected_test({url: url,
+ fetch_option: {redirect: 'manual'},
+ fetch_method: frame.contentWindow.fetch,
+ expected_type: 'opaqueredirect',
+ expected_redirected: false,
+ expected_intercepted_urls: [url]})
+ }),
+ 'mode: "manual", generated relative redirect response');
+
+// =======================================================
+// Tests for requests that are in-scope of the service worker. The service
+// worker returns a generated redirect response. And the fetch follows the
+// redirection multiple times.
+// =======================================================
+promise_test(t => setup_and_clean()
+ .then(() => {
+ // The Fetch spec says: "If request’s redirect count is twenty, return a
+ // network error." https://fetch.spec.whatwg.org/#http-redirect-fetch
+ // So fetch can follow the redirect response 20 times.
+ let urls = [TARGET_URL];
+ for (let i = 0; i < 20; ++i) {
+ urls.unshift(host_info['HTTPS_ORIGIN'] + '/sample?sw=gen&url=' +
+ encodeURIComponent(urls[0]));
+
+ }
+ return redirected_test({url: urls[0],
+ fetch_option: {redirect: 'follow'},
+ fetch_method: frame.contentWindow.fetch,
+ expected_type: 'basic',
+ expected_redirected: true,
+ expected_intercepted_urls: urls})
+ }),
+ 'Fetch should follow the redirect response 20 times');
+
+promise_test(t => setup_and_clean()
+ .then(() => {
+ let urls = [TARGET_URL];
+ // The Fetch spec says: "If request’s redirect count is twenty, return a
+ // network error." https://fetch.spec.whatwg.org/#http-redirect-fetch
+ // So fetch can't follow the redirect response 21 times.
+ for (let i = 0; i < 21; ++i) {
+ urls.unshift(host_info['HTTPS_ORIGIN'] + '/sample?sw=gen&url=' +
+ encodeURIComponent(urls[0]));
+
+ }
+ return promise_rejects_js(
+ t, frame.contentWindow.TypeError,
+ frame.contentWindow.fetch(urls[0], {redirect: 'follow'}),
+ 'Fetch should not follow the redirect response 21 times.')
+ .then(() => {
+ urls.pop();
+ return check_intercepted_urls(urls)
+ });
+ }),
+ 'Fetch should not follow the redirect response 21 times.');
+
+// =======================================================
+// A test for verifying the url of a service-worker-redirected request is
+// propagated to the outer response.
+// =======================================================
+promise_test(t => setup_and_clean()
+ .then(() => {
+ const url = host_info['HTTPS_ORIGIN'] + base_path() + 'sample?url=' +
+ encodeURIComponent(TARGET_URL) +'&sw=fetch-url';
+ return redirected_test({url: url,
+ fetch_option: {},
+ fetch_method: frame.contentWindow.fetch,
+ expected_type: 'basic',
+ expected_redirected: false,
+ expected_intercepted_urls: [url],
+ expected_response_url: TARGET_URL});
+ }),
+ 'The URL for the service worker redirected request should be propagated to ' +
+ 'response.');
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/referer.https.html b/test/wpt/tests/service-workers/service-worker/referer.https.html
new file mode 100644
index 0000000..0957e4c
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/referer.https.html
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<title>Service Worker: check referer of fetch()</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js?pipe=sub"></script>
+<script>
+promise_test(function(t) {
+ var SCOPE = 'resources/referer-iframe.html';
+ var SCRIPT = 'resources/fetch-rewrite-worker.js';
+ var host_info = get_host_info();
+ return service_worker_unregister_and_register(t, SCRIPT, SCOPE)
+ .then(function(registration) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, SCOPE);
+ });
+
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() { return with_iframe(SCOPE); })
+ .then(function(frame) {
+ var channel = new MessageChannel();
+ t.add_cleanup(function() {
+ frame.remove();
+ });
+
+ var onMsg = new Promise(function(resolve) {
+ channel.port1.onmessage = resolve;
+ });
+
+ frame.contentWindow.postMessage({},
+ host_info['HTTPS_ORIGIN'],
+ [channel.port2]);
+ return onMsg;
+ })
+ .then(function(e) {
+ assert_equals(e.data.results, 'finish');
+ });
+ }, 'Verify the referer');
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/referrer-policy-header.https.html b/test/wpt/tests/service-workers/service-worker/referrer-policy-header.https.html
new file mode 100644
index 0000000..784343e
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/referrer-policy-header.https.html
@@ -0,0 +1,67 @@
+<!DOCTYPE html>
+<title>Service Worker: check referer of fetch() with Referrer Policy</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+
+const SCOPE = 'resources/referrer-policy-iframe.html';
+const SCRIPT = 'resources/fetch-rewrite-worker-referrer-policy.js';
+
+promise_test(async t => {
+ const registration =
+ await service_worker_unregister_and_register(t, SCRIPT, SCOPE);
+ await wait_for_state(t, registration.installing, 'activated');
+ t.add_cleanup(() => registration.unregister(),
+ 'Remove registration as a cleanup');
+
+ const full_scope_url = new URL(SCOPE, location.href);
+ const redirect_to = `${full_scope_url.href}?ignore=true`;
+ const frame = await with_iframe(
+ `${SCOPE}?pipe=status(302)|header(Location,${redirect_to})|` +
+ 'header(Referrer-Policy,origin)');
+ assert_equals(frame.contentDocument.referrer,
+ full_scope_url.origin + '/');
+ t.add_cleanup(() => frame.remove());
+}, 'Referrer for a main resource redirected with referrer-policy (origin) ' +
+ 'should only have origin.');
+
+promise_test(async t => {
+ const registration =
+ await service_worker_unregister_and_register(t, SCRIPT, SCOPE, `{type: 'module'}`);
+ await wait_for_state(t, registration.installing, 'activated');
+ t.add_cleanup(() => registration.unregister(),
+ 'Remove registration as a cleanup');
+
+ const full_scope_url = new URL(SCOPE, location.href);
+ const redirect_to = `${full_scope_url.href}?ignore=true`;
+ const frame = await with_iframe(
+ `${SCOPE}?pipe=status(302)|header(Location,${redirect_to})|` +
+ 'header(Referrer-Policy,origin)');
+ assert_equals(frame.contentDocument.referrer,
+ full_scope_url.origin + '/');
+ t.add_cleanup(() => frame.remove());
+}, 'Referrer for a main resource redirected with a module script with referrer-policy (origin) ' +
+ 'should only have origin.');
+
+promise_test(async t => {
+ const registration =
+ await service_worker_unregister_and_register(t, SCRIPT, SCOPE);
+ await wait_for_state(t, registration.installing, 'activated');
+ t.add_cleanup(() => registration.unregister(),
+ 'Remove registration as a cleanup');
+
+ const host_info = get_host_info();
+ const frame = await with_iframe(SCOPE);
+ const channel = new MessageChannel();
+ t.add_cleanup(() => frame.remove());
+ const e = await new Promise(resolve => {
+ channel.port1.onmessage = resolve;
+ frame.contentWindow.postMessage(
+ {}, host_info['HTTPS_ORIGIN'], [channel.port2]);
+ });
+ assert_equals(e.data.results, 'finish');
+}, 'Referrer for fetch requests initiated from a service worker with ' +
+ 'referrer-policy (origin) should only have origin.');
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/referrer-toplevel-script-fetch.https.html b/test/wpt/tests/service-workers/service-worker/referrer-toplevel-script-fetch.https.html
new file mode 100644
index 0000000..65c60a1
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/referrer-toplevel-script-fetch.https.html
@@ -0,0 +1,64 @@
+<!DOCTYPE html>
+<title>Service Worker: check referrer of top-level script fetch</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js?pipe=sub"></script>
+<script>
+
+async function get_toplevel_script_headers(worker) {
+ worker.postMessage("getHeaders");
+ return new Promise((resolve) => {
+ navigator.serviceWorker.onmessage = (event) => {
+ resolve(event.data);
+ };
+ });
+}
+
+promise_test(async (t) => {
+ const script = "resources/test-request-headers-worker.py";
+ const scope = "resources/blank.html";
+ const host_info = get_host_info();
+
+ const registration = await service_worker_unregister_and_register(
+ t, script, scope);
+ t.add_cleanup(() => service_worker_unregister(t, scope));
+ await wait_for_state(t, registration.installing, "activated");
+
+ const expected_referrer = host_info["HTTPS_ORIGIN"] + location.pathname;
+
+ // Check referrer for register().
+ const register_headers = await get_toplevel_script_headers(registration.active);
+ assert_equals(register_headers["referer"], expected_referrer, "referrer of register()");
+
+ // Check referrer for update().
+ await registration.update();
+ await wait_for_state(t, registration.installing, "installed");
+ const update_headers = await get_toplevel_script_headers(registration.waiting);
+ assert_equals(update_headers["referer"], expected_referrer, "referrer of update()");
+}, "Referrer of the top-level script fetch should be the document URL");
+
+promise_test(async (t) => {
+ const script = "resources/test-request-headers-worker.py";
+ const scope = "resources/blank.html";
+ const host_info = get_host_info();
+
+ const registration = await service_worker_unregister_and_register(
+ t, script, scope, {type: 'module'});
+ t.add_cleanup(() => service_worker_unregister(t, scope));
+ await wait_for_state(t, registration.installing, "activated");
+
+ const expected_referrer = host_info["HTTPS_ORIGIN"] + location.pathname;
+
+ // Check referrer for register().
+ const register_headers = await get_toplevel_script_headers(registration.active);
+ assert_equals(register_headers["referer"], expected_referrer, "referrer of register()");
+
+ // Check referrer for update().
+ await registration.update();
+ await wait_for_state(t, registration.installing, "installed");
+ const update_headers = await get_toplevel_script_headers(registration.waiting);
+ assert_equals(update_headers["referer"], expected_referrer, "referrer of update()");
+}, "Referrer of the module script fetch should be the document URL");
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/register-closed-window.https.html b/test/wpt/tests/service-workers/service-worker/register-closed-window.https.html
new file mode 100644
index 0000000..9c1b639
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/register-closed-window.https.html
@@ -0,0 +1,35 @@
+<!DOCTYPE html>
+<title>Service Worker: Register() on Closed Window</title>
+<meta name=timeout content=long>
+<script src="/resources/testharness.js"></script>
+<script src="resources/testharness-helpers.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+
+var host_info = get_host_info();
+var frameURL = host_info['HTTPS_ORIGIN'] + base_path() +
+ 'resources/register-closed-window-iframe.html';
+
+async_test(function(t) {
+ var frame;
+ with_iframe(frameURL).then(function(f) {
+ frame = f;
+ return new Promise(function(resolve) {
+ window.addEventListener('message', function messageHandler(evt) {
+ window.removeEventListener('message', messageHandler);
+ resolve(evt.data);
+ });
+ frame.contentWindow.postMessage('START', '*');
+ });
+ }).then(function(result) {
+ assert_equals(result, 'OK', 'frame should complete without crashing');
+ frame.remove();
+ t.done();
+ }).catch(unreached_rejection(t));
+}, 'Call register() on ServiceWorkerContainer owned by closed window.');
+
+</script>
+</body>
diff --git a/test/wpt/tests/service-workers/service-worker/register-default-scope.https.html b/test/wpt/tests/service-workers/service-worker/register-default-scope.https.html
new file mode 100644
index 0000000..1d86548
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/register-default-scope.https.html
@@ -0,0 +1,69 @@
+<!DOCTYPE html>
+<title>register() and scope</title>
+<script src="/resources/testharness.js"></script>
+<script src="resources/testharness-helpers.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+
+promise_test(function(t) {
+ var script = 'resources/empty-worker.js';
+ var script_url = new URL(script, location.href);
+ var expected_scope = new URL('./', script_url).href;
+ return service_worker_unregister(t, expected_scope)
+ .then(function() {
+ return navigator.serviceWorker.register('resources/empty-worker.js');
+ }).then(function(registration) {
+ assert_equals(registration.scope, expected_scope,
+ 'The default scope should be URL("./", script_url)');
+ return registration.unregister();
+ }).then(function() {
+ t.done();
+ });
+ }, 'default scope');
+
+promise_test(function(t) {
+ // This script must be different than the 'default scope' test, or else
+ // the scopes will collide.
+ var script = 'resources/empty.js';
+ var script_url = new URL(script, location.href);
+ var expected_scope = new URL('./', script_url).href;
+ return service_worker_unregister(t, expected_scope)
+ .then(function() {
+ return navigator.serviceWorker.register('resources/empty.js',
+ { scope: undefined });
+ }).then(function(registration) {
+ assert_equals(registration.scope, expected_scope,
+ 'The default scope should be URL("./", script_url)');
+ return registration.unregister();
+ }).then(function() {
+ t.done();
+ });
+ }, 'undefined scope');
+
+promise_test(function(t) {
+ var script = 'resources/simple-fetch-worker.js';
+ var script_url = new URL(script, location.href);
+ var expected_scope = new URL('./', script_url).href;
+ return service_worker_unregister(t, expected_scope)
+ .then(function() {
+ return navigator.serviceWorker.register('resources/empty.js',
+ { scope: null });
+ })
+ .then(
+ function(registration) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, registration.scope);
+ });
+
+ assert_unreached('register should fail');
+ },
+ function(error) {
+ assert_equals(error.name, 'SecurityError',
+ 'passing a null scope should be interpreted as ' +
+ 'scope="null" which violates the path restriction');
+ t.done();
+ });
+ }, 'null scope');
+
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/register-same-scope-different-script-url.https.html b/test/wpt/tests/service-workers/service-worker/register-same-scope-different-script-url.https.html
new file mode 100644
index 0000000..6eb00f3
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/register-same-scope-different-script-url.https.html
@@ -0,0 +1,233 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+var script1 = normalizeURL('resources/empty-worker.js');
+var script2 = normalizeURL('resources/empty-worker.js?new');
+
+async_test(function(t) {
+ var scope = 'resources/scope/register-new-script-concurrently';
+ var register_promise1;
+ var register_promise2;
+
+ service_worker_unregister(t, scope)
+ .then(function() {
+ register_promise1 = navigator.serviceWorker.register(script1,
+ {scope: scope});
+ register_promise2 = navigator.serviceWorker.register(script2,
+ {scope: scope});
+ return register_promise1;
+ })
+ .then(function(registration) {
+ assert_equals(registration.installing.scriptURL, script1,
+ 'on first register, first script should be installing');
+ assert_equals(registration.waiting, null,
+ 'on first register, waiting should be null');
+ assert_equals(registration.active, null,
+ 'on first register, active should be null');
+ return register_promise2;
+ })
+ .then(function(registration) {
+ assert_equals(
+ registration.installing.scriptURL, script2,
+ 'on second register, second script should be installing');
+ // Spec allows racing: the first register may have finished
+ // or the second one could have terminated the installing worker.
+ assert_true(registration.waiting == null ||
+ registration.waiting.scriptURL == script1,
+ 'on second register, .waiting should be null or the ' +
+ 'first script');
+ assert_true(registration.active == null ||
+ (registration.waiting == null &&
+ registration.active.scriptURL == script1),
+ 'on second register, .active should be null or the ' +
+ 'first script');
+ return registration.unregister();
+ })
+ .then(function() {
+ t.done();
+ })
+ .catch(unreached_rejection(t));
+ }, 'Register different scripts concurrently');
+
+async_test(function(t) {
+ var scope = 'resources/scope/register-then-register-new-script';
+ var registration;
+
+ service_worker_unregister_and_register(t, script1, scope)
+ .then(function(r) {
+ registration = r;
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() {
+ assert_equals(registration.installing, null,
+ 'on activated, installing should be null');
+ assert_equals(registration.waiting, null,
+ 'on activated, waiting should be null');
+ assert_equals(registration.active.scriptURL, script1,
+ 'on activated, the first script should be active');
+ return navigator.serviceWorker.register(script2, {scope:scope});
+ })
+ .then(function(r) {
+ registration = r;
+ assert_equals(registration.installing.scriptURL, script2,
+ 'on second register, the second script should be ' +
+ 'installing');
+ assert_equals(registration.waiting, null,
+ 'on second register, waiting should be null');
+ assert_equals(registration.active.scriptURL, script1,
+ 'on second register, the first script should be ' +
+ 'active');
+ return wait_for_state(t, registration.installing, 'installed');
+ })
+ .then(function() {
+ assert_equals(registration.installing, null,
+ 'on installed, installing should be null');
+ assert_equals(registration.waiting.scriptURL, script2,
+ 'on installed, the second script should be waiting');
+ assert_equals(registration.active.scriptURL, script1,
+ 'on installed, the first script should be active');
+ return registration.unregister();
+ })
+ .then(function() {
+ t.done();
+ })
+ .catch(unreached_rejection(t));
+ }, 'Register then register new script URL');
+
+async_test(function(t) {
+ var scope = 'resources/scope/register-then-register-new-script-404';
+ var registration;
+
+ service_worker_unregister_and_register(t, script1, scope)
+ .then(function(r) {
+ registration = r;
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() {
+ assert_equals(registration.installing, null,
+ 'on activated, installing should be null');
+ assert_equals(registration.waiting, null,
+ 'on activated, waiting should be null');
+ assert_equals(registration.active.scriptURL, script1,
+ 'on activated, the first script should be active');
+ return navigator.serviceWorker.register('this-will-404.js',
+ {scope:scope});
+ })
+ .then(
+ function() { assert_unreached('register should reject'); },
+ function(error) {
+ assert_equals(registration.installing, null,
+ 'on rejected, installing should be null');
+ assert_equals(registration.waiting, null,
+ 'on rejected, waiting should be null');
+ assert_equals(registration.active.scriptURL, script1,
+ 'on rejected, the first script should be active');
+ return registration.unregister();
+ })
+ .then(function() {
+ t.done();
+ })
+ .catch(unreached_rejection(t));
+ }, 'Register then register new script URL that 404s');
+
+async_test(function(t) {
+ var scope = 'resources/scope/register-then-register-new-script-reject-install';
+ var reject_script = normalizeURL('resources/reject-install-worker.js');
+ var registration;
+
+ service_worker_unregister_and_register(t, script1, scope)
+ .then(function(r) {
+ registration = r;
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() {
+ assert_equals(registration.installing, null,
+ 'on activated, installing should be null');
+ assert_equals(registration.waiting, null,
+ 'on activated, waiting should be null');
+ assert_equals(registration.active.scriptURL, script1,
+ 'on activated, the first script should be active');
+ return navigator.serviceWorker.register(reject_script, {scope:scope});
+ })
+ .then(function(r) {
+ registration = r;
+ assert_equals(registration.installing.scriptURL, reject_script,
+ 'on update, the second script should be installing');
+ assert_equals(registration.waiting, null,
+ 'on update, waiting should be null');
+ assert_equals(registration.active.scriptURL, script1,
+ 'on update, the first script should be active');
+ return wait_for_state(t, registration.installing, 'redundant');
+ })
+ .then(function() {
+ assert_equals(registration.installing, null,
+ 'on redundant, installing should be null');
+ assert_equals(registration.waiting, null,
+ 'on redundant, waiting should be null');
+ assert_equals(registration.active.scriptURL, script1,
+ 'on redundant, the first script should be active');
+ return registration.unregister();
+ })
+ .then(function() {
+ t.done();
+ })
+ .catch(unreached_rejection(t));
+ }, 'Register then register new script that does not install');
+
+async_test(function(t) {
+ var scope = 'resources/scope/register-new-script-controller';
+ var iframe;
+ var registration;
+
+ service_worker_unregister_and_register(t, script1, scope)
+ .then(function(r) {
+ registration = r;
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() {
+ return with_iframe(scope);
+ })
+ .then(function(frame) {
+ iframe = frame;
+ return navigator.serviceWorker.register(script2, { scope: scope })
+ })
+ .then(function(r) {
+ registration = r;
+ return wait_for_state(t, registration.installing, 'installed');
+ })
+ .then(function() {
+ var sw_container = iframe.contentWindow.navigator.serviceWorker;
+ assert_equals(sw_container.controller.scriptURL, script1,
+ 'the old version should control the old doc');
+ return with_iframe(scope);
+ })
+ .then(function(frame) {
+ var sw_container = frame.contentWindow.navigator.serviceWorker;
+ assert_equals(sw_container.controller.scriptURL, script1,
+ 'the old version should control a new doc');
+ var onactivated_promise = wait_for_state(t,
+ registration.waiting,
+ 'activated');
+ frame.remove();
+ iframe.remove();
+ return onactivated_promise;
+ })
+ .then(function() {
+ return with_iframe(scope);
+ })
+ .then(function(frame) {
+ var sw_container = frame.contentWindow.navigator.serviceWorker;
+ assert_equals(sw_container.controller.scriptURL, script2,
+ 'the new version should control a new doc');
+ frame.remove();
+ return registration.unregister();
+ })
+ .then(function() {
+ t.done();
+ })
+ .catch(unreached_rejection(t));
+ }, 'Register same-scope new script url effect on controller');
+
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/register-wait-forever-in-install-worker.https.html b/test/wpt/tests/service-workers/service-worker/register-wait-forever-in-install-worker.https.html
new file mode 100644
index 0000000..0920b5c
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/register-wait-forever-in-install-worker.https.html
@@ -0,0 +1,57 @@
+<!DOCTYPE html>
+<title>Service Worker: Register wait-forever-in-install-worker</title>
+<script src="/resources/testharness.js"></script>
+<script src="resources/testharness-helpers.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+
+promise_test(function(t) {
+ var bad_script = 'resources/wait-forever-in-install-worker.js';
+ var good_script = 'resources/empty-worker.js';
+ var scope = 'resources/wait-forever-in-install-worker';
+ var other_scope = 'resources/wait-forever-in-install-worker-other';
+ var registration;
+ var registerPromise;
+
+ return navigator.serviceWorker.register(bad_script, {scope: scope})
+ .then(function(r) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ registration = r;
+ assert_equals(registration.installing.scriptURL,
+ normalizeURL(bad_script));
+
+ // This register job should not start until the first
+ // register for the same scope completes.
+ registerPromise =
+ navigator.serviceWorker.register(good_script, {scope: scope});
+
+ // In order to test that the above register does not complete
+ // we will perform a register() on a different scope. The
+ // assumption here is that the previous register call would
+ // have completed in the same timeframe if it was able to do
+ // so.
+ return navigator.serviceWorker.register(good_script,
+ {scope: other_scope});
+ })
+ .then(function(swr) {
+ return swr.unregister();
+ })
+ .then(function() {
+ assert_equals(registration.installing.scriptURL,
+ normalizeURL(bad_script));
+ registration.installing.postMessage('STOP_WAITING');
+ return registerPromise;
+ })
+ .then(function(swr) {
+ assert_equals(registration.installing.scriptURL,
+ normalizeURL(good_script));
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ }, 'register worker that calls waitUntil with a promise that never ' +
+ 'resolves in oninstall');
+
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/registration-basic.https.html b/test/wpt/tests/service-workers/service-worker/registration-basic.https.html
new file mode 100644
index 0000000..759b424
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/registration-basic.https.html
@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<title>Service Worker: Registration (basic)</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+const script = 'resources/registration-worker.js';
+
+promise_test(async (t) => {
+ const scope = 'resources/registration/normal';
+ const registration = await navigator.serviceWorker.register(script, {scope});
+ t.add_cleanup(() => registration.unregister());
+ assert_true(
+ registration instanceof ServiceWorkerRegistration,
+ 'Successfully registered.');
+}, 'Registering normal scope');
+
+promise_test(async (t) => {
+ const scope = 'resources/registration/scope-with-fragment#ref';
+ const registration = await navigator.serviceWorker.register(script, {scope});
+ t.add_cleanup(() => registration.unregister());
+ assert_true(
+ registration instanceof ServiceWorkerRegistration,
+ 'Successfully registered.');
+ assert_equals(
+ registration.scope,
+ normalizeURL('resources/registration/scope-with-fragment'),
+ 'A fragment should be removed from scope');
+}, 'Registering scope with fragment');
+
+promise_test(async (t) => {
+ const scope = 'resources/';
+ const registration = await navigator.serviceWorker.register(script, {scope})
+ t.add_cleanup(() => registration.unregister());
+ assert_true(
+ registration instanceof ServiceWorkerRegistration,
+ 'Successfully registered.');
+}, 'Registering same scope as the script directory');
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/registration-end-to-end.https.html b/test/wpt/tests/service-workers/service-worker/registration-end-to-end.https.html
new file mode 100644
index 0000000..1af4582
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/registration-end-to-end.https.html
@@ -0,0 +1,88 @@
+<!DOCTYPE html>
+<title>Service Worker: registration end-to-end</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+var t = async_test('Registration: end-to-end');
+t.step(function() {
+
+ var scope = 'resources/in-scope/';
+ var serviceWorkerStates = [];
+ var lastServiceWorkerState = '';
+ var receivedMessageFromPort = '';
+
+ assert_true(navigator.serviceWorker instanceof ServiceWorkerContainer);
+ assert_equals(typeof navigator.serviceWorker.register, 'function');
+ assert_equals(typeof navigator.serviceWorker.getRegistration, 'function');
+
+ service_worker_unregister_and_register(
+ t, 'resources/end-to-end-worker.js', scope)
+ .then(onRegister)
+ .catch(unreached_rejection(t));
+
+ function sendMessagePort(worker, from) {
+ var messageChannel = new MessageChannel();
+ worker.postMessage({from:from, port:messageChannel.port2}, [messageChannel.port2]);
+ return messageChannel.port1;
+ }
+
+ function onRegister(registration) {
+ var sw = registration.installing;
+ serviceWorkerStates.push(sw.state);
+ lastServiceWorkerState = sw.state;
+
+ var sawMessage = new Promise(t.step_func(function(resolve) {
+ sendMessagePort(sw, 'registering doc').onmessage = t.step_func(function (e) {
+ receivedMessageFromPort = e.data;
+ resolve();
+ });
+ }));
+
+ var sawActive = new Promise(t.step_func(function(resolve) {
+ sw.onstatechange = t.step_func(function() {
+ serviceWorkerStates.push(sw.state);
+
+ switch (sw.state) {
+ case 'installed':
+ assert_equals(lastServiceWorkerState, 'installing');
+ break;
+ case 'activating':
+ assert_equals(lastServiceWorkerState, 'installed');
+ break;
+ case 'activated':
+ assert_equals(lastServiceWorkerState, 'activating');
+ break;
+ default:
+ // We won't see 'redundant' because onstatechange is
+ // overwritten before calling unregister.
+ assert_unreached('Unexpected state: ' + sw.state);
+ }
+
+ lastServiceWorkerState = sw.state;
+ if (sw.state === 'activated')
+ resolve();
+ });
+ }));
+
+ Promise.all([sawMessage, sawActive]).then(t.step_func(function() {
+ assert_array_equals(serviceWorkerStates,
+ ['installing', 'installed', 'activating', 'activated'],
+ 'Service worker should pass through all states');
+
+ assert_equals(receivedMessageFromPort, 'Ack for: registering doc');
+
+ var sawRedundant = new Promise(t.step_func(function(resolve) {
+ sw.onstatechange = t.step_func(function() {
+ assert_equals(sw.state, 'redundant');
+ resolve();
+ });
+ }));
+ registration.unregister();
+ sawRedundant.then(t.step_func(function() {
+ t.done();
+ }));
+ }));
+ }
+});
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/registration-events.https.html b/test/wpt/tests/service-workers/service-worker/registration-events.https.html
new file mode 100644
index 0000000..5bcfd66
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/registration-events.https.html
@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<title>Service Worker: registration events</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+promise_test(function(t) {
+ var scope = 'resources/in-scope/';
+ return service_worker_unregister_and_register(
+ t, 'resources/events-worker.js', scope)
+ .then(function(registration) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ return onRegister(registration.installing);
+ });
+
+ function sendMessagePort(worker, from) {
+ var messageChannel = new MessageChannel();
+ worker.postMessage({from:from, port:messageChannel.port2}, [messageChannel.port2]);
+ return messageChannel.port1;
+ }
+
+ function onRegister(sw) {
+ return new Promise(function(resolve) {
+ sw.onstatechange = function() {
+ if (sw.state === 'activated')
+ resolve();
+ };
+ }).then(function() {
+ return new Promise(function(resolve) {
+ sendMessagePort(sw, 'registering doc').onmessage = resolve;
+ });
+ }).then(function(e) {
+ assert_array_equals(e.data.events,
+ ['install', 'activate'],
+ 'Worker should see install then activate events');
+ });
+ }
+}, 'Registration: events');
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/registration-iframe.https.html b/test/wpt/tests/service-workers/service-worker/registration-iframe.https.html
new file mode 100644
index 0000000..ae39ddf
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/registration-iframe.https.html
@@ -0,0 +1,116 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Service Worker: Registration for iframe</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+
+// Set script url and scope url relative to the iframe's document's url. Assert
+// the implementation parses the urls against the iframe's document's url.
+async_test(function(t) {
+ const url = 'resources/blank.html';
+ const iframe_scope = 'registration-with-valid-scope';
+ const scope = normalizeURL('resources/' + iframe_scope);
+ const iframe_script = 'empty-worker.js';
+ const script = normalizeURL('resources/' + iframe_script);
+ var frame;
+ var registration;
+
+ service_worker_unregister(t, scope)
+ .then(function() { return with_iframe(url); })
+ .then(function(f) {
+ frame = f;
+ return frame.contentWindow.navigator.serviceWorker.register(
+ iframe_script,
+ { scope: iframe_scope });
+ })
+ .then(function(r) {
+ registration = r;
+ return wait_for_state(t, r.installing, 'activated');
+ })
+ .then(function() {
+ assert_equals(registration.scope, scope,
+ 'registration\'s scope must be parsed against the ' +
+ '"relevant global object"');
+ assert_equals(registration.active.scriptURL, script,
+ 'worker\'s scriptURL must be parsed against the ' +
+ '"relevant global object"');
+ return registration.unregister();
+ })
+ .then(function() {
+ frame.remove();
+ t.done();
+ })
+ .catch(unreached_rejection(t));
+ }, 'register method should use the "relevant global object" to parse its ' +
+ 'scriptURL and scope - normal case');
+
+// Set script url and scope url relative to the parent frame's document's url.
+// Assert the implementation throws a TypeError exception.
+async_test(function(t) {
+ const url = 'resources/blank.html';
+ const iframe_scope = 'resources/registration-with-scope-to-non-existing-url';
+ const scope = normalizeURL('resources/' + iframe_scope);
+ const script = 'resources/empty-worker.js';
+ var frame;
+ var registration;
+
+ service_worker_unregister(t, scope)
+ .then(function() { return with_iframe(url); })
+ .then(function(f) {
+ frame = f;
+ return frame.contentWindow.navigator.serviceWorker.register(
+ script,
+ { scope: iframe_scope });
+ })
+ .then(
+ function() {
+ assert_unreached('register() should reject');
+ },
+ function(e) {
+ assert_equals(e.name, 'TypeError',
+ 'register method with scriptURL and scope parsed to ' +
+ 'nonexistent location should reject with TypeError');
+ frame.remove();
+ t.done();
+ })
+ .catch(unreached_rejection(t));
+ }, 'register method should use the "relevant global object" to parse its ' +
+ 'scriptURL and scope - error case');
+
+// Set the scope url to a non-subdirectory of the script url. Assert the
+// implementation throws a SecurityError exception.
+async_test(function(t) {
+ const url = 'resources/blank.html';
+ const scope = 'registration-with-disallowed-scope';
+ const iframe_scope = '../' + scope;
+ const script = 'empty-worker.js';
+ var frame;
+ var registration;
+
+ service_worker_unregister(t, scope)
+ .then(function() { return with_iframe(url); })
+ .then(function(f) {
+ frame = f;
+ return frame.contentWindow.navigator.serviceWorker.register(
+ script,
+ { scope: iframe_scope });
+ })
+ .then(
+ function() {
+ assert_unreached('register() should reject');
+ },
+ function(e) {
+ assert_equals(e.name, 'SecurityError',
+ 'The scope set to a non-subdirectory of the scriptURL ' +
+ 'should reject with SecurityError');
+ frame.remove();
+ t.done();
+ })
+ .catch(unreached_rejection(t));
+ }, 'A scope url should start with the given script url');
+
+</script>
+</body>
diff --git a/test/wpt/tests/service-workers/service-worker/registration-mime-types.https.html b/test/wpt/tests/service-workers/service-worker/registration-mime-types.https.html
new file mode 100644
index 0000000..3a21aac
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/registration-mime-types.https.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<title>Service Worker: Registration (MIME types)</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script src="resources/registration-tests-mime-types.js"></script>
+<script>
+registration_tests_mime_types((script, options) => navigator.serviceWorker.register(script, options));
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/registration-schedule-job.https.html b/test/wpt/tests/service-workers/service-worker/registration-schedule-job.https.html
new file mode 100644
index 0000000..25d758e
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/registration-schedule-job.https.html
@@ -0,0 +1,107 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<meta name=timeout content=long>
+<title>Service Worker: Schedule Job algorithm</title>
+<script src="/resources/testharness.js"></script>
+<script src="resources/testharness-helpers.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+// Tests for https://w3c.github.io/ServiceWorker/#schedule-job-algorithm
+// Non-equivalent register jobs should not be coalesced.
+const scope = 'resources/';
+const script1 = 'resources/empty.js';
+const script2 = 'resources/empty.js?change';
+
+async function cleanup() {
+ const registration = await navigator.serviceWorker.getRegistration(scope);
+ if (registration)
+ await registration.unregister();
+}
+
+function absolute_url(url) {
+ return new URL(url, self.location).toString();
+}
+
+// Test that a change to `script` starts a new register job.
+promise_test(async t => {
+ await cleanup();
+ t.add_cleanup(cleanup);
+
+ // Make a registration.
+ const registration = await
+ navigator.serviceWorker.register(script1, {scope});
+
+ // Schedule two more register jobs.
+ navigator.serviceWorker.register(script1, {scope});
+ await navigator.serviceWorker.register(script2, {scope});
+
+ // The jobs should not have been coalesced.
+ const worker = get_newest_worker(registration);
+ assert_equals(worker.scriptURL, absolute_url(script2));
+}, 'different scriptURL');
+
+// Test that a change to `updateViaCache` starts a new register job.
+promise_test(async t => {
+ await cleanup();
+ t.add_cleanup(cleanup);
+
+ // Check defaults.
+ const registration = await
+ navigator.serviceWorker.register(script1, {scope});
+ assert_equals(registration.updateViaCache, 'imports');
+
+ // Schedule two more register jobs.
+ navigator.serviceWorker.register(script1, {scope});
+ await navigator.serviceWorker.register(script1, {scope,
+ updateViaCache: 'none'});
+
+ // The jobs should not have been coalesced.
+ assert_equals(registration.updateViaCache, 'none');
+}, 'different updateViaCache');
+
+// Test that a change to `type` starts a new register job.
+promise_test(async t => {
+ await cleanup();
+ t.add_cleanup(cleanup);
+
+ const scriptForTypeCheck = 'resources/type-check-worker.js';
+ // Check defaults.
+ const registration = await
+ navigator.serviceWorker.register(scriptForTypeCheck, {scope});
+
+ let worker_type = await new Promise((resolve) => {
+ navigator.serviceWorker.onmessage = (event) => {
+ resolve(event.data);
+ };
+ // The jobs should not have been coalesced. get_newest_worker() helps the
+ // test fail with stable output on browers that incorrectly coalesce
+ // register jobs, since then sometimes registration is not a new worker as
+ // expected.
+ const worker = get_newest_worker(registration);
+ // The argument of postMessage doesn't matter for this case.
+ worker.postMessage('');
+ });
+
+ assert_equals(worker_type, 'classic');
+
+ // Schedule two more register jobs.
+ navigator.serviceWorker.register(scriptForTypeCheck, {scope});
+ await navigator.serviceWorker.register(scriptForTypeCheck, {scope, type: 'module'});
+
+ worker_type = await new Promise((resolve) => {
+ navigator.serviceWorker.onmessage = (event) => {
+ resolve(event.data);
+ };
+ // The jobs should not have been coalesced. get_newest_worker() helps the
+ // test fail with stable output on browers that incorrectly coalesce
+ // register jobs, since then sometimes registration is not a new worker as
+ // expected.
+ const worker = get_newest_worker(registration);
+ // The argument of postMessage doesn't matter for this case.
+ worker.postMessage('');
+ });
+
+ assert_equals(worker_type, 'module');
+}, 'different type');
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/registration-scope-module-static-import.https.html b/test/wpt/tests/service-workers/service-worker/registration-scope-module-static-import.https.html
new file mode 100644
index 0000000..5c75295
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/registration-scope-module-static-import.https.html
@@ -0,0 +1,41 @@
+<!DOCTYPE html>
+<title>Service Worker: Static imports from module top-level scripts shouldn't be affected by the service worker script path restriction</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+// https://w3c.github.io/ServiceWorker/#path-restriction
+// is applied to top-level scripts in
+// https://w3c.github.io/ServiceWorker/#update-algorithm
+// but not to submodules imported from top-level scripts.
+async function runTest(t, script, scope) {
+ const script_url = new URL(script, location.href);
+ await service_worker_unregister(t, scope);
+ const registration = await
+ navigator.serviceWorker.register(script, {type: 'module'});
+ t.add_cleanup(_ => registration.unregister());
+ const msg = await new Promise(resolve => {
+ registration.installing.postMessage('ping');
+ navigator.serviceWorker.onmessage = resolve;
+ });
+ assert_equals(msg.data, 'pong');
+}
+
+promise_test(async t => {
+ await runTest(t,
+ 'resources/scope2/imported-module-script.js',
+ 'resources/scope2/');
+ }, 'imported-module-script.js works when used as top-level');
+
+promise_test(async t => {
+ await runTest(t,
+ 'resources/scope1/module-worker-importing-scope2.js',
+ 'resources/scope1/');
+ }, 'static imports to outside path restriction should be allowed');
+
+promise_test(async t => {
+ await runTest(t,
+ 'resources/scope1/module-worker-importing-redirect-to-scope2.js',
+ 'resources/scope1/');
+ }, 'static imports redirecting to outside path restriction should be allowed');
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/registration-scope.https.html b/test/wpt/tests/service-workers/service-worker/registration-scope.https.html
new file mode 100644
index 0000000..141875f
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/registration-scope.https.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<title>Service Worker: Registration (scope)</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script src="resources/registration-tests-scope.js"></script>
+<script>
+registration_tests_scope((script, options) => navigator.serviceWorker.register(script, options));
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/registration-script-module.https.html b/test/wpt/tests/service-workers/service-worker/registration-script-module.https.html
new file mode 100644
index 0000000..9e39a1f
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/registration-script-module.https.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<title>Service Worker: Registration (module script)</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script src="resources/registration-tests-script.js"></script>
+<script>
+registration_tests_script(
+ (script, options) => navigator.serviceWorker.register(
+ script,
+ Object.assign({type: 'module'}, options)),
+ 'module');
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/registration-script-url.https.html b/test/wpt/tests/service-workers/service-worker/registration-script-url.https.html
new file mode 100644
index 0000000..bda61ad
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/registration-script-url.https.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<title>Service Worker: Registration (scriptURL)</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script src="resources/registration-tests-script-url.js"></script>
+<script>
+registration_tests_script_url((script, options) => navigator.serviceWorker.register(script, options));
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/registration-script.https.html b/test/wpt/tests/service-workers/service-worker/registration-script.https.html
new file mode 100644
index 0000000..f1e51fd
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/registration-script.https.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<title>Service Worker: Registration (script)</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script src="resources/registration-tests-script.js"></script>
+<script>
+registration_tests_script(
+ (script, options) => navigator.serviceWorker.register(script, options),
+ 'classic'
+);
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/registration-security-error.https.html b/test/wpt/tests/service-workers/service-worker/registration-security-error.https.html
new file mode 100644
index 0000000..860c2d2
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/registration-security-error.https.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<title>Service Worker: Registration (SecurityError)</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script src="resources/registration-tests-security-error.js"></script>
+<script>
+registration_tests_security_error((script, options) => navigator.serviceWorker.register(script, options));
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/registration-service-worker-attributes.https.html b/test/wpt/tests/service-workers/service-worker/registration-service-worker-attributes.https.html
new file mode 100644
index 0000000..f7b52d5
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/registration-service-worker-attributes.https.html
@@ -0,0 +1,72 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+'use strict';
+promise_test(function(t) {
+ var scope = 'resources/scope/installing-waiting-active-after-registration';
+ var worker_url = 'resources/empty-worker.js';
+ var expected_url = normalizeURL(worker_url);
+ var newest_worker;
+ var registration;
+
+ return service_worker_unregister_and_register(t, worker_url, scope)
+ .then(function(r) {
+ t.add_cleanup(function() {
+ return r.unregister();
+ });
+ registration = r;
+ newest_worker = registration.installing;
+ assert_equals(registration.installing.scriptURL, expected_url,
+ 'installing before updatefound');
+ assert_equals(registration.waiting, null,
+ 'waiting before updatefound');
+ assert_equals(registration.active, null,
+ 'active before updatefound');
+ return wait_for_update(t, registration);
+ })
+ .then(function() {
+ assert_equals(registration.installing, newest_worker,
+ 'installing after updatefound');
+ assert_equals(registration.waiting, null,
+ 'waiting after updatefound');
+ assert_equals(registration.active, null,
+ 'active after updatefound');
+ return wait_for_state(t, registration.installing, 'installed');
+ })
+ .then(function() {
+ assert_equals(registration.installing, null,
+ 'installing after installed');
+ assert_equals(registration.waiting, newest_worker,
+ 'waiting after installed');
+ assert_equals(registration.active, null,
+ 'active after installed');
+ return wait_for_state(t, registration.waiting, 'activated');
+ })
+ .then(function() {
+ assert_equals(registration.installing, null,
+ 'installing after activated');
+ assert_equals(registration.waiting, null,
+ 'waiting after activated');
+ assert_equals(registration.active, newest_worker,
+ 'active after activated');
+ return Promise.all([
+ wait_for_state(t, registration.active, 'redundant'),
+ registration.unregister()
+ ]);
+ })
+ .then(function() {
+ assert_equals(registration.installing, null,
+ 'installing after redundant');
+ assert_equals(registration.waiting, null,
+ 'waiting after redundant');
+ // According to spec, Clear Registration runs Update State which is
+ // immediately followed by setting active to null, which means by the
+ // time the event loop turns and the Promise for statechange is
+ // resolved, this will be gone.
+ assert_equals(registration.active, null,
+ 'active should be null after redundant');
+ });
+ }, 'installing/waiting/active after registration');
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/registration-updateviacache.https.html b/test/wpt/tests/service-workers/service-worker/registration-updateviacache.https.html
new file mode 100644
index 0000000..b2f6bbc
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/registration-updateviacache.https.html
@@ -0,0 +1,204 @@
+<!DOCTYPE html>
+<title>Service Worker: Registration-updateViaCache</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="resources/testharness-helpers.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+ const UPDATE_VIA_CACHE_VALUES = [undefined, 'imports', 'all', 'none'];
+ const SCRIPT_URL = 'resources/update-max-aged-worker.py';
+ const SCOPE = 'resources/blank.html';
+
+ async function cleanup() {
+ const reg = await navigator.serviceWorker.getRegistration(SCOPE);
+ if (!reg) return;
+ if (reg.scope == new URL(SCOPE, location).href) {
+ return reg.unregister();
+ };
+ }
+
+ function getScriptTimes(sw, testName) {
+ return new Promise(resolve => {
+ navigator.serviceWorker.addEventListener('message', function listener(event) {
+ if (event.data.test !== testName) return;
+ navigator.serviceWorker.removeEventListener('message', listener);
+ resolve({
+ mainTime: event.data.mainTime,
+ importTime: event.data.importTime
+ });
+ });
+
+ sw.postMessage('');
+ });
+ }
+
+ // Test creating registrations & triggering an update.
+ for (const updateViaCache of UPDATE_VIA_CACHE_VALUES) {
+ const testName = `register-with-updateViaCache-${updateViaCache}`;
+
+ promise_test(async t => {
+ await cleanup();
+
+ const opts = {scope: SCOPE};
+
+ if (updateViaCache) opts.updateViaCache = updateViaCache;
+
+ const reg = await navigator.serviceWorker.register(
+ `${SCRIPT_URL}?test=${testName}`,
+ opts
+ );
+
+ assert_equals(reg.updateViaCache, updateViaCache || 'imports', "reg.updateViaCache");
+
+ const sw = reg.installing || reg.waiting || reg.active;
+ await wait_for_state(t, sw, 'activated');
+ const values = await getScriptTimes(sw, testName);
+ await reg.update();
+
+ if (updateViaCache == 'all') {
+ assert_equals(reg.installing, null, "No new service worker");
+ }
+ else {
+ const newWorker = reg.installing;
+ assert_true(!!newWorker, "New worker installing");
+ const newValues = await getScriptTimes(newWorker, testName);
+
+ if (!updateViaCache || updateViaCache == 'imports') {
+ assert_not_equals(values.mainTime, newValues.mainTime, "Main script should have updated");
+ assert_equals(values.importTime, newValues.importTime, "Imported script should be the same");
+ }
+ else if (updateViaCache == 'none') {
+ assert_not_equals(values.mainTime, newValues.mainTime, "Main script should have updated");
+ assert_not_equals(values.importTime, newValues.importTime, "Imported script should have updated");
+ }
+ else {
+ // We should have handled all of the possible values for updateViaCache.
+ // If this runs, something's gone very wrong.
+ throw Error(`Unexpected updateViaCache value: ${updateViaCache}`);
+ }
+ }
+
+ await cleanup();
+ }, testName);
+ }
+
+ // Test changing the updateViaCache value of an existing registration.
+ for (const updateViaCache1 of UPDATE_VIA_CACHE_VALUES) {
+ for (const updateViaCache2 of UPDATE_VIA_CACHE_VALUES) {
+ const testName = `register-with-updateViaCache-${updateViaCache1}-then-${updateViaCache2}`;
+
+ promise_test(async t => {
+ await cleanup();
+
+ const fullScriptUrl = `${SCRIPT_URL}?test=${testName}`;
+ let opts = {scope: SCOPE};
+ if (updateViaCache1) opts.updateViaCache = updateViaCache1;
+
+ const reg = await navigator.serviceWorker.register(fullScriptUrl, opts);
+
+ const sw = reg.installing;
+ await wait_for_state(t, sw, 'activated');
+ const values = await getScriptTimes(sw, testName);
+
+ const frame = await with_iframe(SCOPE);
+ const reg_in_frame = await frame.contentWindow.navigator.serviceWorker.getRegistration(normalizeURL(SCOPE));
+ assert_equals(reg_in_frame.updateViaCache, updateViaCache1 || 'imports', "reg_in_frame.updateViaCache");
+
+ opts = {scope: SCOPE};
+ if (updateViaCache2) opts.updateViaCache = updateViaCache2;
+
+ await navigator.serviceWorker.register(fullScriptUrl, opts);
+
+ const expected_updateViaCache = updateViaCache2 || 'imports';
+
+ assert_equals(reg.updateViaCache, expected_updateViaCache, "reg.updateViaCache updated");
+ // If the update happens via the cache, the scripts will come back byte-identical.
+ // We bypass the byte-identical check if the script URL has changed, but not if
+ // only the updateViaCache value has changed.
+ if (updateViaCache2 == 'all') {
+ assert_equals(reg.installing, null, "No new service worker");
+ }
+ // If there's no change to the updateViaCache value, register should be a no-op.
+ // The default value should behave as 'imports'.
+ else if ((updateViaCache1 || 'imports') == (updateViaCache2 || 'imports')) {
+ assert_equals(reg.installing, null, "No new service worker");
+ }
+ else {
+ const newWorker = reg.installing;
+ assert_true(!!newWorker, "New worker installing");
+ const newValues = await getScriptTimes(newWorker, testName);
+
+ if (!updateViaCache2 || updateViaCache2 == 'imports') {
+ assert_not_equals(values.mainTime, newValues.mainTime, "Main script should have updated");
+ assert_equals(values.importTime, newValues.importTime, "Imported script should be the same");
+ }
+ else if (updateViaCache2 == 'none') {
+ assert_not_equals(values.mainTime, newValues.mainTime, "Main script should have updated");
+ assert_not_equals(values.importTime, newValues.importTime, "Imported script should have updated");
+ }
+ else {
+ // We should have handled all of the possible values for updateViaCache2.
+ // If this runs, something's gone very wrong.
+ throw Error(`Unexpected updateViaCache value: ${updateViaCache}`);
+ }
+ }
+
+ // Wait for all registration related tasks on |frame| to complete.
+ await wait_for_activation_on_sample_scope(t, frame.contentWindow);
+ // The updateViaCache change should have been propagated to all
+ // corresponding JS registration objects.
+ assert_equals(reg_in_frame.updateViaCache, expected_updateViaCache, "reg_in_frame.updateViaCache updated");
+ frame.remove();
+
+ await cleanup();
+ }, testName);
+ }
+ }
+
+ // Test accessing updateViaCache of an unregistered registration.
+ for (const updateViaCache of UPDATE_VIA_CACHE_VALUES) {
+ const testName = `access-updateViaCache-after-unregister-${updateViaCache}`;
+
+ promise_test(async t => {
+ await cleanup();
+
+ const opts = {scope: SCOPE};
+
+ if (updateViaCache) opts.updateViaCache = updateViaCache;
+
+ const reg = await navigator.serviceWorker.register(
+ `${SCRIPT_URL}?test=${testName}`,
+ opts
+ );
+
+ const expected_updateViaCache = updateViaCache || 'imports';
+ assert_equals(reg.updateViaCache, expected_updateViaCache, "reg.updateViaCache");
+
+ await reg.unregister();
+
+ // Keep the original value.
+ assert_equals(reg.updateViaCache, expected_updateViaCache, "reg.updateViaCache");
+
+ await cleanup();
+ }, testName);
+ }
+
+ promise_test(async t => {
+ await cleanup();
+ t.add_cleanup(cleanup);
+
+ const registration = await navigator.serviceWorker.register(
+ 'resources/empty.js',
+ {scope: SCOPE});
+ assert_equals(registration.updateViaCache, 'imports',
+ 'before update attempt');
+
+ const fail = navigator.serviceWorker.register(
+ 'resources/malformed-worker.py?parse-error',
+ {scope: SCOPE, updateViaCache: 'none'});
+ await promise_rejects_js(t, TypeError, fail);
+ assert_equals(registration.updateViaCache, 'imports',
+ 'after update attempt');
+ }, 'updateViaCache is not updated if register() rejects');
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/rejections.https.html b/test/wpt/tests/service-workers/service-worker/rejections.https.html
new file mode 100644
index 0000000..8002ad9
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/rejections.https.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<title>Service Worker: Rejection Types</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+
+(function() {
+ var t = async_test('Rejections are DOMExceptions');
+ t.step(function() {
+
+ navigator.serviceWorker.register('http://example.com').then(
+ t.step_func(function() { assert_unreached('Registration should fail'); }),
+ t.step_func(function(reason) {
+ assert_true(reason instanceof DOMException);
+ assert_true(reason instanceof Error);
+ t.done();
+ }));
+ });
+}());
+
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/request-end-to-end.https.html b/test/wpt/tests/service-workers/service-worker/request-end-to-end.https.html
new file mode 100644
index 0000000..a39cead
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/request-end-to-end.https.html
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<title>Service Worker: FetchEvent.request passed to onfetch</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+'use strict';
+
+promise_test(t => {
+ var url = 'resources/request-end-to-end-worker.js';
+ var scope = 'resources/blank.html';
+ return service_worker_unregister_and_register(t, url, scope)
+ .then(r => {
+ add_completion_callback(() => { r.unregister(); });
+ return wait_for_state(t, r.installing, 'activated');
+ })
+ .then(() => { return with_iframe(scope); })
+ .then(frame => {
+ add_completion_callback(() => { frame.remove(); });
+
+ var result = JSON.parse(frame.contentDocument.body.textContent);
+ assert_equals(result.url, frame.src, 'request.url');
+ assert_equals(result.method, 'GET', 'request.method');
+ assert_equals(result.referrer, location.href, 'request.referrer');
+ assert_equals(result.mode, 'navigate', 'request.mode');
+ assert_equals(result.request_construct_error, '',
+ 'Constructing a Request with a Request whose mode ' +
+ 'is navigate and non-empty RequestInit must not throw a ' +
+ 'TypeError.')
+ assert_equals(result.credentials, 'include', 'request.credentials');
+ assert_equals(result.redirect, 'manual', 'request.redirect');
+ assert_equals(result.headers['user-agent'], undefined,
+ 'Default User-Agent header should not be passed to ' +
+ 'onfetch event.')
+ assert_equals(result.append_header_error, 'TypeError',
+ 'Appending a new header to the request must throw a ' +
+ 'TypeError.')
+ });
+ }, 'Test FetchEvent.request passed to onfetch');
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/resource-timing-bodySize.https.html b/test/wpt/tests/service-workers/service-worker/resource-timing-bodySize.https.html
new file mode 100644
index 0000000..5c2b1eb
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resource-timing-bodySize.https.html
@@ -0,0 +1,55 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<meta name="timeout" content="long">
+<script src="/common/utils.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+const {REMOTE_ORIGIN} = get_host_info();
+
+/*
+ This test does the following:
+ - Loads a service worker
+ - Loads an iframe in the service worker's scope
+ - The service worker tries to fetch a resource which is either:
+ - constructed inside the service worker
+ - fetched from a different URL ny the service worker
+ - Streamed from a differend URL by the service worker
+ - Passes through
+ - By default the RT entry should have encoded/decoded body size. except for
+ the case where the response is an opaque pass-through.
+*/
+function test_scenario({tao, mode, name}) {
+ promise_test(async (t) => {
+ const uid = token();
+ const worker_url = `resources/fetch-response.js?uid=${uid}`;
+ const scope = `resources/fetch-response.html?uid=${uid}`;
+ const iframe = document.createElement('iframe');
+ const path = name === "passthrough" ? `element-timing/resources/TAOImage.py?origin=*&tao=${
+ tao === "pass" ? "wildcard" : "none"})}` : name;
+
+ iframe.src = `${scope}&path=${encodeURIComponent(
+ `${mode === "same-origin" ? "" : REMOTE_ORIGIN}/${path}`)}&mode=${mode}`;
+ const registration = await service_worker_unregister_and_register(t, worker_url, scope);
+ t.add_cleanup(() => registration.unregister());
+ t.add_cleanup(() => iframe.remove());
+ await wait_for_state(t, registration.installing, 'activated');
+ const waitForMessage = new Promise(resolve =>
+ window.addEventListener('message', ({data}) => resolve(data)));
+ document.body.appendChild(iframe);
+ const {buffer, entry} = await waitForMessage;
+ const expectPass = name !== "passthrough" || mode !== "no-cors";
+ assert_equals(buffer.byteLength, expectPass ? entry.decodedBodySize : 0);
+ assert_equals(buffer.byteLength, expectPass ? entry.encodedBodySize : 0);
+ }, `Response body size: ${name}, ${mode}, TAO ${tao}`);
+}
+for (const mode of ["cors", "no-cors", "same-origin"]) {
+ for (const tao of ["pass", "fail"])
+ for (const name of ['constructed', 'forward', 'stream', 'passthrough']) {
+ test_scenario({tao, mode, name});
+ }
+}
+
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/resource-timing-cross-origin.https.html b/test/wpt/tests/service-workers/service-worker/resource-timing-cross-origin.https.html
new file mode 100644
index 0000000..2155d7f
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resource-timing-cross-origin.https.html
@@ -0,0 +1,46 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<meta charset="utf-8" />
+<title>This test validates Resource Timing for cross origin content fetched by Service Worker from an originally same-origin URL.</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+</head>
+
+<body>
+<script>
+function test_sw_resource_timing({ mode }) {
+ promise_test(async t => {
+ const worker_url = `resources/worker-fetching-cross-origin.js?mode=${mode}`;
+ const scope = 'resources/iframe-with-image.html';
+ const registration = await service_worker_unregister_and_register(t, worker_url, scope);
+ await wait_for_state(t, registration.installing, 'activated');
+ const frame = await with_iframe(scope);
+ const frame_performance = frame.contentWindow.performance;
+ // Check that there is one entry for which the timing allow check algorithm failed.
+ const entries = frame_performance.getEntriesByType('resource');
+ assert_equals(entries.length, 1);
+ const entry = entries[0];
+ assert_equals(entry.redirectStart, 0, 'redirectStart should be 0 in cross-origin request.');
+ assert_equals(entry.redirectEnd, 0, 'redirectEnd should be 0 in cross-origin request.');
+ assert_equals(entry.domainLookupStart, entry.fetchStart, 'domainLookupStart should be 0 in cross-origin request.');
+ assert_equals(entry.domainLookupEnd, entry.fetchStart, 'domainLookupEnd should be 0 in cross-origin request.');
+ assert_equals(entry.connectStart, entry.fetchStart, 'connectStart should be 0 in cross-origin request.');
+ assert_equals(entry.connectEnd, entry.fetchStart, 'connectEnd should be 0 in cross-origin request.');
+ assert_greater_than(entry.responseStart, entry.fetchStart, 'responseStart should be 0 in cross-origin request.');
+ assert_equals(entry.secureConnectionStart, entry.fetchStart, 'secureConnectionStart should be 0 in cross-origin request.');
+ assert_equals(entry.transferSize, 0, 'decodedBodySize should be 0 in cross-origin request.');
+ frame.remove();
+ await registration.unregister();
+ }, `Test that timing allow check fails when service worker changes origin from same to cross origin (${mode}).`);
+}
+
+test_sw_resource_timing({ mode: "cors" });
+test_sw_resource_timing({ mode: "no-cors" });
+
+
+</script>
+</body>
+</html>
diff --git a/test/wpt/tests/service-workers/service-worker/resource-timing-fetch-variants.https.html b/test/wpt/tests/service-workers/service-worker/resource-timing-fetch-variants.https.html
new file mode 100644
index 0000000..8d4f0be
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resource-timing-fetch-variants.https.html
@@ -0,0 +1,121 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Test various interactions between fetch, service-workers and resource timing</title>
+<meta charset="utf-8" />
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<link rel="help" href="https://w3c.github.io/resource-timing/" >
+<!--
+ This test checks that the different properties in a PerformanceResourceTimingEntry
+ measure what they are supposed to measure according to spec.
+
+ It is achieved by generating programmatic delays and redirects inside a service worker,
+ and checking how the different metrics respond to the delays and redirects.
+
+ The deltas are not measured precisely, but rather relatively to the delay.
+ The delay needs to be long enough so that it's clear that what's measured is the test's
+ programmatic delay and not arbitrary system delays.
+-->
+</head>
+
+<body>
+<script>
+
+const delay = 200;
+const absolutePath = `${base_path()}/simple.txt`
+function toSequence({before, after, entry}) {
+ /*
+ The order of keys is the same as in this chart:
+ https://w3c.github.io/resource-timing/#attribute-descriptions
+ */
+ const keys = [
+ 'startTime',
+ 'redirectStart',
+ 'redirectEnd',
+ 'workerStart',
+ 'fetchStart',
+ 'connectStart',
+ 'requestStart',
+ 'responseStart',
+ 'responseEnd'
+ ];
+
+ let cursor = before;
+ const step = value => {
+ // A zero/null value, reflect that in the sequence
+ if (!value)
+ return value;
+
+ // Value is the same as before
+ if (value === cursor)
+ return "same";
+
+ // Oops, value is in the wrong place
+ if (value < cursor)
+ return "back";
+
+ // Delta is greater than programmatic delay, this is where the delay is measured.
+ if ((value - cursor) >= delay)
+ return "delay";
+
+ // Some small delta, probably measuring an actual networking stack delay
+ return "tick";
+ }
+
+ const res = keys.map(key => {
+ const value = step(entry[key]);
+ if (entry[key])
+ cursor = entry[key];
+ return [key, value];
+ });
+
+ return Object.fromEntries([...res, ['after', step(after)]]);
+}
+async function testVariant(t, variant) {
+ const worker_url = 'resources/fetch-variants-worker.js';
+ const url = encodeURIComponent(`simple.txt?delay=${delay}&variant=${variant}`);
+ const scope = `resources/iframe-with-fetch-variants.html?url=${url}`;
+ const registration = await service_worker_unregister_and_register(t, worker_url, scope);
+ t.add_cleanup(() => registration.unregister());
+ await wait_for_state(t, registration.installing, 'activated');
+ const frame = await with_iframe(scope);
+ t.add_cleanup(() => frame.remove());
+ const result = await new Promise(resolve => window.addEventListener('message', message => {
+ resolve(message.data);
+ }))
+
+ return toSequence(result);
+}
+
+promise_test(async t => {
+ const result = await testVariant(t, 'redirect');
+ assert_equals(result.redirectStart, 0);
+}, 'Redirects done from within a service-worker should not be exposed to client ResourceTiming');
+
+promise_test(async t => {
+ const result = await testVariant(t, 'forward');
+ assert_equals(result.connectStart, 'same');
+}, 'Connection info from within a service-worker should not be exposed to client ResourceTiming');
+
+promise_test(async t => {
+ const result = await testVariant(t, 'forward');
+ assert_not_equals(result.requestStart, 'back');
+}, 'requestStart should never be before fetchStart');
+
+promise_test(async t => {
+ const result = await testVariant(t, 'delay-after-fetch');
+ const whereIsDelayMeasured = Object.entries(result).find(r => r[1] === 'delay')[0];
+ assert_equals(whereIsDelayMeasured, 'responseStart');
+}, 'Delay from within service-worker (after internal fetching) should be accessible through `responseStart`');
+
+promise_test(async t => {
+ const result = await testVariant(t, 'delay-before-fetch');
+ const whereIsDelayMeasured = Object.entries(result).find(r => r[1] === 'delay')[0];
+ assert_equals(whereIsDelayMeasured, 'responseStart');
+}, 'Delay from within service-worker (before internal fetching) should be measured before responseStart in the client ResourceTiming entry');
+</script>
+
+</body>
+</html>
diff --git a/test/wpt/tests/service-workers/service-worker/resource-timing.sub.https.html b/test/wpt/tests/service-workers/service-worker/resource-timing.sub.https.html
new file mode 100644
index 0000000..e8328f3
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resource-timing.sub.https.html
@@ -0,0 +1,150 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+function resourceUrl(path) {
+ return "https://{{host}}:{{ports[https][0]}}" + base_path() + path;
+}
+
+function crossOriginUrl(path) {
+ return "https://{{hosts[alt][]}}:{{ports[https][0]}}" + base_path() + path;
+}
+
+// Verify existance of a PerformanceEntry and the order between the timings.
+//
+// |options| has these properties:
+// performance: Performance interface to verify existance of the entry.
+// resource: the path to the resource.
+// mode: 'cross-origin' to load the resource from third-party origin.
+// description: the description passed to each assertion.
+// should_no_performance_entry: no entry is expected to be recorded when it's
+// true.
+function verify(options) {
+ const url = options.mode === 'cross-origin' ? crossOriginUrl(options.resource)
+ : resourceUrl(options.resource);
+ const entryList = options.performance.getEntriesByName(url, 'resource');
+ if (options.should_no_performance_entry) {
+ // The performance timeline may not have an entry for a resource
+ // which failed to load.
+ assert_equals(entryList.length, 0, options.description);
+ return;
+ }
+
+ assert_equals(entryList.length, 1, options.description);
+ const entry = entryList[0];
+ assert_equals(entry.entryType, 'resource', options.description);
+
+ // workerStart is recorded between startTime and fetchStart.
+ assert_greater_than(entry.workerStart, 0, options.description);
+ assert_greater_than_equal(entry.workerStart, entry.startTime, options.description);
+ assert_less_than_equal(entry.workerStart, entry.fetchStart, options.description);
+
+ if (options.mode === 'cross-origin') {
+ assert_equals(entry.responseStart, 0, options.description);
+ assert_greater_than_equal(entry.responseEnd, entry.fetchStart, options.description);
+ } else {
+ assert_greater_than_equal(entry.responseStart, entry.fetchStart, options.description);
+ assert_greater_than_equal(entry.responseEnd, entry.responseStart, options.description);
+ }
+
+ // responseEnd follows fetchStart.
+ assert_greater_than(entry.responseEnd, entry.fetchStart, options.description);
+ // duration always has some value.
+ assert_greater_than(entry.duration, 0, options.description);
+
+ if (options.resource.indexOf('redirect.py') != -1) {
+ assert_less_than_equal(entry.workerStart, entry.redirectStart,
+ options.description);
+ } else {
+ assert_equals(entry.redirectStart, 0, options.description);
+ }
+}
+
+promise_test(async (t) => {
+ const worker_url = 'resources/resource-timing-worker.js';
+ const scope = 'resources/resource-timing-iframe.sub.html';
+
+ const registration = await service_worker_unregister_and_register(t, worker_url, scope);
+ t.add_cleanup(() => registration.unregister());
+ await wait_for_state(t, registration.installing, 'activated');
+ const frame = await with_iframe(scope);
+ t.add_cleanup(() => frame.remove());
+
+ const performance = frame.contentWindow.performance;
+ verify({
+ performance: performance,
+ resource: 'resources/sample.js',
+ mode: 'same-origin',
+ description: 'Generated response',
+ });
+ verify({
+ performance: performance,
+ resource: 'resources/empty.js',
+ mode: 'same-origin',
+ description: 'Network fallback',
+ });
+ verify({
+ performance: performance,
+ resource: 'resources/redirect.py?Redirect=empty.js',
+ mode: 'same-origin',
+ description: 'Redirect',
+ });
+ verify({
+ performance: performance,
+ resource: 'resources/square.png',
+ mode: 'same-origin',
+ description: 'Network fallback image',
+ });
+ // Test that worker start is available on cross-origin no-cors
+ // subresources.
+ verify({
+ performance: performance,
+ resource: 'resources/square.png',
+ mode: 'cross-origin',
+ description: 'Network fallback cross-origin image',
+ });
+
+ // Tests for resouces which failed to load.
+ verify({
+ performance: performance,
+ resource: 'resources/missing.jpg',
+ mode: 'same-origin',
+ description: 'Network fallback load failure',
+ });
+ verify({
+ performance: performance,
+ resource: 'resources/missing.asis', // ORB-compatible 404 response.
+ mode: 'cross-origin',
+ description: 'Network fallback cross-origin load failure (404 response)',
+ });
+ // Tests for respondWith(fetch()).
+ verify({
+ performance: performance,
+ resource: 'resources/missing.jpg?SWRespondsWithFetch',
+ mode: 'same-origin',
+ description: 'Resource in iframe, nonexistent but responded with fetch to another.',
+ });
+ verify({
+ performance: performance,
+ resource: 'resources/sample.txt?SWFetched',
+ mode: 'same-origin',
+ description: 'Resource fetched as response from missing.jpg?SWRespondsWithFetch.',
+ should_no_performance_entry: true,
+ });
+ // Test for a normal resource that is unaffected by the Service Worker.
+ verify({
+ performance: performance,
+ resource: 'resources/empty-worker.js',
+ mode: 'same-origin',
+ description: 'Resource untouched by the Service Worker.',
+ });
+}, 'Controlled resource loads');
+
+test(() => {
+ const url = resourceUrl('resources/test-helpers.sub.js');
+ const entry = window.performance.getEntriesByName(url, 'resource')[0];
+ assert_equals(entry.workerStart, 0, 'Non-controlled');
+}, 'Non-controlled resource loads');
+
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/resources/404.py b/test/wpt/tests/service-workers/service-worker/resources/404.py
new file mode 100644
index 0000000..1ee4af1
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/404.py
@@ -0,0 +1,5 @@
+# iframe does not fire onload event if the response's content-type is not
+# text/plain or text/html so this script exists if you want to test a 404 load
+# in an iframe.
+def main(req, res):
+ return 404, [(b'Content-Type', b'text/plain')], b"Page not found"
diff --git a/test/wpt/tests/service-workers/service-worker/resources/about-blank-replacement-blank-dynamic-nested-frame.html b/test/wpt/tests/service-workers/service-worker/resources/about-blank-replacement-blank-dynamic-nested-frame.html
new file mode 100644
index 0000000..1e0c620
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/about-blank-replacement-blank-dynamic-nested-frame.html
@@ -0,0 +1,21 @@
+<!doctype html>
+<html>
+<body>
+<script>
+function nestedLoaded() {
+ parent.postMessage({ type: 'NESTED_LOADED' }, '*');
+}
+
+// dynamically add an about:blank iframe
+var f = document.createElement('iframe');
+f.onload = nestedLoaded;
+document.body.appendChild(f);
+
+// Helper routine to make it slightly easier for our parent to find
+// the nested frame.
+function nested() {
+ return f.contentWindow;
+}
+</script>
+</body>
+</html>
diff --git a/test/wpt/tests/service-workers/service-worker/resources/about-blank-replacement-blank-nested-frame.html b/test/wpt/tests/service-workers/service-worker/resources/about-blank-replacement-blank-nested-frame.html
new file mode 100644
index 0000000..16f7e7c
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/about-blank-replacement-blank-nested-frame.html
@@ -0,0 +1,21 @@
+<!doctype html>
+<html>
+<body>
+<script>
+function nestedLoaded() {
+ parent.postMessage({ type: 'NESTED_LOADED' }, '*');
+}
+
+// Helper routine to make it slightly easier for our parent to find
+// the nested frame.
+function nested() {
+ return document.getElementById('nested').contentWindow;
+}
+
+// NOTE: Make sure not to touch the iframe directly here. We want to
+// test the case where the initial about:blank document is not
+// directly accessed before load.
+</script>
+<iframe id="nested" onload="nestedLoaded()"></iframe>
+</body>
+</html>
diff --git a/test/wpt/tests/service-workers/service-worker/resources/about-blank-replacement-frame.py b/test/wpt/tests/service-workers/service-worker/resources/about-blank-replacement-frame.py
new file mode 100644
index 0000000..a29ff9d
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/about-blank-replacement-frame.py
@@ -0,0 +1,31 @@
+def main(request, response):
+ if b'nested' in request.GET:
+ return (
+ [(b'Content-Type', b'text/html')],
+ b'failed: nested frame was not intercepted by the service worker'
+ )
+
+ return ([(b'Content-Type', b'text/html')], b"""
+<!doctype html>
+<html>
+<body>
+<script>
+function nestedLoaded() {
+ parent.postMessage({ type: 'NESTED_LOADED' }, '*');
+}
+</script>
+<iframe src="?nested=true" id="nested" onload="nestedLoaded()"></iframe>
+<script>
+// Helper routine to make it slightly easier for our parent to find
+// the nested frame.
+function nested() {
+ return document.getElementById('nested').contentWindow;
+}
+
+// NOTE: Make sure not to touch the iframe directly here. We want to
+// test the case where the initial about:blank document is not
+// directly accessed before load.
+</script>
+</body>
+</html>
+""")
diff --git a/test/wpt/tests/service-workers/service-worker/resources/about-blank-replacement-ping-frame.py b/test/wpt/tests/service-workers/service-worker/resources/about-blank-replacement-ping-frame.py
new file mode 100644
index 0000000..30fbbbb
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/about-blank-replacement-ping-frame.py
@@ -0,0 +1,49 @@
+def main(request, response):
+ if b'nested' in request.GET:
+ return (
+ [(b'Content-Type', b'text/html')],
+ b'failed: nested frame was not intercepted by the service worker'
+ )
+
+ return ([(b'Content-Type', b'text/html')], b"""
+<!doctype html>
+<html>
+<body>
+<script>
+function nestedLoaded() {
+ parent.postMessage({ type: 'NESTED_LOADED' }, '*');
+}
+</script>
+<iframe src="?nested=true&amp;ping=true" id="nested" onload="nestedLoaded()"></iframe>
+<script>
+// Helper routine to make it slightly easier for our parent to find
+// the nested frame.
+function nested() {
+ return document.getElementById('nested').contentWindow;
+}
+
+// This modifies the nested iframe immediately and does not wait for it to
+// load. This effectively modifies the global for the initial about:blank
+// document. Any modifications made here should be preserved after the
+// frame loads because the global should be re-used.
+let win = nested();
+if (win.location.href !== 'about:blank') {
+ parent.postMessage({
+ type: 'NESTED_LOADED',
+ result: 'failed: nested iframe does not have an initial about:blank URL'
+ }, '*');
+} else {
+ win.navigator.serviceWorker.addEventListener('message', evt => {
+ if (evt.data.type === 'PING') {
+ evt.source.postMessage({
+ type: 'PONG',
+ location: win.location.toString()
+ });
+ }
+ });
+ win.navigator.serviceWorker.startMessages();
+}
+</script>
+</body>
+</html>
+""")
diff --git a/test/wpt/tests/service-workers/service-worker/resources/about-blank-replacement-popup-frame.py b/test/wpt/tests/service-workers/service-worker/resources/about-blank-replacement-popup-frame.py
new file mode 100644
index 0000000..04c12a6
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/about-blank-replacement-popup-frame.py
@@ -0,0 +1,32 @@
+def main(request, response):
+ if b'nested' in request.GET:
+ return (
+ [(b'Content-Type', b'text/html')],
+ b'failed: nested frame was not intercepted by the service worker'
+ )
+
+ return ([(b'Content-Type', b'text/html')], b"""
+<!doctype html>
+<html>
+<body>
+<script>
+function nestedLoaded() {
+ parent.postMessage({ type: 'NESTED_LOADED' }, '*');
+}
+
+let popup = window.open('?nested=true');
+popup.onload = nestedLoaded;
+
+addEventListener('unload', evt => {
+ popup.close();
+}, { once: true });
+
+// Helper routine to make it slightly easier for our parent to find
+// the nested popup window.
+function nested() {
+ return popup;
+}
+</script>
+</body>
+</html>
+""")
diff --git a/test/wpt/tests/service-workers/service-worker/resources/about-blank-replacement-srcdoc-nested-frame.html b/test/wpt/tests/service-workers/service-worker/resources/about-blank-replacement-srcdoc-nested-frame.html
new file mode 100644
index 0000000..0122a00
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/about-blank-replacement-srcdoc-nested-frame.html
@@ -0,0 +1,22 @@
+<!doctype html>
+<html>
+<body>
+<script>
+function nestedLoaded() {
+ parent.postMessage({ type: 'NESTED_LOADED' }, '*');
+}
+</script>
+<iframe id="nested" srcdoc="<div></div>" onload="nestedLoaded()"></iframe>
+<script>
+// Helper routine to make it slightly easier for our parent to find
+// the nested frame.
+function nested() {
+ return document.getElementById('nested').contentWindow;
+}
+
+// NOTE: Make sure not to touch the iframe directly here. We want to
+// test the case where the initial about:blank document is not
+// directly accessed before load.
+</script>
+</body>
+</html>
diff --git a/test/wpt/tests/service-workers/service-worker/resources/about-blank-replacement-uncontrolled-nested-frame.html b/test/wpt/tests/service-workers/service-worker/resources/about-blank-replacement-uncontrolled-nested-frame.html
new file mode 100644
index 0000000..8950915
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/about-blank-replacement-uncontrolled-nested-frame.html
@@ -0,0 +1,22 @@
+<!doctype html>
+<html>
+<body>
+<script>
+function nestedLoaded() {
+ parent.postMessage({ type: 'NESTED_LOADED' }, '*');
+}
+</script>
+<iframe src="empty.html?nested=true" id="nested" onload="nestedLoaded()"></iframe>
+<script>
+// Helper routine to make it slightly easier for our parent to find
+// the nested frame.
+function nested() {
+ return document.getElementById('nested').contentWindow;
+}
+
+// NOTE: Make sure not to touch the iframe directly here. We want to
+// test the case where the initial about:blank document is not
+// directly accessed before load.
+</script>
+</body>
+</html>
diff --git a/test/wpt/tests/service-workers/service-worker/resources/about-blank-replacement-worker.js b/test/wpt/tests/service-workers/service-worker/resources/about-blank-replacement-worker.js
new file mode 100644
index 0000000..f43598e
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/about-blank-replacement-worker.js
@@ -0,0 +1,95 @@
+// Helper routine to find a client that matches a particular URL. Note, we
+// require that Client to be controlled to avoid false matches with other
+// about:blank windows the browser might have. The initial about:blank should
+// inherit the controller from its parent.
+async function getClientByURL(url) {
+ let list = await clients.matchAll();
+ return list.find(client => client.url === url);
+}
+
+// Helper routine to perform a ping-pong with the given target client. We
+// expect the Client to respond with its location URL.
+async function pingPong(target) {
+ function waitForPong() {
+ return new Promise(resolve => {
+ self.addEventListener('message', function onMessage(evt) {
+ if (evt.data.type === 'PONG') {
+ resolve(evt.data.location);
+ }
+ });
+ });
+ }
+
+ target.postMessage({ type: 'PING' })
+ return await waitForPong(target);
+}
+
+addEventListener('fetch', async evt => {
+ let url = new URL(evt.request.url);
+ if (!url.searchParams.get('nested')) {
+ return;
+ }
+
+ evt.respondWith(async function() {
+ // Find the initial about:blank document.
+ const client = await getClientByURL('about:blank');
+ if (!client) {
+ return new Response('failure: could not find about:blank client');
+ }
+
+ // If the nested frame is configured to support a ping-pong, then
+ // ping it now to verify its message listener exists. We also
+ // verify the Client's idea of its own location URL while we are doing
+ // this.
+ if (url.searchParams.get('ping')) {
+ const loc = await pingPong(client);
+ if (loc !== 'about:blank') {
+ return new Response(`failure: got location {$loc}, expected about:blank`);
+ }
+ }
+
+ // Finally, allow the nested frame to complete loading. We place the
+ // Client ID we found for the initial about:blank in the body.
+ return new Response(client.id);
+ }());
+});
+
+addEventListener('message', evt => {
+ if (evt.data.type !== 'GET_CLIENT_ID') {
+ return;
+ }
+
+ evt.waitUntil(async function() {
+ let url = new URL(evt.data.url);
+
+ // Find the given Client by its URL.
+ let client = await getClientByURL(evt.data.url);
+ if (!client) {
+ evt.source.postMessage({
+ type: 'GET_CLIENT_ID',
+ result: `failure: could not find ${evt.data.url} client`
+ });
+ return;
+ }
+
+ // If the Client supports a ping-pong, then do it now to verify
+ // the message listener exists and its location matches the
+ // Client object.
+ if (url.searchParams.get('ping')) {
+ let loc = await pingPong(client);
+ if (loc !== evt.data.url) {
+ evt.source.postMessage({
+ type: 'GET_CLIENT_ID',
+ result: `failure: got location ${loc}, expected ${evt.data.url}`
+ });
+ return;
+ }
+ }
+
+ // Finally, send the client ID back.
+ evt.source.postMessage({
+ type: 'GET_CLIENT_ID',
+ result: client.id
+ });
+ }());
+});
diff --git a/test/wpt/tests/service-workers/service-worker/resources/basic-module-2.js b/test/wpt/tests/service-workers/service-worker/resources/basic-module-2.js
new file mode 100644
index 0000000..189b1c8
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/basic-module-2.js
@@ -0,0 +1 @@
+export default 'hello again!';
diff --git a/test/wpt/tests/service-workers/service-worker/resources/basic-module.js b/test/wpt/tests/service-workers/service-worker/resources/basic-module.js
new file mode 100644
index 0000000..789a89b
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/basic-module.js
@@ -0,0 +1 @@
+export default 'hello!';
diff --git a/test/wpt/tests/service-workers/service-worker/resources/blank.html b/test/wpt/tests/service-workers/service-worker/resources/blank.html
new file mode 100644
index 0000000..a3c3a46
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/blank.html
@@ -0,0 +1,2 @@
+<!DOCTYPE html>
+<title>Empty doc</title>
diff --git a/test/wpt/tests/service-workers/service-worker/resources/bytecheck-worker-imported-script.py b/test/wpt/tests/service-workers/service-worker/resources/bytecheck-worker-imported-script.py
new file mode 100644
index 0000000..1931c77
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/bytecheck-worker-imported-script.py
@@ -0,0 +1,20 @@
+import time
+
+def main(request, response):
+ headers = [(b'Content-Type', b'application/javascript'),
+ (b'Cache-Control', b'max-age=0'),
+ (b'Access-Control-Allow-Origin', b'*')]
+
+ imported_content_type = b''
+ if b'imported' in request.GET:
+ imported_content_type = request.GET[b'imported']
+
+ imported_content = b'default'
+ if imported_content_type == b'time':
+ imported_content = b'%f' % time.time()
+
+ body = b'''
+ // %s
+ ''' % (imported_content)
+
+ return headers, body
diff --git a/test/wpt/tests/service-workers/service-worker/resources/bytecheck-worker.py b/test/wpt/tests/service-workers/service-worker/resources/bytecheck-worker.py
new file mode 100644
index 0000000..10f3bce
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/bytecheck-worker.py
@@ -0,0 +1,38 @@
+import time
+
+def main(request, response):
+ headers = [(b'Content-Type', b'application/javascript'),
+ (b'Cache-Control', b'max-age=0')]
+
+ main_content_type = b''
+ if b'main' in request.GET:
+ main_content_type = request.GET[b'main']
+
+ main_content = b'default'
+ if main_content_type == b'time':
+ main_content = b'%f' % time.time()
+
+ imported_request_path = b''
+ if b'path' in request.GET:
+ imported_request_path = request.GET[b'path']
+
+ imported_request_type = b''
+ if b'imported' in request.GET:
+ imported_request_type = request.GET[b'imported']
+
+ imported_request = b''
+ if imported_request_type == b'time':
+ imported_request = b'?imported=time'
+
+ if b'type' in request.GET and request.GET[b'type'] == b'module':
+ body = b'''
+ // %s
+ import '%sbytecheck-worker-imported-script.py%s';
+ ''' % (main_content, imported_request_path, imported_request)
+ else:
+ body = b'''
+ // %s
+ importScripts('%sbytecheck-worker-imported-script.py%s');
+ ''' % (main_content, imported_request_path, imported_request)
+
+ return headers, body
diff --git a/test/wpt/tests/service-workers/service-worker/resources/claim-blob-url-worker-fetch-iframe.html b/test/wpt/tests/service-workers/service-worker/resources/claim-blob-url-worker-fetch-iframe.html
new file mode 100644
index 0000000..12ae1a8
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/claim-blob-url-worker-fetch-iframe.html
@@ -0,0 +1,21 @@
+<!doctype html>
+<script>
+const baseLocation = window.location;
+const workerScript =
+ `self.onmessage = async (e) => {
+ const url = new URL(e.data, '${baseLocation}').href;
+ const response = await fetch(url);
+ const text = await response.text();
+ self.postMessage(text);
+ };`;
+const blob = new Blob([workerScript], { type: 'text/javascript' });
+const blobUrl = URL.createObjectURL(blob);
+const worker = new Worker(blobUrl);
+
+function fetch_in_worker(url) {
+ return new Promise((resolve) => {
+ worker.onmessage = (e) => resolve(e.data);
+ worker.postMessage(url);
+ });
+}
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/resources/claim-nested-worker-fetch-iframe.html b/test/wpt/tests/service-workers/service-worker/resources/claim-nested-worker-fetch-iframe.html
new file mode 100644
index 0000000..2fa15db
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/claim-nested-worker-fetch-iframe.html
@@ -0,0 +1,16 @@
+<!doctype html>
+<script>
+// An iframe that starts a nested worker. Our parent frame (the test page) calls
+// fetch_in_worker() to ask the nested worker to perform a fetch to see whether
+// it's controlled by a service worker.
+var worker = new Worker('./claim-nested-worker-fetch-parent-worker.js');
+
+function fetch_in_worker(url) {
+ return new Promise((resolve) => {
+ worker.onmessage = (event) => {
+ resolve(event.data);
+ };
+ worker.postMessage(url);
+ });
+}
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/resources/claim-nested-worker-fetch-parent-worker.js b/test/wpt/tests/service-workers/service-worker/resources/claim-nested-worker-fetch-parent-worker.js
new file mode 100644
index 0000000..f5ff7c2
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/claim-nested-worker-fetch-parent-worker.js
@@ -0,0 +1,12 @@
+try {
+ var worker = new Worker('./claim-worker-fetch-worker.js');
+
+ self.onmessage = (event) => {
+ worker.postMessage(event.data);
+ }
+ worker.onmessage = (event) => {
+ self.postMessage(event.data);
+ };
+} catch (e) {
+ self.postMessage("Fail: " + e.data);
+}
diff --git a/test/wpt/tests/service-workers/service-worker/resources/claim-shared-worker-fetch-iframe.html b/test/wpt/tests/service-workers/service-worker/resources/claim-shared-worker-fetch-iframe.html
new file mode 100644
index 0000000..ad865b8
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/claim-shared-worker-fetch-iframe.html
@@ -0,0 +1,13 @@
+<!doctype html>
+<script>
+var worker = new SharedWorker('./claim-shared-worker-fetch-worker.js');
+
+function fetch_in_shared_worker(url) {
+ return new Promise((resolve) => {
+ worker.port.onmessage = (event) => {
+ resolve(event.data);
+ };
+ worker.port.postMessage(url);
+ });
+}
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/resources/claim-shared-worker-fetch-worker.js b/test/wpt/tests/service-workers/service-worker/resources/claim-shared-worker-fetch-worker.js
new file mode 100644
index 0000000..ddc8bea
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/claim-shared-worker-fetch-worker.js
@@ -0,0 +1,8 @@
+self.onconnect = (event) => {
+ var port = event.ports[0];
+ event.ports[0].onmessage = (evt) => {
+ fetch(evt.data)
+ .then(response => response.text())
+ .then(text => port.postMessage(text));
+ };
+};
diff --git a/test/wpt/tests/service-workers/service-worker/resources/claim-with-redirect-iframe.html b/test/wpt/tests/service-workers/service-worker/resources/claim-with-redirect-iframe.html
new file mode 100644
index 0000000..4150d7e
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/claim-with-redirect-iframe.html
@@ -0,0 +1,48 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="test-helpers.sub.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<body>
+<script>
+var host_info = get_host_info();
+
+function send_result(result) {
+ window.parent.postMessage({message: result},
+ host_info['HTTPS_ORIGIN']);
+}
+
+function executeTask(params) {
+ // Execute task for each parameter
+ if (params.has('register')) {
+ var worker_url = decodeURIComponent(params.get('register'));
+ var scope = decodeURIComponent(params.get('scope'));
+ navigator.serviceWorker.register(worker_url, {scope: scope})
+ .then(r => send_result('registered'));
+ } else if (params.has('redirected')) {
+ send_result('redirected');
+ } else if (params.has('update')) {
+ var scope = decodeURIComponent(params.get('update'));
+ navigator.serviceWorker.getRegistration(scope)
+ .then(r => r.update())
+ .then(() => send_result('updated'));
+ } else if (params.has('unregister')) {
+ var scope = decodeURIComponent(params.get('unregister'));
+ navigator.serviceWorker.getRegistration(scope)
+ .then(r => r.unregister())
+ .then(succeeded => {
+ if (succeeded) {
+ send_result('unregistered');
+ } else {
+ send_result('failure: unregister');
+ }
+ });
+ } else {
+ send_result('unknown parameter: ' + params.toString());
+ }
+}
+
+var params = new URLSearchParams(location.search.slice(1));
+executeTask(params);
+</script>
+</body>
diff --git a/test/wpt/tests/service-workers/service-worker/resources/claim-worker-fetch-iframe.html b/test/wpt/tests/service-workers/service-worker/resources/claim-worker-fetch-iframe.html
new file mode 100644
index 0000000..92c5d15
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/claim-worker-fetch-iframe.html
@@ -0,0 +1,13 @@
+<!doctype html>
+<script>
+var worker = new Worker('./claim-worker-fetch-worker.js');
+
+function fetch_in_worker(url) {
+ return new Promise((resolve) => {
+ worker.onmessage = (event) => {
+ resolve(event.data);
+ };
+ worker.postMessage(url);
+ });
+}
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/resources/claim-worker-fetch-worker.js b/test/wpt/tests/service-workers/service-worker/resources/claim-worker-fetch-worker.js
new file mode 100644
index 0000000..7080181
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/claim-worker-fetch-worker.js
@@ -0,0 +1,5 @@
+self.onmessage = (event) => {
+ fetch(event.data)
+ .then(response => response.text())
+ .then(text => self.postMessage(text));
+};
diff --git a/test/wpt/tests/service-workers/service-worker/resources/claim-worker.js b/test/wpt/tests/service-workers/service-worker/resources/claim-worker.js
new file mode 100644
index 0000000..1800407
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/claim-worker.js
@@ -0,0 +1,19 @@
+self.addEventListener('message', function(event) {
+ self.clients.claim()
+ .then(function(result) {
+ if (result !== undefined) {
+ event.data.port.postMessage(
+ 'FAIL: claim() should be resolved with undefined');
+ return;
+ }
+ event.data.port.postMessage('PASS');
+ })
+ .catch(function(error) {
+ event.data.port.postMessage('FAIL: exception: ' + error.name);
+ });
+ });
+
+self.addEventListener('fetch', function(event) {
+ if (!/404/.test(event.request.url))
+ event.respondWith(new Response('Intercepted!'));
+ });
diff --git a/test/wpt/tests/service-workers/service-worker/resources/classic-worker.js b/test/wpt/tests/service-workers/service-worker/resources/classic-worker.js
new file mode 100644
index 0000000..36a32b1
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/classic-worker.js
@@ -0,0 +1 @@
+importScripts('./imported-classic-script.js');
diff --git a/test/wpt/tests/service-workers/service-worker/resources/client-id-worker.js b/test/wpt/tests/service-workers/service-worker/resources/client-id-worker.js
new file mode 100644
index 0000000..ec71b34
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/client-id-worker.js
@@ -0,0 +1,27 @@
+self.onmessage = function(e) {
+ var port = e.data.port;
+ var message = [];
+
+ var promise = Promise.resolve()
+ .then(function() {
+ // 1st matchAll()
+ return self.clients.matchAll().then(function(clients) {
+ clients.forEach(function(client) {
+ message.push(client.id);
+ });
+ });
+ })
+ .then(function() {
+ // 2nd matchAll()
+ return self.clients.matchAll().then(function(clients) {
+ clients.forEach(function(client) {
+ message.push(client.id);
+ });
+ });
+ })
+ .then(function() {
+ // Send an array containing ids of clients from 1st and 2nd matchAll()
+ port.postMessage(message);
+ });
+ e.waitUntil(promise);
+};
diff --git a/test/wpt/tests/service-workers/service-worker/resources/client-navigate-frame.html b/test/wpt/tests/service-workers/service-worker/resources/client-navigate-frame.html
new file mode 100644
index 0000000..7e186f8
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/client-navigate-frame.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<script>
+ fetch("clientId")
+ .then(function(response) {
+ return response.text();
+ })
+ .then(function(text) {
+ parent.postMessage({id: text}, "*");
+ });
+</script>
+<body style="background-color: red;"></body>
diff --git a/test/wpt/tests/service-workers/service-worker/resources/client-navigate-worker.js b/test/wpt/tests/service-workers/service-worker/resources/client-navigate-worker.js
new file mode 100644
index 0000000..6101d5d
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/client-navigate-worker.js
@@ -0,0 +1,92 @@
+importScripts("worker-testharness.js");
+importScripts("test-helpers.sub.js");
+importScripts("/common/get-host-info.sub.js")
+importScripts("testharness-helpers.js")
+
+setup({ explicit_done: true });
+
+self.onfetch = function(e) {
+ if (e.request.url.indexOf("client-navigate-frame.html") >= 0) {
+ return;
+ }
+ e.respondWith(new Response(e.clientId));
+};
+
+function pass(test, url) {
+ return { result: test,
+ url: url };
+}
+
+function fail(test, reason) {
+ return { result: "FAILED " + test + " " + reason }
+}
+
+self.onmessage = function(e) {
+ var port = e.data.port;
+ var test = e.data.test;
+ var clientId = e.data.clientId;
+ var clientUrl = "";
+ if (test === "test_client_navigate_success") {
+ promise_test(function(t) {
+ this.add_cleanup(() => port.postMessage(pass(test, clientUrl)));
+ return self.clients.get(clientId)
+ .then(client => client.navigate("client-navigated-frame.html"))
+ .then(client => {
+ clientUrl = client.url;
+ assert_true(client instanceof WindowClient);
+ })
+ .catch(unreached_rejection(t));
+ }, "Return value should be instance of WindowClient");
+ done();
+ } else if (test === "test_client_navigate_cross_origin") {
+ promise_test(function(t) {
+ this.add_cleanup(() => port.postMessage(pass(test, clientUrl)));
+ var path = new URL('client-navigated-frame.html', self.location.href).pathname;
+ var url = get_host_info()['HTTPS_REMOTE_ORIGIN'] + path;
+ return self.clients.get(clientId)
+ .then(client => client.navigate(url))
+ .then(client => {
+ clientUrl = (client && client.url) || "";
+ assert_equals(client, null,
+ 'cross-origin navigate resolves with null');
+ })
+ .catch(unreached_rejection(t));
+ }, "Navigating to different origin should resolve with null");
+ done();
+ } else if (test === "test_client_navigate_about_blank") {
+ promise_test(function(t) {
+ this.add_cleanup(function() { port.postMessage(pass(test, "")); });
+ return self.clients.get(clientId)
+ .then(client => promise_rejects_js(t, TypeError, client.navigate("about:blank")))
+ .catch(unreached_rejection(t));
+ }, "Navigating to about:blank should reject with TypeError");
+ done();
+ } else if (test === "test_client_navigate_mixed_content") {
+ promise_test(function(t) {
+ this.add_cleanup(function() { port.postMessage(pass(test, "")); });
+ var path = new URL('client-navigated-frame.html', self.location.href).pathname;
+ // Insecure URL should fail since the frame is owned by a secure parent
+ // and navigating to http:// would create a mixed-content violation.
+ var url = get_host_info()['HTTP_REMOTE_ORIGIN'] + path;
+ return self.clients.get(clientId)
+ .then(client => promise_rejects_js(t, TypeError, client.navigate(url)))
+ .catch(unreached_rejection(t));
+ }, "Navigating to mixed-content iframe should reject with TypeError");
+ done();
+ } else if (test === "test_client_navigate_redirect") {
+ var host_info = get_host_info();
+ var url = new URL(host_info['HTTPS_REMOTE_ORIGIN']).toString() +
+ new URL("client-navigated-frame.html", location).pathname.substring(1);
+ promise_test(function(t) {
+ this.add_cleanup(() => port.postMessage(pass(test, clientUrl)));
+ return self.clients.get(clientId)
+ .then(client => client.navigate("redirect.py?Redirect=" + url))
+ .then(client => {
+ clientUrl = (client && client.url) || ""
+ assert_equals(client, null);
+ })
+ .catch(unreached_rejection(t));
+ }, "Redirecting to another origin should resolve with null");
+ done();
+ }
+};
diff --git a/test/wpt/tests/service-workers/service-worker/resources/client-navigated-frame.html b/test/wpt/tests/service-workers/service-worker/resources/client-navigated-frame.html
new file mode 100644
index 0000000..307f7f9
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/client-navigated-frame.html
@@ -0,0 +1,3 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<body style="background-color: green;"></body>
diff --git a/test/wpt/tests/service-workers/service-worker/resources/client-url-of-blob-url-worker.html b/test/wpt/tests/service-workers/service-worker/resources/client-url-of-blob-url-worker.html
new file mode 100644
index 0000000..00f6ace
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/client-url-of-blob-url-worker.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<html>
+<script>
+
+// Return a URL of a client when it's successful.
+function createAndFetchFromBlobWorker() {
+ const fetchURL = new URL('get-worker-client-url.txt', window.location).href;
+ const workerScript =
+ `self.onmessage = async (e) => {
+ const response = await fetch(e.data.url);
+ const text = await response.text();
+ self.postMessage({"result": text, "expected": self.location.href});
+ };`;
+ const blob = new Blob([workerScript], { type: 'text/javascript' });
+ const blobUrl = URL.createObjectURL(blob);
+
+ const worker = new Worker(blobUrl);
+ return new Promise((resolve, reject) => {
+ worker.onmessage = e => resolve(e.data);
+ worker.onerror = e => reject(e.message);
+ worker.postMessage({"url": fetchURL});
+ });
+}
+
+</script>
+</html>
diff --git a/test/wpt/tests/service-workers/service-worker/resources/client-url-of-blob-url-worker.js b/test/wpt/tests/service-workers/service-worker/resources/client-url-of-blob-url-worker.js
new file mode 100644
index 0000000..fd754f8
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/client-url-of-blob-url-worker.js
@@ -0,0 +1,10 @@
+addEventListener('fetch', e => {
+ if (e.request.url.includes('get-worker-client-url')) {
+ e.respondWith((async () => {
+ const clients = await self.clients.matchAll({type: 'worker'});
+ if (clients.length != 1)
+ return new Response('one worker client should exist');
+ return new Response(clients[0].url);
+ })());
+ }
+});
diff --git a/test/wpt/tests/service-workers/service-worker/resources/clients-frame-freeze.html b/test/wpt/tests/service-workers/service-worker/resources/clients-frame-freeze.html
new file mode 100644
index 0000000..7468a66
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/clients-frame-freeze.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script>
+ document.addEventListener('freeze', () => {
+ opener.postMessage('frozen', "*");
+ });
+
+ window.onmessage = (e) => {
+ if (e.data == 'freeze') {
+ test_driver.freeze();
+ }
+ };
+ opener.postMessage('loaded', '*');
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/resources/clients-get-client-types-frame-worker.js b/test/wpt/tests/service-workers/service-worker/resources/clients-get-client-types-frame-worker.js
new file mode 100644
index 0000000..0a1461b
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/clients-get-client-types-frame-worker.js
@@ -0,0 +1,11 @@
+onmessage = function(e) {
+ if (e.data.cmd == 'GetClientId') {
+ fetch('clientId')
+ .then(function(response) {
+ return response.text();
+ })
+ .then(function(text) {
+ e.data.port.postMessage({clientId: text});
+ });
+ }
+};
diff --git a/test/wpt/tests/service-workers/service-worker/resources/clients-get-client-types-frame.html b/test/wpt/tests/service-workers/service-worker/resources/clients-get-client-types-frame.html
new file mode 100644
index 0000000..4324e6d
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/clients-get-client-types-frame.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<script>
+fetch('clientId')
+ .then(function(response) {
+ return response.text();
+ })
+ .then(function(text) {
+ parent.postMessage({clientId: text}, '*');
+ });
+
+onmessage = function(e) {
+ if (e.data == 'StartWorker') {
+ var w = new Worker('clients-get-client-types-frame-worker.js');
+ w.postMessage({cmd:'GetClientId', port:e.ports[0]}, [e.ports[0]]);
+ }
+}
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/resources/clients-get-client-types-shared-worker.js b/test/wpt/tests/service-workers/service-worker/resources/clients-get-client-types-shared-worker.js
new file mode 100644
index 0000000..fadef97
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/clients-get-client-types-shared-worker.js
@@ -0,0 +1,10 @@
+onconnect = function(e) {
+ var port = e.ports[0];
+ fetch('clientId')
+ .then(function(response) {
+ return response.text();
+ })
+ .then(function(text) {
+ port.postMessage({clientId: text});
+ });
+};
diff --git a/test/wpt/tests/service-workers/service-worker/resources/clients-get-client-types-worker.js b/test/wpt/tests/service-workers/service-worker/resources/clients-get-client-types-worker.js
new file mode 100644
index 0000000..0a1461b
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/clients-get-client-types-worker.js
@@ -0,0 +1,11 @@
+onmessage = function(e) {
+ if (e.data.cmd == 'GetClientId') {
+ fetch('clientId')
+ .then(function(response) {
+ return response.text();
+ })
+ .then(function(text) {
+ e.data.port.postMessage({clientId: text});
+ });
+ }
+};
diff --git a/test/wpt/tests/service-workers/service-worker/resources/clients-get-cross-origin-frame.html b/test/wpt/tests/service-workers/service-worker/resources/clients-get-cross-origin-frame.html
new file mode 100644
index 0000000..e16bb11
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/clients-get-cross-origin-frame.html
@@ -0,0 +1,50 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="test-helpers.sub.js"></script>
+<script>
+var host_info = get_host_info();
+var scope = 'blank.html?clients-get';
+var script = 'clients-get-worker.js';
+
+var registration;
+var worker;
+var wait_for_worker_promise = navigator.serviceWorker.getRegistration(scope)
+ .then(function(reg) {
+ if (reg)
+ return reg.unregister();
+ })
+ .then(function() {
+ return navigator.serviceWorker.register(script, {scope: scope});
+ })
+ .then(function(reg) {
+ registration = reg;
+ worker = reg.installing;
+ return new Promise(function(resolve) {
+ worker.addEventListener('statechange', function() {
+ if (worker.state == 'activated')
+ resolve();
+ });
+ });
+ });
+
+window.addEventListener('message', function(e) {
+ var cross_origin_client_ids = [];
+ cross_origin_client_ids.push(e.data.clientId);
+ wait_for_worker_promise
+ .then(function() {
+ return with_iframe(scope);
+ })
+ .then(function(iframe) {
+ add_completion_callback(function() { iframe.remove(); });
+ navigator.serviceWorker.onmessage = function(e) {
+ registration.unregister();
+ window.parent.postMessage(
+ { type: 'clientId', value: e.data }, host_info['HTTPS_ORIGIN']
+ );
+ };
+ registration.active.postMessage({clientIds: cross_origin_client_ids});
+ });
+});
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/resources/clients-get-frame.html b/test/wpt/tests/service-workers/service-worker/resources/clients-get-frame.html
new file mode 100644
index 0000000..27143d4
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/clients-get-frame.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<script>
+
+ fetch("clientId")
+ .then(function(response) {
+ return response.text();
+ })
+ .then(function(text) {
+ parent.postMessage({clientId: text}, "*");
+ });
+
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/resources/clients-get-other-origin.html b/test/wpt/tests/service-workers/service-worker/resources/clients-get-other-origin.html
new file mode 100644
index 0000000..6342fe0
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/clients-get-other-origin.html
@@ -0,0 +1,64 @@
+<!DOCTYPE html>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="test-helpers.sub.js"></script>
+<script>
+var host_info = get_host_info();
+var SCOPE = 'blank.html?clients-get';
+var SCRIPT = 'clients-get-worker.js';
+
+var registration;
+var worker;
+var wait_for_worker_promise = navigator.serviceWorker.getRegistration(SCOPE)
+ .then(function(reg) {
+ if (reg)
+ return reg.unregister();
+ })
+ .then(function() {
+ return navigator.serviceWorker.register(SCRIPT, {scope: SCOPE});
+ })
+ .then(function(reg) {
+ registration = reg;
+ worker = reg.installing;
+ return new Promise(function(resolve) {
+ worker.addEventListener('statechange', function() {
+ if (worker.state == 'activated')
+ resolve();
+ });
+ });
+ });
+
+function send_result(result) {
+ window.parent.postMessage(
+ {result: result},
+ host_info['HTTPS_ORIGIN']);
+}
+
+window.addEventListener("message", on_message, false);
+
+function on_message(e) {
+ if (e.origin != host_info['HTTPS_ORIGIN']) {
+ console.error('invalid origin: ' + e.origin);
+ return;
+ }
+ if (e.data.message == 'get_client_id') {
+ var otherOriginClientId = e.data.clientId;
+ wait_for_worker_promise
+ .then(function() {
+ return with_iframe(SCOPE);
+ })
+ .then(function(iframe) {
+ var channel = new MessageChannel();
+ channel.port1.onmessage = function(e) {
+ navigator.serviceWorker.getRegistration(SCOPE)
+ .then(function(reg) {
+ reg.unregister();
+ send_result(e.data);
+ });
+ };
+ iframe.contentWindow.navigator.serviceWorker.controller.postMessage(
+ {port:channel.port2, clientId: otherOriginClientId,
+ message: 'get_other_client_id'}, [channel.port2]);
+ })
+ }
+}
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/resources/clients-get-resultingClientId-worker.js b/test/wpt/tests/service-workers/service-worker/resources/clients-get-resultingClientId-worker.js
new file mode 100644
index 0000000..5a46ff9
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/clients-get-resultingClientId-worker.js
@@ -0,0 +1,60 @@
+let savedPort = null;
+let savedResultingClientId = null;
+
+async function getTestingPage() {
+ const clientList = await self.clients.matchAll({ type: 'window', includeUncontrolled: true });
+ for (let c of clientList) {
+ if (c.url.endsWith('clients-get.https.html')) {
+ c.focus();
+ return c;
+ }
+ }
+ return null;
+}
+
+async function destroyResultingClient(testingPage) {
+ const destroyedPromise = new Promise(resolve => {
+ self.addEventListener('message', e => {
+ if (e.data.msg == 'resultingClientDestroyed') {
+ resolve();
+ }
+ }, {once: true});
+ });
+ testingPage.postMessage({ msg: 'destroyResultingClient' });
+ return destroyedPromise;
+}
+
+self.addEventListener('fetch', async (e) => {
+ let { resultingClientId } = e;
+ savedResultingClientId = resultingClientId;
+
+ if (e.request.url.endsWith('simple.html?fail')) {
+ e.waitUntil((async () => {
+ const testingPage = await getTestingPage();
+ await destroyResultingClient(testingPage);
+ testingPage.postMessage({ msg: 'resultingClientDestroyedAck',
+ resultingDestroyedClientId: savedResultingClientId });
+ })());
+ return;
+ }
+
+ e.respondWith(fetch(e.request));
+});
+
+self.addEventListener('message', (e) => {
+ let { msg, resultingClientId } = e.data;
+ e.waitUntil((async () => {
+ if (msg == 'getIsResultingClientUndefined') {
+ const client = await self.clients.get(resultingClientId);
+ let isUndefined = typeof client == 'undefined';
+ e.source.postMessage({ msg: 'getIsResultingClientUndefined',
+ isResultingClientUndefined: isUndefined });
+ return;
+ }
+ if (msg == 'getResultingClientId') {
+ e.source.postMessage({ msg: 'getResultingClientId',
+ resultingClientId: savedResultingClientId });
+ return;
+ }
+ })());
+});
diff --git a/test/wpt/tests/service-workers/service-worker/resources/clients-get-worker.js b/test/wpt/tests/service-workers/service-worker/resources/clients-get-worker.js
new file mode 100644
index 0000000..8effa56
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/clients-get-worker.js
@@ -0,0 +1,41 @@
+// This worker is designed to expose information about clients that is only available from Service Worker contexts.
+//
+// In the case of the `onfetch` handler, it provides the `clientId` property of
+// the `event` object. In the case of the `onmessage` handler, it provides the
+// Client instance attributes of the requested clients.
+self.onfetch = function(e) {
+ if (/\/clientId$/.test(e.request.url)) {
+ e.respondWith(new Response(e.clientId));
+ return;
+ }
+};
+
+self.onmessage = function(e) {
+ var client_ids = e.data.clientIds;
+ var message = [];
+
+ e.waitUntil(Promise.all(
+ client_ids.map(function(client_id) {
+ return self.clients.get(client_id);
+ }))
+ .then(function(clients) {
+ // No matching client for a given id or a matched client is off-origin
+ // from the service worker.
+ if (clients.length == 1 && clients[0] == undefined) {
+ e.source.postMessage(clients[0]);
+ } else {
+ clients.forEach(function(client) {
+ if (client instanceof Client) {
+ message.push([client.visibilityState,
+ client.focused,
+ client.url,
+ client.type,
+ client.frameType]);
+ } else {
+ message.push(client);
+ }
+ });
+ e.source.postMessage(message);
+ }
+ }));
+};
diff --git a/test/wpt/tests/service-workers/service-worker/resources/clients-matchall-blob-url-worker.html b/test/wpt/tests/service-workers/service-worker/resources/clients-matchall-blob-url-worker.html
new file mode 100644
index 0000000..ee89a0d
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/clients-matchall-blob-url-worker.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html>
+<script>
+const workerScript = `
+ self.onmessage = (e) => {
+ self.postMessage("Worker is ready.");
+ };
+`;
+const blob = new Blob([workerScript], { type: 'text/javascript' });
+const blobUrl = URL.createObjectURL(blob);
+const worker = new Worker(blobUrl);
+
+function waitForWorker() {
+ return new Promise(resolve => {
+ worker.onmessage = resolve;
+ worker.postMessage("Ping to worker.");
+ });
+}
+</script>
+</html>
diff --git a/test/wpt/tests/service-workers/service-worker/resources/clients-matchall-client-types-dedicated-worker.js b/test/wpt/tests/service-workers/service-worker/resources/clients-matchall-client-types-dedicated-worker.js
new file mode 100644
index 0000000..5a3f04d
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/clients-matchall-client-types-dedicated-worker.js
@@ -0,0 +1,3 @@
+onmessage = function(e) {
+ postMessage(e.data);
+};
diff --git a/test/wpt/tests/service-workers/service-worker/resources/clients-matchall-client-types-iframe.html b/test/wpt/tests/service-workers/service-worker/resources/clients-matchall-client-types-iframe.html
new file mode 100644
index 0000000..7607b03
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/clients-matchall-client-types-iframe.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<title>Empty doc</title>
+<!--
+ Change the page URL using the History API to ensure that ServiceWorkerClient
+ uses the creation URL.
+-->
+<body onload="history.pushState({}, 'title', 'url-modified-via-pushstate.html')">
+</body>
diff --git a/test/wpt/tests/service-workers/service-worker/resources/clients-matchall-client-types-shared-worker.js b/test/wpt/tests/service-workers/service-worker/resources/clients-matchall-client-types-shared-worker.js
new file mode 100644
index 0000000..1ae72fb
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/clients-matchall-client-types-shared-worker.js
@@ -0,0 +1,4 @@
+onconnect = function(e) {
+ var port = e.ports[0];
+ port.postMessage('started');
+};
diff --git a/test/wpt/tests/service-workers/service-worker/resources/clients-matchall-on-evaluation-worker.js b/test/wpt/tests/service-workers/service-worker/resources/clients-matchall-on-evaluation-worker.js
new file mode 100644
index 0000000..f1559ac
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/clients-matchall-on-evaluation-worker.js
@@ -0,0 +1,11 @@
+importScripts('test-helpers.sub.js');
+
+var page_url = normalizeURL('../clients-matchall-on-evaluation.https.html');
+
+self.clients.matchAll({includeUncontrolled: true})
+ .then(function(clients) {
+ clients.forEach(function(client) {
+ if (client.url == page_url)
+ client.postMessage('matched');
+ });
+ });
diff --git a/test/wpt/tests/service-workers/service-worker/resources/clients-matchall-worker.js b/test/wpt/tests/service-workers/service-worker/resources/clients-matchall-worker.js
new file mode 100644
index 0000000..13e111a
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/clients-matchall-worker.js
@@ -0,0 +1,40 @@
+self.onmessage = function(e) {
+ var port = e.data.port;
+ var options = e.data.options;
+
+ e.waitUntil(self.clients.matchAll(options)
+ .then(function(clients) {
+ var message = [];
+ clients.forEach(function(client) {
+ var frame_type = client.frameType;
+ if (client.url.indexOf('clients-matchall-include-uncontrolled.https.html') > -1 &&
+ client.frameType == 'auxiliary') {
+ // The test tab might be opened using window.open() by the test framework.
+ // In that case, just pretend it's top-level!
+ frame_type = 'top-level';
+ }
+ if (e.data.includeLifecycleState) {
+ message.push({visibilityState: client.visibilityState,
+ focused: client.focused,
+ url: client.url,
+ lifecycleState: client.lifecycleState,
+ type: client.type,
+ frameType: frame_type});
+ } else {
+ message.push([client.visibilityState,
+ client.focused,
+ client.url,
+ client.type,
+ frame_type]);
+ }
+ });
+ // Sort by url
+ if (!e.data.disableSort) {
+ message.sort(function(a, b) { return a[2] > b[2] ? 1 : -1; });
+ }
+ port.postMessage(message);
+ })
+ .catch(e => {
+ port.postMessage('clients.matchAll() rejected: ' + e);
+ }));
+};
diff --git a/test/wpt/tests/service-workers/service-worker/resources/controlled-frame-postMessage.html b/test/wpt/tests/service-workers/service-worker/resources/controlled-frame-postMessage.html
new file mode 100644
index 0000000..c4428e8
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/controlled-frame-postMessage.html
@@ -0,0 +1,39 @@
+<html>
+<body>
+<script>
+var messageData;
+function registerMessage()
+{
+ navigator.serviceWorker.onmessage = (e) => {
+ if (window.messageData === undefined)
+ window.messageData = e.data;
+ }
+}
+
+function listenToMessages()
+{
+ messageData = [];
+ setTimeout(() => {
+ navigator.serviceWorker.addEventListener("message", (e) => {
+ messageData.push(e.data);
+ }, { once:true });
+ }, 500);
+ setTimeout(() => {
+ navigator.serviceWorker.onmessage = (e) => {
+ messageData.push(e.data);
+ };
+ }, 1000);
+}
+
+if (window.location.search === "?repeatMessages") {
+ setTimeout(() => {
+ registerMessage();
+ }, 500);
+} else if (window.location.search.includes("listener")) {
+ listenToMessages();
+} else {
+ registerMessage();
+}
+</script>
+</body>
+</html>
diff --git a/test/wpt/tests/service-workers/service-worker/resources/controlled-worker-late-postMessage.js b/test/wpt/tests/service-workers/service-worker/resources/controlled-worker-late-postMessage.js
new file mode 100644
index 0000000..41d2db4
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/controlled-worker-late-postMessage.js
@@ -0,0 +1,6 @@
+setTimeout(() => {
+ navigator.serviceWorker.onmessage = e => self.postMessage(e.data);
+}, 500);
+setTimeout(() => {
+ self.postMessage("No message received");
+}, 5000);
diff --git a/test/wpt/tests/service-workers/service-worker/resources/controlled-worker-postMessage.js b/test/wpt/tests/service-workers/service-worker/resources/controlled-worker-postMessage.js
new file mode 100644
index 0000000..628dc65
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/controlled-worker-postMessage.js
@@ -0,0 +1,4 @@
+navigator.serviceWorker.onmessage = e => self.postMessage(e.data);
+setTimeout(() => {
+ self.postMessage("No message received");
+}, 5000);
diff --git a/test/wpt/tests/service-workers/service-worker/resources/cors-approved.txt b/test/wpt/tests/service-workers/service-worker/resources/cors-approved.txt
new file mode 100644
index 0000000..1cd89bb
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/cors-approved.txt
@@ -0,0 +1 @@
+plaintext
diff --git a/test/wpt/tests/service-workers/service-worker/resources/cors-approved.txt.headers b/test/wpt/tests/service-workers/service-worker/resources/cors-approved.txt.headers
new file mode 100644
index 0000000..f7985fd
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/cors-approved.txt.headers
@@ -0,0 +1,3 @@
+Content-Type: text/plain
+Access-Control-Allow-Origin: *
+
diff --git a/test/wpt/tests/service-workers/service-worker/resources/cors-denied.txt b/test/wpt/tests/service-workers/service-worker/resources/cors-denied.txt
new file mode 100644
index 0000000..ff333bd
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/cors-denied.txt
@@ -0,0 +1,2 @@
+this file is served without Access-Control-Allow-Origin headers so it should not
+be readable from cross-origin.
diff --git a/test/wpt/tests/service-workers/service-worker/resources/create-blob-url-worker.js b/test/wpt/tests/service-workers/service-worker/resources/create-blob-url-worker.js
new file mode 100644
index 0000000..57e4882
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/create-blob-url-worker.js
@@ -0,0 +1,22 @@
+const childWorkerScript = `
+ self.onmessage = async (e) => {
+ const response = await fetch(e.data);
+ const text = await response.text();
+ self.postMessage(text);
+ };
+`;
+const blob = new Blob([childWorkerScript], { type: 'text/javascript' });
+const blobUrl = URL.createObjectURL(blob);
+const childWorker = new Worker(blobUrl);
+
+// When a message comes from the parent frame, sends a resource url to the child
+// worker.
+self.onmessage = (e) => {
+ childWorker.postMessage(e.data);
+};
+
+// When a message comes from the child worker, sends a content of fetch() to the
+// parent frame.
+childWorker.onmessage = (e) => {
+ self.postMessage(e.data);
+};
diff --git a/test/wpt/tests/service-workers/service-worker/resources/create-out-of-scope-worker.html b/test/wpt/tests/service-workers/service-worker/resources/create-out-of-scope-worker.html
new file mode 100644
index 0000000..b51c451
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/create-out-of-scope-worker.html
@@ -0,0 +1,19 @@
+<!doctype html>
+<script>
+const workerUrl = '../out-of-scope/sample-synthesized-worker.js?dedicated';
+const worker = new Worker(workerUrl);
+const workerPromise = new Promise(resolve => {
+ worker.onmessage = e => {
+ // `e.data` is 'worker loading intercepted by service worker' when a worker
+ // is intercepted by a service worker.
+ resolve(e.data);
+ }
+ worker.onerror = _ => {
+ resolve('worker loading was not intercepted by service worker');
+ }
+});
+
+function getWorkerPromise() {
+ return workerPromise;
+}
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/resources/echo-content.py b/test/wpt/tests/service-workers/service-worker/resources/echo-content.py
new file mode 100644
index 0000000..70ae4b6
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/echo-content.py
@@ -0,0 +1,16 @@
+# This is a copy of fetch/api/resources/echo-content.py since it's more
+# convenient in this directory due to service worker's path restriction.
+from wptserve.utils import isomorphic_encode
+
+def main(request, response):
+
+ headers = [(b"X-Request-Method", isomorphic_encode(request.method)),
+ (b"X-Request-Content-Length", request.headers.get(b"Content-Length", b"NO")),
+ (b"X-Request-Content-Type", request.headers.get(b"Content-Type", b"NO")),
+
+ # Avoid any kind of content sniffing on the response.
+ (b"Content-Type", b"text/plain")]
+
+ content = request.body
+
+ return headers, content
diff --git a/test/wpt/tests/service-workers/service-worker/resources/echo-cookie-worker.py b/test/wpt/tests/service-workers/service-worker/resources/echo-cookie-worker.py
new file mode 100644
index 0000000..561f64a
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/echo-cookie-worker.py
@@ -0,0 +1,24 @@
+def main(request, response):
+ headers = [(b"Content-Type", b"text/javascript")]
+
+ values = []
+ for key in request.cookies:
+ for cookie in request.cookies.get_list(key):
+ values.append(b'"%s": "%s"' % (key, cookie.value))
+
+ # Update the counter to change the script body for every request to trigger
+ # update of the service worker.
+ key = request.GET[b'key']
+ counter = request.server.stash.take(key)
+ if counter is None:
+ counter = 0
+ counter += 1
+ request.server.stash.put(key, counter)
+
+ body = b"""
+// %d
+self.addEventListener('message', e => {
+ e.source.postMessage({%s})
+});""" % (counter, b','.join(values))
+
+ return headers, body
diff --git a/test/wpt/tests/service-workers/service-worker/resources/echo-message-to-source-worker.js b/test/wpt/tests/service-workers/service-worker/resources/echo-message-to-source-worker.js
new file mode 100644
index 0000000..bbbd35f
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/echo-message-to-source-worker.js
@@ -0,0 +1,3 @@
+addEventListener('message', evt => {
+ evt.source.postMessage(evt.data);
+});
diff --git a/test/wpt/tests/service-workers/service-worker/resources/embed-and-object-are-not-intercepted-worker.js b/test/wpt/tests/service-workers/service-worker/resources/embed-and-object-are-not-intercepted-worker.js
new file mode 100644
index 0000000..ffcdb75
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/embed-and-object-are-not-intercepted-worker.js
@@ -0,0 +1,14 @@
+// This worker intercepts a request for EMBED/OBJECT and responds with a
+// response that indicates that interception occurred. The tests expect
+// that interception does not occur.
+self.addEventListener('fetch', e => {
+ if (e.request.url.indexOf('embedded-content-from-server.html') != -1) {
+ e.respondWith(fetch('embedded-content-from-service-worker.html'));
+ return;
+ }
+
+ if (e.request.url.indexOf('green.png') != -1) {
+ e.respondWith(Promise.reject('network error to show interception occurred'));
+ return;
+ }
+ });
diff --git a/test/wpt/tests/service-workers/service-worker/resources/embed-image-is-not-intercepted-iframe.html b/test/wpt/tests/service-workers/service-worker/resources/embed-image-is-not-intercepted-iframe.html
new file mode 100644
index 0000000..7b8b257
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/embed-image-is-not-intercepted-iframe.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>iframe for embed-and-object-are-not-intercepted test</title>
+<body>
+<embed type="image/png" src="/images/green.png"></embed>
+<script>
+// Our parent (the root frame of the test) will examine this to get the result.
+var test_promise = new Promise(resolve => {
+ if (!navigator.serviceWorker.controller)
+ resolve('FAIL: this iframe is not controlled');
+
+ const elem = document.querySelector('embed');
+ elem.addEventListener('load', e => {
+ resolve('request was not intercepted');
+ });
+ elem.addEventListener('error', e => {
+ resolve('FAIL: request was intercepted');
+ });
+ });
+</script>
+</body>
diff --git a/test/wpt/tests/service-workers/service-worker/resources/embed-is-not-intercepted-iframe.html b/test/wpt/tests/service-workers/service-worker/resources/embed-is-not-intercepted-iframe.html
new file mode 100644
index 0000000..3914991
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/embed-is-not-intercepted-iframe.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>iframe for embed-and-object-are-not-intercepted test</title>
+<body>
+<script>
+// The EMBED element will call this with the result about whether the EMBED
+// request was intercepted by the service worker.
+var report_result;
+
+// Our parent (the root frame of the test) will examine this to get the result.
+var test_promise = new Promise(resolve => {
+ report_result = resolve;
+ });
+</script>
+
+<embed src="embedded-content-from-server.html"></embed>
+</body>
diff --git a/test/wpt/tests/service-workers/service-worker/resources/embed-navigation-is-not-intercepted-iframe.html b/test/wpt/tests/service-workers/service-worker/resources/embed-navigation-is-not-intercepted-iframe.html
new file mode 100644
index 0000000..5e86f67
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/embed-navigation-is-not-intercepted-iframe.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>iframe for embed-and-object-are-not-intercepted test</title>
+<body>
+<script>
+// The EMBED element will call this with the result about whether the EMBED
+// request was intercepted by the service worker.
+var report_result;
+
+// Our parent (the root frame of the test) will examine this to get the result.
+var test_promise = new Promise(resolve => {
+ report_result = resolve;
+ });
+
+let el = document.createElement('embed');
+el.src = "/common/blank.html";
+el.addEventListener('load', _ => {
+ window[0].location = "/service-workers/service-worker/resources/embedded-content-from-server.html";
+}, { once: true });
+document.body.appendChild(el);
+</script>
+
+</body>
diff --git a/test/wpt/tests/service-workers/service-worker/resources/embedded-content-from-server.html b/test/wpt/tests/service-workers/service-worker/resources/embedded-content-from-server.html
new file mode 100644
index 0000000..ff50a9c
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/embedded-content-from-server.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>embed for embed-and-object-are-not-intercepted test</title>
+<script>
+window.parent.report_result('request for embedded content was not intercepted');
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/resources/embedded-content-from-service-worker.html b/test/wpt/tests/service-workers/service-worker/resources/embedded-content-from-service-worker.html
new file mode 100644
index 0000000..2e2b923
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/embedded-content-from-service-worker.html
@@ -0,0 +1,7 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>embed for embed-and-object-are-not-intercepted test</title>
+<script>
+window.parent.report_result('request for embedded content was intercepted by service worker');
+</script>
+
diff --git a/test/wpt/tests/service-workers/service-worker/resources/empty-but-slow-worker.js b/test/wpt/tests/service-workers/service-worker/resources/empty-but-slow-worker.js
new file mode 100644
index 0000000..92abac7
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/empty-but-slow-worker.js
@@ -0,0 +1,8 @@
+addEventListener('fetch', evt => {
+ if (evt.request.url.endsWith('slow')) {
+ // Performance.now() might be a bit better here, but Date.now() has
+ // better compat in workers right now.
+ let start = Date.now();
+ while(Date.now() - start < 2000);
+ }
+});
diff --git a/test/wpt/tests/service-workers/service-worker/resources/empty-worker.js b/test/wpt/tests/service-workers/service-worker/resources/empty-worker.js
new file mode 100644
index 0000000..49ceb26
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/empty-worker.js
@@ -0,0 +1 @@
+// Do nothing.
diff --git a/test/wpt/tests/service-workers/service-worker/resources/empty.h2.js b/test/wpt/tests/service-workers/service-worker/resources/empty.h2.js
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/empty.h2.js
diff --git a/test/wpt/tests/service-workers/service-worker/resources/empty.html b/test/wpt/tests/service-workers/service-worker/resources/empty.html
new file mode 100644
index 0000000..6feb119
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/empty.html
@@ -0,0 +1,6 @@
+<!doctype html>
+<html>
+<body>
+hello world
+</body>
+</html>
diff --git a/test/wpt/tests/service-workers/service-worker/resources/empty.js b/test/wpt/tests/service-workers/service-worker/resources/empty.js
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/empty.js
diff --git a/test/wpt/tests/service-workers/service-worker/resources/enable-client-message-queue.html b/test/wpt/tests/service-workers/service-worker/resources/enable-client-message-queue.html
new file mode 100644
index 0000000..512bd14
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/enable-client-message-queue.html
@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<script>
+ // The state variable is used by handle_message to record the time
+ // at which a message was handled. It's updated by the scripts
+ // loaded by the <script> tags at the bottom of the file as well as
+ // by the event listener added here.
+ var state = 'init';
+ addEventListener('DOMContentLoaded', () => state = 'loaded');
+
+ // We expect to get three ping messages from the service worker.
+ const expected = ['init', 'install', 'start'];
+ let promises = {};
+ let resolvers = {};
+ expected.forEach(name => {
+ promises[name] = new Promise(resolve => resolvers[name] = resolve);
+ });
+
+ // Once all messages have been dispatched, the state in which each
+ // of them was dispatched is recorded in the draft. At that point
+ // the draft becomes the final report.
+ var draft = {};
+ var report = Promise.all(Object.values(promises)).then(() => window.draft);
+
+ // This message handler is installed by the 'install' script.
+ function handle_message(event) {
+ const data = event.data.data;
+ draft[data] = state;
+ resolvers[data]();
+ }
+</script>
+
+<!--
+ The controlling service worker will delay the response to these
+ fetch requests until the test instructs it how to reply. Note that
+ the event loop keeps spinning while the parser is blocked.
+-->
+<script src="empty.js?key=install"></script>
+<script src="empty.js?key=start"></script>
+<script src="empty.js?key=finish"></script>
diff --git a/test/wpt/tests/service-workers/service-worker/resources/end-to-end-worker.js b/test/wpt/tests/service-workers/service-worker/resources/end-to-end-worker.js
new file mode 100644
index 0000000..d45a505
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/end-to-end-worker.js
@@ -0,0 +1,7 @@
+onmessage = function(e) {
+ var message = e.data;
+ if (typeof message === 'object' && 'port' in message) {
+ var response = 'Ack for: ' + message.from;
+ message.port.postMessage(response);
+ }
+};
diff --git a/test/wpt/tests/service-workers/service-worker/resources/events-worker.js b/test/wpt/tests/service-workers/service-worker/resources/events-worker.js
new file mode 100644
index 0000000..80a2188
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/events-worker.js
@@ -0,0 +1,12 @@
+var eventsSeen = [];
+
+function handler(event) { eventsSeen.push(event.type); }
+
+['activate', 'install'].forEach(function(type) {
+ self.addEventListener(type, handler);
+ });
+
+onmessage = function(e) {
+ var message = e.data;
+ message.port.postMessage({events: eventsSeen});
+};
diff --git a/test/wpt/tests/service-workers/service-worker/resources/extendable-event-async-waituntil.js b/test/wpt/tests/service-workers/service-worker/resources/extendable-event-async-waituntil.js
new file mode 100644
index 0000000..8a975b0
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/extendable-event-async-waituntil.js
@@ -0,0 +1,210 @@
+// This worker calls waitUntil() and respondWith() asynchronously and
+// reports back to the test whether they threw.
+//
+// These test cases are confusing. Bear in mind that the event is active
+// (calling waitUntil() is allowed) if:
+// * The pending promise count is not 0, or
+// * The event dispatch flag is set.
+
+// Controlled by 'init'/'done' messages.
+var resolveLockPromise;
+var port;
+
+self.addEventListener('message', function(event) {
+ var waitPromise;
+ var resolveTestPromise;
+
+ switch (event.data.step) {
+ case 'init':
+ event.waitUntil(new Promise((res) => { resolveLockPromise = res; }));
+ port = event.data.port;
+ break;
+ case 'done':
+ resolveLockPromise();
+ break;
+
+ // Throws because waitUntil() is called in a task after event dispatch
+ // finishes.
+ case 'no-current-extension-different-task':
+ async_task_waituntil(event).then(reportResultExpecting('InvalidStateError'));
+ break;
+
+ // OK because waitUntil() is called in a microtask that runs after the
+ // event handler runs, while the event dispatch flag is still set.
+ case 'no-current-extension-different-microtask':
+ async_microtask_waituntil(event).then(reportResultExpecting('OK'));
+ break;
+
+ // OK because the second waitUntil() is called while the first waitUntil()
+ // promise is still pending.
+ case 'current-extension-different-task':
+ event.waitUntil(new Promise((res) => { resolveTestPromise = res; }));
+ async_task_waituntil(event).then(reportResultExpecting('OK')).then(resolveTestPromise);
+ break;
+
+ // OK because all promises involved resolve "immediately", so the second
+ // waitUntil() is called during the microtask checkpoint at the end of
+ // event dispatching, when the event dispatch flag is still set.
+ case 'during-event-dispatch-current-extension-expired-same-microtask-turn':
+ waitPromise = Promise.resolve();
+ event.waitUntil(waitPromise);
+ waitPromise.then(() => { return sync_waituntil(event); })
+ .then(reportResultExpecting('OK'))
+ break;
+
+ // OK for the same reason as above.
+ case 'during-event-dispatch-current-extension-expired-same-microtask-turn-extra':
+ waitPromise = Promise.resolve();
+ event.waitUntil(waitPromise);
+ waitPromise.then(() => { return async_microtask_waituntil(event); })
+ .then(reportResultExpecting('OK'))
+ break;
+
+
+ // OK because the pending promise count is decremented in a microtask
+ // queued upon fulfillment of the first waitUntil() promise, so the second
+ // waitUntil() is called while the pending promise count is still
+ // positive.
+ case 'after-event-dispatch-current-extension-expired-same-microtask-turn':
+ waitPromise = makeNewTaskPromise();
+ event.waitUntil(waitPromise);
+ waitPromise.then(() => { return sync_waituntil(event); })
+ .then(reportResultExpecting('OK'))
+ break;
+
+ // Throws because the second waitUntil() is called after the pending
+ // promise count was decremented to 0.
+ case 'after-event-dispatch-current-extension-expired-same-microtask-turn-extra':
+ waitPromise = makeNewTaskPromise();
+ event.waitUntil(waitPromise);
+ waitPromise.then(() => { return async_microtask_waituntil(event); })
+ .then(reportResultExpecting('InvalidStateError'))
+ break;
+
+ // Throws because the second waitUntil() is called in a new task, after
+ // first waitUntil() promise settled and the event dispatch flag is unset.
+ case 'current-extension-expired-different-task':
+ event.waitUntil(Promise.resolve());
+ async_task_waituntil(event).then(reportResultExpecting('InvalidStateError'));
+ break;
+
+ case 'script-extendable-event':
+ self.dispatchEvent(new ExtendableEvent('nontrustedevent'));
+ break;
+ }
+
+ event.source.postMessage('ACK');
+ });
+
+self.addEventListener('fetch', function(event) {
+ const path = new URL(event.request.url).pathname;
+ const step = path.substring(path.lastIndexOf('/') + 1);
+ let response;
+ switch (step) {
+ // OK because waitUntil() is called while the respondWith() promise is still
+ // unsettled, so the pending promise count is positive.
+ case 'pending-respondwith-async-waituntil':
+ var resolveFetch;
+ response = new Promise((res) => { resolveFetch = res; });
+ event.respondWith(response);
+ async_task_waituntil(event)
+ .then(reportResultExpecting('OK'))
+ .then(() => { resolveFetch(new Response('OK')); });
+ break;
+
+ // OK because all promises involved resolve "immediately", so waitUntil() is
+ // called during the microtask checkpoint at the end of event dispatching,
+ // when the event dispatch flag is still set.
+ case 'during-event-dispatch-respondwith-microtask-sync-waituntil':
+ response = Promise.resolve(new Response('RESP'));
+ event.respondWith(response);
+ response.then(() => { return sync_waituntil(event); })
+ .then(reportResultExpecting('OK'));
+ break;
+
+ // OK because all promises involved resolve "immediately", so waitUntil() is
+ // called during the microtask checkpoint at the end of event dispatching,
+ // when the event dispatch flag is still set.
+ case 'during-event-dispatch-respondwith-microtask-async-waituntil':
+ response = Promise.resolve(new Response('RESP'));
+ event.respondWith(response);
+ response.then(() => { return async_microtask_waituntil(event); })
+ .then(reportResultExpecting('OK'));
+ break;
+
+ // OK because the pending promise count is decremented in a microtask queued
+ // upon fulfillment of the respondWith() promise, so waitUntil() is called
+ // while the pending promise count is still positive.
+ case 'after-event-dispatch-respondwith-microtask-sync-waituntil':
+ response = makeNewTaskPromise().then(() => {return new Response('RESP');});
+ event.respondWith(response);
+ response.then(() => { return sync_waituntil(event); })
+ .then(reportResultExpecting('OK'));
+ break;
+
+
+ // Throws because waitUntil() is called after the pending promise count was
+ // decremented to 0.
+ case 'after-event-dispatch-respondwith-microtask-async-waituntil':
+ response = makeNewTaskPromise().then(() => {return new Response('RESP');});
+ event.respondWith(response);
+ response.then(() => { return async_microtask_waituntil(event); })
+ .then(reportResultExpecting('InvalidStateError'))
+ break;
+ }
+});
+
+self.addEventListener('nontrustedevent', function(event) {
+ sync_waituntil(event).then(reportResultExpecting('InvalidStateError'));
+ });
+
+function reportResultExpecting(expectedResult) {
+ return function (result) {
+ port.postMessage({result : result, expected: expectedResult});
+ return result;
+ };
+}
+
+function sync_waituntil(event) {
+ return new Promise((res, rej) => {
+ try {
+ event.waitUntil(Promise.resolve());
+ res('OK');
+ } catch (error) {
+ res(error.name);
+ }
+ });
+}
+
+function async_microtask_waituntil(event) {
+ return new Promise((res, rej) => {
+ Promise.resolve().then(() => {
+ try {
+ event.waitUntil(Promise.resolve());
+ res('OK');
+ } catch (error) {
+ res(error.name);
+ }
+ });
+ });
+}
+
+function async_task_waituntil(event) {
+ return new Promise((res, rej) => {
+ setTimeout(() => {
+ try {
+ event.waitUntil(Promise.resolve());
+ res('OK');
+ } catch (error) {
+ res(error.name);
+ }
+ }, 0);
+ });
+}
+
+// Returns a promise that settles in a separate task.
+function makeNewTaskPromise() {
+ return new Promise(resolve => {
+ setTimeout(resolve, 0);
+ });
+}
diff --git a/test/wpt/tests/service-workers/service-worker/resources/extendable-event-waituntil.js b/test/wpt/tests/service-workers/service-worker/resources/extendable-event-waituntil.js
new file mode 100644
index 0000000..20a9eb0
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/extendable-event-waituntil.js
@@ -0,0 +1,87 @@
+var pendingPorts = [];
+var portResolves = [];
+
+onmessage = function(e) {
+ var message = e.data;
+ if ('port' in message) {
+ var resolve = self.portResolves.shift();
+ if (resolve)
+ resolve(message.port);
+ else
+ self.pendingPorts.push(message.port);
+ }
+};
+
+function fulfillPromise() {
+ return new Promise(function(resolve) {
+ // Make sure the oninstall/onactivate callback returns first.
+ Promise.resolve().then(function() {
+ var port = self.pendingPorts.shift();
+ if (port)
+ resolve(port);
+ else
+ self.portResolves.push(resolve);
+ });
+ }).then(function(port) {
+ port.postMessage('SYNC');
+ return new Promise(function(resolve) {
+ port.onmessage = function(e) {
+ if (e.data == 'ACK')
+ resolve();
+ };
+ });
+ });
+}
+
+function rejectPromise() {
+ return new Promise(function(resolve, reject) {
+ // Make sure the oninstall/onactivate callback returns first.
+ Promise.resolve().then(reject);
+ });
+}
+
+function stripScopeName(url) {
+ return url.split('/').slice(-1)[0];
+}
+
+oninstall = function(e) {
+ switch (stripScopeName(self.location.href)) {
+ case 'install-fulfilled':
+ e.waitUntil(fulfillPromise());
+ break;
+ case 'install-rejected':
+ e.waitUntil(rejectPromise());
+ break;
+ case 'install-multiple-fulfilled':
+ e.waitUntil(fulfillPromise());
+ e.waitUntil(fulfillPromise());
+ break;
+ case 'install-reject-precedence':
+ // Three "extend lifetime promises" are needed to verify that the user
+ // agent waits for all promises to settle even in the event of rejection.
+ // The first promise is fulfilled on demand by the client, the second is
+ // immediately scheduled for rejection, and the third is fulfilled on
+ // demand by the client (but only after the first promise has been
+ // fulfilled).
+ //
+ // User agents which simply expose `Promise.all` semantics in this case
+ // (by entering the "redundant state" following the rejection of the
+ // second promise but prior to the fulfillment of the third) can be
+ // identified from the client context.
+ e.waitUntil(fulfillPromise());
+ e.waitUntil(rejectPromise());
+ e.waitUntil(fulfillPromise());
+ break;
+ }
+};
+
+onactivate = function(e) {
+ switch (stripScopeName(self.location.href)) {
+ case 'activate-fulfilled':
+ e.waitUntil(fulfillPromise());
+ break;
+ case 'activate-rejected':
+ e.waitUntil(rejectPromise());
+ break;
+ }
+};
diff --git a/test/wpt/tests/service-workers/service-worker/resources/fail-on-fetch-worker.js b/test/wpt/tests/service-workers/service-worker/resources/fail-on-fetch-worker.js
new file mode 100644
index 0000000..517f289
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/fail-on-fetch-worker.js
@@ -0,0 +1,5 @@
+importScripts('worker-testharness.js');
+
+this.addEventListener('fetch', function(event) {
+ event.respondWith(new Response('ERROR'));
+ });
diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-access-control-login.html b/test/wpt/tests/service-workers/service-worker/resources/fetch-access-control-login.html
new file mode 100644
index 0000000..ee29680
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-access-control-login.html
@@ -0,0 +1,16 @@
+<script>
+// Set authentication info
+window.addEventListener("message", function(evt) {
+ var port = evt.ports[0];
+ document.cookie = 'cookie=' + evt.data.cookie;
+ var xhr = new XMLHttpRequest();
+ xhr.addEventListener('load', function() {
+ port.postMessage({msg: 'LOGIN FINISHED'});
+ }, false);
+ xhr.open('GET',
+ './fetch-access-control.py?Auth',
+ true,
+ evt.data.username, evt.data.password);
+ xhr.send();
+ }, false);
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-access-control.py b/test/wpt/tests/service-workers/service-worker/resources/fetch-access-control.py
new file mode 100644
index 0000000..380a7d6
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-access-control.py
@@ -0,0 +1,114 @@
+import json
+import os
+from base64 import decodebytes
+
+from wptserve.utils import isomorphic_decode, isomorphic_encode
+
+def main(request, response):
+ headers = []
+ headers.append((b'X-ServiceWorker-ServerHeader', b'SetInTheServer'))
+
+ if b"ACAOrigin" in request.GET:
+ for item in request.GET[b"ACAOrigin"].split(b","):
+ headers.append((b"Access-Control-Allow-Origin", item))
+
+ for suffix in [b"Headers", b"Methods", b"Credentials"]:
+ query = b"ACA%s" % suffix
+ header = b"Access-Control-Allow-%s" % suffix
+ if query in request.GET:
+ headers.append((header, request.GET[query]))
+
+ if b"ACEHeaders" in request.GET:
+ headers.append((b"Access-Control-Expose-Headers", request.GET[b"ACEHeaders"]))
+
+ if (b"Auth" in request.GET and not request.auth.username) or b"AuthFail" in request.GET:
+ status = 401
+ headers.append((b'WWW-Authenticate', b'Basic realm="Restricted"'))
+ body = b'Authentication canceled'
+ return status, headers, body
+
+ if b"PNGIMAGE" in request.GET:
+ headers.append((b"Content-Type", b"image/png"))
+ body = decodebytes(b"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1B"
+ b"AACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAAhSURBVDhPY3wro/KfgQLABKXJBqMG"
+ b"jBoAAqMGDLwBDAwAEsoCTFWunmQAAAAASUVORK5CYII=")
+ return headers, body
+
+ if b"VIDEO" in request.GET:
+ if b"mp4" in request.GET:
+ headers.append((b"Content-Type", b"video/mp4"))
+ body = open(os.path.join(request.doc_root, u"media", u"movie_5.mp4"), "rb").read()
+ else:
+ headers.append((b"Content-Type", b"video/ogg"))
+ body = open(os.path.join(request.doc_root, u"media", u"movie_5.ogv"), "rb").read()
+
+ length = len(body)
+ # If "PartialContent" is specified, the requestor wants to test range
+ # requests. For the initial request, respond with "206 Partial Content"
+ # and don't send the entire content. Then expect subsequent requests to
+ # have a "Range" header with a byte range. Respond with that range.
+ if b"PartialContent" in request.GET:
+ if length < 1:
+ return 500, headers, b"file is too small for range requests"
+ start = 0
+ end = length - 1
+ if b"Range" in request.headers:
+ range_header = request.headers[b"Range"]
+ prefix = b"bytes="
+ split_header = range_header[len(prefix):].split(b"-")
+ # The first request might be "bytes=0-". We want to force a range
+ # request, so just return the first byte.
+ if split_header[0] == b"0" and split_header[1] == b"":
+ end = start
+ # Otherwise, it is a range request. Respect the values sent.
+ if split_header[0] != b"":
+ start = int(split_header[0])
+ if split_header[1] != b"":
+ end = int(split_header[1])
+ else:
+ # The request doesn't have a range. Force a range request by
+ # returning the first byte.
+ end = start
+
+ headers.append((b"Accept-Ranges", b"bytes"))
+ headers.append((b"Content-Length", isomorphic_encode(str(end -start + 1))))
+ headers.append((b"Content-Range", b"bytes %d-%d/%d" % (start, end, length)))
+ chunk = body[start:(end + 1)]
+ return 206, headers, chunk
+ return headers, body
+
+ username = request.auth.username if request.auth.username else b"undefined"
+ password = request.auth.password if request.auth.username else b"undefined"
+ cookie = request.cookies[b'cookie'].value if b'cookie' in request.cookies else b"undefined"
+
+ files = []
+ for key, values in request.POST.items():
+ assert len(values) == 1
+ value = values[0]
+ if not hasattr(value, u"file"):
+ continue
+ data = value.file.read()
+ files.append({u"key": isomorphic_decode(key),
+ u"name": value.file.name,
+ u"type": value.type,
+ u"error": 0, #TODO,
+ u"size": len(data),
+ u"content": data})
+
+ get_data = {isomorphic_decode(key):isomorphic_decode(request.GET[key]) for key, value in request.GET.items()}
+ post_data = {isomorphic_decode(key):isomorphic_decode(request.POST[key]) for key, value in request.POST.items()
+ if not hasattr(request.POST[key], u"file")}
+ headers_data = {isomorphic_decode(key):isomorphic_decode(request.headers[key]) for key, value in request.headers.items()}
+
+ data = {u"jsonpResult": u"success",
+ u"method": request.method,
+ u"headers": headers_data,
+ u"body": isomorphic_decode(request.body),
+ u"files": files,
+ u"GET": get_data,
+ u"POST": post_data,
+ u"username": isomorphic_decode(username),
+ u"password": isomorphic_decode(password),
+ u"cookie": isomorphic_decode(cookie)}
+
+ return headers, u"report( %s )" % json.dumps(data)
diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-canvas-tainting-double-write-worker.js b/test/wpt/tests/service-workers/service-worker/resources/fetch-canvas-tainting-double-write-worker.js
new file mode 100644
index 0000000..17723dc
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-canvas-tainting-double-write-worker.js
@@ -0,0 +1,7 @@
+self.addEventListener('fetch', (event) => {
+ url = new URL(event.request.url);
+ if (url.search == '?PNGIMAGE') {
+ localUrl = new URL(url.pathname + url.search, self.location);
+ event.respondWith(fetch(localUrl));
+ }
+});
diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-canvas-tainting-iframe.html b/test/wpt/tests/service-workers/service-worker/resources/fetch-canvas-tainting-iframe.html
new file mode 100644
index 0000000..75d766c
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-canvas-tainting-iframe.html
@@ -0,0 +1,70 @@
+<html>
+<title>iframe for fetch canvas tainting test</title>
+<script>
+const NOT_TAINTED = 'NOT_TAINTED';
+const TAINTED = 'TAINTED';
+const LOAD_ERROR = 'LOAD_ERROR';
+
+// Creates an image/video element with src=|url| and an optional |cross_origin|
+// attibute. Tries to read from the image/video using a canvas element. Returns
+// NOT_TAINTED if it could be read, TAINTED if it could not be read, and
+// LOAD_ERROR if loading the image/video failed.
+function create_test_case_promise(url, cross_origin) {
+ return new Promise(resolve => {
+ if (url.indexOf('PNGIMAGE') != -1) {
+ const img = document.createElement('img');
+ if (cross_origin != '') {
+ img.crossOrigin = cross_origin;
+ }
+ img.onload = function() {
+ try {
+ const canvas = document.createElement('canvas');
+ canvas.width = 100;
+ canvas.height = 100;
+ const context = canvas.getContext('2d');
+ context.drawImage(img, 0, 0);
+ context.getImageData(0, 0, 100, 100);
+ resolve(NOT_TAINTED);
+ } catch (e) {
+ resolve(TAINTED);
+ }
+ };
+ img.onerror = function() {
+ resolve(LOAD_ERROR);
+ }
+ img.src = url;
+ return;
+ }
+
+ if (url.indexOf('VIDEO') != -1) {
+ const video = document.createElement('video');
+ video.autoplay = true;
+ video.muted = true;
+ if (cross_origin != '') {
+ video.crossOrigin = cross_origin;
+ }
+ video.onplay = function() {
+ try {
+ const canvas = document.createElement('canvas');
+ canvas.width = 100;
+ canvas.height = 100;
+ const context = canvas.getContext('2d');
+ context.drawImage(video, 0, 0);
+ context.getImageData(0, 0, 100, 100);
+ resolve(NOT_TAINTED);
+ } catch (e) {
+ resolve(TAINTED);
+ }
+ };
+ video.onerror = function() {
+ resolve(LOAD_ERROR);
+ }
+ video.src = url;
+ return;
+ }
+
+ resolve('unknown resource type');
+ });
+}
+</script>
+</html>
diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-canvas-tainting-tests.js b/test/wpt/tests/service-workers/service-worker/resources/fetch-canvas-tainting-tests.js
new file mode 100644
index 0000000..2aada36
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-canvas-tainting-tests.js
@@ -0,0 +1,241 @@
+// This is the main driver of the canvas tainting tests.
+const NOT_TAINTED = 'NOT_TAINTED';
+const TAINTED = 'TAINTED';
+const LOAD_ERROR = 'LOAD_ERROR';
+
+let frame;
+
+// Creates a single promise_test.
+function canvas_taint_test(url, cross_origin, expected_result) {
+ promise_test(t => {
+ return frame.contentWindow.create_test_case_promise(url, cross_origin)
+ .then(result => {
+ assert_equals(result, expected_result);
+ });
+ }, 'url "' + url + '" with crossOrigin "' + cross_origin + '" should be ' +
+ expected_result);
+}
+
+
+// Runs all the tests. The given |params| has these properties:
+// * |resource_path|: the relative path to the (image/video) resource to test.
+// * |cache|: when true, the service worker bounces responses into
+// Cache Storage and back out before responding with them.
+function do_canvas_tainting_tests(params) {
+ const host_info = get_host_info();
+ let resource_path = params.resource_path;
+ if (params.cache)
+ resource_path += "&cache=true";
+ const resource_url = host_info['HTTPS_ORIGIN'] + resource_path;
+ const remote_resource_url = host_info['HTTPS_REMOTE_ORIGIN'] + resource_path;
+
+ // Set up the service worker and the frame.
+ promise_test(function(t) {
+ const SCOPE = 'resources/fetch-canvas-tainting-iframe.html';
+ const SCRIPT = 'resources/fetch-rewrite-worker.js';
+ const host_info = get_host_info();
+
+ // login_https() is needed because some test cases use credentials.
+ return login_https(t)
+ .then(function() {
+ return service_worker_unregister_and_register(t, SCRIPT, SCOPE);
+ })
+ .then(function(registration) {
+ promise_test(() => {
+ if (frame)
+ frame.remove();
+ return registration.unregister();
+ }, 'restore global state');
+
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() { return with_iframe(SCOPE); })
+ .then(f => {
+ frame = f;
+ });
+ }, 'initialize global state');
+
+ // Reject tests. Add '&reject' so the service worker responds with a rejected promise.
+ // A load error is expected.
+ canvas_taint_test(resource_url + '&reject', '', LOAD_ERROR);
+ canvas_taint_test(resource_url + '&reject', 'anonymous', LOAD_ERROR);
+ canvas_taint_test(resource_url + '&reject', 'use-credentials', LOAD_ERROR);
+
+ // Fallback tests. Add '&ignore' so the service worker does not respond to the fetch
+ // request, and we fall back to network.
+ canvas_taint_test(resource_url + '&ignore', '', NOT_TAINTED);
+ canvas_taint_test(remote_resource_url + '&ignore', '', TAINTED);
+ canvas_taint_test(remote_resource_url + '&ignore', 'anonymous', LOAD_ERROR);
+ canvas_taint_test(
+ remote_resource_url + '&ACAOrigin=' + host_info['HTTPS_ORIGIN'] +
+ '&ignore',
+ 'anonymous',
+ NOT_TAINTED);
+ canvas_taint_test(remote_resource_url + '&ignore', 'use-credentials', LOAD_ERROR);
+ canvas_taint_test(
+ remote_resource_url + '&ACAOrigin=' + host_info['HTTPS_ORIGIN'] +
+ '&ignore',
+ 'use-credentials',
+ LOAD_ERROR);
+ canvas_taint_test(
+ remote_resource_url + '&ACAOrigin=' + host_info['HTTPS_ORIGIN'] +
+ '&ACACredentials=true&ignore',
+ 'use-credentials',
+ NOT_TAINTED);
+
+ // Credential tests (with fallback). Add '&Auth' so the server requires authentication.
+ // Furthermore, add '&ignore' so the service worker falls back to network.
+ canvas_taint_test(resource_url + '&Auth&ignore', '', NOT_TAINTED);
+ canvas_taint_test(remote_resource_url + '&Auth&ignore', '', TAINTED);
+ canvas_taint_test(
+ remote_resource_url + '&Auth&ignore', 'anonymous', LOAD_ERROR);
+ canvas_taint_test(
+ remote_resource_url + '&Auth&ignore',
+ 'use-credentials',
+ LOAD_ERROR);
+ canvas_taint_test(
+ remote_resource_url + '&Auth&ACAOrigin=' + host_info['HTTPS_ORIGIN'] +
+ '&ignore',
+ 'use-credentials',
+ LOAD_ERROR);
+ canvas_taint_test(
+ remote_resource_url + '&Auth&ACAOrigin=' + host_info['HTTPS_ORIGIN'] +
+ '&ACACredentials=true&ignore',
+ 'use-credentials',
+ NOT_TAINTED);
+
+ // In the following tests, the service worker provides a response.
+ // Add '&url' so the service worker responds with fetch(url).
+ // Add '&mode' to configure the fetch request options.
+
+ // Basic response tests. Set &url to the original url.
+ canvas_taint_test(
+ resource_url + '&mode=same-origin&url=' + encodeURIComponent(resource_url),
+ '',
+ NOT_TAINTED);
+ canvas_taint_test(
+ resource_url + '&mode=same-origin&url=' + encodeURIComponent(resource_url),
+ 'anonymous',
+ NOT_TAINTED);
+ canvas_taint_test(
+ resource_url + '&mode=same-origin&url=' + encodeURIComponent(resource_url),
+ 'use-credentials',
+ NOT_TAINTED);
+ canvas_taint_test(
+ remote_resource_url + '&mode=same-origin&url=' +
+ encodeURIComponent(resource_url),
+ '',
+ NOT_TAINTED);
+ canvas_taint_test(
+ remote_resource_url + '&mode=same-origin&url=' +
+ encodeURIComponent(resource_url),
+ 'anonymous',
+ NOT_TAINTED);
+ canvas_taint_test(
+ remote_resource_url + '&mode=same-origin&url=' +
+ encodeURIComponent(resource_url),
+ 'use-credentials',
+ NOT_TAINTED);
+
+ // Opaque response tests. Set &url to the cross-origin URL, and &mode to
+ // 'no-cors' so we expect an opaque response.
+ canvas_taint_test(
+ resource_url +
+ '&mode=no-cors&url=' + encodeURIComponent(remote_resource_url),
+ '',
+ TAINTED);
+ canvas_taint_test(
+ resource_url +
+ '&mode=no-cors&url=' + encodeURIComponent(remote_resource_url),
+ 'anonymous',
+ LOAD_ERROR);
+ canvas_taint_test(
+ resource_url +
+ '&mode=no-cors&url=' + encodeURIComponent(remote_resource_url),
+ 'use-credentials',
+ LOAD_ERROR);
+ canvas_taint_test(
+ remote_resource_url +
+ '&mode=no-cors&url=' + encodeURIComponent(remote_resource_url),
+ '',
+ TAINTED);
+ canvas_taint_test(
+ remote_resource_url +
+ '&mode=no-cors&url=' + encodeURIComponent(remote_resource_url),
+ 'anonymous',
+ LOAD_ERROR);
+ canvas_taint_test(
+ remote_resource_url +
+ '&mode=no-cors&url=' + encodeURIComponent(remote_resource_url),
+ 'use-credentials',
+ LOAD_ERROR);
+
+ // CORS response tests. Set &url to the cross-origin URL, and &mode
+ // to 'cors' to attempt a CORS request.
+ canvas_taint_test(
+ resource_url + '&mode=cors&url=' +
+ encodeURIComponent(remote_resource_url +
+ '&ACAOrigin=' + host_info['HTTPS_ORIGIN']),
+ '',
+ LOAD_ERROR); // We expect LOAD_ERROR since the server doesn't respond
+ // with an Access-Control-Allow-Credentials header.
+ canvas_taint_test(
+ resource_url + '&mode=cors&credentials=same-origin&url=' +
+ encodeURIComponent(remote_resource_url +
+ '&ACAOrigin=' + host_info['HTTPS_ORIGIN']),
+ '',
+ NOT_TAINTED);
+ canvas_taint_test(
+ resource_url + '&mode=cors&url=' +
+ encodeURIComponent(remote_resource_url +
+ '&ACAOrigin=' + host_info['HTTPS_ORIGIN']),
+ 'anonymous',
+ NOT_TAINTED);
+ canvas_taint_test(
+ resource_url + '&mode=cors&url=' +
+ encodeURIComponent(remote_resource_url +
+ '&ACAOrigin=' + host_info['HTTPS_ORIGIN']),
+ 'use-credentials',
+ LOAD_ERROR); // We expect LOAD_ERROR since the server doesn't respond
+ // with an Access-Control-Allow-Credentials header.
+ canvas_taint_test(
+ resource_url + '&mode=cors&url=' +
+ encodeURIComponent(
+ remote_resource_url +
+ '&ACACredentials=true&ACAOrigin=' + host_info['HTTPS_ORIGIN']),
+ 'use-credentials',
+ NOT_TAINTED);
+ canvas_taint_test(
+ remote_resource_url + '&mode=cors&url=' +
+ encodeURIComponent(remote_resource_url +
+ '&ACAOrigin=' + host_info['HTTPS_ORIGIN']),
+ '',
+ LOAD_ERROR); // We expect LOAD_ERROR since the server doesn't respond
+ // with an Access-Control-Allow-Credentials header.
+ canvas_taint_test(
+ remote_resource_url + '&mode=cors&credentials=same-origin&url=' +
+ encodeURIComponent(remote_resource_url +
+ '&ACAOrigin=' + host_info['HTTPS_ORIGIN']),
+ '',
+ NOT_TAINTED);
+ canvas_taint_test(
+ remote_resource_url + '&mode=cors&url=' +
+ encodeURIComponent(remote_resource_url +
+ '&ACAOrigin=' + host_info['HTTPS_ORIGIN']),
+ 'anonymous',
+ NOT_TAINTED);
+ canvas_taint_test(
+ remote_resource_url + '&mode=cors&url=' +
+ encodeURIComponent(remote_resource_url +
+ '&ACAOrigin=' + host_info['HTTPS_ORIGIN']),
+ 'use-credentials',
+ LOAD_ERROR); // We expect LOAD_ERROR since the server doesn't respond
+ // with an Access-Control-Allow-Credentials header.
+ canvas_taint_test(
+ remote_resource_url + '&mode=cors&url=' +
+ encodeURIComponent(
+ remote_resource_url +
+ '&ACACredentials=true&ACAOrigin=' + host_info['HTTPS_ORIGIN']),
+ 'use-credentials',
+ NOT_TAINTED);
+}
diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-cors-exposed-header-names-worker.js b/test/wpt/tests/service-workers/service-worker/resources/fetch-cors-exposed-header-names-worker.js
new file mode 100644
index 0000000..145952a
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-cors-exposed-header-names-worker.js
@@ -0,0 +1,3 @@
+self.addEventListener('fetch', (e) => {
+ e.respondWith(fetch(e.request));
+ });
diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-cors-xhr-iframe.html b/test/wpt/tests/service-workers/service-worker/resources/fetch-cors-xhr-iframe.html
new file mode 100644
index 0000000..d88c510
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-cors-xhr-iframe.html
@@ -0,0 +1,170 @@
+<script src="/common/get-host-info.sub.js"></script>
+<script src="test-helpers.sub.js?pipe=sub"></script>
+<script>
+var path = base_path() + 'fetch-access-control.py';
+var host_info = get_host_info();
+var SUCCESS = 'SUCCESS';
+var FAIL = 'FAIL';
+
+function create_test_case_promise(url, with_credentials) {
+ return new Promise(function(resolve) {
+ var xhr = new XMLHttpRequest();
+ xhr.onload = function() {
+ if (xhr.status == 200) {
+ resolve(SUCCESS);
+ } else {
+ resolve("STATUS" + xhr.status);
+ }
+ }
+ xhr.onerror = function() {
+ resolve(FAIL);
+ }
+ xhr.responseType = 'text';
+ xhr.withCredentials = with_credentials;
+ xhr.open('GET', url, true);
+ xhr.send();
+ });
+}
+
+window.addEventListener('message', async (evt) => {
+ var port = evt.ports[0];
+ var url = host_info['HTTPS_ORIGIN'] + path;
+ var remote_url = host_info['HTTPS_REMOTE_ORIGIN'] + path;
+ var TEST_CASES = [
+ // Reject tests
+ [url + '?reject', false, FAIL],
+ [url + '?reject', true, FAIL],
+ [remote_url + '?reject', false, FAIL],
+ [remote_url + '?reject', true, FAIL],
+ // Event handler exception tests
+ [url + '?throw', false, SUCCESS],
+ [url + '?throw', true, SUCCESS],
+ [remote_url + '?throw', false, FAIL],
+ [remote_url + '?throw', true, FAIL],
+ // Reject(resolve-null) tests
+ [url + '?resolve-null', false, FAIL],
+ [url + '?resolve-null', true, FAIL],
+ [remote_url + '?resolve-null', false, FAIL],
+ [remote_url + '?resolve-null', true, FAIL],
+ // Fallback tests
+ [url + '?ignore', false, SUCCESS],
+ [url + '?ignore', true, SUCCESS],
+ [remote_url + '?ignore', false, FAIL, true], // Executed in serial.
+ [remote_url + '?ignore', true, FAIL, true], // Executed in serial.
+ [
+ remote_url + '?ACAOrigin=' + host_info['HTTPS_ORIGIN'] + '&ignore',
+ false, SUCCESS
+ ],
+ [
+ remote_url + '?ACAOrigin=' + host_info['HTTPS_ORIGIN'] + '&ignore',
+ true, FAIL, true // Executed in serial.
+ ],
+ [
+ remote_url + '?ACAOrigin=' + host_info['HTTPS_ORIGIN'] +
+ '&ACACredentials=true&ignore',
+ true, SUCCESS
+ ],
+ // Credential test (fallback)
+ [url + '?Auth&ignore', false, SUCCESS],
+ [url + '?Auth&ignore', true, SUCCESS],
+ [remote_url + '?Auth&ignore', false, FAIL],
+ [remote_url + '?Auth&ignore', true, FAIL],
+ [
+ remote_url + '?Auth&ACAOrigin=' + host_info['HTTPS_ORIGIN'] + '&ignore',
+ false, 'STATUS401'
+ ],
+ [
+ remote_url + '?Auth&ACAOrigin=' + host_info['HTTPS_ORIGIN'] + '&ignore',
+ true, FAIL, true // Executed in serial.
+ ],
+ [
+ remote_url + '?Auth&ACAOrigin=' + host_info['HTTPS_ORIGIN'] +
+ '&ACACredentials=true&ignore',
+ true, SUCCESS
+ ],
+ // Basic response
+ [
+ url + '?mode=same-origin&url=' + encodeURIComponent(url),
+ false, SUCCESS
+ ],
+ [
+ url + '?mode=same-origin&url=' + encodeURIComponent(url),
+ false, SUCCESS
+ ],
+ [
+ remote_url + '?mode=same-origin&url=' + encodeURIComponent(url),
+ false, SUCCESS
+ ],
+ [
+ remote_url + '?mode=same-origin&url=' + encodeURIComponent(url),
+ false, SUCCESS
+ ],
+ // Opaque response
+ [
+ url + '?mode=no-cors&url=' + encodeURIComponent(remote_url),
+ false, FAIL
+ ],
+ [
+ url + '?mode=no-cors&url=' + encodeURIComponent(remote_url),
+ false, FAIL
+ ],
+ [
+ remote_url + '?mode=no-cors&url=' + encodeURIComponent(remote_url),
+ false, FAIL
+ ],
+ [
+ remote_url + '?mode=no-cors&url=' + encodeURIComponent(remote_url),
+ false, FAIL
+ ],
+ // CORS response
+ [
+ url + '?mode=cors&url=' +
+ encodeURIComponent(remote_url + '?ACAOrigin=' +
+ host_info['HTTPS_ORIGIN']),
+ false, SUCCESS
+ ],
+ [
+ url + '?mode=cors&url=' +
+ encodeURIComponent(remote_url + '?ACAOrigin=' +
+ host_info['HTTPS_ORIGIN']),
+ true, FAIL
+ ],
+ [
+ url + '?mode=cors&url=' +
+ encodeURIComponent(remote_url + '?ACAOrigin=' +
+ host_info['HTTPS_ORIGIN'] +
+ '&ACACredentials=true'),
+ true, SUCCESS
+ ],
+ [
+ remote_url + '?mode=cors&url=' +
+ encodeURIComponent(remote_url + '?ACAOrigin=' +
+ host_info['HTTPS_ORIGIN']),
+ false, SUCCESS
+ ],
+ [
+ remote_url +
+ '?mode=cors&url=' +
+ encodeURIComponent(remote_url + '?ACAOrigin=' +
+ host_info['HTTPS_ORIGIN']),
+ true, FAIL
+ ],
+ [
+ remote_url +
+ '?mode=cors&url=' +
+ encodeURIComponent(remote_url + '?ACAOrigin=' +
+ host_info['HTTPS_ORIGIN'] +
+ '&ACACredentials=true'),
+ true, SUCCESS
+ ]
+ ];
+
+ let counter = 0;
+ for (let test of TEST_CASES) {
+ let result = await create_test_case_promise(test[0], test[1]);
+ let testName = 'test ' + (++counter) + ': ' + test[0] + ' with credentials ' + test[1] + ' must be ' + test[2];
+ port.postMessage({testName: testName, result: result === test[2]});
+ }
+ port.postMessage('done');
+ }, false);
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-csp-iframe.html b/test/wpt/tests/service-workers/service-worker/resources/fetch-csp-iframe.html
new file mode 100644
index 0000000..33bf041
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-csp-iframe.html
@@ -0,0 +1,16 @@
+<script>
+var meta = document.createElement('meta');
+meta.setAttribute('http-equiv', 'Content-Security-Policy');
+meta.setAttribute('content', decodeURIComponent(location.search.substring(1)));
+document.head.appendChild(meta);
+
+function load_image(url) {
+ return new Promise(function(resolve, reject) {
+ var img = document.createElement('img');
+ document.body.appendChild(img);
+ img.onload = resolve;
+ img.onerror = reject;
+ img.src = url;
+ });
+}
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-csp-iframe.html.sub.headers b/test/wpt/tests/service-workers/service-worker/resources/fetch-csp-iframe.html.sub.headers
new file mode 100644
index 0000000..5a1c7b9
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-csp-iframe.html.sub.headers
@@ -0,0 +1 @@
+Content-Security-Policy: img-src https://{{host}}:{{ports[https][0]}}; connect-src 'unsafe-inline' 'self'
diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-error-worker.js b/test/wpt/tests/service-workers/service-worker/resources/fetch-error-worker.js
new file mode 100644
index 0000000..788252c
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-error-worker.js
@@ -0,0 +1,22 @@
+importScripts("/resources/testharness.js");
+
+function doTest(event)
+{
+ if (!event.request.url.includes("fetch-error-test"))
+ return;
+
+ let counter = 0;
+ const stream = new ReadableStream({ pull: controller => {
+ switch (++counter) {
+ case 1:
+ controller.enqueue(new Uint8Array([1]));
+ return;
+ default:
+ // We asynchronously error the stream so that there is ample time to resolve the fetch promise and call text() on the response.
+ step_timeout(() => controller.error("Sorry"), 50);
+ }
+ }});
+ event.respondWith(new Response(stream));
+}
+
+self.addEventListener("fetch", doTest);
diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-event-add-async-worker.js b/test/wpt/tests/service-workers/service-worker/resources/fetch-event-add-async-worker.js
new file mode 100644
index 0000000..a5a44a5
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-event-add-async-worker.js
@@ -0,0 +1,6 @@
+importScripts('/resources/testharness.js');
+
+promise_test(async () => {
+ await new Promise(handler => { step_timeout(handler, 0); });
+ self.addEventListener('fetch', () => {});
+}, 'fetch event added asynchronously does not throw');
diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-event-after-navigation-within-page-iframe.html b/test/wpt/tests/service-workers/service-worker/resources/fetch-event-after-navigation-within-page-iframe.html
new file mode 100644
index 0000000..bf8a6d5
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-event-after-navigation-within-page-iframe.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<script>
+function fetch_url(url) {
+ return new Promise(function(resolve, reject) {
+ var request = new XMLHttpRequest();
+ request.addEventListener('load', function(event) {
+ if (request.status == 200)
+ resolve(request.response);
+ else
+ reject(new Error('fetch_url: ' + request.statusText + " : " + url));
+ });
+ request.addEventListener('error', function(event) {
+ reject(new Error('fetch_url encountered an error: ' + url));
+ });
+ request.addEventListener('abort', function(event) {
+ reject(new Error('fetch_url was aborted: ' + url));
+ });
+ request.open('GET', url);
+ request.send();
+ });
+}
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-event-async-respond-with-worker.js b/test/wpt/tests/service-workers/service-worker/resources/fetch-event-async-respond-with-worker.js
new file mode 100644
index 0000000..dc3f1a1
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-event-async-respond-with-worker.js
@@ -0,0 +1,66 @@
+// This worker attempts to call respondWith() asynchronously after the
+// fetch event handler finished. It reports back to the test whether
+// an exception was thrown.
+
+// These get reset at the start of a test case.
+let reportResult;
+
+// The test page sends a message to tell us that a new test case is starting.
+// We expect a fetch event after this.
+self.addEventListener('message', (event) => {
+ // Ensure tests run mutually exclusive.
+ if (reportResult) {
+ event.source.postMessage('testAlreadyRunning');
+ return;
+ }
+
+ const resultPromise = new Promise((resolve) => {
+ reportResult = resolve;
+ // Tell the client that everything is initialized and that it's safe to
+ // proceed with the test without relying on the order of events (which some
+ // browsers like Chrome may not guarantee).
+ event.source.postMessage('messageHandlerInitialized');
+ });
+
+ // Keep the worker alive until the test case finishes, and report
+ // back the result to the test page.
+ event.waitUntil(resultPromise.then(result => {
+ reportResult = null;
+ event.source.postMessage(result);
+ }));
+});
+
+// Calls respondWith() and reports back whether an exception occurred.
+function tryRespondWith(event) {
+ try {
+ event.respondWith(new Response());
+ reportResult({didThrow: false});
+ } catch (error) {
+ reportResult({didThrow: true, error: error.name});
+ }
+}
+
+function respondWithInTask(event) {
+ setTimeout(() => {
+ tryRespondWith(event);
+ }, 0);
+}
+
+function respondWithInMicrotask(event) {
+ Promise.resolve().then(() => {
+ tryRespondWith(event);
+ });
+}
+
+self.addEventListener('fetch', function(event) {
+ const path = new URL(event.request.url).pathname;
+ const test = path.substring(path.lastIndexOf('/') + 1);
+
+ // If this is a test case, try respondWith() and report back to the test page
+ // the result.
+ if (test == 'respondWith-in-task') {
+ respondWithInTask(event);
+ } else if (test == 'respondWith-in-microtask') {
+ respondWithInMicrotask(event);
+ }
+});
diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-event-handled-worker.js b/test/wpt/tests/service-workers/service-worker/resources/fetch-event-handled-worker.js
new file mode 100644
index 0000000..53ee149
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-event-handled-worker.js
@@ -0,0 +1,37 @@
+// This worker reports back the final state of FetchEvent.handled (RESOLVED or
+// REJECTED) to the test.
+
+self.addEventListener('message', function(event) {
+ self.port = event.data.port;
+});
+
+self.addEventListener('fetch', function(event) {
+ try {
+ event.handled.then(() => {
+ self.port.postMessage('RESOLVED');
+ }, () => {
+ self.port.postMessage('REJECTED');
+ });
+ } catch (e) {
+ self.port.postMessage('FAILED');
+ return;
+ }
+
+ const search = new URL(event.request.url).search;
+ switch (search) {
+ case '?respondWith-not-called':
+ break;
+ case '?respondWith-not-called-and-event-canceled':
+ event.preventDefault();
+ break;
+ case '?respondWith-called-and-promise-resolved':
+ event.respondWith(Promise.resolve(new Response('body')));
+ break;
+ case '?respondWith-called-and-promise-resolved-to-invalid-response':
+ event.respondWith(Promise.resolve('invalid response'));
+ break;
+ case '?respondWith-called-and-promise-rejected':
+ event.respondWith(Promise.reject(new Error('respondWith rejected')));
+ break;
+ }
+});
diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-event-network-error-controllee-iframe.html b/test/wpt/tests/service-workers/service-worker/resources/fetch-event-network-error-controllee-iframe.html
new file mode 100644
index 0000000..f6c1919
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-event-network-error-controllee-iframe.html
@@ -0,0 +1,60 @@
+<!DOCTYPE html>
+<script>
+function fetch_url(url) {
+ return new Promise(function(resolve, reject) {
+ var request = new XMLHttpRequest();
+ request.addEventListener('load', function(event) {
+ resolve();
+ });
+ request.addEventListener('error', function(event) {
+ reject();
+ });
+ request.open('GET', url);
+ request.send();
+ });
+}
+
+function make_test(testcase) {
+ var name = testcase.name;
+ return fetch_url(window.location.href + '?' + name)
+ .then(
+ function() {
+ if (testcase.expect_load)
+ return Promise.resolve();
+ return Promise.reject(new Error(
+ name + ': expected network error but loaded'));
+ },
+ function() {
+ if (!testcase.expect_load)
+ return Promise.resolve();
+ return Promise.reject(new Error(
+ name + ': expected to load but got network error'));
+ });
+}
+
+function run_tests() {
+ var tests = [
+ { name: 'prevent-default-and-respond-with', expect_load: true },
+ { name: 'prevent-default', expect_load: false },
+ { name: 'reject', expect_load: false },
+ { name: 'unused-body', expect_load: true },
+ { name: 'used-body', expect_load: false },
+ { name: 'unused-fetched-body', expect_load: true },
+ { name: 'used-fetched-body', expect_load: false },
+ { name: 'throw-exception', expect_load: true },
+ ].map(make_test);
+
+ Promise.all(tests)
+ .then(function() {
+ window.parent.notify_test_done('PASS');
+ })
+ .catch(function(error) {
+ window.parent.notify_test_done('FAIL: ' + error.message);
+ });
+}
+
+if (!navigator.serviceWorker.controller)
+ window.parent.notify_test_done('FAIL: no controller');
+else
+ run_tests();
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-event-network-error-worker.js b/test/wpt/tests/service-workers/service-worker/resources/fetch-event-network-error-worker.js
new file mode 100644
index 0000000..5bfe3a0
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-event-network-error-worker.js
@@ -0,0 +1,49 @@
+// Test that multiple fetch handlers do not confuse the implementation.
+self.addEventListener('fetch', function(event) {});
+
+self.addEventListener('fetch', function(event) {
+ var testcase = new URL(event.request.url).search;
+ switch (testcase) {
+ case '?reject':
+ event.respondWith(Promise.reject());
+ break;
+ case '?prevent-default':
+ event.preventDefault();
+ break;
+ case '?prevent-default-and-respond-with':
+ event.preventDefault();
+ break;
+ case '?unused-body':
+ event.respondWith(new Response('body'));
+ break;
+ case '?used-body':
+ var res = new Response('body');
+ res.text();
+ event.respondWith(res);
+ break;
+ case '?unused-fetched-body':
+ event.respondWith(fetch('other.html').then(function(res){
+ return res;
+ }));
+ break;
+ case '?used-fetched-body':
+ event.respondWith(fetch('other.html').then(function(res){
+ res.text();
+ return res;
+ }));
+ break;
+ case '?throw-exception':
+ throw('boom');
+ break;
+ }
+ });
+
+self.addEventListener('fetch', function(event) {});
+
+self.addEventListener('fetch', function(event) {
+ var testcase = new URL(event.request.url).search;
+ if (testcase == '?prevent-default-and-respond-with')
+ event.respondWith(new Response('responding!'));
+ });
+
+self.addEventListener('fetch', function(event) {});
diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-event-network-fallback-worker.js b/test/wpt/tests/service-workers/service-worker/resources/fetch-event-network-fallback-worker.js
new file mode 100644
index 0000000..376bdbe
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-event-network-fallback-worker.js
@@ -0,0 +1,3 @@
+self.addEventListener('fetch', () => {
+ // Do nothing.
+});
diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-event-respond-with-argument-iframe.html b/test/wpt/tests/service-workers/service-worker/resources/fetch-event-respond-with-argument-iframe.html
new file mode 100644
index 0000000..0ebd1ca
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-event-respond-with-argument-iframe.html
@@ -0,0 +1,55 @@
+<!DOCTYPE html>
+<script>
+function fetch_url(url) {
+ return new Promise(function(resolve, reject) {
+ var request = new XMLHttpRequest();
+ request.addEventListener('load', function(event) {
+ resolve();
+ });
+ request.addEventListener('error', function(event) {
+ reject();
+ });
+ request.open('GET', url);
+ request.send();
+ });
+}
+
+function make_test(testcase) {
+ var name = testcase.name;
+ return fetch_url(window.location.href + '?' + name)
+ .then(
+ function() {
+ if (testcase.expect_load)
+ return Promise.resolve();
+ return Promise.reject(new Error(
+ name + ': expected network error but loaded'));
+ },
+ function() {
+ if (!testcase.expect_load)
+ return Promise.resolve();
+ return Promise.reject(new Error(
+ name + ': expected to load but got network error'));
+ });
+}
+
+function run_tests() {
+ var tests = [
+ { name: 'response-object', expect_load: true },
+ { name: 'response-promise-object', expect_load: true },
+ { name: 'other-value', expect_load: false },
+ ].map(make_test);
+
+ Promise.all(tests)
+ .then(function() {
+ window.parent.notify_test_done('PASS');
+ })
+ .catch(function(error) {
+ window.parent.notify_test_done('FAIL: ' + error.message);
+ });
+}
+
+if (!navigator.serviceWorker.controller)
+ window.parent.notify_test_done('FAIL: no controller');
+else
+ run_tests();
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-event-respond-with-argument-worker.js b/test/wpt/tests/service-workers/service-worker/resources/fetch-event-respond-with-argument-worker.js
new file mode 100644
index 0000000..712c4b7
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-event-respond-with-argument-worker.js
@@ -0,0 +1,14 @@
+self.addEventListener('fetch', function(event) {
+ var testcase = new URL(event.request.url).search;
+ switch (testcase) {
+ case '?response-object':
+ event.respondWith(new Response('body'));
+ break;
+ case '?response-promise-object':
+ event.respondWith(Promise.resolve(new Response('body')));
+ break;
+ case '?other-value':
+ event.respondWith(new Object());
+ break;
+ }
+ });
diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-event-respond-with-body-loaded-in-chunk-worker.js b/test/wpt/tests/service-workers/service-worker/resources/fetch-event-respond-with-body-loaded-in-chunk-worker.js
new file mode 100644
index 0000000..d3ba8a8
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-event-respond-with-body-loaded-in-chunk-worker.js
@@ -0,0 +1,7 @@
+'use strict';
+
+self.addEventListener('fetch', event => {
+ if (!event.request.url.match(/body-in-chunk$/))
+ return;
+ event.respondWith(fetch("../../../fetch/api/resources/trickle.py?count=4&delay=50"));
+});
diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-event-respond-with-custom-response-worker.js b/test/wpt/tests/service-workers/service-worker/resources/fetch-event-respond-with-custom-response-worker.js
new file mode 100644
index 0000000..ff24aed
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-event-respond-with-custom-response-worker.js
@@ -0,0 +1,45 @@
+'use strict';
+
+addEventListener('fetch', event => {
+ const url = new URL(event.request.url);
+ const type = url.searchParams.get('type');
+
+ if (!type) return;
+
+ if (type === 'string') {
+ event.respondWith(new Response('PASS'));
+ }
+ else if (type === 'blob') {
+ event.respondWith(
+ new Response(new Blob(['PASS']))
+ );
+ }
+ else if (type === 'buffer-view') {
+ const encoder = new TextEncoder();
+ event.respondWith(
+ new Response(encoder.encode('PASS'))
+ );
+ }
+ else if (type === 'buffer') {
+ const encoder = new TextEncoder();
+ event.respondWith(
+ new Response(encoder.encode('PASS').buffer)
+ );
+ }
+ else if (type === 'form-data') {
+ const body = new FormData();
+ body.set('result', 'PASS');
+ event.respondWith(
+ new Response(body)
+ );
+ }
+ else if (type === 'search-params') {
+ const body = new URLSearchParams();
+ body.set('result', 'PASS');
+ event.respondWith(
+ new Response(body, {
+ headers: { 'Content-Type': 'text/plain' }
+ })
+ );
+ }
+});
diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-event-respond-with-partial-stream-worker.js b/test/wpt/tests/service-workers/service-worker/resources/fetch-event-respond-with-partial-stream-worker.js
new file mode 100644
index 0000000..b7307f2
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-event-respond-with-partial-stream-worker.js
@@ -0,0 +1,28 @@
+let waitUntilResolve;
+
+let bodyController;
+
+self.addEventListener('message', evt => {
+ if (evt.data === 'done') {
+ bodyController.close();
+ waitUntilResolve();
+ }
+});
+
+self.addEventListener('fetch', evt => {
+ if (!evt.request.url.includes('partial-stream.txt')) {
+ return;
+ }
+
+ evt.waitUntil(new Promise(resolve => waitUntilResolve = resolve));
+
+ let body = new ReadableStream({
+ start: controller => {
+ let encoder = new TextEncoder();
+ controller.enqueue(encoder.encode('partial-stream-content'));
+ bodyController = controller;
+ },
+ });
+
+ evt.respondWith(new Response(body));
+});
diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-event-respond-with-readable-stream-chunk-worker.js b/test/wpt/tests/service-workers/service-worker/resources/fetch-event-respond-with-readable-stream-chunk-worker.js
new file mode 100644
index 0000000..f954e3a
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-event-respond-with-readable-stream-chunk-worker.js
@@ -0,0 +1,40 @@
+'use strict';
+
+self.addEventListener('fetch', event => {
+ if (!event.request.url.match(/body-stream$/))
+ return;
+
+ var counter = 0;
+ const encoder = new TextEncoder();
+ const stream = new ReadableStream({ pull: controller => {
+ switch (++counter) {
+ case 1:
+ controller.enqueue(encoder.encode(''));
+ return;
+ case 2:
+ controller.enqueue(encoder.encode('chunk #1'));
+ return;
+ case 3:
+ controller.enqueue(encoder.encode(' '));
+ return;
+ case 4:
+ controller.enqueue(encoder.encode('chunk #2'));
+ return;
+ case 5:
+ controller.enqueue(encoder.encode(' '));
+ return;
+ case 6:
+ controller.enqueue(encoder.encode('chunk #3'));
+ return;
+ case 7:
+ controller.enqueue(encoder.encode(' '));
+ return;
+ case 8:
+ controller.enqueue(encoder.encode('chunk #4'));
+ return;
+ default:
+ controller.close();
+ }
+ }});
+ event.respondWith(new Response(stream));
+});
diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-event-respond-with-readable-stream-worker.js b/test/wpt/tests/service-workers/service-worker/resources/fetch-event-respond-with-readable-stream-worker.js
new file mode 100644
index 0000000..0563513
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-event-respond-with-readable-stream-worker.js
@@ -0,0 +1,81 @@
+'use strict';
+importScripts("/resources/testharness.js");
+
+const map = new Map();
+
+self.addEventListener('fetch', event => {
+ const url = new URL(event.request.url);
+ if (!url.searchParams.has('stream')) return;
+
+ if (url.searchParams.has('observe-cancel')) {
+ const id = url.searchParams.get('id');
+ if (id === undefined) {
+ event.respondWith(new Error('error'));
+ return;
+ }
+ event.waitUntil(new Promise(resolve => {
+ map.set(id, {label: 'pending', resolve});
+ }));
+
+ const stream = new ReadableStream({
+ pull(c) {
+ if (url.searchParams.get('enqueue') === 'true') {
+ url.searchParams.delete('enqueue');
+ c.enqueue(new Uint8Array([65]));
+ }
+ },
+ cancel() {
+ map.get(id).label = 'cancelled';
+ }
+ });
+ event.respondWith(new Response(stream));
+ return;
+ }
+
+ if (url.searchParams.has('query-cancel')) {
+ const id = url.searchParams.get('id');
+ if (id === undefined) {
+ event.respondWith(new Error('error'));
+ return;
+ }
+ const entry = map.get(id);
+ if (entry === undefined) {
+ event.respondWith(new Error('not found'));
+ return;
+ }
+ map.delete(id);
+ entry.resolve();
+ event.respondWith(new Response(entry.label));
+ return;
+ }
+
+ if (url.searchParams.has('use-fetch-stream')) {
+ event.respondWith(async function() {
+ const response = await fetch('pass.txt');
+ return new Response(response.body);
+ }());
+ return;
+ }
+
+ const delayEnqueue = url.searchParams.has('delay');
+
+ const stream = new ReadableStream({
+ start(controller) {
+ const encoder = new TextEncoder();
+
+ const populate = () => {
+ controller.enqueue(encoder.encode('PASS'));
+ controller.close();
+ }
+
+ if (delayEnqueue) {
+ step_timeout(populate, 16);
+ }
+ else {
+ populate();
+ }
+ }
+ });
+
+ event.respondWith(new Response(stream));
+});
diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-event-respond-with-response-body-with-invalid-chunk-iframe.html b/test/wpt/tests/service-workers/service-worker/resources/fetch-event-respond-with-response-body-with-invalid-chunk-iframe.html
new file mode 100644
index 0000000..d15454d
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-event-respond-with-response-body-with-invalid-chunk-iframe.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>respond-with-response-body-with-invalid-chunk</title>
+<body></body>
+<script>
+'use strict';
+
+parent.set_fetch_promise(fetch('body-stream-with-invalid-chunk').then(resp => {
+ const reader = resp.body.getReader();
+ const reader_promise = reader.read();
+ parent.set_reader_promise(reader_promise);
+ // Suppress our expected error.
+ return reader_promise.catch(() => {});
+ }));
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-event-respond-with-response-body-with-invalid-chunk-worker.js b/test/wpt/tests/service-workers/service-worker/resources/fetch-event-respond-with-response-body-with-invalid-chunk-worker.js
new file mode 100644
index 0000000..0254e24
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-event-respond-with-response-body-with-invalid-chunk-worker.js
@@ -0,0 +1,12 @@
+'use strict';
+
+self.addEventListener('fetch', event => {
+ if (!event.request.url.match(/body-stream-with-invalid-chunk$/))
+ return;
+ const stream = new ReadableStream({start: controller => {
+ // The argument is intentionally a string, not a Uint8Array.
+ controller.enqueue('hello');
+ }});
+ const headers = { 'x-content-type-options': 'nosniff' };
+ event.respondWith(new Response(stream, { headers }));
+ });
diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-event-respond-with-stops-propagation-worker.js b/test/wpt/tests/service-workers/service-worker/resources/fetch-event-respond-with-stops-propagation-worker.js
new file mode 100644
index 0000000..18da049
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-event-respond-with-stops-propagation-worker.js
@@ -0,0 +1,15 @@
+var result = null;
+
+self.addEventListener('message', function(event) {
+ event.data.port.postMessage(result);
+ });
+
+self.addEventListener('fetch', function(event) {
+ if (!result)
+ result = 'PASS';
+ event.respondWith(new Response());
+ });
+
+self.addEventListener('fetch', function(event) {
+ result = 'FAIL: fetch event propagated';
+ });
diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-event-test-worker.js b/test/wpt/tests/service-workers/service-worker/resources/fetch-event-test-worker.js
new file mode 100644
index 0000000..813f79d
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-event-test-worker.js
@@ -0,0 +1,224 @@
+function handleHeaders(event) {
+ const headers = Array.from(event.request.headers);
+ event.respondWith(new Response(JSON.stringify(headers)));
+}
+
+function handleString(event) {
+ event.respondWith(new Response('Test string'));
+}
+
+function handleBlob(event) {
+ event.respondWith(new Response(new Blob(['Test blob'])));
+}
+
+function handleReferrer(event) {
+ event.respondWith(new Response(new Blob(
+ ['Referrer: ' + event.request.referrer])));
+}
+
+function handleReferrerPolicy(event) {
+ event.respondWith(new Response(new Blob(
+ ['ReferrerPolicy: ' + event.request.referrerPolicy])));
+}
+
+function handleReferrerFull(event) {
+ event.respondWith(new Response(new Blob(
+ ['Referrer: ' + event.request.referrer + '\n' +
+ 'ReferrerPolicy: ' + event.request.referrerPolicy])));
+}
+
+function handleClientId(event) {
+ var body;
+ if (event.clientId !== "") {
+ body = 'Client ID Found: ' + event.clientId;
+ } else {
+ body = 'Client ID Not Found';
+ }
+ event.respondWith(new Response(body));
+}
+
+function handleResultingClientId(event) {
+ var body;
+ if (event.resultingClientId !== "") {
+ body = 'Resulting Client ID Found: ' + event.resultingClientId;
+ } else {
+ body = 'Resulting Client ID Not Found';
+ }
+ event.respondWith(new Response(body));
+}
+
+function handleNullBody(event) {
+ event.respondWith(new Response());
+}
+
+function handleFetch(event) {
+ event.respondWith(fetch('other.html'));
+}
+
+function handleFormPost(event) {
+ event.respondWith(new Promise(function(resolve) {
+ event.request.text()
+ .then(function(result) {
+ resolve(new Response(event.request.method + ':' +
+ event.request.headers.get('Content-Type') + ':' +
+ result));
+ });
+ }));
+}
+
+function handleMultipleRespondWith(event) {
+ var logForMultipleRespondWith = '';
+ for (var i = 0; i < 3; ++i) {
+ logForMultipleRespondWith += '(' + i + ')';
+ try {
+ event.respondWith(new Promise(function(resolve) {
+ setTimeout(function() {
+ resolve(new Response(logForMultipleRespondWith));
+ }, 0);
+ }));
+ } catch (e) {
+ logForMultipleRespondWith += '[' + e.name + ']';
+ }
+ }
+}
+
+var lastResponseForUsedCheck = undefined;
+
+function handleUsedCheck(event) {
+ if (!lastResponseForUsedCheck) {
+ event.respondWith(fetch('other.html').then(function(response) {
+ lastResponseForUsedCheck = response;
+ return response;
+ }));
+ } else {
+ event.respondWith(new Response(
+ 'bodyUsed: ' + lastResponseForUsedCheck.bodyUsed));
+ }
+}
+function handleFragmentCheck(event) {
+ var body;
+ if (event.request.url.indexOf('#') === -1) {
+ body = 'Fragment Not Found';
+ } else {
+ body = 'Fragment Found :' +
+ event.request.url.substring(event.request.url.indexOf('#'));
+ }
+ event.respondWith(new Response(body));
+}
+function handleCache(event) {
+ event.respondWith(new Response(event.request.cache));
+}
+function handleEventSource(event) {
+ if (event.request.mode === 'navigate') {
+ return;
+ }
+ var data = {
+ mode: event.request.mode,
+ cache: event.request.cache,
+ credentials: event.request.credentials
+ };
+ var body = 'data:' + JSON.stringify(data) + '\n\n';
+ event.respondWith(new Response(body, {
+ headers: { 'Content-Type': 'text/event-stream' }
+ }
+ ));
+}
+
+function handleIntegrity(event) {
+ event.respondWith(new Response(event.request.integrity));
+}
+
+function handleRequestBody(event) {
+ event.respondWith(event.request.text().then(text => {
+ return new Response(text);
+ }));
+}
+
+function handleKeepalive(event) {
+ event.respondWith(new Response(event.request.keepalive));
+}
+
+function handleIsReloadNavigation(event) {
+ const request = event.request;
+ const body =
+ `method = ${request.method}, ` +
+ `isReloadNavigation = ${request.isReloadNavigation}`;
+ event.respondWith(new Response(body));
+}
+
+function handleIsHistoryNavigation(event) {
+ const request = event.request;
+ const body =
+ `method = ${request.method}, ` +
+ `isHistoryNavigation = ${request.isHistoryNavigation}`;
+ event.respondWith(new Response(body));
+}
+
+function handleUseAndIgnore(event) {
+ const request = event.request;
+ request.text();
+ return;
+}
+
+function handleCloneAndIgnore(event) {
+ const request = event.request;
+ request.clone().text();
+ return;
+}
+
+var handle_status_count = 0;
+function handleStatus(event) {
+ handle_status_count++;
+ event.respondWith(async function() {
+ const res = await fetch(event.request);
+ const text = await res.text();
+ return new Response(`${text}. Request was sent ${handle_status_count} times.`,
+ {"status": new URL(event.request.url).searchParams.get("status")});
+ }());
+}
+
+self.addEventListener('fetch', function(event) {
+ var url = event.request.url;
+ var handlers = [
+ { pattern: '?headers', fn: handleHeaders },
+ { pattern: '?string', fn: handleString },
+ { pattern: '?blob', fn: handleBlob },
+ { pattern: '?referrerFull', fn: handleReferrerFull },
+ { pattern: '?referrerPolicy', fn: handleReferrerPolicy },
+ { pattern: '?referrer', fn: handleReferrer },
+ { pattern: '?clientId', fn: handleClientId },
+ { pattern: '?resultingClientId', fn: handleResultingClientId },
+ { pattern: '?ignore', fn: function() {} },
+ { pattern: '?null', fn: handleNullBody },
+ { pattern: '?fetch', fn: handleFetch },
+ { pattern: '?form-post', fn: handleFormPost },
+ { pattern: '?multiple-respond-with', fn: handleMultipleRespondWith },
+ { pattern: '?used-check', fn: handleUsedCheck },
+ { pattern: '?fragment-check', fn: handleFragmentCheck },
+ { pattern: '?cache', fn: handleCache },
+ { pattern: '?eventsource', fn: handleEventSource },
+ { pattern: '?integrity', fn: handleIntegrity },
+ { pattern: '?request-body', fn: handleRequestBody },
+ { pattern: '?keepalive', fn: handleKeepalive },
+ { pattern: '?isReloadNavigation', fn: handleIsReloadNavigation },
+ { pattern: '?isHistoryNavigation', fn: handleIsHistoryNavigation },
+ { pattern: '?use-and-ignore', fn: handleUseAndIgnore },
+ { pattern: '?clone-and-ignore', fn: handleCloneAndIgnore },
+ { pattern: '?status', fn: handleStatus },
+ ];
+
+ var handler = null;
+ for (var i = 0; i < handlers.length; ++i) {
+ if (url.indexOf(handlers[i].pattern) != -1) {
+ handler = handlers[i];
+ break;
+ }
+ }
+
+ if (handler) {
+ handler.fn(event);
+ } else {
+ event.respondWith(new Response(new Blob(
+ ['Service Worker got an unexpected request: ' + url])));
+ }
+ });
diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-event-within-sw-worker.js b/test/wpt/tests/service-workers/service-worker/resources/fetch-event-within-sw-worker.js
new file mode 100644
index 0000000..5903bab
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-event-within-sw-worker.js
@@ -0,0 +1,48 @@
+skipWaiting();
+
+addEventListener('fetch', event => {
+ const url = new URL(event.request.url);
+
+ if (url.origin != location.origin) return;
+
+ if (url.pathname.endsWith('/sample.txt')) {
+ event.respondWith(new Response('intercepted'));
+ return;
+ }
+
+ if (url.pathname.endsWith('/sample.txt-inner-fetch')) {
+ event.respondWith(fetch('sample.txt'));
+ return;
+ }
+
+ if (url.pathname.endsWith('/sample.txt-inner-cache')) {
+ event.respondWith(
+ caches.open('test-inner-cache').then(cache =>
+ cache.add('sample.txt').then(() => cache.match('sample.txt'))
+ )
+ );
+ return;
+ }
+
+ if (url.pathname.endsWith('/show-notification')) {
+ // Copy the currect search string onto the icon url
+ const iconURL = new URL('notification_icon.py', location);
+ iconURL.search = url.search;
+
+ event.respondWith(
+ registration.showNotification('test', {
+ icon: iconURL
+ }).then(() => registration.getNotifications()).then(notifications => {
+ for (const n of notifications) n.close();
+ return new Response('done');
+ })
+ );
+ return;
+ }
+
+ if (url.pathname.endsWith('/notification_icon.py')) {
+ new BroadcastChannel('icon-request').postMessage('yay');
+ event.respondWith(new Response('done'));
+ return;
+ }
+});
diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-header-visibility-iframe.html b/test/wpt/tests/service-workers/service-worker/resources/fetch-header-visibility-iframe.html
new file mode 100644
index 0000000..0d9ab6f
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-header-visibility-iframe.html
@@ -0,0 +1,66 @@
+<script src="/common/get-host-info.sub.js"></script>
+<script src="test-helpers.sub.js?pipe=sub"></script>
+<script>
+ var host_info = get_host_info();
+ var uri = document.location + '?check-ua-header';
+
+ var headers = new Headers();
+ headers.set('User-Agent', 'custom_ua');
+
+ // Check the custom UA case
+ fetch(uri, { headers: headers }).then(function(response) {
+ return response.text();
+ }).then(function(text) {
+ if (text == 'custom_ua') {
+ parent.postMessage('PASS', '*');
+ } else {
+ parent.postMessage('withUA FAIL - expected "custom_ua", got "' + text + '"', '*');
+ }
+ }).catch(function(err) {
+ parent.postMessage('withUA FAIL - unexpected error: ' + err, '*');
+ });
+
+ // Check the default UA case
+ fetch(uri, {}).then(function(response) {
+ return response.text();
+ }).then(function(text) {
+ if (text == 'NO_UA') {
+ parent.postMessage('PASS', '*');
+ } else {
+ parent.postMessage('noUA FAIL - expected "NO_UA", got "' + text + '"', '*');
+ }
+ }).catch(function(err) {
+ parent.postMessage('noUA FAIL - unexpected error: ' + err, '*');
+ });
+
+ var uri = document.location + '?check-accept-header';
+ var headers = new Headers();
+ headers.set('Accept', 'hmm');
+
+ // Check for custom accept header
+ fetch(uri, { headers: headers }).then(function(response) {
+ return response.text();
+ }).then(function(text) {
+ if (text === headers.get('Accept')) {
+ parent.postMessage('PASS', '*');
+ } else {
+ parent.postMessage('custom accept FAIL - expected ' + headers.get('Accept') +
+ ' got "' + text + '"', '*');
+ }
+ }).catch(function(err) {
+ parent.postMessage('custom accept FAIL - unexpected error: ' + err, '*');
+ });
+
+ // Check for default accept header
+ fetch(uri).then(function(response) {
+ return response.text();
+ }).then(function(text) {
+ if (text === '*/*') {
+ parent.postMessage('PASS', '*');
+ } else {
+ parent.postMessage('accept FAIL - expected */* got "' + text + '"', '*');
+ }
+ }).catch(function(err) {
+ parent.postMessage('accept FAIL - unexpected error: ' + err, '*');
+ });
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-mixed-content-iframe-inscope-to-inscope.html b/test/wpt/tests/service-workers/service-worker/resources/fetch-mixed-content-iframe-inscope-to-inscope.html
new file mode 100644
index 0000000..64a634e
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-mixed-content-iframe-inscope-to-inscope.html
@@ -0,0 +1,71 @@
+<script src="/common/get-host-info.sub.js"></script>
+<script src="test-helpers.sub.js?pipe=sub"></script>
+<script>
+var image_path = base_path() + 'fetch-access-control.py?PNGIMAGE';
+var host_info = get_host_info();
+var results = '';
+
+function test1() {
+ var img = document.createElement('img');
+ document.body.appendChild(img);
+ img.onload = function() {
+ test2();
+ };
+ img.onerror = function() {
+ results += 'FAIL(1)';
+ test2();
+ };
+ img.src = './sample?url=' +
+ encodeURIComponent(host_info['HTTPS_ORIGIN'] + image_path);
+}
+
+function test2() {
+ var img = document.createElement('img');
+ document.body.appendChild(img);
+ img.onload = function() {
+ test3();
+ };
+ img.onerror = function() {
+ results += 'FAIL(2)';
+ test3();
+ };
+ img.src = './sample?mode=no-cors&url=' +
+ encodeURIComponent(host_info['HTTPS_REMOTE_ORIGIN'] + image_path);
+}
+
+function test3() {
+ var img = document.createElement('img');
+ document.body.appendChild(img);
+ img.onload = function() {
+ results += 'FAIL(3)';
+ test4();
+ };
+ img.onerror = function() {
+ test4();
+ };
+ img.src = './sample?mode=no-cors&url=' +
+ encodeURIComponent(host_info['HTTP_ORIGIN'] + image_path);
+}
+
+function test4() {
+ var img = document.createElement('img');
+ document.body.appendChild(img);
+ img.onload = function() {
+ results += 'FAIL(4)';
+ finish();
+ };
+ img.onerror = function() {
+ finish();
+ };
+ img.src = './sample?mode=no-cors&url=' +
+ encodeURIComponent(host_info['HTTP_REMOTE_ORIGIN'] + image_path);
+}
+
+function finish() {
+ results += 'finish';
+ window.parent.postMessage({results: results}, host_info['HTTPS_ORIGIN']);
+}
+</script>
+
+<body onload='test1();'>
+</body>
diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-mixed-content-iframe-inscope-to-outscope.html b/test/wpt/tests/service-workers/service-worker/resources/fetch-mixed-content-iframe-inscope-to-outscope.html
new file mode 100644
index 0000000..be0b5c8
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-mixed-content-iframe-inscope-to-outscope.html
@@ -0,0 +1,80 @@
+<script src="/common/get-host-info.sub.js"></script>
+<script src="test-helpers.sub.js?pipe=sub"></script>
+<script>
+var image_path = base_path() + 'fetch-access-control.py?PNGIMAGE';
+var host_info = get_host_info();
+var results = '';
+
+function test1() {
+ var img = document.createElement('img');
+ document.body.appendChild(img);
+ img.onload = function() {
+ test2();
+ };
+ img.onerror = function() {
+ results += 'FAIL(1)';
+ test2();
+ };
+ img.src = host_info['HTTPS_ORIGIN'] + image_path;
+}
+
+function test2() {
+ var img = document.createElement('img');
+ document.body.appendChild(img);
+ img.onload = function() {
+ test3();
+ };
+ img.onerror = function() {
+ results += 'FAIL(2)';
+ test3();
+ };
+ img.src = host_info['HTTPS_REMOTE_ORIGIN'] + image_path;
+}
+
+function test3() {
+ var img = document.createElement('img');
+ document.body.appendChild(img);
+ img.onload = function() {
+ results += 'FAIL(3)';
+ test4();
+ };
+ img.onerror = function() {
+ test4();
+ };
+ img.src = host_info['HTTP_ORIGIN'] + image_path;
+}
+
+function test4() {
+ var img = document.createElement('img');
+ document.body.appendChild(img);
+ img.onload = function() {
+ results += 'FAIL(4)';
+ test5();
+ };
+ img.onerror = function() {
+ test5();
+ };
+ img.src = host_info['HTTP_REMOTE_ORIGIN'] + image_path;
+}
+
+function test5() {
+ var img = document.createElement('img');
+ document.body.appendChild(img);
+ img.onload = function() {
+ finish();
+ };
+ img.onerror = function() {
+ results += 'FAIL(5)';
+ finish();
+ };
+ img.src = './sample?generate-png';
+}
+
+function finish() {
+ results += 'finish';
+ window.parent.postMessage({results: results}, host_info['HTTPS_ORIGIN']);
+}
+</script>
+
+<body onload='test1();'>
+</body>
diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-mixed-content-iframe.html b/test/wpt/tests/service-workers/service-worker/resources/fetch-mixed-content-iframe.html
new file mode 100644
index 0000000..2831c38
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-mixed-content-iframe.html
@@ -0,0 +1,71 @@
+<!DOCTYPE html>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="test-helpers.sub.js?pipe=sub"></script>
+<script>
+var params = get_query_params(location.href);
+var SCOPE = 'fetch-mixed-content-iframe-inscope-to-' + params['target'] + '.html';
+var URL = 'fetch-rewrite-worker.js';
+var host_info = get_host_info();
+
+window.addEventListener('message', on_message, false);
+
+navigator.serviceWorker.getRegistration(SCOPE)
+ .then(function(registration) {
+ if (registration)
+ return registration.unregister();
+ })
+ .then(function() {
+ return navigator.serviceWorker.register(URL, {scope: SCOPE});
+ })
+ .then(function(registration) {
+ return new Promise(function(resolve) {
+ registration.addEventListener('updatefound', function() {
+ resolve(registration.installing);
+ });
+ });
+ })
+ .then(function(worker) {
+ worker.addEventListener('statechange', on_state_change);
+ })
+ .catch(function(reason) {
+ window.parent.postMessage({results: 'FAILURE: ' + reason.message},
+ host_info['HTTPS_ORIGIN']);
+ });
+
+function on_state_change(event) {
+ if (event.target.state != 'activated')
+ return;
+ var frame = document.createElement('iframe');
+ frame.src = SCOPE;
+ document.body.appendChild(frame);
+}
+
+function on_message(e) {
+ navigator.serviceWorker.getRegistration(SCOPE)
+ .then(function(registration) {
+ if (registration)
+ return registration.unregister();
+ })
+ .then(function() {
+ window.parent.postMessage(e.data, host_info['HTTPS_ORIGIN']);
+ })
+ .catch(function(reason) {
+ window.parent.postMessage({results: 'FAILURE: ' + reason.message},
+ host_info['HTTPS_ORIGIN']);
+ });
+}
+
+function get_query_params(url) {
+ var search = (new URL(url)).search;
+ if (!search) {
+ return {};
+ }
+ var ret = {};
+ var params = search.substring(1).split('&');
+ params.forEach(function(param) {
+ var element = param.split('=');
+ ret[decodeURIComponent(element[0])] = decodeURIComponent(element[1]);
+ });
+ return ret;
+}
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-request-css-base-url-iframe.html b/test/wpt/tests/service-workers/service-worker/resources/fetch-request-css-base-url-iframe.html
new file mode 100644
index 0000000..504e104
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-request-css-base-url-iframe.html
@@ -0,0 +1,20 @@
+<html>
+<head>
+<title>iframe for css base url test</title>
+</head>
+<body>
+<script>
+// Load a stylesheet. Create it dynamically so we can construct the href URL
+// dynamically.
+const link = document.createElement('link');
+link.rel = 'stylesheet';
+link.type = 'text/css';
+// Add "request-url-path" to the path to help distinguish the request URL from
+// the response URL. Add |document.location.search| (chosen by the test main
+// page) to tell the service worker how to respond to the request.
+link.href = 'request-url-path/fetch-request-css-base-url-style.css' +
+ document.location.search;
+document.head.appendChild(link);
+</script>
+</body>
+</html>
diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-request-css-base-url-style.css b/test/wpt/tests/service-workers/service-worker/resources/fetch-request-css-base-url-style.css
new file mode 100644
index 0000000..f14fcaa
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-request-css-base-url-style.css
@@ -0,0 +1 @@
+body { background-image: url("./sample.png");}
diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-request-css-base-url-worker.js b/test/wpt/tests/service-workers/service-worker/resources/fetch-request-css-base-url-worker.js
new file mode 100644
index 0000000..f3d6a73
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-request-css-base-url-worker.js
@@ -0,0 +1,45 @@
+let source;
+let resolveDone;
+let done = new Promise(resolve => resolveDone = resolve);
+
+// The page messages this worker to ask for the result. Keep the worker alive
+// via waitUntil() until the result is sent.
+self.addEventListener('message', event => {
+ source = event.data.port;
+ source.postMessage('pong');
+ event.waitUntil(done);
+});
+
+self.addEventListener('fetch', event => {
+ const url = new URL(event.request.url);
+
+ // For the CSS file, respond in a way that may change the response URL,
+ // depending on |url.search|.
+ const cssPath = 'request-url-path/fetch-request-css-base-url-style.css';
+ if (url.pathname.indexOf(cssPath) != -1) {
+ // Respond with a different URL, deleting "request-url-path/".
+ if (url.search == '?fetch') {
+ event.respondWith(fetch('fetch-request-css-base-url-style.css?fetch'));
+ }
+ // Respond with new Response().
+ else if (url.search == '?newResponse') {
+ const styleString = 'body { background-image: url("./sample.png");}';
+ const headers = {'content-type': 'text/css'};
+ event.respondWith(new Response(styleString, headers));
+ }
+ }
+
+ // The image request indicates what the base URL of the CSS was. Message the
+ // result back to the test page.
+ else if (url.pathname.indexOf('sample.png') != -1) {
+ // For some reason |source| is undefined here when running the test manually
+ // in Firefox. The test author experimented with both using Client
+ // (event.source) and MessagePort to try to get the test to pass, but
+ // failed.
+ source.postMessage({
+ url: event.request.url,
+ referrer: event.request.referrer
+ });
+ resolveDone();
+ }
+});
diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-mime-check-cross.css b/test/wpt/tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-mime-check-cross.css
new file mode 100644
index 0000000..9a7545d
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-mime-check-cross.css
@@ -0,0 +1 @@
+#crossOriginCss { color: blue; }
diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-mime-check-cross.html b/test/wpt/tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-mime-check-cross.html
new file mode 100644
index 0000000..3211f78
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-mime-check-cross.html
@@ -0,0 +1 @@
+#crossOriginHtml { color: red; }
diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-mime-check-iframe.html b/test/wpt/tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-mime-check-iframe.html
new file mode 100644
index 0000000..9a4aded
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-mime-check-iframe.html
@@ -0,0 +1,17 @@
+<style type="text/css">
+#crossOriginCss { color: red; }
+#crossOriginHtml { color: blue; }
+#sameOriginCss { color: red; }
+#sameOriginHtml { color: red; }
+#synthetic { color: red; }
+</style>
+<link href="./cross-origin-css.css?mime=no" rel="stylesheet" type="text/css">
+<link href="./cross-origin-html.css?mime=no" rel="stylesheet" type="text/css">
+<link href="./fetch-request-css-cross-origin-mime-check-same.css" rel="stylesheet" type="text/css">
+<link href="./fetch-request-css-cross-origin-mime-check-same.html" rel="stylesheet" type="text/css">
+<link href="./synthetic.css?mime=no" rel="stylesheet" type="text/css">
+<h1 id=crossOriginCss>I should be blue</h1>
+<h1 id=crossOriginHtml>I should be blue</h1>
+<h1 id=sameOriginCss>I should be blue</h1>
+<h1 id=sameOriginHtml>I should be blue</h1>
+<h1 id=synthetic>I should be blue</h1>
diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-mime-check-same.css b/test/wpt/tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-mime-check-same.css
new file mode 100644
index 0000000..55455bd
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-mime-check-same.css
@@ -0,0 +1 @@
+#sameOriginCss { color: blue; }
diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-mime-check-same.html b/test/wpt/tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-mime-check-same.html
new file mode 100644
index 0000000..6fad4b9
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-mime-check-same.html
@@ -0,0 +1 @@
+#sameOriginHtml { color: blue; }
diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-read-contents.html b/test/wpt/tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-read-contents.html
new file mode 100644
index 0000000..c902366
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-read-contents.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>iframe: cross-origin CSS via service worker</title>
+
+<!-- Service worker responds with a cross-origin opaque response. -->
+<link href="cross-origin-css.css" rel="stylesheet" type="text/css">
+
+<!-- Service worker responds with a cross-origin CORS approved response. -->
+<link href="cross-origin-css.css?cors" rel="stylesheet" type="text/css">
+
+<!-- Service worker falls back to network. This is a same-origin response. -->
+<link href="fetch-request-css-cross-origin-mime-check-same.css" rel="stylesheet" type="text/css">
+
+<!-- Service worker responds with a new Response() synthetic response. -->
+<link href="synthetic.css" rel="stylesheet" type="text/css">
diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-worker.js b/test/wpt/tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-worker.js
new file mode 100644
index 0000000..a71e912
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-worker.js
@@ -0,0 +1,65 @@
+importScripts('/common/get-host-info.sub.js');
+importScripts('test-helpers.sub.js');
+
+const HOST_INFO = get_host_info();
+const REMOTE_ORIGIN = HOST_INFO.HTTPS_REMOTE_ORIGIN;
+const BASE_PATH = base_path();
+const CSS_FILE = 'fetch-request-css-cross-origin-mime-check-cross.css';
+const HTML_FILE = 'fetch-request-css-cross-origin-mime-check-cross.html';
+
+function add_pipe_header(url_str, header) {
+ if (url_str.indexOf('?pipe=') == -1) {
+ url_str += '?pipe=';
+ } else {
+ url_str += '|';
+ }
+ url_str += `header${header}`;
+ return url_str;
+}
+
+self.addEventListener('fetch', function(event) {
+ const url = new URL(event.request.url);
+
+ const use_mime =
+ (url.searchParams.get('mime') != 'no');
+ const mime_header = '(Content-Type, text/css)';
+
+ const use_cors =
+ (url.searchParams.has('cors'));
+ const cors_header = '(Access-Control-Allow-Origin, *)';
+
+ const file = url.pathname.substring(url.pathname.lastIndexOf('/') + 1);
+
+ // Respond with a cross-origin CSS resource, using CORS if desired.
+ if (file == 'cross-origin-css.css') {
+ let fetch_url = REMOTE_ORIGIN + BASE_PATH + CSS_FILE;
+ if (use_mime)
+ fetch_url = add_pipe_header(fetch_url, mime_header);
+ if (use_cors)
+ fetch_url = add_pipe_header(fetch_url, cors_header);
+ const mode = use_cors ? 'cors' : 'no-cors';
+ event.respondWith(fetch(fetch_url, {'mode': mode}));
+ return;
+ }
+
+ // Respond with a cross-origin CSS resource with an HTML name. This is only
+ // used in the MIME sniffing test, so MIME is never added.
+ if (file == 'cross-origin-html.css') {
+ const fetch_url = REMOTE_ORIGIN + BASE_PATH + HTML_FILE;
+ event.respondWith(fetch(fetch_url, {mode: 'no-cors'}));
+ return;
+ }
+
+ // Respond with synthetic CSS.
+ if (file == 'synthetic.css') {
+ let headers = {};
+ if (use_mime) {
+ headers['Content-Type'] = 'text/css';
+ }
+
+ event.respondWith(new Response("#synthetic { color: blue; }", {headers}));
+ return;
+ }
+
+ // Otherwise, fallback to network.
+ });
diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-request-fallback-iframe.html b/test/wpt/tests/service-workers/service-worker/resources/fetch-request-fallback-iframe.html
new file mode 100644
index 0000000..d117d0f
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-request-fallback-iframe.html
@@ -0,0 +1,32 @@
+<script>
+function xhr(url) {
+ return new Promise(function(resolve, reject) {
+ var request = new XMLHttpRequest();
+ request.addEventListener(
+ 'error',
+ function() { reject(new Error()); });
+ request.addEventListener(
+ 'load',
+ function(event) { resolve(request.response); });
+ request.open('GET', url);
+ request.send();
+ });
+}
+
+function load_image(url, cross_origin) {
+ return new Promise(function(resolve, reject) {
+ var img = document.createElement('img');
+ document.body.appendChild(img);
+ img.onload = function() {
+ resolve();
+ };
+ img.onerror = function() {
+ reject(new Error());
+ };
+ if (cross_origin != '') {
+ img.crossOrigin = cross_origin;
+ }
+ img.src = url;
+ });
+}
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-request-fallback-worker.js b/test/wpt/tests/service-workers/service-worker/resources/fetch-request-fallback-worker.js
new file mode 100644
index 0000000..3b028b2
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-request-fallback-worker.js
@@ -0,0 +1,13 @@
+var requests = [];
+
+self.addEventListener('message', function(event) {
+ event.data.port.postMessage({requests: requests});
+ requests = [];
+ });
+
+self.addEventListener('fetch', function(event) {
+ requests.push({
+ url: event.request.url,
+ mode: event.request.mode
+ });
+ });
diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-request-html-imports-iframe.html b/test/wpt/tests/service-workers/service-worker/resources/fetch-request-html-imports-iframe.html
new file mode 100644
index 0000000..07a0842
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-request-html-imports-iframe.html
@@ -0,0 +1,13 @@
+<script src="/common/get-host-info.sub.js"></script>
+<script type="text/javascript">
+ var hostInfo = get_host_info();
+ var makeLink = function(id, url) {
+ var link = document.createElement('link');
+ link.rel = 'import'
+ link.id = id;
+ link.href = url;
+ document.documentElement.appendChild(link);
+ };
+ makeLink('same', hostInfo.HTTPS_ORIGIN + '/sample-dir/same.html');
+ makeLink('other', hostInfo.HTTPS_REMOTE_ORIGIN + '/sample-dir/other.html');
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-request-html-imports-worker.js b/test/wpt/tests/service-workers/service-worker/resources/fetch-request-html-imports-worker.js
new file mode 100644
index 0000000..110727b
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-request-html-imports-worker.js
@@ -0,0 +1,30 @@
+importScripts('/common/get-host-info.sub.js');
+var host_info = get_host_info();
+
+self.addEventListener('fetch', function(event) {
+ var url = event.request.url;
+ if (url.indexOf('sample-dir') == -1) {
+ return;
+ }
+ var result = 'mode=' + event.request.mode +
+ ' credentials=' + event.request.credentials;
+ if (url == host_info.HTTPS_ORIGIN + '/sample-dir/same.html') {
+ event.respondWith(new Response(
+ result +
+ '<link id="same-same" rel="import" ' +
+ 'href="' + host_info.HTTPS_ORIGIN + '/sample-dir/same-same.html">' +
+ '<link id="same-other" rel="import" ' +
+ ' href="' + host_info.HTTPS_REMOTE_ORIGIN +
+ '/sample-dir/same-other.html">'));
+ } else if (url == host_info.HTTPS_REMOTE_ORIGIN + '/sample-dir/other.html') {
+ event.respondWith(new Response(
+ result +
+ '<link id="other-same" rel="import" ' +
+ ' href="' + host_info.HTTPS_ORIGIN + '/sample-dir/other-same.html">' +
+ '<link id="other-other" rel="import" ' +
+ ' href="' + host_info.HTTPS_REMOTE_ORIGIN +
+ '/sample-dir/other-other.html">'));
+ } else {
+ event.respondWith(new Response(result));
+ }
+ });
diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-request-no-freshness-headers-iframe.html b/test/wpt/tests/service-workers/service-worker/resources/fetch-request-no-freshness-headers-iframe.html
new file mode 100644
index 0000000..e6e9380
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-request-no-freshness-headers-iframe.html
@@ -0,0 +1 @@
+<script src="./fetch-request-no-freshness-headers-script.py"></script>
diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-request-no-freshness-headers-script.py b/test/wpt/tests/service-workers/service-worker/resources/fetch-request-no-freshness-headers-script.py
new file mode 100644
index 0000000..bf8df15
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-request-no-freshness-headers-script.py
@@ -0,0 +1,6 @@
+def main(request, response):
+ headers = []
+ # Sets an ETag header to check the cache revalidation behavior.
+ headers.append((b"ETag", b"abc123"))
+ headers.append((b"Content-Type", b"text/javascript"))
+ return headers, b"/* empty script */"
diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-request-no-freshness-headers-worker.js b/test/wpt/tests/service-workers/service-worker/resources/fetch-request-no-freshness-headers-worker.js
new file mode 100644
index 0000000..2bd59d7
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-request-no-freshness-headers-worker.js
@@ -0,0 +1,18 @@
+var requests = [];
+
+self.addEventListener('message', function(event) {
+ event.data.port.postMessage({requests: requests});
+ });
+
+self.addEventListener('fetch', function(event) {
+ var url = event.request.url;
+ var headers = [];
+ for (var header of event.request.headers) {
+ headers.push(header);
+ }
+ requests.push({
+ url: url,
+ headers: headers
+ });
+ event.respondWith(fetch(event.request));
+ });
diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-request-redirect-iframe.html b/test/wpt/tests/service-workers/service-worker/resources/fetch-request-redirect-iframe.html
new file mode 100644
index 0000000..ffd76bf
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-request-redirect-iframe.html
@@ -0,0 +1,35 @@
+<script>
+function xhr(url) {
+ return new Promise(function(resolve, reject) {
+ var request = new XMLHttpRequest();
+ request.addEventListener(
+ 'error',
+ function(event) { reject(event); });
+ request.addEventListener(
+ 'load',
+ function(event) { resolve(request.response); });
+ request.open('GET', url);
+ request.send();
+ });
+}
+
+function load_image(url) {
+ return new Promise(function(resolve, reject) {
+ var img = document.createElement('img');
+ document.body.appendChild(img);
+ img.onload = resolve;
+ img.onerror = reject;
+ img.src = url;
+ });
+}
+
+function load_audio(url) {
+ return new Promise(function(resolve, reject) {
+ var audio = document.createElement('audio');
+ document.body.appendChild(audio);
+ audio.oncanplay = resolve;
+ audio.onerror = reject;
+ audio.src = url;
+ });
+}
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-request-resources-iframe.https.html b/test/wpt/tests/service-workers/service-worker/resources/fetch-request-resources-iframe.https.html
new file mode 100644
index 0000000..86e9f4b
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-request-resources-iframe.https.html
@@ -0,0 +1,87 @@
+<script src="test-helpers.sub.js?pipe=sub"></script>
+<body>
+<script>
+
+function load_image(url, cross_origin) {
+ const img = document.createElement('img');
+ if (cross_origin != '') {
+ img.crossOrigin = cross_origin;
+ }
+ img.src = url;
+}
+
+function load_script(url, cross_origin) {
+ const script = document.createElement('script');
+ script.src = url;
+ if (cross_origin != '') {
+ script.crossOrigin = cross_origin;
+ }
+ document.body.appendChild(script);
+}
+
+function load_css(url, cross_origin) {
+ const link = document.createElement('link');
+ link.rel = 'stylesheet'
+ link.href = url;
+ link.type = 'text/css';
+ if (cross_origin != '') {
+ link.crossOrigin = cross_origin;
+ }
+ document.body.appendChild(link);
+}
+
+function load_font(url) {
+ const fontFace = new FontFace('test', 'url(' + url + ')');
+ fontFace.load();
+}
+
+function load_css_image(url, type) {
+ const div = document.createElement('div');
+ document.body.appendChild(div);
+ div.style[type] = 'url(' + url + ')';
+}
+
+function load_css_image_set(url, type) {
+ const div = document.createElement('div');
+ document.body.appendChild(div);
+ div.style[type] = 'image-set(url(' + url + ') 1x)';
+ if (!div.style[type]) {
+ div.style[type] = '-webkit-image-set(url(' + url + ') 1x)';
+ }
+}
+
+function load_script_with_integrity(url, integrity) {
+ const script = document.createElement('script');
+ script.src = url;
+ script.integrity = integrity;
+ document.body.appendChild(script);
+}
+
+function load_css_with_integrity(url, integrity) {
+ const link = document.createElement('link');
+ link.rel = 'stylesheet'
+ link.href = url;
+ link.type = 'text/css';
+ link.integrity = integrity;
+ document.body.appendChild(link);
+}
+
+function load_audio(url, cross_origin) {
+ const audio = document.createElement('audio');
+ if (cross_origin != '') {
+ audio.crossOrigin = cross_origin;
+ }
+ audio.src = url;
+ document.body.appendChild(audio);
+}
+
+function load_video(url, cross_origin) {
+ const video = document.createElement('video');
+ if (cross_origin != '') {
+ video.crossOrigin = cross_origin;
+ }
+ video.src = url;
+ document.body.appendChild(video);
+}
+</script>
+</body>
diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-request-resources-worker.js b/test/wpt/tests/service-workers/service-worker/resources/fetch-request-resources-worker.js
new file mode 100644
index 0000000..983cccb
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-request-resources-worker.js
@@ -0,0 +1,26 @@
+const requests = [];
+let port = undefined;
+
+self.onmessage = e => {
+ const message = e.data;
+ if ('port' in message) {
+ port = message.port;
+ port.postMessage({ready: true});
+ }
+};
+
+self.addEventListener('fetch', e => {
+ const url = e.request.url;
+ if (!url.includes('sample?test')) {
+ return;
+ }
+ port.postMessage({
+ url: url,
+ mode: e.request.mode,
+ redirect: e.request.redirect,
+ credentials: e.request.credentials,
+ integrity: e.request.integrity,
+ destination: e.request.destination
+ });
+ e.respondWith(Promise.reject());
+});
diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-request-xhr-iframe.https.html b/test/wpt/tests/service-workers/service-worker/resources/fetch-request-xhr-iframe.https.html
new file mode 100644
index 0000000..b3ddec1
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-request-xhr-iframe.https.html
@@ -0,0 +1,208 @@
+<script src="/common/get-host-info.sub.js"></script>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/test-helpers.sub.js?pipe=sub"></script>
+<script>
+var host_info = get_host_info();
+
+function get_boundary(headers) {
+ var reg = new RegExp('multipart\/form-data; boundary=(.*)');
+ for (var i = 0; i < headers.length; ++i) {
+ if (headers[i][0] != 'content-type') {
+ continue;
+ }
+ var regResult = reg.exec(headers[i][1]);
+ if (!regResult) {
+ continue;
+ }
+ return regResult[1];
+ }
+ return '';
+}
+
+function xhr_send(url_base, method, data, with_credentials) {
+ return new Promise(function(resolve, reject) {
+ var xhr = new XMLHttpRequest();
+ xhr.onload = function() {
+ resolve(JSON.parse(xhr.response));
+ };
+ xhr.onerror = function() {
+ reject('XHR should succeed.');
+ };
+ xhr.responseType = 'text';
+ if (with_credentials) {
+ xhr.withCredentials = true;
+ }
+ xhr.open(method, url_base + '/sample?test', true);
+ xhr.send(data);
+ });
+}
+
+function get_sorted_header_name_list(headers) {
+ var header_names = [];
+ var idx, name;
+
+ for (idx = 0; idx < headers.length; ++idx) {
+ name = headers[idx][0];
+ // The `Accept-Language` header is optional; its presence should not
+ // influence test results.
+ //
+ // > 4. If request’s header list does not contain `Accept-Language`, user
+ // > agents should append `Accept-Language`/an appropriate value to
+ // > request's header list.
+ //
+ // https://fetch.spec.whatwg.org/#fetching
+ if (name === 'accept-language') {
+ continue;
+ }
+
+ header_names.push(name);
+ }
+ header_names.sort();
+ return header_names;
+}
+
+function get_header_test() {
+ return xhr_send(host_info['HTTPS_ORIGIN'], 'GET', '', false)
+ .then(function(response) {
+ assert_array_equals(
+ get_sorted_header_name_list(response.headers),
+ ["accept"],
+ 'event.request has the expected headers for same-origin GET.');
+ });
+}
+
+function post_header_test() {
+ return xhr_send(host_info['HTTPS_ORIGIN'], 'POST', '', false)
+ .then(function(response) {
+ assert_array_equals(
+ get_sorted_header_name_list(response.headers),
+ ["accept", "content-type"],
+ 'event.request has the expected headers for same-origin POST.');
+ });
+}
+
+function cross_origin_get_header_test() {
+ return xhr_send(host_info['HTTPS_REMOTE_ORIGIN'], 'GET', '', false)
+ .then(function(response) {
+ assert_array_equals(
+ get_sorted_header_name_list(response.headers),
+ ["accept"],
+ 'event.request has the expected headers for cross-origin GET.');
+ });
+}
+
+function cross_origin_post_header_test() {
+ return xhr_send(host_info['HTTPS_REMOTE_ORIGIN'], 'POST', '', false)
+ .then(function(response) {
+ assert_array_equals(
+ get_sorted_header_name_list(response.headers),
+ ["accept", "content-type"],
+ 'event.request has the expected headers for cross-origin POST.');
+ });
+}
+
+function string_test() {
+ return xhr_send(host_info['HTTPS_ORIGIN'], 'POST', 'test string', false)
+ .then(function(response) {
+ assert_equals(response.method, 'POST');
+ assert_equals(response.body, 'test string');
+ });
+}
+
+function blob_test() {
+ return xhr_send(host_info['HTTPS_ORIGIN'], 'POST', new Blob(['test blob']),
+ false)
+ .then(function(response) {
+ assert_equals(response.method, 'POST');
+ assert_equals(response.body, 'test blob');
+ });
+}
+
+function custom_method_test() {
+ return xhr_send(host_info['HTTPS_ORIGIN'], 'XXX', 'test string xxx', false)
+ .then(function(response) {
+ assert_equals(response.method, 'XXX');
+ assert_equals(response.body, 'test string xxx');
+ });
+}
+
+function options_method_test() {
+ return xhr_send(host_info['HTTPS_ORIGIN'], 'OPTIONS', 'test string xxx', false)
+ .then(function(response) {
+ assert_equals(response.method, 'OPTIONS');
+ assert_equals(response.body, 'test string xxx');
+ });
+}
+
+function form_data_test() {
+ var formData = new FormData();
+ formData.append('sample string', '1234567890');
+ formData.append('sample blob', new Blob(['blob content']));
+ formData.append('sample file', new File(['file content'], 'file.dat'));
+ return xhr_send(host_info['HTTPS_ORIGIN'], 'POST', formData, false)
+ .then(function(response) {
+ assert_equals(response.method, 'POST');
+ var boundary = get_boundary(response.headers);
+ var expected_body =
+ '--' + boundary + '\r\n' +
+ 'Content-Disposition: form-data; name="sample string"\r\n' +
+ '\r\n' +
+ '1234567890\r\n' +
+ '--' + boundary + '\r\n' +
+ 'Content-Disposition: form-data; name="sample blob"; ' +
+ 'filename="blob"\r\n' +
+ 'Content-Type: application/octet-stream\r\n' +
+ '\r\n' +
+ 'blob content\r\n' +
+ '--' + boundary + '\r\n' +
+ 'Content-Disposition: form-data; name="sample file"; ' +
+ 'filename="file.dat"\r\n' +
+ 'Content-Type: application/octet-stream\r\n' +
+ '\r\n' +
+ 'file content\r\n' +
+ '--' + boundary + '--\r\n';
+ assert_equals(response.body, expected_body, "form data response content is as expected");
+ });
+}
+
+function mode_credentials_test() {
+ return xhr_send(host_info['HTTPS_ORIGIN'], 'GET', '', false)
+ .then(function(response){
+ assert_equals(response.mode, 'cors');
+ assert_equals(response.credentials, 'same-origin');
+ return xhr_send(host_info['HTTPS_ORIGIN'], 'GET', '', true);
+ })
+ .then(function(response){
+ assert_equals(response.mode, 'cors');
+ assert_equals(response.credentials, 'include');
+ return xhr_send(host_info['HTTPS_REMOTE_ORIGIN'], 'GET', '', false);
+ })
+ .then(function(response){
+ assert_equals(response.mode, 'cors');
+ assert_equals(response.credentials, 'same-origin');
+ return xhr_send(host_info['HTTPS_REMOTE_ORIGIN'], 'GET', '', true);
+ })
+ .then(function(response){
+ assert_equals(response.mode, 'cors');
+ assert_equals(response.credentials, 'include');
+ });
+}
+
+function data_url_test() {
+ return new Promise(function(resolve, reject) {
+ var xhr = new XMLHttpRequest();
+ xhr.onload = function() {
+ resolve(xhr.response);
+ };
+ xhr.onerror = function() {
+ reject('XHR should succeed.');
+ };
+ xhr.responseType = 'text';
+ xhr.open('GET', 'data:text/html,Foobar', true);
+ xhr.send();
+ })
+ .then(function(data) {
+ assert_equals(data, 'Foobar');
+ });
+}
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-request-xhr-sync-error-worker.js b/test/wpt/tests/service-workers/service-worker/resources/fetch-request-xhr-sync-error-worker.js
new file mode 100644
index 0000000..b8d3db9
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-request-xhr-sync-error-worker.js
@@ -0,0 +1,19 @@
+"use strict";
+
+self.onfetch = event => {
+ if (event.request.url.endsWith("non-existent-stream-1.txt")) {
+ const rs1 = new ReadableStream();
+ event.respondWith(new Response(rs1));
+ rs1.cancel(1);
+ } else if (event.request.url.endsWith("non-existent-stream-2.txt")) {
+ const rs2 = new ReadableStream({
+ start(controller) { controller.error(1) }
+ });
+ event.respondWith(new Response(rs2));
+ } else if (event.request.url.endsWith("non-existent-stream-3.txt")) {
+ const rs3 = new ReadableStream({
+ pull(controller) { controller.error(1) }
+ });
+ event.respondWith(new Response(rs3));
+ }
+};
diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-request-xhr-sync-iframe.html b/test/wpt/tests/service-workers/service-worker/resources/fetch-request-xhr-sync-iframe.html
new file mode 100644
index 0000000..900762f
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-request-xhr-sync-iframe.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<title>Service Worker: Synchronous XHR is intercepted iframe</title>
+<script>
+'use strict';
+
+function performSyncXHR(url) {
+ var syncXhr = new XMLHttpRequest();
+ syncXhr.open('GET', url, false);
+ syncXhr.send();
+
+ return syncXhr;
+}
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-request-xhr-sync-on-worker-worker.js b/test/wpt/tests/service-workers/service-worker/resources/fetch-request-xhr-sync-on-worker-worker.js
new file mode 100644
index 0000000..0d24ffc
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-request-xhr-sync-on-worker-worker.js
@@ -0,0 +1,41 @@
+'use strict';
+
+self.onfetch = function(event) {
+ if (event.request.url.indexOf('non-existent-file.txt') !== -1) {
+ event.respondWith(new Response('Response from service worker'));
+ } else if (event.request.url.indexOf('/iframe_page') !== -1) {
+ event.respondWith(new Response(
+ '<!DOCTYPE html>\n' +
+ '<script>\n' +
+ 'function performSyncXHROnWorker(url) {\n' +
+ ' return new Promise((resolve) => {\n' +
+ ' var worker =\n' +
+ ' new Worker(\'./worker_script\');\n' +
+ ' worker.addEventListener(\'message\', (msg) => {\n' +
+ ' resolve(msg.data);\n' +
+ ' });\n' +
+ ' worker.postMessage({\n' +
+ ' url: url\n' +
+ ' });\n' +
+ ' });\n' +
+ '}\n' +
+ '</script>',
+ {
+ headers: [['content-type', 'text/html']]
+ }));
+ } else if (event.request.url.indexOf('/worker_script') !== -1) {
+ event.respondWith(new Response(
+ 'self.onmessage = (msg) => {' +
+ ' const syncXhr = new XMLHttpRequest();' +
+ ' syncXhr.open(\'GET\', msg.data.url, false);' +
+ ' syncXhr.send();' +
+ ' self.postMessage({' +
+ ' status: syncXhr.status,' +
+ ' responseText: syncXhr.responseText' +
+ ' });' +
+ '}',
+ {
+ headers: [['content-type', 'application/javascript']]
+ }));
+ }
+};
diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-request-xhr-sync-worker.js b/test/wpt/tests/service-workers/service-worker/resources/fetch-request-xhr-sync-worker.js
new file mode 100644
index 0000000..070e572
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-request-xhr-sync-worker.js
@@ -0,0 +1,7 @@
+'use strict';
+
+self.onfetch = function(event) {
+ if (event.request.url.indexOf('non-existent-file.txt') !== -1) {
+ event.respondWith(new Response('Response from service worker'));
+ }
+};
diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-request-xhr-worker.js b/test/wpt/tests/service-workers/service-worker/resources/fetch-request-xhr-worker.js
new file mode 100644
index 0000000..4e42837
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-request-xhr-worker.js
@@ -0,0 +1,22 @@
+self.addEventListener('fetch', function(event) {
+ var url = event.request.url;
+ if (url.indexOf('sample?test') == -1) {
+ return;
+ }
+ event.respondWith(new Promise(function(resolve) {
+ var headers = [];
+ for (var header of event.request.headers) {
+ headers.push(header);
+ }
+ event.request.text()
+ .then(function(result) {
+ resolve(new Response(JSON.stringify({
+ method: event.request.method,
+ mode: event.request.mode,
+ credentials: event.request.credentials,
+ headers: headers,
+ body: result
+ })));
+ });
+ }));
+ });
diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-response-taint-iframe.html b/test/wpt/tests/service-workers/service-worker/resources/fetch-response-taint-iframe.html
new file mode 100644
index 0000000..5f09efe
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-response-taint-iframe.html
@@ -0,0 +1,2 @@
+<!DOCTYPE html>
+<body></body>
diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-response-xhr-iframe.https.html b/test/wpt/tests/service-workers/service-worker/resources/fetch-response-xhr-iframe.https.html
new file mode 100644
index 0000000..c26eebe
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-response-xhr-iframe.https.html
@@ -0,0 +1,53 @@
+<script src="/common/get-host-info.sub.js"></script>
+<script src="test-helpers.sub.js?pipe=sub"></script>
+<script>
+var host_info = get_host_info();
+
+function xhr_send(method, data) {
+ return new Promise(function(resolve, reject) {
+ var xhr = new XMLHttpRequest();
+ xhr.onload = function() {
+ resolve(xhr);
+ };
+ xhr.onerror = function() {
+ reject('XHR should succeed.');
+ };
+ xhr.responseType = 'text';
+ xhr.open(method, './sample?test', true);
+ xhr.send(data);
+ });
+}
+
+function coalesce_headers_test() {
+ return xhr_send('POST', 'test string')
+ .then(function(xhr) {
+ window.parent.postMessage({results: xhr.getResponseHeader('foo')},
+ host_info['HTTPS_ORIGIN']);
+
+ return new Promise(function(resolve) {
+ window.addEventListener('message', function handle(evt) {
+ if (evt.data !== 'ACK') {
+ return;
+ }
+
+ window.removeEventListener('message', handle);
+ resolve();
+ });
+ });
+ });
+}
+
+window.addEventListener('message', function(evt) {
+ var port;
+
+ if (evt.data !== 'START') {
+ return;
+ }
+
+ port = evt.ports[0];
+
+ coalesce_headers_test()
+ .then(function() { port.postMessage({results: 'finish'}); })
+ .catch(function(e) { port.postMessage({results: 'failure:' + e}); });
+ });
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-response-xhr-worker.js b/test/wpt/tests/service-workers/service-worker/resources/fetch-response-xhr-worker.js
new file mode 100644
index 0000000..0301b12
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-response-xhr-worker.js
@@ -0,0 +1,12 @@
+self.addEventListener('fetch', function(event) {
+ var url = event.request.url;
+ if (url.indexOf('sample?test') == -1) {
+ return;
+ }
+ event.respondWith(new Promise(function(resolve) {
+ var headers = new Headers;
+ headers.append('foo', 'foo');
+ headers.append('foo', 'bar');
+ resolve(new Response('hello world', {'headers': headers}));
+ }));
+ });
diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-response.html b/test/wpt/tests/service-workers/service-worker/resources/fetch-response.html
new file mode 100644
index 0000000..6d27cf1
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-response.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+
+<script>
+ const params =new URLSearchParams(location.search);
+ const mode = params.get("mode") || "cors";
+ const path = params.get('path');
+ const bufferPromise =
+ new Promise(resolve =>
+ fetch(path, {mode})
+ .then(response => resolve(response.arrayBuffer()))
+ .catch(() => resolve(new Uint8Array())));
+
+ const entryPromise = new Promise(resolve => {
+ new PerformanceObserver(entries => {
+ const byName = entries.getEntriesByType("resource").find(e => e.name.includes(path));
+ if (byName)
+ resolve(byName);
+ }).observe({entryTypes: ["resource"]});
+ });
+
+ Promise.all([bufferPromise, entryPromise]).then(([buffer, entry]) => {
+ parent.postMessage({
+ buffer,
+ entry: entry.toJSON(),
+ }, '*');
+ });
+
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-response.js b/test/wpt/tests/service-workers/service-worker/resources/fetch-response.js
new file mode 100644
index 0000000..775efc0
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-response.js
@@ -0,0 +1,35 @@
+self.addEventListener('fetch', event => {
+ const path = event.request.url.match(/\/(?<name>[^\/]+)$/);
+ switch (path?.groups?.name) {
+ case 'constructed':
+ event.respondWith(new Response(new Uint8Array([1, 2, 3])));
+ break;
+ case 'forward':
+ event.respondWith(fetch('/common/text-plain.txt'));
+ break;
+ case 'stream':
+ event.respondWith((async() => {
+ const res = await fetch('/common/text-plain.txt');
+ const body = await res.body;
+ const reader = await body.getReader();
+ const stream = new ReadableStream({
+ async start(controller) {
+ while (true) {
+ const {done, value} = await reader.read();
+ if (done)
+ break;
+
+ controller.enqueue(value);
+ }
+ controller.close();
+ reader.releaseLock();
+ }
+ });
+ return new Response(stream);
+ })());
+ break;
+ default:
+ event.respondWith(fetch(event.request));
+ break;
+ }
+});
diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-rewrite-worker-referrer-policy.js b/test/wpt/tests/service-workers/service-worker/resources/fetch-rewrite-worker-referrer-policy.js
new file mode 100644
index 0000000..64c99c9
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-rewrite-worker-referrer-policy.js
@@ -0,0 +1,4 @@
+// This script is intended to be served with the `Referrer-Policy` header as
+// defined in the corresponding `.headers` file.
+
+importScripts('fetch-rewrite-worker.js');
diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-rewrite-worker-referrer-policy.js.headers b/test/wpt/tests/service-workers/service-worker/resources/fetch-rewrite-worker-referrer-policy.js.headers
new file mode 100644
index 0000000..5ae4265
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-rewrite-worker-referrer-policy.js.headers
@@ -0,0 +1,2 @@
+Content-Type: application/javascript
+Referrer-Policy: origin
diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-rewrite-worker.js b/test/wpt/tests/service-workers/service-worker/resources/fetch-rewrite-worker.js
new file mode 100644
index 0000000..20a8066
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-rewrite-worker.js
@@ -0,0 +1,166 @@
+// By default, this worker responds to fetch events with
+// respondWith(fetch(request)). Additionally, if the request has a &url
+// parameter, it fetches the provided URL instead. Because it forwards fetch
+// events to this other URL, it is called the "fetch rewrite" worker.
+//
+// The worker also looks for other params on the request to do more custom
+// behavior, like falling back to network or throwing an error.
+
+function get_query_params(url) {
+ var search = (new URL(url)).search;
+ if (!search) {
+ return {};
+ }
+ var ret = {};
+ var params = search.substring(1).split('&');
+ params.forEach(function(param) {
+ var element = param.split('=');
+ ret[decodeURIComponent(element[0])] = decodeURIComponent(element[1]);
+ });
+ return ret;
+}
+
+function get_request_init(base, params) {
+ var init = {};
+ init['method'] = params['method'] || base['method'];
+ init['mode'] = params['mode'] || base['mode'];
+ if (init['mode'] == 'navigate') {
+ init['mode'] = 'same-origin';
+ }
+ init['credentials'] = params['credentials'] || base['credentials'];
+ init['redirect'] = params['redirect-mode'] || base['redirect'];
+ return init;
+}
+
+self.addEventListener('fetch', function(event) {
+ var params = get_query_params(event.request.url);
+ var init = get_request_init(event.request, params);
+ var url = params['url'];
+ if (params['ignore']) {
+ return;
+ }
+ if (params['throw']) {
+ throw new Error('boom');
+ }
+ if (params['reject']) {
+ event.respondWith(new Promise(function(resolve, reject) {
+ reject();
+ }));
+ return;
+ }
+ if (params['resolve-null']) {
+ event.respondWith(new Promise(function(resolve) {
+ resolve(null);
+ }));
+ return;
+ }
+ if (params['generate-png']) {
+ var binary = atob(
+ 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAA' +
+ 'RnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAAhSURBVDhPY3wro/Kf' +
+ 'gQLABKXJBqMGjBoAAqMGDLwBDAwAEsoCTFWunmQAAAAASUVORK5CYII=');
+ var array = new Uint8Array(binary.length);
+ for(var i = 0; i < binary.length; i++) {
+ array[i] = binary.charCodeAt(i);
+ };
+ event.respondWith(new Response(new Blob([array], {type: 'image/png'})));
+ return;
+ }
+ if (params['check-ua-header']) {
+ var ua = event.request.headers.get('User-Agent');
+ if (ua) {
+ // We have a user agent!
+ event.respondWith(new Response(new Blob([ua])));
+ } else {
+ // We don't have a user-agent!
+ event.respondWith(new Response(new Blob(["NO_UA"])));
+ }
+ return;
+ }
+ if (params['check-accept-header']) {
+ var accept = event.request.headers.get('Accept');
+ if (accept) {
+ event.respondWith(new Response(accept));
+ } else {
+ event.respondWith(new Response('NO_ACCEPT'));
+ }
+ return;
+ }
+ event.respondWith(new Promise(function(resolve, reject) {
+ var request = event.request;
+ if (url) {
+ request = new Request(url, init);
+ } else if (params['change-request']) {
+ request = new Request(request, init);
+ }
+ const response_promise = params['navpreload'] ? event.preloadResponse
+ : fetch(request);
+ response_promise.then(function(response) {
+ var expectedType = params['expected_type'];
+ if (expectedType && response.type !== expectedType) {
+ // Resolve a JSON object with a failure instead of rejecting
+ // in order to distinguish this from a NetworkError, which
+ // may be expected even if the type is correct.
+ resolve(new Response(JSON.stringify({
+ result: 'failure',
+ detail: 'got ' + response.type + ' Response.type instead of ' +
+ expectedType
+ })));
+ }
+
+ var expectedRedirected = params['expected_redirected'];
+ if (typeof expectedRedirected !== 'undefined') {
+ var expected_redirected = (expectedRedirected === 'true');
+ if(response.redirected !== expected_redirected) {
+ // This is simply determining how to pass an error to the outer
+ // test case(fetch-request-redirect.https.html).
+ var execptedResolves = params['expected_resolves'];
+ if (execptedResolves === 'true') {
+ // Reject a JSON object with a failure since promise is expected
+ // to be resolved.
+ reject(new Response(JSON.stringify({
+ result: 'failure',
+ detail: 'got '+ response.redirected +
+ ' Response.redirected instead of ' +
+ expectedRedirected
+ })));
+ } else {
+ // Resolve a JSON object with a failure since promise is
+ // expected to be rejected.
+ resolve(new Response(JSON.stringify({
+ result: 'failure',
+ detail: 'got '+ response.redirected +
+ ' Response.redirected instead of ' +
+ expectedRedirected
+ })));
+ }
+ }
+ }
+
+ if (params['clone']) {
+ response = response.clone();
+ }
+
+ // |cache| means to bounce responses through Cache Storage and back.
+ if (params['cache']) {
+ var cacheName = "cached-fetches-" + performance.now() + "-" +
+ event.request.url;
+ var cache;
+ var cachedResponse;
+ return self.caches.open(cacheName).then(function(opened) {
+ cache = opened;
+ return cache.put(request, response);
+ }).then(function() {
+ return cache.match(request);
+ }).then(function(cached) {
+ cachedResponse = cached;
+ return self.caches.delete(cacheName);
+ }).then(function() {
+ resolve(cachedResponse);
+ });
+ } else {
+ resolve(response);
+ }
+ }, reject)
+ }));
+ });
diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-rewrite-worker.js.headers b/test/wpt/tests/service-workers/service-worker/resources/fetch-rewrite-worker.js.headers
new file mode 100644
index 0000000..123053b
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-rewrite-worker.js.headers
@@ -0,0 +1,2 @@
+Content-Type: text/javascript
+Service-Worker-Allowed: /
diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-variants-worker.js b/test/wpt/tests/service-workers/service-worker/resources/fetch-variants-worker.js
new file mode 100644
index 0000000..b950b9a
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-variants-worker.js
@@ -0,0 +1,35 @@
+importScripts('/common/get-host-info.sub.js');
+importScripts('test-helpers.sub.js');
+importScripts('/resources/testharness.js');
+
+const storedResponse = new Response(new Blob(['a simple text file']))
+const absolultePath = `${base_path()}/simple.txt`
+
+self.addEventListener('fetch', event => {
+ const search = new URLSearchParams(new URL(event.request.url).search.substr(1))
+ const variant = search.get('variant')
+ const delay = search.get('delay')
+ if (!variant)
+ return
+
+ switch (variant) {
+ case 'forward':
+ event.respondWith(fetch(event.request.url))
+ break
+ case 'redirect':
+ event.respondWith(fetch(`/xhr/resources/redirect.py?location=${base_path()}/simple.txt`))
+ break
+ case 'delay-before-fetch':
+ event.respondWith(
+ new Promise(resolve => {
+ step_timeout(() => fetch(event.request.url).then(resolve), delay)
+ }))
+ break
+ case 'delay-after-fetch':
+ event.respondWith(new Promise(resolve => {
+ fetch(event.request.url)
+ .then(response => step_timeout(() => resolve(response), delay))
+ }))
+ break
+ }
+});
diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-waits-for-activate-worker.js b/test/wpt/tests/service-workers/service-worker/resources/fetch-waits-for-activate-worker.js
new file mode 100644
index 0000000..92a96ff
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-waits-for-activate-worker.js
@@ -0,0 +1,31 @@
+var activatePromiseResolve;
+
+addEventListener('activate', function(evt) {
+ evt.waitUntil(new Promise(function(resolve) {
+ activatePromiseResolve = resolve;
+ }));
+});
+
+addEventListener('message', async function(evt) {
+ switch (evt.data) {
+ case 'CLAIM':
+ evt.waitUntil(new Promise(async resolve => {
+ await clients.claim();
+ evt.source.postMessage('CLAIMED');
+ resolve();
+ }));
+ break;
+ case 'ACTIVATE':
+ if (typeof activatePromiseResolve !== 'function') {
+ throw new Error('Not activating!');
+ }
+ activatePromiseResolve();
+ break;
+ default:
+ throw new Error('Unknown message!');
+ }
+});
+
+addEventListener('fetch', function(evt) {
+ evt.respondWith(new Response('Hello world'));
+});
diff --git a/test/wpt/tests/service-workers/service-worker/resources/form-poster.html b/test/wpt/tests/service-workers/service-worker/resources/form-poster.html
new file mode 100644
index 0000000..cd11a30
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/form-poster.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<meta charset="utf-8"/>
+<meta name="referrer" content="origin">
+<form method="POST" id="form"></form>
+<script>
+function onLoad() {
+ const params = new URLSearchParams(self.location.search);
+ const form = document.getElementById('form');
+ form.action = params.get('target');
+ form.submit();
+}
+self.addEventListener('load', onLoad);
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/resources/frame-for-getregistrations.html b/test/wpt/tests/service-workers/service-worker/resources/frame-for-getregistrations.html
new file mode 100644
index 0000000..7fc35f1
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/frame-for-getregistrations.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<title>Service Worker: frame for getRegistrations()</title>
+<script>
+var scope = 'scope-for-getregistrations';
+var script = 'empty-worker.js';
+var registration;
+
+navigator.serviceWorker.register(script, { scope: scope })
+ .then(function(r) { registration = r; window.parent.postMessage('ready', '*'); })
+
+self.onmessage = function(e) {
+ if (e.data == 'unregister') {
+ registration.unregister()
+ .then(function() {
+ e.ports[0].postMessage('unregistered');
+ });
+ }
+};
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/resources/get-resultingClientId-worker.js b/test/wpt/tests/service-workers/service-worker/resources/get-resultingClientId-worker.js
new file mode 100644
index 0000000..f0e6c7b
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/get-resultingClientId-worker.js
@@ -0,0 +1,107 @@
+// This worker expects a fetch event for a navigation and messages back the
+// result of clients.get(event.resultingClientId).
+
+// Resolves when the test finishes.
+let testFinishPromise;
+let resolveTestFinishPromise;
+let rejectTestFinishPromise;
+
+// Resolves to clients.get(event.resultingClientId) from the fetch event.
+let getPromise;
+let resolveGetPromise;
+let rejectGetPromise;
+
+let resultingClientId;
+
+function startTest() {
+ testFinishPromise = new Promise((resolve, reject) => {
+ resolveTestFinishPromise = resolve;
+ rejectTestFinishPromise = reject;
+ });
+
+ getPromise = new Promise((resolve, reject) => {
+ resolveGetPromise = resolve;
+ rejectGetPromise = reject;
+ });
+}
+
+async function describeGetPromiseResult(promise) {
+ const result = {};
+
+ await promise.then(
+ (client) => {
+ result.promiseState = 'fulfilled';
+ if (client === undefined) {
+ result.promiseValue = 'undefinedValue';
+ } else if (client instanceof Client) {
+ result.promiseValue = 'client';
+ result.client = {
+ id: client.id,
+ url: client.url
+ };
+ } else {
+ result.promiseValue = 'unknown';
+ }
+ },
+ (error) => {
+ result.promiseState = 'rejected';
+ });
+
+ return result;
+}
+
+async function handleGetResultingClient(event) {
+ // Note that this message can arrive before |resultingClientId| is populated.
+ const result = await describeGetPromiseResult(getPromise);
+ // |resultingClientId| must be populated by now.
+ result.queriedId = resultingClientId;
+ event.source.postMessage(result);
+};
+
+async function handleGetClient(event) {
+ const id = event.data.id;
+ const result = await describeGetPromiseResult(self.clients.get(id));
+ result.queriedId = id;
+ event.source.postMessage(result);
+};
+
+self.addEventListener('message', (event) => {
+ if (event.data.command == 'startTest') {
+ startTest();
+ event.waitUntil(testFinishPromise);
+ event.source.postMessage('ok');
+ return;
+ }
+
+ if (event.data.command == 'finishTest') {
+ resolveTestFinishPromise();
+ event.source.postMessage('ok');
+ return;
+ }
+
+ if (event.data.command == 'getResultingClient') {
+ event.waitUntil(handleGetResultingClient(event));
+ return;
+ }
+
+ if (event.data.command == 'getClient') {
+ event.waitUntil(handleGetClient(event));
+ return;
+ }
+});
+
+async function handleFetch(event) {
+ try {
+ resultingClientId = event.resultingClientId;
+ const client = await self.clients.get(resultingClientId);
+ resolveGetPromise(client);
+ } catch (error) {
+ rejectGetPromise(error);
+ }
+}
+
+self.addEventListener('fetch', (event) => {
+ if (event.request.mode != 'navigate')
+ return;
+ event.waitUntil(handleFetch(event));
+});
diff --git a/test/wpt/tests/service-workers/service-worker/resources/http-to-https-redirect-and-register-iframe.html b/test/wpt/tests/service-workers/service-worker/resources/http-to-https-redirect-and-register-iframe.html
new file mode 100644
index 0000000..bcab353
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/http-to-https-redirect-and-register-iframe.html
@@ -0,0 +1,25 @@
+<!doctype html>
+<title>register, unregister, and report result to opener</title>
+<body>
+<script>
+'use strict';
+
+if (!navigator.serviceWorker) {
+ window.opener.postMessage('FAIL: navigator.serviceWorker is undefined', '*');
+} else {
+ navigator.serviceWorker.register('empty-worker.js', {scope: 'scope-register'})
+ .then(
+ registration => {
+ registration.unregister().then(() => {
+ window.opener.postMessage('OK', '*');
+ });
+ },
+ error => {
+ window.opener.postMessage('FAIL: ' + error.name, '*');
+ })
+ .catch(error => {
+ window.opener.postMessage('ERROR: ' + error.name, '*');
+ });
+}
+</script>
+</body>
diff --git a/test/wpt/tests/service-workers/service-worker/resources/iframe-with-fetch-variants.html b/test/wpt/tests/service-workers/service-worker/resources/iframe-with-fetch-variants.html
new file mode 100644
index 0000000..3a61d7b
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/iframe-with-fetch-variants.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<meta charset="utf-8" />
+<script>
+ const url = new URL(new URLSearchParams(location.search.substr(1)).get('url'), location.href);
+ const before = performance.now();
+ fetch(url)
+ .then(r => r.text())
+ .then(() =>
+ parent.postMessage({
+ before,
+ after: performance.now(),
+ entry: performance.getEntriesByName(url)[0].toJSON()
+ }));
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/resources/iframe-with-image.html b/test/wpt/tests/service-workers/service-worker/resources/iframe-with-image.html
new file mode 100644
index 0000000..ce78840
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/iframe-with-image.html
@@ -0,0 +1,2 @@
+<!DOCTYPE html>
+<img src="square">
diff --git a/test/wpt/tests/service-workers/service-worker/resources/immutable-prototype-serviceworker.js b/test/wpt/tests/service-workers/service-worker/resources/immutable-prototype-serviceworker.js
new file mode 100644
index 0000000..d8a94ad
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/immutable-prototype-serviceworker.js
@@ -0,0 +1,19 @@
+function prototypeChain(global) {
+ let result = [];
+ while (global !== null) {
+ let thrown = false;
+ let next = Object.getPrototypeOf(global);
+ try {
+ Object.setPrototypeOf(global, {});
+ result.push('mutable');
+ } catch (e) {
+ result.push('immutable');
+ }
+ global = next;
+ }
+ return result;
+}
+
+self.onmessage = function(e) {
+ e.data.postMessage(prototypeChain(self));
+};
diff --git a/test/wpt/tests/service-workers/service-worker/resources/import-echo-cookie-worker-module.py b/test/wpt/tests/service-workers/service-worker/resources/import-echo-cookie-worker-module.py
new file mode 100644
index 0000000..8f0b68e
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/import-echo-cookie-worker-module.py
@@ -0,0 +1,6 @@
+def main(request, response):
+ # This script generates a worker script for static imports from module
+ # service workers.
+ headers = [(b'Content-Type', b'text/javascript')]
+ body = b"import './echo-cookie-worker.py?key=%s'" % request.GET[b'key']
+ return headers, body
diff --git a/test/wpt/tests/service-workers/service-worker/resources/import-echo-cookie-worker.js b/test/wpt/tests/service-workers/service-worker/resources/import-echo-cookie-worker.js
new file mode 100644
index 0000000..f5eac95
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/import-echo-cookie-worker.js
@@ -0,0 +1 @@
+importScripts(`echo-cookie-worker.py${location.search}`);
diff --git a/test/wpt/tests/service-workers/service-worker/resources/import-mime-type-worker.py b/test/wpt/tests/service-workers/service-worker/resources/import-mime-type-worker.py
new file mode 100644
index 0000000..b6e82f3
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/import-mime-type-worker.py
@@ -0,0 +1,10 @@
+def main(request, response):
+ if b'mime' in request.GET:
+ return (
+ [(b'Content-Type', b'application/javascript')],
+ b"importScripts('./mime-type-worker.py?mime=%s');" % request.GET[b'mime']
+ )
+ return (
+ [(b'Content-Type', b'application/javascript')],
+ b"importScripts('./mime-type-worker.py');"
+ )
diff --git a/test/wpt/tests/service-workers/service-worker/resources/import-relative.xsl b/test/wpt/tests/service-workers/service-worker/resources/import-relative.xsl
new file mode 100644
index 0000000..063a62d
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/import-relative.xsl
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
+ <xsl:import href="xslt-pass.xsl"/>
+</xsl:stylesheet>
diff --git a/test/wpt/tests/service-workers/service-worker/resources/import-scripts-404-after-update-plus-update-worker.js b/test/wpt/tests/service-workers/service-worker/resources/import-scripts-404-after-update-plus-update-worker.js
new file mode 100644
index 0000000..e9899d8
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/import-scripts-404-after-update-plus-update-worker.js
@@ -0,0 +1,8 @@
+// This worker imports a script that returns 200 on the first request and 404
+// on the second request, and a script that is updated every time when
+// requesting it.
+const params = new URLSearchParams(location.search);
+const key = params.get('Key');
+const additional_key = params.get('AdditionalKey');
+importScripts(`update-worker.py?Key=${key}&Mode=not_found`,
+ `update-worker.py?Key=${additional_key}&Mode=normal`);
diff --git a/test/wpt/tests/service-workers/service-worker/resources/import-scripts-404-after-update.js b/test/wpt/tests/service-workers/service-worker/resources/import-scripts-404-after-update.js
new file mode 100644
index 0000000..b569346
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/import-scripts-404-after-update.js
@@ -0,0 +1,6 @@
+// This worker imports a script that returns 200 on the first request and 404
+// on the second request. The resulting body also changes each time it is
+// requested.
+const params = new URLSearchParams(location.search);
+const key = params.get('Key');
+importScripts(`update-worker.py?Key=${key}&Mode=not_found`);
diff --git a/test/wpt/tests/service-workers/service-worker/resources/import-scripts-404.js b/test/wpt/tests/service-workers/service-worker/resources/import-scripts-404.js
new file mode 100644
index 0000000..19c7a4b
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/import-scripts-404.js
@@ -0,0 +1 @@
+importScripts('404.py');
diff --git a/test/wpt/tests/service-workers/service-worker/resources/import-scripts-cross-origin-worker.sub.js b/test/wpt/tests/service-workers/service-worker/resources/import-scripts-cross-origin-worker.sub.js
new file mode 100644
index 0000000..b432854
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/import-scripts-cross-origin-worker.sub.js
@@ -0,0 +1 @@
+importScripts('https://{{domains[www1]}}:{{ports[https][0]}}/service-workers/service-worker/resources/import-scripts-version.py');
diff --git a/test/wpt/tests/service-workers/service-worker/resources/import-scripts-data-url-worker.js b/test/wpt/tests/service-workers/service-worker/resources/import-scripts-data-url-worker.js
new file mode 100644
index 0000000..fdabdaf
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/import-scripts-data-url-worker.js
@@ -0,0 +1 @@
+importScripts('data:text/javascript,');
diff --git a/test/wpt/tests/service-workers/service-worker/resources/import-scripts-diff-resource-map-worker.js b/test/wpt/tests/service-workers/service-worker/resources/import-scripts-diff-resource-map-worker.js
new file mode 100644
index 0000000..0fdcb0f
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/import-scripts-diff-resource-map-worker.js
@@ -0,0 +1,10 @@
+importScripts('/resources/testharness.js');
+
+let echo1 = null;
+let echo2 = null;
+let arg1 = 'import-scripts-get.py?output=echo1&msg=test1';
+let arg2 = 'import-scripts-get.py?output=echo2&msg=test2';
+
+importScripts(arg1, arg2);
+assert_equals(echo1, 'test1');
+assert_equals(echo2, 'test2');
diff --git a/test/wpt/tests/service-workers/service-worker/resources/import-scripts-echo.py b/test/wpt/tests/service-workers/service-worker/resources/import-scripts-echo.py
new file mode 100644
index 0000000..d38d660
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/import-scripts-echo.py
@@ -0,0 +1,6 @@
+def main(req, res):
+ return ([
+ (b'Cache-Control', b'no-cache, must-revalidate'),
+ (b'Pragma', b'no-cache'),
+ (b'Content-Type', b'application/javascript')],
+ b'echo_output = "%s";\n' % req.GET[b'msg'])
diff --git a/test/wpt/tests/service-workers/service-worker/resources/import-scripts-get.py b/test/wpt/tests/service-workers/service-worker/resources/import-scripts-get.py
new file mode 100644
index 0000000..ab7b84e
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/import-scripts-get.py
@@ -0,0 +1,6 @@
+def main(req, res):
+ return ([
+ (b'Cache-Control', b'no-cache, must-revalidate'),
+ (b'Pragma', b'no-cache'),
+ (b'Content-Type', b'application/javascript')],
+ b'%s = "%s";\n' % (req.GET[b'output'], req.GET[b'msg']))
diff --git a/test/wpt/tests/service-workers/service-worker/resources/import-scripts-mime-types-worker.js b/test/wpt/tests/service-workers/service-worker/resources/import-scripts-mime-types-worker.js
new file mode 100644
index 0000000..d4f1f3e
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/import-scripts-mime-types-worker.js
@@ -0,0 +1,49 @@
+const badMimeTypes = [
+ null, // no MIME type
+ 'text/plain',
+];
+
+const validMimeTypes = [
+ 'application/ecmascript',
+ 'application/javascript',
+ 'application/x-ecmascript',
+ 'application/x-javascript',
+ 'text/ecmascript',
+ 'text/javascript',
+ 'text/javascript1.0',
+ 'text/javascript1.1',
+ 'text/javascript1.2',
+ 'text/javascript1.3',
+ 'text/javascript1.4',
+ 'text/javascript1.5',
+ 'text/jscript',
+ 'text/livescript',
+ 'text/x-ecmascript',
+ 'text/x-javascript',
+];
+
+function importScriptsWithMimeType(mimeType) {
+ importScripts(`./mime-type-worker.py${mimeType ? '?mime=' + mimeType : ''}`);
+}
+
+importScripts('/resources/testharness.js');
+
+for (const mimeType of badMimeTypes) {
+ test(() => {
+ assert_throws_dom(
+ 'NetworkError',
+ () => { importScriptsWithMimeType(mimeType); },
+ `importScripts with ${mimeType ? 'bad' : 'no'} MIME type ${mimeType || ''} throws NetworkError`,
+ );
+ }, `Importing script with ${mimeType ? 'bad' : 'no'} MIME type ${mimeType || ''}`);
+}
+
+for (const mimeType of validMimeTypes) {
+ test(() => {
+ try {
+ importScriptsWithMimeType(mimeType);
+ } catch {
+ assert_unreached(`importScripts with MIME type ${mimeType} should not throw`);
+ }
+ }, `Importing script with valid JavaScript MIME type ${mimeType}`);
+}
diff --git a/test/wpt/tests/service-workers/service-worker/resources/import-scripts-redirect-import.js b/test/wpt/tests/service-workers/service-worker/resources/import-scripts-redirect-import.js
new file mode 100644
index 0000000..56c04f0
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/import-scripts-redirect-import.js
@@ -0,0 +1 @@
+// empty script
diff --git a/test/wpt/tests/service-workers/service-worker/resources/import-scripts-redirect-on-second-time-worker.js b/test/wpt/tests/service-workers/service-worker/resources/import-scripts-redirect-on-second-time-worker.js
new file mode 100644
index 0000000..f612ab8
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/import-scripts-redirect-on-second-time-worker.js
@@ -0,0 +1,7 @@
+// This worker imports a script that returns 200 on the first request and a
+// redirect on the second request. The resulting body also changes each time it
+// is requested.
+const params = new URLSearchParams(location.search);
+const key = params.get('Key');
+importScripts(`update-worker.py?Key=${key}&Mode=redirect&` +
+ `Redirect=update-worker.py?Key=${key}%26Mode=normal`);
diff --git a/test/wpt/tests/service-workers/service-worker/resources/import-scripts-redirect-worker.js b/test/wpt/tests/service-workers/service-worker/resources/import-scripts-redirect-worker.js
new file mode 100644
index 0000000..d02a453
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/import-scripts-redirect-worker.js
@@ -0,0 +1 @@
+importScripts('redirect.py?Redirect=import-scripts-version.py');
diff --git a/test/wpt/tests/service-workers/service-worker/resources/import-scripts-resource-map-worker.js b/test/wpt/tests/service-workers/service-worker/resources/import-scripts-resource-map-worker.js
new file mode 100644
index 0000000..b3b9bc4
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/import-scripts-resource-map-worker.js
@@ -0,0 +1,15 @@
+importScripts('/resources/testharness.js');
+
+let version = null;
+importScripts('import-scripts-version.py');
+// Once imported, the stored script should be loaded for subsequent importScripts.
+const expected_version = version;
+
+version = null;
+importScripts('import-scripts-version.py');
+assert_equals(expected_version, version, 'second import');
+
+version = null;
+importScripts('import-scripts-version.py', 'import-scripts-version.py',
+ 'import-scripts-version.py');
+assert_equals(expected_version, version, 'multiple imports');
diff --git a/test/wpt/tests/service-workers/service-worker/resources/import-scripts-updated-flag-worker.js b/test/wpt/tests/service-workers/service-worker/resources/import-scripts-updated-flag-worker.js
new file mode 100644
index 0000000..e016646
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/import-scripts-updated-flag-worker.js
@@ -0,0 +1,31 @@
+importScripts('/resources/testharness.js');
+
+let echo_output = null;
+
+// Tests importing a script that sets |echo_output| to the query string.
+function test_import(str) {
+ echo_output = null;
+ importScripts('import-scripts-echo.py?msg=' + str);
+ assert_equals(echo_output, str);
+}
+
+test_import('root');
+test_import('root-and-message');
+
+self.addEventListener('install', () => {
+ test_import('install');
+ test_import('install-and-message');
+ });
+
+self.addEventListener('message', e => {
+ var error = null;
+ echo_output = null;
+
+ try {
+ importScripts('import-scripts-echo.py?msg=' + e.data);
+ } catch (e) {
+ error = e && e.name;
+ }
+
+ e.source.postMessage({ error: error, value: echo_output });
+ });
diff --git a/test/wpt/tests/service-workers/service-worker/resources/import-scripts-version.py b/test/wpt/tests/service-workers/service-worker/resources/import-scripts-version.py
new file mode 100644
index 0000000..cde2854
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/import-scripts-version.py
@@ -0,0 +1,17 @@
+import datetime
+import time
+
+epoch = datetime.datetime(1970, 1, 1)
+
+def main(req, res):
+ # Artificially delay response time in order to ensure uniqueness of
+ # computed value
+ time.sleep(0.1)
+
+ now = (datetime.datetime.now() - epoch).total_seconds()
+
+ return ([
+ (b'Cache-Control', b'no-cache, must-revalidate'),
+ (b'Pragma', b'no-cache'),
+ (b'Content-Type', b'application/javascript')],
+ u'version = "%s";\n' % now)
diff --git a/test/wpt/tests/service-workers/service-worker/resources/imported-classic-script.js b/test/wpt/tests/service-workers/service-worker/resources/imported-classic-script.js
new file mode 100644
index 0000000..5fc5204
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/imported-classic-script.js
@@ -0,0 +1 @@
+const imported = 'A classic script.';
diff --git a/test/wpt/tests/service-workers/service-worker/resources/imported-module-script.js b/test/wpt/tests/service-workers/service-worker/resources/imported-module-script.js
new file mode 100644
index 0000000..56d196d
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/imported-module-script.js
@@ -0,0 +1 @@
+export const imported = 'A module script.';
diff --git a/test/wpt/tests/service-workers/service-worker/resources/indexeddb-worker.js b/test/wpt/tests/service-workers/service-worker/resources/indexeddb-worker.js
new file mode 100644
index 0000000..9add476
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/indexeddb-worker.js
@@ -0,0 +1,57 @@
+self.addEventListener('message', function(e) {
+ var message = e.data;
+ if (message.action === 'create') {
+ e.waitUntil(deleteDB()
+ .then(doIndexedDBTest)
+ .then(function() {
+ message.port.postMessage({ type: 'created' });
+ })
+ .catch(function(reason) {
+ message.port.postMessage({ type: 'error', value: reason });
+ }));
+ } else if (message.action === 'cleanup') {
+ e.waitUntil(deleteDB()
+ .then(function() {
+ message.port.postMessage({ type: 'done' });
+ })
+ .catch(function(reason) {
+ message.port.postMessage({ type: 'error', value: reason });
+ }));
+ }
+ });
+
+function deleteDB() {
+ return new Promise(function(resolve, reject) {
+ var delete_request = indexedDB.deleteDatabase('db');
+
+ delete_request.onsuccess = resolve;
+ delete_request.onerror = reject;
+ });
+}
+
+function doIndexedDBTest(port) {
+ return new Promise(function(resolve, reject) {
+ var open_request = indexedDB.open('db');
+
+ open_request.onerror = reject;
+ open_request.onupgradeneeded = function() {
+ var db = open_request.result;
+ db.createObjectStore('store');
+ };
+ open_request.onsuccess = function() {
+ var db = open_request.result;
+ var tx = db.transaction('store', 'readwrite');
+ var store = tx.objectStore('store');
+ store.put('value', 'key');
+
+ tx.onerror = function() {
+ db.close();
+ reject(tx.error);
+ };
+ tx.oncomplete = function() {
+ db.close();
+ resolve();
+ };
+ };
+ });
+}
diff --git a/test/wpt/tests/service-workers/service-worker/resources/install-event-type-worker.js b/test/wpt/tests/service-workers/service-worker/resources/install-event-type-worker.js
new file mode 100644
index 0000000..1c94ae2
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/install-event-type-worker.js
@@ -0,0 +1,9 @@
+importScripts('worker-testharness.js');
+
+self.oninstall = function(event) {
+ assert_true(event instanceof ExtendableEvent, 'instance of ExtendableEvent');
+ assert_true(event instanceof InstallEvent, 'instance of InstallEvent');
+ assert_equals(event.type, 'install', '`type` property value');
+ assert_false(event.cancelable, '`cancelable` property value');
+ assert_false(event.bubbles, '`bubbles` property value');
+};
diff --git a/test/wpt/tests/service-workers/service-worker/resources/install-worker.html b/test/wpt/tests/service-workers/service-worker/resources/install-worker.html
new file mode 100644
index 0000000..ed20cd4
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/install-worker.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<html>
+<body>
+<p>Loading...</p>
+<script>
+async function install() {
+ let script;
+ for (const q of location.search.slice(1).split('&')) {
+ if (q.split('=')[0] === 'script') {
+ script = q.split('=')[1];
+ }
+ }
+ const scope = location.href;
+ const reg = await navigator.serviceWorker.register(script, {scope});
+ await navigator.serviceWorker.ready;
+ location.reload();
+}
+
+install();
+</script>
+</body>
+</html>
diff --git a/test/wpt/tests/service-workers/service-worker/resources/interface-requirements-worker.sub.js b/test/wpt/tests/service-workers/service-worker/resources/interface-requirements-worker.sub.js
new file mode 100644
index 0000000..a3f239b
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/interface-requirements-worker.sub.js
@@ -0,0 +1,59 @@
+'use strict';
+
+// This file checks additional interface requirements, on top of the basic IDL
+// that is validated in service-workers/idlharness.any.js
+
+importScripts('/resources/testharness.js');
+
+test(function() {
+ var req = new Request('http://{{host}}/',
+ {method: 'POST',
+ headers: [['Content-Type', 'Text/Html']]});
+ assert_equals(
+ new ExtendableEvent('ExtendableEvent').type,
+ 'ExtendableEvent', 'Type of ExtendableEvent should be ExtendableEvent');
+ assert_throws_js(TypeError, function() {
+ new FetchEvent('FetchEvent');
+ }, 'FetchEvent constructor with one argument throws');
+ assert_throws_js(TypeError, function() {
+ new FetchEvent('FetchEvent', {});
+ }, 'FetchEvent constructor with empty init dict throws');
+ assert_throws_js(TypeError, function() {
+ new FetchEvent('FetchEvent', {request: null});
+ }, 'FetchEvent constructor with null request member throws');
+ assert_equals(
+ new FetchEvent('FetchEvent', {request: req}).type,
+ 'FetchEvent', 'Type of FetchEvent should be FetchEvent');
+ assert_equals(
+ new FetchEvent('FetchEvent', {request: req}).cancelable,
+ false, 'Default FetchEvent.cancelable should be false');
+ assert_equals(
+ new FetchEvent('FetchEvent', {request: req}).bubbles,
+ false, 'Default FetchEvent.bubbles should be false');
+ assert_equals(
+ new FetchEvent('FetchEvent', {request: req}).clientId,
+ '', 'Default FetchEvent.clientId should be the empty string');
+ assert_equals(
+ new FetchEvent('FetchEvent', {request: req, cancelable: false}).cancelable,
+ false, 'FetchEvent.cancelable should be false');
+ assert_equals(
+ new FetchEvent('FetchEvent', {request: req, clientId : 'test-client-id'}).clientId, 'test-client-id',
+ 'FetchEvent.clientId with option {clientId : "test-client-id"} should be "test-client-id"');
+ assert_equals(
+ new FetchEvent('FetchEvent', {request : req}).request.url,
+ 'http://{{host}}/',
+ 'FetchEvent.request.url should return the value it was initialized to');
+ assert_equals(
+ new FetchEvent('FetchEvent', {request : req}).isReload,
+ undefined,
+ 'FetchEvent.isReload should not exist');
+
+ }, 'Event constructors');
+
+test(() => {
+ assert_false('XMLHttpRequest' in self);
+ }, 'xhr is not exposed');
+
+test(() => {
+ assert_false('createObjectURL' in self.URL);
+ }, 'URL.createObjectURL is not exposed')
diff --git a/test/wpt/tests/service-workers/service-worker/resources/invalid-blobtype-iframe.https.html b/test/wpt/tests/service-workers/service-worker/resources/invalid-blobtype-iframe.https.html
new file mode 100644
index 0000000..04a9cb5
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/invalid-blobtype-iframe.https.html
@@ -0,0 +1,28 @@
+<script src="test-helpers.sub.js"></script>
+<script>
+
+function xhr_send(method, data) {
+ return new Promise(function(resolve, reject) {
+ var xhr = new XMLHttpRequest();
+ xhr.onload = function() {
+ if (xhr.getResponseHeader('Content-Type') !== null) {
+ reject('Content-Type must be null.');
+ }
+ resolve();
+ };
+ xhr.onerror = function() {
+ reject('XHR must succeed.');
+ };
+ xhr.responseType = 'text';
+ xhr.open(method, './sample?test', true);
+ xhr.send(data);
+ });
+}
+
+window.addEventListener('message', function(evt) {
+ var port = evt.ports[0];
+ xhr_send('POST', 'test string')
+ .then(function() { port.postMessage({results: 'finish'}); })
+ .catch(function(e) { port.postMessage({results: 'failure:' + e}); });
+ });
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/resources/invalid-blobtype-worker.js b/test/wpt/tests/service-workers/service-worker/resources/invalid-blobtype-worker.js
new file mode 100644
index 0000000..865dc30
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/invalid-blobtype-worker.js
@@ -0,0 +1,10 @@
+self.addEventListener('fetch', function(event) {
+ var url = event.request.url;
+ if (url.indexOf('sample?test') == -1) {
+ return;
+ }
+ event.respondWith(new Promise(function(resolve) {
+ // null byte in blob type
+ resolve(new Response(new Blob([],{type: 'a\0b'})));
+ }));
+ });
diff --git a/test/wpt/tests/service-workers/service-worker/resources/invalid-chunked-encoding-with-flush.py b/test/wpt/tests/service-workers/service-worker/resources/invalid-chunked-encoding-with-flush.py
new file mode 100644
index 0000000..05977c6
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/invalid-chunked-encoding-with-flush.py
@@ -0,0 +1,9 @@
+import time
+def main(request, response):
+ response.headers.set(b"Content-Type", b"application/javascript")
+ response.headers.set(b"Transfer-encoding", b"chunked")
+ response.write_status_headers()
+
+ time.sleep(1)
+
+ response.writer.write(b"XX\r\n\r\n")
diff --git a/test/wpt/tests/service-workers/service-worker/resources/invalid-chunked-encoding.py b/test/wpt/tests/service-workers/service-worker/resources/invalid-chunked-encoding.py
new file mode 100644
index 0000000..a8edd06
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/invalid-chunked-encoding.py
@@ -0,0 +1,2 @@
+def main(request, response):
+ return [(b"Content-Type", b"application/javascript"), (b"Transfer-encoding", b"chunked")], b"XX\r\n\r\n"
diff --git a/test/wpt/tests/service-workers/service-worker/resources/invalid-header-iframe.https.html b/test/wpt/tests/service-workers/service-worker/resources/invalid-header-iframe.https.html
new file mode 100644
index 0000000..8f0e6ba
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/invalid-header-iframe.https.html
@@ -0,0 +1,25 @@
+<script src="test-helpers.sub.js"></script>
+<script>
+
+function xhr_send(method, data) {
+ return new Promise(function(resolve, reject) {
+ var xhr = new XMLHttpRequest();
+ xhr.onload = function() {
+ reject('XHR must fail.');
+ };
+ xhr.onerror = function() {
+ resolve();
+ };
+ xhr.responseType = 'text';
+ xhr.open(method, './sample?test', true);
+ xhr.send(data);
+ });
+}
+
+window.addEventListener('message', function(evt) {
+ var port = evt.ports[0];
+ xhr_send('POST', 'test string')
+ .then(function() { port.postMessage({results: 'finish'}); })
+ .catch(function(e) { port.postMessage({results: 'failure:' + e}); });
+ });
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/resources/invalid-header-worker.js b/test/wpt/tests/service-workers/service-worker/resources/invalid-header-worker.js
new file mode 100644
index 0000000..850874b
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/invalid-header-worker.js
@@ -0,0 +1,12 @@
+self.addEventListener('fetch', function(event) {
+ var url = event.request.url;
+ if (url.indexOf('sample?test') == -1) {
+ return;
+ }
+ event.respondWith(new Promise(function(resolve) {
+ var headers = new Headers;
+ headers.append('foo', 'foo');
+ headers.append('foo', 'b\0r'); // header value with a null byte
+ resolve(new Response('hello world', {'headers': headers}));
+ }));
+ });
diff --git a/test/wpt/tests/service-workers/service-worker/resources/iso-latin1-header-iframe.html b/test/wpt/tests/service-workers/service-worker/resources/iso-latin1-header-iframe.html
new file mode 100644
index 0000000..cf2fa8d
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/iso-latin1-header-iframe.html
@@ -0,0 +1,23 @@
+<script>
+function xhr_send(method, data) {
+ return new Promise(function(resolve, reject) {
+ var xhr = new XMLHttpRequest();
+ xhr.onload = function() {
+ resolve();
+ };
+ xhr.onerror = function() {
+ reject('XHR must succeed.');
+ };
+ xhr.responseType = 'text';
+ xhr.open(method, './sample?test', true);
+ xhr.send(data);
+ });
+}
+
+window.addEventListener('message', function(evt) {
+ var port = evt.ports[0];
+ xhr_send('POST', 'test string')
+ .then(function() { port.postMessage({results: 'finish'}); })
+ .catch(function(e) { port.postMessage({results: 'failure:' + e}); });
+ });
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/resources/iso-latin1-header-worker.js b/test/wpt/tests/service-workers/service-worker/resources/iso-latin1-header-worker.js
new file mode 100644
index 0000000..d9ecca2
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/iso-latin1-header-worker.js
@@ -0,0 +1,12 @@
+self.addEventListener('fetch', function(event) {
+ var url = event.request.url;
+ if (url.indexOf('sample?test') == -1) {
+ return;
+ }
+
+ event.respondWith(new Promise(function(resolve) {
+ var headers = new Headers;
+ headers.append('TEST', 'ßÀ¿'); // header value holds the Latin1 (ISO8859-1) string.
+ resolve(new Response('hello world', {'headers': headers}));
+ }));
+ });
diff --git a/test/wpt/tests/service-workers/service-worker/resources/load_worker.js b/test/wpt/tests/service-workers/service-worker/resources/load_worker.js
new file mode 100644
index 0000000..18c673b
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/load_worker.js
@@ -0,0 +1,29 @@
+function run_test(data, sender) {
+ if (data === 'xhr') {
+ const xhr = new XMLHttpRequest();
+ xhr.open('GET', 'synthesized-response.txt', true);
+ xhr.responseType = 'text';
+ xhr.send();
+ xhr.onload = evt => sender.postMessage(xhr.responseText);
+ xhr.onerror = () => sender.postMessage('XHR failed!');
+ } else if (data === 'fetch') {
+ fetch('synthesized-response.txt')
+ .then(response => response.text())
+ .then(data => sender.postMessage(data))
+ .catch(error => sender.postMessage('Fetch failed!'));
+ } else if (data === 'importScripts') {
+ importScripts('synthesized-response.js');
+ // |message| is provided by 'synthesized-response.js';
+ sender.postMessage(message);
+ } else {
+ sender.postMessage('Unexpected message! ' + data);
+ }
+}
+
+// Entry point for dedicated workers.
+self.onmessage = evt => run_test(evt.data, self);
+
+// Entry point for shared workers.
+self.onconnect = evt => {
+ evt.ports[0].onmessage = e => run_test(e.data, evt.ports[0]);
+};
diff --git a/test/wpt/tests/service-workers/service-worker/resources/loaded.html b/test/wpt/tests/service-workers/service-worker/resources/loaded.html
new file mode 100644
index 0000000..0cabce6
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/loaded.html
@@ -0,0 +1,9 @@
+<script>
+addEventListener('load', function() {
+ opener.postMessage({ type: 'LOADED' }, '*');
+});
+
+addEventListener('pageshow', function() {
+ opener.postMessage({ type: 'PAGESHOW' }, '*');
+});
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/resources/local-url-inherit-controller-frame.html b/test/wpt/tests/service-workers/service-worker/resources/local-url-inherit-controller-frame.html
new file mode 100644
index 0000000..b1e554d
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/local-url-inherit-controller-frame.html
@@ -0,0 +1,130 @@
+<!DOCTYPE html>
+<html>
+<script>
+
+const fetchURL = new URL('sample.js', window.location).href;
+
+const frameControllerText =
+`<script>
+ let t = null;
+ try {
+ if (navigator.serviceWorker.controller) {
+ t = navigator.serviceWorker.controller.scriptURL;
+ }
+ } catch (e) {
+ t = e.message;
+ } finally {
+ parent.postMessage({ data: t }, '*');
+ }
+</` + `script>`;
+
+const frameFetchText =
+`<script>
+ fetch('${fetchURL}', { mode: 'no-cors' }).then(response => {
+ return response.text();
+ }).then(text => {
+ parent.postMessage({ data: text }, '*');
+ }).catch(e => {
+ parent.postMessage({ data: e.message }, '*');
+ });
+</` + `script>`;
+
+const workerControllerText =
+`let t = navigator.serviceWorker.controller
+ ? navigator.serviceWorker.controller.scriptURL
+ : null;
+self.postMessage(t);`;
+
+const workerFetchText =
+`fetch('${fetchURL}', { mode: 'no-cors' }).then(response => {
+ return response.text();
+}).then(text => {
+ self.postMessage(text);
+}).catch(e => {
+ self.postMessage(e.message);
+});`
+
+function getChildText(opts) {
+ if (opts.child === 'iframe') {
+ if (opts.check === 'controller') {
+ return frameControllerText;
+ }
+
+ if (opts.check === 'fetch') {
+ return frameFetchText;
+ }
+
+ throw('unexpected feature to check: ' + opts.check);
+ }
+
+ if (opts.child === 'worker') {
+ if (opts.check === 'controller') {
+ return workerControllerText;
+ }
+
+ if (opts.check === 'fetch') {
+ return workerFetchText;
+ }
+
+ throw('unexpected feature to check: ' + opts.check);
+ }
+
+ throw('unexpected child type ' + opts.child);
+}
+
+function makeURL(opts) {
+ let mimetype = opts.child === 'iframe' ? 'text/html'
+ : 'text/javascript';
+
+ if (opts.scheme === 'blob') {
+ let blob = new Blob([getChildText(opts)], { type: mimetype });
+ return URL.createObjectURL(blob);
+ }
+
+ if (opts.scheme === 'data') {
+ return `data:${mimetype},${getChildText(opts)}`;
+ }
+
+ throw(`unexpected URL scheme ${opts.scheme}`);
+}
+
+function testWorkerChild(url) {
+ let w = new Worker(url);
+ return new Promise((resolve, reject) => {
+ w.onmessage = resolve;
+ w.onerror = evt => {
+ reject(evt.message);
+ }
+ });
+}
+
+function testIframeChild(url) {
+ let frame = document.createElement('iframe');
+ frame.src = url;
+ document.body.appendChild(frame);
+
+ return new Promise(resolve => {
+ addEventListener('message', evt => {
+ resolve(evt.data);
+ }, { once: true });
+ });
+}
+
+function testURL(opts, url) {
+ if (opts.child === 'worker') {
+ return testWorkerChild(url);
+ }
+
+ if (opts.child === 'iframe') {
+ return testIframeChild(url);
+ }
+
+ throw(`unexpected child type ${opts.child}`);
+}
+
+function checkChildController(opts) {
+ let url = makeURL(opts);
+ return testURL(opts, url);
+}
+</script>
+</html>
diff --git a/test/wpt/tests/service-workers/service-worker/resources/local-url-inherit-controller-worker.js b/test/wpt/tests/service-workers/service-worker/resources/local-url-inherit-controller-worker.js
new file mode 100644
index 0000000..4b7aad0
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/local-url-inherit-controller-worker.js
@@ -0,0 +1,5 @@
+addEventListener('fetch', evt => {
+ if (evt.request.url.includes('sample')) {
+ evt.respondWith(new Response('intercepted'));
+ }
+});
diff --git a/test/wpt/tests/service-workers/service-worker/resources/location-setter.html b/test/wpt/tests/service-workers/service-worker/resources/location-setter.html
new file mode 100644
index 0000000..f0ced06
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/location-setter.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<meta charset="utf-8"/>
+<meta name="referrer" content="origin">
+<script>
+function onLoad() {
+ const params = new URLSearchParams(self.location.search);
+ self.location = params.get('target');
+}
+self.addEventListener('load', onLoad);
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/resources/malformed-http-response.asis b/test/wpt/tests/service-workers/service-worker/resources/malformed-http-response.asis
new file mode 100644
index 0000000..bc3c68d
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/malformed-http-response.asis
@@ -0,0 +1 @@
+HAHAHA THIS IS NOT HTTP AND THE BROWSER SHOULD CONSIDER IT A NETWORK ERROR
diff --git a/test/wpt/tests/service-workers/service-worker/resources/malformed-worker.py b/test/wpt/tests/service-workers/service-worker/resources/malformed-worker.py
new file mode 100644
index 0000000..319b6e2
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/malformed-worker.py
@@ -0,0 +1,14 @@
+def main(request, response):
+ headers = [(b"Content-Type", b"application/javascript")]
+
+ body = {u'parse-error': u'var foo = function() {;',
+ u'undefined-error': u'foo.bar = 42;',
+ u'uncaught-exception': u'throw new DOMException("AbortError");',
+ u'caught-exception': u'try { throw new Error; } catch(e) {}',
+ u'import-malformed-script': u'importScripts("malformed-worker.py?parse-error");',
+ u'import-no-such-script': u'importScripts("no-such-script.js");',
+ u'top-level-await': u'await Promise.resolve(1);',
+ u'instantiation-error': u'import nonexistent from "./imported-module-script.js";',
+ u'instantiation-error-and-top-level-await': u'import nonexistent from "./imported-module-script.js"; await Promise.resolve(1);'}[request.url_parts.query]
+
+ return headers, body
diff --git a/test/wpt/tests/service-workers/service-worker/resources/message-vs-microtask.html b/test/wpt/tests/service-workers/service-worker/resources/message-vs-microtask.html
new file mode 100644
index 0000000..2c45c59
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/message-vs-microtask.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<script>
+ let draft = [];
+ var resolve_manual_promise;
+ let manual_promise =
+ new Promise(resolve => resolve_manual_promise = resolve).then(() => draft.push('microtask'));
+
+ let resolve_message_promise;
+ let message_promise = new Promise(resolve => resolve_message_promise = resolve);
+ function handle_message(event) {
+ draft.push('message');
+ resolve_message_promise();
+ }
+
+ var result = Promise.all([manual_promise, message_promise]).then(() => draft);
+</script>
+
+<script src="empty.js?key=start"></script>
diff --git a/test/wpt/tests/service-workers/service-worker/resources/mime-sniffing-worker.js b/test/wpt/tests/service-workers/service-worker/resources/mime-sniffing-worker.js
new file mode 100644
index 0000000..5c34a7a
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/mime-sniffing-worker.js
@@ -0,0 +1,9 @@
+self.addEventListener('fetch', function(event) {
+ // Use an empty content-type value to force mime-sniffing. Note, this
+ // must be passed to the constructor since the mime-type of the Response
+ // is fixed and cannot be later changed.
+ var res = new Response('<!DOCTYPE html>\n<h1 id=\'testid\'>test</h1>', {
+ headers: { 'content-type': '' }
+ });
+ event.respondWith(res);
+ });
diff --git a/test/wpt/tests/service-workers/service-worker/resources/mime-type-worker.py b/test/wpt/tests/service-workers/service-worker/resources/mime-type-worker.py
new file mode 100644
index 0000000..92a602e
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/mime-type-worker.py
@@ -0,0 +1,4 @@
+def main(request, response):
+ if b'mime' in request.GET:
+ return [(b'Content-Type', request.GET[b'mime'])], b""
+ return [], b""
diff --git a/test/wpt/tests/service-workers/service-worker/resources/mint-new-worker.py b/test/wpt/tests/service-workers/service-worker/resources/mint-new-worker.py
new file mode 100644
index 0000000..ebee4ff
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/mint-new-worker.py
@@ -0,0 +1,27 @@
+import random
+
+import time
+
+body = u'''
+onactivate = (e) => e.waitUntil(clients.claim());
+var resolve_wait_until;
+var wait_until = new Promise(resolve => {
+ resolve_wait_until = resolve;
+ });
+onmessage = (e) => {
+ if (e.data == 'wait')
+ e.waitUntil(wait_until);
+ if (e.data == 'go')
+ resolve_wait_until();
+ };'''
+
+def main(request, response):
+ headers = [(b'Cache-Control', b'no-cache, must-revalidate'),
+ (b'Pragma', b'no-cache'),
+ (b'Content-Type', b'application/javascript')]
+
+ skipWaiting = u''
+ if b'skip-waiting' in request.GET:
+ skipWaiting = u'skipWaiting();'
+
+ return headers, u'/* %s %s */ %s %s' % (time.time(), random.random(), skipWaiting, body)
diff --git a/test/wpt/tests/service-workers/service-worker/resources/missing.asis b/test/wpt/tests/service-workers/service-worker/resources/missing.asis
new file mode 100644
index 0000000..4846fe0
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/missing.asis
@@ -0,0 +1,4 @@
+HTTP/1.1 404 Not Found
+Content-Type: text/javascript
+
+alert("hello");
diff --git a/test/wpt/tests/service-workers/service-worker/resources/module-worker.js b/test/wpt/tests/service-workers/service-worker/resources/module-worker.js
new file mode 100644
index 0000000..385fe71
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/module-worker.js
@@ -0,0 +1 @@
+import * as module from './imported-module-script.js';
diff --git a/test/wpt/tests/service-workers/service-worker/resources/multipart-image-iframe.html b/test/wpt/tests/service-workers/service-worker/resources/multipart-image-iframe.html
new file mode 100644
index 0000000..c59b955
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/multipart-image-iframe.html
@@ -0,0 +1,19 @@
+<script>
+function load_multipart_image(src) {
+ return new Promise((resolve, reject) => {
+ const img = document.createElement('img');
+ img.addEventListener('load', () => resolve(img));
+ img.addEventListener('error', (e) => reject(new DOMException('load failed', 'NetworkError')));
+ img.src = src;
+ });
+}
+
+function get_image_data(img) {
+ const canvas = document.createElement('canvas');
+ const context = canvas.getContext('2d');
+ context.drawImage(img, 0, 0);
+ // When |img.src| is cross origin, this should throw a SecurityError.
+ const imageData = context.getImageData(0, 0, 1, 1);
+ return imageData;
+}
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/resources/multipart-image-worker.js b/test/wpt/tests/service-workers/service-worker/resources/multipart-image-worker.js
new file mode 100644
index 0000000..a38fe54
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/multipart-image-worker.js
@@ -0,0 +1,21 @@
+importScripts('/common/get-host-info.sub.js');
+importScripts('test-helpers.sub.js');
+
+const host_info = get_host_info();
+
+const multipart_image_path = base_path() + 'multipart-image.py';
+const sameorigin_url = host_info['HTTPS_ORIGIN'] + multipart_image_path;
+const cross_origin_url = host_info['HTTPS_REMOTE_ORIGIN'] + multipart_image_path;
+
+self.addEventListener('fetch', event => {
+ const url = event.request.url;
+ if (url.indexOf('cross-origin-multipart-image-with-no-cors') >= 0) {
+ event.respondWith(fetch(cross_origin_url, {mode: 'no-cors'}));
+ } else if (url.indexOf('cross-origin-multipart-image-with-cors-rejected') >= 0) {
+ event.respondWith(fetch(cross_origin_url, {mode: 'cors'}));
+ } else if (url.indexOf('cross-origin-multipart-image-with-cors-approved') >= 0) {
+ event.respondWith(fetch(cross_origin_url + '?approvecors', {mode: 'cors'}));
+ } else if (url.indexOf('same-origin-multipart-image') >= 0) {
+ event.respondWith(fetch(sameorigin_url));
+ }
+});
diff --git a/test/wpt/tests/service-workers/service-worker/resources/multipart-image.py b/test/wpt/tests/service-workers/service-worker/resources/multipart-image.py
new file mode 100644
index 0000000..9a3c035
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/multipart-image.py
@@ -0,0 +1,23 @@
+# A request handler that serves a multipart image.
+
+import os
+
+
+BOUNDARY = b'cutHere'
+
+
+def create_part(path):
+ with open(path, u'rb') as f:
+ return b'Content-Type: image/png\r\n\r\n' + f.read() + b'--%s' % BOUNDARY
+
+
+def main(request, response):
+ content_type = b'multipart/x-mixed-replace; boundary=%s' % BOUNDARY
+ headers = [(b'Content-Type', content_type)]
+ if b'approvecors' in request.GET:
+ headers.append((b'Access-Control-Allow-Origin', b'*'))
+
+ image_path = os.path.join(request.doc_root, u'images')
+ body = create_part(os.path.join(image_path, u'red.png'))
+ body = body + create_part(os.path.join(image_path, u'red-16x16.png'))
+ return headers, body
diff --git a/test/wpt/tests/service-workers/service-worker/resources/navigate-window-worker.js b/test/wpt/tests/service-workers/service-worker/resources/navigate-window-worker.js
new file mode 100644
index 0000000..f961743
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/navigate-window-worker.js
@@ -0,0 +1,21 @@
+addEventListener('message', function(evt) {
+ if (evt.data.type === 'GET_CLIENTS') {
+ clients.matchAll(evt.data.opts).then(function(clientList) {
+ var resultList = clientList.map(function(c) {
+ return { url: c.url, frameType: c.frameType, id: c.id };
+ });
+ evt.source.postMessage({ type: 'success', detail: resultList });
+ }).catch(function(err) {
+ evt.source.postMessage({
+ type: 'failure',
+ detail: 'matchAll() rejected with "' + err + '"'
+ });
+ });
+ return;
+ }
+
+ evt.source.postMessage({
+ type: 'failure',
+ detail: 'Unexpected message type "' + evt.data.type + '"'
+ });
+});
diff --git a/test/wpt/tests/service-workers/service-worker/resources/navigation-headers-server.py b/test/wpt/tests/service-workers/service-worker/resources/navigation-headers-server.py
new file mode 100644
index 0000000..5b2e044
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/navigation-headers-server.py
@@ -0,0 +1,19 @@
+def main(request, response):
+ response.status = (200, b"OK")
+ response.headers.set(b"Content-Type", b"text/html")
+ return b"""
+ <script>
+ self.addEventListener('load', evt => {
+ self.parent.postMessage({
+ origin: '%s',
+ referer: '%s',
+ 'sec-fetch-site': '%s',
+ 'sec-fetch-mode': '%s',
+ 'sec-fetch-dest': '%s',
+ });
+ });
+ </script>""" % (request.headers.get(
+ b"origin", b"not set"), request.headers.get(b"referer", b"not set"),
+ request.headers.get(b"sec-fetch-site", b"not set"),
+ request.headers.get(b"sec-fetch-mode", b"not set"),
+ request.headers.get(b"sec-fetch-dest", b"not set"))
diff --git a/test/wpt/tests/service-workers/service-worker/resources/navigation-redirect-body-worker.js b/test/wpt/tests/service-workers/service-worker/resources/navigation-redirect-body-worker.js
new file mode 100644
index 0000000..39f11ba
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/navigation-redirect-body-worker.js
@@ -0,0 +1,11 @@
+self.addEventListener('fetch', function(event) {
+ event.respondWith(
+ fetch(event.request)
+ .then(
+ function(response) {
+ return response;
+ },
+ function(error) {
+ return new Response('Error:' + error);
+ }));
+ });
diff --git a/test/wpt/tests/service-workers/service-worker/resources/navigation-redirect-body.py b/test/wpt/tests/service-workers/service-worker/resources/navigation-redirect-body.py
new file mode 100644
index 0000000..d10329e
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/navigation-redirect-body.py
@@ -0,0 +1,11 @@
+import os
+
+from wptserve.utils import isomorphic_encode
+
+filename = os.path.basename(isomorphic_encode(__file__))
+
+def main(request, response):
+ if request.method == u'POST':
+ return 302, [(b'Location', b'./%s?redirect' % filename)], b''
+
+ return [(b'Content-Type', b'text/plain')], request.request_path
diff --git a/test/wpt/tests/service-workers/service-worker/resources/navigation-redirect-other-origin.html b/test/wpt/tests/service-workers/service-worker/resources/navigation-redirect-other-origin.html
new file mode 100644
index 0000000..d82571d
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/navigation-redirect-other-origin.html
@@ -0,0 +1,89 @@
+<!DOCTYPE html>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="test-helpers.sub.js"></script>
+<script>
+var host_info = get_host_info();
+var SCOPE = 'navigation-redirect-scope1.py';
+var SCRIPT = 'redirect-worker.js';
+
+var registration;
+var worker;
+var wait_for_worker_promise = navigator.serviceWorker.getRegistration(SCOPE)
+ .then(function(reg) {
+ if (reg)
+ return reg.unregister();
+ })
+ .then(function() {
+ return navigator.serviceWorker.register(SCRIPT, {scope: SCOPE});
+ })
+ .then(function(reg) {
+ registration = reg;
+ worker = reg.installing;
+ return new Promise(function(resolve) {
+ worker.addEventListener('statechange', function() {
+ if (worker.state == 'activated')
+ resolve();
+ });
+ });
+ });
+
+function send_result(message_id, result) {
+ window.parent.postMessage(
+ {id: message_id, result: result},
+ host_info['HTTPS_ORIGIN']);
+}
+
+function get_request_infos(worker) {
+ return new Promise(function(resolve) {
+ var channel = new MessageChannel();
+ channel.port1.onmessage = (msg) => {
+ resolve(msg.data.requestInfos);
+ };
+ worker.postMessage({command: 'getRequestInfos', port: channel.port2},
+ [channel.port2]);
+ });
+}
+
+function get_clients(worker, actual_ids) {
+ return new Promise(function(resolve) {
+ var channel = new MessageChannel();
+ channel.port1.onmessage = (msg) => {
+ resolve(msg.data.clients);
+ };
+ worker.postMessage({
+ command: 'getClients',
+ actual_ids,
+ port: channel.port2
+ }, [channel.port2]);
+ });
+}
+
+window.addEventListener('message', on_message, false);
+
+function on_message(e) {
+ if (e.origin != host_info['HTTPS_ORIGIN']) {
+ console.error('invalid origin: ' + e.origin);
+ return;
+ }
+ const command = e.data.message.command;
+ if (command == 'wait_for_worker') {
+ wait_for_worker_promise.then(function() { send_result(e.data.id, 'ok'); });
+ } else if (command == 'get_request_infos') {
+ get_request_infos(worker)
+ .then(function(data) {
+ send_result(e.data.id, data);
+ });
+ } else if (command == 'get_clients') {
+ get_clients(worker, e.data.message.actual_ids)
+ .then(function(data) {
+ send_result(e.data.id, data);
+ });
+ } else if (command == 'unregister') {
+ registration.unregister()
+ .then(function() {
+ send_result(e.data.id, 'ok');
+ });
+ }
+}
+
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/resources/navigation-redirect-out-scope.py b/test/wpt/tests/service-workers/service-worker/resources/navigation-redirect-out-scope.py
new file mode 100644
index 0000000..9b90b14
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/navigation-redirect-out-scope.py
@@ -0,0 +1,22 @@
+def main(request, response):
+ if b"url" in request.GET:
+ headers = [(b"Location", request.GET[b"url"])]
+ return 302, headers, b''
+
+ status = 200
+
+ if b"noLocationRedirect" in request.GET:
+ status = 302
+
+ return status, [(b"content-type", b"text/html")], b'''
+<!DOCTYPE html>
+<script>
+onmessage = event => {
+ window.parent.postMessage(
+ {
+ id: event.data.id,
+ result: location.href
+ }, '*');
+};
+</script>
+'''
diff --git a/test/wpt/tests/service-workers/service-worker/resources/navigation-redirect-scope1.py b/test/wpt/tests/service-workers/service-worker/resources/navigation-redirect-scope1.py
new file mode 100644
index 0000000..9b90b14
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/navigation-redirect-scope1.py
@@ -0,0 +1,22 @@
+def main(request, response):
+ if b"url" in request.GET:
+ headers = [(b"Location", request.GET[b"url"])]
+ return 302, headers, b''
+
+ status = 200
+
+ if b"noLocationRedirect" in request.GET:
+ status = 302
+
+ return status, [(b"content-type", b"text/html")], b'''
+<!DOCTYPE html>
+<script>
+onmessage = event => {
+ window.parent.postMessage(
+ {
+ id: event.data.id,
+ result: location.href
+ }, '*');
+};
+</script>
+'''
diff --git a/test/wpt/tests/service-workers/service-worker/resources/navigation-redirect-scope2.py b/test/wpt/tests/service-workers/service-worker/resources/navigation-redirect-scope2.py
new file mode 100644
index 0000000..9b90b14
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/navigation-redirect-scope2.py
@@ -0,0 +1,22 @@
+def main(request, response):
+ if b"url" in request.GET:
+ headers = [(b"Location", request.GET[b"url"])]
+ return 302, headers, b''
+
+ status = 200
+
+ if b"noLocationRedirect" in request.GET:
+ status = 302
+
+ return status, [(b"content-type", b"text/html")], b'''
+<!DOCTYPE html>
+<script>
+onmessage = event => {
+ window.parent.postMessage(
+ {
+ id: event.data.id,
+ result: location.href
+ }, '*');
+};
+</script>
+'''
diff --git a/test/wpt/tests/service-workers/service-worker/resources/navigation-redirect-to-http-iframe.html b/test/wpt/tests/service-workers/service-worker/resources/navigation-redirect-to-http-iframe.html
new file mode 100644
index 0000000..40e27c6
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/navigation-redirect-to-http-iframe.html
@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="test-helpers.sub.js"></script>
+<script>
+var SCOPE = './redirect.py?Redirect=' + encodeURI('http://example.com');
+var SCRIPT = 'navigation-redirect-to-http-worker.js';
+var host_info = get_host_info();
+
+navigator.serviceWorker.getRegistration(SCOPE)
+ .then(function(registration) {
+ if (registration)
+ return registration.unregister();
+ })
+ .then(function() {
+ return navigator.serviceWorker.register(SCRIPT, {scope: SCOPE});
+ })
+ .then(function(registration) {
+ return new Promise(function(resolve) {
+ registration.addEventListener('updatefound', function() {
+ resolve(registration.installing);
+ });
+ });
+ })
+ .then(function(worker) {
+ worker.addEventListener('statechange', on_state_change);
+ })
+ .catch(function(reason) {
+ window.parent.postMessage({results: 'FAILURE: ' + reason.message},
+ host_info['HTTPS_ORIGIN']);
+ });
+
+function on_state_change(event) {
+ if (event.target.state != 'activated')
+ return;
+ with_iframe(SCOPE, {auto_remove: false})
+ .then(function(frame) {
+ window.parent.postMessage(
+ {results: frame.contentDocument.body.textContent},
+ host_info['HTTPS_ORIGIN']);
+ });
+}
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/resources/navigation-redirect-to-http-worker.js b/test/wpt/tests/service-workers/service-worker/resources/navigation-redirect-to-http-worker.js
new file mode 100644
index 0000000..6f2a8ae
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/navigation-redirect-to-http-worker.js
@@ -0,0 +1,22 @@
+importScripts('/resources/testharness.js');
+
+self.addEventListener('fetch', function(event) {
+ event.respondWith(new Promise(function(resolve) {
+ Promise.resolve()
+ .then(function() {
+ assert_equals(
+ event.request.redirect, 'manual',
+ 'The redirect mode of navigation request must be manual.');
+ return fetch(event.request);
+ })
+ .then(function(response) {
+ assert_equals(
+ response.type, 'opaqueredirect',
+ 'The response type of 302 response must be opaqueredirect.');
+ resolve(new Response('OK'));
+ })
+ .catch(function(error) {
+ resolve(new Response('Failed in SW: ' + error));
+ });
+ }));
+ });
diff --git a/test/wpt/tests/service-workers/service-worker/resources/navigation-timing-worker-extended.js b/test/wpt/tests/service-workers/service-worker/resources/navigation-timing-worker-extended.js
new file mode 100644
index 0000000..79c5408
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/navigation-timing-worker-extended.js
@@ -0,0 +1,22 @@
+importScripts("/resources/testharness.js");
+const timings = {}
+
+const DELAY_ACTIVATION = 500
+
+self.addEventListener('activate', event => {
+ event.waitUntil(new Promise(resolve => {
+ timings.activateWorkerStart = performance.now() + performance.timeOrigin;
+
+ // This gives us enough time to ensure activation would delay fetch handling
+ step_timeout(resolve, DELAY_ACTIVATION);
+ }).then(() => timings.activateWorkerEnd = performance.now() + performance.timeOrigin));
+})
+
+self.addEventListener('fetch', event => {
+ timings.handleFetchEvent = performance.now() + performance.timeOrigin;
+ event.respondWith(Promise.resolve(new Response(new Blob([`
+ <script>
+ parent.postMessage(${JSON.stringify(timings)}, "*")
+ </script>
+ `]))));
+});
diff --git a/test/wpt/tests/service-workers/service-worker/resources/navigation-timing-worker.js b/test/wpt/tests/service-workers/service-worker/resources/navigation-timing-worker.js
new file mode 100644
index 0000000..8539b40
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/navigation-timing-worker.js
@@ -0,0 +1,15 @@
+self.addEventListener('fetch', (event) => {
+ const url = event.request.url;
+
+ // Network fallback.
+ if (url.indexOf('network-fallback') >= 0) {
+ return;
+ }
+
+ // Don't intercept redirect.
+ if (url.indexOf('redirect.py') >= 0) {
+ return;
+ }
+
+ event.respondWith(fetch(url));
+});
diff --git a/test/wpt/tests/service-workers/service-worker/resources/nested-blob-url-worker-created-from-worker.html b/test/wpt/tests/service-workers/service-worker/resources/nested-blob-url-worker-created-from-worker.html
new file mode 100644
index 0000000..fc048e2
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/nested-blob-url-worker-created-from-worker.html
@@ -0,0 +1,16 @@
+<!doctype html>
+<script>
+const baseLocation = window.location;
+const workerUrl = new URL('create-blob-url-worker.js', baseLocation).href;
+const worker = new Worker(workerUrl);
+
+function fetch_in_worker(url) {
+ const resourceUrl = new URL(url, baseLocation).href;
+ return new Promise((resolve) => {
+ worker.onmessage = (event) => {
+ resolve(event.data);
+ };
+ worker.postMessage(resourceUrl);
+ });
+}
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/resources/nested-blob-url-workers.html b/test/wpt/tests/service-workers/service-worker/resources/nested-blob-url-workers.html
new file mode 100644
index 0000000..f0eafcd
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/nested-blob-url-workers.html
@@ -0,0 +1,38 @@
+<!doctype html>
+<script>
+const baseLocation = window.location;
+const parentWorkerScript = `
+ const childWorkerScript = 'self.onmessage = async (e) => {' +
+ ' const response = await fetch(e.data);' +
+ ' const text = await response.text();' +
+ ' self.postMessage(text);' +
+ '};';
+ const blob = new Blob([childWorkerScript], { type: 'text/javascript' });
+ const blobUrl = URL.createObjectURL(blob);
+ const childWorker = new Worker(blobUrl);
+
+ // When a message comes from the parent frame, sends a resource url to the
+ // child worker.
+ self.onmessage = (e) => {
+ childWorker.postMessage(e.data);
+ };
+ // When a message comes from the child worker, sends a content of fetch() to
+ // the parent frame.
+ childWorker.onmessage = (e) => {
+ self.postMessage(e.data);
+ };
+`;
+const blob = new Blob([parentWorkerScript], { type: 'text/javascript' });
+const blobUrl = URL.createObjectURL(blob);
+const worker = new Worker(blobUrl);
+
+function fetch_in_worker(url) {
+ const resourceUrl = new URL(url, baseLocation).href;
+ return new Promise((resolve) => {
+ worker.onmessage = (event) => {
+ resolve(event.data);
+ };
+ worker.postMessage(resourceUrl);
+ });
+}
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/resources/nested-iframe-parent.html b/test/wpt/tests/service-workers/service-worker/resources/nested-iframe-parent.html
new file mode 100644
index 0000000..115ab26
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/nested-iframe-parent.html
@@ -0,0 +1,5 @@
+<!DOCTYPE html>
+<script>
+ navigator.serviceWorker.onmessage = event => parent.postMessage(event.data, '*', event.ports);
+</script>
+<iframe id='child'></iframe>
diff --git a/test/wpt/tests/service-workers/service-worker/resources/nested-parent.html b/test/wpt/tests/service-workers/service-worker/resources/nested-parent.html
new file mode 100644
index 0000000..b4832d4
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/nested-parent.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<meta charset="utf-8"/>
+<meta name="referrer" content="origin">
+<script>
+async function onLoad() {
+ self.addEventListener('message', evt => {
+ if (self.opener)
+ self.opener.postMessage(evt.data, '*');
+ else
+ self.top.postMessage(evt.data, '*');
+ }, { once: true });
+ const params = new URLSearchParams(self.location.search);
+ const frame = document.createElement('iframe');
+ frame.src = params.get('target');
+ document.body.appendChild(frame);
+}
+self.addEventListener('load', onLoad);
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/resources/nested-worker-created-from-blob-url-worker.html b/test/wpt/tests/service-workers/service-worker/resources/nested-worker-created-from-blob-url-worker.html
new file mode 100644
index 0000000..3fad2c9
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/nested-worker-created-from-blob-url-worker.html
@@ -0,0 +1,33 @@
+<!doctype html>
+<script>
+const baseLocation = window.location;
+const parentWorkerScript = `
+ const workerUrl =
+ new URL('postmessage-fetched-text.js', '${baseLocation}').href;
+ const childWorker = new Worker(workerUrl);
+
+ // When a message comes from the parent frame, sends a resource url to the
+ // child worker.
+ self.onmessage = (e) => {
+ childWorker.postMessage(e.data);
+ };
+ // When a message comes from the child worker, sends a content of fetch() to
+ // the parent frame.
+ childWorker.onmessage = (e) => {
+ self.postMessage(e.data);
+ };
+`;
+const blob = new Blob([parentWorkerScript], { type: 'text/javascript' });
+const blobUrl = URL.createObjectURL(blob);
+const worker = new Worker(blobUrl);
+
+function fetch_in_worker(url) {
+ const resourceUrl = new URL(url, baseLocation).href;
+ return new Promise((resolve) => {
+ worker.onmessage = (event) => {
+ resolve(event.data);
+ };
+ worker.postMessage(resourceUrl);
+ });
+}
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/resources/nested_load_worker.js b/test/wpt/tests/service-workers/service-worker/resources/nested_load_worker.js
new file mode 100644
index 0000000..ef0ed8f
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/nested_load_worker.js
@@ -0,0 +1,23 @@
+// Entry point for dedicated workers.
+self.onmessage = evt => {
+ try {
+ const worker = new Worker('load_worker.js');
+ worker.onmessage = evt => self.postMessage(evt.data);
+ worker.postMessage(evt.data);
+ } catch (err) {
+ self.postMessage('Unexpected error! ' + err.message);
+ }
+};
+
+// Entry point for shared workers.
+self.onconnect = evt => {
+ evt.ports[0].onmessage = e => {
+ try {
+ const worker = new Worker('load_worker.js');
+ worker.onmessage = e => evt.ports[0].postMessage(e.data);
+ worker.postMessage(evt.data);
+ } catch (err) {
+ evt.ports[0].postMessage('Unexpected error! ' + err.message);
+ }
+ };
+};
diff --git a/test/wpt/tests/service-workers/service-worker/resources/no-dynamic-import.js b/test/wpt/tests/service-workers/service-worker/resources/no-dynamic-import.js
new file mode 100644
index 0000000..ecedd6c
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/no-dynamic-import.js
@@ -0,0 +1,18 @@
+/** @type {[name: string, url: string][]} */
+const importUrlTests = [
+ ["Module URL", "./basic-module.js"],
+ // In no-dynamic-import-in-module.any.js, this module is also statically imported
+ ["Another module URL", "./basic-module-2.js"],
+ [
+ "Module data: URL",
+ "data:text/javascript;charset=utf-8," +
+ encodeURIComponent(`export default 'hello!';`),
+ ],
+];
+
+for (const [name, url] of importUrlTests) {
+ promise_test(
+ (t) => promise_rejects_js(t, TypeError, import(url), "Import must reject"),
+ name
+ );
+}
diff --git a/test/wpt/tests/service-workers/service-worker/resources/notification_icon.py b/test/wpt/tests/service-workers/service-worker/resources/notification_icon.py
new file mode 100644
index 0000000..71f5a9d
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/notification_icon.py
@@ -0,0 +1,11 @@
+from urllib.parse import parse_qs
+
+from wptserve.utils import isomorphic_encode
+
+def main(req, res):
+ qs_cookie_val = parse_qs(req.url_parts.query).get(u'set-cookie-notification')
+
+ if qs_cookie_val:
+ res.set_cookie(b'notification', isomorphic_encode(qs_cookie_val[0]))
+
+ return b'not really an icon'
diff --git a/test/wpt/tests/service-workers/service-worker/resources/object-image-is-not-intercepted-iframe.html b/test/wpt/tests/service-workers/service-worker/resources/object-image-is-not-intercepted-iframe.html
new file mode 100644
index 0000000..5a20a58
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/object-image-is-not-intercepted-iframe.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>iframe for embed-and-object-are-not-intercepted test</title>
+<body>
+<object type="image/png" data="/images/green.png"></embed>
+<script>
+// Our parent (the root frame of the test) will examine this to get the result.
+var test_promise = new Promise(resolve => {
+ if (!navigator.serviceWorker.controller)
+ resolve('FAIL: this iframe is not controlled');
+
+ const elem = document.querySelector('object');
+ elem.addEventListener('load', e => {
+ resolve('request was not intercepted');
+ });
+ elem.addEventListener('error', e => {
+ resolve('FAIL: request was intercepted');
+ });
+ });
+</script>
+</body>
diff --git a/test/wpt/tests/service-workers/service-worker/resources/object-is-not-intercepted-iframe.html b/test/wpt/tests/service-workers/service-worker/resources/object-is-not-intercepted-iframe.html
new file mode 100644
index 0000000..0aeb819
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/object-is-not-intercepted-iframe.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>iframe for embed-and-object-are-not-intercepted test</title>
+<body>
+<script>
+// The OBJECT element will call this with the result about whether the OBJECT
+// request was intercepted by the service worker.
+var report_result;
+
+// Our parent (the root frame of the test) will examine this to get the result.
+var test_promise = new Promise(resolve => {
+ report_result = resolve;
+ });
+</script>
+
+<object data="embedded-content-from-server.html"></object>
+</body>
+
diff --git a/test/wpt/tests/service-workers/service-worker/resources/object-navigation-is-not-intercepted-iframe.html b/test/wpt/tests/service-workers/service-worker/resources/object-navigation-is-not-intercepted-iframe.html
new file mode 100644
index 0000000..5c8ab79
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/object-navigation-is-not-intercepted-iframe.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>iframe for embed-and-object-are-not-intercepted test</title>
+<body>
+<script>
+// The OBJECT element will call this with the result about whether the OBJECT
+// request was intercepted by the service worker.
+var report_result;
+
+// Our parent (the root frame of the test) will examine this to get the result.
+var test_promise = new Promise(resolve => {
+ report_result = resolve;
+ });
+
+let el = document.createElement('object');
+el.data = "/common/blank.html";
+el.addEventListener('load', _ => {
+ window[0].location = "/service-workers/service-worker/resources/embedded-content-from-server.html";
+}, { once: true });
+document.body.appendChild(el);
+</script>
+
+</body>
+
diff --git a/test/wpt/tests/service-workers/service-worker/resources/onactivate-throw-error-from-nested-event-worker.js b/test/wpt/tests/service-workers/service-worker/resources/onactivate-throw-error-from-nested-event-worker.js
new file mode 100644
index 0000000..7c97014
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/onactivate-throw-error-from-nested-event-worker.js
@@ -0,0 +1,13 @@
+var max_nesting_level = 8;
+
+self.addEventListener('message', function(event) {
+ var level = event.data;
+ if (level < max_nesting_level)
+ dispatchEvent(new MessageEvent('message', { data: level + 1 }));
+ throw Error('error at level ' + level);
+ });
+
+self.addEventListener('activate', function(event) {
+ dispatchEvent(new MessageEvent('message', { data: 1 }));
+ });
+
diff --git a/test/wpt/tests/service-workers/service-worker/resources/onactivate-throw-error-then-cancel-worker.js b/test/wpt/tests/service-workers/service-worker/resources/onactivate-throw-error-then-cancel-worker.js
new file mode 100644
index 0000000..0bd9d31
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/onactivate-throw-error-then-cancel-worker.js
@@ -0,0 +1,3 @@
+self.onerror = function(event) { return true; };
+
+self.addEventListener('activate', function(event) { throw new Error(); });
diff --git a/test/wpt/tests/service-workers/service-worker/resources/onactivate-throw-error-then-prevent-default-worker.js b/test/wpt/tests/service-workers/service-worker/resources/onactivate-throw-error-then-prevent-default-worker.js
new file mode 100644
index 0000000..d56c951
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/onactivate-throw-error-then-prevent-default-worker.js
@@ -0,0 +1,7 @@
+// Ensure we can handle multiple error handlers. One error handler
+// calling preventDefault should cause the event to be treated as
+// handled.
+self.addEventListener('error', function(event) {});
+self.addEventListener('error', function(event) { event.preventDefault(); });
+self.addEventListener('error', function(event) {});
+self.addEventListener('activate', function(event) { throw new Error(); });
diff --git a/test/wpt/tests/service-workers/service-worker/resources/onactivate-throw-error-with-empty-onerror-worker.js b/test/wpt/tests/service-workers/service-worker/resources/onactivate-throw-error-with-empty-onerror-worker.js
new file mode 100644
index 0000000..eb12ae8
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/onactivate-throw-error-with-empty-onerror-worker.js
@@ -0,0 +1,2 @@
+self.addEventListener('error', function(event) {});
+self.addEventListener('activate', function(event) { throw new Error(); });
diff --git a/test/wpt/tests/service-workers/service-worker/resources/onactivate-throw-error-worker.js b/test/wpt/tests/service-workers/service-worker/resources/onactivate-throw-error-worker.js
new file mode 100644
index 0000000..1e88ac5
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/onactivate-throw-error-worker.js
@@ -0,0 +1,7 @@
+// Ensure we can handle multiple activate handlers. One handler throwing an
+// error should cause the event dispatch to be treated as having unhandled
+// errors.
+self.addEventListener('activate', function(event) {});
+self.addEventListener('activate', function(event) {});
+self.addEventListener('activate', function(event) { throw new Error(); });
+self.addEventListener('activate', function(event) {});
diff --git a/test/wpt/tests/service-workers/service-worker/resources/onactivate-waituntil-forever.js b/test/wpt/tests/service-workers/service-worker/resources/onactivate-waituntil-forever.js
new file mode 100644
index 0000000..65b02b1
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/onactivate-waituntil-forever.js
@@ -0,0 +1,8 @@
+'use strict';
+
+self.addEventListener('activate', event => {
+ event.waitUntil(new Promise(() => {
+ // Use a promise that never resolves to prevent this service worker from
+ // advancing past the 'activating' state.
+ }));
+});
diff --git a/test/wpt/tests/service-workers/service-worker/resources/onfetch-waituntil-forever.js b/test/wpt/tests/service-workers/service-worker/resources/onfetch-waituntil-forever.js
new file mode 100644
index 0000000..b905d55
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/onfetch-waituntil-forever.js
@@ -0,0 +1,10 @@
+'use strict';
+
+self.addEventListener('fetch', event => {
+ if (event.request.url.endsWith('waituntil-forever')) {
+ event.respondWith(new Promise(() => {
+ // Use a promise that never resolves to prevent this fetch from
+ // completing.
+ }));
+ }
+});
diff --git a/test/wpt/tests/service-workers/service-worker/resources/oninstall-throw-error-from-nested-event-worker.js b/test/wpt/tests/service-workers/service-worker/resources/oninstall-throw-error-from-nested-event-worker.js
new file mode 100644
index 0000000..6729ab6
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/oninstall-throw-error-from-nested-event-worker.js
@@ -0,0 +1,12 @@
+var max_nesting_level = 8;
+
+self.addEventListener('message', function(event) {
+ var level = event.data;
+ if (level < max_nesting_level)
+ dispatchEvent(new MessageEvent('message', { data: level + 1 }));
+ throw Error('error at level ' + level);
+ });
+
+self.addEventListener('install', function(event) {
+ dispatchEvent(new MessageEvent('message', { data: 1 }));
+ });
diff --git a/test/wpt/tests/service-workers/service-worker/resources/oninstall-throw-error-then-cancel-worker.js b/test/wpt/tests/service-workers/service-worker/resources/oninstall-throw-error-then-cancel-worker.js
new file mode 100644
index 0000000..c2c499a
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/oninstall-throw-error-then-cancel-worker.js
@@ -0,0 +1,3 @@
+self.onerror = function(event) { return true; };
+
+self.addEventListener('install', function(event) { throw new Error(); });
diff --git a/test/wpt/tests/service-workers/service-worker/resources/oninstall-throw-error-then-prevent-default-worker.js b/test/wpt/tests/service-workers/service-worker/resources/oninstall-throw-error-then-prevent-default-worker.js
new file mode 100644
index 0000000..7667c27
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/oninstall-throw-error-then-prevent-default-worker.js
@@ -0,0 +1,7 @@
+// Ensure we can handle multiple error handlers. One error handler
+// calling preventDefault should cause the event to be treated as
+// handled.
+self.addEventListener('error', function(event) {});
+self.addEventListener('error', function(event) { event.preventDefault(); });
+self.addEventListener('error', function(event) {});
+self.addEventListener('install', function(event) { throw new Error(); });
diff --git a/test/wpt/tests/service-workers/service-worker/resources/oninstall-throw-error-with-empty-onerror-worker.js b/test/wpt/tests/service-workers/service-worker/resources/oninstall-throw-error-with-empty-onerror-worker.js
new file mode 100644
index 0000000..8f56d1b
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/oninstall-throw-error-with-empty-onerror-worker.js
@@ -0,0 +1,2 @@
+self.addEventListener('error', function(event) {});
+self.addEventListener('install', function(event) { throw new Error(); });
diff --git a/test/wpt/tests/service-workers/service-worker/resources/oninstall-throw-error-worker.js b/test/wpt/tests/service-workers/service-worker/resources/oninstall-throw-error-worker.js
new file mode 100644
index 0000000..cc2f6d7
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/oninstall-throw-error-worker.js
@@ -0,0 +1,7 @@
+// Ensure we can handle multiple install handlers. One handler throwing an
+// error should cause the event dispatch to be treated as having unhandled
+// errors.
+self.addEventListener('install', function(event) {});
+self.addEventListener('install', function(event) {});
+self.addEventListener('install', function(event) { throw new Error(); });
+self.addEventListener('install', function(event) {});
diff --git a/test/wpt/tests/service-workers/service-worker/resources/oninstall-waituntil-forever.js b/test/wpt/tests/service-workers/service-worker/resources/oninstall-waituntil-forever.js
new file mode 100644
index 0000000..964483f
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/oninstall-waituntil-forever.js
@@ -0,0 +1,8 @@
+'use strict';
+
+self.addEventListener('install', event => {
+ event.waitUntil(new Promise(() => {
+ // Use a promise that never resolves to prevent this service worker from
+ // advancing past the 'installing' state.
+ }));
+});
diff --git a/test/wpt/tests/service-workers/service-worker/resources/oninstall-waituntil-throw-error-worker.js b/test/wpt/tests/service-workers/service-worker/resources/oninstall-waituntil-throw-error-worker.js
new file mode 100644
index 0000000..6cb8f6e
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/oninstall-waituntil-throw-error-worker.js
@@ -0,0 +1,5 @@
+self.addEventListener('install', function(event) {
+ event.waitUntil(new Promise(function(aRequest, aResponse) {
+ throw new Error();
+ }));
+});
diff --git a/test/wpt/tests/service-workers/service-worker/resources/onparse-infiniteloop-worker.js b/test/wpt/tests/service-workers/service-worker/resources/onparse-infiniteloop-worker.js
new file mode 100644
index 0000000..6f439ae
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/onparse-infiniteloop-worker.js
@@ -0,0 +1,8 @@
+'use strict';
+
+// Use an infinite loop to prevent this service worker from advancing past the
+// 'parsed' state.
+let i = 0;
+while (true) {
+ ++i;
+}
diff --git a/test/wpt/tests/service-workers/service-worker/resources/opaque-response-being-preloaded-xhr.html b/test/wpt/tests/service-workers/service-worker/resources/opaque-response-being-preloaded-xhr.html
new file mode 100644
index 0000000..9c6d8bd
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/opaque-response-being-preloaded-xhr.html
@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<body></body>
+<script>
+const URL = 'opaque-response?from=opaque-response-being-preloaded-xhr.html';
+function runTest() {
+ var l = document.createElement('link');
+ // Use link rel=preload to try to get the browser to cache the opaque
+ // response.
+ l.setAttribute('rel', 'preload');
+ l.setAttribute('href', URL);
+ l.setAttribute('as', 'fetch');
+ l.onerror = function() {
+ parent.done('FAIL: preload failed unexpectedly');
+ };
+ document.body.appendChild(l);
+ xhr = new XMLHttpRequest;
+ xhr.withCredentials = true;
+ xhr.open('GET', URL);
+ // opaque-response returns an opaque response from serviceworker and thus
+ // the XHR must fail because it is not no-cors request.
+ // Particularly, the XHR must not reuse the opaque response from the
+ // preload request.
+ xhr.onerror = function() {
+ parent.done('PASS');
+ };
+ xhr.onload = function() {
+ parent.done('FAIL: ' + xhr.responseText);
+ };
+ xhr.send();
+}
+</script>
+<body onload="setTimeout(runTest, 100)"></body>
diff --git a/test/wpt/tests/service-workers/service-worker/resources/opaque-response-preloaded-worker.js b/test/wpt/tests/service-workers/service-worker/resources/opaque-response-preloaded-worker.js
new file mode 100644
index 0000000..4fbe35d
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/opaque-response-preloaded-worker.js
@@ -0,0 +1,12 @@
+importScripts('/common/get-host-info.sub.js');
+
+var remoteUrl = get_host_info()['HTTPS_REMOTE_ORIGIN'] +
+ '/service-workers/service-worker/resources/sample.js'
+
+self.addEventListener('fetch', event => {
+ if (!event.request.url.match(/opaque-response\?from=/)) {
+ return;
+ }
+
+ event.respondWith(fetch(remoteUrl, {mode: 'no-cors'}));
+ });
diff --git a/test/wpt/tests/service-workers/service-worker/resources/opaque-response-preloaded-xhr.html b/test/wpt/tests/service-workers/service-worker/resources/opaque-response-preloaded-xhr.html
new file mode 100644
index 0000000..f31ac9b
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/opaque-response-preloaded-xhr.html
@@ -0,0 +1,35 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<body></body>
+<script>
+const URL = 'opaque-response?from=opaque-response-preloaded-xhr.html';
+function runTest() {
+ var l = document.createElement('link');
+ // Use link rel=preload to try to get the browser to cache the opaque
+ // response.
+ l.setAttribute('rel', 'preload');
+ l.setAttribute('href', URL);
+ l.setAttribute('as', 'fetch');
+ l.onload = function() {
+ xhr = new XMLHttpRequest;
+ xhr.withCredentials = true;
+ xhr.open('GET', URL);
+ // opaque-response returns an opaque response from serviceworker and thus
+ // the XHR must fail because it is not no-cors request.
+ // Particularly, the XHR must not reuse the opaque response from the
+ // preload request.
+ xhr.onerror = function() {
+ parent.done('PASS');
+ };
+ xhr.onload = function() {
+ parent.done('FAIL: ' + xhr.responseText);
+ };
+ xhr.send();
+ };
+ l.onerror = function() {
+ parent.done('FAIL: preload failed unexpectedly');
+ };
+ document.body.appendChild(l);
+}
+</script>
+<body onload="setTimeout(runTest, 100)"></body>
diff --git a/test/wpt/tests/service-workers/service-worker/resources/opaque-script-frame.html b/test/wpt/tests/service-workers/service-worker/resources/opaque-script-frame.html
new file mode 100644
index 0000000..a57aace
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/opaque-script-frame.html
@@ -0,0 +1,21 @@
+<!doctype html>
+<html>
+<head>
+<meta charset="utf-8">
+</head>
+<body>
+<script>
+self.addEventListener('error', evt => {
+ self.parent.postMessage({ type: 'ErrorEvent', msg: evt.message }, '*');
+});
+
+const el = document.createElement('script');
+const params = new URLSearchParams(self.location.search);
+el.src = params.get('script');
+el.addEventListener('load', evt => {
+ runScript();
+});
+document.body.appendChild(el);
+</script>
+</body>
+</html>
diff --git a/test/wpt/tests/service-workers/service-worker/resources/opaque-script-large.js b/test/wpt/tests/service-workers/service-worker/resources/opaque-script-large.js
new file mode 100644
index 0000000..7e1c598
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/opaque-script-large.js
@@ -0,0 +1,41 @@
+function runScript() {
+ throw new Error("Intentional error.");
+}
+
+function unused() {
+ // The following string is intended to be relatively large since some
+ // browsers trigger different code paths based on script size.
+ return "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec a " +
+ "tortor ut orci bibendum blandit non quis diam. Aenean sit amet " +
+ "urna sit amet neque malesuada ultricies at vel nisi. Nunc et lacus " +
+ "est. Nam posuere erat enim, ac fringilla purus pellentesque " +
+ "cursus. Proin sodales eleifend lorem, eu semper massa scelerisque " +
+ "ac. Maecenas pharetra leo malesuada vulputate vulputate. Sed at " +
+ "efficitur odio. In rhoncus neque varius nibh efficitur gravida. " +
+ "Curabitur vitae dolor enim. Mauris semper lobortis libero sed " +
+ "congue. Donec felis ante, fringilla eget urna ut, finibus " +
+ "hendrerit lacus. Donec at interdum diam. Proin a neque vitae diam " +
+ "egestas euismod. Mauris posuere elementum lorem, eget convallis " +
+ "nisl elementum et. In ut leo ac neque dapibus pharetra quis ac " +
+ "velit. Integer pretium lectus non urna vulputate, in interdum mi " +
+ "lobortis. Sed laoreet ex et metus pharetra blandit. Curabitur " +
+ "sollicitudin non neque eu varius. Phasellus posuere congue arcu, " +
+ "in aliquam nunc fringilla a. Morbi id facilisis libero. Phasellus " +
+ "metus. Lorem ipsum dolor sit amet, consectetur adipiscing elit. " +
+ "tortor ut orci bibendum blandit non quis diam. Aenean sit amet " +
+ "urna sit amet neque malesuada ultricies at vel nisi. Nunc et lacus " +
+ "est. Nam posuere erat enim, ac fringilla purus pellentesque " +
+ "cursus. Proin sodales eleifend lorem, eu semper massa scelerisque " +
+ "ac. Maecenas pharetra leo malesuada vulputate vulputate. Sed at " +
+ "efficitur odio. In rhoncus neque varius nibh efficitur gravida. " +
+ "Curabitur vitae dolor enim. Mauris semper lobortis libero sed " +
+ "congue. Donec felis ante, fringilla eget urna ut, finibus " +
+ "hendrerit lacus. Donec at interdum diam. Proin a neque vitae diam " +
+ "egestas euismod. Mauris posuere elementum lorem, eget convallis " +
+ "nisl elementum et. In ut leo ac neque dapibus pharetra quis ac " +
+ "velit. Integer pretium lectus non urna vulputate, in interdum mi " +
+ "lobortis. Sed laoreet ex et metus pharetra blandit. Curabitur " +
+ "sollicitudin non neque eu varius. Phasellus posuere congue arcu, " +
+ "in aliquam nunc fringilla a. Morbi id facilisis libero. Phasellus " +
+ "metus.";
+}
diff --git a/test/wpt/tests/service-workers/service-worker/resources/opaque-script-small.js b/test/wpt/tests/service-workers/service-worker/resources/opaque-script-small.js
new file mode 100644
index 0000000..8b89098
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/opaque-script-small.js
@@ -0,0 +1,3 @@
+function runScript() {
+ throw new Error("Intentional error.");
+}
diff --git a/test/wpt/tests/service-workers/service-worker/resources/opaque-script-sw.js b/test/wpt/tests/service-workers/service-worker/resources/opaque-script-sw.js
new file mode 100644
index 0000000..4d882c6
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/opaque-script-sw.js
@@ -0,0 +1,37 @@
+importScripts('test-helpers.sub.js');
+importScripts('/common/get-host-info.sub.js');
+
+const NAME = 'foo';
+const SAME_ORIGIN_BASE = new URL('./', self.location.href).href;
+const CROSS_ORIGIN_BASE = new URL('./',
+ get_host_info().HTTPS_REMOTE_ORIGIN + base_path()).href;
+
+const urls = [
+ `${SAME_ORIGIN_BASE}opaque-script-small.js`,
+ `${SAME_ORIGIN_BASE}opaque-script-large.js`,
+ `${CROSS_ORIGIN_BASE}opaque-script-small.js`,
+ `${CROSS_ORIGIN_BASE}opaque-script-large.js`,
+];
+
+self.addEventListener('install', evt => {
+ evt.waitUntil(async function() {
+ const c = await caches.open(NAME);
+ const promises = urls.map(async function(u) {
+ const r = await fetch(u, { mode: 'no-cors' });
+ await c.put(u, r);
+ });
+ await Promise.all(promises);
+ }());
+});
+
+self.addEventListener('fetch', evt => {
+ const url = new URL(evt.request.url);
+ if (!url.pathname.includes('opaque-script-small.js') &&
+ !url.pathname.includes('opaque-script-large.js')) {
+ return;
+ }
+ evt.respondWith(async function() {
+ const c = await caches.open(NAME);
+ return c.match(evt.request);
+ }());
+});
diff --git a/test/wpt/tests/service-workers/service-worker/resources/other.html b/test/wpt/tests/service-workers/service-worker/resources/other.html
new file mode 100644
index 0000000..b9f3504
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/other.html
@@ -0,0 +1,3 @@
+<!DOCTYPE html>
+<title>Other</title>
+Here's an other html file.
diff --git a/test/wpt/tests/service-workers/service-worker/resources/override_assert_object_equals.js b/test/wpt/tests/service-workers/service-worker/resources/override_assert_object_equals.js
new file mode 100644
index 0000000..835046d
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/override_assert_object_equals.js
@@ -0,0 +1,58 @@
+// .body attribute of Request and Response object are experimental feture. It is
+// enabled when --enable-experimental-web-platform-features flag is set.
+// Touching this attribute can change the behavior of the objects. To avoid
+// touching it while comparing the objects in LayoutTest, we overwrite
+// assert_object_equals method.
+
+(function() {
+ var original_assert_object_equals = self.assert_object_equals;
+ function _brand(object) {
+ return Object.prototype.toString.call(object).match(/^\[object (.*)\]$/)[1];
+ }
+ var assert_request_equals = function(actual, expected, prefix) {
+ if (typeof actual !== 'object') {
+ assert_equals(actual, expected, prefix);
+ return;
+ }
+ assert_true(actual instanceof Request, prefix);
+ assert_true(expected instanceof Request, prefix);
+ assert_equals(actual.bodyUsed, expected.bodyUsed, prefix + '.bodyUsed');
+ assert_equals(actual.method, expected.method, prefix + '.method');
+ assert_equals(actual.url, expected.url, prefix + '.url');
+ original_assert_object_equals(actual.headers, expected.headers,
+ prefix + '.headers');
+ assert_equals(actual.context, expected.context, prefix + '.context');
+ assert_equals(actual.referrer, expected.referrer, prefix + '.referrer');
+ assert_equals(actual.mode, expected.mode, prefix + '.mode');
+ assert_equals(actual.credentials, expected.credentials,
+ prefix + '.credentials');
+ assert_equals(actual.cache, expected.cache, prefix + '.cache');
+ };
+ var assert_response_equals = function(actual, expected, prefix) {
+ if (typeof actual !== 'object') {
+ assert_equals(actual, expected, prefix);
+ return;
+ }
+ assert_true(actual instanceof Response, prefix);
+ assert_true(expected instanceof Response, prefix);
+ assert_equals(actual.bodyUsed, expected.bodyUsed, prefix + '.bodyUsed');
+ assert_equals(actual.type, expected.type, prefix + '.type');
+ assert_equals(actual.url, expected.url, prefix + '.url');
+ assert_equals(actual.status, expected.status, prefix + '.status');
+ assert_equals(actual.statusText, expected.statusText,
+ prefix + '.statusText');
+ original_assert_object_equals(actual.headers, expected.headers,
+ prefix + '.headers');
+ };
+ var assert_object_equals = function(actual, expected, description) {
+ var prefix = (description ? description + ': ' : '') + _brand(expected);
+ if (expected instanceof Request) {
+ assert_request_equals(actual, expected, prefix);
+ } else if (expected instanceof Response) {
+ assert_response_equals(actual, expected, prefix);
+ } else {
+ original_assert_object_equals(actual, expected, description);
+ }
+ };
+ self.assert_object_equals = assert_object_equals;
+})();
diff --git a/test/wpt/tests/service-workers/service-worker/resources/partitioned-cookies-3p-credentialless-frame.html b/test/wpt/tests/service-workers/service-worker/resources/partitioned-cookies-3p-credentialless-frame.html
new file mode 100644
index 0000000..25ddf60
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/partitioned-cookies-3p-credentialless-frame.html
@@ -0,0 +1,114 @@
+<!DOCTYPE html>
+<head>
+<meta charset="utf-8"/>
+<meta name="timeout" content="long">
+<title>Service Worker: Partitioned Cookies 3P Credentialless Iframe</title>
+<script src="/resources/testharness.js"></script>
+<script src="test-helpers.sub.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+</head>
+
+<body>
+<script>
+
+// Check workers registered by a credentialless frame can access cookies set in that frame.
+promise_test(async t => {
+ const script = './partitioned-cookies-3p-sw.js';
+ const scope = './partitioned-cookies-3p-';
+ const absolute_scope = new URL(scope, window.location).href;
+
+ // Set a Partitioned cookie.
+ document.cookie = '__Host-partitioned=123; Secure; Path=/; SameSite=None; Partitioned;';
+ assert_true(document.cookie.includes('__Host-partitioned=123'));
+
+ // Make sure DOM cannot access the unpartitioned cookie.
+ assert_false(document.cookie.includes('unpartitioned=456'));
+
+ const reg = await service_worker_unregister_and_register(t, script, scope);
+ await wait_for_state(t, reg.installing, 'activated');
+
+ let retrieved_registrations =
+ await navigator.serviceWorker.getRegistrations();
+ let filtered_registrations =
+ retrieved_registrations.filter(reg => reg.scope == absolute_scope);
+
+ // on_message will be reassigned below based on the expected reply from the service worker.
+ let on_message;
+ self.addEventListener('message', ev => on_message(ev));
+ navigator.serviceWorker.addEventListener('message', evt => {
+ self.postMessage(evt.data, '*');
+ });
+
+ // First test that the worker script started correctly and message passing is enabled.
+ let resolve_wait_promise;
+ let wait_promise = new Promise(resolve => {
+ resolve_wait_promise = resolve;
+ });
+ let got;
+ on_message = ev => {
+ got = ev.data;
+ resolve_wait_promise();
+ };
+ filtered_registrations[0].active.postMessage({type: 'test_message'});
+ await wait_promise;
+ assert_true(got.ok, 'Message passing');
+
+ // Test that the partitioned cookie is available to this worker via CookieStore API.
+ wait_promise = new Promise(resolve => {
+ resolve_wait_promise = resolve;
+ });
+ on_message = ev => {
+ got = ev.data;
+ resolve_wait_promise();
+ };
+ filtered_registrations[0].active.postMessage({type: 'echo_cookies_js'});
+ await wait_promise;
+ assert_true(got.ok, 'Get cookies');
+ assert_true(
+ got.cookies.includes('__Host-partitioned'),
+ 'Credentialless frame worker can access partitioned cookie via JS');
+ assert_false(
+ got.cookies.includes('unpartitioned'),
+ 'Credentialless frame worker cannot access unpartitioned cookie via JS');
+
+ // Test that the partitioned cookie is available to this worker via HTTP.
+ wait_promise = new Promise(resolve => {
+ resolve_wait_promise = resolve;
+ });
+ on_message = ev => {
+ got = ev.data;
+ resolve_wait_promise();
+ };
+ filtered_registrations[0].active.postMessage({ type: 'echo_cookies_http' });
+ await wait_promise;
+ assert_true(got.ok, 'Get cookies');
+ assert_true(
+ got.cookies.includes('__Host-partitioned'),
+ 'Credentialless frame worker can access partitioned cookie via HTTP');
+ assert_false(
+ got.cookies.includes('unpartitioned'),
+ 'Credentialless frame worker cannot access unpartitioned cookie via HTTP');
+
+ // Test that the partitioned cookie is not available to this worker in HTTP
+ // requests from importScripts.
+ wait_promise = new Promise(resolve => {
+ resolve_wait_promise = resolve;
+ });
+ on_message = ev => {
+ got = ev.data;
+ resolve_wait_promise();
+ };
+ filtered_registrations[0].active.postMessage({ type: 'echo_cookies_import' });
+ await wait_promise;
+ assert_true(got.ok, 'Get cookies');
+ assert_true(
+ got.cookies.includes('__Host-partitioned'),
+ 'Credentialless frame worker can access partitioned cookie via importScripts');
+ assert_false(
+ got.cookies.includes('unpartitioned'),
+ 'Credentialless frame worker cannot access unpartitioned cookie via importScripts');
+});
+
+</script>
+</body>
+</html>
diff --git a/test/wpt/tests/service-workers/service-worker/resources/partitioned-cookies-3p-frame.html b/test/wpt/tests/service-workers/service-worker/resources/partitioned-cookies-3p-frame.html
new file mode 100644
index 0000000..00b3412
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/partitioned-cookies-3p-frame.html
@@ -0,0 +1,108 @@
+<!DOCTYPE html>
+<head>
+<meta charset="utf-8"/>
+<meta name="timeout" content="long">
+<title>Service Worker: Partitioned Cookies 3P Iframe</title>
+<script src="/resources/testharness.js"></script>
+<script src="test-helpers.sub.js"></script>
+</head>
+
+<body>
+<script>
+
+promise_test(async t => {
+ const script = './partitioned-cookies-3p-sw.js';
+ const scope = './partitioned-cookies-3p-';
+ const absolute_scope = new URL(scope, window.location).href;
+
+ assert_false(document.cookie.includes('__Host-partitioned=123'), 'DOM cannot access partitioned cookie');
+ assert_true(document.cookie.includes('unpartitioned=456'), 'DOM can access unpartitioned cookie');
+
+ const reg = await service_worker_unregister_and_register(t, script, scope);
+ await wait_for_state(t, reg.installing, 'activated');
+
+ let retrieved_registrations =
+ await navigator.serviceWorker.getRegistrations();
+ let filtered_registrations =
+ retrieved_registrations.filter(reg => reg.scope == absolute_scope);
+
+ // on_message will be reassigned below based on the expected reply from the service worker.
+ let on_message;
+ self.addEventListener('message', ev => on_message(ev));
+ navigator.serviceWorker.addEventListener('message', evt => {
+ self.postMessage(evt.data, '*');
+ });
+
+ // First test that the worker script started correctly and message passing is enabled.
+ let resolve_wait_promise;
+ let wait_promise = new Promise(resolve => {
+ resolve_wait_promise = resolve;
+ });
+ let got;
+ on_message = ev => {
+ got = ev.data;
+ resolve_wait_promise();
+ };
+ filtered_registrations[0].active.postMessage({type: 'test_message'});
+ await wait_promise;
+ assert_true(got.ok, 'Message passing');
+
+ // Test that the partitioned cookie is not available to this worker via HTTP.
+ wait_promise = new Promise(resolve => {
+ resolve_wait_promise = resolve;
+ });
+ on_message = ev => {
+ got = ev.data;
+ resolve_wait_promise();
+ };
+ filtered_registrations[0].active.postMessage({type: 'echo_cookies_http'});
+ await wait_promise;
+ assert_true(got.ok, 'Get cookies');
+ assert_false(
+ got.cookies.includes('__Host-partitioned'),
+ 'Worker cannot access partitioned cookie via HTTP');
+ assert_true(
+ got.cookies.includes('unpartitioned'),
+ 'Worker can access unpartitioned cookie via HTTP');
+
+ // Test that the partitioned cookie is not available to this worker via CookieStore API.
+ wait_promise = new Promise(resolve => {
+ resolve_wait_promise = resolve;
+ });
+ on_message = ev => {
+ got = ev.data;
+ resolve_wait_promise();
+ };
+ filtered_registrations[0].active.postMessage({type: 'echo_cookies_js'});
+ await wait_promise;
+ assert_true(got.ok, 'Get cookies');
+ assert_false(
+ got.cookies.includes('__Host-partitioned'),
+ 'Worker cannot access partitioned cookie via JS');
+ assert_true(
+ got.cookies.includes('unpartitioned'),
+ 'Worker can access unpartitioned cookie via JS');
+
+ // Test that the partitioned cookie is not available to this worker in HTTP
+ // requests from importScripts.
+ wait_promise = new Promise(resolve => {
+ resolve_wait_promise = resolve;
+ });
+ on_message = ev => {
+ got = ev.data;
+ resolve_wait_promise();
+ };
+ filtered_registrations[0].active.postMessage({type: 'echo_cookies_import'});
+ await wait_promise;
+ assert_true(got.ok, 'Get cookies');
+ assert_false(
+ got.cookies.includes('__Host-partitioned'),
+ 'Worker cannot access partitioned cookie via importScripts');
+ assert_true(
+ got.cookies.includes('unpartitioned'),
+ 'Worker can access unpartitioned cookie via importScripts');
+});
+
+</script>
+</body>
+</html>
diff --git a/test/wpt/tests/service-workers/service-worker/resources/partitioned-cookies-3p-sw.js b/test/wpt/tests/service-workers/service-worker/resources/partitioned-cookies-3p-sw.js
new file mode 100644
index 0000000..767dbf4
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/partitioned-cookies-3p-sw.js
@@ -0,0 +1,53 @@
+self.addEventListener('message', ev => ev.waitUntil(onMessage(ev)));
+
+async function onMessage(event) {
+ if (!event.data)
+ return;
+ switch (event.data.type) {
+ case 'test_message':
+ return onTestMessage(event);
+ case 'echo_cookies_http':
+ return onEchoCookiesHttp(event);
+ case 'echo_cookies_js':
+ return onEchoCookiesJs(event);
+ case 'echo_cookies_import':
+ return onEchoCookiesImport(event);
+ default:
+ return;
+ }
+}
+
+// test_message just verifies that the message passing is working.
+async function onTestMessage(event) {
+ event.source.postMessage({ok: true});
+}
+
+async function onEchoCookiesHttp(event) {
+ try {
+ const resp = await fetch(
+ `${self.origin}/cookies/resources/list.py`, {credentials: 'include'});
+ const cookies = await resp.json();
+ event.source.postMessage({ok: true, cookies: Object.keys(cookies)});
+ } catch (err) {
+ event.source.postMessage({ok: false});
+ }
+}
+
+// echo_cookies returns the names of all of the cookies available to the worker.
+async function onEchoCookiesJs(event) {
+ try {
+ const cookie_objects = await self.cookieStore.getAll();
+ const cookies = cookie_objects.map(c => c.name);
+ event.source.postMessage({ok: true, cookies});
+ } catch (err) {
+ event.source.postMessage({ok: false});
+ }
+}
+
+// Sets `self._cookies` variable, array of the names of cookies available to
+// the request.
+importScripts(`${self.origin}/cookies/resources/list-cookies-for-script.py`);
+
+function onEchoCookiesImport(event) {
+ event.source.postMessage({ok: true, cookies: self._cookies});
+}
diff --git a/test/wpt/tests/service-workers/service-worker/resources/partitioned-cookies-3p-window.html b/test/wpt/tests/service-workers/service-worker/resources/partitioned-cookies-3p-window.html
new file mode 100644
index 0000000..40d38b3
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/partitioned-cookies-3p-window.html
@@ -0,0 +1,35 @@
+<!DOCTYPE html>
+<head>
+<meta charset="utf-8"/>
+<meta name="timeout" content="long">
+<title>Service Worker: Partitioned Cookies 3P Window</title>
+<script src="/resources/testharness.js"></script>
+</head>
+
+<body>
+<script>
+
+promise_test(async t => {
+ assert_true(
+ location.search.includes('origin='), 'First party origin passed');
+ const first_party_origin = decodeURIComponent(
+ location.search.split('origin=')[1]);
+ const iframe = document.createElement('iframe');
+ iframe.src = new URL(
+ './partitioned-cookies-3p-frame.html',
+ first_party_origin + location.pathname).href;
+ document.body.appendChild(iframe);
+ await fetch_tests_from_window(iframe.contentWindow);
+
+ const credentialless_frame = document.createElement('iframe');
+ credentialless_frame.credentialless = true;
+ credentialless_frame.src = new URL(
+ './partitioned-cookies-3p-credentialless-frame.html',
+ first_party_origin + location.pathname).href;
+ document.body.appendChild(credentialless_frame);
+ await fetch_tests_from_window(credentialless_frame.contentWindow);
+});
+
+</script>
+</body>
+</html>
diff --git a/test/wpt/tests/service-workers/service-worker/resources/partitioned-cookies-sw.js b/test/wpt/tests/service-workers/service-worker/resources/partitioned-cookies-sw.js
new file mode 100644
index 0000000..767dbf4
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/partitioned-cookies-sw.js
@@ -0,0 +1,53 @@
+self.addEventListener('message', ev => ev.waitUntil(onMessage(ev)));
+
+async function onMessage(event) {
+ if (!event.data)
+ return;
+ switch (event.data.type) {
+ case 'test_message':
+ return onTestMessage(event);
+ case 'echo_cookies_http':
+ return onEchoCookiesHttp(event);
+ case 'echo_cookies_js':
+ return onEchoCookiesJs(event);
+ case 'echo_cookies_import':
+ return onEchoCookiesImport(event);
+ default:
+ return;
+ }
+}
+
+// test_message just verifies that the message passing is working.
+async function onTestMessage(event) {
+ event.source.postMessage({ok: true});
+}
+
+async function onEchoCookiesHttp(event) {
+ try {
+ const resp = await fetch(
+ `${self.origin}/cookies/resources/list.py`, {credentials: 'include'});
+ const cookies = await resp.json();
+ event.source.postMessage({ok: true, cookies: Object.keys(cookies)});
+ } catch (err) {
+ event.source.postMessage({ok: false});
+ }
+}
+
+// echo_cookies returns the names of all of the cookies available to the worker.
+async function onEchoCookiesJs(event) {
+ try {
+ const cookie_objects = await self.cookieStore.getAll();
+ const cookies = cookie_objects.map(c => c.name);
+ event.source.postMessage({ok: true, cookies});
+ } catch (err) {
+ event.source.postMessage({ok: false});
+ }
+}
+
+// Sets `self._cookies` variable, array of the names of cookies available to
+// the request.
+importScripts(`${self.origin}/cookies/resources/list-cookies-for-script.py`);
+
+function onEchoCookiesImport(event) {
+ event.source.postMessage({ok: true, cookies: self._cookies});
+}
diff --git a/test/wpt/tests/service-workers/service-worker/resources/partitioned-service-worker-iframe-claim.html b/test/wpt/tests/service-workers/service-worker/resources/partitioned-service-worker-iframe-claim.html
new file mode 100644
index 0000000..12b048e
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/partitioned-service-worker-iframe-claim.html
@@ -0,0 +1,59 @@
+<!DOCTYPE html>
+<title>Service Worker: 3P iframe for partitioned service workers</title>
+<script src="./test-helpers.sub.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="./partitioned-utils.js"></script>
+
+<body>
+ <script>
+ // 1p mode will respond to requests for its current controller and
+ // postMessage when its controller changes.
+ async function onLoad1pMode(){
+ self.addEventListener('message', evt => {
+ if(!evt.data)
+ return;
+
+ if (evt.data.type === "get-controller") {
+ window.parent.postMessage({controller: navigator.serviceWorker.controller});
+ }
+ });
+
+ navigator.serviceWorker.addEventListener('controllerchange', evt => {
+ window.parent.postMessage({status: "success", context: "1p"}, '*');
+ });
+ }
+
+ // 3p mode will tell its SW to claim and then postMessage its results
+ // automatically.
+ async function onLoad3pMode() {
+ reg = await setupServiceWorker();
+
+ if(navigator.serviceWorker.controller != null){
+ //This iframe is already under control of a service worker, testing for
+ // a controller change will timeout. Return a failure.
+ window.parent.postMessage({status: "failure", context: "3p"}, '*');
+ return;
+ }
+
+ // Once this client is claimed, let the test know.
+ navigator.serviceWorker.addEventListener('controllerchange', evt => {
+ window.parent.postMessage({status: "success", context: "3p"}, '*');
+ });
+
+ // Trigger the SW to claim.
+ reg.active.postMessage({type: "claim"});
+
+ }
+
+ const request_url = new URL(window.location.href);
+ var url_search = request_url.search.substr(1);
+
+ if(url_search == "1p-mode") {
+ self.addEventListener('load', onLoad1pMode);
+ }
+ else if(url_search == "3p-mode") {
+ self.addEventListener('load', onLoad3pMode);
+ }
+ // Else do nothing.
+ </script>
+</body> \ No newline at end of file
diff --git a/test/wpt/tests/service-workers/service-worker/resources/partitioned-service-worker-nested-iframe-child.html b/test/wpt/tests/service-workers/service-worker/resources/partitioned-service-worker-nested-iframe-child.html
new file mode 100644
index 0000000..d05fef4
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/partitioned-service-worker-nested-iframe-child.html
@@ -0,0 +1,44 @@
+<!DOCTYPE html>
+<title>Service Worker: Innermost nested iframe for partitioned service workers</title>
+<script src="./test-helpers.sub.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="./partitioned-utils.js"></script>
+
+<body>
+Innermost 1p iframe (A2) with 3p ancestor (A1-B-A2-A3): this iframe will
+register a service worker when it loads and then add its own iframe (A3) that
+will attempt to navigate to a url. ServiceWorker will intercept this navigation
+and resolve the ServiceWorker's internal Promise. When
+ThirdPartyStoragePartitioning is enabled, this iframe should be partitioned
+from the main frame and should not share a ServiceWorker.
+<script>
+
+async function onLoad() {
+ // Set-up the ServiceWorker for this iframe, defined in:
+ // service-workers/service-worker/resources/partitioned-utils.js
+ await setupServiceWorker();
+
+ // When the SW's iframe finishes it'll post a message. This forwards
+ // it up to the middle-iframe.
+ self.addEventListener('message', evt => {
+ window.parent.postMessage(evt.data, '*');
+ });
+
+ // Now that we have set up the ServiceWorker, we need it to
+ // intercept a navigation that will resolve its promise.
+ // To do this, we create an additional iframe to send that
+ // navigation request to resolve (`resolve.fakehtml`). If we're
+ // partitioned then there shouldn't be a promise to resolve. Defined
+ // in: service-workers/service-worker/resources/partitioned-storage-sw.js
+ const resolve_frame_url = new URL('./partitioned-resolve.fakehtml?FromNestedFrame', self.location);
+ const frame_resolve = await new Promise(resolve => {
+ var frame = document.createElement('iframe');
+ frame.src = resolve_frame_url;
+ frame.onload = function() { resolve(frame); };
+ document.body.appendChild(frame);
+ });
+}
+
+self.addEventListener('load', onLoad);
+</script>
+</body> \ No newline at end of file
diff --git a/test/wpt/tests/service-workers/service-worker/resources/partitioned-service-worker-nested-iframe-parent.html b/test/wpt/tests/service-workers/service-worker/resources/partitioned-service-worker-nested-iframe-parent.html
new file mode 100644
index 0000000..f748e2f
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/partitioned-service-worker-nested-iframe-parent.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<title>Service Worker: Middle nested iframe for partitioned service workers</title>
+<script src="./test-helpers.sub.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="./partitioned-utils.js"></script>
+
+<body>
+Middle of the nested iframes (3p ancestor or B in A1-B-A2).
+<script>
+
+async function onLoad() {
+ // The innermost iframe will recieve a message from the
+ // ServiceWorker and pass it to this iframe. We need to
+ // then pass that message to the main frame to complete
+ // the test.
+ self.addEventListener('message', evt => {
+ window.parent.postMessage(evt.data, '*');
+ });
+
+ // Embed the innermost iframe and set-up the service worker there.
+ const innermost_iframe_url = new URL('./partitioned-service-worker-nested-iframe-child.html',
+ get_host_info().HTTPS_ORIGIN + self.location.pathname);
+ var frame = document.createElement('iframe');
+ frame.src = innermost_iframe_url;
+ document.body.appendChild(frame);
+}
+
+self.addEventListener('load', onLoad);
+</script>
+</body> \ No newline at end of file
diff --git a/test/wpt/tests/service-workers/service-worker/resources/partitioned-service-worker-third-party-iframe-getRegistrations.html b/test/wpt/tests/service-workers/service-worker/resources/partitioned-service-worker-third-party-iframe-getRegistrations.html
new file mode 100644
index 0000000..747c058
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/partitioned-service-worker-third-party-iframe-getRegistrations.html
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<title>Service Worker: 3P iframe for partitioned service workers</title>
+<script src="./test-helpers.sub.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="./partitioned-utils.js"></script>
+
+<body>
+ This iframe will register a service worker when it loads and then will use
+ getRegistrations to get a handle to the SW. It will then postMessage to the
+ SW to retrieve the SW's ID. This iframe will then forward that message up,
+ eventually, to the test.
+ <script>
+
+ async function onLoad() {
+ const scope = './partitioned-'
+ const absoluteScope = new URL(scope, window.location).href;
+
+ await setupServiceWorker();
+
+ // Once the SW sends us its ID, forward it up to the window.
+ navigator.serviceWorker.addEventListener('message', evt => {
+ window.parent.postMessage(evt.data, '*');
+ });
+
+ // Now get the SW with getRegistrations.
+ const retrieved_registrations =
+ await navigator.serviceWorker.getRegistrations();
+
+ // It's possible that other tests have left behind other service workers.
+ // This steps filters those other SWs out.
+ const filtered_registrations =
+ retrieved_registrations.filter(reg => reg.scope == absoluteScope);
+
+ filtered_registrations[0].active.postMessage({type: "get-id"});
+
+ }
+
+ self.addEventListener('load', onLoad);
+ </script>
+</body> \ No newline at end of file
diff --git a/test/wpt/tests/service-workers/service-worker/resources/partitioned-service-worker-third-party-iframe-matchAll.html b/test/wpt/tests/service-workers/service-worker/resources/partitioned-service-worker-third-party-iframe-matchAll.html
new file mode 100644
index 0000000..7a2c366
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/partitioned-service-worker-third-party-iframe-matchAll.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<title>Service Worker: 3P iframe for partitioned service workers</title>
+<script src="./test-helpers.sub.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="./partitioned-utils.js"></script>
+
+<body>
+ This iframe will register a service worker when it loads and then will use
+ getRegistrations to get a handle to the SW. It will then postMessage to the
+ SW to get the SW's clients via matchAll(). This iframe will then forward the
+ SW's response up, eventually, to the test.
+ <script>
+ async function onLoad() {
+ reg = await setupServiceWorker();
+
+ // Once the SW sends us its ID, forward it up to the window.
+ navigator.serviceWorker.addEventListener('message', evt => {
+ window.parent.postMessage(evt.data, '*');
+ });
+
+ reg.active.postMessage({type: "get-match-all"});
+
+ }
+
+ self.addEventListener('load', onLoad);
+ </script>
+</body> \ No newline at end of file
diff --git a/test/wpt/tests/service-workers/service-worker/resources/partitioned-service-worker-third-party-iframe.html b/test/wpt/tests/service-workers/service-worker/resources/partitioned-service-worker-third-party-iframe.html
new file mode 100644
index 0000000..1b7f671
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/partitioned-service-worker-third-party-iframe.html
@@ -0,0 +1,36 @@
+<!DOCTYPE html>
+<title>Service Worker: 3P iframe for partitioned service workers</title>
+<script src="./test-helpers.sub.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="./partitioned-utils.js"></script>
+
+
+<body>
+This iframe will register a service worker when it loads and then add its own
+iframe that will attempt to navigate to a url that service worker will intercept
+and use to resolve the service worker's internal Promise.
+<script>
+
+async function onLoad() {
+ await setupServiceWorker();
+
+ // When the SW's iframe finishes it'll post a message. This forwards it up to
+ // the window.
+ self.addEventListener('message', evt => {
+ window.parent.postMessage(evt.data, '*');
+ });
+
+ // Now try to resolve the SW's promise. If we're partitioned then there
+ // shouldn't be a promise to resolve.
+ const resolve_frame_url = new URL('./partitioned-resolve.fakehtml?From3pFrame', self.location);
+ const frame_resolve = await new Promise(resolve => {
+ var frame = document.createElement('iframe');
+ frame.src = resolve_frame_url;
+ frame.onload = function() { resolve(frame); };
+ document.body.appendChild(frame);
+ });
+}
+
+self.addEventListener('load', onLoad);
+</script>
+</body> \ No newline at end of file
diff --git a/test/wpt/tests/service-workers/service-worker/resources/partitioned-service-worker-third-party-window.html b/test/wpt/tests/service-workers/service-worker/resources/partitioned-service-worker-third-party-window.html
new file mode 100644
index 0000000..86384ce
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/partitioned-service-worker-third-party-window.html
@@ -0,0 +1,41 @@
+<!DOCTYPE html>
+<title>Service Worker: 3P window for partitioned service workers</title>
+<script src="./test-helpers.sub.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+
+
+<body>
+This page should be opened as a third-party window. It then loads an iframe
+specified by the query parameter. Finally it forwards the postMessage from the
+iframe up to the opener (the test).
+
+<script>
+
+async function onLoad() {
+ const message_promise = new Promise(resolve => {
+ self.addEventListener('message', evt => {
+ resolve(evt.data);
+ });
+ });
+
+ const search_param = new URLSearchParams(window.location.search);
+ const iframe_url = search_param.get('target');
+
+ var frame = document.createElement('iframe');
+ frame.src = iframe_url;
+ frame.style.position = 'absolute';
+ document.body.appendChild(frame);
+
+
+ await message_promise.then(data => {
+ // We're done, forward the message and clean up.
+ window.opener.postMessage(data, '*');
+
+ frame.remove();
+ });
+}
+
+self.addEventListener('load', onLoad);
+
+</script>
+</body> \ No newline at end of file
diff --git a/test/wpt/tests/service-workers/service-worker/resources/partitioned-storage-sw.js b/test/wpt/tests/service-workers/service-worker/resources/partitioned-storage-sw.js
new file mode 100644
index 0000000..00f7979
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/partitioned-storage-sw.js
@@ -0,0 +1,81 @@
+// Holds the promise that the "resolve.fakehtml" call attempts to resolve.
+// This is "the SW's promise" that other parts of the test refer to.
+var promise;
+// Stores the resolve funcution for the current promise.
+var pending_resolve_func = null;
+// Unique ID to determine which service worker is being used.
+const ID = Math.random();
+
+function callAndResetResolve() {
+ var local_resolve = pending_resolve_func;
+ pending_resolve_func = null;
+ local_resolve();
+}
+
+self.addEventListener('fetch', function(event) {
+ fetchEventHandler(event);
+})
+
+self.addEventListener('message', (event) => {
+ event.waitUntil(async function() {
+ if(!event.data)
+ return;
+
+ if (event.data.type === "get-id") {
+ event.source.postMessage({ID: ID});
+ }
+ else if(event.data.type === "get-match-all") {
+ clients.matchAll({includeUncontrolled: true}).then(clients_list => {
+ const url_list = clients_list.map(item => item.url);
+ event.source.postMessage({urls_list: url_list});
+ });
+ }
+ else if(event.data.type === "claim") {
+ await clients.claim();
+ }
+ }());
+});
+
+async function fetchEventHandler(event){
+ var request_url = new URL(event.request.url);
+ var url_search = request_url.search.substr(1);
+ request_url.search = "";
+ if ( request_url.href.endsWith('waitUntilResolved.fakehtml') ) {
+
+ if (pending_resolve_func != null) {
+ // Respond with an error if there is already a pending promise
+ event.respondWith(Response.error());
+ return;
+ }
+
+ // Create the new promise.
+ promise = new Promise(function(resolve) {
+ pending_resolve_func = resolve;
+ });
+ event.waitUntil(promise);
+
+ event.respondWith(new Response(`
+ <html>
+ Promise created by ${url_search}
+ <script>self.parent.postMessage({ ID:${ID}, source: "${url_search}"
+ }, '*');</script>
+ </html>
+ `, {headers: {'Content-Type': 'text/html'}}
+ ));
+
+ }
+ else if ( request_url.href.endsWith('resolve.fakehtml') ) {
+ var has_pending = !!pending_resolve_func;
+ event.respondWith(new Response(`
+ <html>
+ Promise settled for ${url_search}
+ <script>self.parent.postMessage({ ID:${ID}, has_pending: ${has_pending},
+ source: "${url_search}" }, '*');</script>
+ </html>
+ `, {headers: {'Content-Type': 'text/html'}}));
+
+ if (has_pending) {
+ callAndResetResolve();
+ }
+ }
+} \ No newline at end of file
diff --git a/test/wpt/tests/service-workers/service-worker/resources/partitioned-utils.js b/test/wpt/tests/service-workers/service-worker/resources/partitioned-utils.js
new file mode 100644
index 0000000..22e90be
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/partitioned-utils.js
@@ -0,0 +1,110 @@
+// The resolve function for the current pending event listener's promise.
+// It is nulled once the promise is resolved.
+var message_event_promise_resolve = null;
+
+function messageEventHandler(evt) {
+ if (message_event_promise_resolve) {
+ local_resolve = message_event_promise_resolve;
+ message_event_promise_resolve = null;
+ local_resolve(evt.data);
+ }
+}
+
+function makeMessagePromise() {
+ if (message_event_promise_resolve != null) {
+ // Do not create a new promise until the previous is settled.
+ return;
+ }
+
+ return new Promise(resolve => {
+ message_event_promise_resolve = resolve;
+ });
+}
+
+// Loads a url for the frame type and then returns a promise for
+// the data that was postMessage'd from the loaded frame.
+// If the frame type is 'window' then `url` is encoded into the search param
+// as the url the 3p window is meant to iframe.
+function loadAndReturnSwData(t, url, frame_type) {
+ if (frame_type !== 'iframe' && frame_type !== 'window') {
+ return;
+ }
+
+ const message_promise = makeMessagePromise();
+
+ // Create the iframe or window and then return the promise for data.
+ if ( frame_type === 'iframe' ) {
+ const frame = with_iframe(url, false);
+ t.add_cleanup(async () => {
+ const f = await frame;
+ f.remove();
+ });
+ }
+ else {
+ // 'window' case.
+ const search_param = new URLSearchParams();
+ search_param.append('target', url);
+
+ const third_party_window_url = new URL(
+ './resources/partitioned-service-worker-third-party-window.html' +
+ '?' + search_param,
+ get_host_info().HTTPS_NOTSAMESITE_ORIGIN + self.location.pathname);
+
+ const w = window.open(third_party_window_url);
+ t.add_cleanup(() => w.close());
+ }
+
+ return message_promise;
+}
+
+// Checks for an existing service worker registration. If not present,
+// registers and maintains a service worker. Used in windows or iframes
+// that will be partitioned from the main frame.
+async function setupServiceWorker() {
+
+ const script = './partitioned-storage-sw.js';
+ const scope = './partitioned-';
+
+ var reg = await navigator.serviceWorker.register(script, { scope: scope });
+
+ // We should keep track if we installed a worker or not. If we did then we
+ // need to uninstall it. Otherwise we let the top level test uninstall it
+ // (If partitioning is not working).
+ var installed_a_worker = true;
+ await new Promise(resolve => {
+ // Check if a worker is already activated.
+ var worker = reg.active;
+ // If so, just resolve.
+ if ( worker ) {
+ installed_a_worker = false;
+ resolve();
+ return;
+ }
+
+ //Otherwise check if one is waiting.
+ worker = reg.waiting;
+ // If not waiting, grab the installing worker.
+ if ( !worker ) {
+ worker = reg.installing;
+ }
+
+ // Resolve once it's activated.
+ worker.addEventListener('statechange', evt => {
+ if (worker.state === 'activated') {
+ resolve();
+ }
+ });
+ });
+
+ self.addEventListener('unload', async () => {
+ // If we didn't install a worker then that means the top level test did, and
+ // that test is therefore responsible for cleaning it up.
+ if ( !installed_a_worker ) {
+ return;
+ }
+
+ await reg.unregister();
+ });
+
+ return reg;
+} \ No newline at end of file
diff --git a/test/wpt/tests/service-workers/service-worker/resources/pass-through-worker.js b/test/wpt/tests/service-workers/service-worker/resources/pass-through-worker.js
new file mode 100644
index 0000000..5eaf48d
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/pass-through-worker.js
@@ -0,0 +1,3 @@
+addEventListener('fetch', evt => {
+ evt.respondWith(fetch(evt.request));
+});
diff --git a/test/wpt/tests/service-workers/service-worker/resources/pass.txt b/test/wpt/tests/service-workers/service-worker/resources/pass.txt
new file mode 100644
index 0000000..7ef22e9
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/pass.txt
@@ -0,0 +1 @@
+PASS
diff --git a/test/wpt/tests/service-workers/service-worker/resources/performance-timeline-worker.js b/test/wpt/tests/service-workers/service-worker/resources/performance-timeline-worker.js
new file mode 100644
index 0000000..6c6dfcb
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/performance-timeline-worker.js
@@ -0,0 +1,62 @@
+importScripts('/resources/testharness.js');
+
+promise_test(function(test) {
+ var durationMsec = 100;
+ // There are limits to our accuracy here. Timers may fire up to a
+ // millisecond early due to platform-dependent rounding. In addition
+ // the performance API introduces some rounding as well to prevent
+ // timing attacks.
+ var accuracy = 1.5;
+ return new Promise(function(resolve) {
+ performance.mark('startMark');
+ setTimeout(resolve, durationMsec);
+ }).then(function() {
+ performance.mark('endMark');
+ performance.measure('measure', 'startMark', 'endMark');
+ var startMark = performance.getEntriesByName('startMark')[0];
+ var endMark = performance.getEntriesByName('endMark')[0];
+ var measure = performance.getEntriesByType('measure')[0];
+ assert_equals(measure.startTime, startMark.startTime);
+ assert_approx_equals(endMark.startTime - startMark.startTime,
+ measure.duration, 0.001);
+ assert_greater_than(measure.duration, durationMsec - accuracy);
+ assert_equals(performance.getEntriesByType('mark').length, 2);
+ assert_equals(performance.getEntriesByType('measure').length, 1);
+ performance.clearMarks('startMark');
+ performance.clearMeasures('measure');
+ assert_equals(performance.getEntriesByType('mark').length, 1);
+ assert_equals(performance.getEntriesByType('measure').length, 0);
+ });
+ }, 'User Timing');
+
+promise_test(function(test) {
+ return fetch('sample.txt')
+ .then(function(resp) {
+ return resp.text();
+ })
+ .then(function(text) {
+ var expectedResources = ['testharness.js', 'sample.txt'];
+ assert_equals(performance.getEntriesByType('resource').length, expectedResources.length);
+ for (var i = 0; i < expectedResources.length; i++) {
+ var entry = performance.getEntriesByType('resource')[i];
+ assert_true(entry.name.endsWith(expectedResources[i]));
+ assert_equals(entry.workerStart, 0);
+ assert_greater_than(entry.startTime, 0);
+ assert_greater_than(entry.responseEnd, entry.startTime);
+ }
+ return new Promise(function(resolve) {
+ performance.onresourcetimingbufferfull = _ => {
+ resolve('bufferfull');
+ }
+ performance.setResourceTimingBufferSize(expectedResources.length);
+ fetch('sample.txt');
+ });
+ })
+ .then(function(result) {
+ assert_equals(result, 'bufferfull');
+ performance.clearResourceTimings();
+ assert_equals(performance.getEntriesByType('resource').length, 0);
+ })
+ }, 'Resource Timing');
+
+done();
diff --git a/test/wpt/tests/service-workers/service-worker/resources/postmessage-blob-url.js b/test/wpt/tests/service-workers/service-worker/resources/postmessage-blob-url.js
new file mode 100644
index 0000000..9095194
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/postmessage-blob-url.js
@@ -0,0 +1,5 @@
+self.onmessage = e => {
+ fetch(e.data)
+ .then(response => response.text())
+ .then(text => e.source.postMessage('Worker reply:' + text));
+};
diff --git a/test/wpt/tests/service-workers/service-worker/resources/postmessage-dictionary-transferables-worker.js b/test/wpt/tests/service-workers/service-worker/resources/postmessage-dictionary-transferables-worker.js
new file mode 100644
index 0000000..87a4500
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/postmessage-dictionary-transferables-worker.js
@@ -0,0 +1,24 @@
+var messageHandler = function(port, e) {
+ var text_decoder = new TextDecoder;
+ port.postMessage({
+ content: text_decoder.decode(e.data),
+ byteLength: e.data.byteLength
+ });
+
+ // Send back the array buffer via Client.postMessage.
+ port.postMessage(e.data, {transfer: [e.data.buffer]});
+
+ port.postMessage({
+ content: text_decoder.decode(e.data),
+ byteLength: e.data.byteLength
+ });
+};
+
+self.addEventListener('message', e => {
+ if (e.ports[0]) {
+ // Wait for messages sent via MessagePort.
+ e.ports[0].onmessage = messageHandler.bind(null, e.ports[0]);
+ return;
+ }
+ messageHandler(e.source, e);
+ });
diff --git a/test/wpt/tests/service-workers/service-worker/resources/postmessage-echo-worker.js b/test/wpt/tests/service-workers/service-worker/resources/postmessage-echo-worker.js
new file mode 100644
index 0000000..f088ad1
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/postmessage-echo-worker.js
@@ -0,0 +1,3 @@
+self.addEventListener('message', event => {
+ event.source.postMessage(event.data);
+});
diff --git a/test/wpt/tests/service-workers/service-worker/resources/postmessage-fetched-text.js b/test/wpt/tests/service-workers/service-worker/resources/postmessage-fetched-text.js
new file mode 100644
index 0000000..9fc6717
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/postmessage-fetched-text.js
@@ -0,0 +1,5 @@
+self.onmessage = async (e) => {
+ const response = await fetch(e.data);
+ const text = await response.text();
+ self.postMessage(text);
+};
diff --git a/test/wpt/tests/service-workers/service-worker/resources/postmessage-msgport-to-client-worker.js b/test/wpt/tests/service-workers/service-worker/resources/postmessage-msgport-to-client-worker.js
new file mode 100644
index 0000000..7af935f
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/postmessage-msgport-to-client-worker.js
@@ -0,0 +1,19 @@
+self.onmessage = function(e) {
+ e.waitUntil(self.clients.matchAll().then(function(clients) {
+ clients.forEach(function(client) {
+ var messageChannel = new MessageChannel();
+ messageChannel.port1.onmessage =
+ onMessageViaMessagePort.bind(null, messageChannel.port1);
+ client.postMessage(undefined, [messageChannel.port2]);
+ });
+ }));
+};
+
+function onMessageViaMessagePort(port, e) {
+ var message = e.data;
+ if ('value' in message) {
+ port.postMessage({ack: 'Acking value: ' + message.value});
+ } else if ('done' in message) {
+ port.postMessage({done: true});
+ }
+}
diff --git a/test/wpt/tests/service-workers/service-worker/resources/postmessage-on-load-worker.js b/test/wpt/tests/service-workers/service-worker/resources/postmessage-on-load-worker.js
new file mode 100644
index 0000000..c2b0bcb
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/postmessage-on-load-worker.js
@@ -0,0 +1,9 @@
+if ('DedicatedWorkerGlobalScope' in self &&
+ self instanceof DedicatedWorkerGlobalScope) {
+ postMessage('dedicated worker script loaded');
+} else if ('SharedWorkerGlobalScope' in self &&
+ self instanceof SharedWorkerGlobalScope) {
+ self.onconnect = evt => {
+ evt.ports[0].postMessage('shared worker script loaded');
+ };
+}
diff --git a/test/wpt/tests/service-workers/service-worker/resources/postmessage-to-client-worker.js b/test/wpt/tests/service-workers/service-worker/resources/postmessage-to-client-worker.js
new file mode 100644
index 0000000..1791306
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/postmessage-to-client-worker.js
@@ -0,0 +1,10 @@
+self.onmessage = function(e) {
+ e.waitUntil(self.clients.matchAll().then(function(clients) {
+ clients.forEach(function(client) {
+ client.postMessage('Sending message via clients');
+ if (!Array.isArray(clients))
+ client.postMessage('clients is not an array');
+ client.postMessage('quit');
+ });
+ }));
+};
diff --git a/test/wpt/tests/service-workers/service-worker/resources/postmessage-transferables-worker.js b/test/wpt/tests/service-workers/service-worker/resources/postmessage-transferables-worker.js
new file mode 100644
index 0000000..d35c1c9
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/postmessage-transferables-worker.js
@@ -0,0 +1,24 @@
+var messageHandler = function(port, e) {
+ var text_decoder = new TextDecoder;
+ port.postMessage({
+ content: text_decoder.decode(e.data),
+ byteLength: e.data.byteLength
+ });
+
+ // Send back the array buffer via Client.postMessage.
+ port.postMessage(e.data, [e.data.buffer]);
+
+ port.postMessage({
+ content: text_decoder.decode(e.data),
+ byteLength: e.data.byteLength
+ });
+};
+
+self.addEventListener('message', e => {
+ if (e.ports[0]) {
+ // Wait for messages sent via MessagePort.
+ e.ports[0].onmessage = messageHandler.bind(null, e.ports[0]);
+ return;
+ }
+ messageHandler(e.source, e);
+ });
diff --git a/test/wpt/tests/service-workers/service-worker/resources/postmessage-worker.js b/test/wpt/tests/service-workers/service-worker/resources/postmessage-worker.js
new file mode 100644
index 0000000..858cf04
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/postmessage-worker.js
@@ -0,0 +1,19 @@
+var port;
+
+// Exercise the 'onmessage' handler:
+self.onmessage = function(e) {
+ var message = e.data;
+ if ('port' in message) {
+ port = message.port;
+ }
+};
+
+// And an event listener:
+self.addEventListener('message', function(e) {
+ var message = e.data;
+ if ('value' in message) {
+ port.postMessage('Acking value: ' + message.value);
+ } else if ('done' in message) {
+ port.postMessage('quit');
+ }
+ });
diff --git a/test/wpt/tests/service-workers/service-worker/resources/range-request-to-different-origins-worker.js b/test/wpt/tests/service-workers/service-worker/resources/range-request-to-different-origins-worker.js
new file mode 100644
index 0000000..cab6058
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/range-request-to-different-origins-worker.js
@@ -0,0 +1,40 @@
+// This worker is meant to test range requests where the responses come from
+// multiple origins. It forwards the first request to a cross-origin URL
+// (generating an opaque response). The server is expected to return a 206
+// Partial Content response. Then the worker lets subsequent range requests
+// fall back to network (generating same-origin responses). The intent is to try
+// to trick the browser into treating the resource as same-origin.
+//
+// It would also be interesting to do the reverse test where the first request
+// goes to the same-origin URL, and subsequent range requests go cross-origin in
+// 'no-cors' mode to receive opaque responses. But the service worker cannot do
+// this, because in 'no-cors' mode the 'range' HTTP header is disallowed.
+
+importScripts('/common/get-host-info.sub.js')
+
+let initial = true;
+function is_initial_request() {
+ const old = initial;
+ initial = false;
+ return old;
+}
+
+self.addEventListener('fetch', e => {
+ const url = new URL(e.request.url);
+ if (url.search.indexOf('VIDEO') == -1) {
+ // Fall back for non-video.
+ return;
+ }
+
+ // Make the first request go cross-origin.
+ if (is_initial_request()) {
+ const cross_origin_url = get_host_info().HTTPS_REMOTE_ORIGIN +
+ url.pathname + url.search;
+ const cross_origin_request = new Request(cross_origin_url,
+ {mode: 'no-cors', headers: e.request.headers});
+ e.respondWith(fetch(cross_origin_request));
+ return;
+ }
+
+ // Fall back to same origin for subsequent range requests.
+ });
diff --git a/test/wpt/tests/service-workers/service-worker/resources/range-request-with-different-cors-modes-worker.js b/test/wpt/tests/service-workers/service-worker/resources/range-request-with-different-cors-modes-worker.js
new file mode 100644
index 0000000..7580b0b
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/range-request-with-different-cors-modes-worker.js
@@ -0,0 +1,60 @@
+// This worker is meant to test range requests where the responses are a mix of
+// opaque ones and non-opaque ones. It forwards the first request to a
+// cross-origin URL (generating an opaque response). The server is expected to
+// return a 206 Partial Content response. Then the worker forwards subsequent
+// range requests to that URL, with CORS sharing generating a non-opaque
+// responses. The intent is to try to trick the browser into treating the
+// resource as non-opaque.
+//
+// It would also be interesting to do the reverse test where the first request
+// uses 'cors', and subsequent range requests use 'no-cors' mode. But the
+// service worker cannot do this, because in 'no-cors' mode the 'range' HTTP
+// header is disallowed.
+
+importScripts('/common/get-host-info.sub.js')
+
+let initial = true;
+function is_initial_request() {
+ const old = initial;
+ initial = false;
+ return old;
+}
+
+self.addEventListener('fetch', e => {
+ const url = new URL(e.request.url);
+ if (url.search.indexOf('VIDEO') == -1) {
+ // Fall back for non-video.
+ return;
+ }
+
+ let cross_origin_url = get_host_info().HTTPS_REMOTE_ORIGIN +
+ url.pathname + url.search;
+
+ // The first request is no-cors.
+ if (is_initial_request()) {
+ const init = { mode: 'no-cors', headers: e.request.headers };
+ const cross_origin_request = new Request(cross_origin_url, init);
+ e.respondWith(fetch(cross_origin_request));
+ return;
+ }
+
+ // Subsequent range requests are cors.
+
+ // Copy headers needed for range requests.
+ let my_headers = new Headers;
+ if (e.request.headers.get('accept'))
+ my_headers.append('accept', e.request.headers.get('accept'));
+ if (e.request.headers.get('range'))
+ my_headers.append('range', e.request.headers.get('range'));
+
+ // Add &ACAOrigin to allow CORS.
+ cross_origin_url += '&ACAOrigin=' + get_host_info().HTTPS_ORIGIN;
+ // Add &ACAHeaders to allow range requests.
+ cross_origin_url += '&ACAHeaders=accept,range';
+
+ // Make the CORS request.
+ const init = { mode: 'cors', headers: my_headers };
+ const cross_origin_request = new Request(cross_origin_url, init);
+ e.respondWith(fetch(cross_origin_request));
+ });
+
diff --git a/test/wpt/tests/service-workers/service-worker/resources/redirect-worker.js b/test/wpt/tests/service-workers/service-worker/resources/redirect-worker.js
new file mode 100644
index 0000000..82e21fc
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/redirect-worker.js
@@ -0,0 +1,145 @@
+// We store an empty response for each fetch event request we see
+// in this Cache object so we can get the list of urls in the
+// message event.
+var cacheName = 'urls-' + self.registration.scope;
+
+var waitUntilPromiseList = [];
+
+// Sends the requests seen by this worker. The output is:
+// {
+// requestInfos: [
+// {url: url1, resultingClientId: id1},
+// {url: url2, resultingClientId: id2},
+// ]
+// }
+async function getRequestInfos(event) {
+ // Wait for fetch events to finish.
+ await Promise.all(waitUntilPromiseList);
+ waitUntilPromiseList = [];
+
+ // Generate the message.
+ const cache = await caches.open(cacheName);
+ const requestList = await cache.keys();
+ const requestInfos = [];
+ for (let i = 0; i < requestList.length; i++) {
+ const response = await cache.match(requestList[i]);
+ const body = await response.json();
+ requestInfos[i] = {
+ url: requestList[i].url,
+ resultingClientId: body.resultingClientId
+ };
+ }
+ await caches.delete(cacheName);
+
+ event.data.port.postMessage({requestInfos});
+}
+
+// Sends the results of clients.get(id) from this worker. The
+// input is:
+// {
+// actual_ids: {a: id1, b: id2, x: id3}
+// }
+//
+// The output is:
+// {
+// clients: {
+// a: {found: false},
+// b: {found: false},
+// x: {
+// id: id3,
+// url: url1,
+// found: true
+// }
+// }
+// }
+async function getClients(event) {
+ // |actual_ids| is like:
+ // {a: id1, b: id2, x: id3}
+ const actual_ids = event.data.actual_ids;
+ const result = {}
+ for (let key of Object.keys(actual_ids)) {
+ const id = actual_ids[key];
+ const client = await self.clients.get(id);
+ if (client === undefined)
+ result[key] = {found: false};
+ else
+ result[key] = {found: true, url: client.url, id: client.id};
+ }
+ event.data.port.postMessage({clients: result});
+}
+
+self.addEventListener('message', async function(event) {
+ if (event.data.command == 'getRequestInfos') {
+ event.waitUntil(getRequestInfos(event));
+ return;
+ }
+
+ if (event.data.command == 'getClients') {
+ event.waitUntil(getClients(event));
+ return;
+ }
+});
+
+function get_query_params(url) {
+ var search = (new URL(url)).search;
+ if (!search) {
+ return {};
+ }
+ var ret = {};
+ var params = search.substring(1).split('&');
+ params.forEach(function(param) {
+ var element = param.split('=');
+ ret[decodeURIComponent(element[0])] = decodeURIComponent(element[1]);
+ });
+ return ret;
+}
+
+self.addEventListener('fetch', function(event) {
+ var waitUntilPromise = caches.open(cacheName).then(function(cache) {
+ const responseBody = {};
+ responseBody['resultingClientId'] = event.resultingClientId;
+ const headers = new Headers({'Content-Type': 'application/json'});
+ const response = new Response(JSON.stringify(responseBody), {headers});
+ return cache.put(event.request, response);
+ });
+ event.waitUntil(waitUntilPromise);
+
+ var params = get_query_params(event.request.url);
+ if (!params['sw']) {
+ // To avoid races, add the waitUntil() promise to our global list.
+ // If we get a message event before we finish here, it will wait
+ // these promises to complete before proceeding to read from the
+ // cache.
+ waitUntilPromiseList.push(waitUntilPromise);
+ return;
+ }
+
+ event.respondWith(waitUntilPromise.then(async () => {
+ if (params['sw'] == 'gen') {
+ return Response.redirect(params['url']);
+ } else if (params['sw'] == 'gen-manual') {
+ // Note this differs from Response.redirect() in that relative URLs are
+ // preserved.
+ return new Response("", {
+ status: 301,
+ headers: {location: params['url']},
+ });
+ } else if (params['sw'] == 'fetch') {
+ return fetch(event.request);
+ } else if (params['sw'] == 'fetch-url') {
+ return fetch(params['url']);
+ } else if (params['sw'] == 'follow') {
+ return fetch(new Request(event.request.url, {redirect: 'follow'}));
+ } else if (params['sw'] == 'manual') {
+ return fetch(new Request(event.request.url, {redirect: 'manual'}));
+ } else if (params['sw'] == 'manualThroughCache') {
+ const url = event.request.url;
+ await caches.delete(url)
+ const cache = await self.caches.open(url);
+ const response = await fetch(new Request(url, {redirect: 'manual'}));
+ await cache.put(event.request, response);
+ return cache.match(url);
+ }
+ // unexpected... trigger an interception failure
+ }));
+ });
diff --git a/test/wpt/tests/service-workers/service-worker/resources/redirect.py b/test/wpt/tests/service-workers/service-worker/resources/redirect.py
new file mode 100644
index 0000000..bd559d5
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/redirect.py
@@ -0,0 +1,27 @@
+from wptserve.utils import isomorphic_decode
+
+def main(request, response):
+ if b'Status' in request.GET:
+ status = int(request.GET[b"Status"])
+ else:
+ status = 302
+
+ headers = []
+
+ url = isomorphic_decode(request.GET[b'Redirect'])
+ headers.append((b"Location", url))
+
+ if b"ACAOrigin" in request.GET:
+ for item in request.GET[b"ACAOrigin"].split(b","):
+ headers.append((b"Access-Control-Allow-Origin", item))
+
+ for suffix in [b"Headers", b"Methods", b"Credentials"]:
+ query = b"ACA%s" % suffix
+ header = b"Access-Control-Allow-%s" % suffix
+ if query in request.GET:
+ headers.append((header, request.GET[query]))
+
+ if b"ACEHeaders" in request.GET:
+ headers.append((b"Access-Control-Expose-Headers", request.GET[b"ACEHeaders"]))
+
+ return status, headers, b""
diff --git a/test/wpt/tests/service-workers/service-worker/resources/referer-iframe.html b/test/wpt/tests/service-workers/service-worker/resources/referer-iframe.html
new file mode 100644
index 0000000..295ff45
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/referer-iframe.html
@@ -0,0 +1,39 @@
+<script src="/common/get-host-info.sub.js"></script>
+<script src="test-helpers.sub.js"></script>
+<script>
+function check_referer(url, expected_referer) {
+ return fetch(url)
+ .then(function(res) { return res.json(); })
+ .then(function(headers) {
+ if (headers['referer'] === expected_referer) {
+ return Promise.resolve();
+ } else {
+ return Promise.reject('Referer for ' + url + ' must be ' +
+ expected_referer + ' but got ' +
+ headers['referer']);
+ }
+ });
+}
+
+window.addEventListener('message', function(evt) {
+ var host_info = get_host_info();
+ var port = evt.ports[0];
+ check_referer('request-headers.py?ignore=true',
+ host_info['HTTPS_ORIGIN'] +
+ base_path() + 'referer-iframe.html')
+ .then(function() {
+ return check_referer(
+ 'request-headers.py',
+ host_info['HTTPS_ORIGIN'] +
+ base_path() + 'referer-iframe.html');
+ })
+ .then(function() {
+ return check_referer(
+ 'request-headers.py?url=request-headers.py',
+ host_info['HTTPS_ORIGIN'] +
+ base_path() + 'fetch-rewrite-worker.js');
+ })
+ .then(function() { port.postMessage({results: 'finish'}); })
+ .catch(function(e) { port.postMessage({results: 'failure:' + e}); });
+ });
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/resources/referrer-policy-iframe.html b/test/wpt/tests/service-workers/service-worker/resources/referrer-policy-iframe.html
new file mode 100644
index 0000000..9ef3cd1
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/referrer-policy-iframe.html
@@ -0,0 +1,32 @@
+<script src="/common/get-host-info.sub.js"></script>
+<script src="test-helpers.sub.js"></script>
+<script>
+function check_referer(url, expected_referer) {
+ return fetch(url)
+ .then(function(res) { return res.json(); })
+ .then(function(headers) {
+ if (headers['referer'] === expected_referer) {
+ return Promise.resolve();
+ } else {
+ return Promise.reject('Referer for ' + url + ' must be ' +
+ expected_referer + ' but got ' +
+ headers['referer']);
+ }
+ });
+}
+
+window.addEventListener('message', function(evt) {
+ var host_info = get_host_info();
+ var port = evt.ports[0];
+ check_referer('request-headers.py?ignore=true',
+ host_info['HTTPS_ORIGIN'] +
+ base_path() + 'referrer-policy-iframe.html')
+ .then(function() {
+ return check_referer(
+ 'request-headers.py?url=request-headers.py',
+ host_info['HTTPS_ORIGIN'] + '/');
+ })
+ .then(function() { port.postMessage({results: 'finish'}); })
+ .catch(function(e) { port.postMessage({results: 'failure:' + e}); });
+ });
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/resources/register-closed-window-iframe.html b/test/wpt/tests/service-workers/service-worker/resources/register-closed-window-iframe.html
new file mode 100644
index 0000000..117f254
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/register-closed-window-iframe.html
@@ -0,0 +1,19 @@
+<html>
+<head>
+<script>
+window.addEventListener('message', async function(evt) {
+ if (evt.data === 'START') {
+ var w = window.open('./');
+ var sw = w.navigator.serviceWorker;
+ w.close();
+ w = null;
+ try {
+ await sw.register('doesntmatter.js');
+ } finally {
+ parent.postMessage('OK', '*');
+ }
+ }
+});
+</script>
+</head>
+</html>
diff --git a/test/wpt/tests/service-workers/service-worker/resources/register-iframe.html b/test/wpt/tests/service-workers/service-worker/resources/register-iframe.html
new file mode 100644
index 0000000..f5a040e
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/register-iframe.html
@@ -0,0 +1,4 @@
+<script type="text/javascript">
+navigator.serviceWorker.register('empty-worker.js',
+ {scope: 'register-iframe.html'});
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/resources/register-rewrite-worker.html b/test/wpt/tests/service-workers/service-worker/resources/register-rewrite-worker.html
new file mode 100644
index 0000000..bf06317
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/register-rewrite-worker.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<meta charset="utf-8"/>
+<script>
+async function onLoad() {
+ const params = new URLSearchParams(self.location.search);
+ const scope = self.origin + params.get('scopepath');
+ const script = './fetch-rewrite-worker.js';
+ const reg = await navigator.serviceWorker.register(script, { scope: scope });
+ // In nested cases we may be impacted by partitioning or not depending on
+ // the browser. With partitioning we will be installing a new worker here,
+ // but without partitioning the worker will already exist. Handle both cases.
+ if (reg.installing) {
+ await new Promise(resolve => {
+ const worker = reg.installing;
+ worker.addEventListener('statechange', evt => {
+ if (worker.state === 'activated') {
+ resolve();
+ }
+ });
+ });
+ if (reg.navigationPreload) {
+ await reg.navigationPreload.enable();
+ }
+ }
+ if (window.opener) {
+ window.opener.postMessage({ type: 'SW-REGISTERED' }, '*');
+ } else {
+ window.top.postMessage({ type: 'SW-REGISTERED' }, '*');
+ }
+}
+self.addEventListener('load', onLoad);
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/resources/registration-tests-mime-types.js b/test/wpt/tests/service-workers/service-worker/resources/registration-tests-mime-types.js
new file mode 100644
index 0000000..037e6c0
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/registration-tests-mime-types.js
@@ -0,0 +1,96 @@
+// Registration tests that mostly verify the MIME type.
+//
+// This file tests every MIME type so it necessarily starts many service
+// workers, so it may be slow.
+function registration_tests_mime_types(register_method) {
+ promise_test(function(t) {
+ var script = 'resources/mime-type-worker.py';
+ var scope = 'resources/scope/no-mime-type-worker/';
+ return promise_rejects_dom(t,
+ 'SecurityError',
+ register_method(script, {scope: scope}),
+ 'Registration of no MIME type script should fail.');
+ }, 'Registering script with no MIME type');
+
+ promise_test(function(t) {
+ var script = 'resources/mime-type-worker.py?mime=text/plain';
+ var scope = 'resources/scope/bad-mime-type-worker/';
+ return promise_rejects_dom(t,
+ 'SecurityError',
+ register_method(script, {scope: scope}),
+ 'Registration of plain text script should fail.');
+ }, 'Registering script with bad MIME type');
+
+ /**
+ * ServiceWorkerContainer.register() should throw a TypeError, according to
+ * step 17.1 of https://w3c.github.io/ServiceWorker/#importscripts
+ *
+ * "[17] If an uncaught runtime script error occurs during the above step, then:
+ * [17.1] Invoke Reject Job Promise with job and TypeError"
+ *
+ * (Where the "uncaught runtime script error" is thrown by an unsuccessful
+ * importScripts())
+ */
+ promise_test(function(t) {
+ var script = 'resources/import-mime-type-worker.py';
+ var scope = 'resources/scope/no-mime-type-worker/';
+ return promise_rejects_js(t,
+ TypeError,
+ register_method(script, {scope: scope}),
+ 'Registration of no MIME type imported script should fail.');
+ }, 'Registering script that imports script with no MIME type');
+
+ promise_test(function(t) {
+ var script = 'resources/import-mime-type-worker.py?mime=text/plain';
+ var scope = 'resources/scope/bad-mime-type-worker/';
+ return promise_rejects_js(t,
+ TypeError,
+ register_method(script, {scope: scope}),
+ 'Registration of plain text imported script should fail.');
+ }, 'Registering script that imports script with bad MIME type');
+
+ const validMimeTypes = [
+ 'application/ecmascript',
+ 'application/javascript',
+ 'application/x-ecmascript',
+ 'application/x-javascript',
+ 'text/ecmascript',
+ 'text/javascript',
+ 'text/javascript1.0',
+ 'text/javascript1.1',
+ 'text/javascript1.2',
+ 'text/javascript1.3',
+ 'text/javascript1.4',
+ 'text/javascript1.5',
+ 'text/jscript',
+ 'text/livescript',
+ 'text/x-ecmascript',
+ 'text/x-javascript'
+ ];
+
+ for (const validMimeType of validMimeTypes) {
+ promise_test(() => {
+ var script = `resources/mime-type-worker.py?mime=${validMimeType}`;
+ var scope = 'resources/scope/good-mime-type-worker/';
+
+ return register_method(script, {scope}).then(registration => {
+ assert_true(
+ registration instanceof ServiceWorkerRegistration,
+ 'Successfully registered.');
+ return registration.unregister();
+ });
+ }, `Registering script with good MIME type ${validMimeType}`);
+
+ promise_test(() => {
+ var script = `resources/import-mime-type-worker.py?mime=${validMimeType}`;
+ var scope = 'resources/scope/good-mime-type-worker/';
+
+ return register_method(script, { scope }).then(registration => {
+ assert_true(
+ registration instanceof ServiceWorkerRegistration,
+ 'Successfully registered.');
+ return registration.unregister();
+ });
+ }, `Registering script that imports script with good MIME type ${validMimeType}`);
+ }
+}
diff --git a/test/wpt/tests/service-workers/service-worker/resources/registration-tests-scope.js b/test/wpt/tests/service-workers/service-worker/resources/registration-tests-scope.js
new file mode 100644
index 0000000..30c424b
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/registration-tests-scope.js
@@ -0,0 +1,120 @@
+// Registration tests that mostly exercise the scope option.
+function registration_tests_scope(register_method) {
+ promise_test(function(t) {
+ var script = 'resources/empty-worker.js';
+ var scope = 'resources/scope%2fencoded-slash-in-scope';
+ return promise_rejects_js(t,
+ TypeError,
+ register_method(script, {scope: scope}),
+ 'URL-encoded slash in the scope should be rejected.');
+ }, 'Scope including URL-encoded slash');
+
+ promise_test(function(t) {
+ var script = 'resources/empty-worker.js';
+ var scope = 'resources/scope%5cencoded-slash-in-scope';
+ return promise_rejects_js(t,
+ TypeError,
+ register_method(script, {scope: scope}),
+ 'URL-encoded backslash in the scope should be rejected.');
+ }, 'Scope including URL-encoded backslash');
+
+ promise_test(function(t) {
+ var script = 'resources/empty-worker.js';
+ var scope = 'data:text/html,';
+ return promise_rejects_js(t,
+ TypeError,
+ register_method(script, {scope: scope}),
+ 'scope URL scheme is not "http" or "https"');
+ }, 'Scope URL scheme is a data: URL');
+
+ promise_test(function(t) {
+ var script = 'resources/empty-worker.js';
+ var scope = new URL('resources', location).href.replace('https:', 'ftp:');
+ return promise_rejects_js(t,
+ TypeError,
+ register_method(script, {scope: scope}),
+ 'scope URL scheme is not "http" or "https"');
+ }, 'Scope URL scheme is an ftp: URL');
+
+ promise_test(function(t) {
+ // URL-encoded full-width 'scope'.
+ var name = '%ef%bd%93%ef%bd%83%ef%bd%8f%ef%bd%90%ef%bd%85';
+ var script = 'resources/empty-worker.js';
+ var scope = 'resources/' + name + '/escaped-multibyte-character-scope';
+ return register_method(script, {scope: scope})
+ .then(function(registration) {
+ assert_equals(
+ registration.scope,
+ normalizeURL(scope),
+ 'URL-encoded multibyte characters should be available.');
+ return registration.unregister();
+ });
+ }, 'Scope including URL-encoded multibyte characters');
+
+ promise_test(function(t) {
+ // Non-URL-encoded full-width "scope".
+ var name = String.fromCodePoint(0xff53, 0xff43, 0xff4f, 0xff50, 0xff45);
+ var script = 'resources/empty-worker.js';
+ var scope = 'resources/' + name + '/non-escaped-multibyte-character-scope';
+ return register_method(script, {scope: scope})
+ .then(function(registration) {
+ assert_equals(
+ registration.scope,
+ normalizeURL(scope),
+ 'Non-URL-encoded multibyte characters should be available.');
+ return registration.unregister();
+ });
+ }, 'Scope including non-escaped multibyte characters');
+
+ promise_test(function(t) {
+ var script = 'resources/empty-worker.js';
+ var scope = 'resources/././scope/self-reference-in-scope';
+ return register_method(script, {scope: scope})
+ .then(function(registration) {
+ assert_equals(
+ registration.scope,
+ normalizeURL('resources/scope/self-reference-in-scope'),
+ 'Scope including self-reference should be normalized.');
+ return registration.unregister();
+ });
+ }, 'Scope including self-reference');
+
+ promise_test(function(t) {
+ var script = 'resources/empty-worker.js';
+ var scope = 'resources/../resources/scope/parent-reference-in-scope';
+ return register_method(script, {scope: scope})
+ .then(function(registration) {
+ assert_equals(
+ registration.scope,
+ normalizeURL('resources/scope/parent-reference-in-scope'),
+ 'Scope including parent-reference should be normalized.');
+ return registration.unregister();
+ });
+ }, 'Scope including parent-reference');
+
+ promise_test(function(t) {
+ var script = 'resources/empty-worker.js';
+ var scope = 'resources/scope////consecutive-slashes-in-scope';
+ return register_method(script, {scope: scope})
+ .then(function(registration) {
+ // Although consecutive slashes in the scope are not unified, the
+ // scope is under the script directory and registration should
+ // succeed.
+ assert_equals(
+ registration.scope,
+ normalizeURL(scope),
+ 'Should successfully be registered.');
+ return registration.unregister();
+ })
+ }, 'Scope including consecutive slashes');
+
+ promise_test(function(t) {
+ var script = 'resources/empty-worker.js';
+ var scope = 'filesystem:' + normalizeURL('resources/scope/filesystem-scope-url');
+ return promise_rejects_js(t,
+ TypeError,
+ register_method(script, {scope: scope}),
+ 'Registering with the scope that has same-origin filesystem: URL ' +
+ 'should fail with TypeError.');
+ }, 'Scope URL is same-origin filesystem: URL');
+}
diff --git a/test/wpt/tests/service-workers/service-worker/resources/registration-tests-script-url.js b/test/wpt/tests/service-workers/service-worker/resources/registration-tests-script-url.js
new file mode 100644
index 0000000..55cbe6f
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/registration-tests-script-url.js
@@ -0,0 +1,82 @@
+// Registration tests that mostly exercise the scriptURL parameter.
+function registration_tests_script_url(register_method) {
+ promise_test(function(t) {
+ var script = 'resources%2fempty-worker.js';
+ var scope = 'resources/scope/encoded-slash-in-script-url';
+ return promise_rejects_js(t,
+ TypeError,
+ register_method(script, {scope: scope}),
+ 'URL-encoded slash in the script URL should be rejected.');
+ }, 'Script URL including URL-encoded slash');
+
+ promise_test(function(t) {
+ var script = 'resources%2Fempty-worker.js';
+ var scope = 'resources/scope/encoded-slash-in-script-url';
+ return promise_rejects_js(t,
+ TypeError,
+ register_method(script, {scope: scope}),
+ 'URL-encoded slash in the script URL should be rejected.');
+ }, 'Script URL including uppercase URL-encoded slash');
+
+ promise_test(function(t) {
+ var script = 'resources%5cempty-worker.js';
+ var scope = 'resources/scope/encoded-slash-in-script-url';
+ return promise_rejects_js(t,
+ TypeError,
+ register_method(script, {scope: scope}),
+ 'URL-encoded backslash in the script URL should be rejected.');
+ }, 'Script URL including URL-encoded backslash');
+
+ promise_test(function(t) {
+ var script = 'resources%5Cempty-worker.js';
+ var scope = 'resources/scope/encoded-slash-in-script-url';
+ return promise_rejects_js(t,
+ TypeError,
+ register_method(script, {scope: scope}),
+ 'URL-encoded backslash in the script URL should be rejected.');
+ }, 'Script URL including uppercase URL-encoded backslash');
+
+ promise_test(function(t) {
+ var script = 'data:application/javascript,';
+ var scope = 'resources/scope/data-url-in-script-url';
+ return promise_rejects_js(t,
+ TypeError,
+ register_method(script, {scope: scope}),
+ 'Data URLs should not be registered as service workers.');
+ }, 'Script URL is a data URL');
+
+ promise_test(function(t) {
+ var script = 'data:application/javascript,';
+ var scope = new URL('resources/scope/data-url-in-script-url', location);
+ return promise_rejects_js(t,
+ TypeError,
+ register_method(script, {scope: scope}),
+ 'Data URLs should not be registered as service workers.');
+ }, 'Script URL is a data URL and scope URL is not relative');
+
+ promise_test(function(t) {
+ var script = 'resources/././empty-worker.js';
+ var scope = 'resources/scope/parent-reference-in-script-url';
+ return register_method(script, {scope: scope})
+ .then(function(registration) {
+ assert_equals(
+ get_newest_worker(registration).scriptURL,
+ normalizeURL('resources/empty-worker.js'),
+ 'Script URL including self-reference should be normalized.');
+ return registration.unregister();
+ });
+ }, 'Script URL including self-reference');
+
+ promise_test(function(t) {
+ var script = 'resources/../resources/empty-worker.js';
+ var scope = 'resources/scope/parent-reference-in-script-url';
+ return register_method(script, {scope: scope})
+ .then(function(registration) {
+ assert_equals(
+ get_newest_worker(registration).scriptURL,
+ normalizeURL('resources/empty-worker.js'),
+ 'Script URL including parent-reference should be normalized.');
+ return registration.unregister();
+ });
+ }, 'Script URL including parent-reference');
+}
diff --git a/test/wpt/tests/service-workers/service-worker/resources/registration-tests-script.js b/test/wpt/tests/service-workers/service-worker/resources/registration-tests-script.js
new file mode 100644
index 0000000..e5bdaf4
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/registration-tests-script.js
@@ -0,0 +1,121 @@
+// Registration tests that mostly exercise the service worker script contents or
+// response.
+function registration_tests_script(register_method, type) {
+ promise_test(function(t) {
+ var script = 'resources/invalid-chunked-encoding.py';
+ var scope = 'resources/scope/invalid-chunked-encoding/';
+ return promise_rejects_js(t,
+ TypeError,
+ register_method(script, {scope: scope}),
+ 'Registration of invalid chunked encoding script should fail.');
+ }, 'Registering invalid chunked encoding script');
+
+ promise_test(function(t) {
+ var script = 'resources/invalid-chunked-encoding-with-flush.py';
+ var scope = 'resources/scope/invalid-chunked-encoding-with-flush/';
+ return promise_rejects_js(t,
+ TypeError,
+ register_method(script, {scope: scope}),
+ 'Registration of invalid chunked encoding script should fail.');
+ }, 'Registering invalid chunked encoding script with flush');
+
+ promise_test(function(t) {
+ var script = 'resources/malformed-worker.py?parse-error';
+ var scope = 'resources/scope/parse-error';
+ return promise_rejects_js(t,
+ TypeError,
+ register_method(script, {scope: scope}),
+ 'Registration of script including parse error should fail.');
+ }, 'Registering script including parse error');
+
+ promise_test(function(t) {
+ var script = 'resources/malformed-worker.py?undefined-error';
+ var scope = 'resources/scope/undefined-error';
+ return promise_rejects_js(t,
+ TypeError,
+ register_method(script, {scope: scope}),
+ 'Registration of script including undefined error should fail.');
+ }, 'Registering script including undefined error');
+
+ promise_test(function(t) {
+ var script = 'resources/malformed-worker.py?uncaught-exception';
+ var scope = 'resources/scope/uncaught-exception';
+ return promise_rejects_js(t,
+ TypeError,
+ register_method(script, {scope: scope}),
+ 'Registration of script including uncaught exception should fail.');
+ }, 'Registering script including uncaught exception');
+
+ if (type === 'classic') {
+ promise_test(function(t) {
+ var script = 'resources/malformed-worker.py?import-malformed-script';
+ var scope = 'resources/scope/import-malformed-script';
+ return promise_rejects_js(t,
+ TypeError,
+ register_method(script, {scope: scope}),
+ 'Registration of script importing malformed script should fail.');
+ }, 'Registering script importing malformed script');
+ }
+
+ if (type === 'module') {
+ promise_test(function(t) {
+ var script = 'resources/malformed-worker.py?top-level-await';
+ var scope = 'resources/scope/top-level-await';
+ return promise_rejects_js(t,
+ TypeError,
+ register_method(script, {scope: scope}),
+ 'Registration of script with top-level await should fail.');
+ }, 'Registering script with top-level await');
+
+ promise_test(function(t) {
+ var script = 'resources/malformed-worker.py?instantiation-error';
+ var scope = 'resources/scope/instantiation-error';
+ return promise_rejects_js(t,
+ TypeError,
+ register_method(script, {scope: scope}),
+ 'Registration of script with module instantiation error should fail.');
+ }, 'Registering script with module instantiation error');
+
+ promise_test(function(t) {
+ var script = 'resources/malformed-worker.py?instantiation-error-and-top-level-await';
+ var scope = 'resources/scope/instantiation-error-and-top-level-await';
+ return promise_rejects_js(t,
+ TypeError,
+ register_method(script, {scope: scope}),
+ 'Registration of script with module instantiation error and top-level await should fail.');
+ }, 'Registering script with module instantiation error and top-level await');
+ }
+
+ promise_test(function(t) {
+ var script = 'resources/no-such-worker.js';
+ var scope = 'resources/scope/no-such-worker';
+ return promise_rejects_js(t,
+ TypeError,
+ register_method(script, {scope: scope}),
+ 'Registration of non-existent script should fail.');
+ }, 'Registering non-existent script');
+
+ if (type === 'classic') {
+ promise_test(function(t) {
+ var script = 'resources/malformed-worker.py?import-no-such-script';
+ var scope = 'resources/scope/import-no-such-script';
+ return promise_rejects_js(t,
+ TypeError,
+ register_method(script, {scope: scope}),
+ 'Registration of script importing non-existent script should fail.');
+ }, 'Registering script importing non-existent script');
+ }
+
+ promise_test(function(t) {
+ var script = 'resources/malformed-worker.py?caught-exception';
+ var scope = 'resources/scope/caught-exception';
+ return register_method(script, {scope: scope})
+ .then(function(registration) {
+ assert_true(
+ registration instanceof ServiceWorkerRegistration,
+ 'Successfully registered.');
+ return registration.unregister();
+ });
+ }, 'Registering script including caught exception');
+
+}
diff --git a/test/wpt/tests/service-workers/service-worker/resources/registration-tests-security-error.js b/test/wpt/tests/service-workers/service-worker/resources/registration-tests-security-error.js
new file mode 100644
index 0000000..c45fbd4
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/registration-tests-security-error.js
@@ -0,0 +1,78 @@
+// Registration tests that mostly exercise SecurityError cases.
+function registration_tests_security_error(register_method) {
+ promise_test(function(t) {
+ var script = 'resources/registration-worker.js';
+ var scope = 'resources';
+ return promise_rejects_dom(t,
+ 'SecurityError',
+ register_method(script, {scope: scope}),
+ 'Registering same scope as the script directory without the last ' +
+ 'slash should fail with SecurityError.');
+ }, 'Registering same scope as the script directory without the last slash');
+
+ promise_test(function(t) {
+ var script = 'resources/registration-worker.js';
+ var scope = 'different-directory/';
+ return promise_rejects_dom(t,
+ 'SecurityError',
+ register_method(script, {scope: scope}),
+ 'Registration scope outside the script directory should fail ' +
+ 'with SecurityError.');
+ }, 'Registration scope outside the script directory');
+
+ promise_test(function(t) {
+ var script = 'resources/registration-worker.js';
+ var scope = 'http://example.com/';
+ return promise_rejects_dom(t,
+ 'SecurityError',
+ register_method(script, {scope: scope}),
+ 'Registration scope outside domain should fail with SecurityError.');
+ }, 'Registering scope outside domain');
+
+ promise_test(function(t) {
+ var script = 'http://example.com/worker.js';
+ var scope = 'http://example.com/scope/';
+ return promise_rejects_dom(t,
+ 'SecurityError',
+ register_method(script, {scope: scope}),
+ 'Registration script outside domain should fail with SecurityError.');
+ }, 'Registering script outside domain');
+
+ promise_test(function(t) {
+ var script = 'resources/redirect.py?Redirect=' +
+ encodeURIComponent('/resources/registration-worker.js');
+ var scope = 'resources/scope/redirect/';
+ return promise_rejects_dom(t,
+ 'SecurityError',
+ register_method(script, {scope: scope}),
+ 'Registration of redirected script should fail.');
+ }, 'Registering redirected script');
+
+ promise_test(function(t) {
+ var script = 'resources/empty-worker.js';
+ var scope = 'resources/../scope/parent-reference-in-scope';
+ return promise_rejects_dom(t,
+ 'SecurityError',
+ register_method(script, {scope: scope}),
+ 'Scope not under the script directory should be rejected.');
+ }, 'Scope including parent-reference and not under the script directory');
+
+ promise_test(function(t) {
+ var script = 'resources////empty-worker.js';
+ var scope = 'resources/scope/consecutive-slashes-in-script-url';
+ return promise_rejects_dom(t,
+ 'SecurityError',
+ register_method(script, {scope: scope}),
+ 'Consecutive slashes in the script url should not be unified.');
+ }, 'Script URL including consecutive slashes');
+
+ promise_test(function(t) {
+ var script = 'filesystem:' + normalizeURL('resources/empty-worker.js');
+ var scope = 'resources/scope/filesystem-script-url';
+ return promise_rejects_js(t,
+ TypeError,
+ register_method(script, {scope: scope}),
+ 'Registering a script which has same-origin filesystem: URL should ' +
+ 'fail with TypeError.');
+ }, 'Script URL is same-origin filesystem: URL');
+}
diff --git a/test/wpt/tests/service-workers/service-worker/resources/registration-worker.js b/test/wpt/tests/service-workers/service-worker/resources/registration-worker.js
new file mode 100644
index 0000000..44d1d27
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/registration-worker.js
@@ -0,0 +1 @@
+// empty for now
diff --git a/test/wpt/tests/service-workers/service-worker/resources/reject-install-worker.js b/test/wpt/tests/service-workers/service-worker/resources/reject-install-worker.js
new file mode 100644
index 0000000..41f07fd
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/reject-install-worker.js
@@ -0,0 +1,3 @@
+self.oninstall = function(event) {
+ event.waitUntil(Promise.reject());
+};
diff --git a/test/wpt/tests/service-workers/service-worker/resources/reply-to-message.html b/test/wpt/tests/service-workers/service-worker/resources/reply-to-message.html
new file mode 100644
index 0000000..8a70e2a
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/reply-to-message.html
@@ -0,0 +1,7 @@
+<!DOCTYPE html>
+<script>
+window.addEventListener('message', event => {
+ var port = event.ports[0];
+ port.postMessage(event.data);
+ });
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/resources/request-end-to-end-worker.js b/test/wpt/tests/service-workers/service-worker/resources/request-end-to-end-worker.js
new file mode 100644
index 0000000..6bd2b72
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/request-end-to-end-worker.js
@@ -0,0 +1,34 @@
+'use strict';
+
+onfetch = function(e) {
+ var headers = {};
+ for (var header of e.request.headers) {
+ var key = header[0], value = header[1];
+ headers[key] = value;
+ }
+ var append_header_error = '';
+ try {
+ e.request.headers.append('Test-Header', 'TestValue');
+ } catch (error) {
+ append_header_error = error.name;
+ }
+
+ var request_construct_error = '';
+ try {
+ new Request(e.request, {method: 'GET'});
+ } catch (error) {
+ request_construct_error = error.name;
+ }
+
+ e.respondWith(new Response(JSON.stringify({
+ url: e.request.url,
+ method: e.request.method,
+ referrer: e.request.referrer,
+ headers: headers,
+ mode: e.request.mode,
+ credentials: e.request.credentials,
+ redirect: e.request.redirect,
+ append_header_error: append_header_error,
+ request_construct_error: request_construct_error
+ })));
+};
diff --git a/test/wpt/tests/service-workers/service-worker/resources/request-headers.py b/test/wpt/tests/service-workers/service-worker/resources/request-headers.py
new file mode 100644
index 0000000..6ab148e
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/request-headers.py
@@ -0,0 +1,8 @@
+import json
+
+from wptserve.utils import isomorphic_decode
+
+def main(request, response):
+ data = {isomorphic_decode(key):isomorphic_decode(request.headers[key]) for key, value in request.headers.items()}
+
+ return [(b"Content-Type", b"application/json")], json.dumps(data)
diff --git a/test/wpt/tests/service-workers/service-worker/resources/resource-timing-iframe.sub.html b/test/wpt/tests/service-workers/service-worker/resources/resource-timing-iframe.sub.html
new file mode 100644
index 0000000..ec4c726
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/resource-timing-iframe.sub.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<script src="empty.js"></script>
+<script src="sample.js"></script>
+<script src="redirect.py?Redirect=empty.js"></script>
+<img src="square.png">
+<img src="https://{{hosts[alt][]}}:{{ports[https][0]}}/service-workers/service-worker/resources/square.png">
+<img src="missing.jpg">
+<img src="https://{{hosts[alt][]}}:{{ports[https][0]}}/service-workers/service-worker/resources/missing.asis">
+<img src='missing.jpg?SWRespondsWithFetch'>
+<script src='empty-worker.js'></script>
diff --git a/test/wpt/tests/service-workers/service-worker/resources/resource-timing-worker.js b/test/wpt/tests/service-workers/service-worker/resources/resource-timing-worker.js
new file mode 100644
index 0000000..b74e8cd
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/resource-timing-worker.js
@@ -0,0 +1,12 @@
+self.addEventListener('fetch', function(event) {
+ if (event.request.url.indexOf('sample.js') != -1) {
+ event.respondWith(new Promise(resolve => {
+ // Slightly delay the response so we ensure we get a non-zero
+ // duration.
+ setTimeout(_ => resolve(new Response('// Empty javascript')), 50);
+ }));
+ }
+ else if (event.request.url.indexOf('missing.jpg?SWRespondsWithFetch') != -1) {
+ event.respondWith(fetch('sample.txt?SWFetched'));
+ }
+});
diff --git a/test/wpt/tests/service-workers/service-worker/resources/respond-then-throw-worker.js b/test/wpt/tests/service-workers/service-worker/resources/respond-then-throw-worker.js
new file mode 100644
index 0000000..adb48de
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/respond-then-throw-worker.js
@@ -0,0 +1,40 @@
+var syncport = null;
+
+self.addEventListener('message', function(e) {
+ if ('port' in e.data) {
+ if (syncport) {
+ syncport(e.data.port);
+ } else {
+ syncport = e.data.port;
+ }
+ }
+});
+
+function sync() {
+ return new Promise(function(resolve) {
+ if (syncport) {
+ resolve(syncport);
+ } else {
+ syncport = resolve;
+ }
+ }).then(function(port) {
+ port.postMessage('SYNC');
+ return new Promise(function(resolve) {
+ port.onmessage = function(e) {
+ if (e.data === 'ACK') {
+ resolve();
+ }
+ }
+ });
+ });
+}
+
+
+self.addEventListener('fetch', function(event) {
+ // In Firefox the result would depend on a race between fetch handling
+ // and exception handling code. On the assumption that this might be a common
+ // design error, we explicitly allow the exception to be handled first.
+ event.respondWith(sync().then(() => new Response('intercepted')));
+
+ throw("error");
+ });
diff --git a/test/wpt/tests/service-workers/service-worker/resources/respond-with-body-accessed-response-iframe.html b/test/wpt/tests/service-workers/service-worker/resources/respond-with-body-accessed-response-iframe.html
new file mode 100644
index 0000000..7be3148
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/respond-with-body-accessed-response-iframe.html
@@ -0,0 +1,20 @@
+<script>
+var callback;
+
+// Creates a <script> element with |url| source, and returns a promise for the
+// result of the executed script. Uses JSONP because some responses to |url|
+// are opaque so their body cannot be tested directly.
+function getJSONP(url) {
+ var sc = document.createElement('script');
+ sc.src = url;
+ var promise = new Promise(function(resolve, reject) {
+ // This callback function is called by appending a script element.
+ callback = resolve;
+ sc.addEventListener(
+ 'error',
+ function() { reject('Failed to load url:' + url); });
+ });
+ document.body.appendChild(sc);
+ return promise;
+}
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/resources/respond-with-body-accessed-response-worker.js b/test/wpt/tests/service-workers/service-worker/resources/respond-with-body-accessed-response-worker.js
new file mode 100644
index 0000000..c602109
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/respond-with-body-accessed-response-worker.js
@@ -0,0 +1,93 @@
+importScripts('/common/get-host-info.sub.js');
+importScripts('test-helpers.sub.js');
+
+function getQueryParams(url) {
+ var search = (new URL(url)).search;
+ if (!search) {
+ return {};
+ }
+ var ret = {};
+ var params = search.substring(1).split('&');
+ params.forEach(function(param) {
+ var element = param.split('=');
+ ret[decodeURIComponent(element[0])] = decodeURIComponent(element[1]);
+ });
+ return ret;
+}
+
+function createResponse(params) {
+ if (params['type'] == 'basic') {
+ return fetch('respond-with-body-accessed-response.jsonp');
+ }
+ if (params['type'] == 'opaque') {
+ return fetch(get_host_info()['HTTPS_REMOTE_ORIGIN'] + base_path() +
+ 'respond-with-body-accessed-response.jsonp',
+ {mode: 'no-cors'});
+ }
+ if (params['type'] == 'default') {
+ return Promise.resolve(new Response('callback(\'OK\');'));
+ }
+
+ return Promise.reject(new Error('unexpected type :' + params['type']));
+}
+
+function cloneResponseIfNeeded(params, response) {
+ if (params['clone'] == '1') {
+ return response.clone();
+ } else if (params['clone'] == '2') {
+ response.clone();
+ return response;
+ }
+ return response;
+}
+
+function passThroughCacheIfNeeded(params, request, response) {
+ return new Promise(function(resolve) {
+ if (params['passThroughCache'] == 'true') {
+ var cache_name = request.url;
+ var cache;
+ self.caches.delete(cache_name)
+ .then(function() {
+ return self.caches.open(cache_name);
+ })
+ .then(function(c) {
+ cache = c;
+ return cache.put(request, response);
+ })
+ .then(function() {
+ return cache.match(request.url);
+ })
+ .then(function(res) {
+ // Touch .body here to test the behavior after touching it.
+ res.body;
+ resolve(res);
+ });
+ } else {
+ resolve(response);
+ }
+ })
+}
+
+self.addEventListener('fetch', function(event) {
+ if (event.request.url.indexOf('TestRequest') == -1) {
+ return;
+ }
+ var params = getQueryParams(event.request.url);
+ event.respondWith(
+ createResponse(params)
+ .then(function(response) {
+ // Touch .body here to test the behavior after touching it.
+ response.body;
+ return cloneResponseIfNeeded(params, response);
+ })
+ .then(function(response) {
+ // Touch .body here to test the behavior after touching it.
+ response.body;
+ return passThroughCacheIfNeeded(params, event.request, response);
+ })
+ .then(function(response) {
+ // Touch .body here to test the behavior after touching it.
+ response.body;
+ return response;
+ }));
+ });
diff --git a/test/wpt/tests/service-workers/service-worker/resources/respond-with-body-accessed-response.jsonp b/test/wpt/tests/service-workers/service-worker/resources/respond-with-body-accessed-response.jsonp
new file mode 100644
index 0000000..b9c28f5
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/respond-with-body-accessed-response.jsonp
@@ -0,0 +1 @@
+callback('OK');
diff --git a/test/wpt/tests/service-workers/service-worker/resources/sample-worker-interceptor.js b/test/wpt/tests/service-workers/service-worker/resources/sample-worker-interceptor.js
new file mode 100644
index 0000000..c06f8dd
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/sample-worker-interceptor.js
@@ -0,0 +1,62 @@
+importScripts('/common/get-host-info.sub.js');
+
+const text = 'worker loading intercepted by service worker';
+const dedicated_worker_script = `postMessage('${text}');`;
+const shared_worker_script =
+ `onconnect = evt => evt.ports[0].postMessage('${text}');`;
+
+let source;
+let resolveDone;
+let done = new Promise(resolve => resolveDone = resolve);
+
+// The page messages this worker to ask for the result. Keep the worker alive
+// via waitUntil() until the result is sent.
+self.addEventListener('message', event => {
+ source = event.data.port;
+ source.postMessage({id: event.source.id});
+ source.onmessage = resolveDone;
+ event.waitUntil(done);
+});
+
+self.onfetch = event => {
+ const url = event.request.url;
+ const destination = event.request.destination;
+
+ if (source)
+ source.postMessage({clientId:event.clientId, resultingClientId: event.resultingClientId});
+
+ // Request handler for a synthesized response.
+ if (url.indexOf('synthesized') != -1) {
+ let script_headers = new Headers({ "Content-Type": "text/javascript" });
+ if (destination === 'worker')
+ event.respondWith(new Response(dedicated_worker_script, { 'headers': script_headers }));
+ else if (destination === 'sharedworker')
+ event.respondWith(new Response(shared_worker_script, { 'headers': script_headers }));
+ else
+ event.respondWith(new Response('Unexpected request! ' + destination));
+ return;
+ }
+
+ // Request handler for a same-origin response.
+ if (url.indexOf('same-origin') != -1) {
+ event.respondWith(fetch('postmessage-on-load-worker.js'));
+ return;
+ }
+
+ // Request handler for a cross-origin response.
+ if (url.indexOf('cors') != -1) {
+ const filename = 'postmessage-on-load-worker.js';
+ const path = (new URL(filename, self.location)).pathname;
+ let new_url = get_host_info()['HTTPS_REMOTE_ORIGIN'] + path;
+ let mode;
+ if (url.indexOf('no-cors') != -1) {
+ // Test no-cors mode.
+ mode = 'no-cors';
+ } else {
+ // Test cors mode.
+ new_url += '?pipe=header(Access-Control-Allow-Origin,*)';
+ mode = 'cors';
+ }
+ event.respondWith(fetch(new_url, { mode: mode }));
+ }
+};
diff --git a/test/wpt/tests/service-workers/service-worker/resources/sample.html b/test/wpt/tests/service-workers/service-worker/resources/sample.html
new file mode 100644
index 0000000..12a1799
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/sample.html
@@ -0,0 +1,2 @@
+<!DOCTYPE html>
+<body>Hello world
diff --git a/test/wpt/tests/service-workers/service-worker/resources/sample.js b/test/wpt/tests/service-workers/service-worker/resources/sample.js
new file mode 100644
index 0000000..b8889db
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/sample.js
@@ -0,0 +1 @@
+var hello = "world";
diff --git a/test/wpt/tests/service-workers/service-worker/resources/sample.txt b/test/wpt/tests/service-workers/service-worker/resources/sample.txt
new file mode 100644
index 0000000..802992c
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/sample.txt
@@ -0,0 +1 @@
+Hello world
diff --git a/test/wpt/tests/service-workers/service-worker/resources/sandboxed-iframe-fetch-event-iframe.html b/test/wpt/tests/service-workers/service-worker/resources/sandboxed-iframe-fetch-event-iframe.html
new file mode 100644
index 0000000..239fa73
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/sandboxed-iframe-fetch-event-iframe.html
@@ -0,0 +1,63 @@
+<script>
+function with_iframe(url) {
+ return new Promise(resolve => {
+ let frame = document.createElement('iframe');
+ frame.src = url;
+ frame.onload = () => { resolve(frame); };
+ document.body.appendChild(frame);
+ });
+}
+
+function with_sandboxed_iframe(url, sandbox) {
+ return new Promise(resolve => {
+ let frame = document.createElement('iframe');
+ frame.sandbox = sandbox;
+ frame.src = url;
+ frame.onload = () => { resolve(frame); };
+ document.body.appendChild(frame);
+ });
+}
+
+function fetch_from_worker(url) {
+ return new Promise(resolve => {
+ let blob = new Blob([
+ `fetch('${url}', {mode: 'no-cors'})` +
+ " .then(() => { self.postMessage('OK'); });"]);
+ let worker_url = URL.createObjectURL(blob);
+ let worker = new Worker(worker_url);
+ worker.onmessage = resolve;
+ });
+}
+
+function run_test(type) {
+ const base_path = location.href;
+ switch (type) {
+ case 'fetch':
+ return fetch(`${base_path}&test=fetch`, {mode: 'no-cors'});
+ case 'fetch-from-worker':
+ return fetch_from_worker(`${base_path}&test=fetch-from-worker`);
+ case 'iframe':
+ return with_iframe(`${base_path}&test=iframe`);
+ case 'sandboxed-iframe':
+ return with_sandboxed_iframe(`${base_path}&test=sandboxed-iframe`,
+ "allow-scripts");
+ case 'sandboxed-iframe-same-origin':
+ return with_sandboxed_iframe(
+ `${base_path}&test=sandboxed-iframe-same-origin`,
+ "allow-scripts allow-same-origin");
+ default:
+ return Promise.reject(`Unknown type: ${type}`);
+ }
+}
+
+window.onmessage = event => {
+ let id = event.data['id'];
+ run_test(event.data['type'])
+ .then(() => {
+ window.top.postMessage({id: id, result: 'done'}, '*');
+ })
+ .catch(e => {
+ window.top.postMessage({id: id, result: 'error: ' + e.toString()}, '*');
+ });
+};
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/resources/sandboxed-iframe-fetch-event-iframe.py b/test/wpt/tests/service-workers/service-worker/resources/sandboxed-iframe-fetch-event-iframe.py
new file mode 100644
index 0000000..0281b6c
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/sandboxed-iframe-fetch-event-iframe.py
@@ -0,0 +1,18 @@
+import os.path
+
+from wptserve.utils import isomorphic_decode
+
+def main(request, response):
+ header = [(b'Content-Type', b'text/html')]
+ if b'test' in request.GET:
+ with open(os.path.join(os.path.dirname(isomorphic_decode(__file__)), u'sample.js'), u'r') as f:
+ body = f.read()
+ return (header, body)
+
+ if b'sandbox' in request.GET:
+ header.append((b'Content-Security-Policy',
+ b'sandbox %s' % request.GET[b'sandbox']))
+ with open(os.path.join(os.path.dirname(isomorphic_decode(__file__)),
+ u'sandboxed-iframe-fetch-event-iframe.html'), u'r') as f:
+ body = f.read()
+ return (header, body)
diff --git a/test/wpt/tests/service-workers/service-worker/resources/sandboxed-iframe-fetch-event-worker.js b/test/wpt/tests/service-workers/service-worker/resources/sandboxed-iframe-fetch-event-worker.js
new file mode 100644
index 0000000..4035a8b
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/sandboxed-iframe-fetch-event-worker.js
@@ -0,0 +1,20 @@
+var requests = [];
+
+self.addEventListener('message', function(event) {
+ event.waitUntil(self.clients.matchAll()
+ .then(function(clients) {
+ var client_urls = [];
+ for(var client of clients){
+ client_urls.push(client.url);
+ }
+ client_urls = client_urls.sort();
+ event.data.port.postMessage(
+ {clients: client_urls, requests: requests});
+ requests = [];
+ }));
+ });
+
+self.addEventListener('fetch', function(event) {
+ requests.push(event.request.url);
+ event.respondWith(fetch(event.request));
+ });
diff --git a/test/wpt/tests/service-workers/service-worker/resources/sandboxed-iframe-navigator-serviceworker-iframe.html b/test/wpt/tests/service-workers/service-worker/resources/sandboxed-iframe-navigator-serviceworker-iframe.html
new file mode 100644
index 0000000..1d682e4
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/sandboxed-iframe-navigator-serviceworker-iframe.html
@@ -0,0 +1,25 @@
+<script>
+window.onmessage = function(e) {
+ const id = e.data['id'];
+ try {
+ var sw = window.navigator.serviceWorker;
+ } catch (e) {
+ window.top.postMessage({
+ id: id,
+ result: 'navigator.serviceWorker failed: ' + e.name
+ }, '*');
+ return;
+ }
+
+ window.navigator.serviceWorker.getRegistration()
+ .then(function() {
+ window.top.postMessage({id: id, result:'ok'}, '*');
+ })
+ .catch(function(e) {
+ window.top.postMessage({
+ id: id,
+ result: 'getRegistration() failed: ' + e.name
+ }, '*');
+ });
+};
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/resources/scope1/module-worker-importing-redirect-to-scope2.js b/test/wpt/tests/service-workers/service-worker/resources/scope1/module-worker-importing-redirect-to-scope2.js
new file mode 100644
index 0000000..ae681ba
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/scope1/module-worker-importing-redirect-to-scope2.js
@@ -0,0 +1 @@
+import * as module from './redirect.py?Redirect=/service-workers/service-worker/resources/scope2/imported-module-script.js';
diff --git a/test/wpt/tests/service-workers/service-worker/resources/scope1/module-worker-importing-scope2.js b/test/wpt/tests/service-workers/service-worker/resources/scope1/module-worker-importing-scope2.js
new file mode 100644
index 0000000..e285052
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/scope1/module-worker-importing-scope2.js
@@ -0,0 +1 @@
+import * as module from '../scope2/imported-module-script.js';
diff --git a/test/wpt/tests/service-workers/service-worker/resources/scope1/redirect.py b/test/wpt/tests/service-workers/service-worker/resources/scope1/redirect.py
new file mode 100644
index 0000000..bb4c874
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/scope1/redirect.py
@@ -0,0 +1,6 @@
+import os
+import imp
+# Use the file from the parent directory.
+mod = imp.load_source("_parent", os.path.join(os.path.dirname(os.path.dirname(__file__)),
+ os.path.basename(__file__)))
+main = mod.main
diff --git a/test/wpt/tests/service-workers/service-worker/resources/scope2/import-scripts-echo.py b/test/wpt/tests/service-workers/service-worker/resources/scope2/import-scripts-echo.py
new file mode 100644
index 0000000..5f785b5
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/scope2/import-scripts-echo.py
@@ -0,0 +1,6 @@
+def main(req, res):
+ return ([
+ (b'Cache-Control', b'no-cache, must-revalidate'),
+ (b'Pragma', b'no-cache'),
+ (b'Content-Type', b'application/javascript')],
+ b'echo_output = "%s (scope2/)";\n' % req.GET[b'msg'])
diff --git a/test/wpt/tests/service-workers/service-worker/resources/scope2/imported-module-script.js b/test/wpt/tests/service-workers/service-worker/resources/scope2/imported-module-script.js
new file mode 100644
index 0000000..a18e704
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/scope2/imported-module-script.js
@@ -0,0 +1,4 @@
+export const imported = 'A module script.';
+onmessage = msg => {
+ msg.source.postMessage('pong');
+};
diff --git a/test/wpt/tests/service-workers/service-worker/resources/scope2/simple.txt b/test/wpt/tests/service-workers/service-worker/resources/scope2/simple.txt
new file mode 100644
index 0000000..cd87667
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/scope2/simple.txt
@@ -0,0 +1 @@
+a simple text file (scope2/)
diff --git a/test/wpt/tests/service-workers/service-worker/resources/scope2/worker_interception_redirect_webworker.py b/test/wpt/tests/service-workers/service-worker/resources/scope2/worker_interception_redirect_webworker.py
new file mode 100644
index 0000000..bb4c874
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/scope2/worker_interception_redirect_webworker.py
@@ -0,0 +1,6 @@
+import os
+import imp
+# Use the file from the parent directory.
+mod = imp.load_source("_parent", os.path.join(os.path.dirname(os.path.dirname(__file__)),
+ os.path.basename(__file__)))
+main = mod.main
diff --git a/test/wpt/tests/service-workers/service-worker/resources/secure-context-service-worker.js b/test/wpt/tests/service-workers/service-worker/resources/secure-context-service-worker.js
new file mode 100644
index 0000000..5ba99f0
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/secure-context-service-worker.js
@@ -0,0 +1,21 @@
+self.addEventListener('fetch', event => {
+ let url = new URL(event.request.url);
+ if (url.pathname.indexOf('sender.html') != -1) {
+ event.respondWith(new Response(
+ "<script>window.parent.postMessage('interception', '*');</script>",
+ { headers: { 'Content-Type': 'text/html'} }
+ ));
+ } else if (url.pathname.indexOf('report') != -1) {
+ self.clients.matchAll().then(clients => {
+ for (client of clients) {
+ client.postMessage(url.searchParams.get('result'));
+ }
+ });
+ event.respondWith(
+ new Response(
+ '<script>window.close()</script>',
+ { headers: { 'Content-Type': 'text/html'} }
+ )
+ );
+ }
+});
diff --git a/test/wpt/tests/service-workers/service-worker/resources/secure-context/sender.html b/test/wpt/tests/service-workers/service-worker/resources/secure-context/sender.html
new file mode 100644
index 0000000..05e5882
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/secure-context/sender.html
@@ -0,0 +1 @@
+<script>window.parent.postMessage('network', '*');</script>
diff --git a/test/wpt/tests/service-workers/service-worker/resources/secure-context/window.html b/test/wpt/tests/service-workers/service-worker/resources/secure-context/window.html
new file mode 100644
index 0000000..071a507
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/secure-context/window.html
@@ -0,0 +1,15 @@
+<body>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="../test-helpers.sub.js"></script>
+<script>
+const HTTPS_PREFIX = get_host_info().HTTPS_ORIGIN + base_path();
+
+window.onmessage = event => {
+ window.location = HTTPS_PREFIX + 'report?result=' + event.data;
+};
+
+const frame = document.createElement('iframe');
+frame.src = HTTPS_PREFIX + 'sender.html';
+document.body.appendChild(frame);
+</script>
+</body>
diff --git a/test/wpt/tests/service-workers/service-worker/resources/service-worker-csp-worker.py b/test/wpt/tests/service-workers/service-worker/resources/service-worker-csp-worker.py
new file mode 100644
index 0000000..35a4696
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/service-worker-csp-worker.py
@@ -0,0 +1,183 @@
+bodyDefault = b'''
+importScripts('worker-testharness.js');
+importScripts('test-helpers.sub.js');
+importScripts('/common/get-host-info.sub.js');
+
+var host_info = get_host_info();
+
+test(function() {
+ var import_script_failed = false;
+ try {
+ importScripts(host_info.HTTPS_REMOTE_ORIGIN +
+ base_path() + 'empty.js');
+ } catch(e) {
+ import_script_failed = true;
+ }
+ assert_true(import_script_failed,
+ 'Importing the other origins script should fail.');
+ }, 'importScripts test for default-src');
+
+test(function() {
+ assert_throws_js(EvalError,
+ function() { eval('1 + 1'); },
+ 'eval() should throw EvalError.')
+ assert_throws_js(EvalError,
+ function() { new Function('1 + 1'); },
+ 'new Function() should throw EvalError.')
+ }, 'eval test for default-src');
+
+async_test(function(t) {
+ fetch(host_info.HTTPS_REMOTE_ORIGIN +
+ base_path() + 'fetch-access-control.py?ACAOrigin=*',
+ {mode: 'cors'})
+ .then(function(response){
+ assert_unreached('fetch should fail.');
+ }, function(){
+ t.done();
+ })
+ .catch(unreached_rejection(t));
+ }, 'Fetch test for default-src');
+
+async_test(function(t) {
+ var REDIRECT_URL = host_info.HTTPS_ORIGIN +
+ base_path() + 'redirect.py?Redirect=';
+ var OTHER_BASE_URL = host_info.HTTPS_REMOTE_ORIGIN +
+ base_path() + 'fetch-access-control.py?'
+ fetch(REDIRECT_URL + encodeURIComponent(OTHER_BASE_URL + 'ACAOrigin=*'),
+ {mode: 'cors'})
+ .then(function(response){
+ assert_unreached('Redirected fetch should fail.');
+ }, function(){
+ t.done();
+ })
+ .catch(unreached_rejection(t));
+ }, 'Redirected fetch test for default-src');'''
+
+bodyScript = b'''
+importScripts('worker-testharness.js');
+importScripts('test-helpers.sub.js');
+importScripts('/common/get-host-info.sub.js');
+
+var host_info = get_host_info();
+
+test(function() {
+ var import_script_failed = false;
+ try {
+ importScripts(host_info.HTTPS_REMOTE_ORIGIN +
+ base_path() + 'empty.js');
+ } catch(e) {
+ import_script_failed = true;
+ }
+ assert_true(import_script_failed,
+ 'Importing the other origins script should fail.');
+ }, 'importScripts test for script-src');
+
+test(function() {
+ assert_throws_js(EvalError,
+ function() { eval('1 + 1'); },
+ 'eval() should throw EvalError.')
+ assert_throws_js(EvalError,
+ function() { new Function('1 + 1'); },
+ 'new Function() should throw EvalError.')
+ }, 'eval test for script-src');
+
+async_test(function(t) {
+ fetch(host_info.HTTPS_REMOTE_ORIGIN +
+ base_path() + 'fetch-access-control.py?ACAOrigin=*',
+ {mode: 'cors'})
+ .then(function(response){
+ t.done();
+ }, function(){
+ assert_unreached('fetch should not fail.');
+ })
+ .catch(unreached_rejection(t));
+ }, 'Fetch test for script-src');
+
+async_test(function(t) {
+ var REDIRECT_URL = host_info.HTTPS_ORIGIN +
+ base_path() + 'redirect.py?Redirect=';
+ var OTHER_BASE_URL = host_info.HTTPS_REMOTE_ORIGIN +
+ base_path() + 'fetch-access-control.py?'
+ fetch(REDIRECT_URL + encodeURIComponent(OTHER_BASE_URL + 'ACAOrigin=*'),
+ {mode: 'cors'})
+ .then(function(response){
+ t.done();
+ }, function(){
+ assert_unreached('Redirected fetch should not fail.');
+ })
+ .catch(unreached_rejection(t));
+ }, 'Redirected fetch test for script-src');'''
+
+bodyConnect = b'''
+importScripts('worker-testharness.js');
+importScripts('test-helpers.sub.js');
+importScripts('/common/get-host-info.sub.js');
+
+var host_info = get_host_info();
+
+test(function() {
+ var import_script_failed = false;
+ try {
+ importScripts(host_info.HTTPS_REMOTE_ORIGIN +
+ base_path() + 'empty.js');
+ } catch(e) {
+ import_script_failed = true;
+ }
+ assert_false(import_script_failed,
+ 'Importing the other origins script should not fail.');
+ }, 'importScripts test for connect-src');
+
+test(function() {
+ var eval_failed = false;
+ try {
+ eval('1 + 1');
+ new Function('1 + 1');
+ } catch(e) {
+ eval_failed = true;
+ }
+ assert_false(eval_failed,
+ 'connect-src without unsafe-eval should not block eval().');
+ }, 'eval test for connect-src');
+
+async_test(function(t) {
+ fetch(host_info.HTTPS_REMOTE_ORIGIN +
+ base_path() + 'fetch-access-control.py?ACAOrigin=*',
+ {mode: 'cors'})
+ .then(function(response){
+ assert_unreached('fetch should fail.');
+ }, function(){
+ t.done();
+ })
+ .catch(unreached_rejection(t));
+ }, 'Fetch test for connect-src');
+
+async_test(function(t) {
+ var REDIRECT_URL = host_info.HTTPS_ORIGIN +
+ base_path() + 'redirect.py?Redirect=';
+ var OTHER_BASE_URL = host_info.HTTPS_REMOTE_ORIGIN +
+ base_path() + 'fetch-access-control.py?'
+ fetch(REDIRECT_URL + encodeURIComponent(OTHER_BASE_URL + 'ACAOrigin=*'),
+ {mode: 'cors'})
+ .then(function(response){
+ assert_unreached('Redirected fetch should fail.');
+ }, function(){
+ t.done();
+ })
+ .catch(unreached_rejection(t));
+ }, 'Redirected fetch test for connect-src');'''
+
+def main(request, response):
+ headers = []
+ headers.append((b'Content-Type', b'application/javascript'))
+ directive = request.GET[b'directive']
+ body = b'ERROR: Unknown directive'
+ if directive == b'default':
+ headers.append((b'Content-Security-Policy', b"default-src 'self'"))
+ body = bodyDefault
+ elif directive == b'script':
+ headers.append((b'Content-Security-Policy', b"script-src 'self'"))
+ body = bodyScript
+ elif directive == b'connect':
+ headers.append((b'Content-Security-Policy', b"connect-src 'self'"))
+ body = bodyConnect
+ return headers, body
diff --git a/test/wpt/tests/service-workers/service-worker/resources/service-worker-header.py b/test/wpt/tests/service-workers/service-worker/resources/service-worker-header.py
new file mode 100644
index 0000000..d64a9d2
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/service-worker-header.py
@@ -0,0 +1,20 @@
+def main(request, response):
+ service_worker_header = request.headers.get(b'service-worker')
+
+ if b'header' in request.GET and service_worker_header != b'script':
+ return 400, [(b'Content-Type', b'text/plain')], b'Bad Request'
+
+ if b'no-header' in request.GET and service_worker_header == b'script':
+ return 400, [(b'Content-Type', b'text/plain')], b'Bad Request'
+
+ # no-cache itself to ensure the user agent finds a new version for each
+ # update.
+ headers = [(b'Cache-Control', b'no-cache, must-revalidate'),
+ (b'Pragma', b'no-cache'),
+ (b'Content-Type', b'application/javascript')]
+ body = b'/* This is a service worker script */\n'
+
+ if b'import' in request.GET:
+ body += b"importScripts('%s');" % request.GET[b'import']
+
+ return 200, headers, body
diff --git a/test/wpt/tests/service-workers/service-worker/resources/service-worker-interception-dynamic-import-worker.js b/test/wpt/tests/service-workers/service-worker/resources/service-worker-interception-dynamic-import-worker.js
new file mode 100644
index 0000000..680e07f
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/service-worker-interception-dynamic-import-worker.js
@@ -0,0 +1 @@
+import('./service-worker-interception-network-worker.js');
diff --git a/test/wpt/tests/service-workers/service-worker/resources/service-worker-interception-network-worker.js b/test/wpt/tests/service-workers/service-worker/resources/service-worker-interception-network-worker.js
new file mode 100644
index 0000000..5ff3900
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/service-worker-interception-network-worker.js
@@ -0,0 +1 @@
+postMessage('LOADED_FROM_NETWORK');
diff --git a/test/wpt/tests/service-workers/service-worker/resources/service-worker-interception-service-worker.js b/test/wpt/tests/service-workers/service-worker/resources/service-worker-interception-service-worker.js
new file mode 100644
index 0000000..6b43a37
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/service-worker-interception-service-worker.js
@@ -0,0 +1,9 @@
+const kURL = '/service-worker-interception-network-worker.js';
+const kScript = 'postMessage("LOADED_FROM_SERVICE_WORKER")';
+const kHeaders = [['content-type', 'text/javascript']];
+
+self.addEventListener('fetch', e => {
+ // Serve a generated response for kURL.
+ if (e.request.url.indexOf(kURL) != -1)
+ e.respondWith(new Response(kScript, { headers: kHeaders }));
+});
diff --git a/test/wpt/tests/service-workers/service-worker/resources/service-worker-interception-static-import-worker.js b/test/wpt/tests/service-workers/service-worker/resources/service-worker-interception-static-import-worker.js
new file mode 100644
index 0000000..e570958
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/service-worker-interception-static-import-worker.js
@@ -0,0 +1 @@
+import './service-worker-interception-network-worker.js';
diff --git a/test/wpt/tests/service-workers/service-worker/resources/silence.oga b/test/wpt/tests/service-workers/service-worker/resources/silence.oga
new file mode 100644
index 0000000..af59188
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/silence.oga
Binary files differ
diff --git a/test/wpt/tests/service-workers/service-worker/resources/simple-intercept-worker.js b/test/wpt/tests/service-workers/service-worker/resources/simple-intercept-worker.js
new file mode 100644
index 0000000..f8b5f8c
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/simple-intercept-worker.js
@@ -0,0 +1,5 @@
+self.onfetch = function(event) {
+ if (event.request.url.indexOf('simple') != -1)
+ event.respondWith(
+ new Response(new Blob(['intercepted by service worker'])));
+};
diff --git a/test/wpt/tests/service-workers/service-worker/resources/simple-intercept-worker.js.headers b/test/wpt/tests/service-workers/service-worker/resources/simple-intercept-worker.js.headers
new file mode 100644
index 0000000..a17a9a3
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/simple-intercept-worker.js.headers
@@ -0,0 +1 @@
+Content-Type: application/javascript
diff --git a/test/wpt/tests/service-workers/service-worker/resources/simple.html b/test/wpt/tests/service-workers/service-worker/resources/simple.html
new file mode 100644
index 0000000..0c3e3e7
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/simple.html
@@ -0,0 +1,3 @@
+<!DOCTYPE html>
+<title>Simple</title>
+Here's a simple html file.
diff --git a/test/wpt/tests/service-workers/service-worker/resources/simple.txt b/test/wpt/tests/service-workers/service-worker/resources/simple.txt
new file mode 100644
index 0000000..9e3cb91
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/simple.txt
@@ -0,0 +1 @@
+a simple text file
diff --git a/test/wpt/tests/service-workers/service-worker/resources/skip-waiting-installed-worker.js b/test/wpt/tests/service-workers/service-worker/resources/skip-waiting-installed-worker.js
new file mode 100644
index 0000000..6f7008b
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/skip-waiting-installed-worker.js
@@ -0,0 +1,33 @@
+var saw_activate_event = false
+
+self.addEventListener('activate', function() {
+ saw_activate_event = true;
+ });
+
+self.addEventListener('message', function(event) {
+ var port = event.data.port;
+ event.waitUntil(self.skipWaiting()
+ .then(function(result) {
+ if (result !== undefined) {
+ port.postMessage('FAIL: Promise should be resolved with undefined');
+ return;
+ }
+
+ if (!saw_activate_event) {
+ port.postMessage(
+ 'FAIL: Promise should be resolved after activate event is dispatched');
+ return;
+ }
+
+ if (self.registration.active.state !== 'activating') {
+ port.postMessage(
+ 'FAIL: Promise should be resolved before ServiceWorker#state is set to activated');
+ return;
+ }
+
+ port.postMessage('PASS');
+ })
+ .catch(function(e) {
+ port.postMessage('FAIL: unexpected exception: ' + e);
+ }));
+ });
diff --git a/test/wpt/tests/service-workers/service-worker/resources/skip-waiting-worker.js b/test/wpt/tests/service-workers/service-worker/resources/skip-waiting-worker.js
new file mode 100644
index 0000000..3fc1d1e
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/skip-waiting-worker.js
@@ -0,0 +1,21 @@
+importScripts('worker-testharness.js');
+
+promise_test(function() {
+ return skipWaiting()
+ .then(function(result) {
+ assert_equals(result, undefined,
+ 'Promise should be resolved with undefined');
+ })
+ .then(function() {
+ var promises = [];
+ for (var i = 0; i < 8; ++i)
+ promises.push(self.skipWaiting());
+ return Promise.all(promises);
+ })
+ .then(function(results) {
+ results.forEach(function(r) {
+ assert_equals(r, undefined,
+ 'Promises should be resolved with undefined');
+ });
+ });
+ }, 'skipWaiting');
diff --git a/test/wpt/tests/service-workers/service-worker/resources/square.png b/test/wpt/tests/service-workers/service-worker/resources/square.png
new file mode 100644
index 0000000..01c9666
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/square.png
Binary files differ
diff --git a/test/wpt/tests/service-workers/service-worker/resources/square.png.sub.headers b/test/wpt/tests/service-workers/service-worker/resources/square.png.sub.headers
new file mode 100644
index 0000000..7341132
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/square.png.sub.headers
@@ -0,0 +1,2 @@
+Content-Type: image/png
+Access-Control-Allow-Origin: *
diff --git a/test/wpt/tests/service-workers/service-worker/resources/stalling-service-worker.js b/test/wpt/tests/service-workers/service-worker/resources/stalling-service-worker.js
new file mode 100644
index 0000000..fdf1e6c
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/stalling-service-worker.js
@@ -0,0 +1,54 @@
+async function post_message_to_client(role, message, ports) {
+ (await clients.matchAll()).forEach(client => {
+ if (new URL(client.url).searchParams.get('role') === role) {
+ client.postMessage(message, ports);
+ }
+ });
+}
+
+async function post_message_to_child(message, ports) {
+ await post_message_to_client('child', message, ports);
+}
+
+function ping_message(data) {
+ return { type: 'ping', data };
+}
+
+self.onmessage = event => {
+ const message = ping_message(event.data);
+ post_message_to_child(message);
+ post_message_to_parent(message);
+}
+
+async function post_message_to_parent(message, ports) {
+ await post_message_to_client('parent', message, ports);
+}
+
+function fetch_message(key) {
+ return { type: 'fetch', key };
+}
+
+// Send a message to the parent along with a MessagePort to respond
+// with.
+function report_fetch_request(key) {
+ const channel = new MessageChannel();
+ const reply = new Promise(resolve => {
+ channel.port1.onmessage = resolve;
+ }).then(event => event.data);
+ return post_message_to_parent(fetch_message(key), [channel.port2]).then(() => reply);
+}
+
+function respond_with_script(script) {
+ return new Response(new Blob(script, { type: 'text/javascript' }));
+}
+
+// Whenever a controlled document requests a URL with a 'key' search
+// parameter we report the request to the parent frame and wait for
+// a response. The content of the response is then used to respond to
+// the fetch request.
+addEventListener('fetch', event => {
+ let key = new URL(event.request.url).searchParams.get('key');
+ if (key) {
+ event.respondWith(report_fetch_request(key).then(respond_with_script));
+ }
+});
diff --git a/test/wpt/tests/service-workers/service-worker/resources/subdir/blank.html b/test/wpt/tests/service-workers/service-worker/resources/subdir/blank.html
new file mode 100644
index 0000000..a3c3a46
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/subdir/blank.html
@@ -0,0 +1,2 @@
+<!DOCTYPE html>
+<title>Empty doc</title>
diff --git a/test/wpt/tests/service-workers/service-worker/resources/subdir/import-scripts-echo.py b/test/wpt/tests/service-workers/service-worker/resources/subdir/import-scripts-echo.py
new file mode 100644
index 0000000..f745d7a
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/subdir/import-scripts-echo.py
@@ -0,0 +1,6 @@
+def main(req, res):
+ return ([
+ (b'Cache-Control', b'no-cache, must-revalidate'),
+ (b'Pragma', b'no-cache'),
+ (b'Content-Type', b'application/javascript')],
+ b'echo_output = "%s (subdir/)";\n' % req.GET[b'msg'])
diff --git a/test/wpt/tests/service-workers/service-worker/resources/subdir/simple.txt b/test/wpt/tests/service-workers/service-worker/resources/subdir/simple.txt
new file mode 100644
index 0000000..86bcdd7
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/subdir/simple.txt
@@ -0,0 +1 @@
+a simple text file (subdir/)
diff --git a/test/wpt/tests/service-workers/service-worker/resources/subdir/worker_interception_redirect_webworker.py b/test/wpt/tests/service-workers/service-worker/resources/subdir/worker_interception_redirect_webworker.py
new file mode 100644
index 0000000..bb4c874
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/subdir/worker_interception_redirect_webworker.py
@@ -0,0 +1,6 @@
+import os
+import imp
+# Use the file from the parent directory.
+mod = imp.load_source("_parent", os.path.join(os.path.dirname(os.path.dirname(__file__)),
+ os.path.basename(__file__)))
+main = mod.main
diff --git a/test/wpt/tests/service-workers/service-worker/resources/success.py b/test/wpt/tests/service-workers/service-worker/resources/success.py
new file mode 100644
index 0000000..a026991
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/success.py
@@ -0,0 +1,8 @@
+def main(request, response):
+ headers = []
+
+ if b"ACAOrigin" in request.GET:
+ for item in request.GET[b"ACAOrigin"].split(b","):
+ headers.append((b"Access-Control-Allow-Origin", item))
+
+ return headers, b"{ \"result\": \"success\" }"
diff --git a/test/wpt/tests/service-workers/service-worker/resources/svg-target-reftest-001-frame.html b/test/wpt/tests/service-workers/service-worker/resources/svg-target-reftest-001-frame.html
new file mode 100644
index 0000000..59fb524
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/svg-target-reftest-001-frame.html
@@ -0,0 +1,3 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<img src="/images/green.svg">
diff --git a/test/wpt/tests/service-workers/service-worker/resources/svg-target-reftest-001.html b/test/wpt/tests/service-workers/service-worker/resources/svg-target-reftest-001.html
new file mode 100644
index 0000000..9a93d3b
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/svg-target-reftest-001.html
@@ -0,0 +1,5 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Green svg box reference file</title>
+<p>Pass if you see a green box below.</p>
+<iframe src="svg-target-reftest-001-frame.html">
diff --git a/test/wpt/tests/service-workers/service-worker/resources/svg-target-reftest-frame.html b/test/wpt/tests/service-workers/service-worker/resources/svg-target-reftest-frame.html
new file mode 100644
index 0000000..d6fc820
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/svg-target-reftest-frame.html
@@ -0,0 +1,2 @@
+<!DOCTYPE html>
+<img src="/images/colors.svg#green">
diff --git a/test/wpt/tests/service-workers/service-worker/resources/test-helpers.sub.js b/test/wpt/tests/service-workers/service-worker/resources/test-helpers.sub.js
new file mode 100644
index 0000000..7430152
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/test-helpers.sub.js
@@ -0,0 +1,300 @@
+// Adapter for testharness.js-style tests with Service Workers
+
+/**
+ * @param options an object that represents RegistrationOptions except for scope.
+ * @param options.type a WorkerType.
+ * @param options.updateViaCache a ServiceWorkerUpdateViaCache.
+ * @see https://w3c.github.io/ServiceWorker/#dictdef-registrationoptions
+ */
+function service_worker_unregister_and_register(test, url, scope, options) {
+ if (!scope || scope.length == 0)
+ return Promise.reject(new Error('tests must define a scope'));
+
+ if (options && options.scope)
+ return Promise.reject(new Error('scope must not be passed in options'));
+
+ options = Object.assign({ scope: scope }, options);
+ return service_worker_unregister(test, scope)
+ .then(function() {
+ return navigator.serviceWorker.register(url, options);
+ })
+ .catch(unreached_rejection(test,
+ 'unregister and register should not fail'));
+}
+
+// This unregisters the registration that precisely matches scope. Use this
+// when unregistering by scope. If no registration is found, it just resolves.
+function service_worker_unregister(test, scope) {
+ var absoluteScope = (new URL(scope, window.location).href);
+ return navigator.serviceWorker.getRegistration(scope)
+ .then(function(registration) {
+ if (registration && registration.scope === absoluteScope)
+ return registration.unregister();
+ })
+ .catch(unreached_rejection(test, 'unregister should not fail'));
+}
+
+function service_worker_unregister_and_done(test, scope) {
+ return service_worker_unregister(test, scope)
+ .then(test.done.bind(test));
+}
+
+function unreached_fulfillment(test, prefix) {
+ return test.step_func(function(result) {
+ var error_prefix = prefix || 'unexpected fulfillment';
+ assert_unreached(error_prefix + ': ' + result);
+ });
+}
+
+// Rejection-specific helper that provides more details
+function unreached_rejection(test, prefix) {
+ return test.step_func(function(error) {
+ var reason = error.message || error.name || error;
+ var error_prefix = prefix || 'unexpected rejection';
+ assert_unreached(error_prefix + ': ' + reason);
+ });
+}
+
+/**
+ * Adds an iframe to the document and returns a promise that resolves to the
+ * iframe when it finishes loading. The caller is responsible for removing the
+ * iframe later if needed.
+ *
+ * @param {string} url
+ * @returns {HTMLIFrameElement}
+ */
+function with_iframe(url) {
+ return new Promise(function(resolve) {
+ var frame = document.createElement('iframe');
+ frame.className = 'test-iframe';
+ frame.src = url;
+ frame.onload = function() { resolve(frame); };
+ document.body.appendChild(frame);
+ });
+}
+
+function normalizeURL(url) {
+ return new URL(url, self.location).toString().replace(/#.*$/, '');
+}
+
+function wait_for_update(test, registration) {
+ if (!registration || registration.unregister == undefined) {
+ return Promise.reject(new Error(
+ 'wait_for_update must be passed a ServiceWorkerRegistration'));
+ }
+
+ return new Promise(test.step_func(function(resolve) {
+ var handler = test.step_func(function() {
+ registration.removeEventListener('updatefound', handler);
+ resolve(registration.installing);
+ });
+ registration.addEventListener('updatefound', handler);
+ }));
+}
+
+// Return true if |state_a| is more advanced than |state_b|.
+function is_state_advanced(state_a, state_b) {
+ if (state_b === 'installing') {
+ switch (state_a) {
+ case 'installed':
+ case 'activating':
+ case 'activated':
+ case 'redundant':
+ return true;
+ }
+ }
+
+ if (state_b === 'installed') {
+ switch (state_a) {
+ case 'activating':
+ case 'activated':
+ case 'redundant':
+ return true;
+ }
+ }
+
+ if (state_b === 'activating') {
+ switch (state_a) {
+ case 'activated':
+ case 'redundant':
+ return true;
+ }
+ }
+
+ if (state_b === 'activated') {
+ switch (state_a) {
+ case 'redundant':
+ return true;
+ }
+ }
+ return false;
+}
+
+function wait_for_state(test, worker, state) {
+ if (!worker || worker.state == undefined) {
+ return Promise.reject(new Error(
+ 'wait_for_state needs a ServiceWorker object to be passed.'));
+ }
+ if (worker.state === state)
+ return Promise.resolve(state);
+
+ if (is_state_advanced(worker.state, state)) {
+ return Promise.reject(new Error(
+ `Waiting for ${state} but the worker is already ${worker.state}.`));
+ }
+ return new Promise(test.step_func(function(resolve, reject) {
+ worker.addEventListener('statechange', test.step_func(function() {
+ if (worker.state === state)
+ resolve(state);
+
+ if (is_state_advanced(worker.state, state)) {
+ reject(new Error(
+ `The state of the worker becomes ${worker.state} while waiting` +
+ `for ${state}.`));
+ }
+ }));
+ }));
+}
+
+// Declare a test that runs entirely in the ServiceWorkerGlobalScope. The |url|
+// is the service worker script URL. This function:
+// - Instantiates a new test with the description specified in |description|.
+// The test will succeed if the specified service worker can be successfully
+// registered and installed.
+// - Creates a new ServiceWorker registration with a scope unique to the current
+// document URL. Note that this doesn't allow more than one
+// service_worker_test() to be run from the same document.
+// - Waits for the new worker to begin installing.
+// - Imports tests results from tests running inside the ServiceWorker.
+function service_worker_test(url, description) {
+ // If the document URL is https://example.com/document and the script URL is
+ // https://example.com/script/worker.js, then the scope would be
+ // https://example.com/script/scope/document.
+ var scope = new URL('scope' + window.location.pathname,
+ new URL(url, window.location)).toString();
+ promise_test(function(test) {
+ return service_worker_unregister_and_register(test, url, scope)
+ .then(function(registration) {
+ add_completion_callback(function() {
+ registration.unregister();
+ });
+ return wait_for_update(test, registration)
+ .then(function(worker) {
+ return fetch_tests_from_worker(worker);
+ });
+ });
+ }, description);
+}
+
+function base_path() {
+ return location.pathname.replace(/\/[^\/]*$/, '/');
+}
+
+function test_login(test, origin, username, password, cookie) {
+ return new Promise(function(resolve, reject) {
+ with_iframe(
+ origin + base_path() +
+ 'resources/fetch-access-control-login.html')
+ .then(test.step_func(function(frame) {
+ var channel = new MessageChannel();
+ channel.port1.onmessage = test.step_func(function() {
+ frame.remove();
+ resolve();
+ });
+ frame.contentWindow.postMessage(
+ {username: username, password: password, cookie: cookie},
+ origin, [channel.port2]);
+ }));
+ });
+}
+
+function test_websocket(test, frame, url) {
+ return new Promise(function(resolve, reject) {
+ var ws = new frame.contentWindow.WebSocket(url, ['echo', 'chat']);
+ var openCalled = false;
+ ws.addEventListener('open', test.step_func(function(e) {
+ assert_equals(ws.readyState, 1, "The WebSocket should be open");
+ openCalled = true;
+ ws.close();
+ }), true);
+
+ ws.addEventListener('close', test.step_func(function(e) {
+ assert_true(openCalled, "The WebSocket should be closed after being opened");
+ resolve();
+ }), true);
+
+ ws.addEventListener('error', reject);
+ });
+}
+
+function login_https(test) {
+ var host_info = get_host_info();
+ return test_login(test, host_info.HTTPS_REMOTE_ORIGIN,
+ 'username1s', 'password1s', 'cookie1')
+ .then(function() {
+ return test_login(test, host_info.HTTPS_ORIGIN,
+ 'username2s', 'password2s', 'cookie2');
+ });
+}
+
+function websocket(test, frame) {
+ return test_websocket(test, frame, get_websocket_url());
+}
+
+function get_websocket_url() {
+ return 'wss://{{host}}:{{ports[wss][0]}}/echo';
+}
+
+// The navigator.serviceWorker.register() method guarantees that the newly
+// installing worker is available as registration.installing when its promise
+// resolves. However some tests test installation using a <link> element where
+// it is possible for the installing worker to have already become the waiting
+// or active worker. So this method is used to get the newest worker when these
+// tests need access to the ServiceWorker itself.
+function get_newest_worker(registration) {
+ if (registration.installing)
+ return registration.installing;
+ if (registration.waiting)
+ return registration.waiting;
+ if (registration.active)
+ return registration.active;
+}
+
+function register_using_link(script, options) {
+ var scope = options.scope;
+ var link = document.createElement('link');
+ link.setAttribute('rel', 'serviceworker');
+ link.setAttribute('href', script);
+ link.setAttribute('scope', scope);
+ document.getElementsByTagName('head')[0].appendChild(link);
+ return new Promise(function(resolve, reject) {
+ link.onload = resolve;
+ link.onerror = reject;
+ })
+ .then(() => navigator.serviceWorker.getRegistration(scope));
+}
+
+function with_sandboxed_iframe(url, sandbox) {
+ return new Promise(function(resolve) {
+ var frame = document.createElement('iframe');
+ frame.sandbox = sandbox;
+ frame.src = url;
+ frame.onload = function() { resolve(frame); };
+ document.body.appendChild(frame);
+ });
+}
+
+// Registers, waits for activation, then unregisters on a sample scope.
+//
+// This can be used to wait for a period of time needed to register,
+// activate, and then unregister a service worker. When checking that
+// certain behavior does *NOT* happen, this is preferable to using an
+// arbitrary delay.
+async function wait_for_activation_on_sample_scope(t, window_or_workerglobalscope) {
+ const script = '/service-workers/service-worker/resources/empty-worker.js';
+ const scope = 'resources/there/is/no/there/there?' + Date.now();
+ let registration = await window_or_workerglobalscope.navigator.serviceWorker.register(script, { scope });
+ await wait_for_state(t, registration.installing, 'activated');
+ await registration.unregister();
+}
+
diff --git a/test/wpt/tests/service-workers/service-worker/resources/test-request-headers-worker.js b/test/wpt/tests/service-workers/service-worker/resources/test-request-headers-worker.js
new file mode 100644
index 0000000..566e2e9
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/test-request-headers-worker.js
@@ -0,0 +1,10 @@
+// Add a unique UUID per request to induce service worker script update.
+// Time stamp: %UUID%
+
+// The server injects the request headers here as a JSON string.
+const headersAsJson = `%HEADERS%`;
+const headers = JSON.parse(headersAsJson);
+
+self.addEventListener('message', async (e) => {
+ e.source.postMessage(headers);
+});
diff --git a/test/wpt/tests/service-workers/service-worker/resources/test-request-headers-worker.py b/test/wpt/tests/service-workers/service-worker/resources/test-request-headers-worker.py
new file mode 100644
index 0000000..78a9335
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/test-request-headers-worker.py
@@ -0,0 +1,21 @@
+import json
+import os
+import uuid
+import sys
+
+from wptserve.utils import isomorphic_decode
+
+def main(request, response):
+ path = os.path.join(os.path.dirname(isomorphic_decode(__file__)),
+ u"test-request-headers-worker.js")
+ body = open(path, u"rb").read()
+
+ data = {isomorphic_decode(key):isomorphic_decode(request.headers[key]) for key, value in request.headers.items()}
+ body = body.replace(b"%HEADERS%", json.dumps(data).encode("utf-8"))
+ body = body.replace(b"%UUID%", str(uuid.uuid4()).encode("utf-8"))
+
+ headers = []
+ headers.append((b"ETag", b"etag"))
+ headers.append((b"Content-Type", b'text/javascript'))
+
+ return headers, body
diff --git a/test/wpt/tests/service-workers/service-worker/resources/test-request-mode-worker.js b/test/wpt/tests/service-workers/service-worker/resources/test-request-mode-worker.js
new file mode 100644
index 0000000..566e2e9
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/test-request-mode-worker.js
@@ -0,0 +1,10 @@
+// Add a unique UUID per request to induce service worker script update.
+// Time stamp: %UUID%
+
+// The server injects the request headers here as a JSON string.
+const headersAsJson = `%HEADERS%`;
+const headers = JSON.parse(headersAsJson);
+
+self.addEventListener('message', async (e) => {
+ e.source.postMessage(headers);
+});
diff --git a/test/wpt/tests/service-workers/service-worker/resources/test-request-mode-worker.py b/test/wpt/tests/service-workers/service-worker/resources/test-request-mode-worker.py
new file mode 100644
index 0000000..8449841
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/test-request-mode-worker.py
@@ -0,0 +1,22 @@
+import json
+import os
+import uuid
+import sys
+
+from wptserve.utils import isomorphic_decode
+
+def main(request, response):
+ path = os.path.join(os.path.dirname(isomorphic_decode(__file__)),
+ u"test-request-mode-worker.js")
+ body = open(path, u"rb").read()
+
+ data = {isomorphic_decode(key):isomorphic_decode(request.headers[key]) for key, value in request.headers.items()}
+
+ body = body.replace(b"%HEADERS%", json.dumps(data).encode("utf-8"))
+ body = body.replace(b"%UUID%", str(uuid.uuid4()).encode("utf-8"))
+
+ headers = []
+ headers.append((b"ETag", b"etag"))
+ headers.append((b"Content-Type", b'text/javascript'))
+
+ return headers, body
diff --git a/test/wpt/tests/service-workers/service-worker/resources/testharness-helpers.js b/test/wpt/tests/service-workers/service-worker/resources/testharness-helpers.js
new file mode 100644
index 0000000..b1a5b96
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/testharness-helpers.js
@@ -0,0 +1,136 @@
+/*
+ * testharness-helpers contains various useful extensions to testharness.js to
+ * allow them to be used across multiple tests before they have been
+ * upstreamed. This file is intended to be usable from both document and worker
+ * environments, so code should for example not rely on the DOM.
+ */
+
+// Asserts that two objects |actual| and |expected| are weakly equal under the
+// following definition:
+//
+// |a| and |b| are weakly equal if any of the following are true:
+// 1. If |a| is not an 'object', and |a| === |b|.
+// 2. If |a| is an 'object', and all of the following are true:
+// 2.1 |a.p| is weakly equal to |b.p| for all own properties |p| of |a|.
+// 2.2 Every own property of |b| is an own property of |a|.
+//
+// This is a replacement for the the version of assert_object_equals() in
+// testharness.js. The latter doesn't handle own properties correctly. I.e. if
+// |a.p| is not an own property, it still requires that |b.p| be an own
+// property.
+//
+// Note that |actual| must not contain cyclic references.
+self.assert_object_equals = function(actual, expected, description) {
+ var object_stack = [];
+
+ function _is_equal(actual, expected, prefix) {
+ if (typeof actual !== 'object') {
+ assert_equals(actual, expected, prefix);
+ return;
+ }
+ assert_equals(typeof expected, 'object', prefix);
+ assert_equals(object_stack.indexOf(actual), -1,
+ prefix + ' must not contain cyclic references.');
+
+ object_stack.push(actual);
+
+ Object.getOwnPropertyNames(expected).forEach(function(property) {
+ assert_own_property(actual, property, prefix);
+ _is_equal(actual[property], expected[property],
+ prefix + '.' + property);
+ });
+ Object.getOwnPropertyNames(actual).forEach(function(property) {
+ assert_own_property(expected, property, prefix);
+ });
+
+ object_stack.pop();
+ }
+
+ function _brand(object) {
+ return Object.prototype.toString.call(object).match(/^\[object (.*)\]$/)[1];
+ }
+
+ _is_equal(actual, expected,
+ (description ? description + ': ' : '') + _brand(expected));
+};
+
+// Equivalent to assert_in_array, but uses a weaker equivalence relation
+// (assert_object_equals) than '==='.
+function assert_object_in_array(actual, expected_array, description) {
+ assert_true(expected_array.some(function(element) {
+ try {
+ assert_object_equals(actual, element);
+ return true;
+ } catch (e) {
+ return false;
+ }
+ }), description);
+}
+
+// Assert that the two arrays |actual| and |expected| contain the same set of
+// elements as determined by assert_object_equals. The order is not significant.
+//
+// |expected| is assumed to not contain any duplicates as determined by
+// assert_object_equals().
+function assert_array_equivalent(actual, expected, description) {
+ assert_true(Array.isArray(actual), description);
+ assert_equals(actual.length, expected.length, description);
+ expected.forEach(function(expected_element) {
+ // assert_in_array treats the first argument as being 'actual', and the
+ // second as being 'expected array'. We are switching them around because
+ // we want to be resilient against the |actual| array containing
+ // duplicates.
+ assert_object_in_array(expected_element, actual, description);
+ });
+}
+
+// Asserts that two arrays |actual| and |expected| contain the same set of
+// elements as determined by assert_object_equals(). The corresponding elements
+// must occupy corresponding indices in their respective arrays.
+function assert_array_objects_equals(actual, expected, description) {
+ assert_true(Array.isArray(actual), description);
+ assert_equals(actual.length, expected.length, description);
+ actual.forEach(function(value, index) {
+ assert_object_equals(value, expected[index],
+ description + ' : object[' + index + ']');
+ });
+}
+
+// Asserts that |object| that is an instance of some interface has the attribute
+// |attribute_name| following the conditions specified by WebIDL, but it's
+// acceptable that the attribute |attribute_name| is an own property of the
+// object because we're in the middle of moving the attribute to a prototype
+// chain. Once we complete the transition to prototype chains,
+// assert_will_be_idl_attribute must be replaced with assert_idl_attribute
+// defined in testharness.js.
+//
+// FIXME: Remove assert_will_be_idl_attribute once we complete the transition
+// of moving the DOM attributes to prototype chains. (http://crbug.com/43394)
+function assert_will_be_idl_attribute(object, attribute_name, description) {
+ assert_equals(typeof object, "object", description);
+
+ assert_true("hasOwnProperty" in object, description);
+
+ // Do not test if |attribute_name| is not an own property because
+ // |attribute_name| is in the middle of the transition to a prototype
+ // chain. (http://crbug.com/43394)
+
+ assert_true(attribute_name in object, description);
+}
+
+// Stringifies a DOM object. This function stringifies not only own properties
+// but also DOM attributes which are on a prototype chain. Note that
+// JSON.stringify only stringifies own properties.
+function stringifyDOMObject(object)
+{
+ function deepCopy(src) {
+ if (typeof src != "object")
+ return src;
+ var dst = Array.isArray(src) ? [] : {};
+ for (var property in src) {
+ dst[property] = deepCopy(src[property]);
+ }
+ return dst;
+ }
+ return JSON.stringify(deepCopy(object));
+}
diff --git a/test/wpt/tests/service-workers/service-worker/resources/trickle.py b/test/wpt/tests/service-workers/service-worker/resources/trickle.py
new file mode 100644
index 0000000..6423f7f
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/trickle.py
@@ -0,0 +1,14 @@
+import time
+
+def main(request, response):
+ delay = float(request.GET.first(b"ms", 500)) / 1E3
+ count = int(request.GET.first(b"count", 50))
+ # Read request body
+ request.body
+ time.sleep(delay)
+ response.headers.set(b"Content-type", b"text/plain")
+ response.write_status_headers()
+ time.sleep(delay)
+ for i in range(count):
+ response.writer.write_content(b"TEST_TRICKLE\n")
+ time.sleep(delay)
diff --git a/test/wpt/tests/service-workers/service-worker/resources/type-check-worker.js b/test/wpt/tests/service-workers/service-worker/resources/type-check-worker.js
new file mode 100644
index 0000000..1779e23
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/type-check-worker.js
@@ -0,0 +1,10 @@
+let type = '';
+try {
+ importScripts('empty.js');
+ type = 'classic';
+} catch (e) {
+ type = 'module';
+}
+onmessage = e => {
+ e.source.postMessage(type);
+};
diff --git a/test/wpt/tests/service-workers/service-worker/resources/unregister-controller-page.html b/test/wpt/tests/service-workers/service-worker/resources/unregister-controller-page.html
new file mode 100644
index 0000000..18a95ee
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/unregister-controller-page.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<script>
+function fetch_url(url) {
+ return new Promise(function(resolve, reject) {
+ var request = new XMLHttpRequest();
+ request.addEventListener('load', function(event) {
+ if (request.status == 200)
+ resolve(request.response);
+ else
+ reject(Error(request.statusText));
+ });
+ request.open('GET', url);
+ request.send();
+ });
+}
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/resources/unregister-immediately-helpers.js b/test/wpt/tests/service-workers/service-worker/resources/unregister-immediately-helpers.js
new file mode 100644
index 0000000..91a30de
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/unregister-immediately-helpers.js
@@ -0,0 +1,19 @@
+'use strict';
+
+// Returns a promise for a network response that contains the Clear-Site-Data:
+// "storage" header.
+function clear_site_data() {
+ return fetch('resources/blank.html?pipe=header(Clear-Site-Data,"storage")');
+}
+
+async function assert_no_registrations_exist() {
+ const registrations = await navigator.serviceWorker.getRegistrations();
+ assert_equals(registrations.length, 0);
+}
+
+async function add_controlled_iframe(test, url) {
+ const frame = await with_iframe(url);
+ test.add_cleanup(() => { frame.remove(); });
+ assert_not_equals(frame.contentWindow.navigator.serviceWorker.controller, null);
+ return frame;
+}
diff --git a/test/wpt/tests/service-workers/service-worker/resources/unregister-rewrite-worker.html b/test/wpt/tests/service-workers/service-worker/resources/unregister-rewrite-worker.html
new file mode 100644
index 0000000..f5d0367
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/unregister-rewrite-worker.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<meta charset="utf-8"/>
+<script>
+async function onLoad() {
+ const params = new URLSearchParams(self.location.search);
+ const scope = self.origin + params.get('scopepath');
+ const reg = await navigator.serviceWorker.getRegistration(scope);
+ if (reg) {
+ await reg.unregister();
+ }
+ if (window.opener) {
+ window.opener.postMessage({ type: 'SW-UNREGISTERED' }, '*');
+ } else {
+ window.top.postMessage({ type: 'SW-UNREGISTERED' }, '*');
+ }
+}
+self.addEventListener('load', onLoad);
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/resources/update-claim-worker.py b/test/wpt/tests/service-workers/service-worker/resources/update-claim-worker.py
new file mode 100644
index 0000000..64914a9
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/update-claim-worker.py
@@ -0,0 +1,24 @@
+import time
+
+script = u'''
+// Time stamp: %s
+// (This ensures the source text is *not* a byte-for-byte match with any
+// previously-fetched version of this script.)
+
+// This no-op fetch handler is necessary to bypass explicitly the no fetch
+// handler optimization by which this service worker script can be skipped.
+addEventListener('fetch', event => {
+ return;
+ });
+
+addEventListener('install', event => {
+ event.waitUntil(self.skipWaiting());
+ });
+
+addEventListener('activate', event => {
+ event.waitUntil(self.clients.claim());
+ });'''
+
+
+def main(request, response):
+ return [(b'Content-Type', b'application/javascript')], script % time.time()
diff --git a/test/wpt/tests/service-workers/service-worker/resources/update-during-installation-worker.js b/test/wpt/tests/service-workers/service-worker/resources/update-during-installation-worker.js
new file mode 100644
index 0000000..f1997bd
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/update-during-installation-worker.js
@@ -0,0 +1,61 @@
+'use strict';
+
+const installEventFired = new Promise(resolve => {
+ self.fireInstallEvent = resolve;
+});
+
+const installFinished = new Promise(resolve => {
+ self.finishInstall = resolve;
+});
+
+addEventListener('install', event => {
+ fireInstallEvent();
+ event.waitUntil(installFinished);
+});
+
+addEventListener('message', event => {
+ let resolveWaitUntil;
+ event.waitUntil(new Promise(resolve => { resolveWaitUntil = resolve; }));
+
+ // Use a dedicated MessageChannel for every request so senders can wait for
+ // individual requests to finish, and concurrent requests (to different
+ // workers) don't cause race conditions.
+ const port = event.data;
+ port.onmessage = (event) => {
+ switch (event.data) {
+ case 'awaitInstallEvent':
+ installEventFired.then(() => {
+ port.postMessage('installEventFired');
+ }).finally(resolveWaitUntil);
+ break;
+
+ case 'finishInstall':
+ installFinished.then(() => {
+ port.postMessage('installFinished');
+ }).finally(resolveWaitUntil);
+ finishInstall();
+ break;
+
+ case 'callUpdate': {
+ const channel = new MessageChannel();
+ registration.update().then(() => {
+ channel.port2.postMessage({
+ success: true,
+ });
+ }).catch((exception) => {
+ channel.port2.postMessage({
+ success: false,
+ exception: exception.name,
+ });
+ }).finally(resolveWaitUntil);
+ port.postMessage(channel.port1, [channel.port1]);
+ break;
+ }
+
+ default:
+ port.postMessage('Unexpected command ' + event.data);
+ resolveWaitUntil();
+ break;
+ }
+ };
+});
diff --git a/test/wpt/tests/service-workers/service-worker/resources/update-during-installation-worker.py b/test/wpt/tests/service-workers/service-worker/resources/update-during-installation-worker.py
new file mode 100644
index 0000000..3e15926
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/update-during-installation-worker.py
@@ -0,0 +1,11 @@
+import random
+
+def main(request, response):
+ headers = [(b'Content-Type', b'application/javascript'),
+ (b'Cache-Control', b'max-age=0')]
+ # Plug in random.random() to the worker so update() finds a new worker every time.
+ body = u'''
+// %s
+importScripts('update-during-installation-worker.js');
+ '''.strip() % (random.random())
+ return headers, body
diff --git a/test/wpt/tests/service-workers/service-worker/resources/update-fetch-worker.py b/test/wpt/tests/service-workers/service-worker/resources/update-fetch-worker.py
new file mode 100644
index 0000000..02cbb42
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/update-fetch-worker.py
@@ -0,0 +1,18 @@
+import random
+import time
+
+def main(request, response):
+ # no-cache itself to ensure the user agent finds a new version for each update.
+ headers = [(b'Cache-Control', b'no-cache, must-revalidate'),
+ (b'Pragma', b'no-cache')]
+
+ content_type = b''
+ extra_body = u''
+
+ content_type = b'application/javascript'
+ headers.append((b'Content-Type', content_type))
+
+ extra_body = u"self.onfetch = (event) => { event.respondWith(fetch(event.request)); };"
+
+ # Return a different script for each access.
+ return headers, u'/* %s %s */ %s' % (time.time(), random.random(), extra_body)
diff --git a/test/wpt/tests/service-workers/service-worker/resources/update-max-aged-worker-imported-script.py b/test/wpt/tests/service-workers/service-worker/resources/update-max-aged-worker-imported-script.py
new file mode 100644
index 0000000..7cc5a65
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/update-max-aged-worker-imported-script.py
@@ -0,0 +1,14 @@
+import time
+
+from wptserve.utils import isomorphic_encode
+
+def main(request, response):
+ headers = [(b'Content-Type', b'application/javascript'),
+ (b'Cache-Control', b'max-age=86400'),
+ (b'Last-Modified', isomorphic_encode(time.strftime(u"%a, %d %b %Y %H:%M:%S GMT", time.gmtime())))]
+
+ body = u'''
+ const importTime = {time:8f};
+ '''.format(time=time.time())
+
+ return headers, body
diff --git a/test/wpt/tests/service-workers/service-worker/resources/update-max-aged-worker.py b/test/wpt/tests/service-workers/service-worker/resources/update-max-aged-worker.py
new file mode 100644
index 0000000..4f87906
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/update-max-aged-worker.py
@@ -0,0 +1,30 @@
+import time
+import json
+
+from wptserve.utils import isomorphic_decode, isomorphic_encode
+
+def main(request, response):
+ headers = [(b'Content-Type', b'application/javascript'),
+ (b'Cache-Control', b'max-age=86400'),
+ (b'Last-Modified', isomorphic_encode(time.strftime(u"%a, %d %b %Y %H:%M:%S GMT", time.gmtime())))]
+
+ test = request.GET[b'test']
+
+ body = u'''
+ const mainTime = {time:8f};
+ const testName = {test};
+ importScripts('update-max-aged-worker-imported-script.py');
+
+ addEventListener('message', event => {{
+ event.source.postMessage({{
+ mainTime,
+ importTime,
+ test: {test}
+ }});
+ }});
+ '''.format(
+ time=time.time(),
+ test=json.dumps(isomorphic_decode(test))
+ )
+
+ return headers, body
diff --git a/test/wpt/tests/service-workers/service-worker/resources/update-missing-import-scripts-imported-worker.py b/test/wpt/tests/service-workers/service-worker/resources/update-missing-import-scripts-imported-worker.py
new file mode 100644
index 0000000..1547cb5
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/update-missing-import-scripts-imported-worker.py
@@ -0,0 +1,9 @@
+def main(request, response):
+ key = request.GET[b'key']
+ already_requested = request.server.stash.take(key)
+
+ if already_requested is None:
+ request.server.stash.put(key, True)
+ return [(b'Content-Type', b'application/javascript')], b'// initial script'
+
+ response.status = (404, b'Not found: should not have been able to import this script twice!')
diff --git a/test/wpt/tests/service-workers/service-worker/resources/update-missing-import-scripts-main-worker.py b/test/wpt/tests/service-workers/service-worker/resources/update-missing-import-scripts-main-worker.py
new file mode 100644
index 0000000..1c447e1
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/update-missing-import-scripts-main-worker.py
@@ -0,0 +1,15 @@
+from wptserve.utils import isomorphic_decode
+
+def main(request, response):
+ key = request.GET[b'key']
+ already_requested = request.server.stash.take(key)
+
+ header = [(b'Content-Type', b'application/javascript')]
+ initial_script = u'importScripts("./update-missing-import-scripts-imported-worker.py?key={0}")'.format(isomorphic_decode(key))
+ updated_script = u'// removed importScripts()'
+
+ if already_requested is None:
+ request.server.stash.put(key, True)
+ return header, initial_script
+
+ return header, updated_script
diff --git a/test/wpt/tests/service-workers/service-worker/resources/update-nocookie-worker.py b/test/wpt/tests/service-workers/service-worker/resources/update-nocookie-worker.py
new file mode 100644
index 0000000..34eff02
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/update-nocookie-worker.py
@@ -0,0 +1,14 @@
+import random
+import time
+
+def main(request, response):
+ # no-cache itself to ensure the user agent finds a new version for each update.
+ headers = [(b'Cache-Control', b'no-cache, must-revalidate'),
+ (b'Pragma', b'no-cache')]
+
+ # Set a normal mimetype.
+ content_type = b'application/javascript'
+
+ headers.append((b'Content-Type', content_type))
+ # Return a different script for each access.
+ return headers, u'// %s %s' % (time.time(), random.random())
diff --git a/test/wpt/tests/service-workers/service-worker/resources/update-recovery-worker.py b/test/wpt/tests/service-workers/service-worker/resources/update-recovery-worker.py
new file mode 100644
index 0000000..9ac7ce7
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/update-recovery-worker.py
@@ -0,0 +1,25 @@
+def main(request, response):
+ # Set mode to 'init' for initial fetch.
+ mode = b'init'
+ if b'update-recovery-mode' in request.cookies:
+ mode = request.cookies[b'update-recovery-mode'].value
+
+ # no-cache itself to ensure the user agent finds a new version for each update.
+ headers = [(b'Cache-Control', b'no-cache, must-revalidate'),
+ (b'Pragma', b'no-cache')]
+
+ extra_body = b''
+
+ if mode == b'init':
+ # Install a bad service worker that will break the controlled
+ # document navigation.
+ response.set_cookie(b'update-recovery-mode', b'bad')
+ extra_body = b"addEventListener('fetch', function(e) { e.respondWith(Promise.reject()); });"
+ elif mode == b'bad':
+ # When the update tries to pull the script again, update to
+ # a worker service worker that does not break document
+ # navigation. Serve the same script from then on.
+ response.delete_cookie(b'update-recovery-mode')
+
+ headers.append((b'Content-Type', b'application/javascript'))
+ return headers, b'%s' % (extra_body)
diff --git a/test/wpt/tests/service-workers/service-worker/resources/update-registration-with-type.py b/test/wpt/tests/service-workers/service-worker/resources/update-registration-with-type.py
new file mode 100644
index 0000000..3cabc0f
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/update-registration-with-type.py
@@ -0,0 +1,33 @@
+def classic_script():
+ return b"""
+ importScripts('./imported-classic-script.js');
+ self.onmessage = e => {
+ e.source.postMessage(imported);
+ };
+ """
+
+def module_script():
+ return b"""
+ import * as module from './imported-module-script.js';
+ self.onmessage = e => {
+ e.source.postMessage(module.imported);
+ };
+ """
+
+# Returns the classic script for a first request and
+# returns the module script for second and subsequent requests.
+def main(request, response):
+ headers = [(b'Content-Type', b'application/javascript'),
+ (b'Pragma', b'no-store'),
+ (b'Cache-Control', b'no-store')]
+
+ classic_first = request.GET[b'classic_first']
+ key = request.GET[b'key']
+ requested_once = request.server.stash.take(key)
+ if requested_once is None:
+ request.server.stash.put(key, True)
+ body = classic_script() if classic_first == b'1' else module_script()
+ else:
+ body = module_script() if classic_first == b'1' else classic_script()
+
+ return 200, headers, body
diff --git a/test/wpt/tests/service-workers/service-worker/resources/update-smaller-body-after-update-worker.js b/test/wpt/tests/service-workers/service-worker/resources/update-smaller-body-after-update-worker.js
new file mode 100644
index 0000000..d43f6b2
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/update-smaller-body-after-update-worker.js
@@ -0,0 +1 @@
+// Hello world!
diff --git a/test/wpt/tests/service-workers/service-worker/resources/update-smaller-body-before-update-worker.js b/test/wpt/tests/service-workers/service-worker/resources/update-smaller-body-before-update-worker.js
new file mode 100644
index 0000000..30c8783
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/update-smaller-body-before-update-worker.js
@@ -0,0 +1,2 @@
+// Hello world!
+// **with extra body**
diff --git a/test/wpt/tests/service-workers/service-worker/resources/update-worker-from-file.py b/test/wpt/tests/service-workers/service-worker/resources/update-worker-from-file.py
new file mode 100644
index 0000000..ac0850f
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/update-worker-from-file.py
@@ -0,0 +1,33 @@
+import os
+
+from wptserve.utils import isomorphic_encode
+
+def serve_js_from_file(request, response, filename):
+ body = b''
+ path = os.path.join(os.path.dirname(isomorphic_encode(__file__)), filename)
+ with open(path, 'rb') as f:
+ body = f.read()
+ return (
+ [
+ (b'Cache-Control', b'no-cache, must-revalidate'),
+ (b'Pragma', b'no-cache'),
+ (b'Content-Type', b'application/javascript')
+ ], body)
+
+def main(request, response):
+ key = request.GET[b"Key"]
+
+ visited_count = request.server.stash.take(key)
+ if visited_count is None:
+ visited_count = 0
+
+ # Keep how many times the test requested this resource.
+ visited_count += 1
+ request.server.stash.put(key, visited_count)
+
+ # Serve a file based on how many times it's requested.
+ if visited_count == 1:
+ return serve_js_from_file(request, response, request.GET[b"First"])
+ if visited_count == 2:
+ return serve_js_from_file(request, response, request.GET[b"Second"])
+ raise u"Unknown state"
diff --git a/test/wpt/tests/service-workers/service-worker/resources/update-worker.py b/test/wpt/tests/service-workers/service-worker/resources/update-worker.py
new file mode 100644
index 0000000..5638a88
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/update-worker.py
@@ -0,0 +1,62 @@
+from urllib.parse import unquote
+
+from wptserve.utils import isomorphic_decode, isomorphic_encode
+
+def redirect_response(request, response, visited_count):
+ # |visited_count| is used as a unique id to differentiate responses
+ # every time.
+ location = b'empty.js'
+ if b'Redirect' in request.GET:
+ location = isomorphic_encode(unquote(isomorphic_decode(request.GET[b'Redirect'])))
+ return (301,
+ [
+ (b'Cache-Control', b'no-cache, must-revalidate'),
+ (b'Pragma', b'no-cache'),
+ (b'Content-Type', b'application/javascript'),
+ (b'Location', location),
+ ],
+ u'/* %s */' % str(visited_count))
+
+def not_found_response():
+ return 404, [(b'Content-Type', b'text/plain')], u"Page not found"
+
+def ok_response(request, response, visited_count,
+ extra_body=u'', mime_type=b'application/javascript'):
+ # |visited_count| is used as a unique id to differentiate responses
+ # every time.
+ return (
+ [
+ (b'Cache-Control', b'no-cache, must-revalidate'),
+ (b'Pragma', b'no-cache'),
+ (b'Content-Type', mime_type)
+ ],
+ u'/* %s */ %s' % (str(visited_count), extra_body))
+
+def main(request, response):
+ key = request.GET[b"Key"]
+ mode = request.GET[b"Mode"]
+
+ visited_count = request.server.stash.take(key)
+ if visited_count is None:
+ visited_count = 0
+
+ # Keep how many times the test requested this resource.
+ visited_count += 1
+ request.server.stash.put(key, visited_count)
+
+ # Return a response based on |mode| only when it's the second time (== update).
+ if visited_count == 2:
+ if mode == b'normal':
+ return ok_response(request, response, visited_count)
+ if mode == b'bad_mime_type':
+ return ok_response(request, response, visited_count, mime_type=b'text/html')
+ if mode == b'not_found':
+ return not_found_response()
+ if mode == b'redirect':
+ return redirect_response(request, response, visited_count)
+ if mode == b'syntax_error':
+ return ok_response(request, response, visited_count, extra_body=u'badsyntax(isbad;')
+ if mode == b'throw_install':
+ return ok_response(request, response, visited_count, extra_body=u"addEventListener('install', function(e) { throw new Error('boom'); });")
+
+ return ok_response(request, response, visited_count)
diff --git a/test/wpt/tests/service-workers/service-worker/resources/update/update-after-oneday.https.html b/test/wpt/tests/service-workers/service-worker/resources/update/update-after-oneday.https.html
new file mode 100644
index 0000000..9d4c982
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/update/update-after-oneday.https.html
@@ -0,0 +1,8 @@
+<body>
+<script>
+function load_image(url) {
+ var img = document.createElement('img');
+ img.src = url;
+}
+</script>
+</body>
diff --git a/test/wpt/tests/service-workers/service-worker/resources/update_shell.py b/test/wpt/tests/service-workers/service-worker/resources/update_shell.py
new file mode 100644
index 0000000..2070509
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/update_shell.py
@@ -0,0 +1,32 @@
+# This serves a different response to each request, to test service worker
+# updates. If |filename| is provided, it writes that file into the body.
+#
+# Usage:
+# navigator.serviceWorker.register('update_shell.py?filename=worker.js')
+#
+# This registers worker.js as a service worker, and every update check
+# will return a new response.
+import os
+import random
+import time
+
+from wptserve.utils import isomorphic_encode
+
+def main(request, response):
+ # Set no-cache to ensure the user agent finds a new version for each update.
+ headers = [(b'Cache-Control', b'no-cache, must-revalidate'),
+ (b'Pragma', b'no-cache'),
+ (b'Content-Type', b'application/javascript')]
+
+ # Return a different script for each access.
+ timestamp = u'// %s %s' % (time.time(), random.random())
+ body = isomorphic_encode(timestamp) + b'\n'
+
+ # Inject the file into the response.
+ if b'filename' in request.GET:
+ path = os.path.join(os.path.dirname(isomorphic_encode(__file__)),
+ request.GET[b'filename'])
+ with open(path, 'rb') as f:
+ body += f.read()
+
+ return headers, body
diff --git a/test/wpt/tests/service-workers/service-worker/resources/vtt-frame.html b/test/wpt/tests/service-workers/service-worker/resources/vtt-frame.html
new file mode 100644
index 0000000..c3ac803
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/vtt-frame.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Page Title</title>
+<video>
+ <track>
+</video>
diff --git a/test/wpt/tests/service-workers/service-worker/resources/wait-forever-in-install-worker.js b/test/wpt/tests/service-workers/service-worker/resources/wait-forever-in-install-worker.js
new file mode 100644
index 0000000..af85a73
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/wait-forever-in-install-worker.js
@@ -0,0 +1,12 @@
+var waitUntilResolve;
+self.addEventListener('install', function(event) {
+ event.waitUntil(new Promise(function(resolve) {
+ waitUntilResolve = resolve;
+ }));
+ });
+
+self.addEventListener('message', function(event) {
+ if (event.data === 'STOP_WAITING') {
+ waitUntilResolve();
+ }
+ });
diff --git a/test/wpt/tests/service-workers/service-worker/resources/websocket-worker.js b/test/wpt/tests/service-workers/service-worker/resources/websocket-worker.js
new file mode 100644
index 0000000..bb2dc81
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/websocket-worker.js
@@ -0,0 +1,35 @@
+let port;
+let received = false;
+
+function reportFailure(details) {
+ port.postMessage('FAIL: ' + details);
+}
+
+onmessage = event => {
+ port = event.source;
+
+ const ws = new WebSocket('wss://{{host}}:{{ports[wss][0]}}/echo');
+ ws.onopen = () => {
+ ws.send('Hello');
+ };
+ ws.onmessage = msg => {
+ if (msg.data !== 'Hello') {
+ reportFailure('Unexpected reply: ' + msg.data);
+ return;
+ }
+
+ received = true;
+ ws.close();
+ };
+ ws.onclose = (event) => {
+ if (!received) {
+ reportFailure('Closed before receiving reply: ' + event.code);
+ return;
+ }
+
+ port.postMessage('PASS');
+ };
+ ws.onerror = () => {
+ reportFailure('Got an error event');
+ };
+};
diff --git a/test/wpt/tests/service-workers/service-worker/resources/websocket.js b/test/wpt/tests/service-workers/service-worker/resources/websocket.js
new file mode 100644
index 0000000..fc6abd2
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/websocket.js
@@ -0,0 +1,7 @@
+self.urls = [];
+self.addEventListener('fetch', function(event) {
+ self.urls.push(event.request.url);
+ });
+self.addEventListener('message', function(event) {
+ event.data.port.postMessage({urls: self.urls});
+ });
diff --git a/test/wpt/tests/service-workers/service-worker/resources/window-opener.html b/test/wpt/tests/service-workers/service-worker/resources/window-opener.html
new file mode 100644
index 0000000..32d0744
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/window-opener.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<meta charset="utf-8"/>
+<meta name="referrer" content="origin">
+<script>
+function onLoad() {
+ self.onmessage = evt => {
+ if (self.opener)
+ self.opener.postMessage(evt.data, '*');
+ else
+ self.top.postMessage(evt.data, '*');
+ }
+ const params = new URLSearchParams(self.location.search);
+ const w = window.open(params.get('target'));
+ self.addEventListener('unload', evt => w.close());
+}
+self.addEventListener('load', onLoad);
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/resources/windowclient-navigate-worker.js b/test/wpt/tests/service-workers/service-worker/resources/windowclient-navigate-worker.js
new file mode 100644
index 0000000..383f666
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/windowclient-navigate-worker.js
@@ -0,0 +1,75 @@
+importScripts('/resources/testharness.js');
+
+function matchQuery(queryString) {
+ return self.location.search.substr(1) === queryString;
+}
+
+async function navigateTest(t, e) {
+ const port = e.data.port;
+ const url = e.data.url;
+ const expected = e.data.expected;
+
+ let p = clients.matchAll({ includeUncontrolled : true })
+ .then(function(clients) {
+ for (const client of clients) {
+ if (client.url === e.data.clientUrl) {
+ assert_equals(client.frameType, e.data.frameType);
+ return client.navigate(url);
+ }
+ }
+ throw 'Could not locate window client.';
+ }).then(function(newClient) {
+ // If we didn't reject, we better get resolved with the right thing.
+ if (newClient === null) {
+ assert_equals(newClient, expected);
+ } else {
+ assert_equals(newClient.url, expected);
+ }
+ });
+
+ if (typeof self[expected] === "function") {
+ // It's a JS error type name. We are expecting our promise to be rejected
+ // with that error.
+ p = promise_rejects_js(t, self[expected], p);
+ }
+
+ // Let our caller know we are done.
+ return p.finally(() => port.postMessage(null));
+}
+
+function getTestClient() {
+ return clients.matchAll({ includeUncontrolled: true })
+ .then(function(clients) {
+ for (const client of clients) {
+ if (client.url.includes('windowclient-navigate.https.html')) {
+ return client;
+ }
+ }
+
+ throw new Error('Service worker was unable to locate test client.');
+ });
+}
+
+function waitForMessage(client) {
+ const channel = new MessageChannel();
+ client.postMessage({ port: channel.port2 }, [channel.port2]);
+
+ return new Promise(function(resolve) {
+ channel.port1.onmessage = resolve;
+ });
+}
+
+// The worker must remain in the "installing" state for the duration of some
+// sub-tests. In order to achieve this coordination without relying on global
+// state, the worker must create a message channel with the client from within
+// the "install" event handler.
+if (matchQuery('installing')) {
+ self.addEventListener('install', function(e) {
+ e.waitUntil(getTestClient().then(waitForMessage));
+ });
+}
+
+self.addEventListener('message', function(e) {
+ e.waitUntil(promise_test(t => navigateTest(t, e),
+ e.data.description + " worker side"));
+});
diff --git a/test/wpt/tests/service-workers/service-worker/resources/worker-client-id-worker.js b/test/wpt/tests/service-workers/service-worker/resources/worker-client-id-worker.js
new file mode 100644
index 0000000..f592629
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/worker-client-id-worker.js
@@ -0,0 +1,25 @@
+addEventListener('fetch', evt => {
+ if (evt.request.url.includes('worker-echo-client-id.js')) {
+ evt.respondWith(new Response(
+ 'fetch("fetch-echo-client-id").then(r => r.text()).then(t => self.postMessage(t));',
+ { headers: { 'Content-Type': 'application/javascript' }}));
+ return;
+ }
+
+ if (evt.request.url.includes('fetch-echo-client-id')) {
+ evt.respondWith(new Response(evt.clientId));
+ return;
+ }
+
+ if (evt.request.url.includes('frame.html')) {
+ evt.respondWith(new Response(''));
+ return;
+ }
+});
+
+addEventListener('message', evt => {
+ if (evt.data === 'echo-client-id') {
+ evt.ports[0].postMessage(evt.source.id);
+ return;
+ }
+});
diff --git a/test/wpt/tests/service-workers/service-worker/resources/worker-fetching-cross-origin.js b/test/wpt/tests/service-workers/service-worker/resources/worker-fetching-cross-origin.js
new file mode 100644
index 0000000..a81bb3d
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/worker-fetching-cross-origin.js
@@ -0,0 +1,12 @@
+importScripts('/common/get-host-info.sub.js');
+importScripts('test-helpers.sub.js');
+
+self.addEventListener('fetch', event => {
+ const host_info = get_host_info();
+ // The sneaky Service Worker changes the same-origin 'square' request for a cross-origin image.
+ if (event.request.url.indexOf('square') != -1) {
+ const searchParams = new URLSearchParams(location.search);
+ const mode = searchParams.get("mode") || "cors";
+ event.respondWith(fetch(`${host_info['HTTPS_REMOTE_ORIGIN']}${base_path()}square.png`, { mode }));
+ }
+});
diff --git a/test/wpt/tests/service-workers/service-worker/resources/worker-interception-redirect-serviceworker.js b/test/wpt/tests/service-workers/service-worker/resources/worker-interception-redirect-serviceworker.js
new file mode 100644
index 0000000..d36b0b6
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/worker-interception-redirect-serviceworker.js
@@ -0,0 +1,53 @@
+let name;
+if (self.registration.scope.indexOf('scope1') != -1)
+ name = 'sw1';
+if (self.registration.scope.indexOf('scope2') != -1)
+ name = 'sw2';
+
+
+self.addEventListener('fetch', evt => {
+ // There are three types of requests this service worker handles.
+
+ // (1) The first request for the worker, which will redirect elsewhere.
+ // "redirect.py" means to test network redirect, so let network handle it.
+ if (evt.request.url.indexOf('redirect.py') != -1) {
+ return;
+ }
+ // "sw-redirect" means to test service worker redirect, so respond with a
+ // redirect.
+ if (evt.request.url.indexOf('sw-redirect') != -1) {
+ const url = new URL(evt.request.url);
+ const redirect_to = url.searchParams.get('Redirect');
+ evt.respondWith(Response.redirect(redirect_to));
+ return;
+ }
+
+ // (2) After redirect, the request is for a "webworker.py" URL.
+ // Add a search parameter to indicate this service worker handled the
+ // final request for the worker.
+ if (evt.request.url.indexOf('webworker.py') != -1) {
+ const greeting = encodeURIComponent(`${name} saw the request for the worker script`);
+ // Serve from `./subdir/`, not `./`,
+ // to conform that the base URL used in the worker is
+ // the response URL (`./subdir/`), not the current request URL (`./`).
+ evt.respondWith(fetch(`subdir/worker_interception_redirect_webworker.py?greeting=${greeting}`));
+ return;
+ }
+
+ const path = (new URL(evt.request.url)).pathname;
+
+ // (3) The worker does an importScripts() to import-scripts-echo.py. Indicate
+ // that this service worker handled the request.
+ if (evt.request.url.indexOf('import-scripts-echo.py') != -1) {
+ const msg = encodeURIComponent(`${name} saw importScripts from the worker: ${path}`);
+ evt.respondWith(fetch(`import-scripts-echo.py?msg=${msg}`));
+ return;
+ }
+
+ // (4) The worker does a fetch() to simple.txt. Indicate that this service
+ // worker handled the request.
+ if (evt.request.url.indexOf('simple.txt') != -1) {
+ evt.respondWith(new Response(`${name} saw the fetch from the worker: ${path}`));
+ return;
+ }
+});
diff --git a/test/wpt/tests/service-workers/service-worker/resources/worker-interception-redirect-webworker.js b/test/wpt/tests/service-workers/service-worker/resources/worker-interception-redirect-webworker.js
new file mode 100644
index 0000000..b7e6d81
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/worker-interception-redirect-webworker.js
@@ -0,0 +1,56 @@
+// This is the (shared or dedicated) worker file for the
+// worker-interception-redirect test. It should be served by the corresponding
+// .py file instead of being served directly.
+//
+// This file is served from both resources/*webworker.py,
+// resources/scope2/*webworker.py and resources/subdir/*webworker.py.
+// Relative paths are used in `fetch()` and `importScripts()` to confirm that
+// the correct base URLs are used.
+
+// This greeting text is meant to be injected by the Python script that serves
+// this file, to indicate how the script was served (from network or from
+// service worker).
+//
+// We can't just use a sub pipe and name this file .sub.js since we want
+// to serve the file from multiple URLs (see above).
+let greeting = '%GREETING_TEXT%';
+if (!greeting)
+ greeting = 'the worker script was served from network';
+
+// Call importScripts() which fills |echo_output| with a string indicating
+// whether a service worker intercepted the importScripts() request.
+let echo_output;
+const import_scripts_msg = encodeURIComponent(
+ 'importScripts: served from network');
+let import_scripts_greeting = 'not set';
+try {
+ importScripts(`import-scripts-echo.py?msg=${import_scripts_msg}`);
+ import_scripts_greeting = echo_output;
+} catch(e) {
+ import_scripts_greeting = 'importScripts failed';
+}
+
+async function runTest(port) {
+ port.postMessage(greeting);
+
+ port.postMessage(import_scripts_greeting);
+
+ const response = await fetch('simple.txt');
+ const text = await response.text();
+ port.postMessage('fetch(): ' + text);
+
+ port.postMessage(self.location.href);
+}
+
+if ('DedicatedWorkerGlobalScope' in self &&
+ self instanceof DedicatedWorkerGlobalScope) {
+ runTest(self);
+} else if (
+ 'SharedWorkerGlobalScope' in self &&
+ self instanceof SharedWorkerGlobalScope) {
+ self.onconnect = function(e) {
+ const port = e.ports[0];
+ port.start();
+ runTest(port);
+ };
+}
diff --git a/test/wpt/tests/service-workers/service-worker/resources/worker-load-interceptor.js b/test/wpt/tests/service-workers/service-worker/resources/worker-load-interceptor.js
new file mode 100644
index 0000000..ebc0db6
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/worker-load-interceptor.js
@@ -0,0 +1,16 @@
+importScripts('/common/get-host-info.sub.js');
+
+const response_text = 'This load was successfully intercepted.';
+const response_script =
+ `const message = 'This load was successfully intercepted.';`;
+
+self.onfetch = event => {
+ const url = event.request.url;
+ if (url.indexOf('synthesized-response.txt') != -1) {
+ event.respondWith(new Response(response_text));
+ } else if (url.indexOf('synthesized-response.js') != -1) {
+ event.respondWith(new Response(
+ response_script,
+ {headers: {'Content-Type': 'application/javascript'}}));
+ }
+};
diff --git a/test/wpt/tests/service-workers/service-worker/resources/worker-testharness.js b/test/wpt/tests/service-workers/service-worker/resources/worker-testharness.js
new file mode 100644
index 0000000..73e97be
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/worker-testharness.js
@@ -0,0 +1,49 @@
+/*
+ * worker-test-harness should be considered a temporary polyfill around
+ * testharness.js for supporting Service Worker based tests. It should not be
+ * necessary once the test harness is able to drive worker based tests natively.
+ * See https://github.com/w3c/testharness.js/pull/82 for status of effort to
+ * update upstream testharness.js. Once the upstreaming is complete, tests that
+ * reference worker-test-harness should be updated to directly import
+ * testharness.js.
+ */
+
+importScripts('/resources/testharness.js');
+
+(function() {
+ var next_cache_index = 1;
+
+ // Returns a promise that resolves to a newly created Cache object. The
+ // returned Cache will be destroyed when |test| completes.
+ function create_temporary_cache(test) {
+ var uniquifier = String(++next_cache_index);
+ var cache_name = self.location.pathname + '/' + uniquifier;
+
+ test.add_cleanup(function() {
+ return self.caches.delete(cache_name);
+ });
+
+ return self.caches.delete(cache_name)
+ .then(function() {
+ return self.caches.open(cache_name);
+ });
+ }
+
+ self.create_temporary_cache = create_temporary_cache;
+})();
+
+// Runs |test_function| with a temporary unique Cache passed in as the only
+// argument. The function is run as a part of Promise chain owned by
+// promise_test(). As such, it is expected to behave in a manner identical (with
+// the exception of the argument) to a function passed into promise_test().
+//
+// E.g.:
+// cache_test(function(cache) {
+// // Do something with |cache|, which is a Cache object.
+// }, "Some Cache test");
+function cache_test(test_function, description) {
+ promise_test(function(test) {
+ return create_temporary_cache(test)
+ .then(test_function);
+ }, description);
+}
diff --git a/test/wpt/tests/service-workers/service-worker/resources/worker_interception_redirect_webworker.py b/test/wpt/tests/service-workers/service-worker/resources/worker_interception_redirect_webworker.py
new file mode 100644
index 0000000..4ed5bee
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/worker_interception_redirect_webworker.py
@@ -0,0 +1,20 @@
+# This serves the worker JavaScript file. It takes a |greeting| request
+# parameter to inject into the JavaScript to indicate how the request
+# reached the server.
+import os
+
+from wptserve.utils import isomorphic_decode
+
+def main(request, response):
+ path = os.path.join(os.path.dirname(isomorphic_decode(__file__)),
+ u"worker-interception-redirect-webworker.js")
+ body = open(path, u"rb").read()
+ if b"greeting" in request.GET:
+ body = body.replace(b"%GREETING_TEXT%", request.GET[b"greeting"])
+ else:
+ body = body.replace(b"%GREETING_TEXT%", b"")
+
+ headers = []
+ headers.append((b"Content-Type", b"text/javascript"))
+
+ return headers, body
diff --git a/test/wpt/tests/service-workers/service-worker/resources/xhr-content-length-worker.js b/test/wpt/tests/service-workers/service-worker/resources/xhr-content-length-worker.js
new file mode 100644
index 0000000..604deec
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/xhr-content-length-worker.js
@@ -0,0 +1,22 @@
+// Service worker for the xhr-content-length test.
+
+self.addEventListener("fetch", event => {
+ const url = new URL(event.request.url);
+ const type = url.searchParams.get("type");
+
+ if (type === "no-content-length") {
+ event.respondWith(new Response("Hello!"));
+ }
+
+ if (type === "larger-content-length") {
+ event.respondWith(new Response("meeeeh", { headers: [["Content-Length", "10000"]] }));
+ }
+
+ if (type === "double-content-length") {
+ event.respondWith(new Response("meeeeh", { headers: [["Content-Length", "10000"], ["Content-Length", "10000"]] }));
+ }
+
+ if (type === "bogus-content-length") {
+ event.respondWith(new Response("meeeeh", { headers: [["Content-Length", "test"]] }));
+ }
+});
diff --git a/test/wpt/tests/service-workers/service-worker/resources/xhr-iframe.html b/test/wpt/tests/service-workers/service-worker/resources/xhr-iframe.html
new file mode 100644
index 0000000..4c57bbb
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/xhr-iframe.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>iframe for xhr tests</title>
+<script>
+async function xhr(url, options) {
+ return new Promise((resolve, reject) => {
+ const xhr = new XMLHttpRequest();
+ const opts = options ? options : {};
+ xhr.onload = () => {
+ resolve(xhr);
+ };
+ xhr.onerror = () => {
+ reject('xhr failed');
+ };
+
+ xhr.open('GET', url);
+ if (opts.responseType) {
+ xhr.responseType = opts.responseType;
+ }
+ xhr.send();
+ });
+}
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/resources/xhr-response-url-worker.js b/test/wpt/tests/service-workers/service-worker/resources/xhr-response-url-worker.js
new file mode 100644
index 0000000..906ad50
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/xhr-response-url-worker.js
@@ -0,0 +1,32 @@
+// Service worker for the xhr-response-url test.
+
+self.addEventListener('fetch', event => {
+ const url = new URL(event.request.url);
+ const respondWith = url.searchParams.get('respondWith');
+ if (!respondWith)
+ return;
+
+ if (respondWith == 'fetch') {
+ const target = url.searchParams.get('url');
+ event.respondWith(fetch(target));
+ return;
+ }
+
+ if (respondWith == 'string') {
+ const headers = {'content-type': 'text/plain'};
+ event.respondWith(new Response('hello', {headers}));
+ return;
+ }
+
+ if (respondWith == 'document') {
+ const doc = `
+ <!DOCTYPE html>
+ <html>
+ <title>hi</title>
+ <body>hello</body>
+ </html>`;
+ const headers = {'content-type': 'text/html'};
+ event.respondWith(new Response(doc, {headers}));
+ return;
+ }
+});
diff --git a/test/wpt/tests/service-workers/service-worker/resources/xsl-base-url-iframe.xml b/test/wpt/tests/service-workers/service-worker/resources/xsl-base-url-iframe.xml
new file mode 100644
index 0000000..065a07a
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/xsl-base-url-iframe.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0"?>
+<?xml-stylesheet type="text/xsl" href="resources/request-url-path/import-relative.xsl"?>
+<stylesheet-test>
+This tests a stylesheet which has a xsl:import with a relative URL.
+</stylesheet-test>
diff --git a/test/wpt/tests/service-workers/service-worker/resources/xsl-base-url-worker.js b/test/wpt/tests/service-workers/service-worker/resources/xsl-base-url-worker.js
new file mode 100644
index 0000000..50e2b18
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/xsl-base-url-worker.js
@@ -0,0 +1,12 @@
+self.addEventListener('fetch', event => {
+ const url = new URL(event.request.url);
+
+ // For the import-relative.xsl file, respond in a way that changes the
+ // response URL. This is expected to change the base URL and allow the import
+ // from the file to succeed.
+ const path = 'request-url-path/import-relative.xsl';
+ if (url.pathname.indexOf(path) != -1) {
+ // Respond with a different URL, deleting "request-url-path/".
+ event.respondWith(fetch('import-relative.xsl'));
+ }
+});
diff --git a/test/wpt/tests/service-workers/service-worker/resources/xslt-pass.xsl b/test/wpt/tests/service-workers/service-worker/resources/xslt-pass.xsl
new file mode 100644
index 0000000..2cd7f2f
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/resources/xslt-pass.xsl
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
+ <xsl:template match="/">
+ <html>
+ <body>
+ <p>PASS</p>
+ </body>
+ </html>
+ </xsl:template>
+</xsl:stylesheet>
diff --git a/test/wpt/tests/service-workers/service-worker/respond-with-body-accessed-response.https.html b/test/wpt/tests/service-workers/service-worker/respond-with-body-accessed-response.https.html
new file mode 100644
index 0000000..f6713d8
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/respond-with-body-accessed-response.https.html
@@ -0,0 +1,54 @@
+<!DOCTYPE html>
+<title>Service Worker responds with .body accessed response.</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+let frame;
+
+promise_test(t => {
+ const SCOPE = 'resources/respond-with-body-accessed-response-iframe.html';
+ const SCRIPT = 'resources/respond-with-body-accessed-response-worker.js';
+
+ return service_worker_unregister_and_register(t, SCRIPT, SCOPE)
+ .then(reg => {
+ promise_test(t => {
+ if (frame)
+ frame.remove();
+ return reg.unregister();
+ }, 'restore global state');
+ return wait_for_state(t, reg.installing, 'activated');
+ })
+ .then(() => { return with_iframe(SCOPE); })
+ .then(f => { frame = f; });
+ }, 'initialize global state');
+
+const TEST_CASES = [
+ "type=basic",
+ "type=opaque",
+ "type=default",
+ "type=basic&clone=1",
+ "type=opaque&clone=1",
+ "type=default&clone=1",
+ "type=basic&clone=2",
+ "type=opaque&clone=2",
+ "type=default&clone=2",
+ "type=basic&passThroughCache=true",
+ "type=opaque&passThroughCache=true",
+ "type=default&passThroughCache=true",
+ "type=basic&clone=1&passThroughCache=true",
+ "type=opaque&clone=1&passThroughCache=true",
+ "type=default&clone=1&passThroughCache=true",
+ "type=basic&clone=2&passThroughCache=true",
+ "type=opaque&clone=2&passThroughCache=true",
+ "type=default&clone=2&passThroughCache=true",
+];
+
+TEST_CASES.forEach(param => {
+ promise_test(t => {
+ const url = 'TestRequest?' + param;
+ return frame.contentWindow.getJSONP(url)
+ .then(result => { assert_equals(result, 'OK'); });
+ }, 'test: ' + param);
+ });
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/same-site-cookies.https.html b/test/wpt/tests/service-workers/service-worker/same-site-cookies.https.html
new file mode 100644
index 0000000..1d9b60d
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/same-site-cookies.https.html
@@ -0,0 +1,496 @@
+<!DOCTYPE html>
+<meta charset="utf-8"/>
+<meta name="timeout" content="long">
+<title>Service Worker: Same-site cookie behavior</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script>
+<script src="/cookies/resources/cookie-helper.sub.js"></script>
+<body>
+<script>
+'use strict';
+
+const COOKIE_VALUE = 'COOKIE_VALUE';
+
+function make_nested_url(nested_origins, target_url) {
+ for (let i = nested_origins.length - 1; i >= 0; --i) {
+ target_url = new URL(
+ `./resources/nested-parent.html?target=${encodeURIComponent(target_url)}`,
+ nested_origins[i] + self.location.pathname);
+ }
+ return target_url;
+}
+
+const scopepath = '/cookies/resources/postToParent.py?with-sw';
+
+async function unregister_service_worker(origin, nested_origins=[]) {
+ let target_url = origin +
+ '/service-workers/service-worker/resources/unregister-rewrite-worker.html' +
+ '?scopepath=' + encodeURIComponent(scopepath);
+ target_url = make_nested_url(nested_origins, target_url);
+ const w = window.open(target_url);
+ try {
+ await wait_for_message('SW-UNREGISTERED');
+ } finally {
+ w.close();
+ }
+}
+
+async function register_service_worker(origin, nested_origins=[]) {
+ let target_url = origin +
+ '/service-workers/service-worker/resources/register-rewrite-worker.html' +
+ '?scopepath=' + encodeURIComponent(scopepath);
+ target_url = make_nested_url(nested_origins, target_url);
+ const w = window.open(target_url);
+ try {
+ await wait_for_message('SW-REGISTERED');
+ } finally {
+ w.close();
+ }
+}
+
+async function run_test(t, origin, navaction, swaction, expected,
+ redirect_origins=[], nested_origins=[]) {
+ if (swaction === 'navpreload') {
+ assert_true('navigationPreload' in ServiceWorkerRegistration.prototype,
+ 'navigation preload must be supported');
+ }
+ const sw_param = swaction === 'no-sw' ? 'no-sw' : 'with-sw';
+ let action_param = '';
+ if (swaction === 'fallback') {
+ action_param = '&ignore';
+ } else if (swaction !== 'no-sw') {
+ action_param = '&' + swaction;
+ }
+ const navpreload_param = swaction === 'navpreload' ? '&navpreload' : '';
+ const change_request_param = swaction === 'change-request' ? '&change-request' : '';
+ const target_string = origin + `/cookies/resources/postToParent.py?` +
+ `${sw_param}${action_param}`
+ let target_url = new URL(target_string);
+
+ for (let i = redirect_origins.length - 1; i >= 0; --i) {
+ const redirect_url = new URL(
+ `./resources/redirect.py?Status=307&Redirect=${encodeURIComponent(target_url)}`,
+ redirect_origins[i] + self.location.pathname);
+ target_url = redirect_url;
+ }
+
+ if (navaction === 'window.open') {
+ target_url = new URL(
+ `./resources/window-opener.html?target=${encodeURIComponent(target_url)}`,
+ self.origin + self.location.pathname);
+ } else if (navaction === 'form post') {
+ target_url = new URL(
+ `./resources/form-poster.html?target=${encodeURIComponent(target_url)}`,
+ self.origin + self.location.pathname);
+ } else if (navaction === 'set location') {
+ target_url = new URL(
+ `./resources/location-setter.html?target=${encodeURIComponent(target_url)}`,
+ self.origin + self.location.pathname);
+ }
+
+ const w = window.open(make_nested_url(nested_origins, target_url));
+ t.add_cleanup(() => w.close());
+
+ const result = await wait_for_message('COOKIES');
+ verifySameSiteCookieState(expected, COOKIE_VALUE, result.data);
+}
+
+promise_test(async t => {
+ await resetSameSiteCookies(self.origin, COOKIE_VALUE);
+ await register_service_worker(self.origin);
+
+ await resetSameSiteCookies(SECURE_SUBDOMAIN_ORIGIN, COOKIE_VALUE);
+ await register_service_worker(SECURE_SUBDOMAIN_ORIGIN);
+
+ await resetSameSiteCookies(SECURE_CROSS_SITE_ORIGIN, COOKIE_VALUE);
+ await register_service_worker(SECURE_CROSS_SITE_ORIGIN);
+
+ await register_service_worker(self.origin,
+ [self.origin, SECURE_CROSS_SITE_ORIGIN]);
+}, 'Setup service workers');
+
+promise_test(t => {
+ return run_test(t, self.origin, 'window.open', 'no-sw',
+ SameSiteStatus.STRICT);
+}, 'same-origin, window.open with no service worker');
+
+promise_test(t => {
+ return run_test(t, self.origin, 'window.open', 'fallback',
+ SameSiteStatus.STRICT);
+}, 'same-origin, window.open with fallback');
+
+promise_test(t => {
+ return run_test(t, self.origin, 'window.open', 'passthrough',
+ SameSiteStatus.STRICT);
+}, 'same-origin, window.open with passthrough');
+
+promise_test(t => {
+ return run_test(t, self.origin, 'window.open', 'change-request',
+ SameSiteStatus.STRICT);
+}, 'same-origin, window.open with change-request');
+
+promise_test(t => {
+ return run_test(t, self.origin, 'window.open', 'navpreload',
+ SameSiteStatus.STRICT);
+}, 'same-origin, window.open with navpreload');
+
+promise_test(t => {
+ return run_test(t, SECURE_SUBDOMAIN_ORIGIN, 'window.open', 'no-sw',
+ SameSiteStatus.STRICT);
+}, 'same-site, window.open with no service worker');
+
+promise_test(t => {
+ return run_test(t, SECURE_SUBDOMAIN_ORIGIN, 'window.open', 'fallback',
+ SameSiteStatus.STRICT);
+}, 'same-site, window.open with fallback');
+
+promise_test(t => {
+ return run_test(t, SECURE_SUBDOMAIN_ORIGIN, 'window.open', 'passthrough',
+ SameSiteStatus.STRICT);
+}, 'same-site, window.open with passthrough');
+
+promise_test(t => {
+ return run_test(t, SECURE_SUBDOMAIN_ORIGIN, 'window.open', 'change-request',
+ SameSiteStatus.STRICT);
+}, 'same-site, window.open with change-request');
+
+promise_test(t => {
+ return run_test(t, SECURE_SUBDOMAIN_ORIGIN, 'window.open', 'navpreload',
+ SameSiteStatus.STRICT);
+}, 'same-site, window.open with navpreload');
+
+promise_test(t => {
+ return run_test(t, SECURE_CROSS_SITE_ORIGIN, 'window.open', 'no-sw',
+ SameSiteStatus.LAX);
+}, 'cross-site, window.open with no service worker');
+
+promise_test(t => {
+ return run_test(t, SECURE_CROSS_SITE_ORIGIN, 'window.open', 'fallback',
+ SameSiteStatus.LAX);
+}, 'cross-site, window.open with fallback');
+
+promise_test(t => {
+ return run_test(t, SECURE_CROSS_SITE_ORIGIN, 'window.open', 'passthrough',
+ SameSiteStatus.LAX);
+}, 'cross-site, window.open with passthrough');
+
+promise_test(t => {
+ return run_test(t, SECURE_CROSS_SITE_ORIGIN, 'window.open', 'change-request',
+ SameSiteStatus.STRICT);
+}, 'cross-site, window.open with change-request');
+
+promise_test(t => {
+ return run_test(t, SECURE_CROSS_SITE_ORIGIN, 'window.open', 'navpreload',
+ SameSiteStatus.LAX);
+}, 'cross-site, window.open with navpreload');
+
+//
+// window.open redirect tests
+//
+promise_test(t => {
+ return run_test(t, self.origin, 'window.open', 'no-sw',
+ SameSiteStatus.STRICT, [SECURE_SUBDOMAIN_ORIGIN]);
+}, 'same-origin, window.open with no service worker and same-site redirect');
+
+promise_test(t => {
+ return run_test(t, self.origin, 'window.open', 'fallback',
+ SameSiteStatus.STRICT, [SECURE_SUBDOMAIN_ORIGIN]);
+}, 'same-origin, window.open with fallback and same-site redirect');
+
+promise_test(t => {
+ return run_test(t, self.origin, 'window.open', 'passthrough',
+ SameSiteStatus.STRICT, [SECURE_SUBDOMAIN_ORIGIN]);
+}, 'same-origin, window.open with passthrough and same-site redirect');
+
+promise_test(t => {
+ return run_test(t, self.origin, 'window.open', 'change-request',
+ SameSiteStatus.STRICT, [SECURE_SUBDOMAIN_ORIGIN]);
+}, 'same-origin, window.open with change-request and same-site redirect');
+
+promise_test(t => {
+ return run_test(t, self.origin, 'window.open', 'navpreload',
+ SameSiteStatus.STRICT, [SECURE_SUBDOMAIN_ORIGIN]);
+}, 'same-origin, window.open with navpreload and same-site redirect');
+
+promise_test(t => {
+ return run_test(t, self.origin, 'window.open', 'no-sw',
+ SameSiteStatus.LAX, [SECURE_CROSS_SITE_ORIGIN]);
+}, 'same-origin, window.open with no service worker and cross-site redirect');
+
+promise_test(t => {
+ return run_test(t, self.origin, 'window.open', 'fallback',
+ SameSiteStatus.LAX, [SECURE_CROSS_SITE_ORIGIN]);
+}, 'same-origin, window.open with fallback and cross-site redirect');
+
+promise_test(t => {
+ return run_test(t, self.origin, 'window.open', 'passthrough',
+ SameSiteStatus.LAX, [SECURE_CROSS_SITE_ORIGIN]);
+}, 'same-origin, window.open with passthrough and cross-site redirect');
+
+promise_test(t => {
+ return run_test(t, self.origin, 'window.open', 'change-request',
+ SameSiteStatus.STRICT, [SECURE_CROSS_SITE_ORIGIN]);
+}, 'same-origin, window.open with change-request and cross-site redirect');
+
+promise_test(t => {
+ return run_test(t, self.origin, 'window.open', 'navpreload',
+ SameSiteStatus.LAX, [SECURE_CROSS_SITE_ORIGIN]);
+}, 'same-origin, window.open with navpreload and cross-site redirect');
+
+promise_test(t => {
+ return run_test(t, self.origin, 'window.open', 'no-sw',
+ SameSiteStatus.LAX, [SECURE_CROSS_SITE_ORIGIN, self.origin]);
+}, 'same-origin, window.open with no service worker, cross-site redirect, and ' +
+ 'same-origin redirect');
+
+promise_test(t => {
+ return run_test(t, self.origin, 'window.open', 'fallback',
+ SameSiteStatus.LAX, [SECURE_CROSS_SITE_ORIGIN, self.origin]);
+}, 'same-origin, window.open with fallback, cross-site redirect, and ' +
+ 'same-origin redirect');
+
+promise_test(t => {
+ return run_test(t, self.origin, 'window.open', 'passthrough',
+ SameSiteStatus.LAX, [SECURE_CROSS_SITE_ORIGIN, self.origin]);
+}, 'same-origin, window.open with passthrough, cross-site redirect, and ' +
+ 'same-origin redirect');
+
+promise_test(t => {
+ return run_test(t, self.origin, 'window.open', 'change-request',
+ SameSiteStatus.STRICT, [SECURE_CROSS_SITE_ORIGIN, self.origin]);
+}, 'same-origin, window.open with change-request, cross-site redirect, and ' +
+ 'same-origin redirect');
+
+promise_test(t => {
+ return run_test(t, self.origin, 'window.open', 'navpreload',
+ SameSiteStatus.LAX, [SECURE_CROSS_SITE_ORIGIN, self.origin]);
+}, 'same-origin, window.open with navpreload, cross-site redirect, and ' +
+ 'same-origin redirect');
+
+//
+// Double-nested frame calling open.window() tests
+//
+promise_test(t => {
+ return run_test(t, self.origin, 'window.open', 'no-sw',
+ SameSiteStatus.STRICT, [],
+ [self.origin, SECURE_CROSS_SITE_ORIGIN]);
+}, 'same-origin, nested window.open with cross-site middle frame and ' +
+ 'no service worker');
+
+promise_test(t => {
+ return run_test(t, self.origin, 'window.open', 'fallback',
+ SameSiteStatus.STRICT, [],
+ [self.origin, SECURE_CROSS_SITE_ORIGIN]);
+}, 'same-origin, nested window.open with cross-site middle frame and ' +
+ 'fallback service worker');
+
+promise_test(t => {
+ return run_test(t, self.origin, 'window.open', 'passthrough',
+ SameSiteStatus.STRICT, [],
+ [self.origin, SECURE_CROSS_SITE_ORIGIN]);
+}, 'same-origin, nested window.open with cross-site middle frame and ' +
+ 'passthrough service worker');
+
+promise_test(t => {
+ return run_test(t, self.origin, 'window.open', 'change-request',
+ SameSiteStatus.STRICT, [],
+ [self.origin, SECURE_CROSS_SITE_ORIGIN]);
+}, 'same-origin, nested window.open with cross-site middle frame and ' +
+ 'change-request service worker');
+
+promise_test(t => {
+ return run_test(t, self.origin, 'window.open', 'navpreload',
+ SameSiteStatus.STRICT, [],
+ [self.origin, SECURE_CROSS_SITE_ORIGIN]);
+}, 'same-origin, nested window.open with cross-site middle frame and ' +
+ 'navpreload service worker');
+
+//
+// Double-nested frame setting location tests
+//
+promise_test(t => {
+ return run_test(t, self.origin, 'set location', 'no-sw',
+ SameSiteStatus.CROSS_SITE, [],
+ [self.origin, SECURE_CROSS_SITE_ORIGIN]);
+}, 'same-origin, nested set location with cross-site middle frame and ' +
+ 'no service worker');
+
+promise_test(t => {
+ return run_test(t, self.origin, 'set location', 'fallback',
+ SameSiteStatus.CROSS_SITE, [],
+ [self.origin, SECURE_CROSS_SITE_ORIGIN]);
+}, 'same-origin, nested set location with cross-site middle frame and ' +
+ 'fallback service worker');
+
+promise_test(t => {
+ return run_test(t, self.origin, 'set location', 'passthrough',
+ SameSiteStatus.CROSS_SITE, [],
+ [self.origin, SECURE_CROSS_SITE_ORIGIN]);
+}, 'same-origin, nested set location with cross-site middle frame and ' +
+ 'passthrough service worker');
+
+promise_test(t => {
+ return run_test(t, self.origin, 'set location', 'change-request',
+ SameSiteStatus.CROSS_SITE, [],
+ [self.origin, SECURE_CROSS_SITE_ORIGIN]);
+}, 'same-origin, nested set location with cross-site middle frame and ' +
+ 'change-request service worker');
+
+promise_test(t => {
+ return run_test(t, self.origin, 'set location', 'navpreload',
+ SameSiteStatus.CROSS_SITE, [],
+ [self.origin, SECURE_CROSS_SITE_ORIGIN]);
+}, 'same-origin, nested set location with cross-site middle frame and ' +
+ 'navpreload service worker');
+
+//
+// Form POST tests
+//
+promise_test(t => {
+ return run_test(t, self.origin, 'form post', 'no-sw', SameSiteStatus.STRICT);
+}, 'same-origin, form post with no service worker');
+
+promise_test(t => {
+ return run_test(t, self.origin, 'form post', 'fallback',
+ SameSiteStatus.STRICT);
+}, 'same-origin, form post with fallback');
+
+promise_test(t => {
+ return run_test(t, self.origin, 'form post', 'passthrough',
+ SameSiteStatus.STRICT);
+}, 'same-origin, form post with passthrough');
+
+promise_test(t => {
+ return run_test(t, self.origin, 'form post', 'change-request',
+ SameSiteStatus.STRICT);
+}, 'same-origin, form post with change-request');
+
+promise_test(t => {
+ return run_test(t, SECURE_SUBDOMAIN_ORIGIN, 'form post', 'no-sw',
+ SameSiteStatus.STRICT);
+}, 'same-site, form post with no service worker');
+
+promise_test(t => {
+ return run_test(t, SECURE_SUBDOMAIN_ORIGIN, 'form post', 'fallback',
+ SameSiteStatus.STRICT);
+}, 'same-site, form post with fallback');
+
+promise_test(t => {
+ return run_test(t, SECURE_SUBDOMAIN_ORIGIN, 'form post', 'passthrough',
+ SameSiteStatus.STRICT);
+}, 'same-site, form post with passthrough');
+
+promise_test(t => {
+ return run_test(t, SECURE_SUBDOMAIN_ORIGIN, 'form post', 'change-request',
+ SameSiteStatus.STRICT);
+}, 'same-site, form post with change-request');
+
+promise_test(t => {
+ return run_test(t, SECURE_CROSS_SITE_ORIGIN, 'form post', 'no-sw',
+ SameSiteStatus.CROSS_SITE);
+}, 'cross-site, form post with no service worker');
+
+promise_test(t => {
+ return run_test(t, SECURE_CROSS_SITE_ORIGIN, 'form post', 'fallback',
+ SameSiteStatus.CROSS_SITE);
+}, 'cross-site, form post with fallback');
+
+promise_test(t => {
+ return run_test(t, SECURE_CROSS_SITE_ORIGIN, 'form post', 'passthrough',
+ SameSiteStatus.CROSS_SITE);
+}, 'cross-site, form post with passthrough');
+
+promise_test(t => {
+ return run_test(t, SECURE_CROSS_SITE_ORIGIN, 'form post', 'change-request',
+ SameSiteStatus.STRICT);
+}, 'cross-site, form post with change-request');
+
+//
+// Form POST redirect tests
+//
+promise_test(t => {
+ return run_test(t, self.origin, 'form post', 'no-sw',
+ SameSiteStatus.STRICT, [SECURE_SUBDOMAIN_ORIGIN]);
+}, 'same-origin, form post with no service worker and same-site redirect');
+
+promise_test(t => {
+ return run_test(t, self.origin, 'form post', 'fallback',
+ SameSiteStatus.STRICT, [SECURE_SUBDOMAIN_ORIGIN]);
+}, 'same-origin, form post with fallback and same-site redirect');
+
+promise_test(t => {
+ return run_test(t, self.origin, 'form post', 'passthrough',
+ SameSiteStatus.STRICT, [SECURE_SUBDOMAIN_ORIGIN]);
+}, 'same-origin, form post with passthrough and same-site redirect');
+
+promise_test(t => {
+ return run_test(t, self.origin, 'form post', 'change-request',
+ SameSiteStatus.STRICT, [SECURE_SUBDOMAIN_ORIGIN]);
+}, 'same-origin, form post with change-request and same-site redirect');
+
+// navpreload is not supported for POST requests
+
+promise_test(t => {
+ return run_test(t, self.origin, 'form post', 'no-sw',
+ SameSiteStatus.CROSS_SITE, [SECURE_CROSS_SITE_ORIGIN]);
+}, 'same-origin, form post with no service worker and cross-site redirect');
+
+promise_test(t => {
+ return run_test(t, self.origin, 'form post', 'fallback',
+ SameSiteStatus.CROSS_SITE, [SECURE_CROSS_SITE_ORIGIN]);
+}, 'same-origin, form post with fallback and cross-site redirect');
+
+promise_test(t => {
+ return run_test(t, self.origin, 'form post', 'passthrough',
+ SameSiteStatus.CROSS_SITE, [SECURE_CROSS_SITE_ORIGIN]);
+}, 'same-origin, form post with passthrough and cross-site redirect');
+
+promise_test(t => {
+ return run_test(t, self.origin, 'form post', 'change-request',
+ SameSiteStatus.STRICT, [SECURE_CROSS_SITE_ORIGIN]);
+}, 'same-origin, form post with change-request and cross-site redirect');
+
+// navpreload is not supported for POST requests
+
+promise_test(t => {
+ return run_test(t, self.origin, 'form post', 'no-sw',
+ SameSiteStatus.CROSS_SITE, [SECURE_CROSS_SITE_ORIGIN,
+ self.origin]);
+}, 'same-origin, form post with no service worker, cross-site redirect, and ' +
+ 'same-origin redirect');
+
+promise_test(t => {
+ return run_test(t, self.origin, 'form post', 'fallback',
+ SameSiteStatus.CROSS_SITE, [SECURE_CROSS_SITE_ORIGIN,
+ self.origin]);
+}, 'same-origin, form post with fallback, cross-site redirect, and ' +
+ 'same-origin redirect');
+
+promise_test(t => {
+ return run_test(t, self.origin, 'form post', 'passthrough',
+ SameSiteStatus.CROSS_SITE, [SECURE_CROSS_SITE_ORIGIN,
+ self.origin]);
+}, 'same-origin, form post with passthrough, cross-site redirect, and ' +
+ 'same-origin redirect');
+
+promise_test(t => {
+ return run_test(t, self.origin, 'form post', 'change-request',
+ SameSiteStatus.STRICT, [SECURE_CROSS_SITE_ORIGIN,
+ self.origin]);
+}, 'same-origin, form post with change-request, cross-site redirect, and ' +
+ 'same-origin redirect');
+
+// navpreload is not supported for POST requests
+
+promise_test(async t => {
+ await unregister_service_worker(self.origin);
+ await unregister_service_worker(SECURE_SUBDOMAIN_ORIGIN);
+ await unregister_service_worker(SECURE_CROSS_SITE_ORIGIN);
+ await unregister_service_worker(self.origin,
+ [self.origin, SECURE_CROSS_SITE_ORIGIN]);
+}, 'Cleanup service workers');
+
+</script>
+</body>
diff --git a/test/wpt/tests/service-workers/service-worker/sandboxed-iframe-fetch-event.https.html b/test/wpt/tests/service-workers/service-worker/sandboxed-iframe-fetch-event.https.html
new file mode 100644
index 0000000..ba34e79
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/sandboxed-iframe-fetch-event.https.html
@@ -0,0 +1,536 @@
+<!DOCTYPE html>
+<title>ServiceWorker FetchEvent for sandboxed iframe.</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+var lastCallbackId = 0;
+var callbacks = {};
+function doTest(frame, type) {
+ return new Promise(function(resolve) {
+ var id = ++lastCallbackId;
+ callbacks[id] = resolve;
+ frame.contentWindow.postMessage({id: id, type: type}, '*');
+ });
+}
+
+// Asks the service worker for data about requests and clients seen. The
+// worker posts a message back with |data| where:
+// |data.requests|: the requests the worker received FetchEvents for
+// |data.clients|: the URLs of all the worker's clients
+// The worker clears its data after responding.
+function getResultsFromWorker(worker) {
+ return new Promise(resolve => {
+ let channel = new MessageChannel();
+ channel.port1.onmessage = msg => {
+ resolve(msg.data);
+ };
+ worker.postMessage({port: channel.port2}, [channel.port2]);
+ });
+}
+
+window.onmessage = function (e) {
+ message = e.data;
+ var id = message['id'];
+ var callback = callbacks[id];
+ delete callbacks[id];
+ callback(message['result']);
+};
+
+const SCOPE = 'resources/sandboxed-iframe-fetch-event-iframe.py';
+const SCRIPT = 'resources/sandboxed-iframe-fetch-event-worker.js';
+const expected_base_url = new URL(SCOPE, location.href);
+// Service worker controlling |SCOPE|.
+let worker;
+// A normal iframe.
+// This should be controlled by a service worker.
+let normal_frame;
+// An iframe created by <iframe sandbox='allow-scripts'>.
+// This should NOT be controlled by a service worker.
+let sandboxed_frame;
+// An iframe created by <iframe sandbox='allow-scripts allow-same-origin'>.
+// This should be controlled by a service worker.
+let sandboxed_same_origin_frame;
+// An iframe whose response header has
+// 'Content-Security-Policy: allow-scripts'.
+// This should NOT be controlled by a service worker.
+let sandboxed_frame_by_header;
+// An iframe whose response header has
+// 'Content-Security-Policy: allow-scripts allow-same-origin'.
+// This should be controlled by a service worker.
+let sandboxed_same_origin_frame_by_header;
+
+promise_test(t => {
+ return service_worker_unregister_and_register(t, SCRIPT, SCOPE)
+ .then(function(registration) {
+ add_completion_callback(() => registration.unregister());
+ worker = registration.installing;
+ return wait_for_state(t, registration.installing, 'activated');
+ });
+}, 'Prepare a service worker.');
+
+promise_test(t => {
+ return with_iframe(SCOPE + '?iframe')
+ .then(f => {
+ normal_frame = f;
+ add_completion_callback(() => f.remove());
+ return getResultsFromWorker(worker);
+ })
+ .then(data => {
+ let requests = data.requests;
+ assert_equals(requests.length, 1);
+ assert_equals(requests[0], expected_base_url + '?iframe');
+ assert_true(data.clients.includes(expected_base_url + '?iframe'));
+ });
+}, 'Prepare a normal iframe.');
+
+promise_test(t => {
+ return with_sandboxed_iframe(SCOPE + '?sandboxed-iframe', 'allow-scripts')
+ .then(f => {
+ sandboxed_frame = f;
+ add_completion_callback(() => f.remove());
+ return getResultsFromWorker(worker);
+ })
+ .then(data => {
+ assert_equals(data.requests.length, 0);
+ assert_false(data.clients.includes(expected_base_url +
+ '?sandboxed-iframe'));
+ });
+}, 'Prepare an iframe sandboxed by <iframe sandbox="allow-scripts">.');
+
+promise_test(t => {
+ return with_sandboxed_iframe(SCOPE + '?sandboxed-iframe-same-origin',
+ 'allow-scripts allow-same-origin')
+ .then(f => {
+ sandboxed_same_origin_frame = f;
+ add_completion_callback(() => f.remove());
+ return getResultsFromWorker(worker);
+ })
+ .then(data => {
+ let requests = data.requests;
+ assert_equals(requests.length, 1);
+ assert_equals(requests[0],
+ expected_base_url + '?sandboxed-iframe-same-origin');
+ assert_true(data.clients.includes(
+ expected_base_url + '?sandboxed-iframe-same-origin'));
+ })
+}, 'Prepare an iframe sandboxed by ' +
+ '<iframe sandbox="allow-scripts allow-same-origin">.');
+
+promise_test(t => {
+ const iframe_full_url = expected_base_url + '?sandbox=allow-scripts&' +
+ 'sandboxed-frame-by-header';
+ return with_iframe(iframe_full_url)
+ .then(f => {
+ sandboxed_frame_by_header = f;
+ add_completion_callback(() => f.remove());
+ return getResultsFromWorker(worker);
+ })
+ .then(data => {
+ let requests = data.requests;
+ assert_equals(requests.length, 1,
+ 'Service worker should provide the response');
+ assert_equals(requests[0], iframe_full_url);
+ assert_false(data.clients.includes(iframe_full_url),
+ 'Service worker should NOT control the sandboxed page');
+ });
+}, 'Prepare an iframe sandboxed by CSP HTTP header with allow-scripts.');
+
+promise_test(t => {
+ const iframe_full_url =
+ expected_base_url + '?sandbox=allow-scripts%20allow-same-origin&' +
+ 'sandboxed-iframe-same-origin-by-header';
+ return with_iframe(iframe_full_url)
+ .then(f => {
+ sandboxed_same_origin_frame_by_header = f;
+ add_completion_callback(() => f.remove());
+ return getResultsFromWorker(worker);
+ })
+ .then(data => {
+ let requests = data.requests;
+ assert_equals(requests.length, 1);
+ assert_equals(requests[0], iframe_full_url);
+ assert_true(data.clients.includes(iframe_full_url));
+ })
+}, 'Prepare an iframe sandboxed by CSP HTTP header with allow-scripts and ' +
+ 'allow-same-origin.');
+
+promise_test(t => {
+ let frame = normal_frame;
+ return doTest(frame, 'fetch')
+ .then(result => {
+ assert_equals(result, 'done');
+ return getResultsFromWorker(worker);
+ })
+ .then(data => {
+ let requests = data.requests;
+ assert_equals(requests.length, 1,
+ 'The fetch request should be handled by SW.');
+ assert_equals(requests[0], frame.src + '&test=fetch');
+ });
+}, 'Fetch request from a normal iframe');
+
+promise_test(t => {
+ let frame = normal_frame;
+ return doTest(frame, 'fetch-from-worker')
+ .then(result => {
+ assert_equals(result, 'done');
+ return getResultsFromWorker(worker);
+ })
+ .then(data => {
+ let requests = data.requests;
+ assert_equals(requests.length, 1,
+ 'The fetch request should be handled by SW.');
+ assert_equals(requests[0], frame.src + '&test=fetch-from-worker');
+ });
+}, 'Fetch request from a worker in a normal iframe');
+
+promise_test(t => {
+ let frame = normal_frame;
+ return doTest(frame, 'iframe')
+ .then(result => {
+ assert_equals(result, 'done');
+ return getResultsFromWorker(worker);
+ })
+ .then(data => {
+ let requests = data.requests;
+ assert_equals(requests.length, 1,
+ 'The request should be handled by SW.');
+ assert_equals(requests[0], frame.src + '&test=iframe');
+ assert_true(data.clients.includes(frame.src + '&test=iframe'));
+
+ });
+}, 'Request for an iframe in the normal iframe');
+
+promise_test(t => {
+ let frame = normal_frame;
+ return doTest(frame, 'sandboxed-iframe')
+ .then(result => {
+ assert_equals(result, 'done');
+ return getResultsFromWorker(worker);
+ })
+ .then(data => {
+ assert_equals(data.requests.length, 0,
+ 'The request should NOT be handled by SW.');
+ assert_false(data.clients.includes(
+ frame.src + '&test=sandboxed-iframe'));
+ });
+}, 'Request for an sandboxed iframe with allow-scripts flag in the normal ' +
+ 'iframe');
+
+promise_test(t => {
+ let frame = normal_frame;
+ return doTest(frame, 'sandboxed-iframe-same-origin')
+ .then(result => {
+ assert_equals(result, 'done');
+ return getResultsFromWorker(worker);
+ })
+ .then(data => {
+ let requests = data.requests;
+ assert_equals(requests.length, 1,
+ 'The request should be handled by SW.');
+ assert_equals(requests[0],
+ frame.src + '&test=sandboxed-iframe-same-origin');
+ assert_true(data.clients.includes(
+ frame.src + '&test=sandboxed-iframe-same-origin'));
+ });
+}, 'Request for an sandboxed iframe with allow-scripts and ' +
+ 'allow-same-origin flag in the normal iframe');
+
+promise_test(t => {
+ let frame = sandboxed_frame;
+ return doTest(frame, 'fetch')
+ .then(result => {
+ assert_equals(result, 'done');
+ return getResultsFromWorker(worker);
+ })
+ .then(data => {
+ assert_equals(data.requests.length, 0,
+ 'The fetch request should NOT be handled by SW.');
+ });
+}, 'Fetch request from iframe sandboxed by an attribute with allow-scripts ' +
+ 'flag');
+
+promise_test(t => {
+ let frame = sandboxed_frame;
+ return doTest(frame, 'fetch-from-worker')
+ .then(result => {
+ assert_equals(result, 'done');
+ return getResultsFromWorker(worker);
+ })
+ .then(data => {
+ assert_equals(data.requests.length, 0,
+ 'The fetch request should NOT be handled by SW.');
+ });
+}, 'Fetch request from a worker in iframe sandboxed by an attribute with ' +
+ 'allow-scripts flag');
+
+promise_test(t => {
+ let frame = sandboxed_frame;
+ return doTest(frame, 'iframe')
+ .then(result => {
+ assert_equals(result, 'done');
+ return getResultsFromWorker(worker);
+ })
+ .then(data => {
+ assert_equals(data.requests.length, 0,
+ 'The request should NOT be handled by SW.');
+ assert_false(data.clients.includes(frame.src + '&test=iframe'));
+ });
+}, 'Request for an iframe in the iframe sandboxed by an attribute with ' +
+ 'allow-scripts flag');
+
+promise_test(t => {
+ let frame = sandboxed_frame;
+ return doTest(frame, 'sandboxed-iframe')
+ .then(result => {
+ assert_equals(result, 'done');
+ return getResultsFromWorker(worker);
+ })
+ .then(data => {
+ assert_equals(data.requests.length, 0,
+ 'The request should NOT be handled by SW.');
+ assert_false(data.clients.includes(
+ frame.src + '&test=sandboxed-iframe'));
+ });
+}, 'Request for an sandboxed iframe with allow-scripts flag in the iframe ' +
+ 'sandboxed by an attribute with allow-scripts flag');
+
+promise_test(t => {
+ let frame = sandboxed_frame;
+ return doTest(frame, 'sandboxed-iframe-same-origin')
+ .then(result => {
+ assert_equals(result, 'done');
+ return getResultsFromWorker(worker);
+ })
+ .then(data => {
+ assert_equals(data.requests.length, 0,
+ 'The request should NOT be handled by SW.');
+ assert_false(data.clients.includes(
+ frame.src + '&test=sandboxed-iframe-same-origin'));
+ });
+}, 'Request for an sandboxed iframe with allow-scripts and ' +
+ 'allow-same-origin flag in the iframe sandboxed by an attribute with ' +
+ 'allow-scripts flag');
+
+promise_test(t => {
+ let frame = sandboxed_same_origin_frame;
+ return doTest(frame, 'fetch')
+ .then(result => {
+ assert_equals(result, 'done');
+ return getResultsFromWorker(worker);
+ })
+ .then(data => {
+ let requests = data.requests;
+ assert_equals(requests.length, 1,
+ 'The fetch request should be handled by SW.');
+ assert_equals(requests[0], frame.src + '&test=fetch');
+ });
+}, 'Fetch request from iframe sandboxed by an attribute with allow-scripts ' +
+ 'and allow-same-origin flag');
+
+promise_test(t => {
+ let frame = sandboxed_same_origin_frame;
+ return doTest(frame, 'fetch-from-worker')
+ .then(result => {
+ assert_equals(result, 'done');
+ return getResultsFromWorker(worker);
+ })
+ .then(data => {
+ let requests = data.requests;
+ assert_equals(requests.length, 1,
+ 'The fetch request should be handled by SW.');
+ assert_equals(requests[0],
+ frame.src + '&test=fetch-from-worker');
+ });
+}, 'Fetch request from a worker in iframe sandboxed by an attribute with ' +
+ 'allow-scripts and allow-same-origin flag');
+
+promise_test(t => {
+ let frame = sandboxed_same_origin_frame;
+ return doTest(frame, 'iframe')
+ .then(result => {
+ assert_equals(result, 'done');
+ return getResultsFromWorker(worker);
+ })
+ .then(data => {
+ let requests = data.requests;
+ assert_equals(requests.length, 1,
+ 'The request should be handled by SW.');
+ assert_equals(requests[0], frame.src + '&test=iframe');
+ assert_true(data.clients.includes(frame.src + '&test=iframe'));
+ });
+}, 'Request for an iframe in the iframe sandboxed by an attribute with ' +
+ 'allow-scripts and allow-same-origin flag');
+
+promise_test(t => {
+ let frame = sandboxed_same_origin_frame;
+ return doTest(frame, 'sandboxed-iframe')
+ .then(result => {
+ assert_equals(result, 'done');
+ return getResultsFromWorker(worker);
+ })
+ .then(data => {
+ assert_equals(data.requests.length, 0,
+ 'The request should NOT be handled by SW.');
+ assert_false(data.clients.includes(
+ frame.src + '&test=sandboxed-iframe'));
+ });
+}, 'Request for an sandboxed iframe with allow-scripts flag in the iframe ' +
+ 'sandboxed by attribute with allow-scripts and allow-same-origin flag');
+
+promise_test(t => {
+ let frame = sandboxed_same_origin_frame;
+ return doTest(frame, 'sandboxed-iframe-same-origin')
+ .then(result => {
+ assert_equals(result, 'done');
+ return getResultsFromWorker(worker);
+ })
+ .then(data => {
+ let requests = data.requests;
+ assert_equals(requests.length, 1,
+ 'The request should be handled by SW.');
+ assert_equals(requests[0],
+ frame.src + '&test=sandboxed-iframe-same-origin');
+ assert_true(data.clients.includes(
+ frame.src + '&test=sandboxed-iframe-same-origin'));
+ });
+}, 'Request for an sandboxed iframe with allow-scripts and ' +
+ 'allow-same-origin flag in the iframe sandboxed by attribute with ' +
+ 'allow-scripts and allow-same-origin flag');
+
+promise_test(t => {
+ let frame = sandboxed_frame_by_header;
+ return doTest(frame, 'fetch')
+ .then(result => {
+ assert_equals(result, 'done');
+ return getResultsFromWorker(worker);
+ })
+ .then(data => {
+ assert_equals(data.requests.length, 0,
+ 'The request should NOT be handled by SW.');
+ });
+}, 'Fetch request from iframe sandboxed by CSP HTTP header with ' +
+ 'allow-scripts flag');
+
+promise_test(t => {
+ let frame = sandboxed_frame_by_header;
+ return doTest(frame, 'iframe')
+ .then(result => {
+ assert_equals(result, 'done');
+ return getResultsFromWorker(worker);
+ })
+ .then(data => {
+ assert_equals(data.requests.length, 0,
+ 'The request should NOT be handled by SW.');
+ assert_false(data.clients.includes(frame.src + '&test=iframe'));
+ });
+}, 'Request for an iframe in the iframe sandboxed by CSP HTTP header with ' +
+ 'allow-scripts flag');
+
+promise_test(t => {
+ let frame = sandboxed_frame_by_header;
+ return doTest(frame, 'sandboxed-iframe')
+ .then(result => {
+ assert_equals(result, 'done');
+ return getResultsFromWorker(worker);
+ })
+ .then(data => {
+ assert_equals(data.requests.length, 0,
+ 'The request should NOT be handled by SW.');
+ assert_false(data.clients.includes(
+ frame.src + '&test=sandboxed-iframe'));
+ });
+}, 'Request for an sandboxed iframe with allow-scripts flag in the iframe ' +
+ 'sandboxed by CSP HTTP header with allow-scripts flag');
+
+promise_test(t => {
+ let frame = sandboxed_frame_by_header;
+ return doTest(frame, 'sandboxed-iframe-same-origin')
+ .then(result => {
+ assert_equals(result, 'done');
+ return getResultsFromWorker(worker);
+ })
+ .then(data => {
+ assert_equals(data.requests.length, 0,
+ 'The request should NOT be handled by SW.');
+ assert_false(data.clients.includes(
+ frame.src + '&test=sandboxed-iframe-same-origin'));
+ });
+}, 'Request for an sandboxed iframe with allow-scripts and ' +
+ 'allow-same-origin flag in the iframe sandboxed by CSP HTTP header with ' +
+ 'allow-scripts flag');
+
+promise_test(t => {
+ let frame = sandboxed_same_origin_frame_by_header;
+ return doTest(frame, 'fetch')
+ .then(result => {
+ assert_equals(result, 'done');
+ return getResultsFromWorker(worker);
+ })
+ .then(data => {
+ let requests = data.requests;
+ assert_equals(requests.length, 1,
+ 'The request should be handled by SW.');
+ assert_equals(requests[0], frame.src + '&test=fetch');
+ });
+}, 'Fetch request from iframe sandboxed by CSP HTTP header with ' +
+ 'allow-scripts and allow-same-origin flag');
+
+promise_test(t => {
+ let frame = sandboxed_same_origin_frame_by_header;
+ return doTest(frame, 'iframe')
+ .then(result => {
+ assert_equals(result, 'done');
+ return getResultsFromWorker(worker);
+ })
+ .then(data => {
+ let requests = data.requests;
+ assert_equals(requests.length, 1,
+ 'The request should be handled by SW.');
+ assert_equals(requests[0], frame.src + '&test=iframe');
+ assert_true(data.clients.includes(frame.src + '&test=iframe'));
+ });
+}, 'Request for an iframe in the iframe sandboxed by CSP HTTP header with ' +
+ 'allow-scripts and allow-same-origin flag');
+
+promise_test(t => {
+ let frame = sandboxed_same_origin_frame_by_header;
+ return doTest(frame, 'sandboxed-iframe')
+ .then(result => {
+ assert_equals(result, 'done');
+ return getResultsFromWorker(worker);
+ })
+ .then(data => {
+ assert_equals(data.requests.length, 0,
+ 'The request should NOT be handled by SW.');
+ assert_false(
+ data.clients.includes(frame.src + '&test=sandboxed-iframe'));
+ });
+}, 'Request for an sandboxed iframe with allow-scripts flag in the ' +
+ 'iframe sandboxed by CSP HTTP header with allow-scripts and ' +
+ 'allow-same-origin flag');
+
+promise_test(t => {
+ let frame = sandboxed_same_origin_frame_by_header;
+ return doTest(frame, 'sandboxed-iframe-same-origin')
+ .then(result => {
+ assert_equals(result, 'done');
+ return getResultsFromWorker(worker);
+ })
+ .then(data => {
+ let requests = data.requests;
+ assert_equals(requests.length, 1,
+ 'The request should be handled by SW.');
+ assert_equals(requests[0],
+ frame.src + '&test=sandboxed-iframe-same-origin');
+ assert_true(data.clients.includes(
+ frame.src + '&test=sandboxed-iframe-same-origin'));
+ });
+}, 'Request for an sandboxed iframe with allow-scripts and ' +
+ 'allow-same-origin flag in the iframe sandboxed by CSP HTTP header with ' +
+ 'allow-scripts and allow-same-origin flag');
+</script>
+</body>
diff --git a/test/wpt/tests/service-workers/service-worker/sandboxed-iframe-navigator-serviceworker.https.html b/test/wpt/tests/service-workers/service-worker/sandboxed-iframe-navigator-serviceworker.https.html
new file mode 100644
index 0000000..70be6ef
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/sandboxed-iframe-navigator-serviceworker.https.html
@@ -0,0 +1,120 @@
+<!DOCTYPE html>
+<title>Accessing navigator.serviceWorker in sandboxed iframe.</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+var lastCallbackId = 0;
+var callbacks = {};
+function postMessageAndWaitResult(frame) {
+ return new Promise(function(resolve, reject) {
+ var id = ++lastCallbackId;
+ callbacks[id] = resolve;
+ frame.contentWindow.postMessage({id:id}, '*');
+ const timeout = 1000;
+ step_timeout(() => reject("no msg back after " + timeout + "ms"), timeout);
+ });
+}
+
+window.onmessage = function(e) {
+ message = e.data;
+ var id = message['id'];
+ var callback = callbacks[id];
+ delete callbacks[id];
+ callback(message.result);
+};
+
+promise_test(function(t) {
+ var url = 'resources/sandboxed-iframe-navigator-serviceworker-iframe.html';
+ var frame;
+ return with_iframe(url)
+ .then(function(f) {
+ frame = f;
+ add_result_callback(() => { frame.remove(); });
+ return postMessageAndWaitResult(f);
+ })
+ .then(function(result) {
+ assert_equals(result, 'ok');
+ });
+ }, 'Accessing navigator.serviceWorker in normal iframe should not throw.');
+
+promise_test(function(t) {
+ var url = 'resources/sandboxed-iframe-navigator-serviceworker-iframe.html';
+ var frame;
+ return with_sandboxed_iframe(url, 'allow-scripts')
+ .then(function(f) {
+ frame = f;
+ add_result_callback(() => { frame.remove(); });
+ return postMessageAndWaitResult(f);
+ })
+ .then(function(result) {
+ assert_equals(
+ result,
+ 'navigator.serviceWorker failed: SecurityError');
+ });
+ }, 'Accessing navigator.serviceWorker in sandboxed iframe should throw.');
+
+promise_test(function(t) {
+ var url = 'resources/sandboxed-iframe-navigator-serviceworker-iframe.html';
+ var frame;
+ return with_sandboxed_iframe(url, 'allow-scripts allow-same-origin')
+ .then(function(f) {
+ frame = f;
+ add_result_callback(() => { frame.remove(); });
+ return postMessageAndWaitResult(f);
+ })
+ .then(function(result) {
+ assert_equals(result, 'ok');
+ });
+ },
+ 'Accessing navigator.serviceWorker in sandboxed iframe with ' +
+ 'allow-same-origin flag should not throw.');
+
+promise_test(function(t) {
+ var url = 'resources/sandboxed-iframe-navigator-serviceworker-iframe.html';
+ var frame;
+ return new Promise(function(resolve) {
+ frame = document.createElement('iframe');
+ add_result_callback(() => { frame.remove(); });
+ frame.sandbox = '';
+ frame.src = url;
+ frame.onload = resolve;
+ document.body.appendChild(frame);
+ // Switch the sandbox attribute while loading the iframe.
+ frame.sandbox = 'allow-scripts allow-same-origin';
+ })
+ .then(function() {
+ return postMessageAndWaitResult(frame)
+ })
+ .then(function(result) {
+ // The HTML spec seems to say that changing the sandbox attribute
+ // after the iframe is inserted into its parent document does not
+ // affect the sandboxing. If that's true, the frame should still
+ // act as if it still doesn't have
+ // 'allow-scripts allow-same-origin' set and throw a SecurityError.
+ //
+ // 1) From Section 4.8.5 "The iframe element":
+ // "When an iframe element is inserted into a document that has a
+ // browsing context, the user agent must create a new browsing
+ // context..."
+ // 2) "Create a new browsing context" expands to Section 7.1
+ // "Browsing contexts", which includes creating a Document and
+ // "Implement the sandboxing for document."
+ // 3) "Implement the sandboxing" expands to Section 7.6 "Sandboxing",
+ // which includes "populate document's active sandboxing flag set".
+ //
+ // It's not clear whether navigation subsequently creates a new
+ // Document, but I'm assuming it wouldn't.
+ // https://html.spec.whatwg.org/multipage/embedded-content.html#attr-iframe-sandbox
+ assert_true(
+ false,
+ 'should NOT get message back from a sandboxed frame where scripts are not allowed to execute');
+ })
+ .catch(msg => {
+ assert_true(msg.startsWith('no msg back'), 'expecting error message "no msg back"');
+ });
+ }, 'Switching iframe sandbox attribute while loading the iframe');
+
+</script>
+</body>
diff --git a/test/wpt/tests/service-workers/service-worker/secure-context.https.html b/test/wpt/tests/service-workers/service-worker/secure-context.https.html
new file mode 100644
index 0000000..666a5d3
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/secure-context.https.html
@@ -0,0 +1,57 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Ensure service worker is bypassed in insecure contexts</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+
+// This test checks that an HTTPS iframe embedded in an HTTP document is not
+// loaded via a service worker, since it's not a secure context. To that end, we
+// first register a service worker, wait for its activation, and create an
+// iframe that is controlled by said service worker. We use the iframe as a
+// way to receive messages from the service worker.
+// The bulk of the test begins by opening an HTTP window with the noopener
+// option, installing a message event handler, and embedding an HTTPS iframe. If
+// the browser behaves correctly then the iframe will be loaded from the network
+// and will contain a script that posts a message to the parent window,
+// informing it that it was loaded from the network. If, however, the iframe is
+// intercepted, the service worker will return a page with a script that posts a
+// message to the parent window, informing it that it was intercepted.
+// Upon getting either result, the window will report the result to the service
+// worker by navigating to a reporting URL. The service worker will then inform
+// all clients about the result, including the controlled iframe from the
+// beginning of the test. The message event handler will verify that the result
+// is as expected, concluding the test.
+promise_test(t => {
+ const SCRIPT = "resources/secure-context-service-worker.js";
+ const SCOPE = "resources/";
+ const HTTP_IFRAME_URL = get_host_info().HTTP_ORIGIN + base_path() + SCOPE + "secure-context/window.html";
+ return service_worker_unregister_and_register(t, SCRIPT, SCOPE)
+ .then(registration => {
+ t.add_cleanup(() => {
+ return registration.unregister();
+ });
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(() => {
+ return with_iframe(SCOPE + "blank.html");
+ })
+ .then(iframe => {
+ t.add_cleanup(() => {
+ iframe.remove();
+ });
+ return new Promise(resolve => {
+ iframe.contentWindow.navigator.serviceWorker.onmessage = t.step_func(event => {
+ assert_equals(event.data, 'network');
+ resolve();
+ });
+ window.open(HTTP_IFRAME_URL, 'MyWindow', 'noopener');
+ });
+ });
+})
+
+</script>
+</body>
diff --git a/test/wpt/tests/service-workers/service-worker/service-worker-csp-connect.https.html b/test/wpt/tests/service-workers/service-worker/service-worker-csp-connect.https.html
new file mode 100644
index 0000000..226f4a4
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/service-worker-csp-connect.https.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<title>Service Worker: CSP connect directive for ServiceWorker script</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js?pipe=sub"></script>
+<script>
+service_worker_test(
+ 'resources/service-worker-csp-worker.py?directive=connect',
+ 'CSP test for connect-src in ServiceWorkerGlobalScope');
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/service-worker-csp-default.https.html b/test/wpt/tests/service-workers/service-worker/service-worker-csp-default.https.html
new file mode 100644
index 0000000..1d4e762
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/service-worker-csp-default.https.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<title>Service Worker: CSP default directive for ServiceWorker script</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js?pipe=sub"></script>
+<script>
+service_worker_test(
+ 'resources/service-worker-csp-worker.py?directive=default',
+ 'CSP test for default-src in ServiceWorkerGlobalScope');
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/service-worker-csp-script.https.html b/test/wpt/tests/service-workers/service-worker/service-worker-csp-script.https.html
new file mode 100644
index 0000000..14c2eb7
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/service-worker-csp-script.https.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<title>Service Worker: CSP script directive for ServiceWorker script</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js?pipe=sub"></script>
+<script>
+service_worker_test(
+ 'resources/service-worker-csp-worker.py?directive=script',
+ 'CSP test for script-src in ServiceWorkerGlobalScope');
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/service-worker-header.https.html b/test/wpt/tests/service-workers/service-worker/service-worker-header.https.html
new file mode 100644
index 0000000..fb902cd
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/service-worker-header.https.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<title>Service Worker: Service-Worker header</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+
+promise_test(async t => {
+ const script = 'resources/service-worker-header.py'
+ + '?header&import=service-worker-header.py?no-header';
+ const scope = 'resources/service-worker-header';
+ const expected_url = normalizeURL(script);
+ const registration =
+ await service_worker_unregister_and_register(t, script, scope);
+ t.add_cleanup(() => registration.unregister());
+ assert_true(registration instanceof ServiceWorkerRegistration);
+
+ await wait_for_state(t, registration.installing, 'activated');
+ await registration.update();
+}, 'A request to fetch service worker main script should have Service-Worker '
+ + 'header and imported scripts should not have one');
+
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/serviceworker-message-event-historical.https.html b/test/wpt/tests/service-workers/service-worker/serviceworker-message-event-historical.https.html
new file mode 100644
index 0000000..fac8f20
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/serviceworker-message-event-historical.https.html
@@ -0,0 +1,45 @@
+<!DOCTYPE html>
+<title>Service Worker: ServiceWorkerMessageEvent</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+
+promise_test(function(t) {
+ var scope = 'resources/blank.html';
+ var url = 'resources/postmessage-to-client-worker.js';
+ return service_worker_unregister_and_register(t, url, scope)
+ .then(function(r) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ return wait_for_state(t, r.installing, 'activated');
+ })
+ .then(function() {
+ return with_iframe(scope);
+ })
+ .then(function(frame) {
+ var w = frame.contentWindow;
+ var worker = w.navigator.serviceWorker.controller;
+ assert_equals(
+ self.ServiceWorkerMessageEvent, undefined,
+ 'ServiceWorkerMessageEvent should not be defined.');
+ return new Promise(function(resolve) {
+ w.navigator.serviceWorker.onmessage = t.step_func(function(e) {
+ assert_true(
+ e instanceof w.MessageEvent,
+ 'message events should use MessageEvent interface.');
+ assert_true(e.source instanceof w.ServiceWorker);
+ assert_equals(e.type, 'message');
+ assert_equals(e.source, worker,
+ 'source should equal to the controller.');
+ assert_equals(e.ports.length, 0);
+ resolve();
+ });
+ worker.postMessage('PING');
+ });
+ });
+ }, 'Test MessageEvent supplants ServiceWorkerMessageEvent.');
+
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/serviceworkerobject-scripturl.https.html b/test/wpt/tests/service-workers/service-worker/serviceworkerobject-scripturl.https.html
new file mode 100644
index 0000000..6004985
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/serviceworkerobject-scripturl.https.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<title>ServiceWorker object: scriptURL property</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+'use strict';
+
+function url_test(name, url) {
+ const scope = 'resources/scope/' + name;
+ const expectedURL = normalizeURL(url);
+
+ promise_test(async t => {
+ const registration =
+ await service_worker_unregister_and_register(t, url, scope);
+ const worker = registration.installing;
+ assert_equals(worker.scriptURL, expectedURL, 'scriptURL');
+ await registration.unregister();
+ }, 'Verify the scriptURL property: ' + name);
+}
+
+url_test('relative', 'resources/empty-worker.js');
+url_test('with-fragment', 'resources/empty-worker.js#ref');
+url_test('with-query', 'resources/empty-worker.js?ref');
+url_test('absolute', normalizeURL('./resources/empty-worker.js'));
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/skip-waiting-installed.https.html b/test/wpt/tests/service-workers/service-worker/skip-waiting-installed.https.html
new file mode 100644
index 0000000..b604f65
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/skip-waiting-installed.https.html
@@ -0,0 +1,70 @@
+<!DOCTYPE html>
+<title>Service Worker: Skip waiting installed worker</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+'use strict';
+
+promise_test(function(t) {
+ var scope = 'resources/blank.html?skip-waiting-installed';
+ var url1 = 'resources/empty.js';
+ var url2 = 'resources/skip-waiting-installed-worker.js';
+ var frame, frame_sw, service_worker, registration, onmessage, oncontrollerchanged;
+ var saw_message = new Promise(function(resolve) {
+ onmessage = function(e) {
+ resolve(e.data);
+ };
+ })
+ .then(function(message) {
+ assert_equals(
+ message, 'PASS',
+ 'skipWaiting promise should be resolved with undefined');
+ });
+ var saw_controllerchanged = new Promise(function(resolve) {
+ oncontrollerchanged = function() {
+ assert_equals(
+ frame_sw.controller.scriptURL, normalizeURL(url2),
+ 'Controller scriptURL should change to the second one');
+ assert_equals(registration.active.scriptURL, normalizeURL(url2),
+ 'Worker which calls skipWaiting should be active by controllerchange');
+ resolve();
+ };
+ });
+ return service_worker_unregister_and_register(t, url1, scope)
+ .then(function(r) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ return wait_for_state(t, r.installing, 'activated');
+ })
+ .then(function() {
+ return with_iframe(scope);
+ })
+ .then(function(f) {
+ frame = f;
+ frame_sw = f.contentWindow.navigator.serviceWorker;
+ assert_equals(
+ frame_sw.controller.scriptURL, normalizeURL(url1),
+ 'Document controller scriptURL should equal to the first one');
+ frame_sw.oncontrollerchange = t.step_func(oncontrollerchanged);
+ return navigator.serviceWorker.register(url2, {scope: scope});
+ })
+ .then(function(r) {
+ registration = r;
+ service_worker = r.installing;
+ return wait_for_state(t, service_worker, 'installed');
+ })
+ .then(function() {
+ var channel = new MessageChannel();
+ channel.port1.onmessage = t.step_func(onmessage);
+ service_worker.postMessage({port: channel.port2}, [channel.port2]);
+ return Promise.all([saw_message, saw_controllerchanged]);
+ })
+ .then(function() {
+ frame.remove();
+ });
+ }, 'Test skipWaiting when a installed worker is waiting');
+
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/skip-waiting-using-registration.https.html b/test/wpt/tests/service-workers/service-worker/skip-waiting-using-registration.https.html
new file mode 100644
index 0000000..412ee2a
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/skip-waiting-using-registration.https.html
@@ -0,0 +1,66 @@
+<!DOCTYPE html>
+<title>Service Worker: Skip waiting using registration</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+'use strict';
+
+promise_test(function(t) {
+ var scope = 'resources/blank.html?skip-waiting-using-registration';
+ var url1 = 'resources/empty.js';
+ var url2 = 'resources/skip-waiting-worker.js';
+ var frame, frame_sw, sw_registration, oncontrollerchanged;
+ var saw_controllerchanged = new Promise(function(resolve) {
+ oncontrollerchanged = function(e) {
+ resolve(e);
+ };
+ })
+ .then(function(e) {
+ assert_equals(e.type, 'controllerchange',
+ 'Event name should be "controllerchange"');
+ assert_true(
+ e.target instanceof frame.contentWindow.ServiceWorkerContainer,
+ 'Event target should be a ServiceWorkerContainer');
+ assert_equals(e.target.controller.state, 'activating',
+ 'Controller state should be activating');
+ assert_equals(
+ frame_sw.controller.scriptURL, normalizeURL(url2),
+ 'Controller scriptURL should change to the second one');
+ });
+
+ return service_worker_unregister_and_register(t, url1, scope)
+ .then(function(registration) {
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() {
+ return with_iframe(scope);
+ })
+ .then(function(f) {
+ t.add_cleanup(function() {
+ f.remove();
+ });
+ frame = f;
+ frame_sw = f.contentWindow.navigator.serviceWorker;
+ assert_equals(
+ frame_sw.controller.scriptURL, normalizeURL(url1),
+ 'Document controller scriptURL should equal to the first one');
+ frame_sw.oncontrollerchange = t.step_func(oncontrollerchanged);
+ return navigator.serviceWorker.register(url2, {scope: scope});
+ })
+ .then(function(registration) {
+ sw_registration = registration;
+ t.add_cleanup(function() {
+ return registration.unregister();
+ });
+ return saw_controllerchanged;
+ })
+ .then(function() {
+ assert_not_equals(sw_registration.active, null,
+ 'Registration active worker should not be null');
+ return fetch_tests_from_worker(sw_registration.active);
+ });
+ }, 'Test skipWaiting while a client is using the registration');
+
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/skip-waiting-without-client.https.html b/test/wpt/tests/service-workers/service-worker/skip-waiting-without-client.https.html
new file mode 100644
index 0000000..62060a8
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/skip-waiting-without-client.https.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<title>Service Worker: Skip waiting without client</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+
+service_worker_test(
+ 'resources/skip-waiting-worker.js',
+ 'Test single skipWaiting() when no client attached');
+
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/skip-waiting-without-using-registration.https.html b/test/wpt/tests/service-workers/service-worker/skip-waiting-without-using-registration.https.html
new file mode 100644
index 0000000..ced64e5
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/skip-waiting-without-using-registration.https.html
@@ -0,0 +1,44 @@
+<!DOCTYPE html>
+<title>Service Worker: Skip waiting without using registration</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+'use strict';
+
+promise_test(function(t) {
+ var scope = 'resources/blank.html?skip-waiting-without-using-registration';
+ var url = 'resources/skip-waiting-worker.js';
+ var frame_sw, sw_registration;
+
+ return service_worker_unregister(t, scope)
+ .then(function() {
+ return with_iframe(scope);
+ })
+ .then(function(f) {
+ t.add_cleanup(function() {
+ f.remove();
+ });
+ frame_sw = f.contentWindow.navigator.serviceWorker;
+ assert_equals(frame_sw.controller, null,
+ 'Document controller should be null');
+ return navigator.serviceWorker.register(url, {scope: scope});
+ })
+ .then(function(registration) {
+ sw_registration = registration;
+ t.add_cleanup(function() {
+ return registration.unregister();
+ });
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() {
+ assert_equals(frame_sw.controller, null,
+ 'Document controller should still be null');
+ assert_not_equals(sw_registration.active, null,
+ 'Registration active worker should not be null');
+ return fetch_tests_from_worker(sw_registration.active);
+ });
+ }, 'Test skipWaiting while a client is not being controlled');
+
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/skip-waiting.https.html b/test/wpt/tests/service-workers/service-worker/skip-waiting.https.html
new file mode 100644
index 0000000..f8392fc
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/skip-waiting.https.html
@@ -0,0 +1,58 @@
+<!DOCTYPE html>
+<title>Service Worker: Skip waiting</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+'use strict';
+
+promise_test(function(t) {
+ var scope = 'resources/blank.html?skip-waiting';
+ var url1 = 'resources/empty.js';
+ var url2 = 'resources/empty-worker.js';
+ var url3 = 'resources/skip-waiting-worker.js';
+ var sw_registration, activated_worker, waiting_worker;
+ return service_worker_unregister_and_register(t, url1, scope)
+ .then(function(registration) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ sw_registration = registration;
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() {
+ return with_iframe(scope);
+ })
+ .then(function(f) {
+ t.add_cleanup(function() {
+ f.remove();
+ });
+ return navigator.serviceWorker.register(url2, {scope: scope});
+ })
+ .then(function(registration) {
+ return wait_for_state(t, registration.installing, 'installed');
+ })
+ .then(function() {
+ activated_worker = sw_registration.active;
+ waiting_worker = sw_registration.waiting;
+ assert_equals(activated_worker.scriptURL, normalizeURL(url1),
+ 'Worker with url1 should be activated');
+ assert_equals(waiting_worker.scriptURL, normalizeURL(url2),
+ 'Worker with url2 should be waiting');
+ return navigator.serviceWorker.register(url3, {scope: scope});
+ })
+ .then(function(registration) {
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() {
+ assert_equals(activated_worker.state, 'redundant',
+ 'Worker with url1 should be redundant');
+ assert_equals(waiting_worker.state, 'redundant',
+ 'Worker with url2 should be redundant');
+ assert_equals(sw_registration.active.scriptURL, normalizeURL(url3),
+ 'Worker with url3 should be activated');
+ });
+ }, 'Test skipWaiting with both active and waiting workers');
+
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/state.https.html b/test/wpt/tests/service-workers/service-worker/state.https.html
new file mode 100644
index 0000000..7358e58
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/state.https.html
@@ -0,0 +1,74 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+promise_test(function (t) {
+ var currentState = 'test-is-starting';
+ var scope = 'resources/state/';
+
+ return service_worker_unregister_and_register(
+ t, 'resources/empty-worker.js', scope)
+ .then(function(registration) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ var sw = registration.installing;
+
+ assert_equals(sw.state, 'installing',
+ 'the service worker should be in "installing" state.');
+ checkStateTransition(sw.state);
+ return onStateChange(sw);
+ });
+
+ function checkStateTransition(newState) {
+ switch (currentState) {
+ case 'test-is-starting':
+ break; // anything goes
+ case 'installing':
+ assert_in_array(newState, ['installed', 'redundant']);
+ break;
+ case 'installed':
+ assert_in_array(newState, ['activating', 'redundant']);
+ break;
+ case 'activating':
+ assert_in_array(newState, ['activated', 'redundant']);
+ break;
+ case 'activated':
+ assert_equals(newState, 'redundant');
+ break;
+ case 'redundant':
+ assert_unreached('a ServiceWorker should not transition out of ' +
+ 'the "redundant" state');
+ break;
+ default:
+ assert_unreached('should not transition into unknown state "' +
+ newState + '"');
+ break;
+ }
+ currentState = newState;
+ }
+
+ function onStateChange(expectedTarget) {
+ return new Promise(function(resolve) {
+ expectedTarget.addEventListener('statechange', resolve);
+ }).then(function(event) {
+ assert_true(event.target instanceof ServiceWorker,
+ 'the target of the statechange event should be a ' +
+ 'ServiceWorker.');
+ assert_equals(event.target, expectedTarget,
+ 'the target of the statechange event should be ' +
+ 'the installing ServiceWorker');
+ assert_equals(event.type, 'statechange',
+ 'the type of the event should be "statechange".');
+
+ checkStateTransition(event.target.state);
+
+ if (event.target.state != 'activated')
+ return onStateChange(expectedTarget);
+ });
+ }
+}, 'Service Worker state property and "statechange" event');
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/svg-target-reftest.https.html b/test/wpt/tests/service-workers/service-worker/svg-target-reftest.https.html
new file mode 100644
index 0000000..3710ee6
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/svg-target-reftest.https.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<meta charset="utf-8">
+<title>Service worker interception does not break SVG fragment targets</title>
+<meta name="assert" content="SVG with link fragment should render correctly when intercepted by a service worker.">
+<script src="resources/test-helpers.sub.js"></script>
+<link rel="match" href="resources/svg-target-reftest-001.html">
+<p>Pass if you see a green box below.</p>
+<script>
+// We want to use utility functions designed for testharness.js where
+// there is a test object. We don't have a test object in reftests
+// so fake one for now.
+const fake_test = { step_func: f => f };
+
+async function runTest() {
+ const script = './resources/pass-through-worker.js';
+ const scope = './resources/svg-target-reftest-frame.html';
+ let reg = await navigator.serviceWorker.register(script, { scope });
+ await wait_for_state(fake_test, reg.installing, 'activated');
+ let f = await with_iframe(scope);
+ document.documentElement.classList.remove('reftest-wait');
+ await reg.unregister();
+ // Note, we cannot remove the frame explicitly because we can't
+ // tell when the reftest completes.
+}
+runTest();
+</script>
+</html>
diff --git a/test/wpt/tests/service-workers/service-worker/synced-state.https.html b/test/wpt/tests/service-workers/service-worker/synced-state.https.html
new file mode 100644
index 0000000..0e9f63a
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/synced-state.https.html
@@ -0,0 +1,93 @@
+<!doctype html>
+<title>ServiceWorker: worker objects have synced state</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+// Tests that ServiceWorker objects representing the same Service Worker
+// entity have the same state. JS-level equality is now required according to
+// the spec.
+'use strict';
+
+function nextChange(worker) {
+ return new Promise(function(resolve, reject) {
+ worker.addEventListener('statechange', function handler(event) {
+ try {
+ worker.removeEventListener('statechange', handler);
+ resolve(event.currentTarget.state);
+ } catch (err) {
+ reject(err);
+ }
+ });
+ });
+}
+
+promise_test(function(t) {
+ var scope = 'resources/synced-state';
+ var script = 'resources/empty-worker.js';
+ var registration, worker;
+
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(function(r) {
+ registration = r;
+ worker = registration.installing;
+
+ t.add_cleanup(function() {
+ return r.unregister();
+ });
+
+ return nextChange(worker);
+ })
+ .then(function(state) {
+ assert_equals(state, 'installed',
+ 'original SW should be installed');
+ assert_equals(registration.installing, null,
+ 'in installed, .installing should be null');
+ assert_equals(registration.waiting, worker,
+ 'in installed, .waiting should be equal to the ' +
+ 'original worker');
+ assert_equals(registration.waiting.state, 'installed',
+ 'in installed, .waiting should be installed');
+ assert_equals(registration.active, null,
+ 'in installed, .active should be null');
+
+ return nextChange(worker);
+ })
+ .then(function(state) {
+ assert_equals(state, 'activating',
+ 'original SW should be activating');
+ assert_equals(registration.installing, null,
+ 'in activating, .installing should be null');
+ assert_equals(registration.waiting, null,
+ 'in activating, .waiting should be null');
+ assert_equals(registration.active, worker,
+ 'in activating, .active should be equal to the ' +
+ 'original worker');
+ assert_equals(
+ registration.active.state, 'activating',
+ 'in activating, .active should be activating');
+
+ return nextChange(worker);
+ })
+ .then(function(state) {
+ assert_equals(state, 'activated',
+ 'original SW should be activated');
+ assert_equals(registration.installing, null,
+ 'in activated, .installing should be null');
+ assert_equals(registration.waiting, null,
+ 'in activated, .waiting should be null');
+ assert_equals(registration.active, worker,
+ 'in activated, .active should be equal to the ' +
+ 'original worker');
+ assert_equals(registration.active.state, 'activated',
+ 'in activated .active should be activated');
+ })
+ .then(function() {
+ return navigator.serviceWorker.getRegistration(scope);
+ })
+ .then(function(r) {
+ assert_equals(r, registration, 'getRegistration should return the ' +
+ 'same object');
+ });
+ }, 'worker objects for the same entity have the same state');
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/tentative/static-router/README.md b/test/wpt/tests/service-workers/service-worker/tentative/static-router/README.md
new file mode 100644
index 0000000..8826b3c
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/tentative/static-router/README.md
@@ -0,0 +1,4 @@
+A test stuite for the ServiceWorker Static Routing API.
+
+WICG proposal: https://github.com/WICG/proposals/issues/102
+Specification PR: https://github.com/w3c/ServiceWorker/pull/1686
diff --git a/test/wpt/tests/service-workers/service-worker/tentative/static-router/resources/direct.txt b/test/wpt/tests/service-workers/service-worker/tentative/static-router/resources/direct.txt
new file mode 100644
index 0000000..f3d9861
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/tentative/static-router/resources/direct.txt
@@ -0,0 +1 @@
+Network
diff --git a/test/wpt/tests/service-workers/service-worker/tentative/static-router/resources/simple-test-for-condition-main-resource.html b/test/wpt/tests/service-workers/service-worker/tentative/static-router/resources/simple-test-for-condition-main-resource.html
new file mode 100644
index 0000000..0c3e3e7
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/tentative/static-router/resources/simple-test-for-condition-main-resource.html
@@ -0,0 +1,3 @@
+<!DOCTYPE html>
+<title>Simple</title>
+Here's a simple html file.
diff --git a/test/wpt/tests/service-workers/service-worker/tentative/static-router/resources/simple.html b/test/wpt/tests/service-workers/service-worker/tentative/static-router/resources/simple.html
new file mode 100644
index 0000000..0c3e3e7
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/tentative/static-router/resources/simple.html
@@ -0,0 +1,3 @@
+<!DOCTYPE html>
+<title>Simple</title>
+Here's a simple html file.
diff --git a/test/wpt/tests/service-workers/service-worker/tentative/static-router/resources/static-router-sw.js b/test/wpt/tests/service-workers/service-worker/tentative/static-router/resources/static-router-sw.js
new file mode 100644
index 0000000..4655ab5
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/tentative/static-router/resources/static-router-sw.js
@@ -0,0 +1,35 @@
+'use strict';
+
+var requests = [];
+
+self.addEventListener('install', e => {
+ e.registerRouter([
+ {
+ condition: {urlPattern: '/**/*.txt??*'},
+ // Note: "??*" is for allowing arbitrary query strings.
+ // Upon my experiment, the URLPattern needs two '?'s for specifying
+ // a coming string as a query.
+ source: 'network'
+ }, {
+ condition: {
+ urlPattern: '/**/simple-test-for-condition-main-resource.html'},
+ source: 'network'
+ }]);
+ self.skipWaiting();
+});
+
+self.addEventListener('activate', e => {
+ e.waitUntil(clients.claim());
+});
+
+self.addEventListener('fetch', function(event) {
+ requests.push({url: event.request.url, mode: event.request.mode});
+ const url = new URL(event.request.url);
+ const nonce = url.searchParams.get('nonce');
+ event.respondWith(new Response(nonce));
+});
+
+self.addEventListener('message', function(event) {
+ event.data.port.postMessage({requests: requests});
+ requests = [];
+});
diff --git a/test/wpt/tests/service-workers/service-worker/tentative/static-router/resources/test-helpers.sub.js b/test/wpt/tests/service-workers/service-worker/tentative/static-router/resources/test-helpers.sub.js
new file mode 100644
index 0000000..64a7f7d
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/tentative/static-router/resources/test-helpers.sub.js
@@ -0,0 +1,303 @@
+// Copied from
+// service-workers/service-worker/resources/testharness-helpers.js to be used under tentative.
+
+// Adapter for testharness.js-style tests with Service Workers
+
+/**
+ * @param options an object that represents RegistrationOptions except for scope.
+ * @param options.type a WorkerType.
+ * @param options.updateViaCache a ServiceWorkerUpdateViaCache.
+ * @see https://w3c.github.io/ServiceWorker/#dictdef-registrationoptions
+ */
+function service_worker_unregister_and_register(test, url, scope, options) {
+ if (!scope || scope.length == 0)
+ return Promise.reject(new Error('tests must define a scope'));
+
+ if (options && options.scope)
+ return Promise.reject(new Error('scope must not be passed in options'));
+
+ options = Object.assign({ scope: scope }, options);
+ return service_worker_unregister(test, scope)
+ .then(function() {
+ return navigator.serviceWorker.register(url, options);
+ })
+ .catch(unreached_rejection(test,
+ 'unregister and register should not fail'));
+}
+
+// This unregisters the registration that precisely matches scope. Use this
+// when unregistering by scope. If no registration is found, it just resolves.
+function service_worker_unregister(test, scope) {
+ var absoluteScope = (new URL(scope, window.location).href);
+ return navigator.serviceWorker.getRegistration(scope)
+ .then(function(registration) {
+ if (registration && registration.scope === absoluteScope)
+ return registration.unregister();
+ })
+ .catch(unreached_rejection(test, 'unregister should not fail'));
+}
+
+function service_worker_unregister_and_done(test, scope) {
+ return service_worker_unregister(test, scope)
+ .then(test.done.bind(test));
+}
+
+function unreached_fulfillment(test, prefix) {
+ return test.step_func(function(result) {
+ var error_prefix = prefix || 'unexpected fulfillment';
+ assert_unreached(error_prefix + ': ' + result);
+ });
+}
+
+// Rejection-specific helper that provides more details
+function unreached_rejection(test, prefix) {
+ return test.step_func(function(error) {
+ var reason = error.message || error.name || error;
+ var error_prefix = prefix || 'unexpected rejection';
+ assert_unreached(error_prefix + ': ' + reason);
+ });
+}
+
+/**
+ * Adds an iframe to the document and returns a promise that resolves to the
+ * iframe when it finishes loading. The caller is responsible for removing the
+ * iframe later if needed.
+ *
+ * @param {string} url
+ * @returns {HTMLIFrameElement}
+ */
+function with_iframe(url) {
+ return new Promise(function(resolve) {
+ var frame = document.createElement('iframe');
+ frame.className = 'test-iframe';
+ frame.src = url;
+ frame.onload = function() { resolve(frame); };
+ document.body.appendChild(frame);
+ });
+}
+
+function normalizeURL(url) {
+ return new URL(url, self.location).toString().replace(/#.*$/, '');
+}
+
+function wait_for_update(test, registration) {
+ if (!registration || registration.unregister == undefined) {
+ return Promise.reject(new Error(
+ 'wait_for_update must be passed a ServiceWorkerRegistration'));
+ }
+
+ return new Promise(test.step_func(function(resolve) {
+ var handler = test.step_func(function() {
+ registration.removeEventListener('updatefound', handler);
+ resolve(registration.installing);
+ });
+ registration.addEventListener('updatefound', handler);
+ }));
+}
+
+// Return true if |state_a| is more advanced than |state_b|.
+function is_state_advanced(state_a, state_b) {
+ if (state_b === 'installing') {
+ switch (state_a) {
+ case 'installed':
+ case 'activating':
+ case 'activated':
+ case 'redundant':
+ return true;
+ }
+ }
+
+ if (state_b === 'installed') {
+ switch (state_a) {
+ case 'activating':
+ case 'activated':
+ case 'redundant':
+ return true;
+ }
+ }
+
+ if (state_b === 'activating') {
+ switch (state_a) {
+ case 'activated':
+ case 'redundant':
+ return true;
+ }
+ }
+
+ if (state_b === 'activated') {
+ switch (state_a) {
+ case 'redundant':
+ return true;
+ }
+ }
+ return false;
+}
+
+function wait_for_state(test, worker, state) {
+ if (!worker || worker.state == undefined) {
+ return Promise.reject(new Error(
+ 'wait_for_state needs a ServiceWorker object to be passed.'));
+ }
+ if (worker.state === state)
+ return Promise.resolve(state);
+
+ if (is_state_advanced(worker.state, state)) {
+ return Promise.reject(new Error(
+ `Waiting for ${state} but the worker is already ${worker.state}.`));
+ }
+ return new Promise(test.step_func(function(resolve, reject) {
+ worker.addEventListener('statechange', test.step_func(function() {
+ if (worker.state === state)
+ resolve(state);
+
+ if (is_state_advanced(worker.state, state)) {
+ reject(new Error(
+ `The state of the worker becomes ${worker.state} while waiting` +
+ `for ${state}.`));
+ }
+ }));
+ }));
+}
+
+// Declare a test that runs entirely in the ServiceWorkerGlobalScope. The |url|
+// is the service worker script URL. This function:
+// - Instantiates a new test with the description specified in |description|.
+// The test will succeed if the specified service worker can be successfully
+// registered and installed.
+// - Creates a new ServiceWorker registration with a scope unique to the current
+// document URL. Note that this doesn't allow more than one
+// service_worker_test() to be run from the same document.
+// - Waits for the new worker to begin installing.
+// - Imports tests results from tests running inside the ServiceWorker.
+function service_worker_test(url, description) {
+ // If the document URL is https://example.com/document and the script URL is
+ // https://example.com/script/worker.js, then the scope would be
+ // https://example.com/script/scope/document.
+ var scope = new URL('scope' + window.location.pathname,
+ new URL(url, window.location)).toString();
+ promise_test(function(test) {
+ return service_worker_unregister_and_register(test, url, scope)
+ .then(function(registration) {
+ add_completion_callback(function() {
+ registration.unregister();
+ });
+ return wait_for_update(test, registration)
+ .then(function(worker) {
+ return fetch_tests_from_worker(worker);
+ });
+ });
+ }, description);
+}
+
+function base_path() {
+ return location.pathname.replace(/\/[^\/]*$/, '/');
+}
+
+function test_login(test, origin, username, password, cookie) {
+ return new Promise(function(resolve, reject) {
+ with_iframe(
+ origin + base_path() +
+ 'resources/fetch-access-control-login.html')
+ .then(test.step_func(function(frame) {
+ var channel = new MessageChannel();
+ channel.port1.onmessage = test.step_func(function() {
+ frame.remove();
+ resolve();
+ });
+ frame.contentWindow.postMessage(
+ {username: username, password: password, cookie: cookie},
+ origin, [channel.port2]);
+ }));
+ });
+}
+
+function test_websocket(test, frame, url) {
+ return new Promise(function(resolve, reject) {
+ var ws = new frame.contentWindow.WebSocket(url, ['echo', 'chat']);
+ var openCalled = false;
+ ws.addEventListener('open', test.step_func(function(e) {
+ assert_equals(ws.readyState, 1, "The WebSocket should be open");
+ openCalled = true;
+ ws.close();
+ }), true);
+
+ ws.addEventListener('close', test.step_func(function(e) {
+ assert_true(openCalled, "The WebSocket should be closed after being opened");
+ resolve();
+ }), true);
+
+ ws.addEventListener('error', reject);
+ });
+}
+
+function login_https(test) {
+ var host_info = get_host_info();
+ return test_login(test, host_info.HTTPS_REMOTE_ORIGIN,
+ 'username1s', 'password1s', 'cookie1')
+ .then(function() {
+ return test_login(test, host_info.HTTPS_ORIGIN,
+ 'username2s', 'password2s', 'cookie2');
+ });
+}
+
+function websocket(test, frame) {
+ return test_websocket(test, frame, get_websocket_url());
+}
+
+function get_websocket_url() {
+ return 'wss://{{host}}:{{ports[wss][0]}}/echo';
+}
+
+// The navigator.serviceWorker.register() method guarantees that the newly
+// installing worker is available as registration.installing when its promise
+// resolves. However some tests test installation using a <link> element where
+// it is possible for the installing worker to have already become the waiting
+// or active worker. So this method is used to get the newest worker when these
+// tests need access to the ServiceWorker itself.
+function get_newest_worker(registration) {
+ if (registration.installing)
+ return registration.installing;
+ if (registration.waiting)
+ return registration.waiting;
+ if (registration.active)
+ return registration.active;
+}
+
+function register_using_link(script, options) {
+ var scope = options.scope;
+ var link = document.createElement('link');
+ link.setAttribute('rel', 'serviceworker');
+ link.setAttribute('href', script);
+ link.setAttribute('scope', scope);
+ document.getElementsByTagName('head')[0].appendChild(link);
+ return new Promise(function(resolve, reject) {
+ link.onload = resolve;
+ link.onerror = reject;
+ })
+ .then(() => navigator.serviceWorker.getRegistration(scope));
+}
+
+function with_sandboxed_iframe(url, sandbox) {
+ return new Promise(function(resolve) {
+ var frame = document.createElement('iframe');
+ frame.sandbox = sandbox;
+ frame.src = url;
+ frame.onload = function() { resolve(frame); };
+ document.body.appendChild(frame);
+ });
+}
+
+// Registers, waits for activation, then unregisters on a sample scope.
+//
+// This can be used to wait for a period of time needed to register,
+// activate, and then unregister a service worker. When checking that
+// certain behavior does *NOT* happen, this is preferable to using an
+// arbitrary delay.
+async function wait_for_activation_on_sample_scope(t, window_or_workerglobalscope) {
+ const script = '/service-workers/service-worker/resources/empty-worker.js';
+ const scope = 'resources/there/is/no/there/there?' + Date.now();
+ let registration = await window_or_workerglobalscope.navigator.serviceWorker.register(script, { scope });
+ await wait_for_state(t, registration.installing, 'activated');
+ await registration.unregister();
+}
+
diff --git a/test/wpt/tests/service-workers/service-worker/tentative/static-router/static-router-main-resource.https.html b/test/wpt/tests/service-workers/service-worker/tentative/static-router/static-router-main-resource.https.html
new file mode 100644
index 0000000..5a55783
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/tentative/static-router/static-router-main-resource.https.html
@@ -0,0 +1,58 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Static Router: simply skip fetch handler for main resource if pattern matches</title>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+const SCRIPT = 'resources/static-router-sw.js';
+const SCOPE = 'resources/';
+const REGISTERED_ROUTE_HTML =
+ 'resources/simple-test-for-condition-main-resource.html';
+const NON_REGISTERED_ROUTE_HTML = 'resources/simple.html';
+const host_info = get_host_info();
+const path = new URL(".", window.location).pathname;
+
+// Register a service worker, then create an iframe at url.
+function iframeTest(url, callback, name) {
+ return promise_test(async t => {
+ const reg = await service_worker_unregister_and_register(t, SCRIPT, SCOPE);
+ add_completion_callback(() => reg.unregister());
+ const worker = reg.installing;
+ await wait_for_state(t, worker, 'activated');
+ const iframe = await with_iframe(url);
+ const iwin = iframe.contentWindow;
+ t.add_cleanup(() => iframe.remove());
+ await callback(t, iwin, worker);
+ }, name);
+}
+
+function get_fetched_urls(worker) {
+ return new Promise(function(resolve) {
+ var channel = new MessageChannel();
+ channel.port1.onmessage = function(msg) { resolve(msg); };
+ worker.postMessage({port: channel.port2}, [channel.port2]);
+ });
+}
+
+iframeTest(REGISTERED_ROUTE_HTML, async (t, iwin, worker) => {
+ const fetched_urls = await get_fetched_urls(worker);
+ const {requests} = fetched_urls.data;
+ assert_equals(requests.length, 0);
+ assert_equals(iwin.document.body.innerText, "Here's a simple html file.");
+}, 'Main resource load matched with the condition');
+
+iframeTest(NON_REGISTERED_ROUTE_HTML, async (t, iwin, worker) => {
+ const fetched_urls = await get_fetched_urls(worker);
+ const {requests} = fetched_urls.data;
+ assert_equals(requests.length, 1);
+ assert_equals(
+ requests[0].url,
+ `${host_info['HTTPS_ORIGIN']}${path}${NON_REGISTERED_ROUTE_HTML}`);
+ assert_equals(requests[0].mode, 'navigate');
+}, 'Main resource load not matched with the condition');
+
+</script>
+</body>
diff --git a/test/wpt/tests/service-workers/service-worker/tentative/static-router/static-router-subresource.https.html b/test/wpt/tests/service-workers/service-worker/tentative/static-router/static-router-subresource.https.html
new file mode 100644
index 0000000..721c279
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/tentative/static-router/static-router-subresource.https.html
@@ -0,0 +1,48 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Static Router: simply skip fetch handler if pattern matches</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+const SCRIPT = 'resources/static-router-sw.js';
+const SCOPE = 'resources/';
+const HTML_FILE = 'resources/simple.html';
+const TXT_FILE = 'resources/direct.txt';
+
+// Register a service worker, then create an iframe at url.
+function iframeTest(url, callback, name) {
+ return promise_test(async t => {
+ const reg = await service_worker_unregister_and_register(t, SCRIPT, SCOPE);
+ add_completion_callback(() => reg.unregister());
+ await wait_for_state(t, reg.installing, 'activated');
+ const iframe = await with_iframe(url);
+ const iwin = iframe.contentWindow;
+ t.add_cleanup(() => iframe.remove());
+ await callback(t, iwin);
+ }, name);
+}
+
+function randomString() {
+ let result = "";
+ for (let i = 0; i < 5; i++) {
+ result += String.fromCharCode(97 + Math.floor(Math.random() * 26));
+ }
+ return result;
+}
+
+iframeTest(HTML_FILE, async (t, iwin) => {
+ const rnd = randomString();
+ const response = await iwin.fetch('?nonce=' + rnd);
+ assert_equals(await response.text(), rnd);
+}, 'Subresource load not matched with the condition');
+
+iframeTest(TXT_FILE, async (t, iwin) => {
+ const rnd = randomString();
+ const response = await iwin.fetch('?nonce=' + rnd);
+ assert_equals(await response.text(), "Network\n");
+}, 'Subresource load matched with the condition');
+
+</script>
+</body>
diff --git a/test/wpt/tests/service-workers/service-worker/uncontrolled-page.https.html b/test/wpt/tests/service-workers/service-worker/uncontrolled-page.https.html
new file mode 100644
index 0000000..e22ca8f
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/uncontrolled-page.https.html
@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<title>Service Worker: Registration</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+function fetch_url(url) {
+ return new Promise(function(resolve, reject) {
+ var request = new XMLHttpRequest();
+ request.addEventListener('load', function(event) {
+ if (request.status == 200)
+ resolve(request.response);
+ else
+ reject(Error(request.statusText));
+ });
+ request.open('GET', url);
+ request.send();
+ });
+}
+var worker = 'resources/fail-on-fetch-worker.js';
+
+promise_test(function(t) {
+ var scope = 'resources/scope/uncontrolled-page/';
+ return service_worker_unregister_and_register(t, worker, scope)
+ .then(function(reg) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ return wait_for_state(t, reg.installing, 'activated');
+ })
+ .then(function() {
+ return fetch_url('resources/simple.txt');
+ })
+ .then(function(text) {
+ assert_equals(text, 'a simple text file\n');
+ });
+ }, 'Fetch events should not go through uncontrolled page.');
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/unregister-controller.https.html b/test/wpt/tests/service-workers/service-worker/unregister-controller.https.html
new file mode 100644
index 0000000..3bf4cff
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/unregister-controller.https.html
@@ -0,0 +1,108 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+var worker_url = 'resources/simple-intercept-worker.js';
+
+async_test(function(t) {
+ var scope =
+ 'resources/unregister-controller-page.html?load-before-unregister';
+ var frame_window;
+ var controller;
+ var registration;
+ var frame;
+
+ service_worker_unregister_and_register(t, worker_url, scope)
+ .then(function(r) {
+ registration = r;
+ return wait_for_state(t, r.installing, 'activated');
+ })
+ .then(function() {
+ return with_iframe(scope);
+ })
+ .then(function(f) {
+ frame = f;
+ frame_window = frame.contentWindow;
+ controller = frame_window.navigator.serviceWorker.controller;
+ assert_true(controller instanceof frame_window.ServiceWorker,
+ 'document should load with a controller');
+ return registration.unregister();
+ })
+ .then(function() {
+ assert_equals(frame_window.navigator.serviceWorker.controller,
+ controller,
+ 'unregistration should not modify controller');
+ return frame_window.fetch_url('simple.txt');
+ })
+ .then(function(response) {
+ assert_equals(response, 'intercepted by service worker',
+ 'controller should intercept requests');
+ frame.remove();
+ t.done();
+ })
+ .catch(unreached_rejection(t));
+ }, 'Unregister does not affect existing controller');
+
+async_test(function(t) {
+ var scope =
+ 'resources/unregister-controller-page.html?load-after-unregister';
+ var registration;
+ var frame;
+
+ service_worker_unregister_and_register(t, worker_url, scope)
+ .then(function(r) {
+ registration = r;
+ return wait_for_state(t, r.installing, 'activated');
+ })
+ .then(function() {
+ return registration.unregister();
+ })
+ .then(function() {
+ return with_iframe(scope);
+ })
+ .then(function(f) {
+ frame = f;
+ var frame_window = frame.contentWindow;
+ assert_equals(frame_window.navigator.serviceWorker.controller, null,
+ 'document should not have a controller');
+ return frame_window.fetch_url('simple.txt');
+ })
+ .then(function(response) {
+ assert_equals(response, 'a simple text file\n',
+ 'requests should not be intercepted');
+ frame.remove();
+ t.done();
+ })
+ .catch(unreached_rejection(t));
+ }, 'Unregister prevents control of subsequent navigations');
+
+async_test(function(t) {
+ var scope =
+ 'resources/scope/no-new-controllee-even-if-registration-is-still-used';
+ var registration;
+
+ service_worker_unregister_and_register(t, worker_url, scope)
+ .then(function(r) {
+ registration = r;
+ return wait_for_state(t, r.installing, 'activated');
+ })
+ .then(function() {
+ return with_iframe(scope);
+ })
+ .then(function(frame) {
+ return registration.unregister();
+ })
+ .then(function() {
+ return with_iframe(scope);
+ })
+ .then(function(frame) {
+ assert_equals(frame.contentWindow.navigator.serviceWorker.controller,
+ null,
+ 'document should not have a controller');
+ frame.remove();
+ t.done();
+ })
+ .catch(unreached_rejection(t));
+ }, 'Unregister prevents new controllee even if registration is still in use');
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/unregister-immediately-before-installed.https.html b/test/wpt/tests/service-workers/service-worker/unregister-immediately-before-installed.https.html
new file mode 100644
index 0000000..79cdaf0
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/unregister-immediately-before-installed.https.html
@@ -0,0 +1,57 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Use Clear-Site-Data to immediately unregister service workers</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script src="resources/unregister-immediately-helpers.js"></script>
+<body>
+<script>
+'use strict';
+
+// These tests use the Clear-Site-Data network response header to immediately
+// unregister a service worker registration with a worker whose state is
+// 'installing' or 'parsed'. Clear-Site-Data must delete the registration,
+// abort the installation and then clear the registration by setting the
+// worker's state to 'redundant'.
+
+promise_test(async test => {
+ // This test keeps the the service worker in the 'parsed' state by using a
+ // script with an infinite loop.
+ const script_url = 'resources/onparse-infiniteloop-worker.js';
+ const scope_url =
+ 'resources/scope-for-unregister-immediately-with-parsed-worker';
+
+ await service_worker_unregister(test, /*scope=*/script_url);
+
+ // Clear-Site-Data must cause register() to fail.
+ const register_promise = promise_rejects_dom(test, 'AbortError',
+ navigator.serviceWorker.register(script_url, { scope: scope_url}));;
+
+ await Promise.all([clear_site_data(), register_promise]);
+
+ await assert_no_registrations_exist();
+ }, 'Clear-Site-Data must abort service worker registration.');
+
+promise_test(async test => {
+ // This test keeps the the service worker in the 'installing' state by using a
+ // script with an install event waitUntil() promise that never resolves.
+ const script_url = 'resources/oninstall-waituntil-forever.js';
+ const scope_url =
+ 'resources/scope-for-unregister-immediately-with-installing-worker';
+
+ const registration = await service_worker_unregister_and_register(
+ test, script_url, scope_url);
+ const service_worker = registration.installing;
+
+ // Clear-Site-Data must cause install to fail.
+ await Promise.all([
+ clear_site_data(),
+ wait_for_state(test, service_worker, 'redundant')]);
+
+ await assert_no_registrations_exist();
+ }, 'Clear-Site-Data must unregister a registration with a worker '
+ + 'in the "installing" state.');
+
+</script>
+</body>
diff --git a/test/wpt/tests/service-workers/service-worker/unregister-immediately-during-extendable-events.https.html b/test/wpt/tests/service-workers/service-worker/unregister-immediately-during-extendable-events.https.html
new file mode 100644
index 0000000..6ba87a7
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/unregister-immediately-during-extendable-events.https.html
@@ -0,0 +1,50 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Use Clear-Site-Data to immediately unregister service workers</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script src="resources/unregister-immediately-helpers.js"></script>
+<body>
+<script>
+'use strict';
+
+// These tests use the Clear-Site-Data network response header to immediately
+// unregister a service worker registration with a worker that has pending
+// extendable events. Clear-Site-Data must delete the registration,
+// abort all pending extendable events and then clear the registration by
+// setting the worker's state to 'redundant'
+
+promise_test(async test => {
+ // Use a service worker script that can produce fetch events with pending
+ // respondWith() promises that never resolve.
+ const script_url = 'resources/onfetch-waituntil-forever.js';
+ const scope_url =
+ 'resources/blank.html?unregister-immediately-with-fetch-event';
+
+ const registration = await service_worker_unregister_and_register(
+ test, script_url, scope_url);
+
+ await wait_for_state(test, registration.installing, 'activated');
+
+ const frame = await add_controlled_iframe(test, scope_url);
+
+ // Clear-Site-Data must cause the pending fetch promise to reject.
+ const fetch_promise = promise_rejects_js(
+ test, TypeError, frame.contentWindow.fetch('waituntil-forever'));
+
+ const event_watcher = new EventWatcher(
+ test, frame.contentWindow.navigator.serviceWorker, 'controllerchange');
+
+ await Promise.all([
+ clear_site_data(),
+ fetch_promise,
+ event_watcher.wait_for('controllerchange'),
+ wait_for_state(test, registration.active, 'redundant'),]);
+
+ assert_equals(frame.contentWindow.navigator.serviceWorker.controller, null);
+ await assert_no_registrations_exist();
+}, 'Clear-Site-Data must fail pending subresource fetch events.');
+
+</script>
+</body>
diff --git a/test/wpt/tests/service-workers/service-worker/unregister-immediately.https.html b/test/wpt/tests/service-workers/service-worker/unregister-immediately.https.html
new file mode 100644
index 0000000..54be40a
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/unregister-immediately.https.html
@@ -0,0 +1,134 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Use Clear-Site-Data to immediately unregister service workers</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script src="resources/unregister-immediately-helpers.js"></script>
+<body>
+<script>
+'use strict';
+
+// These tests use the Clear-Site-Data network response header to immediately
+// unregister a service worker registration with a worker whose state is
+// 'installed', 'waiting', 'activating' or 'activated'. Immediately
+// unregistering runs the "Clear Registration" algorithm without waiting for the
+// active worker's controlled clients to unload.
+
+promise_test(async test => {
+ // This test keeps the the service worker in the 'activating' state by using a
+ // script with an activate event waitUntil() promise that never resolves.
+ const script_url = 'resources/onactivate-waituntil-forever.js';
+ const scope_url =
+ 'resources/scope-for-unregister-immediately-with-waiting-worker';
+
+ const registration = await service_worker_unregister_and_register(
+ test, script_url, scope_url);
+ const service_worker = registration.installing;
+
+ await wait_for_state(test, service_worker, 'activating');
+
+ // Clear-Site-Data must cause activation to fail.
+ await Promise.all([
+ clear_site_data(),
+ wait_for_state(test, service_worker, 'redundant')]);
+
+ await assert_no_registrations_exist();
+ }, 'Clear-Site-Data must unregister a registration with a worker '
+ + 'in the "activating" state.');
+
+promise_test(async test => {
+ // Create an registration with two service workers: one activated and one
+ // installed.
+ const script_url = 'resources/update_shell.py';
+ const scope_url =
+ 'resources/scope-for-unregister-immediately-with-with-update';
+
+ const registration = await service_worker_unregister_and_register(
+ test, script_url, scope_url);
+ const first_service_worker = registration.installing;
+
+ await wait_for_state(test, first_service_worker, 'activated');
+ registration.update();
+
+ const event_watcher = new EventWatcher(test, registration, 'updatefound');
+ await event_watcher.wait_for('updatefound');
+
+ const second_service_worker = registration.installing;
+ await wait_for_state(test, second_service_worker, 'installed');
+
+ // Clear-Site-Data must clear both workers from the registration.
+ await Promise.all([
+ clear_site_data(),
+ wait_for_state(test, first_service_worker, 'redundant'),
+ wait_for_state(test, second_service_worker, 'redundant')]);
+
+ await assert_no_registrations_exist();
+}, 'Clear-Site-Data must unregister an activated registration with '
+ + 'an update waiting.');
+
+promise_test(async test => {
+ const script_url = 'resources/empty.js';
+ const scope_url =
+ 'resources/blank.html?unregister-immediately-with-controlled-client';
+
+ const registration = await service_worker_unregister_and_register(
+ test, script_url, scope_url);
+ const service_worker = registration.installing;
+
+ await wait_for_state(test, service_worker, 'activated');
+ const frame = await add_controlled_iframe(test, scope_url);
+ const frame_registration =
+ await frame.contentWindow.navigator.serviceWorker.ready;
+
+ const event_watcher = new EventWatcher(
+ test, frame.contentWindow.navigator.serviceWorker, 'controllerchange');
+
+ // Clear-Site-Data must remove the iframe's controller.
+ await Promise.all([
+ clear_site_data(),
+ event_watcher.wait_for('controllerchange'),
+ wait_for_state(test, service_worker, 'redundant')]);
+
+ assert_equals(frame.contentWindow.navigator.serviceWorker.controller, null);
+ await assert_no_registrations_exist();
+
+ // The ready promise must continue to resolve with the unregistered
+ // registration.
+ assert_equals(frame_registration,
+ await frame.contentWindow.navigator.serviceWorker.ready);
+}, 'Clear-Site-Data must unregister an activated registration with controlled '
+ + 'clients.');
+
+promise_test(async test => {
+ const script_url = 'resources/empty.js';
+ const scope_url =
+ 'resources/blank.html?unregister-immediately-while-waiting-to-clear';
+
+ const registration = await service_worker_unregister_and_register(
+ test, script_url, scope_url);
+ const service_worker = registration.installing;
+
+ await wait_for_state(test, service_worker, 'activated');
+ const frame = await add_controlled_iframe(test, scope_url);
+
+ const event_watcher = new EventWatcher(
+ test, frame.contentWindow.navigator.serviceWorker, 'controllerchange');
+
+ // Unregister waits to clear the registration until no controlled clients
+ // exist.
+ await registration.unregister();
+
+ // Clear-Site-Data must clear the unregistered registration immediately.
+ await Promise.all([
+ clear_site_data(),
+ event_watcher.wait_for('controllerchange'),
+ wait_for_state(test, service_worker, 'redundant')]);
+
+ assert_equals(frame.contentWindow.navigator.serviceWorker.controller, null);
+ await assert_no_registrations_exist();
+}, 'Clear-Site-Data must clear an unregistered registration waiting for '
+ + ' controlled clients to unload.');
+
+</script>
+</body>
diff --git a/test/wpt/tests/service-workers/service-worker/unregister-then-register-new-script.https.html b/test/wpt/tests/service-workers/service-worker/unregister-then-register-new-script.https.html
new file mode 100644
index 0000000..d046423
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/unregister-then-register-new-script.https.html
@@ -0,0 +1,136 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+var worker_url = 'resources/empty-worker.js';
+
+promise_test(async function(t) {
+ const scope = 'resources/scope/unregister-then-register-new-script-that-exists';
+ const registration = await service_worker_unregister_and_register(t, worker_url, scope);
+ t.add_cleanup(() => registration.unregister());
+
+ const newWorkerURL = worker_url + '?new';
+ await wait_for_state(t, registration.installing, 'activated');
+
+ const iframe = await with_iframe(scope);
+ t.add_cleanup(() => iframe.remove());
+
+ await registration.unregister();
+
+ const newRegistration = await navigator.serviceWorker.register(newWorkerURL, { scope });
+ t.add_cleanup(() => newRegistration.unregister());
+
+ assert_equals(
+ registration.installing,
+ null,
+ 'before activated registration.installing'
+ );
+ assert_equals(
+ registration.waiting,
+ null,
+ 'before activated registration.waiting'
+ );
+ assert_equals(
+ registration.active.scriptURL,
+ normalizeURL(worker_url),
+ 'before activated registration.active'
+ );
+ assert_equals(
+ newRegistration.installing.scriptURL,
+ normalizeURL(newWorkerURL),
+ 'before activated newRegistration.installing'
+ );
+ assert_equals(
+ newRegistration.waiting,
+ null,
+ 'before activated newRegistration.waiting'
+ );
+ assert_equals(
+ newRegistration.active,
+ null,
+ 'before activated newRegistration.active'
+ );
+ iframe.remove();
+
+ await wait_for_state(t, newRegistration.installing, 'activated');
+
+ assert_equals(
+ newRegistration.installing,
+ null,
+ 'after activated newRegistration.installing'
+ );
+ assert_equals(
+ newRegistration.waiting,
+ null,
+ 'after activated newRegistration.waiting'
+ );
+ assert_equals(
+ newRegistration.active.scriptURL,
+ normalizeURL(newWorkerURL),
+ 'after activated newRegistration.active'
+ );
+
+ const newIframe = await with_iframe(scope);
+ t.add_cleanup(() => newIframe.remove());
+
+ assert_equals(
+ newIframe.contentWindow.navigator.serviceWorker.controller.scriptURL,
+ normalizeURL(newWorkerURL),
+ 'the new worker should control a new document'
+ );
+}, 'Registering a new script URL while an unregistered registration is in use');
+
+promise_test(async function(t) {
+ const scope = 'resources/scope/unregister-then-register-new-script-that-404s';
+ const registration = await service_worker_unregister_and_register(t, worker_url, scope);
+ t.add_cleanup(() => registration.unregister());
+
+ await wait_for_state(t, registration.installing, 'activated');
+
+ const iframe = await with_iframe(scope);
+ t.add_cleanup(() => iframe.remove());
+
+ await registration.unregister();
+
+ await promise_rejects_js(
+ t, TypeError,
+ navigator.serviceWorker.register('this-will-404', { scope })
+ );
+
+ assert_equals(registration.installing, null, 'registration.installing');
+ assert_equals(registration.waiting, null, 'registration.waiting');
+ assert_equals(registration.active.scriptURL, normalizeURL(worker_url), 'registration.active');
+
+ const newIframe = await with_iframe(scope);
+ t.add_cleanup(() => newIframe.remove());
+
+ assert_equals(newIframe.contentWindow.navigator.serviceWorker.controller, null, 'Document should not be controlled');
+}, 'Registering a new script URL that 404s does not resurrect unregistered registration');
+
+promise_test(async function(t) {
+ const scope = 'resources/scope/unregister-then-register-reject-install-worker';
+ const registration = await service_worker_unregister_and_register(t, worker_url, scope);
+ t.add_cleanup(() => registration.unregister());
+
+ await wait_for_state(t, registration.installing, 'activated');
+
+ const iframe = await with_iframe(scope);
+ t.add_cleanup(() => iframe.remove());
+
+ await registration.unregister();
+
+ const newRegistration = await navigator.serviceWorker.register(
+ 'resources/reject-install-worker.js', { scope }
+ );
+ t.add_cleanup(() => newRegistration.unregister());
+
+ await wait_for_state(t, newRegistration.installing, 'redundant');
+
+ assert_equals(registration.installing, null, 'registration.installing');
+ assert_equals(registration.waiting, null, 'registration.waiting');
+ assert_equals(registration.active.scriptURL, normalizeURL(worker_url),
+ 'registration.active');
+ assert_not_equals(registration, newRegistration, 'New registration is different');
+}, 'Registering a new script URL that fails to install does not resurrect unregistered registration');
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/unregister-then-register.https.html b/test/wpt/tests/service-workers/service-worker/unregister-then-register.https.html
new file mode 100644
index 0000000..b61608c
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/unregister-then-register.https.html
@@ -0,0 +1,107 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+var worker_url = 'resources/empty-worker.js';
+
+promise_test(async function(t) {
+ const scope = 'resources/scope/re-register-resolves-to-new-value';
+ const registration = await service_worker_unregister_and_register(t, worker_url, scope);
+ t.add_cleanup(() => registration.unregister());
+
+ await wait_for_state(t, registration.installing, 'activated');
+ await registration.unregister();
+ const newRegistration = await navigator.serviceWorker.register(worker_url, { scope });
+ t.add_cleanup(() => newRegistration.unregister());
+
+ assert_not_equals(
+ registration, newRegistration,
+ 'register should resolve to a new value'
+ );
+ }, 'Unregister then register resolves to a new value');
+
+promise_test(async function(t) {
+ const scope = 'resources/scope/re-register-while-old-registration-in-use';
+ const registration = await service_worker_unregister_and_register(t, worker_url, scope);
+ t.add_cleanup(() => registration.unregister());
+
+ await wait_for_state(t, registration.installing, 'activated');
+ const frame = await with_iframe(scope);
+ t.add_cleanup(() => frame.remove());
+
+ await registration.unregister();
+ const newRegistration = await navigator.serviceWorker.register(worker_url, { scope });
+ t.add_cleanup(() => newRegistration.unregister());
+
+ assert_not_equals(
+ registration, newRegistration,
+ 'Unregister and register should always create a new registration'
+ );
+}, 'Unregister then register does not resolve to the original value even if the registration is in use.');
+
+promise_test(function(t) {
+ var scope = 'resources/scope/re-register-does-not-affect-existing-controllee';
+ var iframe;
+ var registration;
+ var controller;
+
+ return service_worker_unregister_and_register(t, worker_url, scope)
+ .then(function(r) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ registration = r;
+ return wait_for_state(t, r.installing, 'activated');
+ })
+ .then(function() {
+ return with_iframe(scope);
+ })
+ .then(function(frame) {
+ iframe = frame;
+ controller = iframe.contentWindow.navigator.serviceWorker.controller;
+ return registration.unregister();
+ })
+ .then(function() {
+ return navigator.serviceWorker.register(worker_url, { scope: scope });
+ })
+ .then(function(newRegistration) {
+ assert_equals(registration.installing, null,
+ 'installing version is null');
+ assert_equals(registration.waiting, null, 'waiting version is null');
+ assert_equals(
+ iframe.contentWindow.navigator.serviceWorker.controller,
+ controller,
+ 'the worker from the first registration is the controller');
+ iframe.remove();
+ });
+ }, 'Unregister then register does not affect existing controllee');
+
+promise_test(async function(t) {
+ const scope = 'resources/scope/resurrection';
+ const altWorkerURL = worker_url + '?alt';
+ const registration = await service_worker_unregister_and_register(t, worker_url, scope);
+ t.add_cleanup(() => registration.unregister());
+
+ await wait_for_state(t, registration.installing, 'activating');
+ const iframe = await with_iframe(scope);
+ t.add_cleanup(() => iframe.remove());
+
+ await registration.unregister();
+ const newRegistration = await navigator.serviceWorker.register(altWorkerURL, { scope });
+ t.add_cleanup(() => newRegistration.unregister());
+
+ assert_equals(newRegistration.active, null, 'Registration is new');
+
+ await wait_for_state(t, newRegistration.installing, 'activating');
+
+ const newIframe = await with_iframe(scope);
+ t.add_cleanup(() => newIframe.remove());
+
+ const iframeController = iframe.contentWindow.navigator.serviceWorker.controller;
+ const newIframeController = newIframe.contentWindow.navigator.serviceWorker.controller;
+
+ assert_not_equals(iframeController, newIframeController, 'iframes have different controllers');
+}, 'Unregister then register does not resurrect the registration');
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/unregister.https.html b/test/wpt/tests/service-workers/service-worker/unregister.https.html
new file mode 100644
index 0000000..492aecb
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/unregister.https.html
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+async_test(function(t) {
+ var scope = 'resources/scope/unregister-twice';
+ var registration;
+ navigator.serviceWorker.register('resources/empty-worker.js',
+ {scope: scope})
+ .then(function(r) {
+ registration = r;
+ return registration.unregister();
+ })
+ .then(function() {
+ return registration.unregister();
+ })
+ .then(function(value) {
+ assert_equals(value, false,
+ 'unregistering twice should resolve with false');
+ t.done();
+ })
+ .catch(unreached_rejection(t));
+ }, 'Unregister twice');
+
+async_test(function(t) {
+ var scope = 'resources/scope/successful-unregister/';
+ navigator.serviceWorker.register('resources/empty-worker.js',
+ {scope: scope})
+ .then(function(registration) {
+ return registration.unregister();
+ })
+ .then(function(value) {
+ assert_equals(value, true,
+ 'unregistration should resolve with true');
+ t.done();
+ })
+ .catch(unreached_rejection(t));
+ }, 'Register then unregister');
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/update-after-navigation-fetch-event.https.html b/test/wpt/tests/service-workers/service-worker/update-after-navigation-fetch-event.https.html
new file mode 100644
index 0000000..ff51f7f
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/update-after-navigation-fetch-event.https.html
@@ -0,0 +1,91 @@
+<!DOCTYPE html>
+<meta name=timeout content=long>
+<title>Service Worker: Update should be triggered after a navigation</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+
+async function cleanup(frame, registration) {
+ if (frame)
+ frame.remove();
+ if (registration)
+ await registration.unregister();
+}
+
+promise_test(async t => {
+ const script = 'resources/update_shell.py?filename=empty.js';
+ const scope = 'resources/scope/update';
+ let registration;
+ let frame;
+
+ async function run() {
+ registration = await service_worker_unregister_and_register(
+ t, script, scope);
+ await wait_for_state(t, registration.installing, 'activated');
+
+ // Navigation should trigger update.
+ frame = await with_iframe(scope);
+ await wait_for_update(t, registration);
+ }
+
+ try {
+ await run();
+ } finally {
+ await cleanup(frame, registration);
+ }
+}, 'Update should be triggered after a navigation (no fetch event worker).');
+
+promise_test(async t => {
+ const script = 'resources/update_shell.py?filename=simple-intercept-worker.js';
+ const scope = 'resources/scope/update';
+ let registration;
+ let frame;
+
+ async function run() {
+ registration = await service_worker_unregister_and_register(
+ t, script, scope);
+ await wait_for_state(t, registration.installing, 'activated');
+
+ // Navigation should trigger update (network fallback).
+ frame = await with_iframe(scope + '?ignore');
+ await wait_for_update(t, registration);
+
+ // Navigation should trigger update (respondWith called).
+ frame.src = scope + '?string';
+ await wait_for_update(t, registration);
+ }
+
+ try {
+ await run();
+ } finally {
+ await cleanup(frame, registration);
+ }
+}, 'Update should be triggered after a navigation (fetch event worker).');
+
+promise_test(async t => {
+ const script = 'resources/update_shell.py?filename=empty.js';
+ const scope = 'resources/';
+ let registration;
+ let frame;
+
+ async function run() {
+ registration = await service_worker_unregister_and_register(
+ t, script, scope);
+ await wait_for_state(t, registration.installing, 'activated');
+
+ // Navigation should trigger update. Don't use with_iframe as it waits for
+ // the onload event.
+ frame = document.createElement('iframe');
+ frame.src = 'resources/malformed-http-response.asis';
+ document.body.appendChild(frame);
+ await wait_for_update(t, registration);
+ }
+
+ try {
+ await run();
+ } finally {
+ await cleanup(frame, registration);
+ }
+}, 'Update should be triggered after a navigation (network error).');
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/update-after-navigation-redirect.https.html b/test/wpt/tests/service-workers/service-worker/update-after-navigation-redirect.https.html
new file mode 100644
index 0000000..6e821fe
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/update-after-navigation-redirect.https.html
@@ -0,0 +1,74 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Service Worker: Update should be triggered after redirects during navigation</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+
+promise_test(async t => {
+ // This test does a navigation that goes through a redirect chain. Each
+ // request in the chain has a service worker. Each service worker has no
+ // fetch event handler. The redirects are performed by redirect.py.
+ const script = 'resources/update-nocookie-worker.py';
+ const scope1 = 'resources/redirect.py?scope1';
+ const scope2 = 'resources/redirect.py?scope2';
+ const scope3 = 'resources/empty.html';
+ let registration1;
+ let registration2;
+ let registration3;
+ let frame;
+
+ async function cleanup() {
+ if (frame)
+ frame.remove();
+ if (registration1)
+ return registration1.unregister();
+ if (registration2)
+ return registration2.unregister();
+ if (registration3)
+ return registration3.unregister();
+ }
+
+ async function make_active_registration(scope) {
+ const registration =
+ await service_worker_unregister_and_register(t, script, scope);
+ await wait_for_state(t, registration.installing, 'activated');
+ return registration;
+ }
+
+ async function run() {
+ // Make the registrations.
+ registration1 = await make_active_registration(scope1);
+ registration2 = await make_active_registration(scope2);
+ registration3 = await make_active_registration(scope3);
+
+ // Make the promises that resolve on update.
+ const saw_update1 = wait_for_update(t, registration1);
+ const saw_update2 = wait_for_update(t, registration2);
+ const saw_update3 = wait_for_update(t, registration3);
+
+ // Create a URL for the redirect chain: scope1 -> scope2 -> scope3.
+ // Build the URL in reverse order.
+ let url = `${base_path()}${scope3}`;
+ url = `${base_path()}${scope2}&Redirect=${encodeURIComponent(url)}`
+ url = `${base_path()}${scope1}&Redirect=${encodeURIComponent(url)}`
+
+ // Navigate to the URL.
+ frame = await with_iframe(url);
+
+ // Each registration should update.
+ await saw_update1;
+ await saw_update2;
+ await saw_update3;
+ }
+
+ try {
+ await run();
+ } finally {
+ await cleanup();
+ }
+}, 'service workers are updated on redirects during navigation');
+
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/update-after-oneday.https.html b/test/wpt/tests/service-workers/service-worker/update-after-oneday.https.html
new file mode 100644
index 0000000..e7a8aa4
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/update-after-oneday.https.html
@@ -0,0 +1,51 @@
+<!DOCTYPE html>
+<!-- This test requires browser to treat all registrations are older than 24 hours.
+ Preference 'dom.serviceWorkers.testUpdateOverOneDay' should be enabled during
+ the execution of the test -->
+<title>Service Worker: Functional events should trigger update if last update time is over 24 hours</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+
+promise_test(function(t) {
+ var script = 'resources/update-nocookie-worker.py';
+ var scope = 'resources/update/update-after-oneday.https.html';
+ var expected_url = normalizeURL(script);
+ var registration;
+ var frame;
+
+ return service_worker_unregister_and_register(t, expected_url, scope)
+ .then(function(r) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ registration = r;
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() { return with_iframe(scope); })
+ .then(function(f) {
+ frame = f;
+ return wait_for_update(t, registration);
+ })
+ .then(function() {
+ assert_equals(registration.installing.scriptURL, expected_url,
+ 'new installing should be set after update resolves.');
+ assert_equals(registration.waiting, null,
+ 'waiting should still be null after update resolves.');
+ assert_equals(registration.active.scriptURL, expected_url,
+ 'active should still exist after update found.');
+ return wait_for_state(t, registration.installing, 'installed');
+ })
+ .then(function() {
+ // Trigger a non-navigation fetch event
+ frame.contentWindow.load_image(normalizeURL('resources/update/sample'));
+ return wait_for_update(t, registration);
+ })
+ .then(function() {
+ frame.remove();
+ })
+ }, 'Update should be triggered after a functional event when last update time is over 24 hours');
+
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/update-bytecheck-cors-import.https.html b/test/wpt/tests/service-workers/service-worker/update-bytecheck-cors-import.https.html
new file mode 100644
index 0000000..121a737
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/update-bytecheck-cors-import.https.html
@@ -0,0 +1,92 @@
+<!doctype html>
+<meta charset=utf-8>
+<title></title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="resources/testharness-helpers.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script>
+// Tests of updating a service worker. This file contains cors cases only.
+
+/*
+ * @param string main
+ * Decide the content of the main script, where 'default' is for constant
+ * content while 'time' is for time-variant content.
+ * @param string imported
+ * Decide the content of the imported script, where 'default' is for constant
+ * content while 'time' is for time-variant content.
+ */
+const settings = [{main: 'default', imported: 'default'},
+ {main: 'default', imported: 'time' },
+ {main: 'time', imported: 'default'},
+ {main: 'time', imported: 'time' }];
+
+const host_info = get_host_info();
+settings.forEach(({main, imported}) => {
+ promise_test(async (t) => {
+ // Specify a cross origin path to load imported scripts from a cross origin.
+ const path = host_info.HTTPS_REMOTE_ORIGIN +
+ '/service-workers/service-worker/resources/';
+ const script = 'resources/bytecheck-worker.py' +
+ '?main=' + main +
+ '&imported=' + imported +
+ '&path=' + path +
+ '&type=classic';
+ const scope = 'resources/blank.html';
+
+ // Register a service worker.
+ const swr = await service_worker_unregister_and_register(t, script, scope);
+ t.add_cleanup(() => swr.unregister());
+ const sw = await wait_for_update(t, swr);
+ await wait_for_state(t, sw, 'activated');
+ assert_array_equals([swr.active, swr.waiting, swr.installing],
+ [sw, null, null]);
+
+ // Update the service worker registration.
+ await swr.update();
+
+ // If there should be a new service worker.
+ if (main === 'time' || imported === 'time') {
+ return wait_for_update(t, swr);
+ }
+ // Otherwise, make sure there is no newly created service worker.
+ assert_array_equals([swr.active, swr.waiting, swr.installing],
+ [sw, null, null]);
+ }, `Test(main: ${main}, imported: ${imported})`);
+});
+
+settings.forEach(({main, imported}) => {
+ promise_test(async (t) => {
+ // Specify a cross origin path to load imported scripts from a cross origin.
+ const path = host_info.HTTPS_REMOTE_ORIGIN +
+ '/service-workers/service-worker/resources/';
+ const script = 'resources/bytecheck-worker.py' +
+ '?main=' + main +
+ '&imported=' + imported +
+ '&path=' + path +
+ '&type=module';
+ const scope = 'resources/blank.html';
+
+ // Register a service worker.
+ const swr = await service_worker_unregister_and_register(t, script, scope, {type: 'module'});
+ t.add_cleanup(() => swr.unregister());
+ const sw = await wait_for_update(t, swr);
+ await wait_for_state(t, sw, 'activated');
+ assert_array_equals([swr.active, swr.waiting, swr.installing],
+ [sw, null, null]);
+
+ // Update the service worker registration.
+ await swr.update();
+
+ // If there should be a new service worker.
+ if (main === 'time' || imported === 'time') {
+ return wait_for_update(t, swr);
+ }
+ // Otherwise, make sure there is no newly created service worker.
+ assert_array_equals([swr.active, swr.waiting, swr.installing],
+ [sw, null, null]);
+ }, `Test module script(main: ${main}, imported: ${imported})`);
+});
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/update-bytecheck.https.html b/test/wpt/tests/service-workers/service-worker/update-bytecheck.https.html
new file mode 100644
index 0000000..3e5a28b
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/update-bytecheck.https.html
@@ -0,0 +1,92 @@
+<!doctype html>
+<meta charset=utf-8>
+<title></title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="resources/testharness-helpers.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script>
+// Tests of updating a service worker. This file contains non-cors cases only.
+
+/*
+ * @param string main
+ * Decide the content of the main script, where 'default' is for constant
+ * content while 'time' is for time-variant content.
+ * @param string imported
+ * Decide the content of the imported script, where 'default' is for constant
+ * content while 'time' is for time-variant content.
+ */
+const settings = [{main: 'default', imported: 'default'},
+ {main: 'default', imported: 'time' },
+ {main: 'time', imported: 'default'},
+ {main: 'time', imported: 'time' }];
+
+const host_info = get_host_info();
+settings.forEach(({main, imported}) => {
+ promise_test(async (t) => {
+ // Empty path results in the same origin imported scripts.
+ const path = '';
+ const script = 'resources/bytecheck-worker.py' +
+ '?main=' + main +
+ '&imported=' + imported +
+ '&path=' + path +
+ '&type=classic';
+ const scope = 'resources/blank.html';
+
+ // Register a service worker.
+ const swr = await service_worker_unregister_and_register(t, script, scope);
+ t.add_cleanup(() => swr.unregister());
+ const sw = await wait_for_update(t, swr);
+ await wait_for_state(t, sw, 'activated');
+ assert_array_equals([swr.active, swr.waiting, swr.installing],
+ [sw, null, null]);
+
+ // Update the service worker registration.
+ await swr.update();
+
+ // If there should be a new service worker.
+ if (main === 'time' || imported === 'time') {
+ return wait_for_update(t, swr);
+ }
+ // Otherwise, make sure there is no newly created service worker.
+ assert_array_equals([swr.active, swr.waiting, swr.installing],
+ [sw, null, null]);
+ }, `Test(main: ${main}, imported: ${imported})`);
+});
+
+settings.forEach(({main, imported}) => {
+ promise_test(async (t) => {
+ // Empty path results in the same origin imported scripts.
+ const path = './';
+ const script = 'resources/bytecheck-worker.py' +
+ '?main=' + main +
+ '&imported=' + imported +
+ '&path=' + path +
+ '&type=module';
+ const scope = 'resources/blank.html';
+
+ // Register a module service worker.
+ const swr = await service_worker_unregister_and_register(t, script, scope,
+ {type: 'module'});
+
+ t.add_cleanup(() => swr.unregister());
+ const sw = await wait_for_update(t, swr);
+ await wait_for_state(t, sw, 'activated');
+ assert_array_equals([swr.active, swr.waiting, swr.installing],
+ [sw, null, null]);
+
+ // Update the service worker registration.
+ await swr.update();
+
+ // If there should be a new service worker.
+ if (main === 'time' || imported === 'time') {
+ return wait_for_update(t, swr);
+ }
+ // Otherwise, make sure there is no newly created service worker.
+ assert_array_equals([swr.active, swr.waiting, swr.installing],
+ [sw, null, null]);
+ }, `Test module script(main: ${main}, imported: ${imported})`);
+});
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/update-import-scripts.https.html b/test/wpt/tests/service-workers/service-worker/update-import-scripts.https.html
new file mode 100644
index 0000000..a2df529
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/update-import-scripts.https.html
@@ -0,0 +1,135 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Tests for importScripts: import scripts ignored error</title>
+<script src="/common/utils.js"></script>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+// This file contains tests to check if imported scripts appropriately updated.
+
+const SCOPE = 'resources/simple.txt';
+
+// Create a service worker (update-worker-from-file.py), which is initially
+// |initial_worker| and |updated_worker| later.
+async function prepare_ready_update_worker_from_file(
+ t, initial_worker, updated_worker) {
+ const key = token();
+ const worker_url = `resources/update-worker-from-file.py?` +
+ `First=${initial_worker}&Second=${updated_worker}&Key=${key}`;
+ const expected_url = normalizeURL(worker_url);
+
+ const registration = await service_worker_unregister_and_register(
+ t, worker_url, SCOPE);
+ await wait_for_state(t, registration.installing, 'activated');
+ assert_equals(registration.installing, null,
+ 'prepare_ready: installing');
+ assert_equals(registration.waiting, null,
+ 'prepare_ready: waiting');
+ assert_equals(registration.active.scriptURL, expected_url,
+ 'prepare_ready: active');
+ return [registration, expected_url];
+}
+
+// Create a service worker using the script under resources/.
+async function prepare_ready_normal_worker(t, filename, additional_params='') {
+ const key = token();
+ const worker_url = `resources/${filename}?Key=${key}&${additional_params}`;
+ const expected_url = normalizeURL(worker_url);
+
+ const registration = await service_worker_unregister_and_register(
+ t, worker_url, SCOPE);
+ await wait_for_state(t, registration.installing, 'activated');
+ assert_equals(registration.installing, null,
+ 'prepare_ready: installing');
+ assert_equals(registration.waiting, null,
+ 'prepare_ready: waiting');
+ assert_equals(registration.active.scriptURL, expected_url,
+ 'prepare_ready: active');
+ return [registration, expected_url];
+}
+
+function assert_installing_and_active(registration, expected_url) {
+ assert_equals(registration.installing.scriptURL, expected_url,
+ 'assert_installing_and_active: installing');
+ assert_equals(registration.waiting, null,
+ 'assert_installing_and_active: waiting');
+ assert_equals(registration.active.scriptURL, expected_url,
+ 'assert_installing_and_active: active');
+}
+
+function assert_waiting_and_active(registration, expected_url) {
+ assert_equals(registration.installing, null,
+ 'assert_waiting_and_active: installing');
+ assert_equals(registration.waiting.scriptURL, expected_url,
+ 'assert_waiting_and_active: waiting');
+ assert_equals(registration.active.scriptURL, expected_url,
+ 'assert_waiting_and_active: active');
+}
+
+function assert_active_only(registration, expected_url) {
+ assert_equals(registration.installing, null,
+ 'assert_active_only: installing');
+ assert_equals(registration.waiting, null,
+ 'assert_active_only: waiting');
+ assert_equals(registration.active.scriptURL, expected_url,
+ 'assert_active_only: active');
+}
+
+promise_test(async t => {
+ const [registration, expected_url] =
+ await prepare_ready_update_worker_from_file(
+ t, 'empty.js', 'import-scripts-404.js');
+ t.add_cleanup(() => registration.unregister());
+
+ await promise_rejects_js(t, TypeError, registration.update());
+ assert_active_only(registration, expected_url);
+}, 'update() should fail when a new worker imports an unavailable script.');
+
+promise_test(async t => {
+ const [registration, expected_url] =
+ await prepare_ready_update_worker_from_file(
+ t, 'import-scripts-404-after-update.js', 'empty.js');
+ t.add_cleanup(() => registration.unregister());
+
+ await Promise.all([registration.update(), wait_for_update(t, registration)]);
+ assert_installing_and_active(registration, expected_url);
+
+ await wait_for_state(t, registration.installing, 'installed');
+ assert_waiting_and_active(registration, expected_url);
+
+ await wait_for_state(t, registration.waiting, 'activated');
+ assert_active_only(registration, expected_url);
+}, 'update() should succeed when the old imported script no longer exist but ' +
+ "the new worker doesn't import it.");
+
+promise_test(async t => {
+ const [registration, expected_url] = await prepare_ready_normal_worker(
+ t, 'import-scripts-404-after-update.js');
+ t.add_cleanup(() => registration.unregister());
+
+ await registration.update();
+ assert_active_only(registration, expected_url);
+}, 'update() should treat 404 on imported scripts as no change.');
+
+promise_test(async t => {
+ const [registration, expected_url] = await prepare_ready_normal_worker(
+ t, 'import-scripts-404-after-update-plus-update-worker.js',
+ `AdditionalKey=${token()}`);
+ t.add_cleanup(() => registration.unregister());
+
+ await promise_rejects_js(t, TypeError, registration.update());
+ assert_active_only(registration, expected_url);
+}, 'update() should find an update in an imported script but update() should ' +
+ 'result in failure due to missing the other imported script.');
+
+promise_test(async t => {
+ const [registration, expected_url] = await prepare_ready_normal_worker(
+ t, 'import-scripts-cross-origin-worker.sub.js');
+ t.add_cleanup(() => registration.unregister());
+ await registration.update();
+ assert_installing_and_active(registration, expected_url);
+}, 'update() should work with cross-origin importScripts.');
+</script>
+</body>
diff --git a/test/wpt/tests/service-workers/service-worker/update-missing-import-scripts.https.html b/test/wpt/tests/service-workers/service-worker/update-missing-import-scripts.https.html
new file mode 100644
index 0000000..66e8bfa
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/update-missing-import-scripts.https.html
@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<title>Service Worker: update with missing importScripts</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script src="/common/utils.js"></script>
+<body>
+<script>
+/**
+ * Test ServiceWorkerRegistration.update() when importScripts in a service worker
+ * script is no longer available (but was initially).
+ */
+let registration = null;
+
+promise_test(async (test) => {
+ const script = `resources/update-missing-import-scripts-main-worker.py?key=${token()}`;
+ const scope = 'resources/update-missing-import-scripts';
+
+ registration = await service_worker_unregister_and_register(test, script, scope);
+
+ add_completion_callback(() => { registration.unregister(); });
+
+ await wait_for_state(test, registration.installing, 'activated');
+}, 'Initialize global state');
+
+promise_test(test => {
+ return new Promise(resolve => {
+ registration.addEventListener('updatefound', resolve);
+ registration.update();
+ });
+}, 'Update service worker with new script that\'s missing importScripts()');
+</script>
+</body>
diff --git a/test/wpt/tests/service-workers/service-worker/update-module-request-mode.https.html b/test/wpt/tests/service-workers/service-worker/update-module-request-mode.https.html
new file mode 100644
index 0000000..b3875d2
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/update-module-request-mode.https.html
@@ -0,0 +1,45 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<meta name="timeout" content="long">
+<title>Test that mode is set to same-origin for a main module</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+// Tests a main module service worker script fetch during an update check.
+// The fetch should have the mode set to 'same-origin'.
+//
+// The test works by registering a main module service worker. It then does an
+// update. The test server responds with an updated worker script that remembers
+// the http request. The updated worker reports back this request to the test
+// page.
+promise_test(async (t) => {
+ const script = "resources/test-request-mode-worker.py";
+ const scope = "resources/";
+
+ // Register the service worker.
+ await service_worker_unregister(t, scope);
+ const registration = await navigator.serviceWorker.register(
+ script, {scope, type: 'module'});
+ await wait_for_state(t, registration.installing, 'activated');
+
+ // Do an update.
+ await registration.update();
+
+ // Ask the new worker what the request was.
+ const newWorker = registration.installing;
+ const sawMessage = new Promise((resolve) => {
+ navigator.serviceWorker.onmessage = (event) => {
+ resolve(event.data);
+ };
+ });
+ newWorker.postMessage('getHeaders');
+ const result = await sawMessage;
+
+ // Test the result.
+ assert_equals(result['sec-fetch-mode'], 'same-origin');
+ assert_equals(result['origin'], undefined);
+
+}, 'headers of a main module script');
+
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/update-no-cache-request-headers.https.html b/test/wpt/tests/service-workers/service-worker/update-no-cache-request-headers.https.html
new file mode 100644
index 0000000..6ebad4b
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/update-no-cache-request-headers.https.html
@@ -0,0 +1,48 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Test that cache is being bypassed/validated in no-cache mode on update</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+// Tests a service worker script fetch during an update check which
+// bypasses/validates the browser cache. The fetch should have the
+// 'if-none-match' request header.
+//
+// This tests the Update step:
+// "Set request’s cache mode to "no-cache" if any of the following are true..."
+// https://w3c.github.io/ServiceWorker/#update-algorithm
+//
+// The test works by registering a service worker with |updateViaCache|
+// set to "none". It then does an update. The test server responds with
+// an updated worker script that remembers the http request headers.
+// The updated worker reports back these headers to the test page.
+promise_test(async (t) => {
+ const script = "resources/test-request-headers-worker.py";
+ const scope = "resources/";
+
+ // Register the service worker.
+ await service_worker_unregister(t, scope);
+ const registration = await navigator.serviceWorker.register(
+ script, {scope, updateViaCache: 'none'});
+ await wait_for_state(t, registration.installing, 'activated');
+
+ // Do an update.
+ await registration.update();
+
+ // Ask the new worker what the request headers were.
+ const newWorker = registration.installing;
+ const sawMessage = new Promise((resolve) => {
+ navigator.serviceWorker.onmessage = (event) => {
+ resolve(event.data);
+ };
+ });
+ newWorker.postMessage('getHeaders');
+ const result = await sawMessage;
+
+ // Test the result.
+ assert_equals(result['service-worker'], 'script');
+ assert_equals(result['if-none-match'], 'etag');
+}, 'headers in no-cache mode');
+
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/update-not-allowed.https.html b/test/wpt/tests/service-workers/service-worker/update-not-allowed.https.html
new file mode 100644
index 0000000..0a54aa9
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/update-not-allowed.https.html
@@ -0,0 +1,140 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+'use strict';
+
+function send_message_to_worker_and_wait_for_response(worker, message) {
+ return new Promise(resolve => {
+ // Use a dedicated channel for every request to avoid race conditions on
+ // concurrent requests.
+ const channel = new MessageChannel();
+ worker.postMessage(channel.port1, [channel.port1]);
+
+ let messageReceived = false;
+ channel.port2.onmessage = event => {
+ assert_false(messageReceived, 'Already received response for ' + message);
+ messageReceived = true;
+ resolve(event.data);
+ };
+ channel.port2.postMessage(message);
+ });
+}
+
+async function ensure_install_event_fired(worker) {
+ const response = await send_message_to_worker_and_wait_for_response(worker, 'awaitInstallEvent');
+ assert_equals('installEventFired', response);
+ assert_equals('installing', worker.state, 'Expected worker to be installing.');
+}
+
+async function finish_install(worker) {
+ await ensure_install_event_fired(worker);
+ const response = await send_message_to_worker_and_wait_for_response(worker, 'finishInstall');
+ assert_equals('installFinished', response);
+}
+
+async function activate_service_worker(t, worker) {
+ await finish_install(worker);
+ // By waiting for both states at the same time, the test fails
+ // quickly if the installation fails, avoiding a timeout.
+ await Promise.race([wait_for_state(t, worker, 'activated'),
+ wait_for_state(t, worker, 'redundant')]);
+ assert_equals('activated', worker.state, 'Service worker should be activated.');
+}
+
+async function update_within_service_worker(worker) {
+ // This function returns a Promise that resolves when update()
+ // has been called but is not necessarily finished yet.
+ // Call finish() on the returned object to wait for update() settle.
+ const port = await send_message_to_worker_and_wait_for_response(worker, 'callUpdate');
+ let messageReceived = false;
+ return {
+ finish: () => {
+ return new Promise(resolve => {
+ port.onmessage = event => {
+ assert_false(messageReceived, 'Update already finished.');
+ messageReceived = true;
+ resolve(event.data);
+ };
+ });
+ },
+ };
+}
+
+async function update_from_client_and_await_installing_version(test, registration) {
+ const updatefound = wait_for_update(test, registration);
+ registration.update();
+ await updatefound;
+ return registration.installing;
+}
+
+async function spin_up_service_worker(test) {
+ const script = 'resources/update-during-installation-worker.py';
+ const scope = 'resources/blank.html';
+
+ const registration = await service_worker_unregister_and_register(test, script, scope);
+ test.add_cleanup(async () => {
+ if (registration.installing) {
+ // If there is an installing worker, we need to finish installing it.
+ // Otherwise, the tests fails with an timeout because unregister() blocks
+ // until the install-event-handler finishes.
+ const worker = registration.installing;
+ await send_message_to_worker_and_wait_for_response(worker, 'awaitInstallEvent');
+ await send_message_to_worker_and_wait_for_response(worker, 'finishInstall');
+ }
+ return registration.unregister();
+ });
+
+ return registration;
+}
+
+promise_test(async t => {
+ const registration = await spin_up_service_worker(t);
+ const worker = registration.installing;
+ await ensure_install_event_fired(worker);
+
+ const result = registration.update();
+ await activate_service_worker(t, worker);
+ return result;
+}, 'ServiceWorkerRegistration.update() from client succeeds while installing service worker.');
+
+promise_test(async t => {
+ const registration = await spin_up_service_worker(t);
+ const worker = registration.installing;
+ await ensure_install_event_fired(worker);
+
+ // Add event listener to fail the test if update() succeeds.
+ const updatefound = t.step_func(async () => {
+ registration.removeEventListener('updatefound', updatefound);
+ // Activate new worker so non-compliant browsers don't fail with timeout.
+ await activate_service_worker(t, registration.installing);
+ assert_unreached("update() should have failed");
+ });
+ registration.addEventListener('updatefound', updatefound);
+
+ const update = await update_within_service_worker(worker);
+ // Activate worker to ensure update() finishes and the test doesn't timeout
+ // in non-compliant browsers.
+ await activate_service_worker(t, worker);
+
+ const response = await update.finish();
+ assert_false(response.success, 'update() should have failed.');
+ assert_equals('InvalidStateError', response.exception, 'update() should have thrown InvalidStateError.');
+}, 'ServiceWorkerRegistration.update() from installing service worker throws.');
+
+promise_test(async t => {
+ const registration = await spin_up_service_worker(t);
+ const worker1 = registration.installing;
+ await activate_service_worker(t, worker1);
+
+ const worker2 = await update_from_client_and_await_installing_version(t, registration);
+ await ensure_install_event_fired(worker2);
+
+ const update = await update_within_service_worker(worker1);
+ // Activate the new version so that update() finishes and the test doesn't timeout.
+ await activate_service_worker(t, worker2);
+ const response = await update.finish();
+ assert_true(response.success, 'update() from active service worker should have succeeded.');
+}, 'ServiceWorkerRegistration.update() from active service worker succeeds while installing service worker.');
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/update-on-navigation.https.html b/test/wpt/tests/service-workers/service-worker/update-on-navigation.https.html
new file mode 100644
index 0000000..5273420
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/update-on-navigation.https.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<title>Update on navigation</title>
+<script src='/resources/testharness.js'></script>
+<script src='/resources/testharnessreport.js'></script>
+<script src='resources/test-helpers.sub.js'></script>
+<script>
+promise_test(async (t) => {
+ var script = 'resources/update-fetch-worker.py';
+ var scope = 'resources/trickle.py?ms=1000&count=1';
+
+ const registration = await service_worker_unregister_and_register(t, script, scope);
+ t.add_cleanup(() => registration.unregister());
+
+ if (registration.installing)
+ await wait_for_state(t, registration.installing, 'activated');
+
+ const frame = await with_iframe(scope);
+ t.add_cleanup(() => frame.remove());
+}, 'The active service worker in charge of a navigation load should not be terminated as part of updating the registration');
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/update-recovery.https.html b/test/wpt/tests/service-workers/service-worker/update-recovery.https.html
new file mode 100644
index 0000000..17608d2
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/update-recovery.https.html
@@ -0,0 +1,73 @@
+<!DOCTYPE html>
+<title>Service Worker: recovery by navigation update</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/testharness-helpers.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+promise_test(function(t) {
+ var scope = 'resources/simple.txt';
+ var worker_url = 'resources/update-recovery-worker.py';
+ var expected_url = normalizeURL(worker_url);
+ var registration;
+
+ function with_bad_iframe(url) {
+ return new Promise(function(resolve, reject) {
+ var frame = document.createElement('iframe');
+
+ // There is no cross-browser event to listen for to detect an
+ // iframe that fails to load due to a bad interception. Unfortunately
+ // we have to use a timeout.
+ var timeout = setTimeout(function() {
+ frame.remove();
+ resolve();
+ }, 5000);
+
+ // If we do get a load event, though, we know something went wrong.
+ frame.addEventListener('load', function() {
+ clearTimeout(timeout);
+ frame.remove();
+ reject('expected bad iframe should not fire a load event!');
+ });
+
+ frame.src = url;
+ document.body.appendChild(frame);
+ });
+ }
+
+ function with_update(t) {
+ return new Promise(function(resolve, reject) {
+ registration.addEventListener('updatefound', function onUpdate() {
+ registration.removeEventListener('updatefound', onUpdate);
+ wait_for_state(t, registration.installing, 'activated').then(function() {
+ resolve();
+ });
+ });
+ });
+ }
+
+ return service_worker_unregister_and_register(t, worker_url, scope)
+ .then(function(r) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ registration = r;
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() {
+ return Promise.all([
+ with_update(t),
+ with_bad_iframe(scope)
+ ]);
+ })
+ .then(function() {
+ return with_iframe(scope);
+ })
+ .then(function(frame) {
+ assert_equals(frame.contentWindow.navigator.serviceWorker.controller.scriptURL,
+ expected_url);
+ frame.remove();
+ });
+ }, 'Recover from a bad service worker by updating after a failed navigation.');
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/update-registration-with-type.https.html b/test/wpt/tests/service-workers/service-worker/update-registration-with-type.https.html
new file mode 100644
index 0000000..269e61b
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/update-registration-with-type.https.html
@@ -0,0 +1,208 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Service Worker: Update the registration with a different script type.</title>
+<!-- common.js is for guid() -->
+<script src="/common/security-features/resources/common.sub.js"></script>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+// The following two tests check that a registration is updated correctly
+// with different script type. At first Service Worker is registered as
+// classic script type, then it is re-registered as module script type,
+// and vice versa. A main script is also updated at the same time.
+promise_test(async t => {
+ const key = guid();
+ const script = `resources/update-registration-with-type.py?classic_first=1&key=${key}`;
+ const scope = 'resources/update-registration-with-type';
+ await service_worker_unregister(t, scope);
+ t.add_cleanup(() => service_worker_unregister(t, scope));
+
+ // Register with classic script type.
+ const firstRegistration = await navigator.serviceWorker.register(script, {
+ scope: scope,
+ type: 'classic'
+ });
+ const firstWorker = firstRegistration.installing;
+ await wait_for_state(t, firstWorker, 'activated');
+ firstWorker.postMessage(' ');
+ let msgEvent = await new Promise(r => navigator.serviceWorker.onmessage = r);
+ assert_equals(msgEvent.data, 'A classic script.');
+
+ // Re-register with module script type.
+ const secondRegistration = await navigator.serviceWorker.register(script, {
+ scope: scope,
+ type: 'module'
+ });
+ const secondWorker = secondRegistration.installing;
+ secondWorker.postMessage(' ');
+ msgEvent = await new Promise(r => navigator.serviceWorker.onmessage = r);
+ assert_equals(msgEvent.data, 'A module script.');
+
+ assert_not_equals(firstWorker, secondWorker);
+ assert_equals(firstRegistration, secondRegistration);
+}, 'Update the registration with a different script type (classic => module).');
+
+promise_test(async t => {
+ const key = guid();
+ const script = `resources/update-registration-with-type.py?classic_first=0&key=${key}`;
+ const scope = 'resources/update-registration-with-type';
+ await service_worker_unregister(t, scope);
+ t.add_cleanup(() => service_worker_unregister(t, scope));
+
+ // Register with module script type.
+ const firstRegistration = await navigator.serviceWorker.register(script, {
+ scope: scope,
+ type: 'module'
+ });
+ const firstWorker = firstRegistration.installing;
+ await wait_for_state(t, firstWorker, 'activated');
+ firstWorker.postMessage(' ');
+ let msgEvent = await new Promise(r => navigator.serviceWorker.onmessage = r);
+ assert_equals(msgEvent.data, 'A module script.');
+
+ // Re-register with classic script type.
+ const secondRegistration = await navigator.serviceWorker.register(script, {
+ scope: scope,
+ type: 'classic'
+ });
+ const secondWorker = secondRegistration.installing;
+ secondWorker.postMessage(' ');
+ msgEvent = await new Promise(r => navigator.serviceWorker.onmessage = r);
+ assert_equals(msgEvent.data, 'A classic script.');
+
+ assert_not_equals(firstWorker, secondWorker);
+ assert_equals(firstRegistration, secondRegistration);
+}, 'Update the registration with a different script type (module => classic).');
+
+// The following two tests change the script type while keeping
+// the script identical.
+promise_test(async t => {
+ const script = 'resources/empty-worker.js';
+ const scope = 'resources/update-registration-with-type';
+ await service_worker_unregister(t, scope);
+ t.add_cleanup(() => service_worker_unregister(t, scope));
+
+ // Register with classic script type.
+ const firstRegistration = await navigator.serviceWorker.register(script, {
+ scope: scope,
+ type: 'classic'
+ });
+ const firstWorker = firstRegistration.installing;
+ await wait_for_state(t, firstWorker, 'activated');
+
+ // Re-register with module script type.
+ const secondRegistration = await navigator.serviceWorker.register(script, {
+ scope: scope,
+ type: 'module'
+ });
+ const secondWorker = secondRegistration.installing;
+
+ assert_not_equals(firstWorker, secondWorker);
+ assert_equals(firstRegistration, secondRegistration);
+}, 'Update the registration with a different script type (classic => module) '
+ + 'and with a same main script.');
+
+promise_test(async t => {
+ const script = 'resources/empty-worker.js';
+ const scope = 'resources/update-registration-with-type';
+ await service_worker_unregister(t, scope);
+ t.add_cleanup(() => service_worker_unregister(t, scope));
+
+ // Register with module script type.
+ const firstRegistration = await navigator.serviceWorker.register(script, {
+ scope: scope,
+ type: 'module'
+ });
+ const firstWorker = firstRegistration.installing;
+ await wait_for_state(t, firstWorker, 'activated');
+
+ // Re-register with classic script type.
+ const secondRegistration = await navigator.serviceWorker.register(script, {
+ scope: scope,
+ type: 'classic'
+ });
+ const secondWorker = secondRegistration.installing;
+
+ assert_not_equals(firstWorker, secondWorker);
+ assert_equals(firstRegistration, secondRegistration);
+}, 'Update the registration with a different script type (module => classic) '
+ + 'and with a same main script.');
+
+// This test checks that a registration is not updated with the same script
+// type and the same main script.
+promise_test(async t => {
+ const script = 'resources/empty-worker.js';
+ const scope = 'resources/update-registration-with-type';
+ await service_worker_unregister(t, scope);
+ t.add_cleanup(() => service_worker_unregister(t, scope));
+
+ // Register with module script type.
+ const firstRegistration = await navigator.serviceWorker.register(script, {
+ scope: scope,
+ type: 'module'
+ });
+ await wait_for_state(t, firstRegistration.installing, 'activated');
+
+ // Re-register with module script type.
+ const secondRegistration = await navigator.serviceWorker.register(script, {
+ scope: scope,
+ type: 'module'
+ });
+ assert_equals(secondRegistration.installing, null);
+
+ assert_equals(firstRegistration, secondRegistration);
+}, 'Does not update the registration with the same script type and '
+ + 'the same main script.');
+
+// In the case (classic => module), a worker script contains importScripts()
+// that is disallowed on module scripts, so the second registration is
+// expected to fail script evaluation.
+promise_test(async t => {
+ const script = 'resources/classic-worker.js';
+ const scope = 'resources/update-registration-with-type';
+ await service_worker_unregister(t, scope);
+ t.add_cleanup(() => service_worker_unregister(t, scope));
+
+ // Register with classic script type.
+ const firstRegistration = await navigator.serviceWorker.register(script, {
+ scope: scope,
+ type: 'classic'
+ });
+ assert_not_equals(firstRegistration.installing, null);
+ await wait_for_state(t, firstRegistration.installing, 'activated');
+
+ // Re-register with module script type and expect TypeError.
+ return promise_rejects_js(t, TypeError, navigator.serviceWorker.register(script, {
+ scope: scope,
+ type: 'module'
+ }), 'Registering with invalid evaluation should be failed.');
+}, 'Update the registration with a different script type (classic => module) '
+ + 'and with a same main script. Expect evaluation failed.');
+
+// In the case (module => classic), a worker script contains static-import
+// that is disallowed on classic scripts, so the second registration is
+// expected to fail script evaluation.
+promise_test(async t => {
+ const script = 'resources/module-worker.js';
+ const scope = 'resources/update-registration-with-type';
+ await service_worker_unregister(t, scope);
+ t.add_cleanup(() => service_worker_unregister(t, scope));
+
+ // Register with module script type.
+ const firstRegistration = await navigator.serviceWorker.register(script, {
+ scope: scope,
+ type: 'module'
+ });
+ assert_not_equals(firstRegistration.installing, null);
+ await wait_for_state(t, firstRegistration.installing, 'activated');
+
+ // Re-register with classic script type and expect TypeError.
+ return promise_rejects_js(t, TypeError, navigator.serviceWorker.register(script, {
+ scope: scope,
+ type: 'classic'
+ }), 'Registering with invalid evaluation should be failed.');
+}, 'Update the registration with a different script type (module => classic) '
+ + 'and with a same main script. Expect evaluation failed.');
+</script>
+</body>
diff --git a/test/wpt/tests/service-workers/service-worker/update-result.https.html b/test/wpt/tests/service-workers/service-worker/update-result.https.html
new file mode 100644
index 0000000..d8ed94f
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/update-result.https.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<title>Service Worker: update() should resolve a ServiceWorkerRegistration</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+
+promise_test(async function(t) {
+ const script = './resources/empty.js';
+ const scope = './resources/empty.html?update-result';
+
+ let reg = await navigator.serviceWorker.register(script, { scope });
+ t.add_cleanup(async _ => await reg.unregister());
+ await wait_for_state(t, reg.installing, 'activated');
+
+ let result = await reg.update();
+ assert_true(result instanceof ServiceWorkerRegistration,
+ 'update() should resolve a ServiceWorkerRegistration');
+}, 'ServiceWorkerRegistration.update() should resolve a registration object');
+
+</script>
+</body>
diff --git a/test/wpt/tests/service-workers/service-worker/update.https.html b/test/wpt/tests/service-workers/service-worker/update.https.html
new file mode 100644
index 0000000..f9fded3
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/update.https.html
@@ -0,0 +1,164 @@
+<!DOCTYPE html>
+<title>Service Worker: Registration update()</title>
+<meta name="timeout" content="long">
+<script src="/common/utils.js"></script>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/testharness-helpers.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+'use strict';
+
+const SCOPE = 'resources/simple.txt';
+
+// Create a service worker (update-worker.py). The response to update() will be
+// different based on the mode.
+async function prepare_ready_registration_with_mode(t, mode) {
+ const key = token();
+ const worker_url = `resources/update-worker.py?Key=${key}&Mode=${mode}`;
+ const expected_url = normalizeURL(worker_url);
+ const registration = await service_worker_unregister_and_register(
+ t, worker_url, SCOPE);
+ await wait_for_state(t, registration.installing, 'activated');
+ assert_equals(registration.installing, null,
+ 'prepare_ready: installing');
+ assert_equals(registration.waiting, null,
+ 'prepare_ready: waiting');
+ assert_equals(registration.active.scriptURL, expected_url,
+ 'prepare_ready: active');
+ return [registration, expected_url];
+}
+
+// Create a service worker (update-worker-from-file.py), which is initially
+// |initial_worker| and |updated_worker| later.
+async function prepare_ready_registration_with_file(
+ t, initial_worker, updated_worker) {
+ const key = token();
+ const worker_url = `resources/update-worker-from-file.py?` +
+ `First=${initial_worker}&Second=${updated_worker}&Key=${key}`;
+ const expected_url = normalizeURL(worker_url);
+
+ const registration = await service_worker_unregister_and_register(
+ t, worker_url, SCOPE);
+ await wait_for_state(t, registration.installing, 'activated');
+ assert_equals(registration.installing, null,
+ 'prepare_ready: installing');
+ assert_equals(registration.waiting, null,
+ 'prepare_ready: waiting');
+ assert_equals(registration.active.scriptURL, expected_url,
+ 'prepare_ready: active');
+ return [registration, expected_url];
+}
+
+function assert_installing_and_active(registration, expected_url) {
+ assert_equals(registration.installing.scriptURL, expected_url,
+ 'assert_installing_and_active: installing');
+ assert_equals(registration.waiting, null,
+ 'assert_installing_and_active: waiting');
+ assert_equals(registration.active.scriptURL, expected_url,
+ 'assert_installing_and_active: active');
+}
+
+function assert_waiting_and_active(registration, expected_url) {
+ assert_equals(registration.installing, null,
+ 'assert_waiting_and_active: installing');
+ assert_equals(registration.waiting.scriptURL, expected_url,
+ 'assert_waiting_and_active: waiting');
+ assert_equals(registration.active.scriptURL, expected_url,
+ 'assert_waiting_and_active: active');
+}
+
+function assert_active_only(registration, expected_url) {
+ assert_equals(registration.installing, null,
+ 'assert_active_only: installing');
+ assert_equals(registration.waiting, null,
+ 'assert_active_only: waiting');
+ assert_equals(registration.active.scriptURL, expected_url,
+ 'assert_active_only: active');
+}
+
+promise_test(async t => {
+ const [registration, expected_url] =
+ await prepare_ready_registration_with_mode(t, 'normal');
+ t.add_cleanup(() => registration.unregister());
+
+ await Promise.all([registration.update(), wait_for_update(t, registration)]);
+ assert_installing_and_active(registration, expected_url);
+
+ await wait_for_state(t, registration.installing, 'installed');
+ assert_waiting_and_active(registration, expected_url);
+
+ await wait_for_state(t, registration.waiting, 'activated');
+ assert_active_only(registration, expected_url);
+}, 'update() should succeed when new script is available.');
+
+promise_test(async t => {
+ const [registration, expected_url] =
+ await prepare_ready_registration_with_mode(t, 'bad_mime_type');
+ t.add_cleanup(() => registration.unregister());
+
+ await promise_rejects_dom(t, 'SecurityError', registration.update());
+ assert_active_only(registration, expected_url);
+}, 'update() should fail when mime type is invalid.');
+
+promise_test(async t => {
+ const [registration, expected_url] =
+ await prepare_ready_registration_with_mode(t, 'redirect');
+ t.add_cleanup(() => registration.unregister());
+
+ await promise_rejects_js(t, TypeError, registration.update());
+ assert_active_only(registration, expected_url);
+}, 'update() should fail when a response for the main script is redirect.');
+
+promise_test(async t => {
+ const [registration, expected_url] =
+ await prepare_ready_registration_with_mode(t, 'syntax_error');
+ t.add_cleanup(() => registration.unregister());
+
+ await promise_rejects_js(t, TypeError, registration.update());
+ assert_active_only(registration, expected_url);
+}, 'update() should fail when a new script contains a syntax error.');
+
+promise_test(async t => {
+ const [registration, expected_url] =
+ await prepare_ready_registration_with_mode(t, 'throw_install');
+ t.add_cleanup(() => registration.unregister());
+
+ await Promise.all([registration.update(), wait_for_update(t, registration)]);
+ assert_installing_and_active(registration, expected_url);
+}, 'update() should resolve when the install event throws.');
+
+promise_test(async t => {
+ const [registration, expected_url] =
+ await prepare_ready_registration_with_mode(t, 'normal');
+ t.add_cleanup(() => registration.unregister());
+
+ // We need to hold a client alive so that unregister() below doesn't remove
+ // the registration before update() has had a chance to look at the pending
+ // uninstall flag.
+ const frame = await with_iframe(SCOPE);
+ t.add_cleanup(() => frame.remove());
+
+ await promise_rejects_js(
+ t, TypeError,
+ Promise.all([registration.unregister(), registration.update()]));
+}, 'update() should fail when the pending uninstall flag is set.');
+
+promise_test(async t => {
+ const [registration, expected_url] =
+ await prepare_ready_registration_with_file(
+ t,
+ 'update-smaller-body-before-update-worker.js',
+ 'update-smaller-body-after-update-worker.js');
+ t.add_cleanup(() => registration.unregister());
+
+ await Promise.all([registration.update(), wait_for_update(t, registration)]);
+ assert_installing_and_active(registration, expected_url);
+
+ await wait_for_state(t, registration.installing, 'installed');
+ assert_waiting_and_active(registration, expected_url);
+
+ await wait_for_state(t, registration.waiting, 'activated');
+ assert_active_only(registration, expected_url);
+}, 'update() should succeed when the script shrinks.');
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/waiting.https.html b/test/wpt/tests/service-workers/service-worker/waiting.https.html
new file mode 100644
index 0000000..499e581
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/waiting.https.html
@@ -0,0 +1,47 @@
+<!DOCTYPE html>
+<title>ServiceWorker: navigator.serviceWorker.waiting</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+
+const SCRIPT = 'resources/empty-worker.js';
+const SCOPE = 'resources/blank.html';
+
+promise_test(async t => {
+
+ t.add_cleanup(async() => {
+ if (frame)
+ frame.remove();
+ if (registration)
+ await registration.unregister();
+ });
+
+ await service_worker_unregister(t, SCOPE);
+ const frame = await with_iframe(SCOPE);
+ const registration =
+ await navigator.serviceWorker.register(SCRIPT, {scope: SCOPE});
+ await wait_for_state(t, registration.installing, 'installed');
+ const controller = frame.contentWindow.navigator.serviceWorker.controller;
+ assert_equals(controller, null, 'controller');
+ assert_equals(registration.active, null, 'registration.active');
+ assert_equals(registration.waiting.state, 'installed',
+ 'registration.waiting');
+ assert_equals(registration.installing, null, 'registration.installing');
+}, 'waiting is set after installation');
+
+// Tests that the ServiceWorker objects returned from waiting attribute getter
+// that represent the same service worker are the same objects.
+promise_test(async t => {
+ const registration1 =
+ await service_worker_unregister_and_register(t, SCRIPT, SCOPE);
+ const registration2 = await navigator.serviceWorker.getRegistration(SCOPE);
+ assert_equals(registration1.waiting, registration2.waiting,
+ 'ServiceWorkerRegistration.waiting should return the same ' +
+ 'object');
+ await registration1.unregister();
+}, 'The ServiceWorker objects returned from waiting attribute getter that ' +
+ 'represent the same service worker are the same objects');
+</script>
+</body>
diff --git a/test/wpt/tests/service-workers/service-worker/websocket-in-service-worker.https.html b/test/wpt/tests/service-workers/service-worker/websocket-in-service-worker.https.html
new file mode 100644
index 0000000..cda9d6f
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/websocket-in-service-worker.https.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<title>Service Worker: WebSockets can be created in a Service Worker</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+promise_test(t => {
+ const SCRIPT = 'resources/websocket-worker.js?pipe=sub';
+ const SCOPE = 'resources/blank.html';
+ let registration;
+ return service_worker_unregister_and_register(t, SCRIPT, SCOPE)
+ .then(r => {
+ add_completion_callback(() => { r.unregister(); });
+ registration = r;
+ return wait_for_state(t, r.installing, 'activated');
+ })
+ .then(() => {
+ return new Promise(resolve => {
+ navigator.serviceWorker.onmessage = t.step_func(msg => {
+ assert_equals(msg.data, 'PASS');
+ resolve();
+ });
+ registration.active.postMessage({});
+ });
+ });
+ }, 'Verify WebSockets can be created in a Service Worker');
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/websocket.https.html b/test/wpt/tests/service-workers/service-worker/websocket.https.html
new file mode 100644
index 0000000..cbfed45
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/websocket.https.html
@@ -0,0 +1,45 @@
+<!DOCTYPE html>
+<title>Service Worker: WebSocket handshake channel is not intercepted</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js?pipe=sub"></script>
+<script>
+
+promise_test(function(t) {
+ var path = new URL(".", window.location).pathname
+ var url = 'resources/websocket.js';
+ var scope = 'resources/blank.html?websocket';
+ var host_info = get_host_info();
+ var frameURL = host_info['HTTPS_ORIGIN'] + path + scope;
+ var frame;
+
+ return service_worker_unregister_and_register(t, url, scope)
+ .then(function(registration) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() { return with_iframe(frameURL); })
+ .then(function(f) {
+ frame = f;
+ return websocket(t, frame);
+ })
+ .then(function() {
+ var channel = new MessageChannel();
+ return new Promise(function(resolve) {
+ channel.port1.onmessage = resolve;
+ frame.contentWindow.navigator.serviceWorker.controller.postMessage({port: channel.port2}, [channel.port2]);
+ });
+ })
+ .then(function(e) {
+ for (var url in e.data.urls) {
+ assert_equals(url.indexOf(get_websocket_url()), -1,
+ "Observed an unexpected FetchEvent for the WebSocket handshake");
+ }
+ frame.remove();
+ });
+ }, 'Verify WebSocket handshake channel does not get intercepted');
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/webvtt-cross-origin.https.html b/test/wpt/tests/service-workers/service-worker/webvtt-cross-origin.https.html
new file mode 100644
index 0000000..9394ff7
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/webvtt-cross-origin.https.html
@@ -0,0 +1,175 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>cross-origin webvtt returned by service worker is detected</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js?pipe=sub"></script>
+<body>
+<script>
+// This file tests responses for WebVTT text track from a service worker. It
+// creates an iframe with a <track> element, controlled by a service worker.
+// Each test tries to load a text track, the service worker intercepts the
+// requests and responds with opaque or non-opaque responses. As the
+// crossorigin attribute is not set, request's mode is always "same-origin",
+// and as specified in https://fetch.spec.whatwg.org/#http-fetch,
+// a response from a service worker whose type is neither "basic" nor
+// "default" is rejected.
+
+const host_info = get_host_info();
+const kScript = 'resources/fetch-rewrite-worker.js';
+// Add '?ignore' so the service worker falls back for the navigation.
+const kScope = 'resources/vtt-frame.html?ignore';
+let frame;
+
+function load_track(url) {
+ const track = frame.contentDocument.querySelector('track');
+ const result = new Promise((resolve, reject) => {
+ track.onload = (e => {
+ resolve('load event');
+ });
+ track.onerror = (e => {
+ resolve('error event');
+ });
+ });
+
+ track.src = url;
+ // Setting mode to hidden seems needed, or else the text track requests don't
+ // occur.
+ track.track.mode = 'hidden';
+ return result;
+}
+
+promise_test(t => {
+ return service_worker_unregister_and_register(t, kScript, kScope)
+ .then(registration => {
+ promise_test(() => {
+ frame.remove();
+ return registration.unregister();
+ }, 'restore global state');
+
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(() => {
+ return with_iframe(kScope);
+ })
+ .then(f => {
+ frame = f;
+ })
+ }, 'initialize global state');
+
+promise_test(t => {
+ let url = '/media/foo.vtt';
+ // Add '?url' and tell the service worker to fetch a same-origin URL.
+ url += '?url=' + host_info.HTTPS_ORIGIN + '/media/foo.vtt';
+ return load_track(url)
+ .then(result => {
+ assert_equals(result, 'load event');
+ });
+ }, 'same-origin text track should load');
+
+promise_test(t => {
+ let url = '/media/foo.vtt';
+ // Add '?url' and tell the service worker to fetch a cross-origin URL.
+ url += '?url=' + get_host_info().HTTPS_REMOTE_ORIGIN + '/media/foo.vtt';
+ return load_track(url)
+ .then(result => {
+ assert_equals(result, 'error event');
+ });
+ }, 'cross-origin text track with no-cors request should not load');
+
+promise_test(t => {
+ let url = '/media/foo.vtt';
+ // Add '?url' and tell the service worker to fetch a cross-origin URL that
+ // doesn't support CORS.
+ url += '?url=' + get_host_info().HTTPS_REMOTE_ORIGIN +
+ '/media/foo-no-cors.vtt';
+ // Add '&mode' to tell the service worker to do a CORS request.
+ url += '&mode=cors';
+ return load_track(url)
+ .then(result => {
+ assert_equals(result, 'error event');
+ });
+ }, 'cross-origin text track with rejected cors request should not load');
+
+promise_test(t => {
+ let url = '/media/foo.vtt';
+ // Add '?url' and tell the service worker to fetch a cross-origin URL.
+ url += '?url=' + get_host_info().HTTPS_REMOTE_ORIGIN + '/media/foo.vtt';
+ // Add '&mode' to tell the service worker to do a CORS request.
+ url += '&mode=cors';
+ // Add '&credentials=same-origin' to allow Access-Control-Allow-Origin=* so
+ // that CORS will succeed if the service approves it.
+ url += '&credentials=same-origin';
+ return load_track(url)
+ .then(result => {
+ assert_equals(result, 'error event');
+ });
+ }, 'cross-origin text track with approved cors request should not load');
+
+// Redirect tests.
+
+promise_test(t => {
+ let url = '/media/foo.vtt';
+ // Add '?url' and tell the service worker to fetch a same-origin URL that redirects...
+ redirector_url = host_info.HTTPS_ORIGIN + base_path() + 'resources/redirect.py?Redirect=';
+ // ... to a same-origin URL.
+ redirect_target = host_info.HTTPS_ORIGIN + '/media/foo.vtt';
+ url += '?url=' + encodeURIComponent(redirector_url + encodeURIComponent(redirect_target));
+ return load_track(url)
+ .then(result => {
+ assert_equals(result, 'load event');
+ });
+ }, 'same-origin text track that redirects same-origin should load');
+
+promise_test(t => {
+ let url = '/media/foo.vtt';
+ // Add '?url' and tell the service worker to fetch a same-origin URL that redirects...
+ redirector_url = host_info.HTTPS_ORIGIN + base_path() + 'resources/redirect.py?Redirect=';
+ // ... to a cross-origin URL.
+ redirect_target = host_info.HTTPS_REMOTE_ORIGIN + '/media/foo.vtt';
+ url += '?url=' + encodeURIComponent(redirector_url + encodeURIComponent(redirect_target));
+ return load_track(url)
+ .then(result => {
+ assert_equals(result, 'error event');
+ });
+ }, 'same-origin text track that redirects cross-origin should not load');
+
+
+promise_test(t => {
+ let url = '/media/foo.vtt';
+ // Add '?url' and tell the service worker to fetch a same-origin URL that redirects...
+ redirector_url = host_info.HTTPS_ORIGIN + base_path() + 'resources/redirect.py?Redirect=';
+ // ... to a cross-origin URL.
+ redirect_target = host_info.HTTPS_REMOTE_ORIGIN + '/media/foo-no-cors.vtt';
+ url += '?url=' + encodeURIComponent(redirector_url + encodeURIComponent(redirect_target));
+ // Add '&mode' to tell the service worker to do a CORS request.
+ url += '&mode=cors';
+ // Add '&credentials=same-origin' to allow Access-Control-Allow-Origin=* so
+ // that CORS will succeed if the server approves it.
+ url += '&credentials=same-origin';
+ return load_track(url)
+ .then(result => {
+ assert_equals(result, 'error event');
+ });
+ }, 'same-origin text track that redirects to a cross-origin text track with rejected cors should not load');
+
+promise_test(t => {
+ let url = '/media/foo.vtt';
+ // Add '?url' and tell the service worker to fetch a same-origin URL that redirects...
+ redirector_url = host_info.HTTPS_ORIGIN + base_path() + 'resources/redirect.py?Redirect=';
+ // ... to a cross-origin URL.
+ redirect_target = host_info.HTTPS_REMOTE_ORIGIN + '/media/foo.vtt';
+ url += '?url=' + encodeURIComponent(redirector_url + encodeURIComponent(redirect_target));
+ // Add '&mode' to tell the service worker to do a CORS request.
+ url += '&mode=cors';
+ // Add '&credentials=same-origin' to allow Access-Control-Allow-Origin=* so
+ // that CORS will succeed if the server approves it.
+ url += '&credentials=same-origin';
+ return load_track(url)
+ .then(result => {
+ assert_equals(result, 'error event');
+ });
+ }, 'same-origin text track that redirects to a cross-origin text track with approved cors should not load');
+</script>
+</body>
diff --git a/test/wpt/tests/service-workers/service-worker/windowclient-navigate.https.html b/test/wpt/tests/service-workers/service-worker/windowclient-navigate.https.html
new file mode 100644
index 0000000..ad60f78
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/windowclient-navigate.https.html
@@ -0,0 +1,190 @@
+<!DOCTYPE html>
+<title>Service Worker: WindowClient.navigate() tests</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+'use strict';
+
+const SCOPE = 'resources/blank.html';
+const SCRIPT_URL = 'resources/windowclient-navigate-worker.js';
+const CROSS_ORIGIN_URL =
+ get_host_info()['HTTPS_REMOTE_ORIGIN'] + base_path() + 'resources/blank.html';
+
+navigateTest({
+ description: 'normal',
+ destUrl: 'blank.html?navigate',
+ expected: normalizeURL(SCOPE) + '?navigate',
+});
+
+navigateTest({
+ description: 'blank url',
+ destUrl: '',
+ expected: normalizeURL(SCRIPT_URL)
+});
+
+navigateTest({
+ description: 'in scope but not controlled test on installing worker',
+ destUrl: 'blank.html?navigate',
+ expected: 'TypeError',
+ waitState: 'installing',
+});
+
+navigateTest({
+ description: 'in scope but not controlled test on active worker',
+ destUrl: 'blank.html?navigate',
+ expected: 'TypeError',
+ controlled: false,
+});
+
+navigateTest({
+ description: 'out of scope',
+ srcUrl: '/common/blank.html',
+ destUrl: 'blank.html?navigate',
+ expected: 'TypeError',
+});
+
+navigateTest({
+ description: 'cross orgin url',
+ destUrl: CROSS_ORIGIN_URL,
+ expected: null
+});
+
+navigateTest({
+ description: 'invalid url (http://[example.com])',
+ destUrl: 'http://[example].com',
+ expected: 'TypeError'
+});
+
+navigateTest({
+ description: 'invalid url (view-source://example.com)',
+ destUrl: 'view-source://example.com',
+ expected: 'TypeError'
+});
+
+navigateTest({
+ description: 'invalid url (file:///)',
+ destUrl: 'file:///',
+ expected: 'TypeError'
+});
+
+navigateTest({
+ description: 'invalid url (about:blank)',
+ destUrl: 'about:blank',
+ expected: 'TypeError'
+});
+
+navigateTest({
+ description: 'navigate on a top-level window client',
+ destUrl: 'blank.html?navigate',
+ srcUrl: 'resources/loaded.html',
+ scope: 'resources/loaded.html',
+ expected: normalizeURL(SCOPE) + '?navigate',
+ frameType: 'top-level'
+});
+
+async function createFrame(t, parameters) {
+ if (parameters.frameType === 'top-level') {
+ // Wait for window.open is completed.
+ await new Promise(resolve => {
+ const win = window.open(parameters.srcUrl);
+ t.add_cleanup(() => win.close());
+ window.addEventListener('message', (e) => {
+ if (e.data.type === 'LOADED') {
+ resolve();
+ }
+ });
+ });
+ }
+
+ if (parameters.frameType === 'nested') {
+ const frame = await with_iframe(parameters.srcUrl);
+ t.add_cleanup(() => frame.remove());
+ }
+}
+
+function navigateTest(overrideParameters) {
+ // default parameters
+ const parameters = {
+ description: null,
+ srcUrl: SCOPE,
+ destUrl: null,
+ expected: null,
+ waitState: 'activated',
+ scope: SCOPE,
+ controlled: true,
+ // `frameType` can be 'nested' for an iframe WindowClient or 'top-level' for
+ // a main frame WindowClient.
+ frameType: 'nested'
+ };
+
+ for (const key in overrideParameters)
+ parameters[key] = overrideParameters[key];
+
+ promise_test(async function(t) {
+ let pausedLifecyclePort;
+ let scriptUrl = SCRIPT_URL;
+
+ // For in-scope-but-not-controlled test on installing worker,
+ // if the waitState is "installing", then append the query to scriptUrl.
+ if (parameters.waitState === 'installing') {
+ scriptUrl += '?' + parameters.waitState;
+
+ navigator.serviceWorker.addEventListener('message', (event) => {
+ if (event.data.port) {
+ pausedLifecyclePort = event.data.port;
+ }
+ });
+ }
+
+ t.add_cleanup(() => {
+ // Some tests require that the worker remain in a given lifecycle phase.
+ // "Clean up" logic for these tests requires signaling the worker to
+ // release the hold; this allows the worker to be properly discarded
+ // prior to the execution of additional tests.
+ if (pausedLifecyclePort) {
+ // The value of the posted message is inconsequential. A string is
+ // specified here solely to aid in test debugging.
+ pausedLifecyclePort.postMessage('continue lifecycle');
+ }
+ });
+
+ // Create a frame that is not controlled by a service worker.
+ if (!parameters.controlled) {
+ await createFrame(t, parameters);
+ }
+
+ const registration = await service_worker_unregister_and_register(
+ t, scriptUrl, parameters.scope);
+ const serviceWorker = registration.installing;
+ await wait_for_state(t, serviceWorker, parameters.waitState);
+ t.add_cleanup(() => registration.unregister());
+
+ // Create a frame after a service worker is registered so that the frmae is
+ // controlled by an active service worker.
+ if (parameters.controlled) {
+ await createFrame(t, parameters);
+ }
+
+ const response = await new Promise(resolve => {
+ const channel = new MessageChannel();
+ channel.port1.onmessage = t.step_func(resolve);
+ serviceWorker.postMessage({
+ port: channel.port2,
+ url: parameters.destUrl,
+ clientUrl: new URL(parameters.srcUrl, location).toString(),
+ frameType: parameters.frameType,
+ expected: parameters.expected,
+ description: parameters.description,
+ }, [channel.port2]);
+ });
+
+ assert_equals(response.data, null);
+ await fetch_tests_from_worker(serviceWorker);
+ }, parameters.description);
+}
+</script>
+</body>
diff --git a/test/wpt/tests/service-workers/service-worker/worker-client-id.https.html b/test/wpt/tests/service-workers/service-worker/worker-client-id.https.html
new file mode 100644
index 0000000..4e4d316
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/worker-client-id.https.html
@@ -0,0 +1,58 @@
+<!DOCTYPE html>
+<title>Service Worker: Workers should have their own unique client Id</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+
+// Get the iframe client ID by calling postMessage() on its controlling
+// worker. This will cause the service worker to post back the
+// MessageEvent.source.id value.
+function getFrameClientId(frame) {
+ return new Promise(resolve => {
+ let mc = new MessageChannel();
+ frame.contentWindow.navigator.serviceWorker.controller.postMessage(
+ 'echo-client-id', [mc.port2]);
+ mc.port1.onmessage = evt => {
+ resolve(evt.data);
+ };
+ });
+}
+
+// Get the worker client ID by creating a worker that performs an intercepted
+// fetch(). The synthetic fetch() response will contain the FetchEvent.clientId
+// value. This is then posted back to here.
+function getWorkerClientId(frame) {
+ return new Promise(resolve => {
+ let w = new frame.contentWindow.Worker('worker-echo-client-id.js');
+ w.onmessage = evt => {
+ resolve(evt.data);
+ };
+ });
+}
+
+promise_test(async function(t) {
+ const script = './resources/worker-client-id-worker.js';
+ const scope = './resources/worker-client-id';
+ const frame = scope + '/frame.html';
+
+ let reg = await navigator.serviceWorker.register(script, { scope });
+ t.add_cleanup(async _ => await reg.unregister());
+ await wait_for_state(t, reg.installing, 'activated');
+
+ let f = await with_iframe(frame);
+ t.add_cleanup(_ => f.remove());
+
+ let frameClientId = await getFrameClientId(f);
+ assert_not_equals(frameClientId, null, 'frame client id should exist');
+
+ let workerClientId = await getWorkerClientId(f);
+ assert_not_equals(workerClientId, null, 'worker client id should exist');
+
+ assert_not_equals(frameClientId, workerClientId,
+ 'frame and worker client ids should be different');
+}, 'Verify workers have a unique client id separate from their owning documents window');
+
+</script>
+</body>
diff --git a/test/wpt/tests/service-workers/service-worker/worker-in-sandboxed-iframe-by-csp-fetch-event.https.html b/test/wpt/tests/service-workers/service-worker/worker-in-sandboxed-iframe-by-csp-fetch-event.https.html
new file mode 100644
index 0000000..c8480bf
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/worker-in-sandboxed-iframe-by-csp-fetch-event.https.html
@@ -0,0 +1,132 @@
+<!DOCTYPE html>
+<title>ServiceWorker FetchEvent issued from workers in an iframe sandboxed via CSP HTTP response header.</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+let lastCallbackId = 0;
+let callbacks = {};
+function doTest(frame, type) {
+ return new Promise(function(resolve) {
+ var id = ++lastCallbackId;
+ callbacks[id] = resolve;
+ frame.contentWindow.postMessage({id: id, type: type}, '*');
+ });
+}
+
+// Asks the service worker for data about requests and clients seen. The
+// worker posts a message back with |data| where:
+// |data.requests|: the requests the worker received FetchEvents for
+// |data.clients|: the URLs of all the worker's clients
+// The worker clears its data after responding.
+function getResultsFromWorker(worker) {
+ return new Promise(resolve => {
+ let channel = new MessageChannel();
+ channel.port1.onmessage = msg => {
+ resolve(msg.data);
+ };
+ worker.postMessage({port: channel.port2}, [channel.port2]);
+ });
+}
+
+window.onmessage = function (e) {
+ message = e.data;
+ let id = message['id'];
+ let callback = callbacks[id];
+ delete callbacks[id];
+ callback(message['result']);
+};
+
+const SCOPE = 'resources/sandboxed-iframe-fetch-event-iframe.py';
+const SCRIPT = 'resources/sandboxed-iframe-fetch-event-worker.js';
+const expected_base_url = new URL(SCOPE, location.href);
+// A service worker controlling |SCOPE|.
+let worker;
+// An iframe whose response header has
+// 'Content-Security-Policy: allow-scripts'.
+// This should NOT be controlled by a service worker.
+let sandboxed_frame_by_header;
+// An iframe whose response header has
+// 'Content-Security-Policy: allow-scripts allow-same-origin'.
+// This should be controlled by a service worker.
+let sandboxed_same_origin_frame_by_header;
+
+promise_test(t => {
+ return service_worker_unregister_and_register(t, SCRIPT, SCOPE)
+ .then(function(registration) {
+ add_completion_callback(() => registration.unregister());
+ worker = registration.installing;
+ return wait_for_state(t, registration.installing, 'activated');
+ });
+}, 'Prepare a service worker.');
+
+promise_test(t => {
+ const iframe_full_url = expected_base_url + '?sandbox=allow-scripts&' +
+ 'sandboxed-frame-by-header';
+ return with_iframe(iframe_full_url)
+ .then(f => {
+ sandboxed_frame_by_header = f;
+ add_completion_callback(() => f.remove());
+ return getResultsFromWorker(worker);
+ })
+ .then(data => {
+ let requests = data.requests;
+ assert_equals(requests.length, 1,
+ 'Service worker should provide the response');
+ assert_equals(requests[0], iframe_full_url);
+ assert_false(data.clients.includes(iframe_full_url),
+ 'Service worker should NOT control the sandboxed page');
+ });
+}, 'Prepare an iframe sandboxed by CSP HTTP header with allow-scripts.');
+
+promise_test(t => {
+ const iframe_full_url =
+ expected_base_url + '?sandbox=allow-scripts%20allow-same-origin&' +
+ 'sandboxed-iframe-same-origin-by-header';
+ return with_iframe(iframe_full_url)
+ .then(f => {
+ sandboxed_same_origin_frame_by_header = f;
+ add_completion_callback(() => f.remove());
+ return getResultsFromWorker(worker);
+ })
+ .then(data => {
+ let requests = data.requests;
+ assert_equals(requests.length, 1);
+ assert_equals(requests[0], iframe_full_url);
+ assert_true(data.clients.includes(iframe_full_url));
+ })
+}, 'Prepare an iframe sandboxed by CSP HTTP header with allow-scripts and ' +
+ 'allow-same-origin.');
+
+promise_test(t => {
+ let frame = sandboxed_frame_by_header;
+ return doTest(frame, 'fetch-from-worker')
+ .then(result => {
+ assert_equals(result, 'done');
+ return getResultsFromWorker(worker);
+ })
+ .then(data => {
+ assert_equals(data.requests.length, 0,
+ 'The request should NOT be handled by SW.');
+ });
+}, 'Fetch request from a worker in iframe sandboxed by CSP HTTP header ' +
+ 'allow-scripts flag');
+
+promise_test(t => {
+ let frame = sandboxed_same_origin_frame_by_header;
+ return doTest(frame, 'fetch-from-worker')
+ .then(result => {
+ assert_equals(result, 'done');
+ return getResultsFromWorker(worker);
+ })
+ .then(data => {
+ let requests = data.requests;
+ assert_equals(requests.length, 1,
+ 'The request should be handled by SW.');
+ assert_equals(requests[0], frame.src + '&test=fetch-from-worker');
+ });
+}, 'Fetch request from a worker in iframe sandboxed by CSP HTTP header ' +
+ 'with allow-scripts and allow-same-origin flag');
+</script>
+</body>
diff --git a/test/wpt/tests/service-workers/service-worker/worker-interception-redirect.https.html b/test/wpt/tests/service-workers/service-worker/worker-interception-redirect.https.html
new file mode 100644
index 0000000..8d566b9
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/worker-interception-redirect.https.html
@@ -0,0 +1,212 @@
+<!DOCTYPE html>
+<title>Service Worker: controlling Worker/SharedWorker</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+// This tests service worker interception for worker clients, when the request
+// for the worker script goes through redirects. For example, a request can go
+// through a chain of URLs like A -> B -> C -> D and each URL might fall in the
+// scope of a different service worker, if any.
+// The two key questions are:
+// 1. Upon a redirect from A -> B, should a service worker for scope B
+// intercept the request?
+// 2. After the final response, which service worker controls the resulting
+// client?
+//
+// The standard prescribes the following:
+// 1. The service worker for scope B intercepts the redirect. *However*, once a
+// request falls back to network (i.e., a service worker did not call
+// respondWith()) and a redirect is then received from network, no service
+// worker should intercept that redirect or any subsequent redirects.
+// 2. The final service worker that got a fetch event (or would have, in the
+// case of a non-fetch-event worker) becomes the controller of the client.
+//
+// The standard may change later, see:
+// https://github.com/w3c/ServiceWorker/issues/1289
+//
+// The basic test setup is:
+// 1. Page registers service workers for scope1 and scope2.
+// 2. Page requests a worker from scope1.
+// 3. The request is redirected to scope2 or out-of-scope.
+// 4. The worker posts message to the page describing where the final response
+// was served from (service worker or network).
+// 5. The worker does an importScripts() and fetch(), and posts back the
+// responses, which describe where the responses where served from.
+
+// Globals for easier cleanup.
+const scope1 = 'resources/scope1';
+const scope2 = 'resources/scope2';
+let frame;
+
+function get_message_from_worker(port) {
+ return new Promise(resolve => {
+ port.onmessage = evt => {
+ resolve(evt.data);
+ }
+ });
+}
+
+async function cleanup() {
+ if (frame)
+ frame.remove();
+
+ const reg1 = await navigator.serviceWorker.getRegistration(scope1);
+ if (reg1)
+ await reg1.unregister();
+ const reg2 = await navigator.serviceWorker.getRegistration(scope2);
+ if (reg2)
+ await reg2.unregister();
+}
+
+// Builds the worker script URL, which encodes information about where
+// to redirect to. The URL falls in sw1's scope.
+//
+// - |redirector| is "network" or "serviceworker". If "serviceworker", sw1 will
+// respondWith() a redirect. Otherwise, it falls back to network and the server
+// responds with a redirect.
+// - |redirect_location| is "scope2" or "out-of-scope". If "scope2", the
+// redirect ends up in sw2's scope2. Otherwise it's out of scope.
+function build_worker_url(redirector, redirect_location) {
+ let redirect_path;
+ // Set path to redirect.py, a file on the server that serves
+ // a redirect. When sw1 sees this URL, it falls back to network.
+ if (redirector == 'network')
+ redirector_path = 'redirect.py';
+ // Set path to 'sw-redirect', to tell the service worker
+ // to respond with redirect.
+ else if (redirector == 'serviceworker')
+ redirector_path = 'sw-redirect';
+
+ let redirect_to = base_path() + 'resources/';
+ // Append "scope2/" to redirect_to, so the redirect falls in scope2.
+ // Otherwise no change is needed, as the parent "resources/" directory is
+ // used, and is out-of-scope.
+ if (redirect_location == 'scope2')
+ redirect_to += 'scope2/';
+ // Append the name of the file which serves the worker script.
+ redirect_to += 'worker_interception_redirect_webworker.py';
+
+ return `scope1/${redirector_path}?Redirect=${redirect_to}`
+}
+
+promise_test(async t => {
+ await cleanup();
+ const service_worker = 'resources/worker-interception-redirect-serviceworker.js';
+ const registration1 = await navigator.serviceWorker.register(service_worker, {scope: scope1});
+ await wait_for_state(t, registration1.installing, 'activated');
+ const registration2 = await navigator.serviceWorker.register(service_worker, {scope: scope2});
+ await wait_for_state(t, registration2.installing, 'activated');
+
+ promise_test(t => {
+ return cleanup();
+ }, 'cleanup global state');
+}, 'initialize global state');
+
+async function worker_redirect_test(worker_request_url,
+ worker_expected_url,
+ expected_main_resource_message,
+ expected_import_scripts_message,
+ expected_fetch_message,
+ description) {
+ for (const workerType of ['DedicatedWorker', 'SharedWorker']) {
+ for (const type of ['classic', 'module']) {
+ promise_test(async t => {
+ // Create a frame to load the worker from. This way we can remove the
+ // frame to destroy the worker client when the test is done.
+ frame = await with_iframe('resources/blank.html');
+ t.add_cleanup(() => { frame.remove(); });
+
+ // Start the worker.
+ let w;
+ let port;
+ if (workerType === 'DedicatedWorker') {
+ w = new frame.contentWindow.Worker(worker_request_url, {type});
+ port = w;
+ } else {
+ w = new frame.contentWindow.SharedWorker(worker_request_url, {type});
+ port = w.port;
+ w.port.start();
+ }
+ w.onerror = t.unreached_func('Worker error');
+
+ // Expect a message from the worker indicating which service worker
+ // provided the response for the worker script request, if any.
+ const data = await get_message_from_worker(port);
+
+ // The worker does an importScripts(). Expect a message from the worker
+ // indicating which service worker provided the response for the
+ // importScripts(), if any.
+ const import_scripts_message = await get_message_from_worker(port);
+ test(() => {
+ if (type === 'classic') {
+ assert_equals(import_scripts_message,
+ expected_import_scripts_message);
+ } else {
+ assert_equals(import_scripts_message, 'importScripts failed');
+ }
+ }, `${description} (${type} ${workerType}, importScripts())`);
+
+ // The worker does a fetch(). Expect a message from the worker
+ // indicating which service worker provided the response for the
+ // fetch(), if any.
+ const fetch_message = await get_message_from_worker(port);
+ test(() => {
+ assert_equals(fetch_message, expected_fetch_message);
+ }, `${description} (${type} ${workerType}, fetch())`);
+
+ // Expect a message from the worker indicating |self.location|.
+ const worker_actual_url = await get_message_from_worker(port);
+ test(() => {
+ assert_equals(
+ worker_actual_url,
+ (new URL(worker_expected_url, location.href)).toString(),
+ 'location.href');
+ }, `${description} (${type} ${workerType}, location.href)`);
+
+ assert_equals(data, expected_main_resource_message);
+
+ }, `${description} (${type} ${workerType})`);
+ }
+ }
+}
+
+// request to sw1 scope gets network redirect to sw2 scope
+worker_redirect_test(
+ build_worker_url('network', 'scope2'),
+ 'resources/scope2/worker_interception_redirect_webworker.py',
+ 'the worker script was served from network',
+ 'sw1 saw importScripts from the worker: /service-workers/service-worker/resources/scope2/import-scripts-echo.py',
+ 'fetch(): sw1 saw the fetch from the worker: /service-workers/service-worker/resources/scope2/simple.txt',
+ 'Case #1: network scope1->scope2');
+
+// request to sw1 scope gets network redirect to out-of-scope
+worker_redirect_test(
+ build_worker_url('network', 'out-scope'),
+ 'resources/worker_interception_redirect_webworker.py',
+ 'the worker script was served from network',
+ 'sw1 saw importScripts from the worker: /service-workers/service-worker/resources/import-scripts-echo.py',
+ 'fetch(): sw1 saw the fetch from the worker: /service-workers/service-worker/resources/simple.txt',
+ 'Case #2: network scope1->out-scope');
+
+// request to sw1 scope gets service-worker redirect to sw2 scope
+worker_redirect_test(
+ build_worker_url('serviceworker', 'scope2'),
+ 'resources/subdir/worker_interception_redirect_webworker.py?greeting=sw2%20saw%20the%20request%20for%20the%20worker%20script',
+ 'sw2 saw the request for the worker script',
+ 'sw2 saw importScripts from the worker: /service-workers/service-worker/resources/subdir/import-scripts-echo.py',
+ 'fetch(): sw2 saw the fetch from the worker: /service-workers/service-worker/resources/subdir/simple.txt',
+ 'Case #3: sw scope1->scope2');
+
+// request to sw1 scope gets service-worker redirect to out-of-scope
+worker_redirect_test(
+ build_worker_url('serviceworker', 'out-scope'),
+ 'resources/worker_interception_redirect_webworker.py',
+ 'the worker script was served from network',
+ 'sw1 saw importScripts from the worker: /service-workers/service-worker/resources/import-scripts-echo.py',
+ 'fetch(): sw1 saw the fetch from the worker: /service-workers/service-worker/resources/simple.txt',
+ 'Case #4: sw scope1->out-scope');
+</script>
+</body>
diff --git a/test/wpt/tests/service-workers/service-worker/worker-interception.https.html b/test/wpt/tests/service-workers/service-worker/worker-interception.https.html
new file mode 100644
index 0000000..27983d8
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/worker-interception.https.html
@@ -0,0 +1,244 @@
+<!DOCTYPE html>
+<title>Service Worker: intercepting Worker script loads</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+
+// ========== Worker main resource interception tests ==========
+
+async function setup_service_worker(t, service_worker_url, scope) {
+ const r = await service_worker_unregister_and_register(
+ t, service_worker_url, scope);
+ t.add_cleanup(() => service_worker_unregister(t, scope));
+ await wait_for_state(t, r.installing, 'activated');
+ return r.active;
+}
+
+promise_test(async t => {
+ const worker_url = 'resources/sample-synthesized-worker.js?dedicated';
+ const service_worker_url = 'resources/sample-worker-interceptor.js';
+ const scope = worker_url;
+
+ const serviceWorker = await setup_service_worker(t, service_worker_url, scope);
+
+ const channels = new MessageChannel();
+ serviceWorker.postMessage({port: channels.port1}, [channels.port1]);
+
+ const clientId = await new Promise(resolve => channels.port2.onmessage = (e) => resolve(e.data.id));
+
+ const resultPromise = new Promise(resolve => channels.port2.onmessage = (e) => resolve(e.data));
+
+ const w = new Worker(worker_url);
+ const data = await new Promise((resolve, reject) => {
+ w.onmessage = e => resolve(e.data);
+ w.onerror = e => reject(e.message);
+ });
+ assert_equals(data, 'worker loading intercepted by service worker');
+
+ const results = await resultPromise;
+ assert_equals(results.clientId, clientId);
+ assert_true(!!results.resultingClientId.length);
+
+ channels.port2.postMessage("done");
+}, `Verify a dedicated worker script request gets correct client Ids`);
+
+promise_test(async t => {
+ const worker_url = 'resources/sample-synthesized-worker.js?dedicated';
+ const service_worker_url = 'resources/sample-worker-interceptor.js';
+ const scope = worker_url;
+
+ await setup_service_worker(t, service_worker_url, scope);
+ const w = new Worker(worker_url);
+ const data = await new Promise((resolve, reject) => {
+ w.onmessage = e => resolve(e.data);
+ w.onerror = e => reject(e.message);
+ });
+ assert_equals(data, 'worker loading intercepted by service worker');
+}, `Verify a dedicated worker script request issued from a uncontrolled ` +
+ `document is intercepted by worker's own service worker.`);
+
+promise_test(async t => {
+ const frame_url = 'resources/create-out-of-scope-worker.html';
+ const service_worker_url = 'resources/sample-worker-interceptor.js';
+ const scope = frame_url;
+
+ const registration = await service_worker_unregister_and_register(
+ t, service_worker_url, scope);
+ t.add_cleanup(() => service_worker_unregister(t, scope));
+ await wait_for_state(t, registration.installing, 'activated');
+
+ const frame = await with_iframe(frame_url);
+ t.add_cleanup(_ => frame.remove());
+
+ assert_equals(
+ frame.contentWindow.navigator.serviceWorker.controller.scriptURL,
+ get_newest_worker(registration).scriptURL,
+ 'the frame should be controlled by a service worker'
+ );
+
+ const result = await frame.contentWindow.getWorkerPromise();
+
+ assert_equals(result,
+ 'worker loading was not intercepted by service worker');
+}, `Verify an out-of-scope dedicated worker script request issued from a ` +
+ `controlled document should not be intercepted by document's service ` +
+ `worker.`);
+
+promise_test(async t => {
+ const worker_url = 'resources/sample-synthesized-worker.js?shared';
+ const service_worker_url = 'resources/sample-worker-interceptor.js';
+ const scope = worker_url;
+
+ await setup_service_worker(t, service_worker_url, scope);
+ const w = new SharedWorker(worker_url);
+ const data = await new Promise((resolve, reject) => {
+ w.port.onmessage = e => resolve(e.data);
+ w.onerror = e => reject(e.message);
+ });
+ assert_equals(data, 'worker loading intercepted by service worker');
+}, `Verify a shared worker script request issued from a uncontrolled ` +
+ `document is intercepted by worker's own service worker.`);
+
+promise_test(async t => {
+ const worker_url = 'resources/sample-same-origin-worker.js?dedicated';
+ const service_worker_url = 'resources/sample-worker-interceptor.js';
+ const scope = worker_url;
+
+ await setup_service_worker(t, service_worker_url, scope);
+ const w = new Worker(worker_url);
+ const data = await new Promise((resolve, reject) => {
+ w.onmessage = e => resolve(e.data);
+ w.onerror = e => reject(e.message);
+ });
+ assert_equals(data, 'dedicated worker script loaded');
+}, 'Verify a same-origin worker script served by a service worker succeeds ' +
+ 'in starting a dedicated worker.');
+
+promise_test(async t => {
+ const worker_url = 'resources/sample-same-origin-worker.js?shared';
+ const service_worker_url = 'resources/sample-worker-interceptor.js';
+ const scope = worker_url;
+
+ await setup_service_worker(t, service_worker_url, scope);
+ const w = new SharedWorker(worker_url);
+ const data = await new Promise((resolve, reject) => {
+ w.port.onmessage = e => resolve(e.data);
+ w.onerror = e => reject(e.message);
+ });
+ assert_equals(data, 'shared worker script loaded');
+}, 'Verify a same-origin worker script served by a service worker succeeds ' +
+ 'in starting a shared worker.');
+
+promise_test(async t => {
+ const worker_url = 'resources/sample-cors-worker.js?dedicated';
+ const service_worker_url = 'resources/sample-worker-interceptor.js';
+ const scope = worker_url;
+
+ await setup_service_worker(t, service_worker_url, scope);
+ const w = new Worker(worker_url);
+ const watcher = new EventWatcher(t, w, ['message', 'error']);
+ await watcher.wait_for('error');
+}, 'Verify a cors worker script served by a service worker fails dedicated ' +
+ 'worker start.');
+
+promise_test(async t => {
+ const worker_url = 'resources/sample-cors-worker.js?shared';
+ const service_worker_url = 'resources/sample-worker-interceptor.js';
+ const scope = worker_url;
+
+ await setup_service_worker(t, service_worker_url, scope);
+ const w = new SharedWorker(worker_url);
+ const watcher = new EventWatcher(t, w, ['message', 'error']);
+ await watcher.wait_for('error');
+}, 'Verify a cors worker script served by a service worker fails shared ' +
+ 'worker start.');
+
+promise_test(async t => {
+ const worker_url = 'resources/sample-no-cors-worker.js?dedicated';
+ const service_worker_url = 'resources/sample-worker-interceptor.js';
+ const scope = worker_url;
+
+ await setup_service_worker(t, service_worker_url, scope);
+ const w = new Worker(worker_url);
+ const watcher = new EventWatcher(t, w, ['message', 'error']);
+ await watcher.wait_for('error');
+}, 'Verify a no-cors cross-origin worker script served by a service worker ' +
+ 'fails dedicated worker start.');
+
+promise_test(async t => {
+ const worker_url = 'resources/sample-no-cors-worker.js?shared';
+ const service_worker_url = 'resources/sample-worker-interceptor.js';
+ const scope = worker_url;
+
+ await setup_service_worker(t, service_worker_url, scope);
+ const w = new SharedWorker(worker_url);
+ const watcher = new EventWatcher(t, w, ['message', 'error']);
+ await watcher.wait_for('error');
+}, 'Verify a no-cors cross-origin worker script served by a service worker ' +
+ 'fails shared worker start.');
+
+// ========== Worker subresource interception tests ==========
+
+const scope_for_subresource_interception = 'resources/load_worker.js';
+
+promise_test(async t => {
+ const service_worker_url = 'resources/worker-load-interceptor.js';
+ const r = await service_worker_unregister_and_register(
+ t, service_worker_url, scope_for_subresource_interception);
+ await wait_for_state(t, r.installing, 'activated');
+}, 'Register a service worker for worker subresource interception tests.');
+
+// Do not call this function multiple times without waiting for the promise
+// resolution because this sets new event handlers on |worker|.
+// TODO(nhiroki): To isolate multiple function calls, use MessagePort instead of
+// worker's onmessage event handler.
+async function request_on_worker(worker, resource_type) {
+ const data = await new Promise((resolve, reject) => {
+ if (worker instanceof Worker) {
+ worker.onmessage = e => resolve(e.data);
+ worker.onerror = e => reject(e);
+ worker.postMessage(resource_type);
+ } else if (worker instanceof SharedWorker) {
+ worker.port.onmessage = e => resolve(e.data);
+ worker.onerror = e => reject(e);
+ worker.port.postMessage(resource_type);
+ } else {
+ reject('Unexpected worker type!');
+ }
+ });
+ assert_equals(data, 'This load was successfully intercepted.');
+}
+
+async function subresource_test(worker) {
+ await request_on_worker(worker, 'xhr');
+ await request_on_worker(worker, 'fetch');
+ await request_on_worker(worker, 'importScripts');
+}
+
+promise_test(async t => {
+ await subresource_test(new Worker('resources/load_worker.js'));
+}, 'Requests on a dedicated worker controlled by a service worker.');
+
+promise_test(async t => {
+ await subresource_test(new SharedWorker('resources/load_worker.js'));
+}, 'Requests on a shared worker controlled by a service worker.');
+
+promise_test(async t => {
+ await subresource_test(new Worker('resources/nested_load_worker.js'));
+}, 'Requests on a dedicated worker nested in a dedicated worker and ' +
+ 'controlled by a service worker');
+
+promise_test(async t => {
+ await subresource_test(new SharedWorker('resources/nested_load_worker.js'));
+}, 'Requests on a dedicated worker nested in a shared worker and controlled ' +
+ 'by a service worker');
+
+promise_test(async t => {
+ await service_worker_unregister(t, scope_for_subresource_interception);
+}, 'Unregister a service worker for subresource interception tests.');
+
+</script>
+</body>
diff --git a/test/wpt/tests/service-workers/service-worker/xhr-content-length.https.window.js b/test/wpt/tests/service-workers/service-worker/xhr-content-length.https.window.js
new file mode 100644
index 0000000..1ae320e
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/xhr-content-length.https.window.js
@@ -0,0 +1,55 @@
+// META: script=resources/test-helpers.sub.js
+
+let frame;
+
+promise_test(async (t) => {
+ const scope = "resources/empty.html";
+ const script = "resources/xhr-content-length-worker.js";
+ const registration = await service_worker_unregister_and_register(t, script, scope);
+ await wait_for_state(t, registration.installing, "activated");
+ frame = await with_iframe(scope);
+}, "Setup");
+
+promise_test(async t => {
+ const xhr = new frame.contentWindow.XMLHttpRequest();
+ xhr.open("GET", "test?type=no-content-length");
+ xhr.send();
+ const event = await new Promise(resolve => xhr.onload = resolve);
+ assert_equals(xhr.getResponseHeader("content-length"), null);
+ assert_false(event.lengthComputable);
+ assert_equals(event.total, 0);
+ assert_equals(event.loaded, xhr.responseText.length);
+}, `Synthetic response without Content-Length header`);
+
+promise_test(async t => {
+ const xhr = new frame.contentWindow.XMLHttpRequest();
+ xhr.open("GET", "test?type=larger-content-length");
+ xhr.send();
+ const event = await new Promise(resolve => xhr.onload = resolve);
+ assert_equals(xhr.getResponseHeader("content-length"), "10000");
+ assert_true(event.lengthComputable);
+ assert_equals(event.total, 10000);
+ assert_equals(event.loaded, xhr.responseText.length);
+}, `Synthetic response with Content-Length header with value larger than response body length`);
+
+promise_test(async t => {
+ const xhr = new frame.contentWindow.XMLHttpRequest();
+ xhr.open("GET", "test?type=double-content-length");
+ xhr.send();
+ const event = await new Promise(resolve => xhr.onload = resolve);
+ assert_equals(xhr.getResponseHeader("content-length"), "10000, 10000");
+ assert_true(event.lengthComputable);
+ assert_equals(event.total, 10000);
+ assert_equals(event.loaded, xhr.responseText.length);
+}, `Synthetic response with two Content-Length headers value larger than response body length`);
+
+promise_test(async t => {
+ const xhr = new frame.contentWindow.XMLHttpRequest();
+ xhr.open("GET", "test?type=bogus-content-length");
+ xhr.send();
+ const event = await new Promise(resolve => xhr.onload = resolve);
+ assert_equals(xhr.getResponseHeader("content-length"), "test");
+ assert_false(event.lengthComputable);
+ assert_equals(event.total, 0);
+ assert_equals(event.loaded, xhr.responseText.length);
+}, `Synthetic response with bogus Content-Length header`);
diff --git a/test/wpt/tests/service-workers/service-worker/xhr-response-url.https.html b/test/wpt/tests/service-workers/service-worker/xhr-response-url.https.html
new file mode 100644
index 0000000..673ca52
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/xhr-response-url.https.html
@@ -0,0 +1,103 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Service Worker: XHR responseURL uses the response url</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+const scope = 'resources/xhr-iframe.html';
+const script = 'resources/xhr-response-url-worker.js';
+let iframe;
+
+function build_url(options) {
+ const url = new URL('test', window.location);
+ const opts = options ? options : {};
+ if (opts.respondWith)
+ url.searchParams.set('respondWith', opts.respondWith);
+ if (opts.url)
+ url.searchParams.set('url', opts.url.href);
+ return url.href;
+}
+
+promise_test(async (t) => {
+ const registration =
+ await service_worker_unregister_and_register(t, script, scope);
+ await wait_for_state(t, registration.installing, 'activated');
+ iframe = await with_iframe(scope);
+}, 'global setup');
+
+// Test that XMLHttpRequest.responseURL uses the response URL from the service
+// worker.
+promise_test(async (t) => {
+ // Build a URL that tells the service worker to respondWith(fetch(|target|)).
+ const target = new URL('resources/sample.txt', window.location);
+ const url = build_url({
+ respondWith: 'fetch',
+ url: target
+ });
+
+ // Perform the XHR.
+ const xhr = await iframe.contentWindow.xhr(url);
+ assert_equals(xhr.responseURL, target.href, 'responseURL');
+}, 'XHR responseURL should be the response URL');
+
+// Same as above with a generated response.
+promise_test(async (t) => {
+ // Build a URL that tells the service worker to respondWith(new Response()).
+ const url = build_url({respondWith: 'string'});
+
+ // Perform the XHR.
+ const xhr = await iframe.contentWindow.xhr(url);
+ assert_equals(xhr.responseURL, url, 'responseURL');
+}, 'XHR responseURL should be the response URL (generated response)');
+
+// Test that XMLHttpRequest.responseXML is a Document whose URL is the
+// response URL from the service worker.
+promise_test(async (t) => {
+ // Build a URL that tells the service worker to respondWith(fetch(|target|)).
+ const target = new URL('resources/blank.html', window.location);
+ const url = build_url({
+ respondWith: 'fetch',
+ url: target
+ });
+
+ // Perform the XHR.
+ const xhr = await iframe.contentWindow.xhr(url, {responseType: 'document'});
+ assert_equals(xhr.responseURL, target.href, 'responseURL');
+
+ // The document's URL uses the response URL:
+ // "Set |document|’s URL to |response|’s url."
+ // https://xhr.spec.whatwg.org/#document-response
+ assert_equals(xhr.responseXML.URL, target.href, 'responseXML.URL');
+
+ // The document's base URL falls back to the document URL:
+ // https://html.spec.whatwg.org/multipage/urls-and-fetching.html#document-base-url
+ assert_equals(xhr.responseXML.baseURI, target.href, 'responseXML.baseURI');
+}, 'XHR Document should use the response URL');
+
+// Same as above with a generated response from the service worker.
+promise_test(async (t) => {
+ // Build a URL that tells the service worker to
+ // respondWith(new Response()) with a document response.
+ const url = build_url({respondWith: 'document'});
+
+ // Perform the XHR.
+ const xhr = await iframe.contentWindow.xhr(url, {responseType: 'document'});
+ assert_equals(xhr.responseURL, url, 'responseURL');
+
+ // The document's URL uses the response URL, which is the request URL:
+ // "Set |document|’s URL to |response|’s url."
+ // https://xhr.spec.whatwg.org/#document-response
+ assert_equals(xhr.responseXML.URL, url, 'responseXML.URL');
+
+ // The document's base URL falls back to the document URL:
+ // https://html.spec.whatwg.org/multipage/urls-and-fetching.html#document-base-url
+ assert_equals(xhr.responseXML.baseURI, url, 'responseXML.baseURI');
+}, 'XHR Document should use the response URL (generated response)');
+
+promise_test(async (t) => {
+ if (iframe)
+ iframe.remove();
+ await service_worker_unregister(t, scope);
+}, 'global cleanup');
+</script>
diff --git a/test/wpt/tests/service-workers/service-worker/xsl-base-url.https.html b/test/wpt/tests/service-workers/service-worker/xsl-base-url.https.html
new file mode 100644
index 0000000..1d3c364
--- /dev/null
+++ b/test/wpt/tests/service-workers/service-worker/xsl-base-url.https.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Service Worker: XSL's base URL must be the response URL</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js?pipe=sub"></script>
+<script>
+// This test loads an XML document which is controlled a service worker. The
+// document loads a stylesheet and a service worker responds with another URL.
+// The stylesheet imports a relative URL to test that the base URL is the
+// response URL from the service worker.
+promise_test(async (t) => {
+ const SCOPE = 'resources/xsl-base-url-iframe.xml';
+ const SCRIPT = 'resources/xsl-base-url-worker.js';
+ let worker;
+ let frame;
+
+ t.add_cleanup(() => {
+ if (frame)
+ frame.remove();
+ service_worker_unregister(t, SCOPE);
+ });
+
+ const registration = await service_worker_unregister_and_register(
+ t, SCRIPT, SCOPE);
+ worker = registration.installing;
+ await wait_for_state(t, worker, 'activated');
+
+ frame = await with_iframe(SCOPE);
+ assert_equals(frame.contentDocument.body.textContent, 'PASS');
+}, 'base URL when service worker does respondWith(fetch(responseUrl))');
+</script>
diff --git a/test/wpt/tests/storage/META.yml b/test/wpt/tests/storage/META.yml
new file mode 100644
index 0000000..2aad1fb
--- /dev/null
+++ b/test/wpt/tests/storage/META.yml
@@ -0,0 +1,4 @@
+spec: https://storage.spec.whatwg.org/
+suggested_reviewers:
+ - annevk
+ - inexorabletash
diff --git a/test/wpt/tests/storage/README.md b/test/wpt/tests/storage/README.md
new file mode 100644
index 0000000..8d0dca3
--- /dev/null
+++ b/test/wpt/tests/storage/README.md
@@ -0,0 +1,7 @@
+This directory contains the Storage test suite.
+
+To run the tests in this test suite within a browser, go to: <https://wpt.live/storage/>.
+
+The living standard is: <https://storage.spec.whatwg.org/>
+
+The API is at: <https://storage.spec.whatwg.org/#api>
diff --git a/test/wpt/tests/storage/buckets/META.yml b/test/wpt/tests/storage/buckets/META.yml
new file mode 100644
index 0000000..4f21506
--- /dev/null
+++ b/test/wpt/tests/storage/buckets/META.yml
@@ -0,0 +1,5 @@
+spec: https://github.com/WICG/storage-buckets
+suggested_reviewers:
+ - ayui
+ - jsbell
+ - pwnall
diff --git a/test/wpt/tests/storage/buckets/bucket-quota-indexeddb.tentative.https.any.js b/test/wpt/tests/storage/buckets/bucket-quota-indexeddb.tentative.https.any.js
new file mode 100644
index 0000000..ba82edb
--- /dev/null
+++ b/test/wpt/tests/storage/buckets/bucket-quota-indexeddb.tentative.https.any.js
@@ -0,0 +1,35 @@
+// META: title=Bucket quota enforcement for indexeddb
+// META: script=/storage/buckets/resources/util.js
+
+promise_test(async t => {
+ const arraySize = 1e6;
+ const objectStoreName = "storageManager";
+ const dbname =
+ this.window ? window.location.pathname : 'estimate-worker.https.html';
+
+ let quota = arraySize / 2;
+ const bucket = await navigator.storageBuckets.open('idb', {quota});
+
+ await indexedDbDeleteRequest(bucket.indexedDB, dbname);
+
+ const db =
+ await indexedDbOpenRequest(t, bucket.indexedDB, dbname, (db_to_upgrade) => {
+ db_to_upgrade.createObjectStore(objectStoreName);
+ });
+
+ const txn = db.transaction(objectStoreName, 'readwrite');
+ const buffer = new ArrayBuffer(arraySize);
+ const view = new Uint8Array(buffer);
+
+ for (let i = 0; i < arraySize; i++) {
+ view[i] = Math.floor(Math.random() * 255);
+ }
+
+ const testBlob = new Blob([buffer], {type: 'binary/random'});
+ txn.objectStore(objectStoreName).add(testBlob, 1);
+
+ await promise_rejects_dom(
+ t, 'QuotaExceededError', transactionPromise(txn));
+
+ db.close();
+}, 'IDB respects bucket quota');
diff --git a/test/wpt/tests/storage/buckets/bucket-storage-policy.tentative.https.any.js b/test/wpt/tests/storage/buckets/bucket-storage-policy.tentative.https.any.js
new file mode 100644
index 0000000..d6dce36
--- /dev/null
+++ b/test/wpt/tests/storage/buckets/bucket-storage-policy.tentative.https.any.js
@@ -0,0 +1,21 @@
+// META: title=Buckets API: Tests for bucket storage policies.
+// META: script=/storage/buckets/resources/util.js
+// META: global=window,worker
+
+'use strict';
+
+promise_test(async testCase => {
+ await prepareForBucketTest(testCase);
+
+ await promise_rejects_js(
+ testCase, TypeError,
+ navigator.storageBuckets.open('negative', {quota: -1}));
+
+ await promise_rejects_js(
+ testCase, TypeError, navigator.storageBuckets.open('zero', {quota: 0}));
+
+ await promise_rejects_js(
+ testCase, TypeError,
+ navigator.storageBuckets.open(
+ 'above_max', {quota: Number.MAX_SAFE_INTEGER + 1}));
+}, 'The open promise should reject with a TypeError when quota is requested outside the range of 1 to Number.MAX_SAFE_INTEGER.');
diff --git a/test/wpt/tests/storage/buckets/resources/cached-resource.txt b/test/wpt/tests/storage/buckets/resources/cached-resource.txt
new file mode 100644
index 0000000..c57eff5
--- /dev/null
+++ b/test/wpt/tests/storage/buckets/resources/cached-resource.txt
@@ -0,0 +1 @@
+Hello World! \ No newline at end of file
diff --git a/test/wpt/tests/storage/buckets/resources/util.js b/test/wpt/tests/storage/buckets/resources/util.js
new file mode 100644
index 0000000..425303c
--- /dev/null
+++ b/test/wpt/tests/storage/buckets/resources/util.js
@@ -0,0 +1,57 @@
+'use strict';
+
+// Makes sure initial bucket state is as expected and to clean up after the test
+// is over (whether it passes or fails).
+async function prepareForBucketTest(test) {
+ // Verify initial state.
+ assert_equals('', (await navigator.storageBuckets.keys()).join());
+ // Clean up after test.
+ test.add_cleanup(async function() {
+ const keys = await navigator.storageBuckets.keys();
+ for (const key of keys) {
+ await navigator.storageBuckets.delete(key);
+ }
+ });
+}
+
+function indexedDbOpenRequest(t, idb, dbname, upgrade_func) {
+ return new Promise((resolve, reject) => {
+ const openRequest = idb.open(dbname);
+ t.add_cleanup(() => {
+ indexedDbDeleteRequest(idb, dbname);
+ });
+
+ openRequest.onerror = () => {
+ reject(openRequest.error);
+ };
+ openRequest.onsuccess = () => {
+ resolve(openRequest.result);
+ };
+ openRequest.onupgradeneeded = event => {
+ upgrade_func(openRequest.result);
+ };
+ });
+}
+
+function indexedDbDeleteRequest(idb, name) {
+ return new Promise((resolve, reject) => {
+ const deleteRequest = idb.deleteDatabase(name);
+ deleteRequest.onerror = () => {
+ reject(deleteRequest.error);
+ };
+ deleteRequest.onsuccess = () => {
+ resolve();
+ };
+ });
+}
+
+function transactionPromise(txn) {
+ return new Promise((resolve, reject) => {
+ txn.onabort = () => {
+ reject(txn.error);
+ };
+ txn.oncomplete = () => {
+ resolve();
+ };
+ });
+}
diff --git a/test/wpt/tests/storage/estimate-indexeddb.https.any.js b/test/wpt/tests/storage/estimate-indexeddb.https.any.js
new file mode 100644
index 0000000..f0b82b9
--- /dev/null
+++ b/test/wpt/tests/storage/estimate-indexeddb.https.any.js
@@ -0,0 +1,61 @@
+// META: title=StorageManager: estimate() for indexeddb
+// META: script=/storage/buckets/resources/util.js
+
+test(t => {
+ assert_true('estimate' in navigator.storage);
+ assert_equals(typeof navigator.storage.estimate, 'function');
+ assert_true(navigator.storage.estimate() instanceof Promise);
+}, 'estimate() method exists and returns a Promise');
+
+promise_test(async t => {
+ const estimate = await navigator.storage.estimate();
+ assert_equals(typeof estimate, 'object');
+ assert_true('usage' in estimate);
+ assert_equals(typeof estimate.usage, 'number');
+ assert_true('quota' in estimate);
+ assert_equals(typeof estimate.quota, 'number');
+}, 'estimate() resolves to dictionary with members');
+
+promise_test(async t => {
+ const arraySize = 1e6;
+ const objectStoreName = "storageManager";
+ const dbname =
+ this.window ? window.location.pathname : 'estimate-worker.https.html';
+
+ await indexedDbDeleteRequest(indexedDB, dbname);
+ let estimate = await navigator.storage.estimate();
+
+ const usageBeforeCreate = estimate.usage;
+ const db =
+ await indexedDbOpenRequest(t, indexedDB, dbname, (db_to_upgrade) => {
+ db_to_upgrade.createObjectStore(objectStoreName);
+ });
+
+ estimate = await navigator.storage.estimate();
+ const usageAfterCreate = estimate.usage;
+
+ assert_greater_than(
+ usageAfterCreate, usageBeforeCreate,
+ 'estimated usage should increase after object store is created');
+
+ const txn = db.transaction(objectStoreName, 'readwrite');
+ const buffer = new ArrayBuffer(arraySize);
+ const view = new Uint8Array(buffer);
+
+ for (let i = 0; i < arraySize; i++) {
+ view[i] = Math.floor(Math.random() * 255);
+ }
+
+ const testBlob = new Blob([buffer], {type: 'binary/random'});
+ txn.objectStore(objectStoreName).add(testBlob, 1);
+
+ await transactionPromise(txn);
+
+ estimate = await navigator.storage.estimate();
+ const usageAfterPut = estimate.usage;
+ assert_greater_than(
+ usageAfterPut, usageAfterCreate,
+ 'estimated usage should increase after large value is stored');
+
+ db.close();
+}, 'estimate() shows usage increase after large value is stored');
diff --git a/test/wpt/tests/storage/estimate-parallel.https.any.js b/test/wpt/tests/storage/estimate-parallel.https.any.js
new file mode 100644
index 0000000..090f004
--- /dev/null
+++ b/test/wpt/tests/storage/estimate-parallel.https.any.js
@@ -0,0 +1,13 @@
+// META: title=StorageManager: multiple estimate() calls in parallel
+
+promise_test(async t => {
+ let r1, r2;
+ await Promise.all([
+ navigator.storage.estimate().then(r => { r1 = r; }),
+ navigator.storage.estimate().then(r => { r2 = r; })
+ ]);
+ assert_true(('usage' in r1) && ('quota' in r1),
+ 'first response should have expected fields');
+ assert_true(('usage' in r2) && ('quota' in r2),
+ 'second response should have expected fields');
+}, 'Multiple estimate() calls in parallel should complete');
diff --git a/test/wpt/tests/storage/estimate-usage-details-caches.https.tentative.any.js b/test/wpt/tests/storage/estimate-usage-details-caches.https.tentative.any.js
new file mode 100644
index 0000000..bf889f8
--- /dev/null
+++ b/test/wpt/tests/storage/estimate-usage-details-caches.https.tentative.any.js
@@ -0,0 +1,20 @@
+// META: title=StorageManager: estimate() for caches
+
+promise_test(async t => {
+ let estimate = await navigator.storage.estimate();
+
+ const cachesUsageBeforeCreate = estimate.usageDetails.caches || 0;
+
+ const cacheName = 'testCache';
+ const cache = await caches.open(cacheName);
+ t.add_cleanup(() => caches.delete(cacheName));
+
+ await cache.put('/test.json', new Response('x'.repeat(1024*1024)));
+
+ estimate = await navigator.storage.estimate();
+ assert_true('caches' in estimate.usageDetails);
+ const cachesUsageAfterPut = estimate.usageDetails.caches;
+ assert_greater_than(
+ cachesUsageAfterPut, cachesUsageBeforeCreate,
+ 'estimated usage should increase after value is stored');
+}, 'estimate() shows usage increase after large value is stored');
diff --git a/test/wpt/tests/storage/estimate-usage-details-indexeddb.https.tentative.any.js b/test/wpt/tests/storage/estimate-usage-details-indexeddb.https.tentative.any.js
new file mode 100644
index 0000000..551cede
--- /dev/null
+++ b/test/wpt/tests/storage/estimate-usage-details-indexeddb.https.tentative.any.js
@@ -0,0 +1,59 @@
+// META: title=StorageManager: estimate() usage details for indexeddb
+// META: script=helpers.js
+// META: script=../IndexedDB/resources/support-promises.js
+
+promise_test(async t => {
+ const estimate = await navigator.storage.estimate()
+ assert_equals(typeof estimate.usageDetails, 'object');
+}, 'estimate() resolves to dictionary with usageDetails member');
+
+promise_test(async t => {
+ // We use 100KB here because db compaction usually happens every few MB
+ // 100KB is large enough to avoid a false positive (small amounts of metadata
+ // getting written for some random reason), and small enough to avoid
+ // compaction with a reasonably high probability.
+ const writeSize = 1024 * 100;
+ const objectStoreName = 'store';
+ const dbname = self.location.pathname;
+
+ await indexedDB.deleteDatabase(dbname);
+ let usageAfterWrite, usageBeforeWrite;
+ // TODO(crbug.com/906867): Refactor this test to better deal with db/log
+ // compaction flakiness
+ // The for loop here is to help make this test less flaky. The reason it is
+ // flaky is that database and/or log compaction could happen in the middle of
+ // this loop. The problem is that this test runs in a large batch of tests,
+ // and previous tests might have created a lot of garbage which could trigger
+ // compaction. Suppose the initial estimate shows 1MB usage before creating
+ // the db. Compaction could happen after this step and before we measure
+ // usage at the end, meaning the 1MB could be wiped to 0, an extra 1024 * 100
+ // is put in, and the actual increase in usage does not reach our expected
+ // increase. Loop 10 times here to be safe (and reduce the number of bot
+ // builds that fail); all it takes is one iteration without compaction for
+ // this to pass.
+ for (let i = 0; i < 10; i++) {
+ const db = await createDB(dbname, objectStoreName, t);
+ let estimate = await navigator.storage.estimate();
+
+ // If usage is 0, usageDetails does not include the usage (undefined)
+ usageBeforeWrite = estimate.usageDetails.indexedDB || 0;
+
+ const txn = db.transaction(objectStoreName, 'readwrite');
+ const valueToStore = largeValue(writeSize, Math.random() * 255);
+ txn.objectStore(objectStoreName).add(valueToStore, 1);
+
+ await transactionPromise(txn);
+
+ estimate = await navigator.storage.estimate();
+ usageAfterWrite = estimate.usageDetails.indexedDB;
+ db.close();
+
+ if (usageAfterWrite - usageBeforeWrite >= writeSize) {
+ break;
+ }
+ }
+
+ assert_greater_than_equal(usageAfterWrite - usageBeforeWrite,
+ writeSize);
+}, 'estimate() usage details reflects increase in indexedDB after large ' +
+ 'value is stored');
diff --git a/test/wpt/tests/storage/estimate-usage-details-service-workers.https.tentative.window.js b/test/wpt/tests/storage/estimate-usage-details-service-workers.https.tentative.window.js
new file mode 100644
index 0000000..cf3a2aa
--- /dev/null
+++ b/test/wpt/tests/storage/estimate-usage-details-service-workers.https.tentative.window.js
@@ -0,0 +1,38 @@
+// META: title=StorageManager: estimate() for service worker registrations
+const wait_for_active = worker => new Promise(resolve =>{
+ if (worker.active) { resolve(worker.active); }
+
+ const listen_for_active = worker => e => {
+ if (e.target.state === 'activated') { resolve(worker.active); }
+ }
+
+ if (worker.waiting) {
+ worker.waiting
+ .addEventListener('statechange', listen_for_active(worker.waiting));
+ }
+ if (worker.installing) {
+ worker.installing
+ .addEventListener('statechange', listen_for_active(worker.installing));
+ }
+});
+
+promise_test(async t => {
+ let estimate = await navigator.storage.estimate();
+ const usageBeforeCreate = estimate.usageDetails.serviceWorkerRegistrations ||
+ 0;
+ // Note: helpers.js is an arbitrary file; it could be any file that
+ // exists, but this test does not depend on the contents of said file.
+ const serviceWorkerRegistration = await
+ navigator.serviceWorker.register('./helpers.js');
+
+ t.add_cleanup(() => serviceWorkerRegistration.unregister());
+ await wait_for_active(serviceWorkerRegistration);
+
+ estimate = await navigator.storage.estimate();
+ assert_true('serviceWorkerRegistrations' in estimate.usageDetails);
+
+ const usageAfterCreate = estimate.usageDetails.serviceWorkerRegistrations;
+ assert_greater_than(
+ usageAfterCreate, usageBeforeCreate,
+ 'estimated usage should increase after service worker is registered');
+}, 'estimate() shows usage increase after large value is stored');
diff --git a/test/wpt/tests/storage/estimate-usage-details.https.tentative.any.js b/test/wpt/tests/storage/estimate-usage-details.https.tentative.any.js
new file mode 100644
index 0000000..2a1cea5
--- /dev/null
+++ b/test/wpt/tests/storage/estimate-usage-details.https.tentative.any.js
@@ -0,0 +1,12 @@
+// META: title=StorageManager: estimate() should have usage details
+
+promise_test(async t => {
+ const estimate = await navigator.storage.estimate();
+ assert_equals(typeof estimate, 'object');
+ assert_true('usage' in estimate);
+ assert_equals(typeof estimate.usage, 'number');
+ assert_true('quota' in estimate);
+ assert_equals(typeof estimate.quota, 'number');
+ assert_true('usageDetails' in estimate);
+ assert_equals(typeof estimate.usageDetails, 'object');
+}, 'estimate() resolves to dictionary with members, including usageDetails');
diff --git a/test/wpt/tests/storage/helpers.js b/test/wpt/tests/storage/helpers.js
new file mode 100644
index 0000000..b524c1b
--- /dev/null
+++ b/test/wpt/tests/storage/helpers.js
@@ -0,0 +1,46 @@
+/**
+ * @description - Function will create a database with the supplied name
+ * and also create an object store with the specified name.
+ * If a db with the name dbName exists, this will raze the
+ * existing DB beforehand.
+ * @param {string} dbName
+ * @param {string} objectStoreName
+ * @param {testCase} t
+ * @returns {Promise} - A promise that resolves to an indexedDB open request
+ */
+function createDB(dbName, objectStoreName, t) {
+ return new Promise((resolve, reject) => {
+ const openRequest = indexedDB.open(dbName);
+ t.add_cleanup(() => {
+ indexedDB.deleteDatabase(dbName);
+ });
+ openRequest.onerror = () => {
+ reject(openRequest.error);
+ };
+ openRequest.onsuccess = () => {
+ resolve(openRequest.result);
+ };
+ openRequest.onupgradeneeded = (event) => {
+ openRequest.result.createObjectStore(objectStoreName);
+ };
+ });
+}
+
+/**
+ * @description - This function will wrap an IDBTransaction in a promise,
+ * resolving in the oncomplete() method and rejecting with the
+ * transaction error in the onabort() case.
+ * @param {IDBTransaction} transaction - The transaction to wrap in a promise.
+ * @returns {Promise} - A promise that resolves when the transaction is either
+ * aborted or completed.
+ */
+function transactionPromise(transaction) {
+ return new Promise((resolve, reject) => {
+ transaction.onabort = () => {
+ reject(transaction.error);
+ };
+ transaction.oncomplete = () => {
+ resolve();
+ };
+ });
+}
diff --git a/test/wpt/tests/storage/idlharness.https.any.js b/test/wpt/tests/storage/idlharness.https.any.js
new file mode 100644
index 0000000..773fac4
--- /dev/null
+++ b/test/wpt/tests/storage/idlharness.https.any.js
@@ -0,0 +1,18 @@
+// META: script=/resources/WebIDLParser.js
+// META: script=/resources/idlharness.js
+// META: timeout=long
+
+'use strict';
+
+idl_test(
+ ['storage'],
+ ['html'],
+ idl_array => {
+ idl_array.add_objects({ StorageManager: ['navigator.storage'] });
+ if (self.Window) {
+ idl_array.add_objects({ Navigator: ['navigator'] });
+ } else {
+ idl_array.add_objects({ WorkerNavigator: ['navigator'] });
+ }
+ }
+);
diff --git a/test/wpt/tests/storage/opaque-origin.https.window.js b/test/wpt/tests/storage/opaque-origin.https.window.js
new file mode 100644
index 0000000..cc1d31f
--- /dev/null
+++ b/test/wpt/tests/storage/opaque-origin.https.window.js
@@ -0,0 +1,80 @@
+// META: title=StorageManager API and opaque origins
+
+function load_iframe(src, sandbox) {
+ return new Promise(resolve => {
+ const iframe = document.createElement('iframe');
+ iframe.onload = () => { resolve(iframe); };
+ if (sandbox)
+ iframe.sandbox = sandbox;
+ iframe.srcdoc = src;
+ iframe.style.display = 'none';
+ document.documentElement.appendChild(iframe);
+ });
+}
+
+function wait_for_message(iframe) {
+ return new Promise(resolve => {
+ self.addEventListener('message', function listener(e) {
+ if (e.source === iframe.contentWindow && "result" in e.data) {
+ resolve(e.data);
+ self.removeEventListener('message', listener);
+ }
+ });
+ });
+}
+
+function make_script(snippet) {
+ return '<script src="/resources/testharness.js"></script>' +
+ '<script>' +
+ ' window.onmessage = () => {' +
+ ' try {' +
+ ' (' + snippet + ')' +
+ ' .then(' +
+ ' result => {' +
+ ' window.parent.postMessage({result: "no rejection"}, "*");' +
+ ' }, ' +
+ ' error => {' +
+ ' try {' +
+ ' assert_throws_js(TypeError, () => { throw error; });' +
+ ' window.parent.postMessage({result: "correct rejection"}, "*");' +
+ ' } catch (e) {' +
+ ' window.parent.postMessage({result: "incorrect rejection"}, "*");' +
+ ' }' +
+ ' });' +
+ ' } catch (ex) {' +
+ // Report if not implemented/exposed, rather than time out.
+ ' window.parent.postMessage({result: "API access threw"}, "*");' +
+ ' }' +
+ ' };' +
+ '<\/script>';
+}
+
+['navigator.storage.persisted()',
+ 'navigator.storage.estimate()',
+ // persist() can prompt, so make sure we test that last
+ 'navigator.storage.persist()',
+].forEach(snippet => {
+ promise_test(t => {
+ return load_iframe(make_script(snippet))
+ .then(iframe => {
+ iframe.contentWindow.postMessage({}, '*');
+ return wait_for_message(iframe);
+ })
+ .then(message => {
+ assert_equals(message.result, 'no rejection',
+ `${snippet} should not reject`);
+ });
+ }, `${snippet} in non-sandboxed iframe should not reject`);
+
+ promise_test(t => {
+ return load_iframe(make_script(snippet), 'allow-scripts')
+ .then(iframe => {
+ iframe.contentWindow.postMessage({}, '*');
+ return wait_for_message(iframe);
+ })
+ .then(message => {
+ assert_equals(message.result, 'correct rejection',
+ `${snippet} should reject with TypeError`);
+ });
+ }, `${snippet} in sandboxed iframe should reject with TypeError`);
+});
diff --git a/test/wpt/tests/storage/partitioned-estimate-usage-details-caches.tentative.https.sub.html b/test/wpt/tests/storage/partitioned-estimate-usage-details-caches.tentative.https.sub.html
new file mode 100644
index 0000000..dc2af7c
--- /dev/null
+++ b/test/wpt/tests/storage/partitioned-estimate-usage-details-caches.tentative.https.sub.html
@@ -0,0 +1,74 @@
+<!DOCTYPE html>
+<meta name=help href="https://privacycg.github.io/storage-partitioning/">
+<title>Partitioned estimate() usage details for caches test</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+
+<body>
+ <script>
+ const usageDetails = async () =>
+ (await navigator.storage.estimate()).usageDetails.caches || 0;
+
+ const createSomeUsage = async () => {
+ const cache_name = token();
+ const cache_url = `/foo-${cache_name}`;
+ const cache = await caches.open(cache_name);
+ await cache.put(cache_url, new Response('x'.repeat(128)));
+ return [cache, cache_url];
+ }
+
+ const testPath = () => location.pathname.split("/").slice(0, -1).join("/");
+
+ let alt_origin = "https://{{hosts[alt][]}}:{{ports[https][0]}}";
+ let details = {};
+
+ const iframe = document.createElement("iframe");
+ iframe.src = `https://{{host}}:{{ports[https][0]}}${testPath()}` +
+ `/resources/partitioned-estimate-usage-details-caches-helper-frame.html`
+ document.body.appendChild(iframe);
+
+
+ async_test(test => {
+ if (location.origin === alt_origin)
+ return;
+
+
+ let cache, cache_url;
+ window.addEventListener("message", test.step_func(async event => {
+ if (event.data === "iframe-is-ready") {
+ details.init = await usageDetails();
+ [cache, cache_url] = await createSomeUsage(test);
+ details.after = await usageDetails();
+ assert_greater_than(details.after, details.init);
+
+ iframe.contentWindow.postMessage("get-details", iframe.origin);
+ }
+ }));
+
+ window.addEventListener("message", test.step_func(event => {
+ if (event.data.source === "same-site") {
+ details.same_site = event.data;
+
+ const cross_site_window = window
+ .open(`${alt_origin}${location.pathname}`, "", "noopener=false");
+
+ test.add_cleanup(() => cross_site_window.close());
+ }
+ if (event.data.source === "cross-site") {
+ details.cross_site = event.data;
+
+ // Some cleanup
+ test.step(async () => await cache.delete(cache_url));
+
+ test.step(() => {
+ assert_true(details.cross_site.init == 0, "Usage should be 0.");
+ assert_equals(details.same_site.init, details.after);
+ });
+
+ test.done();
+ }
+ }));
+ }, "Partitioned estimate() usage details for caches test.");
+ </script>
+</body>
diff --git a/test/wpt/tests/storage/partitioned-estimate-usage-details-indexeddb.tentative.https.sub.html b/test/wpt/tests/storage/partitioned-estimate-usage-details-indexeddb.tentative.https.sub.html
new file mode 100644
index 0000000..98a1ed8
--- /dev/null
+++ b/test/wpt/tests/storage/partitioned-estimate-usage-details-indexeddb.tentative.https.sub.html
@@ -0,0 +1,84 @@
+<!DOCTYPE html>
+<meta name=help href="https://privacycg.github.io/storage-partitioning/">
+<title>Partitioned estimate() usage details for indexeddb test</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="./helpers.js"></script>
+<script src="../IndexedDB/resources/support-promises.js"></script>
+
+<body>
+ <script>
+ const usageDetails = async () =>
+ (await navigator.storage.estimate()).usageDetails.indexedDB || 0;
+
+ const createSomeUsage = async (test) => {
+ // We use 100KB here because db compaction usually happens every few MB
+ // 100KB is large enough to avoid a false positive (small amounts of
+ // metadata getting written for some random reason), and small enough to
+ // avoid compaction with a reasonably high probability.
+ const write_size = 1024 * 100;
+ const object_store_name = token();
+ const db_name = self.location.pathname;
+
+ await indexedDB.deleteDatabase(db_name);
+ const db = await createDB(db_name, object_store_name, test);
+ const transaction = db.transaction(object_store_name, 'readwrite');
+ const value_to_store = largeValue(write_size, Math.random() * 255);
+ transaction.objectStore(object_store_name).add(value_to_store, 1);
+
+ await transactionPromise(transaction);
+ return db;
+ }
+
+ const testPath = () => location.pathname.split("/").slice(0, -1).join("/");
+
+ let alt_origin = "https://{{hosts[alt][]}}:{{ports[https][0]}}";
+ let details = {};
+
+ const iframe = document.createElement("iframe");
+ iframe.src = `https://{{host}}:{{ports[https][0]}}${testPath()}/resources` +
+ `/partitioned-estimate-usage-details-indexeddb-helper-frame.html`;
+ document.body.appendChild(iframe);
+
+ async_test(test => {
+ if (location.origin === alt_origin)
+ return;
+
+ let db;
+ window.addEventListener("message", test.step_func(async event => {
+ if (event.data === "iframe-is-ready") {
+ details.init = await usageDetails();
+ db = await createSomeUsage(test);
+ details.after = await usageDetails();
+ assert_greater_than(details.after, details.init);
+
+ iframe.contentWindow.postMessage("get-details", iframe.origin);
+ }
+ }));
+
+ window.addEventListener("message", test.step_func(event => {
+ if (event.data.source === "same-site") {
+ details.same_site = event.data;
+
+ const cross_site_window = window
+ .open(`${alt_origin}${location.pathname}`, "", "noopener=false");
+ test.add_cleanup(() => cross_site_window.close());
+ }
+ if (event.data.source === "cross-site") {
+ details.cross_site = event.data;
+
+ // Some cleanup
+ test.step(async () => await db.close());
+
+ test.step(() => {
+ assert_true(details.cross_site.init == 0, "Usage should be 0.");
+ assert_equals(details.same_site.init, details.after);
+ });
+
+ test.done();
+ }
+ }));
+ }, "Partitioned estimate() usage details for indexeddb test.");
+ </script>
+</body>
diff --git a/test/wpt/tests/storage/partitioned-estimate-usage-details-service-workers.tentative.https.sub.html b/test/wpt/tests/storage/partitioned-estimate-usage-details-service-workers.tentative.https.sub.html
new file mode 100644
index 0000000..c52ca34
--- /dev/null
+++ b/test/wpt/tests/storage/partitioned-estimate-usage-details-service-workers.tentative.https.sub.html
@@ -0,0 +1,88 @@
+<!DOCTYPE html>
+<meta name=help href="https://privacycg.github.io/storage-partitioning/">
+<title>Partitioned estimate() usage details for service workers test</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<body>
+ <script>
+ const usageDetails = async () => {
+ return (await navigator.storage.estimate())
+ .usageDetails.serviceWorkerRegistrations || 0;
+ }
+
+ const createSomeUsage = async () => {
+ const wait_for_active = worker => new Promise(resolve => {
+ if (worker.active) { resolve(worker.active); }
+
+ const listen_for_active = worker => e => {
+ if (e.target.state === 'activated') { resolve(worker.active); }
+ }
+
+ if (worker.waiting) {
+ worker.waiting
+ .addEventListener('statechange', listen_for_active(worker.waiting));
+ }
+ if (worker.installing) {
+ worker.installing.addEventListener('statechange',
+ listen_for_active(worker.installing));
+ }
+ });
+
+ const service_worker_registration =
+ await navigator.serviceWorker.register('resources/worker.js');
+ await wait_for_active(service_worker_registration);
+ return service_worker_registration;
+ }
+
+ const testPath = () => location.pathname.split("/").slice(0, -1).join("/");
+
+ let alt_origin = "https://{{hosts[alt][]}}:{{ports[https][0]}}";
+ let details = {};
+
+ const iframe = document.createElement("iframe");
+ iframe.src = `https://{{host}}:{{ports[https][0]}}${testPath()}/resources` +
+ `/partitioned-estimate-usage-details-service-workers-helper-frame.html`
+ document.body.appendChild(iframe);
+
+ async_test(test => {
+ if (location.origin === alt_origin)
+ return;
+
+ let service_worker_registration;
+ window.addEventListener("message", test.step_func(async event => {
+ if (event.data === "iframe-is-ready") {
+ details.init = await usageDetails();
+ service_worker_registration = await createSomeUsage();
+ details.after = await usageDetails();
+ assert_greater_than(details.after, details.init);
+
+ iframe.contentWindow.postMessage("get-details", iframe.origin);
+ }
+ }));
+
+ window.addEventListener("message", test.step_func(event => {
+ if (event.data.source === "same-site") {
+ details.same_site = event.data;
+
+ const cross_site_window = window
+ .open(`${alt_origin}${location.pathname}`, "", "noopener=false");
+ test.add_cleanup(() => cross_site_window.close());
+ }
+ if (event.data.source === "cross-site") {
+ details.cross_site = event.data;
+
+ // More cleanup.
+ test.step(() => service_worker_registration.unregister());
+
+ test.step(() => {
+ assert_true(details.cross_site.init == 0, "Usage should be 0.");
+ assert_equals(details.same_site.init, details.after);
+ });
+
+ test.done();
+ }
+ }));
+ }, "Partitioned estimate() usage details for service workers test.");
+ </script>
+</body>
diff --git a/test/wpt/tests/storage/permission-query.https.any.js b/test/wpt/tests/storage/permission-query.https.any.js
new file mode 100644
index 0000000..9984bda
--- /dev/null
+++ b/test/wpt/tests/storage/permission-query.https.any.js
@@ -0,0 +1,10 @@
+// META: title=The Permission API registration for "persistent-storage"
+
+promise_test(async t => {
+ const status =
+ await navigator.permissions.query({name: 'persistent-storage'});
+ assert_equals(status.constructor, PermissionStatus,
+ 'query() result should resolve to a PermissionStatus');
+ assert_true(['granted','denied', 'prompt'].includes(status.state),
+ 'state should be a PermissionState');
+}, 'The "persistent-storage" permission is recognized');
diff --git a/test/wpt/tests/storage/persist-permission-manual.https.html b/test/wpt/tests/storage/persist-permission-manual.https.html
new file mode 100644
index 0000000..aa49900
--- /dev/null
+++ b/test/wpt/tests/storage/persist-permission-manual.https.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>StorageManager: permission state is granted</title>
+ <p>Clear all persistent storage permissions before running this test.</p>
+ <p>Test passes if there is a permission prompt and click allow store persistent data</p>
+ <meta name="help" href="https://storage.spec.whatwg.org/#dom-storagemanager-persist">
+ <meta name="author" title="Mozilla" href="https://www.mozilla.org">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ </head>
+ <body>
+ <script>
+ promise_test(function(t) {
+ return navigator.storage.persist()
+ .then(function(result) {
+ assert_true(result);
+ return navigator.storage.persisted();
+ })
+ .then(function(result) {
+ assert_true(result);
+ })
+ }, 'Expect permission state is granted after calling persist()');
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/storage/persisted.https.any.js b/test/wpt/tests/storage/persisted.https.any.js
new file mode 100644
index 0000000..57e15f0
--- /dev/null
+++ b/test/wpt/tests/storage/persisted.https.any.js
@@ -0,0 +1,14 @@
+// META: title=StorageManager: persisted()
+
+test(function(t) {
+ assert_true('persisted' in navigator.storage);
+ assert_equals(typeof navigator.storage.persisted, 'function');
+ assert_true(navigator.storage.persisted() instanceof Promise);
+}, 'persisted() method exists and returns a Promise');
+
+promise_test(function(t) {
+ return navigator.storage.persisted().then(function(result) {
+ assert_equals(typeof result, 'boolean');
+ assert_equals(result, false);
+ });
+}, 'persisted() returns a promise and resolves as boolean with false');
diff --git a/test/wpt/tests/storage/quotachange-in-detached-iframe.tentative.https.html b/test/wpt/tests/storage/quotachange-in-detached-iframe.tentative.https.html
new file mode 100644
index 0000000..123af50
--- /dev/null
+++ b/test/wpt/tests/storage/quotachange-in-detached-iframe.tentative.https.html
@@ -0,0 +1,21 @@
+<!doctype html>
+<meta charset="utf-8">
+<title>quotachange event on DOMWindow of detached iframe</title>
+<link rel="author" href="jarrydg@chromium.org" title="Jarryd">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<iframe id="iframe"></iframe>
+<script>
+'use strict';
+
+test(t => {
+ const iframe = document.getElementById('iframe');
+ const frameWindow = iframe.contentWindow;
+ const storageManager = frameWindow.navigator.storage;
+
+ iframe.parentNode.removeChild(iframe);
+ const emptyListener = () => {};
+ storageManager.addEventListener('quotachange', emptyListener);
+ storageManager.removeEventListener('quotachange', emptyListener);
+}, "Add quotachange listener on detached iframe.");
+</script>
diff --git a/test/wpt/tests/storage/resources/partitioned-estimate-usage-details-caches-helper-frame.html b/test/wpt/tests/storage/resources/partitioned-estimate-usage-details-caches-helper-frame.html
new file mode 100644
index 0000000..0679c1d
--- /dev/null
+++ b/test/wpt/tests/storage/resources/partitioned-estimate-usage-details-caches-helper-frame.html
@@ -0,0 +1,30 @@
+<!-- ToDo: Change the virtual suite expected file content once the necessary
+ partitioning implementation is completed -->
+<!DOCTYPE html>
+<meta name=help href="https://privacycg.github.io/storage-partitioning/">
+<title>Helper frame</title>
+
+<script>
+ const usageDetails = async () =>
+ (await navigator.storage.estimate()).usageDetails.caches || 0;
+
+ let details = {};
+
+ window.addEventListener("message", async event => {
+ if (event.data === "get-details") {
+ details.source = "same-site";
+ details.init = await usageDetails();
+ event.source.postMessage(details, event.source.origin);
+ }
+ });
+
+ window.addEventListener("load", async () => {
+ if (parent.opener) {
+ details.source = "cross-site";
+ details.init = await usageDetails();
+ parent.opener.postMessage(details, parent.opener.origin);
+ }
+ });
+
+ window.parent.postMessage("iframe-is-ready", window.parent.origin);
+</script>
diff --git a/test/wpt/tests/storage/resources/partitioned-estimate-usage-details-indexeddb-helper-frame.html b/test/wpt/tests/storage/resources/partitioned-estimate-usage-details-indexeddb-helper-frame.html
new file mode 100644
index 0000000..fd2cfb6
--- /dev/null
+++ b/test/wpt/tests/storage/resources/partitioned-estimate-usage-details-indexeddb-helper-frame.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<meta name=help href="https://privacycg.github.io/storage-partitioning/">
+<title>Helper frame</title>
+
+<script>
+ const usageDetails = async () =>
+ (await navigator.storage.estimate()).usageDetails.indexedDB || 0;
+
+ let details = {};
+
+ window.addEventListener("message", async event => {
+ if (event.data === "get-details") {
+ details.source = "same-site";
+ details.init = await usageDetails();
+ event.source.postMessage(details, event.source.origin);
+ }
+ });
+
+ window.addEventListener("load", async () => {
+ if (parent.opener) {
+ details.source = "cross-site";
+ details.init = await usageDetails();
+ parent.opener.postMessage(details, parent.opener.origin);
+ }
+ });
+
+ window.parent.postMessage("iframe-is-ready", window.parent.origin);
+</script>
diff --git a/test/wpt/tests/storage/resources/partitioned-estimate-usage-details-service-workers-helper-frame.html b/test/wpt/tests/storage/resources/partitioned-estimate-usage-details-service-workers-helper-frame.html
new file mode 100644
index 0000000..25d7554
--- /dev/null
+++ b/test/wpt/tests/storage/resources/partitioned-estimate-usage-details-service-workers-helper-frame.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<meta name=help href="https://privacycg.github.io/storage-partitioning/">
+<title>Helper frame</title>
+
+<script>
+ const usageDetails = async () => {
+ return (await navigator.storage.estimate())
+ .usageDetails.serviceWorkerRegistrations || 0
+ }
+
+ let details = {};
+
+ window.addEventListener("message", async event => {
+ if (event.data === "get-details") {
+ details.source = "same-site";
+ details.init = await usageDetails();
+ event.source.postMessage(details, event.source.origin);
+ }
+ });
+
+ window.addEventListener("load", async () => {
+ if (parent.opener) {
+ details.source = "cross-site";
+ details.init = await usageDetails();
+ parent.opener.postMessage(details, parent.opener.origin);
+ }
+ });
+
+ window.parent.postMessage("iframe-is-ready", window.parent.origin);
+</script>
diff --git a/test/wpt/tests/storage/resources/worker.js b/test/wpt/tests/storage/resources/worker.js
new file mode 100644
index 0000000..9271c76
--- /dev/null
+++ b/test/wpt/tests/storage/resources/worker.js
@@ -0,0 +1,3 @@
+// Dummy service worker to observe some weight when querying the storage usage
+// details from of the service worker from estimate().
+globalThis.oninstall = e => {};
diff --git a/test/wpt/tests/storage/storagemanager-estimate.https.any.js b/test/wpt/tests/storage/storagemanager-estimate.https.any.js
new file mode 100644
index 0000000..c2f5c56
--- /dev/null
+++ b/test/wpt/tests/storage/storagemanager-estimate.https.any.js
@@ -0,0 +1,60 @@
+// META: title=StorageManager: estimate()
+
+test(function(t) {
+ assert_true(navigator.storage.estimate() instanceof Promise);
+}, 'estimate() method returns a Promise');
+
+promise_test(function(t) {
+ return navigator.storage.estimate().then(function(result) {
+ assert_equals(typeof result, 'object');
+ assert_true('usage' in result);
+ assert_equals(typeof result.usage, 'number');
+ assert_true('quota' in result);
+ assert_equals(typeof result.quota, 'number');
+ });
+}, 'estimate() resolves to dictionary with members');
+
+promise_test(function(t) {
+ const large_value = new Uint8Array(1e6);
+ const dbname = `db-${location}-${t.name}`;
+ let db, before, after;
+
+ indexedDB.deleteDatabase(dbname);
+ return new Promise((resolve, reject) => {
+ const open = indexedDB.open(dbname);
+ open.onerror = () => { reject(open.error); };
+ open.onupgradeneeded = () => {
+ const connection = open.result;
+ connection.createObjectStore('store');
+ };
+ open.onsuccess = () => {
+ const connection = open.result;
+ t.add_cleanup(() => {
+ connection.close();
+ indexedDB.deleteDatabase(dbname);
+ });
+ resolve(connection);
+ };
+ })
+ .then(connection => {
+ db = connection;
+ return navigator.storage.estimate();
+ })
+ .then(estimate => {
+ before = estimate.usage;
+ return new Promise((resolve, reject) => {
+ const tx = db.transaction('store', 'readwrite');
+ tx.objectStore('store').put(large_value, 'key');
+ tx.onabort = () => { reject(tx.error); };
+ tx.oncomplete = () => { resolve(); };
+ });
+ })
+ .then(() => {
+ return navigator.storage.estimate();
+ })
+ .then(estimate => {
+ after = estimate.usage;
+ assert_greater_than(after, before,
+ 'estimated usage should increase');
+ });
+}, 'estimate() shows usage increase after 1MB IndexedDB record is stored');
diff --git a/test/wpt/tests/storage/storagemanager-persist-persisted-match.https.any.js b/test/wpt/tests/storage/storagemanager-persist-persisted-match.https.any.js
new file mode 100644
index 0000000..edbe67f
--- /dev/null
+++ b/test/wpt/tests/storage/storagemanager-persist-persisted-match.https.any.js
@@ -0,0 +1,9 @@
+// META: title=StorageManager: result of persist() matches result of persisted()
+
+promise_test(async t => {
+ var persistResult = await navigator.storage.persist();
+ assert_equals(typeof persistResult, 'boolean', persistResult + ' should be boolean');
+ var persistedResult = await navigator.storage.persisted();
+ assert_equals(typeof persistedResult, 'boolean', persistedResult + ' should be boolean');
+ assert_equals(persistResult, persistedResult);
+}, 'navigator.storage.persist() resolves to a value that matches navigator.storage.persisted()');
diff --git a/test/wpt/tests/storage/storagemanager-persist.https.window.js b/test/wpt/tests/storage/storagemanager-persist.https.window.js
new file mode 100644
index 0000000..13e17a1
--- /dev/null
+++ b/test/wpt/tests/storage/storagemanager-persist.https.window.js
@@ -0,0 +1,10 @@
+// META: title=StorageManager: persist()
+
+promise_test(function() {
+ var promise = navigator.storage.persist();
+ assert_true(promise instanceof Promise,
+ 'navigator.storage.persist() returned a Promise.');
+ return promise.then(function(result) {
+ assert_equals(typeof result, 'boolean', result + ' should be boolean');
+ });
+}, 'navigator.storage.persist() returns a promise that resolves.');
diff --git a/test/wpt/tests/storage/storagemanager-persist.https.worker.js b/test/wpt/tests/storage/storagemanager-persist.https.worker.js
new file mode 100644
index 0000000..fcf8175
--- /dev/null
+++ b/test/wpt/tests/storage/storagemanager-persist.https.worker.js
@@ -0,0 +1,8 @@
+// META: title=StorageManager: persist() (worker)
+importScripts("/resources/testharness.js");
+
+test(function() {
+ assert_false('persist' in navigator.storage);
+}, 'navigator.storage.persist should not exist in workers');
+
+done();
diff --git a/test/wpt/tests/storage/storagemanager-persisted.https.any.js b/test/wpt/tests/storage/storagemanager-persisted.https.any.js
new file mode 100644
index 0000000..7099940
--- /dev/null
+++ b/test/wpt/tests/storage/storagemanager-persisted.https.any.js
@@ -0,0 +1,10 @@
+// META: title=StorageManager: persisted()
+
+promise_test(function() {
+ var promise = navigator.storage.persisted();
+ assert_true(promise instanceof Promise,
+ 'navigator.storage.persisted() returned a Promise.');
+ return promise.then(function (result) {
+ assert_equals(typeof result, 'boolean', result + ' should be boolean');
+ });
+}, 'navigator.storage.persisted() returns a promise that resolves.');
diff --git a/test/wpt/tests/websockets/Close-1000-reason.any.js b/test/wpt/tests/websockets/Close-1000-reason.any.js
new file mode 100644
index 0000000..79ad6a0
--- /dev/null
+++ b/test/wpt/tests/websockets/Close-1000-reason.any.js
@@ -0,0 +1,21 @@
+// META: script=constants.sub.js
+// META: variant=?default
+// META: variant=?wss
+// META: variant=?wpt_flags=h2
+
+var test = async_test("Create WebSocket - Close the Connection - close(1000, reason) - readyState should be in CLOSED state and wasClean is TRUE - Connection should be closed");
+
+var wsocket = CreateWebSocket(false, false);
+var isOpenCalled = false;
+
+wsocket.addEventListener('open', test.step_func(function(evt) {
+ wsocket.close(1000, "Clean Close");
+ isOpenCalled = true;
+}), true);
+
+wsocket.addEventListener('close', test.step_func(function(evt) {
+ assert_true(isOpenCalled, "WebSocket connection should be opened");
+ assert_equals(wsocket.readyState, 3, "readyState should be 3(CLOSED)");
+ assert_equals(evt.wasClean, true, "wasClean should be TRUE");
+ test.done();
+}), true);
diff --git a/test/wpt/tests/websockets/Close-1000-verify-code.any.js b/test/wpt/tests/websockets/Close-1000-verify-code.any.js
new file mode 100644
index 0000000..c3a9274
--- /dev/null
+++ b/test/wpt/tests/websockets/Close-1000-verify-code.any.js
@@ -0,0 +1,21 @@
+// META: script=constants.sub.js
+// META: variant=?default
+// META: variant=?wss
+// META: variant=?wpt_flags=h2
+
+var test = async_test("Create WebSocket - Close the Connection - close(1000, reason) - event.code == 1000 and event.reason = 'Clean Close'");
+
+var wsocket = CreateWebSocket(false, false);
+var isOpenCalled = false;
+
+wsocket.addEventListener('open', test.step_func(function(evt) {
+ wsocket.close(1000, "Clean Close");
+ isOpenCalled = true;
+}), true);
+
+wsocket.addEventListener('close', test.step_func(function(evt) {
+ assert_true(isOpenCalled, "WebSocket connection should be open");
+ assert_equals(evt.code, 1000, "CloseEvent.code should be 1000");
+ assert_equals(evt.reason, "Clean Close", "CloseEvent.reason should be the same as the reason sent in close");
+ test.done();
+}), true);
diff --git a/test/wpt/tests/websockets/Close-1000.any.js b/test/wpt/tests/websockets/Close-1000.any.js
new file mode 100644
index 0000000..2f535ba
--- /dev/null
+++ b/test/wpt/tests/websockets/Close-1000.any.js
@@ -0,0 +1,21 @@
+// META: script=constants.sub.js
+// META: variant=?default
+// META: variant=?wss
+// META: variant=?wpt_flags=h2
+
+var test = async_test("Create WebSocket - Close the Connection - close(1000) - readyState should be in CLOSED state and wasClean is TRUE - Connection should be closed");
+
+var wsocket = CreateWebSocket(false, false);
+var isOpenCalled = false;
+
+wsocket.addEventListener('open', test.step_func(function(evt) {
+ wsocket.close(1000);
+ isOpenCalled = true;
+}), true);
+
+wsocket.addEventListener('close', test.step_func(function(evt) {
+ assert_true(isOpenCalled, "WebSocket connection should be opened");
+ assert_equals(wsocket.readyState, 3, "readyState should be 3(CLOSED)");
+ assert_equals(evt.wasClean, true, "wasClean should be TRUE");
+ test.done();
+}), true);
diff --git a/test/wpt/tests/websockets/Close-1005-verify-code.any.js b/test/wpt/tests/websockets/Close-1005-verify-code.any.js
new file mode 100644
index 0000000..28f84c8
--- /dev/null
+++ b/test/wpt/tests/websockets/Close-1005-verify-code.any.js
@@ -0,0 +1,21 @@
+// META: script=constants.sub.js
+// META: variant=?default
+// META: variant=?wss
+// META: variant=?wpt_flags=h2
+
+var test = async_test("Create WebSocket - Close the Connection - close() - return close code is 1005 - Connection should be closed");
+
+var wsocket = CreateWebSocket(false, false);
+var isOpenCalled = false;
+
+wsocket.addEventListener('open', test.step_func(function(evt) {
+ wsocket.close();
+ isOpenCalled = true;
+}), true);
+
+wsocket.addEventListener('close', test.step_func(function(evt) {
+ assert_true(isOpenCalled, "WebSocket connection should be open");
+ assert_equals(evt.code, 1005, "CloseEvent.code should be 1005");
+ assert_equals(evt.reason, "", "CloseEvent.reason should be empty");
+ test.done();
+}), true);
diff --git a/test/wpt/tests/websockets/Close-1005.any.js b/test/wpt/tests/websockets/Close-1005.any.js
new file mode 100644
index 0000000..5055e28
--- /dev/null
+++ b/test/wpt/tests/websockets/Close-1005.any.js
@@ -0,0 +1,18 @@
+// META: script=constants.sub.js
+// META: variant=?default
+// META: variant=?wss
+// META: variant=?wpt_flags=h2
+
+var test = async_test("Create WebSocket - Close the Connection - close(1005) - see '7.1.5. The WebSocket Connection Close Code' in http://www.ietf.org/rfc/rfc6455.txt");
+
+var wsocket = CreateWebSocket(false, false);
+var isOpenCalled = false;
+
+wsocket.addEventListener('open', test.step_func(function(evt) {
+ assert_throws_dom("INVALID_ACCESS_ERR", function() {
+ wsocket.close(1005, "1005 - reserved code")
+ });
+ test.done();
+}), true);
+
+wsocket.addEventListener('close', test.unreached_func('close event should not fire'), true);
diff --git a/test/wpt/tests/websockets/Close-2999-reason.any.js b/test/wpt/tests/websockets/Close-2999-reason.any.js
new file mode 100644
index 0000000..6336c7d
--- /dev/null
+++ b/test/wpt/tests/websockets/Close-2999-reason.any.js
@@ -0,0 +1,17 @@
+// META: script=constants.sub.js
+// META: variant=?default
+// META: variant=?wss
+// META: variant=?wpt_flags=h2
+
+var test = async_test("Create WebSocket - Close the Connection - close(2999, reason) - INVALID_ACCESS_ERR is thrown");
+
+var wsocket = CreateWebSocket(false, false);
+
+wsocket.addEventListener('open', test.step_func(function(evt) {
+ assert_throws_dom("INVALID_ACCESS_ERR", function() {
+ wsocket.close(2999, "Close not in range 3000-4999")
+ });
+ test.done();
+}), true);
+
+wsocket.addEventListener('close', test.unreached_func('close event should not fire'), true);
diff --git a/test/wpt/tests/websockets/Close-3000-reason.any.js b/test/wpt/tests/websockets/Close-3000-reason.any.js
new file mode 100644
index 0000000..8e34ce7
--- /dev/null
+++ b/test/wpt/tests/websockets/Close-3000-reason.any.js
@@ -0,0 +1,21 @@
+// META: script=constants.sub.js
+// META: variant=?default
+// META: variant=?wss
+// META: variant=?wpt_flags=h2
+
+var test = async_test("Create WebSocket - Close the Connection - close(3000, reason) - readyState should be in CLOSED state and wasClean is TRUE - Connection should be closed");
+
+var wsocket = CreateWebSocket(false, false);
+var isOpenCalled = false;
+
+wsocket.addEventListener('open', test.step_func(function(evt) {
+ wsocket.close(3000, "Clean Close with code - 3000");
+ isOpenCalled = true;
+}), true);
+
+wsocket.addEventListener('close', test.step_func(function(evt) {
+ assert_true(isOpenCalled, "WebSocket connection should be open");
+ assert_equals(wsocket.readyState, 3, "readyState should be 3(CLOSED)");
+ assert_equals(evt.wasClean, true, "wasClean should be TRUE");
+ test.done();
+}), true);
diff --git a/test/wpt/tests/websockets/Close-3000-verify-code.any.js b/test/wpt/tests/websockets/Close-3000-verify-code.any.js
new file mode 100644
index 0000000..a6703de
--- /dev/null
+++ b/test/wpt/tests/websockets/Close-3000-verify-code.any.js
@@ -0,0 +1,20 @@
+// META: script=constants.sub.js
+// META: variant=?default
+// META: variant=?wss
+// META: variant=?wpt_flags=h2
+
+var test = async_test("Create WebSocket - Close the Connection - close(3000, reason) - verify return code is 3000 - Connection should be closed");
+
+var wsocket = CreateWebSocket(false, false);
+var isOpenCalled = false;
+
+wsocket.addEventListener('open', test.step_func(function(evt) {
+ wsocket.close(3000, "Clean Close");
+ isOpenCalled = true;
+}), true);
+
+wsocket.addEventListener('close', test.step_func(function(evt) {
+ assert_true(isOpenCalled, "WebSocket connection should be open");
+ assert_equals(evt.code, 3000, "CloseEvent.code should be 3000");
+ test.done();
+}), true);
diff --git a/test/wpt/tests/websockets/Close-4999-reason.any.js b/test/wpt/tests/websockets/Close-4999-reason.any.js
new file mode 100644
index 0000000..8c2a1c9
--- /dev/null
+++ b/test/wpt/tests/websockets/Close-4999-reason.any.js
@@ -0,0 +1,21 @@
+// META: script=constants.sub.js
+// META: variant=?default
+// META: variant=?wss
+// META: variant=?wpt_flags=h2
+
+var test = async_test("Create WebSocket - Close the Connection - close(4999, reason) - readyState should be in CLOSED state and wasClean is TRUE - Connection should be closed");
+
+var wsocket = CreateWebSocket(false, false);
+var isOpenCalled = false;
+
+wsocket.addEventListener('open', test.step_func(function(evt) {
+ wsocket.close(3000, "Clean Close with code - 4999");
+ isOpenCalled = true;
+}), true);
+
+wsocket.addEventListener('close', test.step_func(function(evt) {
+ assert_true(isOpenCalled, "WebSocket connection should be open");
+ assert_equals(wsocket.readyState, 3, "readyState should be 3(CLOSED)");
+ assert_equals(evt.wasClean, true, "wasClean should be TRUE");
+ test.done();
+}), true);
diff --git a/test/wpt/tests/websockets/Close-Reason-124Bytes.any.js b/test/wpt/tests/websockets/Close-Reason-124Bytes.any.js
new file mode 100644
index 0000000..063b12b
--- /dev/null
+++ b/test/wpt/tests/websockets/Close-Reason-124Bytes.any.js
@@ -0,0 +1,20 @@
+// META: script=constants.sub.js
+// META: variant=?default
+// META: variant=?wpt_flags=h2
+// META: variant=?wss
+
+var test = async_test("Create WebSocket - Close the Connection - close(code, 'reason more than 123 bytes') - SYNTAX_ERR is thrown");
+
+var wsocket = CreateWebSocket(false, false);
+var isOpenCalled = false;
+
+wsocket.addEventListener('open', test.step_func(function(evt) {
+ var reason = "0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123";
+ assert_equals(reason.length, 124);
+ assert_throws_dom("SYNTAX_ERR", function() {
+ wsocket.close(1000, reason)
+ });
+ test.done();
+}), true);
+
+wsocket.addEventListener('close', test.unreached_func('close event should not fire'), true);
diff --git a/test/wpt/tests/websockets/Close-delayed.any.js b/test/wpt/tests/websockets/Close-delayed.any.js
new file mode 100644
index 0000000..9e0a60c
--- /dev/null
+++ b/test/wpt/tests/websockets/Close-delayed.any.js
@@ -0,0 +1,27 @@
+// META: script=constants.sub.js
+// META: variant=?default
+// META: variant=?wss
+// META: variant=?wpt_flags=h2
+
+var test = async_test("Create WebSocket - Close the Connection - close should not emit until handshake completes - Connection should be closed");
+
+var wsocket = new WebSocket(`${SCHEME_DOMAIN_PORT}/delayed-passive-close`);
+var startTime;
+var isOpenCalled = false;
+
+wsocket.addEventListener('open', test.step_func(function(evt) {
+ startTime = performance.now();
+ wsocket.close();
+ isOpenCalled = true;
+}), true);
+
+wsocket.addEventListener('close', test.step_func(function(evt) {
+ const elapsed = performance.now() - startTime;
+ assert_true(isOpenCalled, "WebSocket connection should be open");
+ assert_equals(wsocket.readyState, 3, "readyState should be 3(CLOSED)");
+ assert_equals(evt.wasClean, true, "wasClean should be TRUE");
+ const jitterAllowance = 100;
+ assert_greater_than_equal(elapsed, 1000 - jitterAllowance,
+ 'one second should have elapsed')
+ test.done();
+}), true);
diff --git a/test/wpt/tests/websockets/Close-onlyReason.any.js b/test/wpt/tests/websockets/Close-onlyReason.any.js
new file mode 100644
index 0000000..243eb05
--- /dev/null
+++ b/test/wpt/tests/websockets/Close-onlyReason.any.js
@@ -0,0 +1,17 @@
+// META: script=constants.sub.js
+// META: variant=?default
+// META: variant=?wss
+// META: variant=?wpt_flags=h2
+
+var test = async_test("Create WebSocket - Close the Connection - close(only reason) - INVALID_ACCESS_ERR is thrown");
+
+var wsocket = CreateWebSocket(false, false);
+
+wsocket.addEventListener('open', test.step_func(function(evt) {
+ assert_throws_dom("INVALID_ACCESS_ERR", function() {
+ wsocket.close("Close with only reason")
+ });
+ test.done();
+}), true);
+
+wsocket.addEventListener('close', test.unreached_func('close event should not fire'), true);
diff --git a/test/wpt/tests/websockets/Close-readyState-Closed.any.js b/test/wpt/tests/websockets/Close-readyState-Closed.any.js
new file mode 100644
index 0000000..6c7b5f1
--- /dev/null
+++ b/test/wpt/tests/websockets/Close-readyState-Closed.any.js
@@ -0,0 +1,21 @@
+// META: script=constants.sub.js
+// META: variant=?default
+// META: variant=?wss
+// META: variant=?wpt_flags=h2
+
+var test = async_test("Create WebSocket - Close the Connection - readyState should be in CLOSED state and wasClean is TRUE - Connection should be closed");
+
+var wsocket = CreateWebSocket(false, false);
+var isOpenCalled = false;
+
+wsocket.addEventListener('open', test.step_func(function(evt) {
+ wsocket.close();
+ isOpenCalled = true;
+}), true);
+
+wsocket.addEventListener('close', test.step_func(function(evt) {
+ assert_true(isOpenCalled, "WebSocket connection should be open");
+ assert_equals(wsocket.readyState, 3, "readyState should be 3(CLOSED)");
+ assert_equals(evt.wasClean, true, "wasClean should be TRUE");
+ test.done();
+}), true);
diff --git a/test/wpt/tests/websockets/Close-readyState-Closing.any.js b/test/wpt/tests/websockets/Close-readyState-Closing.any.js
new file mode 100644
index 0000000..221130b
--- /dev/null
+++ b/test/wpt/tests/websockets/Close-readyState-Closing.any.js
@@ -0,0 +1,20 @@
+// META: script=constants.sub.js
+// META: variant=?default
+// META: variant=?wss
+// META: variant=?wpt_flags=h2
+
+var test = async_test("Create WebSocket - Close the Connection - readyState should be in CLOSING state just before onclose is called");
+
+var wsocket = CreateWebSocket(false, false);
+var isOpenCalled = false;
+
+wsocket.addEventListener('open', test.step_func(function(evt) {
+ isOpenCalled = true;
+ wsocket.close();
+ assert_equals(wsocket.readyState, 2, "readyState should be 2(CLOSING)");
+}), true);
+
+wsocket.addEventListener('close', test.step_func(function(evt) {
+ assert_true(isOpenCalled, 'open must be called');
+ test.done();
+}), true);
diff --git a/test/wpt/tests/websockets/Close-reason-unpaired-surrogates.any.js b/test/wpt/tests/websockets/Close-reason-unpaired-surrogates.any.js
new file mode 100644
index 0000000..e5a71d2
--- /dev/null
+++ b/test/wpt/tests/websockets/Close-reason-unpaired-surrogates.any.js
@@ -0,0 +1,22 @@
+// META: script=constants.sub.js
+// META: variant=?default
+// META: variant=?wss
+// META: variant=?wpt_flags=h2
+
+var test = async_test("Create WebSocket - Close the Connection - close(reason with unpaired surrogates) - connection should get closed");
+
+var wsocket = CreateWebSocket(false, false);
+var isOpenCalled = false;
+var replacementChar = "\uFFFD";
+var reason = "\uD807";
+
+wsocket.addEventListener('open', test.step_func(function(evt) {
+ wsocket.close(1000, reason);
+ isOpenCalled = true;
+}), true);
+
+wsocket.addEventListener('close', test.step_func(function(evt) {
+ assert_true(isOpenCalled, "WebSocket connection should be opened");
+ assert_equals(evt.reason, replacementChar, "reason replaced with replacement character");
+ test.done();
+}), true);
diff --git a/test/wpt/tests/websockets/Close-server-initiated-close.any.js b/test/wpt/tests/websockets/Close-server-initiated-close.any.js
new file mode 100644
index 0000000..82fd457
--- /dev/null
+++ b/test/wpt/tests/websockets/Close-server-initiated-close.any.js
@@ -0,0 +1,21 @@
+// META: script=constants.sub.js
+// META: variant=?default
+// META: variant=?wpt_flags=h2
+// META: variant=?wss
+
+var test = async_test("Create WebSocket - Server initiated Close - Client sends back a CLOSE - readyState should be in CLOSED state and wasClean is TRUE - Connection should be closed");
+
+var wsocket = CreateWebSocket(false, false);
+var isOpenCalled = false;
+
+wsocket.addEventListener('open', test.step_func(function(evt) {
+ wsocket.send("Goodbye");
+ isOpenCalled = true;
+}), true);
+
+wsocket.addEventListener('close', test.step_func(function(evt) {
+ assert_true(isOpenCalled, "WebSocket connection should be open");
+ assert_equals(wsocket.readyState, 3, "readyState should be 3(CLOSED)");
+ assert_equals(evt.wasClean, true, "wasClean should be TRUE");
+ test.done();
+}), true);
diff --git a/test/wpt/tests/websockets/Close-undefined.any.js b/test/wpt/tests/websockets/Close-undefined.any.js
new file mode 100644
index 0000000..e24ef0c
--- /dev/null
+++ b/test/wpt/tests/websockets/Close-undefined.any.js
@@ -0,0 +1,19 @@
+// META: script=constants.sub.js
+// META: variant=?default
+// META: variant=?wpt_flags=h2
+// META: variant=?wss
+
+var test = async_test();
+
+var wsocket = CreateWebSocket(false, false);
+var isOpenCalled = false;
+
+wsocket.addEventListener('open', test.step_func(function(evt) {
+ isOpenCalled = true;
+ wsocket.close(undefined);
+}), true);
+
+wsocket.addEventListener('close', test.step_func(function(evt) {
+ assert_true(isOpenCalled, 'open event must fire');
+ test.done();
+}), true);
diff --git a/test/wpt/tests/websockets/Create-asciiSep-protocol-string.any.js b/test/wpt/tests/websockets/Create-asciiSep-protocol-string.any.js
new file mode 100644
index 0000000..d0102ce
--- /dev/null
+++ b/test/wpt/tests/websockets/Create-asciiSep-protocol-string.any.js
@@ -0,0 +1,12 @@
+// META: script=constants.sub.js
+// META: variant=?default
+// META: variant=?wss
+// META: variant=?wpt_flags=h2
+
+test(function() {
+ var asciiWithSep = "/echo";
+ var wsocket;
+ assert_throws_dom("SYNTAX_ERR", function() {
+ wsocket = CreateWebSocketWithAsciiSep(asciiWithSep)
+ });
+}, "Create WebSocket - Pass a valid URL and a protocol string with an ascii separator character - SYNTAX_ERR is thrown")
diff --git a/test/wpt/tests/websockets/Create-blocked-port.any.js b/test/wpt/tests/websockets/Create-blocked-port.any.js
new file mode 100644
index 0000000..2962312
--- /dev/null
+++ b/test/wpt/tests/websockets/Create-blocked-port.any.js
@@ -0,0 +1,97 @@
+// META: script=constants.sub.js
+// META: variant=?default
+// META: variant=?wss
+// META: variant=?wpt_flags=h2
+
+async_test(t => {
+ const ws = CreateWebSocketWithBlockedPort(__PORT)
+ ws.onerror = t.unreached_func()
+ ws.onopen = t.step_func_done()
+}, 'Basic check');
+// list of bad ports according to
+// https://fetch.spec.whatwg.org/#port-blocking
+[
+ 1, // tcpmux
+ 7, // echo
+ 9, // discard
+ 11, // systat
+ 13, // daytime
+ 15, // netstat
+ 17, // qotd
+ 19, // chargen
+ 20, // ftp-data
+ 21, // ftp
+ 22, // ssh
+ 23, // telnet
+ 25, // smtp
+ 37, // time
+ 42, // name
+ 43, // nicname
+ 53, // domain
+ 69, // tftp
+ 77, // priv-rjs
+ 79, // finger
+ 87, // ttylink
+ 95, // supdup
+ 101, // hostriame
+ 102, // iso-tsap
+ 103, // gppitnp
+ 104, // acr-nema
+ 109, // pop2
+ 110, // pop3
+ 111, // sunrpc
+ 113, // auth
+ 115, // sftp
+ 117, // uucp-path
+ 119, // nntp
+ 123, // ntp
+ 135, // loc-srv / epmap
+ 137, // netbios-ns
+ 139, // netbios-ssn
+ 143, // imap2
+ 179, // bgp
+ 389, // ldap
+ 427, // afp (alternate)
+ 465, // smtp (alternate)
+ 512, // print / exec
+ 513, // login
+ 514, // shell
+ 515, // printer
+ 526, // tempo
+ 530, // courier
+ 531, // chat
+ 532, // netnews
+ 540, // uucp
+ 548, // afp
+ 554, // rtsp
+ 556, // remotefs
+ 563, // nntp+ssl
+ 587, // smtp (outgoing)
+ 601, // syslog-conn
+ 636, // ldap+ssl
+ 989, // ftps-data
+ 990, // ftps
+ 993, // ldap+ssl
+ 995, // pop3+ssl
+ 1719, // h323gatestat
+ 1720, // h323hostcall
+ 1723, // pptp
+ 2049, // nfs
+ 3659, // apple-sasl
+ 4045, // lockd
+ 6000, // x11
+ 6566, // sane-port
+ 6665, // irc (alternate)
+ 6666, // irc (alternate)
+ 6667, // irc (default)
+ 6668, // irc (alternate)
+ 6669, // irc (alternate)
+ 6697, // irc+tls
+ 10080, // amanda
+].forEach(blockedPort => {
+ async_test(t => {
+ const ws = CreateWebSocketWithBlockedPort(blockedPort)
+ ws.onerror = t.step_func_done()
+ ws.onopen = t.unreached_func()
+ }, "WebSocket blocked port test " + blockedPort)
+})
diff --git a/test/wpt/tests/websockets/Create-extensions-empty.any.js b/test/wpt/tests/websockets/Create-extensions-empty.any.js
new file mode 100644
index 0000000..98a7d65
--- /dev/null
+++ b/test/wpt/tests/websockets/Create-extensions-empty.any.js
@@ -0,0 +1,20 @@
+// META: timeout=long
+// META: script=constants.sub.js
+// META: variant=?default
+// META: variant=?wpt_flags=h2
+// META: variant=?wss
+
+var test = async_test("Create WebSocket - wsocket.extensions should be set to '' after connection is established - Connection should be closed");
+
+var wsocket = new WebSocket(SCHEME_DOMAIN_PORT + "/handshake_no_extensions");
+var isOpenCalled = false;
+
+wsocket.addEventListener('open', test.step_func_done(function(evt) {
+ wsocket.close();
+ isOpenCalled = true;
+ assert_equals(wsocket.extensions, "", "extensions should be empty");
+}), true);
+
+wsocket.addEventListener('close', test.step_func_done(function(evt) {
+ assert_true(isOpenCalled, "WebSocket connection should be closed");
+}), true);
diff --git a/test/wpt/tests/websockets/Create-http-urls.any.js b/test/wpt/tests/websockets/Create-http-urls.any.js
new file mode 100644
index 0000000..17590fc
--- /dev/null
+++ b/test/wpt/tests/websockets/Create-http-urls.any.js
@@ -0,0 +1,19 @@
+test(() => {
+ const url = new URL ("/", location);
+ url.protocol = "http";
+ const httpURL = url.href;
+ url.protocol = "https";
+ const httpsURL = url.href;
+ url.protocol = "ws";
+ const wsURL = url.href;
+ url.protocol = "wss";
+ const wssURL = url.href;
+
+ let ws = new WebSocket(httpURL);
+ assert_equals(ws.url, wsURL);
+ ws.close();
+
+ ws = new WebSocket(httpsURL);
+ assert_equals(ws.url, wssURL);
+ ws.close();
+}, "WebSocket: ensure both HTTP schemes are supported");
diff --git a/test/wpt/tests/websockets/Create-invalid-urls.any.js b/test/wpt/tests/websockets/Create-invalid-urls.any.js
new file mode 100644
index 0000000..73c9fad
--- /dev/null
+++ b/test/wpt/tests/websockets/Create-invalid-urls.any.js
@@ -0,0 +1,14 @@
+[
+ "ws://foo bar.com/",
+ "wss://foo bar.com/",
+ "ftp://"+location.host+"/",
+ "mailto:example@example.org",
+ "about:blank",
+ location.origin + "/#",
+ location.origin + "/#test",
+ "#test"
+].forEach(input => {
+ test(() => {
+ assert_throws_dom("SyntaxError", () => new WebSocket(input));
+ }, `new WebSocket("${input}") should throw a "SyntaxError" DOMException`);
+});
diff --git a/test/wpt/tests/websockets/Create-non-absolute-url.any.js b/test/wpt/tests/websockets/Create-non-absolute-url.any.js
new file mode 100644
index 0000000..5a7b179
--- /dev/null
+++ b/test/wpt/tests/websockets/Create-non-absolute-url.any.js
@@ -0,0 +1,14 @@
+[
+ "test",
+ "?",
+ null,
+ 123,
+].forEach(input => {
+ test(() => {
+ const url = new URL(input, location);
+ url.protocol = "ws";
+ const ws = new WebSocket(input);
+ assert_equals(ws.url, url.href);
+ ws.close();
+ }, `Create WebSocket - Pass a non absolute URL: ${input}`);
+});
diff --git a/test/wpt/tests/websockets/Create-nonAscii-protocol-string.any.js b/test/wpt/tests/websockets/Create-nonAscii-protocol-string.any.js
new file mode 100644
index 0000000..fda926a
--- /dev/null
+++ b/test/wpt/tests/websockets/Create-nonAscii-protocol-string.any.js
@@ -0,0 +1,12 @@
+// META: script=constants.sub.js
+// META: variant=?default
+// META: variant=?wss
+// META: variant=?wpt_flags=h2
+
+test(function() {
+ var nonAsciiProtocol = "\u0080echo";
+ var wsocket;
+ assert_throws_dom("SYNTAX_ERR", function() {
+ wsocket = CreateWebSocketNonAsciiProtocol(nonAsciiProtocol)
+ });
+}, "Create WebSocket - Pass a valid URL and a protocol string with non-ascii values - SYNTAX_ERR is thrown")
diff --git a/test/wpt/tests/websockets/Create-on-worker-shutdown.any.js b/test/wpt/tests/websockets/Create-on-worker-shutdown.any.js
new file mode 100644
index 0000000..218bf7c
--- /dev/null
+++ b/test/wpt/tests/websockets/Create-on-worker-shutdown.any.js
@@ -0,0 +1,26 @@
+async_test(t => {
+ function workerCode() {
+ close();
+ var ws = new WebSocket(self.location.origin.replace('http', 'ws'));
+ var data = {
+ originalState: ws.readyState,
+ afterCloseState: null
+ };
+
+ ws.close();
+
+ data.afterCloseState = ws.readyState;
+ postMessage(data);
+ }
+
+ var workerBlob = new Blob([workerCode.toString() + ";workerCode();"], {
+ type: "application/javascript"
+ });
+
+ var w = new Worker(URL.createObjectURL(workerBlob));
+ w.onmessage = t.step_func(function(e) {
+ assert_equals(e.data.originalState, WebSocket.CONNECTING, "WebSocket created on worker shutdown is in connecting state.");
+ assert_equals(e.data.afterCloseState, WebSocket.CLOSING, "Closed WebSocket created on worker shutdown is in closing state.");
+ t.done();
+ });
+}, 'WebSocket created after a worker self.close()');
diff --git a/test/wpt/tests/websockets/Create-protocol-with-space.any.js b/test/wpt/tests/websockets/Create-protocol-with-space.any.js
new file mode 100644
index 0000000..a85d4e5
--- /dev/null
+++ b/test/wpt/tests/websockets/Create-protocol-with-space.any.js
@@ -0,0 +1,11 @@
+// META: script=constants.sub.js
+// META: variant=?default
+// META: variant=?wpt_flags=h2
+// META: variant=?wss
+
+test(function() {
+ var wsocket;
+ assert_throws_dom("SYNTAX_ERR", function() {
+ wsocket = CreateWebSocketWithSpaceInProtocol("ec ho")
+ });
+}, "Create WebSocket - Pass a valid URL and a protocol string with a space in it - SYNTAX_ERR is thrown")
diff --git a/test/wpt/tests/websockets/Create-protocols-repeated-case-insensitive.any.js b/test/wpt/tests/websockets/Create-protocols-repeated-case-insensitive.any.js
new file mode 100644
index 0000000..1a508e8
--- /dev/null
+++ b/test/wpt/tests/websockets/Create-protocols-repeated-case-insensitive.any.js
@@ -0,0 +1,11 @@
+// META: script=constants.sub.js
+// META: variant=?default
+// META: variant=?wss
+// META: variant=?wpt_flags=h2
+
+test(function() {
+ var wsocket;
+ assert_throws_dom("SYNTAX_ERR", function() {
+ wsocket = CreateWebSocketWithRepeatedProtocolsCaseInsensitive()
+ });
+}, "Create WebSocket - Pass a valid URL and an array of protocol strings with repeated values but different case - SYNTAX_ERR is thrown")
diff --git a/test/wpt/tests/websockets/Create-protocols-repeated.any.js b/test/wpt/tests/websockets/Create-protocols-repeated.any.js
new file mode 100644
index 0000000..2f12a47
--- /dev/null
+++ b/test/wpt/tests/websockets/Create-protocols-repeated.any.js
@@ -0,0 +1,11 @@
+// META: script=constants.sub.js
+// META: variant=?default
+// META: variant=?wss
+// META: variant=?wpt_flags=h2
+
+test(function() {
+ var wsocket;
+ assert_throws_dom("SYNTAX_ERR", function() {
+ wsocket = CreateWebSocketWithRepeatedProtocols()
+ });
+}, "Create WebSocket - Pass a valid URL and an array of protocol strings with repeated values - SYNTAX_ERR is thrown")
diff --git a/test/wpt/tests/websockets/Create-url-with-space.any.js b/test/wpt/tests/websockets/Create-url-with-space.any.js
new file mode 100644
index 0000000..f2bea5b
--- /dev/null
+++ b/test/wpt/tests/websockets/Create-url-with-space.any.js
@@ -0,0 +1,12 @@
+// META: script=constants.sub.js
+// META: variant=?default
+// META: variant=?wpt_flags=h2
+// META: variant=?wss
+
+test(function() {
+ var wsocket;
+ var spaceUrl = "web platform.test";
+ assert_throws_dom("SYNTAX_ERR", function() {
+ wsocket = CreateWebSocketWithSpaceInUrl(spaceUrl)
+ });
+}, "Create WebSocket - Pass a URL with a space - SYNTAX_ERR should be thrown")
diff --git a/test/wpt/tests/websockets/Create-url-with-windows-1252-encoding.html b/test/wpt/tests/websockets/Create-url-with-windows-1252-encoding.html
new file mode 100644
index 0000000..6596b5e
--- /dev/null
+++ b/test/wpt/tests/websockets/Create-url-with-windows-1252-encoding.html
@@ -0,0 +1,20 @@
+<!doctype html>
+<meta charset=windows-1252>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script>
+test(() => {
+ const url = new URL("/", location);
+ url.protocol = "ws";
+ const input = "?\u20AC";
+ const expected = url.href + "?%E2%82%AC";
+
+ let ws = new WebSocket(url.href + input);
+ assert_equals(ws.url, expected);
+ ws.close();
+
+ ws = new WebSocket("/" + input);
+ assert_equals(ws.url, expected);
+ ws.close();
+}, "URL's percent-encoding is always in UTF-8 for WebSocket");
+</script>
diff --git a/test/wpt/tests/websockets/Create-valid-url-array-protocols.any.js b/test/wpt/tests/websockets/Create-valid-url-array-protocols.any.js
new file mode 100644
index 0000000..fe71fd7
--- /dev/null
+++ b/test/wpt/tests/websockets/Create-valid-url-array-protocols.any.js
@@ -0,0 +1,21 @@
+// META: script=constants.sub.js
+// META: variant=?default
+// META: variant=?wpt_flags=h2
+// META: variant=?wss
+
+var test = async_test("Create WebSocket - Pass a valid URL and array of protocol strings - Connection should be closed");
+
+var wsocket = CreateWebSocket(false, true);
+var isOpenCalled = false;
+
+wsocket.addEventListener('open', test.step_func(function(evt) {
+ assert_equals(wsocket.readyState, 1, "readyState should be 1(OPEN)");
+ wsocket.close();
+ isOpenCalled = true;
+}), true);
+
+wsocket.addEventListener('close', test.step_func(function(evt) {
+ assert_true(isOpenCalled, "WebSocket connection should be open");
+ assert_equals(evt.wasClean, true, "wasClean should be true");
+ test.done();
+}), true);
diff --git a/test/wpt/tests/websockets/Create-valid-url-binaryType-blob.any.js b/test/wpt/tests/websockets/Create-valid-url-binaryType-blob.any.js
new file mode 100644
index 0000000..7840ff3
--- /dev/null
+++ b/test/wpt/tests/websockets/Create-valid-url-binaryType-blob.any.js
@@ -0,0 +1,21 @@
+// META: script=constants.sub.js
+// META: variant=?default
+// META: variant=?wpt_flags=h2
+// META: variant=?wss
+
+var test = async_test("Create WebSocket - wsocket.binaryType should be set to 'blob' after connection is established - Connection should be closed");
+
+var wsocket = CreateWebSocket(false, false);
+var isOpenCalled = false;
+
+wsocket.addEventListener('open', test.step_func(function(evt) {
+ assert_equals(wsocket.binaryType, "blob", "binaryType should be set to Blob");
+ wsocket.close();
+ isOpenCalled = true;
+}), true);
+
+wsocket.addEventListener('close', test.step_func(function(evt) {
+ assert_true(isOpenCalled, "WebSocket connection should be open");
+ assert_equals(evt.wasClean, true, "wasClean should be true");
+ test.done();
+}), true);
diff --git a/test/wpt/tests/websockets/Create-valid-url-protocol-empty.any.js b/test/wpt/tests/websockets/Create-valid-url-protocol-empty.any.js
new file mode 100644
index 0000000..f18a9d8
--- /dev/null
+++ b/test/wpt/tests/websockets/Create-valid-url-protocol-empty.any.js
@@ -0,0 +1,10 @@
+// META: script=constants.sub.js
+// META: variant=?default
+// META: variant=?wpt_flags=h2
+// META: variant=?wss
+
+test(function() {
+ var wsocket = CreateWebSocket(true, false);
+ assert_equals(wsocket.protocol, "", "protocol should be empty");
+ wsocket.close();
+}, "Create WebSocket - wsocket.protocol should be empty before connection is established")
diff --git a/test/wpt/tests/websockets/Create-valid-url-protocol-setCorrectly.any.js b/test/wpt/tests/websockets/Create-valid-url-protocol-setCorrectly.any.js
new file mode 100644
index 0000000..c5d06ac
--- /dev/null
+++ b/test/wpt/tests/websockets/Create-valid-url-protocol-setCorrectly.any.js
@@ -0,0 +1,21 @@
+// META: script=constants.sub.js
+// META: variant=?default
+// META: variant=?wpt_flags=h2
+// META: variant=?wss
+
+var test = async_test("Create WebSocket - Pass a valid URL and protocol string - Connection should be closed");
+
+var wsocket = CreateWebSocket(true, false);
+var isOpenCalled = false;
+
+wsocket.addEventListener('open', test.step_func(function(evt) {
+ assert_equals(wsocket.protocol, "echo", "protocol should be set to echo");
+ wsocket.close();
+ isOpenCalled = true;
+}), true);
+
+wsocket.addEventListener('close', test.step_func(function(evt) {
+ assert_true(isOpenCalled, "WebSocket connection should be open");
+ assert_equals(evt.wasClean, true, "wasClean should be true");
+ test.done();
+}), true);
diff --git a/test/wpt/tests/websockets/Create-valid-url-protocol-string.any.js b/test/wpt/tests/websockets/Create-valid-url-protocol-string.any.js
new file mode 100644
index 0000000..10e928d
--- /dev/null
+++ b/test/wpt/tests/websockets/Create-valid-url-protocol-string.any.js
@@ -0,0 +1,21 @@
+// META: script=constants.sub.js
+// META: variant=?default
+// META: variant=?wss
+// META: variant=?wpt_flags=h2
+
+var test = async_test("Create WebSocket - Pass a valid URL and protocol string - Connection should be closed");
+
+var wsocket = CreateWebSocket(true, false);
+var isOpenCalled = false;
+
+wsocket.addEventListener('open', test.step_func(function(evt) {
+ assert_equals(wsocket.readyState, 1, "readyState should be 1(OPEN)");
+ wsocket.close();
+ isOpenCalled = true;
+}), true);
+
+wsocket.addEventListener('close', test.step_func(function(evt) {
+ assert_true(isOpenCalled, "WebSocket connection should be open");
+ assert_equals(evt.wasClean, true, "wasClean should be true");
+ test.done();
+}), true);
diff --git a/test/wpt/tests/websockets/Create-valid-url-protocol.any.js b/test/wpt/tests/websockets/Create-valid-url-protocol.any.js
new file mode 100644
index 0000000..37b5a0e
--- /dev/null
+++ b/test/wpt/tests/websockets/Create-valid-url-protocol.any.js
@@ -0,0 +1,21 @@
+// META: script=constants.sub.js
+// META: variant=?default
+// META: variant=?wpt_flags=h2
+// META: variant=?wss
+
+var test = async_test("Create WebSocket - Pass a valid URL and a protocol string - Connection should be closed");
+
+var wsocket = CreateWebSocket(true, false);
+var isOpenCalled = false;
+
+wsocket.addEventListener('open', test.step_func(function(evt) {
+ assert_equals(wsocket.readyState, 1, "readyState should be 1(OPEN)");
+ wsocket.close();
+ isOpenCalled = true;
+}), true);
+
+wsocket.addEventListener('close', test.step_func(function(evt) {
+ assert_true(isOpenCalled, "WebSocket connection should be open");
+ assert_equals(evt.wasClean, true, "wasClean should be true");
+ test.done();
+}), true);
diff --git a/test/wpt/tests/websockets/Create-valid-url.any.js b/test/wpt/tests/websockets/Create-valid-url.any.js
new file mode 100644
index 0000000..1df995f
--- /dev/null
+++ b/test/wpt/tests/websockets/Create-valid-url.any.js
@@ -0,0 +1,21 @@
+// META: script=constants.sub.js
+// META: variant=?default
+// META: variant=?wpt_flags=h2
+// META: variant=?wss
+
+var test = async_test("Create WebSocket - Pass a valid URL - Connection should be closed");
+
+var wsocket = CreateWebSocket(false, false);
+var isOpenCalled = false;
+
+wsocket.addEventListener('open', test.step_func(function(evt) {
+ assert_equals(wsocket.readyState, 1, "readyState should be 1(OPEN)");
+ wsocket.close();
+ isOpenCalled = true;
+}), true);
+
+wsocket.addEventListener('close', test.step_func(function(evt) {
+ assert_true(isOpenCalled, "WebSocket connection should be open");
+ assert_equals(evt.wasClean, true, "wasClean should be true");
+ test.done();
+}), true);
diff --git a/test/wpt/tests/websockets/META.yml b/test/wpt/tests/websockets/META.yml
new file mode 100644
index 0000000..da999b9
--- /dev/null
+++ b/test/wpt/tests/websockets/META.yml
@@ -0,0 +1,6 @@
+spec: https://websockets.spec.whatwg.org/
+suggested_reviewers:
+ - jdm
+ - ricea
+ - yutakahirano
+ - zqzhang
diff --git a/test/wpt/tests/websockets/README.md b/test/wpt/tests/websockets/README.md
new file mode 100644
index 0000000..ea35c70
--- /dev/null
+++ b/test/wpt/tests/websockets/README.md
@@ -0,0 +1 @@
+Tests for the [WebSockets Standard](https://websockets.spec.whatwg.org/).
diff --git a/test/wpt/tests/websockets/Send-0byte-data.any.js b/test/wpt/tests/websockets/Send-0byte-data.any.js
new file mode 100644
index 0000000..4176de4
--- /dev/null
+++ b/test/wpt/tests/websockets/Send-0byte-data.any.js
@@ -0,0 +1,30 @@
+// META: script=constants.sub.js
+// META: variant=?default
+// META: variant=?wss
+// META: variant=?wpt_flags=h2
+
+var test = async_test("Send 0 byte data on a WebSocket - Connection should be closed");
+
+var data = "";
+var wsocket = CreateWebSocket(false, false);
+var isOpenCalled = false;
+var isMessageCalled = false;
+
+wsocket.addEventListener('open', test.step_func(function(evt) {
+ wsocket.send(data);
+ assert_equals(data.length, wsocket.bufferedAmount);
+ isOpenCalled = true;
+}), true);
+
+wsocket.addEventListener('message', test.step_func(function(evt) {
+ isMessageCalled = true;
+ assert_equals(evt.data, data);
+ wsocket.close();
+}), true);
+
+wsocket.addEventListener('close', test.step_func(function(evt) {
+ assert_true(isOpenCalled, "WebSocket connection should be open");
+ assert_true(isMessageCalled, "message should be received")
+ assert_equals(evt.wasClean, true, "wasClean should be true");
+ test.done();
+}), true);
diff --git a/test/wpt/tests/websockets/Send-65K-data.any.js b/test/wpt/tests/websockets/Send-65K-data.any.js
new file mode 100644
index 0000000..20e5ba7
--- /dev/null
+++ b/test/wpt/tests/websockets/Send-65K-data.any.js
@@ -0,0 +1,33 @@
+// META: script=constants.sub.js
+// META: variant=?default
+// META: variant=?wpt_flags=h2
+// META: variant=?wss
+
+var test = async_test("Send 65K data on a WebSocket - Connection should be closed");
+
+var data = "";
+var wsocket = CreateWebSocket(false, false);
+var isOpenCalled = false;
+var isMessageCalled = false;
+
+wsocket.addEventListener('open', test.step_func(function(evt) {
+ for (var i = 0; i < 65000; i++) {
+ data = data + "c";
+ }
+ wsocket.send(data);
+ assert_equals(data.length, wsocket.bufferedAmount);
+ isOpenCalled = true;
+}), true);
+
+wsocket.addEventListener('message', test.step_func(function(evt) {
+ isMessageCalled = true;
+ assert_equals(evt.data, data);
+ wsocket.close();
+}), true);
+
+wsocket.addEventListener('close', test.step_func(function(evt) {
+ assert_true(isOpenCalled, "WebSocket connection should be open");
+ assert_true(isMessageCalled, "message should be received")
+ assert_equals(evt.wasClean, true, "wasClean should be true");
+ test.done();
+}), true);
diff --git a/test/wpt/tests/websockets/Send-before-open.any.js b/test/wpt/tests/websockets/Send-before-open.any.js
new file mode 100644
index 0000000..4fdbf71
--- /dev/null
+++ b/test/wpt/tests/websockets/Send-before-open.any.js
@@ -0,0 +1,11 @@
+// META: script=constants.sub.js
+// META: variant=?default
+// META: variant=?wss
+// META: variant=?wpt_flags=h2
+
+test(function() {
+ var wsocket = CreateWebSocket(false, false);
+ assert_throws_dom("INVALID_STATE_ERR", function() {
+ wsocket.send("Message to send")
+ });
+}, "Send data on a WebSocket before connection is opened - INVALID_STATE_ERR is returned")
diff --git a/test/wpt/tests/websockets/Send-binary-65K-arraybuffer.any.js b/test/wpt/tests/websockets/Send-binary-65K-arraybuffer.any.js
new file mode 100644
index 0000000..6bee660
--- /dev/null
+++ b/test/wpt/tests/websockets/Send-binary-65K-arraybuffer.any.js
@@ -0,0 +1,33 @@
+// META: script=constants.sub.js
+// META: variant=?default
+// META: variant=?wpt_flags=h2
+// META: variant=?wss
+
+var test = async_test("Send 65K binary data on a WebSocket - ArrayBuffer - Connection should be closed");
+
+var data = "";
+var datasize = 65000;
+var wsocket = CreateWebSocket(false, false);
+var isOpenCalled = false;
+var isMessageCalled = false;
+
+wsocket.addEventListener('open', test.step_func(function(evt) {
+ wsocket.binaryType = "arraybuffer";
+ data = new ArrayBuffer(datasize);
+ wsocket.send(data);
+ assert_equals(datasize, wsocket.bufferedAmount);
+ isOpenCalled = true;
+}), true);
+
+wsocket.addEventListener('message', test.step_func(function(evt) {
+ isMessageCalled = true;
+ assert_equals(evt.data.byteLength, datasize);
+ wsocket.close();
+}), true);
+
+wsocket.addEventListener('close', test.step_func(function(evt) {
+ assert_true(isOpenCalled, "WebSocket connection should be open");
+ assert_true(isMessageCalled, "message should be received")
+ assert_equals(evt.wasClean, true, "wasClean should be true");
+ test.done();
+}), true);
diff --git a/test/wpt/tests/websockets/Send-binary-arraybuffer.any.js b/test/wpt/tests/websockets/Send-binary-arraybuffer.any.js
new file mode 100644
index 0000000..0b34e0c
--- /dev/null
+++ b/test/wpt/tests/websockets/Send-binary-arraybuffer.any.js
@@ -0,0 +1,33 @@
+// META: script=constants.sub.js
+// META: variant=?default
+// META: variant=?wpt_flags=h2
+// META: variant=?wss
+
+var test = async_test("Send binary data on a WebSocket - ArrayBuffer - Connection should be closed");
+
+var data = "";
+var datasize = 15;
+var wsocket = CreateWebSocket(false, false);
+var isOpenCalled = false;
+var isMessageCalled = false;
+
+wsocket.addEventListener('open', test.step_func(function(evt) {
+ wsocket.binaryType = "arraybuffer";
+ data = new ArrayBuffer(datasize);
+ wsocket.send(data);
+ assert_equals(datasize, wsocket.bufferedAmount);
+ isOpenCalled = true;
+}), true);
+
+wsocket.addEventListener('message', test.step_func(function(evt) {
+ isMessageCalled = true;
+ assert_equals(evt.data.byteLength, datasize);
+ wsocket.close();
+}), true);
+
+wsocket.addEventListener('close', test.step_func(function(evt) {
+ assert_true(isOpenCalled, "WebSocket connection should be open");
+ assert_true(isMessageCalled, "message should be received")
+ assert_equals(evt.wasClean, true, "wasClean should be true");
+ test.done();
+}), true);
diff --git a/test/wpt/tests/websockets/Send-binary-arraybufferview-float32.any.js b/test/wpt/tests/websockets/Send-binary-arraybufferview-float32.any.js
new file mode 100644
index 0000000..47ee5b1
--- /dev/null
+++ b/test/wpt/tests/websockets/Send-binary-arraybufferview-float32.any.js
@@ -0,0 +1,40 @@
+// META: script=constants.sub.js
+// META: variant=?default
+// META: variant=?wpt_flags=h2
+// META: variant=?wss
+
+var test = async_test("Send binary data on a WebSocket - ArrayBufferView - Float32Array - Connection should be closed");
+
+var data = "";
+var datasize = 8;
+var view;
+var wsocket = CreateWebSocket(false, false);
+var isOpenCalled = false;
+var isMessageCalled = false;
+
+wsocket.addEventListener('open', test.step_func(function(evt) {
+ wsocket.binaryType = "arraybuffer";
+ data = new ArrayBuffer(datasize);
+ view = new Float32Array(data);
+ for (var i = 0; i < 2; i++) {
+ view[i] = i;
+ }
+ wsocket.send(view);
+ isOpenCalled = true;
+}), true);
+
+wsocket.addEventListener('message', test.step_func(function(evt) {
+ isMessageCalled = true;
+ var resultView = new Float32Array(evt.data);
+ for (var i = 0; i < resultView.length; i++) {
+ assert_equals(resultView[i], view[i], "ArrayBufferView returned is the same");
+ }
+ wsocket.close();
+}), true);
+
+wsocket.addEventListener('close', test.step_func(function(evt) {
+ assert_true(isOpenCalled, "WebSocket connection should be open");
+ assert_true(isMessageCalled, "message should be received")
+ assert_equals(evt.wasClean, true, "wasClean should be true");
+ test.done();
+}), true);
diff --git a/test/wpt/tests/websockets/Send-binary-arraybufferview-float64.any.js b/test/wpt/tests/websockets/Send-binary-arraybufferview-float64.any.js
new file mode 100644
index 0000000..78bcb13
--- /dev/null
+++ b/test/wpt/tests/websockets/Send-binary-arraybufferview-float64.any.js
@@ -0,0 +1,40 @@
+// META: script=constants.sub.js
+// META: variant=?default
+// META: variant=?wpt_flags=h2
+// META: variant=?wss
+
+var test = async_test("Send binary data on a WebSocket - ArrayBufferView - Float64Array - Connection should be closed");
+
+var data = "";
+var datasize = 8;
+var view;
+var wsocket = CreateWebSocket(false, false);
+var isOpenCalled = false;
+var isMessageCalled = false;
+
+wsocket.addEventListener('open', test.step_func(function(evt) {
+ wsocket.binaryType = "arraybuffer";
+ data = new ArrayBuffer(datasize);
+ view = new Float64Array(data);
+ for (var i = 0; i < 1; i++) {
+ view[i] = i;
+ }
+ wsocket.send(view);
+ isOpenCalled = true;
+}), true);
+
+wsocket.addEventListener('message', test.step_func(function(evt) {
+ isMessageCalled = true;
+ var resultView = new Float64Array(evt.data);
+ for (var i = 0; i < resultView.length; i++) {
+ assert_equals(resultView[i], view[i], "ArrayBufferView returned is the same");
+ }
+ wsocket.close();
+}), true);
+
+wsocket.addEventListener('close', test.step_func(function(evt) {
+ assert_true(isOpenCalled, "WebSocket connection should be open");
+ assert_true(isMessageCalled, "message should be received")
+ assert_equals(evt.wasClean, true, "wasClean should be true");
+ test.done();
+}), true);
diff --git a/test/wpt/tests/websockets/Send-binary-arraybufferview-int16-offset.any.js b/test/wpt/tests/websockets/Send-binary-arraybufferview-int16-offset.any.js
new file mode 100644
index 0000000..3dd6455
--- /dev/null
+++ b/test/wpt/tests/websockets/Send-binary-arraybufferview-int16-offset.any.js
@@ -0,0 +1,40 @@
+// META: script=constants.sub.js
+// META: variant=?default
+// META: variant=?wpt_flags=h2
+// META: variant=?wss
+
+var test = async_test("Send binary data on a WebSocket - ArrayBufferView - Int16Array with offset - Connection should be closed");
+
+var data = "";
+var datasize = 8;
+var view;
+var wsocket = CreateWebSocket(false, false);
+var isOpenCalled = false;
+var isMessageCalled = false;
+
+wsocket.addEventListener('open', test.step_func(function(evt) {
+ wsocket.binaryType = "arraybuffer";
+ data = new ArrayBuffer(datasize);
+ view = new Int16Array(data, 2);
+ for (var i = 0; i < 4; i++) {
+ view[i] = i;
+ }
+ wsocket.send(view);
+ isOpenCalled = true;
+}), true);
+
+wsocket.addEventListener('message', test.step_func(function(evt) {
+ isMessageCalled = true;
+ var resultView = new Int16Array(evt.data);
+ for (var i = 0; i < resultView.length; i++) {
+ assert_equals(resultView[i], view[i], "ArrayBufferView returned is the same");
+ }
+ wsocket.close();
+}), true);
+
+wsocket.addEventListener('close', test.step_func(function(evt) {
+ assert_true(isOpenCalled, "WebSocket connection should be open");
+ assert_true(isMessageCalled, "message should be received")
+ assert_equals(evt.wasClean, true, "wasClean should be true");
+ test.done();
+}), true);
diff --git a/test/wpt/tests/websockets/Send-binary-arraybufferview-int32.any.js b/test/wpt/tests/websockets/Send-binary-arraybufferview-int32.any.js
new file mode 100644
index 0000000..853ba39
--- /dev/null
+++ b/test/wpt/tests/websockets/Send-binary-arraybufferview-int32.any.js
@@ -0,0 +1,40 @@
+// META: script=constants.sub.js
+// META: variant=?default
+// META: variant=?wpt_flags=h2
+// META: variant=?wss
+
+var test = async_test("Send binary data on a WebSocket - ArrayBufferView - Int32Array - Connection should be closed");
+
+var data = "";
+var datasize = 8;
+var view;
+var wsocket = CreateWebSocket(false, false);
+var isOpenCalled = false;
+var isMessageCalled = false;
+
+wsocket.addEventListener('open', test.step_func(function(evt) {
+ wsocket.binaryType = "arraybuffer";
+ data = new ArrayBuffer(datasize);
+ view = new Int32Array(data);
+ for (var i = 0; i < 2; i++) {
+ view[i] = i;
+ }
+ wsocket.send(view);
+ isOpenCalled = true;
+}), true);
+
+wsocket.addEventListener('message', test.step_func(function(evt) {
+ isMessageCalled = true;
+ var resultView = new Int32Array(evt.data);
+ for (var i = 0; i < resultView.length; i++) {
+ assert_equals(resultView[i], view[i], "ArrayBufferView returned is the same");
+ }
+ wsocket.close();
+}), true);
+
+wsocket.addEventListener('close', test.step_func(function(evt) {
+ assert_true(isOpenCalled, "WebSocket connection should be open");
+ assert_true(isMessageCalled, "message should be received")
+ assert_equals(evt.wasClean, true, "wasClean should be true");
+ test.done();
+}), true);
diff --git a/test/wpt/tests/websockets/Send-binary-arraybufferview-int8.any.js b/test/wpt/tests/websockets/Send-binary-arraybufferview-int8.any.js
new file mode 100644
index 0000000..aa90020
--- /dev/null
+++ b/test/wpt/tests/websockets/Send-binary-arraybufferview-int8.any.js
@@ -0,0 +1,40 @@
+// META: script=constants.sub.js
+// META: variant=?default
+// META: variant=?wss
+// META: variant=?wpt_flags=h2
+
+var test = async_test("Send binary data on a WebSocket - ArrayBufferView - Int8Array - Connection should be closed");
+
+var data = "";
+var datasize = 8;
+var int8View;
+var wsocket = CreateWebSocket(false, false);
+var isOpenCalled = false;
+var isMessageCalled = false;
+
+wsocket.addEventListener('open', test.step_func(function(evt) {
+ wsocket.binaryType = "arraybuffer";
+ data = new ArrayBuffer(datasize);
+ int8View = new Int8Array(data);
+ for (var i = 0; i < 8; i++) {
+ int8View[i] = i;
+ }
+ wsocket.send(int8View);
+ isOpenCalled = true;
+}), true);
+
+wsocket.addEventListener('message', test.step_func(function(evt) {
+ isMessageCalled = true;
+ var resultView = new Int8Array(evt.data);
+ for (var i = 0; i < resultView.length; i++) {
+ assert_equals(resultView[i], int8View[i], "ArrayBufferView returned is the same");
+ }
+ wsocket.close();
+}), true);
+
+wsocket.addEventListener('close', test.step_func(function(evt) {
+ assert_true(isOpenCalled, "WebSocket connection should be open");
+ assert_true(isMessageCalled, "message should be received")
+ assert_equals(evt.wasClean, true, "wasClean should be true");
+ test.done();
+}), true);
diff --git a/test/wpt/tests/websockets/Send-binary-arraybufferview-uint16-offset-length.any.js b/test/wpt/tests/websockets/Send-binary-arraybufferview-uint16-offset-length.any.js
new file mode 100644
index 0000000..a3c1f32
--- /dev/null
+++ b/test/wpt/tests/websockets/Send-binary-arraybufferview-uint16-offset-length.any.js
@@ -0,0 +1,40 @@
+// META: script=constants.sub.js
+// META: variant=?default
+// META: variant=?wpt_flags=h2
+// META: variant=?wss
+
+var test = async_test("Send binary data on a WebSocket - ArrayBufferView - Uint16Array with offset and length - Connection should be closed");
+
+var data = "";
+var datasize = 8;
+var view;
+var wsocket = CreateWebSocket(false, false);
+var isOpenCalled = false;
+var isMessageCalled = false;
+
+wsocket.addEventListener('open', test.step_func(function(evt) {
+ wsocket.binaryType = "arraybuffer";
+ data = new ArrayBuffer(datasize);
+ view = new Uint16Array(data, 2, 2);
+ for (var i = 0; i < 4; i++) {
+ view[i] = i;
+ }
+ wsocket.send(view);
+ isOpenCalled = true;
+}), true);
+
+wsocket.addEventListener('message', test.step_func(function(evt) {
+ isMessageCalled = true;
+ var resultView = new Uint16Array(evt.data);
+ for (var i = 0; i < resultView.length; i++) {
+ assert_equals(resultView[i], view[i], "ArrayBufferView returned is the same");
+ }
+ wsocket.close();
+}), true);
+
+wsocket.addEventListener('close', test.step_func(function(evt) {
+ assert_true(isOpenCalled, "WebSocket connection should be open");
+ assert_true(isMessageCalled, "message should be received")
+ assert_equals(evt.wasClean, true, "wasClean should be true");
+ test.done();
+}), true);
diff --git a/test/wpt/tests/websockets/Send-binary-arraybufferview-uint32-offset.any.js b/test/wpt/tests/websockets/Send-binary-arraybufferview-uint32-offset.any.js
new file mode 100644
index 0000000..fede995
--- /dev/null
+++ b/test/wpt/tests/websockets/Send-binary-arraybufferview-uint32-offset.any.js
@@ -0,0 +1,40 @@
+// META: script=constants.sub.js
+// META: variant=?default
+// META: variant=?wpt_flags=h2
+// META: variant=?wss
+
+var test = async_test("Send binary data on a WebSocket - ArrayBufferView - Uint32Array with offset - Connection should be closed");
+
+var data = "";
+var datasize = 8;
+var view;
+var wsocket = CreateWebSocket(false, false);
+var isOpenCalled = false;
+var isMessageCalled = false;
+
+wsocket.addEventListener('open', test.step_func(function(evt) {
+ wsocket.binaryType = "arraybuffer";
+ data = new ArrayBuffer(datasize);
+ view = new Uint32Array(data, 0);
+ for (var i = 0; i < 2; i++) {
+ view[i] = i;
+ }
+ wsocket.send(view);
+ isOpenCalled = true;
+}), true);
+
+wsocket.addEventListener('message', test.step_func(function(evt) {
+ isMessageCalled = true;
+ var resultView = new Uint32Array(evt.data);
+ for (var i = 0; i < resultView.length; i++) {
+ assert_equals(resultView[i], view[i], "ArrayBufferView returned is the same");
+ }
+ wsocket.close();
+}), true);
+
+wsocket.addEventListener('close', test.step_func(function(evt) {
+ assert_true(isOpenCalled, "WebSocket connection should be open");
+ assert_true(isMessageCalled, "message should be received")
+ assert_equals(evt.wasClean, true, "wasClean should be true");
+ test.done();
+}), true);
diff --git a/test/wpt/tests/websockets/Send-binary-arraybufferview-uint8-offset-length.any.js b/test/wpt/tests/websockets/Send-binary-arraybufferview-uint8-offset-length.any.js
new file mode 100644
index 0000000..de3ae00
--- /dev/null
+++ b/test/wpt/tests/websockets/Send-binary-arraybufferview-uint8-offset-length.any.js
@@ -0,0 +1,40 @@
+// META: script=constants.sub.js
+// META: variant=?default
+// META: variant=?wpt_flags=h2
+// META: variant=?wss
+
+var test = async_test("Send binary data on a WebSocket - ArrayBufferView - Uint8Array with offset and length - Connection should be closed");
+
+var data = "";
+var datasize = 8;
+var view;
+var wsocket = CreateWebSocket(false, false);
+var isOpenCalled = false;
+var isMessageCalled = false;
+
+wsocket.addEventListener('open', test.step_func(function(evt) {
+ wsocket.binaryType = "arraybuffer";
+ data = new ArrayBuffer(datasize);
+ view = new Uint8Array(data, 2, 4);
+ for (var i = 0; i < 8; i++) {
+ view[i] = i;
+ }
+ wsocket.send(view);
+ isOpenCalled = true;
+}), true);
+
+wsocket.addEventListener('message', test.step_func(function(evt) {
+ isMessageCalled = true;
+ var resultView = new Uint8Array(evt.data);
+ for (var i = 0; i < resultView.length; i++) {
+ assert_equals(resultView[i], view[i], "ArrayBufferView returned is the same");
+ }
+ wsocket.close();
+}), true);
+
+wsocket.addEventListener('close', test.step_func(function(evt) {
+ assert_true(isOpenCalled, "WebSocket connection should be open");
+ assert_true(isMessageCalled, "message should be received")
+ assert_equals(evt.wasClean, true, "wasClean should be true");
+ test.done();
+}), true);
diff --git a/test/wpt/tests/websockets/Send-binary-arraybufferview-uint8-offset.any.js b/test/wpt/tests/websockets/Send-binary-arraybufferview-uint8-offset.any.js
new file mode 100644
index 0000000..089174b
--- /dev/null
+++ b/test/wpt/tests/websockets/Send-binary-arraybufferview-uint8-offset.any.js
@@ -0,0 +1,40 @@
+// META: script=constants.sub.js
+// META: variant=?default
+// META: variant=?wpt_flags=h2
+// META: variant=?wss
+
+var test = async_test("Send binary data on a WebSocket - ArrayBufferView - Uint8Array with offset - Connection should be closed");
+
+var data = "";
+var datasize = 8;
+var view;
+var wsocket = CreateWebSocket(false, false);
+var isOpenCalled = false;
+var isMessageCalled = false;
+
+wsocket.addEventListener('open', test.step_func(function(evt) {
+ wsocket.binaryType = "arraybuffer";
+ data = new ArrayBuffer(datasize);
+ view = new Uint8Array(data, 2);
+ for (var i = 0; i < 8; i++) {
+ view[i] = i;
+ }
+ wsocket.send(view);
+ isOpenCalled = true;
+}), true);
+
+wsocket.addEventListener('message', test.step_func(function(evt) {
+ isMessageCalled = true;
+ var resultView = new Uint8Array(evt.data);
+ for (var i = 0; i < resultView.length; i++) {
+ assert_equals(resultView[i], view[i], "ArrayBufferView returned is the same");
+ }
+ wsocket.close();
+}), true);
+
+wsocket.addEventListener('close', test.step_func(function(evt) {
+ assert_true(isOpenCalled, "WebSocket connection should be open");
+ assert_true(isMessageCalled, "message should be received")
+ assert_equals(evt.wasClean, true, "wasClean should be true");
+ test.done();
+}), true);
diff --git a/test/wpt/tests/websockets/Send-binary-blob.any.js b/test/wpt/tests/websockets/Send-binary-blob.any.js
new file mode 100644
index 0000000..5131b71
--- /dev/null
+++ b/test/wpt/tests/websockets/Send-binary-blob.any.js
@@ -0,0 +1,36 @@
+// META: script=constants.sub.js
+// META: variant=?default
+// META: variant=?wpt_flags=h2
+// META: variant=?wss
+
+var test = async_test("Send binary data on a WebSocket - Blob - Connection should be closed");
+
+var data = "";
+var datasize = 65000;
+var isOpenCalled = false;
+var isMessageCalled = false;
+
+var wsocket = CreateWebSocket(false, false);
+
+wsocket.addEventListener('open', test.step_func(function(evt) {
+ wsocket.binaryType = "blob";
+ for (var i = 0; i < datasize; i++)
+ data += String.fromCharCode(0);
+ data = new Blob([data]);
+ isOpenCalled = true;
+ wsocket.send(data);
+}), true);
+
+wsocket.addEventListener('message', test.step_func(function(evt) {
+ isMessageCalled = true;
+ assert_true(evt.data instanceof Blob);
+ assert_equals(evt.data.size, datasize);
+ wsocket.close();
+}), true);
+
+wsocket.addEventListener('close', test.step_func(function(evt) {
+ assert_true(isOpenCalled, "WebSocket connection should be open");
+ assert_true(isMessageCalled, "message should be received");
+ assert_true(evt.wasClean, "wasClean should be true");
+ test.done();
+}), true);
diff --git a/test/wpt/tests/websockets/Send-data.any.js b/test/wpt/tests/websockets/Send-data.any.js
new file mode 100644
index 0000000..a606ada
--- /dev/null
+++ b/test/wpt/tests/websockets/Send-data.any.js
@@ -0,0 +1,30 @@
+// META: script=constants.sub.js
+// META: variant=?default
+// META: variant=?wss
+// META: variant=?wpt_flags=h2
+
+var test = async_test("Send data on a WebSocket - Connection should be closed");
+
+var data = "Message to send";
+var wsocket = CreateWebSocket(false, false);
+var isOpenCalled = false;
+var isMessageCalled = false;
+
+wsocket.addEventListener('open', test.step_func(function(evt) {
+ wsocket.send(data);
+ assert_equals(data.length, wsocket.bufferedAmount);
+ isOpenCalled = true;
+}), true);
+
+wsocket.addEventListener('message', test.step_func(function(evt) {
+ isMessageCalled = true;
+ assert_equals(evt.data, data);
+ wsocket.close();
+}), true);
+
+wsocket.addEventListener('close', test.step_func(function(evt) {
+ assert_true(isMessageCalled, "message should be received");
+ assert_true(isOpenCalled, "WebSocket connection should be open");
+ assert_equals(evt.wasClean, true, "wasClean should be true");
+ test.done();
+}), true);
diff --git a/test/wpt/tests/websockets/Send-data.worker.js b/test/wpt/tests/websockets/Send-data.worker.js
new file mode 100644
index 0000000..5a8bdd5
--- /dev/null
+++ b/test/wpt/tests/websockets/Send-data.worker.js
@@ -0,0 +1,26 @@
+// META: variant=?default
+// META: variant=?wss
+// META: variant=?wpt_flags=h2
+
+importScripts("/resources/testharness.js");
+importScripts('constants.sub.js')
+
+var data = "test data";
+
+async_test(function(t) {
+
+ var wsocket = CreateWebSocket(false, false);
+
+ wsocket.addEventListener('open', function (e) {
+ wsocket.send(data)
+ }, true)
+
+ wsocket.addEventListener('message', t.step_func_done(function(e) {
+ assert_equals(e.data, data);
+ }), true);
+
+ wsocket.addEventListener('close', t.unreached_func('the close event should not fire'), true);
+
+}, "Send data on a WebSocket in a Worker")
+
+done();
diff --git a/test/wpt/tests/websockets/Send-null.any.js b/test/wpt/tests/websockets/Send-null.any.js
new file mode 100644
index 0000000..e621bb8
--- /dev/null
+++ b/test/wpt/tests/websockets/Send-null.any.js
@@ -0,0 +1,32 @@
+// META: script=constants.sub.js
+// META: variant=?default
+// META: variant=?wpt_flags=h2
+// META: variant=?wss
+
+var test = async_test("Send null data on a WebSocket - Connection should be closed");
+
+var data = null;
+var nullReturned = false;
+var wsocket = CreateWebSocket(false, false);
+var isOpenCalled = false;
+var isMessageCalled = false;
+
+wsocket.addEventListener('open', test.step_func(function(evt) {
+ wsocket.send(data);
+ isOpenCalled = true;
+}), true);
+
+wsocket.addEventListener('message', test.step_func(function(evt) {
+ isMessageCalled = true;
+ if ("null" == evt.data || "" == evt.data)
+ nullReturned = true;
+ assert_true(nullReturned);
+ wsocket.close();
+}), true);
+
+wsocket.addEventListener('close', test.step_func(function(evt) {
+ assert_true(isOpenCalled, "WebSocket connection should be open");
+ assert_true(isMessageCalled, "message should be received");
+ assert_equals(evt.wasClean, true, "wasClean should be true");
+ test.done();
+}), true);
diff --git a/test/wpt/tests/websockets/Send-paired-surrogates.any.js b/test/wpt/tests/websockets/Send-paired-surrogates.any.js
new file mode 100644
index 0000000..51e4fb9
--- /dev/null
+++ b/test/wpt/tests/websockets/Send-paired-surrogates.any.js
@@ -0,0 +1,30 @@
+// META: script=constants.sub.js
+// META: variant=?default
+// META: variant=?wpt_flags=h2
+// META: variant=?wss
+
+var test = async_test("Send paired surrogates data on a WebSocket - Connection should be closed");
+
+var data = "\uD801\uDC07";
+var wsocket = CreateWebSocket(false, false);
+var isOpenCalled = false;
+var isMessageCalled = false;
+
+wsocket.addEventListener('open', test.step_func(function(evt) {
+ wsocket.send(data);
+ assert_equals(data.length * 2, wsocket.bufferedAmount);
+ isOpenCalled = true;
+}), true);
+
+wsocket.addEventListener('message', test.step_func(function(evt) {
+ isMessageCalled = true;
+ assert_equals(evt.data, data);
+ wsocket.close();
+}), true);
+
+wsocket.addEventListener('close', test.step_func(function(evt) {
+ assert_true(isOpenCalled, "WebSocket connection should be open");
+ assert_true(isMessageCalled, "message should be received");
+ assert_equals(evt.wasClean, true, "wasClean should be true");
+ test.done();
+}), true);
diff --git a/test/wpt/tests/websockets/Send-unicode-data.any.js b/test/wpt/tests/websockets/Send-unicode-data.any.js
new file mode 100644
index 0000000..a3556b2
--- /dev/null
+++ b/test/wpt/tests/websockets/Send-unicode-data.any.js
@@ -0,0 +1,30 @@
+// META: script=constants.sub.js
+// META: variant=?default
+// META: variant=?wpt_flags=h2
+// META: variant=?wss
+
+var test = async_test("Send unicode data on a WebSocket - Connection should be closed");
+
+var data = "¥¥¥¥¥¥";
+var wsocket = CreateWebSocket(false, false);
+var isOpenCalled = false;
+var isMessageCalled = false;
+
+wsocket.addEventListener('open', test.step_func(function(evt) {
+ wsocket.send(data);
+ assert_equals(data.length * 2, wsocket.bufferedAmount);
+ isOpenCalled = true;
+}), true);
+
+wsocket.addEventListener('message', test.step_func(function(evt) {
+ isMessageCalled = true;
+ assert_equals(evt.data, data);
+ wsocket.close();
+}), true);
+
+wsocket.addEventListener('close', test.step_func(function(evt) {
+ assert_true(isOpenCalled, "WebSocket connection should be open");
+ assert_true(isMessageCalled, "message should be received");
+ assert_equals(evt.wasClean, true, "wasClean should be true");
+ test.done();
+}), true);
diff --git a/test/wpt/tests/websockets/Send-unpaired-surrogates.any.js b/test/wpt/tests/websockets/Send-unpaired-surrogates.any.js
new file mode 100644
index 0000000..cbbcc6e
--- /dev/null
+++ b/test/wpt/tests/websockets/Send-unpaired-surrogates.any.js
@@ -0,0 +1,30 @@
+// META: script=constants.sub.js
+// META: variant=?default
+// META: variant=?wss
+// META: variant=?wpt_flags=h2
+
+var test = async_test("Send unpaired surrogates on a WebSocket - Connection should be closed");
+
+var data = "\uD807";
+var replacementChar = "\uFFFD";
+var wsocket = CreateWebSocket(false, false);
+var isOpenCalled = false;
+var isMessageCalled = false;
+
+wsocket.addEventListener('open', test.step_func(function(evt) {
+ wsocket.send(data);
+ isOpenCalled = true;
+}), true);
+
+wsocket.addEventListener('message', test.step_func(function(evt) {
+ isMessageCalled = true;
+ assert_equals(evt.data, replacementChar);
+ wsocket.close();
+}), true);
+
+wsocket.addEventListener('close', test.step_func(function(evt) {
+ assert_true(isOpenCalled, "WebSocket connection should be open");
+ assert_true(isMessageCalled, "message should be received");
+ assert_equals(evt.wasClean, true, "wasClean should be true");
+ test.done();
+}), true);
diff --git a/test/wpt/tests/websockets/back-forward-cache-with-closed-websocket-connection-ccns.tentative.window.js b/test/wpt/tests/websockets/back-forward-cache-with-closed-websocket-connection-ccns.tentative.window.js
new file mode 100644
index 0000000..f6ee5ed
--- /dev/null
+++ b/test/wpt/tests/websockets/back-forward-cache-with-closed-websocket-connection-ccns.tentative.window.js
@@ -0,0 +1,31 @@
+// META: title=Testing BFCache support for page with closed WebSocket connection and "Cache-Control: no-store" header.
+// META: script=/common/dispatcher/dispatcher.js
+// META: script=/common/utils.js
+// META: script=/html/browsers/browsing-the-web/back-forward-cache/resources/rc-helper.js
+// META: script=/html/browsers/browsing-the-web/remote-context-helper/resources/remote-context-helper.js
+// META: script=/websockets/constants.sub.js
+// META: script=resources/websockets-test-helpers.sub.js
+
+'use strict';
+
+promise_test(async t => {
+ const rcHelper = new RemoteContextHelper();
+
+ // Open a window with noopener so that BFCache will work.
+ const rc1 = await rcHelper.addWindow(
+ /*config=*/ { headers: [['Cache-Control', 'no-store']] },
+ /*options=*/ { features: 'noopener' }
+ );
+ // Make sure that we only run the remaining of the test when page with
+ // "Cache-Control: no-store" header is eligible for BFCache.
+ await assertBFCacheEligibility(rc1, /*shouldRestoreFromBFCache=*/ true);
+
+ await openThenCloseWebSocket(rc1);
+ // The page should not be eligible for BFCache because of the usage
+ // of WebSocket.
+ await assertBFCacheEligibility(rc1, /*shouldRestoreFromBFCache=*/ false);
+ await assertNotRestoredFromBFCache(rc1, [
+ 'WebSocketSticky',
+ 'MainResourceHasCacheControlNoStore'
+ ]);
+});
diff --git a/test/wpt/tests/websockets/back-forward-cache-with-closed-websocket-connection.window.js b/test/wpt/tests/websockets/back-forward-cache-with-closed-websocket-connection.window.js
new file mode 100644
index 0000000..30b8e63
--- /dev/null
+++ b/test/wpt/tests/websockets/back-forward-cache-with-closed-websocket-connection.window.js
@@ -0,0 +1,20 @@
+// META: title=Testing BFCache support for page with closed WebSocket connection.
+// META: script=/common/dispatcher/dispatcher.js
+// META: script=/common/utils.js
+// META: script=/html/browsers/browsing-the-web/back-forward-cache/resources/rc-helper.js
+// META: script=/html/browsers/browsing-the-web/remote-context-helper/resources/remote-context-helper.js
+// META: script=/websockets/constants.sub.js
+// META: script=resources/websockets-test-helpers.sub.js
+
+'use strict';
+
+promise_test(async t => {
+ const rcHelper = new RemoteContextHelper();
+
+ // Open a window with noopener so that BFCache will work.
+ const rc1 = await rcHelper.addWindow(
+ /*config=*/ null, /*options=*/ { features: 'noopener' });
+ await openThenCloseWebSocket(rc1);
+ // The page should be eligible for BFCache because the WebSocket connection has been closed.
+ await assertBFCacheEligibility(rc1, /*shouldRestoreFromBFCache=*/ true);
+});
diff --git a/test/wpt/tests/websockets/back-forward-cache-with-open-websocket-connection-ccns.tentative.window.js b/test/wpt/tests/websockets/back-forward-cache-with-open-websocket-connection-ccns.tentative.window.js
new file mode 100644
index 0000000..f37a04a
--- /dev/null
+++ b/test/wpt/tests/websockets/back-forward-cache-with-open-websocket-connection-ccns.tentative.window.js
@@ -0,0 +1,32 @@
+// META: title=Testing BFCache support for page with open WebSocket connection and "Cache-Control: no-store" header.
+// META: script=/common/dispatcher/dispatcher.js
+// META: script=/common/utils.js
+// META: script=/html/browsers/browsing-the-web/back-forward-cache/resources/rc-helper.js
+// META: script=/html/browsers/browsing-the-web/remote-context-helper/resources/remote-context-helper.js
+// META: script=/websockets/constants.sub.js
+// META: script=resources/websockets-test-helpers.sub.js
+
+'use strict';
+
+promise_test(async t => {
+ const rcHelper = new RemoteContextHelper();
+
+ // Open a window with noopener so that BFCache will work.
+ const rc1 = await rcHelper.addWindow(
+ /*config=*/ { headers: [['Cache-Control', 'no-store']] },
+ /*options=*/ { features: 'noopener' }
+ );
+ // Make sure that we only run the remaining of the test when page with
+ // "Cache-Control: no-store" header is eligible for BFCache.
+ await assertBFCacheEligibility(rc1, /*shouldRestoreFromBFCache=*/ true);
+
+ await openWebSocket(rc1);
+ // The page should not be eligible for BFCache because of the usage
+ // of WebSocket.
+ await assertBFCacheEligibility(rc1, /*shouldRestoreFromBFCache=*/ false);
+ await assertNotRestoredFromBFCache(rc1, [
+ 'WebSocket',
+ 'WebSocketSticky',
+ 'MainResourceHasCacheControlNoStore'
+ ]);
+});
diff --git a/test/wpt/tests/websockets/back-forward-cache-with-open-websocket-connection.window.js b/test/wpt/tests/websockets/back-forward-cache-with-open-websocket-connection.window.js
new file mode 100644
index 0000000..6c48a57
--- /dev/null
+++ b/test/wpt/tests/websockets/back-forward-cache-with-open-websocket-connection.window.js
@@ -0,0 +1,21 @@
+// META: title=Testing BFCache support for page with open WebSocket connection.
+// META: script=/common/dispatcher/dispatcher.js
+// META: script=/common/utils.js
+// META: script=/html/browsers/browsing-the-web/back-forward-cache/resources/rc-helper.js
+// META: script=/html/browsers/browsing-the-web/remote-context-helper/resources/remote-context-helper.js
+// META: script=/websockets/constants.sub.js
+// META: script=resources/websockets-test-helpers.sub.js
+
+'use strict';
+
+promise_test(async t => {
+ const rcHelper = new RemoteContextHelper();
+
+ // Open a window with noopener so that BFCache will work.
+ const rc1 = await rcHelper.addWindow(
+ /*config=*/ null, /*options=*/ { features: 'noopener' });
+ await openWebSocket(rc1);
+ // The page should not be eligible for BFCache because of open WebSocket connection.
+ await assertBFCacheEligibility(rc1, /*shouldRestoreFromBFCache=*/ false);
+ await assertNotRestoredFromBFCache(rc1, ['websocket']);
+});
diff --git a/test/wpt/tests/websockets/basic-auth.any.js b/test/wpt/tests/websockets/basic-auth.any.js
new file mode 100644
index 0000000..9fbdc5d
--- /dev/null
+++ b/test/wpt/tests/websockets/basic-auth.any.js
@@ -0,0 +1,17 @@
+// META: global=window,worker
+// META: script=constants.sub.js
+// META: variant=?wss
+// META: variant=?wpt_flags=h2
+
+async_test(t => {
+ const url = __SCHEME + '://' + 'foo:bar@' + __SERVER__NAME + ':' + __PORT + '/basic_auth';
+ const ws = new WebSocket(url);
+ ws.onopen = () => {
+ ws.onclose = ws.onerror = null;
+ ws.close();
+ t.done();
+ };
+ ws.onerror = ws.onclose = t.unreached_func('open should succeed');
+}, 'HTTP basic authentication should work with WebSockets');
+
+done();
diff --git a/test/wpt/tests/websockets/binary/001.html b/test/wpt/tests/websockets/binary/001.html
new file mode 100644
index 0000000..077bf79
--- /dev/null
+++ b/test/wpt/tests/websockets/binary/001.html
@@ -0,0 +1,27 @@
+<!doctype html>
+<title>WebSockets: Send/Receive blob, blob size less than network array buffer</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=../constants.sub.js></script>
+<meta name="variant" content="?default">
+<meta name="variant" content="?wss">
+<meta name="variant" content="?wpt_flags=h2">
+<div id=log></div>
+<script>
+async_test(function(t){
+ var ws = new WebSocket(SCHEME_DOMAIN_PORT + '/echo');
+ var data = "";
+ var datasize = 10;
+ ws.onopen = t.step_func(function(e) {
+ ws.binaryType = "blob";
+ data = new ArrayBuffer(datasize);
+ ws.send(data);
+ })
+ ws.onmessage = t.step_func(function(e) {
+ assert_true(e.data instanceof Blob);
+ assert_equals(e.data.size, datasize);
+ t.done();
+ })
+ ws.onclose = t.unreached_func('close event should not fire');
+});
+</script>
diff --git a/test/wpt/tests/websockets/binary/002.html b/test/wpt/tests/websockets/binary/002.html
new file mode 100644
index 0000000..5587776
--- /dev/null
+++ b/test/wpt/tests/websockets/binary/002.html
@@ -0,0 +1,28 @@
+<!doctype html>
+<title>WebSockets: Send/Receive blob, blob size greater than network array buffer</title>
+<meta name=timeout content=long>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=../constants.sub.js></script>
+<meta name="variant" content="?default">
+<meta name="variant" content="?wss">
+<meta name="variant" content="?wpt_flags=h2">
+<div id=log></div>
+<script>
+async_test(function(t){
+ var ws = new WebSocket(SCHEME_DOMAIN_PORT+'/echo');
+ var data = "";
+ var datasize = 100000;
+ ws.onopen = t.step_func(function(e) {
+ ws.binaryType = "blob";
+ data = new ArrayBuffer(datasize);
+ ws.send(data);
+ });
+ ws.onmessage = t.step_func(function(e) {
+ assert_true(e.data instanceof Blob);
+ assert_equals(e.data.size, datasize);
+ t.done();
+ });
+ ws.onclose = t.unreached_func('close event should not fire');
+});
+</script>
diff --git a/test/wpt/tests/websockets/binary/004.html b/test/wpt/tests/websockets/binary/004.html
new file mode 100644
index 0000000..8ca4e92
--- /dev/null
+++ b/test/wpt/tests/websockets/binary/004.html
@@ -0,0 +1,27 @@
+<!doctype html>
+<title>WebSockets: Send/Receive ArrayBuffer, size greater than network array buffer</title>
+<meta name=timeout content=long>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=../constants.sub.js></script>
+<meta name="variant" content="?default">
+<meta name="variant" content="?wss">
+<meta name="variant" content="?wpt_flags=h2">
+<div id=log></div>
+<script>
+async_test(function(t){
+ var ws = new WebSocket(SCHEME_DOMAIN_PORT+'/echo');
+ var data = "";
+ var datasize = 100000;
+ ws.onopen = t.step_func(function(e) {
+ ws.binaryType = "arraybuffer";
+ data = new ArrayBuffer(datasize);
+ ws.send(data);
+ })
+ ws.onmessage = t.step_func(function(e) {
+ assert_equals(e.data.byteLength, datasize);
+ t.done();
+ })
+ ws.onclose = t.unreached_func('close event should not fire');
+});
+</script>
diff --git a/test/wpt/tests/websockets/binary/005.html b/test/wpt/tests/websockets/binary/005.html
new file mode 100644
index 0000000..e89f4c0
--- /dev/null
+++ b/test/wpt/tests/websockets/binary/005.html
@@ -0,0 +1,26 @@
+<!doctype html>
+<title>WebSockets: Send/Receive ArrayBuffer, size less than network array buffer</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=../constants.sub.js></script>
+<meta name="variant" content="?default">
+<meta name="variant" content="?wss">
+<meta name="variant" content="?wpt_flags=h2">
+<div id=log></div>
+<script>
+async_test(function(t){
+ var ws = new WebSocket(SCHEME_DOMAIN_PORT+'/echo');
+ var data = "";
+ var datasize = 10;
+ ws.onopen = t.step_func(function(e) {
+ ws.binaryType = "arraybuffer";
+ data = new ArrayBuffer(datasize);
+ ws.send(data);
+ });
+ ws.onmessage = t.step_func(function(e) {
+ assert_equals(e.data.byteLength, datasize);
+ t.done();
+ });
+ ws.onclose = t.unreached_func('close event should not fire');
+ });
+</script>
diff --git a/test/wpt/tests/websockets/binaryType-wrong-value.any.js b/test/wpt/tests/websockets/binaryType-wrong-value.any.js
new file mode 100644
index 0000000..683fb47
--- /dev/null
+++ b/test/wpt/tests/websockets/binaryType-wrong-value.any.js
@@ -0,0 +1,23 @@
+// META: script=constants.sub.js
+// META: variant=?default
+// META: variant=?wss
+// META: variant=?wpt_flags=h2
+
+var test = async_test("Create WebSocket - set binaryType to something other than blob or arraybuffer - SYNTAX_ERR is returned - Connection should be closed");
+
+let wsocket = CreateWebSocket(false, false);
+let opened = false;
+
+wsocket.addEventListener('open', test.step_func(function(evt) {
+ opened = true;
+ assert_equals(wsocket.binaryType, "blob");
+ wsocket.binaryType = "notBlobOrArrayBuffer";
+ assert_equals(wsocket.binaryType, "blob");
+ wsocket.close();
+}), true);
+
+wsocket.addEventListener('close', test.step_func(function(evt) {
+ assert_true(opened, "connection should be opened");
+ assert_true(evt.wasClean, "wasClean should be true");
+ test.done();
+}), true);
diff --git a/test/wpt/tests/websockets/bufferedAmount-unchanged-by-sync-xhr.any.js b/test/wpt/tests/websockets/bufferedAmount-unchanged-by-sync-xhr.any.js
new file mode 100644
index 0000000..c15536d
--- /dev/null
+++ b/test/wpt/tests/websockets/bufferedAmount-unchanged-by-sync-xhr.any.js
@@ -0,0 +1,25 @@
+// META: script=constants.sub.js
+// META: global=window,dedicatedworker,sharedworker
+// META: variant=?default
+// META: variant=?wss
+// META: variant=?wpt_flags=h2
+
+async_test(t => {
+ const ws = CreateWebSocket(false, false);
+ ws.onopen = t.step_func(() => {
+ ws.onclose = ws.onerror = null;
+ assert_equals(ws.bufferedAmount, 0);
+ ws.send('hello');
+ assert_equals(ws.bufferedAmount, 5);
+ // Stop execution for 1s with a sync XHR.
+ const xhr = new XMLHttpRequest();
+ xhr.open('GET', '/common/blank.html?pipe=trickle(d1)', false);
+ xhr.send();
+ assert_equals(ws.bufferedAmount, 5);
+ ws.close();
+ t.done();
+ });
+ ws.onerror = ws.onclose = t.unreached_func('open should succeed');
+}, 'bufferedAmount should not be updated during a sync XHR');
+
+done();
diff --git a/test/wpt/tests/websockets/close-invalid.any.js b/test/wpt/tests/websockets/close-invalid.any.js
new file mode 100644
index 0000000..c964c83
--- /dev/null
+++ b/test/wpt/tests/websockets/close-invalid.any.js
@@ -0,0 +1,21 @@
+// META: script=constants.sub.js
+// META: variant=?default
+// META: variant=?wss
+// META: variant=?wpt_flags=h2
+
+[
+ [0, "0"],
+ [500, "500"],
+ [NaN, "NaN"],
+ ["string", "String"],
+ [null, "null"],
+ [0x10000 + 1000, "2**16+1000"],
+].forEach(function(t) {
+ test(function() {
+ var ws = CreateWebSocket(false, false);
+ assert_throws_dom("InvalidAccessError", function() {
+ ws.close(t[0]);
+ });
+ ws.onerror = this.unreached_func();
+ }, t[1] + " on a websocket");
+});
diff --git a/test/wpt/tests/websockets/closing-handshake/002.html b/test/wpt/tests/websockets/closing-handshake/002.html
new file mode 100644
index 0000000..8d1e43b
--- /dev/null
+++ b/test/wpt/tests/websockets/closing-handshake/002.html
@@ -0,0 +1,23 @@
+<!doctype html>
+<title>WebSockets: server sends closing handshake</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=../constants.sub.js></script>
+<meta name="variant" content="?default">
+<meta name="variant" content="?wss">
+<meta name="variant" content="?wpt_flags=h2">
+<div id=log></div>
+<script>
+async_test(function(t){
+ var ws = new WebSocket(SCHEME_DOMAIN_PORT+'/echo_exit');
+ ws.onmessage = ws.onerror = t.unreached_func();
+ ws.onopen = t.step_func(function(e) {
+ ws.send('Goodbye');
+ })
+ ws.onclose = t.step_func(function(e) {
+ assert_equals(e.wasClean, true, 'e.wasClean');
+ ws.onclose = t.unreached_func();
+ t.step_timeout(() => t.done(), 50);
+ });
+});
+</script>
diff --git a/test/wpt/tests/websockets/closing-handshake/003.html b/test/wpt/tests/websockets/closing-handshake/003.html
new file mode 100644
index 0000000..43e1603
--- /dev/null
+++ b/test/wpt/tests/websockets/closing-handshake/003.html
@@ -0,0 +1,24 @@
+<!doctype html>
+<title>WebSockets: client sends closing handshake</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=../constants.sub.js></script>
+<meta name="variant" content="?default">
+<meta name="variant" content="?wss">
+<meta name="variant" content="?wpt_flags=h2">
+<div id=log></div>
+<script>
+async_test(function(t) {
+ var ws = new WebSocket(SCHEME_DOMAIN_PORT + '/echo');
+ ws.onmessage = ws.onerror = t.unreached_func();
+ ws.onopen = t.step_func(function(e) {
+ ws.close();
+ });
+ ws.onclose = t.step_func(function(e) {
+ assert_equals(e.code, 1005, 'e.code');
+ assert_equals(e.wasClean, true, 'e.wasClean');
+ ws.onclose = t.unreached_func();
+ t.step_timeout(() => t.done(), 50);
+ });
+});
+</script>
diff --git a/test/wpt/tests/websockets/closing-handshake/004.html b/test/wpt/tests/websockets/closing-handshake/004.html
new file mode 100644
index 0000000..96411ea
--- /dev/null
+++ b/test/wpt/tests/websockets/closing-handshake/004.html
@@ -0,0 +1,25 @@
+<!doctype html>
+<title>WebSockets: data after closing handshake</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=../constants.sub.js></script>
+<meta name="variant" content="?default">
+<meta name="variant" content="?wss">
+<meta name="variant" content="?wpt_flags=h2">
+<div id=log></div>
+<script>
+async_test(function(t) {
+ var ws = new WebSocket(SCHEME_DOMAIN_PORT+'/echo_close_data');
+ ws.onmessage = ws.onerror = t.unreached_func();
+
+ ws.onopen = t.step_func(function(e) {
+ ws.send('Goodbye');
+ })
+
+ ws.onclose = t.step_func(function(e) {
+ assert_equals(e.wasClean, true);
+ ws.onclose = t.unreached_func();
+ t.step_timeout(() => t.done(), 50);
+ })
+});
+</script>
diff --git a/test/wpt/tests/websockets/constants.sub.js b/test/wpt/tests/websockets/constants.sub.js
new file mode 100644
index 0000000..fd3c3b8
--- /dev/null
+++ b/test/wpt/tests/websockets/constants.sub.js
@@ -0,0 +1,94 @@
+const __SERVER__NAME = "{{host}}";
+const __PATH = "echo";
+
+let __SCHEME;
+let __PORT;
+if (url_has_flag('h2')) {
+ __SCHEME = 'wss';
+ __PORT = "{{ports[h2][0]}}";
+} else if (url_has_variant('wss') || location.protocol === 'https:') {
+ __SCHEME = 'wss';
+ __PORT = "{{ports[wss][0]}}";
+} else {
+ __SCHEME = 'ws';
+ __PORT = "{{ports[ws][0]}}";
+}
+
+const SCHEME_DOMAIN_PORT = __SCHEME + '://' + __SERVER__NAME + ':' + __PORT;
+
+function url_has_variant(variant) {
+ const params = new URLSearchParams(location.search);
+ return params.get(variant) === "";
+}
+
+function url_has_flag(flag) {
+ const params = new URLSearchParams(location.search);
+ return params.getAll("wpt_flags").indexOf(flag) !== -1;
+}
+
+function IsWebSocket() {
+ if (!self.WebSocket) {
+ assert_true(false, "Browser does not support WebSocket");
+ }
+}
+
+function CreateWebSocketNonAsciiProtocol(nonAsciiProtocol) {
+ IsWebSocket();
+ const url = SCHEME_DOMAIN_PORT + "/" + __PATH;
+ return new WebSocket(url, nonAsciiProtocol);
+}
+
+function CreateWebSocketWithAsciiSep(asciiWithSep) {
+ IsWebSocket();
+ const url = SCHEME_DOMAIN_PORT + "/" + __PATH;
+ return new WebSocket(url, asciiWithSep);
+}
+
+function CreateWebSocketWithBlockedPort(blockedPort) {
+ IsWebSocket();
+ const url = __SCHEME + "://" + __SERVER__NAME + ":" + blockedPort + "/" + __PATH;
+ return new WebSocket(url);
+}
+
+function CreateWebSocketWithSpaceInUrl(urlWithSpace) {
+ IsWebSocket();
+ const url = __SCHEME + "://" + urlWithSpace + ":" + __PORT + "/" + __PATH;
+ return new WebSocket(url);
+}
+
+function CreateWebSocketWithSpaceInProtocol(protocolWithSpace) {
+ IsWebSocket();
+ const url = SCHEME_DOMAIN_PORT + "/" + __PATH;
+ return new WebSocket(url, protocolWithSpace);
+}
+
+function CreateWebSocketWithRepeatedProtocols() {
+ IsWebSocket();
+ const url = SCHEME_DOMAIN_PORT + "/" + __PATH;
+ return new WebSocket(url, ["echo", "echo"]);
+}
+
+function CreateWebSocketWithRepeatedProtocolsCaseInsensitive() {
+ IsWebSocket();
+ const url = SCHEME_DOMAIN_PORT + "/" + __PATH;
+ wsocket = new WebSocket(url, ["echo", "eCho"]);
+}
+
+function CreateInsecureWebSocket() {
+ IsWebSocket();
+ const url = `ws://${__SERVER__NAME}:{{ports[ws][0]}}/${__PATH}`;
+ return new WebSocket(url);
+}
+
+function CreateWebSocket(isProtocol, isProtocols) {
+ IsWebSocket();
+ const url = SCHEME_DOMAIN_PORT + "/" + __PATH;
+
+ if (isProtocol) {
+ return new WebSocket(url, "echo");
+ }
+ if (isProtocols) {
+ return new WebSocket(url, ["echo", "chat"]);
+ }
+ return new WebSocket(url);
+}
diff --git a/test/wpt/tests/websockets/constructor.any.js b/test/wpt/tests/websockets/constructor.any.js
new file mode 100644
index 0000000..0cef206
--- /dev/null
+++ b/test/wpt/tests/websockets/constructor.any.js
@@ -0,0 +1,10 @@
+// META: script=constants.sub.js
+// META: variant=?default
+// META: variant=?wss
+// META: variant=?wpt_flags=h2
+
+test(function() {
+ var ws = new WebSocket(SCHEME_DOMAIN_PORT + "/" + __PATH,
+ "echo", "Stray argument")
+ assert_true(ws instanceof WebSocket, "Expected a WebSocket instance.")
+}, "Calling the WebSocket constructor with too many arguments should not throw.")
diff --git a/test/wpt/tests/websockets/constructor/001.html b/test/wpt/tests/websockets/constructor/001.html
new file mode 100644
index 0000000..13493e3
--- /dev/null
+++ b/test/wpt/tests/websockets/constructor/001.html
@@ -0,0 +1,14 @@
+<!doctype html>
+<title>WebSockets: new WebSocket() with no args</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=../constants.sub.js></script>
+<meta name="variant" content="?default">
+<meta name="variant" content="?wss">
+<meta name="variant" content="?wpt_flags=h2">
+<div id=log></div>
+<script>
+test(function() {
+ assert_throws_js(TypeError, function(){new WebSocket()});
+});
+</script>
diff --git a/test/wpt/tests/websockets/constructor/004.html b/test/wpt/tests/websockets/constructor/004.html
new file mode 100644
index 0000000..8143210
--- /dev/null
+++ b/test/wpt/tests/websockets/constructor/004.html
@@ -0,0 +1,36 @@
+<!doctype html>
+<title>WebSockets: new WebSocket(url, invalid protocol)</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=../constants.sub.js></script>
+<meta name="variant" content="?default">
+<meta name="variant" content="?wss">
+<meta name="variant" content="?wpt_flags=h2">
+<div id=log></div>
+<script>
+// empty string
+test(function() {
+ assert_throws_dom("SyntaxError", function() {
+ new WebSocket(SCHEME_DOMAIN_PORT + '/empty-message', "")
+ })
+});
+
+// chars below U+0020 except U+0000; U+0000 is tested in a separate test
+for (var i = 1; i < 0x20; ++i) {
+ test(function() {
+ assert_throws_dom("SyntaxError", function() {
+ new WebSocket(SCHEME_DOMAIN_PORT + '/empty-message',
+ "a"+String.fromCharCode(i)+"b")
+ }, 'char code '+i);
+ })
+}
+// some chars above U+007E
+for (var i = 0x7F; i < 0x100; ++i) {
+ test(function() {
+ assert_throws_dom("SyntaxError", function() {
+ new WebSocket(SCHEME_DOMAIN_PORT + '/empty-message',
+ "a"+String.fromCharCode(i)+"b")
+ }, 'char code '+i);
+ })
+}
+</script>
diff --git a/test/wpt/tests/websockets/constructor/005.html b/test/wpt/tests/websockets/constructor/005.html
new file mode 100644
index 0000000..9d467de
--- /dev/null
+++ b/test/wpt/tests/websockets/constructor/005.html
@@ -0,0 +1,14 @@
+<!doctype html>
+<title>WebSockets: return value</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=../constants.sub.js></script>
+<meta name="variant" content="?default">
+<meta name="variant" content="?wss">
+<meta name="variant" content="?wpt_flags=h2">
+<div id=log></div>
+<script>
+test(function() {
+ assert_true(new WebSocket(SCHEME_DOMAIN_PORT + '/empty-message') instanceof WebSocket);
+});
+</script>
diff --git a/test/wpt/tests/websockets/constructor/006.html b/test/wpt/tests/websockets/constructor/006.html
new file mode 100644
index 0000000..5987583
--- /dev/null
+++ b/test/wpt/tests/websockets/constructor/006.html
@@ -0,0 +1,29 @@
+<!doctype html>
+<title>WebSockets: converting first arguments</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=../constants.sub.js></script>
+<meta name="variant" content="?default">
+<meta name="variant" content="?wss">
+<meta name="variant" content="?wpt_flags=h2">
+<div id=log></div>
+<script>
+async_test(function(t) {
+ var a = document.createElement('a');
+ a.href = SCHEME_DOMAIN_PORT+'/echo';
+ var ws = new WebSocket(a); // should stringify arguments; <a> stringifies to its .href
+ assert_equals(ws.url, a.href);
+ ws.onopen = t.step_func(function(e) {
+ ws.send('test');
+ });
+ ws.onmessage = t.step_func(function(e) {
+ assert_equals(e.data, 'test');
+ ws.onclose = t.step_func(function(e) {
+ ws.onclose = t.unreached_func();
+ t.step_timeout(() => t.done(), 50);
+ });
+ ws.close();
+ });
+ ws.onerror = ws.onclose = t.unreached_func();
+});
+</script>
diff --git a/test/wpt/tests/websockets/constructor/007.html b/test/wpt/tests/websockets/constructor/007.html
new file mode 100644
index 0000000..e126d1a
--- /dev/null
+++ b/test/wpt/tests/websockets/constructor/007.html
@@ -0,0 +1,17 @@
+<!doctype html>
+<title>WebSockets: new WebSocket(url, null char)</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=../constants.sub.js></script>
+<meta name="variant" content="?default">
+<meta name="variant" content="?wss">
+<meta name="variant" content="?wpt_flags=h2">
+<div id=log></div>
+<script>
+test(function() {
+ assert_throws_dom("SyntaxError", function() {
+ new WebSocket(SCHEME_DOMAIN_PORT + '/empty-message',
+ 'a' + String.fromCharCode(0) + 'b')
+ })
+});
+</script>
diff --git a/test/wpt/tests/websockets/constructor/008.html b/test/wpt/tests/websockets/constructor/008.html
new file mode 100644
index 0000000..e10c652
--- /dev/null
+++ b/test/wpt/tests/websockets/constructor/008.html
@@ -0,0 +1,15 @@
+<!doctype html>
+<title>WebSockets: new WebSocket(url with not blocked port)</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=../constants.sub.js></script>
+<meta name="variant" content="?default">
+<meta name="variant" content="?wss">
+<div id=log></div>
+<script>
+//Pass condition is to not throw
+test(function(){new WebSocket('ws://example.invalid:80/')});
+test(function(){new WebSocket('ws://example.invalid:443/')});
+test(function(){new WebSocket('wss://example.invalid:80/')});
+test(function(){new WebSocket('wss://example.invalid:443/')});
+</script>
diff --git a/test/wpt/tests/websockets/constructor/009.html b/test/wpt/tests/websockets/constructor/009.html
new file mode 100644
index 0000000..f8123c2
--- /dev/null
+++ b/test/wpt/tests/websockets/constructor/009.html
@@ -0,0 +1,24 @@
+<!doctype html>
+<title>WebSockets: protocol</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=../constants.sub.js></script>
+<meta name="variant" content="?default">
+<meta name="variant" content="?wss">
+<meta name="variant" content="?wpt_flags=h2">
+<div id=log></div>
+<script>
+async_test(function(t) {
+ var ws = new WebSocket(SCHEME_DOMAIN_PORT+'/protocol', 'foobar');
+
+ ws.onmessage = t.step_func(function(e) {
+ assert_equals(ws.protocol, 'foobar');
+ ws.onclose = t.step_func(function(e) {
+ ws.onclose = t.unreached_func();
+ t.step_timeout(() => t.done(), 50);
+ })
+ ws.close();
+ })
+ ws.onerror = t.unreached_func();
+});
+</script>
diff --git a/test/wpt/tests/websockets/constructor/010.html b/test/wpt/tests/websockets/constructor/010.html
new file mode 100644
index 0000000..e5bc6ec
--- /dev/null
+++ b/test/wpt/tests/websockets/constructor/010.html
@@ -0,0 +1,22 @@
+<!doctype html>
+<title>WebSockets: protocol in response but no requested protocol</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=../constants.sub.js></script>
+<meta name="variant" content="?default">
+<meta name="variant" content="?wss">
+<meta name="variant" content="?wpt_flags=h2">
+<div id=log></div>
+<script>
+async_test(function(t) {
+ var ws = new WebSocket(SCHEME_DOMAIN_PORT+'/handshake_protocol');
+ ws.onopen = ws.onmessage = ws.onclose = t.step_func(e => assert_unreached(e.type));
+ ws.onerror = t.step_func(function(e) {
+ ws.onclose = t.step_func(function(e) {
+ assert_false(e.wasClean, 'e.wasClean should be false');
+ assert_equals(e.code, 1006, 'e.code should be 1006');
+ t.done();
+ });
+ })
+});
+</script>
diff --git a/test/wpt/tests/websockets/constructor/011.html b/test/wpt/tests/websockets/constructor/011.html
new file mode 100644
index 0000000..33b09db
--- /dev/null
+++ b/test/wpt/tests/websockets/constructor/011.html
@@ -0,0 +1,28 @@
+<!doctype html>
+<title>WebSockets: protocol mismatch</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=../constants.sub.js></script>
+<meta name="variant" content="?default">
+<meta name="variant" content="?wss">
+<meta name="variant" content="?wpt_flags=h2">
+<div id=log></div>
+<script>
+async_test(function(t) {
+ // Sub-protocol matching is case-sensitive.
+ var ws = new WebSocket(SCHEME_DOMAIN_PORT+'/handshake_protocol', 'FOOBAR');
+ var gotOpen = false;
+ var gotError = false;
+ ws.onopen = t.step_func(function(e) {
+ gotOpen = true;
+ });
+ ws.onerror = t.step_func(function(e) {
+ gotError = true;
+ });
+ ws.onclose = t.step_func(function(e) {
+ assert_false(gotOpen, 'got open');
+ assert_true(gotError, 'got error');
+ t.done();
+ });
+});
+</script>
diff --git a/test/wpt/tests/websockets/constructor/012.html b/test/wpt/tests/websockets/constructor/012.html
new file mode 100644
index 0000000..ba2b6b2
--- /dev/null
+++ b/test/wpt/tests/websockets/constructor/012.html
@@ -0,0 +1,20 @@
+<!doctype html>
+<title>WebSockets: no protocol in response</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=../constants.sub.js></script>
+<meta name="variant" content="?default">
+<meta name="variant" content="?wss">
+<meta name="variant" content="?wpt_flags=h2">
+<div id=log></div>
+<script>
+async_test(function(t) {
+ var ws = new WebSocket(SCHEME_DOMAIN_PORT+'/handshake_no_protocol', 'foobar');
+ ws.onclose = t.step_func(function(e) {
+ ws.onclose = t.unreached_func();
+ t.step_timeout(() => t.done(), 50);
+ })
+ ws.onmessage = t.unreached_func();
+});
+</script>
+
diff --git a/test/wpt/tests/websockets/constructor/013.html b/test/wpt/tests/websockets/constructor/013.html
new file mode 100644
index 0000000..d599fde
--- /dev/null
+++ b/test/wpt/tests/websockets/constructor/013.html
@@ -0,0 +1,42 @@
+<!doctype html>
+<title>WebSockets: multiple WebSocket objects</title>
+<meta name=timeout content=long>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=../constants.sub.js></script>
+<meta name="variant" content="?default">
+<meta name="variant" content="?wss">
+<meta name="variant" content="?wpt_flags=h2">
+<div id=log></div>
+<script>
+async_test(function(t) {
+ // test that the events are fired as they should when opening 25 websockets and
+ // sending a message on each and then closing when getting the message back
+ var ws = [];
+ var events = 0;
+ for (var i = 0; i < 25; ++i) {
+ ws[i] = new WebSocket(SCHEME_DOMAIN_PORT+'/echo');
+ ws[i].id = i;
+ ws[i].onopen = t.step_func(function(e) {
+ events++;
+ this.send(this.id);
+ this.onopen = t.step_func(function() {assert_unreached()});
+ }, ws[i]);
+ ws[i].onmessage = t.step_func(function(e) {
+ events++;
+ assert_equals(e.data, ''+this.id);
+ this.close();
+ this.onmessage = t.step_func(function() {assert_unreached()});
+ }, ws[i]);
+ ws[i].onclose = t.step_func(function(e) {
+ events++;
+ if (events == 75) {
+ t.done();
+ }
+ this.onclose = t.step_func(function() {assert_unreached()});
+ }, ws[i]);
+ ws[i].onerror = t.step_func(function() {assert_unreached()});
+ }
+});
+</script>
+
diff --git a/test/wpt/tests/websockets/constructor/014.html b/test/wpt/tests/websockets/constructor/014.html
new file mode 100644
index 0000000..afa0dac
--- /dev/null
+++ b/test/wpt/tests/websockets/constructor/014.html
@@ -0,0 +1,39 @@
+<!doctype html>
+<title>WebSockets: serialize establish a connection</title>
+<meta name="timeout" content="long">
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=../constants.sub.js></script>
+<meta name="variant" content="?default">
+<meta name="variant" content="?wss">
+<div id=log></div>
+<script>
+
+async_test(function(t) {
+ var ws = [];
+ var events = 0;
+ var prevDate;
+ var date;
+ for (var i = 0; i < 2; ++i) {
+ ws[i] = new WebSocket(SCHEME_DOMAIN_PORT+'/handshake_sleep_2');
+ ws[i].id = i;
+ ws[i].onopen = t.step_func(function(e) {
+ events++;
+ date = new Date();
+ if (prevDate) {
+ assert_greater_than(date - prevDate, 1000);
+ }
+ prevDate = date;
+ this.onopen = t.step_func(function() {assert_unreached()});
+ }.bind(ws[i]))
+ ws[i].onclose = t.step_func(function() {
+ events++;
+ if (events == 4) {
+ t.done();
+ }
+ this.onclose = t.step_func(function() {assert_unreached()});
+ }.bind(ws[i]));
+ ws[i].onerror = ws[i].onmessage = t.step_func(function() {assert_unreached()});
+ }
+});
+</script>
diff --git a/test/wpt/tests/websockets/constructor/016.html b/test/wpt/tests/websockets/constructor/016.html
new file mode 100644
index 0000000..1860505
--- /dev/null
+++ b/test/wpt/tests/websockets/constructor/016.html
@@ -0,0 +1,20 @@
+<!doctype html>
+<meta charset=windows-1252>
+<title>WebSockets: non-ascii URL in query, document encoding windows-1252</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=../constants.sub.js></script>
+<meta name="variant" content="?default">
+<meta name="variant" content="?wss">
+<meta name="variant" content="?wpt_flags=h2">
+<div id=log></div>
+<script>
+async_test(function(t) {
+ var ws = new WebSocket(SCHEME_DOMAIN_PORT+'/echo-query_v13?åäö');
+ ws.onclose = t.step_func(function() {assert_unreached()});
+ ws.onmessage = t.step_func(function(e) {
+ assert_equals(e.data, '%C3%A5%C3%A4%C3%B6');
+ t.done();
+ });
+});
+</script>
diff --git a/test/wpt/tests/websockets/constructor/017.html b/test/wpt/tests/websockets/constructor/017.html
new file mode 100644
index 0000000..e1795b1
--- /dev/null
+++ b/test/wpt/tests/websockets/constructor/017.html
@@ -0,0 +1,19 @@
+<!doctype html>
+<title>WebSockets: too few slashes after ws: and wss:</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=../constants.sub.js></script>
+<meta name="variant" content="?default">
+<meta name="variant" content="?wss">
+<meta name="variant" content="?wpt_flags=h2">
+<div id=log></div>
+<script>
+var tests = [
+ [__SCHEME + ':', __PORT],
+ [__SCHEME + ':/', __PORT],
+];
+//Pass condition is to not throw
+for (var i = 0; i < tests.length; ++i) {
+ test(function(){new WebSocket(tests[i][0] + location.hostname + ':' + tests[i][1] + '/echo')}, tests[i][0]);
+}
+</script>
diff --git a/test/wpt/tests/websockets/constructor/018.html b/test/wpt/tests/websockets/constructor/018.html
new file mode 100644
index 0000000..71f7376
--- /dev/null
+++ b/test/wpt/tests/websockets/constructor/018.html
@@ -0,0 +1,20 @@
+<!doctype html>
+<title>WebSockets: NULL char in url</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=../constants.sub.js></script>
+<meta name="variant" content="?default">
+<meta name="variant" content="?wss">
+<meta name="variant" content="?wpt_flags=h2">
+<div id=log></div>
+<script>
+async_test(function(t) {
+ var ws = new WebSocket(SCHEME_DOMAIN_PORT+'/echo-query?x\u0000y\u0000');
+ ws.onmessage = t.step_func(function(e) {
+ assert_equals(e.data, 'x%00y');
+ ws.close();
+ t.done();
+ })
+ ws.onclose = ws.onerror = t.step_func(function(e) {assert_unreached(e.type)});
+});
+</script>
diff --git a/test/wpt/tests/websockets/constructor/019.html b/test/wpt/tests/websockets/constructor/019.html
new file mode 100644
index 0000000..8fbb1cb
--- /dev/null
+++ b/test/wpt/tests/websockets/constructor/019.html
@@ -0,0 +1,21 @@
+<!doctype html>
+<title>WebSockets: uppercase 'WS:'</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=../constants.sub.js></script>
+<meta name="variant" content="?default">
+<meta name="variant" content="?wss">
+<meta name="variant" content="?wpt_flags=h2">
+<div id=log></div>
+<script>
+async_test(function(t) {
+ var scheme = SCHEME_DOMAIN_PORT.split('://')[0];
+ var domain = SCHEME_DOMAIN_PORT.split('://')[1];
+ var ws = new WebSocket(scheme.toUpperCase()+'://'+domain+'/echo');
+ ws.onopen = t.step_func(function(e) {
+ ws.close();
+ t.done();
+ })
+ ws.onclose = ws.onerror = ws.onmessage = t.step_func(function() {assert_unreached()});
+});
+</script>
diff --git a/test/wpt/tests/websockets/constructor/020.html b/test/wpt/tests/websockets/constructor/020.html
new file mode 100644
index 0000000..e4d61f3
--- /dev/null
+++ b/test/wpt/tests/websockets/constructor/020.html
@@ -0,0 +1,21 @@
+<!doctype html>
+<title>WebSockets: uppercase host</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=../constants.sub.js></script>
+<meta name="variant" content="?default">
+<meta name="variant" content="?wss">
+<meta name="variant" content="?wpt_flags=h2">
+<div id=log></div>
+<script>
+async_test(function(t) {
+ var scheme = SCHEME_DOMAIN_PORT.split('://')[0];
+ var domain = SCHEME_DOMAIN_PORT.split('://')[1];
+ var ws = new WebSocket(scheme+'://'+domain.toUpperCase()+'/echo');
+ ws.onopen = t.step_func(function(e) {
+ ws.close();
+ t.done();
+ });
+ ws.onclose = ws.onerror = ws.onmessage = t.step_func(function() {assert_unreached()});
+});
+</script>
diff --git a/test/wpt/tests/websockets/constructor/021.html b/test/wpt/tests/websockets/constructor/021.html
new file mode 100644
index 0000000..d3854fe
--- /dev/null
+++ b/test/wpt/tests/websockets/constructor/021.html
@@ -0,0 +1,12 @@
+<!doctype html>
+<title>WebSockets: Same sub protocol twice</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=../constants.sub.js></script>
+<meta name="variant" content="?default">
+<meta name="variant" content="?wss">
+<meta name="variant" content="?wpt_flags=h2">
+<div id=log></div>
+<script>
+test(function() {assert_throws_dom("SyntaxError", function(){new WebSocket("ws://certo2.oslo.osa/protocol_array",["foobar, foobar"])})});
+</script>
diff --git a/test/wpt/tests/websockets/constructor/022.html b/test/wpt/tests/websockets/constructor/022.html
new file mode 100644
index 0000000..fd53c0f
--- /dev/null
+++ b/test/wpt/tests/websockets/constructor/022.html
@@ -0,0 +1,23 @@
+<!doctype html>
+<title>WebSockets: protocol array</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=../constants.sub.js></script>
+<meta name="variant" content="?default">
+<meta name="variant" content="?wss">
+<meta name="variant" content="?wpt_flags=h2">
+<div id=log></div>
+<script>
+async_test(function(t) {
+ var ws = new WebSocket(SCHEME_DOMAIN_PORT+'/protocol_array',['foobar','foobar2']);
+ ws.onmessage = t.step_func(function(e) {
+ assert_equals(ws.protocol, 'foobar');
+ ws.onclose = t.step_func(function(e) {
+ ws.onclose = t.unreached_func();
+ t.step_timeout(() => t.done(), 50);
+ });
+ ws.close();
+ });
+ ws.onerror = t.unreached_func();
+});
+</script>
diff --git a/test/wpt/tests/websockets/cookies/001.html b/test/wpt/tests/websockets/cookies/001.html
new file mode 100644
index 0000000..abec94e
--- /dev/null
+++ b/test/wpt/tests/websockets/cookies/001.html
@@ -0,0 +1,28 @@
+<!doctype html>
+<title>WebSockets: Cookie in request</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=../constants.sub.js></script>
+<meta name="variant" content="?default">
+<meta name="variant" content="?wss&wpt_flags=https">
+<meta name="variant" content="?wpt_flags=h2">
+<div id=log></div>
+<script>
+var cookie_id = ((new Date())-0) + '.' + Math.random();
+async_test(function(t) {
+ if (window.WebSocket) {
+ document.cookie = 'ws_test_'+cookie_id+'=test; Path=/';
+ }
+ t.add_cleanup(function() {
+ // remove cookie
+ document.cookie = 'ws_test_'+cookie_id+'=; Path=/; Expires=Sun, 06 Nov 1994 08:49:37 GMT';
+ });
+ var ws = new WebSocket(SCHEME_DOMAIN_PORT+'/echo-cookie');
+ ws.onmessage = t.step_func(function(e) {
+ assert_regexp_match(e.data, new RegExp('ws_test_'+cookie_id+'=test'));
+ ws.close();
+ t.done();
+ });
+ ws.onerror = ws.onclose = t.step_func(function(e) {assert_unreached(e.type)});
+});
+</script>
diff --git a/test/wpt/tests/websockets/cookies/002.html b/test/wpt/tests/websockets/cookies/002.html
new file mode 100644
index 0000000..758ce47
--- /dev/null
+++ b/test/wpt/tests/websockets/cookies/002.html
@@ -0,0 +1,26 @@
+<!doctype html>
+<title>WebSockets: Set-Cookie in response</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=../constants.sub.js></script>
+<meta name="variant" content="?default">
+<meta name="variant" content="?wss&wpt_flags=https">
+<meta name="variant" content="?wpt_flags=h2">
+<div id=log></div>
+<script>
+var cookie_id = ((new Date())-0) + '.' + Math.random();
+async_test(function(t) {
+ t.add_cleanup(function() {
+ // remove cookie
+ document.cookie = 'ws_test_'+cookie_id+'=; Path=/; Expires=Sun, 06 Nov 1994 08:49:37 GMT';
+ });
+ var ws = new WebSocket(SCHEME_DOMAIN_PORT+'/set-cookie?'+cookie_id);
+ ws.onopen = t.step_func(function(e) {
+ assert_regexp_match(document.cookie, new RegExp('ws_test_'+cookie_id+'=test'));
+ ws.close();
+ ws.onclose = null;
+ t.done();
+ });
+ ws.onerror = ws.onclose = t.step_func(function(e) {assert_unreached(e.type)});
+});
+</script>
diff --git a/test/wpt/tests/websockets/cookies/003.html b/test/wpt/tests/websockets/cookies/003.html
new file mode 100644
index 0000000..9f770ae
--- /dev/null
+++ b/test/wpt/tests/websockets/cookies/003.html
@@ -0,0 +1,34 @@
+<!doctype html>
+<title>WebSockets: sending HttpOnly cookies in ws request</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=../constants.sub.js></script>
+<meta name="variant" content="?default">
+<meta name="variant" content="?wss&wpt_flags=https">
+<meta name="variant" content="?wpt_flags=h2">
+<div id=log></div>
+<script>
+setup({explicit_done:true})
+var cookie_id = ((new Date())-0) + '.' + Math.random();
+
+var t = async_test(function(t) {
+ var iframe = document.createElement('iframe');
+ t.add_cleanup(function() {
+ // remove cookie
+ iframe.src = 'support/set-cookie.py?'+encodeURIComponent('ws_test_'+cookie_id+'=; Path=/; HttpOnly; Expires=Sun, 06 Nov 1994 08:49:37 GMT');
+ iframe.onload = done;
+ });
+ iframe.src = 'support/set-cookie.py?'+encodeURIComponent('ws_test_'+cookie_id+'=test; Path=/; HttpOnly');
+ iframe.onload = t.step_func(function() {
+ var ws = new WebSocket(SCHEME_DOMAIN_PORT+'/echo-cookie');
+ ws.onmessage = t.step_func(function(e) {
+ ws.close();
+ ws.onclose = null;
+ assert_regexp_match(e.data, new RegExp('ws_test_'+cookie_id+'=test'));
+ t.done();
+ });
+ ws.onerror = ws.onclose = t.step_func(function(e) {assert_unreached(e.type)});
+ });
+ document.body.appendChild(iframe);
+});
+</script>
diff --git a/test/wpt/tests/websockets/cookies/004.html b/test/wpt/tests/websockets/cookies/004.html
new file mode 100644
index 0000000..523daba
--- /dev/null
+++ b/test/wpt/tests/websockets/cookies/004.html
@@ -0,0 +1,31 @@
+<!doctype html>
+<title>WebSockets: setting HttpOnly cookies in ws response, checking document.cookie</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=../constants.sub.js></script>
+<meta name="variant" content="?default">
+<meta name="variant" content="?wss&wpt_flags=https">
+<div id=log></div>
+<script>
+setup({explicit_done:true})
+var cookie_id = ((new Date())-0) + '.' + Math.random();
+
+var t = async_test(function(t) {
+ var iframe = document.createElement('iframe');
+ t.add_cleanup(function() {
+ // remove cookie
+ iframe.src = 'support/set-cookie.py?'+encodeURIComponent('ws_test_'+cookie_id+'=; Path=/; HttpOnly; Expires=Sun, 06 Nov 1994 08:49:37 GMT');
+ iframe.onload = done;
+ });
+ var url = SCHEME_DOMAIN_PORT+'/set-cookie_http?'+cookie_id;
+ var ws = new WebSocket(url);
+ ws.onopen = t.step_func(function(e) {
+ ws.close();
+ ws.onclose = null;
+ assert_false(new RegExp('ws_test_'+cookie_id+'=test').test(document.cookie));
+ t.done();
+ });
+ ws.onerror = ws.onclose = t.step_func(function(e) {assert_unreached(e.type)});
+ document.body.appendChild(iframe);
+});
+</script>
diff --git a/test/wpt/tests/websockets/cookies/005.html b/test/wpt/tests/websockets/cookies/005.html
new file mode 100644
index 0000000..f3e334c
--- /dev/null
+++ b/test/wpt/tests/websockets/cookies/005.html
@@ -0,0 +1,35 @@
+<!doctype html>
+<title>WebSockets: setting HttpOnly cookies in ws response, checking ws request</title>
+<meta name=timeout content=long>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=../constants.sub.js></script>
+<meta name="variant" content="?default">
+<meta name="variant" content="?wss&wpt_flags=https">
+<div id=log></div>
+<script>
+setup({explicit_done:true})
+var cookie_id = ((new Date())-0) + '.' + Math.random();
+
+var t = async_test(function(t) {
+ var iframe = document.createElement('iframe');
+ t.add_cleanup(function() {
+ // remove cookie
+ iframe.src = 'support/set-cookie.py?'+encodeURIComponent('ws_test_'+cookie_id+'=; Path=/; HttpOnly; Expires=Sun, 06 Nov 1994 08:49:37 GMT');
+ iframe.onload = done;
+ });
+ var ws = new WebSocket(SCHEME_DOMAIN_PORT+'/set-cookie_http?'+cookie_id);
+ ws.onopen = t.step_func(function(e) {
+ var ws2 = new WebSocket(SCHEME_DOMAIN_PORT+'/echo-cookie');
+ ws2.onmessage = t.step_func(function(e) {
+ ws.close();
+ ws.onclose = null;
+ ws2.close();
+ assert_regexp_match(e.data, new RegExp('ws_test_'+cookie_id+'=test'));
+ t.done();
+ });
+ });
+ ws.onerror = ws.onclose = t.step_func(function(e) {assert_unreached(e.type)});
+ document.body.appendChild(iframe);
+})
+</script>
diff --git a/test/wpt/tests/websockets/cookies/006.html b/test/wpt/tests/websockets/cookies/006.html
new file mode 100644
index 0000000..6e12bfa
--- /dev/null
+++ b/test/wpt/tests/websockets/cookies/006.html
@@ -0,0 +1,35 @@
+<!doctype html>
+<title>WebSockets: setting Secure cookie with document.cookie, checking ws request</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=../constants.sub.js></script>
+<meta name="variant" content="?default">
+<meta name="variant" content="?wss&wpt_flags=https">
+<meta name="variant" content="?wpt_flags=h2">
+<div id=log></div>
+<script>
+var cookie_id = ((new Date())-0) + '.' + Math.random();
+async_test(function(t) {
+ if (window.WebSocket) {
+ document.cookie = 'ws_test_'+cookie_id+'=test; Path=/; Secure';
+ }
+ t.add_cleanup(function() {
+ // remove cookie
+ document.cookie = 'ws_test_'+cookie_id+'=; Path=/; Secure; Expires=Sun, 06 Nov 1994 08:49:37 GMT';
+ });
+ var ws = new WebSocket(SCHEME_DOMAIN_PORT+'/echo-cookie');
+ ws.onmessage = t.step_func(function(e) {
+ ws.close();
+ var cookie_was_seen = e.data.indexOf('ws_test_'+cookie_id+'=test') != -1;
+ if (SCHEME_DOMAIN_PORT.substr(0,3) == 'wss') {
+ assert_true(cookie_was_seen,
+ 'cookie should have been visible to wss');
+ } else {
+ assert_false(cookie_was_seen,
+ 'cookie should not have been visible to ws');
+ }
+ t.done();
+ })
+ ws.onerror = ws.onclose = t.step_func(function(e) {assert_unreached(e.type)});
+});
+</script>
diff --git a/test/wpt/tests/websockets/cookies/007.html b/test/wpt/tests/websockets/cookies/007.html
new file mode 100644
index 0000000..3e69bfc
--- /dev/null
+++ b/test/wpt/tests/websockets/cookies/007.html
@@ -0,0 +1,36 @@
+<!doctype html>
+<title>WebSockets: when to process set-cookie fields in ws response</title>
+<meta name=timeout content=long>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=../constants.sub.js></script>
+<meta name="variant" content="?default">
+<meta name="variant" content="?wss&wpt_flags=https">
+<meta name="variant" content="?wpt_flags=h2">
+<div id=log></div>
+<script>
+var cookie_id = ((new Date())-0) + '.' + Math.random();
+async_test(function(t) {
+ t.add_cleanup(function() {
+ // remove cookie
+ document.cookie = 'ws_test_'+cookie_id+'; Path=/; Expires=Sun, 06 Nov 1994 08:49:37 GMT';
+ });
+ var ws = new WebSocket(SCHEME_DOMAIN_PORT+'/set-cookie?'+cookie_id);
+ ws.onopen = t.step_func(function(e) {
+ ws.close();
+ ws.onclose = null;
+ assert_regexp_match(document.cookie, new RegExp('ws_test_'+cookie_id+'=test'));
+ t.done();
+ });
+ ws.onerror = ws.onclose = t.step_func(function() {assert_unreached()});
+
+ // sleep for 2 seconds with sync xhr
+ var sleep = new XMLHttpRequest();
+ sleep.open('GET', '/common/blank.html?pipe=trickle(d2)', false);
+ sleep.send(null);
+
+ if (new RegExp('ws_test_'+cookie_id+'=test').test(document.cookie)) {
+ assert_unreached('cookie was set during script execution');
+ }
+});
+</script>
diff --git a/test/wpt/tests/websockets/cookies/support/set-cookie.py b/test/wpt/tests/websockets/cookies/support/set-cookie.py
new file mode 100644
index 0000000..71cd8bc
--- /dev/null
+++ b/test/wpt/tests/websockets/cookies/support/set-cookie.py
@@ -0,0 +1,7 @@
+from urllib.parse import unquote
+
+from wptserve.utils import isomorphic_encode
+
+def main(request, response):
+ response.headers.set(b'Set-Cookie', isomorphic_encode(unquote(request.url_parts.query)))
+ return [(b"Content-Type", b"text/plain")], b""
diff --git a/test/wpt/tests/websockets/cookies/support/websocket-cookies-helper.sub.js b/test/wpt/tests/websockets/cookies/support/websocket-cookies-helper.sub.js
new file mode 100644
index 0000000..a7fae25
--- /dev/null
+++ b/test/wpt/tests/websockets/cookies/support/websocket-cookies-helper.sub.js
@@ -0,0 +1,57 @@
+// Set up global variables.
+(_ => {
+ var HOST = '{{host}}';
+ var CROSS_ORIGIN_HOST = '{{hosts[alt][]}}';
+ var WSS_PORT = ':{{ports[wss][0]}}';
+ var HTTPS_PORT = ':{{ports[https][0]}}';
+
+ window.WSS_ORIGIN = 'wss://' + HOST + WSS_PORT;
+ window.WSS_CROSS_SITE_ORIGIN = 'wss://' + CROSS_ORIGIN_HOST + WSS_PORT;
+ window.HTTPS_ORIGIN = 'https://' + HOST + HTTPS_PORT;
+ window.HTTPS_CROSS_SITE_ORIGIN = 'https://' + CROSS_ORIGIN_HOST + HTTPS_PORT;
+})();
+
+// Sets a cookie with each SameSite option.
+function setSameSiteCookies(origin, value) {
+ return new Promise(resolve => {
+ const ws = new WebSocket(origin + '/set-cookies-samesite?value=' + value);
+ ws.onopen = () => {
+ ws.close();
+ };
+ ws.onclose = resolve;
+ });
+}
+
+// Clears cookies set by setSameSiteCookies().
+function clearSameSiteCookies(origin) {
+ return new Promise(resolve => {
+ const ws = new WebSocket(origin + '/set-cookies-samesite?clear');
+ ws.onopen = () => ws.close();
+ ws.onclose = resolve;
+ });
+}
+
+// Gets value of Cookie header sent in request.
+function connectAndGetRequestCookiesFrom(origin) {
+ return new Promise((resolve, reject) => {
+ var ws = new WebSocket(origin + '/echo-cookie');
+ ws.onmessage = evt => {
+ var cookies = evt.data
+ resolve(cookies);
+ ws.onerror = undefined;
+ ws.onclose = undefined;
+ };
+ ws.onerror = () => reject('Unexpected error event');
+ ws.onclose = evt => reject('Unexpected close event: ' + JSON.stringify(evt));
+ });
+}
+
+// Assert that a given cookie is or is not present in the string |cookies|.
+function assertCookie(cookies, name, value, present) {
+ var assertion = present ? assert_true : assert_false;
+ var description = name + '=' + value + ' cookie is' +
+ (present ? ' ' : ' not ') + 'present.';
+ var re = new RegExp('(?:^|; )' + name + '=' + value + '(?:$|;)');
+ assertion(re.test(cookies), description);
+}
+
diff --git a/test/wpt/tests/websockets/cookies/third-party-cookie-accepted.https.html b/test/wpt/tests/websockets/cookies/third-party-cookie-accepted.https.html
new file mode 100644
index 0000000..208d297
--- /dev/null
+++ b/test/wpt/tests/websockets/cookies/third-party-cookie-accepted.https.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<meta charset="utf-8"/>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="support/websocket-cookies-helper.sub.js"></script>
+<script>
+promise_test(() => {
+ var value = '' + Math.random();
+ var origin = WSS_CROSS_SITE_ORIGIN;
+ return setSameSiteCookies(origin, value).then(
+ () => { return connectAndGetRequestCookiesFrom(origin); }
+ ).then(
+ cookies => {
+ assert_not_equals(cookies, '(none)', 'request should contain cookies.');
+ // SameSite cookies are blocked.
+ assertCookie(cookies, 'samesite-unspecified', value, false /* present */);
+ assertCookie(cookies, 'samesite-lax', value, false /* present */);
+ assertCookie(cookies, 'samesite-strict', value, false /* present */);
+ // SameSite=None third-party cookie is not blocked.
+ assertCookie(cookies, 'samesite-none', value, true /* present */);
+ return clearSameSiteCookies(origin);
+ }
+ );
+}, 'Test that third-party cookies are accepted for WebSockets.');
+</script>
diff --git a/test/wpt/tests/websockets/eventhandlers.any.js b/test/wpt/tests/websockets/eventhandlers.any.js
new file mode 100644
index 0000000..f596328
--- /dev/null
+++ b/test/wpt/tests/websockets/eventhandlers.any.js
@@ -0,0 +1,15 @@
+// META: script=constants.sub.js
+// META: variant=?default
+// META: variant=?wss
+// META: variant=?wpt_flags=h2
+
+function testEventHandler(name) {
+ test(function() {
+ var ws = CreateWebSocket(true, false);
+ assert_equals(ws["on" + name], null);
+ ws["on" + name] = function() {};
+ ws["on" + name] = 2;
+ assert_equals(ws["on" + name], null);
+ }, "Event handler for " + name + " should have [TreatNonCallableAsNull]")
+}
+["open", "error", "close", "message"].forEach(testEventHandler);
diff --git a/test/wpt/tests/websockets/extended-payload-length.html b/test/wpt/tests/websockets/extended-payload-length.html
new file mode 100644
index 0000000..92e3802
--- /dev/null
+++ b/test/wpt/tests/websockets/extended-payload-length.html
@@ -0,0 +1,72 @@
+<!doctype html>
+<title>WebSockets : Boundary-value tests for the 'Extended payload length' field in RFC6455 section5.2 'Base Framing Protocol'</title>
+<meta name=timeout content=long>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=constants.sub.js></script>
+<meta name="variant" content="?default">
+<meta name="variant" content="?wss">
+<meta name="variant" content="?wpt_flags=h2">
+<div id=log></div>
+<script>
+async_test(function(t){
+ var ws = new WebSocket(SCHEME_DOMAIN_PORT+'/echo');
+ var datasize = 125;
+ var data = null;
+ ws.onopen = t.step_func(function(e) {
+ data = new Array(datasize + 1).join('a');
+ ws.send(data);
+ });
+ ws.onmessage = t.step_func(function(e) {
+ assert_equals(e.data, data);
+ t.done();
+ });
+ ws.onclose = t.unreached_func('close event should not fire');
+}, "Application data is 125 byte which means any 'Extended payload length' field isn't used at all.");
+
+async_test(function(t){
+ var ws = new WebSocket(SCHEME_DOMAIN_PORT+'/echo');
+ var datasize = 126;
+ var data = null;
+ ws.onopen = t.step_func(function(e) {
+ data = new Array(datasize + 1).join('a');
+ ws.send(data);
+ });
+ ws.onmessage = t.step_func(function(e) {
+ assert_equals(e.data, data);
+ t.done();
+ });
+ ws.onclose = t.unreached_func('close event should not fire');
+}, "Application data is 126 byte which starts to use the 16 bit 'Extended payload length' field.");
+
+async_test(function(t){
+ var ws = new WebSocket(SCHEME_DOMAIN_PORT+'/echo');
+ var datasize = 0xFFFF;
+ var data = null;
+ ws.onopen = t.step_func(function(e) {
+ data = new Array(datasize + 1).join('a');
+ ws.send(data);
+ });
+ ws.onmessage = t.step_func(function(e) {
+ assert_equals(e.data, data);
+ t.done();
+ });
+ ws.onclose = t.unreached_func('close event should not fire');
+}, "Application data is 0xFFFF byte which means the upper bound of the 16 bit 'Extended payload length' field.");
+
+async_test(function(t){
+ var ws = new WebSocket(SCHEME_DOMAIN_PORT+'/echo');
+ var datasize = 0xFFFF + 1;
+ var data = null;
+ ws.onopen = t.step_func(function(e) {
+ data = new Array(datasize + 1).join('a');
+ ws.send(data);
+ });
+ ws.onmessage = t.step_func(function(e) {
+ assert_equals(e.data, data);
+ t.done();
+ });
+ ws.onclose = t.unreached_func('close event should not fire');
+}, "Application data is (0xFFFF + 1) byte which starts to use the 64 bit 'Extended payload length' field");
+
+</script>
diff --git a/test/wpt/tests/websockets/handlers/basic_auth_wsh.py b/test/wpt/tests/websockets/handlers/basic_auth_wsh.py
new file mode 100644
index 0000000..72e920a
--- /dev/null
+++ b/test/wpt/tests/websockets/handlers/basic_auth_wsh.py
@@ -0,0 +1,26 @@
+#!/usr/bin/python
+
+"""A WebSocket handler that enforces basic HTTP authentication. Username is
+'foo' and password is 'bar'."""
+
+
+from mod_pywebsocket.handshake import AbortedByUserException
+
+
+def web_socket_do_extra_handshake(request):
+ authorization = request.headers_in.get('authorization')
+ if authorization is None or authorization != 'Basic Zm9vOmJhcg==':
+ if request.protocol == "HTTP/2":
+ request.status = 401
+ request.headers_out["Content-Length"] = "0"
+ request.headers_out['www-authenticate'] = 'Basic realm="camelot"'
+ else:
+ request.connection.write(b'HTTP/1.1 401 Unauthorized\x0d\x0a'
+ b'Content-Length: 0\x0d\x0a'
+ b'WWW-Authenticate: Basic realm="camelot"\x0d\x0a'
+ b'\x0d\x0a')
+ raise AbortedByUserException('Abort the connection')
+
+
+def web_socket_transfer_data(request):
+ pass
diff --git a/test/wpt/tests/websockets/handlers/delayed-passive-close_wsh.py b/test/wpt/tests/websockets/handlers/delayed-passive-close_wsh.py
new file mode 100644
index 0000000..7d55b88
--- /dev/null
+++ b/test/wpt/tests/websockets/handlers/delayed-passive-close_wsh.py
@@ -0,0 +1,27 @@
+#!/usr/bin/python
+from mod_pywebsocket import common
+import time
+
+def web_socket_do_extra_handshake(request):
+ pass
+
+
+def web_socket_transfer_data(request):
+ # Wait for the close frame to arrive.
+ request.ws_stream.receive_message()
+
+
+def web_socket_passive_closing_handshake(request):
+ # Echo close status code and reason
+ code, reason = request.ws_close_code, request.ws_close_reason
+
+ # No status received is a reserved pseudo code representing an empty code,
+ # so echo back an empty code in this case.
+ if code == common.STATUS_NO_STATUS_RECEIVED:
+ code = None
+
+ # The browser may error the connection if the closing handshake takes too
+ # long, but hopefully no browser will have a timeout this short.
+ time.sleep(1)
+
+ return code, reason
diff --git a/test/wpt/tests/websockets/handlers/echo-cookie_wsh.py b/test/wpt/tests/websockets/handlers/echo-cookie_wsh.py
new file mode 100644
index 0000000..98620b6
--- /dev/null
+++ b/test/wpt/tests/websockets/handlers/echo-cookie_wsh.py
@@ -0,0 +1,12 @@
+#!/usr/bin/python
+
+from mod_pywebsocket import msgutil
+
+def web_socket_do_extra_handshake(request):
+ request.ws_cookie = request.headers_in.get('cookie')
+
+def web_socket_transfer_data(request):
+ if request.ws_cookie is not None:
+ msgutil.send_message(request, request.ws_cookie)
+ else:
+ msgutil.send_message(request, '(none)')
diff --git a/test/wpt/tests/websockets/handlers/echo-query_v13_wsh.py b/test/wpt/tests/websockets/handlers/echo-query_v13_wsh.py
new file mode 100644
index 0000000..d670e6e
--- /dev/null
+++ b/test/wpt/tests/websockets/handlers/echo-query_v13_wsh.py
@@ -0,0 +1,11 @@
+#!/usr/bin/python
+
+from mod_pywebsocket import msgutil
+
+def web_socket_do_extra_handshake(request):
+ pass
+
+def web_socket_transfer_data(request):
+ while True:
+ msgutil.send_message(request, request.unparsed_uri.split('?')[1] or '')
+ return
diff --git a/test/wpt/tests/websockets/handlers/echo-query_wsh.py b/test/wpt/tests/websockets/handlers/echo-query_wsh.py
new file mode 100644
index 0000000..3921913
--- /dev/null
+++ b/test/wpt/tests/websockets/handlers/echo-query_wsh.py
@@ -0,0 +1,9 @@
+#!/usr/bin/python
+
+from mod_pywebsocket import msgutil
+
+def web_socket_do_extra_handshake(request):
+ pass # Always accept.
+
+def web_socket_transfer_data(request):
+ msgutil.send_message(request, request.unparsed_uri.split('?', 1)[1] or '')
diff --git a/test/wpt/tests/websockets/handlers/echo_close_data_wsh.py b/test/wpt/tests/websockets/handlers/echo_close_data_wsh.py
new file mode 100644
index 0000000..31ffcbb
--- /dev/null
+++ b/test/wpt/tests/websockets/handlers/echo_close_data_wsh.py
@@ -0,0 +1,20 @@
+#!/usr/bin/python
+
+_GOODBYE_MESSAGE = u'Goodbye'
+
+def web_socket_do_extra_handshake(request):
+ # This example handler accepts any request. See origin_check_wsh.py for how
+ # to reject access from untrusted scripts based on origin value.
+
+ pass # Always accept.
+
+
+def web_socket_transfer_data(request):
+ while True:
+ line = request.ws_stream.receive_message()
+ if line is None:
+ return
+ if isinstance(line, str):
+ if line == _GOODBYE_MESSAGE:
+ return
+ request.ws_stream.send_message(line, binary=False)
diff --git a/test/wpt/tests/websockets/handlers/echo_exit_wsh.py b/test/wpt/tests/websockets/handlers/echo_exit_wsh.py
new file mode 100644
index 0000000..8f6f7f8
--- /dev/null
+++ b/test/wpt/tests/websockets/handlers/echo_exit_wsh.py
@@ -0,0 +1,19 @@
+#!/usr/bin/python
+
+_GOODBYE_MESSAGE = u'Goodbye'
+
+def web_socket_do_extra_handshake(request):
+ # This example handler accepts any request. See origin_check_wsh.py for how
+ # to reject access from untrusted scripts based on origin value.
+
+ pass # Always accept.
+
+
+def web_socket_transfer_data(request):
+ while True:
+ line = request.ws_stream.receive_message()
+ if line is None:
+ return
+ if isinstance(line, str):
+ if line == _GOODBYE_MESSAGE:
+ return
diff --git a/test/wpt/tests/websockets/handlers/echo_raw_wsh.py b/test/wpt/tests/websockets/handlers/echo_raw_wsh.py
new file mode 100644
index 0000000..e1fc266
--- /dev/null
+++ b/test/wpt/tests/websockets/handlers/echo_raw_wsh.py
@@ -0,0 +1,16 @@
+#!/usr/bin/python
+
+from mod_pywebsocket import msgutil
+
+
+def web_socket_do_extra_handshake(request):
+ pass # Always accept.
+
+def web_socket_transfer_data(request):
+ while True:
+ line = msgutil.receive_message(request)
+ if line == b'exit':
+ return
+
+ if line is not None:
+ request.connection.write(line)
diff --git a/test/wpt/tests/websockets/handlers/echo_wsh.py b/test/wpt/tests/websockets/handlers/echo_wsh.py
new file mode 100644
index 0000000..7367b70
--- /dev/null
+++ b/test/wpt/tests/websockets/handlers/echo_wsh.py
@@ -0,0 +1,36 @@
+#!/usr/bin/python
+
+from mod_pywebsocket import common
+
+_GOODBYE_MESSAGE = u'Goodbye'
+
+def web_socket_do_extra_handshake(request):
+ # This example handler accepts any request. See origin_check_wsh.py for how
+ # to reject access from untrusted scripts based on origin value.
+ if request.ws_requested_protocols:
+ if "echo" in request.ws_requested_protocols:
+ request.ws_protocol = "echo"
+
+
+def web_socket_transfer_data(request):
+ while True:
+ line = request.ws_stream.receive_message()
+ if line is None:
+ return
+ if isinstance(line, str):
+ request.ws_stream.send_message(line, binary=False)
+ if line == _GOODBYE_MESSAGE:
+ return
+ else:
+ request.ws_stream.send_message(line, binary=True)
+
+def web_socket_passive_closing_handshake(request):
+ # Echo close status code and reason
+ code, reason = request.ws_close_code, request.ws_close_reason
+
+ # No status received is a reserved pseudo code representing an empty code,
+ # so echo back an empty code in this case.
+ if code == common.STATUS_NO_STATUS_RECEIVED:
+ code = None
+
+ return code, reason
diff --git a/test/wpt/tests/websockets/handlers/empty-message_wsh.py b/test/wpt/tests/websockets/handlers/empty-message_wsh.py
new file mode 100644
index 0000000..0eb107f
--- /dev/null
+++ b/test/wpt/tests/websockets/handlers/empty-message_wsh.py
@@ -0,0 +1,13 @@
+#!/usr/bin/python
+
+from mod_pywebsocket import msgutil
+
+def web_socket_do_extra_handshake(request):
+ pass # Always accept.
+
+def web_socket_transfer_data(request):
+ line = msgutil.receive_message(request)
+ if line == "":
+ msgutil.send_message(request, 'pass')
+ else:
+ msgutil.send_message(request, 'fail')
diff --git a/test/wpt/tests/websockets/handlers/handshake_no_extensions_wsh.py b/test/wpt/tests/websockets/handlers/handshake_no_extensions_wsh.py
new file mode 100644
index 0000000..0d0f0a8
--- /dev/null
+++ b/test/wpt/tests/websockets/handlers/handshake_no_extensions_wsh.py
@@ -0,0 +1,9 @@
+#!/usr/bin/python
+
+
+def web_socket_do_extra_handshake(request):
+ request.ws_extension_processors = []
+
+
+def web_socket_transfer_data(request):
+ pass
diff --git a/test/wpt/tests/websockets/handlers/handshake_no_protocol_wsh.py b/test/wpt/tests/websockets/handlers/handshake_no_protocol_wsh.py
new file mode 100644
index 0000000..ffc2ae8
--- /dev/null
+++ b/test/wpt/tests/websockets/handlers/handshake_no_protocol_wsh.py
@@ -0,0 +1,8 @@
+#!/usr/bin/python
+
+def web_socket_do_extra_handshake(request):
+ # Trick pywebsocket into believing no subprotocol was requested.
+ request.ws_requested_protocols = None
+
+def web_socket_transfer_data(request):
+ pass
diff --git a/test/wpt/tests/websockets/handlers/handshake_protocol_wsh.py b/test/wpt/tests/websockets/handlers/handshake_protocol_wsh.py
new file mode 100644
index 0000000..2ca20c0
--- /dev/null
+++ b/test/wpt/tests/websockets/handlers/handshake_protocol_wsh.py
@@ -0,0 +1,7 @@
+#!/usr/bin/python
+
+def web_socket_do_extra_handshake(request):
+ request.ws_protocol = 'foobar'
+
+def web_socket_transfer_data(request):
+ pass \ No newline at end of file
diff --git a/test/wpt/tests/websockets/handlers/handshake_sleep_2_wsh.py b/test/wpt/tests/websockets/handlers/handshake_sleep_2_wsh.py
new file mode 100644
index 0000000..78de7c7
--- /dev/null
+++ b/test/wpt/tests/websockets/handlers/handshake_sleep_2_wsh.py
@@ -0,0 +1,9 @@
+#!/usr/bin/python
+
+import time
+
+def web_socket_do_extra_handshake(request):
+ time.sleep(2)
+
+def web_socket_transfer_data(request):
+ pass
diff --git a/test/wpt/tests/websockets/handlers/invalid_wsh.py b/test/wpt/tests/websockets/handlers/invalid_wsh.py
new file mode 100644
index 0000000..4bfc3ce
--- /dev/null
+++ b/test/wpt/tests/websockets/handlers/invalid_wsh.py
@@ -0,0 +1,8 @@
+#!/usr/bin/python
+
+def web_socket_do_extra_handshake(request):
+ request.connection.write(b"FOO BAR BAZ\r\n\r\n")
+
+
+def web_socket_transfer_data(request):
+ pass
diff --git a/test/wpt/tests/websockets/handlers/msg_channel_wsh.py b/test/wpt/tests/websockets/handlers/msg_channel_wsh.py
new file mode 100644
index 0000000..7a66646
--- /dev/null
+++ b/test/wpt/tests/websockets/handlers/msg_channel_wsh.py
@@ -0,0 +1,234 @@
+#!/usr/bin/python
+import json
+import logging
+import urllib
+import threading
+import traceback
+from queue import Empty
+
+from mod_pywebsocket import stream, msgutil
+from wptserve import stash as stashmod
+
+logger = logging.getLogger()
+
+address, authkey = stashmod.load_env_config()
+stash = stashmod.Stash("msg_channel", address=address, authkey=authkey)
+
+# Backend for websocket based channels.
+#
+# Each socket connection has a uuid identifying the channel and a
+# direction which is either "read" or "write". There can be only 1
+# "read" channel per uuid, but multiple "write" channels
+# (i.e. multiple producer, single consumer).
+#
+# The websocket connection URL contains the uuid and the direction as
+# named query parameters.
+#
+# Channels are backed by a queue which is stored in the stash (one
+# queue per uuid).
+#
+# The representation of a queue in the stash is a tuple (queue,
+# has_reader, writer_count). The first field is the queue itself, the
+# latter are effectively reference counts for reader channels (which
+# is zero or one, represented by a bool) and writer channels. Once
+# both counts drop to zero the queue can be deleted.
+#
+# Entries on the queue itself are formed of (command, data) pairs. The
+# command can be either "close", signalling the socket is closing and
+# the reference count on the channel should be decremented, or
+# "message", which indicates a message.
+
+
+def log(uuid, msg, level="debug"):
+ msg = f"{uuid}: {msg}"
+ getattr(logger, level)(msg)
+
+
+def web_socket_do_extra_handshake(request):
+ return
+
+
+def web_socket_transfer_data(request):
+ """Handle opening a websocket connection."""
+
+ uuid, direction = parse_request(request)
+ log(uuid, f"Got web_socket_transfer_data {direction}")
+
+ # Get or create the relevant queue from the stash and update the refcount
+ with stash.lock:
+ value = stash.take(uuid)
+ if value is None:
+ queue = stash.get_queue()
+ if direction == "read":
+ has_reader = True
+ writer_count = 0
+ else:
+ has_reader = False
+ writer_count = 1
+ else:
+ queue, has_reader, writer_count = value
+ if direction == "read":
+ if has_reader:
+ raise ValueError("Tried to start multiple readers for the same queue")
+ has_reader = True
+ else:
+ writer_count += 1
+
+ stash.put(uuid, (queue, has_reader, writer_count))
+
+ if direction == "read":
+ run_read(request, uuid, queue)
+ elif direction == "write":
+ run_write(request, uuid, queue)
+
+ log(uuid, f"transfer_data loop exited {direction}")
+ close_channel(uuid, direction)
+
+
+def web_socket_passive_closing_handshake(request):
+ """Handle a client initiated close.
+
+ When the client closes a reader, put a message in the message
+ queue indicating the close. For a writer we don't need special
+ handling here because receive_message in run_read will return an
+ empty message in this case, so that loop will exit on its own.
+ """
+ uuid, direction = parse_request(request)
+ log(uuid, f"Got web_socket_passive_closing_handshake {direction}")
+
+ if direction == "read":
+ with stash.lock:
+ data = stash.take(uuid)
+ stash.put(uuid, data)
+ if data is not None:
+ queue = data[0]
+ queue.put(("close", None))
+
+ return request.ws_close_code, request.ws_close_reason
+
+
+def parse_request(request):
+ query = request.unparsed_uri.split('?')[1]
+ GET = dict(urllib.parse.parse_qsl(query))
+ uuid = GET["uuid"]
+ direction = GET["direction"]
+ return uuid, direction
+
+
+def wait_for_close(request, uuid, queue):
+ """Listen for messages on the socket for a read connection to a channel."""
+ closed = False
+ while not closed:
+ try:
+ msg = request.ws_stream.receive_message()
+ if msg is None:
+ break
+ try:
+ cmd, data = json.loads(msg)
+ except ValueError:
+ cmd = None
+ if cmd == "close":
+ closed = True
+ log(uuid, "Got client initiated close")
+ else:
+ log(uuid, f"Unexpected message on read socket {msg}", "warning")
+ except Exception:
+ if not (request.server_terminated or request.client_terminated):
+ log(uuid, f"Got exception in wait_for_close\n{traceback.format_exc()}")
+ closed = True
+
+ if not request.server_terminated:
+ queue.put(("close", None))
+
+
+def run_read(request, uuid, queue):
+ """Main loop for a read-type connection.
+
+ This mostly just listens on the queue for new messages of the
+ form (message, data). Supported messages are:
+ message - Send `data` on the WebSocket
+ close - Close the reader queue
+
+ In addition there's a thread that listens for messages on the
+ socket itself. Typically this socket shouldn't recieve any
+ messages, but it can recieve an explicit "close" message,
+ indicating the socket should be disconnected.
+ """
+
+ close_thread = threading.Thread(target=wait_for_close, args=(request, uuid, queue), daemon=True)
+ close_thread.start()
+
+ while True:
+ try:
+ data = queue.get(True, 1)
+ except Empty:
+ if request.server_terminated or request.client_terminated:
+ break
+ else:
+ cmd, body = data
+ log(uuid, f"queue.get ({cmd}, {body})")
+ if cmd == "close":
+ break
+ if cmd == "message":
+ msgutil.send_message(request, json.dumps(body))
+ else:
+ log(uuid, f"Unknown queue command {cmd}", level="warning")
+
+
+def run_write(request, uuid, queue):
+ """Main loop for a write-type connection.
+
+ Messages coming over the socket have the format (command, data).
+ The recognised commands are:
+ message - Send the message `data` over the channel.
+ disconnectReader - Close the reader connection for this channel.
+ delete - Force-delete the entire channel and the underlying queue.
+ """
+ while True:
+ msg = request.ws_stream.receive_message()
+ if msg is None:
+ break
+ cmd, body = json.loads(msg)
+ if cmd == "disconnectReader":
+ queue.put(("close", None))
+ elif cmd == "message":
+ log(uuid, f"queue.put ({cmd}, {body})")
+ queue.put((cmd, body))
+ elif cmd == "delete":
+ close_channel(uuid, None)
+
+
+def close_channel(uuid, direction):
+ """Update the channel state in the stash when closing a connection
+
+ This updates the stash entry, including refcounts, once a
+ connection to a channel is closed.
+
+ Params:
+ uuid - the UUID of the channel being closed.
+ direction - "read" if a read connection was closed, "write" if a
+ write connection was closed, None to remove the
+ underlying queue from the stash entirely.
+
+ """
+ log(uuid, f"Got close_channel {direction}")
+ with stash.lock:
+ data = stash.take(uuid)
+ if data is None:
+ log(uuid, "Message queue already deleted")
+ return
+ if direction is None:
+ # Return without replacing the channel in the stash
+ log(uuid, "Force deleting message queue")
+ return
+ queue, has_reader, writer_count = data
+ if direction == "read":
+ has_reader = False
+ else:
+ writer_count -= 1
+
+ if has_reader or writer_count > 0 or not queue.empty():
+ log(uuid, f"Updating refcount {has_reader}, {writer_count}")
+ stash.put(uuid, (queue, has_reader, writer_count))
+ else:
+ log(uuid, "Deleting message queue")
diff --git a/test/wpt/tests/websockets/handlers/origin_wsh.py b/test/wpt/tests/websockets/handlers/origin_wsh.py
new file mode 100644
index 0000000..ce5f3a7
--- /dev/null
+++ b/test/wpt/tests/websockets/handlers/origin_wsh.py
@@ -0,0 +1,11 @@
+#!/usr/bin/python
+
+from mod_pywebsocket import msgutil
+
+
+def web_socket_do_extra_handshake(request):
+ pass # Always accept.
+
+
+def web_socket_transfer_data(request):
+ msgutil.send_message(request, request.ws_origin)
diff --git a/test/wpt/tests/websockets/handlers/protocol_array_wsh.py b/test/wpt/tests/websockets/handlers/protocol_array_wsh.py
new file mode 100644
index 0000000..be24ee0
--- /dev/null
+++ b/test/wpt/tests/websockets/handlers/protocol_array_wsh.py
@@ -0,0 +1,14 @@
+#!/usr/bin/python
+
+from mod_pywebsocket import msgutil
+
+def web_socket_do_extra_handshake(request):
+ line = request.headers_in.get('sec-websocket-protocol')
+ request.ws_protocol = line.split(',', 1)[0]
+
+#pass
+
+def web_socket_transfer_data(request):
+ while True:
+ msgutil.send_message(request, request.ws_protocol)
+ return
diff --git a/test/wpt/tests/websockets/handlers/protocol_wsh.py b/test/wpt/tests/websockets/handlers/protocol_wsh.py
new file mode 100644
index 0000000..10bdf33
--- /dev/null
+++ b/test/wpt/tests/websockets/handlers/protocol_wsh.py
@@ -0,0 +1,12 @@
+#!/usr/bin/python
+
+from mod_pywebsocket import msgutil
+
+def web_socket_do_extra_handshake(request):
+ request.ws_protocol = request.headers_in.get('sec-websocket-protocol')
+#pass
+
+def web_socket_transfer_data(request):
+ while True:
+ msgutil.send_message(request, request.ws_protocol)
+ return
diff --git a/test/wpt/tests/websockets/handlers/receive-backpressure_wsh.py b/test/wpt/tests/websockets/handlers/receive-backpressure_wsh.py
new file mode 100644
index 0000000..9c2e470
--- /dev/null
+++ b/test/wpt/tests/websockets/handlers/receive-backpressure_wsh.py
@@ -0,0 +1,14 @@
+#!/usr/bin/python
+
+import time
+
+
+def web_socket_do_extra_handshake(request):
+ # Turn off permessage-deflate, otherwise it shrinks our 8MB buffer to 8KB.
+ request.ws_extension_processors = []
+
+
+def web_socket_transfer_data(request):
+ # Wait two seconds to cause backpressure.
+ time.sleep(2);
+ request.ws_stream.receive_message()
diff --git a/test/wpt/tests/websockets/handlers/receive-many-with-backpressure_wsh.py b/test/wpt/tests/websockets/handlers/receive-many-with-backpressure_wsh.py
new file mode 100644
index 0000000..8e35bee
--- /dev/null
+++ b/test/wpt/tests/websockets/handlers/receive-many-with-backpressure_wsh.py
@@ -0,0 +1,23 @@
+# Sleep to build backpressure, receive messages, and send back their length.
+# Used by send-many-64K-messages-with-backpressure.any.js.
+
+
+import time
+
+
+def web_socket_do_extra_handshake(request):
+ # Compression will interfere with backpressure, so disable the
+ # permessage-delate extension.
+ request.ws_extension_processors = []
+
+
+def web_socket_transfer_data(request):
+ while True:
+ # Don't read the message immediately, so backpressure can build.
+ time.sleep(0.1)
+ line = request.ws_stream.receive_message()
+ if line is None:
+ return
+ # Send back the size of the message as acknowledgement that it was
+ # received.
+ request.ws_stream.send_message(str(len(line)), binary=False)
diff --git a/test/wpt/tests/websockets/handlers/referrer_wsh.py b/test/wpt/tests/websockets/handlers/referrer_wsh.py
new file mode 100644
index 0000000..9df652d
--- /dev/null
+++ b/test/wpt/tests/websockets/handlers/referrer_wsh.py
@@ -0,0 +1,12 @@
+#!/usr/bin/python
+
+from mod_pywebsocket import msgutil
+
+def web_socket_do_extra_handshake(request):
+ pass
+
+def web_socket_transfer_data(request):
+ referrer = request.headers_in.get("referer")
+ if referrer is None:
+ referrer = "MISSING AS PER FETCH"
+ msgutil.send_message(request, referrer)
diff --git a/test/wpt/tests/websockets/handlers/send-backpressure_wsh.py b/test/wpt/tests/websockets/handlers/send-backpressure_wsh.py
new file mode 100644
index 0000000..d3288d0
--- /dev/null
+++ b/test/wpt/tests/websockets/handlers/send-backpressure_wsh.py
@@ -0,0 +1,39 @@
+#!/usr/bin/python
+
+import time
+
+# The amount of internal buffering a WebSocket connection has is not
+# standardised, and varies depending upon the OS. Setting this number too small
+# will result in false negatives, as the entire message gets buffered. Setting
+# this number too large will result in false positives, when it takes more than
+# 2 seconds to transmit the message anyway. This number was arrived at by
+# trial-and-error.
+MESSAGE_SIZE = 1024 * 1024
+
+# With Windows 10 and Python 3, the OS will buffer an entire message in memory
+# and return from send() immediately, even if it is very large. To work around
+# this problem, send multiple messages.
+MESSAGE_COUNT = 16
+
+
+def web_socket_do_extra_handshake(request):
+ # Turn off permessage-deflate, otherwise it shrinks our big message to a
+ # tiny message.
+ request.ws_extension_processors = []
+
+
+def web_socket_transfer_data(request):
+ # Send empty message to fill the ReadableStream queue
+ request.ws_stream.send_message(b'', binary=True)
+
+ # TODO(ricea@chromium.org): Use time.perf_counter() when migration to python
+ # 3 is complete. time.time() can go backwards.
+ start_time = time.time()
+
+ # The large messages that will be blocked by backpressure.
+ for i in range(MESSAGE_COUNT):
+ request.ws_stream.send_message(b' ' * MESSAGE_SIZE, binary=True)
+
+ # Report the time taken to send the large message.
+ request.ws_stream.send_message(str(time.time() - start_time),
+ binary=False)
diff --git a/test/wpt/tests/websockets/handlers/set-cookie-secure_wsh.py b/test/wpt/tests/websockets/handlers/set-cookie-secure_wsh.py
new file mode 100644
index 0000000..052a882
--- /dev/null
+++ b/test/wpt/tests/websockets/handlers/set-cookie-secure_wsh.py
@@ -0,0 +1,11 @@
+#!/usr/bin/python
+import urllib
+
+
+def web_socket_do_extra_handshake(request):
+ url_parts = urllib.parse.urlsplit(request.uri)
+ request.extra_headers.append(('Set-Cookie', 'ws_test_'+(url_parts.query or '')+'=test; Secure; Path=/'))
+
+def web_socket_transfer_data(request):
+ # Expect close() from user agent.
+ request.ws_stream.receive_message()
diff --git a/test/wpt/tests/websockets/handlers/set-cookie_http_wsh.py b/test/wpt/tests/websockets/handlers/set-cookie_http_wsh.py
new file mode 100644
index 0000000..5331091
--- /dev/null
+++ b/test/wpt/tests/websockets/handlers/set-cookie_http_wsh.py
@@ -0,0 +1,11 @@
+#!/usr/bin/python
+
+import urllib
+
+def web_socket_do_extra_handshake(request):
+ url_parts = urllib.parse.urlsplit(request.uri)
+ request.extra_headers.append(('Set-Cookie', 'ws_test_'+(url_parts.query or '')+'=test; Path=/; HttpOnly\x0D\x0ASec-WebSocket-Origin: '+request.ws_origin))
+
+def web_socket_transfer_data(request):
+ # Expect close from user agent.
+ request.ws_stream.receive_message()
diff --git a/test/wpt/tests/websockets/handlers/set-cookie_wsh.py b/test/wpt/tests/websockets/handlers/set-cookie_wsh.py
new file mode 100644
index 0000000..5fe3ad9
--- /dev/null
+++ b/test/wpt/tests/websockets/handlers/set-cookie_wsh.py
@@ -0,0 +1,11 @@
+#!/usr/bin/python
+import urllib
+
+
+def web_socket_do_extra_handshake(request):
+ url_parts = urllib.parse.urlsplit(request.uri)
+ request.extra_headers.append(('Set-Cookie', 'ws_test_'+(url_parts.query or '')+'=test; Path=/'))
+
+def web_socket_transfer_data(request):
+ # Expect close from user agent.
+ request.ws_stream.receive_message()
diff --git a/test/wpt/tests/websockets/handlers/set-cookies-samesite_wsh.py b/test/wpt/tests/websockets/handlers/set-cookies-samesite_wsh.py
new file mode 100644
index 0000000..59f0a4a
--- /dev/null
+++ b/test/wpt/tests/websockets/handlers/set-cookies-samesite_wsh.py
@@ -0,0 +1,25 @@
+import urllib
+
+
+def web_socket_do_extra_handshake(request):
+ url_parts = urllib.parse.urlsplit(request.uri)
+ max_age = ""
+ if "clear" in url_parts.query:
+ max_age = "; Max-Age=0"
+ value = "1"
+ if "value" in url_parts.query:
+ value = urllib.parse.parse_qs(url_parts.query)["value"][0]
+ cookies = [
+ "samesite-unspecified={}; Path=/".format(value) + max_age,
+ "samesite-lax={}; Path=/; SameSite=Lax".format(value) + max_age,
+ "samesite-strict={}; Path=/; SameSite=Strict".format(value) + max_age,
+ # SameSite=None cookies must be Secure.
+ "samesite-none={}; Path=/; SameSite=None; Secure".format(value) + max_age
+ ]
+ for cookie in cookies:
+ request.extra_headers.append(("Set-Cookie", cookie))
+
+
+def web_socket_transfer_data(request):
+ # Expect close() from user agent.
+ request.ws_stream.receive_message()
diff --git a/test/wpt/tests/websockets/handlers/simple_handshake_wsh.py b/test/wpt/tests/websockets/handlers/simple_handshake_wsh.py
new file mode 100644
index 0000000..ad46687
--- /dev/null
+++ b/test/wpt/tests/websockets/handlers/simple_handshake_wsh.py
@@ -0,0 +1,35 @@
+#!/usr/bin/python
+
+from mod_pywebsocket import common, stream
+from mod_pywebsocket.handshake import AbortedByUserException, hybi
+
+
+def web_socket_do_extra_handshake(request):
+ # Send simple response header. This test implements the handshake manually,
+ # so that we can send the header in the same packet as the close frame.
+ msg = (b'HTTP/1.1 101 Switching Protocols:\x0D\x0A'
+ b'Connection: Upgrade\x0D\x0A'
+ b'Upgrade: WebSocket\x0D\x0A'
+ b'Set-Cookie: ws_test=test\x0D\x0A'
+ b'Sec-WebSocket-Origin: %s\x0D\x0A'
+ b'Sec-WebSocket-Accept: %s\x0D\x0A\x0D\x0A') % (request.ws_origin.encode(
+ 'UTF-8'), hybi.compute_accept_from_unicode(request.headers_in.get(common.SEC_WEBSOCKET_KEY_HEADER)))
+ # Create a clean close frame.
+ close_body = stream.create_closing_handshake_body(1001, 'PASS')
+ close_frame = stream.create_close_frame(close_body)
+ # Concatenate the header and the close frame and write them to the socket.
+ request.connection.write(msg + close_frame)
+ # Wait for the responding close frame from the user agent. It's not possible
+ # to use the stream methods at this point because the stream hasn't been
+ # established from pywebsocket's point of view. Instead just read the
+ # correct number of bytes.
+ # Warning: reading the wrong number of bytes here will make the test
+ # flaky.
+ MASK_LENGTH = 4
+ request.connection.read(len(close_frame) + MASK_LENGTH)
+ # Close the socket without pywebsocket sending its own handshake response.
+ raise AbortedByUserException('Abort the connection')
+
+
+def web_socket_transfer_data(request):
+ pass
diff --git a/test/wpt/tests/websockets/handlers/sleep_10_v13_wsh.py b/test/wpt/tests/websockets/handlers/sleep_10_v13_wsh.py
new file mode 100644
index 0000000..b0f1dde
--- /dev/null
+++ b/test/wpt/tests/websockets/handlers/sleep_10_v13_wsh.py
@@ -0,0 +1,24 @@
+#!/usr/bin/python
+
+import sys, urllib, time
+from mod_pywebsocket import msgutil
+
+def web_socket_do_extra_handshake(request):
+ request.connection.write(b'x')
+ time.sleep(2)
+ request.connection.write(b'x')
+ time.sleep(2)
+ request.connection.write(b'x')
+ time.sleep(2)
+ request.connection.write(b'x')
+ time.sleep(2)
+ request.connection.write(b'x')
+ time.sleep(2)
+ return
+
+def web_socket_transfer_data(request):
+ while True:
+ line = msgutil.receive_message(request)
+ if line == 'Goodbye':
+ return
+ request.ws_stream.send_message(line, binary=False)
diff --git a/test/wpt/tests/websockets/handlers/stash_responder_blocking_wsh.py b/test/wpt/tests/websockets/handlers/stash_responder_blocking_wsh.py
new file mode 100644
index 0000000..10ecdfe
--- /dev/null
+++ b/test/wpt/tests/websockets/handlers/stash_responder_blocking_wsh.py
@@ -0,0 +1,45 @@
+#!/usr/bin/python
+import json
+import threading
+import wptserve.stash
+from mod_pywebsocket import msgutil
+
+address, authkey = wptserve.stash.load_env_config()
+path = "/stash_responder_blocking"
+stash = wptserve.stash.Stash(path, address=address, authkey=authkey)
+cv = threading.Condition()
+
+def handle_set(key, value):
+ with cv:
+ stash.put(key, value)
+ cv.notify_all()
+
+def handle_get(key):
+ with cv:
+ while True:
+ value = stash.take(key)
+ if value is not None:
+ return value
+ cv.wait()
+
+def web_socket_do_extra_handshake(request):
+ pass
+
+def web_socket_transfer_data(request):
+ line = request.ws_stream.receive_message()
+
+ query = json.loads(line)
+ action = query["action"]
+ key = query["key"]
+
+ if action == "set":
+ value = query["value"]
+ handle_set(key, value)
+ response = {}
+ elif action == "get":
+ value = handle_get(key)
+ response = {"value": value}
+ else:
+ response = {}
+
+ msgutil.send_message(request, json.dumps(response))
diff --git a/test/wpt/tests/websockets/handlers/stash_responder_wsh.py b/test/wpt/tests/websockets/handlers/stash_responder_wsh.py
new file mode 100644
index 0000000..d18ad3b
--- /dev/null
+++ b/test/wpt/tests/websockets/handlers/stash_responder_wsh.py
@@ -0,0 +1,45 @@
+#!/usr/bin/python
+import json
+import urllib
+from mod_pywebsocket import msgutil
+from wptserve import stash
+
+address, authkey = stash.load_env_config()
+stash = stash.Stash("/stash_responder", address=address, authkey=authkey)
+
+def web_socket_do_extra_handshake(request):
+ return
+
+def web_socket_transfer_data(request):
+ while True:
+ line = request.ws_stream.receive_message()
+ if line == "echo":
+ query = request.unparsed_uri.split('?')[1]
+ GET = dict(urllib.parse.parse_qsl(query))
+
+ # TODO(kristijanburnik): This code should be reused from
+ # /mixed-content/generic/expect.py or implemented more generally
+ # for other tests.
+ path = GET.get("path", request.unparsed_uri.split('?')[0])
+ key = GET["key"]
+ action = GET["action"]
+
+ if action == "put":
+ value = GET["value"]
+ stash.take(key=key, path=path)
+ stash.put(key=key, value=value, path=path)
+ response_data = json.dumps({"status": "success", "result": key})
+ elif action == "purge":
+ value = stash.take(key=key, path=path)
+ response_data = json.dumps({"status": "success", "result": value})
+ elif action == "take":
+ value = stash.take(key=key, path=path)
+ if value is None:
+ status = "allowed"
+ else:
+ status = "blocked"
+ response_data = json.dumps({"status": status, "result": value})
+
+ msgutil.send_message(request, response_data)
+
+ return
diff --git a/test/wpt/tests/websockets/handlers/wrong_accept_key_wsh.py b/test/wpt/tests/websockets/handlers/wrong_accept_key_wsh.py
new file mode 100644
index 0000000..43240e1
--- /dev/null
+++ b/test/wpt/tests/websockets/handlers/wrong_accept_key_wsh.py
@@ -0,0 +1,19 @@
+#!/usr/bin/python
+
+import sys, urllib, time
+
+
+def web_socket_do_extra_handshake(request):
+ msg = (b'HTTP/1.1 101 Switching Protocols:\x0D\x0A'
+ b'Connection: Upgrade\x0D\x0A'
+ b'Upgrade: WebSocket\x0D\x0A'
+ b'Sec-WebSocket-Origin: %s\x0D\x0A'
+ b'Sec-WebSocket-Accept: thisisawrongacceptkey\x0D\x0A\x0D\x0A') % request.ws_origin.encode('UTF-8')
+ request.connection.write(msg)
+ return
+
+
+def web_socket_transfer_data(request):
+ while True:
+ request.ws_stream.send_message('test', binary=False)
+ return
diff --git a/test/wpt/tests/websockets/idlharness.any.js b/test/wpt/tests/websockets/idlharness.any.js
new file mode 100644
index 0000000..653cc36
--- /dev/null
+++ b/test/wpt/tests/websockets/idlharness.any.js
@@ -0,0 +1,17 @@
+// META: script=/resources/WebIDLParser.js
+// META: script=/resources/idlharness.js
+
+// https://websockets.spec.whatwg.org/
+
+"use strict";
+
+idl_test(
+ ['websockets'],
+ ['html', 'dom'],
+ idl_array => {
+ idl_array.add_objects({
+ WebSocket: ['new WebSocket("ws://invalid")'],
+ CloseEvent: ['new CloseEvent("close")'],
+ });
+ }
+);
diff --git a/test/wpt/tests/websockets/interfaces/CloseEvent/clean-close.html b/test/wpt/tests/websockets/interfaces/CloseEvent/clean-close.html
new file mode 100644
index 0000000..8614028
--- /dev/null
+++ b/test/wpt/tests/websockets/interfaces/CloseEvent/clean-close.html
@@ -0,0 +1,24 @@
+<!doctype html>
+<title>WebSockets: wasClean, true</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=../../constants.sub.js></script>
+<meta name="variant" content="?default">
+<meta name="variant" content="?wss">
+<meta name="variant" content="?wpt_flags=h2">
+<div id=log></div>
+<script>
+async_test(function(t) {
+ var ws = new WebSocket(SCHEME_DOMAIN_PORT+'/echo');
+ ws.onopen = t.step_func(function(e) {
+ ws.send('Test');
+ });
+ ws.onmessage = t.step_func(function(e) {
+ ws.close();
+ });
+ ws.onclose = t.step_func(function(e) {
+ assert_equals(e.wasClean,true);
+ t.done();
+ });
+});
+</script>
diff --git a/test/wpt/tests/websockets/interfaces/CloseEvent/constructor.html b/test/wpt/tests/websockets/interfaces/CloseEvent/constructor.html
new file mode 100644
index 0000000..1ed86bd
--- /dev/null
+++ b/test/wpt/tests/websockets/interfaces/CloseEvent/constructor.html
@@ -0,0 +1,35 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>CloseEvent: constructor</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<div id=log></div>
+<script>
+test(function() {
+ var event = new CloseEvent("foo");
+ assert_true(event instanceof CloseEvent, "should be a CloseEvent");
+ assert_equals(event.type, "foo");
+ assert_false(event.bubbles, "bubbles");
+ assert_false(event.cancelable, "cancelable");
+ assert_false(event.wasClean, "wasClean");
+ assert_equals(event.code, 0);
+ assert_equals(event.reason, "");
+}, "new CloseEvent() without dictionary");
+
+test(function() {
+ var event = new CloseEvent("foo", {
+ bubbles: true,
+ cancelable: true,
+ wasClean: true,
+ code: 7,
+ reason: "x",
+ });
+ assert_true(event instanceof CloseEvent, "should be a CloseEvent");
+ assert_equals(event.type, "foo");
+ assert_true(event.bubbles, "bubbles");
+ assert_true(event.cancelable, "cancelable");
+ assert_true(event.wasClean, "wasClean");
+ assert_equals(event.code, 7);
+ assert_equals(event.reason, "x");
+}, "new CloseEvent() with dictionary");
+</script>
diff --git a/test/wpt/tests/websockets/interfaces/CloseEvent/historical.html b/test/wpt/tests/websockets/interfaces/CloseEvent/historical.html
new file mode 100644
index 0000000..24528a8
--- /dev/null
+++ b/test/wpt/tests/websockets/interfaces/CloseEvent/historical.html
@@ -0,0 +1,12 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>CloseEvent: historical initialization</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<div id=log></div>
+<script>
+test(function() {
+ assert_false("initCloseEvent" in CloseEvent.prototype);
+ assert_false("initCloseEvent" in new CloseEvent('close'));
+}, "initCloseEvent");
+</script>
diff --git a/test/wpt/tests/websockets/interfaces/WebSocket/bufferedAmount/bufferedAmount-arraybuffer.html b/test/wpt/tests/websockets/interfaces/WebSocket/bufferedAmount/bufferedAmount-arraybuffer.html
new file mode 100644
index 0000000..5d2bfd0
--- /dev/null
+++ b/test/wpt/tests/websockets/interfaces/WebSocket/bufferedAmount/bufferedAmount-arraybuffer.html
@@ -0,0 +1,27 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>WebSockets: bufferedAmount for ArrayBuffer</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=../../../constants.sub.js></script>
+<meta name="variant" content="?default">
+<meta name="variant" content="?wss">
+<meta name="variant" content="?wpt_flags=h2">
+<div id=log></div>
+<script>
+async_test(function(t) {
+ var ws = new WebSocket(SCHEME_DOMAIN_PORT+'/echo');
+ var datasize = 10;
+ ws.onopen = t.step_func(function(e) {
+ ws.binaryType = "arraybuffer";
+ var data = new ArrayBuffer(datasize);
+ ws.send(data);
+ assert_equals(ws.bufferedAmount, data.byteLength);
+ })
+ ws.onmessage = t.step_func(function(e) {
+ assert_equals(e.data.byteLength, datasize);
+ t.done();
+ })
+ ws.onclose = t.unreached_func('close event should not fire');
+});
+</script>
diff --git a/test/wpt/tests/websockets/interfaces/WebSocket/bufferedAmount/bufferedAmount-blob.html b/test/wpt/tests/websockets/interfaces/WebSocket/bufferedAmount/bufferedAmount-blob.html
new file mode 100644
index 0000000..d0028da
--- /dev/null
+++ b/test/wpt/tests/websockets/interfaces/WebSocket/bufferedAmount/bufferedAmount-blob.html
@@ -0,0 +1,28 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>WebSockets: bufferedAmount for blob</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=../../../constants.sub.js></script>
+<meta name="variant" content="?default">
+<meta name="variant" content="?wss">
+<meta name="variant" content="?wpt_flags=h2">
+<div id=log></div>
+<script>
+async_test(function(t) {
+ var ws = new WebSocket(SCHEME_DOMAIN_PORT+'/echo');
+ var datasize = 10;
+ ws.onopen = t.step_func(function(e) {
+ ws.binaryType = "blob";
+ var data = new ArrayBuffer(datasize);
+ ws.send(data);
+ assert_equals(ws.bufferedAmount, data.byteLength);
+ });
+ ws.onmessage = t.step_func(function(e) {
+ assert_true(e.data instanceof Blob);
+ assert_equals(e.data.size, datasize);
+ t.done();
+ });
+ ws.onclose = t.unreached_func('close event should not fire');
+});
+</script>
diff --git a/test/wpt/tests/websockets/interfaces/WebSocket/bufferedAmount/bufferedAmount-defineProperty-getter.html b/test/wpt/tests/websockets/interfaces/WebSocket/bufferedAmount/bufferedAmount-defineProperty-getter.html
new file mode 100644
index 0000000..ea6e70c
--- /dev/null
+++ b/test/wpt/tests/websockets/interfaces/WebSocket/bufferedAmount/bufferedAmount-defineProperty-getter.html
@@ -0,0 +1,18 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>WebSockets: defineProperty getter for bufferedAmount</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=../../../constants.sub.js></script>
+<meta name="variant" content="?default">
+<meta name="variant" content="?wss">
+<div id=log></div>
+<script>
+test(function(){
+ Object.defineProperty(WebSocket.prototype, 'bufferedAmount', {
+ get: function() { return 'foo'; }
+ });
+ var ws = new WebSocket('ws://example.invalid/');
+ assert_equals(ws.bufferedAmount, 'foo');
+});
+</script>
diff --git a/test/wpt/tests/websockets/interfaces/WebSocket/bufferedAmount/bufferedAmount-defineProperty-setter.html b/test/wpt/tests/websockets/interfaces/WebSocket/bufferedAmount/bufferedAmount-defineProperty-setter.html
new file mode 100644
index 0000000..8f0fa5c
--- /dev/null
+++ b/test/wpt/tests/websockets/interfaces/WebSocket/bufferedAmount/bufferedAmount-defineProperty-setter.html
@@ -0,0 +1,20 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>WebSockets: defineProperty setter for bufferedAmount</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=../../../constants.sub.js></script>
+<meta name="variant" content="?default">
+<meta name="variant" content="?wss">
+<div id=log></div>
+<script>
+test(function() {
+ window.setter_ran = false;
+ Object.defineProperty(WebSocket.prototype, 'bufferedAmount', {
+ set: function(v) { window[v] = true; }
+ });
+ var ws = new WebSocket('ws://example.invalid/');
+ ws.bufferedAmount = 'setter_ran';
+ assert_true(setter_ran);
+});
+</script>
diff --git a/test/wpt/tests/websockets/interfaces/WebSocket/bufferedAmount/bufferedAmount-deleting.html b/test/wpt/tests/websockets/interfaces/WebSocket/bufferedAmount/bufferedAmount-deleting.html
new file mode 100644
index 0000000..1d99636
--- /dev/null
+++ b/test/wpt/tests/websockets/interfaces/WebSocket/bufferedAmount/bufferedAmount-deleting.html
@@ -0,0 +1,23 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>WebSockets: delete bufferedAmount</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=../../../constants.sub.js></script>
+<meta name="variant" content="?default">
+<meta name="variant" content="?wss">
+<div id=log></div>
+<script>
+test(function(){
+ var ws = new WebSocket(SCHEME_DOMAIN_PORT+'/echo');
+ assert_equals(ws.bufferedAmount, 0, 'after creation');
+ ws.close();
+ delete ws.bufferedAmount;
+ assert_equals(ws.bufferedAmount, 0,
+ 'after attempt to delete ws.bufferedAmount');
+ delete WebSocket.prototype.bufferedAmount;
+ assert_equals(ws.bufferedAmount, undefined,
+ 'after attempt to delete WebSocket.prototype.bufferedAmount');
+});
+</script>
+
diff --git a/test/wpt/tests/websockets/interfaces/WebSocket/bufferedAmount/bufferedAmount-getting.html b/test/wpt/tests/websockets/interfaces/WebSocket/bufferedAmount/bufferedAmount-getting.html
new file mode 100644
index 0000000..92bcea6
--- /dev/null
+++ b/test/wpt/tests/websockets/interfaces/WebSocket/bufferedAmount/bufferedAmount-getting.html
@@ -0,0 +1,54 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>WebSockets: bufferedAmount after send()ing</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=../../../constants.sub.js></script>
+<meta name="variant" content="?default">
+<meta name="variant" content="?wss">
+<meta name="variant" content="?wpt_flags=h2">
+<div id=log></div>
+<script>
+async_test(function(t){
+ // bufferedAmount should increase sync in the send() method and decrease between
+ // events in the event loop (so never while script is running).
+ var ws = new WebSocket(SCHEME_DOMAIN_PORT+'/echo');
+ ws.onopen = t.step_func(function(e) {
+ ws.send('x');
+ assert_equals(ws.bufferedAmount, 1, 'bufferedAmount after sent "x"');
+ ws.send('\u00E5');
+ assert_equals(ws.bufferedAmount, 1+2, 'bufferedAmount after sent "x", "\u00E5"');
+ ws.send('\u5336');
+ assert_equals(ws.bufferedAmount, 1+2+3, 'bufferedAmount after sent "x", "\u00E5", "\u5336"');
+ ws.send('\uD801\uDC7E');
+ assert_equals(ws.bufferedAmount, 1+2+3+4, 'bufferedAmount after sent "x", "\u00E5", "\u5336", "\uD801\uDC7E"');
+ })
+ var i = 0;
+ ws.onmessage = t.step_func(function(e) {
+ i++;
+ switch(i) {
+ case 1:
+ assert_equals(e.data, 'x');
+ assert_true(ws.bufferedAmount < 2+3+4 + 1, 'bufferedAmount after received "x"');
+ break;
+ case 2:
+ assert_equals(e.data, '\u00E5');
+ assert_true(ws.bufferedAmount < 3+4 + 1, 'bufferedAmount after received "x", "\u00E5"');
+ break;
+ case 3:
+ assert_equals(e.data, '\u5336');
+ assert_true(ws.bufferedAmount < 4 + 1, 'bufferedAmount after received "x", "\u00E5", "\u5336"');
+ break;
+ case 4:
+ assert_equals(e.data, '\uD801\uDC7E');
+ assert_equals(ws.bufferedAmount, 0, 'bufferedAmount after received "x", "\u00E5", "\u5336", "\uD801\uDC7E"');
+ t.done();
+ break;
+ default:
+ assert_unreached(i);
+ }
+ })
+ ws.onerror = ws.onclose = t.step_func(function() {assert_unreached()});
+});
+</script>
+
diff --git a/test/wpt/tests/websockets/interfaces/WebSocket/bufferedAmount/bufferedAmount-initial.html b/test/wpt/tests/websockets/interfaces/WebSocket/bufferedAmount/bufferedAmount-initial.html
new file mode 100644
index 0000000..be37b6d
--- /dev/null
+++ b/test/wpt/tests/websockets/interfaces/WebSocket/bufferedAmount/bufferedAmount-initial.html
@@ -0,0 +1,15 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>WebSockets: getting bufferedAmount</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=../../../constants.sub.js></script>
+<meta name="variant" content="?default">
+<meta name="variant" content="?wss">
+<div id=log></div>
+<script>
+test(function(){
+ var ws = new WebSocket(SCHEME_DOMAIN_PORT+'/');
+ assert_equals(ws.bufferedAmount, 0);
+});
+</script>
diff --git a/test/wpt/tests/websockets/interfaces/WebSocket/bufferedAmount/bufferedAmount-large.html b/test/wpt/tests/websockets/interfaces/WebSocket/bufferedAmount/bufferedAmount-large.html
new file mode 100644
index 0000000..18c5482
--- /dev/null
+++ b/test/wpt/tests/websockets/interfaces/WebSocket/bufferedAmount/bufferedAmount-large.html
@@ -0,0 +1,29 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>WebSockets: bufferedAmount for 65K data</title>
+<meta name=timeout content=long>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=../../../constants.sub.js></script>
+<meta name="variant" content="?default">
+<meta name="variant" content="?wss">
+<meta name="variant" content="?wpt_flags=h2">
+<div id=log></div>
+<script>
+async_test(function(t) {
+ var ws = new WebSocket(SCHEME_DOMAIN_PORT+'/echo');
+ var data = "";
+ ws.onopen = t.step_func(function(e) {
+ for (var i = 0; i < 65000; i++) {
+ data = data + "x";
+ }
+ ws.send(data);
+ assert_equals(data.length, ws.bufferedAmount);
+ });
+ ws.onmessage = t.step_func(function(e) {
+ assert_equals(e.data, data);
+ t.done();
+ })
+ ws.onclose = t.unreached_func('close event should not fire');
+});
+</script>
diff --git a/test/wpt/tests/websockets/interfaces/WebSocket/bufferedAmount/bufferedAmount-readonly.html b/test/wpt/tests/websockets/interfaces/WebSocket/bufferedAmount/bufferedAmount-readonly.html
new file mode 100644
index 0000000..152da69
--- /dev/null
+++ b/test/wpt/tests/websockets/interfaces/WebSocket/bufferedAmount/bufferedAmount-readonly.html
@@ -0,0 +1,16 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>WebSockets: setting bufferedAmount</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=../../../constants.sub.js></script>
+<meta name="variant" content="?default">
+<meta name="variant" content="?wss">
+<div id=log></div>
+<script>
+test(function(){
+ var ws = new WebSocket(SCHEME_DOMAIN_PORT+'/');
+ ws.bufferedAmount = 5;
+ assert_equals(ws.bufferedAmount, 0);
+});
+</script>
diff --git a/test/wpt/tests/websockets/interfaces/WebSocket/bufferedAmount/bufferedAmount-unicode.html b/test/wpt/tests/websockets/interfaces/WebSocket/bufferedAmount/bufferedAmount-unicode.html
new file mode 100644
index 0000000..ab01f3c
--- /dev/null
+++ b/test/wpt/tests/websockets/interfaces/WebSocket/bufferedAmount/bufferedAmount-unicode.html
@@ -0,0 +1,25 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>WebSockets: bufferedAmount for unicode data</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=../../../constants.sub.js></script>
+<meta name="variant" content="?default">
+<meta name="variant" content="?wss">
+<meta name="variant" content="?wpt_flags=h2">
+<div id=log></div>
+<script>
+async_test(function(t) {
+ var ws = new WebSocket(SCHEME_DOMAIN_PORT+'/echo');
+ var data = "¥¥¥¥¥¥";
+ ws.onopen = t.step_func(function(e) {
+ ws.send(data);
+ assert_equals(data.length * 2, ws.bufferedAmount);
+ });
+ ws.onmessage = t.step_func(function(e) {
+ assert_equals(e.data, data);
+ t.done();
+ });
+ ws.onclose = t.unreached_func('close event should not fire');
+});
+</script>
diff --git a/test/wpt/tests/websockets/interfaces/WebSocket/close/close-basic.html b/test/wpt/tests/websockets/interfaces/WebSocket/close/close-basic.html
new file mode 100644
index 0000000..b646ca4
--- /dev/null
+++ b/test/wpt/tests/websockets/interfaces/WebSocket/close/close-basic.html
@@ -0,0 +1,26 @@
+<!doctype html>
+<title>WebSockets: close()</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=../../../constants.sub.js></script>
+<meta name="variant" content="?default">
+<meta name="variant" content="?wss">
+<div id=log></div>
+<script>
+async_test(function(t) {
+ var ws = new WebSocket(SCHEME_DOMAIN_PORT+'/');
+ ws.onclose = t.step_func(function(e) {
+ assert_equals(e instanceof CloseEvent, true, 'e instanceof CloseEvent');
+ assert_equals(e.wasClean, false, 'e.wasClean');
+ e.wasClean = true;
+ assert_equals(e.wasClean, false, 'e.wasClean = true');
+ delete e.wasClean;
+ assert_equals(e.wasClean, false, 'delete e.wasClean');
+ delete CloseEvent.prototype.wasClean;
+ assert_equals(e.wasClean, undefined, 'delete CloseEvent.prototype.wasClean');
+ t.done();
+ });
+ ws.close();
+ assert_equals(ws.readyState, ws.CLOSING);
+});
+</script>
diff --git a/test/wpt/tests/websockets/interfaces/WebSocket/close/close-connecting.html b/test/wpt/tests/websockets/interfaces/WebSocket/close/close-connecting.html
new file mode 100644
index 0000000..de038ca
--- /dev/null
+++ b/test/wpt/tests/websockets/interfaces/WebSocket/close/close-connecting.html
@@ -0,0 +1,25 @@
+<!doctype html>
+<title>WebSockets: close() when connecting</title>
+<meta name=timeout content=long>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=../../../constants.sub.js></script>
+<meta name="variant" content="?default">
+<meta name="variant" content="?wss">
+<div id=log></div>
+<script>
+async_test(function(t) {
+ var ws = new WebSocket(SCHEME_DOMAIN_PORT+'/sleep_10_v13');
+ t.step_timeout(function() {
+ assert_equals(ws.readyState, ws.CONNECTING);
+ ws.close();
+ assert_equals(ws.readyState, ws.CLOSING);
+ ws.onclose = t.step_func(function(e) {
+ assert_equals(ws.readyState, ws.CLOSED);
+ assert_equals(e.wasClean, false);
+ t.done();
+ });
+ }, 1000);
+ ws.onopen = ws.onclose = t.unreached_func();
+});
+</script>
diff --git a/test/wpt/tests/websockets/interfaces/WebSocket/close/close-multiple.html b/test/wpt/tests/websockets/interfaces/WebSocket/close/close-multiple.html
new file mode 100644
index 0000000..e440d80
--- /dev/null
+++ b/test/wpt/tests/websockets/interfaces/WebSocket/close/close-multiple.html
@@ -0,0 +1,29 @@
+<!doctype html>
+<title>WebSockets: close() several times</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=../../../constants.sub.js></script>
+<meta name="variant" content="?default">
+<meta name="variant" content="?wss">
+<div id=log></div>
+<script>
+var i = 0;
+async_test(function(t) {
+ var ws = new WebSocket(SCHEME_DOMAIN_PORT+'/');
+ ws.onclose = t.step_func(function(e) {
+ i++;
+ });
+ ws.close();
+ ws.close();
+ ws.close();
+ var f = t.step_func(function() {
+ if (i < 1) {
+ t.step_timeout(f, 500);
+ return;
+ }
+ assert_equals(i, 1);
+ t.done()
+ });
+ t.step_timeout(f, 500);
+});
+</script>
diff --git a/test/wpt/tests/websockets/interfaces/WebSocket/close/close-nested.html b/test/wpt/tests/websockets/interfaces/WebSocket/close/close-nested.html
new file mode 100644
index 0000000..74b8fa0
--- /dev/null
+++ b/test/wpt/tests/websockets/interfaces/WebSocket/close/close-nested.html
@@ -0,0 +1,28 @@
+<!doctype html>
+<title>WebSockets: close() in close event handler</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=../../../constants.sub.js></script>
+<meta name="variant" content="?default">
+<meta name="variant" content="?wss">
+<div id=log></div>
+<script>
+async_test(function(t) {
+ var ws = new WebSocket(SCHEME_DOMAIN_PORT+'/');
+ var i = 0;
+ ws.onclose = t.step_func(function(e) {
+ i++;
+ if (i == 1) {
+ assert_equals(ws.readyState, ws.CLOSED);
+ ws.close();
+ assert_equals(ws.readyState, ws.CLOSED);
+ }
+ t.step_timeout(function() {
+ assert_equals(i, 1);
+ t.done();
+ }, 50);
+ });
+ ws.close();
+ assert_equals(ws.readyState, ws.CLOSING);
+});
+</script>
diff --git a/test/wpt/tests/websockets/interfaces/WebSocket/close/close-replace.html b/test/wpt/tests/websockets/interfaces/WebSocket/close/close-replace.html
new file mode 100644
index 0000000..e9d2364
--- /dev/null
+++ b/test/wpt/tests/websockets/interfaces/WebSocket/close/close-replace.html
@@ -0,0 +1,15 @@
+<!doctype html>
+<title>WebSockets: replacing close</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=../../../constants.sub.js></script>
+<meta name="variant" content="?default">
+<meta name="variant" content="?wss">
+<div id=log></div>
+<script>
+test(function() {
+ var ws = new WebSocket(SCHEME_DOMAIN_PORT+'/');
+ ws.close = 5;
+ assert_equals(ws.close, 5);
+});
+</script>
diff --git a/test/wpt/tests/websockets/interfaces/WebSocket/close/close-return.html b/test/wpt/tests/websockets/interfaces/WebSocket/close/close-return.html
new file mode 100644
index 0000000..e74c9b0
--- /dev/null
+++ b/test/wpt/tests/websockets/interfaces/WebSocket/close/close-return.html
@@ -0,0 +1,14 @@
+<!doctype html>
+<title>WebSockets: close() return value</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=../../../constants.sub.js></script>
+<meta name="variant" content="?default">
+<meta name="variant" content="?wss">
+<div id=log></div>
+<script>
+test(function() {
+ var ws = new WebSocket(SCHEME_DOMAIN_PORT+'/');
+ assert_equals(ws.close(), undefined);
+});
+</script>
diff --git a/test/wpt/tests/websockets/interfaces/WebSocket/constants/001.html b/test/wpt/tests/websockets/interfaces/WebSocket/constants/001.html
new file mode 100644
index 0000000..7d79bf5
--- /dev/null
+++ b/test/wpt/tests/websockets/interfaces/WebSocket/constants/001.html
@@ -0,0 +1,17 @@
+<!doctype html>
+<title>WebSockets: getting constants on constructor</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=../../../constants.sub.js></script>
+<meta name="variant" content="?default">
+<meta name="variant" content="?wss">
+<div id=log></div>
+<script>
+var constants = ['CONNECTING', 'OPEN', 'CLOSING', 'CLOSED'];
+for (var i = 0; i < constants.length; ++i) {
+ test(function(){
+ var ws = new WebSocket(SCHEME_DOMAIN_PORT+'/');
+ assert_equals(WebSocket[constants[i]], i, 'WebSocket.'+constants[i]);
+ }, "Constants on constructors " + constants[i]);
+};
+</script>
diff --git a/test/wpt/tests/websockets/interfaces/WebSocket/constants/002.html b/test/wpt/tests/websockets/interfaces/WebSocket/constants/002.html
new file mode 100644
index 0000000..6810bc6
--- /dev/null
+++ b/test/wpt/tests/websockets/interfaces/WebSocket/constants/002.html
@@ -0,0 +1,24 @@
+<!doctype html>
+<title>WebSockets: setting constants</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=../../../constants.sub.js></script>
+<meta name="variant" content="?default">
+<meta name="variant" content="?wss">
+<div id=log></div>
+<script>
+// this test is testing WebIDL stuff
+var ws = new WebSocket(SCHEME_DOMAIN_PORT+'/');
+var constants = ['CONNECTING', 'OPEN', 'CLOSING', 'CLOSED'];
+for (var i = 0; i < constants.length; ++i) {
+ test(function() {
+ WebSocket[constants[i]] = 5; // should be ignored, has { ReadOnly }
+ WebSocket.prototype[constants[i]] = 5; // should be ignored, has { ReadOnly }
+ ws[constants[i]] = 5; // should be ignored, { ReadOnly } is inherited from prototype
+ assert_equals(WebSocket[constants[i]], i, 'WebSocket.'+constants[i]);
+ assert_equals(WebSocket.prototype[constants[i]], i, 'WebSocket.prototype.'+constants[i]);
+ assert_equals(ws[constants[i]], i, 'ws.'+constants[i]);
+ }, "Readonly constants " + constants[i]);
+};
+</script>
+
diff --git a/test/wpt/tests/websockets/interfaces/WebSocket/constants/003.html b/test/wpt/tests/websockets/interfaces/WebSocket/constants/003.html
new file mode 100644
index 0000000..4a86af8
--- /dev/null
+++ b/test/wpt/tests/websockets/interfaces/WebSocket/constants/003.html
@@ -0,0 +1,22 @@
+<!doctype html>
+<title>WebSockets: deleting constants</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=../../../constants.sub.js></script>
+<meta name="variant" content="?default">
+<meta name="variant" content="?wss">
+<div id=log></div>
+<script>
+var constants = ['CONNECTING', 'OPEN', 'CLOSING', 'CLOSED'];
+for (var i = 0; i < constants.length; ++i) {
+ test(function(){
+ var ws = new WebSocket(SCHEME_DOMAIN_PORT+'/');
+ delete WebSocket[constants[i]]; // should be ignored, has { DontDelete }
+ delete WebSocket.prototype[constants[i]]; // should be ignored, has { DontDelete }
+ delete ws[constants[i]]; // should be ignored, there is no such property on the object
+ assert_equals(WebSocket[constants[i]], i, 'WebSocket.'+constants[i]);
+ assert_equals(WebSocket.prototype[constants[i]], i, 'WebSocket.prototype.'+constants[i]);
+ assert_equals(ws[constants[i]], i, 'ws.'+constants[i]);
+ })
+};
+</script>
diff --git a/test/wpt/tests/websockets/interfaces/WebSocket/constants/004.html b/test/wpt/tests/websockets/interfaces/WebSocket/constants/004.html
new file mode 100644
index 0000000..2ca3830
--- /dev/null
+++ b/test/wpt/tests/websockets/interfaces/WebSocket/constants/004.html
@@ -0,0 +1,21 @@
+<!doctype html>
+<title>WebSockets: getting constants on prototype and object</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=../../../constants.sub.js></script>
+<meta name="variant" content="?default">
+<meta name="variant" content="?wss">
+<div id=log></div>
+<script>
+var constants = ['CONNECTING', 'OPEN', 'CLOSING', 'CLOSED'];
+for (var i = 0; i < constants.length; ++i) {
+ test(function() {
+ var ws = new WebSocket(SCHEME_DOMAIN_PORT+'/');
+ assert_equals(WebSocket.prototype[constants[i]], i);
+ }, 'WebSocket.prototype.'+constants[i]);
+ test(function() {
+ var ws = new WebSocket(SCHEME_DOMAIN_PORT+'/');
+ assert_equals(ws[constants[i]], i);
+ }, 'ws.'+constants[i]);
+};
+</script>
diff --git a/test/wpt/tests/websockets/interfaces/WebSocket/constants/005.html b/test/wpt/tests/websockets/interfaces/WebSocket/constants/005.html
new file mode 100644
index 0000000..26d5b24
--- /dev/null
+++ b/test/wpt/tests/websockets/interfaces/WebSocket/constants/005.html
@@ -0,0 +1,20 @@
+<!doctype html>
+<title>WebSockets: defineProperty getter for constants</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=../../../constants.sub.js></script>
+<meta name="variant" content="?default">
+<meta name="variant" content="?wss">
+<div id=log></div>
+<script>
+var constants = ['CONNECTING', 'OPEN', 'CLOSING', 'CLOSED'];
+for (var i = 0; i < constants.length; ++i) {
+ test(function() {
+ assert_throws_js(TypeError, function() {
+ Object.defineProperty(WebSocket.prototype, constants[i], {
+ get: function() { return 'foo'; }
+ });
+ });
+ }, "defineProperty getter " + constants[i]);
+};
+</script>
diff --git a/test/wpt/tests/websockets/interfaces/WebSocket/constants/006.html b/test/wpt/tests/websockets/interfaces/WebSocket/constants/006.html
new file mode 100644
index 0000000..78126c8
--- /dev/null
+++ b/test/wpt/tests/websockets/interfaces/WebSocket/constants/006.html
@@ -0,0 +1,20 @@
+<!doctype html>
+<title>WebSockets: defineProperty setter for constants</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=../../../constants.sub.js></script>
+<meta name="variant" content="?default">
+<meta name="variant" content="?wss">
+<div id=log></div>
+<script>
+var constants = ['CONNECTING', 'OPEN', 'CLOSING', 'CLOSED'];
+for (var i = 0; i < constants.length; ++i) {
+ test(function() {
+ assert_throws_js(TypeError, function(){
+ Object.defineProperty(WebSocket.prototype, constants[i], {
+ set: function() { return 'foo'; }
+ });
+ });
+ }, "defineProperty setter " + constants[i])
+};
+</script>
diff --git a/test/wpt/tests/websockets/interfaces/WebSocket/events/001.html b/test/wpt/tests/websockets/interfaces/WebSocket/events/001.html
new file mode 100644
index 0000000..88dcf9e
--- /dev/null
+++ b/test/wpt/tests/websockets/interfaces/WebSocket/events/001.html
@@ -0,0 +1,18 @@
+<!doctype html>
+<title>WebSockets: getting on*</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=../../../constants.sub.js></script>
+<meta name="variant" content="?default">
+<meta name="variant" content="?wss">
+<meta name="variant" content="?wpt_flags=h2">
+<div id=log></div>
+<script>
+var events = ['open', 'message', 'error', 'close'];
+for (var i = 0; i < events.length; ++i) {
+ test(function(){
+ var ws = new WebSocket(SCHEME_DOMAIN_PORT+'/');
+ assert_equals(ws['on'+events[i]], null, 'on'+events[i]);
+ });
+};
+</script>
diff --git a/test/wpt/tests/websockets/interfaces/WebSocket/events/002.html b/test/wpt/tests/websockets/interfaces/WebSocket/events/002.html
new file mode 100644
index 0000000..4817308
--- /dev/null
+++ b/test/wpt/tests/websockets/interfaces/WebSocket/events/002.html
@@ -0,0 +1,20 @@
+<!doctype html>
+<title>WebSockets: setting on*</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=../../../constants.sub.js></script>
+<meta name="variant" content="?default">
+<meta name="variant" content="?wss">
+<meta name="variant" content="?wpt_flags=h2">
+<div id=log></div>
+<script>
+var events = ['open', 'message', 'error', 'close'];
+for (var i = 0; i < events.length; ++i) {
+ test(function(){
+ var ws = new WebSocket(SCHEME_DOMAIN_PORT+'/');
+ var foo = function () {};
+ ws['on'+events[i]] = foo;
+ assert_equals(ws['on'+events[i]], foo);
+ });
+}
+</script>
diff --git a/test/wpt/tests/websockets/interfaces/WebSocket/events/003.html b/test/wpt/tests/websockets/interfaces/WebSocket/events/003.html
new file mode 100644
index 0000000..a5373ec
--- /dev/null
+++ b/test/wpt/tests/websockets/interfaces/WebSocket/events/003.html
@@ -0,0 +1,21 @@
+<!doctype html>
+<title>WebSockets: listening for events with onopen</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=../../../constants.sub.js></script>
+<meta name="variant" content="?default">
+<meta name="variant" content="?wss">
+<div id=log></div>
+<script>
+async_test(function(t) {
+ var ws = new WebSocket(SCHEME_DOMAIN_PORT+'/');
+ var foo = t.step_func(function (e) {
+ if (e.detail == 5)
+ t.done();
+ })
+ ws.onopen = foo;
+ var ev = document.createEvent('UIEvents');
+ ev.initUIEvent('open', false, false, window, 5);
+ ws.dispatchEvent(ev);
+});
+</script>
diff --git a/test/wpt/tests/websockets/interfaces/WebSocket/events/004.html b/test/wpt/tests/websockets/interfaces/WebSocket/events/004.html
new file mode 100644
index 0000000..9c5144c
--- /dev/null
+++ b/test/wpt/tests/websockets/interfaces/WebSocket/events/004.html
@@ -0,0 +1,16 @@
+<!doctype html>
+<title>WebSockets: members of EventTarget</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=../../../constants.sub.js></script>
+<meta name="variant" content="?default">
+<meta name="variant" content="?wss">
+<div id=log></div>
+<script>
+test(function(){
+ var ws = new WebSocket(SCHEME_DOMAIN_PORT+'/');
+ assert_equals(typeof ws.addEventListener, 'function');
+ assert_equals(typeof ws.removeEventListener, 'function');
+ assert_equals(typeof ws.dispatchEvent, 'function');
+});
+</script>
diff --git a/test/wpt/tests/websockets/interfaces/WebSocket/events/006.html b/test/wpt/tests/websockets/interfaces/WebSocket/events/006.html
new file mode 100644
index 0000000..de2f556
--- /dev/null
+++ b/test/wpt/tests/websockets/interfaces/WebSocket/events/006.html
@@ -0,0 +1,17 @@
+<!doctype html>
+<title>WebSockets: 'on*' in ws</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=../../../constants.sub.js></script>
+<meta name="variant" content="?default">
+<meta name="variant" content="?wss">
+<div id=log></div>
+<script>
+test(function(){
+ var ws = new WebSocket(SCHEME_DOMAIN_PORT+'/');
+ assert_equals('onopen' in ws, true, 'onopen');
+ assert_equals('onmessage' in ws, true, 'onmessage');
+ assert_equals('onerror' in ws, true, 'onerror');
+ assert_equals('onclose' in ws, true, 'onclose');
+});
+</script>
diff --git a/test/wpt/tests/websockets/interfaces/WebSocket/events/007.html b/test/wpt/tests/websockets/interfaces/WebSocket/events/007.html
new file mode 100644
index 0000000..0fe7241
--- /dev/null
+++ b/test/wpt/tests/websockets/interfaces/WebSocket/events/007.html
@@ -0,0 +1,22 @@
+<!doctype html>
+<title>WebSockets: listening for events with onmessage</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=../../../constants.sub.js></script>
+<meta name="variant" content="?default">
+<meta name="variant" content="?wss">
+<meta name="variant" content="?wpt_flags=h2">
+<div id=log></div>
+<script>
+async_test(function(t) {
+ var ws = new WebSocket(SCHEME_DOMAIN_PORT+'/');
+ var foo = t.step_func(function (e) {
+ if (e.detail == 5)
+ t.done();
+ })
+ ws.onmessage = foo;
+ var ev = document.createEvent('UIEvents');
+ ev.initUIEvent('message', false, false, window, 5);
+ ws.dispatchEvent(ev);
+});
+</script>
diff --git a/test/wpt/tests/websockets/interfaces/WebSocket/events/008.html b/test/wpt/tests/websockets/interfaces/WebSocket/events/008.html
new file mode 100644
index 0000000..066eb09
--- /dev/null
+++ b/test/wpt/tests/websockets/interfaces/WebSocket/events/008.html
@@ -0,0 +1,24 @@
+<!doctype html>
+<title>WebSockets: listening for events with onerror</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=../../../constants.sub.js></script>
+<meta name="variant" content="?default">
+<meta name="variant" content="?wss">
+<div id=log></div>
+<script>
+async_test(function(t) {
+ var ws = new WebSocket(SCHEME_DOMAIN_PORT+'/');
+ var run = false;
+ var foo = t.step_func(function (e) {
+ run = true;
+ assert_equals(e.detail, 5)
+ });
+ ws.onerror = foo;
+ var ev = document.createEvent('UIEvents');
+ ev.initUIEvent('error', false, false, window, 5);
+ ws.dispatchEvent(ev);
+ assert_true(run);
+ t.done();
+});
+</script>
diff --git a/test/wpt/tests/websockets/interfaces/WebSocket/events/009.html b/test/wpt/tests/websockets/interfaces/WebSocket/events/009.html
new file mode 100644
index 0000000..b9e56e2
--- /dev/null
+++ b/test/wpt/tests/websockets/interfaces/WebSocket/events/009.html
@@ -0,0 +1,21 @@
+<!doctype html>
+<title>WebSockets: listening for events with onclose</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=../../../constants.sub.js></script>
+<meta name="variant" content="?default">
+<meta name="variant" content="?wss">
+<div id=log></div>
+<script>
+async_test(function(t) {
+ var ws = new WebSocket(SCHEME_DOMAIN_PORT+'/');
+ var foo = t.step_func(function (e) {
+ if (e.detail == 5)
+ t.done();
+ });
+ ws.onclose = foo;
+ var ev = document.createEvent('UIEvents');
+ ev.initUIEvent('close', false, false, window, 5);
+ ws.dispatchEvent(ev);
+});
+</script>
diff --git a/test/wpt/tests/websockets/interfaces/WebSocket/events/010.html b/test/wpt/tests/websockets/interfaces/WebSocket/events/010.html
new file mode 100644
index 0000000..360e7d9
--- /dev/null
+++ b/test/wpt/tests/websockets/interfaces/WebSocket/events/010.html
@@ -0,0 +1,21 @@
+<!doctype html>
+<title>WebSockets: setting event handlers to undefined</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=../../../constants.sub.js></script>
+<meta name="variant" content="?default">
+<meta name="variant" content="?wss">
+<div id=log></div>
+<script>
+var events = ['onclose', 'onopen', 'onerror', 'onmessage'];
+for (var i = 0; i < events.length; ++i) {
+ test(function(){
+ var ws = new WebSocket(SCHEME_DOMAIN_PORT+'/empty-message');
+ var foo = function() {}
+ ws[events[i]] = foo;
+ assert_equals(ws[events[i]], foo, events[i]);
+ ws[events[i]] = undefined;
+ assert_equals(ws[events[i]], null, events[i]);
+ });
+}
+</script>
diff --git a/test/wpt/tests/websockets/interfaces/WebSocket/events/011.html b/test/wpt/tests/websockets/interfaces/WebSocket/events/011.html
new file mode 100644
index 0000000..f648575
--- /dev/null
+++ b/test/wpt/tests/websockets/interfaces/WebSocket/events/011.html
@@ -0,0 +1,18 @@
+<!doctype html>
+<title>WebSockets: setting event handlers to 1</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=../../../constants.sub.js></script>
+<meta name="variant" content="?default">
+<meta name="variant" content="?wss">
+<div id=log></div>
+<script>
+var events = ['onclose', 'onopen', 'onerror', 'onmessage'];
+for (var i = 0; i < events.length; ++i) {
+ test(function(t) {
+ var ws = new WebSocket(SCHEME_DOMAIN_PORT+'/empty-message');
+ ws[events[i]] = 1;
+ assert_equals(ws[events[i]], null);
+ }, events[i]);
+};
+</script>
diff --git a/test/wpt/tests/websockets/interfaces/WebSocket/events/012.html b/test/wpt/tests/websockets/interfaces/WebSocket/events/012.html
new file mode 100644
index 0000000..bdd63e3
--- /dev/null
+++ b/test/wpt/tests/websockets/interfaces/WebSocket/events/012.html
@@ -0,0 +1,18 @@
+<!doctype html>
+<title>WebSockets: setting event handlers to ";"</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=../../../constants.sub.js></script>
+<meta name="variant" content="?default">
+<meta name="variant" content="?wss">
+<div id=log></div>
+<script>
+var events = ['onclose', 'onopen', 'onerror', 'onmessage'];
+for (var i = 0; i < events.length; ++i) {
+ test(function(t) {
+ var ws = new WebSocket(SCHEME_DOMAIN_PORT+'/empty-message');
+ ws[events[i]] = ";";
+ assert_equals(ws[events[i]], null);
+ }, events[i]);
+};
+</script>
diff --git a/test/wpt/tests/websockets/interfaces/WebSocket/events/013.html b/test/wpt/tests/websockets/interfaces/WebSocket/events/013.html
new file mode 100644
index 0000000..9e251c6
--- /dev/null
+++ b/test/wpt/tests/websockets/interfaces/WebSocket/events/013.html
@@ -0,0 +1,20 @@
+<!doctype html>
+<title>WebSockets: setting event handlers to {handleEvent:function(){}}</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=../../../constants.sub.js></script>
+<meta name="variant" content="?default">
+<meta name="variant" content="?wss">
+<div id=log></div>
+<script>
+var events = ['onclose', 'onopen', 'onerror', 'onmessage'];
+for (var i = 0; i < events.length; ++i) {
+ test(function(t) {
+ var ws = new WebSocket(SCHEME_DOMAIN_PORT+'/empty-message');
+ var obj = {handleEvent:this.unreached_func("handleEvent was called")};
+ ws[events[i]] = obj;
+ assert_equals(ws[events[i]], obj);
+ ws.dispatchEvent(new Event(events[i].substr(2)));
+ }, events[i]);
+};
+</script>
diff --git a/test/wpt/tests/websockets/interfaces/WebSocket/events/014.html b/test/wpt/tests/websockets/interfaces/WebSocket/events/014.html
new file mode 100644
index 0000000..9fcd8b3
--- /dev/null
+++ b/test/wpt/tests/websockets/interfaces/WebSocket/events/014.html
@@ -0,0 +1,21 @@
+<!doctype html>
+<title>WebSockets: setting event handlers to null</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=../../../constants.sub.js></script>
+<meta name="variant" content="?default">
+<meta name="variant" content="?wss">
+<div id=log></div>
+<script>
+var events = ['onclose', 'onopen', 'onerror', 'onmessage'];
+for (var i = 0; i < events.length; ++i) {
+ test(function() {
+ var ws = new WebSocket(SCHEME_DOMAIN_PORT+'/empty-message');
+ var foo = function() {}
+ ws[events[i]] = foo;
+ assert_equals(ws[events[i]], foo, events[i]);
+ ws[events[i]] = null;
+ assert_equals(ws[events[i]], null, events[i]);
+ }, "Setting event handlers to null " + events[i]);
+};
+</script>
diff --git a/test/wpt/tests/websockets/interfaces/WebSocket/events/015.html b/test/wpt/tests/websockets/interfaces/WebSocket/events/015.html
new file mode 100644
index 0000000..5089c0f
--- /dev/null
+++ b/test/wpt/tests/websockets/interfaces/WebSocket/events/015.html
@@ -0,0 +1,36 @@
+<!doctype html>
+<title>WebSockets: instanceof on events</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=../../../constants.sub.js></script>
+<meta name="variant" content="?default">
+<meta name="variant" content="?wss">
+<meta name="variant" content="?wpt_flags=h2">
+<div id=log></div>
+<script>
+async_test(function(t) {
+ var ws = new WebSocket(SCHEME_DOMAIN_PORT+'/echo_raw');
+ ws.onopen = t.step_func(function(e) {
+ assert_true(e instanceof Event);
+ // first a text frame, then a frame with reserved opcode 3
+ // which should fail the connection
+
+ // send '\\x81\\x04test\\x83\\x03LOL' in bytes
+ ws.send(new Uint8Array([129, 4, 116, 101, 115, 116, 131, 3, 76, 79, 76]));
+ });
+ ws.onmessage = t.step_func(function(e) {
+ assert_true(e instanceof Event);
+ assert_true(e instanceof MessageEvent);
+ assert_equals(ws.readyState, ws.OPEN);
+ })
+ ws.onerror = t.step_func(function(e) {
+ assert_true(e instanceof Event);
+ assert_equals(ws.readyState, ws.CLOSED);
+ })
+ ws.onclose = t.step_func(function(e) {
+ assert_true(e instanceof Event);
+ assert_true(e instanceof CloseEvent);
+ t.done();
+ })
+});
+</script>
diff --git a/test/wpt/tests/websockets/interfaces/WebSocket/events/016.html b/test/wpt/tests/websockets/interfaces/WebSocket/events/016.html
new file mode 100644
index 0000000..8b5aaf9
--- /dev/null
+++ b/test/wpt/tests/websockets/interfaces/WebSocket/events/016.html
@@ -0,0 +1,39 @@
+<!doctype html>
+<title>WebSockets: addEventListener</title>
+<meta name="timeout" content="long">
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=../../../constants.sub.js></script>
+<meta name="variant" content="?default">
+<meta name="variant" content="?wss">
+<meta name="variant" content="?wpt_flags=h2">
+<div id=log></div>
+<script>
+async_test(function(t) {
+ var ws = new WebSocket(SCHEME_DOMAIN_PORT+'/echo');
+ var count = 0;
+ var checkCount = t.step_func(function (c, e) {
+ count++;
+ assert_equals(count, c);
+ });
+ // no spec requires this order for event listeners but the web does
+ ws.addEventListener('open', t.step_func(function(e) {
+ checkCount(1, e);
+ ws.send('Goodbye');
+ }), false);
+ ws.onopen = t.step_func(function(e) {checkCount(2, e) });
+ ws.addEventListener('open', t.step_func(function(e) {checkCount(3, e); }), false);
+
+ ws.addEventListener('message', t.step_func(function(e) {checkCount(4, e); }), false);
+ ws.onmessage = t.step_func(function(e) {checkCount(5, e) });
+ ws.addEventListener('message', t.step_func(function(e) {checkCount(6, e); }), false);
+
+ ws.addEventListener('close', t.step_func(function(e) {checkCount(7, e); }), false);
+ ws.onclose = t.step_func(function(e) {checkCount(8, e) });
+ ws.addEventListener('close', t.step_func(function(e) {
+ checkCount(9, e);
+ t.done();
+ }), false);
+
+});
+</script>
diff --git a/test/wpt/tests/websockets/interfaces/WebSocket/events/017.html b/test/wpt/tests/websockets/interfaces/WebSocket/events/017.html
new file mode 100644
index 0000000..a9f06ea
--- /dev/null
+++ b/test/wpt/tests/websockets/interfaces/WebSocket/events/017.html
@@ -0,0 +1,56 @@
+<!doctype html>
+<title>WebSockets: this, e.target, e.currentTarget, e.eventPhase</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=../../../constants.sub.js></script>
+<meta name="variant" content="?default">
+<meta name="variant" content="?wss">
+<meta name="variant" content="?wpt_flags=h2">
+<div id=log></div>
+<script>
+async_test(function(t) {
+ var ws = new WebSocket(SCHEME_DOMAIN_PORT+'/echo_raw');
+ ws.addEventListener('open', function(e) {
+ var this_val = this;
+ t.step(function() {
+ // first a text frame, then a frame with reserved opcode 3
+ // which should fail the connection
+
+ // send '\\x81\\x04test\\x83\\x03LOL' in bytes
+ ws.send(new Uint8Array([129, 4, 116, 101, 115, 116, 131, 3, 76, 79, 76]));
+ assert_equals(this_val, ws);
+ assert_equals(e.target, ws);
+ assert_equals(e.currentTarget, ws);
+ assert_equals(e.eventPhase, 2);
+ });
+ }, false);
+ ws.addEventListener('message', function(e) {
+ var this_val = this;
+ t.step(function() {
+ assert_equals(this_val, ws);
+ assert_equals(e.target, ws);
+ assert_equals(e.currentTarget, ws);
+ assert_equals(e.eventPhase, 2);
+ });
+ }, false);
+ ws.addEventListener('error', function(e) {
+ var this_val = this;
+ t.step(function() {
+ assert_equals(this_val, ws);
+ assert_equals(e.target, ws);
+ assert_equals(e.currentTarget, ws);
+ assert_equals(e.eventPhase, 2);
+ });
+ }, false);
+ ws.addEventListener('close', function(e) {
+ var this_val = this;
+ t.step(function() {
+ assert_equals(this_val, ws);
+ assert_equals(e.target, ws);
+ assert_equals(e.currentTarget, ws);
+ assert_equals(e.eventPhase, 2);
+ t.done();
+ });
+ }, false);
+});
+</script>
diff --git a/test/wpt/tests/websockets/interfaces/WebSocket/events/018.html b/test/wpt/tests/websockets/interfaces/WebSocket/events/018.html
new file mode 100644
index 0000000..a340c69
--- /dev/null
+++ b/test/wpt/tests/websockets/interfaces/WebSocket/events/018.html
@@ -0,0 +1,52 @@
+<!doctype html>
+<title>WebSockets: toString(), bubbles, cancelable</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=../../../constants.sub.js></script>
+<meta name="variant" content="?default">
+<meta name="variant" content="?wss">
+<meta name="variant" content="?wpt_flags=h2">
+<div id=log></div>
+<script>
+var ws = null;
+setup(function() {
+ ws = new WebSocket(SCHEME_DOMAIN_PORT+'/echo_raw');
+});
+
+async_test(function(t) {
+ let openFired = false;
+ let messageFired = false;
+ let errorFired = false;
+ ws.addEventListener('open', t.step_func(function(e) {
+ openFired = true;
+ // first a text frame, then a frame with reserved opcode 3
+ // which should fail the connection
+
+ // send '\\x81\\x04test\\x83\\x03LOL' in bytes
+ ws.send(new Uint8Array([129, 4, 116, 101, 115, 116, 131, 3, 76, 79, 76]));
+ assert_equals(e.toString(), '[object Event]', "open e.toString()");
+ assert_equals(e.bubbles, false, 'open e.bubbles');
+ assert_equals(e.cancelable, false, 'open e.cancelable');
+ }), false);
+ ws.addEventListener('message', t.step_func(function(e) {
+ messageFired = true;
+ assert_equals(e.toString(), '[object MessageEvent]', "message e.toString()");
+ assert_equals(e.bubbles, false, 'message e.bubbles');
+ assert_equals(e.cancelable, false, 'message e.cancelable');
+ }), false);
+ ws.addEventListener('error', t.step_func(function(e) {
+ errorFired = true;
+ assert_equals(e.toString(), '[object Event]', "error e.toString()");
+ assert_equals(e.bubbles, false, 'error e.bubbles');
+ assert_equals(e.cancelable, false, 'error e.cancelable');
+ }), false);
+ ws.addEventListener('close', t.step_func_done(function(e) {
+ assert_true(openFired, 'open event should fire');
+ assert_true(messageFired, 'message event should fire');
+ assert_true(errorFired, 'error event should fire');
+ assert_equals(e.toString(), '[object CloseEvent]', "close e.toString()");
+ assert_equals(e.bubbles, false, 'close e.bubbles');
+ assert_equals(e.cancelable, false, 'close e.cancelable');
+ }), false);
+}, "open, message, error and close events");
+</script>
diff --git a/test/wpt/tests/websockets/interfaces/WebSocket/events/019.html b/test/wpt/tests/websockets/interfaces/WebSocket/events/019.html
new file mode 100644
index 0000000..deb079f
--- /dev/null
+++ b/test/wpt/tests/websockets/interfaces/WebSocket/events/019.html
@@ -0,0 +1,31 @@
+<!doctype html>
+<title>WebSockets: removeEventListener</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=../../../constants.sub.js></script>
+<meta name="variant" content="?default">
+<meta name="variant" content="?wss">
+<div id=log></div>
+<script>
+var events = ['open', 'message', 'error', 'close'];
+for (var i = 0; i < events.length; ++i) {
+ test(function(t) {
+ var ws = new WebSocket(SCHEME_DOMAIN_PORT+'/echo');
+ ws.close();
+ var got = [];
+ var event;
+ function addThis(e) {
+ got.push(e.type);
+ }
+ ws.addEventListener(events[i], addThis, false);
+ ws.removeEventListener(events[i], addThis, false);
+ event = document.createEvent('Event');
+ event.initEvent(events[i], false, false);
+ ws.dispatchEvent(event);
+ assert_equals(got.length, 0);
+ if (got.length) {
+ debug('Got: '+got);
+ }
+ })
+};
+</script>
diff --git a/test/wpt/tests/websockets/interfaces/WebSocket/events/020.html b/test/wpt/tests/websockets/interfaces/WebSocket/events/020.html
new file mode 100644
index 0000000..f43b0af
--- /dev/null
+++ b/test/wpt/tests/websockets/interfaces/WebSocket/events/020.html
@@ -0,0 +1,17 @@
+<!doctype html>
+<title>WebSockets: error events</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=../../../constants.sub.js></script>
+<meta name="variant" content="?default">
+<meta name="variant" content="?wss">
+<div id=log></div>
+<script>
+async_test(function(t) {
+ var ws = new WebSocket('ws://example.invalid/');
+ ws.onerror = t.step_func(function(e) {
+ assert_true(e instanceof Event);
+ t.done();
+ })
+});
+</script>
diff --git a/test/wpt/tests/websockets/interfaces/WebSocket/extensions/001.html b/test/wpt/tests/websockets/interfaces/WebSocket/extensions/001.html
new file mode 100644
index 0000000..bd26483
--- /dev/null
+++ b/test/wpt/tests/websockets/interfaces/WebSocket/extensions/001.html
@@ -0,0 +1,14 @@
+<!doctype html>
+<title>WebSockets: getting extensions in connecting</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=../../../constants.sub.js></script>
+<meta name="variant" content="?default">
+<meta name="variant" content="?wss">
+<div id=log></div>
+<script>
+test(function(t) {
+ // The extensions attribute must initially return the empty string
+ assert_equals((new WebSocket(SCHEME_DOMAIN_PORT+'/empty-message')).extensions, '');
+});
+</script>
diff --git a/test/wpt/tests/websockets/interfaces/WebSocket/protocol/protocol-initial.html b/test/wpt/tests/websockets/interfaces/WebSocket/protocol/protocol-initial.html
new file mode 100644
index 0000000..2e7bf66
--- /dev/null
+++ b/test/wpt/tests/websockets/interfaces/WebSocket/protocol/protocol-initial.html
@@ -0,0 +1,14 @@
+<!doctype html>
+<title>WebSockets: getting protocol in connecting</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=../../../constants.sub.js></script>
+<meta name="variant" content="?default">
+<meta name="variant" content="?wss">
+<div id=log></div>
+<script>
+test(function(t) {
+ // The protocol attribute must initially return the empty string
+ assert_equals((new WebSocket(SCHEME_DOMAIN_PORT + '/empty-message')).protocol, '');
+});
+</script>
diff --git a/test/wpt/tests/websockets/interfaces/WebSocket/readyState/001.html b/test/wpt/tests/websockets/interfaces/WebSocket/readyState/001.html
new file mode 100644
index 0000000..15b73fd
--- /dev/null
+++ b/test/wpt/tests/websockets/interfaces/WebSocket/readyState/001.html
@@ -0,0 +1,13 @@
+<!doctype html>
+<title>WebSockets: getting readyState in connecting</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=../../../constants.sub.js></script>
+<meta name="variant" content="?default">
+<meta name="variant" content="?wss">
+<div id=log></div>
+<script>
+test(function(t) {
+ assert_equals((new WebSocket(SCHEME_DOMAIN_PORT+'/')).readyState, WebSocket.CONNECTING);
+});
+</script>
diff --git a/test/wpt/tests/websockets/interfaces/WebSocket/readyState/002.html b/test/wpt/tests/websockets/interfaces/WebSocket/readyState/002.html
new file mode 100644
index 0000000..239e5d7
--- /dev/null
+++ b/test/wpt/tests/websockets/interfaces/WebSocket/readyState/002.html
@@ -0,0 +1,15 @@
+<!doctype html>
+<title>WebSockets: setting readyState</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=../../../constants.sub.js></script>
+<meta name="variant" content="?default">
+<meta name="variant" content="?wss">
+<div id=log></div>
+<script>
+test(function(t) {
+ var ws = new WebSocket(SCHEME_DOMAIN_PORT+'/');
+ ws.readyState = 5;
+ assert_equals(ws.readyState, ws.CONNECTING);
+});
+</script>
diff --git a/test/wpt/tests/websockets/interfaces/WebSocket/readyState/003.html b/test/wpt/tests/websockets/interfaces/WebSocket/readyState/003.html
new file mode 100644
index 0000000..65d86c4
--- /dev/null
+++ b/test/wpt/tests/websockets/interfaces/WebSocket/readyState/003.html
@@ -0,0 +1,18 @@
+<!doctype html>
+<title>WebSockets: delete readyState</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=../../../constants.sub.js></script>
+<meta name="variant" content="?default">
+<meta name="variant" content="?wss">
+<div id=log></div>
+<script>
+test(function(t) {
+ var ws = new WebSocket(SCHEME_DOMAIN_PORT+'/');
+ ws.close();
+ delete ws.readyState;
+ assert_equals(ws.readyState, ws.CLOSING, 'delete ws.readyState');
+ delete WebSocket.prototype.readyState;
+ assert_equals(ws.readyState, undefined, 'delete WebSocket.prototype.readyState');
+});
+</script>
diff --git a/test/wpt/tests/websockets/interfaces/WebSocket/readyState/004.html b/test/wpt/tests/websockets/interfaces/WebSocket/readyState/004.html
new file mode 100644
index 0000000..0645816
--- /dev/null
+++ b/test/wpt/tests/websockets/interfaces/WebSocket/readyState/004.html
@@ -0,0 +1,17 @@
+<!doctype html>
+<title>WebSockets: defineProperty getter for readyState</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=../../../constants.sub.js></script>
+<meta name="variant" content="?default">
+<meta name="variant" content="?wss">
+<div id=log></div>
+<script>
+test(function() {
+ Object.defineProperty(WebSocket.prototype, 'readyState', {
+ get: function() { return 'foo'; }
+ });
+ var ws = new WebSocket('ws://example.invalid/');
+ assert_equals(ws.readyState, 'foo');
+});
+</script>
diff --git a/test/wpt/tests/websockets/interfaces/WebSocket/readyState/005.html b/test/wpt/tests/websockets/interfaces/WebSocket/readyState/005.html
new file mode 100644
index 0000000..bee179f
--- /dev/null
+++ b/test/wpt/tests/websockets/interfaces/WebSocket/readyState/005.html
@@ -0,0 +1,19 @@
+<!doctype html>
+<title>WebSockets: defineProperty setter for readyState</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=../../../constants.sub.js></script>
+<meta name="variant" content="?default">
+<meta name="variant" content="?wss">
+<div id=log></div>
+<script>
+test(function(){
+ window.setter_ran = false;
+ Object.defineProperty(WebSocket.prototype, 'readyState', {
+ set: function(v) { window[v] = true; }
+ });
+ var ws = new WebSocket('ws://example.invalid/');
+ ws.readyState = 'setter_ran';
+ assert_true(setter_ran);
+});
+</script>
diff --git a/test/wpt/tests/websockets/interfaces/WebSocket/readyState/006.html b/test/wpt/tests/websockets/interfaces/WebSocket/readyState/006.html
new file mode 100644
index 0000000..4290c00
--- /dev/null
+++ b/test/wpt/tests/websockets/interfaces/WebSocket/readyState/006.html
@@ -0,0 +1,19 @@
+<!doctype html>
+<title>WebSockets: getting readyState in open</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=../../../constants.sub.js></script>
+<meta name="variant" content="?default">
+<meta name="variant" content="?wss">
+<div id=log></div>
+<script>
+async_test(function(t) {
+ var ws = new WebSocket(SCHEME_DOMAIN_PORT+'/echo');
+ ws.onopen = t.step_func(function(e) {
+ assert_equals(ws.readyState, ws.OPEN);
+ ws.close();
+ t.done();
+ });
+ ws.onerror = ws.onmessage = ws.onclose = t.step_func(function() {assert_unreached()});
+});
+</script>
diff --git a/test/wpt/tests/websockets/interfaces/WebSocket/readyState/007.html b/test/wpt/tests/websockets/interfaces/WebSocket/readyState/007.html
new file mode 100644
index 0000000..69b5d9c
--- /dev/null
+++ b/test/wpt/tests/websockets/interfaces/WebSocket/readyState/007.html
@@ -0,0 +1,19 @@
+<!doctype html>
+<title>WebSockets: getting readyState in closing</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=../../../constants.sub.js></script>
+<meta name="variant" content="?default">
+<meta name="variant" content="?wss">
+<div id=log></div>
+<script>
+async_test(function(t) {
+ var ws = new WebSocket(SCHEME_DOMAIN_PORT+'/echo');
+ ws.onopen = t.step_func(function(e) {
+ ws.close();
+ assert_equals(ws.readyState, ws.CLOSING);
+ t.done();
+ });
+ ws.onerror = ws.onmessage = ws.onclose = t.step_func(function() {assert_unreached()});
+});
+</script>
diff --git a/test/wpt/tests/websockets/interfaces/WebSocket/readyState/008.html b/test/wpt/tests/websockets/interfaces/WebSocket/readyState/008.html
new file mode 100644
index 0000000..d085a7f
--- /dev/null
+++ b/test/wpt/tests/websockets/interfaces/WebSocket/readyState/008.html
@@ -0,0 +1,21 @@
+<!doctype html>
+<title>WebSockets: getting readyState in closed</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=../../../constants.sub.js></script>
+<meta name="variant" content="?default">
+<meta name="variant" content="?wss">
+<div id=log></div>
+<script>
+async_test(function(t) {
+ var ws = new WebSocket(SCHEME_DOMAIN_PORT+'/echo');
+ ws.onopen = t.step_func(function(e) {
+ ws.onclose = t.step_func(function(e) {
+ assert_equals(ws.readyState, ws.CLOSED);
+ t.done();
+ })
+ ws.close();
+ });
+ ws.onerror = ws.onmessage = ws.onclose = t.step_func(function() {assert_unreached()});
+});
+</script>
diff --git a/test/wpt/tests/websockets/interfaces/WebSocket/send/001.html b/test/wpt/tests/websockets/interfaces/WebSocket/send/001.html
new file mode 100644
index 0000000..8abc655
--- /dev/null
+++ b/test/wpt/tests/websockets/interfaces/WebSocket/send/001.html
@@ -0,0 +1,15 @@
+<!doctype html>
+<title>WebSockets: send() with no args</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=../../../constants.sub.js></script>
+<meta name="variant" content="?default">
+<meta name="variant" content="?wss">
+<div id=log></div>
+<script>
+test(function(t) {
+ var ws = new WebSocket(SCHEME_DOMAIN_PORT+'/');
+ assert_throws_js(TypeError, function(){ws.send()});
+});
+</script>
+
diff --git a/test/wpt/tests/websockets/interfaces/WebSocket/send/002.html b/test/wpt/tests/websockets/interfaces/WebSocket/send/002.html
new file mode 100644
index 0000000..a51c677
--- /dev/null
+++ b/test/wpt/tests/websockets/interfaces/WebSocket/send/002.html
@@ -0,0 +1,15 @@
+<!doctype html>
+<title>WebSockets: replacing send</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=../../../constants.sub.js></script>
+<meta name="variant" content="?default">
+<meta name="variant" content="?wss">
+<div id=log></div>
+<script>
+test(function() {
+ var ws = new WebSocket(SCHEME_DOMAIN_PORT+'/');
+ ws.send = 5;
+ assert_equals(ws.send, 5);
+});
+</script>
diff --git a/test/wpt/tests/websockets/interfaces/WebSocket/send/003.html b/test/wpt/tests/websockets/interfaces/WebSocket/send/003.html
new file mode 100644
index 0000000..069f24c
--- /dev/null
+++ b/test/wpt/tests/websockets/interfaces/WebSocket/send/003.html
@@ -0,0 +1,15 @@
+<!doctype html>
+<title>WebSockets: send() when readyState is CONNECTING</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=../../../constants.sub.js></script>
+<meta name="variant" content="?default">
+<meta name="variant" content="?wss">
+<div id=log></div>
+<script>
+test(function(t) {
+ var ws = new WebSocket(SCHEME_DOMAIN_PORT+'/');
+ assert_throws_dom("INVALID_STATE_ERR", function(){ws.send('a')});
+});
+</script>
+
diff --git a/test/wpt/tests/websockets/interfaces/WebSocket/send/004.html b/test/wpt/tests/websockets/interfaces/WebSocket/send/004.html
new file mode 100644
index 0000000..7125d19
--- /dev/null
+++ b/test/wpt/tests/websockets/interfaces/WebSocket/send/004.html
@@ -0,0 +1,25 @@
+<!doctype html>
+<title>WebSockets: send() with unpaired surrogate when readyState is CONNECTING</title>
+
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=../../../constants.sub.js></script>
+<meta name="variant" content="?default">
+<meta name="variant" content="?wss">
+<div id=log></div>
+<script>
+test(function(t) {
+ var ws = new WebSocket(SCHEME_DOMAIN_PORT+'/');
+ assert_throws_dom("INVALID_STATE_ERR", function(){ws.send('a\uDC00x')});
+}, "lone low surrogate");
+
+test(function(t) {
+ var ws = new WebSocket(SCHEME_DOMAIN_PORT+'/');
+ assert_throws_dom("INVALID_STATE_ERR", function(){ws.send('a\uD800x')});
+}, "lone high surrogate");
+
+test(function(t) {
+ var ws = new WebSocket(SCHEME_DOMAIN_PORT+'/');
+ assert_throws_dom("INVALID_STATE_ERR", function(){ws.send('a\uDC00\uD800x')});
+}, "surrogates in wrong order");
+</script>
diff --git a/test/wpt/tests/websockets/interfaces/WebSocket/send/005.html b/test/wpt/tests/websockets/interfaces/WebSocket/send/005.html
new file mode 100644
index 0000000..5da4600
--- /dev/null
+++ b/test/wpt/tests/websockets/interfaces/WebSocket/send/005.html
@@ -0,0 +1,19 @@
+<!doctype html>
+<title>WebSockets: send() return value</title>
+<meta name="timeout" content="long">
+
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=../../../constants.sub.js></script>
+<meta name="variant" content="?default">
+<meta name="variant" content="?wss">
+<div id=log></div>
+<script>
+async_test(function(t){
+ var ws = new WebSocket(SCHEME_DOMAIN_PORT+'/echo');
+ ws.onopen = t.step_func(function(e) {
+ assert_equals(ws.send('test'), undefined);
+ t.done();
+ });
+});
+</script>
diff --git a/test/wpt/tests/websockets/interfaces/WebSocket/send/006.html b/test/wpt/tests/websockets/interfaces/WebSocket/send/006.html
new file mode 100644
index 0000000..4095c0b
--- /dev/null
+++ b/test/wpt/tests/websockets/interfaces/WebSocket/send/006.html
@@ -0,0 +1,28 @@
+<!doctype html>
+<title>WebSockets: send() with unpaired surrogate when readyState is OPEN</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=../../../constants.sub.js></script>
+<meta name="variant" content="?default">
+<meta name="variant" content="?wss">
+<meta name="variant" content="?wpt_flags=h2">
+<div id="log"></div>
+<script>
+async_test(function(t) {
+ var ws = new WebSocket(SCHEME_DOMAIN_PORT+'/echo');
+ ws.onopen = t.step_func(function(e) {
+ // lone low surrogate, lone high surrogate + surrogates in wrong order.
+ ws.send('a\uDC00xb\uD800xc\uDC00\uD800x');
+ })
+ ws.onmessage = t.step_func(function(e) {
+ assert_equals(e.data, 'a\uFFFDxb\uFFFDxc\uFFFD\uFFFDx');
+ ws.onclose = t.step_func(function(e) {
+ ws.onclose = t.unreached_func();
+ t.step_timeout(() => t.done(), 50);
+ });
+ ws.close();
+ })
+ // This will be overridden if the message event fires.
+ ws.onclose = t.unreached_func('close event should not fire before message event');
+ });
+</script>
diff --git a/test/wpt/tests/websockets/interfaces/WebSocket/send/007.html b/test/wpt/tests/websockets/interfaces/WebSocket/send/007.html
new file mode 100644
index 0000000..6a56142
--- /dev/null
+++ b/test/wpt/tests/websockets/interfaces/WebSocket/send/007.html
@@ -0,0 +1,27 @@
+<!doctype html>
+<title>WebSockets: close() followed by send()</title>
+
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=../../../constants.sub.js></script>
+<meta name="variant" content="?default">
+<meta name="variant" content="?wss">
+<meta name="variant" content="?wpt_flags=h2">
+<div id=log></div>
+<script>
+
+async_test(function(t) {
+ var ws = new WebSocket(SCHEME_DOMAIN_PORT+'/echo');
+ ws.onopen = t.step_func(function(e) {
+ // test that nothing strange happens if we send something after close()
+ ws.close();
+ var sent = ws.send('test');
+ assert_equals(sent, undefined);
+ });
+ ws.onclose = t.step_func(function(e) {
+ ws.onclose = t.unreached_func();
+ t.step_timeout(() => t.done(), 50);
+ });
+ ws.onerror = ws.onmessage = t.unreached_func();
+});
+</script>
diff --git a/test/wpt/tests/websockets/interfaces/WebSocket/send/008.html b/test/wpt/tests/websockets/interfaces/WebSocket/send/008.html
new file mode 100644
index 0000000..709c066
--- /dev/null
+++ b/test/wpt/tests/websockets/interfaces/WebSocket/send/008.html
@@ -0,0 +1,25 @@
+<!doctype html>
+<title>WebSockets: send() in onclose</title>
+
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=../../../constants.sub.js></script>
+<meta name="variant" content="?default">
+<meta name="variant" content="?wss">
+<div id=log></div>
+<script>
+async_test(function(t) {
+ var ws = new WebSocket(SCHEME_DOMAIN_PORT+'/echo');
+ ws.onopen = t.step_func(function(e) {
+ ws.send('Goodbye');
+ })
+ ws.onclose = t.step_func(function(e) {
+ // test that nothing strange happens when send()ing in closed state
+ var sent = ws.send('test');
+ assert_equals(sent, undefined);
+ ws.onclose = t.unreached_func();
+ t.step_timeout(() => t.done(), 50);
+ })
+ ws.onerror = t.unreached_func();
+});
+</script>
diff --git a/test/wpt/tests/websockets/interfaces/WebSocket/send/009.html b/test/wpt/tests/websockets/interfaces/WebSocket/send/009.html
new file mode 100644
index 0000000..57da896
--- /dev/null
+++ b/test/wpt/tests/websockets/interfaces/WebSocket/send/009.html
@@ -0,0 +1,27 @@
+<!doctype html>
+<title>WebSockets: send('')</title>
+
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=../../../constants.sub.js></script>
+<meta name="variant" content="?default">
+<meta name="variant" content="?wss">
+<meta name="variant" content="?wpt_flags=h2">
+<div id=log></div>
+<script>
+async_test(function(t){
+ var ws = new WebSocket(SCHEME_DOMAIN_PORT+'/empty-message');
+ ws.onopen = t.step_func(function(e) {
+ ws.send('');
+ })
+ ws.onmessage = t.step_func(function(e) {
+ assert_equals(e.data, 'pass');
+ ws.close();
+ });
+ ws.onclose = t.step_func(function(e) {
+ ws.onclose = t.unreached_func();
+ t.step_timeout(() => t.done(), 50);
+ });
+ ws.onerror = t.unreached_func();
+});
+</script>
diff --git a/test/wpt/tests/websockets/interfaces/WebSocket/send/010.html b/test/wpt/tests/websockets/interfaces/WebSocket/send/010.html
new file mode 100644
index 0000000..4a008b6
--- /dev/null
+++ b/test/wpt/tests/websockets/interfaces/WebSocket/send/010.html
@@ -0,0 +1,42 @@
+<!doctype html>
+<title>WebSockets: sending non-strings</title>
+<meta name="timeout" content="long">
+
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=../../../constants.sub.js></script>
+<meta name="variant" content="?default">
+<meta name="variant" content="?wss">
+<div id=log></div>
+<script>
+async_test(function(outer) {
+ var ws = new WebSocket(SCHEME_DOMAIN_PORT+'/echo');
+ var stuffToSend = [null, undefined, 1, window, document.body, {}, [], ws, function(){}, new Error()]
+ var tests = [];
+
+ for (var i=0; i<stuffToSend.length; i++) {
+ tests.push(async_test(document.title + " (" + stuffToSend[i] + ")"));
+ }
+
+ i = 0;
+ function sendNext() {
+ if (i === stuffToSend.length) {
+ outer.done()
+ ws.close();
+ } else {
+ var t = tests[i];
+ ws.onmessage = t.step_func(function(e) {
+ assert_equals(e.data, String(stuffToSend[i]));
+ i++;
+ sendNext();
+ t.done();
+ });
+ ws.onclose = ws.onerror = t.step_func(function() {assert_unreached()});
+ ws.send(stuffToSend[i]);
+ }
+ }
+ ws.onopen = outer.step_func(function(e) {
+ sendNext();
+ });
+}, "Constructor succeeds");
+</script>
diff --git a/test/wpt/tests/websockets/interfaces/WebSocket/send/011.html b/test/wpt/tests/websockets/interfaces/WebSocket/send/011.html
new file mode 100644
index 0000000..5f63c44
--- /dev/null
+++ b/test/wpt/tests/websockets/interfaces/WebSocket/send/011.html
@@ -0,0 +1,28 @@
+<!doctype html>
+<title>WebSockets: sending non-ascii, combining chars and non-BMP</title>
+
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=../../../constants.sub.js></script>
+<meta name="variant" content="?default">
+<meta name="variant" content="?wss">
+<meta name="variant" content="?wpt_flags=h2">
+<div id=log></div>
+<script>
+
+async_test(function(t) {
+ var ws = new WebSocket(SCHEME_DOMAIN_PORT+'/echo');
+ ws.onopen = t.step_func(function(e) {
+ ws.send('\u00E5 a\u030A \uD801\uDC7E');
+ });
+ ws.onmessage = t.step_func(function(e) {
+ assert_equals(e.data, '\u00E5 a\u030A \uD801\uDC7E');
+ ws.onclose = t.step_func(function(e) {
+ ws.onclose = t.unreached_func();
+ t.step_timeout(() => t.done(), 50);
+ })
+ ws.close();
+ })
+ ws.onclose = ws.onerror = t.unreached_func();
+});
+</script>
diff --git a/test/wpt/tests/websockets/interfaces/WebSocket/send/012.html b/test/wpt/tests/websockets/interfaces/WebSocket/send/012.html
new file mode 100644
index 0000000..9876c7b
--- /dev/null
+++ b/test/wpt/tests/websockets/interfaces/WebSocket/send/012.html
@@ -0,0 +1,28 @@
+<!doctype html>
+<title>WebSockets: sending null</title>
+
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=../../../constants.sub.js></script>
+<meta name="variant" content="?default">
+<meta name="variant" content="?wss">
+<meta name="variant" content="?wpt_flags=h2">
+<div id=log></div>
+<script>
+
+async_test(function(t){
+ var ws = new WebSocket(SCHEME_DOMAIN_PORT+'/echo');
+ ws.onopen = t.step_func(function(e) {
+ ws.send(null);
+ });
+ ws.onmessage = t.step_func(function(e) {
+ assert_equals(e.data, 'null');
+ ws.onclose = t.step_func(function(e) {
+ ws.onclose = t.unreached_func();
+ t.step_timeout(() => t.done(), 50);
+ })
+ ws.close();
+ });
+ ws.onclose = ws.onerror = t.unreached_func();
+});
+</script>
diff --git a/test/wpt/tests/websockets/interfaces/WebSocket/url/001.html b/test/wpt/tests/websockets/interfaces/WebSocket/url/001.html
new file mode 100644
index 0000000..6c7306d
--- /dev/null
+++ b/test/wpt/tests/websockets/interfaces/WebSocket/url/001.html
@@ -0,0 +1,13 @@
+<!doctype html>
+<title>WebSockets: getting url</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=../../../constants.sub.js></script>
+<meta name="variant" content="?default">
+<meta name="variant" content="?wss">
+<div id=log></div>
+<script>
+test(function() {
+ assert_equals((new WebSocket(SCHEME_DOMAIN_PORT)).url, SCHEME_DOMAIN_PORT+'/');
+});
+</script>
diff --git a/test/wpt/tests/websockets/interfaces/WebSocket/url/002.html b/test/wpt/tests/websockets/interfaces/WebSocket/url/002.html
new file mode 100644
index 0000000..e1cc6d0
--- /dev/null
+++ b/test/wpt/tests/websockets/interfaces/WebSocket/url/002.html
@@ -0,0 +1,15 @@
+<!doctype html>
+<title>WebSockets: setting url</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=../../../constants.sub.js></script>
+<meta name="variant" content="?default">
+<meta name="variant" content="?wss">
+<div id=log></div>
+<script>
+test(function() {
+ var ws = new WebSocket(SCHEME_DOMAIN_PORT+'/');
+ ws.url = SCHEME_DOMAIN_PORT+'/test';
+ assert_equals(ws.url, SCHEME_DOMAIN_PORT+'/');
+});
+</script>
diff --git a/test/wpt/tests/websockets/interfaces/WebSocket/url/003.html b/test/wpt/tests/websockets/interfaces/WebSocket/url/003.html
new file mode 100644
index 0000000..aaae33a
--- /dev/null
+++ b/test/wpt/tests/websockets/interfaces/WebSocket/url/003.html
@@ -0,0 +1,17 @@
+<!doctype html>
+<title>WebSockets: deleting url</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=../../../constants.sub.js></script>
+<meta name="variant" content="?default">
+<meta name="variant" content="?wss">
+<div id=log></div>
+<script>
+test(function() {
+ var ws = new WebSocket(SCHEME_DOMAIN_PORT+'/');
+ delete ws.url;
+ assert_equals(ws.url, SCHEME_DOMAIN_PORT+'/', 'delete ws.url');
+ delete WebSocket.prototype.url;
+ assert_equals(ws.url, undefined, 'delete WebSocket.prototype.url');
+});
+</script>
diff --git a/test/wpt/tests/websockets/interfaces/WebSocket/url/004.html b/test/wpt/tests/websockets/interfaces/WebSocket/url/004.html
new file mode 100644
index 0000000..7db5e1e
--- /dev/null
+++ b/test/wpt/tests/websockets/interfaces/WebSocket/url/004.html
@@ -0,0 +1,17 @@
+<!doctype html>
+<title>WebSockets: 'URL'</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=../../../constants.sub.js></script>
+<meta name="variant" content="?default">
+<meta name="variant" content="?wss">
+<div id=log></div>
+<script>
+test(function() {
+ var ws = new WebSocket(SCHEME_DOMAIN_PORT+'/');
+ assert_equals(ws.URL, undefined);
+ assert_equals('URL' in ws, false);
+ assert_equals(WebSocket.prototype.URL, undefined);
+ assert_equals('URL' in WebSocket.prototype, false);
+});
+</script>
diff --git a/test/wpt/tests/websockets/interfaces/WebSocket/url/005.html b/test/wpt/tests/websockets/interfaces/WebSocket/url/005.html
new file mode 100644
index 0000000..00a5d90
--- /dev/null
+++ b/test/wpt/tests/websockets/interfaces/WebSocket/url/005.html
@@ -0,0 +1,17 @@
+<!doctype html>
+<title>WebSockets: defineProperty getter for url</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=../../../constants.sub.js></script>
+<meta name="variant" content="?default">
+<meta name="variant" content="?wss">
+<div id=log></div>
+<script>
+test(function() {
+ Object.defineProperty(WebSocket.prototype, 'url', {
+ get: function() { return 'foo'; }
+ });
+ var ws = new WebSocket('ws://example.invalid/');
+ assert_equals(ws.url, 'foo');
+});
+</script>
diff --git a/test/wpt/tests/websockets/interfaces/WebSocket/url/006.html b/test/wpt/tests/websockets/interfaces/WebSocket/url/006.html
new file mode 100644
index 0000000..6e83770
--- /dev/null
+++ b/test/wpt/tests/websockets/interfaces/WebSocket/url/006.html
@@ -0,0 +1,19 @@
+<!doctype html>
+<title>WebSockets: defineProperty setter for url</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=../../../constants.sub.js></script>
+<meta name="variant" content="?default">
+<meta name="variant" content="?wss">
+<div id=log></div>
+<script>
+test(function() {
+ window.setter_ran = false;
+ Object.defineProperty(WebSocket.prototype, 'url', {
+ set: function(v) { window[v] = true; }
+ });
+ var ws = new WebSocket('ws://example.invalid/');
+ ws.url = 'setter_ran';
+ assert_true(setter_ran);
+});
+</script>
diff --git a/test/wpt/tests/websockets/interfaces/WebSocket/url/resolve.html b/test/wpt/tests/websockets/interfaces/WebSocket/url/resolve.html
new file mode 100644
index 0000000..2452073
--- /dev/null
+++ b/test/wpt/tests/websockets/interfaces/WebSocket/url/resolve.html
@@ -0,0 +1,14 @@
+<!doctype html>
+<title>WebSocket#url: resolving</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=../../../constants.sub.js></script>
+<meta name="variant" content="?default">
+<meta name="variant" content="?wss">
+<div id=log></div>
+<script>
+test(function() {
+ var ws = new WebSocket(SCHEME_DOMAIN_PORT + '/echo?foo%20bar baz');
+ assert_equals(ws.url, SCHEME_DOMAIN_PORT + '/echo?foo%20bar%20baz');
+});
+</script>
diff --git a/test/wpt/tests/websockets/keeping-connection-open/001.html b/test/wpt/tests/websockets/keeping-connection-open/001.html
new file mode 100644
index 0000000..ab5bd1a
--- /dev/null
+++ b/test/wpt/tests/websockets/keeping-connection-open/001.html
@@ -0,0 +1,29 @@
+<!doctype html>
+<title>WebSockets: 20s inactivity after handshake</title>
+<meta name=timeout content=long>
+<p>Note: This test takes 20 seconds to run.</p>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=../constants.sub.js></script>
+<meta name="variant" content="?default">
+<meta name="variant" content="?wss">
+<meta name="variant" content="?wpt_flags=h2">
+<div id=log></div>
+<script>
+async_test(function(t) {
+ var ws = new WebSocket(SCHEME_DOMAIN_PORT+'/echo');
+ ws.onclose = ws.onerror = ws.onmessage = t.unreached_func();
+ ws.onopen = t.step_func(function(e) {
+ t.step_timeout(function() {
+ ws.send('test');
+ ws.onmessage = t.step_func(function(e) {
+ assert_equals(e.data, 'test');
+ ws.onclose = t.step_func(function(e) {
+ t.step_timeout(() => t.done(), 50);
+ });
+ ws.close();
+ });
+ }, 20000);
+ })
+});
+</script>
diff --git a/test/wpt/tests/websockets/mixed-content.https.any.js b/test/wpt/tests/websockets/mixed-content.https.any.js
new file mode 100644
index 0000000..b7a6d83
--- /dev/null
+++ b/test/wpt/tests/websockets/mixed-content.https.any.js
@@ -0,0 +1,7 @@
+// META: global=window,worker
+// META: script=constants.sub.js
+
+test(() => {
+ assert_throws_dom('SecurityError', () => CreateInsecureWebSocket(),
+ 'constructor should throw');
+}, 'constructing an insecure WebSocket in a secure context should throw');
diff --git a/test/wpt/tests/websockets/multi-globals/message-received.html b/test/wpt/tests/websockets/multi-globals/message-received.html
new file mode 100644
index 0000000..704b1e3
--- /dev/null
+++ b/test/wpt/tests/websockets/multi-globals/message-received.html
@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<title>"A WebSocket message has been received", with multiple globals in play</title>
+<link rel="help" href="https://html.spec.whatwg.org/multipage/comms.html#feedback-from-the-protocol">
+<link rel="author" title="Domenic Denicola" href="mailto:d@domenic.me">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<!-- This is the entry global -->
+
+<iframe src="support/incumbent.sub.html"></iframe>
+
+<script>
+"use strict";
+setup({ explicit_done: true });
+
+window.onload = function() {
+ async_test(function(t) {
+ frames[0].setupWebSocket("arraybuffer", t.step_func_done(function(relevantWindow, event) {
+ assert_equals(event.constructor, relevantWindow.MessageEvent);
+ assert_equals(event.data.constructor, relevantWindow.ArrayBuffer);
+ }));
+ }, "ArrayBuffer should be created in the relevant realm of the WebSocket");
+
+ async_test(function(t) {
+ frames[0].setupWebSocket("blob", t.step_func_done(function(relevantWindow, event) {
+ assert_equals(event.constructor, relevantWindow.MessageEvent);
+ assert_equals(event.data.constructor, relevantWindow.Blob);
+ }));
+ }, "Blob should be created in the relevant realm of the WebSocket");
+
+ done();
+};
+</script>
diff --git a/test/wpt/tests/websockets/multi-globals/support/incumbent.sub.html b/test/wpt/tests/websockets/multi-globals/support/incumbent.sub.html
new file mode 100644
index 0000000..a138b70
--- /dev/null
+++ b/test/wpt/tests/websockets/multi-globals/support/incumbent.sub.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<title>Incumbent page used as a test helper</title>
+
+<iframe src="relevant.html" id="r"></iframe>
+
+<script>
+"use strict";
+
+const relevant = document.querySelector("#r").contentWindow;
+
+window.setupWebSocket = (binaryType, fn) => {
+ const wsocket = new relevant.WebSocket("ws://{{host}}:{{ports[ws][0]}}/echo");
+
+ wsocket.addEventListener("open", () => {
+ wsocket.binaryType = binaryType;
+ wsocket.send(new ArrayBuffer(15));
+ });
+
+ wsocket.addEventListener("message", ev => {
+ fn(relevant, ev);
+ });
+};
+
+</script>
diff --git a/test/wpt/tests/websockets/multi-globals/support/relevant.html b/test/wpt/tests/websockets/multi-globals/support/relevant.html
new file mode 100644
index 0000000..44f42ed
--- /dev/null
+++ b/test/wpt/tests/websockets/multi-globals/support/relevant.html
@@ -0,0 +1,2 @@
+<!DOCTYPE html>
+<title>Relevant page used as a test helper</title>
diff --git a/test/wpt/tests/websockets/multi-globals/url-parsing/current/current.html b/test/wpt/tests/websockets/multi-globals/url-parsing/current/current.html
new file mode 100644
index 0000000..82a48d4
--- /dev/null
+++ b/test/wpt/tests/websockets/multi-globals/url-parsing/current/current.html
@@ -0,0 +1,2 @@
+<!DOCTYPE html>
+<title>Current page used as a test helper</title>
diff --git a/test/wpt/tests/websockets/multi-globals/url-parsing/incumbent/incumbent.html b/test/wpt/tests/websockets/multi-globals/url-parsing/incumbent/incumbent.html
new file mode 100644
index 0000000..2c5572b
--- /dev/null
+++ b/test/wpt/tests/websockets/multi-globals/url-parsing/incumbent/incumbent.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<title>Incumbent page used as a test helper</title>
+
+<iframe src="../current/current.html" id="current"></iframe>
+
+<script>
+ const current = document.querySelector("#current").contentWindow;
+
+ window.hello = () => {
+ window.ws = new current.WebSocket('foo');
+ ws.close();
+ };
+</script>
diff --git a/test/wpt/tests/websockets/multi-globals/url-parsing/url-parsing.html b/test/wpt/tests/websockets/multi-globals/url-parsing/url-parsing.html
new file mode 100644
index 0000000..21ef6cd
--- /dev/null
+++ b/test/wpt/tests/websockets/multi-globals/url-parsing/url-parsing.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<title>Multiple globals for base URL in WebSocket constructor</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<!-- This is the entry global -->
+
+<iframe src="incumbent/incumbent.html"></iframe>
+
+<script>
+async_test((t) => {
+ onload = t.step_func_done(() => {
+ frames[0].hello();
+ // Inside constructors, "this's relevant settings object" === "current settings object",
+ // because of how "this" is constructed.
+ // https://github.com/whatwg/websockets/issues/46
+ const expectedUrl = new URL('current/foo', location.href);
+ expectedUrl.protocol = 'ws:';
+ assert_equals(frames[0].ws.url, expectedUrl.href);
+ });
+});
+</script>
diff --git a/test/wpt/tests/websockets/opening-handshake/001.html b/test/wpt/tests/websockets/opening-handshake/001.html
new file mode 100644
index 0000000..d8585d8
--- /dev/null
+++ b/test/wpt/tests/websockets/opening-handshake/001.html
@@ -0,0 +1,20 @@
+<!doctype html>
+<title>WebSockets: invalid handshake</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=../constants.sub.js></script>
+<meta name="variant" content="?default">
+<meta name="variant" content="?wss">
+<meta name="variant" content="?wpt_flags=h2">
+<div id=log></div>
+<script>
+async_test(function(t) {
+ var ws = new WebSocket(SCHEME_DOMAIN_PORT+'/invalid');
+ ws.onclose = t.step_func(function(e) {
+ assert_false(e.wasClean);
+ ws.onclose = t.unreached_func();
+ t.step_timeout(() => t.done(), 50);
+ });
+ ws.onmessage = ws.onopen = t.unreached_func();
+});
+</script>
diff --git a/test/wpt/tests/websockets/opening-handshake/002.html b/test/wpt/tests/websockets/opening-handshake/002.html
new file mode 100644
index 0000000..00d8dcc
--- /dev/null
+++ b/test/wpt/tests/websockets/opening-handshake/002.html
@@ -0,0 +1,24 @@
+<!doctype html>
+<title>WebSockets: valid handshake</title>
+<meta name=timeout content=long>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=../constants.sub.js></script>
+<meta name="variant" content="?default">
+<meta name="variant" content="?wss">
+<meta name="variant" content="?wpt_flags=h2">
+<div id=log></div>
+<script>
+async_test(function(t) {
+ var ws = new WebSocket(SCHEME_DOMAIN_PORT+'/echo');
+ ws.onopen = t.step_func(function(e) {
+ ws.onclose = t.step_func(function(e) {
+ assert_equals(e.wasClean, true);
+ ws.onclose = t.unreached_func();
+ t.step_timeout(() => t.done(), 50);
+ })
+ ws.close();
+ });
+ ws.onerror = ws.onmessage = ws.onclose = t.unreached_func();
+});
+</script>
diff --git a/test/wpt/tests/websockets/opening-handshake/003-sets-origin.worker.js b/test/wpt/tests/websockets/opening-handshake/003-sets-origin.worker.js
new file mode 100644
index 0000000..d10e8cb
--- /dev/null
+++ b/test/wpt/tests/websockets/opening-handshake/003-sets-origin.worker.js
@@ -0,0 +1,17 @@
+importScripts("/resources/testharness.js");
+importScripts('../constants.sub.js');
+
+async_test(function(t) {
+ var ws = new WebSocket(SCHEME_DOMAIN_PORT+'/origin');
+ ws.onmessage = t.step_func(function(e) {
+ assert_equals(e.data, location.protocol+'//'+location.host);
+ ws.onclose = t.step_func(function(e) {
+ assert_equals(e.wasClean, true);
+ ws.onclose = t.unreached_func();
+ t.step_timeout(() => t.done(), 50);
+ })
+ ws.close();
+ })
+ ws.onerror = ws.onclose = t.unreached_func();
+}, "origin set in a Worker");
+done();
diff --git a/test/wpt/tests/websockets/opening-handshake/003.html b/test/wpt/tests/websockets/opening-handshake/003.html
new file mode 100644
index 0000000..1fc7535
--- /dev/null
+++ b/test/wpt/tests/websockets/opening-handshake/003.html
@@ -0,0 +1,27 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>WebSockets: origin</title>
+<meta name="timeout" content="long">
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=../constants.sub.js></script>
+<meta name="variant" content="?default">
+<meta name="variant" content="?wss">
+<div id=log></div>
+<script>
+async_test(function(t) {
+ const url = SCHEME_DOMAIN_PORT+'/origin',
+ ws = new WebSocket(url.replace("://", "://天気ã®è‰¯ã„æ—¥."));
+ ws.onmessage = t.step_func(function(e) {
+ assert_equals(e.origin, new URL(url).origin.replace("://", "://xn--n8j6ds53lwwkrqhv28a."))
+ assert_equals(e.data, location.protocol+'//'+location.host);
+ ws.onclose = t.step_func(function(e) {
+ assert_equals(e.wasClean, true);
+ ws.onclose = t.unreached_func();
+ t.step_timeout(() => t.done(), 50);
+ })
+ ws.close();
+ })
+ ws.onerror = ws.onclose = t.unreached_func();
+});
+</script>
diff --git a/test/wpt/tests/websockets/opening-handshake/005.html b/test/wpt/tests/websockets/opening-handshake/005.html
new file mode 100644
index 0000000..dcbd8df
--- /dev/null
+++ b/test/wpt/tests/websockets/opening-handshake/005.html
@@ -0,0 +1,25 @@
+<!doctype html>
+<title>WebSockets: response header and close frame in same packet</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=../constants.sub.js></script>
+<meta name="variant" content="?default">
+<meta name="variant" content="?wss">
+<div id=log></div>
+<script>
+async_test(function(t) {
+ var ws = new WebSocket(SCHEME_DOMAIN_PORT+'/simple_handshake');
+ ws.onmessage = t.unreached_func();
+ ws.onopen = t.step_func(function(e) {
+ ws.onclose = t.step_func(function(e) {
+ assert_equals(e.wasClean, true);
+ assert_equals(e.code, 1001);
+ assert_equals(e.reason, 'PASS');
+ ws.onclose = t.unreached_func('onclose should not be called twice');
+ t.step_timeout(() => t.done(), 50);
+ })
+ ws.close();
+ })
+ ws.onclose = t.unreached_func('onclose should not be called before onopen');
+});
+</script>
diff --git a/test/wpt/tests/websockets/referrer.any.js b/test/wpt/tests/websockets/referrer.any.js
new file mode 100644
index 0000000..0972a1d
--- /dev/null
+++ b/test/wpt/tests/websockets/referrer.any.js
@@ -0,0 +1,13 @@
+// META: script=constants.sub.js
+
+async_test(t => {
+ const ws = new WebSocket(SCHEME_DOMAIN_PORT + "/referrer");
+ ws.onmessage = t.step_func_done(e => {
+ assert_equals(e.data, "MISSING AS PER FETCH");
+ ws.close();
+ });
+
+ // Avoid timeouts in case of failure
+ ws.onclose = t.unreached_func("close");
+ ws.onerror = t.unreached_func("error");
+}, "Ensure no Referer header is included");
diff --git a/test/wpt/tests/websockets/remove-own-iframe-during-onerror.window.js b/test/wpt/tests/websockets/remove-own-iframe-during-onerror.window.js
new file mode 100644
index 0000000..aa1cf60
--- /dev/null
+++ b/test/wpt/tests/websockets/remove-own-iframe-during-onerror.window.js
@@ -0,0 +1,23 @@
+// META: script=constants.sub.js
+// META: timeout=long
+// META: variant=?default
+// META: variant=?wss
+// META: variant=?wpt_flags=h2
+
+async_test(t => {
+ window.wsurl = SCHEME_DOMAIN_PORT + '/does-not-exist';
+ let wsframe;
+ window.wsonerror = () => {
+ wsframe.remove();
+ // If this didn't crash then the test passed.
+ t.done();
+ };
+ wsframe = document.createElement('iframe');
+ wsframe.srcdoc = `<script>
+const ws = new WebSocket(parent.wsurl);
+ws.onerror = parent.wsonerror;
+</script>`;
+ onload = () => document.body.appendChild(wsframe);
+}, 'removing an iframe from within an onerror handler should work');
+
+done();
diff --git a/test/wpt/tests/websockets/resources/websockets-test-helpers.sub.js b/test/wpt/tests/websockets/resources/websockets-test-helpers.sub.js
new file mode 100644
index 0000000..2680670
--- /dev/null
+++ b/test/wpt/tests/websockets/resources/websockets-test-helpers.sub.js
@@ -0,0 +1,25 @@
+// The file including this must also include `/websockets/constants.sub.js to
+// pick up the necessary constants.
+
+// Opens a new WebSocket connection.
+async function openWebSocket(remoteContextHelper) {
+ let return_value = await remoteContextHelper.executeScript((domain) => {
+ return new Promise((resolve) => {
+ var webSocketInNotRestoredReasonsTests = new WebSocket(domain + '/echo');
+ webSocketInNotRestoredReasonsTests.onopen = () => { resolve(42); };
+ });
+ }, [SCHEME_DOMAIN_PORT]);
+ assert_equals(return_value, 42);
+}
+
+// Opens a new WebSocket connection and then close it.
+async function openThenCloseWebSocket(remoteContextHelper) {
+ let return_value = await remoteContextHelper.executeScript((domain) => {
+ return new Promise((resolve) => {
+ var testWebSocket = new WebSocket(domain + '/echo');
+ testWebSocket.onopen = () => { testWebSocket.close() };
+ testWebSocket.onclose = () => { resolve(42) };
+ });
+ }, [SCHEME_DOMAIN_PORT]);
+ assert_equals(return_value, 42);
+}
diff --git a/test/wpt/tests/websockets/security/001.html b/test/wpt/tests/websockets/security/001.html
new file mode 100644
index 0000000..a34ae39
--- /dev/null
+++ b/test/wpt/tests/websockets/security/001.html
@@ -0,0 +1,16 @@
+<!doctype html>
+<title>WebSockets: wrong accept key</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=../constants.sub.js></script>
+<meta name="variant" content="?default">
+<meta name="variant" content="?wss">
+<div id=log></div>
+<script>
+async_test(function(t) {
+ var ws = new WebSocket(SCHEME_DOMAIN_PORT+'/wrong_accept_key');
+ ws.onclose = t.step_func(function(e) {
+ t.done();
+ });
+});
+</script>
diff --git a/test/wpt/tests/websockets/security/002.html b/test/wpt/tests/websockets/security/002.html
new file mode 100644
index 0000000..3286cca
--- /dev/null
+++ b/test/wpt/tests/websockets/security/002.html
@@ -0,0 +1,20 @@
+<!doctype html>
+<title>WebSockets: check Sec-WebSocket-Key</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=../constants.sub.js></script>
+<meta name="variant" content="?default">
+<meta name="variant" content="?wss">
+<div id=log></div>
+<script>
+async_test(function(t) {
+ var xhr = new XMLHttpRequest();
+ xhr.onload = t.step_func(function() {
+ assert_equals(xhr.responseText, 'PASS');
+ t.done();
+ });
+ xhr.open("GET", "check.py", true);
+ xhr.setRequestHeader('Sec-WebSocket-Key', 'jW7qmdXj5Kk5jTClF1BN3');
+ xhr.send(null);
+});
+</script>
diff --git a/test/wpt/tests/websockets/security/check.py b/test/wpt/tests/websockets/security/check.py
new file mode 100644
index 0000000..716b837
--- /dev/null
+++ b/test/wpt/tests/websockets/security/check.py
@@ -0,0 +1,2 @@
+def main(request, response):
+ return b"FAIL" if b'Sec-WebSocket-Key' in request.headers else b"PASS"
diff --git a/test/wpt/tests/websockets/send-many-64K-messages-with-backpressure.any.js b/test/wpt/tests/websockets/send-many-64K-messages-with-backpressure.any.js
new file mode 100644
index 0000000..78c244b
--- /dev/null
+++ b/test/wpt/tests/websockets/send-many-64K-messages-with-backpressure.any.js
@@ -0,0 +1,49 @@
+// META: global=window,worker
+// META: script=constants.sub.js
+// META: timeout=long
+// META: variant=?default
+// META: variant=?wss
+// META: variant=?wpt_flags=h2
+
+// This is a repro for Chromium bug https://crbug.com/1286909. It will timeout
+// if the bug is present.
+
+// With 0.1 second server-side delay per message, sending 50 messages will take
+// around 5 seconds.
+const MESSAGES_TO_SEND = 50;
+
+// 65536 is the magic number that triggers the bug, as it precisely fills the
+// mojo pipe.
+const MESSAGE_SIZE = 65536;
+
+promise_test(async t => {
+ const message = new Uint8Array(MESSAGE_SIZE);
+ const ws =
+ new WebSocket(SCHEME_DOMAIN_PORT + '/receive-many-with-backpressure');
+ let opened = false;
+ ws.onopen = t.step_func(() => {
+ opened = true;
+ for (let i = 0; i < MESSAGES_TO_SEND; i++) {
+ ws.send(message);
+ }
+ });
+ let responsesReceived = 0;
+ ws.onmessage = t.step_func(({data}) => {
+ assert_equals(data, String(MESSAGE_SIZE), 'size must match');
+ if (++responsesReceived == MESSAGES_TO_SEND) {
+ ws.close();
+ }
+ });
+ let resolvePromise;
+ const promise = new Promise(resolve => {
+ resolvePromise = resolve;
+ });
+ ws.onclose = t.step_func(({wasClean}) => {
+ assert_true(opened, 'connection should have been opened');
+ assert_true(wasClean, 'close should be clean');
+ resolvePromise();
+ });
+ return promise;
+},
+ `sending ${MESSAGES_TO_SEND} messages of size ${MESSAGE_SIZE} with ` +
+ 'backpressure applied should not hang');
diff --git a/test/wpt/tests/websockets/stream/tentative/README.md b/test/wpt/tests/websockets/stream/tentative/README.md
new file mode 100644
index 0000000..6c51588
--- /dev/null
+++ b/test/wpt/tests/websockets/stream/tentative/README.md
@@ -0,0 +1,9 @@
+# WebSocketStream tentative tests
+
+Tests in this directory are for the proposed "WebSocketStream" interface to the
+WebSocket protocol. This is not yet standardised and browsers should not be
+expected to pass these tests.
+
+See the explainer at
+https://github.com/ricea/websocketstream-explainer/blob/master/README.md for
+more information about the API.
diff --git a/test/wpt/tests/websockets/stream/tentative/abort.any.js b/test/wpt/tests/websockets/stream/tentative/abort.any.js
new file mode 100644
index 0000000..9047f24
--- /dev/null
+++ b/test/wpt/tests/websockets/stream/tentative/abort.any.js
@@ -0,0 +1,50 @@
+// META: script=../../constants.sub.js
+// META: script=resources/url-constants.js
+// META: script=/common/utils.js
+// META: global=window,worker
+// META: variant=?wss
+// META: variant=?wpt_flags=h2
+
+promise_test(async t => {
+ const controller = new AbortController();
+ controller.abort();
+ const key = token();
+ const wsUrl = new URL(
+ `/fetch/api/resources/stash-put.py?key=${key}&value=connected`,
+ location.href);
+ wsUrl.protocol = wsUrl.protocol.replace('http', 'ws');
+ // We intentionally use the port for the HTTP server, not the WebSocket
+ // server, because we don't expect the connection to be performed.
+ const wss = new WebSocketStream(wsUrl, { signal: controller.signal });
+ await promise_rejects_dom(
+ t, 'AbortError', wss.opened, 'opened should reject');
+ await promise_rejects_dom(
+ t, 'AbortError', wss.closed, 'closed should reject');
+ // An incorrect implementation could pass this test due a race condition,
+ // but it is hard to completely eliminate the possibility.
+ const response = await fetch(`/fetch/api/resources/stash-take.py?key=${key}`);
+ assert_equals(await response.text(), 'null', 'response should be null');
+}, 'abort before constructing should prevent connection');
+
+promise_test(async t => {
+ const controller = new AbortController();
+ const wss = new WebSocketStream(`${BASEURL}/handshake_sleep_2`,
+ { signal: controller.signal });
+ // Give the connection a chance to start.
+ await new Promise(resolve => t.step_timeout(resolve, 0));
+ controller.abort();
+ await promise_rejects_dom(
+ t, 'AbortError', wss.opened, 'opened should reject');
+ await promise_rejects_dom(
+ t, 'AbortError', wss.closed, 'closed should reject');
+}, 'abort during handshake should work');
+
+promise_test(async t => {
+ const controller = new AbortController();
+ const wss = new WebSocketStream(ECHOURL, { signal: controller.signal });
+ const { readable, writable } = await wss.opened;
+ controller.abort();
+ writable.getWriter().write('connected');
+ const { value } = await readable.getReader().read();
+ assert_equals(value, 'connected', 'value should match');
+}, 'abort after connect should do nothing');
diff --git a/test/wpt/tests/websockets/stream/tentative/backpressure-receive.any.js b/test/wpt/tests/websockets/stream/tentative/backpressure-receive.any.js
new file mode 100644
index 0000000..236bb2e
--- /dev/null
+++ b/test/wpt/tests/websockets/stream/tentative/backpressure-receive.any.js
@@ -0,0 +1,40 @@
+// META: script=../../constants.sub.js
+// META: script=resources/url-constants.js
+// META: global=window,worker
+// META: timeout=long
+// META: variant=?wss
+// META: variant=?wpt_flags=h2
+
+// Allow for this much timer jitter.
+const JITTER_ALLOWANCE_MS = 200;
+const LARGE_MESSAGE_COUNT = 16;
+
+// This test works by using a server WebSocket handler which sends a large
+// message, and then sends a second message with the time it measured the first
+// message taking. On the browser side, we wait 2 seconds before reading from
+// the socket. This should ensure it takes at least 2 seconds to finish sending
+// the large message.
+promise_test(async t => {
+ const wss = new WebSocketStream(`${BASEURL}/send-backpressure`);
+ const { readable } = await wss.opened;
+ const reader = readable.getReader();
+
+ // Create backpressure for 2 seconds.
+ await new Promise(resolve => t.step_timeout(resolve, 2000));
+
+ // Skip the empty message used to fill the readable queue.
+ await reader.read();
+
+ // Skip the large messages.
+ for (let i = 0; i < LARGE_MESSAGE_COUNT; ++i) {
+ await reader.read();
+ }
+
+ // Read the time it took.
+ const { value, done } = await reader.read();
+
+ // A browser can pass this test simply by being slow. This may be a source of
+ // flakiness for browsers that do not implement backpressure properly.
+ assert_greater_than_equal(Number(value), 2 - JITTER_ALLOWANCE_MS / 1000,
+ 'data send should have taken at least 2 seconds');
+}, 'backpressure should be applied to received messages');
diff --git a/test/wpt/tests/websockets/stream/tentative/backpressure-send.any.js b/test/wpt/tests/websockets/stream/tentative/backpressure-send.any.js
new file mode 100644
index 0000000..e4a80f6
--- /dev/null
+++ b/test/wpt/tests/websockets/stream/tentative/backpressure-send.any.js
@@ -0,0 +1,25 @@
+// META: script=../../constants.sub.js
+// META: script=resources/url-constants.js
+// META: global=window,worker
+// META: timeout=long
+// META: variant=?wss
+// META: variant=?wpt_flags=h2
+
+// Allow for this much timer jitter.
+const JITTER_ALLOWANCE_MS = 200;
+
+// The amount of buffering a WebSocket connection has is not standardised, but
+// it's reasonable to expect that it will not be as large as 8MB.
+const MESSAGE_SIZE = 8 * 1024 * 1024;
+
+// In this test, the server WebSocket handler waits 2 seconds, and the browser
+// times how long it takes to send the first message.
+promise_test(async t => {
+ const wss = new WebSocketStream(`${BASEURL}/receive-backpressure`);
+ const { writable } = await wss.opened;
+ const writer = writable.getWriter();
+ const start = performance.now();
+ await writer.write(new Uint8Array(MESSAGE_SIZE));
+ const elapsed = performance.now() - start;
+ assert_greater_than_equal(elapsed, 2000 - JITTER_ALLOWANCE_MS);
+}, 'backpressure should be applied to sent messages');
diff --git a/test/wpt/tests/websockets/stream/tentative/close.any.js b/test/wpt/tests/websockets/stream/tentative/close.any.js
new file mode 100644
index 0000000..2be9034
--- /dev/null
+++ b/test/wpt/tests/websockets/stream/tentative/close.any.js
@@ -0,0 +1,187 @@
+// META: script=../../constants.sub.js
+// META: script=resources/url-constants.js
+// META: global=window,worker
+// META: variant=?wss
+// META: variant=?wpt_flags=h2
+
+promise_test(async () => {
+ const wss = new WebSocketStream(ECHOURL);
+ await wss.opened;
+ wss.close({code: 3456, reason: 'pizza'});
+ const { code, reason } = await wss.closed;
+ assert_equals(code, 3456, 'code should match');
+ assert_equals(reason, 'pizza', 'reason should match');
+}, 'close code should be sent to server and reflected back');
+
+promise_test(async () => {
+ const wss = new WebSocketStream(ECHOURL);
+ await wss.opened;
+ wss.close();
+ const { code, reason } = await wss.closed;
+ assert_equals(code, 1005, 'code should be unset');
+ assert_equals(reason, '', 'reason should be empty');
+}, 'no close argument should send empty Close frame');
+
+promise_test(async () => {
+ const wss = new WebSocketStream(ECHOURL);
+ await wss.opened;
+ wss.close({});
+ const { code, reason } = await wss.closed;
+ assert_equals(code, 1005, 'code should be unset');
+ assert_equals(reason, '', 'reason should be empty');
+}, 'unspecified close code should send empty Close frame');
+
+promise_test(async () => {
+ const wss = new WebSocketStream(ECHOURL);
+ await wss.opened;
+ wss.close({reason: ''});
+ const { code, reason } = await wss.closed;
+ assert_equals(code, 1005, 'code should be unset');
+ assert_equals(reason, '', 'reason should be empty');
+}, 'unspecified close code with empty reason should send empty Close frame');
+
+promise_test(async () => {
+ const wss = new WebSocketStream(ECHOURL);
+ await wss.opened;
+ wss.close({reason: 'non-empty'});
+ const { code, reason } = await wss.closed;
+ assert_equals(code, 1000, 'code should be set');
+ assert_equals(reason, 'non-empty', 'reason should match');
+}, 'unspecified close code with non-empty reason should set code to 1000');
+
+promise_test(async () => {
+ const wss = new WebSocketStream(ECHOURL);
+ await wss.opened;
+ assert_throws_js(TypeError, () => wss.close(true),
+ 'close should throw a TypeError');
+}, 'close(true) should throw a TypeError');
+
+promise_test(async () => {
+ const wss = new WebSocketStream(ECHOURL);
+ await wss.opened;
+ const reason = '.'.repeat(124);
+ assert_throws_dom('SyntaxError', () => wss.close({ reason }),
+ 'close should throw a TypeError');
+}, 'close() with an overlong reason should throw');
+
+promise_test(t => {
+ const wss = new WebSocketStream(ECHOURL);
+ wss.close();
+ return Promise.all([
+ promise_rejects_dom(
+ t, 'NetworkError', wss.opened, 'opened promise should reject'),
+ promise_rejects_dom(
+ t, 'NetworkError', wss.closed, 'closed promise should reject'),
+ ]);
+}, 'close during handshake should work');
+
+for (const invalidCode of [999, 1001, 2999, 5000]) {
+ promise_test(async () => {
+ const wss = new WebSocketStream(ECHOURL);
+ await wss.opened;
+ assert_throws_dom('InvalidAccessError', () => wss.close({ code: invalidCode }),
+ 'close should throw a TypeError');
+ }, `close() with invalid code ${invalidCode} should throw`);
+}
+
+promise_test(async () => {
+ const wss = new WebSocketStream(ECHOURL);
+ const { writable } = await wss.opened;
+ writable.getWriter().close();
+ const { code, reason } = await wss.closed;
+ assert_equals(code, 1005, 'code should be unset');
+ assert_equals(reason, '', 'reason should be empty');
+}, 'closing the writable should result in a clean close');
+
+promise_test(async () => {
+ const wss = new WebSocketStream(`${BASEURL}/delayed-passive-close`);
+ const { writable } = await wss.opened;
+ const startTime = performance.now();
+ await writable.getWriter().close();
+ const elapsed = performance.now() - startTime;
+ const jitterAllowance = 100;
+ assert_greater_than_equal(elapsed, 1000 - jitterAllowance,
+ 'one second should have elapsed');
+}, 'writer close() promise should not resolve until handshake completes');
+
+const abortOrCancel = [
+ {
+ method: 'abort',
+ voweling: 'aborting',
+ stream: 'writable',
+ },
+ {
+ method: 'cancel',
+ voweling: 'canceling',
+ stream: 'readable',
+ },
+];
+
+for (const { method, voweling, stream } of abortOrCancel) {
+
+ promise_test(async () => {
+ const wss = new WebSocketStream(ECHOURL);
+ const info = await wss.opened;
+ info[stream][method]();
+ const { code, reason } = await wss.closed;
+ assert_equals(code, 1005, 'code should be unset');
+ assert_equals(reason, '', 'reason should be empty');
+ }, `${voweling} the ${stream} should result in a clean close`);
+
+ promise_test(async () => {
+ const wss = new WebSocketStream(ECHOURL);
+ const info = await wss.opened;
+ info[stream][method]({ code: 3333 });
+ const { code, reason } = await wss.closed;
+ assert_equals(code, 3333, 'code should be used');
+ assert_equals(reason, '', 'reason should be empty');
+ }, `${voweling} the ${stream} with a code should send that code`);
+
+ promise_test(async () => {
+ const wss = new WebSocketStream(ECHOURL);
+ const info = await wss.opened;
+ info[stream][method]({ code: 3456, reason: 'set' });
+ const { code, reason } = await wss.closed;
+ assert_equals(code, 3456, 'code should be used');
+ assert_equals(reason, 'set', 'reason should be used');
+ }, `${voweling} the ${stream} with a code and reason should use them`);
+
+ promise_test(async () => {
+ const wss = new WebSocketStream(ECHOURL);
+ const info = await wss.opened;
+ info[stream][method]({ reason: 'specified' });
+ const { code, reason } = await wss.closed;
+ assert_equals(code, 1005, 'code should be unset');
+ assert_equals(reason, '', 'reason should be empty');
+ }, `${voweling} the ${stream} with a reason but no code should be ignored`);
+
+ promise_test(async () => {
+ const wss = new WebSocketStream(ECHOURL);
+ const info = await wss.opened;
+ info[stream][method]({ code: 999 });
+ const { code, reason } = await wss.closed;
+ assert_equals(code, 1005, 'code should be unset');
+ assert_equals(reason, '', 'reason should be empty');
+ }, `${voweling} the ${stream} with an invalid code should be ignored`);
+
+ promise_test(async () => {
+ const wss = new WebSocketStream(ECHOURL);
+ const info = await wss.opened;
+ info[stream][method]({ code: 1000, reason: 'x'.repeat(128) });
+ const { code, reason } = await wss.closed;
+ assert_equals(code, 1005, 'code should be unset');
+ assert_equals(reason, '', 'reason should be empty');
+ }, `${voweling} the ${stream} with an invalid reason should be ignored`);
+
+ // DOMExceptions are only ignored because the |code| attribute is too small to
+ // be a valid WebSocket close code.
+ promise_test(async () => {
+ const wss = new WebSocketStream(ECHOURL);
+ const info = await wss.opened;
+ info[stream][method](new DOMException('yes', 'DataCloneError'));
+ const { code, reason } = await wss.closed;
+ assert_equals(code, 1005, 'code should be unset');
+ assert_equals(reason, '', 'reason should be empty');
+ }, `${voweling} the ${stream} with a DOMException should be ignored`);
+
+}
diff --git a/test/wpt/tests/websockets/stream/tentative/constructor.any.js b/test/wpt/tests/websockets/stream/tentative/constructor.any.js
new file mode 100644
index 0000000..4d67d81
--- /dev/null
+++ b/test/wpt/tests/websockets/stream/tentative/constructor.any.js
@@ -0,0 +1,67 @@
+// META: script=../../constants.sub.js
+// META: script=resources/url-constants.js
+// META: global=window,worker
+// META: variant=?wss
+// META: variant=?wpt_flags=h2
+
+test(() => {
+ assert_throws_js(TypeError, () => new WebSocketStream(),
+ 'constructor should throw');
+}, 'constructing with no URL should throw');
+
+test(() => {
+ assert_throws_dom('SyntaxError', () => new WebSocketStream('invalid:'),
+ 'constructor should throw');
+}, 'constructing with an invalid URL should throw');
+
+test(() => {
+ assert_throws_js(TypeError,
+ () => new WebSocketStream(`${BASEURL}/`, true),
+ 'constructor should throw');
+}, 'constructing with invalid options should throw');
+
+test(() => {
+ assert_throws_js(TypeError,
+ () => new WebSocketStream(`${BASEURL}/`, {protocols: 'hi'}),
+ 'constructor should throw');
+}, 'protocols should be required to be a list');
+
+promise_test(async () => {
+ const wss = new WebSocketStream(ECHOURL);
+ await wss.opened;
+ assert_equals(wss.url, ECHOURL, 'url should match');
+ wss.close();
+}, 'constructing with a valid URL should work');
+
+promise_test(async () => {
+ const wss = new WebSocketStream(`${BASEURL}/protocol_array`,
+ {protocols: ['alpha', 'beta']});
+ const { readable, protocol } = await wss.opened;
+ assert_equals(protocol, 'alpha', 'protocol should be right');
+ const reader = readable.getReader();
+ const { value, done } = await reader.read();
+ assert_equals(value, 'alpha', 'message contents should match');
+ wss.close();
+}, 'setting a protocol in the constructor should work');
+
+promise_test(t => {
+ const wss = new WebSocketStream(`${BASEURL}/404`);
+ return Promise.all([
+ promise_rejects_dom(t, 'NetworkError', wss.opened, 'opened should reject'),
+ promise_rejects_dom(t, 'NetworkError', wss.closed, 'closed should reject'),
+ ]);
+}, 'connection failure should reject the promises');
+
+promise_test(async () => {
+ const wss = new WebSocketStream(ECHOURL);
+ const { readable, writable, protocol, extensions} = await wss.opened;
+ // Verify that |readable| really is a ReadableStream using the getReader()
+ // brand check. If it doesn't throw the test passes.
+ ReadableStream.prototype.getReader.call(readable);
+ // Verify that |writable| really is a WritableStream using the getWriter()
+ // brand check. If it doesn't throw the test passes.
+ WritableStream.prototype.getWriter.call(writable);
+ assert_equals(typeof protocol, 'string', 'protocol should be a string');
+ assert_equals(typeof extensions, 'string', 'extensions should be a string');
+ wss.close();
+}, 'wss.opened should resolve to the right types');
diff --git a/test/wpt/tests/websockets/stream/tentative/resources/url-constants.js b/test/wpt/tests/websockets/stream/tentative/resources/url-constants.js
new file mode 100644
index 0000000..fe681af
--- /dev/null
+++ b/test/wpt/tests/websockets/stream/tentative/resources/url-constants.js
@@ -0,0 +1,8 @@
+// The file including this must also include ../constants.sub.js to pick up the
+// necessary constants.
+
+const {BASEURL, ECHOURL} = (() => {
+ const BASEURL = SCHEME_DOMAIN_PORT;
+ const ECHOURL = `${BASEURL}/echo`;
+ return {BASEURL, ECHOURL};
+})();
diff --git a/test/wpt/tests/websockets/unload-a-document/001-1.html b/test/wpt/tests/websockets/unload-a-document/001-1.html
new file mode 100644
index 0000000..7cf4ab6
--- /dev/null
+++ b/test/wpt/tests/websockets/unload-a-document/001-1.html
@@ -0,0 +1,25 @@
+<!doctype html>
+<title>WebSockets: navigating top-level browsing context</title>
+<script src=../constants.sub.js></script>
+<script>
+var controller = opener || parent;
+var t = controller.t;
+var assert_unreached = controller.assert_unreached;
+var uuid = controller.uuid;
+t.add_cleanup(function() {delete sessionStorage[uuid];});
+t.step(function() {
+ if (sessionStorage[uuid]) {
+ t.done();
+ } else {
+ sessionStorage[uuid] = 'true';
+ var ws = new WebSocket(SCHEME_DOMAIN_PORT+'/echo');
+ ws.onopen = t.step_func(function(e) {
+ t.step_timeout(function() {
+ assert_unreached('document was not discarded');
+ }, 1000);
+ controller.navigate();
+ })
+ ws.onerror = ws.onmessage = t.step_func(e => assert_unreached("Got unexpected event " + e.type));
+ }
+});
+</script>
diff --git a/test/wpt/tests/websockets/unload-a-document/001-2.html b/test/wpt/tests/websockets/unload-a-document/001-2.html
new file mode 100644
index 0000000..24c419c
--- /dev/null
+++ b/test/wpt/tests/websockets/unload-a-document/001-2.html
@@ -0,0 +1,4 @@
+<!doctype html>
+<title>WebSockets: navigating top-level browsing context</title>
+<body onload="history.back()"></body>
+</html>
diff --git a/test/wpt/tests/websockets/unload-a-document/001.html b/test/wpt/tests/websockets/unload-a-document/001.html
new file mode 100644
index 0000000..28ce9c4
--- /dev/null
+++ b/test/wpt/tests/websockets/unload-a-document/001.html
@@ -0,0 +1,26 @@
+<!doctype html>
+<title>WebSockets: navigating top-level browsing context</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=/common/utils.js></script>
+<script src=../constants.sub.js></script>
+<meta name="variant" content="?default">
+<meta name="variant" content="?wss">
+<meta name="variant" content="?wpt_flags=h2">
+<p>Test requires popup blocker disabled</p>
+<div id=log></div>
+<script>
+var t = async_test();
+var w;
+var uuid;
+t.step(function() {
+ uuid = token()
+ w = window.open("001-1.html" + location.search);
+ add_result_callback(function() {
+ w.close();
+ });
+});
+navigate = t.step_func(function() {
+ w.location = w.location.href.replace("001-1.html", "001-2.html");
+});
+</script>
diff --git a/test/wpt/tests/websockets/unload-a-document/002-1.html b/test/wpt/tests/websockets/unload-a-document/002-1.html
new file mode 100644
index 0000000..1922bb4
--- /dev/null
+++ b/test/wpt/tests/websockets/unload-a-document/002-1.html
@@ -0,0 +1,32 @@
+<!doctype html>
+<title>WebSockets: navigating top-level browsing context with closed websocket</title>
+<script src=../constants.sub.js></script>
+<script>
+var controller = opener || parent;
+var t = controller.t;
+var assert_equals = controller.assert_equals;
+var assert_unreached = controller.assert_unreached ;
+var uuid = controller.uuid;
+t.add_cleanup(function() {delete sessionStorage[uuid];});
+t.step(function() {
+ // this test can fail if the document is unloaded on navigation e.g. due to OOM
+ if (sessionStorage[uuid]) {
+ assert_unreached('document was discarded');
+ } else {
+ sessionStorage[uuid] = 'true';
+ var ws = new WebSocket(SCHEME_DOMAIN_PORT+'/echo');
+ ws.onopen = t.step_func(function(e) {
+
+ t.step_timeout(function() {
+ assert_equals(ws.readyState, ws.CLOSED, 'ws.readyState');
+ t.done();
+ }, 4000);
+ ws.close();
+ ws.onclose = t.step_func(function() {
+ controller.navigate();
+ });
+ })
+ ws.onerror = ws.onmessage = t.step_func(e => assert_unreached("Got unexpected event " + e.type));
+ }
+});
+</script>
diff --git a/test/wpt/tests/websockets/unload-a-document/002-2.html b/test/wpt/tests/websockets/unload-a-document/002-2.html
new file mode 100644
index 0000000..9a246a1
--- /dev/null
+++ b/test/wpt/tests/websockets/unload-a-document/002-2.html
@@ -0,0 +1,4 @@
+<!doctype html>
+<title>WebSockets: navigating top-level browsing context with closed websocket</title>
+<body onload="history.back()"></body>
+</html>
diff --git a/test/wpt/tests/websockets/unload-a-document/002.html b/test/wpt/tests/websockets/unload-a-document/002.html
new file mode 100644
index 0000000..f79b3b7
--- /dev/null
+++ b/test/wpt/tests/websockets/unload-a-document/002.html
@@ -0,0 +1,27 @@
+<!doctype html>
+<title>WebSockets: navigating top-level browsing context with closed websocket</title>
+<meta name=timeout content=long>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=/common/utils.js></script>
+<script src=../constants.sub.js></script>
+<meta name="variant" content="?default">
+<meta name="variant" content="?wss">
+<meta name="variant" content="?wpt_flags=h2">
+<p>Test requires popup blocker disabled</p>
+<div id=log></div>
+<script>
+var t = async_test();
+var w;
+var uuid;
+t.step(function() {
+ uuid = token()
+ w = window.open("002-1.html" + location.search);
+ add_result_callback(function() {
+ w.close();
+ });
+});
+navigate = t.step_func(function() {
+ w.location = w.location.href.replace("002-1.html", "002-2.html");
+});
+</script>
diff --git a/test/wpt/tests/websockets/unload-a-document/003.html b/test/wpt/tests/websockets/unload-a-document/003.html
new file mode 100644
index 0000000..554daf4
--- /dev/null
+++ b/test/wpt/tests/websockets/unload-a-document/003.html
@@ -0,0 +1,14 @@
+<!doctype html>
+<title>WebSockets: navigating nested browsing context</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=/common/utils.js></script>
+<div id=log></div>
+<script>
+var uuid;
+var t = async_test(function() {uuid = token()});
+var navigate = t.step_func(function() {
+ document.getElementsByTagName("iframe")[0].src = 'data:text/html,<body onload="history.back()">';
+});
+</script>
+<iframe src=001-1.html></iframe> \ No newline at end of file
diff --git a/test/wpt/tests/websockets/unload-a-document/004.html b/test/wpt/tests/websockets/unload-a-document/004.html
new file mode 100644
index 0000000..bb15cd8
--- /dev/null
+++ b/test/wpt/tests/websockets/unload-a-document/004.html
@@ -0,0 +1,16 @@
+<!doctype html>
+<title>WebSockets: navigating nested browsing context with closed websocket</title>
+<meta name=timeout content=long>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=/common/utils.js></script>
+<div id=log></div>
+<script>
+var uuid;
+var t = async_test();
+t.step(function() {uuid = token()});
+var navigate = t.step_func(function() {
+ document.getElementsByTagName("iframe")[0].src = 'data:text/html,<body onload="history.back()">';
+});
+</script>
+<iframe src=002-1.html></iframe>
diff --git a/test/wpt/tests/websockets/unload-a-document/005-1.html b/test/wpt/tests/websockets/unload-a-document/005-1.html
new file mode 100644
index 0000000..e084ade
--- /dev/null
+++ b/test/wpt/tests/websockets/unload-a-document/005-1.html
@@ -0,0 +1,22 @@
+<!doctype html>
+<title>WebSockets: navigating nested browsing context with a websocket in top-level</title>
+<script src=../constants.sub.js></script>
+<script>
+var t = opener.t;
+var assert_unreached = opener.assert_unreached;
+var hasRun = false;
+function run(){
+ var ws = new WebSocket(SCHEME_DOMAIN_PORT+'/echo');
+ ws.onopen = t.step_func(function(e) {
+ t.step_timeout(function() {
+ ws.send('test');
+ }, 1000);
+ window[0].location = 'data:text/html,<body onload="history.back()">';
+ ws.onmessage = t.step_func_done(function(e) {
+ ws.close();
+ });
+ });
+ ws.onerror = ws.onmessage = ws.onclose = t.step_func(e => assert_unreached("Got unexpected event " + e.type));
+}
+</script>
+<iframe src='data:text/html,foo' onload='if (hasRun) return; hasRun = true; t.step(run)'></iframe>
diff --git a/test/wpt/tests/websockets/unload-a-document/005.html b/test/wpt/tests/websockets/unload-a-document/005.html
new file mode 100644
index 0000000..1abb1b5
--- /dev/null
+++ b/test/wpt/tests/websockets/unload-a-document/005.html
@@ -0,0 +1,21 @@
+<!doctype html>
+<title>WebSockets: navigating nested browsing context with a websocket in top-level</title>
+<meta name=timeout content=long>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=../constants.sub.js></script>
+<meta name="variant" content="?default">
+<meta name="variant" content="?wss">
+<meta name="variant" content="?wpt_flags=h2">
+<div id=log></div>
+<p>Test requires popup blocker disabled</p>
+<div id=log></div>
+<script>
+var t = async_test();
+t.step(function() {
+ var w = window.open("005-1.html" + location.search);
+ add_result_callback(function() {
+ w.close();
+ });
+});
+</script>
diff --git a/test/wpt/tests/wpt b/test/wpt/tests/wpt
new file mode 100644
index 0000000..e0abacd
--- /dev/null
+++ b/test/wpt/tests/wpt
@@ -0,0 +1,10 @@
+#!/usr/bin/env python3
+
+if __name__ == "__main__":
+ import sys
+ if sys.version_info < (3, 7):
+ sys.stderr.write("wpt requires Python 3.7 or higher\n")
+ sys.exit(1)
+
+ from tools.wpt import wpt
+ wpt.main()
diff --git a/test/wpt/tests/wpt.py b/test/wpt/tests/wpt.py
new file mode 100644
index 0000000..c38fe78
--- /dev/null
+++ b/test/wpt/tests/wpt.py
@@ -0,0 +1,7 @@
+# This file exists to allow `python wpt <command>` to work on Windows:
+# https://github.com/web-platform-tests/wpt/pull/6907 and
+# https://github.com/web-platform-tests/wpt/issues/23095
+import os
+abspath = os.path.abspath(__file__)
+os.chdir(os.path.dirname(abspath))
+exec(compile(open("wpt", "r").read(), "wpt", 'exec'))
diff --git a/test/wpt/tests/xhr/META.yml b/test/wpt/tests/xhr/META.yml
new file mode 100644
index 0000000..c343cea
--- /dev/null
+++ b/test/wpt/tests/xhr/META.yml
@@ -0,0 +1,7 @@
+spec: https://xhr.spec.whatwg.org/
+suggested_reviewers:
+ - caitp
+ - Manishearth
+ - jdm
+ - annevk
+ - wisniewskit
diff --git a/test/wpt/tests/xhr/README.md b/test/wpt/tests/xhr/README.md
new file mode 100644
index 0000000..8fbe615
--- /dev/null
+++ b/test/wpt/tests/xhr/README.md
@@ -0,0 +1,7 @@
+Tests for the [XMLHttpRequest Standard](https://xhr.spec.whatwg.org/).
+
+More XMLHttpRequest-related tests can be found in
+
+* /cors
+* /fetch
+* /url (failure.html in particular)
diff --git a/test/wpt/tests/xhr/XMLHttpRequest-withCredentials.any.js b/test/wpt/tests/xhr/XMLHttpRequest-withCredentials.any.js
new file mode 100644
index 0000000..27ffa70
--- /dev/null
+++ b/test/wpt/tests/xhr/XMLHttpRequest-withCredentials.any.js
@@ -0,0 +1,40 @@
+test(function() {
+ var client = new XMLHttpRequest()
+ assert_false(client.withCredentials, "withCredentials defaults to false")
+ client.withCredentials = true
+ assert_true(client.withCredentials, "is true after setting")
+}, "default value is false, set value is true")
+
+test(function() {
+ var client = new XMLHttpRequest()
+ client.open("GET", "resources/delay.py?ms=1000", true)
+ client.withCredentials = true
+ assert_true(client.withCredentials, "set in OPEN state")
+}, "can also be set in OPEN state")
+
+test(function() {
+ var client = new XMLHttpRequest()
+ client.open("GET", "resources/delay.py?ms=1000", false)
+ client.withCredentials = true
+ assert_true(client.withCredentials, "set in OPEN state")
+}, "setting on synchronous XHR")
+
+async_test(function() {
+ var client = new XMLHttpRequest()
+ client.open("GET", "resources/delay.py?ms=1000")
+ client.send()
+ assert_throws_dom("InvalidStateError", function() { client.withCredentials = true })
+ client.onreadystatechange = this.step_func(function() {
+ assert_throws_dom("InvalidStateError", function() { client.withCredentials = true })
+ if (client.readyState === 4) {
+ this.done()
+ }
+ })
+}, "setting withCredentials when not in UNSENT, OPENED state (asynchronous)")
+
+test(function() {
+ var client = new XMLHttpRequest()
+ client.open("GET", "resources/delay.py?ms=1000", false)
+ client.send();
+ assert_throws_dom("InvalidStateError", function() { client.withCredentials = true })
+}, "setting withCredentials when in DONE state (synchronous)")
diff --git a/test/wpt/tests/xhr/abort-after-receive.any.js b/test/wpt/tests/xhr/abort-after-receive.any.js
new file mode 100644
index 0000000..d42d6d6
--- /dev/null
+++ b/test/wpt/tests/xhr/abort-after-receive.any.js
@@ -0,0 +1,30 @@
+// META: title=XMLHttpRequest: abort() after successful receive should not fire "abort" event
+
+ var test = async_test();
+
+ test.step(function() {
+ var client = new XMLHttpRequest();
+
+ client.onreadystatechange = test.step_func(function() {
+ if (client.readyState == 4) {
+ // abort should not cause the "abort" event to fire
+
+ client.abort();
+
+ assert_equals(client.readyState, 0);
+
+ test.step_timeout(function(){ // use a timeout to catch any implementation that might queue an abort event for later - just in case
+ test.done()
+ }, 200);
+ }
+ });
+
+ client.onabort = test.step_func(function () {
+ // this should not fire!
+
+ assert_unreached("abort() should not cause the abort event to fire");
+ });
+
+ client.open("GET", "resources/well-formed.xml", true);
+ client.send(null);
+ });
diff --git a/test/wpt/tests/xhr/abort-after-send.any.js b/test/wpt/tests/xhr/abort-after-send.any.js
new file mode 100644
index 0000000..0ffd887
--- /dev/null
+++ b/test/wpt/tests/xhr/abort-after-send.any.js
@@ -0,0 +1,29 @@
+// META: title=XMLHttpRequest: abort() after send()
+// META: script=resources/xmlhttprequest-event-order.js
+
+ var test = async_test()
+ test.step(function() {
+ var client = new XMLHttpRequest(),
+ control_flag = false;
+ prepare_xhr_for_event_order_test(client);
+ client.addEventListener("readystatechange", test.step_func(function() {
+ if(client.readyState == 4) {
+ control_flag = true
+ if (self.GLOBAL.isWindow()) {
+ assert_equals(client.responseXML, null)
+ }
+ assert_equals(client.responseText, "")
+ assert_equals(client.status, 0)
+ assert_equals(client.statusText, "")
+ assert_equals(client.getAllResponseHeaders(), "")
+ assert_equals(client.getResponseHeader('Content-Type'), null)
+ }
+ }))
+ client.open("GET", "resources/well-formed.xml", true)
+ client.send(null)
+ client.abort()
+ assert_true(control_flag)
+ assert_equals(client.readyState, 0)
+ assert_xhr_event_order_matches([1, "loadstart(0,0,false)", 4, "abort(0,0,false)", "loadend(0,0,false)"])
+ test.done()
+ })
diff --git a/test/wpt/tests/xhr/abort-after-stop.window.js b/test/wpt/tests/xhr/abort-after-stop.window.js
new file mode 100644
index 0000000..a254648
--- /dev/null
+++ b/test/wpt/tests/xhr/abort-after-stop.window.js
@@ -0,0 +1,22 @@
+// META: title=XMLHttpRequest: abort event should fire when stop() method is used
+
+ var test = async_test();
+ window.onload = test.step_func(function() {
+ var client = new XMLHttpRequest();
+ var abortFired = false;
+ var sync = true;
+ client.onabort = test.step_func(function (e) {
+ assert_false(sync);
+ assert_equals(e.type, 'abort');
+ assert_equals(client.status, 0);
+ abortFired = true;
+ });
+ client.open("GET", "resources/delay.py?ms=3000", true);
+ client.send(null);
+ test.step_timeout(() => {
+ assert_equals(abortFired, true);
+ test.done();
+ }, 200);
+ window.stop();
+ sync = false;
+ });
diff --git a/test/wpt/tests/xhr/abort-after-timeout.any.js b/test/wpt/tests/xhr/abort-after-timeout.any.js
new file mode 100644
index 0000000..fe8b749
--- /dev/null
+++ b/test/wpt/tests/xhr/abort-after-timeout.any.js
@@ -0,0 +1,43 @@
+// META: title=XMLHttpRequest: abort() after a timeout should not fire "abort" event
+
+ var test = async_test();
+
+ test.step(function() {
+ // timeout is 100ms
+ // the download would otherwise take 1000ms
+ // we check after 300ms to make sure abort does not fire an "abort" event
+
+ var timeoutFired = false;
+
+ var client = new XMLHttpRequest();
+
+ assert_true('timeout' in client, 'xhr.timeout is not supported in this user agent');
+
+ client.timeout = 100;
+
+ test.step_timeout(() => {
+ assert_true(timeoutFired);
+
+ // abort should not cause the "abort" event to fire
+ client.abort();
+
+ test.step_timeout(() => { // use a timeout to catch any implementation that might queue an abort event for later - just in case
+ test.done()
+ }, 200);
+
+ assert_equals(client.readyState, 0);
+ }, 300);
+
+ client.ontimeout = function () {
+ timeoutFired = true;
+ };
+
+ client.onabort = test.step_func(function () {
+ // this should not fire!
+
+ assert_unreached("abort() should not cause the abort event to fire");
+ });
+
+ client.open("GET", "/common/blank.html?pipe=trickle(d1)", true);
+ client.send(null);
+ });
diff --git a/test/wpt/tests/xhr/abort-during-done.window.js b/test/wpt/tests/xhr/abort-during-done.window.js
new file mode 100644
index 0000000..f885e59
--- /dev/null
+++ b/test/wpt/tests/xhr/abort-during-done.window.js
@@ -0,0 +1,78 @@
+// META: title=XMLHttpRequest: abort() during DONE
+
+ async_test(test => {
+ var client = new XMLHttpRequest(),
+ result = [],
+ expected = [1, 4] // open() -> 1, send() -> 4
+ client.onreadystatechange = test.step_func(function() {
+ result.push(client.readyState)
+ })
+ client.open("GET", "resources/well-formed.xml", false)
+ client.send(null)
+ assert_equals(client.readyState, 4)
+ assert_equals(client.status, 200)
+ assert_equals(client.statusText, "OK")
+ assert_equals(client.responseXML.documentElement.localName, "html")
+ client.abort()
+ assert_equals(client.readyState, 0)
+ assert_equals(client.status, 0)
+ assert_equals(client.statusText, "")
+ assert_equals(client.responseXML, null)
+ assert_equals(client.getAllResponseHeaders(), "")
+ assert_array_equals(result, expected)
+ test.done()
+ }, document.title + " (sync)")
+
+ async_test(test => {
+ var client = new XMLHttpRequest(),
+ result = [],
+ expected = [1, 4] // open() -> 1, send() -> 4
+ client.onreadystatechange = test.step_func(function() {
+ result.push(client.readyState);
+ if (client.readyState === 4) {
+ assert_equals(client.readyState, 4)
+ assert_equals(client.status, 200)
+ assert_equals(client.statusText, "OK")
+ assert_equals(client.responseXML.documentElement.localName, "html")
+ client.abort();
+ assert_equals(client.readyState, 0)
+ assert_equals(client.status, 0)
+ assert_equals(client.statusText, "")
+ assert_equals(client.responseXML, null)
+ assert_equals(client.getAllResponseHeaders(), "")
+ test.done()
+ }
+ })
+ client.open("GET", "resources/well-formed.xml", false)
+ client.send(null)
+ assert_equals(client.readyState, 0)
+ assert_equals(client.status, 200)
+ assert_equals(client.statusText, "OK")
+ assert_equals(client.responseXML.documentElement.localName, "html")
+ }, document.title + " (sync aborted in readystatechange)")
+
+ async_test(test => {
+ var client = new XMLHttpRequest(),
+ result = [],
+ expected = [1, 2, 3, 4]
+ client.onreadystatechange = test.step_func(function() {
+ result.push(client.readyState);
+ if (client.readyState === 4) {
+ assert_equals(client.readyState, 4)
+ assert_equals(client.status, 200)
+ assert_equals(client.responseXML.documentElement.localName, "html")
+ client.abort();
+ assert_equals(client.readyState, 0)
+ assert_equals(client.status, 0)
+ assert_equals(client.statusText, "")
+ assert_equals(client.responseXML, null)
+ assert_equals(client.getAllResponseHeaders(), "")
+ test.step_timeout(function() {
+ assert_array_equals(result, expected)
+ test.done();
+ }, 100); // wait a bit in case XHR timeout causes spurious event
+ }
+ })
+ client.open("GET", "resources/well-formed.xml")
+ client.send(null)
+ }, document.title + " (async)")
diff --git a/test/wpt/tests/xhr/abort-during-headers-received.window.js b/test/wpt/tests/xhr/abort-during-headers-received.window.js
new file mode 100644
index 0000000..0e7140a
--- /dev/null
+++ b/test/wpt/tests/xhr/abort-during-headers-received.window.js
@@ -0,0 +1,41 @@
+// META: title=XMLHttpRequest: abort() during HEADERS_RECEIVED
+
+ async_test(test => {
+ var client = new XMLHttpRequest(),
+ result = [],
+ expected = [1, 2, 4]
+ client.onreadystatechange = test.step_func(function() {
+ result.push(client.readyState);
+ if (client.readyState === 2) {
+ assert_equals(client.status, 200)
+ assert_equals(client.statusText, "OK")
+ assert_equals(client.responseXML, null)
+ client.abort();
+ assert_equals(client.readyState, 0)
+ assert_equals(client.status, 0)
+ assert_equals(client.statusText, "")
+ assert_equals(client.responseXML, null)
+ assert_equals(client.getAllResponseHeaders(), "")
+ }
+ if (client.readyState === 4) {
+ assert_equals(client.readyState, 4)
+ assert_equals(client.status, 0)
+ assert_equals(client.statusText, "")
+ assert_equals(client.responseXML, null)
+ assert_equals(client.getAllResponseHeaders(), "")
+ }
+ })
+ client.onloadend = test.step_func(function() {
+ assert_equals(client.readyState, 4)
+ assert_equals(client.status, 0)
+ assert_equals(client.statusText, "")
+ assert_equals(client.responseXML, null)
+ assert_equals(client.getAllResponseHeaders(), "")
+ test.step_timeout(function() {
+ assert_array_equals(result, expected)
+ test.done();
+ }, 100); // wait a bit in case XHR timeout causes spurious event
+ })
+ client.open("GET", "resources/well-formed.xml")
+ client.send(null)
+ })
diff --git a/test/wpt/tests/xhr/abort-during-loading.window.js b/test/wpt/tests/xhr/abort-during-loading.window.js
new file mode 100644
index 0000000..6fd217b
--- /dev/null
+++ b/test/wpt/tests/xhr/abort-during-loading.window.js
@@ -0,0 +1,41 @@
+// META: title=XMLHttpRequest: abort() during LOADING
+
+ async_test(test => {
+ var client = new XMLHttpRequest(),
+ result = [],
+ expected = [1, 2, 3, 4]
+ client.onreadystatechange = test.step_func(function() {
+ result.push(client.readyState);
+ if (client.readyState === 3) {
+ assert_equals(client.status, 200)
+ assert_equals(client.statusText, "OK")
+ assert_equals(client.responseXML, null)
+ client.abort();
+ assert_equals(client.readyState, 0)
+ assert_equals(client.status, 0)
+ assert_equals(client.statusText, "")
+ assert_equals(client.responseXML, null)
+ assert_equals(client.getAllResponseHeaders(), "")
+ }
+ if (client.readyState === 4) {
+ assert_equals(client.readyState, 4)
+ assert_equals(client.status, 0)
+ assert_equals(client.statusText, "")
+ assert_equals(client.responseXML, null)
+ assert_equals(client.getAllResponseHeaders(), "")
+ }
+ })
+ client.onloadend = test.step_func(function() {
+ assert_equals(client.readyState, 4)
+ assert_equals(client.status, 0)
+ assert_equals(client.statusText, "")
+ assert_equals(client.responseXML, null)
+ assert_equals(client.getAllResponseHeaders(), "")
+ test.step_timeout(function() {
+ assert_array_equals(result, expected)
+ test.done();
+ }, 100); // wait a bit in case XHR timeout causes spurious event
+ })
+ client.open("GET", "resources/well-formed.xml")
+ client.send(null)
+ })
diff --git a/test/wpt/tests/xhr/abort-during-open.any.js b/test/wpt/tests/xhr/abort-during-open.any.js
new file mode 100644
index 0000000..42a1bce
--- /dev/null
+++ b/test/wpt/tests/xhr/abort-during-open.any.js
@@ -0,0 +1,18 @@
+var test = async_test("XMLHttpRequest: abort() during OPEN");
+test.step(function() {
+ var client = new XMLHttpRequest()
+ client.open("GET", "...")
+ client.onreadystatechange = function() {
+ test.step(function() {
+ assert_unreached()
+ })
+ }
+ assert_equals(client.readyState, 1, "before abort()")
+ assert_equals(client.status, 0)
+ assert_equals(client.statusText, "")
+ client.abort()
+ assert_equals(client.readyState, 1, "after abort()")
+ assert_equals(client.status, 0)
+ assert_equals(client.statusText, "")
+})
+test.done()
diff --git a/test/wpt/tests/xhr/abort-during-readystatechange.any.js b/test/wpt/tests/xhr/abort-during-readystatechange.any.js
new file mode 100644
index 0000000..a036376
--- /dev/null
+++ b/test/wpt/tests/xhr/abort-during-readystatechange.any.js
@@ -0,0 +1,19 @@
+"use strict";
+setup({ single_test: true });
+
+const xhr = new XMLHttpRequest();
+
+// In jsdom's implementation, this would cause a crash, as after firing readystatechange for HEADERS_RECEIVED, it would
+// try to manipulate internal state. But that internal state got cleared during abort(). So jsdom needed to be modified
+// to check if that internal state had gone away as a result of firing readystatechange, and if so, bail out.
+
+xhr.addEventListener("readystatechange", () => {
+ if (xhr.readyState === xhr.HEADERS_RECEIVED) {
+ xhr.abort();
+ } else if (xhr.readyState === xhr.DONE) {
+ done();
+ }
+});
+
+xhr.open("GET", "/common/blank.html");
+xhr.send();
diff --git a/test/wpt/tests/xhr/abort-during-unsent.any.js b/test/wpt/tests/xhr/abort-during-unsent.any.js
new file mode 100644
index 0000000..648ca05
--- /dev/null
+++ b/test/wpt/tests/xhr/abort-during-unsent.any.js
@@ -0,0 +1,19 @@
+// META: title=XMLHttpRequest: abort() during UNSENT
+
+ var test = async_test()
+ test.step(function() {
+ var client = new XMLHttpRequest()
+ client.onreadystatechange = function() {
+ test.step(function() {
+ assert_unreached()
+ })
+ }
+ assert_equals(client.readyState, 0)
+ assert_equals(client.status, 0)
+ assert_equals(client.statusText, "")
+ client.abort()
+ assert_equals(client.readyState, 0)
+ assert_equals(client.status, 0)
+ assert_equals(client.statusText, "")
+ })
+ test.done()
diff --git a/test/wpt/tests/xhr/abort-during-upload.any.js b/test/wpt/tests/xhr/abort-during-upload.any.js
new file mode 100644
index 0000000..fe4963d
--- /dev/null
+++ b/test/wpt/tests/xhr/abort-during-upload.any.js
@@ -0,0 +1,17 @@
+// META: title=XMLHttpRequest: abort() while sending data
+// META: script=resources/xmlhttprequest-event-order.js
+
+ var test = async_test()
+ test.step(function() {
+ var client = new XMLHttpRequest()
+ prepare_xhr_for_event_order_test(client);
+ client.open("POST", "resources/delay.py?ms=1000")
+ client.addEventListener("loadend", function(e) {
+ test.step(function() {
+ assert_xhr_event_order_matches([1, "loadstart(0,0,false)", "upload.loadstart(0,9999,true)", 4, "upload.abort(0,0,false)", "upload.loadend(0,0,false)", "abort(0,0,false)", "loadend(0,0,false)"]);
+ test.done()
+ })
+ });
+ client.send((new Array(10000)).join('a'))
+ client.abort()
+ })
diff --git a/test/wpt/tests/xhr/abort-event-abort.any.js b/test/wpt/tests/xhr/abort-event-abort.any.js
new file mode 100644
index 0000000..c730e71
--- /dev/null
+++ b/test/wpt/tests/xhr/abort-event-abort.any.js
@@ -0,0 +1,32 @@
+// META: title=XMLHttpRequest: The abort() method: do not fire abort event in OPENED state when send() flag is unset.
+
+ var test = async_test()
+
+ test.step(function()
+ {
+ var xhr = new XMLHttpRequest()
+
+ xhr.onreadystatechange = function()
+ {
+ test.step(function()
+ {
+ if (xhr.readyState == 1)
+ {
+ xhr.abort();
+ assert_equals(xhr.readyState, 1, "abort() cannot change readyState when readyState is 1 and send() flag is unset")
+ }
+ });
+ };
+
+ xhr.onabort = function(e)
+ {
+ test.step(function()
+ {
+ assert_unreached('when abort() is called, state is OPENED with the send() flag being unset, must not fire abort event per spec')
+ });
+ };
+
+ xhr.open("GET", "./resources/content.py", true); // This should cause a readystatechange event that calls abort()
+ xhr.send() // should not throw since abort() was a no-op
+ test.done()
+ });
diff --git a/test/wpt/tests/xhr/abort-event-listeners.any.js b/test/wpt/tests/xhr/abort-event-listeners.any.js
new file mode 100644
index 0000000..67bbae6
--- /dev/null
+++ b/test/wpt/tests/xhr/abort-event-listeners.any.js
@@ -0,0 +1,13 @@
+// META: title=XMLHttpRequest: abort() should not reset event listeners
+
+ var test = async_test()
+ test.step(function() {
+ var client = new XMLHttpRequest(),
+ test = function() {}
+ client.onreadystatechange = test
+ client.open("GET", "resources/well-formed.xml")
+ client.send(null)
+ client.abort()
+ assert_equals(client.onreadystatechange, test)
+ })
+ test.done()
diff --git a/test/wpt/tests/xhr/abort-event-loadend.any.js b/test/wpt/tests/xhr/abort-event-loadend.any.js
new file mode 100644
index 0000000..7c19c6d
--- /dev/null
+++ b/test/wpt/tests/xhr/abort-event-loadend.any.js
@@ -0,0 +1,30 @@
+// META: title=XMLHttpRequest: The abort() method: Fire a progress event named loadend
+
+ var test = async_test(function(test)
+ {
+ var xhr = new XMLHttpRequest();
+
+ xhr.onloadstart = function()
+ {
+ test.step(function()
+ {
+ if (xhr.readyState == 1)
+ {
+ xhr.abort();
+ }
+ });
+ };
+
+ xhr.onloadend = function(e)
+ {
+ test.step(function()
+ {
+ assert_true(e instanceof ProgressEvent);
+ assert_equals(e.type, "loadend");
+ test.done();
+ });
+ };
+
+ xhr.open("GET", "resources/content.py", true);
+ xhr.send();
+ });
diff --git a/test/wpt/tests/xhr/abort-event-order.htm b/test/wpt/tests/xhr/abort-event-order.htm
new file mode 100644
index 0000000..f05c206
--- /dev/null
+++ b/test/wpt/tests/xhr/abort-event-order.htm
@@ -0,0 +1,52 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-abort()-method" data-tested-assertations="following-sibling::ol/li[4]/ol/li[3] following-sibling::ol/li[4]/ol/li[5] following-sibling::ol/li[4]/ol/li[6] following-sibling::ol/li[4]/ol/li[7]/ol/li[3] following-sibling::ol/li[4]/ol/li[7]/ol/li[4] following-sibling::ol/li[5]" />
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="resources/xmlhttprequest-event-order.js"></script>
+ <title>XMLHttpRequest: The abort() method: abort and loadend events</title>
+</head>
+
+<body>
+ <div id="log"></div>
+
+ <script type="text/javascript">
+ var test = async_test();
+
+ test.step(function()
+ {
+ var xhr = new XMLHttpRequest();
+ prepare_xhr_for_event_order_test(xhr);
+
+ xhr.addEventListener("loadstart", function() {
+ test.step(function()
+ {
+ var readyState = xhr.readyState;
+ if (readyState == 1)
+ {
+ xhr.abort();
+ VerifyResult();
+ } else {
+ assert_unreached('Loadstart event should not fire in readyState '+readyState);
+ }
+ });
+ });
+
+ function VerifyResult()
+ {
+ test.step(function()
+ {
+ assert_xhr_event_order_matches([1, "loadstart(0,0,false)", 4, "upload.abort(0,0,false)", "upload.loadend(0,0,false)", "abort(0,0,false)", "loadend(0,0,false)"]);
+
+ assert_equals(xhr.readyState, 0, 'state should be UNSENT');
+ test.done();
+ });
+ };
+
+ xhr.open("POST", "./resources/content.py", true);
+ xhr.send("Test Message");
+ });
+ </script>
+</body>
+</html>
diff --git a/test/wpt/tests/xhr/abort-upload-event-abort.any.js b/test/wpt/tests/xhr/abort-upload-event-abort.any.js
new file mode 100644
index 0000000..3c85a55
--- /dev/null
+++ b/test/wpt/tests/xhr/abort-upload-event-abort.any.js
@@ -0,0 +1,31 @@
+ var test = async_test("XMLHttpRequest: The abort() method: Fire a progress event named abort on the XMLHttpRequestUpload object");
+
+ test.step(function()
+ {
+ var xhr = new XMLHttpRequest();
+
+ xhr.onloadstart = function()
+ {
+ test.step(function()
+ {
+ if (xhr.readyState == 1)
+ {
+ xhr.abort();
+ }
+ });
+ };
+
+ xhr.upload.onabort = function(e)
+ {
+ test.step(function()
+ {
+ assert_true(e instanceof ProgressEvent);
+ assert_equals(e.type, "abort");
+ assert_equals(e.target, xhr.upload);
+ test.done();
+ });
+ };
+
+ xhr.open("POST", "./resources/content.py", true);
+ xhr.send("Test Message");
+ });
diff --git a/test/wpt/tests/xhr/abort-upload-event-loadend.any.js b/test/wpt/tests/xhr/abort-upload-event-loadend.any.js
new file mode 100644
index 0000000..91c5dc5
--- /dev/null
+++ b/test/wpt/tests/xhr/abort-upload-event-loadend.any.js
@@ -0,0 +1,31 @@
+ var test = async_test("XMLHttpRequest: The abort() method: Fire a progress event named loadend on the XMLHttpRequestUpload object");
+
+ test.step(function()
+ {
+ var xhr = new XMLHttpRequest();
+
+ xhr.onloadstart = function()
+ {
+ test.step(function ()
+ {
+ if (xhr.readyState == 1)
+ {
+ xhr.abort();
+ }
+ });
+ };
+
+ xhr.upload.onloadend = function(e)
+ {
+ test.step(function()
+ {
+ assert_true(e instanceof ProgressEvent);
+ assert_equals(e.type, "loadend");
+ assert_equals(e.target, xhr.upload);
+ test.done();
+ });
+ };
+
+ xhr.open("POST", "./resources/content.py", true);
+ xhr.send("Test Message");
+ });
diff --git a/test/wpt/tests/xhr/access-control-and-redirects-async-same-origin.any.js b/test/wpt/tests/xhr/access-control-and-redirects-async-same-origin.any.js
new file mode 100644
index 0000000..11d38fa
--- /dev/null
+++ b/test/wpt/tests/xhr/access-control-and-redirects-async-same-origin.any.js
@@ -0,0 +1,61 @@
+// META: title=Tests that asynchronous XMLHttpRequests handle redirects according to the CORS standard.
+// META: script=/common/get-host-info.sub.js
+
+ function runTest(test, path, credentials, expectSuccess) {
+ const xhr = new XMLHttpRequest();
+ xhr.withCredentials = credentials;
+ xhr.open("GET", "resources/redirect.py?location=" + get_host_info().HTTP_REMOTE_ORIGIN + path, true);
+
+ xhr.onload = test.step_func_done(function() {
+ assert_true(expectSuccess);
+ assert_equals(xhr.responseText, "PASS: Cross-domain access allowed.");
+ });
+ xhr.onerror = test.step_func_done(function() {
+ assert_false(expectSuccess);
+ assert_equals(xhr.status, 0);
+ });
+ xhr.send(null);
+ }
+
+ const withoutCredentials = false;
+ const withCredentials = true;
+ const succeeds = true;
+ const fails = false;
+
+ // Test simple same origin requests that receive cross origin redirects.
+
+ // The redirect response passes the access check.
+ async_test(t => {
+ runTest(t, "/xhr/resources/access-control-basic-allow-star.py",
+ withoutCredentials, succeeds)
+ }, "Request without credentials is redirected to a cross-origin response with Access-Control-Allow-Origin=* (with star)");
+
+ // The redirect response fails the access check because credentials were sent.
+ async_test(t => {
+ runTest(t, "/xhr/resources/access-control-basic-allow-star.py",
+ withCredentials, fails)
+ }, "Request with credentials is redirected to a cross-origin response with Access-Control-Allow-Origin=* (with star)");
+
+ // The redirect response passes the access check.
+ async_test(t => {
+ runTest(t, "/xhr/resources/access-control-basic-allow.py",
+ withoutCredentials, succeeds)
+ }, "Request without credentials is redirected to a cross-origin response with a specific Access-Control-Allow-Origin");
+
+ // The redirect response passes the access check.
+ async_test(t => {
+ runTest(t, "/xhr/resources/access-control-basic-allow.py",
+ withCredentials, succeeds)
+ }, "Request with credentials is redirected to a cross-origin response with a specific Access-Control-Allow-Origin");
+
+ // forbidding credentials. The redirect response passes the access check.
+ async_test(t => {
+ runTest(t, "/xhr/resources/access-control-basic-allow-no-credentials.py",
+ withoutCredentials, succeeds)
+ }, "Request without credentials is redirected to a cross-origin response with a specific Access-Control-Allow-Origin (no credentials)");
+
+ // forbidding credentials. The redirect response fails the access check.
+ async_test(t => {
+ runTest(t, "/xhr/resources/access-control-basic-allow-no-credentials.py",
+ withCredentials, fails)
+ }, "Request with credentials is redirected to a cross-origin response with a specific Access-Control-Allow-Origin (no credentials)");
diff --git a/test/wpt/tests/xhr/access-control-and-redirects-async.any.js b/test/wpt/tests/xhr/access-control-and-redirects-async.any.js
new file mode 100644
index 0000000..c88b882
--- /dev/null
+++ b/test/wpt/tests/xhr/access-control-and-redirects-async.any.js
@@ -0,0 +1,79 @@
+// META: title=Tests that asynchronous XMLHttpRequests handle redirects according to the CORS standard.
+// META: script=/common/get-host-info.sub.js
+
+ function runTest(test, destination, parameters, customHeader, local, expectSuccess) {
+ const xhr = new XMLHttpRequest();
+ const url = (local ? get_host_info().HTTP_ORIGIN : get_host_info().HTTP_REMOTE_ORIGIN) +
+ "/xhr/resources/redirect-cors.py?location=" + destination + "&" + parameters;
+
+ xhr.open("GET", url, true);
+
+ if (customHeader)
+ xhr.setRequestHeader("x-test", "test");
+
+ xhr.onload = test.step_func_done(function() {
+ assert_true(expectSuccess);
+ assert_true(xhr.responseText.startsWith("PASS"));
+ });
+ xhr.onerror = test.step_func_done(function() {
+ assert_false(expectSuccess);
+ assert_equals(xhr.status, 0);
+ });
+ xhr.send();
+ }
+
+ const withCustomHeader = true;
+ const withoutCustomHeader = false;
+ const local = true;
+ const remote = false;
+ const succeeds = true;
+ const fails = false;
+
+ // Test simple cross origin requests that receive redirects.
+
+ // The redirect response fails the access check because the redirect lacks a CORS header.
+ async_test(t => {
+ runTest(t, get_host_info().HTTP_REMOTE_ORIGIN +
+ "/xhr/resources/access-control-basic-allow-star.py", "",
+ withoutCustomHeader, remote, fails)
+ }, "Request is redirected without CORS headers to a response with Access-Control-Allow-Origin=*");
+
+ // The redirect response passes the access check.
+ async_test(t => {
+ runTest(t, get_host_info().HTTP_REMOTE_ORIGIN +
+ "/xhr/resources/access-control-basic-allow-star.py", "allow_origin=true",
+ withoutCustomHeader, remote, succeeds)
+ }, "Request is redirected to a response with Access-Control-Allow-Origin=*");
+
+ // The redirect response fails the access check because user info was sent.
+ async_test(t => {
+ runTest(t, get_host_info().HTTP_REMOTE_ORIGIN.replace("http://", "http://username:password@") +
+ "/xhr/resources/access-control-basic-allow-star.py", "allow_origin=true",
+ withoutCustomHeader, remote, fails)
+ }, "Request with user info is redirected to a response with Access-Control-Allow-Origin=*");
+
+ // The redirect response fails the access check because the URL scheme is unsupported.
+ async_test(t => {
+ runTest(t, "foo://bar.cgi", "allow_origin=true", withoutCustomHeader, remote, fails)
+ }, "Request is redirect to a bad URL");
+
+ // The preflighted redirect response fails the access check because of preflighting.
+ async_test(t => {
+ runTest(t, get_host_info().HTTP_REMOTE_ORIGIN +
+ "/xhr/resources/access-control-basic-allow-star.py",
+ "allow_origin=true&redirect_preflight=true", withCustomHeader, remote, fails)
+ }, "Preflighted request is redirected to a response with Access-Control-Allow-Origin=*");
+
+ // The preflighted redirect response fails the access check after successful preflighting.
+ async_test(t => {
+ runTest(t, get_host_info().HTTP_REMOTE_ORIGIN +
+ "/xhr/resources/access-control-basic-allow-star.py",
+ "allow_origin=true&allow_header=x-test&redirect_preflight=true",
+ withCustomHeader, remote, fails)
+ }, "Preflighted request is redirected to a response with Access-Control-Allow-Origin=* and header allowed");
+
+ // The same-origin redirect response passes the access check.
+ async_test(t => {
+ runTest(t, get_host_info().HTTP_ORIGIN + "/xhr/resources/pass.txt",
+ "", withCustomHeader, local, succeeds)
+ }, "Request is redirected to a same-origin resource file");
diff --git a/test/wpt/tests/xhr/access-control-and-redirects.any.js b/test/wpt/tests/xhr/access-control-and-redirects.any.js
new file mode 100644
index 0000000..815d345
--- /dev/null
+++ b/test/wpt/tests/xhr/access-control-and-redirects.any.js
@@ -0,0 +1,50 @@
+// META: title=Tests that redirects between origins are allowed when access control is involved.
+// META: script=/common/get-host-info.sub.js
+
+ function runSync(test, url)
+ {
+ const xhr = new XMLHttpRequest();
+ xhr.open("GET", url, false);
+ xhr.send();
+ assert_equals(xhr.responseText, "PASS: Cross-domain access allowed.");
+ test.done();
+ }
+ function runAsync(test, url)
+ {
+ const xhr = new XMLHttpRequest();
+ xhr.open("GET", url, true);
+ xhr.onload = test.step_func_done(function() {
+ assert_equals(xhr.responseText, "PASS: Cross-domain access allowed.");
+ });
+ xhr.onerror = test.unreached_func("Network error");
+ xhr.send();
+ test.done();
+ }
+ test(t => {
+ runSync(t, "resources/redirect-cors.py?location=" + get_host_info().HTTP_REMOTE_ORIGIN +
+ "/xhr/resources/access-control-basic-allow.py")
+ }, "Local sync redirect to remote origin");
+ async_test(t => {
+ runAsync(t, "resources/redirect-cors.py?location=" + get_host_info().HTTP_REMOTE_ORIGIN +
+ "/xhr/resources/access-control-basic-allow.py")
+ }, "Local async redirect to remote origin");
+ test(t => {
+ runSync(t, get_host_info().HTTP_REMOTE_ORIGIN +
+ "/xhr/resources/redirect-cors.py?location=" + get_host_info().HTTP_ORIGIN +
+ "/xhr/resources/access-control-basic-allow.py&allow_origin=true")
+ }, "Remote sync redirect to local origin");
+ async_test(t => {
+ runAsync(t, get_host_info().HTTP_REMOTE_ORIGIN +
+ "/xhr/resources/redirect-cors.py?location=" + get_host_info().HTTP_ORIGIN +
+ "/xhr/resources/access-control-basic-allow.py&allow_origin=true")
+ }, "Remote async redirect to local origin");
+ test(t => {
+ runSync(t, get_host_info().HTTP_REMOTE_ORIGIN +
+ "/xhr/resources/redirect-cors.py?location=" + get_host_info().HTTP_REMOTE_ORIGIN +
+ "/xhr/resources/access-control-basic-allow.py&allow_origin=true")
+ }, "Remote sync redirect to same remote origin");
+ async_test(t => {
+ runAsync(t, get_host_info().HTTP_REMOTE_ORIGIN +
+ "/xhr/resources/redirect-cors.py?location=" + get_host_info().HTTP_REMOTE_ORIGIN +
+ "/xhr/resources/access-control-basic-allow.py&allow_origin=true")
+ }, "Remote async redirect to same remote origin");
diff --git a/test/wpt/tests/xhr/access-control-basic-allow-access-control-origin-header-data-url.htm b/test/wpt/tests/xhr/access-control-basic-allow-access-control-origin-header-data-url.htm
new file mode 100644
index 0000000..0d66ad7
--- /dev/null
+++ b/test/wpt/tests/xhr/access-control-basic-allow-access-control-origin-header-data-url.htm
@@ -0,0 +1,43 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Tests that cross-origin access is granted to null-origin embedded iframe</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/common/get-host-info.sub.js"></script>
+ </head>
+ <body>
+ <script type="text/javascript">
+const url = get_host_info().HTTP_REMOTE_ORIGIN + "/xhr/resources/access-control-origin-header.py";
+async_test(function(test) {
+ window.addEventListener("message", test.step_func(function(evt) {
+ if (evt.data == "ready") {
+ document.getElementById("frame").contentWindow.postMessage(url, "*");
+ } else {
+ assert_equals(evt.data, "PASS: Cross-domain access allowed.\nHTTP_ORIGIN: null");
+ test.done();
+ }
+ }), false);
+}, "Access granted to null-origin iframe");
+ </script>
+ <iframe id="frame" src='data:text/html,
+ <script>
+(function() {
+ parent.postMessage("ready", "*");
+ window.addEventListener("message", function(evt) {
+ try {
+ const url = evt.data;
+ const xhr = new XMLHttpRequest;
+
+ xhr.open("GET", url, false);
+ xhr.send();
+
+ parent.postMessage(xhr.responseText, "*");
+ } catch(e) {
+ parent.postMessage(e.message, "*");
+ }
+ });
+})();
+ </script>'>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/access-control-basic-allow-access-control-origin-header.any.js b/test/wpt/tests/xhr/access-control-basic-allow-access-control-origin-header.any.js
new file mode 100644
index 0000000..12b9cf2
--- /dev/null
+++ b/test/wpt/tests/xhr/access-control-basic-allow-access-control-origin-header.any.js
@@ -0,0 +1,13 @@
+// META: title=Access control test with origin header
+// META: script=/common/get-host-info.sub.js
+
+ async_test(function(test) {
+ const xhr = new XMLHttpRequest;
+
+ xhr.open("GET", get_host_info().HTTP_REMOTE_ORIGIN + "/xhr/resources/access-control-origin-header.py", false);
+ xhr.send();
+
+ assert_equals(xhr.responseText, "PASS: Cross-domain access allowed.\n" +
+ "HTTP_ORIGIN: " + get_host_info().HTTP_ORIGIN);
+ test.done();
+ }, "Access control test with origin header");
diff --git a/test/wpt/tests/xhr/access-control-basic-allow-async.any.js b/test/wpt/tests/xhr/access-control-basic-allow-async.any.js
new file mode 100644
index 0000000..3f1ff3c
--- /dev/null
+++ b/test/wpt/tests/xhr/access-control-basic-allow-async.any.js
@@ -0,0 +1,19 @@
+// META: title=Testing a basic asynchronous CORS XHR request.
+// META: script=/common/get-host-info.sub.js
+
+ async_test(function(test) {
+ const xhr = new XMLHttpRequest;
+
+ xhr.onreadystatechange = test.step_func(function() {
+ if (xhr.readyState == xhr.DONE) {
+ assert_equals(xhr.responseText, "PASS: Cross-domain access allowed.");
+ test.done();
+ }
+ });
+
+ xhr.onerror = test.unreached_func("FAIL: Network error.");
+
+ xhr.open("GET", get_host_info().HTTP_REMOTE_ORIGIN +
+ "/xhr/resources/access-control-basic-allow.py", true);
+ xhr.send();
+ }, "Basic async cross-origin XHR request");
diff --git a/test/wpt/tests/xhr/access-control-basic-allow-non-cors-safelisted-method-async.any.js b/test/wpt/tests/xhr/access-control-basic-allow-non-cors-safelisted-method-async.any.js
new file mode 100644
index 0000000..1e37f43
--- /dev/null
+++ b/test/wpt/tests/xhr/access-control-basic-allow-non-cors-safelisted-method-async.any.js
@@ -0,0 +1,17 @@
+// META: title=Tests cross-origin async request with non-CORS-safelisted method
+// META: script=/common/get-host-info.sub.js
+
+ async_test((test) => {
+ const xhr = new XMLHttpRequest;
+
+ xhr.onload = test.step_func_done(() => {
+ assert_equals(xhr.responseText, "PASS: Cross-domain access allowed.\nPASS: PUT data received");
+ });
+
+ xhr.onerror = test.unreached_func("Unexpected error.");
+
+ xhr.open("PUT", get_host_info().HTTP_REMOTE_ORIGIN +
+ "/xhr/resources/access-control-basic-put-allow.py");
+ xhr.setRequestHeader("Content-Type", "text/plain; charset=UTF-8");
+ xhr.send("PASS: PUT data received");
+ }, "Allow async PUT request");
diff --git a/test/wpt/tests/xhr/access-control-basic-allow-non-cors-safelisted-method.any.js b/test/wpt/tests/xhr/access-control-basic-allow-non-cors-safelisted-method.any.js
new file mode 100644
index 0000000..f238f0d
--- /dev/null
+++ b/test/wpt/tests/xhr/access-control-basic-allow-non-cors-safelisted-method.any.js
@@ -0,0 +1,14 @@
+// META: title=Tests cross-origin request with non-CORS-safelisted method
+// META: script=/common/get-host-info.sub.js
+
+ test(function() {
+ const xhr = new XMLHttpRequest;
+
+ xhr.open("PUT", get_host_info().HTTP_REMOTE_ORIGIN + "/xhr/resources/access-control-basic-put-allow.py", false);
+
+ xhr.setRequestHeader("Content-Type", "text/plain; charset=UTF-8");
+
+ xhr.send("PASS: PUT data received");
+
+ assert_equals(xhr.responseText, "PASS: Cross-domain access allowed.\nPASS: PUT data received");
+ }, "Allow PUT request");
diff --git a/test/wpt/tests/xhr/access-control-basic-allow-preflight-cache-invalidation-by-header.any.js b/test/wpt/tests/xhr/access-control-basic-allow-preflight-cache-invalidation-by-header.any.js
new file mode 100644
index 0000000..c967383
--- /dev/null
+++ b/test/wpt/tests/xhr/access-control-basic-allow-preflight-cache-invalidation-by-header.any.js
@@ -0,0 +1,38 @@
+// META: title=Preflight cache should be invalidated in presence of custom header
+// META: script=/common/get-host-info.sub.js
+// META: script=/common/utils.js
+
+ const uuid = token();
+ const xhr = new XMLHttpRequest;
+
+ async_test(function(test) {
+ xhr.onerror = test.unreached_func("FAIL: Network error.");
+ xhr.onload = test.step_func(function() {
+ // Token reset. We can start the test now.
+ assert_equals(xhr.responseText, "PASS");
+ firstRequest();
+ });
+
+ xhr.open("GET", get_host_info().HTTP_REMOTE_ORIGIN + "/xhr/resources/reset-token.py?token=" + uuid, true);
+ xhr.send();
+
+ function firstRequest() {
+ xhr.onload = test.step_func(function() {
+ assert_equals(xhr.responseText, "PASS: First PUT request.");
+ secondRequest();
+ });
+ xhr.open("PUT", get_host_info().HTTP_REMOTE_ORIGIN + "/xhr/resources/access-control-basic-preflight-cache-invalidation.py?token=" + uuid, true);
+ xhr.send();
+ }
+
+ function secondRequest() {
+ xhr.onload = test.step_func(function() {
+ assert_equals(xhr.responseText, "PASS: Second OPTIONS request was sent.");
+ test.done();
+ });
+ // Send a header not included in the inital cache.
+ xhr.open("PUT", get_host_info().HTTP_REMOTE_ORIGIN + "/xhr/resources/access-control-basic-preflight-cache-invalidation.py?token=" + uuid, true);
+ xhr.setRequestHeader("x-test", "headerValue");
+ xhr.send();
+ }
+ }, "Preflight cache should be invalidated in presence of custom header");
diff --git a/test/wpt/tests/xhr/access-control-basic-allow-preflight-cache-invalidation-by-method.any.js b/test/wpt/tests/xhr/access-control-basic-allow-preflight-cache-invalidation-by-method.any.js
new file mode 100644
index 0000000..bb8a72c
--- /dev/null
+++ b/test/wpt/tests/xhr/access-control-basic-allow-preflight-cache-invalidation-by-method.any.js
@@ -0,0 +1,37 @@
+// META: title=Preflight cache should be invalidated by changed method
+// META: script=/common/get-host-info.sub.js
+// META: script=/common/utils.js
+
+ const uuid = token();
+ const xhr = new XMLHttpRequest;
+
+ async_test(function(test) {
+ xhr.onerror = test.unreached_func("FAIL: Network error.");
+ xhr.onload = test.step_func(function() {
+ // Token reset. We can start the test now.
+ assert_equals(xhr.responseText, "PASS");
+ firstRequest();
+ });
+
+ xhr.open("GET", get_host_info().HTTP_REMOTE_ORIGIN + "/xhr/resources/reset-token.py?token=" + uuid, true);
+ xhr.send();
+
+ function firstRequest() {
+ xhr.onload = test.step_func(function() {
+ assert_equals(xhr.responseText, "PASS: First PUT request.");
+ secondRequest();
+ });
+ xhr.open("PUT", get_host_info().HTTP_REMOTE_ORIGIN + "/xhr/resources/access-control-basic-preflight-cache-invalidation.py?token=" + uuid, true);
+ xhr.send();
+ }
+
+ function secondRequest() {
+ xhr.onload = test.step_func(function() {
+ assert_equals(xhr.responseText, "PASS: Second OPTIONS request was sent.");
+ test.done();
+ });
+ // Send a header not included in the inital cache.
+ xhr.open("XMETHOD", get_host_info().HTTP_REMOTE_ORIGIN + "/xhr/resources/access-control-basic-preflight-cache-invalidation.py?token=" + uuid, true);
+ xhr.send();
+ }
+ }, "Preflight cache should be invalidated by changed method");
diff --git a/test/wpt/tests/xhr/access-control-basic-allow-preflight-cache-timeout.any.js b/test/wpt/tests/xhr/access-control-basic-allow-preflight-cache-timeout.any.js
new file mode 100644
index 0000000..00de0b3
--- /dev/null
+++ b/test/wpt/tests/xhr/access-control-basic-allow-preflight-cache-timeout.any.js
@@ -0,0 +1,37 @@
+// META: title=Preflight cache should be invalidated on timeout
+// META: timeout=long
+// META: script=/common/get-host-info.sub.js
+// META: script=/common/utils.js
+
+ const uuid = token();
+ let xhr = new XMLHttpRequest;
+
+ async_test(function(test) {
+ xhr.onerror = test.unreached_func("FAIL: Network error.");
+ xhr.onload = test.step_func(function() {
+ // Token reset. We can start the test now.
+ assert_equals(xhr.responseText, "PASS");
+ firstRequest();
+ });
+
+ xhr.open("GET", get_host_info().HTTP_REMOTE_ORIGIN + "/xhr/resources/reset-token.py?token=" + uuid, true);
+ xhr.send();
+
+ function firstRequest() {
+ xhr.onload = test.step_func(function() {
+ assert_equals(xhr.responseText, "PASS: First PUT request.");
+ step_timeout(secondRequest, 3000); // 3 seconds
+ });
+ xhr.open("PUT", get_host_info().HTTP_REMOTE_ORIGIN + "/xhr/resources/access-control-basic-preflight-cache-timeout.py?token=" + uuid, true);
+ xhr.send();
+ }
+
+ function secondRequest() {
+ xhr.onload = test.step_func(function() {
+ assert_equals(xhr.responseText, "PASS: Second OPTIONS request was sent.");
+ test.done();
+ });
+ xhr.open("PUT", get_host_info().HTTP_REMOTE_ORIGIN + "/xhr/resources/access-control-basic-preflight-cache-timeout.py?token=" + uuid, true);
+ xhr.send();
+ }
+ }, "Preflight cache should be invalidated on timeout");
diff --git a/test/wpt/tests/xhr/access-control-basic-allow-preflight-cache.any.js b/test/wpt/tests/xhr/access-control-basic-allow-preflight-cache.any.js
new file mode 100644
index 0000000..0bdf955
--- /dev/null
+++ b/test/wpt/tests/xhr/access-control-basic-allow-preflight-cache.any.js
@@ -0,0 +1,35 @@
+// META: title=Preflight cache should allow second request without preflight OPTIONS request
+// META: script=/common/get-host-info.sub.js
+// META: script=/common/utils.js
+
+ const uuid = token();
+
+ async_test(function(test) {
+ const xhr = new XMLHttpRequest;
+ xhr.onerror = test.unreached_func("FAIL: Network error.");
+ xhr.onload = test.step_func(function() {
+ // Token reset. We can start the test now.
+ assert_equals(xhr.responseText, "PASS");
+ firstRequest();
+ });
+
+ xhr.open("GET", get_host_info().HTTP_REMOTE_ORIGIN + "/xhr/resources/reset-token.py?token=" + uuid, true);
+ xhr.send();
+
+ function firstRequest() {
+ xhr.onload = test.step_func(function() {
+ assert_equals(xhr.responseText, "PASS: First PUT request.");
+ secondRequest();
+ });
+ xhr.open("PUT", get_host_info().HTTP_REMOTE_ORIGIN + "/xhr/resources/access-control-basic-preflight-cache.py?token=" + uuid, true);
+ xhr.send();
+ }
+
+ function secondRequest() {
+ xhr.onload = test.step_func_done(function() {
+ assert_equals(xhr.responseText, "PASS: Second PUT request. Preflight worked.");
+ });
+ xhr.open("PUT", get_host_info().HTTP_REMOTE_ORIGIN + "/xhr/resources/access-control-basic-preflight-cache.py?token=" + uuid, true);
+ xhr.send();
+ }
+ }, "Preflight cache should allow second request");
diff --git a/test/wpt/tests/xhr/access-control-basic-allow-star.any.js b/test/wpt/tests/xhr/access-control-basic-allow-star.any.js
new file mode 100644
index 0000000..c7ab3fe
--- /dev/null
+++ b/test/wpt/tests/xhr/access-control-basic-allow-star.any.js
@@ -0,0 +1,12 @@
+// META: title=Tests "*" setting for Access-Control-Allow-Origin header
+// META: script=/common/get-host-info.sub.js
+
+ const xhr = new XMLHttpRequest;
+
+ test(function(test) {
+ xhr.open("GET", get_host_info().HTTP_REMOTE_ORIGIN + "/xhr/resources/access-control-basic-allow-star.py", false);
+
+ xhr.send();
+
+ assert_equals(xhr.responseText, "PASS: Cross-domain access allowed.");
+ }, "Allow star");
diff --git a/test/wpt/tests/xhr/access-control-basic-allow.any.js b/test/wpt/tests/xhr/access-control-basic-allow.any.js
new file mode 100644
index 0000000..c895383
--- /dev/null
+++ b/test/wpt/tests/xhr/access-control-basic-allow.any.js
@@ -0,0 +1,12 @@
+// META: title=Tests CORS with Access-Control-Allow-Origin header
+// META: script=/common/get-host-info.sub.js
+
+ test(function() {
+ const xhr = new XMLHttpRequest;
+
+ xhr.open("GET", get_host_info().HTTP_REMOTE_ORIGIN + "/xhr/resources/access-control-basic-allow.py", false);
+
+ xhr.send();
+
+ assert_equals(xhr.responseText, "PASS: Cross-domain access allowed.");
+ }, "Allow basic");
diff --git a/test/wpt/tests/xhr/access-control-basic-cors-safelisted-request-headers.htm b/test/wpt/tests/xhr/access-control-basic-cors-safelisted-request-headers.htm
new file mode 100644
index 0000000..5687049
--- /dev/null
+++ b/test/wpt/tests/xhr/access-control-basic-cors-safelisted-request-headers.htm
@@ -0,0 +1,31 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Tests that CORS-safelisted request headers are permitted in cross-origin request</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/common/get-host-info.sub.js"></script>
+ </head>
+ <body>
+ <script type="text/javascript">
+ test(function() {
+ const xhr = new XMLHttpRequest;
+
+ xhr.open("POST", get_host_info().HTTP_REMOTE_ORIGIN + "/xhr/resources/access-control-basic-cors-safelisted-request-headers.py", false);
+
+ xhr.setRequestHeader("Accept", "*");
+ xhr.setRequestHeader("Accept-Language", "ru");
+ xhr.setRequestHeader("Content-Language", "ru");
+ xhr.setRequestHeader("Content-Type", "text/plain");
+
+ xhr.send();
+
+ assert_equals(xhr.responseText,
+ "Accept: *\n" +
+ "Accept-Language: ru\n" +
+ "Content-Language: ru\n" +
+ "Content-Type: text/plain\n");
+ }, "Request with CORS-safelisted headers");
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/access-control-basic-cors-safelisted-response-headers.htm b/test/wpt/tests/xhr/access-control-basic-cors-safelisted-response-headers.htm
new file mode 100644
index 0000000..fb58957
--- /dev/null
+++ b/test/wpt/tests/xhr/access-control-basic-cors-safelisted-response-headers.htm
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Tests that CORS-safelisted response headers are permitted in cross-origin request</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/common/get-host-info.sub.js"></script>
+ </head>
+ <body>
+ <script type="text/javascript">
+ test(function() {
+ const xhr = new XMLHttpRequest;
+
+ xhr.open("GET", get_host_info().HTTP_REMOTE_ORIGIN +
+ "/xhr/resources/access-control-basic-cors-safelisted-response-headers.py", false);
+ xhr.send();
+
+ assert_not_equals(xhr.getResponseHeader("cache-control"), null);
+ assert_not_equals(xhr.getResponseHeader("content-language"), null);
+ assert_not_equals(xhr.getResponseHeader("content-type"), null);
+ assert_not_equals(xhr.getResponseHeader("content-length"), null);
+ assert_not_equals(xhr.getResponseHeader("expires"), null);
+ assert_not_equals(xhr.getResponseHeader("last-modified"), null);
+ assert_not_equals(xhr.getResponseHeader("pragma"), null);
+ assert_equals(xhr.getResponseHeader("x-webkit"), null);
+
+ assert_not_equals(xhr.getAllResponseHeaders().match("en"), null);
+ assert_equals(xhr.getAllResponseHeaders().match("foobar"), null);
+ }, "Response with CORS-safelisted headers");
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/access-control-basic-denied.htm b/test/wpt/tests/xhr/access-control-basic-denied.htm
new file mode 100644
index 0000000..970e09d
--- /dev/null
+++ b/test/wpt/tests/xhr/access-control-basic-denied.htm
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Tests CORS denying resource without Access-Control-Allow-Origin header</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/common/get-host-info.sub.js"></script>
+ </head>
+ <body>
+ <script type="text/javascript">
+ const path = "/xhr/resources/access-control-basic-denied.py";
+
+ test(function() {
+ const xhr = new XMLHttpRequest;
+
+ xhr.open("GET", get_host_info().HTTP_ORIGIN + path, false);
+ xhr.send();
+ assert_equals(xhr.status, 200);
+ }, "Same-origin request accepted");
+
+ test(function() {
+ const xhr = new XMLHttpRequest;
+
+ xhr.open("GET", get_host_info().HTTP_REMOTE_ORIGIN + path, false);
+ assert_throws_dom("NetworkError", () => xhr.send());
+ assert_equals(xhr.status, 0);
+ }, "Cross-origin request denied");
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/access-control-basic-get-fail-non-simple.htm b/test/wpt/tests/xhr/access-control-basic-get-fail-non-simple.htm
new file mode 100644
index 0000000..97370bd
--- /dev/null
+++ b/test/wpt/tests/xhr/access-control-basic-get-fail-non-simple.htm
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Tests CORS denying preflighted request to resource without CORS headers for OPTIONS</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/common/get-host-info.sub.js"></script>
+ </head>
+ <body>
+ <script type="text/javascript">
+ test(function() {
+ const xhr = new XMLHttpRequest;
+
+ xhr.open("GET", get_host_info().HTTP_REMOTE_ORIGIN +
+ "/xhr/resources/access-control-basic-options-not-supported.py", false);
+
+ // Non-CORS-safelisted header
+ xhr.setRequestHeader("x-test", "foobar");
+
+ // This fails because the server-side script is not prepared for an OPTIONS request
+ assert_throws_dom("NetworkError", () => xhr.send());
+ assert_equals(xhr.status, 0);
+ }, "Preflighted cross-origin request denied");
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/access-control-basic-non-cors-safelisted-content-type.htm b/test/wpt/tests/xhr/access-control-basic-non-cors-safelisted-content-type.htm
new file mode 100644
index 0000000..0e0e971
--- /dev/null
+++ b/test/wpt/tests/xhr/access-control-basic-non-cors-safelisted-content-type.htm
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Tests cross-origin request with non-CORS-safelisted content type</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/common/get-host-info.sub.js"></script>
+ </head>
+ <body>
+ <script type="text/javascript">
+ test(() => {
+ const xhr = new XMLHttpRequest;
+
+ xhr.open("PUT", get_host_info().HTTP_REMOTE_ORIGIN +
+ "/xhr/resources/access-control-basic-put-allow.py", false);
+ xhr.setRequestHeader("Content-Type", "text/plain");
+ xhr.send("PASS: PUT data received");
+
+ assert_equals(xhr.responseText, "PASS: Cross-domain access allowed.\nPASS: PUT data received");
+
+ xhr.open("PUT", get_host_info().HTTP_REMOTE_ORIGIN +
+ "/xhr/resources/access-control-basic-put-allow.py", false);
+ xhr.setRequestHeader("Content-Type", "application/xml");
+
+ assert_throws_dom("NetworkError", () => xhr.send("FAIL: PUT data received"));
+ assert_equals(xhr.status, 0, "Cross-domain access was denied in 'send'.");
+ }, "Deny cross-origin request with non-CORS-safelisted content type");
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/access-control-basic-post-success-no-content-type.htm b/test/wpt/tests/xhr/access-control-basic-post-success-no-content-type.htm
new file mode 100644
index 0000000..7e7a7d3
--- /dev/null
+++ b/test/wpt/tests/xhr/access-control-basic-post-success-no-content-type.htm
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Tests that POST requests with text content and no content-type set explicitly don't generate a preflight request.</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/common/get-host-info.sub.js"></script>
+ </head>
+ <body>
+ <script type="text/javascript">
+ async_test(function(test) {
+ const xhr = new XMLHttpRequest;
+
+ xhr.open("POST", get_host_info().HTTP_REMOTE_ORIGIN + "/xhr/resources/access-control-basic-options-not-supported.py");
+
+ xhr.onerror = test.unreached_func("Network error.");
+
+ xhr.onload = test.step_func_done(function() {
+ assert_equals(xhr.status, 200);
+ });
+
+ xhr.send("Test");
+ }, "POST request with text content and no Content-Type header");
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/access-control-basic-post-with-non-cors-safelisted-content-type.htm b/test/wpt/tests/xhr/access-control-basic-post-with-non-cors-safelisted-content-type.htm
new file mode 100644
index 0000000..f63e6bc
--- /dev/null
+++ b/test/wpt/tests/xhr/access-control-basic-post-with-non-cors-safelisted-content-type.htm
@@ -0,0 +1,37 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Non-CORS-safelisted value in the Content-Type header results in a request preflight</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/common/get-host-info.sub.js"></script>
+ </head>
+ <body>
+ <script type="text/javascript">
+ test(function() {
+ const xhr = new XMLHttpRequest;
+
+ xhr.open("POST", get_host_info().HTTP_ORIGIN +
+ "/xhr/resources/access-control-basic-options-not-supported.py", false);
+
+ xhr.setRequestHeader("Content-Type", "application/xml");
+
+ xhr.send();
+
+ assert_equals(xhr.status, 200, "Same-origin access doesn't issue preflight; not denied.");
+ }, "Same-origin request with non-safelisted content type succeeds");
+
+ test(function() {
+ const xhr = new XMLHttpRequest;
+
+ xhr.open("POST", get_host_info().HTTP_REMOTE_ORIGIN +
+ "/xhr/resources/access-control-basic-options-not-supported.py", false);
+
+ xhr.setRequestHeader("Content-Type", "application/xml");
+
+ assert_throws_dom("NetworkError", () => xhr.send());
+ assert_equals(xhr.status, 0, "Cross-domain access was denied in 'send'.");
+ }, "CORS request with non-safelisted content type sends preflight and fails");
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/access-control-basic-preflight-denied.htm b/test/wpt/tests/xhr/access-control-basic-preflight-denied.htm
new file mode 100644
index 0000000..6475186
--- /dev/null
+++ b/test/wpt/tests/xhr/access-control-basic-preflight-denied.htm
@@ -0,0 +1,31 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Tests async XHR preflight denial due to lack of CORS headers</title>
+ <!--The original test addressed a more specific issue involving caching,
+ but that issue has since been resolved.
+ We maintain this test as a basic test of invalid preflight denial.
+ Please refer to the comment in the following link for more information:
+ https://chromium-review.googlesource.com/c/chromium/src/+/630338#message-0280542b95c9b0f82b121dc373320c04fcaece31
+ -->
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/common/get-host-info.sub.js"></script>
+ </head>
+ <body>
+ <script type="text/javascript">
+ async_test((test) => {
+ const xhr = new XMLHttpRequest;
+ xhr.onerror = test.step_func_done(() => {
+ assert_equals(xhr.status, 0);
+ });
+
+ xhr.onload = test.unreached_func("Request succeeded unexpectedly");
+
+ xhr.open("FOO", get_host_info().HTTP_REMOTE_ORIGIN +
+ "/xhr/resources/access-control-basic-denied.py");
+ xhr.send();
+ });
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/access-control-expose-headers-on-redirect.html b/test/wpt/tests/xhr/access-control-expose-headers-on-redirect.html
new file mode 100644
index 0000000..f06ec0c
--- /dev/null
+++ b/test/wpt/tests/xhr/access-control-expose-headers-on-redirect.html
@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>XHR should respect access-control-expose-headers header on redirect</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+</head>
+<body>
+<script type="text/javascript">
+async_test((test) => {
+ const xhr = new XMLHttpRequest;
+
+ xhr.onerror = test.unreached_func("Unexpected error.");
+
+ xhr.onload = test.step_func_done(() => {
+ assert_equals(xhr.status, 200);
+ assert_equals(xhr.getResponseHeader('foo'), 'bar');
+ assert_equals(xhr.getResponseHeader('hoge'), null);
+ });
+
+ const destination = get_host_info().HTTP_REMOTE_ORIGIN +
+ '/common/blank.html?pipe=header(access-control-allow-origin,*)|' +
+ 'header(access-control-expose-headers,foo)|' +
+ 'header(foo,bar)|header(hoge,fuga)';
+ const url =
+ 'resources/redirect.py?location=' + encodeURIComponent(destination);
+ xhr.open('GET', url);
+ xhr.send();
+});
+</script>
+</body>
+</html>
diff --git a/test/wpt/tests/xhr/access-control-preflight-async-header-denied.htm b/test/wpt/tests/xhr/access-control-preflight-async-header-denied.htm
new file mode 100644
index 0000000..a00cc58
--- /dev/null
+++ b/test/wpt/tests/xhr/access-control-preflight-async-header-denied.htm
@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Async request denied at preflight because of non-CORS-safelisted header</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/common/get-host-info.sub.js"></script>
+ <script src="/common/utils.js"></script>
+ </head>
+ <body>
+ <script type="text/javascript">
+ const uuid = token();
+ const url = get_host_info().HTTP_REMOTE_ORIGIN +
+ "/xhr/resources/access-control-preflight-denied.py?token=" + uuid;
+
+ async_test((test) => {
+ let xhr = new XMLHttpRequest;
+ xhr.open("GET", url + "&command=reset", false);
+ xhr.send();
+
+ xhr = new XMLHttpRequest;
+ xhr.open("GET", url + "&command=header", true);
+ xhr.setRequestHeader("x-test", "foo");
+
+ xhr.onload = test.unreached_func(
+ "Cross-domain access with custom header allowed without throwing exception");
+
+ xhr.onerror = test.step_func_done(() => {
+ xhr = new XMLHttpRequest;
+ xhr.open("GET", url + "&command=complete", false);
+ xhr.send();
+ assert_equals(xhr.responseText, "Request successfully blocked.");
+ });
+
+ xhr.send();
+ }, "Async request denied at preflight");
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/access-control-preflight-async-method-denied.htm b/test/wpt/tests/xhr/access-control-preflight-async-method-denied.htm
new file mode 100644
index 0000000..a0425a4
--- /dev/null
+++ b/test/wpt/tests/xhr/access-control-preflight-async-method-denied.htm
@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Async request denied at preflight because of non-CORS-safelisted method</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/common/get-host-info.sub.js"></script>
+ <script src="/common/utils.js"></script>
+ </head>
+ <body>
+ <script type="text/javascript">
+ const uuid = token();
+ const url = get_host_info().HTTP_REMOTE_ORIGIN +
+ "/xhr/resources/access-control-preflight-denied.py?token=" + uuid;
+
+ async_test((test) => {
+ let xhr = new XMLHttpRequest;
+ xhr.open("GET", url + "&command=reset", false);
+ xhr.send();
+
+ xhr = new XMLHttpRequest;
+ xhr.open("DELETE", url + "&command=method", true);
+
+ xhr.onload = test.unreached_func(
+ "Cross-domain access with non-CORS-safelisted method allowed without throwing exception");
+
+ xhr.onerror = test.step_func_done(() => {
+ xhr = new XMLHttpRequest;
+ xhr.open("GET", url + "&command=complete", false);
+ xhr.send();
+ assert_equals(xhr.responseText, "Request successfully blocked.");
+ });
+
+ xhr.send();
+ }, "Async request denied at preflight");
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/access-control-preflight-async-not-supported.htm b/test/wpt/tests/xhr/access-control-preflight-async-not-supported.htm
new file mode 100644
index 0000000..a4dc06d
--- /dev/null
+++ b/test/wpt/tests/xhr/access-control-preflight-async-not-supported.htm
@@ -0,0 +1,37 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Async PUT request denied at preflight</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/common/get-host-info.sub.js"></script>
+ <script src="/common/utils.js"></script>
+ </head>
+ <body>
+ <script type="text/javascript">
+const uuid = token();
+const url = get_host_info().HTTP_REMOTE_ORIGIN +
+ "/xhr/resources/access-control-preflight-denied.py?token=" + uuid;
+
+async_test((test) => {
+ let xhr = new XMLHttpRequest;
+ xhr.open("GET", url + "&command=reset", false);
+ xhr.send();
+
+ xhr = new XMLHttpRequest;
+ xhr.open("PUT", url, true);
+
+ xhr.onload = test.unreached_func("Cross-domain access allowed unexpectedly.");
+
+ xhr.onerror = test.step_func_done(() => {
+ xhr = new XMLHttpRequest;
+ xhr.open("GET", url + "&command=complete", false);
+ xhr.send();
+ assert_equals(xhr.responseText, "Request successfully blocked.");
+ });
+
+ xhr.send();
+});
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/access-control-preflight-credential-async.htm b/test/wpt/tests/xhr/access-control-preflight-credential-async.htm
new file mode 100644
index 0000000..ad7117f
--- /dev/null
+++ b/test/wpt/tests/xhr/access-control-preflight-credential-async.htm
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Tests proper handling of cross-origin async request with credentials</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/common/get-host-info.sub.js"></script>
+ </head>
+ <body>
+ <script type="text/javascript">
+ async_test((test) => {
+ const xhr = new XMLHttpRequest;
+
+ xhr.open("PUT", get_host_info().HTTP_REMOTE_ORIGIN +
+ "/xhr/resources/access-control-auth-basic.py?uid=fooUser",
+ true, "fooUser", "barPass");
+ xhr.withCredentials = true;
+
+ xhr.onerror = test.unreached_func("Unexpected error.");
+
+ xhr.onload = test.step_func_done(() => {
+ assert_equals(xhr.status, 401, "Request raises HTTP 401: Unauthorized error.");
+ });
+
+ xhr.send();
+ }, "CORS async request with URL credentials");
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/access-control-preflight-credential-sync.htm b/test/wpt/tests/xhr/access-control-preflight-credential-sync.htm
new file mode 100644
index 0000000..3844d02
--- /dev/null
+++ b/test/wpt/tests/xhr/access-control-preflight-credential-sync.htm
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Tests proper handling of cross-origin sync request with credentials</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/common/get-host-info.sub.js"></script>
+ </head>
+ <body>
+ <script type="text/javascript">
+ test(() => {
+ const xhr = new XMLHttpRequest;
+
+ xhr.open("PUT", get_host_info().HTTP_REMOTE_ORIGIN + "/xhr/resources/access-control-auth-basic.py?uid=fooUser", false, "fooUser", "barPass");
+
+ xhr.withCredentials = true;
+
+ xhr.send();
+
+ assert_equals(xhr.status, 401, "Request raises HTTP 401: Unauthorized error.");
+ }, "CORS sync request with URL credentials");
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/access-control-preflight-headers-async.htm b/test/wpt/tests/xhr/access-control-preflight-headers-async.htm
new file mode 100644
index 0000000..ffea72a
--- /dev/null
+++ b/test/wpt/tests/xhr/access-control-preflight-headers-async.htm
@@ -0,0 +1,35 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Test that async CORS requests with custom headers are sent with OPTIONS preflight</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/common/get-host-info.sub.js"></script>
+ <script src="/common/utils.js"></script>
+ </head>
+ <body>
+ <script type="text/javascript">
+async_test((test) => {
+ let xhr = new XMLHttpRequest;
+ const uuid = token();
+
+ xhr.open("GET", get_host_info().HTTP_REMOTE_ORIGIN +
+ "/xhr/resources/reset-token.py?token=" + uuid, false);
+ xhr.send();
+
+ xhr = new XMLHttpRequest;
+ xhr.open("GET", get_host_info().HTTP_REMOTE_ORIGIN +
+ "/xhr/resources/no-custom-header-on-preflight.py?token=" + uuid);
+ xhr.setRequestHeader("x-test", "foobar");
+
+ xhr.onerror = test.unreached_func("Unexpected error");
+
+ xhr.onload = test.step_func_done(() => {
+ assert_equals(xhr.responseText, "PASS");
+ });
+
+ xhr.send();
+}, "Preflighted async request with custom header");
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/access-control-preflight-headers-sync.htm b/test/wpt/tests/xhr/access-control-preflight-headers-sync.htm
new file mode 100644
index 0000000..2ae9fe8
--- /dev/null
+++ b/test/wpt/tests/xhr/access-control-preflight-headers-sync.htm
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Test that sync CORS requests with custom headers are not sent with OPTIONS preflight</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/common/get-host-info.sub.js"></script>
+ <script src="/common/utils.js"></script>
+ </head>
+ <body>
+ <script type="text/javascript">
+ test(function() {
+ let xhr = new XMLHttpRequest;
+ const uuid = token();
+
+ xhr.open("GET", get_host_info().HTTP_REMOTE_ORIGIN +
+ "/xhr/resources/reset-token.py?token=" + uuid, false);
+ xhr.send();
+
+ xhr = new XMLHttpRequest;
+ xhr.open("GET", get_host_info().HTTP_REMOTE_ORIGIN +
+ "/xhr/resources/no-custom-header-on-preflight.py?token=" + uuid, false);
+ xhr.setRequestHeader("x-test", "foobar");
+ xhr.send();
+ assert_equals(xhr.responseText, "PASS");
+ }, "Preflighted sync request with custom header");
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/access-control-preflight-request-allow-headers-returns-star.any.js b/test/wpt/tests/xhr/access-control-preflight-request-allow-headers-returns-star.any.js
new file mode 100644
index 0000000..567dd25
--- /dev/null
+++ b/test/wpt/tests/xhr/access-control-preflight-request-allow-headers-returns-star.any.js
@@ -0,0 +1,26 @@
+// META: title=Access-Control-Allow-Headers supports *
+// META: script=/common/get-host-info.sub.js
+"use strict";
+
+async_test(t => {
+ const xhr = new XMLHttpRequest();
+
+ xhr.open("GET", corsURL("resources/access-control-preflight-request-allow-headers-returns-star.py"));
+
+ xhr.setRequestHeader("X-Test", "foobar");
+
+ xhr.onerror = t.unreached_func("Error occurred.");
+
+ xhr.onload = t.step_func_done(() => {
+ assert_equals(xhr.status, 200);
+ assert_equals(xhr.responseText, "PASS");
+ });
+
+ xhr.send();
+});
+
+function corsURL(path) {
+ const url = new URL(path, location.href);
+ url.hostname = get_host_info().REMOTE_HOST;
+ return url.href;
+}
diff --git a/test/wpt/tests/xhr/access-control-preflight-request-header-lowercase.htm b/test/wpt/tests/xhr/access-control-preflight-request-header-lowercase.htm
new file mode 100644
index 0000000..7dc4608
--- /dev/null
+++ b/test/wpt/tests/xhr/access-control-preflight-request-header-lowercase.htm
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Access-Control-Request-Headers values should be lowercase</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/common/get-host-info.sub.js"></script>
+ </head>
+ <body>
+ <script type="text/javascript">
+ async_test(function(test) {
+ const xhr = new XMLHttpRequest;
+
+ xhr.open("GET", get_host_info().HTTP_REMOTE_ORIGIN + "/xhr/resources/access-control-preflight-request-header-lowercase.py");
+
+ xhr.setRequestHeader("X-Test", "foobar");
+
+ xhr.onerror = test.unreached_func("Error occurred.");
+
+ xhr.onload = test.step_func_done(function() {
+ assert_equals(xhr.status, 200);
+ assert_equals(xhr.responseText, "PASS");
+ });
+
+ xhr.send();
+ }, "Request with uppercase header set");
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/access-control-preflight-request-header-returns-origin.any.js b/test/wpt/tests/xhr/access-control-preflight-request-header-returns-origin.any.js
new file mode 100644
index 0000000..2b68a6f
--- /dev/null
+++ b/test/wpt/tests/xhr/access-control-preflight-request-header-returns-origin.any.js
@@ -0,0 +1,26 @@
+// META: title=Access-Control-Request-Origin accept different origin between preflight and actual request
+// META: script=/common/get-host-info.sub.js
+"use strict";
+
+async_test(t => {
+ const xhr = new XMLHttpRequest();
+
+ xhr.open("GET", corsURL("resources/access-control-preflight-request-header-returns-origin.py"));
+
+ xhr.setRequestHeader("X-Test", "foobar");
+
+ xhr.onerror = t.unreached_func("Error occurred.");
+
+ xhr.onload = t.step_func_done(() => {
+ assert_equals(xhr.status, 200);
+ assert_equals(xhr.responseText, "PASS");
+ });
+
+ xhr.send();
+});
+
+function corsURL(path) {
+ const url = new URL(path, location.href);
+ url.hostname = get_host_info().REMOTE_HOST;
+ return url.href;
+}
diff --git a/test/wpt/tests/xhr/access-control-preflight-request-header-sorted.htm b/test/wpt/tests/xhr/access-control-preflight-request-header-sorted.htm
new file mode 100644
index 0000000..830e0fb
--- /dev/null
+++ b/test/wpt/tests/xhr/access-control-preflight-request-header-sorted.htm
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Tests that Access-Control-Request-Headers are sorted.</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+</head>
+<body>
+<script>
+async_test((test) => {
+ const xhr = new XMLHttpRequest();
+ const url = get_host_info().HTTP_REMOTE_ORIGIN + '/xhr/resources/access-control-preflight-request-header-sorted.py';
+ xhr.open('GET', url);
+ xhr.setRequestHeader("X-Custom-Test", "foobar");
+ xhr.setRequestHeader("X-Custom-ua", "foobar");
+ xhr.setRequestHeader("X-Custom-V", "foobar");
+ xhr.setRequestHeader("X-Custom-s", "foobar");
+ xhr.setRequestHeader("X-Custom-U", "foobar");
+ xhr.onerror = test.unreached_func('xhr failure');
+ xhr.onload = test.step_func_done(() => {
+ assert_equals(xhr.responseText, 'PASS');
+ });
+ xhr.send();
+});
+</script>
+</body>
+</html>
diff --git a/test/wpt/tests/xhr/access-control-preflight-request-headers-origin.htm b/test/wpt/tests/xhr/access-control-preflight-request-headers-origin.htm
new file mode 100644
index 0000000..fc11abc
--- /dev/null
+++ b/test/wpt/tests/xhr/access-control-preflight-request-headers-origin.htm
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Test that 'Origin' is not included in Access-Control-Request-Headers in a preflight request</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/common/get-host-info.sub.js"></script>
+ </head>
+ <body>
+ <script type="text/javascript">
+async_test((test) => {
+ const xhr = new XMLHttpRequest;
+ const url = get_host_info().HTTP_REMOTE_ORIGIN +
+ "/xhr/resources/access-control-preflight-request-headers-origin.py";
+
+ xhr.open("GET", url);
+ xhr.setRequestHeader("x-pass", "PASS");
+
+ xhr.onerror = test.unreached_func("Unexpected error");
+
+ xhr.onload = test.step_func_done(() => {
+ assert_equals(xhr.responseText, "PASS");
+ });
+
+ xhr.send();
+}, "'Origin' should not be included in CORS Request-Headers");
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/access-control-preflight-request-invalid-status-301.htm b/test/wpt/tests/xhr/access-control-preflight-request-invalid-status-301.htm
new file mode 100644
index 0000000..62fc480
--- /dev/null
+++ b/test/wpt/tests/xhr/access-control-preflight-request-invalid-status-301.htm
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Tests that preflight requests returning invalid 301 status code result in error.</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/common/get-host-info.sub.js"></script>
+ </head>
+ <body>
+ <script type="text/javascript">
+ async_test((test) => {
+ const xhr = new XMLHttpRequest;
+
+ xhr.open("GET", get_host_info().HTTP_REMOTE_ORIGIN + "/xhr/resources/access-control-preflight-request-invalid-status.py?code=301");
+
+ xhr.setRequestHeader("x-pass", "pass");
+
+ xhr.onerror = test.step_func_done(function() {
+ assert_equals(xhr.status, 0);
+ });
+
+ xhr.onload = test.unreached_func("Invalid 301 response to preflight should result in error.");
+
+ xhr.send();
+ }, "Request with 301 preflight response");
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/access-control-preflight-request-invalid-status-400.htm b/test/wpt/tests/xhr/access-control-preflight-request-invalid-status-400.htm
new file mode 100644
index 0000000..9e76d9e
--- /dev/null
+++ b/test/wpt/tests/xhr/access-control-preflight-request-invalid-status-400.htm
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Tests that preflight requests returning invalid 400 status code result in error.</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/common/get-host-info.sub.js"></script>
+ </head>
+ <body>
+ <script type="text/javascript">
+ async_test((test) => {
+ const xhr = new XMLHttpRequest;
+
+ xhr.open("GET", get_host_info().HTTP_REMOTE_ORIGIN + "/xhr/resources/access-control-preflight-request-invalid-status.py?code=400");
+
+ xhr.setRequestHeader("x-pass", "pass");
+
+ xhr.onerror = test.step_func_done(function() {
+ assert_equals(xhr.status, 0);
+ });
+
+ xhr.onload = test.unreached_func("Invalid 400 response to preflight should result in error.");
+
+ xhr.send();
+ }, "Request with 400 preflight response");
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/access-control-preflight-request-invalid-status-501.htm b/test/wpt/tests/xhr/access-control-preflight-request-invalid-status-501.htm
new file mode 100644
index 0000000..f2ed85b
--- /dev/null
+++ b/test/wpt/tests/xhr/access-control-preflight-request-invalid-status-501.htm
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Tests that preflight requests returning invalid 501 status code result in error.</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/common/get-host-info.sub.js"></script>
+ </head>
+ <body>
+ <script type="text/javascript">
+ async_test((test) => {
+ const xhr = new XMLHttpRequest;
+
+ xhr.open("GET", get_host_info().HTTP_REMOTE_ORIGIN + "/xhr/resources/access-control-preflight-request-invalid-status.py?code=501");
+
+ xhr.setRequestHeader("x-pass", "pass");
+
+ xhr.onerror = test.step_func_done(function() {
+ assert_equals(xhr.status, 0);
+ });
+
+ xhr.onload = test.unreached_func("Invalid 501 response to preflight should result in error.");
+
+ xhr.send();
+ }, "Request with 501 preflight response");
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/access-control-preflight-request-must-not-contain-cookie.htm b/test/wpt/tests/xhr/access-control-preflight-request-must-not-contain-cookie.htm
new file mode 100644
index 0000000..6dd8e6d
--- /dev/null
+++ b/test/wpt/tests/xhr/access-control-preflight-request-must-not-contain-cookie.htm
@@ -0,0 +1,57 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Preflight request must not contain any cookie header</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/common/get-host-info.sub.js"></script>
+ </head>
+ <body>
+ <script type="text/javascript">
+ async_test((test) => {
+ function setupCookie() {
+ const xhr = new XMLHttpRequest;
+ // Delete all preexisting cookies and set a cookie named "foo"
+ xhr.open("GET", get_host_info().HTTP_REMOTE_ORIGIN +
+ "/xhr/resources/access-control-cookie.py?cookie_name=foo");
+ xhr.withCredentials = true;
+ xhr.send();
+ xhr.onerror = test.unreached_func("Unexpected error.");
+ xhr.onload = test.step_func(() => {
+ assert_equals(xhr.status, 200);
+ sendPreflightedRequest();
+ });
+ }
+
+ function sendPreflightedRequest() {
+ const xhr = new XMLHttpRequest;
+ // Request to server-side file fails if cookie is included in preflight
+ xhr.open("GET", get_host_info().HTTP_REMOTE_ORIGIN +
+ "/xhr/resources/access-control-preflight-request-must-not-contain-cookie.py");
+ xhr.withCredentials = true;
+ xhr.setRequestHeader("X-Proprietary-Header", "foo");
+ xhr.onerror = test.unreached_func("Unexpected error.");
+ xhr.onload = test.step_func(() => {
+ assert_equals(xhr.status, 200);
+ assert_equals(xhr.responseText, "COOKIE");
+ cleanupCookies();
+ });
+ xhr.send();
+ }
+
+ function cleanupCookies() {
+ const xhr = new XMLHttpRequest;
+ // Delete all cookies
+ xhr.open("GET", get_host_info().HTTP_REMOTE_ORIGIN +
+ "/xhr/resources/access-control-cookie.py");
+ xhr.withCredentials = true;
+ xhr.send();
+ xhr.onerror = test.unreached_func("Unexpected error.");
+ xhr.onload = test.step_func_done(() => {});
+ }
+
+ setupCookie();
+ });
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/access-control-preflight-sync-header-denied.htm b/test/wpt/tests/xhr/access-control-preflight-sync-header-denied.htm
new file mode 100644
index 0000000..8697f1e
--- /dev/null
+++ b/test/wpt/tests/xhr/access-control-preflight-sync-header-denied.htm
@@ -0,0 +1,34 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Sync request denied at preflight because of non-CORS-safelisted header</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/common/get-host-info.sub.js"></script>
+ <script src="/common/utils.js"></script>
+ </head>
+ <body>
+ <script type="text/javascript">
+const uuid = token();
+const url = get_host_info().HTTP_REMOTE_ORIGIN +
+ "/xhr/resources/access-control-preflight-denied.py?token=" + uuid;
+
+test(() => {
+ let xhr = new XMLHttpRequest;
+ xhr.open("GET", url + "&command=reset", false);
+ xhr.send();
+
+ xhr = new XMLHttpRequest;
+ xhr.open("GET", url + "&command=header", false);
+ xhr.setRequestHeader("x-test", "foo");
+
+ assert_throws_dom("NetworkError", () => xhr.send());
+
+ xhr = new XMLHttpRequest;
+ xhr.open("GET", url + "&command=complete", false);
+ xhr.send();
+ assert_equals(xhr.responseText, "Request successfully blocked.");
+}, "Sync request denied at preflight");
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/access-control-preflight-sync-method-denied.htm b/test/wpt/tests/xhr/access-control-preflight-sync-method-denied.htm
new file mode 100644
index 0000000..0ca6c5c
--- /dev/null
+++ b/test/wpt/tests/xhr/access-control-preflight-sync-method-denied.htm
@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Sync request denied at preflight because of non-CORS-safelisted method</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/common/get-host-info.sub.js"></script>
+ <script src="/common/utils.js"></script>
+ </head>
+ <body>
+ <script type="text/javascript">
+const uuid = token();
+const url = get_host_info().HTTP_REMOTE_ORIGIN +
+ "/xhr/resources/access-control-preflight-denied.py?token=" + uuid;
+
+test(() => {
+ let xhr = new XMLHttpRequest;
+ xhr.open("GET", url + "&command=reset", false);
+ xhr.send();
+
+ xhr = new XMLHttpRequest;
+ xhr.open("DELETE", url + "&command=method", false);
+
+ assert_throws_dom("NetworkError", () => xhr.send());
+
+ xhr = new XMLHttpRequest;
+ xhr.open("GET", url + "&command=complete", false);
+ xhr.send();
+ assert_equals(xhr.responseText, "Request successfully blocked.");
+});
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/access-control-preflight-sync-not-supported.htm b/test/wpt/tests/xhr/access-control-preflight-sync-not-supported.htm
new file mode 100644
index 0000000..f5df4a2
--- /dev/null
+++ b/test/wpt/tests/xhr/access-control-preflight-sync-not-supported.htm
@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Sync PUT request denied at preflight</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/common/get-host-info.sub.js"></script>
+ <script src="/common/utils.js"></script>
+ </head>
+ <body>
+ <script type="text/javascript">
+const uuid = token();
+const url = get_host_info().HTTP_REMOTE_ORIGIN +
+ "/xhr/resources/access-control-preflight-denied.py?token=" + uuid;
+
+test(() => {
+ let xhr = new XMLHttpRequest;
+ xhr.open("GET", url + "&command=reset", false);
+ xhr.send();
+
+ xhr = new XMLHttpRequest;
+ xhr.open("PUT", url, false);
+
+ assert_throws_dom("NetworkError", () => xhr.send(""));
+
+ xhr = new XMLHttpRequest;
+ xhr.open("GET", url + "&command=complete", false);
+ xhr.send();
+ assert_equals(xhr.responseText, "Request successfully blocked.");
+});
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/access-control-recursive-failed-request.htm b/test/wpt/tests/xhr/access-control-recursive-failed-request.htm
new file mode 100644
index 0000000..2c2bcef
--- /dev/null
+++ b/test/wpt/tests/xhr/access-control-recursive-failed-request.htm
@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Recursively repeated CORS requests with failed preflights should never result in unexpected behavior</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/common/get-host-info.sub.js"></script>
+ </head>
+ <body>
+ <script type="text/javascript">
+let requestCount = 0;
+const requestMax = 10;
+
+async_test((test) => {
+ function preflightRequest() {
+ const xhr = new XMLHttpRequest;
+
+ xhr.onload = test.unreached_func("Request succeeded unexpectedly.");
+
+ xhr.onerror = test.step_func(() => {
+ assert_equals(xhr.status, 0);
+ if (++requestCount >= requestMax) {
+ test.done();
+ return;
+ }
+ preflightRequest();
+ });
+
+ xhr.open("GET", get_host_info().HTTP_REMOTE_ORIGIN +
+ "/xhr/resources/access-control-basic-denied.py");
+ xhr.send();
+ }
+
+ preflightRequest();
+});
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/access-control-response-with-body-sync.htm b/test/wpt/tests/xhr/access-control-response-with-body-sync.htm
new file mode 100644
index 0000000..d4c90ae
--- /dev/null
+++ b/test/wpt/tests/xhr/access-control-response-with-body-sync.htm
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Tests body from CORS preflight response and actual response with sync request</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/common/get-host-info.sub.js"></script>
+ </head>
+ <body>
+ <script type="text/javascript">
+test(() => {
+ const xhr = new XMLHttpRequest;
+
+ xhr.open("GET", get_host_info().HTTP_REMOTE_ORIGIN+
+ "/xhr/resources/access-control-allow-with-body.py", false);
+ xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
+ xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
+ xhr.send();
+
+ assert_equals(xhr.status, 200);
+ assert_equals(xhr.responseText, "PASS");
+});
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/access-control-response-with-body.htm b/test/wpt/tests/xhr/access-control-response-with-body.htm
new file mode 100644
index 0000000..3ab0521
--- /dev/null
+++ b/test/wpt/tests/xhr/access-control-response-with-body.htm
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Tests that XHR doesn't prepend the body from CORS preflight response to the actual response</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/common/get-host-info.sub.js"></script>
+ </head>
+ <body>
+ <script type="text/javascript">
+async_test((test) => {
+ const xhr = new XMLHttpRequest;
+
+ xhr.onerror = test.unreached_func("Unexpected error.");
+
+ xhr.onload = test.step_func_done(() => {
+ assert_equals(xhr.status, 200);
+ assert_equals(xhr.responseText, "PASS");
+ });
+
+ xhr.open("GET", get_host_info().HTTP_REMOTE_ORIGIN +
+ "/xhr/resources/access-control-allow-with-body.py");
+ xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
+ xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
+ xhr.send();
+});
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/access-control-response-with-exposed-headers.htm b/test/wpt/tests/xhr/access-control-response-with-exposed-headers.htm
new file mode 100644
index 0000000..c6f7bf5
--- /dev/null
+++ b/test/wpt/tests/xhr/access-control-response-with-exposed-headers.htm
@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Test CORS response with 'Access-Control-Expose-Headers' header</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/common/get-host-info.sub.js"></script>
+ </head>
+ <body>
+ <script type="text/javascript">
+async_test((test) => {
+ const xhr = new XMLHttpRequest;
+
+ xhr.open("GET", get_host_info().HTTP_REMOTE_ORIGIN +
+ "/xhr/resources/pass.txt?pipe=" +
+ "header(Cache-Control,no-cache)|" +
+ "header(Access-Control-Max-Age,0)|" +
+ "header(Access-Control-Allow-Origin,*)|" +
+ "header(X-foo,BAR)|" +
+ "header(x-test,TEST)|" +
+ "header(Access-Control-Expose-Headers,x-Foo)|",
+ "header(Content-Type,text/html)");
+
+ xhr.onerror = test.unreached_func("Unexpected error");
+
+ xhr.onload = test.step_func_done(() => {
+ assert_equals(xhr.status, 200);
+ assert_equals(xhr.getResponseHeader("X-FOO"), "BAR");
+ assert_equals(xhr.getResponseHeader("x-foo"), "BAR");
+ assert_equals(xhr.getResponseHeader("x-test"), null);
+ assert_equals(xhr.responseText, "PASS\n");
+ });
+
+ xhr.send();
+});
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/access-control-sandboxed-iframe-allow-origin-null.htm b/test/wpt/tests/xhr/access-control-sandboxed-iframe-allow-origin-null.htm
new file mode 100644
index 0000000..ae96668
--- /dev/null
+++ b/test/wpt/tests/xhr/access-control-sandboxed-iframe-allow-origin-null.htm
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Tests that sandboxed iframe has CORS XHR access to a server that accepts null domain</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/common/get-host-info.sub.js"></script>
+ </head>
+ <body>
+ <script type="text/javascript">
+async_test((test) => {
+ window.addEventListener("message", test.step_func((evt) => {
+ if (evt.data === "ready") {
+ document.getElementById("frame").contentWindow.postMessage(
+ get_host_info().HTTP_ORIGIN +
+ "/xhr/resources/pass.txt?pipe=" +
+ "header(Cache-Control,no-store)|" +
+ "header(Content-Type,text/plain)|" +
+ "header(Access-Control-Allow-Credentials,true)|" +
+ "header(Access-Control-Allow-External,true)|" +
+ "header(Access-Control-Allow-Origin,null)", "*");
+ } else {
+ assert_equals(evt.data.trim(), "PASS");
+ test.done();
+ }
+ }), false);
+});
+ </script>
+ <iframe id="frame" sandbox="allow-scripts" src="/xhr/resources/access-control-sandboxed-iframe.html">
+ </iframe>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/access-control-sandboxed-iframe-allow.htm b/test/wpt/tests/xhr/access-control-sandboxed-iframe-allow.htm
new file mode 100644
index 0000000..a3dbed4
--- /dev/null
+++ b/test/wpt/tests/xhr/access-control-sandboxed-iframe-allow.htm
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Tests that sandboxed iframe has CORS XHR access to a server that accepts all domains</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/common/get-host-info.sub.js"></script>
+ </head>
+ <body>
+ <script type="text/javascript">
+async_test((test) => {
+ window.addEventListener("message", test.step_func((evt) => {
+ if (evt.data === "ready") {
+ document.getElementById("frame").contentWindow.postMessage(
+ get_host_info().HTTP_ORIGIN +
+ "/xhr/resources/pass.txt?pipe=" +
+ "header(Cache-Control,no-store)|" +
+ "header(Content-Type,text/plain)|" +
+ "header(Access-Control-Allow-Credentials,true)|" +
+ "header(Access-Control-Allow-External,true)|" +
+ "header(Access-Control-Allow-Origin,*)", "*");
+ } else {
+ assert_equals(evt.data.trim(), "PASS");
+ test.done();
+ }
+ }), false);
+});
+ </script>
+ <iframe id="frame" sandbox="allow-scripts" src="/xhr/resources/access-control-sandboxed-iframe.html">
+ </iframe>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/access-control-sandboxed-iframe-denied-without-wildcard.htm b/test/wpt/tests/xhr/access-control-sandboxed-iframe-denied-without-wildcard.htm
new file mode 100644
index 0000000..a703a92
--- /dev/null
+++ b/test/wpt/tests/xhr/access-control-sandboxed-iframe-denied-without-wildcard.htm
@@ -0,0 +1,43 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Tests that sandboxed iframe does not have CORS XHR access to server with "Access-Control-Allow-Origin" set to the original origin</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/common/get-host-info.sub.js"></script>
+ </head>
+ <body>
+ <script type="text/javascript">
+const path = "/xhr/resources/pass.txt?pipe=" +
+ "header(Cache-Control,no-store)|" +
+ "header(Content-Type,text/plain)" +
+ "header(Access-Control-Allow-Credentials,true)|" +
+ "header(Access-Control-Allow-Origin," + get_host_info().HTTP_ORIGIN + ")";
+
+async_test((test) => {
+ const xhr = new XMLHttpRequest;
+ xhr.open("GET", get_host_info().HTTP_REMOTE_ORIGIN + path);
+ xhr.send();
+ xhr.onerror = test.unreached_func("Unexpected error");
+ xhr.onload = test.step_func_done(() => {
+ assert_equals(xhr.status, 200);
+ assert_equals(xhr.responseText.trim(), "PASS");
+ });
+}, "Check that path exists and is accessible via CORS XHR request");
+
+async_test((test) => {
+ window.addEventListener("message", test.step_func((evt) => {
+ if (evt.data === "ready") {
+ document.getElementById("frame").contentWindow.postMessage(
+ get_host_info().HTTP_REMOTE_ORIGIN + path, "*");
+ } else {
+ assert_equals(evt.data, "Exception thrown. Sandboxed iframe XHR access was denied in 'send'.");
+ test.done();
+ }
+ }), false);
+}, "Sandboxed iframe is denied CORS access to server that allows parent origin");
+ </script>
+ <iframe id="frame" sandbox="allow-scripts" src="/xhr/resources/access-control-sandboxed-iframe.html">
+ </iframe>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/access-control-sandboxed-iframe-denied.htm b/test/wpt/tests/xhr/access-control-sandboxed-iframe-denied.htm
new file mode 100644
index 0000000..5b62991
--- /dev/null
+++ b/test/wpt/tests/xhr/access-control-sandboxed-iframe-denied.htm
@@ -0,0 +1,41 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Tests that sandboxed iframe does not have CORS XHR access to its server</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/common/get-host-info.sub.js"></script>
+ </head>
+ <body>
+ <script type="text/javascript">
+const path = "/xhr/resources/pass.txt?pipe=" +
+ "header(Cache-Control,no-store)|" +
+ "header(Content-Type,text/plain)";
+
+async_test((test) => {
+ const xhr = new XMLHttpRequest;
+ xhr.open("GET", get_host_info().HTTP_ORIGIN + path);
+ xhr.send();
+ xhr.onerror = test.unreached_func("Unexpected error");
+ xhr.onload = test.step_func_done(() => {
+ assert_equals(xhr.status, 200);
+ assert_equals(xhr.responseText.trim(), "PASS");
+ });
+}, "Check that path exists and is accessible via local XHR request");
+
+async_test((test) => {
+ window.addEventListener("message", test.step_func((evt) => {
+ if (evt.data === "ready") {
+ document.getElementById("frame").contentWindow.postMessage(
+ get_host_info().HTTP_ORIGIN + path, "*");
+ } else {
+ assert_equals(evt.data, "Exception thrown. Sandboxed iframe XHR access was denied in 'send'.");
+ test.done();
+ }
+ }), false);
+}, "Sandboxed iframe is denied access to path");
+ </script>
+ <iframe id="frame" sandbox="allow-scripts" src="/xhr/resources/access-control-sandboxed-iframe.html">
+ </iframe>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/allow-lists-starting-with-comma.htm b/test/wpt/tests/xhr/allow-lists-starting-with-comma.htm
new file mode 100644
index 0000000..ece699b
--- /dev/null
+++ b/test/wpt/tests/xhr/allow-lists-starting-with-comma.htm
@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Allow lists starting with a comma should be parsed correctly</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/common/get-host-info.sub.js"></script>
+ </head>
+ <body>
+ <script type="text/javascript">
+ async_test(function(test) {
+ const client = new XMLHttpRequest();
+ let url = "xhr/resources/access-control-allow-lists.py?headers=,y-lol,x-print,%20,,,y-print&origin=" +
+ get_host_info().HTTP_ORIGIN;
+ client.open("GET", get_host_info().HTTP_REMOTE_ORIGIN + '/' + url, false);
+ client.setRequestHeader('x-print', 'unicorn')
+ client.setRequestHeader('y-print', 'narwhal')
+ // Sending GET request with custom headers
+ assert_equals(client.send(null), undefined);
+ const response = JSON.parse(client.response);
+ assert_equals(response['x-print'], "unicorn");
+ assert_equals(response['y-print'], "narwhal");
+
+ url = "xhr/resources/access-control-allow-lists.py?methods=,,PUT,GET&origin=" +
+ get_host_info().HTTP_ORIGIN;
+ client.open("PUT", get_host_info().HTTP_REMOTE_ORIGIN + '/' + url, false);
+ // Sending PUT request
+ assert_equals(client.send(null), undefined);
+ test.done();
+ }, "Allow lists starting with a comma should be parsed correctly");
+ </script>
+ </body>
+ </html>
diff --git a/test/wpt/tests/xhr/anonymous-mode-unsupported.htm b/test/wpt/tests/xhr/anonymous-mode-unsupported.htm
new file mode 100644
index 0000000..f995ec2
--- /dev/null
+++ b/test/wpt/tests/xhr/anonymous-mode-unsupported.htm
@@ -0,0 +1,40 @@
+<!doctype html>
+<html>
+ <head>
+ <title>XMLHttpRequest: anonymous mode unsupported</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ /*
+ Older versions of the XMLHttpRequest spec had an 'anonymous' mode
+ The point of this mode was to handle same-origin requests like other-origin requests,
+ i.e. require preflight, drop authentication data (cookies and HTTP auth)
+ Also the Origin: and Referer: headers would not be sent
+
+ This mode was dropped due to lack of implementations and interest,
+ and this test is here just to assert failure if any implementation
+ supports this based on an older spec version.
+ */
+ document.cookie = 'test=anonymous-mode-unsupported'
+ test = async_test();
+ test.add_cleanup(function(){
+ // make sure we clean up the cookie again to avoid confusing other tests..
+ document.cookie = 'test=;expires=Fri, 28 Feb 2014 07:25:59 GMT';
+ })
+ test.step(function() {
+ var client = new XMLHttpRequest({anonymous:true})
+ client.open("GET", "resources/inspect-headers.py?filter_name=cookie")
+ client.onreadystatechange = test.step_func(function(){
+ if(client.readyState === 4){
+ assert_equals(client.responseText, 'Cookie: test=anonymous-mode-unsupported\n', 'The deprecated anonymous:true should be ignored, cookie sent anyway')
+ test.done();
+ }
+ });
+ client.send(null)
+ })
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/blob-range.any.js b/test/wpt/tests/xhr/blob-range.any.js
new file mode 100644
index 0000000..2a5c54f
--- /dev/null
+++ b/test/wpt/tests/xhr/blob-range.any.js
@@ -0,0 +1,246 @@
+// See also /fetch/range/blob.any.js
+
+const supportedBlobRange = [
+ {
+ name: "A simple blob range request.",
+ data: ["A simple Hello, World! example"],
+ type: "text/plain",
+ range: "bytes=9-21",
+ content_length: 13,
+ content_range: "bytes 9-21/30",
+ result: "Hello, World!",
+ },
+ {
+ name: "A blob range request with no type.",
+ data: ["A simple Hello, World! example"],
+ type: undefined,
+ range: "bytes=9-21",
+ content_length: 13,
+ content_range: "bytes 9-21/30",
+ result: "Hello, World!",
+ },
+ {
+ name: "A blob range request with no end.",
+ data: ["Range with no end"],
+ type: "text/plain",
+ range: "bytes=11-",
+ content_length: 6,
+ content_range: "bytes 11-16/17",
+ result: "no end",
+ },
+ {
+ name: "A blob range request with no start.",
+ data: ["Range with no start"],
+ type: "text/plain",
+ range: "bytes=-8",
+ content_length: 8,
+ content_range: "bytes 11-18/19",
+ result: "no start",
+ },
+ {
+ name: "A simple blob range request with whitespace.",
+ data: ["A simple Hello, World! example"],
+ type: "text/plain",
+ range: "bytes= \t9-21",
+ content_length: 13,
+ content_range: "bytes 9-21/30",
+ result: "Hello, World!",
+ },
+ {
+ name: "Blob content with short content and a large range end",
+ data: ["Not much here"],
+ type: "text/plain",
+ range: "bytes=4-100000000000",
+ content_length: 9,
+ content_range: "bytes 4-12/13",
+ result: "much here",
+ },
+ {
+ name: "Blob content with short content and a range end matching content length",
+ data: ["Not much here"],
+ type: "text/plain",
+ range: "bytes=4-13",
+ content_length: 9,
+ content_range: "bytes 4-12/13",
+ result: "much here",
+ },
+ {
+ name: "Blob range with whitespace before and after hyphen",
+ data: ["Valid whitespace #1"],
+ type: "text/plain",
+ range: "bytes=5 - 10",
+ content_length: 6,
+ content_range: "bytes 5-10/19",
+ result: " white",
+ },
+ {
+ name: "Blob range with whitespace after hyphen",
+ data: ["Valid whitespace #2"],
+ type: "text/plain",
+ range: "bytes=-\t 5",
+ content_length: 5,
+ content_range: "bytes 14-18/19",
+ result: "ce #2",
+ },
+ {
+ name: "Blob range with whitespace around equals sign",
+ data: ["Valid whitespace #3"],
+ type: "text/plain",
+ range: "bytes \t =\t 6-",
+ content_length: 13,
+ content_range: "bytes 6-18/19",
+ result: "whitespace #3",
+ },
+];
+
+const unsupportedBlobRange = [
+ {
+ name: "Blob range with no value",
+ data: ["Blob range should have a value"],
+ type: "text/plain",
+ range: "",
+ },
+ {
+ name: "Blob range with incorrect range header",
+ data: ["A"],
+ type: "text/plain",
+ range: "byte=0-"
+ },
+ {
+ name: "Blob range with incorrect range header #2",
+ data: ["A"],
+ type: "text/plain",
+ range: "bytes"
+ },
+ {
+ name: "Blob range with incorrect range header #3",
+ data: ["A"],
+ type: "text/plain",
+ range: "bytes\t \t"
+ },
+ {
+ name: "Blob range request with multiple range values",
+ data: ["Multiple ranges are not currently supported"],
+ type: "text/plain",
+ range: "bytes=0-5,15-",
+ },
+ {
+ name: "Blob range request with multiple range values and whitespace",
+ data: ["Multiple ranges are not currently supported"],
+ type: "text/plain",
+ range: "bytes=0-5, 15-",
+ },
+ {
+ name: "Blob range request with trailing comma",
+ data: ["Range with invalid trailing comma"],
+ type: "text/plain",
+ range: "bytes=0-5,",
+ },
+ {
+ name: "Blob range with no start or end",
+ data: ["Range with no start or end"],
+ type: "text/plain",
+ range: "bytes=-",
+ },
+ {
+ name: "Blob range request with short range end",
+ data: ["Range end should be greater than range start"],
+ type: "text/plain",
+ range: "bytes=10-5",
+ },
+ {
+ name: "Blob range start should be an ASCII digit",
+ data: ["Range start must be an ASCII digit"],
+ type: "text/plain",
+ range: "bytes=x-5",
+ },
+ {
+ name: "Blob range should have a dash",
+ data: ["Blob range should have a dash"],
+ type: "text/plain",
+ range: "bytes=5",
+ },
+ {
+ name: "Blob range end should be an ASCII digit",
+ data: ["Range end must be an ASCII digit"],
+ type: "text/plain",
+ range: "bytes=5-x",
+ },
+ {
+ name: "Blob range should include '-'",
+ data: ["Range end must include '-'"],
+ type: "text/plain",
+ range: "bytes=x",
+ },
+ {
+ name: "Blob range should include '='",
+ data: ["Range end must include '='"],
+ type: "text/plain",
+ range: "bytes 5-",
+ },
+ {
+ name: "Blob range should include 'bytes='",
+ data: ["Range end must include 'bytes='"],
+ type: "text/plain",
+ range: "5-",
+ },
+ {
+ name: "Blob content with short content and a large range start",
+ data: ["Not much here"],
+ type: "text/plain",
+ range: "bytes=100000-",
+ },
+ {
+ name: "Blob content with short content and a range start matching the content length",
+ data: ["Not much here"],
+ type: "text/plain",
+ range: "bytes=13-",
+ },
+];
+
+supportedBlobRange.forEach(({ name, data, type, range, content_length, content_range, result }) => {
+ promise_test(async t => {
+ const blob = new Blob(data, { "type" : type });
+ const blobURL = URL.createObjectURL(blob);
+ t.add_cleanup(() => URL.revokeObjectURL(blobURL));
+ const xhr = new XMLHttpRequest();
+ xhr.open("GET", blobURL);
+ xhr.responseType = "text";
+ xhr.setRequestHeader("Range", range);
+ await new Promise(resolve => {
+ xhr.onloadend = resolve;
+ xhr.send();
+ });
+ assert_equals(xhr.status, 206, "HTTP status is 206");
+ assert_equals(xhr.getResponseHeader("Content-Type"), type || "", "Content-Type is " + xhr.getResponseHeader("Content-Type"));
+ assert_equals(xhr.getResponseHeader("Content-Length"), content_length.toString(), "Content-Length is " + xhr.getResponseHeader("Content-Length"));
+ assert_equals(xhr.getResponseHeader("Content-Range"), content_range, "Content-Range is " + xhr.getResponseHeader("Content-Range"));
+ assert_equals(xhr.responseText, result, "Response's body is correct");
+ const all = xhr.getAllResponseHeaders().toLowerCase();
+ assert_true(all.includes(`content-type: ${type || ""}`), "Expected Content-Type in getAllResponseHeaders()");
+ assert_true(all.includes(`content-length: ${content_length}`), "Expected Content-Length in getAllResponseHeaders()");
+ assert_true(all.includes(`content-range: ${content_range}`), "Expected Content-Range in getAllResponseHeaders()")
+ }, name);
+});
+
+unsupportedBlobRange.forEach(({ name, data, type, range }) => {
+ promise_test(t => {
+ const blob = new Blob(data, { "type" : type });
+ const blobURL = URL.createObjectURL(blob);
+ t.add_cleanup(() => URL.revokeObjectURL(blobURL));
+
+ const xhr = new XMLHttpRequest();
+ xhr.open("GET", blobURL, false);
+ xhr.setRequestHeader("Range", range);
+ assert_throws_dom("NetworkError", () => xhr.send());
+
+ xhr.open("GET", blobURL);
+ xhr.setRequestHeader("Range", range);
+ xhr.responseType = "text";
+ return new Promise((resolve, reject) => {
+ xhr.onload = reject;
+ xhr.onerror = resolve;
+ xhr.send();
+ });
+ }, name);
+});
diff --git a/test/wpt/tests/xhr/close-worker-with-xhr-in-progress.html b/test/wpt/tests/xhr/close-worker-with-xhr-in-progress.html
new file mode 100644
index 0000000..4d03bea
--- /dev/null
+++ b/test/wpt/tests/xhr/close-worker-with-xhr-in-progress.html
@@ -0,0 +1,26 @@
+<!doctype html>
+<html>
+<head>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+async_test(t => {
+ function workerCode(origin) {
+ const xhr = new XMLHttpRequest();
+ xhr.open('GET', origin + '/xhr/resources/image.gif?pipe=trickle(100:d2)', true);
+ xhr.responseType = 'blob';
+ xhr.send();
+ postMessage('sent');
+ }
+
+ const workerBlob = new Blob([workerCode.toString() + ";workerCode('" + location.origin + "');"], {type:"application/javascript"});
+ const w = new Worker(URL.createObjectURL(workerBlob));
+ w.onmessage = t.step_func(e => {
+ assert_equals(e.data, 'sent');
+ t.step_timeout(t.step_func(() => {
+ w.terminate();
+ t.step_timeout(t.step_func_done(() => {}), 500);
+ }, 100));
+ });
+}, 'Terminating a worker with a XHR in progress doesn\'t crash');
+</script>
diff --git a/test/wpt/tests/xhr/content-type-unmodified.any.js b/test/wpt/tests/xhr/content-type-unmodified.any.js
new file mode 100644
index 0000000..92705d6
--- /dev/null
+++ b/test/wpt/tests/xhr/content-type-unmodified.any.js
@@ -0,0 +1,16 @@
+"use strict";
+
+async_test(t => {
+ const xhr = new XMLHttpRequest();
+
+ xhr.open("POST", "resources/echo-content-type.py");
+ xhr.setRequestHeader("content-type", "application/json; charset=UTF-8");
+
+ xhr.onerror = t.unreached_func("Error occurred.");
+
+ xhr.onload = t.step_func_done(() => {
+ assert_equals(xhr.status, 200);
+ assert_equals(xhr.responseText, "application/json; charset=UTF-8");
+ });
+ xhr.send("{ \"x\":\"a\"}");
+});
diff --git a/test/wpt/tests/xhr/cookies.http.html b/test/wpt/tests/xhr/cookies.http.html
new file mode 100644
index 0000000..0ab71dd
--- /dev/null
+++ b/test/wpt/tests/xhr/cookies.http.html
@@ -0,0 +1,41 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<body>
+<p>Derived from historical testcase for <a href="http://bugs.webkit.org/show_bug.cgi?id=3420">WebKit bug 3420</a>:
+XMLHttpRequest does not handle set-cookie headers.</p>
+
+<script>
+ function clearCookies()
+ {
+ return new Promise(resolve => {
+ var req = new XMLHttpRequest;
+ req.open("POST", "resources/get-set-cookie.py?clear=1");
+ req.onload = () => resolve();
+ req.send("");
+ });
+ }
+ function getAndSetCookies()
+ {
+ return new Promise(resolve => {
+ var req = new XMLHttpRequest;
+ req.open("POST", "resources/get-set-cookie.py");
+ req.onload = () => resolve(req.responseText);
+ req.send("");
+ });
+ }
+
+ promise_test(async function(t) {
+ await clearCookies();
+ var response = await getAndSetCookies();
+ assert_equals(response.match(/.*WK-test=1.*/), null,
+ "The cookie must not be present after clear. clearCookies() failed. Must be a bug in the test!");
+ var response = await getAndSetCookies();
+ assert_equals(response.match(/.*WK-test-secure=1.*/), null,
+ "a secure cookie was sent via HTTP");
+ assert_regexp_match(response, /.*WK-test=1.*/, "an insecure cookie was sent");
+ await clearCookies();
+ }, "Basic non-cross-site cookie handling in XHR");
+</script>
+</html>
diff --git a/test/wpt/tests/xhr/cors-expose-star.sub.any.js b/test/wpt/tests/xhr/cors-expose-star.sub.any.js
new file mode 100644
index 0000000..9d88046
--- /dev/null
+++ b/test/wpt/tests/xhr/cors-expose-star.sub.any.js
@@ -0,0 +1,52 @@
+// META: script=../fetch/api/resources/utils.js
+
+const url = "http://{{host}}:{{ports[http][1]}}" + dirname(location.pathname) + "resources/top.txt",
+ sharedHeaders = "?pipe=header(Access-Control-Expose-Headers,*)|header(Test,X)|header(*,whoa)|"
+
+async_test(function() {
+ const headers = "header(Access-Control-Allow-Origin,*)"
+ var client = new XMLHttpRequest();
+ client.open("GET", url + sharedHeaders + headers);
+ client.send();
+ client.onreadystatechange = this.step_func(function () {
+ if (this.readyState == this.HEADERS_RECEIVED) {
+ assert_equals(client.getResponseHeader("test"), "X");
+ assert_equals(client.getResponseHeader("set-cookie"), null);
+ assert_equals(client.getResponseHeader("*"), "whoa");
+ this.done();
+ }
+ });
+}, "Basic Access-Control-Expose-Headers: * support")
+
+async_test(function() {
+ const origin = location.origin, // assuming an ASCII origin
+ headers = "header(Access-Control-Allow-Origin," + origin + ")|header(Access-Control-Allow-Credentials,true)"
+ var client = new XMLHttpRequest();
+ client.open("GET", url + sharedHeaders + headers);
+ client.withCredentials = true;
+ client.send();
+ client.onreadystatechange = this.step_func(function () {
+ if (this.readyState == this.HEADERS_RECEIVED) {
+ assert_equals(client.getResponseHeader("content-type"), "text/plain"); // safelisted
+ assert_equals(client.getResponseHeader("test"), null);
+ assert_equals(client.getResponseHeader("set-cookie"), null);
+ assert_equals(client.getResponseHeader("*"), "whoa");
+ this.done();
+ }
+ });
+}, "* for credentialed fetches only matches literally")
+
+async_test(function() {
+ const headers = "header(Access-Control-Allow-Origin,*)|header(Access-Control-Expose-Headers,set-cookie\\,*)"
+ var client = new XMLHttpRequest();
+ client.open("GET", url + sharedHeaders + headers);
+ client.send();
+ client.onreadystatechange = this.step_func(function () {
+ if (this.readyState == this.HEADERS_RECEIVED) {
+ assert_equals(client.getResponseHeader("test"), "X");
+ assert_equals(client.getResponseHeader("set-cookie"), null);
+ assert_equals(client.getResponseHeader("*"), "whoa");
+ this.done();
+ }
+ });
+}, "* can be one of several values")
diff --git a/test/wpt/tests/xhr/cors-upload.any.js b/test/wpt/tests/xhr/cors-upload.any.js
new file mode 100644
index 0000000..3beb063
--- /dev/null
+++ b/test/wpt/tests/xhr/cors-upload.any.js
@@ -0,0 +1,59 @@
+// META: title=Cross-Origin POST with preflight and FormData body should send body
+// META: script=/common/get-host-info.sub.js
+"use strict";
+
+function testCorsFormDataUpload(description, path, method, form, headers, withCredentials) {
+ const test = async_test(description);
+ const client = new XMLHttpRequest();
+ const url = corsURL(path);
+
+ client.open(method, url, true);
+ client.withCredentials = withCredentials;
+ for (const key of Object.keys(headers)) {
+ client.setRequestHeader(key, headers[key]);
+ }
+
+ client.send(form);
+
+ client.onload = () => {
+ test.step(() => {
+ assert_equals(client.status, 200);
+ assert_regexp_match(client.responseText, /Content-Disposition: form-data/);
+
+ for (const key of form.keys()) {
+ assert_regexp_match(client.responseText, new RegExp(key));
+ assert_regexp_match(client.responseText, new RegExp(form.get(key)));
+ }
+ });
+ test.done();
+ };
+}
+
+function corsURL(path) {
+ const url = new URL(path, location.href);
+ url.hostname = get_host_info().REMOTE_HOST;
+ return url.href;
+}
+
+const form = new FormData();
+form.append("key", "value");
+
+testCorsFormDataUpload(
+ "Cross-Origin POST FormData body but no preflight",
+ "resources/echo-content-cors.py",
+ "POST",
+ form,
+ {},
+ false
+);
+
+testCorsFormDataUpload(
+ "Cross-Origin POST with preflight and FormData body",
+ "resources/echo-content-cors.py",
+ "POST",
+ form,
+ {
+ Authorization: "Bearer access-token"
+ },
+ true
+);
diff --git a/test/wpt/tests/xhr/data-uri.htm b/test/wpt/tests/xhr/data-uri.htm
new file mode 100644
index 0000000..88a7d78
--- /dev/null
+++ b/test/wpt/tests/xhr/data-uri.htm
@@ -0,0 +1,41 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>XMLHttpRequest: data URLs</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+
+<script>
+ function do_test(method, url, mimeType, testNamePostfix) {
+ if (typeof mimeType === 'undefined' || mimeType === null) mimeType = 'text/plain';
+ var test = async_test("XHR method " + method + " with MIME type " + mimeType + (testNamePostfix||''));
+ test.step(function() {
+ var client = new XMLHttpRequest(),
+ body = method === "HEAD" ? "" : "Hello, World!";
+ client.onreadystatechange = test.step_func(function () {
+ if (client.readyState !== 4) {
+ return;
+ }
+ assert_equals(client.responseText, body);
+ assert_equals(client.status, 200);
+ assert_equals(client.getResponseHeader('Content-Type'), mimeType);
+ var allHeaders = client.getAllResponseHeaders();
+ assert_regexp_match(allHeaders, /content\-type\:/i, 'getAllResponseHeaders() includes Content-Type');
+ assert_false(/content\-length\:/i.test(allHeaders), 'getAllResponseHeaders() must not include Content-Length');
+ test.done();
+ });
+ client.open(method, url);
+ client.send(null);
+ });
+ }
+ do_test('GET', "data:text/plain,Hello, World!");
+ do_test('GET', "data:text/plain;base64,SGVsbG8sIFdvcmxkIQ==", undefined, " (base64)");
+ do_test('GET', "data:text/html,Hello, World!", 'text/html');
+ do_test('GET', "data:text/html;charset=UTF-8,Hello, World!", 'text/html;charset=UTF-8');
+ do_test('GET', "data:image/png,Hello, World!", 'image/png');
+ do_test('POST', "data:text/plain,Hello, World!");
+ do_test('PUT', "data:text/plain,Hello, World!");
+ do_test('DELETE', "data:text/plain,Hello, World!");
+ do_test('HEAD', "data:text/plain,Hello, World!");
+ do_test('UNICORN', "data:text/plain,Hello, World!");
+</script>
diff --git a/test/wpt/tests/xhr/event-abort.any.js b/test/wpt/tests/xhr/event-abort.any.js
new file mode 100644
index 0000000..5b17ece
--- /dev/null
+++ b/test/wpt/tests/xhr/event-abort.any.js
@@ -0,0 +1,15 @@
+// META: title=XMLHttpRequest: abort event
+
+var test = async_test();
+test.step(function () {
+ var client = new XMLHttpRequest();
+ client.onabort = test.step_func(function () {
+ test.done();
+ });
+ client.open("GET", "resources/well-formed.xml");
+ client.send(null);
+ client.abort();
+ test.step_timeout(() => {
+ assert_unreached("onabort not called after 4 ms");
+ }, 4);
+});
diff --git a/test/wpt/tests/xhr/event-error-order.sub.html b/test/wpt/tests/xhr/event-error-order.sub.html
new file mode 100644
index 0000000..f03707e
--- /dev/null
+++ b/test/wpt/tests/xhr/event-error-order.sub.html
@@ -0,0 +1,35 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta name="assert" content="Check the order of events fired when the request has failed.">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="resources/xmlhttprequest-event-order.js"></script>
+ <title>XMLHttpRequest: event - error (order of events)</title>
+</head>
+
+<body>
+ <div id="log"></div>
+
+ <script type="text/javascript">
+ var test = async_test();
+
+ test.step(function()
+ {
+ var xhr = new XMLHttpRequest();
+ prepare_xhr_for_event_order_test(xhr);
+
+ xhr.addEventListener("loadend", function() {
+ test.step(function() {
+ // no progress events due to CORS failure
+ assert_xhr_event_order_matches([1, "loadstart(0,0,false)", "upload.loadstart(0,12,true)", 4, "upload.error(0,0,false)", "upload.loadend(0,0,false)", "error(0,0,false)", "loadend(0,0,false)"]);
+ test.done();
+ });
+ });
+
+ xhr.open("POST", "http://nonexistent.{{host}}:{{ports[http][0]}}", true);
+ xhr.send("Test Message");
+ });
+ </script>
+</body>
+</html>
diff --git a/test/wpt/tests/xhr/event-error.sub.any.js b/test/wpt/tests/xhr/event-error.sub.any.js
new file mode 100644
index 0000000..ecc4678
--- /dev/null
+++ b/test/wpt/tests/xhr/event-error.sub.any.js
@@ -0,0 +1,28 @@
+// META: title=XMLHttpRequest Test: event - error
+
+async_test(function(t) {
+ var client = new XMLHttpRequest();
+ client.onerror = t.step_func(function (e) {
+ assert_true(e instanceof ProgressEvent);
+ assert_equals(e.type, "error");
+ t.done();
+ });
+
+ client.open('GET', 'http://nonexistent.{{host}}:{{ports[http][0]}}');
+ client.send('null');
+}, 'onerror should be called');
+
+async_test((t) => {
+ const xhr = new XMLHttpRequest();
+ xhr.open('GET', 'resources/bad-chunk-encoding.py');
+ xhr.addEventListener('load', t.unreached_func('load'));
+ xhr.addEventListener('error', t.step_func((e) => {
+ assert_equals(e.loaded, 0, 'loaded');
+ assert_equals(e.total, 0, 'total');
+ }));
+ xhr.addEventListener('loadend', t.step_func_done((e) => {
+ assert_equals(e.loaded, 0, 'loaded');
+ assert_equals(e.total, 0, 'total');
+ }));
+ xhr.send();
+}, 'error while reading body should report zeros for loaded and total');
diff --git a/test/wpt/tests/xhr/event-load.any.js b/test/wpt/tests/xhr/event-load.any.js
new file mode 100644
index 0000000..dcb92cc
--- /dev/null
+++ b/test/wpt/tests/xhr/event-load.any.js
@@ -0,0 +1,21 @@
+// META: title=XMLHttpRequest: The send() method: Fire an event named load (synchronous flag is unset)
+
+var test = async_test();
+test.step(function () {
+ var client = new XMLHttpRequest();
+ client.onload = test.step_func(function (e) {
+ assert_true(e instanceof ProgressEvent);
+ assert_equals(e.type, "load");
+ assert_equals(client.readyState, 4);
+ test.done();
+ });
+ client.onreadystatechange = test.step_func(function () {
+ if (client.readyState !== 4) return;
+
+ test.step_timeout(() => {
+ assert_unreached("Didn't get load event within 4ms of readystatechange==4");
+ }, 4);
+ });
+ client.open("GET", "resources/well-formed.xml");
+ client.send(null);
+});
diff --git a/test/wpt/tests/xhr/event-loadend.any.js b/test/wpt/tests/xhr/event-loadend.any.js
new file mode 100644
index 0000000..16087b5
--- /dev/null
+++ b/test/wpt/tests/xhr/event-loadend.any.js
@@ -0,0 +1,19 @@
+// META: title=XMLHttpRequest: loadend event
+
+var test = async_test();
+test.step(function () {
+ var client = new XMLHttpRequest();
+ client.onloadend = test.step_func(function (e) {
+ assert_true(e instanceof ProgressEvent);
+ assert_equals(e.type, "loadend");
+ test.done();
+ });
+ client.onreadystatechange = function () {
+ if (client.readyState !== 4) return;
+ test.step_timeout(() => {
+ assert_unreached("onloadend not called after 100 ms");
+ }, 100);
+ };
+ client.open("GET", "resources/well-formed.xml");
+ client.send(null);
+});
diff --git a/test/wpt/tests/xhr/event-loadstart-upload.any.js b/test/wpt/tests/xhr/event-loadstart-upload.any.js
new file mode 100644
index 0000000..3918adb
--- /dev/null
+++ b/test/wpt/tests/xhr/event-loadstart-upload.any.js
@@ -0,0 +1,19 @@
+// META: title=XMLHttpRequest: The send() method: Fire a progress event named loadstart on upload object (synchronous flag is unset)
+
+var test = async_test();
+test.step(function () {
+ var client = new XMLHttpRequest();
+ client.upload.onloadstart = test.step_func(function (e) {
+ assert_true(e instanceof ProgressEvent);
+ assert_equals(e.total, 7, 'upload.onloadstart: event.total');
+ assert_equals(e.loaded, 0, 'upload.onloadstart: event.loaded');
+ assert_equals(e.type, "loadstart");
+ test.done();
+ });
+ client.onreadystatechange = test.step_func(function () {
+ if (client.readyState === 4)
+ assert_unreached("onloadstart not called.");
+ });
+ client.open("POST", "resources/trickle.py?ms=5&count=8");
+ client.send('foo=bar');
+});
diff --git a/test/wpt/tests/xhr/event-loadstart.any.js b/test/wpt/tests/xhr/event-loadstart.any.js
new file mode 100644
index 0000000..55af4c3
--- /dev/null
+++ b/test/wpt/tests/xhr/event-loadstart.any.js
@@ -0,0 +1,17 @@
+// META: title=XMLHttpRequest: loadstart event
+
+var test = async_test();
+test.step(function () {
+ var client = new XMLHttpRequest();
+ client.onloadstart = test.step_func(function (e) {
+ assert_true(e instanceof ProgressEvent);
+ assert_equals(e.type, "loadstart");
+ assert_equals(client.readyState, 1);
+ test.done();
+ });
+ test.step_timeout(function () {
+ assert_unreached("onloadstart not called after 500 ms");
+ }, 500);
+ client.open("GET", "resources/well-formed.xml");
+ client.send(null);
+});
diff --git a/test/wpt/tests/xhr/event-progress.any.js b/test/wpt/tests/xhr/event-progress.any.js
new file mode 100644
index 0000000..094d361
--- /dev/null
+++ b/test/wpt/tests/xhr/event-progress.any.js
@@ -0,0 +1,18 @@
+// META: title=XMLHttpRequest: The send() method: Fire a progress event named progress (synchronous flag is unset)
+// META: timeout=long
+
+var test = async_test();
+test.step(function () {
+ var client = new XMLHttpRequest();
+ client.onprogress = test.step_func(function (e) {
+ assert_true(e instanceof ProgressEvent);
+ assert_equals(e.type, "progress");
+ test.done();
+ });
+ client.onreadystatechange = test.step_func(function () {
+ if (client.readyState === 4)
+ assert_unreached("onprogress not called.");
+ });
+ client.open("GET", "resources/trickle.py?count=4&delay=150");
+ client.send(null);
+});
diff --git a/test/wpt/tests/xhr/event-readystate-sync-open.any.js b/test/wpt/tests/xhr/event-readystate-sync-open.any.js
new file mode 100644
index 0000000..a8a4fe7
--- /dev/null
+++ b/test/wpt/tests/xhr/event-readystate-sync-open.any.js
@@ -0,0 +1,23 @@
+// META: title=XMLHttpRequest: open() call fires sync readystate event
+
+const title = "XMLHttpRequest: open() call fires sync readystate event";
+
+test(function () {
+ var client = new XMLHttpRequest()
+ var eventsFired = []
+ client.onreadystatechange = function () {
+ eventsFired.push(client.readyState)
+ }
+ client.open('GET', "...", false)
+ assert_array_equals(eventsFired, [1])
+}, title + ' (sync)');
+
+test(function () {
+ var client = new XMLHttpRequest()
+ var eventsFired = []
+ client.onreadystatechange = function () {
+ eventsFired.push(client.readyState)
+ }
+ client.open('GET', "...", true)
+ assert_array_equals(eventsFired, [1])
+}, title + ' (async)');
diff --git a/test/wpt/tests/xhr/event-readystatechange-loaded.any.js b/test/wpt/tests/xhr/event-readystatechange-loaded.any.js
new file mode 100644
index 0000000..a33e6f8
--- /dev/null
+++ b/test/wpt/tests/xhr/event-readystatechange-loaded.any.js
@@ -0,0 +1,23 @@
+// META: title=XMLHttpRequest: the LOADING state change may be emitted multiple times
+
+var test = async_test();
+
+test.step(function () {
+ var client = new XMLHttpRequest();
+ var countedLoading = 0;
+
+ client.onreadystatechange = test.step_func(function () {
+ if (client.readyState === 3) {
+ countedLoading += 1;
+ }
+
+ if (client.readyState === 4) {
+ assert_greater_than(countedLoading, 1, "LOADING state change may be emitted multiple times");
+
+ test.done();
+ }
+ });
+
+ client.open("GET", "resources/trickle.py?count=10"); // default timeout in trickle.py is 1/2 sec, so this request will take 5 seconds to complete
+ client.send(null);
+});
diff --git a/test/wpt/tests/xhr/event-timeout-order.any.js b/test/wpt/tests/xhr/event-timeout-order.any.js
new file mode 100644
index 0000000..74daff9
--- /dev/null
+++ b/test/wpt/tests/xhr/event-timeout-order.any.js
@@ -0,0 +1,21 @@
+// META: title=XMLHttpRequest: event - timeout (order of events)
+// META: script=resources/xmlhttprequest-event-order.js
+
+var test = async_test();
+test.step(function () {
+ var xhr = new XMLHttpRequest();
+ prepare_xhr_for_event_order_test(xhr);
+ xhr.addEventListener("loadend", function () {
+ test.step(function () {
+ assert_xhr_event_order_matches([1, "loadstart(0,0,false)", "upload.loadstart(0,12,true)", 4, "upload.timeout(0,0,false)", "upload.loadend(0,0,false)", "timeout(0,0,false)", "loadend(0,0,false)"]);
+ test.done();
+ });
+ });
+
+ xhr.timeout = 5;
+ xhr.open("POST", "resources/delay.py?ms=20000");
+ xhr.send("Test Message");
+ test.step_timeout(() => {
+ assert_unreached("ontimeout not called.");
+ }, 2000);
+});
diff --git a/test/wpt/tests/xhr/event-timeout.any.js b/test/wpt/tests/xhr/event-timeout.any.js
new file mode 100644
index 0000000..c73cd1a
--- /dev/null
+++ b/test/wpt/tests/xhr/event-timeout.any.js
@@ -0,0 +1,18 @@
+// META: title=XMLHttpRequest: timeout event
+
+var test = async_test();
+test.step(function () {
+ var client = new XMLHttpRequest();
+ client.ontimeout = function () {
+ test.step(function () {
+ assert_equals(client.readyState, 4);
+ test.done();
+ });
+ };
+ client.timeout = 5;
+ client.open("GET", "resources/delay.py?ms=20000");
+ client.send(null);
+ test.step_timeout(() => {
+ assert_unreached("ontimeout not called.");
+ }, 1000);
+});
diff --git a/test/wpt/tests/xhr/event-upload-progress-crossorigin.any.js b/test/wpt/tests/xhr/event-upload-progress-crossorigin.any.js
new file mode 100644
index 0000000..9f4c44a
--- /dev/null
+++ b/test/wpt/tests/xhr/event-upload-progress-crossorigin.any.js
@@ -0,0 +1,26 @@
+// META: title=XMLHttpRequest: upload progress event for cross-origin requests
+// META: script=/common/get-host-info.sub.js
+
+const remote = get_host_info().HTTP_REMOTE_ORIGIN + "/xhr/resources/corsenabled.py",
+ redirect = "resources/redirect.py?code=307&location=" + remote;
+
+[remote, redirect].forEach(url => {
+ async_test(test => {
+ const client = new XMLHttpRequest();
+ client.upload.onprogress = test.step_func_done()
+ client.onload = test.unreached_func()
+ client.open("POST", url)
+ client.send("On time: " + url)
+ }, "Upload events registered on time (" + url + ")");
+});
+
+[remote, redirect].forEach(url => {
+ async_test(test => {
+ const client = new XMLHttpRequest();
+ client.onload = test.step_func_done();
+ client.open("POST", url);
+ client.send("Too late: " + url);
+ client.upload.onloadstart = test.unreached_func(); // registered too late
+ client.upload.onprogress = test.unreached_func(); // registered too late
+ }, "Upload events registered too late (" + url + ")");
+});
diff --git a/test/wpt/tests/xhr/event-upload-progress.any.js b/test/wpt/tests/xhr/event-upload-progress.any.js
new file mode 100644
index 0000000..6fe6591
--- /dev/null
+++ b/test/wpt/tests/xhr/event-upload-progress.any.js
@@ -0,0 +1,30 @@
+// META: title=XMLHttpRequest: upload progress event
+// META: script=/common/get-host-info.sub.js
+
+const remote = get_host_info().HTTP_REMOTE_ORIGIN + "/xhr/resources/corsenabled.py",
+ redirect = "resources/redirect.py?code=307&location=" + remote;
+
+[remote, redirect].forEach(url => {
+ async_test(test => {
+ const client = new XMLHttpRequest();
+ const data = "On time: " + url;
+ client.upload.onprogress = test.step_func_done(e => {
+ assert_true(e.lengthComputable);
+ assert_equals(e.total, data.length);
+ });
+ client.onload = test.unreached_func();
+ client.open("POST", url);
+ client.send(data);
+ }, "Upload events registered on time (" + url + ")");
+});
+
+[remote, redirect].forEach(url => {
+ async_test(test => {
+ const client = new XMLHttpRequest();
+ client.onload = test.step_func_done();
+ client.open("POST", url);
+ client.send("Too late: " + url);
+ client.upload.onloadstart = test.unreached_func(); // registered too late
+ client.upload.onprogress = test.unreached_func(); // registered too late
+ }, "Upload events registered too late (" + url + ")");
+});
diff --git a/test/wpt/tests/xhr/firing-events-http-content-length.html b/test/wpt/tests/xhr/firing-events-http-content-length.html
new file mode 100644
index 0000000..4748ce3
--- /dev/null
+++ b/test/wpt/tests/xhr/firing-events-http-content-length.html
@@ -0,0 +1,32 @@
+<!doctype html>
+<html>
+ <head>
+ <title>ProgressEvent: firing events for HTTP with Content-Length</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ async_test(t => {
+ const xhr = new XMLHttpRequest();
+ let progressHappened = false;
+
+ xhr.onprogress = t.step_func(pe => {
+ assert_equals(pe.type, "progress");
+ assert_greater_than_equal(pe.loaded, 0, "loaded");
+ assert_true(pe.lengthComputable, "lengthComputable");
+ assert_equals(pe.total, 1300, "total");
+ progressHappened = true;
+ });
+
+ xhr.onloadend = t.step_func_done(() => {
+ assert_true(progressHappened);
+ });
+
+ xhr.open("GET", "resources/trickle.py?ms=0&count=100&specifylength=1", true);
+ xhr.send(null);
+ });
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/firing-events-http-no-content-length.html b/test/wpt/tests/xhr/firing-events-http-no-content-length.html
new file mode 100644
index 0000000..ddf7dd8
--- /dev/null
+++ b/test/wpt/tests/xhr/firing-events-http-no-content-length.html
@@ -0,0 +1,35 @@
+<!doctype html>
+<html>
+ <head>
+ <title>ProgressEvent: firing events for HTTP with no Content-Length</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ async_test(t => {
+ const xhr = new XMLHttpRequest();
+ let progressHappened = false;
+
+ xhr.onprogress = t.step_func(pe => {
+ assert_equals(pe.type, "progress");
+ assert_greater_than_equal(pe.loaded, 0, "loaded");
+ assert_false(pe.lengthComputable, "lengthComputable");
+ assert_equals(pe.total, 0, "total");
+ progressHappened = true;
+ });
+
+ // "loadstart", "error", "abort", "load" tests are out of scope.
+ // They SHOULD be tested in each spec that implement ProgressEvent.
+
+ xhr.onloadend = t.step_func_done(() => {
+ assert_true(progressHappened);
+ });
+
+ xhr.open("GET", "resources/trickle.py?ms=0&count=100", true);
+ xhr.send(null);
+ });
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/folder.txt b/test/wpt/tests/xhr/folder.txt
new file mode 100644
index 0000000..bf1a1fd
--- /dev/null
+++ b/test/wpt/tests/xhr/folder.txt
@@ -0,0 +1 @@
+top
diff --git a/test/wpt/tests/xhr/formdata.html b/test/wpt/tests/xhr/formdata.html
new file mode 100644
index 0000000..b308916
--- /dev/null
+++ b/test/wpt/tests/xhr/formdata.html
@@ -0,0 +1,90 @@
+<!doctype html>
+<html lang=en>
+<meta charset=utf-8>
+<title>XMLHttpRequest: Construct and upload FormData</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/html/semantics/forms/form-submission-0/resources/targetted-form.js"></script>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#interface-formdata" data-tested-assertations="following::P[1]" />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#dom-formdata" data-tested-assertations=".. following::P[1]" />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#dom-formdata-append" data-tested-assertations=".. following::UL[1]/LI[1] following::UL[1]/LI[2] following::UL[1]/LI[3]" />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#dom-XMLHttpRequest-send-FormData" data-tested-assertations="following::DD[1]" />
+<link rel="help" href="https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#constructing-the-form-data-set">
+
+<div id="log"></div>
+<form id="form">
+ <input type="hidden" name="key" value="value">
+</form>
+<script>
+ function do_test (name, fd, expected) {
+ var test = async_test(name);
+ test.step(function() {
+ var client = new XMLHttpRequest();
+ client.onreadystatechange = test.step_func(function () {
+ if (client.readyState !== 4) return;
+ assert_equals(client.responseText, expected);
+ test.done();
+ });
+ client.open("POST", "resources/upload.py");
+ client.send(fd);
+ });
+ }
+
+ function create_formdata () {
+ var fd = new FormData();
+ for (var i = 0; i < arguments.length; i++) {
+ fd.append.apply(fd, arguments[i]);
+ };
+ return fd;
+ }
+
+ do_test("empty formdata", new FormData(), '\n');
+ do_test("formdata with string", create_formdata(['key', 'value']), 'key=value,\n');
+ do_test("formdata with named string", create_formdata(['key', new Blob(['value'], {type: 'text/plain'}), 'kv.txt']), '\nkey=kv.txt:text/plain:5,');
+ do_test("formdata from form", new FormData(document.getElementById('form')), 'key=value,\n');
+
+ do_test("formdata with blob", create_formdata(['key', new Blob(['value'], {type: 'text/x-value'})]), '\nkey=blob:text/x-value:5,');
+ do_test("formdata with named blob", create_formdata(['key', new Blob(['value'], {type: 'text/x-value'}), 'blob.txt']), '\nkey=blob.txt:text/x-value:5,');
+
+ // If 3rd argument is given and 2nd is not a Blob, formdata.append() should throw
+ const append_test = async_test('formdata.append() should throw if value is string and file name is given'); // needs to be async just because the others above are
+ append_test.step(function(){
+ assert_throws_js(TypeError, function(){
+ create_formdata('a', 'b', 'c');
+ });
+ });
+ append_test.done();
+
+ test(() => {
+ let form = populateForm('<input name=n1 value=v1>');
+ let formDataInEvent = null;
+ form.addEventListener('formdata', e => {
+ e.formData.append('h1', 'vh1');
+ formDataInEvent = e.formData;
+ });
+ let formData = new FormData(form);
+ assert_equals(formData.get('h1'), 'vh1');
+ assert_equals(formData.get('n1'), 'v1');
+ assert_not_equals(formData, formDataInEvent,
+ '"formData" attribute should be different from the ' +
+ 'FromData object created by "new"');
+
+ formDataInEvent.append('later-key', 'later-value');
+ assert_false(formData.has('later-key'));
+ }, 'Newly created FormData contains entries added to "formData" IDL ' +
+ 'attribute of FormDataEvent.');
+
+ test(() => {
+ let form = populateForm('<input name=n11 value=v11>');
+ let counter = 0;
+ form.addEventListener('formdata', e => {
+ ++counter;
+ assert_throws_dom('InvalidStateError', () => { new FormData(e.target) });
+ });
+ new FormData(form);
+ assert_equals(counter, 1);
+
+ form.submit();
+ assert_equals(counter, 2);
+ }, '|new FormData()| in formdata event handler should throw');
+</script>
diff --git a/test/wpt/tests/xhr/formdata/append-formelement.html b/test/wpt/tests/xhr/formdata/append-formelement.html
new file mode 100644
index 0000000..72c81aa
--- /dev/null
+++ b/test/wpt/tests/xhr/formdata/append-formelement.html
@@ -0,0 +1,52 @@
+<!doctype html>
+<meta charset="utf-8">
+<title>FormData.append (with form element)
+</title>
+<link rel="help" href="https://xhr.spec.whatwg.org/#dom-formdata-append">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<form id="form"></form>
+<script>
+ test(function() {
+ var fd = new FormData(document.getElementById("form"));
+ fd.append('key', 'value1');
+ assert_equals(fd.get('key'), "value1");
+ }, 'testFormDataAppendToForm1');
+ test(function() {
+ var fd = new FormData(document.getElementById("form"));
+ fd.append('key', 'value2');
+ fd.append('key', 'value1');
+ assert_equals(fd.get('key'), "value2");
+ }, 'testFormDataAppendToForm2');
+ test(function() {
+ var fd = new FormData(document.getElementById("form"));
+ fd.append('key', undefined);
+ assert_equals(fd.get('key'), "undefined");
+ }, 'testFormDataAppendToFormUndefined1');
+ test(function() {
+ var fd = new FormData(document.getElementById("form"));
+ fd.append('key', undefined);
+ fd.append('key', 'value1');
+ assert_equals(fd.get('key'), "undefined");
+ }, 'testFormDataAppendToFormUndefined2');
+ test(function() {
+ var fd = new FormData(document.getElementById("form"));
+ fd.append('key', null);
+ assert_equals(fd.get('key'), "null");
+ }, 'testFormDataAppendToFormNull1');
+ test(function() {
+ var fd = new FormData(document.getElementById("form"));
+ fd.append('key', null);
+ fd.append('key', 'value1');
+ assert_equals(fd.get('key'), "null");
+ }, 'testFormDataAppendToFormNull2');
+ test(function() {
+ var fd = new FormData(document.getElementById("form"));
+ assert_throws_js(TypeError, () => {fd.append('name', "string", 'filename')});
+ }, 'testFormDataAppendToFormString');
+ test(function() {
+ var fd = new FormData(document.getElementById("form"));
+ assert_throws_js(TypeError, () => {fd.append('name', new URLSearchParams(), 'filename')});
+ }, 'testFormDataAppendToFormWrongPlatformObject');
+</script>
diff --git a/test/wpt/tests/xhr/formdata/append.any.js b/test/wpt/tests/xhr/formdata/append.any.js
new file mode 100644
index 0000000..fb36561
--- /dev/null
+++ b/test/wpt/tests/xhr/formdata/append.any.js
@@ -0,0 +1,37 @@
+// META: title=FormData.append
+
+ test(function() {
+ assert_equals(create_formdata(['key', 'value1']).get('key'), "value1");
+ }, 'testFormDataAppend1');
+ test(function() {
+ assert_equals(create_formdata(['key', 'value2'], ['key', 'value1']).get('key'), "value2");
+ }, 'testFormDataAppend2');
+ test(function() {
+ assert_equals(create_formdata(['key', undefined]).get('key'), "undefined");
+ }, 'testFormDataAppendUndefined1');
+ test(function() {
+ assert_equals(create_formdata(['key', undefined], ['key', 'value1']).get('key'), "undefined");
+ }, 'testFormDataAppendUndefined2');
+ test(function() {
+ assert_equals(create_formdata(['key', null]).get('key'), "null");
+ }, 'testFormDataAppendNull1');
+ test(function() {
+ assert_equals(create_formdata(['key', null], ['key', 'value1']).get('key'), "null");
+ }, 'testFormDataAppendNull2');
+ test(function() {
+ var before = new Date(new Date().getTime() - 2000); // two seconds ago, in case there's clock drift
+ var fd = create_formdata(['key', new Blob(), 'blank.txt']).get('key');
+ assert_equals(fd.name, "blank.txt");
+ assert_equals(fd.type, "");
+ assert_equals(fd.size, 0);
+ assert_greater_than_equal(fd.lastModified, before);
+ assert_less_than_equal(fd.lastModified, new Date());
+ }, 'testFormDataAppendEmptyBlob');
+
+ function create_formdata() {
+ var fd = new FormData();
+ for (var i = 0; i < arguments.length; i++) {
+ fd.append.apply(fd, arguments[i]);
+ };
+ return fd;
+ }
diff --git a/test/wpt/tests/xhr/formdata/constructor-formelement.html b/test/wpt/tests/xhr/formdata/constructor-formelement.html
new file mode 100644
index 0000000..813e1d2
--- /dev/null
+++ b/test/wpt/tests/xhr/formdata/constructor-formelement.html
@@ -0,0 +1,150 @@
+<!DOCTYPE html>
+<title>FormData: constructor (with form element)</title>
+<link rel="author" title="Domenic Denicola" href="mailto:d@domenic.me">
+<link rel="help" href="https://xhr.spec.whatwg.org/#dom-formdata">
+<link rel="help" href="https://html.spec.whatwg.org/multipage/#constructing-form-data-set">
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<form>
+ <output name="do-not-submit-me-1"></output>
+
+ <datalist>
+ <input type="text" name="do-not-submit-me-2" value="bad">
+ <select name="do-not-submit-me-3">
+ <option value="bad" selected></option>
+ </select>
+ <input type="checkbox" name="do-not-submit-me-4" checked>
+ </datalist>
+
+ <input type="text" name="do-not-submit-me-5" disabled value="bad">
+ <fieldset disabled>
+ <input type="text" name="do-not-submit-me-6" value="bad">
+ </fieldset>
+
+ <button name="do-not-submit-me-7">bad</button>
+ <input type="submit" name="do-not-submit-me-8" value="bad">
+ <input type="reset" name="do-not-submit-me-9" value="bad">
+ <input type="image" name="do-not-submit-me-10" value="bad">
+
+ <input type="checkbox" name="do-not-submit-me-11">
+ <input type="radio" name="do-not-submit-me-12">
+
+ <input type="text" value="do-not-submit-me-13">
+ <input type="text" name="" value="do-not-submit-me-14">
+
+ <object name="do-not-submit-me-15"></object>
+
+ <select name="select-1">
+ <option disabled value="do-not-submit-me-16"></option>
+ <option value="do-not-submit-me-17"></option>
+ <option disabled value="do-not-submit-me-18" selected></option>
+ </select>
+
+ <select name="select-2">
+ <option value="do-not-submit-me-19"></option>
+ <option value="submit-me-1" selected></option>
+ </select>
+
+ <select name="select-3" multiple>
+ <option value="do-not-submit-me-20"></option>
+ <option value="submit-me-2" selected></option>
+ <option value="do-not-submit-me-21"></option>
+ <option value="submit-me-3" selected></option>
+ </select>
+
+ <input type="checkbox" name="submit-me-4" value="checkbox-1" checked>
+ <input type="checkbox" name="submit-me-5" checked>
+
+ <input type="radio" name="submit-me-6" value="radio-1" checked>
+ <input type="radio" name="submit-me-7" checked>
+
+ <!-- not tested: <input type="file"> with selected files -->
+
+ <input type="file" name="file-1">
+
+ <!-- not tested: <object>s that allow form submission -->
+
+ <input type="text" name="submit-me-8" value="text-1">
+ <input type="text" name="submit-me-8" value="text-2">
+ <input type="search" name="submit-me-9" value="search-1">
+ <input type="url" name="submit-me-10" value="url-1">
+ <input type="hidden" name="submit-me-11" value="hidden-1">
+ <input type="password" name="submit-me-12" value="password-1">
+ <input type="number" name="submit-me-13" value="11">
+ <input type="range" name="submit-me-14" value="11">
+ <input type="color" name="submit-me-15" value="#123456">
+
+ <textarea name="submit-me-16">textarea value
+with linebreaks set to LF</textarea>
+
+ <!-- this generates two form data entries! -->
+ <input type="text" name="dirname-is-special" dirname="submit-me-17" value="dirname-value">
+
+ <input type="text" name="submit-me-21">
+</form>
+
+<script>
+"use strict";
+
+test(() => {
+
+ const form = document.querySelector("form");
+
+ const input = document.createElement("input");
+ input.name = "submit-me-18-\uDC01";
+ input.value = "value-\uDC01";
+ assert_equals(input.name, "submit-me-18-\uDC01", "input.name accepts unpaired surrogates");
+ assert_equals(input.value, "value-\uDC01", "input.value accepts unpaired surrogates");
+ form.appendChild(input);
+
+ const input2 = document.createElement("input");
+ input2.name = "submit-me-\r19\n";
+ input2.value = "value\n\r";
+ assert_equals(input2.name, "submit-me-\r19\n", "input.name accepts \\r and \\n");
+ assert_equals(input2.value, "value", "input.value when type=text should not contain newlines");
+ form.appendChild(input2);
+
+ const formData = new FormData(form);
+
+ const expected = [
+ ["select-2", "submit-me-1"],
+ ["select-3", ["submit-me-2", "submit-me-3"]],
+ ["submit-me-4", "checkbox-1"],
+ ["submit-me-5", "on"],
+ ["submit-me-6", "radio-1"],
+ ["submit-me-7", "on"],
+ ["submit-me-8", ["text-1", "text-2"]],
+ ["submit-me-9", "search-1"],
+ ["submit-me-10", "url-1"],
+ ["submit-me-11", "hidden-1"],
+ ["submit-me-12", "password-1"],
+ ["submit-me-13", "11"],
+ ["submit-me-14", "11"],
+ ["submit-me-15", "#123456"],
+ ["submit-me-16", "textarea value\nwith linebreaks set to LF"],
+ ["dirname-is-special", "dirname-value"],
+ ["submit-me-17", "ltr"],
+ ["submit-me-18-\uFFFD", "value-\uFFFD"],
+ ["submit-me-\r19\n", "value"],
+ ["submit-me-21", ""]
+ ];
+
+ for (const t of expected) {
+ const field = t[0];
+ const valueOrValues = t[1];
+ const values = Array.isArray(valueOrValues) ? valueOrValues : [valueOrValues];
+ assert_array_equals(formData.getAll(field), values, field);
+ }
+
+ const fileEntry = formData.getAll("file-1");
+ assert_equals(fileEntry.length, 1);
+ assert_equals(fileEntry[0], formData.get("file-1"));
+ assert_equals(fileEntry[0].constructor, File);
+ assert_equals(fileEntry[0].size, 0);
+ assert_equals(fileEntry[0].name, "");
+ assert_equals(fileEntry[0].type, "application/octet-stream");
+
+}, "test that FormData is correctly constructed from the form data set");
+</script>
diff --git a/test/wpt/tests/xhr/formdata/constructor-submitter.html b/test/wpt/tests/xhr/formdata/constructor-submitter.html
new file mode 100644
index 0000000..542357d
--- /dev/null
+++ b/test/wpt/tests/xhr/formdata/constructor-submitter.html
@@ -0,0 +1,100 @@
+<!DOCTYPE html>
+<meta charset='utf-8'>
+<link rel='help' href='https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#constructing-the-form-data-set'>
+<link ref='help' href='https://xhr.spec.whatwg.org/#dom-formdata'>
+<script src='/resources/testharness.js'></script>
+<script src='/resources/testharnessreport.js'></script>
+
+<button name=outerNamed value=GO form='myform'></button>
+<form id='myform' onsubmit='return false'>
+ <input name=n1 value=v1>
+ <button name=named value=GO></button>
+ <button id=unnamed value=unnamed></button>
+ <button form="another" name=unassociated value=unassociated></button>
+ <input type=image name=namedImage src='/media/1x1-green.png'></button>
+ <input type=image id=unnamedImage src='/media/1x1-green.png'></button>
+ <input type=image name=unactivatedImage src='/media/1x1-green.png'></button>
+ <input name=n3 value=v3>
+</form>
+
+<form id='another'>
+ <button name=unassociated2 value=unassociated></button>
+</form>
+
+<script>
+function assertFormDataEntries(formData, expectedEntries) {
+ const expectedEntryNames = expectedEntries.map((entry) => entry[0]);
+ const actualEntries = [...formData.entries()];
+ const actualEntryNames = actualEntries.map((entry) => entry[0]);
+ assert_array_equals(actualEntryNames, expectedEntryNames);
+ for (let i = 0; i < actualEntries.length; i++) {
+ assert_array_equals(actualEntries[i], expectedEntries[i]);
+ }
+}
+
+const form = document.querySelector('#myform');
+
+test(() => {
+ assertFormDataEntries(
+ new FormData(form, null),
+ [['n1', 'v1'], ['n3', 'v3']]
+ );
+}, 'FormData construction should allow a null submitter'); // the use case here is so web developers can avoid null checks, e.g. `new FormData(e.target, e.submitter)`
+
+test(() => {
+ assertFormDataEntries(new FormData(undefined, undefined), []);
+}, 'FormData construction should allow an undefined form and an undefined submitter');
+
+test(() => {
+ assertFormDataEntries(new FormData(undefined, null), []);
+}, 'FormData construction should allow an undefined form and a null submitter');
+
+test(() => {
+ assert_throws_js(TypeError, () => new FormData(form, document.querySelector('[name=n1]')));
+}, 'FormData construction should throw a TypeError if a non-null submitter is not a submit button');
+
+test(() => {
+ assert_throws_dom('NotFoundError', () => new FormData(form, document.querySelector('[name=unassociated]')));
+ assert_throws_dom('NotFoundError', () => new FormData(form, document.querySelector('[name=unassociated2]')));
+}, "FormData construction should throw a 'NotFoundError' DOMException if a non-null submitter is not owned by the form");
+
+test(() => {
+ assertFormDataEntries(
+ new FormData(form, document.querySelector('[name=named]')),
+ [['n1', 'v1'], ['named', 'GO'], ['n3', 'v3']]
+ );
+ assertFormDataEntries(
+ new FormData(form, document.querySelector('[name=outerNamed]')),
+ [['outerNamed', 'GO'], ['n1', 'v1'], ['n3', 'v3']]
+ );
+}, 'The constructed FormData object should contain an in-tree-order entry for a named submit button submitter');
+
+test(() => {
+ assertFormDataEntries(
+ new FormData(form, document.querySelector('#unnamed')),
+ [['n1', 'v1'], ['n3', 'v3']]
+ );
+}, 'The constructed FormData object should not contain an entry for an unnamed submit button submitter');
+
+test(() => {
+ const submitter1 = document.querySelector('[name=namedImage]');
+ submitter1.click();
+ const submitter2 = document.querySelector('#unnamedImage');
+ submitter2.click();
+ assertFormDataEntries(
+ new FormData(form, submitter1),
+ [['n1', 'v1'], ['namedImage.x', '0'], ['namedImage.y', '0'], ['n3', 'v3']]
+ );
+ assertFormDataEntries(
+ new FormData(form, submitter2),
+ [['n1', 'v1'], ['x', '0'], ['y', '0'], ['n3', 'v3']]
+ );
+}, 'The constructed FormData object should contain in-tree-order entries for an activated Image Button submitter');
+
+test(() => {
+ assertFormDataEntries(
+ new FormData(form, document.querySelector('[name=unactivatedImage]')),
+ [['n1', 'v1'], ['unactivatedImage.x', '0'], ['unactivatedImage.y', '0'], ['n3', 'v3']]
+ );
+}, 'The constructed FormData object should contain in-tree-order entries for an unactivated Image Button submitter');
+</script>
diff --git a/test/wpt/tests/xhr/formdata/constructor.any.js b/test/wpt/tests/xhr/formdata/constructor.any.js
new file mode 100644
index 0000000..4370453
--- /dev/null
+++ b/test/wpt/tests/xhr/formdata/constructor.any.js
@@ -0,0 +1,6 @@
+// META: title=FormData: constructor
+
+test(() => {
+ assert_throws_js(TypeError, () => { new FormData(null); });
+ assert_throws_js(TypeError, () => { new FormData("string"); });
+}, "Constructors should throw a type error");
diff --git a/test/wpt/tests/xhr/formdata/delete-formelement.html b/test/wpt/tests/xhr/formdata/delete-formelement.html
new file mode 100644
index 0000000..62862bd
--- /dev/null
+++ b/test/wpt/tests/xhr/formdata/delete-formelement.html
@@ -0,0 +1,41 @@
+<!doctype html>
+<html lang=en>
+<meta charset=utf-8>
+<title>FormData: delete (with form element)</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#dom-formdata-get" />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#dom-formdata-getall" />
+<div id="log"></div>
+<form id="form1">
+ <input type="hidden" name="key" value="value1">
+ <input type="hidden" name="key" value="value2">
+</form>
+<form id="form2">
+ <input type="hidden" name="key1" value="value1">
+ <input type="hidden" name="key2" value="value2">
+</form>
+<form id="empty-form"></form>
+<script>
+ test(function() {
+ var fd = new FormData(document.getElementById('form1'));
+ fd.delete('key');
+ assert_equals(fd.get('key'), null);
+ }, 'testFormDataDeleteFromForm');
+ test(function() {
+ var fd = new FormData(document.getElementById('form1'));
+ fd.delete('nil');
+ assert_equals(fd.get('key'), 'value1');
+ }, 'testFormDataDeleteFromFormNonExistentKey');
+ test(function() {
+ var fd = new FormData(document.getElementById('form2'));
+ fd.delete('key1');
+ assert_equals(fd.get('key1'), null);
+ assert_equals(fd.get('key2'), 'value2');
+ }, 'testFormDataDeleteFromFormOtherKey');
+ test(function() {
+ var fd = new FormData(document.getElementById('empty-form'));
+ fd.delete('key');
+ assert_equals(fd.get('key'), null);
+ }, 'testFormDataDeleteFromEmptyForm');
+</script>
diff --git a/test/wpt/tests/xhr/formdata/delete.any.js b/test/wpt/tests/xhr/formdata/delete.any.js
new file mode 100644
index 0000000..9424614
--- /dev/null
+++ b/test/wpt/tests/xhr/formdata/delete.any.js
@@ -0,0 +1,26 @@
+// META: title=FormData: delete
+
+ test(function() {
+ var fd = create_formdata(['key', 'value1'], ['key', 'value2']);
+ fd.delete('key');
+ assert_equals(fd.get('key'), null);
+ }, 'testFormDataDelete');
+ test(function() {
+ var fd = create_formdata(['key', 'value1'], ['key', 'value2']);
+ fd.delete('nil');
+ assert_equals(fd.get('key'), 'value1');
+ }, 'testFormDataDeleteNonExistentKey');
+ test(function() {
+ var fd = create_formdata(['key1', 'value1'], ['key2', 'value2']);
+ fd.delete('key1');
+ assert_equals(fd.get('key1'), null);
+ assert_equals(fd.get('key2'), 'value2');
+ }, 'testFormDataDeleteOtherKey');
+
+ function create_formdata() {
+ var fd = new FormData();
+ for (var i = 0; i < arguments.length; i++) {
+ fd.append.apply(fd, arguments[i]);
+ };
+ return fd;
+ }
diff --git a/test/wpt/tests/xhr/formdata/foreach.any.js b/test/wpt/tests/xhr/formdata/foreach.any.js
new file mode 100644
index 0000000..9fc1e2d
--- /dev/null
+++ b/test/wpt/tests/xhr/formdata/foreach.any.js
@@ -0,0 +1,56 @@
+// META: title=FormData: foreach
+
+ var fd = new FormData();
+ fd.append('n1', 'v1');
+ fd.append('n2', 'v2');
+ fd.append('n3', 'v3');
+ fd.append('n1', 'v4');
+ fd.append('n2', 'v5');
+ fd.append('n3', 'v6');
+ fd.delete('n2');
+
+ var file = new File(['hello'], "hello.txt");
+ fd.append('f1', file);
+
+ var expected_keys = ['n1', 'n3', 'n1', 'n3', 'f1'];
+ var expected_values = ['v1', 'v3', 'v4', 'v6', file];
+ test(function() {
+ var mykeys = [], myvalues = [];
+ for(var entry of fd) {
+ assert_equals(entry.length, 2,
+ 'Default iterator should yield key/value pairs');
+ mykeys.push(entry[0]);
+ myvalues.push(entry[1]);
+ }
+ assert_array_equals(mykeys, expected_keys,
+ 'Default iterator should see duplicate keys');
+ assert_array_equals(myvalues, expected_values,
+ 'Default iterator should see non-deleted values');
+ }, 'Iterator should return duplicate keys and non-deleted values');
+ test(function() {
+ var mykeys = [], myvalues = [];
+ for(var entry of fd.entries()) {
+ assert_equals(entry.length, 2,
+ 'entries() iterator should yield key/value pairs');
+ mykeys.push(entry[0]);
+ myvalues.push(entry[1]);
+ }
+ assert_array_equals(mykeys, expected_keys,
+ 'entries() iterator should see duplicate keys');
+ assert_array_equals(myvalues, expected_values,
+ 'entries() iterator should see non-deleted values');
+ }, 'Entries iterator should return duplicate keys and non-deleted values');
+ test(function() {
+ var mykeys = [];
+ for(var entry of fd.keys())
+ mykeys.push(entry);
+ assert_array_equals(mykeys, expected_keys,
+ 'keys() iterator should see duplicate keys');
+ }, 'Keys iterator should return duplicates');
+ test(function() {
+ var myvalues = [];
+ for(var entry of fd.values())
+ myvalues.push(entry);
+ assert_array_equals(myvalues, expected_values,
+ 'values() iterator should see non-deleted values');
+ }, 'Values iterator should return non-deleted values');
diff --git a/test/wpt/tests/xhr/formdata/get-formelement.html b/test/wpt/tests/xhr/formdata/get-formelement.html
new file mode 100644
index 0000000..801db6c
--- /dev/null
+++ b/test/wpt/tests/xhr/formdata/get-formelement.html
@@ -0,0 +1,34 @@
+<!doctype html>
+<html lang=en>
+<meta charset=utf-8>
+<title>FormData: get and getAll (with form element)</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#dom-formdata-get" />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#dom-formdata-getall" />
+<div id="log"></div>
+<form id="form">
+ <input type="hidden" name="key" value="value1">
+ <input type="hidden" name="key" value="value2">
+</form>
+<form id="empty-form"></form>
+<script>
+ test(function() {
+ assert_equals(new FormData(document.getElementById('form')).get('key'), "value1");
+ }, 'testFormDataGetFromForm');
+ test(function() {
+ assert_equals(new FormData(document.getElementById('form')).get('nil'), null);
+ }, 'testFormDataGetFromFormNull');
+ test(function() {
+ assert_equals(new FormData(document.getElementById('empty-form')).get('key'), null);
+ }, 'testFormDataGetFromEmptyForm');
+ test(function() {
+ assert_array_equals(new FormData(document.getElementById('form')).getAll('key'), ["value1", "value2"]);
+ }, 'testFormDataGetAllFromForm');
+ test(function() {
+ assert_array_equals(new FormData(document.getElementById('form')).getAll('nil'), []);
+ }, 'testFormDataGetAllFromFormNull');
+ test(function() {
+ assert_array_equals(new FormData(document.getElementById('empty-form')).getAll('key'), []);
+ }, 'testFormDataGetAllFromEmptyForm');
+</script>
diff --git a/test/wpt/tests/xhr/formdata/get.any.js b/test/wpt/tests/xhr/formdata/get.any.js
new file mode 100644
index 0000000..b307f1e
--- /dev/null
+++ b/test/wpt/tests/xhr/formdata/get.any.js
@@ -0,0 +1,28 @@
+// META: title=FormData: get and getAll
+
+ test(function() {
+ assert_equals(create_formdata(['key', 'value1'], ['key', 'value2']).get('key'), "value1");
+ }, 'testFormDataGet');
+ test(function() {
+ assert_equals(create_formdata(['key', 'value1'], ['key', 'value2']).get('nil'), null);
+ }, 'testFormDataGetNull1');
+ test(function() {
+ assert_equals(create_formdata().get('key'), null);
+ }, 'testFormDataGetNull2');
+ test(function() {
+ assert_array_equals(create_formdata(['key', 'value1'], ['key', 'value2']).getAll('key'), ["value1", "value2"]);
+ }, 'testFormDataGetAll');
+ test(function() {
+ assert_array_equals(create_formdata(['key', 'value1'], ['key', 'value2']).getAll('nil'), []);
+ }, 'testFormDataGetAllEmpty1');
+ test(function() {
+ assert_array_equals(create_formdata().getAll('key'), []);
+ }, 'testFormDataGetAllEmpty2');
+
+ function create_formdata() {
+ var fd = new FormData();
+ for (var i = 0; i < arguments.length; i++) {
+ fd.append.apply(fd, arguments[i]);
+ };
+ return fd;
+ }
diff --git a/test/wpt/tests/xhr/formdata/has-formelement.html b/test/wpt/tests/xhr/formdata/has-formelement.html
new file mode 100644
index 0000000..9edbad3
--- /dev/null
+++ b/test/wpt/tests/xhr/formdata/has-formelement.html
@@ -0,0 +1,25 @@
+<!doctype html>
+<html lang=en>
+<meta charset=utf-8>
+<title>FormData: has (with form element)</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#dom-formdata-get" />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#dom-formdata-getall" />
+<div id="log"></div>
+<form id="form">
+ <input type="hidden" name="key" value="value1">
+ <input type="hidden" name="key" value="value2">
+</form>
+<form id="empty-form"></form>
+<script>
+ test(function() {
+ assert_equals(new FormData(document.getElementById('form')).has('key'), true);
+ }, 'testFormDataHasFromForm');
+ test(function() {
+ assert_equals(new FormData(document.getElementById('form')).has('nil'), false);
+ }, 'testFormDataHasFromFormNull');
+ test(function() {
+ assert_equals(new FormData(document.getElementById('empty-form')).has('key'), false);
+ }, 'testFormDataHasFromEmptyForm');
+</script>
diff --git a/test/wpt/tests/xhr/formdata/has.any.js b/test/wpt/tests/xhr/formdata/has.any.js
new file mode 100644
index 0000000..2c1a3fd
--- /dev/null
+++ b/test/wpt/tests/xhr/formdata/has.any.js
@@ -0,0 +1,19 @@
+// META: title=FormData: has
+
+ test(function() {
+ assert_equals(create_formdata(['key', 'value1'], ['key', 'value2']).has('key'), true);
+ }, 'testFormDataHas');
+ test(function() {
+ assert_equals(create_formdata(['key', 'value1'], ['key', 'value2']).has('nil'), false);
+ }, 'testFormDataHasEmpty1');
+ test(function() {
+ assert_equals(create_formdata().has('key'), false);
+ }, 'testFormDataHasEmpty2');
+
+ function create_formdata() {
+ var fd = new FormData();
+ for (var i = 0; i < arguments.length; i++) {
+ fd.append.apply(fd, arguments[i]);
+ };
+ return fd;
+ }
diff --git a/test/wpt/tests/xhr/formdata/iteration.any.js b/test/wpt/tests/xhr/formdata/iteration.any.js
new file mode 100644
index 0000000..1633fd9
--- /dev/null
+++ b/test/wpt/tests/xhr/formdata/iteration.any.js
@@ -0,0 +1,65 @@
+// META: title=FormData: changes to entry list during iteration
+
+// These are tests for next()'s behavior as specified in
+// https://webidl.spec.whatwg.org/#es-iterator-prototype-object
+
+"use strict";
+
+function createFormData(input) {
+ const formData = new FormData();
+
+ for (const [name, value] of input) {
+ formData.append(name, value);
+ }
+
+ return formData;
+}
+
+test(() => {
+ const formData = createFormData([["foo", "0"],
+ ["baz", "1"],
+ ["BAR", "2"]]);
+ const actualKeys = [];
+ const actualValues = [];
+ for (const [name, value] of formData) {
+ actualKeys.push(name);
+ actualValues.push(value);
+ formData.delete("baz");
+ }
+ assert_array_equals(actualKeys, ["foo", "BAR"]);
+ assert_array_equals(actualValues, ["0", "2"]);
+}, "Iteration skips elements removed while iterating");
+
+test(() => {
+ const formData = createFormData([["foo", "0"],
+ ["baz", "1"],
+ ["BAR", "2"],
+ ["quux", "3"]]);
+ const actualKeys = [];
+ const actualValues = [];
+ for (const [name, value] of formData) {
+ actualKeys.push(name);
+ actualValues.push(value);
+ if (name === "baz")
+ formData.delete("foo");
+ }
+ assert_array_equals(actualKeys, ["foo", "baz", "quux"]);
+ assert_array_equals(actualValues, ["0", "1", "3"]);
+}, "Removing elements already iterated over causes an element to be skipped during iteration");
+
+test(() => {
+ const formData = createFormData([["foo", "0"],
+ ["baz", "1"],
+ ["BAR", "2"],
+ ["quux", "3"]]);
+ const actualKeys = [];
+ const actualValues = [];
+ for (const [name, value] of formData) {
+ actualKeys.push(name);
+ actualValues.push(value);
+ if (name === "baz")
+ formData.append("X-yZ", "4");
+ }
+ assert_array_equals(actualKeys, ["foo", "baz", "BAR", "quux", "X-yZ"]);
+ assert_array_equals(actualValues, ["0", "1", "2", "3", "4"]);
+}, "Appending a value pair during iteration causes it to be reached during iteration");
diff --git a/test/wpt/tests/xhr/formdata/set-blob.any.js b/test/wpt/tests/xhr/formdata/set-blob.any.js
new file mode 100644
index 0000000..01946fa
--- /dev/null
+++ b/test/wpt/tests/xhr/formdata/set-blob.any.js
@@ -0,0 +1,61 @@
+// META: title=formData.set(blob) and formData.set(file)
+
+"use strict";
+
+const formData = new FormData();
+
+test(() => {
+ const value = new Blob();
+ formData.set("blob-1", value);
+ const blob1 = formData.get("blob-1");
+ assert_not_equals(blob1, value);
+ assert_equals(blob1.constructor.name, "File");
+ assert_equals(blob1.name, "blob");
+ assert_equals(blob1.type, "");
+ assert_equals(formData.get("blob-1") === formData.get("blob-1"), true, "should return the same value when get the same blob entry from FormData");
+ assert_less_than(Math.abs(blob1.lastModified - Date.now()), 200, "lastModified should be now");
+}, "blob without type");
+
+test(() => {
+ const value = new Blob([], { type: "text/plain" });
+ formData.set("blob-2", value);
+ const blob2 = formData.get("blob-2");
+ assert_not_equals(blob2, value);
+ assert_equals(blob2.constructor.name, "File");
+ assert_equals(blob2.name, "blob");
+ assert_equals(blob2.type, "text/plain");
+ assert_less_than(Math.abs(blob2.lastModified - Date.now()), 200, "lastModified should be now");
+}, "blob with type");
+
+test(() => {
+ const value = new Blob();
+ formData.set("blob-3", value, "custom name");
+ const blob3 = formData.get("blob-3");
+ assert_not_equals(blob3, value);
+ assert_equals(blob3.constructor.name, "File");
+ assert_equals(blob3.name, "custom name");
+ assert_equals(blob3.type, "");
+ assert_less_than(Math.abs(blob3.lastModified - Date.now()), 200, "lastModified should be now");
+}, "blob with custom name");
+
+test(() => {
+ const value = new File([], "name");
+ formData.set("file-1", value);
+ const file1 = formData.get("file-1");
+ assert_equals(file1, value);
+ assert_equals(file1.constructor.name, "File");
+ assert_equals(file1.name, "name");
+ assert_equals(file1.type, "");
+ assert_less_than(Math.abs(file1.lastModified - Date.now()), 200, "lastModified should be now");
+}, "file without lastModified or custom name");
+
+test(() => {
+ const value = new File([], "name", { lastModified: 123 });
+ formData.set("file-2", value, "custom name");
+ const file2 = formData.get("file-2");
+ assert_not_equals(file2, value);
+ assert_equals(file2.constructor.name, "File");
+ assert_equals(file2.name, "custom name");
+ assert_equals(file2.type, "");
+ assert_equals(file2.lastModified, 123, "lastModified should be 123");
+}, "file with lastModified and custom name");
diff --git a/test/wpt/tests/xhr/formdata/set-formelement.html b/test/wpt/tests/xhr/formdata/set-formelement.html
new file mode 100644
index 0000000..d3213f8
--- /dev/null
+++ b/test/wpt/tests/xhr/formdata/set-formelement.html
@@ -0,0 +1,51 @@
+<!doctype html>
+<meta charset="utf-8">
+<title>FormData: set (with form element)</title>
+<link rel="help" href="https://xhr.spec.whatwg.org/#dom-formdata-set">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<form id="form"></form>
+<script>
+ test(function() {
+ var fd = new FormData(document.getElementById("form"));
+ fd.set('key', 'value1');
+ assert_equals(fd.get('key'), "value1");
+ }, 'testFormDataSetToForm1');
+ test(function() {
+ var fd = new FormData(document.getElementById("form"));
+ fd.set('key', 'value2');
+ fd.set('key', 'value1');
+ assert_equals(fd.get('key'), "value1");
+ }, 'testFormDataSetToForm2');
+ test(function() {
+ var fd = new FormData(document.getElementById("form"));
+ fd.set('key', undefined);
+ assert_equals(fd.get('key'), "undefined");
+ }, 'testFormDataSetToFormUndefined1');
+ test(function() {
+ var fd = new FormData(document.getElementById("form"));
+ fd.set('key', undefined);
+ fd.set('key', 'value1');
+ assert_equals(fd.get('key'), "value1");
+ }, 'testFormDataSetToFormUndefined2');
+ test(function() {
+ var fd = new FormData(document.getElementById("form"));
+ fd.set('key', null);
+ assert_equals(fd.get('key'), "null");
+ }, 'testFormDataSetToFormNull1');
+ test(function() {
+ var fd = new FormData(document.getElementById("form"));
+ fd.set('key', null);
+ fd.set('key', 'value1');
+ assert_equals(fd.get('key'), "value1");
+ }, 'testFormDataSetToFormNull2');
+ test(function() {
+ var fd = new FormData(document.getElementById("form"));
+ assert_throws_js(TypeError, () => {fd.set('name', "string", 'filename')});
+ }, 'testFormDataSetToFormString');
+ test(function() {
+ var fd = new FormData(document.getElementById("form"));
+ assert_throws_js(TypeError, () => {fd.set('name', new URLSearchParams(), 'filename')});
+ }, 'testFormDataSetToFormWrongPlatformObject');
+</script>
diff --git a/test/wpt/tests/xhr/formdata/set.any.js b/test/wpt/tests/xhr/formdata/set.any.js
new file mode 100644
index 0000000..734e55b
--- /dev/null
+++ b/test/wpt/tests/xhr/formdata/set.any.js
@@ -0,0 +1,36 @@
+// META: title=FormData: set
+
+ test(function() {
+ assert_equals(create_formdata(['key', 'value1']).get('key'), "value1");
+ }, 'testFormDataSet1');
+ test(function() {
+ assert_equals(create_formdata(['key', 'value2'], ['key', 'value1']).get('key'), "value1");
+ }, 'testFormDataSet2');
+ test(function() {
+ assert_equals(create_formdata(['key', undefined]).get('key'), "undefined");
+ }, 'testFormDataSetUndefined1');
+ test(function() {
+ assert_equals(create_formdata(['key', undefined], ['key', 'value1']).get('key'), "value1");
+ }, 'testFormDataSetUndefined2');
+ test(function() {
+ assert_equals(create_formdata(['key', null]).get('key'), "null");
+ }, 'testFormDataSetNull1');
+ test(function() {
+ assert_equals(create_formdata(['key', null], ['key', 'value1']).get('key'), "value1");
+ }, 'testFormDataSetNull2');
+ test(function() {
+ var fd = new FormData();
+ fd.set('key', new Blob([]), 'blank.txt');
+ var file = fd.get('key');
+
+ assert_true(file instanceof File);
+ assert_equals(file.name, 'blank.txt');
+ }, 'testFormDataSetEmptyBlob');
+
+ function create_formdata() {
+ var fd = new FormData();
+ for (var i = 0; i < arguments.length; i++) {
+ fd.set.apply(fd, arguments[i]);
+ };
+ return fd;
+ }
diff --git a/test/wpt/tests/xhr/getallresponseheaders-cookies.htm b/test/wpt/tests/xhr/getallresponseheaders-cookies.htm
new file mode 100644
index 0000000..2cd8098
--- /dev/null
+++ b/test/wpt/tests/xhr/getallresponseheaders-cookies.htm
@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>XMLHttpRequest: getAllResponseHeaders() excludes cookies</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#dom-xmlhttprequest-getallresponseheaders" data-tested-assertations="/following::OL[1]/LI[1] /following::OL[1]/LI[3]" />
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ var test = async_test()
+ test.step(function() {
+ var client = new XMLHttpRequest()
+ assert_equals(client.getAllResponseHeaders(), "")
+ client.onreadystatechange = function() {
+ test.step(function() {
+ var headers = client.getAllResponseHeaders().toLowerCase()
+ if(client.readyState == 1) {
+ assert_equals(headers, "")
+ }
+ if(client.readyState > 1) {
+ assert_true(headers.indexOf("\r\n") != -1, "carriage return")
+ assert_true(headers.indexOf("content-type") != -1, "content-type")
+ assert_true(headers.indexOf("x-custom-header") != -1, "x-custom-header")
+ assert_false(headers.indexOf("set-cookie") != -1, "set-cookie")
+ assert_false(headers.indexOf("set-cookie2") != -1, "set-cookie2")
+ }
+ if(client.readyState == 4)
+ test.done()
+ })
+ }
+ client.open("GET", "resources/headers.py")
+ client.send(null)
+ })
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/getallresponseheaders-status.htm b/test/wpt/tests/xhr/getallresponseheaders-status.htm
new file mode 100644
index 0000000..a02ab45
--- /dev/null
+++ b/test/wpt/tests/xhr/getallresponseheaders-status.htm
@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>XMLHttpRequest: getAllResponseHeaders() excludes status</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#dom-xmlhttprequest-getallresponseheaders" data-tested-assertations="/following::OL[1]/LI[1] /following::OL[1]/LI[3]" />
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ async_test(function() {
+ var client = new XMLHttpRequest()
+ assert_equals(client.getAllResponseHeaders(), "")
+
+ client.onreadystatechange = this.step_func(function() {
+ var headers = client.getAllResponseHeaders().toLowerCase()
+ if(client.readyState == 1) {
+ assert_equals(headers, "")
+ }
+ if(client.readyState > 1) {
+ assert_false(headers.indexOf("200 ok") != -1)
+ assert_false(headers.indexOf("http/1.") != -1)
+ }
+ if(client.readyState == 4)
+ this.done()
+ })
+ client.open("GET", "resources/headers.py")
+ client.send(null)
+ })
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/getallresponseheaders.htm b/test/wpt/tests/xhr/getallresponseheaders.htm
new file mode 100644
index 0000000..759d6b6
--- /dev/null
+++ b/test/wpt/tests/xhr/getallresponseheaders.htm
@@ -0,0 +1,35 @@
+<!doctype html>
+<title>XMLHttpRequest: getAllResponseHeaders()</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<div id="log"></div>
+<script>
+async_test((t) => {
+ const client = new XMLHttpRequest()
+ client.onload = t.step_func_done(() => {
+ assert_equals(client.getAllResponseHeaders(), "also-here: Mr. PB\r\newok: lego\r\nfoo-test: 1, 2\r\n__custom: token\r\n")
+ })
+ client.onerror = t.unreached_func("unexpected error")
+ client.open("GET", "resources/headers.asis")
+ client.send(null)
+});
+
+[
+ ["content-length", "0", "header-content-length"],
+ ["content-length", "0, 0", "header-content-length-twice"],
+ ["double-trouble", ", ", "headers-double-empty"],
+ ["foo-test", "1, 2, 3", "headers-basic"],
+ ["heya", ", \u000B\u000C, 1, , , 2", "headers-some-are-empty"],
+ ["www-authenticate", "1, 2, 3, 4", "headers-www-authenticate"],
+].forEach(testValues => {
+ async_test(t => {
+ const client = new XMLHttpRequest();
+ client.onload = t.step_func_done(() => {
+ assert_equals(client.getAllResponseHeaders(), testValues[0] + ": " + testValues[1] + "\r\n");
+ });
+ client.onerror = t.unreached_func("unexpected error");
+ client.open("GET", "resources/" + testValues[2] + ".asis");
+ client.send();
+ });
+});
+</script>
diff --git a/test/wpt/tests/xhr/getresponseheader-case-insensitive.htm b/test/wpt/tests/xhr/getresponseheader-case-insensitive.htm
new file mode 100644
index 0000000..6a96149
--- /dev/null
+++ b/test/wpt/tests/xhr/getresponseheader-case-insensitive.htm
@@ -0,0 +1,34 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>XMLHttpRequest: getResponseHeader() case-insensitive matching</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#dom-xmlhttprequest-getresponseheader">
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ var test = async_test()
+ test.step(function() {
+ var client = new XMLHttpRequest()
+ client.onreadystatechange = function() {
+ test.step(function() {
+ if(client.readyState == 4) {
+ assert_equals(client.getResponseHeader("x-custom-header"), "test")
+ assert_equals(client.getResponseHeader("X-Custom-Header"), "test")
+ assert_equals(client.getResponseHeader("X-CUSTOM-HEADER"), "test")
+ assert_equals(client.getResponseHeader("X-custom-HEADER"), "test")
+ assert_equals(client.getResponseHeader("X-CUSTOM-header-COMMA"), "1, 2")
+ assert_equals(client.getResponseHeader("X-CUSTOM-no-such-header-in-response"), null)
+ assert_equals(client.getResponseHeader("CONTENT-TYPE"), "text/plain")
+ test.done()
+ }
+ })
+ }
+ client.open("GET", "resources/headers.py")
+ client.send(null)
+ })
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/getresponseheader-chunked-trailer.htm b/test/wpt/tests/xhr/getresponseheader-chunked-trailer.htm
new file mode 100644
index 0000000..0659a3a
--- /dev/null
+++ b/test/wpt/tests/xhr/getresponseheader-chunked-trailer.htm
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>XMLHttpRequest: getResponseHeader() and HTTP trailer</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#dom-xmlhttprequest-getresponseheader" data-tested-assertations="/following::OL[1]/LI[4] /following::OL[1]/LI[5] /following::OL[1]/LI[6]" />
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ var test = async_test()
+ test.step(function() {
+ var client = new XMLHttpRequest()
+ client.onreadystatechange = function() {
+ test.step(function() {
+ if(client.readyState == 4) {
+ assert_equals(client.getResponseHeader('Trailer'), 'X-Test-Me')
+ assert_equals(client.getResponseHeader('X-Test-Me'), null)
+ assert_equals(client.getAllResponseHeaders().indexOf('Trailer header value'), -1)
+ assert_regexp_match(client.getAllResponseHeaders(), /trailer:\sX-Test-Me/)
+ assert_equals(client.responseText, "First chunk\r\nSecond chunk\r\nYet another (third) chunk\r\nYet another (fourth) chunk\r\n")
+ test.done()
+ }
+ })
+ }
+ client.open("GET", "resources/chunked.py")
+ client.send(null)
+ })
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/getresponseheader-cookies-and-more.htm b/test/wpt/tests/xhr/getresponseheader-cookies-and-more.htm
new file mode 100644
index 0000000..053fe44
--- /dev/null
+++ b/test/wpt/tests/xhr/getresponseheader-cookies-and-more.htm
@@ -0,0 +1,36 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>XMLHttpRequest: getResponseHeader() custom/non-existent headers and cookies</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#dom-xmlhttprequest-getresponseheader" data-tested-assertations="following::OL[1]/LI[3] following::OL[1]/LI[5] following::OL[1]/LI[6]" />
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ var test = async_test()
+ test.step(function() {
+ var client = new XMLHttpRequest()
+ client.onreadystatechange = function() {
+ test.step(function() {
+ if(client.readyState == 1) {
+ assert_equals(client.getResponseHeader("x-custom-header"), null)
+ }
+ if(client.readyState > 1) {
+ assert_equals(client.getResponseHeader("x-custom-header"), "test")
+ assert_equals(client.getResponseHeader("x-custom-header-empty"), "")
+ assert_equals(client.getResponseHeader("set-cookie"), null)
+ assert_equals(client.getResponseHeader("set-cookie2"), null)
+ assert_equals(client.getResponseHeader("x-non-existent-header"), null)
+ }
+ if(client.readyState == 4)
+ test.done()
+ })
+ }
+ client.open("GET", "resources/headers.py")
+ client.send(null)
+ })
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/getresponseheader-error-state.htm b/test/wpt/tests/xhr/getresponseheader-error-state.htm
new file mode 100644
index 0000000..c9695fd
--- /dev/null
+++ b/test/wpt/tests/xhr/getresponseheader-error-state.htm
@@ -0,0 +1,36 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>XMLHttpRequest: getResponseHeader() in error state (failing cross-origin test)</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#dom-xmlhttprequest-getresponseheader" data-tested-assertations="following::OL[1]/LI[2]" />
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ var test = async_test()
+ test.step(function() {
+
+ var client = new XMLHttpRequest()
+ client.onreadystatechange = function() {
+ test.step(function() {
+ if(client.readyState == 1) {
+ assert_equals(client.getResponseHeader("x-custom-header"), null)
+ }
+ if(client.readyState > 1) {
+ assert_equals(client.getResponseHeader("x-custom-header"), null)
+ }
+ if(client.readyState == 4){
+ assert_equals(client.getResponseHeader("x-custom-header"), null)
+ test.done()
+ }
+ })
+ }
+ var url = location.protocol + "//" + 'www1.' + location.host + (location.pathname.replace(/getresponseheader-error-state\.htm/, 'resources/nocors/folder.txt'))
+ client.open("GET", url)
+ client.send(null)
+ })
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/getresponseheader-server-date.htm b/test/wpt/tests/xhr/getresponseheader-server-date.htm
new file mode 100644
index 0000000..409bc35
--- /dev/null
+++ b/test/wpt/tests/xhr/getresponseheader-server-date.htm
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>XMLHttpRequest: getResponseHeader() server and date</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#dom-xmlhttprequest-getresponseheader" data-tested-assertations="/following::OL[1]/LI[4] /following::OL[1]/LI[5] /following::OL[1]/LI[6]" />
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ var test = async_test()
+ test.step(function() {
+ var client = new XMLHttpRequest()
+ client.onreadystatechange = function() {
+ test.step(function() {
+ if(client.readyState == 4) {
+ assert_true(client.getResponseHeader("Server") != null)
+ assert_true(client.getResponseHeader("Date") != null)
+ test.done()
+ }
+ })
+ }
+ client.open("GET", "resources/headers.py")
+ client.send(null)
+ })
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/getresponseheader-special-characters.htm b/test/wpt/tests/xhr/getresponseheader-special-characters.htm
new file mode 100644
index 0000000..980f848
--- /dev/null
+++ b/test/wpt/tests/xhr/getresponseheader-special-characters.htm
@@ -0,0 +1,34 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>XMLHttpRequest: getResponseHeader() funny characters</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#dom-xmlhttprequest-getresponseheader" data-tested-assertations="/following::OL[1]/LI[5] /following::OL[1]/LI[6]" />
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ var test = async_test()
+ test.step(function() {
+ var client = new XMLHttpRequest()
+ client.onreadystatechange = function() {
+ test.step(function() {
+ if(client.readyState == 4) {
+ assert_equals(client.getResponseHeader("x-custom-header "), null)
+ assert_equals(client.getResponseHeader(" x-custom-header"), null)
+ assert_equals(client.getResponseHeader("x-custom-header-bytes"), "\xE2\x80\xA6")
+ assert_equals(client.getResponseHeader("x¾"), null)
+ assert_equals(client.getResponseHeader("x-custom-header\n"), null)
+ assert_equals(client.getResponseHeader("\nx-custom-header"), null)
+ assert_equals(client.getResponseHeader("x-custom-header:"), null)
+ test.done()
+ }
+ })
+ }
+ client.open("GET", "resources/headers.py")
+ client.send(null)
+ })
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/getresponseheader-unsent-opened-state.htm b/test/wpt/tests/xhr/getresponseheader-unsent-opened-state.htm
new file mode 100644
index 0000000..e3bc272
--- /dev/null
+++ b/test/wpt/tests/xhr/getresponseheader-unsent-opened-state.htm
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>XMLHttpRequest: getResponseHeader() in unsent, opened states</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#dom-xmlhttprequest-getresponseheader" data-tested-assertations="/following::OL[1]/LI[1]" />
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ var test = async_test()
+ test.step(function() {
+ var client = new XMLHttpRequest()
+ assert_equals(client.getResponseHeader("x-custom-header"), null)
+ client.onreadystatechange = function() {
+ test.step(function() {
+ if(client.readyState < 2) {
+ assert_equals(client.getResponseHeader("x-custom-header"), null)
+ assert_equals(client.getResponseHeader("CONTENT-TYPE"), null)
+ test.done()
+ }
+ })
+ }
+ client.open("GET", "resources/headers.py")
+ assert_equals(client.getResponseHeader("x-custom-header"), null)
+ assert_equals(client.getResponseHeader("Date"), null)
+ client.send(null)
+ })
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/getresponseheader.any.js b/test/wpt/tests/xhr/getresponseheader.any.js
new file mode 100644
index 0000000..6eeccd0
--- /dev/null
+++ b/test/wpt/tests/xhr/getresponseheader.any.js
@@ -0,0 +1,18 @@
+[
+ ["content-length", "0", "header-content-length"],
+ ["content-length", "0, 0", "header-content-length-twice"],
+ ["double-trouble", ", ", "headers-double-empty"],
+ ["foo-test", "1, 2, 3", "headers-basic"],
+ ["heya", ", \u000B\u000C, 1, , , 2", "headers-some-are-empty"],
+ ["www-authenticate", "1, 2, 3, 4", "headers-www-authenticate"],
+].forEach(testValues => {
+ async_test(t => {
+ const client = new XMLHttpRequest();
+ client.onload = t.step_func_done(() => {
+ assert_equals(client.getResponseHeader(testValues[0]), testValues[1]);
+ });
+ client.onerror = t.unreached_func("unexpected error");
+ client.open("GET", "resources/" + testValues[2] + ".asis");
+ client.send();
+ }, "getResponseHeader('" + testValues[0] + "') expects " + testValues[1]);
+});
diff --git a/test/wpt/tests/xhr/header-user-agent-async.htm b/test/wpt/tests/xhr/header-user-agent-async.htm
new file mode 100644
index 0000000..8c1d0b6
--- /dev/null
+++ b/test/wpt/tests/xhr/header-user-agent-async.htm
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Test that async requests (both OPTIONS preflight and regular) are sent with the User-Agent header</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/common/get-host-info.sub.js"></script>
+</head>
+<body>
+<script type="text/javascript">
+ async_test((test) => {
+ let xhr = new XMLHttpRequest;
+ xhr.open("GET", get_host_info().HTTP_REMOTE_ORIGIN + "/xhr/resources/header-user-agent.py");
+ xhr.setRequestHeader("x-test", "foobar");
+
+ xhr.onerror = test.unreached_func("Unexpected error");
+
+ xhr.onload = test.step_func_done(() => {
+ assert_equals(xhr.responseText, "PASS");
+ });
+
+ xhr.send();
+ }, "Async request has User-Agent header");
+</script>
+</body>
+</html>
diff --git a/test/wpt/tests/xhr/header-user-agent-sync.htm b/test/wpt/tests/xhr/header-user-agent-sync.htm
new file mode 100644
index 0000000..d88aac2
--- /dev/null
+++ b/test/wpt/tests/xhr/header-user-agent-sync.htm
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Test that sync requests (both OPTIONS preflight and regular) are sent with the User-Agent header</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/common/get-host-info.sub.js"></script>
+</head>
+<body>
+<script type="text/javascript">
+ test(function() {
+ let xhr = new XMLHttpRequest;
+ xhr.open("post", get_host_info().HTTP_REMOTE_ORIGIN + "/xhr/resources/header-user-agent.py", false);
+ xhr.setRequestHeader("x-test", "foobar");
+ xhr.send();
+ assert_equals(xhr.responseText, "PASS");
+ }, "Sync request has User-Agent header");
+</script>
+</body>
+</html>
diff --git a/test/wpt/tests/xhr/headers-normalize-response.htm b/test/wpt/tests/xhr/headers-normalize-response.htm
new file mode 100644
index 0000000..3d472f6
--- /dev/null
+++ b/test/wpt/tests/xhr/headers-normalize-response.htm
@@ -0,0 +1,43 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Whitespace and null in header values</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<div id=log></div>
+<script>
+function error(val) {
+ test(() => {
+ const client = new XMLHttpRequest();
+ client.open("GET", "resources/parse-headers.py?my-custom-header="+encodeURIComponent(val), false);
+ assert_throws_dom("NetworkError", () => client.send());
+ }, "Header value: " + val.replace("\0", "\\0"));
+}
+
+function matchHeaderValue(val) {
+ test(function () {
+ var client = new XMLHttpRequest();
+ var trimmed = val.trim();
+ client.open("GET", "resources/parse-headers.py?my-custom-header="+encodeURIComponent(val), false);
+ client.send();
+ var r = client.getResponseHeader("My-Custom-Header");
+
+ assert_equals(r, trimmed);
+ }, "Header value: " + val.replace(/\t/g, "[tab]").replace(/ /g, "_"));
+}
+
+error("hello world\0");
+error("\0hello world");
+error("hello\0world");
+matchHeaderValue(" hello world");
+matchHeaderValue("hello world ");
+matchHeaderValue(" hello world ");
+matchHeaderValue("\thello world");
+matchHeaderValue("hello world\t");
+matchHeaderValue("\thello world\t");
+matchHeaderValue("hello world");
+matchHeaderValue("hello\tworld");
+error("\0");
+matchHeaderValue(" ");
+matchHeaderValue("\t");
+matchHeaderValue("");
+</script>
diff --git a/test/wpt/tests/xhr/historical.html b/test/wpt/tests/xhr/historical.html
new file mode 100644
index 0000000..4af3da9
--- /dev/null
+++ b/test/wpt/tests/xhr/historical.html
@@ -0,0 +1,15 @@
+<!doctype html>
+<meta charset="utf-8">
+<title>Historical features</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+["moz-blob", "moz-chunked-text", "moz-chunked-arraybuffer"].forEach(function(rt) {
+ test(function() {
+ var xhr = new XMLHttpRequest();
+ xhr.responseType = rt;
+ assert_equals(xhr.responseType, "");
+ }, "Support for responseType = " + rt);
+});
+</script>
diff --git a/test/wpt/tests/xhr/idlharness.any.js b/test/wpt/tests/xhr/idlharness.any.js
new file mode 100644
index 0000000..2e0e0c8
--- /dev/null
+++ b/test/wpt/tests/xhr/idlharness.any.js
@@ -0,0 +1,28 @@
+// META: global=window,dedicatedworker,sharedworker
+// META: script=/resources/WebIDLParser.js
+// META: script=/resources/idlharness.js
+// META: timeout=long
+
+idl_test(
+ ['xhr'],
+ ['dom', 'html'],
+ idl_array => {
+ idl_array.add_objects({
+ XMLHttpRequest: ['new XMLHttpRequest()'],
+ XMLHttpRequestUpload: ['(new XMLHttpRequest()).upload'],
+ FormData: ['new FormData()'],
+ ProgressEvent: ['new ProgressEvent("type")'],
+ });
+ if (self.Window) {
+ self.form = document.createElement('form');
+ self.submitter = document.createElement('button');
+ self.form.appendChild(self.submitter);
+ idl_array.add_objects({
+ FormData: [
+ 'new FormData(form)',
+ 'new FormData(form, submitter)'
+ ],
+ });
+ }
+ }
+);
diff --git a/test/wpt/tests/xhr/json.any.js b/test/wpt/tests/xhr/json.any.js
new file mode 100644
index 0000000..0f49ff5
--- /dev/null
+++ b/test/wpt/tests/xhr/json.any.js
@@ -0,0 +1,23 @@
+// See also /fetch/api/response/json.any.js
+
+async_test(t => {
+ const xhr = new XMLHttpRequest();
+ xhr.responseType = "json";
+ xhr.open("GET", `data:,\uFEFF{ "b": 1, "a": 2, "b": 3 }`);
+ xhr.send();
+ xhr.onload = t.step_func_done(() => {
+ assert_array_equals(Object.keys(xhr.response), ["b", "a"]);
+ assert_equals(xhr.response.a, 2);
+ assert_equals(xhr.response.b, 3);
+ });
+}, "Ensure the correct JSON parser is used");
+
+async_test(t => {
+ const client = new XMLHttpRequest();
+ client.responseType = 'json';
+ client.open("GET", "resources/utf16-bom.json");
+ client.send();
+ client.onload = t.step_func_done(() => {
+ assert_equals(client.response, null);
+ });
+}, "Ensure UTF-16 results in an error");
diff --git a/test/wpt/tests/xhr/loadstart-and-state.html b/test/wpt/tests/xhr/loadstart-and-state.html
new file mode 100644
index 0000000..8b344a5
--- /dev/null
+++ b/test/wpt/tests/xhr/loadstart-and-state.html
@@ -0,0 +1,40 @@
+<!doctype html>
+<title>XMLHttpRequest: loadstart event corner cases</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<div id=log></div>
+<script>
+async_test(t => {
+ const client = new XMLHttpRequest
+ client.onloadstart = t.step_func(() => {
+ assert_throws_dom("InvalidStateError", () => client.setRequestHeader("General", "Organa"))
+ assert_throws_dom("InvalidStateError", () => client.withCredentials = true)
+ assert_throws_dom("InvalidStateError", () => client.send())
+ client.onloadstart = null
+ client.open("GET", "data:,BB-8")
+ client.send()
+ })
+ client.onload = t.step_func_done(() => {
+ assert_equals(client.responseText, "BB-8")
+ })
+ client.open("GET", "data:,R2-D2")
+ client.send()
+}, "open() during loadstart")
+
+async_test(t => {
+ const client = new XMLHttpRequest
+ let abortFired = false
+ client.onloadstart = t.step_func_done(() => {
+ assert_equals(client.readyState, 1)
+ client.abort()
+ assert_true(abortFired)
+ assert_equals(client.readyState, 0)
+ })
+ client.onabort = t.step_func(() => {
+ abortFired = true
+ assert_equals(client.readyState, 4)
+ })
+ client.open("GET", "data:,K-2SO")
+ client.send()
+}, "abort() during loadstart")
+</script>
diff --git a/test/wpt/tests/xhr/open-after-abort.htm b/test/wpt/tests/xhr/open-after-abort.htm
new file mode 100644
index 0000000..c9ef6e7
--- /dev/null
+++ b/test/wpt/tests/xhr/open-after-abort.htm
@@ -0,0 +1,77 @@
+<!doctype html>
+<html>
+ <head>
+ <title>XMLHttpRequest: open() after abort()</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-open()-method" data-tested-assertations="following::ol/li[15] following::ol/li[15]/ol/li[1] following::ol/li[15]/ol/li[2]" />
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ test(function(t) {
+ var client = new XMLHttpRequest(),
+ result = [],
+ expected = [
+ 'readystatechange', 0, 1, // open()
+ 'readystatechange', 2, 4, // abort()
+ 'abort', 2, 4, // abort()
+ 'loadend', 2, 4, // abort()
+ 'readystatechange', 3, 1, // open()
+ ]
+
+ var state = 0
+
+ client.onreadystatechange = t.step_func(function() {
+ result.push('readystatechange', state, client.readyState)
+ })
+ client.onabort = t.step_func(function() {
+ // abort event must be fired synchronously from abort().
+ assert_equals(state, 2)
+
+ // readystatechange should be fired before abort.
+ assert_array_equals(result, [
+ 'readystatechange', 0, 1, // open()
+ 'readystatechange', 2, 4, // abort()
+ ])
+
+ // readyState should be set to unsent (0) at the very end of abort(),
+ // after this (and onloadend) is called.
+ assert_equals(client.readyState, 4)
+
+ result.push('abort', state, client.readyState)
+ })
+ client.onloadend = t.step_func(function() {
+ // abort event must be fired synchronously from abort().
+ assert_equals(state, 2)
+
+ // readystatechange should be fired before abort.
+ assert_array_equals(result, [
+ 'readystatechange', 0, 1, // open()
+ 'readystatechange', 2, 4, // abort()
+ 'abort', 2, 4, // abort()
+ ])
+
+ // readyState should be set to unsent (0) at the very end of abort(),
+ // after this is called.
+ assert_equals(client.readyState, 4)
+
+ result.push('loadend', state, client.readyState)
+ })
+
+ client.open("GET", "resources/well-formed.xml")
+ assert_equals(client.readyState, 1)
+
+ state = 1
+ client.send(null)
+ state = 2
+ client.abort()
+ assert_equals(client.readyState, 0)
+ state = 3
+ client.open("GET", "resources/well-formed.xml")
+ assert_equals(client.readyState, 1)
+ assert_array_equals(result, expected)
+ })
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/open-after-setrequestheader.htm b/test/wpt/tests/xhr/open-after-setrequestheader.htm
new file mode 100644
index 0000000..ca1ae25
--- /dev/null
+++ b/test/wpt/tests/xhr/open-after-setrequestheader.htm
@@ -0,0 +1,33 @@
+<!doctype html>
+<html>
+ <head>
+ <title>XMLHttpRequest: open() after setRequestHeader()</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-open()-method">
+
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ var test = async_test()
+ test.step(function() {
+ var client = new XMLHttpRequest()
+ client.onreadystatechange = function() {
+ test.step(function() {
+ if(client.readyState === 4){
+ assert_equals(client.responseText, '')
+ test.done()
+ }
+ })
+ }
+ client.open("GET", "resources/inspect-headers.py?filter_name=X-foo")
+ assert_equals(client.readyState, 1)
+ client.setRequestHeader('X-foo', 'bar')
+ client.open("GET", "resources/inspect-headers.py?filter_name=X-foo")
+ assert_equals(client.readyState, 1)
+ client.send()
+ })
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/open-after-stop.window.js b/test/wpt/tests/xhr/open-after-stop.window.js
new file mode 100644
index 0000000..e836a52
--- /dev/null
+++ b/test/wpt/tests/xhr/open-after-stop.window.js
@@ -0,0 +1,43 @@
+// window.stop() below prevents the load event from firing, so wait until it is
+// fired to start the test.
+setup({explicit_done: true });
+
+onload = () => {
+ async_test(function(t) {
+ const client = new XMLHttpRequest();
+
+ const result = [];
+ const expected = [
+ 'readystatechange', 0, 1, // open()
+ ];
+
+ let state = 0;
+
+ client.onreadystatechange = t.step_func(() => {
+ result.push('readystatechange', state, client.readyState);
+ });
+ client.onabort = t.unreached_func("abort should not be fired after window.stop() and open()");
+ client.onloadend = t.unreached_func("loadend should not be fired after window.stop() and open()");
+
+ client.open("GET", "resources/well-formed.xml");
+ assert_equals(client.readyState, 1);
+
+ state = 1;
+ client.send(null);
+ state = 2;
+ window.stop();
+ // Unlike client.abort(), window.stop() does not change readyState
+ // immediately, rather through a task...
+ assert_equals(client.readyState, 1);
+ state = 3;
+ // ... which is then canceled when we open a new request anyway.
+ client.open("GET", "resources/well-formed.xml");
+ assert_equals(client.readyState, 1);
+ assert_array_equals(result, expected);
+
+ // Give the abort and loadend events a chance to fire (erroneously) before
+ // calling this a success.
+ t.step_timeout(t.step_func_done(), 1000);
+ }, "open() after window.stop()");
+ done();
+};
diff --git a/test/wpt/tests/xhr/open-during-abort-event.htm b/test/wpt/tests/xhr/open-during-abort-event.htm
new file mode 100644
index 0000000..22c3be9
--- /dev/null
+++ b/test/wpt/tests/xhr/open-during-abort-event.htm
@@ -0,0 +1,56 @@
+<!doctype html>
+<title>XMLHttpRequest: open() during abort event - abort() called from upload.onloadstart</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+async_test(t => {
+ let client = new XMLHttpRequest(),
+ log = [],
+ lastTest = false,
+ expected = [
+ 'readyState before abort() 1',
+ "upload.onabort - before open() 4",
+ "readyState after open() 1",
+ "client.onabort 1",
+ "client.onloadend 1",
+ "readyState after abort() 1",
+ "client.onload 4",
+ "client.onloadend 4"
+ ]
+
+ client.upload.onloadstart = t.step_func(() => {
+ log.push('readyState before abort() '+client.readyState)
+ client.abort()
+ log.push('readyState after abort() '+client.readyState)
+ })
+
+ client.upload.onabort = t.step_func(() => {
+ log.push('upload.onabort - before open() ' + client.readyState)
+ client.open("GET", "resources/content.py")
+ log.push('readyState after open() ' + client.readyState)
+ client.send(null)
+ })
+
+ client.onabort = t.step_func(() => {
+ // happens immediately after all of upload.onabort, so readyState is 1
+ log.push('client.onabort ' + client.readyState)
+ })
+
+ client.onloadend = t.step_func(() => {
+ log.push('client.onloadend ' + client.readyState)
+ if(lastTest) {
+ assert_array_equals(log, expected)
+ t.done()
+ }
+ lastTest = true
+ })
+
+ client.onload = t.step_func(() => {
+ log.push('client.onload ' + client.readyState)
+ })
+
+ client.open("POST", "resources/content.py")
+ client.send("non-empty")
+})
+</script>
diff --git a/test/wpt/tests/xhr/open-during-abort-processing.htm b/test/wpt/tests/xhr/open-during-abort-processing.htm
new file mode 100644
index 0000000..706eb32
--- /dev/null
+++ b/test/wpt/tests/xhr/open-during-abort-processing.htm
@@ -0,0 +1,62 @@
+<!doctype html>
+<title>XMLHttpRequest: open() during abort processing - abort() called from onloadstart</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+async_test(t => {
+ let client = new XMLHttpRequest(),
+ test_state = 1,
+ log = [],
+ expected = [
+ "onloadstart readyState before abort() 1",
+ "onreadystatechange readyState before open() 4",
+ "onreadystatechange readyState after open() 1",
+ "onloadstart readyState 1",
+ "client.onabort 1",
+ "readyState after abort() 1",
+ "client.onload 4"
+ ]
+
+ client.onreadystatechange = t.step_func(() => {
+ if(test_state === 2){
+ test_state = 3
+ log.push('onreadystatechange readyState before open() ' + client.readyState)
+ client.open("GET", "resources/content.py")
+ log.push('onreadystatechange readyState after open() ' + client.readyState)
+ client.send(null)
+ }
+ })
+
+ client.onloadstart = t.step_func(() => {
+ if(test_state === 1){
+ test_state = 2
+ log.push('onloadstart readyState before abort() ' + client.readyState)
+ client.abort()
+ log.push('readyState after abort() ' + client.readyState)
+ }else{
+ log.push('onloadstart readyState ' + client.readyState)
+ }
+ })
+
+ client.upload.onabort = t.step_func(() => {
+ log.push('upload.onabort ' + client.readyState)
+ })
+
+ client.onabort = t.step_func(() => {
+ log.push('client.onabort ' + client.readyState)
+ })
+
+ client.upload.onloadend = t.step_func(() => {
+ log.push('upload.onloadend ' + client.readyState)
+ })
+
+ client.onload = t.step_func_done(() => {
+ log.push('client.onload ' + client.readyState)
+ assert_array_equals(log, expected)
+ })
+
+ client.open("POST", "resources/content.py")
+ client.send('abcd')
+})
+</script>
diff --git a/test/wpt/tests/xhr/open-during-abort.htm b/test/wpt/tests/xhr/open-during-abort.htm
new file mode 100644
index 0000000..d03ca7a
--- /dev/null
+++ b/test/wpt/tests/xhr/open-during-abort.htm
@@ -0,0 +1,33 @@
+<!doctype html>
+<html>
+ <head>
+ <title>XMLHttpRequest: open() during abort()</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ test(function() {
+ var client = new XMLHttpRequest(),
+ abort_flag = false,
+ result = [],
+ expected = [1, 4, 1] // open() => 1, abort() => 4, open() => 1
+
+ client.onreadystatechange = this.step_func(function() {
+ result.push(client.readyState)
+ if (abort_flag) {
+ abort_flag = false
+ client.open("GET", "...")
+ }
+ })
+ client.open("GET", "resources/well-formed.xml")
+ client.send(null)
+ abort_flag = true
+ client.abort()
+ assert_array_equals(result, expected)
+ assert_equals(client.readyState, 1) // abort() should only set state to UNSENT when DONE
+ })
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/open-method-bogus.htm b/test/wpt/tests/xhr/open-method-bogus.htm
new file mode 100644
index 0000000..13bb18b
--- /dev/null
+++ b/test/wpt/tests/xhr/open-method-bogus.htm
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>XMLHttpRequest: open() - bogus methods</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-open()-method" data-tested-assertations="following::ol[1]/li[4]" />
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ function method(method) {
+ test(function() {
+ var client = new XMLHttpRequest()
+ assert_throws_dom("SyntaxError", function() { client.open(method, "...") })
+ }, document.title + " (" + method + ")")
+ }
+ method("")
+ method(">")
+ method(" GET")
+ method("G T")
+ method("@GET")
+ method("G:ET")
+ method("GET?")
+ method("GET\n")
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/open-method-case-insensitive.htm b/test/wpt/tests/xhr/open-method-case-insensitive.htm
new file mode 100644
index 0000000..1033817
--- /dev/null
+++ b/test/wpt/tests/xhr/open-method-case-insensitive.htm
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>XMLHttpRequest: open() - case-insensitive methods test</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-open()-method" data-tested-assertations="following::ol/li[5]" />
+
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ function method(method) {
+ test(function() {
+ var client = new XMLHttpRequest()
+ client.open(method, "resources/content.py", false)
+ client.send(null)
+ assert_equals(client.getResponseHeader("x-request-method"), method.toUpperCase())
+ }, document.title + " (" + method.toUpperCase() + ")")
+ }
+ method("deLETE")
+ method("get")
+ method("heAd")
+ method("OpTIOns")
+ method("post")
+ method("Put")
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/open-method-case-sensitive.htm b/test/wpt/tests/xhr/open-method-case-sensitive.htm
new file mode 100644
index 0000000..270e32d
--- /dev/null
+++ b/test/wpt/tests/xhr/open-method-case-sensitive.htm
@@ -0,0 +1,31 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>XMLHttpRequest: open() - case-sensitive methods test</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-open()-method" data-tested-assertations="following::ol/li[5]" />
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ function method(method) {
+ test(function() {
+ var client = new XMLHttpRequest()
+ client.open(method, "resources/content.py", false)
+ client.send(null)
+ assert_equals(client.getResponseHeader("x-request-method"), method)
+ }, document.title + " (" + method + ")")
+ }
+ method("XUNICORN")
+ method("xUNIcorn")
+ method("chiCKEN")
+ method("PATCH")
+ method("patCH")
+ method("copy")
+ method("COpy")
+ method("inDEX")
+ method("movE")
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/open-method-insecure.htm b/test/wpt/tests/xhr/open-method-insecure.htm
new file mode 100644
index 0000000..a6bf442
--- /dev/null
+++ b/test/wpt/tests/xhr/open-method-insecure.htm
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>XMLHttpRequest: open() - "insecure" methods</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-open()-method" data-tested-assertations="following::ol/li[5] following::ol/li[6]" />
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ function method(method) {
+ test(function() {
+ var client = new XMLHttpRequest()
+ assert_throws_dom("SecurityError", function() { client.open(method, "...") })
+ }, document.title + " (" + method + ")")
+ }
+ method("track")
+ method("TRACK")
+ method("trAck")
+ method("TRACE")
+ method("trace")
+ method("traCE")
+ method("connect")
+ method("CONNECT")
+ method("connECT")
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/open-method-responsetype-set-sync.htm b/test/wpt/tests/xhr/open-method-responsetype-set-sync.htm
new file mode 100644
index 0000000..7858c91
--- /dev/null
+++ b/test/wpt/tests/xhr/open-method-responsetype-set-sync.htm
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>XMLHttpRequest: open() sync request not allowed if responseType is set</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-open()-method" data-tested-assertations="following::ol[1]/li[10]" />
+
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ // Note: the case of calling synchronous open() first, and then setting
+ // responseType, is tested in responsetype.html.
+ function request(type) {
+ test(function() {
+ var client = new XMLHttpRequest()
+ client.onreadystatechange = this.step_func(function(){
+ assert_unreached('No events should fire here')
+ })
+ client.responseType = type
+ assert_throws_dom("InvalidAccessError", function() { client.open('GET', "...", false) })
+ }, document.title + " (" + type + ")")
+ }
+ request("arraybuffer")
+ request("blob")
+ request("json")
+ request("text")
+ request("document")
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/open-open-send.htm b/test/wpt/tests/xhr/open-open-send.htm
new file mode 100644
index 0000000..ebc1801
--- /dev/null
+++ b/test/wpt/tests/xhr/open-open-send.htm
@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>XMLHttpRequest: open() - open() - send()</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-open()-method" data-tested-assertations="following::ol/li[14]/ul/li[1] following::ol/li[14]/ul/li[2] following::ol/li[15]/ol/li[1] following::ol/li[15]/ol/li[2]" />
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ var test = async_test()
+ test.step(function() {
+ var client = new XMLHttpRequest(),
+ result = [],
+ expected = [1,2,3,4]
+ client.onreadystatechange = function() {
+ test.step(function() {
+ result.push(client.readyState)
+ if(4 == client.readyState) {
+ assert_array_equals(result, expected)
+ assert_equals(client.responseText, 'top\n')
+ test.done()
+ }
+ })
+ }
+ client.open("GET", "resources/folder.txt")
+ client.open("GET", "folder.txt")
+ client.send(null)
+ })
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/open-open-sync-send.htm b/test/wpt/tests/xhr/open-open-sync-send.htm
new file mode 100644
index 0000000..b0badfd
--- /dev/null
+++ b/test/wpt/tests/xhr/open-open-sync-send.htm
@@ -0,0 +1,31 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>XMLHttpRequest: open() - open() (sync) - send()</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-open()-method" data-tested-assertations="following::ol/li[14]/ul/li[1] following::ol/li[14]/ul/li[2] following::ol/li[14]/ul/li[3] following::ol/li[15]/ol/li[1] following::ol/li[15]/ol/li[2]" />
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ var test = async_test()
+ test.step(function() {
+ var client = new XMLHttpRequest(),
+ result = [],
+ expected = [1,4]
+ client.onreadystatechange = function() {
+ test.step(function() {
+ result.push(client.readyState)
+ })
+ }
+ client.open("GET", "folder.txt")
+ client.open("GET", "folder.txt", false)
+ client.send(null)
+ assert_equals(client.responseText, 'top\n')
+ assert_array_equals(result, expected)
+ test.done()
+ })
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/open-parameters-toString.htm b/test/wpt/tests/xhr/open-parameters-toString.htm
new file mode 100644
index 0000000..c059482
--- /dev/null
+++ b/test/wpt/tests/xhr/open-parameters-toString.htm
@@ -0,0 +1,54 @@
+<!doctype html>
+<title>XMLHttpRequest: open() attempts to toString its string parameters</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+test(() => {
+ let log = [];
+ let expected = [
+ 'method',
+ 'url',
+ // NOTE: 'async' intentionally missing
+ 'username',
+ 'password',
+ ];
+
+ let xhr = new XMLHttpRequest;
+ xhr.open(
+ {
+ toString() {
+ log.push('method');
+ return 'get';
+ },
+ },
+ {
+ toString() {
+ log.push('url');
+ return location.href;
+ },
+ },
+ // NOTE: ToBoolean should not invoke valueOf
+ {
+ valueOf() {
+ log.push('async');
+ return true;
+ },
+ },
+ {
+ toString() {
+ log.push('username');
+ return 'username';
+ },
+ },
+ {
+ toString() {
+ log.push('password');
+ return 'password';
+ },
+ }
+ );
+
+ assert_array_equals(log, expected);
+});
+</script>
diff --git a/test/wpt/tests/xhr/open-referer.htm b/test/wpt/tests/xhr/open-referer.htm
new file mode 100644
index 0000000..d7ed793
--- /dev/null
+++ b/test/wpt/tests/xhr/open-referer.htm
@@ -0,0 +1,20 @@
+<!doctype html>
+<html>
+ <head>
+ <title>XMLHttpRequest: open() - value of Referer header</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-open()-method">
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ test(function() {
+ var client = new XMLHttpRequest()
+ client.open("POST", "resources/inspect-headers.py?filter_name=referer", false)
+ client.send(null)
+ assert_equals(client.responseText, "Referer: "+location.href+'\n')
+ })
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/open-send-during-abort.htm b/test/wpt/tests/xhr/open-send-during-abort.htm
new file mode 100644
index 0000000..dc6f86b
--- /dev/null
+++ b/test/wpt/tests/xhr/open-send-during-abort.htm
@@ -0,0 +1,27 @@
+<!doctype html>
+<title>XMLHttpRequest: open() during abort()</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+async_test(t => {
+ let result = [],
+ client = new XMLHttpRequest(),
+ expected = [1, 4, 1, 'hello']
+ client.open("GET", "data:text/plain,")
+ result.push(client.readyState)
+ client.send()
+ client.onreadystatechange = t.step_func(() => {
+ client.onreadystatechange = null
+ result.push(client.readyState)
+ client.open("GET", "data:text/plain,hello")
+ client.onload = t.step_func_done(() => {
+ result.push(client.responseText)
+ assert_array_equals(result, expected)
+ })
+ client.send()
+ })
+ client.abort()
+ result.push(client.readyState) // surprise! should not be "unsent" even though we called abort()
+})
+</script>
diff --git a/test/wpt/tests/xhr/open-send-open.htm b/test/wpt/tests/xhr/open-send-open.htm
new file mode 100644
index 0000000..d57592c
--- /dev/null
+++ b/test/wpt/tests/xhr/open-send-open.htm
@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>XMLHttpRequest: open() - send() - open()</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-open()-method" data-tested-assertations="following::ol/li[14]/ul/li[1] following::ol/li[14]/ul/li[2] following::ol/li[15]/ol/li[1] following::ol/li[15]/ol/li[2]" />
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ var test = async_test()
+ test.step(function() {
+ var client = new XMLHttpRequest(),
+ result = [],
+ expected = [1, 'a', 'b', 'c']
+ client.onreadystatechange = function() {
+ test.step(function() {
+ result.push(client.readyState)
+ })
+ }
+ client.open("GET", "folder.txt")
+ result.push('a')
+ client.send()
+ result.push('b')
+ client.open("GET", "folder.txt")
+ result.push('c')
+ assert_array_equals(result, expected)
+ test.done()
+ })
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/open-sync-open-send.htm b/test/wpt/tests/xhr/open-sync-open-send.htm
new file mode 100644
index 0000000..cc81c52
--- /dev/null
+++ b/test/wpt/tests/xhr/open-sync-open-send.htm
@@ -0,0 +1,41 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>XMLHttpRequest: open() (sync) - send() - open()</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-open()-method" data-tested-assertations="following::ol[1]/li[14]/ul/li[1] following::ol[1]/li[14]/ul/li[2] following::ol[1]/li[14]/ul/li[3] following::ol[1]/li[15]/ol/li[1] following::ol[1]/li[15]/ol/li[2]" />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-responsexml-attribute" data-tested-assertations="following::ol[1]/li[2]" />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-responsetext-attribute" data-tested-assertations="following::ol[1]/li[2]" />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-status-attribute" data-tested-assertations="following::ol[1]/li[1]" />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-statustext-attribute" data-tested-assertations="following::ol[1]/li[1]" />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-getallresponseheaders()-method" data-tested-assertations="following::ol[1]/li[1]" />
+
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ var test = async_test()
+ test.step(function() {
+ var client = new XMLHttpRequest(),
+ result = [],
+ expected = [1]
+ client.onreadystatechange = function() {
+ test.step(function() {
+ result.push(client.readyState)
+ })
+ }
+ client.open("GET", "folder.txt")
+ client.send(null)
+ client.open("GET", "folder.txt", false)
+ assert_array_equals(result, expected)
+ assert_equals(client.responseXML, null)
+ assert_equals(client.responseText, "")
+ assert_equals(client.status, 0)
+ assert_equals(client.statusText, "")
+ assert_equals(client.getAllResponseHeaders(), "")
+ test.done()
+ })
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/open-url-about-blank-window.htm b/test/wpt/tests/xhr/open-url-about-blank-window.htm
new file mode 100644
index 0000000..5be3b77
--- /dev/null
+++ b/test/wpt/tests/xhr/open-url-about-blank-window.htm
@@ -0,0 +1,23 @@
+<!doctype html>
+<html>
+ <head>
+ <title>XMLHttpRequest: open() resolving URLs (about:blank iframe)</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-open()-method" data-tested-assertations="following::ol/li[2]/ol/li[2] following::ol/li[7] following::ol/li[14]/ul/li[2]" />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-responsetext-attribute" data-tested-assertations="following::ol/li[4]" />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#concept-xmlhttprequest-document" data-tested-assertations=".." />
+ </head>
+ <body>
+ <div id="log"></div>
+ <iframe src="about:blank"></iframe>
+ <script>
+ test(function() {
+ var client = new self[0].XMLHttpRequest()
+ client.open("GET", "folder.txt", false)
+ client.send("")
+ assert_equals(client.responseText, "top\n")
+ })
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/open-url-base-inserted-after-open.htm b/test/wpt/tests/xhr/open-url-base-inserted-after-open.htm
new file mode 100644
index 0000000..a4d641f
--- /dev/null
+++ b/test/wpt/tests/xhr/open-url-base-inserted-after-open.htm
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>XMLHttpRequest: open() resolving URLs - insert &lt;base> after open()</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-open()-method" data-tested-assertations="following::ol/li[2]/ol/li[2] following::ol/li[7] following::ol/li[14]/ul/li[2]" />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-responsetext-attribute" data-tested-assertations="following::ol/li[4]" />
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ test(function() {
+ var client = new XMLHttpRequest(),
+ base = document.createElement("base")
+ base.href = location.href.replace(/\/[^/]*$/, '') + "/resources/"
+ client.open("GET", "folder.txt", false)
+ document.getElementsByTagName("head")[0].appendChild(base)
+ client.send(null)
+ assert_equals(client.responseText, "top\n")
+ })
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/open-url-base-inserted.htm b/test/wpt/tests/xhr/open-url-base-inserted.htm
new file mode 100644
index 0000000..69ad619
--- /dev/null
+++ b/test/wpt/tests/xhr/open-url-base-inserted.htm
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>XMLHttpRequest: open() resolving URLs - insert &lt;base></title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-open()-method" data-tested-assertations="following::ol/li[2]/ol/li[2] following::ol/li[7] following::ol/li[14]/ul/li[2]" />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-responsetext-attribute" data-tested-assertations="following::ol/li[4]" />
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ test(function() {
+ var client = new XMLHttpRequest(),
+ base = document.createElement("base")
+ base.href = location.href.replace(/\/[^/]*$/, '') + "/resources/"
+ document.getElementsByTagName("head")[0].appendChild(base)
+ client.open("GET", "folder.txt", false)
+ client.send(null)
+ assert_equals(client.responseText, "bottom\n")
+ })
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/open-url-base.htm b/test/wpt/tests/xhr/open-url-base.htm
new file mode 100644
index 0000000..3c0e8c9
--- /dev/null
+++ b/test/wpt/tests/xhr/open-url-base.htm
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>XMLHttpRequest: open() resolving URLs - &lt;base></title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <base href="./resources/">
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-open()-method" data-tested-assertations="following::ol/li[2]/ol/li[2] following::ol/li[7] following::ol/li[14]/ul/li[2]" />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-responsetext-attribute" data-tested-assertations="following::ol/li[4]" />
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ test(function() {
+ var client = new XMLHttpRequest()
+ client.open("GET", "folder.txt", false)
+ client.send(null)
+ assert_equals(client.responseText, "bottom\n")
+ })
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/open-url-encoding.htm b/test/wpt/tests/xhr/open-url-encoding.htm
new file mode 100644
index 0000000..5155a57
--- /dev/null
+++ b/test/wpt/tests/xhr/open-url-encoding.htm
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset=windows-1252>
+ <title>XMLHttpRequest: open() - URL encoding</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ test(function() {
+ var client = new XMLHttpRequest()
+ client.open("GET", "resources/content.py?\u00DF", false) // This is the German "eszett" character
+ client.send()
+ assert_equals(client.getResponseHeader("x-request-query"), "%DF")
+ }, "percent encode characters");
+ test(function() {
+ var client = new XMLHttpRequest()
+ client.open("GET", "resources/content.py?\uD83D", false)
+ client.send()
+ assert_equals(client.getResponseHeader("x-request-query"), "%26%2365533%3B");
+ }, "lone surrogate");
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/open-url-fragment.htm b/test/wpt/tests/xhr/open-url-fragment.htm
new file mode 100644
index 0000000..03f4016
--- /dev/null
+++ b/test/wpt/tests/xhr/open-url-fragment.htm
@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>XMLHttpRequest: open() resolving URLs - fragment identifier</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-open()-method" data-tested-assertations="following::ol[1]/li[7]" />
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ test(function() {
+ var client = new XMLHttpRequest()
+ client.open("GET", "folder.txt#foobar", false)
+ client.send(null)
+ assert_equals(client.responseText, "top\n")
+ })
+ test(function() {
+ var client = new XMLHttpRequest()
+ client.open("GET", "resources/requri.py#foobar", false)
+ client.send(null)
+ assert_regexp_match(client.responseText, /xhr\/resources\/requri\.py$/)
+ }, 'make sure fragment is removed from URL before request')
+ test(function() {
+ var client = new XMLHttpRequest()
+ client.open("GET", "resources/requri.py?help=#foobar", false)
+ client.send(null)
+ assert_regexp_match(client.responseText, /xhr\/resources\/requri\.py\?help=$/)
+ }, 'make sure fragment is removed from URL before request (with query string)')
+ test(function() {
+ var client = new XMLHttpRequest()
+ client.open("GET", "resources/requri.py?" +encodeURIComponent("#foobar"), false)
+ client.send(null)
+ assert_regexp_match(client.responseText, /xhr\/resources\/requri\.py\?%23foobar$/)
+ }, 'make sure escaped # is not removed')
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/open-url-javascript-window-2.htm b/test/wpt/tests/xhr/open-url-javascript-window-2.htm
new file mode 100644
index 0000000..f5ddd42
--- /dev/null
+++ b/test/wpt/tests/xhr/open-url-javascript-window-2.htm
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>XMLHttpRequest: open() - resolving URLs (javascript: &lt;iframe>; 2)</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-open()-method" data-tested-assertations="following::ol[1]/li[2]/ol[1]/li[2] following::ol[1]/li[7] following::ol[1]/li[14]/ul/li[2]" />
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ var test = async_test()
+ test.step(function() {
+ var iframe = document.body.appendChild(document.createElement("iframe"))
+ iframe.src = "javascript:parent.test.step(function() { var x = new XMLHttpRequest(); x.open('GET', 'folder.txt', false); x.send(null); parent.assert_equals(x.responseText, 'top\\n'); parent.test.done() })"
+ })
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/open-url-javascript-window.htm b/test/wpt/tests/xhr/open-url-javascript-window.htm
new file mode 100644
index 0000000..cd208d5
--- /dev/null
+++ b/test/wpt/tests/xhr/open-url-javascript-window.htm
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>XMLHttpRequest: open() - resolving URLs (javascript: &lt;iframe>; 1)</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-open()-method" data-tested-assertations="following::ol[1]/li[2]/ol[1]/li[2] following::ol[1]/li[7] following::ol[1]/li[14]/ul/li[2]" />
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ var test = async_test()
+ function request() {
+ test.step(function() {
+ var x = new XMLHttpRequest()
+ x.open("GET", "folder.txt", false)
+ x.send(null)
+ assert_equals(x.responseText, "top\n")
+ test.done()
+ })
+ }
+ test.step(function() {
+ var iframe = document.body.appendChild(document.createElement("iframe"))
+ iframe.src = "javascript:parent.request()"
+ })
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/open-url-multi-window-2.htm b/test/wpt/tests/xhr/open-url-multi-window-2.htm
new file mode 100644
index 0000000..64cc96b
--- /dev/null
+++ b/test/wpt/tests/xhr/open-url-multi-window-2.htm
@@ -0,0 +1,25 @@
+<!doctype html>
+<html>
+ <head>
+ <title>XMLHttpRequest: open() resolving URLs (multi-Window; 2; evil)</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-open()-method" data-tested-assertations="following::ol[1]/li[2]/ol[1]/li[1]" />
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ function init(){ // called from page inside IFRAME
+ test(function() {
+ var client = new self[0].XMLHttpRequest();
+ let exceptionCtor = self[0].DOMException;
+ document.body.removeChild(document.getElementsByTagName("iframe")[0])
+ assert_throws_dom("InvalidStateError", exceptionCtor, function() {
+ client.open("GET", "folder.txt")
+ }, "open() when associated document's IFRAME is removed");
+ });
+ }
+ </script>
+ <iframe src="resources/init.htm"></iframe>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/open-url-multi-window-3.htm b/test/wpt/tests/xhr/open-url-multi-window-3.htm
new file mode 100644
index 0000000..e156857
--- /dev/null
+++ b/test/wpt/tests/xhr/open-url-multi-window-3.htm
@@ -0,0 +1,25 @@
+<!doctype html>
+<html>
+ <head>
+ <title>XMLHttpRequest: open() resolving URLs (multi-Window; 3; evil)</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ function init() {
+ test(function() {
+ var client = new self[0].XMLHttpRequest();
+ let exceptionCtor = self[0].DOMException;
+ client.open("GET", "folder.txt")
+ document.body.removeChild(document.getElementsByTagName("iframe")[0])
+ assert_throws_dom("InvalidStateError", exceptionCtor, function() {
+ client.send(null)
+ }, "send() when associated document's IFRAME is removed");
+ })
+ }
+ </script>
+ <iframe src="resources/init.htm"></iframe>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/open-url-multi-window-4.htm b/test/wpt/tests/xhr/open-url-multi-window-4.htm
new file mode 100644
index 0000000..3804c9b
--- /dev/null
+++ b/test/wpt/tests/xhr/open-url-multi-window-4.htm
@@ -0,0 +1,50 @@
+<!doctype html>
+<html>
+ <head>
+ <title>XMLHttpRequest: open() resolving URLs (multi-Window; 4; evil)</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ /*
+ It's unclear what the pass condition should be for this test.
+ Implementations:
+ Firefox, Opera (Presto): terminate request with no further events when IFRAME is removed.
+ Chrome: completes request to readyState=4 but responseText is "" so it's pretty much terminated with an extra event for "DONE" state
+ Pass condition is now according to my suggested spec text in https://github.com/whatwg/xhr/pull/3 , if that's not accepted we'll have to amend this test
+ */
+ var test = async_test()
+ function init() {
+ test.step(function() {
+ var hasErrorEvent = false
+ var client = new self[0].XMLHttpRequest()
+ client.onreadystatechange = function() {
+ test.step(function() {
+ if(client.readyState == 4) {
+ assert_equals(client.responseText, "", "responseText is empty on inactive document error condition")
+ }
+ })
+ }
+ client.addEventListener('error', function(){
+ test.step(function() {
+ hasErrorEvent = true
+ assert_equals(client.readyState, 4, "readyState is 4 when error listener fires")
+ })
+ })
+ client.addEventListener('loadend', function(){
+ test.step(function() {
+ assert_true(hasErrorEvent, "should get an error event")
+ test.done()
+ })
+ })
+ client.open("GET", "folder.txt")
+ client.send(null)
+ document.body.removeChild(document.getElementsByTagName("iframe")[0])
+ })
+ }
+ </script>
+ <iframe src="resources/init.htm"></iframe>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/open-url-multi-window-5.htm b/test/wpt/tests/xhr/open-url-multi-window-5.htm
new file mode 100644
index 0000000..32c467a
--- /dev/null
+++ b/test/wpt/tests/xhr/open-url-multi-window-5.htm
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>XMLHttpRequest: open() resolving URLs (multi-Window; 5)</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-open()-method" data-tested-assertations="following::ol[1]/li[2]/ol[1]/li[1]" />
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ var test = async_test(),
+ client,
+ exceptionCtor,
+ count = 0;
+ function init() {
+ test.step(function() {
+ if(0 == count) {
+ client = new self[0].XMLHttpRequest();
+ exceptionCtor = self[0].DOMException;
+ count++
+ self[0].location.reload()
+ } else if(1 == count) {
+ assert_throws_dom("InvalidStateError", exceptionCtor, function() {client.open("GET", "...") });
+ test.done()
+ }
+ })
+ }
+ </script>
+ <iframe src="resources/init.htm"></iframe>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/open-url-multi-window-6.htm b/test/wpt/tests/xhr/open-url-multi-window-6.htm
new file mode 100644
index 0000000..d834a04
--- /dev/null
+++ b/test/wpt/tests/xhr/open-url-multi-window-6.htm
@@ -0,0 +1,41 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>XMLHttpRequest: open() in document that is not fully active (but may be active) should throw</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-open()-method">
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ var test = async_test(),
+ client,
+ count = 0,
+ win = window.open("resources/init.htm");
+ test.add_cleanup(function() { win.close(); });
+ function init() {
+ test.step(function() {
+ if(0 == count) {
+ var doc = win.document;
+ var ifr = document.createElement("iframe");
+ ifr.onload = function() {
+ // Again, do things async so we're not doing loads from inside
+ // load events.
+ test.step_timeout(function() {
+ client = new ifr.contentWindow.XMLHttpRequest();
+ count++;
+ // Important to do a normal navigation, not a reload.
+ win.location.href = "resources/init.htm?avoid-replace";
+ }, 0);
+ }
+ doc.body.appendChild(ifr);
+ } else if(1 == count) {
+ assert_throws_dom("InvalidStateError", function() { client.open("GET", "...") })
+ test.done()
+ }
+ })
+ }
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/open-url-multi-window.htm b/test/wpt/tests/xhr/open-url-multi-window.htm
new file mode 100644
index 0000000..347f4b7
--- /dev/null
+++ b/test/wpt/tests/xhr/open-url-multi-window.htm
@@ -0,0 +1,31 @@
+<!doctype html>
+<html>
+ <head>
+ <title>XMLHttpRequest: open() resolving URLs (multi-Window; 1)</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-open()-method" data-tested-assertations="following::ol[1]/li[2]/ol[1]/li[2] following::ol[1]/li[7] following::ol[1]/li[14]/ul/li[2]" />
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ var test = async_test()
+ function init() {
+ test.step(function() {
+ var client = new self[0].XMLHttpRequest()
+ client.onreadystatechange = function() {
+ test.step(function() {
+ if(client.readyState == 4) {
+ assert_equals(client.responseText, "bottom\n")
+ test.done()
+ }
+ })
+ }
+ client.open("GET", "folder.txt")
+ client.send("")
+ })
+ }
+ </script>
+ <iframe src="resources/init.htm"></iframe>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/open-url-redirected-sharedworker-origin.htm b/test/wpt/tests/xhr/open-url-redirected-sharedworker-origin.htm
new file mode 100644
index 0000000..0269991
--- /dev/null
+++ b/test/wpt/tests/xhr/open-url-redirected-sharedworker-origin.htm
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>XMLHttpRequest: redirected classic shared worker scripts, origin and referrer</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+var finalWorkerURL = "workerxhr-origin-referrer.js";
+var url = "resources/redirect.py?location=" + encodeURIComponent(finalWorkerURL);
+fetch_tests_from_worker(new SharedWorker(url));
+</script>
diff --git a/test/wpt/tests/xhr/open-url-redirected-worker-origin.htm b/test/wpt/tests/xhr/open-url-redirected-worker-origin.htm
new file mode 100644
index 0000000..a0e0648
--- /dev/null
+++ b/test/wpt/tests/xhr/open-url-redirected-worker-origin.htm
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>XMLHttpRequest: redirected classic dedicated worker scripts, origin and referrer</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+var finalWorkerURL = "workerxhr-origin-referrer.js";
+var url = "resources/redirect.py?location=" + encodeURIComponent(finalWorkerURL);
+fetch_tests_from_worker(new Worker(url));
+</script>
diff --git a/test/wpt/tests/xhr/open-url-worker-origin.htm b/test/wpt/tests/xhr/open-url-worker-origin.htm
new file mode 100644
index 0000000..21cf1fc
--- /dev/null
+++ b/test/wpt/tests/xhr/open-url-worker-origin.htm
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>XMLHttpRequest: worker scripts, origin and referrer</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+fetch_tests_from_worker(new Worker("resources/workerxhr-origin-referrer.js"));
+</script>
diff --git a/test/wpt/tests/xhr/open-url-worker-simple.htm b/test/wpt/tests/xhr/open-url-worker-simple.htm
new file mode 100644
index 0000000..a77ef6f
--- /dev/null
+++ b/test/wpt/tests/xhr/open-url-worker-simple.htm
@@ -0,0 +1,25 @@
+ <!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8" />
+ <title>XMLHttpRequest: relative URLs in worker scripts resolved by script URL</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-open()-method" data-tested-assertations="following::OL[1]/LI[3] following::OL[1]/LI[3]/ol[1]/li[1]" />
+</head>
+<body>
+ <div id="log"></div>
+ <script type="text/javascript">
+ var test = async_test()
+ var worker = new Worker("resources/workerxhr-simple.js")
+ worker.onmessage = function (e) {
+ test.step(function(){
+ assert_equals(e.data, 'PASSED')
+ test.done()
+ })
+ }
+ worker.postMessage('start')
+ </script>
+</body>
+</html>
+
diff --git a/test/wpt/tests/xhr/open-user-password-non-same-origin.htm b/test/wpt/tests/xhr/open-user-password-non-same-origin.htm
new file mode 100644
index 0000000..e49888c
--- /dev/null
+++ b/test/wpt/tests/xhr/open-user-password-non-same-origin.htm
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>XMLHttpRequest: open() - user/pass argument and non same-origin URL doesn't throw</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-open()-method" data-tested-assertations="following::ol/li[9]/ol/li[1] following::ol/li[9]/ol/li[2] following::ol/li[15]/ol/li[1]" />
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ var m = "GET",
+ u = "http://test2.w3.org/",
+ a = false
+ test(function() {
+ var client = new XMLHttpRequest()
+ client.open(m, u, a, "x")
+ assert_equals(client.readyState, 1, "open() was successful - 1")
+ var client2 = new XMLHttpRequest()
+ client2.open(m, u, a, "x", "x")
+ assert_equals(client2.readyState, 1, "open() was successful - 2")
+ })
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/over-1-meg.any.js b/test/wpt/tests/xhr/over-1-meg.any.js
new file mode 100644
index 0000000..ad58314
--- /dev/null
+++ b/test/wpt/tests/xhr/over-1-meg.any.js
@@ -0,0 +1,16 @@
+"use strict";
+
+async_test(t => {
+ const xhr = new XMLHttpRequest();
+ xhr.open("GET", "./resources/over-1-meg.txt");
+
+ xhr.addEventListener("load", t.step_func_done(() => {
+ const result = xhr.responseText;
+ const desiredResult = "abcd".repeat(290000);
+
+ assert_equals(result.length, desiredResult.length); // to avoid large diffs if they are lengthwise different
+ assert_equals(result, desiredResult);
+ }));
+
+ xhr.send();
+});
diff --git a/test/wpt/tests/xhr/overridemimetype-blob.html b/test/wpt/tests/xhr/overridemimetype-blob.html
new file mode 100644
index 0000000..fef0dfe
--- /dev/null
+++ b/test/wpt/tests/xhr/overridemimetype-blob.html
@@ -0,0 +1,57 @@
+<!doctype html>
+<title>XMLHttpRequest: overrideMimeType() and responseType = "blob"</title>
+<meta name="timeout" content="long">
+<meta charset="utf-8">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+async_test(t => {
+ const client = new XMLHttpRequest();
+ client.onload = t.step_func_done(() => {
+ assert_equals(client.getResponseHeader("Content-Type"), "");
+ assert_equals(client.response.type, "text/xml");
+ });
+ client.open("GET", "resources/status.py");
+ client.responseType = "blob";
+ client.send();
+}, "Use text/xml as fallback MIME type");
+
+async_test(t => {
+ const client = new XMLHttpRequest();
+ client.onload = t.step_func_done(() => {
+ assert_equals(client.getResponseHeader("Content-Type"), "");
+ assert_equals(client.response.type, "text/xml");
+ })
+ client.open("GET", "resources/status.py?content=thisshouldnotmakeadifferencebutdoes");
+ client.responseType = "blob";
+ client.send();
+}, "Use text/xml as fallback MIME type, 2");
+
+promise_test(() => {
+ // Don't load generated-mime-types.json as sending them all over the network would be prohibitive
+ return fetch("../mimesniff/mime-types/resources/mime-types.json").then(res => res.json()).then(runTests);
+}, "Loading data…");
+
+function runTests(tests) {
+ let index = 0;
+ tests.forEach((val) => {
+ if(typeof val === "string") {
+ return;
+ }
+ index++;
+ async_test(t => {
+ const client = new XMLHttpRequest(),
+ expectedOutput = val.output !== null ? val.output : "application/octet-stream";
+ client.onload = t.step_func_done(() => {
+ assert_equals(client.getResponseHeader("Content-Type"), "");
+ assert_equals(client.response.type, expectedOutput);
+ });
+ client.open("GET", "resources/status.py");
+ client.responseType = "blob";
+ client.overrideMimeType(val.input);
+ client.send();
+ }, index + ") MIME types need to be parsed and serialized: " + val.input);
+ });
+}
+</script>
diff --git a/test/wpt/tests/xhr/overridemimetype-done-state.any.js b/test/wpt/tests/xhr/overridemimetype-done-state.any.js
new file mode 100644
index 0000000..5e70492
--- /dev/null
+++ b/test/wpt/tests/xhr/overridemimetype-done-state.any.js
@@ -0,0 +1,20 @@
+// META title= XMLHttpRequest: overrideMimeType() in DONE state</title>
+
+/**
+ * Spec: <https://xhr.spec.whatwg.org/#the-overridemimetype()-method>; data-tested-assertations="/following::ol/li[1]"
+ */
+var test = async_test();
+var client = new XMLHttpRequest();
+client.onreadystatechange = test.step_func( function() {
+ if (client.readyState !== 4) return;
+ var text = client.responseText;
+ assert_not_equals(text, "");
+ assert_throws_dom("InvalidStateError", function() { client.overrideMimeType('application/xml;charset=Shift-JIS'); });
+ if (GLOBAL.isWindow()) {
+ assert_equals(client.responseXML, null);
+ }
+ assert_equals(client.responseText, text);
+ test.done();
+});
+client.open("GET", "resources/status.py?type="+encodeURIComponent('text/plain;charset=iso-8859-1')+'&content=%3Cmsg%3E%83%65%83%58%83%67%3C%2Fmsg%3E');
+client.send();
diff --git a/test/wpt/tests/xhr/overridemimetype-edge-cases.window.js b/test/wpt/tests/xhr/overridemimetype-edge-cases.window.js
new file mode 100644
index 0000000..3f57e9a
--- /dev/null
+++ b/test/wpt/tests/xhr/overridemimetype-edge-cases.window.js
@@ -0,0 +1,50 @@
+const testURL = "resources/status.py?type=" + encodeURIComponent("text/plain;charset=windows-1252") + "&content=%C2%F0";
+
+async_test(t => {
+ const client = new XMLHttpRequest();
+ client.onload = t.step_func_done(() => {
+ assert_equals(client.responseText, "\uFFFD\uFFFD");
+ });
+ client.overrideMimeType("text/plain;charset=UTF-8");
+ client.open("GET", testURL);
+ client.send();
+}, "overrideMimeType() is not reset by open(), basic");
+
+async_test(t => {
+ const client = new XMLHttpRequest();
+ let secondTime = false;
+ client.onload = t.step_func(() => {
+ if(!secondTime) {
+ assert_equals(client.responseText, "\uFFFD\uFFFD");
+ secondTime = true;
+ client.open("GET", testURL);
+ client.send();
+ } else {
+ assert_equals(client.responseText, "\uFFFD\uFFFD");
+ t.done();
+ }
+ });
+ client.open("GET", testURL);
+ client.overrideMimeType("text/plain;charset=UTF-8")
+ client.send();
+}, "overrideMimeType() is not reset by open()");
+
+async_test(t => {
+ const client = new XMLHttpRequest();
+ client.onload = t.step_func_done(() => {
+ assert_equals(client.responseText, "Âð")
+ });
+ client.open("GET", testURL);
+ client.overrideMimeType("text/xml");
+ client.send();
+}, "If charset is not overridden by overrideMimeType() the original continues to be used");
+
+async_test(t => {
+ const client = new XMLHttpRequest();
+ client.onload = t.step_func_done(() => {
+ assert_equals(client.responseText, "\uFFFD\uFFFD")
+ });
+ client.open("GET", testURL);
+ client.overrideMimeType("text/plain;charset=342");
+ client.send();
+}, "Charset can be overridden by overrideMimeType() with a bogus charset");
diff --git a/test/wpt/tests/xhr/overridemimetype-headers-received-state-force-shiftjis.htm b/test/wpt/tests/xhr/overridemimetype-headers-received-state-force-shiftjis.htm
new file mode 100644
index 0000000..578e28c
--- /dev/null
+++ b/test/wpt/tests/xhr/overridemimetype-headers-received-state-force-shiftjis.htm
@@ -0,0 +1,34 @@
+<!doctype html>
+<html>
+ <head>
+ <title>XMLHttpRequest: overrideMimeType() in HEADERS RECEIVED state, enforcing Shift-JIS encoding</title>
+ <meta charset="utf-8">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-overridemimetype()-method" data-tested-assertations="/following::ol/li[1] /following::ol/li[4]" />
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ var test = async_test();
+ var client = new XMLHttpRequest();
+ var readyState2Reached = false;
+ client.onreadystatechange = test.step_func( function() {
+ if(client.readyState===2){
+ readyState2Reached = true;
+ try{
+ client.overrideMimeType('text/plain;charset=Shift-JIS');
+ }catch(e){
+ assert_unreached('overrideMimeType should not throw in state 2');
+ }
+ }
+ if (client.readyState !== 4) return;
+ assert_equals( readyState2Reached, true, "readyState = 2 event fired" );
+ assert_equals( client.responseText, 'テスト', 'overrideMimeType() in HEADERS RECEIVED state set encoding' );
+ test.done();
+ });
+ client.open("GET", "resources/status.py?type="+encodeURIComponent('text/html;charset=UTF-8')+'&content=%83%65%83%58%83%67');
+ client.send( '' );
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/overridemimetype-invalid-mime-type.htm b/test/wpt/tests/xhr/overridemimetype-invalid-mime-type.htm
new file mode 100644
index 0000000..506aff8
--- /dev/null
+++ b/test/wpt/tests/xhr/overridemimetype-invalid-mime-type.htm
@@ -0,0 +1,41 @@
+<!doctype html>
+<title>XMLHttpRequest: overrideMimeType() and invalid MIME types</title>
+<meta charset="utf-8">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<link rel="help" href="https://xhr.spec.whatwg.org/#the-overridemimetype()-method">
+<div id="log"></div>
+<script>
+async_test(t => {
+ const client = new XMLHttpRequest()
+ client.onload = t.step_func_done(() => {
+ assert_equals(client.responseText, "ÿ")
+ assert_equals(client.getResponseHeader("Content-Type"), "text/html;charset=windows-1252")
+ })
+ client.open("GET", "resources/status.py?type=" + encodeURIComponent("text/html;charset=windows-1252") + "&content=%FF")
+ client.overrideMimeType("bogus")
+ client.send()
+}, "Bogus MIME type does not override encoding")
+
+async_test(t => {
+ const client = new XMLHttpRequest()
+ client.onload = t.step_func_done(() => {
+ assert_equals(client.responseText, "ÿ")
+ assert_equals(client.getResponseHeader("Content-Type"), "text/html;charset=windows-1252")
+ })
+ client.open("GET", "resources/status.py?type=" + encodeURIComponent("text/html;charset=windows-1252") + "&content=%FF")
+ client.overrideMimeType("bogus;charset=Shift_JIS")
+ client.send()
+}, "Bogus MIME type does not override encoding, 2")
+
+async_test(t => {
+ const client = new XMLHttpRequest()
+ client.onload = t.step_func_done(() => {
+ assert_equals(client.responseXML, null)
+ assert_equals(client.getResponseHeader("Content-Type"), "text/xml")
+ })
+ client.open("GET", "resources/status.py?type=" + encodeURIComponent("text/xml") + "&content=" + encodeURIComponent("<x/>"))
+ client.overrideMimeType("bogus")
+ client.send()
+}, "Bogus MIME type does override MIME type")
+</script>
diff --git a/test/wpt/tests/xhr/overridemimetype-loading-state.htm b/test/wpt/tests/xhr/overridemimetype-loading-state.htm
new file mode 100644
index 0000000..06e4d5f
--- /dev/null
+++ b/test/wpt/tests/xhr/overridemimetype-loading-state.htm
@@ -0,0 +1,32 @@
+<!doctype html>
+<html>
+ <head>
+ <title>XMLHttpRequest: overrideMimeType() in LOADING state</title>
+ <meta charset="utf-8">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-overridemimetype()-method" data-tested-assertations="/following::ol/li[1]" />
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ var test = async_test();
+ test.step(function() {
+ var client = new XMLHttpRequest();
+ client.onreadystatechange = test.step_func(function() {
+ if (client.readyState === 3){
+ assert_throws_dom("InvalidStateError", function(){
+ client.overrideMimeType('application/xml;charset=Shift-JIS');
+ });
+ }else if(client.readyState===4){
+ assert_equals(client.responseXML, null);
+ test.done();
+ }
+ });
+ client.open("GET", "resources/status.py?type="+encodeURIComponent('text/plain;charset=iso-8859-1')+'&content=%3Cmsg%3E%83%65%83%58%83%67%3C%2Fmsg%3E');
+ client.send();
+ });
+ </script>
+
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/overridemimetype-open-state-force-utf-8.htm b/test/wpt/tests/xhr/overridemimetype-open-state-force-utf-8.htm
new file mode 100644
index 0000000..5a26100
--- /dev/null
+++ b/test/wpt/tests/xhr/overridemimetype-open-state-force-utf-8.htm
@@ -0,0 +1,27 @@
+<!doctype html>
+<html>
+ <head>
+ <title>XMLHttpRequest: overrideMimeType() in open state, enforcing UTF-8 encoding</title>
+ <meta charset="utf-8">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-overridemimetype()-method" data-tested-assertations="/following::ol/li[3] /following::ol/li[4]" />
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ var test = async_test();
+ test.step(function() {
+ var client = new XMLHttpRequest();
+ client.onreadystatechange = function() {
+ if (client.readyState !== 4) return;
+ assert_equals( client.responseText, 'テスト' );
+ test.done();
+ };
+ client.open("GET", "resources/status.py?type="+encodeURIComponent('text/html;charset=Shift-JIS')+'&content='+encodeURIComponent('テスト'));
+ client.overrideMimeType('text/plain;charset=UTF-8');
+ client.send( '' );
+ });
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/overridemimetype-open-state-force-xml.htm b/test/wpt/tests/xhr/overridemimetype-open-state-force-xml.htm
new file mode 100644
index 0000000..00a4c0d
--- /dev/null
+++ b/test/wpt/tests/xhr/overridemimetype-open-state-force-xml.htm
@@ -0,0 +1,34 @@
+<!doctype html>
+<html>
+ <head>
+ <title>XMLHttpRequest: overrideMimeType() in open state, XML MIME type with UTF-8 charset</title>
+ <meta charset="utf-8">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-overridemimetype()-method" data-tested-assertations="/following::ol/li[3] /following::ol/li[4]" />
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ var test = async_test();
+ test.step(function() {
+ var client = new XMLHttpRequest();
+ client.onreadystatechange = function() {
+ if (client.readyState !== 4) return;
+ try{
+ var str = client.responseXML.documentElement.tagName+client.responseXML.documentElement.firstChild.tagName+client.responseXML.documentElement.firstChild.textContent;
+ }catch(e){
+ assert_unreached('Exception when reading responseXML');
+ }
+ assert_equals( client.responseXML.documentElement.tagName, 'test' );
+ assert_equals( client.responseXML.documentElement.firstChild.tagName, 'message' );
+ assert_equals( client.responseXML.documentElement.firstChild.textContent, 'Hello Worldï¼' );
+ test.done();
+ };
+ client.open("GET", "resources/status.py?type="+encodeURIComponent('text/plain;charset=Shift-JIS')+'&content='+encodeURIComponent('<test><message>Hello Worldï¼</message></test>'));
+ client.overrideMimeType('application/xml;charset=UTF-8');
+ client.send();
+ });
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/overridemimetype-unsent-state-force-shiftjis.any.js b/test/wpt/tests/xhr/overridemimetype-unsent-state-force-shiftjis.any.js
new file mode 100644
index 0000000..b3125b0
--- /dev/null
+++ b/test/wpt/tests/xhr/overridemimetype-unsent-state-force-shiftjis.any.js
@@ -0,0 +1,12 @@
+async_test(t => {
+ const client = new XMLHttpRequest();
+ client.overrideMimeType('text/plain;charset=Shift-JIS');
+ client.onreadystatechange = t.step_func(() => {
+ if (client.readyState === 4) {
+ assert_equals( client.responseText, 'テスト' );
+ t.done();
+ }
+ });
+ client.open("GET", "resources/status.py?type="+encodeURIComponent('text/html;charset=iso-8859-1')+'&content=%83%65%83%58%83%67');
+ client.send( '' );
+}, "XMLHttpRequest: overrideMimeType() in unsent state, enforcing Shift-JIS encoding");
diff --git a/test/wpt/tests/xhr/preserve-ua-header-on-redirect.htm b/test/wpt/tests/xhr/preserve-ua-header-on-redirect.htm
new file mode 100644
index 0000000..fad883c
--- /dev/null
+++ b/test/wpt/tests/xhr/preserve-ua-header-on-redirect.htm
@@ -0,0 +1,43 @@
+<!doctype html>
+<html>
+ <head>
+ <title>XMLHttpRequest: User-Agent header is preserved on redirect</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ var test = async_test()
+ test.step(function() {
+ var client = new XMLHttpRequest()
+ client.onreadystatechange = function() {
+ test.step(function() {
+ if(client.readyState == 4) {
+ assert_equals(client.responseText, 'User-Agent: '+navigator.userAgent+'\n')
+ test.done()
+ }
+ })
+ }
+ client.open("POST", "resources/redirect.py?location="+encodeURIComponent("inspect-headers.py?filter_name=user-agent"))
+ client.send(null)
+ })
+
+ var test2 = async_test()
+ test2.step(function() {
+ var client = new XMLHttpRequest()
+ client.onreadystatechange = function() {
+ test2.step(function() {
+ if(client.readyState == 4) {
+ assert_equals(client.responseText, 'User-Agent: TEST\n')
+ test2.done()
+ }
+ })
+ }
+ client.open("POST", "resources/redirect.py?location="+encodeURIComponent("inspect-headers.py?filter_name=user-agent"))
+ client.setRequestHeader("User-Agent", "TEST")
+ client.send(null)
+ })
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/progress-events-response-data-gzip.htm b/test/wpt/tests/xhr/progress-events-response-data-gzip.htm
new file mode 100644
index 0000000..0580646
--- /dev/null
+++ b/test/wpt/tests/xhr/progress-events-response-data-gzip.htm
@@ -0,0 +1,83 @@
+<!doctype html>
+<html>
+ <head>
+ <title>XMLHttpRequest: progress events and GZIP encoding</title>
+ <meta name="timeout" content="long">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#firing-events-using-the-progressevent-interface-for-http" data-tested-assertations="following::p[contains(text(),'content-encodings')]" />
+ <!-- TODO: find better spec reference when https://www.w3.org/Bugs/Public/show_bug.cgi?id=25587 is fixed -->
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ var test = async_test()
+ test.step(function() {
+ var client = new XMLHttpRequest()
+ /*
+
+ Two behaviours are considered acceptable, so there are two ways to
+ pass this test
+
+ a) Set data for the compressed resource:
+ * event.total reflects the Content-length of the gzipp'ed resource
+ * event.loaded how many gzipped bytes have arrived over the wire so far
+ * lengthComputable is true
+
+ or
+
+ b) If the implementation does not provide progress details for the compressed
+ resource, set
+ * lengthComputable to false
+ * event.total to 0
+ * event.loaded to the number of bytes available so far after gzip decoding
+
+ Implications of this are tested here as follows:
+
+ * If lengthComputable is true:
+ * Event.total must match Content-length header
+ * event.loaded must only ever increase in progress events
+ (and may never repeat its value).
+ * event.loaded must never exceed the Content-length.
+
+ * If lengthComputable is false:
+ * event.total should be 0
+ * event.loaded must only ever increase in progress events
+ (and may never repeat its value).
+ * event.loaded should be the length of the decompressed content, i.e.
+ bigger than Content-length header value when finished loading
+
+ */
+ var lastTotal;
+ var lastLoaded = -1;
+ client.addEventListener('loadend', test.step_func(function(e){
+ var len = parseInt(client.getResponseHeader('content-length'), 10)
+ if(e.lengthComputable){
+ assert_equals(e.total, len, 'event.total is content-length')
+ assert_equals(e.loaded, len, 'event.loaded should be content-length at loadend')
+ }else{
+ assert_equals(e.total, 0, 'if implementation can\'t compute event.total for gzipped content it is 0')
+ assert_true(e.loaded >= len, 'event.loaded should be set even if total is not computable')
+ }
+ test.done();
+ }), false)
+ client.addEventListener('progress', test.step_func(function(e){
+ if(lastTotal === undefined){
+ lastTotal = e.total;
+ }
+ if(e.lengthComputable && e.total && e.loaded){
+ assert_equals(e.total, lastTotal, 'event.total should remain invariant')
+ assert_less_than_equal(e.loaded, lastTotal, 'event.loaded should not exceed content-length')
+ }else{
+ assert_equals(e.total, 0, 'event.total should be 0')
+ }
+ assert_greater_than(e.loaded, lastLoaded, 'event.loaded should only ever increase')
+ lastLoaded = e.loaded;
+ }), false)
+ // image.gif is 165375 bytes compressed. Sending 45000 bytes at a time with 1 second delay will load it in 4 seconds
+ client.open("GET", "resources/image.gif?pipe=gzip|trickle(45000:d1:r2)", true)
+ client.send()
+ })
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/progressevent-constructor.html b/test/wpt/tests/xhr/progressevent-constructor.html
new file mode 100644
index 0000000..0e771f4
--- /dev/null
+++ b/test/wpt/tests/xhr/progressevent-constructor.html
@@ -0,0 +1,47 @@
+<!doctype html>
+<title>ProgressEvent constructor</title>
+<link rel="help" href="https://xhr.spec.whatwg.org/#interface-progressevent">
+<link rel="help" href="https://dom.spec.whatwg.org/#concept-event-constructor">
+<link rel="help" href="https://dom.spec.whatwg.org/#interface-event">
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<div id=log></div>
+<script>
+test(function() {
+ var ev = new ProgressEvent("test")
+ assert_equals(ev.type, "test")
+ assert_equals(ev.target, null)
+ assert_equals(ev.currentTarget, null)
+ assert_equals(ev.eventPhase, Event.NONE)
+ assert_equals(ev.bubbles, false)
+ assert_equals(ev.cancelable, false)
+ assert_equals(ev.defaultPrevented, false)
+ assert_equals(ev.isTrusted, false)
+ assert_true(ev.timeStamp > 0)
+ assert_true("initEvent" in ev)
+ assert_equals(ev.lengthComputable, false)
+ assert_equals(ev.loaded, 0)
+ assert_equals(ev.total, 0)
+}, "Default event values.")
+test(function() {
+ var ev = new ProgressEvent("test")
+ assert_equals(ev["initProgressEvent"], undefined)
+}, "There must not be a initProgressEvent().")
+test(function() {
+ var ev = new ProgressEvent("I am an event", { type: "trololol", bubbles: true, cancelable: false})
+ assert_equals(ev.type, "I am an event")
+ assert_equals(ev.bubbles, true)
+ assert_equals(ev.cancelable, false)
+}, "Basic test.")
+test(function() {
+ var ev = new ProgressEvent(null, { lengthComputable: "hah", loaded: "2" })
+ assert_equals(ev.type, "null")
+ assert_equals(ev.lengthComputable, true)
+ assert_equals(ev.loaded, 2)
+}, "ECMAScript value conversion test.")
+test(function() {
+ var ev = new ProgressEvent("Xx", { lengthcomputable: true})
+ assert_equals(ev.type, "Xx")
+ assert_equals(ev.lengthComputable, false)
+}, "ProgressEventInit members must be matched case-sensitively.")
+</script>
diff --git a/test/wpt/tests/xhr/progressevent-interface.html b/test/wpt/tests/xhr/progressevent-interface.html
new file mode 100644
index 0000000..7552ff7
--- /dev/null
+++ b/test/wpt/tests/xhr/progressevent-interface.html
@@ -0,0 +1,49 @@
+<!DOCTYPE html>
+<title>The ProgressEvent interface</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+test(function() {
+ assert_equals(typeof ProgressEvent, "function")
+ assert_equals(ProgressEvent.length, 1)
+})
+test(function() {
+ var desc = Object.getOwnPropertyDescriptor(ProgressEvent, "prototype")
+ assert_equals(desc.value, ProgressEvent.prototype)
+ assert_equals(desc.writable, false)
+ assert_equals(desc.enumerable, false)
+ assert_equals(desc.configurable, false)
+ assert_throws_js(TypeError, function() {
+ "use strict";
+ delete ProgressEvent.prototype;
+ })
+ assert_equals(ProgressEvent.prototype.constructor, ProgressEvent)
+ assert_equals(Object.getPrototypeOf(ProgressEvent.prototype), Event.prototype)
+}, "interface prototype object")
+var attributes = [
+ ["boolean", "lengthComputable"],
+ ["unsigned long long", "loaded"],
+ ["unsigned long long", "total"]
+];
+attributes.forEach(function(a) {
+ test(function() {
+ var desc = Object.getOwnPropertyDescriptor(ProgressEvent.prototype, a[1])
+ assert_equals(desc.enumerable, true)
+ assert_equals(desc.configurable, true)
+ assert_throws_js(TypeError, function() {
+ ProgressEvent.prototype[a[1]]
+ })
+ })
+})
+test(function() {
+ for (var p in window) {
+ assert_not_equals(p, "ProgressEvent")
+ }
+}, "Interface objects properties should not be Enumerable")
+test(function() {
+ assert_true(!!window.ProgressEvent, "Interface should exist.")
+ assert_true(delete window.ProgressEvent, "The delete operator should return true.")
+ assert_equals(window.ProgressEvent, undefined, "Interface should be gone.")
+}, "Should be able to delete ProgressEvent.")
+</script>
diff --git a/test/wpt/tests/xhr/request-content-length.any.js b/test/wpt/tests/xhr/request-content-length.any.js
new file mode 100644
index 0000000..054d2cc
--- /dev/null
+++ b/test/wpt/tests/xhr/request-content-length.any.js
@@ -0,0 +1,31 @@
+async_test(test => {
+ const client = new XMLHttpRequest();
+ const data = "This is 22 bytes long.";
+ let happened = false;
+ client.upload.onprogress = test.step_func(e => {
+ assert_true(e.lengthComputable);
+ assert_equals(e.total, data.length);
+ happened = true;
+ });
+ client.onload = test.step_func_done(() => {
+ assert_true(happened);
+ assert_true(client.responseText.includes(`Content-Length: ${data.length}`));
+ });
+ client.open("POST", "resources/echo-headers.py");
+ client.send(data);
+}, "Uploads need to set the Content-Length header");
+
+async_test(test => {
+ const client = new XMLHttpRequest();
+ const data = "blah";
+ const url = URL.createObjectURL(new Blob([data]));
+ client.open("GET", url);
+ client.send();
+ client.onload = test.step_func_done(e => {
+ assert_true(e.lengthComputable);
+ assert_equals(e.total, data.length);
+ assert_equals(e.loaded, data.length);
+ assert_equals(client.responseText, data);
+ assert_equals(client.getResponseHeader("Content-Length"), String(data.length));
+ });
+}, "Fetched blob: URLs set the Content-Length header");
diff --git a/test/wpt/tests/xhr/resources/accept-language.py b/test/wpt/tests/xhr/resources/accept-language.py
new file mode 100644
index 0000000..b68cf35
--- /dev/null
+++ b/test/wpt/tests/xhr/resources/accept-language.py
@@ -0,0 +1,3 @@
+def main(request, response):
+ return [(b"Content-Type", b"text/plain"),
+ request.headers.get(b"Accept-Language", b"NO")]
diff --git a/test/wpt/tests/xhr/resources/accept.py b/test/wpt/tests/xhr/resources/accept.py
new file mode 100644
index 0000000..842df80
--- /dev/null
+++ b/test/wpt/tests/xhr/resources/accept.py
@@ -0,0 +1,2 @@
+def main(request, response):
+ return [(b"Content-Type", b"text/plain")], request.headers.get(b"accept", b"NO")
diff --git a/test/wpt/tests/xhr/resources/access-control-allow-lists.py b/test/wpt/tests/xhr/resources/access-control-allow-lists.py
new file mode 100644
index 0000000..32c11a0
--- /dev/null
+++ b/test/wpt/tests/xhr/resources/access-control-allow-lists.py
@@ -0,0 +1,26 @@
+import json
+
+from wptserve.utils import isomorphic_decode
+
+def main(request, response):
+ if b"origin" in request.GET:
+ response.headers.set(b"Access-Control-Allow-Origin", request.GET[b"origin"])
+ elif b"origins" in request.GET:
+ for origin in request.GET[b"origins"].split(b','):
+ response.headers.set(b"Access-Control-Allow-Origin", request.GET[b"origin"])
+
+ if b"headers" in request.GET:
+ response.headers.set(b"Access-Control-Allow-Headers", request.GET[b"headers"])
+ if b"methods" in request.GET:
+ response.headers.set(b"Access-Control-Allow-Methods", request.GET[b"methods"])
+
+ headers = dict(request.headers)
+
+ for header in headers:
+ headers[header] = headers[header][0]
+
+ str_headers = {}
+ for key, val in headers.items():
+ str_headers[isomorphic_decode(key)] = isomorphic_decode(val)
+
+ return json.dumps(str_headers)
diff --git a/test/wpt/tests/xhr/resources/access-control-allow-with-body.py b/test/wpt/tests/xhr/resources/access-control-allow-with-body.py
new file mode 100644
index 0000000..18564e2
--- /dev/null
+++ b/test/wpt/tests/xhr/resources/access-control-allow-with-body.py
@@ -0,0 +1,15 @@
+def main(request, response):
+ headers = {
+ b"Cache-Control": b"no-store",
+ b"Access-Control-Allow-Headers": b"X-Requested-With",
+ b"Access-Control-Max-Age": 0,
+ b"Access-Control-Allow-Origin": b"*",
+ b"Access-Control-Allow-Methods": b"*",
+ b"Vary": b"Accept-Encoding",
+ b"Content-Type": b"text/plain"
+ }
+
+ for (name, value) in headers.items():
+ response.headers.set(name, value)
+
+ response.content = b"PASS"
diff --git a/test/wpt/tests/xhr/resources/access-control-auth-basic.py b/test/wpt/tests/xhr/resources/access-control-auth-basic.py
new file mode 100644
index 0000000..d1c5579
--- /dev/null
+++ b/test/wpt/tests/xhr/resources/access-control-auth-basic.py
@@ -0,0 +1,17 @@
+def main(request, response):
+ response.headers.set(b"Cache-Control", b"no-store")
+ response.headers.set(b"Access-Control-Allow-Origin", request.headers.get(b"origin"))
+ response.headers.set(b"Access-Control-Allow-Credentials", b"true")
+ uid = request.GET.first(b"uid", None)
+
+ if request.method == u"OPTIONS":
+ response.headers.set(b"Access-Control-Allow-Methods", b"PUT")
+ else:
+ username = request.auth.username
+ password = request.auth.password
+ if (not username) or (username != uid):
+ response.headers.set(b"WWW-Authenticate", b"Basic realm='Test Realm/Cross Origin'")
+ response.status = 401
+ response.content = b"Authentication cancelled"
+ else:
+ response.content = b"User: " + username + b", Password: " + password
diff --git a/test/wpt/tests/xhr/resources/access-control-basic-allow-no-credentials.py b/test/wpt/tests/xhr/resources/access-control-basic-allow-no-credentials.py
new file mode 100644
index 0000000..e0668c6
--- /dev/null
+++ b/test/wpt/tests/xhr/resources/access-control-basic-allow-no-credentials.py
@@ -0,0 +1,5 @@
+def main(request, response):
+ response.headers.set(b"Content-Type", b"text/plain")
+ response.headers.set(b"Access-Control-Allow-Origin", request.headers.get(b"origin"))
+
+ response.content = b"PASS: Cross-domain access allowed."
diff --git a/test/wpt/tests/xhr/resources/access-control-basic-allow-star.py b/test/wpt/tests/xhr/resources/access-control-basic-allow-star.py
new file mode 100644
index 0000000..38c5107
--- /dev/null
+++ b/test/wpt/tests/xhr/resources/access-control-basic-allow-star.py
@@ -0,0 +1,5 @@
+def main(request, response):
+ response.headers.set(b"Content-Type", b"text/plain")
+ response.headers.set(b"Access-Control-Allow-Origin", b"*")
+
+ response.content = b"PASS: Cross-domain access allowed."
diff --git a/test/wpt/tests/xhr/resources/access-control-basic-allow.py b/test/wpt/tests/xhr/resources/access-control-basic-allow.py
new file mode 100644
index 0000000..f1ca587
--- /dev/null
+++ b/test/wpt/tests/xhr/resources/access-control-basic-allow.py
@@ -0,0 +1,6 @@
+def main(request, response):
+ response.headers.set(b"Content-Type", b"text/plain")
+ response.headers.set(b"Access-Control-Allow-Credentials", b"true")
+ response.headers.set(b"Access-Control-Allow-Origin", request.headers.get(b"origin"))
+
+ response.content = b"PASS: Cross-domain access allowed."
diff --git a/test/wpt/tests/xhr/resources/access-control-basic-cors-safelisted-request-headers.py b/test/wpt/tests/xhr/resources/access-control-basic-cors-safelisted-request-headers.py
new file mode 100644
index 0000000..46523a9
--- /dev/null
+++ b/test/wpt/tests/xhr/resources/access-control-basic-cors-safelisted-request-headers.py
@@ -0,0 +1,16 @@
+from wptserve.utils import isomorphic_decode
+
+def main(request, response):
+ response.headers.set(b"Cache-Control", b"no-store")
+
+ # This should be a simple request; deny preflight
+ if request.method != u"POST":
+ response.status = 400
+ return
+
+ response.headers.set(b"Access-Control-Allow-Credentials", b"true")
+ response.headers.set(b"Access-Control-Allow-Origin", request.headers.get(b"origin"))
+
+ for header in [b"Accept", b"Accept-Language", b"Content-Language", b"Content-Type"]:
+ value = request.headers.get(header)
+ response.content += isomorphic_decode(header) + u": " + (isomorphic_decode(value) if value else u"<None>") + u'\n'
diff --git a/test/wpt/tests/xhr/resources/access-control-basic-cors-safelisted-response-headers.py b/test/wpt/tests/xhr/resources/access-control-basic-cors-safelisted-response-headers.py
new file mode 100644
index 0000000..346b6b9
--- /dev/null
+++ b/test/wpt/tests/xhr/resources/access-control-basic-cors-safelisted-response-headers.py
@@ -0,0 +1,19 @@
+def main(request, response):
+ headers = {
+ # CORS-safelisted
+ b"content-type": b"text/plain",
+ b"cache-control": b"no cache",
+ b"content-language": b"en",
+ b"expires": b"Fri, 30 Oct 1998 14:19:41 GMT",
+ b"last-modified": b"Tue, 15 Nov 1994 12:45:26 GMT",
+ b"pragma": b"no-cache",
+
+ # Non-CORS-safelisted
+ b"x-test": b"foobar",
+
+ b"Access-Control-Allow-Origin": b"*"
+ }
+ for header in headers:
+ response.headers.set(header, headers[header])
+
+ response.content = b"PASS: Cross-domain access allowed."
diff --git a/test/wpt/tests/xhr/resources/access-control-basic-denied.py b/test/wpt/tests/xhr/resources/access-control-basic-denied.py
new file mode 100644
index 0000000..0d3964c
--- /dev/null
+++ b/test/wpt/tests/xhr/resources/access-control-basic-denied.py
@@ -0,0 +1,5 @@
+def main(request, response):
+ response.headers.set(b"Cache-Control", b"no-store")
+ response.headers.set(b"Content-Type", b"text/plain")
+
+ response.text = b"FAIL: Cross-domain access allowed."
diff --git a/test/wpt/tests/xhr/resources/access-control-basic-options-not-supported.py b/test/wpt/tests/xhr/resources/access-control-basic-options-not-supported.py
new file mode 100644
index 0000000..bb3f63e
--- /dev/null
+++ b/test/wpt/tests/xhr/resources/access-control-basic-options-not-supported.py
@@ -0,0 +1,12 @@
+def main(request, response):
+ response.headers.set(b"Cache-Control", b"no-store")
+
+ # Allow simple requests, but deny preflight
+ if request.method != u"OPTIONS":
+ if b"origin" in request.headers:
+ response.headers.set(b"Access-Control-Allow-Credentials", b"true")
+ response.headers.set(b"Access-Control-Allow-Origin", request.headers[b"origin"])
+ else:
+ response.status = 500
+ else:
+ response.status = 400
diff --git a/test/wpt/tests/xhr/resources/access-control-basic-preflight-cache-invalidation.py b/test/wpt/tests/xhr/resources/access-control-basic-preflight-cache-invalidation.py
new file mode 100644
index 0000000..f4f592d
--- /dev/null
+++ b/test/wpt/tests/xhr/resources/access-control-basic-preflight-cache-invalidation.py
@@ -0,0 +1,49 @@
+from wptserve.utils import isomorphic_encode
+
+def main(request, response):
+ def fail(message):
+ response.content = b"FAIL " + isomorphic_encode(request.method) + b": " + message
+
+ def getState(token):
+ server_state = request.server.stash.take(token)
+ if not server_state:
+ return b"Uninitialized"
+ return server_state
+
+ def setState(state, token):
+ request.server.stash.put(token, state)
+
+ response.headers.set(b"Access-Control-Allow-Origin", request.headers.get(b"origin"))
+ response.headers.set(b"Access-Control-Allow-Credentials", b"true")
+ token = request.GET.first(b"token", None)
+ state = getState(token)
+
+ if state == b"Uninitialized":
+ if request.method == u"OPTIONS":
+ response.headers.set(b"Access-Control-Allow-Methods", b"PUT")
+ response.headers.set(b"Access-Control-Max-Age", 10)
+ setState(b"OPTIONSSent", token)
+ else:
+ fail(state)
+ elif state == b"OPTIONSSent":
+ if request.method == u"PUT":
+ response.content = b"PASS: First PUT request."
+ setState(b"FirstPUTSent", token)
+ else:
+ fail(state)
+ elif state == b"FirstPUTSent":
+ if request.method == u"OPTIONS":
+ response.headers.set(b"Access-Control-Allow-Methods", b"PUT, XMETHOD")
+ response.headers.set(b"Access-Control-Allow-Headers", b"x-test")
+ setState(b"SecondOPTIONSSent", token)
+ elif request.method == u"PUT":
+ fail(b"Second PUT request sent without preflight")
+ else:
+ fail(state)
+ elif state == b"SecondOPTIONSSent":
+ if request.method == u"PUT" or request.method == u"XMETHOD":
+ response.content = b"PASS: Second OPTIONS request was sent."
+ else:
+ fail(state)
+ else:
+ fail(state)
diff --git a/test/wpt/tests/xhr/resources/access-control-basic-preflight-cache-timeout.py b/test/wpt/tests/xhr/resources/access-control-basic-preflight-cache-timeout.py
new file mode 100644
index 0000000..00c319f
--- /dev/null
+++ b/test/wpt/tests/xhr/resources/access-control-basic-preflight-cache-timeout.py
@@ -0,0 +1,50 @@
+from wptserve.utils import isomorphic_encode
+
+def main(request, response):
+ def fail(message):
+ response.content = b"FAIL " + isomorphic_encode(request.method) + b": " + message
+
+ def getState(token):
+ server_state = request.server.stash.take(token)
+ if not server_state:
+ return b"Uninitialized"
+ return server_state
+
+ def setState(state, token):
+ request.server.stash.put(token, state)
+
+ response.headers.set(b"Access-Control-Allow-Origin", request.headers.get(b"origin"))
+ response.headers.set(b"Access-Control-Allow-Credentials", b"true")
+ token = request.GET.first(b"token", None)
+ state = getState(token)
+
+ if state == b"Uninitialized":
+ if request.method == u"OPTIONS":
+ response.headers.set(b"Access-Control-Allow-Methods", b"PUT")
+ response.headers.set(b"Access-Control-Allow-Headers", b"x-test")
+ response.headers.set(b"Access-Control-Max-Age", 1)
+ setState(b"OPTIONSSent", token)
+ else:
+ fail(state)
+ elif state == b"OPTIONSSent":
+ if request.method == u"PUT":
+ response.content = b"PASS: First PUT request."
+ setState(b"FirstPUTSent", token)
+ else:
+ fail(state)
+ elif state == b"FirstPUTSent":
+ if request.method == u"OPTIONS":
+ response.headers.set(b"Access-Control-Allow-Methods", b"PUT")
+ response.headers.set(b"Access-Control-Allow-Headers", b"x-test")
+ setState(b"SecondOPTIONSSent", token)
+ elif request.method == u"PUT":
+ fail(b"Second PUT request sent without preflight")
+ else:
+ fail(state)
+ elif state == b"SecondOPTIONSSent":
+ if request.method == u"PUT":
+ response.content = b"PASS: Second OPTIONS request was sent."
+ else:
+ fail(state)
+ else:
+ fail(state)
diff --git a/test/wpt/tests/xhr/resources/access-control-basic-preflight-cache.py b/test/wpt/tests/xhr/resources/access-control-basic-preflight-cache.py
new file mode 100644
index 0000000..7a6bb60
--- /dev/null
+++ b/test/wpt/tests/xhr/resources/access-control-basic-preflight-cache.py
@@ -0,0 +1,50 @@
+
+from wptserve.utils import isomorphic_encode
+
+def main(request, response):
+ def fail(message):
+ response.content = b"FAIL " + isomorphic_encode(request.method) + b": " + message
+ response.status = 400
+
+ def getState(token):
+ server_state = request.server.stash.take(token)
+ if not server_state:
+ return b"Uninitialized"
+ return server_state
+
+ def setState(state, token):
+ request.server.stash.put(token, state)
+
+ response.headers.set(b"Access-Control-Allow-Origin", request.headers.get(b"origin"))
+ response.headers.set(b"Access-Control-Allow-Credentials", b"true")
+ token = request.GET.first(b"token", None)
+ state = getState(token)
+
+ if state == b"Uninitialized":
+ if request.method == u"OPTIONS":
+ response.headers.set(b"Access-Control-Allow-Methods", b"PUT")
+ response.headers.set(b"Access-Control-Max-Age", 10)
+ setState(b"OPTIONSSent", token)
+ else:
+ fail(state)
+ elif state == b"OPTIONSSent":
+ if request.method == u"PUT":
+ response.content = b"PASS: First PUT request."
+ setState(b"FirstPUTSent", token)
+ else:
+ fail(state)
+ elif state == b"FirstPUTSent":
+ if request.method == u"PUT":
+ response.content = b"PASS: Second PUT request. Preflight worked."
+ elif request.method == u"OPTIONS":
+ response.headers.set(b"Access-Control-Allow-Methods", b"PUT")
+ setState(b"FAILSecondOPTIONSSent", token)
+ else:
+ fail(state)
+ elif state == b"FAILSecondOPTIONSSent":
+ if request.method == u"PUT":
+ fail(b"Second OPTIONS request was sent. Preflight failed.")
+ else:
+ fail(state)
+ else:
+ fail(state)
diff --git a/test/wpt/tests/xhr/resources/access-control-basic-put-allow.py b/test/wpt/tests/xhr/resources/access-control-basic-put-allow.py
new file mode 100644
index 0000000..9b347bc
--- /dev/null
+++ b/test/wpt/tests/xhr/resources/access-control-basic-put-allow.py
@@ -0,0 +1,22 @@
+from wptserve.utils import isomorphic_encode
+
+def main(request, response):
+ if request.method == u"OPTIONS":
+ response.headers.set(b"Content-Type", b"text/plain")
+ response.headers.set(b"Access-Control-Allow-Credentials", b"true")
+ response.headers.set(b"Access-Control-Allow-Methods", b"PUT")
+ response.headers.set(b"Access-Control-Allow-Origin", request.headers.get(b"origin"))
+
+ elif request.method == u"PUT":
+ response.headers.set(b"Content-Type", b"text/plain")
+ response.headers.set(b"Access-Control-Allow-Credentials", b"true")
+ response.headers.set(b"Access-Control-Allow-Origin", request.headers.get(b"origin"))
+ response.content = b"PASS: Cross-domain access allowed."
+ try:
+ response.content += b"\n" + request.body
+ except:
+ response.content += b"Could not read in content."
+
+ else:
+ response.headers.set(b"Content-Type", b"text/plain")
+ response.content = b"Wrong method: " + isomorphic_encode(request.method)
diff --git a/test/wpt/tests/xhr/resources/access-control-cookie.py b/test/wpt/tests/xhr/resources/access-control-cookie.py
new file mode 100644
index 0000000..8c26475
--- /dev/null
+++ b/test/wpt/tests/xhr/resources/access-control-cookie.py
@@ -0,0 +1,16 @@
+import datetime
+
+def main(request, response):
+ cookie_name = request.GET.first(b"cookie_name", b"")
+
+ response.headers.set(b"Cache-Control", b"no-store")
+ response.headers.set(b"Access-Control-Allow-Origin", request.headers.get(b"origin"))
+ response.headers.set(b"Access-Control-Allow-Credentials", b"true")
+
+ for cookie in request.cookies:
+ # Set cookie to expire yesterday
+ response.set_cookie(cookie, b"deleted", expires=-datetime.timedelta(days=1))
+
+ if cookie_name:
+ # Set cookie to expire tomorrow
+ response.set_cookie(cookie_name, b"COOKIE", expires=datetime.timedelta(days=1))
diff --git a/test/wpt/tests/xhr/resources/access-control-origin-header.py b/test/wpt/tests/xhr/resources/access-control-origin-header.py
new file mode 100644
index 0000000..65a2c64
--- /dev/null
+++ b/test/wpt/tests/xhr/resources/access-control-origin-header.py
@@ -0,0 +1,8 @@
+def main(request, response):
+ response.headers.set(b"Content-Type", b"text/plain")
+ response.headers.set(b"Cache-Control", b"no-cache, no-store")
+ response.headers.set(b"Access-Control-Allow-External", b"true")
+ response.headers.set(b"Access-Control-Allow-Origin", b"*")
+
+ response.content = b"PASS: Cross-domain access allowed.\n"
+ response.content += b"HTTP_ORIGIN: " + request.headers.get(b"origin")
diff --git a/test/wpt/tests/xhr/resources/access-control-preflight-denied.py b/test/wpt/tests/xhr/resources/access-control-preflight-denied.py
new file mode 100644
index 0000000..1ec3037
--- /dev/null
+++ b/test/wpt/tests/xhr/resources/access-control-preflight-denied.py
@@ -0,0 +1,49 @@
+def main(request, response):
+ def fail(message):
+ response.content = b"FAIL: " + message
+ response.status = 400
+
+ def getState(token):
+ server_state = request.server.stash.take(token)
+ if not server_state:
+ return b"Uninitialized"
+ return server_state
+
+ def setState(token, state):
+ request.server.stash.put(token, state)
+
+ def resetState(token):
+ setState(token, b"")
+
+ response.headers.set(b"Cache-Control", b"no-store")
+ response.headers.set(b"Access-Control-Allow-Origin", request.headers.get(b"origin"))
+ response.headers.set(b"Access-Control-Max-Age", 1)
+ token = request.GET.first(b"token", None)
+ state = getState(token)
+ command = request.GET.first(b"command", None)
+
+ if command == b"reset":
+ if request.method == u"GET":
+ resetState(token)
+ response.content = b"Server state reset"
+ else:
+ fail(b"Invalid Method.")
+ elif state == b"Uninitialized":
+ if request.method == u"OPTIONS":
+ response.content = b"This request should not be displayed."
+ setState(token, b"Denied")
+ else:
+ fail(state)
+ elif state == b"Denied":
+ if request.method == u"GET" and command == b"complete":
+ resetState(token)
+ response.content = b"Request successfully blocked."
+ else:
+ setState(token, b"Deny Ignored")
+ fail(b"The request was not denied.")
+ elif state == b"Deny Ignored":
+ resetState(token)
+ fail(state)
+ else:
+ resetState(token)
+ fail(b"Unknown Error.")
diff --git a/test/wpt/tests/xhr/resources/access-control-preflight-request-allow-headers-returns-star.py b/test/wpt/tests/xhr/resources/access-control-preflight-request-allow-headers-returns-star.py
new file mode 100644
index 0000000..87d3616
--- /dev/null
+++ b/test/wpt/tests/xhr/resources/access-control-preflight-request-allow-headers-returns-star.py
@@ -0,0 +1,12 @@
+def main(request, response):
+ if request.method == "OPTIONS":
+ response.headers.set(b"Access-Control-Allow-Origin", b"*")
+ response.headers.set(b"Access-Control-Allow-Headers", b"*")
+ response.status = 200
+ elif request.method == "GET":
+ response.headers.set(b"Access-Control-Allow-Origin", b"*")
+ if request.headers.get(b"X-Test"):
+ response.headers.set(b"Content-Type", b"text/plain")
+ response.content = b"PASS"
+ else:
+ response.status = 400
diff --git a/test/wpt/tests/xhr/resources/access-control-preflight-request-header-lowercase.py b/test/wpt/tests/xhr/resources/access-control-preflight-request-header-lowercase.py
new file mode 100644
index 0000000..e77fc9a
--- /dev/null
+++ b/test/wpt/tests/xhr/resources/access-control-preflight-request-header-lowercase.py
@@ -0,0 +1,16 @@
+def main(request, response):
+ response.headers.set(b"Cache-Control", b"no-store")
+ response.headers.set(b"Access-Control-Allow-Origin", b"*")
+ response.headers.set(b"Access-Control-Max-Age", 0)
+
+ if request.method == u"OPTIONS":
+ if b"x-test" in [header.strip(b" ") for header in
+ request.headers.get(b"Access-Control-Request-Headers").split(b",")]:
+ response.headers.set(b"Access-Control-Allow-Headers", b"X-Test")
+ else:
+ response.status = 400
+ elif request.method == u"GET":
+ if request.headers.get(b"X-Test"):
+ response.content = b"PASS"
+ else:
+ response.status = 400
diff --git a/test/wpt/tests/xhr/resources/access-control-preflight-request-header-returns-origin.py b/test/wpt/tests/xhr/resources/access-control-preflight-request-header-returns-origin.py
new file mode 100644
index 0000000..d2c6abe
--- /dev/null
+++ b/test/wpt/tests/xhr/resources/access-control-preflight-request-header-returns-origin.py
@@ -0,0 +1,12 @@
+def main(request, response):
+ if request.method == "OPTIONS":
+ response.headers.set(b"Access-Control-Allow-Origin", request.headers.get(b"origin"))
+ response.headers.set(b"Access-Control-Allow-Headers", b"X-Test")
+ response.status = 200
+ elif request.method == "GET":
+ response.headers.set(b"Access-Control-Allow-Origin", b"*")
+ if request.headers.get(b"X-Test"):
+ response.headers.set(b"Content-Type", b"text/plain")
+ response.content = "PASS"
+ else:
+ response.status = 400
diff --git a/test/wpt/tests/xhr/resources/access-control-preflight-request-header-sorted.py b/test/wpt/tests/xhr/resources/access-control-preflight-request-header-sorted.py
new file mode 100644
index 0000000..4e708a9
--- /dev/null
+++ b/test/wpt/tests/xhr/resources/access-control-preflight-request-header-sorted.py
@@ -0,0 +1,18 @@
+def main(request, response):
+ response.headers.set(b'Cache-Control', b'no-store')
+ response.headers.set(b'Access-Control-Allow-Origin',
+ request.headers.get(b'origin'))
+
+ headers = b'x-custom-s,x-custom-test,x-custom-u,x-custom-ua,x-custom-v'
+ if request.method == u'OPTIONS':
+ response.headers.set(b'Access-Control-Max-Age', b'0')
+ response.headers.set(b'Access-Control-Allow-Headers', headers)
+ # Access-Control-Request-Headers should be sorted.
+ if headers != request.headers.get(b'Access-Control-Request-Headers'):
+ response.status = 400
+ else:
+ if request.headers.get(b'x-custom-s'):
+ response.content = b'PASS'
+ else:
+ response.status = 400
+ response.content = b'FAIL'
diff --git a/test/wpt/tests/xhr/resources/access-control-preflight-request-headers-origin.py b/test/wpt/tests/xhr/resources/access-control-preflight-request-headers-origin.py
new file mode 100644
index 0000000..b8fb220
--- /dev/null
+++ b/test/wpt/tests/xhr/resources/access-control-preflight-request-headers-origin.py
@@ -0,0 +1,12 @@
+def main(request, response):
+ response.headers.set(b"Cache-Control", b"no-store")
+ response.headers.set(b"Access-Control-Allow-Origin", b"*")
+
+ if request.method == u"OPTIONS":
+ if b"origin" in request.headers.get(b"Access-Control-Request-Headers").lower():
+ response.status = 400
+ response.content = b"Error: 'origin' included in Access-Control-Request-Headers"
+ else:
+ response.headers.set(b"Access-Control-Allow-Headers", b"x-pass")
+ else:
+ response.content = request.headers.get(b"x-pass")
diff --git a/test/wpt/tests/xhr/resources/access-control-preflight-request-invalid-status.py b/test/wpt/tests/xhr/resources/access-control-preflight-request-invalid-status.py
new file mode 100644
index 0000000..2a59059
--- /dev/null
+++ b/test/wpt/tests/xhr/resources/access-control-preflight-request-invalid-status.py
@@ -0,0 +1,16 @@
+def main(request, response):
+ try:
+ code = int(request.GET.first(b"code", None))
+ except:
+ code = None
+
+ if request.method == u"OPTIONS":
+ if code:
+ response.status = code
+ response.headers.set(b"Access-Control-Max-Age", 1)
+ response.headers.set(b"Access-Control-Allow-Headers", b"x-pass")
+ else:
+ response.status = 200
+
+ response.headers.set(b"Cache-Control", b"no-store")
+ response.headers.set(b"Access-Control-Allow-Origin", request.headers.get(b"origin"))
diff --git a/test/wpt/tests/xhr/resources/access-control-preflight-request-must-not-contain-cookie.py b/test/wpt/tests/xhr/resources/access-control-preflight-request-must-not-contain-cookie.py
new file mode 100644
index 0000000..89a0451
--- /dev/null
+++ b/test/wpt/tests/xhr/resources/access-control-preflight-request-must-not-contain-cookie.py
@@ -0,0 +1,12 @@
+def main(request, response):
+ if request.method == u"OPTIONS" and request.cookies.get(b"foo"):
+ response.status = 400
+ else:
+ response.headers.set(b"Cache-Control", b"no-store")
+ response.headers.set(b"Access-Control-Allow-Origin", request.headers.get(b"origin"))
+ response.headers.set(b"Access-Control-Allow-Credentials", b"true")
+ response.headers.set(b"Access-Control-Allow-Headers", b"X-Proprietary-Header")
+ response.headers.set(b"Connection", b"close")
+
+ if request.cookies.get(b"foo"):
+ response.content = request.cookies[b"foo"].value
diff --git a/test/wpt/tests/xhr/resources/access-control-sandboxed-iframe.html b/test/wpt/tests/xhr/resources/access-control-sandboxed-iframe.html
new file mode 100644
index 0000000..7e47275
--- /dev/null
+++ b/test/wpt/tests/xhr/resources/access-control-sandboxed-iframe.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<html>
+ <body>
+ <script type="text/javascript">
+window.addEventListener("message", (evt) => {
+ const url = evt.data;
+ const xhr = new XMLHttpRequest;
+
+ xhr.open("GET", url, false);
+
+ try {
+ xhr.send();
+ } catch(e) {
+ parent.postMessage("Exception thrown. Sandboxed iframe XHR access was denied in 'send'.", "*");
+ return;
+ }
+
+ parent.postMessage(xhr.responseText, "*");
+}, false);
+
+parent.postMessage("ready", "*");
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/resources/auth1/auth.py b/test/wpt/tests/xhr/resources/auth1/auth.py
new file mode 100644
index 0000000..db4f7bc
--- /dev/null
+++ b/test/wpt/tests/xhr/resources/auth1/auth.py
@@ -0,0 +1,12 @@
+import imp
+import os
+
+from wptserve.utils import isomorphic_decode
+
+here = os.path.dirname(os.path.abspath(isomorphic_decode(__file__)))
+
+def main(request, response):
+ auth = imp.load_source(u"", os.path.join(here,
+ u"..",
+ u"authentication.py"))
+ return auth.main(request, response)
diff --git a/test/wpt/tests/xhr/resources/auth10/auth.py b/test/wpt/tests/xhr/resources/auth10/auth.py
new file mode 100644
index 0000000..db4f7bc
--- /dev/null
+++ b/test/wpt/tests/xhr/resources/auth10/auth.py
@@ -0,0 +1,12 @@
+import imp
+import os
+
+from wptserve.utils import isomorphic_decode
+
+here = os.path.dirname(os.path.abspath(isomorphic_decode(__file__)))
+
+def main(request, response):
+ auth = imp.load_source(u"", os.path.join(here,
+ u"..",
+ u"authentication.py"))
+ return auth.main(request, response)
diff --git a/test/wpt/tests/xhr/resources/auth11/auth.py b/test/wpt/tests/xhr/resources/auth11/auth.py
new file mode 100644
index 0000000..db4f7bc
--- /dev/null
+++ b/test/wpt/tests/xhr/resources/auth11/auth.py
@@ -0,0 +1,12 @@
+import imp
+import os
+
+from wptserve.utils import isomorphic_decode
+
+here = os.path.dirname(os.path.abspath(isomorphic_decode(__file__)))
+
+def main(request, response):
+ auth = imp.load_source(u"", os.path.join(here,
+ u"..",
+ u"authentication.py"))
+ return auth.main(request, response)
diff --git a/test/wpt/tests/xhr/resources/auth2/auth.py b/test/wpt/tests/xhr/resources/auth2/auth.py
new file mode 100644
index 0000000..db4f7bc
--- /dev/null
+++ b/test/wpt/tests/xhr/resources/auth2/auth.py
@@ -0,0 +1,12 @@
+import imp
+import os
+
+from wptserve.utils import isomorphic_decode
+
+here = os.path.dirname(os.path.abspath(isomorphic_decode(__file__)))
+
+def main(request, response):
+ auth = imp.load_source(u"", os.path.join(here,
+ u"..",
+ u"authentication.py"))
+ return auth.main(request, response)
diff --git a/test/wpt/tests/xhr/resources/auth2/corsenabled.py b/test/wpt/tests/xhr/resources/auth2/corsenabled.py
new file mode 100644
index 0000000..bec6687
--- /dev/null
+++ b/test/wpt/tests/xhr/resources/auth2/corsenabled.py
@@ -0,0 +1,18 @@
+import imp
+import os
+
+from wptserve.utils import isomorphic_decode
+
+here = os.path.dirname(isomorphic_decode(__file__))
+
+def main(request, response):
+ response.headers.set(b'Access-Control-Allow-Origin', request.headers.get(b"origin"))
+ response.headers.set(b'Access-Control-Allow-Credentials', b'true')
+ response.headers.set(b'Access-Control-Allow-Methods', b'GET')
+ response.headers.set(b'Access-Control-Allow-Headers', b'authorization, x-user, x-pass')
+ response.headers.set(b'Access-Control-Expose-Headers', b'x-challenge, xhr-user, ses-user')
+ auth = imp.load_source(u"", os.path.abspath(os.path.join(here, os.pardir, u"authentication.py")))
+ if request.method == u"OPTIONS":
+ return b""
+ else:
+ return auth.main(request, response)
diff --git a/test/wpt/tests/xhr/resources/auth3/auth.py b/test/wpt/tests/xhr/resources/auth3/auth.py
new file mode 100644
index 0000000..db4f7bc
--- /dev/null
+++ b/test/wpt/tests/xhr/resources/auth3/auth.py
@@ -0,0 +1,12 @@
+import imp
+import os
+
+from wptserve.utils import isomorphic_decode
+
+here = os.path.dirname(os.path.abspath(isomorphic_decode(__file__)))
+
+def main(request, response):
+ auth = imp.load_source(u"", os.path.join(here,
+ u"..",
+ u"authentication.py"))
+ return auth.main(request, response)
diff --git a/test/wpt/tests/xhr/resources/auth4/auth.py b/test/wpt/tests/xhr/resources/auth4/auth.py
new file mode 100644
index 0000000..db4f7bc
--- /dev/null
+++ b/test/wpt/tests/xhr/resources/auth4/auth.py
@@ -0,0 +1,12 @@
+import imp
+import os
+
+from wptserve.utils import isomorphic_decode
+
+here = os.path.dirname(os.path.abspath(isomorphic_decode(__file__)))
+
+def main(request, response):
+ auth = imp.load_source(u"", os.path.join(here,
+ u"..",
+ u"authentication.py"))
+ return auth.main(request, response)
diff --git a/test/wpt/tests/xhr/resources/auth5/auth.py b/test/wpt/tests/xhr/resources/auth5/auth.py
new file mode 100644
index 0000000..43b376e
--- /dev/null
+++ b/test/wpt/tests/xhr/resources/auth5/auth.py
@@ -0,0 +1,15 @@
+from wptserve.utils import isomorphic_decode
+
+def main(request, response):
+ if request.auth.username == b'usr' and request.auth.password == b'secret':
+ response.headers.set(b'Content-type', b'text/plain')
+ content = b""
+ else:
+ response.status = 401
+ response.headers.set(b'Status', b'401 Authorization required')
+ response.headers.set(b'WWW-Authenticate', b'Basic realm="test"')
+ content = b'User name/password wrong or not given: '
+
+ content += b"%s\n%s" % (request.auth.username or b'',
+ request.auth.password or b'')
+ return content
diff --git a/test/wpt/tests/xhr/resources/auth6/auth.py b/test/wpt/tests/xhr/resources/auth6/auth.py
new file mode 100644
index 0000000..e18319e
--- /dev/null
+++ b/test/wpt/tests/xhr/resources/auth6/auth.py
@@ -0,0 +1,15 @@
+from wptserve.utils import isomorphic_decode
+
+def main(request, response):
+ if request.auth.username == b'usr' and request.auth.password == b'secret':
+ response.headers.set(b'Content-type', b'text/plain')
+ content = b""
+ else:
+ response.status = 401
+ response.headers.set(b'Status', b'401 Authorization required')
+ response.headers.set(b'WWW-Authenticate', b'Basic realm="test"')
+ content = b'User name/password wrong or not given: '
+
+ content += b"%s\n%s" % (request.auth.username,
+ request.auth.password)
+ return content
diff --git a/test/wpt/tests/xhr/resources/auth7/corsenabled.py b/test/wpt/tests/xhr/resources/auth7/corsenabled.py
new file mode 100644
index 0000000..7a06062
--- /dev/null
+++ b/test/wpt/tests/xhr/resources/auth7/corsenabled.py
@@ -0,0 +1,20 @@
+import imp
+import os
+
+from wptserve.utils import isomorphic_decode
+
+here = os.path.dirname(isomorphic_decode(__file__))
+
+def main(request, response):
+ response.headers.set(b'Access-Control-Allow-Origin', request.headers.get(b"origin"))
+ response.headers.set(b'Access-Control-Allow-Credentials', b'true')
+ response.headers.set(b'Access-Control-Allow-Methods', b'GET')
+ response.headers.set(b'Access-Control-Allow-Headers', b'authorization, x-user, x-pass')
+ response.headers.set(b'Access-Control-Expose-Headers', b'x-challenge, xhr-user, ses-user')
+ auth = imp.load_source(u"", os.path.join(here,
+ os.pardir,
+ u"authentication.py"))
+ if request.method == u"OPTIONS":
+ return b""
+ else:
+ return auth.main(request, response)
diff --git a/test/wpt/tests/xhr/resources/auth8/corsenabled-no-authorize.py b/test/wpt/tests/xhr/resources/auth8/corsenabled-no-authorize.py
new file mode 100644
index 0000000..af8e7c4
--- /dev/null
+++ b/test/wpt/tests/xhr/resources/auth8/corsenabled-no-authorize.py
@@ -0,0 +1,20 @@
+import imp
+import os
+
+from wptserve.utils import isomorphic_decode
+
+here = os.path.dirname(isomorphic_decode(__file__))
+
+def main(request, response):
+ response.headers.set(b'Access-Control-Allow-Origin', request.headers.get(b"origin"))
+ response.headers.set(b'Access-Control-Allow-Credentials', b'true')
+ response.headers.set(b'Access-Control-Allow-Methods', b'GET')
+ response.headers.set(b'Access-Control-Allow-Headers', b'x-user, x-pass')
+ response.headers.set(b'Access-Control-Expose-Headers', b'x-challenge, xhr-user, ses-user')
+ auth = imp.load_source(u"", os.path.join(here,
+ os.pardir,
+ u"authentication.py"))
+ if request.method == u"OPTIONS":
+ return b""
+ else:
+ return auth.main(request, response)
diff --git a/test/wpt/tests/xhr/resources/auth9/auth.py b/test/wpt/tests/xhr/resources/auth9/auth.py
new file mode 100644
index 0000000..db4f7bc
--- /dev/null
+++ b/test/wpt/tests/xhr/resources/auth9/auth.py
@@ -0,0 +1,12 @@
+import imp
+import os
+
+from wptserve.utils import isomorphic_decode
+
+here = os.path.dirname(os.path.abspath(isomorphic_decode(__file__)))
+
+def main(request, response):
+ auth = imp.load_source(u"", os.path.join(here,
+ u"..",
+ u"authentication.py"))
+ return auth.main(request, response)
diff --git a/test/wpt/tests/xhr/resources/authentication.py b/test/wpt/tests/xhr/resources/authentication.py
new file mode 100644
index 0000000..50b5c80
--- /dev/null
+++ b/test/wpt/tests/xhr/resources/authentication.py
@@ -0,0 +1,24 @@
+def main(request, response):
+ session_user = request.auth.username
+ session_pass = request.auth.password
+ expected_user_name = request.headers.get(b"X-User", None)
+
+ token = expected_user_name
+ if session_user is None and session_pass is None:
+ if token is not None and request.server.stash.take(token) is not None:
+ return b'FAIL (did not authorize)'
+ else:
+ if token is not None:
+ request.server.stash.put(token, b"1")
+ status = (401, b'Unauthorized')
+ headers = [(b'WWW-Authenticate', b'Basic realm="test"')]
+ return status, headers, b'FAIL (should be transparent)'
+ else:
+ if request.server.stash.take(token) == b"1":
+ challenge = b"DID"
+ else:
+ challenge = b"DID-NOT"
+ headers = [(b'XHR-USER', expected_user_name),
+ (b'SES-USER', session_user),
+ (b"X-challenge", challenge)]
+ return headers, session_user + b"\n" + session_pass
diff --git a/test/wpt/tests/xhr/resources/bad-chunk-encoding.py b/test/wpt/tests/xhr/resources/bad-chunk-encoding.py
new file mode 100644
index 0000000..7ed0ad4
--- /dev/null
+++ b/test/wpt/tests/xhr/resources/bad-chunk-encoding.py
@@ -0,0 +1,17 @@
+import time
+
+def main(request, response):
+ delay = 0.1
+ count = 5
+ time.sleep(delay)
+ response.headers.set(b"Transfer-Encoding", b"chunked")
+ response.headers.set(b"Content-Type", b"text/plain")
+ response.headers.set(b"X-Content-Type-Options", b"nosniff")
+ response.headers.set(b"Connection", b"close")
+ response.close_connection = True
+ response.write_status_headers()
+ time.sleep(delay)
+ for i in range(count):
+ response.writer.write_content(b"a\r\nTEST_CHUNK\r\n")
+ time.sleep(delay)
+ response.writer.write_content(b"garbage")
diff --git a/test/wpt/tests/xhr/resources/base.xml b/test/wpt/tests/xhr/resources/base.xml
new file mode 100644
index 0000000..ed01aec
--- /dev/null
+++ b/test/wpt/tests/xhr/resources/base.xml
@@ -0,0 +1 @@
+<base xmlns="http://www.w3.org/1999/xhtml" href="https://example.com/"/>
diff --git a/test/wpt/tests/xhr/resources/chunked.py b/test/wpt/tests/xhr/resources/chunked.py
new file mode 100644
index 0000000..6f67d90
--- /dev/null
+++ b/test/wpt/tests/xhr/resources/chunked.py
@@ -0,0 +1,17 @@
+def main(request, response):
+ chunks = [b"First chunk\r\n",
+ b"Second chunk\r\n",
+ b"Yet another (third) chunk\r\n",
+ b"Yet another (fourth) chunk\r\n",
+ ]
+ response.headers.set(b"Transfer-Encoding", b"chunked")
+ response.headers.set(b"Trailer", b"X-Test-Me")
+ response.headers.set(b"Content-Type", b"text/plain")
+ response.write_status_headers()
+
+ for value in chunks:
+ response.writer.write(b"%x\r\n" % len(value))
+ response.writer.write(value)
+ response.writer.write(b"\r\n")
+ response.writer.write(b"0\r\n")
+ response.writer.write(b"X-Test-Me: Trailer header value\r\n\r\n")
diff --git a/test/wpt/tests/xhr/resources/conditional.py b/test/wpt/tests/xhr/resources/conditional.py
new file mode 100644
index 0000000..2c24830
--- /dev/null
+++ b/test/wpt/tests/xhr/resources/conditional.py
@@ -0,0 +1,29 @@
+def main(request, response):
+ tag = request.GET.first(b"tag", None)
+ match = request.headers.get(b"If-None-Match", None)
+ date = request.GET.first(b"date", b"")
+ modified = request.headers.get(b"If-Modified-Since", None)
+ cors = request.GET.first(b"cors", None)
+
+ if request.method == u"OPTIONS":
+ response.headers.set(b"Access-Control-Allow-Origin", b"*")
+ response.headers.set(b"Access-Control-Allow-Headers", b"IF-NONE-MATCH")
+ return b""
+
+ if tag:
+ response.headers.set(b"ETag", b'"%s"' % tag)
+ elif date:
+ response.headers.set(b"Last-Modified", date)
+
+ if cors:
+ response.headers.set(b"Access-Control-Allow-Origin", b"*")
+
+ if ((match is not None and match == tag) or
+ (modified is not None and modified == date)):
+ response.status = (304, b"SUPERCOOL")
+ return b""
+ else:
+ if not cors:
+ response.headers.set(b"Access-Control-Allow-Origin", b"*")
+ response.headers.set(b"Content-Type", b"text/plain")
+ return b"MAYBE NOT"
diff --git a/test/wpt/tests/xhr/resources/content.py b/test/wpt/tests/xhr/resources/content.py
new file mode 100644
index 0000000..764ca36
--- /dev/null
+++ b/test/wpt/tests/xhr/resources/content.py
@@ -0,0 +1,20 @@
+from wptserve.utils import isomorphic_encode
+
+def main(request, response):
+ response_ctype = b''
+
+ if b"response_charset_label" in request.GET:
+ response_ctype = b";charset=" + request.GET.first(b"response_charset_label")
+
+ headers = [(b"Content-type", b"text/plain" + response_ctype),
+ (b"X-Request-Method", isomorphic_encode(request.method)),
+ (b"X-Request-Query", isomorphic_encode(request.url_parts.query) if request.url_parts.query else b"NO"),
+ (b"X-Request-Content-Length", request.headers.get(b"Content-Length", b"NO")),
+ (b"X-Request-Content-Type", request.headers.get(b"Content-Type", b"NO"))]
+
+ if b"content" in request.GET:
+ content = request.GET.first(b"content")
+ else:
+ content = request.body
+
+ return headers, content
diff --git a/test/wpt/tests/xhr/resources/corsenabled.py b/test/wpt/tests/xhr/resources/corsenabled.py
new file mode 100644
index 0000000..aae19f1
--- /dev/null
+++ b/test/wpt/tests/xhr/resources/corsenabled.py
@@ -0,0 +1,25 @@
+import time
+
+from wptserve.utils import isomorphic_encode
+
+def main(request, response):
+ headers = [(b"Access-Control-Allow-Origin", b"*"),
+ (b"Access-Control-Allow-Credentials", b"true"),
+ (b"Access-Control-Allow-Methods", b"GET, POST, PUT, FOO"),
+ (b"Access-Control-Allow-Headers", b"x-test, x-foo"),
+ (b"Access-Control-Expose-Headers", b"x-request-method, x-request-content-type, x-request-query, x-request-content-length, x-request-data")]
+
+ if b"delay" in request.GET:
+ delay = int(request.GET.first(b"delay"))
+ time.sleep(delay)
+
+ if b"safelist_content_type" in request.GET:
+ headers.append((b"Access-Control-Allow-Headers", b"content-type"))
+
+ headers.append((b"X-Request-Method", isomorphic_encode(request.method)))
+ headers.append((b"X-Request-Query", isomorphic_encode(request.url_parts.query) if request.url_parts.query else b"NO"))
+ headers.append((b"X-Request-Content-Length", request.headers.get(b"Content-Length", b"NO")))
+ headers.append((b"X-Request-Content-Type", request.headers.get(b"Content-Type", b"NO")))
+ headers.append((b"X-Request-Data", request.body))
+
+ return headers, b"Test"
diff --git a/test/wpt/tests/xhr/resources/delay.py b/test/wpt/tests/xhr/resources/delay.py
new file mode 100644
index 0000000..61a0ed2
--- /dev/null
+++ b/test/wpt/tests/xhr/resources/delay.py
@@ -0,0 +1,7 @@
+import time
+
+def main(request, response):
+ delay = float(request.GET.first(b"ms", 500))
+ time.sleep(delay / 1E3)
+
+ return [(b"Access-Control-Allow-Origin", b"*"), (b"Access-Control-Allow-Methods", b"YO"), (b"Content-type", b"text/plain")], b"TEST_DELAY"
diff --git a/test/wpt/tests/xhr/resources/echo-content-cors.py b/test/wpt/tests/xhr/resources/echo-content-cors.py
new file mode 100644
index 0000000..4ffb3e7
--- /dev/null
+++ b/test/wpt/tests/xhr/resources/echo-content-cors.py
@@ -0,0 +1,23 @@
+def main(request, response):
+ headers = [(b"X-Request-Method", request.method),
+ (b"X-Request-Content-Length", request.headers.get(b"Content-Length", b"NO")),
+ (b"X-Request-Content-Type", request.headers.get(b"Content-Type", b"NO")),
+ (b"Access-Control-Allow-Credentials", b"true"),
+ # Avoid any kind of content sniffing on the response.
+ (b"Content-Type", b"text/plain")]
+
+ origin = request.GET.first(b"origin", request.headers.get(b"origin"))
+ if origin != None:
+ headers.append((b"Access-Control-Allow-Origin", origin))
+
+ request_headers = request.GET.first(b"origin", request.headers.get(b"access-control-request-headers"))
+ if request_headers != None:
+ headers.append((b"Access-Control-Allow-Headers", request_headers))
+
+ request_method = request.GET.first(b"origin", request.headers.get(b"access-control-request-method"))
+ if request_method != None:
+ headers.append((b"Access-Control-Allow-Methods", b"OPTIONS, " + request_method))
+
+ content = request.body
+
+ return headers, content
diff --git a/test/wpt/tests/xhr/resources/echo-content-type.py b/test/wpt/tests/xhr/resources/echo-content-type.py
new file mode 100644
index 0000000..595c88f
--- /dev/null
+++ b/test/wpt/tests/xhr/resources/echo-content-type.py
@@ -0,0 +1,6 @@
+def main(request, response):
+ response.headers.set(b"Content-Type", b"text/plain")
+ response.headers.set(b"Connection", b"close")
+ response.status = 200
+ response.content = request.headers.get(b"Content-Type")
+ response.close_connection = True
diff --git a/test/wpt/tests/xhr/resources/echo-headers.py b/test/wpt/tests/xhr/resources/echo-headers.py
new file mode 100644
index 0000000..58a7ed5
--- /dev/null
+++ b/test/wpt/tests/xhr/resources/echo-headers.py
@@ -0,0 +1,7 @@
+def main(request, response):
+ response.writer.write_status(200)
+ response.writer.write_header(b"Content-Type", b"text/plain")
+ response.writer.write_header(b"Connection", b"close")
+ response.writer.end_headers()
+ response.writer.write(str(request.raw_headers))
+ response.close_connection = True
diff --git a/test/wpt/tests/xhr/resources/echo-method.py b/test/wpt/tests/xhr/resources/echo-method.py
new file mode 100644
index 0000000..564c3db
--- /dev/null
+++ b/test/wpt/tests/xhr/resources/echo-method.py
@@ -0,0 +1,16 @@
+# This handler is designed to verify that UAs correctly discard the body of
+# responses to HTTP HEAD requests. If the response body is written to a
+# separate TCP packet, then this behavior cannot be verified. This handler uses
+# the response writer to ensure that the body is transmitted in the same packet
+# as the headers. In this way, non-conforming UAs will consistently fail the
+# associated test.
+
+def main(request, response):
+ content = request.method
+
+ response.add_required_headers = False
+ response.writer.write(u'''HTTP/1.1 200 OK
+Content-type: text/plain
+Content-Length: {}
+
+{}'''.format(len(content), content))
diff --git a/test/wpt/tests/xhr/resources/empty-div-utf8-html.py b/test/wpt/tests/xhr/resources/empty-div-utf8-html.py
new file mode 100644
index 0000000..5d4c936
--- /dev/null
+++ b/test/wpt/tests/xhr/resources/empty-div-utf8-html.py
@@ -0,0 +1,5 @@
+def main(request, response):
+ headers = [(b"Content-type", b"text/html;charset=utf-8")]
+ content = b"<!DOCTYPE html><div></div>"
+
+ return headers, content
diff --git a/test/wpt/tests/xhr/resources/folder.txt b/test/wpt/tests/xhr/resources/folder.txt
new file mode 100644
index 0000000..fef12e2
--- /dev/null
+++ b/test/wpt/tests/xhr/resources/folder.txt
@@ -0,0 +1 @@
+bottom
diff --git a/test/wpt/tests/xhr/resources/form.py b/test/wpt/tests/xhr/resources/form.py
new file mode 100644
index 0000000..c592943
--- /dev/null
+++ b/test/wpt/tests/xhr/resources/form.py
@@ -0,0 +1,2 @@
+def main(request, response):
+ return b"id:%s;value:%s;" % (request.POST.first(b"id"), request.POST.first(b"value"))
diff --git a/test/wpt/tests/xhr/resources/get-set-cookie.py b/test/wpt/tests/xhr/resources/get-set-cookie.py
new file mode 100644
index 0000000..234280d
--- /dev/null
+++ b/test/wpt/tests/xhr/resources/get-set-cookie.py
@@ -0,0 +1,18 @@
+import datetime
+
+def main(request, response):
+ response.headers.set(b"Content-type", b"text/plain")
+
+ # By default use a session cookie.
+ expiration = None
+ if request.GET.get(b"clear"):
+ # If deleting, expire yesterday.
+ expiration = -datetime.timedelta(days=1)
+
+ response.set_cookie(b"WK-test", b"1", expires=expiration)
+ response.set_cookie(b"WK-test-secure", b"1", secure=True,
+ expires=expiration)
+ content = b""
+ for cookie in request.cookies:
+ content = content + b" " + cookie + b"=" + request.cookies.get(cookie).value
+ response.content = content
diff --git a/test/wpt/tests/xhr/resources/gzip.py b/test/wpt/tests/xhr/resources/gzip.py
new file mode 100644
index 0000000..fd1ca92
--- /dev/null
+++ b/test/wpt/tests/xhr/resources/gzip.py
@@ -0,0 +1,24 @@
+import gzip as gzip_module
+
+from io import BytesIO
+
+def main(request, response):
+ if b"content" in request.GET:
+ output = request.GET[b"content"]
+ else:
+ output = request.body
+
+ out = BytesIO()
+ with gzip_module.GzipFile(fileobj=out, mode="w") as f:
+ f.write(output)
+ output = out.getvalue()
+
+ headers = [(b"Content-type", b"text/plain"),
+ (b"Content-Encoding", b"gzip"),
+ (b"X-Request-Method", request.method),
+ (b"X-Request-Query", request.url_parts.query if request.url_parts.query else b"NO"),
+ (b"X-Request-Content-Length", request.headers.get(b"Content-Length", b"NO")),
+ (b"X-Request-Content-Type", request.headers.get(b"Content-Type", b"NO")),
+ (b"Content-Length", len(output))]
+
+ return headers, output
diff --git a/test/wpt/tests/xhr/resources/header-content-length-twice.asis b/test/wpt/tests/xhr/resources/header-content-length-twice.asis
new file mode 100644
index 0000000..e319698
--- /dev/null
+++ b/test/wpt/tests/xhr/resources/header-content-length-twice.asis
@@ -0,0 +1,3 @@
+HTTP/1.0 200 NANANA
+CONTENT-LENGTH: 0
+content-length: 0
diff --git a/test/wpt/tests/xhr/resources/header-content-length.asis b/test/wpt/tests/xhr/resources/header-content-length.asis
new file mode 100644
index 0000000..ef7071d
--- /dev/null
+++ b/test/wpt/tests/xhr/resources/header-content-length.asis
@@ -0,0 +1,2 @@
+HTTP/1.0 200 NANANA
+CONTENT-LENGTH: 0
diff --git a/test/wpt/tests/xhr/resources/header-user-agent.py b/test/wpt/tests/xhr/resources/header-user-agent.py
new file mode 100644
index 0000000..ac6af13
--- /dev/null
+++ b/test/wpt/tests/xhr/resources/header-user-agent.py
@@ -0,0 +1,15 @@
+def main(request, response):
+ response.headers.set(b"Access-Control-Allow-Origin", b"*")
+ response.headers.set(b"Access-Control-Max-Age", 0)
+ response.headers.set(b'Access-Control-Allow-Headers', b"x-test")
+
+ if request.method == u"OPTIONS":
+ if not request.headers.get(b"User-Agent"):
+ response.content = b"FAIL: User-Agent header missing in preflight request."
+ response.status = 400
+ else:
+ if request.headers.get(b"User-Agent"):
+ response.content = b"PASS"
+ else:
+ response.content = b"FAIL: User-Agent header missing in request"
+ response.status = 400
diff --git a/test/wpt/tests/xhr/resources/headers-basic.asis b/test/wpt/tests/xhr/resources/headers-basic.asis
new file mode 100644
index 0000000..fe37b1b
--- /dev/null
+++ b/test/wpt/tests/xhr/resources/headers-basic.asis
@@ -0,0 +1,4 @@
+HTTP/1.1 280 HELLO
+foo-test: 1
+foo-test: 2
+foo-test: 3
diff --git a/test/wpt/tests/xhr/resources/headers-double-empty.asis b/test/wpt/tests/xhr/resources/headers-double-empty.asis
new file mode 100644
index 0000000..14304b2
--- /dev/null
+++ b/test/wpt/tests/xhr/resources/headers-double-empty.asis
@@ -0,0 +1,3 @@
+HTTP/1.1 444 HI
+double-trouble:
+double-trouble:
diff --git a/test/wpt/tests/xhr/resources/headers-some-are-empty.asis b/test/wpt/tests/xhr/resources/headers-some-are-empty.asis
new file mode 100644
index 0000000..1783e1a
--- /dev/null
+++ b/test/wpt/tests/xhr/resources/headers-some-are-empty.asis
@@ -0,0 +1,7 @@
+HTTP/1.0 200 MEH
+HEYA:
+HEYA:
+HEYA: 1
+HEYA:
+HEYA:
+HEYA: 2
diff --git a/test/wpt/tests/xhr/resources/headers-www-authenticate.asis b/test/wpt/tests/xhr/resources/headers-www-authenticate.asis
new file mode 100644
index 0000000..6f9905e
--- /dev/null
+++ b/test/wpt/tests/xhr/resources/headers-www-authenticate.asis
@@ -0,0 +1,4 @@
+HTTP/1.1 280 HELLO
+www-authenticate: 1
+www-authenticate: 2
+www-authenticate: 3, 4
diff --git a/test/wpt/tests/xhr/resources/headers.asis b/test/wpt/tests/xhr/resources/headers.asis
new file mode 100644
index 0000000..69273ac
--- /dev/null
+++ b/test/wpt/tests/xhr/resources/headers.asis
@@ -0,0 +1,6 @@
+HTTP/1.1 200 YAYAYAYA
+foo-TEST: 1
+FOO-test: 2
+__Custom: token
+ALSO-here: Mr. PB
+ewok: lego
diff --git a/test/wpt/tests/xhr/resources/headers.py b/test/wpt/tests/xhr/resources/headers.py
new file mode 100644
index 0000000..00b37c6
--- /dev/null
+++ b/test/wpt/tests/xhr/resources/headers.py
@@ -0,0 +1,12 @@
+# -*- coding: utf-8 -*-
+
+def main(request, response):
+ response.headers.set(b"Content-Type", b"text/plain")
+ response.headers.set(b"X-Custom-Header", b"test")
+ response.headers.set(b"Set-Cookie", b"test")
+ response.headers.set(b"Set-Cookie2", b"test")
+ response.headers.set(b"X-Custom-Header-Empty", b"")
+ response.headers.set(b"X-Custom-Header-Comma", b"1")
+ response.headers.append(b"X-Custom-Header-Comma", b"2")
+ response.headers.set(b"X-Custom-Header-Bytes", u"…".encode("utf-8"))
+ return b"TEST"
diff --git a/test/wpt/tests/xhr/resources/image.gif b/test/wpt/tests/xhr/resources/image.gif
new file mode 100644
index 0000000..6d1174a
--- /dev/null
+++ b/test/wpt/tests/xhr/resources/image.gif
Binary files differ
diff --git a/test/wpt/tests/xhr/resources/img-utf8-html.py b/test/wpt/tests/xhr/resources/img-utf8-html.py
new file mode 100644
index 0000000..3057674
--- /dev/null
+++ b/test/wpt/tests/xhr/resources/img-utf8-html.py
@@ -0,0 +1,5 @@
+def main(request, response):
+ headers = [(b"Content-type", b"text/html;charset=utf-8")]
+ content = b"<img>foo"
+
+ return headers, content
diff --git a/test/wpt/tests/xhr/resources/img.jpg b/test/wpt/tests/xhr/resources/img.jpg
new file mode 100644
index 0000000..7aa9362
--- /dev/null
+++ b/test/wpt/tests/xhr/resources/img.jpg
Binary files differ
diff --git a/test/wpt/tests/xhr/resources/infinite-redirects.py b/test/wpt/tests/xhr/resources/infinite-redirects.py
new file mode 100644
index 0000000..a60942d
--- /dev/null
+++ b/test/wpt/tests/xhr/resources/infinite-redirects.py
@@ -0,0 +1,24 @@
+from wptserve.utils import isomorphic_encode
+
+def main(request, response):
+ location = u"%s://%s%s" % (request.url_parts.scheme,
+ request.url_parts.netloc,
+ request.url_parts.path)
+ page = u"alternate"
+ type = 302
+ mix = 0
+ if request.GET.first(b"page", None) == b"alternate":
+ page = u"default"
+
+ if request.GET.first(b"type", None) == b"301":
+ type = 301
+
+ if request.GET.first(b"mix", None) == b"1":
+ mix = 1
+ type = 302 if type == 301 else 301
+
+ new_location = u"%s?page=%s&type=%s&mix=%s" % (location, page, type, mix)
+ headers = [(b"Cache-Control", b"no-cache"),
+ (b"Pragma", b"no-cache"),
+ (b"Location", isomorphic_encode(new_location))]
+ return 301, headers, u"Hello guest. You have been redirected to " + new_location
diff --git a/test/wpt/tests/xhr/resources/init.htm b/test/wpt/tests/xhr/resources/init.htm
new file mode 100644
index 0000000..6f936c4
--- /dev/null
+++ b/test/wpt/tests/xhr/resources/init.htm
@@ -0,0 +1,20 @@
+<!doctype html>
+<html>
+ <head>
+ <title>support init file</title>
+ </head>
+ <body>
+ <script>
+ onload = function() {
+ // Run async, because navigations from inside onload can be a bit weird.
+ setTimeout(function() {
+ if (parent != window) {
+ parent.init()
+ } else {
+ opener.init();
+ }
+ }, 0);
+ }
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/resources/inspect-headers.py b/test/wpt/tests/xhr/resources/inspect-headers.py
new file mode 100644
index 0000000..123d637
--- /dev/null
+++ b/test/wpt/tests/xhr/resources/inspect-headers.py
@@ -0,0 +1,36 @@
+from wptserve.utils import isomorphic_encode
+
+def get_response(raw_headers, filter_value, filter_name):
+ result = b""
+ # raw_headers.raw_items() returns the (name, value) header pairs as
+ # tuples of strings. Convert them to bytes before comparing.
+ # TODO: Get access to the raw headers, so that whitespace between
+ # name, ":" and value can also be checked:
+ # https://github.com/web-platform-tests/wpt/issues/28756
+ for field in raw_headers.raw_items():
+ name = isomorphic_encode(field[0])
+ value = isomorphic_encode(field[1])
+ if filter_value:
+ if value == filter_value:
+ result += name + b","
+ elif name.lower() == filter_name:
+ result += name + b": " + value + b"\n"
+ return result
+
+def main(request, response):
+ headers = []
+ if b"cors" in request.GET:
+ headers.append((b"Access-Control-Allow-Origin", b"*"))
+ headers.append((b"Access-Control-Allow-Credentials", b"true"))
+ headers.append((b"Access-Control-Allow-Methods", b"GET, POST, PUT, FOO"))
+ headers.append((b"Access-Control-Allow-Headers", b"x-test, x-foo"))
+ headers.append((
+ b"Access-Control-Expose-Headers",
+ b"x-request-method, x-request-content-type, x-request-query, x-request-content-length"))
+ headers.append((b"content-type", b"text/plain"))
+
+ filter_value = request.GET.first(b"filter_value", b"")
+ filter_name = request.GET.first(b"filter_name", b"").lower()
+ result = get_response(request.raw_headers, filter_value, filter_name)
+
+ return headers, result
diff --git a/test/wpt/tests/xhr/resources/invalid-utf8-html.py b/test/wpt/tests/xhr/resources/invalid-utf8-html.py
new file mode 100644
index 0000000..825f2d8
--- /dev/null
+++ b/test/wpt/tests/xhr/resources/invalid-utf8-html.py
@@ -0,0 +1,5 @@
+def main(request, response):
+ headers = [(b"Content-type", b"text/html;charset=utf-8")]
+ content = b"\xff"
+
+ return headers, content
diff --git a/test/wpt/tests/xhr/resources/last-modified.py b/test/wpt/tests/xhr/resources/last-modified.py
new file mode 100644
index 0000000..db08e4b
--- /dev/null
+++ b/test/wpt/tests/xhr/resources/last-modified.py
@@ -0,0 +1,9 @@
+from wptserve.utils import isomorphic_decode, isomorphic_encode
+
+def main(request, response):
+ import datetime, os
+ srcpath = os.path.join(os.path.dirname(isomorphic_decode(__file__)), u"well-formed.xml")
+ srcmoddt = datetime.datetime.utcfromtimestamp(os.path.getmtime(srcpath))
+ response.headers.set(b"Last-Modified", isomorphic_encode(srcmoddt.strftime(u"%a, %d %b %Y %H:%M:%S GMT")))
+ response.headers.set(b"Content-Type", b"application/xml")
+ return open(srcpath, u"r").read()
diff --git a/test/wpt/tests/xhr/resources/no-custom-header-on-preflight.py b/test/wpt/tests/xhr/resources/no-custom-header-on-preflight.py
new file mode 100644
index 0000000..835c128
--- /dev/null
+++ b/test/wpt/tests/xhr/resources/no-custom-header-on-preflight.py
@@ -0,0 +1,27 @@
+def main(request, response):
+ def getState(token):
+ server_state = request.server.stash.take(token)
+ if not server_state:
+ return b"Uninitialized"
+ return server_state
+
+ def setState(state, token):
+ request.server.stash.put(token, state)
+
+ response.headers.set(b"Access-Control-Allow-Origin", b"*")
+ response.headers.set(b"Access-Control-Allow-Headers", b"x-test")
+ response.headers.set(b"Access-Control-Max-Age", 0)
+ token = request.GET.first(b"token", None)
+
+ if request.method == u"OPTIONS":
+ if request.headers.get(b"x-test"):
+ response.content = b"FAIL: Invalid header in preflight request."
+ response.status = 400
+ else:
+ setState(b"PASS", token)
+ else:
+ if request.headers.get(b"x-test"):
+ response.content = getState(token)
+ else:
+ response.content = b"FAIL: X-Test header missing in request"
+ response.status = 400
diff --git a/test/wpt/tests/xhr/resources/nocors/folder.txt b/test/wpt/tests/xhr/resources/nocors/folder.txt
new file mode 100644
index 0000000..5257b48
--- /dev/null
+++ b/test/wpt/tests/xhr/resources/nocors/folder.txt
@@ -0,0 +1 @@
+not CORS-enabled \ No newline at end of file
diff --git a/test/wpt/tests/xhr/resources/over-1-meg.txt b/test/wpt/tests/xhr/resources/over-1-meg.txt
new file mode 100644
index 0000000..be47867
--- /dev/null
+++ b/test/wpt/tests/xhr/resources/over-1-meg.txt
@@ -0,0 +1 @@
+abcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd \ No newline at end of file
diff --git a/test/wpt/tests/xhr/resources/parse-headers.py b/test/wpt/tests/xhr/resources/parse-headers.py
new file mode 100644
index 0000000..8520baa
--- /dev/null
+++ b/test/wpt/tests/xhr/resources/parse-headers.py
@@ -0,0 +1,6 @@
+def main(request, response):
+ content = u""
+ if b"my-custom-header" in request.GET:
+ val = request.GET.first(b"my-custom-header")
+ response.headers.set(b"My-Custom-Header", val)
+ return content
diff --git a/test/wpt/tests/xhr/resources/pass.txt b/test/wpt/tests/xhr/resources/pass.txt
new file mode 100644
index 0000000..7ef22e9
--- /dev/null
+++ b/test/wpt/tests/xhr/resources/pass.txt
@@ -0,0 +1 @@
+PASS
diff --git a/test/wpt/tests/xhr/resources/redirect-cors.py b/test/wpt/tests/xhr/resources/redirect-cors.py
new file mode 100644
index 0000000..5d030a6
--- /dev/null
+++ b/test/wpt/tests/xhr/resources/redirect-cors.py
@@ -0,0 +1,20 @@
+def main(request, response):
+ location = request.GET.first(b"location")
+
+ if request.method == u"OPTIONS":
+ if b"redirect_preflight" in request.GET:
+ response.status = 302
+ response.headers.set(b"Location", location)
+ else:
+ response.status = 200
+ response.headers.set(b"Access-Control-Allow-Methods", b"GET")
+ response.headers.set(b"Access-Control-Max-Age", 1)
+ elif request.method == u"GET":
+ response.status = 302
+ response.headers.set(b"Location", location)
+
+ if b"allow_origin" in request.GET:
+ response.headers.set(b"Access-Control-Allow-Origin", request.headers.get(b"origin"))
+
+ if b"allow_header" in request.GET:
+ response.headers.set(b"Access-Control-Allow-Headers", request.GET.first(b"allow_header"))
diff --git a/test/wpt/tests/xhr/resources/redirect.py b/test/wpt/tests/xhr/resources/redirect.py
new file mode 100644
index 0000000..3839b63
--- /dev/null
+++ b/test/wpt/tests/xhr/resources/redirect.py
@@ -0,0 +1,16 @@
+import time
+
+from wptserve.utils import isomorphic_encode
+
+def main(request, response):
+ code = int(request.GET.first(b"code", 302))
+ location = request.GET.first(b"location", isomorphic_encode(request.url_parts.path + u"?followed"))
+
+ if b"delay" in request.GET:
+ delay = float(request.GET.first(b"delay"))
+ time.sleep(delay / 1E3)
+
+ if b"followed" in request.GET:
+ return [(b"Content:Type", b"text/plain")], b"MAGIC HAPPENED"
+ else:
+ return (code, b"WEBSRT MARKETING"), [(b"Location", location)], b"TEST"
diff --git a/test/wpt/tests/xhr/resources/requri.py b/test/wpt/tests/xhr/resources/requri.py
new file mode 100644
index 0000000..c7f7330
--- /dev/null
+++ b/test/wpt/tests/xhr/resources/requri.py
@@ -0,0 +1,5 @@
+def main(request, response):
+ if b"full" in request.GET:
+ return request.url
+ else:
+ return request.request_path
diff --git a/test/wpt/tests/xhr/resources/reset-token.py b/test/wpt/tests/xhr/resources/reset-token.py
new file mode 100644
index 0000000..257c2aa
--- /dev/null
+++ b/test/wpt/tests/xhr/resources/reset-token.py
@@ -0,0 +1,5 @@
+def main(request, response):
+ response.headers.set(b"Access-Control-Allow-Origin", request.headers.get(b"origin"))
+ token = request.GET[b"token"]
+ request.server.stash.put(token, b"")
+ response.content = b"PASS"
diff --git a/test/wpt/tests/xhr/resources/responseType-document-in-worker.js b/test/wpt/tests/xhr/resources/responseType-document-in-worker.js
new file mode 100644
index 0000000..37ba9bd
--- /dev/null
+++ b/test/wpt/tests/xhr/resources/responseType-document-in-worker.js
@@ -0,0 +1,9 @@
+self.importScripts('/resources/testharness.js');
+
+test(function() {
+ let xhr = new XMLHttpRequest();
+ xhr.responseType = "document";
+ assert_not_equals(xhr.responseType, "document");
+}, "Setting XMLHttpRequest responseType to 'document' in a worker should have no effect.");
+
+done();
diff --git a/test/wpt/tests/xhr/resources/responseXML-unavailable-in-worker.js b/test/wpt/tests/xhr/resources/responseXML-unavailable-in-worker.js
new file mode 100644
index 0000000..06da02a
--- /dev/null
+++ b/test/wpt/tests/xhr/resources/responseXML-unavailable-in-worker.js
@@ -0,0 +1,9 @@
+self.importScripts('/resources/testharness.js');
+
+test(function() {
+ let xhr = new XMLHttpRequest();
+ assert_false(xhr.hasOwnProperty("responseXML"), "responseXML should not be available on instances.");
+ assert_false(XMLHttpRequest.prototype.hasOwnProperty("responseXML"), "responseXML should not be on the prototype.");
+}, "XMLHttpRequest's responseXML property should not be exposed in workers.");
+
+done();
diff --git a/test/wpt/tests/xhr/resources/send-after-setting-document-domain-window-1.htm b/test/wpt/tests/xhr/resources/send-after-setting-document-domain-window-1.htm
new file mode 100644
index 0000000..4e4c3fa
--- /dev/null
+++ b/test/wpt/tests/xhr/resources/send-after-setting-document-domain-window-1.htm
@@ -0,0 +1,23 @@
+<!doctype html>
+<html>
+ <head>
+ <title>XMLHttpRequest: send() with document.domain set: loading documents from original origin after setting document.domain</title>
+ <script src="send-after-setting-document-domain-window-helper.js"></script>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-open()-method" data-tested-assertations="following::ol[1]/li[2]/ol[1]/li[3]" />
+ </head>
+ <body>
+ <script>
+ run_test(function() {
+ document.domain = document.domain; // this is not a noop, it does actually change the security context
+ var client = new XMLHttpRequest();
+ client.open("GET", "status.py?content=hello", false);
+ client.send(null);
+ assert_equals(client.responseText, "hello");
+ document.domain = document.domain.replace(/^\w+\./, "");
+ client.open("GET", "status.py?content=hello2", false);
+ client.send(null);
+ assert_equals(client.responseText, "hello2");
+ }, "loading documents from original origin after setting document.domain");
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/resources/send-after-setting-document-domain-window-2.htm b/test/wpt/tests/xhr/resources/send-after-setting-document-domain-window-2.htm
new file mode 100644
index 0000000..cd9f0c6
--- /dev/null
+++ b/test/wpt/tests/xhr/resources/send-after-setting-document-domain-window-2.htm
@@ -0,0 +1,20 @@
+<!doctype html>
+<html>
+ <head>
+ <title>XMLHttpRequest: send() with document.domain set: loading documents from the origin document.domain was set to should throw</title>
+ <script src="send-after-setting-document-domain-window-helper.js"></script>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-open()-method" data-tested-assertations="following::ol[1]/li[2]/ol[1]/li[3]" />
+ </head>
+ <body>
+ <script>
+ run_test(function() {
+ document.domain = document.domain.replace(/^\w+\./, "");
+ var client = new XMLHttpRequest();
+ client.open("GET", location.protocol + "//" + document.domain + location.pathname.replace(/[^\/]*$/, "") + "status.py?content=hello3", false);
+ assert_throws_dom("NetworkError", function() {
+ client.send(null);
+ });
+ }, "loading documents from the origin document.domain was set to should throw");
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/resources/send-after-setting-document-domain-window-helper.js b/test/wpt/tests/xhr/resources/send-after-setting-document-domain-window-helper.js
new file mode 100644
index 0000000..0c239cf
--- /dev/null
+++ b/test/wpt/tests/xhr/resources/send-after-setting-document-domain-window-helper.js
@@ -0,0 +1,32 @@
+function assert_equals(value, expected) {
+ if (value != expected) {
+ throw "Got wrong value.\nExpected '" + expected + "',\ngot '" + value + "'";
+ }
+}
+
+function assert_throws_dom(expected_exc, func) {
+ try {
+ func.call(this);
+ } catch(e) {
+ if (e.constructor.name != "DOMException") {
+ throw `Exception ${e.constructor.name || "unknown"} that was not a DOMException was thrown`;
+ }
+ var actual = e.name || e.type;
+ if (actual != expected_exc) {
+ throw "Got wrong exception.\nExpected '" + expected_exc + "',\ngot '" + actual + "'.";
+ }
+ return;
+ }
+ throw "Expected exception, but none was thrown";
+}
+
+function run_test(test, name) {
+ var result = {passed: true, message: null, name: name};
+ try {
+ test();
+ } catch(e) {
+ result.passed = false;
+ result.message = e + "";
+ }
+ opener.postMessage(result, "*");
+}
diff --git a/test/wpt/tests/xhr/resources/shift-jis-html.py b/test/wpt/tests/xhr/resources/shift-jis-html.py
new file mode 100644
index 0000000..d8941e9
--- /dev/null
+++ b/test/wpt/tests/xhr/resources/shift-jis-html.py
@@ -0,0 +1,6 @@
+def main(request, response):
+ headers = [(b"Content-type", b"text/html;charset=shift-jis")]
+ # Shift-JIS bytes for katakana TE SU TO ('test')
+ content = bytes([0x83, 0x65, 0x83, 0x58, 0x83, 0x67])
+
+ return headers, content
diff --git a/test/wpt/tests/xhr/resources/status.py b/test/wpt/tests/xhr/resources/status.py
new file mode 100644
index 0000000..05a59d5
--- /dev/null
+++ b/test/wpt/tests/xhr/resources/status.py
@@ -0,0 +1,11 @@
+from wptserve.utils import isomorphic_encode
+
+def main(request, response):
+ code = int(request.GET.first(b"code", 200))
+ text = request.GET.first(b"text", b"OMG")
+ content = request.GET.first(b"content", b"")
+ type = request.GET.first(b"type", b"")
+ status = (code, text)
+ headers = [(b"Content-Type", type),
+ (b"X-Request-Method", isomorphic_encode(request.method))]
+ return status, headers, content
diff --git a/test/wpt/tests/xhr/resources/top.txt b/test/wpt/tests/xhr/resources/top.txt
new file mode 100644
index 0000000..83a3157
--- /dev/null
+++ b/test/wpt/tests/xhr/resources/top.txt
@@ -0,0 +1 @@
+top \ No newline at end of file
diff --git a/test/wpt/tests/xhr/resources/trickle.py b/test/wpt/tests/xhr/resources/trickle.py
new file mode 100644
index 0000000..eab5310
--- /dev/null
+++ b/test/wpt/tests/xhr/resources/trickle.py
@@ -0,0 +1,15 @@
+import time
+
+def main(request, response):
+ chunk = b"TEST_TRICKLE\n"
+ delay = float(request.GET.first(b"ms", 500)) / 1E3
+ count = int(request.GET.first(b"count", 50))
+ if b"specifylength" in request.GET:
+ response.headers.set(b"Content-Length", count * len(chunk))
+ time.sleep(delay)
+ response.headers.set(b"Content-type", b"text/plain")
+ response.write_status_headers()
+ time.sleep(delay)
+ for i in range(count):
+ response.writer.write_content(chunk)
+ time.sleep(delay)
diff --git a/test/wpt/tests/xhr/resources/upload.py b/test/wpt/tests/xhr/resources/upload.py
new file mode 100644
index 0000000..e35e46e
--- /dev/null
+++ b/test/wpt/tests/xhr/resources/upload.py
@@ -0,0 +1,17 @@
+from wptserve.utils import isomorphic_encode
+
+def main(request, response):
+ content = []
+
+ for key, values in sorted(item for item in request.POST.items() if not hasattr(item[1][0], u"filename")):
+ content.append(b"%s=%s," % (key, values[0]))
+ content.append(b"\n")
+
+ for key, values in sorted(item for item in request.POST.items() if hasattr(item[1][0], u"filename")):
+ value = values[0]
+ content.append(b"%s=%s:%s:%d," % (key,
+ isomorphic_encode(value.filename),
+ isomorphic_encode(value.headers[u"Content-Type"]) if value.headers[u"Content-Type"] is not None else b"None",
+ len(value.file.read())))
+
+ return b"".join(content)
diff --git a/test/wpt/tests/xhr/resources/utf16-bom.json b/test/wpt/tests/xhr/resources/utf16-bom.json
new file mode 100644
index 0000000..5fd0a58
--- /dev/null
+++ b/test/wpt/tests/xhr/resources/utf16-bom.json
Binary files differ
diff --git a/test/wpt/tests/xhr/resources/utf16.txt b/test/wpt/tests/xhr/resources/utf16.txt
new file mode 100644
index 0000000..0085dfa
--- /dev/null
+++ b/test/wpt/tests/xhr/resources/utf16.txt
Binary files differ
diff --git a/test/wpt/tests/xhr/resources/well-formed.xml b/test/wpt/tests/xhr/resources/well-formed.xml
new file mode 100644
index 0000000..2f4f126
--- /dev/null
+++ b/test/wpt/tests/xhr/resources/well-formed.xml
@@ -0,0 +1,4 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <p id="n&#49;">1</p>
+ <p xmlns="namespacesarejuststrings" id="n2">2</p>
+</html>
diff --git a/test/wpt/tests/xhr/resources/win-1252-html.py b/test/wpt/tests/xhr/resources/win-1252-html.py
new file mode 100644
index 0000000..0ef5283
--- /dev/null
+++ b/test/wpt/tests/xhr/resources/win-1252-html.py
@@ -0,0 +1,5 @@
+def main(request, response):
+ headers = [(b"Content-type", b"text/html;charset=windows-1252")]
+ content = chr(0xff)
+
+ return headers, content
diff --git a/test/wpt/tests/xhr/resources/win-1252-xml.py b/test/wpt/tests/xhr/resources/win-1252-xml.py
new file mode 100644
index 0000000..e26c7f9
--- /dev/null
+++ b/test/wpt/tests/xhr/resources/win-1252-xml.py
@@ -0,0 +1,5 @@
+def main(request, response):
+ headers = [(b"Content-type", b"application/xml;charset=windows-1252")]
+ content = b'<\xff/>'
+
+ return headers, content
diff --git a/test/wpt/tests/xhr/resources/workerxhr-origin-referrer.js b/test/wpt/tests/xhr/resources/workerxhr-origin-referrer.js
new file mode 100644
index 0000000..e378de2
--- /dev/null
+++ b/test/wpt/tests/xhr/resources/workerxhr-origin-referrer.js
@@ -0,0 +1,63 @@
+importScripts("/resources/testharness.js")
+
+async_test(function() {
+ var expected = 'Referer: ' +
+ location.href.replace(/[^/]*$/, '') +
+ "workerxhr-origin-referrer.js\n"
+
+ var xhr = new XMLHttpRequest()
+ xhr.onreadystatechange = this.step_func(function() {
+ if (xhr.readyState == 4) {
+ assert_equals(xhr.responseText, expected)
+ this.done()
+ }
+ })
+ xhr.open('GET', 'inspect-headers.py?filter_name=referer', true)
+ xhr.send()
+}, 'Referer header')
+
+async_test(function() {
+ var expected = 'Origin: ' +
+ location.protocol +
+ '//' +
+ location.hostname +
+ (location.port === "" ? "" : ":" + location.port) +
+ '\n'
+
+ var xhr = new XMLHttpRequest()
+ xhr.onreadystatechange = this.step_func(function() {
+ if (xhr.readyState == 4) {
+ assert_equals(xhr.responseText, expected)
+ this.done()
+ }
+ })
+ var url = location.protocol +
+ '//www2.' +
+ location.hostname +
+ (location.port === "" ? "" : ":" + location.port) +
+ location.pathname.replace(/[^/]*$/, '') +
+ 'inspect-headers.py?filter_name=origin&cors'
+ xhr.open('GET', url, true)
+ xhr.send()
+}, 'Origin header')
+
+async_test(function() {
+ // If "origin" / base URL is the origin of this JS file, we can load files
+ // from the server it originates from.. and requri.py will be able to tell us
+ // what the requested URL was
+
+ var expected = location.href.replace(/[^/]*$/, '') +
+ 'requri.py?full'
+
+ var xhr = new XMLHttpRequest()
+ xhr.onreadystatechange = this.step_func(function() {
+ if (xhr.readyState == 4) {
+ assert_equals(xhr.responseText, expected)
+ this.done()
+ }
+ })
+ xhr.open('GET', 'requri.py?full', true)
+ xhr.send()
+}, 'Request URL test')
+
+done()
diff --git a/test/wpt/tests/xhr/resources/workerxhr-simple.js b/test/wpt/tests/xhr/resources/workerxhr-simple.js
new file mode 100644
index 0000000..9bae5a5
--- /dev/null
+++ b/test/wpt/tests/xhr/resources/workerxhr-simple.js
@@ -0,0 +1,9 @@
+var xhr=new XMLHttpRequest()
+xhr.onreadystatechange = function(){
+ if(xhr.readyState == 4){
+ var status = xhr.responseText === 'bottom\n' ? 'PASSED' : 'FAILED'
+ self.postMessage(status)
+ }
+}
+xhr.open('GET', 'folder.txt', true)
+xhr.send()
diff --git a/test/wpt/tests/xhr/resources/xmlhttprequest-event-order.js b/test/wpt/tests/xhr/resources/xmlhttprequest-event-order.js
new file mode 100644
index 0000000..b6bb6cd
--- /dev/null
+++ b/test/wpt/tests/xhr/resources/xmlhttprequest-event-order.js
@@ -0,0 +1,83 @@
+(function(global) {
+ var recorded_xhr_events = [];
+
+ function record_xhr_event(e) {
+ var prefix = e.target instanceof XMLHttpRequestUpload ? "upload." : "";
+ recorded_xhr_events.push((prefix || "") + e.type + "(" + e.loaded + "," + e.total + "," + e.lengthComputable + ")");
+ }
+
+ global.prepare_xhr_for_event_order_test = function(xhr) {
+ xhr.addEventListener("readystatechange", function(e) {
+ recorded_xhr_events.push(xhr.readyState);
+ });
+ var events = ["loadstart", "progress", "abort", "timeout", "error", "load", "loadend"];
+ for(var i=0; i<events.length; ++i) {
+ xhr.addEventListener(events[i], record_xhr_event);
+ }
+ if ("upload" in xhr) {
+ for(var i=0; i<events.length; ++i) {
+ xhr.upload.addEventListener(events[i], record_xhr_event);
+ }
+ }
+ }
+
+ function getNextEvent(arr) {
+ var event = { str: arr.shift() };
+
+ // we can only handle strings, numbers (readystates) and undefined
+ if (event.str === undefined) {
+ return event;
+ }
+
+ if (typeof event.str !== "string") {
+ if (Number.isInteger(event.str)) {
+ event.state = event.str;
+ event.str = "readystatechange(" + event.str + ")";
+ } else {
+ throw "Test error: unexpected event type " + event.str;
+ }
+ }
+
+ // parse out the general type, loaded and total values
+ var type = event.type = event.str.split("(")[0].split(".").pop();
+ var loadedAndTotal = event.str.match(/.*\((\d+),(\d+),(true|false)\)/);
+ if (loadedAndTotal) {
+ event.loaded = parseInt(loadedAndTotal[1]);
+ event.total = parseInt(loadedAndTotal[2]);
+ event.lengthComputable = loadedAndTotal[3] == "true";
+ }
+
+ return event;
+ }
+
+ global.assert_xhr_event_order_matches = function(expected) {
+ var recorded = recorded_xhr_events;
+ var lastRecordedLoaded = -1;
+ while(expected.length && recorded.length) {
+ var currentExpected = getNextEvent(expected),
+ currentRecorded = getNextEvent(recorded);
+
+ // skip to the last progress event if we've hit one (note the next
+ // event after a progress event should be a LOADING readystatechange,
+ // if there are multiple progress events in a row).
+ while (recorded.length && currentRecorded.type == "progress" &&
+ parseInt(recorded) === 3) {
+ assert_greater_than(currentRecorded.loaded, lastRecordedLoaded,
+ "progress event 'loaded' values must only increase");
+ lastRecordedLoaded = currentRecorded.loaded;
+ }
+ if (currentRecorded.type == "loadend") {
+ recordedProgressCount = 0;
+ lastRecordedLoaded = -1;
+ }
+
+ assert_equals(currentRecorded.str, currentExpected.str);
+ }
+ if (recorded.length) {
+ throw "\nUnexpected extra events: " + recorded.join(", ");
+ }
+ if (expected.length) {
+ throw "\nExpected more events: " + expected.join(", ");
+ }
+ }
+}(this));
diff --git a/test/wpt/tests/xhr/resources/xmlhttprequest-timeout-aborted.js b/test/wpt/tests/xhr/resources/xmlhttprequest-timeout-aborted.js
new file mode 100644
index 0000000..dd0b645
--- /dev/null
+++ b/test/wpt/tests/xhr/resources/xmlhttprequest-timeout-aborted.js
@@ -0,0 +1,15 @@
+if (this.document === undefined)
+ importScripts("xmlhttprequest-timeout.js");
+/*
+This sets up three requests:
+The first request will only be open()ed, not aborted, timeout will be TIME_REGULAR_TIMEOUT but will never triggered because send() isn't called.
+After TIME_NORMAL_LOAD, the test asserts that no load/error/timeout/abort events fired
+
+Second request will be aborted immediately after send(), test asserts that abort fired
+
+Third request is set up to call abort() after TIME_NORMAL_LOAD, but it also has a TIME_REGULAR_TIMEOUT timeout. Asserts that timeout fired.
+(abort() is called later and should not fire an abort event per spec. This is untested!)
+*/
+runTestRequests([ ["AbortedRequest", false, "only open()ed, not aborted"],
+ ["AbortedRequest", true, "aborted immediately after send()", -1],
+ ["AbortedRequest", true, "call abort() after TIME_NORMAL_LOAD", TIME_NORMAL_LOAD] ]);
diff --git a/test/wpt/tests/xhr/resources/xmlhttprequest-timeout-abortedonmain.js b/test/wpt/tests/xhr/resources/xmlhttprequest-timeout-abortedonmain.js
new file mode 100644
index 0000000..97477f9
--- /dev/null
+++ b/test/wpt/tests/xhr/resources/xmlhttprequest-timeout-abortedonmain.js
@@ -0,0 +1,8 @@
+/*
+This test sets up two requests:
+one that gets abort()ed from a 0ms timeout (0ms will obviously be clamped to whatever the implementation's minimal value is), asserts abort event fires
+one that will be aborted after TIME_DELAY, (with a timeout at TIME_REGULAR_TIMEOUT) asserts abort event fires. Does not assert that the timeout event does *not* fire.
+*/
+
+runTestRequests([ ["AbortedRequest", true, "abort() from a 0ms timeout", 0],
+ ["AbortedRequest", true, "aborted after TIME_DELAY", TIME_DELAY] ]);
diff --git a/test/wpt/tests/xhr/resources/xmlhttprequest-timeout-overrides.js b/test/wpt/tests/xhr/resources/xmlhttprequest-timeout-overrides.js
new file mode 100644
index 0000000..be8e4a9
--- /dev/null
+++ b/test/wpt/tests/xhr/resources/xmlhttprequest-timeout-overrides.js
@@ -0,0 +1,12 @@
+if (this.document === undefined)
+ importScripts("xmlhttprequest-timeout.js");
+/*
+Sets up three requests to a resource that will take 0.6 seconds to load:
+1) timeout first set to TIME_NORMAL_LOAD, after TIME_REGULAR_TIMEOUT timeout is set to 0, asserts load fires
+2) timeout first set to TIME_NORMAL_LOAD, after TIME_DELAY timeout is set to TIME_REGULAR_TIMEOUT, asserts load fires (race condition..?!?)
+3) timeout first set to 0, after TIME_REGULAR_TIMEOUT it is set to TIME_REGULAR_TIMEOUT * 10, asserts load fires
+*/
+
+runTestRequests([ ["RequestTracker", true, "timeout disabled after initially set", TIME_NORMAL_LOAD, TIME_REGULAR_TIMEOUT, 0],
+ ["RequestTracker", true, "timeout overrides load after a delay", TIME_NORMAL_LOAD, TIME_DELAY, TIME_REGULAR_TIMEOUT],
+ ["RequestTracker", true, "timeout enabled after initially disabled", 0, TIME_REGULAR_TIMEOUT, TIME_NORMAL_LOAD * 10] ]);
diff --git a/test/wpt/tests/xhr/resources/xmlhttprequest-timeout-overridesexpires.js b/test/wpt/tests/xhr/resources/xmlhttprequest-timeout-overridesexpires.js
new file mode 100644
index 0000000..5151b5d
--- /dev/null
+++ b/test/wpt/tests/xhr/resources/xmlhttprequest-timeout-overridesexpires.js
@@ -0,0 +1,12 @@
+if (this.document === undefined)
+ importScripts("xmlhttprequest-timeout.js");
+/*
+ Starts three requests:
+ 1) XHR to resource which will take a least TIME_XHR_LOAD ms with timeout initially set to TIME_NORMAL_LOAD ms. After TIME_LATE_TIMEOUT ms timeout is supposedly reset to TIME_DELAY ms,
+ but the resource should have finished loading already. Asserts "load" fires.
+ 2) XHR with initial timeout set to TIME_NORMAL_LOAD, after TIME_REGULAR_TIMEOUT sets timeout to TIME_DELAY+100. Asserts "timeout" fires.
+ 3) XHR with initial timeout set to TIME_DELAY, after TIME_REGULAR_TIMEOUT sets timeout to 500ms. Asserts "timeout" fires (the change happens when timeout already fired and the request is done).
+*/
+runTestRequests([ ["RequestTracker", true, "timeout set to expiring value after load fires", TIME_NORMAL_LOAD, TIME_LATE_TIMEOUT, TIME_DELAY],
+ ["RequestTracker", true, "timeout set to expired value before load fires", TIME_NORMAL_LOAD, TIME_REGULAR_TIMEOUT, TIME_DELAY+100],
+ ["RequestTracker", true, "timeout set to non-expiring value after timeout fires", TIME_DELAY, TIME_REGULAR_TIMEOUT, 500] ]);
diff --git a/test/wpt/tests/xhr/resources/xmlhttprequest-timeout-runner.js b/test/wpt/tests/xhr/resources/xmlhttprequest-timeout-runner.js
new file mode 100644
index 0000000..151226a
--- /dev/null
+++ b/test/wpt/tests/xhr/resources/xmlhttprequest-timeout-runner.js
@@ -0,0 +1,21 @@
+
+function testResultCallbackHandler(event) {
+ if (event.data == "done") {
+ done();
+ return;
+ }
+ if (event.data.type == "is") {
+ test(function() { assert_equals(event.data.got, event.data.expected); }, "Timeout test: " + event.data.msg);
+ return;
+ }
+ if (event.data.type == "ok") {
+ test(function() { assert_true(event.data.bool); }, "Timeout test: " + event.data.msg);
+ return;
+ }
+}
+
+window.addEventListener("message", testResultCallbackHandler);
+
+// Setting up testharness.js
+setup({ explicit_done: true });
+
diff --git a/test/wpt/tests/xhr/resources/xmlhttprequest-timeout-simple.js b/test/wpt/tests/xhr/resources/xmlhttprequest-timeout-simple.js
new file mode 100644
index 0000000..ed45c81
--- /dev/null
+++ b/test/wpt/tests/xhr/resources/xmlhttprequest-timeout-simple.js
@@ -0,0 +1,6 @@
+if (this.document === undefined)
+ importScripts("xmlhttprequest-timeout.js");
+
+runTestRequests([ ["RequestTracker", true, "no time out scheduled, load fires normally", 0],
+ ["RequestTracker", true, "load fires normally", TIME_NORMAL_LOAD],
+ ["RequestTracker", true, "timeout hit before load", TIME_REGULAR_TIMEOUT] ]);
diff --git a/test/wpt/tests/xhr/resources/xmlhttprequest-timeout-synconmain.js b/test/wpt/tests/xhr/resources/xmlhttprequest-timeout-synconmain.js
new file mode 100644
index 0000000..e77d25b
--- /dev/null
+++ b/test/wpt/tests/xhr/resources/xmlhttprequest-timeout-synconmain.js
@@ -0,0 +1,2 @@
+runTestRequests([ ["SyncRequestSettingTimeoutAfterOpen", null, "timeout after open"],
+ ["SyncRequestSettingTimeoutBeforeOpen", null, "timeout before open"] ]);
diff --git a/test/wpt/tests/xhr/resources/xmlhttprequest-timeout-synconworker.js b/test/wpt/tests/xhr/resources/xmlhttprequest-timeout-synconworker.js
new file mode 100644
index 0000000..a3b56c8
--- /dev/null
+++ b/test/wpt/tests/xhr/resources/xmlhttprequest-timeout-synconworker.js
@@ -0,0 +1,11 @@
+if (this.document === undefined){
+ importScripts("xmlhttprequest-timeout.js");
+}else{
+ throw "This test expects to be run as a Worker";
+}
+
+/* NOT TESTED: setting timeout before calling open( ... , false) in a worker context. The test code always calls open() first. */
+
+runTestRequests([ ["RequestTracker", false, "no time out scheduled, load fires normally", 0],
+ ["RequestTracker", false, "load fires normally", TIME_NORMAL_LOAD],
+ ["RequestTracker", false, "timeout hit before load", TIME_REGULAR_TIMEOUT] ]);
diff --git a/test/wpt/tests/xhr/resources/xmlhttprequest-timeout-twice.js b/test/wpt/tests/xhr/resources/xmlhttprequest-timeout-twice.js
new file mode 100644
index 0000000..31111b6
--- /dev/null
+++ b/test/wpt/tests/xhr/resources/xmlhttprequest-timeout-twice.js
@@ -0,0 +1,6 @@
+if (this.document === undefined)
+ importScripts("xmlhttprequest-timeout.js");
+
+runTestRequests([ ["RequestTracker", true, "load fires normally with no timeout set, twice", 0, TIME_REGULAR_TIMEOUT, 0],
+ ["RequestTracker", true, "load fires normally with same timeout set twice", TIME_NORMAL_LOAD, TIME_REGULAR_TIMEOUT, TIME_NORMAL_LOAD],
+ ["RequestTracker", true, "timeout fires normally with same timeout set twice", TIME_REGULAR_TIMEOUT, TIME_DELAY, TIME_REGULAR_TIMEOUT] ]);
diff --git a/test/wpt/tests/xhr/resources/xmlhttprequest-timeout.js b/test/wpt/tests/xhr/resources/xmlhttprequest-timeout.js
new file mode 100644
index 0000000..9375bab
--- /dev/null
+++ b/test/wpt/tests/xhr/resources/xmlhttprequest-timeout.js
@@ -0,0 +1,333 @@
+/* Test adapted from Alex Vincent's XHR2 timeout tests, written for Mozilla.
+ https://hg.mozilla.org/mozilla-central/file/tip/content/base/test/
+ Released into the public domain or under BSD, according to
+ https://bugzilla.mozilla.org/show_bug.cgi?id=525816#c86
+*/
+
+/* Notes:
+ - All times are expressed in milliseconds in this test suite.
+ - Test harness code is at the end of this file.
+ - We generate only one request at a time, to avoid overloading the HTTP
+ request handlers.
+ */
+
+var TIME_NORMAL_LOAD = 5000;
+var TIME_LATE_TIMEOUT = 4000;
+var TIME_XHR_LOAD = 3000;
+var TIME_REGULAR_TIMEOUT = 2000;
+var TIME_SYNC_TIMEOUT = 1000;
+var TIME_DELAY = 1000;
+
+/*
+ * This should point to a resource that responds with a text/plain resource after a delay of TIME_XHR_LOAD milliseconds.
+ */
+var STALLED_REQUEST_URL = "delay.py?ms=" + (TIME_XHR_LOAD);
+
+var inWorker = false;
+try {
+ inWorker = !(self instanceof Window);
+} catch (e) {
+ inWorker = true;
+}
+
+if (!inWorker)
+ STALLED_REQUEST_URL = "resources/" + STALLED_REQUEST_URL;
+
+function message(obj) {
+ if (inWorker)
+ self.postMessage(obj);
+ else
+ self.postMessage(obj, "*");
+}
+
+function is(got, expected, msg) {
+ var obj = {};
+ obj.type = "is";
+ obj.got = got;
+ obj.expected = expected;
+ obj.msg = msg;
+
+ message(obj);
+}
+
+function ok(bool, msg) {
+ var obj = {};
+ obj.type = "ok";
+ obj.bool = bool;
+ obj.msg = msg;
+
+ message(obj);
+}
+
+/**
+ * Generate and track results from a XMLHttpRequest with regards to timeouts.
+ *
+ * @param {String} id The test description.
+ * @param {Number} timeLimit The initial setting for the request timeout.
+ * @param {Number} resetAfter (Optional) The time after sending the request, to
+ * reset the timeout.
+ * @param {Number} resetTo (Optional) The delay to reset the timeout to.
+ *
+ * @note The actual testing takes place in handleEvent(event).
+ * The requests are generated in startXHR().
+ *
+ * @note If resetAfter and resetTo are omitted, only the initial timeout setting
+ * applies.
+ *
+ * @constructor
+ * @implements DOMEventListener
+ */
+function RequestTracker(async, id, timeLimit /*[, resetAfter, resetTo]*/) {
+ this.async = async;
+ this.id = id;
+ this.timeLimit = timeLimit;
+
+ if (arguments.length > 3) {
+ this.mustReset = true;
+ this.resetAfter = arguments[3];
+ this.resetTo = arguments[4];
+ }
+
+ this.hasFired = false;
+}
+RequestTracker.prototype = {
+ /**
+ * Start the XMLHttpRequest!
+ */
+ startXHR: function() {
+ var req = new XMLHttpRequest();
+ this.request = req;
+ req.open("GET", STALLED_REQUEST_URL, this.async);
+ var me = this;
+ function handleEvent(e) { return me.handleEvent(e); };
+ req.onerror = handleEvent;
+ req.onload = handleEvent;
+ req.onabort = handleEvent;
+ req.ontimeout = handleEvent;
+
+ req.timeout = this.timeLimit;
+
+ if (this.mustReset) {
+ var resetTo = this.resetTo;
+ self.setTimeout(function() {
+ req.timeout = resetTo;
+ }, this.resetAfter);
+ }
+
+ try {
+ req.send(null);
+ }
+ catch (e) {
+ // Synchronous case in workers.
+ ok(!this.async && this.timeLimit < TIME_XHR_LOAD && e.name == "TimeoutError", "Unexpected error: " + e);
+ TestCounter.testComplete();
+ }
+ },
+
+ /**
+ * Get a message describing this test.
+ *
+ * @returns {String} The test description.
+ */
+ getMessage: function() {
+ var rv = this.id + ", ";
+ if (this.mustReset) {
+ rv += "original timeout at " + this.timeLimit + ", ";
+ rv += "reset at " + this.resetAfter + " to " + this.resetTo;
+ }
+ else {
+ rv += "timeout scheduled at " + this.timeLimit;
+ }
+ return rv;
+ },
+
+ /**
+ * Check the event received, and if it's the right (and only) one we get.
+ *
+ * @param {DOMProgressEvent} evt An event of type "load" or "timeout".
+ */
+ handleEvent: function(evt) {
+ if (this.hasFired) {
+ ok(false, "Only one event should fire: " + this.getMessage());
+ return;
+ }
+ this.hasFired = true;
+
+ var type = evt.type, expectedType;
+ // The XHR responds after TIME_XHR_LOAD milliseconds with a load event.
+ var timeLimit = this.mustReset && (this.resetAfter < Math.min(TIME_XHR_LOAD, this.timeLimit)) ?
+ this.resetTo :
+ this.timeLimit;
+ if ((timeLimit == 0) || (timeLimit >= TIME_XHR_LOAD)) {
+ expectedType = "load";
+ }
+ else {
+ expectedType = "timeout";
+ }
+ is(type, expectedType, this.getMessage());
+ TestCounter.testComplete();
+ }
+};
+
+/**
+ * Generate and track XMLHttpRequests which will have abort() called on.
+ *
+ * @param shouldAbort {Boolean} True if we should call abort at all.
+ * @param abortDelay {Number} The time in ms to wait before calling abort().
+ */
+function AbortedRequest(shouldAbort, id, abortDelay) {
+ this.shouldAbort = shouldAbort;
+ this.abortDelay = abortDelay;
+ this.hasFired = false;
+}
+AbortedRequest.prototype = {
+ /**
+ * Start the XMLHttpRequest!
+ */
+ startXHR: function() {
+ var req = new XMLHttpRequest();
+ this.request = req;
+ req.open("GET", STALLED_REQUEST_URL);
+ var _this = this;
+ function handleEvent(e) { return _this.handleEvent(e); };
+ req.onerror = handleEvent;
+ req.onload = handleEvent;
+ req.onabort = handleEvent;
+ req.ontimeout = handleEvent;
+
+ req.timeout = TIME_REGULAR_TIMEOUT;
+
+ function abortReq() {
+ req.abort();
+ }
+
+ if (!this.shouldAbort) {
+ self.setTimeout(function() {
+ try {
+ _this.noEventsFired();
+ }
+ catch (e) {
+ ok(false, "Unexpected error: " + e);
+ TestCounter.testComplete();
+ }
+ }, TIME_NORMAL_LOAD);
+ }
+ else {
+ // Abort events can only be triggered on sent requests.
+ req.send();
+ if (this.abortDelay == -1) {
+ abortReq();
+ }
+ else {
+ self.setTimeout(abortReq, this.abortDelay);
+ }
+ }
+ },
+
+ /**
+ * Ensure that no events fired at all, especially not our timeout event.
+ */
+ noEventsFired: function() {
+ ok(!this.hasFired, "No events should fire for an unsent, unaborted request");
+ // We're done; if timeout hasn't fired by now, it never will.
+ TestCounter.testComplete();
+ },
+
+ /**
+ * Get a message describing this test.
+ *
+ * @returns {String} The test description.
+ */
+ getMessage: function() {
+ return "time to abort is " + this.abortDelay + ", timeout set at " + TIME_REGULAR_TIMEOUT;
+ },
+
+ /**
+ * Check the event received, and if it's the right (and only) one we get.
+ *
+ * WebKit fires abort events even for DONE and UNSENT states, which is
+ * discussed in http://webkit.org/b/98404
+ * That's why we chose to accept secondary "abort" events in this test.
+ *
+ * @param {DOMProgressEvent} evt An event of type "load" or "timeout".
+ */
+ handleEvent: function(evt) {
+ if (this.hasFired && evt.type != "abort") {
+ ok(false, "Only abort event should fire: " + this.getMessage());
+ return;
+ }
+
+ var expectedEvent = (this.abortDelay >= TIME_REGULAR_TIMEOUT && !this.hasFired) ? "timeout" : "abort";
+ this.hasFired = true;
+ is(evt.type, expectedEvent, this.getMessage());
+ TestCounter.testComplete();
+ }
+};
+
+function SyncRequestSettingTimeoutAfterOpen() {
+ this.startXHR = function() {
+ var pass = false;
+ var req = new XMLHttpRequest();
+ req.open("GET", STALLED_REQUEST_URL, false);
+ try {
+ req.timeout = TIME_SYNC_TIMEOUT;
+ }
+ catch (e) {
+ pass = true;
+ }
+ ok(pass, "Synchronous XHR must not allow a timeout to be set - setting timeout must throw");
+ TestCounter.testComplete();
+ };
+ return this;
+};
+
+function SyncRequestSettingTimeoutBeforeOpen() {
+ this.startXHR = function() {
+ var pass = false;
+ var req = new XMLHttpRequest();
+ req.timeout = TIME_SYNC_TIMEOUT;
+ try {
+ req.open("GET", STALLED_REQUEST_URL, false);
+ }
+ catch (e) {
+ pass = true;
+ }
+ ok(pass, "Synchronous XHR must not allow a timeout to be set - calling open() after timeout is set must throw");
+ TestCounter.testComplete();
+ }
+ return this;
+};
+
+var TestRequests = [];
+
+// This code controls moving from one test to another.
+var TestCounter = {
+ testComplete: function() {
+ // Allow for the possibility there are other events coming.
+ self.setTimeout(function() {
+ TestCounter.next();
+ }, TIME_NORMAL_LOAD);
+ },
+
+ next: function() {
+ var test = TestRequests.shift();
+
+ if (test) {
+ test.startXHR();
+ }
+ else {
+ message("done");
+ }
+ }
+};
+
+function runTestRequests(testRequests) {
+ if (location.search) {
+ testRequests = testRequests.filter(test => test[2] == decodeURIComponent(location.search.substr(1)));
+ }
+ TestRequests = testRequests.map(test => {
+ var constructor = test.shift();
+ return new self[constructor](...test)
+ });
+ TestCounter.next();
+}
diff --git a/test/wpt/tests/xhr/resources/zlib.py b/test/wpt/tests/xhr/resources/zlib.py
new file mode 100644
index 0000000..6388139
--- /dev/null
+++ b/test/wpt/tests/xhr/resources/zlib.py
@@ -0,0 +1,19 @@
+import zlib
+
+def main(request, response):
+ if b"content" in request.GET:
+ output = request.GET[b"content"]
+ else:
+ output = request.body
+
+ output = zlib.compress(output, 9)
+
+ headers = [(b"Content-type", b"text/plain"),
+ (b"Content-Encoding", b"deflate"),
+ (b"X-Request-Method", request.method),
+ (b"X-Request-Query", request.url_parts.query if request.url_parts.query else b"NO"),
+ (b"X-Request-Content-Length", request.headers.get(b"Content-Length", b"NO")),
+ (b"X-Request-Content-Type", request.headers.get(b"Content-Type", b"NO")),
+ (b"Content-Length", len(output))]
+
+ return headers, output
diff --git a/test/wpt/tests/xhr/response-body-errors.any.js b/test/wpt/tests/xhr/response-body-errors.any.js
new file mode 100644
index 0000000..4edfed0
--- /dev/null
+++ b/test/wpt/tests/xhr/response-body-errors.any.js
@@ -0,0 +1,23 @@
+// This will transmit two chunks TEST_CHUNK and then garbage, which should result in an error.
+const url = "/fetch/api/resources/bad-chunk-encoding.py?ms=1&count=2";
+
+test(() => {
+ client = new XMLHttpRequest();
+ client.open("GET", url, false);
+ assert_throws_dom("NetworkError", () => client.send());
+}, "Synchronous XMLHttpRequest should throw on bad chunk");
+
+async_test(t => {
+ client = new XMLHttpRequest();
+ client.open("GET", url, true);
+ client.onreadystatechange = t.step_func(() => {
+ if (client.readyState === 3) {
+ assert_true(client.responseText.indexOf("TEST_CHUNK") !== -1);
+ }
+ });
+ client.onerror = t.step_func_done(() => {
+ assert_equals(client.responseText, "");
+ });
+ client.onload = t.unreached_func();
+ client.send();
+}, "Asynchronous XMLHttpRequest should clear response on bad chunk");
diff --git a/test/wpt/tests/xhr/response-data-arraybuffer.htm b/test/wpt/tests/xhr/response-data-arraybuffer.htm
new file mode 100644
index 0000000..7eaf719
--- /dev/null
+++ b/test/wpt/tests/xhr/response-data-arraybuffer.htm
@@ -0,0 +1,54 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-responsetype-attribute" data-tested-assertations="following::ol[1]/li[4]" />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-response-attribute" data-tested-assertations="following::a[contains(@href,'#arraybuffer-response-entity-body')]/.." />
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <title>XMLHttpRequest: The response attribute: ArrayBuffer data</title>
+</head>
+
+<body>
+ <div id="log"></div>
+
+ <script type="text/javascript">
+ var test = async_test();
+
+ test.step(function()
+ {
+ var xhr = new XMLHttpRequest();
+
+ xhr.onreadystatechange = function()
+ {
+ if (xhr.readyState == 4)
+ {
+ test.step(function()
+ {
+ assert_equals(xhr.status, 200);
+
+ var buf = xhr.response;
+ assert_true(buf instanceof ArrayBuffer);
+
+ var arr = new Uint8Array(buf);
+ assert_equals(arr.length, 5);
+ assert_equals(arr[0], 0x48, "Expect 'H'");
+ assert_equals(arr[1], 0x65, "Expect 'e'");
+ assert_equals(arr[2], 0x6c, "Expect 'l'");
+ assert_equals(arr[3], 0x6c, "Expect 'l'");
+ assert_equals(arr[4], 0x6f, "Expect 'o'");
+
+ assert_equals(xhr.response, xhr.response,
+ "Response should be cached");
+
+ test.done();
+ });
+ }
+ };
+
+ xhr.open("GET", "./resources/content.py?content=Hello", true);
+ xhr.responseType = "arraybuffer";
+ xhr.send();
+ });
+ </script>
+</body>
+</html>
diff --git a/test/wpt/tests/xhr/response-data-blob.htm b/test/wpt/tests/xhr/response-data-blob.htm
new file mode 100644
index 0000000..19731d3
--- /dev/null
+++ b/test/wpt/tests/xhr/response-data-blob.htm
@@ -0,0 +1,55 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-send()-method" />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-responsetype-attribute" data-tested-assertations="following::ol[1]/li[4]"/>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-response-attribute" data-tested-assertations="following::a[contains(@href,'#blob-response-entity-body')]/.." />
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <title>XMLHttpRequest: The response attribute: Blob data</title>
+</head>
+
+<body>
+ <div id="log"></div>
+
+ <script type="text/javascript">
+ var test = async_test();
+
+ test.step(function()
+ {
+ var xhr = new XMLHttpRequest();
+ var content = "Hello";
+ var blob;
+
+ xhr.onreadystatechange = function()
+ {
+ if (xhr.readyState == 4)
+ {
+ test.step(function()
+ {
+ blob = xhr.response;
+ assert_equals(xhr.response, xhr.response,
+ "Response should be cached");
+ assert_true(blob instanceof Blob, 'blob is a Blob');
+
+ var reader = new FileReader();
+ reader.onload = function()
+ {
+ test.step(function()
+ {
+ assert_equals(reader.result, content);
+ test.done();
+ });
+ };
+ reader.readAsText(blob);
+ });
+ }
+ }
+
+ xhr.open("GET", "./resources/content.py?content=" + content, true);
+ xhr.responseType = "blob";
+ xhr.send();
+ });
+ </script>
+</body>
+</html>
diff --git a/test/wpt/tests/xhr/response-data-deflate.htm b/test/wpt/tests/xhr/response-data-deflate.htm
new file mode 100644
index 0000000..bce2745
--- /dev/null
+++ b/test/wpt/tests/xhr/response-data-deflate.htm
@@ -0,0 +1,42 @@
+<!doctype html>
+<html>
+ <head>
+ <title>XMLHttpRequest: content-encoding:deflate response was correctly inflated</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <link rel="help" href="https://dvcs.w3.org/hg/xhr/raw-file/tip/Overview.html#the-send()-method" data-tested-assertations="following::p[contains(text(),'content-encodings')]" />
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ function request(input) {
+ var test = async_test();
+ test.step(function() {
+ var client = new XMLHttpRequest()
+
+ client.open("POST", "resources/zlib.py", false);
+
+ client.onreadystatechange = test.step_func(function () {
+ if (client.readyState === 4) {
+ var len = parseInt(client.getResponseHeader('content-length'), 10);
+
+ assert_equals(client.getResponseHeader('content-encoding'), 'deflate');
+ assert_true(len < input.length);
+ assert_equals(client.responseText, input);
+ test.done();
+ }
+ });
+
+ client.send(input);
+ });
+ }
+
+ var wellCompressableData = '';
+ for (var i = 0; i < 500; i++) {
+ wellCompressableData += 'foofoofoofoofoofoofoo';
+ }
+
+ request(wellCompressableData);
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/response-data-gzip.htm b/test/wpt/tests/xhr/response-data-gzip.htm
new file mode 100644
index 0000000..a3d2713
--- /dev/null
+++ b/test/wpt/tests/xhr/response-data-gzip.htm
@@ -0,0 +1,42 @@
+<!doctype html>
+<html>
+ <head>
+ <title>XMLHttpRequest: GZIP response was correctly inflated</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <link rel="help" href="https://dvcs.w3.org/hg/xhr/raw-file/tip/Overview.html#the-send()-method" data-tested-assertations="following::p[contains(text(),'content-encodings')]" />
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ function request(input) {
+ var test = async_test();
+ test.step(function() {
+ var client = new XMLHttpRequest()
+
+ client.open("POST", "resources/gzip.py", false);
+
+ client.onreadystatechange = test.step_func(function () {
+ if (client.readyState === 4) {
+ var len = parseInt(client.getResponseHeader('content-length'), 10);
+
+ assert_equals(client.getResponseHeader('content-encoding'), 'gzip');
+ assert_true(len < input.length);
+ assert_equals(client.responseText, input);
+ test.done();
+ }
+ });
+
+ client.send(input);
+ }, document.title);
+ }
+
+ var wellCompressableData = '';
+ for (var i = 0; i < 500; i++) {
+ wellCompressableData += 'foofoofoofoofoofoofoo';
+ }
+
+ request(wellCompressableData);
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/response-data-progress.htm b/test/wpt/tests/xhr/response-data-progress.htm
new file mode 100644
index 0000000..78e7494
--- /dev/null
+++ b/test/wpt/tests/xhr/response-data-progress.htm
@@ -0,0 +1,52 @@
+<!doctype html>
+<html lang="en">
+<head>
+ <meta charset="utf-8">
+ <title>XMLHttpRequest: progress events grow response body size</title>
+ <meta name="timeout" content="long">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-send()-method" data-tested-assertations="following::a[contains(@href,'#make-progress-notifications')]/.. following::a[contains(@href,'#make-progress-notifications')]/../following:p[1]" />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#make-progress-notifications" data-tested-assertations=".." />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#handler-xhr-onprogress" data-tested-assertations="/../.." />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#event-xhr-progress" data-tested-assertations="/../.." />
+</head>
+
+<div id="log"></div>
+
+<script>
+
+function doTest(test, expectedLengthComputable, expectedTotal, url) {
+ var client = new XMLHttpRequest();
+ var lastSize = 0;
+
+ client.onprogress = test.step_func(function(e) {
+ assert_equals(e.total, expectedTotal);
+ assert_equals(e.lengthComputable, expectedLengthComputable);
+
+ var currentSize = client.responseText.length;
+
+ if (lastSize > 0 && currentSize > lastSize) {
+ // growth from a positive size to bigger!
+ test.done();
+ }
+
+ lastSize = currentSize;
+ });
+
+ client.onreadystatechange = test.step_func(function() {
+ if (client.readyState === 4) {
+ assert_unreached("onprogress not called multiple times, or response body did not grow.");
+ }
+ });
+
+ client.open("GET", url);
+ client.send(null);
+ return client;
+}
+
+async_test(function () { doTest(this, false, 0, "resources/trickle.py?count=6&delay=150"); },
+ document.title + ', unknown content-length');
+async_test(function () { doTest(this, true, 78, "resources/trickle.py?count=6&delay=150&specifylength=1"); },
+ document.title + ', known content-length');
+</script>
diff --git a/test/wpt/tests/xhr/response-invalid-responsetype.htm b/test/wpt/tests/xhr/response-invalid-responsetype.htm
new file mode 100644
index 0000000..603c4cd
--- /dev/null
+++ b/test/wpt/tests/xhr/response-invalid-responsetype.htm
@@ -0,0 +1,38 @@
+<!doctype html>
+<html>
+ <head>
+ <title>XMLHttpRequest: response is plain text if responseType is set to an invalid string</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-response-attribute" data-tested-assertations="following::dd[2]/ol[1]/li[2]" />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-responsetype-attribute" data-tested-assertations="following::ol[1]/li[4]" /><!-- Not quite - but this is handled in WebIDL, not the XHR spec -->
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ function request(type) {
+ var test = async_test(document.title+' ('+type+')')
+ test.step(function() {
+ var client = new XMLHttpRequest()
+ client.responseType = type
+ assert_equals(client.responseType, '')
+ client.open("GET", "resources/folder.txt", true)
+ client.onload = function(){
+ test.step(function(){
+ assert_equals(client.responseType, '')
+ assert_equals(client.response, 'bottom\n')
+ assert_equals(typeof client.response, 'string')
+ test.done()
+ })
+ }
+ client.send(null)
+ })
+ }
+ request("arrayBuffer") // case sensitive
+ request("JSON") // case sensitive
+ request("glob")
+ request("txt")
+ request("text/html")
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/response-json.htm b/test/wpt/tests/xhr/response-json.htm
new file mode 100644
index 0000000..a694d7f
--- /dev/null
+++ b/test/wpt/tests/xhr/response-json.htm
@@ -0,0 +1,61 @@
+<!doctype html>
+<html>
+ <head>
+ <title>XMLHttpRequest: responseType json</title>
+ <meta charset="utf-8">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-responsetype-attribute" data-tested-assertations="following::OL[1]/LI[4]" />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-response-attribute" data-tested-assertations="following::dt[2]/dt[4] following::dt[2]/dt[4]/following::dd[1]" />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#json-response-entity-body" data-tested-assertations="following::ol[1]/li[1] following::ol[1]/li[2] following::ol[1]/li[3]" />
+
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ function setupXHR () {
+ var client = new XMLHttpRequest()
+ client.open('POST', "resources/content.py", true)
+ client.responseType = 'json'
+ return client
+ }
+ function makeTest(data, expectedResponse, description){
+ var test = async_test(description)
+ var xhr = setupXHR()
+ assert_equals(xhr.responseType, 'json')
+ xhr.onreadystatechange = function(){
+ if(xhr.readyState === 4){
+ test.step(function(){
+ assert_equals(xhr.status, 200)
+ assert_equals(xhr.responseType, 'json')
+ assert_equals(typeof xhr.response, 'object')
+ if(expectedResponse){ // if the expectedResponse is not null, we iterate over properties to do a deeper comparison..
+ for(var prop in expectedResponse){
+ if (expectedResponse[prop] instanceof Array) {
+ assert_array_equals(expectedResponse[prop], xhr.response[prop])
+ }else{
+ assert_equals(expectedResponse[prop], xhr.response[prop])
+ }
+ }
+ }else{
+ assert_equals(xhr.response, expectedResponse) // null comparison, basically
+ }
+ assert_equals(xhr.response, xhr.response,
+ "Response should be cached")
+ test.done()
+ })
+ }
+ }
+ xhr.send(data)
+ }
+ // no data
+ makeTest("", null, 'json response with no data: response property is null')
+ // malformed
+ makeTest('{"test":"foo"', null, 'json response with malformed data: response property is null')
+ // real object
+ var obj = {alpha:'a-z', integer:15003, negated:-20, b1:true, b2:false, myAr:['a', 'b', 'c', 1, 2, 3]}
+ makeTest(JSON.stringify(obj), obj, 'JSON object roundtrip')
+ makeTest('{"日本語":"ã«ã»ã‚“ã”"}', {"日本語":"ã«ã»ã‚“ã”"}, 'JSON roundtrip with Japanese text')
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/response-method.htm b/test/wpt/tests/xhr/response-method.htm
new file mode 100644
index 0000000..1bf26ba
--- /dev/null
+++ b/test/wpt/tests/xhr/response-method.htm
@@ -0,0 +1,21 @@
+<!doctype html>
+<html>
+ <head>
+ <title>XMLHttpRequest: influence of HTTP method on response</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ ["GET", "HEAD", "POST"].forEach(function(method) {
+ test(function() {
+ var client = new XMLHttpRequest()
+ client.open(method, "resources/echo-method.py", false)
+ client.send()
+ assert_equals(client.responseText, (method === "HEAD" ? "" : method))
+ }, method)
+ })
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/responseText-status.html b/test/wpt/tests/xhr/responseText-status.html
new file mode 100644
index 0000000..7d57590
--- /dev/null
+++ b/test/wpt/tests/xhr/responseText-status.html
@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>XMLHttpRequest Test: responseText - status</title>
+<link rel="author" title="Intel" href="http://www.intel.com">
+<meta name="assert" content="Check if XMLHttpRequest.responseText return empty string if state is not LOADING or DONE">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<div id="log"></div>
+
+<script>
+
+async_test(function (t) {
+ var client = new XMLHttpRequest();
+ t.step(function () {
+ assert_equals(client.responseText, "");
+ });
+
+ client.onreadystatechange = t.step_func(function () {
+ if (client.readyState == 1 || client.readyState == 2) {
+ assert_equals(client.responseText, "");
+ }
+
+ if (client.readyState == 3) {
+ t.done();
+ }
+ });
+
+ client.open("GET", "resources/headers.py")
+ client.send(null)
+}, document.title);
+
+</script>
diff --git a/test/wpt/tests/xhr/responseType-document-in-worker.html b/test/wpt/tests/xhr/responseType-document-in-worker.html
new file mode 100644
index 0000000..9a04320
--- /dev/null
+++ b/test/wpt/tests/xhr/responseType-document-in-worker.html
@@ -0,0 +1,13 @@
+<!doctype html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+</head>
+<body>
+<script>
+ fetch_tests_from_worker(new Worker("resources/responseType-document-in-worker.js"));
+</script>
+</body>
+</html>
diff --git a/test/wpt/tests/xhr/responseXML-unavailable-in-worker.html b/test/wpt/tests/xhr/responseXML-unavailable-in-worker.html
new file mode 100644
index 0000000..dcb4cb0
--- /dev/null
+++ b/test/wpt/tests/xhr/responseXML-unavailable-in-worker.html
@@ -0,0 +1,13 @@
+<!doctype html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+</head>
+<body>
+<script>
+ fetch_tests_from_worker(new Worker("resources/responseXML-unavailable-in-worker.js"));
+</script>
+</body>
+</html>
diff --git a/test/wpt/tests/xhr/responsedocument-decoding.htm b/test/wpt/tests/xhr/responsedocument-decoding.htm
new file mode 100644
index 0000000..7fb4b2d
--- /dev/null
+++ b/test/wpt/tests/xhr/responsedocument-decoding.htm
@@ -0,0 +1,39 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>XMLHttpRequest: response document decoding</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ </head>
+ <body>
+ <script>
+ function request(type, input, expected) {
+ async_test((test) => {
+ const client = new XMLHttpRequest();
+ client.responseType = 'document';
+ client.open("GET", "resources/status.py?content=" + input + "&type=" + encodeURIComponent(type), true);
+ client.onload = test.step_func_done(() => {
+ assert_equals(client.responseXML.querySelector("x").textContent, expected);
+ })
+ client.send(null);
+ }, document.title + " (" + type + " " + input + ")");
+ }
+
+ const encoded_content = "%e6%a9%9f";
+ const encoded_xml =
+ encodeURIComponent("<?xml version='1.0' encoding='windows-1252'?><x>") + encoded_content + encodeURIComponent("<\/x>");
+ const encoded_html =
+ encodeURIComponent("<!doctype html><meta charset=windows-1252><x>") + encoded_content + encodeURIComponent("<\/x>");
+ const decoded_as_windows_1252 = "\u00e6\u00a9\u0178";
+ const decoded_as_utf_8 = "\u6a5f";
+
+ request("application/xml", encoded_xml, decoded_as_windows_1252);
+ request("application/xml;charset=utf-8", encoded_xml, decoded_as_utf_8);
+ request("application/xml;charset=windows-1252", encoded_xml, decoded_as_windows_1252);
+ request("text/html", encoded_html, decoded_as_windows_1252);
+ request("text/html;charset=utf-8", encoded_html, decoded_as_utf_8);
+ request("text/html;charset=windows-1252", encoded_html, decoded_as_windows_1252);
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/responsetext-decoding.htm b/test/wpt/tests/xhr/responsetext-decoding.htm
new file mode 100644
index 0000000..fae0104
--- /dev/null
+++ b/test/wpt/tests/xhr/responsetext-decoding.htm
@@ -0,0 +1,93 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>XMLHttpRequest: responseText decoding</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ </head>
+ <body>
+ <script>
+ function create_html(content) {
+ return "<!doctype html><meta charset=windows-1252><x>" + content + "</x>";
+ }
+ function create_encoded_html(encoded_content) {
+ return encodeURIComponent("<!doctype html><meta charset=windows-1252><x>") + encoded_content + encodeURIComponent("<\/x>");
+ }
+ function create_xml(content) {
+ return "<?xml version='1.0' encoding='windows-1252'?><x>" + content + "</x>";
+ }
+ function create_encoded_xml(encoded_content) {
+ return encodeURIComponent("<?xml version='1.0' encoding='windows-1252'?><x>") + encoded_content + encodeURIComponent("<\/x>");
+ }
+ function request(type, input, output, responseType) {
+ async_test((test) => {
+ const client = new XMLHttpRequest();
+ if (responseType !== undefined) {
+ client.responseType = responseType;
+ }
+ client.open("GET", "resources/status.py?content=" + input + "&type=" + encodeURIComponent(type), true);
+ client.onload = test.step_func_done(() => {
+ assert_equals(client.responseText, output);
+ })
+ client.send(null);
+ }, document.title + " (" + type + " " + input + " " + (responseType ? " " + responseType : "empty") + ")");
+ }
+
+ const encoded_content = "%e6%a9%9f";
+ const decoded_as_windows_1252 = "\u00e6\u00a9\u0178";
+ const decoded_as_utf_8 = "\u6a5f";
+ const encoded_xml = create_encoded_xml(encoded_content);
+ const encoded_html = create_encoded_html(encoded_content);
+ const xml_decoded_as_windows_1252 = create_xml(decoded_as_windows_1252);
+ const xml_decoded_as_utf_8 = create_xml(decoded_as_utf_8);
+ const html_decoded_as_windows_1252 = create_html(decoded_as_windows_1252);
+ const html_decoded_as_utf_8 = create_html(decoded_as_utf_8);
+
+ // "default" response type
+ // An XML-ish response is sniffed.
+ request("application/xml", encoded_xml, xml_decoded_as_windows_1252);
+ // An HTML-ish response isn't sniffed.
+ request("text/html", encoded_html, html_decoded_as_utf_8);
+ request("application/xml;charset=utf-8", encoded_xml, xml_decoded_as_utf_8);
+ request("application/xml;charset=windows-1252", encoded_xml, xml_decoded_as_windows_1252);
+ request("text/html;charset=utf-8", encoded_html, html_decoded_as_utf_8);
+ request("text/html;charset=windows-1252", encoded_html, html_decoded_as_windows_1252);
+ request("text/plain;charset=windows-1252", "%FF", "\u00FF");
+ request("text/plain", "%FF", "\uFFFD");
+ request("text/plain", "%FE%FF", "");
+ request("text/plain", "%FE%FF%FE%FF", "\uFEFF");
+ request("text/plain", "%EF%BB%BF", "");
+ request("text/plain", "%EF%BB%BF%EF%BB%BF", "\uFEFF");
+ request("text/plain", "%C2", "\uFFFD");
+ request("text/xml", "%FE%FF", "");
+ request("text/xml", "%FE%FF%FE%FF", "\uFEFF");
+ request("text/xml", "%EF%BB%BF", "");
+ request("text/xml", "%EF%BB%BF%EF%BB%BF", "\uFEFF");
+ request("text/plain", "%E3%81%B2", "\u3072");
+
+ // "text" response type
+ // An XML-ish response isn't sniffed.
+ request("application/xml", encoded_xml, xml_decoded_as_utf_8, "text");
+ // An HTML-ish response isn't sniffed.
+ request("text/html", encoded_html, html_decoded_as_utf_8, "text");
+ request("application/xml;charset=utf-8", encoded_xml, xml_decoded_as_utf_8, "text");
+ request("application/xml;charset=windows-1252", encoded_xml, xml_decoded_as_windows_1252, "text");
+ request("text/html;charset=utf-8", encoded_html, html_decoded_as_utf_8, "text");
+ request("text/html;charset=windows-1252", encoded_html, html_decoded_as_windows_1252, "text");
+ request("text/plain;charset=windows-1252", "%FF", "\u00FF", "text");
+ request("text/plain", "%FF", "\uFFFD", "text");
+ request("text/plain", "%FE%FF", "", "text");
+ request("text/plain", "%FE%FF%FE%FF", "\uFEFF", "text");
+ request("text/plain", "%EF%BB%BF", "", "text");
+ request("text/plain", "%EF%BB%BF%EF%BB%BF", "\uFEFF", "text");
+ request("text/plain", "%C2", "\uFFFD", "text");
+ request("text/plain;charset=bogus", "%C2", "\uFFFD", "text");
+ request("text/xml", "%FE%FF", "", "text");
+ request("text/xml", "%FE%FF%FE%FF", "\uFEFF", "text");
+ request("text/xml", "%EF%BB%BF", "", "text");
+ request("text/xml", "%EF%BB%BF%EF%BB%BF", "\uFEFF", "text");
+ request("text/plain", "%E3%81%B2", "\u3072", "text");
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/responsetype.any.js b/test/wpt/tests/xhr/responsetype.any.js
new file mode 100644
index 0000000..444c3e3
--- /dev/null
+++ b/test/wpt/tests/xhr/responsetype.any.js
@@ -0,0 +1,135 @@
+// META: title=XMLHttpRequest.responseType
+
+/**
+ * Author: Mathias Bynens <http://mathiasbynens.be/>
+ * Author: Ms2ger <mailto:Ms2ger@gmail.com>
+ *
+ * Spec: <https://xhr.spec.whatwg.org/#the-responsetype-attribute>
+ */
+test(function() {
+ var xhr = new XMLHttpRequest();
+ assert_equals(xhr.responseType, '');
+}, 'Initial value of responseType');
+
+var types = ['', 'json', 'document', 'arraybuffer', 'blob', 'text', "nosuchtype"];
+
+function isIgnoredType(type) {
+ if (type == "nosuchtype") {
+ return true;
+ }
+
+ if (type != "document") {
+ return false;
+ }
+
+ // "document" is ignored only on workers.
+ return GLOBAL.isWorker();
+}
+
+function expectedType(type) {
+ if (!isIgnoredType(type)) {
+ return type;
+ }
+
+ return "";
+}
+
+types.forEach(function(type) {
+ test(function() {
+ var xhr = new XMLHttpRequest();
+ xhr.responseType = type;
+ assert_equals(xhr.responseType, expectedType(type));
+ }, 'Set responseType to ' + format_value(type) + ' when readyState is UNSENT.');
+
+ test(function() {
+ var xhr = new XMLHttpRequest();
+ xhr.open('get', '/');
+ xhr.responseType = type;
+ assert_equals(xhr.responseType, expectedType(type));
+ }, 'Set responseType to ' + format_value(type) + ' when readyState is OPENED.');
+
+ async_test(function() {
+ var xhr = new XMLHttpRequest();
+ xhr.open('get', '/');
+ xhr.onreadystatechange = this.step_func(function() {
+ if (xhr.readyState === XMLHttpRequest.HEADERS_RECEIVED) {
+ xhr.responseType = type;
+ assert_equals(xhr.responseType, expectedType(type));
+ this.done();
+ }
+ });
+ xhr.send();
+ }, 'Set responseType to ' + format_value(type) + ' when readyState is HEADERS_RECEIVED.');
+
+ async_test(function() {
+ var xhr = new XMLHttpRequest();
+ xhr.open('get', '/');
+ xhr.onreadystatechange = this.step_func(function() {
+ if (xhr.readyState === XMLHttpRequest.LOADING) {
+ if (isIgnoredType(type)) {
+ xhr.responseType = type;
+ } else {
+ assert_throws_dom("InvalidStateError", function() {
+ xhr.responseType = type;
+ });
+ }
+ assert_equals(xhr.responseType, "");
+ this.done();
+ }
+ });
+ xhr.send();
+ }, 'Set responseType to ' + format_value(type) + ' when readyState is LOADING.');
+
+ async_test(function() {
+ var xhr = new XMLHttpRequest();
+ xhr.open('get', '/');
+ xhr.onreadystatechange = this.step_func(function() {
+ if (xhr.readyState === XMLHttpRequest.DONE) {
+ var text = xhr.responseText;
+ assert_not_equals(text, "");
+ if (isIgnoredType(type)) {
+ xhr.responseType = type;
+ } else {
+ assert_throws_dom("InvalidStateError", function() {
+ xhr.responseType = type;
+ });
+ }
+ assert_equals(xhr.responseType, "");
+ assert_equals(xhr.responseText, text);
+ this.done();
+ }
+ });
+ xhr.send();
+ }, 'Set responseType to ' + format_value(type) + ' when readyState is DONE.');
+
+ // Note: the case of setting responseType first, and then calling synchronous
+ // open(), is tested in open-method-responsetype-set-sync.htm.
+ test(function() {
+ var xhr = new XMLHttpRequest();
+ xhr.open('get', '/', false);
+ if (GLOBAL.isWorker() || isIgnoredType(type)) {
+ // Setting responseType on workers is valid even for a sync XHR.
+ xhr.responseType = type;
+ assert_equals(xhr.responseType, expectedType(type));
+ } else {
+ assert_throws_dom("InvalidAccessError", function() {
+ xhr.responseType = type;
+ });
+ }
+ }, 'Set responseType to ' + format_value(type) + ' when readyState is OPENED and the sync flag is set.');
+
+ test(function() {
+ var xhr = new XMLHttpRequest();
+ xhr.open('get', '/', false);
+ xhr.send();
+ assert_equals(xhr.readyState, XMLHttpRequest.DONE);
+ if (isIgnoredType(type)) {
+ xhr.responseType = type;
+ } else {
+ assert_throws_dom("InvalidStateError", function() {
+ xhr.responseType = type;
+ });
+ }
+ assert_equals(xhr.responseType, "");
+ }, 'Set responseType to ' + format_value(type) + ' when readyState is DONE and the sync flag is set.');
+});
diff --git a/test/wpt/tests/xhr/responseurl.html b/test/wpt/tests/xhr/responseurl.html
new file mode 100644
index 0000000..b730e04
--- /dev/null
+++ b/test/wpt/tests/xhr/responseurl.html
@@ -0,0 +1,37 @@
+<!doctype html>
+<html>
+ <head>
+ <title>XMLHttpRequest: responseURL test</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-responseurl-attribute"/>
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ test(function() {
+ var client = new XMLHttpRequest()
+ assert_equals(client.responseURL, "")
+
+ client.open("GET", "foo.html", false)
+ client.send()
+
+ expected = location.href.replace(/[^/]*$/, 'foo.html')
+ assert_equals(client.status, 404)
+ assert_equals(client.responseURL, expected)
+ }, "404 response has proper responseURL")
+ test(function() {
+ var client = new XMLHttpRequest()
+ assert_equals(client.responseURL, "")
+
+ target = "image.gif"
+ client.open("GET", "resources/redirect.py?location=" + target, false)
+ client.send()
+
+ expected = location.href.replace(/[^/]*$/, "resources/" + target)
+ assert_equals(client.status, 200)
+ assert_equals(client.responseURL, expected)
+ }, "Redirected response has proper responseURL")
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/responsexml-basic.htm b/test/wpt/tests/xhr/responsexml-basic.htm
new file mode 100644
index 0000000..a3ce7b5
--- /dev/null
+++ b/test/wpt/tests/xhr/responsexml-basic.htm
@@ -0,0 +1,33 @@
+<!doctype html>
+<html>
+ <head>
+ <title>XMLHttpRequest: responseXML basic test</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-responsexml-attribute" data-tested-assertations="following::ol[1]/li[2] following::ol[1]/li[4]" />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#document-response-entity-body" data-tested-assertations="following::ol[1]/li[1] following::ol[1]/li[6] following::ol[1]/li[10]" />
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ test(function() {
+ var client = new XMLHttpRequest()
+ assert_equals(client.responseXML, null)
+ client.open("GET", "resources/well-formed.xml", false)
+ assert_equals(client.responseXML, null)
+ client.send(null)
+ assert_equals(client.responseXML.documentElement.localName, "html", 'localName is html')
+ assert_equals(client.responseXML.documentElement.childNodes.length, 5, 'childNodes is 5')
+ assert_equals(client.responseXML.getElementById("n1").localName, client.responseXML.documentElement.childNodes[1].localName)
+ assert_equals(client.responseXML.getElementById("n2"), client.responseXML.documentElement.childNodes[3], 'getElementById("n2")')
+ assert_equals(client.responseXML.getElementsByTagName("p")[1].namespaceURI, "namespacesarejuststrings", 'namespaceURI')
+ })
+ test(function() {
+ var client = new XMLHttpRequest()
+ client.open("GET", "resources/status.py?type=application/xml", false)
+ client.send(null)
+ assert_equals(client.responseXML, null)
+ }, 'responseXML on empty response documents')
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/responsexml-document-properties.htm b/test/wpt/tests/xhr/responsexml-document-properties.htm
new file mode 100644
index 0000000..a9f14f8
--- /dev/null
+++ b/test/wpt/tests/xhr/responsexml-document-properties.htm
@@ -0,0 +1,123 @@
+<!doctype html>
+<html>
+ <head>
+ <title>XMLHttpRequest: responseXML document properties</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ var timePreXHR = Math.floor(new Date().getTime(new Date().getTime() - 3000) / 1000); // three seconds ago, in case there's clock drift
+ var client = new XMLHttpRequest()
+ client.open("GET", "resources/well-formed.xml", false)
+ client.send(null)
+ var responseURLObject = new URL('resources/well-formed.xml', location.href);
+ var responseURL = responseURLObject.href
+ var responseDomain = responseURLObject.hostname
+ var expected = {
+ domain:responseDomain,
+ URL:responseURL,
+ documentURI:responseURL,
+ baseURI:responseURL,
+ referrer:'',
+ title:'',
+ contentType:'application/xml',
+ readyState:'complete',
+ location:null,
+ defaultView:null,
+ body:null,
+ doctype:null,
+ all:HTMLAllCollection,
+ cookie:''
+ }
+
+ for (var name in expected) {
+ runTest(name, expected[name])
+ }
+
+ function runTest(name, value){
+ test(function(){
+ if (name == "all") {
+ assert_equals(Object.getPrototypeOf(client.responseXML[name]), value.prototype)
+ } else {
+ assert_equals(client.responseXML[name], value)
+ }
+ }, name)
+ }
+
+ // Parse a "lastModified" value and convert it to a Date.
+ // See https://html.spec.whatwg.org/multipage/dom.html#dom-document-lastmodified
+ function parseLastModified(value) {
+ const [undefined, month, day, year, hours, minutes, seconds] =
+ /^(\d\d)\/(\d\d)\/(\d+) (\d\d):(\d\d):(\d\d)$/.exec(value);
+ return new Date(year, month - 1, day, hours, minutes, seconds);
+ }
+
+ async_test(t => {
+ const client = new XMLHttpRequest();
+ client.open("GET", "resources/redirect.py?location=well-formed.xml");
+ client.send();
+ client.onload = t.step_func_done(() => {
+ assert_equals(client.responseXML.URL, responseURL);
+ assert_equals(client.responseXML.baseURI, responseURL);
+ });
+ }, "Test document URL properties after redirect");
+
+ async_test(t => {
+ const client = new XMLHttpRequest();
+ client.open("GET", "resources/redirect.py?location=base.xml");
+ client.send();
+ client.onload = t.step_func_done(() => {
+ const localResponseURL = new URL('resources/base.xml', location.href).href;
+ assert_equals(client.responseXML.URL, localResponseURL);
+ assert_equals(client.responseXML.baseURI, 'https://example.com/');
+ client.responseXML.documentElement.remove();
+ assert_equals(client.responseXML.baseURI, localResponseURL);
+ const newBase = document.createElement("base"),
+ newBaseURL = "https://elsewhere.example/";
+ newBase.href = "https://elsewhere.example/";
+ client.responseXML.appendChild(newBase);
+ assert_equals(client.responseXML.baseURI, newBaseURL);
+ newBase.remove();
+ document.head.appendChild(newBase);
+ assert_equals(client.responseXML.baseURI, localResponseURL);
+ newBase.remove();
+ });
+ }, "Test document URL properties of document with <base> after redirect");
+
+ test(function() {
+ var lastModified = Math.floor(parseLastModified(client.responseXML.lastModified).getTime() / 1000);
+ var now = Math.floor(new Date().getTime(new Date().getTime() + 3000) / 1000); // three seconds from now, in case there's clock drift
+ assert_greater_than_equal(lastModified, timePreXHR);
+ assert_less_than_equal(lastModified, now);
+ }, 'lastModified set to time of response if no HTTP header provided')
+
+ test(function() {
+ var client2 = new XMLHttpRequest()
+ client2.open("GET", "resources/last-modified.py", false)
+ client2.send(null)
+ assert_equals((new Date(client2.getResponseHeader('Last-Modified'))).getTime(), (parseLastModified(client2.responseXML.lastModified)).getTime())
+ }, 'lastModified set to related HTTP header if provided')
+
+ test(function() {
+ client.responseXML.cookie = "thisshouldbeignored"
+ assert_equals(client.responseXML.cookie, "")
+ }, 'cookie (after setting it)')
+
+ var objectProps = [
+ "styleSheets",
+ "implementation",
+ "images",
+ "forms",
+ "links",
+ ];
+
+ for (let prop of objectProps) {
+ test(function() {
+ assert_equals(typeof(client.responseXML[prop]), "object")
+ }, prop + " should be an object")
+ }
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/responsexml-get-twice.htm b/test/wpt/tests/xhr/responsexml-get-twice.htm
new file mode 100644
index 0000000..e86a6d5
--- /dev/null
+++ b/test/wpt/tests/xhr/responsexml-get-twice.htm
@@ -0,0 +1,66 @@
+<!doctype html>
+<meta charset="utf-8">
+<title></title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+ async_test(function() {
+ var client = new XMLHttpRequest()
+ client.open("GET", "resources/well-formed.xml")
+ client.responseType = "document"
+ assert_equals(client.responseType, "document")
+ client.send()
+ client.onload = this.step_func_done(function() {
+ var first = client.response
+ var second = client.response
+ assert_not_equals(first, null)
+ assert_not_equals(second, null)
+ assert_equals(first, second)
+ })
+ }, "Getting response, then response")
+
+ async_test(function() {
+ var client = new XMLHttpRequest()
+ client.open("GET", "resources/well-formed.xml")
+ client.responseType = "document"
+ assert_equals(client.responseType, "document")
+ client.send()
+ client.onload = this.step_func_done(function() {
+ var first = client.responseXML
+ var second = client.responseXML
+ assert_not_equals(first, null)
+ assert_not_equals(second, null)
+ assert_equals(first, second)
+ })
+ }, "Getting responseXML, then responseXML")
+
+ async_test(function() {
+ var client = new XMLHttpRequest()
+ client.open("GET", "resources/well-formed.xml")
+ client.responseType = "document"
+ assert_equals(client.responseType, "document")
+ client.send()
+ client.onload = this.step_func_done(function() {
+ var first = client.responseXML
+ var second = client.response
+ assert_not_equals(first, null)
+ assert_not_equals(second, null)
+ assert_equals(first, second)
+ })
+ }, "Getting responseXML, then response")
+
+ async_test(function() {
+ var client = new XMLHttpRequest()
+ client.open("GET", "resources/well-formed.xml")
+ client.responseType = "document"
+ assert_equals(client.responseType, "document")
+ client.send()
+ client.onload = this.step_func_done(function() {
+ var first = client.response
+ var second = client.responseXML
+ assert_not_equals(first, null)
+ assert_not_equals(second, null)
+ assert_equals(first, second)
+ })
+ }, "Getting response, then responseXML")
+</script>
diff --git a/test/wpt/tests/xhr/responsexml-invalid-type.html b/test/wpt/tests/xhr/responsexml-invalid-type.html
new file mode 100644
index 0000000..57ba462
--- /dev/null
+++ b/test/wpt/tests/xhr/responsexml-invalid-type.html
@@ -0,0 +1,21 @@
+<!doctype html>
+<html>
+<head>
+<title>XMLHttpRequest: response with an invalid responseXML document</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+</head>
+<body>
+<script>
+promise_test(async () => {
+ var xhr = new XMLHttpRequest();
+ xhr.responseType = "document";
+ xhr.open("GET", "resources/top.txt");
+ xhr.send();
+ await new Promise(r => xhr.onreadystatechange = () => { if (xhr.readyState == xhr.DONE) r(); });
+ assert_equals(xhr.responseXML, null);
+ assert_equals(xhr.response, null);
+});
+</script>
+</body>
+</html>
diff --git a/test/wpt/tests/xhr/responsexml-media-type.htm b/test/wpt/tests/xhr/responsexml-media-type.htm
new file mode 100644
index 0000000..ece413d
--- /dev/null
+++ b/test/wpt/tests/xhr/responsexml-media-type.htm
@@ -0,0 +1,41 @@
+<!doctype html>
+<html>
+ <head>
+ <title>XMLHttpRequest: responseXML MIME type tests</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-responsexml-attribute" data-tested-assertations="following::ol[1]/li[4]" />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#document-response-entity-body" data-tested-assertations="following::ol[1]/li[3] following::ol[1]/li[4] following::ol[1]/li[6] following::ol[1]/li[10]" />
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ function request(type, succeed) {
+ test(function() {
+ var client = new XMLHttpRequest()
+ client.open("GET", "resources/status.py?content=<x><\/x>&type=" + encodeURIComponent(type), false)
+ client.send(null)
+ if(!succeed)
+ assert_equals(client.responseXML, null)
+ else
+ assert_equals(client.responseXML.documentElement.localName, "x")
+ }, document.title + " ('" + type + "', should "+(succeed?'':'not')+" parse)")
+ }
+ request("", true)
+ request("text/html", false)
+ request("bogus", true)
+ request("bogus+xml", true)
+ request("text/plain;+xml", false)
+ request("text/plainxml", false)
+ request("video/x-awesome+xml", true)
+ request("video/x-awesome", false)
+ request("text/xml", true)
+ request("application", true)
+ request("text/xsl", false)
+ request("text/plain", false)
+ request("application/rdf", false)
+ request("application/xhtml+xml", true)
+ request("image/svg+xml", true)
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/responsexml-non-document-types.htm b/test/wpt/tests/xhr/responsexml-non-document-types.htm
new file mode 100644
index 0000000..6d7feea
--- /dev/null
+++ b/test/wpt/tests/xhr/responsexml-non-document-types.htm
@@ -0,0 +1,45 @@
+<!doctype html>
+<html>
+ <head>
+ <title>XMLHttpRequest: responseXML/responseText on other responseType</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-responsexml-attribute" data-tested-assertations="following::ol[1]/li[1]" />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-responsetext-attribute" data-tested-assertations="following::ol[1]/li[1]" />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-responsetype-attribute" data-tested-assertations="following::ol[1]/li[4]" />
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ function request(type) {
+ var test = async_test(document.title+' ('+type+')')
+ test.step(function() {
+ var client = new XMLHttpRequest()
+ client.responseType = type
+ client.open("GET", "resources/well-formed.xml", true)
+ client.onload = function(){
+ test.step(function(){
+ if(type !== 'document'){
+ assert_throws_dom("InvalidStateError", function() {
+ var x = client.responseXML;
+ }, 'responseXML throw for '+type)
+ }
+ if(type !== 'text'){
+ assert_throws_dom("InvalidStateError", function() {
+ var x = client.responseText;
+ }, 'responseText throws for '+type)
+ }
+ test.done()
+ })
+ }
+ client.send(null)
+ })
+ }
+ request("arraybuffer")
+ request("blob")
+ request("json")
+ request("text")
+ request("document")
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/responsexml-non-well-formed.htm b/test/wpt/tests/xhr/responsexml-non-well-formed.htm
new file mode 100644
index 0000000..216da81
--- /dev/null
+++ b/test/wpt/tests/xhr/responsexml-non-well-formed.htm
@@ -0,0 +1,30 @@
+<!doctype html>
+<html>
+ <head>
+ <title>XMLHttpRequest: responseXML non well-formed tests</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-responsexml-attribute" data-tested-assertations="following::ol[1]/li[4]" />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#document-response-entity-body" data-tested-assertations="following::ol[1]/li[6] following::ol[1]/li[10]" />
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ function request(content) {
+ test(function() {
+ var client = new XMLHttpRequest()
+ client.open("GET", "resources/status.py?type=text/xml&content=" + encodeURIComponent(content), false)
+ client.send(null)
+ assert_equals(client.responseXML, null)
+ })
+ }
+ request("<x")
+ request("<x></x")
+ request("<x>&amp</x>")
+ request("<x><y></x></y>") // misnested tags
+ request("<x></x><y></y>") // two root elements is not allowed
+ request("<x> <![CDATA[ foobar ]></x>") // CDATA should end with ]]>
+ request("<x> <!CDATA[ foobar ]]></x>") // CDATA should start with <![
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/security-consideration.sub.html b/test/wpt/tests/xhr/security-consideration.sub.html
new file mode 100644
index 0000000..a364e2c
--- /dev/null
+++ b/test/wpt/tests/xhr/security-consideration.sub.html
@@ -0,0 +1,36 @@
+<!doctype html>
+<html>
+ <head>
+ <title>ProgressEvent: security consideration</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#security-considerations" data-tested-assertations="/following-sibling::p" />
+ <link rel="help" href="https://fetch.spec.whatwg.org/#http-fetch" data-tested-assertations="/following-sibling::ol[1]/li[3]/ol[1]/li[6]" />
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ async_test(function() {
+ var xhr = new XMLHttpRequest();
+
+ xhr.onprogress = this.unreached_func("MUST NOT dispatch progress event.");
+ xhr.onload = this.unreached_func("MUST NOT dispatch load event.");
+ xhr.onerror = this.step_func(function(pe) {
+ assert_equals(pe.type, "error");
+ assert_equals(pe.loaded, 0, "loaded is zero.");
+ assert_false(pe.lengthComputable, "lengthComputable is false.");
+ assert_equals(pe.total, 0, "total is zero.");
+ });
+ xhr.onloadend = this.step_func(function(pe) {
+ assert_equals(pe.type, "loadend");
+ assert_equals(pe.loaded, 0, "loaded is zero.");
+ assert_false(pe.lengthComputable, "lengthComputable is false.");
+ assert_equals(pe.total, 0, "total is zero.");
+ this.done();
+ });
+ xhr.open("GET", "http://{{host}}:{{ports[http][1]}}/xhr/resources/img.jpg", true);
+ xhr.send(null);
+ })
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/send-accept-language.htm b/test/wpt/tests/xhr/send-accept-language.htm
new file mode 100644
index 0000000..737b5c2
--- /dev/null
+++ b/test/wpt/tests/xhr/send-accept-language.htm
@@ -0,0 +1,27 @@
+<!doctype html>
+<html>
+ <head>
+ <title>XMLHttpRequest: send() - Accept-Language</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-send()-method">
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ test(function() {
+ var client = new XMLHttpRequest()
+ client.open('GET', 'resources/inspect-headers.py?filter_name=accept-language', false)
+ client.send(null)
+ assert_regexp_match(client.responseText, /Accept-Language:\s.+/)
+ }, 'Send "sensible" default value, whatever that means')
+ test(function() {
+ var client = new XMLHttpRequest()
+ client.open("GET", "resources/inspect-headers.py?filter_name=accept-language", false)
+ client.setRequestHeader("Accept-Language", "x-GameSpeak")
+ client.send(null)
+ assert_equals(client.responseText, "Accept-Language: x-GameSpeak\n")
+ })
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/send-accept.htm b/test/wpt/tests/xhr/send-accept.htm
new file mode 100644
index 0000000..2731eb6
--- /dev/null
+++ b/test/wpt/tests/xhr/send-accept.htm
@@ -0,0 +1,24 @@
+<!doctype html>
+<html>
+ <head>
+ <title>XMLHttpRequest: send() - Accept</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-send()-method" data-tested-assertations="following::code[contains(text(),'*/*')]/.. following::code[contains(text(),'Accept')]/.. following::code[contains(text(),'Accept')]/../following::ul[1]/li[1]" />
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ test(function() {
+ var client = new XMLHttpRequest()
+ client.open("GET", "resources/accept.py", false)
+ client.send(null)
+ assert_equals(client.responseText, "*/*")
+ client.open("GET", "resources/accept.py", false)
+ client.setRequestHeader("Accept", "x-something/vague, text/html5")
+ client.send(null)
+ assert_equals(client.responseText, "x-something/vague, text/html5")
+ })
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/send-after-setting-document-domain.htm b/test/wpt/tests/xhr/send-after-setting-document-domain.htm
new file mode 100644
index 0000000..49eeb95
--- /dev/null
+++ b/test/wpt/tests/xhr/send-after-setting-document-domain.htm
@@ -0,0 +1,39 @@
+<!doctype html>
+<html>
+ <head>
+ <title>XMLHttpRequest: send() with document.domain set</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-open()-method" data-tested-assertations="following::ol[1]/li[2]/ol[1]/li[3]" />
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ var test_base_url = location.protocol+'//www2.'+location.host+"/xhr/resources/",
+ test_windows = [
+ window.open(test_base_url + "send-after-setting-document-domain-window-1.htm"),
+ window.open(test_base_url + "send-after-setting-document-domain-window-2.htm"),
+ ],
+ num_tests_left = test_windows.length;
+
+ async_test(function(wrapper_test) {
+ window.addEventListener("message", function(evt) {
+ // run a shadow test that just forwards the results
+ async_test(function(test) {
+ assert_true(evt.data.passed, evt.data.message);
+ test.done();
+ }, evt.data.name);
+
+ // after last result comes in, close all test
+ // windows and complete the wrapper test.
+ if (--num_tests_left == 0) {
+ for (var i=0; i<test_windows.length; ++i) {
+ test_windows[i].close();
+ }
+ wrapper_test.done();
+ }
+ }, false);
+ }, "All tests ran");
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/send-authentication-basic-cors-not-enabled.htm b/test/wpt/tests/xhr/send-authentication-basic-cors-not-enabled.htm
new file mode 100644
index 0000000..4120196
--- /dev/null
+++ b/test/wpt/tests/xhr/send-authentication-basic-cors-not-enabled.htm
@@ -0,0 +1,29 @@
+<!doctype html>
+<html>
+ <head>
+ <title>XMLHttpRequest: send() - "Basic" authenticated CORS requests with user name and password passed to open() (asserts failure)</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/common/utils.js"></script>
+ <script src="/common/get-host-info.sub.js"></script>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-open()-method" data-tested-assertations="following::ol[1]/li[9]/ol[1]/li[1] following::ol[1]/li[9]/ol[1]/li[2]" />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-send()-method" data-tested-assertations="following::code[contains(@title,'http-authorization')]/.." />
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ test(function() {
+ var client = new XMLHttpRequest(),
+ urlstart = get_host_info().REMOTE_ORIGIN + location.pathname.replace(/\/[^\/]*$/, '/')
+ client.withCredentials = true
+ user = token()
+ client.open("GET", urlstart + "resources/auth10/auth.py", false, user, 'pass')
+ client.setRequestHeader("x-user", user)
+ assert_throws_dom("NetworkError", function(){ client.send(null) })
+ assert_equals(client.responseText, '')
+ assert_equals(client.status, 0)
+ assert_equals(client.getResponseHeader('x-challenge'), null)
+ }, document.title)
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/send-authentication-basic-cors.htm b/test/wpt/tests/xhr/send-authentication-basic-cors.htm
new file mode 100644
index 0000000..fcacdd5
--- /dev/null
+++ b/test/wpt/tests/xhr/send-authentication-basic-cors.htm
@@ -0,0 +1,35 @@
+<!doctype html>
+<html>
+ <head>
+ <title>XMLHttpRequest: send() - "Basic" authenticated CORS requests with user name and password passed to open() (asserts failure)</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-open()-method" data-tested-assertations="following::ol[1]/li[9]/ol[1]/li[1] following::ol[1]/li[9]/ol[1]/li[2]" />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-send()-method" data-tested-assertations="following::code[contains(@title,'http-authorization')]/.." />
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ var test = async_test()
+ test.step(function() {
+ var client = new XMLHttpRequest(),
+ urlstart = 'www1.'+location.host + location.pathname.replace(/\/[^\/]*$/, '/')
+ client.withCredentials = true
+ client.open("GET", location.protocol+'//'+urlstart + "resources/auth1/corsenabled.py", true, 'user', 'pass')
+ client.setRequestHeader("x-user", 'user')
+ client.setRequestHeader("x-pass", 'pass')
+ client.onreadystatechange = function(){
+ if (client.readyState === 4) {
+ test.step(function(){
+ assert_equals(client.responseText, '')
+ assert_equals(client.status, 0)
+ assert_equals(client.getResponseHeader('x-challenge'), null)
+ test.done()
+ })
+ }
+ }
+ client.send(null)
+ }, document.title)
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/send-authentication-basic-repeat-no-args.htm b/test/wpt/tests/xhr/send-authentication-basic-repeat-no-args.htm
new file mode 100644
index 0000000..38f1b20
--- /dev/null
+++ b/test/wpt/tests/xhr/send-authentication-basic-repeat-no-args.htm
@@ -0,0 +1,33 @@
+<!doctype html>
+<html>
+ <head>
+ <title>XMLHttpRequest: send() - "Basic" authenticated requests with user name and password passed to open() in first request, without in second</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/common/utils.js"></script>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-open()-method" data-tested-assertations="following::ol[1]/li[9]/ol[1]/li[1] following::ol[1]/li[9]/ol[1]/li[2]" />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-send()-method" data-tested-assertations="following::code[contains(@title,'http-authorization')]/.." />
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ test(function() {
+ var client = new XMLHttpRequest(),
+ urlstart = location.host + location.pathname.replace(/\/[^\/]*$/, '/'),
+ user = token()
+ client.open("GET", location.protocol+'//'+urlstart + "resources/auth11/auth.py", false, user, 'pass')
+ client.setRequestHeader("x-user", user)
+ client.send(null)
+ // Repeat request but *without* credentials in the open() call.
+ // Is the UA supposed to cache credentials from above request and use them? Yes.
+ client.open("GET", location.protocol+'//'+urlstart + "resources/auth11/auth.py", false)
+ client.setRequestHeader("x-user", user)
+ client.send(null)
+
+ assert_equals(client.responseText, user + "\n" + 'pass')
+ //assert_equals(client.getResponseHeader('x-challenge'), 'DID-NOT')
+
+ }, document.title)
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/send-authentication-basic-setrequestheader-and-arguments.htm b/test/wpt/tests/xhr/send-authentication-basic-setrequestheader-and-arguments.htm
new file mode 100644
index 0000000..9915e88
--- /dev/null
+++ b/test/wpt/tests/xhr/send-authentication-basic-setrequestheader-and-arguments.htm
@@ -0,0 +1,36 @@
+<!doctype html>
+<html>
+ <head>
+ <title>XMLHttpRequest: send() - "Basic" authenticated request using setRequestHeader() and open() arguments (asserts header wins)</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/common/utils.js"></script>
+ <!-- These spec references do not make much sense simply because the spec doesn't say very much about this.. -->
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-setrequestheader()-method" data-tested-assertations="following::ol[1]/li[6]" />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-send()-method" data-tested-assertations="following::code[contains(@title,'http-authorization')]/.." />
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ var test = async_test()
+ test.step(function() {
+ var client = new XMLHttpRequest(),
+ urlstart = location.host + location.pathname.replace(/\/[^\/]*$/, '/'),
+ user = token()
+ client.open("GET", location.protocol+'//'+urlstart + "resources/auth9/auth.py", false, 'open-' + user, 'open-pass')
+ client.setRequestHeader("x-user", user)
+ client.setRequestHeader('Authorization', 'Basic ' + btoa(user + ":pass"))
+ client.onreadystatechange = function () {
+ if (client.readyState < 4) {return}
+ test.step( function () {
+ assert_equals(client.responseText, user + '\npass')
+ assert_equals(client.status, 200)
+ assert_equals(client.getResponseHeader('x-challenge'), 'DID-NOT')
+ test.done()
+ })
+ }
+ client.send(null)
+ })
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/send-authentication-basic-setrequestheader-existing-session.htm b/test/wpt/tests/xhr/send-authentication-basic-setrequestheader-existing-session.htm
new file mode 100644
index 0000000..6e68f09
--- /dev/null
+++ b/test/wpt/tests/xhr/send-authentication-basic-setrequestheader-existing-session.htm
@@ -0,0 +1,53 @@
+<!doctype html>
+<html>
+ <head>
+ <title>XMLHttpRequest: send() - "Basic" authenticated request using setRequestHeader() when there is an existing session</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/common/utils.js"></script>
+ <!-- These spec references do not make much sense simply because the spec doesn't say very much about this.. -->
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-setrequestheader()-method" data-tested-assertations="following::ol[1]/li[6]" />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-send()-method" data-tested-assertations="following::code[contains(@title,'http-authorization')]/.." />
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ var test = async_test()
+ test.step(function() {
+ var client = new XMLHttpRequest(),
+ urlstart = location.host + location.pathname.replace(/\/[^\/]*$/, '/')
+ // Initial request: no information is known to the UA about whether resources/auth4/auth.py requires authentication,
+ // hence it first sends a normal request, gets a 401 response that will not be passed on to the JS, and sends a new
+ // request with an Authorization header before returning
+ // (Note: this test will only work as expected if run once per browsing session)
+ var open_user = token()
+ client.open("GET", location.protocol+'//'+urlstart + "resources/auth4/auth.py", false, open_user, 'open-pass')
+ client.setRequestHeader('X-User', open_user)
+ // initial request - this will get a 401 response and re-try with HTTP auth
+ client.send(null)
+ assert_true(client.responseText == (open_user + '\nopen-pass'), 'responseText should contain the right user and password')
+ assert_equals(client.status, 200)
+ assert_equals(client.getResponseHeader('x-challenge'), 'DID')
+ // Another request, this time user,pass is omitted and an Authorization header set explicitly
+ // Here the URL is known to require authentication (from the request above), and the UA has cached open-user:open-pass credentials
+ // However, these session credentials should now be overridden by the setRequestHeader() call so the UA should immediately
+ // send basic Authorization header with credentials user:pass. (This part is perhaps not well specified anywhere)
+ var user = token();
+ client.open("GET", location.protocol+'//'+urlstart + "resources/auth4/auth.py", true)
+ client.setRequestHeader("x-user", user)
+ client.setRequestHeader('Authorization', 'Basic ' + btoa(user + ":pass"))
+ client.onreadystatechange = function () {
+ if (client.readyState < 4) {return}
+ test.step( function () {
+ assert_equals(client.responseText, user + '\npass')
+ assert_equals(client.status, 200)
+ assert_equals(client.getResponseHeader('x-challenge'), 'DID-NOT')
+ test.done()
+ } )
+ }
+ client.send(null)
+ })
+ </script>
+ <p>Note: this test will only work as expected once per browsing session. Restart browser to re-test.</p>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/send-authentication-basic-setrequestheader.htm b/test/wpt/tests/xhr/send-authentication-basic-setrequestheader.htm
new file mode 100644
index 0000000..84d0dd6
--- /dev/null
+++ b/test/wpt/tests/xhr/send-authentication-basic-setrequestheader.htm
@@ -0,0 +1,36 @@
+<!doctype html>
+<html>
+ <head>
+ <title>XMLHttpRequest: send() - "Basic" authenticated request using setRequestHeader()</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/common/utils.js"></script>
+ <!-- These spec references do not make much sense simply because the spec doesn't say very much about this.. -->
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-setrequestheader()-method" data-tested-assertations="following::ol[1]/li[6]" />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-send()-method" data-tested-assertations="following::code[contains(@title,'http-authorization')]/.." />
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ var test = async_test()
+ test.step(function() {
+ var client = new XMLHttpRequest(),
+ urlstart = location.host + location.pathname.replace(/\/[^\/]*$/, '/'),
+ user = token()
+ client.open("GET", location.protocol+'//'+urlstart + "resources/auth2/auth.py", false)
+ client.setRequestHeader("x-user", user)
+ client.setRequestHeader('Authorization', 'Basic ' + btoa(user + ":pass"))
+ client.onreadystatechange = function () {
+ if (client.readyState < 4) {return}
+ test.step( function () {
+ assert_equals(client.responseText, user + '\npass')
+ assert_equals(client.status, 200)
+ assert_equals(client.getResponseHeader('x-challenge'), 'DID-NOT')
+ test.done()
+ } )
+ }
+ client.send(null)
+ })
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/send-authentication-basic.htm b/test/wpt/tests/xhr/send-authentication-basic.htm
new file mode 100644
index 0000000..ae3ee57
--- /dev/null
+++ b/test/wpt/tests/xhr/send-authentication-basic.htm
@@ -0,0 +1,27 @@
+<!doctype html>
+<html>
+ <head>
+ <title>XMLHttpRequest: send() - "Basic" authenticated requests with user name and password passed to open()</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/common/utils.js"></script>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-open()-method" data-tested-assertations="following::ol[1]/li[9]/ol[1]/li[1] following::ol[1]/li[9]/ol[1]/li[2]" />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-send()-method" data-tested-assertations="following::code[contains(@title,'http-authorization')]/.." />
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ test(function() {
+ var client = new XMLHttpRequest(),
+ urlstart = location.host + location.pathname.replace(/\/[^\/]*$/, '/'),
+ user = token();
+ client.open("GET", location.protocol+'//'+urlstart + "resources/auth1/auth.py", false, user, 'pass')
+ client.setRequestHeader("x-user", user)
+ client.send(null)
+ assert_equals(client.responseText, user + "\n" + 'pass')
+ assert_equals(client.getResponseHeader('x-challenge'), 'DID')
+ }, document.title)
+ </script>
+ <p>Note: this test will only work as expected once per browsing session. Restart browser to re-test.</p>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/send-authentication-competing-names-passwords.htm b/test/wpt/tests/xhr/send-authentication-competing-names-passwords.htm
new file mode 100644
index 0000000..bc6755c
--- /dev/null
+++ b/test/wpt/tests/xhr/send-authentication-competing-names-passwords.htm
@@ -0,0 +1,50 @@
+<!doctype html>
+<html>
+ <head>
+ <title>XMLHttpRequest: send() - "Basic" authenticated requests with competing user name/password options</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/common/utils.js"></script>
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ function request(user1, pass1, user2, pass2, name) {
+ test(function() {
+ const client = new XMLHttpRequest(),
+ userwin = user2 || user1,
+ passwin = pass2 || pass1;
+ let urlstart = "";
+ if (user1 || pass1) {
+ urlstart = "http://";
+ if (user1) {
+ urlstart += user1;
+ }
+ if (pass1) {
+ urlstart += ":" + pass1;
+ }
+ urlstart += "@" + location.host + location.pathname.replace(/\/[^\/]*$/, '/');
+ }
+ client.open("GET", urlstart + "resources/authentication.py", false, user2, pass2);
+ client.setRequestHeader("x-user", userwin);
+ client.send(null);
+ assert_equals(client.responseText, ((userwin||'') + "\n" + (passwin||'')), 'responseText should contain the right user and password');
+ }, "XMLHttpRequest user/pass options: " + name);
+ }
+ // Cannot have just a password
+ request(null, null, token(), null, "user in open()");
+ request(null, null, token(), token(), "user/pass in open()");
+ request(null, null, token(), token(), "another user/pass in open(); must override cached credentials from previous test");
+ request(null, token(), token(), null, "pass in URL, user in open()");
+ request(null, token(), token(), token(), "pass in URL, user/pass in open()");
+ request(token(), null, null, null, "user in URL");
+ request(token(), null, null, token(), "user in URL, pass in open()");
+ request(token(), token(), null, null, "user/pass in URL");
+ request(token(), null, token(), null, "user in URL and open()");
+ request(token(), null, token(), token(), "user in URL; user/pass in open()");
+ request(token(), token(), token(), null, "user/pass in URL; user in open()");
+ request(token(), token(), null, token(), "user/pass in URL; pass in open()");
+ request(token(), token(), token(), token(), "user/pass in URL and open()");
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/send-authentication-cors-basic-setrequestheader.htm b/test/wpt/tests/xhr/send-authentication-cors-basic-setrequestheader.htm
new file mode 100644
index 0000000..3c488dc
--- /dev/null
+++ b/test/wpt/tests/xhr/send-authentication-cors-basic-setrequestheader.htm
@@ -0,0 +1,31 @@
+<!doctype html>
+<html>
+ <head>
+ <title>XMLHttpRequest: send() - "Basic" authenticated CORS request using setRequestHeader() (expects to succeed)</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/common/utils.js"></script>
+ <script src="/common/get-host-info.sub.js"></script>
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ async_test(test => {
+ var client = new XMLHttpRequest(),
+ urlstart = get_host_info().REMOTE_ORIGIN + location.pathname.replace(/\/[^\/]*$/, '/'),
+ user = token()
+ client.open("GET", urlstart + "resources/auth2/corsenabled.py", false)
+ client.withCredentials = true
+ client.setRequestHeader("x-user", user)
+ client.setRequestHeader("x-pass", 'pass')
+ client.setRequestHeader('Authorization', 'Basic ' + btoa(user + ":pass"))
+ client.onload = test.step_func_done(() => {
+ assert_equals(client.responseText, user + '\npass', 'responseText should contain the right user and password')
+ assert_equals(client.status, 200)
+ assert_equals(client.getResponseHeader('x-challenge'), 'DID-NOT')
+ })
+ client.send(null)
+ })
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/send-authentication-cors-setrequestheader-no-cred.htm b/test/wpt/tests/xhr/send-authentication-cors-setrequestheader-no-cred.htm
new file mode 100644
index 0000000..eed619c
--- /dev/null
+++ b/test/wpt/tests/xhr/send-authentication-cors-setrequestheader-no-cred.htm
@@ -0,0 +1,62 @@
+<!doctype html>
+<html>
+ <head>
+ <title>XMLHttpRequest: send() - "Basic" authenticated CORS request using setRequestHeader() but not setting withCredentials (expects to succeed)</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/common/utils.js"></script>
+ <script src="/common/get-host-info.sub.js"></script>
+ <!-- These spec references do not make much sense simply because the spec doesn't say very much about this.. -->
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-setrequestheader()-method" data-tested-assertations="following::ol[1]/li[6]" />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-send()-method" data-tested-assertations="following::code[contains(@title,'http-authorization')]/.." />
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ function doTest(desc, pathsuffix, conditionsFunc, errorFunc, endFunc) {
+ var test = async_test(desc)
+ test.step(function() {
+ var client = new XMLHttpRequest(),
+ urlstart = get_host_info().REMOTE_ORIGIN + location.pathname.replace(/\/[^\/]*$/, '/'),
+ user = token()
+ client.open("GET", urlstart + "resources/" + pathsuffix, false)
+ client.setRequestHeader("x-user", user)
+ client.setRequestHeader("x-pass", 'pass')
+ client.setRequestHeader("Authorization", "Basic " + btoa(user + ":pass"))
+ client.onerror = test.step_func(errorFunc)
+ client.onreadystatechange = test.step_func(function () {
+ if(client.readyState < 4) {return}
+ conditionsFunc(client, test, user)
+ })
+ if(endFunc) {
+ client.onloadend = test.step_func(endFunc)
+ }
+ client.send(null)
+ })
+ }
+
+ doTest("CORS request with setRequestHeader auth to URL accepting Authorization header", "auth7/corsenabled.py", function (client, test, user) {
+ assert_true(client.responseText == (user + "\npass"), "responseText should contain the right user and password")
+ assert_equals(client.status, 200)
+ assert_equals(client.getResponseHeader("x-challenge"), "DID-NOT")
+ test.done()
+ }, function(){
+ assert_unreached("Cross-domain request is permitted and should not cause an error")
+ this.done()
+ })
+
+ var errorFired = false;
+ doTest("CORS request with setRequestHeader auth to URL NOT accepting Authorization header", "auth8/corsenabled-no-authorize.py", function (client, test, user) {
+ assert_equals(client.responseText, '')
+ assert_equals(client.status, 0)
+ }, function(e){
+ errorFired = true
+ assert_equals(e.type, 'error', 'Error event fires when Authorize is a user-set header but not allowed by the CORS endpoint')
+ }, function() {
+ assert_true(errorFired, 'The error event should fire')
+ this.done()
+ })
+
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/send-authentication-existing-session-manual.htm b/test/wpt/tests/xhr/send-authentication-existing-session-manual.htm
new file mode 100644
index 0000000..a80efd6
--- /dev/null
+++ b/test/wpt/tests/xhr/send-authentication-existing-session-manual.htm
@@ -0,0 +1,33 @@
+<!doctype html>
+<html>
+ <head>
+ <title>XMLHttpRequest: send() - "Basic" authenticated requests with user name and password from interactive session</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/common/utils.js"></script>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-send()-method" data-tested-assertations="following::code[contains(@title,'http-authorization')]/.." />
+ </head>
+ <body>
+ <p>Please follow these steps to complete the test:</p>
+ <script>var user = token();</script>
+ <ol>
+ <li>Load <a href="resources/auth3/auth.py">page</a> and authenticate with username "<script>document.write(user)</script>" and password "pass"</li>
+ <li>Go back</li>
+ <li>Click <button onclick="location.href = location.href + '?dotest&user=' + user">complete test</button></li>
+ </ol>
+ <div id="log"></div>
+ <script>
+ if (location.search.indexOf('?dotest') != -1) {
+ test(function() {
+ var user = location.search.slice(location.search.indexOf('&user=') + 6),
+ client = new XMLHttpRequest(),
+ urlstart = location.host + location.pathname.replace(/\/[^\/]*$/, '/')
+ client.open("GET", location.protocol+'//'+urlstart + "resources/auth3/auth.py", false)
+ client.send(null)
+ assert_equals(client.responseText, user + "\n" + 'pass')
+ assert_equals(client.getResponseHeader('x-challenge'), 'DID-NOT')
+ }, document.title)
+ }
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/send-authentication-prompt-2-manual.htm b/test/wpt/tests/xhr/send-authentication-prompt-2-manual.htm
new file mode 100644
index 0000000..023a40a
--- /dev/null
+++ b/test/wpt/tests/xhr/send-authentication-prompt-2-manual.htm
@@ -0,0 +1,25 @@
+<!doctype html>
+<html>
+ <head>
+ <title>XMLHttpRequest: WWW-Authenticate challenge when user,pass are not passed to open()</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-send()-method" data-tested-assertations="following::code[contains(@title,'http-authorization')]/.." />
+ </head>
+ <body>
+ <p>Please follow these steps to complete the test:</p>
+ <ol>
+ <li>If you are prompted for user name and password, type in 'usr' and 'secret'</li>
+ </ol>
+ <div id="log"></div>
+ <script>
+ test(function() {
+ var client = new XMLHttpRequest(),
+ urlstart = location.host + location.pathname.replace(/\/[^\/]*$/, '/')
+ client.open("GET", location.protocol+'//'+urlstart + "resources/auth6/auth.py", false)
+ client.send(null)
+ assert_equals(client.responseText, 'usr' + "\n" + 'secret')
+ }, document.title)
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/send-authentication-prompt-manual.htm b/test/wpt/tests/xhr/send-authentication-prompt-manual.htm
new file mode 100644
index 0000000..a836c59
--- /dev/null
+++ b/test/wpt/tests/xhr/send-authentication-prompt-manual.htm
@@ -0,0 +1,25 @@
+<!doctype html>
+<html>
+ <head>
+ <title>XMLHttpRequest: send() - "Basic" authentication gets 401 response</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-send()-method" data-tested-assertations="following::code[contains(@title,'http-authorization')]/.." />
+ </head>
+ <body>
+ <p>Please follow these steps to complete the test:</p>
+ <ol>
+ <li>If you are prompted for user name and password, type in 'usr' and 'secret'</li>
+ </ol>
+ <div id="log"></div>
+ <script>
+ test(function() {
+ var client = new XMLHttpRequest(),
+ urlstart = location.host + location.pathname.replace(/\/[^\/]*$/, '/')
+ client.open("GET", location.protocol+'//'+urlstart + "resources/auth5/auth.py", false, 'usr', 'wrongpassword')
+ client.send(null)
+ assert_equals(client.responseText, 'usr' + "\n" + 'secret')
+ }, document.title)
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/send-blob-with-no-mime-type.html b/test/wpt/tests/xhr/send-blob-with-no-mime-type.html
new file mode 100644
index 0000000..e7ab989
--- /dev/null
+++ b/test/wpt/tests/xhr/send-blob-with-no-mime-type.html
@@ -0,0 +1,61 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-send()-method" data-tested-assertations="following::ol[1]/li[4] following::ol[1]/li[4]/dl[1]/dd[2]/p[3]"/>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-status-attribute" data-tested-assertations="following::ol[1]/li[3]"/>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-responsetype-attribute" data-tested-assertations="following::ol[1]/li[4]"/>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-response-attribute" data-tested-assertations="following::a[contains(@href,'#blob-response-entity-body')]/.."/>
+
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <title>XMLHttpRequest: The send() method: Blob data with no mime type</title>
+</head>
+
+<body>
+ <div id="log"></div>
+
+ <script type="text/javascript">
+ var blobTests = [
+ ["no mime type", new Blob(["data"])],
+ ["invalid mime type", new Blob(["data"], {type: "Invalid \r\n mime \r\n type"})]
+ ];
+
+ function doSyncTest(testItem, method) {
+ test(function() {
+ var xhr = new XMLHttpRequest();
+ xhr.open(method, "./resources/content.py", false);
+ xhr.send(testItem[1]);
+
+ assert_equals(xhr.getResponseHeader("X-Request-Content-Length"), "4");
+ assert_equals(xhr.getResponseHeader("X-Request-Content-Type"), "NO");
+ }, "Synchronous blob loading with " + testItem[0] + " [" + method + "]");
+ }
+
+ function doAsyncTest(testItem, method) {
+ var atest = async_test("Asynchronous blob loading with " + testItem[0] + " [" + method + "]");
+ atest.step(function() {
+ var xhr = new XMLHttpRequest();
+ xhr.onreadystatechange = function() {
+ if (xhr.readyState == 4) {
+ atest.step(function() {
+ assert_equals(xhr.getResponseHeader("X-Request-Content-Length"), "4");
+ assert_equals(xhr.getResponseHeader("X-Request-Content-Type"), "NO");
+ });
+ atest.done();
+ }
+ }
+ xhr.open(method, "./resources/content.py", true);
+ xhr.send(testItem[1]);
+ });
+ }
+
+ blobTests.forEach(function(item){
+ doSyncTest(item, "POST");
+ doAsyncTest(item, "POST");
+
+ doSyncTest(item, "PUT");
+ doAsyncTest(item, "PUT");
+ });
+ </script>
+</body>
+</html>
diff --git a/test/wpt/tests/xhr/send-conditional-cors.htm b/test/wpt/tests/xhr/send-conditional-cors.htm
new file mode 100644
index 0000000..f46f18a
--- /dev/null
+++ b/test/wpt/tests/xhr/send-conditional-cors.htm
@@ -0,0 +1,42 @@
+<!doctype html>
+<title>XMLHttpRequest: send() - conditional cross-origin requests</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=/cors/support.js?pipe=sub></script>
+<div id=log></div>
+<script>
+function request(withCORS, desc) {
+ async_test(t => {
+ const client = new XMLHttpRequest,
+ identifier = Math.random(),
+ cors = withCORS ? "&cors=yes" : "",
+ url = CROSSDOMAIN + "resources/conditional.py?tag=" + identifier + cors
+ client.onload = t.step_func(() => {
+ assert_equals(client.status, 200)
+ assert_equals(client.statusText, "OK")
+ assert_equals(client.responseText, "MAYBE NOT")
+
+ if(withCORS) {
+ client.onload = t.step_func_done(() => {
+ assert_equals(client.status, 304)
+ assert_equals(client.statusText, "SUPERCOOL")
+ assert_equals(client.responseText, "")
+ })
+ } else {
+ client.onload = null
+ client.onerror = t.step_func_done(() => {
+ assert_equals(client.status, 0)
+ assert_equals(client.statusText, "")
+ })
+ }
+ client.open("GET", url)
+ client.setRequestHeader("If-None-Match", identifier)
+ client.send()
+ })
+ client.open("GET", url)
+ client.send()
+ }, desc)
+}
+request(false, "304 without appropriate CORS header")
+request(true, "304 with appropriate CORS header")
+</script>
diff --git a/test/wpt/tests/xhr/send-conditional.htm b/test/wpt/tests/xhr/send-conditional.htm
new file mode 100644
index 0000000..cbe3e94
--- /dev/null
+++ b/test/wpt/tests/xhr/send-conditional.htm
@@ -0,0 +1,34 @@
+<!doctype html>
+<html>
+ <head>
+ <title>XMLHttpRequest: send() - conditional requests</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-send()-method" data-tested-assertations="following::code[contains(text(),'Modified')]/.." />
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ function request(type) {
+ test(function() {
+ var client = new XMLHttpRequest,
+ identifier = type == "tag" ? Math.random() : new Date().toGMTString(),
+ url = "resources/conditional.py?" + type + "=" + identifier
+ client.open("GET", url, false)
+ client.send(null)
+ assert_equals(client.status, 200)
+ assert_equals(client.statusText, "OK")
+ assert_equals(client.responseText, "MAYBE NOT")
+ client.open("GET", url, false)
+ client.setRequestHeader(type == "tag" ? "If-None-Match" : "If-Modified-Since", identifier)
+ client.send(null)
+ assert_equals(client.status, 304)
+ assert_equals(client.statusText, "SUPERCOOL")
+ assert_equals(client.responseText, "")
+ }, document.title + " (" + type + ")")
+ }
+ request("tag")
+ request("date")
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/send-content-type-charset.htm b/test/wpt/tests/xhr/send-content-type-charset.htm
new file mode 100644
index 0000000..a968bb3
--- /dev/null
+++ b/test/wpt/tests/xhr/send-content-type-charset.htm
@@ -0,0 +1,115 @@
+<!doctype html>
+<html>
+ <head>
+ <title>XMLHttpRequest: send() - charset parameter of Content-Type</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ function request(input, output, title) {
+ title = title || document.title + ' - ' + input;
+ test(function() {
+ var client = new XMLHttpRequest()
+ client.open("POST", "resources/content.py", false)
+ if(input !== null)
+ client.setRequestHeader("Content-Type", input)
+ client.send("TEST")
+ assert_equals(client.responseText, "TEST")
+ assert_equals(client.getResponseHeader("x-request-content-type"), output)
+ }, title)
+ }
+
+ request(
+ "text; charset=ascii",
+ "text; charset=ascii",
+ "header with invalid MIME type is not changed"
+ )
+ request(
+ "",
+ "",
+ "header with invalid MIME type (empty string) is not changed"
+ )
+ request(
+ "charset=ascii",
+ "charset=ascii",
+ "known charset but bogus header - missing MIME type"
+ )
+ request(
+ "charset=bogus",
+ "charset=bogus",
+ "bogus charset and bogus header - missing MIME type"
+ )
+ request(
+ "text/plain;charset=utf-8",
+ "text/plain;charset=utf-8",
+ "If charset= param is UTF-8 (case-insensitive), it should not be changed"
+ )
+ request(
+ "text/x-pink-unicorn",
+ "text/x-pink-unicorn",
+ "If no charset= param is given, implementation should not add one - unknown MIME"
+ )
+ request(
+ "text/plain",
+ "text/plain",
+ "If no charset= param is given, implementation should not add one - known MIME"
+ )
+ request(
+ "text/plain; hi=bye",
+ "text/plain; hi=bye",
+ "If no charset= param is given, implementation should not add one - known MIME, unknown param, two spaces"
+ )
+ request(
+ "text/x-thepiano;charset= waddup",
+ "text/x-thepiano;charset=UTF-8",
+ "charset given but wrong, fix it (unknown MIME, bogus charset)"
+ )
+ request(
+ "text/plain;charset=utf-8;charset=waddup",
+ "text/plain;charset=utf-8;charset=waddup",
+ "If charset= param is UTF-8 (case-insensitive), it should not be changed (bogus charset)"
+ )
+ request(
+ "text/plain;charset=shift-jis",
+ "text/plain;charset=UTF-8",
+ "charset given but wrong, fix it (known MIME, actual charset)"
+ )
+ request(
+ "text/x-pink-unicorn; charset=windows-1252; charset=bogus; notrelated; charset=ascii",
+ "text/x-pink-unicorn;charset=UTF-8",
+ "Multiple non-UTF-8 charset parameters deduplicate, bogus parameter dropped"
+ )
+ request(
+ null,
+ "text/plain;charset=UTF-8",
+ "No content type set, give MIME and charset"
+ )
+ request(
+ "text/plain;charset= utf-8",
+ "text/plain;charset=UTF-8",
+ "charset with leading space that is UTF-8 does change")
+ request(
+ "text/plain;charset=utf-8 ;x=x",
+ "text/plain;charset=utf-8 ;x=x",
+ "charset with trailing space that is UTF-8 does not change");
+ request(
+ "text/plain;charset=\"utf-8\"",
+ "text/plain;charset=\"utf-8\"",
+ "charset in double quotes that is UTF-8 does not change")
+ request(
+ "text/plain;charset=\" utf-8\"",
+ "text/plain;charset=UTF-8",
+ "charset in double quotes with space")
+ request(
+ "text/plain;charset=\"u\\t\\f-8\"",
+ "text/plain;charset=\"u\\t\\f-8\"",
+ "charset in double quotes with backslashes that is UTF-8 does not change")
+ request(
+ "YO/yo;charset=x;yo=YO; X=y",
+ "yo/yo;charset=UTF-8;yo=YO;x=y",
+ "unknown parameters need to be preserved")
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/send-content-type-string.htm b/test/wpt/tests/xhr/send-content-type-string.htm
new file mode 100644
index 0000000..6c2c008
--- /dev/null
+++ b/test/wpt/tests/xhr/send-content-type-string.htm
@@ -0,0 +1,26 @@
+<!doctype html>
+<html>
+ <head>
+ <title>XMLHttpRequest: send() - Content-Type</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#dom-XMLHttpRequest-send-document" data-tested-assertations="following::p[1] following::p[2] following::p[3]" />
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ function request(data, expected_type) {
+ test(function() {
+ var client = new XMLHttpRequest()
+ client.open("POST", "resources/content.py", false)
+ client.send(data)
+ assert_equals(client.getResponseHeader("x-request-content-type"), expected_type)
+ })
+ }
+ request("TEST", "text/plain;charset=UTF-8")
+ function init(fr) { request(fr.contentDocument, fr.getAttribute("data-t")) }
+ </script>
+ <iframe src="resources/win-1252-xml.py" onload="init(this)" data-t="application/xml;charset=UTF-8"></iframe>
+ <iframe src="resources/win-1252-html.py" onload="init(this)" data-t="text/html;charset=UTF-8"></iframe>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/send-data-arraybuffer.any.js b/test/wpt/tests/xhr/send-data-arraybuffer.any.js
new file mode 100644
index 0000000..71933d0
--- /dev/null
+++ b/test/wpt/tests/xhr/send-data-arraybuffer.any.js
@@ -0,0 +1,31 @@
+// META: title=XMLHttpRequest.send(arraybuffer)
+
+var test = async_test();
+test.step(function()
+{
+ var xhr = new XMLHttpRequest();
+ var buf = new ArrayBuffer(5);
+ var arr = new Uint8Array(buf);
+ arr[0] = 0x48;
+ arr[1] = 0x65;
+ arr[2] = 0x6c;
+ arr[3] = 0x6c;
+ arr[4] = 0x6f;
+
+ xhr.onreadystatechange = function()
+ {
+ if (xhr.readyState == 4)
+ {
+ test.step(function()
+ {
+ assert_equals(xhr.status, 200);
+ assert_equals(xhr.response, "Hello");
+
+ test.done();
+ });
+ }
+ };
+
+ xhr.open("POST", "./resources/content.py", true);
+ xhr.send(buf);
+});
diff --git a/test/wpt/tests/xhr/send-data-arraybufferview.any.js b/test/wpt/tests/xhr/send-data-arraybufferview.any.js
new file mode 100644
index 0000000..a3985b4
--- /dev/null
+++ b/test/wpt/tests/xhr/send-data-arraybufferview.any.js
@@ -0,0 +1,18 @@
+// META: title=XMLHttpRequest.send(arraybufferview)
+
+var test = async_test();
+test.step(function()
+{
+ var str = "Hello";
+ var bytes = str.split("").map(function(ch) { return ch.charCodeAt(0); });
+ var xhr = new XMLHttpRequest();
+ var arr = new Uint8Array(bytes);
+
+ xhr.onload = test.step_func_done(function() {
+ assert_equals(xhr.status, 200);
+ assert_equals(xhr.response, str);
+ });
+
+ xhr.open("POST", "./resources/content.py", true);
+ xhr.send(arr);
+});
diff --git a/test/wpt/tests/xhr/send-data-blob.htm b/test/wpt/tests/xhr/send-data-blob.htm
new file mode 100644
index 0000000..5285fc1
--- /dev/null
+++ b/test/wpt/tests/xhr/send-data-blob.htm
@@ -0,0 +1,62 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-send()-method" data-tested-assertations="following::ol[1]/li[4] following::ol[1]/li[4]/dl[1]/dd[2]/p[3]"/>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-status-attribute" data-tested-assertations="following::ol[1]/li[3]"/>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-responsetype-attribute" data-tested-assertations="following::ol[1]/li[4]"/>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-response-attribute" data-tested-assertations="following::a[contains(@href,'#blob-response-entity-body')]/.."/>
+
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <title>XMLHttpRequest: The send() method: Blob data</title>
+</head>
+
+<body>
+ <div id="log"></div>
+
+ <script type="text/javascript">
+ var test = async_test();
+
+ test.step(function()
+ {
+ var xhr = new XMLHttpRequest();
+ var xhr2 = new XMLHttpRequest();
+
+ var content = "Hello";
+ var blob;
+
+ xhr.onreadystatechange = function()
+ {
+ if (xhr.readyState == 4)
+ {
+ test.step(function()
+ {
+ blob = xhr.response;
+ assert_true(blob instanceof Blob, "Blob from XHR Response");
+
+ xhr2.open("POST", "./resources/content.py", true);
+ xhr2.send(blob);
+ });
+ }
+ }
+
+ xhr2.onreadystatechange = function()
+ {
+ if (xhr2.readyState == 4)
+ {
+ test.step(function()
+ {
+ assert_equals(xhr2.status, 200);
+ assert_equals(xhr2.response, content);
+ test.done();
+ });
+ }
+ };
+
+ xhr.open("GET", "./resources/content.py?content=" + content, true);
+ xhr.responseType = "blob";
+ xhr.send();
+ });
+ </script>
+</body>
+</html>
diff --git a/test/wpt/tests/xhr/send-data-es-object.any.js b/test/wpt/tests/xhr/send-data-es-object.any.js
new file mode 100644
index 0000000..92286bc
--- /dev/null
+++ b/test/wpt/tests/xhr/send-data-es-object.any.js
@@ -0,0 +1,58 @@
+// META: title=XMLHttpRequest.send(ES object)
+
+function do_test(obj, expected, name) {
+ var test = async_test(name)
+ test.step(function() {
+ var client = new XMLHttpRequest()
+ client.onload = test.step_func(function () {
+ assert_equals(client.responseText, expected)
+ test.done()
+ });
+ client.open('POST', 'resources/content.py')
+ if (expected.exception) {
+ if (expected.exception.identity) {
+ assert_throws_exactly(expected.exception.identity,
+ function(){client.send(obj)})
+ } else {
+ assert_throws_js(expected.exception.ctor,
+ function(){client.send(obj)})
+ }
+ test.done()
+ } else {
+ client.send(obj)
+ }
+ });
+}
+
+do_test({}, '[object Object]', 'sending a plain empty object')
+do_test(Math, '[object Math]', 'sending the ES Math object')
+do_test(new XMLHttpRequest, '[object XMLHttpRequest]', 'sending a new XHR instance')
+do_test(new ReadableStream, '[object ReadableStream]', 'sending a new ReadableStream instance')
+do_test({toString:function(){}}, 'undefined', 'sending object that stringifies to undefined')
+do_test({toString:function(){return null}}, 'null', 'sending object that stringifies to null')
+var ancestor = {toString: function(){
+ var ar=[]
+ for (var prop in this) {
+ if (this.hasOwnProperty(prop)) {
+ ar.push(prop+'='+this[prop])
+ }
+ };
+ return ar.join('&')
+}};
+
+var myObj = Object.create(ancestor, {foo:{value:1, enumerable: true}, bar:{value:'foo', enumerable:true}})
+do_test(myObj, 'foo=1&bar=foo', 'object that stringifies to query string')
+
+var myFakeJSON = {a:'a', b:'b', toString:function(){ return JSON.stringify(this, function(key, val){ return key ==='toString'?undefined:val; }) }}
+do_test(myFakeJSON, '{"a":"a","b":"b"}', 'object that stringifies to JSON string')
+
+var myFakeDoc1 = {valueOf:function(){return document}}
+do_test(myFakeDoc1, '[object Object]', 'object whose valueOf() returns a document - ignore valueOf(), stringify')
+
+var myFakeDoc2 = {toString:function(){return document}}
+var expectedError = self.GLOBAL.isWorker() ? ReferenceError : TypeError;
+do_test(myFakeDoc2, {exception: { ctor: expectedError } }, 'object whose toString() returns a document, expected to throw')
+
+var err = {name:'FooError', message:'bar'};
+var myThrower = {toString:function(){throw err;}};
+do_test(myThrower, {exception: { identity: err }}, 'object whose toString() throws, expected to throw')
diff --git a/test/wpt/tests/xhr/send-data-formdata.any.js b/test/wpt/tests/xhr/send-data-formdata.any.js
new file mode 100644
index 0000000..6ff0479
--- /dev/null
+++ b/test/wpt/tests/xhr/send-data-formdata.any.js
@@ -0,0 +1,21 @@
+// META: title=XMLHttpRequest.send(formdata)
+
+var test = async_test();
+test.step(function()
+{
+ var xhr = new XMLHttpRequest();
+ var form = new FormData();
+ form.append("id", "0");
+ form.append("value", "zero");
+
+ xhr.onreadystatechange = test.step_func(() => {
+ if (xhr.readyState == 4) {
+ assert_equals(xhr.status, 200);
+ assert_equals(xhr.response, "id:0;value:zero;");
+ test.done();
+ }
+ });
+
+ xhr.open("POST", "./resources/form.py", true);
+ xhr.send(form);
+});
diff --git a/test/wpt/tests/xhr/send-data-sharedarraybuffer.any.js b/test/wpt/tests/xhr/send-data-sharedarraybuffer.any.js
new file mode 100644
index 0000000..79774c3
--- /dev/null
+++ b/test/wpt/tests/xhr/send-data-sharedarraybuffer.any.js
@@ -0,0 +1,27 @@
+// META: title=XMLHttpRequest.send(sharedarraybuffer)
+
+test(() => {
+ const xhr = new XMLHttpRequest();
+ // See https://github.com/whatwg/html/issues/5380 for why not `new SharedArrayBuffer()`
+ const buf = new WebAssembly.Memory({ shared:true, initial:1, maximum:1 }).buffer;
+
+ xhr.open("POST", "./resources/content.py", true);
+ assert_throws_js(TypeError, function() {
+ xhr.send(buf)
+ });
+}, "sending a SharedArrayBuffer");
+
+["Int8Array", "Uint8Array", "Uint8ClampedArray", "Int16Array", "Uint16Array",
+ "Int32Array", "Uint32Array", "BigInt64Array", "BigUint64Array",
+ "Float32Array", "Float64Array", "DataView"].forEach((type) => {
+ test(() => {
+ const xhr = new XMLHttpRequest();
+ // See https://github.com/whatwg/html/issues/5380 for why not `new SharedArrayBuffer()`
+ const arr = new self[type](new WebAssembly.Memory({ shared:true, initial:1, maximum:1 }).buffer);
+
+ xhr.open("POST", "./resources/content.py", true);
+ assert_throws_js(TypeError, function() {
+ xhr.send(arr)
+ });
+ }, `sending a ${type} backed by a SharedArrayBuffer`);
+});
diff --git a/test/wpt/tests/xhr/send-data-string-invalid-unicode.any.js b/test/wpt/tests/xhr/send-data-string-invalid-unicode.any.js
new file mode 100644
index 0000000..d9dc5a6
--- /dev/null
+++ b/test/wpt/tests/xhr/send-data-string-invalid-unicode.any.js
@@ -0,0 +1,46 @@
+// META: title=XMLHttpRequest.send(invalidUnicodeString)
+
+const LEFT_SURROGATE = '\ud83d';
+const RIGHT_SURROGATE = '\udc94';
+
+// Unmatched surrogates should be replaced with the unicode replacement
+// character, 0xFFFD. '$' in these templates is replaced with one of
+// LEFT_SURROGATE or RIGHT_SURROGATE according to the test.
+const TEMPLATES = {
+ '$': [239, 191, 189],
+ '$ab': [239, 191, 189, 97, 98],
+ 'a$b': [97, 239, 191, 189, 98],
+ 'ab$': [97, 98, 239, 191, 189],
+};
+
+for (const surrogate of [LEFT_SURROGATE, RIGHT_SURROGATE]) {
+ for (const [template, expected] of Object.entries(TEMPLATES)) {
+ const invalidString = template.replace('$', surrogate);
+ const printableString = template.replace(
+ '$', '\\u{' + surrogate.charCodeAt(0).toString(16) + '}');
+ async_test(t => {
+ xhrSendStringAndCheckResponseBody(t, invalidString, expected);
+ }, `invalid unicode '${printableString}' should be fixed with ` +
+ `replacement character`);
+ }
+}
+
+// For the sake of completeness, verify that matched surrogates work.
+async_test(t => {
+ xhrSendStringAndCheckResponseBody(t, LEFT_SURROGATE + RIGHT_SURROGATE,
+ [240, 159, 146, 148]);
+}, 'valid unicode should be sent correctly');
+
+function xhrSendStringAndCheckResponseBody(t, string, expected) {
+ const xhr = new XMLHttpRequest();
+ xhr.responseType = 'arraybuffer';
+ xhr.onload = t.step_func(() => {
+ assert_equals(xhr.status, 200, 'status should be 200');
+ const actualBody = new Uint8Array(xhr.response);
+ assert_array_equals(actualBody, expected, 'content should match');
+ t.done();
+ });
+ xhr.onerror = t.unreached_func('no error should occur');
+ xhr.open('POST', 'resources/content.py', true);
+ xhr.send(string);
+}
diff --git a/test/wpt/tests/xhr/send-data-unexpected-tostring.htm b/test/wpt/tests/xhr/send-data-unexpected-tostring.htm
new file mode 100644
index 0000000..203c386
--- /dev/null
+++ b/test/wpt/tests/xhr/send-data-unexpected-tostring.htm
@@ -0,0 +1,56 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>XMLHttpRequest: passing objects that interfere with the XHR instance to send()</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<link rel="help" href="https://xhr.spec.whatwg.org/#the-send()-method" data-tested-assertations="following::ol/li[4]" />
+<link rel="help" href="https://webidl.spec.whatwg.org/#es-union" data-tested-assertations="following::ol/li[16]" />
+
+
+<div id="log"></div>
+
+<script>
+ var test1 = async_test('abort() called from data stringification')
+ test1.step(function() {
+ var client = new XMLHttpRequest()
+ var objAbortsOnStringification = {toString:function(){
+ client.abort();
+ }}
+ client.open('POST', 'resources/content.py')
+ client.send(objAbortsOnStringification)
+ assert_equals(client.readyState, 1)
+ test1.done()
+ });
+
+ var test2 = async_test('open() called from data stringification')
+ test2.step(function() {
+ var client = new XMLHttpRequest()
+ var objOpensOnStringification = {toString:function(){
+ client.open('POST', 'resources/status.py?text=second_open_wins');
+ }}
+ client.onloadend = test2.step_func(function(){
+ assert_equals(client.statusText, 'second_open_wins')
+ test2.done()
+ })
+ client.open('POST', 'resources/status.py?text=first_open_wins')
+ client.send(objOpensOnStringification)
+ });
+
+ var test3 = async_test('send() called from data stringification')
+ test3.step(function() {
+ var client = new XMLHttpRequest()
+ var objSendsOnStringification = {toString:function(){
+ client.send('bomb!');
+ }}
+ client.onload = test3.step_func(function(){
+ assert_equals(client.responseText, 'bomb!')
+ test3.done()
+ })
+ client.open('POST', 'resources/content.py')
+ assert_throws_dom('InvalidStateError', function(){
+ client.send(objSendsOnStringification)
+ })
+ });
+
+
+</script>
diff --git a/test/wpt/tests/xhr/send-entity-body-basic.htm b/test/wpt/tests/xhr/send-entity-body-basic.htm
new file mode 100644
index 0000000..526a1fb
--- /dev/null
+++ b/test/wpt/tests/xhr/send-entity-body-basic.htm
@@ -0,0 +1,28 @@
+<!doctype html>
+<html>
+ <head>
+ <title>XMLHttpRequest: send() - data argument</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-send()-method" data-tested-assertations="/following::ol/li[4]" />
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ function request(input, output) {
+ test(function() {
+ var client = new XMLHttpRequest()
+ client.open("POST", "resources/content.py", false)
+ client.send(input)
+ assert_equals(client.responseText, output)
+ }, document.title + " (" + output + ")")
+ }
+ request(1, "1")
+ request(10000000, "10000000")
+ request([2,2], "2,2")
+ request(false, "false")
+ request("A\0A", "A\0A")
+ request(new URLSearchParams([[1, 2], [3, 4]]), "1=2&3=4")
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/send-entity-body-document-bogus.htm b/test/wpt/tests/xhr/send-entity-body-document-bogus.htm
new file mode 100644
index 0000000..c10e70a
--- /dev/null
+++ b/test/wpt/tests/xhr/send-entity-body-document-bogus.htm
@@ -0,0 +1,26 @@
+<!doctype html>
+<title>XMLHttpRequest: send() - Document with serialization errors</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+function serialize(input, output) {
+ async_test(t => {
+ const client = new XMLHttpRequest
+ client.open("POST", "resources/content.py")
+ client.send(input)
+ client.onload = t.step_func_done(() => {
+ assert_equals(client.responseText, output)
+ })
+ }, "Serializing documents through XMLHttpRequest: '" + output + "'")
+}
+
+var doc = document.implementation.createDocument(null, null, null)
+serialize(doc, "")
+doc.appendChild(doc.createElement("test:test"))
+serialize(doc, "<test:test/>")
+doc.childNodes[0].setAttribute("test:test", "gee")
+serialize(doc, "<test:test test:test=\"gee\"/>")
+doc.childNodes[0].setAttribute("x", "\uD800")
+serialize(doc, "<test:test test:test=\"gee\" x=\"\uFFFD\"/>")
+</script>
diff --git a/test/wpt/tests/xhr/send-entity-body-document.htm b/test/wpt/tests/xhr/send-entity-body-document.htm
new file mode 100644
index 0000000..e3a1070
--- /dev/null
+++ b/test/wpt/tests/xhr/send-entity-body-document.htm
@@ -0,0 +1,92 @@
+<!doctype html>
+<html>
+ <head>
+ <title>XMLHttpRequest: send() - Document</title>
+ <meta charset="utf-8">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-send()-method" data-tested-assertations="/following::ol/li[4]" />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#dom-XMLHttpRequest-send-document" data-tested-assertations="/following::dd" />
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ var tests = [
+ {
+ title: 'XML document, windows-1252',
+ url: 'resources/win-1252-xml.py',
+ contentType: 'application/xml;charset=UTF-8',
+ responseText: '<\u00FF\/>'
+ },
+ // Invalid character code in document turns into U+FFFD.
+ {
+ title: 'HTML document, invalid UTF-8',
+ url: 'resources/invalid-utf8-html.py',
+ contentType: 'text/html;charset=UTF-8',
+ responseText: '<body>\uFFFD<\/body>'
+ },
+ // Correctly serialized Shift-JIS.
+ {
+ title: 'HTML document, shift-jis',
+ url: 'resources/shift-jis-html.py',
+ contentType: 'text/html;charset=UTF-8',
+ responseText: '<body>\u30C6\u30b9\u30c8<\/body>'
+ },
+ // There's some markup included, but it's not really relevant for this
+ // test suite, so we do an indexOf() test.
+ {
+ title: 'plain text file',
+ url: 'folder.txt',
+ contentType: 'text/html;charset=UTF-8',
+ responseText: 'top'
+ },
+ // This test does not want to assert anything about what markup a
+ // standalone image should be wrapped in. Hence this test lacks a
+ // responseText expectation.
+ {
+ title: 'image file',
+ url: 'resources/image.gif',
+ contentType: 'text/html;charset=UTF-8'
+ },
+ {
+ title: 'img tag',
+ url: 'resources/img-utf8-html.py',
+ contentType: 'text/html;charset=UTF-8',
+ responseText: '<img>foo'
+ },
+ {
+ title: 'empty div',
+ url: 'resources/empty-div-utf8-html.py',
+ contentType: 'text/html;charset=UTF-8',
+ responseText: '<!DOCTYPE html><html><head></head><body><div></div></body></html>'
+ }
+ ];
+
+ tests.forEach(function(t) {
+ async_test(function() {
+ var iframe = document.createElement("iframe");
+ iframe.onload = this.step_func_done(function() {
+ var client = new XMLHttpRequest()
+ client.open("POST", "resources/content.py?response_charset_label=UTF-8", false)
+ client.send(iframe.contentDocument)
+ assert_equals(client.getResponseHeader('X-Request-Content-Type'),
+ t.contentType,
+ 'document should be serialized and sent as ' + t.contentType)
+ // The indexOf() assertion will overlook some stuff, e.g. XML
+ // prologues that shouldn't be there (looking at you, Presto).
+ // However, arguably these things have little to do with the XHR
+ // functionality we're testing.
+ if (t.responseText) {
+ assert_true(client.responseText.indexOf(t.responseText) != -1,
+ JSON.stringify(t.responseText) + " not in " +
+ JSON.stringify(client.responseText));
+ }
+ assert_equals(client.responseXML, null)
+ });
+ iframe.src = t.url;
+ document.body.appendChild(iframe);
+ }, t.title);
+ });
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/send-entity-body-empty.htm b/test/wpt/tests/xhr/send-entity-body-empty.htm
new file mode 100644
index 0000000..f307e77
--- /dev/null
+++ b/test/wpt/tests/xhr/send-entity-body-empty.htm
@@ -0,0 +1,26 @@
+<!doctype html>
+<html>
+ <head>
+ <title>XMLHttpRequest: send("") - empty entity body</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-send()-method" data-tested-assertations="following::ol[1]/li[7]" />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#dom-XMLHttpRequest-send-a-string" data-tested-assertations="following::p[1] following::p[2] following::p[3]" />
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ function request(method) {
+ var client = new XMLHttpRequest()
+ client.open(method, "resources/content.py", false)
+ client.upload.onloadstart = function(){assert_unreached('this event should not fire for empty strings')}
+ client.send("")
+ var expectedLength = method == "HEAD" ? "NO" : "0";
+ assert_equals(client.getResponseHeader("x-request-content-length"), expectedLength)
+ }
+ test(function() { request("POST"); }, document.title + " (POST)");
+ test(function() { request("PUT"); }, document.title + " (PUT)");
+ test(function() { request("HEAD"); }, document.title + " (HEAD)");
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/send-entity-body-get-head-async.htm b/test/wpt/tests/xhr/send-entity-body-get-head-async.htm
new file mode 100644
index 0000000..4a7deec
--- /dev/null
+++ b/test/wpt/tests/xhr/send-entity-body-get-head-async.htm
@@ -0,0 +1,39 @@
+<!doctype html>
+<html>
+ <head>
+ <title>XMLHttpRequest: send() - non-empty data argument and GET/HEAD - async, no upload events should fire</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-send()-method" data-tested-assertations="following::OL[1]/LI[3] following::OL[1]/LI[7] following::OL[1]/LI[8]" />
+
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ function request(method) {
+ var test = async_test( document.title + " (" + method + ")")
+ var events=[]
+ var logEvt = function (e) {
+ events.push(e.type)
+ }
+ var client = new XMLHttpRequest()
+ client.open(method, "resources/content.py")
+ client.upload.addEventListener('progress', logEvt)
+ client.upload.addEventListener('loadend', logEvt)
+ client.upload.addEventListener('loadstart', logEvt)
+ client.addEventListener('loadend', function(){
+ test.step(function(){
+ assert_equals(client.getResponseHeader("x-request-content-length"), "NO")
+ assert_equals(client.getResponseHeader("x-request-method"), method)
+ assert_equals(client.responseText, "")
+ assert_array_equals(events, [])
+ test.done()
+ })
+ })
+ client.send("TEST")
+ }
+ request("GET")
+ request("HEAD")
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/send-entity-body-get-head.htm b/test/wpt/tests/xhr/send-entity-body-get-head.htm
new file mode 100644
index 0000000..95ff711
--- /dev/null
+++ b/test/wpt/tests/xhr/send-entity-body-get-head.htm
@@ -0,0 +1,36 @@
+<!doctype html>
+<html>
+ <head>
+ <title>XMLHttpRequest: send() - non-empty data argument and GET/HEAD</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-send()-method" data-tested-assertations="following::OL[1]/LI[3] following::OL[1]/LI[7]" />
+
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ function request(method) {
+ test(function() {
+ var events=[]
+ var logEvt = function (e) {
+ events.push(e.type)
+ }
+ var client = new XMLHttpRequest()
+ client.open(method, "resources/content.py", false)
+ client.send("TEST")
+ client.upload.addEventListener('progress', logEvt)
+ client.upload.addEventListener('loadend', logEvt)
+ client.upload.addEventListener('loadstart', logEvt)
+
+ assert_equals(client.getResponseHeader("x-request-content-length"), "NO")
+ assert_equals(client.getResponseHeader("x-request-method"), method)
+ assert_equals(client.responseText, "")
+ assert_array_equals(events, [])
+ }, document.title + " (" + method + ")")
+ }
+ request("GET")
+ request("HEAD")
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/send-entity-body-none.htm b/test/wpt/tests/xhr/send-entity-body-none.htm
new file mode 100644
index 0000000..d757cb3
--- /dev/null
+++ b/test/wpt/tests/xhr/send-entity-body-none.htm
@@ -0,0 +1,40 @@
+<!doctype html>
+<html>
+ <head>
+ <title>XMLHttpRequest: send(null) - no entity body</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-send()-method" data-tested-assertations="following::ol[1]/li[4] following::ol[1]/li[7]" />
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ function noContentTypeTest(method) {
+ var client = new XMLHttpRequest()
+ client.open(method, "resources/content.py", false)
+ client.upload.onloadstart = function(){assert_unreached('this event should not fire for null')}
+ client.send(null)
+ var expectedLength = method == "HEAD" ? "NO" : "0";
+ assert_equals(client.getResponseHeader("x-request-content-length"), expectedLength)
+ assert_equals(client.getResponseHeader("x-request-content-type"), "NO")
+ }
+ test(function() { noContentTypeTest("POST"); }, "No content type (POST)");
+ test(function() { noContentTypeTest("PUT"); }, "No content type (PUT)");
+ test(function() { noContentTypeTest("HEAD"); }, "No content type (HEAD)");
+
+ function explicitContentTypeTest(method) {
+ var client = new XMLHttpRequest()
+ client.open(method, "resources/content.py", false)
+ var content_type = 'application/x-foo'
+ client.setRequestHeader('Content-Type', content_type)
+ client.send(null)
+ var expectedLength = method == "HEAD" ? "NO" : "0";
+ assert_equals(client.getResponseHeader("x-request-content-length"), expectedLength)
+ assert_equals(client.getResponseHeader("x-request-content-type"), content_type)
+ }
+ test(function() { explicitContentTypeTest("POST"); }, "Explicit content type (POST)");
+ test(function() { explicitContentTypeTest("PUT"); }, "Explicit content type (PUT)");
+ test(function() { explicitContentTypeTest("HEAD"); }, "Explicit content type (HEAD)");
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/send-network-error-async-events.sub.htm b/test/wpt/tests/xhr/send-network-error-async-events.sub.htm
new file mode 100644
index 0000000..c51a05c
--- /dev/null
+++ b/test/wpt/tests/xhr/send-network-error-async-events.sub.htm
@@ -0,0 +1,47 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#handler-xhr-onerror" data-tested-assertations="../.." />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-send()-method" data-tested-assertations="following::ol[1]/li[9]/ol/li[2] following::ol[1]/li[9]/ol/li[3]" />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#infrastructure-for-the-send()-method" data-tested-assertations="following::dt[4] following::dd[4]/p" />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#network-error" data-tested-assertations=".." />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#request-error" data-tested-assertations="following::ol[1]/li[4] following::ol[1]/li[6] following::ol[1]/li[7] following::ol[1]/li[7]/ol/li[3] following::ol[1]/li[7]/ol/li[4] following::ol[1]/li[9] following::ol[1]/li[10]" />
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <title>XMLHttpRequest: The send() method: Fire a progress event named error when Network error happens (synchronous flag is unset)</title>
+</head>
+
+<body>
+ <div id="log"></div>
+
+ <script type="text/javascript">
+ var test = async_test();
+
+ test.step(function(){
+ var xhr = new XMLHttpRequest();
+ var expect = ["loadstart", "upload.loadstart", 4, "upload.error", "upload.loadend", "error", "loadend"];
+ var actual = [];
+
+ xhr.onreadystatechange = test.step_func(() => {
+ if (xhr.readyState == 4) {
+ actual.push(xhr.readyState);
+ }
+ });
+
+ xhr.onloadstart = test.step_func(e => { actual.push(e.type); })
+ xhr.onloadend = test.step_func_done(e => {
+ actual.push(e.type);
+ assert_array_equals(actual, expect);
+ })
+ xhr.onerror = test.step_func(e => { actual.push(e.type); })
+
+ xhr.upload.onloadstart = test.step_func(e => { actual.push("upload." + e.type); })
+ xhr.upload.onloadend = test.step_func(e => { actual.push("upload." + e.type); })
+ xhr.upload.onerror = test.step_func(e => { actual.push("upload." + e.type); })
+
+ xhr.open("POST", "http://nonexistent.{{host}}:{{ports[http][0]}}", true);
+ xhr.send("Test Message");
+ });
+ </script>
+</body>
+</html>
diff --git a/test/wpt/tests/xhr/send-network-error-sync-events.sub.htm b/test/wpt/tests/xhr/send-network-error-sync-events.sub.htm
new file mode 100644
index 0000000..8011c58
--- /dev/null
+++ b/test/wpt/tests/xhr/send-network-error-sync-events.sub.htm
@@ -0,0 +1,45 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#infrastructure-for-the-send()-method" data-tested-assertations="following::dt[4] following::dd[4]/p" />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#network-error" data-tested-assertations=".." />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#request-error" data-tested-assertations="following::ol[1]/li[4] following::ol[1]/li[5]" />
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <title>XMLHttpRequest: The send() method: Throw a "throw an "NetworkError" exception when Network error happens (synchronous flag is set)</title>
+</head>
+
+<body>
+ <div id="log"></div>
+
+ <script type="text/javascript">
+ test(function()
+ {
+ var xhr = new XMLHttpRequest();
+
+ xhr.open("POST", "http://{{host}}:1", false); // Bad port.
+
+ assert_throws_dom("NetworkError", function()
+ {
+ xhr.send("Test Message");
+ });
+ assert_equals(xhr.readyState, 4)
+
+ }, "http URL");
+
+ test(function()
+ {
+ var xhr = new XMLHttpRequest();
+
+ xhr.open("GET", "data:text/html;charset=utf-8;base64,PT0NUWVBFIGh0bWw%2BDQo8", false);
+
+ assert_throws_dom("NetworkError", function()
+ {
+ xhr.send("Test Message");
+ });
+ assert_equals(xhr.readyState, 4)
+
+ }, "data URL");
+ </script>
+</body>
+</html>
diff --git a/test/wpt/tests/xhr/send-no-response-event-loadend.htm b/test/wpt/tests/xhr/send-no-response-event-loadend.htm
new file mode 100644
index 0000000..0a1eda5
--- /dev/null
+++ b/test/wpt/tests/xhr/send-no-response-event-loadend.htm
@@ -0,0 +1,48 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <title>XMLHttpRequest: The send() method: Fire a progress event named loadend (no response entity body)</title>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#handler-xhr-onloadend" data-tested-assertations="/../.." />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#event-xhr-loadend" data-tested-assertations="/../.." />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#infrastructure-for-the-send()-method" data-tested-assertations="following::dt[10] /following-sibling::ol/li[10]" />
+</head>
+
+<body>
+ <div id="log"></div>
+
+ <script type="text/javascript">
+ var test = async_test();
+
+ test.step(function ()
+ {
+ var xhr = new XMLHttpRequest();
+
+ xhr.onreadystatechange = function()
+ {
+ test.step(function()
+ {
+ if (xhr.readyState == 4)
+ {
+ assert_equals(xhr.response, "");
+ }
+ });
+ };
+
+ xhr.onloadend = function(e)
+ {
+ test.step(function()
+ {
+ assert_true(e instanceof ProgressEvent);
+ assert_equals(e.type, "loadend");
+ test.step(function() { test.done(); });
+ });
+ };
+
+ xhr.open("POST", "./resources/content.py", true);
+ xhr.send();
+ });
+ </script>
+</body>
+</html>
diff --git a/test/wpt/tests/xhr/send-no-response-event-loadstart.htm b/test/wpt/tests/xhr/send-no-response-event-loadstart.htm
new file mode 100644
index 0000000..cd4a068
--- /dev/null
+++ b/test/wpt/tests/xhr/send-no-response-event-loadstart.htm
@@ -0,0 +1,48 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#handler-xhr-onloadstart" data-tested-assertations="/../.." />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#event-xhr-loadstart" data-tested-assertations="/../.." />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-send()-method" data-tested-assertations="/following-sibling::ol/li[9]/ol/li[2]" />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-response-attribute" data-tested-assertations="/following-sibling::ol/li[1]" />
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <title>XMLHttpRequest: The send() method: Fire a progress event named loadstart (no response entity body and the state is LOADING)</title>
+</head>
+
+<body>
+ <div id="log"></div>
+
+ <script type="text/javascript">
+ var test = async_test();
+
+ test.step(function()
+ {
+ var xhr = new XMLHttpRequest();
+
+ xhr.onreadystatechange = function()
+ {
+ test.step(function()
+ {
+ if (xhr.readyState == 3)
+ {
+ assert_equals(xhr.response, "");
+ }
+ else if (xhr.readyState == 4)
+ {
+ assert_unreached("loadstart event did not fire in LOADING state!");
+ }
+ });
+ };
+
+ xhr.onloadstart = function()
+ {
+ test.step(function() { test.done("Test done!"); });
+ };
+
+ xhr.open("POST", "./resources/content.py", true);
+ xhr.send();
+ });
+ </script>
+</body>
+</html>
diff --git a/test/wpt/tests/xhr/send-no-response-event-order.htm b/test/wpt/tests/xhr/send-no-response-event-order.htm
new file mode 100644
index 0000000..44c1d77
--- /dev/null
+++ b/test/wpt/tests/xhr/send-no-response-event-order.htm
@@ -0,0 +1,45 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#handler-xhr-onloadstart" data-tested-assertations="../.." />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#handler-xhr-onloadend" data-tested-assertations="../.." />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#event-xhr-loadstart" data-tested-assertations="../.." />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#event-xhr-loadend" data-tested-assertations="../.." />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-send()-method" data-tested-assertations="following-sibling::ol/li[9]/ol/li[2]" />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#infrastructure-for-the-send()-method" data-tested-assertations="following::dt[10] following::a[contains(@href,'#switch-done')]/.." />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#switch-done" data-tested-assertations="following::ol[1]/li[3] following::ol[1]/li[4] following::ol[1]/li[6] following::ol[1]/li[7]" />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-response-attribute" data-tested-assertations="following-sibling::ol/li[1]" />
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="resources/xmlhttprequest-event-order.js"></script>
+ <title>XMLHttpRequest: The send() method: event order when there is no response entity body</title>
+</head>
+
+<body>
+ <div id="log"></div>
+
+ <script type="text/javascript">
+ var test = async_test();
+
+ test.step(function()
+ {
+ var xhr = new XMLHttpRequest();
+ prepare_xhr_for_event_order_test(xhr);
+
+ xhr.addEventListener("readystatechange", test.step_func(function() {
+ if (xhr.readyState == 3) {
+ assert_equals(xhr.response, "");
+ }
+ }));
+
+ xhr.addEventListener("loadend", test.step_func(function(e) {
+ assert_xhr_event_order_matches([1, "loadstart(0,0,false)", 2, "progress(0,0,false)", 4,"load(0,0,false)", "loadend(0,0,false)"]);
+ test.done();
+ }));
+
+ xhr.open("POST", "./resources/content.py", true);
+ xhr.send();
+ });
+ </script>
+</body>
+</html>
diff --git a/test/wpt/tests/xhr/send-non-same-origin.htm b/test/wpt/tests/xhr/send-non-same-origin.htm
new file mode 100644
index 0000000..bb9f32c
--- /dev/null
+++ b/test/wpt/tests/xhr/send-non-same-origin.htm
@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>XMLHttpRequest: send() - non same-origin</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <base>
+ </head>
+ <body>
+ <div id="log"></div>
+ <script src="/common/get-host-info.sub.js"></script>
+ <script>
+ // Setting base URL before running the tests
+ var host_info = get_host_info();
+ document.getElementsByTagName("base")[0].setAttribute("href", host_info.HTTP_REMOTE_ORIGIN);
+
+ function url(url) {
+ test(function() {
+ var client = new XMLHttpRequest()
+ client.open("GET", url, false)
+ assert_throws_dom("NetworkError", function() { client.send() })
+ }, document.title + " (" + url + ")")
+ }
+ url("mailto:test@example.org")
+ url("tel:+31600000000")
+ url(host_info.HTTP_REMOTE_ORIGIN)
+ url("javascript:alert('FAIL')")
+ url("folder.txt")
+ url("about:blank")
+ url("blob:bogusidentifier")
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/send-receive-utf16.htm b/test/wpt/tests/xhr/send-receive-utf16.htm
new file mode 100644
index 0000000..6d6fb90
--- /dev/null
+++ b/test/wpt/tests/xhr/send-receive-utf16.htm
@@ -0,0 +1,37 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>XMLHttpRequest: The send() method: receive data which is UTF-16 encoded</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#text-response" data-tested-assertations="following::ol/li[9]" />
+<div id="log"></div>
+
+<script>
+ async_test(function() {
+ var client = new XMLHttpRequest();
+ client.onload = this.step_func_done(function(e) {
+ assert_equals(client.responseText, 'æøå\nテスト\n')
+ });
+ client.open("GET", "resources/utf16.txt");
+ client.send(null);
+ }, 'UTF-16 with BOM, no encoding in content-type');
+
+ async_test(function() {
+ var client = new XMLHttpRequest();
+ client.onload = this.step_func_done(function(e) {
+ assert_equals(client.responseText, 'æøå\nテスト\n')
+ });
+ client.open("GET", "resources/status.py?code=200&type=text%2Fplain%3Bcharset%3DUTF-16&content=%E6%00%F8%00%E5%00%0A%00%C6%30%B9%30%C8%30%0A%00");
+ client.send(null);
+ }, 'UTF-16 without BOM, with charset label in content-type');
+
+ async_test(function() {
+ var client = new XMLHttpRequest();
+ client.onload = this.step_func_done(function(e) {
+ // plenty of EF BF BD Replacement Character in this invalid input..
+ assert_equals(client.responseText, "\ufffd\u0000\ufffd\u0000\ufffd\u0000\u000a\u0000\ufffd\u0030\ufffd\u0030\ufffd\u0030\u000a\u0000")
+ });
+ client.open("GET", "resources/status.py?code=200&type=text%2Fplain%3Bcharset%3DUTF-8&content=%E6%00%F8%00%E5%00%0A%00%C6%30%B9%30%C8%30%0A%00");
+ client.send(null);
+ }, 'UTF-16 without BOM, mislabelled as UTF-8 in content-type');
+</script>
diff --git a/test/wpt/tests/xhr/send-redirect-bogus-sync.htm b/test/wpt/tests/xhr/send-redirect-bogus-sync.htm
new file mode 100644
index 0000000..0f0598b
--- /dev/null
+++ b/test/wpt/tests/xhr/send-redirect-bogus-sync.htm
@@ -0,0 +1,26 @@
+<!doctype html>
+<html>
+ <head>
+ <title>XMLHttpRequest: send() - Redirects (bogus Location header; sync)</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#infrastructure-for-the-send()-method" data-tested-assertations="following::dl[1]/dt[2] following::dl[1]/dd[2]/ol/li[1] following::dl[1]/dd[2]/ol/li[3]" />
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ function redirect(code, location) {
+ test(function() {
+ var client = new XMLHttpRequest()
+ client.open("GET", "resources/redirect.py?location=" + location + "&code=" + code, false)
+ assert_throws_dom("NetworkError", function() { client.send(null) })
+ }, document.title + " (" + code + ": " + location + ")")
+ }
+ redirect("301", "foobar://abcd")
+ redirect("302", "http://z.")
+ redirect("302", "mailto:someone@example.org")
+ redirect("303", "http://z.")
+ redirect("303", "tel:1234567890")
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/send-redirect-bogus.htm b/test/wpt/tests/xhr/send-redirect-bogus.htm
new file mode 100644
index 0000000..ad50b56
--- /dev/null
+++ b/test/wpt/tests/xhr/send-redirect-bogus.htm
@@ -0,0 +1,36 @@
+<!doctype html>
+<html>
+ <head>
+ <title>XMLHttpRequest: send() - Redirects (bogus Location header)</title>
+ <meta name=timeout content=long>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#infrastructure-for-the-send()-method" data-tested-assertations="following::dl[1]/dt[2] following::dl[1]/dd[2]/ol/li[1] following::dl[1]/dd[2]/ol/li[3]" />
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ function redirect(code, location) {
+ var test = async_test(document.title + " (" + code + ": " + location + ")");
+ test.step(function() {
+ var client = new XMLHttpRequest()
+ client.onreadystatechange = function() {
+ test.step(function() {
+ if(client.readyState == 4) {
+ assert_equals(client.status, 0)
+ assert_equals(client.statusText, "")
+ test.done()
+ }
+ })
+ }
+ client.open("GET", "resources/redirect.py?location=" + location + "&code=" + code)
+ client.send(null)
+ })
+ }
+ redirect("302", "http://example.not")
+ redirect("302", "mailto:someone@example.org")
+ redirect("303", "http://example.not")
+ redirect("303", "foobar:someone@example.org")
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/send-redirect-infinite-sync.htm b/test/wpt/tests/xhr/send-redirect-infinite-sync.htm
new file mode 100644
index 0000000..cc6d7a2
--- /dev/null
+++ b/test/wpt/tests/xhr/send-redirect-infinite-sync.htm
@@ -0,0 +1,24 @@
+<!doctype html>
+<html>
+ <head>
+ <title>XMLHttpRequest: send() - Redirects (infinite loop; sync)</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#infrastructure-for-the-send()-method" data-tested-assertations="following::dl[1]/dt[2] following::dl[1]/dd[2]/p[1]" />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#network-error" data-tested-assertations=".." />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#request-error" data-tested-assertations="following::ol[1]/li[5]" />
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ function redirect(code) {
+ test(function() {
+ var client = new XMLHttpRequest()
+ client.open("GET", "resources/infinite-redirects.py?type="+code, false)
+ assert_throws_dom("NetworkError", function() { client.send(null) })
+ }, document.title + " (" + code + ")")
+ }
+ redirect("301")
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/send-redirect-infinite.htm b/test/wpt/tests/xhr/send-redirect-infinite.htm
new file mode 100644
index 0000000..54e2ea8
--- /dev/null
+++ b/test/wpt/tests/xhr/send-redirect-infinite.htm
@@ -0,0 +1,35 @@
+<!doctype html>
+<html>
+ <head>
+ <title>XMLHttpRequest: send() - Redirects (infinite loop)</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#handler-xhr-onerror" data-tested-assertations="../.." />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#infrastructure-for-the-send()-method" data-tested-assertations="following::dl[1]/dt[2] following::dl[1]/dd[2]/p[1]" />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#network-error" data-tested-assertations=".." />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#request-error" data-tested-assertations="following::ol[1]/li[4] following::ol[1]/li[9] following::ol[1]/li[10]" />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-responsetext-attribute" data-tested-assertations="following::ol[1]/li[3]" />
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ var test = async_test()
+ var client = new XMLHttpRequest(),
+ errorEventFired = false,
+ code = 301
+ client.open("GET", "resources/infinite-redirects.py?type="+code)
+ client.onerror = function(){
+ errorEventFired = true
+ }
+ client.onloadend = function(){
+ test.step(function() {
+ assert_equals(errorEventFired, true)
+ assert_equals(client.responseText, '')
+ assert_equals(client.readyState, 4)
+ test.done()
+ })
+ }
+ client.send(null)
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/send-redirect-no-location.htm b/test/wpt/tests/xhr/send-redirect-no-location.htm
new file mode 100644
index 0000000..3a80348
--- /dev/null
+++ b/test/wpt/tests/xhr/send-redirect-no-location.htm
@@ -0,0 +1,40 @@
+<!doctype html>
+<html>
+ <head>
+ <title>XMLHttpRequest: send() - Redirects (no Location header)</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#infrastructure-for-the-send()-method" data-tested-assertations="following::dl[1]/dt[2]" />
+ <!--
+ NOTE: the XHR spec does not really handle this scenario. It's handled in the Fetch spec:
+ "If response's headers do not contain a header whose name is Location, return response."
+ -->
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ function redirect(code) {
+ var test = async_test(document.title + " (" + code + ")")
+ test.step(function() {
+ var client = new XMLHttpRequest()
+ client.onreadystatechange = function() {
+ test.step(function() {
+ if(client.readyState == 4) {
+ assert_equals(client.status + "", code)
+ assert_equals(client.statusText, "ABE ODDYSSEE")
+ assert_equals(client.responseXML.documentElement.localName, "x")
+ test.done()
+ }
+ })
+ }
+ client.open("GET", "resources/status.py?content=<x>x<\/x>&type=text/xml&text=ABE ODDYSSEE&code=" + code)
+ client.send(null)
+ })
+ }
+ redirect("301")
+ redirect("302")
+ redirect("303")
+ redirect("307")
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/send-redirect-post-upload.htm b/test/wpt/tests/xhr/send-redirect-post-upload.htm
new file mode 100644
index 0000000..b1db8ec
--- /dev/null
+++ b/test/wpt/tests/xhr/send-redirect-post-upload.htm
@@ -0,0 +1,140 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#handler-xhr-onprogress" data-tested-assertations="../.." />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#event-xhr-progress" data-tested-assertations="../.." />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-send()-method" data-tested-assertations="following::dt[@id="dom-xmlhttprequest-send-bodyinit"]/following::dd[1]/p[2] following::ol[1]/li[9]//li[1] following::ol[1]/li[9]//li[2]" />
+ <link rel="help" href="https://fetch.spec.whatwg.org/#http-fetch" data-tested-assertations="following::ol[1]/li[6]/dl/dd[1]//dd[3]" />
+ <link rel="help" href="https://fetch.spec.whatwg.org/#concept-http-redirect-fetch" data-tested-assertations="following::li[16]" />
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <title>XMLHttpRequest: The send() method: POSTing to URL that redirects</title>
+</head>
+
+<body>
+ <div id="log"></div>
+
+ <script type="text/javascript">
+ function testRedirectPost(params) {
+ var test = async_test(document.title + " (" + params.name + ")");
+ var actual = [];
+ // We check upload.onprogress with a boolean because it *might* fire more than once
+ var progressFiredReadyState1 = false;
+
+ var expectedHeaders, expectedEvents;
+
+ // 307 redirects should resend the POST data, and events and headers will be a little different..
+ if(params.expectResendPost) {
+ expectedHeaders = {
+ "X-Request-Content-Length": "12000",
+ "X-Request-Content-Type": "text/plain;charset=UTF-8",
+ "X-Request-Method": "POST",
+ "X-Request-Query": "NO",
+ "Content-Length": "12000"
+ }
+ expectedEvents = [
+ "xhr onreadystatechange 1",
+ "xhr loadstart 1",
+ "upload loadstart 1",
+ "upload loadend 1",
+ "xhr onreadystatechange 2",
+ "xhr onreadystatechange 3",
+ "xhr onreadystatechange 4",
+ "xhr load 4",
+ "xhr loadend 4"
+ ];
+ } else {
+ // setting the right expectations for POST resent as GET without request body
+ expectedHeaders = {
+ "X-Request-Content-Length": "NO",
+ "X-Request-Content-Type": "NO",
+ "X-Request-Method": "GET",
+ "X-Request-Query": "NO"
+ }
+ expectedEvents = [
+ "xhr onreadystatechange 1",
+ "xhr loadstart 1",
+ "upload loadstart 1",
+ "upload loadend 1",
+ "xhr onreadystatechange 2",
+ /* we expect no onreadystatechange readyState=3 event because there is no loading content */
+ "xhr onreadystatechange 4",
+ "xhr load 4",
+ "xhr loadend 4"
+ ];
+ }
+ // Override expectations if provided.
+ if(params.expectedContentType)
+ expectedHeaders["X-Request-Content-Type"] = params.expectedContentType;
+
+ test.step(function()
+ {
+ var xhr = new XMLHttpRequest();
+
+ xhr.upload.onloadstart = test.step_func(function(e) {
+ actual.push("upload loadstart " + xhr.readyState);
+ });
+ xhr.upload.onprogress = test.step_func(function(e) {
+ // events every 50ms, one final when uploading is done
+ if(xhr.readyState >= xhr.HEADERS_RECEIVED) {
+ assert_equals(xhr.status, 200, "JS never gets to see the 30x status code");
+ }
+ progressFiredReadyState1 = xhr.readyState === xhr.OPENED;
+ });
+ xhr.upload.onloadend = test.step_func(function() {
+ actual.push("upload loadend " + xhr.readyState);
+ });
+ xhr.onloadstart = test.step_func(function() {
+ actual.push("xhr loadstart " + xhr.readyState);
+ });
+ xhr.onreadystatechange = test.step_func(function() {
+ if(xhr.readyState >= xhr.HEADERS_RECEIVED) {
+ assert_equals(xhr.status, 200, "JS never gets to see the 30x status code");
+ }
+
+ // The UA may fire multiple "readystatechange" events while in
+ // the "loading" state.
+ // https://xhr.spec.whatwg.org/#the-send()-method
+ if (xhr.readyState === 3 && actual[actual.length - 1] === "xhr onreadystatechange 3") {
+ return;
+ }
+
+ actual.push("xhr onreadystatechange " + xhr.readyState);
+ });
+ xhr.onload = test.step_func(function(e)
+ {
+ actual.push("xhr load " + xhr.readyState);
+ });
+ xhr.onloadend = test.step_func(function(e)
+ {
+ actual.push("xhr loadend " + xhr.readyState);
+
+ assert_true(progressFiredReadyState1, "One progress event should fire on xhr.upload when readyState is 1");
+
+ // Headers will tell us if data was sent when expected
+ for(var header in expectedHeaders) {
+ assert_equals(xhr.getResponseHeader(header), expectedHeaders[header], header);
+ }
+
+ assert_array_equals(actual, expectedEvents, "events firing in expected order and states");
+ if (params.expectedBody)
+ assert_equals(xhr.response, params.expectedBody, 'request body was resent');
+ test.done();
+ });
+
+ xhr.open("POST", "./resources/redirect.py?location=content.py&code=" + params.code, true);
+ xhr.send(params.body);
+ });
+ }
+
+ const stringBody = "Test Message".repeat(1000);
+ const blobBody = new Blob(new Array(1000).fill("Test Message"));
+
+ testRedirectPost({name: "301", code: 301, expectResendPost: false, body: stringBody});
+ testRedirectPost({name: "302", code: 302, expectResendPost: false, body: stringBody});
+ testRedirectPost({name: "303", code: 303, expectResendPost: false, body: stringBody});
+ testRedirectPost({name: "307 (string)", code: 307, expectResendPost: true, body: stringBody, expectedBody: stringBody });
+ testRedirectPost({name: "307 (blob)", code: 307, expectResendPost: true, body: blobBody, expectedBody: stringBody, expectedContentType: "NO" });
+ </script>
+</body>
+</html>
diff --git a/test/wpt/tests/xhr/send-redirect-to-cors.htm b/test/wpt/tests/xhr/send-redirect-to-cors.htm
new file mode 100644
index 0000000..54d7eb5
--- /dev/null
+++ b/test/wpt/tests/xhr/send-redirect-to-cors.htm
@@ -0,0 +1,92 @@
+<!doctype html>
+<html>
+ <head>
+ <title>XMLHttpRequest: send() - Redirect to CORS-enabled resource</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ function extractBody(body) {
+ if (body === null) {
+ return { body: "", type: "NO" };
+ }
+ if (typeof body === "string") {
+ return { body: body, type: "text/plain;charset=UTF-8" };
+ }
+ if (body instanceof Uint8Array) {
+ const arr = Array.prototype.slice.call(body);
+ return { body: String.fromCharCode.apply(null, arr), type: "NO" }
+ }
+ return { body: "EXTRACT NOT IMPLEMENTED", type: "EXTRACT NOT IMPLEMENTED" }
+ }
+
+ function redirect(code, name = code, method = "GET", body = null, explicitType = null, safelistContentType = false) {
+ async_test(t => {
+ let { body: expectedBody, type: expectedType } = extractBody(body);
+ if (explicitType !== null) {
+ expectedType = explicitType;
+ }
+ let expectedMethod = method;
+ if (((code === "301" || code === "302") && method === "POST") || (code === "303" && method !== "GET" && method !== "HEAD")) {
+ expectedMethod = "GET";
+ expectedBody = "";
+ expectedType = "NO";
+ }
+ const client = new XMLHttpRequest();
+ client.onreadystatechange = t.step_func(() => {
+ if (client.readyState === 4) {
+ if ((expectedMethod === "GET" && expectedType === "NO") || explicitType !== "application/x-pony" || safelistContentType) {
+ assert_equals(client.status, 200);
+ assert_equals(client.getResponseHeader("x-request-method"), expectedMethod);
+ assert_equals(client.getResponseHeader("x-request-content-type"), expectedType);
+ assert_equals(client.getResponseHeader("x-request-data"), expectedBody);
+ } else {
+ // "application/x-pony" is not safelisted by corsenabled.py -> network error
+ assert_equals(client.status, 0);
+ assert_equals(client.statusText, "");
+ assert_equals(client.responseText, "");
+ assert_equals(client.responseXML, null);
+ }
+ t.done();
+ }
+ });
+ let safelist = "";
+ if (safelistContentType) {
+ safelist = "?safelist_content_type";
+ }
+ client.open(method, "resources/redirect.py?location="+encodeURIComponent("http://www2."+location.host+(location.pathname.replace(/[^\/]+$/, ''))+'resources/corsenabled.py')+safelist+"&code=" + code);
+ if (explicitType !== null) {
+ client.setRequestHeader("Content-Type", explicitType);
+ }
+ client.send(body);
+ }, document.title + " (" + name + ")");
+ }
+ // corsenabled.py safelists methods GET, POST, PUT, and FOO
+ redirect("301")
+ redirect("301", "301 GET with explicit Content-Type", "GET", null, "application/x-pony")
+ redirect("301", "301 GET with explicit Content-Type safelisted", "GET", null, "application/x-pony", true)
+ redirect("303", "303 GET with explicit Content-Type safelisted", "GET", null, "application/x-pony", true)
+ redirect("302")
+ redirect("303")
+ redirect("302", "302 FOO with string and explicit Content-Type safelisted", "FOO", "test", "application/x-pony", true)
+ redirect("303", "303 FOO with string and explicit Content-Type safelisted", "FOO", "test", "application/x-pony", true)
+ redirect("307")
+ redirect("307", "307 post with null", "POST", null)
+ redirect("307", "307 post with string", "POST", "hello")
+ redirect("307", "307 post with typed array", "POST", new Uint8Array([65, 66, 67]))
+ redirect("301", "301 POST with string and explicit Content-Type", "POST", "yoyo", "application/x-pony")
+ redirect("301", "301 POST with string and explicit Content-Type safelisted", "POST", "yoyo", "application/x-pony", true)
+ redirect("302", "302 POST with string and explicit Content-Type", "POST", "yoyo", "application/x-pony")
+ redirect("307", "307 POST with string and explicit Content-Type", "POST", "yoyo", "application/x-pony")
+ redirect("307", "307 FOO with string and explicit Content-Type", "FOO", "yoyo", "application/x-pony")
+ redirect("308", "308 POST with string and explicit Content-Type", "POST", "yoyo", "application/x-pony")
+ redirect("308", "308 FOO with string and explicit Content-Type", "FOO", "yoyo", "application/x-pony")
+ redirect("308", "308 FOO with string and explicit Content-Type text/plain", "FOO", "yoyo", "text/plain")
+ redirect("308", "308 FOO with string and explicit Content-Type multipart/form-data", "FOO", "yoyo", "multipart/form-data")
+ redirect("308", "308 FOO with string and explicit Content-Type safelisted", "FOO", "yoyo", "application/thunderstorm", true)
+ redirect("307", "307 POST with string and explicit Content-Type safelisted", "POST", "yoyo", "application/thunderstorm", true)
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/send-redirect-to-non-cors.htm b/test/wpt/tests/xhr/send-redirect-to-non-cors.htm
new file mode 100644
index 0000000..c6886a5
--- /dev/null
+++ b/test/wpt/tests/xhr/send-redirect-to-non-cors.htm
@@ -0,0 +1,37 @@
+<!doctype html>
+<html>
+ <head>
+ <title>XMLHttpRequest: send() - Redirect to cross-origin resource, not CORS-enabled</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#infrastructure-for-the-send()-method" data-tested-assertations="following::dl[1]/dt[2] following::dl[1]/dd[2]/ol/li[1] following::dl[1]/dd[2]/ol/li[3]" />
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ function redirect(code) {
+ var test = async_test(document.title + " (" + code + ")")
+ test.step(function() {
+ var client = new XMLHttpRequest()
+ client.onreadystatechange = function() {
+ test.step(function() {
+ if(client.readyState == 4) {
+ assert_equals(client.getResponseHeader("x-request-method"), null)
+ assert_equals(client.getResponseHeader("x-request-content-type"), null)
+ assert_equals(client.responseText, '')
+ test.done()
+ }
+ })
+ }
+ client.open("GET", "resources/redirect.py?location="+encodeURIComponent("http://www2."+location.host+(location.pathname.replace(/[^\/]+$/, ''))+'resources/content.py')+"&code=" + code)
+ client.setRequestHeader("Content-Type", "application/x-pony")
+ client.send(null)
+ })
+ }
+ redirect("301")
+ redirect("302")
+ redirect("303")
+ redirect("307")
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/send-redirect.htm b/test/wpt/tests/xhr/send-redirect.htm
new file mode 100644
index 0000000..16b3231
--- /dev/null
+++ b/test/wpt/tests/xhr/send-redirect.htm
@@ -0,0 +1,36 @@
+<!doctype html>
+<html>
+ <head>
+ <title>XMLHttpRequest: send() - Redirects (basics)</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#infrastructure-for-the-send()-method" data-tested-assertations="following::dl[1]/dt[2] following::dl[1]/dd[2]/ol/li[1] following::dl[1]/dd[2]/ol/li[2]" />
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ function redirect(code) {
+ var test = async_test(document.title + " (" + code + ")")
+ test.step(function() {
+ var client = new XMLHttpRequest()
+ client.onreadystatechange = function() {
+ test.step(function() {
+ if(client.readyState == 4) {
+ assert_equals(client.getResponseHeader("x-request-method"), "GET")
+ assert_equals(client.getResponseHeader("x-request-content-type"), "application/x-pony")
+ test.done()
+ }
+ })
+ }
+ client.open("GET", "resources/redirect.py?location=content.py&code=" + code)
+ client.setRequestHeader("Content-Type", "application/x-pony")
+ client.send(null)
+ })
+ }
+ redirect("301")
+ redirect("302")
+ redirect("303")
+ redirect("307")
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/send-response-event-order.htm b/test/wpt/tests/xhr/send-response-event-order.htm
new file mode 100644
index 0000000..041cb23
--- /dev/null
+++ b/test/wpt/tests/xhr/send-response-event-order.htm
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#handler-xhr-onloadstart" data-tested-assertations="../.." />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#handler-xhr-onloadend" data-tested-assertations="../.." />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#event-xhr-loadstart" data-tested-assertations="../.." />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#event-xhr-loadend" data-tested-assertations="../.." />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-send()-method" data-tested-assertations="following-sibling::ol/li[9]/ol/li[2] following-sibling::ol/li[9]/ol/li[3] following::a[contains(@href,'#make-upload-progress-notifications')]/.. following::a[contains(@href,'#make-progress-notifications')]/.." />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#make-upload-progress-notifications" data-tested-assertations="following::ul[1]/li[1] following::ul[1]/li[2]/ol[1]/li[2] following::ul[1]/li[2]/ol[1]/li[3] following::ul[1]/li[2]/ol[1]/li[4]" />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#make-progress-notifications" data-tested-assertations=".." />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#infrastructure-for-the-send()-method" data-tested-assertations="following::a[contains(@href,'#switch-done')]/.." />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#switch-done" data-tested-assertations="following::ol[1]/li[3] following::ol[1]/li[4] following::ol[1]/li[5] following::ol[1]/li[6] following::ol[1]/li[7]" />
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="resources/xmlhttprequest-event-order.js"></script>
+ <title>XMLHttpRequest: The send() method: event order when synchronous flag is unset</title>
+</head>
+
+<body>
+ <div id="log"></div>
+
+ <script type="text/javascript">
+ var test = async_test();
+
+ test.step(function()
+ {
+ var xhr = new XMLHttpRequest();
+ prepare_xhr_for_event_order_test(xhr);
+
+ xhr.addEventListener("loadend", test.step_func(function() {
+ assert_xhr_event_order_matches([1, "loadstart(0,0,false)", "upload.loadstart(0,12,true)", "upload.progress(12,12,true)", "upload.load(12,12,true)", "upload.loadend(12,12,true)", 2, 3, "progress(12,12,true)", 4, "load(12,12,true)", "loadend(12,12,true)"]);
+ test.done();
+ }));
+
+ xhr.open("POST", "./resources/content.py", true);
+ xhr.send("Test Message");
+ });
+ </script>
+</body>
+</html>
diff --git a/test/wpt/tests/xhr/send-response-upload-event-loadend.htm b/test/wpt/tests/xhr/send-response-upload-event-loadend.htm
new file mode 100644
index 0000000..99a239a
--- /dev/null
+++ b/test/wpt/tests/xhr/send-response-upload-event-loadend.htm
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#handler-xhr-onloadend" data-tested-assertations="../.." />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#event-xhr-loadend" data-tested-assertations="../.." />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-send()-method" data-tested-assertations="following::a[contains(@href,'#make-upload-progress-notifications')]/.. following::ol[1]/li[8]" />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#make-upload-progress-notifications" data-tested-assertations="following::ul[1]/li[2]/ol[1]/li[4]" />
+
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <title>XMLHttpRequest: The send() method: Fire a progress event named loadend on the XMLHttpRequestUpload (synchronous flag is unset)</title>
+</head>
+
+<body>
+ <div id="log"></div>
+
+ <script type="text/javascript">
+ var test = async_test();
+
+ test.step(function()
+ {
+ var xhr = new XMLHttpRequest();
+
+ xhr.upload.onloadend = function(e)
+ {
+ test.step(function()
+ {
+ assert_true(e instanceof ProgressEvent);
+ assert_equals(e.type, "loadend");
+ assert_equals(e.target, xhr.upload);
+ test.done();
+ });
+ };
+
+ xhr.open("POST", "./resources/content.py", true);
+ xhr.send("Test Message");
+ });
+ </script>
+</body>
+</html>
diff --git a/test/wpt/tests/xhr/send-response-upload-event-loadstart.htm b/test/wpt/tests/xhr/send-response-upload-event-loadstart.htm
new file mode 100644
index 0000000..7a9be9f
--- /dev/null
+++ b/test/wpt/tests/xhr/send-response-upload-event-loadstart.htm
@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#handler-xhr-onloadstart" data-tested-assertations="../.." />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#event-xhr-loadstart" data-tested-assertations="../.." />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-send()-method" data-tested-assertations="following::ol[1]/li[8] following-sibling::ol/li[9]/ol/li[3]" />
+
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <title>XMLHttpRequest: The send() method: Fire a progress event named loadstart on the XMLHttpRequestUpload (synchronous flag is unset)</title>
+</head>
+
+<body>
+ <div id="log"></div>
+
+ <script type="text/javascript">
+ var test = async_test();
+
+ test.step(function()
+ {
+ var xhr = new XMLHttpRequest();
+
+ xhr.upload.onloadstart = function(e)
+ {
+ test.step(function()
+ {
+ assert_true(e instanceof ProgressEvent);
+ assert_equals(e.type, "loadstart");
+ assert_equals(e.target, xhr.upload);
+ test.done();
+ });
+ };
+
+ xhr.open("POST", "./resources/content.py", true);
+ xhr.send("Test Message");
+ });
+ </script>
+</body>
+</html>
diff --git a/test/wpt/tests/xhr/send-response-upload-event-progress.htm b/test/wpt/tests/xhr/send-response-upload-event-progress.htm
new file mode 100644
index 0000000..914aed7
--- /dev/null
+++ b/test/wpt/tests/xhr/send-response-upload-event-progress.htm
@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#handler-xhr-onprogress" data-tested-assertations="../.." />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#event-xhr-progress" data-tested-assertations="../.." />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-send()-method" data-tested-assertations="following::a[contains(@href,'#make-upload-progress-notifications')]/.. following::ol[1]/li[8]" />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#make-upload-progress-notifications" data-tested-assertations="following::ul[1]/li[2]/ol[1]/li[2]" />
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <title>XMLHttpRequest: The send() method: Fire a progress event named progress on the XMLHttpRequestUpload (synchronous flag is unset)</title>
+</head>
+
+<body>
+ <div id="log"></div>
+
+ <script type="text/javascript">
+ var test = async_test();
+
+ test.step(function()
+ {
+ var xhr = new XMLHttpRequest();
+
+ xhr.upload.onprogress = function(e)
+ {
+ test.step(function()
+ {
+ assert_true(e instanceof ProgressEvent);
+ assert_equals(e.type, "progress");
+ assert_equals(e.target, xhr.upload);
+ test.done();
+ });
+ };
+
+ xhr.open("POST", "./resources/content.py", true);
+ xhr.send("Test Message");
+ });
+ </script>
+</body>
+</html>
diff --git a/test/wpt/tests/xhr/send-send.any.js b/test/wpt/tests/xhr/send-send.any.js
new file mode 100644
index 0000000..64b1554
--- /dev/null
+++ b/test/wpt/tests/xhr/send-send.any.js
@@ -0,0 +1,7 @@
+test(function() {
+ var client = new XMLHttpRequest()
+ client.open("GET", "resources/well-formed.xml")
+ client.send(null)
+ assert_throws_dom("InvalidStateError", function() { client.send(null) })
+ client.abort()
+}, "XMLHttpRequest: send() - send()");
diff --git a/test/wpt/tests/xhr/send-sync-blocks-async.htm b/test/wpt/tests/xhr/send-sync-blocks-async.htm
new file mode 100644
index 0000000..74f08a5
--- /dev/null
+++ b/test/wpt/tests/xhr/send-sync-blocks-async.htm
@@ -0,0 +1,53 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <title>XMLHttpRequest: sync requests should block events on pending async requests</title>
+</head>
+
+<body>
+ <div id="log"></div>
+
+ <script type="text/javascript">
+ var test = async_test();
+
+ var expect = ['sync 4', 'async 2', 'async 3', 'async 4']
+ var actual = []
+
+ test.step(function()
+ {
+ var xhr_async = new XMLHttpRequest()
+ xhr_async.open('GET', 'resources/delay.py?ms=1000', true) // first launch an async request, completes in 1 second
+ xhr_async.onreadystatechange = test.step_func(() => {
+ actual.push('async ' + xhr_async.readyState)
+ if(xhr_async.readyState === 4 && actual.indexOf('sync 4')>-1){
+ VerifyResult()
+ }
+ });
+ xhr_async.send()
+
+ test.step_timeout(() => {
+ var xhr_sync = new XMLHttpRequest();
+ xhr_sync.open('GET', 'resources/delay.py?ms=2000', false) // here's a sync request that will take 2 seconds to finish
+ xhr_sync.onreadystatechange = test.step_func(() => {
+ actual.push('sync ' + xhr_sync.readyState)
+ if(xhr_sync.readyState === 4 && actual.indexOf('async 4')>-1){
+ VerifyResult()
+ }
+ });
+ xhr_sync.send()
+ }, 10);
+
+ function VerifyResult()
+ {
+ test.step(function()
+ {
+ assert_array_equals(actual, expect);
+ test.done();
+ });
+ };
+ });
+ </script>
+</body>
+</html>
diff --git a/test/wpt/tests/xhr/send-sync-no-response-event-load.htm b/test/wpt/tests/xhr/send-sync-no-response-event-load.htm
new file mode 100644
index 0000000..a2a5516
--- /dev/null
+++ b/test/wpt/tests/xhr/send-sync-no-response-event-load.htm
@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#handler-xhr-onload" data-tested-assertations="../.." />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#event-xhr-load" data-tested-assertations="../.." />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#infrastructure-for-the-send()-method" data-tested-assertations="following::dt[11] following::a[contains(@href,'#switch-done')]/.." />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#switch-done" data-tested-assertations="following::ol/li[1] following::ol/li[6]" />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-response-attribute" data-tested-assertations="/following::ol/li[3]" />
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <title>XMLHttpRequest: The send() method: Fire an event named load (no response entity body and the synchronous flag is set)</title>
+</head>
+
+<body>
+ <div id="log"></div>
+
+ <script type="text/javascript">
+ test(function()
+ {
+ var xhr = new XMLHttpRequest();
+ var pass = false;
+
+ xhr.onload = function(e)
+ {
+ assert_true(e instanceof ProgressEvent);
+ assert_equals(e.type, "load");
+ pass = true;
+ };
+
+ xhr.open("POST", "./resources/content.py", false);
+ xhr.send();
+
+ assert_equals(xhr.response, "");
+ assert_true(pass);
+ });
+ </script>
+</body>
+</html>
diff --git a/test/wpt/tests/xhr/send-sync-no-response-event-loadend.htm b/test/wpt/tests/xhr/send-sync-no-response-event-loadend.htm
new file mode 100644
index 0000000..7da2a31
--- /dev/null
+++ b/test/wpt/tests/xhr/send-sync-no-response-event-loadend.htm
@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#handler-xhr-onloadend" data-tested-assertations="../.." />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#event-xhr-loadend" data-tested-assertations="../.." />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#infrastructure-for-the-send()-method" data-tested-assertations="following::dt[11] following::a[contains(@href,'#switch-done')]/.." />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#switch-done" data-tested-assertations="following::ol/li[1] following::ol/li[7]" />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-response-attribute" data-tested-assertations="/following::ol/li[3]" />
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <title>XMLHttpRequest: The send() method: Fire an event named loadend (no response entity body and the synchronous flag is set)</title>
+</head>
+
+<body>
+ <div id="log"></div>
+
+ <script type="text/javascript">
+ test(function()
+ {
+ var xhr = new XMLHttpRequest();
+ var pass = false;
+
+ xhr.onloadend = function(e)
+ {
+ assert_true(e instanceof ProgressEvent);
+ assert_equals(e.type, "loadend");
+ pass = true;
+ };
+
+ xhr.open("POST", "./resources/content.py", false);
+ xhr.send();
+
+ assert_equals(xhr.response, "");
+ assert_true(pass);
+ });
+ </script>
+</body>
+</html>
diff --git a/test/wpt/tests/xhr/send-sync-no-response-event-order.htm b/test/wpt/tests/xhr/send-sync-no-response-event-order.htm
new file mode 100644
index 0000000..c7e3172
--- /dev/null
+++ b/test/wpt/tests/xhr/send-sync-no-response-event-order.htm
@@ -0,0 +1,51 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>XMLHttpRequest: The send() method: event order when synchronous flag is set and there is no response entity body</title>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#handler-xhr-onloadstart" data-tested-assertations="../.." />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#handler-xhr-onloadend" data-tested-assertations="../.." />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#event-xhr-loadstart" data-tested-assertations="../.." />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#event-xhr-loadend" data-tested-assertations="../.." />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-send()-method" data-tested-assertations="following-sibling::ol[1]/li[9]" />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#same-origin-request-steps" data-tested-assertations="following::DL[1]/DT[1]"/>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#infrastructure-for-the-send()-method" data-tested-assertations="following::dt[11] following::a[contains(@href,'#switch-done')]/.." />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#switch-done" data-tested-assertations="following::ol[1]/li[1] following::ol[1]/li[3] following::ol[1]/li[4] following::ol[1]/li[6] following::ol[1]/li[7]" />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-response-attribute" data-tested-assertations="following::ol/li[3]" />
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+</head>
+
+<body>
+ <div id="log"></div>
+
+ <script type="text/javascript">
+ test(function () {
+ var xhr = new XMLHttpRequest();
+ var expect = [4, "load", "loadend"];
+ var actual = [];
+
+ xhr.onreadystatechange = function()
+ {
+ if (xhr.readyState == 4)
+ {
+ actual.push(xhr.readyState);
+ }
+ };
+
+ xhr.onloadstart = function(e){ actual.push(e.type); };
+ xhr.onload = function(e){ actual.push(e.type); };
+ xhr.onloadend = function(e){ actual.push(e.type); };
+
+ xhr.upload.onload = function(e){ actual.push("upload." + e.type); };
+ xhr.upload.onloadstart = function(e){ actual.push("upload." + e.type); };
+ xhr.upload.onloadend = function(e){ actual.push("upload." + e.type);};
+
+ xhr.open("POST", "./resources/content.py", false);
+ xhr.send();
+
+ assert_equals(xhr.response, "");
+ assert_array_equals(actual, expect);
+ });
+ </script>
+</body>
+</html>
diff --git a/test/wpt/tests/xhr/send-sync-response-event-order.htm b/test/wpt/tests/xhr/send-sync-response-event-order.htm
new file mode 100644
index 0000000..f7e4b0b
--- /dev/null
+++ b/test/wpt/tests/xhr/send-sync-response-event-order.htm
@@ -0,0 +1,35 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="resources/xmlhttprequest-event-order.js"></script>
+ <title>XMLHttpRequest: The send() method: event order when synchronous flag is set</title>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#handler-xhr-onloadstart" data-tested-assertations="../.." />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#handler-xhr-onloadend" data-tested-assertations="../.." />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#event-xhr-loadstart" data-tested-assertations="../.." />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#event-xhr-loadend" data-tested-assertations="../.." />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-send()-method" data-tested-assertations="following-sibling::ol/li[9]" />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#same-origin-request-steps" data-tested-assertations="following::DL[1]/DT[1]"/>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#infrastructure-for-the-send()-method" data-tested-assertations="following::dt[11] following::a[contains(@href,'#switch-done')]/.." />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#switch-done" data-tested-assertations="following::ol[1]/li[1] following::ol[1]/li[3] following::ol[1]/li[4] following::ol[1]/li[6] following::ol[1]/li[7]" />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-response-attribute" data-tested-assertations="following::ol/li[3]" />
+</head>
+
+<body>
+ <div id="log"></div>
+
+ <script type="text/javascript">
+ test(function () {
+ var xhr = new XMLHttpRequest();
+ prepare_xhr_for_event_order_test(xhr);
+
+ xhr.open("POST", "./resources/content.py", false);
+ xhr.send("Test Message");
+
+ assert_equals(xhr.response, "Test Message");
+ assert_xhr_event_order_matches([1, 4, "load(12,12,true)", "loadend(12,12,true)"]);
+ });
+ </script>
+</body>
+</html>
diff --git a/test/wpt/tests/xhr/send-sync-timeout.htm b/test/wpt/tests/xhr/send-sync-timeout.htm
new file mode 100644
index 0000000..46d8686
--- /dev/null
+++ b/test/wpt/tests/xhr/send-sync-timeout.htm
@@ -0,0 +1,29 @@
+<!doctype html>
+<html>
+ <head>
+ <title>XMLHttpRequest: timeout during sync send() should not run</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-send()-method"/>
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ var test = async_test(),
+ hasrun = false
+ test.step(function() {
+ client = new XMLHttpRequest()
+ client.open("GET", "folder.txt", false)
+ test.step_timeout(() => { hasrun = true }, 0)
+ client.onreadystatechange = function() {
+ test.step(function() {
+ assert_equals(client.readyState, 4)
+ assert_false(hasrun)
+ })
+ }
+ client.send(null)
+ test.done()
+ })
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/send-timeout-events.htm b/test/wpt/tests/xhr/send-timeout-events.htm
new file mode 100644
index 0000000..eae2568
--- /dev/null
+++ b/test/wpt/tests/xhr/send-timeout-events.htm
@@ -0,0 +1,62 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <title>XMLHttpRequest: The send() method: timeout is not 0 </title>
+</head>
+
+<body>
+ <div id="log"></div>
+
+ <script type="text/javascript">
+ async_test(t => {
+ const xhr = new XMLHttpRequest(),
+ expect = [4, "", "upload.timeout", "upload.loadend", "timeout", "loadend"];
+ let actual = [];
+
+ xhr.onreadystatechange = t.step_func(() => {
+ if (xhr.readyState == 4) {
+ actual.push(xhr.readyState, xhr.response);
+ }
+ });
+
+ xhr.onloadend = t.step_func_done(e => {
+ assert_equals(e.loaded, 0);
+ assert_equals(e.total, 0);
+ actual.push(e.type);
+ assert_array_equals(actual, expect);
+ });
+
+ xhr.ontimeout = t.step_func(e => {
+ assert_equals(e.loaded, 0);
+ assert_equals(e.total, 0);
+ actual.push(e.type);
+ });
+
+
+ xhr.upload.onloadend = t.step_func(e => {
+ assert_equals(e.loaded, 0);
+ assert_equals(e.total, 0);
+ actual.push("upload." + e.type);
+ });
+
+ xhr.upload.ontimeout = t.step_func(e => {
+ assert_equals(e.loaded, 0);
+ assert_equals(e.total, 0);
+ actual.push("upload." + e.type);
+ });
+
+
+ let content = "";
+ for (var i = 0; i < 121026; i++) {
+ content += "[" + i + "]";
+ }
+
+ xhr.open("POST", "./resources/trickle.py", true);
+ xhr.timeout = 1;
+ xhr.send(content);
+ });
+ </script>
+</body>
+</html>
diff --git a/test/wpt/tests/xhr/send-usp.any.js b/test/wpt/tests/xhr/send-usp.any.js
new file mode 100644
index 0000000..b0baf4a
--- /dev/null
+++ b/test/wpt/tests/xhr/send-usp.any.js
@@ -0,0 +1,46 @@
+const NUM_TESTS = 128;
+
+function encode(n) {
+ if (n === 0x20) {
+ return "\x2B";
+ }
+
+ if (n === 0x2A || n === 0x2D || n === 0x2E ||
+ (0x30 <= n && n <= 0x39) || (0x41 <= n && n <= 0x5A) ||
+ n === 0x5F || (0x61 <= n && n <= 0x7A)) {
+ return String.fromCharCode(n);
+ }
+
+ var s = n.toString(16).toUpperCase();
+ return "%" + (s.length === 2 ? s : '0' + s);
+}
+
+ var tests = [];
+ var overall_test = async_test("Overall fetch with URLSearchParams");
+ for (var i = 0; i < NUM_TESTS; i++) {
+ // Multiple subtests so that failures can be fine-grained
+ tests[i] = async_test("XMLHttpRequest.send(URLSearchParams) (" + i + ")");
+ }
+
+ // We use a single XHR since this test tends to time out
+ // with 128 consecutive fetches when run in parallel
+ // with many other WPT tests.
+ var x = new XMLHttpRequest();
+ x.onload = overall_test.step_func(function() {
+ var response_split = x.response.split("&");
+ overall_test.done();
+ for (var i = 0; i < NUM_TESTS; i++) {
+ tests[i].step(function() {
+ assert_equals(response_split[i], "a" + i + "="+encode(i));
+ tests[i].done();
+ });
+ }
+ });
+ x.onerror = overall_test.unreached_func();
+
+ x.open("POST", "resources/content.py");
+ var usp = new URLSearchParams();
+ for (var i = 0; i < NUM_TESTS; i++) {
+ usp.append("a" + i, String.fromCharCode(i));
+ }
+ x.send(usp)
diff --git a/test/wpt/tests/xhr/setrequestheader-after-send.htm b/test/wpt/tests/xhr/setrequestheader-after-send.htm
new file mode 100644
index 0000000..174e9ec
--- /dev/null
+++ b/test/wpt/tests/xhr/setrequestheader-after-send.htm
@@ -0,0 +1,27 @@
+<!doctype html>
+<html>
+ <head>
+ <title>XMLHttpRequest: setRequestHeader() after send()</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-setrequestheader()-method" data-tested-assertations="/following::ol/li[2]" />
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ var test = async_test()
+ test.step(function() {
+ var client = new XMLHttpRequest()
+ client.open("GET", "resources/delay.py?ms=0")
+ client.onreadystatechange = function() {
+ test.step(function() {
+ assert_throws_dom("InvalidStateError", function() { client.setRequestHeader("x-test", "test") })
+ if(client.readyState == 4)
+ test.done()
+ })
+ }
+ client.send(null)
+ })
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/setrequestheader-allow-empty-value.htm b/test/wpt/tests/xhr/setrequestheader-allow-empty-value.htm
new file mode 100644
index 0000000..4479504
--- /dev/null
+++ b/test/wpt/tests/xhr/setrequestheader-allow-empty-value.htm
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>XMLHttpRequest: setRequestHeader() - empty header</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-setrequestheader()-method">
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ function request(value) {
+ test(function() {
+ var client = new XMLHttpRequest()
+ client.open("POST", "resources/inspect-headers.py?filter_name=X-Empty", false)
+ client.setRequestHeader('X-Empty', value)
+ client.send(null)
+ assert_equals(client.responseText, 'X-Empty: ' + value + '\n' )
+ }, document.title + " (" + value + ")")
+ }
+ request("")
+ request(null)
+ request(undefined)
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/setrequestheader-allow-whitespace-in-value.htm b/test/wpt/tests/xhr/setrequestheader-allow-whitespace-in-value.htm
new file mode 100644
index 0000000..f2e0a37
--- /dev/null
+++ b/test/wpt/tests/xhr/setrequestheader-allow-whitespace-in-value.htm
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>XMLHttpRequest: setRequestHeader() - header value with whitespace</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-setrequestheader()-method">
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ function request(value) {
+ test(function() {
+ var client = new XMLHttpRequest()
+ client.open("POST", "resources/inspect-headers.py?filter_name=X-Empty", false)
+ client.setRequestHeader('X-Empty', value)
+ client.send(null)
+ assert_equals(client.responseText, 'X-Empty: ' + value.trim() + '\n' )
+ }, document.title + " (" + value + ")")
+ }
+ request(" ")
+ request(" t")
+ request("t ")
+ request(" t ")
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/setrequestheader-before-open.htm b/test/wpt/tests/xhr/setrequestheader-before-open.htm
new file mode 100644
index 0000000..5c377fb
--- /dev/null
+++ b/test/wpt/tests/xhr/setrequestheader-before-open.htm
@@ -0,0 +1,18 @@
+<!doctype html>
+<html>
+ <head>
+ <title>XMLHttpRequest: setRequestHeader() before open()</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-setrequestheader()-method" data-tested-assertations="following::ol/li[1]" />
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ test(function() {
+ var client = new XMLHttpRequest()
+ assert_throws_dom("InvalidStateError", function() { client.setRequestHeader("x-test", "test") })
+ }, 'setRequestHeader invoked before open()')
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/setrequestheader-bogus-name.htm b/test/wpt/tests/xhr/setrequestheader-bogus-name.htm
new file mode 100644
index 0000000..ce2308c
--- /dev/null
+++ b/test/wpt/tests/xhr/setrequestheader-bogus-name.htm
@@ -0,0 +1,59 @@
+<!doctype html>
+<html>
+ <head>
+ <title>XMLHttpRequest: setRequestHeader() name argument checks</title>
+ <meta charset="utf-8">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-setrequestheader()-method" data-tested-assertations="/following::ol/li[3]" />
+ </head>
+ <body>
+ <div id="log"></div>
+<!--
+ CHAR = <any US-ASCII character (octets 0 - 127)>
+ CTL = <any US-ASCII control character
+ (octets 0 - 31) and DEL (127)>
+ SP = <US-ASCII SP, space (32)>
+ HT = <US-ASCII HT, horizontal-tab (9)>
+ token = 1*<any CHAR except CTLs or separators>
+ separators = "(" | ")" | "<" | ">" | "@"
+ | "," | ";" | ":" | "\" | <">
+ | "/" | "[" | "]" | "?" | "="
+ | "{" | "}" | SP | HT
+ field-name = token
+-->
+ <script>
+ function try_name(name) {
+ test(function() {
+ var client = new XMLHttpRequest()
+ client.open("GET", "...")
+ assert_throws_dom("SyntaxError", function() { client.setRequestHeader(name, 'x-value') })
+ }, "setRequestHeader should throw with header name " + format_value(invalid_headers[i]) + ".")
+ }
+ function try_byte_string(name) {
+ test(function() {
+ var client = new XMLHttpRequest()
+ client.open("GET", "...")
+ assert_throws_js(TypeError, function() { client.setRequestHeader(name, 'x-value') })
+ }, "setRequestHeader should throw with header name " + format_value(invalid_byte_strings[i]) + ".")
+ }
+ var invalid_headers = ["(", ")", "<", ">", "@", ",", ";", ":", "\\",
+ "\"", "/", "[", "]", "?", "=", "{", "}", " ",
+ /* HT already tested in the loop below */
+ "\u007f", "", "t\rt", "t\nt", "t: t", "t:t",
+ "t<t", "t t", " tt", ":tt", "\ttt", "\vtt", "t\0t",
+ "t\"t", "t,t", "t;t", "()[]{}", "a?B", "a=B"]
+ var invalid_byte_strings = ["テスト", "X-テスト"]
+ for (var i = 0; i < 32; ++i) {
+ invalid_headers.push(String.fromCharCode(i))
+ }
+ for (var i = 0; i < invalid_headers.length; ++i) {
+ try_name(invalid_headers[i])
+ }
+ for (var i = 0; i < invalid_byte_strings.length; ++i) {
+ try_byte_string(invalid_byte_strings[i])
+ }
+
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/setrequestheader-bogus-value.htm b/test/wpt/tests/xhr/setrequestheader-bogus-value.htm
new file mode 100644
index 0000000..cba341c
--- /dev/null
+++ b/test/wpt/tests/xhr/setrequestheader-bogus-value.htm
@@ -0,0 +1,36 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>XMLHttpRequest: setRequestHeader() value argument checks</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-setrequestheader()-method" data-tested-assertations="/following::ol/li[4]" />
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ function try_value(value) {
+ test(function() {
+ var client = new XMLHttpRequest();
+ client.open("GET", "...");
+ assert_throws_dom("SyntaxError", function() { client.setRequestHeader("x-test", value) }, ' given value ' + value+', ');
+ });
+ }
+ try_value("t\x00t");
+ try_value("t\rt");
+ try_value("t\nt");
+ test(function() {
+ var client = new XMLHttpRequest();
+ client.open("GET", "...");
+ assert_throws_js(TypeError, function() { client.setRequestHeader("x-test", "テスト") }, ' given value テスト,');
+ });
+
+ test(function() {
+ var client = new XMLHttpRequest()
+ client.open("GET", "...")
+ assert_throws_js(TypeError, function() { client.setRequestHeader("x-test") })
+ }, 'Omitted value argument')
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/setrequestheader-case-insensitive.htm b/test/wpt/tests/xhr/setrequestheader-case-insensitive.htm
new file mode 100644
index 0000000..1aed30d
--- /dev/null
+++ b/test/wpt/tests/xhr/setrequestheader-case-insensitive.htm
@@ -0,0 +1,34 @@
+<!doctype html>
+<html>
+ <head>
+ <title>XMLHttpRequest: setRequestHeader() - headers that differ in case</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-setrequestheader()-method">
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ test(function() {
+ var client = new XMLHttpRequest()
+ client.open("POST", "resources/inspect-headers.py?filter_value=t1, t2, t3", false)
+ client.setRequestHeader("x-test", "t1")
+ client.setRequestHeader("X-TEST", "t2")
+ client.setRequestHeader("X-teST", "t3")
+ client.send(null)
+ assert_equals(client.responseText, "x-test,")
+ })
+
+ test(() => {
+ const client = new XMLHttpRequest
+ client.open("GET", "resources/echo-headers.py", false)
+ client.setRequestHeader("THIS-IS-A-TEST", "1")
+ client.setRequestHeader("THIS-is-A-test", "2")
+ client.setRequestHeader("content-TYPE", "x/x")
+ client.send()
+ assert_regexp_match(client.responseText, /content-TYPE/)
+ assert_regexp_match(client.responseText, /THIS-IS-A-TEST: 1, 2/)
+ })
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/setrequestheader-combining.window.js b/test/wpt/tests/xhr/setrequestheader-combining.window.js
new file mode 100644
index 0000000..fc847eb
--- /dev/null
+++ b/test/wpt/tests/xhr/setrequestheader-combining.window.js
@@ -0,0 +1,12 @@
+test(() => {
+ const client = new XMLHttpRequest();
+ client.open("POST", "resources/inspect-headers.py?filter_name=test-me", false);
+ client.setRequestHeader("test-me", "");
+ client.setRequestHeader("test-me", "");
+ client.setRequestHeader("test-me", " ");
+ client.setRequestHeader("test-me", "\t");
+ client.setRequestHeader("test-me", "x\tx");
+ client.setRequestHeader("test-me", "");
+ client.send();
+ assert_equals(client.responseText, "test-me: , , , , x\tx, \n");
+}, "setRequestHeader() combining header values");
diff --git a/test/wpt/tests/xhr/setrequestheader-content-type.htm b/test/wpt/tests/xhr/setrequestheader-content-type.htm
new file mode 100644
index 0000000..0723839
--- /dev/null
+++ b/test/wpt/tests/xhr/setrequestheader-content-type.htm
@@ -0,0 +1,220 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>XMLHttpRequest: setRequestHeader() - Content-Type header</title>
+ <meta name="timeout" content="long">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-setrequestheader()-method">
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ function request(inputGenerator, headersToSend, expectedType, title) {
+ test(function() {
+ const toSend = inputGenerator(),
+ client = new XMLHttpRequest()
+ client.open("POST", "resources/inspect-headers.py?filter_name=Content-Type", false)
+ for(header in headersToSend) {
+ if (headersToSend.hasOwnProperty(header)) {
+ client.setRequestHeader(header, headersToSend[header]);
+ }
+ }
+ client.send(toSend)
+
+ const actual = client.responseText
+ if (expectedType === undefined || expectedType === null) {
+ assert_equals(actual, "");
+ } else if (expectedType instanceof RegExp) {
+ assert_regexp_match(actual, expectedType);
+ } else {
+ assert_equals(actual, "Content-Type: " + expectedType + "\n");
+ }
+ }, title)
+ }
+ request(
+ function _String() { return ""; },
+ {"Content-Type": ""},
+ "",
+ 'setRequestHeader("") sends a blank string'
+ )
+ request(
+ function _String() { return ""; },
+ {"Content-Type": " "},
+ "",
+ 'setRequestHeader(" ") sends the string " "'
+ )
+ request(
+ function _String() { return ""; },
+ {"Content-Type": null},
+ "null",
+ 'setRequestHeader(null) sends the string "null"'
+ )
+ request(
+ function _String() { return ""; },
+ {"Content-Type": undefined},
+ "undefined",
+ 'setRequestHeader(undefined) sends the string "undefined"'
+ )
+ request(
+ function _String() { return "test"; },
+ {},
+ "text/plain;charset=UTF-8",
+ 'String request has correct default Content-Type of "text/plain;charset=UTF-8"'
+ )
+ request(
+ function _String() { return "test()"; },
+ {"Content-Type": "text/javascript;charset=ASCII"},
+ "text/javascript;charset=UTF-8",
+ "String request keeps setRequestHeader() Content-Type, with charset adjusted to UTF-8"
+ )
+ request(
+ function _XMLDocument() { return new DOMParser().parseFromString("<xml/>", "application/xml"); },
+ {"Content-Type": ""},
+ "",
+ 'XML Document request respects setRequestHeader("")'
+ )
+ request(
+ function _XMLDocument() { return new DOMParser().parseFromString("<xml/>", "application/xml"); },
+ {},
+ "application/xml;charset=UTF-8",
+ 'XML Document request has correct default Content-Type of "application/xml;charset=UTF-8"'
+ )
+ request(
+ function _XMLDocument() { return new DOMParser().parseFromString("<xml/>", "application/xml"); },
+ {"Content-Type": "application/xhtml+xml;charset=ASCII"},
+ "application/xhtml+xml;charset=UTF-8",
+ "XML Document request keeps setRequestHeader() Content-Type, with charset adjusted to UTF-8"
+ )
+ request(
+ function _HTMLDocument() { return new DOMParser().parseFromString("<html></html>", "text/html"); },
+ {"Content-Type": ""},
+ "",
+ 'HTML Document request respects setRequestHeader("")'
+ )
+ request(
+ function _HTMLDocument() { return new DOMParser().parseFromString("<html></html>", "text/html"); },
+ {},
+ "text/html;charset=UTF-8",
+ 'HTML Document request has correct default Content-Type of "text/html;charset=UTF-8"'
+ )
+ request(
+ function _HTMLDocument() { return new DOMParser().parseFromString("<html></html>", "text/html"); },
+ {"Content-Type": "text/html+junk;charset=ASCII"},
+ "text/html+junk;charset=UTF-8",
+ "HTML Document request keeps setRequestHeader() Content-Type, with charset adjusted to UTF-8"
+ )
+ request(
+ function _Blob() { return new Blob(["test"]); },
+ {"Content-Type": ""},
+ "",
+ 'Blob request respects setRequestHeader("") to be specified'
+ )
+ request(
+ function _Blob() { return new Blob(["test"]); },
+ {},
+ undefined,
+ "Blob request with unset type sends no Content-Type without setRequestHeader() call"
+ )
+ request(
+ function _Blob() { return new Blob(["test"]); },
+ {"Content-Type": "application/xml;charset=ASCII"},
+ "application/xml;charset=ASCII",
+ "Blob request with unset type keeps setRequestHeader() Content-Type and charset"
+ )
+ request(
+ function _Blob() { return new Blob(["<xml/>"], {type : "application/xml;charset=ASCII"}); },
+ {"Content-Type": ""},
+ "",
+ 'Blob request with set type respects setRequestHeader("") to be specified'
+ )
+ request(
+ function _Blob() { return new Blob(["<xml/>"], {type : "application/xml;charset=ASCII"}); },
+ {},
+ "application/xml;charset=ascii", // new Blob lowercases the type argument
+ "Blob request with set type uses that it for Content-Type unless setRequestHeader()"
+ )
+ request(
+ function _Blob() { return new Blob(["<xml/>"], {type : "application/xml;charset=UTF8"}); },
+ {"Content-Type": "application/xml+junk;charset=ASCII"},
+ "application/xml+junk;charset=ASCII",
+ "Blob request with set type keeps setRequestHeader() Content-Type and charset"
+ )
+ request(
+ function _ArrayBuffer() { return new ArrayBuffer(10); },
+ {"Content-Type": ""},
+ "",
+ 'ArrayBuffer request respects setRequestHeader("")'
+ )
+ request(
+ function _ArrayBuffer() { return new ArrayBuffer(10); },
+ {},
+ undefined,
+ "ArrayBuffer request sends no Content-Type without setRequestHeader() call"
+ )
+ request(
+ function _ArrayBuffer() { return new ArrayBuffer(10); },
+ {"Content-Type": "application/xml;charset=ASCII"},
+ "application/xml;charset=ASCII",
+ "ArrayBuffer request keeps setRequestHeader() Content-Type and charset"
+ )
+ request(
+ function _Uint8Array() { return new Uint8Array(new ArrayBuffer(10)); },
+ {"Content-Type": ""},
+ "",
+ 'ArrayBufferView request respects setRequestHeader("")'
+ )
+ request(
+ function _Uint8Array() { return new Uint8Array(new ArrayBuffer(10)); },
+ {},
+ undefined,
+ "ArrayBufferView request sends no Content-Type without setRequestHeader() call"
+ )
+ request(
+ function _Uint8Array() { return new Uint8Array(new ArrayBuffer(10)); },
+ {"Content-Type": "application/xml;charset=ASCII"},
+ "application/xml;charset=ASCII",
+ "ArrayBufferView request keeps setRequestHeader() Content-Type and charset"
+ )
+ request(
+ function _FormData() { return new FormData(); },
+ {"Content-Type": ""},
+ "",
+ 'FormData request respects setRequestHeader("")'
+ )
+ request(
+ function _FormData() { return new FormData(); },
+ {},
+ /multipart\/form-data; boundary=(.*)/,
+ 'FormData request has correct default Content-Type of "multipart\/form-data; boundary=_"'
+ )
+ request(
+ function _FormData() { return new FormData(); },
+ {"Content-Type": "application/xml;charset=ASCII"},
+ "application/xml;charset=ASCII",
+ "FormData request keeps setRequestHeader() Content-Type and charset"
+ )
+ request(
+ function _URLSearchParams() { return new URLSearchParams("q=testQ&topic=testTopic") },
+ {"Content-Type": ""},
+ "",
+ 'URLSearchParams respects setRequestHeader("")'
+ )
+ request(
+ function _URLSearchParams() { return new URLSearchParams("q=testQ&topic=testTopic") },
+ {},
+ "application/x-www-form-urlencoded;charset=UTF-8",
+ 'URLSearchParams request has correct default Content-Type of "application/x-www-form-urlencoded;charset=UTF-8"'
+ )
+ request(
+ function _URLSearchParams() { return new URLSearchParams("q=testQ&topic=testTopic") },
+ {"Content-Type": "application/xml;charset=ASCII"},
+ "application/xml;charset=UTF-8",
+ "URLSearchParams request keeps setRequestHeader() Content-Type, with charset adjusted to UTF-8"
+ // the default Content-Type for URLSearchParams has a charset specified (utf-8) in
+ // https://fetch.spec.whatwg.org/#bodyinit, so the user's must be changed to match it
+ // as per https://xhr.spec.whatwg.org/#the-send%28%29-method step 4.
+ )
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/setrequestheader-header-allowed.htm b/test/wpt/tests/xhr/setrequestheader-header-allowed.htm
new file mode 100644
index 0000000..14b7187
--- /dev/null
+++ b/test/wpt/tests/xhr/setrequestheader-header-allowed.htm
@@ -0,0 +1,34 @@
+<!doctype html>
+<html>
+ <head>
+ <title>XMLHttpRequest: setRequestHeader() - headers that are allowed</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-setrequestheader()-method">
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ function request(header) {
+ test(function() {
+ var client = new XMLHttpRequest()
+ client.open("POST", "resources/inspect-headers.py?filter_value=t1, t2", false)
+ client.setRequestHeader(header, "t1")
+ client.setRequestHeader(header, "t2")
+ client.send(null)
+ assert_equals(client.responseText, header + ",")
+ }, document.title + " (" + header + ")")
+ }
+ request("Authorization")
+ request("Pragma")
+ request("User-Agent")
+ request("Content-Transfer-Encoding")
+ request("Content-Type")
+ request("Overwrite")
+ request("If")
+ request("Status-URI")
+ request("X-Pink-Unicorn")
+ request("!#$%&'*+-.^_`|~0123456789abcdefghijklmnopqrstuvwxyz")
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/setrequestheader-header-forbidden.htm b/test/wpt/tests/xhr/setrequestheader-header-forbidden.htm
new file mode 100644
index 0000000..0b27377
--- /dev/null
+++ b/test/wpt/tests/xhr/setrequestheader-header-forbidden.htm
@@ -0,0 +1,95 @@
+<!doctype html>
+<html>
+ <head>
+ <title>XMLHttpRequest: setRequestHeader() - headers that are forbidden</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-setrequestheader()-method">
+
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ test(function() {
+ var client = new XMLHttpRequest()
+ client.open("POST", "resources/inspect-headers.py?filter_value=TEST", false)
+ client.setRequestHeader("Accept-Charset", "TEST")
+ client.setRequestHeader("Accept-Encoding", "TEST")
+ client.setRequestHeader("Connection", "TEST")
+ client.setRequestHeader("Content-Length", "TEST")
+ client.setRequestHeader("Cookie", "TEST")
+ client.setRequestHeader("Cookie2", "TEST")
+ client.setRequestHeader("Date", "TEST")
+ client.setRequestHeader("DNT", "TEST")
+ client.setRequestHeader("Expect", "TEST")
+ client.setRequestHeader("Host", "TEST")
+ client.setRequestHeader("Keep-Alive", "TEST")
+ client.setRequestHeader("Referer", "TEST")
+ client.setRequestHeader("TE", "TEST")
+ client.setRequestHeader("Trailer", "TEST")
+ client.setRequestHeader("Transfer-Encoding", "TEST")
+ client.setRequestHeader("Upgrade", "TEST")
+ client.setRequestHeader("Via", "TEST")
+ client.setRequestHeader("Proxy-", "TEST")
+ client.setRequestHeader("Proxy-LIES", "TEST")
+ client.setRequestHeader("Proxy-Authorization", "TEST")
+ client.setRequestHeader("Sec-", "TEST")
+ client.setRequestHeader("Sec-X", "TEST")
+ client.send(null)
+ assert_equals(client.responseText, "")
+ })
+
+ test (function() {
+
+ let forbiddenMethods = [
+ "TRACE",
+ "TRACK",
+ "CONNECT",
+ "trace",
+ "track",
+ "connect",
+ "trace,",
+ "GET,track ",
+ " connect",
+ ];
+
+ let overrideHeaders = [
+ "x-http-method-override",
+ "x-http-method",
+ "x-method-override",
+ "X-HTTP-METHOD-OVERRIDE",
+ "X-HTTP-METHOD",
+ "X-METHOD-OVERRIDE",
+ ];
+
+ for (forbiddenMethod of forbiddenMethods) {
+ for (overrideHeader of overrideHeaders) {
+ var client = new XMLHttpRequest()
+ client.open("POST",
+ `resources/inspect-headers.py?filter_value=${forbiddenMethod}`, false)
+ client.setRequestHeader(overrideHeader, forbiddenMethod)
+ client.send(null)
+ assert_equals(client.responseText, "")
+ }
+ }
+
+ let permittedValues = [
+ "GETTRACE",
+ "GET",
+ "\",TRACE\",",
+ ];
+
+ for (permittedValue of permittedValues) {
+ for (overrideHeader of overrideHeaders) {
+ var client = new XMLHttpRequest()
+ client.open("POST",
+ `resources/inspect-headers.py?filter_name=${overrideHeader}`, false)
+ client.setRequestHeader(overrideHeader, permittedValue)
+ client.send(null)
+ assert_equals(client.responseText, overrideHeader + ": " + permittedValue + "\n")
+ }
+ }
+ })
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/setrequestheader-open-setrequestheader.htm b/test/wpt/tests/xhr/setrequestheader-open-setrequestheader.htm
new file mode 100644
index 0000000..d77d34f
--- /dev/null
+++ b/test/wpt/tests/xhr/setrequestheader-open-setrequestheader.htm
@@ -0,0 +1,53 @@
+<!DOCTYPE html>
+<!--
+Test from https://bugzilla.mozilla.org/show_bug.cgi?id=819051
+-->
+<head>
+ <title>XMLHttpRequest: setRequestHeader() and open()</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-open()-method">
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-setrequestheader()-method">
+</head>
+<body>
+ <p id="log"></p>
+<script type="text/javascript">
+async_test(test => {
+
+var url = "resources/inspect-headers.py";
+
+var xhr = new XMLHttpRequest();
+xhr.open("GET", url + "?filter_name=x-appended-to-this");
+xhr.setRequestHeader("X-appended-to-this", "False");
+xhr.open("GET", url + "?filter_name=x-appended-to-this");
+xhr.setRequestHeader("X-appended-to-this", "True");
+
+xhr.onreadystatechange = test.step_func(() => {
+ if (xhr.readyState == 4) {
+ assert_equals(xhr.responseText, "X-appended-to-this: True\n", "Set headers record should have been cleared by open.");
+ test_standard_header();
+ }
+})
+
+xhr.send();
+
+function test_standard_header () {
+ var header_tested = "Accept";
+ var xhr = new XMLHttpRequest();
+ xhr.open("GET", url + "?filter_name=accept");
+ xhr.setRequestHeader("Accept", "foo/bar");
+ xhr.open("GET", url + "?filter_name=accept");
+ xhr.setRequestHeader("Accept", "bar/foo");
+
+ xhr.onreadystatechange = test.step_func(() => {
+ if (xhr.readyState == 4) {
+ assert_equals(xhr.responseText, "Accept: bar/foo\n", "Set headers record should have been cleared by open.");
+ test.done();
+ }
+ })
+
+ xhr.send();
+}
+
+})
+</script>
diff --git a/test/wpt/tests/xhr/status-async.htm b/test/wpt/tests/xhr/status-async.htm
new file mode 100644
index 0000000..dcf7d62
--- /dev/null
+++ b/test/wpt/tests/xhr/status-async.htm
@@ -0,0 +1,62 @@
+<!doctype html>
+<html>
+ <head>
+ <title>XMLHttpRequest: status/statusText - various responses</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-status-attribute" data-tested-assertations="following::ol/li[1] following::ol/li[3]" />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-statustext-attribute" data-tested-assertations="following::ol/li[1] following::ol/li[3]" />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-getresponseheader()-method" data-tested-assertations="following::ol/li[5]" />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-responsetext-attribute" data-tested-assertations="following::ol/li[4]" />
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ var counter=0
+ function statusRequest(method, code, text, content, type) {
+ counter++
+ var test = async_test(document.title +' '+ counter+" (" + method + " " + code + ")")
+ test.step(function() {
+ var client = new XMLHttpRequest()
+ client.onreadystatechange = function(e) {
+ test.step(function() {
+ if(client.readyState > 1) {
+ assert_equals(client.status, code)
+ assert_equals(client.statusText, text)
+ assert_equals(client.getResponseHeader("X-Request-Method"), method)
+ if(client.readyState == 4) {
+ if(method != "HEAD") {
+ if(type == "text/xml") {
+ assert_equals(client.responseXML.documentElement.localName, "x")
+ }
+ assert_equals(client.responseText, content)
+ }
+ test.done()
+ }
+ }else{
+ assert_equals(client.status, 0)
+ assert_equals(client.statusText, "")
+ }
+ }, this)
+ }
+ client.open(method, "resources/status.py?code=" + encodeURIComponent(code) + "&text=" + text + "&content=" + encodeURIComponent(content) + "&type=" + encodeURIComponent(type))
+ client.send(null)
+ })
+ }
+ function status(code, text, content, type) {
+ statusRequest("GET", code, text, content, type)
+ statusRequest("HEAD", code, text, content, type)
+ statusRequest("CHICKEN", code, text, content, type)
+ }
+ status(204, "UNICORNSWIN", "", "")
+ status(401, "OH HELLO", "Not today.", "")
+ status(402, "FIVE BUCKS", "<x>402<\/x>", "text/xml")
+ status(402, "FREE", "Nice!", "text/doesnotmatter")
+ status(402, "402 TEH AWESOME", "", "")
+ status(502, "YO", "", "")
+ status(502, "lowercase", "SWEET POTATO", "text/plain")
+ status(503, "HOUSTON WE HAVE A", "503", "text/plain")
+ status(699, "WAY OUTTA RANGE", "699", "text/plain")
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/status-basic.htm b/test/wpt/tests/xhr/status-basic.htm
new file mode 100644
index 0000000..fed7cab
--- /dev/null
+++ b/test/wpt/tests/xhr/status-basic.htm
@@ -0,0 +1,51 @@
+<!doctype html>
+<html>
+ <head>
+ <title>XMLHttpRequest: status/statusText - various responses</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-status-attribute" data-tested-assertations="following::ol/li[3]" />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-statustext-attribute" data-tested-assertations="following::ol/li[3]" />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-getresponseheader()-method" data-tested-assertations="following::ol/li[5]" />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-responsetext-attribute" data-tested-assertations="following::ol/li[4]" />
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ var counter = 0
+ function statusRequest(method, code, text, content, type) {
+ counter++
+ test(function() {
+ var client = new XMLHttpRequest()
+ assert_equals(client.status, 0);
+ client.open(method, "resources/status.py?code=" + code + "&text=" + encodeURIComponent(text) + "&content=" + encodeURIComponent(content) + "&type=" + encodeURIComponent(type), false)
+ assert_equals(client.status, 0);
+ client.send(null)
+ assert_equals(client.status, code)
+ assert_equals(client.statusText, text)
+ assert_equals(client.getResponseHeader("X-Request-Method"), method)
+ if(method != "HEAD") {
+ if(type == "text/xml") {
+ assert_equals(client.responseXML.documentElement.localName, "x")
+ }
+ assert_equals(client.responseText, content)
+ }
+ }, document.title + " " + counter + " (" + method + " " + code + ")")
+ }
+ function status(code, text, content, type) {
+ statusRequest("GET", code, text, content, type)
+ statusRequest("HEAD", code, text, content, type)
+ statusRequest("CHICKEN", code, text, content, type)
+ }
+ status(204, "UNICORNSWIN", "", "")
+ status(401, "OH HELLO", "Not today.", "")
+ status(402, "FIVE BUCKS", "<x>402<\/x>", "text/xml")
+ status(402, "FREE", "Nice!", "text/doesnotmatter")
+ status(402, "402 TEH AWESOME", "", "")
+ status(502, "YO", "", "")
+ status(502, "lowercase", "SWEET POTATO", "text/plain")
+ status(503, "HOUSTON WE HAVE A", "503", "text/plain")
+ status(699, "WAY OUTTA RANGE", "699", "text/plain")
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/status-error.htm b/test/wpt/tests/xhr/status-error.htm
new file mode 100644
index 0000000..76709c2
--- /dev/null
+++ b/test/wpt/tests/xhr/status-error.htm
@@ -0,0 +1,87 @@
+<!doctype html>
+<html>
+ <head>
+ <title>XMLHttpRequest: status error handling</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#handler-xhr-onerror" data-tested-assertations="../.." />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-status-attribute" data-tested-assertations="/following::ol/li[3]" />
+ </head>
+ <body>
+ <p>This shouldn't be tested inside a tunnel.</p>
+ <div id="log"></div>
+ <script>
+ function noError(method, code) {
+ var test = async_test(document.title + " " + method + " " + code)
+
+ test.step(function() {
+ var client = new XMLHttpRequest()
+ client.open(method, "resources/status.py?code=" + code, true)
+
+ client.onreadystatechange = test.step_func(function() {
+ assert_equals(client.response, "", "response data")
+ assert_equals(client.status, code, "response status")
+
+ if (client.readyState == client.DONE)
+ /* Give extra time for a bogus error event to pop up */
+ test.step_timeout(() => { test.done() }, 100)
+ })
+ client.onerror = test.step_func(function() {
+ assert_unreached("HTTP error should not throw error event")
+ })
+ client.send()
+ })
+ }
+
+ function unknownScheme() {
+ test(() => {
+ var client = new XMLHttpRequest();
+ client.open("GET", "foobar://dummy", false);
+ try {
+ client.send();
+ } catch(ex) {}
+ assert_equals(client.status, 0, "response data");
+ }, "Unknown scheme");
+ }
+
+
+ function postOnBlob() {
+ test(() => {
+ var u = URL.createObjectURL(new Blob([""], {type: 'text/plain'}));
+ var client = new XMLHttpRequest();
+ client.open("POST", u, false);
+ try {
+ client.send();
+ } catch(ex) {}
+ assert_equals(client.status, 0, "response data");
+ }, "POST on blob uri");
+ }
+
+ noError('GET', 200)
+ noError('GET', 400)
+ noError('GET', 401)
+ noError('GET', 404)
+ noError('GET', 410)
+ noError('GET', 500)
+ noError('GET', 699)
+
+ noError('HEAD', 200)
+ noError('HEAD', 404)
+ noError('HEAD', 500)
+ noError('HEAD', 699)
+
+ noError('POST', 200)
+ noError('POST', 404)
+ noError('POST', 500)
+ noError('POST', 699)
+
+ noError('PUT', 200)
+ noError('PUT', 404)
+ noError('PUT', 500)
+ noError('PUT', 699)
+
+ unknownScheme();
+ postOnBlob();
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/status.h2.window.js b/test/wpt/tests/xhr/status.h2.window.js
new file mode 100644
index 0000000..13aa2b3
--- /dev/null
+++ b/test/wpt/tests/xhr/status.h2.window.js
@@ -0,0 +1,21 @@
+// See also /fetch/api/basic/status.h2.any.js
+
+[
+ 200,
+ 210,
+ 400,
+ 404,
+ 410,
+ 500,
+ 502
+].forEach(status => {
+ async_test(t => {
+ const client = new XMLHttpRequest();
+ client.open("GET", "/xhr/resources/status.py?code=" + status);
+ client.send();
+ client.onload = t.step_func_done(() => {
+ assert_equals(client.status, status, "status should be " + status);
+ assert_equals(client.statusText, "", "statusText should be the empty string");
+ });
+ }, "statusText over H2 for status " + status + " should be the empty string");
+});
diff --git a/test/wpt/tests/xhr/sync-no-progress.any.js b/test/wpt/tests/xhr/sync-no-progress.any.js
new file mode 100644
index 0000000..aba8204
--- /dev/null
+++ b/test/wpt/tests/xhr/sync-no-progress.any.js
@@ -0,0 +1,13 @@
+// META: timeout=long
+test(t => {
+ let xhr = new XMLHttpRequest();
+ let loadEventFired = false;
+ xhr.onprogress = t.unreached_func('progress event should not be fired');
+ xhr.onload = () => {
+ loadEventFired = true;
+ };
+ xhr.open('GET', 'resources/trickle.py?count=4&delay=150', false);
+ xhr.send();
+ // Check the load event as a sanity check that the test is working.
+ assert_true(loadEventFired, 'load event should have fired');
+}, 'progress event should not be fired by sync XHR');
diff --git a/test/wpt/tests/xhr/sync-no-timeout.any.js b/test/wpt/tests/xhr/sync-no-timeout.any.js
new file mode 100644
index 0000000..ac73e0b
--- /dev/null
+++ b/test/wpt/tests/xhr/sync-no-timeout.any.js
@@ -0,0 +1,16 @@
+// META: global=window,dedicatedworker,sharedworker
+// META: timeout=long
+
+// This is a regression test for https://crbug.com/844268, when a timeout of 10
+// seconds was applied to XHR in Chrome. There should be no timeout unless the
+// "timeout" parameter is set on the object.
+test(t => {
+ let xhr = new XMLHttpRequest();
+
+ // For practical reasons, we can't wait forever. 12 seconds is long enough to
+ // reliably reproduce the bug in Chrome.
+ xhr.open('GET', 'resources/trickle.py?ms=1000&count=12', false);
+
+ // The test will fail if this throws.
+ xhr.send();
+}, 'Sync XHR should not have a timeout');
diff --git a/test/wpt/tests/xhr/sync-xhr-and-window-onload.html b/test/wpt/tests/xhr/sync-xhr-and-window-onload.html
new file mode 100644
index 0000000..3ba9e7a
--- /dev/null
+++ b/test/wpt/tests/xhr/sync-xhr-and-window-onload.html
@@ -0,0 +1,25 @@
+<!doctype html>
+<body>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+async_test((t) => {
+ let onloadIsCalled = false;
+ window.addEventListener('load', () => {
+ onloadIsCalled = true;
+ }, {once: true});
+ document.addEventListener('readystatechange', t.step_func(() => {
+ if (document.readyState !== 'complete') {
+ return;
+ }
+ const xhr = new XMLHttpRequest();
+ xhr.open('GET', 'resources/pass.txt', false /* async */);
+ xhr.send();
+ assert_false(onloadIsCalled);
+ // The load event eventually arrives.
+ window.addEventListener('load', t.step_func_done(() => {
+ }), {once: 'true'});
+ }));
+}, 'sync XHR should not fire window.onload synchronously');
+</script>
+</body>
diff --git a/test/wpt/tests/xhr/sync-xhr-supported-by-feature-policy.html b/test/wpt/tests/xhr/sync-xhr-supported-by-feature-policy.html
new file mode 100644
index 0000000..45588bf
--- /dev/null
+++ b/test/wpt/tests/xhr/sync-xhr-supported-by-feature-policy.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<title>Test that sync-xhr is advertised in the feature list</title>
+<link rel="help" href="https://w3c.github.io/webappsec-feature-policy/#dom-featurepolicy-features">
+<link rel="help" href="https://xhr.spec.whatwg.org/#feature-policy-integration">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+test(() => {
+ assert_in_array('sync-xhr', document.featurePolicy.features());
+}, 'document.featurePolicy.features should advertise sync-xhr.');
+</script>
diff --git a/test/wpt/tests/xhr/template-element.html b/test/wpt/tests/xhr/template-element.html
new file mode 100644
index 0000000..c23c997
--- /dev/null
+++ b/test/wpt/tests/xhr/template-element.html
@@ -0,0 +1,36 @@
+<!doctype html>
+<title>XMLHttpRequest: template element parsing</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<div id=log></div>
+<script>
+async_test(t => {
+ const client = new XMLHttpRequest
+ client.open("GET", "data:text/xml,<template xmlns='http://www.w3.org/1999/xhtml'><test/></template>")
+ client.send()
+ client.onload = t.step_func_done(() => {
+ assert_equals(client.responseXML.documentElement.childElementCount, 0)
+ assert_equals(client.responseXML.documentElement.content.firstChild.localName, "test")
+ })
+})
+
+async_test(t => {
+ const client = new XMLHttpRequest
+ client.open("GET", "data:text/xml,<template><test/></template>")
+ client.send()
+ client.onload = t.step_func_done(() => {
+ assert_equals(client.responseXML.documentElement.childElementCount, 1)
+ assert_equals(client.responseXML.documentElement.firstChild.localName, "test")
+ })
+})
+
+async_test(t => {
+ const client = new XMLHttpRequest
+ client.open("GET", "data:text/xml,<template xmlns='http://www.w3.org/2000/svg'><test/></template>")
+ client.send()
+ client.onload = t.step_func_done(() => {
+ assert_equals(client.responseXML.documentElement.childElementCount, 1)
+ assert_equals(client.responseXML.documentElement.firstChild.localName, "test")
+ })
+})
+</script>
diff --git a/test/wpt/tests/xhr/thrown-error-in-events.html b/test/wpt/tests/xhr/thrown-error-in-events.html
new file mode 100644
index 0000000..d8f1933
--- /dev/null
+++ b/test/wpt/tests/xhr/thrown-error-in-events.html
@@ -0,0 +1,60 @@
+<!DOCTYPE html>
+<title>Errors thrown in XMLHttpRequest events get to window.onerror</title>
+<link rel="author" title="Domenic Denicola" href="mailto:d@domenic.me">
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<script>
+"use strict";
+
+setup({ allow_uncaught_exception: true });
+
+promise_test(() => {
+ const error = new Error("oh no!");
+ let resolve;
+
+ window.addEventListener("error", event => {
+ assert_equals(event.error, error);
+ resolve();
+ });
+
+ const xhr = new window.XMLHttpRequest();
+
+ xhr.addEventListener("load", () => {
+ throw error;
+ });
+
+ xhr.open("GET", location.href);
+ xhr.send();
+
+ return new Promise(r => {
+ resolve = r;
+ });
+
+}, "errors thrown in XMLHttpRequest's load event (using addEventListener) goes to window.onerror");
+
+promise_test(() => {
+ const error = new Error("oh no 2!");
+ let resolve;
+
+ window.addEventListener("error", event => {
+ assert_equals(event.error, error);
+ resolve();
+ });
+
+ const xhr = new window.XMLHttpRequest();
+
+ xhr.onload = () => {
+ throw error;
+ };
+
+ xhr.open("GET", location.href);
+ xhr.send();
+
+ return new Promise(r => {
+ resolve = r;
+ });
+
+}, "errors thrown in XMLHttpRequest's load event (using onload) goes to window.onerror");
+</script>
diff --git a/test/wpt/tests/xhr/timeout-cors-async.htm b/test/wpt/tests/xhr/timeout-cors-async.htm
new file mode 100644
index 0000000..74397ba
--- /dev/null
+++ b/test/wpt/tests/xhr/timeout-cors-async.htm
@@ -0,0 +1,43 @@
+<!doctype html>
+<html>
+ <head>
+ <title>XMLHttpRequest: timeout event and cross-origin request</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-timeout-attribute" data-tested-assertations="following::ol[1]/li[2]" />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#handler-xhr-ontimeout" data-tested-assertations="../.."/>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#timeout-error" data-tested-assertations=".."/>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#request-error" data-tested-assertations="following::ol[1]/li[4] following::ol[1]/li[9]"/>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#cross-origin-request-event-rules" data-tested-assertations="following::dl//code[contains(@title,'dom-XMLHttpRequest-timeout')]/.. following::dl//code[contains(@title,'dom-XMLHttpRequest-timeout')]/../following-sibling::dd following::dt[1] following::dd[1]" />
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ var test = async_test()
+ var client = new XMLHttpRequest()
+ var gotTimeout = false
+ client.open("GET", "http://www2." + location.hostname + (location.port ? ":" + location.port : "") +(location.pathname.replace(/[^\/]+$/, '')+'resources/corsenabled.py')+"?delay=2&code=200")
+ client.timeout = 100
+ client.addEventListener('timeout', function (e) {
+ test.step(function() {
+ assert_equals(e.type, 'timeout')
+ assert_equals(client.status, 0)
+ gotTimeout = true
+ })
+ })
+ client.addEventListener('load', function (e) {
+ test.step(function() {
+ assert_unreached('load event should not fire')
+ })
+ })
+ client.addEventListener('loadend', function (e) {
+ test.step(function() {
+ assert_true(gotTimeout, "timeout event should fire")
+ test.done()
+ })
+ })
+
+ client.send(null)
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/timeout-multiple-fetches.html b/test/wpt/tests/xhr/timeout-multiple-fetches.html
new file mode 100644
index 0000000..4f4998c
--- /dev/null
+++ b/test/wpt/tests/xhr/timeout-multiple-fetches.html
@@ -0,0 +1,32 @@
+<!doctype html>
+<title>XMLHttpRequest: timeout, redirects, and CORS preflights</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=/common/get-host-info.sub.js></script>
+<script src=/common/utils.js></script>
+<div id=log></div>
+<script>
+async_test(t => {
+ const client = new XMLHttpRequest
+ client.open("GET", "resources/redirect.py?delay=500&location=delay.py") // 500 + 500 = 1000
+ client.timeout = 750
+ client.send()
+ client.ontimeout = t.step_func_done(() => {
+ assert_equals(client.readyState, 4)
+ })
+ client.onload = t.unreached_func("load event fired")
+}, "Redirects should not reset the timer")
+
+async_test(t => {
+ // Use a unique ID to prevent caching of the preflight making the test flaky.
+ const uuid = token();
+ const client = new XMLHttpRequest
+ client.open("YO", get_host_info().HTTP_REMOTE_ORIGIN + "/xhr/resources/delay.py?uuid=" + uuid)
+ client.timeout = 750
+ client.send()
+ client.ontimeout = t.step_func_done(() => {
+ assert_equals(client.readyState, 4)
+ })
+ client.onload = t.unreached_func("load event fired")
+}, "CORS preflights should not reset the timer")
+</script>
diff --git a/test/wpt/tests/xhr/timeout-sync.htm b/test/wpt/tests/xhr/timeout-sync.htm
new file mode 100644
index 0000000..d8b2cc4
--- /dev/null
+++ b/test/wpt/tests/xhr/timeout-sync.htm
@@ -0,0 +1,25 @@
+<!doctype html>
+<html>
+ <head>
+ <title>XMLHttpRequest: timeout not allowed for sync requests</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-timeout-attribute" data-tested-assertations="following::ol/li[1]" />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-open()-method" data-tested-assertations="following::ol/li[10]" />
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ test(function() {
+ var client = new XMLHttpRequest()
+ client.open('GET', 'folder.txt', false)
+ assert_throws_dom("InvalidAccessError", function() { client.timeout = 1000 })
+ }, 'setting timeout attribute on sync request')
+ test(function() {
+ var client = new XMLHttpRequest()
+ client.timeout = 1000
+ assert_throws_dom("InvalidAccessError", function() { client.open('GET', 'folder.txt', false) })
+ }, 'open() with async false when timeout is set')
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/xhr-authorization-redirect.any.js b/test/wpt/tests/xhr/xhr-authorization-redirect.any.js
new file mode 100644
index 0000000..beed7c3
--- /dev/null
+++ b/test/wpt/tests/xhr/xhr-authorization-redirect.any.js
@@ -0,0 +1,28 @@
+// META: global=window,sharedworker,dedicatedworker
+// META: script=/common/get-host-info.sub.js
+
+const authorizationValue = "Basic " + btoa("user:pass");
+function getAuthorizationHeaderValue(url)
+{
+ var client = new XMLHttpRequest();
+ client.open("GET", url, false);
+ client.setRequestHeader("Authorization", authorizationValue);
+ const promise = new Promise(resolve => client.onloadend = () => resolve(client.responseText));
+ client.send();
+ return promise;
+}
+
+promise_test(async test => {
+ const result = await getAuthorizationHeaderValue("/fetch/api/resources/dump-authorization-header.py");
+ assert_equals(result, authorizationValue);
+}, "getAuthorizationHeaderValue - no redirection");
+
+promise_test(async test => {
+ const result = await getAuthorizationHeaderValue("/fetch/api/resources/redirect.py?location=" + encodeURIComponent("/fetch/api/resources/dump-authorization-header.py"));
+ assert_equals(result, authorizationValue);
+}, "getAuthorizationHeaderValue - same origin redirection");
+
+promise_test(async (test) => {
+ const result = await getAuthorizationHeaderValue(get_host_info().HTTP_REMOTE_ORIGIN + "/fetch/api/resources/redirect.py?allow_headers=Authorization&location=" + encodeURIComponent(get_host_info().HTTP_ORIGIN + "/fetch/api/resources/dump-authorization-header.py"));
+ assert_equals(result, "none");
+}, "getAuthorizationHeaderValue - cross origin redirection");
diff --git a/test/wpt/tests/xhr/xhr-timeout-longtask.any.js b/test/wpt/tests/xhr/xhr-timeout-longtask.any.js
new file mode 100644
index 0000000..1617131
--- /dev/null
+++ b/test/wpt/tests/xhr/xhr-timeout-longtask.any.js
@@ -0,0 +1,14 @@
+async_test(function() {
+ var client = new XMLHttpRequest();
+ client.open("GET", "resources/delay.py?ms=100", true);
+
+ client.timeout = 150;
+ client.ontimeout = this.step_func(assert_unreached);
+ client.onloadend = () => this.done();
+
+ client.send();
+
+ const start = performance.now();
+ while (performance.now() - start < 200) { }
+}, "Long tasks should not trigger load timeout")
+
diff --git a/test/wpt/tests/xhr/xmlhttprequest-basic.htm b/test/wpt/tests/xhr/xmlhttprequest-basic.htm
new file mode 100644
index 0000000..c48b610
--- /dev/null
+++ b/test/wpt/tests/xhr/xmlhttprequest-basic.htm
@@ -0,0 +1,45 @@
+<!doctype html>
+<html>
+ <head>
+ <title>XMLHttpRequest: prototype and members</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#dom-xmlhttprequest" data-tested-assertations="following::ol/li[1]" />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#xmlhttprequest" data-tested-assertations="." />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#states" data-tested-assertations="following::dfn[2] following::dfn[3] following::dfn[4] following::dfn[5] following::dfn[6]" />
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ test(function() {
+ XMLHttpRequest.prototype.test = function() { return "TEH" }
+ var client = new XMLHttpRequest()
+ assert_equals(client.test(), "TEH")
+ var members = ["onreadystatechange",
+ "open",
+ "setRequestHeader",
+ "send",
+ "abort",
+ "status",
+ "statusText",
+ "getResponseHeader",
+ "getAllResponseHeaders",
+ "responseText",
+ "responseXML"]
+ for(var x in members)
+ assert_true(members[x] in client, members[x])
+ var constants = ["UNSENT",
+ "OPENED",
+ "HEADERS_RECEIVED",
+ "LOADING",
+ "DONE"],
+ i = 0
+ for(var x in constants) {
+ assert_equals(client[constants[x]], i, constants[x])
+ assert_equals(XMLHttpRequest[constants[x]], i, "XHR " + constants[x])
+ i++
+ }
+ })
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/xmlhttprequest-eventtarget.htm b/test/wpt/tests/xhr/xmlhttprequest-eventtarget.htm
new file mode 100644
index 0000000..ea58fd4
--- /dev/null
+++ b/test/wpt/tests/xhr/xmlhttprequest-eventtarget.htm
@@ -0,0 +1,48 @@
+<!doctype html>
+<html>
+ <head>
+ <title>XMLHttpRequest: implements EventTarget</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#xmlhttprequesteventtarget" data-tested-assertations=".." />
+ <!-- Obviously, most of the stuff actually being tested here is covered in the DOM events spec, not in the XHR spec -->
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ var test = async_test(),
+ x = null,
+ expected = ["a1", "b1", "c1", "a2", "b2", "c2", "a3", "c3", "a4", "c4"],
+ result = []
+ function callback(e) {
+ result.push("b" + x.readyState)
+ test.step(function() {
+ if(x.readyState == 3)
+ assert_unreached()
+ })
+ }
+ test.step(function() {
+ x = new XMLHttpRequest()
+ x.onreadystatechange = function() {
+ test.step(function() {
+ result.push("a" + x.readyState)
+ })
+ }
+ x.addEventListener("readystatechange", callback, false)
+ x.addEventListener("readystatechange", function() {
+ test.step(function() {
+ result.push("c" + x.readyState)
+ if(x.readyState == 2)
+ x.removeEventListener("readystatechange", callback, false)
+ if(x.readyState == 4) {
+ assert_array_equals(result, expected)
+ test.done()
+ }
+ })
+ }, false)
+ x.open("GET", "folder.txt")
+ x.send(null)
+ })
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/xmlhttprequest-network-error-sync.htm b/test/wpt/tests/xhr/xmlhttprequest-network-error-sync.htm
new file mode 100644
index 0000000..ee367fb
--- /dev/null
+++ b/test/wpt/tests/xhr/xmlhttprequest-network-error-sync.htm
@@ -0,0 +1,34 @@
+<!doctype html>
+<html>
+ <head>
+ <title>XMLHttpRequest: members during network errors (sync)</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#infrastructure-for-the-send()-method" data-tested-assertations="following::dl[1]/dt[2] following::dl[1]/dd[2]/p[1]" />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#network-error" data-tested-assertations=".." />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#request-error" data-tested-assertations="following::ol[1]/li[5]" />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-status-attribute" data-tested-assertations="following::ol[1]/li[1] following::ol[1]/li[2]" />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-statustext-attribute" data-tested-assertations="following::ol[1]/li[1] following::ol[1]/li[2]" />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-getresponseheader()-method" data-tested-assertations="following::ol[1]/li[1] following::ol[1]/li[2]" />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-getallresponseheaders()-method" data-tested-assertations="following::ol[1]/li[1] following::ol[1]/li[2]" />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-responsetext-attribute" data-tested-assertations="following::ol[1]/li[2] following::ol[1]/li[3]" />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-responsexml-attribute" data-tested-assertations="following::ol[1]/li[2] following::ol[1]/li[3]" />
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ test(function() {
+ var client = new XMLHttpRequest()
+ client.open("GET", "resources/infinite-redirects.py", false)
+ assert_throws_dom("NetworkError", function() { client.send(null) }, "send")
+ assert_equals(client.status, 0, "status")
+ assert_equals(client.statusText, "", "statusText")
+ assert_equals(client.getAllResponseHeaders(), "", "getAllResponseHeaders")
+ assert_equals(client.getResponseHeader("content-type"), null, "getResponseHeader")
+ assert_equals(client.responseText, "", "responseText")
+ assert_equals(client.responseXML, null, "responseXML")
+ assert_equals(client.readyState, client.DONE, "readyState")
+ })
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/xmlhttprequest-network-error.htm b/test/wpt/tests/xhr/xmlhttprequest-network-error.htm
new file mode 100644
index 0000000..c8e3200
--- /dev/null
+++ b/test/wpt/tests/xhr/xmlhttprequest-network-error.htm
@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>XMLHttpRequest: members during network errors</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#infrastructure-for-the-send()-method" data-tested-assertations="following::dl[1]/dt[2] following::dl[1]/dd[2]/ol/li[1] following::dl[1]/dd[2]/ol/li[2]" />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-status-attribute" data-tested-assertations="following::ol[1]/li[1] following::ol[1]/li[2]" />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-statustext-attribute" data-tested-assertations="following::ol[1]/li[1] following::ol[1]/li[2]" />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-getresponseheader()-method" data-tested-assertations="following::ol[1]/li[1] following::ol[1]/li[2]" />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-getallresponseheaders()-method" data-tested-assertations="following::ol[1]/li[1] following::ol[1]/li[2]" />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-responsetext-attribute" data-tested-assertations="following::ol[1]/li[2] following::ol[1]/li[3]" />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-responsexml-attribute" data-tested-assertations="following::ol[1]/li[2] following::ol[1]/li[3]" />
+
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ var test = async_test()
+ test.step(function() {
+ var client = new XMLHttpRequest()
+ client.onreadystatechange = function() {
+ test.step(function() {
+ assert_equals(client.status, 0, "status")
+ assert_equals(client.statusText, "", "statusText")
+ assert_equals(client.getAllResponseHeaders(), "", "getAllResponseHeaders")
+ assert_equals(client.getResponseHeader("content-type"), null, "getResponseHeader")
+ assert_equals(client.responseText, "", "responseText")
+ assert_equals(client.responseXML, null, "responseXML")
+ if(client.readyState == 4)
+ test.done()
+ })
+ }
+ client.open("GET", "resources/infinite-redirects.py")
+ client.send(null)
+ })
+ </script>
+ </body>
+</html>
diff --git a/test/wpt/tests/xhr/xmlhttprequest-sync-block-defer-scripts-subframe.html b/test/wpt/tests/xhr/xmlhttprequest-sync-block-defer-scripts-subframe.html
new file mode 100644
index 0000000..be46a12
--- /dev/null
+++ b/test/wpt/tests/xhr/xmlhttprequest-sync-block-defer-scripts-subframe.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<script>
+ var x = 0;
+</script>
+<!-- This script's URI is:
+ var xhr = new XMLHttpRequest();
+ xhr.open('GET', 'data:text/plain,aaa', false);
+ xhr.send();
+ x=1;
+ -->
+<script defer src="data:application/javascript,var%20x%20=%200;%20var%20xhr%20=%20new%20XMLHttpRequest();%20xhr.open('GET',%20'data:text/plain,aaa',%20false);%20xhr.send();%20x=1"></script>
+
+<!-- This script's URI is:
+ parent.postMessage(x, '*');
+-->
+<script defer src="data:application/javascript,parent.postMessage(x, '*');"></script>
+
diff --git a/test/wpt/tests/xhr/xmlhttprequest-sync-block-defer-scripts.html b/test/wpt/tests/xhr/xmlhttprequest-sync-block-defer-scripts.html
new file mode 100644
index 0000000..0aabdd4
--- /dev/null
+++ b/test/wpt/tests/xhr/xmlhttprequest-sync-block-defer-scripts.html
@@ -0,0 +1,15 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Check that a sync XHR in a defer script blocks later defer scripts from running</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<!--
+ We run the test in a subframe, because something in the testharness stuff
+ interferes with defer scripts -->
+<script>
+ var t = async_test();
+ onmessage = t.step_func_done(function(e) {
+ assert_equals(e.data, 1);
+ });
+</script>
+<iframe src="xmlhttprequest-sync-block-defer-scripts-subframe.html"></iframe>
diff --git a/test/wpt/tests/xhr/xmlhttprequest-sync-block-scripts.html b/test/wpt/tests/xhr/xmlhttprequest-sync-block-scripts.html
new file mode 100644
index 0000000..d6714ac
--- /dev/null
+++ b/test/wpt/tests/xhr/xmlhttprequest-sync-block-scripts.html
@@ -0,0 +1,22 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Check that while a sync XHR is in flight async script loads don't complete and run script</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<body>
+<script>
+var scriptRan = false;
+var onloadFired = false;
+test(function() {
+ var s = document.createElement("script");
+ s.src = "data:application/javascript,scriptRan = true;";
+ s.onload = function() { onloadFired = true; }
+ document.body.appendChild(s);
+ var xhr = new XMLHttpRequest();
+ xhr.open("GET", "data:,", false);
+ xhr.send();
+ assert_false(scriptRan, "Script should not have run");
+ assert_false(onloadFired, "load event for <script> should not have fired");
+});
+</script>
+</body>
diff --git a/test/wpt/tests/xhr/xmlhttprequest-sync-default-feature-policy.sub.html b/test/wpt/tests/xhr/xmlhttprequest-sync-default-feature-policy.sub.html
new file mode 100644
index 0000000..ab5b78b
--- /dev/null
+++ b/test/wpt/tests/xhr/xmlhttprequest-sync-default-feature-policy.sub.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<body>
+ <meta charset="utf-8">
+ <title>Synchronous XMLHttpRequest Feature Policy Test</title>
+ <script src=/resources/testharness.js></script>
+ <script src=/resources/testharnessreport.js></script>
+ <script src=/feature-policy/resources/featurepolicy.js></script>
+ <script src=util/utils.js></script>
+ <script>
+ 'use strict';
+ run_all_fp_tests_allow_all(
+ 'http://{{hosts[alt][]}}:{{ports[http][0]}}',
+ 'sync-xhr',
+ 'NetworkError',
+ () => {
+ return new Promise((resolve, reject) => {
+ try {
+ var xhr = new XMLHttpRequest();
+ xhr.open("GET", "data:,", false);
+ try {
+ xhr.send();
+ } catch(e) {
+ reject(e);
+ }
+ } catch(e) {
+ reject({"name": "UnexpectedException:" + e.name});
+ }
+ resolve();
+ });
+ });
+ </script>
+</body>
diff --git a/test/wpt/tests/xhr/xmlhttprequest-sync-not-hang-scriptloader-subframe.html b/test/wpt/tests/xhr/xmlhttprequest-sync-not-hang-scriptloader-subframe.html
new file mode 100644
index 0000000..aeff2af
--- /dev/null
+++ b/test/wpt/tests/xhr/xmlhttprequest-sync-not-hang-scriptloader-subframe.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<script>
+ function secondScriptRan() {
+ parent.postMessage("done", "*");
+ }
+
+ function createSecondScript() {
+ var script = document.createElement("script");
+ script.src = "data:application/javascript,secondScriptRan()";
+ document.head.appendChild(script);
+
+ var xhr = new XMLHttpRequest();
+ xhr.open("GET", "data:,", false);
+ xhr.send();
+ }
+</script>
+<script src="data:application/javascript,createSecondScript()" defer></script>
diff --git a/test/wpt/tests/xhr/xmlhttprequest-sync-not-hang-scriptloader.html b/test/wpt/tests/xhr/xmlhttprequest-sync-not-hang-scriptloader.html
new file mode 100644
index 0000000..bbec1ed
--- /dev/null
+++ b/test/wpt/tests/xhr/xmlhttprequest-sync-not-hang-scriptloader.html
@@ -0,0 +1,16 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Ensure that an async script added during a defer script that then does a
+ sync XHR still runs</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<!--
+ We run the test in a subframe, because something in the testharness stuff
+ interferes with defer scripts -->
+<script>
+ var t = async_test();
+ onmessage = t.step_func_done(function(e) {
+ assert_equals(e.data, "done");
+ });
+</script>
+<iframe src="xmlhttprequest-sync-not-hang-scriptloader-subframe.html"></iframe>
diff --git a/test/wpt/tests/xhr/xmlhttprequest-timeout-aborted.html b/test/wpt/tests/xhr/xmlhttprequest-timeout-aborted.html
new file mode 100644
index 0000000..199899c
--- /dev/null
+++ b/test/wpt/tests/xhr/xmlhttprequest-timeout-aborted.html
@@ -0,0 +1,29 @@
+ <!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8" />
+ <title>XHR2 Timeout Property Tests</title>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-abort()-method" data-tested-assertations="following-sibling::ol/li[4] following-sibling::ol/li[4]/ol/li[5]" />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-timeout-attribute" data-tested-assertations="following-sibling::ol/li[2]" />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#abort-error" data-tested-assertations=".."/>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#event-xhr-abort" data-tested-assertations="../.." />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#timeout-error" data-tested-assertations=".."/>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#event-xhr-timeout" data-tested-assertations="../.." />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#request-error" data-tested-assertations="following::ol/li[9]"/>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#infrastructure-for-the-send()-method" data-tested-assertations="following-sibling::dl//code[contains(@title,'dom-XMLHttpRequest-timeout')]/.. following-sibling::dl//code[contains(@title,'dom-XMLHttpRequest-timeout')]/../following-sibling::dd following::dt[1] following::dd[1]" />
+ <meta name=timeout content=long>
+ <meta name=variant content="?only open()ed, not aborted">
+ <meta name=variant content="?aborted immediately after send()">
+ <meta name=variant content="?call abort() after TIME_NORMAL_LOAD">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="resources/xmlhttprequest-timeout.js"></script>
+ <script src="resources/xmlhttprequest-timeout-runner.js"></script>
+</head>
+<body>
+ <h1>Description</h1>
+ <p>This test validates that the XHR2 timeout property behaves as expected in async cases in document (i.e. non-worker) context.</p>
+ <div id="log"></div>
+ <script src="resources/xmlhttprequest-timeout-aborted.js" type="text/javascript"></script>
+</body>
+</html>
diff --git a/test/wpt/tests/xhr/xmlhttprequest-timeout-abortedonmain.html b/test/wpt/tests/xhr/xmlhttprequest-timeout-abortedonmain.html
new file mode 100644
index 0000000..1dfe595
--- /dev/null
+++ b/test/wpt/tests/xhr/xmlhttprequest-timeout-abortedonmain.html
@@ -0,0 +1,25 @@
+ <!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8" />
+ <title>XHR2 Timeout Property Tests</title>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-abort()-method" data-tested-assertations="following-sibling::ol/li[4] following-sibling::ol/li[4]/ol/li[5]" />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#abort-error" data-tested-assertations=".."/>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#event-xhr-abort" data-tested-assertations="../.." />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#request-error" data-tested-assertations="following::ol/li[9]"/>
+ <meta name=timeout content=long>
+ <meta name=variant content="?abort() from a 0ms timeout">
+ <meta name=variant content="?aborted after TIME_DELAY">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="resources/xmlhttprequest-timeout.js"></script>
+ <script src="resources/xmlhttprequest-timeout-runner.js"></script>
+</head>
+<body>
+ <h1>Description</h1>
+ <p>This test validates that the XHR2 timeout property behaves as expected in async cases in document (i.e. non-worker) context.</p>
+ <div id="log"></div>
+ <script src="resources/xmlhttprequest-timeout-abortedonmain.js" type="text/javascript"></script>
+</body>
+</html>
+
diff --git a/test/wpt/tests/xhr/xmlhttprequest-timeout-overrides.html b/test/wpt/tests/xhr/xmlhttprequest-timeout-overrides.html
new file mode 100644
index 0000000..afebfe7
--- /dev/null
+++ b/test/wpt/tests/xhr/xmlhttprequest-timeout-overrides.html
@@ -0,0 +1,26 @@
+ <!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8" />
+ <title>XHR2 Timeout Property Tests</title>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-timeout-attribute" data-tested-assertations="following::ol[1]/li[2]" />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#handler-xhr-ontimeout" data-tested-assertations="../.."/>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#timeout-error" data-tested-assertations=".."/>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#request-error" data-tested-assertations="following::ol[1]/li[9]"/>
+ <meta name=timeout content=long>
+ <meta name=variant content="?timeout disabled after initially set">
+ <meta name=variant content="?timeout overrides load after a delay">
+ <meta name=variant content="?timeout enabled after initially disabled">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="resources/xmlhttprequest-timeout.js"></script>
+ <script src="resources/xmlhttprequest-timeout-runner.js"></script>
+</head>
+<body>
+ <h1>Description</h1>
+ <p>This test validates that the XHR2 timeout property behaves as expected in async cases in document (i.e. non-worker) context.</p>
+ <div id="log"></div>
+ <script src="resources/xmlhttprequest-timeout-overrides.js"></script>
+</body>
+</html>
+
diff --git a/test/wpt/tests/xhr/xmlhttprequest-timeout-overridesexpires.html b/test/wpt/tests/xhr/xmlhttprequest-timeout-overridesexpires.html
new file mode 100644
index 0000000..b708593
--- /dev/null
+++ b/test/wpt/tests/xhr/xmlhttprequest-timeout-overridesexpires.html
@@ -0,0 +1,26 @@
+ <!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8" />
+ <title>XHR2 Timeout Property Tests</title>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-timeout-attribute" data-tested-assertations="following::ol[1]/li[2]" />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#handler-xhr-ontimeout" data-tested-assertations="../.."/>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#timeout-error" data-tested-assertations=".."/>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#request-error" data-tested-assertations="following::ol[1]/li[9]"/>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#infrastructure-for-the-send()-method" data-tested-assertations="following-sibling::dl//code[contains(@title,'dom-XMLHttpRequest-timeout')]/.. following-sibling::dl//code[contains(@title,'dom-XMLHttpRequest-timeout')]/../following-sibling::dd following::dt[1] following::dd[1]" />
+ <meta name=timeout content=long>
+ <meta name=variant content="?timeout set to expiring value after load fires">
+ <meta name=variant content="?timeout set to expired value before load fires">
+ <meta name=variant content="?timeout set to non-expiring value after timeout fires">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="resources/xmlhttprequest-timeout.js"></script>
+ <script src="resources/xmlhttprequest-timeout-runner.js"></script>
+</head>
+<body>
+ <h1>Description</h1>
+ <p>This test validates that the XHR2 timeout property behaves as expected in async cases in document (i.e. non-worker) context.</p>
+ <div id="log"></div>
+ <script src="resources/xmlhttprequest-timeout-overridesexpires.js" type="text/javascript"></script>
+</body>
+</html>
diff --git a/test/wpt/tests/xhr/xmlhttprequest-timeout-reused.html b/test/wpt/tests/xhr/xmlhttprequest-timeout-reused.html
new file mode 100644
index 0000000..48a9893
--- /dev/null
+++ b/test/wpt/tests/xhr/xmlhttprequest-timeout-reused.html
@@ -0,0 +1,49 @@
+ <!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8" />
+ <title>XHR2 Timeout Property Tests</title>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#timeout-error" />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-timeout-attribute" data-tested-assertations="following::ol[1]/li[2]" />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#handler-xhr-ontimeout" data-tested-assertations="../.."/>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#timeout-error" data-tested-assertations=".."/>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#request-error" data-tested-assertations="following::ol[1]/li[9]"/>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#infrastructure-for-the-send()-method" data-tested-assertations="following-sibling::dl//code[contains(@title,'dom-XMLHttpRequest-timeout')]/.. following-sibling::dl//code[contains(@title,'dom-XMLHttpRequest-timeout')]/../following-sibling::dd following::dt[1] following::dd[1]" />
+ <meta name=timeout content=long>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+</head>
+<body>
+ <div id="log"></div>
+ <script type="text/javascript">
+
+var test = async_test();
+
+function startRequest() {
+ xhr.open("GET", "./resources/content.py?content=Hi", true);
+ xhr.timeout = 2000;
+ test.step_timeout(function () {
+ xhr.send();
+ }, 1000);
+}
+
+test.step(function()
+{
+ var count = 0;
+ xhr = new XMLHttpRequest();
+ xhr.onload = function () {
+ assert_equals(xhr.response, "Hi");
+ if (++count == 2) {
+ test.done();
+ }
+ }
+ xhr.ontimeout = function () {
+ assert_unreached("HTTP error should not timeout");
+ }
+ startRequest();
+ test.step_timeout(startRequest, 3500);
+});
+
+ </script>
+</body>
+</html>
diff --git a/test/wpt/tests/xhr/xmlhttprequest-timeout-simple.html b/test/wpt/tests/xhr/xmlhttprequest-timeout-simple.html
new file mode 100644
index 0000000..ac8984f
--- /dev/null
+++ b/test/wpt/tests/xhr/xmlhttprequest-timeout-simple.html
@@ -0,0 +1,27 @@
+ <!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8" />
+ <title>XHR2 Timeout Property Tests</title>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-timeout-attribute" data-tested-assertations="following::ol[1]/li[2]" />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#handler-xhr-ontimeout" data-tested-assertations="../.."/>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#timeout-error" data-tested-assertations=".."/>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#request-error" data-tested-assertations="following::ol[1]/li[9]"/>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#infrastructure-for-the-send()-method" data-tested-assertations="following-sibling::dl//code[contains(@title,'dom-XMLHttpRequest-timeout')]/.. following-sibling::dl//code[contains(@title,'dom-XMLHttpRequest-timeout')]/../following-sibling::dd following::dt[1] following::dd[1]" />
+ <meta name=timeout content=long>
+ <meta name=variant content="?no time out scheduled, load fires normally">
+ <meta name=variant content="?load fires normally">
+ <meta name=variant content="?timeout hit before load">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="resources/xmlhttprequest-timeout.js"></script>
+ <script src="resources/xmlhttprequest-timeout-runner.js"></script>
+</head>
+<body>
+ <h1>Description</h1>
+ <p>This test validates that the XHR2 timeout property behaves as expected in async cases in document (i.e. non-worker) context.</p>
+ <div id="log"></div>
+ <script src="resources/xmlhttprequest-timeout-simple.js"></script>
+</body>
+</html>
+
diff --git a/test/wpt/tests/xhr/xmlhttprequest-timeout-synconmain.html b/test/wpt/tests/xhr/xmlhttprequest-timeout-synconmain.html
new file mode 100644
index 0000000..eb9ecb7
--- /dev/null
+++ b/test/wpt/tests/xhr/xmlhttprequest-timeout-synconmain.html
@@ -0,0 +1,23 @@
+ <!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8" />
+ <title>XHR2 Timeout Property Tests</title>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-timeout-attribute" data-tested-assertations="following::ol[1]/li[1]" />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-open()-method" data-tested-assertations="following::ol[1]/li[10]" />
+ <meta name=timeout content=long>
+ <meta name=variant content="?timeout after open">
+ <meta name=variant content="?timeout before open">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="resources/xmlhttprequest-timeout.js"></script>
+ <script src="resources/xmlhttprequest-timeout-runner.js"></script>
+</head>
+<body>
+ <h1>Description</h1>
+ <p>This test validates that the XHR2 timeout property behaves as expected in async cases in document (i.e. non-worker) context.</p>
+ <div id="log"></div>
+ <script src="resources/xmlhttprequest-timeout-synconmain.js" type="text/javascript"></script>
+</body>
+</html>
+
diff --git a/test/wpt/tests/xhr/xmlhttprequest-timeout-twice.html b/test/wpt/tests/xhr/xmlhttprequest-timeout-twice.html
new file mode 100644
index 0000000..ce3b10b
--- /dev/null
+++ b/test/wpt/tests/xhr/xmlhttprequest-timeout-twice.html
@@ -0,0 +1,28 @@
+ <!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8" />
+ <title>XHR2 Timeout Property Tests</title>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#timeout-error" />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-timeout-attribute" data-tested-assertations="following::ol[1]/li[2]" />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#handler-xhr-ontimeout" data-tested-assertations="../.."/>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#timeout-error" data-tested-assertations=".."/>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#request-error" data-tested-assertations="following::ol[1]/li[9]"/>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#infrastructure-for-the-send()-method" data-tested-assertations="following-sibling::dl//code[contains(@title,'dom-XMLHttpRequest-timeout')]/.. following-sibling::dl//code[contains(@title,'dom-XMLHttpRequest-timeout')]/../following-sibling::dd following::dt[1] following::dd[1]" />
+ <meta name=timeout content=long>
+ <meta name=variant content="?load fires normally with no timeout set, twice">
+ <meta name=variant content="?load fires normally with same timeout set twice">
+ <meta name=variant content="?timeout fires normally with same timeout set twice">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="resources/xmlhttprequest-timeout.js"></script>
+ <script src="resources/xmlhttprequest-timeout-runner.js"></script>
+</head>
+<body>
+ <h1>Description</h1>
+ <p>This test validates that the XHR2 timeout property behaves as expected in async cases in document (i.e. non-worker) context.</p>
+ <div id="log"></div>
+ <script src="resources/xmlhttprequest-timeout-twice.js" type="text/javascript"></script>
+</body>
+</html>
+
diff --git a/test/wpt/tests/xhr/xmlhttprequest-timeout-worker-aborted.html b/test/wpt/tests/xhr/xmlhttprequest-timeout-worker-aborted.html
new file mode 100644
index 0000000..91fdab3
--- /dev/null
+++ b/test/wpt/tests/xhr/xmlhttprequest-timeout-worker-aborted.html
@@ -0,0 +1,31 @@
+ <!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8" />
+ <title>XHR2 Timeout Property Tests in Worker</title>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-abort()-method" data-tested-assertations="following-sibling::ol/li[4] following-sibling::ol/li[4]/ol/li[5]" />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-timeout-attribute" data-tested-assertations="following-sibling::ol/li[2]" />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#abort-error" data-tested-assertations=".."/>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#event-xhr-abort" data-tested-assertations="../.." />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#timeout-error" data-tested-assertations=".."/>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#event-xhr-timeout" data-tested-assertations="../.." />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#request-error" data-tested-assertations="following::ol/li[9]"/>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#infrastructure-for-the-send()-method" data-tested-assertations="following-sibling::dl//code[contains(@title,'dom-XMLHttpRequest-timeout')]/.. following-sibling::dl//code[contains(@title,'dom-XMLHttpRequest-timeout')]/../following-sibling::dd following::dt[1] following::dd[1]" />
+ <meta name=timeout content=long>
+ <meta name=variant content="?only open()ed, not aborted">
+ <meta name=variant content="?aborted immediately after send()">
+ <meta name=variant content="?call abort() after TIME_NORMAL_LOAD">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="resources/xmlhttprequest-timeout-runner.js"></script>
+</head>
+<body>
+ <h1>Description</h1>
+ <p>This test validates that the XHR2 timeout property behaves as expected in in a worker context.</p>
+ <div id="log"></div>
+ <script type="text/javascript">
+ var worker = new Worker("resources/xmlhttprequest-timeout-aborted.js");
+ worker.addEventListener("message", testResultCallbackHandler);
+ </script>
+</body>
+</html>
diff --git a/test/wpt/tests/xhr/xmlhttprequest-timeout-worker-overrides.html b/test/wpt/tests/xhr/xmlhttprequest-timeout-worker-overrides.html
new file mode 100644
index 0000000..b2ad952
--- /dev/null
+++ b/test/wpt/tests/xhr/xmlhttprequest-timeout-worker-overrides.html
@@ -0,0 +1,27 @@
+ <!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8" />
+ <title>XHR2 Timeout Property Tests in Worker</title>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-timeout-attribute" data-tested-assertations="following::ol[1]/li[2]" />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#handler-xhr-ontimeout" data-tested-assertations="../.."/>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#timeout-error" data-tested-assertations=".."/>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#request-error" data-tested-assertations="following::ol[1]/li[9]"/>
+ <meta name=timeout content=long>
+ <meta name=variant content="?timeout disabled after initially set">
+ <meta name=variant content="?timeout overrides load after a delay">
+ <meta name=variant content="?timeout enabled after initially disabled">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="resources/xmlhttprequest-timeout-runner.js"></script>
+</head>
+<body>
+ <h1>Description</h1>
+ <p>This test validates that the XHR2 timeout property behaves as expected in in a worker context.</p>
+ <div id="log"></div>
+ <script type="text/javascript">
+ var worker = new Worker("resources/xmlhttprequest-timeout-overrides.js" + location.search);
+ worker.addEventListener("message", testResultCallbackHandler);
+ </script>
+</body>
+</html>
diff --git a/test/wpt/tests/xhr/xmlhttprequest-timeout-worker-overridesexpires.html b/test/wpt/tests/xhr/xmlhttprequest-timeout-worker-overridesexpires.html
new file mode 100644
index 0000000..07a8656
--- /dev/null
+++ b/test/wpt/tests/xhr/xmlhttprequest-timeout-worker-overridesexpires.html
@@ -0,0 +1,28 @@
+ <!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8" />
+ <title>XHR2 Timeout Property Tests in Worker</title>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-timeout-attribute" data-tested-assertations="following::ol[1]/li[2]" />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#handler-xhr-ontimeout" data-tested-assertations="../.."/>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#timeout-error" data-tested-assertations=".."/>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#request-error" data-tested-assertations="following::ol[1]/li[9]"/>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#infrastructure-for-the-send()-method" data-tested-assertations="following-sibling::dl//code[contains(@title,'dom-XMLHttpRequest-timeout')]/.. following-sibling::dl//code[contains(@title,'dom-XMLHttpRequest-timeout')]/../following-sibling::dd following::dt[1] following::dd[1]" />
+ <meta name=timeout content=long>
+ <meta name=variant content="?timeout set to expiring value after load fires">
+ <meta name=variant content="?timeout set to expired value before load fires">
+ <meta name=variant content="?timeout set to non-expiring value after timeout fires">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="resources/xmlhttprequest-timeout-runner.js"></script>
+</head>
+<body>
+ <h1>Description</h1>
+ <p>This test validates that the XHR2 timeout property behaves as expected in in a worker context.</p>
+ <div id="log"></div>
+ <script type="text/javascript">
+ var worker = new Worker("resources/xmlhttprequest-timeout-overridesexpires.js");
+ worker.addEventListener("message", testResultCallbackHandler);
+ </script>
+</body>
+</html>
diff --git a/test/wpt/tests/xhr/xmlhttprequest-timeout-worker-simple.html b/test/wpt/tests/xhr/xmlhttprequest-timeout-worker-simple.html
new file mode 100644
index 0000000..fa5c899
--- /dev/null
+++ b/test/wpt/tests/xhr/xmlhttprequest-timeout-worker-simple.html
@@ -0,0 +1,29 @@
+ <!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8" />
+ <title>XHR2 Timeout Property Tests in Worker</title>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-timeout-attribute" data-tested-assertations="following::ol[1]/li[2]" />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#handler-xhr-ontimeout" data-tested-assertations="../.."/>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#timeout-error" data-tested-assertations=".."/>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#request-error" data-tested-assertations="following::ol[1]/li[9]"/>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#infrastructure-for-the-send()-method" data-tested-assertations="following-sibling::dl//code[contains(@title,'dom-XMLHttpRequest-timeout')]/.. following-sibling::dl//code[contains(@title,'dom-XMLHttpRequest-timeout')]/../following-sibling::dd following::dt[1] following::dd[1]" />
+ <meta name=timeout content=long>
+ <meta name=variant content="?no time out scheduled, load fires normally">
+ <meta name=variant content="?load fires normally">
+ <meta name=variant content="?timeout hit before load">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="resources/xmlhttprequest-timeout-runner.js"></script>
+</head>
+<body>
+ <h1>Description</h1>
+ <p>This test validates that the XHR2 timeout property behaves as expected in in a worker context.</p>
+ <div id="log"></div>
+ <script type="text/javascript">
+ var worker = new Worker("resources/xmlhttprequest-timeout-simple.js" + location.search);
+ worker.onmessage = testResultCallbackHandler;
+ </script>
+</body>
+</html>
+
diff --git a/test/wpt/tests/xhr/xmlhttprequest-timeout-worker-synconworker.html b/test/wpt/tests/xhr/xmlhttprequest-timeout-worker-synconworker.html
new file mode 100644
index 0000000..187db12
--- /dev/null
+++ b/test/wpt/tests/xhr/xmlhttprequest-timeout-worker-synconworker.html
@@ -0,0 +1,28 @@
+ <!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8" />
+ <title>XHR2 Timeout Property Tests in Worker</title>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-timeout-attribute" data-tested-assertations="following::ol[1]/li[2]" />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#handler-xhr-ontimeout" data-tested-assertations="../.."/>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#timeout-error" data-tested-assertations=".."/>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#request-error" data-tested-assertations="following::ol[1]/li[9]"/>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#infrastructure-for-the-send()-method" data-tested-assertations="following-sibling::dl//code[contains(@title,'dom-XMLHttpRequest-timeout')]/.. following-sibling::dl//code[contains(@title,'dom-XMLHttpRequest-timeout')]/../following-sibling::dd following::dt[1] following::dd[1]" />
+ <meta name=timeout content=long>
+ <meta name=variant content="?no time out scheduled, load fires normally">
+ <meta name=variant content="?load fires normally">
+ <meta name=variant content="?timeout hit before load">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="resources/xmlhttprequest-timeout-runner.js"></script>
+</head>
+<body>
+ <h1>Description</h1>
+ <p>This test validates that the XHR2 timeout property behaves as expected in in a worker context.</p>
+ <div id="log"></div>
+ <script type="text/javascript">
+ var worker = new Worker("resources/xmlhttprequest-timeout-synconworker.js" + location.search);
+ worker.addEventListener("message", testResultCallbackHandler);
+ </script>
+</body>
+</html>
diff --git a/test/wpt/tests/xhr/xmlhttprequest-timeout-worker-twice.html b/test/wpt/tests/xhr/xmlhttprequest-timeout-worker-twice.html
new file mode 100644
index 0000000..f9ad25a
--- /dev/null
+++ b/test/wpt/tests/xhr/xmlhttprequest-timeout-worker-twice.html
@@ -0,0 +1,29 @@
+ <!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8" />
+ <title>XHR2 Timeout Property Tests in Worker</title>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-timeout-attribute" data-tested-assertations="following::ol[1]/li[2]" />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#handler-xhr-ontimeout" data-tested-assertations="../.."/>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#timeout-error" data-tested-assertations=".."/>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#request-error" data-tested-assertations="following::ol[1]/li[9]"/>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#infrastructure-for-the-send()-method" data-tested-assertations="following-sibling::dl//code[contains(@title,'dom-XMLHttpRequest-timeout')]/.. following-sibling::dl//code[contains(@title,'dom-XMLHttpRequest-timeout')]/../following-sibling::dd following::dt[1] following::dd[1]" />
+ <meta name=timeout content=long>
+ <meta name=variant content="?load fires normally with no timeout set, twice">
+ <meta name=variant content="?load fires normally with same timeout set twice">
+ <meta name=variant content="?timeout fires normally with same timeout set twice">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="resources/xmlhttprequest-timeout-runner.js"></script>
+</head>
+<body>
+ <h1>Description</h1>
+ <p>This test validates that the XHR2 timeout property behaves as expected in in a worker context.</p>
+ <div id="log"></div>
+ <script type="text/javascript">
+ var worker = new Worker("resources/xmlhttprequest-timeout-twice.js" + location.search);
+ worker.addEventListener("message", testResultCallbackHandler);
+ </script>
+</body>
+</html>
+
diff --git a/test/wpt/tests/xhr/xmlhttprequest-unsent.htm b/test/wpt/tests/xhr/xmlhttprequest-unsent.htm
new file mode 100644
index 0000000..82282b0
--- /dev/null
+++ b/test/wpt/tests/xhr/xmlhttprequest-unsent.htm
@@ -0,0 +1,36 @@
+<!doctype html>
+<html>
+ <head>
+ <title>XMLHttpRequest: members during UNSENT</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <link rel="help" href="https://xhr.spec.whatwg.org/#dom-xmlhttprequest-unsent" data-tested-assertations=".. following::dd" />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#dom-xmlhttprequest-setrequestheader" data-tested-assertations="following::ol/li[1]" />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-send()-method" data-tested-assertations="following::ol/li[1]" />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-status-attribute" data-tested-assertations="following::ol/li[1]" />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-statustext-attribute" data-tested-assertations="following::ol/li[1]" />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-getresponseheader()-method" data-tested-assertations="following::ol/li[1]" />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-getallresponseheaders()-method" data-tested-assertations="following::ol/li[1]" />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-responsetext-attribute" data-tested-assertations="following::ol/li[2]" />
+ <link rel="help" href="https://xhr.spec.whatwg.org/#the-responsexml-attribute" data-tested-assertations="following::ol/li[2]" />
+
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+ test(function() {
+ var client = new XMLHttpRequest()
+ assert_throws_dom("InvalidStateError", function() { client.setRequestHeader("x-test", "test") }, "setRequestHeader")
+ assert_throws_dom("InvalidStateError", function() { client.send(null) }, "send")
+ assert_equals(client.status, 0, "status")
+ assert_equals(client.statusText, "", "statusText")
+ assert_equals(client.getAllResponseHeaders(), "", "getAllResponseHeaders")
+ assert_equals(client.getResponseHeader("x-test"), null, "getResponseHeader")
+ assert_equals(client.responseText, "", "responseText")
+ assert_equals(client.responseXML, null, "responseXML")
+
+ assert_equals(client.readyState, client.UNSENT, "readyState")
+ })
+ </script>
+ </body>
+</html>
diff --git a/types/README.md b/types/README.md
new file mode 100644
index 0000000..20a721c
--- /dev/null
+++ b/types/README.md
@@ -0,0 +1,6 @@
+# undici-types
+
+This package is a dual-publish of the [undici](https://www.npmjs.com/package/undici) library types. The `undici` package **still contains types**. This package is for users who _only_ need undici types (such as for `@types/node`). It is published alongside every release of `undici`, so you can always use the same version.
+
+- [GitHub nodejs/undici](https://github.com/nodejs/undici)
+- [Undici Documentation](https://undici.nodejs.org/#/)
diff --git a/types/agent.d.ts b/types/agent.d.ts
new file mode 100644
index 0000000..58081ce
--- /dev/null
+++ b/types/agent.d.ts
@@ -0,0 +1,31 @@
+import { URL } from 'url'
+import Pool from './pool'
+import Dispatcher from "./dispatcher";
+
+export default Agent
+
+declare class Agent extends Dispatcher{
+ constructor(opts?: Agent.Options)
+ /** `true` after `dispatcher.close()` has been called. */
+ closed: boolean;
+ /** `true` after `dispatcher.destroyed()` has been called or `dispatcher.close()` has been called and the dispatcher shutdown has completed. */
+ destroyed: boolean;
+ /** Dispatches a request. */
+ dispatch(options: Agent.DispatchOptions, handler: Dispatcher.DispatchHandlers): boolean;
+}
+
+declare namespace Agent {
+ export interface Options extends Pool.Options {
+ /** Default: `(origin, opts) => new Pool(origin, opts)`. */
+ factory?(origin: string | URL, opts: Object): Dispatcher;
+ /** Integer. Default: `0` */
+ maxRedirections?: number;
+
+ interceptors?: { Agent?: readonly Dispatcher.DispatchInterceptor[] } & Pool.Options["interceptors"]
+ }
+
+ export interface DispatchOptions extends Dispatcher.DispatchOptions {
+ /** Integer. */
+ maxRedirections?: number;
+ }
+}
diff --git a/types/api.d.ts b/types/api.d.ts
new file mode 100644
index 0000000..400341d
--- /dev/null
+++ b/types/api.d.ts
@@ -0,0 +1,43 @@
+import { URL, UrlObject } from 'url'
+import { Duplex } from 'stream'
+import Dispatcher from './dispatcher'
+
+export {
+ request,
+ stream,
+ pipeline,
+ connect,
+ upgrade,
+}
+
+/** Performs an HTTP request. */
+declare function request(
+ url: string | URL | UrlObject,
+ options?: { dispatcher?: Dispatcher } & Omit<Dispatcher.RequestOptions, 'origin' | 'path' | 'method'> & Partial<Pick<Dispatcher.RequestOptions, 'method'>>,
+): Promise<Dispatcher.ResponseData>;
+
+/** A faster version of `request`. */
+declare function stream(
+ url: string | URL | UrlObject,
+ options: { dispatcher?: Dispatcher } & Omit<Dispatcher.RequestOptions, 'origin' | 'path'>,
+ factory: Dispatcher.StreamFactory
+): Promise<Dispatcher.StreamData>;
+
+/** For easy use with `stream.pipeline`. */
+declare function pipeline(
+ url: string | URL | UrlObject,
+ options: { dispatcher?: Dispatcher } & Omit<Dispatcher.PipelineOptions, 'origin' | 'path'>,
+ handler: Dispatcher.PipelineHandler
+): Duplex;
+
+/** Starts two-way communications with the requested resource. */
+declare function connect(
+ url: string | URL | UrlObject,
+ options?: { dispatcher?: Dispatcher } & Omit<Dispatcher.ConnectOptions, 'origin' | 'path'>
+): Promise<Dispatcher.ConnectData>;
+
+/** Upgrade to a different protocol. */
+declare function upgrade(
+ url: string | URL | UrlObject,
+ options?: { dispatcher?: Dispatcher } & Omit<Dispatcher.UpgradeOptions, 'origin' | 'path'>
+): Promise<Dispatcher.UpgradeData>;
diff --git a/types/balanced-pool.d.ts b/types/balanced-pool.d.ts
new file mode 100644
index 0000000..d1e9375
--- /dev/null
+++ b/types/balanced-pool.d.ts
@@ -0,0 +1,18 @@
+import Pool from './pool'
+import Dispatcher from './dispatcher'
+import { URL } from 'url'
+
+export default BalancedPool
+
+declare class BalancedPool extends Dispatcher {
+ constructor(url: string | string[] | URL | URL[], options?: Pool.Options);
+
+ addUpstream(upstream: string | URL): BalancedPool;
+ removeUpstream(upstream: string | URL): BalancedPool;
+ upstreams: Array<string>;
+
+ /** `true` after `pool.close()` has been called. */
+ closed: boolean;
+ /** `true` after `pool.destroyed()` has been called or `pool.close()` has been called and the pool shutdown has completed. */
+ destroyed: boolean;
+}
diff --git a/types/cache.d.ts b/types/cache.d.ts
new file mode 100644
index 0000000..4c33335
--- /dev/null
+++ b/types/cache.d.ts
@@ -0,0 +1,36 @@
+import type { RequestInfo, Response, Request } from './fetch'
+
+export interface CacheStorage {
+ match (request: RequestInfo, options?: MultiCacheQueryOptions): Promise<Response | undefined>,
+ has (cacheName: string): Promise<boolean>,
+ open (cacheName: string): Promise<Cache>,
+ delete (cacheName: string): Promise<boolean>,
+ keys (): Promise<string[]>
+}
+
+declare const CacheStorage: {
+ prototype: CacheStorage
+ new(): CacheStorage
+}
+
+export interface Cache {
+ match (request: RequestInfo, options?: CacheQueryOptions): Promise<Response | undefined>,
+ matchAll (request?: RequestInfo, options?: CacheQueryOptions): Promise<readonly Response[]>,
+ add (request: RequestInfo): Promise<undefined>,
+ addAll (requests: RequestInfo[]): Promise<undefined>,
+ put (request: RequestInfo, response: Response): Promise<undefined>,
+ delete (request: RequestInfo, options?: CacheQueryOptions): Promise<boolean>,
+ keys (request?: RequestInfo, options?: CacheQueryOptions): Promise<readonly Request[]>
+}
+
+export interface CacheQueryOptions {
+ ignoreSearch?: boolean,
+ ignoreMethod?: boolean,
+ ignoreVary?: boolean
+}
+
+export interface MultiCacheQueryOptions extends CacheQueryOptions {
+ cacheName?: string
+}
+
+export declare const caches: CacheStorage
diff --git a/types/client.d.ts b/types/client.d.ts
new file mode 100644
index 0000000..56e78cc
--- /dev/null
+++ b/types/client.d.ts
@@ -0,0 +1,97 @@
+import { URL } from 'url'
+import { TlsOptions } from 'tls'
+import Dispatcher from './dispatcher'
+import buildConnector from "./connector";
+
+/**
+ * A basic HTTP/1.1 client, mapped on top a single TCP/TLS connection. Pipelining is disabled by default.
+ */
+export class Client extends Dispatcher {
+ constructor(url: string | URL, options?: Client.Options);
+ /** Property to get and set the pipelining factor. */
+ pipelining: number;
+ /** `true` after `client.close()` has been called. */
+ closed: boolean;
+ /** `true` after `client.destroyed()` has been called or `client.close()` has been called and the client shutdown has completed. */
+ destroyed: boolean;
+}
+
+export declare namespace Client {
+ export interface OptionsInterceptors {
+ Client: readonly Dispatcher.DispatchInterceptor[];
+ }
+ export interface Options {
+ /** TODO */
+ interceptors?: OptionsInterceptors;
+ /** The maximum length of request headers in bytes. Default: Node.js' `--max-http-header-size` or `16384` (16KiB). */
+ maxHeaderSize?: number;
+ /** The amount of time, in milliseconds, the parser will wait to receive the complete HTTP headers (Node 14 and above only). Default: `300e3` milliseconds (300s). */
+ headersTimeout?: number;
+ /** @deprecated unsupported socketTimeout, use headersTimeout & bodyTimeout instead */
+ socketTimeout?: never;
+ /** @deprecated unsupported requestTimeout, use headersTimeout & bodyTimeout instead */
+ requestTimeout?: never;
+ /** TODO */
+ connectTimeout?: number;
+ /** The timeout after which a request will time out, in milliseconds. Monitors time between receiving body data. Use `0` to disable it entirely. Default: `300e3` milliseconds (300s). */
+ bodyTimeout?: number;
+ /** @deprecated unsupported idleTimeout, use keepAliveTimeout instead */
+ idleTimeout?: never;
+ /** @deprecated unsupported keepAlive, use pipelining=0 instead */
+ keepAlive?: never;
+ /** the timeout, in milliseconds, after which a socket without active requests will time out. Monitors time between activity on a connected socket. This value may be overridden by *keep-alive* hints from the server. Default: `4e3` milliseconds (4s). */
+ keepAliveTimeout?: number;
+ /** @deprecated unsupported maxKeepAliveTimeout, use keepAliveMaxTimeout instead */
+ maxKeepAliveTimeout?: never;
+ /** the maximum allowed `idleTimeout`, in milliseconds, when overridden by *keep-alive* hints from the server. Default: `600e3` milliseconds (10min). */
+ keepAliveMaxTimeout?: number;
+ /** A number of milliseconds subtracted from server *keep-alive* hints when overriding `idleTimeout` to account for timing inaccuracies caused by e.g. transport latency. Default: `1e3` milliseconds (1s). */
+ keepAliveTimeoutThreshold?: number;
+ /** TODO */
+ socketPath?: string;
+ /** The amount of concurrent requests to be sent over the single TCP/TLS connection according to [RFC7230](https://tools.ietf.org/html/rfc7230#section-6.3.2). Default: `1`. */
+ pipelining?: number;
+ /** @deprecated use the connect option instead */
+ tls?: never;
+ /** If `true`, an error is thrown when the request content-length header doesn't match the length of the request body. Default: `true`. */
+ strictContentLength?: boolean;
+ /** TODO */
+ maxCachedSessions?: number;
+ /** TODO */
+ maxRedirections?: number;
+ /** TODO */
+ connect?: buildConnector.BuildOptions | buildConnector.connector;
+ /** TODO */
+ maxRequestsPerClient?: number;
+ /** TODO */
+ localAddress?: string;
+ /** Max response body size in bytes, -1 is disabled */
+ maxResponseSize?: number;
+ /** Enables a family autodetection algorithm that loosely implements section 5 of RFC 8305. */
+ autoSelectFamily?: boolean;
+ /** The amount of time in milliseconds to wait for a connection attempt to finish before trying the next address when using the `autoSelectFamily` option. */
+ autoSelectFamilyAttemptTimeout?: number;
+ /**
+ * @description Enables support for H2 if the server has assigned bigger priority to it through ALPN negotiation.
+ * @default false
+ */
+ allowH2?: boolean;
+ /**
+ * @description Dictates the maximum number of concurrent streams for a single H2 session. It can be overridden by a SETTINGS remote frame.
+ * @default 100
+ */
+ maxConcurrentStreams?: number
+ }
+ export interface SocketInfo {
+ localAddress?: string
+ localPort?: number
+ remoteAddress?: string
+ remotePort?: number
+ remoteFamily?: string
+ timeout?: number
+ bytesWritten?: number
+ bytesRead?: number
+ }
+}
+
+export default Client;
diff --git a/types/connector.d.ts b/types/connector.d.ts
new file mode 100644
index 0000000..bd92433
--- /dev/null
+++ b/types/connector.d.ts
@@ -0,0 +1,34 @@
+import { TLSSocket, ConnectionOptions } from 'tls'
+import { IpcNetConnectOpts, Socket, TcpNetConnectOpts } from 'net'
+
+export default buildConnector
+declare function buildConnector (options?: buildConnector.BuildOptions): buildConnector.connector
+
+declare namespace buildConnector {
+ export type BuildOptions = (ConnectionOptions | TcpNetConnectOpts | IpcNetConnectOpts) & {
+ allowH2?: boolean;
+ maxCachedSessions?: number | null;
+ socketPath?: string | null;
+ timeout?: number | null;
+ port?: number;
+ keepAlive?: boolean | null;
+ keepAliveInitialDelay?: number | null;
+ }
+
+ export interface Options {
+ hostname: string
+ host?: string
+ protocol: string
+ port: string
+ servername?: string
+ localAddress?: string | null
+ httpSocket?: Socket
+ }
+
+ export type Callback = (...args: CallbackArgs) => void
+ type CallbackArgs = [null, Socket | TLSSocket] | [Error, null]
+
+ export interface connector {
+ (options: buildConnector.Options, callback: buildConnector.Callback): void
+ }
+}
diff --git a/types/content-type.d.ts b/types/content-type.d.ts
new file mode 100644
index 0000000..f2a87f1
--- /dev/null
+++ b/types/content-type.d.ts
@@ -0,0 +1,21 @@
+/// <reference types="node" />
+
+interface MIMEType {
+ type: string
+ subtype: string
+ parameters: Map<string, string>
+ essence: string
+}
+
+/**
+ * Parse a string to a {@link MIMEType} object. Returns `failure` if the string
+ * couldn't be parsed.
+ * @see https://mimesniff.spec.whatwg.org/#parse-a-mime-type
+ */
+export function parseMIMEType (input: string): 'failure' | MIMEType
+
+/**
+ * Convert a MIMEType object to a string.
+ * @see https://mimesniff.spec.whatwg.org/#serialize-a-mime-type
+ */
+export function serializeAMimeType (mimeType: MIMEType): string
diff --git a/types/cookies.d.ts b/types/cookies.d.ts
new file mode 100644
index 0000000..aa38cae
--- /dev/null
+++ b/types/cookies.d.ts
@@ -0,0 +1,28 @@
+/// <reference types="node" />
+
+import type { Headers } from './fetch'
+
+export interface Cookie {
+ name: string
+ value: string
+ expires?: Date | number
+ maxAge?: number
+ domain?: string
+ path?: string
+ secure?: boolean
+ httpOnly?: boolean
+ sameSite?: 'Strict' | 'Lax' | 'None'
+ unparsed?: string[]
+}
+
+export function deleteCookie (
+ headers: Headers,
+ name: string,
+ attributes?: { name?: string, domain?: string }
+): void
+
+export function getCookies (headers: Headers): Record<string, string>
+
+export function getSetCookies (headers: Headers): Cookie[]
+
+export function setCookie (headers: Headers, cookie: Cookie): void
diff --git a/types/diagnostics-channel.d.ts b/types/diagnostics-channel.d.ts
new file mode 100644
index 0000000..85d4482
--- /dev/null
+++ b/types/diagnostics-channel.d.ts
@@ -0,0 +1,67 @@
+import { Socket } from "net";
+import { URL } from "url";
+import Connector from "./connector";
+import Dispatcher from "./dispatcher";
+
+declare namespace DiagnosticsChannel {
+ interface Request {
+ origin?: string | URL;
+ completed: boolean;
+ method?: Dispatcher.HttpMethod;
+ path: string;
+ headers: string;
+ addHeader(key: string, value: string): Request;
+ }
+ interface Response {
+ statusCode: number;
+ statusText: string;
+ headers: Array<Buffer>;
+ }
+ type Error = unknown;
+ interface ConnectParams {
+ host: URL["host"];
+ hostname: URL["hostname"];
+ protocol: URL["protocol"];
+ port: URL["port"];
+ servername: string | null;
+ }
+ type Connector = Connector.connector;
+ export interface RequestCreateMessage {
+ request: Request;
+ }
+ export interface RequestBodySentMessage {
+ request: Request;
+ }
+ export interface RequestHeadersMessage {
+ request: Request;
+ response: Response;
+ }
+ export interface RequestTrailersMessage {
+ request: Request;
+ trailers: Array<Buffer>;
+ }
+ export interface RequestErrorMessage {
+ request: Request;
+ error: Error;
+ }
+ export interface ClientSendHeadersMessage {
+ request: Request;
+ headers: string;
+ socket: Socket;
+ }
+ export interface ClientBeforeConnectMessage {
+ connectParams: ConnectParams;
+ connector: Connector;
+ }
+ export interface ClientConnectedMessage {
+ socket: Socket;
+ connectParams: ConnectParams;
+ connector: Connector;
+ }
+ export interface ClientConnectErrorMessage {
+ error: Error;
+ socket: Socket;
+ connectParams: ConnectParams;
+ connector: Connector;
+ }
+}
diff --git a/types/dispatcher.d.ts b/types/dispatcher.d.ts
new file mode 100644
index 0000000..efc53ee
--- /dev/null
+++ b/types/dispatcher.d.ts
@@ -0,0 +1,241 @@
+import { URL } from 'url'
+import { Duplex, Readable, Writable } from 'stream'
+import { EventEmitter } from 'events'
+import { Blob } from 'buffer'
+import { IncomingHttpHeaders } from './header'
+import BodyReadable from './readable'
+import { FormData } from './formdata'
+import Errors from './errors'
+
+type AbortSignal = unknown;
+
+export default Dispatcher
+
+/** Dispatcher is the core API used to dispatch requests. */
+declare class Dispatcher extends EventEmitter {
+ /** Dispatches a request. This API is expected to evolve through semver-major versions and is less stable than the preceding higher level APIs. It is primarily intended for library developers who implement higher level APIs on top of this. */
+ dispatch(options: Dispatcher.DispatchOptions, handler: Dispatcher.DispatchHandlers): boolean;
+ /** Starts two-way communications with the requested resource. */
+ connect(options: Dispatcher.ConnectOptions): Promise<Dispatcher.ConnectData>;
+ connect(options: Dispatcher.ConnectOptions, callback: (err: Error | null, data: Dispatcher.ConnectData) => void): void;
+ /** Performs an HTTP request. */
+ request(options: Dispatcher.RequestOptions): Promise<Dispatcher.ResponseData>;
+ request(options: Dispatcher.RequestOptions, callback: (err: Error | null, data: Dispatcher.ResponseData) => void): void;
+ /** For easy use with `stream.pipeline`. */
+ pipeline(options: Dispatcher.PipelineOptions, handler: Dispatcher.PipelineHandler): Duplex;
+ /** A faster version of `Dispatcher.request`. */
+ stream(options: Dispatcher.RequestOptions, factory: Dispatcher.StreamFactory): Promise<Dispatcher.StreamData>;
+ stream(options: Dispatcher.RequestOptions, factory: Dispatcher.StreamFactory, callback: (err: Error | null, data: Dispatcher.StreamData) => void): void;
+ /** Upgrade to a different protocol. */
+ upgrade(options: Dispatcher.UpgradeOptions): Promise<Dispatcher.UpgradeData>;
+ upgrade(options: Dispatcher.UpgradeOptions, callback: (err: Error | null, data: Dispatcher.UpgradeData) => void): void;
+ /** Closes the client and gracefully waits for enqueued requests to complete before invoking the callback (or returning a promise if no callback is provided). */
+ close(): Promise<void>;
+ close(callback: () => void): void;
+ /** Destroy the client abruptly with the given err. All the pending and running requests will be asynchronously aborted and error. Waits until socket is closed before invoking the callback (or returning a promise if no callback is provided). Since this operation is asynchronously dispatched there might still be some progress on dispatched requests. */
+ destroy(): Promise<void>;
+ destroy(err: Error | null): Promise<void>;
+ destroy(callback: () => void): void;
+ destroy(err: Error | null, callback: () => void): void;
+
+ on(eventName: 'connect', callback: (origin: URL, targets: readonly Dispatcher[]) => void): this;
+ on(eventName: 'disconnect', callback: (origin: URL, targets: readonly Dispatcher[], error: Errors.UndiciError) => void): this;
+ on(eventName: 'connectionError', callback: (origin: URL, targets: readonly Dispatcher[], error: Errors.UndiciError) => void): this;
+ on(eventName: 'drain', callback: (origin: URL) => void): this;
+
+
+ once(eventName: 'connect', callback: (origin: URL, targets: readonly Dispatcher[]) => void): this;
+ once(eventName: 'disconnect', callback: (origin: URL, targets: readonly Dispatcher[], error: Errors.UndiciError) => void): this;
+ once(eventName: 'connectionError', callback: (origin: URL, targets: readonly Dispatcher[], error: Errors.UndiciError) => void): this;
+ once(eventName: 'drain', callback: (origin: URL) => void): this;
+
+
+ off(eventName: 'connect', callback: (origin: URL, targets: readonly Dispatcher[]) => void): this;
+ off(eventName: 'disconnect', callback: (origin: URL, targets: readonly Dispatcher[], error: Errors.UndiciError) => void): this;
+ off(eventName: 'connectionError', callback: (origin: URL, targets: readonly Dispatcher[], error: Errors.UndiciError) => void): this;
+ off(eventName: 'drain', callback: (origin: URL) => void): this;
+
+
+ addListener(eventName: 'connect', callback: (origin: URL, targets: readonly Dispatcher[]) => void): this;
+ addListener(eventName: 'disconnect', callback: (origin: URL, targets: readonly Dispatcher[], error: Errors.UndiciError) => void): this;
+ addListener(eventName: 'connectionError', callback: (origin: URL, targets: readonly Dispatcher[], error: Errors.UndiciError) => void): this;
+ addListener(eventName: 'drain', callback: (origin: URL) => void): this;
+
+ removeListener(eventName: 'connect', callback: (origin: URL, targets: readonly Dispatcher[]) => void): this;
+ removeListener(eventName: 'disconnect', callback: (origin: URL, targets: readonly Dispatcher[], error: Errors.UndiciError) => void): this;
+ removeListener(eventName: 'connectionError', callback: (origin: URL, targets: readonly Dispatcher[], error: Errors.UndiciError) => void): this;
+ removeListener(eventName: 'drain', callback: (origin: URL) => void): this;
+
+ prependListener(eventName: 'connect', callback: (origin: URL, targets: readonly Dispatcher[]) => void): this;
+ prependListener(eventName: 'disconnect', callback: (origin: URL, targets: readonly Dispatcher[], error: Errors.UndiciError) => void): this;
+ prependListener(eventName: 'connectionError', callback: (origin: URL, targets: readonly Dispatcher[], error: Errors.UndiciError) => void): this;
+ prependListener(eventName: 'drain', callback: (origin: URL) => void): this;
+
+ prependOnceListener(eventName: 'connect', callback: (origin: URL, targets: readonly Dispatcher[]) => void): this;
+ prependOnceListener(eventName: 'disconnect', callback: (origin: URL, targets: readonly Dispatcher[], error: Errors.UndiciError) => void): this;
+ prependOnceListener(eventName: 'connectionError', callback: (origin: URL, targets: readonly Dispatcher[], error: Errors.UndiciError) => void): this;
+ prependOnceListener(eventName: 'drain', callback: (origin: URL) => void): this;
+
+ listeners(eventName: 'connect'): ((origin: URL, targets: readonly Dispatcher[]) => void)[]
+ listeners(eventName: 'disconnect'): ((origin: URL, targets: readonly Dispatcher[], error: Errors.UndiciError) => void)[];
+ listeners(eventName: 'connectionError'): ((origin: URL, targets: readonly Dispatcher[], error: Errors.UndiciError) => void)[];
+ listeners(eventName: 'drain'): ((origin: URL) => void)[];
+
+ rawListeners(eventName: 'connect'): ((origin: URL, targets: readonly Dispatcher[]) => void)[]
+ rawListeners(eventName: 'disconnect'): ((origin: URL, targets: readonly Dispatcher[], error: Errors.UndiciError) => void)[];
+ rawListeners(eventName: 'connectionError'): ((origin: URL, targets: readonly Dispatcher[], error: Errors.UndiciError) => void)[];
+ rawListeners(eventName: 'drain'): ((origin: URL) => void)[];
+
+ emit(eventName: 'connect', origin: URL, targets: readonly Dispatcher[]): boolean;
+ emit(eventName: 'disconnect', origin: URL, targets: readonly Dispatcher[], error: Errors.UndiciError): boolean;
+ emit(eventName: 'connectionError', origin: URL, targets: readonly Dispatcher[], error: Errors.UndiciError): boolean;
+ emit(eventName: 'drain', origin: URL): boolean;
+}
+
+declare namespace Dispatcher {
+ export interface DispatchOptions {
+ origin?: string | URL;
+ path: string;
+ method: HttpMethod;
+ /** Default: `null` */
+ body?: string | Buffer | Uint8Array | Readable | null | FormData;
+ /** Default: `null` */
+ headers?: IncomingHttpHeaders | string[] | null;
+ /** Query string params to be embedded in the request URL. Default: `null` */
+ query?: Record<string, any>;
+ /** Whether the requests can be safely retried or not. If `false` the request won't be sent until all preceding requests in the pipeline have completed. Default: `true` if `method` is `HEAD` or `GET`. */
+ idempotent?: boolean;
+ /** Whether the response is expected to take a long time and would end up blocking the pipeline. When this is set to `true` further pipelining will be avoided on the same connection until headers have been received. */
+ blocking?: boolean;
+ /** Upgrade the request. Should be used to specify the kind of upgrade i.e. `'Websocket'`. Default: `method === 'CONNECT' || null`. */
+ upgrade?: boolean | string | null;
+ /** The amount of time, in milliseconds, the parser will wait to receive the complete HTTP headers. Defaults to 300 seconds. */
+ headersTimeout?: number | null;
+ /** The timeout after which a request will time out, in milliseconds. Monitors time between receiving body data. Use 0 to disable it entirely. Defaults to 300 seconds. */
+ bodyTimeout?: number | null;
+ /** Whether the request should stablish a keep-alive or not. Default `false` */
+ reset?: boolean;
+ /** Whether Undici should throw an error upon receiving a 4xx or 5xx response from the server. Defaults to false */
+ throwOnError?: boolean;
+ /** For H2, it appends the expect: 100-continue header, and halts the request body until a 100-continue is received from the remote server*/
+ expectContinue?: boolean;
+ }
+ export interface ConnectOptions {
+ path: string;
+ /** Default: `null` */
+ headers?: IncomingHttpHeaders | string[] | null;
+ /** Default: `null` */
+ signal?: AbortSignal | EventEmitter | null;
+ /** This argument parameter is passed through to `ConnectData` */
+ opaque?: unknown;
+ /** Default: 0 */
+ maxRedirections?: number;
+ /** Default: `null` */
+ responseHeader?: 'raw' | null;
+ }
+ export interface RequestOptions extends DispatchOptions {
+ /** Default: `null` */
+ opaque?: unknown;
+ /** Default: `null` */
+ signal?: AbortSignal | EventEmitter | null;
+ /** Default: 0 */
+ maxRedirections?: number;
+ /** Default: `null` */
+ onInfo?: (info: { statusCode: number, headers: Record<string, string | string[]> }) => void;
+ /** Default: `null` */
+ responseHeader?: 'raw' | null;
+ /** Default: `64 KiB` */
+ highWaterMark?: number;
+ }
+ export interface PipelineOptions extends RequestOptions {
+ /** `true` if the `handler` will return an object stream. Default: `false` */
+ objectMode?: boolean;
+ }
+ export interface UpgradeOptions {
+ path: string;
+ /** Default: `'GET'` */
+ method?: string;
+ /** Default: `null` */
+ headers?: IncomingHttpHeaders | string[] | null;
+ /** A string of comma separated protocols, in descending preference order. Default: `'Websocket'` */
+ protocol?: string;
+ /** Default: `null` */
+ signal?: AbortSignal | EventEmitter | null;
+ /** Default: 0 */
+ maxRedirections?: number;
+ /** Default: `null` */
+ responseHeader?: 'raw' | null;
+ }
+ export interface ConnectData {
+ statusCode: number;
+ headers: IncomingHttpHeaders;
+ socket: Duplex;
+ opaque: unknown;
+ }
+ export interface ResponseData {
+ statusCode: number;
+ headers: IncomingHttpHeaders;
+ body: BodyReadable & BodyMixin;
+ trailers: Record<string, string>;
+ opaque: unknown;
+ context: object;
+ }
+ export interface PipelineHandlerData {
+ statusCode: number;
+ headers: IncomingHttpHeaders;
+ opaque: unknown;
+ body: BodyReadable;
+ context: object;
+ }
+ export interface StreamData {
+ opaque: unknown;
+ trailers: Record<string, string>;
+ }
+ export interface UpgradeData {
+ headers: IncomingHttpHeaders;
+ socket: Duplex;
+ opaque: unknown;
+ }
+ export interface StreamFactoryData {
+ statusCode: number;
+ headers: IncomingHttpHeaders;
+ opaque: unknown;
+ context: object;
+ }
+ export type StreamFactory = (data: StreamFactoryData) => Writable;
+ export interface DispatchHandlers {
+ /** Invoked before request is dispatched on socket. May be invoked multiple times when a request is retried when the request at the head of the pipeline fails. */
+ onConnect?(abort: () => void): void;
+ /** Invoked when an error has occurred. */
+ onError?(err: Error): void;
+ /** Invoked when request is upgraded either due to a `Upgrade` header or `CONNECT` method. */
+ onUpgrade?(statusCode: number, headers: Buffer[] | string[] | null, socket: Duplex): void;
+ /** Invoked when statusCode and headers have been received. May be invoked multiple times due to 1xx informational headers. */
+ onHeaders?(statusCode: number, headers: Buffer[] | string[] | null, resume: () => void, statusText: string): boolean;
+ /** Invoked when response payload data is received. */
+ onData?(chunk: Buffer): boolean;
+ /** Invoked when response payload and trailers have been received and the request has completed. */
+ onComplete?(trailers: string[] | null): void;
+ /** Invoked when a body chunk is sent to the server. May be invoked multiple times for chunked requests */
+ onBodySent?(chunkSize: number, totalBytesSent: number): void;
+ }
+ export type PipelineHandler = (data: PipelineHandlerData) => Readable;
+ export type HttpMethod = 'GET' | 'HEAD' | 'POST' | 'PUT' | 'DELETE' | 'CONNECT' | 'OPTIONS' | 'TRACE' | 'PATCH';
+
+ /**
+ * @link https://fetch.spec.whatwg.org/#body-mixin
+ */
+ interface BodyMixin {
+ readonly body?: never; // throws on node v16.6.0
+ readonly bodyUsed: boolean;
+ arrayBuffer(): Promise<ArrayBuffer>;
+ blob(): Promise<Blob>;
+ formData(): Promise<never>;
+ json(): Promise<unknown>;
+ text(): Promise<string>;
+ }
+
+ export interface DispatchInterceptor {
+ (dispatch: Dispatcher['dispatch']): Dispatcher['dispatch']
+ }
+}
diff --git a/types/errors.d.ts b/types/errors.d.ts
new file mode 100644
index 0000000..7923ddd
--- /dev/null
+++ b/types/errors.d.ts
@@ -0,0 +1,128 @@
+import { IncomingHttpHeaders } from "./header";
+import Client from './client'
+
+export default Errors
+
+declare namespace Errors {
+ export class UndiciError extends Error {
+ name: string;
+ code: string;
+ }
+
+ /** Connect timeout error. */
+ export class ConnectTimeoutError extends UndiciError {
+ name: 'ConnectTimeoutError';
+ code: 'UND_ERR_CONNECT_TIMEOUT';
+ }
+
+ /** A header exceeds the `headersTimeout` option. */
+ export class HeadersTimeoutError extends UndiciError {
+ name: 'HeadersTimeoutError';
+ code: 'UND_ERR_HEADERS_TIMEOUT';
+ }
+
+ /** Headers overflow error. */
+ export class HeadersOverflowError extends UndiciError {
+ name: 'HeadersOverflowError'
+ code: 'UND_ERR_HEADERS_OVERFLOW'
+ }
+
+ /** A body exceeds the `bodyTimeout` option. */
+ export class BodyTimeoutError extends UndiciError {
+ name: 'BodyTimeoutError';
+ code: 'UND_ERR_BODY_TIMEOUT';
+ }
+
+ export class ResponseStatusCodeError extends UndiciError {
+ constructor (
+ message?: string,
+ statusCode?: number,
+ headers?: IncomingHttpHeaders | string[] | null,
+ body?: null | Record<string, any> | string
+ );
+ name: 'ResponseStatusCodeError';
+ code: 'UND_ERR_RESPONSE_STATUS_CODE';
+ body: null | Record<string, any> | string
+ status: number
+ statusCode: number
+ headers: IncomingHttpHeaders | string[] | null;
+ }
+
+ /** Passed an invalid argument. */
+ export class InvalidArgumentError extends UndiciError {
+ name: 'InvalidArgumentError';
+ code: 'UND_ERR_INVALID_ARG';
+ }
+
+ /** Returned an invalid value. */
+ export class InvalidReturnValueError extends UndiciError {
+ name: 'InvalidReturnValueError';
+ code: 'UND_ERR_INVALID_RETURN_VALUE';
+ }
+
+ /** The request has been aborted by the user. */
+ export class RequestAbortedError extends UndiciError {
+ name: 'AbortError';
+ code: 'UND_ERR_ABORTED';
+ }
+
+ /** Expected error with reason. */
+ export class InformationalError extends UndiciError {
+ name: 'InformationalError';
+ code: 'UND_ERR_INFO';
+ }
+
+ /** Request body length does not match content-length header. */
+ export class RequestContentLengthMismatchError extends UndiciError {
+ name: 'RequestContentLengthMismatchError';
+ code: 'UND_ERR_REQ_CONTENT_LENGTH_MISMATCH';
+ }
+
+ /** Response body length does not match content-length header. */
+ export class ResponseContentLengthMismatchError extends UndiciError {
+ name: 'ResponseContentLengthMismatchError';
+ code: 'UND_ERR_RES_CONTENT_LENGTH_MISMATCH';
+ }
+
+ /** Trying to use a destroyed client. */
+ export class ClientDestroyedError extends UndiciError {
+ name: 'ClientDestroyedError';
+ code: 'UND_ERR_DESTROYED';
+ }
+
+ /** Trying to use a closed client. */
+ export class ClientClosedError extends UndiciError {
+ name: 'ClientClosedError';
+ code: 'UND_ERR_CLOSED';
+ }
+
+ /** There is an error with the socket. */
+ export class SocketError extends UndiciError {
+ name: 'SocketError';
+ code: 'UND_ERR_SOCKET';
+ socket: Client.SocketInfo | null
+ }
+
+ /** Encountered unsupported functionality. */
+ export class NotSupportedError extends UndiciError {
+ name: 'NotSupportedError';
+ code: 'UND_ERR_NOT_SUPPORTED';
+ }
+
+ /** No upstream has been added to the BalancedPool. */
+ export class BalancedPoolMissingUpstreamError extends UndiciError {
+ name: 'MissingUpstreamError';
+ code: 'UND_ERR_BPL_MISSING_UPSTREAM';
+ }
+
+ export class HTTPParserError extends UndiciError {
+ name: 'HTTPParserError';
+ code: string;
+ }
+
+ /** The response exceed the length allowed. */
+ export class ResponseExceededMaxSizeError extends UndiciError {
+ name: 'ResponseExceededMaxSizeError';
+ code: 'UND_ERR_RES_EXCEEDED_MAX_SIZE';
+ }
+}
diff --git a/types/fetch.d.ts b/types/fetch.d.ts
new file mode 100644
index 0000000..440f2b0
--- /dev/null
+++ b/types/fetch.d.ts
@@ -0,0 +1,209 @@
+// based on https://github.com/Ethan-Arrowood/undici-fetch/blob/249269714db874351589d2d364a0645d5160ae71/index.d.ts (MIT license)
+// and https://github.com/node-fetch/node-fetch/blob/914ce6be5ec67a8bab63d68510aabf07cb818b6d/index.d.ts (MIT license)
+/// <reference types="node" />
+
+import { Blob } from 'buffer'
+import { URL, URLSearchParams } from 'url'
+import { ReadableStream } from 'stream/web'
+import { FormData } from './formdata'
+
+import Dispatcher from './dispatcher'
+
+export type RequestInfo = string | URL | Request
+
+export declare function fetch (
+ input: RequestInfo,
+ init?: RequestInit
+): Promise<Response>
+
+export type BodyInit =
+ | ArrayBuffer
+ | AsyncIterable<Uint8Array>
+ | Blob
+ | FormData
+ | Iterable<Uint8Array>
+ | NodeJS.ArrayBufferView
+ | URLSearchParams
+ | null
+ | string
+
+export interface BodyMixin {
+ readonly body: ReadableStream | null
+ readonly bodyUsed: boolean
+
+ readonly arrayBuffer: () => Promise<ArrayBuffer>
+ readonly blob: () => Promise<Blob>
+ readonly formData: () => Promise<FormData>
+ readonly json: () => Promise<unknown>
+ readonly text: () => Promise<string>
+}
+
+export interface SpecIterator<T, TReturn = any, TNext = undefined> {
+ next(...args: [] | [TNext]): IteratorResult<T, TReturn>;
+}
+
+export interface SpecIterableIterator<T> extends SpecIterator<T> {
+ [Symbol.iterator](): SpecIterableIterator<T>;
+}
+
+export interface SpecIterable<T> {
+ [Symbol.iterator](): SpecIterator<T>;
+}
+
+export type HeadersInit = string[][] | Record<string, string | ReadonlyArray<string>> | Headers
+
+export declare class Headers implements SpecIterable<[string, string]> {
+ constructor (init?: HeadersInit)
+ readonly append: (name: string, value: string) => void
+ readonly delete: (name: string) => void
+ readonly get: (name: string) => string | null
+ readonly has: (name: string) => boolean
+ readonly set: (name: string, value: string) => void
+ readonly getSetCookie: () => string[]
+ readonly forEach: (
+ callbackfn: (value: string, key: string, iterable: Headers) => void,
+ thisArg?: unknown
+ ) => void
+
+ readonly keys: () => SpecIterableIterator<string>
+ readonly values: () => SpecIterableIterator<string>
+ readonly entries: () => SpecIterableIterator<[string, string]>
+ readonly [Symbol.iterator]: () => SpecIterator<[string, string]>
+}
+
+export type RequestCache =
+ | 'default'
+ | 'force-cache'
+ | 'no-cache'
+ | 'no-store'
+ | 'only-if-cached'
+ | 'reload'
+
+export type RequestCredentials = 'omit' | 'include' | 'same-origin'
+
+type RequestDestination =
+ | ''
+ | 'audio'
+ | 'audioworklet'
+ | 'document'
+ | 'embed'
+ | 'font'
+ | 'image'
+ | 'manifest'
+ | 'object'
+ | 'paintworklet'
+ | 'report'
+ | 'script'
+ | 'sharedworker'
+ | 'style'
+ | 'track'
+ | 'video'
+ | 'worker'
+ | 'xslt'
+
+export interface RequestInit {
+ method?: string
+ keepalive?: boolean
+ headers?: HeadersInit
+ body?: BodyInit
+ redirect?: RequestRedirect
+ integrity?: string
+ signal?: AbortSignal | null
+ credentials?: RequestCredentials
+ mode?: RequestMode
+ referrer?: string
+ referrerPolicy?: ReferrerPolicy
+ window?: null
+ dispatcher?: Dispatcher
+ duplex?: RequestDuplex
+}
+
+export type ReferrerPolicy =
+ | ''
+ | 'no-referrer'
+ | 'no-referrer-when-downgrade'
+ | 'origin'
+ | 'origin-when-cross-origin'
+ | 'same-origin'
+ | 'strict-origin'
+ | 'strict-origin-when-cross-origin'
+ | 'unsafe-url';
+
+export type RequestMode = 'cors' | 'navigate' | 'no-cors' | 'same-origin'
+
+export type RequestRedirect = 'error' | 'follow' | 'manual'
+
+export type RequestDuplex = 'half'
+
+export declare class Request implements BodyMixin {
+ constructor (input: RequestInfo, init?: RequestInit)
+
+ readonly cache: RequestCache
+ readonly credentials: RequestCredentials
+ readonly destination: RequestDestination
+ readonly headers: Headers
+ readonly integrity: string
+ readonly method: string
+ readonly mode: RequestMode
+ readonly redirect: RequestRedirect
+ readonly referrerPolicy: string
+ readonly url: string
+
+ readonly keepalive: boolean
+ readonly signal: AbortSignal
+ readonly duplex: RequestDuplex
+
+ readonly body: ReadableStream | null
+ readonly bodyUsed: boolean
+
+ readonly arrayBuffer: () => Promise<ArrayBuffer>
+ readonly blob: () => Promise<Blob>
+ readonly formData: () => Promise<FormData>
+ readonly json: () => Promise<unknown>
+ readonly text: () => Promise<string>
+
+ readonly clone: () => Request
+}
+
+export interface ResponseInit {
+ readonly status?: number
+ readonly statusText?: string
+ readonly headers?: HeadersInit
+}
+
+export type ResponseType =
+ | 'basic'
+ | 'cors'
+ | 'default'
+ | 'error'
+ | 'opaque'
+ | 'opaqueredirect'
+
+export type ResponseRedirectStatus = 301 | 302 | 303 | 307 | 308
+
+export declare class Response implements BodyMixin {
+ constructor (body?: BodyInit, init?: ResponseInit)
+
+ readonly headers: Headers
+ readonly ok: boolean
+ readonly status: number
+ readonly statusText: string
+ readonly type: ResponseType
+ readonly url: string
+ readonly redirected: boolean
+
+ readonly body: ReadableStream | null
+ readonly bodyUsed: boolean
+
+ readonly arrayBuffer: () => Promise<ArrayBuffer>
+ readonly blob: () => Promise<Blob>
+ readonly formData: () => Promise<FormData>
+ readonly json: () => Promise<unknown>
+ readonly text: () => Promise<string>
+
+ readonly clone: () => Response
+
+ static error (): Response
+ static json(data: any, init?: ResponseInit): Response
+ static redirect (url: string | URL, status: ResponseRedirectStatus): Response
+}
diff --git a/types/file.d.ts b/types/file.d.ts
new file mode 100644
index 0000000..c695b7a
--- /dev/null
+++ b/types/file.d.ts
@@ -0,0 +1,39 @@
+// Based on https://github.com/octet-stream/form-data/blob/2d0f0dc371517444ce1f22cdde13f51995d0953a/lib/File.ts (MIT)
+/// <reference types="node" />
+
+import { Blob } from 'buffer'
+
+export interface BlobPropertyBag {
+ type?: string
+ endings?: 'native' | 'transparent'
+}
+
+export interface FilePropertyBag extends BlobPropertyBag {
+ /**
+ * The last modified date of the file as the number of milliseconds since the Unix epoch (January 1, 1970 at midnight). Files without a known last modified date return the current date.
+ */
+ lastModified?: number
+}
+
+export declare class File extends Blob {
+ /**
+ * Creates a new File instance.
+ *
+ * @param fileBits An `Array` strings, or [`ArrayBuffer`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer), [`ArrayBufferView`](https://developer.mozilla.org/en-US/docs/Web/API/ArrayBufferView), [`Blob`](https://developer.mozilla.org/en-US/docs/Web/API/Blob) objects, or a mix of any of such objects, that will be put inside the [`File`](https://developer.mozilla.org/en-US/docs/Web/API/File).
+ * @param fileName The name of the file.
+ * @param options An options object containing optional attributes for the file.
+ */
+ constructor(fileBits: ReadonlyArray<string | NodeJS.ArrayBufferView | Blob>, fileName: string, options?: FilePropertyBag)
+
+ /**
+ * Name of the file referenced by the File object.
+ */
+ readonly name: string
+
+ /**
+ * The last modified date of the file as the number of milliseconds since the Unix epoch (January 1, 1970 at midnight). Files without a known last modified date return the current date.
+ */
+ readonly lastModified: number
+
+ readonly [Symbol.toStringTag]: string
+}
diff --git a/types/filereader.d.ts b/types/filereader.d.ts
new file mode 100644
index 0000000..f05d231
--- /dev/null
+++ b/types/filereader.d.ts
@@ -0,0 +1,54 @@
+/// <reference types="node" />
+
+import { Blob } from 'buffer'
+import { DOMException, Event, EventInit, EventTarget } from './patch'
+
+export declare class FileReader {
+ __proto__: EventTarget & FileReader
+
+ constructor ()
+
+ readAsArrayBuffer (blob: Blob): void
+ readAsBinaryString (blob: Blob): void
+ readAsText (blob: Blob, encoding?: string): void
+ readAsDataURL (blob: Blob): void
+
+ abort (): void
+
+ static readonly EMPTY = 0
+ static readonly LOADING = 1
+ static readonly DONE = 2
+
+ readonly EMPTY = 0
+ readonly LOADING = 1
+ readonly DONE = 2
+
+ readonly readyState: number
+
+ readonly result: string | ArrayBuffer | null
+
+ readonly error: DOMException | null
+
+ onloadstart: null | ((this: FileReader, event: ProgressEvent) => void)
+ onprogress: null | ((this: FileReader, event: ProgressEvent) => void)
+ onload: null | ((this: FileReader, event: ProgressEvent) => void)
+ onabort: null | ((this: FileReader, event: ProgressEvent) => void)
+ onerror: null | ((this: FileReader, event: ProgressEvent) => void)
+ onloadend: null | ((this: FileReader, event: ProgressEvent) => void)
+}
+
+export interface ProgressEventInit extends EventInit {
+ lengthComputable?: boolean
+ loaded?: number
+ total?: number
+}
+
+export declare class ProgressEvent {
+ __proto__: Event & ProgressEvent
+
+ constructor (type: string, eventInitDict?: ProgressEventInit)
+
+ readonly lengthComputable: boolean
+ readonly loaded: number
+ readonly total: number
+}
diff --git a/types/formdata.d.ts b/types/formdata.d.ts
new file mode 100644
index 0000000..df29a57
--- /dev/null
+++ b/types/formdata.d.ts
@@ -0,0 +1,108 @@
+// Based on https://github.com/octet-stream/form-data/blob/2d0f0dc371517444ce1f22cdde13f51995d0953a/lib/FormData.ts (MIT)
+/// <reference types="node" />
+
+import { File } from './file'
+import { SpecIterator, SpecIterableIterator } from './fetch'
+
+/**
+ * A `string` or `File` that represents a single value from a set of `FormData` key-value pairs.
+ */
+declare type FormDataEntryValue = string | File
+
+/**
+ * Provides a way to easily construct a set of key/value pairs representing form fields and their values, which can then be easily sent using fetch().
+ */
+export declare class FormData {
+ /**
+ * Appends a new value onto an existing key inside a FormData object,
+ * or adds the key if it does not already exist.
+ *
+ * The difference between `set()` and `append()` is that if the specified key already exists, `set()` will overwrite all existing values with the new one, whereas `append()` will append the new value onto the end of the existing set of values.
+ *
+ * @param name The name of the field whose data is contained in `value`.
+ * @param value The field's value. This can be [`Blob`](https://developer.mozilla.org/en-US/docs/Web/API/Blob)
+ or [`File`](https://developer.mozilla.org/en-US/docs/Web/API/File). If none of these are specified the value is converted to a string.
+ * @param fileName The filename reported to the server, when a Blob or File is passed as the second parameter. The default filename for Blob objects is "blob". The default filename for File objects is the file's filename.
+ */
+ append(name: string, value: unknown, fileName?: string): void
+
+ /**
+ * Set a new value for an existing key inside FormData,
+ * or add the new field if it does not already exist.
+ *
+ * @param name The name of the field whose data is contained in `value`.
+ * @param value The field's value. This can be [`Blob`](https://developer.mozilla.org/en-US/docs/Web/API/Blob)
+ or [`File`](https://developer.mozilla.org/en-US/docs/Web/API/File). If none of these are specified the value is converted to a string.
+ * @param fileName The filename reported to the server, when a Blob or File is passed as the second parameter. The default filename for Blob objects is "blob". The default filename for File objects is the file's filename.
+ *
+ */
+ set(name: string, value: unknown, fileName?: string): void
+
+ /**
+ * Returns the first value associated with a given key from within a `FormData` object.
+ * If you expect multiple values and want all of them, use the `getAll()` method instead.
+ *
+ * @param {string} name A name of the value you want to retrieve.
+ *
+ * @returns A `FormDataEntryValue` containing the value. If the key doesn't exist, the method returns null.
+ */
+ get(name: string): FormDataEntryValue | null
+
+ /**
+ * Returns all the values associated with a given key from within a `FormData` object.
+ *
+ * @param {string} name A name of the value you want to retrieve.
+ *
+ * @returns An array of `FormDataEntryValue` whose key matches the value passed in the `name` parameter. If the key doesn't exist, the method returns an empty list.
+ */
+ getAll(name: string): FormDataEntryValue[]
+
+ /**
+ * Returns a boolean stating whether a `FormData` object contains a certain key.
+ *
+ * @param name A string representing the name of the key you want to test for.
+ *
+ * @return A boolean value.
+ */
+ has(name: string): boolean
+
+ /**
+ * Deletes a key and its value(s) from a `FormData` object.
+ *
+ * @param name The name of the key you want to delete.
+ */
+ delete(name: string): void
+
+ /**
+ * Executes given callback function for each field of the FormData instance
+ */
+ forEach: (
+ callbackfn: (value: FormDataEntryValue, key: string, iterable: FormData) => void,
+ thisArg?: unknown
+ ) => void
+
+ /**
+ * Returns an [`iterator`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols) allowing to go through all keys contained in this `FormData` object.
+ * Each key is a `string`.
+ */
+ keys: () => SpecIterableIterator<string>
+
+ /**
+ * Returns an [`iterator`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols) allowing to go through all values contained in this object `FormData` object.
+ * Each value is a [`FormDataValue`](https://developer.mozilla.org/en-US/docs/Web/API/FormDataEntryValue).
+ */
+ values: () => SpecIterableIterator<FormDataEntryValue>
+
+ /**
+ * Returns an [`iterator`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols) allowing to go through the `FormData` key/value pairs.
+ * The key of each pair is a string; the value is a [`FormDataValue`](https://developer.mozilla.org/en-US/docs/Web/API/FormDataEntryValue).
+ */
+ entries: () => SpecIterableIterator<[string, FormDataEntryValue]>
+
+ /**
+ * An alias for FormData#entries()
+ */
+ [Symbol.iterator]: () => SpecIterableIterator<[string, FormDataEntryValue]>
+
+ readonly [Symbol.toStringTag]: string
+}
diff --git a/types/global-dispatcher.d.ts b/types/global-dispatcher.d.ts
new file mode 100644
index 0000000..728f95c
--- /dev/null
+++ b/types/global-dispatcher.d.ts
@@ -0,0 +1,9 @@
+import Dispatcher from "./dispatcher";
+
+export {
+ getGlobalDispatcher,
+ setGlobalDispatcher
+}
+
+declare function setGlobalDispatcher<DispatcherImplementation extends Dispatcher>(dispatcher: DispatcherImplementation): void;
+declare function getGlobalDispatcher(): Dispatcher;
diff --git a/types/global-origin.d.ts b/types/global-origin.d.ts
new file mode 100644
index 0000000..322542d
--- /dev/null
+++ b/types/global-origin.d.ts
@@ -0,0 +1,7 @@
+export {
+ setGlobalOrigin,
+ getGlobalOrigin
+}
+
+declare function setGlobalOrigin(origin: string | URL | undefined): void;
+declare function getGlobalOrigin(): URL | undefined; \ No newline at end of file
diff --git a/types/handlers.d.ts b/types/handlers.d.ts
new file mode 100644
index 0000000..eb4f5a9
--- /dev/null
+++ b/types/handlers.d.ts
@@ -0,0 +1,9 @@
+import Dispatcher from "./dispatcher";
+
+export declare class RedirectHandler implements Dispatcher.DispatchHandlers{
+ constructor (dispatch: Dispatcher, maxRedirections: number, opts: Dispatcher.DispatchOptions, handler: Dispatcher.DispatchHandlers)
+}
+
+export declare class DecoratorHandler implements Dispatcher.DispatchHandlers{
+ constructor (handler: Dispatcher.DispatchHandlers)
+}
diff --git a/types/header.d.ts b/types/header.d.ts
new file mode 100644
index 0000000..bfdb329
--- /dev/null
+++ b/types/header.d.ts
@@ -0,0 +1,4 @@
+/**
+ * The header type declaration of `undici`.
+ */
+export type IncomingHttpHeaders = Record<string, string | string[] | undefined>;
diff --git a/types/index.d.ts b/types/index.d.ts
new file mode 100644
index 0000000..0ea8bdc
--- /dev/null
+++ b/types/index.d.ts
@@ -0,0 +1,65 @@
+import Dispatcher from'./dispatcher'
+import { setGlobalDispatcher, getGlobalDispatcher } from './global-dispatcher'
+import { setGlobalOrigin, getGlobalOrigin } from './global-origin'
+import Pool from'./pool'
+import { RedirectHandler, DecoratorHandler } from './handlers'
+
+import BalancedPool from './balanced-pool'
+import Client from'./client'
+import buildConnector from'./connector'
+import errors from'./errors'
+import Agent from'./agent'
+import MockClient from'./mock-client'
+import MockPool from'./mock-pool'
+import MockAgent from'./mock-agent'
+import mockErrors from'./mock-errors'
+import ProxyAgent from'./proxy-agent'
+import RetryHandler from'./retry-handler'
+import { request, pipeline, stream, connect, upgrade } from './api'
+
+export * from './cookies'
+export * from './fetch'
+export * from './file'
+export * from './filereader'
+export * from './formdata'
+export * from './diagnostics-channel'
+export * from './websocket'
+export * from './content-type'
+export * from './cache'
+export { Interceptable } from './mock-interceptor'
+
+export { Dispatcher, BalancedPool, Pool, Client, buildConnector, errors, Agent, request, stream, pipeline, connect, upgrade, setGlobalDispatcher, getGlobalDispatcher, setGlobalOrigin, getGlobalOrigin, MockClient, MockPool, MockAgent, mockErrors, ProxyAgent, RedirectHandler, DecoratorHandler, RetryHandler }
+export default Undici
+
+declare namespace Undici {
+ var Dispatcher: typeof import('./dispatcher').default
+ var Pool: typeof import('./pool').default;
+ var RedirectHandler: typeof import ('./handlers').RedirectHandler
+ var DecoratorHandler: typeof import ('./handlers').DecoratorHandler
+ var RetryHandler: typeof import ('./retry-handler').default
+ var createRedirectInterceptor: typeof import ('./interceptors').createRedirectInterceptor
+ var BalancedPool: typeof import('./balanced-pool').default;
+ var Client: typeof import('./client').default;
+ var buildConnector: typeof import('./connector').default;
+ var errors: typeof import('./errors').default;
+ var Agent: typeof import('./agent').default;
+ var setGlobalDispatcher: typeof import('./global-dispatcher').setGlobalDispatcher;
+ var getGlobalDispatcher: typeof import('./global-dispatcher').getGlobalDispatcher;
+ var request: typeof import('./api').request;
+ var stream: typeof import('./api').stream;
+ var pipeline: typeof import('./api').pipeline;
+ var connect: typeof import('./api').connect;
+ var upgrade: typeof import('./api').upgrade;
+ var MockClient: typeof import('./mock-client').default;
+ var MockPool: typeof import('./mock-pool').default;
+ var MockAgent: typeof import('./mock-agent').default;
+ var mockErrors: typeof import('./mock-errors').default;
+ var fetch: typeof import('./fetch').fetch;
+ var Headers: typeof import('./fetch').Headers;
+ var Response: typeof import('./fetch').Response;
+ var Request: typeof import('./fetch').Request;
+ var FormData: typeof import('./formdata').FormData;
+ var File: typeof import('./file').File;
+ var FileReader: typeof import('./filereader').FileReader;
+ var caches: typeof import('./cache').caches;
+}
diff --git a/types/interceptors.d.ts b/types/interceptors.d.ts
new file mode 100644
index 0000000..047ac17
--- /dev/null
+++ b/types/interceptors.d.ts
@@ -0,0 +1,5 @@
+import Dispatcher from "./dispatcher";
+
+type RedirectInterceptorOpts = { maxRedirections?: number }
+
+export declare function createRedirectInterceptor (opts: RedirectInterceptorOpts): Dispatcher.DispatchInterceptor
diff --git a/types/mock-agent.d.ts b/types/mock-agent.d.ts
new file mode 100644
index 0000000..98cd645
--- /dev/null
+++ b/types/mock-agent.d.ts
@@ -0,0 +1,50 @@
+import Agent from './agent'
+import Dispatcher from './dispatcher'
+import { Interceptable, MockInterceptor } from './mock-interceptor'
+import MockDispatch = MockInterceptor.MockDispatch;
+
+export default MockAgent
+
+interface PendingInterceptor extends MockDispatch {
+ origin: string;
+}
+
+/** A mocked Agent class that implements the Agent API. It allows one to intercept HTTP requests made through undici and return mocked responses instead. */
+declare class MockAgent<TMockAgentOptions extends MockAgent.Options = MockAgent.Options> extends Dispatcher {
+ constructor(options?: MockAgent.Options)
+ /** Creates and retrieves mock Dispatcher instances which can then be used to intercept HTTP requests. If the number of connections on the mock agent is set to 1, a MockClient instance is returned. Otherwise a MockPool instance is returned. */
+ get<TInterceptable extends Interceptable>(origin: string): TInterceptable;
+ get<TInterceptable extends Interceptable>(origin: RegExp): TInterceptable;
+ get<TInterceptable extends Interceptable>(origin: ((origin: string) => boolean)): TInterceptable;
+ /** Dispatches a mocked request. */
+ dispatch(options: Agent.DispatchOptions, handler: Dispatcher.DispatchHandlers): boolean;
+ /** Closes the mock agent and waits for registered mock pools and clients to also close before resolving. */
+ close(): Promise<void>;
+ /** Disables mocking in MockAgent. */
+ deactivate(): void;
+ /** Enables mocking in a MockAgent instance. When instantiated, a MockAgent is automatically activated. Therefore, this method is only effective after `MockAgent.deactivate` has been called. */
+ activate(): void;
+ /** Define host matchers so only matching requests that aren't intercepted by the mock dispatchers will be attempted. */
+ enableNetConnect(): void;
+ enableNetConnect(host: string): void;
+ enableNetConnect(host: RegExp): void;
+ enableNetConnect(host: ((host: string) => boolean)): void;
+ /** Causes all requests to throw when requests are not matched in a MockAgent intercept. */
+ disableNetConnect(): void;
+ pendingInterceptors(): PendingInterceptor[];
+ assertNoPendingInterceptors(options?: {
+ pendingInterceptorsFormatter?: PendingInterceptorsFormatter;
+ }): void;
+}
+
+interface PendingInterceptorsFormatter {
+ format(pendingInterceptors: readonly PendingInterceptor[]): string;
+}
+
+declare namespace MockAgent {
+ /** MockAgent options. */
+ export interface Options extends Agent.Options {
+ /** A custom agent to be encapsulated by the MockAgent. */
+ agent?: Agent;
+ }
+}
diff --git a/types/mock-client.d.ts b/types/mock-client.d.ts
new file mode 100644
index 0000000..51d008c
--- /dev/null
+++ b/types/mock-client.d.ts
@@ -0,0 +1,25 @@
+import Client from './client'
+import Dispatcher from './dispatcher'
+import MockAgent from './mock-agent'
+import { MockInterceptor, Interceptable } from './mock-interceptor'
+
+export default MockClient
+
+/** MockClient extends the Client API and allows one to mock requests. */
+declare class MockClient extends Client implements Interceptable {
+ constructor(origin: string, options: MockClient.Options);
+ /** Intercepts any matching requests that use the same origin as this mock client. */
+ intercept(options: MockInterceptor.Options): MockInterceptor;
+ /** Dispatches a mocked request. */
+ dispatch(options: Dispatcher.DispatchOptions, handlers: Dispatcher.DispatchHandlers): boolean;
+ /** Closes the mock client and gracefully waits for enqueued requests to complete. */
+ close(): Promise<void>;
+}
+
+declare namespace MockClient {
+ /** MockClient options. */
+ export interface Options extends Client.Options {
+ /** The agent to associate this MockClient with. */
+ agent: MockAgent;
+ }
+}
diff --git a/types/mock-errors.d.ts b/types/mock-errors.d.ts
new file mode 100644
index 0000000..3d9e727
--- /dev/null
+++ b/types/mock-errors.d.ts
@@ -0,0 +1,12 @@
+import Errors from './errors'
+
+export default MockErrors
+
+declare namespace MockErrors {
+ /** The request does not match any registered mock dispatches. */
+ export class MockNotMatchedError extends Errors.UndiciError {
+ constructor(message?: string);
+ name: 'MockNotMatchedError';
+ code: 'UND_MOCK_ERR_MOCK_NOT_MATCHED';
+ }
+}
diff --git a/types/mock-interceptor.d.ts b/types/mock-interceptor.d.ts
new file mode 100644
index 0000000..6b3961c
--- /dev/null
+++ b/types/mock-interceptor.d.ts
@@ -0,0 +1,93 @@
+import { IncomingHttpHeaders } from './header'
+import Dispatcher from './dispatcher';
+import { BodyInit, Headers } from './fetch'
+
+export {
+ Interceptable,
+ MockInterceptor,
+ MockScope
+}
+
+/** The scope associated with a mock dispatch. */
+declare class MockScope<TData extends object = object> {
+ constructor(mockDispatch: MockInterceptor.MockDispatch<TData>);
+ /** Delay a reply by a set amount of time in ms. */
+ delay(waitInMs: number): MockScope<TData>;
+ /** Persist the defined mock data for the associated reply. It will return the defined mock data indefinitely. */
+ persist(): MockScope<TData>;
+ /** Define a reply for a set amount of matching requests. */
+ times(repeatTimes: number): MockScope<TData>;
+}
+
+/** The interceptor for a Mock. */
+declare class MockInterceptor {
+ constructor(options: MockInterceptor.Options, mockDispatches: MockInterceptor.MockDispatch[]);
+ /** Mock an undici request with the defined reply. */
+ reply<TData extends object = object>(replyOptionsCallback: MockInterceptor.MockReplyOptionsCallback<TData>): MockScope<TData>;
+ reply<TData extends object = object>(
+ statusCode: number,
+ data?: TData | Buffer | string | MockInterceptor.MockResponseDataHandler<TData>,
+ responseOptions?: MockInterceptor.MockResponseOptions
+ ): MockScope<TData>;
+ /** Mock an undici request by throwing the defined reply error. */
+ replyWithError<TError extends Error = Error>(error: TError): MockScope;
+ /** Set default reply headers on the interceptor for subsequent mocked replies. */
+ defaultReplyHeaders(headers: IncomingHttpHeaders): MockInterceptor;
+ /** Set default reply trailers on the interceptor for subsequent mocked replies. */
+ defaultReplyTrailers(trailers: Record<string, string>): MockInterceptor;
+ /** Set automatically calculated content-length header on subsequent mocked replies. */
+ replyContentLength(): MockInterceptor;
+}
+
+declare namespace MockInterceptor {
+ /** MockInterceptor options. */
+ export interface Options {
+ /** Path to intercept on. */
+ path: string | RegExp | ((path: string) => boolean);
+ /** Method to intercept on. Defaults to GET. */
+ method?: string | RegExp | ((method: string) => boolean);
+ /** Body to intercept on. */
+ body?: string | RegExp | ((body: string) => boolean);
+ /** Headers to intercept on. */
+ headers?: Record<string, string | RegExp | ((body: string) => boolean)> | ((headers: Record<string, string>) => boolean);
+ /** Query params to intercept on */
+ query?: Record<string, any>;
+ }
+ export interface MockDispatch<TData extends object = object, TError extends Error = Error> extends Options {
+ times: number | null;
+ persist: boolean;
+ consumed: boolean;
+ data: MockDispatchData<TData, TError>;
+ }
+ export interface MockDispatchData<TData extends object = object, TError extends Error = Error> extends MockResponseOptions {
+ error: TError | null;
+ statusCode?: number;
+ data?: TData | string;
+ }
+ export interface MockResponseOptions {
+ headers?: IncomingHttpHeaders;
+ trailers?: Record<string, string>;
+ }
+
+ export interface MockResponseCallbackOptions {
+ path: string;
+ origin: string;
+ method: string;
+ body?: BodyInit | Dispatcher.DispatchOptions['body'];
+ headers: Headers | Record<string, string>;
+ maxRedirections: number;
+ }
+
+ export type MockResponseDataHandler<TData extends object = object> = (
+ opts: MockResponseCallbackOptions
+ ) => TData | Buffer | string;
+
+ export type MockReplyOptionsCallback<TData extends object = object> = (
+ opts: MockResponseCallbackOptions
+ ) => { statusCode: number, data?: TData | Buffer | string, responseOptions?: MockResponseOptions }
+}
+
+interface Interceptable extends Dispatcher {
+ /** Intercepts any matching requests that use the same origin as this mock client. */
+ intercept(options: MockInterceptor.Options): MockInterceptor;
+}
diff --git a/types/mock-pool.d.ts b/types/mock-pool.d.ts
new file mode 100644
index 0000000..39e709a
--- /dev/null
+++ b/types/mock-pool.d.ts
@@ -0,0 +1,25 @@
+import Pool from './pool'
+import MockAgent from './mock-agent'
+import { Interceptable, MockInterceptor } from './mock-interceptor'
+import Dispatcher from './dispatcher'
+
+export default MockPool
+
+/** MockPool extends the Pool API and allows one to mock requests. */
+declare class MockPool extends Pool implements Interceptable {
+ constructor(origin: string, options: MockPool.Options);
+ /** Intercepts any matching requests that use the same origin as this mock pool. */
+ intercept(options: MockInterceptor.Options): MockInterceptor;
+ /** Dispatches a mocked request. */
+ dispatch(options: Dispatcher.DispatchOptions, handlers: Dispatcher.DispatchHandlers): boolean;
+ /** Closes the mock pool and gracefully waits for enqueued requests to complete. */
+ close(): Promise<void>;
+}
+
+declare namespace MockPool {
+ /** MockPool options. */
+ export interface Options extends Pool.Options {
+ /** The agent to associate this MockPool with. */
+ agent: MockAgent;
+ }
+}
diff --git a/types/patch.d.ts b/types/patch.d.ts
new file mode 100644
index 0000000..3871acf
--- /dev/null
+++ b/types/patch.d.ts
@@ -0,0 +1,71 @@
+/// <reference types="node" />
+
+// See https://github.com/nodejs/undici/issues/1740
+
+export type DOMException = typeof globalThis extends { DOMException: infer T }
+ ? T
+ : any
+
+export type EventTarget = typeof globalThis extends { EventTarget: infer T }
+ ? T
+ : {
+ addEventListener(
+ type: string,
+ listener: any,
+ options?: any,
+ ): void
+ dispatchEvent(event: Event): boolean
+ removeEventListener(
+ type: string,
+ listener: any,
+ options?: any | boolean,
+ ): void
+ }
+
+export type Event = typeof globalThis extends { Event: infer T }
+ ? T
+ : {
+ readonly bubbles: boolean
+ cancelBubble: () => void
+ readonly cancelable: boolean
+ readonly composed: boolean
+ composedPath(): [EventTarget?]
+ readonly currentTarget: EventTarget | null
+ readonly defaultPrevented: boolean
+ readonly eventPhase: 0 | 2
+ readonly isTrusted: boolean
+ preventDefault(): void
+ returnValue: boolean
+ readonly srcElement: EventTarget | null
+ stopImmediatePropagation(): void
+ stopPropagation(): void
+ readonly target: EventTarget | null
+ readonly timeStamp: number
+ readonly type: string
+ }
+
+export interface EventInit {
+ bubbles?: boolean
+ cancelable?: boolean
+ composed?: boolean
+}
+
+export interface EventListenerOptions {
+ capture?: boolean
+}
+
+export interface AddEventListenerOptions extends EventListenerOptions {
+ once?: boolean
+ passive?: boolean
+ signal?: AbortSignal
+}
+
+export type EventListenerOrEventListenerObject = EventListener | EventListenerObject
+
+export interface EventListenerObject {
+ handleEvent (object: Event): void
+}
+
+export interface EventListener {
+ (evt: Event): void
+}
diff --git a/types/pool-stats.d.ts b/types/pool-stats.d.ts
new file mode 100644
index 0000000..8b6d2bf
--- /dev/null
+++ b/types/pool-stats.d.ts
@@ -0,0 +1,19 @@
+import Pool from "./pool"
+
+export default PoolStats
+
+declare class PoolStats {
+ constructor(pool: Pool);
+ /** Number of open socket connections in this pool. */
+ connected: number;
+ /** Number of open socket connections in this pool that do not have an active request. */
+ free: number;
+ /** Number of pending requests across all clients in this pool. */
+ pending: number;
+ /** Number of queued requests across all clients in this pool. */
+ queued: number;
+ /** Number of currently active requests across all clients in this pool. */
+ running: number;
+ /** Number of active, pending, or queued requests across all clients in this pool. */
+ size: number;
+}
diff --git a/types/pool.d.ts b/types/pool.d.ts
new file mode 100644
index 0000000..7747d48
--- /dev/null
+++ b/types/pool.d.ts
@@ -0,0 +1,28 @@
+import Client from './client'
+import TPoolStats from './pool-stats'
+import { URL } from 'url'
+import Dispatcher from "./dispatcher";
+
+export default Pool
+
+declare class Pool extends Dispatcher {
+ constructor(url: string | URL, options?: Pool.Options)
+ /** `true` after `pool.close()` has been called. */
+ closed: boolean;
+ /** `true` after `pool.destroyed()` has been called or `pool.close()` has been called and the pool shutdown has completed. */
+ destroyed: boolean;
+ /** Aggregate stats for a Pool. */
+ readonly stats: TPoolStats;
+}
+
+declare namespace Pool {
+ export type PoolStats = TPoolStats;
+ export interface Options extends Client.Options {
+ /** Default: `(origin, opts) => new Client(origin, opts)`. */
+ factory?(origin: URL, opts: object): Dispatcher;
+ /** The max number of clients to create. `null` if no limit. Default `null`. */
+ connections?: number | null;
+
+ interceptors?: { Pool?: readonly Dispatcher.DispatchInterceptor[] } & Client.Options["interceptors"]
+ }
+}
diff --git a/types/proxy-agent.d.ts b/types/proxy-agent.d.ts
new file mode 100644
index 0000000..96b2638
--- /dev/null
+++ b/types/proxy-agent.d.ts
@@ -0,0 +1,30 @@
+import Agent from './agent'
+import buildConnector from './connector';
+import Client from './client'
+import Dispatcher from './dispatcher'
+import { IncomingHttpHeaders } from './header'
+import Pool from './pool'
+
+export default ProxyAgent
+
+declare class ProxyAgent extends Dispatcher {
+ constructor(options: ProxyAgent.Options | string)
+
+ dispatch(options: Agent.DispatchOptions, handler: Dispatcher.DispatchHandlers): boolean;
+ close(): Promise<void>;
+}
+
+declare namespace ProxyAgent {
+ export interface Options extends Agent.Options {
+ uri: string;
+ /**
+ * @deprecated use opts.token
+ */
+ auth?: string;
+ token?: string;
+ headers?: IncomingHttpHeaders;
+ requestTls?: buildConnector.BuildOptions;
+ proxyTls?: buildConnector.BuildOptions;
+ clientFactory?(origin: URL, opts: object): Dispatcher;
+ }
+}
diff --git a/types/readable.d.ts b/types/readable.d.ts
new file mode 100644
index 0000000..4549a8c
--- /dev/null
+++ b/types/readable.d.ts
@@ -0,0 +1,61 @@
+import { Readable } from "stream";
+import { Blob } from 'buffer'
+
+export default BodyReadable
+
+declare class BodyReadable extends Readable {
+ constructor(
+ resume?: (this: Readable, size: number) => void | null,
+ abort?: () => void | null,
+ contentType?: string
+ )
+
+ /** Consumes and returns the body as a string
+ * https://fetch.spec.whatwg.org/#dom-body-text
+ */
+ text(): Promise<string>
+
+ /** Consumes and returns the body as a JavaScript Object
+ * https://fetch.spec.whatwg.org/#dom-body-json
+ */
+ json(): Promise<unknown>
+
+ /** Consumes and returns the body as a Blob
+ * https://fetch.spec.whatwg.org/#dom-body-blob
+ */
+ blob(): Promise<Blob>
+
+ /** Consumes and returns the body as an ArrayBuffer
+ * https://fetch.spec.whatwg.org/#dom-body-arraybuffer
+ */
+ arrayBuffer(): Promise<ArrayBuffer>
+
+ /** Not implemented
+ *
+ * https://fetch.spec.whatwg.org/#dom-body-formdata
+ */
+ formData(): Promise<never>
+
+ /** Returns true if the body is not null and the body has been consumed
+ *
+ * Otherwise, returns false
+ *
+ * https://fetch.spec.whatwg.org/#dom-body-bodyused
+ */
+ readonly bodyUsed: boolean
+
+ /** Throws on node 16.6.0
+ *
+ * If body is null, it should return null as the body
+ *
+ * If body is not null, should return the body as a ReadableStream
+ *
+ * https://fetch.spec.whatwg.org/#dom-body-body
+ */
+ readonly body: never | undefined
+
+ /** Dumps the response body by reading `limit` number of bytes.
+ * @param opts.limit Number of bytes to read (optional) - Default: 262144
+ */
+ dump(opts?: { limit: number }): Promise<void>
+}
diff --git a/types/retry-handler.d.ts b/types/retry-handler.d.ts
new file mode 100644
index 0000000..0528eb4
--- /dev/null
+++ b/types/retry-handler.d.ts
@@ -0,0 +1,116 @@
+import Dispatcher from "./dispatcher";
+
+export default RetryHandler;
+
+declare class RetryHandler implements Dispatcher.DispatchHandlers {
+ constructor(
+ options: Dispatcher.DispatchOptions & {
+ retryOptions?: RetryHandler.RetryOptions;
+ },
+ retryHandlers: RetryHandler.RetryHandlers
+ );
+}
+
+declare namespace RetryHandler {
+ export type RetryState = { counter: number; currentTimeout: number };
+
+ export type RetryContext = {
+ state: RetryState;
+ opts: Dispatcher.DispatchOptions & {
+ retryOptions?: RetryHandler.RetryOptions;
+ };
+ }
+
+ export type OnRetryCallback = (result?: Error | null) => void;
+
+ export type RetryCallback = (
+ err: Error,
+ context: {
+ state: RetryState;
+ opts: Dispatcher.DispatchOptions & {
+ retryOptions?: RetryHandler.RetryOptions;
+ };
+ },
+ callback: OnRetryCallback
+ ) => number | null;
+
+ export interface RetryOptions {
+ /**
+ * Callback to be invoked on every retry iteration.
+ * It receives the error, current state of the retry object and the options object
+ * passed when instantiating the retry handler.
+ *
+ * @type {RetryCallback}
+ * @memberof RetryOptions
+ */
+ retry?: RetryCallback;
+ /**
+ * Maximum number of retries to allow.
+ *
+ * @type {number}
+ * @memberof RetryOptions
+ * @default 5
+ */
+ maxRetries?: number;
+ /**
+ * Max number of milliseconds allow between retries
+ *
+ * @type {number}
+ * @memberof RetryOptions
+ * @default 30000
+ */
+ maxTimeout?: number;
+ /**
+ * Initial number of milliseconds to wait before retrying for the first time.
+ *
+ * @type {number}
+ * @memberof RetryOptions
+ * @default 500
+ */
+ minTimeout?: number;
+ /**
+ * Factior to multiply the timeout factor between retries.
+ *
+ * @type {number}
+ * @memberof RetryOptions
+ * @default 2
+ */
+ timeoutFactor?: number;
+ /**
+ * It enables to automatically infer timeout between retries based on the `Retry-After` header.
+ *
+ * @type {boolean}
+ * @memberof RetryOptions
+ * @default true
+ */
+ retryAfter?: boolean;
+ /**
+ * HTTP methods to retry.
+ *
+ * @type {Dispatcher.HttpMethod[]}
+ * @memberof RetryOptions
+ * @default ['GET', 'HEAD', 'OPTIONS', 'PUT', 'DELETE', 'TRACE'],
+ */
+ methods?: Dispatcher.HttpMethod[];
+ /**
+ * Error codes to be retried. e.g. `ECONNRESET`, `ENOTFOUND`, `ETIMEDOUT`, `ECONNREFUSED`, etc.
+ *
+ * @type {string[]}
+ * @default ['ECONNRESET','ECONNREFUSED','ENOTFOUND','ENETDOWN','ENETUNREACH','EHOSTDOWN','EHOSTUNREACH','EPIPE']
+ */
+ errorCodes?: string[];
+ /**
+ * HTTP status codes to be retried.
+ *
+ * @type {number[]}
+ * @memberof RetryOptions
+ * @default [500, 502, 503, 504, 429],
+ */
+ statusCodes?: number[];
+ }
+
+ export interface RetryHandlers {
+ dispatch: Dispatcher["dispatch"];
+ handler: Dispatcher.DispatchHandlers;
+ }
+}
diff --git a/types/webidl.d.ts b/types/webidl.d.ts
new file mode 100644
index 0000000..40cfe06
--- /dev/null
+++ b/types/webidl.d.ts
@@ -0,0 +1,220 @@
+// These types are not exported, and are only used internally
+
+/**
+ * Take in an unknown value and return one that is of type T
+ */
+type Converter<T> = (object: unknown) => T
+
+type SequenceConverter<T> = (object: unknown) => T[]
+
+type RecordConverter<K extends string, V> = (object: unknown) => Record<K, V>
+
+interface ConvertToIntOpts {
+ clamp?: boolean
+ enforceRange?: boolean
+}
+
+interface WebidlErrors {
+ exception (opts: { header: string, message: string }): TypeError
+ /**
+ * @description Throw an error when conversion from one type to another has failed
+ */
+ conversionFailed (opts: {
+ prefix: string
+ argument: string
+ types: string[]
+ }): TypeError
+ /**
+ * @description Throw an error when an invalid argument is provided
+ */
+ invalidArgument (opts: {
+ prefix: string
+ value: string
+ type: string
+ }): TypeError
+}
+
+interface WebidlUtil {
+ /**
+ * @see https://tc39.es/ecma262/#sec-ecmascript-data-types-and-values
+ */
+ Type (object: unknown):
+ | 'Undefined'
+ | 'Boolean'
+ | 'String'
+ | 'Symbol'
+ | 'Number'
+ | 'BigInt'
+ | 'Null'
+ | 'Object'
+
+ /**
+ * @see https://webidl.spec.whatwg.org/#abstract-opdef-converttoint
+ */
+ ConvertToInt (
+ V: unknown,
+ bitLength: number,
+ signedness: 'signed' | 'unsigned',
+ opts?: ConvertToIntOpts
+ ): number
+
+ /**
+ * @see https://webidl.spec.whatwg.org/#abstract-opdef-converttoint
+ */
+ IntegerPart (N: number): number
+}
+
+interface WebidlConverters {
+ /**
+ * @see https://webidl.spec.whatwg.org/#es-DOMString
+ */
+ DOMString (V: unknown, opts?: {
+ legacyNullToEmptyString: boolean
+ }): string
+
+ /**
+ * @see https://webidl.spec.whatwg.org/#es-ByteString
+ */
+ ByteString (V: unknown): string
+
+ /**
+ * @see https://webidl.spec.whatwg.org/#es-USVString
+ */
+ USVString (V: unknown): string
+
+ /**
+ * @see https://webidl.spec.whatwg.org/#es-boolean
+ */
+ boolean (V: unknown): boolean
+
+ /**
+ * @see https://webidl.spec.whatwg.org/#es-any
+ */
+ any <Value>(V: Value): Value
+
+ /**
+ * @see https://webidl.spec.whatwg.org/#es-long-long
+ */
+ ['long long'] (V: unknown): number
+
+ /**
+ * @see https://webidl.spec.whatwg.org/#es-unsigned-long-long
+ */
+ ['unsigned long long'] (V: unknown): number
+
+ /**
+ * @see https://webidl.spec.whatwg.org/#es-unsigned-long
+ */
+ ['unsigned long'] (V: unknown): number
+
+ /**
+ * @see https://webidl.spec.whatwg.org/#es-unsigned-short
+ */
+ ['unsigned short'] (V: unknown, opts?: ConvertToIntOpts): number
+
+ /**
+ * @see https://webidl.spec.whatwg.org/#idl-ArrayBuffer
+ */
+ ArrayBuffer (V: unknown): ArrayBufferLike
+ ArrayBuffer (V: unknown, opts: { allowShared: false }): ArrayBuffer
+
+ /**
+ * @see https://webidl.spec.whatwg.org/#es-buffer-source-types
+ */
+ TypedArray (
+ V: unknown,
+ TypedArray: NodeJS.TypedArray | ArrayBufferLike
+ ): NodeJS.TypedArray | ArrayBufferLike
+ TypedArray (
+ V: unknown,
+ TypedArray: NodeJS.TypedArray | ArrayBufferLike,
+ opts?: { allowShared: false }
+ ): NodeJS.TypedArray | ArrayBuffer
+
+ /**
+ * @see https://webidl.spec.whatwg.org/#es-buffer-source-types
+ */
+ DataView (V: unknown, opts?: { allowShared: boolean }): DataView
+
+ /**
+ * @see https://webidl.spec.whatwg.org/#BufferSource
+ */
+ BufferSource (
+ V: unknown,
+ opts?: { allowShared: boolean }
+ ): NodeJS.TypedArray | ArrayBufferLike | DataView
+
+ ['sequence<ByteString>']: SequenceConverter<string>
+
+ ['sequence<sequence<ByteString>>']: SequenceConverter<string[]>
+
+ ['record<ByteString, ByteString>']: RecordConverter<string, string>
+
+ [Key: string]: (...args: any[]) => unknown
+}
+
+export interface Webidl {
+ errors: WebidlErrors
+ util: WebidlUtil
+ converters: WebidlConverters
+
+ /**
+ * @description Performs a brand-check on {@param V} to ensure it is a
+ * {@param cls} object.
+ */
+ brandCheck <Interface>(V: unknown, cls: Interface, opts?: { strict?: boolean }): asserts V is Interface
+
+ /**
+ * @see https://webidl.spec.whatwg.org/#es-sequence
+ * @description Convert a value, V, to a WebIDL sequence type.
+ */
+ sequenceConverter <Type>(C: Converter<Type>): SequenceConverter<Type>
+
+ illegalConstructor (): never
+
+ /**
+ * @see https://webidl.spec.whatwg.org/#es-to-record
+ * @description Convert a value, V, to a WebIDL record type.
+ */
+ recordConverter <K extends string, V>(
+ keyConverter: Converter<K>,
+ valueConverter: Converter<V>
+ ): RecordConverter<K, V>
+
+ /**
+ * Similar to {@link Webidl.brandCheck} but allows skipping the check if third party
+ * interfaces are allowed.
+ */
+ interfaceConverter <Interface>(cls: Interface): (
+ V: unknown,
+ opts?: { strict: boolean }
+ ) => asserts V is typeof cls
+
+ // TODO(@KhafraDev): a type could likely be implemented that can infer the return type
+ // from the converters given?
+ /**
+ * Converts a value, V, to a WebIDL dictionary types. Allows limiting which keys are
+ * allowed, values allowed, optional and required keys. Auto converts the value to
+ * a type given a converter.
+ */
+ dictionaryConverter (converters: {
+ key: string,
+ defaultValue?: unknown,
+ required?: boolean,
+ converter: (...args: unknown[]) => unknown,
+ allowedValues?: unknown[]
+ }[]): (V: unknown) => Record<string, unknown>
+
+ /**
+ * @see https://webidl.spec.whatwg.org/#idl-nullable-type
+ * @description allows a type, V, to be null
+ */
+ nullableConverter <T>(
+ converter: Converter<T>
+ ): (V: unknown) => ReturnType<typeof converter> | null
+
+ argumentLengthCheck (args: { length: number }, min: number, context: {
+ header: string
+ message?: string
+ }): void
+}
diff --git a/types/websocket.d.ts b/types/websocket.d.ts
new file mode 100644
index 0000000..15a357d
--- /dev/null
+++ b/types/websocket.d.ts
@@ -0,0 +1,131 @@
+/// <reference types="node" />
+
+import type { Blob } from 'buffer'
+import type { MessagePort } from 'worker_threads'
+import {
+ EventTarget,
+ Event,
+ EventInit,
+ EventListenerOptions,
+ AddEventListenerOptions,
+ EventListenerOrEventListenerObject
+} from './patch'
+import Dispatcher from './dispatcher'
+import { HeadersInit } from './fetch'
+
+export type BinaryType = 'blob' | 'arraybuffer'
+
+interface WebSocketEventMap {
+ close: CloseEvent
+ error: Event
+ message: MessageEvent
+ open: Event
+}
+
+interface WebSocket extends EventTarget {
+ binaryType: BinaryType
+
+ readonly bufferedAmount: number
+ readonly extensions: string
+
+ onclose: ((this: WebSocket, ev: WebSocketEventMap['close']) => any) | null
+ onerror: ((this: WebSocket, ev: WebSocketEventMap['error']) => any) | null
+ onmessage: ((this: WebSocket, ev: WebSocketEventMap['message']) => any) | null
+ onopen: ((this: WebSocket, ev: WebSocketEventMap['open']) => any) | null
+
+ readonly protocol: string
+ readonly readyState: number
+ readonly url: string
+
+ close(code?: number, reason?: string): void
+ send(data: string | ArrayBufferLike | Blob | ArrayBufferView): void
+
+ readonly CLOSED: number
+ readonly CLOSING: number
+ readonly CONNECTING: number
+ readonly OPEN: number
+
+ addEventListener<K extends keyof WebSocketEventMap>(
+ type: K,
+ listener: (this: WebSocket, ev: WebSocketEventMap[K]) => any,
+ options?: boolean | AddEventListenerOptions
+ ): void
+ addEventListener(
+ type: string,
+ listener: EventListenerOrEventListenerObject,
+ options?: boolean | AddEventListenerOptions
+ ): void
+ removeEventListener<K extends keyof WebSocketEventMap>(
+ type: K,
+ listener: (this: WebSocket, ev: WebSocketEventMap[K]) => any,
+ options?: boolean | EventListenerOptions
+ ): void
+ removeEventListener(
+ type: string,
+ listener: EventListenerOrEventListenerObject,
+ options?: boolean | EventListenerOptions
+ ): void
+}
+
+export declare const WebSocket: {
+ prototype: WebSocket
+ new (url: string | URL, protocols?: string | string[] | WebSocketInit): WebSocket
+ readonly CLOSED: number
+ readonly CLOSING: number
+ readonly CONNECTING: number
+ readonly OPEN: number
+}
+
+interface CloseEventInit extends EventInit {
+ code?: number
+ reason?: string
+ wasClean?: boolean
+}
+
+interface CloseEvent extends Event {
+ readonly code: number
+ readonly reason: string
+ readonly wasClean: boolean
+}
+
+export declare const CloseEvent: {
+ prototype: CloseEvent
+ new (type: string, eventInitDict?: CloseEventInit): CloseEvent
+}
+
+interface MessageEventInit<T = any> extends EventInit {
+ data?: T
+ lastEventId?: string
+ origin?: string
+ ports?: (typeof MessagePort)[]
+ source?: typeof MessagePort | null
+}
+
+interface MessageEvent<T = any> extends Event {
+ readonly data: T
+ readonly lastEventId: string
+ readonly origin: string
+ readonly ports: ReadonlyArray<typeof MessagePort>
+ readonly source: typeof MessagePort | null
+ initMessageEvent(
+ type: string,
+ bubbles?: boolean,
+ cancelable?: boolean,
+ data?: any,
+ origin?: string,
+ lastEventId?: string,
+ source?: typeof MessagePort | null,
+ ports?: (typeof MessagePort)[]
+ ): void;
+}
+
+export declare const MessageEvent: {
+ prototype: MessageEvent
+ new<T>(type: string, eventInitDict?: MessageEventInit<T>): MessageEvent<T>
+}
+
+interface WebSocketInit {
+ protocols?: string | string[],
+ dispatcher?: Dispatcher,
+ headers?: HeadersInit
+}